File size: 16,233 Bytes
df016b4
 
 
 
c58fb03
 
 
df016b4
c58fb03
df016b4
c58fb03
 
 
 
 
 
 
 
 
 
 
 
 
 
 
df016b4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76dda73
 
 
c58fb03
 
 
df016b4
 
 
 
 
 
c58fb03
df016b4
 
 
 
 
 
 
 
 
 
c58fb03
df016b4
 
 
 
 
 
 
 
 
 
 
c58fb03
df016b4
c58fb03
df016b4
 
 
c58fb03
 
 
df016b4
 
c58fb03
df016b4
 
c58fb03
df016b4
 
 
 
 
 
 
 
 
76dda73
 
 
 
 
 
df016b4
 
c58fb03
df016b4
 
 
 
 
c58fb03
 
df016b4
4c0002a
 
 
df016b4
 
4c0002a
c58fb03
df016b4
 
 
76dda73
 
 
 
 
1fca061
76dda73
 
 
 
1fca061
 
 
 
 
 
 
 
 
 
76dda73
 
 
 
1fca061
76dda73
 
 
 
1fca061
76dda73
 
 
 
1fca061
76dda73
1fca061
76dda73
1fca061
 
 
 
 
76dda73
 
 
 
1fca061
76dda73
 
 
 
1fca061
76dda73
 
 
 
1fca061
76dda73
 
 
df016b4
 
 
 
 
 
 
 
 
 
 
 
 
76dda73
 
df016b4
c58fb03
 
 
 
df016b4
 
4c0002a
8f84bc2
c58fb03
8f84bc2
 
4c0002a
df016b4
 
c58fb03
 
 
 
 
 
 
 
df016b4
 
 
 
 
c58fb03
 
df016b4
 
76dda73
 
4c0002a
76dda73
 
 
 
 
 
 
 
 
 
 
 
df016b4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c58fb03
 
 
4c0002a
 
 
 
 
 
df016b4
c58fb03
df016b4
c58fb03
 
 
 
df016b4
 
 
 
 
 
 
 
 
 
 
 
 
 
c58fb03
 
 
 
df016b4
 
 
c58fb03
df016b4
 
 
 
c58fb03
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
df016b4
 
 
 
c58fb03
 
 
df016b4
 
