JavaScript 56.7%
TypeScript 42.6%
1import Anthropic from "@anthropic-ai/sdk";2import { prisma } from "./prisma";3import { loadFileData } from "./storage";45const anthropic = new Anthropic({6 apiKey: process.env.ANTHROPIC_API_KEY,7});89const MODEL = "claude-opus-4-6";1011export async function aiSearch(query: string, userId?: string): Promise<{12 results: Array<{ fileId: string; name: string; relevance: string; snippet: string; folder?: string; mimeType?: string }>;13 answer: string;14}> {15 // Include files owned by user + legacy files with no owner (userId=null)16 const where: Record<string, unknown> = userId17 ? { OR: [{ userId }, { userId: null }] }18 : {};1920 const files = await prisma.file.findMany({21 where,22 select: { id: true, name: true, mimeType: true, size: true, aiTags: true, aiDescription: true, ocrText: true, createdAt: true, folder: { select: { name: true } } },23 take: 200,24 orderBy: { createdAt: "desc" },25 });2627 const fileList = files.map((f) => {28 const parts = [`- ${f.name} (${f.mimeType}, ID:${f.id})`];29 if (f.folder) parts.push(` Dossier: ${f.folder.name}`);30 if (f.aiDescription) parts.push(` Description: ${f.aiDescription}`);31 if (f.aiTags) parts.push(` Tags: ${f.aiTags}`);32 if (f.ocrText) parts.push(` OCR: ${f.ocrText.substring(0, 200)}`);33 return parts.join("\n");34 }).join("\n");3536 const response = await anthropic.messages.create({37 model: MODEL,38 max_tokens: 1024,39 messages: [{ role: "user", content: `Tu es un assistant de recherche de fichiers. Voici la liste des fichiers:\n\n${fileList}\n\nRecherche: "${query}"\n\nRéponds en JSON:\n{"results": [{"fileId": "...", "name": "...", "relevance": "high|medium|low", "snippet": "courte explication", "folder": "nom du dossier ou null", "mimeType": "..."}], "answer": "résumé"}` }],40 });4142 try {43 const text = response.content[0].type === "text" ? response.content[0].text : "";44 const jsonMatch = text.match(/\{[\s\S]*\}/);45 if (jsonMatch) return JSON.parse(jsonMatch[0]);46 } catch {}47 return { results: [], answer: "Aucun résultat trouvé." };48}4950export async function aiAutoTag(fileId: string): Promise<string[]> {51 const file = await prisma.file.findUnique({ where: { id: fileId }, select: { id: true, name: true, mimeType: true, size: true, ocrText: true, storagePath: true, isEncrypted: true } });52 if (!file) return [];5354 let contentHint = `Fichier: ${file.name}\nType: ${file.mimeType}\nTaille: ${Number(file.size)} bytes`;55 if (file.ocrText) contentHint += `\nContenu OCR: ${file.ocrText.substring(0, 500)}`;5657 if (file.mimeType.startsWith("text/") || file.mimeType.includes("json") || file.mimeType.includes("xml")) {58 try {59 contentHint += `\nContenu: ${(await loadFileData(file.storagePath, file.isEncrypted)).toString("utf-8").substring(0, 1000)}`;60 } catch {}61 }6263 const response = await anthropic.messages.create({64 model: MODEL,65 max_tokens: 256,66 messages: [{ role: "user", content: `Analyse ce fichier et suggère 3-5 tags pertinents en français.\n\n${contentHint}\n\nRéponds UNIQUEMENT avec un JSON array de strings. Ex: ["finance", "rapport", "2024"]` }],67 });6869 try {70 const text = response.content[0].type === "text" ? response.content[0].text : "";71 const match = text.match(/\[[\s\S]*\]/);72 if (match) {73 const tags = JSON.parse(match[0]) as string[];74 await prisma.file.update({ where: { id: fileId }, data: { aiTags: JSON.stringify(tags) } });75 return tags;76 }77 } catch {}78 return [];79}8081export async function aiDescribe(fileId: string): Promise<string> {82 const file = await prisma.file.findUnique({ where: { id: fileId }, select: { id: true, name: true, mimeType: true, storagePath: true, ocrText: true, isEncrypted: true } });83 if (!file) return "";8485 if (file.mimeType.startsWith("image/")) {86 try {87 {88 const base64 = (await loadFileData(file.storagePath, file.isEncrypted)).toString("base64");89 const mediaType = file.mimeType as "image/jpeg" | "image/png" | "image/gif" | "image/webp";90 const response = await anthropic.messages.create({91 model: MODEL,92 max_tokens: 512,93 messages: [{ role: "user", content: [{ type: "image", source: { type: "base64", media_type: mediaType, data: base64 } }, { type: "text", text: "Décris cette image en détail en français. Mentionne le sujet principal, les couleurs, le contexte et tout élément notable." }] }],94 });95 const desc = response.content[0].type === "text" ? response.content[0].text : "";96 await prisma.file.update({ where: { id: fileId }, data: { aiDescription: desc } });97 return desc;98 }99 } catch {}100 }101102 let hint = `Fichier: ${file.name}\nType: ${file.mimeType}`;103 if (file.mimeType.startsWith("text/") || file.mimeType.includes("json")) {104 try {105 hint += `\nContenu: ${(await loadFileData(file.storagePath, file.isEncrypted)).toString("utf-8").substring(0, 2000)}`;106 } catch {}107 }108 if (file.ocrText) hint += `\nOCR: ${file.ocrText.substring(0, 500)}`;109110 const response = await anthropic.messages.create({111 model: MODEL,112 max_tokens: 256,113 messages: [{ role: "user", content: `Décris ce fichier de manière utile en français (2-3 phrases):\n\n${hint}` }],114 });115 const desc = response.content[0].type === "text" ? response.content[0].text : "";116 await prisma.file.update({ where: { id: fileId }, data: { aiDescription: desc } });117 return desc;118}119120export async function aiOcr(fileId: string): Promise<string> {121 const file = await prisma.file.findUnique({ where: { id: fileId }, select: { id: true, name: true, mimeType: true, storagePath: true, isEncrypted: true } });122 if (!file) return "";123124 if (file.mimeType.startsWith("image/")) {125 try {126 const base64 = (await loadFileData(file.storagePath, file.isEncrypted)).toString("base64");127 const mediaType = file.mimeType as "image/jpeg" | "image/png" | "image/gif" | "image/webp";128 const response = await anthropic.messages.create({129 model: MODEL,130 max_tokens: 4096,131 messages: [{ role: "user", content: [{ type: "image", source: { type: "base64", media_type: mediaType, data: base64 } }, { type: "text", text: "Extrais tout le texte visible dans cette image de manière précise. Préserve la structure (titres, listes, tableaux). Retourne uniquement le texte extrait, sans commentaire." }] }],132 });133 const text = response.content[0].type === "text" ? response.content[0].text : "";134 await prisma.file.update({ where: { id: fileId }, data: { ocrText: text } });135 return text;136 } catch {}137 }138139 if (file.mimeType === "application/pdf") {140 try {141 const base64 = (await loadFileData(file.storagePath, file.isEncrypted)).toString("base64");142 const response = await anthropic.messages.create({143 model: MODEL,144 max_tokens: 8192,145 messages: [{ role: "user", content: [{ type: "document", source: { type: "base64", media_type: "application/pdf", data: base64 } }, { type: "text", text: "Extrais tout le texte de ce document PDF en préservant la structure. Retourne uniquement le texte extrait." }] as Anthropic.ContentBlockParam[] }],146 });147 const text = response.content[0].type === "text" ? response.content[0].text : "";148 await prisma.file.update({ where: { id: fileId }, data: { ocrText: text } });149 return text;150 } catch {}151 }152 return "";153}154155export type ChatMessage = { role: "user" | "assistant"; content: string };156157export async function aiChat(message: string, history: ChatMessage[] = [], userId?: string): Promise<string> {158 let fileContext = "";159 if (userId) {160 const where = { OR: [{ userId }, { userId: null }] };161 const recentFiles = await prisma.file.findMany({162 where,163 select: { name: true, mimeType: true, aiTags: true, aiDescription: true, folder: { select: { name: true } } },164 take: 30,165 orderBy: { createdAt: "desc" },166 });167 const totalCount = await prisma.file.count({ where });168 fileContext = `\nL'utilisateur possède ${totalCount} fichier(s) au total. Voici les 30 plus récents:\n` +169 recentFiles.map(f =>170 `- ${f.name} (${f.mimeType})${f.folder ? ` [dossier: ${f.folder.name}]` : ""}${f.aiDescription ? ` — ${f.aiDescription}` : ""}${f.aiTags ? ` [tags: ${f.aiTags}]` : ""}`171 ).join("\n");172 }173174 const response = await anthropic.messages.create({175 model: MODEL,176 max_tokens: 2048,177 system: `Tu es l'assistant IA de SPB Cloud, un cloud personnel privé. Tu aides l'utilisateur avec ses fichiers, réponds en français, et tu es concis et utile.${fileContext}`,178 messages: [179 ...history,180 { role: "user", content: message },181 ],182 });183 return response.content[0].type === "text" ? response.content[0].text : "";184}185186export async function buildFileContext(userId: string): Promise<string> {187 // Include files owned by user + legacy files with no owner (userId=null)188 const where = { OR: [{ userId }, { userId: null }] };189 const recentFiles = await prisma.file.findMany({190 where,191 select: { name: true, mimeType: true, aiTags: true, aiDescription: true, ocrText: true, folder: { select: { name: true } } },192 take: 30,193 orderBy: { createdAt: "desc" },194 });195 const totalCount = await prisma.file.count({ where });196 return `L'utilisateur possède ${totalCount} fichier(s) au total. Voici les 30 plus récents:\n` +197 recentFiles.map(f => {198 const parts = [`- ${f.name} (${f.mimeType})`];199 if (f.folder) parts.push(`[dossier: ${f.folder.name}]`);200 if (f.aiDescription) parts.push(`— ${f.aiDescription}`);201 if (f.aiTags) parts.push(`[tags: ${f.aiTags}]`);202 if (f.ocrText) parts.push(`[OCR dispo: ${f.ocrText.substring(0, 150)}...]`);203 return parts.join(" ");204 }).join("\n");205}206