Files
Apple 129e4ea1fc feat(platform): add new services, tools, tests and crews modules
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
2026-03-03 07:14:14 -08:00

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>