Files
videogen/client/src/lib/folders.ts
2026-08-15 13:43:52 +02:00

168 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { ID, Query, databaseId, tables } from './appwrite';
import { teamRechte } from './permissions';
/**
* Ein Ordner ist ein **privater Wissens-Scope**, kein Sortier-Ordner
* (konzept-bilder-feed.md §9). Beim Generieren zieht P4 nur die Attribute des
* gewählten Ordners er überschreibt die brand-weite Ebene vollständig,
* gemischt wird nicht.
*/
export type Ordner = {
$id: string;
brand_id: string;
name: string;
theme_md?: string;
zweck?: string;
startwert_modus?: Startwert;
ist_default?: boolean;
post_count?: number;
signal_count?: number;
};
/** `sammlung` = nur einsortieren · `wissens_scope` = daraus erstellen. */
export type Zweck = 'sammlung' | 'wissens_scope';
/** Woher die Attribut-Scores beim Anlegen kommen. */
export type Startwert = 'erben' | 'aus_posts' | 'neutral';
export const ZWECKE: { wert: Zweck; titel: string; erklaerung: string }[] = [
{
wert: 'wissens_scope',
titel: 'Daraus erstellen',
erklaerung: 'Generierungen ziehen ihr Wissen aus diesem Ordner. Das ist der Normalfall.',
},
{
wert: 'sammlung',
titel: 'Nur sammeln',
erklaerung: 'Reine Ablage zum Sortieren. Beeinflusst keine Generierung.',
},
];
export const STARTWERTE: { wert: Startwert; titel: string; erklaerung: string }[] = [
{
wert: 'erben',
titel: 'Vom Konto erben',
erklaerung: 'Startet mit einer Kopie der bisherigen Scores. Gut, wenn der Ordner die Marke fortsetzt.',
},
{
wert: 'aus_posts',
titel: 'Nur aus den Posts',
erklaerung:
'Startet leer und lernt ausschließlich aus dem, was hier landet. Gut, wenn der Ordner bewusst anders aussehen soll als der Rest.',
},
];
export async function ordnerListe(brandId: string): Promise<Ordner[]> {
const res = await tables.listRows({
databaseId,
tableId: 'folders',
queries: [Query.equal('brand_id', brandId), Query.limit(100)],
});
return res.rows as unknown as Ordner[];
}
export async function ordnerLesen(id: string): Promise<Ordner> {
const row = await tables.getRow({ databaseId, tableId: 'folders', rowId: id });
return row as unknown as Ordner;
}
export async function ordnerAnlegen(
brandId: string,
teamId: string,
daten: { name: string; zweck: Zweck; startwert_modus: Startwert; theme_md?: string },
): Promise<Ordner> {
const row = await tables.createRow({
databaseId,
tableId: 'folders',
rowId: ID.unique(),
data: {
brand_id: brandId,
name: daten.name,
zweck: daten.zweck,
startwert_modus: daten.startwert_modus,
theme_md: daten.theme_md ?? '',
ist_default: false,
post_count: 0,
signal_count: 0,
},
permissions: teamRechte(teamId),
});
const ordner = row as unknown as Ordner;
await ordnerInitialisieren(brandId, teamId, ordner.$id, daten.startwert_modus);
return ordner;
}
/**
* Füllt `attribute_scores` für einen frischen Ordner.
*
* `erben` kopiert die brand-weite Ebene (`folder_id = null`) der Ordner setzt
* die Marke fort. `aus_posts` und `neutral` starten leer: dort soll das Wissen
* ausschließlich aus dem entstehen, was später hineinkommt. Genau das ist der
* Zweck der Ordner Wissen **segmentieren statt mitteln**
* (konzept-bilder-feed.md §9).
*
* Läuft vorerst im Client statt in der Function `ordner-initialisieren`. Das
* ist vertretbar, weil nur eigene Zeilen kopiert werden und die Zeilenrechte
* das ohnehin begrenzen beim Umzug in eine Function ändert sich nur der Ort.
*/
export async function ordnerInitialisieren(
brandId: string,
teamId: string,
folderId: string,
modus: Startwert,
): Promise<number> {
if (modus !== 'erben') return 0;
const quelle = await tables.listRows({
databaseId,
tableId: 'attribute_scores',
queries: [Query.equal('brand_id', brandId), Query.isNull('folder_id'), Query.limit(200)],
});
const rechte = teamRechte(teamId);
let kopiert = 0;
for (const z of quelle.rows as unknown as BrandScore[]) {
await tables.createRow({
databaseId,
tableId: 'attribute_scores',
rowId: ID.unique(),
data: {
brand_id: brandId,
attribute_id: z.attribute_id,
category_id: z.category_id,
folder_id: folderId,
score: z.score ?? 5000,
start_value: z.score ?? 5000,
// Der Ordner erbt den Wert, aber nicht die Sicherheit: k_factor zurück
// auf 32, weil im neuen Scope noch nichts belegt ist.
k_factor: 32,
start_quelle: 'geerbt',
used_count: 0, wins: 0, losses: 0,
},
permissions: rechte,
});
kopiert++;
}
return kopiert;
}
type BrandScore = {
attribute_id: string;
category_id: string;
score?: number;
};
export async function ordnerLoeschen(id: string): Promise<void> {
await tables.deleteRow({ databaseId, tableId: 'folders', rowId: id });
}
/** „4 Posts lernt noch“: ohne Reifegrad ist für den Nutzer nicht erklärbar,
* warum zwei gleiche Prompts verschiedene Bilder ergeben (app-aufbau.md §5.3). */
export function reifegrad(o: Ordner): string {
const posts = o.post_count ?? 0;
const signale = o.signal_count ?? 0;
if (posts === 0) return 'leer';
if (signale < 5) return `${posts} Posts lernt noch`;
return `${posts} Posts eingespielt`;
}