MetiMiester commited on
Commit
1907334
·
verified ·
1 Parent(s): f1f4eb8

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +19 -40
index.html CHANGED
@@ -5,7 +5,7 @@
5
  <meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover" />
6
  <title>BubbleGuard – Safe Chat</title>
7
 
8
- <!-- Favicon & CSS: use RELATIVE paths (no leading /) -->
9
  <link rel="icon" href="logo.png">
10
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
11
  <link href="styles.css" rel="stylesheet">
@@ -32,7 +32,6 @@
32
 
33
  <!-- Chat -->
34
  <main id="chat" class="container chat-wrap" aria-live="polite" aria-label="Chat history">
35
- <!-- Greeting -->
36
  <div class="row-start">
37
  <img src="avatar_female.png" alt="" class="avatar" onerror="this.style.display='none'">
38
  <div class="bubble-them bubble shadow-bubble">
@@ -44,8 +43,6 @@
44
 
45
  <!-- Composer -->
46
  <footer class="composer-wrap">
47
-
48
- <!-- Reply banner -->
49
  <div id="replyBanner" class="reply-banner d-none" role="status" aria-live="polite">
50
  <div class="rb-body">
51
  <div class="rb-line">
@@ -58,7 +55,6 @@
58
 
59
  <div class="container composer">
60
  <input id="fileImg" type="file" accept="image/*" class="d-none" aria-hidden="true">
61
-
62
  <button id="btnImg" class="btn btn-ico" title="Attach image" aria-label="Attach image">+</button>
63
 
64
  <div class="input-shell">
@@ -77,7 +73,6 @@
77
  <button id="btnSend" class="btn send-ios" aria-label="Send">↑</button>
78
  </div>
79
 
80
- <!-- Toast -->
81
  <div class="toast-zone">
82
  <div id="toast" class="toast ios-toast" role="alert" aria-live="assertive" aria-atomic="true">
83
  <div class="toast-body" id="toastBody">Hello</div>
@@ -86,30 +81,20 @@
86
  </footer>
87
  </div>
88
 
89
- <!-- Bootstrap JS -->
90
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
91
 
92
- <!-- App Script -->
93
  <script>
94
- // ---------- Config (RELATIVE API helper) ----------
95
- const api = (p) => new URL(p, window.location.href).toString(); // use: api('check_text')
96
 
97
  // ---------- JSON guard ----------
98
  async function fetchJSON(url, opts) {
99
  const res = await fetch(url, Object.assign({ headers: { 'Accept': 'application/json' } }, opts || {}));
100
  const ct = (res.headers.get('content-type') || '').toLowerCase();
101
  const text = await res.text();
102
- if (!ct.includes('application/json')) {
103
- throw new Error('Non-JSON from API: ' + text.slice(0, 160));
104
- }
105
- let body;
106
- try { body = JSON.parse(text); } catch (e) {
107
- throw new Error('Invalid JSON: ' + text.slice(0, 160));
108
- }
109
- if (!res.ok) {
110
- const msg = (body && (body.detail || body.error)) || (res.status + ' ' + res.statusText);
111
- throw new Error(msg);
112
- }
113
  return body;
114
  }
115
 
@@ -126,14 +111,14 @@
126
 
127
  // ---------- Health ----------
128
  (async () => {
 
129
  try {
130
  const j = await fetchJSON(api('health'));
131
  const t = j.text_thresholds || {};
132
- document.getElementById('health').textContent =
133
- `Online · ${j.device} · T=${t.TEXT_UNSAFE_THR ?? '-'} · S=${t.SHORT_MSG_UNSAFE_THR ?? '-'}`;
134
  } catch (e) {
135
- document.getElementById('health').textContent = 'Offline';
136
  console.warn('health error:', e);
 
137
  }
138
  })();
139
 
@@ -150,7 +135,7 @@
150
  const showToast = (msg) => { toastBody.textContent = msg; toast.show(); };
151
  const esc = (s)=>s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
152
 
153
- // ---------- Reactions & Reply state ----------
154
  const REACTIONS = ["👍","❤️","😂","😮","😢"];
155
  let replyTarget = null;
156
 
@@ -221,7 +206,6 @@
221
  function cancelReply(){ replyTarget = null; $('replyBanner').classList.add('d-none'); }
222
  $('replyCancel').onclick = cancelReply;
223
 
224
- // ---------- Bubble creators ----------
225
  function armBubbleInteractions(bubble, isThem=false){
226
  let pressTimer = null;
227
  const startPress = ()=>{ pressTimer=setTimeout(()=>showReactionsPop(bubble), 380); };
@@ -277,7 +261,6 @@
277
  return b;
278
  }
279
 
280
- // ---------- Blast helper ----------
281
  function blastBubble({ html, side='you', preview=true, icon='🚫', removeDelay=900 }){
282
  const content = preview
283
  ? `<span class="unsafe-icon" aria-hidden="true">${icon}</span><span class="unsafe-preview">${html}</span>`
@@ -287,10 +270,7 @@
287
  setTimeout(()=> bubble.closest('.row-start, .row-end')?.remove(), removeDelay);
288
  }
