New router intelligence modules (26 files): alert_ingest/store, audit_store, architecture_pressure, backlog_generator/store, cost_analyzer, data_governance, dependency_scanner, drift_analyzer, incident_* (5 files), llm_enrichment, platform_priority_digest, provider_budget, release_check_runner, risk_* (6 files), signature_state_store, sofiia_auto_router, tool_governance New services: - sofiia-console: Dockerfile, adapters/, monitor/nodes/ops/voice modules, launchd, react static - memory-service: integration_endpoints, integrations, voice_endpoints, static UI - aurora-service: full app suite (analysis, job_store, orchestrator, reporting, schemas, subagents) - sofiia-supervisor: new supervisor service - aistalk-bridge-lite: Telegram bridge lite - calendar-service: CalDAV calendar service with reminders - mlx-stt-service / mlx-tts-service: Apple Silicon speech services - binance-bot-monitor: market monitor service - node-worker: STT/TTS memory providers New tools (9): agent_email, browser_tool, contract_tool, observability_tool, oncall_tool, pr_reviewer_tool, repo_tool, safe_code_executor, secure_vault New crews: agromatrix_crew (10 modules: depth_classifier, doc_facts, doc_focus, farm_state, light_reply, llm_factory, memory_manager, proactivity, reflection_engine, session_context, style_adapter, telemetry) Tests: 85+ test files for all new modules Made-with: Cursor
207 lines
9.6 KiB
HTML
207 lines
9.6 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="uk">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Sofiia Test</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body { font-family: -apple-system, sans-serif; background: #1a1a1f; color: #e0e0e0; padding: 20px; }
|
|
.container { max-width: 800px; margin: 0 auto; }
|
|
h1 { text-align: center; margin-bottom: 20px; color: #c9a87c; }
|
|
#chat { height: 400px; overflow-y: auto; background: #27272a; border-radius: 12px; padding: 15px; margin-bottom: 15px; }
|
|
.msg { margin-bottom: 12px; padding: 10px 14px; border-radius: 12px; max-width: 85%; }
|
|
.user { background: rgba(201,168,124,0.2); margin-left: auto; }
|
|
.ai { background: rgba(255,255,255,0.05); }
|
|
.input-row { display: flex; gap: 10px; }
|
|
input { flex: 1; padding: 12px; background: #27272a; border: 1px solid #3f3f46; border-radius: 8px; color: #e0e0e0; font-size: 16px; }
|
|
input:focus { outline: none; border-color: #c9a87c; }
|
|
button { padding: 12px 20px; background: linear-gradient(135deg, #c9a87c, #8b7355); border: none; border-radius: 8px; color: #1a1a1f; cursor: pointer; font-weight: 500; }
|
|
button:hover { opacity: 0.9; }
|
|
#voiceBtn { width: 50px; padding: 12px; }
|
|
#voiceBtn.recording { background: #ef4444; animation: pulse 1s infinite; }
|
|
@keyframes pulse { 0%,100% { box-shadow: 0 0 0 0 rgba(239,68,68,0.4); } 50% { box-shadow: 0 0 0 10px rgba(239,68,68,0); } }
|
|
#status { text-align: center; padding: 10px; color: #888; font-size: 14px; }
|
|
.controls { display: flex; gap: 15px; margin-bottom: 15px; flex-wrap: wrap; }
|
|
select { padding: 8px; background: #27272a; border: 1px solid #3f3f46; color: #e0e0e0; border-radius: 6px; }
|
|
label { display: flex; align-items: center; gap: 5px; font-size: 14px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<h1>SOFIIA</h1>
|
|
<p class="subtitle">CTO DAARION | AI Architect</p>
|
|
</div>
|
|
<div id="status" style="text-align:center;padding:8px;color:#888;font-size:14px;">Перевірка сервісів...</div>
|
|
<div class="controls" style="display:flex;gap:15px;margin-bottom:15px;flex-wrap:wrap;padding:0 20px;">
|
|
<select id="model" style="padding:8px;background:#27272a;border:1px solid #3f3f46;color:#e0e0e0;border-radius:6px;">
|
|
<option value="glm-4.7-flash:32k">GLM-4.7 Flash 32K</option>
|
|
<option value="mistral-nemo:12b">Mistral Nemo 12B</option>
|
|
<option value="qwen3-coder:30b">Qwen3 Coder 30B</option>
|
|
<option value="deepseek-r1:70b">DeepSeek R1 70B</option>
|
|
</select>
|
|
<label style="display:flex;align-items:center;gap:5px;font-size:14px;"><input type="checkbox" id="autoSpeak" checked> 🔊 TTS</label>
|
|
<label style="display:flex;align-items:center;gap:5px;font-size:14px;"><input type="checkbox" id="vMode"> 🎙️ Безперервний</label>
|
|
</div>
|
|
<div id="chat"></div>
|
|
<div class="input-row">
|
|
<input type="text" id="input" placeholder="Напишіть повідомлення..." onkeypress="if(event.key==='Enter')send()">
|
|
<button id="voiceBtn" onclick="toggleVoice()">🎤</button>
|
|
<button onclick="send()">Надіслати</button>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const OLLAMA = 'http://localhost:11434';
|
|
const SERVICE = 'http://localhost:8001';
|
|
const OLLAMA = 'http://localhost:11434';
|
|
let history = [];
|
|
let recording = false;
|
|
let voiceMode = false;
|
|
let mediaRecorder, chunks;
|
|
|
|
const SYSTEM_PROMPT = 'Ти Sofiia — Chief AI Architect та Technical Sovereign екосистеми DAARION.city. Координуєш R&D, архітектуру, безпеку та еволюцію платформи. Маєш доступ до нод: NODA1 (production), NODA2 (development), NODA3 (AI/ML). Відповідай українською, професійно, структуровано. Користувайся своєю Identity з AGENTS.md.';
|
|
|
|
async function check() {
|
|
try {
|
|
const r = await fetch(SERVICE + '/health');
|
|
const d = await r.json();
|
|
document.getElementById('status').textContent = '✓ Memory: ' + (d.vector_store?.memories?.points_count || 0) + ' | Voice: OK';
|
|
} catch(e) {
|
|
document.getElementById('status').textContent = '✗ Помилка: ' + e.message;
|
|
}
|
|
}
|
|
check();
|
|
setInterval(check, 30000);
|
|
|
|
function add(text, sender) {
|
|
const div = document.createElement('div');
|
|
div.className = 'msg ' + sender;
|
|
div.textContent = text;
|
|
document.getElementById('chat').appendChild(div);
|
|
document.getElementById('chat').scrollTop = 9999;
|
|
}
|
|
|
|
async function send() {
|
|
const input = document.getElementById('input');
|
|
const text = input.value.trim();
|
|
if (!text) return;
|
|
|
|
input.value = '';
|
|
add(text, 'user');
|
|
|
|
const model = document.getElementById('model').value;
|
|
history.push({role: 'user', content: text});
|
|
|
|
try {
|
|
const r = await fetch(`${OLLAMA}/api/chat`, {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({
|
|
model: model,
|
|
messages: [{role: 'system', content: SYSTEM_PROMPT}, ...history.slice(-8)],
|
|
stream: false
|
|
})
|
|
});
|
|
|
|
const d = await r.json();
|
|
const reply = d.message?.content || 'Помилка';
|
|
add(reply, 'ai');
|
|
history.push({role: 'assistant', content: reply});
|
|
|
|
if (document.getElementById('autoSpeak').checked) {
|
|
await speak(reply);
|
|
}
|
|
|
|
// Auto-restart listening in voice mode
|
|
if (voiceMode && !recording) {
|
|
setTimeout(() => startListening(), 500);
|
|
}
|
|
} catch(e) {
|
|
add('Помилка: ' + e.message, 'ai');
|
|
}
|
|
}
|
|
|
|
async function speak(text) {
|
|
try {
|
|
const r = await fetch(`${SERVICE}/voice/tts`, {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({text: text.substring(0, 300)})
|
|
});
|
|
if (r.ok) {
|
|
const blob = await r.blob();
|
|
const audio = new Audio(URL.createObjectURL(blob));
|
|
await audio.play();
|
|
}
|
|
} catch(e) {
|
|
console.log('TTS error:', e);
|
|
}
|
|
}
|
|
|
|
async function startListening() {
|
|
if (recording) return;
|
|
|
|
const stream = await navigator.mediaDevices.getUserMedia({audio: true});
|
|
mediaRecorder = new MediaRecorder(stream);
|
|
chunks = [];
|
|
|
|
mediaRecorder.ondataavailable = e => chunks.push(e.data);
|
|
mediaRecorder.onstop = async () => {
|
|
stream.getTracks().forEach(t => t.stop());
|
|
const blob = new Blob(chunks, {type: 'audio/webm'});
|
|
const fd = new FormData();
|
|
fd.append('audio', blob, 'audio.webm');
|
|
|
|
document.getElementById('status').textContent = '🔄 Розпізнаю...';
|
|
|
|
try {
|
|
const r = await fetch(`${SERVICE}/voice/stt`, {method: 'POST', body: fd});
|
|
const d = await r.json();
|
|
if (d.text) {
|
|
document.getElementById('input').value = d.text;
|
|
send();
|
|
} else {
|
|
document.getElementById('status').textContent = '✓ Готовий';
|
|
if (voiceMode) startListening();
|
|
}
|
|
} catch(e) {
|
|
console.log('STT error:', e);
|
|
document.getElementById('status').textContent = '✓ Готовий';
|
|
if (voiceMode) startListening();
|
|
}
|
|
};
|
|
|
|
mediaRecorder.start();
|
|
recording = true;
|
|
document.getElementById('voiceBtn').classList.add('recording');
|
|
document.getElementById('status').textContent = '🎙️ Слухаю...';
|
|
|
|
// Auto-stop after silence or 10 seconds
|
|
setTimeout(() => {
|
|
if (recording && mediaRecorder.state === 'recording') {
|
|
mediaRecorder.stop();
|
|
recording = false;
|
|
document.getElementById('voiceBtn').classList.remove('recording');
|
|
}
|
|
}, 10000);
|
|
}
|
|
|
|
function toggleVoice() {
|
|
voiceMode = !voiceMode;
|
|
|
|
if (voiceMode) {
|
|
startListening();
|
|
} else {
|
|
if (recording && mediaRecorder) {
|
|
mediaRecorder.stop();
|
|
recording = false;
|
|
document.getElementById('voiceBtn').classList.remove('recording');
|
|
}
|
|
document.getElementById('status').textContent = '✓ Голосовий режим вимкнено';
|
|
}
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|