@@ -1330,10 +1330,20 @@
inset : 0 ;
background : #0a0a0a ;
z-index : 500 ;
flex-direction : column ;
flex-direction : row ; /* row layout to support full height sidebar */
overflow : hidden ;
}
. studio . open { display : flex ; }
. studio-left {
display : flex ;
flex-direction : column ;
flex : 1 ;
min-width : 0 ;
height : 100 % ;
overflow : hidden ;
}
. studio-topbar {
flex-shrink : 0 ;
display : flex ;
@@ -1348,10 +1358,6 @@
. studio-topbar-center { flex : 1 ; min-width : 0 ; display : flex ; align-items : center ; gap : 10 px ; overflow : hidden ; }
. studio-topbar-right { display : flex ; align-items : center ; gap : 6 px ; flex-shrink : 0 ; }
. studio-body {
flex : 1 ; min-height : 0 ;
display : flex ; overflow : hidden ;
}
. studio-main {
flex : 1 ; min-width : 0 ;
display : flex ; flex-direction : column ; overflow : hidden ;
@@ -1464,7 +1470,7 @@
. sidebar-inner {
flex : 1 ; min-height : 0 ;
display : flex ; flex-direction : column ;
padding-top : 36 px ; overflow : hidden ;
padding-top : 8 px ; overflow : hidden ;
transition : opacity 0.15 s ;
}
. studio-sidebar . collapsed . sidebar-inner { opacity : 0 ; pointer-events : none ; }
@@ -1936,17 +1942,6 @@
< / style >
< / head >
< body >
<!-- Floating services status container -->
< div class = "services-status-container" style = "position: fixed; bottom: 12px; left: 12px; z-index: 11000; display: flex; align-items: center; gap: 14px; background: rgba(18, 18, 18, 0.9); border: 1px solid #2e2e2e; padding: 6px 12px; border-radius: 20px; font-family: monospace; font-size: 11px; color: #aaa; box-shadow: 0 4px 12px rgba(0,0,0,0.5); backdrop-filter: blur(4px);" >
< div style = "display: flex; align-items: center; gap: 6px; cursor: help;" id = "backendStatus" title = "Backend: Offline (API server is down)" >
< span class = "status-dot-led offline" id = "backendDot" > < / span >
< span > API< / span >
< / div >
< div style = "display: flex; align-items: center; gap: 6px; cursor: help;" id = "modelStatus" title = "Model-Engine: Status unknown (API server is down)" >
< span class = "status-dot-led offline" id = "modelDot" > < / span >
< span > Model< / span >
< / div >
< / div >
< div id = "privacyOverlay" class = "privacy-overlay" onclick = "if(privacyMode)disablePrivacyOverlay()" >
< div class = "priv-topbar" >
< div class = "priv-logo" > S< / div >
@@ -2069,12 +2064,23 @@
< / div >
< div class = "header" >
< div class = "controls" >
< div class = "controls" style = "display:flex;align-items:center;gap:8px" >
< span class = "count" id = "count" > 0 images< / span >
< button class = "btn" id = "archiveViewBtn" onclick = "toggleArchiveView()" title = "View archived items" > 📦 Archive< / button >
< a href = "trash.html" class = "btn" title = "View database inconsistencies" style = "text-decoration:none;display:inline-flex;align-items:center;" > 🗑️ Trash< / a >
< button class = "btn" id = "selectBtn" onclick = "toggleSelectMode()" > Select< / button >
< button class = "btn" id = "privacyBtn" onclick = "togglePrivacyMode()" title = "Privacy mode — hides content on focus loss (P)" > 🔓< / button >
<!-- Inline Services LEDs on Dashboard -->
< div class = "services-status-container" style = "display:flex;align-items:center;gap:10px;background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.06);padding:4px 10px;border-radius:15px;font-family:monospace;font-size:10px;color:#888" >
< div style = "display:flex;align-items:center;gap:4px;cursor:help" id = "backendStatus" title = "Backend: Offline (API server is down)" >
< span class = "status-dot-led offline" id = "backendDot" > < / span >
< span > API< / span >
< / div >
< div style = "display:flex;align-items:center;gap:4px;cursor:help" id = "modelStatus" title = "Model-Engine: Status unknown (API server is down)" >
< span class = "status-dot-led offline" id = "modelDot" > < / span >
< span > COMFY< / span >
< / div >
< / div >
< / div >
< div class = "upload-zone" >
< input type = "file" id = "uploadInput" multiple onchange = "handleUpload(event)" / >
@@ -2103,30 +2109,65 @@
<!-- Studio view (replaces old lightbox + faceswap modal) -->
< div id = "studio" class = "studio" >
<!-- Topbar -->
< div class = "studio-topbar " >
< div class = "studio-topbar-left" >
< button class = "btn" onclick = "closeStudio()" style = "font-size:11px;padding:4px 10px" > ✕ Gallery< / button >
< button class = "btn" id = "lbBackBtn" onclick = "jumpToPrevGroup()" style = "font-size:11px;padding:4px 10px;display:none" title = "Go back to previously opened group" > ↺ Back< / button >
< / div >
< div class = "studio-topbar-center" >
< input typ e = "text" id = "lbGroupNameInput"
style = "background:transparent;border:none;border-bottom:1px solid #2a2a2a;color:#fff;font-size:13px;font-weight:600;padding:2px 5px;min-width:80px;max-width:200px;outline:none"
placeholder = "Group name…"
onblur = "updateGroupName()" onkeydown = "if(event.key==='Enter')this.blur() "
title = "Group name (applies to all images in this group)" / >
< span id = "lbPoseTag" style = "font-size:11px;color:#f59e0b;display:none" > < / span >
< span id = "lbImgDims" style = "font-size:11px;color:#444;margin:0 8px" > < / span >
< span id = "lbCounter" style = "font-size:12px;color:#555" > < / span >
< / div >
< div class = "studio-topbar-right" style = "display:flex;align-items:center;gap:6px" >
< button class = "btn" id = "studioPrivacyBtn" onclick = "togglePrivacyMode()" title = "Privacy mode — hides content on focus loss (P)" style = "font-size:12px;padding:4px 8px" > 🔓< / button >
< button class = "btn" id = "sidebarInnerToggle" onclick = "toggleSidebar()" title = "Collapse/expand toolbox" > ⊟< / button >
< / div >
< / div >
<!-- Left area: topbar + main viewer + filmstrip -->
< div class = "studio-left " >
<!-- Topbar -- >
< div class = "studio-topbar" >
< div class = "studio-topbar-left" >
< a href = "./car.html" id = "studioDashboardLink" class = "btn" style = "font-size:11px;padding:4px 10px;text-decoration:none;display:none;align-items:center;gap:4px" > ← Dashboard< / a >
< button class = "btn" id = "studioGalleryBtn" onclick = "closeStudio()" style = "font-size:11px;padding:4px 10px" > ✕ Gallery< / button >
< button class = "btn" id = "lbBackBtn" onclick = "jumpToPrevGroup()" s tyl e= "font-size:11px;padding:4px 10px;display:none" title = "Go back to previously opened group" > ↺ Back< / button >
< / div >
< div class = "studio-topbar-center" style = "display:flex;align-items:center;gap:6px" >
< input type = "text" id = "lbGroupNameInput "
style = "background:transparent;border:none;border-bottom:1px solid #2a2a2a;color:#fff;font-size:13px;font-weight:600;padding:2px 5px;min-width:80px;max-width:200px;outline:none"
placeholder = "Group name…"
onblur = "updateGroupName()" onkeydown = "if(event.key==='Enter')this.blur()"
title = "Group name (applies to all images in this group)" / >
<!-- Body: main viewer + sidebar -- >
< div class = "studio-body" >
< span style = "color:#444;font-size:13px" > /< / span >
< input type = "text" id = "lbTopbarImageNameInput"
style = "background:transparent;border:none;border-bottom:1px solid #2a2a2a;color:#ccc;font-size:12px;padding:2px 5px;min-width:80px;max-width:180px;outline:none"
placeholder = "Image name…"
onblur = "updateTopbarImageName()" onkeydown = "if(event.key==='Enter')this.blur()"
title = "Image name / Character name" / >
< div style = "display:flex;gap:4px;margin-left:4px" >
< button class = "sb-btn" onclick = "copyRawFilename()" style = "padding:2px 6px;font-size:9px;min-width:auto;display:inline-flex;align-items:center;gap:3px;height:20px;border-radius:4px" title = "Copy raw filename (ID) to clipboard" >
< svg width = "8" height = "8" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" >
< rect x = "9" y = "9" width = "13" height = "13" rx = "2" ry = "2" > < / rect >
< path d = "M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" > < / path >
< / svg > ID
< / button >
< button class = "sb-btn" onclick = "copyFilepath()" style = "padding:2px 6px;font-size:9px;min-width:auto;display:inline-flex;align-items:center;gap:3px;height:20px;border-radius:4px" title = "Copy absolute file path to clipboard" >
< svg width = "8" height = "8" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" >
< rect x = "9" y = "9" width = "13" height = "13" rx = "2" ry = "2" > < / rect >
< path d = "M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" > < / path >
< / svg > Path
< / button >
< / div >
< span id = "lbPoseTag" style = "font-size:11px;color:#f59e0b;display:none" > < / span >
< span id = "lbImgDims" style = "font-size:11px;color:#444;margin:0 8px" > < / span >
< span id = "lbCounter" style = "font-size:12px;color:#555" > < / span >
< / div >
< div class = "studio-topbar-right" style = "display:flex;align-items:center;gap:6px" >
<!-- Inline Services LEDs on Studio Topbar -->
< div class = "services-status-container" style = "display:flex;align-items:center;gap:10px;background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.06);padding:4px 10px;border-radius:15px;font-family:monospace;font-size:10px;color:#888;margin-right:6px" >
< div style = "display:flex;align-items:center;gap:4px;cursor:help" id = "studioBackendStatus" title = "Backend: Status unknown" >
< span class = "status-dot-led offline" id = "studioBackendDot" > < / span >
< span > API< / span >
< / div >
< div style = "display:flex;align-items:center;gap:4px;cursor:help" id = "studioModelStatus" title = "Model-Engine: Status unknown" >
< span class = "status-dot-led offline" id = "studioModelDot" > < / span >
< span > COMFY< / span >
< / div >
< / div >
< button class = "btn" id = "studioPrivacyBtn" onclick = "togglePrivacyMode()" title = "Privacy mode — hides content on focus loss (P)" style = "font-size:12px;padding:4px 8px" > 🔓< / button >
< button class = "btn" id = "sidebarInnerToggle" onclick = "toggleSidebar()" title = "Collapse/expand toolbox" > ⊟< / button >
< / div >
< / div >
<!-- Main area: viewer + filmstrip -->
< div class = "studio-main" >
@@ -2179,19 +2220,20 @@
< option value = "15000" > 15s< / option >
< / select >
< / div >
< / div >
< / div > <!-- Close .studio - main -->
< / div > <!-- Close .studio - left -->
<!-- Sidebar toolbox -->
<!-- Sidebar toolbox -->
< div id = "studioSidebar" class = "studio-sidebar" >
< div id = "sidebarInner" class = "sidebar-inner" >
< div class = "sidebar-tabs" >
< button class = "sb-tab active" data-tab = "info" onclick = "switchSidebarTab('info')" > Info< / button >
< button class = "sb-tab" data-tab = "generate" onclick = "switchSidebarTab('generate')" > Ceate< / button >
< button class = "sb-tab" data-tab = "faceswap" onclick = "switchSidebarTab('faceswap')" > 👤< / button >
< button class = "sb-tab" data-tab = "scenery" onclick = "switchSidebarTab('scenery')" > 🎬< / button >
< button class = "sb-tab" data-tab = "segment" onclick = "switchSidebarTab('segment')" > α < / button >
< button class = "sb-tab" data-tab = "orbit" onclick = "switchSidebarTab('orbit')" id = "sbTabOrbit" > ⚛< / button >
< button class = "sb-tab" data-tab = "designer" onclick = "switchSidebarTab('designer')" id = "sbTabDesigner" > Design< / button >
< button class = "sb-tab active" data-tab = "info" onclick = "switchSidebarTab('info')" title = "Info" > ℹ ️ < / button >
< button class = "sb-tab" data-tab = "generate" onclick = "switchSidebarTab('generate')" title = "Create" > 🪄 < / button >
< button class = "sb-tab" data-tab = "faceswap" onclick = "switchSidebarTab('faceswap')" title = "Faceswap" > 👤< / button >
< button class = "sb-tab" data-tab = "scenery" onclick = "switchSidebarTab('scenery')" title = "Scenery" > 🎬< / button >
< button class = "sb-tab" data-tab = "segment" onclick = "switchSidebarTab('segment')" title = "Segment / Background" > α < / button >
< button class = "sb-tab" data-tab = "orbit" onclick = "switchSidebarTab('orbit')" id = "sbTabOrbit" title = "Orbit 3D" > ⚛< / button >
< button class = "sb-tab" data-tab = "designer" onclick = "switchSidebarTab('designer')" id = "sbTabDesigner" title = "Design / Refine" > 🎨 < / button >
< / div >
< div class = "sb-panels" >
<!-- Info panel: static HTML with existing lb* IDs so updateStudio() just works -->
@@ -2355,7 +2397,6 @@
< / div >
< / div >
< / div >
< / div >
<!-- Orbit preview modal -->
< div id = "orbitModal" >
@@ -2503,6 +2544,36 @@
// --- HYDRATION_START ---
const PRELOADED _IMAGES = [
"20260701_051754_image.png" ,
"20260701_051737_image.png" ,
"20260701_051721_image.png" ,
"20260701_051704_image.png" ,
"20260701_051647_image.png" ,
"20260701_051614_image.png" ,
"20260701_051541_image.png" ,
"20260701_051520_image.png" ,
"20260701_050249_image.png" ,
"20260701_050232_image.png" ,
"20260701_050215_image.png" ,
"20260701_050158_image.png" ,
"20260701_050141_image.png" ,
"20260701_050124_image.png" ,
"20260701_050108_image.png" ,
"20260701_050051_image.png" ,
"20260701_050035_image.png" ,
"20260701_050018_image.png" ,
"20260701_050001_image.png" ,
"20260701_045945_image.png" ,
"20260701_045928_image.png" ,
"20260701_045912_image.png" ,
"20260701_045838_image.png" ,
"20260701_045822_image.png" ,
"20260701_045805_image.png" ,
"20260701_045749_image.png" ,
"20260701_045732_image.png" ,
"20260701_045716_image.png" ,
"20260701_045659_1_20260619_202924_image.png" ,
"20260620_014953_image.png" ,
"20260630_130454_sc_0_20260622_101128_image.nobg.png" ,
"20260630_131638_sc_2_20260622_101128_image.nobg.nobg.png" ,
"20260630_151502_sc_0_20260622_101128_image.nobg.png" ,
@@ -7232,7 +7303,6 @@
"20260620_020313_2_20260618_172500_image.png" ,
"20260620_020302_2_20260618_172500_image.png" ,
"20260620_020251_2_20260618_172500_image.png" ,
"20260620_014953_image.png" ,
"20260620_014646_image.png" ,
"20260620_014349_image.png" ,
"20260620_014239_img_77.png" ,
@@ -7943,6 +8013,7 @@
// ============================================
let autoRefreshTimer = null ;
let isInitialLoad = true ;
let knownFiles = new Set ( ) ;
let currentFiles = [ ] ;
let currentGroups = [ ] ;
@@ -8175,6 +8246,7 @@
// --- studio view (replaces lightbox) ---
let lbUrls = [ ] , lbNames = [ ] , lbIdx = 0 ;
let _lastRenderedFilename = null ;
let _activeFilmstripTab = 'active' ; // 'active', 'group', 'hidden', 'source', 'archived'
let _multiSelectModeActive = false ;
const _selectedFilenames = new Set ( ) ;
@@ -9123,6 +9195,23 @@
function updateStudio ( ) {
const fname = lbNames [ lbIdx ] ;
if ( fname && fname !== _lastRenderedFilename ) {
// Image changed, force cancel any active text areas/edits
_lastRenderedFilename = fname ;
const genPromptEl = document . getElementById ( 'lbGenPrompt' ) ;
if ( genPromptEl ) {
genPromptEl . innerHTML = '' ;
genPromptEl . onclick = ( ) => editMetadataField ( 'prompt' ) ;
}
const descEl = document . getElementById ( 'lbDescription' ) ;
if ( descEl ) {
descEl . innerHTML = '' ;
descEl . onclick = ( ) => editMetadataField ( 'description' ) ;
}
} else if ( ! fname ) {
_lastRenderedFilename = null ;
}
if ( document . getElementById ( 'poseCanvas' ) ) _removePoseOverlay ( ) ; // skeleton is per-image
document . getElementById ( 'poseResults' ) ? . remove ( ) ;
@@ -9256,6 +9345,9 @@
const nameInput = document . getElementById ( 'lbNameInput' ) ;
if ( nameInput ) { nameInput . value = imageNames [ fname ] || '' ; nameInput . placeholder = fname ; }
const topbarNameInput = document . getElementById ( 'lbTopbarImageNameInput' ) ;
if ( topbarNameInput ) { topbarNameInput . value = imageNames [ fname ] || '' ; topbarNameInput . placeholder = fname ; }
const clipEl = document . getElementById ( 'lbClipDesc' ) ;
if ( clipEl ) clipEl . textContent = clipDescriptions [ fname ] || '' ;
@@ -9682,6 +9774,8 @@
} ) ;
if ( r . ok ) {
imageNames [ fname ] = newName ;
const topbarNameInput = document . getElementById ( 'lbTopbarImageNameInput' ) ;
if ( topbarNameInput ) topbarNameInput . value = newName ;
showToast ( ` Name updated ` ) ;
}
} catch ( e ) {
@@ -9689,6 +9783,18 @@
}
}
async function updateTopbarImageName ( ) {
const fname = lbNames [ lbIdx ] ;
const topbarNameInput = document . getElementById ( 'lbTopbarImageNameInput' ) ;
if ( ! topbarNameInput ) return ;
const newName = topbarNameInput . value ;
const nameInput = document . getElementById ( 'lbNameInput' ) ;
if ( nameInput ) nameInput . value = newName ;
await updateName ( ) ;
}
async function updateGroupName ( ) {
const gid = lbCurrentGid ;
if ( ! gid ) return ;
@@ -11375,10 +11481,21 @@
async function _pollTurntableBadge ( ) {
try {
let d = null ;
try {
const r = await fetch ( ` ${ API } /output/_data/turntable_status.json?t= ` + Date . now ( ) , { signal : AbortSignal . timeout ( 2000 ) } ) ;
if ( r . ok ) d = await r . json ( ) ;
} catch ( _ ) { }
const gid = lbCurrentGid ;
if ( gid ) {
try {
const r = await fetch ( ` ${ API } /turntable/status/ ${ encodeURIComponent ( gid ) } ` ) ;
if ( r . ok ) d = await r . json ( ) ;
} catch ( _ ) { }
}
if ( ! d ) {
try {
const r = await fetch ( ` ${ API } /output/_data/turntable_status.json?t= ` + Date . now ( ) , { signal : AbortSignal . timeout ( 2000 ) } ) ;
if ( r . ok ) d = await r . json ( ) ;
} catch ( _ ) { }
}
if ( ! d ) {
const r = await fetch ( ` ${ API } /turntable/status ` ) ;
if ( r . ok ) d = await r . json ( ) ;
@@ -11389,7 +11506,7 @@
const tbsDot = document . getElementById ( 'tbsDot' ) ;
const tbsText = document . getElementById ( 'tbsText' ) ;
const total = d . n _total || 0 ;
const done = d . n _complete || 0 ;
const done = ( d . n _complete !== undefined ? d . n _complete : d . n _done ) || 0 ;
if ( total === 0 ) { badge . style . display = 'none' ; }
else if ( done < total ) { badge . textContent = ` ${ done } / ${ total } ` ; badge . style . display = '' ; }
else { badge . style . display = 'none' ; }
@@ -11413,8 +11530,15 @@
// Refresh orbit tab badge on the tab button
const tabBtn = document . getElementById ( 'sbTabOrbit' ) ;
if ( tabBtn && done > 0 ) {
tabBtn . textContent = ` Orbit${ done < total ? ' (' + done + '/' + total + ')' : ' ✓' } ` ;
if ( tabBtn ) {
tabBtn . title = ` Orbit 3D ( ${ done } / ${ total } ) ` ;
if ( total > 0 && done < total ) {
tabBtn . innerHTML = ` ⚛<span style="font-size: 8px; vertical-align: super; background: #fbbf24; color: #1e1b4b; padding: 1px 3px; border-radius: 6px; margin-left: 1px; display: inline-block; line-height: 1; font-weight: bold;"> ${ done } / ${ total } </span> ` ;
} else if ( total > 0 && done === total ) {
tabBtn . innerHTML = ` ⚛<span style="font-size: 8px; vertical-align: super; background: #10b981; color: #fff; padding: 1px 3px; border-radius: 6px; margin-left: 1px; display: inline-block; line-height: 1; font-weight: bold;">✓</span> ` ;
} else {
tabBtn . innerHTML = ` ⚛ ` ;
}
}
} catch ( _ ) { }
}
@@ -12155,7 +12279,8 @@
}
document . getElementById ( 'count' ) . textContent = ` ${ currentGroups . length } group ${ currentGroups . length !== 1 ? 's' : '' } · ${ fileObjects . length } images ` ;
if ( newFileNames . size > 0 ) showToast ( ` ${ newFileNames . size } new image ${ newFileNames . size !== 1 ? 's' : '' } detected ` , 'success' ) ;
if ( newFileNames . size > 0 && ! isInitialLoad ) showToast ( ` ${ newFileNames . size } new image ${ newFileNames . size !== 1 ? 's' : '' } detected ` , 'success' ) ;
isInitialLoad = false ;
// If studio is open, sync lbUrls/lbNames from freshly-loaded groupData
const studioEl = document . getElementById ( 'studio' ) ;
@@ -12419,18 +12544,36 @@
_backendOnline = true ;
_modelEngineOnline = data . model _engine . online ;
const backendClass = data . backend . busy ? 'status-dot-led busy' : 'status-dot-led online' ;
const backendTitle = data . backend . busy ? 'Backend: Active task running' : 'Backend: Idle (Online)' ;
const modelClass = data . model _engine . online ? ( data . model _engine . busy ? 'status-dot-led busy' : 'status-dot-led online' ) : 'status-dot-led offline' ;
const modelTitle = data . model _engine . online
? ( data . model _engine . busy ? 'Model-Engine: Active inference running' : 'Model-Engine: Idle (Online)' )
: 'Model-Engine: Offline (ComfyUI backend is down)' ;
// Dashboard dots
const backendDot = document . getElementById ( 'backendDot' ) ;
if ( backendDot ) {
backendDot . className = 'status-dot-led ' + ( data . backend . busy ? 'busy' : 'online' ) ;
backendDot . parentElement . title = data . backend . busy ? 'Backend: Active task running' : 'Backend: Idle (Online)' ;
backendDot . className = backendClass ;
backendDot . parentElement . title = backendTitle ;
}
const modelDot = document . getElementById ( 'modelDot' ) ;
if ( modelDot ) {
modelDot . className = 'status-dot-led ' + ( data . model _engine . online ? ( data . model _engine . busy ? 'busy' : 'online' ) : 'offline' ) ;
modelDot . parentElement . title = data . model _engine . online
? ( data . model _engine . busy ? 'Model-Engine: Active inference running' : 'Model-Engine: Idle (Online)' )
: 'Model-Engine: Offline (ComfyUI backend is down)' ;
modelDot . className = modelClass ;
modelDot . parentElement . title = modelTitle ;
}
// Studio topbar dots
const studioBackendDot = document . getElementById ( 'studioBackendDot' ) ;
if ( studioBackendDot ) {
studioBackendDot . className = backendClass ;
studioBackendDot . parentElement . title = backendTitle ;
}
const studioModelDot = document . getElementById ( 'studioModelDot' ) ;
if ( studioModelDot ) {
studioModelDot . className = modelClass ;
studioModelDot . parentElement . title = modelTitle ;
}
} else {
throw new Error ( "API responded with error" ) ;
@@ -12439,15 +12582,32 @@
_backendOnline = false ;
_modelEngineOnline = false ;
const offlineClass = 'status-dot-led offline' ;
const backendOfflineTitle = 'Backend: Offline (API server is down)' ;
const modelOfflineTitle = 'Model-Engine: Status unknown (API server is down)' ;
// Dashboard dots
const backendDot = document . getElementById ( 'backendDot' ) ;
if ( backendDot ) {
backendDot . className = 'status-dot-led offline' ;
backendDot . parentElement . title = 'B ackend: Offline (API server is down)' ;
backendDot . className = offlineClass ;
backendDot . parentElement . title = b ackendOfflineTitle ;
}
const modelDot = document . getElementById ( 'modelDot' ) ;
if ( modelDot ) {
modelDot . className = 'status-dot-led offline' ;
modelDot . parentElement . title = 'M odel-Engine: Status unknown (API server is down)' ;
modelDot . className = offlineClass ;
modelDot . parentElement . title = m odelOfflineTitle ;
}
// Studio topbar dots
const studioBackendDot = document . getElementById ( 'studioBackendDot' ) ;
if ( studioBackendDot ) {
studioBackendDot . className = offlineClass ;
studioBackendDot . parentElement . title = backendOfflineTitle ;
}
const studioModelDot = document . getElementById ( 'studioModelDot' ) ;
if ( studioModelDot ) {
studioModelDot . className = offlineClass ;
studioModelDot . parentElement . title = modelOfflineTitle ;
}
}
@@ -13007,6 +13167,14 @@
document . title = PRIV _TITLE ;
showPrivacyOverlay ( ) ;
}
// Direct-to-dashboard back link for group specific pages
if ( typeof LOAD _ONLY _GROUP _ID !== 'undefined' && LOAD _ONLY _GROUP _ID ) {
const dashLink = document . getElementById ( 'studioDashboardLink' ) ;
const galBtn = document . getElementById ( 'studioGalleryBtn' ) ;
if ( dashLink ) dashLink . style . display = 'inline-flex' ;
if ( galBtn ) galBtn . style . display = 'none' ;
}
} ) ;
async function toggleTagAction ( tagName ) {
@@ -13095,7 +13263,7 @@
fileDescriptions [ fname ] = newDesc ;
showToast ( 'Successfully updated both prompt and description!' , 'success' ) ;
updateStudio ( ) ;
refreshNow ( ) ;
refreshNow ( true ) ;
} else {
showToast ( 'Failed to update prompt or description' , 'error' ) ;
}
@@ -13120,17 +13288,19 @@
const el = document . getElementById ( contentId ) ;
if ( ! el || el . querySelector ( 'textarea' ) ) return ;
const buttonLabel = currentVal . trim ( ) === '' ? '✨ Create' : '✨ Refine' ;
el . onclick = null ;
el . innerHTML = `
<textarea id="editFieldArea" style="width:100%;min-height:80px;background:#0d0d0d;border:1px solid #333;color:#ccc;font-size:11px;font-family:monospace;padding:6px;border-radius:4px;outline:none;resize:vertical;line-height:1.4;margin-bottom:4px"> ${ currentVal } </textarea>
<textarea id="editFieldArea_ ${ field } " style="width:100%;min-height:80px;background:#0d0d0d;border:1px solid #333;color:#ccc;font-size:11px;font-family:monospace;padding:6px;border-radius:4px;outline:none;resize:vertical;line-height:1.4;margin-bottom:4px"> ${ currentVal } </textarea>
<div style="display:flex;gap:6px;justify-content:flex-end">
<button class="sb-btn" style="padding:2px 8px;font-size:10px" onclick="cancelEditField(event, ' ${ field } ')">Cancel</button>
<button class="sb-btn" style="padding:2px 8px;font-size:10px" onclick="refineField(event, ' ${ field } ')">✨ Refine </button>
<button class="sb-btn" style="padding:2px 8px;font-size:10px" onclick="refineField(event, ' ${ field } ')">${ buttonLabel } </button>
<button class="sb-btn" style="padding:2px 8px;font-size:10px;background:#10b981;color:#fff" onclick="saveEditField(event, ' ${ field } ')">Save</button>
</div>
` ;
const ta = document . getElementById ( ' editFieldArea' ) ;
const ta = document . getElementById ( ` editFieldArea_ ${ field } ` ) ;
if ( ta ) {
ta . focus ( ) ;
ta . setSelectionRange ( ta . value . length , ta . value . length ) ;
@@ -13151,7 +13321,7 @@
const fname = lbNames [ lbIdx ] ;
if ( ! fname ) return ;
const ta = document . getElementById ( ' editFieldArea' ) ;
const ta = document . getElementById ( ` editFieldArea_ ${ field } ` ) ;
if ( ! ta ) return ;
const newVal = ta . value . trim ( ) ;
@@ -13174,7 +13344,7 @@
showToast ( 'Character & scenery description updated successfully' , 'success' ) ;
}
updateStudio ( ) ;
refreshNow ( ) ;
refreshNow ( true ) ;
} else {
showToast ( 'Failed to save update' , 'error' ) ;
}
@@ -13189,24 +13359,30 @@
const fname = lbNames [ lbIdx ] ;
if ( ! fname ) return ;
const ta = document . getElementById ( ' editFieldArea' ) ;
const ta = document . getElementById ( ` editFieldArea_ ${ field } ` ) ;
if ( ! ta ) return ;
const currentVal = ta . value . trim ( ) ;
const instru ction = prompt ( ` Enter instructions to refine this ${ field } : ` , "Make it more detailed..." ) ;
const a ctionVerb = currentVal === '' ? 'create' : 'refine' ;
const instruction = prompt ( ` Enter instructions to ${ actionVerb } this ${ field } : ` , actionVerb === 'create' ? "Write a detailed description of..." : "Make it more detailed..." ) ;
if ( ! instruction ) return ;
showToast ( ' AI is refin ing your text...' , 'info' ) ;
showToast ( ` AI is generat ing your ${ field } ... ` , 'info' ) ;
try {
const r = await fetch ( ` ${ API } /refine-prompt ` , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON . stringify ( { prompt : currentVal , user _instruction : instruction } )
body : JSON . stringify ( {
prompt : currentVal ,
user _instruction : instruction ,
filename : fname ,
field : field
} )
} ) ;
if ( r . ok ) {
const data = await r . json ( ) ;
ta . value = data . refined ;
showToast ( 'Refined s uccessfully! Click Save to apply.' , 'success' ) ;
showToast ( 'S uccessfully updated ! Click Save to apply.' , 'success' ) ;
} else {
showToast ( 'Failed to refine field: ' + await r . text ( ) , 'error' ) ;
}