File size: 13,133 Bytes
fd4dc0d
 
 
 
 
 
77d0015
 
fd4dc0d
77d0015
fd4dc0d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7f17645
fd4dc0d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1558c28
fd4dc0d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1558c28
fd4dc0d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1558c28
fd4dc0d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1558c28
fd4dc0d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
import { GoogleGenAI, Type, Schema } from "@google/genai";
import { ProjectPlan, NoteType, StyleMemory } from "../types";

const getAI = () => {
    const userKey = localStorage.getItem('user_gemini_api_key');
    const apiKey = userKey || process.env.GEMINI_API_KEY;
    if (!apiKey || apiKey === 'dummy-key') {
        throw new Error("API_KEY_MISSING");
    }
    return new GoogleGenAI({ apiKey: apiKey });
};

// Helper to compress base64 images to avoid Firestore 1MB limit
const resizeBase64Image = (base64Str: string, maxWidth = 800, maxHeight = 800): Promise<string> => {
  return new Promise((resolve) => {
    const img = new Image();
    img.src = base64Str;
    img.onload = () => {
      const canvas = document.createElement('canvas');
      let width = img.width;
      let height = img.height;

      if (width > height) {
        if (width > maxWidth) {
          height *= maxWidth / width;
          width = maxWidth;
        }
      } else {
        if (height > maxHeight) {
          width *= maxHeight / height;
          height = maxHeight;
        }
      }

      canvas.width = width;
      canvas.height = height;
      const ctx = canvas.getContext('2d');
      if (ctx) {
        ctx.drawImage(img, 0, 0, width, height);
        resolve(canvas.toDataURL('image/jpeg', 0.7));
      } else {
        resolve(base64Str);
      }
    };
    img.onerror = () => resolve(base64Str);
  });
};

// System instruction for the planner
const PLANNER_SYSTEM_INSTRUCTION = `
You are the chief architect of the "MindSpark" system.
Analyze the user's complex request and plan it like a task distribution for an expert team.

YOUR TASK:
1. Determine 4-8 steps required for the project.
2. Assign an "Expert Agent" (assignedAgent) and their "Role" (agentRole) for each step. 
3. Steps should follow each other and be in a logical flow.

STYLE GUIDE (MEMORY):
If any past styles or preferences are provided to you, keep this tone and approach in your planning.

AGENT ASSIGNMENT RULES:
- Determine specific experts based on the project type (Software Developer, Designer, Writer, Analyst, Strategist, etc.).
- Determine the 'type' property of each step based on the content: 'text', 'code', 'image'.
- Provide the answer strictly in JSON format. Use English language.
`;

// Schema for structured JSON output
const planSchema: Schema = {
  type: Type.OBJECT,
  properties: {
    title: { type: Type.STRING, description: "A creative and engaging main title for the project" },
    summary: { type: Type.STRING, description: "A short, motivating summary of what this notebook is about." },
    steps: {
      type: Type.ARRAY,
      items: {
        type: Type.OBJECT,
        properties: {
          title: { type: Type.STRING, description: "Title of the step" },
          description: { type: Type.STRING, description: "A short summary of what will be done in this step." },
          type: { type: Type.STRING, enum: ["text", "code", "image"], description: "Content type" },
          assignedAgent: { type: Type.STRING, description: "Name of the expert agent taking the task (e.g., 'Code Architect')" },
          agentRole: { type: Type.STRING, description: "Specific role and responsibility assigned to the agent in this step." },
        },
        required: ["title", "description", "type", "assignedAgent", "agentRole"],
      },
    },
  },
  required: ["title", "summary", "steps"],
};

const isQuotaError = (error: any): boolean => {
  const errorStr = JSON.stringify(error).toLowerCase();
  return (
    error?.status === 'RESOURCE_EXHAUSTED' ||
    error?.code === 429 ||
    errorStr.includes('429') ||
    errorStr.includes('resource_exhausted') ||
    errorStr.includes('quota exceeded') ||
    errorStr.includes('limit reached')
  );
};

