flag impl fin

This commit is contained in:
2026-06-06 17:22:31 -04:00
parent db4c656730
commit 96de91e86c
12 changed files with 1025 additions and 65 deletions

View File

@@ -4349,7 +4349,707 @@
"wiki-distortion-mosaic", "wiki-distortion-mosaic",
"wiki-dock-anthracnose", "wiki-dock-anthracnose",
"wiki-dogwood-anthracnose", "wiki-dogwood-anthracnose",
"wiki-dogwood-leaf-spot" "wiki-dogwood-leaf-spot",
"wiki-dogwood-spot-anthracnose",
"wiki-dollar-spot",
"wiki-double-blossom",
"wiki-douglas-fir-needle-cast",
"wiki-douglas-fir-root-disease",
"wiki-downy-spot",
"wiki-drop",
"wiki-dropping",
"wiki-drought",
"wiki-dryberry-disease",
"wiki-dutch-elm-disease",
"wiki-dwarf-mistletoe",
"wiki-dwarf-mosaic",
"wiki-dwarfism",
"wiki-early-leaf-spot",
"wiki-early-leaf-spot-of-peanut",
"wiki-eggplant-little-leaf",
"wiki-eggplant-mosaic",
"wiki-eggplant-mottle-dwarf",
"wiki-eggplant-phomopsis",
"wiki-elderberry-leaf-spot",
"wiki-elderberry-virus",
"wiki-elephantiasis",
"wiki-elm-leaf-scorch",
"wiki-elm-mosaic",
"wiki-elm-mottle",
"wiki-elm-stripe",
"wiki-elm-witches-broom",
"wiki-elm-yellows",
"wiki-epicarp-lesion",
"wiki-esca",
"wiki-eucalyptus-leaf-spot",
"wiki-euonymus-anthracnose",
"wiki-euphorbia-mosaic",
"wiki-european-stone-fruit-yellows",
"wiki-evergreen-blackberry",
"wiki-exobasidium-leaf-spot",
"wiki-fabraea-leaf-spot",
"wiki-fabrea-leaf-spot",
"wiki-fairy-ring",
"wiki-fairy-ring-leaf-spot",
"wiki-false-blossom",
"wiki-false-loose-smut",
"wiki-false-root-knot-nematode",
"wiki-fanleaf",
"wiki-ferrugem-do-feijoeiro-comum",
"wiki-filbert-stunt",
"wiki-filiform",
"wiki-flavescence-doree",
"wiki-fleck",
"wiki-fleur",
"wiki-flyspeck-of-apple",
"wiki-foliar-nematode",
"wiki-foliar-nematodes",
"wiki-foliar-vein-yellowing",
"wiki-freckle",
"wiki-frosty-pod",
"wiki-fruit-blotch-of-apple",
"wiki-fruit-chimera",
"wiki-fruit-drop",
"wiki-fungal-brown-spot",
"wiki-fungal-scald",
"wiki-fusariosis",
"wiki-fusarium-bark-disease",
"wiki-fused-fingers",
"wiki-galls",
"wiki-giantism",
"wiki-gnomonia-leaf-spot",
"wiki-golden-mosaic",
"wiki-grape-rust",
"wiki-grassy-shoot",
"wiki-gray-mold-of-apple",
"wiki-gray-snow-mold",
"wiki-gray-streak",
"wiki-gray-wall",
"wiki-grease-spot",
"wiki-greasy-blotch",
"wiki-greasy-center",
"wiki-green-blotch",
"wiki-green-mosaic",
"wiki-green-ring",
"wiki-green-scale",
"wiki-greening",
"wiki-ground-nut-rosette",
"wiki-groundnut-leaf-spot",
"wiki-guinea-grass-mosaic",
"wiki-gummosis",
"wiki-hairy-nightshade",
"wiki-hazelnut-mosaic",
"wiki-hazelnut-yellows",
"wiki-head-mold",
"wiki-head-smut",
"wiki-heart-leaf-disorder",
"wiki-heart-leaf-unfurling-disorder",
"wiki-helenium-s",
"wiki-helminthosporium-leaf-spot",
"wiki-hemp-mosaic",
"wiki-hemp-streak",
"wiki-high-mat",
"wiki-holeus-spot",
"wiki-honey-fungus",
"wiki-hop-latent",
"wiki-hop-mosaic",
"wiki-horn-sprout",
"wiki-horse",
"wiki-huanglongbing",
"wiki-impatiens-necrotic-spot",
"wiki-indian-cassava-mosaic",
"wiki-infectious-bud-failure",
"wiki-japanese-beetle",
"wiki-joint-infection",
"wiki-joint-infection-of-apple",
"wiki-kalancho-mosaic",
"wiki-karnal-bunt",
"wiki-kernel-decay",
"wiki-kernel-molds",
"wiki-kernel-shrivel",
"wiki-kernel-spot",
"wiki-khamedj",
"wiki-knot",
"wiki-lace-leaf",
"wiki-late-leaf-rust",
"wiki-latex-eruption",
"wiki-leaf-and-bud-nematode",
"wiki-leaf-and-pod-spot",
"wiki-leaf-and-stem-nematode",
"wiki-leaf-anthracnose",
"wiki-leaf-bleaching",
"wiki-leaf-blister",
"wiki-leaf-crinkle",
"wiki-leaf-cupping",
"wiki-leaf-glazing",
"wiki-leaf-miner",
"wiki-leaf-mottle",
"wiki-leaf-pucker-of-apple",
"wiki-leaf-roll",
"wiki-leaf-scorch",
"wiki-leaf-streak",
"wiki-leaf-stripe",
"wiki-leaf-yellowing",
"wiki-leak",
"wiki-lettuce-infectious-yellows",
"wiki-lettuce-mosaic",
"wiki-lichen",
"wiki-light-leaf-spot",
"wiki-lime-blotch",
"wiki-line-pattern",
"wiki-lint-contamination",
"wiki-little-cherry",
"wiki-liver-spot",
"wiki-lixa-pequena",
"wiki-loose-kernel-smut",
"wiki-lophodermium-leaf-spot",
"wiki-maize-bushy-stunt",
"wiki-maize-chlorotic-dwarf",
"wiki-maize-chlorotic-mottle",
"wiki-maize-dwarf-mosaic",
"wiki-maize-mosaic",
"wiki-maize-rayado-fino",
"wiki-maize-rough-dwarf",
"wiki-maize-stripe",
"wiki-maize-tassel-abortion",
"wiki-maize-white-line-mosaic",
"wiki-maize-yellow-stripe",
"wiki-malayan-leaf-spot",
"wiki-mancha-angular",
"wiki-mango-scale",
"wiki-marginal-leaf-burn",
"wiki-maturity-bronzing",
"wiki-mealiness",
"wiki-mealybug",
"wiki-mela",
"wiki-mid-vein-spot",
"wiki-mild-mosaic",
"wiki-mildew",
"wiki-milo-disease",
"wiki-minor-leaf-spot",
"wiki-mistletoe",
"wiki-mofo-branco",
"wiki-moko",
"wiki-mold",
"wiki-moldy-core",
"wiki-monilia",
"wiki-moorpark-mottle",
"wiki-mop-top",
"wiki-mosaic-disease",
"wiki-mosaic-or-ringspot",
"wiki-mottle-leaf",
"wiki-mouse-ear",
"wiki-murcha-de-fusario",
"wiki-mycorrhiza",
"wiki-myriogenospora-leaf-binding",
"wiki-myrothecium-leaf-spot",
"wiki-mystrosporium-leaf-spot",
"wiki-navel-orange-worm",
"wiki-neck-blast",
"wiki-necrosis",
"wiki-necrotic-leaf-blotch",
"wiki-necrotic-leaf-blotch-of-apple",
"wiki-necrotic-ring-spot",
"wiki-necrotic-ringspot",
"wiki-nematode",
"wiki-net-blotch",
"wiki-nettlehead",
"wiki-noninfectious-bud-failure",
"wiki-northern-anthracnose",
"wiki-oak-root-fungus",
"wiki-oat-leaf-blotch",
"wiki-oat-mosaic",
"wiki-oat-necrotic-mottle",
"wiki-oat-red-leaf",
"wiki-oat-smut",
"wiki-oidio-do-feijoeiro-comum",
"wiki-olive-leaf-spot",
"wiki-orange-dog",
"wiki-orange-rust",
"wiki-other-nematodes",
"wiki-pale-leaf",
"wiki-palm-leaf-spot",
"wiki-panagrolaimus-nematode",
"wiki-panama-disease",
"wiki-panicum-mosaic",
"wiki-papaya-bunchy-top",
"wiki-pasmo",
"wiki-pea-enation-mosaic",
"wiki-pea-leaf-roll",
"wiki-pea-mosaic",
"wiki-pea-seed-borne-mosaic",
"wiki-pea-seedborne-mosaic",
"wiki-peach-leaf-curl",
"wiki-peach-mosaic-virus",
"wiki-peach-rosette",
"wiki-peach-scab",
"wiki-peach-yellow-bud-mosaic",
"wiki-peach-yellow-mottle",
"wiki-peach-yellows",
"wiki-peanut-mottle-virus",
"wiki-peanut-rust",
"wiki-pear-leaf-blister-mite",
"wiki-pear-leaf-curl",
"wiki-pear-leaf-spot",
"wiki-pear-psylla",
"wiki-pear-rust",
"wiki-pear-slug",
"wiki-pear-stony-pit",
"wiki-pearly-root",
"wiki-pecan-scab",
"wiki-pecky-rice",
"wiki-pelargonium-flower-break",
"wiki-pelargonium-line-pattern",
"wiki-pelargonium-ring-pattern",
"wiki-pelargonium-ringspot",
"wiki-pelargonium-zonale-spot",
"wiki-penicillium-mold",
"wiki-pepper-golden-mosaic",
"wiki-pepper-huasteco",
"wiki-pepper-leaf-curl",
"wiki-pepper-mottle",
"wiki-pepper-veinal-mottle",
"wiki-periconia-leaf-spot",
"wiki-persimmon",
"wiki-pestalosphaeria-leaf-spot",
"wiki-pestalotia-leaf-spot",
"wiki-petal-fall",
"wiki-peter-s-scorch",
"wiki-phaeoisariopsis-leaf-spot",
"wiki-phaeoseptoria-leaf-spot",
"wiki-phomopsis-cane-and-leaf-spot",
"wiki-phony-peach",
"wiki-phyllody",
"wiki-phylloxera",
"wiki-physoderma-brown-spot",
"wiki-phytophthora-leaf-fall",
"wiki-phytoplasma",
"wiki-pierce-s-disease",
"wiki-pin-sized-lesion",
"wiki-pink-snow-mold",
"wiki-pistachio",
"wiki-pitting",
"wiki-pleospora-leaf-spot",
"wiki-plum-pox",
"wiki-pod-mottle",
"wiki-podridao-cinzenta-do-caule",
"wiki-podridao-do-colo",
"wiki-podridao-radicular-de-rhizoctonia",
"wiki-podridao-radicular-seca",
"wiki-pokkah-boeng",
"wiki-postbloom-fruit-drop",
"wiki-potato-leaf-roll",
"wiki-potato-mop-top",
"wiki-potato-scab",
"wiki-potato-spindle-tuber-viroid",
"wiki-potato-x",
"wiki-potato-yellow-dwarf",
"wiki-potexvirus-unnamed",
"wiki-powdery-scab",
"wiki-primula-mosaic",
"wiki-primula-mottle",
"wiki-proliferation",
"wiki-prune-dwarf",
"wiki-prunus-necrotic-ringspot",
"wiki-pseudomonas-leaf-spot",
"wiki-pucciniastrum-leaf-rust",
"wiki-pucker-leaf",
"wiki-purple-blotch",
"wiki-purple-top",
"wiki-pyricularia-leaf-spot",
"wiki-pythium-damping-off",
"wiki-pythium-leak",
"wiki-radish-mosaic",
"wiki-rai-mosaic",
"wiki-ramularia-leaf-spot",
"wiki-raspberry",
"wiki-raspberry-bushy-dwarf",
"wiki-raspberry-leaf-curl",
"wiki-raspberry-leaf-spot",
"wiki-raspberry-mosaic",
"wiki-raspberry-veinbanding-mosaic",
"wiki-raspberry-yellow-dwarf",
"wiki-raspberry-yellow-spot",
"wiki-ratoon-stunting",
"wiki-rayadilla",
"wiki-red-blister",
"wiki-red-blotch",
"wiki-red-boot",
"wiki-red-leaf-blotch",
"wiki-red-leather-disease",
"wiki-red-spider",
"wiki-red-stripe",
"wiki-red-thread",
"wiki-redberry-disease",
"wiki-replant-problem",
"wiki-rhizoctonia-damping-off",
"wiki-rhizoctonia-soreshin",
"wiki-rhizomania",
"wiki-rhynchosia-little-leaf",
"wiki-rhynchosia-mosaic",
"wiki-rice-black-streaked-dwarf",
"wiki-rice-blast",
"wiki-rice-bunts",
"wiki-rice-dwarf",
"wiki-rice-grassy-stunt",
"wiki-rice-ragged-stunt",
"wiki-rice-transitory-yellowing",
"wiki-rice-tungro",
"wiki-rice-yellow-mottle",
"wiki-rind-disease",
"wiki-rind-spot",
"wiki-ringspot",
"wiki-ringspots",
"wiki-rna-virus",
"wiki-root-smallpox-disease",
"wiki-rose-leaf",
"wiki-rose-leaf-curl",
"wiki-rose-mosaic",
"wiki-rose-rosette",
"wiki-rosette-of-apple",
"wiki-rosetting",
"wiki-rough-dwarf",
"wiki-roxana",
"wiki-rubbery-wood-of-apple",
"wiki-rubus-stunt",
"wiki-russet",
"wiki-rust-mite",
"wiki-rust-white",
"wiki-rusty-spot",
"wiki-rye-ergot",
"wiki-satellite-panicum-mosaic",
"wiki-scab-of-apple",
"wiki-scab-of-citrus",
"wiki-scab-of-peach",
"wiki-scald-of-apple",
"wiki-sclerotinia",
"wiki-scopulariopsis-leaf-spot",
"wiki-scorch",
"wiki-seedling-disease",
"wiki-seedling-rust",
"wiki-segmented-banana",
"wiki-septoria-brown-spot",
"wiki-septoria-leaf-blotch",
"wiki-septoria-nodorum-blotch",
"wiki-septoria-tritici-blotch",
"wiki-shenandoah",
"wiki-shothole",
"wiki-silver-leaf-of-apple",
"wiki-silvering-disease",
"wiki-sinaloa-tomato-leaf-curl",
"wiki-slime-flux",
"wiki-slime-mold",
"wiki-small-hop",
"wiki-smut",
"wiki-snow-mold",
"wiki-snow-scald",
"wiki-soilborne-mosaic",
"wiki-sooty-blotch-of-apple",
"wiki-sore-shin",
"wiki-southern-anthracnose",
"wiki-southern-root-knot-nematode",
"wiki-southern-rust",
"wiki-speckle",
"wiki-speckled-blotch",
"wiki-speckled-leaf-blotch",
"wiki-speckled-snow-mold",
"wiki-sphacelia",
"wiki-sphaerotheca-pannosa",
"wiki-spike",
"wiki-spike-leaf",
"wiki-spiral-nematode-root-damage",
"wiki-split-peel",
"wiki-spot-anthracnose",
"wiki-spraing",
"wiki-spring-black-stem",
"wiki-squirter",
"wiki-stackburn",
"wiki-stagonospora-blotch",
"wiki-stagonospora-leaf-spot",
"wiki-stalk-smut",
"wiki-stem-and-bulb-nematode",
"wiki-stem-break",
"wiki-stem-nematode",
"wiki-stem-pitting",
"wiki-stem-pitting-of-apple",
"wiki-stem-rust",
"wiki-stem-smut",
"wiki-stem-splitting",
"wiki-stem-streak",
"wiki-stem-twisting-and-bending",
"wiki-stemphylium-leaf-and-stem-spot",
"wiki-sterility-mosaic",
"wiki-sticky-disease",
"wiki-stigmatomycosis",
"wiki-stippling",
"wiki-stolon-decay",
"wiki-stone-pitting",
"wiki-stony-pit",
"wiki-storage-mold",
"wiki-strawberry",
"wiki-strawberry-leaf-roll",
"wiki-strawberry-leaf-spot",
"wiki-strawberry-yellow-edge",
"wiki-strawbreaker",
"wiki-streak",
"wiki-striatura-ulcerosa",
"wiki-stripe",
"wiki-stripe-rust",
"wiki-stubby-root",
"wiki-stylet-nematode",
"wiki-sudden-death",
"wiki-sugar-end",
"wiki-sugarcane-mosaic",
"wiki-sulfur-deficiency",
"wiki-summer-black-stem",
"wiki-sun-blotch",
"wiki-sun-scald",
"wiki-sunblotch",
"wiki-superelongation",
"wiki-sweet-potato",
"wiki-sweet-potato-virus-disease",
"wiki-sycamore",
"wiki-taches-brunes",
"wiki-taiwan-marginal-scorch",
"wiki-take-all",
"wiki-tan-spot",
"wiki-tatter-leaf",
"wiki-tea",
"wiki-terminal-mottle",
"wiki-thrips",
"wiki-tikka-disease",
"wiki-tilletia",
"wiki-tip-over",
"wiki-tobacco",
"wiki-tobacco-ringspot",
"wiki-tobacco-ringspot-virus",
"wiki-tomato",
"wiki-tomato-aspermy",
"wiki-tomato-leaf-curl",
"wiki-tomato-ringspot",
"wiki-top-spotting",
"wiki-tristeza",
"wiki-tropical-rust",
"wiki-trunk-pitting",
"wiki-turnip-mosaic",
"wiki-twig-die-back",
"wiki-vein-clearing",
"wiki-vein-enation",
"wiki-vein-spot",
"wiki-veinbanding",
"wiki-verbena-latent",
"wiki-water-mold",
"wiki-web-blotch",
"wiki-weevil",
"wiki-western-x-disease",
"wiki-wet-feet",
"wiki-wheat-curl-mite",
"wiki-wheat-soil-borne-mosaic",
"wiki-white-blister",
"wiki-white-bract",
"wiki-white-head",
"wiki-white-pine-blister-rust",
"wiki-white-thread",
"wiki-wilt-of-fruit-bunches",
"wiki-winter-injury",
"wiki-wire-stem",
"wiki-wirestem",
"wiki-wirrega-blotch",
"wiki-witch-s-broom",
"wiki-witches-broom-of-apple",
"wiki-withertip",
"wiki-wood-decay",
"wiki-wood-pocket",
"wiki-wood-stains",
"wiki-woolly-apple-aphid",
"wiki-x-disease",
"wiki-xanthomonas-leaf-spot",
"wiki-xyloporosis",
"wiki-xyloporosis-of-apple",
"wiki-yellow-edge",
"wiki-yellow-leaf-curl",
"wiki-yellow-leaf-spot",
"wiki-yellow-mat",
"wiki-yellow-mosaic",
"wiki-yellow-mottle",
"wiki-yellow-net-vein",
"wiki-yellow-pulp",
"wiki-yellow-shoot",
"wiki-yellow-sigatoka",
"wiki-yellow-stunt",
"wiki-yellowing",
"wiki-zebra-chip",
"wiki-zonate-eye-spot",
"tomato-tomato-fruitworm",
"tomato-tomato-hornworm",
"tomato-tobacco-hornworm",
"tomato-brown-tipped-pearl",
"tomato-eggplant-borer",
"tomato-tomato-fruit-borer",
"tomato-eggplant-leafroller",
"tomato-potato-tuber-moth",
"tomato-tomato-borer",
"tomato-tomato-pinworm",
"tomato-root-knot",
"tomato-sting",
"tomato-stubby-root",
"tomato-autogenous-necrosis",
"tomato-fruit-pox",
"tomato-gold-fleck",
"tomato-graywall",
"potato-potato-cyst-nematode",
"potato-lesion-nematode",
"potato-potato-rot-nematode",
"potato-root-knot-nematode",
"potato-sting-nematode",
"potato-stubby-root-nematode",
"potato-aerial-tubers",
"potato-air-pollution-injury",
"potato-black-heart",
"potato-blackspot-bruise",
"potato-elephant-hide",
"potato-hollow-heart",
"potato-internal-brown-spot-heat-necrosis",
"potato-jelly-end-rot",
"potato-physiological-leaf-roll",
"potato-psyllid-yellows",
"potato-shatter-bruise",
"potato-stem-end-browning",
"apple-dagger-nematode",
"apple-lesion-nematode",
"apple-pin-nematode",
"apple-ring-nematode",
"apple-root-knot-nematode",
"apple-bitter-pit",
"apple-blossom-blast",
"apple-fruit-cracking",
"apple-fruit-russet",
"apple-green-mottle",
"apple-hollow-apple",
"apple-internal-bark-necrosis-measles",
"apple-internal-browning",
"apple-jonathan-spot",
"apple-narrow-leaf",
"apple-necrotic-leaf-blotch-of-golden-delicious",
"apple-spray-injury",
"apple-storage-scald",
"apple-sunscald",
"apple-water-core",
"apricot-lesion",
"apricot-ring",
"apricot-root-knot",
"apricot-apricot-gumboil",
"avocado-algal-spot",
"avocado-blackstreak",
"avocado-littleleaf-rosette",
"avocado-tipburn",
"barley-cereal-cyst-nematode",
"barley-cereal-root-knot-nematode",
"barley-root-gall-nematode",
"barley-root-lesion-nematode",
"barley-stunt-nematode",
"barley-physiological-leaf-spot",
"carrot-cyst-nematode",
"carrot-dagger-nematode",
"carrot-lance-nematode",
"carrot-lesion-nematode",
"carrot-root-knot",
"carrot-sting-nematode",
"carrot-stubby-root-nematodes",
"carrot-crown-rot-disorder",
"carrot-heat-canker",
"carrot-hollow-black-heart",
"carrot-ozone-injury",
"carrot-root-scab",
"carrot-speckled-carrot",
"citrus-citrus-slump-nematode",
"citrus-dagger-nematode",
"citrus-lesion-nematode",
"citrus-needle-nematode",
"citrus-root-knot-nematode",
"citrus-sheath-nematode",
"citrus-slow-decline-citrus-nematode",
"citrus-spreading-decline-burrowing-nematode",
"citrus-sting-nematode",
"citrus-stubby-root-nematode",
"citrus-stunt-nematode",
"citrus-amachamiento",
"citrus-blossom-end-clearing",
"citrus-citrus-blight",
"citrus-creasing",
"citrus-crinkle-scurf",
"citrus-lemon-sieve-tube-necrosis",
"citrus-lime-blotch-wood-pocket",
"citrus-mesophyll-collapse",
"citrus-oleocellosis",
"citrus-postharvest-pitting",
"citrus-puffing",
"citrus-rind-breakdown",
"citrus-rind-staining",
"citrus-rind-stipple-of-grapefruit",
"citrus-rumple-of-lemon-fruit",
"citrus-shell-bark-complex",
"citrus-stem-end-rind-breakdown",
"citrus-stylar-end-breakdown-of-tahiti-lime",
"citrus-stylar-end-rind-breakdown",
"citrus-stylar-end-rot",
"citrus-tangerine-dieback",
"citrus-water-spot",
"citrus-zebra-skin",
"coconut-bristle-top",
"coconut-dry-bud-rot",
"coconut-finschafen-disease",
"coconut-frond-rot",
"coconut-leaf-scorch-decline",
"coconut-malaysia-wilt",
"coconut-red-ring-disease",
"coconut-porroca-disease",
"coconut-coconut-lethal-crown-atrophy",
"coffee-root-knot",
"coffee-",
"coffee-hot-and-cold-disease",
"coffee-physiological-effect-of-overbearing",
"corn-awl",
"corn-bulb-and-stem",
"corn-burrowing",
"corn-false-root-knot",
"corn-lance-columbia",
"corn-lesion",
"corn-needle",
"corn-ring",
"corn-root-knot",
"corn-sting",
"corn-stubby-root",
"cucumber-air-pollution-injury",
"cucumber-bitter-fruit",
"cucumber-blossom-end-rot",
"cucumber-bottle-neck-of-fruit",
"cucumber-sandburn",
"cucumber-sunscald-fruit",
"cucumber-windburn",
"cucumber-dagger-american",
"cucumber-lesion",
"cucumber-reniform",
"cucumber-ring",
"cucumber-root-knot",
"cucumber-sting",
"cucumber-stubby-root",
"grape-berry-rot",
"grape-black-measles",
"grape-esca-apoplexy",
"grape-little-leaf",
"grape-oxidant-stipple",
"grape-rupestris-speckle",
"grape-stem-necrosis-water-berry-grape-peduncle-necrosis",
"grape-",
"grape-dagger-american",
"grape-lesion",
"grape-needle",
"grape-reniform",
"grape-ring",
"grape-root-knot",
"grape-stubby-root",
"lettuce-brown-stain"
], ],
"totalFound": 4350 "totalFound": 5050
} }

