Multiple feature additions and improvements across the application: - NextGen Editor: drag handles, smart paste, block actions - Structured views: Kanban and table layouts for notes - Architectural Grid: new brainstorming/agent interface prototype - Flashcards: SM-2 revision algorithm with AI generation - MCP server: robustness improvements - Graph/PDF chat: fix click propagation and copy behavior - Various UI/UX enhancements and bug fixes Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
314 lines
10 KiB
TypeScript
314 lines
10 KiB
TypeScript
|
|
import { GoogleGenAI, Type } from "@google/genai";
|
|
import { BrainstormIdea } from "../types";
|
|
|
|
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
|
|
|
|
const BRAINSTORM_SCHEMA = {
|
|
type: Type.OBJECT,
|
|
properties: {
|
|
ideas: {
|
|
type: Type.ARRAY,
|
|
items: {
|
|
type: Type.OBJECT,
|
|
properties: {
|
|
title: { type: Type.STRING },
|
|
description: { type: Type.STRING },
|
|
connection_to_seed: { type: Type.STRING },
|
|
novelty_score: { type: Type.NUMBER }
|
|
},
|
|
required: ["title", "description", "connection_to_seed", "novelty_score"]
|
|
}
|
|
}
|
|
},
|
|
required: ["ideas"]
|
|
};
|
|
|
|
const SUGGESTIONS_SCHEMA = {
|
|
type: Type.OBJECT,
|
|
properties: {
|
|
suggestions: {
|
|
type: Type.ARRAY,
|
|
items: {
|
|
type: Type.OBJECT,
|
|
properties: {
|
|
title: { type: Type.STRING },
|
|
description: { type: Type.STRING },
|
|
reasoning: { type: Type.STRING }
|
|
},
|
|
required: ["title", "description", "reasoning"]
|
|
}
|
|
}
|
|
},
|
|
required: ["suggestions"]
|
|
};
|
|
|
|
export async function generateBrainstormWave(
|
|
seedIdea: string,
|
|
waveNumber: number,
|
|
contextSummaries: string = ""
|
|
): Promise<Partial<BrainstormIdea>[]> {
|
|
const waveDescriptions = [
|
|
"", // index 0 unused
|
|
"VAGUE 1 (proximité directe) : Sous-aspects, reformulations, variations de l'idée. Reste dans le même domaine.",
|
|
"VAGUE 2 (analogies) : Trouve des parallèles dans d'autres domaines. Comment cette idée se manifeste-t-elle ailleurs ? Quelles techniques d'autres industries pourraient s'appliquer ?",
|
|
"VAGUE 3 (disruption) : Inverse l'idée. Pousse-la à l'extrême. Combine-la avec un domaine totalement non lié. Que se passe-t-il si l'opposé est vrai ?"
|
|
];
|
|
|
|
const prompt = `
|
|
Idée seed : "${seedIdea}"
|
|
Contexte : ${contextSummaries}
|
|
Génère 5 idées pour la VAGUE ${waveNumber} : ${waveDescriptions[waveNumber]}
|
|
Format JSON selon le schéma.
|
|
`;
|
|
|
|
try {
|
|
const response = await ai.models.generateContent({
|
|
model: "gemini-3-flash-preview",
|
|
contents: [{ role: "user", parts: [{ text: prompt }] }],
|
|
config: {
|
|
systemInstruction: "Tu es un expert en brainstorming. Réponds uniquement en JSON valide.",
|
|
responseMimeType: "application/json",
|
|
responseSchema: BRAINSTORM_SCHEMA,
|
|
temperature: 1.0
|
|
}
|
|
});
|
|
|
|
const resText = response.text;
|
|
if (!resText) return [];
|
|
|
|
const parsed = JSON.parse(resText.replace(/^```json\n?/, '').replace(/\n?```$/, '').trim());
|
|
const ideas = Array.isArray(parsed.ideas) ? parsed.ideas : (Array.isArray(parsed) ? parsed : []);
|
|
|
|
return ideas.map((item: any) => ({
|
|
title: item.title,
|
|
description: item.description,
|
|
connectionToSeed: item.connection_to_seed,
|
|
noveltyScore: item.novelty_score,
|
|
waveNumber
|
|
}));
|
|
} catch (error) {
|
|
console.error(`Error generating brainstorm wave ${waveNumber}:`, error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function generateExpansion(parentIdeaTitle: string, parentIdeaDescription: string): Promise<Partial<BrainstormIdea>[]> {
|
|
const prompt = `
|
|
Idée source : "${parentIdeaTitle} - ${parentIdeaDescription}"
|
|
Génère 3 idées d'extension ou de sous-aspects.
|
|
Format JSON.
|
|
`;
|
|
|
|
try {
|
|
const response = await ai.models.generateContent({
|
|
model: "gemini-3-flash-preview",
|
|
contents: [{ role: "user", parts: [{ text: prompt }] }],
|
|
config: {
|
|
systemInstruction: "Tu es un expert en brainstorming. Réponds uniquement en JSON valide.",
|
|
responseMimeType: "application/json",
|
|
responseSchema: BRAINSTORM_SCHEMA,
|
|
temperature: 1.0
|
|
}
|
|
});
|
|
|
|
const resText = response.text;
|
|
if (!resText) return [];
|
|
|
|
const parsed = JSON.parse(resText.replace(/^```json\n?/, '').replace(/\n?```$/, '').trim());
|
|
const ideas = Array.isArray(parsed.ideas) ? parsed.ideas : (Array.isArray(parsed) ? parsed : []);
|
|
|
|
return ideas.map((item: any) => ({
|
|
title: item.title,
|
|
description: item.description,
|
|
connectionToSeed: item.connection_to_seed,
|
|
noveltyScore: item.novelty_score
|
|
}));
|
|
} catch (error) {
|
|
console.error("Error generating expansion:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function getEmbedding(text: string): Promise<number[]> {
|
|
try {
|
|
const result = await ai.models.embedContent({
|
|
model: 'gemini-embedding-2-preview',
|
|
contents: [text],
|
|
});
|
|
return result.embeddings[0].values;
|
|
} catch (error) {
|
|
console.error("Error generating embedding:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export function cosineSimilarity(a: number[], b: number[]): number {
|
|
if (!a || !b || a.length !== b.length) return 0;
|
|
const dotProduct = a.reduce((sum, val, i) => sum + val * b[i], 0);
|
|
const magnitudeA = Math.sqrt(a.reduce((sum, val) => sum + val * val, 0));
|
|
const magnitudeB = Math.sqrt(b.reduce((sum, val) => sum + val * val, 0));
|
|
if (magnitudeA === 0 || magnitudeB === 0) return 0;
|
|
return dotProduct / (magnitudeA * magnitudeB);
|
|
}
|
|
|
|
export async function nameCluster(noteSummaries: string[]): Promise<string> {
|
|
const prompt = `Quel thème commun relie ces notes ? Donne un nom court (2-4 mots).\nNotes :\n${noteSummaries.join('\n- ')}`;
|
|
try {
|
|
const result = await ai.models.generateContent({
|
|
model: "gemini-3-flash-preview",
|
|
contents: prompt
|
|
});
|
|
return result.text.trim();
|
|
} catch (error) {
|
|
console.error("Error naming cluster:", error);
|
|
return "Thematic Cluster";
|
|
}
|
|
}
|
|
|
|
export async function suggestBridgeIdeas(
|
|
clusterAName: string,
|
|
clusterBName: string,
|
|
clusterASummaries: string,
|
|
clusterBSummaries: string
|
|
): Promise<any[]> {
|
|
const prompt = `
|
|
Cluster A (${clusterAName}) contient des notes sur : ${clusterASummaries}
|
|
Cluster B (${clusterBName}) contient des notes sur : ${clusterBSummaries}
|
|
|
|
Ces deux clusters ne sont pas connectés. Propose 3 idées
|
|
de "notes pont" qui pourraient créer un lien créatif entre eux.
|
|
Pour chaque idée : titre, description, pourquoi ça connecte les deux.
|
|
|
|
Format JSON.
|
|
`;
|
|
|
|
try {
|
|
const response = await ai.models.generateContent({
|
|
model: "gemini-3-flash-preview",
|
|
contents: prompt,
|
|
config: {
|
|
responseMimeType: "application/json",
|
|
responseSchema: SUGGESTIONS_SCHEMA
|
|
}
|
|
});
|
|
const parsed = JSON.parse(response.text);
|
|
return Array.isArray(parsed.suggestions) ? parsed.suggestions : [];
|
|
} catch (error) {
|
|
console.error("Error suggesting bridge ideas:", error);
|
|
return [];
|
|
}
|
|
}
|
|
export async function parseDocument(fileUrl: string, fileName: string): Promise<string> {
|
|
const prompt = `Extraits et résume le texte de ce document nommé "${fileName}".
|
|
Si c'est un PDF, ignore les éléments purement graphiques et concentre-toi sur le contenu sémantique.
|
|
Fais une extraction structurée.`;
|
|
|
|
try {
|
|
// In a real scenario, we would use media upload.
|
|
// Here we simulate the extraction.
|
|
const response = await ai.models.generateContent({
|
|
model: "gemini-3-flash-preview",
|
|
contents: [{ role: "user", parts: [{ text: prompt }] }],
|
|
config: {
|
|
systemInstruction: "Tu es un expert en extraction de texte et analyse de documents.",
|
|
temperature: 0.2
|
|
}
|
|
});
|
|
|
|
return response.text || "Échec de l'extraction du texte.";
|
|
} catch (error) {
|
|
console.error("Error parsing document:", error);
|
|
return "Erreur lors de l'analyse du document.";
|
|
}
|
|
}
|
|
|
|
export async function extractActionItems(notes: { title: string; content: string }[]): Promise<string> {
|
|
const notesContext = notes.map(n => `TITLE: ${n.title}\nCONTENT: ${n.content}`).join('\n\n---\n\n');
|
|
const prompt = `
|
|
Analyse les notes suivantes et extrais la liste des actions à accomplir (TODOs).
|
|
Pour chaque tâche, identifie si possible l'assigné et la date limite.
|
|
Présente le résultat sous forme d'un tableau Markdown structuré ou d'une liste claire.
|
|
Si aucune tâche n'est trouvée, indique-le.
|
|
|
|
Notes:
|
|
${notesContext}
|
|
`;
|
|
|
|
try {
|
|
const response = await ai.models.generateContent({
|
|
model: "gemini-3-flash-preview",
|
|
contents: prompt,
|
|
config: {
|
|
systemInstruction: "Tu es un agent spécialisé dans l'organisation et la gestion de tâches. Ton but est d'être précis et exhaustif.",
|
|
temperature: 0.1
|
|
}
|
|
});
|
|
|
|
return response.text;
|
|
} catch (error) {
|
|
console.error("Error extracting action items:", error);
|
|
return "Erreur lors de l'extraction des tâches.";
|
|
}
|
|
}
|
|
|
|
const FLASHCARDS_SCHEMA = {
|
|
type: Type.OBJECT,
|
|
properties: {
|
|
flashcards: {
|
|
type: Type.ARRAY,
|
|
items: {
|
|
type: Type.OBJECT,
|
|
properties: {
|
|
question: { type: Type.STRING },
|
|
answer: { type: Type.STRING }
|
|
},
|
|
required: ["question", "answer"]
|
|
}
|
|
}
|
|
},
|
|
required: ["flashcards"]
|
|
};
|
|
|
|
export async function generateFlashcardsForNote(
|
|
noteTitle: string,
|
|
noteContent: string
|
|
): Promise<{ question: string; answer: string }[]> {
|
|
const prompt = `
|
|
Titre de la note : "${noteTitle}"
|
|
Contenu de la note :
|
|
${noteContent}
|
|
|
|
Génère entre 4 et 8 flashcards (paires question/réponse) d'apprentissage basées sur le contenu ci-dessus.
|
|
|
|
Règles de style :
|
|
- Les questions doivent être claires et guider vers une révision active (ex: "Quelle est la particularité de... ?", "Pourquoi utilise-t-on... ?").
|
|
- Les réponses doivent être courtes et percutantes.
|
|
- Langue : Français.
|
|
- Format de retour : JSON correspondant au schéma.
|
|
`;
|
|
|
|
try {
|
|
const response = await ai.models.generateContent({
|
|
model: "gemini-3.5-flash",
|
|
contents: [{ role: "user", parts: [{ text: prompt }] }],
|
|
config: {
|
|
systemInstruction: "Tu es un assistant de révision agile. Tu convertis le contenu d'un cours ou d'une note en de superbes flashcards mémo-techniques.",
|
|
responseMimeType: "application/json",
|
|
responseSchema: FLASHCARDS_SCHEMA,
|
|
temperature: 0.7
|
|
}
|
|
});
|
|
|
|
const resText = response.text;
|
|
if (!resText) return [];
|
|
|
|
const parsed = JSON.parse(resText.replace(/^```json\n?/, '').replace(/\n?```$/, '').trim());
|
|
return Array.isArray(parsed.flashcards) ? parsed.flashcards : (Array.isArray(parsed) ? parsed : []);
|
|
} catch (error) {
|
|
console.error("Error generating flashcards with Gemini:", error);
|
|
return [];
|
|
}
|
|
}
|
|
|