export const boostPrompt = async (prompt: string): Promise<string> => {
  try {
    const response = await getAI().models.generateContent({
      model: 'gemini-1.5-flash',
      contents: `Rewrite and enrich the following text to be a perfect command (prompt) or a great project idea to be given to an AI assistant.
- If it's a short and simple idea, detail it and add depth.
- If it's complex and messy, structure and clarify it.
- Use a professional, inspiring, and highly effective language.
- Provide only the improved text, do not add any explanations before or after like "Here is the text:".

Original Text:
${prompt}`,
    });
    return response.text ? response.text.trim() : prompt;
  } catch (error) {
    console.error("Prompt boost error:", error);
    if (isQuotaError(error)) {
        return prompt; // Fallback to original prompt if quota hit
    }
    throw error;
  }
};

export const transcribeAudio = async (base64Audio: string): Promise<string> => {
  try {
    const response = await getAI().models.generateContent({
      model: 'gemini-2.5-flash',
      contents: [
        "Transcribe this audio recording into text. Return only the text, do not provide any other explanation. If the audio is not understandable, leave it blank.",
        {
          inlineData: {
            mimeType: "audio/webm",
            data: base64Audio
          }
        }
      ]
    });
    return response.text ? response.text.trim() : "";
  } catch (error) {
    console.error("Transcription error:", error);
    if (isQuotaError(error)) {
        throw new Error("AI system is very busy (Quota reached). Please try typing your input as text.");
    }
    throw error;
  }
};

export const createProjectPlan = async (userPrompt: string, memories: StyleMemory[] = []): Promise<ProjectPlan> => {
  try {
    const memoryContext = memories.length > 0 
      ? `\n\nPAST PREFERENCES AND MEMORY:\n${memories.map(m => `- Project: ${m.projectName}, Style: ${m.styleKeywords.join(', ')}, Summary: ${m.summary}`).join('\n')}\n\nPlease take these styles and tone into account.`
      : "";

    const response = await getAI().models.generateContent({
      model: 'gemini-2.5-flash',
      contents: userPrompt + memoryContext,
      config: {
        systemInstruction: PLANNER_SYSTEM_INSTRUCTION,
        responseMimeType: "application/json",
        responseSchema: planSchema
      },
    });

    if (!response.text) throw new Error("Plan could not be created.");
    return JSON.parse(response.text) as ProjectPlan;
  } catch (error) {
    console.error("Plan Error:", error);
    if (isQuotaError(error)) {
        throw new Error("System is very busy right now (Quota limit exceeded). Please try again in a few minutes or use a shorter description.");
    }
    throw error;
  }
};

