Some checks failed
CI / Build Frontend (push) Has been cancelled
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
M1-01: Move Gemini API key from URL query param to x-goog-api-key header,
preventing key leakage in logs/proxy/telemetry (matches Anthropic/OpenAI pattern)
M1-03/M1-04: Replace Mutex .unwrap() with .unwrap_or_else(|e| e.into_inner())
in MemoryMiddleware and LoopGuardMiddleware — recovers from poison
instead of panicking async runtime
M2-08: Add input validation to agent_create — reject empty names,
out-of-range temperature (0-2), and zero max_tokens
M11-06: Replace Date.now() message ID with crypto.randomUUID()
to prevent collisions in classroom chat
224 lines
6.0 KiB
TypeScript
224 lines
6.0 KiB
TypeScript
/**
|
|
* 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<string>;
|
|
cancelGeneration: () => void;
|
|
loadClassroom: (id: string) => Promise<void>;
|
|
setActiveClassroom: (classroom: Classroom) => void;
|
|
openClassroom: () => void;
|
|
closeClassroom: () => void;
|
|
sendChatMessage: (message: string, sceneContext?: string) => Promise<void>;
|
|
clearError: () => void;
|
|
reset: () => void;
|
|
}
|
|
|
|
export type ClassroomStore = ClassroomState & ClassroomActions;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Store
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export const useClassroomStore = create<ClassroomStore>()((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<GenerationProgressEvent>('classroom:progress', (event) => {
|
|
const { stage, progress, activity } = event.payload;
|
|
set({
|
|
progressStage: stage,
|
|
progressPercent: progress,
|
|
progressActivity: activity,
|
|
});
|
|
});
|
|
|
|
try {
|
|
const result = await invoke<GenerationResult>('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>('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<ClassroomChatMessage[]>('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,
|
|
}),
|
|
}));
|