Files
ai-florist/src/lib/server/gemini/mock.js
Chaewon Lee 0414393be7 chore: QA fixes for edit, map, and result pages
* fix: align result cards with recipe and tighten bouquet edit prompts

* fix: improve masked area edits with aligned masks and inpainting prompts

* refactor: drop floristNote/finalize and add map description

* fix: prevent map page crash from marker effect loop

* chore: add eslint exception for KakaoMap shopMarkerMap
2026-06-16 00:01:27 +09:00

120 lines
3.7 KiB
JavaScript

/** @typedef {import('../flowerFlow/jobStore.js').MoodAnalysis} MoodAnalysis */
/** @typedef {import('../flowerFlow/jobStore.js').BouquetRecipe} BouquetRecipe */
import { formatStrictBouquetImagePrompt } from '../../flowerFlow/bouquetImageFormat.js';
/** @returns {MoodAnalysis} */
export function mockMoodAnalysis() {
return {
colorPalette: ['pale pink', 'ivory', 'light green'],
moodKeywords: ['soft', 'warm', 'natural'],
styleImpression: ['minimal', 'romantic'],
textureKeywords: ['airy', 'delicate'],
energyLevel: 'medium'
};
}
/**
* @param {Partial<import('../flowerFlow/jobStore.js').UserInput>} userInput
* @returns {BouquetRecipe}
*/
export function mockRecipe(userInput = {}) {
const budget = userInput.budget
? `around ₩${userInput.budget.toLocaleString('en-US')}`
: 'around ₩50,000';
const notes = userInput.notes?.toLowerCase() ?? '';
/** @type {string[]} */
let mainFlowers = ['Pink tulip'];
/** @type {string} */
let concept = 'Soft Romantic Tulip Bouquet';
if (/love|사랑/.test(notes)) {
mainFlowers = ['Red rose'];
concept = 'Romantic Rose Bouquet';
} else if (/thank|grateful|감사/.test(notes)) {
mainFlowers = ['Dahlia'];
concept = 'Grateful Dahlia Bouquet';
} else if (/proud|congratul/.test(notes)) {
mainFlowers = ['Sunflower'];
concept = 'Celebratory Sunflower Bouquet';
} else if (/birthday|happy/.test(notes)) {
mainFlowers = ['Gerbera'];
concept = 'Cheerful Gerbera Bouquet';
}
return {
concept,
mainFlowers,
subFlowers: ["Baby's breath", 'Seasonal white flowers'],
greenery: ['Eucalyptus'],
colors: ['pale pink', 'ivory', 'soft green'],
wrapping: 'ivory paper with pale pink ribbon',
shape: 'loose round bouquet',
budget
};
}
/** @param {BouquetRecipe} recipe */
export function mockImagePrompt(recipe) {
return formatStrictBouquetImagePrompt(recipe);
}
/** @param {string} [label] */
export function mockGeneratedImage(label = 'Bouquet') {
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="768" height="1024" viewBox="0 0 768 1024">
<rect width="768" height="1024" fill="#f7f3ef"/>
<text x="50%" y="48%" text-anchor="middle" font-size="42" fill="#6b5b53" font-family="Arial">Mock ${label}</text>
<text x="50%" y="54%" text-anchor="middle" font-size="22" fill="#9a8d84" font-family="Arial">Set OPENAI_API_KEY for real images</text>
</svg>`;
return {
mimeType: 'image/svg+xml',
base64: Buffer.from(svg).toString('base64')
};
}
/**
* Apply a simple swap edit to the recipe in mock mode (e.g. "change tulip to rose").
* @param {BouquetRecipe} recipe
* @param {string} editPrompt
* @returns {BouquetRecipe}
*/
export function mockApplyRecipeEdit(recipe, editPrompt) {
/** @type {BouquetRecipe} */
const updated = structuredClone(recipe);
const lower = editPrompt.toLowerCase();
const swapMatch =
lower.match(/(?:change|replace|swap)\s+(.+?)\s+(?:to|with|into)\s+(.+)/) ??
lower.match(/(.+?)\s+(?:to|into)\s+(.+)/);
if (!swapMatch) return updated;
const fromToken = swapMatch[1].trim().replace(/[.!?]$/, '');
const toToken = swapMatch[2].trim().replace(/[.!?]$/, '');
if (!fromToken || !toToken) return updated;
/** @param {string[]} labels */
const replaceInList = (labels) =>
labels.map((label) => {
if (!label.toLowerCase().includes(fromToken)) return label;
const colorPrefix = label.match(/^(\w+)\s+/i)?.[1];
const capitalizedTo = toToken.charAt(0).toUpperCase() + toToken.slice(1).toLowerCase();
if (colorPrefix && !fromToken.includes(' ')) {
return `${colorPrefix} ${capitalizedTo}`;
}
return label.replace(new RegExp(fromToken, 'i'), capitalizedTo);
});
updated.mainFlowers = replaceInList(updated.mainFlowers);
updated.subFlowers = replaceInList(updated.subFlowers);
updated.greenery = replaceInList(updated.greenery);
return updated;
}