/** * Classroom Store * * Zustand store for classroom generation, chat messages, * and active classroom data. Uses Tauri invoke for backend calls. */ import { create } from 'zustand'; import { invoke } from '@tauri-apps/api/core'; import { listen } from '@tauri-apps/api/event'; import type { Classroom, ClassroomChatMessage, } from '../types/classroom'; import { createLogger } from '../lib/logger'; const log = createLogger('ClassroomStore'); // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- export interface GenerationRequest { topic: string; document?: string; style?: string; level?: string; targetDurationMinutes?: number; sceneCount?: number; customInstructions?: string; language?: string; } export interface GenerationResult { classroomId: string; } export interface GenerationProgressEvent { topic: string; stage: string; progress: number; activity: string; } // --------------------------------------------------------------------------- // Store interface // --------------------------------------------------------------------------- export interface ClassroomState { /** Currently generating classroom */ generating: boolean; /** Generation progress stage */ progressStage: string | null; progressPercent: number; progressActivity: string; /** Topic being generated (used for cancel) */ generatingTopic: string | null; /** The active classroom */ activeClassroom: Classroom | null; /** Whether the ClassroomPlayer overlay is open */ classroomOpen: boolean; /** Chat messages for the active classroom */ chatMessages: ClassroomChatMessage[]; /** Whether chat is loading */ chatLoading: boolean; /** Generation error message */ error: string | null; } export interface ClassroomActions { startGeneration: (request: GenerationRequest) => Promise; cancelGeneration: () => void; loadClassroom: (id: string) => Promise; setActiveClassroom: (classroom: Classroom) => void; openClassroom: () => void; closeClassroom: () => void; sendChatMessage: (message: string, sceneContext?: string) => Promise; clearError: () => void; reset: () => void; } export type ClassroomStore = ClassroomState & ClassroomActions; // --------------------------------------------------------------------------- // Store // --------------------------------------------------------------------------- export const useClassroomStore = create()((set, get) => ({ generating: false, progressStage: null, progressPercent: 0, progressActivity: '', generatingTopic: null, activeClassroom: null, classroomOpen: false, chatMessages: [], chatLoading: false, error: null, startGeneration: async (request) => { set({ generating: true, progressStage: 'agent_profiles', progressPercent: 0, progressActivity: 'Starting generation...', generatingTopic: request.topic, error: null, }); // Listen for progress events from Rust const unlisten = await listen('classroom:progress', (event) => { const { stage, progress, activity } = event.payload; set({ progressStage: stage, progressPercent: progress, progressActivity: activity, }); }); try { const result = await invoke('classroom_generate', { request }); set({ generating: false }); await get().loadClassroom(result.classroomId); set({ classroomOpen: true }); return result.classroomId; } catch (e) { const msg = e instanceof Error ? e.message : String(e); log.error('Generation failed', { error: msg }); set({ generating: false, error: msg }); throw e; } finally { unlisten(); } }, cancelGeneration: () => { const topic = get().generatingTopic; if (topic) { invoke('classroom_cancel_generation', { topic }).catch(() => {}); } set({ generating: false, generatingTopic: null }); }, loadClassroom: async (id) => { try { const classroom = await invoke('classroom_get', { classroomId: id }); set({ activeClassroom: classroom, chatMessages: [] }); } catch (e) { const msg = e instanceof Error ? e.message : String(e); log.error('Failed to load classroom', { error: msg }); set({ error: msg }); } }, setActiveClassroom: (classroom) => { set({ activeClassroom: classroom, chatMessages: [], classroomOpen: true }); }, openClassroom: () => { set({ classroomOpen: true }); }, closeClassroom: () => { set({ classroomOpen: false }); }, sendChatMessage: async (message, sceneContext) => { const classroom = get().activeClassroom; if (!classroom) { log.error('No active classroom'); return; } // Create a local user message for display const userMsg: ClassroomChatMessage = { id: `user-${crypto.randomUUID()}`, agentId: 'user', agentName: '你', agentAvatar: '👤', content: message, timestamp: Date.now(), role: 'user', color: '#3b82f6', }; set((state) => ({ chatMessages: [...state.chatMessages, userMsg], chatLoading: true, })); try { const responses = await invoke('classroom_chat', { request: { classroomId: classroom.id, userMessage: message, sceneContext: sceneContext ?? null, }, }); set((state) => ({ chatMessages: [...state.chatMessages, ...responses], chatLoading: false, })); } catch (e) { const msg = e instanceof Error ? e.message : String(e); log.error('Chat failed', { error: msg }); set({ chatLoading: false }); } }, clearError: () => set({ error: null }), reset: () => set({ generating: false, progressStage: null, progressPercent: 0, progressActivity: '', activeClassroom: null, classroomOpen: false, chatMessages: [], chatLoading: false, error: null, }), }));