voice: локальный агент (fast-path), tool control_light, смелая тема, RUNBOOK
All checks were successful
Deploy / deploy (push) Successful in 3m43s
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:
@@ -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'
|
||||
|
||||
100
app/globals.css
100
app/globals.css
@@ -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;
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
Reference in New Issue
Block a user