Spaces:
Running
Running
File size: 21,573 Bytes
5268478 cc0b604 607f448 cc0b604 553a779 f77f345 607f448 cc0b604 607f448 cc0b604 c0fde57 607f448 c0fde57 cc0b604 607f448 cc0b604 607f448 cc0b604 607f448 cc0b604 607f448 cc0b604 607f448 cc0b604 607f448 cc0b604 553a779 a4135ee cc0b604 a4135ee cc0b604 69d6e46 cc0b604 607f448 cc0b604 607f448 cc0b604 9175a61 48ce3d9 cc0b604 a4135ee cc0b604 584b12d cc0b604 48ce3d9 cc0b604 957076d cc0b604 957076d cc0b604 5268478 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 |
<!doctype html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>MultiModel AI — Multi‑Column Chat</title>
<meta name="description" content="Ask once, view Gemini, OpenAi, Meta, and Alibaba in tabs on mobile and 4 columns on desktop." />
<meta name="theme-color" content="#0b0b0e" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800&display=swap" rel="stylesheet" />
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = { theme: { extend: { colors: { brand: { DEFAULT: '#22c55e', foreground: '#0b0b0e' }, accent: '#60a5fa' }, fontFamily: { sans: ['Inter','ui-sans-serif','system-ui'] } } } };
</script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.1.7/dist/purify.min.js"></script>
<style>:root{color-scheme:dark}html,body{height:100%}html{font-size:12px}
.bubble table{width:100%;border-collapse:collapse;margin-top:.5rem}
.bubble th,.bubble td{border:1px solid rgba(255,255,255,.15);padding:.5rem;vertical-align:top}
.bubble th{background:rgba(255,255,255,.06);font-weight:600}
.bubble pre{background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.1);padding:.75rem;border-radius:.5rem;overflow:auto}
.bubble code{font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace}
.scroll-area{scroll-behavior:smooth; overscroll-behavior:contain}
.scroll-area::-webkit-scrollbar{height:10px;width:10px}
.scroll-area::-webkit-scrollbar-thumb{background:rgba(255,255,255,.12);border-radius:8px}
.scroll-area::-webkit-scrollbar-track{background:transparent}
@keyframes caretBlink{50%{opacity:.2}}
.caret{display:inline-block;width:2px;height:1em;background:rgba(255,255,255,.8);vertical-align:-0.15em;animation:caretBlink 1s steps(2,start) infinite;margin-left:2px}
</style>
</head>
<body class="bg-neutral-950 text-white font-sans antialiased selection:bg-brand/30">
<div class="flex min-h-screen">
<!-- Main -->
<div class="flex-1 flex flex-col min-w-0">
<header class="sticky top-0 z-40 border-b border-white/10 backdrop-blur supports-[backdrop-filter]:bg-neutral-950/70">
<div class="px-4 sm:px-6 lg:px-8 h-12 flex items-center justify-between gap-4">
<div class="inline-flex items-center gap-2">
<span class="inline-flex h-8 w-8 items-center justify-center rounded-lg bg-brand/20 text-brand">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-5 h-5"><path d="M4 5a2 2 0 0 1 2-2h1l1-1h6l1 1h1a2 2 0 0 1 2 2v3H4V5Zm0 5h16v7a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-7Zm5 2a1 1 0 1 0 0 2h6a1 1 0 1 0 0-2H9Z"/></svg>
</span>
<span class="text-base font-extrabold tracking-tight">MultiModel AI</span>
</div>
<div id="tabs" class="inline-flex rounded-lg bg-white/5 p-0.5 text-white/70 text-xs font-medium border border-white/10 shadow-[inset_0_0_0_1px_rgba(255,255,255,0.06)]">
<button data-tab="All" class="tab px-1.5 py-0.5 rounded bg-white/10 text-white">All</button>
<button data-tab="Gemini" class="tab px-1.5 py-0.5 rounded hover:bg-white/10">Gemini</button>
<button data-tab="OpenAi" class="tab px-1.5 py-0.5 rounded hover:bg-white/10">OpenAi</button>
<button data-tab="Meta" class="tab px-1.5 py-0.5 rounded hover:bg-white/10">Meta</button>
<button data-tab="Alibaba" class="tab px-1.5 py-0.5 rounded hover:bg-white/10">Alibaba</button>
</div>
</div>
</header>
<main class="flex-1 flex flex-col">
<!-- Tabs (mobile and desktop) -->
<!-- Chat area -->
<div id="chat" class="relative flex-1 overflow-hidden">
<!-- Mobile panes -->
<div id="mobile-panes" class="lg:hidden h-full overflow-y-auto p-2.5 pb-20 relative scroll-area">
<div id="pane-All" class="pane flex flex-col gap-3"></div>
<div id="pane-Gemini" class="pane hidden flex flex-col gap-3"></div>
<div id="pane-OpenAi" class="pane hidden flex flex-col gap-3"></div>
<div id="pane-Meta" class="pane hidden flex flex-col gap-3"></div>
<div id="pane-Alibaba" class="pane hidden flex flex-col gap-3"></div>
</div>
<!-- Desktop 4 columns -->
<div id="grid" class="hidden lg:grid grid-cols-1 xl:grid-cols-4 gap-3 h-full p-3 pb-20 scroll-area">
<!-- Column template instances -->
<div class="col flex flex-col rounded-xl border border-white/10 bg-white/[0.03] overflow-hidden">
<div class="h-10 border-b border-white/10 px-3 flex items-center gap-2"><span class="h-7 w-7 inline-flex items-center justify-center rounded-full bg-gradient-to-br from-fuchsia-500/30 to-sky-500/30 ring-1 ring-white/10"><img src="https://cdn.simpleicons.org/google/ffffff" alt="Gemini" class="h-4 w-4"/></span><span class="font-semibold">Gemini</span></div>
<div id="col-Gemini" class="flex-1 overflow-y-auto p-2.5 pb-20 flex flex-col gap-2 scroll-area"></div>
</div>
<div class="col flex flex-col rounded-xl border border-white/10 bg-white/[0.03] overflow-hidden">
<div class="h-10 border-b border-white/10 px-3 flex items-center gap-2"><span class="h-7 w-7 inline-flex items-center justify-center rounded-full bg-emerald-500/30 ring-1 ring-white/10"><img src="https://cdn.simpleicons.org/openai/ffffff" alt="OpenAI" class="h-4 w-4"/></span><span class="font-semibold">OpenAi</span></div>
<div id="col-OpenAi" class="flex-1 overflow-y-auto p-2.5 pb-20 flex flex-col gap-2 scroll-area"></div>
</div>
<div class="col flex flex-col rounded-xl border border-white/10 bg-white/[0.03] overflow-hidden">
<div class="h-10 border-b border-white/10 px-3 flex items-center gap-2"><span class="h-7 w-7 inline-flex items-center justify-center rounded-full bg-blue-500/30 ring-1 ring-white/10"><img src="https://cdn.simpleicons.org/meta/ffffff" alt="Meta" class="h-4 w-4"/></span><span class="font-semibold">Meta</span></div>
<div id="col-Meta" class="flex-1 overflow-y-auto p-2.5 pb-20 flex flex-col gap-2 scroll-area"></div>
</div>
<div class="col flex flex-col rounded-xl border border-white/10 bg-white/[0.03] overflow-hidden">
<div class="h-10 border-b border-white/10 px-3 flex items-center gap-2"><span class="h-7 w-7 inline-flex items-center justify-center rounded-full bg-amber-500/30 ring-1 ring-white/10"><img src="https://cdn.simpleicons.org/alibabacloud/ffffff" alt="Alibaba" class="h-4 w-4"/></span><span class="font-semibold">Alibaba</span></div>
<div id="col-Alibaba" class="flex-1 overflow-y-auto p-2.5 pb-20 flex flex-col gap-2 scroll-area"></div>
</div>
</div>
<!-- Composer -->
<div class="absolute bottom-0 left-0 right-0 border-t border-white/10 bg-neutral-950/80 backdrop-blur p-1">
<div class="relative max-w-2xl mx-auto">
<textarea id="prompt" class="w-full h-10 resize-none rounded-lg bg-white/5 border border-white/10 pl-3 pr-44 pt-2.5 mt-1 text-sm placeholder:text-white/40 focus:outline-none focus:ring-2 focus:ring-brand" placeholder="Ask anything"></textarea>
<button id="send" class="absolute right-3 top-1/2 -translate-y-1/2 -mt-1 inline-flex h-6 items-center gap-2 justify-center rounded-lg bg-gradient-to-r from-[#22c55e] to-[#16a34a] px-3 text-sm font-semibold text-black shadow-lg shadow-emerald-500/20 hover:brightness-105 focus:outline-none focus:ring-2 focus:ring-emerald-400/60 transition-colors">Send
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-3 h-3"><path d="M3 12l18-9-9 18-1.8-6.2L3 12z"/></svg>
</button>
<button id="setPrompt" class="absolute right-24 top-1/2 -translate-y-1/2 -mt-1 inline-flex h-6 items-center gap-2 justify-center rounded-lg bg-white/10 px-2 text-sm font-medium text-white hover:bg-white/15 border border-white/10 backdrop-blur-sm transition-colors duration-150"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-3 h-3"><path d="M11 2l1.5 3.5L16 7l-3.5 1.5L11 12l-1.5-3.5L6 7l3.5-1.5L11 2zM18 14l1 2 2 1-2 1-1 2-1-2-2-1 2-1 1-2z"/></svg> Set Prompt</button>
</div>
</div>
</div>
<div id="status" class="hidden px-4 sm:px-6 lg:px-8 pb-4 text-sm"></div>
<footer class="px-4 sm:px-6 lg:px-8 py-3 text-xs text-white/60 border-t border-white/10">
<span class="mr-2">Developer: Hamza</span>
<a href="https://github.com/MuhammadHamza123c" target="_blank" rel="noopener noreferrer" class="underline hover:text-white">GitHub</a>
<span class="mx-2">•</span>
<a href="https://www.linkedin.com/in/muhammad-hamzads/" target="_blank" rel="noopener noreferrer" class="underline hover:text-white">LinkedIn</a>
<span class="mx-2">•</span>
<a href="mailto:muhammadhamzao241@gmail.com" class="underline hover:text-white">Gmail</a>
</footer>
</main>
</div>
</div>
<template id="msg-template">
<div class="msg group flex items-start gap-2">
<div class="avatar h-8 w-8 shrink-0 rounded-full bg-white/10 flex items-center justify-center text-xs font-semibold"></div>
<div class="max-w-[85%]">
<div class="header flex items-center gap-2 mb-1">
<span class="name text-sm font-semibold"></span>
<span class="time text-xs text-white/50"></span>
<button class="copy ml-auto hidden md:inline-flex items-center gap-1 px-2.5 py-1 rounded-md bg-white/5 border border-white/10 text-xs hover:bg-white/10 transition-opacity opacity-0 group-hover:opacity-100 pointer-events-none group-hover:pointer-events-auto"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-3.5 h-3.5"><path d="M16 1H4a2 2 0 0 0-2 2v12h2V3h12V1zm3 4H8a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2z"/></svg>Copy</button>
</div>
<div class="bubble rounded-2xl px-3 py-2 text-sm leading-relaxed whitespace-pre-wrap"></div>
</div>
</div>
</template>
<script>
const BASE='https://46c813706ce2.ngrok-free.app';
const ENDPOINT='/MultiModel';
const MODELS=['Gemini','OpenAi','Meta','Alibaba'];
const LOGOS={
Gemini:'https://cdn.simpleicons.org/google/ffffff',
OpenAi:'https://cdn.simpleicons.org/openai/ffffff',
Meta:'https://cdn.simpleicons.org/meta/ffffff',
Alibaba:'https://cdn.simpleicons.org/alibabacloud/ffffff'
};
const AVATAR_BG={
Gemini:'bg-gradient-to-br from-fuchsia-500/30 to-sky-500/30',
OpenAi:'bg-emerald-500/30',
Meta:'bg-blue-500/30',
Alibaba:'bg-amber-500/30'
};
const promptEl=document.getElementById('prompt');
const sendBtn=document.getElementById('send');
const setBtn=document.getElementById('setPrompt');
const statusEl=document.getElementById('status');
const exportBtn=document.getElementById('export');
const usageEl=document.getElementById('usage');
let lastResponse=null; let chats=0; function bumpUsage(){ chats++; if(usageEl) usageEl.textContent=chats+'/25'; }
function setStatus(msg,type='info'){
const base='fixed inset-0 z-50 flex items-center justify-center text-center px-4 pointer-events-none';
const font='text-base sm:text-lg font-semibold';
const color=(type==='error'?'text-red-400':'text-white/80');
statusEl.className=base+' '+font+' '+color;
statusEl.textContent=msg;
statusEl.classList.remove('hidden');
}
function clearStatus(){ statusEl.classList.add('hidden'); }
function pane(id){ return document.getElementById('pane-'+id); }
function col(id){ return document.getElementById('col-'+id); }
function now(){ return new Date().toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'}); }
function decodeEntities(s){const ta=document.createElement('textarea'); ta.innerHTML=s; return ta.value;}
function scrollToBottom(el){ try{ el.scrollTo({top: el.scrollHeight, behavior:'smooth'}); }catch{ el.scrollTop = el.scrollHeight; } }
function typeAssistant(el, raw, done){
const tokens = raw.split(/(\s+)/); // keep spaces
const total = tokens.length; const perTick = Math.max(1, Math.ceil(total/180)); // ~180 steps max
el.textContent='';
const caret=document.createElement('span'); caret.className='caret'; el.appendChild(caret);
let i=0; function tick(){
for(let k=0;k<perTick && i<total;k++,i++){
// insert before caret to keep it at the end
caret.before(document.createTextNode(tokens[i]));
}
const scroller = el.closest('.overflow-y-auto, .scroll-area'); if(scroller) scrollToBottom(scroller);
if(i<total){ requestAnimationFrame(()=> setTimeout(tick, 16)); }
else { caret.remove(); done && done(); }
}
tick();
}
function addMessageTo(container,{role,name,text}){
const tpl=document.getElementById('msg-template'); const node=tpl.content.firstElementChild.cloneNode(true);
const avatar=node.querySelector('.avatar'); const nameEl=node.querySelector('.name'); const timeEl=node.querySelector('.time'); const bubble=node.querySelector('.bubble'); const copyBtn=node.querySelector('.copy');
nameEl.textContent=name; timeEl.textContent=now();
if(role==='user'){
node.classList.add('flex-row-reverse');
avatar.innerHTML='<span class="inline-flex h-8 w-8 items-center justify-center rounded-full bg-white/10 ring-1 ring-white/10 text-white/70"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="h-4 w-4"><path d="M12 12a5 5 0 1 0-5-5 5 5 0 0 0 5 5Zm0 2c-5 0-9 2.5-9 5.5V22h18v-2.5C21 16.5 17 14 12 14Z"/></svg></span>';
bubble.className+=' bg-brand text-black rounded-br-md';
node.querySelector('.header').classList.add('flex-row-reverse','gap-2');
copyBtn.classList.add('hidden');
}
else { avatar.innerHTML=`<span class="inline-flex h-8 w-8 items-center justify-center rounded-full ${AVATAR_BG[name]||'bg-white/10'} ring-1 ring-white/10"><img src="${LOGOS[name]||'https://cdn.simpleicons.org/circle/ffffff'}" alt="${name}" class="h-4 w-4"/></span>`; bubble.className+=' bg-white/5 border border-white/10'; copyBtn.addEventListener('click',()=>{navigator.clipboard.writeText(text); setStatus('Copied '+name+' response.'); setTimeout(clearStatus,1200);}); }
// Markdown rendering with sanitization (supports tables, lists, code)
const raw=decodeEntities(text).replace(/\u0000/g,'');
if(role==='user'){
bubble.innerHTML=DOMPurify.sanitize(raw).replace(/\n/g,'<br/>');
} else {
// type plain text first, then replace with rich markdown
typeAssistant(bubble, raw, ()=>{ marked.setOptions({gfm:true, breaks:true}); bubble.innerHTML = DOMPurify.sanitize(marked.parse(raw)); });
}
container.appendChild(node);
// autoscroll
const scrollEl = container.closest('.overflow-y-auto, .scroll-area') || container;
if(scrollEl) { scrollToBottom(scrollEl); }
}
function addMessage(targets,{role,name,text}){
// mobile panes
targets.forEach(t=>{ const p=pane(t); if(p) addMessageTo(p,{role,name,text}); });
// desktop columns (skip All)
targets.filter(t=>t!=='All').forEach(t=>{ const c=col(t); if(c) addMessageTo(c,{role,name,text}); });
}
function setActive(tab){
document.querySelectorAll('.tab').forEach(b=>{ b.classList.remove('bg-white/10','text-white'); if(b.dataset.tab===tab) b.classList.add('bg-white/10','text-white'); });
// Mobile panes
document.querySelectorAll('.pane').forEach(p=>p.classList.add('hidden')); if(pane(tab)) pane(tab).classList.remove('hidden');
// Desktop grid toggle
const grid=document.getElementById('grid');
if(!grid) return;
const cols=[...grid.querySelectorAll('.col')];
if(tab==='All'){
grid.classList.remove('xl:grid-cols-1');
grid.classList.add('xl:grid-cols-4');
cols.forEach(c=>{ c.classList.remove('hidden'); c.classList.remove('xl:col-span-4'); });
requestAnimationFrame(()=> scrollToBottom(grid));
} else {
grid.classList.remove('xl:grid-cols-4');
grid.classList.add('xl:grid-cols-1');
cols.forEach(c=>{ const isMatch=c.querySelector('.font-semibold')?.textContent===tab; c.classList.toggle('hidden', !isMatch); c.classList.toggle('xl:col-span-4', isMatch); if(isMatch){ const target=document.getElementById('col-'+tab); requestAnimationFrame(()=> scrollToBottom(target)); } });
}
}
async function setPrompt(){
const q=promptEl.value.trim(); if(!q){ setStatus('Enter text to clean first.', 'error'); return; }
setStatus('Cleaning prompt...', 'info');
setBtn?.setAttribute('disabled','true'); setBtn && (setBtn.textContent='Cleaning...');
try{
const url=BASE+'/set_prompt?text='+encodeURIComponent(q);
const res=await fetch(url,{method:'GET', headers:{'Accept':'application/json','ngrok-skip-browser-warning':'1'}});
if(!res.ok) throw new Error('HTTP '+res.status);
const ct=res.headers.get('content-type')||'';
let data; if(ct.includes('application/json')){ data=await res.json(); } else { const txt=await res.text(); try{ data=JSON.parse(txt); } catch{ data={ _raw: txt }; } }
let cleaned = data['Clean Prompt'] || data.cleanPrompt || data.cleaned || '';
if(!cleaned && typeof data._raw==='string'){
const m=/\"Clean Prompt\"\s*:\s*\"([\s\S]*?)\"/.exec(data._raw); if(m) cleaned=m[1];
}
if(cleaned){ promptEl.value=cleaned; promptEl.focus(); promptEl.setSelectionRange(cleaned.length, cleaned.length); setStatus('Prompt set.', 'info'); setTimeout(clearStatus, 1000); }
else { setStatus('No cleaned prompt returned (got non‑JSON or HTML).', 'error'); }
} catch(e){ setStatus('Set Prompt failed: '+(e.message||e), 'error'); }
finally { setBtn?.removeAttribute('disabled'); setBtn && (setBtn.textContent='Set Prompt'); }
}
async function query(){
const q=promptEl.value.trim(); if(!q){setStatus('Enter a message.', 'error');return}
clearStatus(); bumpUsage(); const targets=['All',...MODELS];
addMessage(targets,{role:'user', name:'You', text:q}); promptEl.value=''; sendBtn.disabled=true;
try{
const url=BASE+ENDPOINT+'?text='+encodeURIComponent(q);
const res=await fetch(url,{method:'GET', headers:{'Accept':'application/json','ngrok-skip-browser-warning':'1'}});
if(!res.ok) throw new Error('HTTP '+res.status);
const ct=res.headers.get('content-type')||'';
let data;
if(ct.includes('application/json')){ data=await res.json(); }
else { const txt=await res.text(); try{ data=JSON.parse(txt); } catch{ data={ _raw: txt }; } }
lastResponse=data;
const keys = Object.keys(data).filter(k=>k!=="_raw");
if(!keys.length){
if(data._raw?.startsWith('<!DOCTYPE')) setStatus('Server returned HTML (likely CORS/ngrok warning). Enable CORS or add allowed origin.', 'error');
else setStatus('No JSON payload from server.', 'error');
return;
}
for(const key of keys){ const text=String(data[key]||''); addMessage(['All', key], {role:'assistant', name:key, text}); }
}catch(e){ setStatus('Request failed. Check server/CORS. '+(e.message||e),'error'); }
finally{ sendBtn.disabled=false; }
}
// Tabs (mobile)
document.addEventListener('click', (e)=>{ const btn=e.target.closest('.tab'); if(btn){ setActive(btn.dataset.tab); } }); setActive('All');
// Actions
sendBtn.addEventListener('click',query); setBtn?.addEventListener('click', setPrompt); promptEl.addEventListener('keydown',e=>{ if(e.key==='Enter'){ if(e.shiftKey){ return; } e.preventDefault(); query(); } });
exportBtn?.addEventListener('click',()=>{ if(!lastResponse){setStatus('Nothing to export.', 'error');return} const blob=new Blob([JSON.stringify(lastResponse,null,2)],{type:'application/json'}); const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download='multimodel-response.json'; a.click(); });
document.getElementById('newChat')?.addEventListener('click',()=>{ document.querySelectorAll('.pane').forEach(p=>p.innerHTML=''); ['Gemini','OpenAi','Meta','Alibaba'].forEach(m=>{ const c=col(m); if(c) c.innerHTML=''; }); clearStatus(); });
</script>
</body>
</html>
|