289
 
290
- // ---------- Input behavior ----------
291
- const typing = $('typing');
292
  function setTyping(on){ typing.classList.toggle('d-none', !on); }
293
- const input = $('input');
294
  input.addEventListener('input', ()=>{
295
  input.style.height='auto';
296
  input.style.height=Math.min(input.scrollHeight, 140)+'px';
@@ -299,9 +279,9 @@
299
  if(e.key==='Escape'){ input.value=''; input.style.height='auto'; }
300
  if(e.key==='Enter' && !e.shiftKey){ e.preventDefault(); sendText(); }
301
  });
 
302
  const normalizeInputText = (t)=> t.replace(/[’‘]/g,"'").replace(/[“”]/g,'"').replace(/\s+/g,' ').trim();
303
 
304
- // ---------- Send text ----------
305
  async function sendText(){
306
  let t = normalizeInputText(input.value);
307
  if(!t) return;
@@ -311,7 +291,7 @@
311
  t = `${quoted}${t}`;
312
  }
313
 
314
- setTyping(true); $('btnSend').disabled=true;
315
  try{
316
  const fd = new FormData(); fd.append('text', t);
317
  const j = await fetchJSON(api('check_text'), { method: 'POST', body: fd });
@@ -324,14 +304,14 @@
324
  }catch(e){ showToast('Error: '+e.message); }
325
  finally{
326
  input.value=''; input.style.height='auto';
327
- setTyping(false); $('btnSend').disabled=false;
328
  }
329
  }
330
- $('btnSend').onclick = sendText;
331
 
332
- // ---------- Image ----------
333
- $('btnImg').onclick = ()=> $('fileImg').click();
334
- $('fileImg').onchange = async ()=>{ const f=$('fileImg').files[0]; if(f) await handleImage(f); $('fileImg').value=''; };
335
 
336
  async function handleImage(file){
337
  setTyping(true);
@@ -359,9 +339,9 @@
359
  else showToast('Only images supported via drop.');
360
  }, false);
361
 
362
- // ---------- Voice ----------
363
  let mediaStream=null, mediaRecorder=null, chunks=[], tick=null, startTs=0;
364
- const btnStart=$('btnStart'), btnStop=$('btnStop'), recTimer=$('recTimer');
365
 
366
  const pickMime = () => {
367
  const prefs=['audio/webm;codecs=opus','audio/webm','audio/mp4;codecs=mp4a.40.2','audio/mp4','audio/ogg;codecs=opus','audio/ogg'];
@@ -373,7 +353,6 @@
373
  btnStart.disabled=recording; btnStop.disabled=!recording;
374
  recTimer.classList.toggle('d-none',!recording);
375
  }
376
- const fmt = (t)=>{ const m=Math.floor(t/60), s=Math.floor(t%60); return `${String(m).padStart(2,'0')}:${String(s).padStart(2,'0')}`; };
377
 
