Fox: /fox/message returns audio_url

Route now waits synchronously for FoxAgent response and returns reply + audio_url.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
main
boban 2026-05-11 22:58:09 +02:00
parent e3ba405548
commit f0a1ab0911
4 changed files with 81 additions and 4 deletions

View File

@ -329,6 +329,13 @@ class FoxAgent
calculate: Rechnen, Umrechnen, Kalkulieren calculate: Rechnen, Umrechnen, Kalkulieren
remind: Erinnerung setzen remind: Erinnerung setzen
brain: Fox Brain / Gedächtnis-Visualisierung öffnen brain: Fox Brain / Gedächtnis-Visualisierung öffnen
close_map / close_poi / close_transit / close_status: Aktives Panel schließen
spotify_play / spotify_pause / spotify_next / spotify_prev: Spotify steuern
mail_open: Mail App öffnen
open_app: Mac App öffnen app=App-Name
screenshot: Screenshot machen
save_desktop: Desktop speichern
volume_up / volume_down: Lautstärke anpassen
response: Normale Antwort wenn keine andere Action passt response: Normale Antwort wenn keine andere Action passt
BEISPIELE: BEISPIELE:
@ -347,6 +354,18 @@ class FoxAgent
"was weißt du" action=brain "was weißt du" action=brain
"dein Gedächtnis" action=brain "dein Gedächtnis" action=brain
"memory" action=brain "memory" action=brain
"spiel musik" action=spotify_play
"nächster song" action=spotify_next
"vorheriger song" action=spotify_prev
"musik pause" action=spotify_pause
"öffne mail" action=mail_open
"öffne finder" action=open_app, app="Finder"
"öffne safari" action=open_app, app="Safari"
"mach einen screenshot" action=screenshot
"lauter" action=volume_up
"leiser" action=volume_down
"schließe die karte" action=close_map
"schließe die restaurants" action=close_poi
BEI action=code: BEI action=code:
- code: VOLLSTÄNDIGER ausführbarer Code, nicht nur Erklärung - code: VOLLSTÄNDIGER ausführbarer Code, nicht nur Erklärung
@ -389,6 +408,9 @@ class FoxAgent
'map', 'poi', 'weather', 'transit', 'route', 'recommend', 'response', 'map', 'poi', 'weather', 'transit', 'route', 'recommend', 'response',
'close_map', 'close_poi', 'close_transit', 'close_status', 'close_map', 'close_poi', 'close_transit', 'close_status',
'web_search', 'open_url', 'code', 'calculate', 'remind', 'brain', 'web_search', 'open_url', 'code', 'calculate', 'remind', 'brain',
'spotify_play', 'spotify_pause', 'spotify_next', 'spotify_prev',
'mail_open', 'open_app', 'screenshot', 'save_desktop',
'volume_up', 'volume_down',
]), ]),
new StringSchema('city', 'Kleingeschriebener Stadtname (leer wenn nicht relevant)'), new StringSchema('city', 'Kleingeschriebener Stadtname (leer wenn nicht relevant)'),
new StringSchema('category', 'POI-Kategorie bei action=poi: restaurant, parking, pharmacy, hotel, doctor, bank, supermarket'), new StringSchema('category', 'POI-Kategorie bei action=poi: restaurant, parking, pharmacy, hotel, doctor, bank, supermarket'),
@ -401,8 +423,9 @@ class FoxAgent
new StringSchema('language', 'Programmiersprache bei action=code (php, js, python, etc.)'), new StringSchema('language', 'Programmiersprache bei action=code (php, js, python, etc.)'),
new StringSchema('result', 'Berechnetes Ergebnis bei action=calculate'), new StringSchema('result', 'Berechnetes Ergebnis bei action=calculate'),
new StringSchema('description', 'Beschreibung was der Code macht, bei action=code'), new StringSchema('description', 'Beschreibung was der Code macht, bei action=code'),
new StringSchema('app', 'App Name bei action=open_app'),
], ],
requiredFields: ['action', 'text', 'city', 'category', 'place', 'to', 'url', 'query', 'code', 'language', 'result', 'description'], requiredFields: ['action', 'text', 'city', 'category', 'place', 'to', 'url', 'query', 'code', 'language', 'result', 'description', 'app'],
); );
// Provider-Chain: Ollama (Mac, qwen2.5:32b) → OpenAI (gpt-4o-mini) → Heuristik. // Provider-Chain: Ollama (Mac, qwen2.5:32b) → OpenAI (gpt-4o-mini) → Heuristik.
@ -657,6 +680,19 @@ class FoxAgent
])); ]));
} }
// Desktop-Actions (Spotify, Mail, App, System) — für Electron Mac Agent
if (in_array($action, [
'spotify_play', 'spotify_pause', 'spotify_next', 'spotify_prev',
'mail_open', 'open_app', 'screenshot', 'save_desktop',
'volume_up', 'volume_down',
], true)) {
broadcast(new FoxAction([
'action' => $action,
'text' => $reply['text'] ?? '',
'app' => $reply['app'] ?? null,
]));
}
$message = Message::create([ $message = Message::create([
'role' => Message::ROLE_ASSISTANT, 'role' => Message::ROLE_ASSISTANT,
'content' => $reply['text'] ?: '...', 'content' => $reply['text'] ?: '...',

View File

@ -500,6 +500,24 @@ document.addEventListener('alpine:init', () => {
return; return;
} }
// Spotify
if (payload.action === 'spotify_play') { window.foxBridge?.macCommand('spotify_play'); if (payload.text) this.notify(payload.text); return; }
if (payload.action === 'spotify_pause') { window.foxBridge?.macCommand('spotify_pause'); if (payload.text) this.notify(payload.text); return; }
if (payload.action === 'spotify_next') { window.foxBridge?.macCommand('spotify_next'); if (payload.text) this.notify(payload.text); return; }
if (payload.action === 'spotify_prev') { window.foxBridge?.macCommand('spotify_prev'); if (payload.text) this.notify(payload.text); return; }
// Desktop Actions
if (payload.action === 'mail_open') { window.foxBridge?.macCommand('mail_open'); if (payload.text) this.notify(payload.text); return; }
if (payload.action === 'volume_up') { window.foxBridge?.macCommand('volume_up'); if (payload.text) this.notify(payload.text); return; }
if (payload.action === 'volume_down') { window.foxBridge?.macCommand('volume_down'); if (payload.text) this.notify(payload.text); return; }
if (payload.action === 'screenshot') { window.foxBridge?.macCommand('screenshot'); if (payload.text) this.notify(payload.text); return; }
if (payload.action === 'save_desktop') { window.foxBridge?.macCommand('save_desktop'); if (payload.text) this.notify(payload.text); return; }
if (payload.action === 'open_app' && payload.app) {
window.foxBridge?.macCommand('app:' + payload.app);
if (payload.text) this.notify(payload.text);
return;
}
if (['map','food','weather','transit','poi'].includes(payload.action)) { if (['map','food','weather','transit','poi'].includes(payload.action)) {
let city = payload.data; let city = payload.data;
if (!city && payload.city) { if (!city && payload.city) {
@ -562,6 +580,8 @@ document.addEventListener('alpine:init', () => {
ml.style.visibility = 'visible'; ml.style.visibility = 'visible';
ml.classList.add('show'); ml.classList.add('show');
} }
document.getElementById('fox-center')?.classList.add('hide');
document.getElementById('fox-mini')?.classList.add('show');
// 200ms warten damit der Browser die Layout-Änderungen committed hat, // 200ms warten damit der Browser die Layout-Änderungen committed hat,
// dann erst resize() (sonst rechnet MapLibre auf alter Container-Größe). // dann erst resize() (sonst rechnet MapLibre auf alter Container-Größe).

View File

@ -2,7 +2,8 @@
<div class="grid-layer"></div> <div class="grid-layer"></div>
<div class="scanlines"></div> <div class="scanlines"></div>
<div id="map-layer" class="map-wrap" wire:ignore><div id="map"></div></div> <div id="map-layer" class="map-wrap" wire:ignore
x-bind:style="mapVisible ? 'opacity:1;pointer-events:all;display:block' : 'opacity:0;pointer-events:none'"><div id="map" wire:ignore></div></div>
{{-- Fox Brain Layer --}} {{-- Fox Brain Layer --}}
<div id="brain-hover-label" class="brain-hover-label"></div> <div id="brain-hover-label" class="brain-hover-label"></div>
@ -61,7 +62,7 @@
<div class="eg eg-r" :class="(edgeGlow || isSpeaking) && 'on'"></div> <div class="eg eg-r" :class="(edgeGlow || isSpeaking) && 'on'"></div>
<div id="fox-center" class="fox-center" wire:ignore.self <div id="fox-center" class="fox-center" wire:ignore.self
:class="{'is-thinking': $wire.status === 'thinking'}" :class="{'is-thinking': $wire.status === 'thinking', 'hide': mapVisible}"
x-on:click="mapVisible && closeMap()"> x-on:click="mapVisible && closeMap()">
<div class="fox-jarvis" <div class="fox-jarvis"
:class="{ 'is-speaking': isSpeaking }"> :class="{ 'is-speaking': isSpeaking }">
@ -131,7 +132,8 @@
</div> </div>
</div> </div>
<button type="button" class="fox-mini" x-on:click="closeMap()" wire:ignore.self> <button type="button" class="fox-mini" x-on:click="closeMap()" wire:ignore.self
:class="{ 'show': mapVisible }">
<div class="mini-rings"> <div class="mini-rings">
<div class="mini-ring mr1"></div> <div class="mini-ring mr1"></div>
<div class="mini-ring mr2"></div> <div class="mini-ring mr2"></div>

View File

@ -35,3 +35,22 @@ Route::prefix('fox')->name('fox.')->group(function () {
Route::get('/brain', [FoxBrainController::class, 'index'])->name('brain'); Route::get('/brain', [FoxBrainController::class, 'index'])->name('brain');
Route::get('/brain/graph', [FoxBrainController::class, 'graph'])->name('brain.graph'); Route::get('/brain/graph', [FoxBrainController::class, 'graph'])->name('brain.graph');
}); });
Route::post('/fox/message', function (\Illuminate\Http\Request $request) {
$text = $request->input('text', '');
if (empty(trim($text))) {
return response()->json(['error' => 'Kein Text'], 400);
}
$userMessage = app(\App\Services\FoxAgent::class)
->recordUserMessage($text, ['source' => 'electron']);
$assistantMessage = app(\App\Services\FoxAgent::class)
->processUserMessage($userMessage);
return response()->json([
'status' => 'ok',
'reply' => $assistantMessage->content,
'audio_url' => $assistantMessage->metadata['audio_url'] ?? null,
]);
})->middleware('web');