feat: LLM provider switcher (Claude/Groq) in settings tab
All checks were successful
Deploy / deploy (push) Successful in 1m27s
All checks were successful
Deploy / deploy (push) Successful in 1m27s
This commit is contained in:
33
app/api/settings/route.ts
Normal file
33
app/api/settings/route.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
export const dynamic = 'force-dynamic'
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { promises as fs } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
const SETTINGS_PATH = '/data/settings.json'
|
||||
const DEFAULTS = {
|
||||
voiceProvider: 'anthropic',
|
||||
anthropicModel: 'claude-haiku-4-5-20251001',
|
||||
groqModel: 'llama-3.3-70b-versatile',
|
||||
}
|
||||
|
||||
async function readSettings() {
|
||||
try {
|
||||
const raw = await fs.readFile(SETTINGS_PATH, 'utf-8')
|
||||
return { ...DEFAULTS, ...JSON.parse(raw) }
|
||||
} catch {
|
||||
return { ...DEFAULTS }
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
return NextResponse.json(await readSettings())
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const body = await req.json()
|
||||
const current = await readSettings()
|
||||
const updated = { ...current, ...body }
|
||||
await fs.mkdir(path.dirname(SETTINGS_PATH), { recursive: true })
|
||||
await fs.writeFile(SETTINGS_PATH, JSON.stringify(updated, null, 2), 'utf-8')
|
||||
return NextResponse.json(updated)
|
||||
}
|
||||
@@ -2,37 +2,44 @@ export const dynamic = 'force-dynamic'
|
||||
export const runtime = 'nodejs'
|
||||
|
||||
import { NextResponse } from 'next/server'
|
||||
import Groq from 'groq-sdk'
|
||||
import { HttpsProxyAgent } from 'https-proxy-agent'
|
||||
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 { cleanForSpeech, stripFillers, isResetCommand } from '@/lib/voice-text'
|
||||
import {
|
||||
loadHistory, saveHistory, resetHistory,
|
||||
HistoryMessage,
|
||||
} from '@/lib/voice-history'
|
||||
import { loadHistory, saveHistory, resetHistory, HistoryMessage } from '@/lib/voice-history'
|
||||
import { promises as nodeFs } from 'node:fs'
|
||||
|
||||
const MODEL = process.env.ANTHROPIC_MODEL || 'claude-haiku-4-5'
|
||||
const MAX_TOKENS = parseInt(process.env.VOICE_MAX_TOKENS || '300', 10)
|
||||
const MAX_TOOL_ROUNDS = 4
|
||||
const RATE_LIMIT_PER_MINUTE = parseInt(process.env.VOICE_RATE_LIMIT || '20', 10)
|
||||
|
||||
const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY || ''
|
||||
const ANTHROPIC_PROXY = process.env.ANTHROPIC_PROXY || ''
|
||||
const GROQ_API_KEY = process.env.GROQ_API_KEY || ''
|
||||
const GROQ_PROXY = process.env.GROQ_PROXY || ''
|
||||
|
||||
// Rate limit
|
||||
const SETTINGS_PATH = '/data/settings.json'
|
||||
const SETTINGS_DEFAULTS = { voiceProvider: 'anthropic', anthropicModel: 'claude-haiku-4-5-20251001', groqModel: 'llama-3.3-70b-versatile' }
|
||||
|
||||
async function getVoiceSettings() {
|
||||
try {
|
||||
const raw = await nodeFs.readFile(SETTINGS_PATH, 'utf-8')
|
||||
return { ...SETTINGS_DEFAULTS, ...JSON.parse(raw) }
|
||||
} catch { return { ...SETTINGS_DEFAULTS } }
|
||||
}
|
||||
|
||||
// ——— Rate limit ———
|
||||
const rateBuckets = new Map<string, { count: number; resetAt: number }>()
|
||||
function rateLimit(key: string): boolean {
|
||||
const now = Date.now()
|
||||
const b = rateBuckets.get(key)
|
||||
if (!b || b.resetAt <= now) {
|
||||
rateBuckets.set(key, { count: 1, resetAt: now + 60_000 })
|
||||
return true
|
||||
}
|
||||
if (!b || b.resetAt <= now) { rateBuckets.set(key, { count: 1, resetAt: now + 60_000 }); return true }
|
||||
if (b.count >= RATE_LIMIT_PER_MINUTE) return false
|
||||
b.count++
|
||||
return true
|
||||
b.count++; return true
|
||||
}
|
||||
let lastSweep = 0
|
||||
function sweep() {
|
||||
@@ -42,7 +49,17 @@ function sweep() {
|
||||
for (const [k, v] of rateBuckets) if (v.resetAt <= now) rateBuckets.delete(k)
|
||||
}
|
||||
|
||||
// Convert OpenAI-style tool schemas to Anthropic format
|
||||
// ——— Groq client ———
|
||||
let _groqClient: Groq | null = null
|
||||
function groqClient(): Groq {
|
||||
if (_groqClient) return _groqClient
|
||||
if (!GROQ_API_KEY) throw new Error('GROQ_API_KEY not set')
|
||||
const httpAgent = GROQ_PROXY ? new HttpsProxyAgent(GROQ_PROXY) : undefined
|
||||
_groqClient = new Groq({ apiKey: GROQ_API_KEY, httpAgent } as any)
|
||||
return _groqClient
|
||||
}
|
||||
|
||||
// ——— Anthropic helpers ———
|
||||
function toAnthropicTools(tools: any[]): any[] {
|
||||
return tools.map(t => ({
|
||||
name: t.function.name,
|
||||
@@ -51,7 +68,6 @@ function toAnthropicTools(tools: any[]): any[] {
|
||||
}))
|
||||
}
|
||||
|
||||
// Convert history to Anthropic messages format
|
||||
function historyToAnthropicMessages(history: HistoryMessage[]): any[] {
|
||||
const result: any[] = []
|
||||
for (const msg of history) {
|
||||
@@ -59,7 +75,7 @@ function historyToAnthropicMessages(history: HistoryMessage[]): any[] {
|
||||
if (msg.role === 'user') {
|
||||
result.push({ role: 'user', content: msg.content || '' })
|
||||
} else if (msg.role === 'assistant') {
|
||||
if (msg.tool_calls && msg.tool_calls.length > 0) {
|
||||
if (msg.tool_calls?.length) {
|
||||
const content: any[] = []
|
||||
if (msg.content) content.push({ type: 'text', text: msg.content })
|
||||
for (const tc of msg.tool_calls) {
|
||||
@@ -73,58 +89,29 @@ function historyToAnthropicMessages(history: HistoryMessage[]): any[] {
|
||||
}
|
||||
} else if (msg.role === 'tool') {
|
||||
const last = result[result.length - 1]
|
||||
const toolResultBlock = {
|
||||
type: 'tool_result',
|
||||
tool_use_id: msg.tool_call_id,
|
||||
content: msg.content || '',
|
||||
}
|
||||
if (last && last.role === 'user' && Array.isArray(last.content)) {
|
||||
last.content.push(toolResultBlock)
|
||||
} else {
|
||||
result.push({ role: 'user', content: [toolResultBlock] })
|
||||
}
|
||||
const block = { type: 'tool_result', tool_use_id: msg.tool_call_id, content: msg.content || '' }
|
||||
if (last?.role === 'user' && Array.isArray(last.content)) last.content.push(block)
|
||||
else result.push({ role: 'user', content: [block] })
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
async function claudeRequest(system: string, messages: any[], tools?: any[]): Promise<any> {
|
||||
const body: any = {
|
||||
model: MODEL,
|
||||
max_tokens: MAX_TOKENS,
|
||||
system,
|
||||
messages,
|
||||
}
|
||||
if (tools && tools.length > 0) {
|
||||
body.tools = tools
|
||||
}
|
||||
|
||||
const fetchOptions: any = {
|
||||
async function claudeRequest(system: string, messages: any[], tools: any[]): Promise<any> {
|
||||
const body: any = { model: 'claude-haiku-4-5-20251001', max_tokens: MAX_TOKENS, system, messages }
|
||||
if (tools.length > 0) body.tools = tools
|
||||
const opts: any = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': ANTHROPIC_API_KEY,
|
||||
'anthropic-version': '2023-06-01',
|
||||
},
|
||||
headers: { 'Content-Type': 'application/json', 'x-api-key': ANTHROPIC_API_KEY, 'anthropic-version': '2023-06-01' },
|
||||
body: JSON.stringify(body),
|
||||
}
|
||||
|
||||
if (ANTHROPIC_PROXY) {
|
||||
fetchOptions.dispatcher = new ProxyAgent(ANTHROPIC_PROXY)
|
||||
}
|
||||
|
||||
const fetchFn = ANTHROPIC_PROXY ? undiciFetch : fetch
|
||||
const res = await fetchFn('https://api.anthropic.com/v1/messages', fetchOptions)
|
||||
|
||||
if (!res.ok) {
|
||||
const err = await res.text()
|
||||
throw new Error(`anthropic_${res.status}: ${err}`)
|
||||
}
|
||||
if (ANTHROPIC_PROXY) opts.dispatcher = new ProxyAgent(ANTHROPIC_PROXY)
|
||||
const res = await (ANTHROPIC_PROXY ? undiciFetch : fetch)('https://api.anthropic.com/v1/messages', opts)
|
||||
if (!res.ok) { const err = await res.text(); throw new Error(`anthropic_${res.status}: ${err}`) }
|
||||
return res.json()
|
||||
}
|
||||
|
||||
type AgentId = 'cosmo' | 'lusya'
|
||||
|
||||
function emitVoice(event: string, agent: AgentId, text?: string) {
|
||||
voiceBus.emit('voice', { event, agent, text, timestamp: new Date().toISOString() })
|
||||
}
|
||||
@@ -136,17 +123,13 @@ export async function POST(req: Request) {
|
||||
const fwd = req.headers.get('x-forwarded-for') || ''
|
||||
const ratekey = tokenMatch?.[1] || (internal ? 'internal' : '') || fwd.split(',')[0].trim() || 'anon'
|
||||
sweep()
|
||||
if (!rateLimit(ratekey)) {
|
||||
return NextResponse.json({ error: 'rate_limited' }, { status: 429 })
|
||||
}
|
||||
if (!rateLimit(ratekey)) return NextResponse.json({ error: 'rate_limited' }, { status: 429 })
|
||||
|
||||
const body = await req.json().catch(() => null)
|
||||
if (!body || typeof body.text !== 'string' || !body.text.trim()) {
|
||||
return NextResponse.json({ error: 'text required' }, { status: 400 })
|
||||
}
|
||||
const userText: string = body.text.trim().slice(0, 4000)
|
||||
const agent: AgentId = body.agent === 'lusya' ? 'lusya' : 'cosmo'
|
||||
if (!body || typeof body.text !== 'string' || !body.text.trim()) return NextResponse.json({ error: 'text required' }, { status: 400 })
|
||||
|
||||
const userText = body.text.trim().slice(0, 4000)
|
||||
const agent: AgentId = body.agent === 'lusya' ? 'lusya' : 'cosmo'
|
||||
emitVoice('command', agent, userText)
|
||||
|
||||
if (isResetCommand(userText)) {
|
||||
@@ -156,77 +139,112 @@ export async function POST(req: Request) {
|
||||
return NextResponse.json({ text: msg, reset: true })
|
||||
}
|
||||
|
||||
const settings = await getVoiceSettings()
|
||||
const provider = settings.voiceProvider || 'anthropic'
|
||||
const history = await loadHistory(agent)
|
||||
const sysPrompt = systemPrompt(agent)
|
||||
const anthropicTools = toAnthropicTools(TOOL_SCHEMAS as any[])
|
||||
|
||||
const messages: any[] = [
|
||||
...historyToAnthropicMessages(history),
|
||||
{ role: 'user', content: userText },
|
||||
]
|
||||
|
||||
let finalText = ''
|
||||
const newTurns: HistoryMessage[] = [{ role: 'user', content: userText }]
|
||||
let finalText = ''
|
||||
|
||||
try {
|
||||
for (let round = 0; round < MAX_TOOL_ROUNDS; round++) {
|
||||
const t0 = Date.now()
|
||||
const resp = await claudeRequest(sysPrompt, messages, anthropicTools)
|
||||
|
||||
console.log(
|
||||
`[voice/chat] ${agent} round ${round + 1} ${Date.now() - t0}ms · ` +
|
||||
`stop=${resp.stop_reason} · in=${resp.usage?.input_tokens} out=${resp.usage?.output_tokens}`
|
||||
)
|
||||
|
||||
const content: any[] = resp.content || []
|
||||
const stopReason: string = resp.stop_reason || 'end_turn'
|
||||
|
||||
if (stopReason === 'tool_use') {
|
||||
const toolUseBlocks = content.filter((b: any) => b.type === 'tool_use')
|
||||
const textBlocks = content.filter((b: any) => b.type === 'text')
|
||||
const partialText = textBlocks.map((b: any) => b.text).join('')
|
||||
|
||||
messages.push({ role: 'assistant', content })
|
||||
newTurns.push({
|
||||
role: 'assistant',
|
||||
content: partialText || null,
|
||||
tool_calls: toolUseBlocks.map((b: any) => ({
|
||||
id: b.id,
|
||||
function: { name: b.name, arguments: JSON.stringify(b.input) },
|
||||
})),
|
||||
// ======== GROQ ========
|
||||
if (provider === 'groq') {
|
||||
const groqModel = settings.groqModel || 'llama-3.3-70b-versatile'
|
||||
const groqMessages: any[] = [
|
||||
{ role: 'system', content: sysPrompt },
|
||||
...history,
|
||||
{ role: 'user', content: userText },
|
||||
]
|
||||
let lastToolSig = ''
|
||||
try {
|
||||
const c = groqClient()
|
||||
for (let round = 0; round < MAX_TOOL_ROUNDS; round++) {
|
||||
const t0 = Date.now()
|
||||
const resp = await c.chat.completions.create({
|
||||
model: groqModel, max_tokens: MAX_TOKENS,
|
||||
messages: groqMessages as any,
|
||||
tools: TOOL_SCHEMAS as any,
|
||||
tool_choice: 'auto',
|
||||
})
|
||||
|
||||
const toolResults: any[] = []
|
||||
for (const tb of toolUseBlocks) {
|
||||
console.log(`[voice/chat] tool ${tb.name}(${JSON.stringify(tb.input).slice(0, 200)})`)
|
||||
const result = await executeTool(tb.name, tb.input || {}, agent)
|
||||
toolResults.push({
|
||||
type: 'tool_result',
|
||||
tool_use_id: tb.id,
|
||||
content: JSON.stringify(result),
|
||||
})
|
||||
newTurns.push({
|
||||
role: 'tool',
|
||||
content: JSON.stringify(result),
|
||||
tool_call_id: tb.id,
|
||||
})
|
||||
const choice = resp.choices[0]
|
||||
const msg = choice.message
|
||||
console.log(`[voice/chat] groq ${agent} r${round+1} ${Date.now()-t0}ms stop=${choice.finish_reason}`)
|
||||
groqMessages.push(msg)
|
||||
if (choice.finish_reason === 'tool_calls' && msg.tool_calls?.length) {
|
||||
const sig = msg.tool_calls.map((tc: any) => `${tc.function.name}:${tc.function.arguments}`).sort().join('|')
|
||||
if (sig === lastToolSig) { finalText = 'Не удалось выполнить запрос.'; break }
|
||||
lastToolSig = sig
|
||||
newTurns.push({ role: 'assistant', content: msg.content ?? null, tool_calls: msg.tool_calls })
|
||||
for (const tc of msg.tool_calls) {
|
||||
let args: any = {}; try { args = JSON.parse(tc.function.arguments) } catch {}
|
||||
const result = await executeTool(tc.function.name, args, agent)
|
||||
groqMessages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify(result) })
|
||||
newTurns.push({ role: 'tool', content: JSON.stringify(result), tool_call_id: tc.id })
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
messages.push({ role: 'user', content: toolResults })
|
||||
continue
|
||||
finalText = msg.content || ''
|
||||
newTurns.push({ role: 'assistant', content: finalText })
|
||||
break
|
||||
}
|
||||
} catch (e: any) {
|
||||
const errStr = String(e?.message || e)
|
||||
console.error('[voice/chat] groq error:', errStr)
|
||||
if (errStr.includes('tool_use_failed') || errStr.includes('Failed to call a function')) {
|
||||
try {
|
||||
const fb = await groqClient().chat.completions.create({
|
||||
model: groqModel, max_tokens: MAX_TOKENS,
|
||||
messages: [{ role: 'system', content: sysPrompt }, { role: 'user', content: userText }],
|
||||
})
|
||||
finalText = fb.choices[0]?.message?.content || 'Не удалось выполнить запрос.'
|
||||
newTurns.push({ role: 'assistant', content: finalText })
|
||||
} catch { finalText = 'Не удалось выполнить запрос.' }
|
||||
} else {
|
||||
const msg = 'Что-то сломалось.'
|
||||
emitVoice('error', agent, msg)
|
||||
return NextResponse.json({ error: 'llm_failed', detail: errStr, text: msg }, { status: 502 })
|
||||
}
|
||||
|
||||
const textBlocks = content.filter((b: any) => b.type === 'text')
|
||||
finalText = textBlocks.map((b: any) => b.text).join('')
|
||||
newTurns.push({ role: 'assistant', content: finalText })
|
||||
break
|
||||
}
|
||||
} catch (e: any) {
|
||||
const errStr = String(e?.message || e)
|
||||
console.error('[voice/chat] claude error:', errStr)
|
||||
const msg = 'Что-то сломалось.'
|
||||
emitVoice('error', agent, msg)
|
||||
return NextResponse.json({ error: 'llm_failed', detail: errStr, text: msg }, { status: 502 })
|
||||
|
||||
// ======== ANTHROPIC ========
|
||||
} else {
|
||||
const anthropicTools = toAnthropicTools(TOOL_SCHEMAS as any[])
|
||||
const messages: any[] = [...historyToAnthropicMessages(history), { role: 'user', content: userText }]
|
||||
try {
|
||||
for (let round = 0; round < MAX_TOOL_ROUNDS; round++) {
|
||||
const t0 = Date.now()
|
||||
const resp = await claudeRequest(sysPrompt, messages, anthropicTools)
|
||||
console.log(`[voice/chat] claude ${agent} r${round+1} ${Date.now()-t0}ms stop=${resp.stop_reason} in=${resp.usage?.input_tokens} out=${resp.usage?.output_tokens}`)
|
||||
const content: any[] = resp.content || []
|
||||
const stopReason = resp.stop_reason || 'end_turn'
|
||||
if (stopReason === 'tool_use') {
|
||||
const toolUseBlocks = content.filter((b: any) => b.type === 'tool_use')
|
||||
const partialText = content.filter((b: any) => b.type === 'text').map((b: any) => b.text).join('')
|
||||
messages.push({ role: 'assistant', content })
|
||||
newTurns.push({
|
||||
role: 'assistant', content: partialText || null,
|
||||
tool_calls: toolUseBlocks.map((b: any) => ({ id: b.id, function: { name: b.name, arguments: JSON.stringify(b.input) } })),
|
||||
})
|
||||
const toolResults: any[] = []
|
||||
for (const tb of toolUseBlocks) {
|
||||
console.log(`[voice/chat] tool ${tb.name}(${JSON.stringify(tb.input).slice(0,200)})`)
|
||||
const result = await executeTool(tb.name, tb.input || {}, agent)
|
||||
toolResults.push({ type: 'tool_result', tool_use_id: tb.id, content: JSON.stringify(result) })
|
||||
newTurns.push({ role: 'tool', content: JSON.stringify(result), tool_call_id: tb.id })
|
||||
}
|
||||
messages.push({ role: 'user', content: toolResults })
|
||||
continue
|
||||
}
|
||||
finalText = content.filter((b: any) => b.type === 'text').map((b: any) => b.text).join('')
|
||||
newTurns.push({ role: 'assistant', content: finalText })
|
||||
break
|
||||
}
|
||||
} catch (e: any) {
|
||||
const errStr = String(e?.message || e)
|
||||
console.error('[voice/chat] claude error:', errStr)
|
||||
const msg = 'Что-то сломалось.'
|
||||
emitVoice('error', agent, msg)
|
||||
return NextResponse.json({ error: 'llm_failed', detail: errStr, text: msg }, { status: 502 })
|
||||
}
|
||||
}
|
||||
|
||||
if (!finalText.trim()) {
|
||||
@@ -235,9 +253,7 @@ export async function POST(req: Request) {
|
||||
return NextResponse.json({ text: msg }, { status: 200 })
|
||||
}
|
||||
|
||||
const updatedHistory: HistoryMessage[] = [...history, ...newTurns]
|
||||
await saveHistory(agent, updatedHistory)
|
||||
|
||||
await saveHistory(agent, [...history, ...newTurns])
|
||||
const cleaned = cleanForSpeech(stripFillers(finalText))
|
||||
emitVoice('response', agent, cleaned)
|
||||
return NextResponse.json({ text: cleaned })
|
||||
|
||||
45
app/page.tsx
45
app/page.tsx
@@ -765,12 +765,57 @@ function SettingsTab({ city, onCityChange, onLogout, theme, onThemeChange }: { c
|
||||
textAlign: 'center', letterSpacing: '4px',
|
||||
}
|
||||
|
||||
const [voiceProvider, setVoiceProvider] = useState<'anthropic' | 'groq'>('anthropic')
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/settings').then(r => r.json()).then(s => {
|
||||
setVoiceProvider(s.voiceProvider || 'anthropic')
|
||||
}).catch(() => {})
|
||||
}, [])
|
||||
|
||||
const handleProviderChange = async (p: 'anthropic' | 'groq') => {
|
||||
setVoiceProvider(p)
|
||||
await fetch('/api/settings', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ voiceProvider: p }),
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
const currentCity = CITIES.find(c => c.id === city) || CITIES[0]
|
||||
|
||||
return (
|
||||
<div style={{ flex: 1, overflowY: 'auto', WebkitOverflowScrolling: 'touch' as any, touchAction: 'pan-y', padding: '24px', display: 'flex', flexDirection: 'column', gap: 16, maxWidth: 560, margin: '0 auto', width: '100%' }}>
|
||||
<h2 style={{ fontSize: 24, fontWeight: 800, color: 'var(--text-primary)', margin: '0 0 8px', letterSpacing: '-0.5px' }}>Настройки</h2>
|
||||
|
||||
{/* Голосовой агент */}
|
||||
<div style={{ background: 'var(--card-bg)', border: '1px solid var(--card-border)', borderRadius: 22, padding: '22px 24px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 16 }}>
|
||||
<span style={{ fontSize: 18 }}>🤖</span>
|
||||
<span style={{ fontSize: 15, fontWeight: 600, color: 'var(--text-primary)' }}>Голосовой агент</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 10 }}>
|
||||
{(['anthropic', 'groq'] as const).map(p => (
|
||||
<button
|
||||
key={p}
|
||||
onClick={() => handleProviderChange(p)}
|
||||
style={{
|
||||
flex: 1, padding: '12px 0', borderRadius: 14,
|
||||
background: voiceProvider === p ? 'rgba(99,102,241,0.15)' : 'rgba(255,255,255,0.03)',
|
||||
border: `1px solid ${voiceProvider === p ? 'rgba(99,102,241,0.4)' : 'rgba(255,255,255,0.07)'}`,
|
||||
color: voiceProvider === p ? '#a5b4fc' : 'var(--text-secondary)',
|
||||
fontSize: 13, fontWeight: 600, cursor: 'pointer', transition: 'all 0.2s ease',
|
||||
}}
|
||||
>
|
||||
{p === 'anthropic' ? '🧠 Claude Haiku' : '⚡ Groq Llama'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: 'var(--text-tertiary)', marginTop: 8, lineHeight: 1.5 }}>
|
||||
{voiceProvider === 'anthropic' ? 'Claude Haiku — точнее, лучше с tools. Платный (~$0.001/запрос).' : 'Groq Llama — быстро, бесплатно. Иногда глючит с tools.'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* City selector */}
|
||||
<div style={{ background: 'var(--card-bg)', border: '1px solid var(--card-border)', borderRadius: 22, padding: '22px 24px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 16 }}>
|
||||
|
||||
@@ -10,7 +10,7 @@ export async function middleware(request: NextRequest) {
|
||||
pathname === '/api/voice/event' ||
|
||||
pathname.startsWith('/api/voice/tools/') ||
|
||||
pathname === '/api/voice/timer'
|
||||
if (!pathname.startsWith('/api/') || pathname.startsWith('/api/auth') || pathname.startsWith('/api/spotify') || isVoiceBearer) {
|
||||
if (!pathname.startsWith('/api/') || pathname.startsWith('/api/auth') || pathname.startsWith('/api/spotify') || pathname.startsWith('/api/settings') || isVoiceBearer) {
|
||||
return NextResponse.next()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user