Compare commits
2 Commits
e0fd5393ca
...
afbb0dfc2d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
afbb0dfc2d | ||
|
|
ebf6f16fde |
@@ -1,58 +1,22 @@
|
||||
'use client';
|
||||
import { headers } from 'next/headers';
|
||||
import { detectUserLanguage, parseAcceptLanguage } from '@/lib/i18n/detect-user-language';
|
||||
import { loadTranslations } from '@/lib/i18n/load-translations';
|
||||
import { LanguageProvider } from '@/lib/i18n/LanguageProvider';
|
||||
import { AuthShell } from '@/components/auth-shell';
|
||||
|
||||
import { LanguageProvider, useLanguage } from '@/lib/i18n/LanguageProvider';
|
||||
import Link from 'next/link';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
|
||||
function AuthHeader() {
|
||||
const { t } = useLanguage();
|
||||
|
||||
return (
|
||||
<header className="p-6 md:p-8 flex justify-between items-center relative z-10">
|
||||
<Link
|
||||
href="/"
|
||||
aria-label={t('general.back')}
|
||||
className="flex items-center gap-2 text-[var(--muted-foreground)] hover:text-[var(--foreground)] transition-colors group"
|
||||
>
|
||||
<div className="w-8 h-8 rounded-full border border-[var(--border)] flex items-center justify-center group-hover:border-[var(--color-brand-accent)] transition-colors">
|
||||
<ArrowLeft size={14} className="group-hover:-translate-x-0.5 transition-transform rtl:rotate-180" />
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Link href="/" className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 bg-[var(--foreground)] text-[var(--background)] rounded-xl flex items-center justify-center shadow-lg">
|
||||
<span className="font-serif font-bold text-xl">M</span>
|
||||
</div>
|
||||
<span className="font-serif text-xl font-medium tracking-tight">Memento</span>
|
||||
</Link>
|
||||
|
||||
<div className="w-8" />
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AuthLayout({
|
||||
export default async function AuthLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const headersList = await headers();
|
||||
const browserLang = parseAcceptLanguage(headersList.get('accept-language'));
|
||||
const initialLanguage = await detectUserLanguage(browserLang);
|
||||
const initialTranslations = await loadTranslations(initialLanguage);
|
||||
|
||||
return (
|
||||
<LanguageProvider>
|
||||
<div className="min-h-screen bg-[#FDFCFB] dark:bg-[#0D0D0D] flex flex-col relative overflow-hidden">
|
||||
<div className="absolute top-[-10%] right-[-10%] w-[50%] h-[50%] bg-[var(--color-brand-accent)]/5 blur-[120px] rounded-full pointer-events-none" />
|
||||
<div className="absolute bottom-[-10%] left-[-10%] w-[50%] h-[50%] bg-[#D4A373]/5 blur-[120px] rounded-full pointer-events-none" />
|
||||
|
||||
<AuthHeader />
|
||||
|
||||
<main className="flex-1 flex items-center justify-center p-4 md:p-6 relative z-10">
|
||||
<div className="w-full max-w-md">
|
||||
{children}
|
||||
<p className="text-center mt-8 text-[9px] text-[var(--muted-foreground)] font-bold uppercase tracking-[0.3em] opacity-40 select-none">
|
||||
© 2026 Memento
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<LanguageProvider initialLanguage={initialLanguage} initialTranslations={initialTranslations}>
|
||||
<AuthShell>{children}</AuthShell>
|
||||
</LanguageProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ export default function InsightsPage() {
|
||||
const [isStale, setIsStale] = useState(false)
|
||||
const [selectedClusterId, setSelectedClusterId] = useState<string | null>(null)
|
||||
const [viewMode, setViewMode] = useState<'graph' | 'dashboard'>('dashboard')
|
||||
const [graphMode, setGraphMode] = useState<'visual' | 'list'>('visual')
|
||||
const [graphMode, setGraphMode] = useState<'visual' | 'list'>('list')
|
||||
const [listFilter, setListFilter] = useState('')
|
||||
const [listSort, setListSort] = useState<'size' | 'alpha' | 'bridges'>('size')
|
||||
/** Un seul cluster déplié à la fois (null = tous repliés) */
|
||||
@@ -445,7 +445,7 @@ export default function InsightsPage() {
|
||||
<button
|
||||
className="p-2 -ms-1 text-foreground hover:bg-foreground/5 rounded-lg transition-colors shrink-0 cursor-pointer focus-visible:ring-2 focus-visible:ring-ochre/50 focus-visible:outline-none"
|
||||
onClick={() => window.dispatchEvent(new CustomEvent('toggle-insights-sidebar'))}
|
||||
aria-label="Toggle sidebar"
|
||||
aria-label={t('insightsView.toggleMenu')}
|
||||
>
|
||||
<Menu size={22} />
|
||||
</button>
|
||||
@@ -717,7 +717,7 @@ export default function InsightsPage() {
|
||||
style={{ backgroundColor: cluster.color }}
|
||||
aria-hidden
|
||||
/>
|
||||
<h3 className="text-xs font-bold uppercase tracking-wider text-ink dark:text-dark-ink truncate flex-1 min-w-0">
|
||||
<h3 className="min-w-0 flex-1 text-[13px] font-semibold leading-snug text-ink dark:text-dark-ink">
|
||||
{name}
|
||||
</h3>
|
||||
<span className="text-[9px] text-concrete shrink-0 tabular-nums">
|
||||
|
||||
@@ -5,7 +5,7 @@ import { BillingPlans } from '@/components/settings/billing-plans';
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export const metadata = {
|
||||
title: 'Billing',
|
||||
title: 'Facturation — Memento',
|
||||
};
|
||||
|
||||
function Fallback() {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { Menu } from 'lucide-react'
|
||||
import { SettingsNav } from '@/components/settings'
|
||||
import { SettingsDocumentTitle } from '@/components/settings/settings-document-title'
|
||||
import { useLanguage } from '@/lib/i18n'
|
||||
|
||||
export default function SettingsLayout({
|
||||
@@ -11,9 +12,10 @@ export default function SettingsLayout({
|
||||
}) {
|
||||
const { t } = useLanguage()
|
||||
return (
|
||||
<div className="flex flex-col h-full bg-[#F2F0E9] dark:bg-zinc-950">
|
||||
<header className="px-4 sm:px-8 md:px-12 pt-8 sm:pt-14 md:pt-20 pb-6 sm:pb-10 md:pb-16 space-y-6 sm:space-y-10 md:space-y-12 shrink-0">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex flex-col h-full bg-background">
|
||||
<SettingsDocumentTitle />
|
||||
<header className="px-4 sm:px-8 md:px-12 pt-6 sm:pt-8 md:pt-10 pb-4 sm:pb-6 shrink-0">
|
||||
<div className="flex items-start gap-3 mb-6">
|
||||
<button
|
||||
className="md:hidden p-2 -ms-1 text-ink dark:text-zinc-200 hover:bg-ink/5 dark:hover:bg-white/10 rounded-lg transition-colors shrink-0 mt-1"
|
||||
onClick={() => window.dispatchEvent(new CustomEvent('open-mobile-sidebar'))}
|
||||
@@ -22,10 +24,10 @@ export default function SettingsLayout({
|
||||
<Menu size={22} />
|
||||
</button>
|
||||
<div>
|
||||
<h1 className="text-3xl sm:text-5xl md:text-[64px] font-serif text-ink dark:text-zinc-50 tracking-tight leading-none italic font-medium">
|
||||
<h1 className="text-2xl sm:text-3xl md:text-4xl font-serif text-ink dark:text-zinc-50 tracking-tight leading-tight font-medium">
|
||||
{t('settings.title')}
|
||||
</h1>
|
||||
<p className="text-[10px] font-bold uppercase tracking-[0.4em] text-concrete dark:text-zinc-500 opacity-60 mt-4">
|
||||
<p className="text-sm text-concrete dark:text-zinc-400 mt-1.5">
|
||||
{t('settings.description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,7 @@ export function McpSettingsHeader() {
|
||||
{ text: t('mcpSettings.helpBox.step2') },
|
||||
{ text: t('mcpSettings.helpBox.step3') },
|
||||
{ text: t('mcpSettings.helpBox.step4'), link: { label: t('mcpSettings.helpBox.step4Link'), href: 'https://modelcontextprotocol.io/docs' } },
|
||||
{ icon: '⚡', text: t('mcpSettings.helpBox.step5') },
|
||||
{ text: t('mcpSettings.helpBox.step5') },
|
||||
]}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -1,19 +1,28 @@
|
||||
'use client'
|
||||
|
||||
import { motion } from 'motion/react'
|
||||
import { Shield } from 'lucide-react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Check } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useState } from 'react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useLanguage } from '@/lib/i18n'
|
||||
import { SUBSCRIPTION_TRIAL_DAYS } from '@/lib/billing/trial-constants'
|
||||
import { useState } from 'react'
|
||||
|
||||
export default function PricingPage() {
|
||||
const { t } = useLanguage()
|
||||
const router = useRouter()
|
||||
const [billingInterval, setBillingInterval] = useState<'monthly' | 'annual'>('monthly')
|
||||
const trialDays = SUBSCRIPTION_TRIAL_DAYS
|
||||
const { data: byokCatalog } = useQuery({
|
||||
queryKey: ['public', 'byok-catalog'],
|
||||
queryFn: async () => {
|
||||
const res = await fetch('/api/public/byok-catalog')
|
||||
if (!res.ok) throw new Error('catalog')
|
||||
return res.json() as Promise<{ providers: { id: string }[] }>
|
||||
},
|
||||
staleTime: 60_000,
|
||||
})
|
||||
const providerCount = byokCatalog?.providers.length || '…'
|
||||
|
||||
const PLANS = [
|
||||
const plans = [
|
||||
{ key: 'basic', popular: false, hasTrial: false, price: t('landing.pricing.basicPrice'), period: '' },
|
||||
{
|
||||
key: 'pro',
|
||||
@@ -39,88 +48,113 @@ export default function PricingPage() {
|
||||
]
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-paper">
|
||||
<section className="py-32 px-8">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="text-center mb-12">
|
||||
<span className="text-[11px] font-bold uppercase tracking-[0.3em] text-ochre mb-4 block">{t('landing.pricing.label')}</span>
|
||||
<h2 className="text-4xl md:text-5xl font-serif tracking-tight text-ink mb-6">{t('landing.pricing.title')}</h2>
|
||||
<p className="text-concrete font-light max-w-xl mx-auto mb-12">{t('landing.pricing.desc')}</p>
|
||||
<main className="min-h-screen bg-[#0B0A09] text-[#F4F1EA] font-[family-name:var(--font-manrope)] selection:bg-[#D4A373]/40 selection:text-white">
|
||||
<nav className="sticky top-0 z-[100] px-5 sm:px-8 py-4 flex items-center justify-between bg-[#0B0A09]/70 backdrop-blur-xl border-b border-white/[0.06]">
|
||||
<Link href="/" className="flex items-center gap-2.5 group">
|
||||
<div className="w-9 h-9 bg-[#F4F1EA] text-[#0B0A09] flex items-center justify-center rounded-lg">
|
||||
<span className="font-serif text-xl font-bold leading-none">M</span>
|
||||
</div>
|
||||
<span className="font-serif text-xl font-medium tracking-tight">Memento</span>
|
||||
</Link>
|
||||
<div className="flex items-center gap-2 sm:gap-3">
|
||||
<Link href="/login" className="text-[13px] text-white/75 hover:text-white transition-colors px-2">
|
||||
{t('landing.nav.login')}
|
||||
</Link>
|
||||
<Link
|
||||
href="/register"
|
||||
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-full bg-[#F4F1EA] text-[#0B0A09] text-[13px] font-semibold hover:bg-white transition-colors"
|
||||
>
|
||||
{t('landing.nav.cta')}
|
||||
</Link>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div className="flex items-center justify-center gap-10 mb-8">
|
||||
<button onClick={() => setBillingInterval('monthly')} className={`group relative py-2 px-1 transition-all ${billingInterval === 'monthly' ? 'text-ink' : 'text-concrete/40 hover:text-concrete'}`}>
|
||||
<span className="text-xs font-black uppercase tracking-[0.2em]">{t('landing.pricing.monthly')}</span>
|
||||
{billingInterval === 'monthly' && (
|
||||
<motion.div layoutId="interval-active-pricing" className="absolute -inset-x-1 -inset-y-0.5 border border-ochre/60" transition={{ type: 'spring', bounce: 0.2, duration: 0.6 }} />
|
||||
)}
|
||||
<section className="px-5 sm:px-8 py-28">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="text-center mb-12">
|
||||
<span className="text-[11px] font-bold uppercase tracking-[0.3em] text-[#D4A373] mb-4 block">
|
||||
{t('landing.pricing.label')}
|
||||
</span>
|
||||
<h1 className="font-serif text-3xl sm:text-5xl tracking-tight mb-4">{t('landing.pricing.title')}</h1>
|
||||
<p className="text-white/70 mb-8">{t('landing.pricing.desc')}</p>
|
||||
<div className="inline-flex p-1 rounded-full border border-white/10 bg-white/[0.03]">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setBillingInterval('monthly')}
|
||||
className={`px-5 py-2 rounded-full text-[12px] font-semibold transition-all ${billingInterval === 'monthly' ? 'bg-[#F4F1EA] text-[#0B0A09]' : 'text-white/70'}`}
|
||||
>
|
||||
{t('landing.pricing.monthly')}
|
||||
</button>
|
||||
<div className="relative">
|
||||
<button onClick={() => setBillingInterval('annual')} className={`group relative py-2 px-1 transition-all ${billingInterval === 'annual' ? 'text-ink' : 'text-concrete/40 hover:text-concrete'}`}>
|
||||
<span className="text-xs font-black uppercase tracking-[0.2em]">{t('landing.pricing.annual')}</span>
|
||||
{billingInterval === 'annual' && (
|
||||
<motion.div layoutId="interval-active-pricing" className="absolute -inset-x-1 -inset-y-0.5 border border-ochre/60" transition={{ type: 'spring', bounce: 0.2, duration: 0.6 }} />
|
||||
)}
|
||||
</button>
|
||||
<div className="absolute -top-6 left-1/2 -translate-x-1/2 whitespace-nowrap">
|
||||
<span className="text-[9px] font-bold text-ochre uppercase tracking-widest italic animate-pulse">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setBillingInterval('annual')}
|
||||
className={`px-5 py-2 rounded-full text-[12px] font-semibold transition-all relative ${billingInterval === 'annual' ? 'bg-[#F4F1EA] text-[#0B0A09]' : 'text-white/70'}`}
|
||||
>
|
||||
{t('landing.pricing.annual')}
|
||||
<span className="absolute -top-3 -right-1 text-[10px] text-[#D4A373] whitespace-nowrap">
|
||||
{t('landing.pricing.savePercent')}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 items-stretch">
|
||||
{PLANS.map((plan) => (
|
||||
<div key={plan.key} className={`relative p-8 rounded-[32px] border flex flex-col transition-all duration-300 hover:shadow-2xl hover:shadow-ink/5 ${plan.popular ? 'bg-ink text-paper border-ink ring-4 ring-ochre/20' : 'bg-white border-border text-ink'}`}>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3">
|
||||
{plans.map((plan) => (
|
||||
<div
|
||||
key={plan.key}
|
||||
className={`rounded-2xl border p-6 flex flex-col ${
|
||||
plan.popular
|
||||
? 'border-[#D4A373]/50 bg-[#D4A373]/10'
|
||||
: 'border-white/[0.08] bg-white/[0.02]'
|
||||
}`}
|
||||
>
|
||||
{plan.popular && (
|
||||
<div className="absolute -top-4 left-1/2 -translate-x-1/2 px-4 py-1 bg-ochre text-ink text-[10px] font-bold uppercase tracking-widest rounded-full">
|
||||
<span className="text-[10px] font-bold uppercase tracking-widest text-[#D4A373] mb-3">
|
||||
{t('landing.pricing.popular')}
|
||||
</div>
|
||||
</span>
|
||||
)}
|
||||
<div className="mb-8">
|
||||
<h4 className="text-[11px] font-bold uppercase tracking-widest mb-2 opacity-60">{t(`landing.pricing.${plan.key}.name`)}</h4>
|
||||
<div className="flex items-baseline gap-1 mb-4">
|
||||
<span className="text-4xl font-serif font-medium">{plan.price}</span>
|
||||
{plan.period && <span className="text-xs opacity-60">{plan.period}</span>}
|
||||
<h2 className="text-[13px] font-medium tracking-wide text-white/80 mb-2">
|
||||
{t(`landing.pricing.${plan.key}.name`)}
|
||||
</h2>
|
||||
<div className="flex items-baseline gap-1 mb-2">
|
||||
<span className="text-3xl font-serif">{plan.price}</span>
|
||||
{plan.period && <span className="text-sm text-white/70">{plan.period}</span>}
|
||||
</div>
|
||||
{plan.hasTrial && (
|
||||
<p className={`text-[11px] font-semibold mb-3 ${plan.popular ? 'text-ochre' : 'text-brand-accent'}`}>
|
||||
<p className="text-[11px] font-semibold text-[#D4A373] mb-3">
|
||||
{t('landing.pricing.trialBadge', { days: trialDays })}
|
||||
</p>
|
||||
)}
|
||||
<p className="text-sm font-light leading-relaxed opacity-80">{t(`landing.pricing.${plan.key}.desc`)}</p>
|
||||
</div>
|
||||
<div className="flex-1 space-y-4 mb-10">
|
||||
<p className="text-sm text-white/70 mb-6">{t(`landing.pricing.${plan.key}.desc`)}</p>
|
||||
<ul className="space-y-2.5 mb-8 flex-1">
|
||||
{plan.hasTrial && (
|
||||
<div className="flex items-start gap-3">
|
||||
<div className={`mt-1 rounded-full p-0.5 ${plan.popular ? 'bg-ochre text-ink' : 'bg-brand-accent/10 text-brand-accent'}`}>
|
||||
<Shield size={10} fill="currentColor" />
|
||||
</div>
|
||||
<span className="text-xs font-medium">{t('landing.pricing.trialFeature', { days: trialDays })}</span>
|
||||
</div>
|
||||
<li className="flex gap-2 text-xs text-[#D4A373]/90">
|
||||
<Check size={12} className="text-[#D4A373] mt-0.5 shrink-0" />
|
||||
{t('landing.pricing.trialFeature', { days: trialDays })}
|
||||
</li>
|
||||
)}
|
||||
{[0, 1, 2, 3, 4, 5].map(j => {
|
||||
const feat = t(`landing.pricing.${plan.key}.feature${j}`)
|
||||
if (!feat || feat === `landing.pricing.${plan.key}.feature${j}`) return null
|
||||
{[0, 1, 2, 3, 4, 5].map((j) => {
|
||||
const feat = t(`landing.pricing.${plan.key}.feature${j}`, { count: providerCount })
|
||||
if (!feat || feat.startsWith('landing.')) return null
|
||||
return (
|
||||
<div key={j} className="flex items-start gap-3">
|
||||
<div className={`mt-1 rounded-full p-0.5 ${plan.popular ? 'bg-ochre text-ink' : 'bg-brand-accent/10 text-brand-accent'}`}>
|
||||
<Shield size={10} fill="currentColor" />
|
||||
</div>
|
||||
<span className="text-xs font-light">{feat}</span>
|
||||
</div>
|
||||
<li key={j} className="flex gap-2 text-sm text-white/80">
|
||||
<Check size={12} className="text-[#D4A373] mt-0.5 shrink-0" />
|
||||
{feat}
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => router.push('/register')}
|
||||
className={`w-full py-4 rounded-2xl text-xs font-bold uppercase tracking-widest transition-all ${plan.popular ? 'bg-ochre text-ink hover:opacity-90' : 'bg-ink text-paper hover:bg-ink/90'}`}
|
||||
</ul>
|
||||
<Link
|
||||
href="/register"
|
||||
className={`py-3 rounded-xl text-center text-[13px] font-semibold transition-colors ${
|
||||
plan.popular
|
||||
? 'bg-[#F4F1EA] text-[#0B0A09] hover:bg-white'
|
||||
: 'bg-white/10 text-white hover:bg-white/15'
|
||||
}`}
|
||||
>
|
||||
{plan.hasTrial
|
||||
? t('landing.pricing.trialCta', { days: trialDays })
|
||||
: t(`landing.pricing.${plan.key}.cta`)}
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -368,11 +368,11 @@ export default async function PrivacyPage({
|
||||
{locale === 'fr' ? 'Confidentialité' : 'Privacy'}
|
||||
</p>
|
||||
<h1 className="font-serif text-4xl sm:text-5xl tracking-tight mb-4">{doc.title}</h1>
|
||||
<p className="text-sm text-white/40 mb-3">{doc.lastUpdated}</p>
|
||||
<p className="text-sm text-white/70 mb-3">{doc.lastUpdated}</p>
|
||||
<p className="text-white/65 leading-relaxed text-lg mb-12">{doc.intro}</p>
|
||||
|
||||
<nav aria-label="Sommaire" className="mb-12 p-5 rounded-xl bg-white/[0.03] border border-white/[0.06]">
|
||||
<p className="text-[11px] uppercase tracking-[0.2em] text-white/40 mb-3">
|
||||
<p className="text-[13px] font-medium tracking-wide text-white/70 mb-3">
|
||||
{locale === 'fr' ? 'Sommaire' : 'Contents'}
|
||||
</p>
|
||||
<ul className="space-y-1.5 text-sm">
|
||||
|
||||
@@ -6,6 +6,7 @@ import { auth } from '@/auth'
|
||||
import bcrypt from 'bcryptjs'
|
||||
import { z } from 'zod'
|
||||
import { SubscriptionTier, SubscriptionStatus } from '@prisma/client'
|
||||
import { addUtcMonths } from '@/lib/billing/period'
|
||||
|
||||
// Schema pour la création d'utilisateur
|
||||
const CreateUserSchema = z.object({
|
||||
@@ -152,8 +153,7 @@ export async function updateUserSubscription(userId: string, tier: string) {
|
||||
const oldTier = existing?.tier ?? 'BASIC'
|
||||
|
||||
const now = new Date()
|
||||
const periodEnd = new Date(now)
|
||||
periodEnd.setFullYear(periodEnd.getFullYear() + 1)
|
||||
const periodEnd = addUtcMonths(now, 1)
|
||||
|
||||
await prisma.subscription.upsert({
|
||||
where: { userId },
|
||||
|
||||
@@ -8,25 +8,12 @@ import { reserveAiUsageOrThrow } from '@/lib/ai-quota'
|
||||
import { QuotaExceededError, QuotaServiceUnavailableError } from '@/lib/entitlements'
|
||||
import { z } from 'zod'
|
||||
import { hasUserAiConsent, aiConsentForbiddenResponse } from '@/lib/consent/server-consent'
|
||||
import { countPlainWords, MIN_WORDS_FOR_TITLE_SUGGESTION, stripHtmlToPlainText } from '@/lib/text/plain-text'
|
||||
|
||||
const requestSchema = z.object({
|
||||
content: z.string().min(1, "Le contenu ne peut pas être vide"),
|
||||
})
|
||||
|
||||
/** Supprime les balises HTML pour extraire le texte brut */
|
||||
function stripHtml(html: string): string {
|
||||
return html
|
||||
.replace(/<[^>]+>/g, ' ')
|
||||
.replace(/ /g, ' ')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'")
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
// Check authentication and user setting
|
||||
@@ -48,17 +35,10 @@ export async function POST(req: NextRequest) {
|
||||
const body = await req.json()
|
||||
const { content: rawContent } = requestSchema.parse(body)
|
||||
|
||||
// Nettoyer le HTML (l'éditeur TipTap envoie du HTML)
|
||||
const content = stripHtml(rawContent)
|
||||
const content = stripHtmlToPlainText(rawContent)
|
||||
|
||||
// Vérifier qu'il y a au moins 10 mots
|
||||
const wordCount = content.split(/\s+/).filter(w => w.length > 0).length
|
||||
|
||||
if (wordCount < 10) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Le contenu doit avoir au moins 10 mots' },
|
||||
{ status: 400 }
|
||||
)
|
||||
if (countPlainWords(content) < MIN_WORDS_FOR_TITLE_SUGGESTION) {
|
||||
return NextResponse.json({ suggestions: [] })
|
||||
}
|
||||
|
||||
const config = await getSystemConfig()
|
||||
|
||||
@@ -5,6 +5,7 @@ import { stripe } from '@/lib/stripe';
|
||||
import { getDynamicPrices, isBillingEnabled } from '@/lib/billing/stripe-prices';
|
||||
import { syncSubscriptionFromStripe } from '@/lib/billing/sync-subscription-from-stripe';
|
||||
import { shouldOfferSubscriptionTrial, SUBSCRIPTION_TRIAL_DAYS } from '@/lib/billing/trial';
|
||||
import { periodsDiffer, rollBillingPeriod } from '@/lib/billing/period';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
@@ -41,9 +42,37 @@ export async function GET(req: NextRequest) {
|
||||
}
|
||||
}
|
||||
try {
|
||||
let subscription = await prisma.subscription.findUnique({ where: { userId } });
|
||||
const stripeSecret = process.env.STRIPE_SECRET_KEY;
|
||||
const canTalkToStripe = !!stripeSecret && stripeSecret !== 'sk_test_placeholder';
|
||||
|
||||
if (
|
||||
subscription?.stripeSubscriptionId
|
||||
&& !subscription.stripeSubscriptionId.startsWith('sub_mock')
|
||||
&& canTalkToStripe
|
||||
) {
|
||||
try {
|
||||
const live = await stripe.subscriptions.retrieve(subscription.stripeSubscriptionId);
|
||||
await syncSubscriptionFromStripe(live, userId);
|
||||
subscription = await prisma.subscription.findUnique({ where: { userId } });
|
||||
} catch (syncErr) {
|
||||
console.error('[billing/status] live Stripe period sync failed:', syncErr);
|
||||
}
|
||||
} else if (subscription && !subscription.cancelAtPeriodEnd) {
|
||||
const rolled = rollBillingPeriod(subscription.currentPeriodStart);
|
||||
if (periodsDiffer(rolled, subscription)) {
|
||||
subscription = await prisma.subscription.update({
|
||||
where: { userId },
|
||||
data: {
|
||||
currentPeriodStart: rolled.currentPeriodStart,
|
||||
currentPeriodEnd: rolled.currentPeriodEnd,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const { tier, status, currentPeriodEnd } = await getUserInfo(userId);
|
||||
const effectiveTier = await getEffectiveTier(userId);
|
||||
const subscription = await prisma.subscription.findUnique({ where: { userId } });
|
||||
const prices = await getDynamicPrices();
|
||||
const billingEnabled = await isBillingEnabled();
|
||||
const { getPackPublicPrices } = await import('@/lib/billing/credit-packs');
|
||||
@@ -89,8 +118,8 @@ export async function GET(req: NextRequest) {
|
||||
tier,
|
||||
effectiveTier,
|
||||
status,
|
||||
currentPeriodEnd: currentPeriodEnd ?? null,
|
||||
currentPeriodStart: subscription?.currentPeriodStart ?? null,
|
||||
currentPeriodEnd: subscription?.currentPeriodEnd?.toISOString() ?? currentPeriodEnd?.toISOString() ?? null,
|
||||
currentPeriodStart: subscription?.currentPeriodStart?.toISOString() ?? null,
|
||||
cancelAtPeriodEnd: subscription?.cancelAtPeriodEnd ?? false,
|
||||
hasStripeSubscription: !!subscription?.stripeSubscriptionId,
|
||||
trialEndsAt: subscription?.trialEndsAt?.toISOString() ?? null,
|
||||
|
||||
@@ -77,7 +77,7 @@ export async function GET() {
|
||||
notebookId: true, updatedAt: true, createdAt: true,
|
||||
},
|
||||
orderBy: { updatedAt: 'desc' },
|
||||
take: 8,
|
||||
take: 12,
|
||||
}),
|
||||
|
||||
prisma.note.count({
|
||||
@@ -86,7 +86,7 @@ export async function GET() {
|
||||
|
||||
prisma.note.findMany({
|
||||
where: { userId, notebookId: null, isArchived: false, trashedAt: null },
|
||||
select: { id: true, title: true, notebookId: true, updatedAt: true },
|
||||
select: { id: true, title: true, content: true, notebookId: true, updatedAt: true },
|
||||
orderBy: { updatedAt: 'desc' },
|
||||
take: 3,
|
||||
}),
|
||||
@@ -132,7 +132,7 @@ export async function GET() {
|
||||
|
||||
prisma.note.findMany({
|
||||
where: { userId, isPinned: true, trashedAt: null, isArchived: false },
|
||||
select: { id: true, title: true, notebookId: true, updatedAt: true },
|
||||
select: { id: true, title: true, content: true, notebookId: true, updatedAt: true },
|
||||
orderBy: { updatedAt: 'desc' },
|
||||
take: 5,
|
||||
}),
|
||||
@@ -170,7 +170,10 @@ export async function GET() {
|
||||
insightRows = [...insightRows, ...viewedRecent]
|
||||
}
|
||||
|
||||
const notebookIds = [...new Set(recentNotes.map(n => n.notebookId).filter(Boolean))] as string[]
|
||||
const notebookIds = [...new Set([
|
||||
...recentNotes.map(n => n.notebookId),
|
||||
...pinnedNotes.map(n => n.notebookId),
|
||||
].filter(Boolean))] as string[]
|
||||
const notebooks = notebookIds.length > 0
|
||||
? await prisma.notebook.findMany({
|
||||
where: { id: { in: notebookIds } },
|
||||
@@ -188,6 +191,7 @@ export async function GET() {
|
||||
inboxPreview: inboxPreview.map(n => ({
|
||||
id: n.id,
|
||||
title: n.title,
|
||||
excerpt: excerptNoteContent(n.content, 80),
|
||||
notebookId: n.notebookId,
|
||||
updatedAt: n.updatedAt.toISOString(),
|
||||
})),
|
||||
@@ -235,8 +239,10 @@ export async function GET() {
|
||||
pinnedNotes: pinnedNotes.map(n => ({
|
||||
id: n.id,
|
||||
title: n.title,
|
||||
excerpt: excerptNoteContent(n.content, 80),
|
||||
notebookId: n.notebookId,
|
||||
updatedAt: n.updatedAt.toISOString(),
|
||||
notebook: n.notebookId ? notebookMap.get(n.notebookId) || null : null,
|
||||
})),
|
||||
writingActivity,
|
||||
agentSuggestions: agentSuggestions.map(s => ({
|
||||
|
||||
@@ -22,7 +22,7 @@ export async function GET() {
|
||||
|
||||
const userId = session.user.id
|
||||
const locale = await detectUserLanguage()
|
||||
const cacheKey = `briefing:sentiment:${userId}:${locale}`
|
||||
const cacheKey = `briefing:sentiment:${userId}:${locale}:v2`
|
||||
|
||||
try {
|
||||
const cached = await redis.get(cacheKey)
|
||||
@@ -39,7 +39,7 @@ export async function GET() {
|
||||
isArchived: false,
|
||||
updatedAt: { gte: weekAgo },
|
||||
},
|
||||
select: { title: true, content: true },
|
||||
select: { id: true, title: true, content: true, notebookId: true },
|
||||
take: 20,
|
||||
orderBy: { updatedAt: 'desc' },
|
||||
})
|
||||
@@ -64,10 +64,14 @@ export async function GET() {
|
||||
}
|
||||
|
||||
const localeLabel = locale === 'fr' ? 'French' : locale === 'en' ? 'English' : locale
|
||||
const voice = locale === 'fr'
|
||||
? 'Write summary in French, second person singular (tu). Example: « Cette semaine, tu as surtout… ». Never write « the person », « the user », or third person.'
|
||||
: `Write summary in ${localeLabel}, second person ("you"). Never write "the person", "the user", or third person.`
|
||||
const prompt = `Analyze the emotional patterns in these notes from the past week. Return ONLY valid JSON (no markdown, no code fences).
|
||||
Write "summary" and "topTopic" in ${localeLabel} (locale code: ${locale}). Keep dominantEmotion keys in English as listed.
|
||||
${voice}
|
||||
Keep dominantEmotion keys in English as listed. One short sentence for summary. Do not quote secrets, passwords, or URLs.
|
||||
|
||||
{"dominantEmotion":"focused|curious|enthusiastic|frustrated|calm|anxious|creative|reflective","sentimentScore":number from -1 to 1,"emotions":{"focused":number,"curious":number,"enthusiastic":number,"frustrated":number,"calm":number,"anxious":number,"creative":number,"reflective":number},"summary":"one sentence describing the emotional pattern","topTopic":"most discussed topic"}
|
||||
{"dominantEmotion":"focused|curious|enthusiastic|frustrated|calm|anxious|creative|reflective","sentimentScore":number from -1 to 1,"emotions":{"focused":number,"curious":number,"enthusiastic":number,"frustrated":number,"calm":number,"anxious":number,"creative":number,"reflective":number},"summary":"one sentence","topTopic":"most discussed topic"}
|
||||
|
||||
Notes:
|
||||
${snippets.slice(0, 3000)}`
|
||||
@@ -84,7 +88,16 @@ ${snippets.slice(0, 3000)}`
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(jsonMatch[0])
|
||||
const payload = { available: true, ...parsed }
|
||||
const relatedNotes = recentNotes.slice(0, 2).map(n => {
|
||||
const title = n.title?.trim()
|
||||
const plain = n.content.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim()
|
||||
return {
|
||||
id: n.id,
|
||||
title: title || (plain ? (plain.length > 80 ? `${plain.slice(0, 80).trim()}…` : plain) : null),
|
||||
notebookId: n.notebookId,
|
||||
}
|
||||
})
|
||||
const payload = { available: true, ...parsed, relatedNotes }
|
||||
try { await redis.setex(cacheKey, CACHE_TTL_SEC, JSON.stringify(payload)) } catch {}
|
||||
return NextResponse.json(payload)
|
||||
} catch (error) {
|
||||
|
||||
13
memento-note/app/api/public/byok-catalog/route.ts
Normal file
13
memento-note/app/api/public/byok-catalog/route.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { getPublicByokCatalog } from '@/lib/ai/byok-catalog'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
/** Liste publique des fournisseurs / modèles BYOK — aucun secret. */
|
||||
export async function GET() {
|
||||
const providers = await getPublicByokCatalog()
|
||||
return NextResponse.json(
|
||||
{ providers },
|
||||
{ headers: { 'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400' } },
|
||||
)
|
||||
}
|
||||
@@ -1,18 +1,16 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { auth } from '@/auth';
|
||||
import { getEffectiveTier } from '@/lib/entitlements';
|
||||
import { isByokProviderAllowed } from '@/lib/byok';
|
||||
import { fetchLiveModelsForProvider, PROVIDER_MODEL_SUGGESTIONS, type FetchModelsResult } from '@/lib/ai/models-list';
|
||||
import { getActiveByokKey, isByokProviderAllowed } from '@/lib/byok';
|
||||
import { decryptApiKey } from '@/lib/crypto';
|
||||
import { fetchLiveModelsForProvider, type FetchModelsResult } from '@/lib/ai/models-list';
|
||||
import { VALID_PROVIDERS, type AiGatewayProvider } from '@/lib/ai/router';
|
||||
|
||||
// Providers that return static suggestions regardless of key
|
||||
const STATIC_PROVIDERS = new Set(['anthropic', 'anthropic_custom', 'custom_anthropic', 'google', 'minimax']);
|
||||
|
||||
/**
|
||||
* GET /api/user/api-keys/live-models?provider=<provider>[&key=<api_key>][&baseUrl=<url>]
|
||||
*
|
||||
* - Static providers (minimax, anthropic, google): returns suggestions immediately, no key needed.
|
||||
* - Live providers (openai, deepseek…): requires key to fetch live from provider.
|
||||
* Liste les modèles chez le fournisseur. Sans clé (saisie ou déjà enregistrée) : liste vide.
|
||||
* Pas de liste de secours figée.
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
const session = await auth();
|
||||
@@ -27,8 +25,8 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
const { searchParams } = request.nextUrl;
|
||||
const provider = searchParams.get('provider') as AiGatewayProvider;
|
||||
const apiKey = searchParams.get('key') ?? '';
|
||||
const baseUrl = searchParams.get('baseUrl') ?? undefined;
|
||||
let apiKey = searchParams.get('key') ?? '';
|
||||
let baseUrl = searchParams.get('baseUrl') ?? undefined;
|
||||
|
||||
if (!provider) {
|
||||
return NextResponse.json({ error: 'Missing provider' }, { status: 400 });
|
||||
@@ -42,16 +40,20 @@ export async function GET(request: NextRequest) {
|
||||
return NextResponse.json({ error: 'Tier restricted' }, { status: 403 });
|
||||
}
|
||||
|
||||
// Static suggestion providers: return immediately without a key
|
||||
if (STATIC_PROVIDERS.has(provider)) {
|
||||
const base = provider === 'anthropic_custom' || provider === 'custom_anthropic' ? 'anthropic' : provider;
|
||||
const models = PROVIDER_MODEL_SUGGESTIONS[base] ?? [];
|
||||
return NextResponse.json({ success: true, models, fromApi: false });
|
||||
if (!apiKey || apiKey.length < 4) {
|
||||
const saved = await getActiveByokKey(session.user.id, provider, tier);
|
||||
if (saved) {
|
||||
try {
|
||||
apiKey = await decryptApiKey(saved.encryptedKey);
|
||||
if (!baseUrl && saved.baseUrl) baseUrl = saved.baseUrl;
|
||||
} catch {
|
||||
apiKey = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Live providers need a key
|
||||
if (!apiKey || apiKey.length < 4) {
|
||||
return NextResponse.json({ success: true, models: PROVIDER_MODEL_SUGGESTIONS[provider] ?? [], fromApi: false });
|
||||
return NextResponse.json({ success: true, models: [], fromApi: false });
|
||||
}
|
||||
|
||||
const result: FetchModelsResult = await fetchLiveModelsForProvider(provider, apiKey, baseUrl);
|
||||
|
||||
@@ -42,8 +42,8 @@ const jetbrainsMono = JetBrains_Mono({
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Memento - Your Digital Notepad",
|
||||
description: "A beautiful note-taking app built with Next.js 16",
|
||||
title: "Memento — Your Second Brain",
|
||||
description: "A Second Brain that connects while you write. Search, brief, and act on your ideas every morning.",
|
||||
manifest: "/api/manifest",
|
||||
icons: {
|
||||
icon: "/icons/icon-512.svg",
|
||||
|
||||
@@ -55,6 +55,16 @@ const typeConfig: Record<string, { icon: typeof Globe }> = {
|
||||
'task-extractor': { icon: ListChecks },
|
||||
}
|
||||
|
||||
function kebabToCamel(value: string) {
|
||||
return value.replace(/-([a-z])/g, (_, letter: string) => letter.toUpperCase())
|
||||
}
|
||||
|
||||
function resolveStoredLabel(t: (key: string) => string, value: string) {
|
||||
if (!value.startsWith('agents.')) return value
|
||||
const translated = t(value)
|
||||
return translated !== value ? translated : value
|
||||
}
|
||||
|
||||
const frequencyKeys: Record<string, string> = {
|
||||
manual: 'agents.frequencies.manual',
|
||||
hourly: 'agents.frequencies.hourly',
|
||||
@@ -209,9 +219,9 @@ export function AgentCard({ agent, onEdit, onRefresh, onToggle }: AgentCardProps
|
||||
<Icon className="w-5 h-5" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<h4 className="text-[13px] font-bold text-foreground">{agent.name}</h4>
|
||||
<h4 className="text-[13px] font-bold text-foreground">{resolveStoredLabel(t, agent.name)}</h4>
|
||||
<p className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground opacity-60">
|
||||
{t(`agents.types.${agent.type || 'custom'}`)}
|
||||
{t(`agents.types.${kebabToCamel(agent.type || 'custom')}`)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -237,7 +247,7 @@ export function AgentCard({ agent, onEdit, onRefresh, onToggle }: AgentCardProps
|
||||
|
||||
{agent.description && (
|
||||
<p className="text-xs text-muted-foreground leading-relaxed line-clamp-3">
|
||||
{agent.description}
|
||||
{resolveStoredLabel(t, agent.description)}
|
||||
</p>
|
||||
)}
|
||||
|
||||
|
||||
@@ -63,14 +63,26 @@ const templateConfig = [
|
||||
type TemplateId = typeof templateConfig[number]['id']
|
||||
type CategoryId = 'all' | 'veille' | 'digest' | 'tools' | 'generate'
|
||||
|
||||
const CATEGORIES: { id: CategoryId; label: string }[] = [
|
||||
{ id: 'all', label: 'Tous' },
|
||||
{ id: 'veille', label: '📡 Veille' },
|
||||
{ id: 'digest', label: '📰 Digest' },
|
||||
{ id: 'tools', label: '🔧 Outils' },
|
||||
{ id: 'generate', label: '✨ Génération' },
|
||||
const CATEGORIES: { id: CategoryId; labelKey: string }[] = [
|
||||
{ id: 'all', labelKey: 'agents.templates.categoryAll' },
|
||||
{ id: 'veille', labelKey: 'agents.templates.categoryWatch' },
|
||||
{ id: 'digest', labelKey: 'agents.templates.categoryDigest' },
|
||||
{ id: 'tools', labelKey: 'agents.templates.categoryTools' },
|
||||
{ id: 'generate', labelKey: 'agents.templates.categoryGenerate' },
|
||||
]
|
||||
|
||||
const PREVIEW_COUNT = 3
|
||||
|
||||
function templateNameKey(id: string) {
|
||||
return `agents.templates.${id}.name`
|
||||
}
|
||||
|
||||
function hasTranslatedName(t: (key: string) => string, id: string) {
|
||||
const key = templateNameKey(id)
|
||||
const name = t(key)
|
||||
return Boolean(name.trim()) && name !== key
|
||||
}
|
||||
|
||||
const typeIcons: Record<string, typeof Globe> = {
|
||||
scraper: Globe,
|
||||
researcher: Search,
|
||||
@@ -98,6 +110,7 @@ export function AgentTemplates({ onInstalled, existingAgentNames }: AgentTemplat
|
||||
const { t } = useLanguage()
|
||||
const [installingId, setInstallingId] = useState<string | null>(null)
|
||||
const [activeCategory, setActiveCategory] = useState<CategoryId>('all')
|
||||
const [showAll, setShowAll] = useState(false)
|
||||
|
||||
const handleInstall = async (tpl: typeof templateConfig[number]) => {
|
||||
setInstallingId(tpl.id)
|
||||
@@ -142,55 +155,62 @@ export function AgentTemplates({ onInstalled, existingAgentNames }: AgentTemplat
|
||||
}
|
||||
}
|
||||
|
||||
const filtered = activeCategory === 'all'
|
||||
const filtered = (activeCategory === 'all'
|
||||
? templateConfig
|
||||
: templateConfig.filter((tpl) => tpl.category === activeCategory)
|
||||
).filter((tpl) => hasTranslatedName(t, tpl.id))
|
||||
|
||||
const visible = showAll ? filtered : filtered.slice(0, PREVIEW_COUNT)
|
||||
const hiddenCount = filtered.length - visible.length
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
{/* Category filter */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{CATEGORIES.map((cat) => (
|
||||
<button
|
||||
key={cat.id}
|
||||
onClick={() => setActiveCategory(cat.id)}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setActiveCategory(cat.id)
|
||||
setShowAll(false)
|
||||
}}
|
||||
className={`px-3 py-1.5 rounded-full text-xs font-semibold transition-all border ${
|
||||
activeCategory === cat.id
|
||||
? 'bg-ink text-paper border-ink'
|
||||
: 'bg-paper text-muted-ink border-border/40 hover:border-ink/30'
|
||||
}`}
|
||||
>
|
||||
{cat.label}
|
||||
{t(cat.labelKey)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Template grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{filtered.map(tpl => {
|
||||
{visible.map(tpl => {
|
||||
const Icon = templateIcons[tpl.id as TemplateId] ?? typeIcons[tpl.type] ?? Settings
|
||||
const isInstalling = installingId === tpl.id
|
||||
const nameKey = `agents.templates.${tpl.id}.name`
|
||||
const nameKey = templateNameKey(tpl.id)
|
||||
const descKey = `agents.templates.${tpl.id}.description`
|
||||
|
||||
return (
|
||||
<div
|
||||
key={tpl.id}
|
||||
className="bg-card/40 border border-dashed border-border rounded-2xl p-6 group cursor-pointer hover:bg-card hover:border-foreground/20 transition-all"
|
||||
className="bg-card/40 border border-dashed border-border rounded-2xl p-6 hover:bg-card hover:border-foreground/20 transition-all"
|
||||
>
|
||||
<div className="w-8 h-8 rounded-lg bg-muted flex items-center justify-center text-muted-foreground group-hover:bg-foreground group-hover:text-background mb-4 transition-all">
|
||||
<div className="w-8 h-8 rounded-lg bg-muted flex items-center justify-center text-muted-foreground mb-4">
|
||||
<Icon className="w-4 h-4" />
|
||||
</div>
|
||||
<div className="flex items-start justify-between gap-2 mb-2">
|
||||
<h4 className="text-[13px] font-bold text-foreground">{t(nameKey)}</h4>
|
||||
{tpl.frequency !== 'manual' && (
|
||||
<span className="text-[10px] font-semibold text-concrete bg-border/20 rounded-full px-2 py-0.5 shrink-0">
|
||||
{tpl.frequency === 'daily' ? '📅 Quotidien' : '📆 Hebdo'}
|
||||
{t(`agents.frequencies.${tpl.frequency}`)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground leading-relaxed mb-4">{t(descKey)}</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleInstall(tpl)}
|
||||
disabled={isInstalling}
|
||||
className="text-[11px] font-bold uppercase tracking-widest text-foreground hover:opacity-60 transition-opacity flex items-center gap-2 disabled:opacity-50"
|
||||
@@ -211,6 +231,25 @@ export function AgentTemplates({ onInstalled, existingAgentNames }: AgentTemplat
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{hiddenCount > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowAll(true)}
|
||||
className="text-[12px] font-semibold text-foreground hover:opacity-70 transition-opacity"
|
||||
>
|
||||
{t('agents.templates.seeAll')}
|
||||
</button>
|
||||
)}
|
||||
{showAll && filtered.length > PREVIEW_COUNT && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowAll(false)}
|
||||
className="text-[12px] font-semibold text-foreground hover:opacity-70 transition-opacity"
|
||||
>
|
||||
{t('agents.templates.showLess')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,26 +16,16 @@ import {
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
// ─── Provider display info ────────────────────────────────────────────────────
|
||||
const PROVIDER_INFO: Record<string, { name: string; hint: string }> = {
|
||||
openai: { name: 'OpenAI', hint: 'GPT-4o, GPT-4' },
|
||||
anthropic: { name: 'Anthropic', hint: 'Claude 3.5 Sonnet, Haiku' },
|
||||
minimax: { name: 'MiniMax', hint: 'MiniMax-M2.7, M2.5' },
|
||||
google: { name: 'Google AI', hint: 'Gemini 1.5 Flash, Pro' },
|
||||
deepseek: { name: 'DeepSeek', hint: 'DeepSeek Chat, Reasoner' },
|
||||
openrouter: { name: 'OpenRouter', hint: 'Multi-provider access' },
|
||||
mistral: { name: 'Mistral AI', hint: 'Mistral Small, Large' },
|
||||
glm: { name: 'GLM (Zhipu)', hint: 'GLM-4, GLM-4-Flash' },
|
||||
zai: { name: 'Zuki Journey', hint: 'OpenAI/Anthropic proxy' },
|
||||
anthropic_custom: { name: 'Anthropic (custom)', hint: 'Anthropic-compatible proxy' },
|
||||
custom_openai: { name: 'Compatible OpenAI', hint: 'Any OpenAI-compatible proxy' },
|
||||
custom_anthropic: { name: 'Compatible Anthropic', hint: 'Any Anthropic-compatible proxy' },
|
||||
custom: { name: 'Custom API', hint: 'Your own endpoint' },
|
||||
}
|
||||
import { providerDisplayName } from '@/lib/ai/provider-labels'
|
||||
import { PROVIDER_MODEL_SUGGESTIONS } from '@/lib/ai/models-list'
|
||||
|
||||
function displayName(provider: string): string {
|
||||
return PROVIDER_INFO[provider]?.name ?? provider
|
||||
return providerDisplayName(provider)
|
||||
}
|
||||
|
||||
function providerHint(provider: string): string {
|
||||
const models = PROVIDER_MODEL_SUGGESTIONS[provider] ?? []
|
||||
return models.slice(0, 2).join(', ')
|
||||
}
|
||||
|
||||
const MANUAL_MODEL_PROVIDERS = new Set(['custom'])
|
||||
@@ -155,20 +145,20 @@ function EditKeyForm({
|
||||
const showModelInput = manualModel || (!loadingModels && models.length === 0)
|
||||
|
||||
return (
|
||||
<div className="mt-2 border border-violet-500/20 rounded-xl bg-violet-500/5 p-4 space-y-3">
|
||||
<p className="text-[10px] font-bold uppercase tracking-widest text-violet-600 dark:text-violet-400">
|
||||
<div className="mt-2 border border-brand-accent/20 rounded-xl bg-brand-accent/5 p-4 space-y-3">
|
||||
<p className="text-[13px] font-semibold uppercase tracking-wider text-brand-accent">
|
||||
{t('byok.editLabel', { name: displayName(savedKey.provider) })}
|
||||
</p>
|
||||
|
||||
{/* Alias */}
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor={`edit-alias-${savedKey.provider}`} className="text-[9px] font-semibold uppercase tracking-widest text-concrete">{t('byok.aliasLabel')}</Label>
|
||||
<Label htmlFor={`edit-alias-${savedKey.provider}`} className="text-[13px] font-medium uppercase tracking-wider text-concrete">{t('byok.aliasLabel')}</Label>
|
||||
<Input id={`edit-alias-${savedKey.provider}`} value={alias} onChange={(e) => setAlias(e.target.value)} placeholder={t('byok.aliasPlaceholder')} />
|
||||
</div>
|
||||
{needsUrl && (
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor={`edit-baseurl-${savedKey.provider}`} className="text-[9px] font-semibold uppercase tracking-widest text-concrete">{t('byok.apiUrl')}</Label>
|
||||
<Label htmlFor={`edit-baseurl-${savedKey.provider}`} className="text-[13px] font-medium uppercase tracking-wider text-concrete">{t('byok.apiUrl')}</Label>
|
||||
<Input id={`edit-baseurl-${savedKey.provider}`} value={baseUrl} onChange={(e) => setBaseUrl(e.target.value.trim())} placeholder="https://api.example.com/v1" />
|
||||
</div>
|
||||
)}
|
||||
@@ -176,7 +166,7 @@ function EditKeyForm({
|
||||
|
||||
{/* Model */}
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor={`edit-model-${savedKey.provider}`} className="text-[9px] font-semibold uppercase tracking-widest text-concrete">{t('byok.model')}</Label>
|
||||
<Label htmlFor={`edit-model-${savedKey.provider}`} className="text-[13px] font-medium uppercase tracking-wider text-concrete">{t('byok.model')}</Label>
|
||||
{showModelDropdown ? (
|
||||
<Select value={model} onValueChange={setModel}>
|
||||
<SelectTrigger id={`edit-model-${savedKey.provider}`}><SelectValue placeholder={t('byok.choose')} /></SelectTrigger>
|
||||
@@ -189,7 +179,7 @@ function EditKeyForm({
|
||||
|
||||
{/* Key rotation (optional) */}
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor={`edit-key-${savedKey.provider}`} className="text-[9px] font-semibold uppercase tracking-widest text-concrete">
|
||||
<Label htmlFor={`edit-key-${savedKey.provider}`} className="text-[13px] font-medium uppercase tracking-wider text-concrete">
|
||||
{t('byok.newKey')} <span className="normal-case font-normal text-concrete">{t('byok.newKeyHint')}</span>
|
||||
</Label>
|
||||
<Input
|
||||
@@ -205,7 +195,7 @@ function EditKeyForm({
|
||||
{/* Test result */}
|
||||
{testResult && (
|
||||
<div className={cn(
|
||||
'flex items-start gap-2 rounded-lg px-3 py-2 text-[10px] border',
|
||||
'flex items-start gap-2 rounded-lg px-3 py-2 text-[13px] border',
|
||||
testResult.ok ? 'bg-emerald-500/10 border-emerald-500/20 text-emerald-700 dark:text-emerald-400' : 'bg-rose-500/10 border-rose-500/20 text-rose-700 dark:text-rose-400'
|
||||
)}>
|
||||
{testResult.ok ? <CheckCircle2 size={12} className="shrink-0 mt-0.5" /> : <XCircle size={12} className="shrink-0 mt-0.5" />}
|
||||
@@ -224,7 +214,7 @@ function EditKeyForm({
|
||||
type="button"
|
||||
disabled={!newKey || newKey.length < 8 || testing}
|
||||
onClick={testModel}
|
||||
className="flex items-center gap-1.5 px-3 py-2 rounded-lg text-[9px] font-bold uppercase tracking-[0.1em] border border-border bg-white dark:bg-white/5 hover:border-violet-400 transition-colors disabled:opacity-40 disabled:pointer-events-none"
|
||||
className="flex items-center gap-1.5 px-3 py-2 rounded-lg text-[13px] font-semibold uppercase tracking-[0.1em] border border-border bg-white dark:bg-white/5 hover:border-brand-accent/50 transition-colors disabled:opacity-40 disabled:pointer-events-none"
|
||||
>
|
||||
{testing ? <Loader2 size={11} className="animate-spin" /> : <FlaskConical size={11} />}
|
||||
{t('byok.test')}
|
||||
@@ -233,7 +223,7 @@ function EditKeyForm({
|
||||
type="button"
|
||||
disabled={saveMutation.isPending}
|
||||
onClick={() => saveMutation.mutate()}
|
||||
className="flex-1 py-2 rounded-lg text-[9px] font-bold uppercase tracking-[0.12em] bg-ink text-paper shadow hover:scale-[1.01] active:scale-[0.99] transition-all disabled:opacity-40"
|
||||
className="flex-1 py-2 rounded-lg text-[13px] font-semibold uppercase tracking-[0.12em] bg-brand-accent text-white shadow hover:scale-[1.01] active:scale-[0.99] transition-all disabled:opacity-40"
|
||||
>
|
||||
{saveMutation.isPending ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : t('byok.saveChanges')}
|
||||
</Button>
|
||||
@@ -286,13 +276,6 @@ export function ByokSettingsPanel() {
|
||||
setTestResult(null)
|
||||
setModel('')
|
||||
setModels([])
|
||||
if (MANUAL_MODEL_PROVIDERS.has(p)) return
|
||||
setLoadingModels(true)
|
||||
try {
|
||||
const list = await fetchModelsFromServer(p)
|
||||
setModels(list)
|
||||
if (list.length > 0) setModel(list[0])
|
||||
} finally { setLoadingModels(false) }
|
||||
}
|
||||
|
||||
async function refreshModels(p: string, key: string, _baseUrl?: string) {
|
||||
@@ -403,10 +386,10 @@ export function ByokSettingsPanel() {
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex items-start gap-5">
|
||||
<div className="p-3 bg-violet-500/10 rounded-2xl text-violet-500 border border-violet-500/20"><KeyRound size={18} /></div>
|
||||
<div className="p-3 bg-brand-accent/10 rounded-2xl text-brand-accent border border-brand-accent/20"><KeyRound size={18} /></div>
|
||||
<div className="flex-1 space-y-1">
|
||||
<h3 className="text-[13px] font-bold text-ink">{t('byokSettings.title')}</h3>
|
||||
<p className="text-[10px] text-concrete leading-relaxed">{t('byokSettings.description')}</p>
|
||||
<p className="text-sm text-concrete leading-relaxed">{t('byokSettings.description')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -419,7 +402,7 @@ export function ByokSettingsPanel() {
|
||||
: 'bg-amber-500/10 text-amber-700 dark:text-amber-400 border-amber-500/20'
|
||||
)}>
|
||||
{activeKey ? (
|
||||
<><Zap size={14} className="shrink-0" /><span>{t('byok.byokActive')} · <strong>{displayName(activeKey.provider)}</strong>{activeKey.model && <> · <code className="font-mono text-[10px]">{activeKey.model}</code></>}{activeKey.alias && <> · {activeKey.alias}</>}</span></>
|
||||
<><Zap size={14} className="shrink-0" /><span>{t('byok.byokActive')} · <strong>{displayName(activeKey.provider)}</strong>{activeKey.model && <> · <code className="font-mono text-[13px]">{activeKey.model}</code></>}{activeKey.alias && <> · {activeKey.alias}</>}</span></>
|
||||
) : (
|
||||
<><Shield size={14} className="shrink-0" /><span>{t('byok.noActiveKey')}</span></>
|
||||
)}
|
||||
@@ -433,7 +416,7 @@ export function ByokSettingsPanel() {
|
||||
{/* Saved keys */}
|
||||
{keys.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-[10px] font-bold uppercase tracking-widest text-concrete">{t('byok.savedKeys')}</p>
|
||||
<p className="text-[13px] font-semibold uppercase tracking-wider text-concrete">{t('byok.savedKeys')}</p>
|
||||
<ul className="space-y-1">
|
||||
{keys.map((key) => (
|
||||
<li key={key.provider}>
|
||||
@@ -447,9 +430,9 @@ export function ByokSettingsPanel() {
|
||||
<div>
|
||||
<div className="text-[13px] font-semibold text-ink leading-tight">{displayName(key.provider)}</div>
|
||||
<div className="flex items-center gap-2 mt-0.5 flex-wrap">
|
||||
{key.model && <code className="text-[9px] font-mono text-brand-accent bg-brand-accent/10 px-1.5 py-0.5 rounded">{key.model}</code>}
|
||||
{key.alias && <span className="text-[9px] text-concrete">{key.alias}</span>}
|
||||
<span className={cn('text-[9px] font-medium', key.isActive ? 'text-emerald-600 dark:text-emerald-400' : 'text-concrete')}>
|
||||
{key.model && <code className="text-[13px] font-mono text-brand-accent bg-brand-accent/10 px-1.5 py-0.5 rounded">{key.model}</code>}
|
||||
{key.alias && <span className="text-[13px] text-concrete">{key.alias}</span>}
|
||||
<span className={cn('text-[13px] font-medium', key.isActive ? 'text-emerald-600 dark:text-emerald-400' : 'text-concrete')}>
|
||||
{key.isActive ? t('byok.activeStatus') : t('byok.inactiveStatus')}
|
||||
</span>
|
||||
</div>
|
||||
@@ -462,7 +445,7 @@ export function ByokSettingsPanel() {
|
||||
className={cn(
|
||||
'h-7 w-7 rounded-lg flex items-center justify-center transition-colors',
|
||||
editingProvider === key.provider
|
||||
? 'text-violet-600 bg-violet-500/10 border border-violet-500/30'
|
||||
? 'text-brand-accent bg-brand-accent/10 border border-brand-accent/30'
|
||||
: 'text-concrete hover:text-ink hover:bg-muted border border-transparent'
|
||||
)}
|
||||
onClick={() => setEditingProvider(editingProvider === key.provider ? null : key.provider)}
|
||||
@@ -492,7 +475,7 @@ export function ByokSettingsPanel() {
|
||||
|
||||
{/* Inline edit form */}
|
||||
{editingProvider === key.provider && (
|
||||
<div className="border border-violet-500/20 border-t-0 rounded-b-xl overflow-hidden">
|
||||
<div className="border border-brand-accent/20 border-t-0 rounded-b-xl overflow-hidden">
|
||||
<EditKeyForm
|
||||
savedKey={key}
|
||||
onDone={() => setEditingProvider(null)}
|
||||
@@ -508,40 +491,40 @@ export function ByokSettingsPanel() {
|
||||
|
||||
{/* Add key form */}
|
||||
<div className="space-y-4 border border-border/60 rounded-2xl p-5">
|
||||
<p className="text-[10px] font-bold uppercase tracking-widest text-concrete">
|
||||
<p className="text-[13px] font-semibold uppercase tracking-wider text-concrete">
|
||||
{keys.length > 0 ? t('byok.addOrReplace') : t('byok.connectProvider')}
|
||||
</p>
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="byok-provider" className="text-[10px] font-semibold uppercase tracking-widest text-concrete">{t('byokSettings.provider')}</Label>
|
||||
<Label htmlFor="byok-provider" className="text-[13px] font-medium uppercase tracking-wider text-concrete">{t('byokSettings.provider')}</Label>
|
||||
<Select value={provider} onValueChange={onProviderChange} disabled={saveMutation.isPending}>
|
||||
<SelectTrigger id="byok-provider"><SelectValue placeholder={t('byok.chooseProvider')} /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{allowed.map((p) => (
|
||||
<SelectItem key={p} value={p}>
|
||||
<span className="font-medium">{displayName(p)}</span>
|
||||
{PROVIDER_INFO[p]?.hint && <span className="ml-2 text-[10px] text-concrete">{PROVIDER_INFO[p].hint}</span>}
|
||||
{providerHint(p) && <span className="ml-2 text-[13px] text-concrete">{providerHint(p)}</span>}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="byok-alias" className="text-[10px] font-semibold uppercase tracking-widest text-concrete">{t('byok.aliasLabel')} <span className="normal-case font-normal">{t('byok.optional')}</span></Label>
|
||||
<Label htmlFor="byok-alias" className="text-[13px] font-medium uppercase tracking-wider text-concrete">{t('byok.aliasLabel')} <span className="normal-case font-normal">{t('byok.optional')}</span></Label>
|
||||
<Input id="byok-alias" value={alias} onChange={(e) => setAlias(e.target.value)} placeholder={t('byok.aliasPlaceholder')} disabled={saveMutation.isPending} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{NEEDS_BASE_URL.has(provider) && (
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="byok-baseurl" className="text-[10px] font-semibold uppercase tracking-widest text-concrete">{t('byok.apiUrl')}</Label>
|
||||
<Label htmlFor="byok-baseurl" className="text-[13px] font-medium uppercase tracking-wider text-concrete">{t('byok.apiUrl')}</Label>
|
||||
<Input id="byok-baseurl" value={baseUrl} onChange={(e) => setBaseUrl(e.target.value.trim())} placeholder="https://api.example.com/v1" disabled={saveMutation.isPending} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="byok-key" className="text-[10px] font-semibold uppercase tracking-widest text-concrete">{t('byok.apiKey')}</Label>
|
||||
<Label htmlFor="byok-key" className="text-[13px] font-medium uppercase tracking-wider text-concrete">{t('byok.apiKey')}</Label>
|
||||
<div className="flex gap-2">
|
||||
<div className="relative flex-1">
|
||||
<Input
|
||||
@@ -554,8 +537,8 @@ export function ByokSettingsPanel() {
|
||||
</div>
|
||||
<button
|
||||
type="button" disabled={!provider || apiKey.length < 8 || verifying} onClick={verifyKey}
|
||||
className={cn('shrink-0 px-4 py-2 rounded-xl text-[10px] font-bold uppercase tracking-[0.1em] transition-all border disabled:opacity-40 disabled:pointer-events-none',
|
||||
keyOk ? 'bg-emerald-500/10 border-emerald-500/40 text-emerald-700 dark:text-emerald-400' : 'bg-white dark:bg-white/5 border-border hover:border-violet-400')}
|
||||
className={cn('shrink-0 px-4 py-2 rounded-xl text-[13px] font-semibold uppercase tracking-[0.1em] transition-all border disabled:opacity-40 disabled:pointer-events-none',
|
||||
keyOk ? 'bg-emerald-500/10 border-emerald-500/40 text-emerald-700 dark:text-emerald-400' : 'bg-white dark:bg-white/5 border-border hover:border-brand-accent/50')}
|
||||
>
|
||||
{verifying ? <Loader2 className="h-4 w-4 animate-spin" /> : keyOk ? <CheckCircle2 className="h-4 w-4" /> : t('byok.verify')}
|
||||
</button>
|
||||
@@ -564,7 +547,7 @@ export function ByokSettingsPanel() {
|
||||
|
||||
{provider && (
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="byok-model" className="text-[10px] font-semibold uppercase tracking-widest text-concrete">{t('byok.model')}</Label>
|
||||
<Label htmlFor="byok-model" className="text-[13px] font-medium uppercase tracking-wider text-concrete">{t('byok.model')}</Label>
|
||||
{loadingModels ? (
|
||||
<div className="flex items-center gap-2 text-xs text-concrete py-2"><Loader2 className="h-3 w-3 animate-spin" />{t('byok.fetchingModels')}</div>
|
||||
) : showModelDropdown ? (
|
||||
@@ -593,13 +576,13 @@ export function ByokSettingsPanel() {
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="button" disabled={!canTest || testing || saveMutation.isPending} onClick={testModel}
|
||||
className="flex items-center gap-2 px-4 py-2.5 rounded-xl text-[10px] font-bold uppercase tracking-[0.1em] transition-all border bg-white dark:bg-white/5 border-border hover:border-violet-400 disabled:opacity-40 disabled:pointer-events-none"
|
||||
className="flex items-center gap-2 px-4 py-2.5 rounded-xl text-[13px] font-semibold uppercase tracking-[0.1em] transition-all border bg-white dark:bg-white/5 border-border hover:border-brand-accent/50 disabled:opacity-40 disabled:pointer-events-none"
|
||||
>
|
||||
{testing ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <FlaskConical size={13} />}{t('byok.test')}
|
||||
</button>
|
||||
<Button
|
||||
type="button" disabled={saveDisabled} onClick={() => saveMutation.mutate()}
|
||||
className="flex-1 py-2.5 rounded-xl text-[10px] font-bold uppercase tracking-[0.15em] transition-all duration-200 bg-ink text-paper shadow-lg hover:scale-[1.01] active:scale-[0.99] disabled:opacity-40 disabled:pointer-events-none disabled:shadow-none"
|
||||
className="flex-1 py-2.5 rounded-xl text-[13px] font-semibold uppercase tracking-[0.15em] transition-all duration-200 bg-brand-accent text-white shadow-lg hover:scale-[1.01] active:scale-[0.99] disabled:opacity-40 disabled:pointer-events-none disabled:shadow-none"
|
||||
>
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
{saveMutation.isPending && <Loader2 className="h-4 w-4 animate-spin" />}
|
||||
|
||||
@@ -9,6 +9,8 @@ import { getNoteById, deleteNote, toggleArchive } from '@/app/actions/notes'
|
||||
import { emitNoteChange } from '@/lib/note-change-sync'
|
||||
import { toast } from 'sonner'
|
||||
import { useLanguage } from '@/lib/i18n'
|
||||
import { ConfirmDeleteNoteDialog } from '@/components/confirm-delete-note-dialog'
|
||||
import { showNoteTrashedToast } from '@/lib/notes/trash-toast'
|
||||
|
||||
const NoteEditor = dynamic(
|
||||
() => import('@/components/note-editor').then(m => ({ default: m.NoteEditor })),
|
||||
@@ -23,6 +25,7 @@ export function ArchiveClient({ notes: initialNotes }: ArchiveClientProps) {
|
||||
const { t } = useLanguage()
|
||||
const [notes, setNotes] = useState<Note[]>(initialNotes)
|
||||
const [editingNote, setEditingNote] = useState<{ note: Note; readOnly: boolean } | null>(null)
|
||||
const [notePendingDelete, setNotePendingDelete] = useState<Note | null>(null)
|
||||
|
||||
const handleOpen = useCallback(async (note: Note, readOnly = false) => {
|
||||
const fresh = await getNoteById(note.id)
|
||||
@@ -50,17 +53,24 @@ export function ArchiveClient({ notes: initialNotes }: ArchiveClientProps) {
|
||||
}
|
||||
}, [t])
|
||||
|
||||
const handleDeleteNote = useCallback(async (note: Note) => {
|
||||
const handleDeleteNote = useCallback((note: Note) => {
|
||||
setNotePendingDelete(note)
|
||||
}, [])
|
||||
|
||||
const confirmDeleteNote = useCallback(async () => {
|
||||
const note = notePendingDelete
|
||||
if (!note) return
|
||||
setNotePendingDelete(null)
|
||||
setNotes((prev) => prev.filter((n) => n.id !== note.id))
|
||||
try {
|
||||
await deleteNote(note.id, { skipRevalidation: true })
|
||||
emitNoteChange({ type: 'deleted', noteId: note.id, notebookId: note.notebookId })
|
||||
toast.success(t('notes.deleted') || 'Note supprimée')
|
||||
showNoteTrashedToast(note, t, () => setNotes((prev) => [note, ...prev]))
|
||||
} catch {
|
||||
setNotes((prev) => [note, ...prev])
|
||||
toast.error(t('general.error'))
|
||||
}
|
||||
}, [t])
|
||||
}, [notePendingDelete, t])
|
||||
|
||||
if (editingNote) {
|
||||
return (
|
||||
@@ -78,11 +88,20 @@ export function ArchiveClient({ notes: initialNotes }: ArchiveClientProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<NotesEditorialView
|
||||
notes={notes}
|
||||
onOpen={handleOpen}
|
||||
onArchiveNote={handleArchiveNote}
|
||||
onDeleteNote={handleDeleteNote}
|
||||
/>
|
||||
<ConfirmDeleteNoteDialog
|
||||
open={notePendingDelete != null}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) setNotePendingDelete(null)
|
||||
}}
|
||||
onConfirm={confirmDeleteNote}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
52
memento-note/components/auth-shell.tsx
Normal file
52
memento-note/components/auth-shell.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
'use client';
|
||||
|
||||
import { useLanguage } from '@/lib/i18n/LanguageProvider';
|
||||
import Link from 'next/link';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
|
||||
function AuthHeader() {
|
||||
const { t } = useLanguage();
|
||||
|
||||
return (
|
||||
<header className="p-6 md:p-8 flex justify-between items-center relative z-10">
|
||||
<Link
|
||||
href="/"
|
||||
aria-label={t('general.back')}
|
||||
className="flex items-center gap-2 text-[var(--muted-foreground)] hover:text-[var(--foreground)] transition-colors group"
|
||||
>
|
||||
<div className="w-8 h-8 rounded-full border border-[var(--border)] flex items-center justify-center group-hover:border-[var(--color-brand-accent)] transition-colors">
|
||||
<ArrowLeft size={14} className="group-hover:-translate-x-0.5 transition-transform rtl:rotate-180" />
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Link href="/" className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 bg-[var(--foreground)] text-[var(--background)] rounded-xl flex items-center justify-center shadow-lg">
|
||||
<span className="font-serif font-bold text-xl">M</span>
|
||||
</div>
|
||||
<span className="font-serif text-xl font-medium tracking-tight">Memento</span>
|
||||
</Link>
|
||||
|
||||
<div className="w-8" />
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
export function AuthShell({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#FDFCFB] dark:bg-[#0D0D0D] flex flex-col relative overflow-hidden">
|
||||
<div className="absolute top-[-10%] right-[-10%] w-[50%] h-[50%] bg-[var(--color-brand-accent)]/5 blur-[120px] rounded-full pointer-events-none" />
|
||||
<div className="absolute bottom-[-10%] left-[-10%] w-[50%] h-[50%] bg-[#D4A373]/5 blur-[120px] rounded-full pointer-events-none" />
|
||||
|
||||
<AuthHeader />
|
||||
|
||||
<main className="flex-1 flex items-center justify-center p-4 md:p-6 relative z-10">
|
||||
<div className="w-full max-w-md">
|
||||
{children}
|
||||
<p className="text-center mt-8 text-[9px] text-[var(--muted-foreground)] font-bold uppercase tracking-[0.3em] opacity-40 select-none">
|
||||
© 2026 Memento
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
42
memento-note/components/confirm-delete-note-dialog.tsx
Normal file
42
memento-note/components/confirm-delete-note-dialog.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
'use client'
|
||||
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import { useLanguage } from '@/lib/i18n'
|
||||
|
||||
export function ConfirmDeleteNoteDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
onConfirm,
|
||||
}: {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
onConfirm: () => void | Promise<void>
|
||||
}) {
|
||||
const { t } = useLanguage()
|
||||
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{t('notes.confirmDeleteTitle')}</AlertDialogTitle>
|
||||
<AlertDialogDescription>{t('notes.confirmDelete')}</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>{t('common.cancel')}</AlertDialogCancel>
|
||||
<AlertDialogAction variant="destructive" onClick={() => { void onConfirm() }}>
|
||||
{t('notes.delete')}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)
|
||||
}
|
||||
@@ -86,18 +86,17 @@ export function DashboardActionStrip({
|
||||
const Wrapper = item.pulse && !prefersReducedMotion ? motion.button : 'button'
|
||||
const motionProps = item.pulse && !prefersReducedMotion
|
||||
? {
|
||||
key: `inbox-pulse-${inboxPulse}`,
|
||||
animate: { scale: [1, 1.03, 1] },
|
||||
transition: { duration: 0.45 },
|
||||
}
|
||||
: {}
|
||||
return (
|
||||
<Wrapper
|
||||
key={item.key}
|
||||
key={item.pulse && !prefersReducedMotion ? `inbox-pulse-${inboxPulse}` : item.key}
|
||||
type="button"
|
||||
onClick={item.onClick}
|
||||
{...motionProps}
|
||||
className={`shrink-0 flex items-center gap-2.5 px-3.5 py-2.5 rounded-xl border transition-all text-start min-w-[108px] ${
|
||||
className={`shrink-0 flex items-center gap-2.5 px-3.5 py-2.5 rounded-xl border transition-all text-start min-w-[128px] ${
|
||||
item.accent
|
||||
? 'border-brand-accent/30 bg-brand-accent/[0.06] hover:border-brand-accent/50 hover:bg-brand-accent/10 shadow-sm'
|
||||
: 'border-border/25 bg-white/60 dark:bg-zinc-900/40 hover:border-border/50'
|
||||
@@ -114,7 +113,7 @@ export function DashboardActionStrip({
|
||||
}`}>
|
||||
{item.value}
|
||||
</p>
|
||||
<p className="text-[8px] font-mono font-bold uppercase tracking-wider text-concrete truncate mt-0.5">
|
||||
<p className="text-[13px] font-medium text-concrete truncate mt-0.5">
|
||||
{item.label}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useState, useRef } from 'react'
|
||||
import { motion, AnimatePresence } from 'motion/react'
|
||||
import { Search, Bot, ChevronLeft, ChevronRight, Loader2 } from 'lucide-react'
|
||||
import { Search, Bot, ChevronLeft, ChevronRight, Loader2, Check } from 'lucide-react'
|
||||
import { useLanguage } from '@/lib/i18n'
|
||||
import { DashboardWidgetTitleRow } from '@/components/dashboard-widget-title-row'
|
||||
|
||||
@@ -14,6 +14,11 @@ export interface AgentSuggestion {
|
||||
suggestedFrequency: string
|
||||
}
|
||||
|
||||
export interface CreatedAgentNotice {
|
||||
id: string | null
|
||||
topic: string
|
||||
}
|
||||
|
||||
export interface DashboardAgentCarouselProps {
|
||||
suggestions: AgentSuggestion[]
|
||||
loading?: boolean
|
||||
@@ -21,6 +26,9 @@ export interface DashboardAgentCarouselProps {
|
||||
formatFrequency: (f: string) => string
|
||||
onAccept: (id: string) => void
|
||||
onDismiss: (id: string) => void
|
||||
createdAgent?: CreatedAgentNotice | null
|
||||
onOpenCreated?: () => void
|
||||
onClearCreated?: () => void
|
||||
prefersReducedMotion?: boolean
|
||||
}
|
||||
|
||||
@@ -31,11 +39,15 @@ export function DashboardAgentCarousel({
|
||||
formatFrequency,
|
||||
onAccept,
|
||||
onDismiss,
|
||||
createdAgent,
|
||||
onOpenCreated,
|
||||
onClearCreated,
|
||||
prefersReducedMotion,
|
||||
}: DashboardAgentCarouselProps) {
|
||||
const { t } = useLanguage()
|
||||
const [idx, setIdx] = useState(0)
|
||||
const directionRef = useRef(1)
|
||||
const safeIdx = suggestions.length === 0 ? 0 : Math.min(idx, suggestions.length - 1)
|
||||
|
||||
const goPrev = () => {
|
||||
directionRef.current = -1
|
||||
@@ -46,24 +58,27 @@ export function DashboardAgentCarousel({
|
||||
setIdx(i => Math.min(suggestions.length - 1, i + 1))
|
||||
}
|
||||
|
||||
const navActions = suggestions.length > 1 ? (
|
||||
const showCreated = !!createdAgent
|
||||
const showNav = !showCreated && suggestions.length > 1
|
||||
|
||||
const navActions = showNav ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={goPrev}
|
||||
disabled={idx === 0}
|
||||
disabled={safeIdx === 0}
|
||||
className="p-1 rounded border border-border/30 disabled:opacity-25"
|
||||
aria-label={t('homeDashboard.intelPrev')}
|
||||
>
|
||||
<ChevronLeft size={12} />
|
||||
</button>
|
||||
<span className="text-[8px] font-mono text-concrete px-1">
|
||||
{idx + 1}/{suggestions.length}
|
||||
{safeIdx + 1}/{suggestions.length}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={goNext}
|
||||
disabled={idx >= suggestions.length - 1}
|
||||
disabled={safeIdx >= suggestions.length - 1}
|
||||
className="p-1 rounded border border-border/30 disabled:opacity-25"
|
||||
aria-label={t('homeDashboard.intelNext')}
|
||||
>
|
||||
@@ -83,9 +98,18 @@ export function DashboardAgentCarousel({
|
||||
icon={<Bot size={12} className="text-brand-accent" />}
|
||||
title={t('homeDashboard.suggestedResearch')}
|
||||
actions={navActions}
|
||||
wrapTitle
|
||||
/>
|
||||
|
||||
{suggestions.length === 0 ? (
|
||||
{showCreated && createdAgent ? (
|
||||
<CreatedAgentPanel
|
||||
topic={createdAgent.topic}
|
||||
hasMore={suggestions.length > 0}
|
||||
onOpen={onOpenCreated}
|
||||
onSeeNext={onClearCreated}
|
||||
t={t}
|
||||
/>
|
||||
) : suggestions.length === 0 ? (
|
||||
<div className="rounded-xl border border-dashed border-border/35 bg-stone-50/50 dark:bg-zinc-950/30 p-3">
|
||||
<p className="text-[11px] text-concrete leading-relaxed">
|
||||
{t('homeDashboard.agentsEmpty')}
|
||||
@@ -93,7 +117,7 @@ export function DashboardAgentCarousel({
|
||||
</div>
|
||||
) : (
|
||||
<AgentSlide
|
||||
current={suggestions[idx]}
|
||||
current={suggestions[safeIdx]}
|
||||
direction={directionRef.current}
|
||||
actingId={actingId}
|
||||
formatFrequency={formatFrequency}
|
||||
@@ -107,6 +131,54 @@ export function DashboardAgentCarousel({
|
||||
)
|
||||
}
|
||||
|
||||
function CreatedAgentPanel({
|
||||
topic,
|
||||
hasMore,
|
||||
onOpen,
|
||||
onSeeNext,
|
||||
t,
|
||||
}: {
|
||||
topic: string
|
||||
hasMore: boolean
|
||||
onOpen?: () => void
|
||||
onSeeNext?: () => void
|
||||
t: (key: string) => string
|
||||
}) {
|
||||
return (
|
||||
<div className="p-3.5 rounded-xl border border-border/25 bg-stone-50/60 dark:bg-zinc-950/40">
|
||||
<div className="flex items-start gap-2 mb-2">
|
||||
<Check size={14} className="text-brand-accent shrink-0 mt-0.5" />
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-semibold text-ink dark:text-dark-ink leading-snug">
|
||||
{t('homeDashboard.agentCreatedInCard')}
|
||||
</p>
|
||||
{topic ? (
|
||||
<p className="text-[11px] text-concrete leading-relaxed mt-1">{topic}</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2 mt-3">
|
||||
{hasMore && onSeeNext ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onSeeNext}
|
||||
className="text-[9px] font-mono uppercase px-2.5 py-1.5 rounded-lg border border-border/40 text-concrete hover:text-ink transition-colors"
|
||||
>
|
||||
{t('homeDashboard.agentSeeNextSuggestion')}
|
||||
</button>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onOpen}
|
||||
className="flex-1 inline-flex items-center justify-center gap-1 text-[9px] font-mono uppercase px-2.5 py-1.5 rounded-lg bg-brand-accent text-white font-bold hover:bg-brand-accent/90"
|
||||
>
|
||||
{t('homeDashboard.agentOpenCreated')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function AgentSlide({
|
||||
current,
|
||||
direction,
|
||||
@@ -165,7 +237,7 @@ function AgentSlide({
|
||||
type="button"
|
||||
disabled={actingId === current.id}
|
||||
onClick={() => onAccept(current.id)}
|
||||
className="flex-1 inline-flex items-center justify-center gap-1 text-[9px] font-mono uppercase px-2.5 py-1.5 rounded-lg bg-ink text-white dark:bg-white dark:text-black font-bold hover:opacity-90 disabled:opacity-40"
|
||||
className="flex-1 inline-flex items-center justify-center gap-1 text-[9px] font-mono uppercase px-2.5 py-1.5 rounded-lg bg-brand-accent text-white font-bold hover:bg-brand-accent/90 disabled:opacity-40"
|
||||
>
|
||||
{actingId === current.id ? <Loader2 size={10} className="animate-spin" /> : null}
|
||||
{t('homeDashboard.createAgent')}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useLanguage } from '@/lib/i18n'
|
||||
import { RevisionHeatmap } from '@/components/flashcards/revision-heatmap'
|
||||
import { UsageMeter } from '@/components/usage-meter'
|
||||
import { DashboardWidgetTitleRow } from '@/components/dashboard-widget-title-row'
|
||||
import { pathNoteTitle } from '@/lib/dashboard/path-title'
|
||||
import type { DashboardWidgetId } from '@/lib/dashboard/layout'
|
||||
|
||||
interface DashboardWidgetShellProps {
|
||||
@@ -41,21 +42,40 @@ export function DashboardWidgetShell({
|
||||
)
|
||||
}
|
||||
|
||||
function inboxDisplayTitle(
|
||||
note: { title: string | null; excerpt?: string },
|
||||
fallback: string,
|
||||
): string {
|
||||
const titled = note.title?.trim()
|
||||
if (titled) return titled
|
||||
const excerpt = note.excerpt?.trim()
|
||||
if (!excerpt) return fallback
|
||||
return excerpt.length > 80 ? `${excerpt.slice(0, 80).trim()}…` : excerpt
|
||||
}
|
||||
|
||||
export function DashboardInboxWidget({
|
||||
count,
|
||||
notes,
|
||||
loading,
|
||||
onOpen,
|
||||
onSelect,
|
||||
formatRelativeTime,
|
||||
}: {
|
||||
count: number
|
||||
notes: Array<{ id: string; title: string | null; notebookId: string | null }>
|
||||
notes: Array<{
|
||||
id: string
|
||||
title: string | null
|
||||
excerpt?: string
|
||||
notebookId: string | null
|
||||
updatedAt?: string
|
||||
}>
|
||||
loading: boolean
|
||||
onOpen: () => void
|
||||
onSelect: (id: string, notebookId: string | null) => void
|
||||
formatRelativeTime?: (date: string) => string
|
||||
}) {
|
||||
const { t } = useLanguage()
|
||||
const reduced = !!useReducedMotion()
|
||||
const untitled = t('homeDashboard.untitled')
|
||||
return (
|
||||
<DashboardWidgetShell
|
||||
widgetId="inbox"
|
||||
@@ -83,21 +103,22 @@ export function DashboardInboxWidget({
|
||||
className="w-full text-start p-2.5 rounded-xl border border-border/20 hover:border-brand-accent/30 hover:bg-brand-accent/[0.03] transition-all"
|
||||
>
|
||||
<p className="text-[11px] text-ink dark:text-dark-ink truncate">
|
||||
{note.title || t('homeDashboard.untitled')}
|
||||
{inboxDisplayTitle(note, untitled)}
|
||||
</p>
|
||||
<p className="text-[9px] text-concrete truncate mt-0.5">
|
||||
{t('homeDashboard.inbox')}
|
||||
{note.updatedAt && formatRelativeTime
|
||||
? ` · ${formatRelativeTime(note.updatedAt)}`
|
||||
: ''}
|
||||
</p>
|
||||
</motion.button>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onOpen}
|
||||
className="w-full flex items-center justify-between gap-2 px-1 pt-1 text-start"
|
||||
className="w-full text-start px-1 pt-1 text-[9px] font-mono uppercase font-bold text-brand-accent hover:underline"
|
||||
>
|
||||
<span className="text-[9px] font-mono uppercase font-bold text-concrete">
|
||||
{t('homeDashboard.inboxSeeAll', { count })}
|
||||
</span>
|
||||
<span className="text-[9px] font-mono uppercase font-bold text-brand-accent">
|
||||
{t('homeDashboard.widgetOpen')} →
|
||||
</span>
|
||||
{t('homeDashboard.inboxOpenList')} →
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -278,12 +299,22 @@ export function DashboardPinnedWidget({
|
||||
notes,
|
||||
loading,
|
||||
onSelect,
|
||||
formatRelativeTime,
|
||||
}: {
|
||||
notes: { id: string; title: string | null; notebookId: string | null }[]
|
||||
notes: {
|
||||
id: string
|
||||
title: string | null
|
||||
excerpt?: string
|
||||
notebookId: string | null
|
||||
updatedAt?: string
|
||||
notebook?: { name: string; color: string | null } | null
|
||||
}[]
|
||||
loading: boolean
|
||||
onSelect: (id: string, notebookId: string | null) => void
|
||||
formatRelativeTime?: (date: string) => string
|
||||
}) {
|
||||
const { t } = useLanguage()
|
||||
const untitled = t('homeDashboard.untitled')
|
||||
return (
|
||||
<DashboardWidgetShell
|
||||
widgetId="pinned"
|
||||
@@ -298,17 +329,37 @@ export function DashboardPinnedWidget({
|
||||
) : notes.length === 0 ? (
|
||||
<p className="text-[10px] text-concrete italic py-2">{t('homeDashboard.widgetPinnedEmpty')}</p>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{notes.map(n => (
|
||||
<div className="space-y-1.5">
|
||||
{notes.map(n => {
|
||||
const displayTitle = pathNoteTitle(n.title, n.excerpt) || untitled
|
||||
const notebookName = n.notebook?.name || t('homeDashboard.pinnedNoNotebook')
|
||||
const notebookColor = n.notebook?.color || '#C4A574'
|
||||
return (
|
||||
<button
|
||||
key={n.id}
|
||||
type="button"
|
||||
onClick={() => onSelect(n.id, n.notebookId)}
|
||||
className="w-full text-[10px] text-ink dark:text-dark-ink truncate text-start p-2 rounded-lg hover:bg-brand-accent/[0.04] transition-colors"
|
||||
className="w-full flex items-center gap-2.5 p-2 rounded-xl border border-border/20 hover:border-brand-accent/30 hover:bg-brand-accent/[0.03] transition-all text-start group"
|
||||
>
|
||||
{n.title || t('homeDashboard.untitled')}
|
||||
<span
|
||||
className="w-1 h-7 rounded-full shrink-0"
|
||||
style={{ backgroundColor: notebookColor }}
|
||||
aria-hidden
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-[11px] text-ink dark:text-dark-ink truncate group-hover:text-brand-accent transition-colors">
|
||||
{displayTitle}
|
||||
</p>
|
||||
<p className="text-[9px] text-concrete truncate mt-0.5">
|
||||
{notebookName}
|
||||
{n.updatedAt && formatRelativeTime
|
||||
? ` · ${formatRelativeTime(n.updatedAt)}`
|
||||
: ''}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</DashboardWidgetShell>
|
||||
@@ -338,8 +389,7 @@ export function DashboardActivityWidget({
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<RevisionHeatmap data={data} />
|
||||
<p className="text-[9px] text-concrete mt-2">{t('homeDashboard.widgetActivityHint')}</p>
|
||||
<RevisionHeatmap data={data} kind="edits" />
|
||||
</>
|
||||
)}
|
||||
</DashboardWidgetShell>
|
||||
@@ -426,15 +476,12 @@ export function DashboardFlashcardsProgressWidget({
|
||||
style={{ width: `${retention}%` }}
|
||||
/>
|
||||
</div>
|
||||
{dueCount > 0 ? (
|
||||
<p className="text-[9px] text-concrete leading-relaxed mb-2">
|
||||
{t('homeDashboard.flashRetentionHint')}
|
||||
</p>
|
||||
<p className="text-[10px] font-medium text-brand-accent group-hover:underline">
|
||||
{t('homeDashboard.flashDueCta', { count: dueCount })} →
|
||||
{dueCount > 0 ? t('homeDashboard.flashDueCta') : t('homeDashboard.flashOpenCta')} →
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-[9px] text-concrete group-hover:text-brand-accent transition-colors">
|
||||
{t('homeDashboard.flashOpenCta')} →
|
||||
</p>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</DashboardWidgetShell>
|
||||
|
||||
@@ -93,12 +93,7 @@ export function DashboardMindOrbit({
|
||||
)}
|
||||
/>
|
||||
|
||||
<motion.div
|
||||
className="relative h-[176px]"
|
||||
initial={reduced ? false : { clipPath: 'circle(0% at 50% 44%)' }}
|
||||
animate={{ clipPath: 'circle(120% at 50% 44%)' }}
|
||||
transition={{ duration: reduced ? 0 : 0.62, ease: EASE }}
|
||||
>
|
||||
<motion.div className="relative h-[200px] overflow-visible">
|
||||
<svg
|
||||
viewBox="0 0 100 100"
|
||||
preserveAspectRatio="none"
|
||||
@@ -133,7 +128,7 @@ export function DashboardMindOrbit({
|
||||
const color = CLUSTER_COLORS[cluster.clusterId % CLUSTER_COLORS.length]
|
||||
const scale = 0.65 + (cluster.noteIds.length / maxCount) * 0.55
|
||||
const size = Math.round(52 * scale)
|
||||
const label = cluster.name || `${t('homeDashboard.theme')} ${cluster.clusterId + 1}`
|
||||
const label = cluster.name?.trim() || t('homeDashboard.theme')
|
||||
const point = points[idx]
|
||||
return (
|
||||
<motion.button
|
||||
@@ -142,11 +137,12 @@ export function DashboardMindOrbit({
|
||||
whileHover={reduced ? undefined : { scale: 1.06 }}
|
||||
whileTap={reduced ? undefined : { scale: 0.97 }}
|
||||
onClick={() => (onOpenCluster ? onOpenCluster(cluster.clusterId) : onOpenInsights())}
|
||||
className="absolute flex flex-col items-center gap-1 group"
|
||||
className="absolute flex flex-col items-center gap-1 group z-[1] hover:z-10"
|
||||
aria-label={label}
|
||||
style={{
|
||||
left: `${point.x}%`,
|
||||
top: `${point.y}%`,
|
||||
width: size + 16,
|
||||
width: Math.max(size + 24, 128),
|
||||
}}
|
||||
initial={reduced ? { x: '-50%', y: '-50%' } : { x: '-50%', y: '-50%', scale: 0.82, opacity: 0.35 }}
|
||||
animate={{ x: '-50%', y: '-50%', scale: 1, opacity: 1 }}
|
||||
@@ -164,7 +160,10 @@ export function DashboardMindOrbit({
|
||||
>
|
||||
{cluster.noteIds.length}
|
||||
</div>
|
||||
<span className="text-[8px] font-medium text-ink/80 dark:text-dark-ink/80 text-center line-clamp-2 leading-tight max-w-[72px] group-hover:text-brand-accent transition-colors">
|
||||
<span className="text-[9px] font-medium text-ink/80 dark:text-dark-ink/80 text-center line-clamp-2 leading-snug max-w-[128px] group-hover:text-brand-accent transition-colors">
|
||||
{label}
|
||||
</span>
|
||||
<span className="pointer-events-none absolute top-full mt-1 max-w-[200px] px-2 py-1 rounded-md bg-ink text-white text-[10px] leading-snug text-center opacity-0 group-hover:opacity-100 transition-opacity shadow-lg z-20">
|
||||
{label}
|
||||
</span>
|
||||
</motion.button>
|
||||
|
||||
@@ -12,16 +12,16 @@ import { DashboardWidgetTitleRow } from '@/components/dashboard-widget-title-row
|
||||
import type { DashboardPath, DashboardPathType } from '@/lib/dashboard/path-types'
|
||||
|
||||
const TYPE_META: Record<DashboardPathType, { Icon: LucideIcon; accent: string }> = {
|
||||
continue: { Icon: PenLine, accent: 'text-ink' },
|
||||
continue: { Icon: PenLine, accent: 'text-brand-accent' },
|
||||
connect: { Icon: Link2, accent: 'text-brand-accent' },
|
||||
'add-link': { Icon: Plus, accent: 'text-emerald-600' },
|
||||
bridge: { Icon: GitBranch, accent: 'text-violet-600' },
|
||||
research: { Icon: Sparkles, accent: 'text-sky-600' },
|
||||
explore: { Icon: Compass, accent: 'text-amber-600' },
|
||||
'add-link': { Icon: Plus, accent: 'text-brand-accent' },
|
||||
bridge: { Icon: GitBranch, accent: 'text-brand-accent' },
|
||||
research: { Icon: Sparkles, accent: 'text-brand-accent' },
|
||||
explore: { Icon: Compass, accent: 'text-brand-accent' },
|
||||
organize: { Icon: Inbox, accent: 'text-brand-accent' },
|
||||
review: { Icon: GraduationCap, accent: 'text-brand-accent' },
|
||||
resurface: { Icon: Lightbulb, accent: 'text-brand-accent' },
|
||||
daily: { Icon: BookOpen, accent: 'text-concrete' },
|
||||
daily: { Icon: BookOpen, accent: 'text-brand-accent' },
|
||||
}
|
||||
|
||||
export interface DashboardNextPathsProps {
|
||||
|
||||
@@ -153,7 +153,7 @@ export function DashboardLinkSuggestions({
|
||||
return (
|
||||
<DashboardWidgetShell
|
||||
widgetId="link-suggestions"
|
||||
icon={<Circle size={12} className="text-emerald-600" />}
|
||||
icon={<Circle size={12} className="text-brand-accent" />}
|
||||
title={t('homeDashboard.widgets.link-suggestions')}
|
||||
>
|
||||
{loading ? (
|
||||
@@ -169,16 +169,16 @@ export function DashboardLinkSuggestions({
|
||||
key={p.id}
|
||||
type="button"
|
||||
onClick={() => onAction(p.id)}
|
||||
className="w-full p-3 rounded-xl border border-border/20 hover:border-emerald-500/30 text-start transition-all"
|
||||
className="w-full p-3 rounded-xl border border-border/20 hover:border-brand-accent/30 text-start transition-all"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2 mb-1">
|
||||
<p className="text-[11px] font-mono font-bold text-ink dark:text-dark-ink truncate">{p.title}</p>
|
||||
{p.score ? (
|
||||
<span className="text-[8px] font-mono text-emerald-600 shrink-0">{p.score}%</span>
|
||||
<span className="text-[8px] font-mono text-brand-accent shrink-0">{p.score}%</span>
|
||||
) : null}
|
||||
</div>
|
||||
<p className="text-[10px] text-concrete line-clamp-2">{p.description}</p>
|
||||
<p className="text-[8px] font-mono uppercase text-emerald-600 mt-2">{t('homeDashboard.pathActions.addLink')} →</p>
|
||||
<p className="text-[8px] font-mono uppercase text-brand-accent mt-2">{t('homeDashboard.pathActions.addLink')} →</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -212,7 +212,7 @@ export function DashboardBridgesWidget({
|
||||
return (
|
||||
<DashboardWidgetShell
|
||||
widgetId="bridges"
|
||||
icon={<Circle size={12} className="text-violet-600" />}
|
||||
icon={<Circle size={12} className="text-brand-accent" />}
|
||||
title={t('homeDashboard.widgets.bridges')}
|
||||
>
|
||||
{loading ? (
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
'use client'
|
||||
|
||||
import { motion } from 'motion/react'
|
||||
import { Clock, ChevronRight, Play, PenLine } from 'lucide-react'
|
||||
import { useLanguage } from '@/lib/i18n'
|
||||
import { DashboardWidgetTitleRow } from '@/components/dashboard-widget-title-row'
|
||||
@@ -24,27 +23,33 @@ export interface DashboardResumeHeroProps {
|
||||
prefersReducedMotion?: boolean
|
||||
}
|
||||
|
||||
function resumeDisplayTitle(note: ResumeNote, fallback: string): string {
|
||||
const titled = note.title?.trim()
|
||||
if (titled) return titled
|
||||
const excerpt = note.excerpt?.trim()
|
||||
if (!excerpt) return fallback
|
||||
return excerpt.length > 80 ? `${excerpt.slice(0, 80).trim()}…` : excerpt
|
||||
}
|
||||
|
||||
export function DashboardResumeHero({
|
||||
notes,
|
||||
loading,
|
||||
onSelect,
|
||||
onCaptureFocus,
|
||||
formatRelativeTime,
|
||||
prefersReducedMotion,
|
||||
}: DashboardResumeHeroProps) {
|
||||
const { t } = useLanguage()
|
||||
const hero = notes[0]
|
||||
const rest = notes.slice(1, 6)
|
||||
const untitled = t('homeDashboard.untitled')
|
||||
const visible = notes.slice(0, 6)
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="rounded-2xl border border-border/30 bg-white dark:bg-zinc-900 p-5 min-h-[220px] animate-pulse">
|
||||
<div className="rounded-2xl border border-border/30 bg-white dark:bg-zinc-900 p-5 min-h-[180px] animate-pulse">
|
||||
<div className="h-4 w-32 bg-stone-100 dark:bg-zinc-800 rounded mb-4" />
|
||||
<div className="h-6 w-3/4 bg-stone-100 dark:bg-zinc-800 rounded mb-3" />
|
||||
<div className="h-16 bg-stone-50 dark:bg-zinc-950 rounded-xl mb-3" />
|
||||
<div className="space-y-2">
|
||||
<div className="h-10 bg-stone-50 dark:bg-zinc-950 rounded-xl" />
|
||||
<div className="h-10 bg-stone-50 dark:bg-zinc-950 rounded-xl" />
|
||||
<div className="h-12 bg-stone-50 dark:bg-zinc-950 rounded-xl" />
|
||||
<div className="h-12 bg-stone-50 dark:bg-zinc-950 rounded-xl" />
|
||||
<div className="h-12 bg-stone-50 dark:bg-zinc-950 rounded-xl" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -61,7 +66,7 @@ export function DashboardResumeHero({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!hero ? (
|
||||
{visible.length === 0 ? (
|
||||
<div className="px-5 pb-5">
|
||||
<div className="rounded-xl border border-dashed border-border/40 bg-stone-50/60 dark:bg-zinc-950/40 p-5 text-center">
|
||||
<Clock size={22} className="mx-auto text-concrete/35 mb-2" strokeWidth={1.25} />
|
||||
@@ -81,46 +86,8 @@ export function DashboardResumeHero({
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<motion.button
|
||||
type="button"
|
||||
whileHover={prefersReducedMotion ? undefined : { y: -1 }}
|
||||
onClick={() => onSelect(hero.id, hero.notebookId)}
|
||||
className="w-full text-start px-5 pb-3 group cursor-pointer"
|
||||
>
|
||||
<div className="p-4 rounded-xl border border-border/25 bg-gradient-to-br from-stone-50/80 to-white dark:from-zinc-950/50 dark:to-zinc-900/80 group-hover:border-brand-accent/35 group-hover:shadow-md transition-all">
|
||||
<div className="flex items-start justify-between gap-3 mb-2">
|
||||
<span
|
||||
className="text-[8px] font-mono font-bold uppercase px-2 py-0.5 rounded text-white shrink-0"
|
||||
style={{ backgroundColor: hero.notebookColor }}
|
||||
>
|
||||
{hero.notebookName}
|
||||
</span>
|
||||
<span className="text-[9px] font-mono text-concrete/70 shrink-0">
|
||||
{formatRelativeTime(hero.updatedAt)}
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="text-base sm:text-lg font-serif font-semibold text-ink dark:text-dark-ink group-hover:text-brand-accent transition-colors leading-snug mb-2 line-clamp-2">
|
||||
{hero.title || t('homeDashboard.untitled')}
|
||||
</h3>
|
||||
{hero.excerpt ? (
|
||||
<p className="text-[11px] text-concrete leading-relaxed line-clamp-3">
|
||||
{hero.excerpt}
|
||||
</p>
|
||||
) : null}
|
||||
<div className="flex items-center gap-1 mt-3 text-[9px] font-mono font-bold uppercase text-brand-accent opacity-80 group-hover:opacity-100 transition-opacity">
|
||||
{t('homeDashboard.resumeOpen')}
|
||||
<ChevronRight size={12} />
|
||||
</div>
|
||||
</div>
|
||||
</motion.button>
|
||||
|
||||
{rest.length > 0 && (
|
||||
<div className="px-5 pb-4 space-y-1.5">
|
||||
<p className="text-[8px] font-mono font-bold uppercase tracking-wider text-concrete/70 mb-1">
|
||||
{t('homeDashboard.resumeAlso')}
|
||||
</p>
|
||||
{rest.map(note => (
|
||||
{visible.map(note => (
|
||||
<button
|
||||
key={note.id}
|
||||
type="button"
|
||||
@@ -134,7 +101,7 @@ export function DashboardResumeHero({
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-[11px] font-semibold text-ink dark:text-dark-ink truncate group-hover:text-brand-accent transition-colors">
|
||||
{note.title || t('homeDashboard.untitled')}
|
||||
{resumeDisplayTitle(note, untitled)}
|
||||
</p>
|
||||
<p className="text-[9px] text-concrete truncate mt-0.5">
|
||||
{note.notebookName}
|
||||
@@ -147,8 +114,6 @@ export function DashboardResumeHero({
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,12 @@ interface EmotionMeta {
|
||||
color: string
|
||||
}
|
||||
|
||||
export interface SentimentRelatedNote {
|
||||
id: string
|
||||
title: string | null
|
||||
notebookId: string | null
|
||||
}
|
||||
|
||||
export interface DashboardSentimentChipProps {
|
||||
available: boolean
|
||||
loading?: boolean
|
||||
@@ -17,6 +23,8 @@ export interface DashboardSentimentChipProps {
|
||||
summary?: string
|
||||
emotions?: Record<string, number>
|
||||
emotionMeta: Record<string, EmotionMeta>
|
||||
relatedNotes?: SentimentRelatedNote[]
|
||||
onSelectNote?: (id: string, notebookId: string | null) => void
|
||||
}
|
||||
|
||||
export function DashboardSentimentChip({
|
||||
@@ -26,6 +34,8 @@ export function DashboardSentimentChip({
|
||||
summary,
|
||||
emotions,
|
||||
emotionMeta,
|
||||
relatedNotes = [],
|
||||
onSelectNote,
|
||||
}: DashboardSentimentChipProps) {
|
||||
const { t } = useLanguage()
|
||||
|
||||
@@ -80,6 +90,24 @@ export function DashboardSentimentChip({
|
||||
</p>
|
||||
)}
|
||||
|
||||
{relatedNotes.length > 0 && onSelectNote && (
|
||||
<div className="space-y-1 pt-1">
|
||||
<p className="text-[8px] font-mono font-bold uppercase tracking-wider text-concrete/70">
|
||||
{t('homeDashboard.sentimentFromNotes')}
|
||||
</p>
|
||||
{relatedNotes.map(note => (
|
||||
<button
|
||||
key={note.id}
|
||||
type="button"
|
||||
onClick={() => onSelectNote(note.id, note.notebookId)}
|
||||
className="w-full text-start text-[11px] text-ink dark:text-dark-ink truncate px-2 py-1.5 rounded-lg hover:bg-brand-accent/[0.04] hover:text-brand-accent transition-colors"
|
||||
>
|
||||
{note.title?.trim() || t('homeDashboard.untitled')}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{emotions && (
|
||||
<div className="space-y-2 pt-1">
|
||||
{Object.entries(emotions)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useState, useEffect, useCallback, useMemo, type ReactNode } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useReducedMotion } from 'motion/react'
|
||||
import { Inbox, Send, Bell, Mail, Loader2 } from 'lucide-react'
|
||||
import { Send, Bell, Loader2, PenLine } from 'lucide-react'
|
||||
import { useLanguage } from '@/lib/i18n'
|
||||
import { useAiConsent } from '@/components/legal/ai-consent-provider'
|
||||
import { redirectToAiConsentSettings } from '@/lib/consent/ai-consent-redirect'
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
} from '@/components/dashboard-path-widgets'
|
||||
import type { DashboardPath } from '@/lib/dashboard/path-types'
|
||||
import { buildFastPathsFromBriefing } from '@/lib/dashboard/paths-fast'
|
||||
import { pathNoteTitle, pickFocusNote } from '@/lib/dashboard/path-title'
|
||||
import { emitAiUsageChanged } from '@/lib/ai-usage-sync'
|
||||
import {
|
||||
DashboardInboxWidget,
|
||||
@@ -50,8 +51,10 @@ import type { LucideIcon } from 'lucide-react'
|
||||
interface BriefingPinnedNote {
|
||||
id: string
|
||||
title: string | null
|
||||
excerpt?: string
|
||||
notebookId: string | null
|
||||
updatedAt: string
|
||||
notebook?: { id: string; name: string; color: string | null; icon: string | null } | null
|
||||
}
|
||||
|
||||
interface ActivityDay {
|
||||
@@ -137,6 +140,7 @@ interface SentimentData {
|
||||
emotions?: Record<string, number>
|
||||
summary?: string
|
||||
topTopic?: string
|
||||
relatedNotes?: Array<{ id: string; title: string | null; notebookId: string | null }>
|
||||
}
|
||||
|
||||
interface MindMapData {
|
||||
@@ -217,7 +221,13 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
|
||||
const [data, setData] = useState<{
|
||||
recentNotes: BriefingNote[]
|
||||
inboxCount: number
|
||||
inboxPreview?: Array<{ id: string; title: string | null; notebookId: string | null }>
|
||||
inboxPreview?: Array<{
|
||||
id: string
|
||||
title: string | null
|
||||
excerpt?: string
|
||||
notebookId: string | null
|
||||
updatedAt?: string
|
||||
}>
|
||||
dueFlashcards: number
|
||||
upcomingReminders: BriefingReminder[]
|
||||
insights: BriefingInsight[]
|
||||
@@ -245,6 +255,7 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
|
||||
const [capturing, setCapturing] = useState(false)
|
||||
const [inboxPulse, setInboxPulse] = useState(0)
|
||||
const [actingSuggestionId, setActingSuggestionId] = useState<string | null>(null)
|
||||
const [createdAgent, setCreatedAgent] = useState<{ id: string | null; topic: string } | null>(null)
|
||||
const [echoRefreshing, setEchoRefreshing] = useState(false)
|
||||
const [dismissingInsightId, setDismissingInsightId] = useState<string | null>(null)
|
||||
const [actingBridgeSuggestionKey, setActingBridgeSuggestionKey] = useState<string | null>(null)
|
||||
@@ -267,7 +278,7 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
|
||||
const loadPaths = useCallback(async (briefing: NonNullable<typeof data>) => {
|
||||
setPathsEnriching(true)
|
||||
try {
|
||||
const focus = briefing.recentNotes[0]
|
||||
const focus = pickFocusNote(briefing.recentNotes)
|
||||
const res = await fetch('/api/briefing/paths', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -418,12 +429,22 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
|
||||
const gmail = data?.gmail
|
||||
const pinnedNotes = data?.pinnedNotes ?? []
|
||||
const writingActivity = data?.writingActivity ?? []
|
||||
const pathsList = paths
|
||||
const recentNotes = data?.recentNotes ?? []
|
||||
const pathsList = useMemo(
|
||||
() => paths.filter(p => p.type !== 'organize' && p.type !== 'review'),
|
||||
[paths],
|
||||
)
|
||||
const pathBridgeKeys = useMemo(
|
||||
() => pathsList
|
||||
.filter(p => p.type === 'bridge' && p.clusterAId != null && p.clusterBId != null)
|
||||
.map(p => `${p.clusterAId}-${p.clusterBId}`),
|
||||
[pathsList],
|
||||
)
|
||||
const focusNote = useMemo(() => pickFocusNote(recentNotes), [recentNotes])
|
||||
const openLoopsList = openLoops
|
||||
const briefingLoading = data === null
|
||||
const pathsLoading = data === null
|
||||
const pathsDetailLoading = pathsEnriching
|
||||
const recentNotes = data?.recentNotes ?? []
|
||||
const topBridgeNotes = useMemo(() => (mindMap?.bridgeNotes ?? []).slice(0, 3), [mindMap?.bridgeNotes])
|
||||
const dateLocale = localeForLanguage(language)
|
||||
|
||||
@@ -434,25 +455,19 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
|
||||
|
||||
const themeCount = mindMap?.clusters.length ?? 0
|
||||
|
||||
const resumeNotes = useMemo(() => recentNotes.map(n => ({
|
||||
const resumeNotes = useMemo(() => recentNotes.flatMap(n => {
|
||||
const displayTitle = pathNoteTitle(n.title, n.content)
|
||||
if (!displayTitle) return []
|
||||
return [{
|
||||
id: n.id,
|
||||
title: n.title,
|
||||
title: displayTitle,
|
||||
excerpt: stripHtml(n.content).slice(0, 180),
|
||||
notebookName: n.notebook?.name || t('homeDashboard.inbox'),
|
||||
notebookColor: n.notebook?.color || '#8B5CF6',
|
||||
updatedAt: n.updatedAt,
|
||||
notebookId: n.notebookId,
|
||||
})), [recentNotes, t])
|
||||
|
||||
const briefingSubtitle = useMemo(() => {
|
||||
if (briefingLoading) return ''
|
||||
const parts: string[] = []
|
||||
if (inboxCount > 0) parts.push(t('homeDashboard.pulseInbox', { count: inboxCount }))
|
||||
if (dueFlashcards > 0) parts.push(t('homeDashboard.pulseReview', { count: dueFlashcards }))
|
||||
if (discoveryCount > 0) parts.push(t('homeDashboard.pulseDiscoveries', { count: discoveryCount }))
|
||||
if (parts.length === 0) return t('homeDashboard.pulseClear')
|
||||
return parts.join(' · ')
|
||||
}, [briefingLoading, inboxCount, dueFlashcards, discoveryCount, t])
|
||||
}]
|
||||
}), [recentNotes, t])
|
||||
|
||||
const handleCapture = useCallback(async () => {
|
||||
const text = captureText.trim()
|
||||
@@ -609,19 +624,20 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
|
||||
|
||||
const handleAcceptSuggestion = useCallback(async (id: string) => {
|
||||
setActingSuggestionId(id)
|
||||
const topic = data?.agentSuggestions?.find(s => s.id === id)?.topic ?? ''
|
||||
try {
|
||||
const res = await fetch(`/api/agents/suggestions/${id}/accept`, { method: 'POST' })
|
||||
const json = await res.json()
|
||||
if (!res.ok) throw new Error(json.error)
|
||||
setData(prev => prev ? { ...prev, agentSuggestions: prev.agentSuggestions?.filter(s => s.id !== id) ?? [] } : prev)
|
||||
setCreatedAgent({ id: json.agentId ?? null, topic })
|
||||
toast.success(t('homeDashboard.agentCreated'))
|
||||
if (json.agentId) router.push(`/agents?id=${json.agentId}`)
|
||||
} catch {
|
||||
toast.error(t('homeDashboard.agentFailed'))
|
||||
} finally {
|
||||
setActingSuggestionId(null)
|
||||
}
|
||||
}, [t, router])
|
||||
}, [t, data?.agentSuggestions])
|
||||
|
||||
const handleDismissSuggestion = useCallback(async (id: string) => {
|
||||
setActingSuggestionId(id)
|
||||
@@ -752,12 +768,17 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
|
||||
case 'capture':
|
||||
return wrap(
|
||||
<div className="relative rounded-xl border border-border/30 bg-white/80 dark:bg-zinc-900/80 backdrop-blur-sm shadow-sm h-full">
|
||||
<div className="flex items-center justify-between gap-2 px-3.5 pt-2.5">
|
||||
<div className="flex items-center justify-between gap-2 px-3 pt-2">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<Inbox size={11} className="text-brand-accent shrink-0" />
|
||||
<span className="text-[8px] font-mono font-bold uppercase tracking-widest text-concrete">
|
||||
<PenLine size={11} className="text-brand-accent shrink-0" />
|
||||
<div className="min-w-0">
|
||||
<span className="text-[8px] font-mono font-bold uppercase tracking-widest text-concrete block">
|
||||
{t('homeDashboard.quickCapture')}
|
||||
</span>
|
||||
<span className="text-[9px] text-concrete leading-tight">
|
||||
{t('homeDashboard.captureGoesToFile')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<DashboardWidgetHelp widgetId="capture" />
|
||||
</div>
|
||||
@@ -767,14 +788,15 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
|
||||
onKeyDown={handleCaptureKeyDown}
|
||||
placeholder={t('homeDashboard.quickCapturePlaceholder')}
|
||||
rows={2}
|
||||
className="w-full text-sm px-3.5 pb-3 pt-1.5 pe-12 bg-transparent outline-none text-ink dark:text-dark-ink resize-none leading-relaxed placeholder:text-concrete/45"
|
||||
className="w-full text-sm px-3 pb-2 pt-1 pe-12 bg-transparent outline-none text-ink dark:text-dark-ink resize-none leading-snug h-[2.75rem] placeholder:text-concrete/45"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCapture}
|
||||
disabled={!captureText.trim() || capturing}
|
||||
className="absolute bottom-2.5 end-2.5 p-2 bg-ink text-white dark:bg-white dark:text-black rounded-lg disabled:opacity-25 hover:scale-105 active:scale-95 transition-all shadow-sm"
|
||||
className="absolute bottom-1.5 end-2 p-1.5 bg-brand-accent text-white rounded-lg disabled:opacity-25 hover:bg-brand-accent/90 hover:scale-105 active:scale-95 transition-all shadow-sm"
|
||||
aria-busy={capturing}
|
||||
aria-label={t('homeDashboard.captureSend')}
|
||||
>
|
||||
{capturing
|
||||
? <Loader2 size={12} className="animate-spin" />
|
||||
@@ -788,7 +810,7 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
|
||||
paths={pathsList}
|
||||
loading={pathsLoading}
|
||||
enriching={pathsEnriching}
|
||||
focusNoteTitle={recentNotes[0]?.title}
|
||||
focusNoteTitle={focusNote ? pathNoteTitle(focusNote.title, focusNote.content) : null}
|
||||
onAction={handlePathAction}
|
||||
prefersReducedMotion={!!prefersReducedMotion}
|
||||
/>,
|
||||
@@ -887,6 +909,7 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
|
||||
dismissingInsightId={dismissingInsightId}
|
||||
actingBridgeSuggestionKey={actingBridgeSuggestionKey}
|
||||
prefersReducedMotion={!!prefersReducedMotion}
|
||||
excludeBridgeSuggestionKeys={pathBridgeKeys}
|
||||
/>,
|
||||
)
|
||||
case 'reminders':
|
||||
@@ -903,7 +926,18 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
|
||||
<div className="h-10 rounded-lg bg-stone-50 dark:bg-zinc-950/40 animate-pulse" />
|
||||
</div>
|
||||
) : reminders.length === 0 ? (
|
||||
<p className="text-[11px] text-concrete italic py-1">{t('homeDashboard.allCaughtUp')}</p>
|
||||
<div className="rounded-xl border border-dashed border-border/35 bg-stone-50/50 dark:bg-zinc-950/30 p-3">
|
||||
<p className="text-[11px] text-concrete leading-relaxed mb-2">
|
||||
{t('homeDashboard.remindersEmpty')}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => router.push('/home?reminders=1&forceList=1')}
|
||||
className="text-[9px] font-mono font-bold uppercase text-brand-accent hover:underline"
|
||||
>
|
||||
{t('homeDashboard.remindersOpenAll')} →
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-1.5">
|
||||
{reminders.slice(0, 4).map(r => (
|
||||
@@ -921,32 +955,14 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{!briefingLoading && (
|
||||
gmail?.connected ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => router.push('/settings/integrations')}
|
||||
className="w-full flex items-center justify-between gap-2 p-2.5 rounded-xl border border-border/20 hover:border-brand-accent/25 transition-all text-start mt-2"
|
||||
onClick={() => router.push('/home?reminders=1&forceList=1')}
|
||||
className="w-full text-start px-1 pt-1 text-[9px] font-mono uppercase font-bold text-brand-accent hover:underline"
|
||||
>
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<Mail size={11} className="text-concrete" />
|
||||
<span className="text-[10px] text-ink dark:text-dark-ink truncate">{t('homeDashboard.gmailCaptures')}</span>
|
||||
{t('homeDashboard.remindersOpenAll')} →
|
||||
</button>
|
||||
</div>
|
||||
<span className="text-[8px] font-mono font-bold text-brand-accent bg-brand-accent/10 px-1.5 py-0.5 rounded">
|
||||
{t('homeDashboard.gmailRecent', { count: gmail.recentCaptures })}
|
||||
</span>
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => router.push('/settings/integrations')}
|
||||
className="w-full text-[9px] font-mono uppercase tracking-wider text-concrete hover:text-brand-accent transition-colors text-start py-2 mt-2"
|
||||
>
|
||||
{t('homeDashboard.gmailConnect')} →
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
</div>,
|
||||
)
|
||||
@@ -971,6 +987,9 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
|
||||
formatFrequency={formatAgentFrequency}
|
||||
onAccept={handleAcceptSuggestion}
|
||||
onDismiss={handleDismissSuggestion}
|
||||
createdAgent={createdAgent}
|
||||
onOpenCreated={() => router.push(createdAgent?.id ? `/agents?id=${createdAgent.id}` : '/agents')}
|
||||
onClearCreated={() => setCreatedAgent(null)}
|
||||
prefersReducedMotion={!!prefersReducedMotion}
|
||||
/>,
|
||||
)
|
||||
@@ -983,6 +1002,8 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
|
||||
summary={sentiment?.summary}
|
||||
emotions={sentiment?.emotions}
|
||||
emotionMeta={EMOTION_META}
|
||||
relatedNotes={sentiment?.relatedNotes}
|
||||
onSelectNote={onNoteSelect}
|
||||
/>,
|
||||
)
|
||||
case 'inbox':
|
||||
@@ -993,6 +1014,7 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
|
||||
loading={briefingLoading}
|
||||
onOpen={() => router.push('/home?forceList=1')}
|
||||
onSelect={onNoteSelect}
|
||||
formatRelativeTime={relTime}
|
||||
/>,
|
||||
)
|
||||
case 'revision':
|
||||
@@ -1043,6 +1065,7 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
|
||||
notes={pinnedNotes}
|
||||
loading={briefingLoading}
|
||||
onSelect={onNoteSelect}
|
||||
formatRelativeTime={relTime}
|
||||
/>,
|
||||
)
|
||||
case 'usage':
|
||||
@@ -1058,11 +1081,11 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
|
||||
aiStatus, insights, topBridgeNotes, bridgeSuggestions, agentActions, handleRefreshEcho,
|
||||
handleEnableAi, echoRefreshing, handleDismissInsight, handleDismissBridgeSuggestion,
|
||||
handleCreateBridgeSuggestion, handleOpenFromInsight, dismissingInsightId,
|
||||
actingBridgeSuggestionKey, reminders, gmail, dateLocale, router, mindMap,
|
||||
agentSuggestions, actingSuggestionId, formatAgentFrequency, handleAcceptSuggestion,
|
||||
actingBridgeSuggestionKey, pathBridgeKeys, reminders, gmail, dateLocale, router, mindMap,
|
||||
agentSuggestions, actingSuggestionId, createdAgent, formatAgentFrequency, handleAcceptSuggestion,
|
||||
handleDismissSuggestion, sentiment, pinnedNotes, writingActivity, themeCount,
|
||||
pathsList, openLoopsList, handlePathAction, dailyReviewItems, linkSuggestionPaths,
|
||||
handleOpenDailyNote, flashcardStats,
|
||||
handleOpenDailyNote, flashcardStats, focusNote,
|
||||
])
|
||||
|
||||
return (
|
||||
@@ -1078,7 +1101,7 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { void reloadBriefingAndPaths() }}
|
||||
className="shrink-0 text-[10px] font-mono font-bold uppercase tracking-wider px-3 py-2 rounded-lg bg-ink text-white dark:bg-white dark:text-black"
|
||||
className="shrink-0 text-[10px] font-mono font-bold uppercase tracking-wider px-3 py-2 rounded-lg bg-brand-accent text-white hover:bg-brand-accent/90"
|
||||
>
|
||||
{t('homeDashboard.briefingRetry')}
|
||||
</button>
|
||||
@@ -1086,8 +1109,7 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
|
||||
)}
|
||||
{/* ── En-tête : orientation en 2 secondes ── */}
|
||||
<header className="mb-5">
|
||||
<div className="flex flex-col sm:flex-row sm:items-end sm:justify-between gap-2 pb-4 border-b border-border/20">
|
||||
<div>
|
||||
<div className="pb-4 border-b border-border/20">
|
||||
<h1 className="font-serif text-2xl sm:text-3xl font-medium text-ink dark:text-dark-ink tracking-tight">
|
||||
{t('homeDashboard.title')}
|
||||
</h1>
|
||||
@@ -1095,12 +1117,6 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
|
||||
{new Date().toLocaleDateString(dateLocale, { weekday: 'long', day: 'numeric', month: 'long' })}
|
||||
</p>
|
||||
</div>
|
||||
{!briefingLoading && briefingSubtitle && (
|
||||
<p className="text-[11px] text-concrete leading-relaxed max-w-md sm:text-end">
|
||||
{briefingSubtitle}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Pulse : file d'attente cognitive en un coup d'œil */}
|
||||
<div className="mt-4">
|
||||
@@ -1121,15 +1137,12 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="pb-24">
|
||||
<div className="pb-8">
|
||||
<DashboardWidgetGrid
|
||||
renderWidget={renderWidget}
|
||||
isWidgetEmpty={(id) => {
|
||||
if (briefingLoading) return false
|
||||
if (id === 'sentiment') return !sentimentLoading && (!sentiment?.available || !sentiment?.dominantEmotion)
|
||||
if (id === 'reminders') return reminders.length === 0
|
||||
if (id === 'revision') return dueFlashcards === 0
|
||||
if (id === 'inbox') return inboxCount === 0
|
||||
return false
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -240,7 +240,19 @@ export function DashboardWidgetGrid({ renderWidget, isWidgetEmpty }: DashboardWi
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-4">
|
||||
<div className={editMode ? 'space-y-4 pb-24' : 'space-y-4'}>
|
||||
{!editMode && loaded && hasVisibleWidgets && (
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setEditMode(true)}
|
||||
className="inline-flex items-center gap-1.5 text-[10px] font-mono uppercase font-bold px-3 py-1.5 rounded-xl text-brand-accent hover:bg-brand-accent/10 transition-colors"
|
||||
>
|
||||
<LayoutGrid size={12} />
|
||||
{t('homeDashboard.widgetCustomize')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{loaded ? (
|
||||
hasVisibleWidgets ? (
|
||||
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
||||
@@ -311,9 +323,8 @@ export function DashboardWidgetGrid({ renderWidget, isWidgetEmpty }: DashboardWi
|
||||
)}
|
||||
</div>
|
||||
|
||||
{editMode && (
|
||||
<div className="fixed bottom-6 left-1/2 -translate-x-1/2 z-40 flex items-center gap-2 px-3 py-2 rounded-2xl bg-ink/92 dark:bg-zinc-900/95 text-white shadow-xl border border-white/10 backdrop-blur-md">
|
||||
{editMode ? (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCatalogOpen(v => !v)}
|
||||
@@ -341,18 +352,8 @@ export function DashboardWidgetGrid({ renderWidget, isWidgetEmpty }: DashboardWi
|
||||
<Check size={12} />
|
||||
{t('homeDashboard.widgetDone')}
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setEditMode(true)}
|
||||
className="inline-flex items-center gap-1.5 text-[10px] font-mono uppercase font-bold px-4 py-2 rounded-xl bg-white/10 hover:bg-white/15 transition-colors"
|
||||
>
|
||||
<LayoutGrid size={12} />
|
||||
{t('homeDashboard.widgetCustomize')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{editMode && catalogOpen && (
|
||||
<div className="fixed bottom-20 left-1/2 -translate-x-1/2 z-40 w-[min(520px,calc(100vw-2rem))] max-h-[min(60vh,480px)] overflow-y-auto custom-scrollbar p-4 rounded-2xl bg-white dark:bg-zinc-900 border border-border/40 shadow-2xl">
|
||||
|
||||
@@ -10,6 +10,7 @@ interface DashboardWidgetTitleRowProps {
|
||||
title: string
|
||||
actions?: ReactNode
|
||||
className?: string
|
||||
wrapTitle?: boolean
|
||||
}
|
||||
|
||||
/** Titre widget : actions d’abord, aide « ? » en dernier — ne masque jamais la navigation. */
|
||||
@@ -19,12 +20,15 @@ export function DashboardWidgetTitleRow({
|
||||
title,
|
||||
actions,
|
||||
className = 'mb-3',
|
||||
wrapTitle,
|
||||
}: DashboardWidgetTitleRowProps) {
|
||||
return (
|
||||
<div className={`flex items-center justify-between gap-2 ${className}`}>
|
||||
<div className="flex items-center gap-2 min-w-0 flex-1">
|
||||
{icon}
|
||||
<h2 className="text-[10px] font-mono font-bold uppercase tracking-widest text-ink dark:text-dark-ink truncate">
|
||||
<div className={`flex items-start justify-between gap-2 ${className}`}>
|
||||
<div className="flex items-start gap-2 min-w-0 flex-1">
|
||||
{icon ? <span className="shrink-0 mt-0.5">{icon}</span> : null}
|
||||
<h2 className={`text-[13px] font-semibold uppercase tracking-wider text-ink dark:text-dark-ink ${
|
||||
wrapTitle ? 'leading-tight whitespace-normal' : 'truncate'
|
||||
}`}>
|
||||
{title}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
@@ -12,6 +12,8 @@ interface HeatmapDay {
|
||||
interface RevisionHeatmapProps {
|
||||
data: HeatmapDay[]
|
||||
className?: string
|
||||
/** Révisions de flashcards, ou notes modifiées sur le tableau de bord. */
|
||||
kind?: 'reviews' | 'edits'
|
||||
}
|
||||
|
||||
function intensityClass(count: number, max: number): string {
|
||||
@@ -28,17 +30,23 @@ function resolveDateLocale(langCode: string): string {
|
||||
return langCode
|
||||
}
|
||||
|
||||
export function RevisionHeatmap({ data, className }: RevisionHeatmapProps) {
|
||||
export function RevisionHeatmap({ data, className, kind = 'reviews' }: RevisionHeatmapProps) {
|
||||
const { t, language } = useLanguage()
|
||||
const [hovered, setHovered] = useState<{ label: string; count: number; date: string } | null>(null)
|
||||
const [selected, setSelected] = useState<{ label: string; count: number; date: string } | null>(null)
|
||||
|
||||
const dateLocale = resolveDateLocale(language ?? 'en')
|
||||
const prefix = kind === 'edits' ? 'homeDashboard.activityHeatmap' : 'flashcards.heatmap'
|
||||
|
||||
const dayLabel = (count: number) => {
|
||||
if (count <= 0) return t(`${prefix}DayNone`)
|
||||
if (count === 1) return t(`${prefix}DayOne`)
|
||||
return t(`${prefix}Day`, { count })
|
||||
}
|
||||
|
||||
const { cells, maxCount, totalReviews, monthLabels } = useMemo(() => {
|
||||
const map = new Map(data.map((d) => [d.date, d.count]))
|
||||
const now = new Date()
|
||||
// todayUTC est le début de la journée courante à minuit UTC
|
||||
const todayUTC = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()))
|
||||
|
||||
const cells: { date: string; count: number; label: string }[] = []
|
||||
@@ -78,22 +86,22 @@ export function RevisionHeatmap({ data, className }: RevisionHeatmapProps) {
|
||||
|
||||
return (
|
||||
<div className={cn('space-y-2', className)}>
|
||||
{/* En-tête */}
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-[10px] font-bold uppercase tracking-widest text-concrete">
|
||||
{t('flashcards.heatmapTitle')}
|
||||
<p className="text-[13px] font-semibold uppercase tracking-wider text-concrete">
|
||||
{t(`${prefix}Title`)}
|
||||
</p>
|
||||
<span className="text-[10px] text-concrete/60">
|
||||
{totalReviews > 0 ? `${totalReviews} révisions · 90 jours` : t('flashcards.heatmapLast90')}
|
||||
<span className="text-[13px] text-concrete/70">
|
||||
{totalReviews > 0
|
||||
? t(`${prefix}Total`, { count: totalReviews })
|
||||
: t(kind === 'edits' ? 'homeDashboard.activityHeatmapLast90' : 'flashcards.heatmapLast90')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Labels de mois au-dessus de la grille */}
|
||||
<div className="relative h-4">
|
||||
<div className="relative h-6">
|
||||
{monthLabels.map((m) => (
|
||||
<span
|
||||
key={m.label + m.index}
|
||||
className="absolute text-[9px] text-concrete/60 font-medium translate-y-0.5"
|
||||
className="absolute text-[13px] text-concrete/70 font-medium leading-tight"
|
||||
style={{ left: pct(m.index) }}
|
||||
>
|
||||
{m.label}
|
||||
@@ -101,14 +109,11 @@ export function RevisionHeatmap({ data, className }: RevisionHeatmapProps) {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Grille pleine largeur */}
|
||||
<div className="grid grid-cols-[repeat(15,minmax(0,1fr))] gap-1 sm:grid-cols-[repeat(18,minmax(0,1fr))]">
|
||||
{cells.map((cell) => {
|
||||
const isHovered = hovered?.date === cell.date
|
||||
const isSelected = selected?.date === cell.date
|
||||
const reviewText = cell.count > 0
|
||||
? `${cell.count} révision${cell.count > 1 ? 's' : ''}`
|
||||
: 'Aucune révision'
|
||||
const reviewText = dayLabel(cell.count)
|
||||
|
||||
return (
|
||||
<button
|
||||
@@ -134,47 +139,29 @@ export function RevisionHeatmap({ data, className }: RevisionHeatmapProps) {
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Info au survol / clic */}
|
||||
<div className="h-6 flex items-center justify-between text-[11px] border-b border-border/20 pb-1">
|
||||
<div className="min-h-7 flex items-center text-[13px] border-b border-border/20 pb-1">
|
||||
{activeInfo ? (
|
||||
<p className="flex items-center gap-1.5 animate-fadeIn">
|
||||
<span className="font-semibold text-foreground">
|
||||
{activeInfo.count > 0
|
||||
? `${activeInfo.count} révision${activeInfo.count > 1 ? 's' : ''}`
|
||||
: 'Aucune révision'}
|
||||
{dayLabel(activeInfo.count)}
|
||||
</span>
|
||||
<span className="text-concrete">· {activeInfo.label}</span>
|
||||
{selected?.date === activeInfo.date && !hovered && (
|
||||
<span className="text-[9px] bg-brand-accent/10 text-brand-accent px-1.5 py-0.2 rounded-full font-medium">
|
||||
sélectionné
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-[10px] text-concrete/40 italic">
|
||||
Survolez ou cliquez sur un carré pour voir le détail
|
||||
<p className="text-[13px] text-concrete/50 italic">
|
||||
{t(`${prefix}Hint`)}
|
||||
</p>
|
||||
)}
|
||||
{selected && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSelected(null)}
|
||||
className="text-[10px] text-brand-accent hover:text-brand-accent/80 hover:underline transition-colors"
|
||||
>
|
||||
Effacer la sélection
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Légende */}
|
||||
<div className="flex items-center gap-2 pt-0.5">
|
||||
<span className="text-[9px] text-concrete/50">Moins</span>
|
||||
<span className="text-[13px] text-concrete/60">{t('flashcards.heatmapLess')}</span>
|
||||
<div className="flex gap-0.5">
|
||||
{['bg-black/[0.06] dark:bg-white/[0.08]', 'bg-brand-accent/20', 'bg-brand-accent/40', 'bg-brand-accent/70', 'bg-brand-accent'].map((cls, i) => (
|
||||
<div key={i} className={cn('w-3 h-3 rounded-[3px]', cls)} />
|
||||
))}
|
||||
</div>
|
||||
<span className="text-[9px] text-concrete/50">Plus</span>
|
||||
<span className="text-[13px] text-concrete/60">{t('flashcards.heatmapMore')}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -35,6 +35,8 @@ import { NotebookOrganizerDialog } from '@/components/wizard/notebook-organizer-
|
||||
import { toast } from 'sonner'
|
||||
import { AnimatePresence, motion } from 'motion/react'
|
||||
import { isDashboardHomeRoute } from '@/lib/dashboard/home-route'
|
||||
import { ConfirmDeleteNoteDialog } from '@/components/confirm-delete-note-dialog'
|
||||
import { showNoteTrashedToast } from '@/lib/notes/trash-toast'
|
||||
|
||||
|
||||
type SortOrder = 'newest' | 'oldest' | 'alpha' | 'manual'
|
||||
@@ -160,6 +162,7 @@ export function HomeClient({
|
||||
const aiMenuRef = useRef<HTMLDivElement>(null)
|
||||
const [showStudyPlanner, setShowStudyPlanner] = useState(false)
|
||||
const [showOrganizer, setShowOrganizer] = useState(false)
|
||||
const [notePendingDelete, setNotePendingDelete] = useState<Note | null>(null)
|
||||
|
||||
const handleExportCSV = useCallback(() => {
|
||||
if (!searchParams.get('notebook')) return
|
||||
@@ -535,21 +538,25 @@ export function HomeClient({
|
||||
[patchNoteInList, t]
|
||||
)
|
||||
|
||||
const handleDeleteNoteFromList = useCallback(
|
||||
async (note: Note) => {
|
||||
const handleDeleteNoteFromList = useCallback((note: Note) => {
|
||||
setNotePendingDelete(note)
|
||||
}, [])
|
||||
|
||||
const confirmDeleteNoteFromList = useCallback(async () => {
|
||||
const note = notePendingDelete
|
||||
if (!note) return
|
||||
setNotePendingDelete(null)
|
||||
removeNoteFromList(note.id)
|
||||
emitNoteChange({ type: 'deleted', noteId: note.id, notebookId: note.notebookId })
|
||||
try {
|
||||
await deleteNote(note.id, { skipRevalidation: true })
|
||||
toast.success(t('notes.deleted') || 'Note supprimée')
|
||||
showNoteTrashedToast(note, t, () => setNotes((prev) => [note, ...prev]))
|
||||
} catch {
|
||||
setNotes((prev) => [note, ...prev])
|
||||
emitNoteChange({ type: 'created', note })
|
||||
toast.error(t('general.error'))
|
||||
}
|
||||
},
|
||||
[removeNoteFromList, t]
|
||||
)
|
||||
}, [notePendingDelete, removeNoteFromList, t])
|
||||
|
||||
const handleArchiveNoteFromList = useCallback(
|
||||
async (note: Note) => {
|
||||
@@ -1491,6 +1498,14 @@ export function HomeClient({
|
||||
/>
|
||||
)}
|
||||
|
||||
<ConfirmDeleteNoteDialog
|
||||
open={notePendingDelete != null}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) setNotePendingDelete(null)
|
||||
}}
|
||||
onConfirm={confirmDeleteNoteFromList}
|
||||
/>
|
||||
|
||||
{showNotebookSlides && currentNotebook && (
|
||||
<NotebookSlidesDialog
|
||||
notebookId={currentNotebook.id}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import { useLanguage } from '@/lib/i18n'
|
||||
import { DashboardWidgetTitleRow } from '@/components/dashboard-widget-title-row'
|
||||
import { MEMORY_ECHO_LEGACY_EN_FALLBACKS } from '@/lib/ai/memory-echo-i18n'
|
||||
import { pathNoteTitle } from '@/lib/dashboard/path-title'
|
||||
|
||||
// ─── Types ─────────────────────────────────────────────
|
||||
|
||||
@@ -104,14 +105,14 @@ function ConnectionDiagram({
|
||||
className="flex-1 min-w-0 p-2.5 rounded-xl border border-border/30 bg-white/80 dark:bg-zinc-900/60 text-start"
|
||||
style={{ borderColor: `${color}30` }}
|
||||
>
|
||||
<p className="text-[10px] font-semibold text-ink dark:text-dark-ink truncate leading-tight">
|
||||
<p className="text-sm font-semibold text-ink dark:text-dark-ink line-clamp-2 leading-tight" title={note1Title}>
|
||||
{note1Title}
|
||||
</p>
|
||||
</div>
|
||||
<div className="shrink-0 flex flex-col items-center gap-0.5 px-1">
|
||||
<div className="w-8 h-px" style={{ background: `linear-gradient(90deg, transparent, ${color}, transparent)` }} />
|
||||
<span
|
||||
className="text-[9px] font-mono font-bold px-2 py-0.5 rounded-full"
|
||||
className="text-[13px] font-medium px-2 py-0.5 rounded-full"
|
||||
style={{ color, backgroundColor: `${color}14`, border: `1px solid ${color}25` }}
|
||||
>
|
||||
{Math.round(score * 100)}%
|
||||
@@ -122,7 +123,7 @@ function ConnectionDiagram({
|
||||
className="flex-1 min-w-0 p-2.5 rounded-xl border border-border/30 bg-white/80 dark:bg-zinc-900/60 text-start"
|
||||
style={{ borderColor: `${color}30` }}
|
||||
>
|
||||
<p className="text-[10px] font-semibold text-ink dark:text-dark-ink truncate leading-tight">
|
||||
<p className="text-sm font-semibold text-ink dark:text-dark-ink line-clamp-2 leading-tight" title={note2Title}>
|
||||
{note2Title}
|
||||
</p>
|
||||
</div>
|
||||
@@ -153,6 +154,7 @@ export interface IntelligenceHubProps {
|
||||
dismissingInsightId: string | null
|
||||
actingBridgeSuggestionKey: string | null
|
||||
prefersReducedMotion: boolean
|
||||
excludeBridgeSuggestionKeys?: string[]
|
||||
}
|
||||
|
||||
// ─── Component ─────────────────────────────────────────
|
||||
@@ -178,6 +180,7 @@ export function IntelligenceHub({
|
||||
dismissingInsightId,
|
||||
actingBridgeSuggestionKey,
|
||||
prefersReducedMotion,
|
||||
excludeBridgeSuggestionKeys,
|
||||
}: IntelligenceHubProps) {
|
||||
const router = useRouter()
|
||||
const { t } = useLanguage()
|
||||
@@ -226,10 +229,13 @@ export function IntelligenceHub({
|
||||
})
|
||||
}
|
||||
|
||||
const hiddenSuggestions = new Set(excludeBridgeSuggestionKeys ?? [])
|
||||
for (const suggestion of bridgeSuggestions) {
|
||||
const key = `${suggestion.clusterAId}-${suggestion.clusterBId}`
|
||||
if (hiddenSuggestions.has(key)) continue
|
||||
items.push({
|
||||
kind: 'suggestion',
|
||||
id: `suggestion-${suggestion.clusterAId}-${suggestion.clusterBId}`,
|
||||
id: `suggestion-${key}`,
|
||||
priority: 80,
|
||||
suggestion,
|
||||
})
|
||||
@@ -245,7 +251,7 @@ export function IntelligenceHub({
|
||||
}
|
||||
|
||||
return items.sort((a, b) => b.priority - a.priority).slice(0, 10)
|
||||
}, [insights, bridgeNotes, bridgeSuggestions, agentActions])
|
||||
}, [insights, bridgeNotes, bridgeSuggestions, agentActions, excludeBridgeSuggestionKeys])
|
||||
|
||||
const counts = useMemo(() => ({
|
||||
all: allItems.length,
|
||||
@@ -302,32 +308,32 @@ export function IntelligenceHub({
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Sparkles size={12} className="text-indigo-500" />
|
||||
<span className="text-[9px] font-mono font-bold uppercase tracking-wider text-indigo-600 dark:text-indigo-400">
|
||||
<Sparkles size={12} className="text-brand-accent" />
|
||||
<span className="text-[13px] font-medium text-brand-accent">
|
||||
{t('homeDashboard.semanticConnection')}
|
||||
</span>
|
||||
{!insight.viewed && (
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-ochre animate-pulse" />
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-brand-accent animate-pulse" />
|
||||
)}
|
||||
</div>
|
||||
<ConnectionDiagram
|
||||
note1Title={insight.note1.title || t('homeDashboard.untitled')}
|
||||
note2Title={insight.note2.title || t('homeDashboard.untitled')}
|
||||
note1Title={pathNoteTitle(insight.note1.title, insight.note1Excerpt) || t('homeDashboard.untitled')}
|
||||
note2Title={pathNoteTitle(insight.note2.title, insight.note2Excerpt) || t('homeDashboard.untitled')}
|
||||
score={insight.score}
|
||||
/>
|
||||
<p className="text-[11px] text-ink/75 dark:text-dark-ink/75 font-serif italic leading-relaxed line-clamp-3 px-1">
|
||||
<p className="text-sm text-ink/75 dark:text-dark-ink/75 font-serif italic leading-relaxed line-clamp-3 px-1">
|
||||
« {text} »
|
||||
</p>
|
||||
{excerpt && (
|
||||
<p className="text-[9px] text-concrete/80 line-clamp-2 mt-2 px-1 border-s-2 border-indigo-500/20 ps-2">
|
||||
<p className="text-sm text-concrete/80 line-clamp-2 mt-2 px-1 border-s-2 border-brand-accent/20 ps-2">
|
||||
{excerpt}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex items-center gap-1.5 mt-auto pt-3 flex-wrap">
|
||||
<div className="flex items-center gap-1.5 mt-auto pt-3 flex-wrap shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onOpenInsightNote(insight, insight.note1.id)}
|
||||
className="inline-flex items-center gap-1 text-[8.5px] font-mono uppercase font-bold px-2.5 py-1.5 rounded-lg bg-indigo-600 text-white hover:bg-indigo-700 transition-colors"
|
||||
className="inline-flex items-center gap-1 text-[13px] font-medium px-2.5 py-1.5 rounded-lg bg-brand-accent text-white hover:bg-brand-accent/90 transition-colors"
|
||||
>
|
||||
<ExternalLink size={9} />
|
||||
{t('homeDashboard.intelOpenNote')}
|
||||
@@ -335,7 +341,7 @@ export function IntelligenceHub({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onOpenInsightNote(insight, insight.note1.id, insight.note2.id)}
|
||||
className="inline-flex items-center gap-1 text-[8.5px] font-mono uppercase font-bold px-2.5 py-1.5 rounded-lg border border-border/40 hover:border-indigo-400/40 transition-colors"
|
||||
className="inline-flex items-center gap-1 text-[13px] font-medium px-2.5 py-1.5 rounded-lg border border-border/40 hover:border-brand-accent/40 transition-colors"
|
||||
>
|
||||
<GitCompare size={9} />
|
||||
{t('homeDashboard.intelCompare')}
|
||||
@@ -358,19 +364,19 @@ export function IntelligenceHub({
|
||||
|
||||
case 'bridge': {
|
||||
const { bridge } = item
|
||||
const title = bridge.note?.title || t('homeDashboard.untitled')
|
||||
const title = pathNoteTitle(bridge.note?.title, bridge.note?.content) || t('homeDashboard.untitled')
|
||||
const excerpt = bridge.note?.content ? stripHtml(bridge.note.content).slice(0, 160) : ''
|
||||
const names = bridge.clusterNames ?? []
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex items-center justify-between gap-2 mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Zap size={12} className="text-ochre" />
|
||||
<span className="text-[9px] font-mono font-bold uppercase tracking-wider text-ochre">
|
||||
<Zap size={12} className="text-brand-accent" />
|
||||
<span className="text-[13px] font-medium text-brand-accent">
|
||||
{t('homeDashboard.bridgeNote')}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-[9px] font-mono font-bold text-ochre bg-ochre/10 px-2 py-0.5 rounded-full">
|
||||
<span className="text-[13px] font-medium text-brand-accent bg-brand-accent/10 px-2 py-0.5 rounded-full">
|
||||
{Math.round(bridge.bridgeScore * 100)}%
|
||||
</span>
|
||||
</div>
|
||||
@@ -379,11 +385,11 @@ export function IntelligenceHub({
|
||||
onClick={() => onNoteSelect(bridge.noteId)}
|
||||
className="text-start group flex-1"
|
||||
>
|
||||
<p className="text-sm font-semibold text-ink dark:text-dark-ink group-hover:text-ochre transition-colors mb-2 leading-snug">
|
||||
<p className="text-sm font-semibold text-ink dark:text-dark-ink group-hover:text-brand-accent transition-colors mb-2 leading-snug">
|
||||
{title}
|
||||
</p>
|
||||
{excerpt && (
|
||||
<p className="text-[10px] text-concrete leading-relaxed line-clamp-3 mb-3">{excerpt}</p>
|
||||
<p className="text-sm text-concrete leading-relaxed line-clamp-3 mb-3">{excerpt}</p>
|
||||
)}
|
||||
{names.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
@@ -396,7 +402,7 @@ export function IntelligenceHub({
|
||||
className="w-1.5 h-1.5 rounded-full"
|
||||
style={{ backgroundColor: CLUSTER_COLORS[i % CLUSTER_COLORS.length] }}
|
||||
/>
|
||||
<span className="text-[8px] font-mono uppercase text-concrete">{name}</span>
|
||||
<span className="text-[13px] text-concrete">{name}</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
@@ -405,7 +411,7 @@ export function IntelligenceHub({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onNoteSelect(bridge.noteId)}
|
||||
className="mt-3 inline-flex items-center gap-1 text-[8.5px] font-mono uppercase font-bold px-2.5 py-1.5 rounded-lg bg-ochre/90 text-white hover:bg-ochre transition-colors w-fit"
|
||||
className="mt-3 inline-flex items-center gap-1 text-[13px] font-medium px-2.5 py-1.5 rounded-lg bg-brand-accent/90 text-white hover:bg-brand-accent transition-colors w-fit"
|
||||
>
|
||||
<ExternalLink size={9} />
|
||||
{t('homeDashboard.intelOpenNote')}
|
||||
@@ -421,28 +427,28 @@ export function IntelligenceHub({
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Lightbulb size={12} className="text-violet-500" />
|
||||
<span className="text-[9px] font-mono font-bold uppercase tracking-wider text-violet-600 dark:text-violet-400 truncate">
|
||||
<Lightbulb size={12} className="text-brand-accent" />
|
||||
<span className="text-[13px] font-medium text-brand-accent truncate">
|
||||
{t('homeDashboard.intelMissingLink')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<span className="px-2 py-1 rounded-lg bg-violet-500/10 text-[9px] font-mono font-bold text-violet-700 dark:text-violet-300 truncate max-w-[45%]">
|
||||
<span className="px-2 py-1 rounded-lg bg-brand-accent/10 text-[13px] font-medium text-brand-accent truncate max-w-[45%]" title={suggestion.clusterAName}>
|
||||
{suggestion.clusterAName}
|
||||
</span>
|
||||
<div className="flex-1 h-px bg-gradient-to-r from-violet-400/40 via-ochre/60 to-violet-400/40" />
|
||||
<span className="px-2 py-1 rounded-lg bg-ochre/10 text-[9px] font-mono font-bold text-ochre truncate max-w-[45%]">
|
||||
<div className="flex-1 h-px bg-gradient-to-r from-brand-accent/40 via-brand-accent/60 to-brand-accent/40" />
|
||||
<span className="px-2 py-1 rounded-lg bg-brand-accent/10 text-[13px] font-medium text-brand-accent truncate max-w-[45%]" title={suggestion.clusterBName}>
|
||||
{suggestion.clusterBName}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm font-semibold text-ink dark:text-dark-ink mb-1.5">{suggestion.suggestedTitle}</p>
|
||||
<p className="text-[10px] text-concrete leading-relaxed line-clamp-2 flex-1">{suggestion.suggestedContent}</p>
|
||||
<div className="flex items-center gap-1.5 mt-3 pt-3 border-t border-border/15">
|
||||
<p className="text-sm text-concrete leading-relaxed line-clamp-2 flex-1">{suggestion.suggestedContent}</p>
|
||||
<div className="flex items-center gap-1.5 mt-3 pt-3 border-t border-border/15 shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
disabled={busy}
|
||||
onClick={() => onCreateBridgeSuggestion(suggestion)}
|
||||
className="inline-flex items-center gap-1 text-[8.5px] font-mono uppercase font-bold px-2.5 py-1.5 rounded-lg bg-violet-600 text-white hover:bg-violet-700 transition-colors disabled:opacity-40"
|
||||
className="inline-flex items-center gap-1 text-[13px] font-medium px-2.5 py-1.5 rounded-lg bg-brand-accent text-white hover:bg-brand-accent/90 transition-colors disabled:opacity-40"
|
||||
>
|
||||
{busy ? <Loader2 size={9} className="animate-spin" /> : <Link2 size={9} />}
|
||||
{t('homeDashboard.createBridgeNote')}
|
||||
@@ -451,7 +457,7 @@ export function IntelligenceHub({
|
||||
type="button"
|
||||
disabled={busy}
|
||||
onClick={() => onDismissBridgeSuggestion(suggestion)}
|
||||
className="text-[8.5px] font-mono uppercase px-2 py-1.5 rounded-lg border border-border/30 text-concrete hover:text-rose-500 disabled:opacity-40"
|
||||
className="text-[13px] font-medium px-2 py-1.5 rounded-lg border border-border/30 text-concrete hover:text-rose-500 disabled:opacity-40"
|
||||
>
|
||||
{t('homeDashboard.dismiss')}
|
||||
</button>
|
||||
@@ -465,25 +471,25 @@ export function IntelligenceHub({
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Bot size={12} className="text-ochre" />
|
||||
<span className="text-[9px] font-mono font-bold uppercase tracking-wider text-ochre">
|
||||
<Bot size={12} className="text-brand-accent" />
|
||||
<span className="text-[13px] font-medium text-brand-accent">
|
||||
{agent.agentName}
|
||||
</span>
|
||||
<span className="text-[8px] font-mono text-concrete/60 ms-auto">
|
||||
<span className="text-[13px] text-concrete/60 ms-auto">
|
||||
{formatRelativeTime(agent.createdAt, t)}
|
||||
</span>
|
||||
</div>
|
||||
{agent.result ? (
|
||||
<p className="text-[11px] text-concrete leading-relaxed line-clamp-4 flex-1">
|
||||
<p className="text-sm text-concrete leading-relaxed line-clamp-4 flex-1">
|
||||
{agent.result}
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-[11px] text-concrete/60 italic flex-1">{t('homeDashboard.intelAgentNoResult')}</p>
|
||||
<p className="text-sm text-concrete/60 italic flex-1">{t('homeDashboard.intelAgentNoResult')}</p>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => router.push('/agents')}
|
||||
className="mt-3 inline-flex items-center gap-1 text-[8.5px] font-mono uppercase font-bold px-2.5 py-1.5 rounded-lg border border-ochre/30 text-ochre hover:bg-ochre/10 transition-colors w-fit"
|
||||
className="mt-3 inline-flex items-center gap-1 text-[13px] font-medium px-2.5 py-1.5 rounded-lg border border-brand-accent/30 text-brand-accent hover:bg-brand-accent/10 transition-colors w-fit"
|
||||
>
|
||||
{t('homeDashboard.intelViewAgent')}
|
||||
<ArrowRight size={9} />
|
||||
@@ -497,10 +503,10 @@ export function IntelligenceHub({
|
||||
const spotlightAccent = (item: IntelItem | null) => {
|
||||
if (!item) return 'from-stone-100/50 to-transparent border-border/30'
|
||||
switch (item.kind) {
|
||||
case 'insight': return 'from-indigo-500/[0.06] via-transparent to-transparent border-indigo-400/25'
|
||||
case 'bridge': return 'from-ochre/[0.06] via-transparent to-transparent border-ochre/25'
|
||||
case 'suggestion': return 'from-violet-500/[0.06] via-transparent to-transparent border-violet-400/25'
|
||||
case 'agent': return 'from-ochre/[0.04] via-transparent to-transparent border-border/30'
|
||||
case 'insight': return 'from-brand-accent/[0.06] via-transparent to-transparent border-brand-accent/25'
|
||||
case 'bridge': return 'from-brand-accent/[0.06] via-transparent to-transparent border-brand-accent/25'
|
||||
case 'suggestion': return 'from-brand-accent/[0.06] via-transparent to-transparent border-brand-accent/25'
|
||||
case 'agent': return 'from-brand-accent/[0.04] via-transparent to-transparent border-border/30'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -518,7 +524,7 @@ export function IntelligenceHub({
|
||||
actions={(
|
||||
<>
|
||||
{newCount > 0 && (
|
||||
<span className="text-[8px] font-mono font-bold text-brand-accent bg-brand-accent/10 px-2 py-0.5 rounded uppercase">
|
||||
<span className="text-[13px] font-medium text-brand-accent bg-brand-accent/10 px-2 py-0.5 rounded">
|
||||
{newCount} {t('homeDashboard.new')}
|
||||
</span>
|
||||
)}
|
||||
@@ -543,7 +549,7 @@ export function IntelligenceHub({
|
||||
<div className="h-[220px] rounded-2xl bg-stone-50 dark:bg-zinc-950/30 animate-pulse" />
|
||||
) : !aiActive ? (
|
||||
<div className="p-5 rounded-2xl border border-dashed border-border/40 bg-stone-50/50 dark:bg-zinc-950/30 text-center space-y-3 min-h-[180px] flex flex-col justify-center">
|
||||
<p className="text-xs text-concrete leading-relaxed">
|
||||
<p className="text-sm text-concrete leading-relaxed">
|
||||
{!hasAiConsent
|
||||
? t('homeDashboard.aiConsentRequired')
|
||||
: !providerReady
|
||||
@@ -554,7 +560,7 @@ export function IntelligenceHub({
|
||||
<button
|
||||
type="button"
|
||||
onClick={onEnableAi}
|
||||
className="text-[9px] font-mono uppercase font-bold px-3 py-1.5 rounded-lg bg-ink text-white dark:bg-white dark:text-black hover:opacity-90 transition-opacity mx-auto"
|
||||
className="text-[13px] font-medium px-3 py-1.5 rounded-lg bg-brand-accent text-white hover:bg-brand-accent/90 transition-colors mx-auto"
|
||||
>
|
||||
{t('homeDashboard.enableAi')}
|
||||
</button>
|
||||
@@ -563,12 +569,12 @@ export function IntelligenceHub({
|
||||
) : allItems.length === 0 ? (
|
||||
<div className="p-5 rounded-2xl border border-dashed border-border/40 bg-stone-50/50 dark:bg-zinc-950/30 text-center space-y-3 min-h-[180px] flex flex-col justify-center">
|
||||
<Brain size={22} className="mx-auto text-concrete/40" strokeWidth={1.25} />
|
||||
<p className="text-xs text-concrete leading-relaxed">{t('homeDashboard.noConnections')}</p>
|
||||
<p className="text-sm text-concrete leading-relaxed">{t('homeDashboard.noConnections')}</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onRefreshEcho}
|
||||
disabled={echoRefreshing}
|
||||
className="inline-flex items-center gap-1.5 text-[9px] font-mono uppercase font-bold px-3 py-1.5 rounded-lg border border-ochre/30 text-ochre hover:bg-ochre/10 transition-all disabled:opacity-40 mx-auto"
|
||||
className="inline-flex items-center gap-1.5 text-[13px] font-medium px-3 py-1.5 rounded-lg border border-brand-accent/30 text-brand-accent hover:bg-brand-accent/10 transition-all disabled:opacity-40 mx-auto"
|
||||
>
|
||||
{echoRefreshing ? <Loader2 size={11} className="animate-spin" /> : <Sparkles size={11} />}
|
||||
{t('homeDashboard.analyzeNotes')}
|
||||
@@ -587,15 +593,15 @@ export function IntelligenceHub({
|
||||
key={f.key}
|
||||
type="button"
|
||||
onClick={() => setFilter(f.key)}
|
||||
className={`shrink-0 inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-[8.5px] font-mono font-bold uppercase tracking-wider transition-all ${
|
||||
className={`shrink-0 inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-[13px] font-medium transition-all ${
|
||||
active
|
||||
? 'bg-ink text-white dark:bg-white dark:text-black shadow-sm'
|
||||
? 'bg-brand-accent text-white shadow-sm'
|
||||
: 'bg-stone-100 dark:bg-zinc-800 text-concrete hover:text-ink dark:hover:text-dark-ink'
|
||||
}`}
|
||||
>
|
||||
{f.label}
|
||||
{count > 0 && (
|
||||
<span className={`text-[7px] px-1 py-px rounded-full ${active ? 'bg-white/20' : 'bg-black/5 dark:bg-white/10'}`}>
|
||||
<span className={`text-[13px] px-1.5 py-0.5 rounded-full ${active ? 'bg-white/20' : 'bg-black/5 dark:bg-white/10'}`}>
|
||||
{count}
|
||||
</span>
|
||||
)}
|
||||
@@ -605,7 +611,7 @@ export function IntelligenceHub({
|
||||
</div>
|
||||
|
||||
{filteredItems.length === 0 ? (
|
||||
<p className="text-xs text-concrete italic text-center py-8">{t('homeDashboard.intelFilterEmpty')}</p>
|
||||
<p className="text-sm text-concrete italic text-center py-8">{t('homeDashboard.intelFilterEmpty')}</p>
|
||||
) : (
|
||||
<>
|
||||
{/* Spotlight carousel */}
|
||||
@@ -616,7 +622,7 @@ export function IntelligenceHub({
|
||||
type="button"
|
||||
onClick={goPrev}
|
||||
disabled={activeIndex === 0}
|
||||
className="absolute start-0 top-1/2 -translate-y-1/2 -translate-x-1 z-10 p-1 rounded-full border border-border/40 bg-white/90 dark:bg-zinc-900/90 shadow-sm disabled:opacity-25 hover:border-ochre/40 transition-all"
|
||||
className="absolute start-0 top-1/2 -translate-y-1/2 -translate-x-1 z-10 p-1 rounded-full border border-border/40 bg-white/90 dark:bg-zinc-900/90 shadow-sm disabled:opacity-25 hover:border-brand-accent/40 transition-all"
|
||||
aria-label={t('homeDashboard.intelPrev')}
|
||||
>
|
||||
<ChevronLeft size={14} />
|
||||
@@ -625,7 +631,7 @@ export function IntelligenceHub({
|
||||
type="button"
|
||||
onClick={goNext}
|
||||
disabled={activeIndex >= filteredItems.length - 1}
|
||||
className="absolute end-0 top-1/2 -translate-y-1/2 translate-x-1 z-10 p-1 rounded-full border border-border/40 bg-white/90 dark:bg-zinc-900/90 shadow-sm disabled:opacity-25 hover:border-ochre/40 transition-all"
|
||||
className="absolute end-0 top-1/2 -translate-y-1/2 translate-x-1 z-10 p-1 rounded-full border border-border/40 bg-white/90 dark:bg-zinc-900/90 shadow-sm disabled:opacity-25 hover:border-brand-accent/40 transition-all"
|
||||
aria-label={t('homeDashboard.intelNext')}
|
||||
>
|
||||
<ChevronRight size={14} />
|
||||
@@ -664,14 +670,14 @@ export function IntelligenceHub({
|
||||
onClick={() => setActiveIndex(idx)}
|
||||
className={`rounded-full transition-all ${
|
||||
idx === activeIndex
|
||||
? 'w-5 h-1.5 bg-ochre'
|
||||
? 'w-5 h-1.5 bg-brand-accent'
|
||||
: 'w-1.5 h-1.5 bg-concrete/25 hover:bg-concrete/50'
|
||||
}`}
|
||||
aria-label={t('homeDashboard.intelGoTo', { index: idx + 1 })}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<span className="text-[8px] font-mono text-concrete/60 uppercase">
|
||||
<span className="text-[13px] text-concrete/60">
|
||||
{t('homeDashboard.intelPosition', { current: activeIndex + 1, total: filteredItems.length })}
|
||||
</span>
|
||||
</div>
|
||||
@@ -684,37 +690,39 @@ export function IntelligenceHub({
|
||||
const isActive = idx === activeIndex
|
||||
let label = ''
|
||||
let Icon = Sparkles
|
||||
let accent = 'text-indigo-500'
|
||||
let accent = 'text-brand-accent'
|
||||
if (item.kind === 'insight') {
|
||||
label = item.insight.note1.title?.slice(0, 28) || t('homeDashboard.untitled')
|
||||
label = pathNoteTitle(item.insight.note1.title, item.insight.note1Excerpt) || t('homeDashboard.untitled')
|
||||
Icon = Sparkles
|
||||
accent = 'text-indigo-500'
|
||||
accent = 'text-brand-accent'
|
||||
} else if (item.kind === 'bridge') {
|
||||
label = item.bridge.note?.title?.slice(0, 28) || t('homeDashboard.untitled')
|
||||
label = pathNoteTitle(item.bridge.note?.title, item.bridge.note?.content) || t('homeDashboard.untitled')
|
||||
Icon = Zap
|
||||
accent = 'text-ochre'
|
||||
accent = 'text-brand-accent'
|
||||
} else if (item.kind === 'suggestion') {
|
||||
label = item.suggestion.suggestedTitle.slice(0, 28)
|
||||
label = item.suggestion.suggestedTitle
|
||||
Icon = Lightbulb
|
||||
accent = 'text-violet-500'
|
||||
accent = 'text-brand-accent'
|
||||
} else {
|
||||
label = item.agent.agentName
|
||||
Icon = Bot
|
||||
accent = 'text-ochre'
|
||||
accent = 'text-brand-accent'
|
||||
}
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
onClick={() => setActiveIndex(idx)}
|
||||
className={`shrink-0 flex items-center gap-1.5 px-2 py-1.5 rounded-lg border text-start max-w-[130px] transition-all ${
|
||||
title={label}
|
||||
aria-label={label}
|
||||
className={`shrink-0 flex items-center gap-1.5 px-2 py-1.5 rounded-lg border text-start max-w-[180px] transition-all ${
|
||||
isActive
|
||||
? 'border-ochre/40 bg-ochre/5 shadow-sm'
|
||||
? 'border-brand-accent/40 bg-brand-accent/5 shadow-sm'
|
||||
: 'border-border/25 bg-stone-50/50 dark:bg-zinc-950/30 hover:border-border/50'
|
||||
}`}
|
||||
>
|
||||
<Icon size={9} className={`shrink-0 ${accent}`} />
|
||||
<span className="text-[8px] font-medium text-ink dark:text-dark-ink truncate">{label}</span>
|
||||
<span className="text-[13px] font-medium text-ink dark:text-dark-ink truncate">{label}</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useLanguage } from '@/lib/i18n'
|
||||
import type { SupportedLanguage } from '@/lib/i18n/load-translations'
|
||||
import { SUBSCRIPTION_TRIAL_DAYS } from '@/lib/billing/trial-constants'
|
||||
import { useEffect, useRef, useState, type ReactNode } from 'react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
|
||||
const ECHO_LINES = ['echo0', 'echo1', 'echo2'] as const
|
||||
|
||||
@@ -39,6 +40,16 @@ export function LandingPage() {
|
||||
const [langOpen, setLangOpen] = useState(false)
|
||||
const [echoIndex, setEchoIndex] = useState(0)
|
||||
const langRef = useRef<HTMLDivElement>(null)
|
||||
const { data: byokCatalog } = useQuery({
|
||||
queryKey: ['public', 'byok-catalog'],
|
||||
queryFn: async () => {
|
||||
const res = await fetch('/api/public/byok-catalog')
|
||||
if (!res.ok) throw new Error('catalog')
|
||||
return res.json() as Promise<{ providers: { id: string; name: string }[] }>
|
||||
},
|
||||
staleTime: 60_000,
|
||||
})
|
||||
const byokProviders = byokCatalog?.providers ?? []
|
||||
|
||||
useEffect(() => {
|
||||
if (!langOpen) return
|
||||
@@ -102,6 +113,22 @@ export function LandingPage() {
|
||||
{ href: '#pricing', label: t('landing.nav.pricing') },
|
||||
]
|
||||
|
||||
const scrollPublicHash = (hash: string) => {
|
||||
const id = hash.replace(/^#/, '')
|
||||
const target = document.getElementById(id)
|
||||
const root = document.querySelector<HTMLElement>('[data-public-scroll-root]')
|
||||
if (!target || !root) return
|
||||
const top = target.getBoundingClientRect().top - root.getBoundingClientRect().top + root.scrollTop - 88
|
||||
root.scrollTo({ top, behavior: 'smooth' })
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const hash = window.location.hash
|
||||
if (!hash) return
|
||||
const id = window.requestAnimationFrame(() => scrollPublicHash(hash))
|
||||
return () => window.cancelAnimationFrame(id)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0B0A09] text-[#F4F1EA] font-[family-name:var(--font-manrope)] selection:bg-[#D4A373]/40 selection:text-white">
|
||||
{/* Nav */}
|
||||
@@ -114,7 +141,16 @@ export function LandingPage() {
|
||||
</Link>
|
||||
<div className="hidden lg:flex items-center gap-8">
|
||||
{NAV.map((l) => (
|
||||
<a key={l.href} href={l.href} className="text-[13px] text-white/55 hover:text-white transition-colors">
|
||||
<a
|
||||
key={l.href}
|
||||
href={l.href}
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
scrollPublicHash(l.href)
|
||||
window.history.replaceState(null, '', l.href)
|
||||
}}
|
||||
className="text-[13px] text-white/75 hover:text-white transition-colors"
|
||||
>
|
||||
{l.label}
|
||||
</a>
|
||||
))}
|
||||
@@ -167,7 +203,7 @@ export function LandingPage() {
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
<Link href="/login" className="hidden sm:inline text-[13px] text-white/55 hover:text-white transition-colors px-2">
|
||||
<Link href="/login" className="hidden sm:inline text-[13px] text-white/75 hover:text-white transition-colors px-2">
|
||||
{t('landing.nav.login')}
|
||||
</Link>
|
||||
<Link
|
||||
@@ -200,13 +236,25 @@ export function LandingPage() {
|
||||
<a
|
||||
key={l.href}
|
||||
href={l.href}
|
||||
onClick={() => setMenuOpen(false)}
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
setMenuOpen(false)
|
||||
scrollPublicHash(l.href)
|
||||
window.history.replaceState(null, '', l.href)
|
||||
}}
|
||||
className="py-4 text-3xl font-serif border-b border-white/10"
|
||||
>
|
||||
{l.label}
|
||||
</a>
|
||||
))}
|
||||
<Link href="/register" onClick={() => setMenuOpen(false)} className="mt-10 py-4 rounded-2xl bg-[#F4F1EA] text-[#0B0A09] text-center font-semibold">
|
||||
<Link
|
||||
href="/login"
|
||||
onClick={() => setMenuOpen(false)}
|
||||
className="mt-10 py-4 text-2xl font-serif text-white/80 text-center border-b border-white/10"
|
||||
>
|
||||
{t('landing.nav.login')}
|
||||
</Link>
|
||||
<Link href="/register" onClick={() => setMenuOpen(false)} className="mt-4 py-4 rounded-2xl bg-[#F4F1EA] text-[#0B0A09] text-center font-semibold">
|
||||
{t('landing.nav.cta')}
|
||||
</Link>
|
||||
</div>
|
||||
@@ -237,7 +285,7 @@ export function LandingPage() {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p className="text-[13px] tracking-[0.12em] uppercase text-white/40 mb-5 font-medium">
|
||||
<p className="text-[13px] tracking-[0.12em] uppercase text-white/70 mb-5 font-medium">
|
||||
{t('landing.hero.eyebrow')}
|
||||
</p>
|
||||
|
||||
@@ -247,7 +295,7 @@ export function LandingPage() {
|
||||
<span className="italic text-[#D4A373]">{t('landing.hero.headlineAccent')}</span>
|
||||
</h1>
|
||||
|
||||
<p className="max-w-xl mx-auto text-[17px] sm:text-lg text-white/55 leading-relaxed mb-10">
|
||||
<p className="max-w-xl mx-auto text-[17px] sm:text-lg text-white/75 leading-relaxed mb-10">
|
||||
{t('landing.hero.subtitle')}
|
||||
</p>
|
||||
|
||||
@@ -259,7 +307,7 @@ export function LandingPage() {
|
||||
{t('landing.hero.cta')}
|
||||
<ArrowRight size={18} className="transition-transform group-hover:translate-x-0.5" />
|
||||
</Link>
|
||||
<p className="text-[12px] text-white/35">{t('landing.hero.ctaHint')}</p>
|
||||
<p className="text-[12px] text-white/60">{t('landing.hero.ctaHint')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -307,7 +355,7 @@ export function LandingPage() {
|
||||
|
||||
{/* Trust strip */}
|
||||
<section className="px-5 sm:px-8 py-10 border-y border-white/[0.06]">
|
||||
<div className="max-w-5xl mx-auto flex flex-wrap items-center justify-center gap-x-10 gap-y-4 text-[12px] sm:text-[13px] text-white/40 tracking-wide">
|
||||
<div className="max-w-5xl mx-auto flex flex-wrap items-center justify-center gap-x-10 gap-y-4 text-[12px] sm:text-[13px] text-white/70 tracking-wide">
|
||||
{['trust0', 'trust1', 'trust2', 'trust3'].map((k) => (
|
||||
<span key={k} className="flex items-center gap-2">
|
||||
<Check size={14} className="text-[#D4A373]" />
|
||||
@@ -324,7 +372,7 @@ export function LandingPage() {
|
||||
<h2 className="font-serif text-3xl sm:text-5xl tracking-tight leading-[1.15] mb-6">
|
||||
{t('landing.pain.title')}
|
||||
</h2>
|
||||
<p className="text-lg text-white/50 leading-relaxed mb-8">{t('landing.pain.desc')}</p>
|
||||
<p className="text-lg text-white/70 leading-relaxed mb-8">{t('landing.pain.desc')}</p>
|
||||
<p className="text-base sm:text-lg font-serif italic text-[#D4A373]/90 leading-relaxed">
|
||||
{t('landing.pain.secondBrain')}
|
||||
</p>
|
||||
@@ -366,7 +414,7 @@ export function LandingPage() {
|
||||
<div className="grid grid-cols-2 gap-2 p-2">
|
||||
{['w0', 'w1', 'w2', 'w3'].map((w) => (
|
||||
<div key={w} className="rounded-xl bg-[#0B0A09]/40 border border-black/10 p-4 min-h-[88px]">
|
||||
<p className="text-[10px] uppercase tracking-wider text-[#A47148] mb-2">{t(`landing.moments.dashboard.${w}Label`)}</p>
|
||||
<p className="text-[13px] font-medium text-[#A47148] mb-2">{t(`landing.moments.dashboard.${w}Label`)}</p>
|
||||
<p className="text-sm font-medium text-[#0B0A09]/80">{t(`landing.moments.dashboard.${w}`)}</p>
|
||||
</div>
|
||||
))}
|
||||
@@ -383,7 +431,7 @@ export function LandingPage() {
|
||||
>
|
||||
<div className="relative h-36 flex items-center justify-center">
|
||||
<Network className="text-[#D4A373]/80" size={48} strokeWidth={1.25} />
|
||||
<p className="absolute bottom-2 left-4 text-[11px] uppercase tracking-wider text-white/35">
|
||||
<p className="absolute bottom-2 left-4 text-[13px] font-medium text-white/75">
|
||||
{t('landing.moments.insights.chip')}
|
||||
</p>
|
||||
</div>
|
||||
@@ -399,7 +447,7 @@ export function LandingPage() {
|
||||
<p className="font-serif text-[#0B0A09] text-base mb-3">{t('landing.moments.revision.card')}</p>
|
||||
<div className="flex gap-2">
|
||||
<span className="flex-1 py-2 rounded-lg bg-[#0B0A09]/5 text-center text-[11px] font-semibold text-[#0B0A09]/50">?</span>
|
||||
<span className="flex-1 py-2 rounded-lg bg-[#0B0A09] text-center text-[11px] font-semibold text-[#F4F1EA]">SM-2</span>
|
||||
<span className="flex-1 py-2 rounded-lg bg-[#0B0A09] text-center text-[13px] font-semibold text-[#F4F1EA]">{t('landing.moments.revision.badge')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</ProductMoment>
|
||||
@@ -418,7 +466,7 @@ export function LandingPage() {
|
||||
<div key={s} className="relative text-center md:text-left">
|
||||
<div className="text-[13px] font-semibold text-[#D4A373] mb-4 tracking-widest">0{i + 1}</div>
|
||||
<h3 className="font-serif text-xl mb-3">{t(`landing.how.${s}.title`)}</h3>
|
||||
<p className="text-sm text-white/45 leading-relaxed">{t(`landing.how.${s}.desc`)}</p>
|
||||
<p className="text-sm text-white/70 leading-relaxed">{t(`landing.how.${s}.desc`)}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -431,14 +479,14 @@ export function LandingPage() {
|
||||
<div className="max-w-2xl mb-14">
|
||||
<p className="text-[12px] uppercase tracking-[0.25em] text-[#D4A373] mb-4 font-medium">{t('landing.agents.label')}</p>
|
||||
<h2 className="font-serif text-3xl sm:text-5xl tracking-tight mb-5">{t('landing.agents.title')}</h2>
|
||||
<p className="text-white/50 text-lg leading-relaxed">{t('landing.agents.desc')}</p>
|
||||
<p className="text-white/70 text-lg leading-relaxed">{t('landing.agents.desc')}</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
{(['scraper', 'researcher', 'slideGen', 'monitor', 'diagramGen', 'custom'] as const).map((key) => (
|
||||
<div key={key} className="p-6 rounded-2xl border border-white/[0.08] bg-white/[0.03] hover:bg-white/[0.06] transition-colors">
|
||||
<Bot size={18} className="text-[#D4A373] mb-4" />
|
||||
<h4 className="font-serif text-lg mb-2">{t(`landing.agents.${key}.title`)}</h4>
|
||||
<p className="text-sm text-white/40 leading-relaxed">{t(`landing.agents.${key}.desc`)}</p>
|
||||
<p className="text-sm text-white/70 leading-relaxed">{t(`landing.agents.${key}.desc`)}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -454,15 +502,19 @@ export function LandingPage() {
|
||||
<span className="text-[12px] uppercase tracking-[0.2em] font-semibold">{t('landing.byok.label')}</span>
|
||||
</div>
|
||||
<h3 className="font-serif text-3xl tracking-tight mb-4">{t('landing.byok.title')}</h3>
|
||||
<p className="text-white/50 leading-relaxed">{t('landing.byok.desc')}</p>
|
||||
<p className="text-white/70 leading-relaxed">{t('landing.byok.desc')}</p>
|
||||
</div>
|
||||
<div className="w-full md:w-[320px] font-mono text-[11px] text-white/35 space-y-1.5 rounded-2xl border border-white/10 bg-black/40 p-5">
|
||||
<p className="text-[#D4A373]">{'{'}</p>
|
||||
<p className="pl-3">"provider": "anthropic",</p>
|
||||
<p className="pl-3">"model": "claude-sonnet",</p>
|
||||
<p className="pl-3 text-[#F4F1EA]/70">"apiKey": "sk-ant-…",</p>
|
||||
<p className="pl-3">"yours": true</p>
|
||||
<p className="text-[#D4A373]">{'}'}</p>
|
||||
<div className="w-full md:w-[320px] text-[13px] text-white/70 space-y-4 rounded-2xl border border-white/10 bg-black/40 p-5">
|
||||
{byokProviders.length > 0 ? (
|
||||
<ul className="grid grid-cols-1 gap-2 text-sm text-[#F4F1EA]">
|
||||
{byokProviders.map((p) => (
|
||||
<li key={p.id}>{p.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p className="text-white/70">{t('landing.byok.pointProvider')}</p>
|
||||
)}
|
||||
<p className="text-[#D4A373]">{t('landing.byok.pointYours')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -472,19 +524,19 @@ export function LandingPage() {
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="font-serif text-3xl sm:text-5xl tracking-tight mb-4">{t('landing.pricing.title')}</h2>
|
||||
<p className="text-white/45 mb-8">{t('landing.pricing.desc')}</p>
|
||||
<p className="text-white/70 mb-8">{t('landing.pricing.desc')}</p>
|
||||
<div className="inline-flex p-1 rounded-full border border-white/10 bg-white/[0.03]">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setBillingInterval('monthly')}
|
||||
className={`px-5 py-2 rounded-full text-[12px] font-semibold transition-all ${billingInterval === 'monthly' ? 'bg-[#F4F1EA] text-[#0B0A09]' : 'text-white/45'}`}
|
||||
className={`px-5 py-2 rounded-full text-[12px] font-semibold transition-all ${billingInterval === 'monthly' ? 'bg-[#F4F1EA] text-[#0B0A09]' : 'text-white/70'}`}
|
||||
>
|
||||
{t('landing.pricing.monthly')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setBillingInterval('annual')}
|
||||
className={`px-5 py-2 rounded-full text-[12px] font-semibold transition-all relative ${billingInterval === 'annual' ? 'bg-[#F4F1EA] text-[#0B0A09]' : 'text-white/45'}`}
|
||||
className={`px-5 py-2 rounded-full text-[12px] font-semibold transition-all relative ${billingInterval === 'annual' ? 'bg-[#F4F1EA] text-[#0B0A09]' : 'text-white/70'}`}
|
||||
>
|
||||
{t('landing.pricing.annual')}
|
||||
<span className="absolute -top-3 -right-1 text-[10px] text-[#D4A373] whitespace-nowrap">
|
||||
@@ -508,19 +560,19 @@ export function LandingPage() {
|
||||
{t('landing.pricing.popular')}
|
||||
</span>
|
||||
)}
|
||||
<h4 className="text-[12px] uppercase tracking-widest text-white/40 mb-2">
|
||||
<h4 className="text-[13px] font-medium tracking-wide text-white/80 mb-2">
|
||||
{t(`landing.pricing.${plan.key}.name`)}
|
||||
</h4>
|
||||
<div className="flex items-baseline gap-1 mb-2">
|
||||
<span className="text-3xl font-serif">{plan.price}</span>
|
||||
{plan.period && <span className="text-xs text-white/35">{plan.period}</span>}
|
||||
{plan.period && <span className="text-sm text-white/70">{plan.period}</span>}
|
||||
</div>
|
||||
{plan.hasTrial && (
|
||||
<p className="text-[11px] font-semibold text-[#D4A373] mb-3">
|
||||
{t('landing.pricing.trialBadge', { days: trialDays })}
|
||||
</p>
|
||||
)}
|
||||
<p className="text-sm text-white/45 mb-6">{t(`landing.pricing.${plan.key}.desc`)}</p>
|
||||
<p className="text-sm text-white/70 mb-6">{t(`landing.pricing.${plan.key}.desc`)}</p>
|
||||
<ul className="space-y-2.5 mb-8 flex-1">
|
||||
{plan.hasTrial && (
|
||||
<li className="flex gap-2 text-xs text-[#D4A373]/90">
|
||||
@@ -529,10 +581,12 @@ export function LandingPage() {
|
||||
</li>
|
||||
)}
|
||||
{[0, 1, 2, 3, 4, 5].map((j) => {
|
||||
const feat = t(`landing.pricing.${plan.key}.feature${j}`)
|
||||
const feat = t(`landing.pricing.${plan.key}.feature${j}`, {
|
||||
count: byokProviders.length || '…',
|
||||
})
|
||||
if (!feat || feat.startsWith('landing.')) return null
|
||||
return (
|
||||
<li key={j} className="flex gap-2 text-xs text-white/60">
|
||||
<li key={j} className="flex gap-2 text-sm text-white/80">
|
||||
<Check size={12} className="text-[#D4A373] mt-0.5 shrink-0" />
|
||||
{feat}
|
||||
</li>
|
||||
@@ -541,7 +595,7 @@ export function LandingPage() {
|
||||
</ul>
|
||||
<Link
|
||||
href="/register"
|
||||
className={`py-3 rounded-xl text-center text-[12px] font-semibold transition-colors ${
|
||||
className={`py-3 rounded-xl text-center text-[13px] font-semibold transition-colors ${
|
||||
plan.popular
|
||||
? 'bg-[#F4F1EA] text-[#0B0A09] hover:bg-white'
|
||||
: 'bg-white/10 text-white hover:bg-white/15'
|
||||
@@ -567,7 +621,7 @@ export function LandingPage() {
|
||||
<h2 className="font-serif text-4xl sm:text-6xl tracking-tight leading-tight mb-6">
|
||||
{t('landing.cta.title')}
|
||||
</h2>
|
||||
<p className="text-white/50 text-lg mb-10">{t('landing.cta.desc')}</p>
|
||||
<p className="text-white/70 text-lg mb-10">{t('landing.cta.desc')}</p>
|
||||
<Link
|
||||
href="/register"
|
||||
className="inline-flex items-center gap-3 px-10 py-4 rounded-full bg-[#F4F1EA] text-[#0B0A09] text-[15px] font-semibold hover:bg-white transition-all hover:scale-[1.02]"
|
||||
@@ -575,7 +629,7 @@ export function LandingPage() {
|
||||
{t('landing.cta.button')}
|
||||
<ArrowRight size={18} />
|
||||
</Link>
|
||||
<p className="mt-4 text-[12px] text-white/30">{t('landing.hero.ctaHint')}</p>
|
||||
<p className="mt-4 text-[12px] text-white/75">{t('landing.hero.ctaHint')}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -588,15 +642,15 @@ export function LandingPage() {
|
||||
</div>
|
||||
<span className="font-serif text-lg">Memento</span>
|
||||
</div>
|
||||
<p className="text-sm text-white/35">{t('landing.footer.desc')}</p>
|
||||
<p className="text-sm text-white/60">{t('landing.footer.desc')}</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-10 text-sm">
|
||||
{(['product', 'community', 'legal'] as const).map((section) => (
|
||||
<div key={section}>
|
||||
<p className="text-[11px] uppercase tracking-widest text-white/40 mb-3">
|
||||
<p className="text-[13px] font-medium tracking-wide text-white/70 mb-3">
|
||||
{t(`landing.footer.${section}.title`)}
|
||||
</p>
|
||||
<ul className="space-y-2 text-white/50">
|
||||
<ul className="space-y-2 text-white/70">
|
||||
{[0, 1, 2].map((j) => {
|
||||
const label = t(`landing.footer.${section}.link${j}`)
|
||||
const href = t(`landing.footer.${section}.link${j}Href`)
|
||||
@@ -616,7 +670,7 @@ export function LandingPage() {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<p className="max-w-6xl mx-auto mt-12 pt-8 border-t border-white/[0.06] text-[11px] text-white/25 tracking-wide">
|
||||
<p className="max-w-6xl mx-auto mt-12 pt-8 border-t border-white/[0.06] text-[13px] text-white/70 tracking-wide">
|
||||
© 2026 Memento. {t('landing.footer.rights')}
|
||||
</p>
|
||||
</footer>
|
||||
@@ -659,7 +713,7 @@ function ProductMoment({
|
||||
{eyebrow}
|
||||
</p>
|
||||
<h3 className="font-serif text-2xl sm:text-3xl tracking-tight mb-3 leading-tight">{title}</h3>
|
||||
<p className={`text-[15px] leading-relaxed ${dark ? 'text-white/50' : 'text-[#0B0A09]/55'}`}>{desc}</p>
|
||||
<p className={`text-[15px] leading-relaxed ${dark ? 'text-white/70' : 'text-[#0B0A09]/75'}`}>{desc}</p>
|
||||
</div>
|
||||
<div className={`${compact ? 'px-4 pb-6' : 'p-6 sm:p-8'} flex items-center`}>
|
||||
<div className="w-full">{children}</div>
|
||||
|
||||
@@ -131,18 +131,18 @@ export function McpSettingsPanel({
|
||||
<Info size={18} />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-[13px] font-bold text-ink">{t('mcpSettings.whatIsMcp.title')}</h4>
|
||||
<h4 className="text-sm font-semibold text-ink">{t('mcpSettings.whatIsMcp.title')}</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<p className="text-[11px] text-concrete leading-relaxed">
|
||||
<p className="text-sm text-concrete leading-relaxed">
|
||||
{t('mcpSettings.whatIsMcp.description')}
|
||||
</p>
|
||||
<a
|
||||
href="https://modelcontextprotocol.io"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1.5 text-[10px] font-bold text-brand-accent uppercase tracking-widest hover:underline mt-4"
|
||||
className="inline-flex items-center gap-1.5 text-sm font-medium text-brand-accent hover:underline mt-4"
|
||||
>
|
||||
{t('mcpSettings.whatIsMcp.learnMore')}
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
@@ -156,21 +156,21 @@ export function McpSettingsPanel({
|
||||
<Server size={18} />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-[13px] font-bold text-ink">{t('mcpSettings.serverStatus.title')}</h4>
|
||||
<h4 className="text-sm font-semibold text-ink">{t('mcpSettings.serverStatus.title')}</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[11px] font-bold text-concrete uppercase tracking-widest">{t('mcpSettings.serverStatus.mode')}</span>
|
||||
<span className="text-[10px] font-bold text-ink uppercase tracking-widest bg-paper dark:bg-white/10 px-3 py-1 rounded-lg border border-border">
|
||||
<span className="text-sm text-concrete">{t('mcpSettings.serverStatus.mode')}</span>
|
||||
<span className="text-xs font-medium text-ink bg-paper dark:bg-white/10 px-3 py-1 rounded-lg border border-border">
|
||||
{serverStatus.mode.toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
{serverStatus.mode === 'sse' && serverStatus.url && (
|
||||
<div className="space-y-2">
|
||||
<span className="text-[10px] font-bold text-concrete uppercase tracking-widest">{t('mcpSettings.serverStatus.url')}</span>
|
||||
<code className="text-[10px] bg-paper dark:bg-black/30 p-3 rounded-xl block break-all font-mono border border-border text-ink">
|
||||
<span className="text-sm text-concrete">{t('mcpSettings.serverStatus.url')}</span>
|
||||
<code className="text-xs bg-paper dark:bg-black/30 p-3 rounded-xl block break-all font-mono border border-border text-ink">
|
||||
{serverStatus.url}
|
||||
</code>
|
||||
</div>
|
||||
@@ -182,22 +182,22 @@ export function McpSettingsPanel({
|
||||
<div className="bg-white/40 dark:bg-white/5 border border-border rounded-2xl overflow-hidden">
|
||||
<div className="flex items-center justify-between p-6 border-b border-border/40">
|
||||
<div className="flex items-center gap-5">
|
||||
<div className="p-3 bg-violet-500/10 rounded-2xl text-violet-500 border border-violet-500/20">
|
||||
<div className="p-3 bg-brand-accent/10 rounded-2xl text-brand-accent border border-brand-accent/20">
|
||||
<Key size={18} />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-[13px] font-bold text-ink">{t('mcpSettings.apiKeys.title')}</h4>
|
||||
<p className="text-[10px] text-concrete mt-0.5">{t('mcpSettings.apiKeys.description')}</p>
|
||||
<h4 className="text-sm font-semibold text-ink">{t('mcpSettings.apiKeys.title')}</h4>
|
||||
<p className="text-sm text-concrete mt-0.5">{t('mcpSettings.apiKeys.description')}</p>
|
||||
</div>
|
||||
</div>
|
||||
{!mcpAllowed ? (
|
||||
<p className="text-[10px] text-concrete max-w-[200px] text-end leading-relaxed">
|
||||
<p className="text-sm text-concrete max-w-[200px] text-end leading-relaxed">
|
||||
{t('mcpSettings.tierRequired', { tier })}
|
||||
</p>
|
||||
) : (
|
||||
<Dialog open={createOpen} onOpenChange={setCreateOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<button className="flex items-center gap-1.5 px-4 py-2 rounded-xl bg-ink text-paper text-[10px] font-bold uppercase tracking-[0.15em] hover:scale-[1.02] active:scale-95 transition-all duration-300 shadow-lg shadow-ink/20">
|
||||
<button className="flex items-center gap-1.5 px-4 py-2 rounded-xl bg-ink text-paper text-sm font-medium hover:opacity-90 active:scale-[0.98] transition-all">
|
||||
<Plus className="h-3.5 w-3.5" />
|
||||
{t('mcpSettings.apiKeys.generate')}
|
||||
</button>
|
||||
@@ -209,7 +209,7 @@ export function McpSettingsPanel({
|
||||
|
||||
{!mcpAllowed && (
|
||||
<div className="px-6 pb-4">
|
||||
<p className="text-[11px] text-amber-800 dark:text-amber-200 bg-amber-500/10 border border-amber-500/20 rounded-xl px-4 py-3 leading-relaxed">
|
||||
<p className="text-sm text-amber-800 dark:text-amber-200 bg-amber-500/10 border border-amber-500/20 rounded-xl px-4 py-3 leading-relaxed">
|
||||
{t('mcpSettings.upgradeHint')}
|
||||
</p>
|
||||
</div>
|
||||
@@ -219,7 +219,7 @@ export function McpSettingsPanel({
|
||||
{keys.length === 0 ? (
|
||||
<div className="text-center py-8">
|
||||
<Key className="h-8 w-8 mx-auto mb-2 text-concrete opacity-30" />
|
||||
<p className="text-[11px] text-concrete">{t('mcpSettings.apiKeys.empty')}</p>
|
||||
<p className="text-sm text-concrete">{t('mcpSettings.apiKeys.empty')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
@@ -243,7 +243,7 @@ export function McpSettingsPanel({
|
||||
</DialogHeader>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<Label className="text-[10px] font-bold text-concrete uppercase tracking-widest">{rawKeyName}</Label>
|
||||
<Label className="text-sm text-concrete">{rawKeyName}</Label>
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<code className="flex-1 text-[10px] bg-paper dark:bg-black/30 p-3 rounded-xl break-all font-mono border border-border text-ink">
|
||||
{showRawKey}
|
||||
@@ -260,7 +260,7 @@ export function McpSettingsPanel({
|
||||
<DialogFooter>
|
||||
<button
|
||||
onClick={() => setShowRawKey(null)}
|
||||
className="px-6 py-2.5 rounded-xl bg-ink text-paper text-[10px] font-bold uppercase tracking-[0.15em]"
|
||||
className="px-6 py-2.5 rounded-xl bg-ink text-paper text-sm font-medium"
|
||||
>
|
||||
{t('mcpSettings.createDialog.done')}
|
||||
</button>
|
||||
@@ -283,7 +283,7 @@ function CreateKeyDialog({ onGenerate, isPending }: { onGenerate: (name: string)
|
||||
</DialogHeader>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<Label htmlFor="key-name" className="text-[10px] font-bold text-concrete uppercase tracking-widest">{t('mcpSettings.createDialog.nameLabel')}</Label>
|
||||
<Label htmlFor="key-name" className="text-sm text-concrete">{t('mcpSettings.createDialog.nameLabel')}</Label>
|
||||
<Input
|
||||
id="key-name"
|
||||
placeholder={t('mcpSettings.createDialog.namePlaceholder')}
|
||||
@@ -297,7 +297,7 @@ function CreateKeyDialog({ onGenerate, isPending }: { onGenerate: (name: string)
|
||||
<button
|
||||
onClick={() => onGenerate(name)}
|
||||
disabled={isPending}
|
||||
className="px-6 py-2.5 rounded-xl bg-ink text-paper text-[10px] font-bold uppercase tracking-[0.15em] disabled:opacity-60"
|
||||
className="px-6 py-2.5 rounded-xl bg-ink text-paper text-sm font-medium disabled:opacity-60"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{isPending ? <Loader2 className="h-4 w-4 animate-spin" /> : <Key className="h-4 w-4" />}
|
||||
@@ -321,9 +321,9 @@ function KeyCard({ keyInfo, onRevoke, onDelete, isPending }: { keyInfo: McpKeyIn
|
||||
<div className="flex items-center justify-between p-4 rounded-2xl border border-border/60 bg-paper/30 dark:bg-black/10">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[13px] font-bold text-ink">{keyInfo.name}</span>
|
||||
<span className="text-sm font-semibold text-ink">{keyInfo.name}</span>
|
||||
<span className={cn(
|
||||
'text-[9px] font-bold uppercase tracking-widest px-2 py-0.5 rounded-lg',
|
||||
'text-xs font-medium px-2 py-0.5 rounded-lg',
|
||||
keyInfo.active
|
||||
? 'bg-emerald-50 dark:bg-emerald-950/40 text-emerald-700 dark:text-emerald-300'
|
||||
: 'bg-concrete/10 text-concrete'
|
||||
@@ -331,7 +331,7 @@ function KeyCard({ keyInfo, onRevoke, onDelete, isPending }: { keyInfo: McpKeyIn
|
||||
{keyInfo.active ? t('mcpSettings.apiKeys.active') : t('mcpSettings.apiKeys.revoked')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-4 text-[10px] text-concrete">
|
||||
<div className="flex gap-4 text-sm text-concrete">
|
||||
<span>{t('mcpSettings.apiKeys.createdAt')}: {formatDate(keyInfo.createdAt)}</span>
|
||||
<span>{t('mcpSettings.apiKeys.lastUsed')}: {formatDate(keyInfo.lastUsedAt)}</span>
|
||||
</div>
|
||||
@@ -341,7 +341,7 @@ function KeyCard({ keyInfo, onRevoke, onDelete, isPending }: { keyInfo: McpKeyIn
|
||||
<button
|
||||
onClick={() => onRevoke(keyInfo.shortId)}
|
||||
disabled={isPending}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-xl border border-border text-[10px] font-bold uppercase tracking-widest text-concrete hover:text-ink hover:border-ink/30 transition-colors disabled:opacity-60"
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-xl border border-border text-sm font-medium text-concrete hover:text-ink hover:border-ink/30 transition-colors disabled:opacity-60"
|
||||
>
|
||||
<Ban className="h-3 w-3" />
|
||||
{t('mcpSettings.apiKeys.revoke')}
|
||||
@@ -350,7 +350,7 @@ function KeyCard({ keyInfo, onRevoke, onDelete, isPending }: { keyInfo: McpKeyIn
|
||||
<button
|
||||
onClick={() => onDelete(keyInfo.shortId)}
|
||||
disabled={isPending}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-rose-500/10 text-rose-600 dark:text-rose-400 text-[10px] font-bold uppercase tracking-widest hover:bg-rose-500/20 transition-colors disabled:opacity-60"
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-rose-500/10 text-rose-600 dark:text-rose-400 text-sm font-medium hover:bg-rose-500/20 transition-colors disabled:opacity-60"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
{t('mcpSettings.apiKeys.delete')}
|
||||
@@ -428,8 +428,8 @@ function ConfigInstructions({ serverStatus }: { serverStatus: McpServerStatus })
|
||||
<ExternalLink size={18} />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-[13px] font-bold text-ink">{t('mcpSettings.configInstructions.title')}</h4>
|
||||
<p className="text-[10px] text-concrete mt-0.5">{t('mcpSettings.configInstructions.description')}</p>
|
||||
<h4 className="text-sm font-semibold text-ink">{t('mcpSettings.configInstructions.title')}</h4>
|
||||
<p className="text-sm text-concrete mt-0.5">{t('mcpSettings.configInstructions.description')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-6 space-y-3">
|
||||
@@ -439,7 +439,7 @@ function ConfigInstructions({ serverStatus }: { serverStatus: McpServerStatus })
|
||||
className="w-full flex items-center justify-between px-5 py-3.5 text-left hover:bg-paper/50 dark:hover:bg-white/5 transition-colors"
|
||||
onClick={() => setExpanded(expanded === cfg.id ? null : cfg.id)}
|
||||
>
|
||||
<span className="text-[11px] font-bold text-ink">{cfg.title}</span>
|
||||
<span className="text-sm font-medium text-ink">{cfg.title}</span>
|
||||
{expanded === cfg.id ? (
|
||||
<ChevronDown className="h-4 w-4 text-concrete" />
|
||||
) : (
|
||||
@@ -448,7 +448,7 @@ function ConfigInstructions({ serverStatus }: { serverStatus: McpServerStatus })
|
||||
</button>
|
||||
{expanded === cfg.id && (
|
||||
<div className="px-5 pb-5">
|
||||
<p className="text-[11px] text-concrete mb-3">{cfg.description}</p>
|
||||
<p className="text-sm text-concrete mb-3">{cfg.description}</p>
|
||||
<div className="relative">
|
||||
<pre className="text-[10px] bg-paper dark:bg-black/30 p-4 rounded-xl overflow-x-auto border border-border">
|
||||
<code>{cfg.snippet}</code>
|
||||
|
||||
@@ -310,7 +310,6 @@ export function NetworkGraph({
|
||||
.attr('stroke-width', d => d.isCentral ? 3 : d.isBridge ? 2.5 : 1.5)
|
||||
.style('filter', d => d.isBridge ? 'drop-shadow(0 0 6px rgba(212, 175, 55, 0.5))' : 'none')
|
||||
|
||||
// Labels de textes ultra-lisibles claire/sombre sans chevauchement
|
||||
node.append('text')
|
||||
.attr('dy', d => d.radius + 13)
|
||||
.attr('text-anchor', 'middle')
|
||||
|
||||
@@ -48,6 +48,8 @@ import { hi } from 'date-fns/locale/hi'
|
||||
import { nl } from 'date-fns/locale/nl'
|
||||
import { pl } from 'date-fns/locale/pl'
|
||||
import { LabelBadge } from './label-badge'
|
||||
import { ConfirmDeleteNoteDialog } from '@/components/confirm-delete-note-dialog'
|
||||
import { showNoteTrashedToast } from '@/lib/notes/trash-toast'
|
||||
import DOMPurify from 'isomorphic-dompurify'
|
||||
import { NoteImages } from './note-images'
|
||||
import { NoteChecklist } from './note-checklist'
|
||||
@@ -308,6 +310,7 @@ export const NoteCard = memo(function NoteCard({
|
||||
await deleteNote(note.id, { skipRevalidation: true })
|
||||
await refreshLabels()
|
||||
emitNoteChange({ type: 'deleted', noteId: note.id, notebookId: note.notebookId })
|
||||
showNoteTrashedToast(note, t, () => setIsHidden(false))
|
||||
} catch (error) {
|
||||
console.error('Failed to delete note:', error)
|
||||
setIsHidden(false)
|
||||
@@ -869,23 +872,11 @@ export const NoteCard = memo(function NoteCard({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Delete Confirmation Dialog */}
|
||||
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{t('notes.confirmDeleteTitle') || t('notes.delete')}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{t('notes.confirmDelete')}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>{t('common.cancel')}</AlertDialogCancel>
|
||||
<AlertDialogAction variant="destructive" onClick={handleDelete}>
|
||||
{t('notes.delete')}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<ConfirmDeleteNoteDialog
|
||||
open={showDeleteDialog}
|
||||
onOpenChange={setShowDeleteDialog}
|
||||
onConfirm={handleDelete}
|
||||
/>
|
||||
|
||||
{/* Leave Share Confirmation Dialog */}
|
||||
<AlertDialog open={showLeaveDialog} onOpenChange={setShowLeaveDialog}>
|
||||
|
||||
@@ -26,6 +26,8 @@ import { FlashcardGenerateDialog } from '@/components/flashcards/flashcard-gener
|
||||
import { NoteShareDialog } from './note-share-dialog'
|
||||
import { InteractivePagePublishDialog } from '@/components/interactive-page/interactive-page-publish-dialog'
|
||||
import { deleteNote, leaveSharedNote } from '@/app/actions/notes'
|
||||
import { ConfirmDeleteNoteDialog } from '@/components/confirm-delete-note-dialog'
|
||||
import { showNoteTrashedToast } from '@/lib/notes/trash-toast'
|
||||
import { emitNoteChange } from '@/lib/note-change-sync'
|
||||
import { useLanguage } from '@/lib/i18n'
|
||||
import { NOTE_COLORS, NoteColor, Note } from '@/lib/types'
|
||||
@@ -65,6 +67,7 @@ export function NoteEditorToolbar({ mode, onClose, onToggleAttachments, attachme
|
||||
template: (note.publishedTemplate as PublishTemplateId | null) ?? null,
|
||||
})
|
||||
const [publishLinkCopied, setPublishLinkCopied] = useState(false)
|
||||
const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false)
|
||||
const [publishTemplate, setPublishTemplate] = useState<PublishTemplateId>('magazine')
|
||||
const [publishRewrite, setPublishRewrite] = useState(false)
|
||||
const [publishEnhanceRemaining, setPublishEnhanceRemaining] = useState<number | null>(null)
|
||||
@@ -887,12 +890,12 @@ export function NoteEditorToolbar({ mode, onClose, onToggleAttachments, attachme
|
||||
onClick={() => { setShowEduMenu(false); setFlashcardsOpen(true) }}
|
||||
className="w-full flex items-center gap-3 px-4 py-3 hover:bg-muted transition-colors text-left"
|
||||
>
|
||||
<div className="p-1.5 rounded-lg bg-purple-50 dark:bg-purple-950/30 text-purple-600 dark:text-purple-400">
|
||||
<div className="p-1.5 rounded-lg bg-brand-accent/10 text-brand-accent">
|
||||
<GraduationCap size={16} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-medium">{t('flashcards.toolbarGenerate')}</div>
|
||||
<div className="text-[10px] text-muted-foreground">{t('flashcards.toolbarGenerateHint') || 'Révision espacée SM-2'}</div>
|
||||
<div className="text-sm text-muted-foreground">{t('flashcards.toolbarGenerateHint') || 'Elles reviennent au bon moment'}</div>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
@@ -1019,14 +1022,7 @@ export function NoteEditorToolbar({ mode, onClose, onToggleAttachments, attachme
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={async () => {
|
||||
try {
|
||||
await deleteNote(note.id, { skipRevalidation: true })
|
||||
emitNoteChange({ type: 'deleted', noteId: note.id, notebookId: note.notebookId })
|
||||
toast.success(t('notes.noteDeletedToast'))
|
||||
onClose()
|
||||
} catch { toast.error(t('notes.deleteNoteFailedToast')) }
|
||||
}}
|
||||
onClick={() => setConfirmDeleteOpen(true)}
|
||||
className="text-red-600 dark:text-red-400 focus:text-red-600"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 me-2" />
|
||||
@@ -1036,6 +1032,19 @@ export function NoteEditorToolbar({ mode, onClose, onToggleAttachments, attachme
|
||||
</DropdownMenu>
|
||||
)}
|
||||
|
||||
<ConfirmDeleteNoteDialog
|
||||
open={confirmDeleteOpen}
|
||||
onOpenChange={setConfirmDeleteOpen}
|
||||
onConfirm={async () => {
|
||||
try {
|
||||
await deleteNote(note.id, { skipRevalidation: true })
|
||||
emitNoteChange({ type: 'deleted', noteId: note.id, notebookId: note.notebookId })
|
||||
showNoteTrashedToast(note, t)
|
||||
onClose()
|
||||
} catch { toast.error(t('notes.deleteNoteFailedToast')) }
|
||||
}}
|
||||
/>
|
||||
|
||||
{shareOpen && (
|
||||
<NoteShareDialog
|
||||
noteId={note.id}
|
||||
|
||||
@@ -23,6 +23,8 @@ import { deleteNote, toggleArchive, togglePin, updateNote } from '@/app/actions/
|
||||
import { ReminderDialog } from '@/components/reminder-dialog'
|
||||
import { useNotebooks } from '@/context/notebooks-context'
|
||||
import { toast } from 'sonner'
|
||||
import { ConfirmDeleteNoteDialog } from '@/components/confirm-delete-note-dialog'
|
||||
import { showNoteTrashedToast } from '@/lib/notes/trash-toast'
|
||||
import { fr } from 'date-fns/locale/fr'
|
||||
import { enUS } from 'date-fns/locale/en-US'
|
||||
import { formatAbsoluteDateLocalized } from '@/lib/utils/format-localized-date'
|
||||
@@ -68,6 +70,7 @@ export function EditorialNoteMenu({
|
||||
const [, startTransition] = useTransition()
|
||||
const [showReminder, setShowReminder] = useState(false)
|
||||
const [movePickerOpen, setMovePickerOpen] = useState(false)
|
||||
const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false)
|
||||
const menuTriggerRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
const handleDelete = (e: React.MouseEvent) => {
|
||||
@@ -76,11 +79,15 @@ export function EditorialNoteMenu({
|
||||
onDeleteNote(note)
|
||||
return
|
||||
}
|
||||
setConfirmDeleteOpen(true)
|
||||
}
|
||||
|
||||
const confirmFallbackDelete = () => {
|
||||
startTransition(async () => {
|
||||
try {
|
||||
await deleteNote(note.id, { skipRevalidation: true })
|
||||
emitNoteChange({ type: 'deleted', noteId: note.id, notebookId: note.notebookId })
|
||||
toast.success(t('notes.deleted') || 'Note supprimée')
|
||||
showNoteTrashedToast(note, t)
|
||||
} catch {
|
||||
toast.error(t('general.error'))
|
||||
}
|
||||
@@ -229,6 +236,12 @@ export function EditorialNoteMenu({
|
||||
onSave={(date) => patchReminder(date)}
|
||||
onRemove={() => patchReminder(null)}
|
||||
/>
|
||||
|
||||
<ConfirmDeleteNoteDialog
|
||||
open={confirmDeleteOpen}
|
||||
onOpenChange={setConfirmDeleteOpen}
|
||||
onConfirm={confirmFallbackDelete}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2078,10 +2078,10 @@ function SlashCommandMenu({ editor, onInsertImage, onSuggestCharts, onGenerateIn
|
||||
{ ...sc('Subscript'), title: t('richTextEditor.slashSubscript'), description: t('richTextEditor.slashSubscriptDesc'), categoryId: 'text' },
|
||||
{ ...sc('Diagramme'), title: t('richTextEditor.slashDiagram'), description: t('richTextEditor.slashDiagramDesc'), categoryId: 'ai' },
|
||||
{ ...sc('Présentation'), title: t('richTextEditor.slashSlides'), description: t('richTextEditor.slashSlidesDesc'), categoryId: 'ai' },
|
||||
{ ...sc('Suggest Charts'), title: t('richTextEditor.slashCharts') || 'Graphiques IA', description: t('richTextEditor.slashChartsDesc') || 'IA suggère des graphiques', categoryId: 'ai' },
|
||||
{ ...sc('Living Block'), title: t('richTextEditor.slashLivingBlock') || 'Bloc vivant', description: t('richTextEditor.slashLivingBlockDesc') || 'Insérer depuis une autre note', categoryId: 'embed' },
|
||||
{ ...sc('Suggest Charts'), title: t('richTextEditor.slashCharts') || 'Proposer des graphiques', description: t('richTextEditor.slashChartsDesc') || 'Suggérer des graphiques d’après la note', categoryId: 'ai' },
|
||||
{ ...sc('Living Block'), title: t('richTextEditor.slashLivingBlock') || 'Bloc lié', description: t('richTextEditor.slashLivingBlockDesc') || 'Ouvrir un passage d’une autre note à côté', categoryId: 'embed' },
|
||||
{ ...sc('Database'), title: t('richTextEditor.slashDatabase'), description: t('richTextEditor.slashDatabaseDesc'), categoryId: 'data', slashKeywords: ['database', 'db', 'base', 'données', 'donnees', 'vue', 'structured', 'structuree', 'structurée'] },
|
||||
{ ...sc('Interactive Demo'), title: t('richTextEditor.slashInteractiveDemo') || 'Démo interactive', description: t('richTextEditor.slashInteractiveDemoDesc') || 'Démo pédagogique étape par étape', categoryId: 'ai', slashKeywords: ['demo', 'interactive', 'attn', 'tutorial', 'démo', 'demo interactive'] },
|
||||
{ ...sc('Interactive Demo'), title: t('richTextEditor.slashInteractiveDemo') || 'Démo pas à pas', description: t('richTextEditor.slashInteractiveDemoDesc') || 'Parcours pédagogique dans la note', categoryId: 'ai', slashKeywords: ['demo', 'interactive', 'attn', 'tutorial', 'démo', 'demo interactive'] },
|
||||
{ ...sc('Toggle'), title: t('richTextEditor.slashToggle'), description: t('richTextEditor.slashToggleDesc'), categoryId: 'text', slashKeywords: ['toggle', 'accordion', 'replier', 'deroulant', 'déroulant', 'section'] },
|
||||
{ ...sc('Callout'), title: t('richTextEditor.slashCallout'), description: t('richTextEditor.slashCalloutDesc'), categoryId: 'text', slashKeywords: ['callout', 'encadre', 'encadré', 'info', 'alerte', 'astuce', 'tip', 'warning'] },
|
||||
{ ...sc('Outline'), title: t('richTextEditor.slashOutline'), description: t('richTextEditor.slashOutlineDesc'), categoryId: 'text', slashKeywords: ['outline', 'sommaire', 'toc', 'matieres', 'matières', 'plan'] },
|
||||
@@ -2194,12 +2194,12 @@ function SlashCommandMenu({ editor, onInsertImage, onSuggestCharts, onGenerateIn
|
||||
finally { setAiLoading(false) }
|
||||
} else if (
|
||||
item.title === 'Suggest Charts'
|
||||
|| item.title === (t('richTextEditor.slashCharts') || 'Graphiques IA')
|
||||
|| item.title === (t('richTextEditor.slashCharts') || 'Proposer des graphiques')
|
||||
) {
|
||||
deleteSlashText(); closeMenu(); onSuggestCharts()
|
||||
} else if (
|
||||
item.title === 'Interactive Demo'
|
||||
|| item.title === (t('richTextEditor.slashInteractiveDemo') || 'Démo interactive')
|
||||
|| item.title === (t('richTextEditor.slashInteractiveDemo') || 'Démo pas à pas')
|
||||
) {
|
||||
deleteSlashText(); closeMenu(); onGenerateInteractiveDemo()
|
||||
} else if (item.title === t('richTextEditor.slashDatabase')) {
|
||||
@@ -2397,7 +2397,7 @@ function SlashCommandMenu({ editor, onInsertImage, onSuggestCharts, onGenerateIn
|
||||
|
||||
const selectedItem = filtered[selectedIndex]
|
||||
const showPreview = selectedItem && [
|
||||
'Table', 'Tableau', 'Database', 'Suggest Charts', 'Suggest Chart', 'Living Block', 'Bloc vivant', 'Diagramme', 'Diagram', 'Présentation', 'Presentation', 'Code Block', 'Code', 'Bloc de code'
|
||||
'Table', 'Tableau', 'Database', 'Suggest Charts', 'Suggest Chart', 'Living Block', 'Bloc vivant', 'Bloc lié', 'Linked block', 'Diagramme', 'Diagram', 'Présentation', 'Presentation', 'Code Block', 'Code', 'Bloc de code'
|
||||
].includes(selectedItem.title)
|
||||
|
||||
return createPortal(
|
||||
|
||||
@@ -78,7 +78,9 @@ export function SearchModal({ isOpen, onClose }: SearchModalProps) {
|
||||
// Load saved queries from localStorage
|
||||
useEffect(() => {
|
||||
try {
|
||||
const stored = localStorage.getItem('momento-search-saved')
|
||||
const stored =
|
||||
localStorage.getItem('memento-search-saved')
|
||||
?? localStorage.getItem('momento-search-saved')
|
||||
if (stored) setSavedQueries(JSON.parse(stored))
|
||||
} catch {}
|
||||
}, [])
|
||||
@@ -405,14 +407,20 @@ export function SearchModal({ isOpen, onClose }: SearchModalProps) {
|
||||
if (!query.trim()) return
|
||||
setSavedQueries(prev => {
|
||||
const next = prev.includes(query.trim()) ? prev : [...prev.slice(-9), query.trim()]
|
||||
try { localStorage.setItem('momento-search-saved', JSON.stringify(next)) } catch {}
|
||||
try {
|
||||
localStorage.setItem('memento-search-saved', JSON.stringify(next))
|
||||
localStorage.removeItem('momento-search-saved')
|
||||
} catch {}
|
||||
return next
|
||||
})
|
||||
}
|
||||
const handleRemoveQuery = () => {
|
||||
setSavedQueries(prev => {
|
||||
const next = prev.filter(q => q !== query.trim())
|
||||
try { localStorage.setItem('momento-search-saved', JSON.stringify(next)) } catch {}
|
||||
try {
|
||||
localStorage.setItem('memento-search-saved', JSON.stringify(next))
|
||||
localStorage.removeItem('momento-search-saved')
|
||||
} catch {}
|
||||
return next
|
||||
})
|
||||
}
|
||||
@@ -671,7 +679,7 @@ export function SearchModal({ isOpen, onClose }: SearchModalProps) {
|
||||
router.push(`/home?openNote=${activeMatch.noteId}`)
|
||||
onClose()
|
||||
}}
|
||||
className="px-5 py-2.5 bg-ink text-white dark:bg-white dark:text-black hover:opacity-90 text-xs font-semibold rounded-xl flex items-center gap-2 transition-all shadow-sm"
|
||||
className="px-5 py-2.5 bg-brand-accent text-white hover:bg-brand-accent/90 text-xs font-semibold rounded-xl flex items-center gap-2 transition-all shadow-sm"
|
||||
>
|
||||
<CornerDownRight size={13} />
|
||||
<span>{t('searchModal.openInEditor')}</span>
|
||||
|
||||
@@ -35,7 +35,7 @@ export function SettingsNav({ className }: SettingsNavProps) {
|
||||
<Link
|
||||
key={tab.id}
|
||||
href={tab.href}
|
||||
className="flex items-center gap-1.5 sm:gap-2.5 px-2 sm:px-4 py-3 text-[10px] font-bold uppercase tracking-[0.18em] transition-all relative whitespace-nowrap text-concrete hover:text-ink/60"
|
||||
className="flex items-center gap-1.5 sm:gap-2 px-2.5 sm:px-3 py-2.5 text-[13px] font-medium transition-all relative whitespace-nowrap text-concrete hover:text-ink"
|
||||
style={{ color: isActive(tab.href) ? 'var(--ink)' : undefined }}
|
||||
>
|
||||
<span style={{ color: isActive(tab.href) ? 'var(--ink)' : 'var(--concrete)' }}>{tab.icon}</span>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'use client';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { loadStripe } from '@stripe/stripe-js';
|
||||
import { EmbeddedCheckoutProvider, EmbeddedCheckout } from '@stripe/react-stripe-js';
|
||||
@@ -59,7 +59,7 @@ function getStripePromise(enabled: boolean) {
|
||||
}
|
||||
|
||||
export function BillingPlans() {
|
||||
const { t } = useLanguage();
|
||||
const { t, language } = useLanguage();
|
||||
const queryClient = useQueryClient();
|
||||
const [interval, setInterval] = useState<Interval>('month');
|
||||
const [checkoutClientSecret, setCheckoutClientSecret] = useState<string | null>(null);
|
||||
@@ -69,6 +69,7 @@ export function BillingPlans() {
|
||||
const [portalLoading, setPortalLoading] = useState(false);
|
||||
const [cancelLoading, setCancelLoading] = useState(false);
|
||||
const [successBanner, setSuccessBanner] = useState<string | null>(null);
|
||||
const plansSectionRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { data: status, isLoading } = useQuery<BillingStatus>({
|
||||
queryKey: ['billing', 'status'],
|
||||
@@ -80,6 +81,17 @@ export function BillingPlans() {
|
||||
},
|
||||
});
|
||||
|
||||
const { data: byokCatalog } = useQuery({
|
||||
queryKey: ['public', 'byok-catalog'],
|
||||
queryFn: async () => {
|
||||
const res = await fetch('/api/public/byok-catalog')
|
||||
if (!res.ok) throw new Error('catalog')
|
||||
return res.json() as Promise<{ providers: { id: string }[] }>
|
||||
},
|
||||
staleTime: 60_000,
|
||||
})
|
||||
const providerCount = byokCatalog?.providers.length || '…'
|
||||
|
||||
const { data: usageData } = useQuery({
|
||||
queryKey: ['usage', 'current'],
|
||||
queryFn: async () => {
|
||||
@@ -235,6 +247,18 @@ export function BillingPlans() {
|
||||
}
|
||||
};
|
||||
|
||||
const scrollToPlans = () => {
|
||||
plansSectionRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
};
|
||||
|
||||
const handleChangePlan = (tier: Tier) => {
|
||||
if (status?.hasStripeSubscription) {
|
||||
void handlePortal('portal');
|
||||
return;
|
||||
}
|
||||
void handleCheckout(tier);
|
||||
};
|
||||
|
||||
const handleCheckoutComplete = useCallback(() => {
|
||||
setIsCheckoutOpen(false);
|
||||
setCheckoutClientSecret(null);
|
||||
@@ -276,11 +300,15 @@ export function BillingPlans() {
|
||||
t('billing.freeF5'),
|
||||
],
|
||||
current: effectiveTier === 'BASIC',
|
||||
buttonText: effectiveTier === 'BASIC' ? (t('billing.currentPlan') || 'Plan Actuel') : t('billing.startCheckout'),
|
||||
buttonText: effectiveTier === 'BASIC'
|
||||
? (t('billing.currentPlan') || 'Plan Actuel')
|
||||
: t('billing.downgradeToFree'),
|
||||
buttonClass: effectiveTier === 'BASIC'
|
||||
? 'bg-paper text-concrete cursor-default'
|
||||
: 'bg-ink text-white shadow-xl shadow-ink/20 hover:scale-[1.02] active:scale-95',
|
||||
onClick: () => {},
|
||||
: 'bg-brand-accent text-white shadow-xl shadow-brand-accent/20 hover:scale-[1.02] active:scale-95',
|
||||
onClick: () => {
|
||||
if (effectiveTier !== 'BASIC') void handleCancelSubscription();
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'pro',
|
||||
@@ -293,7 +321,7 @@ export function BillingPlans() {
|
||||
...(trialEligible ? [t('billing.trialFeature', { days: trialDays })] : []),
|
||||
t('billing.proFeature1'),
|
||||
t('billing.proFeature2'),
|
||||
t('billing.proFeature3'),
|
||||
t('billing.proFeature3', { count: providerCount }),
|
||||
t('billing.proFeature4'),
|
||||
t('billing.proFeature5'),
|
||||
t('billing.proFeature6'),
|
||||
@@ -304,7 +332,7 @@ export function BillingPlans() {
|
||||
buttonClass: effectiveTier === 'PRO'
|
||||
? 'bg-paper text-concrete cursor-default'
|
||||
: 'bg-brand-accent text-white shadow-xl shadow-brand-accent/20 hover:scale-[1.02] active:scale-95',
|
||||
onClick: () => handleCheckout('PRO'),
|
||||
onClick: () => handleChangePlan('PRO'),
|
||||
},
|
||||
{
|
||||
id: 'business',
|
||||
@@ -316,7 +344,7 @@ export function BillingPlans() {
|
||||
...(trialEligible ? [t('billing.trialFeature', { days: trialDays })] : []),
|
||||
t('billing.businessFeature1'),
|
||||
t('billing.businessFeature2'),
|
||||
t('billing.businessFeature3'),
|
||||
t('billing.businessFeature3', { count: providerCount }),
|
||||
t('billing.businessFeature4'),
|
||||
t('billing.businessFeature5'),
|
||||
t('billing.businessFeature6'),
|
||||
@@ -325,8 +353,8 @@ export function BillingPlans() {
|
||||
buttonText: effectiveTier === 'BUSINESS' ? (t('billing.currentPlan') || 'Plan Actuel') : trialCta(t('billing.businessCta') || 'Choisir Plan Business'),
|
||||
buttonClass: effectiveTier === 'BUSINESS'
|
||||
? 'bg-paper text-concrete cursor-default'
|
||||
: 'bg-ink text-white shadow-xl shadow-ink/20 hover:scale-[1.02] active:scale-95',
|
||||
onClick: () => handleCheckout('BUSINESS'),
|
||||
: 'bg-brand-accent text-white shadow-xl shadow-brand-accent/20 hover:scale-[1.02] active:scale-95',
|
||||
onClick: () => handleChangePlan('BUSINESS'),
|
||||
},
|
||||
{
|
||||
id: 'enterprise',
|
||||
@@ -345,22 +373,24 @@ export function BillingPlans() {
|
||||
buttonText: effectiveTier === 'ENTERPRISE' ? (t('billing.currentPlan') || 'Plan Actuel') : (t('billing.contactSales') || 'Contact Sales'),
|
||||
buttonClass: effectiveTier === 'ENTERPRISE'
|
||||
? 'bg-paper text-concrete cursor-default'
|
||||
: 'bg-ink text-white shadow-xl shadow-ink/20 hover:scale-[1.02] active:scale-95',
|
||||
: 'bg-brand-accent text-white shadow-xl shadow-brand-accent/20 hover:scale-[1.02] active:scale-95',
|
||||
onClick: () => { window.location.href = 'mailto:sales@memento-note.com'; },
|
||||
},
|
||||
];
|
||||
|
||||
const plansToShow = isPaid ? plans.filter((p) => p.id !== 'free') : plans;
|
||||
const plansToShow = plans;
|
||||
|
||||
const formatDate = (dateStr: string | null | undefined) => {
|
||||
if (!dateStr) return '—';
|
||||
try {
|
||||
const date = new Date(dateStr);
|
||||
const locale = typeof window !== 'undefined' ? window.navigator.language : 'fr-FR';
|
||||
const locale = language === 'fa' ? 'fa-IR' : language === 'zh' ? 'zh-CN' : language;
|
||||
return new Intl.DateTimeFormat(locale, {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
timeZone: 'UTC',
|
||||
...(language === 'fa' ? { calendar: 'persian' as const } : {}),
|
||||
}).format(date);
|
||||
} catch (e) {
|
||||
return dateStr;
|
||||
@@ -476,19 +506,16 @@ export function BillingPlans() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isPaid && (
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handlePortal}
|
||||
disabled={portalLoading}
|
||||
className="flex items-center gap-2 px-5 py-2.5 bg-ink text-white dark:bg-white dark:text-black rounded-xl text-xs font-semibold hover:opacity-90 disabled:opacity-60 transition-all shadow-md shadow-black/5"
|
||||
onClick={scrollToPlans}
|
||||
className="flex items-center gap-2 px-5 py-2.5 bg-brand-accent text-white rounded-xl text-xs font-semibold hover:opacity-90 transition-all shadow-md shadow-brand-accent/20"
|
||||
>
|
||||
{portalLoading ? <Loader2 className="h-4 w-4 animate-spin" /> : <ExternalLink className="h-4 w-4" />}
|
||||
{t('billing.manageBilling') || 'Gérer la facturation'}
|
||||
{t('billing.changeOffer')}
|
||||
</button>
|
||||
|
||||
{status?.hasStripeSubscription && !status?.cancelAtPeriodEnd && (
|
||||
{isPaid && !status?.cancelAtPeriodEnd && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCancelSubscription}
|
||||
@@ -496,10 +523,27 @@ export function BillingPlans() {
|
||||
className="flex items-center gap-2 px-5 py-2.5 border border-rose-200 text-rose-600 dark:border-rose-800/40 dark:text-rose-400 hover:bg-rose-50/50 dark:hover:bg-rose-950/15 rounded-xl text-xs font-semibold transition-all"
|
||||
>
|
||||
{cancelLoading ? <Loader2 className="h-4 w-4 animate-spin" /> : null}
|
||||
{t('billing.cancelSubscription') || "Résilier l'abonnement"}
|
||||
{t('billing.cancelSubscription')}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{isPaid && status?.hasStripeSubscription && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handlePortal}
|
||||
disabled={portalLoading}
|
||||
className="flex items-center gap-2 px-5 py-2.5 border border-border text-ink rounded-xl text-xs font-semibold hover:bg-paper/60 dark:hover:bg-white/5 disabled:opacity-60 transition-all"
|
||||
>
|
||||
{portalLoading ? <Loader2 className="h-4 w-4 animate-spin" /> : <ExternalLink className="h-4 w-4" />}
|
||||
{t('billing.manageBilling')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isPaid && status?.cancelAtPeriodEnd && (
|
||||
<p className="text-xs text-amber-700 dark:text-amber-300 bg-amber-500/10 border border-amber-500/20 rounded-xl px-3 py-2">
|
||||
{t('billing.cancellingNotice', { date: formatDate(status.currentPeriodEnd) })}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -600,7 +644,7 @@ export function BillingPlans() {
|
||||
type="button"
|
||||
onClick={() => handleBuyPack(pack.id)}
|
||||
disabled={!pack.configured || packLoading !== null}
|
||||
className="mt-auto w-full py-3 rounded-2xl bg-ink text-white dark:bg-white dark:text-black text-[10px] font-bold uppercase tracking-[0.15em] hover:opacity-90 disabled:opacity-40 transition-all"
|
||||
className="mt-auto w-full py-3 rounded-2xl bg-brand-accent text-white text-[13px] font-semibold uppercase tracking-wider hover:opacity-90 disabled:opacity-40 transition-all"
|
||||
>
|
||||
{packLoading === pack.id ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin mx-auto" />
|
||||
@@ -661,8 +705,8 @@ export function BillingPlans() {
|
||||
const pct = totalUsed > 0 && used > 0 ? (used / totalUsed) * 100 : 0
|
||||
const barFillColor =
|
||||
pct >= 40
|
||||
? 'bg-gradient-to-r from-violet-400 to-purple-400'
|
||||
: 'bg-gradient-to-r from-violet-300/80 to-purple-300/80'
|
||||
? 'bg-brand-accent'
|
||||
: 'bg-brand-accent/60'
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
@@ -693,8 +737,7 @@ export function BillingPlans() {
|
||||
{isPaid && <BillingHistory />}
|
||||
|
||||
{/* Interval Toggle & Plan Cards */}
|
||||
{!isPaid && (
|
||||
<div className="space-y-8 pt-6 border-t border-border/40">
|
||||
<div ref={plansSectionRef} className="space-y-8 pt-6 border-t border-border/40">
|
||||
<div className="text-center space-y-2">
|
||||
<h3 className="text-xs font-bold uppercase tracking-[0.2em] text-concrete">
|
||||
{t('billing.upgradePlan') || 'Changer de plan'}
|
||||
@@ -708,7 +751,7 @@ export function BillingPlans() {
|
||||
onClick={() => setInterval('month')}
|
||||
className={cn(
|
||||
'px-4 py-1.5 text-xs font-medium rounded-full transition-all',
|
||||
interval === 'month' ? 'bg-ink text-paper' : 'text-concrete hover:text-ink'
|
||||
interval === 'month' ? 'bg-brand-accent text-white' : 'text-concrete hover:text-ink'
|
||||
)}
|
||||
>
|
||||
{t('billing.monthly')}
|
||||
@@ -718,7 +761,7 @@ export function BillingPlans() {
|
||||
onClick={() => setInterval('year')}
|
||||
className={cn(
|
||||
'px-4 py-1.5 text-xs font-medium rounded-full transition-all',
|
||||
interval === 'year' ? 'bg-ink text-paper' : 'text-concrete hover:text-ink'
|
||||
interval === 'year' ? 'bg-brand-accent text-white' : 'text-concrete hover:text-ink'
|
||||
)}
|
||||
>
|
||||
{t('billing.annual')}
|
||||
@@ -773,7 +816,12 @@ export function BillingPlans() {
|
||||
|
||||
<button
|
||||
onClick={plan.onClick}
|
||||
disabled={plan.current || checkoutLoading !== null}
|
||||
disabled={
|
||||
plan.current
|
||||
|| checkoutLoading !== null
|
||||
|| cancelLoading
|
||||
|| (plan.id === 'free' && !!status?.cancelAtPeriodEnd)
|
||||
}
|
||||
className={cn('w-full py-4 rounded-2xl text-[10px] font-bold uppercase tracking-[0.2em] transition-all duration-300', plan.buttonClass)}
|
||||
>
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
@@ -785,7 +833,6 @@ export function BillingPlans() {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Footer Info */}
|
||||
<div className="bg-slate-50 dark:bg-black/20 rounded-[32px] p-8 border border-border/40 flex flex-col md:flex-row items-center justify-between gap-8">
|
||||
|
||||
31
memento-note/components/settings/settings-document-title.tsx
Normal file
31
memento-note/components/settings/settings-document-title.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect } from 'react'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { useLanguage } from '@/lib/i18n'
|
||||
|
||||
const TITLE_KEYS: Record<string, string> = {
|
||||
'/settings/general': 'generalSettings.title',
|
||||
'/settings/ai': 'aiSettings.title',
|
||||
'/settings/billing': 'billing.title',
|
||||
'/settings/appearance': 'appearance.title',
|
||||
'/settings/profile': 'profile.title',
|
||||
'/settings/data': 'dataManagement.title',
|
||||
'/settings/published': 'settings.publishedTitle',
|
||||
'/settings/integrations': 'integrations.title',
|
||||
'/settings/mcp': 'mcpSettings.title',
|
||||
'/settings/about': 'about.title',
|
||||
}
|
||||
|
||||
export function SettingsDocumentTitle() {
|
||||
const pathname = usePathname()
|
||||
const { t } = useLanguage()
|
||||
|
||||
useEffect(() => {
|
||||
const key = Object.keys(TITLE_KEYS).find((href) => pathname === href || pathname.startsWith(`${href}/`))
|
||||
const section = key ? t(TITLE_KEYS[key]) : t('settings.title')
|
||||
document.title = `${section} — Memento`
|
||||
}, [pathname, t])
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -28,7 +28,7 @@ export function SettingsHelpBox({ title, steps, defaultOpen = false, className }
|
||||
className="w-full flex items-center gap-2 px-4 py-3 text-left hover:bg-border/10 transition-colors"
|
||||
>
|
||||
<HelpCircle size={14} className="text-brand-accent shrink-0" />
|
||||
<span className="text-[12px] font-semibold text-ink flex-1">{title}</span>
|
||||
<span className="text-sm font-semibold text-ink flex-1">{title}</span>
|
||||
{open ? (
|
||||
<ChevronUp size={13} className="text-concrete shrink-0" />
|
||||
) : (
|
||||
@@ -40,10 +40,10 @@ export function SettingsHelpBox({ title, steps, defaultOpen = false, className }
|
||||
<ol className="px-4 pb-4 space-y-2.5 border-t border-border/30 pt-3">
|
||||
{steps.map((step, i) => (
|
||||
<li key={i} className="flex items-start gap-2.5">
|
||||
<span className="w-5 h-5 rounded-full bg-brand-accent/10 text-brand-accent text-[10px] font-bold flex items-center justify-center shrink-0 mt-0.5">
|
||||
<span className="w-5 h-5 rounded-full bg-brand-accent/10 text-brand-accent text-xs font-semibold flex items-center justify-center shrink-0 mt-0.5">
|
||||
{step.icon ?? i + 1}
|
||||
</span>
|
||||
<span className="text-[12px] text-concrete leading-relaxed">
|
||||
<span className="text-sm text-concrete leading-relaxed">
|
||||
{step.text}
|
||||
{step.link && (
|
||||
<>
|
||||
|
||||
@@ -36,6 +36,13 @@ import {
|
||||
Folder,
|
||||
FolderOpen,
|
||||
LayoutGrid,
|
||||
Palette,
|
||||
CreditCard,
|
||||
Database,
|
||||
Globe,
|
||||
Plug,
|
||||
Key,
|
||||
Info,
|
||||
} from 'lucide-react'
|
||||
import { useSearchModal } from '@/context/search-modal-context'
|
||||
import { useLanguage } from '@/lib/i18n'
|
||||
@@ -67,7 +74,7 @@ import { performSignOut } from '@/lib/auth-client'
|
||||
import { isDashboardHomeRoute } from '@/lib/dashboard/home-route'
|
||||
import { useBrainstormSessions, useDeleteBrainstorm } from '@/hooks/use-brainstorm'
|
||||
|
||||
type NavigationView = 'dashboard' | 'notebooks' | 'agents' | 'reminders' | 'brainstorms' | 'revision' | 'insights'
|
||||
type NavigationView = 'dashboard' | 'notebooks' | 'agents' | 'reminders' | 'brainstorms' | 'revision' | 'insights' | 'settings'
|
||||
type SortOrder = 'newest' | 'oldest' | 'alpha' | 'manual'
|
||||
|
||||
const NOTEBOOKS_PANEL_HEIGHT_KEY = 'memento-sidebar-notebooks-height'
|
||||
@@ -859,6 +866,7 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
|
||||
}, [currentNotebookId, notebooks])
|
||||
|
||||
const isDashboardRoute = isDashboardHomeRoute(pathname, searchParams)
|
||||
const panelView: NavigationView = pathname.startsWith('/settings') ? 'settings' : activeView
|
||||
|
||||
const isInboxActive =
|
||||
pathname === '/home' &&
|
||||
@@ -873,6 +881,7 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
|
||||
else if (pathname.startsWith('/agents') || pathname.startsWith('/lab')) setActiveView('agents')
|
||||
else if (pathname === '/insights') setActiveView('insights')
|
||||
else if (pathname.startsWith('/revision')) setActiveView('revision')
|
||||
else if (pathname.startsWith('/settings')) setActiveView('settings')
|
||||
else if (searchParams.get('reminders') === '1' && pathname === '/home') setActiveView('reminders')
|
||||
else if (isDashboardRoute) setActiveView('dashboard')
|
||||
else if (pathname === '/home' || pathname.startsWith('/notes')) setActiveView('notebooks')
|
||||
@@ -1386,8 +1395,8 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
|
||||
)}
|
||||
aria-hidden={isImmersiveRoute && userCollapsed ? true : undefined}
|
||||
>
|
||||
{/* ── Column 1 : Rail d'icônes (54px) — inspiré du prototype ── */}
|
||||
<div className="w-[54px] border-e border-border/40 bg-[#FAF9F5] dark:bg-[#0E0E0E] flex flex-col items-center justify-between py-5 shrink-0 select-none overflow-hidden">
|
||||
{/* ── Column 1 : Rail icône + libellé — destinations lisibles sans survol ── */}
|
||||
<div className="w-[72px] border-e border-border/40 bg-[#FAF9F5] dark:bg-[#0E0E0E] flex flex-col items-center justify-between py-3 shrink-0 select-none overflow-y-auto overflow-x-hidden custom-scrollbar">
|
||||
|
||||
{/* Top : Logo + navigation */}
|
||||
<div className="flex flex-col items-center gap-[18px] w-full">
|
||||
@@ -1425,12 +1434,12 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
|
||||
</DropdownMenu>
|
||||
|
||||
{/* Boutons de navigation principaux */}
|
||||
<div className="flex flex-col gap-2 w-full px-1.5">
|
||||
<div className="flex flex-col gap-1 w-full px-1">
|
||||
{([
|
||||
{
|
||||
id: 'dashboard',
|
||||
icon: LayoutGrid,
|
||||
label: t('nav.dashboard') || 'Dashboard',
|
||||
label: t('nav.home'),
|
||||
onClick: () => {
|
||||
setActiveView('dashboard')
|
||||
router.replace('/home')
|
||||
@@ -1470,7 +1479,7 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
|
||||
{
|
||||
id: 'agents',
|
||||
icon: Bot,
|
||||
label: t('agents.intelligenceOS') || 'Intelligence IA',
|
||||
label: t('nav.agents'),
|
||||
onClick: () => {
|
||||
setActiveView('agents')
|
||||
router.push('/agents')
|
||||
@@ -1492,20 +1501,17 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
|
||||
aria-current={item.isActive ? 'page' : undefined}
|
||||
onClick={item.onClick}
|
||||
className={cn(
|
||||
'w-9 h-9 rounded-lg flex items-center justify-center transition-all relative group',
|
||||
'w-full rounded-lg flex flex-col items-center justify-center gap-0.5 py-1.5 px-0.5 transition-all relative',
|
||||
item.isActive
|
||||
? 'bg-brand-accent/10 text-brand-accent border border-brand-accent/25'
|
||||
: 'text-concrete hover:text-ink dark:hover:text-white hover:bg-black/[0.04] dark:hover:bg-white/[0.04]'
|
||||
: 'text-ink/70 hover:text-ink dark:text-white/70 dark:hover:text-white hover:bg-black/[0.04] dark:hover:bg-white/[0.04]'
|
||||
)}
|
||||
>
|
||||
{item.isActive && (
|
||||
<div className="absolute left-0 top-1/2 -translate-y-1/2 w-1 h-4 bg-brand-accent rounded-r-full" />
|
||||
<div className="absolute start-0 top-1/2 -translate-y-1/2 w-1 h-4 bg-brand-accent rounded-e-full" />
|
||||
)}
|
||||
<item.icon size={16} />
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="absolute left-[50px] top-1/2 -translate-y-1/2 bg-ink dark:bg-white dark:text-ink text-paper text-[9px] font-bold py-1 px-2 rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap z-50 pointer-events-none shadow-md uppercase tracking-wider"
|
||||
>
|
||||
<item.icon size={16} aria-hidden />
|
||||
<span className="text-[11px] leading-tight font-semibold text-center max-w-full line-clamp-2">
|
||||
{item.label}
|
||||
</span>
|
||||
</button>
|
||||
@@ -1519,6 +1525,7 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
|
||||
|
||||
<Link
|
||||
href="/trash"
|
||||
aria-label={t('sidebar.trash')}
|
||||
className={cn(
|
||||
'w-9 h-9 rounded-lg flex items-center justify-center transition-all relative group',
|
||||
pathname === '/trash'
|
||||
@@ -1538,6 +1545,7 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
|
||||
|
||||
<Link
|
||||
href="/archive"
|
||||
aria-label={t('sidebar.archive')}
|
||||
className={cn(
|
||||
'w-9 h-9 rounded-lg flex items-center justify-center transition-all relative group',
|
||||
pathname === '/archive'
|
||||
@@ -1554,6 +1562,7 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
|
||||
|
||||
<Link
|
||||
href="/home?shared=1&forceList=1"
|
||||
aria-label={t('sidebar.sharedWithMe')}
|
||||
className={cn(
|
||||
'w-9 h-9 rounded-lg flex items-center justify-center transition-all relative group',
|
||||
searchParams.get('shared') === '1' && pathname === '/home'
|
||||
@@ -1570,7 +1579,8 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
|
||||
<button
|
||||
onClick={openSearch}
|
||||
className="w-9 h-9 rounded-lg flex items-center justify-center text-concrete hover:text-ink dark:hover:text-white hover:bg-black/[0.04] dark:hover:bg-white/[0.04] transition-all relative group"
|
||||
title="Ctrl+K"
|
||||
aria-label={t('sidebar.searchShortcut')}
|
||||
title={t('sidebar.searchShortcut')}
|
||||
>
|
||||
<Search size={15} />
|
||||
<span className="absolute left-[50px] top-1/2 -translate-y-1/2 bg-ink dark:bg-white dark:text-ink text-paper text-[9px] font-bold py-1 px-2 rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap z-50 pointer-events-none shadow-md uppercase tracking-wider">
|
||||
@@ -1580,6 +1590,7 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
|
||||
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
aria-label={isDark ? t('sidebar.lightMode') : t('sidebar.darkMode')}
|
||||
className="w-9 h-9 rounded-lg flex items-center justify-center text-concrete hover:text-ink dark:hover:text-white hover:bg-black/[0.04] dark:hover:bg-white/[0.04] transition-all relative group"
|
||||
>
|
||||
{isDark ? <Sun size={15} /> : <Moon size={15} />}
|
||||
@@ -1590,6 +1601,8 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
|
||||
|
||||
<Link
|
||||
href="/settings"
|
||||
aria-label={t('nav.settings')}
|
||||
onClick={() => setActiveView('settings')}
|
||||
className={cn(
|
||||
'w-9 h-9 rounded-lg flex items-center justify-center transition-all relative group',
|
||||
pathname.startsWith('/settings')
|
||||
@@ -1606,6 +1619,7 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
|
||||
|
||||
<button
|
||||
onClick={() => performSignOut('/login')}
|
||||
aria-label={t('sidebar.signOut')}
|
||||
className="w-9 h-9 rounded-lg flex items-center justify-center text-concrete hover:text-red-500 hover:bg-rose-500/5 transition-all relative group"
|
||||
>
|
||||
<LogOut size={14} />
|
||||
@@ -1623,14 +1637,14 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
|
||||
<div
|
||||
className={cn(
|
||||
'flex-1 flex flex-col min-h-0 -mx-0 pb-4',
|
||||
activeView === 'notebooks'
|
||||
panelView === 'notebooks'
|
||||
? 'overflow-hidden'
|
||||
: 'overflow-y-auto custom-scrollbar space-y-6',
|
||||
)}
|
||||
>
|
||||
|
||||
<AnimatePresence mode="wait">
|
||||
{activeView === 'dashboard' ? (
|
||||
{panelView === 'dashboard' ? (
|
||||
<motion.div
|
||||
key="dashboard"
|
||||
initial={{ opacity: 0, x: isRtl ? 10 : -10 }}
|
||||
@@ -1649,45 +1663,6 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
|
||||
{t('sidebar.dashboardPanelBody')}
|
||||
</p>
|
||||
<div className="space-y-1.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleInboxClick}
|
||||
className="w-full flex items-center justify-between gap-2 px-3 py-2.5 rounded-xl border border-border/40 bg-white/60 dark:bg-zinc-800/40 hover:border-brand-accent/30 hover:bg-brand-accent/5 transition-all text-[12px] font-medium text-foreground"
|
||||
>
|
||||
<span className="flex items-center gap-2 min-w-0">
|
||||
<Inbox size={14} className="text-brand-accent shrink-0" />
|
||||
{t('homeDashboard.inbox')}
|
||||
</span>
|
||||
{inboxCount > 0 && (
|
||||
<span className="text-[10px] font-mono font-bold text-brand-accent bg-brand-accent/10 px-1.5 py-0.5 rounded shrink-0">
|
||||
{inboxCount}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => router.push('/revision')}
|
||||
className="w-full flex items-center gap-2 px-3 py-2.5 rounded-xl border border-border/40 bg-white/60 dark:bg-zinc-800/40 hover:border-brand-accent/30 hover:bg-brand-accent/5 transition-all text-[12px] font-medium text-foreground"
|
||||
>
|
||||
<GraduationCap size={14} className="text-brand-accent shrink-0" />
|
||||
{t('homeDashboard.review')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleRemindersClick}
|
||||
className="w-full flex items-center gap-2 px-3 py-2.5 rounded-xl border border-border/40 bg-white/60 dark:bg-zinc-800/40 hover:border-brand-accent/30 hover:bg-brand-accent/5 transition-all text-[12px] font-medium text-foreground"
|
||||
>
|
||||
<Bell size={14} className="text-brand-accent shrink-0" />
|
||||
{t('homeDashboard.reminders')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => router.push('/insights')}
|
||||
className="w-full flex items-center gap-2 px-3 py-2.5 rounded-xl border border-border/40 bg-white/60 dark:bg-zinc-800/40 hover:border-brand-accent/30 hover:bg-brand-accent/5 transition-all text-[12px] font-medium text-foreground"
|
||||
>
|
||||
<Sparkles size={14} className="text-brand-accent shrink-0" />
|
||||
{t('homeDashboard.themes')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
@@ -1701,7 +1676,54 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
) : activeView === 'notebooks' ? (
|
||||
) : panelView === 'settings' ? (
|
||||
<motion.div
|
||||
key="settings"
|
||||
initial={{ opacity: 0, x: isRtl ? 10 : -10 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: isRtl ? -10 : 10 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="px-4 pt-4"
|
||||
>
|
||||
<div className="flex items-center gap-1.5 mb-6">
|
||||
<Settings size={14} className="text-brand-accent" />
|
||||
<h3 className="text-xs font-black tracking-widest uppercase text-ink dark:text-dark-ink">
|
||||
{t('settings.title')}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{[
|
||||
{ id: 'general', href: '/settings/general', label: t('generalSettings.title'), icon: Settings },
|
||||
{ id: 'ai', href: '/settings/ai', label: t('aiSettings.title'), icon: Sparkles },
|
||||
{ id: 'billing', href: '/settings/billing', label: t('billing.title'), icon: CreditCard },
|
||||
{ id: 'appearance', href: '/settings/appearance', label: t('appearance.title'), icon: Palette },
|
||||
{ id: 'profile', href: '/settings/profile', label: t('profile.title'), icon: User },
|
||||
{ id: 'data', href: '/settings/data', label: t('dataManagement.title'), icon: Database },
|
||||
{ id: 'published', href: '/settings/published', label: t('settings.publishedTitle') || 'Mes pages', icon: Globe },
|
||||
{ id: 'integrations', href: '/settings/integrations', label: t('integrations.title') || 'Intégrations', icon: Plug },
|
||||
{ id: 'mcp', href: '/settings/mcp', label: t('mcpSettings.title'), icon: Key },
|
||||
{ id: 'about', href: '/settings/about', label: t('about.title'), icon: Info },
|
||||
].map((tab) => {
|
||||
const isActive = pathname === tab.href || pathname.startsWith(`${tab.href}/`)
|
||||
return (
|
||||
<Link
|
||||
key={tab.id}
|
||||
href={tab.href}
|
||||
className={cn(
|
||||
'w-full text-start px-3 py-2 text-[11px] transition-all rounded-lg flex items-center gap-2.5',
|
||||
isActive
|
||||
? 'text-ink bg-brand-accent/10'
|
||||
: 'text-muted-foreground hover:text-ink hover:bg-black/5 dark:hover:bg-white/5',
|
||||
)}
|
||||
>
|
||||
<tab.icon size={12} className="text-concrete shrink-0" />
|
||||
<span className="font-semibold">{tab.label}</span>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</motion.div>
|
||||
) : panelView === 'notebooks' ? (
|
||||
<motion.div
|
||||
key="notebooks"
|
||||
ref={notebooksContainerRef}
|
||||
@@ -1717,7 +1739,7 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
|
||||
<div className="flex items-center gap-1.5">
|
||||
<BookMarked size={14} className="text-brand-accent" />
|
||||
<h3 className="text-xs font-black tracking-widest uppercase text-ink dark:text-dark-ink">
|
||||
{t('sidebar.documents')}
|
||||
{t('nav.notebooks')}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5">
|
||||
@@ -1889,7 +1911,7 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
) : activeView === 'insights' ? (
|
||||
) : panelView === 'insights' ? (
|
||||
<motion.div
|
||||
key="insights"
|
||||
initial={{ opacity: 0, x: isRtl ? 10 : -10 }}
|
||||
@@ -1911,12 +1933,20 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
|
||||
type="button"
|
||||
onClick={() => router.push('/home')}
|
||||
className="w-full flex items-center gap-2 px-3 py-2.5 rounded-xl border border-border/40 bg-white/60 dark:bg-zinc-800/40 hover:border-brand-accent/30 hover:bg-brand-accent/5 transition-all text-[12px] font-medium text-foreground"
|
||||
>
|
||||
<Home size={14} className="text-brand-accent shrink-0" />
|
||||
{t('sidebar.backToHome')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => router.push('/home?forceList=1')}
|
||||
className="w-full flex items-center gap-2 px-3 py-2.5 rounded-xl border border-border/40 bg-white/60 dark:bg-zinc-800/40 hover:border-brand-accent/30 hover:bg-brand-accent/5 transition-all text-[12px] font-medium text-foreground"
|
||||
>
|
||||
<BookOpen size={14} className="text-brand-accent shrink-0" />
|
||||
{t('sidebar.backToNotebooks')}
|
||||
</button>
|
||||
</motion.div>
|
||||
) : activeView === 'revision' ? (
|
||||
) : panelView === 'revision' ? (
|
||||
<motion.div
|
||||
key="revision"
|
||||
initial={{ opacity: 0, x: isRtl ? 10 : -10 }}
|
||||
@@ -1938,12 +1968,20 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
|
||||
type="button"
|
||||
onClick={() => router.push('/home')}
|
||||
className="w-full flex items-center gap-2 px-3 py-2.5 rounded-xl border border-border/40 bg-white/60 dark:bg-zinc-800/40 hover:border-brand-accent/30 hover:bg-brand-accent/5 transition-all text-[12px] font-medium text-foreground"
|
||||
>
|
||||
<Home size={14} className="text-brand-accent shrink-0" />
|
||||
{t('sidebar.backToHome')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => router.push('/home?forceList=1')}
|
||||
className="w-full flex items-center gap-2 px-3 py-2.5 rounded-xl border border-border/40 bg-white/60 dark:bg-zinc-800/40 hover:border-brand-accent/30 hover:bg-brand-accent/5 transition-all text-[12px] font-medium text-foreground"
|
||||
>
|
||||
<BookOpen size={14} className="text-brand-accent shrink-0" />
|
||||
{t('sidebar.backToNotebooks')}
|
||||
</button>
|
||||
</motion.div>
|
||||
) : activeView === 'reminders' ? (
|
||||
) : panelView === 'reminders' ? (
|
||||
<motion.div
|
||||
key="reminders"
|
||||
initial={{ opacity: 0, x: isRtl ? 10 : -10 }}
|
||||
@@ -1962,7 +2000,7 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
|
||||
<SidebarReminders onOpenNote={handleReminderNoteClick} />
|
||||
</div>
|
||||
</motion.div>
|
||||
) : activeView === 'agents' ? (
|
||||
) : panelView === 'agents' ? (
|
||||
<motion.div
|
||||
key="agents"
|
||||
initial={{ opacity: 0, x: isRtl ? -10 : 10 }}
|
||||
@@ -1971,7 +2009,7 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<p className="text-[10px] font-bold text-muted-foreground tracking-widest uppercase mb-4 px-4">
|
||||
{t('agents.intelligenceOS')}
|
||||
{t('nav.agents')}
|
||||
</p>
|
||||
<div className="space-y-1">
|
||||
{[
|
||||
|
||||
@@ -68,10 +68,11 @@ export function ThemeInitializer({ theme, fontSize, fontFamily, accentColor }: T
|
||||
}
|
||||
|
||||
const localAccent = localStorage.getItem('accent-color')
|
||||
const effectiveAccent = localAccent || accentColor || '#A47148'
|
||||
const serverAccent = accentColor || null
|
||||
const effectiveAccent = serverAccent || localAccent || '#A47148'
|
||||
root.style.setProperty('--color-brand-accent', effectiveAccent)
|
||||
if (!localAccent && accentColor) {
|
||||
localStorage.setItem('accent-color', accentColor)
|
||||
if (serverAccent && localAccent !== serverAccent) {
|
||||
localStorage.setItem('accent-color', serverAccent)
|
||||
}
|
||||
}, [theme, fontSize, fontFamily, accentColor])
|
||||
|
||||
|
||||
@@ -165,7 +165,7 @@ export function UsageMeter({ className }: UsageMeterProps) {
|
||||
</div>
|
||||
<span
|
||||
className={cn(
|
||||
'text-[10px] font-medium tabular-nums shrink-0',
|
||||
'text-[10px] font-medium tabular-nums shrink-0 max-w-[42%] truncate',
|
||||
totalPct >= 90
|
||||
? 'text-rose-500'
|
||||
: totalPct >= 70
|
||||
@@ -174,7 +174,7 @@ export function UsageMeter({ className }: UsageMeterProps) {
|
||||
)}
|
||||
title={t('usageMeter.creditsRemaining')}
|
||||
>
|
||||
{remaining}
|
||||
{t('usageMeter.remaining', { count: remaining })}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
@@ -185,12 +185,6 @@ export function UsageMeter({ className }: UsageMeterProps) {
|
||||
</span>
|
||||
)}
|
||||
|
||||
{isProPlus && !unlimited && (
|
||||
<span className="text-[9px] font-bold text-brand-accent uppercase tracking-widest ml-auto">
|
||||
{data.tier}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<ChevronDown
|
||||
size={12}
|
||||
className={cn(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { useDebounce } from './use-debounce'
|
||||
import { useAiConsent } from '@/components/legal/ai-consent-provider'
|
||||
import { countPlainWords, MIN_WORDS_FOR_AUTO_TITLE } from '@/lib/text/plain-text'
|
||||
|
||||
export interface TitleSuggestion {
|
||||
title: string
|
||||
@@ -29,10 +30,8 @@ export function useTitleSuggestions({ content, enabled = true }: UseTitleSuggest
|
||||
return
|
||||
}
|
||||
|
||||
const wordCount = debouncedContent.split(/\s+/).length
|
||||
|
||||
// Need at least 10 words
|
||||
if (wordCount < 10) {
|
||||
// Compter le texte visible, pas les balises de l’éditeur
|
||||
if (countPlainWords(debouncedContent) < MIN_WORDS_FOR_AUTO_TITLE) {
|
||||
setSuggestions([])
|
||||
return
|
||||
}
|
||||
@@ -65,8 +64,11 @@ export function useTitleSuggestions({ content, enabled = true }: UseTitleSuggest
|
||||
if (controller.signal.aborted) return
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new Error(errorData.error || 'Error generating title suggestions')
|
||||
setSuggestions([])
|
||||
if (response.status !== 400) {
|
||||
setError('Failed to generate title suggestions')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
@@ -74,6 +76,7 @@ export function useTitleSuggestions({ content, enabled = true }: UseTitleSuggest
|
||||
} catch (err: any) {
|
||||
if (err.name === 'AbortError') return
|
||||
console.error('Title suggestions error:', err)
|
||||
setSuggestions([])
|
||||
setError('Failed to generate title suggestions')
|
||||
} finally {
|
||||
if (!controller.signal.aborted) {
|
||||
|
||||
23
memento-note/lib/ai/byok-catalog.ts
Normal file
23
memento-note/lib/ai/byok-catalog.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { getAllowedByokProviders } from '@/lib/byok'
|
||||
import { getLivePublicModels } from '@/lib/ai/live-public-catalog'
|
||||
import { providerDisplayName } from '@/lib/ai/provider-labels'
|
||||
|
||||
const ENDPOINT_ONLY = new Set(['custom', 'custom_openai', 'custom_anthropic', 'anthropic_custom'])
|
||||
|
||||
export type PublicByokProvider = {
|
||||
id: string
|
||||
name: string
|
||||
models: string[]
|
||||
}
|
||||
|
||||
/** Catalogue public : fournisseurs BYOK Business, hors adresses perso. */
|
||||
export async function getPublicByokCatalog(): Promise<PublicByokProvider[]> {
|
||||
const liveModels = await getLivePublicModels()
|
||||
return getAllowedByokProviders('BUSINESS')
|
||||
.filter((id) => !ENDPOINT_ONLY.has(id))
|
||||
.map((id) => ({
|
||||
id,
|
||||
name: providerDisplayName(id),
|
||||
models: liveModels[id] ?? [],
|
||||
}))
|
||||
}
|
||||
205
memento-note/lib/ai/live-public-catalog.ts
Normal file
205
memento-note/lib/ai/live-public-catalog.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
import { PROVIDER_MODEL_SUGGESTIONS } from '@/lib/ai/models-list'
|
||||
import { redis } from '@/lib/redis'
|
||||
|
||||
const OPENROUTER_MODELS_URL = 'https://openrouter.ai/api/v1/models'
|
||||
const REDIS_KEY = 'memento:public-byok-models:v2'
|
||||
const REDIS_TTL_SEC = 12 * 60 * 60
|
||||
const MEMORY_TTL_MS = 30 * 60 * 1000
|
||||
const DISPLAY_PER_PROVIDER = 3
|
||||
|
||||
type OpenRouterModel = {
|
||||
id?: string
|
||||
created?: number
|
||||
architecture?: {
|
||||
modality?: string
|
||||
output_modalities?: string[]
|
||||
}
|
||||
}
|
||||
|
||||
const VENDOR_TO_PROVIDER: Record<string, string> = {
|
||||
openai: 'openai',
|
||||
anthropic: 'anthropic',
|
||||
google: 'google',
|
||||
deepseek: 'deepseek',
|
||||
minimax: 'minimax',
|
||||
mistralai: 'mistral',
|
||||
'z-ai': 'glm',
|
||||
}
|
||||
|
||||
let memoryCache: { at: number; models: Record<string, string[]> } | null = null
|
||||
|
||||
function slugOf(id: string): string {
|
||||
return (id.includes('/') ? id.split('/')[1] : id).replace(/:.*$/, '')
|
||||
}
|
||||
|
||||
function isNoisyId(id: string): boolean {
|
||||
const slug = slugOf(id)
|
||||
if (/:(batch|free|nitro)/i.test(id)) return true
|
||||
if (/embed/i.test(slug)) return true
|
||||
if (/-fast$/i.test(slug)) return true
|
||||
if (/-vision|-image|-audio|-preview|-exp|codex|oss|safeguard/i.test(slug)) return true
|
||||
if (/gemma|lyria|voxtral|ministral|devstral|codestral/i.test(slug)) return true
|
||||
if (/chat-latest/i.test(slug)) return true
|
||||
if (/-\d{4,8}$/.test(slug)) return true
|
||||
return false
|
||||
}
|
||||
|
||||
function cleanliness(id: string): number {
|
||||
const slug = slugOf(id)
|
||||
let score = 0
|
||||
if (/-pro$/i.test(slug)) score += 20
|
||||
if (/-lite$/i.test(slug)) score += 5
|
||||
return score
|
||||
}
|
||||
|
||||
function isChatModel(model: OpenRouterModel): boolean {
|
||||
const id = model.id ?? ''
|
||||
if (!id || isNoisyId(id)) return false
|
||||
const outputs = model.architecture?.output_modalities
|
||||
if (outputs && !outputs.includes('text')) return false
|
||||
return true
|
||||
}
|
||||
|
||||
function familyKey(id: string): string {
|
||||
return slugOf(id)
|
||||
.replace(/-pro$/i, '')
|
||||
.replace(/-latest$/i, '')
|
||||
}
|
||||
|
||||
function nativeName(openRouterId: string): string {
|
||||
const slug = openRouterId.includes('/') ? openRouterId.split('/')[1] : openRouterId
|
||||
if (/^minimax-/i.test(slug)) {
|
||||
return slug.replace(/^minimax-/i, 'MiniMax-').replace(/-m(\d)/i, '-M$1')
|
||||
}
|
||||
return slug
|
||||
}
|
||||
|
||||
function pickLatestIds(models: OpenRouterModel[], keepPrefix: boolean): string[] {
|
||||
const byFamily = new Map<string, OpenRouterModel[]>()
|
||||
const order: string[] = []
|
||||
const sorted = [...models].sort((a, b) => (b.created ?? 0) - (a.created ?? 0))
|
||||
for (const model of sorted) {
|
||||
const id = model.id
|
||||
if (!id) continue
|
||||
const key = familyKey(id)
|
||||
if (!byFamily.has(key)) {
|
||||
byFamily.set(key, [])
|
||||
order.push(key)
|
||||
}
|
||||
byFamily.get(key)!.push(model)
|
||||
}
|
||||
|
||||
const picked: string[] = []
|
||||
for (const key of order) {
|
||||
const best = [...(byFamily.get(key) ?? [])].sort(
|
||||
(a, b) => cleanliness(a.id ?? '') - cleanliness(b.id ?? ''),
|
||||
)[0]
|
||||
const id = best?.id
|
||||
if (!id) continue
|
||||
picked.push(keepPrefix ? id.replace(/:.*$/, '') : nativeName(id))
|
||||
if (picked.length >= DISPLAY_PER_PROVIDER) break
|
||||
}
|
||||
return picked
|
||||
}
|
||||
|
||||
function buildFromOpenRouter(models: OpenRouterModel[]): Record<string, string[]> {
|
||||
const byVendor = new Map<string, OpenRouterModel[]>()
|
||||
for (const model of models) {
|
||||
if (!isChatModel(model) || !model.id?.includes('/')) continue
|
||||
const vendor = model.id.split('/')[0]
|
||||
const list = byVendor.get(vendor) ?? []
|
||||
list.push(model)
|
||||
byVendor.set(vendor, list)
|
||||
}
|
||||
|
||||
const result: Record<string, string[]> = {}
|
||||
for (const [vendor, provider] of Object.entries(VENDOR_TO_PROVIDER)) {
|
||||
result[provider] = pickLatestIds(byVendor.get(vendor) ?? [], false)
|
||||
}
|
||||
|
||||
const showcaseVendors = ['openai', 'anthropic', 'google']
|
||||
result.openrouter = showcaseVendors
|
||||
.map((vendor) => {
|
||||
const latest = pickLatestIds(byVendor.get(vendor) ?? [], true)[0]
|
||||
return latest
|
||||
})
|
||||
.filter(Boolean)
|
||||
|
||||
result.zai = [
|
||||
result.openai?.[0],
|
||||
result.anthropic?.[0],
|
||||
result.google?.[0],
|
||||
].filter(Boolean) as string[]
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
async function fetchOpenRouterModels(): Promise<OpenRouterModel[]> {
|
||||
const response = await fetch(OPENROUTER_MODELS_URL, {
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'HTTP-Referer': process.env.NEXTAUTH_URL || 'https://memento-note.com',
|
||||
'X-Title': 'Memento',
|
||||
},
|
||||
signal: AbortSignal.timeout(8000),
|
||||
cache: 'no-store',
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw new Error(`catalog ${response.status}`)
|
||||
}
|
||||
const data = (await response.json()) as { data?: OpenRouterModel[] }
|
||||
return Array.isArray(data.data) ? data.data : []
|
||||
}
|
||||
|
||||
function mergeWithFallback(live: Record<string, string[]>): Record<string, string[]> {
|
||||
const merged: Record<string, string[]> = { ...PROVIDER_MODEL_SUGGESTIONS }
|
||||
for (const [id, models] of Object.entries(live)) {
|
||||
if (models.length > 0) merged[id] = models
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
async function readRedis(): Promise<Record<string, string[]> | null> {
|
||||
try {
|
||||
const raw = await redis.get(REDIS_KEY)
|
||||
if (!raw) return null
|
||||
const parsed = JSON.parse(raw) as Record<string, string[]>
|
||||
return parsed && typeof parsed === 'object' ? parsed : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function writeRedis(models: Record<string, string[]>): Promise<void> {
|
||||
try {
|
||||
await redis.set(REDIS_KEY, JSON.stringify(models), 'EX', REDIS_TTL_SEC)
|
||||
} catch {
|
||||
/* le cache mémoire suffit */
|
||||
}
|
||||
}
|
||||
|
||||
/** Noms de modèles à jour, lus périodiquement — jamais une liste figée seule. */
|
||||
export async function getLivePublicModels(): Promise<Record<string, string[]>> {
|
||||
if (memoryCache && Date.now() - memoryCache.at < MEMORY_TTL_MS) {
|
||||
return memoryCache.models
|
||||
}
|
||||
|
||||
const cached = await readRedis()
|
||||
if (cached) {
|
||||
memoryCache = { at: Date.now(), models: cached }
|
||||
return cached
|
||||
}
|
||||
|
||||
try {
|
||||
const remote = await fetchOpenRouterModels()
|
||||
const merged = mergeWithFallback(buildFromOpenRouter(remote))
|
||||
memoryCache = { at: Date.now(), models: merged }
|
||||
await writeRedis(merged)
|
||||
return merged
|
||||
} catch (error) {
|
||||
console.warn('[live-public-catalog] lecture distante impossible, liste de secours', error)
|
||||
const fallback = { ...PROVIDER_MODEL_SUGGESTIONS }
|
||||
memoryCache = { at: Date.now(), models: fallback }
|
||||
return fallback
|
||||
}
|
||||
}
|
||||
@@ -13,14 +13,15 @@ const PROVIDER_URLS: Record<string, string> = {
|
||||
|
||||
// Fallback popular models when live fetching fails or for providers without /models endpoint (e.g. Anthropic, Google)
|
||||
export const PROVIDER_MODEL_SUGGESTIONS: Record<string, string[]> = {
|
||||
openai: ['gpt-4o-mini', 'gpt-4o', 'gpt-4-turbo', 'gpt-3.5-turbo'],
|
||||
anthropic: ['claude-3-5-sonnet-latest', 'claude-3-5-haiku-latest', 'claude-3-opus-latest'],
|
||||
google: ['gemini-1.5-flash', 'gemini-1.5-pro', 'gemini-2.0-flash-exp'],
|
||||
deepseek: ['deepseek-chat', 'deepseek-coder'],
|
||||
minimax: ['MiniMax-M2.7', 'MiniMax-M2.5', 'MiniMax-M2-her'],
|
||||
mistral: ['mistral-small-latest', 'mistral-medium-latest', 'mistral-large-latest'],
|
||||
glm: ['glm-4', 'glm-4-flash'],
|
||||
openrouter: ['openai/gpt-4o-mini', 'anthropic/claude-3.5-sonnet', 'deepseek/deepseek-chat'],
|
||||
openai: ['gpt-5.6-sol', 'gpt-5.6-terra', 'gpt-5.6-luna'],
|
||||
anthropic: ['claude-opus-5', 'claude-sonnet-5', 'claude-fable-5'],
|
||||
google: ['gemini-3.7-flash', 'gemini-3.6-flash', 'gemini-3.5-flash-lite'],
|
||||
deepseek: ['deepseek-v4-pro', 'deepseek-v4-flash', 'deepseek-chat'],
|
||||
minimax: ['MiniMax-M3', 'MiniMax-M2.7', 'MiniMax-M2.5'],
|
||||
mistral: ['mistral-medium-latest', 'mistral-small-latest', 'mistral-large-latest'],
|
||||
glm: ['glm-5.3', 'glm-5.3-flash', 'glm-5.2'],
|
||||
openrouter: ['openai/gpt-5.6-sol', 'anthropic/claude-opus-5', 'google/gemini-3.7-flash'],
|
||||
zai: ['gpt-5.6-sol', 'claude-sonnet-5', 'gemini-3.7-flash'],
|
||||
custom: [],
|
||||
};
|
||||
|
||||
|
||||
20
memento-note/lib/ai/provider-labels.ts
Normal file
20
memento-note/lib/ai/provider-labels.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/** Noms affichés des fournisseurs — une seule source pour réglages et page publique. */
|
||||
export const PROVIDER_LABELS: Record<string, string> = {
|
||||
openai: 'OpenAI',
|
||||
anthropic: 'Anthropic',
|
||||
minimax: 'MiniMax',
|
||||
google: 'Google AI',
|
||||
deepseek: 'DeepSeek',
|
||||
openrouter: 'OpenRouter',
|
||||
mistral: 'Mistral AI',
|
||||
glm: 'GLM (Zhipu)',
|
||||
zai: 'Zuki Journey',
|
||||
anthropic_custom: 'Anthropic (custom)',
|
||||
custom_openai: 'Compatible OpenAI',
|
||||
custom_anthropic: 'Compatible Anthropic',
|
||||
custom: 'Custom API',
|
||||
}
|
||||
|
||||
export function providerDisplayName(provider: string): string {
|
||||
return PROVIDER_LABELS[provider] ?? provider
|
||||
}
|
||||
56
memento-note/lib/billing/period.ts
Normal file
56
memento-note/lib/billing/period.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
/** Avance une date d’un mois, en gardant le jour du mois (ou le dernier jour si besoin). */
|
||||
export function addUtcMonths(date: Date, months: number): Date {
|
||||
const year = date.getUTCFullYear()
|
||||
const month = date.getUTCMonth() + months
|
||||
const day = date.getUTCDate()
|
||||
const lastDay = new Date(Date.UTC(year, month + 1, 0)).getUTCDate()
|
||||
return new Date(Date.UTC(
|
||||
year,
|
||||
month,
|
||||
Math.min(day, lastDay),
|
||||
date.getUTCHours(),
|
||||
date.getUTCMinutes(),
|
||||
date.getUTCSeconds(),
|
||||
date.getUTCMilliseconds(),
|
||||
))
|
||||
}
|
||||
|
||||
/**
|
||||
* Recalcule la période en cours à partir de la date d’origine.
|
||||
* Sans fiche de paiement, les dates étaient écrites une fois (souvent +1 an) et ne bougeaient plus.
|
||||
*/
|
||||
export function rollBillingPeriod(
|
||||
periodStart: Date,
|
||||
now: Date = new Date(),
|
||||
): { currentPeriodStart: Date; currentPeriodEnd: Date } {
|
||||
if (Number.isNaN(periodStart.getTime())) {
|
||||
const start = new Date(now)
|
||||
return { currentPeriodStart: start, currentPeriodEnd: addUtcMonths(start, 1) }
|
||||
}
|
||||
|
||||
let start = new Date(periodStart)
|
||||
let end = addUtcMonths(start, 1)
|
||||
|
||||
if (start.getTime() > now.getTime()) {
|
||||
return { currentPeriodStart: start, currentPeriodEnd: end }
|
||||
}
|
||||
|
||||
let guard = 0
|
||||
while (end.getTime() <= now.getTime() && guard < 240) {
|
||||
start = end
|
||||
end = addUtcMonths(start, 1)
|
||||
guard += 1
|
||||
}
|
||||
|
||||
return { currentPeriodStart: start, currentPeriodEnd: end }
|
||||
}
|
||||
|
||||
export function periodsDiffer(
|
||||
a: { currentPeriodStart: Date; currentPeriodEnd: Date },
|
||||
b: { currentPeriodStart: Date; currentPeriodEnd: Date },
|
||||
): boolean {
|
||||
return (
|
||||
a.currentPeriodStart.getTime() !== b.currentPeriodStart.getTime()
|
||||
|| a.currentPeriodEnd.getTime() !== b.currentPeriodEnd.getTime()
|
||||
)
|
||||
}
|
||||
@@ -167,9 +167,9 @@ export function normalizeDashboardLayout(raw: unknown): DashboardLayout {
|
||||
const data = raw as Partial<DashboardLayout> & { version?: number }
|
||||
if (!Array.isArray(data.widgets) || data.widgets.length === 0) return getDefaultDashboardLayout()
|
||||
|
||||
// Layout périmé ou vide → preset canonique
|
||||
// Layout périmé, vide, ou version inconnue (essai abandonné) → preset actuel
|
||||
const incomingVersion = typeof data.version === 'number' ? data.version : 0
|
||||
if (incomingVersion < DASHBOARD_LAYOUT_VERSION) {
|
||||
if (incomingVersion !== DASHBOARD_LAYOUT_VERSION) {
|
||||
return getDefaultDashboardLayout()
|
||||
}
|
||||
|
||||
|
||||
38
memento-note/lib/dashboard/path-title.ts
Normal file
38
memento-note/lib/dashboard/path-title.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
const PLACEHOLDER_TITLES = new Set([
|
||||
'untitled',
|
||||
'sans titre',
|
||||
'(sans titre)',
|
||||
'(untitled)',
|
||||
])
|
||||
|
||||
function usableTitle(title: string | null | undefined): string | null {
|
||||
const trimmed = title?.trim()
|
||||
if (!trimmed) return null
|
||||
if (PLACEHOLDER_TITLES.has(trimmed.toLowerCase())) return null
|
||||
return trimmed
|
||||
}
|
||||
|
||||
/** Titre affichable : vrai titre, sinon premier extrait du contenu. */
|
||||
export function pathNoteTitle(
|
||||
title: string | null | undefined,
|
||||
content?: string | null,
|
||||
): string | null {
|
||||
const named = usableTitle(title)
|
||||
if (named) return named
|
||||
if (!content?.trim()) return null
|
||||
const plain = content
|
||||
.replace(/<[^>]+>/g, ' ')
|
||||
.replace(/ /gi, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
if (!plain) return null
|
||||
return plain.length > 80 ? `${plain.slice(0, 80).trim()}…` : plain
|
||||
}
|
||||
|
||||
/** Première note récente qui a un titre ou un extrait — évite un héros « Sans titre ». */
|
||||
export function pickFocusNote<T extends { title: string | null; content: string }>(
|
||||
notes: T[],
|
||||
): T | undefined {
|
||||
if (notes.length === 0) return undefined
|
||||
return notes.find(n => pathNoteTitle(n.title, n.content)) ?? notes[0]
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { DashboardPath } from '@/lib/dashboard/path-types'
|
||||
import { pathNoteTitle, pickFocusNote } from '@/lib/dashboard/path-title'
|
||||
|
||||
function excerpt(text: string, max = 120): string {
|
||||
const plain = text.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim()
|
||||
@@ -45,14 +46,15 @@ export interface BriefingPathsInput {
|
||||
/** Pistes instantanées à partir du briefing déjà chargé — sans requête lourde. */
|
||||
export function buildFastPathsFromBriefing(input: BriefingPathsInput): DashboardPath[] {
|
||||
const paths: DashboardPath[] = []
|
||||
const focus = input.recentNotes[0]
|
||||
const focus = pickFocusNote(input.recentNotes)
|
||||
const continueTitle = focus ? pathNoteTitle(focus.title, focus.content) : null
|
||||
|
||||
if (focus) {
|
||||
if (focus && continueTitle) {
|
||||
paths.push({
|
||||
id: `continue-${focus.id}`,
|
||||
type: 'continue',
|
||||
priority: 100,
|
||||
title: focus.title || 'Untitled',
|
||||
title: continueTitle,
|
||||
description: excerpt(focus.content, 140),
|
||||
actionKey: 'continue',
|
||||
noteId: focus.id,
|
||||
@@ -63,11 +65,12 @@ export function buildFastPathsFromBriefing(input: BriefingPathsInput): Dashboard
|
||||
.filter(i => i.note1.id === focus.id || i.note2.id === focus.id)
|
||||
.slice(0, 2)) {
|
||||
const other = ins.note1.id === focus.id ? ins.note2 : ins.note1
|
||||
const otherTitle = pathNoteTitle(other.title, ins.note1.id === focus.id ? ins.note2Excerpt : ins.note1Excerpt)
|
||||
paths.push({
|
||||
id: `connect-${focus.id}-${other.id}`,
|
||||
type: 'connect',
|
||||
priority: 88,
|
||||
title: other.title || 'Untitled',
|
||||
title: otherTitle || excerpt(ins.insight, 80),
|
||||
description: ins.insight,
|
||||
actionKey: 'compare',
|
||||
noteId: focus.id,
|
||||
@@ -80,11 +83,13 @@ export function buildFastPathsFromBriefing(input: BriefingPathsInput): Dashboard
|
||||
|
||||
const freshInsight = input.insights.find(i => !i.viewed)
|
||||
if (freshInsight) {
|
||||
const left = pathNoteTitle(freshInsight.note1.title, freshInsight.note1Excerpt)
|
||||
const right = pathNoteTitle(freshInsight.note2.title, freshInsight.note2Excerpt)
|
||||
paths.push({
|
||||
id: `resurface-${freshInsight.id}`,
|
||||
type: 'resurface',
|
||||
priority: 85,
|
||||
title: `${freshInsight.note1.title || '…'} ↔ ${freshInsight.note2.title || '…'}`,
|
||||
title: left && right ? `${left} ↔ ${right}` : excerpt(freshInsight.insight, 80),
|
||||
description: freshInsight.insight,
|
||||
actionKey: 'openInsight',
|
||||
insightId: freshInsight.id,
|
||||
@@ -121,28 +126,6 @@ export function buildFastPathsFromBriefing(input: BriefingPathsInput): Dashboard
|
||||
})
|
||||
}
|
||||
|
||||
if (input.inboxCount > 0) {
|
||||
paths.push({
|
||||
id: 'organize-inbox',
|
||||
type: 'organize',
|
||||
priority: 60,
|
||||
title: `${input.inboxCount} notes`,
|
||||
description: 'inbox',
|
||||
actionKey: 'organizeInbox',
|
||||
})
|
||||
}
|
||||
|
||||
if (input.dueFlashcards > 0) {
|
||||
paths.push({
|
||||
id: 'review-flashcards',
|
||||
type: 'review',
|
||||
priority: 55,
|
||||
title: `${input.dueFlashcards}`,
|
||||
description: 'flashcards',
|
||||
actionKey: 'reviewCards',
|
||||
})
|
||||
}
|
||||
|
||||
paths.push({
|
||||
id: 'daily-journal',
|
||||
type: 'daily',
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'server-only'
|
||||
import prisma from '@/lib/prisma'
|
||||
import { clusteringService } from '@/lib/ai/services/clustering.service'
|
||||
import type { DashboardPath } from '@/lib/dashboard/path-types'
|
||||
import { pathNoteTitle, pickFocusNote } from '@/lib/dashboard/path-title'
|
||||
|
||||
export type { DashboardPath, DashboardPathType } from '@/lib/dashboard/path-types'
|
||||
|
||||
@@ -103,11 +104,15 @@ function pathsFromInsights(
|
||||
.slice(0, 2)) {
|
||||
const other = ins.note1.id === focus.id ? ins.note2 : ins.note1
|
||||
if (paths.some(p => p.note2Id === other.id && p.type === 'connect')) continue
|
||||
const otherTitle = pathNoteTitle(
|
||||
other.title,
|
||||
ins.note1.id === focus.id ? ins.note2Excerpt : ins.note1Excerpt,
|
||||
)
|
||||
paths.push({
|
||||
id: `connect-${focus.id}-${other.id}`,
|
||||
type: 'connect',
|
||||
priority: 90 - paths.length,
|
||||
title: other.title || 'Untitled',
|
||||
title: otherTitle || excerpt(ins.insight || ins.note2Excerpt || ins.note1Excerpt || '', 80),
|
||||
description: ins.insight || ins.note2Excerpt || ins.note1Excerpt || '',
|
||||
actionKey: 'compare',
|
||||
noteId: focus.id,
|
||||
@@ -152,7 +157,8 @@ async function findOpenLoops(userId: string, limit = 3): Promise<Array<{ id: str
|
||||
|
||||
export async function buildDashboardPaths(input: BuildPathsInput): Promise<DashboardPath[]> {
|
||||
const paths: DashboardPath[] = []
|
||||
const focus = input.recentNotes[0]
|
||||
const focus = pickFocusNote(input.recentNotes)
|
||||
const continueTitle = focus ? pathNoteTitle(focus.title, focus.content) : null
|
||||
|
||||
let focusClusterId: number | null = null
|
||||
if (focus) {
|
||||
@@ -162,16 +168,18 @@ export async function buildDashboardPaths(input: BuildPathsInput): Promise<Dashb
|
||||
})
|
||||
focusClusterId = member?.clusterId ?? null
|
||||
|
||||
if (continueTitle) {
|
||||
paths.push({
|
||||
id: `continue-${focus.id}`,
|
||||
type: 'continue',
|
||||
priority: 100,
|
||||
title: focus.title || 'Untitled',
|
||||
title: continueTitle,
|
||||
description: excerpt(focus.content, 140),
|
||||
actionKey: 'continue',
|
||||
noteId: focus.id,
|
||||
notebookId: focus.notebookId ?? undefined,
|
||||
})
|
||||
}
|
||||
|
||||
pathsFromInsights(focus, input.insights, paths)
|
||||
|
||||
@@ -181,7 +189,7 @@ export async function buildDashboardPaths(input: BuildPathsInput): Promise<Dashb
|
||||
id: `add-link-${focus.id}-${link.noteId}`,
|
||||
type: 'add-link',
|
||||
priority: 75,
|
||||
title: link.noteTitle || 'Untitled',
|
||||
title: pathNoteTitle(link.noteTitle, link.snippet) || excerpt(link.snippet, 80),
|
||||
description: link.snippet,
|
||||
actionKey: 'addLink',
|
||||
noteId: focus.id,
|
||||
@@ -198,7 +206,11 @@ export async function buildDashboardPaths(input: BuildPathsInput): Promise<Dashb
|
||||
id: `resurface-${freshInsight.id}`,
|
||||
type: 'resurface',
|
||||
priority: 85,
|
||||
title: `${freshInsight.note1.title || '…'} ↔ ${freshInsight.note2.title || '…'}`,
|
||||
title: (() => {
|
||||
const left = pathNoteTitle(freshInsight.note1.title, freshInsight.note1Excerpt)
|
||||
const right = pathNoteTitle(freshInsight.note2.title, freshInsight.note2Excerpt)
|
||||
return left && right ? `${left} ↔ ${right}` : excerpt(freshInsight.insight, 80)
|
||||
})(),
|
||||
description: freshInsight.insight,
|
||||
actionKey: 'openInsight',
|
||||
insightId: freshInsight.id,
|
||||
@@ -257,28 +269,6 @@ export async function buildDashboardPaths(input: BuildPathsInput): Promise<Dashb
|
||||
}
|
||||
}
|
||||
|
||||
if (input.inboxCount > 0) {
|
||||
paths.push({
|
||||
id: 'organize-inbox',
|
||||
type: 'organize',
|
||||
priority: 70,
|
||||
title: `${input.inboxCount} notes`,
|
||||
description: 'inbox',
|
||||
actionKey: 'organizeInbox',
|
||||
})
|
||||
}
|
||||
|
||||
if (input.dueFlashcards > 0) {
|
||||
paths.push({
|
||||
id: 'review-flashcards',
|
||||
type: 'review',
|
||||
priority: 65,
|
||||
title: `${input.dueFlashcards}`,
|
||||
description: 'flashcards',
|
||||
actionKey: 'reviewCards',
|
||||
})
|
||||
}
|
||||
|
||||
paths.push({
|
||||
id: 'daily-journal',
|
||||
type: 'daily',
|
||||
|
||||
@@ -37,7 +37,11 @@ export const THEME_INIT_SCRIPT = `(function () {
|
||||
}
|
||||
root.setAttribute('data-applied-theme', theme);
|
||||
var accentStored = localStorage.getItem('accent-color');
|
||||
root.style.setProperty('--color-brand-accent', accentStored || defaultAccent);
|
||||
var effectiveAccent = defaultAccent || accentStored || '#A47148';
|
||||
root.style.setProperty('--color-brand-accent', effectiveAccent);
|
||||
if (defaultAccent && accentStored !== defaultAccent) {
|
||||
try { localStorage.setItem('accent-color', defaultAccent); } catch (e) {}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Theme script error', e);
|
||||
}
|
||||
|
||||
29
memento-note/lib/notes/trash-toast.ts
Normal file
29
memento-note/lib/notes/trash-toast.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
'use client'
|
||||
|
||||
import { toast } from 'sonner'
|
||||
import { restoreNote } from '@/app/actions/notes'
|
||||
import { emitNoteChange } from '@/lib/note-change-sync'
|
||||
import type { Note } from '@/lib/types'
|
||||
|
||||
export function showNoteTrashedToast(
|
||||
note: Note,
|
||||
t: (key: string) => string,
|
||||
onRestored?: () => void,
|
||||
) {
|
||||
toast.success(t('notes.noteDeletedToast'), {
|
||||
action: {
|
||||
label: t('notes.undoDelete'),
|
||||
onClick: async () => {
|
||||
try {
|
||||
await restoreNote(note.id)
|
||||
const restored = { ...note, trashedAt: null }
|
||||
emitNoteChange({ type: 'created', note: restored })
|
||||
onRestored?.()
|
||||
toast.success(t('trash.noteRestored'))
|
||||
} catch {
|
||||
toast.error(t('general.error'))
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -10,6 +10,17 @@ export const MAX_EMBEDDING_CHARS = EMBEDDING_CHUNK_CHARS
|
||||
const CLIP_FOOTER_PATTERN =
|
||||
/<hr\s*\/?>\s*<p[^>]*>\s*<small>[\s\S]*?<\/small>\s*<\/p>\s*$/i
|
||||
|
||||
/** Seuil serveur : en dessous, pas de génération de titre (clic volontaire inclus). */
|
||||
export const MIN_WORDS_FOR_TITLE_SUGGESTION = 10
|
||||
/** Seuil automatique — aligné sur le texte des réglages (« après 50+ mots »). */
|
||||
export const MIN_WORDS_FOR_AUTO_TITLE = 50
|
||||
|
||||
export function countPlainWords(htmlOrText: string): number {
|
||||
const plain = stripHtmlToPlainText(htmlOrText)
|
||||
if (!plain) return 0
|
||||
return plain.split(/\s+/).filter((w) => w.length > 0).length
|
||||
}
|
||||
|
||||
export function stripHtmlToPlainText(html: string): string {
|
||||
if (!html) return ''
|
||||
return html
|
||||
|
||||
@@ -29,8 +29,12 @@ export function getThemeScript(serverTheme: string = 'light', serverAccentColor:
|
||||
}
|
||||
root.setAttribute('data-applied-theme', theme);
|
||||
var accentStored = localStorage.getItem('accent-color');
|
||||
var effectiveAccent = accentStored || ${JSON.stringify(defaultAccent)};
|
||||
var serverAccent = ${JSON.stringify(defaultAccent)};
|
||||
var effectiveAccent = serverAccent || accentStored || '#A47148';
|
||||
root.style.setProperty('--color-brand-accent', effectiveAccent);
|
||||
if (serverAccent && accentStored !== serverAccent) {
|
||||
try { localStorage.setItem('accent-color', serverAccent); } catch (e) {}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Theme script error', e);
|
||||
}
|
||||
|
||||
@@ -31,14 +31,14 @@
|
||||
"confirmPasswordPlaceholder": "أعد إدخال كلمة المرور",
|
||||
"backToSite": "رجوع",
|
||||
"continueWithGoogle": "متابعة بحساب Google",
|
||||
"createYourSpace": "أنشئ مساحتك",
|
||||
"createYourSpaceSubtitle": "انضم إلى العصر الجديد لتدوين الملاحظات الذكية.",
|
||||
"createYourSpace": "أنشئ دماغك الثاني",
|
||||
"createYourSpaceSubtitle": "يتصل أثناء الكتابة — ليست مجرد تطبيق ملاحظات آخر.",
|
||||
"forgot": "نسيت؟",
|
||||
"oauthAccountNotLinked": "حساب Google هذا لا يتطابق مع حسابك الحالي. استخدم نفس البريد الإلكتروني أو سجّل الدخول بكلمة المرور.",
|
||||
"privacyTerms": "© 2025 Memento Labs — الخصوصية · الشروط",
|
||||
"sessionExpired": "يتم إنشاء موقعك مع التنقل وجدول المحتويات",
|
||||
"welcomeBack": "مرحبًا بعودتك",
|
||||
"welcomeBackSubtitle": "أدخل بيانات الاعتماد للوصول إلى ملاحظاتك.",
|
||||
"welcomeBackSubtitle": "سجّل الدخول لاستعادة ملاحظاتك وروابطك وما كنت قد نسيته.",
|
||||
"checkEmailTitle": "تحقق من بريدك الإلكتروني",
|
||||
"checkEmailDescription": "أرسلنا رابط تأكيد إلى {email}. افتحه لتفعيل حسابك قبل تسجيل الدخول.",
|
||||
"checkEmailDescriptionGeneric": "أرسلنا رابط تأكيد إلى بريدك. افتحه لتفعيل حسابك قبل تسجيل الدخول.",
|
||||
@@ -99,13 +99,13 @@
|
||||
"darkMode": "الوضع الداكن",
|
||||
"dashboardPanelBody": "انتهت جلستك. يرجى تسجيل الدخول مرة أخرى.",
|
||||
"documents": "مستندات",
|
||||
"insightsPanelBody": "الخريطة الدلالية لملاحظاتك: عناقيد موضوعية، ملاحظات جسرية، واقتراحات روابط.",
|
||||
"insightsPanelBody": "خريطة لكيفية ارتباط ملاحظاتك: مواضيع قريبة، ملاحظات جسرية، وروابط يمكنك فتحها.",
|
||||
"lightMode": "الوضع الفاتح",
|
||||
"notebookEmpty": "فارغ",
|
||||
"recentNote": "أُنشئت مؤخراً",
|
||||
"resizeNotebooksPanel": "تغيير حجم لوحة الدفاتر",
|
||||
"resizeSidebar": "تغيير عرض الشريط الجانبي",
|
||||
"revisionPanelBody": "راجع البطاقات التعليمية بخوارزمية SM-2. تُنشأ الحزم من ملاحظاتك.",
|
||||
"revisionPanelBody": "راجع بالبطاقات. التكرار المتباعد يعيدها في الوقت المناسب. تُنشأ الحزم من ملاحظاتك.",
|
||||
"searchNotebooksPlaceholder": "ابحث عن الدفاتر…",
|
||||
"searchShortcut": "بحث (Ctrl+K)"
|
||||
},
|
||||
@@ -130,7 +130,7 @@
|
||||
"add": "إضافة",
|
||||
"adding": "جاري الإضافة...",
|
||||
"close": "إغلاق",
|
||||
"confirmDelete": "هل أنت متأكد أنك تريد حذف هذه الملاحظة؟",
|
||||
"confirmDelete": "ستنتقل هذه الملاحظة إلى سلة المهملات. يمكنك استعادتها لاحقاً.",
|
||||
"confirmLeaveShare": "هل أنت متأكد أنك تريد مغادرة هذه الملاحظة المشتركة؟",
|
||||
"sharedBy": "شاركها",
|
||||
"sharedShort": "مشترك",
|
||||
@@ -991,7 +991,7 @@
|
||||
"dailyNotes": "ملاحظات يومية",
|
||||
"dashboard": "لوحة التحكم",
|
||||
"graphView": "خريطة الروابط",
|
||||
"insights": "المواضيع الدلالية",
|
||||
"insights": "الروابط",
|
||||
"revision": "مراجعة"
|
||||
},
|
||||
"settings": {
|
||||
@@ -2095,14 +2095,14 @@
|
||||
"collapse": "طي"
|
||||
},
|
||||
"mcpSettings": {
|
||||
"title": "MCP",
|
||||
"title": "أدوات خارجية",
|
||||
"description": "إدارة مفاتيح API وتكوين الأدوات الخارجية",
|
||||
"tierRequired": "Pro+ فقط",
|
||||
"upgradeHint": "يتطلب الوصول إلى MCP (مفاتيح API لـ Cursor وClaude Desktop إلخ) خطة Pro أو أعلى. قم بالترقية في الفوترة لفتح هذه الميزة.",
|
||||
"whatIsMcp": {
|
||||
"title": "ما هو MCP؟",
|
||||
"title": "ما فائدة هذا؟",
|
||||
"description": "بروتوكول سياق النموذج (MCP) هو بروتوكول مفتوح يمكّن نماذج الذكاء الاصطناعي من التفاعل بأمان مع الأدوات ومصادر البيانات الخارجية. باستخدام MCP، يمكنك ربط أدوات مثل Claude Code و Cursor و N8N بمثيل Memento الخاص بك لقراءة ملاحظاتك وإنشائها وتنظيمها برمجيًا.",
|
||||
"learnMore": "معرفة المزيد عن MCP"
|
||||
"learnMore": "معرفة المزيد"
|
||||
},
|
||||
"serverStatus": {
|
||||
"title": "حالة الخادم",
|
||||
@@ -2161,12 +2161,12 @@
|
||||
}
|
||||
},
|
||||
"helpBox": {
|
||||
"title": "ما هو MCP (Model Context Protocol)؟",
|
||||
"title": "كيف أصل أداة؟",
|
||||
"step1": "MCP هو بروتوكول يسمح لوكلاء الذكاء الاصطناعي في Memento بالاتصال بالأدوات الخارجية (قواعد البيانات، واجهات API، الملفات، إلخ).",
|
||||
"step2": "يوفر Memento خادم MCP مع 22 أداة — يمكن لوكلائك قراءة/إنشاء الملاحظات، والبحث في قاعدتك، وإدارة الدفاتر، إلخ.",
|
||||
"step3": "أنشئ مفتاح API هنا، ثم قم بتكوينه في عميل MCP الخاص بك (Claude Desktop، Cursor، Continue.dev…) مع عنوان URL للخادم.",
|
||||
"step4": "تنسيق التكوين: عنوان URL لخادم MCP + مفتاحك في رأس Authorization.",
|
||||
"step4Link": "توثيق MCP",
|
||||
"step4Link": "المساعدة الرسمية",
|
||||
"step5": "حالة الاستخدام: اطلب من Claude Desktop كتابة ملاحظة في Memento، أو البحث في دفاترك، أو إنشاء وكيل."
|
||||
}
|
||||
},
|
||||
@@ -2334,6 +2334,17 @@
|
||||
"title": "القوالب",
|
||||
"install": "تثبيت",
|
||||
"installing": "جاري التثبيت...",
|
||||
"seeAll": "عرض الكل",
|
||||
"showLess": "عرض أقل",
|
||||
"categoryAll": "الكل",
|
||||
"categoryWatch": "متابعة",
|
||||
"categoryDigest": "ملخصات",
|
||||
"categoryTools": "أدوات",
|
||||
"categoryGenerate": "إنشاء",
|
||||
"taskExtractor": {
|
||||
"name": "المهام في الملاحظات",
|
||||
"description": "يجد المهام في ملاحظاتك ويجمعها في مكان واحد."
|
||||
},
|
||||
"veilleAI": {
|
||||
"name": "مراقبة الذكاء الاصطناعي",
|
||||
"description": "يجمع البيانات من 5 مواقع متخصصة في الذكاء الاصطناعي وينشئ ملخصًا أسبوعيًا."
|
||||
@@ -2453,7 +2464,7 @@
|
||||
"slideStyle": "يؤثر النمط المرئي على نصف قطر الزاوية والتباعد وكثافة المعلومات."
|
||||
}
|
||||
},
|
||||
"intelligenceOS": "نظام التشغيل الذكي"
|
||||
"intelligenceOS": "الوكلاء"
|
||||
},
|
||||
"chat": {
|
||||
"title": "محادثة الذكاء الاصطناعي",
|
||||
@@ -2740,7 +2751,7 @@
|
||||
"slashDatabaseDesc": "ضمن البيانات المنظمة لدفترك",
|
||||
"slashLinkPreview": "معاينة الرابط",
|
||||
"slashLinkPreviewDesc": "تحويل رابط إلى بطاقة بصرية",
|
||||
"slashLivingBlock": "كتلة حية",
|
||||
"slashLivingBlock": "كتلة مرتبطة",
|
||||
"slashLivingBlockDesc": "إدراج من ملاحظة أخرى",
|
||||
"slashMath": "معادلة",
|
||||
"slashMathDesc": "صيغة رياضية بتدوين LaTeX",
|
||||
@@ -3007,7 +3018,7 @@
|
||||
"proChat": "100 chat messages / month",
|
||||
"later": "لاحقاً",
|
||||
"upgradePricing": "ترقية إلى Pro",
|
||||
"addApiKey": "استخدم مفتاح API الخاص بك (BYOK)",
|
||||
"addApiKey": "استخدم مفتاحك الخاص",
|
||||
"featureBrainstormCreate": "Créations brainstorm",
|
||||
"featureBrainstormEnrich": "Enrichissements brainstorm",
|
||||
"featureBrainstormExpand": "Extensions brainstorm",
|
||||
@@ -3025,10 +3036,10 @@
|
||||
"outOfCredits": "نفدت الأرصدة — خيارات"
|
||||
},
|
||||
"byokSettings": {
|
||||
"title": "مفاتيح API الخاصة بك (BYOK)",
|
||||
"title": "مفاتيح المزوّد الخاصة بك",
|
||||
"description": "Connect your own LLM provider keys to bypass Discovery Pack quotas. Keys are encrypted at rest.",
|
||||
"badgeActive": "BYOK نشط",
|
||||
"tierRequired": "يتطلب BYOK خطة Pro أو أعلى. قم بالترقية لربط مفاتيح API الخاصة بك.",
|
||||
"badgeActive": "المفاتيح نشطة",
|
||||
"tierRequired": "يتطلب هذا الخيار خطة Pro أو أعلى.",
|
||||
"provider": "المزود",
|
||||
"providerPlaceholder": "اختر مزوداً",
|
||||
"alias": "تسمية (اختياري)",
|
||||
@@ -3167,6 +3178,9 @@
|
||||
"fetchInvoicesFailed": "تعذر تحميل سجل الفوترة.",
|
||||
"savePercent": "وفّر ~17%",
|
||||
"cancelSubscription": "إلغاء الاشتراك",
|
||||
"changeOffer": "تغيير العرض",
|
||||
"downgradeToFree": "العودة إلى العرض المجاني",
|
||||
"cancellingNotice": "إلغاء مجدول — الوصول حتى {date}",
|
||||
"disabledByAdmin": "الفوترة والترقيات معطلة حالياً. اتصل بالمسؤول إذا كنت بحاجة إلى وصول.",
|
||||
"tab": "الفواتير",
|
||||
"creditsFromPacks": "أرصدة الحزم",
|
||||
@@ -3284,34 +3298,37 @@
|
||||
"title": "فوّض العمل الثقيل.",
|
||||
"desc": "بحث، استخراج، شرائح، مخططات، مراقبة — وكلاء يكتبون في Second Brain.",
|
||||
"scraper": {
|
||||
"title": "Scraper",
|
||||
"title": "مراقب",
|
||||
"desc": "روابط وRSS → ملاحظات مركّبة بصور."
|
||||
},
|
||||
"researcher": {
|
||||
"title": "Researcher",
|
||||
"title": "باحث",
|
||||
"desc": "استعلامات عميقة ومصادر وملاحظات بحث منظمة."
|
||||
},
|
||||
"slideGen": {
|
||||
"title": "Slide Gen",
|
||||
"title": "الشرائح",
|
||||
"desc": "ملاحظات → عروض أو شرائح HTML تفاعلية."
|
||||
},
|
||||
"monitor": {
|
||||
"title": "Monitor",
|
||||
"title": "مراقب",
|
||||
"desc": "راقب الدفاتر: اتجاهات ورؤى."
|
||||
},
|
||||
"diagramGen": {
|
||||
"title": "Diagram Gen",
|
||||
"title": "رسم بياني",
|
||||
"desc": "أفكار → خرائط ذهنية وتدفقات Excalidraw."
|
||||
},
|
||||
"custom": {
|
||||
"title": "Custom",
|
||||
"title": "مخصص",
|
||||
"desc": "أدوارك ومصادرك وجداولك."
|
||||
}
|
||||
},
|
||||
"byok": {
|
||||
"label": "بدون قفل",
|
||||
"title": "مفاتيحك. نماذجك. دماغك الثاني.",
|
||||
"desc": "أرصدة Memento أو OpenAI وAnthropic وGoogle… بدّل المزوّد بنقرة."
|
||||
"desc": "أرصدة Memento، أو مزوّدك الخاص. بدّل بنقرة — المنتج يبقى لك.",
|
||||
"pointCredits": "أرصدة Memento إن أردت الأمر بسيطًا",
|
||||
"pointProvider": "أو اربط مزوّدك الخاص",
|
||||
"pointYours": "المفتاح يبقى لديك — ولا يُعرض هنا أبدًا"
|
||||
},
|
||||
"pricing": {
|
||||
"label": "Pricing",
|
||||
@@ -3339,7 +3356,7 @@
|
||||
"desc": "للعقول الجادة.",
|
||||
"cta": "انتقل إلى Pro",
|
||||
"feature0": "ملاحظات بلا حد",
|
||||
"feature1": "BYOK",
|
||||
"feature1": "مفاتيحك الخاصة",
|
||||
"feature2": "200 بحث دلالي / شهر",
|
||||
"feature3": "وكلاء (12 تشغيل/شهر)",
|
||||
"feature4": "سجل 30 يومًا",
|
||||
@@ -3350,11 +3367,11 @@
|
||||
"desc": "Second Brain للفريق.",
|
||||
"cta": "اختر Business",
|
||||
"feature0": "10 متعاونين",
|
||||
"feature1": "BYOK · 13 مزودًا",
|
||||
"feature1": "مفاتيحك · {count} مزودين",
|
||||
"feature2": "1000 بحث دلالي",
|
||||
"feature3": "وكلاء (60 تشغيل/شهر)",
|
||||
"feature4": "عصف ذهني بلا حد",
|
||||
"feature5": "API / MCP"
|
||||
"feature5": "أدوات خارجية"
|
||||
},
|
||||
"enterprise": {
|
||||
"name": "Enterprise",
|
||||
@@ -3545,12 +3562,12 @@
|
||||
"feature_search_title": "البحث الدلالي",
|
||||
"feature_search_desc": "ابحث عن أي ملاحظة بالمعنى، ليس فقط بالكلمات المفتاحية.",
|
||||
"feature_flashcards_title": "بطاقات الذكاء الاصطناعي",
|
||||
"feature_flashcards_desc": "أنشئ بطاقات مراجعة SRS من ملاحظاتك بنقرة واحدة.",
|
||||
"feature_flashcards_desc": "أنشئ بطاقات مراجعة من ملاحظاتك بنقرة واحدة.",
|
||||
"feature_brainstorm_title": "العصف الذهني بالذكاء الاصطناعي",
|
||||
"feature_brainstorm_desc": "جلسات عصف ذهني تعاوني مدعومة بالذكاء الاصطناعي.",
|
||||
"feature_chat_title": "الدردشة مع ملاحظاتك",
|
||||
"feature_chat_desc": "اطرح أسئلة على قاعدة معرفتك الشخصية.",
|
||||
"feature_insights_title": "رؤى دلالية",
|
||||
"feature_insights_title": "الروابط",
|
||||
"feature_insights_desc": "اكتشف الروابط الخفية بين أفكارك.",
|
||||
"feature_export_title": "تصدير Markdown",
|
||||
"feature_export_desc": "استورد وصدِّر ملاحظاتك بتنسيق Markdown القياسي.",
|
||||
@@ -3638,9 +3655,10 @@
|
||||
"createDiagramCostHint": "≈ 4 أرصدة ذكاء اصطناعي"
|
||||
},
|
||||
"insightsView": {
|
||||
"title": "رؤى دلالية",
|
||||
"title": "الروابط",
|
||||
"toggleMenu": "إظهار القائمة أو إخفاؤها",
|
||||
"subtitle": "اكتشف البنية المخفية لمعرفتك",
|
||||
"resync": "إعادة مزامنة الشبكة",
|
||||
"resync": "تحديث",
|
||||
"mapping": "جاري الربط…",
|
||||
"loading": "جاري تحميل الملاحظات…",
|
||||
"mappingTitle": "جاري رسم خريطة معرفتك…",
|
||||
@@ -3800,7 +3818,7 @@
|
||||
"apiKey": "مفتاح API",
|
||||
"apiUrl": "رابط API",
|
||||
"baseUrlRequired": "يرجى تقديم عنوان URL للـ API",
|
||||
"byokActive": "BYOK مفعّل",
|
||||
"byokActive": "المفاتيح نشطة",
|
||||
"choose": "اختر…",
|
||||
"chooseModel": "اختر نموذجًا…",
|
||||
"chooseProvider": "اختر مزودًا…",
|
||||
@@ -4011,7 +4029,7 @@
|
||||
"tabProgress": "تقدم",
|
||||
"tapToFlip": "مسافة أو انقر للقلب",
|
||||
"toolbarGenerate": "إنشاء بطاقات تعليمية",
|
||||
"toolbarGenerateHint": "التكرار المتباعد SM-2",
|
||||
"toolbarGenerateHint": "تعود في الوقت المناسب",
|
||||
"totalCardsLabel": "إجمالي البطاقات",
|
||||
"totalReviewsLabel": "إجمالي المراجعات",
|
||||
"upToDate": "محدّث",
|
||||
@@ -4055,6 +4073,9 @@
|
||||
"homeDashboard": {
|
||||
"activityEmptyHint": "حرر الملاحظات للاطلاع على إيقاع كتابتك على مدى آخر 90 يوماً.",
|
||||
"agentCreated": "تم إنشاء الوكيل وتشغيله",
|
||||
"agentCreatedInCard": "الوكيل جاهز.",
|
||||
"agentOpenCreated": "فتح الوكيل",
|
||||
"agentSeeNextSuggestion": "عرض التالي",
|
||||
"agentDiscovery": "وكيل",
|
||||
"agentFailed": "فشل الإنشاء",
|
||||
"agentsEmpty": "لم يُقترح وكلاء بحث بعد. استمر في الكتابة — سيقترح Memento مواضيع تركيب عندما تتجمع ملاحظاتك.",
|
||||
@@ -4063,6 +4084,8 @@
|
||||
"aiFound": "وجد الذكاء الاصطناعي",
|
||||
"aiProviderUnavailable": "الذكاء الاصطناعي غير متاح مؤقتًا. تحقق من إعدادات المزود.",
|
||||
"allCaughtUp": "كل شيء مكتمل.",
|
||||
"remindersEmpty": "لا تذكيرات لليوم.",
|
||||
"remindersOpenAll": "عرض التذكيرات",
|
||||
"alreadySeen": "مشاهد",
|
||||
"analyzeNotes": "حلل ملاحظاتي",
|
||||
"analyzing": "جارٍ التحليل…",
|
||||
@@ -4180,6 +4203,8 @@
|
||||
"pulseReview": "{count} للمراجعة",
|
||||
"quickCapture": "التقاط سريع",
|
||||
"quickCapturePlaceholder": "فكرة، خاطرة… اضغط Enter للحفظ في الوارد.",
|
||||
"captureGoesToFile": "يذهب إلى الوارد.",
|
||||
"captureSend": "إرسال إلى الوارد",
|
||||
"reminders": "التذكيرات",
|
||||
"resumeAlso": "أيضًا مؤخرًا",
|
||||
"resumeEmptyCta": "التقاط فكرة",
|
||||
@@ -4267,6 +4292,7 @@
|
||||
"widgetHide": "إخفاء أداة",
|
||||
"widgetOpen": "فتح",
|
||||
"widgetPinnedEmpty": "لا توجد ملاحظات مثبتة بعد.",
|
||||
"pinnedNoNotebook": "بدون دفتر",
|
||||
"widgetReset": "إعادة تعيين",
|
||||
"widgetResetDone": "تمت استعادة لوحة التحكم الافتراضية.",
|
||||
"widgetStatsBridges": "جسور",
|
||||
|
||||
@@ -31,14 +31,14 @@
|
||||
"confirmPasswordPlaceholder": "Passwort erneut eingeben",
|
||||
"backToSite": "Zurück",
|
||||
"continueWithGoogle": "Mit Google fortfahren",
|
||||
"createYourSpace": "Erstelle deinen Bereich",
|
||||
"createYourSpaceSubtitle": "Willkommen im neuen Zeitalter der intelligenten Notizaufzeichnung.",
|
||||
"createYourSpace": "Erstelle dein zweites Gehirn",
|
||||
"createYourSpaceSubtitle": "Es verbindet sich, während du schreibst — nicht nur eine weitere Notiz-App.",
|
||||
"forgot": "Vergessen?",
|
||||
"oauthAccountNotLinked": "Dieses Google-Konto stimmt nicht mit Ihrem bestehenden Konto überein. Verwenden Sie dieselbe E-Mail oder melden Sie sich mit Ihrem Passwort an.",
|
||||
"privacyTerms": "© 2025 Memento Labs — Datenschutz · AGB",
|
||||
"sessionExpired": "Ihre Seite wird mit Navigation und Inhaltsverzeichnis generiert",
|
||||
"welcomeBack": "Willkommen zurück",
|
||||
"welcomeBackSubtitle": "Geben Sie Ihre Anmeldedaten ein, um auf Ihre Notizen zuzugreifen.",
|
||||
"welcomeBackSubtitle": "Melde dich an, um Notizen, Verbindungen und das, was du vergessen hast, wiederzufinden.",
|
||||
"checkEmailTitle": "E-Mail prüfen",
|
||||
"checkEmailDescription": "Wir haben einen Bestätigungslink an {email} gesendet. Öffnen Sie ihn, um Ihr Konto zu aktivieren.",
|
||||
"checkEmailDescriptionGeneric": "Wir haben einen Bestätigungslink an Ihre E-Mail gesendet. Öffnen Sie ihn, um Ihr Konto zu aktivieren.",
|
||||
@@ -99,13 +99,13 @@
|
||||
"darkMode": "Dunkelmodus",
|
||||
"dashboardPanelBody": "Ihre Sitzung ist abgelaufen. Bitte melden Sie sich erneut an.",
|
||||
"documents": "Dokumente",
|
||||
"insightsPanelBody": "Semantische Karte Ihrer Notizen: thematische Cluster, Brücken-Notizen und Verbindungsvorschläge.",
|
||||
"insightsPanelBody": "Eine Karte, wie Ihre Notizen zusammenhängen: verwandte Themen, Brücken-Notizen und Links zum Öffnen.",
|
||||
"lightMode": "Helles Design",
|
||||
"notebookEmpty": "Leer",
|
||||
"recentNote": "Kürzlich erstellt",
|
||||
"resizeNotebooksPanel": "Notizbuch-Panel-Größe ändern",
|
||||
"resizeSidebar": "Seitenleistenbreite anpassen",
|
||||
"revisionPanelBody": "Wiederholen Sie Karteikarten mit dem SM-2-Algorithmus. Stapel werden aus Ihren Notizen generiert.",
|
||||
"revisionPanelBody": "Wiederholen Sie mit Karteikarten. Die zeitversetzte Wiederholung bringt sie zur richtigen Zeit zurück. Stapel entstehen aus Ihren Notizen.",
|
||||
"searchNotebooksPlaceholder": "Notizbücher suchen…",
|
||||
"searchShortcut": "Suchen (Strg+K)"
|
||||
},
|
||||
@@ -130,7 +130,7 @@
|
||||
"add": "Hinzufügen",
|
||||
"adding": "Wird hinzugefügt...",
|
||||
"close": "Schließen",
|
||||
"confirmDelete": "Möchtest du diese Notiz wirklich löschen?",
|
||||
"confirmDelete": "Diese Notiz wird in den Papierkorb verschoben. Sie können sie später wiederherstellen.",
|
||||
"confirmLeaveShare": "Möchten Sie diese geteilte Notiz wirklich verlassen?",
|
||||
"sharedBy": "Geteilt von",
|
||||
"sharedShort": "Geteilt",
|
||||
@@ -991,7 +991,7 @@
|
||||
"dailyNotes": "Tagesnotizen",
|
||||
"dashboard": "Dashboard",
|
||||
"graphView": "Link-Karte",
|
||||
"insights": "Semantische Themen",
|
||||
"insights": "Verbindungen",
|
||||
"revision": "Wiederholen"
|
||||
},
|
||||
"settings": {
|
||||
@@ -2095,14 +2095,14 @@
|
||||
"collapse": "Zusammenklappen"
|
||||
},
|
||||
"mcpSettings": {
|
||||
"title": "MCP",
|
||||
"title": "Externe Werkzeuge",
|
||||
"description": "API-Schlüssel verwalten und externe Tools konfigurieren",
|
||||
"tierRequired": "Nur Pro+",
|
||||
"upgradeHint": "MCP-Zugriff (API-Schlüssel für Cursor, Claude Desktop usw.) erfordert einen Pro-Plan oder höher. In der Abrechnung upgraden, um diese Funktion zu aktivieren.",
|
||||
"whatIsMcp": {
|
||||
"title": "Was ist MCP?",
|
||||
"title": "Wozu dient das?",
|
||||
"description": "Das Model Context Protocol (MCP) ist ein offenes Protokoll, das es KI-Modellen ermöglicht, sicher mit externen Tools und Datenquellen zu interagieren. Mit MCP können Sie Tools wie Claude Code, Cursor oder N8N mit Ihrer Memento-Instanz verbinden, um Ihre Notes programmgesteuert zu lesen, zu erstellen und zu organisieren.",
|
||||
"learnMore": "Mehr über MCP erfahren"
|
||||
"learnMore": "Mehr erfahren"
|
||||
},
|
||||
"serverStatus": {
|
||||
"title": "Serverstatus",
|
||||
@@ -2161,12 +2161,12 @@
|
||||
}
|
||||
},
|
||||
"helpBox": {
|
||||
"title": "Was ist MCP (Model Context Protocol)?",
|
||||
"title": "Wie verbinde ich ein Werkzeug?",
|
||||
"step1": "MCP ist ein Protokoll, das es den KI-Agenten von Memento ermöglicht, sich mit externen Tools (Datenbanken, APIs, Dateien usw.) zu verbinden.",
|
||||
"step2": "Memento stellt einen MCP-Server mit 22 Tools bereit — Ihre Agenten können Notizen lesen/erstellen, Ihre Datenbank durchsuchen, Notizbücher verwalten usw.",
|
||||
"step3": "Erstellen Sie hier einen API-Schlüssel und konfigurieren Sie ihn dann in Ihrem MCP-Client (Claude Desktop, Cursor, Continue.dev…) mit der Server-URL.",
|
||||
"step4": "Konfigurationsformat: MCP-Server-URL + Ihr Schlüssel im Authorization-Header.",
|
||||
"step4Link": "MCP-Dokumentation",
|
||||
"step4Link": "Offizielle Hilfe",
|
||||
"step5": "Anwendungsfall: Bitten Sie Claude Desktop, eine Notiz in Memento zu schreiben, Ihre Notizbücher zu durchsuchen oder einen Agenten zu erstellen."
|
||||
}
|
||||
},
|
||||
@@ -2334,6 +2334,17 @@
|
||||
"title": "Vorlagen",
|
||||
"install": "Installieren",
|
||||
"installing": "Wird installiert...",
|
||||
"seeAll": "Alle anzeigen",
|
||||
"showLess": "Weniger anzeigen",
|
||||
"categoryAll": "Alle",
|
||||
"categoryWatch": "Beobachtung",
|
||||
"categoryDigest": "Zusammenfassungen",
|
||||
"categoryTools": "Werkzeuge",
|
||||
"categoryGenerate": "Erstellen",
|
||||
"taskExtractor": {
|
||||
"name": "Aufgaben aus Notizen",
|
||||
"description": "Findet Aufgaben in Ihren Notizen und sammelt sie an einem Ort."
|
||||
},
|
||||
"veilleAI": {
|
||||
"name": "KI-Watch",
|
||||
"description": "Extrahiert Inhalte von 5 KI-spezialisierten Websites und erstellt eine wöchentliche Zusammenfassung."
|
||||
@@ -2453,7 +2464,7 @@
|
||||
"slideStyle": "Der visuelle Stil beeinflusst den Eckenradius, den Abstand und die Informationsdichte."
|
||||
}
|
||||
},
|
||||
"intelligenceOS": "Intelligentes Betriebssystem"
|
||||
"intelligenceOS": "Agenten"
|
||||
},
|
||||
"chat": {
|
||||
"title": "KI-Chat",
|
||||
@@ -2740,7 +2751,7 @@
|
||||
"slashDatabaseDesc": "Betten Sie die strukturierten Daten Ihres Notizbuchs ein",
|
||||
"slashLinkPreview": "Link-Vorschau",
|
||||
"slashLinkPreviewDesc": "Eine URL in eine visuelle Karte umwandeln",
|
||||
"slashLivingBlock": "Live-Block",
|
||||
"slashLivingBlock": "Verknüpfter Block",
|
||||
"slashLivingBlockDesc": "Aus anderer Notiz einfügen",
|
||||
"slashMath": "Gleichung",
|
||||
"slashMathDesc": "Mathematische Formel in LaTeX-Notation",
|
||||
@@ -3007,7 +3018,7 @@
|
||||
"proChat": "100 chat messages / month",
|
||||
"later": "Später",
|
||||
"upgradePricing": "Auf Pro upgraden",
|
||||
"addApiKey": "Eigenen API-Schlüssel verwenden (BYOK)",
|
||||
"addApiKey": "Eigenen Schlüssel verwenden",
|
||||
"featureBrainstormCreate": "Créations brainstorm",
|
||||
"featureBrainstormEnrich": "Enrichissements brainstorm",
|
||||
"featureBrainstormExpand": "Extensions brainstorm",
|
||||
@@ -3025,10 +3036,10 @@
|
||||
"outOfCredits": "Keine Credits mehr — Optionen"
|
||||
},
|
||||
"byokSettings": {
|
||||
"title": "Ihre API-Schlüssel (BYOK)",
|
||||
"title": "Ihre Anbieter-Schlüssel",
|
||||
"description": "Connect your own LLM provider keys to bypass Discovery Pack quotas. Keys are encrypted at rest.",
|
||||
"badgeActive": "BYOK aktiv",
|
||||
"tierRequired": "BYOK erfordert einen Pro-Plan oder höher. Upgrade, um Ihre API-Schlüssel zu verbinden.",
|
||||
"badgeActive": "Schlüssel aktiv",
|
||||
"tierRequired": "Diese Option erfordert einen Pro-Plan oder höher.",
|
||||
"provider": "Anbieter",
|
||||
"providerPlaceholder": "Anbieter auswählen",
|
||||
"alias": "Bezeichnung (optional)",
|
||||
@@ -3167,6 +3178,9 @@
|
||||
"fetchInvoicesFailed": "Rechnungsverlauf konnte nicht geladen werden.",
|
||||
"savePercent": "~17% sparen",
|
||||
"cancelSubscription": "Abonnement kündigen",
|
||||
"changeOffer": "Angebot wechseln",
|
||||
"downgradeToFree": "Zum kostenlosen Angebot zurück",
|
||||
"cancellingNotice": "Kündigung geplant — Zugang bis {date}",
|
||||
"disabledByAdmin": "Abrechnung und Upgrades sind derzeit deaktiviert. Wenden Sie sich an Ihren Administrator, wenn Sie Zugriff benötigen.",
|
||||
"tab": "Abrechnung",
|
||||
"creditsFromPacks": "Paket-Credits",
|
||||
@@ -3232,7 +3246,7 @@
|
||||
"title": "Der Moment, in dem Ihr Second Brain antwortet.",
|
||||
"desc": "Während Sie schreiben, erkennt Memento semantische Links über Notizbücher hinweg — keine Stichworte, echte Konzeptbrücken.",
|
||||
"card0Label": "Gerade erkannt",
|
||||
"card0": "„Ihre Pricing-Notiz spiegelt die Wettbewerbsanalyse vom Herbst.“",
|
||||
"card0": "„Ihre Notiz zu den Preisen spiegelt die Wettbewerbsanalyse vom Herbst.“",
|
||||
"card1Label": "Brücke",
|
||||
"card1": "Gemeinsames Thema: Positionierung unter Zwang",
|
||||
"card2Label": "Aktion",
|
||||
@@ -3245,7 +3259,7 @@
|
||||
"w0Label": "Nächste Wege",
|
||||
"w0": "3 Aktionen aus Ihrer letzten Notiz",
|
||||
"w1Label": "Tagesreview",
|
||||
"w1": "Inbox · KI · Karteikarten",
|
||||
"w1": "Posteingang · Funde · Karten",
|
||||
"w2Label": "Memory Echo",
|
||||
"w2": "2 neue Verbindungen über Nacht",
|
||||
"w3Label": "Agenten",
|
||||
@@ -3284,34 +3298,37 @@
|
||||
"title": "Delegieren Sie die schwere Arbeit.",
|
||||
"desc": "Recherche, Scraping, Folien, Diagramme, Monitoring — Agenten, die in Ihr Second Brain schreiben.",
|
||||
"scraper": {
|
||||
"title": "Scraper",
|
||||
"title": "Monitor",
|
||||
"desc": "URLs & RSS → synthetisierte Notizen mit Bildern."
|
||||
},
|
||||
"researcher": {
|
||||
"title": "Researcher",
|
||||
"title": "Rechercheur",
|
||||
"desc": "Tiefe Queries, Quellen, strukturierte Research-Notizen."
|
||||
},
|
||||
"slideGen": {
|
||||
"title": "Slide Gen",
|
||||
"title": "Folien",
|
||||
"desc": "Notizen → Decks oder interaktive HTML-Folien."
|
||||
},
|
||||
"monitor": {
|
||||
"title": "Monitor",
|
||||
"title": "Beobachter",
|
||||
"desc": "Notizbücher auf Trends und Insights überwachen."
|
||||
},
|
||||
"diagramGen": {
|
||||
"title": "Diagram Gen",
|
||||
"title": "Diagramm",
|
||||
"desc": "Ideen → Excalidraw Mindmaps & Flows."
|
||||
},
|
||||
"custom": {
|
||||
"title": "Custom",
|
||||
"title": "Benutzerdefiniert",
|
||||
"desc": "Ihre Rollen, Quellen und Zeitpläne."
|
||||
}
|
||||
},
|
||||
"byok": {
|
||||
"label": "Kein Lock-in",
|
||||
"label": "Frei wechseln",
|
||||
"title": "Ihre Schlüssel. Ihre Modelle. Ihr Second Brain.",
|
||||
"desc": "Memento-Credits oder OpenAI, Anthropic, Google… Anbieter in einem Klick wechseln."
|
||||
"desc": "Memento-Credits oder Ihr eigener Anbieter. Wechsel in einem Klick — das Produkt bleibt Ihres.",
|
||||
"pointCredits": "Memento-Credits, wenn Sie es einfach halten wollen",
|
||||
"pointProvider": "Oder Ihren eigenen Anbieter verbinden",
|
||||
"pointYours": "Ihr Schlüssel bleibt bei Ihnen — er wird hier nie angezeigt"
|
||||
},
|
||||
"pricing": {
|
||||
"label": "Pricing",
|
||||
@@ -3339,7 +3356,7 @@
|
||||
"desc": "Für anspruchsvolle Denker.",
|
||||
"cta": "Pro wählen",
|
||||
"feature0": "Unbegrenzte Notizen",
|
||||
"feature1": "BYOK",
|
||||
"feature1": "Eigene Anbieter-Schlüssel",
|
||||
"feature2": "200 semantische Suchen / Monat",
|
||||
"feature3": "Agenten (12 Läufe/Monat)",
|
||||
"feature4": "30 Tage Verlauf",
|
||||
@@ -3350,11 +3367,11 @@
|
||||
"desc": "Team-Second-Brain.",
|
||||
"cta": "Business wählen",
|
||||
"feature0": "10 Mitarbeitende",
|
||||
"feature1": "BYOK · 13 Anbieter",
|
||||
"feature1": "Eigene Schlüssel · {count} Anbieter",
|
||||
"feature2": "1.000 semantische Suchen",
|
||||
"feature3": "Agenten (60 Läufe/Monat)",
|
||||
"feature4": "Unbegrenztes Brainstorm",
|
||||
"feature5": "API / MCP"
|
||||
"feature5": "Externe Werkzeuge"
|
||||
},
|
||||
"enterprise": {
|
||||
"name": "Enterprise",
|
||||
@@ -3545,12 +3562,12 @@
|
||||
"feature_search_title": "Semantische Suche",
|
||||
"feature_search_desc": "Finden Sie jede Notiz nach Bedeutung, nicht nur nach Schlüsselwörtern.",
|
||||
"feature_flashcards_title": "KI-Karteikarten",
|
||||
"feature_flashcards_desc": "SRS-Lernkarten aus Ihren Notizen in einem Klick erstellen.",
|
||||
"feature_flashcards_desc": "Lernkarten aus Ihren Notizen in einem Klick erstellen.",
|
||||
"feature_brainstorm_title": "KI-Brainstorming",
|
||||
"feature_brainstorm_desc": "KI-gestützte kollaborative Brainstorming-Sitzungen.",
|
||||
"feature_chat_title": "Mit Ihren Notizen chatten",
|
||||
"feature_chat_desc": "Stellen Sie Ihrer persönlichen Wissensdatenbank Fragen.",
|
||||
"feature_insights_title": "Semantische Einblicke",
|
||||
"feature_insights_title": "Verbindungen",
|
||||
"feature_insights_desc": "Entdecken Sie versteckte Verbindungen zwischen Ihren Ideen.",
|
||||
"feature_export_title": "Markdown-Export",
|
||||
"feature_export_desc": "Importieren und exportieren Sie Ihre Notizen im Markdown-Format.",
|
||||
@@ -3638,9 +3655,10 @@
|
||||
"createDiagramCostHint": "≈ 4 KI-Credits"
|
||||
},
|
||||
"insightsView": {
|
||||
"title": "Semantische Einblicke",
|
||||
"title": "Verbindungen",
|
||||
"toggleMenu": "Menü ein- oder ausblenden",
|
||||
"subtitle": "Entdecke die verborgene Architektur deines Wissens",
|
||||
"resync": "Netzwerk neu syncen",
|
||||
"resync": "Aktualisieren",
|
||||
"mapping": "Kartierung…",
|
||||
"loading": "Deine Notizen werden geladen…",
|
||||
"mappingTitle": "Dein Wissen wird kartiert…",
|
||||
@@ -3800,7 +3818,7 @@
|
||||
"apiKey": "API-Schlüssel",
|
||||
"apiUrl": "API-URL",
|
||||
"baseUrlRequired": "Bitte geben Sie die API-URL an",
|
||||
"byokActive": "BYOK aktiv",
|
||||
"byokActive": "Schlüssel aktiv",
|
||||
"choose": "Wählen…",
|
||||
"chooseModel": "Wähle ein Modell…",
|
||||
"chooseProvider": "Wähle einen Anbieter…",
|
||||
@@ -4011,7 +4029,7 @@
|
||||
"tabProgress": "Fortschritt",
|
||||
"tapToFlip": "Leertaste oder Tippen zum Umdrehen",
|
||||
"toolbarGenerate": "Karteikarten erstellen",
|
||||
"toolbarGenerateHint": "SM-2 Intervallwiederholung",
|
||||
"toolbarGenerateHint": "Sie kommen zur richtigen Zeit zurück",
|
||||
"totalCardsLabel": "Gesamtkarten",
|
||||
"totalReviewsLabel": "Gesamtüberprüfungen",
|
||||
"upToDate": "Aktuell",
|
||||
@@ -4055,6 +4073,9 @@
|
||||
"homeDashboard": {
|
||||
"activityEmptyHint": "Bearbeiten Sie Notizen, um Ihren Schreibrhythmus der letzten 90 Tage zu sehen.",
|
||||
"agentCreated": "Agent erstellt und gestartet",
|
||||
"agentCreatedInCard": "Der Agent ist bereit.",
|
||||
"agentOpenCreated": "Agent öffnen",
|
||||
"agentSeeNextSuggestion": "Nächsten Vorschlag sehen",
|
||||
"agentDiscovery": "Agent",
|
||||
"agentFailed": "Erstellung fehlgeschlagen",
|
||||
"agentsEmpty": "Noch keine Forschungsagenten vorgeschlagen. Schreiben Sie weiter — Memento schlägt Synthese-Themen vor, wenn Ihre Notizen clustern.",
|
||||
@@ -4063,6 +4084,8 @@
|
||||
"aiFound": "KI gefunden",
|
||||
"aiProviderUnavailable": "Die KI ist vorübergehend nicht verfügbar. Überprüfe deine Anbieter-Einstellungen.",
|
||||
"allCaughtUp": "Alles erledigt.",
|
||||
"remindersEmpty": "Keine Erinnerungen für heute.",
|
||||
"remindersOpenAll": "Erinnerungen ansehen",
|
||||
"alreadySeen": "angesehen",
|
||||
"analyzeNotes": "Meine Notizen analysieren",
|
||||
"analyzing": "Analysieren…",
|
||||
@@ -4180,6 +4203,8 @@
|
||||
"pulseReview": "{count} zu überprüfen",
|
||||
"quickCapture": "Schnellerfassung",
|
||||
"quickCapturePlaceholder": "Eine Idee, ein Gedanke… Enter drücken, um im Posteingang zu erfassen.",
|
||||
"captureGoesToFile": "Geht in den Posteingang.",
|
||||
"captureSend": "In den Posteingang senden",
|
||||
"reminders": "Erinnerungen",
|
||||
"resumeAlso": "Auch kürzlich",
|
||||
"resumeEmptyCta": "Eine Idee erfassen",
|
||||
@@ -4251,7 +4276,7 @@
|
||||
"inbox": "Notizen ohne Notizbuch. Ordnen Sie sie ein, um Ihr Second Brain aufgeräumt zu halten.",
|
||||
"intelligence": "KI-Entdeckungen: semantische Verbindungen zwischen Notizen, Brücken-Ideen und Agenten-Ergebnisse.",
|
||||
"link-suggestions": "Passagen aus anderen Notizen, die es wert sind, in Ihre aktuelle Arbeit eingefügt zu werden.",
|
||||
"mind-map": "Themencluster, größen skaliert nach Notizvolumen. Klicken Sie, um in Insights zu erkunden.",
|
||||
"mind-map": "Themencluster, größen skaliert nach Notizvolumen. Klicken Sie, um in Verbindungen zu erkunden.",
|
||||
"next-paths": "Vorgeschlagene nächste Schritte basierend auf Ihrer zuletzt bearbeiteten Notiz: fortsetzen, verknüpfen, verbinden oder recherchieren.",
|
||||
"open-loops": "Notizen, die Sie begonnen, aber seit 3+ Tagen nicht bearbeitet haben.",
|
||||
"pinned": "Schnellzugriff auf angeheftete Notizen.",
|
||||
@@ -4267,6 +4292,7 @@
|
||||
"widgetHide": "Widget ausblenden",
|
||||
"widgetOpen": "Öffnen",
|
||||
"widgetPinnedEmpty": "Noch keine angehefteten Notizen.",
|
||||
"pinnedNoNotebook": "Kein Notizbuch",
|
||||
"widgetReset": "Zurücksetzen",
|
||||
"widgetResetDone": "Standard-Dashboard wiederhergestellt.",
|
||||
"widgetStatsBridges": "Brücken",
|
||||
|
||||
@@ -32,10 +32,10 @@
|
||||
"signOut": "Sign out",
|
||||
"confirmPassword": "Confirm Password",
|
||||
"confirmPasswordPlaceholder": "Confirm your password",
|
||||
"welcomeBack": "Welcome Back",
|
||||
"welcomeBackSubtitle": "Enter your credentials to access your notes.",
|
||||
"createYourSpace": "Create Your Space",
|
||||
"createYourSpaceSubtitle": "Join the new era of smart note-taking.",
|
||||
"welcomeBack": "Welcome back",
|
||||
"welcomeBackSubtitle": "Sign in to find your notes, your connections, and what you forgot.",
|
||||
"createYourSpace": "Create your second brain",
|
||||
"createYourSpaceSubtitle": "It connects while you write — not just another notes app.",
|
||||
"forgot": "Forgot?",
|
||||
"backToSite": "Back to site",
|
||||
"privacyTerms": "© 2025 Memento Labs — Privacy · Terms",
|
||||
@@ -94,12 +94,13 @@
|
||||
"dropToRoot": "Drop here to move to root",
|
||||
"noReminders": "No active reminders.",
|
||||
"documents": "Documents",
|
||||
"dashboardPanelBody": "Your second brain at a glance: AI suggestions, quick capture, and next steps. Use the shortcuts below to jump into action.",
|
||||
"dashboardPanelBody": "An overview of your notes, next steps, and discoveries. Use the counters at the top of the page to act.",
|
||||
"searchNotebooksPlaceholder": "Search notebooks…",
|
||||
"clearSearch": "Clear search",
|
||||
"insightsPanelBody": "Semantic map of your notes: thematic clusters, bridge notes, and connection suggestions.",
|
||||
"revisionPanelBody": "Review flashcards with the SM-2 algorithm. Decks are generated from your notes.",
|
||||
"insightsPanelBody": "A map of how your notes connect: related themes, bridge notes, and links you can open.",
|
||||
"revisionPanelBody": "Review with cards. They come back at the right time. Sets come from your notes.",
|
||||
"backToNotebooks": "Back to notebooks",
|
||||
"backToHome": "Back to home",
|
||||
"resizeNotebooksPanel": "Resize notebooks panel",
|
||||
"resizeSidebar": "Resize sidebar width",
|
||||
"dailyNote": "Daily Note",
|
||||
@@ -130,7 +131,7 @@
|
||||
"add": "Add",
|
||||
"adding": "Adding...",
|
||||
"close": "Close",
|
||||
"confirmDelete": "Are you sure you want to delete this note?",
|
||||
"confirmDelete": "This note will go to the trash. You can restore it later.",
|
||||
"confirmLeaveShare": "Are you sure you want to leave this shared note?",
|
||||
"sharedBy": "Shared by",
|
||||
"sharedShort": "Shared",
|
||||
@@ -333,6 +334,7 @@
|
||||
"optionsMenuAria": "Options menu",
|
||||
"deleteNoteConfirmItem": "Delete note",
|
||||
"noteDeletedToast": "Note deleted.",
|
||||
"undoDelete": "Undo",
|
||||
"deleteNoteFailedToast": "Could not delete.",
|
||||
"documentInfoAria": "Document information",
|
||||
"noModification": "No changes",
|
||||
@@ -1001,7 +1003,7 @@
|
||||
"nav": {
|
||||
"home": "Home",
|
||||
"notes": "Notes",
|
||||
"notebooks": "NOTEBOOKS",
|
||||
"notebooks": "Notebooks",
|
||||
"generalNotes": "General Notes",
|
||||
"archive": "Archive",
|
||||
"settings": "Settings",
|
||||
@@ -1015,7 +1017,7 @@
|
||||
"support": "Support Memento ☕",
|
||||
"reminders": "Reminders",
|
||||
"graphView": "Link map",
|
||||
"insights": "Semantic themes",
|
||||
"insights": "Connections",
|
||||
"revision": "Review",
|
||||
"dailyNotes": "Daily Notes",
|
||||
"dashboard": "Dashboard",
|
||||
@@ -1876,18 +1878,18 @@
|
||||
"featureTags": "Tags",
|
||||
"featureTitles": "Titles",
|
||||
"unlimited": "Unlimited",
|
||||
"remaining": "{count} left",
|
||||
"remaining": "{count} credits left",
|
||||
"upgradeTitle": "Upgrade to Pro",
|
||||
"upgradeDescription": "Your AI credit balance is empty. Upgrade for more monthly credits, or use your own API key (BYOK).",
|
||||
"upgradeDescription": "Your AI credit balance is empty. Upgrade for more monthly credits, or use your own key.",
|
||||
"proIncludes": "Pro includes:",
|
||||
"proSearch": "1,000 AI credits / month",
|
||||
"proTags": "BYOK (your own keys)",
|
||||
"proTags": "Your own provider keys",
|
||||
"proTitles": "Agents (use credits)",
|
||||
"proReformulate": "Unlimited notes",
|
||||
"proChat": "Email support",
|
||||
"later": "Later",
|
||||
"upgradePricing": "Upgrade to Pro",
|
||||
"addApiKey": "Use your own API key (BYOK)",
|
||||
"addApiKey": "Use your own key",
|
||||
"featureReformulate": "Reformulations",
|
||||
"featureChat": "AI Messages",
|
||||
"featureBrainstormCreate": "Brainstorm creations",
|
||||
@@ -2184,14 +2186,14 @@
|
||||
"collapse": "Collapse"
|
||||
},
|
||||
"mcpSettings": {
|
||||
"title": "MCP",
|
||||
"description": "Manage API keys and configure external tools",
|
||||
"tierRequired": "Pro+ only",
|
||||
"upgradeHint": "MCP access (API keys for Cursor, Claude Desktop, etc.) requires a Pro plan or higher. Upgrade in Billing to unlock this feature.",
|
||||
"title": "External tools",
|
||||
"description": "Connect Cursor, Claude, and other tools to your notes",
|
||||
"tierRequired": "Pro plan or higher",
|
||||
"upgradeHint": "Connecting a tool like Cursor to your notes requires a Pro plan or higher. Upgrade in Billing.",
|
||||
"whatIsMcp": {
|
||||
"title": "What is MCP?",
|
||||
"description": "The Model Context Protocol (MCP) is an open protocol that enables AI models to securely interact with external tools and data sources. With MCP, you can connect tools like Claude Code, Cursor, or N8N to your Memento instance to read, create, and organize your notes programmatically.",
|
||||
"learnMore": "Learn more about MCP"
|
||||
"title": "What this is for",
|
||||
"description": "You can open, search, and organize your notes from another tool — Cursor, Claude, n8n — without sharing your password. An access key links the tool to your account.",
|
||||
"learnMore": "Learn more"
|
||||
},
|
||||
"serverStatus": {
|
||||
"title": "Server Status",
|
||||
@@ -2201,10 +2203,10 @@
|
||||
"url": "URL"
|
||||
},
|
||||
"apiKeys": {
|
||||
"title": "API Keys",
|
||||
"description": "API keys allow external tools to access your notes via MCP. Keep your keys secret.",
|
||||
"generate": "Generate a new key",
|
||||
"empty": "No API keys yet. Generate one to get started.",
|
||||
"title": "Access keys",
|
||||
"description": "A key lets an external tool read and write your notes. Do not share it.",
|
||||
"generate": "New key",
|
||||
"empty": "No keys yet. Create one to get started.",
|
||||
"active": "Active",
|
||||
"revoked": "Revoked",
|
||||
"revoke": "Revoke",
|
||||
@@ -2221,42 +2223,42 @@
|
||||
}
|
||||
},
|
||||
"createDialog": {
|
||||
"title": "Generate API Key",
|
||||
"description": "Create a new API key to connect external tools to your notes.",
|
||||
"title": "New access key",
|
||||
"description": "Give it a name so you remember which tool it belongs to.",
|
||||
"nameLabel": "Key name",
|
||||
"namePlaceholder": "e.g. Claude Code, Cursor, N8N",
|
||||
"generating": "Generating...",
|
||||
"generate": "Generate",
|
||||
"successTitle": "API Key Generated",
|
||||
"successDescription": "Copy your API key now. You won't be able to see it again.",
|
||||
"namePlaceholder": "e.g. Cursor, Claude, n8n",
|
||||
"generating": "Creating…",
|
||||
"generate": "Create",
|
||||
"successTitle": "Key created",
|
||||
"successDescription": "Copy it now. You will not be able to see it again.",
|
||||
"copy": "Copy",
|
||||
"copied": "Copied!",
|
||||
"done": "Done"
|
||||
},
|
||||
"configInstructions": {
|
||||
"title": "Configuration Instructions",
|
||||
"description": "Use your API key to configure these tools.",
|
||||
"title": "How to connect a tool",
|
||||
"description": "Paste your key into the tool, with the address shown beside it.",
|
||||
"claudeCode": {
|
||||
"title": "Claude Code",
|
||||
"description": "Add this to your Claude Code MCP configuration file:"
|
||||
"description": "Paste this into your Claude Code configuration file:"
|
||||
},
|
||||
"cursor": {
|
||||
"title": "Cursor",
|
||||
"description": "Add this to your Cursor MCP settings:"
|
||||
"description": "Paste this into Cursor’s tools settings:"
|
||||
},
|
||||
"n8n": {
|
||||
"title": "N8N",
|
||||
"description": "Use these credentials in your N8N MCP node:"
|
||||
"title": "n8n",
|
||||
"description": "Use these details in the n8n connection node:"
|
||||
}
|
||||
},
|
||||
"helpBox": {
|
||||
"title": "What is MCP (Model Context Protocol)?",
|
||||
"step1": "MCP is a protocol that allows Memento's AI agents to connect to external tools (databases, APIs, files, etc.).",
|
||||
"step2": "Memento exposes an MCP server with 22 tools — your agents can read/create notes, search your base, manage notebooks, etc.",
|
||||
"step3": "Create an API key here, then configure it in your MCP client (Claude Desktop, Cursor, Continue.dev…) with the server URL.",
|
||||
"step4": "Configuration format: MCP server URL + your key in the Authorization header.",
|
||||
"step4Link": "MCP Documentation",
|
||||
"step5": "Use case: ask Claude Desktop to write a note in Memento, search your notebooks, or create an agent."
|
||||
"title": "How do I connect a tool?",
|
||||
"step1": "A tool like Cursor or Claude can open, search, and organize your notes, as if you were doing it here.",
|
||||
"step2": "You create an access key on this page. It works as a password for that tool only.",
|
||||
"step3": "In the tool, enter the server address (shown here) and paste the key.",
|
||||
"step4": "The tool then sends your requests here, so you do not have to copy everything by hand.",
|
||||
"step4Link": "Official help",
|
||||
"step5": "Example: ask Cursor to write a note in Memento, or to search your notebooks."
|
||||
}
|
||||
},
|
||||
"agents": {
|
||||
@@ -2276,6 +2278,7 @@
|
||||
"monitor": "Observer",
|
||||
"slideGenerator": "Slides",
|
||||
"excalidrawGenerator": "Diagram",
|
||||
"taskExtractor": "Tasks",
|
||||
"custom": "Custom"
|
||||
},
|
||||
"typeDescriptions": {
|
||||
@@ -2284,6 +2287,7 @@
|
||||
"monitor": "Watches a notebook and analyzes notes",
|
||||
"slideGenerator": "Creates a PowerPoint presentation from notes",
|
||||
"excalidrawGenerator": "Creates an Excalidraw diagram from notes",
|
||||
"taskExtractor": "Finds tasks in your notes and gathers them",
|
||||
"custom": "Free agent with your own prompt"
|
||||
},
|
||||
"form": {
|
||||
@@ -2423,6 +2427,17 @@
|
||||
"title": "Templates",
|
||||
"install": "Install",
|
||||
"installing": "Installing...",
|
||||
"seeAll": "See all",
|
||||
"showLess": "Show less",
|
||||
"categoryAll": "All",
|
||||
"categoryWatch": "News watch",
|
||||
"categoryDigest": "Summaries",
|
||||
"categoryTools": "Tools",
|
||||
"categoryGenerate": "Create",
|
||||
"taskExtractor": {
|
||||
"name": "Tasks from notes",
|
||||
"description": "Finds tasks in your notes and gathers them in one place."
|
||||
},
|
||||
"veilleAI": {
|
||||
"name": "AI Watch",
|
||||
"description": "Scrapes RSS feeds from 6 AI sites (The Verge, TechCrunch, Ars Technica, MIT Tech Review, WIRED, Korben) and generates a weekly summary."
|
||||
@@ -2542,7 +2557,7 @@
|
||||
"slideStyle": "Visual style affects corner radius, spacing, and information density."
|
||||
}
|
||||
},
|
||||
"intelligenceOS": "Intelligence OS"
|
||||
"intelligenceOS": "Agents"
|
||||
},
|
||||
"chat": {
|
||||
"title": "AI Chat",
|
||||
@@ -2662,11 +2677,11 @@
|
||||
"slashTableDesc": "Insert a simple grid",
|
||||
"slashDatabase": "Structured View",
|
||||
"slashDatabaseDesc": "Embed your notebook's structured data",
|
||||
"slashInteractiveDemo": "Interactive Demo",
|
||||
"slashInteractiveDemoDesc": "AI step-by-step teaching demo (Play / Step)",
|
||||
"slashToggle": "Toggle Section",
|
||||
"slashToggleDesc": "Create a collapsible section",
|
||||
"slashCallout": "Callout",
|
||||
"slashInteractiveDemo": "Step-by-step demo",
|
||||
"slashInteractiveDemoDesc": "A guided walkthrough inside the note",
|
||||
"slashToggle": "Collapsible section",
|
||||
"slashToggleDesc": "Create a section you can fold away",
|
||||
"slashCallout": "Highlighted box",
|
||||
"slashCalloutDesc": "Highlight text (info, warning, tip)",
|
||||
"slashOutline": "Table of Contents",
|
||||
"slashOutlineDesc": "Auto-generated outline from your headings",
|
||||
@@ -2834,10 +2849,10 @@
|
||||
"openPublicPage": "Open public page",
|
||||
"slashSubPage": "Sub-page",
|
||||
"slashSubPageDesc": "Create a linked note inside this note",
|
||||
"slashCharts": "AI Charts",
|
||||
"slashChartsDesc": "AI suggests charts",
|
||||
"slashLivingBlock": "Living Block",
|
||||
"slashLivingBlockDesc": "Insert from another note",
|
||||
"slashCharts": "Suggest charts",
|
||||
"slashChartsDesc": "Suggest charts from this note",
|
||||
"slashLivingBlock": "Linked block",
|
||||
"slashLivingBlockDesc": "Open a passage from another note beside this one",
|
||||
"frequentCommands": "★ Frequent",
|
||||
"mobileBlockActions": "Block Actions",
|
||||
"mobileSelectAll": "Select All",
|
||||
@@ -2936,13 +2951,13 @@
|
||||
"deleteImage": "Delete image"
|
||||
},
|
||||
"flashcards": {
|
||||
"generateTitle": "Generate flashcards",
|
||||
"generateTitle": "Create review cards",
|
||||
"generateAction": "Generate with AI",
|
||||
"confirmSave": "Save to deck",
|
||||
"generateFailed": "Could not generate flashcards",
|
||||
"saveFailed": "Could not save flashcards",
|
||||
"schemaMissing": "Flashcards are not available yet on this server (database migration pending).",
|
||||
"savedCount": "{count} flashcards saved",
|
||||
"confirmSave": "Save to this set",
|
||||
"generateFailed": "Could not create the cards",
|
||||
"saveFailed": "Could not save the cards",
|
||||
"schemaMissing": "Review cards are not available yet on this server.",
|
||||
"savedCount": "{count} cards saved",
|
||||
"cardCount": "Number of cards",
|
||||
"styleLabel": "Card style",
|
||||
"style": {
|
||||
@@ -2953,26 +2968,26 @@
|
||||
"previewHint": "Edit cards before saving",
|
||||
"frontPlaceholder": "Question / front",
|
||||
"backPlaceholder": "Answer / back",
|
||||
"toolbarGenerate": "Generate flashcards",
|
||||
"tabDecks": "Decks",
|
||||
"toolbarGenerate": "Create review cards",
|
||||
"tabDecks": "Sets",
|
||||
"tabProgress": "Progress",
|
||||
"emptyDecks": "No flashcard decks yet",
|
||||
"emptyDecksHint": "Open a note and use the graduation cap in the toolbar to generate cards with AI.",
|
||||
"createDeck": "Create deck",
|
||||
"newDeckPlaceholder": "Thematic deck name…",
|
||||
"deckCreated": "Deck created",
|
||||
"emptyDecks": "No review cards yet",
|
||||
"emptyDecksHint": "Open a note, then the review-cards button at the top of the page.",
|
||||
"createDeck": "New set",
|
||||
"newDeckPlaceholder": "Set name…",
|
||||
"deckCreated": "Set created",
|
||||
"dueCount": "{count} due today",
|
||||
"upToDate": "Up to date",
|
||||
"cardCountLabel": "{count} cards",
|
||||
"masteredShort": "mastered",
|
||||
"viewDeck": "Details",
|
||||
"hideDeck": "Hide",
|
||||
"deckCardsEmpty": "This deck has no cards yet.",
|
||||
"deckCardsEmpty": "This set has no cards yet.",
|
||||
"dueBadge": "Due",
|
||||
"masteredBadge": "Mastered",
|
||||
"review": "Review",
|
||||
"startReview": "Start review",
|
||||
"activeDeck": "Active deck",
|
||||
"activeDeck": "Current set",
|
||||
"statTotal": "Total: {count}",
|
||||
"statDue": "Due: {count}",
|
||||
"statMastered": "Mastered: {count}",
|
||||
@@ -2991,11 +3006,20 @@
|
||||
"easy": "Easy (4)"
|
||||
},
|
||||
"sessionComplete": "Session complete",
|
||||
"backToDecks": "Back to decks",
|
||||
"loadDeckFailed": "Could not load deck",
|
||||
"backToDecks": "Back to sets",
|
||||
"loadDeckFailed": "Could not load this set",
|
||||
"reviewFailed": "Could not save review",
|
||||
"heatmapTitle": "Review activity",
|
||||
"heatmapLast90": "Last 90 days",
|
||||
"heatmapTotal": "{count} reviews · 90 days",
|
||||
"heatmapDay": "{count} reviews",
|
||||
"heatmapDayOne": "1 review",
|
||||
"heatmapDayNone": "No reviews",
|
||||
"heatmapHint": "Hover or click a square for details",
|
||||
"heatmapSelected": "selected",
|
||||
"heatmapClear": "Clear selection",
|
||||
"heatmapLess": "Less",
|
||||
"heatmapMore": "More",
|
||||
"retentionRate": "Retention rate",
|
||||
"masteredLabel": "{count}/{total} mastered",
|
||||
"retentionCurve": "Weekly success rate",
|
||||
@@ -3020,15 +3044,15 @@
|
||||
"editCard": "Edit",
|
||||
"deleteCard": "Delete card",
|
||||
"deleteCardConfirm": "Delete this card?",
|
||||
"deleteDeck": "Delete deck",
|
||||
"deleteDeckConfirm": "Delete this deck and all its cards permanently?",
|
||||
"deckDeleted": "Deck deleted",
|
||||
"deleteDeck": "Delete this set",
|
||||
"deleteDeckConfirm": "Delete this set and all its cards?",
|
||||
"deckDeleted": "Set deleted",
|
||||
"cardDeleted": "Card deleted",
|
||||
"cardSaved": "Card saved",
|
||||
"cardTypeBadge": "{type}",
|
||||
"reviewNow": "Review now",
|
||||
"deleteFailed": "Could not delete",
|
||||
"toolbarGenerateHint": "SM-2 Spaced Repetition"
|
||||
"toolbarGenerateHint": "They come back at the right time"
|
||||
},
|
||||
"structuredViews": {
|
||||
"enableTitle": "Organize this notebook (table, kanban, gallery…)",
|
||||
@@ -3324,10 +3348,10 @@
|
||||
"legendConverted": "Converted"
|
||||
},
|
||||
"byokSettings": {
|
||||
"title": "Your API keys (BYOK)",
|
||||
"description": "Connect your own provider keys. With BYOK, usage typically does not consume your Memento credits. Keys are encrypted at rest.",
|
||||
"badgeActive": "BYOK active",
|
||||
"tierRequired": "BYOK requires a Pro plan or higher. Upgrade to connect your API keys.",
|
||||
"title": "Your provider keys",
|
||||
"description": "Connect your own provider keys. Usage with your key typically does not consume your Memento credits. Keys are encrypted at rest.",
|
||||
"badgeActive": "Keys active",
|
||||
"tierRequired": "This option requires a Pro plan or higher.",
|
||||
"provider": "Provider",
|
||||
"providerPlaceholder": "Select a provider",
|
||||
"alias": "Label (optional)",
|
||||
@@ -3382,11 +3406,11 @@
|
||||
"businessAnnualPrice": "€299",
|
||||
"proFeature1": "Unlimited notes",
|
||||
"proFeature2": "1,000 AI credits / month",
|
||||
"proFeature3": "BYOK (your own keys)",
|
||||
"proFeature3": "Your own provider keys",
|
||||
"proFeature4": "Agents (use credits)",
|
||||
"businessFeature1": "10 collaborators included",
|
||||
"businessFeature2": "4,000 AI credits / month",
|
||||
"businessFeature3": "BYOK · 13 providers",
|
||||
"businessFeature3": "Your own keys · {count} providers",
|
||||
"businessFeature4": "Agents & brainstorm (credits)",
|
||||
"enterpriseTitle": "Enterprise",
|
||||
"enterpriseDescription": "Unlimited credits or dedicated pool, SSO, priority support.",
|
||||
@@ -3423,6 +3447,9 @@
|
||||
"billingHistory": "Billing History",
|
||||
"viewInvoices": "Manage invoices in portal",
|
||||
"cancelSubscription": "Cancel subscription",
|
||||
"changeOffer": "Change plan",
|
||||
"downgradeToFree": "Switch to the free plan",
|
||||
"cancellingNotice": "Cancellation scheduled — access until {date}",
|
||||
"nextBillingDate": "Next billing date",
|
||||
"billingPeriod": "Billing period",
|
||||
"planSince": "Member since",
|
||||
@@ -3458,7 +3485,7 @@
|
||||
"proFeature5": "30-day history",
|
||||
"proFeature6": "Email support",
|
||||
"proCta": "Upgrade to Pro",
|
||||
"businessFeature5": "API / MCP",
|
||||
"businessFeature5": "External tools",
|
||||
"businessFeature6": "Priority support",
|
||||
"businessCta": "Upgrade to Business",
|
||||
"recommended": "Recommended",
|
||||
@@ -3539,78 +3566,79 @@
|
||||
"echo": {
|
||||
"eyebrow": "Memory Echo",
|
||||
"title": "The moment your Second Brain talks back.",
|
||||
"desc": "While you write, Memento detects semantic links across notebooks — not keyword matches, real conceptual bridges you can open in a split peek.",
|
||||
"desc": "While you write, Memento finds links across your notebooks — not just matching words, real connections. You can open both notes side by side.",
|
||||
"card0Label": "Just detected",
|
||||
"card0": "\"Your note on pricing mirrors the competitor teardown from last autumn.\"",
|
||||
"card0": "\"Your note on prices echoes last autumn's competitor analysis.\"",
|
||||
"card1Label": "Bridge",
|
||||
"card1": "Shared theme: positioning under constraint",
|
||||
"card2Label": "Action",
|
||||
"card2": "Open both notes side by side — keep writing"
|
||||
},
|
||||
"dashboard": {
|
||||
"eyebrow": "Second Brain dashboard",
|
||||
"eyebrow": "This morning",
|
||||
"title": "A morning that tells you what matters.",
|
||||
"desc": "Your Second Brain briefing: next paths, checklist, discoveries, revisions — widgets you arrange yourself.",
|
||||
"desc": "Your morning briefing: next paths, today's list, discoveries, reviews — cards you arrange.",
|
||||
"w0Label": "Next paths",
|
||||
"w0": "3 actions from your last note",
|
||||
"w1Label": "Daily review",
|
||||
"w1": "Inbox · AI · Flashcards",
|
||||
"w1": "Inbox · Discoveries · Cards",
|
||||
"w2Label": "Memory Echo",
|
||||
"w2": "2 new connections overnight",
|
||||
"w3Label": "Agents",
|
||||
"w3": "1 suggestion ready to run"
|
||||
},
|
||||
"insights": {
|
||||
"eyebrow": "Insights",
|
||||
"title": "See the architecture of your Second Brain.",
|
||||
"desc": "Semantic clusters and bridge notes — a living map of how your knowledge actually connects.",
|
||||
"chip": "Cluster network"
|
||||
"eyebrow": "Connections",
|
||||
"title": "See how your notes connect.",
|
||||
"desc": "Related themes and bridge notes — a living map of your links.",
|
||||
"chip": "Map of links"
|
||||
},
|
||||
"revision": {
|
||||
"eyebrow": "Revision",
|
||||
"title": "Remember on purpose.",
|
||||
"desc": "Generate flashcards from any note. SM-2 spaced repetition built in — knowledge that sticks in your Second Brain.",
|
||||
"card": "What connects Memory Echo to your notes?"
|
||||
"desc": "Create review cards from any note. They come back at the right time.",
|
||||
"card": "What connects Memory Echo to your notes?",
|
||||
"badge": "At the right time"
|
||||
}
|
||||
},
|
||||
"how": {
|
||||
"title": "Three steps. Then your Second Brain compounds.",
|
||||
"title": "Three steps. Then it builds on itself.",
|
||||
"s0": {
|
||||
"title": "Capture freely",
|
||||
"desc": "Write in a next-gen editor — blocks, structured views, smart paste. Clip the web when you need it."
|
||||
"desc": "Write like in a notebook: titles, lists, tables. Save a web page when you need it."
|
||||
},
|
||||
"s1": {
|
||||
"title": "Let it connect",
|
||||
"desc": "Memory Echo and semantic search weave links you didn't plan. Your past work becomes fuel."
|
||||
"desc": "Memory Echo and search weave links you didn't plan. Your past work becomes fuel."
|
||||
},
|
||||
"s2": {
|
||||
"title": "Act every morning",
|
||||
"desc": "Dashboard paths, agents, and flashcards turn a pile of notes into Second Brain momentum."
|
||||
"desc": "Morning paths, agents, and review cards turn a pile of notes into momentum."
|
||||
}
|
||||
},
|
||||
"agents": {
|
||||
"label": "AI agents",
|
||||
"title": "Delegate the heavy lifting.",
|
||||
"desc": "Research, scrape, slides, diagrams, monitoring — agents that write into your Second Brain, not another chat tab you'll forget.",
|
||||
"desc": "Research, monitoring, presentations, diagrams — agents that write into your notebooks, not another chat you'll forget.",
|
||||
"scraper": {
|
||||
"title": "Scraper",
|
||||
"desc": "URLs & RSS → synthesized notes with smart images."
|
||||
"title": "Monitor",
|
||||
"desc": "Addresses and feeds become notes, with the useful images."
|
||||
},
|
||||
"researcher": {
|
||||
"title": "Researcher",
|
||||
"desc": "Deep queries, sources, structured research notes."
|
||||
"desc": "Deep questions, sources, structured notes."
|
||||
},
|
||||
"slideGen": {
|
||||
"title": "Slide Gen",
|
||||
"desc": "Notes → decks or interactive HTML slides."
|
||||
"title": "Slides",
|
||||
"desc": "Your notes become a presentation, or a page to walk through."
|
||||
},
|
||||
"monitor": {
|
||||
"title": "Monitor",
|
||||
"desc": "Watch notebooks for trends and new insights."
|
||||
"title": "Observer",
|
||||
"desc": "It watches your notebooks: trends and new ideas."
|
||||
},
|
||||
"diagramGen": {
|
||||
"title": "Diagram Gen",
|
||||
"desc": "Ideas → Excalidraw mind maps & flows."
|
||||
"title": "Diagram",
|
||||
"desc": "Your ideas become mind maps and flows."
|
||||
},
|
||||
"custom": {
|
||||
"title": "Custom",
|
||||
@@ -3618,9 +3646,12 @@
|
||||
}
|
||||
},
|
||||
"byok": {
|
||||
"label": "No lock-in",
|
||||
"label": "Free to switch",
|
||||
"title": "Your keys. Your models. Your Second Brain.",
|
||||
"desc": "Use Memento credits or plug OpenAI, Anthropic, Google and more. Switch providers in a click — the product stays yours."
|
||||
"desc": "Memento credits, or your own provider. Switch in a click — the product stays yours.",
|
||||
"pointCredits": "Memento credits, if you want it simple",
|
||||
"pointProvider": "Or connect your own provider",
|
||||
"pointYours": "Your key stays with you — it is never shown here"
|
||||
},
|
||||
"pricing": {
|
||||
"label": "Pricing",
|
||||
@@ -3655,7 +3686,7 @@
|
||||
"cta": "Go Pro",
|
||||
"feature0": "Unlimited notes",
|
||||
"feature1": "1,000 AI credits / month",
|
||||
"feature2": "BYOK (your own keys)",
|
||||
"feature2": "Your own provider keys",
|
||||
"feature3": "Agents (use credits)",
|
||||
"feature4": "30-day history",
|
||||
"feature5": "Email support"
|
||||
@@ -3666,9 +3697,9 @@
|
||||
"cta": "Choose Business",
|
||||
"feature0": "10 collaborators",
|
||||
"feature1": "4,000 AI credits / month",
|
||||
"feature2": "BYOK · 13 providers",
|
||||
"feature2": "Your own keys · {count} providers",
|
||||
"feature3": "Agents & brainstorm (credits)",
|
||||
"feature4": "API / MCP",
|
||||
"feature4": "External tools",
|
||||
"feature5": "Priority support"
|
||||
},
|
||||
"enterprise": {
|
||||
@@ -3766,9 +3797,10 @@
|
||||
}
|
||||
},
|
||||
"insightsView": {
|
||||
"title": "Semantic Insights",
|
||||
"title": "Connections",
|
||||
"toggleMenu": "Show or hide the menu",
|
||||
"subtitle": "Discover the hidden architecture of your knowledge",
|
||||
"resync": "Re-sync network",
|
||||
"resync": "Update",
|
||||
"mapping": "Mapping…",
|
||||
"loading": "Loading your notes…",
|
||||
"mappingTitle": "Mapping your knowledge…",
|
||||
@@ -3781,7 +3813,7 @@
|
||||
"analysisFailed": "Analysis failed. Check your AI settings or try again.",
|
||||
"analysisSuccess": "Analysis complete: {count} themes detected.",
|
||||
"analysisNoClusters": "No themes detected yet.",
|
||||
"staleResults": "Showing results from the last analysis. Many notes changed since then — click “Resynchronize network” to refresh.",
|
||||
"staleResults": "Showing results from the last analysis. Many notes have changed since then — click Update.",
|
||||
"semanticGraphLegend": "Detected themes overview (not the link map)",
|
||||
"fitGraphView": "Fit view",
|
||||
"legendFilterPlaceholder": "Filter themes…",
|
||||
@@ -3794,7 +3826,7 @@
|
||||
"clusterFallback": "Theme {index}",
|
||||
"unclusteredNotes": "{count} notes not assigned to a theme (hidden from graph).",
|
||||
"emptyTitle": "Discover your knowledge clusters",
|
||||
"emptyDescription": "Click \"Re-sync network\" to analyze your notes and find hidden connections",
|
||||
"emptyDescription": "Click Update to analyze your notes and find hidden connections",
|
||||
"stats": {
|
||||
"clusters": "Themes",
|
||||
"bridgeNotes": "Bridge notes",
|
||||
@@ -3813,14 +3845,14 @@
|
||||
"affinity": "Affinity {score}%",
|
||||
"scoreHint": "Mean semantic affinity to the two themes this note bridges (cosine similarity).",
|
||||
"moreThemes": "+{count}",
|
||||
"needsResync": "Re-sync the network to refresh bridge pairs.",
|
||||
"needsResync": "Click Update to refresh theme pairs.",
|
||||
"empty": "No significant bridge notes yet. Deepen your research to find new connections."
|
||||
},
|
||||
"suggestions": {
|
||||
"title": "Missing links",
|
||||
"bridging": "Link {clusterA} & {clusterB}",
|
||||
"emptyTitle": "No connection suggestions yet",
|
||||
"emptyDescription": "No strong near-miss theme pairs right now — or re-sync to refresh predictions.",
|
||||
"emptyDescription": "No strong near-miss theme pairs right now — or click Update.",
|
||||
"createNote": "Create bridge note",
|
||||
"created": "Bridge note created",
|
||||
"createError": "Could not create the bridge note",
|
||||
@@ -3931,7 +3963,7 @@
|
||||
"whatWillBeDeleted": "The following will be permanently deleted:",
|
||||
"item1": "All notes, notebooks, and attachments",
|
||||
"item2": "All pgvector semantic embeddings",
|
||||
"item3": "All BYOK API keys",
|
||||
"item3": "All your provider keys",
|
||||
"item4": "All AI conversations and brainstorm sessions",
|
||||
"item5": "Quota and usage history",
|
||||
"item6": "Your Stripe subscription (if active)",
|
||||
@@ -4132,13 +4164,13 @@
|
||||
"step_features_cta": "Let's go!",
|
||||
"feature_search_title": "Semantic search",
|
||||
"feature_search_desc": "Find any note by meaning, not just keywords.",
|
||||
"feature_flashcards_title": "AI Flashcards",
|
||||
"feature_flashcards_desc": "Generate SRS review cards from your notes in one click.",
|
||||
"feature_flashcards_title": "Review cards",
|
||||
"feature_flashcards_desc": "Create review cards from your notes, in one click.",
|
||||
"feature_brainstorm_title": "AI Brainstorm",
|
||||
"feature_brainstorm_desc": "AI-powered collaborative brainstorming sessions.",
|
||||
"feature_chat_title": "Chat with your notes",
|
||||
"feature_chat_desc": "Ask questions to your personal knowledge base.",
|
||||
"feature_insights_title": "Semantic insights",
|
||||
"feature_insights_title": "Connections",
|
||||
"feature_insights_desc": "Discover hidden connections between your ideas.",
|
||||
"feature_export_title": "Markdown export",
|
||||
"feature_export_desc": "Import and export your notes in standard Markdown format.",
|
||||
@@ -4148,14 +4180,14 @@
|
||||
"import_notes_ready": "{count} note(s) imported!",
|
||||
"action_write_title": "Write your first real note",
|
||||
"action_write_desc": "Create a note and start capturing your ideas.",
|
||||
"action_flashcards_title": "Generate your first flashcards",
|
||||
"action_flashcards_desc": "Open a note and click the flashcards button.",
|
||||
"action_flashcards_title": "Create your first review cards",
|
||||
"action_flashcards_desc": "Open a note, then the review-cards button at the top of the page.",
|
||||
"action_brainstorm_title": "Start an AI brainstorm",
|
||||
"action_brainstorm_desc": "Explore your ideas with a dedicated AI agent.",
|
||||
"action_try": "Try",
|
||||
"step_features_cta_all": "All done — let's dive in!",
|
||||
"action_write_where": "Close this → click \"+ New note\" in the sidebar",
|
||||
"action_flashcards_where": "Close this → open a note → 🃏 button in the toolbar",
|
||||
"action_flashcards_where": "Close this, open a note, then the review-cards button at the top of the page.",
|
||||
"action_brainstorm_where": "Close this → \"Canvas\" section in the sidebar",
|
||||
"pill_resume": "✨ Resume tour",
|
||||
"action_done": "Tried!",
|
||||
@@ -4167,8 +4199,8 @@
|
||||
"hint_ai_desc": "Click the ✨ button in the toolbar to open the AI panel — ask questions, summarize, rewrite, or brainstorm directly in your note.",
|
||||
"hint_version_title": "Version history",
|
||||
"hint_version_desc": "Click the ⓘ button in the toolbar → \"Versions\" tab. Enable versioning, then save and restore snapshots of your note at any time.",
|
||||
"hint_flashcards_title": "Generate flashcards",
|
||||
"hint_flashcards_desc": "Click the 🎓 button in the toolbar to auto-generate flashcards from your note for spaced repetition review.",
|
||||
"hint_flashcards_title": "Create review cards",
|
||||
"hint_flashcards_desc": "At the top of the note, click the review-cards button. They come back at the right time.",
|
||||
"hint_links_title": "Links between notes",
|
||||
"hint_links_desc": "Type \"[[\" in the editor to search and link to another note. Linked notes appear as backlinks at the bottom of the note.",
|
||||
"hint_create_note_title": "Create a note",
|
||||
@@ -4176,9 +4208,9 @@
|
||||
"hint_flip_title": "Flip the card",
|
||||
"hint_flip_desc": "Press Space (or click the card) to flip it and reveal the answer.",
|
||||
"hint_rate_keys_title": "Rate with keyboard",
|
||||
"hint_rate_keys_desc": "After flipping, press 1 (Hard), 2 (Difficult), 3 (Good) or 4 (Easy) to rate the card. The SM-2 algorithm schedules your next review automatically.",
|
||||
"hint_rate_keys_desc": "After flipping, press 1 (Hard), 2 (Difficult), 3 (Good) or 4 (Easy). The next review is scheduled at the right time.",
|
||||
"hint_generate_from_note_title": "Generate from a note",
|
||||
"hint_generate_from_note_desc": "Open any note and click the 🎓 button in the toolbar to automatically generate flashcards from its content.",
|
||||
"hint_generate_from_note_desc": "Open a note, then the review-cards button at the top of the page.",
|
||||
"hint_brainstorm_start_title": "Start with an idea",
|
||||
"hint_brainstorm_start_desc": "Type any concept or question in the input field and press Enter. The AI will generate a set of ideas around it.",
|
||||
"hint_brainstorm_deepen_title": "Deepen an idea",
|
||||
@@ -4256,15 +4288,17 @@
|
||||
"homeDashboard": {
|
||||
"title": "Dashboard",
|
||||
"quickCapture": "Quick Capture",
|
||||
"quickCapturePlaceholder": "An idea, a thought… Enter to capture to Inbox.",
|
||||
"captured": "Captured to Inbox",
|
||||
"quickCapturePlaceholder": "An idea, a thought… Enter sends it to the inbox.",
|
||||
"captureGoesToFile": "Goes to the inbox.",
|
||||
"captureSend": "Send to the inbox",
|
||||
"captured": "Added to the inbox.",
|
||||
"captureError": "Failed",
|
||||
"mindMap": "Mind map",
|
||||
"fullMap": "Full map →",
|
||||
"mindMapEmpty": "No themes detected yet. Semantic analysis groups your notes by topic.",
|
||||
"mindMapOpen": "Open insights map →",
|
||||
"mindMapUnavailable": "Mind map unavailable.",
|
||||
"themes": "themes",
|
||||
"themes": "Themes",
|
||||
"bridges": "bridges",
|
||||
"aiEchoes": "AI echoes",
|
||||
"bridgeNote": "Bridge note",
|
||||
@@ -4274,6 +4308,9 @@
|
||||
"createAgent": "Create agent",
|
||||
"agentsEmpty": "No research agents suggested yet. Keep writing — Memento will propose synthesis topics when your notes cluster.",
|
||||
"agentCreated": "Agent created and started",
|
||||
"agentCreatedInCard": "The agent is ready.",
|
||||
"agentOpenCreated": "Open the agent",
|
||||
"agentSeeNextSuggestion": "See the next one",
|
||||
"agentFailed": "Failed to create agent",
|
||||
"notes": "notes",
|
||||
"continue": "Continue",
|
||||
@@ -4288,8 +4325,11 @@
|
||||
"notEnoughNotes": "Not enough notes this week.",
|
||||
"toReview": "To review",
|
||||
"allCaughtUp": "All caught up.",
|
||||
"remindersEmpty": "No reminders for today.",
|
||||
"remindersOpenAll": "See reminders",
|
||||
"toOrganize": "to organize",
|
||||
"inboxSeeAll": "See all {count}",
|
||||
"inboxOpenList": "Open the inbox",
|
||||
"inboxEmpty": "Inbox is empty.",
|
||||
"review": "Review",
|
||||
"cardsDue": "cards due",
|
||||
@@ -4342,7 +4382,15 @@
|
||||
"widgetStatsNotes": "Notes",
|
||||
"widgetAgentActivityEmpty": "No agent activity in the last 48 hours.",
|
||||
"widgetPinnedEmpty": "No pinned notes yet.",
|
||||
"pinnedNoNotebook": "No notebook",
|
||||
"widgetActivityHint": "Notes edited over the last 90 days.",
|
||||
"activityHeatmapTitle": "Notes edited",
|
||||
"activityHeatmapLast90": "Last 90 days",
|
||||
"activityHeatmapTotal": "{count} notes · 90 days",
|
||||
"activityHeatmapDay": "{count} notes",
|
||||
"activityHeatmapDayOne": "1 note",
|
||||
"activityHeatmapDayNone": "No notes that day",
|
||||
"activityHeatmapHint": "Hover or click a square to see the day",
|
||||
"widgetUsageHint": "Single AI credit balance (all features).",
|
||||
"briefingLoadError": "Could not load the dashboard.",
|
||||
"briefingRetry": "Retry",
|
||||
@@ -4362,7 +4410,7 @@
|
||||
"agents": "Suggested research",
|
||||
"sentiment": "Sentiment",
|
||||
"inbox": "Inbox",
|
||||
"revision": "Flashcards",
|
||||
"revision": "Review cards",
|
||||
"stats": "Semantic stats",
|
||||
"agent-activity": "Agent activity",
|
||||
"gmail": "Gmail captures",
|
||||
@@ -4386,7 +4434,7 @@
|
||||
"agents": "AI-suggested research agents for your topics.",
|
||||
"sentiment": "Emotional tone of your notes this week.",
|
||||
"inbox": "Notes waiting to be filed into notebooks.",
|
||||
"revision": "Flashcards due for spaced repetition review.",
|
||||
"revision": "Cards due today. They come back at the right time.",
|
||||
"stats": "Clusters, bridge notes, and total indexed notes.",
|
||||
"agent-activity": "Latest completed agent runs.",
|
||||
"gmail": "Email captures synced from Gmail.",
|
||||
@@ -4412,18 +4460,19 @@
|
||||
"dailyReviewInbox": "Process inbox",
|
||||
"dailyReviewDiscoveries": "Review AI discoveries",
|
||||
"dailyReviewConnection": "Explore one connection",
|
||||
"dailyReviewCards": "Review flashcards",
|
||||
"dailyReviewCards": "Review cards",
|
||||
"openLoopsEmpty": "No stalled notes — you're on track.",
|
||||
"openLoopsStale": "{days}d ago",
|
||||
"dailyNoteOpen": "Open journal",
|
||||
"linkSuggestionsEmpty": "Edit a note to get link suggestions.",
|
||||
"bridgesEmpty": "No bridge opportunities right now.",
|
||||
"flashRetention": "Mastered",
|
||||
"flashStreak": "Streak",
|
||||
"flashTotal": "Cards",
|
||||
"flashEmptyHint": "No flashcards yet. Generate some from a note (graduation-cap icon in the editor), then track your progress here.",
|
||||
"flashRetention": "Remembered",
|
||||
"flashRetentionHint": "Share of cards you remember well.",
|
||||
"flashStreak": "Days in a row",
|
||||
"flashTotal": "In all",
|
||||
"flashEmptyHint": "No cards yet. Create some from a note, then track your progress here.",
|
||||
"flashEmptyCta": "Open reviews",
|
||||
"flashDueCta": "{count} cards due today",
|
||||
"flashDueCta": "Review now",
|
||||
"flashOpenCta": "View progress",
|
||||
"activityEmptyHint": "Edit notes to see your writing rhythm over the last 90 days.",
|
||||
"pathTypes": {
|
||||
@@ -4441,24 +4490,25 @@
|
||||
"widgetHelpLabel": "Widget help",
|
||||
"widgetHelpClose": "Close",
|
||||
"sentimentDominant": "Dominant tone this week",
|
||||
"sentimentFromNotes": "From your notes",
|
||||
"widgetHelp": {
|
||||
"capture": "Jot down a thought in one line. It lands in your Inbox — classify it during your daily review.",
|
||||
"next-paths": "Suggested next steps based on your latest edited note: resume, link, bridge, or research.",
|
||||
"resume": "Your most recently updated notes. Pick up where you left off.",
|
||||
"intelligence": "AI discoveries: semantic links between notes, bridge ideas, and agent findings.",
|
||||
"reminders": "Upcoming note reminders. Empty when everything is on schedule.",
|
||||
"mind-map": "Theme clusters sized by note volume. Click to explore in Insights.",
|
||||
"mind-map": "Theme clusters sized by note volume. Click to explore in Connections.",
|
||||
"agents": "AI-suggested research agents for topics you write about often.",
|
||||
"sentiment": "Emotional tone of notes edited in the last 7 days. Requires at least 3 recent notes and AI enabled.",
|
||||
"inbox": "Notes without a notebook yet. File them to keep your second brain tidy.",
|
||||
"revision": "Flashcards due for spaced-repetition review today.",
|
||||
"revision": "Cards due today. They come back at the right time.",
|
||||
"stats": "Semantic index stats: active themes, bridge notes, total indexed notes.",
|
||||
"agent-activity": "Agents that completed a run in the last 48 hours.",
|
||||
"gmail": "Email captures synced from Gmail integration.",
|
||||
"activity": "Heatmap of notes you edited over the last 90 days.",
|
||||
"pinned": "Quick access to notes you pinned.",
|
||||
"usage": "Monthly AI credit usage by feature.",
|
||||
"daily-review": "10-minute morning checklist: inbox, discoveries, one connection, flashcards.",
|
||||
"daily-review": "10-minute morning list: inbox, discoveries, one connection, cards.",
|
||||
"open-loops": "Notes you started but haven't touched in 3+ days.",
|
||||
"daily-note": "Today's journal entry — one note per day.",
|
||||
"link-suggestions": "Passages from other notes worth linking into your current work.",
|
||||
@@ -4706,7 +4756,7 @@
|
||||
"contactError": "Could not contact server",
|
||||
"baseUrlRequired": "Please provide the API URL",
|
||||
"keyInvalid": "Invalid API key",
|
||||
"byokActive": "BYOK active",
|
||||
"byokActive": "Keys active",
|
||||
"activeStatus": "● Active",
|
||||
"inactiveStatus": "○ Inactive",
|
||||
"confirmDelete": "Delete the {{name}} key?",
|
||||
|
||||
@@ -31,14 +31,14 @@
|
||||
"confirmPasswordPlaceholder": "Confirme su contraseña",
|
||||
"backToSite": "Atrás",
|
||||
"continueWithGoogle": "Continuar con Google",
|
||||
"createYourSpace": "Crea tu espacio",
|
||||
"createYourSpaceSubtitle": "Únete a la nueva era de la toma de notas inteligente.",
|
||||
"createYourSpace": "Crea tu segundo cerebro",
|
||||
"createYourSpaceSubtitle": "Se conecta mientras escribes — no es otra app de notas.",
|
||||
"forgot": "¿Olvidaste?",
|
||||
"oauthAccountNotLinked": "Esta cuenta de Google no coincide con tu cuenta existente. Usa el mismo correo o inicia sesión con tu contraseña.",
|
||||
"privacyTerms": "© 2025 Memento Labs — Privacidad · Términos",
|
||||
"sessionExpired": "Tu sitio se genera con navegación y tabla de contenidos",
|
||||
"welcomeBack": "Bienvenido de nuevo",
|
||||
"welcomeBackSubtitle": "Introduce tus credenciales para acceder a tus notas.",
|
||||
"welcomeBackSubtitle": "Entra para recuperar tus notas, tus conexiones y lo que habías olvidado.",
|
||||
"checkEmailTitle": "Revisa tu correo",
|
||||
"checkEmailDescription": "Enviamos un enlace de confirmación a {email}. Ábrelo para activar tu cuenta antes de iniciar sesión.",
|
||||
"checkEmailDescriptionGeneric": "Enviamos un enlace de confirmación a tu correo. Ábrelo para activar tu cuenta antes de iniciar sesión.",
|
||||
@@ -99,13 +99,13 @@
|
||||
"darkMode": "Modo oscuro",
|
||||
"dashboardPanelBody": "Tu sesión ha expirado. Por favor, inicia sesión de nuevo.",
|
||||
"documents": "Documentos",
|
||||
"insightsPanelBody": "Mapa semántico de tus notas: clusters temáticos, notas puente y sugerencias de conexión.",
|
||||
"insightsPanelBody": "Un mapa de cómo se conectan tus notas: temas cercanos, notas puente y enlaces que puedes abrir.",
|
||||
"lightMode": "Modo claro",
|
||||
"notebookEmpty": "Vacío",
|
||||
"recentNote": "Creadas recientemente",
|
||||
"resizeNotebooksPanel": "Redimensionar panel de cuadernos",
|
||||
"resizeSidebar": "Redimensionar ancho de la barra lateral",
|
||||
"revisionPanelBody": "Revisa flashcards con el algoritmo SM-2. Los mazos se generan a partir de tus notas.",
|
||||
"revisionPanelBody": "Repasa con tarjetas. La repetición espaciada las trae de vuelta en el momento adecuado. Los mazos salen de tus notas.",
|
||||
"searchNotebooksPlaceholder": "Buscar cuadernos…",
|
||||
"searchShortcut": "Buscar (Ctrl+K)"
|
||||
},
|
||||
@@ -130,7 +130,7 @@
|
||||
"add": "Agregar",
|
||||
"adding": "Agregando...",
|
||||
"close": "Cerrar",
|
||||
"confirmDelete": "¿Estás seguro de que quieres eliminar esta nota?",
|
||||
"confirmDelete": "Esta nota irá a la papelera. Podrás recuperarla después.",
|
||||
"confirmLeaveShare": "¿Estás seguro de que quieres abandonar esta nota compartida?",
|
||||
"sharedBy": "Compartido por",
|
||||
"sharedShort": "Compartido",
|
||||
@@ -991,7 +991,7 @@
|
||||
"dailyNotes": "Notas diarias",
|
||||
"dashboard": "Panel",
|
||||
"graphView": "Mapa de enlaces",
|
||||
"insights": "Temas semánticos",
|
||||
"insights": "Conexiones",
|
||||
"revision": "Revisar"
|
||||
},
|
||||
"settings": {
|
||||
@@ -2095,14 +2095,14 @@
|
||||
"collapse": "Colapsar"
|
||||
},
|
||||
"mcpSettings": {
|
||||
"title": "MCP",
|
||||
"title": "Herramientas externas",
|
||||
"description": "Gestiona tus claves API y configura herramientas externas",
|
||||
"tierRequired": "Solo Pro+",
|
||||
"upgradeHint": "El acceso MCP (claves API para Cursor, Claude Desktop, etc.) requiere un plan Pro o superior. Actualiza en Facturación para desbloquear esta función.",
|
||||
"whatIsMcp": {
|
||||
"title": "¿Qué es MCP?",
|
||||
"title": "¿Para qué sirve?",
|
||||
"description": "El Model Context Protocol (MCP) es un protocolo abierto que permite a los modelos de IA interactuar de forma segura con herramientas y fuentes de datos externas. Con MCP, puedes conectar herramientas como Claude Code, Cursor o N8N a tu instancia de Memento para leer, crear y organizar tus notas mediante programación.",
|
||||
"learnMore": "Más información sobre MCP"
|
||||
"learnMore": "Más información"
|
||||
},
|
||||
"serverStatus": {
|
||||
"title": "Estado del servidor",
|
||||
@@ -2161,12 +2161,12 @@
|
||||
}
|
||||
},
|
||||
"helpBox": {
|
||||
"title": "¿Qué es MCP (Model Context Protocol)?",
|
||||
"title": "¿Cómo conectar una herramienta?",
|
||||
"step1": "MCP es un protocolo que permite a los agentes IA de Memento conectarse a herramientas externas (bases de datos, APIs, archivos, etc.).",
|
||||
"step2": "Memento expone un servidor MCP con 22 herramientas — tus agentes pueden leer/crear notas, buscar en tu base, gestionar cuadernos, etc.",
|
||||
"step3": "Crea una clave API aquí y configúrala en tu cliente MCP (Claude Desktop, Cursor, Continue.dev…) con la URL del servidor.",
|
||||
"step4": "Formato de configuración: URL del servidor MCP + tu clave en el header Authorization.",
|
||||
"step4Link": "Documentación MCP",
|
||||
"step4Link": "Ayuda oficial",
|
||||
"step5": "Caso de uso: pide a Claude Desktop que escriba una nota en Memento, busque en tus cuadernos o cree un agente."
|
||||
}
|
||||
},
|
||||
@@ -2334,6 +2334,17 @@
|
||||
"title": "Plantillas",
|
||||
"install": "Instalar",
|
||||
"installing": "Instalando...",
|
||||
"seeAll": "Ver todos",
|
||||
"showLess": "Ver menos",
|
||||
"categoryAll": "Todos",
|
||||
"categoryWatch": "Seguimiento",
|
||||
"categoryDigest": "Resúmenes",
|
||||
"categoryTools": "Herramientas",
|
||||
"categoryGenerate": "Crear",
|
||||
"taskExtractor": {
|
||||
"name": "Tareas en las notas",
|
||||
"description": "Encuentra las tareas en tus notas y las reúne en un solo lugar."
|
||||
},
|
||||
"veilleAI": {
|
||||
"name": "Vigilancia IA",
|
||||
"description": "Extrae contenido de 5 sitios especializados en IA y genera un resumen semanal."
|
||||
@@ -2453,7 +2464,7 @@
|
||||
"slideStyle": "El estilo visual afecta el radio de las esquinas, el espaciado y la densidad de la información."
|
||||
}
|
||||
},
|
||||
"intelligenceOS": "Sistema operativo inteligente"
|
||||
"intelligenceOS": "Agentes"
|
||||
},
|
||||
"chat": {
|
||||
"title": "Chat IA",
|
||||
@@ -2740,7 +2751,7 @@
|
||||
"slashDatabaseDesc": "Incrusta los datos estructurados de tu cuaderno",
|
||||
"slashLinkPreview": "Vista previa de enlace",
|
||||
"slashLinkPreviewDesc": "Convertir una URL en una tarjeta visual",
|
||||
"slashLivingBlock": "Bloque vivo",
|
||||
"slashLivingBlock": "Bloque enlazado",
|
||||
"slashLivingBlockDesc": "Insertar desde otra nota",
|
||||
"slashMath": "Ecuación",
|
||||
"slashMathDesc": "Fórmula matemática en notación LaTeX",
|
||||
@@ -3007,7 +3018,7 @@
|
||||
"proChat": "100 chat messages / month",
|
||||
"later": "Más tarde",
|
||||
"upgradePricing": "Actualizar a Pro",
|
||||
"addApiKey": "Usa tu propia clave API (BYOK)",
|
||||
"addApiKey": "Usa tu propia clave",
|
||||
"featureBrainstormCreate": "Créations brainstorm",
|
||||
"featureBrainstormEnrich": "Enrichissements brainstorm",
|
||||
"featureBrainstormExpand": "Extensions brainstorm",
|
||||
@@ -3025,10 +3036,10 @@
|
||||
"outOfCredits": "Sin créditos — opciones"
|
||||
},
|
||||
"byokSettings": {
|
||||
"title": "Tus claves API (BYOK)",
|
||||
"title": "Tus claves de proveedor",
|
||||
"description": "Connect your own LLM provider keys to bypass Discovery Pack quotas. Keys are encrypted at rest.",
|
||||
"badgeActive": "BYOK activo",
|
||||
"tierRequired": "BYOK requiere un plan Pro o superior. Actualiza para conectar tus claves API.",
|
||||
"badgeActive": "Claves activas",
|
||||
"tierRequired": "Esta opción requiere un plan Pro o superior.",
|
||||
"provider": "Proveedor",
|
||||
"providerPlaceholder": "Selecciona un proveedor",
|
||||
"alias": "Etiqueta (opcional)",
|
||||
@@ -3167,6 +3178,9 @@
|
||||
"fetchInvoicesFailed": "No se pudo cargar el historial de facturación.",
|
||||
"savePercent": "Ahorra ~17%",
|
||||
"cancelSubscription": "Cancelar suscripción",
|
||||
"changeOffer": "Cambiar de oferta",
|
||||
"downgradeToFree": "Volver a la oferta gratuita",
|
||||
"cancellingNotice": "Baja prevista — acceso hasta el {date}",
|
||||
"disabledByAdmin": "La facturación y las mejoras de plan están deshabilitadas. Contacta con tu administrador si necesitas acceso.",
|
||||
"tab": "Facturación",
|
||||
"creditsFromPacks": "Créditos de paquetes",
|
||||
@@ -3239,13 +3253,13 @@
|
||||
"card2": "Abrir ambas notas lado a lado — sigue escribiendo"
|
||||
},
|
||||
"dashboard": {
|
||||
"eyebrow": "Dashboard Second Brain",
|
||||
"eyebrow": "La mañana",
|
||||
"title": "Una mañana que te dice lo que importa.",
|
||||
"desc": "El briefing de tu Second Brain: próximos caminos, checklist, descubrimientos, revisión.",
|
||||
"w0Label": "Próximos caminos",
|
||||
"w0": "3 acciones desde tu última nota",
|
||||
"w1Label": "Revisión diaria",
|
||||
"w1": "Inbox · IA · Flashcards",
|
||||
"w1": "Bandeja · Hallazgos · Tarjetas",
|
||||
"w2Label": "Memory Echo",
|
||||
"w2": "2 conexiones nuevas esta noche",
|
||||
"w3Label": "Agentes",
|
||||
@@ -3284,34 +3298,37 @@
|
||||
"title": "Delega el trabajo pesado.",
|
||||
"desc": "Investigación, scrape, slides, diagramas, monitoreo — agentes que escriben en tu Second Brain.",
|
||||
"scraper": {
|
||||
"title": "Scraper",
|
||||
"title": "Monitor",
|
||||
"desc": "URLs y RSS → notas sintetizadas con imágenes."
|
||||
},
|
||||
"researcher": {
|
||||
"title": "Researcher",
|
||||
"title": "Investigador",
|
||||
"desc": "Consultas profundas, fuentes, notas de investigación."
|
||||
},
|
||||
"slideGen": {
|
||||
"title": "Slide Gen",
|
||||
"title": "Diapositivas",
|
||||
"desc": "Notas → decks o slides HTML interactivas."
|
||||
},
|
||||
"monitor": {
|
||||
"title": "Monitor",
|
||||
"title": "Observador",
|
||||
"desc": "Vigila cuadernos: tendencias e insights."
|
||||
},
|
||||
"diagramGen": {
|
||||
"title": "Diagram Gen",
|
||||
"title": "Diagrama",
|
||||
"desc": "Ideas → mindmaps y flows Excalidraw."
|
||||
},
|
||||
"custom": {
|
||||
"title": "Custom",
|
||||
"title": "Personalizado",
|
||||
"desc": "Tus roles, fuentes y horarios."
|
||||
}
|
||||
},
|
||||
"byok": {
|
||||
"label": "Sin lock-in",
|
||||
"label": "Libre de cambiar",
|
||||
"title": "Tus claves. Tus modelos. Tu Second Brain.",
|
||||
"desc": "Créditos Memento u OpenAI, Anthropic, Google… Cambia de proveedor en un clic."
|
||||
"desc": "Créditos Memento, o tu propio proveedor. Cambia en un clic: el producto sigue siendo tuyo.",
|
||||
"pointCredits": "Créditos Memento, si quieres algo simple",
|
||||
"pointProvider": "O conecta tu propio proveedor",
|
||||
"pointYours": "Tu clave se queda contigo: nunca se muestra aquí"
|
||||
},
|
||||
"pricing": {
|
||||
"label": "Pricing",
|
||||
@@ -3339,7 +3356,7 @@
|
||||
"desc": "Para mentes exigentes.",
|
||||
"cta": "Pasar a Pro",
|
||||
"feature0": "Notas ilimitadas",
|
||||
"feature1": "BYOK",
|
||||
"feature1": "Tus propias claves",
|
||||
"feature2": "200 búsquedas semánticas / mes",
|
||||
"feature3": "Agentes (12 runs/mes)",
|
||||
"feature4": "Historial 30 días",
|
||||
@@ -3350,11 +3367,11 @@
|
||||
"desc": "Second Brain de equipo.",
|
||||
"cta": "Elegir Business",
|
||||
"feature0": "10 colaboradores",
|
||||
"feature1": "BYOK · 13 proveedores",
|
||||
"feature1": "Tus claves · {count} proveedores",
|
||||
"feature2": "1.000 búsquedas semánticas",
|
||||
"feature3": "Agentes (60 runs/mes)",
|
||||
"feature4": "Brainstorm ilimitado",
|
||||
"feature5": "API / MCP"
|
||||
"feature5": "Herramientas externas"
|
||||
},
|
||||
"enterprise": {
|
||||
"name": "Enterprise",
|
||||
@@ -3545,12 +3562,12 @@
|
||||
"feature_search_title": "Búsqueda semántica",
|
||||
"feature_search_desc": "Encuentra cualquier nota por significado, no solo por palabras clave.",
|
||||
"feature_flashcards_title": "Tarjetas IA",
|
||||
"feature_flashcards_desc": "Genera tarjetas de repaso SRS desde tus notas con un clic.",
|
||||
"feature_flashcards_desc": "Genera tarjetas de repaso desde tus notas con un clic.",
|
||||
"feature_brainstorm_title": "Brainstorming IA",
|
||||
"feature_brainstorm_desc": "Sesiones de lluvia de ideas colaborativas con IA.",
|
||||
"feature_chat_title": "Chatea con tus notas",
|
||||
"feature_chat_desc": "Haz preguntas a tu base de conocimiento personal.",
|
||||
"feature_insights_title": "Perspectivas semánticas",
|
||||
"feature_insights_title": "Conexiones",
|
||||
"feature_insights_desc": "Descubre conexiones ocultas entre tus ideas.",
|
||||
"feature_export_title": "Exportación Markdown",
|
||||
"feature_export_desc": "Importa y exporta tus notas en formato Markdown estándar.",
|
||||
@@ -3588,7 +3605,7 @@
|
||||
"hint_flip_title": "Voltear la tarjeta",
|
||||
"hint_flip_desc": "Pulsa Espacio (o haz clic en la tarjeta) para voltearla y revelar la respuesta.",
|
||||
"hint_rate_keys_title": "Valorar con el teclado",
|
||||
"hint_rate_keys_desc": "Después de voltear, pulsa 1 (Difícil), 2 (Difícil), 3 (Bien) o 4 (Fácil) para valorar la tarjeta. El algoritmo SM-2 programa tu próxima revisión automáticamente.",
|
||||
"hint_rate_keys_desc": "Después de voltear, pulsa 1 (Difícil), 2 (Difícil), 3 (Bien) o 4 (Fácil). La próxima revisión se coloca en el momento adecuado.",
|
||||
"hint_generate_from_note_title": "Generar desde una nota",
|
||||
"hint_generate_from_note_desc": "Abre cualquier nota y haz clic en el botón 🎓 de la barra de herramientas para generar automáticamente flashcards a partir de su contenido.",
|
||||
"hint_brainstorm_start_title": "Empezar con una idea",
|
||||
@@ -3638,9 +3655,10 @@
|
||||
"createDiagramCostHint": "≈ 4 créditos de IA"
|
||||
},
|
||||
"insightsView": {
|
||||
"title": "Insights semánticos",
|
||||
"title": "Conexiones",
|
||||
"toggleMenu": "Mostrar u ocultar el menú",
|
||||
"subtitle": "Descubre la arquitectura oculta de tu conocimiento",
|
||||
"resync": "Resincronizar red",
|
||||
"resync": "Actualizar",
|
||||
"mapping": "Mapeando…",
|
||||
"loading": "Cargando tus notas…",
|
||||
"mappingTitle": "Mapeando tu conocimiento…",
|
||||
@@ -3800,7 +3818,7 @@
|
||||
"apiKey": "Clave API",
|
||||
"apiUrl": "URL de API",
|
||||
"baseUrlRequired": "Proporciona la URL de la API",
|
||||
"byokActive": "BYOK activo",
|
||||
"byokActive": "Claves activas",
|
||||
"choose": "Elegir…",
|
||||
"chooseModel": "Elige un modelo…",
|
||||
"chooseProvider": "Elige un proveedor…",
|
||||
@@ -4011,7 +4029,7 @@
|
||||
"tabProgress": "Progreso",
|
||||
"tapToFlip": "Espacio o toca para voltear",
|
||||
"toolbarGenerate": "Generar flashcards",
|
||||
"toolbarGenerateHint": "Repetición espaciada SM-2",
|
||||
"toolbarGenerateHint": "Vuelven en el momento adecuado",
|
||||
"totalCardsLabel": "Total de tarjetas",
|
||||
"totalReviewsLabel": "Total de revisiones",
|
||||
"upToDate": "Actualizado",
|
||||
@@ -4055,6 +4073,9 @@
|
||||
"homeDashboard": {
|
||||
"activityEmptyHint": "Edita notas para ver tu ritmo de escritura en los últimos 90 días.",
|
||||
"agentCreated": "Agente creado e iniciado",
|
||||
"agentCreatedInCard": "El agente está listo.",
|
||||
"agentOpenCreated": "Abrir el agente",
|
||||
"agentSeeNextSuggestion": "Ver la siguiente",
|
||||
"agentDiscovery": "Agente",
|
||||
"agentFailed": "Error al crear",
|
||||
"agentsEmpty": "Aún no se han sugerido agentes de investigación. Sigue escribiendo — Memento propondrá temas de síntesis cuando tus notas se agrupen.",
|
||||
@@ -4063,6 +4084,8 @@
|
||||
"aiFound": "IA encontró",
|
||||
"aiProviderUnavailable": "La IA no está disponible temporalmente. Revisa la configuración de tu proveedor.",
|
||||
"allCaughtUp": "Todo al día.",
|
||||
"remindersEmpty": "Ningún recordatorio para hoy.",
|
||||
"remindersOpenAll": "Ver recordatorios",
|
||||
"alreadySeen": "visto",
|
||||
"analyzeNotes": "Analizar mis notas",
|
||||
"analyzing": "Analizando…",
|
||||
@@ -4180,6 +4203,8 @@
|
||||
"pulseReview": "{count} para revisar",
|
||||
"quickCapture": "Captura rápida",
|
||||
"quickCapturePlaceholder": "Una idea, un pensamiento… Pulsa Enter para capturar en la bandeja.",
|
||||
"captureGoesToFile": "Va a la bandeja.",
|
||||
"captureSend": "Enviar a la bandeja",
|
||||
"reminders": "Recordatorios",
|
||||
"resumeAlso": "También recientemente",
|
||||
"resumeEmptyCta": "Capturar una idea",
|
||||
@@ -4192,7 +4217,7 @@
|
||||
"suggestedBridge": "Conectando {clusterA} & {clusterB}",
|
||||
"suggestedResearch": "Investigación sugerida",
|
||||
"theme": "Tema",
|
||||
"themes": "temas",
|
||||
"themes": "Temas",
|
||||
"title": "Panel",
|
||||
"toOrganize": "para organizar",
|
||||
"toReview": "Para revisar",
|
||||
@@ -4251,7 +4276,7 @@
|
||||
"inbox": "Notas sin cuaderno aún. Archívalas para mantener tu segundo cerebro ordenado.",
|
||||
"intelligence": "Descubrimientos IA: enlaces semánticos entre notas, ideas puente y hallazgos de agentes.",
|
||||
"link-suggestions": "Pasajes de otras notas que vale la pena enlazar a tu trabajo actual.",
|
||||
"mind-map": "Clústeres de temas dimensionados por volumen de notas. Haz clic para explorar en Insights.",
|
||||
"mind-map": "Clústeres de temas dimensionados por volumen de notas. Haz clic para explorar en Conexiones.",
|
||||
"next-paths": "Próximos pasos sugeridos basados en tu última nota editada: reanudar, enlazar, conectar o investigar.",
|
||||
"open-loops": "Notas que empezaste pero no has tocado en 3+ días.",
|
||||
"pinned": "Acceso rápido a tus notas fijadas.",
|
||||
@@ -4267,6 +4292,7 @@
|
||||
"widgetHide": "Ocultar widget",
|
||||
"widgetOpen": "Abrir",
|
||||
"widgetPinnedEmpty": "Aún sin notas fijadas.",
|
||||
"pinnedNoNotebook": "Sin cuaderno",
|
||||
"widgetReset": "Restablecer",
|
||||
"widgetResetDone": "Panel por defecto restaurado.",
|
||||
"widgetStatsBridges": "Puentes",
|
||||
|
||||
@@ -31,14 +31,14 @@
|
||||
"confirmPasswordPlaceholder": "رمز عبور را دوباره وارد کنید",
|
||||
"backToSite": "بازگشت",
|
||||
"continueWithGoogle": "ادامه با گوگل",
|
||||
"createYourSpace": "فضای خود را ایجاد کنید",
|
||||
"createYourSpaceSubtitle": "به عصر جدید یادداشتبرداری هوشمند بپیوندید.",
|
||||
"createYourSpace": "مغز دوم خود را بسازید",
|
||||
"createYourSpaceSubtitle": "همانطور که مینویسید متصل میشود — فقط یک برنامهٔ یادداشت دیگر نیست.",
|
||||
"forgot": "فراموش کردهاید؟",
|
||||
"oauthAccountNotLinked": "این حساب گوگل با حساب موجود شما مطابقت ندارد. از همان ایمیل استفاده کنید یا با رمز عبور خود وارد شوید.",
|
||||
"privacyTerms": "© ۲۰۲۵ Memento Labs — حریم خصوصی · شرایط",
|
||||
"sessionExpired": "سایت شما با ناوبری و فهرست مطالب تولید میشود",
|
||||
"welcomeBack": "خوش آمدید",
|
||||
"welcomeBackSubtitle": "اعتبارنامههای خود را برای دسترسی به یادداشتهایتان وارد کنید.",
|
||||
"welcomeBackSubtitle": "وارد شوید تا یادداشتها، پیوندها و آنچه فراموش کرده بودید را پیدا کنید.",
|
||||
"checkEmailTitle": "ایمیل خود را بررسی کنید",
|
||||
"checkEmailDescription": "لینک تأیید را به {email} فرستادیم. قبل از ورود آن را باز کنید تا حساب فعال شود.",
|
||||
"checkEmailDescriptionGeneric": "لینک تأیید را به ایمیل شما فرستادیم. قبل از ورود آن را باز کنید تا حساب فعال شود.",
|
||||
@@ -99,13 +99,13 @@
|
||||
"darkMode": "حالت تاریک",
|
||||
"dashboardPanelBody": "نشست شما منقضی شده است. لطفاً دوباره وارد شوید.",
|
||||
"documents": "اسناد",
|
||||
"insightsPanelBody": "نقشه معنایی یادداشتهای شما: خوشههای موضوعی، یادداشتهای پل و پیشنهادهای ارتباط.",
|
||||
"insightsPanelBody": "نقشهٔ پیوند یادداشتها: موضوعهای نزدیک، یادداشتهای پل، و پیوندهایی که میتوانید باز کنید.",
|
||||
"lightMode": "حالت روشن",
|
||||
"notebookEmpty": "خالی",
|
||||
"recentNote": "تازه ایجاد شده",
|
||||
"resizeNotebooksPanel": "تغییر اندازه پانل دفترچهها",
|
||||
"resizeSidebar": "تغییر عرض نوار کناری",
|
||||
"revisionPanelBody": "فلشکارتها را با الگوریتم SM-2 مرور کنید. دستهها از یادداشتهای شما تولید میشوند.",
|
||||
"revisionPanelBody": "با فلشکارت مرور کنید. تکرار با فاصله آنها را در زمان درست برمیگرداند. دستهها از یادداشتهای شما ساخته میشوند.",
|
||||
"searchNotebooksPlaceholder": "جستجوی دفترچهها…",
|
||||
"searchShortcut": "جستجو (Ctrl+K)"
|
||||
},
|
||||
@@ -130,7 +130,7 @@
|
||||
"add": "افزودن",
|
||||
"adding": "در حال افزودن...",
|
||||
"close": "بستن",
|
||||
"confirmDelete": "آیا واقعاً میخواهید این یادداشت را حذف کنید؟",
|
||||
"confirmDelete": "این یادداشت به سطل زباله میرود. بعداً میتوانید آن را برگردانید.",
|
||||
"confirmLeaveShare": "آیا مطمئن هستید که میخواهید این یادداشت اشتراکی را ترک کنید؟",
|
||||
"sharedBy": "به اشتراک گذاشته توسط",
|
||||
"sharedShort": "به اشتراک گذاشته شده است",
|
||||
@@ -991,7 +991,7 @@
|
||||
"dailyNotes": "یادداشتهای روزانه",
|
||||
"dashboard": "داشبورد",
|
||||
"graphView": "نقشه پیوندها",
|
||||
"insights": "موضوعات معنایی",
|
||||
"insights": "پیوندها",
|
||||
"revision": "مرور"
|
||||
},
|
||||
"settings": {
|
||||
@@ -2095,14 +2095,14 @@
|
||||
"collapse": "جمع کردن"
|
||||
},
|
||||
"mcpSettings": {
|
||||
"title": "MCP",
|
||||
"title": "ابزارهای بیرونی",
|
||||
"description": "مدیریت کلیدهای API و پیکربندی ابزارهای خارجی",
|
||||
"tierRequired": "فقط Pro+",
|
||||
"upgradeHint": "دسترسی به MCP (کلیدهای API برای Cursor، Claude Desktop و غیره) نیاز به طرح Pro یا بالاتر دارد. برای فعال کردن این قابلیت، در بخش صورتحساب ارتقا دهید.",
|
||||
"whatIsMcp": {
|
||||
"title": "MCP چیست؟",
|
||||
"title": "این به چه کار میآید؟",
|
||||
"description": "پروتکل زمینه مدل (MCP) یک پروتکل باز است که به مدلهای هوش مصنوعی امکان تعامل امن با ابزارها و منابع داده خارجی را میدهد. با MCP میتوانید ابزارهایی مانند Claude Code، Cursor یا N8N را به نمونه Memento خود متصل کنید تا یادداشتهای خود را به صورت برنامهنویسی بخوانید، ایجاد کنید و سازماندهی کنید.",
|
||||
"learnMore": "بیشتر درباره MCP بدانید"
|
||||
"learnMore": "بیشتر بدانید"
|
||||
},
|
||||
"serverStatus": {
|
||||
"title": "وضعیت سرور",
|
||||
@@ -2161,12 +2161,12 @@
|
||||
}
|
||||
},
|
||||
"helpBox": {
|
||||
"title": "MCP (Model Context Protocol) چیست؟",
|
||||
"title": "چطور یک ابزار را وصل کنم؟",
|
||||
"step1": "MCP یک پروتکل است که به عاملهای هوش مصنوعی Memento اجازه میدهد به ابزارهای خارجی (پایگاه داده، API، فایلها و غیره) متصل شوند.",
|
||||
"step2": "Memento یک سرور MCP با ۲۲ ابزار ارائه میدهد — عاملهای شما میتوانند یادداشت بخوانید/ایجاد کنید، در پایگاه داده جستجو کنید، دفترچهها را مدیریت کنید و غیره.",
|
||||
"step3": "اینجا یک کلید API ایجاد کنید، سپس آن را در کلاینت MCP خود (Claude Desktop، Cursor، Continue.dev…) با URL سرور پیکربندی کنید.",
|
||||
"step4": "فرمت پیکربندی: URL سرور MCP + کلید شما در هدر Authorization.",
|
||||
"step4Link": "مستندات MCP",
|
||||
"step4Link": "راهنمای رسمی",
|
||||
"step5": "کاربرد: از Claude Desktop بخواهید یک یادداشت در Memento بنویسد، در دفترچههای شما جستجو کند یا یک عامل ایجاد کند."
|
||||
}
|
||||
},
|
||||
@@ -2334,6 +2334,17 @@
|
||||
"title": "قالبها",
|
||||
"install": "نصب",
|
||||
"installing": "در حال نصب...",
|
||||
"seeAll": "دیدن همه",
|
||||
"showLess": "نمایش کمتر",
|
||||
"categoryAll": "همه",
|
||||
"categoryWatch": "رصد",
|
||||
"categoryDigest": "خلاصهها",
|
||||
"categoryTools": "ابزارها",
|
||||
"categoryGenerate": "ساختن",
|
||||
"taskExtractor": {
|
||||
"name": "کارها در یادداشتها",
|
||||
"description": "کارها را در یادداشتهایتان پیدا میکند و در یک جا جمع میکند."
|
||||
},
|
||||
"veilleAI": {
|
||||
"name": "پایش هوش مصنوعی",
|
||||
"description": "از ۵ سایت تخصصی هوش مصنوعی استخراج و خلاصه هفتگی تولید میکند."
|
||||
@@ -2453,7 +2464,7 @@
|
||||
"slideStyle": "سبک بصری بر شعاع گوشه، فاصله و تراکم اطلاعات تأثیر می گذارد."
|
||||
}
|
||||
},
|
||||
"intelligenceOS": "سیستم عامل هوشمند"
|
||||
"intelligenceOS": "عاملها"
|
||||
},
|
||||
"chat": {
|
||||
"title": "چت هوش مصنوعی",
|
||||
@@ -2740,7 +2751,7 @@
|
||||
"slashDatabaseDesc": "دادههای ساختاریافته دفترچه خود را جاسازی کنید",
|
||||
"slashLinkPreview": "پیشنمایش پیوند",
|
||||
"slashLinkPreviewDesc": "تبدیل یک URL به کارت بصری",
|
||||
"slashLivingBlock": "بلوک زنده",
|
||||
"slashLivingBlock": "بلوک پیوندی",
|
||||
"slashLivingBlockDesc": "درج از یادداشت دیگر",
|
||||
"slashMath": "معادله",
|
||||
"slashMathDesc": "فرمول ریاضی به نمادگذاری LaTeX",
|
||||
@@ -3003,7 +3014,7 @@
|
||||
"proChat": "100 chat messages / month",
|
||||
"later": "بعداً",
|
||||
"upgradePricing": "ارتقا به Pro",
|
||||
"addApiKey": "از کلید API خودتان استفاده کنید (BYOK)",
|
||||
"addApiKey": "از کلید خودتان استفاده کنید",
|
||||
"featureReformulate": "بازنویسی",
|
||||
"featureChat": "پیامهای هوش مصنوعی",
|
||||
"featureBrainstormCreate": "ایجاد طوفان فکری",
|
||||
@@ -3025,10 +3036,10 @@
|
||||
"outOfCredits": "اعتبار تمام شد — گزینهها"
|
||||
},
|
||||
"byokSettings": {
|
||||
"title": "کلیدهای API شما (BYOK)",
|
||||
"title": "کلیدهای ارائهدهنده شما",
|
||||
"description": "Connect your own LLM provider keys to bypass Discovery Pack quotas. Keys are encrypted at rest.",
|
||||
"badgeActive": "BYOK فعال",
|
||||
"tierRequired": "BYOK به طرح Pro یا بالاتر نیاز دارد. ارتقا دهید تا کلیدهای API خود را متصل کنید.",
|
||||
"badgeActive": "کلیدها فعالاند",
|
||||
"tierRequired": "این گزینه به طرح Pro یا بالاتر نیاز دارد.",
|
||||
"provider": "ارائهدهنده",
|
||||
"providerPlaceholder": "یک ارائهدهنده انتخاب کنید",
|
||||
"alias": "برچسب (اختیاری)",
|
||||
@@ -3168,6 +3179,9 @@
|
||||
"fetchInvoicesFailed": "بارگذاری تاریخچه صورتحساب ناموفق بود.",
|
||||
"savePercent": "~۱۷٪ صرفهجویی",
|
||||
"cancelSubscription": "لغو اشتراک",
|
||||
"changeOffer": "تغییر طرح",
|
||||
"downgradeToFree": "بازگشت به طرح رایگان",
|
||||
"cancellingNotice": "لغو برنامهریزی شده — دسترسی تا {date}",
|
||||
"disabledByAdmin": "صورتحساب و ارتقاء طرح در حال حاضر غیرفعال است. اگر به دسترسی نیاز دارید با مدیر خود تماس بگیرید.",
|
||||
"creditsFromPacks": "اعتبار بستهها",
|
||||
"creditsRemaining": "اعتبار باقیمانده",
|
||||
@@ -3284,34 +3298,37 @@
|
||||
"title": "کار سنگین را واگذار کنید.",
|
||||
"desc": "پژوهش، اسکراپ، اسلاید، نمودار، پایش — عاملهایی که در مغز دوم مینویسند.",
|
||||
"scraper": {
|
||||
"title": "Scraper",
|
||||
"title": "پایشگر",
|
||||
"desc": "آدرس و RSS → یادداشتهای سنتزشده با تصویر."
|
||||
},
|
||||
"researcher": {
|
||||
"title": "Researcher",
|
||||
"title": "پژوهشگر",
|
||||
"desc": "پرسوجوی عمیق، منابع، یادداشت پژوهشی ساختیافته."
|
||||
},
|
||||
"slideGen": {
|
||||
"title": "Slide Gen",
|
||||
"title": "اسلایدها",
|
||||
"desc": "یادداشت → ارائه یا اسلاید HTML تعاملی."
|
||||
},
|
||||
"monitor": {
|
||||
"title": "Monitor",
|
||||
"title": "ناظر",
|
||||
"desc": "دفترها را پایش میکند: روند و بینش."
|
||||
},
|
||||
"diagramGen": {
|
||||
"title": "Diagram Gen",
|
||||
"title": "نمودار",
|
||||
"desc": "ایده → مایندمپ و فلو Excalidraw."
|
||||
},
|
||||
"custom": {
|
||||
"title": "Custom",
|
||||
"title": "سفارشی",
|
||||
"desc": "نقشها، منابع و زمانبندی خودتان."
|
||||
}
|
||||
},
|
||||
"byok": {
|
||||
"label": "بدون قفلشدن",
|
||||
"title": "کلیدهای شما. مدلهای شما. مغز دوم شما.",
|
||||
"desc": "اعتبار Memento یا OpenAI، Anthropic، Google… با یک کلیک عوض کنید."
|
||||
"desc": "اعتبار Memento، یا ارائهدهندهٔ خودتان. با یک کلیک عوض کنید — محصول مال شما میماند.",
|
||||
"pointCredits": "اگر ساده میخواهید، اعتبار Memento",
|
||||
"pointProvider": "یا ارائهدهندهٔ خودتان را وصل کنید",
|
||||
"pointYours": "کلید پیش خودتان میماند — اینجا هرگز نشان داده نمیشود"
|
||||
},
|
||||
"pricing": {
|
||||
"label": "Pricing",
|
||||
@@ -3339,7 +3356,7 @@
|
||||
"desc": "برای ذهنهای جدی.",
|
||||
"cta": "برو به Pro",
|
||||
"feature0": "یادداشت نامحدود",
|
||||
"feature1": "BYOK",
|
||||
"feature1": "کلیدهای خودتان",
|
||||
"feature2": "۲۰۰ جستجوی معنایی / ماه",
|
||||
"feature3": "عاملها (۱۲ اجرا/ماه)",
|
||||
"feature4": "تاریخچه ۳۰ روز",
|
||||
@@ -3350,11 +3367,11 @@
|
||||
"desc": "مغز دوم تیمی.",
|
||||
"cta": "انتخاب Business",
|
||||
"feature0": "۱۰ همکار",
|
||||
"feature1": "BYOK · ۱۳ ارائهدهنده",
|
||||
"feature1": "کلیدهای شما · {count} ارائهدهنده",
|
||||
"feature2": "۱۰۰۰ جستجوی معنایی",
|
||||
"feature3": "عاملها (۶۰ اجرا/ماه)",
|
||||
"feature4": "طوفان فکری نامحدود",
|
||||
"feature5": "API / MCP"
|
||||
"feature5": "ابزارهای بیرونی"
|
||||
},
|
||||
"enterprise": {
|
||||
"name": "Enterprise",
|
||||
@@ -3545,12 +3562,12 @@
|
||||
"feature_search_title": "جستجوی معنایی",
|
||||
"feature_search_desc": "هر یادداشتی را بر اساس معنا پیدا کنید، نه فقط کلمات کلیدی.",
|
||||
"feature_flashcards_title": "فلشکارتهای هوش مصنوعی",
|
||||
"feature_flashcards_desc": "کارتهای مرور SRS را با یک کلیک از یادداشتهایتان بسازید.",
|
||||
"feature_flashcards_desc": "کارتهای مرور را با یک کلیک از یادداشتهایتان بسازید.",
|
||||
"feature_brainstorm_title": "طوفان فکری هوش مصنوعی",
|
||||
"feature_brainstorm_desc": "جلسات طوفان فکری مشارکتی با پشتیبانی هوش مصنوعی.",
|
||||
"feature_chat_title": "گفتگو با یادداشتها",
|
||||
"feature_chat_desc": "از پایگاه دانش شخصی خود سوال بپرسید.",
|
||||
"feature_insights_title": "بینشهای معنایی",
|
||||
"feature_insights_title": "پیوندها",
|
||||
"feature_insights_desc": "ارتباطهای پنهان بین ایدههایتان را کشف کنید.",
|
||||
"feature_export_title": "خروجی Markdown",
|
||||
"feature_export_desc": "یادداشتهایتان را با فرمت Markdown استاندارد وارد و صادر کنید.",
|
||||
@@ -3638,9 +3655,10 @@
|
||||
"createDiagramCostHint": "≈ ۴ اعتبار هوش مصنوعی"
|
||||
},
|
||||
"insightsView": {
|
||||
"title": "بینشهای معنایی",
|
||||
"title": "پیوندها",
|
||||
"toggleMenu": "نمایش یا پنهانکردن منو",
|
||||
"subtitle": "معماری پنهان دانش خود را کشف کنید",
|
||||
"resync": "همگامسازی مجدد شبکه",
|
||||
"resync": "بهروزرسانی",
|
||||
"mapping": "در حال نقشهبرداری…",
|
||||
"loading": "در حال بارگذاری یادداشتها…",
|
||||
"mappingTitle": "در حال نقشهبرداری دانش شما…",
|
||||
@@ -3800,7 +3818,7 @@
|
||||
"apiKey": "کلید API",
|
||||
"apiUrl": "آدرس API",
|
||||
"baseUrlRequired": "URL API را ارائه دهید",
|
||||
"byokActive": "BYOK فعال",
|
||||
"byokActive": "کلیدها فعالاند",
|
||||
"choose": "انتخاب…",
|
||||
"chooseModel": "یک مدل انتخاب کنید…",
|
||||
"chooseProvider": "یک تأمینکننده انتخاب کنید…",
|
||||
@@ -4011,7 +4029,7 @@
|
||||
"tabProgress": "پیشرفت",
|
||||
"tapToFlip": "فاصله یا ضربه برای چرخش",
|
||||
"toolbarGenerate": "تولید فلشکارت",
|
||||
"toolbarGenerateHint": "تکرار با فاصله SM-2",
|
||||
"toolbarGenerateHint": "در زمان درست برمیگردند",
|
||||
"totalCardsLabel": "مجموع کارتها",
|
||||
"totalReviewsLabel": "مجموع مرورها",
|
||||
"upToDate": "بهروز",
|
||||
@@ -4055,6 +4073,9 @@
|
||||
"homeDashboard": {
|
||||
"activityEmptyHint": "یادداشتها را ویرایش کنید تا ریتم نوشتن خود را در ۹۰ روز گذشته ببینید.",
|
||||
"agentCreated": "عامل ایجاد و آغاز شد",
|
||||
"agentCreatedInCard": "عامل آماده است.",
|
||||
"agentOpenCreated": "باز کردن عامل",
|
||||
"agentSeeNextSuggestion": "دیدن پیشنهاد بعدی",
|
||||
"agentDiscovery": "عامل",
|
||||
"agentFailed": "ایجاد ناموفق بود",
|
||||
"agentsEmpty": "هنوز عامل تحقیقی پیشنهاد نشده. ادامه دهید — Memento زمانی که یادداشتهای شما خوشهبندی شوند موضوعات ترکیبی پیشنهاد میدهد.",
|
||||
@@ -4063,6 +4084,8 @@
|
||||
"aiFound": "هوش مصنوعی یافت",
|
||||
"aiProviderUnavailable": "هوش مصنوعی موقتاً در دسترس نیست. تنظیمات تأمینکننده خود را بررسی کنید.",
|
||||
"allCaughtUp": "همه بهروز.",
|
||||
"remindersEmpty": "یادآوری برای امروز نیست.",
|
||||
"remindersOpenAll": "دیدن یادآورها",
|
||||
"alreadySeen": "دیدهشده",
|
||||
"analyzeNotes": "تحلیل یادداشتهای من",
|
||||
"analyzing": "در حال تجزیه و تحلیل…",
|
||||
@@ -4180,6 +4203,8 @@
|
||||
"pulseReview": "{count} برای بررسی",
|
||||
"quickCapture": "ضبط سریع",
|
||||
"quickCapturePlaceholder": "یک ایده، یک فکر… برای ثبت در صندوق ورودی Enter بزنید.",
|
||||
"captureGoesToFile": "به صندوق ورودی میرود.",
|
||||
"captureSend": "ارسال به صندوق ورودی",
|
||||
"reminders": "یادآورها",
|
||||
"resumeAlso": "همچنین اخیراً",
|
||||
"resumeEmptyCta": "ثبت یک ایده",
|
||||
@@ -4267,6 +4292,7 @@
|
||||
"widgetHide": "پنهان کردن ویجت",
|
||||
"widgetOpen": "باز کردن",
|
||||
"widgetPinnedEmpty": "هنوز یادداشت سنجاق شدهای نیست.",
|
||||
"pinnedNoNotebook": "بدون دفترچه",
|
||||
"widgetReset": "بازنشانی",
|
||||
"widgetResetDone": "داشبورد پیشفرض بازیابی شد.",
|
||||
"widgetStatsBridges": "پلها",
|
||||
|
||||
@@ -33,9 +33,9 @@
|
||||
"confirmPassword": "Confirmer le mot de passe",
|
||||
"confirmPasswordPlaceholder": "Confirmez votre mot de passe",
|
||||
"welcomeBack": "Bon retour parmi nous",
|
||||
"welcomeBackSubtitle": "Entrez vos identifiants pour accéder à vos notes.",
|
||||
"createYourSpace": "Créer votre espace",
|
||||
"createYourSpaceSubtitle": "Rejoignez la nouvelle ère de la prise de notes intelligente.",
|
||||
"welcomeBackSubtitle": "Retrouvez vos notes, vos liens, et ce que vous aviez oublié.",
|
||||
"createYourSpace": "Créer votre second cerveau",
|
||||
"createYourSpaceSubtitle": "Il se connecte pendant que vous écrivez — pas une app de notes de plus.",
|
||||
"forgot": "Oublié ?",
|
||||
"backToSite": "Retour",
|
||||
"privacyTerms": "© 2025 Memento Labs — Confidentialité · Conditions",
|
||||
@@ -94,12 +94,13 @@
|
||||
"dropToRoot": "Déposez ici pour déplacer à la racine",
|
||||
"noReminders": "Aucun rappel actif.",
|
||||
"documents": "Documents",
|
||||
"dashboardPanelBody": "Votre second brain en un coup d'œil : suggestions IA, capture rapide et prochaines pistes. Raccourcis pour agir tout de suite.",
|
||||
"dashboardPanelBody": "Vue d’ensemble de vos notes, pistes et découvertes. Pour agir, utilisez les boutons en haut de la page.",
|
||||
"searchNotebooksPlaceholder": "Rechercher un carnet…",
|
||||
"clearSearch": "Effacer la recherche",
|
||||
"insightsPanelBody": "Cartographie sémantique de vos notes : clusters thématiques, notes-ponts et suggestions de connexion.",
|
||||
"revisionPanelBody": "Révisez vos flashcards avec l'algorithme SM-2. Les decks sont générés depuis vos notes.",
|
||||
"insightsPanelBody": "La carte des liens entre vos notes : thèmes proches, notes qui font le pont, et pistes à ouvrir.",
|
||||
"revisionPanelBody": "Révisez avec des cartes. Elles reviennent au bon moment. Les jeux viennent de vos notes.",
|
||||
"backToNotebooks": "Retour aux carnets",
|
||||
"backToHome": "Retour à l’accueil",
|
||||
"resizeNotebooksPanel": "Redimensionner la zone des carnets",
|
||||
"resizeSidebar": "Redimensionner la largeur de la barre latérale",
|
||||
"dailyNote": "Note du jour",
|
||||
@@ -130,7 +131,7 @@
|
||||
"add": "Ajouter",
|
||||
"adding": "Ajout...",
|
||||
"close": "Fermer",
|
||||
"confirmDelete": "Voulez-vous vraiment supprimer cette note ?",
|
||||
"confirmDelete": "Cette note ira dans la corbeille. Vous pourrez la récupérer ensuite.",
|
||||
"confirmLeaveShare": "Êtes-vous sûr de vouloir quitter cette note partagée ?",
|
||||
"sharedBy": "Partagé par",
|
||||
"sharedShort": "Partagé",
|
||||
@@ -335,6 +336,7 @@
|
||||
"optionsMenuAria": "Menu des options",
|
||||
"deleteNoteConfirmItem": "Supprimer la note",
|
||||
"noteDeletedToast": "Note supprimée.",
|
||||
"undoDelete": "Annuler",
|
||||
"deleteNoteFailedToast": "Impossible de supprimer.",
|
||||
"documentInfoAria": "Informations du document",
|
||||
"noModification": "Aucune modification",
|
||||
@@ -1007,7 +1009,7 @@
|
||||
"nav": {
|
||||
"home": "Accueil",
|
||||
"notes": "Notes",
|
||||
"notebooks": "CARNETS",
|
||||
"notebooks": "Carnets",
|
||||
"generalNotes": "Notes générales",
|
||||
"archive": "Archives",
|
||||
"settings": "Paramètres",
|
||||
@@ -1021,7 +1023,7 @@
|
||||
"support": "Soutenir Memento ☕",
|
||||
"reminders": "Rappels",
|
||||
"graphView": "Carte des liens",
|
||||
"insights": "Thèmes sémantiques",
|
||||
"insights": "Connexions",
|
||||
"revision": "Révisions",
|
||||
"dailyNotes": "Notes du jour",
|
||||
"dashboard": "Tableau de bord",
|
||||
@@ -1882,18 +1884,18 @@
|
||||
"featureTags": "Étiquettes",
|
||||
"featureTitles": "Titres",
|
||||
"unlimited": "Illimité",
|
||||
"remaining": "{count} restants",
|
||||
"remaining": "{count} crédits restants",
|
||||
"upgradeTitle": "Passer à Pro",
|
||||
"upgradeDescription": "Votre solde de crédits IA est épuisé. Passez à un plan supérieur (plus de crédits mensuels) ou utilisez votre propre clé (BYOK).",
|
||||
"upgradeDescription": "Votre solde de crédits IA est épuisé. Passez à un plan supérieur (plus de crédits mensuels) ou utilisez votre propre clé.",
|
||||
"proIncludes": "Pro inclut :",
|
||||
"proSearch": "1 000 crédits IA / mois",
|
||||
"proTags": "BYOK (vos propres clés)",
|
||||
"proTags": "Vos propres clés fournisseur",
|
||||
"proTitles": "Agents (consomment des crédits)",
|
||||
"proReformulate": "Notes illimitées",
|
||||
"proChat": "Support e-mail",
|
||||
"later": "Plus tard",
|
||||
"upgradePricing": "Passer à Pro",
|
||||
"addApiKey": "Utiliser votre propre clé API (BYOK)",
|
||||
"addApiKey": "Utiliser votre propre clé",
|
||||
"featureReformulate": "Reformulations",
|
||||
"featureChat": "Messages IA",
|
||||
"featureBrainstormCreate": "Créations brainstorm",
|
||||
@@ -2190,14 +2192,14 @@
|
||||
"collapse": "Réduire"
|
||||
},
|
||||
"mcpSettings": {
|
||||
"title": "MCP",
|
||||
"description": "Gérez vos clés API et configurez les outils externes",
|
||||
"tierRequired": "Réservé Pro+",
|
||||
"upgradeHint": "L'accès MCP (clés API pour Cursor, Claude Desktop, etc.) nécessite un abonnement Pro ou supérieur. Passez à Pro dans Facturation pour débloquer cette fonctionnalité.",
|
||||
"title": "Outils externes",
|
||||
"description": "Connectez Cursor, Claude et d’autres outils à vos notes",
|
||||
"tierRequired": "Réservé à l’offre Pro ou plus",
|
||||
"upgradeHint": "Connecter un outil comme Cursor à vos notes est réservé à l’offre Pro ou plus. Passez à Pro dans Facturation.",
|
||||
"whatIsMcp": {
|
||||
"title": "Qu'est-ce que MCP ?",
|
||||
"description": "Le Model Context Protocol (MCP) est un protocole ouvert qui permet aux modèles IA d'interagir de manière sécurisée avec des outils et sources de données externes. Avec MCP, vous pouvez connecter des outils comme Claude Code, Cursor ou N8N à votre instance Memento pour lire, créer et organiser vos notes par programmation.",
|
||||
"learnMore": "En savoir plus sur MCP"
|
||||
"title": "À quoi ça sert ?",
|
||||
"description": "Vous pouvez ouvrir, chercher et classer vos notes depuis un autre outil — Cursor, Claude, n8n — sans y copier votre mot de passe. Une clé d’accès relie l’outil à votre compte.",
|
||||
"learnMore": "En savoir plus"
|
||||
},
|
||||
"serverStatus": {
|
||||
"title": "État du serveur",
|
||||
@@ -2207,10 +2209,10 @@
|
||||
"url": "URL"
|
||||
},
|
||||
"apiKeys": {
|
||||
"title": "Clés API",
|
||||
"description": "Les clés API permettent aux outils externes d'accéder à vos notes via MCP. Gardez vos clés secrètes.",
|
||||
"generate": "Générer une nouvelle clé",
|
||||
"empty": "Aucune clé API. Générez-en une pour commencer.",
|
||||
"title": "Clés d’accès",
|
||||
"description": "Une clé permet à un outil externe de lire et d’écrire vos notes. Ne la partagez pas.",
|
||||
"generate": "Nouvelle clé",
|
||||
"empty": "Aucune clé pour l’instant. Créez-en une pour commencer.",
|
||||
"active": "Actif",
|
||||
"revoked": "Révoquée",
|
||||
"revoke": "Révoquer",
|
||||
@@ -2227,42 +2229,42 @@
|
||||
}
|
||||
},
|
||||
"createDialog": {
|
||||
"title": "Générer une clé API",
|
||||
"description": "Créez une nouvelle clé API pour connecter des outils externes à vos notes.",
|
||||
"title": "Nouvelle clé d’accès",
|
||||
"description": "Donnez-lui un nom pour vous souvenir de l’outil auquel elle sert.",
|
||||
"nameLabel": "Nom de la clé",
|
||||
"namePlaceholder": "ex. Claude Code, Cursor, N8N",
|
||||
"generating": "Génération...",
|
||||
"generate": "Générer",
|
||||
"successTitle": "Clé API générée",
|
||||
"successDescription": "Copiez votre clé API maintenant. Vous ne pourrez plus la voir ensuite.",
|
||||
"namePlaceholder": "ex. Cursor, Claude, n8n",
|
||||
"generating": "Création…",
|
||||
"generate": "Créer",
|
||||
"successTitle": "Clé créée",
|
||||
"successDescription": "Copiez-la maintenant. Vous ne pourrez plus la relire ensuite.",
|
||||
"copy": "Copier",
|
||||
"copied": "Copiée !",
|
||||
"done": "Terminé"
|
||||
},
|
||||
"configInstructions": {
|
||||
"title": "Instructions de configuration",
|
||||
"description": "Utilisez votre clé API pour configurer ces outils.",
|
||||
"title": "Comment brancher un outil",
|
||||
"description": "Collez votre clé dans l’outil, avec l’adresse indiquée à côté.",
|
||||
"claudeCode": {
|
||||
"title": "Claude Code",
|
||||
"description": "Ajoutez ceci à votre fichier de configuration MCP de Claude Code :"
|
||||
"description": "Collez ceci dans le fichier de configuration de Claude Code :"
|
||||
},
|
||||
"cursor": {
|
||||
"title": "Cursor",
|
||||
"description": "Ajoutez ceci à vos paramètres MCP de Cursor :"
|
||||
"description": "Collez ceci dans les réglages d’outils de Cursor :"
|
||||
},
|
||||
"n8n": {
|
||||
"title": "N8N",
|
||||
"description": "Utilisez ces identifiants dans votre nœud MCP N8N :"
|
||||
"title": "n8n",
|
||||
"description": "Utilisez ces informations dans le nœud de connexion de n8n :"
|
||||
}
|
||||
},
|
||||
"helpBox": {
|
||||
"title": "Qu'est-ce que MCP (Model Context Protocol) ?",
|
||||
"step1": "MCP est un protocole qui permet aux agents IA de Memento de se connecter à des outils externes (bases de données, APIs, fichiers, etc.).",
|
||||
"step2": "Memento expose un serveur MCP avec 22 outils — vos agents peuvent lire/créer des notes, chercher dans votre base, gérer les carnets, etc.",
|
||||
"step3": "Créez une clé API ici, puis configurez-la dans votre client MCP (Claude Desktop, Cursor, Continue.dev…) avec l'URL du serveur.",
|
||||
"step4": "Format de configuration : URL du serveur MCP + votre clé dans le header Authorization.",
|
||||
"step4Link": "Documentation MCP",
|
||||
"step5": "Cas d'usage : demandez à Claude Desktop d'écrire une note dans Memento, de chercher dans vos carnets, ou de créer un agent."
|
||||
"title": "Comment connecter un outil ?",
|
||||
"step1": "Un outil comme Cursor ou Claude peut ouvrir, chercher et classer vos notes, comme si vous le faisiez ici.",
|
||||
"step2": "Vous créez une clé d’accès sur cette page. Elle sert de mot de passe pour cet outil seulement.",
|
||||
"step3": "Dans l’outil, vous indiquez l’adresse du serveur (ci-contre) et vous collez la clé.",
|
||||
"step4": "L’outil envoie ensuite vos demandes ici, sans que vous ayez à tout recopier à la main.",
|
||||
"step4Link": "Aide officielle",
|
||||
"step5": "Exemple : demander à Cursor d’écrire une note dans Memento, ou de chercher dans vos carnets."
|
||||
}
|
||||
},
|
||||
"agents": {
|
||||
@@ -2282,6 +2284,7 @@
|
||||
"monitor": "Surveillant",
|
||||
"slideGenerator": "Diaporamas",
|
||||
"excalidrawGenerator": "Diagramme",
|
||||
"taskExtractor": "Tâches",
|
||||
"custom": "Personnalisé"
|
||||
},
|
||||
"typeDescriptions": {
|
||||
@@ -2290,6 +2293,7 @@
|
||||
"monitor": "Surveille un carnet et analyse les notes",
|
||||
"slideGenerator": "Crée une présentation PowerPoint à partir de notes",
|
||||
"excalidrawGenerator": "Crée un diagramme Excalidraw à partir de notes",
|
||||
"taskExtractor": "Relève les tâches dans vos notes et les rassemble",
|
||||
"custom": "Agent libre avec votre propre prompt"
|
||||
},
|
||||
"form": {
|
||||
@@ -2429,6 +2433,17 @@
|
||||
"title": "Modèles",
|
||||
"install": "Installer",
|
||||
"installing": "Installation...",
|
||||
"seeAll": "Voir tous",
|
||||
"showLess": "Voir moins",
|
||||
"categoryAll": "Tous",
|
||||
"categoryWatch": "Veille",
|
||||
"categoryDigest": "Résumés",
|
||||
"categoryTools": "Outils",
|
||||
"categoryGenerate": "Création",
|
||||
"taskExtractor": {
|
||||
"name": "Tâches dans les notes",
|
||||
"description": "Relève les tâches dans vos notes et les rassemble au même endroit."
|
||||
},
|
||||
"veilleAI": {
|
||||
"name": "Veille IA",
|
||||
"description": "Scrape les flux RSS de 6 sites IA (The Verge, TechCrunch, Ars Technica, MIT Tech Review, WIRED, Korben) et génère un résumé hebdomadaire."
|
||||
@@ -2548,7 +2563,7 @@
|
||||
"slideStyle": "Le style visuel affecte les coins arrondis, l'espacement et la densité d'information."
|
||||
}
|
||||
},
|
||||
"intelligenceOS": "Système d'exploitation intelligent"
|
||||
"intelligenceOS": "Agents"
|
||||
},
|
||||
"chat": {
|
||||
"title": "Chat IA",
|
||||
@@ -2668,8 +2683,8 @@
|
||||
"slashTableDesc": "Insérer un tableau simple",
|
||||
"slashDatabase": "Vue structurée",
|
||||
"slashDatabaseDesc": "Intégrer les données structurées de votre carnet",
|
||||
"slashInteractiveDemo": "Démo interactive",
|
||||
"slashInteractiveDemoDesc": "Démo pédagogique IA étape par étape (Play / Step)",
|
||||
"slashInteractiveDemo": "Démo pas à pas",
|
||||
"slashInteractiveDemoDesc": "Parcours pédagogique dans la note",
|
||||
"slashToggle": "Section repliable",
|
||||
"slashToggleDesc": "Créer une section dépliable",
|
||||
"slashCallout": "Encadré",
|
||||
@@ -2840,10 +2855,10 @@
|
||||
"openPublicPage": "Ouvrir la page publique",
|
||||
"slashSubPage": "Sous-page",
|
||||
"slashSubPageDesc": "Créer une note liée dans cette note",
|
||||
"slashCharts": "Graphiques IA",
|
||||
"slashChartsDesc": "IA suggère des graphiques",
|
||||
"slashLivingBlock": "Bloc vivant",
|
||||
"slashLivingBlockDesc": "Insérer depuis une autre note",
|
||||
"slashCharts": "Proposer des graphiques",
|
||||
"slashChartsDesc": "Suggérer des graphiques d’après la note",
|
||||
"slashLivingBlock": "Bloc lié",
|
||||
"slashLivingBlockDesc": "Ouvrir un passage d’une autre note à côté",
|
||||
"frequentCommands": "★ Fréquents",
|
||||
"mobileBlockActions": "Actions sur le bloc",
|
||||
"mobileSelectAll": "Sélectionner tout",
|
||||
@@ -2942,13 +2957,13 @@
|
||||
"deleteImage": "Supprimer l'image"
|
||||
},
|
||||
"flashcards": {
|
||||
"generateTitle": "Générer des flashcards",
|
||||
"generateTitle": "Créer des cartes de révision",
|
||||
"generateAction": "Générer avec l'IA",
|
||||
"confirmSave": "Enregistrer dans le deck",
|
||||
"generateFailed": "Impossible de générer les flashcards",
|
||||
"saveFailed": "Impossible d'enregistrer les flashcards",
|
||||
"schemaMissing": "Les flashcards ne sont pas encore disponibles sur ce serveur (migration base de données en attente).",
|
||||
"savedCount": "{count} flashcards enregistrées",
|
||||
"confirmSave": "Enregistrer dans le jeu",
|
||||
"generateFailed": "Impossible de créer les cartes",
|
||||
"saveFailed": "Impossible d'enregistrer les cartes",
|
||||
"schemaMissing": "Les cartes de révision ne sont pas encore disponibles sur ce serveur.",
|
||||
"savedCount": "{count} cartes enregistrées",
|
||||
"cardCount": "Nombre de cartes",
|
||||
"styleLabel": "Style de cartes",
|
||||
"style": {
|
||||
@@ -2959,26 +2974,26 @@
|
||||
"previewHint": "Modifiez les cartes avant l'enregistrement",
|
||||
"frontPlaceholder": "Question / recto",
|
||||
"backPlaceholder": "Réponse / verso",
|
||||
"toolbarGenerate": "Générer des flashcards",
|
||||
"tabDecks": "Decks",
|
||||
"toolbarGenerate": "Créer des cartes de révision",
|
||||
"tabDecks": "Jeux",
|
||||
"tabProgress": "Progression",
|
||||
"emptyDecks": "Aucun deck de flashcards",
|
||||
"emptyDecksHint": "Ouvrez une note et utilisez l'icône casquette dans la barre d'outils pour générer des cartes avec l'IA.",
|
||||
"createDeck": "Créer un deck",
|
||||
"newDeckPlaceholder": "Nom du deck thématique…",
|
||||
"deckCreated": "Deck créé",
|
||||
"emptyDecks": "Aucune carte de révision",
|
||||
"emptyDecksHint": "Ouvrez une note, puis le bouton de cartes de révision en haut de la page.",
|
||||
"createDeck": "Nouveau jeu",
|
||||
"newDeckPlaceholder": "Nom du jeu…",
|
||||
"deckCreated": "Jeu créé",
|
||||
"dueCount": "{count} à réviser",
|
||||
"upToDate": "À jour",
|
||||
"cardCountLabel": "{count} cartes",
|
||||
"masteredShort": "maîtrisées",
|
||||
"viewDeck": "Détails",
|
||||
"hideDeck": "Masquer",
|
||||
"deckCardsEmpty": "Ce deck ne contient aucune carte pour l'instant.",
|
||||
"deckCardsEmpty": "Ce jeu ne contient aucune carte pour l'instant.",
|
||||
"dueBadge": "À réviser",
|
||||
"masteredBadge": "Maîtrisée",
|
||||
"review": "Réviser",
|
||||
"startReview": "Lancer la révision",
|
||||
"activeDeck": "Deck actif",
|
||||
"activeDeck": "Jeu en cours",
|
||||
"statTotal": "Total : {count}",
|
||||
"statDue": "À réviser : {count}",
|
||||
"statMastered": "Maîtrisées : {count}",
|
||||
@@ -2997,11 +3012,20 @@
|
||||
"easy": "Facile (4)"
|
||||
},
|
||||
"sessionComplete": "Session terminée",
|
||||
"backToDecks": "Retour aux decks",
|
||||
"loadDeckFailed": "Impossible de charger le deck",
|
||||
"backToDecks": "Retour aux jeux",
|
||||
"loadDeckFailed": "Impossible de charger le jeu",
|
||||
"reviewFailed": "Impossible d'enregistrer la révision",
|
||||
"heatmapTitle": "Activité de révision",
|
||||
"heatmapLast90": "90 derniers jours",
|
||||
"heatmapTotal": "{count} révisions · 90 jours",
|
||||
"heatmapDay": "{count} révisions",
|
||||
"heatmapDayOne": "1 révision",
|
||||
"heatmapDayNone": "Aucune révision",
|
||||
"heatmapHint": "Survolez ou cliquez sur un carré pour voir le détail",
|
||||
"heatmapSelected": "sélectionné",
|
||||
"heatmapClear": "Effacer la sélection",
|
||||
"heatmapLess": "Moins",
|
||||
"heatmapMore": "Plus",
|
||||
"retentionRate": "Taux de rétention",
|
||||
"masteredLabel": "{count}/{total} maîtrisées",
|
||||
"retentionCurve": "Taux de succès hebdomadaire",
|
||||
@@ -3026,15 +3050,15 @@
|
||||
"editCard": "Modifier",
|
||||
"deleteCard": "Supprimer la carte",
|
||||
"deleteCardConfirm": "Supprimer cette carte définitivement ?",
|
||||
"deleteDeck": "Supprimer le deck",
|
||||
"deleteDeckConfirm": "Supprimer ce deck et toutes ses cartes définitivement ?",
|
||||
"deckDeleted": "Deck supprimé",
|
||||
"deleteDeck": "Supprimer le jeu",
|
||||
"deleteDeckConfirm": "Supprimer ce jeu et toutes ses cartes ?",
|
||||
"deckDeleted": "Jeu supprimé",
|
||||
"cardDeleted": "Carte supprimée",
|
||||
"cardSaved": "Carte enregistrée",
|
||||
"cardTypeBadge": "{type}",
|
||||
"reviewNow": "Réviser maintenant",
|
||||
"deleteFailed": "Impossible de supprimer",
|
||||
"toolbarGenerateHint": "Révision espacée SM-2"
|
||||
"toolbarGenerateHint": "Elles reviennent au bon moment"
|
||||
},
|
||||
"structuredViews": {
|
||||
"enableTitle": "Organiser ce carnet (tableau, kanban, galerie…)",
|
||||
@@ -3330,10 +3354,10 @@
|
||||
"legendConverted": "Convertie"
|
||||
},
|
||||
"byokSettings": {
|
||||
"title": "Vos clés API (BYOK)",
|
||||
"description": "Connectez vos propres clés fournisseur. Avec le BYOK, l’usage ne consomme en général pas vos crédits Memento. Les clés sont chiffrées au repos.",
|
||||
"badgeActive": "BYOK actif",
|
||||
"tierRequired": "Le BYOK nécessite un abonnement Pro ou supérieur.",
|
||||
"title": "Vos clés fournisseur",
|
||||
"description": "Connectez vos propres clés. L’usage avec votre clé ne consomme en général pas vos crédits Memento. Les clés sont chiffrées au repos.",
|
||||
"badgeActive": "Clés actives",
|
||||
"tierRequired": "Cette option nécessite un abonnement Pro ou supérieur.",
|
||||
"provider": "Fournisseur",
|
||||
"providerPlaceholder": "Choisir un fournisseur",
|
||||
"alias": "Libellé (optionnel)",
|
||||
@@ -3388,11 +3412,11 @@
|
||||
"businessAnnualPrice": "299 €",
|
||||
"proFeature1": "Notes illimitées",
|
||||
"proFeature2": "1 000 crédits IA / mois",
|
||||
"proFeature3": "BYOK (vos propres clés)",
|
||||
"proFeature3": "Vos propres clés fournisseur",
|
||||
"proFeature4": "Agents (consomment des crédits)",
|
||||
"businessFeature1": "10 collaborateurs inclus",
|
||||
"businessFeature2": "4 000 crédits IA / mois",
|
||||
"businessFeature3": "BYOK · 13 fournisseurs",
|
||||
"businessFeature3": "Vos clés · {count} fournisseurs",
|
||||
"businessFeature4": "Agents & brainstorm (crédits)",
|
||||
"enterpriseTitle": "Entreprise",
|
||||
"enterpriseDescription": "Crédits illimités ou pool dédié, SSO, support prioritaire.",
|
||||
@@ -3429,6 +3453,9 @@
|
||||
"billingHistory": "Historique de facturation",
|
||||
"viewInvoices": "Gérer les factures dans le portail",
|
||||
"cancelSubscription": "Résilier l'abonnement",
|
||||
"changeOffer": "Changer d'offre",
|
||||
"downgradeToFree": "Revenir à l'offre gratuite",
|
||||
"cancellingNotice": "Résiliation prévue — accès jusqu’au {date}",
|
||||
"nextBillingDate": "Prochaine date de facturation",
|
||||
"billingPeriod": "Période de facturation",
|
||||
"planSince": "Membre depuis",
|
||||
@@ -3464,7 +3491,7 @@
|
||||
"proFeature5": "Historique 30 jours",
|
||||
"proFeature6": "Support e-mail",
|
||||
"proCta": "Passer à Pro",
|
||||
"businessFeature5": "API / MCP",
|
||||
"businessFeature5": "Outils externes",
|
||||
"businessFeature6": "Support prioritaire",
|
||||
"businessCta": "Passer à Business",
|
||||
"recommended": "Recommandé",
|
||||
@@ -3531,7 +3558,7 @@
|
||||
},
|
||||
"trust": {
|
||||
"trust0": "Un vrai Second Brain, pas un tas de dossiers",
|
||||
"trust1": "Vos propres clés IA (BYOK)",
|
||||
"trust1": "Vos propres clés IA",
|
||||
"trust2": "15 langues, dont le persan RTL",
|
||||
"trust3": "Des agents qui écrivent dans vos carnets"
|
||||
},
|
||||
@@ -3545,88 +3572,92 @@
|
||||
"echo": {
|
||||
"eyebrow": "Memory Echo",
|
||||
"title": "Le moment où votre Second Brain vous répond.",
|
||||
"desc": "Pendant que vous écrivez, Memento détecte des liens sémantiques entre carnets — pas des mots-clés, de vrais ponts conceptuels, ouvrables en aperçu partagé.",
|
||||
"desc": "Pendant que vous écrivez, Memento retrouve des liens entre vos carnets — pas seulement les mêmes mots, de vrais rapprochements. Vous pouvez ouvrir les deux notes côte à côte.",
|
||||
"card0Label": "À l'instant",
|
||||
"card0": "« Votre note sur le pricing fait écho à l'analyse concurrente de l'automne. »",
|
||||
"card0": "« Votre note sur les tarifs fait écho à l'analyse des concurrents de l'automne. »",
|
||||
"card1Label": "Pont",
|
||||
"card1": "Thème partagé : positionnement sous contrainte",
|
||||
"card2Label": "Action",
|
||||
"card2": "Ouvrir les deux notes côte à côte — continuez d'écrire"
|
||||
},
|
||||
"dashboard": {
|
||||
"eyebrow": "Dashboard Second Brain",
|
||||
"eyebrow": "Le matin",
|
||||
"title": "Un matin qui vous dit ce qui compte.",
|
||||
"desc": "Le briefing de votre Second Brain : prochaines pistes, checklist, découvertes, révisions — des widgets que vous arrangez.",
|
||||
"desc": "Le briefing du matin : prochaines pistes, liste du jour, découvertes, révisions — des cartes que vous rangez.",
|
||||
"w0Label": "Prochaines pistes",
|
||||
"w0": "3 actions depuis votre dernière note",
|
||||
"w1Label": "Revue du jour",
|
||||
"w1": "Inbox · IA · Flashcards",
|
||||
"w1": "Boîte de réception · Découvertes · Cartes",
|
||||
"w2Label": "Memory Echo",
|
||||
"w2": "2 nouvelles connexions cette nuit",
|
||||
"w3Label": "Agents",
|
||||
"w3": "1 suggestion prête à lancer"
|
||||
},
|
||||
"insights": {
|
||||
"eyebrow": "Insights",
|
||||
"title": "Voyez l'architecture de votre Second Brain.",
|
||||
"desc": "Clusters sémantiques et notes pont — la carte vivante de vos connexions.",
|
||||
"chip": "Réseau de clusters"
|
||||
"eyebrow": "Connexions",
|
||||
"title": "Voyez comment vos notes se relient.",
|
||||
"desc": "Des thèmes proches et des notes-pont — la carte vivante de vos liens.",
|
||||
"chip": "Carte des liens"
|
||||
},
|
||||
"revision": {
|
||||
"eyebrow": "Révision",
|
||||
"title": "Mémorisez exprès.",
|
||||
"desc": "Générez des flashcards depuis n'importe quelle note. Répétition espacée SM-2 — le savoir qui tient dans votre Second Brain.",
|
||||
"card": "Qu'est-ce qui relie Memory Echo à vos notes ?"
|
||||
"desc": "Générez des cartes de révision depuis n’importe quelle note. Elles reviennent au bon moment.",
|
||||
"card": "Qu'est-ce qui relie Memory Echo à vos notes ?",
|
||||
"badge": "Au bon moment"
|
||||
}
|
||||
},
|
||||
"how": {
|
||||
"title": "Trois étapes. Puis votre Second Brain compound.",
|
||||
"title": "Trois étapes. Ensuite, ça s’accumule.",
|
||||
"s0": {
|
||||
"title": "Capturez librement",
|
||||
"desc": "Écrivez dans un éditeur nouvelle génération — blocs, vues structurées, collage intelligent. Clippez le web si besoin."
|
||||
"desc": "Écrivez comme dans un cahier : titres, listes, tableaux. Enregistrez une page web si besoin."
|
||||
},
|
||||
"s1": {
|
||||
"title": "Laissez connecter",
|
||||
"desc": "Memory Echo et la recherche sémantique tissent des liens que vous n'aviez pas prévus. Votre passé devient du carburant."
|
||||
"desc": "Memory Echo et la recherche retrouvent des liens que vous n'aviez pas prévus. Votre passé devient du carburant."
|
||||
},
|
||||
"s2": {
|
||||
"title": "Agissez chaque matin",
|
||||
"desc": "Pistes du dashboard, agents et flashcards transforment une pile de notes en élan Second Brain."
|
||||
"desc": "Les pistes du matin, les agents et les cartes de révision transforment une pile de notes en élan."
|
||||
}
|
||||
},
|
||||
"agents": {
|
||||
"label": "Agents IA",
|
||||
"title": "Déléguez le gros du travail.",
|
||||
"desc": "Recherche, scrape, slides, diagrammes, veille — des agents qui écrivent dans votre Second Brain, pas un chat de plus que vous oublierez.",
|
||||
"desc": "Recherche, veille, présentations, schémas — des agents qui écrivent dans vos carnets, pas un échange de plus que vous oublierez.",
|
||||
"scraper": {
|
||||
"title": "Scraper",
|
||||
"desc": "URL & RSS → notes synthétisées avec images."
|
||||
"title": "Veilleur",
|
||||
"desc": "Des adresses et des flux deviennent des notes, avec les images utiles."
|
||||
},
|
||||
"researcher": {
|
||||
"title": "Researcher",
|
||||
"desc": "Requêtes profondes, sources, notes structurées."
|
||||
"title": "Chercheur",
|
||||
"desc": "Des questions approfondies, des sources, des notes structurées."
|
||||
},
|
||||
"slideGen": {
|
||||
"title": "Slide Gen",
|
||||
"desc": "Notes → decks ou slides HTML interactives."
|
||||
"title": "Diaporamas",
|
||||
"desc": "Vos notes deviennent une présentation, ou une page à parcourir."
|
||||
},
|
||||
"monitor": {
|
||||
"title": "Monitor",
|
||||
"desc": "Surveille vos carnets : tendances et insights."
|
||||
"title": "Surveillant",
|
||||
"desc": "Il observe vos carnets : tendances et idées nouvelles."
|
||||
},
|
||||
"diagramGen": {
|
||||
"title": "Diagram Gen",
|
||||
"desc": "Idées → mindmaps & flows Excalidraw."
|
||||
"title": "Schémas",
|
||||
"desc": "Vos idées deviennent des cartes mentales et des enchaînements."
|
||||
},
|
||||
"custom": {
|
||||
"title": "Custom",
|
||||
"desc": "Vos rôles, sources et planifications."
|
||||
"title": "Personnalisé",
|
||||
"desc": "Vos rôles, vos sources, vos horaires."
|
||||
}
|
||||
},
|
||||
"byok": {
|
||||
"label": "Sans lock-in",
|
||||
"label": "Libre de changer",
|
||||
"title": "Vos clés. Vos modèles. Votre Second Brain.",
|
||||
"desc": "Crédits Memento ou OpenAI, Anthropic, Google… Changez de fournisseur en un clic — le produit reste le vôtre."
|
||||
"desc": "Crédits Memento, ou votre propre fournisseur. Changez en un clic — le produit reste le vôtre.",
|
||||
"pointCredits": "Les crédits Memento, si vous voulez rester simple",
|
||||
"pointProvider": "Ou connecter votre propre fournisseur",
|
||||
"pointYours": "Votre clé reste chez vous — elle ne s’affiche jamais ici"
|
||||
},
|
||||
"pricing": {
|
||||
"label": "Tarifs",
|
||||
@@ -3661,7 +3692,7 @@
|
||||
"cta": "Passer Pro",
|
||||
"feature0": "Notes illimitées",
|
||||
"feature1": "1 000 crédits IA / mois",
|
||||
"feature2": "BYOK (vos propres clés)",
|
||||
"feature2": "Vos propres clés fournisseur",
|
||||
"feature3": "Agents (consomment des crédits)",
|
||||
"feature4": "Historique 30 jours",
|
||||
"feature5": "Support e-mail"
|
||||
@@ -3672,9 +3703,9 @@
|
||||
"cta": "Choisir Business",
|
||||
"feature0": "10 collaborateurs",
|
||||
"feature1": "4 000 crédits IA / mois",
|
||||
"feature2": "BYOK · 13 fournisseurs",
|
||||
"feature2": "Vos clés · {count} fournisseurs",
|
||||
"feature3": "Agents & brainstorm (crédits)",
|
||||
"feature4": "API / MCP",
|
||||
"feature4": "Outils externes",
|
||||
"feature5": "Support prioritaire"
|
||||
},
|
||||
"enterprise": {
|
||||
@@ -3772,9 +3803,10 @@
|
||||
}
|
||||
},
|
||||
"insightsView": {
|
||||
"title": "Insights sémantiques",
|
||||
"title": "Connexions",
|
||||
"toggleMenu": "Afficher ou masquer le menu",
|
||||
"subtitle": "Découvrez l'architecture cachée de votre savoir",
|
||||
"resync": "Resynchroniser le réseau",
|
||||
"resync": "Mettre à jour",
|
||||
"mapping": "Cartographie…",
|
||||
"loading": "Chargement de vos notes…",
|
||||
"mappingTitle": "Cartographie de votre savoir…",
|
||||
@@ -3787,7 +3819,7 @@
|
||||
"analysisFailed": "L’analyse a échoué. Vérifiez vos paramètres IA ou réessayez.",
|
||||
"analysisSuccess": "Analyse terminée : {count} thèmes détectés.",
|
||||
"analysisNoClusters": "Aucun thème détecté pour l'instant.",
|
||||
"staleResults": "Résultats affichés depuis la dernière analyse. Beaucoup de notes ont changé depuis — cliquez « Resynchroniser le réseau » pour mettre à jour.",
|
||||
"staleResults": "Résultats affichés depuis la dernière analyse. Beaucoup de notes ont changé depuis — cliquez « Mettre à jour ».",
|
||||
"semanticGraphLegend": "Aperçu des thèmes détectés (pas la carte des liens)",
|
||||
"fitGraphView": "Ajuster la vue",
|
||||
"legendFilterPlaceholder": "Filtrer les thèmes…",
|
||||
@@ -3800,7 +3832,7 @@
|
||||
"clusterFallback": "Thème {index}",
|
||||
"unclusteredNotes": "{count} notes non rattachées à un thème (hors graphe).",
|
||||
"emptyTitle": "Découvrez vos clusters de connaissance",
|
||||
"emptyDescription": "Cliquez sur « Resynchroniser le réseau » pour analyser vos notes et révéler des connexions cachées",
|
||||
"emptyDescription": "Cliquez sur « Mettre à jour » pour analyser vos notes et révéler des connexions cachées",
|
||||
"stats": {
|
||||
"clusters": "Thèmes",
|
||||
"bridgeNotes": "Notes pont",
|
||||
@@ -3819,14 +3851,14 @@
|
||||
"affinity": "Affinité {score} %",
|
||||
"scoreHint": "Affinité sémantique moyenne avec les deux thèmes que cette note relie (similarité cosinus).",
|
||||
"moreThemes": "+{count}",
|
||||
"needsResync": "Resynchronisez le réseau pour rafraîchir les paires de thèmes.",
|
||||
"needsResync": "Cliquez « Mettre à jour » pour rafraîchir les paires de thèmes.",
|
||||
"empty": "Aucune note pont significative pour l'instant. Approfondissez vos recherches pour découvrir de nouvelles connexions."
|
||||
},
|
||||
"suggestions": {
|
||||
"title": "Liens manquants",
|
||||
"bridging": "Relier {clusterA} et {clusterB}",
|
||||
"emptyTitle": "Aucune suggestion de connexion",
|
||||
"emptyDescription": "Aucune paire de thèmes « presque liés » pour l'instant — ou resynchronisez pour rafraîchir les prédictions.",
|
||||
"emptyDescription": "Aucune paire de thèmes « presque liés » pour l'instant — ou cliquez « Mettre à jour ».",
|
||||
"createNote": "Créer la note pont",
|
||||
"created": "Note pont créée",
|
||||
"createError": "Impossible de créer la note pont",
|
||||
@@ -3937,7 +3969,7 @@
|
||||
"whatWillBeDeleted": "Les éléments suivants seront définitivement supprimés :",
|
||||
"item1": "Toutes vos notes, carnets et pièces jointes",
|
||||
"item2": "Tous vos embeddings sémantiques pgvector",
|
||||
"item3": "Toutes vos clés API BYOK",
|
||||
"item3": "Toutes vos clés fournisseur",
|
||||
"item4": "Toutes vos conversations IA et sessions de brainstorm",
|
||||
"item5": "Votre historique de quotas et d'utilisation",
|
||||
"item6": "Votre abonnement Stripe (si actif)",
|
||||
@@ -4138,13 +4170,13 @@
|
||||
"step_features_cta": "C'est parti !",
|
||||
"feature_search_title": "Recherche sémantique",
|
||||
"feature_search_desc": "Retrouvez n'importe quelle note par sens, pas seulement par mot-clé.",
|
||||
"feature_flashcards_title": "Flashcards IA",
|
||||
"feature_flashcards_desc": "Générez des cartes de révision SRS depuis vos notes en un clic.",
|
||||
"feature_flashcards_title": "Cartes de révision",
|
||||
"feature_flashcards_desc": "Créez des cartes de révision depuis vos notes, en un clic.",
|
||||
"feature_brainstorm_title": "Brainstorm IA",
|
||||
"feature_brainstorm_desc": "Séances de brainstorming collaboratif alimentées par l'IA.",
|
||||
"feature_chat_title": "Chat avec vos notes",
|
||||
"feature_chat_desc": "Posez des questions à votre base de connaissances personnelle.",
|
||||
"feature_insights_title": "Insights sémantiques",
|
||||
"feature_insights_title": "Connexions",
|
||||
"feature_insights_desc": "Découvrez les connexions cachées entre vos idées.",
|
||||
"feature_export_title": "Export Markdown",
|
||||
"feature_export_desc": "Importez et exportez vos notes au format Markdown standard.",
|
||||
@@ -4154,14 +4186,14 @@
|
||||
"import_notes_ready": "{count} note(s) importée(s) !",
|
||||
"action_write_title": "Écrire votre première vraie note",
|
||||
"action_write_desc": "Créez une note et commencez à capturer vos idées.",
|
||||
"action_flashcards_title": "Générer vos premières flashcards",
|
||||
"action_flashcards_desc": "Ouvrez une note et cliquez sur le bouton flashcards.",
|
||||
"action_flashcards_title": "Créer vos premières cartes de révision",
|
||||
"action_flashcards_desc": "Ouvrez une note, puis le bouton de cartes de révision en haut de la page.",
|
||||
"action_brainstorm_title": "Lancer un brainstorm IA",
|
||||
"action_brainstorm_desc": "Explorez vos idées avec un agent IA dédié.",
|
||||
"action_try": "Essayer",
|
||||
"step_features_cta_all": "Tout est prêt — on plonge !",
|
||||
"action_write_where": "Fermez ce menu → cliquez sur \"+ Nouvelle note\" dans la barre latérale",
|
||||
"action_flashcards_where": "Fermez ce menu → ouvrez une note → bouton 🃏 dans la toolbar",
|
||||
"action_flashcards_where": "Fermez cette fenêtre, ouvrez une note, puis le bouton de cartes de révision en haut de la page.",
|
||||
"action_brainstorm_where": "Fermez ce menu → section \"Canvas\" dans la barre latérale",
|
||||
"pill_resume": "✨ Reprendre la visite",
|
||||
"action_done": "Testé !",
|
||||
@@ -4173,8 +4205,8 @@
|
||||
"hint_ai_desc": "Cliquez sur le bouton ✨ dans la barre d'outils pour ouvrir le panneau IA — posez des questions, résumez, réécrivez ou brainstormez directement dans votre note.",
|
||||
"hint_version_title": "Historique des versions",
|
||||
"hint_version_desc": "Cliquez sur le bouton ⓘ dans la barre d'outils → onglet \"Versions\". Activez le versionnage, puis sauvegardez et restaurez des captures de votre note à tout moment.",
|
||||
"hint_flashcards_title": "Générer des flashcards",
|
||||
"hint_flashcards_desc": "Cliquez sur le bouton 🎓 dans la barre d'outils pour générer automatiquement des flashcards depuis votre note, pour une révision en répétition espacée.",
|
||||
"hint_flashcards_title": "Créer des cartes de révision",
|
||||
"hint_flashcards_desc": "En haut de la note, cliquez sur le bouton de cartes de révision. Elles reviennent au bon moment.",
|
||||
"hint_links_title": "Liens entre notes",
|
||||
"hint_links_desc": "Tapez \"[[\" dans l'éditeur pour rechercher et lier une autre note. Les notes liées apparaissent comme rétroliens en bas de la note.",
|
||||
"hint_create_note_title": "Créer une note",
|
||||
@@ -4182,9 +4214,9 @@
|
||||
"hint_flip_title": "Retourner la carte",
|
||||
"hint_flip_desc": "Appuyez sur Espace (ou cliquez sur la carte) pour la retourner et révéler la réponse.",
|
||||
"hint_rate_keys_title": "Noter au clavier",
|
||||
"hint_rate_keys_desc": "Après avoir retourné la carte, appuyez sur 1 (Difficile), 2 (Laborieux), 3 (Bien) ou 4 (Facile) pour noter. L'algorithme SM-2 planifie automatiquement votre prochaine révision.",
|
||||
"hint_rate_keys_desc": "Après avoir retourné la carte, appuyez sur 1 (Difficile), 2 (Laborieux), 3 (Bien) ou 4 (Facile). La prochaine révision est placée au bon moment.",
|
||||
"hint_generate_from_note_title": "Générer depuis une note",
|
||||
"hint_generate_from_note_desc": "Ouvrez n'importe quelle note et cliquez sur le bouton 🎓 dans la barre d'outils pour générer automatiquement des flashcards depuis son contenu.",
|
||||
"hint_generate_from_note_desc": "Ouvrez une note, puis le bouton de cartes de révision en haut de la page.",
|
||||
"hint_brainstorm_start_title": "Démarrer avec une idée",
|
||||
"hint_brainstorm_start_desc": "Tapez un concept ou une question dans le champ de saisie et appuyez sur Entrée. L'IA génère un ensemble d'idées autour de ce thème.",
|
||||
"hint_brainstorm_deepen_title": "Approfondir une idée",
|
||||
@@ -4262,15 +4294,17 @@
|
||||
"homeDashboard": {
|
||||
"title": "Tableau de bord",
|
||||
"quickCapture": "Capture rapide",
|
||||
"quickCapturePlaceholder": "Une idée, une pensée… Entrée pour capturer dans l'Inbox.",
|
||||
"captured": "Capturé dans l'Inbox",
|
||||
"quickCapturePlaceholder": "Une idée, une pensée… Entrée l’envoie dans la file.",
|
||||
"captureGoesToFile": "Va dans la file.",
|
||||
"captureSend": "Envoyer dans la file",
|
||||
"captured": "Ajouté à la file.",
|
||||
"captureError": "Erreur",
|
||||
"mindMap": "Carte mentale",
|
||||
"fullMap": "Vue complète →",
|
||||
"mindMapEmpty": "Pas encore de thèmes détectés. L'analyse sémantique regroupe vos notes par sujets.",
|
||||
"mindMapOpen": "Ouvrir la cartographie →",
|
||||
"mindMapUnavailable": "Cartographie indisponible.",
|
||||
"themes": "thèmes",
|
||||
"themes": "Thèmes",
|
||||
"bridges": "ponts",
|
||||
"aiEchoes": "échos IA",
|
||||
"bridgeNote": "Note-pont",
|
||||
@@ -4280,6 +4314,9 @@
|
||||
"createAgent": "Créer l'agent",
|
||||
"agentsEmpty": "Aucune recherche suggérée pour l'instant. Continuez à écrire — Memento proposera des synthèses quand vos notes formeront des thèmes.",
|
||||
"agentCreated": "Agent créé et lancé",
|
||||
"agentCreatedInCard": "L'agent est créé.",
|
||||
"agentOpenCreated": "Ouvrir l'agent",
|
||||
"agentSeeNextSuggestion": "Voir la suivante",
|
||||
"agentFailed": "Échec de la création",
|
||||
"notes": "notes",
|
||||
"continue": "Reprendre ici",
|
||||
@@ -4288,14 +4325,17 @@
|
||||
"resumeEmptyCta": "Capturer une idée",
|
||||
"resumeAlso": "Aussi récemment",
|
||||
"untitled": "Sans titre",
|
||||
"inbox": "Inbox",
|
||||
"inbox": "Boîte de réception",
|
||||
"sentiment": "Analyse émotionnelle",
|
||||
"analyzing": "Analyse en cours…",
|
||||
"notEnoughNotes": "Pas assez de notes cette semaine.",
|
||||
"toReview": "À traiter",
|
||||
"allCaughtUp": "Tout est à jour.",
|
||||
"remindersEmpty": "Aucun rappel pour aujourd'hui.",
|
||||
"remindersOpenAll": "Voir les rappels",
|
||||
"toOrganize": "à organiser",
|
||||
"inboxSeeAll": "Voir les {count}",
|
||||
"inboxOpenList": "Ouvrir la file",
|
||||
"inboxEmpty": "Rien à classer.",
|
||||
"review": "Révisions",
|
||||
"cardsDue": "cartes dues",
|
||||
@@ -4348,7 +4388,15 @@
|
||||
"widgetStatsNotes": "Notes",
|
||||
"widgetAgentActivityEmpty": "Aucune activité d'agent sur les 48 dernières heures.",
|
||||
"widgetPinnedEmpty": "Aucune note épinglée pour l'instant.",
|
||||
"pinnedNoNotebook": "Sans carnet",
|
||||
"widgetActivityHint": "Notes modifiées sur les 90 derniers jours.",
|
||||
"activityHeatmapTitle": "Notes modifiées",
|
||||
"activityHeatmapLast90": "90 derniers jours",
|
||||
"activityHeatmapTotal": "{count} notes · 90 jours",
|
||||
"activityHeatmapDay": "{count} notes",
|
||||
"activityHeatmapDayOne": "1 note",
|
||||
"activityHeatmapDayNone": "Aucune note ce jour-là",
|
||||
"activityHeatmapHint": "Survolez ou cliquez sur un carré pour voir le jour",
|
||||
"widgetUsageHint": "Solde unique de crédits IA (toutes les fonctions).",
|
||||
"briefingLoadError": "Impossible de charger le tableau de bord.",
|
||||
"briefingRetry": "Réessayer",
|
||||
@@ -4367,8 +4415,8 @@
|
||||
"mind-map": "Carte mentale",
|
||||
"agents": "Recherches suggérées",
|
||||
"sentiment": "Analyse émotionnelle",
|
||||
"inbox": "Inbox",
|
||||
"revision": "Flashcards",
|
||||
"inbox": "Boîte de réception",
|
||||
"revision": "Cartes de révision",
|
||||
"stats": "Statistiques sémantiques",
|
||||
"agent-activity": "Activité agents",
|
||||
"gmail": "Captures Gmail",
|
||||
@@ -4384,7 +4432,7 @@
|
||||
"flashcards-progress": "Progression apprentissage"
|
||||
},
|
||||
"widgetDescriptions": {
|
||||
"capture": "Saisir une pensée immédiatement dans l'inbox.",
|
||||
"capture": "Noter une pensée tout de suite. Elle va dans la file.",
|
||||
"resume": "Reprendre vos notes les plus récentes.",
|
||||
"intelligence": "Liens sémantiques, idées-ponts et découvertes d'agents.",
|
||||
"reminders": "Rappels de notes à venir en un coup d'œil.",
|
||||
@@ -4415,21 +4463,22 @@
|
||||
"pathCompareHint": "Également lié : {title} — ouvrez Memory Echo pour comparer.",
|
||||
"pathAddLinkHint": "Pensez à lier « {link} » dans votre note.",
|
||||
"dailyReviewHint": "10 min chaque matin — l'habitude qui fait composer votre second brain.",
|
||||
"dailyReviewInbox": "Traiter l'inbox",
|
||||
"dailyReviewInbox": "Traiter la file",
|
||||
"dailyReviewDiscoveries": "Revoir les découvertes IA",
|
||||
"dailyReviewConnection": "Explorer une connexion",
|
||||
"dailyReviewCards": "Réviser les flashcards",
|
||||
"dailyReviewCards": "Réviser les cartes",
|
||||
"openLoopsEmpty": "Aucune note en suspens — vous êtes à jour.",
|
||||
"openLoopsStale": "il y a {days} j",
|
||||
"dailyNoteOpen": "Ouvrir le journal",
|
||||
"linkSuggestionsEmpty": "Modifiez une note pour obtenir des suggestions de liens.",
|
||||
"bridgesEmpty": "Pas d'opportunité de pont pour l'instant.",
|
||||
"flashRetention": "Maîtrisées",
|
||||
"flashStreak": "Série",
|
||||
"flashTotal": "Cartes",
|
||||
"flashEmptyHint": "Pas encore de flashcards. Générez-en depuis une note (icône diplôme dans l’éditeur), puis suivez votre progression ici.",
|
||||
"flashRetention": "Bien retenues",
|
||||
"flashRetentionHint": "Part des cartes que tu retients bien.",
|
||||
"flashStreak": "Jours d’affilée",
|
||||
"flashTotal": "Au total",
|
||||
"flashEmptyHint": "Pas encore de cartes. Créez-en depuis une note, puis suivez votre progression ici.",
|
||||
"flashEmptyCta": "Ouvrir les révisions",
|
||||
"flashDueCta": "{count} cartes à réviser aujourd’hui",
|
||||
"flashDueCta": "Réviser maintenant",
|
||||
"flashOpenCta": "Voir la progression",
|
||||
"activityEmptyHint": "Éditez des notes pour voir votre rythme d’écriture sur 90 jours.",
|
||||
"pathTypes": {
|
||||
@@ -4447,24 +4496,25 @@
|
||||
"widgetHelpLabel": "Aide du widget",
|
||||
"widgetHelpClose": "Fermer",
|
||||
"sentimentDominant": "Tonalité dominante cette semaine",
|
||||
"sentimentFromNotes": "D’après tes notes",
|
||||
"widgetHelp": {
|
||||
"capture": "Saisissez une pensée en une ligne. Elle arrive dans l'Inbox — classez-la lors de votre revue quotidienne.",
|
||||
"capture": "Écrivez une pensée. Elle arrive dans la file, à classer plus tard.",
|
||||
"next-paths": "Prochaines étapes suggérées à partir de votre dernière note : reprendre, lier, créer un pont ou lancer une recherche.",
|
||||
"resume": "Vos notes les plus récemment modifiées. Reprenez là où vous vous êtes arrêté.",
|
||||
"intelligence": "Découvertes IA : liens sémantiques, idées-ponts et résultats d'agents.",
|
||||
"reminders": "Rappels de notes à venir. Vide quand tout est à jour.",
|
||||
"mind-map": "Thèmes regroupés, taille proportionnelle au volume de notes. Cliquez pour explorer dans Insights.",
|
||||
"mind-map": "Thèmes regroupés, taille proportionnelle au volume de notes. Cliquez pour explorer dans Connexions.",
|
||||
"agents": "Agents de recherche suggérés par l'IA pour vos sujets récurrents.",
|
||||
"sentiment": "Tonalité émotionnelle des notes modifiées sur les 7 derniers jours. Nécessite au moins 3 notes récentes et l'IA activée.",
|
||||
"inbox": "Notes sans carnet. Classez-les pour garder un second brain ordonné.",
|
||||
"revision": "Cartes flashcards à réviser aujourd'hui (répétition espacée).",
|
||||
"revision": "Cartes à réviser aujourd'hui. Elles reviennent au bon moment.",
|
||||
"stats": "Statistiques de l'index sémantique : thèmes, notes-ponts, total indexé.",
|
||||
"agent-activity": "Agents ayant terminé une exécution dans les 48 dernières heures.",
|
||||
"gmail": "Captures e-mail synchronisées depuis l'intégration Gmail.",
|
||||
"activity": "Heatmap des notes modifiées sur les 90 derniers jours.",
|
||||
"pinned": "Accès rapide à vos notes épinglées.",
|
||||
"usage": "Consommation mensuelle des crédits IA par fonctionnalité.",
|
||||
"daily-review": "Checklist matinale 10 min : inbox, découvertes, une connexion, flashcards.",
|
||||
"daily-review": "Liste du matin, 10 min : boîte de réception, découvertes, une connexion, cartes.",
|
||||
"open-loops": "Notes commencées mais sans modification depuis 3+ jours.",
|
||||
"daily-note": "Entrée de journal du jour — une note par jour.",
|
||||
"link-suggestions": "Passages d'autres notes à lier dans votre travail en cours.",
|
||||
@@ -4712,7 +4762,7 @@
|
||||
"contactError": "Impossible de contacter le serveur",
|
||||
"baseUrlRequired": "Veuillez renseigner l'URL de l'API",
|
||||
"keyInvalid": "Clé API invalide",
|
||||
"byokActive": "BYOK actif",
|
||||
"byokActive": "Clés actives",
|
||||
"activeStatus": "● Actif",
|
||||
"inactiveStatus": "○ Inactif",
|
||||
"confirmDelete": "Supprimer la clé {{name}} ?",
|
||||
|
||||
@@ -31,14 +31,14 @@
|
||||
"confirmPasswordPlaceholder": "अपना पासवर्ड दोबारा दर्ज करें",
|
||||
"backToSite": "वापस",
|
||||
"continueWithGoogle": "Google के साथ जारी रखें",
|
||||
"createYourSpace": "अपना स्थान बनाएं",
|
||||
"createYourSpaceSubtitle": "स्मार्ट नोट लेखन के नए युग में शामिल हों।",
|
||||
"createYourSpace": "अपना दूसरा दिमाग बनाएँ",
|
||||
"createYourSpaceSubtitle": "जब आप लिखते हैं, यह जुड़ता है — सिर्फ़ एक और नोट ऐप नहीं।",
|
||||
"forgot": "भूल गए?",
|
||||
"oauthAccountNotLinked": "यह Google खाता आपके मौजूदा खाते से मेल नहीं खाता। वही ईमेल उपयोग करें या अपने पासवर्ड से साइन इन करें।",
|
||||
"privacyTerms": "© 2025 Memento Labs — गोपनीयता · शर्तें",
|
||||
"sessionExpired": "आपकी साइट नेविगेशन और विषय-सूची के साथ बनाई जाती है",
|
||||
"welcomeBack": "वापसी पर स्वागत है",
|
||||
"welcomeBackSubtitle": "अपने नोट्स तक पहुँचने के लिए अपनी प्रमाणीकरण जानकारी दर्ज करें।",
|
||||
"welcomeBackSubtitle": "अपने नोट्स, कनेक्शन और जो आप भूल गए थे, उन्हें पाने के लिए साइन इन करें।",
|
||||
"checkEmailTitle": "अपना ईमेल देखें",
|
||||
"checkEmailDescription": "हमने {email} पर पुष्टि लिंक भेजा है। साइन इन से पहले खाता सक्रिय करने के लिए इसे खोलें।",
|
||||
"checkEmailDescriptionGeneric": "हमने आपके ईमेल पर पुष्टि लिंक भेजा है। साइन इन से पहले खाता सक्रिय करने के लिए इसे खोलें।",
|
||||
@@ -99,13 +99,13 @@
|
||||
"darkMode": "डार्क मोड",
|
||||
"dashboardPanelBody": "आपका सत्र समाप्त हो गया है। कृपया पुनः साइन इन करें।",
|
||||
"documents": "दस्तावेज़",
|
||||
"insightsPanelBody": "आपके नोट्स का सिमेंटिक मानचित्र: विषय क्लस्टर, ब्रिज नोट्स और कनेक्शन सुझाव।",
|
||||
"insightsPanelBody": "आपके नोट्स कैसे जुड़ते हैं, उसका नक्शा: नज़दीकी विषय, पुल नोट्स, और खोलने योग्य लिंक।",
|
||||
"lightMode": "लाइट मोड",
|
||||
"notebookEmpty": "खाली",
|
||||
"recentNote": "हाल ही में बनाई गई",
|
||||
"resizeNotebooksPanel": "नोटबुक पैनल का आकार बदलें",
|
||||
"resizeSidebar": "साइडबार चौड़ाई बदलें",
|
||||
"revisionPanelBody": "SM-2 एल्गोरिथम के साथ फ्लैशकार्ड की समीक्षा करें। डेक आपके नोट्स से बनाए जाते हैं।",
|
||||
"revisionPanelBody": "फ्लैशकार्ड से दोहराएँ। अंतराल दोहराव उन्हें सही समय पर वापस लाता है। डेक आपके नोट्स से बनते हैं।",
|
||||
"searchNotebooksPlaceholder": "नोटबुक खोजें…",
|
||||
"searchShortcut": "खोज (Ctrl+K)"
|
||||
},
|
||||
@@ -130,7 +130,7 @@
|
||||
"add": "जोड़ें",
|
||||
"adding": "जोड़ रहे हैं...",
|
||||
"close": "बंद करें",
|
||||
"confirmDelete": "क्या आप वाकई इस नोट को हटाना चाहते हैं?",
|
||||
"confirmDelete": "यह नोट ट्रैश में जाएगा। आप इसे बाद में वापस ला सकते हैं।",
|
||||
"confirmLeaveShare": "क्या आप वाकई इस साझा नोट को छोड़ना चाहते हैं?",
|
||||
"sharedBy": "द्वारा साझा किया गया",
|
||||
"sharedShort": "साझा",
|
||||
@@ -991,7 +991,7 @@
|
||||
"dailyNotes": "दैनिक नोट्स",
|
||||
"dashboard": "डैशबोर्ड",
|
||||
"graphView": "लिंक मानचित्र",
|
||||
"insights": "सिमेंटिक विषय",
|
||||
"insights": "कड़ियाँ",
|
||||
"revision": "समीक्षा"
|
||||
},
|
||||
"settings": {
|
||||
@@ -2095,14 +2095,14 @@
|
||||
"collapse": "संकुचित करें"
|
||||
},
|
||||
"mcpSettings": {
|
||||
"title": "MCP",
|
||||
"title": "बाहरी उपकरण",
|
||||
"description": "API कुंजियाँ प्रबंधित करें और बाहरी टूल कॉन्फ़िगर करें",
|
||||
"tierRequired": "केवल Pro+",
|
||||
"upgradeHint": "MCP एक्सेस (Cursor, Claude Desktop आदि के लिए API कुंजियाँ) के लिए Pro या उससे उच्च योजना आवश्यक है। इस सुविधा को अनलॉक करने के लिए बिलिंग में अपग्रेड करें।",
|
||||
"whatIsMcp": {
|
||||
"title": "MCP क्या है?",
|
||||
"title": "यह किस काम का है?",
|
||||
"description": "मॉडल कॉन्टेक्स्ट प्रोटोकॉल (MCP) एक खुला प्रोटोकॉल है जो AI मॉडल को बाहरी टूल और डेटा स्रोतों के साथ सुरक्षित रूप से इंटरैक्ट करने में सक्षम बनाता है। MCP के साथ, आप Claude Code, Cursor या N8N जैसे टूल को अपने Memento इंस्टेंस से कनेक्ट करके प्रोग्रामेटिक रूप से अपने नोट्स को पढ़ सकते हैं, बना सकते हैं और व्यवस्थित कर सकते हैं।",
|
||||
"learnMore": "MCP के बारे में और जानें"
|
||||
"learnMore": "और जानें"
|
||||
},
|
||||
"serverStatus": {
|
||||
"title": "सर्वर स्थिति",
|
||||
@@ -2161,12 +2161,12 @@
|
||||
}
|
||||
},
|
||||
"helpBox": {
|
||||
"title": "MCP (Model Context Protocol) क्या है?",
|
||||
"title": "कोई टूल कैसे जोड़ें?",
|
||||
"step1": "MCP एक प्रोटोकॉल है जो Memento के AI एजेंटों को बाहरी टूल (डेटाबेस, API, फ़ाइलें आदि) से जुड़ने की अनुमति देता है।",
|
||||
"step2": "Memento 22 टूल के साथ एक MCP सर्वर प्रदान करता है — आपके एजेंट नोट पढ़/बना सकते हैं, अपने डेटाबेस में खोज सकते हैं, नोटबुक प्रबंधित कर सकते हैं आदि।",
|
||||
"step3": "यहाँ एक API कुंजी बनाएँ, फिर इसे अपने MCP क्लाइंट (Claude Desktop, Cursor, Continue.dev…) में सर्वर URL के साथ कॉन्फ़िगर करें।",
|
||||
"step4": "कॉन्फ़िगरेशन प्रारूप: MCP सर्वर URL + Authorization हेडर में आपकी कुंजी।",
|
||||
"step4Link": "MCP दस्तावेज़",
|
||||
"step4Link": "आधिकारिक मदद",
|
||||
"step5": "उपयोग का मामला: Claude Desktop से Memento में नोट लिखने, अपने नोटबुक में खोजने या एजेंट बनाने के लिए कहें।"
|
||||
}
|
||||
},
|
||||
@@ -2334,6 +2334,17 @@
|
||||
"title": "टेम्पलेट",
|
||||
"install": "इंस्टॉल करें",
|
||||
"installing": "इंस्टॉल हो रहा है...",
|
||||
"seeAll": "सभी देखें",
|
||||
"showLess": "कम दिखाएँ",
|
||||
"categoryAll": "सभी",
|
||||
"categoryWatch": "खबरें",
|
||||
"categoryDigest": "सारांश",
|
||||
"categoryTools": "उपकरण",
|
||||
"categoryGenerate": "बनाना",
|
||||
"taskExtractor": {
|
||||
"name": "नोट्स में काम",
|
||||
"description": "आपके नोट्स में काम ढूँढकर एक जगह इकट्ठा करता है।"
|
||||
},
|
||||
"veilleAI": {
|
||||
"name": "AI वॉच",
|
||||
"description": "5 AI विशेष साइटों से डेटा एकत्र करता है और साप्ताहिक सारांश बनाता है।"
|
||||
@@ -2453,7 +2464,7 @@
|
||||
"slideStyle": "दृश्य शैली कोने की त्रिज्या, रिक्ति और सूचना घनत्व को प्रभावित करती है।"
|
||||
}
|
||||
},
|
||||
"intelligenceOS": "इंटेलिजेंस ओएस"
|
||||
"intelligenceOS": "एजेंट"
|
||||
},
|
||||
"chat": {
|
||||
"title": "AI चैट",
|
||||
@@ -2740,7 +2751,7 @@
|
||||
"slashDatabaseDesc": "अपने नोटबुक के संरचित डेटा को एम्बेड करें",
|
||||
"slashLinkPreview": "लिंक पूर्वावलोकन",
|
||||
"slashLinkPreviewDesc": "URL को विज़ुअल कार्ड में बदलें",
|
||||
"slashLivingBlock": "लाइव ब्लॉक",
|
||||
"slashLivingBlock": "जुड़ा ब्लॉक",
|
||||
"slashLivingBlockDesc": "दूसरी नोट से सम्मिलित करें",
|
||||
"slashMath": "समीकरण",
|
||||
"slashMathDesc": "LaTeX संकेतन में गणितीय सूत्र",
|
||||
@@ -3007,7 +3018,7 @@
|
||||
"proChat": "100 chat messages / month",
|
||||
"later": "बाद में",
|
||||
"upgradePricing": "Pro में अपग्रेड करें",
|
||||
"addApiKey": "अपनी स्वयं की API कुंजी का उपयोग करें (BYOK)",
|
||||
"addApiKey": "अपनी कुंजी इस्तेमाल करें",
|
||||
"featureBrainstormCreate": "Créations brainstorm",
|
||||
"featureBrainstormEnrich": "Enrichissements brainstorm",
|
||||
"featureBrainstormExpand": "Extensions brainstorm",
|
||||
@@ -3025,10 +3036,10 @@
|
||||
"outOfCredits": "क्रेडिट खत्म — विकल्प"
|
||||
},
|
||||
"byokSettings": {
|
||||
"title": "आपकी API कुंजियाँ (BYOK)",
|
||||
"title": "आपकी प्रदाता कुंजियाँ",
|
||||
"description": "Connect your own LLM provider keys to bypass Discovery Pack quotas. Keys are encrypted at rest.",
|
||||
"badgeActive": "BYOK सक्रिय",
|
||||
"tierRequired": "BYOK के लिए Pro प्लान या उच्चतर की आवश्यकता है। अपनी API कुंजियाँ कनेक्ट करने के लिए अपग्रेड करें।",
|
||||
"badgeActive": "कुंजियाँ सक्रिय",
|
||||
"tierRequired": "इस विकल्प के लिए Pro या उससे ऊपर चाहिए।",
|
||||
"provider": "प्रदाता",
|
||||
"providerPlaceholder": "प्रदाता चुनें",
|
||||
"alias": "लेबल (वैकल्पिक)",
|
||||
@@ -3167,6 +3178,9 @@
|
||||
"fetchInvoicesFailed": "बिलिंग इतिहास लोड करने में विफल।",
|
||||
"savePercent": "~17% बचाएँ",
|
||||
"cancelSubscription": "सदस्यता रद्द करें",
|
||||
"changeOffer": "योजना बदलें",
|
||||
"downgradeToFree": "मुफ़्त योजना पर वापस जाएँ",
|
||||
"cancellingNotice": "रद्दीकरण तय है — {date} तक पहुँच",
|
||||
"disabledByAdmin": "बिलिंग और अपग्रेड वर्तमान में अक्षम हैं। यदि आपको पहुँच चाहिए तो अपने व्यवस्थापक से संपर्क करें।",
|
||||
"tab": "बिलिंग",
|
||||
"creditsFromPacks": "पैक क्रेडिट",
|
||||
@@ -3232,7 +3246,7 @@
|
||||
"title": "वह पल जब आपका Second Brain जवाब देता है।",
|
||||
"desc": "लिखते समय Memento नोटबुक के बीच अर्थ-संबंध खोजता है — कीवर्ड नहीं, असली अवधारणा पुल।",
|
||||
"card0Label": "अभी मिला",
|
||||
"card0": "“आपका प्राइसिंग नोट पिछले पतझड़ के प्रतिस्पर्धी विश्लेषण से मेल खाता है।”",
|
||||
"card0": "“आपका कीमत वाला नोट पिछले पतझड़ के प्रतिस्पर्धी विश्लेषण से मेल खाता है।”",
|
||||
"card1Label": "पुल",
|
||||
"card1": "साझा थीम: बाधा के तहत पोजिशनिंग",
|
||||
"card2Label": "एक्शन",
|
||||
@@ -3245,7 +3259,7 @@
|
||||
"w0Label": "अगले रास्ते",
|
||||
"w0": "आखिरी नोट से 3 एक्शन",
|
||||
"w1Label": "दैनिक समीक्षा",
|
||||
"w1": "Inbox · AI · फ़्लैशकार्ड",
|
||||
"w1": "इनबॉक्स · खोजें · कार्ड",
|
||||
"w2Label": "Memory Echo",
|
||||
"w2": "रात में 2 नए कनेक्शन",
|
||||
"w3Label": "एजेंट",
|
||||
@@ -3284,34 +3298,37 @@
|
||||
"title": "भारी काम सौंपें।",
|
||||
"desc": "रिसर्च, स्क्रैप, स्लाइड, डायग्राम, मॉनिटरिंग — एजेंट जो आपके Second Brain में लिखते हैं।",
|
||||
"scraper": {
|
||||
"title": "Scraper",
|
||||
"title": "मॉनिटर",
|
||||
"desc": "URL और RSS → छवियों वाली संश्लेषित नोट्स।"
|
||||
},
|
||||
"researcher": {
|
||||
"title": "Researcher",
|
||||
"title": "शोधकर्ता",
|
||||
"desc": "गहन क्वेरी, स्रोत, संरचित रिसर्च नोट्स।"
|
||||
},
|
||||
"slideGen": {
|
||||
"title": "Slide Gen",
|
||||
"title": "स्लाइड्स",
|
||||
"desc": "नोट्स → डेक या इंटरैक्टिव HTML स्लाइड।"
|
||||
},
|
||||
"monitor": {
|
||||
"title": "Monitor",
|
||||
"title": "पर्यवेक्षक",
|
||||
"desc": "नोटबुक पर नज़र: रुझान और इनसाइट।"
|
||||
},
|
||||
"diagramGen": {
|
||||
"title": "Diagram Gen",
|
||||
"title": "आरेख",
|
||||
"desc": "विचार → Excalidraw माइंडमैप और फ़्लो।"
|
||||
},
|
||||
"custom": {
|
||||
"title": "Custom",
|
||||
"title": "कस्टम",
|
||||
"desc": "आपकी भूमिकाएँ, स्रोत और शेड्यूल।"
|
||||
}
|
||||
},
|
||||
"byok": {
|
||||
"label": "कोई लॉक-इन नहीं",
|
||||
"label": "जब चाहें बदलें",
|
||||
"title": "आपकी कुंजियाँ। आपके मॉडल। आपका Second Brain।",
|
||||
"desc": "Memento क्रेडिट या OpenAI, Anthropic, Google… एक क्लिक में बदलें।"
|
||||
"desc": "Memento क्रेडिट, या अपना प्रदाता। एक क्लिक में बदलें — उत्पाद आपका रहता है।",
|
||||
"pointCredits": "सरल रखना हो तो Memento क्रेडिट",
|
||||
"pointProvider": "या अपना प्रदाता जोड़ें",
|
||||
"pointYours": "कुंजी आपके पास रहती है — यहाँ कभी नहीं दिखती"
|
||||
},
|
||||
"pricing": {
|
||||
"label": "Pricing",
|
||||
@@ -3339,7 +3356,7 @@
|
||||
"desc": "गंभीर विचारकों के लिए।",
|
||||
"cta": "Pro लें",
|
||||
"feature0": "असीमित नोट्स",
|
||||
"feature1": "BYOK",
|
||||
"feature1": "अपनी कुंजियाँ",
|
||||
"feature2": "200 सिमेंटिक सर्च / महीना",
|
||||
"feature3": "एजेंट (12 रन/महीना)",
|
||||
"feature4": "30 दिन इतिहास",
|
||||
@@ -3350,11 +3367,11 @@
|
||||
"desc": "टीम Second Brain।",
|
||||
"cta": "Business चुनें",
|
||||
"feature0": "10 सहयोगी",
|
||||
"feature1": "BYOK · 13 प्रोवाइडर",
|
||||
"feature1": "अपनी कुंजियाँ · {count} प्रदाता",
|
||||
"feature2": "1,000 सिमेंटिक सर्च",
|
||||
"feature3": "एजेंट (60 रन/महीना)",
|
||||
"feature4": "असीमित ब्रेनस्टॉर्म",
|
||||
"feature5": "API / MCP"
|
||||
"feature5": "बाहरी उपकरण"
|
||||
},
|
||||
"enterprise": {
|
||||
"name": "Enterprise",
|
||||
@@ -3545,12 +3562,12 @@
|
||||
"feature_search_title": "सिमेंटिक खोज",
|
||||
"feature_search_desc": "केवल कीवर्ड से नहीं, अर्थ से कोई भी नोट खोजें।",
|
||||
"feature_flashcards_title": "AI फ्लैशकार्ड",
|
||||
"feature_flashcards_desc": "एक क्लिक में अपने नोट्स से SRS समीक्षा कार्ड बनाएं।",
|
||||
"feature_flashcards_desc": "एक क्लिक में अपने नोट्स से समीक्षा कार्ड बनाएं।",
|
||||
"feature_brainstorm_title": "AI ब्रेनस्टॉर्म",
|
||||
"feature_brainstorm_desc": "AI-संचालित सहयोगी ब्रेनस्टॉर्मिंग सत्र।",
|
||||
"feature_chat_title": "नोट्स से चैट करें",
|
||||
"feature_chat_desc": "अपने व्यक्तिगत ज्ञान आधार से प्रश्न पूछें।",
|
||||
"feature_insights_title": "सिमेंटिक इनसाइट्स",
|
||||
"feature_insights_title": "कड़ियाँ",
|
||||
"feature_insights_desc": "अपने विचारों के बीच छुपे संबंध खोजें।",
|
||||
"feature_export_title": "Markdown निर्यात",
|
||||
"feature_export_desc": "अपने नोट्स को मानक Markdown प्रारूप में आयात और निर्यात करें।",
|
||||
@@ -3638,9 +3655,10 @@
|
||||
"createDiagramCostHint": "≈ 4 AI क्रेडिट"
|
||||
},
|
||||
"insightsView": {
|
||||
"title": "सिमेंटिक इनसाइट्स",
|
||||
"title": "कड़ियाँ",
|
||||
"toggleMenu": "मेनू दिखाएँ या छिपाएँ",
|
||||
"subtitle": "अपने ज्ञान की छिपी संरचना खोजें",
|
||||
"resync": "नेटवर्क पुनः सिंक करें",
|
||||
"resync": "अपडेट करें",
|
||||
"mapping": "मैपिंग…",
|
||||
"loading": "नोट लोड हो रहे हैं…",
|
||||
"mappingTitle": "आपका ज्ञान मैप किया जा रहा है…",
|
||||
@@ -3800,7 +3818,7 @@
|
||||
"apiKey": "API कुंजी",
|
||||
"apiUrl": "API URL",
|
||||
"baseUrlRequired": "API URL प्रदान करें",
|
||||
"byokActive": "BYOK सक्रिय",
|
||||
"byokActive": "कुंजियाँ सक्रिय",
|
||||
"choose": "चुनें…",
|
||||
"chooseModel": "एक मॉडल चुनें…",
|
||||
"chooseProvider": "एक प्रदाता चुनें…",
|
||||
@@ -4011,7 +4029,7 @@
|
||||
"tabProgress": "प्रगति",
|
||||
"tapToFlip": "पलटने के लिए स्पेस या टैप करें",
|
||||
"toolbarGenerate": "फ्लैशकार्ड बनाएं",
|
||||
"toolbarGenerateHint": "SM-2 अंतराल पुनरावृत्ति",
|
||||
"toolbarGenerateHint": "ये सही समय पर वापस आती हैं",
|
||||
"totalCardsLabel": "कुल कार्ड",
|
||||
"totalReviewsLabel": "कुल समीक्षाएं",
|
||||
"upToDate": "अपडेटेड",
|
||||
@@ -4055,6 +4073,9 @@
|
||||
"homeDashboard": {
|
||||
"activityEmptyHint": "पिछले 90 दिनों की अपनी लेखन लय देखने के लिए नोट्स संपादित करें।",
|
||||
"agentCreated": "एजेंट बनाया और शुरू किया गया",
|
||||
"agentCreatedInCard": "एजेंट तैयार है।",
|
||||
"agentOpenCreated": "एजेंट खोलें",
|
||||
"agentSeeNextSuggestion": "अगला देखें",
|
||||
"agentDiscovery": "एजेंट",
|
||||
"agentFailed": "निर्माण विफल",
|
||||
"agentsEmpty": "अभी तक कोई शोध एजेंट सुझाया नहीं गया। लिखते रहें — जब आपके नोट्स क्लस्टर होंगे तो Memento संश्लेषण विषय प्रस्तावित करेगा।",
|
||||
@@ -4063,6 +4084,8 @@
|
||||
"aiFound": "AI मिला",
|
||||
"aiProviderUnavailable": "AI अस्थायी रूप से अनुपलब्ध है। अपनी प्रदाता सेटिंग्स जांचें।",
|
||||
"allCaughtUp": "सब पूरा।",
|
||||
"remindersEmpty": "आज कोई अनुस्मारक नहीं।",
|
||||
"remindersOpenAll": "अनुस्मारक देखें",
|
||||
"alreadySeen": "देखा गया",
|
||||
"analyzeNotes": "मेरे नोट्स का विश्लेषण करें",
|
||||
"analyzing": "विश्लेषण कर रहा हूँ...",
|
||||
@@ -4180,6 +4203,8 @@
|
||||
"pulseReview": "समीक्षा हेतु {count}",
|
||||
"quickCapture": "त्वरित कैप्चर",
|
||||
"quickCapturePlaceholder": "एक विचार, एक भावना… इनबॉक्स में सहेजने के लिए Enter दबाएँ।",
|
||||
"captureGoesToFile": "इनबॉक्स में जाता है।",
|
||||
"captureSend": "इनबॉक्स में भेजें",
|
||||
"reminders": "अनुस्मारक",
|
||||
"resumeAlso": "हाल ही में",
|
||||
"resumeEmptyCta": "एक विचार कैप्चर करें",
|
||||
@@ -4267,6 +4292,7 @@
|
||||
"widgetHide": "विजेट छिपाएं",
|
||||
"widgetOpen": "खोलें",
|
||||
"widgetPinnedEmpty": "अभी तक कोई पिन की गई नोट नहीं।",
|
||||
"pinnedNoNotebook": "कोई नोटबुक नहीं",
|
||||
"widgetReset": "रीसेट करें",
|
||||
"widgetResetDone": "डिफ़ॉल्ट डैशबोर्ड पुनर्स्थापित।",
|
||||
"widgetStatsBridges": "ब्रिज",
|
||||
|
||||
@@ -31,14 +31,14 @@
|
||||
"confirmPasswordPlaceholder": "Conferma la tua password",
|
||||
"backToSite": "Indietro",
|
||||
"continueWithGoogle": "Continua con Google",
|
||||
"createYourSpace": "Crea il tuo spazio",
|
||||
"createYourSpaceSubtitle": "Unisciti alla nuova era degli appunti intelligenti.",
|
||||
"createYourSpace": "Crea il tuo secondo cervello",
|
||||
"createYourSpaceSubtitle": "Si collega mentre scrivi — non è un'altra app di appunti.",
|
||||
"forgot": "Dimenticato?",
|
||||
"oauthAccountNotLinked": "Questo account Google non corrisponde al tuo account esistente. Usa la stessa email o accedi con la tua password.",
|
||||
"privacyTerms": "© 2025 Memento Labs — Privacy · Termini",
|
||||
"sessionExpired": "Il tuo sito viene generato con navigazione e sommario",
|
||||
"welcomeBack": "Bentornato",
|
||||
"welcomeBackSubtitle": "Inserisci le tue credenziali per accedere alle tue note.",
|
||||
"welcomeBackSubtitle": "Accedi per ritrovare le tue note, i collegamenti e ciò che avevi dimenticato.",
|
||||
"checkEmailTitle": "Controlla la tua e-mail",
|
||||
"checkEmailDescription": "Abbiamo inviato un link di conferma a {email}. Aprilo per attivare l’account prima di accedere.",
|
||||
"checkEmailDescriptionGeneric": "Abbiamo inviato un link di conferma alla tua e-mail. Aprilo per attivare l’account prima di accedere.",
|
||||
@@ -99,13 +99,13 @@
|
||||
"darkMode": "Modalità scura",
|
||||
"dashboardPanelBody": "La tua sessione è scaduta. Effettua di nuovo l'accesso.",
|
||||
"documents": "Documenti",
|
||||
"insightsPanelBody": "Mappa semantica delle tue note: cluster tematici, note ponte e suggerimenti di connessione.",
|
||||
"insightsPanelBody": "Una mappa di come si collegano le tue note: temi vicini, note ponte e link da aprire.",
|
||||
"lightMode": "Modalità chiara",
|
||||
"notebookEmpty": "Vuoto",
|
||||
"recentNote": "Create di recente",
|
||||
"resizeNotebooksPanel": "Ridimensiona pannello quaderni",
|
||||
"resizeSidebar": "Ridimensiona larghezza sidebar",
|
||||
"revisionPanelBody": "Rivedi flashcard con l'algoritmo SM-2. I mazzi sono generati dalle tue note.",
|
||||
"revisionPanelBody": "Ripassa con le flashcard. La ripetizione dilazionata le riporta al momento giusto. I mazzi nascono dalle tue note.",
|
||||
"searchNotebooksPlaceholder": "Cerca quaderni…",
|
||||
"searchShortcut": "Cerca (Ctrl+K)"
|
||||
},
|
||||
@@ -130,7 +130,7 @@
|
||||
"add": "Aggiungi",
|
||||
"adding": "Aggiunta in corso...",
|
||||
"close": "Chiudi",
|
||||
"confirmDelete": "Sei sicuro di voler eliminare questa nota?",
|
||||
"confirmDelete": "Questa nota andrà nel cestino. Potrai recuperarla in seguito.",
|
||||
"confirmLeaveShare": "Sei sicuro di voler abbandonare questa nota condivisa?",
|
||||
"sharedBy": "Condivisa da",
|
||||
"sharedShort": "Condiviso",
|
||||
@@ -991,7 +991,7 @@
|
||||
"dailyNotes": "Note giornaliere",
|
||||
"dashboard": "Dashboard",
|
||||
"graphView": "Mappa link",
|
||||
"insights": "Temi semantici",
|
||||
"insights": "Connessioni",
|
||||
"revision": "Rivedi"
|
||||
},
|
||||
"settings": {
|
||||
@@ -2095,14 +2095,14 @@
|
||||
"collapse": "Comprimi"
|
||||
},
|
||||
"mcpSettings": {
|
||||
"title": "MCP",
|
||||
"title": "Strumenti esterni",
|
||||
"description": "Gestisci le chiavi API e configura gli strumenti esterni",
|
||||
"tierRequired": "Solo Pro+",
|
||||
"upgradeHint": "L'accesso MCP (chiavi API per Cursor, Claude Desktop, ecc.) richiede un piano Pro o superiore. Esegui l'upgrade in Fatturazione per sbloccare questa funzionalità.",
|
||||
"whatIsMcp": {
|
||||
"title": "Cos'è MCP?",
|
||||
"title": "A cosa serve?",
|
||||
"description": "Il Model Context Protocol (MCP) è un protocollo aperto che consente ai modelli di IA di interagire in modo sicuro con strumenti e fonti di dati esterni. Con MCP puoi collegare strumenti come Claude Code, Cursor o N8N alla tua istanza Memento per leggere, creare e organizzare le tue note a livello di programmazione.",
|
||||
"learnMore": "Scopri di più su MCP"
|
||||
"learnMore": "Per saperne di più"
|
||||
},
|
||||
"serverStatus": {
|
||||
"title": "Stato del server",
|
||||
@@ -2161,12 +2161,12 @@
|
||||
}
|
||||
},
|
||||
"helpBox": {
|
||||
"title": "Cos'è MCP (Model Context Protocol)?",
|
||||
"title": "Come collegare uno strumento?",
|
||||
"step1": "MCP è un protocollo che consente agli agenti IA di Memento di connettersi a strumenti esterni (database, API, file, ecc.).",
|
||||
"step2": "Memento espone un server MCP con 22 strumenti — i tuoi agenti possono leggere/creare note, cercare nel tuo database, gestire i taccuini, ecc.",
|
||||
"step3": "Crea una chiave API qui, poi configurala nel tuo client MCP (Claude Desktop, Cursor, Continue.dev…) con l'URL del server.",
|
||||
"step4": "Formato di configurazione: URL del server MCP + la tua chiave nell'header Authorization.",
|
||||
"step4Link": "Documentazione MCP",
|
||||
"step4Link": "Guida ufficiale",
|
||||
"step5": "Caso d'uso: chiedi a Claude Desktop di scrivere una nota in Memento, cercare nei tuoi taccuini o creare un agente."
|
||||
}
|
||||
},
|
||||
@@ -2334,6 +2334,17 @@
|
||||
"title": "Modelli",
|
||||
"install": "Installa",
|
||||
"installing": "Installazione in corso...",
|
||||
"seeAll": "Vedi tutti",
|
||||
"showLess": "Vedi meno",
|
||||
"categoryAll": "Tutti",
|
||||
"categoryWatch": "Rassegna",
|
||||
"categoryDigest": "Riassunti",
|
||||
"categoryTools": "Strumenti",
|
||||
"categoryGenerate": "Crea",
|
||||
"taskExtractor": {
|
||||
"name": "Compiti nelle note",
|
||||
"description": "Trova i compiti nelle tue note e li raccoglie in un unico posto."
|
||||
},
|
||||
"veilleAI": {
|
||||
"name": "Watch IA",
|
||||
"description": "Estrae contenuti da 5 siti specializzati in IA e genera un riepilogo settimanale."
|
||||
@@ -2453,7 +2464,7 @@
|
||||
"slideStyle": "Lo stile visivo influisce sul raggio dell'angolo, sulla spaziatura e sulla densità delle informazioni."
|
||||
}
|
||||
},
|
||||
"intelligenceOS": "Sistema operativo intelligente"
|
||||
"intelligenceOS": "Agenti"
|
||||
},
|
||||
"chat": {
|
||||
"title": "Chat IA",
|
||||
@@ -2740,7 +2751,7 @@
|
||||
"slashDatabaseDesc": "Incorpora i dati strutturati del tuo quaderno",
|
||||
"slashLinkPreview": "Anteprima link",
|
||||
"slashLinkPreviewDesc": "Trasforma un URL in una scheda visiva",
|
||||
"slashLivingBlock": "Blocco live",
|
||||
"slashLivingBlock": "Blocco collegato",
|
||||
"slashLivingBlockDesc": "Inserisci da un'altra nota",
|
||||
"slashMath": "Equazione",
|
||||
"slashMathDesc": "Formula matematica in notazione LaTeX",
|
||||
@@ -3007,7 +3018,7 @@
|
||||
"proChat": "100 chat messages / month",
|
||||
"later": "Più tardi",
|
||||
"upgradePricing": "Passa a Pro",
|
||||
"addApiKey": "Usa la tua chiave API (BYOK)",
|
||||
"addApiKey": "Usa la tua chiave",
|
||||
"featureBrainstormCreate": "Créations brainstorm",
|
||||
"featureBrainstormEnrich": "Enrichissements brainstorm",
|
||||
"featureBrainstormExpand": "Extensions brainstorm",
|
||||
@@ -3025,10 +3036,10 @@
|
||||
"outOfCredits": "Crediti esauriti — opzioni"
|
||||
},
|
||||
"byokSettings": {
|
||||
"title": "Le tue chiavi API (BYOK)",
|
||||
"title": "Le tue chiavi fornitore",
|
||||
"description": "Connect your own LLM provider keys to bypass Discovery Pack quotas. Keys are encrypted at rest.",
|
||||
"badgeActive": "BYOK attivo",
|
||||
"tierRequired": "BYOK richiede un piano Pro o superiore. Passa a Pro per collegare le tue chiavi API.",
|
||||
"badgeActive": "Chiavi attive",
|
||||
"tierRequired": "Questa opzione richiede un piano Pro o superiore.",
|
||||
"provider": "Provider",
|
||||
"providerPlaceholder": "Seleziona un provider",
|
||||
"alias": "Etichetta (facoltativo)",
|
||||
@@ -3167,6 +3178,9 @@
|
||||
"fetchInvoicesFailed": "Impossibile caricare lo storico delle fatture.",
|
||||
"savePercent": "Risparmia ~17%",
|
||||
"cancelSubscription": "Annulla abbonamento",
|
||||
"changeOffer": "Cambia offerta",
|
||||
"downgradeToFree": "Torna all’offerta gratuita",
|
||||
"cancellingNotice": "Disdetta prevista — accesso fino al {date}",
|
||||
"disabledByAdmin": "La fatturazione e gli upgrade sono attualmente disabilitati. Contatta il tuo amministratore se hai bisogno di accesso.",
|
||||
"tab": "Fatturazione",
|
||||
"creditsFromPacks": "Crediti da pacchetti",
|
||||
@@ -3239,13 +3253,13 @@
|
||||
"card2": "Apri entrambe le note affiancate — continua a scrivere"
|
||||
},
|
||||
"dashboard": {
|
||||
"eyebrow": "Dashboard Second Brain",
|
||||
"eyebrow": "La mattina",
|
||||
"title": "Una mattina che ti dice cosa conta.",
|
||||
"desc": "Il briefing del tuo Second Brain: prossimi percorsi, checklist, scoperte, ripasso.",
|
||||
"w0Label": "Prossimi percorsi",
|
||||
"w0": "3 azioni dalla tua ultima nota",
|
||||
"w1Label": "Review giornaliera",
|
||||
"w1": "Inbox · IA · Flashcard",
|
||||
"w1": "Posta · Scoperte · Schede",
|
||||
"w2Label": "Memory Echo",
|
||||
"w2": "2 nuove connessioni stanotte",
|
||||
"w3Label": "Agenti",
|
||||
@@ -3284,34 +3298,37 @@
|
||||
"title": "Delega il lavoro pesante.",
|
||||
"desc": "Ricerca, scrape, slide, diagrammi, monitoraggio — agenti che scrivono nel tuo Second Brain.",
|
||||
"scraper": {
|
||||
"title": "Scraper",
|
||||
"title": "Monitor",
|
||||
"desc": "URL e RSS → note sintetiche con immagini."
|
||||
},
|
||||
"researcher": {
|
||||
"title": "Researcher",
|
||||
"title": "Ricercatore",
|
||||
"desc": "Query profonde, fonti, note di ricerca strutturate."
|
||||
},
|
||||
"slideGen": {
|
||||
"title": "Slide Gen",
|
||||
"title": "Diapositive",
|
||||
"desc": "Note → deck o slide HTML interattive."
|
||||
},
|
||||
"monitor": {
|
||||
"title": "Monitor",
|
||||
"title": "Osservatore",
|
||||
"desc": "Monitora i taccuini: trend e insight."
|
||||
},
|
||||
"diagramGen": {
|
||||
"title": "Diagram Gen",
|
||||
"title": "Diagramma",
|
||||
"desc": "Idee → mindmap e flow Excalidraw."
|
||||
},
|
||||
"custom": {
|
||||
"title": "Custom",
|
||||
"title": "Personalizzato",
|
||||
"desc": "I tuoi ruoli, fonti e pianificazioni."
|
||||
}
|
||||
},
|
||||
"byok": {
|
||||
"label": "Niente lock-in",
|
||||
"label": "Libero di cambiare",
|
||||
"title": "Le tue chiavi. I tuoi modelli. Il tuo Second Brain.",
|
||||
"desc": "Crediti Memento oppure OpenAI, Anthropic, Google… Cambia provider in un clic."
|
||||
"desc": "Crediti Memento, oppure il tuo fornitore. Cambia in un clic: il prodotto resta tuo.",
|
||||
"pointCredits": "Crediti Memento, se vuoi restare sul semplice",
|
||||
"pointProvider": "Oppure collega il tuo fornitore",
|
||||
"pointYours": "La chiave resta da te: qui non viene mai mostrata"
|
||||
},
|
||||
"pricing": {
|
||||
"label": "Pricing",
|
||||
@@ -3339,7 +3356,7 @@
|
||||
"desc": "Per menti esigenti.",
|
||||
"cta": "Passa a Pro",
|
||||
"feature0": "Note illimitate",
|
||||
"feature1": "BYOK",
|
||||
"feature1": "Le tue chiavi",
|
||||
"feature2": "200 ricerche semantic / mese",
|
||||
"feature3": "Agenti (12 run/mese)",
|
||||
"feature4": "Cronologia 30 giorni",
|
||||
@@ -3350,11 +3367,11 @@
|
||||
"desc": "Second Brain di team.",
|
||||
"cta": "Scegli Business",
|
||||
"feature0": "10 collaboratori",
|
||||
"feature1": "BYOK · 13 provider",
|
||||
"feature1": "Le tue chiavi · {count} fornitori",
|
||||
"feature2": "1.000 ricerche semantic",
|
||||
"feature3": "Agenti (60 run/mese)",
|
||||
"feature4": "Brainstorm illimitato",
|
||||
"feature5": "API / MCP"
|
||||
"feature5": "Strumenti esterni"
|
||||
},
|
||||
"enterprise": {
|
||||
"name": "Enterprise",
|
||||
@@ -3545,12 +3562,12 @@
|
||||
"feature_search_title": "Ricerca semantica",
|
||||
"feature_search_desc": "Trova qualsiasi nota per significato, non solo per parole chiave.",
|
||||
"feature_flashcards_title": "Flashcard IA",
|
||||
"feature_flashcards_desc": "Genera schede di ripasso SRS dalle tue note in un clic.",
|
||||
"feature_flashcards_desc": "Genera schede di ripasso dalle tue note in un clic.",
|
||||
"feature_brainstorm_title": "Brainstorming IA",
|
||||
"feature_brainstorm_desc": "Sessioni di brainstorming collaborativo con IA.",
|
||||
"feature_chat_title": "Chatta con le tue note",
|
||||
"feature_chat_desc": "Fai domande alla tua base di conoscenza personale.",
|
||||
"feature_insights_title": "Approfondimenti semantici",
|
||||
"feature_insights_title": "Connessioni",
|
||||
"feature_insights_desc": "Scopri connessioni nascoste tra le tue idee.",
|
||||
"feature_export_title": "Esportazione Markdown",
|
||||
"feature_export_desc": "Importa ed esporta le tue note in formato Markdown.",
|
||||
@@ -3638,9 +3655,10 @@
|
||||
"createDiagramCostHint": "≈ 4 crediti IA"
|
||||
},
|
||||
"insightsView": {
|
||||
"title": "Insights semantici",
|
||||
"title": "Connessioni",
|
||||
"toggleMenu": "Mostra o nascondi il menu",
|
||||
"subtitle": "Scopri l'architettura nascosta della tua conoscenza",
|
||||
"resync": "Risincronizza rete",
|
||||
"resync": "Aggiorna",
|
||||
"mapping": "Mappatura…",
|
||||
"loading": "Caricamento delle note…",
|
||||
"mappingTitle": "Mappatura della tua conoscenza…",
|
||||
@@ -3800,7 +3818,7 @@
|
||||
"apiKey": "Chiave API",
|
||||
"apiUrl": "URL API",
|
||||
"baseUrlRequired": "Fornisci l'URL dell'API",
|
||||
"byokActive": "BYOK attivo",
|
||||
"byokActive": "Chiavi attive",
|
||||
"choose": "Scegli…",
|
||||
"chooseModel": "Scegli un modello…",
|
||||
"chooseProvider": "Scegli un provider…",
|
||||
@@ -4011,7 +4029,7 @@
|
||||
"tabProgress": "Progresso",
|
||||
"tapToFlip": "Spazio o tocca per girare",
|
||||
"toolbarGenerate": "Genera flashcard",
|
||||
"toolbarGenerateHint": "Ripetizione dilazionata SM-2",
|
||||
"toolbarGenerateHint": "Tornano al momento giusto",
|
||||
"totalCardsLabel": "Totale carte",
|
||||
"totalReviewsLabel": "Totale ripassi",
|
||||
"upToDate": "Aggiornato",
|
||||
@@ -4055,6 +4073,9 @@
|
||||
"homeDashboard": {
|
||||
"activityEmptyHint": "Modifica note per vedere il tuo ritmo di scrittura degli ultimi 90 giorni.",
|
||||
"agentCreated": "Agente creato e avviato",
|
||||
"agentCreatedInCard": "L'agente è pronto.",
|
||||
"agentOpenCreated": "Apri l'agente",
|
||||
"agentSeeNextSuggestion": "Vedi la successiva",
|
||||
"agentDiscovery": "Agente",
|
||||
"agentFailed": "Creazione fallita",
|
||||
"agentsEmpty": "Nessun agente di ricerca suggerito ancora. Continua a scrivere — Memento proporrà argomenti di sintesi quando le tue note si raggruppano.",
|
||||
@@ -4063,6 +4084,8 @@
|
||||
"aiFound": "IA trovato",
|
||||
"aiProviderUnavailable": "L'IA non è temporaneamente disponibile. Controlla le impostazioni del provider.",
|
||||
"allCaughtUp": "Tutto aggiornato.",
|
||||
"remindersEmpty": "Nessun promemoria per oggi.",
|
||||
"remindersOpenAll": "Vedi i promemoria",
|
||||
"alreadySeen": "visto",
|
||||
"analyzeNotes": "Analizza le mie note",
|
||||
"analyzing": "Analizzando...",
|
||||
@@ -4180,6 +4203,8 @@
|
||||
"pulseReview": "{count} da rivedere",
|
||||
"quickCapture": "Cattura rapida",
|
||||
"quickCapturePlaceholder": "Un'idea, un pensiero… Premi Invio per acquisire nell'inbox.",
|
||||
"captureGoesToFile": "Va nella posta in arrivo.",
|
||||
"captureSend": "Invia alla posta in arrivo",
|
||||
"reminders": "Promemoria",
|
||||
"resumeAlso": "Anche di recente",
|
||||
"resumeEmptyCta": "Cattura un'idea",
|
||||
@@ -4192,7 +4217,7 @@
|
||||
"suggestedBridge": "Collega {clusterA} & {clusterB}",
|
||||
"suggestedResearch": "Ricerca suggerita",
|
||||
"theme": "Tema",
|
||||
"themes": "temi",
|
||||
"themes": "Temi",
|
||||
"title": "Dashboard",
|
||||
"toOrganize": "da organizzare",
|
||||
"toReview": "Da rivedere",
|
||||
@@ -4251,7 +4276,7 @@
|
||||
"inbox": "Note senza quaderno. Archiviale per mantenere il tuo secondo cervello in ordine.",
|
||||
"intelligence": "Scoperte IA: collegamenti semantici tra note, idee ponte e risultati degli agenti.",
|
||||
"link-suggestions": "Passaggi da altre note vale la pena collegare al tuo lavoro attuale.",
|
||||
"mind-map": "Cluster di temi dimensionati in base al volume di note. Clicca per esplorare in Insights.",
|
||||
"mind-map": "Cluster di temi dimensionati in base al volume di note. Clicca per esplorare in Connessioni.",
|
||||
"next-paths": "Prossimi passi suggeriti in base all'ultima nota modificata: riprendi, collega, collega o ricerca.",
|
||||
"open-loops": "Note che hai iniziato ma non tocchi da 3+ giorni.",
|
||||
"pinned": "Accesso rapido alle note fissate.",
|
||||
@@ -4267,6 +4292,7 @@
|
||||
"widgetHide": "Nascondi widget",
|
||||
"widgetOpen": "Apri",
|
||||
"widgetPinnedEmpty": "Nessuna nota fissata ancora.",
|
||||
"pinnedNoNotebook": "Senza taccuino",
|
||||
"widgetReset": "Reimposta",
|
||||
"widgetResetDone": "Dashboard predefinita ripristinata.",
|
||||
"widgetStatsBridges": "Ponti",
|
||||
|
||||
@@ -31,14 +31,14 @@
|
||||
"confirmPasswordPlaceholder": "パスワードを再入力",
|
||||
"backToSite": "戻す",
|
||||
"continueWithGoogle": "Googleで続行",
|
||||
"createYourSpace": "スペースを作成",
|
||||
"createYourSpaceSubtitle": "スマートなノート取りの新時代へ。",
|
||||
"createYourSpace": "セカンドブレインを作る",
|
||||
"createYourSpaceSubtitle": "書いている最中につながります。ただのノートアプリではありません。",
|
||||
"forgot": "お忘れですか?",
|
||||
"oauthAccountNotLinked": "このGoogleアカウントは既存のアカウントと一致しません。同じメールアドレスを使用するか、パスワードでサインインしてください。",
|
||||
"privacyTerms": "© 2025 Memento Labs — プライバシー · 利用規約",
|
||||
"sessionExpired": "サイトはナビゲーションと目次付きで生成されます",
|
||||
"welcomeBack": "おかえりなさい",
|
||||
"welcomeBackSubtitle": "ノートにアクセスするには認証情報を入力してください。",
|
||||
"welcomeBackSubtitle": "ノート、つながり、忘れていたことを取り戻すためにサインイン。",
|
||||
"checkEmailTitle": "メールを確認してください",
|
||||
"checkEmailDescription": "{email} に確認リンクを送信しました。ログイン前に開いてアカウントを有効化してください。",
|
||||
"checkEmailDescriptionGeneric": "確認リンクをメールで送信しました。ログイン前に開いてアカウントを有効化してください。",
|
||||
@@ -99,13 +99,13 @@
|
||||
"darkMode": "ダークモード",
|
||||
"dashboardPanelBody": "セッションが期限切れです。再度サインインしてください。",
|
||||
"documents": "ドキュメント",
|
||||
"insightsPanelBody": "ノートのセマンティックマップ:テーマクラスター、ブリッジノート、接続の提案。",
|
||||
"insightsPanelBody": "ノートのつながりマップ:近いテーマ、橋渡しノート、開けるリンク。",
|
||||
"lightMode": "ライトモード",
|
||||
"notebookEmpty": "空",
|
||||
"recentNote": "最近作成",
|
||||
"resizeNotebooksPanel": "ノートブックパネルのサイズ変更",
|
||||
"resizeSidebar": "サイドバーの幅を変更",
|
||||
"revisionPanelBody": "SM-2アルゴリズムでフラッシュカードを復習。デッキはノートから生成されます。",
|
||||
"revisionPanelBody": "フラッシュカードで復習。間隔をあけた反復が、ちょうどよい時に戻してくれます。デッキはノートから作られます。",
|
||||
"searchNotebooksPlaceholder": "ノートブックを検索…",
|
||||
"searchShortcut": "検索 (Ctrl+K)"
|
||||
},
|
||||
@@ -130,7 +130,7 @@
|
||||
"add": "追加",
|
||||
"adding": "追加中...",
|
||||
"close": "閉じる",
|
||||
"confirmDelete": "このノートを本当に削除しますか?",
|
||||
"confirmDelete": "このノートはゴミ箱に入ります。後で復元できます。",
|
||||
"confirmLeaveShare": "この共有ノートを退出してもよろしいですか?",
|
||||
"sharedBy": "共有者",
|
||||
"sharedShort": "共有",
|
||||
@@ -991,7 +991,7 @@
|
||||
"dailyNotes": "デイリーメモ",
|
||||
"dashboard": "ダッシュボード",
|
||||
"graphView": "リンクマップ",
|
||||
"insights": "セマンティックテーマ",
|
||||
"insights": "つながり",
|
||||
"revision": "復習"
|
||||
},
|
||||
"settings": {
|
||||
@@ -2095,14 +2095,14 @@
|
||||
"collapse": "折りたたむ"
|
||||
},
|
||||
"mcpSettings": {
|
||||
"title": "MCP",
|
||||
"title": "外部ツール",
|
||||
"description": "APIキーの管理と外部ツールの設定",
|
||||
"tierRequired": "Pro+ のみ",
|
||||
"upgradeHint": "MCPアクセス(Cursor、Claude DesktopなどのAPIキー)にはProプラン以上が必要です。請求設定でアップグレードしてこの機能を有効にしてください。",
|
||||
"whatIsMcp": {
|
||||
"title": "MCPとは?",
|
||||
"title": "これは何のためのもの?",
|
||||
"description": "Model Context Protocol(MCP)は、AIモデルが外部ツールやデータソースと安全にやり取りできるようにするオープンプロトコルです。MCPを使用すると、Claude Code、Cursor、N8NなどのツールをMementoインスタンスに接続し、プログラムでノートの読み取り、作成、整理を行うことができます。",
|
||||
"learnMore": "MCPについて詳しく知る"
|
||||
"learnMore": "詳しく見る"
|
||||
},
|
||||
"serverStatus": {
|
||||
"title": "サーバーステータス",
|
||||
@@ -2161,12 +2161,12 @@
|
||||
}
|
||||
},
|
||||
"helpBox": {
|
||||
"title": "MCP(Model Context Protocol)とは?",
|
||||
"title": "ツールを接続するには?",
|
||||
"step1": "MCPは、MementoのAIエージェントが外部ツール(データベース、API、ファイルなど)に接続できるようにするプロトコルです。",
|
||||
"step2": "Mementoは22のツールを備えたMCPサーバーを提供します — エージェントはノートの読み取り/作成、データベースの検索、ノートブックの管理などが可能です。",
|
||||
"step3": "ここでAPIキーを作成し、MCPクライアント(Claude Desktop、Cursor、Continue.dev…)でサーバーURLと共に設定してください。",
|
||||
"step4": "設定形式:MCPサーバーURL + Authorizationヘッダーにあなたのキー。",
|
||||
"step4Link": "MCPドキュメント",
|
||||
"step4Link": "公式ヘルプ",
|
||||
"step5": "使用例:Claude DesktopにMementoへノートの作成、ノートブックの検索、エージェントの作成を依頼できます。"
|
||||
}
|
||||
},
|
||||
@@ -2334,6 +2334,17 @@
|
||||
"title": "テンプレート",
|
||||
"install": "インストール",
|
||||
"installing": "インストール中...",
|
||||
"seeAll": "すべて見る",
|
||||
"showLess": "少なく表示",
|
||||
"categoryAll": "すべて",
|
||||
"categoryWatch": "情報収集",
|
||||
"categoryDigest": "要約",
|
||||
"categoryTools": "ツール",
|
||||
"categoryGenerate": "作成",
|
||||
"taskExtractor": {
|
||||
"name": "ノート内のタスク",
|
||||
"description": "ノートからタスクを見つけて、一箇所にまとめます。"
|
||||
},
|
||||
"veilleAI": {
|
||||
"name": "AIウォッチ",
|
||||
"description": "AI専門の5サイトをスクレイピングし、週次まとめを生成します。"
|
||||
@@ -2453,7 +2464,7 @@
|
||||
"slideStyle": "視覚的なスタイルは、角の半径、間隔、情報密度に影響します。"
|
||||
}
|
||||
},
|
||||
"intelligenceOS": "インテリジェンスOS"
|
||||
"intelligenceOS": "エージェント"
|
||||
},
|
||||
"chat": {
|
||||
"title": "AIチャット",
|
||||
@@ -2740,7 +2751,7 @@
|
||||
"slashDatabaseDesc": "ノートブックの構造化データを埋め込む",
|
||||
"slashLinkPreview": "リンクプレビュー",
|
||||
"slashLinkPreviewDesc": "URLをビジュアルカードに変換",
|
||||
"slashLivingBlock": "ライブブロック",
|
||||
"slashLivingBlock": "リンクしたブロック",
|
||||
"slashLivingBlockDesc": "別のノートから挿入",
|
||||
"slashMath": "方程式",
|
||||
"slashMathDesc": "LaTeX記法の数学式",
|
||||
@@ -3007,7 +3018,7 @@
|
||||
"proChat": "100 chat messages / month",
|
||||
"later": "後で",
|
||||
"upgradePricing": "Proにアップグレード",
|
||||
"addApiKey": "独自のAPIキーを使用(BYOK)",
|
||||
"addApiKey": "自分のキーを使う",
|
||||
"featureBrainstormCreate": "Créations brainstorm",
|
||||
"featureBrainstormEnrich": "Enrichissements brainstorm",
|
||||
"featureBrainstormExpand": "Extensions brainstorm",
|
||||
@@ -3025,10 +3036,10 @@
|
||||
"outOfCredits": "クレジット切れ — オプション"
|
||||
},
|
||||
"byokSettings": {
|
||||
"title": "APIキー(BYOK)",
|
||||
"title": "自分のキー",
|
||||
"description": "Connect your own LLM provider keys to bypass Discovery Pack quotas. Keys are encrypted at rest.",
|
||||
"badgeActive": "BYOK有効",
|
||||
"tierRequired": "BYOKにはProプラン以上が必要です。APIキーを接続するにはアップグレードしてください。",
|
||||
"badgeActive": "キー有効",
|
||||
"tierRequired": "この機能には Pro 以上が必要です。",
|
||||
"provider": "プロバイダー",
|
||||
"providerPlaceholder": "プロバイダーを選択",
|
||||
"alias": "ラベル(任意)",
|
||||
@@ -3167,6 +3178,9 @@
|
||||
"fetchInvoicesFailed": "請求履歴を読み込めませんでした。",
|
||||
"savePercent": "~17%お得",
|
||||
"cancelSubscription": "サブスクリプションをキャンセル",
|
||||
"changeOffer": "プランを変更",
|
||||
"downgradeToFree": "無料プランに戻る",
|
||||
"cancellingNotice": "解約予定 — {date} まで利用できます",
|
||||
"disabledByAdmin": "請求とアップグレードは現在無効になっています。アクセスが必要な場合は管理者にお問い合わせください。",
|
||||
"tab": "お支払い",
|
||||
"creditsFromPacks": "パッククレジット",
|
||||
@@ -3245,7 +3259,7 @@
|
||||
"w0Label": "次の道筋",
|
||||
"w0": "最新ノートからの3アクション",
|
||||
"w1Label": "デイリーレビュー",
|
||||
"w1": "Inbox · AI · フラッシュカード",
|
||||
"w1": "受信箱 · 発見 · カード",
|
||||
"w2Label": "Memory Echo",
|
||||
"w2": "昨夜の新規接続2件",
|
||||
"w3Label": "エージェント",
|
||||
@@ -3284,34 +3298,37 @@
|
||||
"title": "重い仕事を任せる。",
|
||||
"desc": "調査、スクレイプ、スライド、図、監視——Second Brain に書き込むエージェント。",
|
||||
"scraper": {
|
||||
"title": "Scraper",
|
||||
"title": "モニター",
|
||||
"desc": "URL と RSS → 画像付きの合成ノート。"
|
||||
},
|
||||
"researcher": {
|
||||
"title": "Researcher",
|
||||
"title": "リサーチャー",
|
||||
"desc": "深いクエリ、ソース、構造化された調査ノート。"
|
||||
},
|
||||
"slideGen": {
|
||||
"title": "Slide Gen",
|
||||
"title": "スライド",
|
||||
"desc": "ノート → デッキや対話型 HTML スライド。"
|
||||
},
|
||||
"monitor": {
|
||||
"title": "Monitor",
|
||||
"title": "オブザーバー",
|
||||
"desc": "ノートブックを監視:トレンドとインサイト。"
|
||||
},
|
||||
"diagramGen": {
|
||||
"title": "Diagram Gen",
|
||||
"title": "ダイアグラム",
|
||||
"desc": "アイデア → Excalidraw のマインドマップとフロー。"
|
||||
},
|
||||
"custom": {
|
||||
"title": "Custom",
|
||||
"title": "カスタム",
|
||||
"desc": "役割、ソース、スケジュールを定義。"
|
||||
}
|
||||
},
|
||||
"byok": {
|
||||
"label": "ロックインなし",
|
||||
"label": "いつでも切り替え",
|
||||
"title": "あなたのキー。あなたのモデル。あなたの Second Brain。",
|
||||
"desc": "Memento クレジット、または OpenAI / Anthropic / Google… クリック一つで切替。"
|
||||
"desc": "Memento クレジット、または自分の提供者。クリック一つで切替。製品はあなたのままです。",
|
||||
"pointCredits": "シンプルに済ませるなら Memento クレジット",
|
||||
"pointProvider": "または自分の提供者を接続",
|
||||
"pointYours": "キーは手元に残ります。ここには表示しません"
|
||||
},
|
||||
"pricing": {
|
||||
"label": "Pricing",
|
||||
@@ -3339,7 +3356,7 @@
|
||||
"desc": "本気で考える人へ。",
|
||||
"cta": "Pro にする",
|
||||
"feature0": "ノート無制限",
|
||||
"feature1": "BYOK",
|
||||
"feature1": "自分のキー",
|
||||
"feature2": "意味検索 200/月",
|
||||
"feature3": "エージェント(12回/月)",
|
||||
"feature4": "履歴30日",
|
||||
@@ -3350,11 +3367,11 @@
|
||||
"desc": "チームの Second Brain。",
|
||||
"cta": "Business を選ぶ",
|
||||
"feature0": "共同編集者10人",
|
||||
"feature1": "BYOK · 13プロバイダ",
|
||||
"feature1": "自分のキー · 提供者 {count}",
|
||||
"feature2": "意味検索1000",
|
||||
"feature3": "エージェント(60回/月)",
|
||||
"feature4": "ブレインストーム無制限",
|
||||
"feature5": "API / MCP"
|
||||
"feature5": "外部ツール"
|
||||
},
|
||||
"enterprise": {
|
||||
"name": "Enterprise",
|
||||
@@ -3545,12 +3562,12 @@
|
||||
"feature_search_title": "セマンティック検索",
|
||||
"feature_search_desc": "キーワードだけでなく意味でノートを検索。",
|
||||
"feature_flashcards_title": "AIフラッシュカード",
|
||||
"feature_flashcards_desc": "ノートからSRS復習カードをワンクリックで生成。",
|
||||
"feature_flashcards_desc": "ノートから復習カードをワンクリックで生成。",
|
||||
"feature_brainstorm_title": "AIブレインストーミング",
|
||||
"feature_brainstorm_desc": "AI搭載の共同ブレインストーミングセッション。",
|
||||
"feature_chat_title": "ノートとチャット",
|
||||
"feature_chat_desc": "個人ナレッジベースに質問する。",
|
||||
"feature_insights_title": "セマンティックインサイト",
|
||||
"feature_insights_title": "つながり",
|
||||
"feature_insights_desc": "アイデア間の隠れた関係を発見。",
|
||||
"feature_export_title": "Markdownエクスポート",
|
||||
"feature_export_desc": "標準Markdown形式でノートをインポート/エクスポート。",
|
||||
@@ -3638,9 +3655,10 @@
|
||||
"createDiagramCostHint": "≈ 4 AIクレジット"
|
||||
},
|
||||
"insightsView": {
|
||||
"title": "セマンティックインサイト",
|
||||
"title": "つながり",
|
||||
"toggleMenu": "メニューを表示または隠す",
|
||||
"subtitle": "知識の隠された構造を発見",
|
||||
"resync": "ネットワークを再同期",
|
||||
"resync": "更新",
|
||||
"mapping": "マッピング中…",
|
||||
"loading": "ノート読み込み中…",
|
||||
"mappingTitle": "知識をマッピング中…",
|
||||
@@ -3800,7 +3818,7 @@
|
||||
"apiKey": "APIキー",
|
||||
"apiUrl": "API URL",
|
||||
"baseUrlRequired": "API URLを入力してください",
|
||||
"byokActive": "BYOK有効",
|
||||
"byokActive": "キー有効",
|
||||
"choose": "選択…",
|
||||
"chooseModel": "モデルを選択…",
|
||||
"chooseProvider": "プロバイダを選択…",
|
||||
@@ -4011,7 +4029,7 @@
|
||||
"tabProgress": "進捗",
|
||||
"tapToFlip": "スペースまたはタップで裏返す",
|
||||
"toolbarGenerate": "フラッシュカードを生成",
|
||||
"toolbarGenerateHint": "SM-2間隔反復",
|
||||
"toolbarGenerateHint": "ちょうどいいときに戻ってきます",
|
||||
"totalCardsLabel": "総カード数",
|
||||
"totalReviewsLabel": "総復習数",
|
||||
"upToDate": "最新",
|
||||
@@ -4055,6 +4073,9 @@
|
||||
"homeDashboard": {
|
||||
"activityEmptyHint": "ノートを編集して過去90日間の執筆リズムを確認します。",
|
||||
"agentCreated": "エージェントが作成・開始されました",
|
||||
"agentCreatedInCard": "エージェントの準備ができました。",
|
||||
"agentOpenCreated": "エージェントを開く",
|
||||
"agentSeeNextSuggestion": "次を見る",
|
||||
"agentDiscovery": "エージェント",
|
||||
"agentFailed": "作成に失敗しました",
|
||||
"agentsEmpty": "まだ研究エージェントの提案はありません。書き続けてください — ノートがクラスター化されると、Mementoが統合トピックを提案します。",
|
||||
@@ -4063,6 +4084,8 @@
|
||||
"aiFound": "AIが見つけました",
|
||||
"aiProviderUnavailable": "AIは一時的に利用できません。プロバイダの設定を確認してください。",
|
||||
"allCaughtUp": "すべて完了。",
|
||||
"remindersEmpty": "今日のリマインダーはありません。",
|
||||
"remindersOpenAll": "リマインダーを見る",
|
||||
"alreadySeen": "既読",
|
||||
"analyzeNotes": "メモを分析",
|
||||
"analyzing": "分析中…",
|
||||
@@ -4180,6 +4203,8 @@
|
||||
"pulseReview": "{count}件の復習対象",
|
||||
"quickCapture": "クイックキャプチャ",
|
||||
"quickCapturePlaceholder": "アイデア、考え… Enterで受信箱に保存。",
|
||||
"captureGoesToFile": "受信箱に入ります。",
|
||||
"captureSend": "受信箱に送る",
|
||||
"reminders": "リマインダー",
|
||||
"resumeAlso": "最近のその他",
|
||||
"resumeEmptyCta": "アイデアをキャプチャ",
|
||||
@@ -4267,6 +4292,7 @@
|
||||
"widgetHide": "ウィジェットを非表示",
|
||||
"widgetOpen": "開く",
|
||||
"widgetPinnedEmpty": "ピン留めされたノートはまだありません。",
|
||||
"pinnedNoNotebook": "ノートブックなし",
|
||||
"widgetReset": "リセット",
|
||||
"widgetResetDone": "デフォルトのダッシュボードが復元されました。",
|
||||
"widgetStatsBridges": "ブリッジ",
|
||||
|
||||
@@ -31,14 +31,14 @@
|
||||
"confirmPasswordPlaceholder": "비밀번호를 다시 입력하세요",
|
||||
"backToSite": "뒤로",
|
||||
"continueWithGoogle": "Google로 계속",
|
||||
"createYourSpace": "나의 공간 만들기",
|
||||
"createYourSpaceSubtitle": "스마트 노트 작성의 새로운 시대에 참여하세요.",
|
||||
"createYourSpace": "두 번째 뇌를 만들기",
|
||||
"createYourSpaceSubtitle": "쓰는 동안 연결됩니다. 또 하나의 노트 앱이 아닙니다.",
|
||||
"forgot": "잊으셨나요?",
|
||||
"oauthAccountNotLinked": "이 Google 계정이 기존 계정과 일치하지 않습니다. 같은 이메일을 사용하거나 비밀번호로 로그인하세요.",
|
||||
"privacyTerms": "© 2025 Memento Labs — 개인정보 · 약관",
|
||||
"sessionExpired": "사이트가 탐색 및 목차와 함께 생성됩니다",
|
||||
"welcomeBack": "다시 오신 것을 환영합니다",
|
||||
"welcomeBackSubtitle": "노트에 액세스하려면 자격 증명을 입력하세요.",
|
||||
"welcomeBackSubtitle": "노트와 연결, 그리고 잊었던 내용을 찾으려면 로그인하세요.",
|
||||
"checkEmailTitle": "이메일을 확인하세요",
|
||||
"checkEmailDescription": "{email}(으)로 확인 링크를 보냈습니다. 로그인하기 전에 열어 계정을 활성화하세요.",
|
||||
"checkEmailDescriptionGeneric": "확인 링크를 이메일로 보냈습니다. 로그인하기 전에 열어 계정을 활성화하세요.",
|
||||
@@ -99,13 +99,13 @@
|
||||
"darkMode": "다크 모드",
|
||||
"dashboardPanelBody": "세션이 만료되었습니다. 다시 로그인해 주세요.",
|
||||
"documents": "문서",
|
||||
"insightsPanelBody": "노트의 시맨틱 지도: 테마 클러스터, 브리지 노트, 연결 제안.",
|
||||
"insightsPanelBody": "노트가 어떻게 연결되는지 보여주는 지도: 가까운 주제, 다리 노트, 열 수 있는 링크.",
|
||||
"lightMode": "라이트 모드",
|
||||
"notebookEmpty": "비어있음",
|
||||
"recentNote": "최근 생성됨",
|
||||
"resizeNotebooksPanel": "노트북 패널 크기 조정",
|
||||
"resizeSidebar": "사이드바 너비 조정",
|
||||
"revisionPanelBody": "SM-2 알고리즘으로 플래시카드를 복습하세요. 덱은 노트에서 생성됩니다.",
|
||||
"revisionPanelBody": "플래시카드로 복습하세요. 간격 반복이 알맞은 때에 다시 보여 줍니다. 덱은 노트에서 만들어집니다.",
|
||||
"searchNotebooksPlaceholder": "노트북 검색…",
|
||||
"searchShortcut": "검색 (Ctrl+K)"
|
||||
},
|
||||
@@ -130,7 +130,7 @@
|
||||
"add": "추가",
|
||||
"adding": "추가 중...",
|
||||
"close": "닫기",
|
||||
"confirmDelete": "이 노트를 정말 삭제하시겠습니까?",
|
||||
"confirmDelete": "이 노트는 휴지통으로 이동합니다. 나중에 복원할 수 있습니다.",
|
||||
"confirmLeaveShare": "이 공유 메모를 나가시겠습니까?",
|
||||
"sharedBy": "공유자",
|
||||
"sharedShort": "공유됨",
|
||||
@@ -991,7 +991,7 @@
|
||||
"dailyNotes": "일일 노트",
|
||||
"dashboard": "대시보드",
|
||||
"graphView": "링크 맵",
|
||||
"insights": "시맨틱 테마",
|
||||
"insights": "연결",
|
||||
"revision": "복습"
|
||||
},
|
||||
"settings": {
|
||||
@@ -2095,14 +2095,14 @@
|
||||
"collapse": "접기"
|
||||
},
|
||||
"mcpSettings": {
|
||||
"title": "MCP",
|
||||
"title": "외부 도구",
|
||||
"description": "API 키 관리 및 외부 도구 구성",
|
||||
"tierRequired": "Pro+ 전용",
|
||||
"upgradeHint": "MCP 액세스(Cursor, Claude Desktop 등의 API 키)에는 Pro 플랜 이상이 필요합니다. 결제에서 업그레이드하여 이 기능을 잠금 해제하세요.",
|
||||
"whatIsMcp": {
|
||||
"title": "MCP란 무엇인가요?",
|
||||
"title": "어디에 쓰이나요?",
|
||||
"description": "Model Context Protocol(MCP)은 AI 모델이 외부 도구 및 데이터 소스와 안전하게 상호 작용할 수 있게 하는 오픈 프로토콜입니다. MCP를 사용하면 Claude Code, Cursor, N8N 등의 도구를 Memento 인스턴스에 연결하여 프로그래밍 방식으로 노트를 읽고, 만들고, 정리할 수 있습니다.",
|
||||
"learnMore": "MCP에 대해 자세히 알아보기"
|
||||
"learnMore": "자세히 알아보기"
|
||||
},
|
||||
"serverStatus": {
|
||||
"title": "서버 상태",
|
||||
@@ -2161,12 +2161,12 @@
|
||||
}
|
||||
},
|
||||
"helpBox": {
|
||||
"title": "MCP(Model Context Protocol)란 무엇인가요?",
|
||||
"title": "도구를 어떻게 연결하나요?",
|
||||
"step1": "MCP는 Memento의 AI 에이전트가 외부 도구(데이터베이스, API, 파일 등)에 연결할 수 있게 해주는 프로토콜입니다.",
|
||||
"step2": "Memento는 22개의 도구를 갖춘 MCP 서버를 제공합니다 — 에이전트가 노트를 읽고/만들고, 데이터베이스를 검색하고, 노트북을 관리할 수 있습니다.",
|
||||
"step3": "여기서 API 키를 생성한 후 MCP 클라이언트(Claude Desktop, Cursor, Continue.dev…)에서 서버 URL과 함께 설정하세요.",
|
||||
"step4": "설정 형식: MCP 서버 URL + Authorization 헤더에 키.",
|
||||
"step4Link": "MCP 문서",
|
||||
"step4Link": "공식 도움말",
|
||||
"step5": "사용 사례: Claude Desktop에 Memento에 노트 작성, 노트북 검색 또는 에이전트 생성을 요청하세요."
|
||||
}
|
||||
},
|
||||
@@ -2334,6 +2334,17 @@
|
||||
"title": "템플릿",
|
||||
"install": "설치",
|
||||
"installing": "설치 중...",
|
||||
"seeAll": "모두 보기",
|
||||
"showLess": "접기",
|
||||
"categoryAll": "전체",
|
||||
"categoryWatch": "소식 추적",
|
||||
"categoryDigest": "요약",
|
||||
"categoryTools": "도구",
|
||||
"categoryGenerate": "만들기",
|
||||
"taskExtractor": {
|
||||
"name": "노트의 할 일",
|
||||
"description": "노트에서 할 일을 찾아 한곳에 모읍니다."
|
||||
},
|
||||
"veilleAI": {
|
||||
"name": "AI 와치",
|
||||
"description": "AI 전문 사이트 5곳을 스크랩하여 주간 요약을 생성합니다."
|
||||
@@ -2453,7 +2464,7 @@
|
||||
"slideStyle": "시각적 스타일은 모서리 반경, 간격 및 정보 밀도에 영향을 미칩니다."
|
||||
}
|
||||
},
|
||||
"intelligenceOS": "인텔리전스 OS"
|
||||
"intelligenceOS": "에이전트"
|
||||
},
|
||||
"chat": {
|
||||
"title": "AI 채팅",
|
||||
@@ -2740,7 +2751,7 @@
|
||||
"slashDatabaseDesc": "노트북의 구조화된 데이터 임베드",
|
||||
"slashLinkPreview": "링크 미리보기",
|
||||
"slashLinkPreviewDesc": "URL을 시각적 카드로 변환",
|
||||
"slashLivingBlock": "라이브 블록",
|
||||
"slashLivingBlock": "연결된 블록",
|
||||
"slashLivingBlockDesc": "다른 노트에서 삽입",
|
||||
"slashMath": "방정식",
|
||||
"slashMathDesc": "LaTeX 표기법의 수학 공식",
|
||||
@@ -3007,7 +3018,7 @@
|
||||
"proChat": "100 chat messages / month",
|
||||
"later": "나중에",
|
||||
"upgradePricing": "Pro로 업그레이드",
|
||||
"addApiKey": "자체 API 키 사용 (BYOK)",
|
||||
"addApiKey": "내 키 사용",
|
||||
"featureBrainstormCreate": "Créations brainstorm",
|
||||
"featureBrainstormEnrich": "Enrichissements brainstorm",
|
||||
"featureBrainstormExpand": "Extensions brainstorm",
|
||||
@@ -3025,10 +3036,10 @@
|
||||
"outOfCredits": "크레딧 소진 — 옵션"
|
||||
},
|
||||
"byokSettings": {
|
||||
"title": "API 키 (BYOK)",
|
||||
"title": "내 키",
|
||||
"description": "Connect your own LLM provider keys to bypass Discovery Pack quotas. Keys are encrypted at rest.",
|
||||
"badgeActive": "BYOK 활성화",
|
||||
"tierRequired": "BYOK는 Pro 플랜 이상이 필요합니다. API 키를 연결하려면 업그레이드하세요.",
|
||||
"badgeActive": "키 사용 중",
|
||||
"tierRequired": "이 기능은 Pro 이상에서 사용할 수 있습니다.",
|
||||
"provider": "제공자",
|
||||
"providerPlaceholder": "제공자 선택",
|
||||
"alias": "라벨 (선택)",
|
||||
@@ -3167,6 +3178,9 @@
|
||||
"fetchInvoicesFailed": "결제 내역을 불러올 수 없습니다.",
|
||||
"savePercent": "~17% 절약",
|
||||
"cancelSubscription": "구독 취소",
|
||||
"changeOffer": "요금제 변경",
|
||||
"downgradeToFree": "무료 요금제로 돌아가기",
|
||||
"cancellingNotice": "해지 예정 — {date}까지 이용 가능",
|
||||
"disabledByAdmin": "결제 및 업그레이드가 현재 비활성화되어 있습니다. 액세스가 필요한 경우 관리자에게 문의하세요.",
|
||||
"tab": "결제",
|
||||
"creditsFromPacks": "패키지 크레딧",
|
||||
@@ -3245,7 +3259,7 @@
|
||||
"w0Label": "다음 경로",
|
||||
"w0": "최근 노트에서 나온 3가지 행동",
|
||||
"w1Label": "일일 리뷰",
|
||||
"w1": "Inbox · AI · 플래시카드",
|
||||
"w1": "받은편지함 · 발견 · 카드",
|
||||
"w2Label": "Memory Echo",
|
||||
"w2": "어젯밤 새 연결 2개",
|
||||
"w3Label": "에이전트",
|
||||
@@ -3284,34 +3298,37 @@
|
||||
"title": "무거운 일을 맡기세요.",
|
||||
"desc": "리서치, 스크랩, 슬라이드, 다이어그램, 모니터링 — Second Brain에 쓰는 에이전트.",
|
||||
"scraper": {
|
||||
"title": "Scraper",
|
||||
"title": "모니터",
|
||||
"desc": "URL & RSS → 이미지 포함 합성 노트."
|
||||
},
|
||||
"researcher": {
|
||||
"title": "Researcher",
|
||||
"title": "리서처",
|
||||
"desc": "깊은 질의, 출처, 구조화된 리서치 노트."
|
||||
},
|
||||
"slideGen": {
|
||||
"title": "Slide Gen",
|
||||
"title": "슬라이드",
|
||||
"desc": "노트 → 덱 또는 인터랙티브 HTML 슬라이드."
|
||||
},
|
||||
"monitor": {
|
||||
"title": "Monitor",
|
||||
"title": "관찰자",
|
||||
"desc": "노트북 감시: 트렌드와 인사이트."
|
||||
},
|
||||
"diagramGen": {
|
||||
"title": "Diagram Gen",
|
||||
"title": "도표",
|
||||
"desc": "아이디어 → Excalidraw 마인드맵 & 플로우."
|
||||
},
|
||||
"custom": {
|
||||
"title": "Custom",
|
||||
"title": "사용자 정의",
|
||||
"desc": "역할, 출처, 스케줄 정의."
|
||||
}
|
||||
},
|
||||
"byok": {
|
||||
"label": "락인 없음",
|
||||
"label": "언제든 변경",
|
||||
"title": "당신의 키. 당신의 모델. 당신의 Second Brain.",
|
||||
"desc": "Memento 크레딧 또는 OpenAI, Anthropic, Google… 클릭 한 번으로 전환."
|
||||
"desc": "Memento 크레딧, 또는 자신의 제공자. 클릭 한 번으로 전환 — 제품은 당신의 것입니다.",
|
||||
"pointCredits": "간단하게 쓰려면 Memento 크레딧",
|
||||
"pointProvider": "또는 자신의 제공자를 연결",
|
||||
"pointYours": "키는 당신에게 남습니다. 여기에는 표시되지 않습니다"
|
||||
},
|
||||
"pricing": {
|
||||
"label": "Pricing",
|
||||
@@ -3339,7 +3356,7 @@
|
||||
"desc": "진지한 사유를 위해.",
|
||||
"cta": "Pro로 가기",
|
||||
"feature0": "무제한 노트",
|
||||
"feature1": "BYOK",
|
||||
"feature1": "내 키 사용",
|
||||
"feature2": "의미 검색 200/월",
|
||||
"feature3": "에이전트(12회/월)",
|
||||
"feature4": "기록 30일",
|
||||
@@ -3350,11 +3367,11 @@
|
||||
"desc": "팀 Second Brain.",
|
||||
"cta": "Business 선택",
|
||||
"feature0": "협업자 10명",
|
||||
"feature1": "BYOK · 공급자 13",
|
||||
"feature1": "내 키 · 제공자 {count}",
|
||||
"feature2": "의미 검색 1,000",
|
||||
"feature3": "에이전트(60회/월)",
|
||||
"feature4": "무제한 브레인스토밍",
|
||||
"feature5": "API / MCP"
|
||||
"feature5": "외부 도구"
|
||||
},
|
||||
"enterprise": {
|
||||
"name": "Enterprise",
|
||||
@@ -3545,12 +3562,12 @@
|
||||
"feature_search_title": "시맨틱 검색",
|
||||
"feature_search_desc": "키워드뿐만 아니라 의미로 노트를 찾아보세요.",
|
||||
"feature_flashcards_title": "AI 플래시카드",
|
||||
"feature_flashcards_desc": "노트에서 SRS 복습 카드를 한 번의 클릭으로 생성하세요.",
|
||||
"feature_flashcards_desc": "노트에서 복습 카드를 한 번의 클릭으로 생성하세요.",
|
||||
"feature_brainstorm_title": "AI 브레인스토밍",
|
||||
"feature_brainstorm_desc": "AI 기반 협업 브레인스토밍 세션.",
|
||||
"feature_chat_title": "노트와 채팅",
|
||||
"feature_chat_desc": "개인 지식 베이스에 질문하세요.",
|
||||
"feature_insights_title": "시맨틱 인사이트",
|
||||
"feature_insights_title": "연결",
|
||||
"feature_insights_desc": "아이디어 간의 숨겨진 연결을 발견하세요.",
|
||||
"feature_export_title": "Markdown 내보내기",
|
||||
"feature_export_desc": "표준 Markdown 형식으로 노트를 가져오거나 내보내세요.",
|
||||
@@ -3638,9 +3655,10 @@
|
||||
"createDiagramCostHint": "≈ AI 크레딧 4"
|
||||
},
|
||||
"insightsView": {
|
||||
"title": "시맨틱 인사이트",
|
||||
"title": "연결",
|
||||
"toggleMenu": "메뉴 표시 또는 숨기기",
|
||||
"subtitle": "지식의 숨겨진 구조 발견",
|
||||
"resync": "네트워크 재동기화",
|
||||
"resync": "업데이트",
|
||||
"mapping": "매핑 중…",
|
||||
"loading": "노트 로딩 중…",
|
||||
"mappingTitle": "지식 매핑 중…",
|
||||
@@ -3800,7 +3818,7 @@
|
||||
"apiKey": "API 키",
|
||||
"apiUrl": "API URL",
|
||||
"baseUrlRequired": "API URL을 제공하세요",
|
||||
"byokActive": "BYOK 활성",
|
||||
"byokActive": "키 사용 중",
|
||||
"choose": "선택…",
|
||||
"chooseModel": "모델 선택…",
|
||||
"chooseProvider": "제공자 선택…",
|
||||
@@ -4011,7 +4029,7 @@
|
||||
"tabProgress": "진행률",
|
||||
"tapToFlip": "스페이스 또는 탭하여 뒤집기",
|
||||
"toolbarGenerate": "플래시카드 생성",
|
||||
"toolbarGenerateHint": "SM-2 간격 반복",
|
||||
"toolbarGenerateHint": "알맞은 때에 다시 나옵니다",
|
||||
"totalCardsLabel": "총 카드",
|
||||
"totalReviewsLabel": "총 복습",
|
||||
"upToDate": "최신",
|
||||
@@ -4055,6 +4073,9 @@
|
||||
"homeDashboard": {
|
||||
"activityEmptyHint": "노트를 편집하여 지난 90일간의 작성 리듬을 확인하세요.",
|
||||
"agentCreated": "에이전트가 생성되고 시작되었습니다",
|
||||
"agentCreatedInCard": "에이전트가 준비되었습니다.",
|
||||
"agentOpenCreated": "에이전트 열기",
|
||||
"agentSeeNextSuggestion": "다음 제안 보기",
|
||||
"agentDiscovery": "에이전트",
|
||||
"agentFailed": "생성 실패",
|
||||
"agentsEmpty": "아직 제안된 연구 에이전트가 없습니다. 계속 작성하세요 — 노트가 클러스터되면 Memento가 종합 주제를 제안합니다.",
|
||||
@@ -4063,6 +4084,8 @@
|
||||
"aiFound": "AI가 찾았습니다",
|
||||
"aiProviderUnavailable": "AI를 일시적으로 사용할 수 없습니다. 제공자 설정을 확인하세요.",
|
||||
"allCaughtUp": "모두 완료.",
|
||||
"remindersEmpty": "오늘 알림이 없습니다.",
|
||||
"remindersOpenAll": "알림 보기",
|
||||
"alreadySeen": "봄",
|
||||
"analyzeNotes": "내 노트 분석",
|
||||
"analyzing": "분석 중…",
|
||||
@@ -4180,6 +4203,8 @@
|
||||
"pulseReview": "검토할 {count}",
|
||||
"quickCapture": "빠른 캡처",
|
||||
"quickCapturePlaceholder": "아이디어, 생각… Enter를 눌러 받은편지함에 저장하세요.",
|
||||
"captureGoesToFile": "받은편지함으로 갑니다.",
|
||||
"captureSend": "받은편지함으로 보내기",
|
||||
"reminders": "알림",
|
||||
"resumeAlso": "최근 항목",
|
||||
"resumeEmptyCta": "아이디어 캡처",
|
||||
@@ -4267,6 +4292,7 @@
|
||||
"widgetHide": "위젯 숨기기",
|
||||
"widgetOpen": "열기",
|
||||
"widgetPinnedEmpty": "고정된 노트가 아직 없습니다.",
|
||||
"pinnedNoNotebook": "노트북 없음",
|
||||
"widgetReset": "재설정",
|
||||
"widgetResetDone": "기본 대시보드가 복원되었습니다.",
|
||||
"widgetStatsBridges": "브리지",
|
||||
|
||||
@@ -31,14 +31,14 @@
|
||||
"confirmPasswordPlaceholder": "Bevestig uw wachtwoord",
|
||||
"backToSite": "Terug",
|
||||
"continueWithGoogle": "Doorgaan met Google",
|
||||
"createYourSpace": "Maak je ruimte",
|
||||
"createYourSpaceSubtitle": "Word deel van het nieuwe tijdperk van slim notities maken.",
|
||||
"createYourSpace": "Maak je tweede brein",
|
||||
"createYourSpaceSubtitle": "Het verbindt terwijl je schrijft — geen zoveelste notitie-app.",
|
||||
"forgot": "Vergeten?",
|
||||
"oauthAccountNotLinked": "Dit Google-account komt niet overeen met uw bestaande account. Gebruik hetzelfde e-mailadres of log in met uw wachtwoord.",
|
||||
"privacyTerms": "© 2025 Memento Labs — Privacy · Voorwaarden",
|
||||
"sessionExpired": "Uw site wordt gegenereerd met navigatie en inhoudsopgave",
|
||||
"welcomeBack": "Welkom terug",
|
||||
"welcomeBackSubtitle": "Voer uw inloggegevens in om toegang te krijgen tot uw notities.",
|
||||
"welcomeBackSubtitle": "Meld je aan om je notities, verbanden en wat je was vergeten terug te vinden.",
|
||||
"checkEmailTitle": "Controleer je e-mail",
|
||||
"checkEmailDescription": "We hebben een bevestigingslink naar {email} gestuurd. Open die om je account te activeren voordat je inlogt.",
|
||||
"checkEmailDescriptionGeneric": "We hebben een bevestigingslink naar je e-mail gestuurd. Open die om je account te activeren voordat je inlogt.",
|
||||
@@ -99,13 +99,13 @@
|
||||
"darkMode": "Donkere modus",
|
||||
"dashboardPanelBody": "Uw sessie is verlopen. Meld u opnieuw aan.",
|
||||
"documents": "Documenten",
|
||||
"insightsPanelBody": "Semantische kaart van uw notities: thematische clusters, brugnotities en verbindingsuggesties.",
|
||||
"insightsPanelBody": "Een kaart van hoe je notities samenhangen: verwante thema's, brugnotities en links om te openen.",
|
||||
"lightMode": "Lichte modus",
|
||||
"notebookEmpty": "Leeg",
|
||||
"recentNote": "Recent aangemaakt",
|
||||
"resizeNotebooksPanel": "Notitieboekpaneel vergroten/verkleinen",
|
||||
"resizeSidebar": "Zijbalkbreedte aanpassen",
|
||||
"revisionPanelBody": "Herhaal flashcards met het SM-2-algoritme. Decks worden gegenereerd uit uw notities.",
|
||||
"revisionPanelBody": "Herhaal met flashcards. Gespreide herhaling brengt ze terug op het juiste moment. Decks komen uit je notities.",
|
||||
"searchNotebooksPlaceholder": "Notitieboeken zoeken…",
|
||||
"searchShortcut": "Zoeken (Ctrl+K)"
|
||||
},
|
||||
@@ -130,7 +130,7 @@
|
||||
"add": "Toevoegen",
|
||||
"adding": "Toevoegen...",
|
||||
"close": "Sluiten",
|
||||
"confirmDelete": "Weet je zeker dat je deze notitie wilt verwijderen?",
|
||||
"confirmDelete": "Deze notitie gaat naar de prullenbak. U kunt ze later terugzetten.",
|
||||
"confirmLeaveShare": "Weet u zeker dat u deze gedeelde notitie wilt verlaten?",
|
||||
"sharedBy": "Gedeeld door",
|
||||
"sharedShort": "Gedeeld",
|
||||
@@ -991,7 +991,7 @@
|
||||
"dailyNotes": "Dagelijkse notities",
|
||||
"dashboard": "Dashboard",
|
||||
"graphView": "Linkkaart",
|
||||
"insights": "Semantische thema's",
|
||||
"insights": "Verbanden",
|
||||
"revision": "Herhalen"
|
||||
},
|
||||
"settings": {
|
||||
@@ -2095,14 +2095,14 @@
|
||||
"collapse": "Inklappen"
|
||||
},
|
||||
"mcpSettings": {
|
||||
"title": "MCP",
|
||||
"title": "Externe hulpmiddelen",
|
||||
"description": "Beheer uw API-sleutels en configureer externe tools",
|
||||
"tierRequired": "Alleen Pro+",
|
||||
"upgradeHint": "MCP-toegang (API-sleutels voor Cursor, Claude Desktop, etc.) vereist een Pro-plan of hoger. Upgrade in Facturatie om deze functie te ontgrendelen.",
|
||||
"whatIsMcp": {
|
||||
"title": "Wat is MCP?",
|
||||
"title": "Waar is dit voor?",
|
||||
"description": "Het Model Context Protocol (MCP) is een open protocol waarmee AI-modellen veilig kunnen communiceren met externe tools en gegevensbronnen. Met MCP kunt u tools zoals Claude Code, Cursor of N8N koppelen aan uw Memento-instantie om uw notities programmatisch te lezen, maken en organiseren.",
|
||||
"learnMore": "Meer informatie over MCP"
|
||||
"learnMore": "Meer informatie"
|
||||
},
|
||||
"serverStatus": {
|
||||
"title": "Serverstatus",
|
||||
@@ -2161,12 +2161,12 @@
|
||||
}
|
||||
},
|
||||
"helpBox": {
|
||||
"title": "Wat is MCP (Model Context Protocol)?",
|
||||
"title": "Hoe verbind ik een hulpmiddel?",
|
||||
"step1": "MCP is een protocol waarmee de AI-agents van Memento verbinding kunnen maken met externe tools (databases, API's, bestanden, etc.).",
|
||||
"step2": "Memento biedt een MCP-server met 22 tools — uw agents kunnen notities lezen/maken, zoeken in uw database, notitieboeken beheren, etc.",
|
||||
"step3": "Maak hier een API-sleutel aan en configureer deze vervolgens in uw MCP-client (Claude Desktop, Cursor, Continue.dev…) met de server-URL.",
|
||||
"step4": "Configuratieformaat: MCP-server-URL + uw sleutel in de Authorization-header.",
|
||||
"step4Link": "MCP-documentatie",
|
||||
"step4Link": "Officiële hulp",
|
||||
"step5": "Toepassing: vraag Claude Desktop om een notitie in Memento te schrijven, uw notitieboeken te doorzoeken of een agent te maken."
|
||||
}
|
||||
},
|
||||
@@ -2334,6 +2334,17 @@
|
||||
"title": "Sjablonen",
|
||||
"install": "Installeren",
|
||||
"installing": "Installeren...",
|
||||
"seeAll": "Alles bekijken",
|
||||
"showLess": "Minder tonen",
|
||||
"categoryAll": "Alles",
|
||||
"categoryWatch": "Nieuwsvolging",
|
||||
"categoryDigest": "Samenvattingen",
|
||||
"categoryTools": "Hulpmiddelen",
|
||||
"categoryGenerate": "Maken",
|
||||
"taskExtractor": {
|
||||
"name": "Taken uit notities",
|
||||
"description": "Vindt taken in uw notities en verzamelt ze op één plek."
|
||||
},
|
||||
"veilleAI": {
|
||||
"name": "AI Watch",
|
||||
"description": "Schraapt 5 op AI gespecialiseerde sites en genereert een wekelijkse samenvatting."
|
||||
@@ -2453,7 +2464,7 @@
|
||||
"slideStyle": "De visuele stijl heeft invloed op de hoekradius, de afstand en de informatiedichtheid."
|
||||
}
|
||||
},
|
||||
"intelligenceOS": "Intelligent besturingssysteem"
|
||||
"intelligenceOS": "Agenten"
|
||||
},
|
||||
"chat": {
|
||||
"title": "AI-chat",
|
||||
@@ -2740,7 +2751,7 @@
|
||||
"slashDatabaseDesc": "Sluit de gestructureerde gegevens van uw notitieboek in",
|
||||
"slashLinkPreview": "Linkvoorbeeld",
|
||||
"slashLinkPreviewDesc": "Een URL omzetten in een visuele kaart",
|
||||
"slashLivingBlock": "Live blok",
|
||||
"slashLivingBlock": "Gekoppeld blok",
|
||||
"slashLivingBlockDesc": "Invoegen vanuit andere notitie",
|
||||
"slashMath": "Vergelijking",
|
||||
"slashMathDesc": "Wiskundige formule in LaTeX-notatie",
|
||||
@@ -3007,7 +3018,7 @@
|
||||
"proChat": "100 chat messages / month",
|
||||
"later": "Later",
|
||||
"upgradePricing": "Upgrade naar Pro",
|
||||
"addApiKey": "Gebruik uw eigen API-sleutel (BYOK)",
|
||||
"addApiKey": "Gebruik uw eigen sleutel",
|
||||
"featureBrainstormCreate": "Créations brainstorm",
|
||||
"featureBrainstormEnrich": "Enrichissements brainstorm",
|
||||
"featureBrainstormExpand": "Extensions brainstorm",
|
||||
@@ -3025,10 +3036,10 @@
|
||||
"outOfCredits": "Geen credits meer — opties"
|
||||
},
|
||||
"byokSettings": {
|
||||
"title": "Uw API-sleutels (BYOK)",
|
||||
"title": "Uw aanbiedersleutels",
|
||||
"description": "Connect your own LLM provider keys to bypass Discovery Pack quotas. Keys are encrypted at rest.",
|
||||
"badgeActive": "BYOK actief",
|
||||
"tierRequired": "BYOK vereist een Pro-abonnement of hoger. Upgrade om uw API-sleutels te koppelen.",
|
||||
"badgeActive": "Sleutels actief",
|
||||
"tierRequired": "Deze optie vereist een Pro-abonnement of hoger.",
|
||||
"provider": "Provider",
|
||||
"providerPlaceholder": "Selecteer een provider",
|
||||
"alias": "Label (optioneel)",
|
||||
@@ -3167,6 +3178,9 @@
|
||||
"fetchInvoicesFailed": "Factuurgeschiedenis kon niet worden geladen.",
|
||||
"savePercent": "Bespaar ~17%",
|
||||
"cancelSubscription": "Abonnement opzeggen",
|
||||
"changeOffer": "Ander aanbod kiezen",
|
||||
"downgradeToFree": "Terug naar het gratis aanbod",
|
||||
"cancellingNotice": "Opzegging gepland — toegang tot {date}",
|
||||
"disabledByAdmin": "Facturatie en upgrades zijn momenteel uitgeschakeld. Neem contact op met uw beheerder als u toegang nodig heeft.",
|
||||
"tab": "Facturering",
|
||||
"creditsFromPacks": "Pakket-credits",
|
||||
@@ -3232,7 +3246,7 @@
|
||||
"title": "Het moment waarop je Second Brain antwoordt.",
|
||||
"desc": "Terwijl je schrijft detecteert Memento semantische links tussen notitieboeken — geen keywords, echte conceptbruggen.",
|
||||
"card0Label": "Zojuist gedetecteerd",
|
||||
"card0": "“Je pricing-notitie spiegelt de concurrentieanalyse van vorig najaar.”",
|
||||
"card0": "“Je notitie over prijzen spiegelt de concurrentieanalyse van vorig najaar.”",
|
||||
"card1Label": "Brug",
|
||||
"card1": "Gedeeld thema: positionering onder druk",
|
||||
"card2Label": "Actie",
|
||||
@@ -3245,7 +3259,7 @@
|
||||
"w0Label": "Volgende paden",
|
||||
"w0": "3 acties uit je laatste notitie",
|
||||
"w1Label": "Dagelijkse review",
|
||||
"w1": "Inbox · AI · Flashcards",
|
||||
"w1": "Inbox · Ontdekkingen · Kaarten",
|
||||
"w2Label": "Memory Echo",
|
||||
"w2": "2 nieuwe verbindingen vannacht",
|
||||
"w3Label": "Agents",
|
||||
@@ -3268,7 +3282,7 @@
|
||||
"title": "Drie stappen. Daarna groeit je Second Brain.",
|
||||
"s0": {
|
||||
"title": "Vrij vastleggen",
|
||||
"desc": "Schrijf in een next-gen editor — blokken, gestructureerde views, smart paste."
|
||||
"desc": "Schrijf zoals in een schrift: titels, lijsten, tabellen. Sla een webpagina op als dat nodig is."
|
||||
},
|
||||
"s1": {
|
||||
"title": "Laat verbinden",
|
||||
@@ -3284,34 +3298,37 @@
|
||||
"title": "Delegeer het zware werk.",
|
||||
"desc": "Research, scrape, slides, diagrammen, monitoring — agents die in je Second Brain schrijven.",
|
||||
"scraper": {
|
||||
"title": "Scraper",
|
||||
"title": "Monitor",
|
||||
"desc": "URL’s & RSS → gesynthetiseerde notities met beelden."
|
||||
},
|
||||
"researcher": {
|
||||
"title": "Researcher",
|
||||
"title": "Onderzoeker",
|
||||
"desc": "Diepe queries, bronnen, gestructureerde researchnotities."
|
||||
},
|
||||
"slideGen": {
|
||||
"title": "Slide Gen",
|
||||
"title": "Dia's",
|
||||
"desc": "Notities → decks of interactieve HTML-slides."
|
||||
},
|
||||
"monitor": {
|
||||
"title": "Monitor",
|
||||
"title": "Waarnemer",
|
||||
"desc": "Bewaak notitieboeken: trends en insights."
|
||||
},
|
||||
"diagramGen": {
|
||||
"title": "Diagram Gen",
|
||||
"title": "Diagram",
|
||||
"desc": "Ideeën → Excalidraw mindmaps & flows."
|
||||
},
|
||||
"custom": {
|
||||
"title": "Custom",
|
||||
"title": "Aangepast",
|
||||
"desc": "Jouw rollen, bronnen en planning."
|
||||
}
|
||||
},
|
||||
"byok": {
|
||||
"label": "Geen lock-in",
|
||||
"label": "Vrij te wisselen",
|
||||
"title": "Jouw sleutels. Jouw modellen. Jouw Second Brain.",
|
||||
"desc": "Memento-credits of OpenAI, Anthropic, Google… Wissel van provider in één klik."
|
||||
"desc": "Memento-credits, of je eigen aanbieder. Wissel in één klik — het product blijft van jou.",
|
||||
"pointCredits": "Memento-credits, als je het eenvoudig wilt houden",
|
||||
"pointProvider": "Of verbind je eigen aanbieder",
|
||||
"pointYours": "Je sleutel blijft bij jou — hij wordt hier nooit getoond"
|
||||
},
|
||||
"pricing": {
|
||||
"label": "Pricing",
|
||||
@@ -3339,7 +3356,7 @@
|
||||
"desc": "Voor veeleisende denkers.",
|
||||
"cta": "Ga Pro",
|
||||
"feature0": "Onbeperkte notities",
|
||||
"feature1": "BYOK",
|
||||
"feature1": "Je eigen sleutels",
|
||||
"feature2": "200 semantische zoekopdrachten / maand",
|
||||
"feature3": "Agents (12 runs/maand)",
|
||||
"feature4": "30 dagen geschiedenis",
|
||||
@@ -3350,11 +3367,11 @@
|
||||
"desc": "Team-Second Brain.",
|
||||
"cta": "Kies Business",
|
||||
"feature0": "10 medewerkers",
|
||||
"feature1": "BYOK · 13 providers",
|
||||
"feature1": "Je sleutels · {count} aanbieders",
|
||||
"feature2": "1.000 semantische zoekopdrachten",
|
||||
"feature3": "Agents (60 runs/maand)",
|
||||
"feature4": "Onbeperkt brainstorm",
|
||||
"feature5": "API / MCP"
|
||||
"feature5": "Externe hulpmiddelen"
|
||||
},
|
||||
"enterprise": {
|
||||
"name": "Enterprise",
|
||||
@@ -3545,12 +3562,12 @@
|
||||
"feature_search_title": "Semantisch zoeken",
|
||||
"feature_search_desc": "Vind elke notitie op betekenis, niet alleen op trefwoorden.",
|
||||
"feature_flashcards_title": "AI-flashcards",
|
||||
"feature_flashcards_desc": "Genereer SRS-revisiekaarten uit uw notities met één klik.",
|
||||
"feature_flashcards_desc": "Genereer revisiekaarten uit uw notities met één klik.",
|
||||
"feature_brainstorm_title": "AI-brainstormen",
|
||||
"feature_brainstorm_desc": "AI-gestuurde collaboratieve brainstormsessies.",
|
||||
"feature_chat_title": "Chat met uw notities",
|
||||
"feature_chat_desc": "Stel vragen aan uw persoonlijke kennisbank.",
|
||||
"feature_insights_title": "Semantische inzichten",
|
||||
"feature_insights_title": "Verbanden",
|
||||
"feature_insights_desc": "Ontdek verborgen verbanden tussen uw ideeën.",
|
||||
"feature_export_title": "Markdown-export",
|
||||
"feature_export_desc": "Importeer en exporteer uw notities in Markdown-formaat.",
|
||||
@@ -3638,9 +3655,10 @@
|
||||
"createDiagramCostHint": "≈ 4 AI-credits"
|
||||
},
|
||||
"insightsView": {
|
||||
"title": "Semantische inzichten",
|
||||
"title": "Verbanden",
|
||||
"toggleMenu": "Menu tonen of verbergen",
|
||||
"subtitle": "Ontdek de verborgen architectuur van je kennis",
|
||||
"resync": "Netwerk hersynchroniseren",
|
||||
"resync": "Bijwerken",
|
||||
"mapping": "In kaart brengen…",
|
||||
"loading": "Notizen laden…",
|
||||
"mappingTitle": "Je kennis in kaart brengen…",
|
||||
@@ -3800,7 +3818,7 @@
|
||||
"apiKey": "API-sleutel",
|
||||
"apiUrl": "API-URL",
|
||||
"baseUrlRequired": "Geef de API-URL op",
|
||||
"byokActive": "BYOK actief",
|
||||
"byokActive": "Sleutels actief",
|
||||
"choose": "Kies…",
|
||||
"chooseModel": "Kies een model…",
|
||||
"chooseProvider": "Kies een provider…",
|
||||
@@ -4011,7 +4029,7 @@
|
||||
"tabProgress": "Voortgang",
|
||||
"tapToFlip": "Spatie of tik om om te draaien",
|
||||
"toolbarGenerate": "Flashcards genereren",
|
||||
"toolbarGenerateHint": "SM-2 gespreide herhaling",
|
||||
"toolbarGenerateHint": "Ze komen op het juiste moment terug",
|
||||
"totalCardsLabel": "Totaal kaarten",
|
||||
"totalReviewsLabel": "Totaal herhalingen",
|
||||
"upToDate": "Bijgewerkt",
|
||||
@@ -4055,6 +4073,9 @@
|
||||
"homeDashboard": {
|
||||
"activityEmptyHint": "Bewerk notities om je schrijfritme van de afgelopen 90 dagen te zien.",
|
||||
"agentCreated": "Agent aangemaakt en gestart",
|
||||
"agentCreatedInCard": "De agent is klaar.",
|
||||
"agentOpenCreated": "Agent openen",
|
||||
"agentSeeNextSuggestion": "Volgende bekijken",
|
||||
"agentDiscovery": "Agent",
|
||||
"agentFailed": "Aanmaken mislukt",
|
||||
"agentsEmpty": "Nog geen onderzoeksagenten voorgesteld. Blijf schrijven — Memento stelt synthesisonderwerpen voor wanneer uw notities clusteren.",
|
||||
@@ -4063,6 +4084,8 @@
|
||||
"aiFound": "AI gevonden",
|
||||
"aiProviderUnavailable": "AI is tijdelijk niet beschikbaar. Controleer je provid-instellingen.",
|
||||
"allCaughtUp": "Alles bijgewerkt.",
|
||||
"remindersEmpty": "Geen herinneringen voor vandaag.",
|
||||
"remindersOpenAll": "Herinneringen bekijken",
|
||||
"alreadySeen": "bekeken",
|
||||
"analyzeNotes": "Mijn notities analyseren",
|
||||
"analyzing": "Analyseren…",
|
||||
@@ -4180,6 +4203,8 @@
|
||||
"pulseReview": "{count} te herzien",
|
||||
"quickCapture": "Snelle vastlegging",
|
||||
"quickCapturePlaceholder": "Een idee, een gedachte… Druk op Enter om in inbox te bewaren.",
|
||||
"captureGoesToFile": "Gaat naar de inbox.",
|
||||
"captureSend": "Naar de inbox sturen",
|
||||
"reminders": "Herinneringen",
|
||||
"resumeAlso": "Ook recent",
|
||||
"resumeEmptyCta": "Een idee vastleggen",
|
||||
@@ -4267,6 +4292,7 @@
|
||||
"widgetHide": "Widget verbergen",
|
||||
"widgetOpen": "Openen",
|
||||
"widgetPinnedEmpty": "Nog geen vastgemaakte notities.",
|
||||
"pinnedNoNotebook": "Geen notitieboek",
|
||||
"widgetReset": "Resetten",
|
||||
"widgetResetDone": "Standaard dashboard hersteld.",
|
||||
"widgetStatsBridges": "Bruggen",
|
||||
|
||||
@@ -31,14 +31,14 @@
|
||||
"confirmPasswordPlaceholder": "Potwierdź swoje hasło",
|
||||
"backToSite": "Wstecz",
|
||||
"continueWithGoogle": "Kontynuuj z Google",
|
||||
"createYourSpace": "Utwórz swoją przestrzeń",
|
||||
"createYourSpaceSubtitle": "Dołącz do nowej ery inteligentnego robienia notatek.",
|
||||
"createYourSpace": "Stwórz swój drugi mózg",
|
||||
"createYourSpaceSubtitle": "Łączy się, gdy piszesz — to nie kolejna aplikacja do notatek.",
|
||||
"forgot": "Zapomniałeś?",
|
||||
"oauthAccountNotLinked": "To konto Google nie pasuje do Twojego istniejącego konta. Użyj tego samego emaila lub zaloguj się hasłem.",
|
||||
"privacyTerms": "© 2025 Memento Labs — Prywatność · Warunki",
|
||||
"sessionExpired": "Twoja strona jest generowana z nawigacją i spisem treści",
|
||||
"welcomeBack": "Witamy ponownie",
|
||||
"welcomeBackSubtitle": "Wprowadź swoje dane logowania, aby uzyskać dostęp do notatek.",
|
||||
"welcomeBackSubtitle": "Zaloguj się, aby odnaleźć notatki, połączenia i to, o czym zapomniałeś.",
|
||||
"checkEmailTitle": "Sprawdź e-mail",
|
||||
"checkEmailDescription": "Wysłaliśmy link potwierdzający na {email}. Otwórz go, aby aktywować konto przed logowaniem.",
|
||||
"checkEmailDescriptionGeneric": "Wysłaliśmy link potwierdzający na Twój e-mail. Otwórz go, aby aktywować konto przed logowaniem.",
|
||||
@@ -99,13 +99,13 @@
|
||||
"darkMode": "Tryb ciemny",
|
||||
"dashboardPanelBody": "Twoja sesja wygasła. Zaloguj się ponownie.",
|
||||
"documents": "Dokumenty",
|
||||
"insightsPanelBody": "Mapa semantyczna Twoich notatek: klastry tematyczne, notatki pomostowe i sugestie powiązań.",
|
||||
"insightsPanelBody": "Mapa powiązań Twoich notatek: bliskie tematy, notatki pomostowe i linki do otwarcia.",
|
||||
"lightMode": "Jasny motyw",
|
||||
"notebookEmpty": "Pusty",
|
||||
"recentNote": "Ostatnio utworzone",
|
||||
"resizeNotebooksPanel": "Zmień rozmiar panelu notatników",
|
||||
"resizeSidebar": "Zmień szerokość paska bocznego",
|
||||
"revisionPanelBody": "Powtarzaj fiszki algorytmem SM-2. Talie są generowane z Twoich notatek.",
|
||||
"revisionPanelBody": "Powtarzaj fiszki. Powtórki w odstępach wracają we właściwym momencie. Talie powstają z Twoich notatek.",
|
||||
"searchNotebooksPlaceholder": "Szukaj notatników…",
|
||||
"searchShortcut": "Szukaj (Ctrl+K)"
|
||||
},
|
||||
@@ -130,7 +130,7 @@
|
||||
"add": "Dodaj",
|
||||
"adding": "Dodawanie...",
|
||||
"close": "Zamknij",
|
||||
"confirmDelete": "Czy na pewno chcesz usunąć tę notatkę?",
|
||||
"confirmDelete": "Ta notatka trafi do kosza. Możesz ją później przywrócić.",
|
||||
"confirmLeaveShare": "Czy na pewno chcesz opuścić tę udostępnioną notatkę?",
|
||||
"sharedBy": "Udostępnione przez",
|
||||
"sharedShort": "Wspólny",
|
||||
@@ -991,7 +991,7 @@
|
||||
"dailyNotes": "Codzienne notatki",
|
||||
"dashboard": "Panel główny",
|
||||
"graphView": "Mapa linków",
|
||||
"insights": "Motywy semantyczne",
|
||||
"insights": "Powiązania",
|
||||
"revision": "Powtarzaj"
|
||||
},
|
||||
"settings": {
|
||||
@@ -2095,14 +2095,14 @@
|
||||
"collapse": "Zwiń"
|
||||
},
|
||||
"mcpSettings": {
|
||||
"title": "MCP",
|
||||
"title": "Narzędzia zewnętrzne",
|
||||
"description": "Zarządzaj kluczami API i konfiguruj narzędzia zewnętrzne",
|
||||
"tierRequired": "Tylko Pro+",
|
||||
"upgradeHint": "Dostęp MCP (klucze API dla Cursor, Claude Desktop itp.) wymaga planu Pro lub wyższego. Wykonaj upgrade w Rozliczeniach, aby odblokować tę funkcję.",
|
||||
"whatIsMcp": {
|
||||
"title": "Czym jest MCP?",
|
||||
"title": "Do czego to służy?",
|
||||
"description": "Model Context Protocol (MCP) to otwarty protokół umożliwiający modelom AI bezpieczną interakcję z zewnętrznymi narzędziami i źródłami danych. Dzięki MCP możesz połączyć narzędzia takie jak Claude Code, Cursor czy N8N ze swoją instancją Memento, aby programowo czytać, tworzyć i organizować notatki.",
|
||||
"learnMore": "Dowiedz się więcej o MCP"
|
||||
"learnMore": "Dowiedz się więcej"
|
||||
},
|
||||
"serverStatus": {
|
||||
"title": "Status serwera",
|
||||
@@ -2161,12 +2161,12 @@
|
||||
}
|
||||
},
|
||||
"helpBox": {
|
||||
"title": "Czym jest MCP (Model Context Protocol)?",
|
||||
"title": "Jak podłączyć narzędzie?",
|
||||
"step1": "MCP to protokół, który pozwala agentom AI Memento łączyć się z zewnętrznymi narzędziami (bazami danych, API, plikami itp.).",
|
||||
"step2": "Memento udostępnia serwer MCP z 22 narzędziami — Twoi agenci mogą odczytywać/tworzyć notatki, przeszukiwać bazę, zarządzać notatnikami itp.",
|
||||
"step3": "Utwórz klucz API tutaj, a następnie skonfiguruj go w swoim kliencie MCP (Claude Desktop, Cursor, Continue.dev…) z adresem URL serwera.",
|
||||
"step4": "Format konfiguracji: adres URL serwera MCP + Twój klucz w nagłówku Authorization.",
|
||||
"step4Link": "Dokumentacja MCP",
|
||||
"step4Link": "Oficjalna pomoc",
|
||||
"step5": "Przypadek użycia: poproś Claude Desktop o napisanie notatki w Memento, przeszukanie notatników lub utworzenie agenta."
|
||||
}
|
||||
},
|
||||
@@ -2334,6 +2334,17 @@
|
||||
"title": "Szablony",
|
||||
"install": "Zainstaluj",
|
||||
"installing": "Instalowanie...",
|
||||
"seeAll": "Zobacz wszystkie",
|
||||
"showLess": "Pokaż mniej",
|
||||
"categoryAll": "Wszystkie",
|
||||
"categoryWatch": "Przegląd",
|
||||
"categoryDigest": "Podsumowania",
|
||||
"categoryTools": "Narzędzia",
|
||||
"categoryGenerate": "Tworzenie",
|
||||
"taskExtractor": {
|
||||
"name": "Zadania z notatek",
|
||||
"description": "Znajduje zadania w notatkach i zbiera je w jednym miejscu."
|
||||
},
|
||||
"veilleAI": {
|
||||
"name": "Przegląd AI",
|
||||
"description": "Pobiera dane z 5 stron specjalizujących się w AI i generuje tygodniowe podsumowanie."
|
||||
@@ -2453,7 +2464,7 @@
|
||||
"slideStyle": "Styl wizualny wpływa na promień narożnika, odstępy i gęstość informacji."
|
||||
}
|
||||
},
|
||||
"intelligenceOS": "System operacyjny inteligencji"
|
||||
"intelligenceOS": "Agenci"
|
||||
},
|
||||
"chat": {
|
||||
"title": "Czat AI",
|
||||
@@ -2740,7 +2751,7 @@
|
||||
"slashDatabaseDesc": "Osadź ustrukturyzowane dane swojego notatnika",
|
||||
"slashLinkPreview": "Podgląd linku",
|
||||
"slashLinkPreviewDesc": "Zamień URL w wizualną kartę",
|
||||
"slashLivingBlock": "Blok na żywo",
|
||||
"slashLivingBlock": "Połączony blok",
|
||||
"slashLivingBlockDesc": "Wstaw z innej notatki",
|
||||
"slashMath": "Równanie",
|
||||
"slashMathDesc": "Wzór matematyczny w notacji LaTeX",
|
||||
@@ -3007,7 +3018,7 @@
|
||||
"proChat": "100 chat messages / month",
|
||||
"later": "Później",
|
||||
"upgradePricing": "Ulepsz do Pro",
|
||||
"addApiKey": "Użyj własnego klucza API (BYOK)",
|
||||
"addApiKey": "Użyj własnego klucza",
|
||||
"featureBrainstormCreate": "Créations brainstorm",
|
||||
"featureBrainstormEnrich": "Enrichissements brainstorm",
|
||||
"featureBrainstormExpand": "Extensions brainstorm",
|
||||
@@ -3025,10 +3036,10 @@
|
||||
"outOfCredits": "Brak kredytów — opcje"
|
||||
},
|
||||
"byokSettings": {
|
||||
"title": "Twoje klucze API (BYOK)",
|
||||
"title": "Twoje klucze dostawcy",
|
||||
"description": "Connect your own LLM provider keys to bypass Discovery Pack quotas. Keys are encrypted at rest.",
|
||||
"badgeActive": "BYOK aktywny",
|
||||
"tierRequired": "BYOK wymaga planu Pro lub wyższego. Ulepsz, aby podłączyć swoje klucze API.",
|
||||
"badgeActive": "Klucze aktywne",
|
||||
"tierRequired": "Ta opcja wymaga planu Pro lub wyższego.",
|
||||
"provider": "Dostawca",
|
||||
"providerPlaceholder": "Wybierz dostawcę",
|
||||
"alias": "Etykieta (opcjonalnie)",
|
||||
@@ -3167,6 +3178,9 @@
|
||||
"fetchInvoicesFailed": "Nie udało się załadować historii rozliczeń.",
|
||||
"savePercent": "Oszczędź ~17%",
|
||||
"cancelSubscription": "Anuluj subskrypcję",
|
||||
"changeOffer": "Zmień ofertę",
|
||||
"downgradeToFree": "Wróć do oferty darmowej",
|
||||
"cancellingNotice": "Rezygnacja zaplanowana — dostęp do {date}",
|
||||
"disabledByAdmin": "Rozliczenia i ulepszenia są obecnie wyłączone. Skontaktuj się z administratorem, jeśli potrzebujesz dostępu.",
|
||||
"tab": "Płatności",
|
||||
"creditsFromPacks": "Kredyty z pakietów",
|
||||
@@ -3239,13 +3253,13 @@
|
||||
"card2": "Otwórz obie notatki obok siebie — pisz dalej"
|
||||
},
|
||||
"dashboard": {
|
||||
"eyebrow": "Dashboard Second Brain",
|
||||
"eyebrow": "Poranek",
|
||||
"title": "Poranek, który mówi, co ważne.",
|
||||
"desc": "Briefing Twojego Second Brain: kolejne ścieżki, checklista, odkrycia, powtórki.",
|
||||
"w0Label": "Kolejne ścieżki",
|
||||
"w0": "3 działania z ostatniej notatki",
|
||||
"w1Label": "Przegląd dnia",
|
||||
"w1": "Inbox · AI · Fiszki",
|
||||
"w1": "Skrzynka · Odkrycia · Karty",
|
||||
"w2Label": "Memory Echo",
|
||||
"w2": "2 nowe połączenia tej nocy",
|
||||
"w3Label": "Agenci",
|
||||
@@ -3284,34 +3298,37 @@
|
||||
"title": "Oddaj ciężką pracę.",
|
||||
"desc": "Research, scrape, slajdy, diagramy, monitoring — agenci, którzy piszą do Twojego Second Brain.",
|
||||
"scraper": {
|
||||
"title": "Scraper",
|
||||
"title": "Monitor",
|
||||
"desc": "URL-e i RSS → zsyntetyzowane notatki z obrazami."
|
||||
},
|
||||
"researcher": {
|
||||
"title": "Researcher",
|
||||
"title": "Badacz",
|
||||
"desc": "Głębokie zapytania, źródła, ustrukturyzowane notatki badawcze."
|
||||
},
|
||||
"slideGen": {
|
||||
"title": "Slide Gen",
|
||||
"title": "Slajdy",
|
||||
"desc": "Notatki → decki lub interaktywne slajdy HTML."
|
||||
},
|
||||
"monitor": {
|
||||
"title": "Monitor",
|
||||
"title": "Obserwator",
|
||||
"desc": "Monitoruj notesy: trendy i insighty."
|
||||
},
|
||||
"diagramGen": {
|
||||
"title": "Diagram Gen",
|
||||
"title": "Diagram",
|
||||
"desc": "Pomysły → mindmapy i flow Excalidraw."
|
||||
},
|
||||
"custom": {
|
||||
"title": "Custom",
|
||||
"title": "Niestandardowy",
|
||||
"desc": "Twoje role, źródła i harmonogramy."
|
||||
}
|
||||
},
|
||||
"byok": {
|
||||
"label": "Bez lock-in",
|
||||
"label": "Swobodna zmiana",
|
||||
"title": "Twoje klucze. Twoje modele. Twój Second Brain.",
|
||||
"desc": "Kredyty Memento albo OpenAI, Anthropic, Google… Zmień dostawcę jednym kliknięciem."
|
||||
"desc": "Kredyty Memento albo własny dostawca. Zmień jednym kliknięciem — produkt zostaje Twój.",
|
||||
"pointCredits": "Kredyty Memento, jeśli chcesz zostać przy prostocie",
|
||||
"pointProvider": "Albo połącz własnego dostawcę",
|
||||
"pointYours": "Klucz zostaje u Ciebie — tutaj nigdy go nie pokazujemy"
|
||||
},
|
||||
"pricing": {
|
||||
"label": "Pricing",
|
||||
@@ -3339,7 +3356,7 @@
|
||||
"desc": "Dla wymagających umysłów.",
|
||||
"cta": "Wybierz Pro",
|
||||
"feature0": "Nielimitowane notatki",
|
||||
"feature1": "BYOK",
|
||||
"feature1": "Własne klucze",
|
||||
"feature2": "200 wyszukiwań sem. / mies.",
|
||||
"feature3": "Agenci (12 runów/mies.)",
|
||||
"feature4": "Historia 30 dni",
|
||||
@@ -3350,11 +3367,11 @@
|
||||
"desc": "Second Brain zespołu.",
|
||||
"cta": "Wybierz Business",
|
||||
"feature0": "10 współpracowników",
|
||||
"feature1": "BYOK · 13 dostawców",
|
||||
"feature1": "Własne klucze · {count} dostawców",
|
||||
"feature2": "1000 wyszukiwań sem.",
|
||||
"feature3": "Agenci (60 runów/mies.)",
|
||||
"feature4": "Nielimitowany brainstorm",
|
||||
"feature5": "API / MCP"
|
||||
"feature5": "Narzędzia zewnętrzne"
|
||||
},
|
||||
"enterprise": {
|
||||
"name": "Enterprise",
|
||||
@@ -3545,12 +3562,12 @@
|
||||
"feature_search_title": "Wyszukiwanie semantyczne",
|
||||
"feature_search_desc": "Znajdź każdą notatkę według znaczenia, nie tylko słów kluczowych.",
|
||||
"feature_flashcards_title": "Fiszki AI",
|
||||
"feature_flashcards_desc": "Generuj karty powtórek SRS z notatek jednym kliknięciem.",
|
||||
"feature_flashcards_desc": "Generuj karty powtórek z notatek jednym kliknięciem.",
|
||||
"feature_brainstorm_title": "Burza mózgów AI",
|
||||
"feature_brainstorm_desc": "Sesje wspólnej burzy mózgów wspomaganej przez AI.",
|
||||
"feature_chat_title": "Czatuj z notatkami",
|
||||
"feature_chat_desc": "Zadawaj pytania swojej osobistej bazie wiedzy.",
|
||||
"feature_insights_title": "Spostrzeżenia semantyczne",
|
||||
"feature_insights_title": "Powiązania",
|
||||
"feature_insights_desc": "Odkryj ukryte powiązania między swoimi pomysłami.",
|
||||
"feature_export_title": "Eksport Markdown",
|
||||
"feature_export_desc": "Importuj i eksportuj notatki w formacie Markdown.",
|
||||
@@ -3638,9 +3655,10 @@
|
||||
"createDiagramCostHint": "≈ 4 kredyty AI"
|
||||
},
|
||||
"insightsView": {
|
||||
"title": "Insighty semantyczne",
|
||||
"title": "Powiązania",
|
||||
"toggleMenu": "Pokaż lub ukryj menu",
|
||||
"subtitle": "Odkryj ukrytą architekturę swojej wiedzy",
|
||||
"resync": "Ponowna synchronizacja",
|
||||
"resync": "Aktualizuj",
|
||||
"mapping": "Mapowanie…",
|
||||
"loading": "Ładowanie notatek…",
|
||||
"mappingTitle": "Mapowanie Twojej wiedzy…",
|
||||
@@ -3800,7 +3818,7 @@
|
||||
"apiKey": "Klucz API",
|
||||
"apiUrl": "URL API",
|
||||
"baseUrlRequired": "Podaj adres URL API",
|
||||
"byokActive": "BYOK aktywny",
|
||||
"byokActive": "Klucze aktywne",
|
||||
"choose": "Wybierz…",
|
||||
"chooseModel": "Wybierz model…",
|
||||
"chooseProvider": "Wybierz dostawcę…",
|
||||
@@ -4011,7 +4029,7 @@
|
||||
"tabProgress": "Postęp",
|
||||
"tapToFlip": "Spacja lub stuknij, aby odwrócić",
|
||||
"toolbarGenerate": "Wygeneruj fiszki",
|
||||
"toolbarGenerateHint": "Rozstawione powtarzanie SM-2",
|
||||
"toolbarGenerateHint": "Wracają we właściwym momencie",
|
||||
"totalCardsLabel": "Wszystkie karty",
|
||||
"totalReviewsLabel": "Wszystkie powtórki",
|
||||
"upToDate": "Aktualne",
|
||||
@@ -4055,6 +4073,9 @@
|
||||
"homeDashboard": {
|
||||
"activityEmptyHint": "Edytuj notatki, aby zobaczyć swój rytm pisania z ostatnich 90 dni.",
|
||||
"agentCreated": "Agent utworzony i uruchomiony",
|
||||
"agentCreatedInCard": "Agent jest gotowy.",
|
||||
"agentOpenCreated": "Otwórz agenta",
|
||||
"agentSeeNextSuggestion": "Zobacz następną",
|
||||
"agentDiscovery": "Agent",
|
||||
"agentFailed": "Utworzenie nie powiodło się",
|
||||
"agentsEmpty": "Nie zaproponowano jeszcze agentów badawczych. Pisząc dalej — Memento zaproponuje tematy syntezy, gdy Twoje notatki się pogrupują.",
|
||||
@@ -4063,6 +4084,8 @@
|
||||
"aiFound": "AI znalazł",
|
||||
"aiProviderUnavailable": "AI jest tymczasowo niedostępny. Sprawdź ustawienia dostawcy.",
|
||||
"allCaughtUp": "Wszystko ukończone.",
|
||||
"remindersEmpty": "Brak przypomnień na dziś.",
|
||||
"remindersOpenAll": "Zobacz przypomnienia",
|
||||
"alreadySeen": "widziane",
|
||||
"analyzeNotes": "Przeanalizuj moje notatki",
|
||||
"analyzing": "Analizuję…",
|
||||
@@ -4180,6 +4203,8 @@
|
||||
"pulseReview": "{count} do powtórki",
|
||||
"quickCapture": "Szybki zapis",
|
||||
"quickCapturePlaceholder": "Pomysł, myśl… Naciśnij Enter, aby przechwycić do skrzynki.",
|
||||
"captureGoesToFile": "Trafia do skrzynki.",
|
||||
"captureSend": "Wyślij do skrzynki",
|
||||
"reminders": "Przypomnienia",
|
||||
"resumeAlso": "Również niedawno",
|
||||
"resumeEmptyCta": "Przechwyć pomysł",
|
||||
@@ -4251,7 +4276,7 @@
|
||||
"inbox": "Notatki bez notatnika. Przypisz je, aby utrzymać porządek w swoim drugim mózgu.",
|
||||
"intelligence": "Odkrycia AI: powiązania semantyczne między notatkami, pomysły pomostowe i ustalenia agentów.",
|
||||
"link-suggestions": "Fragmenty z innych notatek, które warto połączyć z Twoją bieżącą pracą.",
|
||||
"mind-map": "Klastry tematyczne o rozmiarze zależnym od liczby notatek. Kliknij, aby eksplorować w Insights.",
|
||||
"mind-map": "Klastry tematyczne o rozmiarze zależnym od liczby notatek. Kliknij, aby eksplorować w Powiązania.",
|
||||
"next-paths": "Sugerowane następne kroki na podstawie ostatnio edytowanej notatki: wznów, połącz, zintegruj lub zbadaj.",
|
||||
"open-loops": "Notatki, które zacząłeś, ale nie dotykałeś od 3+ dni.",
|
||||
"pinned": "Szybki dostęp do przypiętych notatek.",
|
||||
@@ -4267,6 +4292,7 @@
|
||||
"widgetHide": "Ukryj widżet",
|
||||
"widgetOpen": "Otwórz",
|
||||
"widgetPinnedEmpty": "Brak przypiętych notatek.",
|
||||
"pinnedNoNotebook": "Bez notesu",
|
||||
"widgetReset": "Resetuj",
|
||||
"widgetResetDone": "Domyślny pulpit przywrócony.",
|
||||
"widgetStatsBridges": "Pomosty",
|
||||
|
||||
@@ -31,14 +31,14 @@
|
||||
"confirmPasswordPlaceholder": "Confirme sua senha",
|
||||
"backToSite": "Voltar",
|
||||
"continueWithGoogle": "Continuar com Google",
|
||||
"createYourSpace": "Crie seu espaço",
|
||||
"createYourSpaceSubtitle": "Junte-se à nova era de anotações inteligentes.",
|
||||
"createYourSpace": "Crie seu segundo cérebro",
|
||||
"createYourSpaceSubtitle": "Ele se conecta enquanto você escreve — não é mais um app de notas.",
|
||||
"forgot": "Esqueceu?",
|
||||
"oauthAccountNotLinked": "Esta conta do Google não corresponde à sua conta existente. Use o mesmo e-mail ou entre com sua senha.",
|
||||
"privacyTerms": "© 2025 Memento Labs — Privacidade · Termos",
|
||||
"sessionExpired": "Seu site é gerado com navegação e sumário",
|
||||
"welcomeBack": "Bem-vindo de volta",
|
||||
"welcomeBackSubtitle": "Digite suas credenciais para acessar suas notas.",
|
||||
"welcomeBackSubtitle": "Entre para reencontrar suas notas, conexões e o que você tinha esquecido.",
|
||||
"checkEmailTitle": "Verifique o seu e-mail",
|
||||
"checkEmailDescription": "Enviámos um link de confirmação para {email}. Abra-o para ativar a conta antes de entrar.",
|
||||
"checkEmailDescriptionGeneric": "Enviámos um link de confirmação para o seu e-mail. Abra-o para ativar a conta antes de entrar.",
|
||||
@@ -99,13 +99,13 @@
|
||||
"darkMode": "Modo escuro",
|
||||
"dashboardPanelBody": "Sua sessão expirou. Por favor, entre novamente.",
|
||||
"documents": "Documentos",
|
||||
"insightsPanelBody": "Mapa semântico de suas notas: clusters temáticos, notas-ponte e sugestões de conexão.",
|
||||
"insightsPanelBody": "Um mapa de como suas notas se conectam: temas próximos, notas-ponte e links para abrir.",
|
||||
"lightMode": "Modo claro",
|
||||
"notebookEmpty": "Vazio",
|
||||
"recentNote": "Criadas recentemente",
|
||||
"resizeNotebooksPanel": "Redimensionar painel de cadernos",
|
||||
"resizeSidebar": "Redimensionar largura da barra lateral",
|
||||
"revisionPanelBody": "Revise flashcards com o algoritmo SM-2. Os baralhos são gerados a partir de suas notas.",
|
||||
"revisionPanelBody": "Revise com flashcards. A repetição espaçada as traz de volta na hora certa. Os baralhos vêm das suas notas.",
|
||||
"searchNotebooksPlaceholder": "Pesquisar cadernos…",
|
||||
"searchShortcut": "Pesquisar (Ctrl+K)"
|
||||
},
|
||||
@@ -130,7 +130,7 @@
|
||||
"add": "Adicionar",
|
||||
"adding": "Adicionando...",
|
||||
"close": "Fechar",
|
||||
"confirmDelete": "Tem certeza de que deseja excluir esta nota?",
|
||||
"confirmDelete": "Esta nota irá para o lixo. Você poderá recuperá-la depois.",
|
||||
"confirmLeaveShare": "Tem certeza de que deseja sair desta nota compartilhada?",
|
||||
"sharedBy": "Compartilhado por",
|
||||
"sharedShort": "Compartilhado",
|
||||
@@ -991,7 +991,7 @@
|
||||
"dailyNotes": "Notas diárias",
|
||||
"dashboard": "Painel",
|
||||
"graphView": "Mapa de links",
|
||||
"insights": "Temas semânticos",
|
||||
"insights": "Conexões",
|
||||
"revision": "Revisar"
|
||||
},
|
||||
"settings": {
|
||||
@@ -2095,14 +2095,14 @@
|
||||
"collapse": "Recolher"
|
||||
},
|
||||
"mcpSettings": {
|
||||
"title": "MCP",
|
||||
"title": "Ferramentas externas",
|
||||
"description": "Gerencie suas chaves API e configure ferramentas externas",
|
||||
"tierRequired": "Apenas Pro+",
|
||||
"upgradeHint": "O acesso MCP (chaves de API para Cursor, Claude Desktop, etc.) requer um plano Pro ou superior. Faça upgrade em Faturamento para desbloquear este recurso.",
|
||||
"whatIsMcp": {
|
||||
"title": "O que é MCP?",
|
||||
"title": "Para que serve?",
|
||||
"description": "O Model Context Protocol (MCP) é um protocolo aberto que permite que modelos de IA interajam de forma segura com ferramentas e fontes de dados externas. Com o MCP, você pode conectar ferramentas como Claude Code, Cursor ou N8N à sua instância do Memento para ler, criar e organizar suas notas programaticamente.",
|
||||
"learnMore": "Saiba mais sobre o MCP"
|
||||
"learnMore": "Saiba mais"
|
||||
},
|
||||
"serverStatus": {
|
||||
"title": "Status do servidor",
|
||||
@@ -2161,12 +2161,12 @@
|
||||
}
|
||||
},
|
||||
"helpBox": {
|
||||
"title": "O que é MCP (Model Context Protocol)?",
|
||||
"title": "Como ligar uma ferramenta?",
|
||||
"step1": "MCP é um protocolo que permite que os agentes IA do Memento se conectem a ferramentas externas (bancos de dados, APIs, arquivos, etc.).",
|
||||
"step2": "O Memento expõe um servidor MCP com 22 ferramentas — seus agentes podem ler/criar notas, pesquisar em sua base, gerenciar cadernos, etc.",
|
||||
"step3": "Crie uma chave de API aqui e configure-a no seu cliente MCP (Claude Desktop, Cursor, Continue.dev…) com a URL do servidor.",
|
||||
"step4": "Formato de configuração: URL do servidor MCP + sua chave no header Authorization.",
|
||||
"step4Link": "Documentação MCP",
|
||||
"step4Link": "Ajuda oficial",
|
||||
"step5": "Caso de uso: peça ao Claude Desktop para escrever uma nota no Memento, pesquisar em seus cadernos ou criar um agente."
|
||||
}
|
||||
},
|
||||
@@ -2334,6 +2334,17 @@
|
||||
"title": "Modelos",
|
||||
"install": "Instalar",
|
||||
"installing": "Instalando...",
|
||||
"seeAll": "Ver todos",
|
||||
"showLess": "Ver menos",
|
||||
"categoryAll": "Todos",
|
||||
"categoryWatch": "Acompanhamento",
|
||||
"categoryDigest": "Resumos",
|
||||
"categoryTools": "Ferramentas",
|
||||
"categoryGenerate": "Criar",
|
||||
"taskExtractor": {
|
||||
"name": "Tarefas nas notas",
|
||||
"description": "Encontra as tarefas nas suas notas e reúne-as no mesmo lugar."
|
||||
},
|
||||
"veilleAI": {
|
||||
"name": "Watch IA",
|
||||
"description": "Extrai conteúdo de 5 sites especializados em IA e gera um resumo semanal."
|
||||
@@ -2453,7 +2464,7 @@
|
||||
"slideStyle": "O estilo visual afeta o raio do canto, o espaçamento e a densidade da informação."
|
||||
}
|
||||
},
|
||||
"intelligenceOS": "Sistema operacional inteligente"
|
||||
"intelligenceOS": "Agentes"
|
||||
},
|
||||
"chat": {
|
||||
"title": "Chat IA",
|
||||
@@ -2740,7 +2751,7 @@
|
||||
"slashDatabaseDesc": "Incorpore os dados estruturados do seu caderno",
|
||||
"slashLinkPreview": "Pré-visualização de link",
|
||||
"slashLinkPreviewDesc": "Transformar um URL em um cartão visual",
|
||||
"slashLivingBlock": "Bloco dinâmico",
|
||||
"slashLivingBlock": "Bloco ligado",
|
||||
"slashLivingBlockDesc": "Inserir de outra nota",
|
||||
"slashMath": "Equação",
|
||||
"slashMathDesc": "Fórmula matemática em notação LaTeX",
|
||||
@@ -3007,7 +3018,7 @@
|
||||
"proChat": "100 chat messages / month",
|
||||
"later": "Mais tarde",
|
||||
"upgradePricing": "Atualizar para Pro",
|
||||
"addApiKey": "Use a sua própria chave de API (BYOK)",
|
||||
"addApiKey": "Use a sua própria chave",
|
||||
"featureBrainstormCreate": "Créations brainstorm",
|
||||
"featureBrainstormEnrich": "Enrichissements brainstorm",
|
||||
"featureBrainstormExpand": "Extensions brainstorm",
|
||||
@@ -3025,10 +3036,10 @@
|
||||
"outOfCredits": "Sem créditos — opções"
|
||||
},
|
||||
"byokSettings": {
|
||||
"title": "As suas chaves de API (BYOK)",
|
||||
"title": "As suas chaves de fornecedor",
|
||||
"description": "Connect your own LLM provider keys to bypass Discovery Pack quotas. Keys are encrypted at rest.",
|
||||
"badgeActive": "BYOK ativo",
|
||||
"tierRequired": "O BYOK requer um plano Pro ou superior. Faça upgrade para conectar as suas chaves de API.",
|
||||
"badgeActive": "Chaves ativas",
|
||||
"tierRequired": "Esta opção requer um plano Pro ou superior.",
|
||||
"provider": "Provedor",
|
||||
"providerPlaceholder": "Selecione um fornecedor",
|
||||
"alias": "Etiqueta (opcional)",
|
||||
@@ -3167,6 +3178,9 @@
|
||||
"fetchInvoicesFailed": "Falha ao carregar o histórico de cobrança.",
|
||||
"savePercent": "Economize ~17%",
|
||||
"cancelSubscription": "Cancelar subscrição",
|
||||
"changeOffer": "Mudar de oferta",
|
||||
"downgradeToFree": "Voltar à oferta gratuita",
|
||||
"cancellingNotice": "Cancelamento previsto — acesso até {date}",
|
||||
"disabledByAdmin": "A cobrança e as atualizações de plano estão desativadas. Entre em contato com seu administrador se precisar de acesso.",
|
||||
"tab": "Faturação",
|
||||
"creditsFromPacks": "Créditos de pacotes",
|
||||
@@ -3232,20 +3246,20 @@
|
||||
"title": "O momento em que seu Second Brain responde.",
|
||||
"desc": "Enquanto você escreve, o Memento detecta vínculos semânticos entre cadernos — não palavras-chave, pontes conceituais reais.",
|
||||
"card0Label": "Recém-detectado",
|
||||
"card0": "«Sua nota de pricing ecoa a análise de concorrentes do outono.»",
|
||||
"card0": "«Sua nota sobre os preços ecoa a análise de concorrentes do outono.»",
|
||||
"card1Label": "Ponte",
|
||||
"card1": "Tema compartilhado: posicionamento sob restrição",
|
||||
"card2Label": "Ação",
|
||||
"card2": "Abrir as duas notas lado a lado — continue escrevendo"
|
||||
},
|
||||
"dashboard": {
|
||||
"eyebrow": "Dashboard Second Brain",
|
||||
"eyebrow": "A manhã",
|
||||
"title": "Uma manhã que diz o que importa.",
|
||||
"desc": "O briefing do seu Second Brain: próximos caminhos, checklist, descobertas, revisão.",
|
||||
"w0Label": "Próximos caminhos",
|
||||
"w0": "3 ações da sua última nota",
|
||||
"w1Label": "Revisão diária",
|
||||
"w1": "Inbox · IA · Flashcards",
|
||||
"w1": "Caixa de entrada · Descobertas · Cartões",
|
||||
"w2Label": "Memory Echo",
|
||||
"w2": "2 novas conexões esta noite",
|
||||
"w3Label": "Agentes",
|
||||
@@ -3284,34 +3298,37 @@
|
||||
"title": "Delegue o trabalho pesado.",
|
||||
"desc": "Pesquisa, scrape, slides, diagramas, monitoramento — agentes que escrevem no seu Second Brain.",
|
||||
"scraper": {
|
||||
"title": "Scraper",
|
||||
"title": "Monitor",
|
||||
"desc": "URLs e RSS → notas sintetizadas com imagens."
|
||||
},
|
||||
"researcher": {
|
||||
"title": "Researcher",
|
||||
"title": "Pesquisador",
|
||||
"desc": "Consultas profundas, fontes, notas de pesquisa."
|
||||
},
|
||||
"slideGen": {
|
||||
"title": "Slide Gen",
|
||||
"title": "Apresentações",
|
||||
"desc": "Notas → decks ou slides HTML interativas."
|
||||
},
|
||||
"monitor": {
|
||||
"title": "Monitor",
|
||||
"title": "Observador",
|
||||
"desc": "Monitora cadernos: tendências e insights."
|
||||
},
|
||||
"diagramGen": {
|
||||
"title": "Diagram Gen",
|
||||
"title": "Diagrama",
|
||||
"desc": "Ideias → mindmaps e flows Excalidraw."
|
||||
},
|
||||
"custom": {
|
||||
"title": "Custom",
|
||||
"title": "Personalizado",
|
||||
"desc": "Seus papéis, fontes e agendas."
|
||||
}
|
||||
},
|
||||
"byok": {
|
||||
"label": "Sem lock-in",
|
||||
"label": "Livre para mudar",
|
||||
"title": "Suas chaves. Seus modelos. Seu Second Brain.",
|
||||
"desc": "Créditos Memento ou OpenAI, Anthropic, Google… Troque de provedor num clique."
|
||||
"desc": "Créditos Memento, ou o seu próprio fornecedor. Troque num clique — o produto continua seu.",
|
||||
"pointCredits": "Créditos Memento, se quiser manter simples",
|
||||
"pointProvider": "Ou ligue o seu próprio fornecedor",
|
||||
"pointYours": "A chave fica consigo — nunca é mostrada aqui"
|
||||
},
|
||||
"pricing": {
|
||||
"label": "Pricing",
|
||||
@@ -3339,7 +3356,7 @@
|
||||
"desc": "Para mentes exigentes.",
|
||||
"cta": "Ir de Pro",
|
||||
"feature0": "Notas ilimitadas",
|
||||
"feature1": "BYOK",
|
||||
"feature1": "As suas próprias chaves",
|
||||
"feature2": "200 buscas semânticas / mês",
|
||||
"feature3": "Agentes (12 runs/mês)",
|
||||
"feature4": "Histórico 30 dias",
|
||||
@@ -3350,11 +3367,11 @@
|
||||
"desc": "Second Brain de equipe.",
|
||||
"cta": "Escolher Business",
|
||||
"feature0": "10 colaboradores",
|
||||
"feature1": "BYOK · 13 provedores",
|
||||
"feature1": "As suas chaves · {count} fornecedores",
|
||||
"feature2": "1.000 buscas semânticas",
|
||||
"feature3": "Agentes (60 runs/mês)",
|
||||
"feature4": "Brainstorm ilimitado",
|
||||
"feature5": "API / MCP"
|
||||
"feature5": "Ferramentas externas"
|
||||
},
|
||||
"enterprise": {
|
||||
"name": "Enterprise",
|
||||
@@ -3545,12 +3562,12 @@
|
||||
"feature_search_title": "Busca semântica",
|
||||
"feature_search_desc": "Encontre qualquer nota por significado, não apenas por palavras-chave.",
|
||||
"feature_flashcards_title": "Flashcards IA",
|
||||
"feature_flashcards_desc": "Gere cartões de revisão SRS das suas notas com um clique.",
|
||||
"feature_flashcards_desc": "Gere cartões de revisão das suas notas com um clique.",
|
||||
"feature_brainstorm_title": "Brainstorming IA",
|
||||
"feature_brainstorm_desc": "Sessões de brainstorming colaborativo com IA.",
|
||||
"feature_chat_title": "Converse com suas notas",
|
||||
"feature_chat_desc": "Faça perguntas à sua base de conhecimento pessoal.",
|
||||
"feature_insights_title": "Insights semânticos",
|
||||
"feature_insights_title": "Conexões",
|
||||
"feature_insights_desc": "Descubra conexões ocultas entre suas ideias.",
|
||||
"feature_export_title": "Exportação Markdown",
|
||||
"feature_export_desc": "Importe e exporte suas notas em formato Markdown padrão.",
|
||||
@@ -3638,9 +3655,10 @@
|
||||
"createDiagramCostHint": "≈ 4 créditos de IA"
|
||||
},
|
||||
"insightsView": {
|
||||
"title": "Insights semânticos",
|
||||
"title": "Conexões",
|
||||
"toggleMenu": "Mostrar ou ocultar o menu",
|
||||
"subtitle": "Descobre a arquitetura oculta do teu conhecimento",
|
||||
"resync": "Ressincronizar rede",
|
||||
"resync": "Atualizar",
|
||||
"mapping": "Mapeando…",
|
||||
"loading": "A carregar notas…",
|
||||
"mappingTitle": "Mapeando o teu conhecimento…",
|
||||
@@ -3800,7 +3818,7 @@
|
||||
"apiKey": "Chave API",
|
||||
"apiUrl": "URL da API",
|
||||
"baseUrlRequired": "Forneça a URL da API",
|
||||
"byokActive": "BYOK ativo",
|
||||
"byokActive": "Chaves ativas",
|
||||
"choose": "Escolher…",
|
||||
"chooseModel": "Escolha um modelo…",
|
||||
"chooseProvider": "Escolha um provedor…",
|
||||
@@ -4011,7 +4029,7 @@
|
||||
"tabProgress": "Progresso",
|
||||
"tapToFlip": "Espaço ou toque para virar",
|
||||
"toolbarGenerate": "Gerar flashcards",
|
||||
"toolbarGenerateHint": "Repetição espaçada SM-2",
|
||||
"toolbarGenerateHint": "Voltam no momento certo",
|
||||
"totalCardsLabel": "Total de cartões",
|
||||
"totalReviewsLabel": "Total de revisões",
|
||||
"upToDate": "Atualizado",
|
||||
@@ -4055,6 +4073,9 @@
|
||||
"homeDashboard": {
|
||||
"activityEmptyHint": "Edite notas para ver seu ritmo de escrita nos últimos 90 dias.",
|
||||
"agentCreated": "Agente criado e iniciado",
|
||||
"agentCreatedInCard": "O agente está pronto.",
|
||||
"agentOpenCreated": "Abrir o agente",
|
||||
"agentSeeNextSuggestion": "Ver a seguinte",
|
||||
"agentDiscovery": "Agente",
|
||||
"agentFailed": "Falha ao criar",
|
||||
"agentsEmpty": "Nenhum agente de pesquisa sugerido ainda. Continue escrevendo — o Memento proporá tópicos de síntese quando suas notas se agruparem.",
|
||||
@@ -4063,6 +4084,8 @@
|
||||
"aiFound": "IA encontrou",
|
||||
"aiProviderUnavailable": "A IA está temporariamente indisponível. Verifique as configurações do provedor.",
|
||||
"allCaughtUp": "Tudo em dia.",
|
||||
"remindersEmpty": "Nenhum lembrete para hoje.",
|
||||
"remindersOpenAll": "Ver lembretes",
|
||||
"alreadySeen": "visto",
|
||||
"analyzeNotes": "Analisar minhas notas",
|
||||
"analyzing": "Analisando…",
|
||||
@@ -4180,6 +4203,8 @@
|
||||
"pulseReview": "{count} para revisar",
|
||||
"quickCapture": "Captura rápida",
|
||||
"quickCapturePlaceholder": "Uma ideia, um pensamento… Pressione Enter para capturar na caixa de entrada.",
|
||||
"captureGoesToFile": "Vai para a caixa de entrada.",
|
||||
"captureSend": "Enviar para a caixa de entrada",
|
||||
"reminders": "Lembretes",
|
||||
"resumeAlso": "Também recentemente",
|
||||
"resumeEmptyCta": "Capturar uma ideia",
|
||||
@@ -4192,7 +4217,7 @@
|
||||
"suggestedBridge": "Ligar {clusterA} & {clusterB}",
|
||||
"suggestedResearch": "Pesquisa sugerida",
|
||||
"theme": "Tema",
|
||||
"themes": "temas",
|
||||
"themes": "Temas",
|
||||
"title": "Painel",
|
||||
"toOrganize": "para organizar",
|
||||
"toReview": "Para revisar",
|
||||
@@ -4251,7 +4276,7 @@
|
||||
"inbox": "Notas sem caderno ainda. Arquive-as para manter seu segundo cérebro organizado.",
|
||||
"intelligence": "Descobertas IA: links semânticos entre notas, ideias ponte e descobertas de agentes.",
|
||||
"link-suggestions": "Passagens de outras notas que valem a pena vincular ao seu trabalho atual.",
|
||||
"mind-map": "Clusters de temas dimensionados pelo volume de notas. Clique para explorar em Insights.",
|
||||
"mind-map": "Clusters de temas dimensionados pelo volume de notas. Clique para explorar em Conexões.",
|
||||
"next-paths": "Próximos passos sugeridos com base na sua última nota editada: retomar, vincular, conectar ou pesquisar.",
|
||||
"open-loops": "Notas que você começou mas não toca há 3+ dias.",
|
||||
"pinned": "Acesso rápido às notas fixadas.",
|
||||
@@ -4267,6 +4292,7 @@
|
||||
"widgetHide": "Ocultar widget",
|
||||
"widgetOpen": "Abrir",
|
||||
"widgetPinnedEmpty": "Sem notas fixadas ainda.",
|
||||
"pinnedNoNotebook": "Sem caderno",
|
||||
"widgetReset": "Redefinir",
|
||||
"widgetResetDone": "Painel padrão restaurado.",
|
||||
"widgetStatsBridges": "Pontes",
|
||||
|
||||
@@ -31,14 +31,14 @@
|
||||
"confirmPasswordPlaceholder": "Подтвердите пароль",
|
||||
"backToSite": "Назад",
|
||||
"continueWithGoogle": "Продолжить с Google",
|
||||
"createYourSpace": "Создайте своё пространство",
|
||||
"createYourSpaceSubtitle": "Присоединяйтесь к новой эре умных заметок.",
|
||||
"createYourSpace": "Создайте свой второй мозг",
|
||||
"createYourSpaceSubtitle": "Он связывается, пока вы пишете — не просто ещё одно приложение для заметок.",
|
||||
"forgot": "Забыли?",
|
||||
"oauthAccountNotLinked": "Этот аккаунт Google не соответствует вашему существующему аккаунту. Используйте тот же email или войдите с паролем.",
|
||||
"privacyTerms": "© 2025 Memento Labs — Конфиденциальность · Условия",
|
||||
"sessionExpired": "Ваш сайт создаётся с навигацией и оглавлением",
|
||||
"welcomeBack": "С возвращением",
|
||||
"welcomeBackSubtitle": "Введите свои учётные данные для доступа к заметкам.",
|
||||
"welcomeBackSubtitle": "Войдите, чтобы найти заметки, связи и то, что вы забыли.",
|
||||
"checkEmailTitle": "Проверьте почту",
|
||||
"checkEmailDescription": "Мы отправили ссылку подтверждения на {email}. Откройте её, чтобы активировать аккаунт перед входом.",
|
||||
"checkEmailDescriptionGeneric": "Мы отправили ссылку подтверждения на вашу почту. Откройте её, чтобы активировать аккаунт перед входом.",
|
||||
@@ -99,13 +99,13 @@
|
||||
"darkMode": "Тёмный режим",
|
||||
"dashboardPanelBody": "Ваша сессия истекла. Пожалуйста, войдите снова.",
|
||||
"documents": "Документы",
|
||||
"insightsPanelBody": "Семантическая карта ваших заметок: тематические кластеры, мостовые заметки и предложения связей.",
|
||||
"insightsPanelBody": "Карта связей между заметками: близкие темы, заметки-мосты и ссылки, которые можно открыть.",
|
||||
"lightMode": "Светлая тема",
|
||||
"notebookEmpty": "Пусто",
|
||||
"recentNote": "Недавно созданные",
|
||||
"resizeNotebooksPanel": "Изменить размер панели блокнотов",
|
||||
"resizeSidebar": "Изменить ширину боковой панели",
|
||||
"revisionPanelBody": "Повторяйте карточки по алгоритму SM-2. Колоды создаются из ваших заметок.",
|
||||
"revisionPanelBody": "Повторяйте с карточками. Интервальные повторения возвращают их в нужный момент. Колоды создаются из ваших заметок.",
|
||||
"searchNotebooksPlaceholder": "Искать блокноты…",
|
||||
"searchShortcut": "Поиск (Ctrl+K)"
|
||||
},
|
||||
@@ -130,7 +130,7 @@
|
||||
"add": "Добавить",
|
||||
"adding": "Добавление...",
|
||||
"close": "Закрыть",
|
||||
"confirmDelete": "Вы уверены, что хотите удалить эту заметку?",
|
||||
"confirmDelete": "Эта заметка попадёт в корзину. Вы сможете восстановить её позже.",
|
||||
"confirmLeaveShare": "Вы уверены, что хотите покинуть эту общую заметку?",
|
||||
"sharedBy": "Поделился",
|
||||
"sharedShort": "Общий",
|
||||
@@ -991,7 +991,7 @@
|
||||
"dailyNotes": "Ежедневные заметки",
|
||||
"dashboard": "Панель управления",
|
||||
"graphView": "Карта ссылок",
|
||||
"insights": "Семантические темы",
|
||||
"insights": "Связи",
|
||||
"revision": "Повторять"
|
||||
},
|
||||
"settings": {
|
||||
@@ -2095,14 +2095,14 @@
|
||||
"collapse": "Свернуть"
|
||||
},
|
||||
"mcpSettings": {
|
||||
"title": "MCP",
|
||||
"title": "Внешние инструменты",
|
||||
"description": "Управление ключами API и настройка внешних инструментов",
|
||||
"tierRequired": "Только Pro+",
|
||||
"upgradeHint": "Доступ к MCP (ключи API для Cursor, Claude Desktop и т.д.) требует тарифа Pro или выше. Обновите тариф в разделе Биллинг, чтобы разблокировать эту функцию.",
|
||||
"whatIsMcp": {
|
||||
"title": "Что такое MCP?",
|
||||
"title": "Зачем это нужно?",
|
||||
"description": "Model Context Protocol (MCP) — это открытый протокол, позволяющий моделям ИИ безопасно взаимодействовать с внешними инструментами и источниками данных. С помощью MCP вы можете подключить такие инструменты, как Claude Code, Cursor или N8N, к вашему экземпляру Memento для программного чтения, создания и организации заметок.",
|
||||
"learnMore": "Подробнее о MCP"
|
||||
"learnMore": "Подробнее"
|
||||
},
|
||||
"serverStatus": {
|
||||
"title": "Состояние сервера",
|
||||
@@ -2161,12 +2161,12 @@
|
||||
}
|
||||
},
|
||||
"helpBox": {
|
||||
"title": "Что такое MCP (Model Context Protocol)?",
|
||||
"title": "Как подключить инструмент?",
|
||||
"step1": "MCP — это протокол, который позволяет ИИ-агентам Memento подключаться к внешним инструментам (базам данных, API, файлам и т.д.).",
|
||||
"step2": "Memento предоставляет MCP-сервер с 22 инструментами — ваши агенты могут читать/создавать заметки, искать по базе, управлять записными книжками и т.д.",
|
||||
"step3": "Создайте ключ API здесь, затем настройте его в вашем MCP-клиенте (Claude Desktop, Cursor, Continue.dev…) с URL-адресом сервера.",
|
||||
"step4": "Формат конфигурации: URL-адрес MCP-сервера + ваш ключ в заголовке Authorization.",
|
||||
"step4Link": "Документация MCP",
|
||||
"step4Link": "Официальная справка",
|
||||
"step5": "Сценарий использования: попросите Claude Desktop написать заметку в Memento, найти записи в блокнотах или создать агента."
|
||||
}
|
||||
},
|
||||
@@ -2334,6 +2334,17 @@
|
||||
"title": "Шаблоны",
|
||||
"install": "Установить",
|
||||
"installing": "Установка...",
|
||||
"seeAll": "Показать все",
|
||||
"showLess": "Показать меньше",
|
||||
"categoryAll": "Все",
|
||||
"categoryWatch": "Обзор",
|
||||
"categoryDigest": "Сводки",
|
||||
"categoryTools": "Инструменты",
|
||||
"categoryGenerate": "Создание",
|
||||
"taskExtractor": {
|
||||
"name": "Задачи из заметок",
|
||||
"description": "Находит задачи в ваших заметках и собирает их в одном месте."
|
||||
},
|
||||
"veilleAI": {
|
||||
"name": "Обзор ИИ",
|
||||
"description": "Собирает данные с 5 сайтов, специализирующихся на ИИ, и генерирует еженедельную сводку."
|
||||
@@ -2453,7 +2464,7 @@
|
||||
"slideStyle": "Визуальный стиль влияет на радиус угла, расстояние и плотность информации."
|
||||
}
|
||||
},
|
||||
"intelligenceOS": "Интеллектуальная ОС"
|
||||
"intelligenceOS": "Агенты"
|
||||
},
|
||||
"chat": {
|
||||
"title": "ИИ-чат",
|
||||
@@ -2740,7 +2751,7 @@
|
||||
"slashDatabaseDesc": "Встройте структурированные данные вашего блокнота",
|
||||
"slashLinkPreview": "Предпросмотр ссылки",
|
||||
"slashLinkPreviewDesc": "Превратить URL в визуальную карточку",
|
||||
"slashLivingBlock": "Живой блок",
|
||||
"slashLivingBlock": "Связанный блок",
|
||||
"slashLivingBlockDesc": "Вставить из другой заметки",
|
||||
"slashMath": "Уравнение",
|
||||
"slashMathDesc": "Математическая формула в нотации LaTeX",
|
||||
@@ -3007,7 +3018,7 @@
|
||||
"proChat": "100 chat messages / month",
|
||||
"later": "Позже",
|
||||
"upgradePricing": "Обновить до Pro",
|
||||
"addApiKey": "Использовать свой API-ключ (BYOK)",
|
||||
"addApiKey": "Использовать свой ключ",
|
||||
"featureBrainstormCreate": "Créations brainstorm",
|
||||
"featureBrainstormEnrich": "Enrichissements brainstorm",
|
||||
"featureBrainstormExpand": "Extensions brainstorm",
|
||||
@@ -3025,10 +3036,10 @@
|
||||
"outOfCredits": "Кредиты закончились — варианты"
|
||||
},
|
||||
"byokSettings": {
|
||||
"title": "Ваши API-ключи (BYOK)",
|
||||
"title": "Ваши ключи поставщика",
|
||||
"description": "Connect your own LLM provider keys to bypass Discovery Pack quotas. Keys are encrypted at rest.",
|
||||
"badgeActive": "BYOK активен",
|
||||
"tierRequired": "BYOK требует тариф Pro или выше. Обновите тариф для подключения API-ключей.",
|
||||
"badgeActive": "Ключи активны",
|
||||
"tierRequired": "Для этого нужен тариф Pro или выше.",
|
||||
"provider": "Провайдер",
|
||||
"providerPlaceholder": "Выберите провайдера",
|
||||
"alias": "Метка (необязательно)",
|
||||
@@ -3167,6 +3178,9 @@
|
||||
"fetchInvoicesFailed": "Не удалось загрузить историю счетов.",
|
||||
"savePercent": "Экономия ~17%",
|
||||
"cancelSubscription": "Отменить подписку",
|
||||
"changeOffer": "Сменить тариф",
|
||||
"downgradeToFree": "Вернуться к бесплатному тарифу",
|
||||
"cancellingNotice": "Отмена запланирована — доступ до {date}",
|
||||
"disabledByAdmin": "Оплата и обновление планов в настоящее время отключены. Обратитесь к администратору, если вам нужен доступ.",
|
||||
"tab": "Оплата",
|
||||
"creditsFromPacks": "Кредиты из пакетов",
|
||||
@@ -3245,7 +3259,7 @@
|
||||
"w0Label": "Следующие шаги",
|
||||
"w0": "3 действия из последней заметки",
|
||||
"w1Label": "Ежедневный обзор",
|
||||
"w1": "Inbox · ИИ · Карточки",
|
||||
"w1": "Входящие · Находки · Карточки",
|
||||
"w2Label": "Memory Echo",
|
||||
"w2": "2 новые связи за ночь",
|
||||
"w3Label": "Агенты",
|
||||
@@ -3284,34 +3298,37 @@
|
||||
"title": "Делегируйте тяжёлую работу.",
|
||||
"desc": "Исследования, scrape, слайды, диаграммы, мониторинг — агенты, которые пишут в ваш Second Brain.",
|
||||
"scraper": {
|
||||
"title": "Scraper",
|
||||
"title": "Монитор",
|
||||
"desc": "URL и RSS → синтезированные заметки с изображениями."
|
||||
},
|
||||
"researcher": {
|
||||
"title": "Researcher",
|
||||
"title": "Исследователь",
|
||||
"desc": "Глубокие запросы, источники, структурированные заметки."
|
||||
},
|
||||
"slideGen": {
|
||||
"title": "Slide Gen",
|
||||
"title": "Слайды",
|
||||
"desc": "Заметки → презентации или интерактивные HTML-слайды."
|
||||
},
|
||||
"monitor": {
|
||||
"title": "Monitor",
|
||||
"title": "Наблюдатель",
|
||||
"desc": "Следит за блокнотами: тренды и инсайты."
|
||||
},
|
||||
"diagramGen": {
|
||||
"title": "Diagram Gen",
|
||||
"title": "Диаграмма",
|
||||
"desc": "Идеи → mindmap и flow Excalidraw."
|
||||
},
|
||||
"custom": {
|
||||
"title": "Custom",
|
||||
"title": "Пользовательский",
|
||||
"desc": "Ваши роли, источники и расписания."
|
||||
}
|
||||
},
|
||||
"byok": {
|
||||
"label": "Без lock-in",
|
||||
"label": "Свободная смена",
|
||||
"title": "Ваши ключи. Ваши модели. Ваш Second Brain.",
|
||||
"desc": "Кредиты Memento или OpenAI, Anthropic, Google… Меняйте провайдера в один клик."
|
||||
"desc": "Кредиты Memento или свой поставщик. Меняйте в один клик — продукт остаётся вашим.",
|
||||
"pointCredits": "Кредиты Memento, если хотите проще",
|
||||
"pointProvider": "Или подключите своего поставщика",
|
||||
"pointYours": "Ключ остаётся у вас — здесь он никогда не показывается"
|
||||
},
|
||||
"pricing": {
|
||||
"label": "Pricing",
|
||||
@@ -3339,7 +3356,7 @@
|
||||
"desc": "Для требовательных умов.",
|
||||
"cta": "Выбрать Pro",
|
||||
"feature0": "Безлимитные заметки",
|
||||
"feature1": "BYOK",
|
||||
"feature1": "Свои ключи поставщика",
|
||||
"feature2": "200 сем. поисков / мес",
|
||||
"feature3": "Агенты (12 запусков/мес)",
|
||||
"feature4": "История 30 дней",
|
||||
@@ -3350,11 +3367,11 @@
|
||||
"desc": "Second Brain команды.",
|
||||
"cta": "Выбрать Business",
|
||||
"feature0": "10 сотрудников",
|
||||
"feature1": "BYOK · 13 провайдеров",
|
||||
"feature1": "Свои ключи · {count} поставщиков",
|
||||
"feature2": "1000 сем. поисков",
|
||||
"feature3": "Агенты (60 запусков/мес)",
|
||||
"feature4": "Безлимитный brainstorm",
|
||||
"feature5": "API / MCP"
|
||||
"feature5": "Внешние инструменты"
|
||||
},
|
||||
"enterprise": {
|
||||
"name": "Enterprise",
|
||||
@@ -3545,12 +3562,12 @@
|
||||
"feature_search_title": "Семантический поиск",
|
||||
"feature_search_desc": "Находите любую заметку по смыслу, а не только по ключевым словам.",
|
||||
"feature_flashcards_title": "Карточки ИИ",
|
||||
"feature_flashcards_desc": "Создавайте карточки для повторения SRS из заметок одним кликом.",
|
||||
"feature_flashcards_desc": "Создавайте карточки для повторения из заметок одним кликом.",
|
||||
"feature_brainstorm_title": "Мозговой штурм ИИ",
|
||||
"feature_brainstorm_desc": "Совместные сессии мозгового штурма с поддержкой ИИ.",
|
||||
"feature_chat_title": "Чат с заметками",
|
||||
"feature_chat_desc": "Задавайте вопросы своей личной базе знаний.",
|
||||
"feature_insights_title": "Семантические инсайты",
|
||||
"feature_insights_title": "Связи",
|
||||
"feature_insights_desc": "Откройте скрытые связи между вашими идеями.",
|
||||
"feature_export_title": "Экспорт Markdown",
|
||||
"feature_export_desc": "Импортируйте и экспортируйте заметки в формате Markdown.",
|
||||
@@ -3638,9 +3655,10 @@
|
||||
"createDiagramCostHint": "≈ 4 кредита ИИ"
|
||||
},
|
||||
"insightsView": {
|
||||
"title": "Семантические инсайты",
|
||||
"title": "Связи",
|
||||
"toggleMenu": "Показать или скрыть меню",
|
||||
"subtitle": "Откройте скрытую архитектуру вашего знания",
|
||||
"resync": "Пересинхронизировать",
|
||||
"resync": "Обновить",
|
||||
"mapping": "Картирование…",
|
||||
"loading": "Загрузка заметок…",
|
||||
"mappingTitle": "Картирование вашего знания…",
|
||||
@@ -3800,7 +3818,7 @@
|
||||
"apiKey": "API-ключ",
|
||||
"apiUrl": "URL API",
|
||||
"baseUrlRequired": "Укажите URL API",
|
||||
"byokActive": "BYOK активен",
|
||||
"byokActive": "Ключи активны",
|
||||
"choose": "Выбрать…",
|
||||
"chooseModel": "Выберите модель…",
|
||||
"chooseProvider": "Выберите провайдера…",
|
||||
@@ -4011,7 +4029,7 @@
|
||||
"tabProgress": "Прогресс",
|
||||
"tapToFlip": "Пробел или нажмите, чтобы перевернуть",
|
||||
"toolbarGenerate": "Создать карточки",
|
||||
"toolbarGenerateHint": "Интервальное повторение SM-2",
|
||||
"toolbarGenerateHint": "Они возвращаются в нужный момент",
|
||||
"totalCardsLabel": "Всего карточек",
|
||||
"totalReviewsLabel": "Всего повторений",
|
||||
"upToDate": "Актуально",
|
||||
@@ -4055,6 +4073,9 @@
|
||||
"homeDashboard": {
|
||||
"activityEmptyHint": "Редактируйте заметки, чтобы увидеть ваш ритм письма за последние 90 дней.",
|
||||
"agentCreated": "Агент создан и запущен",
|
||||
"agentCreatedInCard": "Агент готов.",
|
||||
"agentOpenCreated": "Открыть агента",
|
||||
"agentSeeNextSuggestion": "Смотреть следующий",
|
||||
"agentDiscovery": "Агент",
|
||||
"agentFailed": "Ошибка создания",
|
||||
"agentsEmpty": "Пока не предложено исследовательских агентов. Продолжайте писать — Memento предложит темы для синтеза, когда заметки сгруппируются.",
|
||||
@@ -4063,6 +4084,8 @@
|
||||
"aiFound": "ИИ нашёл",
|
||||
"aiProviderUnavailable": "ИИ временно недоступен. Проверьте настройки провайдера.",
|
||||
"allCaughtUp": "Всё повторено.",
|
||||
"remindersEmpty": "На сегодня нет напоминаний.",
|
||||
"remindersOpenAll": "Смотреть напоминания",
|
||||
"alreadySeen": "просмотрено",
|
||||
"analyzeNotes": "Анализировать мои заметки",
|
||||
"analyzing": "Анализ…",
|
||||
@@ -4180,6 +4203,8 @@
|
||||
"pulseReview": "{count} на повторение",
|
||||
"quickCapture": "Быстрый захват",
|
||||
"quickCapturePlaceholder": "Идея, мысль… Нажмите Enter, чтобы отправить во входящие.",
|
||||
"captureGoesToFile": "Попадает во входящие.",
|
||||
"captureSend": "Отправить во входящие",
|
||||
"reminders": "Напоминания",
|
||||
"resumeAlso": "Также недавно",
|
||||
"resumeEmptyCta": "Записать идею",
|
||||
@@ -4192,7 +4217,7 @@
|
||||
"suggestedBridge": "Связать {clusterA} & {clusterB}",
|
||||
"suggestedResearch": "Предлагаемое исследование",
|
||||
"theme": "Тема",
|
||||
"themes": "темы",
|
||||
"themes": "Темы",
|
||||
"title": "Панель управления",
|
||||
"toOrganize": "для организации",
|
||||
"toReview": "На повторение",
|
||||
@@ -4267,6 +4292,7 @@
|
||||
"widgetHide": "Скрыть виджет",
|
||||
"widgetOpen": "Открыть",
|
||||
"widgetPinnedEmpty": "Пока нет закреплённых заметок.",
|
||||
"pinnedNoNotebook": "Без блокнота",
|
||||
"widgetReset": "Сбросить",
|
||||
"widgetResetDone": "Панель по умолчанию восстановлена.",
|
||||
"widgetStatsBridges": "Мосты",
|
||||
|
||||
@@ -31,14 +31,14 @@
|
||||
"confirmPasswordPlaceholder": "再次输入密码",
|
||||
"backToSite": "返回",
|
||||
"continueWithGoogle": "使用Google继续",
|
||||
"createYourSpace": "创建你的空间",
|
||||
"createYourSpaceSubtitle": "加入智能笔记的新时代。",
|
||||
"createYourSpace": "创建你的第二大脑",
|
||||
"createYourSpaceSubtitle": "你一边写,它一边建立联系——不是又一个笔记应用。",
|
||||
"forgot": "忘记?",
|
||||
"oauthAccountNotLinked": "此 Google 帐号与您现有的帐号不匹配。请使用相同的邮箱或用密码登录。",
|
||||
"privacyTerms": "© 2025 Memento Labs — 隐私 · 条款",
|
||||
"sessionExpired": "您的网站将包含导航和目录地生成",
|
||||
"welcomeBack": "欢迎回来",
|
||||
"welcomeBackSubtitle": "输入你的凭据以访问笔记。",
|
||||
"welcomeBackSubtitle": "登录以找回你的笔记、关联,以及你已经忘记的内容。",
|
||||
"checkEmailTitle": "请查收邮件",
|
||||
"checkEmailDescription": "我们已向 {email} 发送确认链接。请打开链接以激活账户后再登录。",
|
||||
"checkEmailDescriptionGeneric": "我们已向您的邮箱发送确认链接。请打开链接以激活账户后再登录。",
|
||||
@@ -99,13 +99,13 @@
|
||||
"darkMode": "深色模式",
|
||||
"dashboardPanelBody": "您的会话已过期。请重新登录。",
|
||||
"documents": "文档",
|
||||
"insightsPanelBody": "你笔记的语义地图:主题聚类、桥接笔记和连接建议。",
|
||||
"insightsPanelBody": "笔记如何相连的地图:相近主题、桥接笔记,以及可以打开的链接。",
|
||||
"lightMode": "亮色模式",
|
||||
"notebookEmpty": "空",
|
||||
"recentNote": "最近创建",
|
||||
"resizeNotebooksPanel": "调整笔记本面板大小",
|
||||
"resizeSidebar": "调整侧边栏宽度",
|
||||
"revisionPanelBody": "使用 SM-2 算法复习闪卡。牌组从你的笔记中生成。",
|
||||
"revisionPanelBody": "用闪卡复习。间隔重复会在合适的时候把它们带回。牌组来自你的笔记。",
|
||||
"searchNotebooksPlaceholder": "搜索笔记本…",
|
||||
"searchShortcut": "搜索 (Ctrl+K)"
|
||||
},
|
||||
@@ -130,7 +130,7 @@
|
||||
"add": "添加",
|
||||
"adding": "添加中...",
|
||||
"close": "关闭",
|
||||
"confirmDelete": "确定要删除此笔记吗?",
|
||||
"confirmDelete": "此笔记将移至回收站。之后可以恢复。",
|
||||
"confirmLeaveShare": "确定要离开这条共享笔记吗?",
|
||||
"sharedBy": "共享者",
|
||||
"sharedShort": "共享",
|
||||
@@ -991,7 +991,7 @@
|
||||
"dailyNotes": "每日笔记",
|
||||
"dashboard": "仪表盘",
|
||||
"graphView": "链接地图",
|
||||
"insights": "语义主题",
|
||||
"insights": "关联",
|
||||
"revision": "复习"
|
||||
},
|
||||
"settings": {
|
||||
@@ -2095,14 +2095,14 @@
|
||||
"collapse": "收起"
|
||||
},
|
||||
"mcpSettings": {
|
||||
"title": "MCP",
|
||||
"title": "外部工具",
|
||||
"description": "管理 API 密钥并配置外部工具",
|
||||
"tierRequired": "仅限 Pro+",
|
||||
"upgradeHint": "MCP 访问权限(Cursor、Claude Desktop 等 API 密钥)需要 Pro 或更高版本的计划。请在计费页面升级以解锁此功能。",
|
||||
"whatIsMcp": {
|
||||
"title": "什么是 MCP?",
|
||||
"title": "这有什么用?",
|
||||
"description": "模型上下文协议(MCP)是一个开放协议,使 AI 模型能够与外部工具和数据源安全交互。通过 MCP,您可以将 Claude Code、Cursor 或 N8N 等工具连接到您的 Memento 实例,以编程方式读取、创建和整理笔记。",
|
||||
"learnMore": "了解更多关于 MCP"
|
||||
"learnMore": "了解更多"
|
||||
},
|
||||
"serverStatus": {
|
||||
"title": "服务器状态",
|
||||
@@ -2161,12 +2161,12 @@
|
||||
}
|
||||
},
|
||||
"helpBox": {
|
||||
"title": "什么是 MCP(模型上下文协议)?",
|
||||
"title": "如何连接工具?",
|
||||
"step1": "MCP 是一种协议,允许 Memento 的 AI 代理连接到外部工具(数据库、API、文件等)。",
|
||||
"step2": "Memento 提供了一个包含 22 个工具的 MCP 服务器 — 您的代理可以读取/创建笔记、搜索您的库、管理笔记本等。",
|
||||
"step3": "在此创建 API 密钥,然后在您的 MCP 客户端(Claude Desktop、Cursor、Continue.dev…)中配置该密钥和服务器 URL。",
|
||||
"step4": "配置格式:MCP 服务器 URL + 您的密钥放在 Authorization 请求头中。",
|
||||
"step4Link": "MCP 文档",
|
||||
"step4Link": "官方帮助",
|
||||
"step5": "使用示例:让 Claude Desktop 在 Memento 中写一条笔记、搜索您的笔记本或创建一个代理。"
|
||||
}
|
||||
},
|
||||
@@ -2334,6 +2334,17 @@
|
||||
"title": "模板",
|
||||
"install": "安装",
|
||||
"installing": "安装中...",
|
||||
"seeAll": "查看全部",
|
||||
"showLess": "收起",
|
||||
"categoryAll": "全部",
|
||||
"categoryWatch": "资讯跟踪",
|
||||
"categoryDigest": "摘要",
|
||||
"categoryTools": "工具",
|
||||
"categoryGenerate": "创建",
|
||||
"taskExtractor": {
|
||||
"name": "笔记中的任务",
|
||||
"description": "从笔记中找出任务并集中到一处。"
|
||||
},
|
||||
"veilleAI": {
|
||||
"name": "AI 观察",
|
||||
"description": "抓取 5 个 AI 专业网站并生成每周摘要。"
|
||||
@@ -2453,7 +2464,7 @@
|
||||
"slideStyle": "视觉风格影响角半径、间距和信息密度。"
|
||||
}
|
||||
},
|
||||
"intelligenceOS": "智能操作系统"
|
||||
"intelligenceOS": "智能体"
|
||||
},
|
||||
"chat": {
|
||||
"title": "AI 聊天",
|
||||
@@ -2740,7 +2751,7 @@
|
||||
"slashDatabaseDesc": "嵌入你笔记本的结构化数据",
|
||||
"slashLinkPreview": "链接预览",
|
||||
"slashLinkPreviewDesc": "将 URL 转换为可视化卡片",
|
||||
"slashLivingBlock": "动态块",
|
||||
"slashLivingBlock": "关联块",
|
||||
"slashLivingBlockDesc": "从其他笔记插入",
|
||||
"slashMath": "方程",
|
||||
"slashMathDesc": "LaTeX 表示法的数学公式",
|
||||
@@ -3007,7 +3018,7 @@
|
||||
"proChat": "100 chat messages / month",
|
||||
"later": "稍后",
|
||||
"upgradePricing": "升级到Pro",
|
||||
"addApiKey": "使用您自己的API密钥 (BYOK)",
|
||||
"addApiKey": "使用你自己的密钥",
|
||||
"featureBrainstormCreate": "Créations brainstorm",
|
||||
"featureBrainstormEnrich": "Enrichissements brainstorm",
|
||||
"featureBrainstormExpand": "Extensions brainstorm",
|
||||
@@ -3025,10 +3036,10 @@
|
||||
"outOfCredits": "积分用完 — 选项"
|
||||
},
|
||||
"byokSettings": {
|
||||
"title": "您的API密钥 (BYOK)",
|
||||
"title": "你的服务商密钥",
|
||||
"description": "Connect your own LLM provider keys to bypass Discovery Pack quotas. Keys are encrypted at rest.",
|
||||
"badgeActive": "BYOK 已激活",
|
||||
"tierRequired": "BYOK需要Pro计划或更高。升级以连接您的API密钥。",
|
||||
"badgeActive": "密钥已启用",
|
||||
"tierRequired": "此选项需要 Pro 或更高套餐。",
|
||||
"provider": "提供商",
|
||||
"providerPlaceholder": "选择提供商",
|
||||
"alias": "标签(可选)",
|
||||
@@ -3167,6 +3178,9 @@
|
||||
"fetchInvoicesFailed": "无法加载计费历史记录。",
|
||||
"savePercent": "节省 ~17%",
|
||||
"cancelSubscription": "取消订阅",
|
||||
"changeOffer": "更换套餐",
|
||||
"downgradeToFree": "回到免费套餐",
|
||||
"cancellingNotice": "已安排取消 — 可用至 {date}",
|
||||
"disabledByAdmin": "账单和升级目前已禁用。如需访问,请联系管理员。",
|
||||
"tab": "账单",
|
||||
"creditsFromPacks": "套餐积分",
|
||||
@@ -3284,34 +3298,37 @@
|
||||
"title": "把繁重工作交出去。",
|
||||
"desc": "研究、抓取、幻灯片、图表、监控——写入你第二大脑的智能体。",
|
||||
"scraper": {
|
||||
"title": "Scraper",
|
||||
"title": "监控器",
|
||||
"desc": "URL 与 RSS → 带智能配图的合成笔记。"
|
||||
},
|
||||
"researcher": {
|
||||
"title": "Researcher",
|
||||
"title": "研究员",
|
||||
"desc": "深度查询、来源、结构化研究笔记。"
|
||||
},
|
||||
"slideGen": {
|
||||
"title": "Slide Gen",
|
||||
"title": "幻灯片",
|
||||
"desc": "笔记 → 演示文稿或交互式 HTML 幻灯片。"
|
||||
},
|
||||
"monitor": {
|
||||
"title": "Monitor",
|
||||
"title": "观察者",
|
||||
"desc": "监控笔记本:趋势与洞察。"
|
||||
},
|
||||
"diagramGen": {
|
||||
"title": "Diagram Gen",
|
||||
"title": "图表",
|
||||
"desc": "想法 → Excalidraw 思维导图与流程。"
|
||||
},
|
||||
"custom": {
|
||||
"title": "Custom",
|
||||
"title": "自定义",
|
||||
"desc": "你的角色、来源与计划。"
|
||||
}
|
||||
},
|
||||
"byok": {
|
||||
"label": "无锁定",
|
||||
"title": "你的密钥。你的模型。你的第二大脑。",
|
||||
"desc": "使用 Memento 额度,或接入 OpenAI、Anthropic、Google……一键切换供应商。"
|
||||
"desc": "用 Memento 额度,或接入你自己的服务商。一键切换——产品仍是你的。",
|
||||
"pointCredits": "想简单的话,用 Memento 额度即可",
|
||||
"pointProvider": "也可以连接你自己的服务商",
|
||||
"pointYours": "密钥留在你这边——这里绝不会显示"
|
||||
},
|
||||
"pricing": {
|
||||
"label": "Pricing",
|
||||
@@ -3339,7 +3356,7 @@
|
||||
"desc": "给认真思考的人。",
|
||||
"cta": "升级 Pro",
|
||||
"feature0": "无限笔记",
|
||||
"feature1": "BYOK",
|
||||
"feature1": "使用你自己的密钥",
|
||||
"feature2": "每月 200 次语义搜索",
|
||||
"feature3": "智能体(12 次/月)",
|
||||
"feature4": "30 天历史",
|
||||
@@ -3350,11 +3367,11 @@
|
||||
"desc": "团队第二大脑。",
|
||||
"cta": "选择 Business",
|
||||
"feature0": "含 10 名协作者",
|
||||
"feature1": "BYOK · 13 家供应商",
|
||||
"feature1": "你的密钥 · {count} 家服务商",
|
||||
"feature2": "1000 次语义搜索",
|
||||
"feature3": "智能体(60 次/月)",
|
||||
"feature4": "无限头脑风暴",
|
||||
"feature5": "API / MCP"
|
||||
"feature5": "外部工具"
|
||||
},
|
||||
"enterprise": {
|
||||
"name": "Enterprise",
|
||||
@@ -3545,12 +3562,12 @@
|
||||
"feature_search_title": "语义搜索",
|
||||
"feature_search_desc": "按含义查找任何笔记,而不仅仅是关键词。",
|
||||
"feature_flashcards_title": "AI 闪卡",
|
||||
"feature_flashcards_desc": "一键从笔记生成 SRS 复习卡片。",
|
||||
"feature_flashcards_desc": "一键从笔记生成复习卡片。",
|
||||
"feature_brainstorm_title": "AI 头脑风暴",
|
||||
"feature_brainstorm_desc": "AI 驱动的协作头脑风暴会话。",
|
||||
"feature_chat_title": "与笔记对话",
|
||||
"feature_chat_desc": "向您的个人知识库提问。",
|
||||
"feature_insights_title": "语义洞察",
|
||||
"feature_insights_title": "关联",
|
||||
"feature_insights_desc": "发现您想法之间隐藏的联系。",
|
||||
"feature_export_title": "Markdown 导出",
|
||||
"feature_export_desc": "以标准 Markdown 格式导入和导出笔记。",
|
||||
@@ -3638,9 +3655,10 @@
|
||||
"createDiagramCostHint": "≈ 4 AI 积分"
|
||||
},
|
||||
"insightsView": {
|
||||
"title": "语义洞察",
|
||||
"title": "关联",
|
||||
"toggleMenu": "显示或隐藏菜单",
|
||||
"subtitle": "发现知识背后的隐藏架构",
|
||||
"resync": "重新同步网络",
|
||||
"resync": "更新",
|
||||
"mapping": "映射中…",
|
||||
"loading": "加载笔记中…",
|
||||
"mappingTitle": "正在映射您的知识…",
|
||||
@@ -3800,7 +3818,7 @@
|
||||
"apiKey": "API 密钥",
|
||||
"apiUrl": "API地址",
|
||||
"baseUrlRequired": "请提供 API URL",
|
||||
"byokActive": "BYOK已激活",
|
||||
"byokActive": "密钥已启用",
|
||||
"choose": "选择…",
|
||||
"chooseModel": "选择模型…",
|
||||
"chooseProvider": "选择提供商…",
|
||||
@@ -4011,7 +4029,7 @@
|
||||
"tabProgress": "进度",
|
||||
"tapToFlip": "空格或点击翻转",
|
||||
"toolbarGenerate": "生成闪卡",
|
||||
"toolbarGenerateHint": "SM-2 间隔重复",
|
||||
"toolbarGenerateHint": "它们会在合适的时候回来",
|
||||
"totalCardsLabel": "总卡片数",
|
||||
"totalReviewsLabel": "总复习数",
|
||||
"upToDate": "已更新",
|
||||
@@ -4055,6 +4073,9 @@
|
||||
"homeDashboard": {
|
||||
"activityEmptyHint": "编辑笔记以查看你过去 90 天的写作节奏。",
|
||||
"agentCreated": "代理已创建并启动",
|
||||
"agentCreatedInCard": "代理已准备好。",
|
||||
"agentOpenCreated": "打开代理",
|
||||
"agentSeeNextSuggestion": "查看下一个",
|
||||
"agentDiscovery": "代理",
|
||||
"agentFailed": "创建失败",
|
||||
"agentsEmpty": "还没有建议研究代理。继续写——当你的笔记聚类时,Memento 会提出综合主题。",
|
||||
@@ -4063,6 +4084,8 @@
|
||||
"aiFound": "AI找到",
|
||||
"aiProviderUnavailable": "AI暂时不可用。请检查您的提供商设置。",
|
||||
"allCaughtUp": "全部完成。",
|
||||
"remindersEmpty": "今天没有提醒。",
|
||||
"remindersOpenAll": "查看提醒",
|
||||
"alreadySeen": "已查看",
|
||||
"analyzeNotes": "分析我的笔记",
|
||||
"analyzing": "正在分析……",
|
||||
@@ -4180,6 +4203,8 @@
|
||||
"pulseReview": "{count} 待复习",
|
||||
"quickCapture": "快速捕获",
|
||||
"quickCapturePlaceholder": "一个想法、一个念头……按回车捕获到收件箱。",
|
||||
"captureGoesToFile": "会进入收件箱。",
|
||||
"captureSend": "发送到收件箱",
|
||||
"reminders": "提醒",
|
||||
"resumeAlso": "最近还有",
|
||||
"resumeEmptyCta": "捕获想法",
|
||||
@@ -4267,6 +4292,7 @@
|
||||
"widgetHide": "隐藏小组件",
|
||||
"widgetOpen": "打开",
|
||||
"widgetPinnedEmpty": "还没有置顶笔记。",
|
||||
"pinnedNoNotebook": "未放入笔记本",
|
||||
"widgetReset": "重置",
|
||||
"widgetResetDone": "默认仪表板已恢复。",
|
||||
"widgetStatsBridges": "桥接",
|
||||
|
||||
Reference in New Issue
Block a user