- outlines.py: Fix ECONNRESET/socket-hang-up — Depends session closes before StreamingResponse generator runs; capture presentation data upfront, use async_session_maker() inside inner() for the final DB commit (same pattern as Phase 4) - useCustomTemplates.ts: Filter null-template items in summary map (crashed on presentations without a TemplateModel); use item.layout_count instead of hardcoded 0 - TemplateSelection.tsx: Move custom AI templates section above built-in templates - presentationGeneration.ts + OutlinePage.tsx: Add selectedTemplateId to Redux so template selection persists when navigating away and back to /outline; clearOutlines also resets selectedTemplateId for new presentation flows - DocumentPreviewPage.tsx: Detect JSON file content (table decomposition output) and convert to markdown table or pretty-printed code block before passing to MarkdownRenderer Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
415 lines
12 KiB
TypeScript
415 lines
12 KiB
TypeScript
import { Slide } from "@/app/(presentation-generator)/types/slide";
|
|
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
|
|
|
export interface PresentationData {
|
|
id: string;
|
|
language: string;
|
|
layout: {
|
|
name: string;
|
|
ordered: boolean;
|
|
slides: any[];
|
|
};
|
|
n_slides: number;
|
|
title: string;
|
|
slides: any;
|
|
}
|
|
|
|
interface PresentationGenerationState {
|
|
presentation_id: string | null;
|
|
isLoading: boolean;
|
|
isStreaming: boolean | null;
|
|
outlines: { content: string }[];
|
|
error: string | null;
|
|
presentationData: PresentationData | null;
|
|
isSlidesRendered: boolean;
|
|
isLayoutLoading: boolean;
|
|
/** ID of the selected template (string = custom templateId, or built-in template id) */
|
|
selectedTemplateId: string | null;
|
|
}
|
|
|
|
const initialState: PresentationGenerationState = {
|
|
presentation_id: null,
|
|
outlines: [],
|
|
isSlidesRendered: false,
|
|
isLayoutLoading: false,
|
|
isLoading: false,
|
|
isStreaming: null,
|
|
error: null,
|
|
presentationData: null,
|
|
selectedTemplateId: null,
|
|
};
|
|
|
|
const presentationGenerationSlice = createSlice({
|
|
name: "presentationGeneration",
|
|
initialState,
|
|
reducers: {
|
|
setStreaming: (state, action: PayloadAction<boolean>) => {
|
|
state.isStreaming = action.payload;
|
|
},
|
|
// Loading
|
|
setLoading: (state, action: PayloadAction<boolean>) => {
|
|
state.isLoading = action.payload;
|
|
},
|
|
setLayoutLoading: (state, action: PayloadAction<boolean>) => {
|
|
state.isLayoutLoading = action.payload;
|
|
},
|
|
// Presentation ID
|
|
setPresentationId: (state, action: PayloadAction<string>) => {
|
|
state.presentation_id = action.payload;
|
|
state.error = null;
|
|
},
|
|
// Selected template
|
|
setSelectedTemplateId: (state, action: PayloadAction<string | null>) => {
|
|
state.selectedTemplateId = action.payload;
|
|
},
|
|
// Slides rendereimport { useEffect } from "react"d
|
|
setSlidesRendered: (state, action: PayloadAction<boolean>) => {
|
|
state.isSlidesRendered = action.payload;
|
|
},
|
|
// Error
|
|
setError: (state, action: PayloadAction<string>) => {
|
|
state.error = action.payload;
|
|
state.isLoading = false;
|
|
},
|
|
// Clear presentation data
|
|
clearPresentationData: (state) => {
|
|
state.presentationData = null;
|
|
},
|
|
clearOutlines: (state) => {
|
|
state.outlines = [];
|
|
state.selectedTemplateId = null;
|
|
},
|
|
// Set outlines
|
|
setOutlines: (state, action: PayloadAction<{ content: string }[]>) => {
|
|
state.outlines = action.payload;
|
|
},
|
|
// Set presentation data
|
|
setPresentationData: (state, action: PayloadAction<PresentationData>) => {
|
|
state.presentationData = action.payload;
|
|
},
|
|
deleteSlideOutline: (state, action: PayloadAction<{ index: number }>) => {
|
|
if (state.outlines) {
|
|
// Remove the slide at the given index
|
|
state.outlines = state.outlines.filter(
|
|
(_, idx) => idx !== action.payload.index
|
|
);
|
|
}
|
|
},
|
|
// SLIDE OPERATIONS
|
|
addSlide: (
|
|
state,
|
|
action: PayloadAction<{ slide: Slide; index: number }>
|
|
) => {
|
|
if (state.presentationData?.slides) {
|
|
// Insert the new slide at the specified index
|
|
state.presentationData.slides.splice(
|
|
action.payload.index,
|
|
0,
|
|
action.payload.slide
|
|
);
|
|
|
|
// Update indices for all slides to ensure they remain sequential
|
|
state.presentationData.slides = state.presentationData.slides.map(
|
|
(slide: any, idx: number) => ({
|
|
...slide,
|
|
index: idx,
|
|
})
|
|
);
|
|
}
|
|
},
|
|
deletePresentationSlide: (state, action: PayloadAction<number>) => {
|
|
if (state.presentationData) {
|
|
state.presentationData.slides.splice(action.payload, 1);
|
|
state.presentationData.slides = state.presentationData.slides.map(
|
|
(slide: any, idx: number) => ({
|
|
...slide,
|
|
index: idx,
|
|
})
|
|
);
|
|
}
|
|
},
|
|
updateSlide: (
|
|
state,
|
|
action: PayloadAction<{ index: number; slide: Slide }>
|
|
) => {
|
|
if (
|
|
state.presentationData &&
|
|
state.presentationData.slides[action.payload.index]
|
|
) {
|
|
state.presentationData.slides[action.payload.index] =
|
|
action.payload.slide;
|
|
}
|
|
},
|
|
|
|
// Update slide content at specific data path (for Tiptap text editing)
|
|
updateSlideContent: (
|
|
state,
|
|
action: PayloadAction<{
|
|
slideIndex: number;
|
|
dataPath: string;
|
|
content: string;
|
|
}>
|
|
) => {
|
|
if (
|
|
state.presentationData &&
|
|
state.presentationData.slides &&
|
|
state.presentationData.slides[action.payload.slideIndex]
|
|
) {
|
|
const slide = state.presentationData.slides[action.payload.slideIndex];
|
|
const { dataPath, content } = action.payload;
|
|
|
|
// Helper function to set nested property value
|
|
const setNestedValue = (obj: any, path: string, value: string) => {
|
|
const keys = path.split(/[.\[\]]+/).filter(Boolean);
|
|
let current = obj;
|
|
|
|
// Navigate to the parent object
|
|
for (let i = 0; i < keys.length - 1; i++) {
|
|
const key = keys[i];
|
|
if (isNaN(Number(key))) {
|
|
// String key
|
|
if (!current[key]) {
|
|
current[key] = {};
|
|
}
|
|
current = current[key];
|
|
} else {
|
|
// Array index
|
|
const index = Number(key);
|
|
if (!current[index]) {
|
|
current[index] = {};
|
|
}
|
|
current = current[index];
|
|
}
|
|
}
|
|
|
|
// Set the final value
|
|
const finalKey = keys[keys.length - 1];
|
|
if (isNaN(Number(finalKey))) {
|
|
current[finalKey] = value;
|
|
} else {
|
|
current[Number(finalKey)] = value;
|
|
}
|
|
};
|
|
|
|
// Update the slide content
|
|
if (dataPath && slide.content) {
|
|
setNestedValue(slide.content, dataPath, content);
|
|
}
|
|
}
|
|
},
|
|
|
|
addNewSlide: (state, action: PayloadAction<{ slideData: any; index: number }>) => {
|
|
if (state.presentationData?.slides) {
|
|
// Insert the new slide at the specified index + 1 (after current slide)
|
|
state.presentationData.slides.splice(action.payload.index + 1, 0, action.payload.slideData);
|
|
|
|
// Update indices for all slides to ensure they remain sequential
|
|
state.presentationData.slides = state.presentationData.slides.map(
|
|
(slide: any, idx: number) => ({
|
|
...slide,
|
|
index: idx,
|
|
})
|
|
);
|
|
}
|
|
},
|
|
|
|
// Update slide image at specific data path
|
|
updateSlideImage: (
|
|
state,
|
|
action: PayloadAction<{
|
|
slideIndex: number;
|
|
dataPath: string;
|
|
imageUrl: string;
|
|
prompt?: string;
|
|
}>
|
|
) => {
|
|
if (
|
|
state.presentationData &&
|
|
state.presentationData.slides &&
|
|
state.presentationData.slides[action.payload.slideIndex]
|
|
) {
|
|
const slide = state.presentationData.slides[action.payload.slideIndex];
|
|
const { dataPath, imageUrl, prompt } = action.payload;
|
|
|
|
// Helper function to set nested property value for images
|
|
const setNestedImageValue = (obj: any, path: string, url: string, promptText?: string) => {
|
|
const keys = path.split(/[.\[\]]+/).filter(Boolean);
|
|
let current = obj;
|
|
|
|
// Navigate to the parent object
|
|
for (let i = 0; i < keys.length - 1; i++) {
|
|
const key = keys[i];
|
|
if (isNaN(Number(key))) {
|
|
if (!current[key]) {
|
|
current[key] = {};
|
|
}
|
|
current = current[key];
|
|
} else {
|
|
const index = Number(key);
|
|
if (!current[index]) {
|
|
current[index] = {};
|
|
}
|
|
current = current[index];
|
|
}
|
|
}
|
|
|
|
// Set the image properties
|
|
const finalKey = keys[keys.length - 1];
|
|
const target = isNaN(Number(finalKey)) ? current[finalKey] : current[Number(finalKey)];
|
|
|
|
// Preserve existing properties if the target already exists
|
|
const updatedValue = {
|
|
...(target && typeof target === 'object' ? target : {}),
|
|
__image_url__: url,
|
|
__image_prompt__: promptText || (target?.__image_prompt__) || ''
|
|
};
|
|
|
|
if (isNaN(Number(finalKey))) {
|
|
current[finalKey] = updatedValue;
|
|
} else {
|
|
current[Number(finalKey)] = updatedValue;
|
|
}
|
|
|
|
};
|
|
|
|
// Update the slide image
|
|
if (dataPath && slide.content) {
|
|
setNestedImageValue(slide.content, dataPath, imageUrl, prompt);
|
|
}
|
|
|
|
// Also update the images array if it exists
|
|
if (slide.images && Array.isArray(slide.images)) {
|
|
const imageIndex = parseInt(dataPath.split('[')[1]?.split(']')[0]) || 0;
|
|
if (slide.images[imageIndex] !== undefined) {
|
|
slide.images[imageIndex] = imageUrl;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
updateImageProperties: (
|
|
state,
|
|
action: PayloadAction<{
|
|
slideIndex: number;
|
|
itemIndex: number;
|
|
properties: any;
|
|
}>
|
|
) => {
|
|
if (
|
|
state.presentationData &&
|
|
state.presentationData.slides &&
|
|
state.presentationData.slides[action.payload.slideIndex]
|
|
) {
|
|
const slide = state.presentationData.slides[action.payload.slideIndex];
|
|
const { itemIndex, properties } = action.payload;
|
|
slide['properties'] = {
|
|
...slide.properties,
|
|
[itemIndex]: properties
|
|
};
|
|
|
|
}
|
|
},
|
|
|
|
// Update slide icon at specific data path
|
|
updateSlideIcon: (
|
|
state,
|
|
action: PayloadAction<{
|
|
slideIndex: number;
|
|
dataPath: string;
|
|
iconUrl: string;
|
|
query?: string;
|
|
}>
|
|
) => {
|
|
if (
|
|
state.presentationData &&
|
|
state.presentationData.slides &&
|
|
state.presentationData.slides[action.payload.slideIndex]
|
|
) {
|
|
const slide = state.presentationData.slides[action.payload.slideIndex];
|
|
const { dataPath, iconUrl, query } = action.payload;
|
|
|
|
// Helper function to set nested property value for icons
|
|
const setNestedIconValue = (obj: any, path: string, url: string, queryText?: string) => {
|
|
const keys = path.split(/[.\[\]]+/).filter(Boolean);
|
|
let current = obj;
|
|
|
|
// Navigate to the parent object
|
|
for (let i = 0; i < keys.length - 1; i++) {
|
|
const key = keys[i];
|
|
if (isNaN(Number(key))) {
|
|
if (!current[key]) {
|
|
current[key] = {};
|
|
}
|
|
current = current[key];
|
|
} else {
|
|
const index = Number(key);
|
|
if (!current[index]) {
|
|
current[index] = {};
|
|
}
|
|
current = current[index];
|
|
}
|
|
}
|
|
|
|
// Set the icon properties
|
|
const finalKey = keys[keys.length - 1];
|
|
const target = isNaN(Number(finalKey)) ? current[finalKey] : current[Number(finalKey)];
|
|
|
|
// Preserve existing properties if the target already exists
|
|
const updatedValue = {
|
|
...(target && typeof target === 'object' ? target : {}),
|
|
__icon_url__: url,
|
|
__icon_query__: queryText || (target?.__icon_query__) || ''
|
|
};
|
|
|
|
if (isNaN(Number(finalKey))) {
|
|
current[finalKey] = updatedValue;
|
|
} else {
|
|
current[Number(finalKey)] = updatedValue;
|
|
}
|
|
|
|
// Add debugging
|
|
console.log('Redux: Updated slide icon at path:', path, 'with URL:', url);
|
|
};
|
|
|
|
// Update the slide icon
|
|
if (dataPath && slide.content) {
|
|
setNestedIconValue(slide.content, dataPath, iconUrl, query);
|
|
}
|
|
|
|
// Also update the icons array if it exists
|
|
if (slide.icons && Array.isArray(slide.icons)) {
|
|
const iconIndex = parseInt(dataPath.split('[')[1]?.split(']')[0]) || 0;
|
|
if (slide.icons[iconIndex] !== undefined) {
|
|
slide.icons[iconIndex] = iconUrl;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
},
|
|
});
|
|
|
|
export const {
|
|
setStreaming,
|
|
setLoading,
|
|
setLayoutLoading,
|
|
setPresentationId,
|
|
setSlidesRendered,
|
|
setError,
|
|
clearPresentationData,
|
|
clearOutlines,
|
|
deleteSlideOutline,
|
|
setPresentationData,
|
|
setOutlines,
|
|
setSelectedTemplateId,
|
|
// slides operations
|
|
addSlide,
|
|
updateSlide,
|
|
deletePresentationSlide,
|
|
updateSlideContent,
|
|
updateSlideImage,
|
|
updateImageProperties,
|
|
updateSlideIcon,
|
|
addNewSlide,
|
|
} = presentationGenerationSlice.actions;
|
|
|
|
export default presentationGenerationSlice.reducer;
|