Pierwszy wrzut promptstory
This commit is contained in:
164
services/geminiService.ts
Normal file
164
services/geminiService.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user