Browseren streamer 16 kHz PCM direkte til ElevenLabs' realtime-STT med et engangs-token fra serveren (/api/stt-token); teksten vokser under optagelsen og sendes med kladden, så serveren slipper for at skrive af. 'Spørgsmål og svar' bruger ét audio-element (iOS blokerer ellers det andet play()) og laver spørgsmålets lyd on demand for ældre klip. Admin kan rense favorit-klip med ElevenLabs Audio Isolation (<id>-ren.mp3), aldrig som standard. Claude-Session: https://claude.ai/code/session_01JmhJG6JnhXfzNiKv8f2vHu
862 lines
39 KiB
HTML
862 lines
39 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}Erindringer{% endblock %}
|
|
{% block body %}
|
|
<style>
|
|
/* Ingen vandret scroll på iPhone: alt holder sig inden for kortet, og felter
|
|
har mindst 16 px skrift, ellers zoomer iOS ind. */
|
|
html, body { overflow-x: hidden; }
|
|
.er-side { max-width: 100%; overflow-wrap: anywhere; }
|
|
.er-side audio, .er-side textarea, .er-side .wt-card { max-width: 100%; box-sizing: border-box; }
|
|
.er-side audio { display: block; width: 100%; min-width: 0; }
|
|
.er-side textarea { font-size: 16px !important; min-width: 0; min-height: 7rem; line-height: 1.5; }
|
|
.er-side .wt-btn { white-space: normal; }
|
|
|
|
/* Store, rolige knapper til ældre fingre */
|
|
.er-stor { font-size: 1.1rem; padding: .85rem 1.4rem; min-height: 3.2rem; }
|
|
.er-knapper { display: flex; flex-wrap: wrap; gap: .7rem; justify-content: center; margin-top: 1rem; }
|
|
.er-kort { margin: 1rem 0; padding: 1.1rem 1.2rem; }
|
|
.er-emne { display: flex; flex-direction: column; gap: .5rem; }
|
|
.er-emne h3 { margin: 0; font-size: 1.25rem; }
|
|
.er-emne .wt-btn { align-self: flex-start; }
|
|
|
|
/* Spørgsmålet: stor skrift, god linjeafstand */
|
|
.er-spoergsmaal { font-size: 1.3rem; line-height: 1.5; margin: .3rem 0 .8rem; font-weight: 600; }
|
|
|
|
/* Den runde optage-knap */
|
|
.er-midt { display: flex; flex-direction: column; align-items: center; gap: .8rem; margin: 1.4rem 0; text-align: center; }
|
|
.er-optag {
|
|
width: 7.5rem; height: 7.5rem; min-width: 88px; min-height: 88px; border-radius: 50%;
|
|
border: 3px solid var(--color-accent);
|
|
background: color-mix(in srgb, var(--color-accent) 16%, transparent);
|
|
color: inherit; font: inherit; font-size: 1.2rem; font-weight: 700; cursor: pointer;
|
|
display: flex; flex-direction: column; align-items: center; justify-content: center; gap: .15rem;
|
|
transition: transform .15s, background .2s;
|
|
}
|
|
.er-optag:hover { transform: scale(1.04); }
|
|
.er-optag:disabled { opacity: .5; cursor: default; transform: none; }
|
|
.er-optag.optager {
|
|
background: var(--color-bad); border-color: var(--color-bad); color: #fff;
|
|
animation: er-puls 1.3s ease-in-out infinite;
|
|
}
|
|
.er-optag .er-taeller { font-size: .95rem; font-weight: 500; font-variant-numeric: tabular-nums; }
|
|
@keyframes er-puls {
|
|
0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--color-bad) 55%, transparent); }
|
|
50% { box-shadow: 0 0 0 18px transparent; }
|
|
}
|
|
@media (prefers-reduced-motion: reduce) { .er-optag.optager { animation: none; } }
|
|
|
|
.er-status { min-height: 1.5rem; font-size: 1.05rem; color: var(--color-muted); }
|
|
.er-status.travl { color: var(--color-accent); font-weight: 600; }
|
|
.er-link { background: none; border: 0; padding: .4rem; color: var(--color-accent); text-decoration: underline;
|
|
cursor: pointer; font: inherit; font-size: 1rem; }
|
|
|
|
/* Svar-lister */
|
|
.er-liste { list-style: none; padding: 0; margin: .6rem 0 0; }
|
|
.er-liste li { border-top: 1px solid var(--color-border); padding: .9rem 0; }
|
|
.er-liste li:first-child { border-top: 0; }
|
|
.er-liste .er-q { font-weight: 700; margin: 0 0 .3rem; font-size: 1.05rem; }
|
|
.er-liste .er-meta { color: var(--color-muted); font-size: .9rem; margin: 0 0 .3rem; }
|
|
.er-tekst { margin: 0; white-space: pre-line; line-height: 1.55; font-size: 1.05rem; }
|
|
.er-ret { margin-top: .5rem; }
|
|
.er-ret .wt-btn { font-size: .95rem; }
|
|
.er-dit-svar { background: color-mix(in srgb, var(--color-good) 8%, transparent); border-color: color-mix(in srgb, var(--color-good) 40%, transparent); }
|
|
</style>
|
|
|
|
<div class="er-side" id="er-side"
|
|
data-api="{{ api }}" data-rod="{{ rod }}" data-noegle="{{ noegle }}" data-valgt="{{ valgt }}">
|
|
|
|
<h1>Erindringer</h1>
|
|
<p class="wt-lead">Hej {{ navn }}. Fortæl, så gemmer vi det. Du kan altid holde en pause og komme tilbage.</p>
|
|
|
|
{% if saved %}<div class="wt-flash">{{ saved }}</div>{% endif %}
|
|
<div class="wt-flash" id="er-flash" hidden role="status" aria-live="polite"></div>
|
|
|
|
<!-- Trin 2: vælg emne -->
|
|
<section id="er-emner">
|
|
<h2>Vælg, hvad du vil fortælle om</h2>
|
|
{% for p in projekter %}
|
|
<div class="wt-card er-kort er-emne" data-slug="{{ p.slug }}" data-titel="{{ p.titel }}">
|
|
<h3>{{ p.titel }}</h3>
|
|
{% if p.beskrivelse %}<p style="margin:0">{{ p.beskrivelse }}</p>{% endif %}
|
|
<p class="wt-muted" style="margin:0"><span class="er-antal">{{ p.antal }}</span> svar indtil nu</p>
|
|
<button type="button" class="wt-btn wt-btn--primary er-stor" data-vaelg="{{ p.slug }}"
|
|
aria-label="Fortæl om {{ p.titel }}">Fortæl</button>
|
|
</div>
|
|
{% else %}
|
|
<p class="wt-muted">Der er ingen emner endnu.</p>
|
|
{% endfor %}
|
|
</section>
|
|
|
|
<!-- Trin 3: samtalen -->
|
|
<section id="er-samtale" hidden>
|
|
<h2 id="er-samtale-titel">Samtale</h2>
|
|
<p id="er-dag" hidden class="wt-muted" style="margin:0 0 .8rem">Dagbog for <input type="date" id="er-dato" max="{{ idag }}" value="{{ idag }}" style="font-size:16px;padding:.2rem .4rem"> <span class="wt-muted">(én dag ad gangen; vælg en anden dag for at skrive bagud)</span></p>
|
|
|
|
<div class="wt-card er-kort" id="er-spoergsmaal-kort">
|
|
<p class="wt-muted" style="margin:0 0 .2rem">Spørgsmål <span id="er-nr"></span></p>
|
|
<p class="er-spoergsmaal" id="er-spoergsmaal">Henter spørgsmålet…</p>
|
|
<p class="wt-muted" id="er-afrunding" hidden style="margin:0 0 .6rem">Det var det sidste spørgsmål om emnet for nu. Bagefter kan du holde pause, eller fortælle videre, hvis du har lyst.</p>
|
|
<div class="er-knapper" style="justify-content:flex-start">
|
|
<button type="button" class="wt-btn er-stor" id="er-hoer" hidden aria-label="Hør spørgsmålet læst op">🔊 Hør spørgsmålet</button>
|
|
<button type="button" class="wt-btn" id="er-spring" aria-label="Spring dette spørgsmål over">Spring over</button>
|
|
</div>
|
|
<audio id="er-lyd" preload="auto"></audio>
|
|
</div>
|
|
|
|
<div class="er-midt" id="er-optagelse">
|
|
<button type="button" class="er-optag" id="er-optag" aria-label="Optag dit svar" aria-pressed="false">
|
|
<span id="er-optag-tekst">Optag</span>
|
|
<span class="er-taeller" id="er-taeller" hidden>0:00</span>
|
|
</button>
|
|
<p class="er-status" id="er-status" aria-live="polite"></p>
|
|
<p class="er-tekst" id="er-live" hidden style="width:100%;max-width:32rem;text-align:left;min-height:2.4rem"><span id="er-live-fast"></span><span id="er-live-delvis" class="wt-muted"></span></p>
|
|
<p class="wt-muted" style="margin:0">Tal bare, som du ville fortælle det til et barnebarn.</p>
|
|
|
|
<div id="er-optaget" hidden style="width:100%;max-width:32rem">
|
|
<audio id="er-afspil" controls></audio>
|
|
<div class="er-knapper">
|
|
<button type="button" class="wt-btn wt-btn--primary er-stor" id="er-send">Send svaret</button>
|
|
<button type="button" class="wt-btn er-stor" id="er-om">Tag det om</button>
|
|
</div>
|
|
</div>
|
|
|
|
<button type="button" class="er-link" id="er-skriv-link">Skriv i stedet</button>
|
|
<div id="er-skriv" hidden style="width:100%;max-width:32rem;text-align:left">
|
|
<label for="er-tekstfelt" class="wt-muted">Skriv dit svar her</label>
|
|
<textarea id="er-tekstfelt" rows="5"></textarea>
|
|
<div class="er-knapper">
|
|
<button type="button" class="wt-btn wt-btn--primary er-stor" id="er-send-tekst">Send teksten</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="wt-card er-kort er-dit-svar" id="er-svar-kort" hidden>
|
|
<p class="wt-muted" style="margin:0 0 .3rem">Dit svar</p>
|
|
<p class="er-tekst" id="er-svar-tekst"></p>
|
|
<div class="er-ret" id="er-svar-ret"></div>
|
|
<div class="er-knapper">
|
|
<button type="button" class="wt-btn wt-btn--primary er-stor" id="er-naeste">Næste spørgsmål</button>
|
|
<button type="button" class="wt-btn er-stor" id="er-pause">Hold pause</button>
|
|
</div>
|
|
</div>
|
|
|
|
<h2>Dine svar i dag</h2>
|
|
<ul class="er-liste" id="er-dagens"></ul>
|
|
<p class="wt-muted" id="er-dagens-tom">Ingen endnu. Dit første svar kommer her.</p>
|
|
</section>
|
|
|
|
<!-- Trin 4: tidligere svar, dag for dag; "Afspil begge" spiller spørgsmålet og derefter svaret -->
|
|
<section id="er-tidligere">
|
|
<h2>{% if alle %}Alle svar{% else %}Tidligere svar{% endif %}</h2>
|
|
{% if klip_seneste %}
|
|
<ul class="er-liste">
|
|
{% for k in klip_seneste %}
|
|
{% if loop.first or k.dag != loop.previtem.dag %}
|
|
<li class="er-dagsskel"><h3 style="margin:1.2rem 0 .2rem;font-size:1.05rem"><span data-tid="{{ k.tid }}">{{ k.dag }}</span></h3></li>
|
|
{% endif %}
|
|
<li data-id="{{ k.id }}">
|
|
<p class="er-meta">{{ k.projekt_titel or k.projekt }}{% if k.varighed %} · {{ k.varighed // 60 }}:{{ '%02d' % (k.varighed % 60) }}{% endif %}</p>
|
|
<p class="er-q">{{ k.spoergsmaal }}</p>
|
|
<p class="er-tekst" data-tekst>{{ k.tekst }}</p>
|
|
<div class="er-knapper" style="justify-content:flex-start;margin-top:.4rem">
|
|
<button type="button" class="wt-btn" data-begge="{{ k.id }}" aria-label="Afspil spørgsmål og svar">▶ Spørgsmål og svar</button>
|
|
<button type="button" class="wt-btn" data-ret="{{ k.id }}">Ret teksten</button>
|
|
</div>
|
|
<audio controls preload="none" data-svar="{{ k.id }}" src="{{ rod }}/klip/{{ k.id }}.mp3{% if noegle %}?noegle={{ noegle }}{% endif %}" style="margin-top:.4rem"></audio>
|
|
<div class="er-ret"></div>
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
{% if not alle and antal_alle > klip_seneste|length %}
|
|
<p><a class="wt-btn" href="{{ rod }}/alle{% if noegle %}/{% endif %}">Se alle {{ antal_alle }} svar</a></p>
|
|
{% endif %}
|
|
{% else %}
|
|
<p class="wt-muted">Ingen svar endnu. Det første gemmes her, når du har fortalt noget.</p>
|
|
{% endif %}
|
|
</section>
|
|
</div>
|
|
|
|
<script>
|
|
(function () {
|
|
if (window.__erindringer) return;
|
|
window.__erindringer = true;
|
|
|
|
var side = document.getElementById('er-side');
|
|
var API = side.dataset.api, ROD = side.dataset.rod, NOEGLE = side.dataset.noegle || '';
|
|
var MAKS_SEK = 600; // 10 minutter pr. svar
|
|
var MIMER = ['audio/webm;codecs=opus', 'audio/webm', 'audio/mp4', 'audio/ogg;codecs=opus'];
|
|
|
|
function el(id) { return document.getElementById(id); }
|
|
var ui = {
|
|
emner: el('er-emner'), samtale: el('er-samtale'), titel: el('er-samtale-titel'),
|
|
nr: el('er-nr'), spoergsmaal: el('er-spoergsmaal'), hoer: el('er-hoer'), lyd: el('er-lyd'),
|
|
optagelse: el('er-optagelse'), optag: el('er-optag'), optagTekst: el('er-optag-tekst'), taeller: el('er-taeller'),
|
|
status: el('er-status'), optaget: el('er-optaget'), afspil: el('er-afspil'),
|
|
send: el('er-send'), om: el('er-om'), skrivLink: el('er-skriv-link'), skriv: el('er-skriv'),
|
|
tekstfelt: el('er-tekstfelt'), sendTekst: el('er-send-tekst'),
|
|
svarKort: el('er-svar-kort'), svarTekst: el('er-svar-tekst'), svarRet: el('er-svar-ret'),
|
|
naeste: el('er-naeste'), pause: el('er-pause'), dagens: el('er-dagens'), dagensTom: el('er-dagens-tom'),
|
|
flash: el('er-flash'), afrunding: el('er-afrunding'), spring: el('er-spring'),
|
|
dag: el('er-dag'), dato: el('er-dato'),
|
|
live: el('er-live'), liveFast: el('er-live-fast'), liveDelvis: el('er-live-delvis')
|
|
};
|
|
|
|
// Tilstand for den igangværende samtale
|
|
var st = {
|
|
projekt: null, titel: '', samtale: null, spoergsmaal: '', talt: '', lydSha: '', kilde: '', lydUrl: null,
|
|
kladdeId: null, kladdeKlar: false, dato: '', poll: null,
|
|
recorder: null, stream: null, chunks: [], blob: null, mime: '', ext: 'webm',
|
|
timer: null, sekunder: 0, startet: 0, sender: false, sendtIDag: 0, lydAfspillet: false
|
|
};
|
|
|
|
/* ---------- Hjælpere ---------- */
|
|
|
|
function visFlash(tekst) {
|
|
ui.flash.textContent = tekst;
|
|
ui.flash.hidden = !tekst;
|
|
}
|
|
function status(tekst, travl) {
|
|
ui.status.textContent = tekst || '';
|
|
ui.status.classList.toggle('travl', !!travl);
|
|
}
|
|
function mmss(s) {
|
|
s = Math.max(0, Math.round(s));
|
|
return Math.floor(s / 60) + ':' + ('0' + (s % 60)).slice(-2);
|
|
}
|
|
function datoTekst(iso) {
|
|
try {
|
|
var d = new Date(iso);
|
|
if (isNaN(d)) return iso.slice(0, 10);
|
|
return d.toLocaleDateString('da-DK', { day: 'numeric', month: 'long', year: 'numeric' });
|
|
} catch (e) { return iso.slice(0, 10); }
|
|
}
|
|
function lagerNoegle() { return 'erindringer:samtale:' + st.projekt; }
|
|
function husk(id) { try { sessionStorage.setItem(lagerNoegle(), id); } catch (e) {} }
|
|
function glem() { try { sessionStorage.removeItem(lagerNoegle()); } catch (e) {} }
|
|
function hentHusket() { try { return sessionStorage.getItem(lagerNoegle()); } catch (e) { return null; } }
|
|
|
|
// Fælles fetch: JSON-body eller FormData, altid med nøgle og marker-header.
|
|
// Ingen timeout: transskription kan tage lang tid.
|
|
function kald(sti, data) {
|
|
var erForm = data instanceof FormData;
|
|
if (erForm) data.append('noegle', NOEGLE); else data.noegle = NOEGLE;
|
|
var opts = { method: 'POST', credentials: 'same-origin', headers: { 'X-Requested-With': 'fetch' } };
|
|
if (erForm) { opts.body = data; }
|
|
else { opts.headers['Content-Type'] = 'application/json'; opts.body = JSON.stringify(data); }
|
|
return fetch(API + sti, opts).then(function (r) {
|
|
return r.text().then(function (t) {
|
|
var j = null;
|
|
try { j = t ? JSON.parse(t) : null; } catch (e) {}
|
|
if (!r.ok) throw new Error((j && j.fejl) || 'Noget gik galt (' + r.status + '). Prøv igen om lidt.');
|
|
return j || {};
|
|
});
|
|
}, function () {
|
|
throw new Error('Vi kunne ikke få forbindelse. Tjek, at du har net, og prøv igen.');
|
|
});
|
|
}
|
|
|
|
/* ---------- Ret teksten (bruges både i dag og i tidligere svar) ---------- */
|
|
|
|
// tekstEls: ét eller flere elementer, der viser samme tekst og skal følges ad
|
|
function retTekst(id, tekstEls, holder) {
|
|
if (holder.querySelector('textarea')) return;
|
|
if (!Array.isArray(tekstEls)) tekstEls = [tekstEls];
|
|
holder.innerHTML = '';
|
|
var ta = document.createElement('textarea');
|
|
ta.value = tekstEls[0].textContent;
|
|
ta.rows = 6;
|
|
var gem = document.createElement('button');
|
|
gem.type = 'button'; gem.className = 'wt-btn wt-btn--primary'; gem.textContent = 'Gem';
|
|
var fortryd = document.createElement('button');
|
|
fortryd.type = 'button'; fortryd.className = 'wt-btn'; fortryd.textContent = 'Fortryd';
|
|
var knapper = document.createElement('div');
|
|
knapper.className = 'er-knapper'; knapper.style.justifyContent = 'flex-start';
|
|
knapper.appendChild(gem); knapper.appendChild(fortryd);
|
|
holder.appendChild(ta); holder.appendChild(knapper);
|
|
ta.focus();
|
|
|
|
function tilbage() {
|
|
holder.innerHTML = '';
|
|
var b = document.createElement('button');
|
|
b.type = 'button'; b.className = 'wt-btn'; b.textContent = 'Ret teksten';
|
|
b.setAttribute('data-ret', id);
|
|
holder.appendChild(b);
|
|
}
|
|
fortryd.addEventListener('click', tilbage);
|
|
gem.addEventListener('click', function () {
|
|
var ny = ta.value.trim();
|
|
gem.disabled = true; fortryd.disabled = true; gem.textContent = 'Gemmer…';
|
|
kald('/ret', { id: id, tekst: ny }).then(function () {
|
|
tekstEls.forEach(function (t) { t.textContent = ny; });
|
|
tilbage();
|
|
visFlash('Teksten er rettet.');
|
|
}).catch(function (e) {
|
|
gem.disabled = false; fortryd.disabled = false; gem.textContent = 'Gem';
|
|
visFlash(e.message);
|
|
});
|
|
});
|
|
}
|
|
|
|
// Alle "Ret teksten"-knapper, også dem der oprettes senere
|
|
document.addEventListener('click', function (e) {
|
|
var b = e.target.closest && e.target.closest('[data-ret]');
|
|
if (!b) return;
|
|
var li = b.closest('[data-id]');
|
|
var tekstEl = li ? li.querySelector('[data-tekst]') : null;
|
|
if (!li || !tekstEl) return;
|
|
var els = [tekstEl];
|
|
// Er det dagens seneste svar, skal "Dit svar"-kortet følge med
|
|
if (!ui.svarKort.hidden && ui.svarKort.dataset.id === b.dataset.ret) els.push(ui.svarTekst);
|
|
retTekst(b.dataset.ret, els, b.parentNode);
|
|
});
|
|
|
|
// "Spørgsmål og svar": ÉT afspiller-element spiller først spørgsmålet og skifter så til svaret.
|
|
// (To elementer virker ikke på iPhone: kun det element, man selv trykkede på, må spille videre.)
|
|
document.addEventListener('click', function (e) {
|
|
var b = e.target.closest && e.target.closest('[data-begge]');
|
|
if (!b) return;
|
|
var li = b.closest('[data-id]');
|
|
var sv = li && li.querySelector('audio[data-svar]');
|
|
if (!sv) return;
|
|
document.querySelectorAll('audio').forEach(function (a) { if (a !== sv) { try { a.pause(); } catch (x) {} } });
|
|
var svarSrc = sv.dataset.svarSrc || sv.getAttribute('src');
|
|
sv.dataset.svarSrc = svarSrc;
|
|
var spSrc = svarSrc.replace(/\.mp3(\?|$)/, '-sp.mp3$1');
|
|
var tilSvar = function () {
|
|
sv.onended = null; sv.onerror = null;
|
|
sv.src = svarSrc; sv.currentTime = 0;
|
|
var p = sv.play(); if (p && p.catch) p.catch(function () {});
|
|
};
|
|
sv.onended = tilSvar;
|
|
sv.onerror = tilSvar; // intet spørgsmål gemt (eller det kunne ikke laves): spil svaret
|
|
sv.src = spSrc; sv.currentTime = 0;
|
|
var p = sv.play(); if (p && p.catch) p.catch(tilSvar);
|
|
});
|
|
|
|
// Datoer i "Tidligere svar" på læsbart dansk
|
|
document.querySelectorAll('[data-tid]').forEach(function (s) { s.textContent = datoTekst(s.dataset.tid); });
|
|
|
|
/* ---------- Emnevalg ---------- */
|
|
|
|
function vaelg(slug) {
|
|
var kort = ui.emner.querySelector('[data-slug="' + slug + '"]');
|
|
if (!kort) return;
|
|
st.projekt = slug;
|
|
st.titel = kort.dataset.titel || slug;
|
|
// Dagbogen føres én dag ad gangen: serveren vælger samtalen ud fra datoen
|
|
st.dato = (slug === 'dagbog') ? ui.dato.value : '';
|
|
ui.dag.hidden = (slug !== 'dagbog');
|
|
st.samtale = (slug === 'dagbog') ? null : hentHusket();
|
|
st.sendtIDag = 0;
|
|
ui.titel.textContent = st.titel;
|
|
ui.dagens.innerHTML = '';
|
|
ui.dagensTom.hidden = false;
|
|
visFlash('');
|
|
ui.emner.hidden = true;
|
|
ui.samtale.hidden = false;
|
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
hentSpoergsmaal(true);
|
|
}
|
|
|
|
ui.emner.addEventListener('click', function (e) {
|
|
var b = e.target.closest && e.target.closest('[data-vaelg]');
|
|
if (b) vaelg(b.dataset.vaelg);
|
|
});
|
|
ui.dato.addEventListener('change', function () {
|
|
if (st.projekt !== 'dagbog' || st.sender) return;
|
|
st.dato = ui.dato.value;
|
|
st.samtale = null;
|
|
ui.dagens.innerHTML = ''; ui.dagensTom.hidden = false;
|
|
hentSpoergsmaal(false);
|
|
});
|
|
|
|
function holdPause() {
|
|
stopStream();
|
|
stopTimer();
|
|
nulstilOptagelse();
|
|
ui.svarKort.hidden = true;
|
|
ui.samtale.hidden = true;
|
|
ui.emner.hidden = false;
|
|
// Opdatér tællingen på emnekortet
|
|
var kort = ui.emner.querySelector('[data-slug="' + st.projekt + '"] .er-antal');
|
|
if (kort) kort.textContent = (parseInt(kort.textContent, 10) || 0) + st.sendtIDag;
|
|
st.sendtIDag = 0;
|
|
visFlash('Tak for i dag. Dine svar er gemt.');
|
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
}
|
|
ui.pause.addEventListener('click', holdPause);
|
|
|
|
/* ---------- Spørgsmål ---------- */
|
|
|
|
function hentSpoergsmaal(foersteForsoeg) {
|
|
ui.spoergsmaal.textContent = 'Henter spørgsmålet…';
|
|
ui.nr.textContent = '';
|
|
ui.hoer.hidden = true;
|
|
ui.svarKort.hidden = true;
|
|
nulstilOptagelse();
|
|
ui.optagelse.hidden = false;
|
|
status('');
|
|
kald('/naeste', { projekt: st.projekt, samtale: st.samtale || null, sprunget: hentSprunget(), dato: st.dato || '' }).then(function (j) {
|
|
st.samtale = j.samtale;
|
|
husk(j.samtale);
|
|
st.spoergsmaal = j.spoergsmaal || '';
|
|
st.talt = j.talt || st.spoergsmaal;
|
|
st.lydSha = j.lyd_sha || '';
|
|
st.kilde = j.kilde || '';
|
|
st.lydUrl = j.lyd_url || null;
|
|
ui.spoergsmaal.textContent = st.spoergsmaal;
|
|
ui.afrunding.hidden = !j.afslut;
|
|
ui.nr.textContent = j.nr ? String(j.nr) : '';
|
|
ui.hoer.hidden = !st.lydUrl;
|
|
if (st.lydUrl) {
|
|
ui.lyd.src = st.lydUrl;
|
|
// Afspil automatisk, hvis browseren tillader det; ellers kun på klik
|
|
var p = ui.lyd.play();
|
|
if (p && p.catch) p.catch(function () {});
|
|
}
|
|
}).catch(function (e) {
|
|
// Et gammelt samtale-id kan være udløbet: prøv én gang forfra
|
|
if (foersteForsoeg && st.samtale) { st.samtale = null; glem(); return hentSpoergsmaal(false); }
|
|
ui.spoergsmaal.textContent = 'Vi kunne ikke hente et spørgsmål.';
|
|
visFlash(e.message + ' Du kan holde pause og prøve igen senere.');
|
|
});
|
|
}
|
|
|
|
ui.hoer.addEventListener('click', function () {
|
|
try {
|
|
ui.lyd.currentTime = 0;
|
|
var p = ui.lyd.play();
|
|
if (p && p.catch) p.catch(function () { visFlash('Lyden kunne ikke afspilles. Du kan læse spørgsmålet ovenfor.'); });
|
|
} catch (e) {}
|
|
});
|
|
ui.naeste.addEventListener('click', function () { hentSpoergsmaal(false); });
|
|
|
|
// Sprungne spørgsmål huskes pr. projekt, så intervieweren ikke stiller dem igen
|
|
function sprungetNoegle() { return 'erindringer:sprunget:' + st.projekt; }
|
|
function hentSprunget() { try { return JSON.parse(sessionStorage.getItem(sprungetNoegle()) || '[]'); } catch (e) { return []; } }
|
|
ui.spring.addEventListener('click', function () {
|
|
if (st.sender || !st.spoergsmaal) return;
|
|
try { ui.lyd.pause(); } catch (e) {}
|
|
var liste = hentSprunget();
|
|
if (liste.indexOf(st.spoergsmaal) < 0) liste.push(st.spoergsmaal);
|
|
try { sessionStorage.setItem(sprungetNoegle(), JSON.stringify(liste.slice(-50))); } catch (e) {}
|
|
hentSpoergsmaal(false);
|
|
});
|
|
|
|
/* ---------- Optagelse ---------- */
|
|
|
|
function vaelgMime() {
|
|
if (!window.MediaRecorder || !MediaRecorder.isTypeSupported) return '';
|
|
for (var i = 0; i < MIMER.length; i++) if (MediaRecorder.isTypeSupported(MIMER[i])) return MIMER[i];
|
|
return '';
|
|
}
|
|
function endelse(mime) {
|
|
if (mime.indexOf('mp4') > -1) return 'mp4';
|
|
if (mime.indexOf('ogg') > -1) return 'ogg';
|
|
return 'webm';
|
|
}
|
|
function stopStream() {
|
|
if (st.stream) { st.stream.getTracks().forEach(function (t) { t.stop(); }); st.stream = null; }
|
|
}
|
|
function stopTimer() {
|
|
if (st.timer) { clearInterval(st.timer); st.timer = null; }
|
|
}
|
|
function nulstilOptagelse() {
|
|
st.blob = null; st.chunks = []; st.sekunder = 0;
|
|
ui.optag.disabled = false;
|
|
ui.optag.classList.remove('optager');
|
|
ui.optag.setAttribute('aria-pressed', 'false');
|
|
ui.optagTekst.textContent = 'Optag';
|
|
ui.taeller.hidden = true; ui.taeller.textContent = '0:00';
|
|
ui.optag.hidden = false;
|
|
ui.optaget.hidden = true;
|
|
if (ui.afspil.src) { try { URL.revokeObjectURL(ui.afspil.src); } catch (e) {} ui.afspil.removeAttribute('src'); }
|
|
ui.skriv.hidden = true; ui.skrivLink.hidden = false;
|
|
ui.tekstfelt.value = '';
|
|
ui.send.disabled = false; ui.om.disabled = false; ui.sendTekst.disabled = false;
|
|
st.sender = false;
|
|
}
|
|
|
|
function startOptagelse() {
|
|
visFlash('');
|
|
var mime = vaelgMime();
|
|
if (!window.MediaRecorder || !navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
|
visFlash('Din browser kan ikke optage lyd. Brug "Skriv i stedet" herunder.');
|
|
visSkriv();
|
|
return;
|
|
}
|
|
ui.optag.disabled = true;
|
|
status('Gør mikrofonen klar…');
|
|
liveForbered(); // AudioContext skal skabes i klikket (iOS), og tokenet hentes imens
|
|
navigator.mediaDevices.getUserMedia({ audio: true }).then(function (stream) {
|
|
st.stream = stream;
|
|
st.chunks = [];
|
|
st.mime = mime;
|
|
st.ext = endelse(mime);
|
|
liveStart(stream);
|
|
try {
|
|
st.recorder = mime ? new MediaRecorder(stream, { mimeType: mime }) : new MediaRecorder(stream);
|
|
} catch (e) {
|
|
stopStream();
|
|
throw new Error('Optageren kunne ikke startes. Brug "Skriv i stedet", eller prøv en anden browser.');
|
|
}
|
|
if (!st.mime) st.mime = st.recorder.mimeType || '';
|
|
st.ext = endelse(st.mime);
|
|
st.recorder.addEventListener('dataavailable', function (e) { if (e.data && e.data.size) st.chunks.push(e.data); });
|
|
st.recorder.addEventListener('stop', optagelseFaerdig);
|
|
st.recorder.addEventListener('error', function () {
|
|
visFlash('Optagelsen blev afbrudt. Prøv igen, eller brug "Skriv i stedet".');
|
|
stopOptagelse();
|
|
});
|
|
st.recorder.start(1000);
|
|
st.startet = Date.now();
|
|
st.sekunder = 0;
|
|
ui.optag.disabled = false;
|
|
ui.optag.classList.add('optager');
|
|
ui.optag.setAttribute('aria-pressed', 'true');
|
|
ui.optag.setAttribute('aria-label', 'Stop optagelsen');
|
|
ui.optagTekst.textContent = 'Stop';
|
|
ui.taeller.hidden = false; ui.taeller.textContent = '0:00';
|
|
ui.skrivLink.hidden = true;
|
|
status('Vi optager. Tryk på knappen, når du er færdig.');
|
|
st.timer = setInterval(function () {
|
|
st.sekunder = Math.round((Date.now() - st.startet) / 1000);
|
|
ui.taeller.textContent = mmss(st.sekunder);
|
|
if (st.sekunder >= MAKS_SEK) {
|
|
stopOptagelse();
|
|
visFlash('Vi stoppede optagelsen efter 10 minutter. Du kan sende svaret nu og fortsætte på næste spørgsmål.');
|
|
}
|
|
}, 1000);
|
|
}).catch(function (e) {
|
|
stopStream();
|
|
ui.optag.disabled = false;
|
|
status('');
|
|
var navn = e && e.name;
|
|
if (navn === 'NotAllowedError' || navn === 'SecurityError' || navn === 'PermissionDeniedError') {
|
|
visFlash('Vi fik ikke lov til at bruge mikrofonen. Tillad mikrofonen i browseren, eller brug "Skriv i stedet".');
|
|
} else if (navn === 'NotFoundError' || navn === 'DevicesNotFoundError') {
|
|
visFlash('Vi kunne ikke finde en mikrofon. Brug "Skriv i stedet".');
|
|
} else {
|
|
visFlash((e && e.message) || 'Mikrofonen kunne ikke startes. Prøv igen, eller brug "Skriv i stedet".');
|
|
}
|
|
visSkriv();
|
|
});
|
|
}
|
|
|
|
function stopOptagelse() {
|
|
stopTimer();
|
|
ui.optag.disabled = true;
|
|
status('Gør optagelsen klar…');
|
|
liveStop();
|
|
try {
|
|
if (st.recorder && st.recorder.state !== 'inactive') st.recorder.stop();
|
|
else optagelseFaerdig();
|
|
} catch (e) { optagelseFaerdig(); }
|
|
}
|
|
|
|
/* ---------- Live-afskrift (ElevenLabs realtime STT) ----------
|
|
Mikrofonen sendes som 16 kHz PCM direkte til ElevenLabs over en WebSocket med et
|
|
engangs-token fra serveren (API-nøglen bliver på serveren). Teksten vokser, mens man
|
|
taler; ved stop sendes den med optagelsen, så serveren ikke skal skrive af bagefter. */
|
|
var live = { ctx: null, node: null, src: null, ws: null, token: null, fast: '', delvis: '', aktiv: false, luk: null };
|
|
|
|
function liveForbered() {
|
|
live.fast = ''; live.delvis = ''; live.aktiv = false; live.token = null;
|
|
try { ui.live.hidden = true; ui.liveFast.textContent = ''; ui.liveDelvis.textContent = ''; } catch (e) {}
|
|
try {
|
|
var AC = window.AudioContext || window.webkitAudioContext;
|
|
if (AC && !live.ctx) {
|
|
try { live.ctx = new AC({ sampleRate: 16000 }); } catch (e) { live.ctx = new AC(); }
|
|
}
|
|
if (live.ctx && live.ctx.state === 'suspended') live.ctx.resume().catch(function () {});
|
|
} catch (e) { live.ctx = null; }
|
|
live.token = kald('/stt-token', {}).catch(function () { return null; });
|
|
}
|
|
|
|
function liveStart(stream) {
|
|
if (!live.ctx || !window.WebSocket) return;
|
|
live.token.then(function (t) {
|
|
if (!t || !t.url || !st.stream) return;
|
|
var ws;
|
|
try { ws = new WebSocket(t.url); } catch (e) { return; }
|
|
live.ws = ws;
|
|
ws.onmessage = function (ev) {
|
|
var m; try { m = JSON.parse(ev.data); } catch (e) { return; }
|
|
if (m.message_type === 'partial_transcript') { live.delvis = m.text || ''; }
|
|
else if (m.message_type === 'committed_transcript') {
|
|
if (m.text) live.fast = (live.fast + ' ' + m.text).trim();
|
|
live.delvis = '';
|
|
}
|
|
liveVis();
|
|
};
|
|
ws.onclose = function () { live.aktiv = false; if (live.luk) { live.luk(); live.luk = null; } };
|
|
ws.onerror = function () { live.aktiv = false; };
|
|
ws.onopen = function () {
|
|
if (!st.stream) { try { ws.close(); } catch (e) {} return; }
|
|
live.aktiv = true;
|
|
ui.live.hidden = false;
|
|
try {
|
|
live.src = live.ctx.createMediaStreamSource(stream);
|
|
live.node = live.ctx.createScriptProcessor(4096, 1, 1);
|
|
var faktor = live.ctx.sampleRate / 16000; // Safari ignorerer af og til ønsket samplerate
|
|
live.node.onaudioprocess = function (e) {
|
|
if (!live.aktiv || ws.readyState !== 1) return;
|
|
var inp = e.inputBuffer.getChannelData(0);
|
|
var n = Math.floor(inp.length / faktor);
|
|
var pcm = new Int16Array(n);
|
|
for (var i = 0; i < n; i++) {
|
|
var v = inp[Math.floor(i * faktor)];
|
|
v = Math.max(-1, Math.min(1, v));
|
|
pcm[i] = v < 0 ? v * 32768 : v * 32767;
|
|
}
|
|
var bytes = new Uint8Array(pcm.buffer), bin = '';
|
|
for (var j = 0; j < bytes.length; j += 8192) bin += String.fromCharCode.apply(null, bytes.subarray(j, j + 8192));
|
|
ws.send(JSON.stringify({ message_type: 'input_audio_chunk', audio_base_64: btoa(bin), commit: false, sample_rate: 16000 }));
|
|
};
|
|
live.src.connect(live.node);
|
|
live.node.connect(live.ctx.destination);
|
|
} catch (e) { live.aktiv = false; }
|
|
};
|
|
});
|
|
}
|
|
|
|
function liveVis() {
|
|
ui.liveFast.textContent = live.fast;
|
|
ui.liveDelvis.textContent = live.delvis ? (' ' + live.delvis) : '';
|
|
}
|
|
|
|
// Stopper lydstrømmen, beder om den sidste commit og giver teksten max 2,5 s til at lande
|
|
function liveStop() {
|
|
live.aktiv = false;
|
|
try { if (live.node) { live.node.disconnect(); live.node.onaudioprocess = null; } if (live.src) live.src.disconnect(); } catch (e) {}
|
|
live.node = null; live.src = null;
|
|
var ws = live.ws; live.ws = null;
|
|
if (ws && ws.readyState === 1) {
|
|
try { ws.send(JSON.stringify({ message_type: 'input_audio_chunk', audio_base_64: '', commit: true, sample_rate: 16000 })); } catch (e) {}
|
|
live.aktiv = true; // modtag stadig beskeder, til vi lukker
|
|
setTimeout(function () { live.aktiv = false; try { ws.close(); } catch (e) {} }, 2500);
|
|
}
|
|
}
|
|
|
|
function liveTekst() {
|
|
return (live.fast + (live.delvis ? ' ' + live.delvis : '')).trim();
|
|
}
|
|
|
|
function optagelseFaerdig() {
|
|
stopStream(); // ellers bliver den røde mikrofon-prik stående på iPhone
|
|
ui.optag.classList.remove('optager');
|
|
ui.optag.setAttribute('aria-pressed', 'false');
|
|
ui.optag.setAttribute('aria-label', 'Optag dit svar');
|
|
ui.optagTekst.textContent = 'Optag';
|
|
ui.taeller.hidden = true;
|
|
if (!st.chunks.length) {
|
|
nulstilOptagelse();
|
|
status('');
|
|
visFlash('Der kom ingen lyd med. Prøv igen, og tal tæt på telefonen.');
|
|
return;
|
|
}
|
|
st.blob = new Blob(st.chunks, { type: st.mime || 'application/octet-stream' });
|
|
ui.afspil.src = URL.createObjectURL(st.blob);
|
|
ui.optag.hidden = true;
|
|
ui.optaget.hidden = false;
|
|
status('Hør dit svar igennem, og send det, når du er tilfreds.');
|
|
ui.send.focus();
|
|
// Giv live-afskriften op til 2 s til at sende den sidste sætning, før kladden går op
|
|
if (live.ws || live.aktiv) {
|
|
var blob = st.blob, sendt = false;
|
|
var go = function () { if (!sendt && st.blob === blob) { sendt = true; sendKladde(); } };
|
|
live.luk = go;
|
|
setTimeout(go, 2200);
|
|
} else sendKladde();
|
|
}
|
|
|
|
// Optagelsen sendes op med det samme som kladde, så afskriften er i gang, mens man lytter.
|
|
// "Send svaret" bekræfter bare; "Tag det om" sletter kladden igen.
|
|
function sendKladde() {
|
|
st.kladdeId = null; st.kladdeKlar = false;
|
|
var form = new FormData();
|
|
form.append('lyd', st.blob, 'svar.' + st.ext);
|
|
form.append('kladde', '1');
|
|
// Live-afskriften (hvis den var med) sendes med, så serveren slipper for at skrive af bagefter.
|
|
// Vent et øjeblik på den sidste committed-besked, så kladden får hele teksten med.
|
|
var t = liveTekst();
|
|
if (t) form.append('tekst', t);
|
|
form.append('projekt', st.projekt);
|
|
form.append('samtale', st.samtale || '');
|
|
form.append('spoergsmaal', st.spoergsmaal);
|
|
form.append('spoergsmaal_talt', st.talt || '');
|
|
form.append('lyd_sha', st.lydSha || '');
|
|
form.append('kilde', st.kilde || '');
|
|
var blob = st.blob;
|
|
kald('/svar', form).then(function (j) {
|
|
if (st.blob !== blob) { kald('/slet', { id: j.id }).catch(function () {}); return; } // taget om imens
|
|
st.kladdeId = j.id;
|
|
st.kladdeKlar = true;
|
|
if (j.samtale) { st.samtale = j.samtale; if (st.projekt !== 'dagbog') husk(j.samtale); }
|
|
if (st.sender) bekraeft(); // brugeren trykkede "Send" før kladden var oppe
|
|
}).catch(function () {
|
|
st.kladdeKlar = false; // falder tilbage til almindelig sending ved "Send"
|
|
});
|
|
}
|
|
|
|
function bekraeft() {
|
|
kald('/bekraeft', { id: st.kladdeId }).then(function (j) {
|
|
st.sendtIDag++;
|
|
visSvar(j);
|
|
}).catch(function (e) {
|
|
st.sender = false;
|
|
ui.send.disabled = false; ui.om.disabled = false; ui.optag.disabled = false;
|
|
status('Svaret blev ikke sendt. Det er stadig her, så prøv igen.');
|
|
visFlash(e.message);
|
|
});
|
|
}
|
|
|
|
ui.optag.addEventListener('click', function () {
|
|
if (st.recorder && st.recorder.state === 'recording') stopOptagelse(); else startOptagelse();
|
|
});
|
|
ui.om.addEventListener('click', function () {
|
|
if (st.kladdeId) { kald('/slet', { id: st.kladdeId }).catch(function () {}); }
|
|
st.kladdeId = null; st.kladdeKlar = false;
|
|
nulstilOptagelse();
|
|
status('');
|
|
visFlash('');
|
|
});
|
|
|
|
/* ---------- Skriv i stedet ---------- */
|
|
|
|
function visSkriv() {
|
|
ui.skriv.hidden = false;
|
|
ui.skrivLink.hidden = true;
|
|
ui.tekstfelt.focus();
|
|
}
|
|
ui.skrivLink.addEventListener('click', visSkriv);
|
|
|
|
/* ---------- Send svar ---------- */
|
|
|
|
function sendSvar(form) {
|
|
if (st.sender) return;
|
|
st.sender = true;
|
|
ui.send.disabled = true; ui.om.disabled = true; ui.sendTekst.disabled = true; ui.optag.disabled = true;
|
|
status('Gemmer og skriver af… Det kan tage op til et halvt minut.', true);
|
|
visFlash('');
|
|
form.append('projekt', st.projekt);
|
|
form.append('samtale', st.samtale || '');
|
|
form.append('spoergsmaal', st.spoergsmaal);
|
|
form.append('spoergsmaal_talt', st.talt || '');
|
|
form.append('lyd_sha', st.lydSha || '');
|
|
form.append('kilde', st.kilde || '');
|
|
kald('/svar', form).then(function (j) {
|
|
st.sendtIDag++;
|
|
visSvar(j);
|
|
}).catch(function (e) {
|
|
// Behold det optagede, så det kan sendes igen
|
|
st.sender = false;
|
|
ui.send.disabled = false; ui.om.disabled = false; ui.sendTekst.disabled = false; ui.optag.disabled = false;
|
|
status('Svaret blev ikke sendt. Det er stadig her, så prøv igen.');
|
|
visFlash(e.message);
|
|
});
|
|
}
|
|
|
|
ui.send.addEventListener('click', function () {
|
|
if (!st.blob) { visFlash('Der er ikke noget optaget endnu. Tryk på Optag først.'); return; }
|
|
if (st.sender) return;
|
|
if (st.kladdeId || st.kladdeKlar !== false) {
|
|
// Kladden er oppe (eller på vej): bekræft, i stedet for at sende lyden igen
|
|
st.sender = true;
|
|
ui.send.disabled = true; ui.om.disabled = true; ui.optag.disabled = true;
|
|
status('Gemmer…', true);
|
|
visFlash('');
|
|
if (st.kladdeId) bekraeft();
|
|
return;
|
|
}
|
|
var form = new FormData();
|
|
form.append('lyd', st.blob, 'svar.' + st.ext);
|
|
sendSvar(form);
|
|
});
|
|
ui.sendTekst.addEventListener('click', function () {
|
|
var t = ui.tekstfelt.value.trim();
|
|
if (!t) { visFlash('Skriv lidt i feltet først.'); ui.tekstfelt.focus(); return; }
|
|
var form = new FormData();
|
|
form.append('tekst', t);
|
|
sendSvar(form);
|
|
});
|
|
|
|
function visSvar(j) {
|
|
var venter = (j.status === 'transskriberes');
|
|
var tekst = j.tekst || (venter ? 'Teksten er på vej. Du kan gå videre til næste spørgsmål imens.' : '');
|
|
ui.optagelse.hidden = true;
|
|
status('');
|
|
ui.svarTekst.textContent = tekst;
|
|
ui.svarTekst.classList.toggle('wt-muted', venter);
|
|
ui.svarKort.dataset.id = j.id;
|
|
ui.svarKort.hidden = false;
|
|
var dagensTekst = tilfoejDagens(j, j.tekst || '');
|
|
ui.svarRet.innerHTML = '';
|
|
var ret = document.createElement('button');
|
|
ret.type = 'button'; ret.className = 'wt-btn'; ret.textContent = 'Ret teksten';
|
|
ret.addEventListener('click', function () { retTekst(j.id, [ui.svarTekst, dagensTekst], ui.svarRet); });
|
|
ret.hidden = venter;
|
|
ui.svarRet.appendChild(ret);
|
|
ui.naeste.focus();
|
|
st.kladdeId = null; st.kladdeKlar = false;
|
|
nulstilOptagelse();
|
|
if (venter) ventPaaTekst(j.id, [ui.svarTekst, dagensTekst], ret);
|
|
}
|
|
|
|
// Afskriften kommer i baggrunden: spørg hvert par sekunder, og sæt teksten ind, når den er der
|
|
function ventPaaTekst(id, els, retKnap) {
|
|
if (st.poll) clearInterval(st.poll);
|
|
var n = 0;
|
|
st.poll = setInterval(function () {
|
|
n++;
|
|
kald('/status', { id: id }).then(function (j) {
|
|
if (j.status !== 'transskriberes' || n > 60) {
|
|
clearInterval(st.poll); st.poll = null;
|
|
var t = j.tekst || (j.status === 'fejl' ? 'Afskriften lykkedes ikke; lyden er gemt, og du kan skrive teksten selv.' : '');
|
|
els.forEach(function (e) { if (e) { e.textContent = t; e.classList.remove('wt-muted'); } });
|
|
if (retKnap) retKnap.hidden = false;
|
|
}
|
|
}).catch(function () {});
|
|
}, 2000);
|
|
}
|
|
|
|
// Sætter svaret øverst i "Dine svar i dag" og returnerer tekst-elementet
|
|
function tilfoejDagens(j, tekst) {
|
|
var li = document.createElement('li');
|
|
li.setAttribute('data-id', j.id);
|
|
var q = document.createElement('p'); q.className = 'er-q'; q.textContent = st.spoergsmaal;
|
|
var t = document.createElement('p'); t.className = 'er-tekst'; t.setAttribute('data-tekst', ''); t.textContent = tekst;
|
|
li.appendChild(q); li.appendChild(t);
|
|
if (j.varighed) {
|
|
var m = document.createElement('p'); m.className = 'er-meta'; m.textContent = 'Varighed ' + mmss(j.varighed);
|
|
li.appendChild(m);
|
|
}
|
|
var knapper = document.createElement('div'); knapper.className = 'er-knapper'; knapper.style.justifyContent = 'flex-start';
|
|
if (!j.skrevet) {
|
|
var begge = document.createElement('button');
|
|
begge.type = 'button'; begge.className = 'wt-btn'; begge.textContent = '▶ Spørgsmål og svar'; begge.setAttribute('data-begge', j.id);
|
|
knapper.appendChild(begge);
|
|
}
|
|
var b = document.createElement('button');
|
|
b.type = 'button'; b.className = 'wt-btn'; b.textContent = 'Ret teksten'; b.setAttribute('data-ret', j.id);
|
|
knapper.appendChild(b); li.appendChild(knapper);
|
|
if (!j.skrevet) {
|
|
var a = document.createElement('audio'); a.controls = true; a.preload = 'none'; a.setAttribute('data-svar', j.id);
|
|
a.src = ROD + '/klip/' + j.id + '.mp3' + (NOEGLE ? ('?noegle=' + encodeURIComponent(NOEGLE)) : '');
|
|
a.style.marginTop = '.4rem'; li.appendChild(a);
|
|
}
|
|
var ret = document.createElement('div'); ret.className = 'er-ret'; li.appendChild(ret);
|
|
ui.dagens.insertBefore(li, ui.dagens.firstChild);
|
|
ui.dagensTom.hidden = true;
|
|
return t;
|
|
}
|
|
|
|
// Slip mikrofonen, hvis siden forlades midt i en optagelse
|
|
window.addEventListener('pagehide', function () { stopTimer(); stopStream(); });
|
|
|
|
// Åbn et projekt med det samme, hvis serveren bad om det
|
|
if (side.dataset.valgt) vaelg(side.dataset.valgt);
|
|
})();
|
|
</script>
|
|
{% endblock %}
|