lindaitool commited on
Commit
dbae7ea
·
verified ·
1 Parent(s): 6f499cf

Upload templates/index.html with huggingface_hub

Browse files
Files changed (1) hide show
  1. 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)}