Files
zclaw_openfang/desktop/src/store/classroomStore.ts
iven 619bad30cb
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
fix(security): Gemini API key header + Mutex safety + Agent validation
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
2026-04-04 19:15:50 +08:00

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,
}),
}));