Files
SPA-landing/dictation/index.html

1368 lines
51 KiB
HTML

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>PowerTools Diktat</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
min-height: 100vh;
color: #fff;
padding: 15px;
}
.container {
max-width: 500px;
margin: 0 auto;
}
h1 {
text-align: center;
font-size: 1.4rem;
margin-bottom: 8px;
color: #90CAF9;
}
.session-info {
text-align: center;
font-size: 0.75rem;
color: #666;
margin-bottom: 12px;
}
.status {
text-align: center;
padding: 8px;
border-radius: 8px;
margin-bottom: 15px;
font-size: 0.85rem;
}
.status.ready { background: #2D5A27; }
.status.error { background: #8B0000; }
.status.recording { background: #FF6B00; animation: pulse 1s infinite; }
.status.paused { background: #1a4a6e; }
.status.sent { background: #2D5A27; }
.status.voice-active { background: #9C27B0; }
.status.mode-active { background: #00695C; }
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.7; } }
#dictateBtn {
width: 100%;
padding: 20px;
font-size: 1.2rem;
border: none;
border-radius: 12px;
background: linear-gradient(135deg, #1a4a6e 0%, #2D5A27 100%);
color: white;
cursor: pointer;
margin-bottom: 15px;
transition: transform 0.1s, opacity 0.2s;
}
#dictateBtn:active { transform: scale(0.98); }
#dictateBtn:disabled { opacity: 0.5; }
#dictateBtn.recording {
background: linear-gradient(135deg, #FF6B00 0%, #FF4500 100%);
}
#dictateBtn.paused {
background: linear-gradient(135deg, #1a4a6e 0%, #3a7a9e 100%);
}
#textArea {
width: 100%;
background: rgba(255,255,255,0.1);
border: 1px solid rgba(255,255,255,0.2);
border-radius: 12px;
padding: 12px;
min-height: 120px;
margin-bottom: 12px;
font-size: 1rem;
line-height: 1.4;
color: #fff;
resize: vertical;
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
caret-color: #90CAF9;
}
#textArea::placeholder { color: #666; }
#textArea:focus {
outline: none;
border-color: #90CAF9;
background: rgba(255,255,255,0.15);
}
.mode-indicator {
display: none;
align-items: center;
justify-content: center;
gap: 8px;
padding: 6px 12px;
background: rgba(0, 105, 92, 0.5);
border-radius: 8px;
margin-bottom: 10px;
font-size: 0.8rem;
color: #A5D6A7;
}
.mode-indicator.active { display: flex; }
.btn-row {
display: flex;
gap: 6px;
margin-bottom: 8px;
flex-wrap: wrap;
}
.btn-row button {
flex: 1;
min-width: 55px;
padding: 10px 4px;
font-size: 0.8rem;
border: none;
border-radius: 8px;
cursor: pointer;
transition: opacity 0.2s, transform 0.1s;
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
}
.btn-row button:active { transform: scale(0.95); }
.btn-row button:disabled { opacity: 0.4; }
.btn-row button .icon { font-size: 1.1rem; }
.btn-row button .label { font-size: 0.6rem; }
.btn-spell { background: #37474F; color: white; }
.btn-spell.active { background: #4CAF50; }
.btn-delete { background: #5D4037; color: white; }
.btn-action { background: #1565C0; color: white; }
.btn-format { background: #6A1B9A; color: white; }
.btn-select { background: #00695C; color: white; }
.btn-voice { background: #37474F; color: white; }
.btn-voice.active { background: #9C27B0; }
.btn-nav { background: #FF8F00; color: white; }
#sendBtn {
width: 100%;
padding: 18px;
font-size: 1.1rem;
border: none;
border-radius: 12px;
background: #2D5A27;
color: white;
cursor: pointer;
margin-top: 5px;
}
#sendBtn:disabled { opacity: 0.4; }
.info {
margin-top: 15px;
padding: 10px;
background: rgba(255,255,255,0.05);
border-radius: 8px;
font-size: 0.65rem;
color: #888;
}
.info h4 { color: #90CAF9; margin-bottom: 5px; font-size: 0.75rem; }
.info-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px;
}
.info-section h5 { color: #CE93D8; margin-bottom: 3px; font-size: 0.7rem; }
.info-section ul { margin-left: 10px; }
.info-section li { margin: 1px 0; }
.voice-indicator {
display: none;
align-items: center;
justify-content: center;
gap: 8px;
padding: 8px;
background: rgba(156, 39, 176, 0.3);
border-radius: 8px;
margin-bottom: 10px;
font-size: 0.8rem;
}
.voice-indicator.active { display: flex; }
.voice-indicator .dot {
width: 8px;
height: 8px;
background: #9C27B0;
border-radius: 50%;
animation: pulse 0.5s infinite;
}
#fallbackHint {
display: none;
text-align: center;
font-size: 0.75rem;
color: #90CAF9;
margin-bottom: 10px;
}
#fallbackHint.visible { display: block; }
</style>
</head>
<body>
<div class="container">
<h1>PowerTools Diktat</h1>
<div id="sessionInfo" class="session-info"></div>
<div id="status" class="status ready">Bereit - sage "Hex" für Sprachsteuerung</div>
<div id="debugLog" style="font-size:0.65rem; color:#888; max-height:80px; overflow:auto; margin-bottom:8px; padding:6px; background:rgba(0,0,0,0.4); border-radius:4px;"></div>
<div id="voiceIndicator" class="voice-indicator">
<span class="dot"></span>
<span>🔮 Hex-Modus AN - sage "Hex Hex" zum Beenden</span>
</div>
<div id="modeIndicator" class="mode-indicator">
<span id="modeText">Mode: none</span>
</div>
<button type="button" id="dictateBtn">🎤 Diktat starten</button>
<div id="fallbackHint">
💡 Tipp: Tippe ins Textfeld und nutze das Mikrofon-Symbol auf deiner iOS-Tastatur
</div>
<textarea id="textArea" placeholder="Diktierter Text erscheint hier (Markdown-Format)..." spellcheck="true"></textarea>
<!-- Zeile 1: Löschen + Undo -->
<div class="btn-row">
<button type="button" id="spellBtn" class="btn-spell active" title="Rechtschreibprüfung">
<span class="icon">ABC</span>
<span class="label">Spell</span>
</button>
<button type="button" id="deleteWordsBtn" class="btn-delete" title="delete [n]">
<span class="icon">⌫3</span>
<span class="label">Words</span>
</button>
<button type="button" id="deleteSentenceBtn" class="btn-delete" title="delete sentence">
<span class="icon">⌫.</span>
<span class="label">Sentence</span>
</button>
<button type="button" id="clearBtn" class="btn-delete" title="clear">
<span class="icon">🗑️</span>
<span class="label">Clear</span>
</button>
<button type="button" id="undoBtn" class="btn-action" title="undo">
<span class="icon"></span>
<span class="label">Undo</span>
</button>
</div>
<!-- Zeile 2: Heading + Format -->
<div class="btn-row">
<button type="button" id="heading1Btn" class="btn-format" title="heading one">
<span class="icon">H1</span>
<span class="label">Head 1</span>
</button>
<button type="button" id="heading2Btn" class="btn-format" title="heading two">
<span class="icon">H2</span>
<span class="label">Head 2</span>
</button>
<button type="button" id="heading3Btn" class="btn-format" title="heading three">
<span class="icon">H3</span>
<span class="label">Head 3</span>
</button>
<button type="button" id="boldBtn" class="btn-format" title="bold">
<span class="icon">B</span>
<span class="label">Bold</span>
</button>
<button type="button" id="codeBtn" class="btn-format" title="code">
<span class="icon">`</span>
<span class="label">Code</span>
</button>
</div>
<!-- Zeile 3: Listen + Zitat -->
<div class="btn-row">
<button type="button" id="listBtn" class="btn-format" title="list item">
<span class="icon"></span>
<span class="label">List</span>
</button>
<button type="button" id="numberBtn" class="btn-format" title="numbered">
<span class="icon">1.</span>
<span class="label">Number</span>
</button>
<button type="button" id="quoteBtn" class="btn-format" title="quote">
<span class="icon">"</span>
<span class="label">Quote</span>
</button>
<button type="button" id="newlineBtn" class="btn-action" title="newline">
<span class="icon"></span>
<span class="label">Line</span>
</button>
<button type="button" id="nextBtn" class="btn-nav" title="next">
<span class="icon"></span>
<span class="label">Next</span>
</button>
</div>
<!-- Zeile 4: Tabelle -->
<div class="btn-row">
<button type="button" id="tableBtn" class="btn-format" title="table">
<span class="icon"></span>
<span class="label">Table</span>
</button>
<button type="button" id="rowBtn" class="btn-format" title="table row">
<span class="icon"></span>
<span class="label">Row</span>
</button>
<button type="button" id="modeOffBtn" class="btn-action" title="mode off">
<span class="icon"></span>
<span class="label">Mode Off</span>
</button>
<button type="button" id="voiceBtn" class="btn-voice" title="Voice Control">
<span class="icon">🎤</span>
<span class="label">Voice</span>
</button>
<button type="button" id="selectAllBtn" class="btn-select" title="select all">
<span class="icon"></span>
<span class="label">Sel All</span>
</button>
</div>
<button type="button" id="sendBtn" disabled>An PowerTools senden</button>
<div class="info">
<h4>🔮 "Hex" = AN | "Hex Hex" = AUS</h4>
<div class="info-grid">
<div class="info-section">
<h5>Steuerung</h5>
<ul>
<li>"pause" / "weiter"</li>
<li>"senden"</li>
<li>"zurück" - Undo</li>
<li>"nächste"</li>
</ul>
</div>
<div class="info-section">
<h5>Löschen</h5>
<ul>
<li>"lösche [n]" - n Wörter</li>
<li>"lösche satz"</li>
<li>"alles löschen"</li>
</ul>
</div>
<div class="info-section">
<h5>Format</h5>
<ul>
<li>"überschrift [1/2/3]"</li>
<li>"liste" / "nummer"</li>
<li>"zitat" / "fett" / "code"</li>
<li>"alles" → "fett"</li>
</ul>
</div>
<div class="info-section">
<h5>Tabellen</h5>
<ul>
<li>"tabelle" - neue Tabelle</li>
<li>"nächste" - nächste Zelle</li>
<li>"zeile" - neue Zeile</li>
<li>"fertig" - Modus beenden</li>
</ul>
</div>
</div>
</div>
</div>
<script>
// === Elemente ===
const statusEl = document.getElementById('status');
const dictateBtn = document.getElementById('dictateBtn');
const textArea = document.getElementById('textArea');
const sendBtn = document.getElementById('sendBtn');
const clearBtn = document.getElementById('clearBtn');
const spellBtn = document.getElementById('spellBtn');
const deleteWordsBtn = document.getElementById('deleteWordsBtn');
const deleteSentenceBtn = document.getElementById('deleteSentenceBtn');
const newlineBtn = document.getElementById('newlineBtn');
const undoBtn = document.getElementById('undoBtn');
const voiceBtn = document.getElementById('voiceBtn');
const voiceIndicator = document.getElementById('voiceIndicator');
const modeIndicator = document.getElementById('modeIndicator');
const modeText = document.getElementById('modeText');
const sessionInfo = document.getElementById('sessionInfo');
const nextBtn = document.getElementById('nextBtn');
const selectAllBtn = document.getElementById('selectAllBtn');
const modeOffBtn = document.getElementById('modeOffBtn');
// Format-Buttons
const heading1Btn = document.getElementById('heading1Btn');
const heading2Btn = document.getElementById('heading2Btn');
const heading3Btn = document.getElementById('heading3Btn');
const listBtn = document.getElementById('listBtn');
const numberBtn = document.getElementById('numberBtn');
const quoteBtn = document.getElementById('quoteBtn');
const codeBtn = document.getElementById('codeBtn');
const tableBtn = document.getElementById('tableBtn');
const rowBtn = document.getElementById('rowBtn');
const boldBtn = document.getElementById('boldBtn');
// === State ===
let recognition = null;
let isRecording = false;
let isPaused = false;
let voiceControlActive = false;
let undoStack = [];
let wordDeleteCount = 3;
let listCounter = 1;
// Mode-System: 'none', 'list', 'number', 'table'
let currentMode = 'none';
let tableColumnCount = 0;
let tableCurrentColumn = 0;
// Selection state
let hasSelection = false;
const urlParams = new URLSearchParams(window.location.search);
const sessionId = urlParams.get('session') || 'default';
sessionInfo.textContent = `Session: ${sessionId.substring(0, 8)}...`;
const API_BASE = '/backups/dictation';
// === Englische Zahlwörter ===
const numberWords = {
'one': 1, 'two': 2, 'three': 3, 'four': 4, 'five': 5,
'six': 6, 'seven': 7, 'eight': 8, 'nine': 9, 'ten': 10,
'wan': 1, 'to': 2, 'too': 2, 'tree': 3, 'for': 4
};
// === Debug (früh definiert) ===
const debugLog = document.getElementById('debugLog');
function logDebug(msg) {
console.log('[Dictation]', msg);
if (debugLog) {
const time = new Date().toLocaleTimeString();
debugLog.innerHTML += `${time}: ${msg}<br>`;
debugLog.scrollTop = debugLog.scrollHeight;
}
}
// === Speech Recognition ===
function setupSpeechRecognition() {
logDebug('Setting up Speech Recognition...');
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
logDebug('SpeechRecognition API: ' + (SpeechRecognition ? 'verfügbar' : 'NICHT verfügbar'));
if (!SpeechRecognition) {
statusEl.textContent = 'Speech Recognition nicht verfügbar';
statusEl.className = 'status error';
dictateBtn.textContent = 'Nicht verfügbar';
dictateBtn.disabled = true;
return false;
}
try {
recognition = new SpeechRecognition();
logDebug('Recognition Objekt erstellt');
} catch (e) {
logDebug('FEHLER: ' + e.message);
statusEl.textContent = 'Speech Recognition Fehler: ' + e.message;
statusEl.className = 'status error';
return false;
}
recognition.lang = 'de-DE';
recognition.continuous = true;
recognition.interimResults = true;
logDebug('Recognition konfiguriert (de-DE, continuous)');
recognition.onstart = () => {
logDebug('>>> Recognition STARTED');
showStatus('Aufnahme läuft... sage "Hex" für Sprachbefehle', 'recording');
};
recognition.onaudiostart = () => {
logDebug('>>> Audio capture started');
};
recognition.onspeechstart = () => {
logDebug('>>> Speech detected!');
};
recognition.onspeechend = () => {
logDebug('>>> Speech ended');
};
recognition.onresult = (event) => {
logDebug('>>> onresult event received');
let interimTranscript = '';
let finalTranscript = '';
for (let i = event.resultIndex; i < event.results.length; i++) {
const transcript = event.results[i][0].transcript;
logDebug('Transcript: "' + transcript + '" (final=' + event.results[i].isFinal + ')');
if (event.results[i].isFinal) {
const lower = transcript.trim().toLowerCase();
// "Hex Hex" deaktiviert Voice Control (muss VOR "Hex" geprüft werden!)
if (lower === 'hex hex' || lower === 'hexhex' || lower === 'hex x' ||
lower === 'hext hex' || lower === 'hex text' || lower === 'hex checks' ||
lower.includes('hex hex') || lower.includes('hexhex')) {
logDebug('🔮 Hex Hex erkannt - Voice Control AUS');
toggleVoiceControl(false);
continue;
}
// "Hex" aktiviert Voice Control (auch ohne aktive Sprachsteuerung)
if (lower === 'hex' || lower === 'hax' || lower === 'hecks' ||
lower === 'hext' || lower === 'hecks' || lower === 'ex') {
logDebug('🔮 Hex erkannt - Voice Control AN');
toggleVoiceControl(true);
continue;
}
// Prüfe auf Sprachbefehle
if (voiceControlActive && processVoiceCommand(lower)) {
continue;
}
finalTranscript += transcript + ' ';
} else {
interimTranscript += transcript;
}
}
if (finalTranscript) {
saveUndo();
// Bei Tabellen-Modus Text in aktuelle Zelle
if (currentMode === 'table') {
insertTextInTableCell(finalTranscript.trim());
} else {
textArea.value += finalTranscript;
}
ensureFocus();
}
updateSendButton();
};
recognition.onend = () => {
if (isRecording && !isPaused) {
try {
recognition.start();
} catch (e) {
console.log('Recognition restart delayed');
setTimeout(() => {
if (isRecording && !isPaused) {
try { recognition.start(); } catch (e2) {}
}
}, 100);
}
} else {
updateDictateButton();
}
};
recognition.onerror = (event) => {
console.error('[Dictation] Error:', event.error);
switch (event.error) {
case 'not-allowed':
showStatus('Mikrofon-Zugriff verweigert! Bitte erlauben.', 'error');
isRecording = false;
updateDictateButton();
break;
case 'no-speech':
// Normal, ignorieren
break;
case 'aborted':
// Normal bei Stop, ignorieren
break;
case 'network':
showStatus('Netzwerk-Fehler. Prüfe Verbindung.', 'error');
break;
case 'audio-capture':
showStatus('Kein Mikrofon gefunden!', 'error');
break;
default:
showStatus('Fehler: ' + event.error, 'error');
}
};
recognition.onnomatch = () => {
console.log('[Dictation] No match');
};
return true;
}
// === Deutsche Sprachbefehle ===
// Aktivierung: "Hex" | Deaktivierung: "Hex Hex"
function processVoiceCommand(text) {
logDebug('🔮 Befehl: "' + text + '"');
// === Steuerung ===
// Pause
if (text === 'pause' || text === 'stopp' || text === 'stop') {
pauseDictation();
return true;
}
// Weiter
if (text === 'weiter' || text === 'fortsetzen' || text.includes('weiter')) {
resumeDictation();
return true;
}
// Senden
if (text === 'senden' || text === 'sende' || text === 'abschicken' || text === 'schicken') {
sendText();
return true;
}
// Zurück (Undo)
if (text === 'zurück' || text === 'rückgängig' || text.includes('zurück')) {
undo();
showStatus('↶ Rückgängig', 'ready');
return true;
}
// === Löschen ===
// "lösche [n]" - n Wörter löschen
const loescheMatch = text.match(/lösch[e]?\s*(\d+|eins|zwei|drei|vier|fünf|sechs|sieben|acht|neun|zehn)?/i);
if (loescheMatch) {
let count = 1;
if (loescheMatch[1]) {
const germanNumbers = {
'eins': 1, 'zwei': 2, 'drei': 3, 'vier': 4, 'fünf': 5,
'sechs': 6, 'sieben': 7, 'acht': 8, 'neun': 9, 'zehn': 10
};
count = parseInt(loescheMatch[1]) || germanNumbers[loescheMatch[1].toLowerCase()] || 1;
}
// Prüfe ob "satz" dabei ist
if (text.includes('satz')) {
deleteLastSentence();
showStatus('Satz gelöscht', 'ready');
} else {
deleteLastWords(count);
showStatus(`${count} Wort/Wörter gelöscht`, 'ready');
}
return true;
}
// Alles löschen
if (text === 'alles löschen' || text === 'alles weg' || text === 'leer' ||
(text.includes('alles') && text.includes('lösch'))) {
clearAll();
return true;
}
// === Navigation ===
// Nächste (universell für Listen/Tabellen)
if (text === 'nächste' || text === 'nächstes' || text === 'nächster' ||
text === 'weiter' || text === 'next') {
handleNext();
return true;
}
// Fertig (Modus beenden)
if (text === 'fertig' || text === 'ende' || text === 'beenden' ||
text === 'modus aus' || text === 'modus beenden') {
setMode('none');
return true;
}
// Absatz / Neue Zeile
if (text === 'absatz' || text === 'neue zeile' || text === 'enter' || text === 'umbruch') {
insertNewline();
return true;
}
// === Formatierung ===
// Überschrift mit Level
const ueberschriftMatch = text.match(/überschrift\s*(\d|eins|zwei|drei)?/i);
if (ueberschriftMatch || text === 'überschrift') {
let level = 2; // Default H2
if (ueberschriftMatch && ueberschriftMatch[1]) {
const germanLevels = {'eins': 1, 'zwei': 2, 'drei': 3};
level = parseInt(ueberschriftMatch[1]) || germanLevels[ueberschriftMatch[1].toLowerCase()] || 2;
if (level > 3) level = 3;
}
insertHeading(level);
return true;
}
// Liste (Aufzählung)
if (text === 'liste' || text === 'aufzählung' || text === 'punkt' || text === 'bullet') {
if (currentMode !== 'list') {
setMode('list');
}
insertListItem();
return true;
}
// Nummerierte Liste
if (text === 'nummer' || text === 'nummeriert' || text === 'nummerierung' || text === 'zahlen') {
if (currentMode !== 'number') {
setMode('number');
}
insertNumberedItem();
return true;
}
// Zitat
if (text === 'zitat' || text === 'quote' || text === 'blockzitat') {
insertQuote();
return true;
}
// Fett
if (text === 'fett' || text === 'bold' || text === 'dick') {
if (hasSelection) {
formatSelection('bold');
} else {
insertBold();
}
return true;
}
// Code
if (text === 'code' || text === 'kode' || text === 'monospace') {
if (hasSelection) {
formatSelection('code');
} else {
insertCode();
}
return true;
}
// === Tabellen ===
// Neue Tabelle
if (text === 'tabelle' || text === 'neue tabelle' || text === 'table') {
insertTable();
return true;
}
// Neue Zeile in Tabelle
if (text === 'zeile' || text === 'neue zeile' || text === 'reihe' || text === 'row') {
insertTableRow();
return true;
}
// === Selektion ===
// Alles auswählen
if (text === 'alles' || text === 'alles markieren' || text === 'alles auswählen' ||
text === 'markiere alles') {
selectAll();
return true;
}
// Wort auswählen
if (text === 'wort' || text === 'wort markieren' || text.includes('wort') && text.includes('markier')) {
selectLastWord();
return true;
}
// Satz auswählen
if (text === 'satz markieren' || (text.includes('satz') && text.includes('markier'))) {
selectLastSentence();
return true;
}
logDebug('❓ Kein Befehl erkannt für: "' + text + '"');
return false;
}
// === Mode-System ===
function setMode(mode) {
currentMode = mode;
if (mode === 'none') {
modeIndicator.classList.remove('active');
listCounter = 1;
tableColumnCount = 0;
showStatus('Mode deactivated', 'ready');
} else {
modeIndicator.classList.add('active');
modeText.textContent = `Mode: ${mode.toUpperCase()}`;
showStatus(`${mode} mode ON - say "next" or "mode off"`, 'mode-active');
}
}
// === Next-Befehl (universell) ===
function handleNext() {
saveUndo();
switch (currentMode) {
case 'list':
insertListItem();
break;
case 'number':
insertNumberedItem();
break;
case 'table':
// Zur nächsten Zelle
tableCurrentColumn++;
if (tableCurrentColumn >= tableColumnCount) {
// Zeile beenden
textArea.value = textArea.value.trimEnd() + ' |\n| ';
tableCurrentColumn = 0;
} else {
// Nächste Spalte
textArea.value = textArea.value.trimEnd() + ' | ';
}
ensureFocus();
break;
default:
// Kein Mode aktiv - neue Zeile
insertNewline();
}
}
// === Tabellen-Funktionen ===
function insertTable() {
saveUndo();
const needsNewline = textArea.value.length > 0 && !textArea.value.endsWith('\n');
// Starte Tabellen-Modus mit leerer Header-Zeile
setMode('table');
tableColumnCount = 3; // Default
tableCurrentColumn = 0;
textArea.value += (needsNewline ? '\n\n' : '') + '| ';
ensureFocus();
showStatus('Table mode: enter headers, say "next" for next column, "row" for data rows', 'mode-active');
}
function insertTableRow() {
saveUndo();
if (currentMode !== 'table') {
setMode('table');
tableColumnCount = 3;
}
// Zähle Spalten aus der Header-Zeile wenn noch nicht bekannt
const lines = textArea.value.split('\n');
const headerLine = lines.find(l => l.startsWith('|') && !l.includes('---'));
if (headerLine) {
tableColumnCount = (headerLine.match(/\|/g) || []).length - 1;
if (tableColumnCount < 1) tableColumnCount = 3;
}
// Aktuelle Zeile beenden wenn nötig
const lastLine = lines[lines.length - 1];
if (lastLine.startsWith('|') && !lastLine.endsWith('|')) {
textArea.value = textArea.value.trimEnd() + ' |';
}
// Prüfe ob Separator-Zeile existiert
const hasSeparator = lines.some(l => l.includes('|---'));
if (!hasSeparator && lines.some(l => l.startsWith('|'))) {
// Füge Separator hinzu
const sep = '\n|' + '---|'.repeat(tableColumnCount);
textArea.value += sep;
}
// Neue Datenzeile
textArea.value += '\n| ';
tableCurrentColumn = 0;
ensureFocus();
}
function insertTextInTableCell(text) {
textArea.value += text;
ensureFocus();
}
// === Selektion ===
function selectAll() {
textArea.select();
hasSelection = true;
showStatus('All selected - say "bold", "code", or "quote"', 'voice-active');
}
function selectLastWord() {
const text = textArea.value;
const match = text.match(/\S+\s*$/);
if (match) {
const start = text.length - match[0].length;
textArea.setSelectionRange(start, text.length);
hasSelection = true;
showStatus('Word selected - say "bold", "code", or "quote"', 'voice-active');
}
textArea.focus();
}
function selectLastSentence() {
const text = textArea.value;
// Finde letzten Satzanfang
let start = Math.max(
text.lastIndexOf('. ') + 2,
text.lastIndexOf('! ') + 2,
text.lastIndexOf('? ') + 2,
text.lastIndexOf('.\n') + 2,
text.lastIndexOf('\n\n') + 2,
0
);
textArea.setSelectionRange(start, text.length);
hasSelection = true;
showStatus('Sentence selected - say "bold", "code", or "quote"', 'voice-active');
textArea.focus();
}
function formatSelection(format) {
const start = textArea.selectionStart;
const end = textArea.selectionEnd;
const selectedText = textArea.value.substring(start, end);
if (!selectedText) {
hasSelection = false;
return;
}
saveUndo();
let formatted;
switch (format) {
case 'bold':
formatted = `**${selectedText}**`;
break;
case 'code':
formatted = `\`${selectedText}\``;
break;
case 'quote':
formatted = `> ${selectedText}`;
break;
default:
formatted = selectedText;
}
textArea.value = textArea.value.substring(0, start) + formatted + textArea.value.substring(end);
hasSelection = false;
ensureFocus();
showStatus(`Formatted as ${format}`, 'ready');
}
// === Aktionen ===
function saveUndo() {
undoStack.push(textArea.value);
if (undoStack.length > 50) undoStack.shift();
}
function undo() {
if (undoStack.length > 0) {
textArea.value = undoStack.pop();
updateSendButton();
ensureFocus();
}
}
function ensureFocus() {
// Verzögertes Focus für bessere Kompatibilität
setTimeout(() => {
textArea.focus();
const len = textArea.value.length;
textArea.setSelectionRange(len, len);
}, 10);
}
function deleteLastWords(count) {
saveUndo();
const text = textArea.value.trimEnd();
const words = text.split(/\s+/);
if (words.length > 0) {
const remaining = words.slice(0, -count).join(' ');
textArea.value = remaining + (remaining ? ' ' : '');
updateSendButton();
showStatus(`Deleted ${count} words`, 'ready');
ensureFocus();
}
}
function deleteLastSentence() {
saveUndo();
const text = textArea.value.trimEnd();
const lastDot = Math.max(
text.lastIndexOf('. '),
text.lastIndexOf('! '),
text.lastIndexOf('? '),
text.lastIndexOf('.\n'),
text.lastIndexOf('!\n'),
text.lastIndexOf('?\n')
);
if (lastDot > 0) {
textArea.value = text.substring(0, lastDot + 2);
} else {
textArea.value = '';
}
updateSendButton();
showStatus('Sentence deleted', 'ready');
ensureFocus();
}
function clearAll() {
saveUndo();
textArea.value = '';
listCounter = 1;
setMode('none');
updateSendButton();
showStatus('Cleared', 'ready');
}
function insertNewline() {
saveUndo();
textArea.value += '\n';
ensureFocus();
}
// === Markdown-Formatierung ===
function insertHeading(level = 2) {
saveUndo();
const needsNewline = textArea.value.length > 0 && !textArea.value.endsWith('\n');
const hashes = '#'.repeat(level);
textArea.value += (needsNewline ? '\n\n' : '') + `${hashes} `;
ensureFocus();
showStatus(`Heading ${level} inserted`, 'ready');
}
function insertListItem() {
saveUndo();
const needsNewline = textArea.value.length > 0 && !textArea.value.endsWith('\n');
textArea.value += (needsNewline ? '\n' : '') + '- ';
ensureFocus();
}
function insertNumberedItem() {
saveUndo();
const needsNewline = textArea.value.length > 0 && !textArea.value.endsWith('\n');
textArea.value += (needsNewline ? '\n' : '') + `${listCounter}. `;
listCounter++;
ensureFocus();
}
function insertQuote() {
saveUndo();
if (hasSelection) {
formatSelection('quote');
} else {
const needsNewline = textArea.value.length > 0 && !textArea.value.endsWith('\n');
textArea.value += (needsNewline ? '\n\n' : '') + '> ';
ensureFocus();
showStatus('Quote inserted', 'ready');
}
}
function insertCode() {
saveUndo();
if (textArea.value.length > 0 && !textArea.value.endsWith(' ') && !textArea.value.endsWith('\n')) {
textArea.value += ' ';
}
textArea.value += '`';
ensureFocus();
showStatus('Code started - say "code" again to close', 'ready');
}
function insertBold() {
saveUndo();
textArea.value += '**';
ensureFocus();
showStatus('Bold started - say "bold" again to close', 'ready');
}
function toggleSpellcheck(active) {
if (active === undefined) {
active = !spellBtn.classList.contains('active');
}
textArea.spellcheck = active;
spellBtn.classList.toggle('active', active);
const val = textArea.value;
textArea.value = '';
textArea.value = val;
ensureFocus();
}
function toggleVoiceControl(active) {
if (active === undefined) {
active = !voiceControlActive;
}
voiceControlActive = active;
voiceBtn.classList.toggle('active', active);
voiceIndicator.classList.toggle('active', active);
if (active) {
showStatus('🔮 Hex-Modus AN - deutsche Befehle aktiv', 'voice-active');
} else if (currentMode !== 'none') {
showStatus(`${currentMode}-Modus aktiv`, 'mode-active');
} else if (isRecording) {
showStatus('Aufnahme läuft...', 'recording');
} else {
showStatus('Bereit - sage "Hex" für Sprachsteuerung', 'ready');
}
}
async function startDictation() {
logDebug('startDictation() aufgerufen');
if (!recognition) {
logDebug('FEHLER: recognition ist null');
return;
}
// Erst Mikrofon-Berechtigung holen
logDebug('Frage Mikrofon-Berechtigung an...');
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
stream.getTracks().forEach(track => track.stop());
logDebug('Mikrofon-Berechtigung erhalten!');
} catch (e) {
logDebug('FEHLER Mikrofon: ' + e.message);
showStatus('Mikrofon-Zugriff verweigert!', 'error');
return;
}
isRecording = true;
isPaused = false;
logDebug('Rufe recognition.start() auf...');
try {
recognition.start();
logDebug('recognition.start() erfolgreich');
} catch (e) {
logDebug('recognition.start() Fehler: ' + e.message);
}
updateDictateButton();
ensureFocus();
}
function pauseDictation() {
isPaused = true;
try {
recognition.stop();
} catch (e) {}
updateDictateButton();
showStatus('Paused - edit text or say "continue"', 'paused');
}
function resumeDictation() {
if (!isRecording) {
startDictation();
} else {
isPaused = false;
try {
recognition.start();
} catch (e) {
console.log('Recognition restart');
}
updateDictateButton();
showStatus('Recording...', 'recording');
}
}
function stopDictation() {
isRecording = false;
isPaused = false;
try {
recognition.stop();
} catch (e) {}
updateDictateButton();
showStatus('Ready', 'ready');
}
function updateDictateButton() {
if (!isRecording) {
dictateBtn.textContent = 'Diktat starten';
dictateBtn.className = '';
} else if (isPaused) {
dictateBtn.textContent = 'Fortsetzen';
dictateBtn.className = 'paused';
} else {
dictateBtn.textContent = 'Pausieren';
dictateBtn.className = 'recording';
}
}
function updateSendButton() {
sendBtn.disabled = textArea.value.trim().length === 0;
}
function showStatus(message, className) {
statusEl.textContent = message;
statusEl.className = 'status ' + className;
}
async function sendText() {
const text = textArea.value.trim();
if (!text) return;
sendBtn.disabled = true;
showStatus('Sending...', 'sent');
try {
const data = {
text: text,
timestamp: new Date().toISOString(),
session: sessionId
};
const response = await fetch(`${API_BASE}/${sessionId}.json`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (response.ok || response.status === 201) {
saveUndo();
textArea.value = '';
listCounter = 1;
setMode('none');
showStatus('Sent to PowerTools!', 'sent');
setTimeout(() => {
if (!isRecording) showStatus('Ready', 'ready');
}, 2000);
} else {
throw new Error(`Server error: ${response.status}`);
}
} catch (err) {
console.error('Send error:', err);
showStatus('Error - try again', 'error');
}
updateSendButton();
}
// === Event Listeners ===
dictateBtn.onclick = (e) => {
e.preventDefault();
if (!isRecording) startDictation();
else if (isPaused) resumeDictation();
else pauseDictation();
};
// Prevent buttons from stealing focus
function handleButtonClick(handler) {
return (e) => {
e.preventDefault();
handler();
ensureFocus();
};
}
spellBtn.onclick = handleButtonClick(() => toggleSpellcheck());
deleteWordsBtn.onclick = handleButtonClick(() => deleteLastWords(wordDeleteCount));
deleteSentenceBtn.onclick = handleButtonClick(() => deleteLastSentence());
clearBtn.onclick = handleButtonClick(() => clearAll());
newlineBtn.onclick = handleButtonClick(() => insertNewline());
undoBtn.onclick = handleButtonClick(() => undo());
voiceBtn.onclick = handleButtonClick(() => toggleVoiceControl());
sendBtn.onclick = (e) => { e.preventDefault(); sendText(); };
nextBtn.onclick = handleButtonClick(() => handleNext());
selectAllBtn.onclick = handleButtonClick(() => selectAll());
modeOffBtn.onclick = handleButtonClick(() => setMode('none'));
// Format-Buttons
heading1Btn.onclick = handleButtonClick(() => insertHeading(1));
heading2Btn.onclick = handleButtonClick(() => insertHeading(2));
heading3Btn.onclick = handleButtonClick(() => insertHeading(3));
listBtn.onclick = handleButtonClick(() => { setMode('list'); insertListItem(); });
numberBtn.onclick = handleButtonClick(() => { setMode('number'); insertNumberedItem(); });
quoteBtn.onclick = handleButtonClick(() => insertQuote());
codeBtn.onclick = handleButtonClick(() => hasSelection ? formatSelection('code') : insertCode());
tableBtn.onclick = handleButtonClick(() => insertTable());
rowBtn.onclick = handleButtonClick(() => insertTableRow());
boldBtn.onclick = handleButtonClick(() => hasSelection ? formatSelection('bold') : insertBold());
textArea.oninput = () => {
updateSendButton();
// Check for selection
hasSelection = textArea.selectionStart !== textArea.selectionEnd;
};
textArea.onselect = () => {
hasSelection = textArea.selectionStart !== textArea.selectionEnd;
};
textArea.onmouseup = () => {
hasSelection = textArea.selectionStart !== textArea.selectionEnd;
};
// Long-Press für Wörter-Anzahl
let pressTimer;
deleteWordsBtn.onmousedown = deleteWordsBtn.ontouchstart = (e) => {
pressTimer = setTimeout(() => {
const count = prompt('How many words to delete?', wordDeleteCount);
if (count && !isNaN(count)) {
wordDeleteCount = parseInt(count);
deleteWordsBtn.querySelector('.icon').textContent = `${wordDeleteCount}`;
}
}, 500);
};
deleteWordsBtn.onmouseup = deleteWordsBtn.ontouchend = () => clearTimeout(pressTimer);
// === Init ===
const fallbackHint = document.getElementById('fallbackHint');
const speechSupported = setupSpeechRecognition();
updateSendButton();
// Zeige Status und Fallback-Hint
if (!speechSupported) {
logDebug('Speech Recognition not supported - showing fallback');
fallbackHint.classList.add('visible');
dictateBtn.textContent = '⌨️ Ins Textfeld tippen';
dictateBtn.onclick = () => {
textArea.focus();
};
} else {
logDebug('Speech Recognition ready');
}
// iOS-Erkennung: Zeige Hint auch auf iOS
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
if (isIOS) {
fallbackHint.classList.add('visible');
logDebug('iOS detected - showing keyboard hint');
}
</script>
<footer style="text-align:center;padding:1rem;margin-top:2rem;border-top:1px solid #e5e7eb;font-size:0.85rem;color:#6b7280;">
<a href="#" onclick="openImpressum();return false;" style="color:#6b7280;text-decoration:none;">Impressum</a>
<span style="color:#d1d5db;margin:0 0.5rem;">|</span>
<a href="#" onclick="openDatenschutz();return false;" style="color:#6b7280;text-decoration:none;">Datenschutz</a>
</footer>
<!-- IMPRESSUM MODAL -->
<div class="legal-modal" id="impressumModal" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:9999;align-items:center;justify-content:center;">
<div style="background:white;width:90%;max-width:900px;height:90vh;border-radius:12px;margin:20px;overflow:hidden;display:flex;flex-direction:column;">
<div style="padding:0.75rem 1.5rem;border-bottom:1px solid #e5e7eb;display:flex;justify-content:space-between;align-items:center;flex-shrink:0;">
<h2 style="margin:0;font-size:1.25rem;color:#1f2937;">Impressum</h2>
<button onclick="closeImpressum()" style="background:none;border:none;font-size:1.5rem;cursor:pointer;color:#6b7280;line-height:1;">&times;</button>
</div>
<iframe src="/legal/impressum.html" style="flex:1;width:100%;border:none;"></iframe>
</div>
</div>
<!-- DATENSCHUTZ MODAL -->
<div class="legal-modal" id="datenschutzModal" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:9999;align-items:center;justify-content:center;">
<div style="background:white;width:90%;max-width:900px;height:90vh;border-radius:12px;margin:20px;overflow:hidden;display:flex;flex-direction:column;">
<div style="padding:0.75rem 1.5rem;border-bottom:1px solid #e5e7eb;display:flex;justify-content:space-between;align-items:center;flex-shrink:0;">
<h2 style="margin:0;font-size:1.25rem;color:#1f2937;">Datenschutz</h2>
<button onclick="closeDatenschutz()" style="background:none;border:none;font-size:1.5rem;cursor:pointer;color:#6b7280;line-height:1;">&times;</button>
</div>
<iframe src="/legal/datenschutz.html" style="flex:1;width:100%;border:none;"></iframe>
</div>
</div>
<script>
function openImpressum(){document.getElementById("impressumModal").style.display="flex";}
function closeImpressum(){document.getElementById("impressumModal").style.display="none";}
function openDatenschutz(){document.getElementById("datenschutzModal").style.display="flex";}
function closeDatenschutz(){document.getElementById("datenschutzModal").style.display="none";}
document.addEventListener("keydown",function(e){if(e.key==="Escape"){closeImpressum();closeDatenschutz();}});
["impressumModal","datenschutzModal"].forEach(function(id){
var el=document.getElementById(id);
if(el)el.addEventListener("click",function(e){if(e.target===this)this.style.display="none";});
});
</script>
</body>
</html>