Yuki / services /geminiService.ts
Severian's picture
Upload 43 files
be02369 verified
import { GoogleGenAI, Type, Part } from "@google/genai";
import type { BonsaiAnalysis, PestLibraryEntry, SeasonalGuide, ToolRecommendation, MaintenanceTips, DiaryAIAnalysis, HealthCheckResult, SpeciesIdentificationResult, StylingBlueprint, SoilAnalysis, ProtectionProfile } from '../types';
let ai: GoogleGenAI | null = null;
// Function to initialize or re-initialize the AI client
export function reinitializeAI() {
const apiKey = window.localStorage.getItem('gemini-api-key');
if (apiKey) {
try {
ai = new GoogleGenAI({ apiKey });
} catch (e) {
console.error("Failed to initialize GoogleGenAI, likely due to an invalid API key format.", e);
ai = null;
}
} else {
ai = null;
}
}
// Function to check if the AI is configured
export function isAIConfigured(): boolean {
return ai !== null;
}
// Initialize on load
reinitializeAI();
const protectionProfileSchema = {
type: Type.OBJECT,
properties: {
minTempC: { type: Type.INTEGER, description: "The minimum safe temperature in Celsius the tree can tolerate without protection." },
maxTempC: { type: Type.INTEGER, description: "The maximum safe temperature in Celsius before the tree might suffer from heat stress." },
maxWindKph: { type: Type.INTEGER, description: "The maximum sustained wind speed in km/h the tree can handle before risking damage or dehydration." },
},
required: ["minTempC", "maxTempC", "maxWindKph"]
};
const pestLibrarySchema = {
type: Type.ARRAY,
description: "A library of common pests and diseases for this species and region.",
items: {
type: Type.OBJECT,
properties: {
name: { type: Type.STRING },
type: { type: Type.STRING, enum: ['Pest', 'Disease'] },
description: { type: Type.STRING },
symptoms: { type: Type.ARRAY, items: { type: Type.STRING }},
treatment: {
type: Type.OBJECT,
properties: {
organic: { type: Type.STRING },
chemical: { type: Type.STRING }
},
required: ["organic", "chemical"]
}
},
required: ["name", "type", "description", "symptoms", "treatment"]
}
};
const seasonalGuideSchema = {
type: Type.ARRAY,
description: "A high-level guide for all four seasons.",
items: {
type: Type.OBJECT,
properties: {
season: { type: Type.STRING, enum: ['Spring', 'Summer', 'Autumn', 'Winter'] },
summary: { type: Type.STRING },
tasks: {
type: Type.ARRAY,
items: {
type: Type.OBJECT,
properties: {
task: { type: Type.STRING },
importance: { type: Type.STRING, enum: ['High', 'Medium', 'Low'] }
},
required: ["task", "importance"]
}
}
},
required: ["season", "summary", "tasks"]
}
};
const toolRecommendationsSchema = {
type: Type.ARRAY,
description: "A list of recommended tools and supplies.",
items: {
type: Type.OBJECT,
properties: {
name: { type: Type.STRING },
category: { type: Type.STRING, enum: ['Cutting', 'Wiring', 'Repotting', 'General Care'] },
description: { type: Type.STRING },
level: { type: Type.STRING, enum: ['Essential', 'Recommended', 'Advanced'] }
},
required: ["name", "category", "description", "level"]
}
};
const maintenanceTipsSchema = {
type: Type.OBJECT,
properties: {
sharpening: { type: Type.STRING, description: "A concise guide on how to sharpen this specific tool. Mention needed supplies like whetstones or files." },
cleaning: { type: Type.STRING, description: "Instructions for cleaning the tool after use to prevent disease and rust. Mention things like sap remover." },
storage: { type: Type.STRING, description: "Best practices for storing the tool to ensure its longevity, like oiling and proper placement." }
},
required: ["sharpening", "cleaning", "storage"]
};
const diaryAIAnalysisSchema = {
type: Type.OBJECT,
properties: {
summary: { type: Type.STRING, description: "A concise summary of observed changes since the last log entry. Note new growth, color changes, or any potential issues." },
healthChange: { type: Type.INTEGER, description: "An estimated integer value of the health change since the last photo. Can be positive, negative, or zero." },
suggestions: { type: Type.ARRAY, items: { type: Type.STRING }, description: "1-2 actionable suggestions based on the observations." },
},
required: ["summary"]
};
const healthCheckResultSchema = {
type: Type.OBJECT,
properties: {
probableCause: { type: Type.STRING, description: "The most likely specific cause of the issue (e.g., 'Spider Mite Infestation', 'Root Rot due to Overwatering', 'Iron Chlorosis')." },
confidence: { type: Type.STRING, enum: ['High', 'Medium', 'Low'], description: "The AI's confidence level in this diagnosis." },
explanation: { type: Type.STRING, description: "A detailed explanation of why this diagnosis was reached, referencing visual cues from the image and the user's problem description." },
isPest: { type: Type.BOOLEAN, description: "True if the primary cause is a pest." },
isDisease: { type: Type.BOOLEAN, description: "True if the primary cause is a fungal, bacterial, or viral disease." },
treatmentPlan: {
type: Type.ARRAY,
description: "A step-by-step, actionable treatment plan.",
items: {
type: Type.OBJECT,
properties: {
step: { type: Type.INTEGER },
action: { type: Type.STRING, description: "A short, clear title for the step (e.g., 'Isolate the Tree', 'Apply Neem Oil')." },
details: { type: Type.STRING, description: "Detailed instructions on how to perform the action." }
},
required: ["step", "action", "details"]
}
},
organicAlternatives: { type: Type.STRING, description: "A summary of organic or non-chemical treatment options available." },
preventativeMeasures: { type: Type.STRING, description: "Advice on how to prevent this issue from recurring in the future." }
},
required: ["probableCause", "confidence", "explanation", "isPest", "isDisease", "treatmentPlan", "organicAlternatives", "preventativeMeasures"]
};
const speciesIdentificationSchema = {
type: Type.OBJECT,
properties: {
identifications: {
type: Type.ARRAY,
description: "An array of possible species identifications, ordered from most to least likely.",
items: {
type: Type.OBJECT,
properties: {
commonName: { type: Type.STRING },
scientificName: { type: Type.STRING },
confidence: { type: Type.STRING, enum: ['High', 'Medium', 'Low'] },
reasoning: { type: Type.STRING, description: "Why the AI made this identification based on visual cues." },
generalCareSummary: { type: Type.STRING, description: "A very brief, one-paragraph summary of the most critical care aspects for this species (light, water, soil)." }
},
required: ["commonName", "scientificName", "confidence", "reasoning", "generalCareSummary"]
}
}
},
required: ["identifications"]
};
const soilAnalysisSchema = {
type: Type.OBJECT,
properties: {
components: {
type: Type.ARRAY,
description: "The identified components of the soil mix and their estimated percentages.",
items: {
type: Type.OBJECT,
properties: {
name: { type: Type.STRING, enum: ['Akadama', 'Pumice', 'Lava Rock', 'Organic Compost', 'Kiryu', 'Pine Bark', 'Diatomaceous Earth', 'Sand', 'Grit', 'Other'] },
percentage: { type: Type.INTEGER },
},
required: ["name", "percentage"]
}
},
drainageRating: { type: Type.STRING, enum: ['Poor', 'Average', 'Good', 'Excellent'] },
waterRetention: { type: Type.STRING, enum: ['Low', 'Medium', 'High'] },
suitabilityAnalysis: { type: Type.STRING, description: "Analysis of the soil's suitability for the specified species and location." },
improvementSuggestions: { type: Type.STRING, description: "Actionable suggestions to improve the soil mix." }
},
required: ["components", "drainageRating", "waterRetention", "suitabilityAnalysis", "improvementSuggestions"]
};
const stylingBlueprintSchema = {
type: Type.OBJECT,
properties: {
summary: { type: Type.STRING, description: "A concise summary of the overall styling strategy." },
canvas: {
type: Type.OBJECT,
properties: {
width: { type: Type.INTEGER, description: "The width of the canvas the coordinates are based on. Always use 1000." },
height: { type: Type.INTEGER, description: "The height of the canvas the coordinates are based on. Always use 1000." },
},
required: ["width", "height"]
},
annotations: {
type: Type.ARRAY,
items: {
type: Type.OBJECT,
properties: {
type: { type: Type.STRING, enum: ['PRUNE_LINE', 'WIRE_DIRECTION', 'REMOVE_BRANCH', 'FOLIAGE_REFINEMENT', 'JIN_SHARI', 'TRUNK_LINE', 'EXPOSE_ROOT'] },
points: {
type: Type.ARRAY,
description: "An array of {x, y} coordinates for drawing lines or polygons. Required for most types except those using 'path'.",
items: {
type: Type.OBJECT,
properties: {
x: { type: Type.INTEGER },
y: { type: Type.INTEGER }
},
required: ["x", "y"]
}
},
path: { type: Type.STRING, description: "An SVG path data string (e.g., 'M 10 10 Q 20 20 30 10'). Use for complex curves like wiring directions or trunk lines." },
label: { type: Type.STRING, description: "A concise, user-facing label explaining the annotation (e.g., 'Prune here to shorten branch')." }
},
required: ["type", "label"]
}
}
},
required: ["summary", "canvas", "annotations"]
};
const bonsaiAnalysisSchema = {
type: Type.OBJECT,
properties: {
species: { type: Type.STRING, description: "The scientific or common name of the bonsai species identified."},
healthAssessment: {
type: Type.OBJECT,
description: "A detailed assessment of the bonsai's health.",
properties: {
overallHealth: { type: Type.STRING, description: "A summary of the tree's health (e.g., 'Excellent', 'Needs Attention', 'Stressed')." },
healthScore: { type: Type.INTEGER, description: "A numerical health score from 1 to 100, where 100 is perfect health." },
observations: { type: Type.ARRAY, items: { type: Type.STRING }, description: "Specific visual observations from the image." },
foliageHealth: { type: Type.STRING, description: "Analysis of the leaves/needles (color, density, size)." },
trunkAndNebariHealth: { type: Type.STRING, description: "Analysis of the trunk, bark, and surface roots (nebari)." },
potAndSoilHealth: { type: Type.STRING, description: "Analysis of the pot condition and visible soil surface." },
},
required: ["overallHealth", "healthScore", "observations", "foliageHealth", "trunkAndNebariHealth", "potAndSoilHealth"]
},
careSchedule: {
type: Type.ARRAY,
description: "A 4-week care schedule adjusted for species and climate.",
items: {
type: Type.OBJECT,
properties: {
week: { type: Type.INTEGER, description: "The week number (1-4)." },
task: { type: Type.STRING, description: "The primary task for the week (e.g., 'Watering', 'Fertilizing', 'Pruning')." },
details: { type: Type.STRING, description: "Specific instructions for the task." },
toolsNeeded: { type: Type.ARRAY, items: { type: Type.STRING }, description: "A list of tools needed for the task." },
},
required: ["week", "task", "details"]
}
},
pestAndDiseaseAlerts: {
type: Type.ARRAY,
description: "Potential pests or diseases to watch for, with severity assessment.",
items: {
type: Type.OBJECT,
properties: {
pestOrDisease: { type: Type.STRING },
symptoms: { type: Type.STRING },
treatment: { type: Type.STRING },
severity: { type: Type.STRING, enum: ["Low", "Medium", "High"] }
},
required: ["pestOrDisease", "symptoms", "treatment", "severity"]
}
},
stylingSuggestions: {
type: Type.ARRAY,
description: "Actionable advice for styling the bonsai.",
items: {
type: Type.OBJECT,
properties: {
technique: { type: Type.STRING, enum: ['Pruning', 'Wiring', 'Shaping'] },
description: { type: Type.STRING, description: "Detailed 'how-to' for the technique." },
area: { type: Type.STRING, description: "The part of the tree to apply the technique to." },
},
required: ["technique", "description", "area"]
}
},
environmentalFactors: {
type: Type.OBJECT,
description: "Ideal environmental conditions for the species.",
properties: {
idealLight: { type: Type.STRING, description: "e.g., '6-8 hours of direct morning sun, afternoon shade'" },
idealHumidity: { type: Type.STRING, description: "e.g., 'Prefers 50-70% humidity, mist daily if lower'" },
temperatureRange: { type: Type.STRING, description: "e.g., 'Tolerates 50-85°F (10-30°C)'" },
},
required: ["idealLight", "idealHumidity", "temperatureRange"]
},
wateringAnalysis: {
type: Type.OBJECT,
description: "Specific watering advice.",
properties: {
frequency: { type: Type.STRING, description: "How often to water, e.g., 'Every 1-3 days, check soil first'" },
method: { type: Type.STRING, description: "Recommended watering method, e.g., 'Immersion or top-watering until drains'" },
notes: { type: Type.STRING, description: "Additional notes, e.g., 'Allow soil to become slightly dry between waterings. Use filtered water if possible.'" }
},
required: ["frequency", "method", "notes"]
},
knowledgeNuggets: {
type: Type.ARRAY,
description: "Three interesting, little-known facts about this specific bonsai species.",
items: { type: Type.STRING }
},
estimatedAge: {
type: Type.STRING,
description: "An estimated age range of the bonsai based on its trunk, nebari, and branch structure."
},
fertilizerRecommendations: {
type: Type.ARRAY,
description: "A seasonal fertilization plan.",
items: {
type: Type.OBJECT,
properties: {
phase: { type: Type.STRING, enum: ['Spring Growth', 'Summer Maintenance', 'Autumn Preparation', 'Winter Dormancy'] },
type: { type: Type.STRING, description: "e.g., 'Balanced (e.g., 10-10-10)'" },
frequency: { type: Type.STRING },
notes: { type: Type.STRING }
},
required: ["phase", "type", "frequency", "notes"]
}
},
soilRecipe: {
type: Type.OBJECT,
description: "A precise soil mixture recipe.",
properties: {
components: {
type: Type.ARRAY,
items: {
type: Type.OBJECT,
properties: {
name: { type: Type.STRING, enum: ['Akadama', 'Pumice', 'Lava Rock', 'Organic Compost', 'Kiryu', 'Other'] },
percentage: { type: Type.INTEGER },
notes: { type: Type.STRING }
},
required: ["name", "percentage", "notes"]
}
},
rationale: { type: Type.STRING, description: "Justification for this specific soil mix." }
},
required: ["components", "rationale"]
},
potSuggestion: {
type: Type.OBJECT,
description: "Recommendation for an appropriate pot.",
properties: {
style: { type: Type.STRING, description: "e.g., 'Unglazed Rectangular'" },
size: { type: Type.STRING, description: "Recommended size relative to the tree." },
colorPalette: { type: Type.STRING },
rationale: { type: Type.STRING, description: "Aesthetic and horticultural reasoning." }
},
required: ["style", "size", "colorPalette", "rationale"]
},
seasonalGuide: seasonalGuideSchema,
diagnostics: {
type: Type.ARRAY,
description: "Advanced diagnostics for potential underlying issues.",
items: {
type: Type.OBJECT,
properties: {
issue: { type: Type.STRING },
confidence: { type: Type.STRING, enum: ['High', 'Medium', 'Low'] },
symptoms: { type: Type.STRING, description: "Symptoms to watch for."},
solution: { type: Type.STRING, description: "Preventative or corrective solutions."}
},
required: ["issue", "confidence", "symptoms", "solution"]
}
},
pestLibrary: pestLibrarySchema,
toolRecommendations: toolRecommendationsSchema
},
required: [
"species", "healthAssessment", "careSchedule", "pestAndDiseaseAlerts",
"stylingSuggestions", "environmentalFactors", "wateringAnalysis", "knowledgeNuggets",
"estimatedAge", "fertilizerRecommendations", "soilRecipe", "potSuggestion",
"seasonalGuide", "diagnostics", "pestLibrary", "toolRecommendations"
]
};
async function generate(prompt: string, schema: any) {
if (!isAIConfigured()) {
throw new Error("AI is not configured. Please set your API key in Settings.");
}
const response = await ai!.models.generateContent({
model: 'gemini-2.5-flash',
contents: { parts: [{ text: prompt }] },
config: {
responseMimeType: "application/json",
responseSchema: schema
}
});
const jsonText = response.text.trim();
if (!jsonText) {
console.error("Gemini API returned an empty response.");
return null;
}
return JSON.parse(jsonText);
}
function handleAIError(error: any): null {
console.error("Error communicating with AI:", error);
if (error instanceof Error && (error.message.includes('API key not valid') || error.message.includes('permission to access') || error.message.includes('400'))) {
throw new Error("Your Gemini API key is not valid or has incorrect permissions. Please check it in Settings.");
}
return null;
}
export const analyzeBonsai = async (
imageBase64: string,
species: string,
location: string
): Promise<BonsaiAnalysis | null> => {
if (!isAIConfigured()) {
throw new Error("AI is not configured. Please set your API key in Settings.");
}
try {
const prompt = `You are 'Yuki', a world-renowned bonsai master with 50 years of experience. Analyze the provided image of a bonsai, which the user says is a ${species}, located in/near ${location}. Your analysis MUST be comprehensive, scientifically accurate, and deeply insightful, tailored to both the species and the local climate.
Based on the visual evidence, regional climate data for ${location}, and your expert knowledge of ${species} bonsai, provide a complete and structured analysis covering all the following points:
1. **Confirm Species:** First, confirm if the image looks like a ${species}. If not, identify the correct species. Populate the 'species' field with the correct name.
2. **Health Assessment:** Conduct a thorough health diagnosis. Provide a numerical score (1-100). Detail the health of foliage, trunk/nebari, and pot/soil.
3. **Care Schedule:** Create a highly specific 4-week care plan. Include tasks, detailed instructions, and a list of tools for each week.
4. **Pest/Disease Alerts:** Identify any *active* pests or diseases on the tree. If none, list preventative measures.
5. **Styling Advice:** Provide at least two actionable styling suggestions (pruning, wiring, shaping) suitable for the tree's current state and species characteristics.
6. **Environment:** Specify the ideal light, humidity, and temperature for this species.
7. **Watering:** Give a detailed watering analysis - frequency, method, and important notes.
8. **Age Estimation:** Estimate the tree's age range based on trunk thickness, bark texture, and nebari.
9. **Knowledge Nuggets:** Provide three fascinating, little-known facts about this species. These should be 'insider' knowledge that a true enthusiast would appreciate.
10. **Fertilizer Plan:** Based on the species, health, and time of year, provide a seasonal fertilizer plan. Specify N-P-K ratios or fertilizer type (e.g., solid organic, liquid chemical), and frequency for each season.
11. **Soil Recipe:** Recommend a precise soil mixture recipe in percentages (e.g., 40% Akadama, 30% Pumice, 20% Lava Rock, 10% Compost). Justify why this mix is ideal for this species' drainage and water retention needs.
12. **Pot Selection:** Suggest an appropriate pot. Consider style (e.g., formal upright, cascade), material (e.g., unglazed ceramic), color, and size relative to the tree. Explain the aesthetic and horticultural reasoning.
13. **Full-Year Seasonal Guide:** Provide a high-level guide for all four seasons (Spring, Summer, Autumn, Winter) for this tree in ${location}. For each season, summarize the main goals and list key tasks with their importance level (High, Medium, Low).
14. **Advanced Diagnostics:** Based on subtle cues in the image and common issues for this species/climate, identify 2-3 potential underlying problems (e.g., root rot, nutrient deficiency, salt buildup) even if not in a critical state yet. Provide confidence level, symptoms to watch for, and preventative solutions.
15. **Pest & Disease Library:** Generate a mini-library of the 3 most common pests and diseases for this species in ${location}. This is for general knowledge. For each, provide a description, symptoms, and both organic and chemical treatment options.
16. **Tool & Supply Recommendations:** Provide a list of essential and advanced tools and supplies for this tree. Categorize them (e.g., Cutting, Wiring, Repotting, General Care) and explain why each is needed. Mark each as 'Essential', 'Recommended', or 'Advanced'.`;
const imagePart = {
inlineData: {
mimeType: 'image/jpeg',
data: imageBase64,
},
};
const textPart = { text: prompt };
const response = await ai!.models.generateContent({
model: 'gemini-2.5-flash',
contents: { parts: [imagePart, textPart] },
config: {
responseMimeType: "application/json",
responseSchema: bonsaiAnalysisSchema
}
});
const jsonText = response.text.trim();
if (!jsonText) {
console.error("Gemini API returned an empty response.");
return null;
}
return JSON.parse(jsonText) as BonsaiAnalysis;
} catch (error) {
return handleAIError(error);
}
};
export const analyzeFollowUp = async (
imageBase64: string,
previousAnalysis: BonsaiAnalysis,
species: string,
location: string
): Promise<BonsaiAnalysis | null> => {
if (!isAIConfigured()) {
throw new Error("AI is not configured. Please set your API key in Settings.");
}
try {
const prompt = `You are 'Yuki', a world-renowned bonsai master providing a follow-up consultation for a ${species} in ${location}.
Attached is a **new photo** of the bonsai.
Below is the **previous analysis JSON data** for context.
Your task is to:
1. Perform a full, new analysis based on the **new photo**, using the same comprehensive JSON schema as before.
2. **Crucially, your analysis should reflect changes from the previous state.** In your text fields (like observations, health assessments), explicitly compare and contrast. For example: "Foliage health has improved, appearing much fuller than in the last analysis," or "A new area of concern is the slight yellowing on the left lower branch, which was not present previously."
3. Update the health score and all other fields based on the new visual information. The goal is to create a new, complete report that also serves as a progress update.
Previous Analysis for context:
${JSON.stringify(previousAnalysis, null, 2)}
`;
const imagePart = {
inlineData: {
mimeType: 'image/jpeg',
data: imageBase64,
},
};
const textPart = { text: prompt };
const response = await ai!.models.generateContent({
model: 'gemini-2.5-flash',
contents: { parts: [imagePart, textPart] },
config: {
responseMimeType: "application/json",
responseSchema: bonsaiAnalysisSchema
}
});
const jsonText = response.text.trim();
if (!jsonText) {
console.error("Gemini API returned an empty response for follow-up.");
return null;
}
return JSON.parse(jsonText) as BonsaiAnalysis;
} catch (error) {
return handleAIError(error);
}
};
export const runHealthCheck = async (
imageBase64: string,
species: string,
location: string,
problemCategory: string
): Promise<HealthCheckResult | null> => {
if (!isAIConfigured()) {
throw new Error("AI is not configured. Please set your API key in Settings.");
}
try {
const prompt = `You are 'Yuki', a bonsai diagnostician and plant pathologist. A user needs an urgent health check-up for their bonsai.
- **Species:** ${species}
- **Location:** ${location}
- **Reported Problem Area:** ${problemCategory}
Analyze the provided close-up image showing the problem. Your task is to provide a precise diagnosis and an easy-to-follow treatment plan. Focus ONLY on the visible problem. Be methodical and scientific.
1. **Diagnose:** Identify the specific cause (e.g., 'Spider Mite Infestation', 'Root Rot due to Overwatering', 'Iron Chlorosis'). State your confidence level.
2. **Explain:** Describe *why* you made this diagnosis, citing evidence from the image.
3. **Treat:** Create a numbered, step-by-step treatment plan. The steps must be clear and actionable for an amateur.
4. **Offer Alternatives:** Briefly describe common organic alternatives.
5. **Prevent:** Give advice on preventing recurrence.
6. **Categorize:** Set the 'isPest' and 'isDisease' flags appropriately.
Generate the response strictly following the provided JSON schema.`;
const imagePart = { inlineData: { mimeType: 'image/jpeg', data: imageBase64 }};
const textPart = { text: prompt };
const response = await ai!.models.generateContent({
model: 'gemini-2.5-flash',
contents: { parts: [imagePart, textPart] },
config: {
responseMimeType: "application/json",
responseSchema: healthCheckResultSchema
}
});
const jsonText = response.text.trim();
if (!jsonText) {
console.error("Gemini API returned an empty response for health check.");
return null;
}
return JSON.parse(jsonText) as HealthCheckResult;
} catch (error) {
return handleAIError(error);
}
};
export const identifyBonsaiSpecies = async (imageBase64: string): Promise<SpeciesIdentificationResult | null> => {
if (!isAIConfigured()) {
throw new Error("AI is not configured. Please set your API key in Settings.");
}
try {
const prompt = `You are a world-class botanist specializing in bonsai identification. Analyze the provided image of a tree and identify its species. Provide the most likely common name and the scientific name. If you are uncertain, provide a few possibilities with your confidence level for each. For each identification, provide a brief summary of its essential care needs.`;
const imagePart = { inlineData: { mimeType: 'image/jpeg', data: imageBase64 }};
const textPart = { text: prompt };
const response = await ai!.models.generateContent({
model: 'gemini-2.5-flash',
contents: { parts: [imagePart, textPart] },
config: {
responseMimeType: "application/json",
responseSchema: speciesIdentificationSchema
}
});
const jsonText = response.text.trim();
if (!jsonText) {
console.error("Gemini API returned an empty response for species ID.");
return null;
}
return JSON.parse(jsonText) as SpeciesIdentificationResult;
} catch (error) {
return handleAIError(error);
}
};
export const generatePestLibrary = async (location: string): Promise<PestLibraryEntry[] | null> => {
try {
const prompt = `You are 'Yuki', a bonsai master and entomologist. Generate a Pest & Disease Library for bonsai cultivators located in/near ${location}. Provide a list of the 5 most common pests and diseases they are likely to encounter. For each entry, provide its name, type (Pest or Disease), a brief description, common symptoms to look for on a bonsai tree, and recommended organic and chemical treatment options.`;
const result = await generate(prompt, { type: Type.OBJECT, properties: { result: pestLibrarySchema } });
return result ? result.result : null;
} catch(error) {
return handleAIError(error);
}
}
export const generateSeasonalGuide = async (species: string, location: string): Promise<SeasonalGuide[] | null> => {
try {
const prompt = `You are 'Yuki', a world-renowned bonsai master. Create a detailed, year-round seasonal guide for a ${species} bonsai located in the ${location} climate. For each of the four seasons (Spring, Summer, Autumn, Winter), provide a summary of the main cultivation goals and a list of key tasks with their importance level (High, Medium, Low).`;
const result = await generate(prompt, { type: Type.OBJECT, properties: { result: seasonalGuideSchema } });
return result ? result.result : null;
} catch(error) {
return handleAIError(error);
}
}
export const generateToolGuide = async (): Promise<ToolRecommendation[] | null> => {
try {
const prompt = `You are 'Yuki', a world-renowned bonsai master. Create a comprehensive guide to bonsai tools. Provide a list of essential, recommended, and advanced tools. For each tool, provide its name, category (Cutting, Wiring, Repotting, General Care), a description of its use, and its level (Essential, Recommended, or Advanced).`;
const result = await generate(prompt, { type: Type.OBJECT, properties: { result: toolRecommendationsSchema } });
return result ? result.result : null;
} catch(error) {
return handleAIError(error);
}
}
export const generateMaintenanceTips = async (toolName: string): Promise<MaintenanceTips | null> => {
if (!isAIConfigured()) {
throw new Error("AI is not configured. Please set your API key in Settings.");
}
try {
const prompt = `You are 'Yuki', a bonsai master and tool maintenance expert. Provide a concise, actionable maintenance guide for a specific bonsai tool: "${toolName}". The guide should be easy for an enthusiast to follow. Cover three key areas: sharpening, cleaning, and storage.`;
return await generate(prompt, maintenanceTipsSchema);
} catch(error) {
return handleAIError(error);
}
}
export const analyzeDiaryLog = async (
species: string,
newPhotosBase64: string[],
previousPhotoBase64?: string
): Promise<DiaryAIAnalysis | null> => {
if (!isAIConfigured()) {
throw new Error("AI is not configured. Please set your API key in Settings.");
}
try {
let prompt = `You are 'Yuki', a bonsai master. This is a new diary log for a ${species} bonsai. Analyze the attached new photo(s).`;
const parts: Part[] = [];
if (previousPhotoBase64) {
prompt += ` A previous photo is also provided for comparison. Note the changes in health, growth, and structure. Provide a concise summary of these changes, estimate the health change as a single positive or negative integer (e.g., +5, -2, 0), and list 1-2 actionable suggestions based on the new visual data.`;
parts.push({
inlineData: { mimeType: 'image/jpeg', data: previousPhotoBase64 }
});
parts.push({ text: "Previous photo."});
} else {
prompt += ` This is the first analyzed entry for this tree. Provide a brief summary of the tree's current state, and list 1-2 actionable suggestions.`;
}
newPhotosBase64.forEach(photo => {
parts.push({ inlineData: { mimeType: 'image/jpeg', data: photo }});
});
parts.push({ text: prompt });
const response = await ai!.models.generateContent({
model: 'gemini-2.5-flash',
contents: { parts: parts },
config: {
responseMimeType: "application/json",
responseSchema: diaryAIAnalysisSchema
}
});
const jsonText = response.text.trim();
if (!jsonText) {
console.error("Gemini API returned an empty response.");
return null;
}
return JSON.parse(jsonText) as DiaryAIAnalysis;
} catch (error) {
return handleAIError(error);
}
};
export const analyzeGrowthProgression = async (
species: string,
photos: { date: string, image: string }[]
): Promise<string | null> => {
if (photos.length < 2) return "Please select at least two photos to see a progression.";
if (!isAIConfigured()) {
throw new Error("AI is not configured. Please set your API key in Settings.");
}
try {
const parts: Part[] = [];
let prompt = `You are 'Yuki', a bonsai master. Analyze the following series of images of a ${species} bonsai, provided with their corresponding dates. The images are ordered chronologically. Write a narrative describing the tree's journey and development over this period. Comment on changes in ramification, trunk thickness, leaf/needle health, styling decisions (like pruning or wiring) that are visible, and the overall aesthetic evolution of the tree. Be insightful and encouraging.
`;
photos.forEach(({date, image}) => {
prompt += `Image from: ${new Date(date).toLocaleDateString()}\n`;
parts.push({ inlineData: { mimeType: 'image/jpeg', data: image }});
});
parts.push({text: prompt});
const response = await ai!.models.generateContent({
model: 'gemini-2.5-flash',
contents: { parts },
});
return response.text;
} catch (error) {
return handleAIError(error);
}
};
export const generateStylingBlueprint = async (imageBase64: string, styleInstructions: string): Promise<StylingBlueprint | null> => {
if (!isAIConfigured()) {
throw new Error("AI is not configured. Please set your API key in Settings.");
}
try {
const prompt = `You are 'Yuki', a world-renowned bonsai master and digital artist. You will create a digital styling blueprint for a bonsai tree. The user has provided an image and a styling goal: "${styleInstructions}".
Your task is to analyze the image, identify key structural elements, and then generate a JSON object representing an SVG overlay to guide the user.
**Analysis & Generation Rules:**
1. Mentally overlay a 1000x1000 coordinate grid on the provided image. All coordinates in your response MUST be relative to this 1000x1000 canvas.
2. Identify the trunk line, major branches, and foliage masses.
3. Based on the user's styling goal, create a JSON object conforming to the provided schema.
4. **Annotation Guidelines:**
- **PRUNE_LINE:** Use 'points' to draw a line indicating where a branch should be cut.
- **REMOVE_BRANCH:** Use 'points' to draw a polygon around a branch to be removed.
- **WIRE_DIRECTION:** Use an SVG 'path' string (e.g., "M x1 y1 Q x2 y2 x3 y3") to show the new direction of a wired branch.
- **FOLIAGE_REFINEMENT:** Use 'points' to draw a polygon for the ideal new shape of a foliage pad.
- **JIN_SHARI:** Use 'points' to outline an area on the trunk/branch to be turned into deadwood.
- **TRUNK_LINE:** Use a 'path' to highlight the main flow of the trunk.
- **EXPOSE_ROOT:** Use 'points' to outline an area where soil can be removed to expose more nebari.
5. Every annotation MUST have a clear, concise 'label'.
6. Provide an overall 'summary' of the styling strategy.
Your response MUST be a valid JSON object matching the schema.`;
const imagePart = { inlineData: { mimeType: 'image/jpeg', data: imageBase64 } };
const textPart = { text: prompt };
const response = await ai!.models.generateContent({
model: 'gemini-2.5-flash',
contents: { parts: [imagePart, textPart] },
config: {
responseMimeType: "application/json",
responseSchema: stylingBlueprintSchema
}
});
const jsonText = response.text.trim();
if (!jsonText) {
console.error("Gemini API returned an empty response for styling blueprint.");
return null;
}
return JSON.parse(jsonText) as StylingBlueprint;
} catch (error) {
return handleAIError(error);
}
};
export const generateBonsaiImage = async (prompt: string): Promise<string | null> => {
if (!isAIConfigured()) {
throw new Error("AI is not configured. Please set your API key in Settings.");
}
try {
const response = await ai!.models.generateImages({
model: 'imagen-3.0-generate-002',
prompt: prompt,
config: {
numberOfImages: 1,
outputMimeType: 'image/jpeg',
aspectRatio: '1:1',
},
});
if (response.generatedImages && response.generatedImages.length > 0) {
return response.generatedImages[0].image.imageBytes;
}
return null;
} catch (error) {
return handleAIError(error);
}
};
export const analyzeSoilComposition = async (
imageBase64: string,
species: string,
location: string
): Promise<SoilAnalysis | null> => {
if (!isAIConfigured()) {
throw new Error("AI is not configured. Please set your API key in Settings.");
}
try {
const prompt = `You are a soil scientist specializing in bonsai substrates. Analyze the provided close-up image of a bonsai soil mix. The user states it is for a ${species} bonsai in ${location}. Based on the visual texture, color, and particle shapes:
1. Identify the primary components and estimate their percentages. Components can include: Akadama, Pumice, Lava Rock, Organic Compost, Kiryu, Pine Bark, Diatomaceous Earth, Sand, Grit, or Other.
2. Assess the overall drainage and water retention properties of the mix.
3. Provide a suitability analysis for the specified species and location, considering its water and air requirements.
4. Offer actionable suggestions for how this soil mix could be improved for this specific use case.
Generate the response strictly following the provided JSON schema.`;
const imagePart = { inlineData: { mimeType: 'image/jpeg', data: imageBase64 } };
const textPart = { text: prompt };
const response = await ai!.models.generateContent({
model: 'gemini-2.5-flash',
contents: { parts: [imagePart, textPart] },
config: {
responseMimeType: "application/json",
responseSchema: soilAnalysisSchema
}
});
const jsonText = response.text.trim();
if (!jsonText) {
console.error("Gemini API returned an empty response for soil analysis.");
return null;
}
return JSON.parse(jsonText) as SoilAnalysis;
} catch (error) {
return handleAIError(error);
}
};
export const generateNebariBlueprint = async (imageBase64: string): Promise<StylingBlueprint | null> => {
if (!isAIConfigured()) {
throw new Error("AI is not configured. Please set your API key in Settings.");
}
try {
const prompt = `You are a bonsai master specializing in nebari (root flare) development. Analyze the provided image of the base of a bonsai tree. Your goal is to create a visual guide for the user to improve the nebari over several seasons.
Generate a styling blueprint with annotations showing how to improve the nebari over time. Use annotations like:
- **PRUNE_LINE**: To indicate a root that should be pruned back to encourage finer roots.
- **REMOVE_BRANCH**: To mark a large, unsightly, or crossing root for complete removal during the next repotting.
- **EXPOSE_ROOT**: To outline areas where soil should be carefully removed at the surface to reveal more of the nebari.
Every annotation must have a clear, concise label explaining the action and its purpose (e.g., 'Prune this thick root to encourage radial growth'). Provide a summary of the overall strategy for nebari development.`;
const imagePart = { inlineData: { mimeType: 'image/jpeg', data: imageBase64 } };
const textPart = { text: prompt };
const response = await ai!.models.generateContent({
model: 'gemini-2.5-flash',
contents: { parts: [imagePart, textPart] },
config: {
responseMimeType: "application/json",
responseSchema: stylingBlueprintSchema
}
});
const jsonText = response.text.trim();
if (!jsonText) {
console.error("Gemini API returned an empty response for nebari blueprint.");
return null;
}
return JSON.parse(jsonText) as StylingBlueprint;
} catch (error) {
return handleAIError(error);
}
};
export const getProtectionProfile = async (species: string): Promise<Omit<ProtectionProfile, 'alertsEnabled'> | null> => {
if (!isAIConfigured()) {
throw new Error("AI is not configured. Please set your API key in Settings.");
}
try {
const prompt = `You are a horticultural expert specializing in bonsai. For the bonsai species "${species}", provide its typical environmental tolerance thresholds. Give reasonable, common values for a healthy, established tree.
Respond strictly in JSON format matching the schema.`;
const result = await generate(prompt, protectionProfileSchema);
return result as Omit<ProtectionProfile, 'alertsEnabled'>;
} catch (error) {
return handleAIError(error);
}
};