File size: 6,781 Bytes
f85c67a 9bc9167 cc7da9d 9bc9167 f85c67a 9bc9167 6c612c2 f85c67a 9bc9167 cc7da9d 9bc9167 cc7da9d 9bc9167 6c612c2 9bc9167 cc7da9d 9bc9167 cc7da9d 9bc9167 cc7da9d 9bc9167 f85c67a 9bc9167 |
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 |
<!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>
|