653 lines
29 KiB
HTML
653 lines
29 KiB
HTML
|
|
{% extends "base.html" %}
|
||
|
|
{% block title %}LLM Trainer - AUTARCH{% endblock %}
|
||
|
|
|
||
|
|
{% block content %}
|
||
|
|
<div class="page-header" style="display:flex;align-items:center;gap:1rem;flex-wrap:wrap">
|
||
|
|
<div>
|
||
|
|
<h1>LLM Trainer</h1>
|
||
|
|
<p style="margin:0;font-size:0.85rem;color:var(--text-secondary)">
|
||
|
|
Fine-tune language models on the AUTARCH codebase. Generate datasets, train LoRA adapters, convert to GGUF.
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
<a href="{{ url_for('analyze.index') }}" class="btn btn-sm" style="margin-left:auto">← Analyze</a>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Tab Bar -->
|
||
|
|
<div class="tab-bar">
|
||
|
|
<button class="tab active" data-tab-group="trainer" data-tab="dataset" onclick="showTab('trainer','dataset')">Dataset</button>
|
||
|
|
<button class="tab" data-tab-group="trainer" data-tab="training" onclick="showTab('trainer','training')">Training</button>
|
||
|
|
<button class="tab" data-tab-group="trainer" data-tab="convert" onclick="showTab('trainer','convert')">Convert</button>
|
||
|
|
<button class="tab" data-tab-group="trainer" data-tab="evaluate" onclick="showTab('trainer','evaluate')">Evaluate</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- ==================== DATASET TAB ==================== -->
|
||
|
|
<div class="tab-content active" data-tab-group="trainer" data-tab="dataset">
|
||
|
|
<div class="section">
|
||
|
|
<h2>Codebase Scan</h2>
|
||
|
|
<div class="tool-actions">
|
||
|
|
<button id="btn-scan" class="btn btn-primary" onclick="trScanCodebase()">Scan Codebase</button>
|
||
|
|
</div>
|
||
|
|
<div id="scan-result" style="margin-top:12px"></div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="section">
|
||
|
|
<h2>Generate Training Dataset</h2>
|
||
|
|
<p style="font-size:0.8rem;color:var(--text-muted);margin-bottom:12px">
|
||
|
|
Extracts code, architecture Q&A, and module creation examples from the AUTARCH codebase into JSONL training data.
|
||
|
|
</p>
|
||
|
|
<div class="form-row" style="margin-bottom:12px">
|
||
|
|
<div class="form-group">
|
||
|
|
<label>Format</label>
|
||
|
|
<select id="ds-format">
|
||
|
|
<option value="sharegpt">ShareGPT (conversations)</option>
|
||
|
|
<option value="instruction">Alpaca (instruction/output)</option>
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div style="margin-bottom:12px">
|
||
|
|
<label><input type="checkbox" id="ds-source" checked> Include source code understanding</label><br>
|
||
|
|
<label><input type="checkbox" id="ds-qa" checked> Include architecture Q&A</label><br>
|
||
|
|
<label><input type="checkbox" id="ds-modules" checked> Include module creation examples</label>
|
||
|
|
</div>
|
||
|
|
<div class="tool-actions">
|
||
|
|
<button id="btn-generate" class="btn btn-primary" onclick="trGenerateDataset()">Generate Dataset</button>
|
||
|
|
</div>
|
||
|
|
<pre class="output-panel" id="gen-result" style="margin-top:12px;min-height:0"></pre>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="section">
|
||
|
|
<h2>Saved Datasets</h2>
|
||
|
|
<div class="tool-actions">
|
||
|
|
<button class="btn btn-small" onclick="trListDatasets()">Refresh</button>
|
||
|
|
</div>
|
||
|
|
<div id="datasets-list" style="margin-top:12px">
|
||
|
|
<p class="empty-state">Click "Refresh" to load datasets.</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- ==================== TRAINING TAB ==================== -->
|
||
|
|
<div class="tab-content" data-tab-group="trainer" data-tab="training">
|
||
|
|
<div class="section">
|
||
|
|
<h2>LoRA Fine-Tuning</h2>
|
||
|
|
<p style="font-size:0.8rem;color:var(--text-muted);margin-bottom:12px">
|
||
|
|
Fine-tune a base model with LoRA adapters on your generated dataset. Requires a HuggingFace model ID or local path.
|
||
|
|
</p>
|
||
|
|
|
||
|
|
<div class="form-row" style="margin-bottom:8px">
|
||
|
|
<div class="form-group" style="flex:2">
|
||
|
|
<label>Base Model (local path)</label>
|
||
|
|
<div class="input-row">
|
||
|
|
<input type="text" id="tr-base-model" placeholder="Click Browse or paste a local model path...">
|
||
|
|
<button class="btn btn-small" onclick="trOpenBrowser()">Browse</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="form-group" style="flex:1">
|
||
|
|
<label>Dataset</label>
|
||
|
|
<select id="tr-dataset">
|
||
|
|
<option value="">Select dataset...</option>
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- File Browser Modal -->
|
||
|
|
<div id="model-browser-overlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.7);z-index:1000;display:none;align-items:center;justify-content:center">
|
||
|
|
<div style="background:var(--bg-secondary);border:1px solid var(--border);border-radius:8px;width:90%;max-width:700px;max-height:80vh;display:flex;flex-direction:column;padding:16px">
|
||
|
|
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px">
|
||
|
|
<h3 style="margin:0;flex:1">Select Base Model</h3>
|
||
|
|
<button class="btn btn-small" onclick="trCloseBrowser()">Close</button>
|
||
|
|
</div>
|
||
|
|
<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px">
|
||
|
|
<button class="btn btn-small" onclick="trBrowseUp()" title="Parent directory">↑ Up</button>
|
||
|
|
<code id="browser-path" style="flex:1;font-size:0.78rem;color:var(--text-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap"></code>
|
||
|
|
</div>
|
||
|
|
<div id="browser-entries" style="flex:1;overflow-y:auto;border:1px solid var(--border);border-radius:4px;font-size:0.82rem"></div>
|
||
|
|
<p style="font-size:0.72rem;color:var(--text-muted);margin:8px 0 0">Select a directory containing config.json + model weights, or navigate to find your model.</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<details style="margin-bottom:12px">
|
||
|
|
<summary style="cursor:pointer;font-size:0.85rem;color:var(--accent)">Advanced Settings</summary>
|
||
|
|
<div class="form-row" style="margin-top:8px">
|
||
|
|
<div class="form-group">
|
||
|
|
<label>LoRA Rank (r)</label>
|
||
|
|
<input type="number" id="tr-lora-r" value="16" min="4" max="128" style="max-width:80px">
|
||
|
|
</div>
|
||
|
|
<div class="form-group">
|
||
|
|
<label>LoRA Alpha</label>
|
||
|
|
<input type="number" id="tr-lora-alpha" value="32" min="8" max="256" style="max-width:80px">
|
||
|
|
</div>
|
||
|
|
<div class="form-group">
|
||
|
|
<label>Dropout</label>
|
||
|
|
<input type="number" id="tr-dropout" value="0.05" step="0.01" min="0" max="0.5" style="max-width:80px">
|
||
|
|
</div>
|
||
|
|
<div class="form-group">
|
||
|
|
<label>Epochs</label>
|
||
|
|
<input type="number" id="tr-epochs" value="3" min="1" max="20" style="max-width:80px">
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="form-row">
|
||
|
|
<div class="form-group">
|
||
|
|
<label>Batch Size</label>
|
||
|
|
<input type="number" id="tr-batch" value="4" min="1" max="32" style="max-width:80px">
|
||
|
|
</div>
|
||
|
|
<div class="form-group">
|
||
|
|
<label>Grad Accum Steps</label>
|
||
|
|
<input type="number" id="tr-grad-accum" value="4" min="1" max="32" style="max-width:80px">
|
||
|
|
</div>
|
||
|
|
<div class="form-group">
|
||
|
|
<label>Learning Rate</label>
|
||
|
|
<input type="text" id="tr-lr" value="2e-4" style="max-width:100px">
|
||
|
|
</div>
|
||
|
|
<div class="form-group">
|
||
|
|
<label>Max Seq Length</label>
|
||
|
|
<input type="number" id="tr-seq-len" value="2048" min="512" max="8192" style="max-width:100px">
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div style="margin-top:4px">
|
||
|
|
<label><input type="checkbox" id="tr-4bit" checked> Use 4-bit quantization (QLoRA)</label><br>
|
||
|
|
<label><input type="checkbox" id="tr-unsloth"> Use Unsloth (faster, if installed)</label>
|
||
|
|
</div>
|
||
|
|
</details>
|
||
|
|
|
||
|
|
<div class="tool-actions">
|
||
|
|
<button id="btn-train-start" class="btn btn-primary" onclick="trStartTraining()">Start Training</button>
|
||
|
|
<button id="btn-train-stop" class="btn btn-danger" onclick="trStopTraining()" style="display:none">Stop Training</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div id="training-status" style="margin-top:12px">
|
||
|
|
<div class="stats-grid" style="grid-template-columns:repeat(auto-fit,minmax(120px,1fr))">
|
||
|
|
<div class="stat-card">
|
||
|
|
<div class="stat-label">Phase</div>
|
||
|
|
<div class="stat-value small" id="tr-phase">Idle</div>
|
||
|
|
</div>
|
||
|
|
<div class="stat-card">
|
||
|
|
<div class="stat-label">Progress</div>
|
||
|
|
<div class="stat-value small" id="tr-progress">0%</div>
|
||
|
|
</div>
|
||
|
|
<div class="stat-card">
|
||
|
|
<div class="stat-label">Status</div>
|
||
|
|
<div class="stat-value small" id="tr-message">Ready</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<h4 style="margin-top:16px">Training Log</h4>
|
||
|
|
<pre class="output-panel scrollable" id="training-log" style="max-height:300px;font-size:0.75rem">Training log will appear here...</pre>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- ==================== CONVERT TAB ==================== -->
|
||
|
|
<div class="tab-content" data-tab-group="trainer" data-tab="convert">
|
||
|
|
<div class="section">
|
||
|
|
<h2>GGUF Conversion</h2>
|
||
|
|
<p style="font-size:0.8rem;color:var(--text-muted);margin-bottom:12px">
|
||
|
|
Merge a trained LoRA adapter with its base model and convert to GGUF format for local inference.
|
||
|
|
</p>
|
||
|
|
|
||
|
|
<h4>Saved LoRA Adapters</h4>
|
||
|
|
<div class="tool-actions">
|
||
|
|
<button class="btn btn-small" onclick="trListAdapters()">Refresh Adapters</button>
|
||
|
|
</div>
|
||
|
|
<div id="adapters-list" style="margin-top:8px">
|
||
|
|
<p class="empty-state">Click "Refresh" to load adapters.</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<h4 style="margin-top:16px">Convert to GGUF</h4>
|
||
|
|
<div class="form-row" style="margin-bottom:12px">
|
||
|
|
<div class="form-group" style="flex:2">
|
||
|
|
<label>Adapter Path</label>
|
||
|
|
<input type="text" id="cv-adapter" placeholder="Path to LoRA adapter directory">
|
||
|
|
</div>
|
||
|
|
<div class="form-group">
|
||
|
|
<label>Output Name</label>
|
||
|
|
<input type="text" id="cv-output" placeholder="e.g., Hal_v3" value="Hal_v3">
|
||
|
|
</div>
|
||
|
|
<div class="form-group">
|
||
|
|
<label>Quantization</label>
|
||
|
|
<select id="cv-quant">
|
||
|
|
<option value="Q4_K_M">Q4_K_M (small, fast)</option>
|
||
|
|
<option value="Q5_K_M" selected>Q5_K_M (balanced)</option>
|
||
|
|
<option value="Q6_K">Q6_K (quality)</option>
|
||
|
|
<option value="Q8_0">Q8_0 (high quality)</option>
|
||
|
|
<option value="F16">F16 (unquantized)</option>
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="tool-actions">
|
||
|
|
<button id="btn-convert" class="btn btn-primary" onclick="trConvert()">Merge & Convert</button>
|
||
|
|
</div>
|
||
|
|
<pre class="output-panel" id="convert-result" style="margin-top:12px;min-height:0"></pre>
|
||
|
|
|
||
|
|
<h4 style="margin-top:16px">Available GGUF Models</h4>
|
||
|
|
<div class="tool-actions">
|
||
|
|
<button class="btn btn-small" onclick="trListModels()">Refresh Models</button>
|
||
|
|
</div>
|
||
|
|
<div id="models-list" style="margin-top:8px">
|
||
|
|
<p class="empty-state">Click "Refresh" to load models.</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- ==================== EVALUATE TAB ==================== -->
|
||
|
|
<div class="tab-content" data-tab-group="trainer" data-tab="evaluate">
|
||
|
|
<div class="section">
|
||
|
|
<h2>Model Evaluation</h2>
|
||
|
|
<p style="font-size:0.8rem;color:var(--text-muted);margin-bottom:12px">
|
||
|
|
Test a GGUF model with sample prompts to verify it learned the AUTARCH codebase.
|
||
|
|
</p>
|
||
|
|
|
||
|
|
<div class="form-row" style="margin-bottom:12px">
|
||
|
|
<div class="form-group" style="flex:2">
|
||
|
|
<label>Model Path</label>
|
||
|
|
<select id="ev-model">
|
||
|
|
<option value="">Select model...</option>
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<h4>Test Prompts</h4>
|
||
|
|
<div id="eval-prompts">
|
||
|
|
<div class="input-row" style="margin-bottom:4px">
|
||
|
|
<input type="text" class="eval-prompt" value="What is AUTARCH?" style="flex:1">
|
||
|
|
</div>
|
||
|
|
<div class="input-row" style="margin-bottom:4px">
|
||
|
|
<input type="text" class="eval-prompt" value="How do I create a new defense module?" style="flex:1">
|
||
|
|
</div>
|
||
|
|
<div class="input-row" style="margin-bottom:4px">
|
||
|
|
<input type="text" class="eval-prompt" value="What module categories does AUTARCH support?" style="flex:1">
|
||
|
|
</div>
|
||
|
|
<div class="input-row" style="margin-bottom:4px">
|
||
|
|
<input type="text" class="eval-prompt" value="Create a module that scans for open ports on localhost." style="flex:1">
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<button class="btn btn-small" onclick="trAddPrompt()" style="margin-top:4px">+ Add Prompt</button>
|
||
|
|
|
||
|
|
<div class="tool-actions" style="margin-top:12px">
|
||
|
|
<button id="btn-evaluate" class="btn btn-primary" onclick="trEvaluate()">Run Evaluation</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div id="eval-results" style="margin-top:12px"></div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<script>
|
||
|
|
/* ── LLM Trainer ── */
|
||
|
|
var _trPollInterval = null;
|
||
|
|
|
||
|
|
function trScanCodebase() {
|
||
|
|
var btn = document.getElementById('btn-scan');
|
||
|
|
setLoading(btn, true);
|
||
|
|
postJSON('/llm-trainer/scan', {}).then(function(data) {
|
||
|
|
setLoading(btn, false);
|
||
|
|
var inv = data.inventory || {};
|
||
|
|
var html = '<table class="data-table" style="font-size:0.82rem"><thead><tr>'
|
||
|
|
+ '<th>Category</th><th>Files</th><th>Lines</th></tr></thead><tbody>';
|
||
|
|
var cats = ['modules','core','routes','templates','configs','other'];
|
||
|
|
cats.forEach(function(cat) {
|
||
|
|
var files = inv[cat] || [];
|
||
|
|
var lines = files.reduce(function(s,f){return s+f.lines},0);
|
||
|
|
html += '<tr><td>' + cat + '</td><td>' + files.length + '</td><td>' + lines.toLocaleString() + '</td></tr>';
|
||
|
|
});
|
||
|
|
html += '<tr style="font-weight:bold"><td>Total</td><td>' + data.total_files
|
||
|
|
+ '</td><td>' + data.total_lines.toLocaleString() + '</td></tr>';
|
||
|
|
html += '</tbody></table>';
|
||
|
|
document.getElementById('scan-result').innerHTML = html;
|
||
|
|
}).catch(function() { setLoading(btn, false); });
|
||
|
|
}
|
||
|
|
|
||
|
|
function trGenerateDataset() {
|
||
|
|
var btn = document.getElementById('btn-generate');
|
||
|
|
setLoading(btn, true);
|
||
|
|
postJSON('/llm-trainer/dataset/generate', {
|
||
|
|
format: document.getElementById('ds-format').value,
|
||
|
|
include_source: document.getElementById('ds-source').checked,
|
||
|
|
include_qa: document.getElementById('ds-qa').checked,
|
||
|
|
include_module_creation: document.getElementById('ds-modules').checked,
|
||
|
|
}).then(function(data) {
|
||
|
|
setLoading(btn, false);
|
||
|
|
if (data.error) {
|
||
|
|
renderOutput('gen-result', 'Error: ' + data.error);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
var lines = [
|
||
|
|
'Dataset Generated!',
|
||
|
|
' File: ' + data.filename,
|
||
|
|
' Samples: ' + data.sample_count,
|
||
|
|
' Format: ' + data.format,
|
||
|
|
' Size: ' + (data.size_bytes / 1024).toFixed(1) + ' KB',
|
||
|
|
'',
|
||
|
|
'Preview (first sample):',
|
||
|
|
];
|
||
|
|
if (data.preview && data.preview.length) {
|
||
|
|
lines.push(JSON.stringify(data.preview[0], null, 2));
|
||
|
|
}
|
||
|
|
renderOutput('gen-result', lines.join('\n'));
|
||
|
|
trListDatasets();
|
||
|
|
trRefreshDatasetSelect();
|
||
|
|
}).catch(function() { setLoading(btn, false); });
|
||
|
|
}
|
||
|
|
|
||
|
|
function trListDatasets() {
|
||
|
|
fetchJSON('/llm-trainer/dataset/list').then(function(data) {
|
||
|
|
var ds = data.datasets || [];
|
||
|
|
var container = document.getElementById('datasets-list');
|
||
|
|
if (!ds.length) {
|
||
|
|
container.innerHTML = '<p class="empty-state">No datasets generated yet.</p>';
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
var html = '<table class="data-table" style="font-size:0.8rem"><thead><tr>'
|
||
|
|
+ '<th>File</th><th>Samples</th><th>Size</th><th>Created</th><th>Actions</th></tr></thead><tbody>';
|
||
|
|
ds.forEach(function(d) {
|
||
|
|
html += '<tr><td>' + escapeHtml(d.filename) + '</td>'
|
||
|
|
+ '<td>' + d.sample_count + '</td>'
|
||
|
|
+ '<td>' + (d.size_bytes/1024).toFixed(1) + ' KB</td>'
|
||
|
|
+ '<td>' + d.created.substring(0,16).replace('T',' ') + '</td>'
|
||
|
|
+ '<td>'
|
||
|
|
+ '<button class="btn btn-small" style="font-size:0.65rem;padding:2px 6px;margin-right:4px" '
|
||
|
|
+ 'onclick="trPreviewDataset(\'' + escapeHtml(d.filename) + '\')">Preview</button>'
|
||
|
|
+ '<button class="btn btn-danger btn-small" style="font-size:0.65rem;padding:2px 6px" '
|
||
|
|
+ 'onclick="trDeleteDataset(\'' + escapeHtml(d.filename) + '\')">Delete</button>'
|
||
|
|
+ '</td></tr>';
|
||
|
|
});
|
||
|
|
html += '</tbody></table>';
|
||
|
|
container.innerHTML = html;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function trPreviewDataset(filename) {
|
||
|
|
postJSON('/llm-trainer/dataset/preview', {filename: filename, limit: 5}).then(function(data) {
|
||
|
|
if (data.error) { alert(data.error); return; }
|
||
|
|
var html = '<h4>Preview: ' + escapeHtml(filename) + '</h4>';
|
||
|
|
(data.samples || []).forEach(function(s, i) {
|
||
|
|
html += '<pre class="output-panel" style="font-size:0.75rem;margin-bottom:4px;max-height:150px;overflow-y:auto">'
|
||
|
|
+ escapeHtml(JSON.stringify(s, null, 2)) + '</pre>';
|
||
|
|
});
|
||
|
|
document.getElementById('datasets-list').innerHTML += html;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function trDeleteDataset(filename) {
|
||
|
|
if (!confirm('Delete ' + filename + '?')) return;
|
||
|
|
postJSON('/llm-trainer/dataset/delete', {filename: filename}).then(function() {
|
||
|
|
trListDatasets();
|
||
|
|
trRefreshDatasetSelect();
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function trRefreshDatasetSelect() {
|
||
|
|
fetchJSON('/llm-trainer/dataset/list').then(function(data) {
|
||
|
|
var sel = document.getElementById('tr-dataset');
|
||
|
|
sel.innerHTML = '<option value="">Select dataset...</option>';
|
||
|
|
(data.datasets || []).forEach(function(d) {
|
||
|
|
sel.innerHTML += '<option value="' + escapeHtml(d.path) + '">' + escapeHtml(d.filename)
|
||
|
|
+ ' (' + d.sample_count + ' samples)</option>';
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ── Model Browser ── */
|
||
|
|
var _browserCurrentDir = '';
|
||
|
|
var _browserParentDir = '';
|
||
|
|
|
||
|
|
function trOpenBrowser() {
|
||
|
|
document.getElementById('model-browser-overlay').style.display = 'flex';
|
||
|
|
trBrowseDir(''); // starts at models/ directory
|
||
|
|
}
|
||
|
|
|
||
|
|
function trCloseBrowser() {
|
||
|
|
document.getElementById('model-browser-overlay').style.display = 'none';
|
||
|
|
}
|
||
|
|
|
||
|
|
function trBrowseUp() {
|
||
|
|
if (_browserParentDir) trBrowseDir(_browserParentDir);
|
||
|
|
}
|
||
|
|
|
||
|
|
function trBrowseDir(dir) {
|
||
|
|
postJSON('/llm-trainer/browse', {directory: dir}).then(function(data) {
|
||
|
|
if (data.error) {
|
||
|
|
document.getElementById('browser-entries').innerHTML = '<p style="padding:8px;color:var(--danger)">' + escapeHtml(data.error) + '</p>';
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
_browserCurrentDir = data.current_dir || '';
|
||
|
|
_browserParentDir = data.parent_dir || '';
|
||
|
|
document.getElementById('browser-path').textContent = _browserCurrentDir;
|
||
|
|
|
||
|
|
var entries = data.entries || [];
|
||
|
|
if (!entries.length) {
|
||
|
|
document.getElementById('browser-entries').innerHTML = '<p style="padding:8px;color:var(--text-muted)">Empty directory</p>';
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
var html = '<table class="data-table" style="font-size:0.8rem;margin:0"><tbody>';
|
||
|
|
entries.forEach(function(e) {
|
||
|
|
if (e.is_dir) {
|
||
|
|
var icon = e.is_model ? '📦' : '📁';
|
||
|
|
var label = e.is_model
|
||
|
|
? '<span style="color:var(--success);font-weight:bold">' + escapeHtml(e.name) + '</span> <span style="font-size:0.7rem;color:var(--text-muted)">(HF model)</span>'
|
||
|
|
: escapeHtml(e.name);
|
||
|
|
html += '<tr><td style="cursor:pointer" onclick="trBrowseDir(\'' + escapeHtml(e.path) + '\')">' + icon + ' ' + label + '</td>';
|
||
|
|
if (e.is_model) {
|
||
|
|
html += '<td><button class="btn btn-small" style="font-size:0.65rem;padding:2px 8px" onclick="trSelectModel(\'' + escapeHtml(e.path) + '\')">Select</button></td>';
|
||
|
|
} else {
|
||
|
|
html += '<td></td>';
|
||
|
|
}
|
||
|
|
html += '</tr>';
|
||
|
|
} else {
|
||
|
|
var size = e.size_gb !== undefined ? e.size_gb + ' GB' : '';
|
||
|
|
html += '<tr><td>📄 ' + escapeHtml(e.name) + '</td><td style="font-size:0.7rem;color:var(--text-muted)">' + size + '</td></tr>';
|
||
|
|
}
|
||
|
|
});
|
||
|
|
html += '</tbody></table>';
|
||
|
|
document.getElementById('browser-entries').innerHTML = html;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function trSelectModel(path) {
|
||
|
|
document.getElementById('tr-base-model').value = path;
|
||
|
|
trCloseBrowser();
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ── Training ── */
|
||
|
|
function trStartTraining() {
|
||
|
|
var baseModel = document.getElementById('tr-base-model').value.trim();
|
||
|
|
var dataset = document.getElementById('tr-dataset').value;
|
||
|
|
if (!baseModel) { alert('Click Browse to select a base model'); return; }
|
||
|
|
if (!dataset) { alert('Please select a dataset'); return; }
|
||
|
|
|
||
|
|
var config = {
|
||
|
|
base_model: baseModel,
|
||
|
|
dataset: dataset,
|
||
|
|
lora_r: parseInt(document.getElementById('tr-lora-r').value),
|
||
|
|
lora_alpha: parseInt(document.getElementById('tr-lora-alpha').value),
|
||
|
|
lora_dropout: parseFloat(document.getElementById('tr-dropout').value),
|
||
|
|
num_epochs: parseInt(document.getElementById('tr-epochs').value),
|
||
|
|
batch_size: parseInt(document.getElementById('tr-batch').value),
|
||
|
|
gradient_accumulation_steps: parseInt(document.getElementById('tr-grad-accum').value),
|
||
|
|
learning_rate: parseFloat(document.getElementById('tr-lr').value),
|
||
|
|
max_seq_length: parseInt(document.getElementById('tr-seq-len').value),
|
||
|
|
use_4bit: document.getElementById('tr-4bit').checked,
|
||
|
|
use_unsloth: document.getElementById('tr-unsloth').checked,
|
||
|
|
};
|
||
|
|
|
||
|
|
var btn = document.getElementById('btn-train-start');
|
||
|
|
setLoading(btn, true);
|
||
|
|
postJSON('/llm-trainer/train/start', config).then(function(data) {
|
||
|
|
setLoading(btn, false);
|
||
|
|
if (data.error) {
|
||
|
|
alert('Training failed: ' + data.error);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
document.getElementById('btn-train-start').style.display = 'none';
|
||
|
|
document.getElementById('btn-train-stop').style.display = '';
|
||
|
|
trStartPolling();
|
||
|
|
}).catch(function() { setLoading(btn, false); });
|
||
|
|
}
|
||
|
|
|
||
|
|
function trStopTraining() {
|
||
|
|
postJSON('/llm-trainer/train/stop', {}).then(function() {
|
||
|
|
trStopPolling();
|
||
|
|
document.getElementById('btn-train-start').style.display = '';
|
||
|
|
document.getElementById('btn-train-stop').style.display = 'none';
|
||
|
|
trUpdateStatus();
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function trStartPolling() {
|
||
|
|
if (_trPollInterval) clearInterval(_trPollInterval);
|
||
|
|
_trPollInterval = setInterval(trUpdateStatus, 3000);
|
||
|
|
trUpdateStatus();
|
||
|
|
}
|
||
|
|
|
||
|
|
function trStopPolling() {
|
||
|
|
if (_trPollInterval) { clearInterval(_trPollInterval); _trPollInterval = null; }
|
||
|
|
}
|
||
|
|
|
||
|
|
function trUpdateStatus() {
|
||
|
|
fetchJSON('/llm-trainer/train/status').then(function(data) {
|
||
|
|
document.getElementById('tr-phase').textContent = data.phase || 'idle';
|
||
|
|
document.getElementById('tr-progress').textContent = (data.progress || 0) + '%';
|
||
|
|
document.getElementById('tr-message').textContent = data.message || '';
|
||
|
|
if (data.training_log) {
|
||
|
|
document.getElementById('training-log').textContent = data.training_log;
|
||
|
|
var log = document.getElementById('training-log');
|
||
|
|
log.scrollTop = log.scrollHeight;
|
||
|
|
}
|
||
|
|
if (!data.training_running && _trPollInterval) {
|
||
|
|
trStopPolling();
|
||
|
|
document.getElementById('btn-train-start').style.display = '';
|
||
|
|
document.getElementById('btn-train-stop').style.display = 'none';
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ── Conversion ── */
|
||
|
|
function trListAdapters() {
|
||
|
|
fetchJSON('/llm-trainer/adapters').then(function(data) {
|
||
|
|
var adapters = data.adapters || [];
|
||
|
|
var container = document.getElementById('adapters-list');
|
||
|
|
if (!adapters.length) {
|
||
|
|
container.innerHTML = '<p class="empty-state">No LoRA adapters found. Train a model first.</p>';
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
var html = '<table class="data-table" style="font-size:0.8rem"><thead><tr>'
|
||
|
|
+ '<th>Name</th><th>Base Model</th><th>LoRA r</th><th>Action</th></tr></thead><tbody>';
|
||
|
|
adapters.forEach(function(a) {
|
||
|
|
html += '<tr><td>' + escapeHtml(a.name) + '</td>'
|
||
|
|
+ '<td>' + escapeHtml(a.base_model || '—') + '</td>'
|
||
|
|
+ '<td>' + (a.r || '—') + '</td>'
|
||
|
|
+ '<td><button class="btn btn-small" style="font-size:0.65rem;padding:2px 6px" '
|
||
|
|
+ 'onclick="document.getElementById(\'cv-adapter\').value=\'' + escapeHtml(a.path) + '\'">Select</button></td></tr>';
|
||
|
|
});
|
||
|
|
html += '</tbody></table>';
|
||
|
|
container.innerHTML = html;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function trConvert() {
|
||
|
|
var adapter = document.getElementById('cv-adapter').value.trim();
|
||
|
|
var output = document.getElementById('cv-output').value.trim();
|
||
|
|
var quant = document.getElementById('cv-quant').value;
|
||
|
|
if (!adapter) { alert('Enter or select an adapter path'); return; }
|
||
|
|
if (!output) { alert('Enter an output name'); return; }
|
||
|
|
|
||
|
|
var btn = document.getElementById('btn-convert');
|
||
|
|
setLoading(btn, true);
|
||
|
|
renderOutput('convert-result', 'Merging and converting — this may take a while...');
|
||
|
|
postJSON('/llm-trainer/convert', {
|
||
|
|
adapter_path: adapter,
|
||
|
|
output_name: output,
|
||
|
|
quantization: quant,
|
||
|
|
}).then(function(data) {
|
||
|
|
setLoading(btn, false);
|
||
|
|
if (data.error) {
|
||
|
|
renderOutput('convert-result', 'Error: ' + data.error);
|
||
|
|
} else if (data.partial) {
|
||
|
|
renderOutput('convert-result', data.message);
|
||
|
|
} else {
|
||
|
|
renderOutput('convert-result', 'GGUF model saved!\n Path: ' + data.output_path
|
||
|
|
+ '\n Size: ' + (data.size_bytes / (1024*1024*1024)).toFixed(2) + ' GB');
|
||
|
|
trListModels();
|
||
|
|
}
|
||
|
|
}).catch(function() { setLoading(btn, false); });
|
||
|
|
}
|
||
|
|
|
||
|
|
function trListModels() {
|
||
|
|
fetchJSON('/llm-trainer/models').then(function(data) {
|
||
|
|
var models = data.models || [];
|
||
|
|
var container = document.getElementById('models-list');
|
||
|
|
if (!models.length) {
|
||
|
|
container.innerHTML = '<p class="empty-state">No GGUF models found.</p>';
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
var html = '<table class="data-table" style="font-size:0.8rem"><thead><tr>'
|
||
|
|
+ '<th>Name</th><th>Size</th><th>Modified</th></tr></thead><tbody>';
|
||
|
|
models.forEach(function(m) {
|
||
|
|
html += '<tr><td>' + escapeHtml(m.filename) + '</td>'
|
||
|
|
+ '<td>' + m.size_gb + ' GB</td>'
|
||
|
|
+ '<td>' + m.modified.substring(0,16).replace('T',' ') + '</td></tr>';
|
||
|
|
});
|
||
|
|
html += '</tbody></table>';
|
||
|
|
container.innerHTML = html;
|
||
|
|
|
||
|
|
// Update evaluate model select
|
||
|
|
var sel = document.getElementById('ev-model');
|
||
|
|
sel.innerHTML = '<option value="">Select model...</option>';
|
||
|
|
models.forEach(function(m) {
|
||
|
|
sel.innerHTML += '<option value="' + escapeHtml(m.path) + '">' + escapeHtml(m.filename)
|
||
|
|
+ ' (' + m.size_gb + ' GB)</option>';
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ── Evaluate ── */
|
||
|
|
function trAddPrompt() {
|
||
|
|
var div = document.createElement('div');
|
||
|
|
div.className = 'input-row';
|
||
|
|
div.style.marginBottom = '4px';
|
||
|
|
div.innerHTML = '<input type="text" class="eval-prompt" placeholder="Enter test prompt..." style="flex:1">'
|
||
|
|
+ '<button class="btn btn-small" style="font-size:0.65rem;padding:2px 6px;margin-left:4px" '
|
||
|
|
+ 'onclick="this.parentElement.remove()">X</button>';
|
||
|
|
document.getElementById('eval-prompts').appendChild(div);
|
||
|
|
}
|
||
|
|
|
||
|
|
function trEvaluate() {
|
||
|
|
var model = document.getElementById('ev-model').value;
|
||
|
|
if (!model) { alert('Select a model to evaluate'); return; }
|
||
|
|
|
||
|
|
var prompts = [];
|
||
|
|
document.querySelectorAll('.eval-prompt').forEach(function(el) {
|
||
|
|
if (el.value.trim()) prompts.push(el.value.trim());
|
||
|
|
});
|
||
|
|
if (!prompts.length) { alert('Add at least one test prompt'); return; }
|
||
|
|
|
||
|
|
var btn = document.getElementById('btn-evaluate');
|
||
|
|
setLoading(btn, true);
|
||
|
|
document.getElementById('eval-results').innerHTML = '<p style="color:var(--text-muted)">Running evaluation — loading model and generating responses...</p>';
|
||
|
|
|
||
|
|
postJSON('/llm-trainer/evaluate', {model_path: model, prompts: prompts}).then(function(data) {
|
||
|
|
setLoading(btn, false);
|
||
|
|
if (data.error) {
|
||
|
|
document.getElementById('eval-results').innerHTML = '<p style="color:var(--danger)">Error: ' + escapeHtml(data.error) + '</p>';
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
var html = '';
|
||
|
|
(data.results || []).forEach(function(r, i) {
|
||
|
|
html += '<div style="margin-bottom:12px;border:1px solid var(--border);border-radius:6px;padding:12px">'
|
||
|
|
+ '<div style="font-weight:bold;margin-bottom:6px;color:var(--accent)">Prompt: ' + escapeHtml(r.prompt) + '</div>'
|
||
|
|
+ '<pre class="output-panel scrollable" style="max-height:200px;font-size:0.78rem;margin:0">'
|
||
|
|
+ escapeHtml(r.response) + '</pre>'
|
||
|
|
+ '<div style="font-size:0.7rem;color:var(--text-muted);margin-top:4px">Response length: ' + r.length + ' chars</div>'
|
||
|
|
+ '</div>';
|
||
|
|
});
|
||
|
|
document.getElementById('eval-results').innerHTML = html;
|
||
|
|
}).catch(function() { setLoading(btn, false); });
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ── Init ── */
|
||
|
|
document.addEventListener('DOMContentLoaded', function() {
|
||
|
|
trRefreshDatasetSelect();
|
||
|
|
trListModels();
|
||
|
|
});
|
||
|
|
</script>
|
||
|
|
{% endblock %}
|