export const generateStepContent = async (
  projectTitle: string,
  stepTitle: string,
  stepDescription: string,
  stepType: NoteType,
  memories: StyleMemory[] = []
): Promise<string> => {
  const memoryContext = memories.length > 0
    ? `\nPAST STYLE AND TONE:\n${memories.map(m => `- ${m.styleKeywords.join(', ')}`).join(', ')}\nPlease reflect this style and tone in this content as well.`
    : "";

  const contextPrompt = `
CONTEXT: We are working on a project named "${projectTitle}".
CURRENT TASK: "${stepTitle}"
TASK DETAIL: ${stepDescription}
${memoryContext}

YOUR MISSION:
Prepare a detailed, educational, and directly applicable content for this step in the "MindSpark" format.
When the user reads this page, they should have everything they need to complete this step.

If Type is 'TEXT':
- Explain the subject in depth.
- Increase readability using bullet points, lists, and bold text.
- Speak like a professional mentor.

If Type is 'CODE':
- Write the necessary code blocks.
- Explain what the codes do with comment lines or explanations.

Use Markdown format.
  `;

  try {
    if (stepType === NoteType.IMAGE) {
      const imagePrompt = `
        High quality concept art illustration of: ${stepTitle}.
        Context: ${stepDescription}.
        Project Theme: ${projectTitle}.
        Style: Digital art, highly detailed, cinematic lighting, intricate textures, atmospheric, professional composition, masterpiece, 8k resolution, concept art style.
        No text, no labels, no watermarks, high quality.
      `;
      
      try {
          const response = await getAI().models.generateContent({
            model: 'gemini-2.5-flash-image',
            contents: { parts: [{ text: imagePrompt }] },
            config: {
              imageConfig: {
                aspectRatio: '16:9',
              },
            },
          });
          
          const parts = response.candidates?.[0]?.content?.parts || [];
          for (const part of parts) {
            if (part.inlineData) {
              const base64 = `data:${part.inlineData.mimeType || 'image/jpeg'};base64,${part.inlineData.data}`;
              return await resizeBase64Image(base64);
            }
          }
      } catch (innerError) {
          console.error("Image generation failed:", innerError);
          if (isQuotaError(innerError)) {
              return "--- Image could not be generated (System Busy / Quota Limit) ---\n" + stepDescription;
          }
      }
      
      return "--- Image could not be generated ---\n" + stepDescription;
    } else if (stepType === NoteType.TEXT) {
      const textPromise = getAI().models.generateContent({
        model: 'gemini-2.5-flash',
        contents: contextPrompt,
      }).catch(e => {
          console.error("Text content generation error:", e);
          if (isQuotaError(e)) {
              return { text: "Content could not be generated because the system is busy (Quota reached). Please try the 'Regenerate' option later." };
          }
          throw e;
      });

      const imagePrompt = `
        High quality concept art illustration of: ${stepTitle}.
        Context: ${stepDescription}.
        Project Theme: ${projectTitle}.
        Style: Digital art, highly detailed, cinematic lighting, concept art style, clean composition.
        No text, no labels, no watermarks, high quality.
      `;

      const imagePromise = getAI().models.generateContent({
        model: 'gemini-2.5-flash-image',
        contents: { parts: [{ text: imagePrompt }] },
        config: {
          imageConfig: {
            aspectRatio: '16:9',
          },
        },
      }).catch(e => {
        console.error("Image generation failed for text note:", e);
        return null;
      });

      const [textResponse, imageResponse] = await Promise.all([textPromise, imagePromise]);
      let content = (textResponse as any).text || "Content could not be generated.";

      if (imageResponse) {
          const parts = imageResponse.candidates?.[0]?.content?.parts || [];
          for (const part of parts) {
              if (part.inlineData) {
                  const base64 = `data:${part.inlineData.mimeType || 'image/jpeg'};base64,${part.inlineData.data}`;
                  const resizedBase64 = await resizeBase64Image(base64);
                  content = `![${stepTitle}](${resizedBase64})\n\n` + content;
                  break;
              }
          }
      }
      return content;
    } else {
      // Code generation
      try {
          const response = await getAI().models.generateContent({
            model: 'gemini-2.5-flash',
            contents: contextPrompt,
          });
          return response.text || "Content could not be generated.";
      } catch (e) {
          if (isQuotaError(e)) {
              return "```\n// Code could not be generated due to system busy.\n// Please try again later.\n```";
          }
          throw e;
      }
    }
  } catch (error) {
    console.error("Content Gen Error:", error);
    if (isQuotaError(error)) {
        return "System is very busy right now. Please try this step again later by clicking 'Regenerate'.";
    }
    throw error;
  }
};

export const chatWithStep = async (
  projectTitle: string,
  noteTitle: string,
  noteContent: string,
  userQuestion: string,
  history: any[] = []
): Promise<string> => {
  try {
    const contents = [
        ...history.map(h => ({
            role: h.role === 'model' ? 'model' : 'user',
            parts: h.parts
        })),
        { role: 'user', parts: [{ text: userQuestion }] }
    ];

    const response = await getAI().models.generateContent({
        model: 'gemini-2.5-flash',
        contents: contents,
        config: {
            systemInstruction: `You are an expert in the "MindSpark" project. 
            CONTEXT: We are in the "${noteTitle}" step within the "${projectTitle}" project.
            CONTENT OF THIS STEP:
            ${noteContent.substring(0, 1000)}...

            YOUR MISSION: To answer the user's questions about this step, deepen the content, or offer alternative suggestions. 
            - Provide short, concise, and technically deep answers. 
            - Be helpful, show the way. 
            - Use Markdown.`
        }
    });

    return response.text || "Sorry, an answer could not be generated.";
  } catch (error) {
    console.error("Step Chat Error:", error);
    if (isQuotaError(error)) {
        return "Sorry, the system is currently busy (Quota reached). Please ask again in a few minutes.";
    }
    throw error;
  }
};