1368 lines
51 KiB
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;">×</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;">×</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>
|