File size: 4,825 Bytes
5eb1dee
 
 
 
 
ed7c75c
5eb1dee
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ed7c75c
5eb1dee
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d095bd1
 
5eb1dee
 
d095bd1
5eb1dee
d095bd1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5eb1dee
d095bd1
5eb1dee
 
 
 
 
 
 
 
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
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>CPU RAG · Qwen3.5-0.8B + FAISS</title>
<style>
  :root { color-scheme: light dark; --bg:#0f1220; --panel:#1a1e33; --acc:#7c8cff; --mut:#8b90a8; }
  * { box-sizing: border-box; }
  body { margin:0; font-family: system-ui, sans-serif; background:var(--bg); color:#e7e9f3; }
  header { padding:14px 18px; border-bottom:1px solid #2a2f4a; display:flex; gap:12px; align-items:center; }
  header h1 { font-size:15px; margin:0; font-weight:600; }
  header .tag { font-size:11px; color:var(--mut); background:var(--panel); padding:3px 8px; border-radius:20px; }
  main { max-width:820px; margin:0 auto; padding:18px; }
  #chat { display:flex; flex-direction:column; gap:12px; min-height:50vh; }
  .msg { padding:11px 14px; border-radius:12px; max-width:88%; white-space:pre-wrap; line-height:1.45; font-size:14px; }
  .user { align-self:flex-end; background:var(--acc); color:#fff; }
  .bot  { align-self:flex-start; background:var(--panel); }
  .src  { align-self:flex-start; font-size:11px; color:var(--mut); margin-top:-6px; }
  form { display:flex; gap:8px; margin-top:16px; }
  textarea { flex:1; resize:none; padding:11px; border-radius:10px; border:1px solid #2a2f4a; background:#12152a; color:#e7e9f3; font-size:14px; }
  button { padding:0 16px; border:0; border-radius:10px; background:var(--acc); color:#fff; font-weight:600; cursor:pointer; }
  button:disabled { opacity:.5; cursor:default; }
  .bar { display:flex; gap:10px; align-items:center; margin-bottom:14px; font-size:12px; color:var(--mut); }
  .bar input[type=file] { font-size:12px; color:var(--mut); }
</style>
</head>
<body>
<header>
  <h1>🦅 CPU RAG</h1>
  <span class="tag">Qwen3.5-0.8B · bge-small · FAISS</span>
  <span class="tag" id="stat"></span>
</header>
<main>
  <div class="bar">
    <label>Add a document (.txt/.md):</label>
    <input type="file" id="file" accept=".txt,.md" />
    <button id="up" type="button">Upload</button>
    <span id="upmsg"></span>
  </div>
  <div id="chat"></div>
  <form id="form">
    <textarea id="q" rows="2" placeholder="Ask about your documents…"></textarea>
    <button id="send" type="submit">Send</button>
  </form>
</main>
<script>
const chat = document.getElementById('chat');
const stat = document.getElementById('stat');

function add(cls, text) {
  const d = document.createElement('div');
  d.className = 'msg ' + cls; d.textContent = text;
  chat.appendChild(d); chat.scrollIntoView({block:'end'}); return d;
}
async function refreshStats(){
  try { const s = await (await fetch('/stats')).json();
        stat.textContent = s.indexed_chunks + ' chunks indexed'; } catch(e){}
}
refreshStats();

document.getElementById('up').onclick = async () => {
  const f = document.getElementById('file').files[0];
  if (!f) return;
  document.getElementById('upmsg').textContent = 'uploading…';
  const fd = new FormData(); fd.append('file', f);
  const r = await (await fetch('/ingest', {method:'POST', body:fd})).json();
  document.getElementById('upmsg').textContent = '+' + r.added_chunks + ' chunks';
  refreshStats();
};

document.getElementById('form').onsubmit = async (e) => {
  e.preventDefault();
  const q = document.getElementById('q').value.trim();
  if (!q) return;
  document.getElementById('q').value = '';
  document.getElementById('send').disabled = true;
  add('user', q);
  const thinking = add('bot', '…');
  try {
    // Stream tokens so the first word shows in ~1s instead of waiting for the
    // whole answer (decode is ~5-6 tok/s on 2 vCPU).
    const r = await fetch('/v1/chat/completions', {
      method:'POST', headers:{'Content-Type':'application/json'},
      body: JSON.stringify({ messages:[{role:'user', content:q}], temperature:0.3, stream:true })
    });
    const reader = r.body.getReader();
    const dec = new TextDecoder();
    let buf = '', out = '';
    while (true) {
      const {value, done} = await reader.read();
      if (done) break;
      buf += dec.decode(value, {stream:true});
      const lines = buf.split('\n');
      buf = lines.pop();                      // keep partial line
      for (const line of lines) {
        const s = line.trim();
        if (!s.startsWith('data:')) continue;
        const payload = s.slice(5).trim();
        if (payload === '[DONE]') continue;
        try {
          const tok = JSON.parse(payload).choices?.[0]?.delta?.content;
          if (tok) { out += tok; thinking.textContent = out; chat.scrollIntoView({block:'end'}); }
        } catch (_) {}
      }
    }
    if (!out) thinking.textContent = '(no answer)';
  } catch (err) {
    thinking.textContent = 'Error: ' + err;
  }
  document.getElementById('send').disabled = false;
};
</script>
</body>
</html>