c58fb03
 
 
 
 
 
 
df016b4
c58fb03
df016b4
c58fb03
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
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta
    name="viewport"
    content="width=device-width,initial-scale=1,viewport-fit=cover" />
  <title>Qwen2.5‑Coder‑3B‑Instruct — WebGPU (ONNX)</title>
  <meta name="color-scheme" content="dark light" />
  <style>
    :root{
      --bg:#0b1021; --panel:#0e1430; --text:#e9eeff; --muted:#9fb3ff; --accent:#7aa2ff;
      --ok:#2ecc71; --warn:#f39c12; --err:#e74c3c; --radius:14px; --border:1px solid rgba(255,255,255,.08)
    }
    *{box-sizing:border-box}
    body{
      margin:0;
      background:
        radial-gradient(1200px 800px at 10% -10%,#1a2252 0%,transparent 60%),
        radial-gradient(1200px 800px at 110% 10%,#1b2d61 0%,transparent 60%),
        var(--bg);
      color:var(--text);
      font:16px/1.6 system-ui,-apple-system,Segoe UI,Roboto,Inter,Arial,sans-serif;
      padding:20px
    }
    header{display:flex;gap:16px;align-items:center;justify-content:space-between;padding:14px 16px;border-radius:var(--radius);background:linear-gradient(180deg,rgba(255,255,255,.06),rgba(255,255,255,.02));border:var(--border)}
    h1{margin:0;font-size:22px}
    .muted{color:var(--muted);font-size:12px}
    .row{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
    .card{background:linear-gradient(180deg,rgba(255,255,255,.06),rgba(255,255,255,.02));border:var(--border);border-radius:var(--radius);padding:14px;margin-top:16px}
    textarea{width:100%;height:180px;background:var(--panel);border:var(--border);border-radius:12px;color:var(--text);padding:10px}
    input[type="text"]{width:100%;background:var(--panel);border:var(--border);border-radius:12px;color:var(--text);padding:10px}
    button{appearance:none;border:0;border-radius:12px;padding:10px 14px;cursor:pointer;font-weight:600;background:linear-gradient(180deg,#7aa2ff,#4e77ff);color:white}
    button.secondary{background:linear-gradient(180deg,#7780a6,#5a6284)}
    button.ghost{background:transparent;border:var(--border)}
    #chat{display:flex;flex-direction:column;gap:8px;height:280px;overflow:auto;border-radius:12px;background:var(--panel);border:var(--border);padding:10px}
    .msg{padding:10px;border-radius:10px;max-width:90%}
    .me{background:#1b2553;align-self:flex-end}
    .bot{background:#0b143f;align-self:flex-start}
    #log{background:#060a1d;color:#b8d0ff;border:var(--border);border-radius:12px;height:140px;overflow:auto;padding:8px;font-size:12px}
    .chip{display:inline-flex;align-items:center;gap:6px;border-radius:999px;padding:6px 10px;border:var(--border)}
    .ok{background:rgba(46,204,113,.12);border-color:rgba(46,204,113,.35)}
    .warn{background:rgba(243,156,18,.12);border-color:rgba(243,156,18,.35)}
    .err{background:rgba(231,76,60,.12);border-color:rgba(231,76,60,.35)}
    .examples{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:10px;margin-top:10px}
    .examples button{justify-content:flex-start}
    .toggle{display:flex;align-items:center;gap:8px}
    progress{width:220px;height:10px}
    a.link{color:var(--accent);text-decoration:none}
    a.link:hover{text-decoration:underline}
  </style>
</head>
<body>
  <header>
    <div>
      <h1>Qwen2.5‑Coder‑3B‑Instruct — WebGPU (ONNX)</h1>
      <div class="muted">Pure front‑end · Transformers.js v3 + ONNX Runtime Web · WebGPU preferred</div>
    </div>
    <div class="row">
      <button id="load">Load model</button>
      <button id="cancel" class="secondary" style="display:none;">Cancel (UI reset only)</button>
      <button id="clearlog" class="ghost">Clear log</button>
    </div>
  </header>

  <div class="card">
    <div class="row" style="margin-bottom:8px">
      <span class="chip warn">First run will download ~2.1 GB (q4 ONNX weights). Desktop Chrome/Edge recommended. HTTPS or localhost required for WebGPU.</span>
    </div>

    <!-- Confirmation step -->
    <div id="confirm" style="display:none">
      <p>This will download model files (~2.1 GB, quantized q4) to your browser cache and run inference via WebGPU. Proceed?</p>
      <div class="row">
        <button id="confirm-yes">Yes, download now</button>
        <button id="confirm-no" class="secondary">No, go back</button>
      </div>
    </div>

    <!-- Loading / progress -->
    <div id="loading" style="display:none">
      <div class="row" style="align-items:center">
        <progress id="progress" value="0" max="100"></progress>
        <span id="progress-label" class="muted">0%</span>
      </div>
      <div class="muted">
        WebGPU works on secure contexts (HTTPS/localhost). Safari/Firefox may need experimental flags; fallback is WASM/CPU.
      </div>
    </div>

    <!-- Chat UI -->
    <div id="ui" style="display:none">
      <input id="sys" type="text" placeholder="System prompt (optional): You are a senior coding assistant. Keep answers concise." />
      <div id="chat" aria-live="polite" aria-busy="false"></div>
      <textarea id="prompt" placeholder="Ask something (you can paste code)…"></textarea>
      <div class="row">
        <button id="send">Send</button>
        <button id="stop" class="ghost" disabled>Stop</button>
        <span id="status" class="chip" style="display:none"></span>
      </div>
    </div>
  </div>

  <div class="card">
    <h3 style="margin:6px 0">Examples</h3>
    <div class="toggle"><input id="autorun" type="checkbox" checked/> <label for="autorun" class="muted">Auto‑run on click</label></div>
    <div id="examples" class="examples"></div>
  </div>

  <div class="card">
    <h3 style="margin:6px 0">Debug log</h3>
    <div id="log" role="log" aria-live="polite"></div>
  </div>

  <script type="module">
    // ================= Configuration =================
    const MODEL_ID = 'onnx-community/Qwen2.5-Coder-3B-Instruct';
    // 固定到 v3 稳定版(包含 TextStreamer / InterruptableStoppingCriteria / WebGPU 支持)
    const TRANSFORMERS_CDN = 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.0.0';

    // Prefer WebGPU; gracefully fall back if unavailable
    const device = ('gpu' in navigator) ? 'webgpu' : 'auto';

    // Smallest first: q4 → q4f16 → int8 → fp16
    const options = {
      device,                 // prefer WebGPU; fallback to auto if not available
      dtype:  'q4',           // maps to onnx/model_q4.onnx + model_q4.onnx_data
      progress_callback: setProgress,
    };

    // Example presets (rendered as buttons)
    const EXAMPLES = [
      {
        title: 'Parse Apache logs (Python)',
        sys: 'You are a senior coding assistant. Keep answers concise and show tested code.',
        prompt: `Write a Python function parse_log(line: str) that parses Apache combined log format into a dict with keys ip, time, method, path, status, bytes, referrer, ua. Include robust regex, timezone handling, and 5 pytest unit tests.`
      },
      {
        title: 'Refactor callbacks → async/await (JS)',
        sys: 'You are a pragmatic JS refactoring assistant.',
        prompt: `Refactor this Node.js callback code to async/await with proper error handling and backpressure using streams:
const fs = require('fs');
fs.readFile('in.txt', (e, d) => {
  if (e) throw e;
  fs.writeFile('out.txt', d.toString().toUpperCase(), err => {
    if (err) throw err;
    console.log('done');
  });
});
Provide a short explanation.`
      },
      {
        title: 'Cohort retention SQL (Postgres)',
        sys: 'You are a data engineer.',
        prompt: `Given events(user_id, event_time, event_name) with sign_up and active events, write a SQL that computes weekly user retention (cohorted by signup week) as a pivoted table. Assume UTC timestamps. Explain indexes briefly.`
      },
      {
        title: 'Unit tests with pytest',
        sys: 'You are a Python testing expert.',
        prompt: `Generate pytest tests for a function normalize_phone(s: str) that returns E.164 format or raises ValueError. Cover edge cases and property tests with hypothesis.`
      },
      {
        title: 'Explain code step by step',
        sys: 'Be a clear explainer for junior developers.',
        prompt: `Explain the following code step by step, then suggest two improvements for readability and performance:
from collections import defaultdict

def f(nums):
  d = defaultdict(int)
  for x in nums:
    d[x] += 1
  m = max(d.values())
  return [k for k, v in d.items() if v == m]`
      },
      {
        title: 'Regex with explanation',
        sys: 'You write readable regex with comments.',
        prompt: `Write a single regex that matches a valid IPv4 or IPv6 address. Provide a commented, multi-line version and a short, single-line version, plus examples of matches and non-matches.`
      },
      {
        title: 'Document a Go function',
        sys: 'You are a Go reviewer.',
        prompt: `Write a Go doc comment and improve the signature for a function that merges two sorted slices of ints and returns a deduplicated sorted slice. Provide a fully working example.`
      },
      {
        title: 'GitHub Actions CI',
        sys: 'You are a DevOps assistant.',
        prompt: `Create a GitHub Actions workflow (YAML) that runs Python tests on 3.11, caches pip, runs flake8 + pytest, and uploads coverage to Codecov with secrets.CODECOV_TOKEN.`
      }
    ];

    // ================= DOM & State =================
    const $ = s=>document.querySelector(s);
    const logDiv=$('#log'), chatDiv=$('#chat');
    const loadBtn=$('#load'), cancelBtn=$('#cancel');
    const clearBtn=$('#clearlog');
    const ui=$('#ui'), loading=$('#loading');
    const pbar=$('#progress'), plabel=$('#progress-label');
    const sysEl=$('#sys'), promptEl=$('#prompt');
    const sendBtn=$('#send'), stopBtn=$('#stop');
    const statusChip=$('#status');
    const confirmBox=$('#confirm');
    const confirmYes=$('#confirm-yes');
    const confirmNo=$('#confirm-no');
    const exWrap=$('#examples');
    const autoRun=$('#autorun');

    let pipe=null;
    let cancelled=false;
    let stopping=null;         // InterruptableStoppingCriteria
    let currentStreamer=null;  // TextStreamer

    // ================= Utils =================
    function log(...a){
      const s = a.map(x => (typeof x === 'string' ? x : JSON.stringify(x))).join(' ');
      logDiv.textContent += s + '\n';      // ✅ 修复:原代码这里是非法的跨行字符串
      logDiv.scrollTop = logDiv.scrollHeight;
      console.log('[LOG]', ...a);
    }
    function chip(kind,text){ statusChip.className='chip '+(kind||''); statusChip.textContent=text; statusChip.style.display='inline-flex'; }
    function clearChip(){ statusChip.style.display='none'; }
    function setProgress(evt){
      if(evt?.status==='progress'){
        const pct=Math.round(Math.max(0,Math.min(100,evt.progress||0)));
        pbar.value=pct; plabel.textContent=`${pct}% ${evt?.name||evt?.file||''}`.trim();
      } else if(evt?.status){
        log(`status: ${evt.status} ${evt?.name||evt?.file||''}`);
      }
    }

    window.addEventListener('error',e=>log('window.error:',e.message,e.filename,`${e.lineno}:${e.colno}`));
    window.addEventListener('unhandledrejection',e=>log('unhandledrejection:',e.reason?.message||e.reason));

    async function importTransformers(){
      log('Importing library:',TRANSFORMERS_CDN);
      return await import(TRANSFORMERS_CDN);
    }

    function renderExamples(){
      exWrap.innerHTML='';
      EXAMPLES.forEach((ex)=>{
        const b=document.createElement('button');
        b.textContent=ex.title; b.className='ghost';
        b.addEventListener('click',()=>{
          sysEl.value = ex.sys || '';
          promptEl.value = ex.prompt || '';
          if(autoRun.checked){ sendBtn.click(); }
        });
        exWrap.appendChild(b);
      });
    }
    renderExamples();

    // ================= Events =================
    loadBtn.addEventListener('click', ()=>{
      loadBtn.style.display='none';
      confirmBox.style.display='block';
    });

    confirmNo.addEventListener('click', ()=>{
      confirmBox.style.display='none';
      loadBtn.style.display='inline-block';
    });

    confirmYes.addEventListener('click', async ()=>{
      try{
        cancelled=false; confirmBox.style.display='none';
        cancelBtn.style.display='inline-block';
        loading.style.display='block'; pbar.value=0; plabel.textContent='0%';

        const { pipeline, TextStreamer, InterruptableStoppingCriteria } = await importTransformers();

        if (device !== 'webgpu') {
          log('WebGPU not detected; falling back to device=auto (WASM/CPU).');
          chip('warn','WebGPU not detected — using auto');
        } else {
          log('WebGPU detected.');
        }
        log('Transformers.js loaded. Creating text-generation pipeline…');

        pipe = await pipeline('text-generation', MODEL_ID, options);
        stopping = new InterruptableStoppingCriteria();
        // 暴露 streamer 构造器供 send 使用
        window.__hf = { TextStreamer };

        if(cancelled) log('Note: cancel only resets UI and cannot interrupt underlying downloads.');
        loading.style.display='none'; cancelBtn.style.display='none'; ui.style.display='block';
        chip('ok','Model ready'); setTimeout(clearChip,1200);
      }catch(err){
        loading.style.display='none'; cancelBtn.style.display='none'; loadBtn.style.display='inline-block';
        log('❌ Load failed:', err?.message||err);
        if((err?.message||'').includes('404')) log('Check repo ID or private permissions:', MODEL_ID);
        log('Reminder: Transformers.js expects ONNX weights under an onnx/ directory with proper configs.');
      }
    });

    cancelBtn.addEventListener('click',()=>{ cancelled=true; loadBtn.style.display='inline-block'; cancelBtn.style.display='none'; loading.style.display='none'; log('Canceled (UI only).'); });
    clearBtn.addEventListener('click',()=>{ logDiv.textContent=''; });

    function addMsg(text, me=false){
      const d=document.createElement('div'); d.className='msg '+(me?'me':'bot'); d.textContent=text;
      chatDiv.appendChild(d); chatDiv.scrollTop=chatDiv.scrollHeight; return d;
    }

    sendBtn.addEventListener('click', async ()=>{
      try{
        if(!pipe){ chip('warn','Model not loaded'); return; }
        const user=promptEl.value.trim(); if(!user) return; promptEl.value='';
        const sys=sysEl.value.trim();
        addMsg(user,true);
        const botEl=addMsg('…');
        stopBtn.disabled=false; chip('', 'Generating…'); chatDiv.setAttribute('aria-busy','true');

        // v3 推荐的“消息数组”(Chat 格式)
        const messages = [];
        if (sys) messages.push({ role: 'system', content: sys });
        messages.push({ role: 'user', content: user });

        const { TextStreamer } = window.__hf || {};
        let outText = '';
        currentStreamer = new TextStreamer(pipe.tokenizer, {
          skip_prompt: true,
          callback_function: (chunk) => { outText += chunk; botEl.textContent = outText; }
        });

        // 可中断
        stopping?.reset?.();

        const out = await pipe(messages, {
          max_new_tokens: 256,
          temperature: 0.7,
          top_p: 0.9,
          repetition_penalty: 1.05,
          streamer: currentStreamer,
          stopping_criteria: stopping
        });

        // 若未走流式,兜底一次性写入
        if (!outText && Array.isArray(out) && out[0] && out[0].generated_text) {
          botEl.textContent = (typeof out[0].generated_text === 'string')
            ? out[0].generated_text
            : JSON.stringify(out[0].generated_text);
        }

        chip('ok','Done'); setTimeout(clearChip, 1200);
      }catch(err){
        log('❌ Generation error:', err?.message||err);
        chip('err','Generation failed');
      }finally{
        stopBtn.disabled=true; currentStreamer=null; chatDiv.setAttribute('aria-busy','false');
      }
    });

    // Stop:使用 InterruptableStoppingCriteria 即时终止生成
    stopBtn.addEventListener('click',()=>{
      try{
        stopping?.interrupt?.();
        chip('warn','Stopped'); setTimeout(clearChip,1200);
      }catch{}
    });
  </script>

</body>
</html>