Spaces:
Running on Zero
Running on Zero
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Qwen3.8 Studio · Object Detection</title> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap'); | |
| *{box-sizing:border-box;margin:0;padding:0} | |
| :root{ | |
| --bg:#0a0a0c;--panel:#131316;--panel2:#0e0e11;--canvas:#08080a; | |
| --border:#2a2323;--border2:#3f2f2f; | |
| --text:#e4e4e7;--muted:#a1a1aa;--dim:#6b6b75;--faint:#4a4a52; | |
| --accent:#FF4500;--accent2:#FF6347;--accent3:#FFA07A; | |
| --menubar-h:48px;--statusbar-h:30px;--rail-w:52px;--inspector-w:360px;--filmstrip-h:132px; | |
| } | |
| html,body{height:100%} | |
| body{ | |
| background:var(--bg);font-family:'Inter',system-ui,-apple-system,sans-serif; | |
| font-size:13px;color:var(--text);overflow:hidden; | |
| } | |
| /* ══ Layout grid ══ */ | |
| .studio{ | |
| display:grid;height:100vh; | |
| grid-template-rows:var(--menubar-h) 1fr var(--statusbar-h); | |
| grid-template-columns:var(--rail-w) 1fr var(--inspector-w); | |
| grid-template-areas: | |
| "menubar menubar menubar" | |
| "rail workspace inspector" | |
| "statusbar statusbar statusbar"; | |
| } | |
| /* ══ Menu bar ══ */ | |
| .menubar{ | |
| grid-area:menubar;display:flex;align-items:center;gap:14px; | |
| background:var(--panel);border-bottom:1px solid var(--border); | |
| padding:0 16px;z-index:50; | |
| } | |
| .menubar-logo{ | |
| width:28px;height:28px;background:linear-gradient(135deg,#FF4500,#FF6347,#FFA07A); | |
| border-radius:8px;display:flex;align-items:center;justify-content:center; | |
| box-shadow:0 2px 8px rgba(255,69,0,.35);flex-shrink:0; | |
| } | |
| .menubar-logo svg{width:15px;height:15px;fill:#fff} | |
| .menubar-title{font-size:14px;font-weight:700;letter-spacing:-.2px;white-space:nowrap} | |
| .menubar-title span{color:var(--dim);font-weight:500} | |
| .menubar-badge{ | |
| font-size:10px;font-weight:700;padding:2px 8px;border-radius:12px; | |
| background:rgba(255,69,0,.14);color:var(--accent2);border:1px solid rgba(255,69,0,.25); | |
| letter-spacing:.4px;white-space:nowrap; | |
| } | |
| .menubar-badge.fast{background:rgba(34,197,94,.12);color:#4ade80;border-color:rgba(34,197,94,.25)} | |
| .menubar-spacer{flex:1} | |
| .menubar-status{ | |
| display:flex;align-items:center;gap:7px;font-family:'JetBrains Mono',monospace; | |
| font-size:11px;color:var(--dim); | |
| } | |
| .menubar-status .dot{width:7px;height:7px;border-radius:50%;background:#4ade80;box-shadow:0 0 6px rgba(74,222,128,.6)} | |
| .menubar-status .dot.off{background:#f87171;box-shadow:0 0 6px rgba(248,113,113,.5)} | |
| .gh-btn{ | |
| display:inline-flex;align-items:center;gap:6px; | |
| padding:5px 13px;border-radius:7px;text-decoration:none; | |
| font-size:12px;font-weight:600;background:var(--accent);color:#fff; | |
| border:1px solid rgba(255,255,255,.15); | |
| transition:all .15s ease;flex-shrink:0; | |
| } | |
| .gh-btn:hover{background:var(--accent2)} | |
| .gh-btn svg{fill:#fff;width:13px;height:13px} | |
| /* ══ Left icon rail ══ */ | |
| .rail{ | |
| grid-area:rail;background:var(--panel);border-right:1px solid var(--border); | |
| display:flex;flex-direction:column;align-items:center;padding:10px 0;gap:4px; | |
| overflow-y:auto;z-index:40; | |
| } | |
| .rail-btn{ | |
| width:38px;height:38px;border-radius:9px;border:1px solid transparent; | |
| background:transparent;color:var(--dim);cursor:pointer; | |
| display:flex;align-items:center;justify-content:center; | |
| transition:all .15s ease;position:relative; | |
| } | |
| .rail-btn:hover:not(:disabled){background:rgba(255,69,0,.12);color:var(--accent3);border-color:rgba(255,69,0,.25)} | |
| .rail-btn.active{background:rgba(255,69,0,.2);color:var(--accent2);border-color:rgba(255,69,0,.4)} | |
| .rail-btn:disabled{opacity:.3;cursor:not-allowed} | |
| .rail-btn svg{width:17px;height:17px;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round} | |
| .rail-btn svg.fill{fill:currentColor;stroke:none} | |
| .rail-sep{width:24px;height:1px;background:var(--border);margin:6px 0} | |
| .rail-label{ | |
| font-size:8px;font-weight:700;color:var(--faint);letter-spacing:1px; | |
| text-transform:uppercase;margin:8px 0 2px;user-select:none; | |
| } | |
| /* ══ Workspace ══ */ | |
| .workspace{ | |
| grid-area:workspace;display:flex;flex-direction:column;min-width:0;min-height:0; | |
| background:var(--canvas); | |
| } | |
| .canvas-head{ | |
| display:flex;align-items:center;gap:10px;padding:8px 16px; | |
| border-bottom:1px solid var(--border);background:var(--panel2);flex-shrink:0; | |
| } | |
| .seg-control{ | |
| display:inline-flex;background:var(--panel);border:1px solid var(--border); | |
| border-radius:8px;padding:2px;gap:2px; | |
| } | |
| .seg-btn{ | |
| padding:4px 14px;font-size:11px;font-weight:600;color:var(--dim); | |
| background:transparent;border:none;border-radius:6px;cursor:pointer; | |
| font-family:'Inter',sans-serif;transition:all .15s;letter-spacing:.2px; | |
| } | |
| .seg-btn:hover:not(.active){color:var(--muted)} | |
| .seg-btn.active{background:rgba(255,69,0,.18);color:var(--accent2)} | |
| .canvas-meta{ | |
| font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--faint); | |
| overflow:hidden;white-space:nowrap;text-overflow:ellipsis;flex:1; | |
| } | |
| .canvas-actions{display:flex;align-items:center;gap:6px} | |
| .canvas-stage{ | |
| flex:1;position:relative;overflow:hidden;min-height:0; | |
| display:flex;align-items:center;justify-content:center; | |
| background: | |
| repeating-conic-gradient(#0c0c0f 0% 25%, #08080a 0% 50%) 0 0/24px 24px; | |
| } | |
| .canvas-stage img.modern-out-img{ | |
| max-width:calc(100% - 48px);max-height:calc(100% - 48px); | |
| box-shadow:0 12px 48px rgba(0,0,0,.7),0 0 0 1px var(--border); | |
| border-radius:4px;cursor:zoom-in;image-rendering:auto; | |
| animation:canvasIn .3s ease; | |
| } | |
| @keyframes canvasIn{from{opacity:0;transform:scale(.985)}to{opacity:1;transform:scale(1)}} | |
| .canvas-placeholder{ | |
| text-align:center;color:var(--faint);user-select:none; | |
| display:flex;flex-direction:column;align-items:center;gap:14px;padding:24px; | |
| } | |
| .canvas-placeholder svg{width:72px;height:72px;opacity:.5} | |
| .canvas-placeholder .cp-title{font-size:15px;font-weight:600;color:var(--dim)} | |
| .canvas-placeholder .cp-sub{font-size:12px;line-height:1.7;max-width:340px} | |
| .canvas-placeholder kbd{ | |
| padding:1px 6px;background:var(--panel);border:1px solid var(--border2); | |
| border-radius:4px;font-family:'JetBrains Mono',monospace;font-size:10px;color:var(--muted); | |
| } | |
| /* Loader */ | |
| .modern-loader{ | |
| display:none;position:absolute;inset:0;background:rgba(8,8,10,.9); | |
| z-index:15;flex-direction:column;align-items:center;justify-content:center;gap:16px;backdrop-filter:blur(5px); | |
| } | |
| .modern-loader.active{display:flex} | |
| .loader-spinner{ | |
| width:38px;height:38px;border:3px solid var(--border);border-top-color:var(--accent); | |
| border-radius:50%;animation:spin .8s linear infinite; | |
| } | |
| @keyframes spin{to{transform:rotate(360deg)}} | |
| .loader-text{font-size:13px;color:var(--muted);font-weight:500;text-align:center;padding:0 16px} | |
| .loader-bar-track{width:220px;height:4px;background:var(--border);border-radius:2px;overflow:hidden} | |
| .loader-bar-fill{ | |
| height:100%;width:100%;background:linear-gradient(90deg,#FF4500,#FF6347,#FF4500); | |
| background-size:200% 100%;animation:shimmer 1.5s ease-in-out infinite;border-radius:2px; | |
| transition:width .3s ease; | |
| } | |
| .loader-bar-fill.determinate{animation:none;background:var(--accent)} | |
| @keyframes shimmer{0%{background-position:200% 0}100%{background-position:-200% 0}} | |
| /* ══ Filmstrip ══ */ | |
| .filmstrip{ | |
| height:var(--filmstrip-h);flex-shrink:0;background:var(--panel); | |
| border-top:1px solid var(--border);display:flex;min-height:0;overflow:hidden; | |
| } | |
| .filmstrip-section{ | |
| display:flex;flex-direction:column;min-width:0;padding:8px 0 8px 14px; | |
| } | |
| .filmstrip-section.inputs{flex:1.2;border-right:1px solid var(--border)} | |
| .filmstrip-section.history{flex:1} | |
| .filmstrip-label{ | |
| font-size:9px;font-weight:700;color:var(--faint);letter-spacing:1.2px; | |
| text-transform:uppercase;margin-bottom:6px;display:flex;align-items:center;gap:8px; | |
| user-select:none;flex-shrink:0; | |
| } | |
| .filmstrip-label .count{ | |
| font-family:'JetBrains Mono',monospace;font-weight:500;color:var(--dim); | |
| background:var(--panel2);border:1px solid var(--border);border-radius:4px;padding:0 5px;font-size:9px; | |
| } | |
| .filmstrip-row{ | |
| display:flex;gap:8px;overflow-x:auto;overflow-y:hidden;flex:1; | |
| align-items:flex-start;padding-bottom:4px;padding-right:14px; | |
| } | |
| .filmstrip-row::-webkit-scrollbar{height:5px} | |
| .filmstrip-row::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px} | |
| .fs-thumb{ | |
| position:relative;flex-shrink:0;width:76px;height:76px;border-radius:8px;overflow:hidden; | |
| border:2px solid var(--border);cursor:pointer;transition:all .15s;background:var(--panel2); | |
| animation:thumbIn .25s ease; | |
| } | |
| @keyframes thumbIn{from{opacity:0;transform:scale(.92)}to{opacity:1;transform:scale(1)}} | |
| .fs-thumb:hover{border-color:var(--border2);transform:translateY(-2px)} | |
| .fs-thumb.selected{border-color:var(--accent);box-shadow:0 0 0 2px rgba(255,69,0,.25)} | |
| .fs-thumb img{width:100%;height:100%;object-fit:cover} | |
| .fs-badge{ | |
| position:absolute;bottom:4px;left:4px;background:rgba(0,0,0,.75);color:#fff; | |
| padding:1px 6px;border-radius:4px;font-family:'JetBrains Mono',monospace;font-size:9px;font-weight:600; | |
| } | |
| .fs-remove{ | |
| position:absolute;top:4px;right:4px;width:20px;height:20px;background:rgba(0,0,0,.8); | |
| color:#fff;border:1px solid rgba(255,255,255,.2);border-radius:50%;cursor:pointer; | |
| display:none;align-items:center;justify-content:center;font-size:10px;transition:all .15s;line-height:1; | |
| } | |
| .fs-thumb:hover .fs-remove{display:flex} | |
| .fs-remove:hover{background:#ef4444;border-color:#ef4444} | |
| .fs-add{ | |
| flex-shrink:0;width:76px;height:76px;border-radius:8px;border:2px dashed var(--border2); | |
| display:flex;flex-direction:column;align-items:center;justify-content:center; | |
| cursor:pointer;transition:all .2s;background:rgba(255,69,0,.03);gap:3px; | |
| } | |
| .fs-add:hover{border-color:var(--accent);background:rgba(255,69,0,.08)} | |
| .fs-add .add-icon{font-size:22px;color:var(--dim);font-weight:300} | |
| .fs-add .add-text{font-size:9px;color:var(--dim);font-weight:600;text-transform:uppercase;letter-spacing:.5px} | |
| .fs-empty{ | |
| flex-shrink:0;display:flex;align-items:center;height:76px; | |
| color:var(--faint);font-size:11px;font-style:italic;padding-right:12px; | |
| } | |
| /* ══ Inspector ══ */ | |
| .inspector{ | |
| grid-area:inspector;background:var(--panel);border-left:1px solid var(--border); | |
| display:flex;flex-direction:column;min-height:0;z-index:40; | |
| } | |
| .inspector-tabs{ | |
| display:flex;border-bottom:1px solid var(--border);flex-shrink:0; | |
| } | |
| .insp-tab{ | |
| flex:1;padding:11px 8px;font-size:11px;font-weight:700;color:var(--dim); | |
| background:transparent;border:none;border-bottom:2px solid transparent; | |
| cursor:pointer;font-family:'Inter',sans-serif;text-transform:uppercase;letter-spacing:.8px; | |
| transition:all .15s; | |
| } | |
| .insp-tab:hover:not(.active){color:var(--muted)} | |
| .insp-tab.active{color:var(--accent2);border-bottom-color:var(--accent)} | |
| .insp-page{display:none;flex:1;overflow-y:auto;min-height:0;flex-direction:column} | |
| .insp-page.active{display:flex} | |
| .insp-section{border-bottom:1px solid var(--border);padding:14px 18px} | |
| .insp-section-title{ | |
| font-size:10px;font-weight:700;color:var(--dim);text-transform:uppercase; | |
| letter-spacing:1px;margin-bottom:10px;display:flex;align-items:center;justify-content:space-between; | |
| } | |
| .char-count{font-size:9px;font-weight:500;color:var(--faint);font-family:'JetBrains Mono',monospace;text-transform:none;letter-spacing:0} | |
| .modern-textarea{ | |
| width:100%;background:var(--panel2);border:1px solid var(--border);border-radius:8px; | |
| padding:10px 13px;font-family:'Inter',sans-serif;font-size:13px;color:var(--text); | |
| resize:vertical;outline:none;min-height:64px;transition:border-color .2s;line-height:1.55; | |
| } | |
| .modern-textarea:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(255,69,0,.15)} | |
| .modern-textarea::placeholder{color:var(--faint)} | |
| .modern-textarea.error-flash{ | |
| border-color:#ef4444!important;box-shadow:0 0 0 3px rgba(239,68,68,.2)!important;animation:shake .4s ease; | |
| } | |
| @keyframes shake{0%,100%{transform:translateX(0)}20%,60%{transform:translateX(-4px)}40%,80%{transform:translateX(4px)}} | |
| .suggestions-wrap{display:flex;flex-wrap:wrap;gap:5px} | |
| .suggestion-chip{ | |
| display:inline-flex;align-items:center;padding:4px 11px; | |
| background:rgba(255,69,0,.07);border:1px solid rgba(255,69,0,.18);border-radius:14px; | |
| color:var(--accent3);font-size:11px;font-weight:500;font-family:'Inter',sans-serif; | |
| cursor:pointer;transition:all .15s;white-space:nowrap; | |
| } | |
| .suggestion-chip:hover{background:rgba(255,69,0,.15);border-color:rgba(255,69,0,.35);color:var(--accent2)} | |
| .suggestion-chip.active{background:rgba(255,69,0,.25);border-color:var(--accent);color:#fff} | |
| .lora-native-select{ | |
| width:100%;background:var(--panel2); | |
| border:1px solid var(--border2);border-radius:8px; | |
| padding:9px 34px 9px 13px; | |
| font-family:'Inter',sans-serif;font-size:12px;font-weight:600;color:var(--text); | |
| outline:none;appearance:none;-webkit-appearance:none; | |
| background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23FF4500' d='M6 8L1 3h10z'/%3E%3C/svg%3E"); | |
| background-repeat:no-repeat;background-position:right 11px center; | |
| cursor:pointer;transition:border-color .2s;color-scheme:dark; | |
| } | |
| .lora-native-select:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(255,69,0,.15)} | |
| .lora-native-select option{background:var(--panel);color:var(--text)} | |
| .btn-run{ | |
| display:flex;align-items:center;justify-content:center;gap:8px;width:100%; | |
| background:linear-gradient(135deg,#FF4500,#CC3700);border:none;border-radius:9px; | |
| padding:12px 24px;cursor:pointer;font-size:14px;font-weight:700;font-family:'Inter',sans-serif; | |
| color:#fff;transition:all .2s ease;letter-spacing:-.2px; | |
| box-shadow:0 4px 16px rgba(255,69,0,.3),inset 0 1px 0 rgba(255,255,255,.1); | |
| } | |
| .btn-run:hover:not(:disabled){ | |
| background:linear-gradient(135deg,#FF6347,#FF4500);transform:translateY(-1px); | |
| box-shadow:0 6px 24px rgba(255,69,0,.45),inset 0 1px 0 rgba(255,255,255,.15); | |
| } | |
| .btn-run:active:not(:disabled){transform:translateY(0)} | |
| .btn-run:disabled{opacity:.55;cursor:not-allowed} | |
| .btn-cancel{ | |
| display:flex;align-items:center;justify-content:center;gap:6px;width:100%; | |
| margin-top:8px;background:transparent;border:1px solid var(--border2);border-radius:9px; | |
| padding:8px 24px;cursor:pointer;font-size:12px;font-weight:600;font-family:'Inter',sans-serif; | |
| color:var(--muted);transition:all .15s; | |
| } | |
| .btn-cancel:hover{border-color:#ef4444;color:#ef4444;background:rgba(239,68,68,.08)} | |
| .run-hint{ | |
| text-align:center;font-size:10px;color:var(--faint);margin-top:8px; | |
| font-family:'JetBrains Mono',monospace; | |
| } | |
| .slider-row{display:flex;align-items:center;gap:9px;min-height:26px;margin-bottom:10px} | |
| .slider-row:last-child{margin-bottom:0} | |
| .slider-row label{font-size:12px;font-weight:500;color:var(--muted);min-width:64px;flex-shrink:0} | |
| .slider-row input[type="range"]{ | |
| flex:1;-webkit-appearance:none;appearance:none;height:5px;background:var(--border); | |
| border-radius:3px;outline:none;min-width:0; | |
| } | |
| .slider-row input[type="range"]::-webkit-slider-thumb{ | |
| -webkit-appearance:none;width:14px;height:14px;background:linear-gradient(135deg,#FF4500,#CC3700); | |
| border-radius:50%;cursor:pointer;box-shadow:0 2px 6px rgba(255,69,0,.4);transition:transform .15s; | |
| } | |
| .slider-row input[type="range"]::-webkit-slider-thumb:hover{transform:scale(1.2)} | |
| .slider-row input[type="range"]::-moz-range-thumb{ | |
| width:14px;height:14px;background:linear-gradient(135deg,#FF4500,#CC3700); | |
| border-radius:50%;cursor:pointer;border:none;box-shadow:0 2px 6px rgba(255,69,0,.4); | |
| } | |
| .slider-val{ | |
| min-width:48px;text-align:right;font-family:'JetBrains Mono',monospace;font-size:11px; | |
| font-weight:500;padding:2px 7px;background:var(--panel2);border:1px solid var(--border); | |
| border-radius:5px;color:var(--muted);flex-shrink:0; | |
| } | |
| /* Examples page */ | |
| .examples-list{padding:12px 14px;display:flex;flex-direction:column;gap:10px} | |
| .example-card{ | |
| background:var(--panel2);border:1px solid var(--border); | |
| border-radius:10px;overflow:hidden;cursor:pointer;transition:all .2s ease;flex-shrink:0; | |
| display:flex;align-items:stretch; | |
| } | |
| .example-card:hover{border-color:var(--accent);box-shadow:0 4px 14px rgba(255,69,0,.15)} | |
| .example-card.loading{opacity:.5;pointer-events:none} | |
| .example-thumb-wrap{width:96px;flex-shrink:0;background:var(--panel);overflow:hidden;display:flex} | |
| .example-thumb-wrap img{width:100%;height:100%;object-fit:cover} | |
| .example-thumb-placeholder{ | |
| width:100%;height:100%;display:flex;align-items:center;justify-content:center; | |
| background:var(--panel);color:var(--border2);font-size:10px; | |
| } | |
| .example-meta{padding:6px 10px 2px;display:flex;align-items:center;gap:5px;flex-wrap:wrap} | |
| .example-lora-badge{ | |
| display:inline-flex;padding:2px 7px;background:rgba(255,69,0,.1);border-radius:4px; | |
| font-size:9px;font-weight:600;color:var(--accent2);font-family:'JetBrains Mono',monospace; | |
| white-space:nowrap;border:1px solid rgba(255,69,0,.25); | |
| } | |
| .example-body{flex:1;display:flex;flex-direction:column;justify-content:center;min-width:0} | |
| .example-prompt-text{ | |
| padding:4px 10px 9px;font-size:11px;color:var(--muted);line-height:1.45; | |
| display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden; | |
| } | |
| /* ══ Status bar ══ */ | |
| .statusbar{ | |
| grid-area:statusbar;background:var(--panel);border-top:1px solid var(--border); | |
| display:flex;align-items:center;gap:4px;padding:0 14px;font-size:11px;z-index:50; | |
| } | |
| .statusbar .sb-section{ | |
| padding:0 10px;display:flex;align-items:center;font-family:'JetBrains Mono',monospace; | |
| font-size:11px;color:var(--faint);overflow:hidden;white-space:nowrap;gap:6px; | |
| } | |
| .statusbar .sb-right{margin-left:auto;display:flex;align-items:center;gap:10px} | |
| .sb-pill{ | |
| padding:2px 12px;border-radius:5px;font-weight:600;font-size:10px; | |
| background:rgba(255,69,0,.1);color:var(--accent2); | |
| } | |
| .sb-pill.error{background:rgba(239,68,68,.12);color:#f87171} | |
| .sb-pill.done{background:rgba(34,197,94,.12);color:#4ade80} | |
| .sb-link{color:var(--faint);text-decoration:none;font-family:'JetBrains Mono',monospace;font-size:10px} | |
| .sb-link:hover{color:var(--accent3)} | |
| /* ══ Toast ══ */ | |
| .toast-notification{ | |
| position:fixed;top:60px;left:50%;transform:translateX(-50%) translateY(-140%); | |
| z-index:9999;padding:10px 22px;border-radius:9px;font-family:'Inter',sans-serif; | |
| font-size:13px;font-weight:600;display:flex;align-items:center;gap:8px; | |
| box-shadow:0 8px 24px rgba(0,0,0,.5); | |
| transition:transform .35s cubic-bezier(.34,1.56,.64,1),opacity .35s ease;opacity:0;pointer-events:none; | |
| } | |
| .toast-notification.visible{transform:translateX(-50%) translateY(0);opacity:1;pointer-events:auto} | |
| .toast-notification.error{background:linear-gradient(135deg,#dc2626,#b91c1c);color:#fff;border:1px solid rgba(255,255,255,.15)} | |
| .toast-notification.warning{background:linear-gradient(135deg,#d97706,#b45309);color:#fff;border:1px solid rgba(255,255,255,.15)} | |
| .toast-notification.info{background:linear-gradient(135deg,#FF4500,#CC3700);color:#fff;border:1px solid rgba(255,255,255,.15)} | |
| .toast-notification .toast-icon{font-size:15px;line-height:1} | |
| /* ══ Lightbox ══ */ | |
| .lightbox{ | |
| position:fixed;inset:0;background:rgba(0,0,0,.92);z-index:10000; | |
| display:none;align-items:center;justify-content:center;cursor:zoom-out; | |
| backdrop-filter:blur(6px);animation:fadeIn .2s ease; | |
| } | |
| .lightbox.visible{display:flex} | |
| .lightbox img{max-width:94vw;max-height:94vh;border-radius:6px;box-shadow:0 20px 60px rgba(0,0,0,.8)} | |
| @keyframes fadeIn{from{opacity:0}to{opacity:1}} | |
| /* ══ Drop overlay ══ */ | |
| .drop-overlay{ | |
| position:fixed;inset:0;z-index:9000;background:rgba(8,8,10,.85); | |
| display:none;align-items:center;justify-content:center;pointer-events:none; | |
| backdrop-filter:blur(3px); | |
| } | |
| .drop-overlay.visible{display:flex} | |
| .drop-overlay-inner{ | |
| border:3px dashed var(--accent);border-radius:22px;padding:56px 76px; | |
| font-size:19px;font-weight:700;color:var(--accent2);background:rgba(255,69,0,.06); | |
| } | |
| /* ══ Scrollbars ══ */ | |
| ::-webkit-scrollbar{width:8px;height:8px} | |
| ::-webkit-scrollbar-track{background:transparent} | |
| ::-webkit-scrollbar-thumb{background:var(--border);border-radius:4px} | |
| ::-webkit-scrollbar-thumb:hover{background:var(--border2)} | |
| /* ══ Touch devices ══ */ | |
| @media (pointer:coarse){ | |
| .fs-remove{display:flex} | |
| .rail-btn{width:44px;height:44px} | |
| .rail-btn svg{width:19px;height:19px} | |
| } | |
| /* ══ Responsive ══ */ | |
| @media(max-width:960px){ | |
| html,body{height:auto} | |
| body{overflow:auto!important} | |
| .studio{ | |
| display:flex;flex-direction:column;height:auto;min-height:100vh; | |
| } | |
| .menubar{ | |
| flex-wrap:wrap;gap:8px;padding:10px 12px;min-height:var(--menubar-h); | |
| position:sticky;top:0; | |
| } | |
| .menubar-title{font-size:13px} | |
| .menubar-status{display:none} | |
| .menubar-spacer{flex:1} | |
| .gh-btn{padding:6px 10px} | |
| .gh-btn span{display:none} | |
| .rail{ | |
| flex-direction:row;justify-content:flex-start;flex-wrap:nowrap; | |
| overflow-x:auto;overflow-y:hidden;border-right:none; | |
| border-bottom:1px solid var(--border);padding:6px 10px;gap:6px; | |
| } | |
| .rail-sep{width:1px;height:26px;margin:0 4px;flex-shrink:0} | |
| .rail-label{display:none} | |
| .rail-btn{flex-shrink:0} | |
| .workspace{min-height:0} | |
| .canvas-head{flex-wrap:wrap;gap:8px;padding:8px 12px} | |
| .canvas-meta{flex-basis:100%;order:3;font-size:10px} | |
| .canvas-stage{min-height:42vh} | |
| .canvas-stage img.modern-out-img{max-width:calc(100% - 20px);max-height:52vh} | |
| .canvas-placeholder .cp-sub{font-size:11px;max-width:280px} | |
| .filmstrip{flex-direction:column;height:auto;max-height:none} | |
| .filmstrip-section.inputs{border-right:none;border-bottom:1px solid var(--border)} | |
| .filmstrip-section{padding:8px 0 8px 12px} | |
| .fs-thumb{width:68px;height:68px} | |
| .fs-add{width:68px;height:68px} | |
| .fs-empty{height:68px} | |
| .inspector{border-left:none;border-top:1px solid var(--border)} | |
| .inspector-tabs{position:sticky;top:0;background:var(--panel);z-index:30} | |
| .insp-tab{padding:13px 8px;font-size:12px} | |
| .insp-page{overflow-y:visible} | |
| .insp-section{padding:14px 16px} | |
| .modern-textarea{font-size:16px} | |
| .btn-run{padding:14px 24px;font-size:15px} | |
| .suggestion-chip{padding:6px 13px;font-size:12px} | |
| .statusbar{flex-wrap:wrap;height:auto;min-height:var(--statusbar-h);padding:4px 10px;gap:2px} | |
| .sb-link{display:none} | |
| .toast-notification{max-width:calc(100vw - 32px);font-size:12px;padding:10px 16px} | |
| .drop-overlay-inner{padding:32px 28px;font-size:15px;border-radius:16px;margin:0 16px} | |
| .example-card{flex-direction:column} | |
| .example-thumb-wrap{width:100%;height:96px} | |
| } | |
| @media(max-width:400px){ | |
| .menubar-badge.fast{display:none} | |
| .seg-btn{padding:4px 12px} | |
| .canvas-stage{min-height:36vh} | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="studio"> | |
| <!-- ══ Menu bar ══ --> | |
| <div class="menubar"> | |
| <div class="menubar-logo"> | |
| <svg viewBox="0 0 24 24" fill="white" xmlns="http://www.w3.org/2000/svg"><path d="M12 23c-3.6 0-8-2.69-8-7.5 0-3.5 3-6.5 4.5-8 .27-.27.75-.08.75.28v2.44c0 .42.5.63.72.28C12.28 7.5 13 3 13 1c0-.42.48-.64.8-.35C18 4.5 20 9 20 12c0 5.5-3.5 11-8 11z"/></svg> | |
| </div> | |
| <div class="menubar-title">Qwen3.8 Studio <span>/ Object Detection</span></div> | |
| <span class="menubar-badge">27B</span> | |
| <span class="menubar-badge fast">ZERO GPU</span> | |
| <div class="menubar-spacer"></div> | |
| <div class="menubar-status"><span class="dot" id="conn-dot"></span><span id="conn-text">connecting…</span></div> | |
| <a href="https://github.com/PRITHIVSAKTHIUR/Qwen3.8-27B-Object-Detection" | |
| target="_blank" rel="noopener" class="gh-btn"> | |
| <svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg> | |
| <span>GitHub</span> | |
| </a> | |
| </div> | |
| <!-- ══ Left icon rail ══ --> | |
| <div class="rail"> | |
| <div class="rail-label">Input</div> | |
| <button class="rail-btn" id="tb-upload" title="Upload image"> | |
| <svg viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg> | |
| </button> | |
| <button class="rail-btn" id="tb-remove" title="Remove image" disabled> | |
| <svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg> | |
| </button> | |
| <button class="rail-btn" id="tb-clear" title="Clear image" disabled> | |
| <svg viewBox="0 0 24 24"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg> | |
| </button> | |
| <div class="rail-sep"></div> | |
| <div class="rail-label">Result</div> | |
| <button class="rail-btn" id="dl-btn-output" title="Download result" disabled> | |
| <svg viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg> | |
| </button> | |
| <button class="rail-btn" id="zoom-btn" title="Zoom result" disabled> | |
| <svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="11" y1="8" x2="11" y2="14"/><line x1="8" y1="11" x2="14" y2="11"/></svg> | |
| </button> | |
| </div> | |
| <!-- ══ Workspace ══ --> | |
| <div class="workspace"> | |
| <div class="canvas-head"> | |
| <div class="seg-control"> | |
| <button class="seg-btn active" id="seg-result">Result</button> | |
| <button class="seg-btn" id="seg-input">Input</button> | |
| </div> | |
| <div class="canvas-meta" id="canvas-meta">No image loaded</div> | |
| <div class="canvas-actions"></div> | |
| </div> | |
| <div class="canvas-stage" id="output-image-container"> | |
| <div class="modern-loader" id="output-loader"> | |
| <div class="loader-spinner"></div> | |
| <div class="loader-text" id="loader-text">Processing image…</div> | |
| <div class="loader-bar-track"><div class="loader-bar-fill" id="loader-bar-fill"></div></div> | |
| </div> | |
| <div class="canvas-placeholder" id="output-placeholder"> | |
| <svg viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg"> | |
| <rect x="8" y="14" width="64" height="52" rx="6" fill="none" stroke="#3f2f2f" stroke-width="2" stroke-dasharray="4 3"/> | |
| <polygon points="12,62 30,40 42,50 54,34 68,62" fill="rgba(255,69,0,0.1)" stroke="#3f2f2f" stroke-width="1.5"/> | |
| <circle cx="28" cy="30" r="6" fill="rgba(255,69,0,0.12)" stroke="#3f2f2f" stroke-width="1.5"/> | |
| </svg> | |
| <div class="cp-title">Canvas is empty</div> | |
| <div class="cp-sub"> | |
| Drop an image anywhere, paste with <kbd>⌘/Ctrl+V</kbd>, or use the rail to upload. | |
| Then write an instruction and press <kbd>⌘/Ctrl</kbd>+<kbd>↵</kbd>. | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Filmstrip --> | |
| <div class="filmstrip"> | |
| <div class="filmstrip-section inputs"> | |
| <div class="filmstrip-label">Input <span class="count" id="tb-image-count">0</span></div> | |
| <div class="filmstrip-row" id="image-gallery-grid"></div> | |
| </div> | |
| <div class="filmstrip-section history"> | |
| <div class="filmstrip-label">History <span class="count" id="history-count">0</span></div> | |
| <div class="filmstrip-row" id="history-strip"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- ══ Inspector ══ --> | |
| <div class="inspector"> | |
| <div class="inspector-tabs"> | |
| <button class="insp-tab active" id="tab-edit">Inference</button> | |
| <button class="insp-tab" id="tab-examples">Examples</button> | |
| </div> | |
| <!-- Edit page --> | |
| <div class="insp-page active" id="page-edit"> | |
| <div class="insp-section"> | |
| <div class="insp-section-title"><span>Instruction</span><span class="char-count" id="char-count">0</span></div> | |
| <textarea id="custom-prompt-input" class="modern-textarea" rows="3" | |
| placeholder="e.g., Detect all cars, Point to the red sign, Map a path from door to window…"></textarea> | |
| </div> | |
| <div class="insp-section"> | |
| <div class="insp-section-title"><span>Quick Prompts</span></div> | |
| <div class="suggestions-wrap" id="suggestions-wrap"></div> | |
| </div> | |
| <div class="insp-section"> | |
| <div class="insp-section-title"><span>Task Type</span></div> | |
| <select id="custom-task-select" class="lora-native-select"> | |
| <option value="Detect">Object Detection (Bounding Boxes)</option> | |
| <option value="Point">Point Localization (Keypoints)</option> | |
| <option value="Spatial">Spatial Guidance (Path Mapping)</option> | |
| </select> | |
| </div> | |
| <div class="insp-section"> | |
| <button id="custom-run-btn" class="btn-run"> | |
| <span id="run-btn-label">Run Inference</span> | |
| </button> | |
| <button id="custom-cancel-btn" class="btn-cancel" style="display:none;">Cancel</button> | |
| <div class="run-hint">⌘/Ctrl + Enter</div> | |
| </div> | |
| <div class="insp-section"> | |
| <div class="insp-section-title"><span>Output JSON</span></div> | |
| <textarea id="custom-output-json" class="modern-textarea" rows="6" readonly placeholder="Result JSON will appear here..."></textarea> | |
| </div> | |
| <div class="insp-section"> | |
| <div class="insp-section-title"><span>Advanced</span></div> | |
| <div class="slider-row"> | |
| <label>Point R</label> | |
| <input type="range" id="custom-point-r" min="2" max="20" step="1" value="4"> | |
| <span class="slider-val" id="custom-point-r-val">4</span> | |
| </div> | |
| <div class="slider-row"> | |
| <label>Box W</label> | |
| <input type="range" id="custom-box-w" min="1" max="8" step="1" value="1"> | |
| <span class="slider-val" id="custom-box-w-val">1</span> | |
| </div> | |
| <div class="slider-row"> | |
| <label>Txt Scale</label> | |
| <input type="range" id="custom-txt-s" min="0.2" max="1.5" step="0.05" value="0.4"> | |
| <span class="slider-val" id="custom-txt-s-val">0.4</span> | |
| </div> | |
| <div class="slider-row"> | |
| <label>GPU Dur</label> | |
| <input type="range" id="custom-gpu-dur" min="0" max="6" step="1" value="1"> | |
| <span class="slider-val" id="custom-gpu-dur-val">90s</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Examples page --> | |
| <div class="insp-page" id="page-examples"> | |
| <div class="examples-list" id="examples-scroll"></div> | |
| </div> | |
| </div> | |
| <!-- ══ Status bar ══ --> | |
| <div class="statusbar"> | |
| <div class="sb-section" id="sb-image-count">No input</div> | |
| <div class="sb-right"> | |
| <a class="sb-link" href="https://huggingface.co/Qwen/Qwen3.8-27B" target="_blank" rel="noopener">Qwen3.8-27B</a> | |
| <span class="sb-pill" id="sb-status">Ready</span> | |
| </div> | |
| </div> | |
| </div> | |
| <input id="custom-file-input" type="file" accept="image/*" style="display:none;" /> | |
| <!-- Lightbox --> | |
| <div class="lightbox" id="lightbox"><img id="lightbox-img" alt="zoom"></div> | |
| <!-- Global drop overlay --> | |
| <div class="drop-overlay" id="drop-overlay"><div class="drop-overlay-inner">Drop image to upload</div></div> | |
| <script type="module"> | |
| import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js"; | |
| /* ── DOM refs ── */ | |
| const galleryGrid = document.getElementById('image-gallery-grid'); | |
| const fileInput = document.getElementById('custom-file-input'); | |
| const btnUpload = document.getElementById('tb-upload'); | |
| const btnRemove = document.getElementById('tb-remove'); | |
| const btnClear = document.getElementById('tb-clear'); | |
| const promptInput = document.getElementById('custom-prompt-input'); | |
| const charCount = document.getElementById('char-count'); | |
| const taskSelect = document.getElementById('custom-task-select'); | |
| const outputJson = document.getElementById('custom-output-json'); | |
| const runBtn = document.getElementById('custom-run-btn'); | |
| const runBtnLabel = document.getElementById('run-btn-label'); | |
| const cancelBtn = document.getElementById('custom-cancel-btn'); | |
| const imgCountTb = document.getElementById('tb-image-count'); | |
| const imgCountSb = document.getElementById('sb-image-count'); | |
| const sbStatus = document.getElementById('sb-status'); | |
| const loader = document.getElementById('output-loader'); | |
| const loaderText = document.getElementById('loader-text'); | |
| const loaderFill = document.getElementById('loader-bar-fill'); | |
| const outBody = document.getElementById('output-image-container'); | |
| const outPh = document.getElementById('output-placeholder'); | |
| const dlBtn = document.getElementById('dl-btn-output'); | |
| const zoomBtn = document.getElementById('zoom-btn'); | |
| const historyStrip = document.getElementById('history-strip'); | |
| const historyCount = document.getElementById('history-count'); | |
| const pointRSlider = document.getElementById('custom-point-r'); | |
| const pointRVal = document.getElementById('custom-point-r-val'); | |
| const boxWSlider = document.getElementById('custom-box-w'); | |
| const boxWVal = document.getElementById('custom-box-w-val'); | |
| const txtSSlider = document.getElementById('custom-txt-s'); | |
| const txtSVal = document.getElementById('custom-txt-s-val'); | |
| const gpuDurSlider = document.getElementById('custom-gpu-dur'); | |
| const gpuDurVal = document.getElementById('custom-gpu-dur-val'); | |
| const lightbox = document.getElementById('lightbox'); | |
| const lightboxImg = document.getElementById('lightbox-img'); | |
| const dropOverlay = document.getElementById('drop-overlay'); | |
| const canvasMeta = document.getElementById('canvas-meta'); | |
| const segResult = document.getElementById('seg-result'); | |
| const segInput = document.getElementById('seg-input'); | |
| const connDot = document.getElementById('conn-dot'); | |
| const connText = document.getElementById('conn-text'); | |
| /* ── State ── */ | |
| let currentImage = null; // {b64, name} | |
| let toastTimer = null; | |
| let running = false; | |
| let currentJob = null; | |
| let currentResult = null; // {b64, text} | |
| let history = []; | |
| let historyIdx = -1; | |
| let viewMode = 'result'; // 'result' | 'input' | |
| const GPU_DURATIONS = [60, 90, 120, 150, 180, 250, 300]; | |
| const SUGGESTIONS = [ | |
| ['Detect Cars', 'Detect all the cars.'], | |
| ['Detect People', 'Detect all the people in the scene.'], | |
| ['Point to Sign', 'Point to the red stop sign.'], | |
| ['Spatial Path', 'Map a path from the door to the lamp.'], | |
| ]; | |
| /* ── Toast ── */ | |
| function showToast(message, type) { | |
| let toast = document.getElementById('app-toast'); | |
| if (!toast) { | |
| toast = document.createElement('div'); | |
| toast.id = 'app-toast'; | |
| toast.className = 'toast-notification'; | |
| toast.innerHTML = '<span class="toast-icon"></span><span class="toast-text"></span>'; | |
| document.body.appendChild(toast); | |
| } | |
| const icon = toast.querySelector('.toast-icon'); | |
| const text = toast.querySelector('.toast-text'); | |
| toast.className = 'toast-notification ' + (type || 'error'); | |
| icon.textContent = type === 'warning' ? '⚠' : type === 'info' ? 'ℹ' : '✗'; | |
| text.textContent = message; | |
| if (toastTimer) clearTimeout(toastTimer); | |
| void toast.offsetWidth; | |
| toast.classList.add('visible'); | |
| toastTimer = setTimeout(() => toast.classList.remove('visible'), 3500); | |
| } | |
| function flashPromptError() { | |
| promptInput.classList.add('error-flash'); | |
| promptInput.focus(); | |
| setTimeout(() => promptInput.classList.remove('error-flash'), 800); | |
| } | |
| function setStatus(text, cls) { | |
| sbStatus.textContent = text; | |
| sbStatus.className = 'sb-pill' + (cls ? ' ' + cls : ''); | |
| } | |
| /* ── View mode ── */ | |
| function setViewMode(mode) { | |
| viewMode = mode; | |
| segResult.classList.toggle('active', mode === 'result'); | |
| segInput.classList.toggle('active', mode === 'input'); | |
| renderCanvas(); | |
| } | |
| segResult.addEventListener('click', () => setViewMode('result')); | |
| segInput.addEventListener('click', () => setViewMode('input')); | |
| function getCanvasImg() { return outBody.querySelector('img.modern-out-img'); } | |
| function renderCanvas() { | |
| let img = getCanvasImg(); | |
| const showResult = viewMode === 'result' && currentResult; | |
| const showInput = viewMode === 'input' && currentImage; | |
| if (showResult || showInput) { | |
| outPh.style.display = 'none'; | |
| if (!img) { | |
| img = document.createElement('img'); | |
| img.className = 'modern-out-img'; | |
| img.addEventListener('click', () => { | |
| if (img.src) { lightboxImg.src = img.src; lightbox.classList.add('visible'); } | |
| }); | |
| outBody.appendChild(img); | |
| } | |
| img.style.display = ''; | |
| if (showResult) { | |
| img.src = currentResult.b64; | |
| canvasMeta.textContent = 'result'; | |
| } else { | |
| img.src = currentImage.b64; | |
| canvasMeta.textContent = 'input · ' + (currentImage.name || 'image'); | |
| } | |
| } else { | |
| if (img) img.style.display = 'none'; | |
| outPh.style.display = ''; | |
| canvasMeta.textContent = currentImage | |
| ? 'input ready — run an edit' | |
| : 'No image loaded'; | |
| } | |
| updateRailState(); | |
| } | |
| function updateRailState() { | |
| const hasResult = !!currentResult; | |
| dlBtn.disabled = !hasResult; | |
| zoomBtn.disabled = !hasResult && !currentImage; | |
| btnRemove.disabled = !currentImage; | |
| btnClear.disabled = !currentImage; | |
| } | |
| /* ── Inputs filmstrip ── */ | |
| function updateCounts() { | |
| const n = currentImage ? 1 : 0; | |
| imgCountTb.textContent = n; | |
| imgCountSb.textContent = n > 0 ? '1 input · ' + history.length + ' result' + (history.length === 1 ? '' : 's') : 'No input'; | |
| } | |
| function setImage(b64, name) { | |
| currentImage = { b64, name: name || 'image' }; | |
| renderFilmstrip(); | |
| updateCounts(); | |
| if (!currentResult || viewMode === 'input') renderCanvas(); | |
| else setViewMode('input'); | |
| } | |
| function removeImage() { | |
| currentImage = null; | |
| renderFilmstrip(); | |
| updateCounts(); | |
| renderCanvas(); | |
| } | |
| function clearAll() { | |
| currentImage = null; | |
| renderFilmstrip(); | |
| updateCounts(); | |
| renderCanvas(); | |
| } | |
| function renderFilmstrip() { | |
| galleryGrid.innerHTML = ''; | |
| if (!currentImage) { | |
| const empty = document.createElement('div'); | |
| empty.className = 'fs-empty'; | |
| empty.textContent = 'Drop or paste an image…'; | |
| galleryGrid.appendChild(empty); | |
| } else { | |
| const t = document.createElement('div'); | |
| t.className = 'fs-thumb selected'; | |
| t.innerHTML = '<img src="' + currentImage.b64 + '" alt="">' + | |
| '<span class="fs-badge">input</span>' + | |
| '<button class="fs-remove">✕</button>'; | |
| t.addEventListener('click', (e) => { | |
| if (e.target.closest('.fs-remove')) return; | |
| setViewMode('input'); | |
| }); | |
| t.querySelector('.fs-remove').addEventListener('click', (e) => { | |
| e.stopPropagation(); removeImage(); | |
| }); | |
| galleryGrid.appendChild(t); | |
| } | |
| const add = document.createElement('div'); | |
| add.className = 'fs-add'; | |
| add.innerHTML = '<span class="add-icon">+</span><span class="add-text">' + (currentImage ? 'Replace' : 'Add') + '</span>'; | |
| add.addEventListener('click', () => fileInput.click()); | |
| galleryGrid.appendChild(add); | |
| updateRailState(); | |
| } | |
| function processFiles(files) { | |
| const file = files && files[0]; | |
| if (!file || !file.type.startsWith('image/')) return 0; | |
| const reader = new FileReader(); | |
| reader.onload = (e) => setImage(e.target.result, file.name); | |
| reader.readAsDataURL(file); | |
| return 1; | |
| } | |
| fileInput.addEventListener('change', (e) => { processFiles(e.target.files); e.target.value = ''; }); | |
| btnUpload.addEventListener('click', () => fileInput.click()); | |
| btnRemove.addEventListener('click', removeImage); | |
| btnClear.addEventListener('click', clearAll); | |
| /* ── Drop anywhere ── */ | |
| let dragDepth = 0; | |
| window.addEventListener('dragenter', (e) => { | |
| if (!e.dataTransfer || !Array.from(e.dataTransfer.types).includes('Files')) return; | |
| e.preventDefault(); | |
| dragDepth++; | |
| dropOverlay.classList.add('visible'); | |
| }); | |
| window.addEventListener('dragleave', (e) => { | |
| if (!e.dataTransfer || !Array.from(e.dataTransfer.types).includes('Files')) return; | |
| dragDepth = Math.max(0, dragDepth - 1); | |
| if (dragDepth === 0) dropOverlay.classList.remove('visible'); | |
| }); | |
| window.addEventListener('dragover', (e) => { e.preventDefault(); }); | |
| window.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| dragDepth = 0; | |
| dropOverlay.classList.remove('visible'); | |
| if (e.dataTransfer.files.length) { | |
| const n = processFiles(e.dataTransfer.files); | |
| if (n > 0) showToast('Image loaded', 'info'); | |
| } | |
| }); | |
| /* ── Paste ── */ | |
| document.addEventListener('paste', (e) => { | |
| if (e.target === promptInput) return; | |
| const items = e.clipboardData && e.clipboardData.items; | |
| if (!items) return; | |
| for (const item of items) { | |
| if (item.kind === 'file' && item.type.startsWith('image/')) { | |
| const f = item.getAsFile(); | |
| if (f) { | |
| e.preventDefault(); | |
| processFiles([f]); | |
| showToast('Pasted image', 'info'); | |
| return; | |
| } | |
| } | |
| } | |
| }); | |
| /* ── Inspector tabs ── */ | |
| const tabEdit = document.getElementById('tab-edit'); | |
| const tabExamples = document.getElementById('tab-examples'); | |
| const pageEdit = document.getElementById('page-edit'); | |
| const pageExamples = document.getElementById('page-examples'); | |
| tabEdit.addEventListener('click', () => { | |
| tabEdit.classList.add('active'); tabExamples.classList.remove('active'); | |
| pageEdit.classList.add('active'); pageExamples.classList.remove('active'); | |
| }); | |
| tabExamples.addEventListener('click', () => { | |
| tabExamples.classList.add('active'); tabEdit.classList.remove('active'); | |
| pageExamples.classList.add('active'); pageEdit.classList.remove('active'); | |
| }); | |
| /* ── Suggestion chips ── */ | |
| const suggWrap = document.getElementById('suggestions-wrap'); | |
| SUGGESTIONS.forEach(([label, prompt]) => { | |
| const chip = document.createElement('button'); | |
| chip.className = 'suggestion-chip'; | |
| chip.textContent = label; | |
| chip.addEventListener('click', () => { | |
| promptInput.value = prompt; | |
| updateCharCount(); | |
| suggWrap.querySelectorAll('.suggestion-chip').forEach(c => c.classList.remove('active')); | |
| chip.classList.add('active'); | |
| promptInput.focus(); | |
| }); | |
| suggWrap.appendChild(chip); | |
| }); | |
| function updateCharCount() { charCount.textContent = promptInput.value.length; } | |
| promptInput.addEventListener('input', () => { | |
| updateCharCount(); | |
| suggWrap.querySelectorAll('.suggestion-chip').forEach(c => c.classList.remove('active')); | |
| }); | |
| updateCharCount(); | |
| /* ── Sliders ── */ | |
| function bindSlider(id, suffix='') { | |
| const slider = document.getElementById(id); | |
| const valSpan = document.getElementById(id + '-val'); | |
| slider.addEventListener('input', () => { valSpan.textContent = slider.value + suffix; }); | |
| } | |
| bindSlider('custom-point-r'); | |
| bindSlider('custom-box-w'); | |
| bindSlider('custom-txt-s'); | |
| gpuDurSlider.addEventListener('input', () => { | |
| gpuDurVal.textContent = GPU_DURATIONS[gpuDurSlider.value] + 's'; | |
| }); | |
| /* ── Loader ── */ | |
| function showLoader(text) { | |
| loaderText.textContent = text || 'Processing image…'; | |
| loaderFill.classList.remove('determinate'); | |
| loaderFill.style.width = '100%'; | |
| loader.classList.add('active'); | |
| setStatus('Processing…'); | |
| } | |
| function setLoaderProgress(text, frac) { | |
| loaderText.textContent = text; | |
| if (frac != null && frac >= 0 && frac <= 1) { | |
| loaderFill.classList.add('determinate'); | |
| loaderFill.style.width = Math.max(4, Math.round(frac * 100)) + '%'; | |
| } | |
| } | |
| function hideLoader(statusText, cls) { | |
| loader.classList.remove('active'); | |
| setStatus(statusText || 'Done', cls || 'done'); | |
| } | |
| /* ── Result / history ── */ | |
| function showResult(entry) { | |
| currentResult = entry; | |
| setViewMode('result'); | |
| outputJson.value = entry.text || ''; | |
| dlBtn.disabled = false; | |
| zoomBtn.disabled = false; | |
| } | |
| function renderHistory() { | |
| historyStrip.innerHTML = ''; | |
| historyCount.textContent = history.length; | |
| if (history.length === 0) { | |
| const empty = document.createElement('div'); | |
| empty.className = 'fs-empty'; | |
| empty.textContent = 'No results yet'; | |
| historyStrip.appendChild(empty); | |
| return; | |
| } | |
| history.forEach((entry, i) => { | |
| const t = document.createElement('div'); | |
| t.className = 'fs-thumb' + (i === historyIdx ? ' selected' : ''); | |
| const im = document.createElement('img'); | |
| im.src = entry.b64; | |
| t.appendChild(im); | |
| t.addEventListener('click', () => { historyIdx = i; showResult(history[i]); renderHistory(); }); | |
| historyStrip.appendChild(t); | |
| }); | |
| } | |
| function pushHistory(entry) { | |
| history.push(entry); | |
| historyIdx = history.length - 1; | |
| renderHistory(); | |
| updateCounts(); | |
| } | |
| /* ── Lightbox ── */ | |
| lightbox.addEventListener('click', () => lightbox.classList.remove('visible')); | |
| document.addEventListener('keydown', (e) => { if (e.key === 'Escape') lightbox.classList.remove('visible'); }); | |
| zoomBtn.addEventListener('click', () => { | |
| const img = getCanvasImg(); | |
| const src = (viewMode === 'result' && currentResult) ? currentResult.b64 | |
| : (currentImage ? currentImage.b64 : null); | |
| if (src) { lightboxImg.src = src; lightbox.classList.add('visible'); } | |
| }); | |
| /* ── Download ── */ | |
| dlBtn.addEventListener('click', () => { | |
| if (currentResult && currentResult.b64) { | |
| const a = document.createElement('a'); | |
| a.href = currentResult.b64; | |
| a.download = 'qwen_object_detection.png'; | |
| document.body.appendChild(a); a.click(); document.body.removeChild(a); | |
| } | |
| }); | |
| /* ── Backend connection ── */ | |
| let client = null; | |
| try { | |
| client = await Client.connect(window.location.origin); | |
| setStatus('Ready'); | |
| connDot.classList.remove('off'); | |
| connText.textContent = 'connected'; | |
| } catch (e) { | |
| console.error('Failed to connect to Gradio backend:', e); | |
| setStatus('Offline', 'error'); | |
| connDot.classList.add('off'); | |
| connText.textContent = 'offline'; | |
| showToast('Could not connect to the backend API', 'error'); | |
| } | |
| /* ── Config: examples ── */ | |
| function renderConfig(cfg) { | |
| const list = document.getElementById('examples-scroll'); | |
| list.innerHTML = ''; | |
| cfg.examples.forEach(ex => { | |
| const card = document.createElement('div'); | |
| card.className = 'example-card'; | |
| const thumbHtml = ex.thumb | |
| ? '<img src="' + ex.thumb + '" alt="">' | |
| : '<div class="example-thumb-placeholder">Preview</div>'; | |
| const promptShort = ex.prompt.length > 90 ? ex.prompt.slice(0, 90) + '…' : ex.prompt; | |
| card.innerHTML = | |
| '<div class="example-thumb-wrap">' + thumbHtml + '</div>' | |
| + '<div class="example-body">' | |
| + '<div class="example-meta"><span class="example-lora-badge"></span></div>' | |
| + '<div class="example-prompt-text"></div>' | |
| + '</div>'; | |
| card.querySelector('.example-lora-badge').textContent = ex.task; | |
| card.querySelector('.example-prompt-text').textContent = promptShort; | |
| card.addEventListener('click', () => loadExample(card, ex.idx)); | |
| list.appendChild(card); | |
| }); | |
| if (cfg.default_gpu_idx !== undefined) { | |
| gpuDurSlider.value = cfg.default_gpu_idx; | |
| gpuDurVal.textContent = GPU_DURATIONS[cfg.default_gpu_idx] + 's'; | |
| } | |
| } | |
| async function loadExample(card, idx) { | |
| if (!client) { showToast('Backend not connected', 'error'); return; } | |
| document.querySelectorAll('.example-card.loading').forEach(c => c.classList.remove('loading')); | |
| card.classList.add('loading'); | |
| showToast('Loading example…', 'info'); | |
| try { | |
| const result = await client.predict('/load_example', { idx: idx }); | |
| const data = result.data[0]; | |
| if (data && data.status === 'ok' && data.image) { | |
| if (data.prompt) { promptInput.value = data.prompt; updateCharCount(); } | |
| if (data.task) { | |
| taskSelect.value = data.task; | |
| } | |
| setImage(data.image, data.name || 'example.jpg'); | |
| tabEdit.click(); | |
| showToast('Example loaded', 'info'); | |
| } else { | |
| showToast('Could not load example image', 'error'); | |
| } | |
| } catch (e) { | |
| console.error('Example load error:', e); | |
| showToast('Could not load example image', 'error'); | |
| } finally { | |
| card.classList.remove('loading'); | |
| } | |
| } | |
| try { | |
| const cfg = await fetch('api/config').then(r => r.json()); | |
| renderConfig(cfg); | |
| } catch (e) { | |
| console.error('Failed to load config:', e); | |
| } | |
| /* ── Run ── */ | |
| function validateBeforeRun() { | |
| const promptVal = promptInput.value.trim(); | |
| const hasImage = !!currentImage; | |
| if (!hasImage && !promptVal) { showToast('Please upload an image and enter a prompt', 'error'); flashPromptError(); return false; } | |
| if (!hasImage) { showToast('Please upload an image', 'error'); return false; } | |
| if (!promptVal) { showToast('Please enter an edit prompt', 'warning'); flashPromptError(); return false; } | |
| return true; | |
| } | |
| function setRunningUI(on) { | |
| running = on; | |
| runBtn.disabled = on; | |
| runBtnLabel.textContent = on ? 'Running…' : 'Run Inference'; | |
| cancelBtn.style.display = on ? 'flex' : 'none'; | |
| } | |
| async function runEdit() { | |
| if (running) return; | |
| if (!client) { showToast('Backend not connected', 'error'); return; } | |
| if (!validateBeforeRun()) return; | |
| setRunningUI(true); | |
| setViewMode('result'); | |
| showLoader('Submitting to queue…'); | |
| const usedPrompt = promptInput.value; | |
| const usedTask = taskSelect.value; | |
| const payload = { | |
| image_b64: currentImage ? currentImage.b64 : '', | |
| prompt: usedPrompt, | |
| task_type: usedTask, | |
| point_radius: parseInt(pointRSlider.value) || 4, | |
| box_thickness: parseInt(boxWSlider.value) || 1, | |
| text_scale: parseFloat(txtSSlider.value) || 0.4, | |
| gpu_duration_seconds: GPU_DURATIONS[gpuDurSlider.value], | |
| }; | |
| try { | |
| currentJob = client.submit('/run_inference', payload); | |
| for await (const msg of currentJob) { | |
| if (msg.type === 'status') { | |
| if (msg.status === 'pending') { | |
| const pos = (msg.position != null && msg.position >= 0) | |
| ? 'In queue — position ' + (msg.position + 1) + (msg.queue_size ? ' of ' + msg.queue_size : '') | |
| : 'Waiting in queue…'; | |
| setLoaderProgress(pos, null); | |
| setStatus('Queued'); | |
| } else if (msg.status === 'generating') { | |
| setStatus('Processing…'); | |
| const pd = msg.progress_data; | |
| if (pd && pd.length > 0) { | |
| const p = pd[pd.length - 1]; | |
| if (p.index != null && p.length) { | |
| const pct = Math.round((p.index / p.length) * 100); | |
| setLoaderProgress('Generating… ' + p.index + '/' + p.length + ' (' + pct + '%)', p.index / p.length); | |
| } else { | |
| setLoaderProgress('Generating…', null); | |
| } | |
| } else { | |
| setLoaderProgress('Processing image…', null); | |
| } | |
| } else if (msg.status === 'error') { | |
| throw new Error(msg.message || 'Generation failed'); | |
| } | |
| } else if (msg.type === 'data') { | |
| const out = msg.data && msg.data[0]; | |
| if (out && out.image) { | |
| const entry = { b64: out.image, text: out.text }; | |
| showResult(entry); | |
| pushHistory(entry); | |
| hideLoader('Done', 'done'); | |
| showToast('Inference successful', 'info'); | |
| } else { | |
| hideLoader('Done', 'done'); | |
| } | |
| } | |
| } | |
| } catch (e) { | |
| console.error('Edit error:', e); | |
| hideLoader('Error', 'error'); | |
| const msg = (e && e.message) ? e.message : 'Generation failed'; | |
| showToast(msg.length > 120 ? msg.slice(0, 120) + '…' : msg, 'error'); | |
| } finally { | |
| setRunningUI(false); | |
| currentJob = null; | |
| } | |
| } | |
| runBtn.addEventListener('click', runEdit); | |
| cancelBtn.addEventListener('click', async () => { | |
| if (currentJob) { | |
| try { await currentJob.cancel(); } catch (e) { console.warn('Cancel error:', e); } | |
| currentJob = null; | |
| } | |
| setRunningUI(false); | |
| hideLoader('Cancelled'); | |
| showToast('Generation cancelled', 'warning'); | |
| }); | |
| document.addEventListener('keydown', (e) => { | |
| if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') { | |
| e.preventDefault(); | |
| runEdit(); | |
| } | |
| }); | |
| renderFilmstrip(); | |
| renderHistory(); | |
| updateCounts(); | |
| renderCanvas(); | |
| </script> | |
| </body> | |
| </html> |