285 lines · 14.0 KB · HTML
RawDownload
<!doctype html><html><head><meta charset="utf-8"><title>lilt bar</title><style>@font-face { font-family: Figtree; src: url(fonts/Figtree.ttf); font-weight: 300 900; }html, body { margin: 0; height: 100%; background: transparent; overflow: hidden; }body { font: 500 12px Figtree, sans-serif; color: #fff; user-select: none; -webkit-user-select: none; cursor: default; }#root { position: fixed; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: flex-end; padding-bottom: 4px; box-sizing: border-box; gap: 6px; }button { all: unset; display: grid; place-items: center; cursor: pointer; }svg { display: block; } /* idle: the small grey handle */.idle { width: 34px; height: 6px; border-radius: 4px; background: rgba(110, 110, 110, .8); border: 1px solid rgba(255, 255, 255, .45); position: relative; }.idle.nt::after { content: ""; position: absolute; right: -10px; top: 0; width: 6px; height: 6px; border-radius: 50%; background: #ff4d4d; animation: pulse 1.4s infinite; } /* hover: mic pill + notetaker circle (grows a chevron when hovered), tooltips sit over their own pill */.row { display: flex; gap: 6px; align-items: center; }.slot { position: relative; display: flex; }.slot.nt { width: 52px; }.pill { height: 30px; border-radius: 15px; background: #161616; box-sizing: border-box; border: 1px solid rgba(255, 255, 255, .2); box-shadow: 0 4px 14px rgba(0, 0, 0, .3); display: flex; align-items: center; justify-content: center; color: #f2f2f2; }.mic { width: 50px; }.mic:hover { background: #222; }.ntp { min-width: 30px; background: #232323; padding: 0; transition: padding .12s; }.ntp .rec { width: 28px; height: 28px; border-radius: 50%; }.ntp.open { padding-right: 6px; }.ntp.open .rec { background: #333; box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .14); }.ntp .dot { position: absolute; top: -3px; left: 21px; width: 7px; height: 7px; border-radius: 50%; background: #5cc0a8; box-shadow: 0 0 0 1.5px #e9f6f1; }.ntp.on .dot { background: #ff4d4d; box-shadow: 0 0 0 1.5px #ffe3e3; animation: pulse 1.4s infinite; }.chev { display: none !important; width: 16px; height: 28px; color: #8d8d8d; }.ntp.open .chev { display: grid !important; }.chev:hover { color: #fff; } .tip { position: absolute; bottom: calc(100% + 6px); left: 50%; transform: translateX(-50%); background: #000; color: #fff; padding: 5px 12px; border-radius: 14px; font-size: 14px; line-height: 18px; font-weight: 500; white-space: nowrap; box-shadow: 0 0 0 1px rgba(255, 255, 255, .12); pointer-events: none; }.tip b { font-weight: 700; } /* popover from the chevron */.pop { width: 206px; background: #1b1b1b; border-radius: 16px; padding: 16px 14px 14px; box-sizing: border-box; box-shadow: 0 0 0 1px rgba(255, 255, 255, .1), 0 10px 30px rgba(0, 0, 0, .45); display: flex; flex-direction: column; align-items: center; gap: 10px; text-align: center; }.pop .t { font-size: 13px; font-weight: 500; color: #f2f2f2; }.pop button { background: #fdf9ea; color: #111; font-weight: 600; font-size: 12px; padding: 7px 14px; border-radius: 8px; }.pop button.ghost { background: #2c2c2c; color: #eee; } /* recording */.recp { height: 28px; border-radius: 14px; padding: 0 12px; gap: 8px; }.wave { display: flex; align-items: center; gap: 2px; height: 20px; }.wave i { display: block; width: 2.5px; border-radius: 2px; background: #fff; height: 3px; transition: height 60ms linear; }.x { width: 18px; height: 18px; border-radius: 50%; background: #333; color: #ddd; font-size: 13px; margin-left: -6px; }.stop { width: 18px; height: 18px; border-radius: 50%; background: #ff4d4d; margin-right: -6px; }.stop::after { content: ""; width: 6px; height: 6px; border-radius: 1.5px; background: #fff; } /* processing */.procp { height: 28px; border-radius: 14px; padding: 0 14px; gap: 4px; }.procp i { width: 5px; height: 5px; border-radius: 50%; background: #fff; animation: dots 1s infinite ease-in-out; }.procp i:nth-child(2) { animation-delay: .15s; }.procp i:nth-child(3) { animation-delay: .3s; } @keyframes pulse { 50% { opacity: .35; } }@keyframes dots { 0%, 100% { opacity: .3; transform: translateY(0); } 50% { opacity: 1; transform: translateY(-3px); } }</style></head><body><div id="root"></div><script>(function () {  'use strict';  var root = document.getElementById('root');  var S = { recording: false, processing: false, notetaker: false, hotkeyLabel: 'Right Alt', settings: {} };  var hover = false, menu = false, tip = null, lastSize = '', BARS = 18;   var ICON = {    mic: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><rect x="9" y="2.5" width="6" height="12" rx="3" fill="currentColor" stroke="none"/><path d="M5 11a7 7 0 0 0 14 0M12 18v3.5"/></svg>',    rec: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4"><circle cx="12" cy="12" r="8.5"/><circle cx="12" cy="12" r="4.2" fill="currentColor" stroke="none"/></svg>',    chev: '<svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M6 15l6-6 6 6"/></svg>',    cal: '<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#cfcfcf" stroke-width="1.6" stroke-linecap="round"><rect x="3.5" y="5" width="17" height="15.5" rx="2.5"/><path d="M3.5 10h17M8 3v4M16 3v4"/><path d="M8 13.5h.01M12 13.5h.01M16 13.5h.01M8 17h.01M12 17h.01" stroke-width="2.4"/></svg>'  };   function view() {    if (S.recording) return 'rec';    if (S.processing) return 'proc';    if (menu) return 'menu';    if (hover) return 'hover';    return 'idle';  }   // Window size per view, kept as small as possible so it never blocks clicks.  function size(v) {    if (v === 'rec') return S.recording === 'handsfree' ? [230, 40] : [190, 40];    if (v === 'proc') return [110, 40];    if (v === 'menu') return [260, 210];    if (v === 'hover') return [300, 76];    return [76, 22];  }   function esc(s) { return String(s).replace(/[&<>"]/g, function (c) { return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]; }); }   function pills() {    var open = tip === 'nt' || tip === 'chev' || menu;    var micTip = tip === 'mic' ? '<div class="tip">Dictate <b>' + esc(S.hotkeyLabel) + '</b></div>' : '';    var ntTip = tip === 'nt' ? '<div class="tip">' + (S.notetaker ? 'Stop Notetaker' : 'Start Notetaker') + ' <b>Ctrl+Alt+M</b></div>' : '';    return '<div class="row">' +      '<div class="slot">' + micTip + '<button class="pill mic" data-act="dictate" data-tip="mic">' + ICON.mic + '</button></div>' +      '<div class="slot nt">' + ntTip + '<div class="pill ntp' + (open ? ' open' : '') + (S.notetaker ? ' on' : '') + '" data-tip="ntp">' +      '<button class="rec" data-act="notetaker" data-tip="nt">' + ICON.rec + '</button><span class="dot"></span>' +      '<button class="chev" data-act="menu" data-tip="chev">' + ICON.chev + '</button></div></div></div>';  }   function render() {    var v = view(), sz = size(v), key = sz.join('x');    if (key !== lastSize) { lastSize = key; window.lilt.bar.resize(sz[0], sz[1]); }    var h = '';    if (v === 'idle') h = '<div class="idle' + (S.notetaker ? ' nt' : '') + '"></div>';    else if (v === 'hover') h = pills();    else if (v === 'menu') {      h = '<div class="pop">' + '<div class="t">' + (S.notetaker ? 'Notetaker is recording' : 'Notetaker') + '</div>' +        '<button data-act="notetaker">' + (S.notetaker ? 'Stop Notetaker' : 'Start Notetaker') + '</button>' +        (S.settings.scratchpadInBar ? '<button class="ghost" data-act="scratchpad">Open Scratchpad</button>' : '') +        '<button class="ghost" data-act="hub">Open lilt</button></div>' + pills();    } else if (v === 'rec') {      var hf = S.recording === 'handsfree', bars = '';      for (var i = 0; i < BARS; i++) bars += '<i></i>';      h = '<div class="pill recp">' + (hf ? '<button class="x" data-act="cancel">&#x2715;</button>' : '') +        '<div class="wave">' + bars + '</div>' + (hf ? '<button class="stop" data-act="stop"></button>' : '') + '</div>';    } else h = '<div class="pill procp"><i></i><i></i><i></i></div>';    root.innerHTML = h;  }   document.documentElement.addEventListener('mouseenter', function () { hover = true; render(); });  document.documentElement.addEventListener('mouseleave', function () { hover = false; menu = false; tip = null; render(); });  root.addEventListener('mouseover', function (e) {    var t = e.target.closest('[data-tip]'), next = t ? t.getAttribute('data-tip') : null;    if (next === 'ntp') next = 'chev';    if (next !== tip && view() === 'hover') { tip = next; render(); }  });  root.addEventListener('click', function (e) {    var b = e.target.closest('[data-act]');    if (!b) return;    var act = b.getAttribute('data-act');    if (act === 'menu') { menu = !menu; tip = null; render(); return; }    menu = false;    window.lilt.bar.action(act);    render();  });   // ------------------------------------------------------------ sounds   function tone(up) {    try {      var c = new AudioContext(), o = c.createOscillator(), g = c.createGain(), t = c.currentTime;      o.type = 'sine';      o.frequency.setValueAtTime(up ? 560 : 820, t);      o.frequency.exponentialRampToValueAtTime(up ? 820 : 560, t + 0.09);      g.gain.setValueAtTime(0.0001, t);      g.gain.exponentialRampToValueAtTime(0.1, t + 0.015);      g.gain.exponentialRampToValueAtTime(0.0001, t + 0.15);      o.connect(g); g.connect(c.destination);      o.start(t); o.stop(t + 0.17);      o.onended = function () { c.close(); };    } catch (e) { /* no audio output */ }  }   // ------------------------------------------------------------ microphone   var WORKLET = "class Tap extends AudioWorkletProcessor{process(i){var c=i[0]&&i[0][0];if(c)this.port.postMessage(c.slice(0));return true}}registerProcessor('tap',Tap)";  var ctx = null, stream = null, micKey = null, closeTimer = 0, session = 0;  var recording = false, chunks = [], pre = [], preLen = 0, recStart = 0, level = 0, levels = [];   function onChunk(c) {    var s = 0;    for (var i = 0; i < c.length; i++) s += c[i] * c[i];    level = Math.max(Math.sqrt(s / c.length), level * 0.9);    if (recording) { chunks.push(c); return; }    // Keep ~300 ms of pre-roll while the mic is warm so the first syllable survives.    pre.push(c); preLen += c.length;    while (pre.length && preLen - pre[0].length > ctx.sampleRate * 0.3) preLen -= pre.shift().length;  }   function getStream(micId) {    var base = { channelCount: 1, echoCancellation: false, noiseSuppression: true, autoGainControl: true };    var want = micId ? Object.assign({ deviceId: { exact: micId } }, base) : base;    return navigator.mediaDevices.getUserMedia({ audio: want }).catch(function () {      return navigator.mediaDevices.getUserMedia({ audio: base }); // chosen mic unplugged    });  }   async function openMic(micId) {    if (stream && micKey === micId) return;    closeMic();    stream = await getStream(micId);    var src;    try { ctx = new AudioContext({ sampleRate: 16000 }); src = ctx.createMediaStreamSource(stream); }    catch (e) { ctx = new AudioContext(); src = ctx.createMediaStreamSource(stream); }    await ctx.audioWorklet.addModule(URL.createObjectURL(new Blob([WORKLET], { type: 'text/javascript' })));    var node = new AudioWorkletNode(ctx, 'tap');    node.port.onmessage = function (e) { onChunk(e.data); };    var mute = ctx.createGain();    mute.gain.value = 0;    src.connect(node); node.connect(mute); mute.connect(ctx.destination);    micKey = micId; pre = []; preLen = 0;  }   function closeMic() {    if (stream) stream.getTracks().forEach(function (t) { t.stop(); });    if (ctx) ctx.close();    stream = null; ctx = null; micKey = null;  }   function idleLater() {    clearTimeout(closeTimer);    closeTimer = setTimeout(function () { if (!recording) closeMic(); }, 120000);  }   function resample(x, from) {    if (from === 16000) return x;    var r = from / 16000, n = Math.floor(x.length / r), y = new Float32Array(n);    for (var i = 0; i < n; i++) {      var a = Math.floor(i * r), b = Math.min(x.length, Math.floor((i + 1) * r)), s = 0;      for (var j = a; j < b; j++) s += x[j];      y[i] = s / Math.max(1, b - a);    }    return y;  }   async function start(cmd) {    var my = ++session;    clearTimeout(closeTimer);    if (cmd.sounds) tone(true);    try { await openMic(cmd.micId || ''); } catch (e) { console.error('microphone:', e); return; }    if (my !== session) return;    chunks = pre.slice(); pre = []; preLen = 0;    recording = true; recStart = performance.now(); levels = [];  }   function stop(cmd) {    session++;    if (cmd.sounds) tone(false);    var ms = recording ? performance.now() - recStart : 0;    var total = 0, i, off = 0;    for (i = 0; i < chunks.length; i++) total += chunks[i].length;    var all = new Float32Array(recording ? total : 0);    if (recording) for (i = 0; i < chunks.length; i++) { all.set(chunks[i], off); off += chunks[i].length; }    var out = recording && ctx ? resample(all, ctx.sampleRate) : all;    recording = false; chunks = [];    window.lilt.bar.audio(out.buffer, Math.round(ms));    idleLater();  }   function cancel() {    session++; recording = false; chunks = [];    idleLater();  }   // Waveform: a scrolling history of the mic level.  function tick() {    if (view() === 'rec') {      levels.push(Math.min(1, Math.sqrt(level) * 2.2));      level *= 0.6;      if (levels.length > BARS) levels.shift();      var bars = root.querySelectorAll('.wave i');      for (var i = 0; i < bars.length; i++) {        var v = levels[i - (BARS - levels.length)] || 0;        bars[i].style.height = (3 + v * 17).toFixed(1) + 'px';      }    }    setTimeout(tick, 50);  }  tick();   window.lilt.bar.onCommand(function (c) {    if (c.type === 'start') start(c);    else if (c.type === 'stop') stop(c);    else if (c.type === 'cancel') cancel();    else if (c.type === 'state') {      var was = view();      S = c;      if (!S.settings) S.settings = {};      if (view() !== was || was === 'hover' || was === 'idle') render();    }  });   render();})();</script></body></html>