Skip to content

Commit 7b226a1

Browse files
committed
feat: move preset controls, apply theme on click, and refine fresh UI layout
1 parent 9278659 commit 7b226a1

5 files changed

Lines changed: 101 additions & 21 deletions

File tree

backend/app/generator_service.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,15 @@ class GeneratorService:
5252
def __init__(self, ppt_service: PPTService):
5353
self.ppt_service = ppt_service
5454

55+
def apply_theme_preset(self, path: str, preset: str, output_path: str | None = None) -> tuple[str, ThemeConfig]:
56+
prs = Presentation(path)
57+
req = GenerateRequest(topic="theme-apply", preset=preset)
58+
theme = self._select_theme(req, [])
59+
self.ppt_service.apply_theme_to_presentation(prs, theme)
60+
save_path = output_path or self.ppt_service._default_output(path, f"preset-{theme.name}")
61+
prs.save(save_path)
62+
return save_path, theme
63+
5564
def generate(self, req: GenerateRequest) -> tuple[str, List[OutlineSlide], ThemeConfig]:
5665
if os.getenv("FAKE_LLM_RESPONSES", "0") == "1":
5766
outline = self._fake_llm_outline(req)

backend/app/main.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
GenerateRequest,
1414
GenerateResponse,
1515
ThemeApplyRequest,
16+
ThemePresetApplyRequest,
1617
UpdateRequest,
1718
)
1819
from .ppt_service import PPTService
@@ -130,3 +131,20 @@ def apply_theme(req: ThemeApplyRequest):
130131
}
131132
except Exception as e:
132133
raise HTTPException(status_code=400, detail=str(e))
134+
135+
136+
@app.post("/api/theme/apply-preset")
137+
def apply_theme_preset(req: ThemePresetApplyRequest):
138+
try:
139+
output, theme = generator_service.apply_theme_preset(req.path, req.preset, req.output_path)
140+
return {
141+
"output_path": output,
142+
"theme": {
143+
"name": theme.name,
144+
"font_name": theme.font_name,
145+
"title_size_pt": theme.title_size_pt,
146+
"body_size_pt": theme.body_size_pt,
147+
},
148+
}
149+
except Exception as e:
150+
raise HTTPException(status_code=400, detail=str(e))

backend/app/models.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ class ThemeApplyRequest(BaseModel):
4444
output_path: Optional[str] = None
4545

4646

47+
class ThemePresetApplyRequest(BaseModel):
48+
path: str
49+
preset: str
50+
output_path: Optional[str] = None
51+
52+
4753
class ChatResponse(BaseModel):
4854
plan: List[EditInstruction]
4955
output_path: str

frontend/src/App.jsx

Lines changed: 41 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,25 @@ export default function App() {
136136
}
137137
}
138138

139+
const applyPresetImmediately = async (presetName) => {
140+
setForm((f) => ({ ...f, preset: presetName }))
141+
if (!pptPath) return
142+
143+
setLoading(true)
144+
try {
145+
const data = await api('/api/theme/apply-preset', {
146+
method: 'POST',
147+
body: JSON.stringify({ path: pptPath, preset: presetName }),
148+
})
149+
await loadPPT(data.output_path)
150+
setNotice(`Preset switched to ${presetName} and applied immediately.`)
151+
} catch (e) {
152+
setNotice(`Apply preset failed: ${e.message}`)
153+
} finally {
154+
setLoading(false)
155+
}
156+
}
157+
139158
const generatePPT = async () => {
140159
if (!form.topic.trim()) return
141160
setLoading(true)
@@ -322,27 +341,6 @@ export default function App() {
322341
</label>
323342

324343
<div className="field-grid">
325-
<label>
326-
Theme preset
327-
<div className="theme-cards">
328-
{PRESET_OPTIONS.map((option) => (
329-
<button
330-
type="button"
331-
key={option.name}
332-
data-testid={`preset-${option.name.toLowerCase()}`}
333-
className={`theme-card ${form.preset === option.name ? 'active' : ''}`}
334-
onClick={() => setForm((f) => ({ ...f, preset: option.name }))}
335-
>
336-
<div className="theme-swatches">
337-
<span style={{ background: option.colors[0] }} />
338-
<span style={{ background: option.colors[1] }} />
339-
</div>
340-
<strong>{option.name}</strong>
341-
<small>{option.desc}</small>
342-
</button>
343-
))}
344-
</div>
345-
</label>
346344
<label>
347345
<span className="label-row">
348346
Audience
@@ -411,6 +409,28 @@ export default function App() {
411409
</div>
412410
</div>
413411

412+
<div className="preset-inline">
413+
<span>Theme preset:</span>
414+
<div className="theme-cards compact">
415+
{PRESET_OPTIONS.map((option) => (
416+
<button
417+
type="button"
418+
key={option.name}
419+
data-testid={`preset-${option.name.toLowerCase()}`}
420+
className={`theme-card ${form.preset === option.name ? 'active' : ''}`}
421+
onClick={() => applyPresetImmediately(option.name)}
422+
disabled={loading}
423+
>
424+
<div className="theme-swatches">
425+
<span style={{ background: option.colors[0] }} />
426+
<span style={{ background: option.colors[1] }} />
427+
</div>
428+
<strong>{option.name}</strong>
429+
</button>
430+
))}
431+
</div>
432+
</div>
433+
414434
{!doc && <div className="skeleton">Start by entering a topic and generating a PPT from the left panel.</div>}
415435
{doc && (
416436
<div className="editor-layout">

frontend/src/styles.css

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,33 @@ textarea { min-height: 84px; resize: vertical; }
119119
gap: 8px;
120120
}
121121

122+
.preset-inline {
123+
display: flex;
124+
align-items: center;
125+
gap: 12px;
126+
margin-bottom: 10px;
127+
}
128+
129+
.preset-inline > span {
130+
color: #475569;
131+
font-size: 13px;
132+
min-width: 92px;
133+
}
134+
135+
.theme-cards.compact {
136+
margin-top: 0;
137+
grid-template-columns: repeat(4, minmax(110px, 1fr));
138+
flex: 1;
139+
}
140+
141+
.theme-cards.compact .theme-card {
142+
padding: 6px 8px;
143+
}
144+
145+
.theme-cards.compact .theme-card small {
146+
display: none;
147+
}
148+
122149
.theme-card {
123150
text-align: left;
124151
background: #f8fafc;

0 commit comments

Comments
 (0)