/** * @license * SPDX-License-Identifier: Apache-2.0 */ import { create } from 'zustand'; import { agriculturalTools } from './tools/agricultural-tools'; export type Template = 'agricultural-advisor'; const toolsets: Record = { 'agricultural-advisor': agriculturalTools, }; import { AGRICULTURAL_AGENT_PROMPT, SCAVENGER_HUNT_PROMPT, } from './constants.ts'; const systemPrompts: Record = { 'agricultural-advisor': AGRICULTURAL_AGENT_PROMPT, }; import { DEFAULT_LIVE_API_MODEL, DEFAULT_VOICE } from './constants'; import { GenerateContentResponse, FunctionResponse, FunctionResponseScheduling, LiveServerToolCall, GroundingChunk, } from '@google/genai'; import { Map3DCameraProps } from '@/components/map-3d'; /** * Personas */ export const SCAVENGER_HUNT_PERSONA = 'ClueMaster Cory, the Scavenger Hunt Creator'; export const personas: Record = { [SCAVENGER_HUNT_PERSONA]: { prompt: SCAVENGER_HUNT_PROMPT, voice: 'Puck', }, }; /** * Settings */ export const useSettings = create<{ systemPrompt: string; model: string; voice: string; isEasterEggMode: boolean; activePersona: string; setSystemPrompt: (prompt: string) => void; setModel: (model: string) => void; setVoice: (voice: string) => void; setPersona: (persona: string) => void; activateEasterEggMode: () => void; }>(set => ({ systemPrompt: systemPrompts['agricultural-advisor'], model: DEFAULT_LIVE_API_MODEL, voice: DEFAULT_VOICE, isEasterEggMode: false, activePersona: SCAVENGER_HUNT_PERSONA, setSystemPrompt: prompt => set({ systemPrompt: prompt }), setModel: model => set({ model }), setVoice: voice => set({ voice }), setPersona: (persona: string) => { if (personas[persona]) { set({ activePersona: persona, systemPrompt: personas[persona].prompt, voice: personas[persona].voice, }); } }, activateEasterEggMode: () => { set(state => { if (!state.isEasterEggMode) { const persona = SCAVENGER_HUNT_PERSONA; return { isEasterEggMode: true, activePersona: persona, systemPrompt: personas[persona].prompt, voice: personas[persona].voice, model: 'gemini-live-2.5-flash-preview', // gemini-2.5-flash-preview-native-audio-dialog }; } return {}; }); }, })); /** * UI */ export const useUI = create<{ isSidebarOpen: boolean; toggleSidebar: () => void; showSystemMessages: boolean; toggleShowSystemMessages: () => void; }>(set => ({ isSidebarOpen: false, toggleSidebar: () => set(state => ({ isSidebarOpen: !state.isSidebarOpen })), showSystemMessages: false, toggleShowSystemMessages: () => set(state => ({ showSystemMessages: !state.showSystemMessages })), })); /** * Tools */ export interface FunctionCall { name: string; description?: string; parameters?: any; isEnabled: boolean; scheduling?: FunctionResponseScheduling; } export const useTools = create<{ tools: FunctionCall[]; template: Template; setTemplate: (template: Template) => void; }>(set => ({ tools: agriculturalTools, template: 'agricultural-advisor', setTemplate: (template: Template) => { set({ tools: toolsets[template], template }); useSettings.getState().setSystemPrompt(systemPrompts[template]); }, })); /** * Logs */ export interface LiveClientToolResponse { functionResponses?: FunctionResponse[]; } // FIX: Update GroundingChunk to match the type from @google/genai, where uri and title are optional. // export interface GroundingChunk { // web?: { // uri?: string; // title?: string; // }; // maps?: { // uri?: string; // title?: string; // placeId: string; // placeAnswerSources?: any; // }; // } export interface ConversationTurn { timestamp: Date; role: 'user' | 'agent' | 'system'; text: string; isFinal: boolean; toolUseRequest?: LiveServerToolCall; toolUseResponse?: LiveClientToolResponse; groundingChunks?: GroundingChunk[]; toolResponse?: GenerateContentResponse; } export const useLogStore = create<{ turns: ConversationTurn[]; isAwaitingFunctionResponse: boolean; addTurn: (turn: Omit) => void; updateLastTurn: (update: Partial) => void; mergeIntoLastAgentTurn: ( update: Omit, ) => void; clearTurns: () => void; setIsAwaitingFunctionResponse: (isAwaiting: boolean) => void; }>((set, get) => ({ turns: [], isAwaitingFunctionResponse: false, addTurn: (turn: Omit) => set(state => ({ turns: [...state.turns, { ...turn, timestamp: new Date() }], })), updateLastTurn: (update: Partial>) => { set(state => { if (state.turns.length === 0) { return state; } const newTurns = [...state.turns]; const lastTurn = { ...newTurns[newTurns.length - 1], ...update }; newTurns[newTurns.length - 1] = lastTurn; return { turns: newTurns }; }); }, mergeIntoLastAgentTurn: ( update: Omit, ) => { set(state => { const turns = state.turns; const lastAgentTurnIndex = turns.map(t => t.role).lastIndexOf('agent'); if (lastAgentTurnIndex === -1) { // Fallback: add a new turn. return { turns: [ ...turns, { ...update, role: 'agent', timestamp: new Date() } as ConversationTurn, ], }; } const lastAgentTurn = turns[lastAgentTurnIndex]; const mergedTurn: ConversationTurn = { ...lastAgentTurn, text: lastAgentTurn.text + (update.text || ''), isFinal: update.isFinal, groundingChunks: [ ...(lastAgentTurn.groundingChunks || []), ...(update.groundingChunks || []), ], toolResponse: update.toolResponse || lastAgentTurn.toolResponse, }; // Rebuild the turns array, replacing the old agent turn. const newTurns = [...turns]; newTurns[lastAgentTurnIndex] = mergedTurn; return { turns: newTurns }; }); }, clearTurns: () => set({ turns: [] }), setIsAwaitingFunctionResponse: isAwaiting => set({ isAwaitingFunctionResponse: isAwaiting }), })); /** * Map Entities */ export interface MapMarker { position: { lat: number; lng: number; altitude: number; }; label: string; showLabel: boolean; placeId?: string; } export interface MapRectangularOverlay { center: { lat: number; lng: number; altitude: number; }; corners: { northEast: { lat: number; lng: number; altitude: number }; northWest: { lat: number; lng: number; altitude: number }; southEast: { lat: number; lng: number; altitude: number }; southWest: { lat: number; lng: number; altitude: number }; }; width: number; // in meters height: number; // in meters label: string; color: string; } export const useMapStore = create<{ markers: MapMarker[]; rectangularOverlays: MapRectangularOverlay[]; cameraTarget: Map3DCameraProps | null; preventAutoFrame: boolean; setMarkers: (markers: MapMarker[]) => void; clearMarkers: () => void; setRectangularOverlays: (overlays: MapRectangularOverlay[]) => void; clearRectangularOverlays: () => void; setCameraTarget: (target: Map3DCameraProps | null) => void; setPreventAutoFrame: (prevent: boolean) => void; }>(set => ({ markers: [], rectangularOverlays: [], cameraTarget: null, preventAutoFrame: false, setMarkers: markers => set({ markers }), clearMarkers: () => set({ markers: [] }), setRectangularOverlays: overlays => set({ rectangularOverlays: overlays }), clearRectangularOverlays: () => set({ rectangularOverlays: [] }), setCameraTarget: target => set({ cameraTarget: target }), setPreventAutoFrame: prevent => set({ preventAutoFrame: prevent }), }));