Files
promptstory/services/geminiService.ts
2026-02-15 13:22:48 +01:00

165 lines
5.1 KiB
TypeScript

import { GoogleGenAI, Type, Schema } from "@google/genai";
import { WizardState, GeneratedContent } from "../types";
import { getSystemPrompt } from "../prompts";
// SCHEMAT ODPOWIEDZI JSON
const responseSchema: Schema = {
type: Type.OBJECT,
properties: {
caption: {
type: Type.STRING,
description: "Gotowy opis pod post na Instagram/YouTube/Strava.",
},
slides: {
type: Type.ARRAY,
description: "Lista elementów wizualnych. Dla Instagrama są to slajdy karuzeli. Dla Stravy są to sugestie zdjęć do galerii (Social Proof, Data, Reward).",
items: {
type: Type.OBJECT,
properties: {
overlay_text: {
type: Type.STRING,
description: "Tekst na grafikę (Instagram) LUB Etykieta typu zdjęcia (Strava: np. 'DANE', 'TWARZ').",
},
image_prompt: {
type: Type.STRING,
description: "Sugestia dla użytkownika w języku polskim, jakie zdjęcie wykonać lub wybrać z galerii (np. 'Zbliżenie na zegarek z tętnem', 'Selfie z błotem na twarzy').",
},
notes: {
type: Type.STRING,
description: "Uzasadnienie, dlaczego to zdjęcie buduje zasięg/historię.",
}
},
required: ["overlay_text", "image_prompt"],
},
},
},
required: ["caption", "slides"],
};
export interface RefinementOptions {
slideCount: number;
feedback: string;
}
export const generateStoryContent = async (
data: WizardState,
apiKey: string,
refinement?: RefinementOptions
): Promise<GeneratedContent> => {
if (!apiKey) {
throw new Error("API Key is missing.");
}
const ai = new GoogleGenAI({ apiKey });
// 1. POBIERZ PROMPT SYSTEMOWY (TERAZ SKLEJANY Z PUZLI)
const BASE_SYSTEM_PROMPT = getSystemPrompt(data);
// 2. PRZYGOTUJ DANE (STATYSTYKI I WAYPOINTS) ZAMIAST SUROWEGO GPX
const statsInfo = `
DYSTANS: ${data.stats.distance}
CZAS TRWANIA: ${data.stats.duration}
PRZEWYŻSZENIA: ${data.stats.elevation}
`;
const waypointsInfo = data.waypoints.length > 0
? data.waypoints.map((wp, i) => ` - Moment ${i+1}: "${wp.header}" (Kontekst: ${wp.context})`).join('\n')
: "Brak zdefiniowanych kluczowych punktów.";
// 2b. PRZYGOTUJ DANE Z WYCIECZKI (JEŚLI SĄ)
let tripInfo = "";
if (data.eventType === 'trip' && data.tripData) {
const stopsList = data.tripData.stops
.filter(s => s.place.trim() !== '')
.map((s, i) => ` ${i+1}. PRZYSTANEK: ${s.place} - Opis: ${s.description}`)
.join('\n');
tripInfo = `
=== SZCZEGÓŁY WYCIECZKI (TRIP ITINERARY) ===
PUNKT STARTOWY: ${data.tripData.startPoint.place} (Opis: ${data.tripData.startPoint.description})
PUNKT KOŃCOWY: ${data.tripData.endPoint.place} (Opis: ${data.tripData.endPoint.description})
PLAN TRASY:
${stopsList}
INSTRUKCJA DODATKOWA: Wykorzystaj te konkretne miejsca w narracji i sugestiach zdjęć.
`;
}
// 3. OBSŁUGA POPRAWEK (REFINEMENT)
let refinementInstruction = "";
if (refinement) {
refinementInstruction = `
!!! WAŻNA AKTUALIZACJA OD UŻYTKOWNIKA (PRIORYTET) !!!
Użytkownik prosi o poprawki do poprzedniej wersji. Zignoruj poprzednie wytyczne dotyczące liczby slajdów, jeśli są inne.
1. WYMAGANA LICZBA SLAJDÓW: Dokładnie ${refinement.slideCount}. Dostosuj tempo historii, aby idealnie wypełnić tę liczbę.
2. UWAGI MERYTORYCZNE: "${refinement.feedback}"
Wprowadź te zmiany do narracji.
`;
}
// 4. SKLEJANIE PEŁNEJ INSTRUKCJI
const FULL_SYSTEM_INSTRUCTION = `
${BASE_SYSTEM_PROMPT}
=== DANE WYDARZENIA (META DANE) ===
TYTUŁ: "${data.title}"
OPIS UŻYTKOWNIKA: "${data.description}"
=== STATYSTYKI AKTYWNOŚCI (Z PLIKU GPX) ===
${statsInfo}
=== KLUCZOWE MOMENTY (WAYPOINTS) ===
${waypointsInfo}
${tripInfo}
${refinementInstruction}
Instrukcja: Wykorzystaj powyższe statystyki, konfigurację i opis użytkownika do stworzenia narracji.
`;
// Budowanie zapytania z tekstem i plikami (tylko obrazy/PDF, bez surowego GPX)
const promptParts: any[] = [];
// Dodaj pełną instrukcję
promptParts.push({ text: FULL_SYSTEM_INSTRUCTION });
// Dodaj pliki (Tylko obrazy i PDFy. GPX jest już przetworzony wyżej jako tekst)
data.files.forEach(file => {
// Ignorujemy treść pliku GPX w promptcie
if (!file.file.name.toLowerCase().endsWith('.gpx')) {
promptParts.push({
inlineData: {
mimeType: file.mimeType,
data: file.content as string
}
});
}
});
try {
const response = await ai.models.generateContent({
model: "gemini-3-flash-preview",
contents: { parts: promptParts },
config: {
responseMimeType: "application/json",
responseSchema: responseSchema,
},
});
if (response.text) {
return JSON.parse(response.text) as GeneratedContent;
} else {
throw new Error("Nie udało się wygenerować treści.");
}
} catch (error) {
console.error("Gemini API Error:", error);
throw error;
}
};