Fix scale-selection 500 error (missing BytesIO import) and compact GPU badge

ai_tools.py: add 'from io import BytesIO' — scale_selection and paste
endpoints used BytesIO directly but it was only imported locally in one
unrelated function, causing NameError on every scale call.

provider-badge.js: redesign badge to fit the narrow (~40px) left toolbar.
Was rendering 'GPU · sdxl_offload · Quadro RTX 3000' inline which wrapped
into multiple lines covering tool icons. Now shows a status dot + short
label (SDXL / OAI / Rep…) with all details moved to the hover tooltip.

https://claude.ai/code/session_01WVDg7amsy1TTtxvpku7bcM
This commit is contained in:
Claude
2026-06-13 23:33:07 +00:00
parent 34d9f5aafc
commit 80830beae2
2 changed files with 45 additions and 39 deletions
+1
View File
@@ -10,6 +10,7 @@ from typing import Optional
import base64 import base64
import asyncio import asyncio
import json import json
from io import BytesIO
from app.services.local_inpaint import ( from app.services.local_inpaint import (
lama_inpaint, opencv_inpaint, lama_available, gpu_available, rembg_available, lama_inpaint, opencv_inpaint, lama_available, gpu_available, rembg_available,
@@ -1,10 +1,6 @@
/** /**
* ProviderBadge — small DOM element showing the active AI provider. * ProviderBadge — compact status indicator in the left toolbar footer.
* Inserted into the toolbar footer on app load. * Shows a dot + 3-5 char label; all details in the tooltip.
*
* Green = remote provider healthy (or local_gpu active)
* Yellow = provider configured but unhealthy/unreachable
* Grey = local only (LaMa + OpenCV)
*/ */
import { getCapabilities } from '../../api/capabilities.js'; import { getCapabilities } from '../../api/capabilities.js';
@@ -15,84 +11,74 @@ export async function mountProviderBadge(container) {
var badge = document.createElement('div'); var badge = document.createElement('div');
badge.id = 'provider-badge'; badge.id = 'provider-badge';
badge.style.cssText = [ badge.style.cssText = [
'display:inline-flex', 'align-items:center', 'gap:5px', 'display:flex', 'flex-direction:column', 'align-items:center', 'gap:2px',
'padding:3px 8px', 'border-radius:10px', 'padding:4px 2px 4px',
'font-size:11px', 'font-family:sans-serif', 'font-size:9px', 'font-family:sans-serif', 'line-height:1.2',
'cursor:default', 'user-select:none', 'cursor:default', 'user-select:none',
'margin:4px', 'opacity:0.85', 'width:100%', 'box-sizing:border-box',
'text-align:center', 'word-break:break-word',
].join(';'); ].join(';');
var dot = document.createElement('span'); var dot = document.createElement('span');
dot.style.cssText = 'width:7px;height:7px;border-radius:50%;display:inline-block;'; dot.style.cssText = 'width:8px;height:8px;border-radius:50%;display:block;flex-shrink:0;';
var label = document.createElement('span'); var label = document.createElement('span');
label.style.cssText = 'color:inherit;max-width:36px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:block;';
var remote = caps.remote || {}; var remote = caps.remote || {};
var local = caps.local || {}; var local = caps.local || {};
if (remote.provider === 'local_gpu') { if (remote.provider === 'local_gpu') {
// Local GPU provider — show GPU name and tier from /api/config local fields
var gpuName = _shortGpuName(local.gpu_device); var gpuName = _shortGpuName(local.gpu_device);
var tier = local.gpu_tier || ''; var tier = local.gpu_tier || '';
if (remote.healthy) { if (remote.healthy) {
dot.style.background = '#44cc44'; dot.style.background = '#44cc44';
badge.style.background = '#1a2a1a';
badge.style.color = '#aaffaa'; badge.style.color = '#aaffaa';
label.textContent = 'GPU · ' + tier + ' · ' + gpuName; label.textContent = _shortTier(tier);
var flagList = [ var flagList = [
local.gpu_fp16 && 'fp16', local.gpu_fp16 && 'fp16',
local.gpu_bf16 && 'bf16', local.gpu_bf16 && 'bf16',
local.gpu_fp8 && 'fp8', local.gpu_fp8 && 'fp8',
local.gpu_tensor_cores && 'tensor-cores', local.gpu_tensor_cores && 'TC',
].filter(Boolean).join(' '); ].filter(Boolean).join(' ');
badge.title = [ badge.title = [
local.gpu_device || gpuName, gpuName,
'VRAM: ' + local.gpu_vram_total + ' GB total ' + local.gpu_vram_free + ' GB free', 'VRAM: ' + local.gpu_vram_total + ' GB total / ' + local.gpu_vram_free + ' GB free',
'Compute: CC ' + local.gpu_cc + ' Eff: ' + local.gpu_eff_vram + ' GB', 'CC: ' + local.gpu_cc + ' Eff VRAM: ' + local.gpu_eff_vram + ' GB',
flagList ? 'Features: ' + flagList : '', flagList ? 'Flags: ' + flagList : '',
'Capabilities: ' + (local.local_gpu_capabilities || []).join(', '), tier ? 'Tier: ' + tier : '',
(local.local_gpu_warnings || []).length (local.local_gpu_warnings || []).length
? '\nWarnings:\n' + local.local_gpu_warnings.join('\n') ? 'Warnings:\n' + local.local_gpu_warnings.join('\n')
: '', : '',
].filter(Boolean).join('\n'); ].filter(Boolean).join('\n');
} else { } else {
dot.style.background = '#ffaa00'; dot.style.background = '#ffaa00';
badge.style.background = '#2a2000';
badge.style.color = '#ffdd88'; badge.style.color = '#ffdd88';
label.textContent = 'Local GPU (not ready)'; label.textContent = 'GPU?';
badge.title = 'local_gpu is configured but the diffusers library may not be installed.\nCheck container logs for details.'; badge.title = 'local_gpu configured but diffusers may not be installed.\nCheck container logs.';
} }
} else if (remote.provider && remote.healthy) { } else if (remote.provider && remote.healthy) {
dot.style.background = '#44cc44'; dot.style.background = '#44cc44';
badge.style.background = '#1a2a1a';
badge.style.color = '#aaffaa'; badge.style.color = '#aaffaa';
label.textContent = _shortProvider(remote.provider);
var overrides = remote.overrides || {};
var overrideEntries = Object.entries(overrides).filter(([, v]) => v);
var overrideStr = overrideEntries.length
? ' · ' + overrideEntries.map(([k, v]) => k + '→' + v).join(', ')
: '';
label.textContent = remote.provider + overrideStr + (local.gpu_detected ? ' · GPU' : '');
var opLines = Object.entries(remote.operations || {}) var opLines = Object.entries(remote.operations || {})
.map(([op, s]) => op + ': ' + (s.provider || remote.provider) + ' ' + (s.healthy ? '✓' : '✗')) .map(([op, s]) => op + ': ' + (s.provider || remote.provider) + ' ' + (s.healthy ? '✓' : '✗'))
.join('\n'); .join('\n');
badge.title = opLines || ('Provider: ' + remote.provider); badge.title = ('Provider: ' + remote.provider) + (opLines ? '\n' + opLines : '');
} else if (remote.provider && !remote.healthy) { } else if (remote.provider && !remote.healthy) {
dot.style.background = '#ffaa00'; dot.style.background = '#ffaa00';
badge.style.background = '#2a2000';
badge.style.color = '#ffdd88'; badge.style.color = '#ffdd88';
label.textContent = remote.provider + ' (offline)'; label.textContent = _shortProvider(remote.provider) + '?';
badge.title = remote.provider + ' is configured but not reachable. Check your .env URL.'; badge.title = remote.provider + ' configured but not reachable.\nCheck your .env URL.';
} else { } else {
dot.style.background = '#888888'; dot.style.background = '#888888';
badge.style.background = '#1a1a1a';
badge.style.color = '#aaaaaa'; badge.style.color = '#aaaaaa';
label.textContent = 'Local' + (local.lama ? ' · LaMa' : '') + (local.gpu_detected ? ' · GPU' : ''); label.textContent = local.lama ? 'LaMa' : 'Local';
badge.title = 'Local only. Set AI_PROVIDER in .env to enable generative tools.'; badge.title = 'Local only (no generative AI).\nSet AI_PROVIDER in .env to enable.';
} }
badge.appendChild(dot); badge.appendChild(dot);
@@ -108,6 +94,25 @@ export async function mountProviderBadge(container) {
function _shortGpuName(name) { function _shortGpuName(name) {
return (name || 'GPU') return (name || 'GPU')
.replace(/^NVIDIA GeForce\s+/i, '') .replace(/^NVIDIA GeForce\s+/i, '')
.replace(/^NVIDIA Quadro\s+/i, '')
.replace(/^NVIDIA\s+/i, '') .replace(/^NVIDIA\s+/i, '')
.replace(/^AMD Radeon\s+/i, ''); .replace(/^AMD Radeon\s+/i, '');
} }
function _shortTier(tier) {
if (!tier) return 'GPU';
// sdxl_offload → SDXL, flux → FLUX, sd15 → SD15
return tier
.replace(/_offload$/, '')
.replace(/_cpu$/, '')
.toUpperCase()
.slice(0, 6);
}
function _shortProvider(p) {
var map = {
openai: 'OAI', replicate: 'Rep', stability: 'Stab',
invokeai: 'Inv', comfyui: 'CUI', local_gpu: 'GPU',
};
return map[p] || (p || 'AI').slice(0, 4);
}