Upload templates/index.html with huggingface_hub
Browse files- templates/index.html +212 -0
templates/index.html
CHANGED
|
@@ -167,6 +167,25 @@ select{cursor:pointer;appearance:none;background-image:url("data:image/svg+xml,%
|
|
| 167 |
.toast.error{border-color:rgba(239,68,68,.3);color:var(--red)}
|
| 168 |
.toast.success{border-color:rgba(34,197,94,.3);color:var(--green)}
|
| 169 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
@media(max-width:640px){
|
| 171 |
.input-row,.input-row-3{grid-template-columns:1fr}
|
| 172 |
.gen-grid{grid-template-columns:repeat(auto-fill,minmax(150px,1fr))}
|
|
@@ -288,6 +307,67 @@ select{cursor:pointer;appearance:none;background-image:url("data:image/svg+xml,%
|
|
| 288 |
<button class="btn btn-secondary" onclick="resetAll()">生成新一组</button>
|
| 289 |
</div>
|
| 290 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 291 |
</div>
|
| 292 |
|
| 293 |
<div class="toast" id="toast"></div>
|
|
@@ -522,6 +602,138 @@ function resetAll(){
|
|
| 522 |
goToStep(0);
|
| 523 |
}
|
| 524 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 525 |
// ── Utils ──
|
| 526 |
function esc(s){const d=document.createElement('div');d.textContent=s;return d.innerHTML}
|
| 527 |
function toast(msg,type=''){const t=$('toast');t.textContent=msg;t.className='toast show '+type;setTimeout(()=>t.className='toast',4000)}
|
|
|
|
| 167 |
.toast.error{border-color:rgba(239,68,68,.3);color:var(--red)}
|
| 168 |
.toast.success{border-color:rgba(34,197,94,.3);color:var(--green)}
|
| 169 |
|
| 170 |
+
.section-divider{display:flex;align-items:center;gap:1rem;margin:3rem 0 1.5rem;color:var(--text-3);font-size:.8rem;text-transform:uppercase;letter-spacing:.08em}
|
| 171 |
+
.section-divider::before,.section-divider::after{content:'';flex:1;height:1px;background:var(--border)}
|
| 172 |
+
.custom-section{border-color:var(--border-light)}
|
| 173 |
+
.custom-intro{font-size:.88rem;color:var(--text-2);margin-bottom:1rem;line-height:1.55}
|
| 174 |
+
.kind-row{display:flex;flex-wrap:wrap;gap:.6rem;margin-bottom:1rem}
|
| 175 |
+
.kind-pill{display:inline-flex;align-items:center;gap:.4rem;padding:.45rem .85rem;border-radius:999px;border:1px solid var(--border);cursor:pointer;font-size:.85rem;transition:background .2s,border-color .2s}
|
| 176 |
+
.kind-pill:has(input:checked){border-color:var(--accent);background:var(--accent-dim);color:var(--accent)}
|
| 177 |
+
.kind-pill input{accent-color:var(--accent)}
|
| 178 |
+
.kind-pill small{opacity:.65;font-size:.72rem}
|
| 179 |
+
.style-details{margin:.75rem 0 1rem;font-size:.82rem;color:var(--text-2)}
|
| 180 |
+
.style-details summary{cursor:pointer;color:var(--text);padding:.25rem 0}
|
| 181 |
+
.style-blurbs{margin-top:.5rem;padding-left:1rem;border-left:2px solid var(--border)}
|
| 182 |
+
.style-blurbs code{font-size:.75rem;background:var(--surface-2);padding:.1rem .3rem;border-radius:4px}
|
| 183 |
+
.custom-progress{margin-top:1rem;padding:.75rem;font-size:.88rem;color:var(--text-2)}
|
| 184 |
+
.custom-result{margin-top:1.25rem;padding:1rem;background:var(--surface-2);border-radius:var(--radius);border:1px solid var(--border)}
|
| 185 |
+
.custom-result img{max-width:100%;border-radius:var(--radius-sm);display:block}
|
| 186 |
+
.custom-result .dl-row{margin-top:.75rem;display:flex;gap:.5rem;flex-wrap:wrap}
|
| 187 |
+
.hidden{display:none!important}
|
| 188 |
+
|
| 189 |
@media(max-width:640px){
|
| 190 |
.input-row,.input-row-3{grid-template-columns:1fr}
|
| 191 |
.gen-grid{grid-template-columns:repeat(auto-fill,minmax(150px,1fr))}
|
|
|
|
| 307 |
<button class="btn btn-secondary" onclick="resetAll()">生成新一组</button>
|
| 308 |
</div>
|
| 309 |
</div>
|
| 310 |
+
|
| 311 |
+
<!-- ═══ Custom: cover / feature / how-to (always visible below) ═══ -->
|
| 312 |
+
<div class="section-divider">
|
| 313 |
+
<span>更多生成</span>
|
| 314 |
+
</div>
|
| 315 |
+
|
| 316 |
+
<div class="card custom-section" id="custom-section">
|
| 317 |
+
<div class="card-title">
|
| 318 |
+
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>
|
| 319 |
+
营销图生成(封面 / 功能 / How-to)
|
| 320 |
+
<span class="badge badge-optional">单张 · 参考你上传的图 + 内置风格库</span>
|
| 321 |
+
</div>
|
| 322 |
+
<p class="custom-intro">选择类型后填写英文或中文需求,并上传参考图(可选,多张)。系统将<strong>合并</strong>你提供的参考与对应类型的<strong>默认风格参考包</strong>,按类型强制应用版式与视觉规则。</p>
|
| 323 |
+
|
| 324 |
+
<div class="kind-row">
|
| 325 |
+
<label class="kind-pill"><input type="radio" name="custom-kind" value="cover" checked> 封面图 <small>16:9</small></label>
|
| 326 |
+
<label class="kind-pill"><input type="radio" name="custom-kind" value="feature"> 功能图</label>
|
| 327 |
+
<label class="kind-pill"><input type="radio" name="custom-kind" value="howto"> How-to 图</label>
|
| 328 |
+
</div>
|
| 329 |
+
|
| 330 |
+
<div class="input-group">
|
| 331 |
+
<label>生成要求(prompt)</label>
|
| 332 |
+
<textarea id="custom-prompt" rows="4" placeholder="例如:A hero banner for an AI writing assistant, showing a laptop and floating text bubbles, tagline space at top, midnight blue and gold palette…"></textarea>
|
| 333 |
+
<div class="input-hint" id="custom-kind-hint">封面图:宽幅 hero,预留标题区;输出比例 <strong>16:9</strong>。</div>
|
| 334 |
+
</div>
|
| 335 |
+
|
| 336 |
+
<div class="card" style="background:var(--surface-2);margin-bottom:1rem;padding:1rem">
|
| 337 |
+
<div class="card-title" style="font-size:.95rem;margin-bottom:.5rem">参考图片(可选,多张)</div>
|
| 338 |
+
<div class="upload-zone" id="custom-upload-zone">
|
| 339 |
+
<svg class="uz-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
|
| 340 |
+
<h3>拖拽或点击上传风格参考</h3>
|
| 341 |
+
<p>可与内置风格包一起使用;最多 6 张</p>
|
| 342 |
+
</div>
|
| 343 |
+
<input type="file" id="custom-file-input" accept="image/jpeg,image/png,image/webp" multiple>
|
| 344 |
+
<div class="preview-grid" id="custom-preview-grid"></div>
|
| 345 |
+
<div class="upload-status" id="custom-upload-status"></div>
|
| 346 |
+
</div>
|
| 347 |
+
|
| 348 |
+
<details class="style-details">
|
| 349 |
+
<summary>内置风格规则说明(按类型)</summary>
|
| 350 |
+
<div class="style-blurbs">
|
| 351 |
+
<p><strong>封面图</strong>:参考 <code>coverreference</code> — 宽幅主视觉、产品/场景层次、电影��光效、预留标题负空间、偏电商/科技发布质感。</p>
|
| 352 |
+
<p><strong>功能图</strong>:参考 <code>features-references</code> — 单点卖点、轻渐变或浅底、图标/抽象 UI、卡片化层次、SaaS 说明图气质。</p>
|
| 353 |
+
<p><strong>How-to</strong>:参考 <code>howtoreferences</code> — 步骤感、教学清晰、单动作焦点、留白与对齐、教程卡片风。</p>
|
| 354 |
+
</div>
|
| 355 |
+
</details>
|
| 356 |
+
|
| 357 |
+
<div class="actions" style="justify-content:flex-start">
|
| 358 |
+
<button class="btn btn-primary" id="btn-custom-gen" onclick="startCustomGeneration()">
|
| 359 |
+
<svg class="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
|
| 360 |
+
生成单张图
|
| 361 |
+
</button>
|
| 362 |
+
</div>
|
| 363 |
+
|
| 364 |
+
<div id="custom-progress" class="custom-progress hidden">
|
| 365 |
+
<div class="spinner spinner-sm" style="display:inline-block;vertical-align:middle;margin-right:.5rem"></div>
|
| 366 |
+
<span id="custom-progress-msg">准备中…</span>
|
| 367 |
+
</div>
|
| 368 |
+
|
| 369 |
+
<div id="custom-result" class="custom-result hidden"></div>
|
| 370 |
+
</div>
|
| 371 |
</div>
|
| 372 |
|
| 373 |
<div class="toast" id="toast"></div>
|
|
|
|
| 602 |
goToStep(0);
|
| 603 |
}
|
| 604 |
|
| 605 |
+
// ── Custom section (cover / feature / howto) ──
|
| 606 |
+
let customFiles=[];
|
| 607 |
+
let customPublicUrls=[];
|
| 608 |
+
let customTaskId=null;
|
| 609 |
+
let customPollTimer=null;
|
| 610 |
+
|
| 611 |
+
const customHint=$('custom-kind-hint');
|
| 612 |
+
document.querySelectorAll('input[name="custom-kind"]').forEach(r=>{
|
| 613 |
+
r.addEventListener('change',()=>{
|
| 614 |
+
if(r.value==='cover')customHint.innerHTML='封面图:宽幅 hero,预留标题区;输出比例 <strong>16:9</strong>。';
|
| 615 |
+
else if(r.value==='feature')customHint.innerHTML='功能图:版式与气质参考内置功能图包;宽高比 <strong>与参考图一致(auto)</strong>。';
|
| 616 |
+
else customHint.innerHTML='How-to 图:步骤教学风;宽高比 <strong>与参考图一致(auto)</strong>。';
|
| 617 |
+
});
|
| 618 |
+
});
|
| 619 |
+
|
| 620 |
+
const cz=$('custom-upload-zone'), cfi=$('custom-file-input'), cpg=$('custom-preview-grid');
|
| 621 |
+
cz.addEventListener('click',()=>cfi.click());
|
| 622 |
+
cz.addEventListener('dragover',e=>{e.preventDefault();cz.classList.add('dragover')});
|
| 623 |
+
cz.addEventListener('dragleave',()=>cz.classList.remove('dragover'));
|
| 624 |
+
cz.addEventListener('drop',e=>{e.preventDefault();cz.classList.remove('dragover');addCustomFiles(e.dataTransfer.files)});
|
| 625 |
+
cfi.addEventListener('change',e=>addCustomFiles(e.target.files));
|
| 626 |
+
|
| 627 |
+
function addCustomFiles(files){
|
| 628 |
+
for(const f of files){
|
| 629 |
+
if(customFiles.length>=6){toast('参考图最多 6 张','error');break}
|
| 630 |
+
if(f.size>10*1024*1024){toast(f.name+' 超过 10MB','error');continue}
|
| 631 |
+
if(!['image/jpeg','image/png','image/webp'].includes(f.type)){toast(f.name+' 格式不支持','error');continue}
|
| 632 |
+
customFiles.push(f);
|
| 633 |
+
}
|
| 634 |
+
customPublicUrls=[];
|
| 635 |
+
renderCustomPreviews();
|
| 636 |
+
}
|
| 637 |
+
|
| 638 |
+
function renderCustomPreviews(){
|
| 639 |
+
cpg.innerHTML='';
|
| 640 |
+
customFiles.forEach((f,i)=>{
|
| 641 |
+
const d=document.createElement('div');d.className='preview-item';
|
| 642 |
+
const img=document.createElement('img');img.src=URL.createObjectURL(f);
|
| 643 |
+
const btn=document.createElement('button');btn.className='preview-remove';btn.textContent='✕';
|
| 644 |
+
btn.onclick=()=>{customFiles.splice(i,1);customPublicUrls=[];renderCustomPreviews();$('custom-upload-status').textContent=''};
|
| 645 |
+
d.append(img,btn);cpg.appendChild(d);
|
| 646 |
+
});
|
| 647 |
+
}
|
| 648 |
+
|
| 649 |
+
async function startCustomGeneration(){
|
| 650 |
+
const prompt=$('custom-prompt').value.trim();
|
| 651 |
+
if(!prompt){toast('请填写生成要求','error');return}
|
| 652 |
+
const kind=document.querySelector('input[name="custom-kind"]:checked').value;
|
| 653 |
+
const btn=$('btn-custom-gen');
|
| 654 |
+
const prog=$('custom-progress'), pr=$('custom-result');
|
| 655 |
+
btn.disabled=true;
|
| 656 |
+
pr.classList.add('hidden');pr.innerHTML='';
|
| 657 |
+
prog.classList.remove('hidden');
|
| 658 |
+
$('custom-progress-msg').textContent='上传参考图中…';
|
| 659 |
+
|
| 660 |
+
let urls=[];
|
| 661 |
+
if(customFiles.length){
|
| 662 |
+
$('custom-upload-status').className='upload-status uploading';
|
| 663 |
+
$('custom-upload-status').textContent='正在上传参考图…';
|
| 664 |
+
const fd=new FormData();
|
| 665 |
+
customFiles.forEach(f=>fd.append('images',f));
|
| 666 |
+
try{
|
| 667 |
+
const res=await fetch('/api/upload-images',{method:'POST',body:fd});
|
| 668 |
+
const data=await res.json();
|
| 669 |
+
if(data.error)throw new Error(data.error);
|
| 670 |
+
urls=data.public_urls||[];
|
| 671 |
+
$('custom-upload-status').className='upload-status done';
|
| 672 |
+
$('custom-upload-status').textContent=urls.length?`✓ 已上传 ${urls.length} 张`:'';
|
| 673 |
+
}catch(e){
|
| 674 |
+
toast('参考图上传失败:'+e.message,'error');
|
| 675 |
+
$('custom-upload-status').className='upload-status error';
|
| 676 |
+
$('custom-upload-status').textContent=e.message;
|
| 677 |
+
prog.classList.add('hidden');
|
| 678 |
+
btn.disabled=false;
|
| 679 |
+
return;
|
| 680 |
+
}
|
| 681 |
+
}else{
|
| 682 |
+
$('custom-upload-status').textContent='';
|
| 683 |
+
}
|
| 684 |
+
|
| 685 |
+
customPublicUrls=urls;
|
| 686 |
+
$('custom-progress-msg').textContent='正在生成(约需 30–90 秒)…';
|
| 687 |
+
|
| 688 |
+
try{
|
| 689 |
+
const res=await fetch('/api/generate-custom',{
|
| 690 |
+
method:'POST',
|
| 691 |
+
headers:{'Content-Type':'application/json'},
|
| 692 |
+
body:JSON.stringify({kind,prompt,image_urls:urls}),
|
| 693 |
+
});
|
| 694 |
+
const data=await res.json();
|
| 695 |
+
if(data.error)throw new Error(data.error);
|
| 696 |
+
customTaskId=data.task_id;
|
| 697 |
+
if(customPollTimer)clearInterval(customPollTimer);
|
| 698 |
+
customPollTimer=setInterval(pollCustomStatus,2500);
|
| 699 |
+
}catch(e){
|
| 700 |
+
toast('启动失败:'+e.message,'error');
|
| 701 |
+
prog.classList.add('hidden');
|
| 702 |
+
}
|
| 703 |
+
btn.disabled=false;
|
| 704 |
+
}
|
| 705 |
+
|
| 706 |
+
async function pollCustomStatus(){
|
| 707 |
+
if(!customTaskId)return;
|
| 708 |
+
try{
|
| 709 |
+
const res=await fetch('/api/status/'+customTaskId);
|
| 710 |
+
const task=await res.json();
|
| 711 |
+
$('custom-progress-msg').textContent=task.message||'生成中…';
|
| 712 |
+
|
| 713 |
+
if(task.status==='completed'){
|
| 714 |
+
clearInterval(customPollTimer);customPollTimer=null;
|
| 715 |
+
prog.classList.add('hidden');
|
| 716 |
+
const img=task.images&&task.images[0];
|
| 717 |
+
const box=$('custom-result');
|
| 718 |
+
box.classList.remove('hidden');
|
| 719 |
+
if(img&&img.status==='ok'){
|
| 720 |
+
box.innerHTML=`<p style="margin-bottom:.5rem;font-weight:600;color:var(--green)">生成成功</p>
|
| 721 |
+
<img src="${img.url}" alt="result">
|
| 722 |
+
<div class="dl-row">
|
| 723 |
+
<a href="${img.url}" download="custom_${img.slot}.png" class="btn btn-primary btn-sm">下载 PNG</a>
|
| 724 |
+
</div>`;
|
| 725 |
+
}else if(img&&img.status==='error'){
|
| 726 |
+
box.innerHTML=`<p class="gc-error">${esc(img.error||'失败')}</p>`;
|
| 727 |
+
}
|
| 728 |
+
}else if(task.status==='error'){
|
| 729 |
+
clearInterval(customPollTimer);customPollTimer=null;
|
| 730 |
+
prog.classList.add('hidden');
|
| 731 |
+
$('custom-result').classList.remove('hidden');
|
| 732 |
+
$('custom-result').innerHTML=`<p class="gc-error">${esc(task.message||'错误')}</p>`;
|
| 733 |
+
}
|
| 734 |
+
}catch(e){console.error(e)}
|
| 735 |
+
}
|
| 736 |
+
|
| 737 |
// ── Utils ──
|
| 738 |
function esc(s){const d=document.createElement('div');d.textContent=s;return d.innerHTML}
|
| 739 |
function toast(msg,type=''){const t=$('toast');t.textContent=msg;t.className='toast show '+type;setTimeout(()=>t.className='toast',4000)}
|