Add persistent live controls and interaction notes

This commit is contained in:
hnzio committed 2026-10-24 14:31:33 +02:00
1 parent b1474bc33d
commit fa17187af4
16 files changed
+882 -62

No files matched your search

+30 -10
View File
@@ -46,6 +46,7 @@ export type PieceInput = {
intro: string;
researchText: string;
moderationNotes: string;
interactionText?: string;
transitionText: string;
presentationText: string;
presentationMode: PresentationMode;
@@ -83,7 +84,10 @@ export type PieceListItem = {
isFavorite: boolean;
};
export type PieceEditor = Omit<PieceInput, 'status' | 'tools' | 'presentationMediaId'> & {
export type PieceEditor = Omit<
PieceInput,
'status' | 'tools' | 'presentationMediaId' | 'interactionText'
> & {
id: string;
status: PieceStatus;
revision: number;
@@ -92,9 +96,12 @@ export type PieceEditor = Omit<PieceInput, 'status' | 'tools' | 'presentationMed
media: PieceMedia[];
presentationMediaId: string | null;
isFavorite: boolean;
interactionText: string;
tools: PieceTool[];
};
type ValidatedPieceInput = PieceInput;
export type PieceMedia = {
id: string;
originalName: string;
@@ -187,7 +194,7 @@ function requireTaxonomyIds(table: 'formats' | 'topics' | 'section_categories',
if (count.count !== ids.length) throw new Error('INVALID_TAXONOMY');
}
function validatePieceInput(input: PieceInput): PieceInput {
function validatePieceInput(input: PieceInput): ValidatedPieceInput {
const title = cleanText(input.title, 200, 'title');
if (!title) throw new Error('INVALID_TITLE');
if (!['inbox', 'preparing', 'ready'].includes(input.status)) throw new Error('INVALID_STATUS');
@@ -252,6 +259,10 @@ function validatePieceInput(input: PieceInput): PieceInput {
intro: cleanText(input.intro, 50_000, 'intro'),
researchText: cleanText(input.researchText, 50_000, 'research'),
moderationNotes: cleanText(input.moderationNotes, 50_000, 'moderation'),
interactionText:
input.interactionText === undefined
? undefined
: cleanText(input.interactionText, 50_000, 'interaction'),
transitionText: cleanText(input.transitionText, 50_000, 'transition'),
presentationText: cleanText(input.presentationText, 50_000, 'presentation'),
presentationMode: input.presentationMode,
@@ -402,6 +413,7 @@ type PieceRow = {
intro: string;
research_text: string;
moderation_notes: string;
interaction_text: string;
transition_text: string;
presentation_text: string;
presentation_mode: PresentationMode;
@@ -428,7 +440,7 @@ export function getPiece(id: string): PieceEditor | null {
const row = database
.prepare(
`SELECT id, title, output_title, summary, format_id, status, duration_seconds, intro, research_text,
moderation_notes, transition_text, presentation_text, presentation_mode,
moderation_notes, interaction_text, transition_text, presentation_text, presentation_mode,
presentation_source_position, presentation_title, presentation_description,
presentation_image_url, presentation_site_name, presentation_provider,
presentation_provider_id, presentation_title_manual, presentation_description_manual,
@@ -452,6 +464,7 @@ export function getPiece(id: string): PieceEditor | null {
intro: row.intro,
researchText: row.research_text,
moderationNotes: row.moderation_notes,
interactionText: row.interaction_text,
transitionText: row.transition_text,
presentationText: row.presentation_text,
presentationMode: row.presentation_mode,
@@ -600,7 +613,7 @@ export function savePiece(id: string, expectedRevision: number, rawInput: PieceI
const result = database
.prepare(
`UPDATE pieces SET title = ?, output_title = ?, summary = ?, format_id = ?, status = ?, duration_seconds = ?,
intro = ?, research_text = ?, moderation_notes = ?, transition_text = ?, presentation_text = ?,
intro = ?, research_text = ?, moderation_notes = ?, interaction_text = COALESCE(?, interaction_text), transition_text = ?, presentation_text = ?,
presentation_mode = ?, presentation_source_position = ?, presentation_title = ?,
presentation_description = ?, presentation_image_url = ?, presentation_site_name = ?,
presentation_provider = ?, presentation_provider_id = ?, presentation_title_manual = ?,
@@ -619,6 +632,7 @@ export function savePiece(id: string, expectedRevision: number, rawInput: PieceI
input.intro,
input.researchText,
input.moderationNotes,
input.interactionText ?? null,
input.transitionText,
input.presentationText,
input.presentationMode,
@@ -650,7 +664,7 @@ export function savePiece(id: string, expectedRevision: number, rawInput: PieceI
return { revision: expectedRevision + 1, updatedAtUtc };
}
function blankPieceInput(): PieceInput {
function blankPieceInput(): ValidatedPieceInput {
return {
title: 'Neues Piece',
outputTitle: '',
@@ -661,6 +675,7 @@ function blankPieceInput(): PieceInput {
intro: '',
researchText: '',
moderationNotes: '',
interactionText: '',
transitionText: '',
presentationText: '',
presentationMode: 'text',
@@ -682,7 +697,7 @@ function blankPieceInput(): PieceInput {
};
}
function templateInput(templateId: string): PieceInput {
function templateInput(templateId: string): ValidatedPieceInput {
if (templateId === 'blank') return blankPieceInput();
const row = getDatabase()
.prepare('SELECT snapshot_json FROM piece_templates WHERE id = ?')
@@ -739,8 +754,8 @@ export function createPiece(template = 'blank'): string {
database
.prepare(
`INSERT INTO pieces (id, title, output_title, summary, format_id, status, duration_seconds, intro,
research_text, moderation_notes, transition_text, presentation_text, created_at_utc,
updated_at_utc, revision) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)`
research_text, moderation_notes, interaction_text, transition_text, presentation_text, created_at_utc,
updated_at_utc, revision) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)`
)
.run(
id,
@@ -753,6 +768,7 @@ export function createPiece(template = 'blank'): string {
input.intro,
input.researchText,
input.moderationNotes,
input.interactionText ?? '',
input.transitionText,
input.presentationText,
now,
@@ -902,13 +918,16 @@ export async function duplicatePiece(sourceId: string): Promise<string> {
database
.prepare(
`INSERT INTO pieces (id, title, output_title, summary, format_id, status, duration_seconds, intro,
research_text, moderation_notes, transition_text, presentation_text, presentation_mode,
research_text, moderation_notes, interaction_text, transition_text, presentation_text, presentation_mode,
presentation_source_position, presentation_title, presentation_description,
presentation_image_url, presentation_site_name, presentation_provider,
presentation_provider_id, presentation_title_manual,
presentation_description_manual, presentation_image_manual,
presentation_metadata_fetched_at_utc, created_at_utc, updated_at_utc, revision)
VALUES (?, ?, ?, ?, ?, 'preparing', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)`
VALUES (
?, ?, ?, ?, ?, 'preparing', ?, ?, ?, ?, ?, ?, ?,
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1
)`
)
.run(
id,
@@ -920,6 +939,7 @@ export async function duplicatePiece(sourceId: string): Promise<string> {
source.intro,
source.researchText,
source.moderationNotes,
source.interactionText,
source.transitionText,
source.presentationText,
source.presentationMode,