View File

@@ -89,6 +89,11 @@ const CONTENT_TYPE_LABELS: Record<string, { emoji: string; title: string; descri
description: description:
"Disease symptom images that users have flagged as potentially incorrect or misleading.", "Disease symptom images that users have flagged as potentially incorrect or misleading.",
}, },
disease_description: {
emoji: "📝",
title: "Disease Descriptions Flagged for Review",
description: "Disease descriptions that users have flagged as potentially inaccurate.",
},
disease_symptoms: { disease_symptoms: {
emoji: "⚠️", emoji: "⚠️",
title: "Disease Symptoms Flagged for Review", title: "Disease Symptoms Flagged for Review",
@@ -245,6 +250,7 @@ async function main() {
const orderedTypes = [ const orderedTypes = [
"plant_image", "plant_image",
"disease_image", "disease_image",
"disease_description",
"disease_symptoms", "disease_symptoms",
"disease_causes", "disease_causes",
"disease_treatment", "disease_treatment",

View File

@@ -20,7 +20,7 @@
*/ */
import "dotenv/config"; import "dotenv/config";
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs"; import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from "fs";
import { resolve, extname } from "path"; import { resolve, extname } from "path";
// Load .env.development for DB creds // Load .env.development for DB creds
@@ -137,17 +137,32 @@ interface DuckDuckGoImageResult {
width: number; width: number;
} }
interface SourceState {
exhausted: boolean;
}
interface ClassProgress { interface ClassProgress {
count: number; count: number;
downloaded: number; downloaded: number;
failed: number; failed: number;
seenUrls: string[]; seenUrls: string[];
exhausted: boolean; exhausted: boolean;
/** Per-source exhaustion tracking — prevents re-scraping exhausted sources on resume */
sources: {
db: SourceState;
duckduckgo: SourceState;
inaturalist: SourceState;
wikimedia: SourceState;
};
} }
interface Progress { interface Progress {
lastUpdated: string; lastUpdated: string;
classes: Record<string, ClassProgress>; classes: Record<string, ClassProgress>;
/** Phase checkpoint: 0=core, 1=full, 2=healthy. On resume, skip to this phase. */
phase: number;
/** Index within the current phase's disease array. On resume, skip to this index. */
phaseIndex: number;
} }
// ─── DB Loading ────────────────────────────────────────────────────────────── // ─── DB Loading ──────────────────────────────────────────────────────────────
@@ -358,7 +373,7 @@ async function searchImagesCommons(
srlimit: "50", srlimit: "50",
sroffset: String(sroffset), sroffset: String(sroffset),
format: "json", format: "json",
origin: "*", // server-side API call // No origin needed — server-side fetch, Wikimedia ignores CORS headers on API calls
}); });
const url = `https://commons.wikimedia.org/w/api.php?${params}`; const url = `https://commons.wikimedia.org/w/api.php?${params}`;
@@ -472,9 +487,40 @@ async function downloadBatch(
function loadProgress(): Progress { function loadProgress(): Progress {
if (!existsSync(PROGRESS_FILE)) { if (!existsSync(PROGRESS_FILE)) {
return { lastUpdated: new Date().toISOString(), classes: {} }; return {
lastUpdated: new Date().toISOString(),
classes: {},
phase: 0,
phaseIndex: 0,
};
}
try {
const raw = JSON.parse(readFileSync(PROGRESS_FILE, "utf-8")) as Partial<Progress>;
// Backward compat: ensure new fields exist
raw.phase ??= 0;
raw.phaseIndex ??= 0;
raw.classes ??= {};
// Ensure each class has the sources field
for (const key of Object.keys(raw.classes)) {
const cp = raw.classes[key] as Partial<ClassProgress>;
cp.sources ??= {
db: { exhausted: false },
duckduckgo: { exhausted: false },
inaturalist: { exhausted: false },
wikimedia: { exhausted: false },
};
cp.seenUrls ??= [];
}
return raw as Progress;
} catch {
console.warn(" ⚠ Corrupt progress file, starting fresh");
return {
lastUpdated: new Date().toISOString(),
classes: {},
phase: 0,
phaseIndex: 0,
};
} }
return JSON.parse(readFileSync(PROGRESS_FILE, "utf-8")) as Progress;
} }
function saveProgress(progress: Progress): void { function saveProgress(progress: Progress): void {
@@ -490,6 +536,12 @@ function getClassProgress(progress: Progress, classId: string): ClassProgress {
failed: 0, failed: 0,
seenUrls: [], seenUrls: [],
exhausted: false, exhausted: false,
sources: {
db: { exhausted: false },
duckduckgo: { exhausted: false },
inaturalist: { exhausted: false },
wikimedia: { exhausted: false },
},
}; };
} }
return progress.classes[classId]; return progress.classes[classId];
@@ -514,6 +566,37 @@ function buildHealthyQueries(plant: string): string[] {
]; ];
} }
// ─── File Reconciliation ───────────────────────────────────────────────────
/**
* Count actual image files in a class directory.
* Returns the count of files matching img_* pattern, OR 0 if dir doesn't exist.
*/
function countImagesInDir(classDir: string): number {
if (!existsSync(classDir)) return 0;
try {
const files = readdirSync(classDir);
return files.filter((f) => f.startsWith("img_")).length;
} catch {
return 0;
}
}
/**
* Reconcile a class's progress count with actual files on disk.
* If files were deleted after the progress file was saved, this
* adjusts the count downward so we re-download the missing ones.
* Returns the reconciled count.
*/
function reconcileClassCount(classDir: string, progressCount: number): number {
const fileCount = countImagesInDir(classDir);
if (fileCount < progressCount) {
console.log(` ↻ File count (${fileCount}) < progress count (${progressCount}) — reconciling`);
return fileCount;
}
return progressCount;
}
// ─── Dataset Collection ───────────────────────────────────────────────────── // ─── Dataset Collection ─────────────────────────────────────────────────────
async function collectClassImages( async function collectClassImages(
@@ -526,14 +609,32 @@ async function collectClassImages(
fastMode = false, // Skip slow DuckDuckGo, use iNat + Commons only fastMode = false, // Skip slow DuckDuckGo, use iNat + Commons only
): Promise<void> { ): Promise<void> {
const cp = getClassProgress(progress, classId); const cp = getClassProgress(progress, classId);
// ── Reconcile with actual files on disk ─────────────────────────────────
const actualCount = reconcileClassCount(classDir, cp.count);
if (actualCount !== cp.count) {
cp.count = actualCount;
saveProgress(progress);
}
const seenUrls = new Set(cp.seenUrls); const seenUrls = new Set(cp.seenUrls);
const sources = cp.sources;
if (cp.count >= target) { if (cp.count >= target) {
console.log(` ✓ Already have ${cp.count}/${target}`); console.log(` ✓ Already have ${cp.count}/${target}`);
return; return;
} }
if (cp.exhausted) { // Check if ALL sources are exhausted
const allExhausted =
sources.db.exhausted &&
sources.duckduckgo.exhausted &&
sources.inaturalist.exhausted &&
sources.wikimedia.exhausted;
if (allExhausted) {
cp.exhausted = true;
saveProgress(progress);
console.log(` ✓ Exhausted (${cp.count}/${target})`); console.log(` ✓ Exhausted (${cp.count}/${target})`);
return; return;
} }
@@ -541,73 +642,111 @@ async function collectClassImages(
mkdirSync(classDir, { recursive: true }); mkdirSync(classDir, { recursive: true });
const allUrls: string[] = []; const allUrls: string[] = [];
let exhausted = false; let anyNewResults = false;
const needed = target - cp.count;
// ── Source 0: Existing DB URLs ────────────────────────────────────────── // ── Source 0: Existing DB URLs ──────────────────────────────────────────
const freshDbUrls = existingUrls.filter((u) => !seenUrls.has(u)); if (!sources.db.exhausted) {
if (freshDbUrls.length > 0) { const freshDbUrls = existingUrls.filter((u) => !seenUrls.has(u));
console.log(` DB: ${freshDbUrls.length} existing URLs`); if (freshDbUrls.length > 0) {
for (const url of freshDbUrls) { console.log(` DB: ${freshDbUrls.length} existing URLs`);
if (allUrls.length >= target) break; for (const url of freshDbUrls) {
seenUrls.add(url); if (allUrls.length >= needed) break;
allUrls.push(url); seenUrls.add(url);
allUrls.push(url);
}
if (freshDbUrls.length > 0) anyNewResults = true;
} }
// DB source is always "exhausted" after processing its initial URLs
sources.db.exhausted = true;
} }
// ── Source 1: DuckDuckGo ────────────────────────────────────────────── // ── Source 1: DuckDuckGo ──────────────────────────────────────────────
// Skip DDG in fast mode (full set — DDG is slowest source) if (!fastMode && !sources.duckduckgo.exhausted && allUrls.length < needed) {
if (!fastMode && allUrls.length < target) {
for (const query of queries) { for (const query of queries) {
if (allUrls.length >= target) break; if (allUrls.length >= needed) break;
process.stdout.write(` DDG: "${query.substring(0, 40)}"... `); process.stdout.write(` DDG: "${query.substring(0, 40)}"... `);
const result = await collectImagesDuckDuckGo(query, target - allUrls.length, seenUrls); const result = await collectImagesDuckDuckGo(query, needed - allUrls.length, seenUrls);
allUrls.push(...result.urls); allUrls.push(...result.urls);
if (result.exhausted) exhausted = true; if (result.exhausted) {
sources.duckduckgo.exhausted = true;
}
if (result.urls.length > 0) anyNewResults = true;
console.log(`${result.urls.length} new`); console.log(`${result.urls.length} new`);
if (allUrls.length >= target) break; if (allUrls.length >= needed) break;
}
// If DDG never gave us anything, mark exhausted to avoid re-trying
if (!anyNewResults && sources.duckduckgo.exhausted) {
/* already marked */
} }
} }
// ── Source 2: iNaturalist ────────────────────────────────────────────── // ── Source 2: iNaturalist ──────────────────────────────────────────────
if (allUrls.length < target) { if (!sources.inaturalist.exhausted && allUrls.length < needed) {
const primaryQuery = queries[0]; const primaryQuery = queries[0];
console.log(` iNat: Searching...`); console.log(` iNat: Searching...`);
const result = await searchImagesInaturalist(primaryQuery, target - allUrls.length, seenUrls); const result = await searchImagesInaturalist(primaryQuery, needed - allUrls.length, seenUrls);
allUrls.push(...result.urls); allUrls.push(...result.urls);
if (result.exhausted) exhausted = true; if (result.exhausted) sources.inaturalist.exhausted = true;
if (result.urls.length > 0) anyNewResults = true;
console.log(` iNat: ${result.urls.length} images`); console.log(` iNat: ${result.urls.length} images`);
} }
// ── Source 3: Wikimedia Commons ──────────────────────────────────────── // ── Source 3: Wikimedia Commons ────────────────────────────────────────
if (allUrls.length < target) { if (!sources.wikimedia.exhausted && allUrls.length < needed) {
const primaryQuery = queries[0]; const primaryQuery = queries[0];
console.log(` Commons: Searching...`); console.log(` Commons: Searching...`);
const result = await searchImagesCommons(primaryQuery, target - allUrls.length, seenUrls); const result = await searchImagesCommons(primaryQuery, needed - allUrls.length, seenUrls);
allUrls.push(...result.urls); allUrls.push(...result.urls);
if (result.exhausted) exhausted = true; if (result.exhausted) sources.wikimedia.exhausted = true;
if (result.urls.length > 0) anyNewResults = true;
console.log(` Commons: ${result.urls.length} images`); console.log(` Commons: ${result.urls.length} images`);
} }
if (allUrls.length === 0) { if (allUrls.length === 0) {
cp.exhausted = exhausted; cp.exhausted = true;
saveProgress(progress); saveProgress(progress);
console.log(` ✗ No images found`); console.log(` ✗ No images found — exhausted`);
return; return;
} }
if (!anyNewResults && allUrls.length > 0) {
// Only DB URLs survived — nothing more will come from searches
cp.exhausted = true;
saveProgress(progress);
}
// Save progress with seen URLs BEFORE downloading // Save progress with seen URLs BEFORE downloading
cp.seenUrls = Array.from(seenUrls); cp.seenUrls = Array.from(seenUrls);
cp.exhausted = exhausted;
saveProgress(progress); saveProgress(progress);
console.log(` Downloading ${allUrls.length} images...`); console.log(` Downloading ${allUrls.length} images...`);
const { downloaded, failed } = await downloadBatch(allUrls, classDir, cp.count); // Use actual file count as start index so filenames don't have gaps
const startIndex = countImagesInDir(classDir);
const { downloaded, failed } = await downloadBatch(allUrls, classDir, startIndex);
cp.count += downloaded; // Re-count actual files on disk after download (more reliable than tracking)
const newTotal = countImagesInDir(classDir);
cp.count = newTotal;
cp.downloaded += downloaded; cp.downloaded += downloaded;
cp.failed += failed; cp.failed += failed;
// Check if all sources exhausted
if (
sources.db.exhausted &&
sources.duckduckgo.exhausted &&
sources.inaturalist.exhausted &&
sources.wikimedia.exhausted
) {
cp.exhausted = true;
}
// Don't mark exhausted if we still have room to grow
if (cp.count >= target) {
cp.exhausted = true;
}
saveProgress(progress); saveProgress(progress);
const pct = Math.round((cp.count / target) * 100); const pct = Math.round((cp.count / target) * 100);
@@ -645,7 +784,12 @@ async function main() {
console.log("PHASE 1: Core Diseases (100 images each)"); console.log("PHASE 1: Core Diseases (100 images each)");
console.log("─".repeat(60)); console.log("─".repeat(60));
for (let i = 0; i < coreDiseases.length; i++) { const coreStart = progress.phase === 0 ? progress.phaseIndex : 0;
if (coreStart > 0) {
console.log(` Resuming from disease #${coreStart + 1} (${((coreStart / coreDiseases.length) * 100).toFixed(0)}% done)`);
}
for (let i = coreStart; i < coreDiseases.length; i++) {
const d = coreDiseases[i]; const d = coreDiseases[i];
const classDir = resolve(DATASET_DIR, d.id); const classDir = resolve(DATASET_DIR, d.id);
const queries = buildSearchQueries(d); const queries = buildSearchQueries(d);
@@ -655,6 +799,11 @@ async function main() {
console.log(`\n[${i + 1}/${coreDiseases.length}] (${pct}%) ${d.name || d.id} (${d.plantId})`); console.log(`\n[${i + 1}/${coreDiseases.length}] (${pct}%) ${d.name || d.id} (${d.plantId})`);
await collectClassImages(d.id, queries, TARGET_CORE, progress, classDir, existingUrls); await collectClassImages(d.id, queries, TARGET_CORE, progress, classDir, existingUrls);
// Save checkpoint: phase 0, at index i
progress.phase = 0;
progress.phaseIndex = i + 1;
saveProgress(progress);
} }
// ── Phase 2: Full set ────────────────────────────────────────────────── // ── Phase 2: Full set ──────────────────────────────────────────────────
@@ -663,7 +812,12 @@ async function main() {
console.log("PHASE 2: Full Disease Set (10 images each)"); console.log("PHASE 2: Full Disease Set (10 images each)");
console.log("─".repeat(60)); console.log("─".repeat(60));
for (let i = 0; i < fullDiseases.length; i++) { const fullStart = progress.phase === 1 ? progress.phaseIndex : 0;
if (fullStart > 0) {
console.log(` Resuming from disease #${fullStart + 1} (${((fullStart / fullDiseases.length) * 100).toFixed(0)}% done)`);
}
for (let i = fullStart; i < fullDiseases.length; i++) {
const d = fullDiseases[i]; const d = fullDiseases[i];
const classDir = resolve(DATASET_DIR, d.id); const classDir = resolve(DATASET_DIR, d.id);
const queries = buildSearchQueries(d); const queries = buildSearchQueries(d);
@@ -673,6 +827,11 @@ async function main() {
console.log(`\n[${i + 1}/${fullDiseases.length}] (${pct}%) ${d.id} (${d.plantId})`); console.log(`\n[${i + 1}/${fullDiseases.length}] (${pct}%) ${d.id} (${d.plantId})`);
await collectClassImages(d.id, queries, TARGET_FULL, progress, classDir, existingUrls, true); await collectClassImages(d.id, queries, TARGET_FULL, progress, classDir, existingUrls, true);
// Save checkpoint: phase 1, at index i
progress.phase = 1;
progress.phaseIndex = i + 1;
saveProgress(progress);
} }
// ── Phase 3: Healthy class ────────────────────────────────────────────── // ── Phase 3: Healthy class ──────────────────────────────────────────────
@@ -683,6 +842,14 @@ async function main() {
const healthyDir = resolve(DATASET_DIR, HEALTHY_CLASS); const healthyDir = resolve(DATASET_DIR, HEALTHY_CLASS);
const healthyCp = getClassProgress(progress, HEALTHY_CLASS); const healthyCp = getClassProgress(progress, HEALTHY_CLASS);
// Reconcile healthy class with files on disk
const healthyActualCount = reconcileClassCount(healthyDir, healthyCp.count);
if (healthyActualCount !== healthyCp.count) {
healthyCp.count = healthyActualCount;
saveProgress(progress);
}
const healthySeen = new Set(healthyCp.seenUrls); const healthySeen = new Set(healthyCp.seenUrls);
if (healthyCp.count >= TARGET_HEALTHY) { if (healthyCp.count >= TARGET_HEALTHY) {
@@ -730,16 +897,23 @@ async function main() {
saveProgress(progress); saveProgress(progress);
console.log(`\n Downloading ${totalHealthyUrls.length} healthy images...`); console.log(`\n Downloading ${totalHealthyUrls.length} healthy images...`);
const healthyStartIndex = countImagesInDir(healthyDir);
const { downloaded, failed } = await downloadBatch( const { downloaded, failed } = await downloadBatch(
totalHealthyUrls, totalHealthyUrls,
healthyDir, healthyDir,
healthyCp.count, healthyStartIndex,
); );
healthyCp.count += downloaded; // Re-count actual files on disk
const newHealthyTotal = countImagesInDir(healthyDir);
healthyCp.count = newHealthyTotal;
healthyCp.downloaded += downloaded; healthyCp.downloaded += downloaded;
healthyCp.failed += failed; healthyCp.failed += failed;
if (healthyCp.count >= TARGET_HEALTHY) {
healthyCp.exhausted = true;
}
const pct = Math.round((healthyCp.count / TARGET_HEALTHY) * 100); const pct = Math.round((healthyCp.count / TARGET_HEALTHY) * 100);
console.log( console.log(
` Got ${downloaded} images. Total: ${healthyCp.count}/${TARGET_HEALTHY} (${pct}%)`, ` Got ${downloaded} images. Total: ${healthyCp.count}/${TARGET_HEALTHY} (${pct}%)`,
@@ -753,6 +927,11 @@ async function main() {
// ── Summary ──────────────────────────────────────────────────────────────── // ── Summary ────────────────────────────────────────────────────────────────
// Mark all phases complete
progress.phase = 3;
progress.phaseIndex = 0;
saveProgress(progress);
const elapsed = Math.round((Date.now() - startTime) / 1000); const elapsed = Math.round((Date.now() - startTime) / 1000);
const mins = Math.floor(elapsed / 60); const mins = Math.floor(elapsed / 60);
const hrs = Math.floor(mins / 60); const hrs = Math.floor(mins / 60);
@@ -765,7 +944,7 @@ async function main() {
} }
console.log("\n" + "=".repeat(60)); console.log("\n" + "=".repeat(60));
console.log("COMPLETE"); console.log(" ✅ ALL PHASES COMPLETE");
console.log("=".repeat(60)); console.log("=".repeat(60));
console.log(` Time: ${hrs}h ${mins % 60}m`); console.log(` Time: ${hrs}h ${mins % 60}m`);
console.log(` Downloaded: ${totalDownloaded} images`); console.log(` Downloaded: ${totalDownloaded} images`);

View File

@@ -10,6 +10,7 @@ import { v4 as uuidv4 } from "uuid";
const VALID_CONTENT_TYPES = [ const VALID_CONTENT_TYPES = [
"plant_image", "plant_image",
"disease_image", "disease_image",
"disease_description",
"disease_symptoms", "disease_symptoms",
"disease_causes", "disease_causes",
"disease_treatment", "disease_treatment",

View File

@@ -140,9 +140,19 @@ function DiseaseCard({
/> />
</div> </div>
<p className="text-sm text-zinc-600 dark:text-zinc-300 leading-relaxed mb-4"> <div className="flex items-start justify-between gap-4 mb-4">
{disease.description} <p className="text-sm text-zinc-600 dark:text-zinc-300 leading-relaxed">
</p> {disease.description}
</p>
<FlagButton
contentType="disease_description"
contentId={disease.id}
fieldName="description"
label="description"
small
className="shrink-0 mt-0.5"
/>
</div>
{/* Details grid */} {/* Details grid */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">

View File

@@ -4,6 +4,7 @@ import { notFound } from "next/navigation";
import type { Metadata } from "next"; import type { Metadata } from "next";
import { getPlantWithDiseases } from "@/lib/api/diseases-db"; import { getPlantWithDiseases } from "@/lib/api/diseases-db";
import { getPlantDescription } from "@/lib/display-helpers"; import { getPlantDescription } from "@/lib/display-helpers";
import BetaNotice from "@/components/BetaNotice";
import DiseaseCards from "./DiseaseCards"; import DiseaseCards from "./DiseaseCards";
import PlantViewTracker from "@/components/PlantViewTracker"; import PlantViewTracker from "@/components/PlantViewTracker";
import FlagPlantImage from "@/components/FlagPlantImage"; import FlagPlantImage from "@/components/FlagPlantImage";
@@ -83,6 +84,8 @@ export default async function PlantDetailPage({ params }: Props) {
</ol> </ol>
</nav> </nav>
<BetaNotice variant="card" className="mb-6" />
{/* Plant hero */} {/* Plant hero */}
<div className="flex flex-col sm:flex-row sm:items-start gap-6 mb-10"> <div className="flex flex-col sm:flex-row sm:items-start gap-6 mb-10">
{/* Plant image */} {/* Plant image */}

View File

@@ -1,7 +1,8 @@
import React, { Suspense } from "react"; import { Suspense } from "react";
import { getBrowsePlants } from "@/lib/api/browse"; import { getBrowsePlants } from "@/lib/api/browse";
import BrowseContent from "./BrowseContent"; import BrowseContent from "./BrowseContent";
import { PlantCardSkeleton } from "@/components/LoadingSkeleton"; import { PlantCardSkeleton } from "@/components/LoadingSkeleton";
import BetaNotice from "@/components/BetaNotice";
/** /**
* Browse page — fetches plants with disease counts from the database * Browse page — fetches plants with disease counts from the database
@@ -12,27 +13,30 @@ export default async function BrowsePage() {
const allPlants = await getBrowsePlants(); const allPlants = await getBrowsePlants();
return ( return (
<Suspense <>
fallback={ <BetaNotice variant="full-width" />
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-8 sm:py-12"> <Suspense
<div className="mb-8"> fallback={
<div className="h-9 w-48 animate-pulse rounded bg-zinc-200 dark:bg-zinc-700" /> <div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-8 sm:py-12">
<div className="mt-2 h-5 w-72 animate-pulse rounded bg-zinc-200 dark:bg-zinc-700" /> <div className="mb-8">
<div className="h-9 w-48 animate-pulse rounded bg-zinc-200 dark:bg-zinc-700" />
<div className="mt-2 h-5 w-72 animate-pulse rounded bg-zinc-200 dark:bg-zinc-700" />
</div>
<div className="mb-6 h-12 w-full animate-pulse rounded-xl bg-zinc-200 dark:bg-zinc-700" />
<div className="flex gap-2 mb-8">
{Array.from({ length: 5 }, (_, i) => (
<div
key={i}
className="h-9 w-24 animate-pulse rounded-full bg-zinc-200 dark:bg-zinc-700"
/>
))}
</div>
<PlantCardSkeleton count={8} />
</div> </div>
<div className="mb-6 h-12 w-full animate-pulse rounded-xl bg-zinc-200 dark:bg-zinc-700" /> }
<div className="flex gap-2 mb-8"> >
{Array.from({ length: 5 }, (_, i) => ( <BrowseContent allPlants={allPlants} />
<div </Suspense>
key={i} </>
className="h-9 w-24 animate-pulse rounded-full bg-zinc-200 dark:bg-zinc-700"
/>
))}
</div>
<PlantCardSkeleton count={8} />
</div>
}
>
<BrowseContent allPlants={allPlants} />
</Suspense>
); );
} }

View File

@@ -0,0 +1,44 @@
/**
* BetaNotice — a banner informing users that the site is in beta,
* community-driven, and most data isn't reviewed by humans yet.
* Encourages use of the Flag button to flag content for review.
*
* Two layout variants:
* - "full-width" (default): stretches edge-to-edge with an inner max-w wrapper
* - "card": rounded card with border, suitable for inside content containers
*/
export default function BetaNotice({
variant = "full-width",
className = "",
}: {
variant?: "full-width" | "card";
className?: string;
}) {
const containerClasses =
variant === "card"
? `rounded-xl bg-warning-amber-50 dark:bg-warning-amber-950/60 border border-warning-amber-200 dark:border-warning-amber-800 ${className}`
: `bg-warning-amber-50 dark:bg-warning-amber-950/60 border-b border-warning-amber-200 dark:border-warning-amber-800 ${className}`;
return (
<div className={containerClasses}>
<div
className={
variant === "card" ? "px-4 sm:px-6 py-3" : "mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-3"
}
>
<p className="text-xs sm:text-sm text-warning-amber-800 dark:text-warning-amber-200 text-center leading-relaxed">
<span className="font-semibold">🚧 Beta Community Driven.</span> Most data here is not
reviewed by humans. Spot something wrong or it could be better? Use the{" "}
<span className="inline-flex items-center gap-1 font-medium whitespace-nowrap">
<svg className="h-3.5 w-3.5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path d="M3.5 2.75a.75.75 0 00-1.5 0v14.5a.75.75 0 001.5 0v-4.392l1.657-.348a6.453 6.453 0 014.271.572 7.948 7.948 0 005.965.524l2.078-.64A.75.75 0 0018 12.25v-8.5a.75.75 0 00-.904-.734l-2.38.501a7.25 7.25 0 01-4.186-.363l-.502-.2a8.75 8.75 0 00-5.053-.439l-1.475.31V2.75z" />
</svg>
Flag
</span>{" "}
button on any image or description to flag it for review.
</p>
</div>
</div>
);
}

View File

@@ -132,9 +132,18 @@ export default function DiseaseCard({
{/* Full description */} {/* Full description */}
<div> <div>
<h4 className="text-sm font-semibold text-zinc-900 dark:text-zinc-100 mb-1"> <div className="flex items-center justify-between mb-1">
Description <h4 className="text-sm font-semibold text-zinc-900 dark:text-zinc-100">
</h4> Description
</h4>
<FlagButton
contentType="disease_description"
contentId={disease.id}
fieldName="description"
label="description"
small
/>
</div>
<p className="text-sm leading-relaxed text-zinc-600 dark:text-zinc-400"> <p className="text-sm leading-relaxed text-zinc-600 dark:text-zinc-400">
{disease.description} {disease.description}
</p> </p>

View File

@@ -8,6 +8,7 @@ import { useState, useCallback } from "react";
export type FlagContentType = export type FlagContentType =
| "plant_image" | "plant_image"
| "disease_image" | "disease_image"
| "disease_description"
| "disease_symptoms" | "disease_symptoms"
| "disease_causes" | "disease_causes"
| "disease_treatment" | "disease_treatment"

View File

@@ -1,3 +1,5 @@
"use client";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import FlagButton from "@/components/FlagButton"; import FlagButton from "@/components/FlagButton";

View File

@@ -130,6 +130,7 @@ export const flaggedContent = sqliteTable(
enum: [ enum: [
"plant_image", "plant_image",
"disease_image", "disease_image",
"disease_description",
"disease_symptoms", "disease_symptoms",
"disease_causes", "disease_causes",
"disease_treatment", "disease_treatment",