Zgemma / static /index.html
THEZYZSTUDIO's picture
Create static/index.html
e195e43 verified
Raw
History Blame Contribute Delete
37.4 kB
<!DOCTYPE html>
<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>