Spaces:
Build error
Build error
| <html lang="ar" dir="rtl"> | |
| <head> | |
| <meta charset="UTF-8"/> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"/> | |
| <title>ุฅุฒุงูุฉ ุงูุฎูููุฉ ุงูุงุญุชุฑุงููุฉ</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"/> | |
| <link href="https://fonts.googleapis.com/css2?family=Cairo:wght@300;400;600;700;900&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet"/> | |
| <style> | |
| /* โโโ TOKENS โโโ */ | |
| :root { | |
| --bg: #050709; | |
| --bg2: #0a0d12; | |
| --bg3: #0f1318; | |
| --surface: #131820; | |
| --surface2: #1a2030; | |
| --border: #1e2836; | |
| --border2: #253040; | |
| --cyan: #00e5ff; | |
| --cyan2: #00b8d4; | |
| --green: #00ff9d; | |
| --amber: #ffb300; | |
| --red: #ff4757; | |
| --text: #e8edf4; | |
| --text2: #8899aa; | |
| --text3: #4a5568; | |
| --glow-c: 0 0 20px #00e5ff40, 0 0 60px #00e5ff18; | |
| --glow-g: 0 0 20px #00ff9d40, 0 0 60px #00ff9d18; | |
| --radius: 14px; | |
| --radius-sm: 8px; | |
| --font: 'Cairo', sans-serif; | |
| --mono: 'JetBrains Mono', monospace; | |
| } | |
| /* โโโ RESET โโโ */ | |
| *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } | |
| html { scroll-behavior: smooth; } | |
| body { | |
| font-family: var(--font); | |
| background: var(--bg); | |
| color: var(--text); | |
| min-height: 100vh; | |
| overflow-x: hidden; | |
| } | |
| /* โโโ BACKGROUND GRID โโโ */ | |
| body::before { | |
| content: ''; | |
| position: fixed; inset: 0; z-index: 0; | |
| background-image: | |
| linear-gradient(rgba(0,229,255,.025) 1px, transparent 1px), | |
| linear-gradient(90deg, rgba(0,229,255,.025) 1px, transparent 1px); | |
| background-size: 60px 60px; | |
| pointer-events: none; | |
| } | |
| body::after { | |
| content: ''; | |
| position: fixed; inset: 0; z-index: 0; | |
| background: radial-gradient(ellipse 80% 60% at 50% -10%, #00e5ff0d 0%, transparent 65%); | |
| pointer-events: none; | |
| } | |
| /* โโโ SCROLLBAR โโโ */ | |
| ::-webkit-scrollbar { width: 6px; } | |
| ::-webkit-scrollbar-track { background: var(--bg2); } | |
| ::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 99px; } | |
| /* โโโ LAYOUT โโโ */ | |
| .app { | |
| position: relative; z-index: 1; | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| padding: 0 20px 80px; | |
| } | |
| /* โโโ HEADER โโโ */ | |
| header { | |
| padding: 32px 0 20px; | |
| display: flex; align-items: center; justify-content: space-between; | |
| border-bottom: 1px solid var(--border); | |
| margin-bottom: 36px; | |
| } | |
| .logo { | |
| display: flex; align-items: center; gap: 14px; | |
| } | |
| .logo-icon { | |
| width: 48px; height: 48px; border-radius: 12px; | |
| background: linear-gradient(135deg, #00e5ff22, #00ff9d15); | |
| border: 1px solid #00e5ff40; | |
| display: grid; place-items: center; | |
| font-size: 22px; | |
| box-shadow: var(--glow-c); | |
| } | |
| .logo-text h1 { | |
| font-size: 1.5rem; font-weight: 900; letter-spacing: -0.3px; | |
| background: linear-gradient(90deg, #fff 30%, var(--cyan)); | |
| -webkit-background-clip: text; -webkit-text-fill-color: transparent; | |
| line-height: 1.1; | |
| } | |
| .logo-text p { font-size: .78rem; color: var(--text2); margin-top: 2px; } | |
| .queue-badge { | |
| display: flex; align-items: center; gap: 8px; | |
| background: var(--surface); border: 1px solid var(--border); | |
| border-radius: 99px; padding: 8px 18px; | |
| font-size: .82rem; color: var(--text2); | |
| cursor: default; | |
| } | |
| .queue-dot { | |
| width: 8px; height: 8px; border-radius: 50%; | |
| background: var(--green); | |
| animation: pulse 2s ease-in-out infinite; | |
| } | |
| @keyframes pulse { 0%,100%{opacity:1;transform:scale(1)} 50%{opacity:.5;transform:scale(.8)} } | |
| /* โโโ MAIN GRID โโโ */ | |
| .main-grid { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 24px; | |
| } | |
| @media (max-width: 768px) { | |
| .main-grid { grid-template-columns: 1fr; } | |
| header { flex-direction: column; align-items: flex-start; gap: 16px; } | |
| } | |
| /* โโโ PANEL โโโ */ | |
| .panel { | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius); | |
| overflow: hidden; | |
| } | |
| .panel-header { | |
| padding: 16px 20px; | |
| border-bottom: 1px solid var(--border); | |
| display: flex; align-items: center; gap: 10px; | |
| font-size: .85rem; font-weight: 600; color: var(--text2); | |
| text-transform: uppercase; letter-spacing: 1.5px; | |
| } | |
| .panel-header .icon { font-size: 1rem; } | |
| .panel-body { padding: 20px; } | |
| /* โโโ DROPZONE โโโ */ | |
| .dropzone { | |
| border: 2px dashed var(--border2); | |
| border-radius: var(--radius); | |
| min-height: 220px; | |
| display: flex; flex-direction: column; align-items: center; justify-content: center; | |
| gap: 14px; cursor: pointer; | |
| transition: border-color .2s, background .2s; | |
| position: relative; overflow: hidden; | |
| background: var(--bg2); | |
| } | |
| .dropzone:hover, .dropzone.dragover { | |
| border-color: var(--cyan); | |
| background: #00e5ff08; | |
| } | |
| .dropzone.dragover::after { | |
| content: ''; | |
| position: absolute; inset: 0; | |
| background: linear-gradient(45deg, transparent 40%, #00e5ff08 50%, transparent 60%); | |
| background-size: 200% 200%; | |
| animation: scan 1.2s linear infinite; | |
| } | |
| @keyframes scan { 0%{background-position:200% 200%} 100%{background-position:-200% -200%} } | |
| .dropzone-icon { | |
| font-size: 3rem; filter: grayscale(0.3); | |
| transition: transform .3s; | |
| } | |
| .dropzone:hover .dropzone-icon { transform: scale(1.1); } | |
| .dropzone p { font-size: .9rem; color: var(--text2); text-align: center; } | |
| .dropzone small { font-size: .75rem; color: var(--text3); } | |
| .dropzone input { position: absolute; inset: 0; opacity: 0; cursor: pointer; } | |
| /* โโโ PREVIEW โโโ */ | |
| .preview-wrap { | |
| margin-top: 16px; display: none; | |
| border-radius: var(--radius-sm); overflow: hidden; | |
| border: 1px solid var(--border); | |
| background: var(--bg2); | |
| } | |
| .preview-wrap.show { display: block; } | |
| .preview-wrap img { | |
| width: 100%; max-height: 260px; | |
| object-fit: contain; display: block; | |
| background: repeating-conic-gradient(#1a2030 0% 25%, #111820 0% 50%) 0 0 / 16px 16px; | |
| } | |
| .preview-meta { | |
| padding: 10px 14px; | |
| font-size: .78rem; color: var(--text3); | |
| font-family: var(--mono); | |
| display: flex; justify-content: space-between; | |
| } | |
| /* โโโ MODE TOGGLE โโโ */ | |
| .mode-section { margin-top: 18px; } | |
| .mode-label { | |
| font-size: .78rem; color: var(--text2); | |
| text-transform: uppercase; letter-spacing: 1.5px; | |
| margin-bottom: 10px; font-weight: 600; | |
| } | |
| .mode-toggle { | |
| display: grid; grid-template-columns: 1fr 1fr; | |
| gap: 10px; | |
| } | |
| .mode-btn { | |
| background: var(--bg2); border: 1px solid var(--border2); | |
| border-radius: var(--radius-sm); | |
| padding: 14px 12px; cursor: pointer; | |
| transition: all .2s; text-align: center; | |
| color: var(--text2); font-family: var(--font); | |
| } | |
| .mode-btn:hover { border-color: var(--cyan2); color: var(--text); } | |
| .mode-btn.active { | |
| border-color: var(--cyan); | |
| background: #00e5ff0f; | |
| color: var(--cyan); | |
| box-shadow: 0 0 14px #00e5ff22; | |
| } | |
| .mode-btn .mode-icon { font-size: 1.4rem; margin-bottom: 6px; } | |
| .mode-btn .mode-name { font-size: .88rem; font-weight: 700; } | |
| .mode-btn .mode-desc { font-size: .72rem; margin-top: 4px; opacity: .7; line-height: 1.4; } | |
| /* โโโ SUBMIT โโโ */ | |
| .btn-submit { | |
| width: 100%; margin-top: 16px; | |
| background: linear-gradient(135deg, var(--cyan2), var(--cyan)); | |
| border: none; border-radius: var(--radius-sm); | |
| padding: 15px 20px; cursor: pointer; | |
| color: #000; font-family: var(--font); font-size: 1rem; font-weight: 800; | |
| transition: all .2s; letter-spacing: .5px; | |
| } | |
| .btn-submit:hover:not(:disabled) { | |
| transform: translateY(-1px); | |
| box-shadow: 0 6px 24px #00e5ff50; | |
| } | |
| .btn-submit:disabled { opacity: .4; cursor: not-allowed; transform: none; } | |
| /* โโโ PROGRESS โโโ */ | |
| .progress-card { | |
| display: none; margin-top: 16px; | |
| background: var(--bg2); border: 1px solid var(--border); | |
| border-radius: var(--radius-sm); padding: 16px; | |
| } | |
| .progress-card.show { display: block; } | |
| .progress-stage { | |
| font-size: .85rem; color: var(--text2); margin-bottom: 10px; | |
| display: flex; align-items: center; gap: 8px; | |
| } | |
| .progress-stage .spin { | |
| width: 14px; height: 14px; border: 2px solid var(--border2); | |
| border-top-color: var(--cyan); border-radius: 50%; | |
| animation: spin .8s linear infinite; flex-shrink: 0; | |
| } | |
| @keyframes spin { to { transform: rotate(360deg); } } | |
| .progress-bar-wrap { | |
| height: 4px; background: var(--border); border-radius: 2px; overflow: hidden; | |
| } | |
| .progress-bar-fill { | |
| height: 100%; background: linear-gradient(90deg, var(--cyan2), var(--cyan)); | |
| border-radius: 2px; width: 0%; | |
| transition: width .4s ease; | |
| position: relative; overflow: hidden; | |
| } | |
| .progress-bar-fill::after { | |
| content: ''; | |
| position: absolute; top: 0; right: -100%; height: 100%; width: 100%; | |
| background: linear-gradient(90deg, transparent, rgba(255,255,255,.4), transparent); | |
| animation: shimmer 1.5s ease-in-out infinite; | |
| } | |
| @keyframes shimmer { to { right: 100%; } } | |
| .queue-info-line { | |
| font-size: .75rem; color: var(--text3); margin-top: 8px; | |
| font-family: var(--mono); | |
| } | |
| /* โโโ RESULT PANEL โโโ */ | |
| .result-empty { | |
| display: flex; flex-direction: column; | |
| align-items: center; justify-content: center; | |
| min-height: 400px; gap: 12px; | |
| color: var(--text3); text-align: center; | |
| } | |
| .result-empty .empty-icon { font-size: 3rem; opacity: .3; } | |
| .result-empty p { font-size: .88rem; } | |
| /* โโโ COMPARISON SLIDER โโโ */ | |
| .compare-wrap { | |
| display: none; position: relative; | |
| border-radius: var(--radius-sm); overflow: hidden; | |
| user-select: none; background: repeating-conic-gradient(#1a2030 0% 25%, #111820 0% 50%) 0 0 / 16px 16px; | |
| cursor: ew-resize; | |
| } | |
| .compare-wrap.show { display: block; } | |
| .compare-wrap img { | |
| width: 100%; max-height: 380px; | |
| object-fit: contain; display: block; | |
| } | |
| .compare-before { | |
| position: absolute; inset: 0; | |
| overflow: hidden; | |
| } | |
| .compare-before img { | |
| position: absolute; top: 0; right: 0; /* RTL */ | |
| height: 100%; object-fit: contain; | |
| width: 100%; max-height: unset; | |
| background: var(--bg3); | |
| } | |
| .compare-clip { width: 50%; } | |
| .compare-handle { | |
| position: absolute; top: 0; bottom: 0; | |
| width: 3px; background: #fff; cursor: ew-resize; | |
| left: 50%; transform: translateX(-50%); | |
| z-index: 10; transition: background .2s; | |
| } | |
| .compare-handle::before { | |
| content: 'โบ'; | |
| position: absolute; top: 50%; left: 50%; | |
| transform: translate(-50%, -50%); | |
| background: #fff; color: #000; | |
| width: 28px; height: 28px; border-radius: 50%; | |
| display: grid; place-items: center; | |
| font-size: 11px; font-weight: 900; | |
| box-shadow: 0 2px 12px #0008; | |
| } | |
| .compare-labels { | |
| position: absolute; top: 10px; left: 0; right: 0; | |
| display: flex; justify-content: space-between; | |
| padding: 0 14px; pointer-events: none; z-index: 5; | |
| } | |
| .compare-labels span { | |
| background: #000a; color: #fff; | |
| font-size: .72rem; padding: 3px 8px; border-radius: 4px; | |
| font-family: var(--mono); | |
| } | |
| /* โโโ RESULT ACTIONS โโโ */ | |
| .result-actions { | |
| display: none; margin-top: 14px; gap: 10px; | |
| flex-wrap: wrap; | |
| } | |
| .result-actions.show { display: flex; } | |
| .btn-dl { | |
| flex: 1; min-width: 120px; | |
| padding: 11px 16px; | |
| border-radius: var(--radius-sm); border: none; cursor: pointer; | |
| font-family: var(--font); font-size: .88rem; font-weight: 700; | |
| transition: all .2s; display: flex; align-items: center; justify-content: center; gap: 6px; | |
| } | |
| .btn-dl.png { | |
| background: var(--surface2); border: 1px solid var(--border2); color: var(--text); | |
| } | |
| .btn-dl.png:hover { border-color: var(--cyan); color: var(--cyan); } | |
| .btn-dl.webp { | |
| background: linear-gradient(135deg, #00ff9d22, #00ff9d12); | |
| border: 1px solid #00ff9d40; color: var(--green); | |
| } | |
| .btn-dl.webp:hover { box-shadow: var(--glow-g); } | |
| /* โโโ ANALYSIS BOX โโโ */ | |
| .analysis-box { | |
| display: none; margin-top: 14px; | |
| background: var(--bg2); border: 1px solid var(--border); | |
| border-radius: var(--radius-sm); padding: 14px 16px; | |
| } | |
| .analysis-box.show { display: block; } | |
| .analysis-title { | |
| font-size: .72rem; color: var(--cyan2); font-weight: 700; | |
| text-transform: uppercase; letter-spacing: 1.5px; margin-bottom: 8px; | |
| display: flex; align-items: center; gap: 6px; | |
| } | |
| .analysis-text { | |
| font-size: .82rem; color: var(--text2); line-height: 1.7; | |
| white-space: pre-wrap; | |
| } | |
| /* โโโ STATS ROW โโโ */ | |
| .stats-row { | |
| display: none; margin-top: 14px; gap: 10px; | |
| } | |
| .stats-row.show { display: flex; } | |
| .stat-chip { | |
| flex: 1; background: var(--bg2); border: 1px solid var(--border); | |
| border-radius: var(--radius-sm); padding: 10px 14px; text-align: center; | |
| } | |
| .stat-chip .sv { font-size: 1.1rem; font-weight: 800; color: var(--cyan); font-family: var(--mono); } | |
| .stat-chip .sl { font-size: .7rem; color: var(--text3); margin-top: 2px; text-transform: uppercase; letter-spacing: 1px; } | |
| /* โโโ ERROR โโโ */ | |
| .error-box { | |
| display: none; margin-top: 14px; | |
| background: #ff475710; border: 1px solid #ff475740; | |
| border-radius: var(--radius-sm); padding: 14px 16px; | |
| font-size: .84rem; color: var(--red); line-height: 1.6; | |
| } | |
| .error-box.show { display: block; } | |
| /* โโโ TOAST โโโ */ | |
| .toast-wrap { | |
| position: fixed; bottom: 30px; left: 50%; transform: translateX(-50%); | |
| z-index: 1000; display: flex; flex-direction: column; align-items: center; gap: 8px; | |
| } | |
| .toast { | |
| background: var(--surface2); border: 1px solid var(--border2); | |
| border-radius: 99px; padding: 10px 22px; | |
| font-size: .84rem; color: var(--text); | |
| animation: toastIn .3s ease forwards; | |
| box-shadow: 0 4px 24px #0008; | |
| max-width: 340px; text-align: center; | |
| } | |
| .toast.error { border-color: var(--red); color: var(--red); background: #ff47570e; } | |
| .toast.success { border-color: var(--green); color: var(--green); background: #00ff9d0e; } | |
| @keyframes toastIn { from{opacity:0;transform:translateY(10px)} to{opacity:1;transform:translateY(0)} } | |
| @keyframes toastOut { to{opacity:0;transform:translateY(10px)} } | |
| /* โโโ SPINNER OVERLAY โโโ */ | |
| .overlay { | |
| display: none; position: absolute; inset: 0; | |
| background: #05070980; backdrop-filter: blur(4px); | |
| border-radius: var(--radius); z-index: 20; | |
| align-items: center; justify-content: center; | |
| flex-direction: column; gap: 14px; | |
| } | |
| .overlay.show { display: flex; } | |
| .overlay-text { font-size: .88rem; color: var(--cyan2); } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="app"> | |
| <!-- HEADER --> | |
| <header> | |
| <div class="logo"> | |
| <div class="logo-icon">โ๏ธ</div> | |
| <div class="logo-text"> | |
| <h1>ุฅุฒุงูุฉ ุงูุฎูููุฉ AI</h1> | |
| <p>ุฏูุฉ ุนุงููุฉ ุญุชู 4K โ ุจุฏูู ุชุดููู</p> | |
| </div> | |
| </div> | |
| <div class="queue-badge"> | |
| <div class="queue-dot" id="queueDot"></div> | |
| <span id="queueText">ุฌุงุฑู ุงูุชุญู ูู...</span> | |
| </div> | |
| </header> | |
| <!-- MAIN GRID --> | |
| <div class="main-grid"> | |
| <!-- LEFT: UPLOAD --> | |
| <div class="panel"> | |
| <div class="panel-header"> | |
| <span class="icon">๐ค</span> | |
| ุฑูุน ุงูุตูุฑุฉ | |
| </div> | |
| <div class="panel-body"> | |
| <!-- Dropzone --> | |
| <div class="dropzone" id="dropzone"> | |
| <div class="dropzone-icon">๐ผ๏ธ</div> | |
| <p>ุงุณุญุจ ุตูุฑุชู ููุง ุฃู ุงููุฑ ููุงุฎุชูุงุฑ</p> | |
| <small>PNG โข JPG โข WebP โข BMP โข TIFF โข AVIF โ ุญุชู 100MB</small> | |
| <input type="file" id="fileInput" accept="image/*"/> | |
| </div> | |
| <!-- Preview --> | |
| <div class="preview-wrap" id="previewWrap"> | |
| <img id="previewImg" src="" alt="ู ุนุงููุฉ"/> | |
| <div class="preview-meta"> | |
| <span id="previewName">โ</span> | |
| <span id="previewSize">โ</span> | |
| </div> | |
| </div> | |
| <!-- Mode --> | |
| <div class="mode-section"> | |
| <div class="mode-label">ูุถุน ุงูุฅุฒุงูุฉ</div> | |
| <div class="mode-toggle"> | |
| <button class="mode-btn active" data-mode="fast" onclick="setMode('fast')"> | |
| <div class="mode-icon">โก</div> | |
| <div class="mode-name">ุงููุถุน ุงูุณุฑูุน</div> | |
| <div class="mode-desc">ู ุซุงูู ููุตูุฑ ุงููุงุถุญุฉ โ ูุชูุฌุฉ ููุฑูุฉ</div> | |
| </button> | |
| <button class="mode-btn" data-mode="thinking" onclick="setMode('thinking')"> | |
| <div class="mode-icon">๐ง </div> | |
| <div class="mode-name">ูุถุน ุงูุชูููุฑ</div> | |
| <div class="mode-desc">ุฃูุตู ุฏูุฉ โ ุดุนุฑ ูุชูุงุตูู ุฏูููุฉ โ ุญุชู ุฏูููุชูู</div> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Submit --> | |
| <button class="btn-submit" id="submitBtn" onclick="submitImage()" disabled> | |
| ุงุจุฏุฃ ุงูุฅุฒุงูุฉ | |
| </button> | |
| <!-- Progress --> | |
| <div class="progress-card" id="progressCard"> | |
| <div class="progress-stage" id="progressStage"> | |
| <div class="spin"></div> | |
| <span id="progressText">ุฌุงุฑู ุงูู ุนุงูุฌุฉ...</span> | |
| </div> | |
| <div class="progress-bar-wrap"> | |
| <div class="progress-bar-fill" id="progressFill"></div> | |
| </div> | |
| <div class="queue-info-line" id="queueLine"></div> | |
| </div> | |
| <!-- Error --> | |
| <div class="error-box" id="errorBox"></div> | |
| </div> | |
| </div> | |
| <!-- RIGHT: RESULT --> | |
| <div class="panel" style="position:relative;"> | |
| <div class="panel-header"> | |
| <span class="icon">โจ</span> | |
| ุงููุชูุฌุฉ | |
| <span id="resultMode" style="margin-right:auto;font-size:.7rem;color:var(--text3);"></span> | |
| </div> | |
| <div class="panel-body"> | |
| <!-- Empty state --> | |
| <div class="result-empty" id="resultEmpty"> | |
| <div class="empty-icon">๐ฎ</div> | |
| <p>ุงุฑูุน ุตูุฑุฉ ูุงุจุฏุฃ ุงูุฅุฒุงูุฉ ูุฑุคูุฉ ุงููุชูุฌุฉ ููุง</p> | |
| </div> | |
| <!-- Compare Slider --> | |
| <div class="compare-wrap" id="compareWrap"> | |
| <img id="resultImg" src="" alt="ุงููุชูุฌุฉ"/> | |
| <div class="compare-before" id="compareBefore"> | |
| <img id="originalImg" src="" alt="ุงูุฃุตููุฉ"/> | |
| </div> | |
| <div class="compare-handle" id="compareHandle"></div> | |
| <div class="compare-labels"> | |
| <span>ุงููุชูุฌุฉ</span> | |
| <span>ุงูุฃุตููุฉ</span> | |
| </div> | |
| </div> | |
| <!-- Stats --> | |
| <div class="stats-row" id="statsRow"> | |
| <div class="stat-chip"> | |
| <div class="sv" id="statTime">โ</div> | |
| <div class="sl">ููุช ุงูู ุนุงูุฌุฉ</div> | |
| </div> | |
| <div class="stat-chip"> | |
| <div class="sv" id="statSize">โ</div> | |
| <div class="sl">ุญุฌู ุงููุงุชุฌ</div> | |
| </div> | |
| <div class="stat-chip"> | |
| <div class="sv" id="statMode">โ</div> | |
| <div class="sl">ุงููุถุน</div> | |
| </div> | |
| </div> | |
| <!-- AI Analysis --> | |
| <div class="analysis-box" id="analysisBox"> | |
| <div class="analysis-title">๐ค ุชุญููู ุงูุฐูุงุก ุงูุงุตุทูุงุนู</div> | |
| <div class="analysis-text" id="analysisText"></div> | |
| </div> | |
| <!-- Download actions --> | |
| <div class="result-actions" id="resultActions"> | |
| <button class="btn-dl png" onclick="download('png')">โฌ๏ธ ุชุญู ูู PNG</button> | |
| <button class="btn-dl webp" onclick="download('webp')">โฌ๏ธ ุชุญู ูู WebP</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div><!-- /main-grid --> | |
| </div><!-- /app --> | |
| <!-- Toast container --> | |
| <div class="toast-wrap" id="toastWrap"></div> | |
| <script> | |
| /* โโโ STATE โโโ */ | |
| let selectedFile = null; | |
| let selectedMode = 'fast'; | |
| let currentTaskId = null; | |
| let ws = null; | |
| let progressInterval = null; | |
| let progressVal = 0; | |
| let isDragging = false; | |
| /* โโโ QUEUE POLLING โโโ */ | |
| async function pollQueue() { | |
| try { | |
| const r = await fetch('/queue-info'); | |
| const d = await r.json(); | |
| const dot = document.getElementById('queueDot'); | |
| const txt = document.getElementById('queueText'); | |
| txt.textContent = `ุงูุทุงุจูุฑ: ${d.waiting}/${d.max} โ ${d.processing ? 'ูุนู ู ุงูุขู' : 'ู ุชุงุญ'}`; | |
| dot.style.background = d.waiting >= d.max ? 'var(--amber)' : 'var(--green)'; | |
| } catch(e) { | |
| document.getElementById('queueText').textContent = 'ูุง ุงุชุตุงู'; | |
| } | |
| } | |
| pollQueue(); | |
| setInterval(pollQueue, 4000); | |
| /* โโโ FILE INPUT โโโ */ | |
| const dropzone = document.getElementById('dropzone'); | |
| const fileInput = document.getElementById('fileInput'); | |
| dropzone.addEventListener('dragover', e => { e.preventDefault(); dropzone.classList.add('dragover'); }); | |
| dropzone.addEventListener('dragleave', () => dropzone.classList.remove('dragover')); | |
| dropzone.addEventListener('drop', e => { | |
| e.preventDefault(); | |
| dropzone.classList.remove('dragover'); | |
| const f = e.dataTransfer.files[0]; | |
| if (f) handleFile(f); | |
| }); | |
| fileInput.addEventListener('change', () => { | |
| if (fileInput.files[0]) handleFile(fileInput.files[0]); | |
| }); | |
| function handleFile(f) { | |
| if (!f.type.startsWith('image/')) { | |
| toast('ููุณู ุญ ููุท ุจู ููุงุช ุงูุตูุฑ', 'error'); | |
| return; | |
| } | |
| if (f.size > 100 * 1024 * 1024) { | |
| toast('ุญุฌู ุงูู ูู ูุชุฌุงูุฒ 100MB', 'error'); | |
| return; | |
| } | |
| selectedFile = f; | |
| // Show preview | |
| const reader = new FileReader(); | |
| reader.onload = e => { | |
| document.getElementById('previewImg').src = e.target.result; | |
| document.getElementById('previewWrap').classList.add('show'); | |
| document.getElementById('originalImg').src = e.target.result; | |
| }; | |
| reader.readAsDataURL(f); | |
| document.getElementById('previewName').textContent = f.name; | |
| document.getElementById('previewSize').textContent = formatBytes(f.size); | |
| document.getElementById('submitBtn').disabled = false; | |
| resetResult(); | |
| } | |
| /* โโโ MODE โโโ */ | |
| function setMode(m) { | |
| selectedMode = m; | |
| document.querySelectorAll('.mode-btn').forEach(b => b.classList.toggle('active', b.dataset.mode === m)); | |
| } | |
| /* โโโ SUBMIT โโโ */ | |
| async function submitImage() { | |
| if (!selectedFile) return; | |
| const btn = document.getElementById('submitBtn'); | |
| btn.disabled = true; | |
| resetResult(); | |
| showProgress('ุฌุงุฑู ุฑูุน ุงูุตูุฑุฉ...', 5); | |
| const fd = new FormData(); | |
| fd.append('file', selectedFile); | |
| fd.append('mode', selectedMode); | |
| try { | |
| const res = await fetch('/upload', { method: 'POST', body: fd }); | |
| const data = await res.json(); | |
| if (!res.ok) { | |
| showError(data.detail || 'ูุดู ูู ุฑูุน ุงูุตูุฑุฉ'); | |
| btn.disabled = false; | |
| return; | |
| } | |
| currentTaskId = data.task_id; | |
| const pos = data.queue_pos; | |
| if (pos > 1) { | |
| showProgress(`ูู ุงูุทุงุจูุฑ โ ุงูู ููุน ${pos}/${data.queue_total}`, 10); | |
| setQueueLine(`ู ูู ุชู ุฑูู ${pos} ูู ุงูุงูุชุธุงุฑ`); | |
| } else { | |
| showProgress('ุฌุงุฑู ุงูุจุฏุก...', 15); | |
| } | |
| connectWebSocket(data.task_id); | |
| } catch(e) { | |
| showError('ุฎุทุฃ ูู ุงูุงุชุตุงู ุจุงูุฎุงุฏู '); | |
| btn.disabled = false; | |
| hideProgress(); | |
| } | |
| } | |
| /* โโโ WEBSOCKET โโโ */ | |
| function connectWebSocket(taskId) { | |
| if (ws) { ws.close(); ws = null; } | |
| const protocol = location.protocol === 'https:' ? 'wss' : 'ws'; | |
| ws = new WebSocket(`${protocol}://${location.host}/ws/${taskId}`); | |
| ws.onmessage = e => { | |
| const msg = JSON.parse(e.data); | |
| handleWSMessage(msg); | |
| }; | |
| ws.onerror = () => { | |
| // fallback to polling | |
| if (currentTaskId === taskId) startPolling(taskId); | |
| }; | |
| ws.onclose = () => {}; | |
| } | |
| function handleWSMessage(msg) { | |
| switch(msg.event) { | |
| case 'queued': | |
| case 'position_update': | |
| showProgress(`ูู ุงูุทุงุจูุฑ โ ุงูู ููุน ${msg.position}/${msg.total}`, 10); | |
| setQueueLine(`ุงูุชุธุงุฑ: ${msg.position} ู ู ${msg.total}`); | |
| break; | |
| case 'stage': | |
| const stages = { | |
| 'ุชุญููู': 30, 'ุชุญููู ุงูุตูุฑุฉ': 30, | |
| 'ุฅุฒุงูุฉ ุงูุฎูููุฉ': 60, 'ุชูููุฏ': 85, | |
| }; | |
| let pv = 20; | |
| for (const [k, v] of Object.entries(stages)) { | |
| if ((msg.stage || '').includes(k)) { pv = v; break; } | |
| } | |
| showProgress(msg.stage || 'ู ุนุงูุฌุฉ...', pv); | |
| if (msg.analysis) setAnalysis(msg.analysis); | |
| setQueueLine(''); | |
| break; | |
| case 'completed': | |
| onCompleted(msg); | |
| break; | |
| case 'failed': | |
| onFailed(msg.error); | |
| break; | |
| } | |
| } | |
| /* โโโ POLLING FALLBACK โโโ */ | |
| function startPolling(taskId) { | |
| clearInterval(progressInterval); | |
| progressInterval = setInterval(async () => { | |
| try { | |
| const r = await fetch(`/status/${taskId}`); | |
| const d = await r.json(); | |
| if (d.status === 'completed') { clearInterval(progressInterval); onCompleted(d); } | |
| else if (d.status === 'failed') { clearInterval(progressInterval); onFailed(d.error); } | |
| else if (d.status === 'pending') showProgress(`ูู ุงูุทุงุจูุฑ โ ${d.queue_pos}`, 10); | |
| else if (d.status === 'processing') showProgress(d.stage || 'ู ุนุงูุฌุฉ...', 50); | |
| } catch(e) {} | |
| }, 1500); | |
| } | |
| /* โโโ COMPLETED โโโ */ | |
| function onCompleted(msg) { | |
| hideProgress(); | |
| document.getElementById('submitBtn').disabled = false; | |
| const resultImg = document.getElementById('resultImg'); | |
| resultImg.src = `/preview/${currentTaskId}?t=${Date.now()}`; | |
| resultImg.onload = () => initCompareSlider(); | |
| document.getElementById('compareWrap').classList.add('show'); | |
| document.getElementById('resultEmpty').style.display = 'none'; | |
| document.getElementById('resultActions').classList.add('show'); | |
| document.getElementById('statsRow').classList.add('show'); | |
| const t = parseFloat(msg.proc_time || 0); | |
| document.getElementById('statTime').textContent = t ? `${t.toFixed(1)}ุซ` : 'โ'; | |
| document.getElementById('statSize').textContent = msg.size_kb ? `${msg.size_kb}KB` : 'โ'; | |
| document.getElementById('statMode').textContent = selectedMode === 'thinking' ? '๐ง ' : 'โก'; | |
| document.getElementById('resultMode').textContent = selectedMode === 'thinking' ? 'ูุถุน ุงูุชูููุฑ' : 'ุงููุถุน ุงูุณุฑูุน'; | |
| if (msg.analysis) setAnalysis(msg.analysis); | |
| toast('ุงูุชู ูุช ุงูุฅุฒุงูุฉ ุจูุฌุงุญ โ ', 'success'); | |
| } | |
| function onFailed(error) { | |
| hideProgress(); | |
| showError(error || 'ูุดูุช ุงูู ุนุงูุฌุฉ'); | |
| document.getElementById('submitBtn').disabled = false; | |
| toast('ูุดูุช ุงูู ุนุงูุฌุฉ โ', 'error'); | |
| } | |
| /* โโโ COMPARE SLIDER โโโ */ | |
| function initCompareSlider() { | |
| const wrap = document.getElementById('compareWrap'); | |
| const before = document.getElementById('compareBefore'); | |
| const handle = document.getElementById('compareHandle'); | |
| let dragging = false; | |
| function setPos(x) { | |
| const rect = wrap.getBoundingClientRect(); | |
| // RTL: flip x | |
| let rel = (rect.right - x) / rect.width; // RTL | |
| rel = Math.max(0.02, Math.min(0.98, rel)); | |
| const pct = (rel * 100).toFixed(1); | |
| before.style.width = pct + '%'; | |
| handle.style.left = (100 - rel * 100).toFixed(1) + '%'; | |
| } | |
| handle.addEventListener('mousedown', e => { dragging = true; e.preventDefault(); }); | |
| wrap.addEventListener('mousedown', e => { | |
| if (e.target === wrap || e.target === document.getElementById('resultImg')) { | |
| dragging = true; setPos(e.clientX); e.preventDefault(); | |
| } | |
| }); | |
| window.addEventListener('mousemove', e => { if (dragging) setPos(e.clientX); }); | |
| window.addEventListener('mouseup', () => { dragging = false; }); | |
| handle.addEventListener('touchstart', e => { dragging = true; e.preventDefault(); }, {passive:false}); | |
| window.addEventListener('touchmove', e => { if (dragging) setPos(e.touches[0].clientX); }, {passive:false}); | |
| window.addEventListener('touchend', () => { dragging = false; }); | |
| // Init at 50% | |
| setPos(wrap.getBoundingClientRect().left + wrap.offsetWidth / 2); | |
| } | |
| /* โโโ DOWNLOAD โโโ */ | |
| function download(fmt) { | |
| if (!currentTaskId) return; | |
| const a = document.createElement('a'); | |
| a.href = `/result/${currentTaskId}?fmt=${fmt}`; | |
| a.download = `nobg.${fmt}`; | |
| document.body.appendChild(a); a.click(); document.body.removeChild(a); | |
| } | |
| /* โโโ UI HELPERS โโโ */ | |
| function showProgress(text, pct) { | |
| document.getElementById('progressCard').classList.add('show'); | |
| document.getElementById('progressText').textContent = text; | |
| document.getElementById('progressFill').style.width = Math.max(progressVal, pct) + '%'; | |
| progressVal = Math.max(progressVal, pct); | |
| document.getElementById('errorBox').classList.remove('show'); | |
| } | |
| function hideProgress() { | |
| document.getElementById('progressFill').style.width = '100%'; | |
| setTimeout(() => { | |
| document.getElementById('progressCard').classList.remove('show'); | |
| progressVal = 0; | |
| document.getElementById('progressFill').style.width = '0%'; | |
| }, 600); | |
| } | |
| function setQueueLine(t) { document.getElementById('queueLine').textContent = t; } | |
| function showError(msg) { | |
| const box = document.getElementById('errorBox'); | |
| box.textContent = 'โ ๏ธ ' + msg; | |
| box.classList.add('show'); | |
| document.getElementById('progressCard').classList.remove('show'); | |
| } | |
| function setAnalysis(text) { | |
| document.getElementById('analysisBox').classList.add('show'); | |
| document.getElementById('analysisText').textContent = text; | |
| } | |
| function resetResult() { | |
| document.getElementById('compareWrap').classList.remove('show'); | |
| document.getElementById('resultActions').classList.remove('show'); | |
| document.getElementById('statsRow').classList.remove('show'); | |
| document.getElementById('analysisBox').classList.remove('show'); | |
| document.getElementById('errorBox').classList.remove('show'); | |
| document.getElementById('resultEmpty').style.display = ''; | |
| document.getElementById('progressCard').classList.remove('show'); | |
| progressVal = 0; | |
| if (ws) { ws.close(); ws = null; } | |
| clearInterval(progressInterval); | |
| } | |
| function formatBytes(b) { | |
| if (b < 1024) return b + ' B'; | |
| if (b < 1048576) return (b/1024).toFixed(1) + ' KB'; | |
| return (b/1048576).toFixed(1) + ' MB'; | |
| } | |
| function toast(msg, type='') { | |
| const wrap = document.getElementById('toastWrap'); | |
| const el = document.createElement('div'); | |
| el.className = 'toast ' + type; | |
| el.textContent = msg; | |
| wrap.appendChild(el); | |
| setTimeout(() => { | |
| el.style.animation = 'toastOut .3s ease forwards'; | |
| setTimeout(() => el.remove(), 300); | |
| }, 3200); | |
| } | |
| </script> | |
| </body> | |
| </html> | |