// ==UserScript==
// @name GLIF Tools
// @namespace http://tampermonkey.net/
// @version 1.5
// @description A set of usefull tools for GLIF.APP
// @author i12bp8
// @match https://glif.app/*
// @run-at document-start
// @grant GM_setValue
// @grant GM_getValue
// @grant unsafeWindow
// ==/UserScript==
(function() {
'use strict';
const originalFetch = unsafeWindow.fetch;
// Replace the global fetch
unsafeWindow.fetch = async (...args) => {
const [url, options] = args;
if (url.includes('/api/run-glif')) {
const modifiedOptions = {...options};
const body = JSON.parse(modifiedOptions.body);
const isPrivate = GM_getValue('isPrivate', true);
// Set private/public mode
body.glifRunIsPublic = !isPrivate;
modifiedOptions.body = JSON.stringify(body);
const response = await originalFetch(url, modifiedOptions);
const clonedResponse = response.clone();
// Process the response stream
processStreamResponse(clonedResponse, isPrivate, body.inputs).catch(err => {
console.error('Error processing response:', err);
});
return response;
}
return originalFetch(...args);
};
function saveToHistory(entry) {
const history = GM_getValue('imageHistory', []);
// Check if this image is already in history to prevent duplicates
const isDuplicate = history.some(item =>
item.url === entry.url &&
item.timestamp === entry.timestamp
);
if (!isDuplicate) {
// Add new entry to the beginning of the array
history.unshift(entry);
// Keep only the last 100 entries to manage storage
if (history.length > 100) {
history.pop();
}
// Save updated history
GM_setValue('imageHistory', history);
console.log('Saved to history:', entry);
}
}
function displayHistoryPanel() {
let historyPanel = document.getElementById('historyPanel');
if (historyPanel) {
historyPanel.remove();
return;
}
const history = GM_getValue('imageHistory', []);
historyPanel = document.createElement('div');
historyPanel.id = 'historyPanel';
historyPanel.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 24px;
border-radius: 16px;
box-shadow: 0 8px 32px rgba(0,0,0,0.15);
z-index: 10001;
max-height: 85vh;
width: 85vw;
max-width: 1200px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 16px;
`;
// Create header
const header = document.createElement('div');
header.style.cssText = `
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 15px;
border-bottom: 1px solid #eee;
gap: 16px;
`;
const titleSection = document.createElement('div');
titleSection.style.cssText = `
display: flex;
align-items: center;
gap: 12px;
`;
const title = document.createElement('h2');
title.textContent = 'Image History';
title.style.cssText = `
margin: 0;
font-size: 24px;
font-weight: 600;
color: #1a1a1a;
`;
const filterContainer = document.createElement('div');
filterContainer.style.cssText = `
display: flex;
align-items: center;
gap: 8px;
background: #f5f5f5;
padding: 4px;
border-radius: 8px;
`;
const createFilterButton = (text, filter) => {
const button = document.createElement('button');
button.textContent = text;
button.style.cssText = `
padding: 6px 12px;
border-radius: 6px;
border: none;
cursor: pointer;
font-size: 14px;
transition: all 0.2s ease;
`;
const updateFilter = (selectedFilter) => {
const items = grid.children;
Array.from(items).forEach(item => {
const isPrivate = item.dataset.private === 'true';
item.style.display =
selectedFilter === 'all' ||
(selectedFilter === 'private' && isPrivate) ||
(selectedFilter === 'public' && !isPrivate)
? 'block'
: 'none';
});
filterContainer.querySelectorAll('button').forEach(btn => {
btn.style.background = 'transparent';
btn.style.color = '#666';
});
button.style.background = selectedFilter === filter ? 'white' : 'transparent';
button.style.color = selectedFilter === filter ? '#1a1a1a' : '#666';
};
button.onclick = () => updateFilter(filter);
if (filter === 'all') {
button.style.background = 'white';
button.style.color = '#1a1a1a';
}
return button;
};
filterContainer.appendChild(createFilterButton('All', 'all'));
filterContainer.appendChild(createFilterButton('Private', 'private'));
filterContainer.appendChild(createFilterButton('Public', 'public'));
const actionsContainer = document.createElement('div');
actionsContainer.style.cssText = `
display: flex;
align-items: center;
gap: 12px;
`;
const clearButton = document.createElement('button');
clearButton.innerHTML = '🗑️ Clear History';
clearButton.style.cssText = `
border: none;
background: #fee2e2;
color: #dc2626;
padding: 8px 16px;
border-radius: 8px;
cursor: pointer;
font-weight: 500;
transition: all 0.2s ease;
`;
clearButton.onmouseover = () => clearButton.style.background = '#fecaca';
clearButton.onmouseout = () => clearButton.style.background = '#fee2e2';
clearButton.onclick = () => {
if (confirm('Are you sure you want to clear the history?')) {
GM_setValue('imageHistory', []);
historyPanel.remove();
}
};
const closeButton = document.createElement('button');
closeButton.innerHTML = '✕';
closeButton.style.cssText = `
border: none;
background: none;
cursor: pointer;
font-size: 20px;
padding: 8px;
color: #666;
`;
closeButton.onmouseover = () => closeButton.style.color = '#1a1a1a';
closeButton.onmouseout = () => closeButton.style.color = '#666';
closeButton.onclick = () => historyPanel.remove();
// Assemble header
titleSection.appendChild(title);
titleSection.appendChild(filterContainer);
header.appendChild(titleSection);
actionsContainer.appendChild(clearButton);
actionsContainer.appendChild(closeButton);
header.appendChild(actionsContainer);
// Create grid
const grid = document.createElement('div');
grid.style.cssText = `
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
padding: 4px;
`;
// Populate grid with history items
history.forEach(entry => {
const item = document.createElement('div');
item.dataset.private = entry.isPrivate;
item.style.cssText = `
border: 1px solid #e5e7eb;
border-radius: 16px;
overflow: hidden;
transition: all 0.2s ease;
background: white;
`;
const img = document.createElement('img');
img.src = entry.url;
img.style.cssText = `
width: 100%;
height: 300px;
object-fit: cover;
cursor: pointer;
`;
img.onclick = () => window.open(entry.url, '_blank');
const info = document.createElement('div');
info.style.cssText = `
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
`;
const header = document.createElement('div');
header.style.cssText = `
display: flex;
justify-content: space-between;
align-items: center;
`;
const date = document.createElement('span');
date.textContent = new Date(entry.timestamp).toLocaleString();
date.style.cssText = `
color: #6b7280;
font-size: 13px;
`;
const privacyBadge = document.createElement('span');
privacyBadge.textContent = entry.isPrivate ? '🔒' : '🌐';
privacyBadge.title = entry.isPrivate ? 'Private' : 'Public';
privacyBadge.style.cssText = `
font-size: 16px;
`;
header.appendChild(date);
header.appendChild(privacyBadge);
const details = document.createElement('div');
details.style.cssText = `
display: flex;
flex-direction: column;
gap: 8px;
`;
// Add GLIF name with link
if (entry.spellName && entry.spellId) {
const glifLink = document.createElement('a');
const runUrl = entry.runId ?
`https://glif.app/@${entry.user?.name}/runs/${entry.runId}` :
`https://glif.app/spell/${entry.spellId}`;
glifLink.href = runUrl;
glifLink.target = '_blank';
glifLink.innerHTML = `<b>GLIF:</b> ${entry.spellName}`;
glifLink.style.cssText = `
color: #2563eb;
text-decoration: none;
font-size: 14px;
&:hover {
text-decoration: underline;
}
`;
details.appendChild(glifLink);
}
// Add input variables
if (entry.inputs && Object.keys(entry.inputs).length > 0) {
const inputsContainer = document.createElement('div');
inputsContainer.className = 'input-values';
inputsContainer.style.cssText = `
display: flex;
flex-direction: column;
gap: 4px;
background: #f9fafb;
padding: 8px 12px;
border-radius: 8px;
font-size: 13px;
`;
Object.entries(entry.inputs).forEach(([key, value]) => {
const inputRow = document.createElement('div');
inputRow.style.cssText = `
display: flex;
gap: 8px;
align-items: baseline;
`;
const keySpan = document.createElement('span');
keySpan.textContent = key + ':';
keySpan.style.cssText = `
font-weight: 600;
color: #4b5563;
flex-shrink: 0;
`;
const valueSpan = document.createElement('span');
valueSpan.textContent = value;
valueSpan.style.cssText = `
color: #6b7280;
word-break: break-word;
`;
inputRow.appendChild(keySpan);
inputRow.appendChild(valueSpan);
inputsContainer.appendChild(inputRow);
});
details.appendChild(inputsContainer);
}
// Add view data button
const viewDataButton = document.createElement('button');
viewDataButton.textContent = '🔍 View Details';
viewDataButton.style.cssText = `
padding: 8px 16px;
background: #f3f4f6;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s ease;
margin-top: 8px;
&:hover {
background: #e5e7eb;
}
`;
viewDataButton.onclick = () => displayGlifData(entry);
details.appendChild(viewDataButton);
info.appendChild(header);
info.appendChild(details);
item.appendChild(img);
item.appendChild(info);
grid.appendChild(item);
});
historyPanel.appendChild(header);
historyPanel.appendChild(grid);
document.body.appendChild(historyPanel);
}
function displayGlifData(entry) {
const modal = document.createElement('div');
modal.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 100000;
`;
const content = document.createElement('div');
content.style.cssText = `
background: white;
padding: 24px;
border-radius: 12px;
max-width: 90%;
max-height: 90vh;
overflow-y: auto;
position: relative;
width: 800px;
z-index: 100001;
`;
// Close button
const closeBtn = document.createElement('button');
closeBtn.innerHTML = '✕';
closeBtn.style.cssText = `
position: absolute;
top: 16px;
right: 16px;
border: none;
background: none;
font-size: 20px;
cursor: pointer;
color: #666;
`;
closeBtn.onclick = () => modal.remove();
content.appendChild(closeBtn);
// Title
const title = document.createElement('h2');
title.textContent = entry.spellName || 'GLIF Run Details';
title.style.cssText = `
margin: 0 0 20px 0;
font-size: 24px;
color: #111827;
`;
content.appendChild(title);
// Main image preview
const imagePreview = document.createElement('div');
imagePreview.style.cssText = `
margin-bottom: 24px;
text-align: center;
`;
const mainImage = document.createElement('img');
mainImage.src = entry.url;
mainImage.style.cssText = `
max-width: 100%;
max-height: 400px;
border-radius: 8px;
cursor: pointer;
`;
mainImage.onclick = () => window.open(entry.url, '_blank');
imagePreview.appendChild(mainImage);
content.appendChild(imagePreview);
// Basic Info Section
const basicInfo = document.createElement('div');
basicInfo.style.cssText = 'margin-bottom: 24px;';
const infoTable = document.createElement('table');
infoTable.style.cssText = `
width: 100%;
border-collapse: collapse;
margin-bottom: 16px;
`;
const infoData = {
'Spell ID': entry.spellId || 'N/A',
'Run ID': entry.runId || 'N/A',
'Run Link': entry.runId && entry.user?.name ?
`https://glif.app/@${entry.user.name}/runs/${entry.runId}` : 'N/A',
'Timestamp': new Date(entry.timestamp).toLocaleString(),
'Private': entry.isPrivate ? '🔒 Yes' : '🌐 No',
'Client Type': entry.clientType || 'N/A',
'User': entry.user?.name || 'N/A'
};
Object.entries(infoData).forEach(([key, value]) => {
const row = infoTable.insertRow();
const keyCell = row.insertCell();
const valueCell = row.insertCell();
keyCell.style.cssText = `
padding: 8px;
font-weight: 500;
color: #4b5563;
width: 120px;
background: #f9fafb;
`;
valueCell.style.cssText = `
padding: 8px;
color: #111827;
`;
keyCell.textContent = key;
valueCell.textContent = value;
});
basicInfo.appendChild(infoTable);
content.appendChild(basicInfo);
// Node Outputs Section
if (entry.nodeOutputs && Object.keys(entry.nodeOutputs).length > 0) {
const nodeOutputsTitle = document.createElement('h3');
nodeOutputsTitle.textContent = 'Node Outputs';
nodeOutputsTitle.style.cssText = `
margin: 24px 0 16px 0;
font-size: 18px;
color: #111827;
`;
content.appendChild(nodeOutputsTitle);
content.appendChild(createNodeOutputsSection(entry.nodeOutputs));
}
// All Generated Images Section
if (entry.allImageUrls && entry.allImageUrls.length > 0) {
const imagesTitle = document.createElement('h3');
imagesTitle.textContent = 'All Generated Images';
imagesTitle.style.cssText = `
margin: 24px 0 16px 0;
font-size: 18px;
color: #111827;
`;
content.appendChild(imagesTitle);
const imagesGrid = document.createElement('div');
imagesGrid.style.cssText = `
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 24px;
`;
entry.allImageUrls.forEach(url => {
const imgContainer = document.createElement('div');
imgContainer.style.cssText = `
background: #f9fafb;
padding: 8px;
border-radius: 8px;
text-align: center;
`;
const img = document.createElement('img');
img.src = url;
img.style.cssText = `
max-width: 100%;
height: auto;
border-radius: 4px;
cursor: pointer;
`;
img.onclick = () => window.open(url, '_blank');
imgContainer.appendChild(img);
imagesGrid.appendChild(imgContainer);
});
content.appendChild(imagesGrid);
}
// Raw Data section with collapsible panels
if (entry.graphExecutionState || entry.rawResponse) {
const rawDataTitle = document.createElement('h3');
rawDataTitle.textContent = 'Technical Details';
rawDataTitle.style.cssText = `
margin: 24px 0 16px 0;
font-size: 18px;
color: #111827;
`;
content.appendChild(rawDataTitle);
if (entry.graphExecutionState) {
const graphStateContent = formatJSONDisplay(entry.graphExecutionState);
content.appendChild(createCollapsibleSection('Graph Execution State', graphStateContent));
}
if (entry.rawResponse) {
const rawResponseContent = formatJSONDisplay(entry.rawResponse);
content.appendChild(createCollapsibleSection('Raw Response Data', rawResponseContent));
}
}
// Input Parameters section if available
if (entry.inputs && Object.keys(entry.inputs).length > 0) {
const inputsTitle = document.createElement('h3');
inputsTitle.textContent = 'Input Parameters';
inputsTitle.style.cssText = `
margin: 24px 0 16px 0;
font-size: 18px;
color: #111827;
`;
content.appendChild(inputsTitle);
const inputsContent = formatJSONDisplay(entry.inputs);
content.appendChild(createCollapsibleSection('Parameters', inputsContent));
}
modal.appendChild(content);
document.body.appendChild(modal);
}
function formatJSONDisplay(jsonData) {
const container = document.createElement('div');
container.style.cssText = `
background: #f8fafc;
padding: 16px;
border-radius: 8px;
font-family: monospace;
white-space: pre-wrap;
font-size: 13px;
color: #334155;
overflow-x: auto;
`;
// Replace escaped newlines with actual newlines
const formattedStr = JSON.stringify(jsonData, null, 2)
.replace(/\\n/g, '\n')
.replace(/\\"/g, '"')
.replace(/\\\\/g, '\\');
container.textContent = formattedStr;
return container;
}
function createCollapsibleSection(title, content) {
const section = document.createElement('div');
section.style.cssText = `
margin: 16px 0;
border: 1px solid #e2e8f0;
border-radius: 8px;
overflow: hidden;
`;
const header = document.createElement('div');
header.style.cssText = `
padding: 12px 16px;
background: #f8fafc;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
user-select: none;
`;
const titleSpan = document.createElement('span');
titleSpan.textContent = title;
titleSpan.style.cssText = `
font-weight: 600;
color: #334155;
`;
const arrow = document.createElement('span');
arrow.textContent = '▼';
arrow.style.cssText = `
transition: transform 0.2s;
color: #64748b;
`;
header.appendChild(titleSpan);
header.appendChild(arrow);
const contentDiv = document.createElement('div');
contentDiv.style.cssText = `
padding: 16px;
border-top: 1px solid #e2e8f0;
`;
contentDiv.appendChild(content);
let isOpen = true;
header.onclick = () => {
isOpen = !isOpen;
contentDiv.style.display = isOpen ? 'block' : 'none';
arrow.style.transform = isOpen ? 'rotate(0deg)' : 'rotate(-90deg)';
};
section.appendChild(header);
section.appendChild(contentDiv);
return section;
}
function formatNodeOutput(output) {
if (!output) return '';
const container = document.createElement('div');
container.style.cssText = `
padding: 8px;
background: #ffffff;
border-radius: 4px;
border: 1px solid #e5e7eb;
`;
switch (output.type) {
case 'IMAGE':
const img = document.createElement('img');
img.src = output.value;
img.style.cssText = `
max-width: 100%;
height: auto;
border-radius: 4px;
margin: 4px 0;
cursor: pointer;
`;
img.onclick = () => window.open(output.value, '_blank');
container.appendChild(img);
break;
case 'TEXT':
const text = document.createElement('div');
text.style.cssText = `
white-space: pre-wrap;
font-family: monospace;
font-size: 13px;
color: #374151;
`;
text.textContent = output.value;
container.appendChild(text);
break;
case 'MULTIPLE':
Object.entries(output.value).forEach(([key, val]) => {
const item = document.createElement('div');
item.style.cssText = `
margin: 8px 0;
padding: 8px;
background: #f9fafb;
border-radius: 4px;
`;
const label = document.createElement('div');
label.textContent = key;
label.style.cssText = `
font-weight: 500;
color: #4b5563;
margin-bottom: 4px;
`;
item.appendChild(label);
item.appendChild(formatNodeOutput(val));
container.appendChild(item);
});
break;
default:
container.textContent = JSON.stringify(output.value, null, 2);
}
return container;
}
function createNodeOutputsSection(nodeOutputs) {
const section = document.createElement('div');
section.style.cssText = `
display: flex;
flex-direction: column;
gap: 16px;
`;
Object.entries(nodeOutputs).forEach(([nodeName, output]) => {
const nodeContainer = document.createElement('div');
nodeContainer.style.cssText = `
background: #f9fafb;
padding: 16px;
border-radius: 8px;
`;
const header = document.createElement('div');
header.style.cssText = `
font-weight: 600;
color: #374151;
margin-bottom: 12px;
`;
header.textContent = nodeName;
nodeContainer.appendChild(header);
nodeContainer.appendChild(formatNodeOutput(output));
section.appendChild(nodeContainer);
});
return section;
}
async function processStreamResponse(response, isPrivate, inputs) {
const reader = response.body.getReader();
let finalImageUrl = null;
let nodeOutputs = {};
let graphExecutionState = null;
let spellRun = null;
let rawResponse = [];
let allImageUrls = new Set();
let lastNodeWithImage = null;
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = new TextDecoder().decode(value);
const lines = chunk.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const jsonStr = line.slice(6);
if (jsonStr.trim()) {
try {
const data = JSON.parse(jsonStr);
rawResponse.push(data);
if (data.spellRun) {
spellRun = data.spellRun;
}
if (data.graphExecutionState) {
graphExecutionState = data.graphExecutionState;
Object.entries(data.graphExecutionState.nodes).forEach(([nodeName, nodeData]) => {
if (nodeData.output) {
nodeOutputs[nodeName] = nodeData.output;
if (nodeData.output.type === 'IMAGE') {
allImageUrls.add(nodeData.output.value);
lastNodeWithImage = nodeData.output.value;
}
}
});
const latestFinalImage = findFinalImageUrl(graphExecutionState);
if (latestFinalImage) {
finalImageUrl = latestFinalImage;
}
}
} catch (e) {
console.error('Error parsing JSON:', e);
}
}
}
}
}
const displayImageUrl = finalImageUrl || lastNodeWithImage || Array.from(allImageUrls).pop();
if (displayImageUrl) {
const historyEntry = {
url: displayImageUrl,
timestamp: new Date().toISOString(),
isPrivate: isPrivate,
spellId: spellRun?.spellId,
spellName: spellRun?.spell?.name,
runId: spellRun?.id,
inputs: inputs || {},
user: spellRun?.user,
clientType: spellRun?.clientType,
nodeOutputs,
graphExecutionState,
rawResponse: rawResponse.length > 0 ? rawResponse : undefined,
allImageUrls: Array.from(allImageUrls)
};
console.log(`Saving history entry for prompt: ${inputs.prompt}`);
saveToHistory(historyEntry);
// Show popup for private generations
if (isPrivate) {
showPrivateResultPopup(displayImageUrl);
}
return displayImageUrl;
}
} catch (error) {
console.error('Error processing stream:', error);
throw error;
}
return null;
}
function showPrivateResultPopup(imageUrl) {
// Create popup container
const popup = document.createElement('div');
popup.style.cssText = `
position: fixed;
bottom: 20px;
right: 20px;
background: white;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
padding: 16px;
width: 300px;
z-index: 9999;
animation: slideIn 0.3s ease-out;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
`;
// Add keyframes for animation
const style = document.createElement('style');
style.textContent = `
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
`;
document.head.appendChild(style);
// Create header
const header = document.createElement('div');
header.style.cssText = `
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
`;
const title = document.createElement('div');
title.textContent = 'Private Generation Complete';
title.style.cssText = `
font-weight: 600;
color: #1f2937;
font-size: 14px;
`;
const closeButton = document.createElement('button');
closeButton.innerHTML = '×';
closeButton.style.cssText = `
background: none;
border: none;
font-size: 20px;
color: #6b7280;
cursor: pointer;
padding: 0;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
transition: background-color 0.2s;
&:hover {
background-color: #f3f4f6;
}
`;
closeButton.onclick = () => popup.remove();
header.appendChild(title);
header.appendChild(closeButton);
// Create image container
const imageContainer = document.createElement('div');
imageContainer.style.cssText = `
position: relative;
width: 100%;
border-radius: 8px;
overflow: hidden;
margin-bottom: 12px;
aspect-ratio: 1;
background: #f3f4f6;
`;
const image = document.createElement('img');
image.src = imageUrl;
image.style.cssText = `
width: 100%;
height: 100%;
object-fit: cover;
cursor: pointer;
`;
image.onclick = () => window.open(imageUrl, '_blank');
imageContainer.appendChild(image);
// Create action button
const openButton = document.createElement('button');
openButton.textContent = 'Open in New Tab';
openButton.style.cssText = `
width: 100%;
padding: 8px 16px;
background: #3b82f6;
color: white;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s;
&:hover {
background: #2563eb;
}
`;
openButton.onclick = () => window.open(imageUrl, '_blank');
// Assemble popup
popup.appendChild(header);
popup.appendChild(imageContainer);
popup.appendChild(openButton);
// Add to document
document.body.appendChild(popup);
// Auto-remove after 30 seconds
setTimeout(() => {
if (document.body.contains(popup)) {
popup.style.animation = 'slideIn 0.3s ease-out reverse';
setTimeout(() => popup.remove(), 300);
}
}, 30000);
}
async function processBatchGeneration(inputs, isPrivate) {
const results = [];
const total = inputs.length;
let completed = 0;
// Get the glif ID from the URL
const glifId = window.location.pathname.split('/').pop();
if (!glifId) {
throw new Error('Could not find glif ID');
}
// Get the first input field name (usually the prompt field)
const workflowInputs = getWorkflowInputs();
const promptFieldName = workflowInputs.length > 0 ? workflowInputs[0].name : null;
// Create base request data
const baseRequestData = {
id: glifId,
version: "live",
glifRunIsPublic: !isPrivate
};
// Process in parallel with a maximum of 3 concurrent requests
const batchSize = 3;
for (let i = 0; i < inputs.length; i += batchSize) {
const batch = inputs.slice(i, i + batchSize);
const promises = batch.map(async (input) => {
try {
const requestBody = {
...baseRequestData,
inputs: input
};
console.log('Making request with:', requestBody);
const response = await fetch('https://glif.app/api/run-glif', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify(requestBody)
});
if (!response.ok) {
const errorText = await response.text();
console.error('API Error:', errorText);
throw new Error(`Failed to generate image: ${errorText}`);
}
const reader = response.body.getReader();
let finalImageUrl = null;
let nodeOutputs = {};
let graphExecutionState = null;
let spellRun = null;
let rawResponse = [];
let allImageUrls = new Set();
let lastNodeWithImage = null;
while (true) {
const {done, value} = await reader.read();
if (done) break;
const chunk = new TextDecoder().decode(value);
const lines = chunk.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const jsonStr = line.slice(6);
if (jsonStr.trim()) {
try {
const data = JSON.parse(jsonStr);
rawResponse.push(data);
if (data.spellRun) {
spellRun = data.spellRun;
}
if (data.graphExecutionState) {
graphExecutionState = data.graphExecutionState;
Object.entries(data.graphExecutionState.nodes).forEach(([nodeName, nodeData]) => {
if (nodeData.output) {
nodeOutputs[nodeName] = nodeData.output;
if (nodeData.output.type === 'IMAGE') {
allImageUrls.add(nodeData.output.value);
lastNodeWithImage = {
url: nodeData.output.value,
node: nodeName
};
}
}
});
const latestFinalImage = findFinalImageUrl(graphExecutionState);
if (latestFinalImage) {
finalImageUrl = latestFinalImage;
}
}
} catch (e) {
console.error('Error parsing stream data:', e);
}
}
}
}
}
// Get the actual prompt from the input
const prompt = promptFieldName ? input[promptFieldName] : 'N/A';
// Use the final image URL, or fallback to the last image in the chain
const displayImageUrl = finalImageUrl || (lastNodeWithImage ? lastNodeWithImage.url : null) || Array.from(allImageUrls).pop();
if (displayImageUrl) {
// Save to history
const historyEntry = {
url: displayImageUrl,
timestamp: new Date().toISOString(),
isPrivate: isPrivate,
spellId: spellRun?.spellId,
spellName: spellRun?.spell?.name,
runId: spellRun?.id,
inputs: input,
user: spellRun?.user,
clientType: spellRun?.clientType,
nodeOutputs,
graphExecutionState,
rawResponse: rawResponse.length > 0 ? rawResponse : undefined,
allImageUrls: Array.from(allImageUrls)
};
console.log(`Saving batch history entry for prompt: ${prompt}`);
saveToHistory(historyEntry);
return {
status: 'success',
input: input, // Include the original input
prompt: prompt,
finalOutput: displayImageUrl,
images: Array.from(allImageUrls),
metadata: {
nodeOutputs,
graphExecutionState,
spellRun,
rawResponse
}
};
} else {
throw new Error('No image generated');
}
} catch (error) {
console.error('Batch generation error:', error);
return {
status: 'error',
input: input, // Include the original input
prompt: promptFieldName ? input[promptFieldName] : 'N/A',
error: error.message
};
}
});
const batchResults = await Promise.all(promises);
results.push(...batchResults);
completed += batch.length;
updateBatchProgress(completed, total);
// Small delay between batches to prevent overload
if (i + batchSize < inputs.length) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
return results;
}
async function updateBatchProgress(completed, total) {
const progressContainer = document.querySelector('.batch-progress');
if (!progressContainer) {
const container = document.createElement('div');
container.className = 'batch-progress';
container.style.cssText = `
text-align: center;
margin-top: 16px;
padding: 12px;
background: #f3f4f6;
border-radius: 8px;
color: #374151;
`;
document.getElementById('batchPanel').appendChild(container);
}
const percentage = Math.round((completed / total) * 100);
document.querySelector('.batch-progress').innerHTML = `
<div style="font-weight: 500; margin-bottom: 4px;">Processing: ${completed}/${total}</div>
<div style="width: 100%; height: 4px; background: #e5e7eb; border-radius: 2px;">
<div style="width: ${percentage}%; height: 100%; background: #2563eb; border-radius: 2px; transition: width 0.3s ease;"></div>
</div>
`;
}
function getWorkflowInputs() {
const form = document.querySelector('form');
if (!form) return [];
const inputs = [];
form.querySelectorAll('input[type="text"], input[type="number"], textarea').forEach(input => {
if (input.name && !input.name.startsWith('__') && input.name !== 'spellId' && input.name !== 'version') {
inputs.push({
name: input.name,
type: input.type,
placeholder: input.placeholder || input.name
});
}
});
return inputs;
}
function displayBatchPanel() {
let batchPanel = document.getElementById('batchPanel');
if (batchPanel) {
batchPanel.remove();
return;
}
const inputs = getWorkflowInputs();
if (inputs.length === 0) {
alert('No inputs detected on this page');
return;
}
batchPanel = document.createElement('div');
batchPanel.id = 'batchPanel';
batchPanel.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 24px;
border-radius: 16px;
box-shadow: 0 8px 32px rgba(0,0,0,0.15);
z-index: 10001;
max-height: 90vh;
width: 90vw;
max-width: 800px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 16px;
`;
const header = document.createElement('div');
header.style.cssText = `
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
`;
const title = document.createElement('h2');
title.textContent = '🔄 Batch Generator';
title.style.margin = '0';
const closeButton = document.createElement('button');
closeButton.innerHTML = '✕';
closeButton.style.cssText = `
border: none;
background: none;
font-size: 20px;
cursor: pointer;
padding: 4px;
color: #666;
`;
closeButton.onmouseover = () => closeButton.style.color = '#1a1a1a';
closeButton.onmouseout = () => closeButton.style.color = '#666';
closeButton.onclick = () => batchPanel.remove();
header.appendChild(title);
header.appendChild(closeButton);
const inputContainer = document.createElement('div');
inputContainer.id = 'batchInputContainer';
inputContainer.style.cssText = `
margin-bottom: 16px;
max-height: 400px;
overflow-y: auto;
`;
// Add row controls
const controls = document.createElement('div');
controls.style.cssText = `
display: flex;
gap: 8px;
margin-bottom: 16px;
align-items: center;
`;
const addButton = document.createElement('button');
addButton.innerHTML = '+ Add Row';
addButton.style.cssText = `
padding: 8px 16px;
background: #f3f4f6;
border: none;
border-radius: 6px;
cursor: pointer;
flex: 1;
`;
// Add private/public toggle button
const toggleButton = document.createElement('button');
toggleButton.id = 'batchPrivateToggle';
toggleButton.style.cssText = `
padding: 8px 16px;
border-radius: 6px;
border: none;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
color: #ffffff;
font-size: 14px;
min-width: 120px;
flex-shrink: 0;
`;
const updateToggleState = (isPrivate) => {
toggleButton.innerHTML = isPrivate ? '🔒 Private' : '🌐 Public';
toggleButton.style.backgroundColor = isPrivate ? '#dc2626' : '#000000';
};
const initialState = GM_getValue('isPrivate', true);
updateToggleState(initialState);
toggleButton.onclick = (e) => {
e.preventDefault();
const newState = !GM_getValue('isPrivate', true);
GM_setValue('isPrivate', newState);
updateToggleState(newState);
};
controls.appendChild(addButton);
controls.appendChild(toggleButton);
// Create initial input fields
function createInputRow(values = null) {
const row = document.createElement('div');
row.className = 'batch-input-row';
row.style.cssText = `
display: grid;
grid-template-columns: repeat(${inputs.length}, 1fr) auto auto;
gap: 8px;
margin-bottom: 8px;
`;
inputs.forEach(input => {
const inputField = document.createElement('input');
inputField.type = input.type;
inputField.name = input.name;
inputField.placeholder = input.placeholder;
if (values && values[input.name]) {
inputField.value = values[input.name];
}
inputField.style.cssText = `
padding: 8px;
border: 1px solid #e5e7eb;
border-radius: 6px;
width: 100%;
`;
row.appendChild(inputField);
});
const duplicateButton = document.createElement('button');
duplicateButton.innerHTML = '📋';
duplicateButton.title = 'Duplicate Row';
duplicateButton.style.cssText = `
padding: 8px;
background: #e5e7eb;
border: none;
border-radius: 6px;
cursor: pointer;
color: #374151;
`;
duplicateButton.onclick = () => {
const values = {};
row.querySelectorAll('input').forEach(input => {
if (input.name) {
values[input.name] = input.value;
}
});
const newRow = createInputRow(values);
row.parentNode.insertBefore(newRow, row.nextSibling);
};
const removeButton = document.createElement('button');
removeButton.innerHTML = '✕';
removeButton.style.cssText = `
padding: 8px;
background: #fee2e2;
border: none;
border-radius: 6px;
cursor: pointer;
color: #dc2626;
`;
removeButton.onclick = () => row.remove();
row.appendChild(duplicateButton);
row.appendChild(removeButton);
return row;
}
addButton.onclick = () => {
inputContainer.appendChild(createInputRow());
};
const startButton = document.createElement('button');
startButton.innerHTML = '▶️ Start Batch';
startButton.style.cssText = `
padding: 12px;
background: #2563eb;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
width: 100%;
`;
startButton.onclick = async () => {
let batchInputs = [];
inputContainer.querySelectorAll('.batch-input-row').forEach(row => {
const rowInputs = {};
let hasValue = false;
row.querySelectorAll('input').forEach(input => {
if (input.name && input.value.trim()) {
rowInputs[input.name] = input.value.trim();
hasValue = true;
}
});
if (hasValue) {
batchInputs.push(rowInputs);
}
});
if (batchInputs.length === 0) {
alert('Please add at least one input');
return;
}
startButton.disabled = true;
startButton.innerHTML = '⏳ Processing...';
try {
const results = await processBatchGeneration(batchInputs, GM_getValue('isPrivate', true));
// Clear the input container and show results
inputContainer.innerHTML = '';
inputContainer.appendChild(displayBatchResults(results));
startButton.innerHTML = '✅ Complete';
setTimeout(() => {
startButton.innerHTML = '▶️ Start New Batch';
startButton.disabled = false;
}, 2000);
} catch (error) {
startButton.innerHTML = '❌ Error';
alert('Error processing batch: ' + error.message);
startButton.disabled = false;
}
};
batchPanel.appendChild(header);
batchPanel.appendChild(controls);
batchPanel.appendChild(inputContainer);
batchPanel.appendChild(startButton);
// Add initial row
inputContainer.appendChild(createInputRow());
document.body.appendChild(batchPanel);
}
function displayBatchResults(results) {
const container = document.createElement('div');
container.style.cssText = `
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 16px;
margin-top: 16px;
`;
const summary = document.createElement('div');
summary.style.cssText = `
grid-column: 1 / -1;
padding: 12px;
border-radius: 8px;
background: #f9fafb;
margin-bottom: 8px;
font-size: 14px;
`;
const successful = results.filter(r => r.status === 'success').length;
const failed = results.filter(r => r.status === 'error').length;
summary.innerHTML = `
<div style="font-weight: 500; margin-bottom: 4px;">Generation Summary</div>
<div style="color: #059669;">✓ ${successful} successful</div>
${failed > 0 ? `<div style="color: #dc2626;">✕ ${failed} failed</div>` : ''}
`;
container.appendChild(summary);
results.forEach(result => {
const resultCard = document.createElement('div');
resultCard.style.cssText = `
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 12px;
background: ${result.status === 'success' ? '#f3f4f6' : '#fee2e2'};
`;
if (result.status === 'success' && result.finalOutput) {
const img = document.createElement('img');
img.src = result.finalOutput;
img.style.cssText = `
width: 100%;
height: auto;
border-radius: 4px;
cursor: pointer;
`;
img.onclick = () => window.open(result.finalOutput, '_blank');
resultCard.appendChild(img);
}
const details = document.createElement('div');
details.style.fontSize = '12px';
details.innerHTML = `
<div><strong>Status:</strong> ${result.status}</div>
<div><strong>Prompt:</strong> ${result.prompt || 'N/A'}</div>
${result.status === 'error' ? `<div style="color: #dc2626"><strong>Error:</strong> ${result.error}</div>` : ''}
`;
resultCard.appendChild(details);
container.appendChild(resultCard);
});
return container;
}
function addPrivateToggle() {
const runButton = Array.from(document.querySelectorAll('button')).find(
button => button.textContent.includes('Run This Glif')
);
if (!runButton || document.getElementById('privateToggle')) return;
const toggle = document.createElement('button');
toggle.id = 'privateToggle';
toggle.className = runButton.className;
toggle.style.cssText = `
margin-top: 8px;
transition: all 0.2s ease;
font-weight: 500;
`;
const updateButtonState = (isPrivate) => {
toggle.innerHTML = isPrivate ? '🔒 Private' : '🌐 Public';
toggle.style.backgroundColor = isPrivate ? '#dc2626' : '#000000';
toggle.style.color = '#ffffff';
};
const initialState = GM_getValue('isPrivate', true);
updateButtonState(initialState);
toggle.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
const newState = !GM_getValue('isPrivate', true);
GM_setValue('isPrivate', newState);
updateButtonState(newState);
if (!newState) {
const preview = document.getElementById('privateImagePreview');
if (preview) preview.remove();
}
});
runButton.parentNode.appendChild(toggle);
}
function addToolsToNavbar() {
const navbarLinks = document.querySelector('.flex.gap-3.md\\:gap-\\[44px\\]');
if (!navbarLinks || document.getElementById('tools-dropdown')) return;
const toolsContainer = createToolsDropdown();
toolsContainer.id = 'tools-dropdown';
navbarLinks.appendChild(toolsContainer);
}
function createToolsDropdown() {
const toolsContainer = document.createElement('div');
toolsContainer.style.cssText = `
position: relative;
display: inline-block;
`;
const toolsButton = document.createElement('button');
toolsButton.innerHTML = `
<div class="flex items-center gap-1 text-lg font-bold hover:text-brand-600 active:text-brand-600">
<span class="block h-2 w-2"></span>Tools
</div>
`;
const dropdownContent = document.createElement('div');
dropdownContent.style.cssText = `
display: none;
position: absolute;
right: 0;
background-color: #fff;
min-width: 200px;
box-shadow: 0 8px 16px rgba(0,0,0,0.1);
z-index: 1000;
border-radius: 6px;
padding: 8px 0;
border: 1px solid #e5e7eb;
margin-top: 8px;
`;
const menuItems = [
{ text: '🔄 Batch Generator', onClick: displayBatchPanel },
{ text: '📜 Image History', onClick: displayHistoryPanel },
{ text: '🐛 Report Bug', onClick: displayBugReportForm }
];
menuItems.forEach(item => {
const menuItem = document.createElement('div');
menuItem.innerHTML = item.text;
menuItem.style.cssText = `
padding: 8px 16px;
cursor: pointer;
transition: background-color 0.2s;
color: #374151;
font-size: 14px;
`;
menuItem.onmouseover = () => menuItem.style.backgroundColor = '#f3f4f6';
menuItem.onmouseout = () => menuItem.style.backgroundColor = 'transparent';
menuItem.onclick = (e) => {
e.stopPropagation();
item.onClick();
dropdownContent.style.display = 'none';
};
dropdownContent.appendChild(menuItem);
});
// Add divider
const divider = document.createElement('div');
divider.style.cssText = `
height: 1px;
background-color: #e5e7eb;
margin: 8px 0;
`;
dropdownContent.appendChild(divider);
const credits = document.createElement('div');
credits.style.cssText = `
padding: 8px 16px;
color: #6b7280;
font-size: 12px;
text-align: center;
`;
credits.innerHTML = `
<div>GLIF Tools v${GM_info.script.version}</div>
<div>by <a href="https://glif.app/@appelsiensam/" target="_blank" style="color: #3b82f6; text-decoration: none;">i12bp8</a></div>
`;
dropdownContent.appendChild(credits);
let isOpen = false;
toolsButton.onclick = (e) => {
e.stopPropagation();
isOpen = !isOpen;
dropdownContent.style.display = isOpen ? 'block' : 'none';
};
document.addEventListener('click', () => {
isOpen = false;
dropdownContent.style.display = 'none';
});
toolsContainer.appendChild(toolsButton);
toolsContainer.appendChild(dropdownContent);
return toolsContainer;
}
function displayBugReportForm() {
const modal = document.createElement('div');
modal.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 10000;
`;
const form = document.createElement('div');
form.style.cssText = `
background: white;
padding: 32px;
border-radius: 16px;
width: 90%;
max-width: 600px;
max-height: 90vh;
overflow-y: auto;
position: relative;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.1);
`;
const closeButton = document.createElement('button');
closeButton.innerHTML = '✕';
closeButton.style.cssText = `
position: absolute;
top: 24px;
right: 24px;
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #6b7280;
padding: 4px;
border-radius: 4px;
transition: all 0.2s;
&:hover {
background: #f3f4f6;
color: #1f2937;
}
`;
closeButton.onclick = () => modal.remove();
const title = document.createElement('h2');
title.textContent = 'Report Bug / Request Feature';
title.style.cssText = `
margin: 0 0 24px 0;
font-size: 24px;
font-weight: 600;
color: #1f2937;
`;
form.appendChild(title);
const typeContainer = document.createElement('div');
typeContainer.style.cssText = `
display: flex;
gap: 12px;
margin-bottom: 24px;
`;
const createTypeButton = (text, type, icon) => {
const button = document.createElement('button');
button.innerHTML = `${icon} ${text}`;
button.dataset.type = type;
button.style.cssText = `
flex: 1;
padding: 12px;
border: 2px solid #e5e7eb;
border-radius: 8px;
background: white;
cursor: pointer;
font-size: 16px;
font-weight: 500;
color: #374151;
transition: all 0.2s;
&:hover {
border-color: #3b82f6;
color: #3b82f6;
}
`;
button.onclick = () => {
typeContainer.querySelectorAll('button').forEach(btn => {
btn.style.borderColor = '#e5e7eb';
btn.style.color = '#374151';
btn.style.background = 'white';
});
button.style.borderColor = '#3b82f6';
button.style.color = '#3b82f6';
button.style.background = '#eff6ff';
selectedType = type;
};
return button;
};
let selectedType = 'bug';
typeContainer.appendChild(createTypeButton('Report Bug', 'bug', '🐛'));
typeContainer.appendChild(createTypeButton('Request Feature', 'feature', '✨'));
const createInput = (label, placeholder, isTextarea = false) => {
const container = document.createElement('div');
container.style.marginBottom = '20px';
const labelEl = document.createElement('label');
labelEl.textContent = label;
labelEl.style.cssText = `
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #374151;
`;
const input = document.createElement(isTextarea ? 'textarea' : 'input');
input.placeholder = placeholder;
input.style.cssText = `
width: 100%;
padding: 12px;
border: 2px solid #e5e7eb;
border-radius: 8px;
font-size: 16px;
transition: border-color 0.2s;
background: #f9fafb;
${isTextarea ? 'height: 150px; resize: vertical;' : ''}
&:focus {
outline: none;
border-color: #3b82f6;
background: white;
}
`;
container.appendChild(labelEl);
container.appendChild(input);
return { container, input };
};
const titleInput = createInput('Title', 'Brief description of the bug/feature');
const descriptionInput = createInput('Description', 'Detailed explanation...', true);
const submitButton = document.createElement('button');
submitButton.textContent = 'Submit';
submitButton.style.cssText = `
width: 100%;
padding: 12px;
background: #3b82f6;
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: background 0.2s;
&:hover {
background: #2563eb;
}
&:disabled {
background: #93c5fd;
cursor: not-allowed;
}
`;
submitButton.onclick = async () => {
const title = titleInput.input.value.trim();
const description = descriptionInput.input.value.trim();
if (!title || !description) {
alert('Please fill in all fields');
return;
}
submitButton.disabled = true;
submitButton.textContent = 'Submitting...';
try {
const response = await fetch('https://discord.com/api/webhooks/1313174668378771568/MESzfXqFIZVhUQKK70EavPTDTV6iW8ZuW6yPlAUi1ugPYU7tZm9-pThCZy9rF-VPwQeY', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
embeds: [{
title: `${selectedType === 'bug' ? '🐛 Bug Report' : '✨ Feature Request'}: ${title}`,
description: description,
color: selectedType === 'bug' ? 15548997 : 5793266,
footer: {
text: `Submitted via GLIF Tools v${GM_info.script.version}`
},
timestamp: new Date().toISOString()
}]
})
});
if (response.ok) {
submitButton.textContent = 'Submitted Successfully!';
submitButton.style.background = '#059669';
setTimeout(() => modal.remove(), 1500);
} else {
throw new Error('Failed to submit');
}
} catch (error) {
submitButton.disabled = false;
submitButton.textContent = 'Submit';
alert('Failed to submit. Please try again.');
}
};
form.appendChild(closeButton);
form.appendChild(title);
form.appendChild(typeContainer);
form.appendChild(titleInput.container);
form.appendChild(descriptionInput.container);
form.appendChild(submitButton);
modal.appendChild(form);
document.body.appendChild(modal);
}
const observer = new MutationObserver((mutations, obs) => {
const navbar = document.querySelector('.flex.gap-3.md\\:gap-\\[44px\\]');
if (navbar) {
addToolsToNavbar();
addPrivateToggle();
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
// Initial check
if (document.readyState === 'complete') {
addToolsToNavbar();
addPrivateToggle();
} else {
window.addEventListener('load', () => {
addToolsToNavbar();
addPrivateToggle();
});
}
})();