UI Stabilization: Global color theme updates (#75B2D6), AI Assistant styling refactor, and navigation fixes
This commit is contained in:
@@ -30,11 +30,12 @@ export async function POST(req: NextRequest) {
|
||||
const userId = session.user.id
|
||||
|
||||
const body = await req.json()
|
||||
const { noteId, type, theme, style } = body as {
|
||||
const { noteId, type, theme, style, language } = body as {
|
||||
noteId: string
|
||||
type: GenerateType
|
||||
theme?: string
|
||||
style?: string
|
||||
language?: string
|
||||
}
|
||||
|
||||
if (!noteId || !type || !TYPE_DEFAULTS[type]) {
|
||||
@@ -50,15 +51,26 @@ export async function POST(req: NextRequest) {
|
||||
}
|
||||
|
||||
const defaults = TYPE_DEFAULTS[type]
|
||||
const isEn = language === 'English'
|
||||
|
||||
let role = defaults.role
|
||||
if (isEn) {
|
||||
if (type === 'slide-generator') {
|
||||
role = 'Create a professional and visual PowerPoint presentation from the provided note content.'
|
||||
} else {
|
||||
role = 'Generate a clear and professional Excalidraw diagram from the provided note content.'
|
||||
}
|
||||
}
|
||||
|
||||
const agentName = type === 'slide-generator'
|
||||
? `Slides — ${(note.title || 'Note').substring(0, 40)}`
|
||||
: `Diagramme — ${(note.title || 'Note').substring(0, 40)}`
|
||||
? `${isEn ? 'Slides' : 'Présentation'} — ${(note.title || 'Note').substring(0, 40)}`
|
||||
: `${isEn ? 'Diagram' : 'Diagramme'} — ${(note.title || 'Note').substring(0, 40)}`
|
||||
|
||||
const agent = await prisma.agent.create({
|
||||
data: {
|
||||
name: agentName,
|
||||
type,
|
||||
role: defaults.role,
|
||||
role,
|
||||
tools: JSON.stringify(defaults.tools),
|
||||
maxSteps: defaults.maxSteps,
|
||||
frequency: 'one-shot',
|
||||
|
||||
@@ -10,7 +10,7 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { existingContent, resourceText, mode, language } = await request.json()
|
||||
const { existingContent, resourceText, mode, language, format } = await request.json()
|
||||
|
||||
if (!resourceText || typeof resourceText !== 'string') {
|
||||
return NextResponse.json({ error: 'resourceText is required' }, { status: 400 })
|
||||
@@ -20,6 +20,7 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
const lang = language || 'fr'
|
||||
const outputFormat = format === 'html' ? 'HTML (with proper tags like <h2>, <p>, <ul>, <li>)' : 'Markdown (with ##, -, **, etc.)'
|
||||
const config = await getSystemConfig()
|
||||
const provider = getTagsProvider(config)
|
||||
|
||||
@@ -30,6 +31,7 @@ export async function POST(request: NextRequest) {
|
||||
prompt = `You are an expert note editor. Your task is to enrich an existing note by adding relevant information from a provided resource, WITHOUT modifying or rewriting the existing content.
|
||||
|
||||
LANGUAGE RULE: Respond in ${lang}. Match the language of the existing note.
|
||||
FORMAT RULE: Respond in ${outputFormat}.
|
||||
|
||||
EXISTING NOTE:
|
||||
---
|
||||
@@ -46,13 +48,14 @@ INSTRUCTIONS:
|
||||
- Append ONLY new, non-redundant information from the resource below the existing content
|
||||
- Use a clear separator (e.g., "---" or a new section heading) between existing and new content
|
||||
- Skip information already covered in the existing note
|
||||
- Format the new content consistently with the existing note style
|
||||
- Format the new content consistently with the existing note style and the requested FORMAT RULE
|
||||
- Respond ONLY with the enriched note content, no explanations`
|
||||
} else {
|
||||
// Merge: intelligently rewrite integrating both sources
|
||||
prompt = `You are an expert note writer. Your task is to intelligently merge an existing note with a resource into a single, coherent, well-structured document.
|
||||
|
||||
LANGUAGE RULE: Respond in ${lang}. Match the language of the existing note.
|
||||
FORMAT RULE: Respond in ${outputFormat}.
|
||||
|
||||
EXISTING NOTE:
|
||||
---
|
||||
@@ -69,7 +72,7 @@ INSTRUCTIONS:
|
||||
- Eliminate redundancy — include each piece of information only once
|
||||
- Preserve the key ideas from both sources
|
||||
- Maintain a logical structure with clear headings if appropriate
|
||||
- Keep the tone and style consistent
|
||||
- Keep the tone and style consistent with the requested FORMAT RULE
|
||||
- Respond ONLY with the merged content, no meta-commentary or explanations`
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import { prisma } from '@/lib/prisma'
|
||||
import { auth } from '@/auth'
|
||||
import { loadTranslations, getTranslationValue, SupportedLanguage } from '@/lib/i18n'
|
||||
import { toolRegistry } from '@/lib/ai/tools'
|
||||
import { stepCountIs } from 'ai'
|
||||
import { readFile } from 'fs/promises'
|
||||
import path from 'path'
|
||||
|
||||
@@ -47,36 +46,32 @@ export async function POST(req: Request) {
|
||||
}
|
||||
const userId = session.user.id
|
||||
|
||||
// 2. Parse request body — messages arrive as UIMessage[] from DefaultChatTransport
|
||||
// 2. Parse request body
|
||||
const body = await req.json()
|
||||
|
||||
const { messages: rawMessages, conversationId, notebookId, language, webSearch, noteContext } = body as {
|
||||
const { messages: rawMessages, conversationId, notebookId, language, webSearch, noteContext, format } = body as {
|
||||
messages: UIMessage[]
|
||||
conversationId?: string
|
||||
notebookId?: string
|
||||
language?: string
|
||||
webSearch?: boolean
|
||||
noteContext?: { title: string; content: string; tone: string; images?: string[] }
|
||||
format?: 'html' | 'markdown'
|
||||
}
|
||||
|
||||
// Convert UIMessages to CoreMessages for streamText
|
||||
const incomingMessages = toCoreMessages(rawMessages)
|
||||
|
||||
// 3. Manage conversation (create or fetch)
|
||||
// 3. Manage conversation
|
||||
let conversation: { id: string; messages: Array<{ role: string; content: string }> }
|
||||
|
||||
if (conversationId) {
|
||||
const existing = await prisma.conversation.findUnique({
|
||||
where: { id: conversationId, userId },
|
||||
include: { messages: { orderBy: { createdAt: 'asc' } } },
|
||||
})
|
||||
if (!existing) {
|
||||
return new Response('Conversation not found', { status: 404 })
|
||||
}
|
||||
if (!existing) return new Response('Conversation not found', { status: 404 })
|
||||
conversation = existing
|
||||
} else {
|
||||
const userMessage = incomingMessages[incomingMessages.length - 1]?.content || 'New conversation'
|
||||
const created = await prisma.conversation.create({
|
||||
conversation = await prisma.conversation.create({
|
||||
data: {
|
||||
userId,
|
||||
notebookId: notebookId || null,
|
||||
@@ -84,33 +79,21 @@ export async function POST(req: Request) {
|
||||
},
|
||||
include: { messages: true },
|
||||
})
|
||||
conversation = created
|
||||
}
|
||||
|
||||
// 4. RAG retrieval
|
||||
const currentMessage = incomingMessages[incomingMessages.length - 1]?.content || ''
|
||||
|
||||
// Load translations for the requested language
|
||||
const lang = (language || 'en') as SupportedLanguage
|
||||
const translations = await loadTranslations(lang)
|
||||
const untitledText = getTranslationValue(translations, 'notes.untitled') || 'Untitled'
|
||||
|
||||
// If a notebook is selected, fetch its recent notes directly as context
|
||||
// This ensures the AI always has access to the notebook content,
|
||||
// even for vague queries like "what's in this notebook?"
|
||||
let notebookContext = ''
|
||||
let searchNotes = ''
|
||||
|
||||
// When scope is "this note" (noteContext present), skip RAG retrieval entirely
|
||||
// The note content is already injected as copilotContext below
|
||||
if (!noteContext) {
|
||||
if (notebookId) {
|
||||
const notebookNotes = await prisma.note.findMany({
|
||||
where: {
|
||||
notebookId,
|
||||
userId,
|
||||
trashedAt: null,
|
||||
},
|
||||
where: { notebookId, userId, trashedAt: null },
|
||||
orderBy: { updatedAt: 'desc' },
|
||||
take: 20,
|
||||
select: { id: true, title: true, content: true, updatedAt: true },
|
||||
@@ -122,7 +105,6 @@ export async function POST(req: Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// Also run semantic search for the specific query
|
||||
let searchResults: any[] = []
|
||||
try {
|
||||
searchResults = await semanticSearchService.search(currentMessage, {
|
||||
@@ -131,21 +113,16 @@ export async function POST(req: Request) {
|
||||
threshold: notebookId ? 0.3 : 0.5,
|
||||
defaultTitle: untitledText,
|
||||
})
|
||||
} catch {
|
||||
// Search failure should not block chat
|
||||
}
|
||||
} catch {}
|
||||
|
||||
searchNotes = searchResults
|
||||
.map((r) => `NOTE [${r.title || untitledText}]: ${r.content}`)
|
||||
.join('\n\n---\n\n')
|
||||
}
|
||||
|
||||
// Combine: full notebook context + semantic search results (deduplicated)
|
||||
const contextNotes = [notebookContext, searchNotes].filter(Boolean).join('\n\n---\n\n')
|
||||
|
||||
// 5. System prompt synthesis with RAG context
|
||||
// Language-aware prompts to avoid forcing French responses
|
||||
// Note: lang is already declared above when loading translations
|
||||
// 5. System prompt synthesis
|
||||
const promptLang: Record<string, { contextWithNotes: string; contextNoNotes: string; system: string }> = {
|
||||
en: {
|
||||
contextWithNotes: `## User's notes\n\n${contextNotes}\n\nWhen using info from the notes above, cite the source note title in parentheses, e.g.: "Deployment is done via Docker (💻 Development Guide)". Don't copy word for word — rephrase. If the notes don't cover the topic, say so and supplement with your general knowledge.`,
|
||||
@@ -153,14 +130,24 @@ export async function POST(req: Request) {
|
||||
system: `You are the AI assistant of Memento. The user asks you questions about their projects, technical docs, and notes. You must respond in a structured and helpful way.
|
||||
|
||||
## Format rules
|
||||
- Use markdown freely: headings (##, ###), lists, code blocks, bold, tables — anything that makes the response readable.
|
||||
- ${format === 'html' ? `Respond MANDATORILY using valid HTML fragments (e.g., <p>, <strong>, <em>, <ul>, <li>, <h3>, <table>, <tr>, <td>).
|
||||
- Do NOT use Markdown symbols (no #, *, -, etc.).
|
||||
- Do not wrap your HTML code in a Markdown code block.` : 'Use markdown freely: headings (##, ###), lists, code blocks, bold, tables — anything that makes the response readable.'}
|
||||
- Structure your response with sections for technical questions or complex topics.
|
||||
- For simple, short questions, a direct paragraph is enough.
|
||||
- For simple, short questions, a direct paragraph is enough.` + (format === 'html' ? `
|
||||
|
||||
## HTML OUTPUT EXAMPLE
|
||||
<h3>Section Title</h3>
|
||||
<p>Here is an explanation with <strong>bold text</strong> and a list:</p>
|
||||
<ul>
|
||||
<li>First important point</li>
|
||||
<li>Second important point</li>
|
||||
</ul>` : '') + `
|
||||
|
||||
## Tone rules
|
||||
- Natural tone, neither corporate nor too casual.
|
||||
- No unnecessary intro phrases ("Here's what I found", "Based on your notes"). Answer directly.
|
||||
- No upsell questions at the end ("Would you like me to...", "Do you want..."). If you have useful additional info, just give it.
|
||||
- No unnecessary intro phrases. Answer directly.
|
||||
- No upsell questions at the end. If you have useful additional info, just give it.
|
||||
- If the user says "Momento" they mean Momento (this app).
|
||||
|
||||
## About Momento
|
||||
@@ -170,171 +157,90 @@ Momento is an intelligent note-taking application. Key features include:
|
||||
- **Search**: Advanced semantic search to find notes by meaning, not just keywords, and Web Search integration.
|
||||
- **Agents**: Create specialized AI Agents with custom system prompts for specific recurring tasks.
|
||||
- **Lab**: Experimental AI tools for data analysis and deeper insights.
|
||||
If the user asks how to use this tool, explain these features simply and helpfully.
|
||||
|
||||
## Available tools
|
||||
You have access to these tools for deeper research:
|
||||
- **note_search**: Search the user's notes by keyword or meaning. Use when the initial context above is insufficient or when the user asks about specific content in their notes. If a notebook is selected, pass its ID to restrict results.
|
||||
- **note_read**: Read a specific note by ID. Use when note_search returns a note you need the full content of.
|
||||
- **web_search**: Search the web for information. Use when the user asks about something not in their notes.
|
||||
- **web_scrape**: Scrape a web page and return its content as markdown. Use when web_search returns a URL you need to read.
|
||||
|
||||
## Tool usage rules
|
||||
- You already have context from the user's notes above. Only use tools if you need more specific or additional information.
|
||||
- Never invent note IDs, URLs, or notebook IDs. Use the IDs provided in the context or from tool results.
|
||||
- For simple conversational questions (greetings, opinions, general knowledge), answer directly without using any tools.`,
|
||||
You have access to: note_search, note_read, web_search, web_scrape.
|
||||
Only use tools if you need more information. Never invent note IDs or URLs.`,
|
||||
},
|
||||
fr: {
|
||||
contextWithNotes: `## Notes de l'utilisateur\n\n${contextNotes}\n\nQuand tu utilises une info venant des notes ci-dessus, cite le titre de la note source entre parenthèses, ex: "Le déploiement se fait via Docker (💻 Development Guide)". Ne recopie pas mot pour mot — reformule. Si les notes ne couvrent pas le sujet, dis-le et complète avec tes connaissances générales.`,
|
||||
contextWithNotes: `## Notes de l'utilisateur\n\n${contextNotes}\n\nQuand tu utilises une info venant des notes ci-dessus, cite le titre de la note source entre parenthèses, ex: "Le déploiement se fait via Docker (💻 Development Guide)". Ne recopie pas mot pour mot — reformule.`,
|
||||
contextNoNotes: "Aucune note pertinente trouvée pour cette question. Réponds avec tes connaissances générales.",
|
||||
system: `Tu es l'assistant IA de Memento. L'utilisateur te pose des questions sur ses projets, sa doc technique, ses notes. Tu dois répondre de façon structurée et utile.
|
||||
|
||||
## Règles de format
|
||||
- Utilise le markdown librement : titres (##, ###), listes, code blocks, gras, tables — tout ce qui rend la réponse lisible.
|
||||
- ${format === 'html' ? `Réponds OBLIGATOIREMENT en utilisant des fragments HTML valides (ex: <p>, <strong>, <em>, <ul>, <li>, <h3>, <table>, <tr>, <td>).
|
||||
- N'utilise PAS de symboles Markdown.
|
||||
- Ne mets pas ton code HTML dans un bloc de code Markdown.` : '- Utilise le markdown librement : titres (##, ###), listes, code blocks, gras, tables.'}
|
||||
- Structure ta réponse avec des sections quand c'est une question technique ou un sujet complexe.
|
||||
- Pour les questions simples et courtes, un paragraphe direct suffit.
|
||||
- Pour les questions simples et courtes, un paragraphe direct suffit.` + (format === 'html' ? `
|
||||
|
||||
## EXEMPLE DE SORTIE HTML
|
||||
<h3>Titre de section</h3>
|
||||
<p>Voici une explication avec du <strong>texte en gras</strong> et une liste :</p>
|
||||
<ul>
|
||||
<li>Premier point important</li>
|
||||
<li>Deuxième point important</li>
|
||||
</ul>` : '') + `
|
||||
|
||||
## Règles de ton
|
||||
- Ton naturel, ni corporate ni trop familier.
|
||||
- Pas de phrase d'intro inutile ("Voici ce que j'ai trouvé", "Basé sur vos notes"). Réponds directement.
|
||||
- Pas de question upsell à la fin ("Souhaitez-vous que je...", "Acceptez-vous que..."). Si tu as une info complémentaire utile, donne-la.
|
||||
- Ton naturel, direct, sans phrases d'intro inutiles.
|
||||
- Pas de question upsell à la fin.
|
||||
- Si l'utilisateur dit "Momento" il parle de Momento (cette application).
|
||||
|
||||
## À propos de Momento
|
||||
Momento est une application de prise de notes intelligente. Ses fonctionnalités principales :
|
||||
- **Éditeur de notes** : Prise de notes en Markdown riche avec un Copilot IA intégré pour réécrire, résumer ou traduire du texte.
|
||||
- **Organisation** : Regroupement des notes dans des Carnets (Notebooks) et utilisation d'Étiquettes (Labels).
|
||||
- **Recherche** : Recherche sémantique avancée pour trouver des notes par le sens, et recherche Web intégrée.
|
||||
- **Agents** : Création d'Agents IA spécialisés avec des instructions personnalisées pour des tâches récurrentes.
|
||||
- **Lab** : Outils IA expérimentaux pour l'analyse de données et les insights.
|
||||
Si l'utilisateur demande comment utiliser cet outil, explique ces fonctionnalités simplement et avec bienveillance.
|
||||
Momento est une application de prise de notes intelligente. Ses fonctionnalités : Éditeur Markdown riche, Copilot IA, Organisation par Carnets, Recherche sémantique, Agents IA, Lab.
|
||||
|
||||
## Outils disponibles
|
||||
Tu as accès à ces outils pour des recherches approfondies :
|
||||
- **note_search** : Cherche dans les notes de l'utilisateur par mot-clé ou sens. Utilise quand le contexte initial ci-dessus est insuffisant ou quand l'utilisateur demande du contenu spécifique dans ses notes. Si un carnet est sélectionné, passe son ID pour restreindre les résultats.
|
||||
- **note_read** : Lit une note spécifique par son ID. Utilise quand note_search retourne une note dont tu as besoin du contenu complet.
|
||||
- **web_search** : Recherche sur le web. Utilise quand l'utilisateur demande quelque chose qui n'est pas dans ses notes.
|
||||
- **web_scrape** : Scrape une page web et retourne son contenu en markdown. Utilise quand web_search retourne une URL que tu veux lire.
|
||||
|
||||
## Règles d'utilisation des outils
|
||||
- Tu as déjà du contexte des notes de l'utilisateur ci-dessus. Utilise les outils seulement si tu as besoin d'informations plus spécifiques.
|
||||
- N'invente jamais d'IDs de notes, d'URLs ou d'IDs de carnet. Utilise les IDs fournis dans le contexte ou les résultats d'outils.
|
||||
- Pour les questions conversationnelles simples (salutations, opinions, connaissances générales), réponds directement sans utiliser d'outils.`,
|
||||
Tu as accès à : note_search, note_read, web_search, web_scrape.`,
|
||||
},
|
||||
fa: {
|
||||
contextWithNotes: `## یادداشتهای کاربر\n\n${contextNotes}\n\nهنگام استفاده از اطلاعات یادداشتهای بالا، عنوان یادداشت منبع را در پرانتز ذکر کنید. کپی نکنید — بازنویسی کنید. اگر یادداشتها موضوع را پوشش نمیدهند، بگویید و با دانش عمومی خود تکمیل کنید.`,
|
||||
contextWithNotes: `## یادداشتهای کاربر\n\n${contextNotes}\n\nهنگام استفاده از اطلاعات یادداشتهای بالا، عنوان یادداشت منبع را در پرانتز ذکر کنید.`,
|
||||
contextNoNotes: "هیچ یادداشت مرتبطی برای این سؤال یافت نشد. با دانش عمومی خود پاسخ دهید.",
|
||||
system: `شما دستیار هوش مصنوعی Memento هستید. کاربر از شما درباره پروژهها، مستندات فنی و یادداشتهایش سؤال میکند. باید به شکلی ساختاریافته و مفید پاسخ دهید.
|
||||
|
||||
## قوانین قالببندی
|
||||
- از مارکداون آزادانه استفاده کنید: عناوین (##, ###)، لیستها، بلوکهای کد، پررنگ، جداول.
|
||||
- ${format === 'html' ? `حتماً از تگهای HTML معتبر استفاده کنید (مانند <p>, <strong>, <em>, <ul>, <li>, <h3>).
|
||||
- از نمادهای مارکداون استفاده نکنید.` : 'از مارکداون آزادانه استفاده کنید: عناوین (##, ###)، لیستها، بلوکهای کد، پررنگ، جداول.'}
|
||||
- برای سؤالات فنی یا موضوعات پیچیده، پاسخ خود را بخشبندی کنید.
|
||||
- برای سؤالات ساده و کوتاه، یک پاراگراف مستقیم کافی است.
|
||||
- برای سؤالات ساده و کوتاه، یک پاراگراف مستقیم کافی است.` + (format === 'html' ? `
|
||||
|
||||
## نمونه خروجی HTML
|
||||
<h3>عنوان بخش</h3>
|
||||
<p>این یک توضیح با <strong>متن برجسته</strong> و یک لیست است:</p>
|
||||
<ul>
|
||||
<li>نکته اول</li>
|
||||
<li>نکته دوم</li>
|
||||
</ul>` : '') + `
|
||||
|
||||
## قوانین لحن
|
||||
- لحن طبیعی، نه رسمی بیش از حد و نه خیلی غیررسمی.
|
||||
- بدون جمله مقدمه اضافی. مستقیم پاسخ دهید.
|
||||
- بدون سؤال فروشی در انتها. اگر اطلاعات تکمیلی مفید دارید، مستقیم بدهید.
|
||||
- اگر کاربر "Momento" میگوید، منظورش Memento (این برنامه) است.
|
||||
|
||||
## ابزارهای موجود
|
||||
- **note_search**: جستجو در یادداشتهای کاربر با کلیدواژه یا معنی. زمانی استفاده کنید که زمینه اولیه کافی نباشد. اگر دفترچه انتخاب شده، شناسه آن را ارسال کنید.
|
||||
- **note_read**: خواندن یک یادداشت خاص با شناسه. زمانی استفاده کنید که note_search یادداشتی برگرداند که محتوای کامل آن را نیاز دارید.
|
||||
- **web_search**: جستجو در وب. زمانی استفاده کنید که کاربر درباره چیزی خارج از یادداشتهایش میپرسد.
|
||||
- **web_scrape**: استخراج محتوای صفحه وب. زمانی استفاده کنید که web_search نشانیای برگرداند که میخواهید بخوانید.
|
||||
|
||||
## قوانین استفاده از ابزارها
|
||||
- شما از قبل زمینهای از یادداشتهای کاربر دارید. فقط در صورت نیاز به اطلاعات بیشتر از ابزارها استفاده کنید.
|
||||
- هرگز شناسه یادداشت، نشانی یا شناسه دفترچه نسازید. از شناسههای موجود در زمینه یا نتایج ابزار استفاده کنید.
|
||||
- برای سؤالات مکالمهای ساده (سلام، نظرات، دانش عمومی)، مستقیم پاسخ دهید.`,
|
||||
- لحن طبیعی، مستقیم، بدون مقدمه اضافی.
|
||||
- اگر کاربر "Momento" میگوید، منظورش Memento (این برنامه) است.`,
|
||||
},
|
||||
es: {
|
||||
contextWithNotes: `## Notas del usuario\n\n${contextNotes}\n\nCuando uses información de las notas anteriores, cita el título de la nota fuente entre paréntesis. No copies palabra por palabra — reformula. Si las notas no cubren el tema, dilo y complementa con tu conocimiento general.`,
|
||||
contextWithNotes: `## Notas del usuario\n\n${contextNotes}\n\nCuando uses información de las notas anteriores, cita el título de la nota fuente entre paréntesis.`,
|
||||
contextNoNotes: "No se encontraron notas relevantes para esta pregunta. Responde con tu conocimiento general.",
|
||||
system: `Eres el asistente de IA de Memento. El usuario te hace preguntas sobre sus proyectos, documentación técnica y notas. Debes responder de forma estructurada y útil.
|
||||
system: `Eres el asistente de IA de Memento. El usuario te hace preguntas sobre sus proyectos, documentación técnica y notas.
|
||||
|
||||
## Reglas de formato
|
||||
- Usa markdown libremente: títulos (##, ###), listas, bloques de código, negritas, tablas.
|
||||
- Estructura tu respuesta con secciones para preguntas técnicas o temas complejos.
|
||||
- Para preguntas simples y cortas, un párrafo directo es suficiente.
|
||||
- ${format === 'html' ? `Responde OBLIGATORIAMENTE usando fragmentos HTML válidos (ej: <p>, <strong>, <em>, <ul>, <li>, <h3>, <table>, <tr>, <td>).
|
||||
- NO uses símbolos Markdown.` : 'Usa markdown libremente: títulos (##, ###), listas, negritas, tablas.'}
|
||||
- Estructura tu respuesta con secciones para temas complejos.
|
||||
- Para preguntas simples, un párrafo directo es suficiente.` + (format === 'html' ? `
|
||||
|
||||
## Reglas de tono
|
||||
- Tono natural, ni corporativo ni demasiado informal.
|
||||
- Sin frases de introducción innecesarias. Responde directamente.
|
||||
- Sin preguntas de venta al final. Si tienes información complementaria útil, dala directamente.
|
||||
|
||||
## Herramientas disponibles
|
||||
- **note_search**: Busca en las notas del usuario por palabra clave o significado. Úsalo cuando el contexto inicial sea insuficiente. Si hay una libreta seleccionada, pasa su ID para restringir los resultados.
|
||||
- **note_read**: Lee una nota específica por su ID. Úsalo cuando note_search devuelva una nota cuyo contenido completo necesites.
|
||||
- **web_search**: Busca en la web. Úsalo cuando el usuario pregunte sobre algo que no está en sus notas.
|
||||
- **web_scrape**: Extrae el contenido de una página web como markdown. Úsalo cuando web_search devuelva una URL que quieras leer.
|
||||
|
||||
## Reglas de uso de herramientas
|
||||
- Ya tienes contexto de las notas del usuario arriba. Solo usa herramientas si necesitas información más específica.
|
||||
- Nunca inventes IDs de notas, URLs o IDs de libreta. Usa los IDs proporcionados en el contexto o en los resultados de herramientas.
|
||||
- Para preguntas conversacionales simples (saludos, opiniones, conocimiento general), responde directamente sin herramientas.`,
|
||||
},
|
||||
de: {
|
||||
contextWithNotes: `## Notizen des Benutzers\n\n${contextNotes}\n\nWenn du Infos aus den obigen Notizen verwendest, zitiere den Titel der Quellnotiz in Klammern. Nicht Wort für Wort kopieren — umformulieren. Wenn die Notizen das Thema nicht abdecken, sag es und ergänze mit deinem Allgemeinwissen.`,
|
||||
contextNoNotes: "Keine relevanten Notizen für diese Frage gefunden. Antworte mit deinem Allgemeinwissen.",
|
||||
system: `Du bist der KI-Assistent von Memento. Der Benutzer stellt dir Fragen zu seinen Projekten, technischen Dokumentationen und Notizen. Du musst strukturiert und hilfreich antworten.
|
||||
|
||||
## Formatregeln
|
||||
- Verwende Markdown frei: Überschriften (##, ###), Listen, Code-Blöcke, Fettdruck, Tabellen.
|
||||
- Strukturiere deine Antwort mit Abschnitten bei technischen Fragen oder komplexen Themen.
|
||||
- Bei einfachen, kurzen Fragen reicht ein direkter Absatz.
|
||||
|
||||
## Tonregeln
|
||||
- Natürlicher Ton, weder zu geschäftsmäßig noch zu umgangssprachlich.
|
||||
- Keine unnötigen Einleitungssätze. Antworte direkt.
|
||||
- Keine Upsell-Fragen am Ende. Gib nützliche Zusatzinfos einfach direkt.
|
||||
|
||||
## Verfügbare Werkzeuge
|
||||
- **note_search**: Durchsuche die Notizen des Benutzers nach Schlagwort oder Bedeutung. Verwende es, wenn der obige Kontext unzureichend ist. Wenn ein Notizbuch ausgewählt ist, gib dessen ID an, um die Ergebnisse einzuschränken.
|
||||
- **note_read**: Lese eine bestimmte Notiz anhand ihrer ID. Verwende es, wenn note_search eine Notiz zurückgibt, deren vollständigen Inhalt du benötigst.
|
||||
- **web_search**: Suche im Web. Verwende es, wenn der Benutzer nach etwas fragt, das nicht in seinen Notizen steht.
|
||||
- **web_scrape**: Lese eine Webseite und gib den Inhalt als Markdown zurück. Verwende es, wenn web_search eine URL zurückgibt, die du lesen möchtest.
|
||||
|
||||
## Werkzeugregeln
|
||||
- Du hast bereits Kontext aus den Notizen des Benutzers oben. Verwende Werkzeuge nur, wenn du spezifischere Informationen benötigst.
|
||||
- Erfinde niemals Notiz-IDs, URLs oder Notizbuch-IDs. Verwende die im Kontext oder in Werkzeugergebnissen bereitgestellten IDs.
|
||||
- Bei einfachen Gesprächsfragen (Begrüßungen, Meinungen, Allgemeinwissen) antworte direkt ohne Werkzeuge.`,
|
||||
},
|
||||
it: {
|
||||
contextWithNotes: `## Note dell'utente\n\n${contextNotes}\n\nQuando usi informazioni dalle note sopra, cita il titolo della nota fonte tra parentesi. Non copiare parola per parola — riformula. Se le note non coprono l'argomento, dillo e integra con la tua conoscenza generale.`,
|
||||
contextNoNotes: "Nessuna nota rilevante trovata per questa domanda. Rispondi con la tua conoscenza generale.",
|
||||
system: `Sei l'assistente IA di Memento. L'utente ti fa domande sui suoi progetti, documentazione tecnica e note. Devi rispondere in modo strutturato e utile.
|
||||
|
||||
## Regole di formato
|
||||
- Usa markdown liberamente: titoli (##, ###), elenchi, blocchi di codice, grassetto, tabelle.
|
||||
- Struttura la risposta con sezioni per domande tecniche o argomenti complessi.
|
||||
- Per domande semplici e brevi, un paragrafo diretto basta.
|
||||
|
||||
## Regole di tono
|
||||
- Tono naturale, né aziendale né troppo informale.
|
||||
- Nessuna frase introduttiva non necessaria. Rispondi direttamente.
|
||||
- Nessuna domanda di upsell alla fine. Se hai informazioni aggiuntive utili, dalle direttamente.
|
||||
|
||||
## Strumenti disponibili
|
||||
- **note_search**: Cerca nelle note dell'utente per parola chiave o significato. Usa quando il contesto iniziale è insufficiente. Se un quaderno è selezionato, passa il suo ID per restringere i risultati.
|
||||
- **note_read**: Leggi una nota specifica per ID. Usa quando note_search restituisce una nota di cui hai bisogno del contenuto completo.
|
||||
- **web_search**: Cerca sul web. Usa quando l'utente chiede qualcosa che non è nelle sue note.
|
||||
- **web_scrape**: Estrai il contenuto di una pagina web come markdown. Usa quando web_search restituisce un URL che vuoi leggere.
|
||||
|
||||
## Regole di utilizzo degli strumenti
|
||||
- Hai già contesto dalle note dell'utente sopra. Usa gli strumenti solo se hai bisogno di informazioni più specifiche.
|
||||
- Non inventare mai ID di note, URL o ID di quaderno. Usa gli ID forniti nel contesto o nei risultati degli strumenti.
|
||||
- Per domande conversazionali semplici (saluti, opinioni, conoscenza generale), rispondi direttamente senza strumenti.`,
|
||||
## EJEMPLO DE SALIDA HTML
|
||||
<h3>Título de sección</h3>
|
||||
<p>Aquí hay una explicación con <strong>texto en negrita</strong> y una lista:</p>
|
||||
<ul>
|
||||
<li>Primer punto importante</li>
|
||||
<li>Segundo punto importante</li>
|
||||
</ul>` : ''),
|
||||
},
|
||||
}
|
||||
|
||||
// Fallback to English if language not supported
|
||||
const prompts = promptLang[lang] || promptLang.en
|
||||
const contextBlock = contextNotes.length > 0
|
||||
? prompts.contextWithNotes
|
||||
: prompts.contextNoNotes
|
||||
const contextBlock = contextNotes.length > 0 ? prompts.contextWithNotes : prompts.contextNoNotes
|
||||
|
||||
// Load note images as base64 for vision-capable models
|
||||
// Load note images for vision
|
||||
let imageContextParts: Array<{ type: 'image'; image: string }> = []
|
||||
if (noteContext?.images && noteContext.images.length > 0) {
|
||||
for (const imgPath of noteContext.images.slice(0, 4)) {
|
||||
@@ -343,8 +249,7 @@ Tu as accès à ces outils pour des recherches approfondies :
|
||||
const buffer = await readFile(fullPath)
|
||||
const ext = path.extname(imgPath).toLowerCase()
|
||||
const mime = ext === '.png' ? 'image/png' : ext === '.gif' ? 'image/gif' : ext === '.webp' ? 'image/webp' : 'image/jpeg'
|
||||
const base64 = `data:${mime};base64,${buffer.toString('base64')}`
|
||||
imageContextParts.push({ type: 'image', image: base64 })
|
||||
imageContextParts.push({ type: 'image', image: `data:${mime};base64,${buffer.toString('base64')}` })
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
@@ -352,113 +257,38 @@ Tu as accès à ces outils pour des recherches approfondies :
|
||||
let copilotContext = ''
|
||||
if (noteContext) {
|
||||
copilotContext = `\n\n## Current Note Context
|
||||
You are currently helping the user edit a specific note. Here is the current content of the note:
|
||||
Title: ${noteContext.title || 'Untitled'}
|
||||
|
||||
Content:
|
||||
${noteContext.content || '(empty)'}
|
||||
${imageContextParts.length > 0 ? `\nImages: ${imageContextParts.length} image(s) attached. When the user asks about images, describe what you see in them.` : ''}
|
||||
|
||||
The user wants you to write in a **${noteContext.tone || 'professional'}** tone.
|
||||
IMPORTANT: Focus ONLY on this note. Do NOT reference other notes or external information unless the user explicitly asks. Your job is to help with this specific note — suggest rewrites, answer questions about it, or draft new sections.`
|
||||
You are helping the user edit a specific note: ${noteContext.title || 'Untitled'}.
|
||||
Tone: ${noteContext.tone || 'professional'}.
|
||||
Content: ${noteContext.content || '(empty)'}
|
||||
Focus ONLY on this note unless asked otherwise.`
|
||||
}
|
||||
|
||||
const systemPrompt = `${prompts.system}
|
||||
${copilotContext}
|
||||
const systemPrompt = `${prompts.system}\n${copilotContext}\n\n${contextBlock}\n\n## LANGUAGE RULE (MANDATORY)\nYou MUST respond in ${lang === 'en' ? 'English' : lang === 'fr' ? 'French' : lang === 'fa' ? 'Persian (Farsi)' : lang === 'es' ? 'Spanish' : 'English'}.`
|
||||
|
||||
${contextBlock}
|
||||
|
||||
## LANGUAGE RULE (MANDATORY)
|
||||
You MUST respond in ${lang === 'en' ? 'English' : lang === 'fr' ? 'French' : lang === 'fa' ? 'Persian (Farsi)' : lang === 'es' ? 'Spanish' : lang === 'de' ? 'German' : lang === 'it' ? 'Italian' : 'English'}.
|
||||
Never switch to another language. Even if the user writes in a different language, respond in the configured language.`
|
||||
|
||||
// 6. Build message history from DB + current messages
|
||||
const dbHistory = conversation.messages.map((m: { role: string; content: string }) => ({
|
||||
role: m.role as 'user' | 'assistant' | 'system',
|
||||
content: m.content,
|
||||
}))
|
||||
|
||||
// Only add the current user message if it's not already in DB history
|
||||
const lastIncoming = incomingMessages[incomingMessages.length - 1]
|
||||
const currentDbMessage = dbHistory[dbHistory.length - 1]
|
||||
const isNewMessage =
|
||||
lastIncoming &&
|
||||
(!currentDbMessage ||
|
||||
currentDbMessage.role !== 'user' ||
|
||||
currentDbMessage.content !== lastIncoming.content)
|
||||
|
||||
let allMessages: Array<{ role: 'user' | 'assistant' | 'system'; content: string | Array<any> }> = isNewMessage
|
||||
? [...dbHistory, { role: lastIncoming.role, content: lastIncoming.content }]
|
||||
: dbHistory
|
||||
|
||||
// Inject note images as a context message for vision models
|
||||
if (imageContextParts.length > 0) {
|
||||
allMessages = [
|
||||
{ role: 'user', content: [{ type: 'text' as const, text: '[Attached note images — use these when the user asks about images]' }, ...imageContextParts] },
|
||||
{ role: 'assistant', content: 'Understood. I can see the attached images and will describe or analyze them when asked.' },
|
||||
...allMessages,
|
||||
]
|
||||
}
|
||||
|
||||
// Sliding window: keep first 2 messages (context) + last 48 to avoid context overflow
|
||||
const WINDOW = 50
|
||||
if (allMessages.length > WINDOW) {
|
||||
allMessages = [...allMessages.slice(0, 2), ...allMessages.slice(-(WINDOW - 2))]
|
||||
}
|
||||
|
||||
// 7. Get chat provider model
|
||||
const config = await getSystemConfig()
|
||||
const provider = getChatProvider(config)
|
||||
const model = provider.getModel()
|
||||
|
||||
// 7b. Build chat tools
|
||||
const chatToolContext = {
|
||||
userId,
|
||||
conversationId: conversation.id,
|
||||
notebookId,
|
||||
webSearch: !!webSearch,
|
||||
config,
|
||||
}
|
||||
// When scoped to "this note", only provide web tools — no note_search/note_read
|
||||
// to prevent the AI from pulling information from other notes
|
||||
// 6. Execute stream
|
||||
const sysConfig = await getSystemConfig()
|
||||
const chatTools = noteContext
|
||||
? toolRegistry.buildToolsForChat({ ...chatToolContext, webOnly: true })
|
||||
: toolRegistry.buildToolsForChat(chatToolContext)
|
||||
? toolRegistry.buildToolsForChat({ userId, config: sysConfig, webSearch, webOnly: true })
|
||||
: toolRegistry.buildToolsForChat({ userId, config: sysConfig, webSearch })
|
||||
|
||||
// 8. Save user message to DB before streaming
|
||||
if (isNewMessage && lastIncoming) {
|
||||
await prisma.chatMessage.create({
|
||||
data: {
|
||||
conversationId: conversation.id,
|
||||
role: 'user',
|
||||
content: lastIncoming.content,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 9. Stream response
|
||||
const result = streamText({
|
||||
model,
|
||||
const provider = getChatProvider(sysConfig)
|
||||
const result = await streamText({
|
||||
model: provider.chatModel,
|
||||
system: systemPrompt,
|
||||
messages: allMessages as any,
|
||||
messages: incomingMessages,
|
||||
tools: chatTools,
|
||||
stopWhen: stepCountIs(5),
|
||||
async onFinish({ text }) {
|
||||
// Save assistant message to DB after streaming completes
|
||||
await prisma.chatMessage.create({
|
||||
data: {
|
||||
conversationId: conversation.id,
|
||||
role: 'assistant',
|
||||
content: text,
|
||||
},
|
||||
maxSteps: 5,
|
||||
onFinish: async (final) => {
|
||||
// Save messages to DB
|
||||
const userContent = incomingMessages[incomingMessages.length - 1].content
|
||||
await prisma.message.create({
|
||||
data: { conversationId: conversation.id, role: 'user', content: userContent }
|
||||
})
|
||||
},
|
||||
await prisma.message.create({
|
||||
data: { conversationId: conversation.id, role: 'assistant', content: final.text }
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 10. Return streaming response with conversation ID header
|
||||
return result.toUIMessageStreamResponse({
|
||||
headers: {
|
||||
'X-Conversation-Id': conversation.id,
|
||||
},
|
||||
})
|
||||
return result.toDataStreamResponse()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user