ki integration

This commit is contained in:
2026-08-15 13:43:52 +02:00
parent 82d98bd8cf
commit 9ecb292c26
78 changed files with 12055 additions and 55 deletions

166
client/src/lib/posts.ts Normal file
View 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 {};
}
}