327 lines
12 KiB
TypeScript
327 lines
12 KiB
TypeScript
|
|
import { GoogleGenAI, Chat, GenerateContentResponse, Type } from "@google/genai";
|
|
import { TriviaQuestion, FTLMission, ChatMessage } from '../types';
|
|
import { LOCAL_LEXICON } from '../ai-voice-model/ai';
|
|
|
|
// --- UNIFIED RUDRA AI SYSTEM INSTRUCTION ---
|
|
const RUDRA_SYSTEM_INSTRUCTION = `You are Rudra (रुद्र), a witty, warm, and knowledgeable Nepali companion.
|
|
|
|
**CORE PERSONA:**
|
|
- **Tone:** You are NOT a robot. You are a "Janne Manchhe" (Knowledgeable person) or a helpful "Sathi" (Friend).
|
|
- **Style:** Speak naturally. Use short, punchy sentences. Avoid long, boring lists unless specifically asked.
|
|
- **Language:**
|
|
- In English: Use a slight South Asian flair but keep it professional.
|
|
- In Nepali: Use "Hajur", "Tapai" respectfully but naturally.
|
|
- In Newari (optional): Use "Jwajalapa" for greetings.
|
|
|
|
**CRITICAL OUTPUT PROTOCOL:**
|
|
You MUST respond in a strict JSON format for EVERY text-based interaction (unless using tools).
|
|
Do NOT output plain text.
|
|
Structure:
|
|
{
|
|
"en": "English conversational response (keep it short!)",
|
|
"ne": "Nepali conversational response (Devanagari, keep it natural)",
|
|
"newa": "Optional Newari phrase if relevant",
|
|
"type": "text" | "quiz"
|
|
}
|
|
|
|
**KNOWLEDGE BASE:**
|
|
${LOCAL_LEXICON}
|
|
|
|
**TRIVIA MODE:**
|
|
If asked for a quiz, set "type": "quiz" and provide valid JSON data. Focus heavily on Nepal's History, Geography, and Culture.
|
|
`;
|
|
|
|
export const createStudyChat = (): Chat => {
|
|
const ai = new GoogleGenAI({ apiKey: process.env.API_KEY });
|
|
return ai.chats.create({
|
|
model: 'gemini-3-flash-preview',
|
|
config: {
|
|
systemInstruction: RUDRA_SYSTEM_INSTRUCTION,
|
|
temperature: 0.8, // Slightly higher for more "human" variance
|
|
responseMimeType: 'application/json'
|
|
},
|
|
});
|
|
};
|
|
|
|
export const getCookingChatSession = (): Chat => {
|
|
const ai = new GoogleGenAI({ apiKey: process.env.API_KEY });
|
|
return ai.chats.create({
|
|
model: 'gemini-3-flash-preview',
|
|
config: {
|
|
systemInstruction: `You are **Bhanse Dai (भान्से दाइ)**, an expert Nepali Chef.
|
|
|
|
**CORE INSTRUCTIONS:**
|
|
1. You are warm, encouraging, and passionate about Nepali heritage cuisine.
|
|
2. You MUST provide responses in BOTH English and Nepali (Devanagari) for every turn.
|
|
3. Use the following JSON format strictly:
|
|
|
|
{
|
|
"en": "English response here...",
|
|
"ne": "नेपाली जवाफ यहाँ..."
|
|
}
|
|
|
|
**CONTENT GUIDELINES:**
|
|
- If asked for a recipe, provide ingredients and brief steps in both languages within the JSON fields.
|
|
- Use terms like "Mitho cha!" (Tasty!) or "Bhat pakauna garo chaina" (Cooking rice isn't hard).
|
|
- Keep recipes concise.
|
|
`,
|
|
responseMimeType: 'application/json'
|
|
}
|
|
});
|
|
};
|
|
|
|
export const generateSummary = async (text: string): Promise<string> => {
|
|
const ai = new GoogleGenAI({ apiKey: process.env.API_KEY });
|
|
const response = await ai.models.generateContent({
|
|
model: 'gemini-3-flash-preview',
|
|
contents: `Summarize this text. Return JSON: { "en": "English summary", "ne": "Nepali summary" }.\n\n${text}`,
|
|
config: { responseMimeType: 'application/json' }
|
|
});
|
|
const json = JSON.parse(response.text || '{}');
|
|
return JSON.stringify(json); // Return stringified JSON for component to parse
|
|
};
|
|
|
|
export const translateText = async (text: string, targetLang: 'en' | 'ne'): Promise<string> => {
|
|
const ai = new GoogleGenAI({ apiKey: process.env.API_KEY });
|
|
const prompt = `Translate "${text}" to ${targetLang === 'en' ? 'English' : 'Nepali'}. Return plain text string.`;
|
|
const response = await ai.models.generateContent({
|
|
model: 'gemini-3-flash-preview',
|
|
contents: prompt
|
|
});
|
|
return response.text || text;
|
|
};
|
|
|
|
export const analyzeImage = async (base64Image: string, prompt: string): Promise<string> => {
|
|
const ai = new GoogleGenAI({ apiKey: process.env.API_KEY });
|
|
|
|
// Extract proper base64 data (remove header if present)
|
|
const base64Data = base64Image.split(',')[1] || base64Image;
|
|
|
|
try {
|
|
const response = await ai.models.generateContent({
|
|
model: 'gemini-2.5-flash-image', // Specialized visual model
|
|
contents: {
|
|
parts: [
|
|
{ text: prompt },
|
|
{
|
|
inlineData: {
|
|
mimeType: 'image/jpeg',
|
|
data: base64Data
|
|
}
|
|
}
|
|
]
|
|
},
|
|
config: {
|
|
systemInstruction: RUDRA_SYSTEM_INSTRUCTION,
|
|
responseMimeType: 'application/json'
|
|
}
|
|
});
|
|
return response.text || "{}";
|
|
} catch (e) {
|
|
console.error("Image Analysis Error", e);
|
|
return JSON.stringify({
|
|
en: "I'm having trouble seeing the image clearly. Please try again.",
|
|
ne: "मैले तस्विर स्पष्ट देख्न सकिन। कृपया पुनः प्रयास गर्नुहोस्।"
|
|
});
|
|
}
|
|
};
|
|
|
|
export const analyzeVideo = async (base64Video: string, mimeType: string, prompt: string): Promise<string> => {
|
|
// Deprecated for UI but kept for API completeness if needed elsewhere
|
|
return "{}";
|
|
};
|
|
|
|
export const generateTrivia = async (topic: string, language: 'en' | 'ne' = 'en'): Promise<TriviaQuestion[]> => {
|
|
const ai = new GoogleGenAI({ apiKey: process.env.API_KEY });
|
|
const langInstruction = language === 'ne' ? 'Generate questions and options in Nepali language (Devanagari).' : 'Generate questions and options in English.';
|
|
|
|
const response = await ai.models.generateContent({
|
|
model: 'gemini-3-flash-preview',
|
|
contents: `Generate 5 multiple choice trivia questions about "${topic}".
|
|
${langInstruction}
|
|
Focus on Nepal context if possible (History, Geography, Culture, Nature).
|
|
Return ONLY a JSON array with objects containing:
|
|
- question (string)
|
|
- options (array of 4 strings)
|
|
- correctAnswer (index 0-3, number)
|
|
- explanation (short string in ${language === 'ne' ? 'Nepali' : 'English'})
|
|
|
|
Ensure questions are interesting and not too easy.`,
|
|
config: { responseMimeType: 'application/json' }
|
|
});
|
|
|
|
try {
|
|
return JSON.parse(response.text || "[]");
|
|
} catch (e) {
|
|
return [];
|
|
}
|
|
};
|
|
|
|
export const explainHoliday = async (holidayName: string): Promise<{ en: string, ne: string }> => {
|
|
const ai = new GoogleGenAI({ apiKey: process.env.API_KEY });
|
|
const response = await ai.models.generateContent({
|
|
model: 'gemini-3-flash-preview',
|
|
contents: `Explain the significance of the Nepali festival/holiday "${holidayName}".
|
|
Provide a short explanation in English and Nepali.
|
|
Return JSON: { "en": "string", "ne": "string" }`,
|
|
config: { responseMimeType: 'application/json' }
|
|
});
|
|
try {
|
|
return JSON.parse(response.text || '{"en": "Info unavailable", "ne": "विवरण उपलब्ध छैन"}');
|
|
} catch {
|
|
return { en: "AI Error", ne: "त्रुटि" };
|
|
}
|
|
};
|
|
|
|
export const matchEmergencyReports = async (foundDescription: string, activeMissions: FTLMission[]) => {
|
|
const lostMissions = activeMissions.filter(m => m.status === 'active' && m.isLost);
|
|
if (lostMissions.length === 0) return { matchId: null, confidence: 0, reasoning: "No active lost reports." };
|
|
|
|
const ai = new GoogleGenAI({ apiKey: process.env.API_KEY });
|
|
const prompt = `
|
|
I have found an item/person with this description: "${foundDescription}".
|
|
Here is a list of active "Lost" reports:
|
|
${JSON.stringify(lostMissions.map(m => ({ id: m.id, title: m.title, desc: m.description, type: m.type })))}
|
|
|
|
Analyze if the found item matches any of the lost reports.
|
|
Return JSON: { "matchId": "string or null", "confidence": number (0-100), "reasoning": "string" }
|
|
`;
|
|
|
|
const response = await ai.models.generateContent({
|
|
model: 'gemini-3-flash-preview',
|
|
contents: prompt,
|
|
config: { responseMimeType: 'application/json' }
|
|
});
|
|
|
|
try {
|
|
return JSON.parse(response.text || '{}');
|
|
} catch {
|
|
return { matchId: null, confidence: 0 };
|
|
}
|
|
};
|
|
|
|
export const interpretDream = async (dreamText: string) => {
|
|
const ai = new GoogleGenAI({ apiKey: process.env.API_KEY });
|
|
const prompt = `
|
|
Act as a mystical interpreter of dreams, combining Nepali folklore/superstition with modern psychology.
|
|
Dream: "${dreamText}"
|
|
|
|
Return JSON with both English (en) and Nepali (ne) translations:
|
|
{
|
|
"folklore": {
|
|
"en": "Traditional interpretation in English",
|
|
"ne": "Traditional interpretation in Nepali (Devanagari)"
|
|
},
|
|
"psychology": {
|
|
"en": "Modern psychological view in English",
|
|
"ne": "Modern psychological view in Nepali (Devanagari)"
|
|
},
|
|
"symbol": "One key symbol from the dream"
|
|
}
|
|
`;
|
|
|
|
const response = await ai.models.generateContent({
|
|
model: 'gemini-3-flash-preview',
|
|
contents: prompt,
|
|
config: { responseMimeType: 'application/json' }
|
|
});
|
|
|
|
try {
|
|
return JSON.parse(response.text || '{}');
|
|
} catch {
|
|
return {
|
|
folklore: { en: "The mists are too thick.", ne: "कुहिरो धेरै बाक्लो छ।" },
|
|
psychology: { en: "Unclear data.", ne: "स्पष्ट डाटा छैन।" },
|
|
symbol: "?"
|
|
};
|
|
}
|
|
};
|
|
|
|
export const createHealthAssistant = (history: any[], isNepali: boolean) => {
|
|
const ai = new GoogleGenAI({ apiKey: process.env.API_KEY });
|
|
return ai.chats.create({
|
|
model: 'gemini-3-flash-preview',
|
|
config: {
|
|
systemInstruction: `You are Prana AI (प्राण), a holistic health coach for a user in Nepal.
|
|
Context: The user has provided their recent health logs: ${JSON.stringify(history)}.
|
|
Style:
|
|
- Language: ${isNepali ? 'Nepali' : 'English'}
|
|
- Tone: Encouraging, knowledgeable about Ayurveda and modern science.
|
|
- If data shows low water/sleep, gently remind them.
|
|
- Keep responses conversational and short (under 50 words).`
|
|
}
|
|
});
|
|
};
|
|
|
|
export const searchPustakalaya = async (query: string) => {
|
|
const ai = new GoogleGenAI({ apiKey: process.env.API_KEY });
|
|
const response = await ai.models.generateContent({
|
|
model: 'gemini-3-flash-preview',
|
|
contents: `User is searching for "${query}" in the context of the Nepali National Library (pustakalaya.org) or CDC.
|
|
Provide a helpful summary of what materials likely exist for this topic (e.g., Grade 10 Science textbooks, Munamadan, etc.).
|
|
Then provide 3 plausible direct links (mock them as likely URLs on pustakalaya.org).
|
|
|
|
Return JSON: { "text": "Summary string...", "links": [{ "title": "Book Title", "uri": "https://pustakalaya.org/..." }] }`,
|
|
config: { responseMimeType: 'application/json' }
|
|
});
|
|
|
|
try {
|
|
return JSON.parse(response.text || '{"text": "Search failed.", "links": []}');
|
|
} catch {
|
|
return { text: "Could not access library database.", links: [] };
|
|
}
|
|
};
|
|
|
|
export const connectToGuruLive = (params: any) => {
|
|
const ai = new GoogleGenAI({ apiKey: process.env.API_KEY });
|
|
return ai.live.connect(params);
|
|
};
|
|
|
|
export const encodeAudio = (data: Uint8Array) => {
|
|
let binary = '';
|
|
const len = data.byteLength;
|
|
for (let i = 0; i < len; i++) {
|
|
binary += String.fromCharCode(data[i]);
|
|
}
|
|
return btoa(binary);
|
|
};
|
|
|
|
export const decodeAudio = (base64: string) => {
|
|
const binaryString = atob(base64);
|
|
const len = binaryString.length;
|
|
const bytes = new Uint8Array(len);
|
|
for (let i = 0; i < len; i++) {
|
|
bytes[i] = binaryString.charCodeAt(i);
|
|
}
|
|
return bytes;
|
|
};
|
|
|
|
export const decodeAudioData = async (
|
|
data: Uint8Array,
|
|
ctx: AudioContext,
|
|
sampleRate: number,
|
|
numChannels: number,
|
|
): Promise<AudioBuffer> => {
|
|
const dataInt16 = new Int16Array(data.buffer);
|
|
const frameCount = dataInt16.length / numChannels;
|
|
const buffer = ctx.createBuffer(numChannels, frameCount, sampleRate);
|
|
|
|
for (let channel = 0; channel < numChannels; channel++) {
|
|
const channelData = buffer.getChannelData(channel);
|
|
for (let i = 0; i < frameCount; i++) {
|
|
channelData[i] = dataInt16[i * numChannels + channel] / 32768.0;
|
|
}
|
|
}
|
|
return buffer;
|
|
};
|
|
|
|
export const requestMicPermission = async () => {
|
|
try {
|
|
await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
};
|