v1.8.0: bge-m3 embeddings, qwen3 LLM, retrieval fixes, input validation, memory poisoning fix
This commit is contained in:
+237
-5
@@ -242,6 +242,49 @@ a[role=button]:active {
|
||||
Sign In
|
||||
</button>
|
||||
<p id="login-err" class="text-error text-xs mt-3 hidden">Invalid credentials</p>
|
||||
|
||||
<section id="page-pages" class="page hidden">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h2 class="text-xl font-bold">Pages</h2>
|
||||
<p class="text-sm text-on-surface-variant mt-1">Edit site pages content</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Page Tabs -->
|
||||
<div class="flex gap-2 mb-6">
|
||||
<button id="pages-tab-privacy" onclick="switchPageTab('privacy')"
|
||||
class="px-4 py-2 rounded-xl text-sm font-semibold bg-primary text-surface transition-all">
|
||||
سياسة الخصوصية
|
||||
</button>
|
||||
<button id="pages-tab-terms" onclick="switchPageTab('terms')"
|
||||
class="px-4 py-2 rounded-xl text-sm font-semibold glass text-on-surface-variant hover:text-primary transition-all">
|
||||
شروط الخدمة
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Editor -->
|
||||
<div class="glass rounded-2xl p-5 space-y-4" style="border:1px solid rgba(78,222,163,0.1)">
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-sm text-on-surface-variant">تعديل محتوى HTML للصفحة</p>
|
||||
<div class="flex gap-2">
|
||||
<a id="pages-preview-btn" href="/privacy" target="_blank"
|
||||
class="glass px-3 py-1.5 rounded-lg text-xs text-on-surface-variant hover:text-primary transition-colors flex items-center gap-1">
|
||||
<span class="msym text-sm">open_in_new</span> معاينة
|
||||
</a>
|
||||
<button onclick="savePageContent()" id="pages-save-btn"
|
||||
class="bg-primary text-surface font-bold px-4 py-1.5 rounded-lg text-xs flex items-center gap-1 hover:shadow-[0_0_15px_rgba(78,222,163,0.4)] transition-all">
|
||||
<span class="msym text-sm">save</span> حفظ
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<textarea id="pages-editor"
|
||||
class="w-full bg-surface/60 border border-white/10 rounded-xl p-4 text-xs font-mono text-on-surface focus:outline-none focus:border-primary/50 transition-colors resize-none"
|
||||
style="height:420px;direction:ltr"></textarea>
|
||||
<p id="pages-save-status" class="text-xs text-primary hidden">✅ تم الحفظ بنجاح</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -292,6 +335,9 @@ a[role=button]:active {
|
||||
<button class="nav-btn w-full flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-semibold transition-all" onclick="showPage('settings',this)">
|
||||
<span class="msym text-lg">settings</span><span>Settings</span>
|
||||
</button>
|
||||
<button class="nav-btn w-full flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-semibold transition-all" onclick="showPage('pages',this)">
|
||||
<span class="msym text-lg">description</span><span>Pages</span>
|
||||
</button>
|
||||
<button class="nav-btn w-full flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-semibold transition-all" onclick="showPage('branding',this)">
|
||||
<span class="msym text-lg">palette</span><span>Branding</span>
|
||||
</button>
|
||||
@@ -516,6 +562,7 @@ curl -X POST https://rag.afaqzaid.com/ingest-url \
|
||||
<div id="settings-ai" class="glass rounded-2xl p-6"></div>
|
||||
<div id="settings-system" class="glass rounded-2xl p-6"></div>
|
||||
</div>
|
||||
<div id="settings-llm" class="glass rounded-2xl p-6 mt-6"></div>
|
||||
<div id="settings-r2" class="glass rounded-2xl p-6 mt-6"></div>
|
||||
</section>
|
||||
|
||||
@@ -805,7 +852,7 @@ curl -X POST https://rag.afaqzaid.com/ingest-url \
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let ADMIN_KEY = 'afakadmin-Ihb7kflFA682Ax3EICnV2ymr4KuNZSo7';
|
||||
// ADMIN_KEY removed — JWT-only auth
|
||||
let JWT_TOKEN = localStorage.getItem('admin_jwt') || '';
|
||||
const RAG_KEY = 'zRf77E_brSEnA2BUi40uxLxT4uofAk661WMZb-cdv4M';
|
||||
let keyVisible = false;
|
||||
@@ -815,7 +862,7 @@ if (!JWT_TOKEN) document.getElementById('login').style.display = 'flex';
|
||||
|
||||
// ── Core API ──────────────────────────────────────────────────────────────────
|
||||
async function api(path, opts={}) {
|
||||
const headers = {'Content-Type':'application/json', 'X-API-Key': ADMIN_KEY};
|
||||
const headers = {'Content-Type':'application/json', 'Authorization':'Bearer '+JWT_TOKEN};
|
||||
if (JWT_TOKEN) headers['Authorization'] = 'Bearer ' + JWT_TOKEN;
|
||||
const r = await fetch(path, {...opts, headers:{...headers, ...(opts.headers||{})}});
|
||||
if (r.status === 401) {
|
||||
@@ -967,7 +1014,7 @@ async function deleteBook(id, filename, btn) {
|
||||
if (!confirm(`حذف الكتاب؟\n"${filename}"\n\nسيُحذف ملف PDF والمتجهات والسجل نهائياً.`)) return;
|
||||
btn.disabled = true; btn.innerHTML = '<span class="msym text-sm animate-spin">autorenew</span>';
|
||||
try {
|
||||
const r = await fetch(`/api/library/${id}`, {method:'DELETE', headers:{'X-API-Key':ADMIN_KEY,'Authorization':'Bearer '+JWT_TOKEN}});
|
||||
const r = await fetch(`/api/library/${id}`, {method:'DELETE', headers:{'Authorization':'Bearer '+JWT_TOKEN}});
|
||||
const d = await r.json();
|
||||
if (r.ok) { document.getElementById('book-row-'+id)?.remove(); loadOverview(); toast('Book deleted','success'); }
|
||||
else { alert('فشل الحذف: '+(d.detail||'خطأ')); btn.disabled=false; btn.innerHTML='<span class="msym text-sm">delete</span>'; }
|
||||
@@ -1078,6 +1125,7 @@ async function loadResources() {
|
||||
}
|
||||
|
||||
async function loadSettings() {
|
||||
loadLLMConfig();
|
||||
try {
|
||||
const d = await api('/admin/settings');
|
||||
document.getElementById('settings-ai').innerHTML = `
|
||||
@@ -1236,7 +1284,7 @@ async function loadBranding() {
|
||||
|
||||
async function saveBranding() {
|
||||
try {
|
||||
const r = await fetch('/admin/branding', {method:'POST', headers:{'Content-Type':'application/json','X-API-Key':ADMIN_KEY,'Authorization':'Bearer '+JWT_TOKEN}, body:JSON.stringify({site_name:document.getElementById('b-site-name').value, site_subtitle:document.getElementById('b-subtitle').value, logo_url:document.getElementById('b-logo').value, primary_color:document.getElementById('b-primary-hex').value, secondary_color:document.getElementById('b-secondary-hex').value})});
|
||||
const r = await fetch('/admin/branding', {method:'POST', headers:{'Content-Type':'application/json','Authorization':'Bearer '+JWT_TOKEN}, body:JSON.stringify({site_name:document.getElementById('b-site-name').value, site_subtitle:document.getElementById('b-subtitle').value, logo_url:document.getElementById('b-logo').value, primary_color:document.getElementById('b-primary-hex').value, secondary_color:document.getElementById('b-secondary-hex').value})});
|
||||
if (r.ok) toast('Branding saved!','success'); else alert('Save failed');
|
||||
} catch(e) { alert('Error: '+e.message); }
|
||||
}
|
||||
@@ -1338,7 +1386,7 @@ async function saveClaudeKey() {
|
||||
const key = document.getElementById('claude-key-input').value.trim();
|
||||
if (!key) return;
|
||||
try {
|
||||
const r = await fetch('/admin/claude/key', {method:'POST', headers:{'Content-Type':'application/json','X-API-Key':ADMIN_KEY,'Authorization':'Bearer '+JWT_TOKEN}, body:JSON.stringify({api_key:key})});
|
||||
const r = await fetch('/admin/claude/key', {method:'POST', headers:{'Content-Type':'application/json','Authorization':'Bearer '+JWT_TOKEN}, body:JSON.stringify({api_key:key})});
|
||||
if (r.ok) { toast('Claude key saved!','success'); loadClaude(); document.getElementById('claude-key-input').value=''; }
|
||||
else toast('Failed to save key');
|
||||
} catch(e) { toast('Error: '+e.message); }
|
||||
@@ -1777,6 +1825,122 @@ function applyScanResult(item) {
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function loadLLMConfig() {
|
||||
try {
|
||||
const d = await api('/admin/llm-config');
|
||||
const isLocal = d.provider !== 'cloudflare';
|
||||
const isCF = d.provider === 'cloudflare';
|
||||
window._llmProvider = d.provider || 'local';
|
||||
document.getElementById('settings-llm').innerHTML = `
|
||||
<h3 class="text-primary text-xs font-bold uppercase tracking-widest mb-5 flex items-center gap-2">
|
||||
<span class="msym text-base">hub</span>LLM Provider
|
||||
<span class="ml-auto text-[10px] font-normal glass px-2 py-0.5 rounded-full ${isCF ? 'text-blue-400' : 'text-emerald-400'}">
|
||||
${isCF ? '⚡ Cloudflare GPU' : '🖥 Local Ollama'}
|
||||
</span>
|
||||
</h3>
|
||||
<div class="flex gap-2 mb-5">
|
||||
<button onclick="llmSetProvider('local')" id="llm-btn-local"
|
||||
class="flex-1 py-2.5 rounded-xl text-xs font-bold transition-all flex items-center justify-center gap-2 ${isLocal ? 'bg-primary text-surface' : 'glass glass-hover text-on-surface-variant'}">
|
||||
<span class="msym text-base">computer</span> Local (Ollama)
|
||||
</button>
|
||||
<button onclick="llmSetProvider('cloudflare')" id="llm-btn-cf"
|
||||
class="flex-1 py-2.5 rounded-xl text-xs font-bold transition-all flex items-center justify-center gap-2 ${isCF ? 'bg-blue-500 text-white' : 'glass glass-hover text-on-surface-variant'}">
|
||||
<span class="msym text-base">cloud</span> Cloudflare GPU
|
||||
</button>
|
||||
</div>
|
||||
<div id="llm-local-info" class="${isLocal ? '' : 'hidden'} glass rounded-xl p-4 mb-4 text-xs text-on-surface-variant space-y-1">
|
||||
<p class="flex items-center gap-2"><span class="msym text-base text-emerald-400">check_circle</span>Running qwen2:7b on your VPS CPU</p>
|
||||
<p class="flex items-center gap-2"><span class="msym text-base text-yellow-400">schedule</span>~30-50s per query — streaming masks latency</p>
|
||||
<p class="flex items-center gap-2"><span class="msym text-base text-primary">lock</span>Fully private — no external requests</p>
|
||||
</div>
|
||||
<div id="llm-cf-fields" class="${isCF ? '' : 'hidden'} space-y-3 mb-4">
|
||||
<div class="glass rounded-xl p-4 text-xs text-on-surface-variant space-y-1 mb-3">
|
||||
<p class="flex items-center gap-2"><span class="msym text-base text-blue-400">bolt</span>GPU inference — 3-5s per query</p>
|
||||
<p class="flex items-center gap-2"><span class="msym text-base text-primary">all_inclusive</span>~900 free queries/day on free tier</p>
|
||||
<p class="flex items-center gap-2"><span class="msym text-base text-on-surface-variant">storage</span>Your PDFs stay local — only text prompt sent</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs text-on-surface-variant block mb-1.5">Account ID</label>
|
||||
<input id="llm-cf-account" type="text" dir="ltr" value="${d.cf_account_id||''}"
|
||||
placeholder="Find in dash.cloudflare.com URL bar"
|
||||
class="w-full bg-surface-high border border-outline-variant rounded-xl px-3 py-2 text-xs font-mono outline-none focus:border-primary transition-colors"/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs text-on-surface-variant block mb-1.5">API Token
|
||||
${d.cf_api_token_configured ? '<span class="text-emerald-400 text-[10px] ml-2">✓ Saved ('+d.cf_api_token_masked+')</span>' : ''}
|
||||
</label>
|
||||
<input id="llm-cf-token" type="password" dir="ltr"
|
||||
placeholder="${d.cf_api_token_configured ? 'Leave blank to keep saved token' : 'Workers AI API token'}"
|
||||
class="w-full bg-surface-high border border-outline-variant rounded-xl px-3 py-2 text-xs font-mono outline-none focus:border-primary transition-colors"/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs text-on-surface-variant block mb-1.5">Model</label>
|
||||
<select id="llm-cf-model" class="w-full bg-surface-high border border-outline-variant rounded-xl px-3 py-2 text-xs outline-none focus:border-primary transition-colors">
|
||||
<option value="@cf/meta/llama-3.1-8b-instruct-fp8-fast" ${(d.cf_model||'').includes('fp8-fast')?'selected':''}>Llama 3.1 8B FP8 Fast ⚡ (Recommended)</option>
|
||||
<option value="@cf/meta/llama-3.1-8b-instruct" ${d.cf_model==='@cf/meta/llama-3.1-8b-instruct'?'selected':''}>Llama 3.1 8B Standard</option>
|
||||
<option value="@cf/meta/llama-3.2-3b-instruct" ${(d.cf_model||'').includes('3.2-3b')?'selected':''}>Llama 3.2 3B (Faster)</option>
|
||||
<option value="@cf/zai-org/glm-4.7-flash" ${(d.cf_model||'').includes('glm')?'selected':''}>GLM-4.7 Flash (100+ languages)</option>
|
||||
<option value="@cf/qwen/qwen3-30b-a3b-fp8" ${(d.cf_model||'').includes('qwen3-30b')?'selected':''}>Qwen 3 30B ⭐ (Best Arabic)</option>
|
||||
<option value="@cf/qwen/qwq-32b" ${(d.cf_model||'').includes('qwq-32b')?'selected':''}>Qwen QwQ 32B (Reasoning)</option>
|
||||
<option value="@cf/deepseek-ai/deepseek-r1-distill-qwen-32b" ${(d.cf_model||'').includes('deepseek-r1')?'selected':''}>DeepSeek R1 32B (Reasoning)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button onclick="saveLLMConfig()" class="flex-1 bg-primary text-surface font-bold py-2.5 rounded-xl text-sm flex items-center justify-center gap-2 hover:shadow-[0_0_20px_rgba(78,222,163,0.4)] transition-all">
|
||||
<span class="msym text-base">save</span> Save
|
||||
</button>
|
||||
<button onclick="testLLMConfig()" class="glass glass-hover px-4 py-2.5 rounded-xl text-sm text-on-surface-variant flex items-center gap-2">
|
||||
<span class="msym text-base">wifi_tethering</span> Test
|
||||
</button>
|
||||
</div>
|
||||
<p id="llm-status" class="text-xs text-center mt-3 hidden"></p>`;
|
||||
} catch(e) { showErr('settings-llm', e.message); }
|
||||
}
|
||||
|
||||
function llmSetProvider(p) {
|
||||
window._llmProvider = p;
|
||||
const isLocal = p !== 'cloudflare', isCF = p === 'cloudflare';
|
||||
document.getElementById('llm-btn-local').className = `flex-1 py-2.5 rounded-xl text-xs font-bold transition-all flex items-center justify-center gap-2 ${isLocal?'bg-primary text-surface':'glass glass-hover text-on-surface-variant'}`;
|
||||
document.getElementById('llm-btn-cf').className = `flex-1 py-2.5 rounded-xl text-xs font-bold transition-all flex items-center justify-center gap-2 ${isCF?'bg-blue-500 text-white':'glass glass-hover text-on-surface-variant'}`;
|
||||
document.getElementById('llm-local-info').classList.toggle('hidden', isCF);
|
||||
document.getElementById('llm-cf-fields').classList.toggle('hidden', isLocal);
|
||||
}
|
||||
|
||||
async function saveLLMConfig() {
|
||||
const status = document.getElementById('llm-status');
|
||||
const payload = {
|
||||
provider: window._llmProvider || 'local',
|
||||
cf_account_id: (document.getElementById('llm-cf-account')?.value||'').trim(),
|
||||
cf_api_token: (document.getElementById('llm-cf-token')?.value||'').trim(),
|
||||
cf_model: document.getElementById('llm-cf-model')?.value || '@cf/meta/llama-3.1-8b-instruct-fp8-fast',
|
||||
};
|
||||
status.textContent = 'Saving...'; status.classList.remove('hidden'); status.style.color = '';
|
||||
try {
|
||||
await api('/admin/llm-config', {method:'POST', body:JSON.stringify(payload)});
|
||||
status.textContent = '✅ Saved — restart API container to apply';
|
||||
status.style.color = '#4edea3';
|
||||
setTimeout(loadLLMConfig, 1500);
|
||||
} catch(e) { status.textContent = '❌ '+e.message; status.style.color = '#f87171'; }
|
||||
}
|
||||
|
||||
async function testLLMConfig() {
|
||||
const status = document.getElementById('llm-status');
|
||||
const payload = {
|
||||
provider: window._llmProvider || 'local',
|
||||
cf_account_id: (document.getElementById('llm-cf-account')?.value||'').trim(),
|
||||
cf_api_token: (document.getElementById('llm-cf-token')?.value||'').trim(),
|
||||
cf_model: document.getElementById('llm-cf-model')?.value || '@cf/meta/llama-3.1-8b-instruct-fp8-fast',
|
||||
};
|
||||
status.textContent = 'Testing...'; status.classList.remove('hidden'); status.style.color = '';
|
||||
try {
|
||||
const r = await api('/admin/llm-test', {method:'POST', body:JSON.stringify(payload)});
|
||||
status.textContent = r.ok ? '✅ '+r.detail : '❌ '+r.detail;
|
||||
status.style.color = r.ok ? '#4edea3' : '#f87171';
|
||||
} catch(e) { status.textContent = '❌ '+e.message; status.style.color = '#f87171'; }
|
||||
}
|
||||
|
||||
// design-taste-frontend: Spotlight border cards
|
||||
document.querySelectorAll('.spotlight-card').forEach(card => {
|
||||
card.addEventListener('mousemove', e => {
|
||||
@@ -1917,5 +2081,73 @@ async function retryMetaExtraction() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<script>
|
||||
// ── Pages Editor ─────────────────────────────────────────────────────────────
|
||||
let _currentPage = 'privacy';
|
||||
|
||||
function switchPageTab(page) {
|
||||
_currentPage = page;
|
||||
['privacy','terms'].forEach(p => {
|
||||
const btn = document.getElementById('pages-tab-' + p);
|
||||
if (p === page) {
|
||||
btn.className = 'px-4 py-2 rounded-xl text-sm font-semibold bg-primary text-surface transition-all';
|
||||
} else {
|
||||
btn.className = 'px-4 py-2 rounded-xl text-sm font-semibold glass text-on-surface-variant hover:text-primary transition-all';
|
||||
}
|
||||
});
|
||||
document.getElementById('pages-preview-btn').href = '/' + page;
|
||||
document.getElementById('pages-save-status').classList.add('hidden');
|
||||
loadPageContent(page);
|
||||
}
|
||||
|
||||
async function loadPageContent(page) {
|
||||
const editor = document.getElementById('pages-editor');
|
||||
editor.value = 'جاري التحميل...';
|
||||
try {
|
||||
const res = await fetch('/admin/pages/' + page, {
|
||||
headers: { 'Authorization': 'Bearer ' + JWT_TOKEN }
|
||||
});
|
||||
const data = await res.json();
|
||||
editor.value = data.content || '';
|
||||
} catch(e) {
|
||||
editor.value = 'خطأ في التحميل: ' + e.message;
|
||||
}
|
||||
}
|
||||
|
||||
async function savePageContent() {
|
||||
const btn = document.getElementById('pages-save-btn');
|
||||
const status = document.getElementById('pages-save-status');
|
||||
const content = document.getElementById('pages-editor').value;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="msym text-sm">progress_activity</span> جاري الحفظ...';
|
||||
try {
|
||||
const res = await fetch('/admin/pages/' + _currentPage, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + JWT_TOKEN },
|
||||
body: JSON.stringify({ content })
|
||||
});
|
||||
if (res.ok) {
|
||||
status.classList.remove('hidden');
|
||||
setTimeout(() => status.classList.add('hidden'), 3000);
|
||||
}
|
||||
} catch(e) {
|
||||
alert('فشل الحفظ: ' + e.message);
|
||||
}
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<span class="msym text-sm">save</span> حفظ';
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const origShowPage = window.showPage;
|
||||
if (origShowPage) {
|
||||
window.showPage = function(page, btn) {
|
||||
origShowPage(page, btn);
|
||||
if (page === 'pages') loadPageContent(_currentPage);
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user