Spaces:
Runtime error
Runtime error
Download frontend/src/components/slideDeckEdit.ts from firebird-technologies/NoteStack-Test: direct link, hf CLI and curl.
- Browser
- Download file 9.21 kB
-
https://huggingface.co/spaces/firebird-technologies/NoteStack-Test/resolve/main/frontend/src/components/slideDeckEdit.ts
- Command line
-
hf download hf://spaces/firebird-technologies/NoteStack-Test/frontend/src/components/slideDeckEdit.ts
-
curl -L -o slideDeckEdit.ts https://huggingface.co/spaces/firebird-technologies/NoteStack-Test/resolve/main/frontend/src/components/slideDeckEdit.ts
9.21 kB
| /** The slide deck editor's rules: what a slide can gain or lose, how it changes to another layout, and what must not be | |
| * left empty. They mirror the server (backend/app/slides/content.py: LIMITS and clamp_slide), which coerces whatever | |
| * comes back anyway; these only keep the editor from offering what the server would undo. */ | |
| import type { EditSlide } from "../api/endpoints"; | |
| export type DeckFmt = "detailed" | "presenter"; | |
| /** The layouts a slide can have, in the order the editor lists them. */ | |
| export const LAYOUTS: { id: string; name: string }[] = [ | |
| { id: "title", name: "Title" }, | |
| { id: "section", name: "Section" }, | |
| { id: "points", name: "Points" }, | |
| { id: "two_column", name: "Two columns" }, | |
| { id: "stat", name: "Big number" }, | |
| { id: "quote", name: "Quote" }, | |
| { id: "agenda", name: "Agenda" }, | |
| { id: "closing", name: "Closing" }, | |
| ]; | |
| export const MAX_SLIDES = 30; | |
| const max = (fmt: DeckFmt) => ({ points: fmt === "presenter" ? 4 : 5, agenda: 14, takeaways: 3, items: 4 }); | |
| const clone = (s: EditSlide): EditSlide => JSON.parse(JSON.stringify(s)) as EditSlide; | |
| /** A new slide of `layout`, with placeholder text to type over. */ | |
| export function newSlide(layout: string, fmt: DeckFmt): EditSlide { | |
| const blank: EditSlide = { | |
| layout: "section", variant: "", kicker: "", heading: "New slide", lead: "", points: [], | |
| left: { label: "", items: [] }, right: { label: "", items: [] }, stat: { value: "", label: "" }, | |
| quote: { text: "", by: "" }, takeaways: [], closing: "", notes: "", sources: [], | |
| }; | |
| return convert(blank, layout, fmt); | |
| } | |
| /** The slide changed to another layout. Its words carry over (points become a column's items, a closing's | |
| * takeaways...); what the new layout needs and the slide lacks is filled with placeholder text. The old layout's | |
| * content stays in the slide, so changing back brings it back. */ | |
| export function convert(s: EditSlide, layout: string, fmt: DeckFmt): EditSlide { | |
| const out = { ...clone(s), layout, variant: "" }; | |
| const m = max(fmt); | |
| const words = [...s.points.map((p) => p.text), ...s.takeaways, ...s.left.items, ...s.right.items].filter(Boolean); | |
| const some = (n: number, fallback: string[]) => (words.length >= 2 ? words : fallback).slice(0, n); | |
| if (layout === "points" && out.points.length < 2) { | |
| out.points = some(m.points, ["First point", "Second point", "Third point"]).map((text) => ({ term: "", text })); | |
| } else if (layout === "agenda" && out.points.length < 2) { | |
| out.points = some(m.agenda, ["First topic", "Second topic", "Third topic"]).map((text) => ({ term: "", text })); | |
| } else if (layout === "two_column" && !(out.left.items.length && out.right.items.length)) { | |
| const items = some(2 * m.items, ["A point on this side", "Another point", "A point on that side", "Another point"]); | |
| const half = Math.ceil(items.length / 2); | |
| out.left = { label: out.left.label || "One side", items: items.slice(0, half).slice(0, m.items) }; | |
| out.right = { label: out.right.label || "The other side", items: items.slice(half).slice(0, m.items) }; | |
| } else if (layout === "stat" && !(out.stat.value && out.stat.label)) { | |
| out.stat = { value: out.stat.value || "42%", label: out.stat.label || words[0] || "What this number means" }; | |
| } else if (layout === "quote" && !out.quote.text) { | |
| out.quote = { text: words[0] || out.lead || "A line worth quoting.", by: out.quote.by }; | |
| } else if (layout === "closing" && !out.takeaways.length && !out.closing) { | |
| out.takeaways = some(m.takeaways, ["First takeaway", "Second takeaway", "Third takeaway"]); | |
| } | |
| return out; | |
| } | |
| export type AddAction = { id: string; label: string; apply: (s: EditSlide) => EditSlide }; | |
| /** The text boxes a slide can gain: only those its design has a place for (`slots`, from the server) and that it | |
| * does not have yet, within the format's limits. */ | |
| export function addActions(s: EditSlide, slots: string[], fmt: DeckFmt): AddAction[] { | |
| const m = max(fmt); | |
| const has = (slot: string) => slots.includes(slot); | |
| const act = (id: string, label: string, change: (c: EditSlide) => void): AddAction => ({ | |
| id, label, apply: (from) => { | |
| const c = clone(from); | |
| change(c); | |
| return c; | |
| }, | |
| }); | |
| const out: AddAction[] = []; | |
| if (has("kicker") && !s.kicker) out.push(act("kicker", "Kicker", (c) => (c.kicker = "Kicker"))); | |
| if (has("lead") && !s.lead) out.push(act("lead", "Subtitle", (c) => (c.lead = "A sentence that adds context."))); | |
| if (s.layout === "points" && has("body") && s.points.length < m.points) { | |
| out.push(act("point", "Bullet", (c) => c.points.push({ term: "", text: "New point" }))); | |
| } | |
| if (s.layout === "agenda" && has("body") && s.points.length < m.agenda) { | |
| out.push(act("point", "Topic", (c) => c.points.push({ term: "", text: "New topic" }))); | |
| } | |
| if (s.layout === "two_column") { | |
| for (const side of ["left", "right"] as const) { | |
| const name = side === "left" ? "Left" : "Right"; | |
| if (!s[side].label) out.push(act(`${side}-label`, `${name} heading`, (c) => (c[side].label = "Heading"))); | |
| if (s[side].items.length < m.items) out.push(act(`${side}-item`, `${name} item`, (c) => c[side].items.push("New item"))); | |
| } | |
| } | |
| if (s.layout === "quote" && has("by") && !s.quote.by) out.push(act("by", "Author", (c) => (c.quote.by = "Name"))); | |
| if (s.layout === "closing") { | |
| if (has("body") && s.takeaways.length < m.takeaways) out.push(act("takeaway", "Takeaway", (c) => c.takeaways.push("New takeaway"))); | |
| if (has("closing") && !s.closing) out.push(act("closing", "Closing line", (c) => (c.closing = "One line to end on."))); | |
| } | |
| return out; | |
| } | |
| /** The text boxes on a slide that can be removed (a point only while the slide keeps enough of them; the heading, a | |
| * stat or a quote never: the layout needs them). Each is the `data-f` of the text drawn in that box. */ | |
| export function removable(s: EditSlide): string[] { | |
| const out: string[] = []; | |
| if (s.kicker) out.push("kicker"); | |
| if (s.lead) out.push("lead"); | |
| if (s.quote.by) out.push("quote.by"); | |
| if (s.closing && s.takeaways.length) out.push("closing"); | |
| const minPoints = 2; | |
| s.points.forEach((p, i) => { | |
| if (p.term) out.push(`points.${i}.term`); | |
| if (s.points.length > minPoints) out.push(`points.${i}.text`); | |
| }); | |
| if (s.takeaways.length > 1 || s.closing) s.takeaways.forEach((_, i) => out.push(`takeaways.${i}`)); | |
| for (const side of ["left", "right"] as const) { | |
| if (s[side].label) out.push(`${side}.label`); | |
| if (s[side].items.length > 1) s[side].items.forEach((_, i) => out.push(`${side}.items.${i}`)); | |
| } | |
| return out; | |
| } | |
| /** The slide without the text box at `field`: a point, takeaway or column item goes; other text is emptied. */ | |
| export function removeField(s: EditSlide, field: string): EditSlide { | |
| const c = clone(s); | |
| const [a, b, d] = field.split("."); | |
| if (a === "points" && d === "text") c.points.splice(Number(b), 1); | |
| else if (a === "points" && d === "term") c.points[Number(b)].term = ""; | |
| else if (a === "takeaways") c.takeaways.splice(Number(b), 1); | |
| else if ((a === "left" || a === "right") && b === "items") c[a].items.splice(Number(d), 1); | |
| else if ((a === "left" || a === "right") && b === "label") c[a].label = ""; | |
| else if (a === "quote" && b === "by") c.quote.by = ""; | |
| else if (a === "kicker" || a === "lead" || a === "closing") c[a] = ""; | |
| return c; | |
| } | |
| /** The slide with the text at `field` ("points.2.text", "left.items.0", "heading") set to `text`. */ | |
| export function setText(s: EditSlide, field: string, text: string): EditSlide { | |
| const c = clone(s); | |
| const path = field.split("."); | |
| let at: Record<string, unknown> | unknown[] = c as unknown as Record<string, unknown>; | |
| for (const k of path.slice(0, -1)) at = (at as Record<string, unknown>)[k] as Record<string, unknown>; | |
| const last = path[path.length - 1]; | |
| if (Array.isArray(at)) at[Number(last)] = text; | |
| else if (at && typeof at === "object") at[last] = text; | |
| return c; | |
| } | |
| const blank = (t: string) => !t.replace(/\s+/g, ""); | |
| /** What stops the deck from being saved as it is ("Slide 3: the heading can't be empty."), or null. */ | |
| export function problem(slides: EditSlide[]): string | null { | |
| for (const [i, s] of slides.entries()) { | |
| const at = (what: string) => `Slide ${i + 1}: ${what} can't be empty.`; | |
| if (blank(s.heading) && s.layout !== "quote" && s.layout !== "closing") return at("the heading"); | |
| if (s.layout === "points" || s.layout === "agenda") { | |
| const n = s.points.findIndex((p) => blank(p.text)); | |
| if (n >= 0) return at(`${s.layout === "agenda" ? "topic" : "point"} ${n + 1}`); | |
| } | |
| if (s.layout === "two_column") { | |
| for (const side of ["left", "right"] as const) { | |
| const n = s[side].items.findIndex(blank); | |
| if (n >= 0) return at(`the ${side} column's item ${n + 1}`); | |
| } | |
| } | |
| if (s.layout === "stat" && (blank(s.stat.value) || blank(s.stat.label))) return at(blank(s.stat.value) ? "the number" : "the number's label"); | |
| if (s.layout === "quote" && blank(s.quote.text)) return at("the quote"); | |
| if (s.layout === "closing") { | |
| const n = s.takeaways.findIndex(blank); | |
| if (n >= 0) return at(`takeaway ${n + 1}`); | |
| } | |
| } | |
| return null; | |
| } | |