pardazesh / index.html
Elias207's picture
Update index.html
ff79bf5 verified
Raw
History Blame Contribute Delete
62.1 kB
<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>تبدیل عکس به عروسک بافتنی</title>
<link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<style>
:root {
--app-font: 'Vazirmatn', sans-serif;
--app-bg: #F8F9FC;
--panel-bg: #FFFFFF;
--panel-border: #EAEFF7;
--input-bg: #F6F8FB;
--input-border: #E1E7EF;
--text-primary: #1A202C;
--text-secondary: #626F86;
--text-tertiary: #8A94A6;
--accent-primary: #4A6CFA;
--accent-primary-hover: #3553D6;
--accent-primary-glow: rgba(74, 108, 250, 0.25);
--accent-secondary: #0FD4A8;
--danger-color: #e53e3e;
--danger-color-hover: #c53030;
--shadow-sm: 0 1px 2px 0 rgba(26, 32, 44, 0.03);
--shadow-md: 0 4px 6px -1px rgba(26, 32, 44, 0.05), 0 2px 4px -2px rgba(26, 32, 44, 0.04);
--shadow-lg: 0 10px 15px -3px rgba(26, 32, 44, 0.06), 0 4px 6px -4px rgba(26, 32, 44, 0.05);
--shadow-xl: 0 20px 25px -5px rgba(26, 32, 44, 0.07), 0 8px 10px -6px rgba(26, 32, 44, 0.05);
--radius-card: 24px;
--radius-btn: 14px;
--radius-input: 12px;
--transition-smooth: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
}
@keyframes fadeIn { from { opacity: 0; transform: translateY(15px); } to { opacity: 1; transform: translateY(0); } }
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
@keyframes float { 0%, 100% { transform: translateY(0px); } 50% { transform: translateY(-10px); } }
@keyframes slideInUp { from { opacity: 0; transform: translateY(30px); } to { opacity: 1; transform: translateY(0); } }
@keyframes pulse-glow { 0%, 100% { filter: drop-shadow(0 5px 15px rgba(74, 108, 250, 0.2)); } 50% { filter: drop-shadow(0 8px 25px rgba(74, 108, 250, 0.4)); } }
@keyframes move-neural {
from { background-position: 0 0, 15px 15px; }
to { background-position: 30px 60px, 45px 75px; }
}
body {
font-family: var(--app-font); background-color: var(--app-bg); color: var(--text-primary);
margin: 0; padding: 2.5rem 1rem; display: flex; justify-content: center; align-items: flex-start; min-height: 100vh;
}
.container { max-width: 820px; width: 100%; }
header {
position: relative; text-align: center; margin-bottom: 2.5rem;
padding: 2rem 0; animation: fadeIn 0.8s 0.1s ease-out backwards;
overflow: hidden; border-radius: var(--radius-card);
}
header::before {
content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0;
background-image: radial-gradient(circle at center, rgba(74, 108, 250, 0.1) 1px, transparent 1px), radial-gradient(circle at center, rgba(74, 108, 250, 0.1) 1px, transparent 1px);
background-size: 30px 30px; background-position: 0 0, 15px 15px;
animation: move-neural 20s linear infinite; z-index: 0;
}
.header-content { position: relative; z-index: 1; }
.header-animation-container {
width: 200px; /* Adjusted size for new animation */
height: 200px;/* Adjusted size for new animation */
margin: 0 auto 1.5rem;
display: flex;
justify-content: center;
align-items: center;
}
/* --- NEW ANIMATION STYLES for Knitted Person --- */
@keyframes draw-head-outline-cycle {
0%, 5.5% { stroke-dashoffset: 158; } /* 0s -> 0.5s: خط مخفی است */
33.3% { stroke-dashoffset: 0; } /* 0.5s -> 3s: خط رسم می‌شود */
100% { stroke-dashoffset: 0; } /* 3s -> 9s: خط باقی می‌ماند */
}
@keyframes fill-head-texture-cycle {
0%, 33.3% { opacity: 0; transform: scale(0.9); } /* 0s -> 3s: بافت مخفی است */
44.4% { opacity: 1; transform: scale(1); } /* 3s -> 4s: بافت ظاهر می‌شود */
100% { opacity: 1; transform: scale(1); } /* 4s -> 9s: بافت باقی می‌ماند */
}
@keyframes draw-body-outline-cycle {
0%, 38.8% { stroke-dashoffset: 220; } /* 0s -> 3.5s: خط مخفی است */
72.2% { stroke-dashoffset: 0; } /* 3.5s -> 6.5s: خط رسم می‌شود */
100% { stroke-dashoffset: 0; } /* 6.5s -> 9s: خط باقی می‌ماند */
}
@keyframes fill-body-texture-cycle {
0%, 72.2% { opacity: 0; transform: scale(0.9); } /* 0s -> 6.5s: بافت مخفی است */
83.3% { opacity: 1; transform: scale(1); } /* 6.5s -> 7.5s: بافت ظاهر می‌شود */
100% { opacity: 1; transform: scale(1); } /* 7.5s -> 9s: بافت باقی می‌ماند */
}
@keyframes draw-details-cycle {
0%, 77.7% { stroke-dashoffset: 60; } /* 0s -> 7s: جزئیات مخفی هستند */
88.8% { stroke-dashoffset: 0; } /* 7s -> 8s: جزئیات رسم می‌شوند */
100% { stroke-dashoffset: 0; } /* 8s -> 9s: جزئیات باقی می‌مانند */
}
@keyframes dot-visibility-cycle {
0% { opacity: 0; }
5.5% { opacity: 1; } /* 0.5s: ظاهر می‌شود */
83.3% { opacity: 1; } /* 7.5s: شروع به محو شدن می‌کند */
94.4%, 100% { opacity: 0; }
}
@keyframes dot-movement-cycle {
0%, 5.5% { offset-distance: 0%; } /* 0s -> 0.5s: در نقطه شروع است */
83.3% { offset-distance: 100%; } /* 0.5s -> 7.5s: در حال حرکت است */
100% { offset-distance: 100%; } /* 7.5s -> 9s: در نقطه پایان است */
}
#head-outline {
stroke-dasharray: 158;
animation: draw-head-outline-cycle 9s infinite;
}
#head-fill {
transform-origin: center;
animation: fill-head-texture-cycle 9s infinite;
}
#body-outline {
stroke-dasharray: 220;
animation: draw-body-outline-cycle 9s infinite;
}
#body-fill {
transform-origin: center;
animation: fill-body-texture-cycle 9s infinite;
}
#arm-left, #arm-right, #leg-left, #leg-right, #smile {
stroke-dasharray: 60;
animation: draw-details-cycle 9s infinite;
}
#magic-knitting-dot {
animation: dot-visibility-cycle 9s infinite, dot-movement-cycle 9s infinite;
}
/* --- END NEW ANIMATION STYLES --- */
h1 {
font-size: 2.8rem; font-weight: 800; margin: 0 0 0.8rem 0;
background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary));
-webkit-background-clip: text; -webkit-text-fill-color: transparent; letter-spacing: -1px;
}
.subtitle { font-size: 1.1rem; color: var(--text-secondary); margin-top: 0; padding: 0 1rem; }
main, .sample-gallery-section {
padding: 3rem; background-color: var(--panel-bg); border-radius: var(--radius-card);
box-shadow: var(--shadow-xl); border: 1px solid var(--panel-border); animation: fadeIn 0.8s 0.3s ease-out backwards;
}
.sample-gallery-section {
margin-top: 3rem;
animation-delay: 0.5s;
}
.form-group { margin-bottom: 2.5rem; }
.form-group:last-child { margin-bottom: 0; }
.form-label { display: flex; align-items: center; gap: 0.75rem; font-weight: 700; color: var(--text-primary); font-size: 1.2em; margin-bottom: 1.2rem; }
.form-label svg { width: 24px; height: 24px; color: var(--accent-primary); }
#upload-area {
position: relative; border: 2px dashed var(--input-border); border-radius: var(--radius-input); padding: 2.5rem; text-align: center;
cursor: pointer; transition: var(--transition-smooth); background-color: var(--input-bg); min-height: 200px; display: flex;
flex-direction: column; justify-content: center; align-items: center; overflow: hidden;
}
#upload-area.drag-over, #upload-area:hover:not(.has-file) { border-color: var(--accent-primary); background-color: #fff; box-shadow: 0 0 15px var(--accent-primary-glow); }
#upload-area.has-file { border-style: solid; border-color: var(--panel-border); padding: 0; cursor: default; }
#upload-content { display: flex; flex-direction: column; align-items: center; gap: 1rem; }
#upload-icon svg { width: 48px; height: 48px; color: var(--accent-primary); stroke-width: 1.5; opacity: 0.8; }
#upload-area p { margin: 0; color: var(--text-secondary); font-weight: 500; }
#preview-image-main { display: none; width: 100%; height: 100%; object-fit: contain; position: absolute; top: 0; left: 0; }
#remove-file-btn-main {
display: none; position: absolute; top: 12px; right: 12px; background-color: rgba(26, 32, 44, 0.6); color: white; border: none;
width: 32px; height: 32px; border-radius: 50%; cursor: pointer; font-size: 1.2rem; line-height: 1; z-index: 10;
transition: var(--transition-smooth); padding: 0;
}
#remove-file-btn-main:hover { background-color: var(--danger-color); transform: scale(1.1); }
#upload-area.has-file #upload-content { display: none; }
#upload-area.has-file #preview-image-main, #upload-area.has-file #remove-file-btn-main { display: block; }
.settings-group { margin-bottom: 1.5rem; margin-top: -1.5rem; }
.settings-header { display: flex; justify-content: center; margin-bottom: 0rem; }
#settings-btn {
background-color: #fff; border: 1px solid var(--panel-border); width: 48px; height: 48px;
border-radius: 50%; cursor: pointer; display: flex; align-items: center; justify-content: center;
box-shadow: var(--shadow-md); transition: var(--transition-smooth);
}
#settings-btn svg { width: 24px; height: 24px; color: var(--text-secondary); transition: var(--transition-smooth); }
#settings-btn:hover { transform: scale(1.1) rotate(15deg); box-shadow: 0 0 15px var(--accent-primary-glow); border-color: var(--accent-primary); }
#settings-btn:hover svg { color: var(--accent-primary); }
#settings-btn.open { border-color: var(--accent-primary); background-color: var(--accent-primary); }
#settings-btn.open svg { color: #fff; transform: rotate(90deg); }
#prompt-panel {
max-height: 0; opacity: 0; overflow: hidden; visibility: hidden;
transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
background-color: var(--input-bg); border-radius: var(--radius-input);
padding: 0 1.5rem; border: 1px solid transparent;
}
#prompt-panel.open {
max-height: 500px; opacity: 1; visibility: visible;
margin-top: 1.5rem; padding: 1.5rem;
border-color: var(--panel-border);
}
#prompt-textarea {
width: 100%; background-color: #fff; border: 1px solid var(--input-border);
border-radius: var(--radius-input); padding: 1rem; font-family: var(--app-font);
font-size: 1rem; color: var(--text-primary); resize: vertical; min-height: 100px;
box-sizing: border-box; transition: var(--transition-smooth); direction: ltr; text-align: left;
}
#prompt-textarea:focus { border-color: var(--accent-primary); box-shadow: 0 0 10px var(--accent-primary-glow); outline: none; }
#reset-prompt-btn {
background: none; border: none; color: var(--accent-primary); font-weight: 600;
cursor: pointer; margin-top: 1rem; padding: 0.5rem; float: left;
transition: var(--transition-smooth);
}
#reset-prompt-btn:hover { color: var(--accent-primary-hover); text-decoration: underline; }
#submit-btn {
display: flex; align-items: center; justify-content: center; gap: 0.75rem; width: 100%; padding: 1.1rem; font-size: 1.2rem; font-weight: 700; background: linear-gradient(95deg, var(--accent-secondary) 0%, var(--accent-primary) 100%);
color: #fff; border: none; border-radius: var(--radius-btn); cursor: pointer; transition: all 0.3s ease; box-shadow: 0 6px 12px -3px var(--accent-primary-glow), 0 6px 12px -3px rgba(15, 212, 168, 0.25);
}
#submit-btn svg { width: 24px; height: 24px; filter: drop-shadow(0 0 5px rgba(255,255,255,0.5)); }
#submit-btn:hover:not(:disabled) { transform: translateY(-5px) scale(1.02); box-shadow: 0 8px 20px -4px var(--accent-primary-glow), 0 8px 20px -4px rgba(15, 212, 168, 0.3); }
#submit-btn:disabled { background: var(--text-tertiary); cursor: not-allowed; box-shadow: none; opacity: 0.7; }
#submit-btn .spinner { width: 20px; height: 20px; border: 3px solid rgba(255, 255, 255, 0.4); border-top-color: #fff; border-radius: 50%; animation: spin 0.8s linear infinite; display: none; }
#result-container { min-height: 250px; position: relative; padding: 1rem; background-color: var(--input-bg); border-radius: var(--radius-card); border: 2px dashed var(--input-border); box-shadow: var(--shadow-sm) inset; transition: var(--transition-smooth); display: flex; align-items: center; justify-content: center; flex-direction: column; box-sizing: border-box; }
#result-container.loading, #result-container.has-content:has(.ip-reset-guide-container) { padding: 0; border: none; background-color: transparent; box-shadow: none; }
#result-container.has-content { border-style: solid; border-color: var(--panel-border); background-color: var(--panel-bg); }
#loading-placeholder { display: none; }
#result-container.loading #loading-placeholder { display: flex; animation: fadeIn 0.5s; justify-content: center; align-items: center; width: 100%; }
.generator-container { position: relative; width: 100%; max-width: 550px; height: 300px; border: 2px solid #38bdf8; border-radius: 20px; overflow: hidden; box-shadow: 0 0 40px rgba(56, 189, 248, 0.3); animation: pulse 5s infinite cubic-bezier(0.4, 0, 0.6, 1); background-color: #161b22; color: #f0f6fc; }
@keyframes pulse { 0% { box-shadow: 0 0 40px rgba(56, 189, 248, 0.3); } 50% { box-shadow: 0 0 60px rgba(56, 189, 248, 0.7); } 100% { box-shadow: 0 0 40px rgba(56, 189, 248, 0.3); } }
.text-overlay { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 20px; font-weight: 700; text-shadow: 0 0 20px rgba(56, 189, 248, 0.8), 0 0 30px rgba(187, 134, 252, 0.5); animation: glow-text 7s infinite ease-in-out; font-family: var(--app-font); width: 90%; text-align: center; }
@keyframes glow-text { 0% { opacity: 0.7; } 50% { opacity: 1; } 100% { opacity: 0.7; } }
.progress-bar { position: absolute; bottom: 0; left: 0; width: 0%; height: 6px; background: linear-gradient(to right, #38bdf8, #bb86fc, #facc15); transition: width 0.5s linear; }
#result-images-grid { display: none; width: 100%; grid-template-columns: 1fr; gap: 1rem; padding: 1rem; }
#result-container.has-content #result-images-grid { display: grid; animation: fadeIn 0.5s; }
.result-image-wrapper { display: flex; flex-direction: column; gap: 0.75rem; }
.result-image-display { width: 100%; object-fit: contain; border-radius: var(--radius-input); cursor: pointer; box-shadow: var(--shadow-md); border: 1px solid var(--panel-border); transition: var(--transition-smooth); }
.result-image-display:hover { box-shadow: var(--shadow-lg); transform: scale(1.02); }
#error-message { color: var(--danger-color); text-align: center; margin-top: 1rem; display: none; font-weight: 500; }
#lightbox { position: fixed; inset: 0; background-color: rgba(18, 24, 38, 0.8); backdrop-filter: blur(10px); display: flex; align-items: center; justify-content: center; z-index: 1000; opacity: 0; transition: opacity 0.3s; pointer-events: none; }
#lightbox.visible { opacity: 1; pointer-events: auto; }
#lightbox-content {
position: relative; animation: fadeIn 0.3s ease; display: flex;
flex-direction: column; align-items: center; justify-content: center;
width: 100%; height: 100%; gap: 1rem; padding: 1rem 0;
}
#lightbox-img {
max-width: 85vw; max-height: 85vh; object-fit: contain; border-radius: var(--radius-card);
box-shadow: var(--shadow-xl);
}
.lightbox-btn { position: absolute; background-color: rgba(255, 255, 255, 0.1); color: white; border: 1px solid rgba(255,255,255,0.2); width: 44px; height: 44px; border-radius: 50%; cursor: pointer; display: flex; justify-content: center; align-items: center; transition: all 0.2s; text-decoration: none; font-size: 24px; z-index: 1010; }
.lightbox-btn:hover { background-color: rgba(255, 255, 255, 0.2); transform: scale(1.1); }
#lightbox-close { top: 2rem; right: 2rem; }
#lightbox-download { top: 2rem; left: 2rem; }
#lightbox-prev, #lightbox-next { top: 50%; transform: translateY(-50%); font-size: 32px; display: none; }
#lightbox-prev { left: 1rem; }
#lightbox-next { right: 1rem; }
#sample-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.5rem;
margin-top: 1.5rem;
}
.sample-item {
position: relative;
cursor: pointer;
border-radius: var(--radius-input);
overflow: hidden;
box-shadow: var(--shadow-md);
transition: var(--transition-smooth);
aspect-ratio: 1 / 1;
}
.sample-item img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
transition: transform 0.3s ease;
}
.sample-item:hover {
transform: translateY(-5px);
box-shadow: var(--shadow-lg);
}
.sample-item:hover img {
transform: scale(1.05);
}
.sample-item .loading-overlay {
position: absolute;
inset: 0;
background-color: rgba(26, 32, 44, 0.7);
backdrop-filter: blur(4px);
display: none;
align-items: center;
justify-content: center;
z-index: 10;
}
.sample-item.is-loading .loading-overlay {
display: flex;
}
.sample-item .loading-overlay .spinner {
width: 40px;
height: 40px;
border: 4px solid rgba(255, 255, 255, 0.4);
border-top-color: #fff;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
:root {
--primary-gradient-guide: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--success-gradient-guide: linear-gradient(135deg, #56ab2f 0%, #a8e063 100%);
--guide-bg: rgba(255, 255, 255, 0.98);
--guide-border: rgba(102, 126, 234, 0.2);
--guide-text-title: #2d3748;
--guide-text-body: #4a5568;
--guide-accent: #667eea;
--radius-md-guide: 12px;
--radius-lg-guide: 20px;
}
.ip-reset-guide-container {
text-align: right; background: var(--guide-bg); backdrop-filter: blur(10px); padding: 40px;
border-radius: var(--radius-lg-guide); box-shadow: var(--shadow-xl); border: 1px solid var(--guide-border);
animation: slideInUp 0.6s cubic-bezier(0.4, 0, 0.2, 1) both; max-width: 650px;
width: 100%; position: relative; overflow: hidden; box-sizing: border-box;
}
.ip-reset-guide-container::before {
content: ''; position: absolute; top: 0; left: 0; right: 0; height: 5px;
background: var(--primary-gradient-guide);
}
.guide-header { display: flex; align-items: center; margin-bottom: 30px; }
.guide-header-icon { width: 60px; height: 60px; margin-left: 20px; animation: float 3s ease-in-out infinite; }
.guide-header h2 { font-size: 1.5rem; color: var(--guide-text-title); font-weight: 700; margin: 0; }
.guide-header p { color: var(--guide-text-body); font-size: 0.8rem; margin-top: 5px; }
.guide-content { font-size: 0.95rem; color: var(--guide-text-body); line-height: 1.8; }
.info-card {
background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%);
border: 1px solid rgba(102, 126, 234, 0.2); border-radius: var(--radius-md-guide);
padding: 20px; margin: 20px 0; position: relative; overflow: hidden;
}
.info-card p { font-size: 0.85rem; line-height: 1.7; }
.info-card::before {
content: ''; position: absolute; top: 0; right: 0; width: 4px; height: 100%;
background: var(--primary-gradient-guide);
}
.info-card-header { display: flex; align-items: center; margin-bottom: 12px; }
.info-card-icon { width: 24px; height: 24px; margin-left: 12px; }
.info-card-title { font-weight: 600; color: var(--guide-text-title); font-size: 1rem; }
.method-cards { display: grid; gap: 15px; margin: 20px 0; }
.method-card {
background: linear-gradient(135deg, #f6f8fb 0%, #ffffff 100%); border: 1px solid #e2e8f0;
border-radius: var(--radius-md-guide); padding: 20px; display: flex; align-items: flex-start;
transition: var(--transition-smooth); cursor: pointer;
}
.method-card:hover { transform: translateX(5px); box-shadow: var(--shadow-lg); border-color: var(--guide-accent); }
.method-card-icon { width: 40px; height: 40px; margin-left: 15px; flex-shrink: 0; }
.method-card-content { flex: 1; }
.method-card-title { font-weight: 600; color: var(--guide-text-title); margin-bottom: 8px; display: flex; align-items: center; font-size: 0.9rem; }
.method-badge {
background: var(--success-gradient-guide); color: white; font-size: 0.7rem; padding: 2px 8px;
border-radius: 12px; margin-right: 8px; font-weight: 500;
}
.method-card-desc { color: var(--guide-text-body); font-size: 0.9rem; line-height: 1.6; }
.summary-section {
margin-top: 25px; padding: 20px; border-radius: var(--radius-md-guide);
background: linear-gradient(135deg, #56ab2f15 0%, #a8e06315 100%);
border: 1px solid rgba(86, 171, 47, 0.2); position: relative; overflow: hidden;
}
.summary-section::before {
content: ''; position: absolute; top: 0; right: 0; width: 4px; height: 100%;
background: var(--success-gradient-guide);
}
.summary-header { display: flex; align-items: center; margin-bottom: 10px; }
.summary-icon { width: 24px; height: 24px; margin-left: 10px; }
.summary-title { font-weight: 600; color: #2f5a33; font-size: 1rem; }
.summary-text { color: #2f5a33; font-size: 0.9rem; line-height: 1.7; }
.guide-actions { display: flex; gap: 15px; margin-top: 30px; padding-top: 25px; border-top: 1px solid #e2e8f0; }
.action-button {
padding: 14px 24px; border: none; border-radius: var(--radius-md-guide); font-size: 0.95rem; font-weight: 600;
cursor: pointer; font-family: inherit; flex: 1; transition: var(--transition-smooth);
position: relative; overflow: hidden; display: flex; align-items: center; justify-content: center;
}
.action-button-icon { width: 20px; height: 20px; margin-right: 8px; margin-left: 0; }
.back-button { background: white; color: var(--guide-text-body); border: 2px solid #e2e8f0; flex: 0.4; }
.back-button:hover { background: #f7fafc; border-color: var(--guide-accent); transform: translateY(-2px); box-shadow: var(--shadow-md); }
.retry-button { background: var(--primary-gradient-guide); color: white; flex: 0.6; box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3); }
.retry-button:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4); }
.retry-button::before {
content: ''; position: absolute; top: 0; left: -100%; width: 100%; height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
transition: left 0.5s;
}
.retry-button:hover::before { left: 100%; }
.video-button-container { text-align: center; margin: 25px 0 10px 0; }
.elegant-video-button {
display: inline-flex; align-items: center; justify-content: center; padding: 7px 18px;
background-color: #f0f2f5; color: var(--guide-accent); border: 1px solid #e2e8f0;
text-decoration: none; border-radius: var(--radius-md-guide); font-weight: 600; font-size: 0.8rem;
cursor: pointer; font-family: inherit; transition: all 0.3s ease; box-shadow: var(--shadow-sm);
}
.elegant-video-button:hover {
background: var(--primary-gradient-guide); color: white; border-color: transparent;
transform: translateY(-2px); box-shadow: 0 6px 16px rgba(102, 126, 234, 0.3);
}
.elegant-video-button-icon { width: 16px; height: 16px; margin-left: 8px; fill: currentColor; }
@media (max-width: 768px) {
main, .sample-gallery-section { padding: 1.5rem; } h1 { font-size: 2.2rem; }
#result-images-grid { grid-template-columns: 1fr; }
.header-animation-container { width: 150px; height: 150px; margin-bottom: 1rem;}
.ip-reset-guide-container { padding: 25px; }
.guide-header h2 { font-size: 1.2rem; }
.method-card { padding: 15px; }
#sample-grid { gap: 1rem; }
}
@media (max-width: 480px) {
#sample-grid { grid-template-columns: repeat(2, 1fr); }
}
</style>
</head>
<body>
<div class="container">
<header>
<div class="header-content">
<div class="header-animation-container">
<svg viewBox="0 0 250 250" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="yarn-stroke-pattern" patternUnits="userSpaceOnUse" width="10" height="10" patternTransform="rotate(45)">
<line x1="0" y1="0" x2="10" y2="0" stroke="#E57373" stroke-width="5" />
<line x1="0" y1="5" x2="10" y2="5" stroke="#FFB74D" stroke-width="5" />
</pattern>
<pattern id="knit-fill-pattern" patternUnits="userSpaceOnUse" width="8" height="8">
<path d="M 0 0 L 4 4 L 8 0" stroke="#FFCC80" stroke-width="1.5" fill="none" />
</pattern>
<filter id="dot-glow-filter">
<feGaussianBlur stdDeviation="3.5" result="coloredBlur"/>
<feMerge>
<feMergeNode in="coloredBlur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<!-- گروه آدمک -->
<g id="knitted-person">
<!-- لایه‌های پرکننده -->
<circle id="head-fill" cx="125" cy="60" r="25" fill="url(#knit-fill-pattern)" />
<ellipse id="body-fill" cx="125" cy="130" rx="35" ry="45" fill="url(#knit-fill-pattern)" />
<!-- خطوط اصلی -->
<circle id="head-outline" cx="125" cy="60" r="25" fill="none" stroke="url(#yarn-stroke-pattern)" stroke-width="9" />
<ellipse id="body-outline" cx="125" cy="130" rx="35" ry="45" fill="none" stroke="url(#yarn-stroke-pattern)" stroke-width="9" />
<!-- جزئیات نهایی -->
<path id="arm-left" d="M90,120 L60,100" stroke="#A1887F" stroke-width="6" stroke-linecap="round"/>
<path id="arm-right" d="M160,120 L190,100" stroke="#A1887F" stroke-width="6" stroke-linecap="round"/>
<path id="leg-left" d="M110,175 L90,210" stroke="#A1887F" stroke-width="6" stroke-linecap="round"/>
<path id="leg-right" d="M140,175 L160,210" stroke="#A1887F" stroke-width="6" stroke-linecap="round"/>
<path id="smile" d="M118,65 Q125,75 132,65" stroke="#D32F2F" stroke-width="3" fill="none" stroke-linecap="round"/>
</g>
<!-- نقطه جادویی -->
<circle id="magic-knitting-dot" r="7" fill="#FFFFFF" filter="url(#dot-glow-filter)" style="offset-path: path('M125,35 A25,25 0 1,0 125,85 A25,25 0 0,0 125,35 Z M125,85 A35,45 0 0,0 125,175 A35,45 0 0,0 125,85 Z');" />
</svg>
</div>
<h1>عکس‌های خود را به عروسک بافتنی تبدیل کنید 🧶</h1>
<p class="subtitle">با هوش مصنوعی، تصاویر خود را به دنیای نرم و دوست‌داشتنی آمیگورومی ببرید.</p>
</div>
</header>
<main>
<div class="form-group">
<div class="form-label"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4Z"/></svg>۱. تصویر خود را برای تبدیل انتخاب کنید</div>
<label id="upload-area" for="file-input">
<div id="upload-content">
<div id="upload-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="17 8 12 3 7 8"></polyline><line x1="12" y1="3" x2="12" y2="15"></line></svg></div>
<p>فایل تصویر را اینجا بکشید یا برای انتخاب کلیک کنید</p>
</div>
<img id="preview-image-main" src="" alt="پیش‌نمایش تصویر برای تبدیل">
<button type="button" id="remove-file-btn-main" title="حذف تصویر">&times;</button>
</label>
<input type="file" id="file-input" accept="image/jpeg, image/png" hidden>
</div>
<div class="form-group settings-group">
<div class="settings-header">
<button id="settings-btn" title="تنظیمات پیشرفته">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="4" y1="21" x2="4" y2="14"></line><line x1="4" y1="10" x2="4" y2="3"></line><line x1="12" y1="21" x2="12" y2="12"></line><line x1="12" y1="8" x2="12" y2="3"></line><line x1="20" y1="21" x2="20" y2="16"></line><line x1="20" y1="12" x2="20" y2="3"></line><line x1="1" y1="14" x2="7" y2="14"></line><line x1="9" y1="8" x2="15" y2="8"></line><line x1="17" y1="16" x2="23" y2="16"></line></svg>
</button>
</div>
<div id="prompt-panel">
<label for="prompt-textarea" class="form-label" style="font-size: 1.1em; margin-bottom: 1rem;">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>
دستورالعمل سفارشی (پرامپت)
</label>
<textarea id="prompt-textarea" rows="4" placeholder="دستورالعمل خود را برای هوش مصنوعی اینجا بنویسید..."></textarea>
<button id="reset-prompt-btn">بازنشانی به پیش‌فرض</button>
</div>
</div>
<div class="form-group">
<button id="submit-btn" disabled><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 3L12 8L17 10L12 12L10 17L8 12L3 10L8 8L10 3z"/></svg><span id="btn-text">شروع ساخت</span><div class="spinner"></div></button>
<p id="error-message"></p>
</div>
<div class="form-group">
<div class="form-label"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 3L12 8L17 10L12 12L10 17L8 12L3 10L8 8L10 3z"/><path d="M21 14l-1.5 3-3-1.5 3-3 1.5 3z"/><path d="M19.5 2.5l-3 1.5 1.5 3 3-1.5-1.5-3z"/></svg>۲. نتیجه خلاقانه را ببینید</div>
<div id="result-container"></div>
</div>
</main>
<section class="sample-gallery-section">
<div class="form-label">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-compass"><circle cx="12" cy="12" r="10"></circle><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"></polygon></svg>
یا با یک نمونه شروع کنید
</div>
<div id="sample-grid">
<div class="sample-item" data-restored-src="https://uploadkon.ir/uploads/390710_25Negar-1760100657714.png">
<img src="https://uploadkon.ir/uploads/333a10_252.png" alt="تصویر نمونه برای تبدیل">
<div class="loading-overlay"><div class="spinner"></div></div>
</div>
<div class="sample-item" data-restored-src="https://uploadkon.ir/uploads/e41610_25Negar-1760100882244.png">
<img src="https://uploadkon.ir/uploads/33d110_25com-bongasoft-addremovewatermark-1756029389631.png" alt="تصویر نمونه برای تبدیل">
<div class="loading-overlay"><div class="spinner"></div></div>
</div>
<div class="sample-item" data-restored-src="https://uploadkon.ir/uploads/736610_25Negar-1760101089072.png">
<img src="https://uploadkon.ir/uploads/459e10_251.png" alt="تصویر نمونه برای تبدیل">
<div class="loading-overlay"><div class="spinner"></div></div>
</div>
</div>
</section>
</div>
<div id="lightbox">
<div id="lightbox-content">
<img id="lightbox-img" src="">
<button id="lightbox-close" class="lightbox-btn" title="بستن">&times;</button>
<a id="lightbox-download" title="دانلود تصویر" class="lightbox-btn" href="#" download><svg fill="white" viewBox="0 0 24 24"><path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/></svg></a>
<button id="lightbox-prev" class="lightbox-btn" title="قبلی">&#10094;</button>
<button id="lightbox-next" class="lightbox-btn" title="بعدی">&#10095;</button>
</div>
</div>
<script>
const DEFAULT_PROMPT = `{
"prompt": {
"type": "image_transformation",
"style": "amigurumi_crochet",
"description": "Create a crocheted amigurumi interpretation of the uploaded photo, recreated entirely in soft yarn textures. Transform the scene into plush dolls and fabric elements while maintaining the original composition, color palette, and recognizable poses. All elements including background and characters should appear handcrafted with visible stitching, button eyes, and detailed crochet patterns. The final result should have a cute, soft, and three-dimensional appearance resembling a handmade diorama.",
"technical_details": {
"medium": "yarn",
"technique": "crochet",
"texture": "soft, plush, tactile",
"distinguishing_features": [
"visible stitching",
"button eyes",
"detailed crochet patterns",
"handmade aesthetic"
]
},
"composition_notes": "Preserve the original spatial arrangement and color relationships while transforming all elements into crocheted forms"
}
}`;
const PROMPT_STORAGE_KEY = 'userAmigurumiPrompt';
const GPU_QUOTA_ERROR_HTML = `<div class="ip-reset-guide-container">
<div class="guide-header">
<svg class="guide-header-icon" viewbox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<defs><lineargradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" style="stop-color: #667eea; stop-opacity: 1;"></stop><stop offset="100%" style="stop-color: #764ba2; stop-opacity: 1;"></stop></lineargradient></defs>
<circle cx="50" cy="50" r="45" fill="url(#grad1)" opacity="0.1"></circle><circle cx="50" cy="50" r="35" fill="none" stroke="url(#grad1)" stroke-width="2" opacity="0.3"></circle><path d="M35 50 L45 60 L65 40" stroke="url(#grad1)" stroke-width="4" fill="none" stroke-linecap="round" stroke-linejoin="round"></path><circle cx="65" cy="35" r="8" fill="#fee140"></circle><path d="M62 35 L68 35 M65 32 L65 38" stroke="white" stroke-width="2" stroke-linecap="round"></path>
</svg>
<div>
<h2>یک قدم تا ساخت تصاویر جدید</h2>
<p>نیازمند تغییر نقطه دستیابی</p>
<p>بین نقطه دستیابی&nbsp; جابجا شوید <span style="font-size: 8pt;">تلاش مجدد بزنید تا تصاویر مجدداً ساخته بشه</span></p>
</div>
</div>
<div class="guide-content">
<div class="info-card">
<div class="info-card-header">
<svg class="info-card-icon" viewbox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" fill="#667eea" opacity="0.2"></path><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" stroke="#667eea" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>
<span class="info-card-title">قانون ساده این نسخه</span>
</div>
<p>طبق ویدیو آموزشی پایین، نقطه دستیابی گوشی رو تغییر بدید با هر بار ساخت تصویر نیاز است بین نقطه دستیابی جابجا بشید.</p>
</div>
<div class="summary-section">
<div class="summary-header">
<svg class="summary-icon" viewbox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="12" r="10" fill="#56ab2f" opacity="0.2"></circle><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2z" stroke="#56ab2f" stroke-width="2"></path><path d="M9 12l2 2 4-4" stroke="#56ab2f" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>
<span class="summary-title">خلاصه راهنما</span>
</div>
<div class="summary-text">هربار که این صفحه را مشاهده کردید: از اینترنت سیم‌کارت استفاده کنید، VPN را خاموش کرده و طبق ویدیو آموزشی پایین نقطه دستیابی رو تغییر دهید. سپس روی «تلاش مجدد» کلیک کنید. با این روش ساده می‌توانید به صورت نامحدود تصویر بسازید! ☘️</div>
</div>
<div class="video-button-container">
<button id="tutorialLinkBtn" class="elegant-video-button">
<svg class="elegant-video-button-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"></path></svg>
دیدن ویدیو آموزشی استفاده نامحدود
</button>
</div>
</div>
<div class="guide-actions">
<button class="action-button back-button"><svg class="action-button-icon" viewbox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M19 12H5M12 19l-7-7 7-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>بازگشت</button>
<button class="action-button retry-button"><svg class="action-button-icon" viewbox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M23 4v6h-6M1 20v-6h6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path><path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>تلاش مجدد</button>
</div>
</div>`;
const LOADING_STATE_HTML = `<div id="loading-placeholder"><div style="display: flex; flex-direction: column; align-items: center; width: 100%;"><div class="generator-container"><div class="text-overlay" id="loading-status-text">منتظر شروع پردازش...</div><div class="progress-bar" id="loading-progress-bar"></div></div></div></div><div id="result-images-grid"></div>`;
const idbHelper = {
db: null, dbName: 'aiFaceCreatorDB', dbVersion: 1,
init() { return new Promise((resolve, reject) => { const request = indexedDB.open(this.dbName, this.dbVersion); request.onupgradeneeded = (e) => { this.db = e.target.result; if (!this.db.objectStoreNames.contains('sourceImage')) this.db.createObjectStore('sourceImage', { keyPath: 'id' }); }; request.onsuccess = (e) => { this.db = e.target.result; resolve(this.db); }; request.onerror = (e) => { console.error('IndexedDB error:', e.target.error); reject(e.target.error); }; }); },
async perform(storeName, mode, action) { if (!this.db) await this.init(); return new Promise((resolve, reject) => { const transaction = this.db.transaction(storeName, mode); const store = transaction.objectStore(storeName); action(store, resolve, reject); transaction.onerror = (e) => reject(e.target.error); }); },
saveSourceImage(file) { return this.perform('sourceImage', 'readwrite', (store, resolve) => { store.put({ id: 'user-image', file: file }).onsuccess = resolve; }); },
getSourceImage() { return this.perform('sourceImage', 'readonly', (store, resolve) => { store.get('user-image').onsuccess = (e) => resolve(e.target.result ? e.target.result.file : null); }); },
deleteSourceImage() { return this.perform('sourceImage', 'readwrite', (store, resolve) => { store.delete('user-image').onsuccess = resolve; }); },
};
const IMAGE_GENERATOR_API_URL = "https://prithivmlmods-photo-mate-i2i.hf.space/gradio_api/";
const FN_INDEX = 2;
const dom = {
uploadArea: document.getElementById('upload-area'), fileInput: document.getElementById('file-input'),
previewImage: document.getElementById('preview-image-main'), removeFileBtn: document.getElementById('remove-file-btn-main'),
submitBtn: document.getElementById('submit-btn'),
resultContainer: document.getElementById('result-container'), errorMessage: document.getElementById('error-message'),
lightbox: document.getElementById('lightbox'), lightboxImg: document.getElementById('lightbox-img'),
lightboxClose: document.getElementById('lightbox-close'), lightboxDownload: document.getElementById('lightbox-download'),
lightboxPrev: document.getElementById('lightbox-prev'), lightboxNext: document.getElementById('lightbox-next'),
settingsBtn: document.getElementById('settings-btn'),
promptPanel: document.getElementById('prompt-panel'),
promptTextarea: document.getElementById('prompt-textarea'),
resetPromptBtn: document.getElementById('reset-prompt-btn'),
};
let uploadedFile = null;
let lightboxImageUrls = [];
let currentLightboxIndex = 0;
const handleFile = async (file) => {
if (!file || !file.type.startsWith('image/')) {
displayError('لطفا یک فایل تصویری انتخاب کنید.');
return;
}
uploadedFile = file;
dom.previewImage.src = URL.createObjectURL(file);
dom.uploadArea.classList.add('has-file');
await idbHelper.saveSourceImage(file);
checkFormState();
clearResult();
};
const resetUploader = async () => {
uploadedFile = null;
dom.fileInput.value = '';
dom.previewImage.src = '';
dom.uploadArea.classList.remove('has-file');
await idbHelper.deleteSourceImage();
checkFormState();
clearResult();
};
const checkFormState = () => {
const isFormReady = !!uploadedFile;
dom.submitBtn.disabled = !isFormReady;
};
const setLoading = (isLoading) => {
const btnSpinner = dom.submitBtn.querySelector('.spinner');
const btnText = document.getElementById('btn-text');
const btnIcon = dom.submitBtn.querySelector('svg');
if (isLoading) {
clearResult();
dom.resultContainer.classList.remove('has-content');
dom.resultContainer.classList.add('loading');
btnSpinner.style.display = 'inline-block';
btnIcon.style.display = 'none';
btnText.textContent = 'در حال بافتن...';
dom.submitBtn.disabled = true;
const loadingText = document.getElementById('loading-status-text');
if (loadingText) {
loadingText.textContent = "در حال ارسال به سرور...";
}
} else {
dom.resultContainer.classList.remove('loading');
btnSpinner.style.display = 'none';
btnIcon.style.display = 'inline-block';
btnText.textContent = 'شروع ساخت';
checkFormState();
}
};
const displayResult = (imageUrl) => {
const resultGrid = document.getElementById('result-images-grid');
if (!resultGrid) return;
resultGrid.innerHTML = '';
const wrapper = document.createElement('div');
wrapper.className = 'result-image-wrapper';
const img = document.createElement('img');
img.src = imageUrl;
img.alt = 'تصویر تبدیل شده به سبک بافتنی';
img.className = 'result-image-display';
img.onclick = (e) => {
openLightbox([e.target.src], 0);
};
wrapper.appendChild(img);
resultGrid.appendChild(wrapper);
dom.resultContainer.classList.add('has-content');
resultGrid.style.display = 'grid';
};
const clearResult = () => { dom.resultContainer.classList.remove('has-content'); dom.resultContainer.innerHTML = LOADING_STATE_HTML; dom.errorMessage.style.display = 'none'; };
const displayError = (message) => {
const lowerCaseMessage = String(message).toLowerCase();
const quotaErrorKeywords = ["exceeded", "quota", "capacity", "queue_full", "خطا در یافتن تصویر خروجی", "صف سرور پر است"];
clearResult();
dom.resultContainer.classList.add('has-content');
if (quotaErrorKeywords.some(keyword => lowerCaseMessage.includes(keyword.toLowerCase()))) {
dom.resultContainer.innerHTML = GPU_QUOTA_ERROR_HTML;
dom.resultContainer.querySelector('.back-button').addEventListener('click', clearResult);
dom.resultContainer.querySelector('.retry-button').addEventListener('click', handleGenerationRequest);
dom.resultContainer.querySelector('#tutorialLinkBtn').addEventListener('click', () => { window.parent.postMessage({ type: 'NAVIGATE_TO_URL', url: '#/nav/online/news/getSingle/1149635/eyJpdiI6IjhHVGhPQWJwb3E0cjRXbnFWTW5BaUE9PSIsInZhbHVlIjoiS1V0dTdvT21wbXAwSXZaK1RCTG1pVXZqdlFJa1hXV1RKa2FLem9zU3pXMjd5MmlVOGc2YWY0NVdNR3h3Smp1aSIsIm1hYyI6IjY1NTA5ZDYzMjAzMTJhMGQyMWQ4NjA4ZDgyNGZjZDVlY2MyNjdiMjA2NWYzOWRjY2M4ZmVjYWRlMWNlMWQ3ODEiLCJ0YWciOiIifQ==/21135210' }, '*'); });
} else {
dom.resultContainer.innerHTML = `<p id="error-message" style="display: block;"></p>`;
document.getElementById('error-message').textContent = message;
}
};
async function urlToImageFile(url, filename) {
try {
const proxyUrl = 'https://corsproxy.io/?';
const response = await fetch(proxyUrl + encodeURIComponent(url));
if (!response.ok) {
throw new Error(`Failed to fetch image through proxy: ${response.statusText}`);
}
const blob = await response.blob();
const mimeType = blob.type || (url.endsWith('.png') ? 'image/png' : 'image/jpeg');
const finalFilename = filename || `sample-${Date.now()}.${mimeType.split('/')[1] || 'png'}`;
return new File([blob], finalFilename, { type: mimeType });
} catch (error) {
console.error("CORS Proxy fetch failed:", error);
const response = await fetch(url);
const blob = await response.blob();
const mimeType = blob.type || (url.endsWith('.png') ? 'image/png' : 'image/jpeg');
const finalFilename = filename || `sample-${Date.now()}.${mimeType.split('/')[1] || 'png'}`;
return new File([blob], finalFilename, { type: mimeType });
}
}
async function uploadFileToImageGenerator(file) { const formData = new FormData(); formData.append("files", file); const response = await fetch(`${IMAGE_GENERATOR_API_URL}upload`, { method: "POST", body: formData }); if (!response.ok) return null; const result = await response.json(); return { path: result[0], url: `${IMAGE_GENERATOR_API_URL}file=${result[0]}`, orig_name: file.name, size: file.size, mime_type: file.type, meta: { _type: "gradio.FileData" } }; }
function listenForImageResult(sessionHash, onProgress) { return new Promise((resolve, reject) => { const eventSource = new EventSource(`${IMAGE_GENERATOR_API_URL}queue/data?session_hash=${sessionHash}`); eventSource.onmessage = (event) => { const data = JSON.parse(event.data); switch (data.msg) { case "estimation": case "process_starts": onProgress({ status: 'started' }); break; case "progress": if (data.progress_data && data.progress_data.length > 0) { const p = data.progress_data[0]; const pct = Math.round((p.index / p.length) * 100); onProgress({ status: 'progress', percent: pct }); } break; case "process_completed": eventSource.close(); try { const finalImageUrl = data.output.data[0][1].url; resolve(finalImageUrl); } catch (e) { reject(new Error("خطا در یافتن تصویر خروجی در پاسخ سرور.")); } break; case "queue_full": eventSource.close(); reject(new Error("صف سرور پر است. لطفاً کمی بعد دوباره تلاش کنید.")); break; } }; eventSource.onerror = (err) => { eventSource.close(); reject(new Error("خطا در ارتباط با سرور. اتصال قطع شد.")); }; }); }
async function executeSingleGeneration(onProgress, retryCount = 3, delayMs = 1500) {
for (let attempt = 1; attempt <= retryCount; attempt++) {
try {
if (attempt > 1) {
console.log(`Attempt ${attempt} of ${retryCount}...`);
const statusTextEl = document.getElementById('loading-status-text');
if(statusTextEl) statusTextEl.textContent = `اتصال ناپایدار است، تلاش مجدد (${attempt}/${retryCount})...`;
await new Promise(resolve => setTimeout(resolve, delayMs));
}
const finalPrompt = dom.promptTextarea.value || DEFAULT_PROMPT;
const uploadedFileData = await uploadFileToImageGenerator(uploadedFile);
if (!uploadedFileData) throw new Error("آپلود فایل به سرور تصویرساز با شکست مواجه شد.");
const sessionHash = Math.random().toString(36).substring(2, 15);
const payload = {
data: [uploadedFileData, finalPrompt, "PhotoCleanser", false, 0, true, 2.5, 28],
event_data: null,
fn_index: FN_INDEX,
trigger_id: 8,
session_hash: sessionHash
};
const joinResponse = await fetch(`${IMAGE_GENERATOR_API_URL}queue/join?`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
if (!joinResponse.ok) throw new Error("خطا در ارسال درخواست نهایی: " + await joinResponse.text());
return listenForImageResult(sessionHash, onProgress);
} catch (error) {
console.error(`Attempt ${attempt} failed:`, error);
if (attempt === retryCount || !(error instanceof TypeError && error.message.toLowerCase().includes('failed to fetch'))) {
throw error;
}
}
}
}
async function proceedWithGeneration() {
if (!uploadedFile) return;
setLoading(true);
const updateOverallProgress = (progressData, index) => {
if (index > 0) return;
const statusTextEl = document.getElementById('loading-status-text');
const progressBarEl = document.getElementById('loading-progress-bar');
if (!statusTextEl || !progressBarEl) return;
switch (progressData.status) {
case 'started':
statusTextEl.textContent = 'پردازش شروع شد...';
progressBarEl.style.width = '0%';
break;
case 'progress':
progressBarEl.style.width = `${progressData.percent}%`;
statusTextEl.textContent = `در حال تبدیل به بافتنی: ${progressData.percent}%`;
break;
}
};
try {
const imageUrl = await executeSingleGeneration(updateOverallProgress);
displayResult(imageUrl);
} catch (error) {
console.error("Generation failed:", error);
displayError(error.message || 'یک خطای ناشناخته رخ داد.');
} finally {
setLoading(false);
}
}
async function handleGenerationRequest() {
if (dom.submitBtn.disabled && !dom.resultContainer.querySelector('.retry-button')) return;
await proceedWithGeneration();
}
const showLightboxImage = (index) => {
if (index < 0 || index >= lightboxImageUrls.length) return;
currentLightboxIndex = index;
dom.lightboxImg.src = lightboxImageUrls[currentLightboxIndex];
dom.lightboxPrev.style.display = currentLightboxIndex > 0 ? 'flex' : 'none';
dom.lightboxNext.style.display = currentLightboxIndex < lightboxImageUrls.length - 1 ? 'flex' : 'none';
};
const handleDownloadClick = async (e) => {
e.preventDefault();
const imageUrl = dom.lightboxImg.src;
if (!imageUrl) return;
try {
const response = await fetch(imageUrl);
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = `ai-crochet-image-${Date.now()}.png`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
} catch (error) {
console.error('Error fetching image for download:', error);
alert('خطا در آماده‌سازی فایل برای دانلود.');
}
};
const openLightbox = (urls, startIndex = 0) => {
lightboxImageUrls = urls;
currentLightboxIndex = startIndex;
showLightboxImage(currentLightboxIndex);
dom.lightbox.classList.add('visible');
};
const closeLightbox = () => {
dom.lightbox.classList.remove('visible');
};
async function handleSampleClick(event) {
const sampleItem = event.currentTarget;
if (sampleItem.classList.contains('is-loading')) return;
document.querySelectorAll('.sample-item').forEach(el => el.classList.remove('is-loading'));
sampleItem.classList.add('is-loading');
window.scrollTo({ top: 0, behavior: 'smooth' });
try {
const originalImageUrl = sampleItem.querySelector('img').src;
const restoredImageUrl = sampleItem.dataset.restoredSrc;
if (!restoredImageUrl) {
throw new Error('URL تصویر نمونه یافت نشد.');
}
const imageFile = await urlToImageFile(originalImageUrl, 'sample.png');
await handleFile(imageFile);
displayResult(restoredImageUrl);
} catch (error) {
console.error("Error processing sample image:", error);
displayError("خطا در پردازش تصویر نمونه. لطفاً اتصال اینترنت خود را بررسی کرده و دوباره تلاش کنید.");
} finally {
sampleItem.classList.remove('is-loading');
}
}
const loadInitialData = async () => {
await idbHelper.init();
const savedFile = await idbHelper.getSourceImage();
if (savedFile) {
uploadedFile = savedFile;
dom.previewImage.src = URL.createObjectURL(savedFile);
dom.uploadArea.classList.add('has-file');
}
checkFormState();
clearResult();
};
document.addEventListener('DOMContentLoaded', () => {
loadInitialData();
dom.submitBtn.addEventListener('click', handleGenerationRequest);
dom.removeFileBtn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); resetUploader(); });
dom.fileInput.addEventListener('change', (e) => handleFile(e.target.files[0]));
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(e => dom.uploadArea.addEventListener(e, p => { p.preventDefault(); p.stopPropagation(); }));
['dragenter', 'dragover'].forEach(e => dom.uploadArea.addEventListener(e, () => { if (!dom.uploadArea.classList.contains('has-file')) dom.uploadArea.classList.add('drag-over'); }));
['dragleave', 'drop'].forEach(e => dom.uploadArea.addEventListener(e, () => dom.uploadArea.classList.remove('drag-over')));
dom.uploadArea.addEventListener('drop', e => { if (!dom.uploadArea.classList.contains('has-file')) handleFile(e.dataTransfer.files[0]); });
dom.lightboxClose.addEventListener('click', closeLightbox);
dom.lightboxDownload.addEventListener('click', handleDownloadClick);
dom.lightbox.addEventListener('click', (e) => { if (e.target === dom.lightbox) closeLightbox(); });
dom.lightboxPrev.addEventListener('click', () => showLightboxImage(currentLightboxIndex - 1));
dom.lightboxNext.addEventListener('click', () => showLightboxImage(currentLightboxIndex + 1));
document.querySelectorAll('#sample-grid .sample-item').forEach(sampleItem => {
sampleItem.addEventListener('click', handleSampleClick);
});
dom.promptTextarea.value = localStorage.getItem(PROMPT_STORAGE_KEY) || DEFAULT_PROMPT;
dom.settingsBtn.addEventListener('click', () => {
dom.promptPanel.classList.toggle('open');
dom.settingsBtn.classList.toggle('open');
});
dom.promptTextarea.addEventListener('input', () => {
localStorage.setItem(PROMPT_STORAGE_KEY, dom.promptTextarea.value);
});
dom.resetPromptBtn.addEventListener('click', () => {
dom.promptTextarea.value = DEFAULT_PROMPT;
localStorage.setItem(PROMPT_STORAGE_KEY, DEFAULT_PROMPT);
});
});
</script>
</body>
</html>