165 lines
5.1 KiB
TypeScript
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;
|
|
}
|
|
};
|