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>