- 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>
201 lines
5.3 KiB
TypeScript
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;
|