Spaces:
Runtime error
Runtime error
| <html lang="ar" dir="rtl"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>ZYZ Fine-Tuner — The ZYZ Studio</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link href="https://fonts.googleapis.com/css2?family=Tajawal:wght@300;400;500;700;900&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet"> | |
| <style> | |
| /* ═══════════════════════════════════════════════ | |
| TOKENS | |
| ═══════════════════════════════════════════════ */ | |
| :root { | |
| --bg: #07080f; | |
| --surface: #0d0f1c; | |
| --card: #111528; | |
| --border: #1e2340; | |
| --accent: #5b6af7; | |
| --accent2: #a78bfa; | |
| --green: #22d3a0; | |
| --red: #f43f5e; | |
| --yellow: #fbbf24; | |
| --text: #e2e8f0; | |
| --muted: #64748b; | |
| --radius: 14px; | |
| --mono: 'Space Mono', monospace; | |
| --arabic: 'Tajawal', sans-serif; | |
| } | |
| /* ═══════════════════════════════════════════════ | |
| RESET & BASE | |
| ═══════════════════════════════════════════════ */ | |
| *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } | |
| html { scroll-behavior: smooth; } | |
| body { | |
| font-family: var(--arabic); | |
| background: var(--bg); | |
| color: var(--text); | |
| min-height: 100vh; | |
| overflow-x: hidden; | |
| } | |
| /* Grid noise texture */ | |
| body::before { | |
| content: ''; | |
| position: fixed; inset: 0; | |
| background-image: | |
| radial-gradient(ellipse 80% 60% at 70% 10%, rgba(91,106,247,.12) 0%, transparent 60%), | |
| radial-gradient(ellipse 60% 40% at 20% 80%, rgba(167,139,250,.08) 0%, transparent 60%); | |
| pointer-events: none; | |
| z-index: 0; | |
| } | |
| /* ═══════════════════════════════════════════════ | |
| PAGES SYSTEM | |
| ═══════════════════════════════════════════════ */ | |
| .page { | |
| position: fixed; inset: 0; | |
| display: flex; flex-direction: column; | |
| align-items: center; justify-content: center; | |
| padding: 24px; | |
| opacity: 0; pointer-events: none; | |
| transform: translateY(24px); | |
| transition: opacity .45s cubic-bezier(.4,0,.2,1), transform .45s cubic-bezier(.4,0,.2,1); | |
| z-index: 1; | |
| } | |
| .page.active { | |
| opacity: 1; pointer-events: all; transform: none; | |
| } | |
| /* ═══════════════════════════════════════════════ | |
| PAGE 1 — WELCOME | |
| ═══════════════════════════════════════════════ */ | |
| #page-welcome { gap: 32px; } | |
| .logo-ring { | |
| width: 120px; height: 120px; | |
| border-radius: 50%; | |
| border: 2px solid transparent; | |
| background: | |
| linear-gradient(var(--card), var(--card)) padding-box, | |
| linear-gradient(135deg, var(--accent), var(--accent2)) border-box; | |
| display: flex; align-items: center; justify-content: center; | |
| font-size: 48px; | |
| animation: pulse-ring 3s ease-in-out infinite; | |
| position: relative; | |
| } | |
| .logo-ring::after { | |
| content: ''; | |
| position: absolute; inset: -8px; | |
| border-radius: 50%; | |
| border: 1px solid rgba(91,106,247,.2); | |
| animation: pulse-ring 3s ease-in-out infinite .5s; | |
| } | |
| @keyframes pulse-ring { | |
| 0%,100% { box-shadow: 0 0 0 0 rgba(91,106,247,.3); } | |
| 50% { box-shadow: 0 0 0 20px rgba(91,106,247,.0); } | |
| } | |
| .welcome-title { | |
| font-size: clamp(2rem, 5vw, 3.5rem); | |
| font-weight: 900; | |
| text-align: center; | |
| line-height: 1.1; | |
| background: linear-gradient(135deg, #fff 20%, var(--accent2) 70%, var(--accent) 100%); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| } | |
| .welcome-sub { | |
| color: var(--muted); | |
| font-size: 1.05rem; | |
| text-align: center; | |
| max-width: 420px; | |
| line-height: 1.7; | |
| } | |
| .btn-start { | |
| margin-top: 8px; | |
| padding: 18px 64px; | |
| font-family: var(--arabic); | |
| font-size: 1.2rem; | |
| font-weight: 700; | |
| color: #fff; | |
| background: linear-gradient(135deg, var(--accent), var(--accent2)); | |
| border: none; border-radius: 50px; | |
| cursor: pointer; | |
| position: relative; | |
| overflow: hidden; | |
| transition: transform .2s, box-shadow .2s; | |
| box-shadow: 0 0 40px rgba(91,106,247,.4); | |
| } | |
| .btn-start::before { | |
| content: ''; | |
| position: absolute; inset: 0; | |
| background: linear-gradient(135deg, rgba(255,255,255,.15), transparent); | |
| opacity: 0; transition: opacity .2s; | |
| } | |
| .btn-start:hover { transform: translateY(-3px); box-shadow: 0 0 60px rgba(91,106,247,.6); } | |
| .btn-start:hover::before { opacity: 1; } | |
| .btn-start:active { transform: translateY(0); } | |
| .badge { | |
| font-family: var(--mono); | |
| font-size: .7rem; | |
| color: var(--accent); | |
| border: 1px solid var(--border); | |
| padding: 4px 12px; | |
| border-radius: 20px; | |
| letter-spacing: .05em; | |
| } | |
| /* ═══════════════════════════════════════════════ | |
| PAGE 2 — MODEL SELECT | |
| ═══════════════════════════════════════════════ */ | |
| #page-select { gap: 28px; max-width: 700px; width: 100%; } | |
| .page-title { | |
| font-size: 1.6rem; | |
| font-weight: 700; | |
| text-align: center; | |
| } | |
| .page-title span { color: var(--accent2); } | |
| .models-grid { | |
| width: 100%; | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); | |
| gap: 16px; | |
| } | |
| .model-card { | |
| background: var(--card); | |
| border: 1.5px solid var(--border); | |
| border-radius: var(--radius); | |
| padding: 20px 16px; | |
| cursor: pointer; | |
| transition: border-color .2s, transform .2s, box-shadow .2s; | |
| text-align: center; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .model-card::before { | |
| content: ''; | |
| position: absolute; inset: 0; | |
| background: linear-gradient(135deg, rgba(91,106,247,.08), transparent); | |
| opacity: 0; transition: opacity .2s; | |
| } | |
| .model-card:hover { border-color: var(--accent); transform: translateY(-4px); box-shadow: 0 12px 40px rgba(91,106,247,.25); } | |
| .model-card:hover::before { opacity: 1; } | |
| .model-card.selected { border-color: var(--accent2); box-shadow: 0 0 0 2px rgba(167,139,250,.3); } | |
| .model-card.selected::before { opacity: 1; } | |
| .model-icon { font-size: 2.2rem; margin-bottom: 10px; } | |
| .model-name { font-size: 1rem; font-weight: 700; margin-bottom: 4px; } | |
| .model-desc { font-size: .78rem; color: var(--muted); line-height: 1.5; margin-bottom: 8px; } | |
| .model-tags { display: flex; gap: 6px; flex-wrap: wrap; justify-content: center; } | |
| .model-tag { | |
| font-family: var(--mono); font-size: .65rem; | |
| padding: 2px 8px; border-radius: 20px; | |
| background: rgba(91,106,247,.15); color: var(--accent2); | |
| border: 1px solid rgba(91,106,247,.2); | |
| } | |
| .btn-confirm { | |
| padding: 14px 48px; | |
| font-family: var(--arabic); font-size: 1rem; font-weight: 700; | |
| color: #fff; | |
| background: linear-gradient(135deg, var(--accent), var(--accent2)); | |
| border: none; border-radius: 50px; cursor: pointer; | |
| transition: transform .2s, opacity .2s; | |
| opacity: .4; pointer-events: none; | |
| } | |
| .btn-confirm.enabled { opacity: 1; pointer-events: all; } | |
| .btn-confirm.enabled:hover { transform: translateY(-2px); } | |
| /* ═══════════════════════════════════════════════ | |
| PAGE 3 — DOWNLOAD | |
| ═══════════════════════════════════════════════ */ | |
| #page-download { gap: 28px; max-width: 540px; width: 100%; } | |
| .status-card { | |
| width: 100%; | |
| background: var(--card); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius); | |
| padding: 28px 24px; | |
| display: flex; flex-direction: column; gap: 20px; | |
| } | |
| .status-header { | |
| display: flex; align-items: center; gap: 12px; | |
| } | |
| .status-icon { | |
| width: 40px; height: 40px; border-radius: 50%; | |
| background: rgba(91,106,247,.15); | |
| display: flex; align-items: center; justify-content: center; | |
| font-size: 1.2rem; flex-shrink: 0; | |
| } | |
| .status-label { font-size: .8rem; color: var(--muted); } | |
| .status-value { font-size: 1rem; font-weight: 600; } | |
| /* Progress bar */ | |
| .progress-wrap { | |
| width: 100%; | |
| background: var(--border); | |
| border-radius: 999px; | |
| height: 8px; | |
| overflow: hidden; | |
| position: relative; | |
| } | |
| .progress-fill { | |
| height: 100%; | |
| border-radius: 999px; | |
| background: linear-gradient(90deg, var(--accent), var(--accent2)); | |
| transition: width .4s cubic-bezier(.4,0,.2,1); | |
| position: relative; | |
| } | |
| .progress-fill::after { | |
| content: ''; | |
| position: absolute; top: 0; right: 0; bottom: 0; | |
| width: 40px; | |
| background: linear-gradient(90deg, transparent, rgba(255,255,255,.4)); | |
| animation: shimmer 1.5s infinite; | |
| } | |
| @keyframes shimmer { | |
| 0% { transform: translateX(40px); opacity: 0; } | |
| 50% { opacity: 1; } | |
| 100% { transform: translateX(-200px); opacity: 0; } | |
| } | |
| .progress-nums { | |
| display: flex; justify-content: space-between; | |
| font-family: var(--mono); font-size: .8rem; color: var(--muted); | |
| } | |
| .progress-pct { color: var(--accent2); font-weight: 700; font-size: 1rem; } | |
| .stats-row { | |
| display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px; | |
| } | |
| .stat-box { | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-radius: 10px; | |
| padding: 12px; | |
| text-align: center; | |
| } | |
| .stat-label { font-size: .7rem; color: var(--muted); margin-bottom: 4px; } | |
| .stat-val { font-family: var(--mono); font-size: .95rem; font-weight: 700; color: var(--accent2); } | |
| /* ═══════════════════════════════════════════════ | |
| PAGE 4 — TRAIN DATA | |
| ═══════════════════════════════════════════════ */ | |
| #page-train-data { gap: 24px; max-width: 680px; width: 100%; } | |
| .dataset-textarea { | |
| width: 100%; | |
| height: 260px; | |
| background: var(--card); | |
| border: 1.5px solid var(--border); | |
| border-radius: var(--radius); | |
| color: var(--text); | |
| font-family: var(--mono); | |
| font-size: .82rem; | |
| padding: 16px; | |
| resize: vertical; | |
| transition: border-color .2s; | |
| line-height: 1.6; | |
| direction: ltr; | |
| } | |
| .dataset-textarea:focus { outline: none; border-color: var(--accent); } | |
| .hint-box { | |
| background: rgba(91,106,247,.08); | |
| border: 1px solid rgba(91,106,247,.2); | |
| border-radius: 10px; | |
| padding: 12px 16px; | |
| font-size: .82rem; | |
| color: var(--muted); | |
| line-height: 1.6; | |
| } | |
| .hint-box b { color: var(--accent2); } | |
| .actions-row { display: flex; gap: 12px; width: 100%; } | |
| .btn-secondary { | |
| flex: 1; padding: 14px; | |
| font-family: var(--arabic); font-size: .95rem; font-weight: 600; | |
| color: var(--accent2); | |
| background: transparent; | |
| border: 1.5px solid var(--border); border-radius: 50px; | |
| cursor: pointer; transition: border-color .2s, background .2s; | |
| } | |
| .btn-secondary:hover { border-color: var(--accent2); background: rgba(167,139,250,.08); } | |
| .btn-primary { | |
| flex: 2; padding: 14px; | |
| font-family: var(--arabic); font-size: .95rem; font-weight: 700; | |
| color: #fff; | |
| background: linear-gradient(135deg, var(--green), #059669); | |
| border: none; border-radius: 50px; cursor: pointer; | |
| transition: transform .2s, box-shadow .2s; | |
| box-shadow: 0 0 30px rgba(34,211,160,.3); | |
| } | |
| .btn-primary:hover { transform: translateY(-2px); box-shadow: 0 0 50px rgba(34,211,160,.5); } | |
| /* ═══════════════════════════════════════════════ | |
| PAGE 5 — TRAINING PROGRESS | |
| ═══════════════════════════════════════════════ */ | |
| #page-training { gap: 24px; max-width: 540px; width: 100%; } | |
| .loss-row { | |
| display: flex; align-items: center; justify-content: center; gap: 8px; | |
| font-family: var(--mono); font-size: .85rem; color: var(--muted); | |
| } | |
| .loss-val { color: var(--yellow); font-weight: 700; font-size: 1.1rem; } | |
| .log-box { | |
| width: 100%; | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius); | |
| padding: 14px 16px; | |
| font-family: var(--mono); | |
| font-size: .75rem; | |
| color: var(--muted); | |
| max-height: 100px; | |
| overflow-y: auto; | |
| direction: ltr; | |
| text-align: left; | |
| } | |
| .log-line { padding: 1px 0; } | |
| .log-line.ok { color: var(--green); } | |
| .log-line.warn { color: var(--yellow); } | |
| /* ═══════════════════════════════════════════════ | |
| PAGE 6 — DONE | |
| ═══════════════════════════════════════════════ */ | |
| #page-done { gap: 28px; max-width: 560px; width: 100%; } | |
| .done-circle { | |
| width: 100px; height: 100px; border-radius: 50%; | |
| background: rgba(34,211,160,.1); | |
| border: 2px solid var(--green); | |
| display: flex; align-items: center; justify-content: center; | |
| font-size: 3rem; | |
| animation: bounce-in .6s cubic-bezier(.34,1.56,.64,1); | |
| } | |
| @keyframes bounce-in { | |
| 0% { transform: scale(0); opacity: 0; } | |
| 100% { transform: scale(1); opacity: 1; } | |
| } | |
| .done-title { | |
| font-size: 2rem; font-weight: 900; text-align: center; | |
| color: var(--green); | |
| } | |
| .done-sub { color: var(--muted); text-align: center; font-size: .95rem; } | |
| .done-btns { display: flex; flex-direction: column; gap: 12px; width: 100%; } | |
| .btn-green { | |
| padding: 16px; | |
| font-family: var(--arabic); font-size: 1rem; font-weight: 700; | |
| color: #fff; | |
| background: linear-gradient(135deg, var(--green), #059669); | |
| border: none; border-radius: 50px; cursor: pointer; | |
| transition: transform .2s; text-align: center; | |
| } | |
| .btn-green:hover { transform: translateY(-2px); } | |
| .btn-outline { | |
| padding: 16px; | |
| font-family: var(--arabic); font-size: 1rem; font-weight: 600; | |
| color: var(--text); | |
| background: transparent; | |
| border: 1.5px solid var(--border); border-radius: 50px; cursor: pointer; | |
| transition: border-color .2s, background .2s; | |
| } | |
| .btn-outline:hover { border-color: var(--accent); background: rgba(91,106,247,.08); } | |
| /* ═══════════════════════════════════════════════ | |
| PAGE 7 — TEST | |
| ═══════════════════════════════════════════════ */ | |
| #page-test { gap: 20px; max-width: 640px; width: 100%; } | |
| .chat-box { | |
| width: 100%; | |
| background: var(--card); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius); | |
| min-height: 220px; | |
| max-height: 320px; | |
| overflow-y: auto; | |
| padding: 16px; | |
| display: flex; flex-direction: column; gap: 12px; | |
| } | |
| .msg { | |
| padding: 10px 14px; | |
| border-radius: 12px; | |
| font-size: .9rem; | |
| line-height: 1.6; | |
| max-width: 85%; | |
| } | |
| .msg.user { background: rgba(91,106,247,.2); border: 1px solid rgba(91,106,247,.3); align-self: flex-end; text-align: right; } | |
| .msg.assistant { background: var(--surface); border: 1px solid var(--border); align-self: flex-start; } | |
| .input-row { | |
| width: 100%; display: flex; gap: 10px; | |
| } | |
| .test-input { | |
| flex: 1; padding: 14px 16px; | |
| background: var(--card); | |
| border: 1.5px solid var(--border); | |
| border-radius: 50px; color: var(--text); | |
| font-family: var(--arabic); font-size: .95rem; | |
| transition: border-color .2s; | |
| } | |
| .test-input:focus { outline: none; border-color: var(--accent); } | |
| .btn-send { | |
| padding: 14px 24px; | |
| font-family: var(--arabic); font-size: .95rem; font-weight: 700; | |
| color: #fff; | |
| background: linear-gradient(135deg, var(--accent), var(--accent2)); | |
| border: none; border-radius: 50px; cursor: pointer; | |
| transition: transform .2s; | |
| } | |
| .btn-send:hover { transform: scale(1.05); } | |
| .btn-send:disabled { opacity: .5; pointer-events: none; } | |
| /* ═══════════════════════════════════════════════ | |
| NAV BAR (top) | |
| ═══════════════════════════════════════════════ */ | |
| .topbar { | |
| position: fixed; top: 0; left: 0; right: 0; | |
| height: 52px; | |
| display: flex; align-items: center; | |
| justify-content: space-between; | |
| padding: 0 20px; | |
| background: rgba(7,8,15,.8); | |
| backdrop-filter: blur(12px); | |
| border-bottom: 1px solid var(--border); | |
| z-index: 100; | |
| } | |
| .topbar-logo { | |
| font-family: var(--mono); font-size: .85rem; | |
| color: var(--accent2); | |
| letter-spacing: .05em; | |
| } | |
| .topbar-steps { display: flex; gap: 6px; } | |
| .step-dot { | |
| width: 7px; height: 7px; border-radius: 50%; | |
| background: var(--border); | |
| transition: background .3s, transform .3s; | |
| } | |
| .step-dot.done { background: var(--green); } | |
| .step-dot.active { background: var(--accent2); transform: scale(1.4); } | |
| /* ═══════════════════════════════════════════════ | |
| ERROR | |
| ═══════════════════════════════════════════════ */ | |
| .error-box { | |
| background: rgba(244,63,94,.1); | |
| border: 1px solid rgba(244,63,94,.3); | |
| border-radius: 10px; | |
| padding: 14px 16px; | |
| font-size: .85rem; | |
| color: var(--red); | |
| direction: ltr; | |
| text-align: left; | |
| } | |
| /* Scrollbar */ | |
| ::-webkit-scrollbar { width: 5px; height: 5px; } | |
| ::-webkit-scrollbar-track { background: transparent; } | |
| ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 99px; } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- TOP BAR --> | |
| <div class="topbar"> | |
| <span class="topbar-logo">⚡ ZYZ FINE-TUNER</span> | |
| <div class="topbar-steps" id="topbar-steps"> | |
| <div class="step-dot active" title="البداية"></div> | |
| <div class="step-dot" title="اختيار النموذج"></div> | |
| <div class="step-dot" title="التحميل"></div> | |
| <div class="step-dot" title="البيانات"></div> | |
| <div class="step-dot" title="التدريب"></div> | |
| <div class="step-dot" title="النتيجة"></div> | |
| </div> | |
| </div> | |
| <!-- ══════════════════════════════════════ | |
| PAGE 1: WELCOME | |
| ══════════════════════════════════════ --> | |
| <div class="page active" id="page-welcome"> | |
| <div class="logo-ring">🧠</div> | |
| <h1 class="welcome-title">ZYZ Fine-Tuner</h1> | |
| <p class="welcome-sub"> | |
| منصة تدريب نماذج الذكاء الاصطناعي بأسلوب بسيط وسريع.<br> | |
| اختر النموذج، أدخل بياناتك، وابدأ التدريب في دقائق. | |
| </p> | |
| <button class="btn-start" onclick="goTo('select')">🚀 ابدأ الآن</button> | |
| <span class="badge">LoRA Fine-tuning • QLoRA 4-bit • The ZYZ Studio</span> | |
| </div> | |
| <!-- ══════════════════════════════════════ | |
| PAGE 2: MODEL SELECT | |
| ══════════════════════════════════════ --> | |
| <div class="page" id="page-select"> | |
| <h2 class="page-title">اختر <span>النموذج</span></h2> | |
| <div class="models-grid" id="models-grid"> | |
| <!-- injected by JS --> | |
| </div> | |
| <button class="btn-confirm" id="btn-confirm-model" onclick="confirmModel()"> | |
| ✅ تأكيد الاختيار | |
| </button> | |
| </div> | |
| <!-- ══════════════════════════════════════ | |
| PAGE 3: DOWNLOAD | |
| ══════════════════════════════════════ --> | |
| <div class="page" id="page-download"> | |
| <h2 class="page-title">تحميل <span>النموذج</span></h2> | |
| <div class="status-card"> | |
| <div class="status-header"> | |
| <div class="status-icon">⬇️</div> | |
| <div> | |
| <div class="status-label">الحالة</div> | |
| <div class="status-value" id="dl-status">جاري الاتصال...</div> | |
| </div> | |
| </div> | |
| <div> | |
| <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px"> | |
| <span style="font-size:.85rem;color:var(--muted)">التقدم</span> | |
| <span class="progress-pct" id="dl-pct">0%</span> | |
| </div> | |
| <div class="progress-wrap"> | |
| <div class="progress-fill" id="dl-bar" style="width:0%"></div> | |
| </div> | |
| </div> | |
| <div class="stats-row"> | |
| <div class="stat-box"> | |
| <div class="stat-label">الوقت المنقضي</div> | |
| <div class="stat-val" id="dl-elapsed">0s</div> | |
| </div> | |
| <div class="stat-box"> | |
| <div class="stat-label">الوقت المتبقي</div> | |
| <div class="stat-val" id="dl-eta">—</div> | |
| </div> | |
| <div class="stat-box"> | |
| <div class="stat-label">المرحلة</div> | |
| <div class="stat-val" id="dl-phase">—</div> | |
| </div> | |
| </div> | |
| <div id="dl-error" class="error-box" style="display:none"></div> | |
| </div> | |
| </div> | |
| <!-- ══════════════════════════════════════ | |
| PAGE 4: TRAIN DATA | |
| ══════════════════════════════════════ --> | |
| <div class="page" id="page-train-data"> | |
| <h2 class="page-title">بيانات <span>التدريب</span></h2> | |
| <div class="hint-box"> | |
| 📌 أدخل بياناتك بصيغة <b>JSON</b> — مصفوفة من المحادثات. | |
| كل محادثة تحتوي على <b>messages</b> بأدوار <b>user</b> و <b>assistant</b>. | |
| </div> | |
| <textarea class="dataset-textarea" id="dataset-input" placeholder='[ | |
| { | |
| "messages": [ | |
| {"role": "user", "content": "سؤالك هنا"}, | |
| {"role": "assistant", "content": "الجواب هنا"} | |
| ] | |
| } | |
| ]'></textarea> | |
| <div class="actions-row"> | |
| <button class="btn-secondary" onclick="loadExample()">📋 مثال جاهز</button> | |
| <button class="btn-primary" onclick="startTraining()">⚡ ابدأ التدريب</button> | |
| </div> | |
| <div id="data-error" class="error-box" style="display:none"></div> | |
| </div> | |
| <!-- ══════════════════════════════════════ | |
| PAGE 5: TRAINING PROGRESS | |
| ══════════════════════════════════════ --> | |
| <div class="page" id="page-training"> | |
| <h2 class="page-title">جاري <span>التدريب</span></h2> | |
| <div class="status-card"> | |
| <div class="status-header"> | |
| <div class="status-icon">🔥</div> | |
| <div> | |
| <div class="status-label">الحالة</div> | |
| <div class="status-value" id="tr-status">جاري التدريب...</div> | |
| </div> | |
| </div> | |
| <div> | |
| <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px"> | |
| <span style="font-size:.85rem;color:var(--muted)">التقدم</span> | |
| <span class="progress-pct" id="tr-pct">0%</span> | |
| </div> | |
| <div class="progress-wrap"> | |
| <div class="progress-fill" id="tr-bar" style="width:0%;background:linear-gradient(90deg,var(--green),var(--accent))"></div> | |
| </div> | |
| </div> | |
| <div class="loss-row"> | |
| <span>الخسارة (Loss):</span> | |
| <span class="loss-val" id="tr-loss">—</span> | |
| </div> | |
| <div class="stats-row"> | |
| <div class="stat-box"> | |
| <div class="stat-label">الخطوة</div> | |
| <div class="stat-val" id="tr-step">0/0</div> | |
| </div> | |
| <div class="stat-box"> | |
| <div class="stat-label">الوقت المنقضي</div> | |
| <div class="stat-val" id="tr-elapsed">0s</div> | |
| </div> | |
| <div class="stat-box"> | |
| <div class="stat-label">الوقت المتبقي</div> | |
| <div class="stat-val" id="tr-eta">—</div> | |
| </div> | |
| </div> | |
| <div class="log-box" id="tr-log"></div> | |
| <div id="tr-error" class="error-box" style="display:none"></div> | |
| </div> | |
| </div> | |
| <!-- ══════════════════════════════════════ | |
| PAGE 6: DONE | |
| ══════════════════════════════════════ --> | |
| <div class="page" id="page-done"> | |
| <div class="done-circle">✅</div> | |
| <h2 class="done-title">اكتمل التدريب!</h2> | |
| <p class="done-sub">تم تدريب النموذج بنجاح باستخدام بياناتك. يمكنك الآن تجربته أو تحميله.</p> | |
| <div class="done-btns"> | |
| <button class="btn-green" onclick="goTo('test')">🧪 جرّب النموذج الآن</button> | |
| <button class="btn-outline" onclick="saveModel()">💾 تحميل النموذج</button> | |
| <button class="btn-outline" onclick="resetAll()">🔄 تدريب نموذج جديد</button> | |
| </div> | |
| <div id="save-msg" style="font-size:.85rem;color:var(--green);text-align:center;display:none"></div> | |
| </div> | |
| <!-- ══════════════════════════════════════ | |
| PAGE 7: TEST | |
| ══════════════════════════════════════ --> | |
| <div class="page" id="page-test"> | |
| <h2 class="page-title">🧪 <span>جرّب النموذج</span></h2> | |
| <div class="chat-box" id="chat-box"> | |
| <div class="msg assistant">مرحباً! أنا النموذج المُدرَّب. اسألني أي شيء 👋</div> | |
| </div> | |
| <div class="input-row"> | |
| <input class="test-input" id="test-input" placeholder="اكتب سؤالاً هنا..." onkeydown="if(event.key==='Enter')sendMsg()"> | |
| <button class="btn-send" id="btn-send" onclick="sendMsg()">إرسال</button> | |
| </div> | |
| <button class="btn-outline" onclick="goTo('done')" style="width:100%;margin-top:4px">← رجوع</button> | |
| </div> | |
| <!-- ══════════════════════════════════════ | |
| SCRIPT | |
| ══════════════════════════════════════ --> | |
| <script> | |
| // ── State ────────────────────────────────────────── | |
| const PAGES = ['welcome','select','download','train-data','training','done','test']; | |
| let currentPage = 'welcome'; | |
| let selectedModel = null; | |
| let pollTimer = null; | |
| // ── Navigation ──────────────────────────────────── | |
| function goTo(page) { | |
| document.getElementById('page-' + currentPage).classList.remove('active'); | |
| document.getElementById('page-' + page).classList.add('active'); | |
| currentPage = page; | |
| updateStepDots(); | |
| } | |
| function updateStepDots() { | |
| const map = { welcome:0, select:1, download:2, 'train-data':3, training:4, done:5, test:5 }; | |
| const idx = map[currentPage] ?? 0; | |
| const dots = document.querySelectorAll('.step-dot'); | |
| dots.forEach((d, i) => { | |
| d.classList.remove('active','done'); | |
| if (i < idx) d.classList.add('done'); | |
| else if (i === idx) d.classList.add('active'); | |
| }); | |
| } | |
| // ── Models ──────────────────────────────────────── | |
| const ICONS = ['💎','🔷','⚡']; | |
| async function loadModels() { | |
| try { | |
| const r = await fetch('/api/models'); | |
| const models = await r.json(); | |
| const grid = document.getElementById('models-grid'); | |
| grid.innerHTML = models.map((m, i) => ` | |
| <div class="model-card" id="mc-${i}" onclick="selectModel(${i}, '${m.id}', '${m.name}')"> | |
| <div class="model-icon">${ICONS[i] || '🤖'}</div> | |
| <div class="model-name">${m.name}</div> | |
| <div class="model-desc">${m.desc}</div> | |
| <div class="model-tags"> | |
| <span class="model-tag">${m.size}</span> | |
| <span class="model-tag">${m.ram}</span> | |
| </div> | |
| </div> | |
| `).join(''); | |
| } catch(e) { | |
| console.error(e); | |
| } | |
| } | |
| function selectModel(idx, id, name) { | |
| selectedModel = { id, name }; | |
| document.querySelectorAll('.model-card').forEach(c => c.classList.remove('selected')); | |
| document.getElementById('mc-' + idx).classList.add('selected'); | |
| const btn = document.getElementById('btn-confirm-model'); | |
| btn.classList.add('enabled'); | |
| btn.textContent = `✅ تأكيد: ${name}`; | |
| } | |
| async function confirmModel() { | |
| if (!selectedModel) return; | |
| goTo('download'); | |
| startDownload(); | |
| } | |
| // ── Download ───────────────────────────────────── | |
| async function startDownload() { | |
| try { | |
| await fetch('/api/download', { | |
| method: 'POST', | |
| headers: {'Content-Type':'application/json'}, | |
| body: JSON.stringify({ model_id: selectedModel.id }) | |
| }); | |
| pollTimer = setInterval(pollDownload, 800); | |
| } catch(e) { | |
| showDlError('فشل الاتصال بالخادم: ' + e.message); | |
| } | |
| } | |
| async function pollDownload() { | |
| try { | |
| const r = await fetch('/api/state'); | |
| const s = await r.json(); | |
| document.getElementById('dl-status').textContent = s.message || '...'; | |
| document.getElementById('dl-pct').textContent = s.progress + '%'; | |
| document.getElementById('dl-bar').style.width = s.progress + '%'; | |
| document.getElementById('dl-elapsed').textContent = fmtTime(s.elapsed); | |
| document.getElementById('dl-eta').textContent = s.eta ? fmtTime(s.eta) : '—'; | |
| document.getElementById('dl-phase').textContent = s.phase; | |
| if (s.phase === 'ready') { | |
| clearInterval(pollTimer); | |
| setTimeout(() => goTo('train-data'), 800); | |
| } else if (s.phase === 'error') { | |
| clearInterval(pollTimer); | |
| showDlError(s.error || 'حدث خطأ غير معروف'); | |
| } | |
| } catch(e) { | |
| // ignore transient errors | |
| } | |
| } | |
| function showDlError(msg) { | |
| const el = document.getElementById('dl-error'); | |
| el.textContent = '❌ ' + msg; | |
| el.style.display = 'block'; | |
| } | |
| // ── Dataset ─────────────────────────────────────── | |
| function loadExample() { | |
| const example = [ | |
| { messages: [ | |
| { role: "user", content: "ما هو 1 + 1؟" }, | |
| { role: "assistant", content: "2" } | |
| ]}, | |
| { messages: [ | |
| { role: "user", content: "احسب 1+1" }, | |
| { role: "assistant", content: "الناتج هو 2" } | |
| ]}, | |
| { messages: [ | |
| { role: "user", content: "واحد زائد واحد يساوي؟" }, | |
| { role: "assistant", content: "يساوي 2" } | |
| ]}, | |
| { messages: [ | |
| { role: "user", content: "كم 1+1؟" }, | |
| { role: "assistant", content: "2" } | |
| ]}, | |
| { messages: [ | |
| { role: "user", content: "جمع 1 و 1 كم يصبح؟" }, | |
| { role: "assistant", content: "يصبح 2" } | |
| ]} | |
| ]; | |
| document.getElementById('dataset-input').value = JSON.stringify(example, null, 2); | |
| } | |
| async function startTraining() { | |
| const raw = document.getElementById('dataset-input').value.trim(); | |
| const errEl = document.getElementById('data-error'); | |
| errEl.style.display = 'none'; | |
| if (!raw) { | |
| errEl.textContent = '⚠️ الحقل فارغ. أدخل بيانات التدريب أولاً.'; | |
| errEl.style.display = 'block'; return; | |
| } | |
| let dataset; | |
| try { dataset = JSON.parse(raw); } catch(e) { | |
| errEl.textContent = '❌ خطأ في صيغة JSON: ' + e.message; | |
| errEl.style.display = 'block'; return; | |
| } | |
| if (!Array.isArray(dataset) || dataset.length === 0) { | |
| errEl.textContent = '⚠️ البيانات يجب أن تكون مصفوفة غير فارغة.'; | |
| errEl.style.display = 'block'; return; | |
| } | |
| try { | |
| const r = await fetch('/api/train', { | |
| method: 'POST', | |
| headers: {'Content-Type':'application/json'}, | |
| body: JSON.stringify({ dataset }) | |
| }); | |
| const res = await r.json(); | |
| if (!res.ok) { | |
| errEl.textContent = '❌ ' + res.msg; | |
| errEl.style.display = 'block'; return; | |
| } | |
| goTo('training'); | |
| pollTimer = setInterval(pollTraining, 800); | |
| } catch(e) { | |
| errEl.textContent = '❌ فشل الاتصال: ' + e.message; | |
| errEl.style.display = 'block'; | |
| } | |
| } | |
| // ── Training poll ───────────────────────────────── | |
| const logLines = []; | |
| async function pollTraining() { | |
| try { | |
| const r = await fetch('/api/state'); | |
| const s = await r.json(); | |
| document.getElementById('tr-status').textContent = s.message || '...'; | |
| document.getElementById('tr-pct').textContent = s.progress + '%'; | |
| document.getElementById('tr-bar').style.width = s.progress + '%'; | |
| document.getElementById('tr-elapsed').textContent = fmtTime(s.elapsed); | |
| document.getElementById('tr-eta').textContent = s.eta ? fmtTime(s.eta) : '—'; | |
| document.getElementById('tr-step').textContent = | |
| s.total_steps ? `${s.current_step}/${s.total_steps}` : '—'; | |
| document.getElementById('tr-loss').textContent = | |
| s.loss !== null && s.loss !== undefined ? s.loss : '—'; | |
| if (s.message && !logLines.includes(s.message)) { | |
| logLines.push(s.message); | |
| const lb = document.getElementById('tr-log'); | |
| const d = document.createElement('div'); | |
| d.className = 'log-line' + (s.message.startsWith('✅') ? ' ok' : ''); | |
| d.textContent = `> ${s.message}`; | |
| lb.appendChild(d); | |
| lb.scrollTop = lb.scrollHeight; | |
| } | |
| if (s.phase === 'done') { | |
| clearInterval(pollTimer); | |
| setTimeout(() => goTo('done'), 1000); | |
| } else if (s.phase === 'error') { | |
| clearInterval(pollTimer); | |
| const errEl = document.getElementById('tr-error'); | |
| errEl.textContent = '❌ ' + (s.error || 'حدث خطأ'); | |
| errEl.style.display = 'block'; | |
| } | |
| } catch(e) {} | |
| } | |
| // ── Test ────────────────────────────────────────── | |
| async function sendMsg() { | |
| const input = document.getElementById('test-input'); | |
| const prompt = input.value.trim(); | |
| if (!prompt) return; | |
| const box = document.getElementById('chat-box'); | |
| addMsg(box, prompt, 'user'); | |
| input.value = ''; | |
| const btn = document.getElementById('btn-send'); | |
| btn.disabled = true; | |
| const thinkingEl = document.createElement('div'); | |
| thinkingEl.className = 'msg assistant'; | |
| thinkingEl.textContent = '⏳ جاري التفكير...'; | |
| box.appendChild(thinkingEl); | |
| box.scrollTop = box.scrollHeight; | |
| try { | |
| const r = await fetch('/api/infer', { | |
| method: 'POST', | |
| headers: {'Content-Type':'application/json'}, | |
| body: JSON.stringify({ prompt }) | |
| }); | |
| const res = await r.json(); | |
| thinkingEl.remove(); | |
| addMsg(box, res.ok ? res.response : ('❌ ' + res.msg), 'assistant'); | |
| } catch(e) { | |
| thinkingEl.remove(); | |
| addMsg(box, '❌ خطأ في الاتصال', 'assistant'); | |
| } | |
| btn.disabled = false; | |
| box.scrollTop = box.scrollHeight; | |
| } | |
| function addMsg(box, text, role) { | |
| const d = document.createElement('div'); | |
| d.className = 'msg ' + role; | |
| d.textContent = text; | |
| box.appendChild(d); | |
| } | |
| // ── Save ────────────────────────────────────────── | |
| async function saveModel() { | |
| const msg = document.getElementById('save-msg'); | |
| msg.style.display = 'block'; | |
| msg.textContent = '💾 جاري الحفظ...'; | |
| try { | |
| const r = await fetch('/api/save', { method: 'POST' }); | |
| const res = await r.json(); | |
| msg.textContent = res.ok | |
| ? `✅ تم الحفظ في: ${res.path}` | |
| : `❌ ${res.msg}`; | |
| } catch(e) { | |
| msg.textContent = '❌ خطأ في الحفظ'; | |
| } | |
| } | |
| // ── Reset ───────────────────────────────────────── | |
| async function resetAll() { | |
| await fetch('/api/reset', { method: 'POST' }); | |
| selectedModel = null; | |
| document.getElementById('btn-confirm-model').classList.remove('enabled'); | |
| document.getElementById('btn-confirm-model').textContent = '✅ تأكيد الاختيار'; | |
| document.querySelectorAll('.model-card').forEach(c => c.classList.remove('selected')); | |
| document.getElementById('dataset-input').value = ''; | |
| document.getElementById('chat-box').innerHTML = | |
| '<div class="msg assistant">مرحباً! أنا النموذج المُدرَّب. اسألني أي شيء 👋</div>'; | |
| logLines.length = 0; | |
| document.getElementById('tr-log').innerHTML = ''; | |
| document.getElementById('save-msg').style.display = 'none'; | |
| goTo('welcome'); | |
| } | |
| // ── Utils ────────────────────────────────────────── | |
| function fmtTime(s) { | |
| if (!s) return '0s'; | |
| const m = Math.floor(s / 60); | |
| const sec = s % 60; | |
| return m > 0 ? `${m}m ${sec}s` : `${sec}s`; | |
| } | |
| // ── Init ────────────────────────────────────────── | |
| loadModels(); | |
| </script> | |
| </body> | |
| </html> | |