|  | <!doctype html> | 
					
						
						|  | <html lang="en"> | 
					
						
						|  | <head> | 
					
						
						|  | <meta charset="utf-8"/> | 
					
						
						|  | <meta name="viewport" content="width=device-width, initial-scale=1"/> | 
					
						
						|  | <title>Local Chat with Transformers.js</title> | 
					
						
						|  | <style> | 
					
						
						|  | body{margin:0;font-family:system-ui,sans-serif;background:#0f1115;color:#e8eaf0;display:grid;grid-template-rows:auto 1fr auto;height:100vh;} | 
					
						
						|  | header,footer{background:#171a21;padding:8px 12px;border-bottom:1px solid #2a2f39;} | 
					
						
						|  | header{display:flex;gap:8px;align-items:center;flex-wrap:wrap;} | 
					
						
						|  | select,button,textarea,input{background:#11141a;color:#e8eaf0;border:1px solid #2a2f39;border-radius:6px;padding:6px 8px;font:inherit;} | 
					
						
						|  | button{cursor:pointer;} | 
					
						
						|  | main{overflow:auto;padding:10px;} | 
					
						
						|  | .chat{display:flex;flex-direction:column;gap:10px;max-width:900px;margin:auto;} | 
					
						
						|  | .msg{display:flex;} | 
					
						
						|  | .msg.user{justify-content:flex-end;} | 
					
						
						|  | .bubble{padding:8px 10px;border-radius:8px;max-width:80%;white-space:pre-wrap;word-break:break-word;} | 
					
						
						|  | .msg.user .bubble{background:#1b2433;} | 
					
						
						|  | .msg.assistant .bubble{background:#141923;} | 
					
						
						|  | .toast{position:fixed;bottom:12px;left:50%;transform:translateX(-50%);background:#11141a;padding:6px 10px;border:1px solid #2a2f39;border-radius:6px;opacity:0;transition:opacity .2s;} | 
					
						
						|  | .toast.show{opacity:1;} | 
					
						
						|  | .modal-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.5);display:grid;place-items:center;} | 
					
						
						|  | .modal{background:#171a21;padding:16px;border-radius:8px;width:min(400px,90%);} | 
					
						
						|  | </style> | 
					
						
						|  | </head> | 
					
						
						|  | <body> | 
					
						
						|  | <header> | 
					
						
						|  | <label for="model">Model:</label> | 
					
						
						|  | <select id="model"> | 
					
						
						|  | <option value="Xenova/distilGPT2">Xenova/distilGPT2</option> | 
					
						
						|  | <option value="Xenova/TinyLlama-1.1B-Chat-v1.0">Xenova/TinyLlama-1.1B-Chat-v1.0</option> | 
					
						
						|  | <option value="Xenova/Mistral-7B-Instruct-v0.2">Xenova/Mistral-7B-Instruct-v0.2</option> | 
					
						
						|  | </select> | 
					
						
						|  | <button id="set-token">Set token</button> | 
					
						
						|  | <span id="status">Idle</span> | 
					
						
						|  | </header> | 
					
						
						|  | <main><div id="chat" class="chat"></div></main> | 
					
						
						|  | <footer> | 
					
						
						|  | <textarea id="input" placeholder="Type here…" rows="2" style="flex:1;"></textarea> | 
					
						
						|  | <button id="send">Send</button> | 
					
						
						|  | </footer> | 
					
						
						|  |  | 
					
						
						|  | <div id="toast" class="toast"></div> | 
					
						
						|  |  | 
					
						
						|  | <div id="token-modal" class="modal-backdrop" hidden> | 
					
						
						|  | <div class="modal"> | 
					
						
						|  | <h3>Enter HF token</h3> | 
					
						
						|  | <input id="token-input" type="password" placeholder="hf_xxx" style="width:100%"/> | 
					
						
						|  | <div style="margin-top:8px;text-align:right;"> | 
					
						
						|  | <button id="token-cancel">Cancel</button> | 
					
						
						|  | <button id="token-save">Save</button> | 
					
						
						|  | </div> | 
					
						
						|  | </div> | 
					
						
						|  | </div> | 
					
						
						|  |  | 
					
						
						|  | <script src="https://cdn.jsdelivr.net/npm/@xenova/transformers"></script> | 
					
						
						|  | <script> | 
					
						
						|  | const { pipeline, env } = window.transformers; | 
					
						
						|  | const chatEl = document.getElementById('chat'); | 
					
						
						|  | const inputEl = document.getElementById('input'); | 
					
						
						|  | const sendBtn = document.getElementById('send'); | 
					
						
						|  | const modelSel = document.getElementById('model'); | 
					
						
						|  | const statusEl = document.getElementById('status'); | 
					
						
						|  | const toastEl = document.getElementById('toast'); | 
					
						
						|  | const tokenModal = document.getElementById('token-modal'); | 
					
						
						|  | const tokenInput = document.getElementById('token-input'); | 
					
						
						|  |  | 
					
						
						|  | let state = { pipe:null, modelId:null, task:'text-generation' }; | 
					
						
						|  | const savedToken = localStorage.getItem('hf_token'); if(savedToken){env.HF_TOKEN=savedToken;} | 
					
						
						|  |  | 
					
						
						|  | function addMessage(role,text){ | 
					
						
						|  | const row=document.createElement('div'); | 
					
						
						|  | row.className='msg '+role; | 
					
						
						|  | const bub=document.createElement('div'); | 
					
						
						|  | bub.className='bubble'; | 
					
						
						|  | bub.textContent=text; | 
					
						
						|  | row.appendChild(bub); | 
					
						
						|  | chatEl.appendChild(row); | 
					
						
						|  | chatEl.scrollTop=chatEl.scrollHeight; | 
					
						
						|  | return bub; | 
					
						
						|  | } | 
					
						
						|  | function showToast(msg){ | 
					
						
						|  | toastEl.textContent=msg; | 
					
						
						|  | toastEl.classList.add('show'); | 
					
						
						|  | setTimeout(()=>toastEl.classList.remove('show'),2000); | 
					
						
						|  | } | 
					
						
						|  | function showTokenModal(){ | 
					
						
						|  | tokenModal.hidden=false; | 
					
						
						|  | tokenInput.value=localStorage.getItem('hf_token')||''; | 
					
						
						|  | } | 
					
						
						|  | function hideTokenModal(){ tokenModal.hidden=true; } | 
					
						
						|  | function setToken(tok){ | 
					
						
						|  | if(tok){ env.HF_TOKEN=tok; localStorage.setItem('hf_token',tok);} | 
					
						
						|  | else{ delete env.HF_TOKEN; localStorage.removeItem('hf_token');} | 
					
						
						|  | } | 
					
						
						|  | function isUnauthorizedError(err){ | 
					
						
						|  | return (err && (err.message||String(err))).includes('Unauthorized access to file'); | 
					
						
						|  | } | 
					
						
						|  | async function withAuthRetry(fn){ | 
					
						
						|  | try { | 
					
						
						|  | return await fn(); | 
					
						
						|  | } catch(e){ | 
					
						
						|  | if(isUnauthorizedError(e)){ | 
					
						
						|  | return new Promise((resolve,reject)=>{ | 
					
						
						|  | showTokenModal(); | 
					
						
						|  | tokenModal.querySelector('#token-save').onclick=()=>{ | 
					
						
						|  | const val=tokenInput.value.trim(); | 
					
						
						|  | hideTokenModal(); | 
					
						
						|  | if(val){ setToken(val); fn().then(resolve).catch(reject);} | 
					
						
						|  | else{ reject(e);} | 
					
						
						|  | }; | 
					
						
						|  | tokenModal.querySelector('#token-cancel').onclick=()=>{ | 
					
						
						|  | hideTokenModal(); | 
					
						
						|  | reject(e); | 
					
						
						|  | }; | 
					
						
						|  | }); | 
					
						
						|  | } | 
					
						
						|  | throw e; | 
					
						
						|  | } | 
					
						
						|  | } | 
					
						
						|  | async function ensurePipeline(modelId,task){ | 
					
						
						|  | if(state.pipe && state.modelId===modelId && state.task===task) return state.pipe; | 
					
						
						|  | statusEl.textContent='Loading model…'; | 
					
						
						|  | const pipe=await withAuthRetry(()=>pipeline(task,modelId,{device:'webgpu'})); | 
					
						
						|  | state.pipe=pipe; state.modelId=modelId; state.task=task; | 
					
						
						|  | statusEl.textContent='Ready'; | 
					
						
						|  | return pipe; | 
					
						
						|  | } | 
					
						
						|  | function isAsyncIterable(obj){ return obj && typeof obj[Symbol.asyncIterator]==='function'; } | 
					
						
						|  | async function generate(text,bubble){ | 
					
						
						|  | const modelId=modelSel.value; | 
					
						
						|  | await ensurePipeline(modelId,'text-generation'); | 
					
						
						|  | const opts={max_new_tokens:128,temperature:0.7,top_p:0.9,repetition_penalty:1.1}; | 
					
						
						|  | let streamObj; try{ streamObj=state.pipe(text,{...opts,stream:true}); }catch{} | 
					
						
						|  | if(isAsyncIterable(streamObj)){ | 
					
						
						|  | let full=''; try{ | 
					
						
						|  | for await (const out of streamObj){ | 
					
						
						|  | const t=(out.token && out.token.text)||out.text||''; full+=t; bubble.textContent=full; | 
					
						
						|  | } | 
					
						
						|  | return; | 
					
						
						|  | }catch(e){ | 
					
						
						|  | if(isUnauthorizedError(e)){ | 
					
						
						|  | await withAuthRetry(()=>generate(text,bubble)); return; | 
					
						
						|  | } | 
					
						
						|  | throw e; | 
					
						
						|  | } | 
					
						
						|  | } | 
					
						
						|  | const out=await withAuthRetry(()=>state.pipe(text,{...opts,stream:false})); | 
					
						
						|  | const textOut=Array.isArray(out)?(out[0]?.generated_text||out[0]?.text): (out.generated_text||out.text)||''; | 
					
						
						|  | bubble.textContent=textOut; | 
					
						
						|  | } | 
					
						
						|  | async function onSend(){ | 
					
						
						|  | const val=inputEl.value.trim(); if(!val) return; | 
					
						
						|  | inputEl.value=''; addMessage('user',val); const bub=addMessage('assistant','…'); | 
					
						
						|  | try { await generate(val,bub); } | 
					
						
						|  | catch(e){ bub.textContent='Error: '+(e.message||e); } | 
					
						
						|  | } | 
					
						
						|  | sendBtn.onclick=onSend; | 
					
						
						|  | inputEl.onkeydown=e=>{ if(e.key==='Enter'&&!e.shiftKey){ e.preventDefault(); onSend(); } }; | 
					
						
						|  | document.getElementById('set-token').onclick=()=>{ | 
					
						
						|  | showTokenModal(); | 
					
						
						|  | tokenModal.querySelector('#token-save').onclick=()=>{ | 
					
						
						|  | const val=tokenInput.value.trim(); | 
					
						
						|  | hideTokenModal(); setToken(val); showToast(val?'Token saved':'Token cleared'); | 
					
						
						|  | }; | 
					
						
						|  | tokenModal.querySelector('#token-cancel').onclick=()=>hideTokenModal(); | 
					
						
						|  | }; | 
					
						
						|  |  | 
					
						
						|  | addMessage('assistant','Hello! Fully local via Transformers.js. Choose model and send a message.'); | 
					
						
						|  | </script> | 
					
						
						|  | </body> | 
					
						
						|  | </html> | 
					
						
						|  |  |