|
|
<!doctype html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="utf-8" /> |
|
|
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1"/> |
|
|
<title>ICIS Mini-Hub β Mobile</title> |
|
|
<style> |
|
|
:root{--bg:#071026;--card:#071827;--accent:#06b6d4;--muted:#94a3b8;--text:#e6eef8} |
|
|
*{box-sizing:border-box} |
|
|
html,body{height:100%;margin:0;background:linear-gradient(180deg,var(--bg),#00101a);font-family:Inter,system-ui,Roboto,Arial} |
|
|
.app{max-width:480px;margin:0 auto;height:100vh;display:flex;flex-direction:column;padding:12px;gap:8px} |
|
|
header{display:flex;align-items:center;gap:10px} |
|
|
.brand{color:var(--text);font-weight:700;font-size:18px} |
|
|
.sub{color:var(--muted);font-size:12px} |
|
|
.chat-window{flex:1;background:transparent;padding:8px;overflow:auto;display:flex;flex-direction:column;gap:8px} |
|
|
.bubble{max-width:86%;padding:10px 12px;border-radius:12px;color:var(--text);line-height:1.3;word-wrap:break-word;white-space:pre-wrap} |
|
|
.user{align-self:flex-end;background:linear-gradient(90deg,#0ea5a4,#06b6d4);border-bottom-right-radius:4px} |
|
|
.bot{align-self:flex-start;background:var(--card);border-bottom-left-radius:4px;color:var(--text);box-shadow:0 2px 8px rgba(2,6,23,.6)} |
|
|
.meta{font-size:11px;color:var(--muted);margin-top:4px} |
|
|
.composer{display:flex;gap:8px;align-items:center;padding:8px;background:transparent} |
|
|
.input{flex:1;display:flex;gap:8px;align-items:center;background:rgba(255,255,255,0.02);padding:8px;border-radius:999px} |
|
|
input[type="text"]{flex:1;background:transparent;border:0;color:var(--text);outline:none;padding:6px 4px;font-size:15px} |
|
|
.icon-btn{width:40px;height:40px;border-radius:10px;border:0;background:transparent;color:var(--muted);display:flex;align-items:center;justify-content:center;cursor:pointer} |
|
|
.icon-btn[disabled]{opacity:0.35;cursor:not-allowed} |
|
|
.send{background:var(--accent);color:#002; padding:8px 12px;border-radius:12px;border:0;font-weight:700} |
|
|
.attachment-preview{display:flex;align-items:center;gap:8px;padding:8px;background:rgba(255,255,255,0.02);border-radius:8px} |
|
|
.remove-btn{background:transparent;border:1px solid rgba(255,255,255,0.04);padding:6px 8px;border-radius:8px;color:var(--muted);cursor:pointer} |
|
|
footer.small{text-align:center;padding:6px 0;color:var(--muted);font-size:12px} |
|
|
@media(min-width:520px){.app{margin-top:30px;border-radius:12px;box-shadow:0 10px 30px rgba(2,6,23,.6);padding:16px}} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<div class="app" role="application"> |
|
|
<header> |
|
|
<div> |
|
|
<div class="brand">ICIS Mini-Hub</div> |
|
|
<div class="sub">Attach first (image/pdf) β add prompt β Send</div> |
|
|
</div> |
|
|
</header> |
|
|
|
|
|
<main class="chat-window" id="chatWindow" aria-live="polite"> |
|
|
<div class="meta small">Tip: Only one file at a time. Attach and optionally remove before sending.</div> |
|
|
</main> |
|
|
|
|
|
<div id="attachArea" class="attachment-area"> |
|
|
|
|
|
</div> |
|
|
|
|
|
<div class="composer" aria-label="Composer"> |
|
|
<div class="input" id="composer"> |
|
|
<button class="icon-btn" id="imgBtn" title="Attach image">πΌοΈ</button> |
|
|
<button class="icon-btn" id="pdfBtn" title="Attach PDF">π</button> |
|
|
|
|
|
<input type="file" id="imgInput" accept="image/*" style="display:none"> |
|
|
<input type="file" id="pdfInput" accept="application/pdf" style="display:none"> |
|
|
|
|
|
<input type="text" id="prompt" placeholder="Type a message or add prompt..." aria-label="Message"> |
|
|
</div> |
|
|
|
|
|
<button class="send" id="sendBtn">Send</button> |
|
|
</div> |
|
|
|
|
|
<footer class="small">Function used will be shown with chat replies; image/pdf give direct concise answers.</footer> |
|
|
</div> |
|
|
|
|
|
<script> |
|
|
|
|
|
const imgBtn = document.getElementById('imgBtn'); |
|
|
const pdfBtn = document.getElementById('pdfBtn'); |
|
|
const imgInput = document.getElementById('imgInput'); |
|
|
const pdfInput = document.getElementById('pdfInput'); |
|
|
const sendBtn = document.getElementById('sendBtn'); |
|
|
const promptIn = document.getElementById('prompt'); |
|
|
const chatWindow = document.getElementById('chatWindow'); |
|
|
const attachArea = document.getElementById('attachArea'); |
|
|
|
|
|
let attachedFile = null; |
|
|
|
|
|
function renderBubble(text, who='bot', meta='') { |
|
|
const b = document.createElement('div'); |
|
|
b.className = 'bubble ' + (who==='user' ? 'user' : 'bot'); |
|
|
b.innerText = text; |
|
|
chatWindow.appendChild(b); |
|
|
if(meta){ |
|
|
const m = document.createElement('div'); |
|
|
m.className = 'meta small'; |
|
|
m.innerText = meta; |
|
|
chatWindow.appendChild(m); |
|
|
} |
|
|
chatWindow.scrollTop = chatWindow.scrollHeight; |
|
|
} |
|
|
|
|
|
function showAttachmentPreview() { |
|
|
attachArea.innerHTML = ''; |
|
|
if(!attachedFile) return; |
|
|
const box = document.createElement('div'); |
|
|
box.className = 'attachment-preview'; |
|
|
if(attachedFile.type === 'image') { |
|
|
const img = document.createElement('img'); |
|
|
img.src = attachedFile.previewUrl; |
|
|
img.style.maxWidth = '72px'; |
|
|
img.style.maxHeight = '72px'; |
|
|
img.style.borderRadius = '8px'; |
|
|
box.appendChild(img); |
|
|
} else { |
|
|
const ico = document.createElement('div'); |
|
|
ico.innerText = 'π'; |
|
|
ico.style.fontSize = '28px'; |
|
|
box.appendChild(ico); |
|
|
} |
|
|
const info = document.createElement('div'); |
|
|
info.style.flex = '1'; |
|
|
info.innerHTML = `<div style="font-weight:600;color:#fff">${attachedFile.file.name}</div> |
|
|
<div style="font-size:12px;color:#9aa9b6">${(attachedFile.file.size/1024|0)} KB β’ ${attachedFile.type.toUpperCase()}</div>`; |
|
|
box.appendChild(info); |
|
|
const remove = document.createElement('button'); |
|
|
remove.className = 'remove-btn'; |
|
|
remove.innerText = 'Remove'; |
|
|
remove.onclick = ()=> { |
|
|
clearAttachment(); |
|
|
}; |
|
|
box.appendChild(remove); |
|
|
attachArea.appendChild(box); |
|
|
} |
|
|
|
|
|
imgBtn.addEventListener('click', ()=> imgInput.click()); |
|
|
pdfBtn.addEventListener('click', ()=> pdfInput.click()); |
|
|
|
|
|
|
|
|
imgInput.addEventListener('change', (e)=>{ |
|
|
const f = e.target.files[0]; |
|
|
if(!f) return; |
|
|
attachedFile = {type:'image', file:f}; |
|
|
|
|
|
attachedFile.previewUrl = URL.createObjectURL(f); |
|
|
pdfBtn.disabled = true; |
|
|
imgBtn.disabled = false; |
|
|
showAttachmentPreview(); |
|
|
|
|
|
}); |
|
|
|
|
|
pdfInput.addEventListener('change', (e)=>{ |
|
|
const f = e.target.files[0]; |
|
|
if(!f) return; |
|
|
attachedFile = {type:'pdf', file:f}; |
|
|
pdfBtn.disabled = false; |
|
|
imgBtn.disabled = true; |
|
|
showAttachmentPreview(); |
|
|
|
|
|
}); |
|
|
|
|
|
function clearAttachment(){ |
|
|
if(attachedFile && attachedFile.previewUrl){ |
|
|
URL.revokeObjectURL(attachedFile.previewUrl); |
|
|
} |
|
|
attachedFile = null; |
|
|
imgInput.value = ''; |
|
|
pdfInput.value = ''; |
|
|
imgBtn.disabled = false; |
|
|
pdfBtn.disabled = false; |
|
|
attachArea.innerHTML = ''; |
|
|
} |
|
|
|
|
|
async function postJSON(url, body){ |
|
|
const resp = await fetch(url, { |
|
|
method:'POST', |
|
|
headers: {'Content-Type':'application/json'}, |
|
|
body: JSON.stringify(body) |
|
|
}); |
|
|
return resp.json(); |
|
|
} |
|
|
|
|
|
async function postForm(url, file, prompt){ |
|
|
const fd = new FormData(); |
|
|
fd.append('file', file); |
|
|
fd.append('prompt', prompt || ''); |
|
|
const resp = await fetch(url, {method:'POST', body:fd}); |
|
|
return resp.json(); |
|
|
} |
|
|
|
|
|
sendBtn.addEventListener('click', async ()=>{ |
|
|
const text = promptIn.value.trim(); |
|
|
|
|
|
if(!text && !attachedFile) return; |
|
|
|
|
|
|
|
|
if(text) renderBubble(text, 'user'); |
|
|
|
|
|
try { |
|
|
let result = null; |
|
|
if(attachedFile){ |
|
|
|
|
|
if(attachedFile.type === 'image'){ |
|
|
result = await postForm('/analyze_image', attachedFile.file, text); |
|
|
if(result.error) renderBubble(result.error, 'bot'); |
|
|
else renderBubble(result.response || 'No response', 'bot', 'Direct (image)'); |
|
|
} else { |
|
|
result = await postForm('/summarize_pdf', attachedFile.file, text); |
|
|
if(result.error) renderBubble(result.error, 'bot'); |
|
|
else renderBubble(result.response || 'No response', 'bot', 'Direct (pdf)'); |
|
|
} |
|
|
|
|
|
clearAttachment(); |
|
|
} else { |
|
|
|
|
|
const json = await postJSON('/chat', {query: text}); |
|
|
if(json.error) renderBubble(json.error, 'bot'); |
|
|
else { |
|
|
const func = json.function_used || 'chat'; |
|
|
const resp = json.response || 'No response'; |
|
|
renderBubble(resp, 'bot', 'Function: ' + func); |
|
|
} |
|
|
} |
|
|
} catch (err) { |
|
|
console.error(err); |
|
|
renderBubble('Network or server error', 'bot'); |
|
|
} |
|
|
|
|
|
promptIn.value = ''; |
|
|
}); |
|
|
|
|
|
|
|
|
promptIn.addEventListener('keydown', (e)=>{ |
|
|
if(e.key === 'Enter' && !e.shiftKey){ |
|
|
e.preventDefault(); |
|
|
sendBtn.click(); |
|
|
} |
|
|
}); |
|
|
</script> |
|
|
</body> |
|
|
</html> |
|
|
|