J / index.html
Skydata001's picture
Upload 5 files
7d6a5e2 verified
<!DOCTYPE html>
<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>