chome / index.html
chanraphone4's picture
Upload index.html
cd69fbf verified
Raw
History Blame Contribute Delete
67 kB
<!DOCTYPE html>
<html lang="km">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>αž§αž”αž€αžšαžŽαŸαž€αŸ’αž›αžΌαž“αžšαžΌαž”αž—αžΆαž– AI - AI Image Cloner</title>
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- FontAwesome for Icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Google Fonts (Kantumruy & Battambang for Khmer) -->
<link href="https://fonts.googleapis.com/css2?family=Kantumruy+Pro:wght@300;400;600;700&family=Battambang:wght@400;700&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Kantumruy Pro', 'Battambang', sans-serif;
background-color: #0f172a;
}
.glass {
background: rgba(30, 41, 59, 0.7);
backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.btn-pop-3d,
.choice-pop-3d,
.icon-pop-3d {
transform: translateY(0);
transition: transform 160ms ease, box-shadow 160ms ease, border-color 160ms ease, background 160ms ease;
}
.btn-pop-3d {
box-shadow: 0 7px 0 rgba(15, 23, 42, 0.95), 0 16px 32px rgba(99, 102, 241, 0.22);
}
.btn-pop-3d:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 9px 0 rgba(15, 23, 42, 0.95), 0 22px 40px rgba(99, 102, 241, 0.28);
}
.btn-pop-3d:active:not(:disabled) {
transform: translateY(4px);
box-shadow: 0 3px 0 rgba(15, 23, 42, 0.95), 0 10px 20px rgba(99, 102, 241, 0.18);
}
.btn-pop-3d:disabled {
transform: none;
box-shadow: none;
}
.choice-pop-3d {
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06), 0 5px 0 rgba(15, 23, 42, 0.9);
}
.choice-pop-3d:hover {
transform: translateY(-2px);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08), 0 7px 0 rgba(15, 23, 42, 0.9), 0 14px 26px rgba(99, 102, 241, 0.18);
}
.choice-pop-3d.active-style {
background: linear-gradient(180deg, rgba(79, 70, 229, 0.24), rgba(15, 23, 42, 0.72));
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 6px 0 rgba(49, 46, 129, 0.85), 0 16px 30px rgba(99, 102, 241, 0.22);
}
.icon-pop-3d {
box-shadow: 0 3px 0 rgba(15, 23, 42, 0.85);
}
.icon-pop-3d:hover {
transform: translateY(-1px);
box-shadow: 0 4px 0 rgba(15, 23, 42, 0.85), 0 10px 18px rgba(99, 102, 241, 0.14);
}
.icon-pop-3d:active {
transform: translateY(2px);
box-shadow: 0 1px 0 rgba(15, 23, 42, 0.85);
}
</style>
</head>
<body class="text-slate-100 min-h-screen flex flex-col justify-between">
<!-- Header -->
<header class="border-b border-slate-800 bg-slate-900/80 backdrop-blur sticky top-0 z-50">
<div class="max-w-7xl mx-auto px-4 py-3 flex flex-col gap-3 lg:flex-row lg:justify-between lg:items-center">
<div class="flex items-center space-x-3">
<div class="p-2 bg-gradient-to-tr from-indigo-500 to-purple-500 rounded-lg shadow-lg shadow-indigo-500/30">
<i class="fa-solid fa-clone text-lg text-white"></i>
</div>
<div>
<h1 class="text-lg font-bold bg-gradient-to-r from-indigo-400 via-purple-400 to-pink-400 bg-clip-text text-transparent">
αž§αž”αž€αžšαžŽαŸαž€αŸ’αž›αžΌαž“αžšαžΌαž”αž—αžΆαž– AI
</h1>
<p class="hidden sm:block text-[11px] text-slate-400">αž‡αŸ†αž“αžΆαž“αŸ‹αžŠαŸ†αž”αžΌαž„αŸ– αžŸαžΆαž˜αž‰αŸ’αž‰ αž›αžΏαž“ αž“αž·αž„αž„αžΆαž™αžŸαŸ’αžšαž½αž›αž”αŸ’αžšαžΎαž”αŸ’αžšαžΆαžŸαŸ‹</p>
</div>
</div>
<div class="w-full lg:w-auto flex flex-col gap-1.5 lg:items-end">
<div class="w-full lg:w-auto flex items-center gap-1.5 bg-slate-950/60 border border-slate-800 rounded-lg px-2 py-1.5">
<i class="fa-solid fa-key text-xs text-indigo-400 shrink-0"></i>
<input id="apiKeyInput" type="password" autocomplete="off" spellcheck="false" placeholder="API Key" class="min-w-0 flex-1 w-0 sm:w-52 lg:w-56 bg-transparent text-xs text-slate-200 placeholder-slate-500 focus:outline-none">
<button id="toggleApiKeyBtn" type="button" class="icon-pop-3d shrink-0 w-7 h-7 flex items-center justify-center text-slate-400 hover:text-slate-200 p-1" title="αž”αž„αŸ’αž αžΆαž‰/αž›αžΆαž€αŸ‹ API Key">
<i id="toggleApiKeyIcon" class="fa-solid fa-eye"></i>
</button>
<button id="refreshBalanceBtn" type="button" class="icon-pop-3d shrink-0 w-7 h-7 flex items-center justify-center text-slate-400 hover:text-slate-200 p-1" title="αž–αž·αž“αž·αžαŸ’αž™ balance">
<i class="fa-solid fa-rotate"></i>
</button>
</div>
<div class="flex flex-wrap items-center gap-1.5 text-[10px]">
<span class="flex h-2 w-2 relative shrink-0">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
<span class="relative inline-flex rounded-full h-2 w-2 bg-emerald-500"></span>
</span>
<span class="leading-none text-slate-300 font-medium bg-slate-800/80 px-2 py-1 rounded-full border border-slate-700">R-Tool-AI</span>
<span id="customerStatus" class="max-w-[142px] sm:max-w-none truncate leading-none text-slate-300 font-medium bg-slate-800/80 px-2 py-1 rounded-full border border-slate-700 whitespace-nowrap">Customer: --</span>
<span id="keyExpiresStatus" class="leading-none text-slate-300 font-medium bg-slate-800/80 px-2 py-1 rounded-full border border-slate-700 whitespace-nowrap">Expiration: --</span>
<span id="creditLimitStatus" class="leading-none text-slate-300 font-medium bg-slate-800/80 px-2 py-1 rounded-full border border-slate-700 whitespace-nowrap">Credit limit: --</span>
</div>
</div>
</div>
</header>
<!-- Main Content -->
<main class="max-w-7xl mx-auto px-4 py-8 w-full flex-grow">
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8">
<!-- Left Panel: Upload and Settings -->
<section class="lg:col-span-5 space-y-6">
<!-- Dropzone for Image -->
<div class="glass rounded-2xl p-6 shadow-xl">
<h2 class="text-lg font-semibold mb-4 flex items-center gap-2 text-indigo-400">
<i class="fa-solid fa-image"></i> ៑. αž”αž‰αŸ’αž…αžΌαž›αžšαžΌαž”αž—αžΆαž–αž…αŸ’αž”αžΆαž”αŸ‹αžŠαžΎαž˜
</h2>
<div id="dropzone" class="border-2 border-dashed border-slate-700 hover:border-indigo-500 rounded-xl p-8 text-center cursor-pointer transition-all bg-slate-950/40 relative overflow-hidden group">
<input type="file" id="imageInput" accept="image/png,image/jpeg,image/webp,image/gif" multiple class="hidden">
<div id="uploadPlaceholder" class="space-y-4">
<div class="mx-auto w-16 h-16 rounded-full bg-slate-800/80 flex items-center justify-center border border-slate-700 group-hover:scale-110 transition-transform">
<i class="fa-solid fa-cloud-arrow-up text-2xl text-indigo-400"></i>
</div>
<div>
<p class="font-medium text-slate-200">αž’αžΌαžŸαžšαžΌαž”αž—αžΆαž–αž˜αž€αž‘αžΈαž“αŸαŸ‡ ឬ αž…αž»αž…αžŠαžΎαž˜αŸ’αž”αžΈαž‡αŸ’αžšαžΎαžŸαžšαžΎαžŸαž˜αŸ’αžαž„αž”αžΆαž“αž…αŸ’αžšαžΎαž“</p>
<p class="text-xs text-slate-500 mt-1">αž‚αžΆαŸ†αž‘αŸ’αžšαž”αŸ’αžšαž—αŸαž‘ PNG, JPG, JPEG, WEBP, GIF (αžšαžΌαž”αž“αžΈαž˜αž½αž™αŸ—αž€αŸ’αžšαŸ„αž˜ 5MB)</p>
</div>
</div>
<div id="imagePreviewContainer" class="hidden relative w-full rounded-lg border border-slate-700 bg-slate-900 p-3">
<div class="flex items-center justify-between gap-3 mb-3">
<span id="selectedImagesCount" class="text-xs text-indigo-300 font-semibold bg-indigo-500/10 border border-indigo-500/20 rounded-full px-3 py-1">0 αžšαžΌαž”αžŠαžΎαž˜</span>
<button id="removeImageBtn" type="button" class="btn-pop-3d bg-red-500/80 hover:bg-red-600 text-white px-3 py-1.5 rounded-lg transition-colors shadow-lg backdrop-blur-sm text-xs flex items-center gap-1.5" title="αž›αž»αž”αž‘αžΆαŸ†αž„αž’αžŸαŸ‹">
<i class="fa-solid fa-trash-can"></i> αž›αž»αž”αž‘αžΆαŸ†αž„αž’αžŸαŸ‹
</button>
</div>
<div id="imagePreviewGrid" class="grid grid-cols-2 sm:grid-cols-3 gap-3 max-h-72 overflow-y-auto pr-1">
<!-- Source image previews will be injected dynamically here -->
</div>
</div>
</div>
</div>
<!-- Clone Configurations -->
<div class="glass rounded-2xl p-6 shadow-xl space-y-5">
<h2 class="text-lg font-semibold flex items-center gap-2 text-indigo-400">
<i class="fa-solid fa-sliders"></i> ្. αž€αžΆαžšαž€αŸ†αžŽαžαŸ‹αž€αžΆαžšαž€αŸ’αž›αžΌαž“ (Clone Settings)
</h2>
<!-- Clone Quantity -->
<div>
<div class="flex justify-between items-center mb-2">
<label class="text-sm font-medium text-slate-300">αž…αŸ†αž“αž½αž“αž€αŸ’αž›αžΌαž“αž€αŸ’αž“αž»αž„αž˜αž½αž™αžšαžΌαž”αžŠαžΎαž˜</label>
<span id="quantityVal" class="text-indigo-400 font-bold bg-indigo-500/10 px-2.5 py-0.5 rounded-md border border-indigo-500/20">1 αžšαžΌαž”αž—αžΆαž–</span>
</div>
<input type="range" id="cloneQuantity" min="1" max="8" value="1" class="w-full h-2 bg-slate-800 rounded-lg appearance-none cursor-pointer accent-indigo-500 focus:outline-none">
<div class="flex justify-between text-[10px] text-slate-500 mt-1 px-1">
<span>1 αžšαžΌαž”αž—αžΆαž–</span>
<span>4 αžšαžΌαž”αž—αžΆαž–</span>
<span>8 αžšαžΌαž”αž—αžΆαž–</span>
</div>
</div>
<!-- Similarity Options -->
<div>
<label class="text-sm font-medium text-slate-300 block mb-2">αž€αž˜αŸ’αžšαž·αžαžŸαŸ’αžšαžŠαŸ€αž„αž“αžΉαž„αž…αŸ’αž”αžΆαž”αŸ‹αžŠαžΎαž˜</label>
<div class="grid grid-cols-3 gap-2">
<button type="button" data-similarity="high" class="similarity-btn choice-pop-3d border border-slate-700 bg-slate-950/40 rounded-xl p-3 text-center transition-all hover:bg-slate-800 active-style border-indigo-500 text-indigo-400">
<i class="fa-solid fa-crosshairs block text-lg mb-1"></i>
<span class="text-xs font-semibold">αžŸαŸ’αžšαžŠαŸ€αž„αžαŸ’αž›αžΆαŸ†αž„</span>
</button>
<button type="button" data-similarity="medium" class="similarity-btn choice-pop-3d border border-slate-700 bg-slate-950/40 rounded-xl p-3 text-center transition-all hover:bg-slate-800">
<i class="fa-solid fa-shuffle block text-lg mb-1"></i>
<span class="text-xs font-semibold">αžŸαŸ’αžšαžŠαŸ€αž„αž›αŸ’αž˜αž˜</span>
</button>
<button type="button" data-similarity="creative" class="similarity-btn choice-pop-3d border border-slate-700 bg-slate-950/40 rounded-xl p-3 text-center transition-all hover:bg-slate-800">
<i class="fa-solid fa-wand-magic-sparkles block text-lg mb-1"></i>
<span class="text-xs font-semibold">αž…αŸ’αž“αŸƒαž”αŸ’αžšαžŒαž·αžαžαŸ’αž˜αžΈ</span>
</button>
</div>
</div>
<!-- AI Model Selector -->
<div>
<label class="text-sm font-medium text-slate-300 block mb-2 flex items-center justify-between">
<span>αž‡αŸ’αžšαžΎαžŸαžšαžΎαžŸ AI Model</span>
<button id="reloadModelsBtn" type="button" class="icon-pop-3d text-[10px] text-indigo-300 hover:text-indigo-200 font-semibold flex items-center gap-1 px-2 py-1 border border-slate-800 bg-slate-950/40">
<i class="fa-solid fa-rotate"></i> Load Models
</button>
</label>
<select id="modelSelect" class="w-full bg-slate-950/60 border border-slate-800 rounded-xl px-4 py-3 text-sm text-slate-200 focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 transition-colors">
<option value="google/gemini-2.5-flash-image">Google Gemini 2.5 Flash Image</option>
</select>
<p id="modelHelpText" class="text-[10px] text-slate-500 mt-2">αž”αž‰αŸ’αž‡αžΈαž“αŸαŸ‡αž”αž„αŸ’αž αžΆαž‰ model αžŠαŸ‚αž›αž‚αžΆαŸ†αž‘αŸ’αžš image input αž“αž·αž„ image outputαŸ”</p>
</div>
<!-- Extra Prompt Modifiers -->
<div>
<label class="text-sm font-medium text-slate-300 block mb-2 flex items-center justify-between">
<span>αž”αž“αŸ’αžαŸ‚αž˜αž€αžΆαžšαžŽαŸ‚αž“αžΆαŸ†αž–αž·αžŸαŸαžŸ (αž‚αŸ†αž“αž·αžαž”αž“αŸ’αžαŸ‚αž˜)</span>
<span class="text-[10px] text-slate-500 font-normal">αžŸαŸ’αžšαŸαž…αž…αž·αžαŸ’αž</span>
</label>
<textarea id="promptModifier" rows="2" placeholder="αž§αž‘αžΆαž αžšαžŽαŸαŸ– αž”αŸ’αžαžΌαžšαžŸαŸ’αž‘αžΈαž›αž‡αžΆαž‚αŸ†αž“αžΌαžšαž‡αžΈαžœαž…αž› (Anime), αž”αž“αŸ’αžαŸ‚αž˜αž–αž“αŸ’αž›αžΊαžαŸ’αž„αŸƒαž›αž·αž…, αž”αŸ’αžαžΌαžšαž•αŸ’αž‘αŸƒαžαžΆαž„αž€αŸ’αžšαŸ„αž™..." class="w-full bg-slate-950/60 border border-slate-800 rounded-xl px-4 py-3 text-sm text-slate-200 placeholder-slate-500 focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 transition-colors resize-none"></textarea>
</div>
<!-- Action Button -->
<button id="generateBtn" disabled class="btn-pop-3d w-full py-4 px-6 rounded-xl font-bold bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 text-white shadow-lg shadow-indigo-500/20 hover:shadow-indigo-500/40 hover:opacity-95 transition-all disabled:opacity-50 disabled:cursor-not-allowed disabled:shadow-none flex items-center justify-center gap-2">
<i class="fa-solid fa-bolt"></i> αž…αžΆαž”αŸ‹αž•αŸ’αžαžΎαž˜αž€αŸ’αž›αžΌαž“αžšαžΌαž”αž—αžΆαž–
</button>
</div>
</section>
<!-- Right Panel: Results & Gallery -->
<section class="lg:col-span-7 flex flex-col space-y-6">
<div class="glass rounded-2xl p-6 shadow-xl flex-grow flex flex-col">
<div class="flex justify-between items-center mb-6">
<h2 class="text-lg font-semibold flex items-center gap-2 text-indigo-400">
<i class="fa-solid fa-images"></i> αž›αž‘αŸ’αž’αž•αž›αž“αŸƒαž€αžΆαžšαž€αŸ’αž›αžΌαž“ (Cloned Gallery)
</h2>
<button id="downloadAllBtn" class="btn-pop-3d hidden text-xs bg-slate-800 hover:bg-slate-700 text-slate-300 border border-slate-700 px-3 py-1.5 rounded-lg transition-colors flex items-center gap-1.5">
<i class="fa-solid fa-download"></i> αž‘αžΆαž‰αž™αž€αžšαžΌαž”αž‘αžΆαŸ†αž„αž’αžŸαŸ‹
</button>
</div>
<!-- Gallery Content Area -->
<div id="galleryContainer" class="flex-grow flex flex-col justify-center items-center text-center p-8 border border-dashed border-slate-800 rounded-xl bg-slate-950/20">
<div id="galleryEmptyState" class="space-y-4 max-w-sm">
<div class="w-20 h-20 mx-auto rounded-full bg-slate-900 flex items-center justify-center border border-slate-800 text-slate-600">
<i class="fa-solid fa-shapes text-3xl"></i>
</div>
<div>
<h3 class="font-semibold text-slate-300">αž˜αž·αž“αž‘αžΆαž“αŸ‹αž˜αžΆαž“αžšαžΌαž”αž—αžΆαž–αž€αŸ’αž›αžΌαž“αž“αŸ…αž‘αžΎαž™αž‘αŸ</h3>
<p class="text-sm text-slate-500 mt-1">αžŸαžΌαž˜αž”αž‰αŸ’αž…αžΌαž›αžšαžΌαž”αž—αžΆαž– αžšαž½αž…αž…αž»αž… "αž…αžΆαž”αŸ‹αž•αŸ’αžαžΎαž˜αž€αŸ’αž›αžΌαž“αžšαžΌαž”αž—αžΆαž–" αžŠαžΎαž˜αŸ’αž”αžΈαž”αž„αŸ’αž αžΆαž‰αž›αž‘αŸ’αž’αž•αž›!</p>
</div>
</div>
<!-- Grid View for cloned results -->
<div id="clonesGrid" class="hidden w-full grid grid-cols-1 sm:grid-cols-2 gap-4">
<!-- Cloned cards will be injected dynamically here -->
</div>
</div>
</div>
</section>
</div>
</main>
<!-- Unified Popup for iPhone & Desktop Preview -->
<div id="previewModal" class="fixed inset-0 bg-slate-950/95 z-50 hidden flex-col items-center justify-center p-4 backdrop-blur-md">
<div class="relative max-w-md w-full flex flex-col items-center">
<!-- Close Button -->
<button id="closeModalBtn" class="icon-pop-3d absolute -top-12 right-0 bg-slate-800 hover:bg-slate-700 text-white p-2.5 rounded-full transition-all border border-slate-700">
<i class="fa-solid fa-xmark text-lg"></i>
</button>
<!-- iOS Instructions Alert Box -->
<div id="iosInstruction" class="w-full bg-indigo-600/20 border border-indigo-500/40 rounded-xl p-4 mb-4 text-center">
<p class="text-sm font-semibold text-indigo-300 flex items-center justify-center gap-2">
<i class="fa-solid fa-hand-pointer animate-bounce"></i> αžŸαž˜αŸ’αžšαžΆαž”αŸ‹αž‘αžΌαžšαžŸαž–αŸ’αž‘ iPhone
</p>
<p class="text-xs text-slate-300 mt-1">αžŸαžΌαž˜αž…αž»αž…αžŸαž„αŸ’αž€αžαŸ‹ (Long Press) αž›αžΎαžšαžΌαž”αž—αžΆαž–αžαžΆαž„αž€αŸ’αžšαŸ„αž˜ αžšαž½αž…αž‡αŸ’αžšαžΎαžŸαžšαžΎαžŸ <strong>"αžšαž€αŸ’αžŸαžΆαž‘αž»αž€αž€αŸ’αž“αž»αž„αžšαžΌαž”αž—αžΆαž–" (Save to Photos)</strong> αžŠαžΎαž˜αŸ’αž”αžΈαž‘αžΆαž‰αž™αž€αŸ”</p>
</div>
<!-- Image Container -->
<div class="w-full bg-slate-900 rounded-xl overflow-hidden border border-slate-800 p-2 shadow-2xl">
<img id="modalTargetImg" src="" alt="Preview" class="w-full max-h-[60vh] object-contain rounded-lg">
</div>
<!-- Standard Download Button (Hidden on iOS) -->
<div id="standardDownloadBtnContainer" class="mt-4 w-full">
<a id="modalDownloadBtn" href="#" download="cloned_image.png" class="btn-pop-3d w-full justify-center bg-indigo-600 hover:bg-indigo-700 text-white px-6 py-3 rounded-xl font-bold transition-colors flex items-center gap-2">
<i class="fa-solid fa-download"></i> αž‘αžΆαž‰αž™αž€αžšαžΌαž”αž“αŸαŸ‡
</a>
</div>
</div>
</div>
<!-- Admin Contact Notice Modal -->
<div id="adminNoticeModal" class="fixed inset-0 bg-slate-950/80 z-50 hidden items-center justify-center p-4 backdrop-blur-sm">
<div class="max-w-md w-full bg-slate-900 border border-red-500/30 rounded-2xl p-6 shadow-2xl">
<div class="flex items-start gap-3">
<div class="w-10 h-10 rounded-full bg-red-500/15 border border-red-500/30 flex items-center justify-center text-red-300 shrink-0">
<i class="fa-solid fa-triangle-exclamation"></i>
</div>
<div class="min-w-0">
<h3 id="adminNoticeTitle" class="text-lg font-bold text-white">αžŸαžΌαž˜αž‘αžΆαž€αŸ‹αž‘αž„ Admin</h3>
<p id="adminNoticeMessage" class="text-sm text-slate-300 mt-1 leading-relaxed">αžŸαžΌαž˜αž‘αžΆαž€αŸ‹αž‘αž„ Admin αžŠαžΎαž˜αŸ’αž”αžΈαž‘αž·αž‰ αž¬αž”αž“αŸ’αž API KeyαŸ”</p>
</div>
</div>
<div class="mt-5 grid gap-2 text-sm">
<a href="mailto:chanraphone4@gmail.com" class="flex items-center gap-2 text-indigo-200 hover:text-indigo-100 bg-slate-950/60 border border-slate-800 rounded-lg px-3 py-2">
<i class="fa-solid fa-envelope text-indigo-300"></i>
<span>chanraphone4@gmail.com</span>
</a>
<a href="tel:077857878" class="flex items-center gap-2 text-indigo-200 hover:text-indigo-100 bg-slate-950/60 border border-slate-800 rounded-lg px-3 py-2">
<i class="fa-solid fa-phone text-indigo-300"></i>
<span>077857878</span>
</a>
<a href="tel:086680000" class="flex items-center gap-2 text-indigo-200 hover:text-indigo-100 bg-slate-950/60 border border-slate-800 rounded-lg px-3 py-2">
<i class="fa-solid fa-phone text-indigo-300"></i>
<span>086680000</span>
</a>
</div>
<button id="adminNoticeCloseBtn" type="button" class="btn-pop-3d mt-5 w-full bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2.5 rounded-xl font-semibold transition-colors">
OK
</button>
</div>
</div>
<!-- Error/Notification Toast -->
<div id="toast" class="fixed bottom-6 right-6 z-50 transform translate-y-20 opacity-0 transition-all duration-300 max-w-md w-full">
<div class="bg-red-500/90 text-white p-4 rounded-xl shadow-2xl backdrop-blur flex items-center gap-3 border border-red-400/20">
<i id="toastIcon" class="fa-solid fa-circle-exclamation text-xl"></i>
<span id="toastMessage" class="text-sm font-medium">αž˜αžΆαž“αž€αŸ†αž αž»αžŸαž˜αž½αž™αž”αžΆαž“αž€αžΎαžαž‘αžΎαž„!</span>
</div>
</div>
<!-- Footer -->
<footer class="border-t border-slate-800/80 bg-slate-950/60 py-4 mt-12 text-center text-xs text-slate-500">
<div class="max-w-7xl mx-auto px-4">
<p>Β© 2026 R-Tool-AI αŸ” αž’αŸ’αž“αž€αž•αž›αž·αžαž’αžΌαž›αŸ– 077857878 / 086680000</p>
</div>
</footer>
<!-- Scripts -->
<script>
// OpenRouter API configuration
const openRouterEndpoint = "https://openrouter.ai/api/v1/chat/completions";
const openRouterKeyEndpoint = "https://openrouter.ai/api/v1/key";
const openRouterModelsEndpoint = "https://openrouter.ai/api/v1/models";
const defaultOpenRouterModel = "google/gemini-2.5-flash-image";
const workspaceCreditLimit = 1;
const workspaceCreditLimitResetInterval = "weekly";
const openRouterAppTitle = "AI Image Cloner";
const adminContactEmail = "chanraphone4@gmail.com";
const adminContactPhones = "077857878 / 086680000";
const apiKeyStorageKey = "openrouter_api_key";
const selectedModelStorageKey = "openrouter_selected_model";
const requestCountStorageKey = "openrouter_request_count";
const supportedImageTypes = ["image/png", "image/jpeg", "image/webp", "image/gif"];
let uploadedImages = [];
let selectedSimilarity = "high";
let generatedImagesArray = [];
let balanceRefreshTimer;
let latestKeyData = null;
const savedOpenRouterModel = localStorage.getItem(selectedModelStorageKey);
let selectedOpenRouterModel = savedOpenRouterModel && !["undefined", "null", ""].includes(savedOpenRouterModel)
? savedOpenRouterModel
: defaultOpenRouterModel;
let requestCount = parseInt(localStorage.getItem(requestCountStorageKey) || "0", 10);
// Detect device
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
// DOM elements
const imageInput = document.getElementById('imageInput');
const dropzone = document.getElementById('dropzone');
const uploadPlaceholder = document.getElementById('uploadPlaceholder');
const imagePreviewContainer = document.getElementById('imagePreviewContainer');
const imagePreviewGrid = document.getElementById('imagePreviewGrid');
const selectedImagesCount = document.getElementById('selectedImagesCount');
const removeImageBtn = document.getElementById('removeImageBtn');
const cloneQuantity = document.getElementById('cloneQuantity');
const quantityVal = document.getElementById('quantityVal');
const apiKeyInput = document.getElementById('apiKeyInput');
const toggleApiKeyBtn = document.getElementById('toggleApiKeyBtn');
const toggleApiKeyIcon = document.getElementById('toggleApiKeyIcon');
const refreshBalanceBtn = document.getElementById('refreshBalanceBtn');
const customerStatus = document.getElementById('customerStatus');
const creditLimitStatus = document.getElementById('creditLimitStatus');
const keyExpiresStatus = document.getElementById('keyExpiresStatus');
const modelSelect = document.getElementById('modelSelect');
const reloadModelsBtn = document.getElementById('reloadModelsBtn');
const modelHelpText = document.getElementById('modelHelpText');
const promptModifier = document.getElementById('promptModifier');
const generateBtn = document.getElementById('generateBtn');
const galleryEmptyState = document.getElementById('galleryEmptyState');
const clonesGrid = document.getElementById('clonesGrid');
const galleryContainer = document.getElementById('galleryContainer');
const downloadAllBtn = document.getElementById('downloadAllBtn');
const similarityBtns = document.querySelectorAll('.similarity-btn');
const toast = document.getElementById('toast');
const toastMessage = document.getElementById('toastMessage');
const toastIcon = document.getElementById('toastIcon');
const adminNoticeModal = document.getElementById('adminNoticeModal');
const adminNoticeTitle = document.getElementById('adminNoticeTitle');
const adminNoticeMessage = document.getElementById('adminNoticeMessage');
const adminNoticeCloseBtn = document.getElementById('adminNoticeCloseBtn');
// Modal elements
const previewModal = document.getElementById('previewModal');
const closeModalBtn = document.getElementById('closeModalBtn');
const iosInstruction = document.getElementById('iosInstruction');
const modalTargetImg = document.getElementById('modalTargetImg');
const standardDownloadBtnContainer = document.getElementById('standardDownloadBtnContainer');
const modalDownloadBtn = document.getElementById('modalDownloadBtn');
apiKeyInput.value = localStorage.getItem(apiKeyStorageKey) || "";
modelSelect.value = selectedOpenRouterModel;
updateRequestCountStatus();
function getOpenRouterApiKey() {
return apiKeyInput.value.trim();
}
function getSelectedOpenRouterModel() {
const model = modelSelect.value || selectedOpenRouterModel || defaultOpenRouterModel;
return ["undefined", "null", ""].includes(model) ? defaultOpenRouterModel : model;
}
function updateRequestCountStatus() {
}
function getCustomerName(keyData) {
return keyData?.name || keyData?.label || "";
}
function updateCustomerStatus(keyData) {
const hasApiKey = Boolean(getOpenRouterApiKey());
const customerName = getCustomerName(keyData);
customerStatus.textContent = customerName
? `Customer: ${customerName}`
: hasApiKey ? "Customer: checking" : "Customer: --";
if (customerName) {
customerStatus.title = customerName;
} else {
customerStatus.removeAttribute("title");
}
}
function formatLimitResetLabel(value) {
if (!value) return "";
const labels = {
daily: "day",
weekly: "week",
monthly: "month"
};
return ` / ${labels[value] || String(value).replaceAll("_", " ")}`;
}
function updateCreditLimitStatus(value, resetInterval = "") {
if (value === undefined) {
creditLimitStatus.textContent = "Credit limit: --";
creditLimitStatus.removeAttribute("title");
return false;
}
if (value === null || value === "") {
creditLimitStatus.textContent = "Credit limit: Unlimited";
creditLimitStatus.removeAttribute("title");
return false;
}
const limit = getFiniteNumber(value);
if (limit === null) {
creditLimitStatus.textContent = `Credit limit: ${String(value)}`;
creditLimitStatus.title = String(value);
return true;
}
const resetLabel = formatLimitResetLabel(resetInterval);
creditLimitStatus.textContent = `Credit limit: $${formatCreditAmount(limit)}${resetLabel}`;
creditLimitStatus.title = resetInterval ? `Resets: ${String(resetInterval)}` : "Credit limit";
return true;
}
function updateWorkspaceCreditLimitStatus() {
const hasWorkspaceLimit = updateCreditLimitStatus(workspaceCreditLimit, workspaceCreditLimitResetInterval);
creditLimitStatus.title = "Workspace guardrail budget configured in OpenRouter.";
return hasWorkspaceLimit;
}
function getCreditRemaining(keyData) {
const directRemaining = getFiniteNumber(keyData?.limit_remaining);
if (directRemaining !== null) return directRemaining;
const keyLimit = getFiniteNumber(keyData?.limit);
const keyUsage = getFiniteNumber(keyData?.usage);
if (keyLimit !== null && keyUsage !== null) {
return Math.max(keyLimit - keyUsage, 0);
}
return null;
}
function isKeyExpired(value) {
if (value === null || value === undefined || value === "") return false;
const expiresDate = new Date(value);
return Number.isFinite(expiresDate.getTime()) && expiresDate.getTime() <= Date.now();
}
function showAdminContactNotice(reason) {
const messages = {
expired: "API Key αžšαž”αžŸαŸ‹αž’αŸ’αž“αž€αž”αžΆαž“αž•αž»αžαž€αŸ†αžŽαžαŸ‹αŸ” αžŸαžΌαž˜αž‘αžΆαž€αŸ‹αž‘αž„ Admin αžŠαžΎαž˜αŸ’αž”αžΈαž‘αž·αž‰ αž¬αž”αž“αŸ’αž keyαŸ”",
credit: "Credit αžšαž”αžŸαŸ‹ API Key αž’αžŸαŸ‹αž αžΎαž™αŸ” αžŸαžΌαž˜αž‘αžΆαž€αŸ‹αž‘αž„ Admin αžŠαžΎαž˜αŸ’αž”αžΈαž‘αž·αž‰ credit αž¬αž”αž“αŸ’αž keyαŸ”"
};
adminNoticeTitle.textContent = reason === "expired" ? "API Key αž•αž»αžαž€αŸ†αžŽαžαŸ‹" : "Credit αž’αžŸαŸ‹";
adminNoticeMessage.textContent = `${messages[reason]} Email: ${adminContactEmail}. Phone: ${adminContactPhones}.`;
adminNoticeModal.classList.remove('hidden');
adminNoticeModal.classList.add('flex');
}
function hideAdminContactNotice() {
adminNoticeModal.classList.add('hidden');
adminNoticeModal.classList.remove('flex');
}
function checkKeyAccessNotice(keyData) {
if (isKeyExpired(keyData?.expires_at)) {
showAdminContactNotice("expired");
return false;
}
const creditRemaining = getCreditRemaining(keyData);
if (creditRemaining !== null && creditRemaining <= 0) {
showAdminContactNotice("credit");
return false;
}
hideAdminContactNotice();
return true;
}
function isCreditError(error) {
const message = String(error?.message || "").toLowerCase();
return error?.status === 402
|| message.includes("credit")
|| message.includes("quota")
|| message.includes("insufficient")
|| message.includes("limit exceeded");
}
function formatTimeRemaining(targetDate) {
const remainingMs = targetDate.getTime() - Date.now();
if (remainingMs <= 0) return "Expired";
const totalMinutes = Math.ceil(remainingMs / 60000);
const days = Math.floor(totalMinutes / 1440);
const hours = Math.floor((totalMinutes % 1440) / 60);
const minutes = totalMinutes % 60;
if (days >= 1) {
return hours > 0 ? `${days}d ${hours}h left` : `${days}d left`;
}
if (hours >= 1) {
return minutes > 0 ? `${hours}h ${minutes}m left` : `${hours}h left`;
}
return `${minutes}m left`;
}
function updateKeyExpiresStatus(value) {
if (value === undefined) {
keyExpiresStatus.textContent = "Expiration: --";
keyExpiresStatus.removeAttribute("title");
return;
}
if (value === null || value === "") {
keyExpiresStatus.textContent = "Expiration: Never";
keyExpiresStatus.removeAttribute("title");
return;
}
const expiresDate = new Date(value);
if (Number.isNaN(expiresDate.getTime())) {
keyExpiresStatus.textContent = `Expiration: ${String(value)}`;
keyExpiresStatus.title = String(value);
return;
}
keyExpiresStatus.textContent = `Expiration: ${formatTimeRemaining(expiresDate)}`;
keyExpiresStatus.title = expiresDate.toLocaleString();
}
function incrementRequestCount() {
requestCount++;
localStorage.setItem(requestCountStorageKey, String(requestCount));
updateRequestCountStatus();
}
function setModelHelp(message, type = "neutral") {
modelHelpText.textContent = message;
modelHelpText.classList.remove('text-slate-500', 'text-emerald-300', 'text-red-300');
if (type === "success") {
modelHelpText.classList.add('text-emerald-300');
} else if (type === "error") {
modelHelpText.classList.add('text-red-300');
} else {
modelHelpText.classList.add('text-slate-500');
}
}
function applyModelOptions(models) {
let currentModel = selectedOpenRouterModel || defaultOpenRouterModel;
const modelOptions = models.length ? models : [{
id: defaultOpenRouterModel,
name: "Google Gemini 2.5 Flash Image"
}];
let hasCurrentModel = modelOptions.some(model => model.id === currentModel);
if (models.length && !hasCurrentModel) {
currentModel = defaultOpenRouterModel;
selectedOpenRouterModel = defaultOpenRouterModel;
localStorage.setItem(selectedModelStorageKey, defaultOpenRouterModel);
hasCurrentModel = modelOptions.some(model => model.id === currentModel);
}
modelSelect.innerHTML = modelOptions.map(model => `
<option value="${escapeHtml(model.id)}">${escapeHtml(model.name || model.id)}</option>
`).join('');
if (!hasCurrentModel) {
modelSelect.insertAdjacentHTML('afterbegin', `<option value="${escapeHtml(currentModel)}">${escapeHtml(currentModel)}</option>`);
}
modelSelect.value = currentModel;
}
function isImageToImageModel(model) {
const inputModalities = model?.architecture?.input_modalities || [];
const outputModalities = model?.architecture?.output_modalities || [];
return inputModalities.includes("image") && outputModalities.includes("image");
}
async function loadImageModels(showToastOnSuccess = false) {
setModelHelp("αž€αŸ†αž–αž»αž„ load image models...");
try {
const data = await fetchOpenRouterJson(openRouterModelsEndpoint, getOpenRouterApiKey());
const models = (data?.data || [])
.filter(isImageToImageModel)
.sort((first, second) => (first.name || first.id).localeCompare(second.name || second.id));
applyModelOptions(models);
setModelHelp(`αž”αžΆαž“ load image-capable models αž…αŸ†αž“αž½αž“ ${models.length}αŸ”`, "success");
if (showToastOnSuccess) showToast("αž”αžΆαž“ load AI Models αžšαž½αž…αžšαžΆαž›αŸ‹!", "success");
} catch (error) {
console.error(error);
applyModelOptions([]);
setModelHelp("αž˜αž·αž“αž’αžΆαž… load models αž”αžΆαž“αŸ” αž”αŸ’αžšαžΎ default model αžŸαž·αž“αŸ”", "error");
}
}
apiKeyInput.addEventListener('input', () => {
localStorage.setItem(apiKeyStorageKey, getOpenRouterApiKey());
clearTimeout(balanceRefreshTimer);
const openRouterApiKey = getOpenRouterApiKey();
latestKeyData = null;
customerStatus.textContent = openRouterApiKey ? "Customer: checking" : "Customer: --";
if (!openRouterApiKey) hideAdminContactNotice();
updateCreditLimitStatus(undefined);
updateKeyExpiresStatus(undefined);
if (openRouterApiKey.length > 20) {
balanceRefreshTimer = setTimeout(() => refreshOpenRouterBalance(false), 900);
}
});
apiKeyInput.addEventListener('blur', () => {
if (getOpenRouterApiKey()) refreshOpenRouterBalance(false);
});
apiKeyInput.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
apiKeyInput.blur();
}
});
toggleApiKeyBtn.addEventListener('click', () => {
const shouldShow = apiKeyInput.type === 'password';
apiKeyInput.type = shouldShow ? 'text' : 'password';
toggleApiKeyIcon.className = shouldShow ? 'fa-solid fa-eye-slash' : 'fa-solid fa-eye';
});
refreshBalanceBtn.addEventListener('click', () => {
refreshOpenRouterBalance(true);
});
modelSelect.addEventListener('change', () => {
selectedOpenRouterModel = getSelectedOpenRouterModel();
localStorage.setItem(selectedModelStorageKey, selectedOpenRouterModel);
setModelHelp(`αž€αŸ†αž–αž»αž„αž”αŸ’αžšαžΎ model: ${selectedOpenRouterModel}`);
});
reloadModelsBtn.addEventListener('click', () => {
loadImageModels(true);
});
function formatCreditAmount(value) {
return Number(value).toLocaleString(undefined, {
minimumFractionDigits: 2,
maximumFractionDigits: 2
});
}
function getFiniteNumber(value) {
if (value === null || value === undefined || value === "") return null;
const numberValue = Number(value);
return Number.isFinite(numberValue) ? numberValue : null;
}
async function fetchOpenRouterJson(url, openRouterApiKey) {
const headers = {
'HTTP-Referer': window.location.origin === 'null' ? 'http://localhost' : window.location.origin,
'X-OpenRouter-Title': openRouterAppTitle
};
if (openRouterApiKey) {
headers.Authorization = `Bearer ${openRouterApiKey}`;
}
const response = await fetch(url, {
method: 'GET',
headers
});
const responseText = await response.text();
const data = responseText ? JSON.parse(responseText) : {};
if (!response.ok) {
const message = data?.error?.message || data?.message || responseText || `HTTP error! status: ${response.status}`;
throw new Error(message);
}
return data;
}
async function refreshOpenRouterBalance(showToastOnSuccess = false) {
const openRouterApiKey = getOpenRouterApiKey();
if (!openRouterApiKey) {
latestKeyData = null;
hideAdminContactNotice();
updateCustomerStatus(null);
updateCreditLimitStatus(undefined);
updateKeyExpiresStatus(undefined);
return;
}
customerStatus.textContent = "Customer: checking";
creditLimitStatus.textContent = "Credit limit: checking";
creditLimitStatus.removeAttribute("title");
keyExpiresStatus.textContent = "Expiration: checking";
try {
const keyInfo = await fetchOpenRouterJson(openRouterKeyEndpoint, openRouterApiKey);
const keyData = keyInfo?.data || {};
latestKeyData = keyData;
updateCustomerStatus(keyData);
const hasKeyCreditLimit = updateCreditLimitStatus(keyData.limit, keyData.limit_reset);
if (!hasKeyCreditLimit) {
updateWorkspaceCreditLimitStatus();
}
updateKeyExpiresStatus(keyData.expires_at);
checkKeyAccessNotice(keyData);
if (showToastOnSuccess) showToast("αž”αžΆαž“αž–αž·αž“αž·αžαŸ’αž™ customer info αžšαž½αž…αžšαžΆαž›αŸ‹!", "success");
} catch (error) {
console.error(error);
latestKeyData = null;
customerStatus.textContent = "Customer: key error";
updateCreditLimitStatus(undefined);
updateKeyExpiresStatus(undefined);
showToast("αž˜αž·αž“αž’αžΆαž…αž–αž·αž“αž·αžαŸ’αž™ OpenRouter balance αž”αžΆαž“αž‘αŸαŸ” αžŸαžΌαž˜αž–αž·αž“αž·αžαŸ’αž™ API KeyαŸ”", "error");
}
}
if (getOpenRouterApiKey()) {
refreshOpenRouterBalance(false);
}
// Manage similarity configurations
similarityBtns.forEach(btn => {
btn.addEventListener('click', () => {
similarityBtns.forEach(b => {
b.classList.remove('active-style', 'border-indigo-500', 'text-indigo-400');
b.classList.add('border-slate-700');
});
btn.classList.remove('border-slate-700');
btn.classList.add('active-style', 'border-indigo-500', 'text-indigo-400');
selectedSimilarity = btn.dataset.similarity;
});
});
// Clone counter update
cloneQuantity.addEventListener('input', (e) => {
quantityVal.textContent = `${e.target.value} αžšαžΌαž”αž—αžΆαž–`;
});
// Drag and drop setup
dropzone.addEventListener('click', (e) => {
if (e.target.closest('button')) return;
imageInput.click();
});
dropzone.addEventListener('dragover', (e) => {
e.preventDefault();
dropzone.classList.add('border-indigo-500', 'bg-indigo-500/5');
});
dropzone.addEventListener('dragleave', () => {
dropzone.classList.remove('border-indigo-500', 'bg-indigo-500/5');
});
dropzone.addEventListener('drop', (e) => {
e.preventDefault();
dropzone.classList.remove('border-indigo-500', 'bg-indigo-500/5');
if (e.dataTransfer.files.length) {
handleImageFiles(Array.from(e.dataTransfer.files));
}
});
imageInput.addEventListener('change', (e) => {
if (e.target.files.length) {
handleImageFiles(Array.from(e.target.files));
}
});
removeImageBtn.addEventListener('click', (e) => {
e.stopPropagation();
resetUpload();
});
async function handleImageFiles(files) {
const validFiles = [];
files.forEach(file => {
if (!supportedImageTypes.includes(file.type)) {
showToast("αž˜αžΆαž“αž―αž€αžŸαžΆαžšαžαŸ’αž›αŸ‡αž˜αž·αž“αž˜αŸ‚αž“ PNG, JPG, WEBP ឬ GIFαŸ” αž”αžΆαž“αžšαŸ†αž›αž„αž―αž€αžŸαžΆαžšαž‘αžΆαŸ†αž„αž“αŸ„αŸ‡αŸ”", "error");
return;
}
if (file.size > 5 * 1024 * 1024) {
showToast("αž˜αžΆαž“αžšαžΌαž”αž—αžΆαž–αžαŸ’αž›αŸ‡αž’αŸ†αž–αŸαž€αŸ” αžŸαžΌαž˜αž”αŸ’αžšαžΎαžšαžΌαž”αž“αžΈαž˜αž½αž™αŸ—αž€αŸ’αžšαŸ„αž˜ 5MBαŸ”", "error");
return;
}
validFiles.push(file);
});
if (!validFiles.length) return;
try {
const images = await Promise.all(validFiles.map(readImageFile));
uploadedImages = uploadedImages.concat(images);
imageInput.value = "";
renderUploadedImages();
showToast(`αž”αžΆαž“αž”αž‰αŸ’αž…αžΌαž›αžšαžΌαž”αžŠαžΎαž˜ ${images.length} αžšαžΌαž”αŸ” αžŸαžšαž»αž” ${uploadedImages.length} αžšαžΌαž”αŸ”`, "success");
} catch (error) {
console.error(error);
showToast("αž€αžΆαžšαž’αžΆαž“αž―αž€αžŸαžΆαžšαžšαžΌαž”αž—αžΆαž–αž˜αžΆαž“αž”αž‰αŸ’αž αžΆ!", "error");
}
}
function readImageFile(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => {
const dataUrl = reader.result;
resolve({
id: `${Date.now()}-${Math.random().toString(16).slice(2)}`,
name: file.name,
type: file.type,
dataUrl,
base64: dataUrl.split(',')[1]
});
};
reader.onerror = reject;
});
}
function escapeHtml(value) {
return value.replace(/[&<>"']/g, char => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
}[char]));
}
applyModelOptions([]);
loadImageModels(false);
function renderUploadedImages() {
if (!uploadedImages.length) {
imagePreviewGrid.innerHTML = "";
selectedImagesCount.textContent = "0 αžšαžΌαž”αžŠαžΎαž˜";
uploadPlaceholder.classList.remove('hidden');
imagePreviewContainer.classList.add('hidden');
generateBtn.setAttribute('disabled', 'true');
return;
}
selectedImagesCount.textContent = `${uploadedImages.length} αžšαžΌαž”αžŠαžΎαž˜`;
imagePreviewGrid.innerHTML = uploadedImages.map((image, index) => `
<div class="relative rounded-lg overflow-hidden border border-slate-700 bg-slate-950/60 h-28 group/source">
<img src="${image.dataUrl}" alt="${escapeHtml(image.name)}" class="w-full h-full object-cover">
<button type="button" onclick="event.stopPropagation(); removeUploadedImage('${image.id}')" class="icon-pop-3d absolute top-2 right-2 bg-red-500/85 hover:bg-red-600 text-white w-7 h-7 rounded-lg transition-colors flex items-center justify-center opacity-100 sm:opacity-0 sm:group-hover/source:opacity-100" title="αž›αž»αž”αžšαžΌαž”αž“αŸαŸ‡">
<i class="fa-solid fa-xmark text-xs"></i>
</button>
<div class="absolute left-2 bottom-2 max-w-[calc(100%-1rem)] text-[10px] text-white bg-slate-950/70 px-2 py-1 rounded-md truncate">#${index + 1} ${escapeHtml(image.name)}</div>
</div>
`).join('');
uploadPlaceholder.classList.add('hidden');
imagePreviewContainer.classList.remove('hidden');
generateBtn.removeAttribute('disabled');
}
window.removeUploadedImage = function(imageId) {
uploadedImages = uploadedImages.filter(image => image.id !== imageId);
renderUploadedImages();
};
function resetUpload() {
uploadedImages = [];
imageInput.value = "";
renderUploadedImages();
}
// Beautiful toast handler
function showToast(message, type = "error") {
toastMessage.textContent = message;
if (type === "success") {
toast.firstElementChild.className = "bg-emerald-500/90 text-white p-4 rounded-xl shadow-2xl backdrop-blur flex items-center gap-3 border border-emerald-400/20";
toastIcon.className = "fa-solid fa-circle-check text-xl";
} else {
toast.firstElementChild.className = "bg-red-500/90 text-white p-4 rounded-xl shadow-2xl backdrop-blur flex items-center gap-3 border border-red-400/20";
toastIcon.className = "fa-solid fa-circle-exclamation text-xl";
}
toast.classList.remove('translate-y-20', 'opacity-0');
setTimeout(() => {
toast.classList.add('translate-y-20', 'opacity-0');
}, 4000);
}
// Exponential backoff mechanism
async function fetchWithRetry(url, options, retries = 3, delay = 1200) {
try {
incrementRequestCount();
const response = await fetch(url, options);
const responseText = await response.text();
let data = {};
try {
data = responseText ? JSON.parse(responseText) : {};
} catch (parseError) {
if (response.ok) throw parseError;
}
if (!response.ok) {
const message = data?.error?.message || data?.message || responseText || `HTTP error! status: ${response.status}`;
const error = new Error(`OpenRouter API error (${response.status}): ${message}`);
error.status = response.status;
throw error;
}
return data;
} catch (error) {
const canRetry = !error.status || error.status === 429 || error.status >= 500;
if (retries > 0 && canRetry) {
await new Promise(resolve => setTimeout(resolve, delay));
return fetchWithRetry(url, options, retries - 1, delay * 2);
}
throw error;
}
}
// Open optimized preview modal
window.openPreviewModal = function(imageSrc, index = 1) {
modalTargetImg.src = imageSrc;
modalDownloadBtn.href = imageSrc;
modalDownloadBtn.download = `cloned_image_${index}.png`;
if (isIOS) {
iosInstruction.classList.remove('hidden');
standardDownloadBtnContainer.classList.add('hidden');
} else {
iosInstruction.classList.add('hidden');
standardDownloadBtnContainer.classList.remove('hidden');
}
previewModal.classList.remove('hidden');
previewModal.classList.add('flex');
};
closeModalBtn.addEventListener('click', () => {
previewModal.classList.add('hidden');
previewModal.classList.remove('flex');
});
adminNoticeCloseBtn.addEventListener('click', hideAdminContactNotice);
// Smart downloader
window.downloadImage = function(src, index) {
if (isIOS) {
openPreviewModal(src, index);
} else {
const link = document.createElement('a');
link.href = src;
link.download = `cloned_image_${index}.png`;
document.body.appendChild(link);
link.click();
link.remove();
}
};
// Main Image Clone Process
generateBtn.addEventListener('click', async () => {
if (!uploadedImages.length) return;
const openRouterApiKey = getOpenRouterApiKey();
if (!openRouterApiKey) {
showToast("αžŸαžΌαž˜αž”αž‰αŸ’αž…αžΌαž› OpenRouter API Key αž˜αž»αž“αž–αŸαž›αž€αŸ’αž›αžΌαž“αžšαžΌαž”αž—αžΆαž–!", "error");
return;
}
try {
const keyInfo = await fetchOpenRouterJson(openRouterKeyEndpoint, openRouterApiKey);
const keyData = keyInfo?.data || {};
latestKeyData = keyData;
updateCustomerStatus(keyData);
const hasKeyCreditLimit = updateCreditLimitStatus(keyData.limit, keyData.limit_reset);
if (!hasKeyCreditLimit) {
updateWorkspaceCreditLimitStatus();
}
updateKeyExpiresStatus(keyData.expires_at);
if (!checkKeyAccessNotice(keyData)) return;
} catch (error) {
console.error(error);
showToast("αž˜αž·αž“αž’αžΆαž…αž–αž·αž“αž·αžαŸ’αž™αžŸαŸ’αžαžΆαž“αž—αžΆαž– API Key αž”αžΆαž“αž‘αŸαŸ” αžŸαžΌαž˜αž–αž·αž“αž·αžαŸ’αž™ Key αž˜αŸ’αžαž„αž‘αŸ€αžαŸ”", "error");
return;
}
const countPerSource = parseInt(cloneQuantity.value);
const totalCloneCount = uploadedImages.length * countPerSource;
const extraPrompt = promptModifier.value.trim();
// Set up gallery layout
galleryEmptyState.classList.add('hidden');
clonesGrid.classList.remove('hidden');
clonesGrid.innerHTML = '';
downloadAllBtn.classList.add('hidden');
galleryContainer.className = "flex-grow p-4 border border-dashed border-slate-800 rounded-xl bg-slate-950/20";
generatedImagesArray = [];
// Add skeletons
for (let i = 1; i <= totalCloneCount; i++) {
clonesGrid.innerHTML += `
<div id="clone-card-${i}" class="bg-slate-900 border border-slate-800 rounded-xl overflow-hidden shadow-lg p-4 flex flex-col justify-between min-h-[320px]">
<div class="relative flex-grow flex items-center justify-center bg-slate-950/40 rounded-lg overflow-hidden h-52">
<div class="text-center space-y-3">
<i class="fa-solid fa-spinner animate-spin text-3xl text-indigo-400"></i>
<p class="text-xs text-slate-400 tracking-wide">αž€αŸ†αž–αž»αž„αž€αŸ’αž›αžΌαž“αžšαžΌαž”αž—αžΆαž–αž‘αžΈ ${i}...</p>
</div>
</div>
<div class="mt-4 flex justify-between items-center">
<span class="text-xs text-indigo-400 bg-indigo-500/10 px-2.5 py-1 rounded-full font-semibold border border-indigo-500/10">αžšαžΌαž”αž—αžΆαž–αž€αŸ’αž›αžΌαž“ #${i}</span>
<button disabled class="btn-pop-3d opacity-50 text-slate-400 bg-slate-800 px-3 py-1.5 rounded-lg text-xs flex items-center gap-1.5 cursor-not-allowed">
<i class="fa-solid fa-spinner animate-spin"></i> αžšαž„αŸ‹αž…αžΆαŸ†
</button>
</div>
</div>
`;
}
generateBtn.setAttribute('disabled', 'true');
generateBtn.innerHTML = `<i class="fa-solid fa-spinner animate-spin"></i> αž€αŸ†αž–αž»αž„αžŠαŸ†αžŽαžΎαžšαž€αžΆαžšαž€αŸ’αž›αžΌαž“...`;
try {
let basePrompt = "Generate a visually similar, high-quality variation of this image. Preserve the central subject, composition, color palette, and general details to make it look like an authentic duplicate clone.";
if (selectedSimilarity === "medium") {
basePrompt = "Create a modern, creative alternative clone of this image. Keep the main core subject but alter small elements, lighting, or the surrounding background slightly for a unique variation.";
} else if (selectedSimilarity === "creative") {
basePrompt = "Use this image as a structural inspiration to create a brand new, artistically distinct clone. Keep only the basic idea or composition while applying a fresh visual style, dynamic range, and imaginative background changes.";
}
if (extraPrompt) {
basePrompt += ` Additionally, incorporate the following creative direction requested by the user: ${extraPrompt}.`;
}
// Execute clones sequentially for mobile stability
let cloneIndex = 1;
for (let sourceIndex = 0; sourceIndex < uploadedImages.length; sourceIndex++) {
const sourceImage = uploadedImages[sourceIndex];
for (let sourceCloneIndex = 1; sourceCloneIndex <= countPerSource; sourceCloneIndex++) {
await generateSingleClone(cloneIndex, basePrompt, sourceImage, openRouterApiKey, sourceIndex + 1, sourceCloneIndex);
cloneIndex++;
}
}
if (generatedImagesArray.length > 0) {
downloadAllBtn.classList.remove('hidden');
if (isIOS) {
showToast("αž€αŸ’αž›αžΌαž“αž”αžΆαž“αž‡αŸ„αž‚αž‡αŸαž™! αž…αž»αž…αž›αžΎαžšαžΌαž”αž“αžΈαž˜αž½αž™αŸ—αžŠαžΎαž˜αŸ’αž”αžΈ 'αžšαž€αŸ’αžŸαžΆαž‘αž»αž€' αž‘αŸ… iPhoneαŸ”", "success");
} else {
showToast(`αž€αžΆαžšαž€αŸ’αž›αžΌαž“αž”αžΆαž“αž‡αŸ„αž‚αž‡αŸαž™αž…αŸ†αž“αž½αž“ ${generatedImagesArray.length} αžšαžΌαž”αž—αžΆαž–!`, "success");
}
} else {
showToast("αž€αžΆαžšαž€αŸ’αž›αžΌαž“αžšαžΌαž”αž—αžΆαž–αž˜αžΆαž“αž”αž‰αŸ’αž αžΆ! αžŸαžΌαž˜αž–αž·αž“αž·αžαŸ’αž™ Model ឬ API KeyαŸ”", "error");
}
} catch (error) {
console.error(error);
if (isCreditError(error)) {
showAdminContactNotice("credit");
}
showToast(error.message || "αž€αžΆαžšαž€αŸ’αž›αžΌαž“αž˜αžΆαž“αž”αž‰αŸ’αž αžΆαŸ” αžŸαžΌαž˜αž–αž·αž“αž·αžαŸ’αž™ Model ឬ API KeyαŸ”", "error");
} finally {
generateBtn.removeAttribute('disabled');
generateBtn.innerHTML = `<i class="fa-solid fa-bolt"></i> αž…αžΆαž”αŸ‹αž•αŸ’αžαžΎαž˜αž€αŸ’αž›αžΌαž“αžšαžΌαž”αž—αžΆαž–`;
refreshOpenRouterBalance(false);
}
});
// Singular Clone Processor
async function generateSingleClone(index, prompt, sourceImage, openRouterApiKey, sourceNumber, sourceCloneNumber) {
const card = document.getElementById(`clone-card-${index}`);
try {
const uniquePrompt = prompt + ` [Source image: ${sourceNumber}. Clone variation: ${sourceCloneNumber}. Make this clone variation distinct and unique to index: ${index + Math.random()}]`;
const imageDataUrl = `data:${sourceImage.type};base64,${sourceImage.base64}`;
const payload = {
model: getSelectedOpenRouterModel(),
messages: [{
role: "user",
content: [
{
type: "text",
text: uniquePrompt
},
{
type: "image_url",
image_url: {
url: imageDataUrl
}
}
]
}],
modalities: ["image", "text"],
image_config: {
aspect_ratio: "1:1"
},
stream: false
};
const data = await fetchWithRetry(openRouterEndpoint, {
method: 'POST',
headers: {
'Authorization': `Bearer ${openRouterApiKey}`,
'Content-Type': 'application/json',
'HTTP-Referer': window.location.origin === 'null' ? 'http://localhost' : window.location.origin,
'X-OpenRouter-Title': openRouterAppTitle
},
body: JSON.stringify(payload)
});
const generatedImage = data.choices?.[0]?.message?.images?.[0];
const imageUrl = generatedImage?.image_url?.url || generatedImage?.imageUrl?.url;
if (!imageUrl) {
throw new Error("OpenRouter returned a response without image data.");
}
const finalImageSrc = imageUrl.startsWith('data:') ? imageUrl : `data:image/png;base64,${imageUrl}`;
generatedImagesArray.push(finalImageSrc);
card.innerHTML = `
<div onclick="openPreviewModal('${finalImageSrc}', ${index})" class="relative flex-grow flex items-center justify-center bg-slate-950/40 rounded-lg overflow-hidden h-52 group/item cursor-pointer">
<img src="${finalImageSrc}" alt="Cloned Image ${index}" class="cloned-success-img w-full h-full object-cover">
<div class="absolute inset-0 bg-slate-950/40 opacity-0 group-hover/item:opacity-100 transition-opacity flex items-center justify-center">
<span class="bg-indigo-600/90 text-white text-xs px-3 py-1.5 rounded-lg flex items-center gap-1">
<i class="fa-solid fa-magnifying-glass-plus"></i> αž˜αžΎαž›αžšαžΌαž”αž’αŸ†
</span>
</div>
</div>
<div class="mt-4 flex justify-between items-center">
<span class="text-xs text-indigo-400 bg-indigo-500/10 px-2.5 py-1 rounded-full font-semibold border border-indigo-500/10">αžšαžΌαž”αžŠαžΎαž˜ #${sourceNumber} β€’ αž€αŸ’αž›αžΌαž“ #${sourceCloneNumber}</span>
<button onclick="downloadImage('${finalImageSrc}', ${index})" class="btn-pop-3d text-xs text-white bg-indigo-600 hover:bg-indigo-700 px-3 py-1.5 rounded-lg flex items-center gap-1.5 transition-colors">
<i class="fa-solid fa-download"></i> αž‘αžΆαž‰αž™αž€
</button>
</div>
`;
} catch (error) {
console.error(error);
if (isCreditError(error)) {
showAdminContactNotice("credit");
}
const shortError = escapeHtml((error.message || "API request failed").slice(0, 120));
card.innerHTML = `
<div class="relative flex-grow flex items-center justify-center bg-red-950/20 border border-red-900/30 rounded-lg h-52">
<div class="text-center p-4">
<i class="fa-solid fa-triangle-exclamation text-2xl text-red-500 mb-2"></i>
<p class="text-xs text-red-400 font-semibold">αž”αžšαžΆαž‡αŸαž™αž€αŸ’αž“αž»αž„αž€αžΆαžšαž€αŸ’αž›αžΌαž“</p>
<p class="text-[9px] text-slate-500 mt-1 break-words">${shortError}</p>
</div>
</div>
<div class="mt-4 flex justify-between items-center">
<span class="text-xs text-red-500/50 bg-red-500/5 px-2.5 py-1 rounded-full font-semibold border border-red-500/10">αžšαžΌαž”αžŠαžΎαž˜ #${sourceNumber} β€’ αž€αŸ’αž›αžΌαž“ #${sourceCloneNumber}</span>
<button disabled class="btn-pop-3d opacity-40 text-slate-500 bg-slate-800 px-3 py-1.5 rounded-lg text-xs cursor-not-allowed">
<i class="fa-solid fa-ban"></i> αž”αžšαžΆαž‡αŸαž™
</button>
</div>
`;
}
}
// Master downloader
downloadAllBtn.addEventListener('click', () => {
if (isIOS) {
showToast("αžŸαž˜αŸ’αžšαžΆαž”αŸ‹ iPhoneαŸ– αžŸαžΌαž˜αž…αž»αž…αž›αžΎαžšαžΌαž”αž“αžΈαž˜αž½αž™αŸ— αžšαž½αž…αžŸαž„αŸ’αž€αžαŸ‹αžŠαžΎαž˜αŸ’αž”αžΈαžšαž€αŸ’αžŸαžΆαž‘αž»αž€αž€αŸ’αž“αž»αž„αž‘αžΌαžšαžŸαž–αŸ’αž‘αŸ”", "success");
return;
}
const successImages = document.querySelectorAll('.cloned-success-img');
successImages.forEach((img, i) => {
setTimeout(() => {
const link = document.createElement('a');
link.href = img.src;
link.download = `cloned_image_${i + 1}.png`;
document.body.appendChild(link);
link.click();
link.remove();
}, i * 300);
});
showToast("αž€αŸ†αž–αž»αž„αž‘αžΆαž‰αž™αž€αžšαžΌαž”αž—αžΆαž–αž‘αžΆαŸ†αž„αž’αžŸαŸ‹...", "success");
});
</script>
</body>
</html>