ppt-tool/frontend/store/slices/wizardSlice.ts
Vadym Samoilenko e8295d6e71 Phase 4: Fix critical bugs, improve document parsing, add vision OCR
- Fix SSE stream 500: use async_session_maker inside StreamingResponse generator
  (Depends session closes when endpoint returns, before streaming starts)
- Fix template application: store template_name in prepare endpoint so worker
  uses the selected custom template instead of defaulting to "general"
- Fix OverlayLoader: replace loading.gif with HamsterLoader component
- Fix parse_mode default: change from "slides" to "layouts" to avoid 70+ layouts
- Update Gemini Flash model to gemini-3.1-flash-image-preview
- Improve DOCX parsing: python-docx for structured table extraction, OCR enabled
- Add vision-based image text extraction via Gemini for uploaded images
- Add LayoutParser integration for slide layout structure analysis
- Add Phase 4 MVP features: transfer ownership, URL input, follow-up questions,
  attachment-to-slide mapping, content router

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 14:07:00 +00:00

201 lines
5.3 KiB
TypeScript

import { createSlice, PayloadAction } from "@reduxjs/toolkit";
export interface WizardUploadedFile {
name: string;
size: number;
type: string;
/** Server path after upload */
serverPath?: string;
}
export interface WizardOutlineItem {
id: string;
title: string;
description: string;
layoutType?: string;
/** Attachment IDs mapped to this slide */
attachmentIds?: string[];
}
export type WizardStep = 1 | 2 | 3 | 4 | 5;
interface WizardState {
currentStep: WizardStep;
/** Step 1: Upload */
uploadedFiles: WizardUploadedFile[];
briefText: string;
/** Step 2: Configure */
selectedClientId: string | null;
selectedDeckId: string | null;
slideCount: number;
instructions: string;
tone: string;
language: string;
/** Step 3: Outline */
outlines: WizardOutlineItem[];
/** Step 4: Generation */
jobId: string | null;
/** Step 5: Editor */
presentationId: string | null;
/** Decomposed document data from server */
decomposedFiles: any[];
/** Map of slide index -> attached file names */
slideAttachments: Record<number, string[]>;
}
const STORAGE_KEY = "deckforge_wizard";
function loadFromStorage(): Partial<WizardState> | null {
if (typeof window === "undefined") return null;
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (raw) return JSON.parse(raw);
} catch {
// ignore
}
return null;
}
function saveToStorage(state: WizardState) {
if (typeof window === "undefined") return;
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
} catch {
// ignore
}
}
const defaultState: WizardState = {
currentStep: 1,
uploadedFiles: [],
briefText: "",
selectedClientId: null,
selectedDeckId: null,
slideCount: 12,
instructions: "",
tone: "professional",
language: "English",
outlines: [],
jobId: null,
presentationId: null,
decomposedFiles: [],
slideAttachments: {},
};
const persisted = loadFromStorage();
const initialState: WizardState = persisted
? { ...defaultState, ...persisted }
: defaultState;
const wizardSlice = createSlice({
name: "wizard",
initialState,
reducers: {
setWizardStep: (state, action: PayloadAction<WizardStep>) => {
state.currentStep = action.payload;
saveToStorage(state);
},
setUploadedFiles: (state, action: PayloadAction<WizardUploadedFile[]>) => {
state.uploadedFiles = action.payload;
saveToStorage(state);
},
setBriefText: (state, action: PayloadAction<string>) => {
state.briefText = action.payload;
saveToStorage(state);
},
setSelectedClient: (state, action: PayloadAction<string | null>) => {
state.selectedClientId = action.payload;
// Reset deck when client changes
state.selectedDeckId = null;
saveToStorage(state);
},
setSelectedDeck: (state, action: PayloadAction<string | null>) => {
state.selectedDeckId = action.payload;
saveToStorage(state);
},
setSlideCount: (state, action: PayloadAction<number>) => {
state.slideCount = action.payload;
saveToStorage(state);
},
setInstructions: (state, action: PayloadAction<string>) => {
state.instructions = action.payload;
saveToStorage(state);
},
setTone: (state, action: PayloadAction<string>) => {
state.tone = action.payload;
saveToStorage(state);
},
setLanguage: (state, action: PayloadAction<string>) => {
state.language = action.payload;
saveToStorage(state);
},
setOutlines: (state, action: PayloadAction<WizardOutlineItem[]>) => {
state.outlines = action.payload;
saveToStorage(state);
},
setJobId: (state, action: PayloadAction<string | null>) => {
state.jobId = action.payload;
saveToStorage(state);
},
setPresentationId: (state, action: PayloadAction<string | null>) => {
state.presentationId = action.payload;
saveToStorage(state);
},
setDecomposedFiles: (state, action: PayloadAction<any[]>) => {
state.decomposedFiles = action.payload;
saveToStorage(state);
},
setSlideAttachments: (
state,
action: PayloadAction<Record<number, string[]>>
) => {
state.slideAttachments = action.payload;
saveToStorage(state);
},
toggleSlideAttachment: (
state,
action: PayloadAction<{ slideIndex: number; fileName: string }>
) => {
const { slideIndex, fileName } = action.payload;
const current = state.slideAttachments[slideIndex] || [];
if (current.includes(fileName)) {
state.slideAttachments[slideIndex] = current.filter(
(f) => f !== fileName
);
if (state.slideAttachments[slideIndex].length === 0) {
delete state.slideAttachments[slideIndex];
}
} else {
state.slideAttachments[slideIndex] = [...current, fileName];
}
saveToStorage(state);
},
resetWizard: (state) => {
Object.assign(state, defaultState);
if (typeof window !== "undefined") {
localStorage.removeItem(STORAGE_KEY);
}
},
},
});
export const {
setWizardStep,
setUploadedFiles,
setBriefText,
setSelectedClient,
setSelectedDeck,
setSlideCount,
setInstructions,
setTone,
setLanguage,
setOutlines,
setJobId,
setPresentationId,
setDecomposedFiles,
setSlideAttachments,
toggleSlideAttachment,
resetWizard,
} = wizardSlice.actions;
export default wizardSlice.reducer;