ppt-tool/frontend/utils/pptx_models_utils.ts
Vadym Samoilenko cf21ba4516 Phase 1-2: Foundation + Admin Panel & Client Management
Phase 1 (Foundation):
- Project restructure (presenton-main → backend/ + frontend/)
- Database schema (8 new models, Alembic config, seed script)
- Auth (Azure AD SSO + dev bypass, JWT sessions, AuthMiddleware)
- RBAC (access_service, rbac_middleware, admin routers)
- Audit logging (fire-and-forget, AuditMiddleware, admin router)
- i18n (react-i18next with 5 namespace files)

Phase 2 (Admin Panel & Client Management):
- Admin panel shell (sidebar layout, role guard, 12 pages)
- Redux admin slice with 18 async thunks
- User management (role changes, deactivation)
- Client management (CRUD, brand config, team management)
- Brand config editor (colors, fonts, logos, voice rules)
- Master deck upload & parser (PPTX → HTML → React pipeline)
- Audit log viewer with filters and CSV/JSON export

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 15:37:17 +00:00

255 lines
7.8 KiB
TypeScript

import { ElementAttributes, SlideAttributesResult } from "@/types/element_attibutes";
import {
PptxSlideModel,
PptxTextBoxModel,
PptxAutoShapeBoxModel,
PptxPictureBoxModel,
PptxConnectorModel,
PptxPositionModel,
PptxFillModel,
PptxStrokeModel,
PptxShadowModel,
PptxFontModel,
PptxParagraphModel,
PptxPictureModel,
PptxObjectFitModel,
PptxBoxShapeEnum,
PptxObjectFitEnum,
PptxAlignment,
PptxShapeType,
PptxConnectorType
} from "@/types/pptx_models";
function convertTextAlignToPptxAlignment(textAlign?: string): PptxAlignment | undefined {
if (!textAlign) return undefined;
switch (textAlign.toLowerCase()) {
case 'left':
return PptxAlignment.LEFT;
case 'center':
return PptxAlignment.CENTER;
case 'right':
return PptxAlignment.RIGHT;
case 'justify':
return PptxAlignment.JUSTIFY;
default:
return PptxAlignment.LEFT;
}
}
function convertLineHeightToRelative(lineHeight?: number, fontSize?: number): number | undefined {
if (!lineHeight) return undefined;
let calculatedLineHeight = 1.2;
if (lineHeight < 10) {
calculatedLineHeight = lineHeight;
}
if (fontSize && fontSize > 0) {
calculatedLineHeight = Math.round((lineHeight / fontSize) * 100) / 100;
}
return calculatedLineHeight - 0.3
}
export function convertElementAttributesToPptxSlides(
slidesAttributes: SlideAttributesResult[]
): PptxSlideModel[] {
return slidesAttributes.map((slideAttributes) => {
const shapes = slideAttributes.elements.map(element => {
return convertElementToPptxShape(element);
}).filter(Boolean);
const slide: PptxSlideModel = {
shapes: shapes as (PptxTextBoxModel | PptxAutoShapeBoxModel | PptxConnectorModel | PptxPictureBoxModel)[],
note: slideAttributes.speakerNote
};
if (slideAttributes.backgroundColor) {
slide.background = {
color: slideAttributes.backgroundColor,
opacity: 1.0
};
}
return slide;
});
}
function convertElementToPptxShape(
element: ElementAttributes
): PptxTextBoxModel | PptxAutoShapeBoxModel | PptxConnectorModel | PptxPictureBoxModel | null {
if (!element.position) {
return null;
}
if (element.tagName === 'img' || (element.className && typeof element.className === 'string' && element.className.includes('image')) || element.imageSrc) {
return convertToPictureBox(element);
}
if (element.innerText && element.innerText.trim().length > 0) {
// Use AutoShape model if there's background color and border radius
if (element.background?.color && element.borderRadius && element.borderRadius.some(radius => radius > 0)) {
return convertToAutoShapeBox(element);
}
return convertToTextBox(element);
}
if (element.tagName === 'hr') {
return convertToConnector(element);
}
return convertToAutoShapeBox(element);
}
function convertToTextBox(element: ElementAttributes): PptxTextBoxModel {
const position: PptxPositionModel = {
left: Math.round(element.position?.left ?? 0),
top: Math.round(element.position?.top ?? 0),
width: Math.round(element.position?.width ?? 0),
height: Math.round(element.position?.height ?? 0)
};
const fill: PptxFillModel | undefined = element.background?.color ? {
color: element.background.color,
opacity: element.background.opacity ?? 1.0
} : undefined;
const font: PptxFontModel | undefined = element.font ? {
name: element.font.name ?? "Inter",
size: Math.round(element.font.size ?? 16),
font_weight: element.font.weight ?? 400,
italic: element.font.italic ?? false,
color: element.font.color ?? "000000"
} : undefined;
const paragraph: PptxParagraphModel = {
spacing: undefined,
alignment: convertTextAlignToPptxAlignment(element.textAlign),
font,
line_height: convertLineHeightToRelative(element.lineHeight, element.font?.size),
text: element.innerText
};
return {
shape_type: "textbox",
margin: undefined,
fill,
position,
text_wrap: element.textWrap ?? true,
paragraphs: [paragraph]
};
}
function convertToAutoShapeBox(element: ElementAttributes): PptxAutoShapeBoxModel {
const position: PptxPositionModel = {
left: Math.round(element.position?.left ?? 0),
top: Math.round(element.position?.top ?? 0),
width: Math.round(element.position?.width ?? 0),
height: Math.round(element.position?.height ?? 0)
};
const fill: PptxFillModel | undefined = element.background?.color ? {
color: element.background.color,
opacity: element.background.opacity ?? 1.0
} : undefined;
const stroke: PptxStrokeModel | undefined = element.border?.color ? {
color: element.border.color,
thickness: element.border.width ?? 1,
opacity: element.border.opacity ?? 1.0
} : undefined;
const shadow: PptxShadowModel | undefined = element.shadow?.color ? {
radius: Math.round(element.shadow.radius ?? 4),
offset: Math.round(element.shadow.offset ? Math.sqrt(element.shadow.offset[0] ** 2 + element.shadow.offset[1] ** 2) : 0),
color: element.shadow.color,
opacity: element.shadow.opacity ?? 0.5,
angle: Math.round(element.shadow.angle ?? 0)
} : undefined;
const paragraphs: PptxParagraphModel[] | undefined = element.innerText ? [{
spacing: undefined,
alignment: convertTextAlignToPptxAlignment(element.textAlign),
font: element.font ? {
name: element.font.name ?? "Inter",
size: Math.round(element.font.size ?? 16),
font_weight: element.font.weight ?? 400,
italic: element.font.italic ?? false,
color: element.font.color ?? "000000"
} : undefined,
line_height: convertLineHeightToRelative(element.lineHeight, element.font?.size),
text: element.innerText
}] : undefined;
const shapeType = element.borderRadius ? PptxShapeType.ROUNDED_RECTANGLE : PptxShapeType.RECTANGLE;
let borderRadius = undefined;
for (const eachCornerRadius of element.borderRadius ?? []) {
if (eachCornerRadius > 0) {
borderRadius = Math.max(borderRadius ?? 0, eachCornerRadius);
}
}
return {
shape_type: "autoshape",
type: shapeType,
margin: undefined,
fill,
stroke,
shadow,
position,
text_wrap: element.textWrap ?? true,
border_radius: borderRadius || undefined,
paragraphs
};
}
function convertToPictureBox(element: ElementAttributes): PptxPictureBoxModel {
const position: PptxPositionModel = {
left: Math.round(element.position?.left ?? 0),
top: Math.round(element.position?.top ?? 0),
width: Math.round(element.position?.width ?? 0),
height: Math.round(element.position?.height ?? 0)
};
const objectFit: PptxObjectFitModel = {
fit: element.objectFit ? (element.objectFit as PptxObjectFitEnum) : PptxObjectFitEnum.CONTAIN
};
const picture: PptxPictureModel = {
is_network: element.imageSrc ? element.imageSrc.startsWith('http') : false,
path: element.imageSrc || ''
};
return {
shape_type: "picture",
position,
margin: undefined,
clip: element.clip ?? true,
invert: element.filters?.invert === 1,
opacity: element.opacity,
border_radius: element.borderRadius ? element.borderRadius.map(r => Math.round(r)) : undefined,
shape: element.shape ? (element.shape as PptxBoxShapeEnum) : PptxBoxShapeEnum.RECTANGLE,
object_fit: objectFit,
picture
};
}
function convertToConnector(element: ElementAttributes): PptxConnectorModel {
const position: PptxPositionModel = {
left: Math.round(element.position?.left ?? 0),
top: Math.round(element.position?.top ?? 0),
width: Math.round(element.position?.width ?? 0),
height: Math.round(element.position?.height ?? 0)
};
return {
shape_type: "connector",
type: PptxConnectorType.STRAIGHT,
position,
thickness: element.border?.width ?? 0.5,
color: element.border?.color || element.background?.color || '000000',
opacity: element.border?.opacity ?? 1.0
};
}