voice: локальный агент (fast-path), tool control_light, смелая тема, RUNBOOK
All checks were successful
Deploy / deploy (push) Successful in 3m43s

- lib/agent-local.ts + route.ts: команды-действия исполняет локальная intent-служба
  (под AGENT_URL, с fallback на LLM); инфо-запросы остаются у LLM
- lib/tools/lights.ts: голосовое управление светом через HA (control_light)
- app/globals.css + page.tsx: космическая indigo/aurora тема (токены, kiosk-safe)
- agent/: sidecar-служба (Dockerfile, predict.py), веса монтируются томом
- RUNBOOK.md: вывод в продакшен на мини-ПК
This commit is contained in:
d.klimov
2026-07-22 21:48:24 +03:00
parent d30ed1bac1
commit e6dab09c38
13 changed files with 975 additions and 32 deletions

View File

@@ -9,6 +9,7 @@ import { fetch as undiciFetch, ProxyAgent } from 'undici'
import { voiceBus } from '@/lib/voice-bus'
import { systemPrompt } from '@/lib/voice-prompts'
import { TOOL_SCHEMAS, executeTool } from '@/lib/tools/_registry'
import { tryLocalAgent } from '@/lib/agent-local'
import { cleanForSpeech, stripFillers, isResetCommand } from '@/lib/voice-text'
import { loadHistory, saveHistory, resetHistory, HistoryMessage } from '@/lib/voice-history'
import { promises as nodeFs } from 'node:fs'
@@ -146,6 +147,23 @@ export async function POST(req: Request) {
const newTurns: HistoryMessage[] = [{ role: 'user', content: userText }]
let finalText = ''
// ======== Локальный агент (fast-path) ========
// Если задан AGENT_URL — команды-действия исполняет локальная intent-служба
// за ~2 мс, минуя облачный LLM. Инфо-запросы и всё неуверенное падают в LLM
// ниже. Любая ошибка → тоже fallback. Без AGENT_URL — поведение не меняется.
try {
const fast = await tryLocalAgent(userText, agent)
if (fast) {
const assistant: HistoryMessage = { role: 'assistant', content: fast.text }
await saveHistory(agent, [...history, ...newTurns, assistant])
emitVoice('response', agent, fast.text)
console.log(`[voice/chat] local-agent ${agent} intent=${fast.intent}`)
return NextResponse.json({ text: fast.text, via: 'local-agent' })
}
} catch (e) {
console.log('[voice/chat] local-agent skip:', (e as Error).message)
}
// ======== GROQ ========
if (provider === 'groq') {
const groqModel = settings.groqModel || 'llama-3.3-70b-versatile'

View File

@@ -8,29 +8,29 @@
Tokens — dark (default)
—————————————————————————————— */
:root {
/* Surfaces */
--bg: #0b0b14;
--bg-secondary: #101020;
--surface-1: #15152a;
--surface-2: #1c1c36;
--surface-3: #242444;
--surface-hover: #1e1e3c;
/* Surfaces — глубокий индиго с холодным подтоном (тема «Космо») */
--bg: #07070f;
--bg-secondary: #0c0c1c;
--surface-1: #14142c;
--surface-2: #1c1c3e;
--surface-3: #26264f;
--surface-hover: #22224a;
/* Borders */
--border-subtle: rgba(255, 255, 255, 0.06);
--border-strong: rgba(255, 255, 255, 0.12);
--border-subtle: rgba(255, 255, 255, 0.07);
--border-strong: rgba(150, 160, 255, 0.18);
--hairline: rgba(255, 255, 255, 0.08);
/* Text */
--text-primary: rgba(255, 255, 255, 0.95);
--text-secondary: rgba(255, 255, 255, 0.6);
--text-tertiary: rgba(255, 255, 255, 0.38);
--text-primary: rgba(255, 255, 255, 0.96);
--text-secondary: rgba(200, 205, 240, 0.62);
--text-tertiary: rgba(190, 195, 235, 0.4);
/* Accents — UI */
--accent: #818cf8;
--accent-strong: #6366f1;
--accent-secondary: #22d3ee;
--accent-glow: rgba(129, 140, 248, 0.22);
/* Accents — UI (ярче и насыщеннее для смелого визуала) */
--accent: #8b93ff;
--accent-strong: #6d5efc;
--accent-secondary: #2ee6f6;
--accent-glow: rgba(139, 120, 252, 0.32);
/* Data palette — semantic, theme-aware */
--data-cool: #38bdf8; /* небо, влажность, охлаждение */
@@ -52,11 +52,12 @@
--data-rose-bg: color-mix(in srgb, var(--data-rose) 12%, var(--surface-2));
--data-violet-bg: color-mix(in srgb, var(--data-violet) 12%, var(--surface-2));
/* Brand gradients */
--gradient-primary: linear-gradient(135deg, #6366f1, #8b5cf6);
--gradient-warm: linear-gradient(135deg, #f59e0b, #ef4444);
--gradient-cool: linear-gradient(135deg, #06b6d4, #3b82f6);
--gradient-green: linear-gradient(135deg, #10b981, #34d399);
/* Brand gradients — многоступенчатые, живые (смелый визуал) */
--gradient-primary: linear-gradient(135deg, #6d5efc 0%, #a855f7 50%, #2ee6f6 100%);
--gradient-warm: linear-gradient(135deg, #fbbf24 0%, #fb7185 100%);
--gradient-cool: linear-gradient(135deg, #22d3ee 0%, #6366f1 100%);
--gradient-green: linear-gradient(135deg, #10b981 0%, #5eead4 100%);
--gradient-aurora: linear-gradient(120deg, #6d5efc 0%, #a855f7 40%, #2ee6f6 100%);
/* Shadows */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4);
@@ -219,19 +220,32 @@ html, body {
filter: blur(80px);
}
.bg-ambient::before {
width: 560px; height: 560px;
background: radial-gradient(circle, var(--accent-glow) 0%, transparent 70%);
top: -180px; right: -80px;
width: 680px; height: 680px;
background: radial-gradient(circle, var(--accent-glow) 0%, transparent 68%);
top: -200px; right: -100px;
animation: float1 22s ease-in-out infinite;
}
.bg-ambient::after {
width: 480px; height: 480px;
background: radial-gradient(circle, rgba(139, 92, 246, 0.14) 0%, transparent 70%);
bottom: -140px; left: -60px;
width: 600px; height: 600px;
background: radial-gradient(circle, rgba(46, 230, 246, 0.16) 0%, transparent 68%);
bottom: -180px; left: -80px;
animation: float2 27s ease-in-out infinite;
}
.light .bg-ambient::after {
background: radial-gradient(circle, rgba(99, 102, 241, 0.1) 0%, transparent 70%);
background: radial-gradient(circle, rgba(8, 145, 178, 0.12) 0%, transparent 70%);
}
/* Третий, центральный ореол — глубина «космоса» */
.bg-ambient .aurora-3 {
position: absolute;
width: 520px; height: 520px;
border-radius: 50%;
filter: blur(90px);
background: radial-gradient(circle, rgba(168, 85, 247, 0.14) 0%, transparent 70%);
top: 30%; left: 40%;
animation: float1 31s ease-in-out infinite reverse;
}
.light .bg-ambient .aurora-3 {
background: radial-gradient(circle, rgba(124, 58, 237, 0.08) 0%, transparent 70%);
}
@keyframes float1 {
@@ -280,7 +294,16 @@ html, body {
content: '';
position: absolute;
inset: 0;
background: radial-gradient(circle at top right, var(--accent-glow), transparent 60%);
background: radial-gradient(circle at top right, var(--accent-glow), transparent 58%);
pointer-events: none;
}
.focus-card-accent::after {
content: '';
position: absolute;
inset: 0;
height: 3px;
background: var(--gradient-aurora);
opacity: 0.9;
pointer-events: none;
}
@@ -292,6 +315,23 @@ html, body {
}
.glass-card:hover { background: var(--surface-hover); }
/* ——— Смелые опциональные утилиты (opt-in, раскладку не меняют) ——— */
/* Светящаяся рамка-акцент для избранных карточек */
.card-glow {
box-shadow: var(--shadow-md),
0 0 0 1px color-mix(in srgb, var(--accent) 35%, transparent),
0 0 28px -6px var(--accent-glow);
}
/* Верхний блик — тонкая градиентная кромка сверху */
.sheen-top { box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06); }
/* Крупные числа в цвете aurora-градиента */
.num-aurora {
background: var(--gradient-aurora);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
/* Hairline divider helper */
.divider {
height: 1px;

View File

@@ -239,7 +239,7 @@ function LockScreen({ onUnlock }: { onUnlock: () => void }) {
position: 'fixed', inset: 0, zIndex: 200, background: '#0c0c18',
display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 40,
}}>
<div className="bg-ambient" />
<div className="bg-ambient"><div className="aurora-3" /></div>
<div style={{ textAlign: 'center', position: 'relative', zIndex: 1 }}>
<div style={{ fontSize: 64, fontWeight: 800, color: 'var(--text-primary)', letterSpacing: '-3px', fontVariantNumeric: 'tabular-nums' }}>
{time.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' })}
@@ -1126,7 +1126,7 @@ function HomePageInner() {
return (
<div className={getWeatherBgClass(weather?.desc || null)} style={{ display: 'flex', height: '100dvh', width: '100%', background: 'var(--bg)', overflow: 'hidden', position: 'relative' }}>
<div className="bg-ambient" />
<div className="bg-ambient"><div className="aurora-3" /></div>
<AnimatePresence>
{screensaverActive && (