Browser / app.js
Hamster4Doom's picture
Update app.js
51920b1 verified
Raw History Blame Contribute Delete
11.5 kB
const CONFIG={owner:"Hamster4Doom",repo:"Krea2",branch:"main"};
const API=`https://huggingface.co/api/models/${CONFIG.owner}/${CONFIG.repo}/tree/${CONFIG.branch}`;
const REPO=`https://huggingface.co/${CONFIG.owner}/${CONFIG.repo}`;
const IMAGE_EXT=/\.(png|jpe?g|webp|avif)$/i;
const MODEL_EXT=/\.safetensors$/i;
const state={items:[],filtered:[]};
const $=id=>document.getElementById(id);
const els={grid:$("grid"),empty:$("emptyState"),emptyMessage:$("emptyMessage"),notice:$("notice"),search:$("searchInput"),sort:$("sortSelect"),results:$("resultLabel"),updated:$("updatedLabel"),count:$("totalCount"),size:$("totalSize"),downloads:$("downloadCount"),refresh:$("refreshButton"),theme:$("themeButton"),dialog:$("detailDialog"),dialogContent:$("dialogContent"),close:$("closeDialog")};
function prettyName(path){return path.split("/").pop().replace(/\.safetensors$/i,"").replace(/^(krea[-_ ]?2[-_ ]?)/i,"").replace(/[-_ ]?krea[-_ ]?2$/i,"").replace(/[_-]+/g," ").replace(/\b\w/g,c=>c.toUpperCase()).trim()||"Untitled LoRA"}
function base(path){return path.replace(/\.[^.]+$/,"").toLowerCase().replace(/(?:[-_](?:preview|cover|thumb|thumbnail|sample(?:[-_]?\d+)?))$/i,"")}
function formatBytes(bytes=0){if(!bytes)return "—";const units=["B","KB","MB","GB"];const i=Math.min(Math.floor(Math.log(bytes)/Math.log(1024)),3);return `${(bytes/1024**i).toFixed(i>1?2:0)} ${units[i]}`}
function esc(value=""){return String(value).replace(/[&<>'"]/g,c=>({"&":"&amp;","<":"&lt;",">":"&gt;","'":"&#39;",'"':"&quot;"}[c]))}
function fileUrl(path,download=false){return `${REPO}/${download?"resolve":"blob"}/${CONFIG.branch}/${path}${download?"?download=true":""}`}
function showSkeleton(){els.grid.innerHTML=Array.from({length:8},()=>'<div class="skeleton"></div>').join("")}
async function loadDownloadCount(){try{const response=await fetch(`https://huggingface.co/api/models/${CONFIG.owner}/${CONFIG.repo}`);if(!response.ok)throw new Error("stats");const data=await response.json();els.downloads.textContent=Number(data.downloads||0).toLocaleString()}catch{els.downloads.textContent="—"}}
async function listAllFiles(){let files=[],cursor="";do{const url=new URL(API);url.searchParams.set("recursive","true");url.searchParams.set("expand","true");url.searchParams.set("limit","50");if(cursor)url.searchParams.set("cursor",cursor);const response=await fetch(url,{headers:{Accept:"application/json"}});if(!response.ok)throw new Error(response.status===401||response.status===403?"private":"http");const batch=await response.json();files.push(...batch);const link=response.headers.get("link")||"";const match=link.match(/[?&]cursor=([^&>]+)>;\s*rel="next"/);cursor=match?decodeURIComponent(match[1]):""}while(cursor);return files.filter(f=>f.type==="file")}
function buildItems(files){const images=files.filter(f=>IMAGE_EXT.test(f.path));const metadata=files.filter(f=>/\.json$/i.test(f.path));return files.filter(f=>MODEL_EXT.test(f.path)).map(file=>{const stem=base(file.path);const filenameStem=stem.split("/").pop();const image=images.find(img=>base(img.path)===stem&&/[-_]preview\./i.test(img.path))||images.find(img=>base(img.path)===stem)||null;const meta=metadata.find(item=>base(item.path)===stem)||null;return{path:file.path,name:prettyName(file.path),size:file.size||0,modified:file.lastCommit?.date||file.lastModified||null,image:image?.path||null,metaPath:meta?.path||null,samples:[]}})}
async function addMetadata(items){await Promise.all(items.map(async item=>{if(!item.metaPath)return;try{const response=await fetch(fileUrl(item.metaPath,true));if(!response.ok)return;const data=await response.json();const folder=item.metaPath.includes("/")?item.metaPath.slice(0,item.metaPath.lastIndexOf("/")+1):"";const resolve=name=>name?(name.includes("/")?name:`${folder}${name}`):null;item.name=data.name||item.name;item.baseModel=data.base_model||"Krea 2";item.triggers=Array.isArray(data.triggers)?data.triggers:[];item.strength=data.recommended_strength||"";item.steps=data.training_steps||"";item.description=data.description||"";item.image=resolve(data.preview)||item.image;item.samples=(Array.isArray(data.samples)?data.samples:[]).map(resolve).filter(Boolean)}catch{}}));return items}
function applyFilters(){const query=els.search.value.trim().toLowerCase();state.filtered=state.items.filter(item=>!query||`${item.name} ${item.path} ${(item.triggers||[]).join(" ")}`.toLowerCase().includes(query));const mode=els.sort.value;state.filtered.sort((a,b)=>mode==="largest"?b.size-a.size:mode==="newest"?new Date(b.modified||0)-new Date(a.modified||0):a.name.localeCompare(b.name));render()}
function render(){els.grid.innerHTML="";els.results.textContent=`${state.filtered.length} of ${state.items.length} LoRA${state.items.length===1?"":"s"}`;els.empty.classList.toggle("hidden",state.filtered.length>0);if(!state.filtered.length){els.emptyMessage.textContent=state.items.length?"No LoRAs match that search.":"Upload .safetensors files to the Krea2 repository and they will appear here automatically.";return}const fragment=document.createDocumentFragment();state.filtered.forEach((item,index)=>{const article=document.createElement("article");article.className="card";article.tabIndex=0;article.setAttribute("role","button");article.setAttribute("aria-label",`View ${item.name}`);article.innerHTML=`<div class="card-image">${item.image?`<img src="${esc(fileUrl(item.image,true))}" alt="${esc(item.name)} preview" loading="lazy">`:`<div class="placeholder"><span>AR</span></div>`}<span class="format-badge">KREA 2</span></div><div class="card-body"><h2 class="card-title" title="${esc(item.name)}">${esc(item.name)}</h2><div class="card-meta"><span>.safetensors</span><span>${formatBytes(item.size)}</span></div></div><span class="card-arrow">↗</span>`;article.addEventListener("click",()=>openDetail(index));article.addEventListener("keydown",e=>{if(e.key==="Enter"||e.key===" "){e.preventDefault();openDetail(index)}});fragment.append(article)});els.grid.append(fragment)}
function openLightbox(item,gallery,startIndex=0){let current=startIndex;const overlay=document.createElement("div");overlay.className="gallery-lightbox";overlay.setAttribute("role","dialog");overlay.setAttribute("aria-modal","true");overlay.setAttribute("aria-label",`${item.name} image gallery`);
const draw=()=>{const path=gallery[current];overlay.innerHTML=`<button class="lightbox-close" type="button" aria-label="Close gallery">×</button><button class="lightbox-nav lightbox-prev" type="button" aria-label="Previous image">‹</button><div class="lightbox-stage"><img src="${esc(fileUrl(path,true))}" alt="${esc(item.name)} ${current===0?"preview":`sample ${current}`}"><div class="lightbox-counter">${current+1} / ${gallery.length}</div></div><button class="lightbox-nav lightbox-next" type="button" aria-label="Next image">›</button>`;overlay.querySelector(".lightbox-close").addEventListener("click",close);overlay.querySelector(".lightbox-prev").addEventListener("click",()=>move(-1));overlay.querySelector(".lightbox-next").addEventListener("click",()=>move(1))};
const move=delta=>{current=(current+delta+gallery.length)%gallery.length;draw()};const onKey=e=>{if(e.key==="Escape")close();if(e.key==="ArrowLeft")move(-1);if(e.key==="ArrowRight")move(1)};const close=()=>{document.removeEventListener("keydown",onKey);overlay.remove()};overlay.addEventListener("click",e=>{if(e.target===overlay)close()});document.addEventListener("keydown",onKey);els.dialog.appendChild(overlay);draw()}
function openDetail(index){
const item = state.filtered[index];
const date = item.modified
? new Date(item.modified).toLocaleDateString(undefined, {year:"numeric", month:"short", day:"numeric"})
: "—";
const gallery = [item.image, ...(item.samples||[])].filter(Boolean);
const media = gallery.length
? `<div class="detail-gallery">${gallery.map((path,i)=>`<button class="gallery-image-button ${i===0?"gallery-hero":""}" type="button" data-gallery-index="${i}" aria-label="Open ${esc(item.name)} ${i?"sample "+i:"preview"} full size"><img src="${esc(fileUrl(path,true))}" alt="${esc(item.name)} ${i?`sample ${i}`:"preview"}" loading="lazy"></button>`).join("")}</div>`
: `<div class="placeholder"><span>AR</span></div>`;
const description = (item.description || "").trim() || "—";
els.dialogContent.innerHTML = `<div class="dialog-layout"><div class="dialog-media">${media}</div><div class="dialog-info">
<div class="eyebrow"><span></span>${esc(item.baseModel||"Krea 2")} CHARACTER LORA</div>
<h2>${esc(item.name)}</h2>
<div class="filename">${esc(item.path)}</div>
<div class="detail-list">
<div><span>Recommended strength</span><strong>${esc(item.strength||"—")}</strong></div>
<div><span>File size</span><strong>${formatBytes(item.size)}</strong></div>
<div><span>Updated</span><strong>${date}</strong></div>
</div>
<div class="detail-description">
<span>Description</span>
<p>${esc((item.description||"").trim() || "—")}</p>
</div>
<div class="detail-actions">
<a class="primary-action" href="${esc(fileUrl(item.path,true))}">↓ Download LoRA</a>
<a class="secondary-action" href="${esc(fileUrl(item.path))}" target="_blank" rel="noopener">View on Hugging Face ↗</a>
<button class="secondary-action" id="copyFilename" type="button">Copy filename</button>
</div>
</div></div>`;
els.dialog.showModal();
els.dialogContent.querySelectorAll("[data-gallery-index]").forEach(button =>
button.addEventListener("click", () => openLightbox(item, gallery, Number(button.dataset.galleryIndex)))
);
$("copyFilename").addEventListener("click", async e => {
await navigator.clipboard.writeText(item.path.split("/").pop());
e.currentTarget.textContent = "Copied ✓";
});
}
async function load(){showSkeleton();els.notice.classList.add("hidden");els.refresh.disabled=true;try{const files=await listAllFiles();state.items=await addMetadata(buildItems(files));els.count.textContent=state.items.length;els.size.textContent=formatBytes(state.items.reduce((sum,item)=>sum+item.size,0));els.updated.textContent=`Checked ${new Date().toLocaleTimeString([],{hour:"numeric",minute:"2-digit"})}`;applyFilters()}catch(error){state.items=[];els.grid.innerHTML="";els.count.textContent="0";els.size.textContent="—";els.results.textContent="Collection unavailable";els.notice.classList.remove("hidden");els.notice.textContent=error.message==="private"?"The Krea2 model repository is private. Make it public so this public Static Space can read and download its LoRAs.":"The Hugging Face repository could not be reached. Try Refresh in a moment.";els.empty.classList.remove("hidden");els.emptyMessage.textContent="The catalog will populate automatically when the repository is accessible."}finally{els.refresh.disabled=false}}
els.search.addEventListener("input",applyFilters);els.sort.addEventListener("change",applyFilters);els.refresh.addEventListener("click",()=>{loadDownloadCount();load()});els.close.addEventListener("click",()=>els.dialog.close());els.dialog.addEventListener("click",e=>{if(e.target===els.dialog)els.dialog.close()});document.addEventListener("keydown",e=>{if(e.key==="/"&&document.activeElement!==els.search){e.preventDefault();els.search.focus()}});els.theme.addEventListener("click",()=>{const next=document.documentElement.dataset.theme==="light"?"dark":"light";document.documentElement.dataset.theme=next;localStorage.setItem("theme",next)});document.documentElement.dataset.theme=localStorage.getItem("theme")||"dark";loadDownloadCount();load();