ki integration
This commit is contained in:
166
client/src/lib/posts.ts
Normal file
166
client/src/lib/posts.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
import { ID, Query, databaseId, tables } from './appwrite';
|
||||
import { teamRechte } from './permissions';
|
||||
|
||||
/**
|
||||
* Ein Post ist ein **Slot-Rezept**, kein Freitext-Prompt – nur deshalb ist er
|
||||
* überhaupt kopierbar (konzept-bilder-feed.md §3). Die Slots liegen strukturiert
|
||||
* in `slots`, damit P19 daraus später Chips ableiten kann, ohne den Prompt im
|
||||
* Wortlaut preiszugeben.
|
||||
*/
|
||||
export type Slots = {
|
||||
kulisse_asset_id?: string;
|
||||
person_asset_id?: string;
|
||||
produkt_asset_id?: string;
|
||||
licht?: string;
|
||||
kamera?: string;
|
||||
farbe?: string;
|
||||
werbetext?: string;
|
||||
};
|
||||
|
||||
export type Post = {
|
||||
$id: string;
|
||||
brand_id: string;
|
||||
folder_id?: string;
|
||||
titel?: string;
|
||||
user_prompt?: string;
|
||||
slots?: string;
|
||||
format?: string;
|
||||
bild_count?: number;
|
||||
status?: 'entwurf' | 'generiert' | 'fehler';
|
||||
sichtbarkeit?: 'privat' | 'oeffentlich';
|
||||
nische?: string;
|
||||
};
|
||||
|
||||
export const FORMATE = [
|
||||
{ wert: '1:1', titel: 'Quadrat', erklaerung: 'Feed-Beiträge, Produktkacheln.' },
|
||||
{ wert: '4:5', titel: 'Hochformat', erklaerung: 'Nimmt im Feed mehr Fläche ein.' },
|
||||
{ wert: '9:16', titel: 'Story', erklaerung: 'Bildschirmfüllend, für Stories und Reels.' },
|
||||
] as const;
|
||||
|
||||
export type Format = (typeof FORMATE)[number]['wert'];
|
||||
|
||||
export async function postsImOrdner(folderId: string): Promise<Post[]> {
|
||||
const res = await tables.listRows({
|
||||
databaseId,
|
||||
tableId: 'posts',
|
||||
queries: [Query.equal('folder_id', folderId), Query.orderDesc('$createdAt'), Query.limit(100)],
|
||||
});
|
||||
return res.rows as unknown as Post[];
|
||||
}
|
||||
|
||||
export async function postsDerMarke(brandId: string): Promise<Post[]> {
|
||||
const res = await tables.listRows({
|
||||
databaseId,
|
||||
tableId: 'posts',
|
||||
queries: [Query.equal('brand_id', brandId), Query.orderDesc('$createdAt'), Query.limit(100)],
|
||||
});
|
||||
return res.rows as unknown as Post[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Legt das Rezept an und reiht die Generierungen ein.
|
||||
*
|
||||
* Der Post entsteht **sofort** mit `status: 'entwurf'`, bevor irgendetwas
|
||||
* generiert wird. Das ist Absicht: Generierung ist asynchron, und wer während
|
||||
* des Wartens weg navigiert, muss das Ergebnis wiederfinden (app-aufbau.md
|
||||
* §2.1). Ohne die Zeile gäbe es nichts, wohin man zurückkehren könnte.
|
||||
*
|
||||
* Je Bild der Kette eine `jobs`-Zeile. Abgearbeitet werden sie vom
|
||||
* Job-Dispatcher mit Server-Key – **nicht** vom Client, denn dafür müsste der
|
||||
* Anbieter-Schlüssel ins Bundle.
|
||||
*/
|
||||
export async function postAnlegen(
|
||||
brandId: string,
|
||||
teamId: string,
|
||||
daten: {
|
||||
folderId: string;
|
||||
titel: string;
|
||||
prompt: string;
|
||||
format: Format;
|
||||
bildCount: number;
|
||||
slots: Slots;
|
||||
nische?: string;
|
||||
},
|
||||
): Promise<Post> {
|
||||
const rechte = teamRechte(teamId);
|
||||
|
||||
const post = (await tables.createRow({
|
||||
databaseId,
|
||||
tableId: 'posts',
|
||||
rowId: ID.unique(),
|
||||
data: {
|
||||
brand_id: brandId,
|
||||
folder_id: daten.folderId,
|
||||
titel: daten.titel,
|
||||
user_prompt: daten.prompt,
|
||||
slots: JSON.stringify(daten.slots),
|
||||
format: daten.format,
|
||||
bild_count: daten.bildCount,
|
||||
status: 'entwurf',
|
||||
sichtbarkeit: 'privat', // Veröffentlichen ist ein aktiver, eigener Schritt
|
||||
kopien_count: 0,
|
||||
feed_score: 0,
|
||||
...(daten.nische ? { nische: daten.nische } : {}),
|
||||
},
|
||||
permissions: rechte,
|
||||
})) as unknown as Post;
|
||||
|
||||
for (let i = 0; i < daten.bildCount; i++) {
|
||||
await tables.createRow({
|
||||
databaseId,
|
||||
tableId: 'jobs',
|
||||
rowId: ID.unique(),
|
||||
data: {
|
||||
brand_id: brandId,
|
||||
typ: 'bild_gen',
|
||||
prompt_template_key: 'P7',
|
||||
status: 'wartend',
|
||||
refs: JSON.stringify({ post_id: post.$id, position: i + 1 }),
|
||||
},
|
||||
permissions: rechte,
|
||||
});
|
||||
}
|
||||
|
||||
return post;
|
||||
}
|
||||
|
||||
export type PostBild = {
|
||||
$id: string;
|
||||
post_id: string;
|
||||
position: number;
|
||||
typ: 'motiv' | 'text_overlay';
|
||||
storage_file_id?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Die Bilderketten mehrerer Posts in einer Abfrage – nicht eine je Post.
|
||||
* Ein Ordner mit 20 Posts wären sonst 20 Rundreisen für eine Listenansicht.
|
||||
*/
|
||||
export async function bilderZuPosts(postIds: string[]): Promise<Map<string, PostBild[]>> {
|
||||
const nach = new Map<string, PostBild[]>();
|
||||
if (!postIds.length) return nach;
|
||||
|
||||
const res = await tables.listRows({
|
||||
databaseId,
|
||||
tableId: 'post_images',
|
||||
queries: [Query.equal('post_id', postIds), Query.orderAsc('position'), Query.limit(200)],
|
||||
});
|
||||
for (const b of res.rows as unknown as PostBild[]) {
|
||||
const liste = nach.get(b.post_id) ?? [];
|
||||
liste.push(b);
|
||||
nach.set(b.post_id, liste);
|
||||
}
|
||||
return nach;
|
||||
}
|
||||
|
||||
export async function postLoeschen(id: string): Promise<void> {
|
||||
await tables.deleteRow({ databaseId, tableId: 'posts', rowId: id });
|
||||
}
|
||||
|
||||
export function slotsLesen(post: Post): Slots {
|
||||
try {
|
||||
return post.slots ? (JSON.parse(post.slots) as Slots) : {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user