/** * generate-flagged-report.ts * * Reads all flagged content from the database and generates a pretty * markdown report organized by content type. The report includes: * - Summary table with counts per content type * - Plant images flagged for review * - Disease images flagged for review * - Disease symptoms flagged for review * - Disease causes flagged for review * - Disease treatment steps flagged for review * - Disease prevention tips flagged for review * * Usage: * npx tsx scripts/generate-flagged-report.ts [--min-flags N] [--output path/to/report.md] * * Options: * --min-flags Minimum flag count to include (default: 1) * --output Output path (default: scripts/.flagged-content-review-needed.md) */ import dotenv from "dotenv"; import path from "node:path"; // Load DB config from .env.development (or .env.production if NODE_ENV=production) const envFile = process.env.NODE_ENV === "production" ? "../.env.production" : "../.env.development"; dotenv.config({ path: path.resolve(__dirname, envFile) }); import { createClient } from "@libsql/client"; import fs from "node:fs"; // ─── Config ───────────────────────────────────────────────────────────────── const MIN_FLAGS = parseInt( process.argv.find((a) => a.startsWith("--min-flags="))?.split("=")[1] ?? "1", 10, ); const OUTPUT_PATH = process.argv.find((a) => a.startsWith("--output="))?.split("=")[1] ?? path.join(__dirname, ".flagged-content-review-needed.md"); // ─── DB Connection ────────────────────────────────────────────────────────── const db = createClient({ url: process.env.DATABASE_URL!, authToken: process.env.DATABASE_TOKEN!, }); // ─── Types ────────────────────────────────────────────────────────────────── interface FlaggedRow { id: string; content_type: string; content_id: string; field_name: string; notes: string; flag_count: number; created_at: string; updated_at: string; } interface PlantRow { id: string; common_name: string; scientific_name: string; family: string; image_url: string; } interface DiseaseRow { id: string; name: string; scientific_name: string; plant_id: string; image_url: string; } // ─── Helpers ──────────────────────────────────────────────────────────────── const CONTENT_TYPE_LABELS: Record = { plant_image: { emoji: "🪴", title: "Plant Images Flagged for Review", description: "Plant images that users have flagged as potentially incorrect or low quality.", }, disease_image: { emoji: "📸", title: "Disease Images Flagged for Review", description: "Disease symptom images that users have flagged as potentially incorrect or misleading.", }, disease_symptoms: { emoji: "⚠️", title: "Disease Symptoms Flagged for Review", description: "Symptom descriptions that users have flagged as potentially inaccurate.", }, disease_causes: { emoji: "🔍", title: "Disease Causes Flagged for Review", description: "Causes and contributing factors that users have flagged as potentially incorrect.", }, disease_treatment: { emoji: "💊", title: "Disease Treatment Steps Flagged for Review", description: "Treatment instructions that users have flagged as potentially incorrect or harmful.", }, disease_prevention: { emoji: "🛡️", title: "Disease Prevention Tips Flagged for Review", description: "Prevention tips that users have flagged as potentially incorrect or misleading.", }, }; function formatDate(iso: string): string { const d = new Date(iso); return d.toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", }); } // ─── Main ─────────────────────────────────────────────────────────────────── async function main() { console.log(`📋 Generating flagged content report (min flags: ${MIN_FLAGS})...`); // Fetch flagged content const flaggedRs = await db.execute({ sql: "SELECT * FROM flagged_content WHERE flag_count >= ? ORDER BY content_type, flag_count DESC, updated_at DESC", args: [MIN_FLAGS], }); const flaggedRows = flaggedRs.rows as unknown as FlaggedRow[]; if (flaggedRows.length === 0) { const report = [ "# 🚩 Flagged Content Review — Nothing to Review", "", `Generated: ${new Date().toISOString()}`, "", "**No content has been flagged for review yet.**", "", "Flagged items will appear here once users flag content for manual review.", "", "---", "", `_Report generated with min-flags=${MIN_FLAGS}_`, "", ].join("\n"); fs.writeFileSync(OUTPUT_PATH, report, "utf-8"); console.log(`✅ Report written to ${OUTPUT_PATH} (no flagged items)`); db.close(); return; } // Collect all unique plant and disease IDs const plantIds = new Set(); const diseaseIds = new Set(); for (const row of flaggedRows) { if (row.content_type === "plant_image") { plantIds.add(row.content_id); } else { diseaseIds.add(row.content_id); } } // Fetch plant names const plantMap = new Map(); if (plantIds.size > 0) { const plantRs = await db.execute({ sql: `SELECT id, common_name, scientific_name, family, image_url FROM plants WHERE id IN (${[...plantIds].map(() => "?").join(",")})`, args: [...plantIds], }); for (const row of plantRs.rows as unknown as PlantRow[]) { plantMap.set(row.id, row); } } // Fetch disease names + their plant references const diseaseMap = new Map(); if (diseaseIds.size > 0) { const diseaseRs = await db.execute({ sql: `SELECT id, name, scientific_name, plant_id, image_url FROM diseases WHERE id IN (${[...diseaseIds].map(() => "?").join(",")})`, args: [...diseaseIds], }); for (const row of diseaseRs.rows as unknown as DiseaseRow[]) { diseaseMap.set(row.id, row); if (!plantMap.has(row.plant_id)) { plantIds.add(row.plant_id); } } // Fetch any missing plant references for diseases if (plantIds.size > 0) { const missingPlantIds = [...plantIds].filter((id) => !plantMap.has(id)); if (missingPlantIds.length > 0) { const plantRs = await db.execute({ sql: `SELECT id, common_name, scientific_name, family, image_url FROM plants WHERE id IN (${missingPlantIds.map(() => "?").join(",")})`, args: missingPlantIds, }); for (const row of plantRs.rows as unknown as PlantRow[]) { plantMap.set(row.id, row); } } } } // Group by content type const groups: Record = {}; for (const row of flaggedRows) { if (!groups[row.content_type]) groups[row.content_type] = []; groups[row.content_type].push(row); } // ─── Build Report ──────────────────────────────────────────────────────── const lines: string[] = []; const totalFlags = flaggedRows.reduce((sum, r) => sum + r.flag_count, 0); lines.push("# 🚩 Flagged Content — Manual Review Needed"); lines.push(""); lines.push(`Generated: ${new Date().toISOString()}`); lines.push(""); lines.push( flaggedRows.length === 1 ? `**${flaggedRows.length} item** flagged for review (${totalFlags} total flags).` : `**${flaggedRows.length} items** flagged for review (${totalFlags} total flags).`, ); lines.push(""); lines.push("Most data in this knowledge base is not reviewed by humans. "); lines.push("Items listed below have been flagged by users for manual review. "); lines.push("Please review each item and take appropriate action."); lines.push(""); // Summary table lines.push("## 📊 Summary"); lines.push(""); lines.push("| Content Type | Count | Total Flags |"); lines.push("|---|---|---|"); const orderedTypes = [ "plant_image", "disease_image", "disease_symptoms", "disease_causes", "disease_treatment", "disease_prevention", ]; for (const type of orderedTypes) { const items = groups[type]; if (!items) continue; const label = CONTENT_TYPE_LABELS[type]?.title ?? type; const count = items.length; const sumFlags = items.reduce((s, r) => s + r.flag_count, 0); lines.push(`| ${label} | ${count} | ${sumFlags} |`); } lines.push(`| **Total** | **${flaggedRows.length}** | **${totalFlags}** |`); lines.push(""); lines.push("---"); lines.push(""); // Detail sections per content type for (const type of orderedTypes) { const items = groups[type]; if (!items) continue; const config = CONTENT_TYPE_LABELS[type]; lines.push(`## ${config?.emoji ?? "📋"} ${config?.title ?? type}`); lines.push(""); lines.push(config?.description ?? ""); lines.push(""); lines.push(`**${items.length} item${items.length === 1 ? "" : "s"} flagged**`); lines.push(""); for (const item of items) { // Build label let label = item.content_id; let plantLabel = ""; if (type === "plant_image") { const plant = plantMap.get(item.content_id); if (plant) { label = `${plant.common_name} (_${plant.scientific_name}_)`; plantLabel = `${plant.family} family`; } } else { const disease = diseaseMap.get(item.content_id); if (disease) { const plant = plantMap.get(disease.plant_id); const plantName = plant?.common_name ?? disease.plant_id; label = `${disease.name} (_${disease.scientific_name}_) on **${plantName}**`; plantLabel = `Affects: ${plantName}`; } } const flagWord = item.flag_count === 1 ? "flag" : "flags"; const firstFlagged = formatDate(item.created_at); const lastFlagged = formatDate(item.updated_at); lines.push(`### ${label}`); lines.push(""); lines.push(`- **Field:** \`${item.field_name}\``); lines.push(`- **Flags:** ${item.flag_count} ${flagWord}`); lines.push(`- **First flagged:** ${firstFlagged}`); lines.push(`- **Last flagged:** ${lastFlagged}`); if (plantLabel) { lines.push(`- **${plantLabel}**`); } if (item.notes) { lines.push(`- **User notes:** ${item.notes}`); } // Show the content data if we can fetch it if (type === "plant_image") { const plant = plantMap.get(item.content_id); if (plant?.image_url) { lines.push(""); lines.push(` ![${plant.common_name}](${plant.image_url})`); } } else { const disease = diseaseMap.get(item.content_id); if (type === "disease_image" && disease?.image_url) { lines.push(""); lines.push(` ![${disease.name}](${disease.image_url})`); } } lines.push(""); } lines.push("---"); lines.push(""); } // Footer lines.push("## ℹ️ How This Works"); lines.push(""); lines.push("1. **Users** click the 🚩 Flag button on any content they believe needs review."); lines.push("2. **The system** stores the flag in the database with a counter."); lines.push( "3. **This report** is generated by querying the database and formatting the results.", ); lines.push("4. **Reviewers** go through each item and take action (fix, update, or dismiss)."); lines.push(""); lines.push("### Taking Action"); lines.push(""); lines.push("After reviewing an item, you can clear its flags by running:"); lines.push(""); lines.push("```sql"); lines.push("DELETE FROM flagged_content WHERE id = '';"); lines.push("```"); lines.push(""); lines.push("Or clear all flags for a specific item by running:"); lines.push(""); lines.push("```sql"); lines.push( "UPDATE flagged_content SET flag_count = 0 WHERE content_id = '' AND field_name = '';", ); lines.push("```"); lines.push(""); lines.push("---"); lines.push(""); lines.push(`_Report generated with min-flags=${MIN_FLAGS}_`); // Write report fs.writeFileSync(OUTPUT_PATH, lines.join("\n"), "utf-8"); console.log(`✅ Report written to ${OUTPUT_PATH}`); console.log(` ${flaggedRows.length} items, ${totalFlags} total flags`); db.close(); } main().catch((err) => { console.error("❌ Failed to generate report:", err); process.exit(1); });