v1.8.6: branding — colors apply instantly, logo upload with size control, fix tracking width
This commit is contained in:
@@ -127,6 +127,7 @@ class BrandingUpdate(BaseModel):
|
||||
site_subtitle: str = None
|
||||
primary_color: str = None
|
||||
secondary_color: str = None
|
||||
logo_size: str = None
|
||||
logo_url: str = None
|
||||
|
||||
class ClaudeKeyUpdate(BaseModel):
|
||||
@@ -669,10 +670,9 @@ def admin_settings(_=Depends(require_admin)):
|
||||
"qdrant_url": QDRANT_URL,
|
||||
"claude_api_configured": bool(os.getenv("CLAUDE_API_KEY", "")),
|
||||
"cache_ttl_hours": 24,
|
||||
"complexity_threshold_words": 30,
|
||||
"confidence_threshold": 0.20, # SCORE_THRESH in brain.py
|
||||
"chunk_size": 150,
|
||||
"chunk_overlap": 200,
|
||||
"chunk_overlap": 2, # sentences (≈30 words)
|
||||
# ── Cloudflare R2 ─────────────────────────────────────────────────────
|
||||
"r2_endpoint": os.getenv("R2_ENDPOINT", ""),
|
||||
"r2_bucket": os.getenv("R2_BUCKET", ""),
|
||||
@@ -753,6 +753,24 @@ def test_r2_connection(_=Depends(require_admin)):
|
||||
except Exception as e:
|
||||
return {"ok": False, "detail": str(e)[:120]}
|
||||
|
||||
|
||||
# ── Upload Logo ────────────────────────────────────────────────────────────────
|
||||
@app.post("/admin/upload-logo")
|
||||
async def upload_logo(payload: dict, _=Depends(require_admin)):
|
||||
import base64, os, uuid, mimetypes
|
||||
data = payload.get("data","")
|
||||
mime_type = payload.get("mime_type","image/png")
|
||||
filename = payload.get("filename","logo.png")
|
||||
if not data:
|
||||
raise HTTPException(status_code=400, detail="No image data")
|
||||
ext = mimetypes.guess_extension(mime_type) or ".png"
|
||||
name = f"logo_{uuid.uuid4().hex[:8]}{ext}"
|
||||
path = f"/opt/rag/static/uploads/{name}"
|
||||
os.makedirs("/opt/rag/static/uploads", exist_ok=True)
|
||||
with open(path, "wb") as f:
|
||||
f.write(base64.b64decode(data))
|
||||
return {"url": f"/static/uploads/{name}"}
|
||||
|
||||
@app.get("/admin/branding")
|
||||
def get_branding(_=Depends(require_admin)):
|
||||
with get_db() as conn:
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -52149,3 +52149,83 @@ $3
|
||||
del
|
||||
$13
|
||||
unacked_mutex
|
||||
*5
|
||||
$3
|
||||
SET
|
||||
$13
|
||||
unacked_mutex
|
||||
$32
|
||||
0ac306665acd11f199e682dacbd0314b
|
||||
$4
|
||||
PXAT
|
||||
$13
|
||||
1779997512054
|
||||
*2
|
||||
$3
|
||||
del
|
||||
$13
|
||||
unacked_mutex
|
||||
*5
|
||||
$3
|
||||
SET
|
||||
$13
|
||||
unacked_mutex
|
||||
$32
|
||||
466013d05acd11f199e682dacbd0314b
|
||||
$4
|
||||
PXAT
|
||||
$13
|
||||
1779997612069
|
||||
*2
|
||||
$3
|
||||
del
|
||||
$13
|
||||
unacked_mutex
|
||||
*5
|
||||
$3
|
||||
SET
|
||||
$13
|
||||
unacked_mutex
|
||||
$32
|
||||
81fcc8c05acd11f199e682dacbd0314b
|
||||
$4
|
||||
PXAT
|
||||
$13
|
||||
1779997712082
|
||||
*2
|
||||
$3
|
||||
del
|
||||
$13
|
||||
unacked_mutex
|
||||
*5
|
||||
$3
|
||||
SET
|
||||
$13
|
||||
unacked_mutex
|
||||
$32
|
||||
bd99e12e5acd11f199e682dacbd0314b
|
||||
$4
|
||||
PXAT
|
||||
$13
|
||||
1779997812097
|
||||
*2
|
||||
$3
|
||||
del
|
||||
$13
|
||||
unacked_mutex
|
||||
*5
|
||||
$3
|
||||
SET
|
||||
$13
|
||||
unacked_mutex
|
||||
$32
|
||||
f936c5125acd11f199e682dacbd0314b
|
||||
$4
|
||||
PXAT
|
||||
$13
|
||||
1779997912111
|
||||
*2
|
||||
$3
|
||||
del
|
||||
$13
|
||||
unacked_mutex
|
||||
|
||||
+66
-11
@@ -484,12 +484,23 @@ a[role=button]:active {
|
||||
<input id="b-subtitle" class="w-full bg-surface-high border border-outline-variant rounded-xl px-4 py-3 text-sm text-on-surface outline-none focus:border-primary transition-colors"/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs text-on-surface-variant mb-2 block">Logo URL</label>
|
||||
<input id="b-logo" placeholder="https://example.com/logo.png" class="w-full bg-surface-high border border-outline-variant rounded-xl px-4 py-3 text-sm text-on-surface outline-none focus:border-primary transition-colors"/>
|
||||
<label class="text-xs text-on-surface-variant mb-2 block">Logo</label>
|
||||
<div class="flex gap-2 mb-2">
|
||||
<input id="b-logo" placeholder="https://example.com/logo.png"
|
||||
class="flex-1 bg-surface-high border border-outline-variant rounded-xl px-4 py-3 text-sm text-on-surface outline-none focus:border-primary transition-colors"
|
||||
oninput="updateLogoPreview()"/>
|
||||
<label class="glass glass-hover px-3 py-2 rounded-xl text-xs text-primary font-bold cursor-pointer flex items-center gap-1 shrink-0">
|
||||
<span class="msym text-sm">upload</span> Upload
|
||||
<input type="file" accept="image/*" class="hidden" onchange="uploadLogo(this)"/>
|
||||
</label>
|
||||
</div>
|
||||
<img id="b-logo-preview" class="hidden rounded-xl mb-2 border border-outline-variant" style="max-width:200px"/>
|
||||
<div class="flex items-center gap-3">
|
||||
<label class="text-xs text-on-surface-variant shrink-0">Size</label>
|
||||
<input type="range" id="b-logo-size" min="20" max="120" value="40" step="4"
|
||||
class="flex-1 accent-primary" oninput="updateLogoPreview()"/>
|
||||
<span id="b-logo-size-val" class="text-xs text-on-surface-variant w-10 text-right">40px</span>
|
||||
</div>
|
||||
<div id="logo-preview" class="hidden">
|
||||
<label class="text-xs text-on-surface-variant mb-2 block">Preview</label>
|
||||
<img id="logo-img" class="h-12 rounded-xl" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="glass rounded-2xl p-6 space-y-5">
|
||||
@@ -515,7 +526,7 @@ a[role=button]:active {
|
||||
<div id="preview-secondary" class="flex-1 h-10 rounded-lg"></div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-on-surface-variant">Changes apply on next page load after saving.</p>
|
||||
<p class="text-xs text-on-surface-variant">Colors apply instantly after saving.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -589,7 +600,6 @@ curl -X POST https://rag.afaqzaid.com/ingest-url \
|
||||
<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>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CLAUDE -->
|
||||
@@ -1300,13 +1310,13 @@ async function loadSettings() {
|
||||
const d = await api('/admin/settings');
|
||||
document.getElementById('settings-ai').innerHTML = `
|
||||
<h3 class="text-primary text-xs font-bold uppercase tracking-widest mb-5 flex items-center gap-2"><span class="msym text-base">smart_toy</span>AI Configuration</h3>
|
||||
${[['Local LLM',d.ollama_model,''],['Claude API',d.claude_api_configured?'✓ Configured':'✗ Not set',d.claude_api_configured?'text-emerald-400':'text-red-400'],['Complexity Threshold',d.complexity_threshold_words+' words',''],['Confidence Threshold',d.confidence_threshold,''],['Cache TTL',d.cache_ttl_hours+'h','']].map(([l,v,cls])=>`
|
||||
${[['Local LLM',d.ollama_model,''],['Claude API',d.claude_api_configured?'✓ Configured':'✗ Not set',d.claude_api_configured?'text-emerald-400':'text-red-400'],['Confidence Threshold',d.confidence_threshold,''],['Cache TTL',d.cache_ttl_hours+'h','']].map(([l,v,cls])=>`
|
||||
<div class="flex justify-between items-center py-3 border-b border-outline-variant/20 last:border-0">
|
||||
<span class="text-sm text-on-surface-variant">${l}</span><span class="text-xs font-mono glass px-2 py-1 rounded-lg ${cls||'text-primary'}">${v}</span>
|
||||
</div>`).join('')}`;
|
||||
document.getElementById('settings-system').innerHTML = `
|
||||
<h3 class="text-primary text-xs font-bold uppercase tracking-widest mb-5 flex items-center gap-2"><span class="msym text-base">build</span>Ingestion Settings</h3>
|
||||
${[['Chunk Size',d.chunk_size+' words'],['Chunk Overlap',d.chunk_overlap+' words'],['Qdrant URL',d.qdrant_url],['Ollama URL',d.ollama_url]].map(([l,v])=>`
|
||||
${[['Chunk Size',d.chunk_size+' words'],['Chunk Overlap',d.chunk_overlap+' sentences'],['Qdrant URL',d.qdrant_url],['Ollama URL',d.ollama_url]].map(([l,v])=>`
|
||||
<div class="flex justify-between items-center py-3 border-b border-outline-variant/20 last:border-0">
|
||||
<span class="text-sm text-on-surface-variant">${l}</span><span class="text-xs font-mono glass px-2 py-1 rounded-lg text-primary truncate max-w-[140px]">${v}</span>
|
||||
</div>`).join('')}`;
|
||||
@@ -1431,6 +1441,39 @@ async function testR2Connection() {
|
||||
} catch(e) { status.textContent = '❌ '+e.message; status.style.color = '#f87171'; }
|
||||
}
|
||||
|
||||
function updateLogoPreview() {
|
||||
const url = document.getElementById('b-logo')?.value;
|
||||
const size = document.getElementById('b-logo-size')?.value || 40;
|
||||
document.getElementById('b-logo-size-val').textContent = size + 'px';
|
||||
const img = document.getElementById('b-logo-preview');
|
||||
if (url) { img.src = url; img.style.height = size + 'px'; img.classList.remove('hidden'); }
|
||||
else img.classList.add('hidden');
|
||||
}
|
||||
|
||||
async function uploadLogo(input) {
|
||||
const file = input.files[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = async (e) => {
|
||||
try {
|
||||
const base64 = e.target.result.split(',')[1];
|
||||
const mime = file.type;
|
||||
const r = await fetch('/admin/upload-logo', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type':'application/json','Authorization':'Bearer '+JWT_TOKEN},
|
||||
body: JSON.stringify({data: base64, mime_type: mime, filename: file.name})
|
||||
});
|
||||
const d = await r.json();
|
||||
if (d.url) {
|
||||
document.getElementById('b-logo').value = d.url;
|
||||
updateLogoPreview();
|
||||
toast('Logo uploaded!', 'success');
|
||||
}
|
||||
} catch(e) { toast('Upload failed: '+e.message); }
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
||||
async function loadBranding() {
|
||||
try {
|
||||
const d = await api('/admin/branding');
|
||||
@@ -1443,6 +1486,13 @@ async function loadBranding() {
|
||||
document.getElementById('b-secondary-hex').value = d.secondary_color||'#95d3ba';
|
||||
document.getElementById('preview-primary').style.background = d.primary_color||'#4edea3';
|
||||
document.getElementById('preview-secondary').style.background = d.secondary_color||'#95d3ba';
|
||||
// Apply colors live to admin CSS variables
|
||||
if(d.primary_color) document.documentElement.style.setProperty('--primary', d.primary_color);
|
||||
if(d.secondary_color) document.documentElement.style.setProperty('--secondary', d.secondary_color);
|
||||
// Logo
|
||||
if(d.logo_url) document.getElementById('b-logo').value = d.logo_url;
|
||||
if(d.logo_size) document.getElementById('b-logo-size').value = d.logo_size;
|
||||
if(d.logo_url) { const img=document.getElementById('b-logo-preview'); img.src=d.logo_url; img.style.height=(d.logo_size||40)+'px'; img.classList.remove('hidden'); }
|
||||
if (d.logo_url) { document.getElementById('logo-img').src=d.logo_url; document.getElementById('logo-preview').classList.remove('hidden'); }
|
||||
document.getElementById('b-primary').oninput = function(){ document.getElementById('b-primary-hex').value=this.value; document.getElementById('preview-primary').style.background=this.value; };
|
||||
document.getElementById('b-secondary').oninput = function(){ document.getElementById('b-secondary-hex').value=this.value; document.getElementById('preview-secondary').style.background=this.value; };
|
||||
@@ -1454,8 +1504,13 @@ async function loadBranding() {
|
||||
|
||||
async function saveBranding() {
|
||||
try {
|
||||
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');
|
||||
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, logo_size:document.getElementById('b-logo-size')?.value||'40', primary_color:document.getElementById('b-primary-hex').value, secondary_color:document.getElementById('b-secondary-hex').value})});
|
||||
if (r.ok) toast('Branding saved!','success');
|
||||
const p=document.getElementById('b-primary-hex')?.value;
|
||||
const s=document.getElementById('b-secondary-hex')?.value;
|
||||
if(p) document.documentElement.style.setProperty('--primary',p);
|
||||
if(s) document.documentElement.style.setProperty('--secondary',s);
|
||||
} else alert('Save failed');
|
||||
} catch(e) { alert('Error: '+e.message); }
|
||||
}
|
||||
|
||||
|
||||
+8
-2
@@ -371,8 +371,14 @@ let currentUser = null;
|
||||
|
||||
// ── Branding ──────────────────────────────────────────────────────────────────
|
||||
fetch('/branding').then(r=>r.json()).then(b=>{
|
||||
if(b.site_name){document.title=b.site_name;document.getElementById('brand-name').textContent=b.site_name;}
|
||||
if(b.site_subtitle) document.getElementById('brand-sub').textContent=b.site_subtitle;
|
||||
if(b.site_name){document.title=b.site_name;const bn=document.getElementById('brand-name');if(bn)bn.textContent=b.site_name;}
|
||||
if(b.site_subtitle){const bs=document.getElementById('brand-sub');if(bs)bs.textContent=b.site_subtitle;}
|
||||
if(b.primary_color) document.documentElement.style.setProperty('--primary', b.primary_color);
|
||||
if(b.secondary_color) document.documentElement.style.setProperty('--secondary', b.secondary_color);
|
||||
if(b.logo_url){
|
||||
const logo=document.getElementById('brand-logo');
|
||||
if(logo){logo.src=b.logo_url;logo.style.height=(b.logo_size||'40')+'px';logo.classList.remove('hidden');}
|
||||
}
|
||||
}).catch(()=>{});
|
||||
|
||||
// ── Counter animation ─────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user