feat(ux): epic UX design improvements across agents, chat, notes, and i18n

Comprehensive UI/UX updates including agent card redesign, chat container
improvements, note editor enhancements, memory echo notifications, and
updated translations for all 15 locales.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Sepehr Ramezani
2026-04-19 23:01:04 +02:00
parent c2a4c22e5f
commit 402e88b788
208 changed files with 493 additions and 318 deletions

View File

@@ -185,20 +185,20 @@ export function AgentsPageClient({
{/* Agents grid */}
{agents.length > 0 && (
<div className="mb-10">
<h3 className="text-sm font-semibold text-slate-500 uppercase tracking-wider mb-3">
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider mb-3">
{t('agents.myAgents')}
</h3>
{/* Search and filter */}
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-3 mb-4">
<div className="relative flex-1 w-full sm:max-w-xs">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<input
type="text"
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
placeholder={t('agents.searchPlaceholder')}
className="w-full pl-9 pr-3 py-2 text-sm bg-white border border-slate-200 rounded-lg outline-none focus:border-primary/40 focus:ring-2 focus:ring-primary/10 transition-all"
className="w-full pl-9 pr-3 py-2 text-sm bg-card border border-border rounded-lg outline-none focus:border-primary/40 focus:ring-2 focus:ring-primary/10 transition-all"
/>
</div>
<div className="flex items-center gap-1.5 flex-wrap">
@@ -209,7 +209,7 @@ export function AgentsPageClient({
className={`px-3 py-1.5 text-xs font-medium rounded-full transition-colors ${
typeFilter === opt.value
? 'bg-primary text-white'
: 'bg-slate-100 text-slate-600 hover:bg-slate-200'
: 'bg-muted text-muted-foreground hover:bg-accent'
}`}
>
{t(opt.labelKey)}
@@ -232,8 +232,8 @@ export function AgentsPageClient({
</div>
) : (
<div className="flex flex-col items-center justify-center py-12 text-center">
<Search className="w-10 h-10 text-slate-300 mb-3" />
<p className="text-sm text-slate-400">{t('agents.noResults')}</p>
<Search className="w-10 h-10 text-muted-foreground/30 mb-3" />
<p className="text-sm text-muted-foreground">{t('agents.noResults')}</p>
</div>
)}
</div>
@@ -242,9 +242,9 @@ export function AgentsPageClient({
{/* Empty state */}
{agents.length === 0 && (
<div className="flex flex-col items-center justify-center py-16 text-center mb-10">
<Bot className="w-16 h-16 text-slate-300 mb-4" />
<h3 className="text-lg font-medium text-slate-500 mb-2">{t('agents.noAgents')}</h3>
<p className="text-sm text-slate-400 max-w-sm">
<Bot className="w-16 h-16 text-muted-foreground/30 mb-4" />
<h3 className="text-lg font-medium text-muted-foreground mb-2">{t('agents.noAgents')}</h3>
<p className="text-sm text-muted-foreground max-w-sm">
{t('agents.noAgentsDescription')}
</p>
</div>

View File

@@ -19,7 +19,7 @@ export default async function AgentsPage() {
])
return (
<div className="flex-1 flex flex-col h-full bg-slate-50/50">
<div className="flex-1 flex flex-col h-full bg-background">
<div className="flex-1 p-8 overflow-y-auto">
<div className="max-w-6xl mx-auto">
<AgentsPageClient

View File

@@ -4,6 +4,7 @@ import { redirect } from 'next/navigation'
import { prisma } from '@/lib/prisma'
import { ChatContainer } from '@/components/chat/chat-container'
import { getConversations } from '@/app/actions/chat-actions'
import { getSystemConfig } from '@/lib/config'
export const metadata: Metadata = {
title: 'Chat IA | Memento',
@@ -17,19 +18,26 @@ export default async function ChatPage() {
const userId = session.user.id
// Fetch initial data
const [conversations, notebooks] = await Promise.all([
const [conversations, notebooks, config] = await Promise.all([
getConversations(),
prisma.notebook.findMany({
where: { userId },
orderBy: { order: 'asc' }
})
}),
getSystemConfig(),
])
// Check if web search tools are configured
const webSearchAvailable = !!(
config.WEB_SEARCH_PROVIDER || config.BRAVE_SEARCH_API_KEY || config.SEARXNG_URL || config.JINA_API_KEY
)
return (
<div className="flex-1 flex flex-col h-full bg-white dark:bg-[#1a1c22]">
<ChatContainer
initialConversations={conversations}
notebooks={notebooks}
<ChatContainer
initialConversations={conversations}
notebooks={notebooks}
webSearchAvailable={webSearchAvailable}
/>
</div>
)

View File

@@ -435,6 +435,7 @@ export async function createNote(data: {
isMarkdown?: boolean
size?: 'small' | 'medium' | 'large'
autoGenerated?: boolean
aiProvider?: string
notebookId?: string | undefined // Assign note to a notebook if provided
skipRevalidation?: boolean // Option to prevent full page refresh for smooth optimistic UI updates
}) {
@@ -459,6 +460,7 @@ export async function createNote(data: {
isMarkdown: data.isMarkdown || false,
size: data.size || 'small',
autoGenerated: data.autoGenerated || null,
aiProvider: data.aiProvider || null,
notebookId: data.notebookId || null,
}
})
@@ -867,7 +869,7 @@ export async function togglePin(id: string, isPinned: boolean) { return updateNo
export async function toggleArchive(id: string, isArchived: boolean) { return updateNote(id, { isArchived }) }
export async function updateColor(id: string, color: string) { return updateNote(id, { color }) }
export async function updateLabels(id: string, labels: string[]) { return updateNote(id, { labels }) }
export async function removeFusedBadge(id: string) { return updateNote(id, { autoGenerated: null }) }
export async function removeFusedBadge(id: string) { return updateNote(id, { autoGenerated: null, aiProvider: null }) }
// Update note size WITHOUT revalidation - client uses optimistic updates
export async function updateSize(id: string, size: 'small' | 'medium' | 'large') {
@@ -941,8 +943,16 @@ export async function updateFullOrderWithoutRevalidation(ids: string[]) {
if (!session?.user?.id) throw new Error('Unauthorized');
const userId = session.user.id;
try {
const updates = ids.map((id: string, index: number) =>
prisma.note.update({ where: { id, userId }, data: { order: index } })
// Verify all notes belong to the user before updating
const notes = await prisma.note.findMany({
where: { id: { in: ids }, userId },
select: { id: true },
})
const ownedIds = new Set(notes.map(n => n.id))
const validIds = ids.filter(id => ownedIds.has(id))
const updates = validIds.map((id: string, index: number) =>
prisma.note.update({ where: { id }, data: { order: index } })
)
await prisma.$transaction(updates)
return { success: true }

View File

@@ -5,6 +5,8 @@ import { semanticSearchService } from '@/lib/ai/services/semantic-search.service
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'
export const maxDuration = 60
@@ -45,11 +47,12 @@ export async function POST(req: Request) {
// 2. Parse request body — messages arrive as UIMessage[] from DefaultChatTransport
const body = await req.json()
const { messages: rawMessages, conversationId, notebookId, language } = body as {
const { messages: rawMessages, conversationId, notebookId, language, webSearch } = body as {
messages: UIMessage[]
conversationId?: string
notebookId?: string
language?: string
webSearch?: boolean
}
// Convert UIMessages to CoreMessages for streamText
@@ -143,7 +146,19 @@ export async function POST(req: Request) {
- 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.
- If the user says "Momento" they mean Memento (this app).`,
- If the user says "Momento" they mean Memento (this app).
## 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.`,
},
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.`,
@@ -159,7 +174,19 @@ export async function POST(req: Request) {
- 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.
- Si l'utilisateur dit "Momento" il parle de Memento (cette application).`,
- Si l'utilisateur dit "Momento" il parle de Memento (cette application).
## 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.`,
},
fa: {
contextWithNotes: `## یادداشت‌های کاربر\n\n${contextNotes}\n\nهنگام استفاده از اطلاعات یادداشت‌های بالا، عنوان یادداشت منبع را در پرانتز ذکر کنید. کپی نکنید — بازنویسی کنید. اگر یادداشت‌ها موضوع را پوشش نمی‌دهند، بگویید و با دانش عمومی خود تکمیل کنید.`,
@@ -175,7 +202,18 @@ export async function POST(req: Request) {
- لحن طبیعی، نه رسمی بیش از حد و نه خیلی غیررسمی.
- بدون جمله مقدمه اضافی. مستقیم پاسخ دهید.
- بدون سؤال فروشی در انتها. اگر اطلاعات تکمیلی مفید دارید، مستقیم بدهید.
- اگر کاربر "Momento" می‌گوید، منظورش Memento (این برنامه) است.`,
- اگر کاربر "Momento" می‌گوید، منظورش Memento (این برنامه) است.
## ابزارهای موجود
- **note_search**: جستجو در یادداشت‌های کاربر با کلیدواژه یا معنی. زمانی استفاده کنید که زمینه اولیه کافی نباشد. اگر دفترچه انتخاب شده، شناسه آن را ارسال کنید.
- **note_read**: خواندن یک یادداشت خاص با شناسه. زمانی استفاده کنید که note_search یادداشتی برگرداند که محتوای کامل آن را نیاز دارید.
- **web_search**: جستجو در وب. زمانی استفاده کنید که کاربر درباره چیزی خارج از یادداشت‌هایش می‌پرسد.
- **web_scrape**: استخراج محتوای صفحه وب. زمانی استفاده کنید که web_search نشانی‌ای برگرداند که می‌خواهید بخوانید.
## قوانین استفاده از ابزارها
- شما از قبل زمینه‌ای از یادداشت‌های کاربر دارید. فقط در صورت نیاز به اطلاعات بیشتر از ابزارها استفاده کنید.
- هرگز شناسه یادداشت، نشانی یا شناسه دفترچه نسازید. از شناسه‌های موجود در زمینه یا نتایج ابزار استفاده کنید.
- برای سؤالات مکالمه‌ای ساده (سلام، نظرات، دانش عمومی)، مستقیم پاسخ دهید.`,
},
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.`,
@@ -190,7 +228,18 @@ export async function POST(req: Request) {
## 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.`,
- 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.`,
@@ -205,7 +254,18 @@ export async function POST(req: Request) {
## 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.`,
- 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.`,
@@ -220,7 +280,18 @@ export async function POST(req: Request) {
## 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.`,
- 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.`,
},
}
@@ -260,6 +331,16 @@ ${lang === 'en' ? 'Respond in the user\'s language.' : lang === 'fr' ? 'Réponds
const provider = getChatProvider(config)
const model = provider.getModel()
// 7b. Build chat tools
const chatToolContext = {
userId,
conversationId: conversation.id,
notebookId,
webSearch: !!webSearch,
config,
}
const chatTools = toolRegistry.buildToolsForChat(chatToolContext)
// 8. Save user message to DB before streaming
if (isNewMessage && lastIncoming) {
await prisma.chatMessage.create({
@@ -276,6 +357,8 @@ ${lang === 'en' ? 'Respond in the user\'s language.' : lang === 'fr' ? 'Réponds
model,
system: systemPrompt,
messages: allMessages,
tools: chatTools,
stopWhen: stepCountIs(5),
async onFinish({ text }) {
// Save assistant message to DB after streaming completes
await prisma.chatMessage.create({