378
  btnStart.onclick = async ()=>{
379
  try{
 
5
  <meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover" />
6
  <title>BubbleGuard – Safe Chat</title>
7
 
8
+ <!-- RELATIVE paths (no leading /) -->
9
  <link rel="icon" href="logo.png">
10
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
11
  <link href="styles.css" rel="stylesheet">
 
32
 
33
  <!-- Chat -->
34
  <main id="chat" class="container chat-wrap" aria-live="polite" aria-label="Chat history">
 
35
  <div class="row-start">
36
  <img src="avatar_female.png" alt="" class="avatar" onerror="this.style.display='none'">
37
  <div class="bubble-them bubble shadow-bubble">
 
43
 
44
  <!-- Composer -->
45
  <footer class="composer-wrap">
 
 
46
  <div id="replyBanner" class="reply-banner d-none" role="status" aria-live="polite">
47
  <div class="rb-body">
48
  <div class="rb-line">
 
55
 
56
  <div class="container composer">
57
  <input id="fileImg" type="file" accept="image/*" class="d-none" aria-hidden="true">
 
58
  <button id="btnImg" class="btn btn-ico" title="Attach image" aria-label="Attach image">+</button>
59
 
60
  <div class="input-shell">
 
73
  <button id="btnSend" class="btn send-ios" aria-label="Send">↑</button>
74
  </div>
75
 
 
76
  <div class="toast-zone">
77
  <div id="toast" class="toast ios-toast" role="alert" aria-live="assertive" aria-atomic="true">
78
  <div class="toast-body" id="toastBody">Hello</div>
 
81
  </footer>
82
  </div>
83
 
 
84
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
85
 
 
86
  <script>
87
+ // ---------- API helper (always relative) ----------
88
+ const api = (p) => new URL(String(p).replace(/^\/+/, ''), window.location.href).toString();
89
 
90
  // ---------- JSON guard ----------
91
  async function fetchJSON(url, opts) {
92
  const res = await fetch(url, Object.assign({ headers: { 'Accept': 'application/json' } }, opts || {}));
93
  const ct = (res.headers.get('content-type') || '').toLowerCase();
94
  const text = await res.text();
95
+ if (!ct.includes('application/json')) throw new Error('Non-JSON: ' + text.slice(0, 160));
96
+ let body; try { body = JSON.parse(text); } catch { throw new Error('Invalid JSON: ' + text.slice(0,160)); }
97
+ if (!res.ok) throw new Error(body.detail || res.status + ' ' + res.statusText);
 
 
 
 
 
 
 
 
98
  return body;
99
  }
100
 
 
111
 
112
  // ---------- Health ----------
113
  (async () => {
114
+ const el = document.getElementById('health');
115
  try {
116
  const j = await fetchJSON(api('health'));
117
  const t = j.text_thresholds || {};
118
+ el.textContent = `Online · ${j.device} · T=${t.TEXT_UNSAFE_THR ?? '-'} · S=${t.SHORT_MSG_UNSAFE_THR ?? '-'}`;
 
119
  } catch (e) {
 
120
  console.warn('health error:', e);
121
+ el.textContent = 'Offline';
122
  }
123
  })();
124
 
 
135
  const showToast = (msg) => { toastBody.textContent = msg; toast.show(); };
136
  const esc = (s)=>s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
137
 
138
+ // ---------- Reactions & Reply ----------
139
  const REACTIONS = ["👍","❤️","😂","😮","😢"];
140
  let replyTarget = null;
141
 
 
206
  function cancelReply(){ replyTarget = null; $('replyBanner').classList.add('d-none'); }
207
  $('replyCancel').onclick = cancelReply;
208
 
 
209
  function armBubbleInteractions(bubble, isThem=false){
210
  let pressTimer = null;
211
  const startPress = ()=>{ pressTimer=setTimeout(()=>showReactionsPop(bubble), 380); };
 
261
  return b;
262
  }
263
 
 
264
  function blastBubble({ html, side='you', preview=true, icon='🚫', removeDelay=900 }){
265
  const content = preview
266
  ? `<span class="unsafe-icon" aria-hidden="true">${icon}</span><span class="unsafe-preview">${html}</span>`
 
270
  setTimeout(()=> bubble.closest('.row-start, .row-end')?.remove(), removeDelay);
271
  }
272
 
 
 
273
  function setTyping(on){ typing.classList.toggle('d-none', !on); }
 
274
  input.addEventListener('input', ()=>{
275
  input.style.height='auto';
276
  input.style.height=Math.min(input.scrollHeight, 140)+'px';
 
279
  if(e.key==='Escape'){ input.value=''; input.style.height='auto'; }
280
  if(e.key==='Enter' && !e.shiftKey){ e.preventDefault(); sendText(); }
281
  });
282
+
283
  const normalizeInputText = (t)=> t.replace(/[’‘]/g,"'").replace(/[“”]/g,'"').replace(/\s+/g,' ').trim();
284
 
 
285
  async function sendText(){
286
  let t = normalizeInputText(input.value);
287
  if(!t) return;
 
291
  t = `${quoted}${t}`;
292
  }
293
 
294
+ setTyping(true); btnSend.disabled=true;
295
  try{
296
  const fd = new FormData(); fd.append('text', t);
297
  const j = await fetchJSON(api('check_text'), { method: 'POST', body: fd });
 
304
  }catch(e){ showToast('Error: '+e.message); }
305
  finally{
306
  input.value=''; input.style.height='auto';
307
+ setTyping(false); btnSend.disabled=false;
308
  }
309
  }
310
+ btnSend.onclick = sendText;
311
 
312
+ // Image
313
+ btnImg.onclick = ()=> fileImg.click();
314
+ fileImg.onchange = async ()=>{ if(fileImg.files[0]) await handleImage(fileImg.files[0]); fileImg.value=''; };
315
 
316
  async function handleImage(file){
317
  setTyping(true);
 
339
  else showToast('Only images supported via drop.');
340
  }, false);
341
 
342
+ // Voice
343
  let mediaStream=null, mediaRecorder=null, chunks=[], tick=null, startTs=0;
344
+ const fmt = (t)=>{ const m=Math.floor(t/60), s=Math.floor(t%60); return `${String(m).padStart(2,'0')}:${String(s).padStart(2,'0')}`; };
345
 
346
  const pickMime = () => {
347
  const prefs=['audio/webm;codecs=opus','audio/webm','audio/mp4;codecs=mp4a.40.2','audio/mp4','audio/ogg;codecs=opus','audio/ogg'];
 
353
  btnStart.disabled=recording; btnStop.disabled=!recording;
354
  recTimer.classList.toggle('d-none',!recording);
355
  }
 
356
 
357
  btnStart.onclick = async ()=>{
358
  try{