feat: brainstorm sessions, PDF document Q&A, embedding fixes, and UI improvements
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 7s

- Add brainstorm feature with collaborative canvas, AI idea generation, live cursors, playback, and export
- Add PDF upload/extraction/ingestion pipeline with pgvector document search (RAG)
- Add document Q&A overlay with streaming chat and PDF preview
- Add note attachments UI with status polling, grid layout, and auto-scroll
- Add task extraction AI tool and agent executor improvements
- Fix NoteEmbedding missing updatedAt column, re-index 66 notes with 1536-dim embeddings
- Fix brainstorm 'Create Note' button: add success toast and redirect to created note
- Fix memory echo notification infinite polling
- Fix chat route to always include document_search tool
- Add brainstorm i18n keys across all 14 locales
- Add socket server for real-time brainstorm collaboration
- Add hierarchical notebook selector and organize notebook dialog improvements
- Add sidebar brainstorm section with session management
- Update prisma schema with brainstorm tables, attachments, and document chunks
This commit is contained in:
Antigravity
2026-05-14 17:43:21 +00:00
parent 195e845f0a
commit 1fcea6ed7d
228 changed files with 57656 additions and 1059 deletions

View File

@@ -47,6 +47,9 @@ next-env.d.ts
/prisma/client-generated
/_backup
# i18n sync helper (optional local venv)
/.venv-i18n/
# Service worker (generated at build time if PWA is re-enabled)
public/sw.js
public/sw.js.map

View File

@@ -0,0 +1,175 @@
# Brainstorm Canvas — Feature Spec for OpenCode
## PROJECT CONTEXT
- **Stack**: Next.js 15 + Prisma + PostgreSQL (pgvector)
- **Location**: ~/dev/Momento/memento-note/
- **Existing models**: User, Note, Notebook, Canvas (Excalidraw), Agent, Conversation, Workflow, etc.
- **Existing API routes**: /app/api/notes, /notebooks, /ai, /canvas, /chat, /agents, etc.
- **UI lib**: Radix UI + Tailwind CSS + shadcn components
- **State**: @tanstack/react-query
- **Auth**: NextAuth (auth.ts, auth.config.ts)
## ⚠️ CRITICAL RULES
1. **DO NOT modify existing models or API routes** — only ADD new ones
2. **DO NOT touch prisma/schema.prisma without creating a migration** — use `npx prisma migrate dev --name add_brainstorm`
3. **Study the existing code patterns** before writing new code — match the project conventions
4. **Look at how existing API routes are structured** (e.g., /app/api/notes/route.ts) and follow the same pattern
5. **Look at how existing components use React Query** and follow the same pattern
6. **Use existing UI components** from /components/ui/ (shadcn) — don't install new UI libs
7. **The project uses TypeScript** — maintain strict typing
8. **Do NOT run npm build** — only dev server if needed for testing
## FEATURE: Brainstorm Canvas (Wave Brainstorming)
### Concept
A temporary workspace for brainstorming ideas using AI-generated "waves" of ideas, displayed as a radial graph. The output feeds back into the notes system.
### Data Model — ADD these 2 models to prisma/schema.prisma:
```prisma
model BrainstormSession {
id String @id @default(cuid())
seedIdea String
sourceNoteId String?
contextNoteIds String? // JSON array of note IDs
exportedNoteId String?
userId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
ideas BrainstormIdea[]
sourceNote Note? @relation(fields: [sourceNoteId], references: [id])
exportedNote Note? @relation(fields: [exportedNoteId], references: [id])
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
@@index([userId, createdAt])
}
model BrainstormIdea {
id String @id @default(cuid())
sessionId String
waveNumber Int // 1, 2, or 3
title String
description String
connectionToSeed String?
noveltyScore Int?
parentIdeaId String? // for "dig deeper" sub-brainstorms
convertedToNoteId String?
relatedNoteIds String? // JSON array
status String @default("active") // active, dismissed, converted
positionX Float?
positionY Float?
createdAt DateTime @default(now())
session BrainstormSession @relation(fields: [sessionId], references: [id], onDelete: Cascade)
parentIdea BrainstormIdea? @relation("IdeaTree", fields: [parentIdeaId], references: [id])
children BrainstormIdea[] @relation("IdeaTree")
convertedNote Note? @relation(fields: [convertedToNoteId], references: [id])
@@index([sessionId])
@@index([waveNumber])
@@index([status])
@@index([parentIdeaId])
}
```
Also add relations to User model:
```
brainstormSessions BrainstormSession[]
```
And to Note model — add these two relations:
```
sourceBrainstormSessions BrainstormSession[] @relation via sourceNoteId
exportedBrainstormSessions BrainstormSession[] @relation via exportedNoteId
convertedBrainstormIdeas BrainstormIdea[] @relation via convertedToNoteId
```
### API Routes — Create /app/api/brainstorm/
1. **POST /api/brainstorm/wave** — Create new brainstorm session
- Input: { seedIdea, sourceNoteId?, contextNoteIds? }
- Uses existing AI setup (check how /api/ai/ routes call LLM)
- Generates 3 waves of ~3 ideas each (9 total)
- For each idea, does an embedding search to find related notes (check how semantic search works in existing code)
- Saves session + ideas to DB
- Returns: { sessionId, ideas: [...] }
2. **POST /api/brainstorm/[sessionId]/expand** — Dig deeper on an idea
- Input: { ideaId }
- Uses the clicked idea as new seed
- Generates 3 more waves, linked as children
- Returns new ideas with parentIdeaId set
3. **POST /api/brainstorm/[sessionId]/dismiss** — Mark idea as not relevant
- Input: { ideaId }
- Sets status = "dismissed"
4. **POST /api/brainstorm/[sessionId]/convert** — Convert idea to a real Note
- Input: { ideaId }
- Creates a Note with pre-filled content
- Auto-tags: "brainstorm", "idée"
- Links to source note if exists
- Sets idea.status = "converted", idea.convertedToNoteId = newNote.id
- Returns the created note
5. **POST /api/brainstorm/[sessionId]/export** — Export session as summary note
- Generates a Markdown summary note
- Groups by waves, shows which ideas were converted
- Links to all converted notes
- Returns the created note
6. **GET /api/brainstorm** — List user's brainstorm sessions
- Returns sessions ordered by date, with idea counts
7. **GET /api/brainstorm/[sessionId]** — Get full session with ideas
- Returns session + all ideas + their status
### Frontend — Canvas Component
Use **react-force-graph-2d** (install it: `npm install react-force-graph-2d`).
It's a React wrapper around d3-force — declarative API, same physics engine.
**Layout:**
- Center node = seed idea (large, white)
- Ring 1 (radius ~150px) = Wave 1 ideas (orange)
- Ring 2 (radius ~300px) = Wave 2 ideas (blue)
- Ring 3 (radius ~450px) = Wave 3 ideas (purple)
- Use d3.forceRadial for ring constraint
- Dismissed nodes = opacity 0.3, smaller
- Converted nodes = green border + icon
- Click node = side panel with details + 3 action buttons
**Side Panel (when clicking a node):**
- Show: title, description, connection to seed, related notes
- 3 buttons: Dig Deeper, Create Note, Dismiss
- Uses existing shadcn Sheet or Dialog component
**Sidebar Integration:**
- Add "Brainstorms" section in the existing sidebar
- List sessions with preview (seed idea + count)
- Click to reopen saved canvas state
**Entry Points:**
1. From a note: Brainstorm button in note toolbar/editor
2. From sidebar: "+ New Brainstorm" button
### AI Prompt for Wave Generation
The LLM prompt should generate 3 waves:
- **Wave 1 — Variations**: Direct variations/expansions of the seed (sous-aspects, reformulations, variations)
- **Wave 2 — Analogies**: Cross-domain analogies (autres domaines, biologie, technologie, etc.)
- **Wave 3 — Disruptions**: Inversions, provocations, ideas that challenge assumptions
Each idea should have:
- title (short)
- description (1-2 sentences)
- connectionToSeed (how it relates to the seed)
- noveltyScore (1-10)
### Implementation Order
1. Prisma schema + migration
2. API routes (start with /wave and /convert)
3. Brainstorm canvas component (react-force-graph-2d)
4. Sidebar integration
5. Note toolbar button
6. Export functionality

View File

@@ -51,7 +51,7 @@ function ResetPasswordForm() {
<CardDescription>{t('resetPassword.invalidLinkDescription')}</CardDescription>
</CardHeader>
<CardFooter>
<Link href="/forgot-password" title="Try again" className="w-full">
<Link href="/forgot-password" title={t('resetPassword.requestNewLink')} className="w-full">
<Button variant="outline" className="w-full">{t('resetPassword.requestNewLink')}</Button>
</Link>
</CardFooter>

View File

@@ -0,0 +1,14 @@
import { Suspense } from 'react'
import { BrainstormPage } from '@/components/brainstorm/brainstorm-page'
export default function BrainstormRoute() {
return (
<Suspense fallback={
<div className="flex items-center justify-center h-full">
<div className="animate-spin h-8 w-8 border-2 border-foreground/20 border-t-foreground rounded-full" />
</div>
}>
<BrainstormPage />
</Suspense>
)
}

View File

@@ -0,0 +1,198 @@
'use server'
import { revalidatePath } from 'next/cache'
import prisma from '@/lib/prisma'
import { auth } from '@/auth'
export async function createBrainstormShare(
sessionId: string,
recipientEmail: string,
permission: 'editor' | 'viewer' = 'editor'
) {
const session = await auth()
if (!session?.user?.id) throw new Error('Unauthorized')
const brainstorm = await prisma.brainstormSession.findUnique({
where: { id: sessionId },
})
if (!brainstorm) throw new Error('Session not found')
if (brainstorm.userId !== session.user.id)
throw new Error('Only the owner can share')
const recipient = await prisma.user.findUnique({
where: { email: recipientEmail },
})
if (!recipient) throw new Error('No account found with this email')
if (recipient.id === session.user.id)
throw new Error('Cannot share with yourself')
const existing = await prisma.brainstormShare.findUnique({
where: { sessionId_userId: { sessionId, userId: recipient.id } },
})
if (existing) {
switch (existing.status) {
case 'accepted':
return { success: true, message: 'already_shared' }
case 'pending':
return { success: true, message: 'already_pending' }
case 'declined':
case 'removed':
await prisma.brainstormShare.update({
where: { id: existing.id },
data: {
status: 'pending',
permission,
notifiedAt: new Date(),
respondedAt: null,
},
})
return { success: true, message: 're_invited' }
}
}
await prisma.brainstormShare.create({
data: {
sessionId,
userId: recipient.id,
sharedBy: session.user.id,
status: 'pending',
permission,
notifiedAt: new Date(),
},
})
return { success: true, message: 'invited' }
}
export async function getPendingBrainstormShares() {
const session = await auth()
if (!session?.user?.id) return []
try {
return await prisma.brainstormShare.findMany({
where: { userId: session.user.id, status: 'pending' },
include: {
session: {
select: { id: true, seedIdea: true, createdAt: true },
},
sharer: {
select: { id: true, name: true, email: true, image: true },
},
},
orderBy: { createdAt: 'desc' },
})
} catch {
return []
}
}
export async function respondToBrainstormShare(
shareId: string,
action: 'accept' | 'decline'
) {
const session = await auth()
if (!session?.user?.id) throw new Error('Unauthorized')
const share = await prisma.brainstormShare.findUnique({
where: { id: shareId },
})
if (!share) throw new Error('Share not found')
if (share.userId !== session.user.id) throw new Error('Unauthorized')
if (share.status !== 'pending')
throw new Error(`Share already ${share.status}`)
const newStatus = action === 'accept' ? 'accepted' : 'declined'
await prisma.brainstormShare.update({
where: { id: shareId },
data: { status: newStatus, respondedAt: new Date() },
})
if (action === 'accept') {
await prisma.brainstormParticipant.upsert({
where: {
sessionId_userId: {
sessionId: share.sessionId,
userId: session.user.id,
},
},
create: {
sessionId: share.sessionId,
userId: session.user.id,
role: share.permission === 'viewer' ? 'viewer' : 'editor',
},
update: {
role: share.permission === 'viewer' ? 'viewer' : 'editor',
},
})
}
revalidatePath('/')
return { success: true }
}
export async function ensureAcceptedSharesHaveParticipants() {
const session = await auth()
if (!session?.user?.id) return
const acceptedShares = await prisma.brainstormShare.findMany({
where: { userId: session.user.id, status: 'accepted' },
select: { sessionId: true, permission: true },
})
for (const share of acceptedShares) {
await prisma.brainstormParticipant.upsert({
where: {
sessionId_userId: {
sessionId: share.sessionId,
userId: session.user.id,
},
},
create: {
sessionId: share.sessionId,
userId: session.user.id,
role: share.permission === 'viewer' ? 'viewer' : 'editor',
},
update: {},
})
}
}
export async function getAcceptedBrainstormShares() {
const session = await auth()
if (!session?.user?.id) return []
try {
return await prisma.brainstormShare.findMany({
where: { userId: session.user.id, status: 'accepted' },
include: {
session: {
select: {
id: true,
seedIdea: true,
createdAt: true,
updatedAt: true,
ideas: { where: { status: 'active' }, select: { id: true } },
},
},
},
orderBy: { session: { updatedAt: 'desc' } },
})
} catch {
return []
}
}
export async function removeBrainstormShare(sessionId: string) {
const session = await auth()
if (!session?.user?.id) throw new Error('Unauthorized')
await prisma.brainstormShare.updateMany({
where: { sessionId, userId: session.user.id, status: 'accepted' },
data: { status: 'removed' },
})
revalidatePath('/')
return { success: true }
}

View File

@@ -245,7 +245,7 @@ export async function executeNotebookOrganization(plan: OrganizationPlan): Promi
data: {
name: group.name.trim(),
icon: '📁',
color: '#75B2D6',
color: '#A47148',
order: nextOrder,
parentId: plan.notebookId, // always a sub-notebook
userId: session.user.id,

View File

@@ -0,0 +1,46 @@
import { NextRequest, NextResponse } from 'next/server'
import prisma from '@/lib/prisma'
import { auth } from '@/auth'
import { verifyParticipant } from '@/lib/brainstorm-collab'
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ sessionId: string }> }
) {
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
try {
const { sessionId } = await params
const { isParticipant } = await verifyParticipant(sessionId, session.user.id)
if (!isParticipant) {
return NextResponse.json({ error: 'Not a participant' }, { status: 403 })
}
const activities = await prisma.brainstormActivity.findMany({
where: { sessionId },
orderBy: { createdAt: 'desc' },
take: 50,
include: {
user: { select: { id: true, name: true, image: true } },
},
})
return NextResponse.json({
success: true,
data: activities.map(a => ({
id: a.id,
action: a.action,
details: a.details ? JSON.parse(a.details) : null,
createdAt: a.createdAt,
user: a.user ? { name: a.user.name, image: a.user.image } : null,
})),
})
} catch (error) {
console.error('Error fetching activity:', error)
return NextResponse.json({ error: 'Failed to fetch activity' }, { status: 500 })
}
}

View File

@@ -0,0 +1,167 @@
import { NextRequest, NextResponse } from 'next/server'
import prisma from '@/lib/prisma'
import { auth } from '@/auth'
import { z } from 'zod'
const convertSchema = z.object({
ideaId: z.string().min(1),
})
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ sessionId: string }> }
) {
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
try {
const { sessionId } = await params
const body = await request.json()
const { ideaId } = convertSchema.parse(body)
const brainstormSession = await prisma.brainstormSession.findFirst({
where: {
id: sessionId,
OR: [
{ userId: session.user.id },
{ participants: { some: { userId: session.user.id } } },
],
},
})
if (!brainstormSession) {
return NextResponse.json({ error: 'Session not found' }, { status: 404 })
}
const idea = await prisma.brainstormIdea.findFirst({
where: { id: ideaId, sessionId },
include: { noteRefs: { include: { note: true } } },
})
if (!idea) {
return NextResponse.json({ error: 'Idea not found' }, { status: 404 })
}
if (idea.status === 'converted') {
return NextResponse.json({ error: 'Already converted' }, { status: 400 })
}
let sourceSection = ''
let targetNotebookId: string | null = null
if (brainstormSession.exportedNoteId) {
const exportedNote = await prisma.note.findUnique({
where: { id: brainstormSession.exportedNoteId },
select: { notebookId: true },
})
if (exportedNote?.notebookId) {
targetNotebookId = exportedNote.notebookId
}
}
if (!targetNotebookId && brainstormSession.sourceNoteId) {
const sourceNote = await prisma.note.findUnique({
where: { id: brainstormSession.sourceNoteId },
select: { title: true, id: true, notebookId: true },
})
if (sourceNote) {
sourceSection = `\n\n**Source note**: [${sourceNote.title || 'Untitled'}](note:${sourceNote.id})`
targetNotebookId = sourceNote.notebookId
}
}
if (!targetNotebookId) {
const notebookName = brainstormSession.seedIdea.length > 40
? brainstormSession.seedIdea.substring(0, 40).trim() + '…'
: brainstormSession.seedIdea
let notebook = await prisma.notebook.findFirst({
where: { userId: session.user.id, name: notebookName, trashedAt: null },
})
if (!notebook) {
const notebookCount = await prisma.notebook.count({ where: { userId: session.user.id, trashedAt: null } })
notebook = await prisma.notebook.create({
data: { userId: session.user.id, name: notebookName, order: notebookCount, icon: 'wind' },
})
}
targetNotebookId = notebook.id
}
let originSection = ''
const validRefs = idea.noteRefs.filter(r => r.noteId && r.note)
if (validRefs.length > 0) {
originSection = '\n\n## Origin\n'
for (const ref of validRefs) {
const relLabel = {
derived_from: 'Derived from',
opposes: 'In opposition with',
extends: 'Extends',
synthesizes: 'Synthesizes',
transposes: 'Transposes',
}[ref.relation] || 'Related to'
originSection += `- **${relLabel}** [${ref.note?.title || 'Untitled'}](note:${ref.noteId}): ${ref.explanation}\n`
}
}
const noteContent = `# ${idea.title}\n\n${idea.description}\n\n---\n\n**Connection to seed**: ${idea.connectionToSeed || 'N/A'}\n**Novelty score**: ${idea.noveltyScore || 'N/A'}/10\n\n**Source brainstorm**: "${brainstormSession.seedIdea}"${sourceSection}${originSection}`
const note = await prisma.note.create({
data: {
userId: session.user.id,
title: idea.title,
content: noteContent,
type: 'markdown',
labels: JSON.stringify(['brainstorm', 'idée']),
notebookId: targetNotebookId,
},
})
const tagPromises: Promise<any>[] = []
tagPromises.push(
prisma.brainstormIdea.update({
where: { id: ideaId },
data: {
status: 'converted',
convertedToNoteId: note.id,
},
})
)
tagPromises.push(
prisma.brainstormNoteRef.updateMany({
where: { ideaId },
data: { verdict: 'accepted' },
})
)
for (const ref of validRefs) {
const existingLabels: string[] = (() => {
try { return JSON.parse((ref.note as any)?.labels || '[]') } catch { return [] }
})()
if (!existingLabels.includes('brainstorm-fruitful')) {
existingLabels.push('brainstorm-fruitful')
tagPromises.push(
prisma.note.update({
where: { id: ref.noteId! },
data: { labels: JSON.stringify(existingLabels) },
})
)
}
}
await prisma.$transaction(tagPromises)
return NextResponse.json({ success: true, data: note }, { status: 201 })
} catch (error: any) {
if (error instanceof z.ZodError) {
return NextResponse.json({ error: error.issues }, { status: 400 })
}
console.error('Error converting idea:', error)
return NextResponse.json(
{ error: error.message || 'Failed to convert idea' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,72 @@
import { NextRequest, NextResponse } from 'next/server'
import prisma from '@/lib/prisma'
import { auth } from '@/auth'
import { z } from 'zod'
import { verifyParticipant, logActivity, captureSnapshot } from '@/lib/brainstorm-collab'
const dismissSchema = z.object({
ideaId: z.string().min(1),
})
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ sessionId: string }> }
) {
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
try {
const { sessionId } = await params
const body = await request.json()
const { ideaId } = dismissSchema.parse(body)
const brainstormSession = await prisma.brainstormSession.findFirst({
where: { id: sessionId },
})
if (!brainstormSession) {
return NextResponse.json({ error: 'Session not found' }, { status: 404 })
}
const { isParticipant } = await verifyParticipant(sessionId, session.user.id, 'editor')
if (!isParticipant) {
return NextResponse.json({ error: 'No edit permission' }, { status: 403 })
}
const idea = await prisma.brainstormIdea.findFirst({
where: { id: ideaId, sessionId },
})
if (!idea) {
return NextResponse.json({ error: 'Idea not found' }, { status: 404 })
}
await prisma.$transaction([
prisma.brainstormIdea.update({
where: { id: ideaId },
data: { status: 'dismissed' },
}),
prisma.brainstormNoteRef.updateMany({
where: { ideaId },
data: { verdict: 'dismissed' },
}),
])
await logActivity(sessionId, 'idea_dismissed', session.user.id, { ideaTitle: idea.title })
await captureSnapshot(sessionId, `Dismissed: ${idea.title}`).catch(() => {})
return NextResponse.json({ success: true })
} catch (error: any) {
if (error instanceof z.ZodError) {
return NextResponse.json({ error: error.issues }, { status: 400 })
}
console.error('Error dismissing idea:', error)
return NextResponse.json(
{ error: error.message || 'Failed to dismiss idea' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,323 @@
import { NextRequest, NextResponse } from 'next/server'
import prisma from '@/lib/prisma'
import { auth } from '@/auth'
import { z } from 'zod'
import { getTagsProvider } from '@/lib/ai/factory'
import { getSystemConfig } from '@/lib/config'
import { embeddingService } from '@/lib/ai/services/embedding.service'
import {
verifyParticipant,
resolveAiContextUserId,
sanitizeNotesForGuest,
captureSnapshot,
} from '@/lib/brainstorm-collab'
const expandSchema = z.object({
ideaId: z.string().min(1),
locale: z.string().optional(),
})
interface ParentNoteRef {
noteId: string | null
relation: string
explanation: string
noteTitle?: string | null
noteSnippet?: string
}
// [UPDATE - SÉCURITÉ] allowedNoteIds : null = hôte (accès total), string[] = invité (IDs publics uniquement)
async function getParentContext(
ideaId: string,
hostUserId: string,
allowedNoteIds: string[] | null
): Promise<{ notes: ParentNoteRef[]; noteIds: string[] }> {
const refs = await prisma.brainstormNoteRef.findMany({
where: { ideaId },
include: { note: { select: { id: true, title: true, content: true } } },
})
let noteIds = refs.map(r => r.noteId).filter(Boolean) as string[]
const notes: ParentNoteRef[] = refs.map(r => ({
noteId: r.noteId,
relation: r.relation,
explanation: r.explanation,
noteTitle: r.note?.title || null,
noteSnippet: (r.note?.content || '').slice(0, 200),
}))
// [UPDATE - SÉCURITÉ] Enrichissement vectoriel uniquement pour l'hôte
const isGuest = allowedNoteIds !== null
if (!isGuest && noteIds.length < 3) {
try {
const idea = await prisma.brainstormIdea.findUnique({ where: { id: ideaId } })
if (idea) {
const embedding = await embeddingService.generateEmbedding(`${idea.title} ${idea.description}`)
const vectorStr = embeddingService.toVectorString(embedding.embedding)
const excludeList = noteIds.length > 0
? noteIds.map(id => `'${id}'`).join(',')
: "''"
const extra = await prisma.$queryRawUnsafe(
`SELECT n.id, n.title
FROM "NoteEmbedding" e
JOIN "Note" n ON n.id = e."noteId"
WHERE n."userId" = $1 AND n."trashedAt" IS NULL AND n.id NOT IN (${excludeList})
ORDER BY e.embedding <=> $2::vector
LIMIT 5`,
hostUserId, vectorStr
) as any[]
for (const n of extra) {
if (!noteIds.includes(n.id)) {
noteIds.push(n.id)
notes.push({ noteId: n.id, relation: 'extends', explanation: `Related to your note "${n.title}"`, noteTitle: n.title })
}
}
}
} catch {}
}
// [UPDATE - SÉCURITÉ] Filtrer selon les permissions invité
if (isGuest) {
const allowedSet = new Set(allowedNoteIds)
return {
notes: notes.filter(n => n.noteId === null || allowedSet.has(n.noteId!)),
noteIds: noteIds.filter(id => allowedSet.has(id)),
}
}
return { notes, noteIds }
}
function buildExpandPromptV2(
parentTitle: string,
parentDesc: string,
seedIdea: string,
parentRefs: ParentNoteRef[],
extraNotes: { id: string; title: string; snippet: string }[],
locale?: string
): string {
let notesSection = ''
const allNotes = [
...parentRefs.filter(r => r.noteId).map(r => ({
id: r.noteId!,
title: r.noteTitle || 'Untitled',
snippet: r.noteSnippet || '',
relation: r.relation,
})),
...extraNotes,
]
if (allNotes.length > 0) {
notesSection = `\nUSER'S NOTES (context from parent idea and knowledge base):\n`
notesSection += allNotes.map(n => `- [ID: ${n.id}] "${n.title}": ${n.snippet || 'See note for details'}`).join('\n')
}
return `You are a creative brainstorming assistant. The user wants to DEEPEN a specific idea from a brainstorming session. Generate 3 waves of sub-ideas that CROSS with the user's existing notes.
ORIGINAL SESSION SEED: ${seedIdea}
PARENT IDEA TO EXPAND: ${parentTitle}: ${parentDesc}
${notesSection}
GENERATION RULES:
WAVE 1 — VARIATIONS (3 sub-ideas):
- Direct expansions, details, or implementations of the parent idea
- At least 1 should build on a note referenced above
WAVE 2 — ANALOGIES (3 sub-ideas):
- Cross-domain parallels from other fields
- At least 1 should transpose a pattern from the user's notes
WAVE 3 — DISRUPTIONS (3 sub-ideas):
- Radical inversions or challenges to the parent idea
- At least 1 should synthesize or oppose a note concept
RESPOND ONLY with a valid JSON array of 9 objects:
{
"wave": number (1, 2, or 3),
"title": string (short, 2-6 words),
"description": string (1-2 sentences, specific and actionable),
"connectionToSeed": string (how it relates to the parent idea),
"noveltyScore": number (1-10),
"noteRefs": [
{
"noteId": string (must match an ID provided above, or null),
"relation": "derived_from" | "opposes" | "extends" | "synthesizes" | "transposes",
"explanation": string
}
]
}
CRITICAL: Each idea MUST have at least 1 noteRef when notes are provided.
LANGUAGE: You MUST write ALL titles, descriptions, connectionToSeed, and explanation fields in ${locale === 'fr' ? 'French' : locale === 'es' ? 'Spanish' : locale === 'de' ? 'German' : locale === 'it' ? 'Italian' : locale === 'pt' ? 'Portuguese' : locale === 'nl' ? 'Dutch' : locale === 'ru' ? 'Russian' : locale === 'zh' ? 'Chinese' : locale === 'ja' ? 'Japanese' : locale === 'ko' ? 'Korean' : locale === 'ar' ? 'Arabic' : locale === 'fa' ? 'Farsi' : locale === 'hi' ? 'Hindi' : locale === 'pl' ? 'Polish' : 'the same language as the seed idea'}.`
}
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ sessionId: string }> }
) {
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
try {
const { sessionId } = await params
const body = await request.json()
const { ideaId, locale } = expandSchema.parse(body)
// [UPDATE - SÉCURITÉ] Vérification du rôle participant (couvre hôte + invités éditeurs)
const { isParticipant } = await verifyParticipant(sessionId, session.user.id, 'editor')
if (!isParticipant) {
return NextResponse.json({ error: 'No edit permission' }, { status: 403 })
}
const brainstormSession = await prisma.brainstormSession.findFirst({
where: { id: sessionId },
include: { ideas: true },
})
if (!brainstormSession) {
return NextResponse.json({ error: 'Session not found' }, { status: 404 })
}
const parentIdea = brainstormSession.ideas.find(i => i.id === ideaId)
if (!parentIdea) {
return NextResponse.json({ error: 'Idea not found' }, { status: 404 })
}
// [UPDATE - SÉCURITÉ] Résoudre le périmètre de notes autorisé selon le rôle
const { isGuest, publicNoteIds, aiUserId } = await resolveAiContextUserId(sessionId, session.user.id)
const { notes: parentRefs, noteIds } = await getParentContext(ideaId, aiUserId, isGuest ? (publicNoteIds ?? []) : null)
let extraNotes: { id: string; title: string; snippet: string }[] = []
if (noteIds.length > 0) {
const dbNotes = await prisma.note.findMany({
where: { id: { in: noteIds }, trashedAt: null },
select: { id: true, title: true, content: true },
})
const rawNotes = dbNotes.map(n => ({ id: n.id, title: n.title || 'Untitled', summary: (n.content || '').slice(0, 200) }))
// [UPDATE - SÉCURITÉ] Sanitize le contenu si invité
const sanitized = isGuest ? sanitizeNotesForGuest(rawNotes) : rawNotes
extraNotes = sanitized.map(n => ({ id: n.id, title: n.title, snippet: n.summary }))
}
const config = await getSystemConfig()
const provider = getTagsProvider(config)
const prompt = buildExpandPromptV2(
parentIdea.title,
parentIdea.description,
brainstormSession.seedIdea,
parentRefs,
extraNotes,
locale
)
const llmResponse = await provider.generateText(prompt)
let newIdeas: any[]
try {
const cleaned = llmResponse.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim()
newIdeas = JSON.parse(cleaned)
if (!Array.isArray(newIdeas)) throw new Error('Not an array')
} catch {
const pickNote = (idx: number) => {
const n = extraNotes[idx % extraNotes.length]
return n ? { noteId: n.id, relation: 'extends' as const, explanation: `Related to "${n.title}"` } : { noteId: null, relation: 'extends' as const, explanation: 'Purely generative' }
}
newIdeas = [
{ wave: 1, title: 'Sub-variation A', description: 'Direct expansion.', connectionToSeed: 'Expansion', noveltyScore: 4, noteRefs: [pickNote(0)] },
{ wave: 1, title: 'Sub-variation B', description: 'Another angle.', connectionToSeed: 'Detail', noveltyScore: 5, noteRefs: [pickNote(1)] },
{ wave: 1, title: 'Sub-variation C', description: 'Implementation detail.', connectionToSeed: 'Implementation', noveltyScore: 3, noteRefs: [pickNote(2)] },
{ wave: 2, title: 'Sub-analogy A', description: 'Cross-domain parallel.', connectionToSeed: 'Analogy', noveltyScore: 7, noteRefs: [pickNote(0)] },
{ wave: 2, title: 'Sub-analogy B', description: 'From another field.', connectionToSeed: 'Parallel', noveltyScore: 6, noteRefs: [pickNote(1)] },
{ wave: 2, title: 'Sub-analogy C', description: 'Inspired by nature.', connectionToSeed: 'Bio-inspired', noveltyScore: 7, noteRefs: [pickNote(2)] },
{ wave: 3, title: 'Sub-disruption A', description: 'Challenge assumption.', connectionToSeed: 'Inversion', noveltyScore: 9, noteRefs: [{ ...pickNote(0), relation: 'opposes' as const }] },
{ wave: 3, title: 'Sub-disruption B', description: 'Remove constraint.', connectionToSeed: 'Removal', noveltyScore: 8, noteRefs: [{ ...pickNote(1), relation: 'synthesizes' as const }] },
{ wave: 3, title: 'Sub-disruption C', description: 'Radical reframe.', connectionToSeed: 'Reframe', noveltyScore: 10, noteRefs: [pickNote(2)] },
]
}
const validNoteIds = new Set(extraNotes.map(n => n.id))
for (let idx = 0; idx < newIdeas.length; idx++) {
const idea = newIdeas[idx]
const angle = (idx % 3) * (2 * Math.PI / 3) + (idea.wave - 1) * 0.5
const baseRadius = 150
const parentRadius = (parentIdea.positionX && parentIdea.positionY)
? Math.sqrt(parentIdea.positionX ** 2 + parentIdea.positionY ** 2)
: 0
const radius = parentRadius + idea.wave * baseRadius
const baseAngle = (parentIdea.positionX && parentIdea.positionY)
? Math.atan2(parentIdea.positionY, parentIdea.positionX)
: 0
const created = await prisma.brainstormIdea.create({
data: {
sessionId,
waveNumber: idea.wave || Math.floor(idx / 3) + 1,
title: idea.title || `Idea ${idx + 1}`,
description: idea.description || '',
connectionToSeed: idea.connectionToSeed || null,
noveltyScore: idea.noveltyScore || null,
parentIdeaId: ideaId,
positionX: Math.cos(baseAngle + angle) * radius,
positionY: Math.sin(baseAngle + angle) * radius,
},
})
if (idea.noteRefs && Array.isArray(idea.noteRefs)) {
for (const ref of idea.noteRefs) {
const noteId = ref.noteId && validNoteIds.has(ref.noteId) ? ref.noteId : null
await prisma.brainstormNoteRef.create({
data: {
ideaId: created.id,
noteId,
relation: ref.relation || 'extends',
explanation: ref.explanation || '',
},
})
}
}
}
const updatedSession = await prisma.brainstormSession.findUnique({
where: { id: sessionId },
include: {
ideas: {
orderBy: [{ waveNumber: 'asc' }, { createdAt: 'asc' }],
include: {
noteRefs: {
include: {
note: { select: { id: true, title: true } },
},
},
},
},
},
})
const cIds = [...new Set((updatedSession?.ideas || []).map((i: any) => i.createdBy).filter(Boolean))]
if (cIds.length > 0) {
const crs = await prisma.user.findMany({ where: { id: { in: cIds } }, select: { id: true, name: true, image: true } })
const cm = new Map(crs.map((c: any) => [c.id, c]))
for (const idea of updatedSession?.ideas || []) { (idea as any).creator = (idea as any).createdBy ? cm.get((idea as any).createdBy) || null : null }
}
await captureSnapshot(sessionId, `Wave expanded: ${parentIdea.title}`).catch(() => {})
return NextResponse.json({ success: true, data: updatedSession })
} catch (error: any) {
if (error instanceof z.ZodError) {
return NextResponse.json({ error: error.issues }, { status: 400 })
}
console.error('Error expanding idea:', error)
return NextResponse.json(
{ error: error.message || 'Failed to expand idea' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,194 @@
import { NextRequest, NextResponse } from 'next/server'
import prisma from '@/lib/prisma'
import { auth } from '@/auth'
import { verifyParticipant } from '@/lib/brainstorm-collab'
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ sessionId: string }> }
) {
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
try {
const { sessionId } = await params
const brainstormSession = await prisma.brainstormSession.findFirst({
where: {
id: sessionId,
OR: [
{ userId: session.user.id },
{ participants: { some: { userId: session.user.id } } },
],
},
include: {
ideas: {
orderBy: [{ waveNumber: 'asc' }, { createdAt: 'asc' }],
include: {
noteRefs: {
include: {
note: { select: { id: true, title: true } },
},
},
},
},
},
})
if (!brainstormSession) {
return NextResponse.json({ error: 'Session not found' }, { status: 404 })
}
if (brainstormSession.exportedNoteId) {
const existingNote = await prisma.note.findUnique({
where: { id: brainstormSession.exportedNoteId },
})
if (existingNote) {
return NextResponse.json({ success: true, data: existingNote })
}
}
const waveLabels: Record<number, string> = {
1: '🔄 Variations',
2: '🔗 Analogies',
3: '💥 Disruptions',
}
const activeIdeas = brainstormSession.ideas.filter(i => i.status !== 'dismissed')
const convertedIdeas = brainstormSession.ideas.filter(i => i.status === 'converted')
const dismissedCount = brainstormSession.ideas.filter(i => i.status === 'dismissed').length
let markdown = `# Brainstorm: ${brainstormSession.seedIdea}\n\n`
markdown += `> Generated on ${brainstormSession.createdAt.toLocaleDateString()}\n\n`
markdown += `---\n\n`
markdown += `**Summary**: ${activeIdeas.length} active ideas, ${convertedIdeas.length} converted to notes, ${dismissedCount} dismissed.\n\n`
for (let wave = 1; wave <= 3; wave++) {
const waveIdeas = activeIdeas.filter(i => i.waveNumber === wave)
if (waveIdeas.length === 0) continue
markdown += `## ${waveLabels[wave] || `Wave ${wave}`}\n\n`
for (const idea of waveIdeas) {
const statusIcon = idea.status === 'converted' ? '✅' : idea.status === 'dismissed' ? '❌' : '💡'
markdown += `### ${statusIcon} ${idea.title}\n`
markdown += `${idea.description}\n\n`
markdown += `- **Connection**: ${idea.connectionToSeed || 'N/A'}\n`
markdown += `- **Novelty**: ${idea.noveltyScore || 'N/A'}/10\n`
const validRefs = (idea.noteRefs || []).filter(r => r.noteId && r.note)
if (validRefs.length > 0) {
markdown += `- **Origin**:\n`
for (const ref of validRefs) {
const relLabel = {
derived_from: 'Derived from',
opposes: 'Opposes',
extends: 'Extends',
synthesizes: 'Synthesizes',
transposes: 'Transposes',
}[ref.relation] || 'Related to'
markdown += ` - ${relLabel} [${ref.note?.title || 'Untitled'}](note:${ref.noteId}): ${ref.explanation}\n`
}
}
if (idea.convertedToNoteId) {
markdown += `- **→ Converted to note**: ${idea.convertedToNoteId}\n`
}
if (idea.parentIdeaId) {
const parent = brainstormSession.ideas.find(i => i.id === idea.parentIdeaId)
if (parent) {
markdown += `- **Parent idea**: ${parent.title}\n`
}
}
markdown += `\n`
}
}
const allReferencedNoteIds = new Set<string>()
for (const idea of brainstormSession.ideas) {
for (const ref of idea.noteRefs || []) {
if (ref.noteId) allReferencedNoteIds.add(ref.noteId)
}
}
if (allReferencedNoteIds.size > 0) {
markdown += `---\n\n## Notes sollicitées\n\n`
const refNotes = await prisma.note.findMany({
where: { id: { in: Array.from(allReferencedNoteIds) } },
select: { id: true, title: true },
})
for (const note of refNotes) {
const refsForNote = brainstormSession.ideas.flatMap(i =>
(i.noteRefs || []).filter(r => r.noteId === note.id)
)
const accepted = refsForNote.filter(r => r.verdict === 'accepted').length
const dismissed = refsForNote.filter(r => r.verdict === 'dismissed').length
const verdictStr = accepted > 0 ? `${accepted} idea(s) accepted` : dismissed > 0 ? `❌ all dismissed` : '⏳ unresolved'
markdown += `- [${note.title || 'Untitled'}](note:${note.id}) — ${verdictStr}\n`
}
markdown += `\n`
}
if (convertedIdeas.length > 0) {
markdown += `## Converted Notes\n\n`
const convertedNoteIds = convertedIdeas
.map(i => i.convertedToNoteId)
.filter(Boolean) as string[]
if (convertedNoteIds.length > 0) {
const notes = await prisma.note.findMany({
where: { id: { in: convertedNoteIds } },
select: { id: true, title: true },
})
for (const note of notes) {
markdown += `- [${note.title || 'Untitled'}](note:${note.id})\n`
}
}
}
const notebookName = brainstormSession.seedIdea.length > 40
? brainstormSession.seedIdea.substring(0, 40).trim() + '…'
: brainstormSession.seedIdea
let notebook = await prisma.notebook.findFirst({
where: { userId: session.user.id, name: notebookName, trashedAt: null },
})
if (!notebook) {
const notebookCount = await prisma.notebook.count({ where: { userId: session.user.id, trashedAt: null } })
notebook = await prisma.notebook.create({
data: {
userId: session.user.id,
name: notebookName,
order: notebookCount,
icon: 'wind',
},
})
}
const note = await prisma.note.create({
data: {
userId: session.user.id,
title: `Synthèse: ${brainstormSession.seedIdea.slice(0, 50)}`,
content: markdown,
type: 'markdown',
labels: JSON.stringify(['brainstorm', 'export']),
notebookId: notebook.id,
},
})
await prisma.brainstormSession.update({
where: { id: sessionId },
data: { exportedNoteId: note.id },
})
return NextResponse.json({ success: true, data: { ...note, _notebookName: notebookName } }, { status: 201 })
} catch (error) {
console.error('Error exporting brainstorm:', error)
return NextResponse.json(
{ error: 'Failed to export brainstorm' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,98 @@
import { NextRequest, NextResponse } from 'next/server'
import prisma from '@/lib/prisma'
import { auth } from '@/auth'
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ sessionId: string }> }
) {
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
try {
const { sessionId } = await params
const brainstormSession = await prisma.brainstormSession.findFirst({
where: {
id: sessionId,
OR: [
{ userId: session.user.id },
{ participants: { some: { userId: session.user.id } } },
],
},
include: {
ideas: {
include: {
noteRefs: {
include: {
note: { select: { id: true, title: true, labels: true } },
},
},
},
},
},
})
if (!brainstormSession) {
return NextResponse.json({ error: 'Session not found' }, { status: 404 })
}
const noteImpactMap = new Map<string, { title: string; labels: string[]; acceptedCount: number; dismissedCount: number }>()
for (const idea of brainstormSession.ideas) {
for (const ref of idea.noteRefs) {
if (!ref.noteId || !ref.note) continue
const existing = noteImpactMap.get(ref.noteId) || {
title: ref.note.title || 'Untitled',
labels: (() => { try { return JSON.parse(ref.note.labels || '[]') } catch { return [] } })(),
acceptedCount: 0,
dismissedCount: 0,
}
if (ref.verdict === 'accepted') existing.acceptedCount++
else if (ref.verdict === 'dismissed') existing.dismissedCount++
noteImpactMap.set(ref.noteId, existing)
}
}
const updatePromises: Promise<any>[] = []
for (const [noteId, impact] of noteImpactMap) {
if (impact.acceptedCount === 0 && impact.dismissedCount > 0) {
if (!impact.labels.includes('brainstorm-dry')) {
impact.labels.push('brainstorm-dry')
updatePromises.push(
prisma.note.update({
where: { id: noteId },
data: { labels: JSON.stringify(impact.labels) },
})
)
}
}
}
if (updatePromises.length > 0) {
await prisma.$transaction(updatePromises)
}
const fruitful = Array.from(noteImpactMap.values()).filter(n => n.acceptedCount > 0).length
const dry = Array.from(noteImpactMap.values()).filter(n => n.acceptedCount === 0 && n.dismissedCount > 0).length
const totalRefs = Array.from(noteImpactMap.values()).length
return NextResponse.json({
success: true,
impact: {
notesSolicited: totalRefs,
notesEnriched: fruitful,
notesMarkedDry: dry,
},
})
} catch (error) {
console.error('Error finalizing brainstorm:', error)
return NextResponse.json(
{ error: 'Failed to finalize brainstorm' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,123 @@
import { NextRequest, NextResponse } from 'next/server'
import prisma from '@/lib/prisma'
import { auth } from '@/auth'
import { z } from 'zod'
import crypto from 'crypto'
import { verifyParticipant, logActivity } from '@/lib/brainstorm-collab'
const inviteSchema = z.object({
role: z.enum(['editor', 'viewer']).default('editor'),
expiresInHours: z.number().min(1).max(168).default(24),
email: z.string().email().optional(),
})
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ sessionId: string }> }
) {
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
try {
const { sessionId } = await params
const { isParticipant } = await verifyParticipant(sessionId, session.user.id, 'host')
if (!isParticipant) {
return NextResponse.json({ error: 'Only the host can invite' }, { status: 403 })
}
const body = await request.json()
const { role: inviteRole, expiresInHours, email } = inviteSchema.parse(body)
const brainstormSession = await prisma.brainstormSession.findUnique({
where: { id: sessionId },
select: { id: true, seedIdea: true, userId: true, inviteToken: true, inviteExpiry: true },
})
if (!brainstormSession) {
return NextResponse.json({ error: 'Session not found' }, { status: 404 })
}
let token = brainstormSession.inviteToken
let expiry = brainstormSession.inviteExpiry
if (!token || (expiry && expiry < new Date())) {
token = crypto.randomBytes(24).toString('hex')
expiry = new Date(Date.now() + expiresInHours * 60 * 60 * 1000)
await prisma.brainstormSession.update({
where: { id: sessionId },
data: { inviteToken: token, inviteExpiry: expiry },
})
}
const inviteUrl = `/brainstorm?invite=${token}`
if (email) {
const recipient = await prisma.user.findUnique({
where: { email },
select: { id: true, name: true, email: true },
})
if (!recipient) {
return NextResponse.json({ error: 'User not found' }, { status: 404 })
}
if (recipient.id === session.user.id) {
return NextResponse.json({ error: 'Cannot invite yourself' }, { status: 400 })
}
const existing = await prisma.brainstormParticipant.findFirst({
where: { sessionId, userId: recipient.id },
})
if (existing) {
return NextResponse.json({ error: 'Already a participant' }, { status: 409 })
}
await prisma.notification.create({
data: {
userId: recipient.id,
type: 'brainstorm_invite',
title: brainstormSession.seedIdea.length > 50
? brainstormSession.seedIdea.substring(0, 50) + '…'
: brainstormSession.seedIdea,
message: `${session.user.name || 'Someone'} invited you to a brainstorm session`,
actionUrl: inviteUrl,
relatedId: sessionId,
},
})
await logActivity(sessionId, 'invite_created', session.user.id, {
role: inviteRole,
targetEmail: email,
targetUserId: recipient.id,
})
return NextResponse.json({
success: true,
mode: 'email',
invitedUser: { name: recipient.name, email: recipient.email },
inviteUrl,
expiresAt: expiry,
})
}
await logActivity(sessionId, 'invite_created', session.user.id, { role: inviteRole })
return NextResponse.json({
success: true,
mode: 'link',
inviteUrl,
token,
expiresAt: expiry,
})
} catch (error: any) {
if (error instanceof z.ZodError) {
return NextResponse.json({ error: error.issues }, { status: 400 })
}
console.error('Error creating invite:', error)
return NextResponse.json({ error: 'Failed to create invite' }, { status: 500 })
}
}

View File

@@ -0,0 +1,224 @@
import { NextRequest, NextResponse } from 'next/server'
import prisma from '@/lib/prisma'
import { auth } from '@/auth'
import { z } from 'zod'
import { verifyParticipant, logActivity, resolveAiContextUserId, captureSnapshot } from '@/lib/brainstorm-collab'
import { embeddingService } from '@/lib/ai/services/embedding.service'
import { getTagsProvider } from '@/lib/ai/factory'
import { getSystemConfig } from '@/lib/config'
import { emitToSession } from '@/lib/socket-emit'
const manualSchema = z.object({
title: z.string().min(1),
description: z.string().optional(),
parentIdeaId: z.string().optional(),
locale: z.string().optional(),
})
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ sessionId: string }> }
) {
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
try {
const { sessionId } = await params
const { isParticipant } = await verifyParticipant(sessionId, session.user.id, 'editor')
if (!isParticipant) {
return NextResponse.json({ error: 'No edit permission' }, { status: 403 })
}
const body = await request.json()
const { title, description, parentIdeaId, locale } = manualSchema.parse(body)
const brainstormSession = await prisma.brainstormSession.findFirst({
where: { id: sessionId },
})
if (!brainstormSession) {
return NextResponse.json({ error: 'Session not found' }, { status: 404 })
}
let wave = 1
let parentIdea: any = null
if (parentIdeaId) {
parentIdea = await prisma.brainstormIdea.findFirst({
where: { id: parentIdeaId, sessionId },
})
if (parentIdea) {
wave = Math.min((parentIdea.waveNumber || 1) + 1, 3)
}
}
const existingIdeas = await prisma.brainstormIdea.count({
where: { sessionId, waveNumber: wave },
})
const angle = (existingIdeas % 4) * (2 * Math.PI / 4) + Math.random() * 0.5
const radius = wave * 200 + Math.random() * 50
const idea = await prisma.brainstormIdea.create({
data: {
sessionId,
waveNumber: wave,
title,
description: description || '',
connectionToSeed: parentIdea
? `Manual response to "${(parentIdea.title || '').substring(0, 40)}"`
: 'Manual idea added by participant',
noveltyScore: null,
parentIdeaId: parentIdeaId || null,
createdBy: session.user.id,
createdByType: 'human',
positionX: Math.cos(angle) * radius,
positionY: Math.sin(angle) * radius,
},
})
// [UPDATE - SÉCURITÉ] Recherche vectorielle guest-safe
let relatedNoteIds: string[] = []
try {
const { isGuest, publicNoteIds, aiUserId } = await resolveAiContextUserId(sessionId, session.user.id)
if (isGuest && (!publicNoteIds || publicNoteIds.length === 0)) {
// Invité sans notes publiques → skip la recherche vectorielle
relatedNoteIds = []
} else {
const embedding = await embeddingService.generateEmbedding(`${title} ${description || ''}`)
const vectorStr = embeddingService.toVectorString(embedding.embedding)
let results: any[]
if (isGuest && publicNoteIds && publicNoteIds.length > 0) {
// Invité : restreindre aux notes publiques uniquement
const idList = publicNoteIds.map(id => `'${id}'`).join(',')
results = await prisma.$queryRawUnsafe(
`SELECT n.id
FROM "NoteEmbedding" e
JOIN "Note" n ON n.id = e."noteId"
WHERE n.id IN (${idList}) AND n."trashedAt" IS NULL
ORDER BY e.embedding <=> $1::vector
LIMIT 3`,
vectorStr
) as any[]
} else {
// Hôte : accès complet
results = await prisma.$queryRawUnsafe(
`SELECT n.id
FROM "NoteEmbedding" e
JOIN "Note" n ON n.id = e."noteId"
WHERE n."userId" = $1 AND n."trashedAt" IS NULL
ORDER BY e.embedding <=> $2::vector
LIMIT 3`,
aiUserId, vectorStr
) as any[]
}
relatedNoteIds = results.map((r: any) => r.id)
}
} catch {}
if (relatedNoteIds.length > 0) {
const notes = await prisma.note.findMany({
where: { id: { in: relatedNoteIds } },
select: { id: true, title: true },
})
for (const note of notes) {
await prisma.brainstormNoteRef.create({
data: {
ideaId: idea.id,
noteId: note.id,
relation: 'extends',
explanation: `Manual idea — related to your note "${note.title}"`,
},
})
}
await prisma.brainstormIdea.update({
where: { id: idea.id },
data: { relatedNoteIds: JSON.stringify(relatedNoteIds) },
})
}
await logActivity(sessionId, 'manual_idea', session.user.id, { ideaTitle: title, ideaId: idea.id })
await captureSnapshot(sessionId, `Manual idea: ${title}`).catch(() => {})
// [UPDATE - TEMPS RÉEL] Retourner immédiatement, enrichissement IA en arrière-plan
// Le client est notifié via Socket : idea:ai_processing → idea:ai_completed | idea:ai_failed
const immediateResponse = NextResponse.json(
{ success: true, data: { ideaId: idea.id, title, status: 'ai_processing' } },
{ status: 201 }
)
// Capturer les valeurs avant la closure asynchrone (session.user peut être undefined plus tard)
const requestingUserId = session.user!.id
const enrichAsync = async () => {
try {
// Notifier le room que l'IA traite ce nœud (bordure pulsante violet)
await emitToSession(sessionId, 'idea:ai_processing', {
ideaId: idea.id,
triggeredBy: requestingUserId,
})
const config = await getSystemConfig()
const provider = getTagsProvider(config)
const lang = locale === 'fr' ? 'French' : locale === 'es' ? 'Spanish' : locale === 'de' ? 'German' : locale === 'it' ? 'Italian' : locale === 'pt' ? 'Portuguese' : locale === 'ja' ? 'Japanese' : locale === 'ko' ? 'Korean' : locale === 'zh' ? 'Chinese' : locale === 'ar' ? 'Arabic' : "the user's language"
const enrichPrompt = `You are an idea enrichment assistant. Given a user's raw brainstorm idea and context, produce a JSON object with:
- "enrichedTitle": a polished, concise version of the title (max 60 chars)
- "enrichedDescription": an expanded 2-3 sentence description that develops the idea further
- "connectionToSeed": a 1-sentence explanation of how this idea connects to the seed topic
- "noveltyScore": a number 1-10 rating how novel/original this idea is
IMPORTANT: You MUST write ALL text in ${lang}.
Seed topic: "${brainstormSession.seedIdea}"
${parentIdea ? `Parent idea this responds to: "${parentIdea.title}"` : ''}
User's raw idea title: "${title}"
User's raw description: "${description || 'none provided'}"
Respond ONLY with the JSON object, no markdown.`
const raw = await provider.generateText(enrichPrompt)
const cleaned = raw.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim()
const enriched = JSON.parse(cleaned)
const enrichedTitle = enriched.enrichedTitle?.substring(0, 60) || title
const enrichedDescription = enriched.enrichedDescription || description || ''
const connectionToSeed = enriched.connectionToSeed || 'Manual idea added by participant'
const noveltyScore = enriched.noveltyScore || null
await prisma.brainstormIdea.update({
where: { id: idea.id },
data: { title: enrichedTitle, description: enrichedDescription, connectionToSeed, noveltyScore },
})
// [UPDATE - TEMPS RÉEL] Notifier la complétion avec les données enrichies
await emitToSession(sessionId, 'idea:ai_completed', {
ideaId: idea.id,
title: enrichedTitle,
description: enrichedDescription,
noveltyScore,
connectionToSeed,
})
} catch (enrichError) {
console.error('Enrichment failed, keeping raw idea:', enrichError)
// [UPDATE - TEMPS RÉEL] Notifier l'échec — le nœud reste en état dégradé
await emitToSession(sessionId, 'idea:ai_failed', { ideaId: idea.id })
}
}
// setImmediate pour ne pas bloquer la réponse HTTP
setImmediate(() => { enrichAsync() })
return immediateResponse
} catch (error: any) {
if (error instanceof z.ZodError) {
return NextResponse.json({ error: error.issues }, { status: 400 })
}
console.error('Error adding manual idea:', error)
return NextResponse.json({ error: 'Failed to add idea' }, { status: 500 })
}
}

View File

@@ -0,0 +1,215 @@
import { NextRequest, NextResponse } from 'next/server'
import prisma from '@/lib/prisma'
import { auth } from '@/auth'
function resolveAccessRole(
brainstormSession: any,
userId: string | null
): 'owner' | 'editor' | 'viewer' | 'guest' | 'none' {
if (!userId) {
return brainstormSession.isPublic ? 'guest' : 'none'
}
if (brainstormSession.userId === userId) return 'owner'
const participant = brainstormSession.participants?.find(
(p: any) => p.userId === userId
)
if (participant) return participant.role
const acceptedShare = brainstormSession.shares?.find(
(s: any) => s.userId === userId && s.status === 'accepted'
)
if (acceptedShare) return acceptedShare.permission
return brainstormSession.isPublic ? 'guest' : 'none'
}
function filterNoteRefsByRole(ideas: any[], role: string): any[] {
const allowedVis = role === 'owner'
? ['public', 'participants', 'owner_only']
: role === 'editor'
? ['public', 'participants', 'owner_only']
: role === 'viewer'
? ['public', 'participants']
: ['public']
return ideas.map((idea: any) => ({
...idea,
noteRefs: (idea.noteRefs || []).filter(
(ref: any) => allowedVis.includes(ref.visibility || 'participants')
),
}))
}
function sanitizeForGuest(session: any): any {
const { participants, shares, ...rest } = session
const filteredIdeas = filterNoteRefsByRole(rest.ideas || [], 'guest')
return {
...rest,
ideas: filteredIdeas,
sourceNote: null,
exportedNote: null,
}
}
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ sessionId: string }> }
) {
const authSession = await auth()
const userId = authSession?.user?.id || null
try {
const { sessionId } = await params
const brainstormSession = await prisma.brainstormSession.findFirst({
where: { id: sessionId },
include: {
ideas: {
orderBy: [{ waveNumber: 'asc' }, { createdAt: 'asc' }],
include: {
noteRefs: {
include: {
note: { select: { id: true, title: true } },
},
},
},
},
sourceNote: {
select: { id: true, title: true },
},
exportedNote: {
select: { id: true, title: true },
},
participants: {
where: userId ? { userId } : undefined,
select: { userId: true, role: true },
},
shares: userId
? {
where: { userId, status: 'accepted' },
select: { userId: true, permission: true },
}
: false,
},
})
if (!brainstormSession) {
return NextResponse.json({ error: 'Not found' }, { status: 404 })
}
const creatorIds = [...new Set(brainstormSession.ideas.map((i: any) => i.createdBy).filter(Boolean))]
const creators = creatorIds.length > 0
? await prisma.user.findMany({ where: { id: { in: creatorIds } }, select: { id: true, name: true, image: true } })
: []
const creatorMap = new Map(creators.map((c: any) => [c.id, c]))
for (const idea of brainstormSession.ideas) {
(idea as any).creator = (idea as any).createdBy ? creatorMap.get((idea as any).createdBy) || null : null
}
const role = resolveAccessRole(brainstormSession, userId)
if (role === 'none') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
let responseData: any = brainstormSession
if (role === 'guest') {
responseData = sanitizeForGuest(brainstormSession)
} else if (role === 'viewer') {
const { participants: _p, shares: _s, ...rest } = brainstormSession as any
responseData = {
...rest,
ideas: filterNoteRefsByRole(rest.ideas || [], 'viewer'),
}
} else {
const { participants: _p, shares: _s, ...rest } = brainstormSession as any
responseData = rest
}
return NextResponse.json({
success: true,
data: responseData,
_meta: { role, canEdit: ['owner', 'editor'].includes(role) || (role === 'guest' && brainstormSession.guestCanEdit) },
})
} catch (error) {
console.error('Error fetching brainstorm session:', error)
return NextResponse.json(
{ error: 'Failed to fetch session' },
{ status: 500 }
)
}
}
export async function PATCH(
request: NextRequest,
{ params }: { params: Promise<{ sessionId: string }> }
) {
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
try {
const { sessionId } = await params
const body = await request.json()
const brainstormSession = await prisma.brainstormSession.findFirst({
where: { id: sessionId, userId: session.user.id },
})
if (!brainstormSession) {
return NextResponse.json({ error: 'Not found' }, { status: 404 })
}
const updates: Record<string, any> = {}
if (typeof body.isPublic === 'boolean') updates.isPublic = body.isPublic
if (typeof body.guestCanEdit === 'boolean') updates.guestCanEdit = body.guestCanEdit
if (Object.keys(updates).length === 0) {
return NextResponse.json({ error: 'No valid fields to update' }, { status: 400 })
}
const updated = await prisma.brainstormSession.update({
where: { id: sessionId },
data: updates,
})
return NextResponse.json({ success: true, data: updated })
} catch (error) {
console.error('Error updating brainstorm session:', error)
return NextResponse.json(
{ error: 'Failed to update session' },
{ status: 500 }
)
}
}
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ sessionId: string }> }
) {
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
try {
const { sessionId } = await params
const brainstormSession = await prisma.brainstormSession.findFirst({
where: { id: sessionId, userId: session.user.id },
})
if (!brainstormSession) {
return NextResponse.json({ error: 'Not found' }, { status: 404 })
}
await prisma.brainstormSession.delete({
where: { id: sessionId },
})
return NextResponse.json({ success: true })
} catch (error) {
console.error('Error deleting brainstorm session:', error)
return NextResponse.json(
{ error: 'Failed to delete session' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,53 @@
import { NextRequest, NextResponse } from 'next/server'
import prisma from '@/lib/prisma'
import { auth } from '@/auth'
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ sessionId: string }> }
) {
const authSession = await auth()
if (!authSession?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
try {
const { sessionId } = await params
const brainstormSession = await prisma.brainstormSession.findFirst({
where: {
id: sessionId,
OR: [
{ userId: authSession.user.id },
{ participants: { some: { userId: authSession.user.id } } },
{ shares: { some: { userId: authSession.user.id, status: 'accepted' } } },
],
} as any,
select: { id: true },
})
if (!brainstormSession) {
return NextResponse.json({ error: 'Not found' }, { status: 404 })
}
const snapshots = await prisma.brainstormSnapshot.findMany({
where: { sessionId },
orderBy: { step: 'asc' },
select: {
id: true,
step: true,
label: true,
activityId: true,
ideaGraph: true,
createdAt: true,
},
})
return NextResponse.json({ success: true, data: snapshots })
} catch (error) {
console.error('Error fetching snapshots:', error)
return NextResponse.json(
{ error: 'Failed to fetch snapshots' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,50 @@
import { NextRequest, NextResponse } from 'next/server'
import prisma from '@/lib/prisma'
import { auth } from '@/auth'
import { z } from 'zod'
const positionSchema = z.object({
ideaId: z.string().min(1),
positionX: z.number(),
positionY: z.number(),
})
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ sessionId: string }> }
) {
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
try {
const { sessionId } = await params
const body = await request.json()
const { ideaId, positionX, positionY } = positionSchema.parse(body)
const brainstormSession = await prisma.brainstormSession.findFirst({
where: { id: sessionId, userId: session.user.id },
})
if (!brainstormSession) {
return NextResponse.json({ error: 'Session not found' }, { status: 404 })
}
await prisma.brainstormIdea.update({
where: { id: ideaId },
data: { positionX, positionY },
})
return NextResponse.json({ success: true })
} catch (error: any) {
if (error instanceof z.ZodError) {
return NextResponse.json({ error: error.issues }, { status: 400 })
}
console.error('Error updating position:', error)
return NextResponse.json(
{ error: error.message || 'Failed to update position' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,85 @@
import { NextRequest, NextResponse } from 'next/server'
import prisma from '@/lib/prisma'
import { auth } from '@/auth'
import { z } from 'zod'
import { logActivity } from '@/lib/brainstorm-collab'
const joinSchema = z.object({
token: z.string().min(1),
})
export async function POST(request: NextRequest) {
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
try {
const body = await request.json()
const { token } = joinSchema.parse(body)
const brainstormSession = await prisma.brainstormSession.findFirst({
where: { inviteToken: token },
select: { id: true, inviteExpiry: true, userId: true, seedIdea: true },
})
if (!brainstormSession) {
return NextResponse.json({ error: 'Invalid invite token' }, { status: 404 })
}
if (brainstormSession.inviteExpiry && brainstormSession.inviteExpiry < new Date()) {
return NextResponse.json({ error: 'Invite expired' }, { status: 410 })
}
const existing = await prisma.brainstormParticipant.findFirst({
where: { sessionId: brainstormSession.id, userId: session.user.id },
})
if (existing) {
return NextResponse.json({
success: true,
sessionId: brainstormSession.id,
role: existing.role,
})
}
const participant = await prisma.brainstormParticipant.create({
data: {
sessionId: brainstormSession.id,
userId: session.user.id,
role: 'editor',
},
})
await logActivity(brainstormSession.id, 'joined', session.user.id)
try {
const joinerName = session.user.name || 'Someone'
const seedPreview = brainstormSession.seedIdea.length > 40
? brainstormSession.seedIdea.substring(0, 40) + '…'
: brainstormSession.seedIdea
await prisma.notification.create({
data: {
userId: brainstormSession.userId,
type: 'brainstorm_joined',
title: `${joinerName} joined your brainstorm`,
message: seedPreview,
actionUrl: `/brainstorm?session=${brainstormSession.id}`,
relatedId: brainstormSession.id,
},
})
} catch {}
return NextResponse.json({
success: true,
sessionId: brainstormSession.id,
role: participant.role,
}, { status: 201 })
} catch (error: any) {
if (error instanceof z.ZodError) {
return NextResponse.json({ error: error.issues }, { status: 400 })
}
console.error('Error joining brainstorm:', error)
return NextResponse.json({ error: 'Failed to join' }, { status: 500 })
}
}

View File

@@ -0,0 +1,407 @@
import { NextRequest, NextResponse } from 'next/server'
import prisma from '@/lib/prisma'
import { auth } from '@/auth'
import { z } from 'zod'
import { getTagsProvider } from '@/lib/ai/factory'
import { getSystemConfig } from '@/lib/config'
import { embeddingService } from '@/lib/ai/services/embedding.service'
import { logActivity, captureSnapshot } from '@/lib/brainstorm-collab'
const waveSchema = z.object({
seedIdea: z.string().min(1, 'Seed idea is required'),
sourceNoteId: z.string().optional(),
contextNoteIds: z.array(z.string()).optional(),
locale: z.string().optional(),
})
interface ClassifiedNote {
id: string
title: string
summary: string
category: 'SUPPORT' | 'TENSION' | 'EXTENSION'
}
async function autoContextSearch(
userId: string,
seedIdea: string,
userNoteIds?: string[]
): Promise<ClassifiedNote[]> {
let candidateIds: string[] = []
if (userNoteIds && userNoteIds.length > 0) {
candidateIds = userNoteIds
} else {
try {
const embedding = await embeddingService.generateEmbedding(seedIdea)
const vectorStr = embeddingService.toVectorString(embedding.embedding)
const results = await prisma.$queryRawUnsafe(
`SELECT n.id
FROM "NoteEmbedding" e
JOIN "Note" n ON n.id = e."noteId"
WHERE n."userId" = $1 AND n."trashedAt" IS NULL
ORDER BY e.embedding <=> $2::vector
LIMIT 8`,
userId, vectorStr
) as any[]
candidateIds = results.map((r: any) => r.id)
} catch {
return []
}
}
if (candidateIds.length === 0) return []
const notes = await prisma.note.findMany({
where: { id: { in: candidateIds }, userId, trashedAt: null },
select: { id: true, title: true, content: true },
})
if (notes.length === 0) return []
const notesForLLM = notes.map(n => ({
id: n.id,
title: n.title || 'Untitled',
snippet: (n.content || '').slice(0, 300),
}))
try {
const config = await getSystemConfig()
const provider = getTagsProvider(config)
const classifyPrompt = `Given the seed idea: "${seedIdea}"
Classify each note as SUPPORT (confirms/reinforces the seed), TENSION (contradicts/questions the seed), or EXTENSION (extends the seed into an adjacent domain).
Notes:
${notesForLLM.map(n => `[${n.id}] "${n.title}": ${n.snippet}`).join('\n')}
Respond ONLY with a valid JSON array of objects:
{ "noteId": string, "category": "SUPPORT" | "TENSION" | "EXTENSION" }`
const raw = await provider.generateText(classifyPrompt)
const cleaned = raw.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim()
const classifications: { noteId: string; category: 'SUPPORT' | 'TENSION' | 'EXTENSION' }[] = JSON.parse(cleaned)
return notes.map(n => {
const cls = classifications.find(c => c.noteId === n.id)
return {
id: n.id,
title: n.title || 'Untitled',
summary: (n.content || '').slice(0, 200),
category: cls?.category || 'EXTENSION',
}
})
} catch {
return notes.map(n => ({
id: n.id,
title: n.title || 'Untitled',
summary: (n.content || '').slice(0, 200),
category: 'EXTENSION' as const,
}))
}
}
function buildPromptV2(seedIdea: string, classifiedNotes: ClassifiedNote[], locale?: string): string {
const supportNotes = classifiedNotes.filter(n => n.category === 'SUPPORT')
const tensionNotes = classifiedNotes.filter(n => n.category === 'TENSION')
const extensionNotes = classifiedNotes.filter(n => n.category === 'EXTENSION')
let notesSection = ''
if (classifiedNotes.length > 0) {
notesSection = `\nUSER'S EXISTING NOTES (classified by relationship to the seed):\n`
if (supportNotes.length > 0) {
notesSection += `\nSUPPORTING NOTES (confirm/reinforce the seed):\n`
notesSection += supportNotes.map(n => `- [ID: ${n.id}] "${n.title}": ${n.summary}`).join('\n')
}
if (tensionNotes.length > 0) {
notesSection += `\nTENSION NOTES (contradict or question the seed):\n`
notesSection += tensionNotes.map(n => `- [ID: ${n.id}] "${n.title}": ${n.summary}`).join('\n')
}
if (extensionNotes.length > 0) {
notesSection += `\nEXTENSION NOTES (extend the seed into adjacent domains):\n`
notesSection += extensionNotes.map(n => `- [ID: ${n.id}] "${n.title}": ${n.summary}`).join('\n')
}
}
return `You are a creative brainstorming assistant with access to the user's personal knowledge base. Your job is to generate ideas that DELIBERATELY CROSS the seed concept with existing notes — creating productive tension, not just variations.
USER'S SEED IDEA: ${seedIdea}
${notesSection}
GENERATION RULES:
WAVE 1 — VARIATIONS (3 ideas):
- At least 1 idea must BUILD ON a SUPPORTING note
- At least 1 idea must RESPOND TO a TENSION note (resolve or embrace the contradiction)
WAVE 2 — ANALOGIES (3 ideas):
- At least 1 idea must FUSE a concept from an EXTENSION note with the seed
- At least 1 idea must be a PATTERN TRANSPOSITION from any note
WAVE 3 — DISRUPTIONS (3 ideas):
- At least 1 idea must INVERT an assumption found in a SUPPORTING note
- At least 1 idea must SYNTHESIZE two notes that appear contradictory
RESPOND ONLY with a valid JSON array of 9 objects:
{
"wave": number (1, 2, or 3),
"title": string (short, 2-6 words),
"description": string (1-2 sentences, specific and actionable),
"connectionToSeed": string (how it relates to the seed),
"noveltyScore": number (1-10),
"noteRefs": [
{
"noteId": string (must match an ID provided above, or null if genuinely no note connects),
"relation": "derived_from" | "opposes" | "extends" | "synthesizes" | "transposes",
"explanation": string (natural language, e.g. "Built on your exploration of occupancy-based scheduling")
}
]
}
CRITICAL: Each idea MUST have at least 1 noteRef. Only use null noteId if genuinely no note connects to that idea.
LANGUAGE: You MUST write ALL titles, descriptions, connectionToSeed, and explanation fields in ${locale === 'fr' ? 'French' : locale === 'es' ? 'Spanish' : locale === 'de' ? 'German' : locale === 'it' ? 'Italian' : locale === 'pt' ? 'Portuguese' : locale === 'nl' ? 'Dutch' : locale === 'ru' ? 'Russian' : locale === 'zh' ? 'Chinese' : locale === 'ja' ? 'Japanese' : locale === 'ko' ? 'Korean' : locale === 'ar' ? 'Arabic' : locale === 'fa' ? 'Farsi' : locale === 'hi' ? 'Hindi' : locale === 'pl' ? 'Polish' : 'the same language as the seed idea'}.`
}
function buildFallbackIdeas(classifiedNotes: ClassifiedNote[]): any[] {
const notesById = Object.fromEntries(classifiedNotes.map(n => [n.id, n]))
const support = classifiedNotes.filter(n => n.category === 'SUPPORT')
const tension = classifiedNotes.filter(n => n.category === 'TENSION')
const extension = classifiedNotes.filter(n => n.category === 'EXTENSION')
const pickNote = (list: ClassifiedNote[], idx: number) => {
if (list.length === 0) return { noteId: null, relation: 'extends' as const, explanation: 'Purely generative idea, no direct note link' }
const n = list[idx % list.length]
return { noteId: n.id, relation: 'extends' as const, explanation: `Inspired by your note "${n.title}"` }
}
return [
{ wave: 1, title: 'Variation A', description: 'A direct variation of the seed idea.', connectionToSeed: 'Direct extension', noveltyScore: 3, noteRefs: [pickNote(support, 0)] },
{ wave: 1, title: 'Variation B', description: 'Another angle on the seed idea.', connectionToSeed: 'Reformulation', noveltyScore: 4, noteRefs: [pickNote(tension, 0)] },
{ wave: 1, title: 'Variation C', description: 'A sub-aspect of the seed.', connectionToSeed: 'Sub-component', noveltyScore: 5, noteRefs: [pickNote(support, 1)] },
{ wave: 2, title: 'Analogy A', description: 'Inspired by biological systems.', connectionToSeed: 'Cross-domain analogy', noveltyScore: 6, noteRefs: [pickNote(extension, 0)] },
{ wave: 2, title: 'Analogy B', description: 'Drawn from technology patterns.', connectionToSeed: 'Tech parallel', noveltyScore: 7, noteRefs: [pickNote(extension, 1)] },
{ wave: 2, title: 'Analogy C', description: 'Based on natural phenomena.', connectionToSeed: 'Nature metaphor', noveltyScore: 6, noteRefs: [pickNote(classifiedNotes, 0)] },
{ wave: 3, title: 'Disruption A', description: 'What if we inverted the core assumption?', connectionToSeed: 'Inversion', noveltyScore: 9, noteRefs: [{ ...(pickNote(support, 0)), relation: 'opposes' as const }] },
{ wave: 3, title: 'Disruption B', description: 'A provocative reframing.', connectionToSeed: 'Challenge premise', noveltyScore: 8, noteRefs: [{ ...(pickNote(tension, 0)), relation: 'synthesizes' as const }] },
{ wave: 3, title: 'Disruption C', description: 'Removing a key constraint entirely.', connectionToSeed: 'Constraint removal', noveltyScore: 10, noteRefs: [pickNote(extension, 2)] },
]
}
export async function POST(request: NextRequest) {
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const userId = session.user.id
try {
const body = await request.json()
const { seedIdea, sourceNoteId, contextNoteIds, locale } = waveSchema.parse(body)
const classifiedNotes = await autoContextSearch(userId, seedIdea, contextNoteIds)
const config = await getSystemConfig()
const provider = getTagsProvider(config)
const prompt = buildPromptV2(seedIdea, classifiedNotes, locale)
const llmResponse = await provider.generateText(prompt)
let ideas: any[]
try {
const cleaned = llmResponse.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim()
ideas = JSON.parse(cleaned)
if (!Array.isArray(ideas)) throw new Error('Not an array')
} catch {
ideas = buildFallbackIdeas(classifiedNotes)
}
const validNoteIds = new Set(classifiedNotes.map(n => n.id))
const brainstormSession = await prisma.brainstormSession.create({
data: {
seedIdea,
sourceNoteId: sourceNoteId || null,
contextNoteIds: contextNoteIds ? JSON.stringify(contextNoteIds) : null,
liveblocksRoomId: `brainstorm-session`,
userId,
},
})
await prisma.brainstormParticipant.create({
data: {
sessionId: brainstormSession.id,
userId,
role: 'host',
},
})
await logActivity(brainstormSession.id, 'wave_generated', userId, { count: ideas.length })
const createdIdeas = []
for (let idx = 0; idx < ideas.length; idx++) {
const idea = ideas[idx]
const angle = (idx % 3) * (2 * Math.PI / 3) + (idea.wave - 1) * 0.5
const radius = idea.wave * 150
const created = await prisma.brainstormIdea.create({
data: {
sessionId: brainstormSession.id,
waveNumber: idea.wave || Math.floor(idx / 3) + 1,
title: idea.title || `Idea ${idx + 1}`,
description: idea.description || '',
connectionToSeed: idea.connectionToSeed || null,
noveltyScore: idea.noveltyScore || null,
relatedNoteIds: JSON.stringify(
(idea.noteRefs || []).map((r: any) => r.noteId).filter(Boolean)
),
positionX: Math.cos(angle) * radius,
positionY: Math.sin(angle) * radius,
},
})
if (idea.noteRefs && Array.isArray(idea.noteRefs)) {
for (const ref of idea.noteRefs) {
const noteId = ref.noteId && validNoteIds.has(ref.noteId) ? ref.noteId : null
await prisma.brainstormNoteRef.create({
data: {
ideaId: created.id,
noteId,
relation: ref.relation || 'extends',
explanation: ref.explanation || '',
},
})
}
}
createdIdeas.push(created)
}
const fullSession = await prisma.brainstormSession.findUnique({
where: { id: brainstormSession.id },
include: {
ideas: {
orderBy: [{ waveNumber: 'asc' }, { createdAt: 'asc' }],
include: {
noteRefs: {
include: {
note: { select: { id: true, title: true } },
},
},
},
},
},
})
const cIds = [...new Set((fullSession?.ideas || []).map((i: any) => i.createdBy).filter(Boolean))]
if (cIds.length > 0) {
const crs = await prisma.user.findMany({ where: { id: { in: cIds } }, select: { id: true, name: true, image: true } })
const cm = new Map(crs.map((c: any) => [c.id, c]))
for (const idea of fullSession?.ideas || []) { (idea as any).creator = (idea as any).createdBy ? cm.get((idea as any).createdBy) || null : null }
}
await captureSnapshot(brainstormSession.id, `Initial wave: ${brainstormSession.seedIdea.substring(0, 30)}`).catch(() => {})
return NextResponse.json({
success: true,
data: fullSession,
contextSummary: {
support: classifiedNotes.filter(n => n.category === 'SUPPORT').length,
tension: classifiedNotes.filter(n => n.category === 'TENSION').length,
extension: classifiedNotes.filter(n => n.category === 'EXTENSION').length,
},
}, { status: 201 })
} catch (error: any) {
if (error instanceof z.ZodError) {
return NextResponse.json({ error: error.issues }, { status: 400 })
}
console.error('Error creating brainstorm:', error)
return NextResponse.json(
{ error: error.message || 'Failed to create brainstorm' },
{ status: 500 }
)
}
}
export async function GET(request: NextRequest) {
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const userId = session.user.id
try {
const ownedSessions = await prisma.brainstormSession.findMany({
where: { userId },
orderBy: { createdAt: 'desc' },
include: {
_count: { select: { ideas: true } },
ideas: {
where: { status: 'active' },
select: { id: true },
},
},
})
const owned = ownedSessions.map(s => ({
id: s.id,
seedIdea: s.seedIdea,
sourceNoteId: s.sourceNoteId,
exportedNoteId: s.exportedNoteId,
createdAt: s.createdAt,
updatedAt: s.updatedAt,
totalIdeas: s._count.ideas,
activeIdeas: s.ideas.length,
_owned: true,
}))
const ownedIds = new Set(owned.map(s => s.id))
let shared: any[] = []
try {
const acceptedShareRows = await prisma.brainstormShare.findMany({
where: { userId, status: 'accepted' },
include: {
session: {
select: {
id: true,
seedIdea: true,
sourceNoteId: true,
exportedNoteId: true,
createdAt: true,
updatedAt: true,
_count: { select: { ideas: true } },
ideas: { where: { status: 'active' }, select: { id: true } },
},
},
},
})
shared = acceptedShareRows
.filter(s => s.session != null && !ownedIds.has(s.session.id))
.map(s => ({
id: s.session.id,
seedIdea: s.session.seedIdea,
sourceNoteId: s.session.sourceNoteId,
exportedNoteId: s.session.exportedNoteId,
createdAt: s.session.createdAt,
updatedAt: s.session.updatedAt,
totalIdeas: s.session._count.ideas,
activeIdeas: s.session.ideas.length,
_owned: false,
}))
} catch (shareError) {
console.error('Error fetching shared brainstorms (non-fatal):', shareError)
}
return NextResponse.json({
success: true,
data: [...owned, ...shared],
})
} catch (error) {
console.error('Error fetching brainstorms:', error)
return NextResponse.json(
{ error: 'Failed to fetch brainstorms' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,48 @@
import { NextResponse } from 'next/server'
import prisma from '@/lib/prisma'
import { auth } from '@/auth'
export async function GET() {
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
try {
const shares = await prisma.brainstormShare.findMany({
where: {
userId: session.user.id,
status: 'accepted',
},
select: {
sessionId: true,
permission: true,
session: {
select: {
id: true,
seedIdea: true,
createdAt: true,
updatedAt: true,
},
},
},
})
const data = shares.map((s) => ({
id: s.session.id,
seedIdea: s.session.seedIdea,
sourceNoteId: null,
exportedNoteId: null,
createdAt: s.session.createdAt,
updatedAt: s.session.updatedAt,
totalIdeas: 0,
activeIdeas: 0,
_isShared: true,
}))
return NextResponse.json({ success: true, data })
} catch (error) {
console.error('Error fetching shared brainstorms:', error)
return NextResponse.json({ success: true, data: [] })
}
}

View File

@@ -48,7 +48,7 @@ export async function POST(req: Request) {
// 2. Parse request body
const body = await req.json()
const { messages: rawMessages, conversationId, notebookId, language, webSearch, noteContext, format } = body as {
const { messages: rawMessages, conversationId, notebookId, language, webSearch, noteContext, format, noteId } = body as {
messages: UIMessage[]
conversationId?: string
notebookId?: string
@@ -56,6 +56,7 @@ export async function POST(req: Request) {
webSearch?: boolean
noteContext?: { title: string; content: string; tone: string; images?: string[] }
format?: 'html' | 'markdown'
noteId?: string
}
const incomingMessages = toCoreMessages(rawMessages)
@@ -107,17 +108,60 @@ export async function POST(req: Request) {
let searchResults: any[] = []
try {
searchResults = await semanticSearchService.search(currentMessage, {
notebookId,
limit: notebookId ? 10 : 5,
threshold: notebookId ? 0.3 : 0.5,
defaultTitle: untitledText,
})
const documentMention = currentMessage.match(
/\b(pdf|document|fichier|pi[eè]ce jointe|attachment|file)\b/i
)
if (documentMention) {
const docResults = await semanticSearchService.searchWithDocuments(
userId, currentMessage, {
notebookId,
limit: notebookId ? 10 : 5,
threshold: notebookId ? 0.3 : 0.5,
includeDocuments: true,
defaultTitle: untitledText,
}
)
searchResults = docResults
} else {
searchResults = await semanticSearchService.search(currentMessage, {
notebookId,
limit: notebookId ? 10 : 5,
threshold: notebookId ? 0.3 : 0.5,
defaultTitle: untitledText,
})
}
} catch {}
searchNotes = searchResults
.map((r) => `NOTE [${r.title || untitledText}]: ${r.content}`)
.map((r) => {
if ((r as any).source === 'document') {
return `DOCUMENT [${(r as any).fileName} p.${(r as any).pageNumber}] (from note: ${r.title || untitledText}):\n${r.content}`
}
return `NOTE [${r.title || untitledText}]: ${r.content}`
})
.join('\n\n---\n\n')
} else if (noteId) {
try {
const docResults = await semanticSearchService.searchWithDocuments(
userId, currentMessage, {
noteId,
limit: 8,
threshold: 0.3,
includeDocuments: true,
defaultTitle: untitledText,
}
)
searchNotes = docResults
.map((r) => {
if ((r as any).source === 'document') {
return `DOCUMENT [${(r as any).fileName} p.${(r as any).pageNumber}]:\n${r.content}`
}
return ''
})
.filter(Boolean)
.join('\n\n---\n\n')
} catch {}
}
const contextNotes = [notebookContext, searchNotes].filter(Boolean).join('\n\n---\n\n')
@@ -125,7 +169,7 @@ export async function POST(req: Request) {
// 5. System prompt synthesis
const promptLang: Record<string, { contextWithNotes: string; contextNoNotes: string; system: string }> = {
en: {
contextWithNotes: `## User's notes\n\n${contextNotes}\n\nWhen using info from the notes above, cite the source note title in parentheses, e.g.: "Deployment is done via Docker (💻 Development Guide)". Don't copy word for word — rephrase. If the notes don't cover the topic, say so and supplement with your general knowledge.`,
contextWithNotes: `## User's notes & documents\n\n${contextNotes}\n\nWhen using info from the notes above, cite the source note title in parentheses, e.g.: "Deployment is done via Docker (💻 Development Guide)". For document passages, cite the filename and page number, e.g.: "The revenue was $5M (📄 report.pdf p.12)". Don't copy word for word — rephrase. If the notes don't cover the topic, say so and supplement with your general knowledge.`,
contextNoNotes: "No relevant notes found for this question. Answer with your general knowledge.",
system: `You are the AI assistant of Memento. The user asks you questions about their projects, technical docs, and notes. You must respond in a structured and helpful way.
@@ -159,11 +203,13 @@ Momento is an intelligent note-taking application. Key features include:
- **Lab**: Experimental AI tools for data analysis and deeper insights.
## Available tools
You have access to: note_search, note_read, web_search, web_scrape.
Only use tools if you need more information. Never invent note IDs or URLs.`,
You have access to: note_search, note_read, document_search, task_extract, web_search, web_scrape.
Only use tools if you need more information. Never invent note IDs or URLs.
- document_search: Searches attached PDF documents for the current note/notebook. Use when the user asks about documents or files.
- task_extract: Extracts action items from notes and creates a synthesis note. Use when the user asks to extract tasks or TODOs.`,
},
fr: {
contextWithNotes: `## Notes de l'utilisateur\n\n${contextNotes}\n\nQuand tu utilises une info venant des notes ci-dessus, cite le titre de la note source entre parenthèses, ex: "Le déploiement se fait via Docker (💻 Development Guide)". Ne recopie pas mot pour mot — reformule.`,
contextWithNotes: `## Notes et documents de l'utilisateur\n\n${contextNotes}\n\nQuand tu utilises une info venant des notes ci-dessus, cite le titre de la note source entre parenthèses, ex: "Le déploiement se fait via Docker (💻 Development Guide)". Pour les documents PDF, cite le nom du fichier et la page, ex: "Le chiffre d'affaires est de 5M$ (📄 rapport.pdf p.12)". Ne recopie pas mot pour mot — reformule.`,
contextNoNotes: "Aucune note pertinente trouvée pour cette question. Réponds avec tes connaissances générales.",
system: `Tu es l'assistant IA de Memento. L'utilisateur te pose des questions sur ses projets, sa doc technique, ses notes. Tu dois répondre de façon structurée et utile.
@@ -191,7 +237,9 @@ Only use tools if you need more information. Never invent note IDs or URLs.`,
Momento est une application de prise de notes intelligente. Ses fonctionnalités : Éditeur Markdown riche, Copilot IA, Organisation par Carnets, Recherche sémantique, Agents IA, Lab.
## Outils disponibles
Tu as accès à : note_search, note_read, web_search, web_scrape.`,
Tu as accès à : note_search, note_read, document_search, task_extract, web_search, web_scrape.
- document_search : Recherche dans les documents PDF attachés à la note/au carnet.
- task_extract : Extrait les tâches/action items des notes et crée une note de synthèse.`,
},
fa: {
contextWithNotes: `## یادداشت‌های کاربر\n\n${contextNotes}\n\nهنگام استفاده از اطلاعات یادداشت‌های بالا، عنوان یادداشت منبع را در پرانتز ذکر کنید.`,
@@ -276,7 +324,7 @@ Focus ONLY on this note unless asked otherwise.`
// 6. Execute stream
const sysConfig = await getSystemConfig()
const chatTools = noteContext
? toolRegistry.buildToolsForChat({ userId, config: sysConfig, webSearch, webOnly: true })
? toolRegistry.buildToolsForChat({ userId, config: sysConfig, webSearch, notebookId: notebookId || undefined })
: toolRegistry.buildToolsForChat({ userId, config: sysConfig, webSearch, notebookId: notebookId || undefined })
const provider = getChatProvider(sysConfig)

View File

@@ -0,0 +1,87 @@
import { NextRequest, NextResponse } from 'next/server'
import prisma from '@/lib/prisma'
import { auth } from '@/auth'
import fs from 'fs'
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string; attachmentId: string }> }
) {
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
try {
const { id: noteId, attachmentId } = await params
const download = request.nextUrl.searchParams.get('download')
const attachment = await prisma.noteAttachment.findFirst({
where: { id: attachmentId, noteId, note: { userId: session.user.id } },
include: download ? undefined : {
chunks: {
select: { id: true, chunkIndex: true, pageNumber: true },
orderBy: { chunkIndex: 'asc' },
},
},
})
if (!attachment) {
return NextResponse.json({ error: 'Not found' }, { status: 404 })
}
if (download) {
if (!fs.existsSync(attachment.filePath)) {
return NextResponse.json({ error: 'File not found' }, { status: 404 })
}
const fileBuffer = fs.readFileSync(attachment.filePath)
return new NextResponse(fileBuffer, {
headers: {
'Content-Type': attachment.mimeType || 'application/pdf',
'Content-Disposition': `inline; filename="${attachment.fileName}"`,
},
})
}
return NextResponse.json({ success: true, data: attachment })
} catch (error) {
console.error('Error fetching attachment:', error)
return NextResponse.json({ error: 'Failed to fetch attachment' }, { status: 500 })
}
}
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string; attachmentId: string }> }
) {
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
try {
const { id: noteId, attachmentId } = await params
const attachment = await prisma.noteAttachment.findFirst({
where: { id: attachmentId, noteId, note: { userId: session.user.id } },
})
if (!attachment) {
return NextResponse.json({ error: 'Not found' }, { status: 404 })
}
try {
if (fs.existsSync(attachment.filePath)) {
fs.unlinkSync(attachment.filePath)
}
} catch {}
await prisma.noteAttachment.delete({ where: { id: attachmentId } })
return NextResponse.json({ success: true })
} catch (error) {
console.error('Error deleting attachment:', error)
return NextResponse.json({ error: 'Failed to delete attachment' }, { status: 500 })
}
}

View File

@@ -0,0 +1,112 @@
import { NextRequest, NextResponse } from 'next/server'
import prisma from '@/lib/prisma'
import { auth } from '@/auth'
import { documentIngestionService } from '@/lib/ai/services/document-ingestion.service'
import fs from 'fs'
import path from 'path'
import { randomUUID } from 'crypto'
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
try {
const { id: noteId } = await params
const note = await prisma.note.findFirst({
where: { id: noteId, userId: session.user.id, trashedAt: null },
})
if (!note) {
return NextResponse.json({ error: 'Note not found' }, { status: 404 })
}
const formData = await request.formData()
const file = formData.get('file') as File | null
if (!file) {
return NextResponse.json({ error: 'No file provided' }, { status: 400 })
}
if (file.size > 20 * 1024 * 1024) {
return NextResponse.json({ error: 'File too large (max 20MB)' }, { status: 400 })
}
if (file.type !== 'application/pdf') {
return NextResponse.json({ error: 'Only PDF files are supported' }, { status: 400 })
}
const dir = path.join(process.cwd(), 'data', 'uploads', 'attachments', noteId)
fs.mkdirSync(dir, { recursive: true })
const fileId = randomUUID()
const filePath = path.join(dir, `${fileId}.pdf`)
fs.writeFileSync(filePath, Buffer.from(await file.arrayBuffer()))
const attachment = await prisma.noteAttachment.create({
data: {
noteId,
fileName: file.name,
fileType: file.type,
fileSize: file.size,
filePath,
mimeType: file.type,
status: 'pending',
},
})
setImmediate(() => {
documentIngestionService.ingest(attachment.id).catch((err) => {
console.error('Document ingestion failed:', err)
})
})
return NextResponse.json({ success: true, data: attachment }, { status: 201 })
} catch (error) {
console.error('Error uploading attachment:', error)
return NextResponse.json({ error: 'Upload failed' }, { status: 500 })
}
}
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
try {
const { id: noteId } = await params
const note = await prisma.note.findFirst({
where: { id: noteId, userId: session.user.id, trashedAt: null },
})
if (!note) {
return NextResponse.json({ error: 'Note not found' }, { status: 404 })
}
const attachments = await prisma.noteAttachment.findMany({
where: { noteId },
orderBy: { createdAt: 'desc' },
select: {
id: true,
fileName: true,
fileSize: true,
mimeType: true,
status: true,
pageCount: true,
error: true,
createdAt: true,
},
})
return NextResponse.json({ success: true, data: attachments })
} catch (error) {
console.error('Error fetching attachments:', error)
return NextResponse.json({ error: 'Failed to fetch attachments' }, { status: 500 })
}
}

View File

@@ -0,0 +1,29 @@
import { NextRequest, NextResponse } from 'next/server'
import prisma from '@/lib/prisma'
import { auth } from '@/auth'
export async function GET(request: NextRequest) {
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const q = request.nextUrl.searchParams.get('q') || ''
if (q.length < 2) {
return NextResponse.json({ users: [] })
}
const users = await prisma.user.findMany({
where: {
id: { not: session.user.id },
OR: [
{ email: { contains: q, mode: 'insensitive' } },
{ name: { contains: q, mode: 'insensitive' } },
],
},
select: { id: true, name: true, email: true, image: true },
take: 8,
})
return NextResponse.json({ users })
}

View File

@@ -19,7 +19,7 @@
--color-memento-ink: #1C1C1C;
--color-primary: #ACB995;
--color-memento-accent: #D4A373;
--color-memento-blue: #75B2D6;
--color-memento-blue: #A47148;
--color-memento-paper-elevated: #faf9f5;
--color-background-light: var(--color-memento-paper);
--color-background-dark: #202020;
@@ -29,7 +29,7 @@
--color-paper: var(--paper);
--color-muted-ink: var(--muted-ink);
--color-concrete: var(--concrete);
--color-blueprint: #75B2D6;
--color-blueprint: #A47148;
--color-ochre: #D4A373;
--color-sage: #A3B18A;
--color-rust: #9B2226;
@@ -273,6 +273,13 @@ html.dark .sidebar-inbox-item.active {
opacity: 0.7;
}
/* Persian/Arabic: avoid uppercase + wide tracking on mixed scripts; bidi class set in TSX */
.note-date-badge.note-date-badge--locale-rtl {
text-transform: none;
letter-spacing: 0.08em;
unicode-bidi: isolate;
}
/* AI send button accent */
.ai-send-btn {
background: var(--ai-accent);
@@ -1181,7 +1188,7 @@ html.font-system * {
.notion-editor-wrapper .ProseMirror p.is-editor-empty:first-child::before,
.notion-editor-wrapper .ProseMirror p.is-empty::before {
content: attr(data-placeholder);
float: left;
float: inline-start;
color: var(--muted-foreground);
pointer-events: none;
height: 0;
@@ -1221,13 +1228,13 @@ html.font-system * {
/* --- Lists --- */
.notion-editor-wrapper .ProseMirror ul {
list-style-type: disc;
padding-left: 1.5rem;
padding-inline-start: 1.5rem;
margin: 0.25em 0;
}
.notion-editor-wrapper .ProseMirror ol {
list-style-type: decimal;
padding-left: 1.5rem;
padding-inline-start: 1.5rem;
margin: 0.25em 0;
}
@@ -1243,7 +1250,7 @@ html.font-system * {
/* --- Task / Todo List --- */
.notion-editor-wrapper .ProseMirror ul[data-type="taskList"] {
list-style: none;
padding-left: 0;
padding-inline-start: 0;
}
.notion-editor-wrapper .ProseMirror ul[data-type="taskList"] li {
@@ -1301,13 +1308,16 @@ html.font-system * {
/* --- Blockquote --- */
.notion-editor-wrapper .ProseMirror blockquote {
border-left: 3px solid var(--primary);
padding: 0.25em 0 0.25em 1em;
border-inline-start: 3px solid var(--primary);
padding-block: 0.25em;
padding-inline-start: 1em;
padding-inline-end: 0;
margin: 0.4em 0;
color: var(--muted-foreground);
font-style: italic;
background: oklch(0.5 0 0 / 0.03);
border-radius: 0 4px 4px 0;
border-start-end-radius: 4px;
border-end-end-radius: 4px;
}
/* --- Code --- */
@@ -1983,13 +1993,13 @@ html.font-system * {
.rt-preview ul,
.rt-preview ol {
padding-left: 1.25rem;
padding-inline-start: 1.25rem;
margin: 0.2em 0;
}
.rt-preview blockquote {
border-left: 3px solid var(--border);
padding-left: 0.75rem;
border-inline-start: 3px solid var(--border);
padding-inline-start: 0.75rem;
color: var(--muted-foreground);
margin: 0.3em 0;
}
@@ -2036,7 +2046,7 @@ html.font-system * {
.fullpage-editor .ProseMirror ol {
font-size: 1.125rem;
line-height: 1.8;
padding-left: 1.5em;
padding-inline-start: 1.5em;
margin-bottom: 1em;
}
@@ -2045,8 +2055,8 @@ html.font-system * {
}
.fullpage-editor .ProseMirror blockquote {
border-left: 3px solid var(--foreground);
padding-left: 1.25rem;
border-inline-start: 3px solid var(--foreground);
padding-inline-start: 1.25rem;
font-size: 1.2rem;
font-style: italic;
font-family: var(--font-memento-serif, Georgia, serif);
@@ -2058,7 +2068,7 @@ html.font-system * {
.fullpage-editor .ProseMirror p.is-editor-empty:first-child::before {
color: var(--muted-foreground);
content: attr(data-placeholder);
float: left;
float: inline-start;
height: 0;
pointer-events: none;
font-size: 1.125rem;

View File

@@ -112,7 +112,7 @@ export default async function RootLayout({
<body className={`${inter.className} ${inter.variable} ${manrope.variable} ${playfair.variable} ${jetbrainsMono.variable}`}>
<Script
id="theme-early"
strategy="beforeInteractive"
strategy="worker"
dangerouslySetInnerHTML={{ __html: getThemeScript(userSettings.theme) }}
/>
<Script

View File

@@ -18,6 +18,7 @@ import {
Pencil,
Activity,
Presentation,
ListChecks,
} from 'lucide-react'
import { toast } from 'sonner'
import { useLanguage } from '@/lib/i18n'
@@ -51,6 +52,7 @@ const typeConfig: Record<string, { icon: typeof Globe }> = {
custom: { icon: Settings },
'slide-generator': { icon: Presentation },
'excalidraw-generator': { icon: Pencil },
'task-extractor': { icon: ListChecks },
}
const frequencyKeys: Record<string, string> = {

View File

@@ -29,13 +29,14 @@ import {
Loader2,
BookOpen,
LifeBuoy,
ListChecks,
} from 'lucide-react'
import { HierarchicalNotebookSelector } from '@/components/hierarchical-notebook-selector'
import { toast } from 'sonner'
import { useLanguage } from '@/lib/i18n'
import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'
type AgentType = 'scraper' | 'researcher' | 'monitor' | 'custom' | 'slide-generator' | 'excalidraw-generator'
type AgentType = 'scraper' | 'researcher' | 'monitor' | 'custom' | 'slide-generator' | 'excalidraw-generator' | 'task-extractor'
function FieldHelp({ tooltip }: { tooltip: string }) {
return (
@@ -59,6 +60,7 @@ const typeIcons: Record<string, typeof Globe> = {
custom: Settings,
'slide-generator': Presentation,
'excalidraw-generator': Pencil,
'task-extractor': ListChecks,
}
const TOOL_PRESETS: Record<string, string[]> = {
@@ -68,6 +70,7 @@ const TOOL_PRESETS: Record<string, string[]> = {
custom: ['memory_search'],
'slide-generator': ['generate_pptx'],
'excalidraw-generator': ['generate_excalidraw'],
'task-extractor': ['note_search', 'note_read', 'task_extract', 'note_create'],
}
interface AgentDetailViewProps {
@@ -386,6 +389,7 @@ export function AgentDetailView({
{ value: 'custom' as AgentType, labelKey: 'agents.types.custom', descKey: 'agents.typeDescriptions.custom', icon: Settings },
{ value: 'slide-generator' as AgentType, labelKey: 'agents.types.slideGenerator', descKey: 'agents.typeDescriptions.slideGenerator', icon: Presentation },
{ value: 'excalidraw-generator' as AgentType, labelKey: 'agents.types.excalidrawGenerator', descKey: 'agents.typeDescriptions.excalidrawGenerator', icon: Pencil },
{ value: 'task-extractor' as AgentType, labelKey: 'agents.types.taskExtractor', descKey: 'agents.typeDescriptions.taskExtractor', icon: ListChecks },
].map(at => {
const TypeIcon = at.icon
return (

View File

@@ -10,6 +10,7 @@ import {
Loader2,
Presentation,
Pencil,
ListChecks,
} from 'lucide-react'
import { toast } from 'sonner'
import { useLanguage } from '@/lib/i18n'
@@ -42,6 +43,7 @@ const templateConfig = [
{ id: 'chercheur', type: 'researcher', roleKey: 'agents.defaultRoles.researcher', urls: [], frequency: 'manual' },
{ id: 'slideGenerator', type: 'slide-generator', roleKey: 'agents.defaultRoles.slideGenerator', urls: [], frequency: 'manual' },
{ id: 'excalidrawGenerator', type: 'excalidraw-generator', roleKey: 'agents.defaultRoles.excalidrawGenerator', urls: [], frequency: 'manual' },
{ id: 'taskExtractor', type: 'task-extractor', roleKey: 'agents.defaultRoles.taskExtractor', urls: [], frequency: 'manual' },
] as const
const typeIcons: Record<string, typeof Globe> = {
@@ -51,6 +53,7 @@ const typeIcons: Record<string, typeof Globe> = {
custom: Settings,
'slide-generator': Presentation,
'excalidraw-generator': Pencil,
'task-extractor': ListChecks,
}
export function AgentTemplates({ onInstalled, existingAgentNames }: AgentTemplatesProps) {
@@ -87,7 +90,9 @@ export function AgentTemplates({ onInstalled, existingAgentNames }: AgentTemplat
? ['note_search', 'note_read', 'generate_pptx']
: tpl.type === 'excalidraw-generator'
? ['note_search', 'note_read', 'generate_excalidraw']
: [],
: tpl.type === 'task-extractor'
? ['note_search', 'note_read', 'task_extract', 'note_create']
: [],
})
toast.success(t('agents.toasts.installSuccess', { name: resolvedName }))
onInstalled()

View File

@@ -152,7 +152,7 @@ export function AIChat({ showFloatingTrigger = true }: { showFloatingTrigger?: b
return (
<Button
onClick={() => setIsOpen(true)}
className="fixed bottom-6 right-6 h-12 w-12 rounded-full shadow-xl z-40 transition-transform hover:scale-105 bg-muted text-foreground hover:bg-muted/80 border border-border"
className="fixed bottom-6 end-6 h-12 w-12 rounded-full shadow-xl z-40 transition-transform hover:scale-105 bg-muted text-foreground hover:bg-muted/80 border border-border"
size="icon"
title={t('ai.openAssistant')}
>
@@ -163,7 +163,7 @@ export function AIChat({ showFloatingTrigger = true }: { showFloatingTrigger?: b
return (
<aside className={cn(
"fixed bottom-20 right-6 border border-border/40 bg-memento-paper dark:bg-background flex flex-col z-40 shadow-2xl rounded-2xl overflow-hidden transition-all duration-300",
"fixed bottom-20 end-6 border border-border/40 bg-memento-paper dark:bg-background flex flex-col z-40 shadow-2xl rounded-2xl overflow-hidden transition-all duration-300",
isExpanded ? "w-[80vw] h-[85vh] max-w-[1200px]" : "h-[700px] max-h-[85vh] w-[360px]"
)}>
{/* Header */}
@@ -239,7 +239,7 @@ export function AIChat({ showFloatingTrigger = true }: { showFloatingTrigger?: b
{/* AI Welcome Message */}
{messages.length === 0 && (
<div className="flex gap-3">
<div className="w-8 h-8 rounded-full bg-memento-blue/10 text-memento-blue flex items-center justify-center flex-shrink-0 border border-memento-blue/20">
<div className="w-8 h-8 rounded-full bg-memento-blue/20 text-memento-blue flex items-center justify-center flex-shrink-0 border border-memento-blue/30 shadow-sm">
<Bot className="h-4 w-4" />
</div>
<div className="bg-memento-paper dark:bg-background border border-border/50 p-3.5 rounded-2xl rounded-tl-sm shadow-sm">
@@ -261,7 +261,7 @@ export function AIChat({ showFloatingTrigger = true }: { showFloatingTrigger?: b
'w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 border text-[10px] font-bold',
msg.role === 'user'
? 'bg-muted border-border text-muted-foreground'
: 'bg-memento-blue/10 text-memento-blue border-memento-blue/20',
: 'bg-memento-blue/20 text-memento-blue border-memento-blue/30 shadow-sm',
)}>
{msg.role === 'user' ? 'U' : <Bot className="h-4 w-4" />}
</div>
@@ -323,7 +323,7 @@ export function AIChat({ showFloatingTrigger = true }: { showFloatingTrigger?: b
history.map(conv => (
<button
key={conv.id}
className="w-full text-left p-3 rounded-xl border border-border/50 hover:bg-muted/50 hover:border-memento-blue/30 transition-all flex flex-col gap-1"
className="w-full text-start p-3 rounded-xl border border-border/50 hover:bg-muted/50 hover:border-memento-blue/30 transition-all flex flex-col gap-1"
onClick={() => {
setConversationId(conv.id)
setMessages(conv.messages.map((m: any) => ({
@@ -353,22 +353,22 @@ export function AIChat({ showFloatingTrigger = true }: { showFloatingTrigger?: b
<div className={cn("p-4 border-t border-border/40 bg-memento-paper dark:bg-background shrink-0", activeTab !== 'chat' && "hidden")}>
{/* Context Scope */}
<div className="mb-3 space-y-2">
<span className="text-[9px] font-bold uppercase tracking-widest text-muted-foreground block ml-1">Source du Contexte</span>
<span className="text-[9px] font-bold uppercase tracking-widest text-muted-foreground block ms-1">Source du Contexte</span>
<button
onClick={() => setChatScope('all')}
className={cn(
'w-full p-2.5 border rounded-lg text-xs flex items-center justify-between transition-all',
chatScope === 'all' ? 'bg-blueprint/10 border-blueprint/30' : 'bg-card border-border hover:border-foreground/20'
chatScope === 'all' ? 'bg-memento-blue/15 border-memento-blue/40 shadow-inner' : 'bg-card border-border hover:border-foreground/20'
)}
>
<div className="flex items-center gap-2">
<Layers className="h-3.5 w-3.5 text-blueprint/60" />
<span className={cn('font-medium', chatScope === 'all' ? 'text-blueprint' : 'text-foreground/60')}>
<Layers className="h-3.5 w-3.5 text-memento-blue/70" />
<span className={cn('font-bold', chatScope === 'all' ? 'text-memento-blue' : 'text-foreground/60')}>
{t('ai.allMyNotes') || 'Toutes mes notes'}
</span>
</div>
{chatScope === 'all' && (
<span className="text-[8px] bg-blueprint/10 text-blueprint px-1.5 py-0.5 rounded uppercase font-bold">Auto</span>
<span className="text-[8px] bg-memento-blue/20 text-memento-blue px-1.5 py-0.5 rounded uppercase font-bold border border-memento-blue/30">Auto</span>
)}
</button>
@@ -389,7 +389,7 @@ export function AIChat({ showFloatingTrigger = true }: { showFloatingTrigger?: b
{/* Tone Selection */}
<div className="mb-3">
<span className="text-[9px] font-bold uppercase tracking-widest text-muted-foreground block mb-1.5 ml-1">{t('ai.writingTone')}</span>
<span className="text-[9px] font-bold uppercase tracking-widest text-muted-foreground block mb-1.5 ms-1">{t('ai.writingTone')}</span>
<div className="grid grid-cols-4 gap-1">
{TONES.map(tone => {
const Icon = tone.icon
@@ -402,7 +402,7 @@ export function AIChat({ showFloatingTrigger = true }: { showFloatingTrigger?: b
className={cn(
"py-1 rounded-md border text-[10px] font-medium transition-all flex flex-col items-center justify-center gap-0.5",
isSelected
? "border-memento-blue bg-memento-blue/10 text-memento-blue shadow-sm"
? "border-memento-blue bg-memento-blue/15 text-memento-blue shadow-sm font-bold"
: "border-border/60 bg-memento-paper dark:bg-background text-muted-foreground hover:bg-muted hover:border-border"
)}
>
@@ -454,7 +454,7 @@ export function AIChat({ showFloatingTrigger = true }: { showFloatingTrigger?: b
onClick={handleSend}
disabled={!input.trim()}
>
<Send className="h-4 w-4 ml-0.5" />
<Send className="h-4 w-4 ms-0.5" />
</Button>
)}
</div>

View File

@@ -0,0 +1,120 @@
'use client'
import React from 'react'
import { motion, AnimatePresence } from 'motion/react'
import { Activity, Lightbulb, UserPlus, X, Zap, Eye } from 'lucide-react'
import type { BrainstormActivityItem } from '@/types/brainstorm'
interface ActivityFeedProps {
activities: BrainstormActivityItem[]
isOpen: boolean
onToggle: () => void
t: (key: string) => string | undefined
}
function getActionIcon(action: string) {
switch (action) {
case 'manual_idea': return <Lightbulb size={12} className="text-memento-blue" />
case 'wave_generated': return <Zap size={12} className="text-orange-500" />
case 'joined': return <UserPlus size={12} className="text-emerald-500" />
case 'idea_dismissed': return <X size={12} className="text-rose-500" />
case 'invite_created': return <UserPlus size={12} className="text-violet-500" />
default: return <Activity size={12} className="text-muted-foreground" />
}
}
function getActionLabel(action: string, t: (key: string) => string | undefined): string {
const key = `brainstorm.activity.${action}`
const translated = t(key)
if (translated) return translated
switch (action) {
case 'manual_idea': return 'added an idea'
case 'wave_generated': return 'generated a wave'
case 'joined': return 'joined the session'
case 'idea_dismissed': return 'dismissed an idea'
case 'invite_created': return 'created an invite'
default: return action
}
}
function timeAgo(dateStr: string, t: (key: string) => string | undefined): string {
const diff = Date.now() - new Date(dateStr).getTime()
const mins = Math.floor(diff / 60000)
if (mins < 1) return t('brainstorm.justNow') || 'just now'
if (mins < 60) return `${mins}m`
const hours = Math.floor(mins / 60)
if (hours < 24) return `${hours}h`
const days = Math.floor(hours / 24)
return `${days}d`
}
export function ActivityFeed({ activities, isOpen, onToggle, t }: ActivityFeedProps) {
return (
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ x: 320, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
exit={{ x: 320, opacity: 0 }}
transition={{ type: 'spring', damping: 25, stiffness: 300 }}
className="absolute top-0 right-16 bottom-0 w-[320px] bg-white/95 dark:bg-[#1A1A1A]/95 backdrop-blur-xl border-l border-border z-30 flex flex-col shadow-[-20px_0_40px_rgba(0,0,0,0.05)]"
>
<div className="p-6 border-b border-border/40 flex items-center justify-between">
<div className="flex items-center gap-2">
<Activity size={14} className="text-orange-500" />
<h3 className="text-[10px] font-bold uppercase tracking-[0.2em] text-foreground">
{t('brainstorm.activityTitle') || 'Activity'}
</h3>
</div>
<button
onClick={onToggle}
className="p-1.5 hover:bg-foreground/5 rounded-full transition-colors"
>
<X size={14} className="text-muted-foreground" />
</button>
</div>
<div className="flex-1 overflow-y-auto">
{activities.length === 0 ? (
<div className="p-6 text-center">
<p className="text-xs italic text-muted-foreground">
{t('brainstorm.noActivity') || 'No activity yet'}
</p>
</div>
) : (
<div className="py-2">
{activities.map((item, idx) => (
<motion.div
key={item.id}
initial={{ opacity: 0, x: 10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: idx * 0.03 }}
className="px-6 py-3 flex items-start gap-3 hover:bg-foreground/[0.02] transition-colors"
>
<div className="mt-0.5 w-5 h-5 rounded-full bg-foreground/5 flex items-center justify-center shrink-0">
{getActionIcon(item.action)}
</div>
<div className="flex-1 min-w-0">
<p className="text-xs text-foreground leading-relaxed">
<span className="font-semibold">
{item.user?.name || 'AI'}
</span>{' '}
{getActionLabel(item.action, t)}
{item.details?.ideaTitle && (
<span className="text-muted-foreground italic"> « {item.details.ideaTitle.length > 30 ? item.details.ideaTitle.substring(0, 30) + '…' : item.details.ideaTitle} »</span>
)}
</p>
<p className="text-[10px] text-muted-foreground mt-0.5">
{timeAgo(item.createdAt, t)}
</p>
</div>
</motion.div>
))}
</div>
)}
</div>
</motion.div>
)}
</AnimatePresence>
)
}

View File

@@ -0,0 +1,370 @@
'use client'
import React, { useCallback, useMemo, useRef, useState } from 'react'
import dynamic from 'next/dynamic'
import { BrainstormSession, BrainstormIdea } from '@/types/brainstorm'
import { useExpandIdea, useDismissIdea, useConvertIdea, useExportBrainstorm } from '@/hooks/use-brainstorm'
import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet'
import { Button } from '@/components/ui/button'
import { Sparkles, X, FileText, Download, ChevronDown } from 'lucide-react'
import { toast } from 'sonner'
const ForceGraph2D = dynamic(() => import('react-force-graph-2d'), {
ssr: false,
})
interface GraphNode {
id: string
name: string
val: number
color: string
borderColor: string
wave: number
status: string
idea: BrainstormIdea
x?: number
y?: number
__bckgDimensions?: [number, number]
}
interface GraphLink {
source: string | GraphNode
target: string | GraphNode
color: string
}
const WAVE_COLORS: Record<number, string> = {
0: '#ffffff',
1: '#f97316',
2: '#3b82f6',
3: '#a855f7',
}
const WAVE_BORDER: Record<number, string> = {
0: '#e5e5e5',
1: '#ea580c',
2: '#2563eb',
3: '#9333ea',
}
const STATUS_ALPHA: Record<string, number> = {
active: 1,
dismissed: 0.25,
converted: 0.9,
}
interface BrainstormCanvasProps {
session: BrainstormSession
}
export function BrainstormCanvas({ session }: BrainstormCanvasProps) {
const fgRef = useRef<any>(null)
const [selectedIdea, setSelectedIdea] = useState<BrainstormIdea | null>(null)
const [isSheetOpen, setIsSheetOpen] = useState(false)
const expandIdea = useExpandIdea(session.id)
const dismissIdea = useDismissIdea(session.id)
const convertIdea = useConvertIdea(session.id)
const exportBrainstorm = useExportBrainstorm(session.id)
const { graphData } = useMemo(() => {
const nodes: GraphNode[] = []
const links: GraphLink[] = []
nodes.push({
id: 'seed',
name: session.seedIdea.length > 30 ? session.seedIdea.slice(0, 30) + '...' : session.seedIdea,
val: 25,
color: WAVE_COLORS[0],
borderColor: WAVE_BORDER[0],
wave: 0,
status: 'active',
idea: {
id: 'seed',
sessionId: session.id,
waveNumber: 0,
title: session.seedIdea,
description: 'Original seed idea',
connectionToSeed: null,
noveltyScore: null,
parentIdeaId: null,
convertedToNoteId: null,
relatedNoteIds: null,
status: 'active',
positionX: null,
positionY: null,
createdAt: session.createdAt,
} as BrainstormIdea,
})
for (const idea of session.ideas) {
const parentNode = idea.parentIdeaId
? idea.parentIdeaId
: 'seed'
nodes.push({
id: idea.id,
name: idea.title,
val: idea.status === 'dismissed' ? 8 : 15,
color: WAVE_COLORS[idea.waveNumber] || WAVE_COLORS[3],
borderColor: idea.status === 'converted' ? '#22c55e' : (WAVE_BORDER[idea.waveNumber] || WAVE_BORDER[3]),
wave: idea.waveNumber,
status: idea.status,
idea,
})
const linkColor = WAVE_COLORS[idea.waveNumber] || WAVE_COLORS[3]
links.push({
source: parentNode,
target: idea.id,
color: idea.status === 'dismissed' ? '#444444' : linkColor,
})
}
return { graphData: { nodes, links } }
}, [session])
const handleNodeClick = useCallback((node: any) => {
setSelectedIdea(node.idea)
setIsSheetOpen(true)
}, [])
const handleExpand = useCallback(async () => {
if (!selectedIdea || selectedIdea.id === 'seed') return
try {
await expandIdea.mutateAsync(selectedIdea.id)
toast.success('Ideas expanded!')
} catch (err: any) {
toast.error(err.message || 'Failed to expand')
}
}, [selectedIdea, expandIdea])
const handleDismiss = useCallback(async () => {
if (!selectedIdea || selectedIdea.id === 'seed') return
try {
await dismissIdea.mutateAsync(selectedIdea.id)
setIsSheetOpen(false)
setSelectedIdea(null)
toast.success('Idea dismissed')
} catch (err: any) {
toast.error(err.message || 'Failed to dismiss')
}
}, [selectedIdea, dismissIdea])
const handleConvert = useCallback(async () => {
if (!selectedIdea || selectedIdea.id === 'seed') return
try {
await convertIdea.mutateAsync(selectedIdea.id)
toast.success('Idea converted to note!')
} catch (err: any) {
toast.error(err.message || 'Failed to convert')
}
}, [selectedIdea, convertIdea])
const handleExport = useCallback(async () => {
try {
const note = await exportBrainstorm.mutateAsync()
toast.success('Exported as note!')
} catch (err: any) {
toast.error(err.message || 'Failed to export')
}
}, [exportBrainstorm])
const paintNode = useCallback((node: any, ctx: CanvasRenderingContext2D, globalScale: number) => {
const label = node.name
const fontSize = Math.max(12 / globalScale, 4)
ctx.font = `${fontSize}px Sans-Serif`
const textWidth = ctx.measureText(label).width
const nodeSize = node.val
const bgDimensions: [number, number] = [textWidth + nodeSize * 0.8, nodeSize * 1.2]
node.__bckgDimensions = bgDimensions
const alpha = STATUS_ALPHA[node.status] || 1
ctx.globalAlpha = alpha
ctx.fillStyle = node.color
ctx.strokeStyle = node.borderColor
ctx.lineWidth = node.status === 'converted' ? 3 / globalScale : 1.5 / globalScale
ctx.beginPath()
ctx.roundRect(
node.x! - bgDimensions[0] / 2,
node.y! - bgDimensions[1] / 2,
bgDimensions[0],
bgDimensions[1],
nodeSize * 0.3
)
ctx.fill()
ctx.stroke()
ctx.fillStyle = node.wave === 0 ? '#000000' : '#ffffff'
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
ctx.fillText(label, node.x!, node.y!)
if (node.status === 'converted') {
ctx.fillStyle = '#22c55e'
ctx.font = `${fontSize * 1.2}px Sans-Serif`
ctx.fillText('✓', node.x! + bgDimensions[0] / 2 - fontSize * 0.5, node.y! - bgDimensions[1] / 2 + fontSize * 0.5)
}
ctx.globalAlpha = 1
}, [])
const waveLegend = [
{ label: 'Seed', color: WAVE_COLORS[0] },
{ label: 'Variations', color: WAVE_COLORS[1] },
{ label: 'Analogies', color: WAVE_COLORS[2] },
{ label: 'Disruptions', color: WAVE_COLORS[3] },
]
return (
<div className="relative w-full h-full bg-zinc-950 rounded-xl overflow-hidden">
<ForceGraph2D
ref={fgRef}
graphData={graphData}
nodeCanvasObject={paintNode}
nodePointerAreaPaint={(node: any, color: string, ctx: CanvasRenderingContext2D) => {
const dims = node.__bckgDimensions
if (!dims) return
ctx.fillStyle = color
ctx.beginPath()
ctx.roundRect(node.x! - dims[0] / 2, node.y! - dims[1] / 2, dims[0], dims[1], (node.val || 10) * 0.3)
ctx.fill()
}}
onNodeClick={handleNodeClick}
linkColor={(link: any) => link.color}
linkWidth={1}
linkDirectionalArrowLength={3}
linkDirectionalArrowRelPos={1}
backgroundColor="#09090b"
nodeVal={(node: any) => node.val}
cooldownTicks={100}
enableNodeDrag={true}
enableZoomInteraction={true}
enablePanInteraction={true}
warmupTicks={50}
/>
<div className="absolute top-4 left-4 flex flex-col gap-1.5">
{waveLegend.map(w => (
<div key={w.label} className="flex items-center gap-2 text-xs text-zinc-400">
<div className="w-3 h-3 rounded-sm border border-zinc-600" style={{ backgroundColor: w.color }} />
<span>{w.label}</span>
</div>
))}
</div>
<div className="absolute top-4 right-4">
<Button
size="sm"
variant="outline"
className="bg-zinc-800 border-zinc-700 text-zinc-300 hover:bg-zinc-700 hover:text-white"
onClick={handleExport}
disabled={exportBrainstorm.isPending}
>
<Download size={14} className="mr-1" />
{exportBrainstorm.isPending ? 'Exporting...' : 'Export'}
</Button>
</div>
<Sheet open={isSheetOpen} onOpenChange={setIsSheetOpen}>
<SheetContent className="bg-zinc-900 border-zinc-800 text-white w-96">
{selectedIdea && (
<>
<SheetHeader>
<SheetTitle className="text-white text-left">
{selectedIdea.id === 'seed' ? session.seedIdea : selectedIdea.title}
</SheetTitle>
</SheetHeader>
<div className="mt-6 space-y-4">
{selectedIdea.description && selectedIdea.id !== 'seed' && (
<div>
<p className="text-xs text-zinc-500 uppercase tracking-wider mb-1">Description</p>
<p className="text-sm text-zinc-300">{selectedIdea.description}</p>
</div>
)}
{selectedIdea.connectionToSeed && (
<div>
<p className="text-xs text-zinc-500 uppercase tracking-wider mb-1">Connection</p>
<p className="text-sm text-zinc-300">{selectedIdea.connectionToSeed}</p>
</div>
)}
{selectedIdea.noveltyScore && (
<div>
<p className="text-xs text-zinc-500 uppercase tracking-wider mb-1">Novelty</p>
<div className="flex items-center gap-2">
<div className="flex-1 h-2 bg-zinc-800 rounded-full overflow-hidden">
<div
className="h-full bg-gradient-to-r from-orange-500 via-memento-blue to-purple-500 rounded-full"
style={{ width: `${selectedIdea.noveltyScore * 10}%` }}
/>
</div>
<span className="text-sm text-zinc-400">{selectedIdea.noveltyScore}/10</span>
</div>
</div>
)}
{selectedIdea.waveNumber > 0 && (
<div>
<p className="text-xs text-zinc-500 uppercase tracking-wider mb-1">Wave</p>
<span
className="inline-block px-2 py-0.5 rounded text-xs font-medium"
style={{
backgroundColor: WAVE_COLORS[selectedIdea.waveNumber] + '30',
color: WAVE_COLORS[selectedIdea.waveNumber],
}}
>
{selectedIdea.waveNumber === 1 ? 'Variation' : selectedIdea.waveNumber === 2 ? 'Analogy' : 'Disruption'}
</span>
</div>
)}
{selectedIdea.status === 'converted' && (
<div className="p-3 bg-green-500/10 border border-green-500/20 rounded-lg">
<p className="text-xs text-green-400 flex items-center gap-1">
<FileText size={12} />
Converted to note
</p>
</div>
)}
{selectedIdea.id !== 'seed' && selectedIdea.status === 'active' && (
<div className="flex flex-col gap-2 pt-4 border-t border-zinc-800">
<Button
onClick={handleExpand}
disabled={expandIdea.isPending}
className="w-full bg-orange-600 hover:bg-orange-700 text-white"
>
<Sparkles size={14} className="mr-1" />
{expandIdea.isPending ? 'Generating...' : 'Dig Deeper'}
</Button>
<Button
onClick={handleConvert}
disabled={convertIdea.isPending}
className="w-full bg-memento-blue hover:bg-blue-700 text-white"
>
<FileText size={14} className="mr-1" />
{convertIdea.isPending ? 'Converting...' : 'Create Note'}
</Button>
<Button
onClick={handleDismiss}
disabled={dismissIdea.isPending}
variant="outline"
className="w-full border-zinc-700 text-zinc-400 hover:text-white hover:bg-zinc-800"
>
<X size={14} className="mr-1" />
Dismiss
</Button>
</div>
)}
</div>
</>
)}
</SheetContent>
</Sheet>
</div>
)
}

View File

@@ -0,0 +1,84 @@
'use client'
import React, { useState } from 'react'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { Sparkles } from 'lucide-react'
import { useLanguage } from '@/lib/i18n'
interface BrainstormCreateDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
onSubmit: (seedIdea: string) => void
isLoading?: boolean
}
export function BrainstormCreateDialog({
open,
onOpenChange,
onSubmit,
isLoading,
}: BrainstormCreateDialogProps) {
const { t } = useLanguage()
const [seedIdea, setSeedIdea] = useState('')
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (!seedIdea.trim()) return
onSubmit(seedIdea.trim())
setSeedIdea('')
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md bg-white dark:bg-zinc-900 border-border">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Sparkles size={18} className="text-orange-500" />
{t('brainstorm.newBrainstorm')}
</DialogTitle>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="text-xs text-muted-foreground uppercase tracking-wider mb-1 block">
{t('brainstorm.seedLabel')}
</label>
<textarea
value={seedIdea}
onChange={(e) => setSeedIdea(e.target.value)}
placeholder={t('brainstorm.ideaPromptDetailed')}
className="w-full h-28 px-3 py-2 text-sm border border-border rounded-lg bg-transparent focus:outline-none focus:ring-2 focus:ring-orange-500/30 resize-none"
autoFocus
/>
</div>
<div className="flex justify-end gap-2">
<Button
type="button"
variant="outline"
onClick={() => onOpenChange(false)}
>
{t('brainstorm.cancel')}
</Button>
<Button
type="submit"
disabled={!seedIdea.trim() || isLoading}
className="bg-orange-600 hover:bg-orange-700 text-white"
>
{isLoading ? (
<>
<Sparkles size={14} className="mr-1 animate-spin" />
{t('brainstorm.generating')}
</>
) : (
<>
<Sparkles size={14} className="mr-1" />
{t('brainstorm.startBrainstorm')}
</>
)}
</Button>
</div>
</form>
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,817 @@
'use client'
import React, { useState, useMemo, useEffect, useRef, useCallback } from 'react'
import { useRouter, useSearchParams } from 'next/navigation'
import { useSession } from 'next-auth/react'
import { motion, AnimatePresence } from 'motion/react'
import {
Zap,
History,
Plus,
Wind,
FileText,
ChevronRight,
UserPlus,
Activity,
Download,
Share2,
Check,
Users,
Globe,
Lock,
} from 'lucide-react'
import dynamic from 'next/dynamic'
import { useLanguage } from '@/lib/i18n'
import {
useBrainstormSession,
useBrainstormSessions,
useCreateBrainstorm,
useExpandIdea,
useDismissIdea,
useConvertIdea,
useExportBrainstorm,
useFinalizeBrainstorm,
useDeleteBrainstorm,
useJoinBrainstorm,
useAddManualIdea,
useBrainstormActivity,
useUpdateBrainstormSettings,
} from '@/hooks/use-brainstorm'
import { useBrainstormSocket } from '@/hooks/use-brainstorm-socket'
import { LiveCursors, PresenceAvatars, useCursorTracking } from '@/components/brainstorm/live-cursors'
import { ActivityFeed } from '@/components/brainstorm/activity-feed'
import { BrainstormShareDialog } from '@/components/brainstorm/brainstorm-share-dialog'
import { GhostCursor } from '@/components/brainstorm/ghost-cursor'
import { PlaybackBar } from '@/components/brainstorm/playback-bar'
import type { BrainstormIdea, BrainstormNoteRef, BrainstormActivityItem } from '@/types/brainstorm'
const WaveCanvas = dynamic(
() =>
import('@/components/brainstorm/wave-canvas').then((m) => ({
default: m.WaveCanvas,
})),
{ ssr: false }
)
function CursorTrackerEffect({ containerRef, moveCursor }: { containerRef: React.RefObject<HTMLDivElement | null>; moveCursor: (c: { x: number; y: number } | null) => void }) {
useCursorTracking(containerRef, moveCursor)
return null
}
const WAVE_COLORS: Record<number, { border: string; bg: string; text: string }> = {
1: { border: 'border-orange-200', bg: 'bg-orange-50', text: 'text-orange-600' },
2: { border: 'border-memento-blue', bg: 'bg-memento-blue', text: 'text-memento-blue' },
3: { border: 'border-violet-200', bg: 'bg-violet-50', text: 'text-violet-600' },
}
export function BrainstormPage() {
const router = useRouter()
const searchParams = useSearchParams()
const { t, language } = useLanguage()
const { data: authSession } = useSession()
const urlSessionId = searchParams.get('session')
const urlSeed = searchParams.get('seed')
const urlSourceNoteId = searchParams.get('sourceNoteId')
const urlInviteToken = searchParams.get('invite')
const [seedInput, setSeedInput] = useState('')
const [selectedIdeaId, setSelectedIdeaId] = useState<string | null>(null)
const [activeSessionId, setActiveSessionId] = useState<string | null>(urlSessionId)
const [autoStarted, setAutoStarted] = useState(false)
useEffect(() => {
if (urlSessionId && urlSessionId !== activeSessionId) {
setActiveSessionId(urlSessionId)
}
}, [urlSessionId])
const [showActivityFeed, setShowActivityFeed] = useState(false)
const [showShareDialog, setShowShareDialog] = useState(false)
const [manualEditCount, setManualEditCount] = useState(0)
const [shareStatus, setShareStatus] = useState<'idle' | 'copying' | 'copied'>('idle')
const { data: sessions, isLoading: sessionsLoading } = useBrainstormSessions()
const { data: sessionResult, isLoading: sessionLoading } = useBrainstormSession(activeSessionId)
const session = sessionResult?.session || null
const sessionMeta = sessionResult?.meta
const isGuest = sessionMeta?.role === 'guest'
const canEdit = sessionMeta?.canEdit ?? true
const updateSettings = useUpdateBrainstormSettings(activeSessionId || '')
const createBrainstorm = useCreateBrainstorm()
const expandIdea = useExpandIdea(activeSessionId || '')
const dismissIdea = useDismissIdea(activeSessionId || '')
const convertIdea = useConvertIdea(activeSessionId || '')
const exportBrainstorm = useExportBrainstorm(activeSessionId || '')
const finalizeBrainstorm = useFinalizeBrainstorm(activeSessionId || '')
const deleteBrainstorm = useDeleteBrainstorm()
const joinMutation = useJoinBrainstorm()
const addManualIdea = useAddManualIdea(activeSessionId || '')
const { data: activities } = useBrainstormActivity(activeSessionId)
const [impactToast, setImpactToast] = useState<{ notesEnriched: number; notesMarkedDry: number } | null>(null)
const [exportError, setExportError] = useState<string | null>(null)
const [exportToast, setExportToast] = useState<{ noteTitle: string; notebookName: string } | null>(null)
const [convertToast, setConvertToast] = useState<{ noteTitle: string; noteId: string } | null>(null)
const [remoteMove, setRemoteMove] = useState<{ ideaId: string; x: number; y: number; _seq: number } | null>(null)
const [playbackIdeas, setPlaybackIdeas] = useState<any[] | null>(null)
const canvasContainerRef = useRef<HTMLDivElement>(null)
const moveSeq = useRef(0)
const { others: socketOthers, moveCursor, activities: socketActivities, socketRef, aiProcessingNodeId } = useBrainstormSocket(
activeSessionId,
authSession?.user?.id || null,
authSession?.user?.name || null,
useCallback((data: { ideaId: string; positionX: number; positionY: number }) => {
moveSeq.current++
setRemoteMove({ ideaId: data.ideaId, x: data.positionX, y: data.positionY, _seq: moveSeq.current })
}, [])
)
const mergedActivities: BrainstormActivityItem[] = useMemo(() => {
const restActivities: BrainstormActivityItem[] = (activities || []) as BrainstormActivityItem[]
const socketActs = (socketActivities || []).map((a: any) => ({
id: `${a.userId}-${a.action}-${Date.now()}-${Math.random()}`,
action: a.action,
details: a.details,
createdAt: new Date().toISOString(),
user: { name: a.userName || null, image: null },
}))
const seen = new Set<string>()
const merged = [...socketActs, ...restActivities]
return merged.filter((a) => {
const key = `${a.action}-${a.user?.name}-${a.details?.ideaTitle || ''}`
if (seen.has(key)) return false
seen.add(key)
return true
}).slice(0, 50)
}, [activities, socketActivities])
const selectedIdea = useMemo(() => {
if (!selectedIdeaId || !session) return null
return session.ideas.find((i) => i.id === selectedIdeaId) || null
}, [selectedIdeaId, session])
useEffect(() => {
if (urlSeed && !autoStarted && !activeSessionId && !createBrainstorm.isPending) {
setAutoStarted(true)
createBrainstorm.mutateAsync({
seedIdea: urlSeed,
sourceNoteId: urlSourceNoteId || undefined,
locale: language,
}).then((result) => {
setActiveSessionId(result.session.id)
router.replace('/brainstorm?session=' + result.session.id)
}).catch(() => {})
}
}, [urlSeed, autoStarted, activeSessionId])
useEffect(() => {
if (urlInviteToken && !autoStarted) {
setAutoStarted(true)
joinMutation.mutateAsync(urlInviteToken).then((result) => {
setActiveSessionId(result.sessionId)
router.replace('/brainstorm?session=' + result.sessionId)
}).catch(() => {})
}
}, [urlInviteToken])
const handleStartBrainstorm = async (seed?: string) => {
const input = seed || seedInput
if (!input.trim()) return
try {
const result = await createBrainstorm.mutateAsync({ seedIdea: input.trim(), locale: language })
setActiveSessionId(result.session.id)
setSeedInput('')
} catch {}
}
const handleDelete = async (sessionId: string) => {
try {
await deleteBrainstorm.mutateAsync(sessionId)
if (activeSessionId === sessionId) {
setActiveSessionId(null)
setSelectedIdeaId(null)
}
} catch {}
}
const handleDeepen = async (idea: BrainstormIdea) => {
try {
await expandIdea.mutateAsync({ ideaId: idea.id, locale: language })
} catch {}
}
const handleDismiss = async (ideaId: string) => {
try {
await dismissIdea.mutateAsync(ideaId)
setSelectedIdeaId(null)
} catch {}
}
const handleConvert = async (idea: BrainstormIdea) => {
try {
const result = await convertIdea.mutateAsync(idea.id)
if (result?.id) {
setConvertToast({ noteTitle: result.title || idea.title, noteId: result.id })
setTimeout(() => {
setConvertToast(null)
router.push(`/?openNote=${result.id}`)
}, 2000)
}
} catch {}
}
const handleExport = async () => {
setExportError(null)
try {
const result = await exportBrainstorm.mutateAsync()
if (result?.id) {
const notebookName = result._notebookName || 'Brainstorm'
setExportToast({ noteTitle: result.title || 'Synthèse', notebookName })
setTimeout(() => {
setExportToast(null)
router.push(`/?openNote=${result.id}`)
}, 2000)
return
}
const impact = await finalizeBrainstorm.mutateAsync()
if (impact) {
setImpactToast(impact)
setTimeout(() => setImpactToast(null), 5000)
}
} catch (err: any) {
setExportError(err?.message || 'Export failed')
setTimeout(() => setExportError(null), 4000)
}
}
const handlePositionUpdate = async (id: string, pos: { x: number; y: number }) => {
if (!activeSessionId) return
socketRef.current?.emit('idea:moved', { ideaId: id, positionX: pos.x, positionY: pos.y, userId: authSession?.user?.id || '' })
try {
await fetch(`/api/brainstorm/${activeSessionId}/update-position`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ ideaId: id, positionX: pos.x, positionY: pos.y }),
})
} catch {}
}
const handleCreateIdea = useCallback(({ title, parentIdeaId }: { title: string; parentIdeaId?: string; x: number; y: number }) => {
addManualIdea.mutate({ title, parentIdeaId, locale: language })
}, [addManualIdea, language])
const isGenerating = createBrainstorm.isPending || expandIdea.isPending
return (
<div className="h-full flex flex-col bg-[#F8F7F2] dark:bg-[#0A0A0A] overflow-hidden">
<div className="p-12 border-b border-border/20 backdrop-blur-md bg-white/20 dark:bg-black/20 z-10 relative overflow-hidden">
<div
className="absolute inset-0 pointer-events-none opacity-[0.03] dark:opacity-[0.05]"
style={{
backgroundImage:
'linear-gradient(#000 1px, transparent 1px), linear-gradient(90deg, #000 1px, transparent 1px)',
backgroundSize: '40px 40px',
}}
/>
<div className="max-w-4xl mx-auto relative">
<div className="flex items-center gap-5 mb-8">
<motion.div
animate={{ rotate: isGenerating ? 360 : 0 }}
transition={{
repeat: isGenerating ? Infinity : 0,
duration: 20,
ease: 'linear',
}}
className="w-14 h-14 rounded-2xl bg-orange-500 shadow-[0_0_20px_rgba(249,115,22,0.2)] flex items-center justify-center text-white"
>
<Wind size={28} />
</motion.div>
<div className="flex-1">
<h1 className="text-4xl font-serif font-medium text-foreground tracking-tight">
{t('brainstorm.title') || 'Waves of Thought'}
</h1>
<div className="flex items-center gap-2 mt-1">
<span className="w-8 h-px bg-orange-400/40" />
<p className="text-[10px] text-muted-foreground tracking-[0.3em] uppercase font-bold">
{t('brainstorm.subtitle') || 'Unfold dimensions of potentiality'}
</p>
</div>
</div>
{session && !isGuest && (
<div className="flex items-center gap-3">
<button
onClick={handleExport}
disabled={exportBrainstorm.isPending}
className="flex items-center gap-2 px-4 py-2 bg-white dark:bg-white/5 border border-border rounded-xl text-xs font-bold uppercase tracking-widest text-muted-foreground hover:text-orange-500 transition-all shadow-sm disabled:opacity-50"
title={t('brainstorm.export') || 'Export'}
>
<Download size={14} />
<span className="hidden sm:inline">{t('brainstorm.export') || 'Export'}</span>
</button>
<button
onClick={() => setShowActivityFeed(!showActivityFeed)}
className={`flex items-center gap-2 px-4 py-2 border border-border rounded-xl text-xs font-bold uppercase tracking-widest transition-all shadow-sm ${showActivityFeed ? 'bg-foreground text-background' : 'bg-white dark:bg-white/5 text-muted-foreground hover:text-foreground'}`}
>
<Activity size={14} />
<span className="hidden sm:inline">{t('brainstorm.activityTitle') || 'Activity'}</span>
</button>
<button
onClick={() => setShowShareDialog(true)}
className="flex items-center gap-2 px-4 py-2 bg-white dark:bg-white/5 border border-border rounded-xl text-xs font-bold uppercase tracking-widest text-muted-foreground hover:text-foreground transition-all shadow-sm"
>
<Share2 size={14} />
<span className="hidden sm:inline">{t('brainstorm.invite') || 'Invite'}</span>
</button>
<div className="flex items-center px-3 py-1.5 bg-white dark:bg-white/5 border border-border rounded-xl shadow-sm transition-all hover:border-emerald-500/30">
<div className="flex items-center gap-2 mr-3" title="Live Collaboration">
<div className="relative flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-emerald-500 shadow-[0_0_8px_rgba(16,185,129,0.6)]"></span>
</div>
<span className="text-[10px] font-bold uppercase tracking-widest text-emerald-600 dark:text-emerald-400 hidden sm:inline-block">Live</span>
</div>
<div className="flex items-center group/avatars">
{authSession?.user?.name && (
<div
className="w-6 h-6 rounded-full flex items-center justify-center text-[10px] font-bold text-white ring-2 ring-white dark:ring-[#1A1A1A] shadow-sm bg-memento-blue z-10 transition-transform hover:scale-110 hover:z-20"
title={`${authSession.user.name} (You)`}
>
{authSession.user.name.charAt(0).toUpperCase()}
</div>
)}
{socketOthers.slice(0, 3).map((user, idx) => (
<div
key={user.userId}
className="w-6 h-6 rounded-full flex items-center justify-center text-[10px] font-bold text-white ring-2 ring-white dark:ring-[#1A1A1A] shadow-sm transition-transform hover:scale-110 hover:z-20"
style={{ backgroundColor: user.color, marginLeft: '-8px', zIndex: 9 - idx }}
title={user.name}
>
{user.name.charAt(0).toUpperCase()}
</div>
))}
{socketOthers.length > 3 && (
<div
className="w-6 h-6 rounded-full flex items-center justify-center text-[10px] font-bold text-muted-foreground bg-black/5 dark:bg-white/10 ring-2 ring-white dark:ring-[#1A1A1A] shadow-sm"
style={{ marginLeft: '-8px', zIndex: 5 }}
title={`${socketOthers.length - 3} other participants`}
>
+{socketOthers.length - 3}
</div>
)}
</div>
</div>
</div>
)}
</div>
<div className="relative group">
<div className="absolute -inset-1 bg-gradient-to-r from-orange-500/20 to-memento-blue/20 rounded-[28px] blur-xl opacity-0 group-focus-within:opacity-100 transition-opacity duration-700" />
<input
type="text"
value={seedInput}
onChange={(e) => setSeedInput(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleStartBrainstorm()}
placeholder={t('brainstorm.placeholder') || 'Enter a concept to unfold...'}
className="w-full relative bg-white dark:bg-[#1A1A1A] border-2 rounded-2xl px-8 py-7 pr-20 outline-none transition-all text-2xl font-serif italic text-foreground shadow-sm group-hover:shadow-md border-border/40 focus:border-orange-400/40 focus:ring-4 focus:ring-orange-500/5"
/>
<button
onClick={() => handleStartBrainstorm()}
disabled={isGenerating || !seedInput.trim()}
className="absolute right-4 top-4 bottom-4 px-6 bg-foreground dark:bg-orange-500 text-background rounded-xl disabled:opacity-50 transition-all hover:scale-[1.02] active:scale-[0.98] flex items-center justify-center gap-2 min-w-[70px] shadow-lg"
>
{isGenerating ? (
<div className="w-6 h-6 border-3 border-white/30 border-t-white rounded-full animate-spin" />
) : (
<Plus size={24} />
)}
</button>
</div>
<AnimatePresence>
{createBrainstorm.isPending && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="mt-6 flex items-center gap-4 text-orange-500/80 italic font-serif"
>
<div className="flex gap-1.5">
{[0.2, 0.4, 0.6].map((d, i) => (
<motion.div
key={i}
animate={{ scale: [1, 1.5, 1], opacity: [0.3, 1, 0.3] }}
transition={{ duration: 1.5, repeat: Infinity, delay: d }}
className="w-1.5 h-1.5 rounded-full bg-orange-500"
/>
))}
</div>
<span className="text-base tracking-tight">
{t('brainstorm.generating') || 'AI is harvesting seeds of thought...'}
</span>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
<div className="flex-1 flex overflow-hidden relative">
<div className="flex-1 relative" ref={canvasContainerRef}>
{session && <LiveCursors others={socketOthers} />}
<GhostCursor
isActive={isGenerating || !!aiProcessingNodeId}
targetId={aiProcessingNodeId || (expandIdea.isPending ? selectedIdeaId : null)}
containerRef={canvasContainerRef}
/>
<CursorTrackerEffect containerRef={canvasContainerRef} moveCursor={moveCursor} />
{sessionLoading ? (
<div className="absolute inset-0 flex items-center justify-center">
<div className="w-8 h-8 border-2 border-foreground/20 border-t-foreground rounded-full animate-spin" />
</div>
) : session ? (
<WaveCanvas
session={session}
onNodeSelect={setSelectedIdeaId}
onPositionUpdate={handlePositionUpdate}
selectedNodeId={selectedIdeaId}
onCreateIdea={handleCreateIdea}
remoteMove={remoteMove}
manualEditTrigger={manualEditCount}
playbackIdeas={playbackIdeas}
/>
) : (
<div className="absolute inset-0 flex items-center justify-center pointer-events-none opacity-20 flex-col gap-6">
<Wind size={120} strokeWidth={1} className="text-muted-foreground animate-pulse" />
<p className="text-xl font-serif italic text-muted-foreground">
The canvas is waiting for your spark...
</p>
</div>
)}
<AnimatePresence>
{session && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="absolute bottom-6 left-6 flex gap-2"
>
<div className="px-4 py-2 bg-[#F4F1EA] dark:bg-black/60 backdrop-blur-xl border border-border shadow-xl rounded-full flex items-center gap-6">
{[1, 2, 3].map((w) => (
<div key={w} className="flex items-center gap-2">
<div
className="w-2 h-2 rounded-full"
style={{
backgroundColor: WAVE_COLORS[w]?.border?.replace('border-', '') === 'orange-200' ? '#fb923c' : w === 2 ? '#60a5fa' : '#a78bfa',
boxShadow: `0 0 8px ${w === 1 ? 'rgba(251,146,60,0.6)' : w === 2 ? 'rgba(96,165,250,0.6)' : 'rgba(167,139,250,0.6)'}`,
}}
/>
<span className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
Wave {w}
</span>
</div>
))}
</div>
{canEdit && (
<button
onClick={() => setManualEditCount((c) => c + 1)}
className="px-6 py-3 bg-[#F4F1EA] dark:bg-black/60 backdrop-blur-xl border border-border shadow-xl rounded-full flex items-center gap-2 text-[10px] font-bold uppercase tracking-widest text-muted-foreground hover:bg-foreground hover:text-background transition-all"
>
<Plus size={14} />
{t('brainstorm.addManualIdea') || 'Add Manual Idea'}
</button>
)}
</motion.div>
)}
</AnimatePresence>
{session && (
<ActivityFeed
activities={mergedActivities}
isOpen={showActivityFeed}
onToggle={() => setShowActivityFeed(!showActivityFeed)}
t={t}
/>
)}
{session && activeSessionId && (
<PlaybackBar
sessionId={activeSessionId}
onSnapshotSelect={setPlaybackIdeas}
onExitPlayback={() => setPlaybackIdeas(null)}
/>
)}
</div>
<AnimatePresence>
{selectedIdea && (
<motion.div
initial={{ x: '100%' }}
animate={{ x: 0 }}
exit={{ x: '100%' }}
className="w-[400px] border-l border-border bg-white dark:bg-[#1A1A1A] flex flex-col z-20 shadow-[-20px_0_40px_rgba(0,0,0,0.05)]"
>
<div className="p-8 flex-1 overflow-y-auto">
<div className="flex items-center justify-between mb-8">
<div
className={`px-3 py-1 rounded-full text-[10px] font-bold uppercase tracking-widest border ${
selectedIdea.waveNumber === 1
? 'border-orange-300 dark:border-orange-700 bg-orange-50 dark:bg-orange-500/15 text-orange-600 dark:text-orange-400'
: selectedIdea.waveNumber === 2
? 'border-memento-blue dark:border-blue-700 bg-memento-blue dark:bg-memento-blue/15 text-memento-blue dark:text-memento-blue'
: 'border-violet-300 dark:border-violet-700 bg-violet-50 dark:bg-violet-500/15 text-violet-600 dark:text-violet-400'
}`}
>
{t('brainstorm.wave') || 'Wave'} {selectedIdea.waveNumber}
</div>
<div className="flex items-center gap-2">
{selectedIdea.status === 'converted' && (
<span className="text-[10px] font-bold text-emerald-500 uppercase tracking-widest bg-emerald-500/10 px-2 py-1 rounded-full">
{t('brainstorm.noteCreated') || 'Note Created'}
</span>
)}
<button
onClick={() => setSelectedIdeaId(null)}
className="p-2 hover:bg-foreground/5 rounded-full transition-colors text-muted-foreground"
>
<ChevronRight size={20} />
</button>
</div>
</div>
<h2 className="text-3xl font-serif font-medium text-foreground mb-2">
{selectedIdea.title}
</h2>
<div className="flex items-center gap-4 mb-8">
<div className="flex items-center gap-1">
<Zap size={14} className="text-orange-500" />
<span className="text-xs font-bold text-muted-foreground">
{t('brainstorm.novelty') || 'Novelty'}: {selectedIdea.noveltyScore || 'N/A'}/10
</span>
</div>
<div className="flex items-center gap-1.5">
{selectedIdea.createdByType === 'human' ? (
<>
<div className="w-4 h-4 rounded-full bg-memento-blue flex items-center justify-center text-[8px] font-bold text-white">
{(selectedIdea as any).creator?.name?.charAt(0)?.toUpperCase() || 'U'}
</div>
<span className="text-[10px] font-bold uppercase tracking-widest text-memento-blue">
{t('brainstorm.humanIdea') || 'Human'}
</span>
</>
) : (
<>
<span className="text-violet-500 text-xs"></span>
<span className="text-[10px] font-bold uppercase tracking-widest text-violet-500">
{t('brainstorm.aiIdea') || 'AI'}
</span>
</>
)}
</div>
</div>
<p className="text-foreground/80 leading-relaxed font-light mb-10 text-lg">
{selectedIdea.description}
</p>
{selectedIdea.connectionToSeed && (
<div className="p-6 bg-slate-50 dark:bg-white/[0.03] rounded-2xl border border-border/40 mb-10">
<h4 className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground mb-3">
{t('brainstorm.originConnection') || 'Origin connection'}
</h4>
<p className="text-sm italic text-muted-foreground leading-relaxed">
&ldquo;{selectedIdea.connectionToSeed}&rdquo;
</p>
</div>
)}
{selectedIdea.noteRefs && selectedIdea.noteRefs.length > 0 && (
<div className="space-y-3 mb-10">
<h4 className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground flex items-center gap-1">
{t('brainstorm.ideaOrigin') || 'Origin of the idea'}
</h4>
{selectedIdea.noteRefs.map((ref: BrainstormNoteRef) => {
const isPositive = ref.relation === 'derived_from' || ref.relation === 'extends'
const isNegative = ref.relation === 'opposes'
const badgeColor = isPositive
? 'bg-emerald-500/10 dark:bg-emerald-500/20 text-emerald-600 dark:text-emerald-400 border-emerald-200 dark:border-emerald-700'
: isNegative
? 'bg-rose-500/10 dark:bg-rose-500/20 text-rose-600 dark:text-rose-400 border-rose-200 dark:border-rose-700'
: 'bg-amber-500/10 dark:bg-amber-500/20 text-amber-600 dark:text-amber-400 border-amber-200 dark:border-amber-700'
const relLabel = t(`brainstorm.${ref.relation}`) || ref.relation
const vis = (ref as any).visibility || 'participants'
const isRestricted = vis === 'owner_only'
return (
<div
key={ref.id}
className="p-4 rounded-xl border border-border bg-white dark:bg-white/5"
>
<div className="flex items-start gap-3">
<span className={`shrink-0 px-2 py-0.5 rounded-full text-[9px] font-bold uppercase tracking-wider border ${badgeColor}`}>
{relLabel}
</span>
{isRestricted && !isGuest && (
<span className="shrink-0 px-1.5 py-0.5 rounded-full text-[8px] font-bold uppercase tracking-wider border border-amber-200 dark:border-amber-700 bg-amber-500/10 text-amber-600 dark:text-amber-400 flex items-center gap-0.5">
<Lock size={8} /> Owner
</span>
)}
<div className="flex-1 min-w-0">
{ref.note ? (
<p className="text-sm font-medium text-foreground truncate">
{ref.note.title || 'Untitled'}
</p>
) : (
<p className="text-sm italic text-muted-foreground">
{t('brainstorm.noNoteLink') || 'Purely generative idea'}
</p>
)}
<p className="text-xs italic text-muted-foreground mt-1 leading-relaxed">
{ref.explanation}
</p>
</div>
{ref.noteId && (
<button
onClick={() => router.push(`/?openNote=${ref.noteId}`)}
className="shrink-0 px-2 py-1 text-[9px] font-bold uppercase tracking-wider rounded-lg bg-foreground/5 hover:bg-foreground/10 text-muted-foreground hover:text-foreground transition-all"
>
{t('brainstorm.viewNote') || 'View'}
</button>
)}
</div>
</div>
)
})}
</div>
)}
{canEdit && (
<div className="grid grid-cols-1 gap-4">
<div className="grid grid-cols-2 gap-4">
<button
onClick={() => handleDeepen(selectedIdea)}
disabled={expandIdea.isPending}
className="flex flex-col items-center justify-center p-6 border-2 border-dashed border-border rounded-2xl hover:border-orange-400/40 hover:bg-orange-500/5 transition-all group disabled:opacity-50"
>
<Wind size={24} className="text-muted-foreground group-hover:text-orange-500 mb-2" />
<span className="text-[11px] font-bold uppercase tracking-widest text-muted-foreground group-hover:text-foreground">
{expandIdea.isPending ? (t('brainstorm.deepening') || 'Generating...') : (t('brainstorm.deepen') || 'Deepen')}
</span>
</button>
<button
onClick={() => handleConvert(selectedIdea)}
disabled={selectedIdea.status === 'converted' || convertIdea.isPending}
className="flex flex-col items-center justify-center p-6 border-2 border-dashed border-border rounded-2xl hover:border-emerald-400/40 hover:bg-emerald-500/5 transition-all group disabled:opacity-50"
>
<FileText size={24} className="text-muted-foreground group-hover:text-emerald-500 mb-2" />
<span className="text-[11px] font-bold uppercase tracking-widest text-muted-foreground group-hover:text-foreground">
{convertIdea.isPending ? (t('brainstorm.converting') || 'Converting...') : (t('brainstorm.extract') || 'Create Note')}
</span>
</button>
</div>
<button
onClick={() => handleDismiss(selectedIdea.id)}
className="w-full py-4 text-[10px] font-bold uppercase tracking-[0.2em] text-muted-foreground hover:text-rose-500 hover:bg-rose-500/5 rounded-xl transition-all border border-transparent hover:border-rose-500/10"
>
{t('brainstorm.dismiss') || 'Not pertinent'}
</button>
</div>
)}
{isGuest && (
<div className="mt-6 px-4 py-3 bg-orange-500/5 border border-orange-500/10 rounded-xl flex items-center gap-2">
<Globe size={14} className="text-orange-500 shrink-0" />
<span className="text-[11px] text-orange-600 dark:text-orange-400">
Vous consultez ce brainstorm en tant qu'invité. Connectez-vous pour modifier.
</span>
</div>
)}
</div>
</motion.div>
)}
</AnimatePresence>
<div className="w-16 border-l border-border flex flex-col items-center py-6 gap-6 bg-white dark:bg-[#1A1A1A] z-10">
<History size={18} className="text-muted-foreground" />
<div className="w-px flex-1 bg-border/40" />
<div className="flex flex-col gap-3 overflow-y-auto px-2">
{sessions?.map((s) => (
<button
key={s.id}
onClick={() => setActiveSessionId(s.id)}
className={`w-10 h-10 min-h-[40px] rounded-xl flex items-center justify-center text-xs font-bold transition-all shrink-0 ${
activeSessionId === s.id
? 'bg-foreground text-background scale-110 shadow-lg'
: (s as any)._owned === false
? 'bg-memento-blue dark:bg-memento-blue/10 text-memento-blue hover:bg-blue-100 hover:text-memento-blue'
: 'bg-white dark:bg-white/10 text-muted-foreground hover:bg-foreground/5 hover:text-foreground'
}`}
title={s.seedIdea}
>
{s.seedIdea.charAt(0).toUpperCase()}
</button>
))}
</div>
<div className="w-px h-12 bg-border/40" />
</div>
</div>
{activeSessionId && session && (
<BrainstormShareDialog
open={showShareDialog}
onOpenChange={setShowShareDialog}
sessionId={activeSessionId}
seedIdea={session.seedIdea}
isPublic={(session as any).isPublic || false}
guestCanEdit={(session as any).guestCanEdit || false}
/>
)}
<AnimatePresence>
{impactToast && (
<motion.div
initial={{ y: 100, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: 100, opacity: 0 }}
className="fixed bottom-8 left-1/2 -translate-x-1/2 z-50 px-6 py-4 bg-foreground text-background rounded-2xl shadow-2xl flex items-center gap-4 text-sm font-medium"
>
<span>
{impactToast.notesEnriched === 0 && impactToast.notesMarkedDry === 0
? (t('brainstorm.linkCopied') || 'Invite link copied!')
: <>
{impactToast.notesEnriched > 0 && `${impactToast.notesEnriched} note(s) enriched`}
{impactToast.notesEnriched > 0 && impactToast.notesMarkedDry > 0 && ' · '}
{impactToast.notesMarkedDry > 0 && `${impactToast.notesMarkedDry} note(s) marked dry`}
</>
}
</span>
<button onClick={() => setImpactToast(null)} className="text-background/60 hover:text-background">
</button>
</motion.div>
)}
</AnimatePresence>
<AnimatePresence>
{exportError && (
<motion.div
initial={{ y: 100, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: 100, opacity: 0 }}
className="fixed bottom-8 left-1/2 -translate-x-1/2 z-50 px-6 py-4 bg-rose-500 text-white rounded-2xl shadow-2xl flex items-center gap-4 text-sm font-medium"
>
<span>{exportError}</span>
<button onClick={() => setExportError(null)} className="text-white/60 hover:text-white">
</button>
</motion.div>
)}
</AnimatePresence>
<AnimatePresence>
{exportToast && (
<motion.div
initial={{ y: 100, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: 100, opacity: 0 }}
className="fixed bottom-8 left-1/2 -translate-x-1/2 z-50 px-6 py-4 bg-emerald-600 text-white rounded-2xl shadow-2xl flex items-center gap-3 text-sm font-medium"
>
<Wind size={18} />
<div className="flex flex-col">
<span className="font-bold">{exportToast.noteTitle}</span>
<span className="text-[11px] text-emerald-100">Carnet : {exportToast.notebookName}</span>
</div>
<div className="ml-3 flex items-center gap-1.5 text-emerald-200 text-[10px]">
<div className="w-1.5 h-1.5 rounded-full bg-emerald-300 animate-pulse" />
Ouverture…
</div>
</motion.div>
)}
</AnimatePresence>
<AnimatePresence>
{convertToast && (
<motion.div
initial={{ y: 100, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: 100, opacity: 0 }}
className="fixed bottom-8 left-1/2 -translate-x-1/2 z-50 px-6 py-4 bg-emerald-600 text-white rounded-2xl shadow-2xl flex items-center gap-3 text-sm font-medium"
>
<FileText size={18} />
<div className="flex flex-col">
<span className="font-bold">{convertToast.noteTitle}</span>
<span className="text-[11px] text-emerald-100">{t('brainstorm.noteCreated') || 'Note Created'}</span>
</div>
<div className="ml-3 flex items-center gap-1.5 text-emerald-200 text-[10px]">
<div className="w-1.5 h-1.5 rounded-full bg-emerald-300 animate-pulse" />
Ouverture
</div>
</motion.div>
)}
</AnimatePresence>
</div>
)
}

View File

@@ -0,0 +1,296 @@
'use client'
import React, { useState, useTransition, useEffect, useRef, useCallback } from 'react'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
} from '@/components/ui/dialog'
import { UserPlus, Check, AlertCircle, Globe, Lock, Copy } from 'lucide-react'
import { createBrainstormShare } from '@/app/actions/brainstorm'
import { useUpdateBrainstormSettings } from '@/hooks/use-brainstorm'
interface BrainstormShareDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
sessionId: string
seedIdea: string
isPublic?: boolean
guestCanEdit?: boolean
}
interface UserResult {
id: string
name: string | null
email: string
image: string | null
}
const MESSAGES: Record<string, { text: string; type: 'success' | 'info' }> = {
invited: { text: 'Invitation envoyée !', type: 'success' },
re_invited: { text: 'Invitation renvoyée !', type: 'success' },
already_shared: {
text: 'Cette personne a déjà accès à ce brainstorm.',
type: 'info',
},
already_pending: {
text: 'Une invitation est déjà en attente pour cette personne.',
type: 'info',
},
}
export function BrainstormShareDialog({
open,
onOpenChange,
sessionId,
seedIdea,
isPublic = false,
guestCanEdit = false,
}: BrainstormShareDialogProps) {
const [query, setQuery] = useState('')
const [results, setResults] = useState<UserResult[]>([])
const [showDropdown, setShowDropdown] = useState(false)
const [feedback, setFeedback] = useState<{
text: string
type: 'success' | 'error' | 'info'
} | null>(null)
const [isPending, startTransition] = useTransition()
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const dropdownRef = useRef<HTMLDivElement>(null)
const [publicEnabled, setPublicEnabled] = useState(isPublic)
const [guestEditEnabled, setGuestEditEnabled] = useState(guestCanEdit)
const [linkCopied, setLinkCopied] = useState(false)
const updateSettings = useUpdateBrainstormSettings(sessionId)
useEffect(() => {
setPublicEnabled(isPublic)
setGuestEditEnabled(guestCanEdit)
}, [isPublic, guestCanEdit])
useEffect(() => {
if (!open) {
setQuery('')
setResults([])
setShowDropdown(false)
setFeedback(null)
}
}, [open])
const searchUsers = useCallback(async (q: string) => {
if (q.length < 2) {
setResults([])
setShowDropdown(false)
return
}
try {
const res = await fetch(`/api/users/search?q=${encodeURIComponent(q)}`)
const data = await res.json()
setResults(data.users || [])
setShowDropdown(data.users?.length > 0)
} catch {
setResults([])
}
}, [])
const handleInputChange = (value: string) => {
setQuery(value)
setFeedback(null)
if (debounceRef.current) clearTimeout(debounceRef.current)
debounceRef.current = setTimeout(() => searchUsers(value), 200)
}
const selectUser = (user: UserResult) => {
setQuery(user.email)
setShowDropdown(false)
setResults([])
}
const handleShare = (e: React.FormEvent) => {
e.preventDefault()
if (!query.trim()) return
setShowDropdown(false)
setFeedback(null)
startTransition(async () => {
try {
const result = await createBrainstormShare(sessionId, query.trim())
if (result.message && MESSAGES[result.message]) {
const m = MESSAGES[result.message]
setFeedback({ text: m.text, type: m.type })
} else {
setFeedback({ text: 'Invitation envoyée !', type: 'success' })
}
if (result.message === 'invited' || result.message === 're_invited') {
setQuery('')
}
} catch (err: any) {
setFeedback({ text: err.message || 'Erreur', type: 'error' })
}
})
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md bg-white dark:bg-[#1A1A1A] border-border rounded-2xl">
<DialogHeader>
<DialogTitle className="flex items-center gap-2 text-foreground">
<div className="w-7 h-7 rounded-lg bg-orange-500/10 flex items-center justify-center">
<UserPlus size={14} className="text-orange-500" />
</div>
<span className="font-serif">Partager le brainstorm</span>
</DialogTitle>
<DialogDescription className="text-muted-foreground text-xs italic font-serif truncate">
{seedIdea.length > 60 ? seedIdea.substring(0, 60) + '…' : seedIdea}
</DialogDescription>
</DialogHeader>
<form onSubmit={handleShare} className="space-y-3 mt-2">
<div className="relative" ref={dropdownRef}>
<label className="text-[10px] font-bold uppercase tracking-[0.15em] text-muted-foreground mb-1.5 block">
Rechercher une personne
</label>
<input
type="text"
value={query}
onChange={(e) => handleInputChange(e.target.value)}
onFocus={() => results.length > 0 && setShowDropdown(true)}
onBlur={() => setTimeout(() => setShowDropdown(false), 150)}
placeholder="Nom ou email…"
className="w-full px-4 py-3 text-sm border border-border rounded-xl bg-transparent focus:outline-none focus:ring-2 focus:ring-orange-500/20 focus:border-orange-500/40 transition-all"
autoFocus
/>
{showDropdown && results.length > 0 && (
<div className="absolute z-50 top-full left-0 right-0 mt-1 bg-white dark:bg-[#1A1A1A] border border-border rounded-xl shadow-xl overflow-hidden">
{results.map((user) => (
<button
key={user.id}
type="button"
onMouseDown={(e) => {
e.preventDefault()
selectUser(user)
}}
className="w-full px-4 py-2.5 flex items-center gap-3 hover:bg-orange-500/5 transition-colors text-left"
>
<div className="w-8 h-8 rounded-full bg-orange-500/10 flex items-center justify-center text-xs font-bold text-orange-600 shrink-0">
{(user.name || user.email).charAt(0).toUpperCase()}
</div>
<div className="min-w-0 flex-1">
<p className="text-sm font-medium text-foreground truncate">
{user.name || 'Sans nom'}
</p>
<p className="text-[11px] text-muted-foreground truncate">
{user.email}
</p>
</div>
</button>
))}
</div>
)}
</div>
{feedback && (
<div
className={`flex items-center gap-1.5 px-1 text-[11px] font-medium ${
feedback.type === 'success'
? 'text-emerald-500'
: feedback.type === 'error'
? 'text-rose-500'
: 'text-amber-500'
}`}
>
{feedback.type === 'success' ? (
<Check size={12} />
) : (
<AlertCircle size={12} />
)}
{feedback.text}
</div>
)}
<button
type="submit"
disabled={!query.trim() || isPending}
className="w-full py-3 bg-orange-500 hover:bg-orange-600 text-white text-[10px] font-bold uppercase tracking-[0.15em] rounded-xl disabled:opacity-50 transition-all flex items-center justify-center gap-1.5"
>
<UserPlus size={12} />
{isPending ? 'Envoi…' : 'Partager'}
</button>
</form>
<p className="text-[10px] text-muted-foreground/60 text-center mt-1">
La personne recevra une notification pour accepter ou refuser.
</p>
<div className="border-t border-border mt-4 pt-4">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
{publicEnabled ? (
<Globe size={14} className="text-emerald-500" />
) : (
<Lock size={14} className="text-muted-foreground" />
)}
<span className="text-[10px] font-bold uppercase tracking-[0.15em] text-muted-foreground">
Lien public
</span>
</div>
<button
onClick={() => {
const next = !publicEnabled
setPublicEnabled(next)
if (!next) setGuestEditEnabled(false)
updateSettings.mutate({ isPublic: next, guestCanEdit: next ? guestEditEnabled : false })
}}
className={`relative w-10 h-5 rounded-full transition-colors ${publicEnabled ? 'bg-emerald-500' : 'bg-border'}`}
>
<div
className={`absolute top-0.5 w-4 h-4 rounded-full bg-white shadow-sm transition-transform ${publicEnabled ? 'translate-x-5' : 'translate-x-0.5'}`}
/>
</button>
</div>
{publicEnabled && (
<>
<div className="flex items-center gap-2 mb-3">
<input
readOnly
value={`${typeof window !== 'undefined' ? window.location.origin : ''}/brainstorm?session=${sessionId}`}
className="flex-1 px-3 py-2 text-[11px] bg-foreground/5 border border-border rounded-lg text-muted-foreground truncate"
/>
<button
onClick={() => {
navigator.clipboard.writeText(`${window.location.origin}/brainstorm?session=${sessionId}`)
setLinkCopied(true)
setTimeout(() => setLinkCopied(false), 2000)
}}
className="px-3 py-2 text-[10px] font-bold uppercase tracking-wider bg-foreground/5 hover:bg-foreground/10 rounded-lg transition-colors flex items-center gap-1"
>
{linkCopied ? <Check size={12} className="text-emerald-500" /> : <Copy size={12} />}
</button>
</div>
<div className="flex items-center justify-between">
<span className="text-[10px] text-muted-foreground">
Autoriser les invités à modifier
</span>
<button
onClick={() => {
const next = !guestEditEnabled
setGuestEditEnabled(next)
updateSettings.mutate({ guestCanEdit: next })
}}
className={`relative w-10 h-5 rounded-full transition-colors ${guestEditEnabled ? 'bg-orange-500' : 'bg-border'}`}
>
<div
className={`absolute top-0.5 w-4 h-4 rounded-full bg-white shadow-sm transition-transform ${guestEditEnabled ? 'translate-x-5' : 'translate-x-0.5'}`}
/>
</button>
</div>
</>
)}
</div>
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,123 @@
'use client'
import React, { useEffect, useState, useRef } from 'react'
import { motion, AnimatePresence } from 'motion/react'
interface GhostCursorProps {
isActive: boolean
containerRef: React.RefObject<HTMLDivElement | null>
targetId?: string | null
}
export function GhostCursor({ isActive, containerRef, targetId }: GhostCursorProps) {
const [position, setPosition] = useState({ x: 0, y: 0 })
const [visible, setVisible] = useState(false)
const intervalRef = useRef<NodeJS.Timeout | null>(null)
useEffect(() => {
if (!isActive) {
setVisible(false)
if (intervalRef.current) clearInterval(intervalRef.current)
return
}
const container = containerRef.current
if (!container) return
setVisible(true)
// Initialisation sécurisée
const initialRect = container.getBoundingClientRect()
const initialCx = initialRect.width > 0 ? initialRect.width / 2 : window.innerWidth / 2
const initialCy = initialRect.height > 0 ? initialRect.height / 2 : window.innerHeight / 2
setPosition({
x: initialCx + (Math.random() - 0.5) * 200,
y: initialCy + (Math.random() - 0.5) * 200,
})
let angle = Math.random() * Math.PI * 2
let targetX = initialCx + Math.cos(angle) * 250
let targetY = initialCy + Math.sin(angle) * 250
intervalRef.current = setInterval(() => {
// Recalculer les dimensions à chaque tick pour s'adapter aux redimensionnements
const currentContainer = containerRef.current
if (!currentContainer) return
const containerRect = currentContainer.getBoundingClientRect()
const cx = containerRect.width / 2
const cy = containerRect.height / 2
let currentTargetX = targetX;
let currentTargetY = targetY;
if (targetId) {
const nodeElement = document.querySelector(`[data-id="${targetId}"]`);
if (nodeElement) {
const nodeRect = nodeElement.getBoundingClientRect();
currentTargetX = nodeRect.left - containerRect.left + nodeRect.width / 2;
currentTargetY = nodeRect.top - containerRect.top + nodeRect.height / 2;
}
}
setPosition(prev => {
const dx = currentTargetX - prev.x
const dy = currentTargetY - prev.y
const dist = Math.sqrt(dx * dx + dy * dy)
if (!targetId && dist < 20) {
angle = Math.random() * Math.PI * 2
const radius = 150 + Math.random() * 200
targetX = cx + Math.cos(angle) * radius
targetY = cy + Math.sin(angle) * radius
}
const speed = targetId ? 0.15 : 0.06;
let newX = prev.x + dx * speed;
let newY = prev.y + dy * speed;
// Protection Anti-NaN qui bloquait le curseur en haut à gauche
if (isNaN(newX)) newX = cx;
if (isNaN(newY)) newY = cy;
return {
x: newX,
y: newY,
}
})
}, 50)
return () => {
if (intervalRef.current) clearInterval(intervalRef.current)
}
}, [isActive, containerRef, targetId])
return (
<AnimatePresence>
{visible && (
<motion.div
initial={{ opacity: 0, scale: 0.5 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.5 }}
className="absolute pointer-events-none z-50"
style={{ transform: `translate(${position.x}px, ${position.y}px)` }}
>
<div className="relative">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M0 0L16 6L8 8L6 16L0 0Z" fill="#a78bfa" />
</svg>
<div className="absolute -top-1 -right-1 w-3 h-3">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-violet-400 opacity-50" />
<span className="relative inline-flex rounded-full h-3 w-3 bg-violet-500" />
</div>
<div className="mt-3 ml-3 px-2 py-0.5 rounded-full text-[10px] font-bold text-white whitespace-nowrap shadow-lg bg-gradient-to-r from-violet-500 to-purple-600">
AI
</div>
</div>
</motion.div>
)}
</AnimatePresence>
)
}

View File

@@ -0,0 +1,210 @@
'use client'
import React, { useState } from 'react'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog'
import { UserPlus, Link, Mail, Check, Copy } from 'lucide-react'
interface InviteDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
onInviteByEmail: (email: string, role: 'editor' | 'viewer') => Promise<any>
onInviteByLink: () => Promise<any>
seedIdea: string
isLoading: boolean
t: (key: string) => string | undefined
}
export function InviteDialog({
open,
onOpenChange,
onInviteByEmail,
onInviteByLink,
seedIdea,
isLoading,
t,
}: InviteDialogProps) {
const [tab, setTab] = useState<'email' | 'link'>('email')
const [email, setEmail] = useState('')
const [role, setRole] = useState<'editor' | 'viewer'>('editor')
const [linkCopied, setLinkCopied] = useState(false)
const [emailSent, setEmailSent] = useState(false)
const [localLoading, setLocalLoading] = useState(false)
const handleEmailInvite = async (e: React.FormEvent) => {
e.preventDefault()
if (!email.trim()) return
setLocalLoading(true)
try {
const res = await onInviteByEmail(email.trim(), role)
if (res?.invitedUser) {
setEmailSent(true)
setTimeout(() => {
setEmailSent(false)
setEmail('')
}, 2000)
}
} finally {
setLocalLoading(false)
}
}
const handleLinkCopy = async () => {
setLocalLoading(true)
try {
const res = await onInviteByLink()
if (res?.inviteUrl) {
const url = window.location.origin + res.inviteUrl
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(url)
} else {
const ta = document.createElement('textarea')
ta.value = url
ta.style.position = 'fixed'
ta.style.opacity = '0'
document.body.appendChild(ta)
ta.select()
document.execCommand('copy')
document.body.removeChild(ta)
}
setLinkCopied(true)
setTimeout(() => setLinkCopied(false), 3000)
}
} finally {
setLocalLoading(false)
}
}
const busy = isLoading || localLoading
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md bg-white dark:bg-[#1A1A1A] border-border rounded-2xl">
<DialogHeader>
<DialogTitle className="flex items-center gap-2 text-foreground">
<div className="w-7 h-7 rounded-lg bg-emerald-500/10 flex items-center justify-center">
<UserPlus size={14} className="text-emerald-500" />
</div>
<span className="font-serif">{t('brainstorm.inviteTitle') || 'Invite to brainstorm'}</span>
</DialogTitle>
<DialogDescription className="text-muted-foreground text-xs italic font-serif">
{seedIdea.length > 60 ? seedIdea.substring(0, 60) + '…' : seedIdea}
</DialogDescription>
</DialogHeader>
<div className="flex gap-1 p-1 bg-foreground/5 rounded-xl mt-2">
<button
onClick={() => setTab('email')}
className={`flex-1 flex items-center justify-center gap-1.5 py-2 px-3 rounded-lg text-[10px] font-bold uppercase tracking-[0.1em] transition-all ${
tab === 'email'
? 'bg-white dark:bg-[#2A2A2A] text-foreground shadow-sm'
: 'text-muted-foreground hover:text-foreground'
}`}
>
<Mail size={12} />
{t('brainstorm.inviteByEmail') || 'Email'}
</button>
<button
onClick={() => setTab('link')}
className={`flex-1 flex items-center justify-center gap-1.5 py-2 px-3 rounded-lg text-[10px] font-bold uppercase tracking-[0.1em] transition-all ${
tab === 'link'
? 'bg-white dark:bg-[#2A2A2A] text-foreground shadow-sm'
: 'text-muted-foreground hover:text-foreground'
}`}
>
<Link size={12} />
{t('brainstorm.inviteByLink') || 'Link'}
</button>
</div>
{tab === 'email' ? (
<form onSubmit={handleEmailInvite} className="space-y-4 mt-2">
<div>
<label className="text-[10px] font-bold uppercase tracking-[0.15em] text-muted-foreground mb-1.5 block">
{t('brainstorm.inviteEmailLabel') || 'Email address'}
</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder={t('brainstorm.inviteEmailPlaceholder') || 'colleague@email.com'}
className="w-full px-4 py-3 text-sm border border-border rounded-xl bg-transparent focus:outline-none focus:ring-2 focus:ring-emerald-500/20 focus:border-emerald-500/40 transition-all"
autoFocus
/>
</div>
<div>
<label className="text-[10px] font-bold uppercase tracking-[0.15em] text-muted-foreground mb-1.5 block">
{t('brainstorm.inviteRoleLabel') || 'Role'}
</label>
<div className="flex gap-2">
<button
type="button"
onClick={() => setRole('editor')}
className={`flex-1 py-2.5 rounded-xl text-[10px] font-bold uppercase tracking-[0.1em] border transition-all ${
role === 'editor'
? 'border-emerald-500/40 bg-emerald-500/5 text-emerald-600'
: 'border-border text-muted-foreground hover:border-emerald-500/20'
}`}
>
{t('brainstorm.roleEditor') || 'Editor'}
</button>
<button
type="button"
onClick={() => setRole('viewer')}
className={`flex-1 py-2.5 rounded-xl text-[10px] font-bold uppercase tracking-[0.1em] border transition-all ${
role === 'viewer'
? 'border-memento-blue/40 bg-memento-blue/5 text-memento-blue'
: 'border-border text-muted-foreground hover:border-memento-blue/20'
}`}
>
{t('brainstorm.roleViewer') || 'Viewer'}
</button>
</div>
</div>
<button
type="submit"
disabled={!email.trim() || busy}
className="w-full py-3 bg-emerald-500 hover:bg-emerald-600 text-white text-[10px] font-bold uppercase tracking-[0.15em] rounded-xl disabled:opacity-50 transition-all flex items-center justify-center gap-1.5"
>
{emailSent ? (
<>
<Check size={12} />
{t('brainstorm.inviteSent') || 'Invitation sent!'}
</>
) : (
<>
<Mail size={12} />
{busy ? '...' : (t('brainstorm.sendInvite') || 'Send invitation')}
</>
)}
</button>
</form>
) : (
<div className="space-y-4 mt-2">
<p className="text-xs text-muted-foreground leading-relaxed">
{t('brainstorm.linkDescription') || 'Anyone with this link can join the brainstorm session.'}
</p>
<button
onClick={handleLinkCopy}
disabled={busy}
className="w-full py-3 bg-foreground/5 hover:bg-foreground/10 text-foreground text-[10px] font-bold uppercase tracking-[0.15em] rounded-xl transition-all flex items-center justify-center gap-1.5 border border-border"
>
{linkCopied ? (
<>
<Check size={12} className="text-emerald-500" />
<span className="text-emerald-500">{t('brainstorm.linkCopied') || 'Link copied!'}</span>
</>
) : (
<>
<Copy size={12} />
{busy ? '...' : (t('brainstorm.copyLink') || 'Copy invite link')}
</>
)}
</button>
</div>
)}
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,93 @@
'use client'
import React from 'react'
import { PresenceUser } from '@/hooks/use-brainstorm-socket'
function Cursor({ x, y, name, color }: { x: number; y: number; name: string; color: string }) {
return (
<div
className="absolute pointer-events-none z-50 transition-all duration-75"
style={{ transform: `translate(${x}px, ${y}px)` }}
>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M0 0L16 6L8 8L6 16L0 0Z" fill={color} />
</svg>
<div
className="mt-3 ml-3 px-2 py-0.5 rounded-full text-[10px] font-bold text-white whitespace-nowrap shadow-lg"
style={{ backgroundColor: color }}
>
{name}
</div>
</div>
)
}
export function LiveCursors({ others }: { others: PresenceUser[] }) {
return (
<div className="absolute inset-0 pointer-events-none z-50">
{others.map((user) => {
if (!user.cursor) return null
return (
<Cursor
key={user.userId}
x={user.cursor.x}
y={user.cursor.y}
name={user.name}
color={user.color}
/>
)
})}
</div>
)
}
export function useCursorTracking(
containerRef: React.RefObject<HTMLDivElement | null>,
moveCursor: (cursor: { x: number; y: number } | null) => void
) {
React.useEffect(() => {
const container = containerRef.current
if (!container) return
const handleMove = (e: MouseEvent) => {
const rect = container.getBoundingClientRect()
moveCursor({ x: e.clientX - rect.left, y: e.clientY - rect.top })
}
const handleLeave = () => {
moveCursor(null)
}
container.addEventListener('mousemove', handleMove)
container.addEventListener('mouseleave', handleLeave)
return () => {
container.removeEventListener('mousemove', handleMove)
container.removeEventListener('mouseleave', handleLeave)
}
}, [containerRef, moveCursor])
}
export function PresenceAvatars({ others }: { others: PresenceUser[] }) {
if (others.length === 0) return null
return (
<div className="flex items-center gap-1">
{others.slice(0, 4).map((user) => {
const initial = (user.name || '?').charAt(0).toUpperCase()
return (
<div
key={user.userId}
className="w-6 h-6 rounded-full flex items-center justify-center text-[9px] font-bold text-white border-2 border-white dark:border-zinc-900 shadow-sm"
style={{ backgroundColor: user.color, marginLeft: '-6px' }}
title={user.name}
>
{initial}
</div>
)
})}
{others.length > 4 && (
<span className="text-[9px] text-muted-foreground ml-1">+{others.length - 4}</span>
)}
</div>
)
}

View File

@@ -0,0 +1,98 @@
'use client'
import React, { useState } from 'react'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog'
import { Lightbulb } from 'lucide-react'
interface ManualIdeaDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
onSubmit: (title: string, description?: string) => void
isLoading?: boolean
parentIdeaTitle?: string | null
t: (key: string) => string | undefined
}
export function ManualIdeaDialog({
open,
onOpenChange,
onSubmit,
isLoading,
parentIdeaTitle,
t,
}: ManualIdeaDialogProps) {
const [title, setTitle] = useState('')
const [description, setDescription] = useState('')
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (!title.trim()) return
onSubmit(title.trim(), description.trim() || undefined)
setTitle('')
setDescription('')
onOpenChange(false)
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md bg-white dark:bg-[#1A1A1A] border-border rounded-2xl">
<DialogHeader>
<DialogTitle className="flex items-center gap-2 text-foreground">
<div className="w-7 h-7 rounded-lg bg-memento-blue/10 flex items-center justify-center">
<Lightbulb size={14} className="text-memento-blue" />
</div>
<span className="font-serif">{t('brainstorm.addIdea') || 'Add an idea'}</span>
</DialogTitle>
<DialogDescription className="text-muted-foreground text-xs">
{parentIdeaTitle
? `${t('brainstorm.respondsTo') || 'Responds to'} « ${parentIdeaTitle.length > 40 ? parentIdeaTitle.substring(0, 40) + '…' : parentIdeaTitle} »`
: (t('brainstorm.manualIdeaDesc') || 'Share your idea with the brainstorm canvas')
}
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4 mt-2">
<div>
<label className="text-[10px] font-bold uppercase tracking-[0.15em] text-muted-foreground mb-1.5 block">
{t('brainstorm.manualIdeaTitle') || 'Title'}
</label>
<input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder={t('brainstorm.manualIdeaTitlePlaceholder') || 'Your idea in a few words...'}
className="w-full px-4 py-3 text-sm border border-border rounded-xl bg-transparent focus:outline-none focus:ring-2 focus:ring-memento-blue/20 focus:border-memento-blue/40 transition-all font-serif"
autoFocus
/>
</div>
<div>
<label className="text-[10px] font-bold uppercase tracking-[0.15em] text-muted-foreground mb-1.5 block">
{t('brainstorm.manualIdeaDescLabel') || 'Description (optional)'}
</label>
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder={t('brainstorm.manualIdeaDescPlaceholder') || 'Elaborate on your idea...'}
className="w-full h-24 px-4 py-3 text-sm border border-border rounded-xl bg-transparent focus:outline-none focus:ring-2 focus:ring-memento-blue/20 focus:border-memento-blue/40 resize-none transition-all"
/>
</div>
<div className="flex justify-end gap-2 pt-2">
<button
type="button"
onClick={() => onOpenChange(false)}
className="px-4 py-2 text-[10px] font-bold uppercase tracking-[0.15em] text-muted-foreground hover:text-foreground rounded-xl hover:bg-foreground/5 transition-all"
>
{t('brainstorm.cancel') || 'Cancel'}
</button>
<button
type="submit"
disabled={!title.trim() || isLoading}
className="px-5 py-2.5 bg-memento-blue hover:bg-memento-blue text-white text-[10px] font-bold uppercase tracking-[0.15em] rounded-xl disabled:opacity-50 transition-all flex items-center gap-1.5"
>
<Lightbulb size={12} />
{isLoading ? (t('brainstorm.adding') || 'Adding...') : (t('brainstorm.addIdea') || 'Add idea')}
</button>
</div>
</form>
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,197 @@
'use client'
import React, { useState, useCallback, useRef, useEffect } from 'react'
import { motion, AnimatePresence } from 'motion/react'
import { Play, Pause, SkipBack, SkipForward, ChevronDown, ChevronUp, RotateCcw } from 'lucide-react'
import { useBrainstormSnapshots } from '@/hooks/use-brainstorm'
interface SnapshotIdea {
id: string
title: string
waveNumber: number
positionX: number | null
positionY: number | null
parentIdeaId: string | null
noveltyScore: number | null
createdByType: string | null
status: string
}
interface Snapshot {
id: string
step: number
label: string | null
ideaGraph: string
createdAt: string
}
interface PlaybackBarProps {
sessionId: string
onSnapshotSelect: (ideas: SnapshotIdea[]) => void
onExitPlayback: () => void
}
export function PlaybackBar({ sessionId, onSnapshotSelect, onExitPlayback }: PlaybackBarProps) {
const { data: snapshots } = useBrainstormSnapshots(sessionId)
const [isOpen, setIsOpen] = useState(false)
const [currentStep, setCurrentStep] = useState(-1)
const [isPlaying, setIsPlaying] = useState(false)
const playIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
const parsedSnapshots: (Snapshot & { ideas: SnapshotIdea[] })[] = React.useMemo(() => {
return (snapshots || []).map((s: Snapshot) => ({
...s,
ideas: JSON.parse(s.ideaGraph) as SnapshotIdea[],
}))
}, [snapshots])
const handleStepChange = useCallback((step: number) => {
if (step === -1) {
setCurrentStep(-1)
onExitPlayback()
return
}
const snapshot = parsedSnapshots[step]
if (snapshot) {
setCurrentStep(step)
onSnapshotSelect(snapshot.ideas)
}
}, [parsedSnapshots, onSnapshotSelect, onExitPlayback])
const togglePlay = useCallback(() => {
if (isPlaying) {
setIsPlaying(false)
if (playIntervalRef.current) clearInterval(playIntervalRef.current)
return
}
setIsPlaying(true)
const startStep = currentStep === -1 ? 0 : currentStep + 1
let step = startStep
playIntervalRef.current = setInterval(() => {
if (step >= parsedSnapshots.length) {
setIsPlaying(false)
if (playIntervalRef.current) clearInterval(playIntervalRef.current)
return
}
handleStepChange(step)
step++
}, 1500)
}, [isPlaying, currentStep, parsedSnapshots.length, handleStepChange])
useEffect(() => {
return () => {
if (playIntervalRef.current) clearInterval(playIntervalRef.current)
}
}, [])
if (!parsedSnapshots || parsedSnapshots.length === 0) return null
const isLive = currentStep === -1
return (
<AnimatePresence>
<motion.div
initial={{ y: 60 }}
animate={{ y: isOpen ? 0 : 40 }}
className="absolute bottom-0 left-0 right-16 z-30"
>
<div className="mx-6 mb-4 bg-white/90 dark:bg-[#1A1A1A]/90 backdrop-blur-xl border border-border rounded-2xl shadow-2xl overflow-hidden">
<button
onClick={() => setIsOpen(!isOpen)}
className="w-full px-5 py-2.5 flex items-center justify-between hover:bg-foreground/5 transition-colors"
>
<div className="flex items-center gap-3">
<div className={`w-2 h-2 rounded-full ${isLive ? 'bg-emerald-500 animate-pulse' : isPlaying ? 'bg-orange-500 animate-pulse' : 'bg-muted-foreground'}`} />
<span className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
{isLive ? 'Live' : `Step ${currentStep + 1}/${parsedSnapshots.length}`}
</span>
{parsedSnapshots[currentStep]?.label && (
<span className="text-[10px] text-muted-foreground/60 truncate max-w-[200px]">
{parsedSnapshots[currentStep].label}
</span>
)}
</div>
<div className="flex items-center gap-2">
{isLive ? (
<span className="text-[9px] font-bold text-emerald-500 uppercase tracking-wider">Live</span>
) : (
<button
onClick={(e) => { e.stopPropagation(); handleStepChange(-1) }}
className="p-1.5 hover:bg-foreground/10 rounded-lg transition-colors"
title="Return to live"
>
<RotateCcw size={12} className="text-muted-foreground" />
</button>
)}
{isOpen ? <ChevronDown size={14} /> : <ChevronUp size={14} />}
</div>
</button>
{isOpen && (
<motion.div
initial={{ height: 0 }}
animate={{ height: 'auto' }}
className="border-t border-border"
>
<div className="px-5 py-3 flex items-center gap-3">
<button
onClick={() => handleStepChange(Math.max(0, currentStep - 1))}
disabled={currentStep <= 0}
className="p-2 hover:bg-foreground/5 rounded-lg transition-colors disabled:opacity-30"
>
<SkipBack size={14} />
</button>
<button
onClick={togglePlay}
className="p-2 hover:bg-foreground/5 rounded-lg transition-colors"
>
{isPlaying ? <Pause size={14} /> : <Play size={14} />}
</button>
<button
onClick={() => handleStepChange(Math.min(parsedSnapshots.length - 1, currentStep + 1))}
disabled={currentStep >= parsedSnapshots.length - 1}
className="p-2 hover:bg-foreground/5 rounded-lg transition-colors disabled:opacity-30"
>
<SkipForward size={14} />
</button>
<div className="flex-1 mx-3">
<input
type="range"
min={-1}
max={parsedSnapshots.length - 1}
value={currentStep}
onChange={(e) => handleStepChange(parseInt(e.target.value))}
className="w-full h-1.5 bg-border rounded-full appearance-none cursor-pointer accent-orange-500"
/>
<div className="flex justify-between mt-1">
<span className="text-[8px] text-muted-foreground/40">Live</span>
<span className="text-[8px] text-muted-foreground/40">{parsedSnapshots.length} steps</span>
</div>
</div>
</div>
<div className="px-5 pb-3 flex gap-1 overflow-x-auto">
{parsedSnapshots.map((s, idx) => (
<button
key={s.id}
onClick={() => handleStepChange(idx)}
className={`shrink-0 px-2.5 py-1 rounded-lg text-[9px] font-bold uppercase tracking-wider transition-all ${
idx === currentStep
? 'bg-orange-500 text-white'
: 'bg-foreground/5 text-muted-foreground hover:bg-foreground/10'
}`}
>
{s.label ? (s.label.length > 20 ? s.label.substring(0, 20) + '…' : s.label) : `#${idx + 1}`}
</button>
))}
</div>
</motion.div>
)}
</div>
</motion.div>
</AnimatePresence>
)
}

View File

@@ -0,0 +1,585 @@
'use client'
import React, { useEffect, useRef, useState, useCallback } from 'react'
import * as d3 from 'd3'
import { BrainstormSession } from '@/types/brainstorm'
interface WaveCanvasProps {
session: BrainstormSession
onNodeSelect: (id: string) => void
onPositionUpdate?: (id: string, pos: { x: number; y: number }) => void
selectedNodeId: string | null
onCreateIdea?: (data: { title: string; parentIdeaId?: string; x: number; y: number }) => void
remoteMove?: { ideaId: string; x: number; y: number; _seq: number } | null
manualEditTrigger?: number
playbackIdeas?: any[] | null
}
const WAVE_COLORS: Record<number, string> = {
1: '#fb923c',
2: '#60a5fa',
3: '#a78bfa',
}
export const WaveCanvas: React.FC<WaveCanvasProps> = ({
session,
onNodeSelect,
onPositionUpdate,
selectedNodeId,
onCreateIdea,
remoteMove,
manualEditTrigger,
playbackIdeas,
}) => {
const svgRef = useRef<SVGSVGElement>(null)
const containerRef = useRef<HTMLDivElement>(null)
const nodeRef = useRef<d3.Selection<SVGGElement, any, SVGGElement, unknown> | null>(null)
const linkRef = useRef<d3.Selection<SVGLineElement, any, SVGGElement, unknown> | null>(null)
const simulationRef = useRef<d3.Simulation<any, any> | null>(null)
const transformRef = useRef<d3.ZoomTransform>(d3.zoomIdentity)
const onNodeSelectRef = useRef(onNodeSelect)
onNodeSelectRef.current = onNodeSelect
const onPositionUpdateRef = useRef(onPositionUpdate)
onPositionUpdateRef.current = onPositionUpdate
const onCreateIdeaRef = useRef(onCreateIdea)
onCreateIdeaRef.current = onCreateIdea
const ideasKey = session?.ideas?.map(i => `${i.id}:${i.status}:${i.positionX}:${i.positionY}`).join('|') || ''
const sessionId = session?.id || ''
const [isDark, setIsDark] = useState(false)
useEffect(() => {
const el = document.documentElement
setIsDark(el.classList.contains('dark'))
const observer = new MutationObserver(() => {
setIsDark(el.classList.contains('dark'))
})
observer.observe(el, { attributes: true, attributeFilter: ['class'] })
return () => observer.disconnect()
}, [])
const [editingNode, setEditingNode] = useState<{
x: number
y: number
parentId: string | null
} | null>(null)
const [editText, setEditText] = useState('')
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
if (editingNode && inputRef.current) {
inputRef.current.focus()
}
}, [editingNode])
useEffect(() => {
if (!manualEditTrigger || manualEditTrigger === 0) return
const container = containerRef.current
if (!container) return
const rect = container.getBoundingClientRect()
setEditingNode({
x: rect.width / 2,
y: rect.height / 2,
parentId: null,
})
setEditText('')
}, [manualEditTrigger])
const toSvgCoords = useCallback((clientX: number, clientY: number) => {
const svg = svgRef.current
if (!svg) return { x: 0, y: 0 }
const pt = svg.createSVGPoint()
pt.x = clientX
pt.y = clientY
const ctm = svg.getScreenCTM()
if (!ctm) return { x: 0, y: 0 }
const svgPt = pt.matrixTransform(ctm.inverse())
const t = transformRef.current
return {
x: (svgPt.x - t.x) / t.k,
y: (svgPt.y - t.y) / t.k,
}
}, [])
const toScreenCoords = useCallback((svgX: number, svgY: number) => {
const svg = svgRef.current
if (!svg) return { x: 0, y: 0 }
const t = transformRef.current
const screenX = svgX * t.k + t.x
const screenY = svgY * t.k + t.y
const ctm = svg.getScreenCTM()
if (!ctm) return { x: 0, y: 0 }
const pt = svg.createSVGPoint()
pt.x = screenX
pt.y = screenY
const screenPt = pt.matrixTransform(ctm)
return { x: screenPt.x, y: screenPt.y }
}, [])
const handleSubmitIdea = useCallback(() => {
if (!editText.trim() || !editingNode || !onCreateIdeaRef.current) return
onCreateIdeaRef.current({
title: editText.trim(),
parentIdeaId: editingNode.parentId || undefined,
x: editingNode.x,
y: editingNode.y,
})
setEditingNode(null)
setEditText('')
}, [editText, editingNode])
useEffect(() => {
if (!svgRef.current || !containerRef.current) return
if (!session?.id || !session?.ideas) return
const activeIdeas = playbackIdeas
? playbackIdeas.map((pi: any) => ({
...pi,
description: '',
connectionToSeed: null,
convertedToNoteId: null,
relatedNoteIds: null,
createdBy: null,
noteRefs: [],
creator: null,
createdAt: new Date(),
sessionId: session.id,
}))
: session.ideas
const width = containerRef.current.clientWidth
const height = containerRef.current.clientHeight
const centerX = width / 2
const centerY = height / 2
const svg = d3.select(svgRef.current)
svg.selectAll('*').remove()
const g = svg.append('g')
const zoom = d3.zoom<SVGSVGElement, unknown>()
.scaleExtent([0.1, 5])
.on('zoom', (event) => {
g.attr('transform', event.transform)
transformRef.current = event.transform
})
svg.call(zoom)
svg.call(zoom.transform, d3.zoomIdentity.translate(centerX, centerY).scale(0.8))
interface D3Node extends d3.SimulationNodeDatum {
id: string
type: 'root' | 'idea'
wave?: number
title: string
color: string
radius: number
status?: string
createdByType?: string | null
creatorInitial?: string
}
interface D3Link extends d3.SimulationLinkDatum<D3Node> {
source: string | D3Node
target: string | D3Node
type: 'wave' | 'parent'
}
const nodes: D3Node[] = []
const links: D3Link[] = []
nodes.push({
id: 'root',
type: 'root',
title: session.seedIdea,
color: '#141414',
radius: 40,
fx: 0,
fy: 0,
})
activeIdeas.forEach((idea) => {
const creator = (idea as any).creator as { name: string | null } | undefined | null
nodes.push({
id: idea.id,
type: 'idea',
wave: idea.waveNumber,
title: idea.title,
color: WAVE_COLORS[idea.waveNumber as 1 | 2 | 3] || '#94a3b8',
radius: idea.status === 'dismissed' ? 18 : 28,
status: idea.status,
createdByType: idea.createdByType || 'ai',
creatorInitial: creator?.name ? creator.name.charAt(0).toUpperCase() : undefined,
x: idea.positionX ?? undefined,
y: idea.positionY ?? undefined,
})
if (idea.parentIdeaId) {
links.push({ source: idea.parentIdeaId, target: idea.id, type: 'parent' })
} else {
links.push({ source: 'root', target: idea.id, type: 'wave' })
}
})
const simulation = d3
.forceSimulation<D3Node>(nodes)
.force(
'link',
d3
.forceLink<D3Node, D3Link>(links)
.id((d) => d.id)
.distance((d) => {
if (d.type === 'wave') {
const targetNode = nodes.find(
(n) =>
n.id ===
(typeof d.target === 'string' ? d.target : (d.target as any).id)
)
return (targetNode?.wave || 1) * 200
}
if (d.type === 'parent') return 180
return 100
})
)
.force('charge', d3.forceManyBody().strength(-800))
.force(
'radial',
d3
.forceRadial<D3Node>(
(d) => {
if (d.type === 'root') return 0
return (d.wave || 1) * 200
},
0,
0
)
.strength(0.8)
)
.force('collision', d3.forceCollide<D3Node>().radius((d) => d.radius + 30))
simulationRef.current = simulation
const ringRadii = [200, 400, 600]
g.selectAll('.ring')
.data(ringRadii)
.enter()
.append('circle')
.attr('class', 'ring')
.attr('r', (d) => d)
.attr('fill', 'none')
.attr('stroke', isDark ? '#ffffff10' : '#e2e8f0')
.attr('stroke-width', 1)
.attr('stroke-dasharray', '4,4')
.style('opacity', 0.5)
const link = g
.append('g')
.selectAll('line')
.data(links)
.enter()
.append('line')
.attr('stroke', (d) =>
d.type === 'wave' ? (isDark ? '#334155' : '#cbd5e1') : (isDark ? '#854d0e' : '#fde047')
)
.attr('stroke-width', (d) => (d.type === 'wave' ? 1.5 : 2))
.attr('stroke-dasharray', (d) => (d.type === 'parent' ? 'none' : '4,4'))
linkRef.current = link
const node = g
.append('g')
.selectAll('.node')
.data(nodes)
.enter()
.append('g')
.attr('class', (d) => `node${d.status === 'dismissed' ? ' dismissed' : ''}`)
.style('cursor', 'pointer')
.style('opacity', (d) => d.status === 'dismissed' ? 0.3 : 1)
.attr('data-id', (d) => d.id)
.on('click', (_event, d) => {
if (d.type === 'idea') onNodeSelectRef.current?.(d.id)
})
.on('dblclick', (event, d) => {
if (!onCreateIdeaRef.current) return
event.stopPropagation()
const parentId = d.type === 'root' ? null : d.id
const angle = Math.random() * Math.PI * 2
const dist = 180
const nx = (d.x || 0) + Math.cos(angle) * dist
const ny = (d.y || 0) + Math.sin(angle) * dist
const screen = toScreenCoords(nx, ny)
const container = containerRef.current
if (!container) return
const rect = container.getBoundingClientRect()
setEditingNode({ x: screen.x - rect.left, y: screen.y - rect.top, parentId })
setEditText('')
})
.call(
d3
.drag<SVGGElement, D3Node>()
.on('start', (event, d) => {
if (!event.active) simulation.alphaTarget(0.3).restart()
d.fx = d.x
d.fy = d.y
})
.on('drag', (event, d) => {
d.fx = event.x
d.fy = event.y
})
.on('end', (event, d) => {
if (!event.active) simulation.alphaTarget(0)
d.fx = null
d.fy = null
if (d.type === 'idea' && onPositionUpdateRef.current) {
onPositionUpdateRef.current(d.id, { x: event.x, y: event.y })
}
}) as any
)
nodeRef.current = node
node
.append('circle')
.attr('r', (d) => d.radius)
.attr('fill', (d) =>
d.status === 'converted'
? (isDark ? '#064e3b' : '#ecfdf5')
: d.type === 'root'
? (isDark ? '#f97316' : '#141414')
: (isDark ? '#1e1e1e' : '#fff')
)
.attr('stroke', (d) =>
d.status === 'converted' ? '#10b981' : d.color
)
.attr('stroke-width', 2)
node
.append('text')
.attr('dy', (d) =>
d.type === 'root' ? '.35em' : d.radius + 20
)
.attr('text-anchor', 'middle')
.attr('fill', (d) =>
d.type === 'root'
? '#fff'
: d.status === 'dismissed'
? (isDark ? '#475569' : '#94a3b8')
: (isDark ? '#e2e8f0' : '#141414')
)
.attr(
'class',
(d) =>
d.type === 'root'
? 'text-[10px] font-bold pointer-events-none tracking-widest'
: 'text-[11px] font-bold uppercase tracking-tight pointer-events-none'
)
.text((d) =>
d.type === 'root'
? 'SEED'
: d.title.length > 18
? d.title.substring(0, 18) + '...'
: d.title
)
node
.filter((d) => d.status === 'converted')
.append('text')
.attr('x', (d) => d.radius * 0.55)
.attr('y', (d) => -d.radius * 0.55)
.attr('text-anchor', 'middle')
.attr('fill', isDark ? '#34d399' : '#10b981')
.attr('font-size', '14px')
.attr('class', 'pointer-events-none')
.text('✓')
node
.filter((d) => {
if (!d.wave || d.type === 'root') return false
try {
const ids: string[] = JSON.parse(
(session.ideas.find((i) => i.id === d.id)?.relatedNoteIds) || '[]'
)
return ids.length > 0
} catch { return false }
})
.append('text')
.attr('x', (d) => -d.radius * 0.55)
.attr('y', (d) => -d.radius * 0.55)
.attr('text-anchor', 'middle')
.attr('fill', isDark ? '#94a3b8' : '#64748b')
.attr('font-size', '12px')
.attr('class', 'pointer-events-none')
.text('📎')
node
.filter((d) => d.type === 'idea' && d.createdByType === 'human')
.append('circle')
.attr('cx', (d) => d.radius * 0.6)
.attr('cy', (d) => d.radius * 0.6)
.attr('r', 7)
.attr('fill', '#3b82f6')
.attr('stroke', isDark ? '#1e1e1e' : '#fff')
.attr('stroke-width', 1.5)
.attr('class', 'pointer-events-none')
node
.filter((d) => d.type === 'idea' && d.createdByType === 'human')
.append('text')
.attr('x', (d) => d.radius * 0.6)
.attr('y', (d) => d.radius * 0.6 + 3.5)
.attr('text-anchor', 'middle')
.attr('fill', '#fff')
.attr('font-size', '8px')
.attr('font-weight', 'bold')
.attr('class', 'pointer-events-none')
.text((d) => d.creatorInitial || 'U')
node
.filter((d) => d.type === 'idea' && d.createdByType === 'ai')
.append('text')
.attr('x', (d) => d.radius * 0.55)
.attr('y', (d) => -d.radius * 0.55 + 4)
.attr('text-anchor', 'middle')
.attr('fill', '#a78bfa')
.attr('font-size', '10px')
.attr('class', 'pointer-events-none')
.text('✦')
g.append('text')
.attr('text-anchor', 'middle')
.attr('dy', 80)
.attr('class', 'text-lg font-serif italic pointer-events-none')
.attr('fill', isDark ? '#94a3b8' : '#333')
.text(session.seedIdea.length > 60 ? session.seedIdea.substring(0, 60) + '...' : session.seedIdea)
svg.on('dblclick', (event) => {
if (!onCreateIdeaRef.current) return
if ((event.target as Element).closest('.node')) return
const container = containerRef.current
if (!container) return
const rect = container.getBoundingClientRect()
setEditingNode({ x: event.clientX - rect.left, y: event.clientY - rect.top, parentId: null })
setEditText('')
})
simulation.on('tick', () => {
link
.attr('x1', (d) => (d.source as any).x)
.attr('y1', (d) => (d.source as any).y)
.attr('x2', (d) => (d.target as any).x)
.attr('y2', (d) => (d.target as any).y)
node.attr('transform', (d) => `translate(${d.x},${d.y})`)
})
return () => {
simulation.stop()
}
}, [sessionId, ideasKey, isDark, toSvgCoords, toScreenCoords, playbackIdeas])
useEffect(() => {
if (!nodeRef.current) return
nodeRef.current.selectAll('circle')
.attr('stroke-width', (d: any) => (d.id === selectedNodeId ? 4 : 2))
.style('filter', (d: any) =>
d.id === selectedNodeId
? `drop-shadow(0 0 12px ${d.color}cc)`
: 'none'
)
}, [selectedNodeId])
useEffect(() => {
if (!remoteMove || !nodeRef.current || !linkRef.current || !simulationRef.current) return
const sim = simulationRef.current
const node = nodeRef.current
const link = linkRef.current
node.each(function(d: any) {
if (d.id === remoteMove.ideaId) {
d.fx = remoteMove.x
d.fy = remoteMove.y
}
})
sim.alpha(0.3).restart()
for (let i = 0; i < 30; i++) sim.tick()
sim.stop()
link
.attr('x1', (d: any) => d.source.x)
.attr('y1', (d: any) => d.source.y)
.attr('x2', (d: any) => d.target.x)
.attr('y2', (d: any) => d.target.y)
node.attr('transform', (d: any) => `translate(${d.x},${d.y})`)
}, [remoteMove])
return (
<div
ref={containerRef}
className="w-full h-full relative cursor-grab active:cursor-grabbing bg-[#F8F7F2] dark:bg-[#0A0A0A] bg-[radial-gradient(#e5e7eb_1px,transparent_1px)] dark:bg-[radial-gradient(#ffffff10_1px,transparent_1px)] [background-size:20px_20px]"
>
<svg ref={svgRef} className="w-full h-full" />
{editingNode && (
<div
className="absolute z-50 pointer-events-auto"
style={{
left: editingNode.x,
top: editingNode.y,
transform: 'translate(-50%, 12px)',
}}
>
<div className="w-[260px] bg-white dark:bg-[#1A1A1A] rounded-2xl shadow-2xl border border-black/10 dark:border-white/10 overflow-hidden">
<div className="px-3.5 pt-3 pb-1 flex items-center gap-2">
<div className="w-5 h-5 rounded-md bg-memento-blue/10 flex items-center justify-center">
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="#3b82f6" strokeWidth="2.5" strokeLinecap="round">
<path d="M12 5v14M5 12h14" />
</svg>
</div>
<span className="text-[10px] font-bold uppercase tracking-[0.15em] text-foreground/50">
{editingNode.parentId ? 'Réponse' : 'Nouvelle idée'}
</span>
</div>
<div className="px-3.5 pb-3">
<input
ref={inputRef}
value={editText}
onChange={(e) => setEditText(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && editText.trim()) handleSubmitIdea()
if (e.key === 'Escape') { setEditingNode(null); setEditText('') }
}}
placeholder={editingNode.parentId ? 'Votre réponse…' : 'Votre idée…'}
className="w-full px-3 py-2.5 text-sm font-serif bg-black/[0.03] dark:bg-white/[0.06] border border-black/[0.06] dark:border-white/[0.12] rounded-xl outline-none focus:border-memento-blue/50 focus:bg-white dark:focus:bg-white/[0.08] transition-all placeholder:text-foreground/25 text-foreground"
/>
<div className="flex items-center justify-between mt-1.5 px-0.5">
<div className="flex items-center gap-2.5">
<kbd className="text-[9px] text-foreground/30 font-mono bg-black/[0.04] px-1.5 py-0.5 rounded"></kbd>
<span className="text-[9px] text-foreground/25">enregistrer</span>
<kbd className="text-[9px] text-foreground/30 font-mono bg-black/[0.04] px-1.5 py-0.5 rounded">esc</kbd>
<span className="text-[9px] text-foreground/25">annuler</span>
</div>
{editingNode.parentId && (
<span className="text-[9px] font-medium text-memento-blue/60 flex items-center gap-0.5">
enfant
</span>
)}
</div>
</div>
</div>
<div className="flex justify-center -mt-[1px]">
<div className="w-3 h-3 bg-white dark:bg-[#1A1A1A] border-r border-b border-black/10 dark:border-white/10 rotate-45 -translate-y-1.5" />
</div>
</div>
)}
<div className="absolute bottom-6 left-6 pointer-events-none">
<p className="text-[10px] font-bold tracking-[0.3em] uppercase text-gray-400 dark:text-gray-600 opacity-60">
Double-clic pour ajouter une idée
</p>
</div>
</div>
)
}

View File

@@ -93,6 +93,19 @@ const ACTION_IDS = [
{ id: 'describe-images', icon: ImageIcon, apiPath: '/api/ai/describe-image', body: (_content: string, images?: string[], lang?: string, _format?: string) => ({ imageUrls: images || [], mode: 'description', language: lang || 'fr' }), resultKey: 'descriptions', i18nKey: 'ai.action.describeImages', isImageAction: true },
]
/** API language names sent to `/api/ai/reformulate` for translate targets */
const TRANSLATE_LANGUAGE_OPTIONS: { api: string; labelKey: string }[] = [
{ api: 'French', labelKey: 'languages.targets.french' },
{ api: 'English', labelKey: 'languages.targets.english' },
{ api: 'Spanish', labelKey: 'languages.targets.spanish' },
{ api: 'German', labelKey: 'languages.targets.german' },
{ api: 'Persian', labelKey: 'languages.targets.persian' },
{ api: 'Portuguese', labelKey: 'languages.targets.portuguese' },
{ api: 'Italian', labelKey: 'languages.targets.italian' },
{ api: 'Chinese', labelKey: 'languages.targets.chinese' },
{ api: 'Japanese', labelKey: 'languages.targets.japanese' },
]
// ── Types ─────────────────────────────────────────────────────────────────────
interface GenerateResult {
@@ -238,8 +251,8 @@ export function ContextualAIChat({
tone: selectedTone,
images: noteImages || [],
}
body.noteId = noteId
} else if (chatScope !== 'all') {
// scope is a notebook ID
body.notebookId = chatScope
}
return body
@@ -297,7 +310,7 @@ export function ContextualAIChat({
noteImages.length > 1 ? `**Image ${d.index + 1}:** ${d.description}` : d.description
).join('\n\n')
if (data.combinedSummary) {
resultText += `\n\n---\n**Résumé:** ${data.combinedSummary}`
resultText += `\n\n---\n${t('ai.inlineSummaryMarkdown')} ${data.combinedSummary}`
}
setActionPreview({ label: t(action.i18nKey), text: resultText })
} catch (e: any) {
@@ -355,7 +368,7 @@ export function ContextualAIChat({
setGenerateResult(null)
const toastId = mToast.loading(
type === 'slides' ? '⏳ Génération de la présentation...' : '⏳ Génération du diagramme...',
type === 'slides' ? t('ai.generateSlidesLoading') : t('ai.generateDiagramLoading'),
{ duration: Infinity }
)
@@ -373,7 +386,7 @@ export function ContextualAIChat({
})
const data = await res.json()
if (!res.ok || !data.success) {
mToast.error(data.error || 'Erreur', { id: toastId })
mToast.error(data.error || t('ai.errorShort'), { id: toastId })
setGenerateLoading(null)
return
}
@@ -391,17 +404,17 @@ export function ContextualAIChat({
generatePollRef.current = null
setGenerateLoading(null)
setGenerateResult({ type, canvasId: poll.canvasId, noteId: poll.noteId })
mToast.success('Prêt !', { id: toastId })
mToast.success(t('ai.readyToast'), { id: toastId })
} else if (poll.status === 'failure') {
clearInterval(generatePollRef.current!)
generatePollRef.current = null
setGenerateLoading(null)
mToast.error(poll.error || 'Erreur', { id: toastId })
mToast.error(poll.error || t('ai.errorShort'), { id: toastId })
}
} catch { }
}, 3000)
} catch {
mToast.error('Erreur', { id: toastId })
mToast.error(t('ai.errorShort'), { id: toastId })
setGenerateLoading(null)
}
}
@@ -516,7 +529,7 @@ export function ContextualAIChat({
}),
})
const data = await res.json()
if (!res.ok) throw new Error(data.error || 'Erreur IA')
if (!res.ok) throw new Error(data.error || t('ai.genericError'))
setResourcePreview({ text: data.enrichedContent, source: mode })
} catch (e: any) {
mToast.error(e.message || t('ai.resource.enrichErrorShort'))
@@ -583,9 +596,9 @@ export function ContextualAIChat({
<div className="flex border-b border-border shrink-0 px-2">
{[
{ id: 'actions', label: 'Actions', icon: <Sparkles size={16} /> },
{ id: 'chat', label: 'Discussion', icon: <MessageSquare size={16} /> },
{ id: 'resource', label: 'Ressource', icon: <Link2 size={16} /> },
{ id: 'actions' as const, label: t('ai.assistantTabActions'), icon: <Sparkles size={16} /> },
{ id: 'chat' as const, label: t('ai.chatTab'), icon: <MessageSquare size={16} /> },
{ id: 'resource' as const, label: t('ai.resourceTab'), icon: <Link2 size={16} /> },
].map(tab => (
<button
key={tab.id}
@@ -627,7 +640,7 @@ export function ContextualAIChat({
<div className="absolute inset-0 z-20 flex flex-col bg-memento-paper/95 dark:bg-background/95 backdrop-blur-md animate-in fade-in slide-in-from-top-4 duration-300">
<div className="px-6 py-4 border-b border-border/40 flex items-center justify-between shrink-0">
<p className="text-[10px] font-bold uppercase tracking-widest text-memento-blue">
{resourcePreview.source === 'chat' ? 'Injecter depuis Discussion' : 'Aperçu IA'}
{resourcePreview.source === 'chat' ? t('ai.resourcePreviewInjectFromChat') : t('ai.resourcePreviewAiTitle')}
</p>
<button onClick={() => setResourcePreview(null)} className="text-foreground/40 hover:text-foreground">
<X size={18} />
@@ -661,7 +674,7 @@ export function ContextualAIChat({
<div className="w-20 h-20 rounded-full bg-card/40 backdrop-blur-sm border border-dashed border-border flex items-center justify-center shadow-sm">
<MessageSquare size={32} className="text-memento-blue/60" />
</div>
<p className="text-xs font-serif italic text-foreground/40 leading-relaxed max-w-[200px]">Posez une question à l'Assistant pour commencer.</p>
<p className="text-xs font-serif italic text-foreground/40 leading-relaxed max-w-[200px]">{t('ai.askToStart')}</p>
</div>
)}
{messages.map((msg: UIMessage) => {
@@ -676,9 +689,9 @@ export function ContextualAIChat({
</div>
{isAssistant && onApplyToNote && (hoveredMsgId === msg.id || messages.at(-1)?.id === msg.id) && (
<div className="flex gap-2 mt-3 opacity-0 group-hover:opacity-100 transition-all">
<button onClick={() => handleInjectFromChat(content, 'replace')} className="px-3 py-1.5 rounded-lg text-[9px] font-bold uppercase tracking-widest bg-foreground text-background hover:opacity-90">REPLACER</button>
<button onClick={() => handleInjectFromChat(content, 'complete')} className="px-3 py-1.5 rounded-lg text-[9px] font-bold uppercase tracking-widest bg-card/40 backdrop-blur-sm border border-border text-foreground hover:bg-card/60">COMPLÉTER</button>
<button onClick={() => handleInjectFromChat(content, 'merge')} className="px-3 py-1.5 rounded-lg text-[9px] font-bold uppercase tracking-widest bg-card/40 backdrop-blur-sm border border-border text-foreground hover:bg-card/60">FUSIONNER</button>
<button onClick={() => handleInjectFromChat(content, 'replace')} className="px-3 py-1.5 rounded-lg text-[9px] font-bold uppercase tracking-widest bg-foreground text-background hover:opacity-90">{t('ai.injectReplace')}</button>
<button onClick={() => handleInjectFromChat(content, 'complete')} className="px-3 py-1.5 rounded-lg text-[9px] font-bold uppercase tracking-widest bg-card/40 backdrop-blur-sm border border-border text-foreground hover:bg-card/60">{t('ai.injectComplete')}</button>
<button onClick={() => handleInjectFromChat(content, 'merge')} className="px-3 py-1.5 rounded-lg text-[9px] font-bold uppercase tracking-widest bg-card/40 backdrop-blur-sm border border-border text-foreground hover:bg-card/60">{t('ai.injectMerge')}</button>
</div>
)}
</div>
@@ -699,7 +712,7 @@ export function ContextualAIChat({
<div className="px-6 py-8 border-t border-border shrink-0 space-y-6">
<div className="flex flex-col gap-4">
<div className="space-y-3">
<label className="text-[10px] uppercase tracking-[0.25em] font-bold text-foreground/40 px-1">CONTEXTE</label>
<label className="text-[10px] uppercase tracking-[0.25em] font-bold text-foreground/40 px-1">{t('ai.chatPanelContext')}</label>
<div className="flex flex-col gap-2">
<button
onClick={() => setChatScope('note')}
@@ -709,19 +722,19 @@ export function ContextualAIChat({
)}
>
<BookOpen size={14} className="text-blueprint/60" />
<span>{t('ai.activeNote') || 'Cette note'}</span>
<span className="ml-auto text-[8px] bg-blueprint/10 text-blueprint px-1.5 py-0.5 rounded uppercase font-bold">Auto</span>
<span>{t('ai.thisNote')}</span>
<span className="ml-auto text-[8px] bg-blueprint/10 text-blueprint px-1.5 py-0.5 rounded uppercase font-bold">{t('ai.scopeAutoBadge')}</span>
</button>
<div className="flex items-center gap-2 px-2">
<div className="h-px flex-1 bg-border/40" />
<span className="text-[9px] font-bold text-muted-foreground uppercase tracking-widest">+ Carnet</span>
<span className="text-[9px] font-bold text-muted-foreground uppercase tracking-widest">{t('ai.chatPanelNotebookPlus')}</span>
<div className="h-px flex-1 bg-border/40" />
</div>
<HierarchicalNotebookSelector
notebooks={(notebooks || []).filter(nb => !nb.trashedAt)}
selectedId={chatScope !== 'note' && chatScope !== 'all' ? chatScope : null}
onSelect={(id) => setChatScope(id)}
placeholder="Inclure un carnet..."
placeholder={t('ai.chatNotebookSelectPlaceholder')}
className="w-full"
size="sm"
dropUp
@@ -729,7 +742,7 @@ export function ContextualAIChat({
</div>
</div>
<div className="space-y-3">
<label className="text-[10px] uppercase tracking-[0.25em] font-bold text-foreground/40 px-1">TON D'ÉCRITURE</label>
<label className="text-[10px] uppercase tracking-[0.25em] font-bold text-foreground/40 px-1">{t('ai.chatPanelWritingTone')}</label>
<div className="grid grid-cols-4 gap-1.5">
{TONES.map((tone) => {
const Icon = tone.icon
@@ -758,7 +771,7 @@ export function ContextualAIChat({
<textarea
rows={4}
className="w-full bg-card/60 border border-border rounded-2xl p-5 pr-14 text-sm outline-none focus:border-memento-blue transition-all resize-none leading-relaxed font-light custom-scrollbar shadow-sm text-foreground"
placeholder="Posez votre question sur cette note..."
placeholder={t('ai.chatNoteQuestionPlaceholder')}
value={input}
onChange={e => setInput(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend() } }}
@@ -768,7 +781,7 @@ export function ContextualAIChat({
<button
onClick={() => setWebSearch(!webSearch)}
className={cn("p-2.5 rounded-xl transition-colors", webSearch ? "text-memento-blue bg-memento-blue/10" : "text-foreground/20 hover:text-foreground")}
title="Web Search"
title={t('ai.webSearchLabel')}
>
<Globe size={18} />
</button>
@@ -777,7 +790,7 @@ export function ContextualAIChat({
</button>
</div>
</div>
<p className="text-[9px] text-foreground/30 text-center mt-2 uppercase tracking-[0.2em] font-bold italic">Maj+Entrée = nouvelle ligne</p>
<p className="text-[9px] text-foreground/30 text-center mt-2 uppercase tracking-[0.2em] font-bold italic">{t('ai.newLineHint')}</p>
</div>
</motion.div>
)}
@@ -855,18 +868,18 @@ export function ContextualAIChat({
>
<div className="mt-2 p-5 bg-card/40 backdrop-blur-sm border border-memento-blue/30 rounded-2xl space-y-5 shadow-sm">
<div className="grid grid-cols-3 gap-2">
{['Français', 'English', 'Español', 'Deutsch', 'Persan', 'Portugais', 'Italiano', 'Chinois', 'Japonais'].map((lang) => (
{TRANSLATE_LANGUAGE_OPTIONS.map(({ api, labelKey }) => (
<button
key={lang}
onClick={() => setTranslateTarget(lang)}
key={api}
onClick={() => setTranslateTarget(api)}
className={cn(
"py-2 px-1 rounded-lg border text-[10px] font-bold uppercase tracking-tighter transition-all",
translateTarget === lang
? "bg-memento-blue border-memento-blue text-white shadow-md shadow-memento-blue/20"
translateTarget === api
? "bg-memento-blue border-memento-blue text-white shadow-md shadow-memento-blue/20"
: "bg-card/60 border-border text-foreground/60 hover:border-foreground/20"
)}
>
{lang}
{t(labelKey)}
</button>
))}
</div>
@@ -880,7 +893,7 @@ export function ContextualAIChat({
setCustomLangInput(e.target.value)
setTranslateTarget(e.target.value)
}}
placeholder="ex: Arabe, Russe..."
placeholder={t('languages.customPlaceholder')}
className="w-full bg-card/60 border border-border rounded-xl px-4 py-2.5 text-[11px] outline-none focus:border-memento-blue transition-all text-foreground"
/>
</div>
@@ -951,8 +964,8 @@ export function ContextualAIChat({
<span className="text-[8px] uppercase tracking-[0.2em] font-bold text-foreground/40 px-1">{t('ai.generate.style')}</span>
<select value={slideStyle} onChange={e => setSlideStyle(e.target.value)} className="w-full bg-card/60 border border-border rounded-lg px-2 py-2 text-[10px] outline-none focus:ring-1 ring-memento-blue/10 transition-all cursor-pointer text-foreground">
<option value="professional">{t('ai.generate.styleProfessional')}</option>
<option value="creative">Creative</option>
<option value="brutalist">Brutalist</option>
<option value="creative">{t('ai.generate.styleCreative')}</option>
<option value="brutalist">{t('ai.generate.styleBrutalist')}</option>
</select>
</div>
</div>
@@ -968,14 +981,14 @@ export function ContextualAIChat({
>
<div className="flex items-center justify-between">
<span className="text-[9px] font-bold text-memento-blue uppercase tracking-widest flex items-center gap-1.5">
<Check size={12} /> Présentation prête
<Check size={12} /> {t('ai.presentationReadyBadge')}
</span>
<a
href={`/lab?id=${generateResult.canvasId}`}
target="_blank"
rel="noopener noreferrer"
className="p-1.5 bg-card/60 rounded-lg text-foreground/50 hover:text-foreground hover:bg-card transition-colors"
title="Voir dans L'Atelier"
title={t('ai.openInLabTitle')}
>
<ExternalLink size={12} />
</a>
@@ -1001,13 +1014,13 @@ export function ContextualAIChat({
document.body.removeChild(a)
URL.revokeObjectURL(url)
} catch {
mToast.error('Échec du téléchargement')
mToast.error(t('ai.downloadFailedToast'))
}
}}
className="flex items-center justify-center gap-2 w-full py-2.5 bg-memento-blue text-white rounded-lg text-[10px] font-bold uppercase tracking-[0.15em] hover:opacity-90 transition-opacity shadow-sm"
>
<Download size={13} />
Télécharger .pptx
{t('ai.pptxDownloadButton')}
</button>
</motion.div>
)}

View File

@@ -0,0 +1,211 @@
'use client'
import { useState, useRef, useEffect } from 'react'
import { useChat } from '@ai-sdk/react'
import { DefaultChatTransport } from 'ai'
import { useLanguage } from '@/lib/i18n'
import { X, Send, FileText, Sparkles, Loader2, User, Plus, Square } from 'lucide-react'
interface Attachment {
id: string
fileName: string
}
interface DocumentQAOverlayProps {
attachment: Attachment
noteId: string
noteContent?: string
onClose: () => void
onApplyToNote?: (content: string) => void
}
function getMessageContent(msg: any): string {
if (typeof msg.content === 'string') return msg.content
if (msg.parts && Array.isArray(msg.parts)) {
return msg.parts
.filter((p: any) => p.type === 'text' && typeof p.text === 'string')
.map((p: any) => p.text)
.join('')
}
return ''
}
export function DocumentQAOverlay({ attachment, noteId, noteContent, onClose, onApplyToNote }: DocumentQAOverlayProps) {
const { t } = useLanguage()
const messagesEndRef = useRef<HTMLDivElement>(null)
const textareaRef = useRef<HTMLTextAreaElement>(null)
const [input, setInput] = useState('')
const pdfUrl = `/api/notes/${noteId}/attachments/${attachment.id}?download=true`
const transport = useRef(new DefaultChatTransport({
api: '/api/chat',
body: {
noteId,
noteContext: {
title: attachment.fileName,
content: noteContent || '',
tone: 'professional',
},
webSearch: false,
},
})).current
const { messages, sendMessage, status, stop } = useChat({ transport })
const isLoading = status === 'submitted' || status === 'streaming'
const lastAssistantContent = [...messages].reverse().find(m => m.role === 'assistant')
const lastAssistantText = lastAssistantContent ? getMessageContent(lastAssistantContent) : ''
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
}, [messages])
useEffect(() => {
const handleEsc = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose()
}
document.addEventListener('keydown', handleEsc)
return () => document.removeEventListener('keydown', handleEsc)
}, [onClose])
const handleSend = async () => {
const text = input.trim()
if (!text || isLoading) return
setInput('')
try {
await sendMessage({ text })
} catch {
}
}
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
handleSend()
}
}
return (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-6 bg-black/40 backdrop-blur-sm">
<div className="w-full max-w-6xl h-[88vh] bg-background border border-border rounded-2xl shadow-2xl flex overflow-hidden">
{/* Left: PDF Preview */}
<div className="flex-1 flex flex-col border-r border-border">
<div className="p-4 border-b border-border flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="p-2 bg-primary/10 text-primary rounded-lg">
<FileText size={18} />
</div>
<div className="min-w-0">
<h3 className="text-sm font-semibold truncate max-w-[300px]">{attachment.fileName}</h3>
<p className="text-[9px] uppercase font-bold tracking-widest text-muted-foreground">
PDF
</p>
</div>
</div>
<button onClick={onClose} className="p-2 hover:bg-muted rounded-full text-muted-foreground transition-colors">
<X size={18} />
</button>
</div>
<div className="flex-1 bg-muted/30 overflow-hidden">
<iframe
src={pdfUrl}
className="w-full h-full border-0"
title={attachment.fileName}
/>
</div>
</div>
{/* Right: Chat */}
<div className="w-[400px] flex flex-col">
<div className="p-4 border-b border-border flex items-center gap-2">
<Sparkles size={16} className="text-primary" />
<h4 className="text-[11px] font-bold uppercase tracking-widest">
{t('attachments.docExpert') || 'Document Expert'}
</h4>
</div>
<div className="flex-1 p-4 overflow-y-auto space-y-3">
{messages.length === 0 && (
<div className="flex flex-col items-center justify-center h-full gap-3 text-center">
<div className="w-14 h-14 rounded-full bg-primary/10 flex items-center justify-center">
<Sparkles size={24} className="text-primary/60" />
</div>
<p className="text-xs text-muted-foreground max-w-[220px] leading-relaxed">
{t('attachments.docQaWelcome') || `Posez une question sur "${attachment.fileName}".`}
</p>
</div>
)}
{messages.map((msg, i) => {
const content = getMessageContent(msg)
if (!content) return null
return (
<div key={msg.id || i} className={`flex gap-2.5 ${msg.role === 'user' ? 'justify-end' : ''}`}>
{msg.role === 'assistant' && (
<div className="w-6 h-6 rounded-full bg-primary/10 flex items-center justify-center shrink-0 mt-0.5">
<Sparkles size={10} className="text-primary" />
</div>
)}
<div
className={`max-w-[85%] rounded-2xl px-3.5 py-2.5 text-[13px] leading-relaxed whitespace-pre-wrap ${
msg.role === 'user'
? 'bg-primary text-primary-foreground'
: 'bg-muted/50 border border-border'
}`}
>
{content}
</div>
{msg.role === 'user' && (
<div className="w-6 h-6 rounded-full bg-muted flex items-center justify-center shrink-0 mt-0.5">
<User size={10} className="text-muted-foreground" />
</div>
)}
</div>
)
})}
{isLoading && !lastAssistantText && (
<div className="flex items-center gap-2 text-muted-foreground">
<Loader2 size={14} className="animate-spin" />
<span className="text-xs">{t('attachments.thinking') || 'Thinking...'}</span>
</div>
)}
<div ref={messagesEndRef} />
</div>
{lastAssistantText && onApplyToNote && (
<div className="px-4 py-2 border-t border-border">
<button
onClick={() => onApplyToNote(lastAssistantText)}
className="w-full flex items-center justify-center gap-2 py-2 text-[11px] font-bold uppercase tracking-widest text-primary hover:bg-primary/10 rounded-lg transition-colors"
>
<Plus size={14} />
{t('attachments.addToNote') || 'Ajouter à la note'}
</button>
</div>
)}
<div className="p-3 border-t border-border">
<div className="relative">
<textarea
ref={textareaRef}
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={t('attachments.askPlaceholder') || 'Ask about this document...'}
className="w-full bg-muted/50 border border-border rounded-xl p-3 pr-11 text-sm outline-none focus:border-primary transition-all resize-none"
rows={2}
disabled={isLoading}
/>
<button
onClick={isLoading ? stop : handleSend}
disabled={!isLoading && !input.trim()}
className="absolute right-2 bottom-2 p-2 bg-primary text-primary-foreground rounded-lg shadow-sm disabled:opacity-50 transition-all"
>
{isLoading ? <Square size={14} /> : <Send size={14} />}
</button>
</div>
</div>
</div>
</div>
</div>
)
}

View File

@@ -111,7 +111,7 @@ export function HierarchicalNotebookSelector({
if (!searchQuery) setIsOpen(false)
}}
className={`flex items-center gap-2.5 px-2 py-1.5 rounded-lg cursor-pointer transition-all group
${isSelected ? 'bg-blueprint/10 text-blueprint font-bold dark:bg-blueprint/10' : 'hover:bg-muted dark:hover:bg-white/5 text-ink'}`}
${isSelected ? 'bg-memento-blue/15 text-memento-blue font-bold dark:bg-memento-blue/20' : 'hover:bg-muted dark:hover:bg-white/5 text-ink'}`}
>
<div className="w-4 flex items-center justify-center">
{hasChildren ? (
@@ -124,7 +124,7 @@ export function HierarchicalNotebookSelector({
) : null}
</div>
<div className={`p-1 rounded ${isSelected ? 'bg-blueprint/20 dark:bg-blueprint/20' : 'bg-muted/50 dark:bg-white/5 group-hover:bg-white/40'}`}>
<div className={`p-1 rounded ${isSelected ? 'bg-memento-blue/25 dark:bg-memento-blue/30' : 'bg-muted/50 dark:bg-white/5 group-hover:bg-white/40'}`}>
{isExpanded && hasChildren ? <FolderOpen size={13} /> : <Folder size={13} />}
</div>
@@ -157,9 +157,9 @@ export function HierarchicalNotebookSelector({
<div
ref={triggerRef}
onClick={() => setIsOpen(prev => !prev)}
className={`w-full bg-card dark:bg-white/5 border border-border/80 rounded-xl outline-none focus:ring-4 ring-blueprint/5 focus:border-blueprint/40 transition-all cursor-pointer text-ink flex items-center gap-3 ${size === 'sm' ? 'px-3 py-2 text-xs' : 'px-4 py-3 text-sm'}`}
className={`w-full bg-card dark:bg-white/5 border border-border/80 rounded-xl outline-none focus:ring-4 ring-memento-blue/10 focus:border-memento-blue/40 transition-all cursor-pointer text-ink flex items-center gap-3 ${size === 'sm' ? 'px-3 py-2 text-xs' : 'px-4 py-3 text-sm'}`}
>
<Folder size={size === 'sm' ? 14 : 16} className="text-blueprint/60 shrink-0" />
<Folder size={size === 'sm' ? 14 : 16} className="text-memento-blue/70 shrink-0" />
<div className="flex-1 flex items-center gap-1 min-w-0">
{path.length > 0 ? (
<div className="flex items-center gap-1.5 truncate">
@@ -201,7 +201,7 @@ export function HierarchicalNotebookSelector({
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder={placeholder}
className="w-full bg-card border border-border rounded-lg pl-9 pr-4 py-2 text-xs outline-none focus:border-blueprint transition-colors"
className="w-full bg-card border border-border rounded-lg pl-9 pr-4 py-2 text-xs outline-none focus:border-memento-blue transition-colors"
/>
</div>
</div>
@@ -216,7 +216,7 @@ export function HierarchicalNotebookSelector({
</span>
<button
onClick={() => setIsOpen(false)}
className="text-[10px] font-bold text-blueprint hover:underline"
className="text-[10px] font-bold text-memento-blue hover:underline"
>
Close
</button>

View File

@@ -488,7 +488,7 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
))}
</div>
)}
<h1 className="font-memento-serif text-4xl font-medium tracking-tight text-foreground leading-tight pr-12">
<h1 className="font-memento-serif text-4xl font-medium tracking-tight text-foreground leading-tight pe-12">
{currentNotebook
? currentNotebook.name
: searchParams.get('shared') === '1'
@@ -600,10 +600,10 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
<button
onClick={() => setOrganizeNotebookOpen(true)}
className="flex items-center gap-2 text-[13px] text-blueprint font-medium hover:opacity-70 transition-opacity"
title="Organiser ce carnet avec l'IA"
title={t('notebook.organizeNotebookWithAITooltip')}
>
<Sparkles size={16} />
<span>Organiser</span>
<span>{t('batch.organize')}</span>
</button>
)}
</div>
@@ -616,7 +616,7 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
"flex items-center gap-2 text-[13px] font-medium transition-opacity",
initialSettings.aiAssistantEnabled ? "text-foreground hover:opacity-70" : "text-muted-foreground opacity-50 cursor-not-allowed"
)}
title={initialSettings.aiAssistantEnabled ? t('notebook.summary') : "Activez l'Assistant IA dans les paramètres pour résumer"}
title={initialSettings.aiAssistantEnabled ? t('notebook.summary') : t('notebook.assistantRequiredForSummarize')}
>
<FileText size={16} />
<span>{t('notebook.summary') || 'Summarize'}</span>
@@ -701,7 +701,7 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
{selectedTagIds.length > 0 && (
<button
onClick={() => setSelectedTagIds([])}
className="px-3 py-1.5 text-[10px] font-bold uppercase tracking-wider text-red-500 hover:underline ml-auto"
className="px-3 py-1.5 text-[10px] font-bold uppercase tracking-wider text-red-500 hover:underline ms-auto"
>
{t('labels.clearAll') || 'Clear all'}
</button>

View File

@@ -37,12 +37,12 @@ export function LabelBadge({
isSelected
? 'bg-foreground text-background border-foreground shadow-sm'
: isAI
? 'bg-[#75B2D6]/10 border-[#75B2D6]/25 text-[#75B2D6]'
? 'bg-[#A47148]/10 border-[#A47148]/25 text-[#A47148]'
: 'bg-[#8D8D8D]/10 border-[#8D8D8D]/25 text-[#8D8D8D]',
)}
>
{isAI && (
<Sparkles size={8} className="text-[#75B2D6]/70" />
<Sparkles size={8} className="text-[#A47148]/70" />
)}
<span className="truncate">{label}</span>
{onRemove && (
@@ -66,8 +66,8 @@ export function LabelBadge({
)}
{isAI && !isSelected && (
<span className="relative flex h-1.5 w-1.5">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-[#75B2D6] opacity-75" />
<span className="relative inline-flex rounded-full h-1.5 w-1.5 bg-[#75B2D6]" />
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-[#A47148] opacity-75" />
<span className="relative inline-flex rounded-full h-1.5 w-1.5 bg-[#A47148]" />
</span>
)}
</button>

View File

@@ -46,6 +46,7 @@ export function MemoryEchoNotification({ onOpenNote }: MemoryEchoNotificationPro
const [fusionNotes, setFusionNotes] = useState<Array<Partial<Note>>>([])
const [demoMode, setDemoMode] = useState(false)
const pollingRef = useRef<ReturnType<typeof setInterval> | null>(null)
const dismissedPermanently = useRef(false)
// Fetch insight on mount
useEffect(() => {
@@ -63,8 +64,6 @@ export function MemoryEchoNotification({ onOpenNote }: MemoryEchoNotificationPro
if (data.insight) {
setInsight(data.insight)
// If we got an insight, check if user is in demo mode
setDemoMode(true)
}
} catch (error) {
console.error('[MemoryEcho] Failed to fetch insight:', error)
@@ -73,28 +72,8 @@ export function MemoryEchoNotification({ onOpenNote }: MemoryEchoNotificationPro
}
}
// Start polling in demo mode after first dismiss
useEffect(() => {
if (isDismissed && demoMode && !pollingRef.current) {
pollingRef.current = setInterval(async () => {
try {
const res = await fetch('/api/ai/echo')
const data = await res.json()
if (data.insight) {
setInsight(data.insight)
setIsDismissed(false)
}
} catch {
// silent
}
}, 15000) // Poll every 15s
}
return () => {
if (pollingRef.current) {
clearInterval(pollingRef.current)
pollingRef.current = null
}
}
if (dismissedPermanently.current) return
}, [isDismissed, demoMode])
const handleView = async () => {
@@ -158,6 +137,11 @@ export function MemoryEchoNotification({ onOpenNote }: MemoryEchoNotificationPro
const handleDismiss = () => {
setIsDismissed(true)
dismissedPermanently.current = true
if (pollingRef.current) {
clearInterval(pollingRef.current)
pollingRef.current = null
}
}
if (isLoading || !insight) {

View File

@@ -0,0 +1,247 @@
'use client'
import { useState, useEffect, useRef, useCallback } from 'react'
import { useLanguage } from '@/lib/i18n'
import { FileText, Loader2, MessageSquare, Trash2, AlertCircle, Plus } from 'lucide-react'
import { toast } from 'sonner'
interface Attachment {
id: string
fileName: string
fileSize: number
mimeType: string
status: 'pending' | 'processing' | 'ready' | 'failed'
pageCount: number | null
error: string | null
createdAt: string
}
interface NoteAttachmentsProps {
noteId: string
onOpenDocQA: (attachment: Attachment) => void
onCountChange?: (count: number) => void
triggerUpload?: number
}
function formatFileSize(bytes: number): string {
if (bytes < 1024) return `${bytes} B`
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
}
export function NoteAttachments({ noteId, onOpenDocQA, onCountChange, triggerUpload }: NoteAttachmentsProps) {
const { t } = useLanguage()
const [attachments, setAttachments] = useState<Attachment[]>([])
const [uploading, setUploading] = useState(false)
const [loading, setLoading] = useState(true)
const fileInputRef = useRef<HTMLInputElement>(null)
const sectionRef = useRef<HTMLDivElement>(null)
const pollingRef = useRef<ReturnType<typeof setInterval> | null>(null)
const fetchAttachments = useCallback(async () => {
try {
const res = await fetch(`/api/notes/${noteId}/attachments`)
if (res.ok) {
const data = await res.json()
const list = data.data || []
setAttachments(list)
onCountChange?.(list.length)
return list
}
} catch {}
return []
}, [noteId])
useEffect(() => {
fetchAttachments().finally(() => setLoading(false))
}, [fetchAttachments])
useEffect(() => {
onCountChange?.(attachments.length)
}, [attachments.length, onCountChange])
useEffect(() => {
const hasPending = attachments.some(a => a.status === 'pending' || a.status === 'processing')
if (hasPending) {
pollingRef.current = setInterval(fetchAttachments, 3000)
} else if (pollingRef.current) {
clearInterval(pollingRef.current)
pollingRef.current = null
}
return () => {
if (pollingRef.current) clearInterval(pollingRef.current)
}
}, [attachments, fetchAttachments])
useEffect(() => {
if (triggerUpload && triggerUpload > 0) {
if (attachments.length > 0) {
sectionRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' })
}
fileInputRef.current?.click()
}
}, [triggerUpload, attachments.length])
const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (!file) return
if (file.type !== 'application/pdf') {
toast.error(t('attachments.onlyPdf') || 'Only PDF files are supported')
return
}
if (file.size > 20 * 1024 * 1024) {
toast.error(t('attachments.maxSize') || 'File too large (max 20MB)')
return
}
setUploading(true)
try {
const formData = new FormData()
formData.append('file', file)
const res = await fetch(`/api/notes/${noteId}/attachments`, {
method: 'POST',
body: formData,
})
if (res.ok) {
const data = await res.json()
setAttachments(prev => [data.data, ...prev])
toast.success(t('attachments.uploaded') || 'File uploaded — analyzing...')
await fetchAttachments()
setTimeout(() => {
sectionRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' })
}, 200)
} else {
const err = await res.json()
toast.error(err.error || t('attachments.uploadFailed') || 'Upload failed')
}
} catch {
toast.error(t('attachments.uploadError') || 'Upload error')
} finally {
setUploading(false)
if (fileInputRef.current) fileInputRef.current.value = ''
}
}
const handleDelete = async (attachmentId: string) => {
try {
const res = await fetch(`/api/notes/${noteId}/attachments/${attachmentId}`, { method: 'DELETE' })
if (res.ok) {
setAttachments(prev => prev.filter(a => a.id !== attachmentId))
toast.success(t('attachments.deleted') || 'Attachment removed')
}
} catch {}
}
if (loading) return null
return (
<>
<input ref={fileInputRef} type="file" accept=".pdf" className="hidden" onChange={handleUpload} />
{attachments.length > 0 && (
<div ref={sectionRef} className="pt-8">
<div className="flex items-center justify-between mb-4">
<h4 className="text-[11px] uppercase font-bold tracking-[.2em] text-muted-foreground">
{t('attachments.title') || 'Documents'}
</h4>
<button
onClick={() => fileInputRef.current?.click()}
disabled={uploading}
className="flex items-center gap-1 text-[11px] text-muted-foreground hover:text-foreground transition-colors disabled:opacity-50"
>
<Plus size={12} />
</button>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{attachments.map(att => (
<div
key={att.id}
className="relative group border border-border rounded-2xl bg-white dark:bg-white/[0.03] overflow-hidden transition-all hover:border-foreground/15"
>
<button
onClick={() => handleDelete(att.id)}
className="absolute top-2 right-2 p-1 rounded-lg text-muted-foreground/40 hover:text-destructive hover:bg-destructive/10 transition-all z-10"
title={t('attachments.remove') || 'Remove'}
>
<Trash2 size={12} />
</button>
{(att.status === 'pending' || att.status === 'processing') && (
<div className="p-5">
<div className="flex items-center gap-3 mb-3">
<div className="p-2.5 bg-primary/10 text-primary rounded-xl">
<FileText size={20} />
</div>
<div className="min-w-0 flex-1">
<p className="text-xs font-medium truncate">{att.fileName}</p>
<p className="text-[10px] text-muted-foreground">{formatFileSize(att.fileSize)}</p>
</div>
</div>
<div className="flex items-center gap-2 px-1">
<Loader2 size={12} className="animate-spin text-primary" />
<span className="text-[11px] text-primary font-medium">
{t('attachments.analyzing') || 'Analyzing document...'}
</span>
</div>
</div>
)}
{att.status === 'ready' && (
<button
onClick={() => onOpenDocQA(att)}
className="w-full text-left p-5"
>
<div className="flex items-center gap-3 mb-4">
<div className="p-2.5 bg-primary/10 text-primary rounded-xl">
<FileText size={20} />
</div>
<div className="min-w-0 flex-1 pr-4">
<p className="text-xs font-medium truncate">{att.fileName}</p>
<p className="text-[10px] text-muted-foreground">
{formatFileSize(att.fileSize)}
{att.pageCount ? ` · ${att.pageCount} pages` : ''}
</p>
</div>
</div>
<div className="flex items-center gap-2 px-1">
<MessageSquare size={12} className="text-primary" />
<span className="text-[11px] text-primary font-medium">
{t('attachments.askQuestions') || 'Ask questions about this document'}
</span>
</div>
</button>
)}
{att.status === 'failed' && (
<div className="p-5">
<div className="flex items-center gap-3 mb-3">
<div className="p-2.5 bg-destructive/10 text-destructive rounded-xl">
<FileText size={20} />
</div>
<div className="min-w-0 flex-1">
<p className="text-xs font-medium truncate">{att.fileName}</p>
<p className="text-[10px] text-destructive">
{att.error || (t('attachments.processingFailed') || 'Processing failed')}
</p>
</div>
</div>
</div>
)}
</div>
))}
</div>
</div>
)}
{uploading && (
<div className="flex items-center gap-2 text-xs text-muted-foreground pt-4">
<Loader2 size={14} className="animate-spin" />
<span>{t('attachments.uploading') || 'Uploading...'}</span>
</div>
)}
</>
)
}

View File

@@ -482,7 +482,7 @@ export const NoteCard = memo(function NoteCard({
>
{/* Drag Handle - Only visible on mobile/touch devices */}
<div
className="muuri-drag-handle absolute top-2 left-2 z-20 cursor-grab active:cursor-grabbing p-2 md:hidden"
className="muuri-drag-handle absolute top-2 start-2 z-20 cursor-grab active:cursor-grabbing p-2 md:hidden"
aria-label={t('notes.dragToReorder') || 'Drag to reorder'}
title={t('notes.dragToReorder') || 'Drag to reorder'}
>
@@ -490,7 +490,7 @@ export const NoteCard = memo(function NoteCard({
</div>
{/* Move to Notebook Dropdown Menu — hidden in trash */}
{!isTrashView && <div onClick={(e) => e.stopPropagation()} className="absolute top-2 right-2 z-20">
{!isTrashView && <div onClick={(e) => e.stopPropagation()} className="absolute top-2 end-2 z-20">
<DropdownMenu open={showNotebookMenu} onOpenChange={setShowNotebookMenu}>
<DropdownMenuTrigger asChild>
<Button
@@ -507,7 +507,7 @@ export const NoteCard = memo(function NoteCard({
{t('notebookSuggestion.moveToNotebook')}
</div>
<DropdownMenuItem onClick={() => handleMoveToNotebook(null)}>
<StickyNote className="h-4 w-4 mr-2" />
<StickyNote className="h-4 w-4 me-2" />
{t('notebookSuggestion.generalNotes')}
</DropdownMenuItem>
{notebooks.filter(nb => !nb.parentId && !nb.trashedAt).map((notebook: any) => {
@@ -523,11 +523,11 @@ export const NoteCard = memo(function NoteCard({
<DropdownMenuSubTrigger className="gap-2">
<NotebookIcon className="h-4 w-4" />
{notebook.name}
<ChevronRight className="h-3 w-3 ml-auto opacity-50" />
<ChevronRight className="h-3 w-3 ms-auto opacity-50 rtl:rotate-180" />
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuItem onClick={() => handleMoveToNotebook(notebook.id)}>
<NotebookIcon className="h-4 w-4 mr-2" />
<NotebookIcon className="h-4 w-4 me-2" />
{notebook.name}
</DropdownMenuItem>
{descendants.map((child: any) => {
@@ -544,8 +544,8 @@ export const NoteCard = memo(function NoteCard({
})()
return (
<DropdownMenuItem key={child.id} onClick={() => handleMoveToNotebook(child.id)}>
<NotebookIcon className="h-4 w-4 mr-2" />
<span className="ml-{depth * 2}">{child.name}</span>
<NotebookIcon className="h-4 w-4 me-2" />
<span style={{ marginInlineStart: `${depth * 0.5}rem` }}>{child.name}</span>
</DropdownMenuItem>
)
})}
@@ -555,14 +555,14 @@ export const NoteCard = memo(function NoteCard({
}
return (
<DropdownMenuItem key={notebook.id} onClick={() => handleMoveToNotebook(notebook.id)}>
<NotebookIcon className="h-4 w-4 mr-2" />
<NotebookIcon className="h-4 w-4 me-2" />
{notebook.name}
</DropdownMenuItem>
)
})}
<DropdownMenuSeparator />
<DropdownMenuItem className="text-muted-foreground" onSelect={() => onCreateSubNotebook?.()}>
<Plus className="h-4 w-4 mr-2" />
<Plus className="h-4 w-4 me-2" />
{t('notebook.createSubNotebook') || 'Nouveau sous-carnet…'}
</DropdownMenuItem>
</DropdownMenuContent>
@@ -575,7 +575,7 @@ export const NoteCard = memo(function NoteCard({
size="sm"
data-testid="pin-button"
className={cn(
"absolute top-2 right-12 z-20 h-8 w-8 p-0 rounded-md transition-opacity",
"absolute top-2 end-12 z-20 h-8 w-8 p-0 rounded-md transition-opacity",
optimisticNote.isPinned ? "opacity-100" : "opacity-0 group-hover:opacity-100"
)}
onClick={(e) => {
@@ -593,7 +593,7 @@ export const NoteCard = memo(function NoteCard({
{/* Reminder Icon - Move slightly if pin button is there */}
{note.reminder && new Date(note.reminder) > new Date() && (
<Bell
className="absolute top-3 right-10 h-4 w-4 text-primary"
className="absolute top-3 end-10 h-4 w-4 text-primary"
/>
)}
@@ -604,7 +604,7 @@ export const NoteCard = memo(function NoteCard({
{t('memoryEcho.fused')}
<button
onClick={handleRemoveFusedBadge}
className="ml-1 opacity-0 group-hover/badge:opacity-100 hover:opacity-100 transition-opacity"
className="ms-1 opacity-0 group-hover/badge:opacity-100 hover:opacity-100 transition-opacity"
title={t('notes.remove') || 'Remove'}
>
<Trash2 className="h-2.5 w-2.5" />
@@ -614,7 +614,7 @@ export const NoteCard = memo(function NoteCard({
{/* Title */}
{note.title && (
<h3 dir="auto" className="text-lg font-heading font-semibold mb-2 pr-20 text-foreground leading-tight tracking-tight flex items-center gap-2">
<h3 dir="auto" className="text-lg font-heading font-semibold mb-2 pe-20 text-foreground leading-tight tracking-tight flex items-center gap-2">
{(() => {
const TypeIcon = NOTE_TYPE_ICONS[note.type] || AlignLeft
return <TypeIcon className="h-4 w-4 shrink-0 text-muted-foreground/50" />
@@ -653,7 +653,7 @@ export const NoteCard = memo(function NoteCard({
handleLeaveShare()
}}
>
<LogOut className="h-3 w-3 mr-1" />
<LogOut className="h-3 w-3 me-1" />
{t('notes.leaveShare')}
</Button>
</div>
@@ -727,7 +727,7 @@ export const NoteCard = memo(function NoteCard({
{owner && (
<div
className={cn(
"absolute bottom-2 left-2 z-20",
"absolute bottom-2 start-2 z-20",
"w-6 h-6 rounded-full text-white text-[10px] font-semibold flex items-center justify-center",
getAvatarColor(owner.name || owner.email || 'Unknown')
)}
@@ -758,7 +758,7 @@ export const NoteCard = memo(function NoteCard({
noteId={note.id}
currentReminder={reminderDate}
onUpdateReminder={handleUpdateReminder}
className="absolute bottom-0 left-0 right-0 p-2 opacity-100 md:opacity-0 group-hover:opacity-100 transition-opacity"
className="absolute bottom-0 start-0 end-0 p-2 opacity-100 md:opacity-0 group-hover:opacity-100 transition-opacity"
/>
)}
@@ -776,7 +776,7 @@ export const NoteCard = memo(function NoteCard({
)}
{/* Connections Badge - Bottom right (spec: amber, absolute) */}
<div className="absolute bottom-2 right-2 z-10">
<div className="absolute bottom-2 end-2 z-10">
<ConnectionsBadge
noteId={note.id}
onClick={() => {

View File

@@ -2,9 +2,11 @@
import { useState, useMemo } from 'react'
import { Note } from '@/lib/types'
import { format, formatDistanceToNow } from 'date-fns'
import { formatDistanceToNow } from 'date-fns'
import { fr } from 'date-fns/locale/fr'
import { enUS } from 'date-fns/locale/en-US'
import { faIR } from 'date-fns/locale/fa-IR'
import { formatAbsoluteDateLocalized } from '@/lib/utils/format-localized-date'
import { X, Info, Clock, Hash, Book, FileText, Calendar, Tag, ChevronRight, Trash2, RotateCcw, Loader2, Check, History as HistoryIcon } from 'lucide-react'
import { cn } from '@/lib/utils'
import { useLanguage } from '@/lib/i18n'
@@ -24,7 +26,9 @@ interface NoteDocumentInfoPanelProps {
}
function getLocale(lang: string) {
return lang === 'fr' ? fr : enUS
if (lang === 'fr') return fr
if (lang === 'fa') return faIR
return enUS
}
function wordCount(text: string) {
@@ -35,13 +39,6 @@ function charCount(text: string) {
return text.replace(/<[^>]+>/g, '').length
}
const noteTypeLabel: Record<string, string> = {
richtext: 'Rich Text',
markdown: 'Markdown',
text: 'Texte',
checklist: 'Liste de tâches',
}
export function NoteDocumentInfoPanel({ note, content, onClose, onNoteRestored }: NoteDocumentInfoPanelProps) {
const { t, language } = useLanguage()
const { notebooks } = useNotebooks()
@@ -56,6 +53,16 @@ export function NoteDocumentInfoPanel({ note, content, onClose, onNoteRestored }
const [isRestoring, setIsRestoring] = useState<string | null>(null)
const locale = getLocale(language)
const displayNoteType = useMemo(() => {
const map: Record<string, string> = {
richtext: t('notes.noteTypes.richtext'),
markdown: t('notes.noteTypes.markdown'),
text: t('notes.noteTypes.text'),
checklist: t('notes.noteTypes.checklist'),
}
return map[note.type] || note.type
}, [t, note.type])
useEffect(() => {
if (activeTab === 'versions' && historyEnabled) {
loadHistory()
@@ -75,7 +82,7 @@ export function NoteDocumentInfoPanel({ note, content, onClose, onNoteRestored }
}
const handleDeleteVersion = async (entryId: string) => {
if (!confirm('Supprimer cette version ?')) return
if (!confirm(t('documentInfo.deleteVersionConfirm'))) return
setIsDeleting(entryId)
try {
await deleteNoteHistoryEntry(note.id, entryId)
@@ -131,7 +138,7 @@ export function NoteDocumentInfoPanel({ note, content, onClose, onNoteRestored }
>
{tab === 'info' && <Info className="h-3 w-3" />}
{tab === 'versions' && <Clock className="h-3 w-3" />}
{tab === 'info' ? 'Info' : 'Versions'}
{tab === 'info' ? t('documentInfo.tabInfo') : t('documentInfo.tabVersions')}
</button>
))}
</div>
@@ -153,11 +160,11 @@ export function NoteDocumentInfoPanel({ note, content, onClose, onNoteRestored }
<div className="grid grid-cols-2 border-b border-border/30">
<div className="flex flex-col items-center gap-1 py-6 border-r border-border/30">
<span className="text-4xl font-bold font-memento-serif tabular-nums tracking-tight">{words}</span>
<span className="text-[10px] uppercase tracking-[0.2em] text-muted-foreground font-semibold">mots</span>
<span className="text-[10px] uppercase tracking-[0.2em] text-muted-foreground font-semibold">{t('documentInfo.wordsLabel')}</span>
</div>
<div className="flex flex-col items-center gap-1 py-6">
<span className="text-4xl font-bold font-memento-serif tabular-nums tracking-tight">{chars}</span>
<span className="text-[10px] uppercase tracking-[0.2em] text-muted-foreground font-semibold">caractères</span>
<span className="text-[10px] uppercase tracking-[0.2em] text-muted-foreground font-semibold">{t('documentInfo.charactersLabel')}</span>
</div>
</div>
@@ -166,7 +173,7 @@ export function NoteDocumentInfoPanel({ note, content, onClose, onNoteRestored }
<div className="flex items-start gap-3 px-4 py-3">
<Book className="h-3.5 w-3.5 text-muted-foreground mt-0.5 shrink-0" />
<div>
<p className="text-[10px] uppercase tracking-widest text-muted-foreground mb-0.5">Carnet</p>
<p className="text-[10px] uppercase tracking-widest text-muted-foreground mb-0.5">{t('documentInfo.notebookLabel')}</p>
<p className="text-sm font-medium">{notebook.name}</p>
</div>
</div>
@@ -175,8 +182,8 @@ export function NoteDocumentInfoPanel({ note, content, onClose, onNoteRestored }
<div className="flex items-start gap-3 px-4 py-3">
<FileText className="h-3.5 w-3.5 text-muted-foreground mt-0.5 shrink-0" />
<div>
<p className="text-[10px] uppercase tracking-widest text-muted-foreground mb-0.5">Type</p>
<p className="text-sm font-medium">{noteTypeLabel[note.type] || note.type}</p>
<p className="text-[10px] uppercase tracking-widest text-muted-foreground mb-0.5">{t('documentInfo.typeLabel')}</p>
<p className="text-sm font-medium">{displayNoteType}</p>
</div>
</div>
@@ -184,8 +191,8 @@ export function NoteDocumentInfoPanel({ note, content, onClose, onNoteRestored }
<div className="flex items-start gap-3 px-4 py-3">
<Calendar className="h-3.5 w-3.5 text-muted-foreground mt-0.5 shrink-0" />
<div>
<p className="text-[10px] uppercase tracking-widest text-muted-foreground mb-0.5">Créée le</p>
<p className="text-sm font-medium">{format(createdAt, 'd MMM yyyy', { locale })}</p>
<p className="text-[10px] uppercase tracking-widest text-muted-foreground mb-0.5">{t('documentInfo.createdLabel')}</p>
<p className="text-sm font-medium">{formatAbsoluteDateLocalized(createdAt, language, 'd MMM yyyy', locale)}</p>
<p className="text-[11px] text-muted-foreground mt-0.5">
{formatDistanceToNow(createdAt, { addSuffix: true, locale })}
</p>
@@ -197,8 +204,8 @@ export function NoteDocumentInfoPanel({ note, content, onClose, onNoteRestored }
<div className="flex items-start gap-3 px-4 py-3">
<Clock className="h-3.5 w-3.5 text-muted-foreground mt-0.5 shrink-0" />
<div>
<p className="text-[10px] uppercase tracking-widest text-muted-foreground mb-0.5">Modifiée</p>
<p className="text-sm font-medium">{format(updatedAt, 'd MMM yyyy · HH:mm', { locale })}</p>
<p className="text-[10px] uppercase tracking-widest text-muted-foreground mb-0.5">{t('documentInfo.modifiedLabel')}</p>
<p className="text-sm font-medium">{formatAbsoluteDateLocalized(updatedAt, language, 'd MMM yyyy · HH:mm', locale)}</p>
<p className="text-[11px] text-muted-foreground mt-0.5">
{formatDistanceToNow(updatedAt, { addSuffix: true, locale })}
</p>
@@ -210,7 +217,7 @@ export function NoteDocumentInfoPanel({ note, content, onClose, onNoteRestored }
<div className="flex items-start gap-3 px-4 py-3">
<Tag className="h-3.5 w-3.5 text-muted-foreground mt-0.5 shrink-0" />
<div className="min-w-0">
<p className="text-[10px] uppercase tracking-widest text-muted-foreground mb-1.5">Labels</p>
<p className="text-[10px] uppercase tracking-widest text-muted-foreground mb-1.5">{t('documentInfo.labelsSection')}</p>
<div className="flex flex-wrap gap-1">
{(note.labels ?? []).map(label => (
<LabelBadge key={label} label={label} />
@@ -223,7 +230,7 @@ export function NoteDocumentInfoPanel({ note, content, onClose, onNoteRestored }
<div className="flex items-start gap-3 px-4 py-3">
<Hash className="h-3.5 w-3.5 text-muted-foreground mt-0.5 shrink-0" />
<div className="min-w-0">
<p className="text-[10px] uppercase tracking-widest text-muted-foreground mb-0.5">ID</p>
<p className="text-[10px] uppercase tracking-widest text-muted-foreground mb-0.5">{t('documentInfo.idLabel')}</p>
<p className="text-[11px] text-muted-foreground font-mono truncate">{note.id}</p>
</div>
</div>
@@ -237,7 +244,7 @@ export function NoteDocumentInfoPanel({ note, content, onClose, onNoteRestored }
{!historyEnabled ? (
<div className="text-center py-6 space-y-3">
<Clock className="h-8 w-8 text-muted-foreground/30 mx-auto" />
<p className="text-sm text-muted-foreground">L'historique n'est pas activé pour cette note.</p>
<p className="text-sm text-muted-foreground">{t('documentInfo.historyDisabled')}</p>
<button
className="text-xs px-4 py-2 rounded-lg bg-foreground text-background font-medium hover:opacity-80 transition-opacity"
onClick={async () => {
@@ -246,12 +253,12 @@ export function NoteDocumentInfoPanel({ note, content, onClose, onNoteRestored }
setShowHistory(true)
}}
>
Activer l'historique
{t('documentInfo.enableHistory')}
</button>
</div>
) : (
<div className="space-y-4">
<p className="text-[10px] uppercase tracking-widest text-muted-foreground font-bold">Versions sauvegardées</p>
<p className="text-[10px] uppercase tracking-widest text-muted-foreground font-bold">{t('documentInfo.savedVersions')}</p>
{/* Save version button */}
<button
@@ -278,11 +285,11 @@ export function NoteDocumentInfoPanel({ note, content, onClose, onNoteRestored }
}}
>
{isSavingVersion ? (
<><Loader2 className="h-3.5 w-3.5 animate-spin" />Sauvegarde…</>
<><Loader2 className="h-3.5 w-3.5 animate-spin" />{t('documentInfo.savingEllipsis')}</>
) : versionSaved ? (
<><Check className="h-3.5 w-3.5" /> Version sauvegardée !</>
<><Check className="h-3.5 w-3.5" /> {t('documentInfo.versionSaved')}</>
) : (
<>Sauvegarder cette version</>
<>{t('documentInfo.saveThisVersion')}</>
)}
</button>
@@ -292,15 +299,15 @@ export function NoteDocumentInfoPanel({ note, content, onClose, onNoteRestored }
{isLoadingHistory && historyEntries.length === 0 ? (
<div className="flex flex-col items-center justify-center py-10 opacity-40">
<Loader2 className="h-6 w-6 animate-spin mb-2" />
<p className="text-[10px] uppercase tracking-widest">Chargement...</p>
<p className="text-[10px] uppercase tracking-widest">{t('documentInfo.loading')}</p>
</div>
) : historyEntries.length === 0 ? (
<div className="text-center py-8 opacity-40 border border-dashed rounded-xl">
<Clock className="h-6 w-6 mx-auto mb-2" />
<p className="text-[10px] uppercase tracking-widest">Aucune version</p>
<p className="text-[10px] uppercase tracking-widest">{t('documentInfo.noVersion')}</p>
</div>
) : (
<div className="relative pl-6 space-y-6 before:absolute before:left-[11px] before:top-2 before:bottom-2 before:w-px before:bg-border/40">
<div className="relative ps-6 space-y-6 before:absolute before:start-[11px] before:top-2 before:bottom-2 before:w-px before:bg-border/40">
{historyEntries.map((entry, idx) => {
const colors = ['#E2E8F0', '#ACB995', '#E9ECEF']
const dotColor = colors[idx % colors.length]
@@ -310,7 +317,7 @@ export function NoteDocumentInfoPanel({ note, content, onClose, onNoteRestored }
<div key={entry.id} className="relative group">
{/* Dot */}
<div
className="absolute -left-[19px] top-1.5 h-3 w-3 rounded-full border-2 border-background z-10 shadow-sm"
className="absolute -start-[19px] top-1.5 h-3 w-3 rounded-full border-2 border-background z-10 shadow-sm"
style={{ backgroundColor: dotColor }}
/>
@@ -319,7 +326,7 @@ export function NoteDocumentInfoPanel({ note, content, onClose, onNoteRestored }
<div className="flex items-center gap-2">
<span className="text-xs font-bold font-mono">v{entry.version}</span>
{isLatest && (
<span className="text-[9px] px-1.5 py-0.5 rounded-md bg-primary/10 text-primary font-bold uppercase tracking-widest">Latest</span>
<span className="text-[9px] px-1.5 py-0.5 rounded-md bg-primary/10 text-primary font-bold uppercase tracking-widest">{t('documentInfo.latestBadge')}</span>
)}
</div>
@@ -328,7 +335,7 @@ export function NoteDocumentInfoPanel({ note, content, onClose, onNoteRestored }
onClick={() => handleRestoreVersion(entry.id)}
disabled={!!isRestoring || !!isDeleting}
className="p-1.5 rounded-lg hover:bg-primary/10 text-muted-foreground hover:text-primary transition-colors"
title="Restaurer"
title={t('documentInfo.restoreTooltip')}
>
{isRestoring === entry.id ? <Loader2 className="h-3 w-3 animate-spin" /> : <RotateCcw className="h-3 w-3" />}
</button>
@@ -336,7 +343,7 @@ export function NoteDocumentInfoPanel({ note, content, onClose, onNoteRestored }
onClick={() => handleDeleteVersion(entry.id)}
disabled={!!isRestoring || !!isDeleting}
className="p-1.5 rounded-lg hover:bg-red-500/10 text-muted-foreground hover:text-red-500 transition-colors"
title="Supprimer"
title={t('documentInfo.deleteTooltip')}
>
{isDeleting === entry.id ? <Loader2 className="h-3 w-3 animate-spin" /> : <Trash2 className="h-3 w-3" />}
</button>
@@ -344,7 +351,7 @@ export function NoteDocumentInfoPanel({ note, content, onClose, onNoteRestored }
</div>
<p className="text-[10px] text-muted-foreground font-medium">
{format(new Date(entry.createdAt), 'd MMM · HH:mm', { locale })}
{formatAbsoluteDateLocalized(new Date(entry.createdAt), language, 'd MMM · HH:mm', locale)}
<span className="mx-1.5 opacity-30">·</span>
{formatDistanceToNow(new Date(entry.createdAt), { addSuffix: true, locale })}
</p>
@@ -357,7 +364,7 @@ export function NoteDocumentInfoPanel({ note, content, onClose, onNoteRestored }
{/* Button to open the full modal (optional, but good to keep if user wants diff) */}
<button
className="w-full flex items-center justify-between p-3 rounded-xl border border-border/40 hover:bg-muted/50 transition-colors text-left group mt-4"
className="w-full flex items-center justify-between p-3 rounded-xl border border-border/40 hover:bg-muted/50 transition-colors text-start group mt-4"
onClick={() => setShowHistory(true)}
>
<div className="flex items-center gap-3">
@@ -365,11 +372,11 @@ export function NoteDocumentInfoPanel({ note, content, onClose, onNoteRestored }
<HistoryIcon className="h-4 w-4" />
</div>
<div>
<p className="text-xs font-bold uppercase tracking-wider">Mode Comparaison</p>
<p className="text-[10px] text-muted-foreground">Comparer les versions côte à côte</p>
<p className="text-xs font-bold uppercase tracking-wider">{t('documentInfo.comparisonMode')}</p>
<p className="text-[10px] text-muted-foreground">{t('documentInfo.comparisonSubtitle')}</p>
</div>
</div>
<ChevronRight className="h-4 w-4 text-muted-foreground" />
<ChevronRight className="h-4 w-4 text-muted-foreground rtl:scale-x-[-1]" />
</button>
</div>
)}

View File

@@ -664,10 +664,10 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
queryClient.invalidateQueries({ queryKey: queryKeys.notes(note.notebookId) })
triggerRefresh()
setIsDirty(false)
toast.success('Note sauvegardée !')
toast.success(t('notes.saved') || 'Saved')
} catch (error) {
console.error('[SAVE] updateNote failed:', error)
toast.error('Erreur lors de la sauvegarde.')
toast.error(t('notes.saveFailed') || 'Save failed')
} finally {
setIsSaving(false)
}

View File

@@ -10,19 +10,30 @@ import { FusionModal } from '@/components/fusion-modal'
import { ReminderDialog } from '@/components/reminder-dialog'
import { ContextualAIChat } from '@/components/contextual-ai-chat'
import { NoteDocumentInfoPanel } from '@/components/note-document-info-panel'
import { format } from 'date-fns'
import { fr } from 'date-fns/locale/fr'
import { enUS } from 'date-fns/locale/en-US'
import { formatAbsoluteDateLocalized } from '@/lib/utils/format-localized-date'
import { ChevronRight } from 'lucide-react'
import { toast } from 'sonner'
import { Note } from '@/lib/types'
import { GhostTags } from '@/components/ghost-tags'
import { LabelBadge } from '@/components/label-badge'
import { NoteAttachments } from '@/components/note-attachments'
import { DocumentQAOverlay } from '@/components/document-qa-overlay'
import { useLanguage } from '@/lib/i18n'
import { useState } from 'react'
interface NoteEditorFullPageProps {
onClose: () => void
}
export function NoteEditorFullPage({ onClose }: NoteEditorFullPageProps) {
const { t, language } = useLanguage()
const dateLocale = language === 'fr' ? fr : enUS
const { state, actions, note, readOnly, notebooks, fileInputRef, globalLabels } = useNoteEditorContext()
const [docQAAttachment, setDocQAAttachment] = useState<{ id: string; fileName: string } | null>(null)
const [attachmentsCount, setAttachmentsCount] = useState(0)
const [uploadTrigger, setUploadTrigger] = useState(0)
const notebookName = notebooks.find(nb => nb.id === note.notebookId)?.name || null
@@ -40,7 +51,7 @@ export function NoteEditorFullPage({ onClose }: NoteEditorFullPageProps) {
<div className="flex-1 flex flex-col overflow-y-auto bg-white dark:bg-background">
{/* TOOLBAR */}
<NoteEditorToolbar mode="fullPage" onClose={onClose} />
<NoteEditorToolbar mode="fullPage" onClose={onClose} onToggleAttachments={() => setUploadTrigger(v => v + 1)} attachmentsCount={attachmentsCount} />
{/* BODY — max-w-4xl, responsive px, py-16 */}
<div className="max-w-4xl mx-auto w-full px-6 sm:px-12 py-16 space-y-12 min-w-0">
@@ -52,7 +63,7 @@ export function NoteEditorFullPage({ onClose }: NoteEditorFullPageProps) {
{notebookName && <span style={{ color: 'var(--color-ink)' }}>{notebookName}</span>}
{notebookName && <ChevronRight size={10} style={{ color: 'var(--color-concrete)' }} />}
<span suppressHydrationWarning style={{ color: 'var(--color-concrete)' }}>
{format(new Date(note.contentUpdatedAt), 'MMM d, yyyy')}
{formatAbsoluteDateLocalized(new Date(note.contentUpdatedAt), language, 'MMM d, yyyy', dateLocale)}
</span>
</div>
@@ -94,8 +105,15 @@ export function NoteEditorFullPage({ onClose }: NoteEditorFullPageProps) {
)}
{/* Content area — max-w-3xl for wider reading column */}
<div className="max-w-3xl mx-auto w-full pb-32">
<div className="max-w-3xl mx-auto w-full space-y-8 pb-32">
<NoteContentArea />
<NoteAttachments
noteId={note.id}
onOpenDocQA={(att) => setDocQAAttachment(att)}
onCountChange={setAttachmentsCount}
triggerUpload={uploadTrigger}
/>
</div>
</div>
</div>
@@ -124,7 +142,7 @@ export function NoteEditorFullPage({ onClose }: NoteEditorFullPageProps) {
const plain = state.content.replace(/<[^>]+>/g, ' ').trim()
const wordCount = plain.split(/\s+/).filter(Boolean).length
if (wordCount < 10) {
toast.error('Ajoutez au moins 10 mots avant de générer un titre.')
toast.error(t('ai.titleGenerationMinWords', { count: wordCount }))
return
}
actions.setIsProcessingAI(true)
@@ -139,14 +157,14 @@ export function NoteEditorFullPage({ onClose }: NoteEditorFullPageProps) {
const s = data.suggestions?.[0]?.title ?? ''
if (s) {
actions.setTitle(s)
toast.success('Titre généré !')
toast.success(t('ai.titleApplied'))
} else {
toast.error('Impossible de générer un titre.')
toast.error(t('ai.titleGenerationFailed'))
}
} else {
toast.error('Erreur lors de la génération du titre.')
toast.error(t('ai.titleGenerationError'))
}
} catch { toast.error('Erreur réseau.') } finally { actions.setIsProcessingAI(false) }
} catch { toast.error(t('ai.networkErrorShort')) } finally { actions.setIsProcessingAI(false) }
}}
/>
</div>
@@ -166,6 +184,20 @@ export function NoteEditorFullPage({ onClose }: NoteEditorFullPageProps) {
</div>
<input ref={fileInputRef} type="file" accept="image/*" multiple className="hidden" onChange={actions.handleImageUpload} />
{docQAAttachment && (
<DocumentQAOverlay
attachment={docQAAttachment}
noteId={note.id}
noteContent={state.content}
onClose={() => setDocQAAttachment(null)}
onApplyToNote={(content) => {
actions.setPreviousContentForCopilot(state.content)
actions.setContent(state.content + '\n\n' + content)
}}
/>
)}
<ReminderDialog
open={state.showReminderDialog}
onOpenChange={actions.setShowReminderDialog}

View File

@@ -18,7 +18,7 @@ import { Badge } from '@/components/ui/badge'
import {
X, Plus, Palette, Image as ImageIcon, Bell, Eye, Link as LinkIcon, Sparkles,
Maximize2, Copy, ArrowLeft, ChevronRight, PanelRight, Check, Loader2, Save, MoreHorizontal,
Trash2, LogOut, Wand2, Share2
Trash2, LogOut, Wand2, Share2, Wind, Paperclip
} from 'lucide-react'
import { NoteShareDialog } from './note-share-dialog'
import { deleteNote, leaveSharedNote } from '@/app/actions/notes'
@@ -32,9 +32,11 @@ import { format } from 'date-fns'
interface NoteEditorToolbarProps {
mode: 'fullPage' | 'dialog'
onClose: () => void
onToggleAttachments?: () => void
attachmentsCount?: number
}
export function NoteEditorToolbar({ mode, onClose }: NoteEditorToolbarProps) {
export function NoteEditorToolbar({ mode, onClose, onToggleAttachments, attachmentsCount }: NoteEditorToolbarProps) {
const { state, actions, note, readOnly, fullPage, notebooks, fileInputRef } = useNoteEditorContext()
const { t } = useLanguage()
const { refreshNotes } = useRefresh()
@@ -95,22 +97,22 @@ export function NoteEditorToolbar({ mode, onClose }: NoteEditorToolbarProps) {
className="flex items-center gap-2 text-foreground hover:opacity-60 transition-opacity"
>
<ArrowLeft size={18} />
<span className="text-sm font-medium">Back to collection</span>
<span className="text-sm font-medium">{t('notes.backToCollection')}</span>
</button>
<div className="flex items-center gap-4">
<span className="hidden sm:flex items-center gap-1.5 text-[11px] text-foreground/40 select-none">
{state.isSaving
? <><Loader2 className="h-3 w-3 animate-spin" /><span>Saving</span></>
? <><Loader2 className="h-3 w-3 animate-spin" /><span>{t('notes.saving')}</span></>
: state.isDirty
? <><span className="h-1.5 w-1.5 rounded-full bg-amber-400 inline-block" /><span>Modified</span></>
: <><Check className="h-3 w-3 text-emerald-500" /><span>Saved</span></>}
? <><span className="h-1.5 w-1.5 rounded-full bg-amber-400 inline-block" /><span>{t('notes.dirtyStatus')}</span></>
: <><Check className="h-3 w-3 text-emerald-500" /><span>{t('notes.savedStatus')}</span></>}
</span>
{state.isMarkdown && !readOnly && (
<button
title={state.showMarkdownPreview ? 'Revenir à l\'édition' : 'Aperçu'}
aria-label={state.showMarkdownPreview ? 'Revenir à l\'édition' : 'Prévisualiser le rendu'}
title={state.showMarkdownPreview ? t('notes.markdownEditingTitle') : t('notes.markdownPreviewTitle')}
aria-label={state.showMarkdownPreview ? t('notes.markdownEditingTitle') : t('notes.markdownPreviewTitle')}
onClick={() => actions.setShowMarkdownPreview(!state.showMarkdownPreview)}
className={cn(
'p-1.5 rounded-full border transition-all duration-300',
@@ -140,8 +142,8 @@ export function NoteEditorToolbar({ mode, onClose }: NoteEditorToolbarProps) {
)}
<button
title="AI Assistant"
aria-label="Ouvrir l'assistant IA"
title={t('ai.openAssistant')}
aria-label={t('ai.openAssistant')}
onClick={() => { actions.setAiOpen(!state.aiOpen); actions.setInfoOpen(false) }}
className={cn(
'p-1.5 rounded-full border transition-all duration-300',
@@ -153,10 +155,41 @@ export function NoteEditorToolbar({ mode, onClose }: NoteEditorToolbarProps) {
<Sparkles size={16} />
</button>
<button
title={t('notes.brainstormThisIdea')}
aria-label={t('notes.brainstormThisIdeaAria')}
onClick={() => {
const title = note.title || ''
const summary = state.content?.replace(/<[^>]*>/g, '').slice(0, 200) || ''
const seed = title ? `${title}. ${summary}` : summary
if (!seed.trim()) return
window.open(`/brainstorm?seed=${encodeURIComponent(seed.slice(0, 300))}&sourceNoteId=${note.id}`, '_self')
}}
className="p-1.5 rounded-full border border-orange-300 dark:border-orange-700 text-orange-500 hover:bg-orange-50 dark:hover:bg-orange-900/20 transition-all"
>
<Wind size={16} />
</button>
{!readOnly && onToggleAttachments && (
<button
title={t('notes.attachments') || 'Attachments'}
aria-label={t('notes.attachments') || 'Attachments'}
onClick={onToggleAttachments}
className="relative p-1.5 rounded-full border border-black/20 dark:border-white/20 text-foreground hover:bg-black/5 dark:hover:bg-white/5 transition-all"
>
<Paperclip size={16} />
{(attachmentsCount ?? 0) > 0 && (
<span className="absolute -top-1 -right-1 w-3.5 h-3.5 bg-primary text-primary-foreground text-[8px] font-bold rounded-full flex items-center justify-center">
{attachmentsCount}
</span>
)}
</button>
)}
{!readOnly && (
<button
title={state.isDirty ? 'Enregistrer' : 'Aucune modification'}
aria-label={state.isDirty ? 'Enregistrer la note' : 'Aucune modification à enregistrer'}
title={state.isDirty ? t('notes.saveNow') : t('notes.noModification')}
aria-label={state.isDirty ? t('notes.saveNoteAria') : t('notes.noChangesToSaveAria')}
onClick={actions.handleSaveInPlace}
disabled={state.isSaving || !state.isDirty}
className={cn(
@@ -172,8 +205,8 @@ export function NoteEditorToolbar({ mode, onClose }: NoteEditorToolbarProps) {
{!readOnly && (
<button
title="Partager la note"
aria-label="Partager la note"
title={t('notes.shareNoteTitle')}
aria-label={t('notes.shareNoteAria')}
onClick={() => setShareOpen(true)}
className="p-1.5 rounded-full border border-black/20 dark:border-white/20 text-foreground hover:bg-black/5 dark:hover:bg-white/5 transition-all"
>
@@ -184,7 +217,7 @@ export function NoteEditorToolbar({ mode, onClose }: NoteEditorToolbarProps) {
{!readOnly && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button aria-label="Menu des options" className="p-1.5 rounded-full border border-black/20 dark:border-white/20 text-foreground hover:bg-black/5 dark:hover:bg-white/5 transition-all">
<button aria-label={t('notes.optionsMenuAria')} className="p-1.5 rounded-full border border-black/20 dark:border-white/20 text-foreground hover:bg-black/5 dark:hover:bg-white/5 transition-all">
<MoreHorizontal size={16} />
</button>
</DropdownMenuTrigger>
@@ -194,14 +227,14 @@ export function NoteEditorToolbar({ mode, onClose }: NoteEditorToolbarProps) {
try {
await deleteNote(note.id)
refreshNotes(note.notebookId)
toast.success('Note supprimée.')
toast.success(t('notes.noteDeletedToast'))
onClose()
} catch { toast.error('Impossible de supprimer.') }
} catch { toast.error(t('notes.deleteNoteFailedToast')) }
}}
className="text-red-600 dark:text-red-400 focus:text-red-600"
>
<Trash2 className="h-4 w-4 mr-2" />
Supprimer la note
<Trash2 className="h-4 w-4 me-2" />
{t('notes.deleteNoteConfirmItem')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@@ -216,7 +249,7 @@ export function NoteEditorToolbar({ mode, onClose }: NoteEditorToolbarProps) {
)}
<button
aria-label="Informations du document"
aria-label={t('notes.documentInfoAria')}
onClick={() => { actions.setInfoOpen(!state.infoOpen); actions.setAiOpen(false) }}
className={cn(
'p-1.5 rounded-full border transition-all duration-300',
@@ -259,11 +292,15 @@ export function NoteEditorToolbar({ mode, onClose }: NoteEditorToolbarProps) {
</Button>
)}
<Button variant="ghost" size="sm"
<Button
variant="ghost"
size="sm"
className={cn('h-8 gap-1.5 px-2 text-xs font-medium transition-all duration-200 rounded-md', state.aiOpen && 'bg-primary/10 text-primary')}
onClick={() => actions.setAiOpen(!state.aiOpen)} title="IA Note">
onClick={() => actions.setAiOpen(!state.aiOpen)}
title={t('ai.aiNoteTitle')}
>
<Sparkles className="h-3.5 w-3.5" />
<span className="hidden sm:inline">IA Note</span>
<span className="hidden sm:inline">{t('ai.aiNoteTitle')}</span>
</Button>
<DropdownMenu>
@@ -330,7 +367,7 @@ export function NoteEditorToolbar({ mode, onClose }: NoteEditorToolbarProps) {
onClick={async () => {
try {
await leaveSharedNote(note.id)
toast.success(t('notes.leftShare') || 'Share removed')
toast.success(t('notes.leftShare'))
refreshNotes(note.notebookId)
onClose()
} catch {

View File

@@ -6,6 +6,7 @@ import { createShareRequest, removeCollaborator, getNoteCollaborators } from '@/
import { toast } from 'sonner'
import { X, UserPlus, Users, Mail, Trash2, Loader2, Share2, Check } from 'lucide-react'
import { cn } from '@/lib/utils'
import { useLanguage } from '@/lib/i18n'
interface Collaborator {
id: string
@@ -21,6 +22,7 @@ interface NoteShareDialogProps {
}
export function NoteShareDialog({ noteId, noteTitle, onClose }: NoteShareDialogProps) {
const { t } = useLanguage()
const [email, setEmail] = useState('')
const [permission, setPermission] = useState<'view' | 'edit'>('view')
const [collaborators, setCollaborators] = useState<Collaborator[]>([])
@@ -60,13 +62,13 @@ export function NoteShareDialog({ noteId, noteTitle, onClose }: NoteShareDialogP
await createShareRequest(noteId, trimmed, permission)
setSent(true)
setEmail('')
toast.success(`Invitation envoyée à ${trimmed}`)
toast.success(t('collaboration.toastInviteSentTo', { email: trimmed }))
setTimeout(() => setSent(false), 2000)
loadCollaborators()
} catch (err: any) {
const msg = err?.message || 'Erreur lors du partage'
if (msg.includes('not found')) toast.error('Aucun compte trouvé avec cet email.')
else if (msg.includes('already shared')) toast.error('Cette note est déjà partagée avec cet utilisateur.')
const msg = err?.message || t('collaboration.toastSharingError')
if (msg.includes('not found')) toast.error(t('collaboration.toastEmailNotFound'))
else if (msg.includes('already shared')) toast.error(t('collaboration.toastAlreadySharedUser'))
else toast.error(msg)
} finally {
setSending(false)
@@ -78,9 +80,11 @@ export function NoteShareDialog({ noteId, noteTitle, onClose }: NoteShareDialogP
try {
await removeCollaborator(noteId, collaboratorId)
setCollaborators(prev => prev.filter(c => c.id !== collaboratorId))
toast.success(`Accès retiré à ${collaboratorEmail || "l'utilisateur"}`)
toast.success(t('collaboration.toastAccessRemoved', {
target: collaboratorEmail || t('collaboration.toastUserFallback'),
}))
} catch {
toast.error("Impossible de retirer l'accès.")
toast.error(t('collaboration.toastRemoveAccessFailed'))
} finally {
setRemovingId(null)
}
@@ -100,8 +104,8 @@ export function NoteShareDialog({ noteId, noteTitle, onClose }: NoteShareDialogP
{/* Header */}
<div className="px-6 pt-6 pb-4 border-b border-black/10 dark:border-white/10 flex items-start justify-between">
<div className="flex items-center gap-2">
<Share2 size={15} className="text-[#75B2D6]" />
<h2 className="text-sm font-bold text-foreground tracking-tight">Partager</h2>
<Share2 size={15} className="text-[#A47148]" />
<h2 className="text-sm font-bold text-foreground tracking-tight">{t('collaboration.shareCompactTitle')}</h2>
</div>
<button
onClick={onClose}
@@ -114,7 +118,7 @@ export function NoteShareDialog({ noteId, noteTitle, onClose }: NoteShareDialogP
{/* Invite form */}
<form onSubmit={handleInvite} className="px-6 py-5 space-y-3">
<label className="text-[9px] uppercase tracking-[0.25em] font-bold text-foreground/40">
Inviter par email
{t('collaboration.inviteByEmailLabel')}
</label>
<div className="flex gap-2">
<div className="relative flex-1">
@@ -126,7 +130,7 @@ export function NoteShareDialog({ noteId, noteTitle, onClose }: NoteShareDialogP
onChange={e => setEmail(e.target.value)}
required
autoFocus
className="w-full pl-9 pr-3 py-2.5 text-[13px] rounded-xl border border-black/15 dark:border-white/15 bg-transparent outline-none focus:ring-2 ring-[#75B2D6]/30 focus:border-[#75B2D6] transition-all placeholder:text-foreground/30"
className="w-full pl-9 pr-3 py-2.5 text-[13px] rounded-xl border border-black/15 dark:border-white/15 bg-transparent outline-none focus:ring-2 ring-[#A47148]/30 focus:border-[#A47148] transition-all placeholder:text-foreground/30"
/>
</div>
{/* Permission toggle */}
@@ -139,11 +143,11 @@ export function NoteShareDialog({ noteId, noteTitle, onClose }: NoteShareDialogP
className={cn(
'px-3 py-2 text-[10px] font-bold uppercase tracking-wide transition-colors',
permission === p
? 'bg-[#75B2D6] text-white'
? 'bg-[#A47148] text-white'
: 'text-foreground/50 hover:bg-black/5 dark:hover:bg-white/5'
)}
>
{p === 'view' ? 'Lire' : 'Éditer'}
{p === 'view' ? t('collaboration.accessReadCompact') : t('collaboration.accessEditCompact')}
</button>
))}
</div>
@@ -158,14 +162,14 @@ export function NoteShareDialog({ noteId, noteTitle, onClose }: NoteShareDialogP
? 'bg-black/5 dark:bg-white/5 text-foreground/30 cursor-not-allowed'
: sent
? 'bg-emerald-500 text-white'
: 'bg-[#75B2D6] text-white hover:opacity-90 shadow-sm shadow-[#75B2D6]/30'
: 'bg-[#A47148] text-white hover:opacity-90 shadow-sm shadow-[#A47148]/30'
)}
>
{sending
? <Loader2 size={13} className="animate-spin" />
: sent
? <><Check size={13} /> Invitation envoyée</>
: <><UserPlus size={13} /> Envoyer l&apos;invitation</>
? <><Check size={13} /> {t('collaboration.invitationSentBadge')}</>
: <><UserPlus size={13} /> {t('collaboration.sendInvitation')}</>
}
</button>
</form>
@@ -175,7 +179,7 @@ export function NoteShareDialog({ noteId, noteTitle, onClose }: NoteShareDialogP
<div className="flex items-center gap-2">
<div className="h-px flex-1 bg-black/10 dark:bg-white/10" />
<span className="text-[9px] uppercase tracking-[0.25em] font-bold text-foreground/30 flex items-center gap-1.5">
<Users size={10} /> Accès partagé
<Users size={10} /> {t('collaboration.sharedAccessLabel')}
</span>
<div className="h-px flex-1 bg-black/10 dark:bg-white/10" />
</div>
@@ -186,7 +190,7 @@ export function NoteShareDialog({ noteId, noteTitle, onClose }: NoteShareDialogP
</div>
) : collaborators.length === 0 ? (
<p className="text-center text-[11px] text-foreground/30 py-4">
Aucun collaborateur pour l&apos;instant.
{t('collaboration.noCollaboratorsEmpty')}
</p>
) : (
<ul className="space-y-2">
@@ -199,14 +203,14 @@ export function NoteShareDialog({ noteId, noteTitle, onClose }: NoteShareDialogP
}
</div>
<div className="flex-1 min-w-0">
<p className="text-[12px] font-semibold text-foreground truncate">{c.name || 'Utilisateur'}</p>
<p className="text-[12px] font-semibold text-foreground truncate">{c.name || t('collaboration.userFallback')}</p>
<p className="text-[10px] text-foreground/40 truncate">{c.email}</p>
</div>
<button
onClick={() => handleRemove(c.id, c.email)}
disabled={removingId === c.id}
className="p-1.5 rounded-lg text-foreground/30 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-950/30 transition-colors disabled:opacity-50"
title="Retirer l'accès"
title={t('collaboration.removeAccessTitle')}
>
{removingId === c.id ? <Loader2 size={13} className="animate-spin" /> : <Trash2 size={13} />}
</button>

View File

@@ -61,7 +61,7 @@ export function NoteTitleBlock() {
const plain = state.content.replace(/<[^>]+>/g, ' ').trim()
const wordCount = plain.split(/\s+/).filter(Boolean).length
if (wordCount < 10) {
toast.error('Ajoutez au moins 10 mots avant de générer un titre.')
toast.error(t('ai.titleGenerationMinWords', { count: wordCount }))
return
}
actions.setIsProcessingAI(true)
@@ -76,20 +76,20 @@ export function NoteTitleBlock() {
const s = data.suggestions?.[0]?.title ?? ''
if (s) {
actions.setTitle(s)
toast.success('Titre généré !')
toast.success(t('ai.titleApplied'))
} else {
toast.error('Impossible de générer un titre.')
toast.error(t('ai.titleGenerationFailed'))
}
} else {
toast.error('Erreur lors de la génération du titre.')
toast.error(t('ai.titleGenerationError'))
}
} catch (e) {
toast.error('Erreur réseau.')
toast.error(t('ai.networkErrorShort'))
} finally { actions.setIsProcessingAI(false) }
}}
disabled={state.isProcessingAI}
className="absolute right-0 top-2 opacity-0 group-hover:opacity-60 hover:!opacity-100 transition-opacity rounded-lg p-2 text-foreground/50 hover:bg-black/5"
title="Générer un titre automatique avec l'IA"
title={t('ai.generateTitlesTooltip')}
>
{state.isProcessingAI ? <Loader2 className="h-5 w-5 animate-spin" /> : <Sparkles className="h-5 w-5" />}
</button>

View File

@@ -1,9 +1,10 @@
'use client'
import { useCallback, useEffect, useMemo, useRef, useState, useTransition } from 'react'
import { format } from 'date-fns'
import { fr } from 'date-fns/locale/fr'
import { enUS } from 'date-fns/locale/en-US'
import { faIR } from 'date-fns/locale/fa-IR'
import { formatAbsoluteDateLocalized } from '@/lib/utils/format-localized-date'
import * as Diff from 'diff'
import {
History, Loader2, RotateCcw, Trash2, GitBranchPlus, Check, GitCompare, X,
@@ -36,12 +37,14 @@ interface NoteHistoryModalProps {
type ViewMode = 'preview' | 'diff'
function getDateLocale(language: string) {
return language === 'fr' ? fr : enUS
if (language === 'fr') return fr
if (language === 'fa') return faIR
return enUS
}
function fmtDate(date: Date | string, language: string): string {
const d = typeof date === 'string' ? new Date(date) : date
return format(d, 'd MMM yyyy HH:mm', { locale: getDateLocale(language) })
return formatAbsoluteDateLocalized(d, language, 'd MMM yyyy HH:mm', getDateLocale(language))
}
function VersionPreview({ entry, language }: { entry: NoteHistoryEntry; language: string }) {
@@ -306,7 +309,7 @@ export function NoteHistoryModal({
)}
>
{/* ── Header ── */}
<div className="border-b border-border/60 px-5 py-3 pr-10">
<div className="border-b border-border/60 px-5 py-3 pe-10">
<DialogTitle className="flex items-center gap-2 text-sm">
<History className="h-4 w-4 text-primary" />
{t('notes.history') || 'Historique'}
@@ -329,7 +332,7 @@ export function NoteHistoryModal({
{t('notes.historyDisabledDesc') || "Activez l'historique pour enregistrer les versions."}
</p>
<Button onClick={handleEnable} disabled={isEnabling} size="lg" className="rounded-full px-8">
{isEnabling && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{isEnabling && <Loader2 className="me-2 h-4 w-4 animate-spin" />}
{t('notes.enableHistory') || "Activer l'historique"}
</Button>
</div>
@@ -346,7 +349,7 @@ export function NoteHistoryModal({
) : (
<div className="grid grid-cols-[210px_1fr]">
{/* ── Left: Version list ── */}
<div className="max-h-[65vh] overflow-y-auto border-r border-border/60 p-2 space-y-1">
<div className="max-h-[65vh] overflow-y-auto border-e border-border/60 p-2 space-y-1">
{entries.map((entry) => {
const isCurrent = entry.version === currentVersion?.version
const isSelected = viewMode === 'preview' && selectedId === entry.id
@@ -373,7 +376,7 @@ export function NoteHistoryModal({
: isSelected ? 'border-primary/40 bg-primary/8' : 'border-transparent hover:bg-muted/60'
)}
>
<div className="flex items-center gap-1.5 pr-10">
<div className="flex items-center gap-1.5 pe-10">
<span className="text-xs font-semibold text-foreground">v{entry.version}</span>
{isCurrent && (
<span className="rounded bg-primary/15 px-1.5 py-px text-[10px] font-medium text-primary whitespace-nowrap">
@@ -386,7 +389,7 @@ export function NoteHistoryModal({
</p>
<div className={cn(
'absolute right-1 top-1/2 -translate-y-1/2 flex items-center gap-0.5',
'absolute end-1 top-1/2 -translate-y-1/2 flex items-center gap-0.5',
isSelected || isDiffSel ? 'opacity-100' : 'opacity-0 group-hover/entry:opacity-100'
)}>
{viewMode === 'preview' && (

View File

@@ -561,7 +561,7 @@ export function NoteInlineEditor({
<Button variant="ghost" size="sm"
className={cn('h-8 gap-1.5 px-2 text-xs font-medium transition-colors', aiOpen && 'bg-primary/10 text-primary')}
onClick={() => setAiOpen(!aiOpen)}
title="IA Note">
title={t('ai.aiNoteTitle')}>
{isProcessingAI
? <Loader2 className="h-3.5 w-3.5 animate-spin" />
: <Sparkles className="h-3.5 w-3.5" />}
@@ -601,7 +601,7 @@ export function NoteInlineEditor({
<GitCommitHorizontal className="h-3.5 w-3.5" />
</Button>
)}
<span className="mr-1 flex items-center gap-1 text-[11px] text-muted-foreground/50 select-none">
<span className="me-1 flex items-center gap-1 text-[11px] text-muted-foreground/50 select-none">
{isSaving ? (
<><Loader2 className="h-3 w-3 animate-spin" /> {t('notes.saving')}</>
) : isDirty ? (
@@ -624,7 +624,7 @@ export function NoteInlineEditor({
})
}}
>
<span className="h-1.5 w-1.5 rounded-full bg-amber-400 mr-1.5" />
<span className="h-1.5 w-1.5 rounded-full bg-amber-400 me-1.5" />
{t('notes.saveNow') || 'Enregistrer'}
</Button>
) : (
@@ -664,8 +664,8 @@ export function NoteInlineEditor({
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={handleToggleArchive}>
{note.isArchived
? <><ArchiveRestore className="h-4 w-4 mr-2" />{t('notes.unarchive')}</>
: <><Archive className="h-4 w-4 mr-2" />{t('notes.archive')}</>}
? <><ArchiveRestore className="h-4 w-4 me-2" />{t('notes.unarchive')}</>
: <><Archive className="h-4 w-4 me-2" />{t('notes.archive')}</>}
</DropdownMenuItem>
{onOpenHistory && (
<DropdownMenuItem
@@ -677,7 +677,7 @@ export function NoteInlineEditor({
}
}}
>
<History className="h-4 w-4 mr-2" />
<History className="h-4 w-4 me-2" />
{note.historyEnabled
? (t('notes.history') || 'Historique')
: (t('notes.enableHistory') || "Activer l'historique")}
@@ -685,7 +685,7 @@ export function NoteInlineEditor({
)}
<DropdownMenuSeparator />
<DropdownMenuItem className="text-red-600 dark:text-red-400" onClick={handleDelete}>
<Trash2 className="h-4 w-4 mr-2" />{t('notes.delete')}
<Trash2 className="h-4 w-4 me-2" />{t('notes.delete')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@@ -813,7 +813,7 @@ export function NoteInlineEditor({
</a>
</div>
<button type="button"
className="absolute right-2 top-2 rounded-full bg-background/80 p-1 opacity-0 transition-opacity group-hover:opacity-100 hover:bg-destructive/10"
className="absolute end-2 top-2 rounded-full bg-background/80 p-1 opacity-0 transition-opacity group-hover:opacity-100 hover:bg-destructive/10"
onClick={() => handleRemoveLink(idx)}
>
<X className="h-3 w-3" />

View File

@@ -25,6 +25,10 @@ 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 { fr } from 'date-fns/locale/fr'
import { enUS } from 'date-fns/locale/en-US'
import { formatAbsoluteDateLocalized } from '@/lib/utils/format-localized-date'
import { cn } from '@/lib/utils'
type NotesEditorialViewProps = {
notes: Note[]
@@ -33,13 +37,16 @@ type NotesEditorialViewProps = {
onOpenHistory?: (note: Note) => void
}
function formatNoteDate(date: Date | string): string {
function formatNoteDate(date: Date | string, language: string): string {
const d = typeof date === 'string' ? new Date(date) : date
return d.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
}).toUpperCase()
const locale = language === 'fr' ? fr : enUS
if (language === 'fa') {
return formatAbsoluteDateLocalized(d, language, 'd MMM yyyy', locale)
}
const month = d.toLocaleDateString('en-US', { month: 'short', timeZone: 'UTC' })
const day = d.getUTCDate()
const year = d.getUTCFullYear()
return `${month.toUpperCase()} ${day}, ${year}`
}
function EditorialNoteMenu({ note, onOpen, onOpenHistory }: {
@@ -113,27 +120,27 @@ function EditorialNoteMenu({ note, onOpen, onOpenHistory }: {
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-52">
<DropdownMenuItem onClick={e => { e.stopPropagation(); onOpen(note) }}>
<Pencil className="h-4 w-4 mr-2 text-foreground/50" />
<Pencil className="h-4 w-4 me-2 text-foreground/50" />
{t('notes.open') || 'Ouvrir'}
</DropdownMenuItem>
<DropdownMenuItem onClick={handlePin}>
<Pin className="h-4 w-4 mr-2 text-foreground/50" />
<Pin className="h-4 w-4 me-2 text-foreground/50" />
{note.isPinned ? (t('notes.unpin') || 'Désépingler') : (t('notes.pin') || 'Épingler')}
</DropdownMenuItem>
<DropdownMenuItem onClick={handleArchive}>
<Archive className="h-4 w-4 mr-2 text-foreground/50" />
<Archive className="h-4 w-4 me-2 text-foreground/50" />
{note.isArchived ? (t('notes.unarchive') || 'Désarchiver') : (t('notes.archive') || 'Archiver')}
</DropdownMenuItem>
{onOpenHistory && (
<DropdownMenuItem onClick={e => { e.stopPropagation(); onOpenHistory(note) }}>
<History className="h-4 w-4 mr-2 text-foreground/50" />
<History className="h-4 w-4 me-2 text-foreground/50" />
{t('notes.history') || 'Historique'}
</DropdownMenuItem>
)}
{/* Rappel */}
<DropdownMenuItem onClick={e => { e.stopPropagation(); setShowReminder(true) }}>
<Bell className="h-4 w-4 mr-2 text-foreground/50" />
<Bell className="h-4 w-4 me-2 text-foreground/50" />
{note.reminder
? (t('reminder.changeReminder') || 'Modifier le rappel')
: (t('reminder.setReminder') || 'Définir un rappel')}
@@ -142,17 +149,17 @@ function EditorialNoteMenu({ note, onOpen, onOpenHistory }: {
{/* Déplacer vers un carnet */}
<DropdownMenuSub>
<DropdownMenuSubTrigger onClick={e => e.stopPropagation()}>
<FolderOpen className="h-4 w-4 mr-2 text-foreground/50" />
<FolderOpen className="h-4 w-4 me-2 text-foreground/50" />
{t('notebookSuggestion.moveToNotebook') || 'Déplacer vers…'}
</DropdownMenuSubTrigger>
<DropdownMenuSubContent className="w-52">
<DropdownMenuItem onClick={e => { e.stopPropagation(); handleMoveToNotebook(null) }}>
<span className="w-4 h-4 rounded-full bg-foreground text-background flex items-center justify-center text-[9px] font-semibold mr-2 shrink-0">N</span>
<span className="w-4 h-4 rounded-full bg-foreground text-background flex items-center justify-center text-[9px] font-semibold me-2 shrink-0">N</span>
{t('notebookSuggestion.generalNotes') || 'Notes générales'}
</DropdownMenuItem>
{notebooks.map((nb: any) => (
<DropdownMenuItem key={nb.id} onClick={e => { e.stopPropagation(); handleMoveToNotebook(nb.id) }}>
<span className="w-4 h-4 rounded-full bg-foreground text-background flex items-center justify-center text-[9px] font-semibold mr-2 shrink-0">{nb.name.charAt(0).toUpperCase()}</span>
<span className="w-4 h-4 rounded-full bg-foreground text-background flex items-center justify-center text-[9px] font-semibold me-2 shrink-0">{nb.name.charAt(0).toUpperCase()}</span>
{nb.name}
</DropdownMenuItem>
))}
@@ -161,7 +168,7 @@ function EditorialNoteMenu({ note, onOpen, onOpenHistory }: {
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleDelete} className="text-destructive focus:text-destructive focus:bg-destructive/10">
<Trash2 className="h-4 w-4 mr-2" />
<Trash2 className="h-4 w-4 me-2" />
{t('notes.delete') || 'Supprimer'}
</DropdownMenuItem>
</DropdownMenuContent>
@@ -252,7 +259,7 @@ function EditorialThumbnail({
type="button"
aria-label={t('notes.generateIllustration') || 'Générer une illustration IA'}
title={t('notes.generateIllustration') || 'Générer une illustration IA'}
className="absolute bottom-2 right-2 flex h-9 w-9 items-center justify-center rounded-full border border-border bg-background/95 text-foreground shadow-card-rest backdrop-blur-sm transition-colors hover:bg-accent z-10 opacity-0 group-hover/thumb:opacity-100 md:opacity-100 focus-visible:opacity-100"
className="absolute bottom-2 end-2 flex h-9 w-9 items-center justify-center rounded-full border border-border bg-background/95 text-foreground shadow-card-rest backdrop-blur-sm transition-colors hover:bg-accent z-10 opacity-0 group-hover/thumb:opacity-100 md:opacity-100 focus-visible:opacity-100"
onClick={handleGenerateSvg}
disabled={busy}
>
@@ -333,7 +340,7 @@ export function NotesEditorialView({
notebookName,
onOpenHistory,
}: NotesEditorialViewProps) {
const { t } = useLanguage()
const { t, language } = useLanguage()
const { data: session } = useSession()
const { data: allLabels } = useLabelsQuery()
const [aiIllustrationEnabled, setAiIllustrationEnabled] = useState(false)
@@ -354,7 +361,8 @@ export function NotesEditorialView({
{notes.map((note: Note, index: number) => {
const title = getNoteDisplayTitle(note, t('notes.untitled') || 'Untitled')
const excerpt = getNotePlainExcerpt(note)
const dateStr = formatNoteDate(note.createdAt)
const dateStr = formatNoteDate(note.createdAt, language)
const editorialRtl = language === 'fa' || language === 'ar'
return (
<motion.article
@@ -365,14 +373,31 @@ export function NotesEditorialView({
className="space-y-4 group cursor-pointer relative pb-8"
onClick={() => onOpen(note)}
>
{/* Date / breadcrumb */}
<div className="note-date-badge">
{notebookName ? `${notebookName}${dateStr}` : dateStr}
{/* Date / breadcrumb — isolated bidi so Latin notebook name + Jalali date dont reorder wrongly */}
<div
className={cn('note-date-badge', editorialRtl && 'note-date-badge--locale-rtl')}
dir={editorialRtl ? 'rtl' : 'ltr'}
>
{notebookName ? (
<>
<bdi className={cn(editorialRtl && 'uppercase tracking-[0.2em]')}>{notebookName}</bdi>
<span className="mx-1.5 select-none text-muted-foreground/80" aria-hidden>
</span>
<bdi dir="rtl" lang={editorialRtl ? 'fa' : undefined}>
{dateStr}
</bdi>
</>
) : (
<bdi dir={editorialRtl ? 'rtl' : 'ltr'} lang={editorialRtl ? 'fa' : undefined}>
{dateStr}
</bdi>
)}
</div>
{/* Actions menu — absolutely positioned at top-right */}
<div
className="absolute top-0 right-0 opacity-0 group-hover:opacity-100 transition-opacity"
className="absolute top-0 end-0 opacity-0 group-hover:opacity-100 transition-opacity"
onClick={e => e.stopPropagation()}
>
<EditorialNoteMenu note={note} onOpen={onOpen} onOpenHistory={onOpenHistory} />

View File

@@ -4,13 +4,14 @@ import { useState, useEffect, useCallback } from 'react'
import { useRouter } from 'next/navigation'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Bell, Check, X, Clock, AlertCircle, CheckCircle2, Circle, Share2, Bot, Trash2, Download, Pencil, Presentation } from 'lucide-react'
import { Bell, Check, X, Clock, AlertCircle, CheckCircle2, Circle, Share2, Bot, Trash2, Download, Pencil, Presentation, Wind } from 'lucide-react'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover'
import { getPendingShareRequests, respondToShareRequest, getNotesWithReminders, toggleReminderDone } from '@/app/actions/notes'
import { getPendingBrainstormShares, respondToBrainstormShare } from '@/app/actions/brainstorm'
import { getUnreadNotifications, markNotificationRead, markAllNotificationsRead, type AppNotification } from '@/app/actions/notifications'
import { toast } from 'sonner'
import { useRefresh } from '@/lib/use-refresh'
@@ -60,23 +61,29 @@ export function NotificationPanel() {
const { t } = useLanguage()
const router = useRouter()
const [requests, setRequests] = useState<ShareRequest[]>([])
const [brainstormShares, setBrainstormShares] = useState<any[]>([])
const [reminders, setReminders] = useState<ReminderNote[]>([])
const [appNotifications, setAppNotifications] = useState<AppNotification[]>([])
const [isLoading, setIsLoading] = useState(false)
const [open, setOpen] = useState(false)
const loadData = useCallback(async () => {
setIsLoading(true)
try {
const [shareData, reminderData, notifData] = await Promise.all([
const [shareData, brainstormData, reminderData, notifData] = await Promise.all([
getPendingShareRequests(),
getPendingBrainstormShares(),
getNotesWithReminders(),
getUnreadNotifications(),
])
setRequests(shareData as any)
setBrainstormShares(brainstormData as any || [])
setReminders((reminderData as any) || [])
setAppNotifications(notifData || [])
} catch (error: any) {
console.error('Failed to load notifications:', error)
} finally {
setIsLoading(false)
}
}, [])
@@ -96,7 +103,7 @@ export function NotificationPanel() {
const overdueReminders = activeReminders.filter(r => new Date(r.reminder!) < now)
const upcomingReminders = activeReminders.filter(r => new Date(r.reminder!) >= now)
const pendingCount = requests.length + overdueReminders.length + appNotifications.length
const pendingCount = requests.length + brainstormShares.length + overdueReminders.length + appNotifications.length
const handleAccept = async (shareId: string) => {
try {
@@ -144,7 +151,29 @@ export function NotificationPanel() {
setAppNotifications([])
}
const hasContent = requests.length > 0 || activeReminders.length > 0 || appNotifications.length > 0
const handleAcceptBrainstorm = async (shareId: string) => {
try {
await respondToBrainstormShare(shareId, 'accept')
setBrainstormShares(prev => prev.filter(s => s.id !== shareId))
toast.success(t('notification.accepted') || 'Accepted')
setOpen(false)
} catch (error: any) {
toast.error(error.message || t('general.error'))
}
}
const handleDeclineBrainstorm = async (shareId: string) => {
try {
await respondToBrainstormShare(shareId, 'decline')
setBrainstormShares(prev => prev.filter(s => s.id !== shareId))
toast.info(t('notification.declined') || 'Declined')
if (brainstormShares.length <= 1) setOpen(false)
} catch (error: any) {
toast.error(error.message || t('general.error'))
}
}
const hasContent = requests.length > 0 || brainstormShares.length > 0 || activeReminders.length > 0 || appNotifications.length > 0
// ── icon bg/color per notification type ──────────────────────────────────
const notifIconStyle = (type: string) => {
@@ -152,13 +181,17 @@ export function NotificationPanel() {
if (type === 'agent_slides_ready') return { bg: `${C.gold}20`, color: C.gold }
if (type === 'agent_canvas_ready') return { bg: `${C.gold}20`, color: C.gold }
if (type === 'agent_failure') return { bg: '#EF444420', color: '#EF4444' }
if (type === 'brainstorm_invite') return { bg: '#10b98120', color: '#10b981' }
if (type === 'brainstorm_joined') return { bg: '#60a5fa20', color: '#60a5fa' }
return { bg: `${C.green}20`, color: C.green }
}
const notifLabelColor = (type: string) => {
if (type.startsWith('agent')) {
if (type === 'agent_failure') return '#EF4444'
return C.gold
if (type === 'agent_failure') return '#EF4444'
if (type === 'brainstorm_invite') return '#10b981'
if (type === 'brainstorm_joined') return '#60a5fa'
return C.gold
}
return C.green
}
@@ -249,6 +282,8 @@ export function NotificationPanel() {
>
{isSlides ? <Presentation className="w-3.5 h-3.5" />
: isCanvas ? <Pencil className="w-3.5 h-3.5" />
: notif.type === 'brainstorm_invite' ? <Wind className="w-3.5 h-3.5" />
: notif.type === 'brainstorm_joined' ? <Wind className="w-3.5 h-3.5" />
: notif.type.startsWith('agent') ? <Bot className="w-3.5 h-3.5" />
: <AlertCircle className="w-3.5 h-3.5" />}
</div>
@@ -262,7 +297,9 @@ export function NotificationPanel() {
{notif.type === 'agent_canvas_ready' && (t('notification.canvasReady') || 'Diagramme prêt')}
{notif.type === 'agent_success' && (t('notification.agentSuccess') || 'Agent terminé')}
{notif.type === 'agent_failure' && (t('notification.agentFailed') || 'Agent échoué')}
{notif.type === 'system' && 'Système'}
{notif.type === 'brainstorm_invite' && (t('notification.brainstormInvite') || 'Brainstorm')}
{notif.type === 'brainstorm_joined' && (t('notification.brainstormJoined') || 'Brainstorm')}
{notif.type === 'system' && t('notification.systemNotification')}
</span>
<p className="text-[13px] font-semibold truncate mt-0.5">{notif.title}</p>
{notif.message && (
@@ -302,7 +339,7 @@ export function NotificationPanel() {
a.download = parsed.filename || `${data.canvas.name || 'presentation'}.pptx`
document.body.appendChild(a); a.click()
document.body.removeChild(a); URL.revokeObjectURL(url)
} catch { toast.error('Échec du téléchargement') }
} catch { toast.error(t('notification.downloadFailed')) }
}}
className="flex items-center gap-1.5 px-3 py-1.5 text-[10px] font-bold rounded-lg text-white uppercase tracking-wide transition-all hover:opacity-90 active:scale-95 shadow-sm"
style={{ background: C.blue }}
@@ -362,6 +399,51 @@ export function NotificationPanel() {
</div>
))}
{/* ── Brainstorm share invites ── */}
{brainstormShares.map((share) => (
<div key={share.id} className="p-4 hover:bg-black/[0.02] transition-colors space-y-3">
<div className="flex items-start gap-3">
<div
className="h-8 w-8 rounded-full flex items-center justify-center text-white font-bold text-[11px] shrink-0 shadow-sm"
style={{ background: `linear-gradient(135deg, #fb923c, #f97316)` }}
>
{(share.sharer?.name || share.sharer?.email || '?')[0].toUpperCase()}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-1.5 mb-0.5">
<Wind className="w-3 h-3" style={{ color: '#fb923c' }} />
<span className="text-[9px] font-bold uppercase tracking-[0.2em]" style={{ color: '#fb923c' }}>
Brainstorm
</span>
</div>
<p className="text-[13px] font-semibold truncate">
{share.sharer?.name || share.sharer?.email}
</p>
<p className="text-[11px] text-foreground/50 truncate">
{t('notification.brainstormShared') || 'invited you to a brainstorm'} « {share.session?.seedIdea?.length > 35 ? share.session.seedIdea.substring(0, 35) + '…' : share.session?.seedIdea} »
</p>
</div>
</div>
<div className="flex gap-2 ml-11">
<button
onClick={() => handleDeclineBrainstorm(share.id)}
className="flex-1 h-7 px-3 text-[11px] font-semibold rounded-lg border border-black/15 text-foreground/60 hover:bg-black/5 transition-all active:scale-95 flex items-center justify-center gap-1"
>
<X className="h-3 w-3" />
{t('notification.decline') || 'Decline'}
</button>
<button
onClick={() => handleAcceptBrainstorm(share.id)}
className="flex-1 h-7 px-3 text-[11px] font-bold rounded-lg text-white transition-all active:scale-95 flex items-center justify-center gap-1 shadow-sm hover:opacity-90"
style={{ background: '#fb923c' }}
>
<Check className="h-3 w-3" />
{t('notification.accept') || 'Accept'}
</button>
</div>
</div>
))}
{/* ── Share requests ── */}
{requests.map((request) => (
<div key={request.id} className="p-4 hover:bg-black/[0.02] transition-colors space-y-3">

View File

@@ -11,6 +11,7 @@ import {
type OrganizationPlan,
} from '@/app/actions/organize-notebook'
import { toast } from 'sonner'
import { useLanguage } from '@/lib/i18n'
interface OrganizeNotebookDialogProps {
open: boolean
@@ -29,6 +30,8 @@ export function OrganizeNotebookDialog({
notebookName,
onDone,
}: OrganizeNotebookDialogProps) {
const { t, language } = useLanguage()
const organizePanelSlide = language === 'fa' || language === 'ar' ? -60 : 60
const [step, setStep] = useState<Step>('idle')
const [plan, setPlan] = useState<OrganizationPlan | null>(null)
const [editableGroups, setEditableGroups] = useState<OrganizationGroup[]>([])
@@ -44,7 +47,7 @@ export function OrganizeNotebookDialog({
const res = await analyzeNotebookForOrganization(notebookId)
if (!res.success || !res.plan) {
setError(res.error ?? 'Erreur inconnue')
setError(res.error ?? t('organizeNotebook.unknownError'))
setStep('idle')
return
}
@@ -94,14 +97,14 @@ export function OrganizeNotebookDialog({
const res = await executeNotebookOrganization(finalPlan)
if (!res.success) {
setError(res.error ?? 'Erreur inconnue')
setError(res.error ?? t('organizeNotebook.unknownError'))
setStep('preview')
return
}
setResult({ created: res.created, moved: res.moved })
setStep('done')
toast.success(`Carnet organisé — ${res.created} sous-carnet(s) créé(s), ${res.moved} note(s) déplacée(s)`)
toast.success(t('organizeNotebook.toastSuccess', { created: res.created, moved: res.moved }))
onDone?.()
}, [plan, editableGroups, onDone])
@@ -138,11 +141,11 @@ export function OrganizeNotebookDialog({
{/* Panel */}
<motion.div
initial={{ opacity: 0, x: 60 }}
initial={{ opacity: 0, x: organizePanelSlide }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 60 }}
exit={{ opacity: 0, x: organizePanelSlide }}
transition={{ type: 'spring', stiffness: 280, damping: 30 }}
className="fixed right-0 top-0 bottom-0 z-50 w-[460px] bg-card border-l border-border shadow-2xl flex flex-col"
className="fixed end-0 top-0 bottom-0 z-50 w-[460px] bg-card border-s border-border shadow-2xl flex flex-col"
onClick={e => e.stopPropagation()}
>
{/* Header */}
@@ -152,7 +155,7 @@ export function OrganizeNotebookDialog({
<Sparkles size={16} className="text-blueprint" />
</div>
<div>
<h2 className="text-[13px] font-semibold text-ink">Organiser le carnet</h2>
<h2 className="text-[13px] font-semibold text-ink">{t('organizeNotebook.title')}</h2>
<p className="text-[11px] text-muted-ink truncate max-w-[240px]">{notebookName}</p>
</div>
</div>
@@ -186,14 +189,10 @@ export function OrganizeNotebookDialog({
)}
<div className="space-y-3">
<p className="text-[13px] text-ink leading-relaxed">
L'IA va analyser les notes de ce carnet et vous proposer un plan de réorganisation en sous-carnets thématiques.
{t('organizeNotebook.intro')}
</p>
<ul className="space-y-2">
{[
'Regroupement par sujet ou thème',
'Création de sous-carnets manquants',
'Aperçu complet avant modification',
].map(item => (
{[t('organizeNotebook.bulletThemes'), t('organizeNotebook.bulletSubfolders'), t('organizeNotebook.bulletPreview')].map(item => (
<li key={item} className="flex items-center gap-2 text-[12px] text-muted-ink">
<div className="w-1.5 h-1.5 rounded-full bg-blueprint shrink-0" />
{item}
@@ -224,8 +223,8 @@ export function OrganizeNotebookDialog({
/>
</div>
<div className="text-center space-y-1.5">
<p className="text-[14px] font-medium text-ink">Analyse en cours…</p>
<p className="text-[12px] text-muted-ink">L'IA lit vos notes et identifie les thèmes</p>
<p className="text-[14px] font-medium text-ink">{t('organizeNotebook.analyzingTitle')}</p>
<p className="text-[12px] text-muted-ink">{t('organizeNotebook.analyzingSubtitle')}</p>
</div>
<div className="flex gap-1.5">
{[0, 1, 2].map(i => (
@@ -253,7 +252,11 @@ export function OrganizeNotebookDialog({
<div className="flex items-center gap-3 p-3 rounded-xl bg-blueprint/5 border border-blueprint/20">
<Sparkles size={12} className="text-blueprint shrink-0" />
<p className="text-[11px] text-blueprint font-medium">
{editableGroups.length} groupe(s) · {totalNotes} note(s) · {newSubNbs} nouveau(x) sous-carnet(s)
{t('organizeNotebook.previewSummary', {
groups: editableGroups.length,
notes: totalNotes,
newSubs: newSubNbs,
})}
</p>
</div>
@@ -290,7 +293,7 @@ export function OrganizeNotebookDialog({
/>
{group.isNew && (
<span className="px-1.5 py-0.5 rounded text-[9px] font-bold uppercase tracking-wider bg-blueprint/10 text-blueprint shrink-0">
Nouveau
{t('organizeNotebook.badgeNew')}
</span>
)}
<div className="flex items-center gap-1 shrink-0">
@@ -329,7 +332,7 @@ export function OrganizeNotebookDialog({
<Check size={10} className="text-blueprint" />
</div>
<span className="text-[11px] text-muted-ink truncate group-hover:text-ink transition-colors">
{note.title || 'Note sans titre'}
{note.title || t('organizeNotebook.untitledNote')}
</span>
</div>
))}
@@ -341,7 +344,7 @@ export function OrganizeNotebookDialog({
{/* Collapsed count */}
{!expandedGroups.has(idx) && (
<div className="px-4 pb-2.5 text-[11px] text-muted-ink/60">
{group.notes.length} note(s)
{t('organizeNotebook.notesInGroup', { count: group.notes.length })}
</div>
)}
</motion.div>
@@ -360,8 +363,8 @@ export function OrganizeNotebookDialog({
>
<Loader2 size={32} className="text-blueprint animate-spin" />
<div className="text-center space-y-1">
<p className="text-[14px] font-medium text-ink">Organisation en cours</p>
<p className="text-[12px] text-muted-ink">Création des sous-carnets et déplacement des notes</p>
<p className="text-[14px] font-medium text-ink">{t('organizeNotebook.executingTitle')}</p>
<p className="text-[12px] text-muted-ink">{t('organizeNotebook.executingSubtitle')}</p>
</div>
</motion.div>
)}
@@ -378,10 +381,10 @@ export function OrganizeNotebookDialog({
<CheckCircle2 size={32} className="text-emerald-500" />
</div>
<div className="text-center space-y-1.5">
<p className="text-[15px] font-semibold text-ink">Carnet organisé !</p>
<p className="text-[15px] font-semibold text-ink">{t('organizeNotebook.doneTitle')}</p>
{result && (
<p className="text-[12px] text-muted-ink">
{result.created} sous-carnet(s) créé(s) · {result.moved} note(s) déplacée(s)
{t('organizeNotebook.doneStats', { created: result.created, moved: result.moved })}
</p>
)}
</div>
@@ -399,7 +402,7 @@ export function OrganizeNotebookDialog({
className="w-full flex items-center justify-center gap-2 px-4 py-2.5 rounded-xl bg-ink text-paper text-[13px] font-semibold hover:opacity-85 transition-opacity"
>
<Sparkles size={14} />
Analyser avec l'IA
{t('organizeNotebook.analyzeButton')}
</button>
)}
@@ -409,7 +412,7 @@ export function OrganizeNotebookDialog({
onClick={() => { setStep('idle'); setError(null) }}
className="flex-1 px-4 py-2.5 rounded-xl border border-border text-[13px] font-medium text-muted-ink hover:text-ink transition-colors"
>
Recommencer
{t('organizeNotebook.restart')}
</button>
<button
onClick={handleExecute}
@@ -422,7 +425,7 @@ export function OrganizeNotebookDialog({
)}
>
<Check size={14} />
Valider
{t('organizeNotebook.confirm')}
</button>
</div>
)}
@@ -432,7 +435,7 @@ export function OrganizeNotebookDialog({
onClick={handleClose}
className="w-full px-4 py-2.5 rounded-xl border border-border text-[13px] font-medium text-muted-ink hover:text-ink transition-colors"
>
Fermer
{t('organizeNotebook.closeButton')}
</button>
)}
</div>

View File

@@ -27,6 +27,7 @@ import {
Sun,
Pin,
PinOff,
Sparkles,
} from 'lucide-react'
import { useLanguage } from '@/lib/i18n'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
@@ -48,8 +49,9 @@ import {
} from '@/components/ui/dropdown-menu'
import { signOut } from 'next-auth/react'
import { useNoteRefresh } from '@/context/NoteRefreshContext'
import { useBrainstormSessions, useDeleteBrainstorm } from '@/hooks/use-brainstorm'
type NavigationView = 'notebooks' | 'agents' | 'reminders'
type NavigationView = 'notebooks' | 'agents' | 'reminders' | 'brainstorms'
type SortOrder = 'newest' | 'oldest' | 'alpha'
function NoteLink({
@@ -61,13 +63,15 @@ function NoteLink({
isActive: boolean
onClick: () => void
}) {
const { language } = useLanguage()
const slideX = language === 'fa' || language === 'ar' ? 10 : -10
return (
<motion.button
initial={{ opacity: 0, x: -10 }}
initial={{ opacity: 0, x: slideX }}
animate={{ opacity: 1, x: 0 }}
onClick={onClick}
className={cn(
'w-full flex items-center gap-2 pl-12 pr-4 py-2 text-[12px] transition-colors rounded-lg text-left',
'w-full flex items-center gap-2 ps-12 pe-4 py-2 text-[12px] transition-colors rounded-lg text-start',
isActive ? 'bg-white/50 text-foreground font-medium' : 'text-muted-foreground hover:text-foreground hover:bg-white/30'
)}
>
@@ -80,6 +84,81 @@ function NoteLink({
)
}
function SidebarBrainstorms() {
const { data: sessions, isLoading } = useBrainstormSessions()
const deleteBrainstorm = useDeleteBrainstorm()
const router = useRouter()
const { t } = useLanguage()
if (isLoading) {
return (
<div className="px-4 space-y-2">
{[1, 2, 3].map(i => (
<div key={i} className="h-12 rounded-xl bg-paper/50 animate-pulse" />
))}
</div>
)
}
if (!sessions || sessions.length === 0) {
return (
<div className="px-4 py-6 text-center">
<Sparkles size={20} className="mx-auto text-orange-400/40 mb-2" />
<p className="text-[11px] text-muted-foreground">{t('brainstorm.noSessions')}</p>
<button
onClick={() => router.push('/brainstorm')}
className="mt-2 text-[11px] text-orange-500 hover:text-orange-400 font-medium"
>
{t('brainstorm.startOne')}
</button>
</div>
)
}
return (
<div className="space-y-0.5">
{sessions.slice(0, 10).map(s => (
<div key={s.id} className="relative group/item">
<button
onClick={() => router.replace(`/brainstorm?session=${s.id}`)}
className={`w-full flex items-center gap-3 px-4 py-2.5 rounded-xl transition-all duration-200 text-start hover:bg-memento-blue/5 group ${
(s as any)._owned === false ? 'border-s-2 border-memento-blue/30 dark:border-memento-blue/70' : ''
}`}
>
<div className={`w-7 h-7 rounded-full flex items-center justify-center shrink-0 ${
(s as any)._owned === false
? 'border border-memento-blue/20 dark:border-memento-blue/80 bg-memento-blue/5 dark:bg-memento-blue/20'
: 'border border-orange-200 dark:border-orange-800/40 bg-orange-50 dark:bg-orange-900/20'
}`}>
<Sparkles size={12} className={(s as any)._owned === false ? 'text-memento-blue' : 'text-orange-500'} />
</div>
<div className="min-w-0 flex-1">
<p className="text-[12px] font-medium truncate">
{s.seedIdea}
{(s as any)._owned === false && <span className="text-[9px] ms-1.5 text-memento-blue/70 font-normal">· partagé</span>}
</p>
<p className="text-[10px] text-muted-foreground">
{s.activeIdeas} {t('brainstorm.ideas')} · {new Date(s.createdAt).toLocaleDateString()}
</p>
</div>
</button>
{(s as any)._owned !== false && (
<button
onClick={(e) => {
e.stopPropagation()
deleteBrainstorm.mutate(s.id)
}}
className="absolute end-2 top-1/2 -translate-y-1/2 p-1.5 rounded-lg opacity-0 group-hover/item:opacity-100 hover:bg-rose-500/10 text-muted-foreground hover:text-rose-500 transition-all"
>
<Trash2 size={13} />
</button>
)}
</div>
))}
</div>
)
}
function SidebarCarnetItem({
carnet,
isActive,
@@ -117,7 +196,8 @@ function SidebarCarnetItem({
isExpanded: boolean
toggleExpand: () => void
}) {
const { t } = useLanguage()
const { t, language } = useLanguage()
const isRtl = language === 'fa' || language === 'ar'
const hasChildren = React.Children.count(children) > 0
const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null)
@@ -133,7 +213,7 @@ function SidebarCarnetItem({
<div className={cn('transition-opacity', isDragging && 'opacity-40')}>
<div
className="flex items-center group relative h-10"
style={{ paddingLeft: `${level * 16}px` }}
style={{ paddingInlineStart: `${level * 16}px` }}
onContextMenu={(e) => {
e.preventDefault()
e.stopPropagation()
@@ -141,15 +221,15 @@ function SidebarCarnetItem({
}}
>
{level > 0 && (
<div className="absolute left-[8px] top-[-10px] bottom-1/2 w-px bg-border/40" />
<div className="absolute start-[8px] top-[-10px] bottom-1/2 w-px bg-border/40" />
)}
{level > 0 && (
<div className="absolute left-[8px] top-1/2 w-[8px] h-px bg-border/40" />
<div className="absolute start-[8px] top-1/2 w-[8px] h-px bg-border/40" />
)}
<div
{...dragHandleProps}
className="absolute left-1 top-1/2 -translate-y-1/2 p-1 rounded text-muted-foreground/30 hover:text-muted-foreground cursor-grab active:cursor-grabbing opacity-0 group-hover:opacity-100 transition-opacity z-10"
className="absolute start-1 top-1/2 -translate-y-1/2 p-1 rounded text-muted-foreground/30 hover:text-muted-foreground cursor-grab active:cursor-grabbing opacity-0 group-hover:opacity-100 transition-opacity z-10"
>
<GripVertical size={12} />
</div>
@@ -161,7 +241,7 @@ function SidebarCarnetItem({
className="p-1 hover:bg-foreground/5 rounded-md transition-colors text-muted-foreground"
>
<motion.div animate={{ rotate: isExpanded ? 90 : 0 }} transition={{ duration: 0.2 }}>
<ChevronRight size={14} />
<ChevronRight size={14} className="rtl:scale-x-[-1]" />
</motion.div>
</button>
) : (
@@ -169,7 +249,7 @@ function SidebarCarnetItem({
)}
<motion.div
whileHover={{ x: 2 }}
whileHover={{ x: isRtl ? -2 : 2 }}
onClick={onCarnetClick}
onDoubleClick={(e) => { e.stopPropagation(); onRename() }}
className={cn(
@@ -180,7 +260,7 @@ function SidebarCarnetItem({
{isActive && (
<motion.div
layoutId="active-indicator"
className="absolute -left-1 w-1 h-4 bg-blueprint rounded-full"
className="absolute -start-1 w-1 h-4 bg-blueprint rounded-full"
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
/>
)}
@@ -193,7 +273,7 @@ function SidebarCarnetItem({
{carnet.initial}
</div>
<div className="flex-1 text-left flex items-center gap-2 min-w-0">
<div className="flex-1 text-start flex items-center gap-2 min-w-0">
<span className={cn(
'text-[12px] font-medium transition-colors truncate',
isActive ? 'text-ink' : 'text-muted-ink group-hover/item:text-ink'
@@ -205,7 +285,7 @@ function SidebarCarnetItem({
<div className="flex items-center gap-1 opacity-0 group-hover/item:opacity-100 transition-opacity shrink-0">
{isPinned && (
<span className="text-blueprint" title="Carnet figé">
<span className="text-blueprint" title={t('notebook.pinnedFrozenTooltip')}>
<Pin size={9} className="opacity-70" />
</span>
)}
@@ -258,8 +338,8 @@ function SidebarCarnetItem({
className="w-full flex items-center gap-2.5 px-3 py-2 text-[12px] text-ink hover:bg-foreground/5 transition-colors"
>
{isPinned
? <><PinOff size={13} className="text-blueprint" /><span>Défiger l'état du carnet</span></>
: <><Pin size={13} className="text-blueprint" /><span>Figer l'état du carnet</span></>
? <><PinOff size={13} className="text-blueprint" /><span>{t('sidebar.unfreezePinnedNotebook')}</span></>
: <><Pin size={13} className="text-blueprint" /><span>{t('sidebar.freezePinnedNotebook')}</span></>
}
</button>
<div className="mx-3 my-1 border-t border-border/50" />
@@ -268,14 +348,14 @@ function SidebarCarnetItem({
className="w-full flex items-center gap-2.5 px-3 py-2 text-[12px] text-ink hover:bg-foreground/5 transition-colors"
>
<Plus size={13} className="text-concrete" />
<span>Nouveau sous-carnet</span>
<span>{t('sidebar.newSubNotebook')}</span>
</button>
<button
onClick={() => { onRename(); setContextMenu(null) }}
className="w-full flex items-center gap-2.5 px-3 py-2 text-[12px] text-ink hover:bg-foreground/5 transition-colors"
>
<Pencil size={13} className="text-concrete" />
<span>Renommer</span>
<span>{t('sidebar.renameNotebook')}</span>
</button>
<div className="mx-3 my-1 border-t border-border/50" />
<button
@@ -283,7 +363,7 @@ function SidebarCarnetItem({
className="w-full flex items-center gap-2.5 px-3 py-2 text-[12px] text-rose-500 hover:bg-rose-50 dark:hover:bg-rose-950/30 transition-colors"
>
<Trash2 size={13} />
<span>Supprimer</span>
<span>{t('common.delete')}</span>
</button>
</motion.div>
)}
@@ -298,8 +378,8 @@ function SidebarCarnetItem({
transition={{ duration: 0.3, ease: [0.23, 1, 0.32, 1] }}
className="overflow-hidden"
>
<div className="relative" style={{ marginLeft: `${(level + 1) * 16 + 10}px` }}>
<div className="absolute left-[-6px] top-0 bottom-4 w-px bg-border/30" />
<div className="relative" style={{ marginInlineStart: `${(level + 1) * 16 + 10}px` }}>
<div className="absolute start-[-6px] top-0 bottom-4 w-px bg-border/30" />
<div className="space-y-0.5 py-1">
{children}
@@ -312,7 +392,7 @@ function SidebarCarnetItem({
/>
))}
{isActive && notes.length === 0 && !hasChildren && (
<p className="pl-8 py-2 text-[10px] italic text-muted-foreground/40 font-light">
<p className="ps-8 py-2 text-[10px] italic text-muted-foreground/40 font-light">
{t('common.noResults') || 'No notes found'}
</p>
)}
@@ -329,7 +409,8 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
const pathname = usePathname()
const searchParams = useSearchParams()
const router = useRouter()
const { t } = useLanguage()
const { t, language } = useLanguage()
const isRtl = language === 'fa' || language === 'ar'
const { notebooks, trashNotebook, updateNotebookOrderOptimistic } = useNotebooks()
const { refreshKey } = useNoteRefresh()
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false)
@@ -733,7 +814,7 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
className="text-destructive focus:text-destructive"
onClick={() => signOut({ callbackUrl: '/login' })}
>
<LogOut className="h-4 w-4 mr-2" />
<LogOut className="h-4 w-4 me-2" />
{t('sidebar.signOut') || 'Se déconnecter'}
</DropdownMenuItem>
</DropdownMenuContent>
@@ -770,6 +851,13 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
>
<Bot size={14} />
</button>
<button
onClick={() => { setActiveView('brainstorms'); router.push('/brainstorm') }}
className={cn('p-1.5 rounded-full transition-all', activeView === 'brainstorms' ? 'bg-orange-500 text-white shadow-sm' : 'text-muted-ink hover:text-ink')}
title={t('brainstorm.sessions')}
>
<Sparkles size={14} />
</button>
</div>
</div>
</div>
@@ -781,9 +869,9 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
{activeView === 'notebooks' ? (
<motion.div
key="notebooks"
initial={{ opacity: 0, x: -10 }}
initial={{ opacity: 0, x: isRtl ? 10 : -10 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 10 }}
exit={{ opacity: 0, x: isRtl ? -10 : 10 }}
transition={{ duration: 0.2 }}
>
{/* Section header with sort button */}
@@ -795,7 +883,7 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
<button
onClick={() => { setCreateParentId(null); setIsCreateDialogOpen(true) }}
className="p-1 text-muted-foreground hover:text-foreground hover:bg-white/40 transition-all rounded"
title={t('notebook.create') || 'Nouveau carnet'}
title={t('notebook.create')}
>
<Plus size={12} />
</button>
@@ -813,14 +901,14 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
initial={{ opacity: 0, scale: 0.9, y: -4 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9, y: -4 }}
className="absolute right-0 top-full mt-1 bg-card border border-border rounded-xl shadow-lg z-50 py-1 min-w-[140px]"
className="absolute end-0 top-full mt-1 bg-card border border-border rounded-xl shadow-lg z-50 py-1 min-w-[140px]"
>
{(['newest', 'oldest', 'alpha'] as SortOrder[]).map(order => (
<button
key={order}
onClick={() => { setSortOrder(order); setShowSortMenu(false) }}
className={cn(
'w-full text-left px-4 py-2 text-[12px] transition-colors',
'w-full text-start px-4 py-2 text-[12px] transition-colors',
sortOrder === order
? 'font-bold text-foreground'
: 'text-muted-foreground hover:text-foreground hover:bg-muted/40'
@@ -872,9 +960,9 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
) : activeView === 'reminders' ? (
<motion.div
key="reminders"
initial={{ opacity: 0, x: -10 }}
initial={{ opacity: 0, x: isRtl ? 10 : -10 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 10 }}
exit={{ opacity: 0, x: isRtl ? -10 : 10 }}
transition={{ duration: 0.2 }}
>
<p className="text-[10px] font-bold text-muted-ink tracking-[0.2em] uppercase mb-4 px-4">
@@ -885,12 +973,12 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
<p className="text-[11px] text-concrete italic">{t('sidebar.noReminders') || 'No active reminders.'}</p>
</div>
</motion.div>
) : (
) : activeView === 'agents' ? (
<motion.div
key="agents"
initial={{ opacity: 0, x: 10 }}
initial={{ opacity: 0, x: isRtl ? -10 : 10 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -10 }}
exit={{ opacity: 0, x: isRtl ? 10 : -10 }}
transition={{ duration: 0.2 }}
>
<p className="text-[10px] font-bold text-muted-foreground tracking-widest uppercase mb-4 px-4">
@@ -900,6 +988,7 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
{[
{ id: 'agents', href: '/agents', label: t('agents.myAgents'), icon: Bot },
{ id: 'lab', href: '/lab', label: t('nav.lab'), icon: FlaskConical },
{ id: 'brainstorm', href: '/brainstorm', label: t('brainstorm.sessions'), icon: Sparkles },
].map(item => {
const isActive = pathname.startsWith(item.href)
return (
@@ -925,6 +1014,28 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
})}
</div>
</motion.div>
) : (
<motion.div
key="brainstorms"
initial={{ opacity: 0, x: isRtl ? -10 : 10 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: isRtl ? 10 : -10 }}
transition={{ duration: 0.2 }}
>
<div className="flex items-center justify-between px-4 mb-3">
<p className="text-[10px] font-bold text-muted-foreground tracking-widest uppercase">
{t('brainstorm.sessions')}
</p>
<button
onClick={() => router.push('/brainstorm')}
className="p-1 text-muted-foreground hover:text-orange-500 transition-colors rounded"
title={t('brainstorm.newBrainstorm')}
>
<Plus size={12} />
</button>
</div>
<SidebarBrainstorms />
</motion.div>
)}
</AnimatePresence>
</div>
@@ -965,7 +1076,7 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
<Trash2 size={14} className={pathname === '/trash' ? 'text-rose-500' : 'text-muted-ink group-hover:text-rose-500'} />
<span>{t('sidebar.trash')}</span>
{trashCount > 0 && (
<span className="ml-auto w-1.5 h-1.5 rounded-full bg-rose-400" />
<span className="ms-auto w-1.5 h-1.5 rounded-full bg-rose-400" />
)}
</Link>

View File

@@ -69,7 +69,7 @@ function DialogContent({
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-close"
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 end-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<XIcon />
<span className="sr-only">Close</span>
@@ -84,7 +84,7 @@ function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
className={cn("flex flex-col gap-2 text-center sm:text-start", className)}
{...props}
/>
)

View File

@@ -0,0 +1,534 @@
# Brainstorm — Documentation Complète
## Vue d'ensemble
Le brainstorm est un outil de génération d'idées assistée par IA basé sur un graphe radial D3. L'utilisateur saisit une "graine" (seed idea), l'IA génère 9 idées en 3 vagues (Variations, Analogies, Disruptions), et l'utilisateur peut approfondir, dismiss, convertir en note, ou ajouter ses propres idées. Le tout en temps-réel via Socket.io.
---
## Architecture
```
[Browser] ←→ [Next.js API Routes] ←→ [PostgreSQL]
↕ ↕
[Socket.io Client] ←→ [Socket.io Server :3002]
```
- **Frontend** : React + D3.js + React Query + Socket.io Client
- **Backend** : Next.js App Router + Prisma + OpenAI
- **Temps réel** : Socket.io sur le port 3002
---
## Modèles de données (Prisma)
### BrainstormSession
| Champ | Type | Description |
|-------|------|-------------|
| id | String (cuid) | PK |
| seedIdea | String | L'idée graine saisie par l'utilisateur |
| sourceNoteId | String? | FK vers la note source (optionnel) |
| contextNoteIds | String? | JSON string des IDs des notes de contexte |
| exportedNoteId | String? | FK vers la note exportée |
| userId | String | FK vers le propriétaire |
| inviteToken | String? | Token d'invitation unique |
| inviteExpiry | DateTime? | Date d'expiration du token |
| status | String | "active" par défaut |
| Relations | | ideas[], participants[], activities[], shares[] |
### BrainstormIdea
| Champ | Type | Description |
|-------|------|-------------|
| id | String (cuid) | PK |
| sessionId | String | FK vers la session |
| waveNumber | Int | 1, 2 ou 3 |
| title | String | Titre de l'idée |
| description | String | Description détaillée |
| connectionToSeed | String? | Lien avec l'idée graine |
| noveltyScore | Int? | Score de nouveauté (0-100) |
| parentIdeaId | String? | FK vers l'idée parent (auto-référence) |
| convertedToNoteId | String? | FK vers la note créée par conversion |
| relatedNoteIds | String? | JSON string des IDs des notes liées |
| status | String | "active", "dismissed", "converted" |
| positionX | Float? | Position X sur le canvas |
| positionY | Float? | Position Y sur le canvas |
| createdBy | String? | FK vers l'utilisateur créateur |
| createdByType | String | "ai" ou "human" |
| Relations | | session, parentIdea?, children[], noteRefs[], creator? |
### BrainstormNoteRef
| Champ | Type | Description |
|-------|------|-------------|
| id | String (cuid) | PK |
| ideaId | String | FK vers l'idée |
| noteId | String? | FK vers la note (peut être null) |
| relation | String | "derived_from", "opposes", "extends", "synthesizes", "transposes", "none_found" |
| explanation | String | Pourquoi cette note est liée |
| verdict | String | "unresolved", "accepted", "dismissed" |
### BrainstormParticipant
| Champ | Type | Description |
|-------|------|-------------|
| sessionId + userId | Unique | Un user = un rôle par session |
| role | String | "host", "editor", "viewer" |
| joinedAt, lastSeenAt | DateTime | Présence |
### BrainstormActivity
| Champ | Type | Description |
|-------|------|-------------|
| action | String | "wave_generated", "manual_idea", "idea_dismissed", "idea_converted", "joined", "invite_created" |
| details | String? | JSON stringifié |
### BrainstormShare
| Champ | Type | Description |
|-------|------|-------------|
| sessionId + userId | Unique | Un partage par couple session/user |
| sharedBy | String | FK vers l'envoyeur |
| status | String | "pending", "accepted", "declined", "removed" |
| permission | String | "editor", "viewer" |
---
## Pages et URLs
| URL | Description |
|-----|-------------|
| `/brainstorm` | Page principale du brainstorm |
| `/brainstorm?session=<id>` | Ouvrir une session spécifique |
| `/brainstorm?seed=<text>` | Auto-créer un brainstorm avec ce seed |
| `/brainstorm?sourceNoteId=<id>` | Créer depuis une note |
| `/brainstorm?invite=<token>` | Rejoindre via token d'invitation |
---
## API Routes
### `POST /api/brainstorm` — Créer un brainstorm
**Input** : `{ seedIdea, sourceNoteId?, contextNoteIds?, locale? }`
**Processus** :
1. Si pas de `contextNoteIds` → embedding du seed → recherche des 8 notes les plus proches
2. LLM classe chaque note : SUPPORT, TENSION, ou EXTENSION
3. LLM génère 9 idées en 3 vagues :
- **Wave 1 (Variations)** : 3 idées, dont au moins 1 de SUPPORT, 1 répondant à TENSION
- **Wave 2 (Analogies)** : 3 idées, dont au moins 1 d'EXTENSION, 1 transposition de pattern
- **Wave 3 (Disruptions)** : 3 idées, dont au moins 1 inversion de SUPPORT, 1 synthèse de contradictions
4. Chaque idée a : wave, title, description, connectionToSeed, noveltyScore, noteRefs[]
5. Positionnement radial : angle = `(idx % 3) * (2π/3) + (wave-1) * 0.5`, radius = `wave * 150`
6. Crée session + host participant + log activity + 9 idées avec noteRefs
**Retour** : Session complète + `{ support, tension, extension }` counts
### `GET /api/brainstorm` — Lister les sessions de l'utilisateur
**Retour** : `BrainstormSessionListItem[]` (id, seedIdea, totalIdeas, activeIdeas, dates)
### `GET /api/brainstorm/shared` — Sessions partagées acceptées
**Processus** : Fetch shares where userId + accepted, upsert participant si manquant
**Retour** : Même format que la liste + `_isShared: true`
### `GET /api/brainstorm/[sessionId]` — Détail d'une session
**Accès** : Propriétaire OU participant OU share accepted
**Retour** : Session complète avec ideas (ordonnées par wave/date), noteRefs, creator, sourceNote, exportedNote
### `DELETE /api/brainstorm/[sessionId]` — Supprimer une session
**Accès** : Propriétaire uniquement
### `POST /api/brainstorm/[sessionId]/expand` — Approfondir une idée
**Accès** : Propriétaire uniquement
**Input** : `{ ideaId, locale? }`
**Processus** :
1. Charge les noteRefs de l'idée parent + 5 notes via embedding
2. LLM génère 9 sous-idées en 3 vagues relatives au parent
3. Positionnement radial autour du parent
4. Crée les idées avec `parentIdeaId = source idea`
**Retour** : Session mise à jour
### `POST /api/brainstorm/[sessionId]/manual-idea` — Ajouter une idée manuelle
**Accès** : Participant avec rôle editor
**Input** : `{ title, description?, parentIdeaId?, locale? }`
**Processus** :
1. Wave = `min(parent.wave + 1, 3)` si parent, sinon 1
2. Crée l'idée avec `createdBy = userId`, `createdByType = 'human'`
3. Auto-link : embedding → 3 notes les plus proches → BrainstormNoteRef avec relation "extends"
4. **Enrichissement IA** (non-bloquant) : LLM polit le titre, enrichit la description, set noveltyScore. Respecte la locale (`IMPORTANT: You MUST write ALL text in ${lang}`)
**Retour** : Session mise à jour (201)
### `POST /api/brainstorm/[sessionId]/dismiss` — Rejeter une idée
**Accès** : Participant avec rôle editor
**Input** : `{ ideaId }`
**Processus** : Transaction → status = "dismissed" + tous noteRefs verdict = "dismissed"
### `POST /api/brainstorm/[sessionId]/convert` — Convertir en note
**Accès** : Propriétaire uniquement
**Input** : `{ ideaId }`
**Processus** :
1. Crée une Note avec :
- Titre = titre de l'idée
- Contenu = markdown formaté (description, connection, novelty, source brainstorm, noteRefs avec relations)
- Labels = `['brainstorm', 'idée']`
- Même notebook que la note source
2. Transaction : status = "converted", convertedToNoteId = note.id, noteRefs verdict = "accepted", tag les notes référencées avec "brainstorm-fruitful"
**Retour** : Note créée (201)
### `POST /api/brainstorm/[sessionId]/finalize` — Finaliser la session
**Accès** : Propriétaire uniquement
**Processus** :
- Pour chaque note référencée : si tous les refs sont dismissed → tag "brainstorm-dry"
- Compte notesEnriched (≥1 accepted) et notesMarkedDry (all dismissed)
**Retour** : `{ notesSolicited, notesEnriched, notesMarkedDry }`
### `POST /api/brainstorm/[sessionId]/export` — Exporter en note
**Accès** : Propriétaire uniquement
**Processus** :
- Génère un markdown complet : header, summary, sections par wave, notes sollicitées, notes converties
- Crée une Note avec labels `['brainstorm', 'export']`
- Update session.exportedNoteId
**Retour** : Note créée (201)
### `POST /api/brainstorm/[sessionId]/update-position` — Sauvegarder la position d'un nœud
**Input** : `{ ideaId, positionX, positionY }`
### `POST /api/brainstorm/[sessionId]/invite` — Créer une invitation
**Accès** : Host uniquement
**Input** : `{ role, expiresInHours?, email? }`
- Si `email` fourni : trouve l'utilisateur, crée une Notification, retourne `{ mode: 'email' }`
- Sinon : génère un token, retourne `{ mode: 'link', inviteUrl }`
### `POST /api/brainstorm/join` — Rejoindre via token
**Input** : `{ token }`
**Processus** : Vérifie token + expiry → crée BrainstormParticipant → notifie le owner
### `GET /api/brainstorm/[sessionId]/activity` — Feed d'activité
**Retour** : 50 dernières activités avec user info
---
## Server Actions (`app/actions/brainstorm.ts`)
### `createBrainstormShare(sessionId, recipientEmail, permission?)`
Partage par email (même pattern que NoteShare).
1. Vérifie que le user est propriétaire
2. Cherche le recipient par email
3. Gère les cas existants :
- `accepted` → retourne `{ message: 'already_shared' }` (succès info)
- `pending` → retourne `{ message: 'already_pending' }` (succès info)
- `declined/removed` → ré-invite (remet à pending)
4. Sinon → crée un `BrainstormShare` avec status pending
### `respondToBrainstormShare(shareId, action)`
Accepter ou refuser un partage.
- Accept → upsert `BrainstormParticipant` (idempotent)
- Decline → update status
### `getPendingBrainstormShares()`
Retourne les shares pending pour l'utilisateur courant (avec session + sharer info).
### `getAcceptedBrainstormShares()`
Retourne les shares accepted (pour afficher dans la sidebar).
### `removeBrainstormShare(sessionId)`
Passe le share en status "removed".
---
## Interface utilisateur — Page Brainstorm
### Layout global
```
┌──────────────────────────────────────────────────────────────┐
│ HEADER (fixed, border-bottom, backdrop-blur) │
│ [Wind icon] Waves of Thought │
│ "Unfold dimensions of potentiality" │
│ [__________ input seed idea __________] [+] │
│ • • • AI is harvesting seeds of thought... │
├──────────────────────────────────┬─────────────┬────┐ │
│ │ │ │ │
│ CANVAS (D3) │ DETAIL │ S │ │
│ │ PANEL │ I │ │
│ ○ ring 1 │ (400px) │ D │ │
│ ○ ring 2 │ │ E │ │
│ ○ ring 3 │ │ B │ │
│ │ │ A │ │
│ [toolbar flottant en bas] │ │ R │ │
│ │ │ │ │
├──────────────────────────────────┴─────────────┴────┘ │
│ [Activity Feed panel] (slide depuis la droite) │
│ [BrainstormShareDialog] (modal) │
│ [Impact Toast] (fixed bottom center) │
└──────────────────────────────────────────────────────────────┘
```
### Header
- **Icône** : `<Wind>` dans un carré orange qui tourne pendant la génération
- **Titre** : "Waves of Thought" (serif)
- **Sous-titre** : "Unfold dimensions of potentiality" (muted)
- **Input seed** : Grand champ serif italic avec glow gradient au focus
- **Bouton submit** : `<Plus>` icône, positionné à droite dans l'input, disabled si vide ou en génération
- **Indicateur de génération** : 3 points orange pulsants + texte "AI is harvesting seeds of thought..."
### Canvas D3 (WaveCanvas)
#### Éléments visuels
- **Fond** : `#F8F7F2` avec grille de points (20px)
- **3 anneaux** : rayons 200, 400, 600 — traits gris pointillés, opacité 0.5
- **Nœud racine** (seed) :
- Cercle noir (#141414), rayon 40
- Texte "SEED" en blanc, 10px bold
- Label de l'idée graine en serif italic 18px, positionné à y=80
- **Nœuds idée** :
- Cercle blanc avec bordure colorée par wave (orange/blue/violet)
- Rayon 28 (18 si dismissed)
- Texte : titre tronqué à 18 caractères, positionné sous le cercle
- Badges :
- **Converti** : fond vert + checkmark ✓
- **Notes liées** : emoji 📎 en haut-gauche
- **Créé par humain** : cercle bleu avec initiale en haut-droite
- **Créé par IA** : symbole ✦ violet en haut-droite
- **Dismissed** : opacité 0.3
- **Liens** :
- Wave (racine → idée) : gris pointillé, 1.5px
- Parent (idée → sous-idée) : jaune solid, 2px
#### Forces D3
- `forceLink` : distance = wave × 200 (wave links), 180 (parent links)
- `forceManyBody` : strength -800 (répulsion)
- `forceRadial` : rayon = wave × 200, strength 0.8
- `forceCollide` : radius + 30
#### Interactions
| Action | Effet |
|--------|-------|
| **Clic sur nœud** | Sélectionne l'idée → ouvre le panneau détail |
| **Double-clic sur nœud** | Ouvre l'éditeur inline pour créer un enfant |
| **Double-clic sur canvas** | Ouvre l'éditeur inline pour créer une idée racine |
| **Drag d'un nœud** | Déplace le nœud, fixe la position, émet `idea:moved` au socket |
| **Zoom/Pan** | d3.zoom, échelle [0.1, 5], centré avec scale 0.8 |
#### Éditeur inline (double-clic)
Apparaît sous le point de clic, carte flottante (260px) :
- En-tête : icône "+" bleue + label "Réponse" ou "Nouvelle idée"
- Input serif avec fond subtil
- Raccourcis : `↵ enregistrer` · `esc annuler`
- Si enfant : label "→ enfant"
- Triangle pointeur sous la carte
- **Enter** → appelle `onCreateIdea({ title, parentIdeaId, x, y })`
- **Escape** → ferme
#### Stabilité du canvas
- Le useEffect D3 ne se redéclenche QUE quand `sessionId` ou `ideasKey` (string sérialisée) changent
- Tous les callbacks (onNodeSelect, onPositionUpdate, onCreateIdea) sont passés via refs (`onNodeSelectRef.current`) pour éviter les re-renders
- Les remote moves (socket) mettent à jour directement les nœuds D3 sans re-render React
### Toolbar flottante (bas du canvas)
Pilule animée qui apparaît quand une session est active :
| Élément | Style | Action |
|---------|-------|--------|
| **Wave 1/2/3** | 3 points colorés (orange/blue/violet) | Légende visuelle |
| **Avatars** | Cercles colorés empilés (max 4, "+N") | Présence des utilisateurs connectés |
| **Exporter** | Texte orange | Appelle `exportBrainstorm` puis `finalizeBrainstorm` |
| **Inviter** | Texte émeraude, icône `<UserPlus>` | Ouvre `BrainstormShareDialog` |
| **Activité** | Icône `<Activity>`, muted/orange si actif | Toggle le panneau Activity Feed |
| **Supprimer** | Icône poubelle, muted → rose au hover | Supprime la session |
### Panneau détail (droite, 400px, slide animé)
Apparaît quand une idée est sélectionnée :
| Section | Contenu |
|---------|---------|
| **Badge wave** | Pilule colorée (Wave 1 orange / Wave 2 blue / Wave 3 violet) |
| **Note créée** | Badge vert si convertie |
| **Fermer** | Bouton chevron droite |
| **Titre** | 3xl serif bold |
| **Score de nouveauté** | `<Zap>` + nombre |
| **Créateur** | Humain = cercle bleu + initiale, IA = spark violet + "IA" |
| **Description** | Texte de l'idée |
| **Connexion au seed** | Carte slate, citation italic |
| **Origine de l'idée** | Cartes noteRef avec badge de relation (emerald=positive, rose=opposes, amber=autre), bouton "View" |
| **Bouton Approfondir** | `<Wind>` + "Deepen", bordure dashed, hover orange → `expandIdea` |
| **Bouton Créer Note** | `<FileText>` + "Create Note", bordure dashed, hover émeraude → `convertIdea` |
| **Bouton Non pertinent** | Texte muted, hover rose → `dismissIdea` |
### Sidebar sessions (bande droite, 64px)
- Icône `<History>` en haut
- Liste scrollable de boutons circulaires (40px) avec la première lettre du seed
- **Actif** : fond sombre, texte blanc, scale 110, shadow
- **Partagé** : fond orange clair, texte orange
- **Normal** : fond blanc, texte muted
- Filet décoratif en bas
### Activity Feed (panneau coulissant, 320px)
Slide depuis la droite avec animation spring :
- En-tête : icône + "Activity" + bouton fermer
- État vide : "No activity yet" italic
- Items : icône par type d'action, username bold, label d'action, titre idée (tronqué 30 chars), temps relatif (1m/1h/1d)
- Types : `manual_idea` (ampoule bleue), `wave_generated` (éclair orange), `joined` (user+ vert), `idea_dismissed` (X rose), `invite_created` (user+ violet)
### BrainstormShareDialog (modal)
- Trigger : bouton "Inviter" de la toolbar
- Header : icône orange + "Partager le brainstorm" + seed idea tronqué
- Champ email avec label "Adresse email"
- Bouton "Partager" orange avec `<UserPlus>`
- Messages de feedback :
- Succès (vert + ✓) : "Invitation envoyée!" / "Invitation renvoyée!"
- Info (ambre + ⚠) : "Cette personne a déjà accès" / "Invitation déjà en attente"
- Erreur (rose + ⚠) : "No account found with this email" etc.
- Note : "La personne recevra une notification pour accepter ou refuser."
### Impact Toast (fixed bottom center)
Apparaît après Export + Finalize :
- "X note(s) enriched / X note(s) marked dry"
- Disparaît après 4 secondes
---
## Sidebar gauche (app principale)
### Section Brainstorms (`SidebarBrainstorms`)
- **Vue vide** : Icône `<Sparkles>` orange + "Aucune session" + "Démarrer →"
- **Loading** : 3 skeletons pulsants
- **Liste** (max 10) :
- **Sessions possédées** : Cercle orange + `<Sparkles>` + seed idea + count idées + date + bouton supprimer (rose au hover)
- **Sessions partagées** : Cercle bleu + `<Sparkles>` + seed idea + badge "partagé" + count idées + date (pas de bouton supprimer)
- Clic → navigate vers `/brainstorm?session={id}`
### Navigation
- Bouton "brainstorms" dans le toggle de vues (icône `<Sparkles>` orange, highlight orange quand actif)
---
## Notifications (cloche)
### Partages brainstorm pending
- Section dédiée entre les reminders et les share requests de notes
- Avatar orange gradient avec initiale de l'envoyeur
- Icône `<Wind>` + label "BRAINSTORM" orange
- Nom de l'envoyeur + "invited you to a brainstorm" + aperçu du seed (tronqué 35 chars)
- Boutons Accepter (orange) / Refuser
### Notifications système
- `brainstorm_invite` : icône `<Wind>` émeraude
- `brainstorm_joined` : icône `<Wind>` bleue
---
## Temps réel (Socket.io)
### Événements client → serveur
| Event | Data | Quand |
|-------|------|-------|
| `cursor:move` | `{ x, y }` ou `null` | Mouvement souris sur le canvas |
| `idea:moved` | `{ ideaId, positionX, positionY, userId }` | Fin de drag d'un nœud |
| `activity:new` | `{ action, userId, userName, details }` | Action utilisateur |
### Événements serveur → client
| Event | Data | Effet |
|-------|------|-------|
| `presence:update` | `PresenceUser[]` | Mise à jour de la liste des utilisateurs connectés |
| `cursor:update` | `{ userId, cursor: {x,y} }` | Déplacement du curseur d'un autre user |
| `activity:new` | `ActivityEvent` | Nouvelle activité dans le feed |
| `idea:moved` | `{ ideaId, positionX, positionY }` | Déplacement d'un nœud par un autre user |
| `idea:added` | — | Placeholder (non utilisé côté client) |
| `idea:dismissed` | — | Placeholder (non utilisé côté client) |
### Flux de déplacement en temps réel
1. User A drag un nœud → fin de drag → `handlePositionUpdate` → émet `idea:moved` + POST API (persist)
2. Socket server → broadcast `idea:moved` aux autres dans la room
3. User B → `useBrainstormSocket` callback → `setRemoteMove(...)` (avec compteur séquentiel)
4. WaveCanvas → `useEffect([remoteMove])` → fixe fx/fy du nœud cible → 30 ticks de simulation → update positions liens/nœuds directement dans D3
### Curseurs live
- Chaque user a un curseur coloré (SVG arrow + nom) affiché sur le canvas des autres
- 12 couleurs assignées en round-robin
- `useCursorTracking` attache un listener mousemove au container et émet `cursor:move`
### Ghost Cursor IA
- Quand une vague IA est en génération, un curseur violet animé apparaît
- Se déplace aléatoirement dans un rayon de 150-350px du centre
- Pulsation violette + badge "AI ✦"
---
## Flux de partage
### Par email (server action — principal)
```
Propriétaire → Dialog email → createBrainstormShare()
→ Cherche recipient par email
→ Crée BrainstormShare (pending)
→ Destinataire voit dans NotificationPanel (cloche)
→ Accept → Upsert BrainstormParticipant
→ Decline → Update status
→ Apparaît dans sidebar (bleu, badge "partagé")
```
### Par lien (API invite — secondaire)
```
Propriétaire → invite route → génère token → copie URL
→ Destinataire ouvre URL avec ?invite=<token>
→ join route → vérifie token + expiry
→ Crée BrainstormParticipant
→ Notifie le owner
```
---
## Hooks React Query
| Hook | Type | Query Key | API |
|------|------|-----------|-----|
| `useBrainstormSessions()` | Query | `['brainstorm', 'sessions']` | GET /api/brainstorm |
| `useSharedBrainstormSessions()` | Query | `['brainstorm', 'shared-sessions']` | GET /api/brainstorm/shared |
| `useBrainstormSession(id)` | Query | `['brainstorm', 'session', id]` | GET /api/brainstorm/{id} |
| `useCreateBrainstorm()` | Mutation | — | POST /api/brainstorm |
| `useExpandIdea(id)` | Mutation | — | POST /api/brainstorm/{id}/expand |
| `useDismissIdea(id)` | Mutation | — | POST /api/brainstorm/{id}/dismiss |
| `useConvertIdea(id)` | Mutation | — | POST /api/brainstorm/{id}/convert |
| `useExportBrainstorm(id)` | Mutation | — | POST /api/brainstorm/{id}/export |
| `useFinalizeBrainstorm(id)` | Mutation | — | POST /api/brainstorm/{id}/finalize |
| `useDeleteBrainstorm()` | Mutation | — | DELETE /api/brainstorm/{id} |
| `useAddManualIdea(id)` | Mutation | — | POST /api/brainstorm/{id}/manual-idea |
| `useBrainstormActivity(id)` | Query | — | GET /api/brainstorm/{id}/activity (refetch 10s) |
---
## Fichiers sources
| Fichier | Rôle |
|---------|------|
| `components/brainstorm/brainstorm-page.tsx` | Page principale complète |
| `components/brainstorm/wave-canvas.tsx` | Canvas D3 (noeuds, liens, interactions) |
| `components/brainstorm/brainstorm-share-dialog.tsx` | Modal de partage par email |
| `components/brainstorm/activity-feed.tsx` | Panneau d'activité |
| `components/brainstorm/ghost-cursor.tsx` | Curseur IA animé |
| `components/brainstorm/live-cursors.tsx` | Curseurs des autres users + avatars |
| `hooks/use-brainstorm.ts` | Tous les hooks React Query |
| `hooks/use-brainstorm-socket.ts` | Hook Socket.io |
| `app/actions/brainstorm.ts` | Server actions (partage) |
| `app/api/brainstorm/route.ts` | POST create + GET list |
| `app/api/brainstorm/[sessionId]/route.ts` | GET/DELETE session |
| `app/api/brainstorm/[sessionId]/expand/route.ts` | POST approfondir |
| `app/api/brainstorm/[sessionId]/manual-idea/route.ts` | POST idée manuelle |
| `app/api/brainstorm/[sessionId]/dismiss/route.ts` | POST rejeter |
| `app/api/brainstorm/[sessionId]/convert/route.ts` | POST convertir en note |
| `app/api/brainstorm/[sessionId]/finalize/route.ts` | POST finaliser |
| `app/api/brainstorm/[sessionId]/export/route.ts` | POST exporter |
| `app/api/brainstorm/[sessionId]/update-position/route.ts` | POST position |
| `app/api/brainstorm/[sessionId]/invite/route.ts` | POST invitation par lien |
| `app/api/brainstorm/[sessionId]/activity/route.ts` | GET activités |
| `app/api/brainstorm/join/route.ts` | POST rejoindre par token |
| `app/api/brainstorm/shared/route.ts` | GET sessions partagées |
| `socket-server.ts` | Serveur Socket.io |
| `types/brainstorm.ts` | Types TypeScript |
| `lib/brainstorm-collab.ts` | verifyParticipant + logActivity |
| `prisma/schema.prisma` | Modèles DB (lignes 454-572) |
| `components/notification-panel.tsx` | Notifications partage brainstorm |
| `components/sidebar.tsx` | SidebarBrainstorms (lignes 85-169) |
---
## Composants obsolètes (existent mais non utilisés)
| Fichier | Note |
|---------|------|
| `components/brainstorm/manual-idea-dialog.tsx` | Remplacé par l'éditeur inline du canvas |
| `components/brainstorm/invite-dialog.tsx` | Remplacé par BrainstormShareDialog |
| `components/brainstorm/brainstorm-create-dialog.tsx` | Remplacé par l'input inline du header |
| `components/brainstorm/brainstorm-canvas.tsx` | Ancienne implémentation avec react-force-graph-2d, remplacé par WaveCanvas (D3 direct) |

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,107 @@
'use client'
import { useEffect, useRef, useState, useCallback } from 'react'
import { io, Socket } from 'socket.io-client'
export interface PresenceUser {
userId: string
name: string
cursor: { x: number; y: number } | null
color: string
}
export interface ActivityEvent {
action: string
userId: string
userName: string
details: any
}
const SOCKET_URL = process.env.NEXT_PUBLIC_SOCKET_URL || 'http://localhost:3001'
export function useBrainstormSocket(
sessionId: string | null,
userId: string | null,
userName: string | null,
onIdeaMoved?: (data: { ideaId: string; positionX: number; positionY: number }) => void
) {
const socketRef = useRef<Socket | null>(null)
const onIdeaMovedRef = useRef(onIdeaMoved)
onIdeaMovedRef.current = onIdeaMoved
const [others, setOthers] = useState<PresenceUser[]>([])
const [activities, setActivities] = useState<ActivityEvent[]>([])
const [aiProcessingNodeId, setAiProcessingNodeId] = useState<string | null>(null)
const effectiveUserId = userId || `guest_${Math.random().toString(36).slice(2, 10)}`
useEffect(() => {
if (!sessionId) return
const socket = io(SOCKET_URL, {
auth: {
userId: effectiveUserId,
sessionId,
name: userName || 'Guest',
isGuest: !userId,
},
transports: ['websocket'],
autoConnect: true,
})
socketRef.current = socket
socket.on('presence:update', (users: PresenceUser[]) => {
setOthers(users.filter(u => u.userId !== effectiveUserId))
})
socket.on('cursor:update', (data: { userId: string; cursor: { x: number; y: number } }) => {
setOthers(prev => prev.map(u =>
u.userId === data.userId ? { ...u, cursor: data.cursor } : u
))
})
socket.on('activity:new', (event: ActivityEvent) => {
setActivities(prev => [event, ...prev].slice(0, 50))
})
socket.on('idea:added', () => {})
socket.on('idea:dismissed', () => {})
socket.on('idea:moved', (data: { ideaId: string; positionX: number; positionY: number }) => {
onIdeaMovedRef.current?.(data)
})
socket.on('idea:ai_processing', (data: { ideaId: string }) => {
setAiProcessingNodeId(data.ideaId)
})
socket.on('idea:ai_completed', (data: { ideaId: string }) => {
setAiProcessingNodeId(prev => prev === data.ideaId ? null : prev)
})
socket.on('idea:ai_failed', (data: { ideaId: string }) => {
setAiProcessingNodeId(prev => prev === data.ideaId ? null : prev)
})
return () => {
socket.disconnect()
socketRef.current = null
setOthers([])
setAiProcessingNodeId(null)
}
}, [sessionId, effectiveUserId, userName])
const moveCursor = useCallback((cursor: { x: number; y: number } | null) => {
socketRef.current?.emit('cursor:move', cursor)
}, [])
const broadcastActivity = useCallback((action: string, details?: any) => {
socketRef.current?.emit('activity:new', {
action,
userId: effectiveUserId,
userName: userName || 'Guest',
details,
})
}, [effectiveUserId, userName])
return { others, activities, moveCursor, broadcastActivity, socketRef, aiProcessingNodeId }
}

View File

@@ -0,0 +1,316 @@
'use client'
import { useQueryClient, useMutation, useQuery } from '@tanstack/react-query'
import { queryKeys } from '@/lib/query-keys'
import type { BrainstormSession, BrainstormSessionListItem } from '@/types/brainstorm'
export interface CreateBrainstormResult {
session: BrainstormSession
contextSummary?: {
support: number
tension: number
extension: number
}
}
export function useBrainstormSessions() {
return useQuery({
queryKey: queryKeys.brainstormSessions(),
queryFn: async (): Promise<BrainstormSessionListItem[]> => {
const res = await fetch('/api/brainstorm', { credentials: 'include' })
const data = await res.json()
return data.data || []
},
})
}
export function useSharedBrainstormSessions() {
return useQuery({
queryKey: queryKeys.brainstormSharedSessions(),
queryFn: async (): Promise<BrainstormSessionListItem[]> => {
const res = await fetch('/api/brainstorm/shared', { credentials: 'include' })
const data = await res.json()
return data.data || []
},
})
}
export interface BrainstormSessionMeta {
role: 'owner' | 'editor' | 'viewer' | 'guest' | 'none'
canEdit: boolean
}
export interface BrainstormSessionResult {
session: BrainstormSession
meta?: BrainstormSessionMeta
}
export function useBrainstormSession(sessionId: string | null) {
return useQuery({
queryKey: queryKeys.brainstormSession(sessionId || ''),
queryFn: async (): Promise<BrainstormSessionResult | null> => {
if (!sessionId) return null
const res = await fetch(`/api/brainstorm/${sessionId}`, { credentials: 'include' })
const data = await res.json()
if (!data.data) return null
return {
session: data.data,
meta: data._meta || undefined,
}
},
enabled: !!sessionId,
})
}
export function useCreateBrainstorm() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (input: {
seedIdea: string
sourceNoteId?: string
contextNoteIds?: string[]
locale?: string
}): Promise<CreateBrainstormResult> => {
const res = await fetch('/api/brainstorm', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(input),
})
const data = await res.json()
if (!data.success) throw new Error(data.error || 'Failed to create brainstorm')
return { session: data.data, contextSummary: data.contextSummary }
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.brainstormSessions() })
},
})
}
export function useExpandIdea(sessionId: string) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (input: { ideaId: string; locale?: string }): Promise<BrainstormSession> => {
const res = await fetch(`/api/brainstorm/${sessionId}/expand`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(input),
})
const data = await res.json()
if (!data.success) throw new Error(data.error || 'Failed to expand idea')
return data.data
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.brainstormSession(sessionId) })
queryClient.invalidateQueries({ queryKey: queryKeys.brainstormSessions() })
},
})
}
export function useDismissIdea(sessionId: string) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (ideaId: string) => {
const res = await fetch(`/api/brainstorm/${sessionId}/dismiss`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ ideaId }),
})
const data = await res.json()
if (!data.success) throw new Error(data.error || 'Failed to dismiss idea')
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.brainstormSession(sessionId) })
},
})
}
export function useConvertIdea(sessionId: string) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (ideaId: string) => {
const res = await fetch(`/api/brainstorm/${sessionId}/convert`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ ideaId }),
})
const data = await res.json()
if (!data.success) throw new Error(data.error || 'Failed to convert idea')
return data.data
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.brainstormSession(sessionId) })
queryClient.invalidateQueries({ queryKey: queryKeys.brainstormSessions() })
},
})
}
export function useExportBrainstorm(sessionId: string) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async () => {
const res = await fetch(`/api/brainstorm/${sessionId}/export`, {
method: 'POST',
credentials: 'include',
})
const data = await res.json()
if (!data.success) throw new Error(data.error || 'Failed to export brainstorm')
return data.data
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.brainstormSession(sessionId) })
queryClient.invalidateQueries({ queryKey: queryKeys.brainstormSessions() })
},
})
}
export function useFinalizeBrainstorm(sessionId: string) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async () => {
const res = await fetch(`/api/brainstorm/${sessionId}/finalize`, {
method: 'POST',
credentials: 'include',
})
const data = await res.json()
if (!data.success) throw new Error(data.error || 'Failed to finalize brainstorm')
return data.impact
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.brainstormSessions() })
},
})
}
export function useDeleteBrainstorm() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (sessionId: string) => {
const res = await fetch(`/api/brainstorm/${sessionId}`, {
method: 'DELETE',
credentials: 'include',
})
const data = await res.json()
if (!data.success) throw new Error(data.error || 'Failed to delete brainstorm')
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.brainstormSessions() })
},
})
}
export function useInviteParticipant(sessionId: string) {
return useMutation({
mutationFn: async (input: { role: 'editor' | 'viewer'; expiresInHours?: number; email?: string }) => {
const res = await fetch(`/api/brainstorm/${sessionId}/invite`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(input),
})
const data = await res.json()
if (!data.success) throw new Error(data.error || 'Failed to create invite')
return data
},
})
}
export function useJoinBrainstorm() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (token: string) => {
const res = await fetch(`/api/brainstorm/join`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ token }),
})
const data = await res.json()
if (!data.success) throw new Error(data.error || 'Failed to join')
return data
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.brainstormSessions() })
},
})
}
export function useAddManualIdea(sessionId: string) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (input: { title: string; description?: string; parentIdeaId?: string; locale?: string }) => {
const res = await fetch(`/api/brainstorm/${sessionId}/manual-idea`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(input),
})
const data = await res.json()
if (!data.success) throw new Error(data.error || 'Failed to add idea')
return data.data
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.brainstormSession(sessionId) })
},
})
}
export function useBrainstormActivity(sessionId: string | null) {
return useQuery({
queryKey: ['brainstorm', 'activity', sessionId],
queryFn: async () => {
if (!sessionId) return []
const res = await fetch(`/api/brainstorm/${sessionId}/activity`, { credentials: 'include' })
const data = await res.json()
return data.data || []
},
enabled: !!sessionId,
refetchInterval: 10000,
})
}
export function useUpdateBrainstormSettings(sessionId: string) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (input: { isPublic?: boolean; guestCanEdit?: boolean }) => {
const res = await fetch(`/api/brainstorm/${sessionId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(input),
})
const data = await res.json()
if (!data.success) throw new Error(data.error || 'Failed to update settings')
return data.data
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.brainstormSession(sessionId) })
},
})
}
export function useBrainstormSnapshots(sessionId: string | null) {
return useQuery({
queryKey: ['brainstorm', 'snapshots', sessionId],
queryFn: async () => {
if (!sessionId) return []
const res = await fetch(`/api/brainstorm/${sessionId}/snapshots`, { credentials: 'include' })
const data = await res.json()
return data.data || []
},
enabled: !!sessionId,
})
}

View File

@@ -23,7 +23,7 @@ import '../tools'
// --- Types ---
export type AgentType = 'scraper' | 'researcher' | 'monitor' | 'custom' | 'slide-generator' | 'excalidraw-generator'
export type AgentType = 'scraper' | 'researcher' | 'monitor' | 'custom' | 'slide-generator' | 'excalidraw-generator' | 'task-extractor'
export interface AgentExecutionResult {
success: boolean
@@ -941,6 +941,28 @@ IMPERATIVE DESIGN RULES:
- Concise points (max 100 chars), punchy and short titles
- Strict JSON for generate_pptx, no text outside JSON.`,
},
'task-extractor': {
fr: `Tu es un expert en gestion de tâches et extraction d'action items. Tu analyses des notes et documents pour identifier toutes les tâches, TODOs, et actions à accomplir.
Utilise OBLIGATOIREMENT l'outil task_extract. Ne réponds PAS avec du texte, appelle directement l'outil.
## RÈGLES
- Identifie TOUTES les tâches explicites et implicites
- Pour chaque tâche, détermine: priorité (High/Medium/Low), assigné, deadline, statut
- Les priorités High = urgent/dates proches, Medium = important, Low = Nice to have
- Regroupe par priorité dans la note de synthèse
- Utilise le format Markdown avec une table récapitulative`,
en: `You are a task extraction specialist. You analyze notes and documents to identify ALL action items, TODOs, and tasks to accomplish.
You MUST use the task_extract tool. Do NOT respond with text, call the tool directly.
## RULES
- Identify ALL explicit and implicit tasks
- For each task, determine: priority (High/Medium/Low), assignee, deadline, status
- High priority = urgent/close deadlines, Medium = important, Low = Nice to have
- Group by priority in the synthesis note
- Use Markdown format with a summary table`,
},
}
// --- Tool-Use Agent ---
@@ -1172,6 +1194,38 @@ async function executeToolUseAgent(
}
break
}
case 'task-extractor': {
const untitled = lang === 'fr' ? 'Sans titre' : 'Untitled'
const dateLocale = lang === 'fr' ? 'fr-FR' : 'en-US'
let notes: any[] = []
if (agent.sourceNotebookId) {
notes = await prisma.note.findMany({
where: { notebookId: agent.sourceNotebookId, userId: agent.userId, isArchived: false, trashedAt: null },
orderBy: { createdAt: 'desc' }, take: 20,
select: { id: true, title: true, content: true, createdAt: true }
})
} else {
notes = await prisma.note.findMany({
where: { userId: agent.userId, isArchived: false, trashedAt: null },
orderBy: { updatedAt: 'desc' }, take: 20,
select: { id: true, title: true, content: true, createdAt: true }
})
}
const notebookId = agent.sourceNotebookId || agent.targetNotebookId || null
prompt = lang === 'fr'
? `Analyse les notes suivantes et extrais TOUS les action items, tâches et TODOs. Utilise l'outil task_extract pour créer une note de synthèse.${notebookId ? ` Passe notebookId="${notebookId}" à task_extract.` : ''}`
: `Analyze the following notes and extract ALL action items, tasks and TODOs. Use the task_extract tool to create a synthesis note.${notebookId ? ` Pass notebookId="${notebookId}" to task_extract.` : ''}`
if (notes.length > 0) {
const notesContext = notes.map(n =>
`### ${n.title || untitled} (${n.createdAt.toLocaleDateString(dateLocale)})\n${n.content.substring(0, 500)}`
).join('\n\n')
prompt += `\n\n${lang === 'fr' ? 'Notes à analyser' : 'Notes to analyze'}:\n\n${notesContext}`
}
prompt += `\n\n${lang === 'fr'
? 'IMPORTANT : Utilise OBLIGATOIREMENT l\'outil task_extract. Ne réponds pas avec du texte, appelle directement l\'outil.'
: 'IMPORTANT: You MUST use the task_extract tool. Do NOT respond with text, call the tool directly.'}`
break
}
default: {
const urls: string[] = agent.sourceUrls ? JSON.parse(agent.sourceUrls) : []
prompt = agent.role || (lang === 'fr' ? 'Accomplis la tâche demandée en utilisant les outils disponibles.' : 'Accomplish the requested task using available tools.')

View File

@@ -0,0 +1,83 @@
interface ChunkInput {
text: string
pageNumber: number
}
export interface DocumentChunkData {
content: string
chunkIndex: number
pageNumber: number
startChar: number
endChar: number
metadata?: string
}
export class DocumentChunkingService {
private readonly CHUNK_SIZE = 800
private readonly OVERLAP = 200
chunk(pages: ChunkInput[]): DocumentChunkData[] {
const chunks: DocumentChunkData[] = []
let globalIndex = 0
let previousTail = ''
for (const page of pages) {
const text = page.text.trim()
if (!text) continue
const sections = this.splitSections(text)
let buffer = previousTail
let bufferStart = 0
for (const section of sections) {
if (buffer.length + section.length > this.CHUNK_SIZE && buffer.length > 0) {
chunks.push({
content: buffer.trim(),
chunkIndex: globalIndex++,
pageNumber: page.pageNumber,
startChar: bufferStart,
endChar: bufferStart + buffer.length,
})
previousTail = buffer.slice(-this.OVERLAP)
buffer = previousTail + '\n' + section
bufferStart += buffer.length - section.length - previousTail.length
} else {
buffer += (buffer ? '\n\n' : '') + section
}
}
if (buffer.trim()) {
chunks.push({
content: buffer.trim(),
chunkIndex: globalIndex++,
pageNumber: page.pageNumber,
startChar: bufferStart,
endChar: bufferStart + buffer.length,
})
previousTail = buffer.slice(-this.OVERLAP)
}
}
return chunks
}
private splitSections(text: string): string[] {
const lines = text.split('\n')
const sections: string[] = []
let current = ''
for (const line of lines) {
const isHeading = /^(#{1,6}\s|[A-Z][A-Z\s]{5,}$)/.test(line.trim())
if (isHeading && current.trim()) {
sections.push(current.trim())
current = line
} else {
current += (current ? '\n' : '') + line
}
}
if (current.trim()) sections.push(current.trim())
return sections
}
}
export const documentChunkingService = new DocumentChunkingService()

View File

@@ -0,0 +1,56 @@
import fs from 'fs'
import path from 'path'
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.mjs'
if (typeof pdfjsLib.GlobalWorkerOptions !== 'undefined') {
pdfjsLib.GlobalWorkerOptions.workerSrc = path.join(
process.cwd(),
'node_modules/pdfjs-dist/legacy/build/pdf.worker.mjs'
)
}
interface ExtractedPage {
pageNumber: number
text: string
}
export interface ExtractedDocument {
pages: ExtractedPage[]
totalPages: number
metadata: { title?: string; author?: string }
}
export class DocumentExtractionService {
async extractPdf(filePath: string): Promise<ExtractedDocument> {
const dataBuffer = fs.readFileSync(filePath)
const doc = await pdfjsLib.getDocument({
data: new Uint8Array(dataBuffer),
useSystemFonts: true,
useWorkerFetch: false,
isEvalSupported: false,
}).promise
const pages: ExtractedPage[] = []
for (let i = 1; i <= doc.numPages; i++) {
const page = await doc.getPage(i)
const content = await page.getTextContent()
const text = content.items
.map((item: any) => item.str)
.join(' ')
pages.push({ pageNumber: i, text })
}
const metadata = await doc.getMetadata().catch(() => null) as any
return {
pages,
totalPages: doc.numPages,
metadata: {
title: metadata?.info?.Title,
author: metadata?.info?.Author,
},
}
}
}
export const documentExtractionService = new DocumentExtractionService()

View File

@@ -0,0 +1,79 @@
import prisma from '@/lib/prisma'
import { documentExtractionService } from './document-extraction.service'
import { documentChunkingService } from './document-chunking.service'
import { embeddingService } from './embedding.service'
export class DocumentIngestionService {
async ingest(attachmentId: string): Promise<void> {
const attachment = await prisma.noteAttachment.findUnique({
where: { id: attachmentId },
})
if (!attachment) throw new Error('Attachment not found')
await prisma.noteAttachment.update({
where: { id: attachmentId },
data: { status: 'processing' },
})
try {
const extracted = await documentExtractionService.extractPdf(attachment.filePath)
await prisma.noteAttachment.update({
where: { id: attachmentId },
data: { pageCount: extracted.totalPages },
})
const chunkInputs = extracted.pages.map(p => ({
text: p.text,
pageNumber: p.pageNumber,
}))
const chunks = documentChunkingService.chunk(chunkInputs)
const created = await Promise.all(
chunks.map(c =>
prisma.documentChunk.create({
data: {
attachmentId,
content: c.content,
chunkIndex: c.chunkIndex,
pageNumber: c.pageNumber,
startChar: c.startChar,
endChar: c.endChar,
metadata: c.metadata,
},
})
)
)
const BATCH_SIZE = 20
for (let i = 0; i < created.length; i += BATCH_SIZE) {
const batch = created.slice(i, i + BATCH_SIZE)
const texts = batch.map(c => c.content)
const embeddings = await embeddingService.generateBatchEmbeddings(texts)
await Promise.all(
batch.map((chunk, idx) =>
prisma.$executeRawUnsafe(
`UPDATE "DocumentChunk" SET embedding = $1::vector WHERE id = $2`,
embeddingService.toVectorString(embeddings[idx].embedding),
chunk.id
)
)
)
}
await prisma.noteAttachment.update({
where: { id: attachmentId },
data: { status: 'ready' },
})
} catch (error: any) {
await prisma.noteAttachment.update({
where: { id: attachmentId },
data: { status: 'failed', error: error.message?.substring(0, 500) },
})
throw error
}
}
}
export const documentIngestionService = new DocumentIngestionService()

View File

@@ -1,7 +1,7 @@
/**
* Embedding Service
* Generates vector embeddings for semantic search and similarity analysis.
* Stores embeddings as native pgvector(1536) in PostgreSQL.
* Stores embeddings as native pgvector in PostgreSQL.
*/
import { getAIProvider } from '../factory'

View File

@@ -385,6 +385,85 @@ export class SemanticSearchService {
await Promise.allSettled(batch.map(noteId => this.indexNote(noteId)))
}
}
async searchWithDocuments(
userId: string,
query: string,
options?: SearchOptions & { noteId?: string; includeDocuments?: boolean }
): Promise<(SearchResult & { source?: 'note' | 'document'; pageNumber?: number; fileName?: string })[]> {
const includeDocuments = options?.includeDocuments !== false
const noteResults = await this.searchAsUser(userId, query, options)
if (!includeDocuments) return noteResults
const queryEmbedding = await embeddingService.generateEmbedding(query)
const vectorStr = embeddingService.toVectorString(queryEmbedding.embedding)
let noteFilter = ''
const params: any[] = [vectorStr, 50, userId]
if (options?.noteId) {
assertSafeId(options.noteId, 'noteId')
params.push(options.noteId)
noteFilter = `AND na."noteId" = $${params.length}`
} else if (options?.notebookId) {
assertSafeId(options.notebookId, 'notebookId')
params.push(options.notebookId)
noteFilter = `AND n."notebookId" = $${params.length}`
}
const documentResults = await prisma.$queryRawUnsafe(
`SELECT
dc.content,
dc."pageNumber",
na."fileName",
na."noteId",
n.title as "noteTitle"
FROM "DocumentChunk" dc
JOIN "NoteAttachment" na ON na.id = dc."attachmentId"
JOIN "Note" n ON n.id = na."noteId"
WHERE dc."embedding" IS NOT NULL
AND na.status = 'ready'
AND n."trashedAt" IS NULL
AND n."userId" = $3
${noteFilter}
ORDER BY dc."embedding" <=> $1::vector
LIMIT $2`,
...params
) as any[]
const K = 60
const fused = new Map<string, any>()
for (let i = 0; i < noteResults.length; i++) {
const r = noteResults[i]
fused.set(r.noteId, {
...r,
source: 'note',
rrfScore: 1 / (K + i + 1),
})
}
for (let i = 0; i < documentResults.length; i++) {
const r = documentResults[i]
const key = `doc_${r.noteId}_${r.pageNumber}_${i}`
fused.set(key, {
noteId: r.noteId,
title: `${r.noteTitle || 'Untitled'}${r.fileName} (p.${r.pageNumber})`,
content: r.content.substring(0, 500),
score: 0.5,
matchType: 'related' as const,
source: 'document',
pageNumber: r.pageNumber,
fileName: r.fileName,
rrfScore: 1 / (K + i + 1),
})
}
return Array.from(fused.values())
.sort((a, b) => b.rrfScore - a.rrfScore)
.slice(0, options?.limit || 20)
}
}
export const semanticSearchService = new SemanticSearchService()

View File

@@ -0,0 +1,73 @@
import { tool } from 'ai'
import { z } from 'zod'
import { toolRegistry } from './registry'
import { embeddingService } from '@/lib/ai/services/embedding.service'
import prisma from '@/lib/prisma'
toolRegistry.register({
name: 'document_search',
description: 'Search within PDF documents attached to notes. Returns relevant passages with page numbers and source document info.',
isInternal: true,
buildTool: (ctx) =>
tool({
description: `Search within PDF documents attached to the user's notes.
Returns matching passages with page numbers, chunk content, and the source note/document info.
Use this when the user asks about specific documents, PDFs, or attached files.`,
inputSchema: z.object({
query: z.string().describe('The search query to find relevant passages in documents'),
noteId: z.string().optional().describe('Optional: restrict search to attachments of a specific note'),
limit: z.number().optional().describe('Max results to return (default 5)').default(5),
}),
execute: async ({ query, noteId, limit = 5 }) => {
try {
const queryEmbedding = await embeddingService.generateEmbedding(query)
const vectorStr = embeddingService.toVectorString(queryEmbedding.embedding)
let noteFilter = ''
const params: any[] = [vectorStr, limit, ctx.userId]
if (noteId) {
noteFilter = `AND na."noteId" = $4`
params.push(noteId)
}
const results = await prisma.$queryRawUnsafe(
`SELECT
dc.id as "chunkId",
dc.content,
dc."pageNumber",
dc."chunkIndex",
na.id as "attachmentId",
na."fileName",
na."pageCount",
na."noteId",
n.title as "noteTitle"
FROM "DocumentChunk" dc
JOIN "NoteAttachment" na ON na.id = dc."attachmentId"
JOIN "Note" n ON n.id = na."noteId"
WHERE dc."embedding" IS NOT NULL
AND na.status = 'ready'
AND n."trashedAt" IS NULL
AND n."userId" = $3
${noteFilter}
ORDER BY dc."embedding" <=> $1::vector
LIMIT $2`,
...params
) as any[]
if (!results.length) return { results: [], message: 'No matching documents found' }
return results.map(r => ({
content: r.content.substring(0, 600),
pageNumber: r.pageNumber,
chunkIndex: r.chunkIndex,
fileName: r.fileName,
noteId: r.noteId,
noteTitle: r.noteTitle || 'Untitled',
}))
} catch (e: any) {
return { error: `Document search failed: ${e.message}` }
}
},
}),
})

View File

@@ -13,6 +13,8 @@ import './memory.tool'
import './excalidraw.tool'
import './pptx.tool'
import './slides.tool'
import './document-search.tool'
import './task-extract.tool'
// Re-export registry
export { toolRegistry, type ToolContext, type RegisteredTool } from './registry'

View File

@@ -60,7 +60,7 @@ const PALETTES: Record<string, Theme> = {
coastal_coral: { primary: '005f73', secondary: '0a9396', accent: 'ee9b00', light: 'e9f5f5', bg: 'ffffff' },
vibrant_orange_mint: { primary: 'e05c00', secondary: '2ec4b6', accent: 'ff9f1c', light: 'edfaf9', bg: 'ffffff' },
platinum_white_gold: { primary: '0a0a0a', secondary: '404040', accent: 'c9a84c', light: 'f5f5f0', bg: 'ffffff' },
architectural_mono: { primary: '1C1C1C', secondary: '75B2D6', accent: 'D4A373', light: 'EDE9DF', bg: 'F2F0E9' },
architectural_mono: { primary: '1C1C1C', secondary: 'A47148', accent: 'D4A373', light: 'EDE9DF', bg: 'F2F0E9' },
minimal_silk: { primary: '212529', secondary: '6c757d', accent: 'dee2e6', light: 'f8f9fa', bg: 'ffffff' },
}

View File

@@ -52,7 +52,7 @@ class ToolRegistry {
* When webOnly is true, only web tools are included (no note access).
*/
buildToolsForChat(ctx: ToolContext & { webOnly?: boolean }): Record<string, any> {
const toolNames: string[] = ctx.webOnly ? [] : ['note_search', 'note_read']
const toolNames: string[] = ctx.webOnly ? [] : ['note_search', 'note_read', 'document_search', 'task_extract']
// Add web tools only when user toggled web search AND config is present
if (ctx.webSearch) {

View File

@@ -0,0 +1,106 @@
import { tool } from 'ai'
import { z } from 'zod'
import { toolRegistry } from './registry'
import { prisma } from '@/lib/prisma'
import { getTagsProvider } from '@/lib/ai/factory'
import { getSystemConfig } from '@/lib/config'
toolRegistry.register({
name: 'task_extract',
description: 'Extract action items (TODOs) from notes in a notebook. Reads all notes, identifies tasks with assignees and deadlines, and creates a synthesis note.',
isInternal: true,
buildTool: (ctx) =>
tool({
description: 'Extract action items from notes in a notebook. Creates a new note with all identified tasks.',
inputSchema: z.object({
notebookId: z.string().optional().describe('Notebook ID to scan. If omitted, scans all user notes.'),
noteIds: z.array(z.string()).optional().describe('Specific note IDs to scan instead of a whole notebook.'),
locale: z.string().optional().describe('Language for the output (fr, en, es, de, etc.)'),
}),
execute: async ({ notebookId, noteIds, locale }) => {
try {
let where: any = { userId: ctx.userId, trashedAt: null }
if (noteIds && noteIds.length > 0) {
where.id = { in: noteIds }
} else if (notebookId) {
where.notebookId = notebookId
}
const notes = await prisma.note.findMany({
where,
select: { id: true, title: true, content: true },
orderBy: { updatedAt: 'desc' },
take: 50,
})
if (notes.length === 0) {
return { error: 'No notes found to analyze' }
}
const notesContext = notes.map(n =>
`[ID: ${n.id}] "${n.title}":\n${(n.content || '').slice(0, 800)}`
).join('\n\n---\n\n')
const lang = locale === 'fr' ? 'français' : locale === 'es' ? 'espagnol' : locale === 'de' ? 'allemand' : locale === 'it' ? 'italien' : locale === 'pt' ? 'portugais' : locale === 'nl' ? 'néerlandais' : locale === 'ru' ? 'russe' : locale === 'zh' ? 'chinois' : locale === 'ja' ? 'japonais' : locale === 'ar' ? 'arabe' : locale === 'fa' ? 'persan' : locale === 'hi' ? 'hindi' : 'English'
const config = await getSystemConfig()
const provider = getTagsProvider(config)
const prompt = `You are a task extraction specialist. Analyze the following notes and extract ALL action items, tasks, and TODOs.
For each task identified, provide:
- **Task**: Clear, actionable description
- **Source**: The note title where it was found
- **Assignee**: If mentioned (otherwise "Unassigned")
- **Deadline**: If mentioned (otherwise "No deadline")
- **Priority**: High/Medium/Low based on urgency signals in the text
- **Status**: If already completed or in-progress based on context
NOTES TO ANALYZE:
${notesContext}
Respond in ${lang}. Structure the output as a clean Markdown document with:
1. A summary paragraph
2. Tasks grouped by priority (High → Medium → Low)
3. A summary table at the end
Format each task as:
### [Priority] Task Title
- **Description**: ...
- **Source note**: ...
- **Assignee**: ...
- **Deadline**: ...
- **Status**: ...`
const result = await provider.generateText(prompt)
const summaryTitle = locale === 'fr'
? `Action Items — ${new Date().toLocaleDateString('fr-FR')}`
: `Action Items — ${new Date().toLocaleDateString('en-US')}`
const createdNote = await prisma.note.create({
data: {
title: summaryTitle,
content: result,
type: 'markdown',
isMarkdown: true,
autoGenerated: true,
userId: ctx.userId,
notebookId: notebookId || null,
},
select: { id: true, title: true },
})
return {
success: true,
noteId: createdNote.id,
title: createdNote.title,
notesAnalyzed: notes.length,
tasksNoteUrl: `/notes/${createdNote.id}`,
}
} catch (e: any) {
return { error: `Task extraction failed: ${e.message}` }
}
},
}),
})

View File

@@ -0,0 +1,130 @@
import prisma from '@/lib/prisma'
export async function verifyParticipant(
sessionId: string,
userId: string,
requiredRole?: 'host' | 'editor' | 'viewer'
): Promise<{ isParticipant: boolean; role: string }> {
const participant = await prisma.brainstormParticipant.findFirst({
where: { sessionId, userId },
})
if (!participant) {
return { isParticipant: false, role: 'none' }
}
await prisma.brainstormParticipant.update({
where: { id: participant.id },
data: { lastSeenAt: new Date() },
})
if (requiredRole === 'host' && participant.role !== 'host') {
return { isParticipant: false, role: participant.role }
}
if (requiredRole === 'editor' && participant.role === 'viewer') {
return { isParticipant: false, role: participant.role }
}
return { isParticipant: true, role: participant.role }
}
export async function logActivity(
sessionId: string,
action: string,
userId?: string | null,
details?: Record<string, any>
) {
await prisma.brainstormActivity.create({
data: {
sessionId,
userId: userId || null,
action,
details: details ? JSON.stringify(details) : null,
},
})
}
// [UPDATE - SÉCURITÉ] Résoudre l'userId et le périmètre de notes autorisé pour les appels IA.
// - Hôte : accès complet à ses notes (publicNoteIds = null)
// - Invité : restreint aux contextNoteIds publics de la session (publicNoteIds = string[] | [])
export async function resolveAiContextUserId(
sessionId: string,
requestingUserId: string
): Promise<{ aiUserId: string; isGuest: boolean; publicNoteIds: string[] | null }> {
const session = await prisma.brainstormSession.findUnique({
where: { id: sessionId },
select: {
userId: true,
contextNoteIds: true,
},
})
if (!session) throw new Error('Session not found')
const isHost = session.userId === requestingUserId
if (isHost) {
return { aiUserId: requestingUserId, isGuest: false, publicNoteIds: null }
}
// Invité : on restreint aux contextNoteIds déclarés publics par l'hôte
const publicNoteIds: string[] = session.contextNoteIds
? (JSON.parse(session.contextNoteIds) as string[])
: []
return {
aiUserId: session.userId,
isGuest: true,
publicNoteIds: publicNoteIds.length > 0 ? publicNoteIds : [],
}
}
// [UPDATE - SÉCURITÉ] Sanitize les notes injectées dans un prompt IA pour un invité.
// Tronque le contenu et masque les entités nommées (Prénom Nom) avec [Person].
export function sanitizeNotesForGuest(
notes: { id: string; title: string | null; summary: string }[]
): { id: string; title: string; summary: string }[] {
const namedEntityRe = /\b[A-ZÀ-Ü][a-zà-ü]+ [A-ZÀ-Ü][a-zà-ü]+\b/g
return notes.map(n => ({
id: n.id,
title: (n.title || 'Note').replace(namedEntityRe, '[Person]'),
summary: n.summary.slice(0, 80).replace(namedEntityRe, '[Person]') + '…',
}))
}
export async function captureSnapshot(
sessionId: string,
label: string,
activityId?: string
): Promise<void> {
const ideas = await prisma.brainstormIdea.findMany({
where: { sessionId, status: 'active' },
select: {
id: true,
title: true,
waveNumber: true,
positionX: true,
positionY: true,
parentIdeaId: true,
noveltyScore: true,
createdByType: true,
status: true,
},
orderBy: [{ waveNumber: 'asc' }, { createdAt: 'asc' }],
})
const maxStep = await prisma.brainstormSnapshot.findFirst({
where: { sessionId },
orderBy: { step: 'desc' },
select: { step: true },
})
await prisma.brainstormSnapshot.create({
data: {
sessionId,
activityId: activityId || null,
step: (maxStep?.step || 0) + 1,
label,
ideaGraph: JSON.stringify(ideas),
},
})
}

View File

@@ -18,6 +18,11 @@ export const queryKeys = {
aiSettings: (userId: string) => ['ai', 'settings', userId] as const,
titleSuggestions: (content: string) => ['ai', 'title-suggestions', content] as const,
autoTags: (content: string, notebookId?: string | null) => ['ai', 'auto-tags', content, notebookId] as const,
// Brainstorm
brainstormSessions: () => ['brainstorm', 'sessions'] as const,
brainstormSharedSessions: () => ['brainstorm', 'shared-sessions'] as const,
brainstormSession: (sessionId: string) => ['brainstorm', 'session', sessionId] as const,
} as const
export type QueryKeys = typeof queryKeys

View File

@@ -0,0 +1,27 @@
// [UPDATE - TEMPS RÉEL] Helper pour émettre des événements Socket.io depuis les API routes Next.js.
// Utilise un canal HTTP interne vers le process socket-server.ts séparé.
// Non-fatal : en cas d'échec, le client récupérera l'état via React Query polling.
export async function emitToSession(
sessionId: string,
event: string,
data: unknown
): Promise<void> {
const socketUrl = process.env.SOCKET_INTERNAL_URL || 'http://localhost:3003'
const internalKey = process.env.SOCKET_INTERNAL_KEY || ''
try {
await fetch(`${socketUrl}/emit`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-internal-key': internalKey,
},
body: JSON.stringify({ sessionId, event, data }),
signal: AbortSignal.timeout(2000), // 2s max — ne pas bloquer l'API
})
} catch {
// Non-fatal — le canal Socket est best-effort
// Le client se resynchronise via invalidation React Query
}
}

View File

@@ -0,0 +1,74 @@
import { format, type Locale } from 'date-fns'
import { toJalaali } from 'jalaali-js'
const JALALI_MONTHS_FA = [
'فروردین',
'اردیبهشت',
'خرداد',
'تیر',
'مرداد',
'شهریور',
'مهر',
'آبان',
'آذر',
'دی',
'بهمن',
'اسفند',
] as const
const PERSIAN_DIGITS = ['۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹'] as const
/** Western digits → Persian (Extended Arabic-Indic) numerals, e.g. 1405 → ۱۴۰۵ */
export function toPersianDigits(input: string): string {
return input.replace(/\d/g, (ch) => PERSIAN_DIGITS[Number(ch)] ?? ch)
}
function pad2(n: number): string {
return n < 10 ? `0${n}` : `${n}`
}
function formatJalaliAbsolute(date: Date, pattern: string): string {
const { jy, jm, jd } = toJalaali(date)
const monthName = JALALI_MONTHS_FA[jm - 1]
const h = pad2(date.getHours())
const min = pad2(date.getMinutes())
let s: string
switch (pattern) {
case 'd MMM yyyy':
s = `${jd} ${monthName} ${jy}`
break
case 'd MMM yyyy · HH:mm':
s = `${jd} ${monthName} ${jy} · ${h}:${min}`
break
case 'd MMM yyyy HH:mm':
s = `${jd} ${monthName} ${jy} ${h}:${min}`
break
case 'd MMM · HH:mm':
s = `${jd} ${monthName} · ${h}:${min}`
break
case 'MMM d, yyyy':
s = `${monthName} ${jd}، ${jy}`
break
default:
s = `${jd} ${monthName} ${jy}`
}
return toPersianDigits(s)
}
/**
* Absolute calendar dates for Persian (`fa`) use the Solar Hijri (Jalali / هجری شمسی) calendar.
* Times remain in the user's local timezone. For relative phrases, keep using `formatDistanceToNow` with `faIR`.
*/
export function formatAbsoluteDateLocalized(
date: Date | string,
language: string,
pattern: string,
locale: Locale
): string {
const d = typeof date === 'string' ? new Date(date) : date
if (language === 'fa') {
return formatJalaliAbsolute(d, pattern)
}
return format(d, pattern, { locale })
}

View File

@@ -32,6 +32,8 @@
},
"sidebar": {
"notes": "الملاحظات",
"recent": "مؤخرًا",
"quickNav": "التنقل السريع",
"reminders": "التذكيرات",
"labels": "التسميات",
"editLabels": "تعديل التسميات",
@@ -40,15 +42,35 @@
"noLabelsInNotebook": "لا توجد تسميات في هذا الدفتر",
"archive": "الأرشيف",
"trash": "المهملات",
"clearFilter": "إزالة الفلتر"
"clearFilter": "إزالة الفلتر",
"inbox": "البريد الوارد",
"sharedWithMe": "مشترك معي",
"sortNewest": "الأحدث أولاً",
"sortOldest": "الأقدم أولاً",
"sortAlpha": "أ → ز",
"accountMenu": "قائمة الحساب",
"profile": "حساب تعريفي",
"signOut": "تسجيل الخروج",
"sortOrder": "ترتيب الترتيب",
"freezePinnedNotebook": "تثبيت ترتيب الشريط الجانبي للكمبيوتر الدفتري",
"unfreezePinnedNotebook": "قم بإزالة تثبيت ترتيب الشريط الجانبي للكمبيوتر الدفتري",
"newSubNotebook": "دفتر فرعي جديد",
"renameNotebook": "إعادة تسمية"
},
"notes": {
"title": "الملاحظات",
"newNote": "ملاحظة جديدة",
"reorganize": "إعادة تنظيم الملاحظات",
"untitled": "بدون عنوان",
"placeholder": "اكتب ملاحظة...",
"markdownPlaceholder": "اكتب ملاحظة... (Markdown مدعوم)",
"titlePlaceholder": "العنوان",
"noteTypes": {
"richtext": "نص غني",
"markdown": "تخفيض السعر",
"text": "نص عادي",
"checklist": "قائمة التحقق"
},
"listItem": "عنصر قائمة",
"addListItem": "+ عنصر قائمة",
"newChecklist": "قائمة تحقق جديدة",
@@ -58,6 +80,7 @@
"confirmDelete": "هل أنت متأكد أنك تريد حذف هذه الملاحظة؟",
"confirmLeaveShare": "هل أنت متأكد أنك تريد مغادرة هذه الملاحظة المشتركة؟",
"sharedBy": "شاركها",
"sharedShort": "مشترك",
"leaveShare": "مغادرة",
"delete": "حذف",
"archive": "أرشفة",
@@ -136,6 +159,8 @@
"dragToReorder": "اسحب لإعادة الترتيب",
"more": "المزيد",
"emptyState": "لا توجد ملاحظات",
"metadataPanel": "تفاصيل",
"metadataNotebook": "دفتر الملاحظات",
"emptyStateTabs": "لا توجد ملاحظات في هذا العرض. استخدم \"ملاحظة جديدة\" في الشريط الجانبي (اقتراحات عناوين بالذكاء الاصطناعي متاحة).",
"inNotebook": "في الدفتر",
"moveFailed": "فشل النقل",
@@ -147,11 +172,6 @@
"unpinned": "غير مثبت",
"redoShortcut": "إعادة (Ctrl+Y)",
"undoShortcut": "تراجع (Ctrl+Z)",
"viewCards": "عرض البطاقات",
"viewCardsTooltip": "شبكة بطاقات مع إعادة ترتيب بالسحب والإفلات",
"viewTabs": "عرض القائمة",
"viewTabsTooltip": "علامات تبويب أعلى، الملاحظة أسفل — اسحب للترتيب",
"viewModeGroup": "وضع عرض الملاحظات",
"reorderTabs": "إعادة ترتيب علامة التبويب",
"modified": "معدلة",
"created": "منشأة",
@@ -160,15 +180,18 @@
"savedStatus": "تم الحفظ",
"dirtyStatus": "معدّل",
"completedLabel": "مكتمل",
"notes.emptyNotebook": "دفتر فارغ",
"notes.emptyNotebookDesc": "لا توجد ملاحظات. انقر على + لإنشاء واحدة.",
"notes.noNoteSelected": "لم يتم تحديد ملاحظة",
"notes.selectOrCreateNote": "اختر ملاحظة من القائمة أو أنشئ واحدة جديدة.",
"notes": {
"emptyNotebook": "دفتر فارغ",
"emptyNotebookDesc": "لا توجد ملاحظات. انقر على + لإنشاء واحدة.",
"noNoteSelected": "لم يتم تحديد ملاحظة",
"selectOrCreateNote": "اختر ملاحظة من القائمة أو أنشئ واحدة جديدة."
},
"commitVersion": "حفظ النسخة",
"versionSaved": "تم حفظ النسخة",
"deleteVersion": "حذف هذه النسخة",
"versionDeleted": "تم حذف النسخة",
"deleteVersionConfirm": "حذف هذه النسخة نهائياً؟",
"deleteVersionDesc": "لا يمكن التراجع عن هذا الإجراء. سيتم حذف النسخة نهائياً من السجل.",
"historyMode": "وضع السجل",
"historyModeManual": "يدوي (زر الالتزام)",
"historyModeAuto": "تلقائي (ذكي)",
@@ -184,6 +207,10 @@
"enableHistory": "تفعيل السجل",
"historyEmpty": "لا توجد نسخ متاحة",
"historySelectVersion": "اختر نسخة لمعاينة محتواها",
"currentVersion": "الحالي",
"compareVersions": "مقارنة",
"diffTitle": "المقارنة",
"diffSelectHint": "انقر على نسختين في القائمة للمقارنة بينهما",
"sortBy": "ترتيب حسب",
"sortDateDesc": "التاريخ (الأحدث)",
"sortDateAsc": "التاريخ (الأقدم)",
@@ -197,10 +224,14 @@
"createFailed": "فشل إنشاء الملاحظة",
"updateFailed": "فشل تحديث الملاحظة",
"archived": "تم أرشفة الملاحظة",
"unarchivedSuccess": "تمت إزالة الملاحظة من الأرشيف",
"archiveFailed": "فشل الأرشفة",
"sort": "ترتيب",
"confirmDeleteTitle": "حذف الملاحظة",
"leftShare": "تمت إزالة المشاركة",
"ideaOrigin": "Origin of the idea",
"noNoteLink": "Purely generative idea",
"dismiss": "Not pertinent",
"dismissed": "تمت إزالة الملاحظة من الحديثة",
"generalNotes": "الملاحظات العامة",
"noteType": "نوع الملاحظة",
@@ -215,11 +246,22 @@
"switchTypeWarning": "قد يفقد بعض التنسيق عند التحويل إلى {type}.",
"switchTypeContentPreserved": "سيتم الحفاظ على المحتوى كنص عادي.",
"switchType": "تحويل إلى {type}",
"deleteVersionDesc": "لا يمكن التراجع عن هذا الإجراء. سيتم حذف النسخة نهائياً من السجل.",
"compareVersions": "مقارنة",
"currentVersion": "الحالي",
"diffSelectHint": "انقر على نسختين في القائمة للمقارنة بينهما",
"diffTitle": "المقارنة"
"saveNow": "احفظ الآن",
"backToCollection": "العودة إلى المجموعة",
"markdownEditingTitle": "العودة إلى التحرير",
"markdownPreviewTitle": "معاينة",
"brainstormThisIdea": "طرح هذه الفكرة",
"brainstormThisIdeaAria": "طرح هذه الفكرة",
"shareNoteTitle": "مشاركة الملاحظة",
"shareNoteAria": "مشاركة الملاحظة",
"saveNoteAria": "حفظ الملاحظة",
"noChangesToSaveAria": "لا توجد تغييرات للحفظ",
"optionsMenuAria": "قائمة الخيارات",
"deleteNoteConfirmItem": "حذف الملاحظة",
"noteDeletedToast": "تم حذف الملاحظة.",
"deleteNoteFailedToast": "لا يمكن الحذف.",
"documentInfoAria": "معلومات الوثيقة",
"noModification": "لا تغييرات"
},
"pagination": {
"previous": "←",
@@ -301,7 +343,24 @@
"accessRevoked": "تم إلغاء صلاحية الوصول",
"errorLoading": "خطأ في تحميل المتعاونين",
"failedToAdd": "فشل في إضافة المتعاون",
"failedToRemove": "فشل في إزالة المتعاون"
"failedToRemove": "فشل في إزالة المتعاون",
"shareCompactTitle": "يشارك",
"inviteByEmailLabel": "دعوة عن طريق البريد الإلكتروني",
"accessReadCompact": "منظر",
"accessEditCompact": "يحرر",
"sendInvitation": "أرسل الدعوة",
"invitationSentBadge": "تم إرسال الدعوة",
"sharedAccessLabel": "الوصول المشترك",
"noCollaboratorsEmpty": "لا يوجد متعاونين حتى الآن.",
"removeAccessTitle": "إزالة الوصول",
"toastInviteSentTo": "تم إرسال الدعوة إلى {email}",
"toastAccessRemoved": "تمت إزالة الوصول إلى {target}",
"toastUserFallback": "المستخدم",
"toastSharingError": "خطأ في المشاركة",
"toastEmailNotFound": "لم يتم العثور على حساب مع هذا البريد الإلكتروني.",
"toastAlreadySharedUser": "تمت مشاركة هذه الملاحظة بالفعل مع هذا المستخدم.",
"toastRemoveAccessFailed": "تعذرت إزالة الوصول.",
"userFallback": "مستخدم"
},
"ai": {
"analyzing": "الذكاء الاصطناعي يحلل...",
@@ -331,6 +390,8 @@
"transforming": "جاري التحويل...",
"transformSuccess": "تم تحويل النص إلى Markdown بنجاح!",
"transformError": "خطأ أثناء التحويل",
"convertToRichtext": "تحويل إلى نص منسق",
"convertingToRichtext": "جارٍ التحويل...",
"assistant": "مساعد الذكاء الاصطناعي",
"generating": "جاري الإنشاء...",
"generateTitles": "إنشاء عناوين",
@@ -394,6 +455,8 @@
"undoAI": "تراجع عن تحويل الذكاء الاصطناعي",
"undoApplied": "تم استعادة النص الأصلي",
"minWordsError": "يجب أن تحتوي الملاحظة على 5 كلمات على الأقل لاستخدام إجراءات الذكاء الاصطناعي.",
"wordCountMin": "الرجاء تحديد {min} كلمات على الأقل لإعادة الصياغة (حالياً {current} كلمة)",
"wordCountMax": "الرجاء تحديد {max} كلمة كحد أقصى لإعادة الصياغة (حالياً {current} كلمة)",
"genericError": "خطأ في الذكاء الاصطناعي",
"actionError": "خطأ أثناء تنفيذ إجراء الذكاء الاصطناعي",
"appliedToNote": "تم التطبيق في الملاحظة",
@@ -409,6 +472,15 @@
"chatTab": "دردشة",
"noteActions": "إجراءات الملاحظة",
"askToStart": "اطرح سؤالاً على المساعد للبدء.",
"chatPanelContext": "سياق",
"chatPanelNotebookPlus": "+ دفتر",
"chatPanelWritingTone": "نغمة الكتابة",
"scopeAutoBadge": "آلي",
"chatNoteQuestionPlaceholder": "اطرح سؤالاً حول هذه المذكرة...",
"chatNotebookSelectPlaceholder": "تضمين دفتر...",
"assistantTabActions": "الإجراءات",
"resourcePreviewAiTitle": "معاينة الذكاء الاصطناعي",
"resourcePreviewInjectFromChat": "أدخل من الدردشة",
"contextLabel": "السياق",
"thisNote": "هذه الملاحظة",
"allMyNotes": "جميع ملاحظاتي",
@@ -420,6 +492,7 @@
"newLineHint": "Shift+Enter = سطر جديد",
"resultLabel": "النتيجة",
"discardAction": "تجاهل",
"organization": "منظمة",
"transformationsDesc": "التحويلات — مطبقة مباشرة في الملاحظة",
"writeMinWordsAction": "اكتب 5 كلمات على الأقل لتفعيل إجراءات الذكاء الاصطناعي.",
"processingAction": "جاري المعالجة...",
@@ -433,7 +506,42 @@
"describeImages": "وصف الصور",
"fixGrammar": "تصحيح القواعد",
"translate": "ترجمة",
"explain": "شرح"
"explain": "شرح",
"toRichText": "تحويل إلى نص منسق"
},
"generate": {
"slides": "إنشاء شرائح",
"sectionLabel": "أدوات التوليد",
"theme": "سمة",
"themeArchitecturalMono": "مونو المعمارية",
"themeVibrantTech": "تقنية نابضة بالحياة",
"themeMinimalSilk": "الحد الأدنى من الحرير",
"style": "أسلوب",
"styleProfessional": "احترافي",
"styleCreative": "مبدع",
"styleBrutalist": "وحشي",
"diagram": "توليد الرسم البياني",
"diagramReadyHint": "تحويل الملاحظة إلى تدفق مرئي",
"diagramType": "نوع الرسم البياني",
"typeAuto": "الكشف التلقائي",
"typeFlowchart": "مخطط انسيابي",
"typeMindMap": "خريطة العقل",
"typeTimeline": "الجدول الزمني",
"typeOrgChart": "المخطط التنظيمي",
"typeArchitecture": "بنيان",
"typeProcessMap": "خريطة العملية",
"styleSketchy": "سطحية",
"styleSoft": "ناعم",
"styleMinimal": "الحد الأدنى",
"styleDraft": "مسودة",
"stylePolished": "مصقول",
"styleHandwritten": "مكتوبة بخط اليد",
"diagramReady": "الرسم البياني جاهز!",
"openInExcalidraw": "افتح في مختبر Excalidraw",
"insertDiagramInNote": "تضمين PNG في الملاحظة الحالية",
"diagramImageAlt": "رسم تخطيطي تم إنشاؤه بواسطة الذكاء الاصطناعي",
"insertedInNote": "تم إدراج الرسم البياني في الملاحظة",
"insertExportError": "خطأ في تصدير/تحميل الرسم التخطيطي"
},
"openAssistant": "فتح مساعد الذكاء الاصطناعي",
"poweredByMomento": "مدعوم من Momento AI",
@@ -451,8 +559,6 @@
"suggestTitle": "اقتراح عنوان بالذكاء الاصطناعي",
"generateTitleFromImage": "إنشاء عنوان من الصورة",
"titleGenerated": "تم إنشاء العنوان من الصورة",
"wordCountMin": "الرجاء تحديد {min} كلمات على الأقل لإعادة الصياغة (حالياً {current} كلمة)",
"wordCountMax": "الرجاء تحديد {max} كلمة كحد أقصى لإعادة الصياغة (حالياً {current} كلمة)",
"resourceTab": "المصدر",
"aiNoteTitle": "ملاحظة الذكاء الاصطناعي",
"injectReplace": "استبدال",
@@ -492,7 +598,24 @@
"preview": "معاينة",
"generatePreview": "إنشاء معاينة",
"emptyNoteHint": "💡 الملاحظة فارغة — سيتم دمج محتوى المصدر مباشرة."
}
},
"cancel": "يلغي",
"copied": "منقول",
"copy": "ينسخ",
"transformations": "التحولات",
"otherLanguage": "لغة أخرى",
"translateNow": "ترجم الآن",
"generationTools": "أدوات التوليد",
"generateSlidesLoading": "⏳ جارٍ إنشاء العرض التقديمي...",
"generateDiagramLoading": "⏳ إنشاء المخطط...",
"errorShort": "خطأ",
"readyToast": "مستعد!",
"downloadFailedToast": "فشل التنزيل",
"pptxDownloadButton": "تحميل .pptx",
"presentationReadyBadge": "العرض التقديمي جاهز",
"openInLabTitle": "فتح في المختبر",
"inlineSummaryMarkdown": "**ملخص:**",
"networkErrorShort": "خطأ في الشبكة."
},
"titleSuggestions": {
"available": "اقتراحات العنوان",
@@ -598,7 +721,19 @@
"untitled": "بدون عنوان",
"notifications": "الإشعارات",
"declined": "تم رفض المشاركة",
"removed": "تمت إزالة الملاحظة من القائمة"
"removed": "تمت إزالة الملاحظة من القائمة",
"slidesReady": "العرض التقديمي جاهز",
"openSlides": "فتح العرض التقديمي",
"canvasReady": "الرسم البياني جاهز",
"pptxReady": "الشرائح جاهزة",
"downloadPptx": "تحميل .pptx",
"markAllRead": "وضع علامة على كل قراءة",
"agentSuccess": "انتهى الوكيل",
"agentFailed": "فشل الوكيل",
"brainstormInvite": "العصف الذهني",
"brainstormJoined": "العصف الذهني",
"systemNotification": "نظام",
"downloadFailed": "فشل التنزيل"
},
"nav": {
"home": "الرئيسية",
@@ -647,6 +782,17 @@
"themeLight": "فاتح",
"themeDark": "داكن",
"themeSystem": "النظام",
"themeBaseGroup": "Base",
"themePalettesGroup": "Color palettes",
"themeSepia": "Sepia",
"themeMidnight": "Midnight",
"themeRose": "Rose",
"themeGreen": "Green",
"themeLavender": "Lavender",
"themeSand": "Sand",
"themeOcean": "Ocean",
"themeSunset": "Sunset",
"themeBlue": "Blue",
"notifications": "الإشعارات",
"language": "اللغة",
"selectLanguage": "اختيار اللغة",
@@ -680,17 +826,8 @@
"desktopNotifications": "إشعارات سطح المكتب",
"desktopNotificationsDesc": "تلقي إشعارات في المتصفح",
"notificationsDesc": "إدارة تفضيلات الإشعارات",
"themeBaseGroup": "Base",
"themePalettesGroup": "Color palettes",
"themeSepia": "Sepia",
"themeMidnight": "Midnight",
"themeRose": "Rose",
"themeGreen": "Green",
"themeLavender": "Lavender",
"themeSand": "Sand",
"themeOcean": "Ocean",
"themeSunset": "Sunset",
"themeBlue": "Blue"
"autoSave": "الحفظ التلقائي",
"autoSaveDesc": "حفظ التغييرات تلقائيًا أثناء الكتابة"
},
"profile": {
"title": "الملف الشخصي",
@@ -855,7 +992,11 @@
"confidence": "ثقة",
"savingReminder": "خطأ في حفظ التذكير",
"removingReminder": "خطأ في إزالة التذكير",
"generatingDescription": "يرجى الانتظار..."
"generatingDescription": "يرجى الانتظار...",
"pinnedFrozenTooltip": "دفتر ملاحظات مثبت — تم تجميد الطلب",
"organizeNotebookWithAITooltip": "قم بتنظيم دفتر الملاحظات هذا باستخدام الذكاء الاصطناعي",
"assistantRequiredForSummarize": "قم بتشغيل AI Assistant في الإعدادات للتلخيص",
"createSubnotebook": "إضافة دفتر فرعي"
},
"notebookSuggestion": {
"title": "النقل إلى {name}؟",
@@ -868,6 +1009,9 @@
},
"admin": {
"title": "لوحة تحكم المشرف",
"adminConsole": "وحدة تحكم المشرف",
"navSection": "ملاحة",
"backToApp": "العودة إلى تذكار",
"userManagement": "إدارة المستخدمين",
"chat": "دردشة الذكاء الاصطناعي",
"lab": "المختبر",
@@ -910,6 +1054,11 @@
"providerEmbeddingRequired": "AI_PROVIDER_EMBEDDING مطلوب",
"providerOllamaOption": "🦙 Ollama (Local & Free)",
"providerOpenAIOption": "🤖 OpenAI (GPT-5, GPT-4)",
"providerAnthropicOption": "🧠 أنثروبي (كلود أبي)",
"providerAnthropicCustomOption": "🧩 مخصص إنساني (واجهة برمجة التطبيقات للرسائل — MiniMax، وما إلى ذلك)",
"anthropicModelHint": "اختر معرف نموذج Claude من الاقتراحات أو أدخل واحدًا يدويًا (لا توجد قائمة نماذج عن بعد لواجهة برمجة التطبيقات الرسمية).",
"anthropicCustomModelHint": "واجهة برمجة تطبيقات الرسائل المتوافقة مع البشر (مثل MiniMax): عنوان URL الأساسي https://api.minimax.io/anthropic (الصين: https://api.minimaxi.com/anthropic)، النموذج MiniMax-M2.7. التضمينات: استخدم الموفر «مخصص» + عنوان URL لـ OpenAI https://api.minimax.io/v1.",
"anthropicCustomNoModelList": "لا تعرض هذه البوابة قائمة نماذج/نمط OpenAI — اختر النموذج من الاقتراحات أو اكتبه (على سبيل المثال، MiniMax-M2.7).",
"providerCustomOption": "🔧 Custom OpenAI-Compatible",
"providerDeepSeekOption": "🔍 DeepSeek",
"providerOpenRouterOption": "🌐 OpenRouter",
@@ -1063,7 +1212,14 @@
"error": "خطأ:",
"testError": "خطأ في الاختبار: {error}",
"tipTitle": "نصيحة:",
"tipDescription": "استخدم لوحة اختبار الذكاء الاصطناعي لتشخيص مشاكل التكوين قبل الاختبار."
"tipDescription": "استخدم لوحة اختبار الذكاء الاصطناعي لتشخيص مشاكل التكوين قبل الاختبار.",
"chatTestTitle": "اختبار مساعد الدردشة",
"chatTestDescription": "اختبر موفر الذكاء الاصطناعي الذي يستخدمه مساعد الدردشة",
"chatGenerationTest": "💬 اختبار مساعد الدردشة:",
"chatStep1": "يرسل رسالة اختبار إلى المساعد",
"chatStep2": "يطلب إجابة موجزة حول ما يفعله المساعد",
"chatStep3": "يظهر الاستجابة النموذجية",
"chatStep4": "التحقق من الاستجابة والكمون"
},
"sidebar": {
"dashboard": "لوحة التحكم",
@@ -1254,6 +1410,7 @@
"notesViewLabel": "عرض الملاحظات",
"notesViewTabs": "علامات تبويب (نمط OneNote)",
"notesViewMasonry": "بطاقات (شبكة)",
"notesViewList": "قائمة (مجلة)",
"selectTheme": "اختر المظهر",
"fontFamilyLabel": "عائلة الخطوط",
"fontFamilyDescription": "اختر الخط المستخدم في جميع أنحاء التطبيق",
@@ -1337,6 +1494,69 @@
"organizeWithAI": "تنظيم بالذكاء الاصطناعي",
"organize": "تنظيم"
},
"organizeNotebook": {
"title": "تنظيم دفتر الملاحظات",
"unknownError": "خطأ غير معروف",
"toastSuccess": "تم تنظيم دفتر الملاحظات - تم إنشاء {تم إنشاء} دفاتر ملاحظات فرعية، وتم نقل {نقل} الملاحظات",
"intro": "سيقوم الذكاء الاصطناعي بتحليل الملاحظات الموجودة في هذا الدفتر واقتراح خطة لإعادة تنظيمها في دفاتر ملاحظات فرعية موضوعية.",
"bulletThemes": "تجميع الملاحظات حسب الموضوع أو الموضوع",
"bulletSubfolders": "إنشاء دفاتر الملاحظات الفرعية المفقودة",
"bulletPreview": "معاينة كاملة قبل أي تغيير",
"analyzingTitle": "جارٍ التحليل…",
"analyzingSubtitle": "يقوم الذكاء الاصطناعي بقراءة ملاحظاتك وتحديد المواضيع",
"previewSummary": "{groups} مجموعة (مجموعات) · {notes} ملاحظات · {newSubs} دفاتر ملاحظات فرعية جديدة",
"badgeNew": "جديد",
"untitledNote": "مذكرة بلا عنوان",
"notesInGroup": "{عدد} ملاحظات",
"executingTitle": "تنظيم…",
"executingSubtitle": "إنشاء دفاتر ملاحظات فرعية وملاحظات متحركة",
"doneTitle": "دفتر الملاحظات منظم!",
"doneStats": "تم إنشاء {تم إنشاء} دفاتر ملاحظات فرعية · {تم نقل} تم نقل الملاحظات",
"analyzeButton": "التحليل باستخدام الذكاء الاصطناعي",
"restart": "ابدأ من جديد",
"confirm": "يتقدم",
"closeButton": "يغلق"
},
"documentInfo": {
"tabInfo": "معلومات",
"tabVersions": "الإصدارات",
"wordsLabel": "كلمات",
"charactersLabel": "الشخصيات",
"notebookLabel": "دفتر الملاحظات",
"typeLabel": "يكتب",
"createdLabel": "مخلوق",
"modifiedLabel": "تم التحديث",
"labelsSection": "التسميات",
"idLabel": "بطاقة تعريف",
"historyDisabled": "لم يتم تمكين السجل لهذه الملاحظة.",
"enableHistory": "تمكين التاريخ",
"savedVersions": "الإصدارات المحفوظة",
"savingEllipsis": "توفير…",
"versionSaved": "تم حفظ الإصدار!",
"saveThisVersion": "احفظ هذا الإصدار",
"loading": "تحميل…",
"noVersion": "لا توجد إصدارات حتى الآن",
"restoreTooltip": "يعيد",
"deleteTooltip": "يمسح",
"comparisonMode": "وضع المقارنة",
"comparisonSubtitle": "مقارنة الإصدارات جنبا إلى جنب",
"deleteVersionConfirm": "هل تريد حذف هذا الإصدار؟",
"latestBadge": "أحدث"
},
"languages": {
"targets": {
"french": "فرنسي",
"english": "إنجليزي",
"spanish": "الأسبانية",
"german": "الألمانية",
"persian": "الفارسية",
"portuguese": "البرتغالية",
"italian": "ايطالي",
"chinese": "الصينية",
"japanese": "اليابانية"
},
"customPlaceholder": "على سبيل المثال العربية والروسية..."
},
"common": {
"unknown": "غير معروف",
"notAvailable": "غير متاح",
@@ -1458,12 +1678,16 @@
"scraper": "مراقب",
"researcher": "باحث",
"monitor": "مراقب",
"slideGenerator": "الشرائح",
"excalidrawGenerator": "رسم بياني",
"custom": "مخصص"
},
"typeDescriptions": {
"scraper": "يجمع البيانات من عدة مواقع وينشئ ملخصًا",
"researcher": "يبحث عن معلومات حول موضوع معين",
"monitor": "يراقب دفتر ملاحظات ويحلل الملاحظات",
"slideGenerator": "إنشاء عرض تقديمي لـ PowerPoint من الملاحظات",
"excalidrawGenerator": "إنشاء مخطط Excalidraw من الملاحظات",
"custom": "وكيل حر بموجهك الخاص"
},
"form": {
@@ -1476,6 +1700,27 @@
"urlsOptional": "(اختياري)",
"sourceNotebook": "دفتر الملاحظات للمراقبة",
"selectNotebook": "اختر دفتر ملاحظات...",
"selectNotes": "ملاحظات للتحليل",
"notesSelected": "تم تحديد {{count}} ملاحظة (ملاحظات).",
"slideTheme": "موضوع العرض",
"slideThemeDefault": "تلقائي",
"slideStyle": "النمط البصري",
"slideStyleSoft": "ناعم (مستحسن)",
"slideStyleSharp": "حادة وكثيفة",
"slideStyleRounded": "مدورة وواسعة",
"slideStylePill": "قسط / حبة",
"excalidrawDiagramType": "نوع الرسم البياني",
"excalidrawDiagramTypeAuto": "تلقائي (اكتشاف المجال)",
"excalidrawDiagramTypeFlowchart": "مخطط انسيابي (عملية)",
"excalidrawDiagramTypeMindmap": "الخريطة الذهنية (الأفكار)",
"excalidrawDiagramTypeOrgChart": "المخطط التنظيمي (الفرق)",
"excalidrawDiagramTypeTimeline": "الجدول الزمني / خارطة الطريق",
"excalidrawDiagramTypeProcessMap": "خريطة العملية (العمليات)",
"excalidrawDiagramTypeArchitectureCloud": "البنية السحابية (المناطق/RG)",
"excalidrawDiagramStyle": "أسلوب الرسم البياني Excalidraw",
"excalidrawDiagramStyleDefault": "ملون (إكسكاليدراو)",
"excalidrawDiagramStyleSketchPlus": "رسم+ (Excalidraw محسّن)",
"excalidrawDiagramStyleAustere": "تقشف (الحد الأدنى)",
"targetNotebook": "دفتر الملاحظات الهدف",
"inbox": "صندوق الوارد",
"instructions": "تعليمات الذكاء الاصطناعي",
@@ -1545,6 +1790,8 @@
"updated": "تم تحديث الوكيل",
"deleted": "تم حذف \"{name}\"",
"deleteError": "خطأ في الحذف",
"running": "جاري التوليد…",
"runningDesc": "قد يستغرق إنشاء بضع دقائق. يمكنك التنقل بحرية.",
"runSuccess": "تم تنفيذ \"{name}\" بنجاح",
"runError": "خطأ: {error}",
"runFailed": "فشل التنفيذ",
@@ -1579,13 +1826,24 @@
"chercheur": {
"name": "باحث المواضيع",
"description": "يبحث عن معلومات متعمقة حول موضوع وينشئ ملاحظة منظمة بمراجع."
},
"slideGenerator": {
"name": "مولد الشرائح",
"description": "يقرأ الملاحظات من دفتر ملاحظات وينشئ عرضًا تقديميًا منظمًا تلقائيًا."
},
"excalidrawGenerator": {
"name": "مولد الرسم البياني",
"description": "يقرأ ملاحظة وينشئ رسمًا تخطيطيًا مرئيًا في Excalidraw Lab."
}
},
"runLog": {
"title": "السجل",
"noHistory": "لا يوجد سجل تنفيذ بعد",
"toolTrace": "{count} استدعاءات أدوات",
"step": "الخطوة {num}"
"step": "الخطوة {num}",
"clearConfirm": "هل أنت متأكد أنك تريد حذف كل السجل لهذا الوكيل؟",
"cleared": "تم حذف التاريخ",
"clearHistory": "مسح التاريخ"
},
"tools": {
"title": "أدوات الوكيل",
@@ -1596,6 +1854,9 @@
"noteCreate": "إنشاء ملاحظة",
"urlFetch": "جلب رابط",
"memorySearch": "الذاكرة",
"generatePptx": "شرائح PPTX",
"generateSlides": "شرائح HTML",
"generateExcalidraw": "مخطط إكسكاليدراو",
"configNeeded": "إعدادات",
"selected": "{count} محدد",
"maxSteps": "الحد الأقصى للتكرارات"
@@ -1607,7 +1868,9 @@
"scraper": "أنت مساعد مراقبة. قم بتجميع المقالات من مواقع مختلفة في ملخص واضح ومنظم.",
"researcher": "أنت باحث دقيق. للموضوع المطلوب، أنشئ ملاحظة بحثية بالسياق والنقاط الرئيسية والمناقشات والمراجع.",
"monitor": "أنت مساعد تحليلي. حلل الملاحظات المقدمة واقترح اتجاهات ومراجع وروابط بين الملاحظات.",
"custom": "أنت مساعد مفيد."
"custom": "أنت مساعد مفيد.",
"slideGenerator": "أنت منشئ العرض التقديمي. اقرأ المحتوى المقدم وقم بإنشاء شرائح منظمة تحتوي على العناوين والنقاط الرئيسية والملخصات.",
"excalidrawGenerator": "أنت منشئ الرسم البياني. قم بتحليل المحتوى المقدم وإنشاء رسم تخطيطي مرئي واضح ومنظم."
},
"help": {
"title": "دليل الوكلاء",
@@ -1641,7 +1904,10 @@
"frequency": "كم مرة يعمل الوكيل تلقائيًا. ابدأ بيدوي للاختبار.",
"instructions": "تعليمات مخصصة تحل محل موجه الذكاء الاصطناعي الافتراضي. اتركه فارغًا لاستخدام التلقائي.",
"tools": "حدد الأدوات التي يمكن للوكيل استخدامها. كل أداة تمنح الوكيل قدرة محددة.",
"maxSteps": "الحد الأقصى لدورات الاستدلال. خطوات أكثر = تحليل أعمق لكن يستغرق وقتًا أطول."
"maxSteps": "الحد الأقصى لدورات الاستدلال. خطوات أكثر = تحليل أعمق لكن يستغرق وقتًا أطول.",
"selectNotes": "حدد ملاحظات محددة لتحليلها. إذا لم يتم تحديد أي شيء، فسيستخدم الوكيل جميع الملاحظات من دفتر الملاحظات.",
"slideTheme": "اختر لوحة ألوان للعرض التقديمي. تلقائي يتيح للذكاء الاصطناعي اتخاذ القرار.",
"slideStyle": "يؤثر النمط المرئي على نصف قطر الزاوية والتباعد وكثافة المعلومات."
}
}
},
@@ -1725,6 +1991,12 @@
"slashCodeDesc": "مقتطف كود",
"slashDivider": "فاصل",
"slashDividerDesc": "فاصل أفقي",
"slashTable": "طاولة",
"slashTableDesc": "أدخل شبكة بسيطة",
"slashDiagram": "رسم بياني",
"slashDiagramDesc": "إنشاء تدفق أو خريطة ذهنية",
"slashSlides": "عرض تقديمي",
"slashSlidesDesc": "قم بإنشاء مجموعة شرائح جميلة",
"slashImage": "صورة",
"slashImageDesc": "تضمين صورة من رابط",
"slashAlignLeft": "محاذاة لليسار",
@@ -1762,5 +2034,70 @@
"subscript": "نص منخفض",
"addBlock": "إضافة كتلة",
"placeholder": "اكتب '/' للأوامر..."
},
"brainstorm": {
"title": "Waves of Thought",
"subtitle": "Unfold dimensions of potentiality",
"placeholder": "Enter a concept to unfold...",
"generating": "AI is harvesting seeds of thought...",
"newBrainstorm": "New Brainstorm",
"noSessions": "No brainstorms yet",
"startOne": "Start one",
"sessions": "Brainstorms",
"seedLabel": "Seed Idea",
"ideaPromptDetailed": "أدخل فكرتك أو سؤالك أو موضوعك لتبادل الأفكار...",
"brainstormThisIdea": "Brainstorm this idea",
"startBrainstorm": "Start Brainstorm",
"spatialMode": "Spatial Exploration Mode",
"wave1": "Wave 1",
"wave2": "Wave 2",
"wave3": "Wave 3",
"export": "Export",
"exporting": "Exporting...",
"wave": "Wave",
"novelty": "Novelty",
"originConnection": "Origin connection",
"linkedNotes": "Linked notes",
"deepen": "Deepen",
"deepening": "Generating...",
"extract": "Create Note",
"converting": "Converting...",
"dismiss": "Not pertinent",
"noteCreated": "Note Created",
"ideas": "ideas",
"cancel": "Cancel",
"delete": "Delete",
"ideaOrigin": "Origin of the idea",
"noNoteLink": "Purely generative idea",
"derived_from": "Derived from",
"opposes": "In opposition with",
"extends": "Extends",
"synthesizes": "Synthesizes",
"transposes": "Transposes",
"none_found": "No note link",
"viewNote": "View note",
"addIdea": "Add idea",
"manualIdeaPrompt": "Title of your idea:",
"invite": "Invite",
"linkCopied": "Invite link copied!",
"activityTitle": "نشاط",
"noActivity": "لا يوجد نشاط بعد",
"justNow": "الآن",
"humanIdea": "بشر",
"aiIdea": "منظمة العفو الدولية",
"respondsTo": "يستجيب ل",
"adding": "جارٍ الإضافة...",
"manualIdeaDesc": "شارك فكرتك مع لوحة العصف الذهني",
"manualIdeaTitle": "عنوان",
"manualIdeaTitlePlaceholder": "فكرتك في بضع كلمات...",
"manualIdeaDescLabel": "الوصف (اختياري)",
"manualIdeaDescPlaceholder": "وضح فكرتك..",
"activity": {
"manual_idea": "أضافت فكرة",
"wave_generated": "ولدت موجة",
"joined": "انضم إلى الجلسة",
"idea_dismissed": "رفضت فكرة",
"invite_created": "أنشأت دعوة"
}
}
}
}

View File

@@ -32,6 +32,8 @@
},
"sidebar": {
"notes": "Notes",
"recent": "Jüngste",
"quickNav": "Schnelle Navigation",
"reminders": "Reminders",
"labels": "Labels",
"editLabels": "Edit labels",
@@ -40,15 +42,35 @@
"noLabelsInNotebook": "Keine Labels in diesem Notizbuch",
"archive": "Archive",
"trash": "Trash",
"clearFilter": "Remove filter"
"clearFilter": "Remove filter",
"inbox": "Posteingang",
"sharedWithMe": "Mit mir geteilt",
"sortNewest": "Das Neueste zuerst",
"sortOldest": "Älteste zuerst",
"sortAlpha": "A → Z",
"accountMenu": "Kontomenü",
"profile": "Profil",
"signOut": "Abmelden",
"sortOrder": "Sortierreihenfolge",
"freezePinnedNotebook": "Pin-Reihenfolge in der Seitenleiste des Notizbuchs",
"unfreezePinnedNotebook": "Pinnwandreihenfolge in der Notizbuch-Seitenleiste aufheben",
"newSubNotebook": "Neues Subnotebook",
"renameNotebook": "Umbenennen"
},
"notes": {
"title": "Notizen",
"newNote": "Neue Notiz",
"reorganize": "Notizen neu organisieren",
"untitled": "Unbenannt",
"placeholder": "Notiz machen...",
"markdownPlaceholder": "Notiz machen... (Markdown unterstützt)",
"titlePlaceholder": "Titel",
"noteTypes": {
"richtext": "Rich-Text",
"markdown": "Abschlag",
"text": "Klartext",
"checklist": "Checkliste"
},
"listItem": "Listenelement",
"addListItem": "+ Listenelement",
"newChecklist": "Neue Checkliste",
@@ -58,6 +80,7 @@
"confirmDelete": "Möchten Sie diese Notiz wirklich löschen?",
"confirmLeaveShare": "Möchten Sie diese geteilte Notiz wirklich verlassen?",
"sharedBy": "Geteilt von",
"sharedShort": "Geteilt",
"leaveShare": "Verlassen",
"delete": "Löschen",
"archive": "Archivieren",
@@ -136,6 +159,8 @@
"dragToReorder": "Ziehen zum Neuanordnen",
"more": "Mehr",
"emptyState": "Keine Notizen vorhanden",
"metadataPanel": "Einzelheiten",
"metadataNotebook": "Notizbuch",
"emptyStateTabs": "Keine Notizen in dieser Ansicht. Nutzen Sie \"Neue Notiz\" in der Seitenleiste (KI-Titelvorschläge im Composer verfügbar).",
"inNotebook": "In Notizbuch",
"moveFailed": "Verschieben fehlgeschlagen",
@@ -147,11 +172,6 @@
"unpinned": "Nicht angepinnt",
"redoShortcut": "Wiederholen (Strg+Y)",
"undoShortcut": "Rückgängig (Strg+Z)",
"viewCards": "Kartenansicht",
"viewCardsTooltip": "Kartenraster mit Drag-and-Drop-Umsortierung",
"viewTabs": "Listenansicht",
"viewTabsTooltip": "Tabs oben, Notiz unten — Tabs ziehen zum Umsortieren",
"viewModeGroup": "Notizen-Anzeigemodus",
"reorderTabs": "Tab umsortieren",
"modified": "Geändert",
"created": "Erstellt",
@@ -160,15 +180,18 @@
"savedStatus": "Gespeichert",
"dirtyStatus": "Geändert",
"completedLabel": "Erledigt",
"notes.emptyNotebook": "Leeres Notizbuch",
"notes.emptyNotebookDesc": "Keine Notizen vorhanden. Klicke auf + um eine zu erstellen.",
"notes.noNoteSelected": "Keine Notiz ausgewählt",
"notes.selectOrCreateNote": "Wähle eine Notiz aus der Liste oder erstelle eine neue.",
"notes": {
"emptyNotebook": "Leeres Notizbuch",
"emptyNotebookDesc": "Keine Notizen vorhanden. Klicke auf + um eine zu erstellen.",
"noNoteSelected": "Keine Notiz ausgewählt",
"selectOrCreateNote": "Wähle eine Notiz aus der Liste oder erstelle eine neue."
},
"commitVersion": "Version speichern",
"versionSaved": "Version gespeichert",
"deleteVersion": "Diese Version löschen",
"versionDeleted": "Version gelöscht",
"deleteVersionConfirm": "Diese Version endgültig löschen?",
"deleteVersionDesc": "Diese Aktion kann nicht rückgängig gemacht werden. Die Version wird dauerhaft aus dem Verlauf gelöscht.",
"historyMode": "Verlaufsmodus",
"historyModeManual": "Manuell (Commit-Schaltfläche)",
"historyModeAuto": "Automatisch (intelligent)",
@@ -184,6 +207,10 @@
"enableHistory": "Verlauf aktivieren",
"historyEmpty": "Keine Versionen verfügbar",
"historySelectVersion": "Wählen Sie eine Version zur Vorschau aus",
"currentVersion": "aktuell",
"compareVersions": "Vergleichen",
"diffTitle": "Vergleich",
"diffSelectHint": "Klicken Sie auf 2 Versionen in der Liste, um sie zu vergleichen",
"sortBy": "Sortieren nach",
"sortDateDesc": "Datum (neueste)",
"sortDateAsc": "Datum (älteste)",
@@ -197,10 +224,14 @@
"createFailed": "Failed to create note",
"updateFailed": "Failed to update note",
"archived": "Note archived",
"unarchivedSuccess": "Notiz aus dem Archiv entfernt",
"archiveFailed": "Failed to archive",
"sort": "Sort",
"confirmDeleteTitle": "Delete note",
"leftShare": "Share removed",
"ideaOrigin": "Origin of the idea",
"noNoteLink": "Purely generative idea",
"dismiss": "Not pertinent",
"dismissed": "Note dismissed from recent",
"generalNotes": "General Notes",
"noteType": "Notiztyp",
@@ -214,7 +245,23 @@
"switchTypeTitle": "Notiztyp ändern?",
"switchTypeWarning": "Formatierung kann beim Wechsel zu {type} verloren gehen.",
"switchTypeContentPreserved": "Dein Inhalt wird als Klartext erhalten.",
"switchType": "Wechseln zu {type}"
"switchType": "Wechseln zu {type}",
"saveNow": "Jetzt sparen",
"backToCollection": "Zurück zur Sammlung",
"markdownEditingTitle": "Zurück zur Bearbeitung",
"markdownPreviewTitle": "Vorschau",
"brainstormThisIdea": "Machen Sie ein Brainstorming zu dieser Idee",
"brainstormThisIdeaAria": "Machen Sie ein Brainstorming zu dieser Idee",
"shareNoteTitle": "Notiz teilen",
"shareNoteAria": "Notiz teilen",
"saveNoteAria": "Notiz speichern",
"noChangesToSaveAria": "Keine Änderungen zum Speichern",
"optionsMenuAria": "Optionsmenü",
"deleteNoteConfirmItem": "Notiz löschen",
"noteDeletedToast": "Notiz gelöscht.",
"deleteNoteFailedToast": "Konnte nicht gelöscht werden.",
"documentInfoAria": "Dokumentinformationen",
"noModification": "Keine Änderungen"
},
"pagination": {
"previous": "←",
@@ -296,7 +343,24 @@
"accessRevoked": "Der Zugriff wurde widerrufen",
"errorLoading": "Fehler beim Laden der Mitarbeiter",
"failedToAdd": "Fehler beim Hinzufügen des Mitarbeiters",
"failedToRemove": "Fehler beim Entfernen des Mitarbeiters"
"failedToRemove": "Fehler beim Entfernen des Mitarbeiters",
"shareCompactTitle": "Aktie",
"inviteByEmailLabel": "Per E-Mail einladen",
"accessReadCompact": "Sicht",
"accessEditCompact": "Bearbeiten",
"sendInvitation": "Einladung senden",
"invitationSentBadge": "Einladung verschickt",
"sharedAccessLabel": "Gemeinsamer Zugriff",
"noCollaboratorsEmpty": "Noch keine Mitarbeiter.",
"removeAccessTitle": "Zugriff entfernen",
"toastInviteSentTo": "Einladung gesendet an {email}",
"toastAccessRemoved": "Zugriff für {target} entfernt",
"toastUserFallback": "der Benutzer",
"toastSharingError": "Fehler beim Teilen",
"toastEmailNotFound": "Mit dieser E-Mail wurde kein Konto gefunden.",
"toastAlreadySharedUser": "Diese Notiz wurde bereits mit diesem Benutzer geteilt.",
"toastRemoveAccessFailed": "Der Zugriff konnte nicht entfernt werden.",
"userFallback": "Benutzer"
},
"ai": {
"analyzing": "KI analysiert...",
@@ -326,6 +390,8 @@
"transforming": "Wird umgewandelt...",
"transformSuccess": "Text erfolgreich in Markdown umgewandelt!",
"transformError": "Fehler bei der Umwandlung",
"convertToRichtext": "In Rich Text konvertieren",
"convertingToRichtext": "Konvertieren...",
"assistant": "KI-Assistent",
"generating": "Wird generiert...",
"generateTitles": "Titel generieren",
@@ -389,6 +455,8 @@
"undoAI": "KI-Transformation rückgängig machen",
"undoApplied": "Originaltext wiederhergestellt",
"minWordsError": "Die Notiz muss mindestens 5 Wörter enthalten, um KI-Aktionen zu nutzen.",
"wordCountMin": "Bitte wählen Sie mindestens {min} Wörter zur Neuformulierung aus (derzeit {aktuelle} Wörter)",
"wordCountMax": "Bitte wählen Sie höchstens {max} Wörter zum Umformulieren aus (derzeit {aktuelle} Wörter).",
"genericError": "KI-Fehler",
"actionError": "Fehler bei der KI-Aktion",
"appliedToNote": "In Notiz angewendet",
@@ -404,6 +472,15 @@
"chatTab": "Chat",
"noteActions": "Notiz-Aktionen",
"askToStart": "Stellen Sie dem Assistenten eine Frage, um zu beginnen.",
"chatPanelContext": "Kontext",
"chatPanelNotebookPlus": "+ Notizbuch",
"chatPanelWritingTone": "Schreibton",
"scopeAutoBadge": "Auto",
"chatNoteQuestionPlaceholder": "Stellen Sie eine Frage zu dieser Notiz...",
"chatNotebookSelectPlaceholder": "Legen Sie ein Notizbuch bei...",
"assistantTabActions": "Aktionen",
"resourcePreviewAiTitle": "KI-Vorschau",
"resourcePreviewInjectFromChat": "Aus dem Chat injizieren",
"contextLabel": "Kontext",
"thisNote": "Diese Notiz",
"allMyNotes": "Alle meine Notizen",
@@ -415,6 +492,7 @@
"newLineHint": "Shift+Enter = neue Zeile",
"resultLabel": "Ergebnis",
"discardAction": "Verwerfen",
"organization": "Organisation",
"transformationsDesc": "Transformationen — direkt in der Notiz angewendet",
"writeMinWordsAction": "Schreibe mindestens 5 Wörter, um KI-Aktionen zu aktivieren.",
"processingAction": "Verarbeitung...",
@@ -425,7 +503,45 @@
"shorten": "Kürzen",
"improve": "Verbessern",
"toMarkdown": "Zu Markdown",
"describeImages": "Describe images"
"describeImages": "Describe images",
"fixGrammar": "Korrigieren Sie die Grammatik",
"translate": "Übersetzen",
"explain": "Erklären",
"toRichText": "In Rich-Text konvertieren"
},
"generate": {
"slides": "Folien generieren",
"sectionLabel": "Generierungstools",
"theme": "Thema",
"themeArchitecturalMono": "Architektonisches Mono",
"themeVibrantTech": "Lebendige Technologie",
"themeMinimalSilk": "Minimale Seide",
"style": "Stil",
"styleProfessional": "Professional",
"styleCreative": "Kreativ",
"styleBrutalist": "Brutalistisch",
"diagram": "Diagramm erstellen",
"diagramReadyHint": "Wandeln Sie eine Notiz in einen visuellen Fluss um",
"diagramType": "Diagrammtyp",
"typeAuto": "Automatische Erkennung",
"typeFlowchart": "Flussdiagramm",
"typeMindMap": "Mindmap",
"typeTimeline": "Zeitleiste",
"typeOrgChart": "Organigramm",
"typeArchitecture": "Architektur",
"typeProcessMap": "Prozesslandkarte",
"styleSketchy": "Skizzenhaft",
"styleSoft": "Weich",
"styleMinimal": "Minimal",
"styleDraft": "Entwurf",
"stylePolished": "Poliert",
"styleHandwritten": "Handschriftlich",
"diagramReady": "Diagramm ist fertig!",
"openInExcalidraw": "Im Excalidraw Lab öffnen",
"insertDiagramInNote": "PNG in aktuelle Notiz einbetten",
"diagramImageAlt": "KI-generiertes Diagramm",
"insertedInNote": "Diagramm in Notiz eingefügt",
"insertExportError": "Fehler beim Exportieren/Hochladen des Diagramms"
},
"openAssistant": "KI-Assistenten öffnen",
"poweredByMomento": "Angetrieben von Momento AI",
@@ -442,7 +558,64 @@
"aiCopilot": "KI-Copilot",
"suggestTitle": "KI-Titelvorschlag",
"generateTitleFromImage": "Generate title from image",
"titleGenerated": "Title generated from image"
"titleGenerated": "Title generated from image",
"resourceTab": "Ressource",
"aiNoteTitle": "AI-Hinweis",
"injectReplace": "Ersetzen",
"injectReplaceTitle": "Ersetzen Sie den Notizinhalt durch diese Nachricht",
"injectComplete": "Vollständig",
"injectCompleteTitle": "Vervollständigen Sie die Notiz mit dieser Nachricht (AI)",
"injectMerge": "Verschmelzen",
"injectMergeTitle": "Mit Notiz zusammenführen (KI)",
"imagesCount": "{count} Bilder",
"resource": {
"failedToLoadUrl": "Diese URL konnte nicht geladen werden",
"pageLoaded": "Seite geladen: {title}",
"pageLoadError": "Fehler beim Laden der Seite",
"pasteOrUrlFirst": "Fügen Sie zuerst Text ein oder laden Sie eine URL",
"enrichError": "Anreicherungsfehler",
"enrichErrorShort": "Anreicherungsfehler",
"contentApplied": "Auf Hinweis ✓ angewendeter Inhalt",
"fromChat": "💬 Aus dem Chat",
"replacement": "↓ Ersatz",
"completedByAI": "✦ Von der KI abgeschlossen",
"mergedByAI": "⟳ Zusammengeführt durch KI",
"rendered": "Gerendert",
"cancel": "Stornieren",
"applyToNote": "Auf Notiz anwenden",
"urlLabel": "URL (optional)",
"resourceText": "Ressourcentext",
"resourcePlaceholder": "Fügen Sie hier Ihren Text ein (Markdown, HTML, einfacher Text…)",
"words": "Worte",
"integrationMode": "Integrationsmodus",
"modeReplace": "Ersetzen",
"modeReplaceDesc": "Direkt, keine KI",
"modeComplete": "Vollständig",
"modeCompleteDesc": "Fügt hinzu, ohne neu zu schreiben",
"modeMerge": "Verschmelzen",
"modeMergeDesc": "Umschreibt und integriert",
"aiProcessing": "KI-Verarbeitung…",
"preview": "Vorschau",
"generatePreview": "Vorschau generieren",
"emptyNoteHint": "💡 Die Notiz ist leer der Ressourceninhalt wird direkt eingebunden."
},
"cancel": "Stornieren",
"copied": "Kopiert",
"copy": "Kopie",
"transformations": "Transformationen",
"otherLanguage": "Eine andere Sprache",
"translateNow": "Jetzt übersetzen",
"generationTools": "Generierungswerkzeuge",
"generateSlidesLoading": "⏳ Präsentation wird erstellt...",
"generateDiagramLoading": "⏳ Diagramm wird erstellt...",
"errorShort": "Fehler",
"readyToast": "Bereit!",
"downloadFailedToast": "Der Download ist fehlgeschlagen",
"pptxDownloadButton": "Laden Sie .pptx herunter",
"presentationReadyBadge": "Präsentation bereit",
"openInLabTitle": "Im Labor öffnen",
"inlineSummaryMarkdown": "**Zusammenfassung:**",
"networkErrorShort": "Netzwerkfehler."
},
"titleSuggestions": {
"available": "Titelvorschläge",
@@ -548,7 +721,19 @@
"untitled": "Unbenannt",
"notifications": "Benachrichtigungen",
"declined": "Freigabe abgelehnt",
"removed": "Note aus der Liste entfernt"
"removed": "Note aus der Liste entfernt",
"slidesReady": "Präsentation bereit",
"openSlides": "Offene Präsentation",
"canvasReady": "Diagramm fertig",
"pptxReady": "Folien bereit",
"downloadPptx": "Laden Sie .pptx herunter",
"markAllRead": "Alles als gelesen markieren",
"agentSuccess": "Agent ist fertig",
"agentFailed": "Der Agent ist fehlgeschlagen",
"brainstormInvite": "Brainstorming",
"brainstormJoined": "Brainstorming",
"systemNotification": "System",
"downloadFailed": "Der Download ist fehlgeschlagen"
},
"nav": {
"home": "Startseite",
@@ -597,6 +782,17 @@
"themeLight": "Hell",
"themeDark": "Dunkel",
"themeSystem": "System",
"themeBaseGroup": "Base",
"themePalettesGroup": "Color palettes",
"themeSepia": "Sepia",
"themeMidnight": "Midnight",
"themeRose": "Rose",
"themeGreen": "Green",
"themeLavender": "Lavender",
"themeSand": "Sand",
"themeOcean": "Ocean",
"themeSunset": "Sunset",
"themeBlue": "Blue",
"notifications": "Benachrichtigungen",
"language": "Sprache",
"selectLanguage": "Sprache auswählen",
@@ -630,17 +826,8 @@
"desktopNotifications": "Desktop-Benachrichtigungen",
"desktopNotificationsDesc": "Benachrichtigungen im Browser erhalten",
"notificationsDesc": "Verwalten Sie Ihre Benachrichtigungseinstellungen",
"themeBaseGroup": "Base",
"themePalettesGroup": "Color palettes",
"themeSepia": "Sepia",
"themeMidnight": "Midnight",
"themeRose": "Rose",
"themeGreen": "Green",
"themeLavender": "Lavender",
"themeSand": "Sand",
"themeOcean": "Ocean",
"themeSunset": "Sunset",
"themeBlue": "Blue"
"autoSave": "Automatisch speichern",
"autoSaveDesc": "Änderungen während der Eingabe automatisch speichern"
},
"profile": {
"title": "Profil",
@@ -707,7 +894,15 @@
"providerDesc": "Wählen Sie Ihren bevorzugten KI-Anbieter",
"providerAutoDesc": "Ollama wenn verfügbar, sonst OpenAI",
"providerOllamaDesc": "100% privat, läuft lokal auf Ihrem Gerät",
"providerOpenAIDesc": "Am genauesten, erfordert API-Schlüssel"
"providerOpenAIDesc": "Am genauesten, erfordert API-Schlüssel",
"aiNote": "AI-Hinweis",
"aiNoteDesc": "Aktivieren Sie die KI-Chat-Schaltfläche und Tools zur Textverbesserung",
"languageDetection": "Spracherkennung",
"languageDetectionDesc": "Erkennt automatisch die Sprache Ihrer Notizen",
"autoLabeling": "Etikettenvorschläge",
"autoLabelingDesc": "Schlagt automatisch Beschriftungen für Ihre Notizen vor und wendet diese an",
"noteHistory": "Notizverlauf",
"noteHistoryDesc": "Aktivieren Sie Versions-Snapshots und die Wiederherstellung aus dem Verlauf"
},
"general": {
"loading": "Wird geladen...",
@@ -764,7 +959,9 @@
"markDone": "Als erledigt markieren",
"markUndone": "Als nicht erledigt markieren",
"todayAt": "Heute um {time}",
"tomorrowAt": "Morgen um {time}"
"tomorrowAt": "Morgen um {time}",
"clearCompleted": "Klar abgeschlossen",
"viewAll": "Alle Erinnerungen anzeigen"
},
"notebook": {
"create": "Notizbuch erstellen",
@@ -795,7 +992,11 @@
"confidence": "Konfidenz",
"savingReminder": "Fehler beim Speichern der Erinnerung",
"removingReminder": "Fehler beim Entfernen der Erinnerung",
"generatingDescription": "Please wait..."
"generatingDescription": "Please wait...",
"pinnedFrozenTooltip": "Angeheftetes Notizbuch Bestellung eingefroren",
"organizeNotebookWithAITooltip": "Organisieren Sie dieses Notizbuch mit KI",
"assistantRequiredForSummarize": "Aktivieren Sie AI Assistant in den Einstellungen, um eine Zusammenfassung vorzunehmen",
"createSubnotebook": "Sub-Notebook hinzufügen"
},
"notebookSuggestion": {
"title": "Nach {name} verschieben?",
@@ -808,6 +1009,9 @@
},
"admin": {
"title": "Admin-Dashboard",
"adminConsole": "Admin-Konsole",
"navSection": "Navigation",
"backToApp": "Zurück zum Andenken",
"userManagement": "Benutzerverwaltung",
"chat": "AI Chat",
"lab": "The Lab",
@@ -850,6 +1054,11 @@
"providerEmbeddingRequired": "AI_PROVIDER_EMBEDDING ist erforderlich",
"providerOllamaOption": "🦙 Ollama (Local & Free)",
"providerOpenAIOption": "🤖 OpenAI (GPT-5, GPT-4)",
"providerAnthropicOption": "🧠 Anthropisch (Claude API)",
"providerAnthropicCustomOption": "🧩 Anthropic-Benutzerdefiniert (Nachrichten-API MiniMax usw.)",
"anthropicModelHint": "Wählen Sie eine Claude-Modell-ID aus den Vorschlägen aus oder geben Sie eine manuell ein (keine Remote-Modellliste für die offizielle API).",
"anthropicCustomModelHint": "Anthropic-kompatible Nachrichten-API (z. B. MiniMax): Basis-URL https://api.minimax.io/anthropic (China: https://api.minimaxi.com/anthropic), Modell MiniMax-M2.7. Einbettungen: Verwenden Sie den Anbieter „Benutzerdefiniert“ + OpenAI-URL https://api.minimax.io/v1.",
"anthropicCustomNoModelList": "Dieses Gateway stellt keine /models-Liste im OpenAI-Stil zur Verfügung wählen Sie das Modell aus den Vorschlägen aus oder geben Sie es ein (z. B. MiniMax-M2.7).",
"providerCustomOption": "🔧 Custom OpenAI-Compatible",
"providerDeepSeekOption": "🔍 DeepSeek",
"providerOpenRouterOption": "🌐 OpenRouter",
@@ -1003,7 +1212,14 @@
"error": "Fehler:",
"testError": "Testfehler: {error}",
"tipTitle": "Tipp:",
"tipDescription": "Verwenden Sie das KI-Test-Panel, um Konfigurationsprobleme vor dem Testen zu diagnostizieren."
"tipDescription": "Verwenden Sie das KI-Test-Panel, um Konfigurationsprobleme vor dem Testen zu diagnostizieren.",
"chatTestTitle": "Chat-Assistent-Test",
"chatTestDescription": "Testen Sie den vom Chat-Assistenten verwendeten KI-Anbieter",
"chatGenerationTest": "💬 Chat-Assistent-Test:",
"chatStep1": "Sendet eine Testnachricht an den Assistenten",
"chatStep2": "Bittet um eine prägnante Antwort darauf, was der Assistent tut",
"chatStep3": "Zeigt die Modellantwort",
"chatStep4": "Überprüft Reaktionsfähigkeit und Latenz"
},
"sidebar": {
"dashboard": "Dashboard",
@@ -1194,6 +1410,7 @@
"notesViewLabel": "Notizen-Ansicht",
"notesViewTabs": "Tabs (OneNote-Stil)",
"notesViewMasonry": "Karten (Raster)",
"notesViewList": "Liste (Magazin)",
"selectTheme": "Select theme",
"fontFamilyLabel": "Schriftfamilie",
"fontFamilyDescription": "Wählen Sie die im gesamten Programm verwendete Schriftart",
@@ -1277,6 +1494,69 @@
"organizeWithAI": "Mit KI organisieren",
"organize": "Organisieren"
},
"organizeNotebook": {
"title": "Notizbuch organisieren",
"unknownError": "Unbekannter Fehler",
"toastSuccess": "Notizbuch organisiert {created} Unternotizbuch(e) erstellt, {moved} Notiz(en) verschoben",
"intro": "AI analysiert die Notizen in diesem Notizbuch und schlägt einen Plan vor, sie in thematische Unternotizbücher neu zu organisieren.",
"bulletThemes": "Gruppieren Sie Notizen nach Thema oder Thema",
"bulletSubfolders": "Erstellen Sie fehlende Unternotizbücher",
"bulletPreview": "Vollständige Vorschau vor jeder Änderung",
"analyzingTitle": "Analysieren…",
"analyzingSubtitle": "KI liest Ihre Notizen und identifiziert Themen",
"previewSummary": "{groups} Gruppe(n) · {notes} Notizen · {newSubs} neue Unternotizbücher",
"badgeNew": "Neu",
"untitledNote": "Unbenannte Notiz",
"notesInGroup": "{count} Notizen",
"executingTitle": "Organisieren…",
"executingSubtitle": "Unternotizbücher erstellen und Notizen verschieben",
"doneTitle": "Notizbuch organisiert!",
"doneStats": "{created} Unternotizbuch(e) erstellt · {moved} Notiz(en) verschoben",
"analyzeButton": "Analysieren Sie mit KI",
"restart": "Fangen Sie von vorne an",
"confirm": "Anwenden",
"closeButton": "Schließen"
},
"documentInfo": {
"tabInfo": "Info",
"tabVersions": "Versionen",
"wordsLabel": "Worte",
"charactersLabel": "Charaktere",
"notebookLabel": "Notizbuch",
"typeLabel": "Typ",
"createdLabel": "Erstellt",
"modifiedLabel": "Aktualisiert",
"labelsSection": "Etiketten",
"idLabel": "AUSWEIS",
"historyDisabled": "Der Verlauf ist für diese Notiz nicht aktiviert.",
"enableHistory": "Verlauf aktivieren",
"savedVersions": "Gespeicherte Versionen",
"savingEllipsis": "Sparen…",
"versionSaved": "Version gespeichert!",
"saveThisVersion": "Speichern Sie diese Version",
"loading": "Laden…",
"noVersion": "Noch keine Versionen",
"restoreTooltip": "Wiederherstellen",
"deleteTooltip": "Löschen",
"comparisonMode": "Vergleichsmodus",
"comparisonSubtitle": "Vergleichen Sie Versionen nebeneinander",
"deleteVersionConfirm": "Diese Version löschen?",
"latestBadge": "Letzte"
},
"languages": {
"targets": {
"french": "Französisch",
"english": "Englisch",
"spanish": "Spanisch",
"german": "Deutsch",
"persian": "persisch",
"portuguese": "Portugiesisch",
"italian": "Italienisch",
"chinese": "chinesisch",
"japanese": "japanisch"
},
"customPlaceholder": "z.B. Arabisch, Russisch…"
},
"common": {
"unknown": "Unbekannt",
"notAvailable": "Nicht verfügbar",
@@ -1398,12 +1678,16 @@
"scraper": "Monitor",
"researcher": "Rechercheur",
"monitor": "Beobachter",
"slideGenerator": "Folien",
"excalidrawGenerator": "Diagramm",
"custom": "Benutzerdefiniert"
},
"typeDescriptions": {
"scraper": "Extrahiert Inhalte von mehreren Websites und erstellt eine Zusammenfassung",
"researcher": "Sucht nach Informationen zu einem Thema",
"monitor": "Überwacht ein Notizbuch und analysiert Notizen",
"slideGenerator": "Erstellt eine PowerPoint-Präsentation aus Notizen",
"excalidrawGenerator": "Erstellt ein Excalidraw-Diagramm aus Notizen",
"custom": "Freier Agent mit Ihrem eigenen Prompt"
},
"form": {
@@ -1416,6 +1700,27 @@
"urlsOptional": "(optional)",
"sourceNotebook": "Zu überwachendes Notizbuch",
"selectNotebook": "Notizbuch auswählen...",
"selectNotes": "Notizen zur Analyse",
"notesSelected": "{{count}} Notiz(en) ausgewählt",
"slideTheme": "Präsentationsthema",
"slideThemeDefault": "Automatisch",
"slideStyle": "Visueller Stil",
"slideStyleSoft": "Weich (empfohlen)",
"slideStyleSharp": "Scharf und dicht",
"slideStyleRounded": "Abgerundet und geräumig",
"slideStylePill": "Premium / Pille",
"excalidrawDiagramType": "Diagrammtyp",
"excalidrawDiagramTypeAuto": "Automatisch (Domänenerkennung)",
"excalidrawDiagramTypeFlowchart": "Flussdiagramm (Prozess)",
"excalidrawDiagramTypeMindmap": "Mindmap (Ideen)",
"excalidrawDiagramTypeOrgChart": "Organigramm (Teams)",
"excalidrawDiagramTypeTimeline": "Zeitleiste/Roadmap",
"excalidrawDiagramTypeProcessMap": "Prozesslandkarte (Operationen)",
"excalidrawDiagramTypeArchitectureCloud": "Cloud-Architektur (Zonen/RG)",
"excalidrawDiagramStyle": "Excalidraw-Diagrammstil",
"excalidrawDiagramStyleDefault": "Farbig (Excalidraw)",
"excalidrawDiagramStyleSketchPlus": "Sketch+ (erweitertes Excalidraw)",
"excalidrawDiagramStyleAustere": "Strenge (minimal)",
"targetNotebook": "Ziel-Notizbuch",
"inbox": "Posteingang",
"instructions": "KI-Anweisungen",
@@ -1485,6 +1790,8 @@
"updated": "Agent aktualisiert",
"deleted": "\"{name}\" gelöscht",
"deleteError": "Fehler beim Löschen",
"running": "Generierung läuft…",
"runningDesc": "Die Generierung kann einige Minuten dauern. Sie können frei navigieren.",
"runSuccess": "\"{name}\" erfolgreich ausgeführt",
"runError": "Fehler: {error}",
"runFailed": "Ausführung fehlgeschlagen",
@@ -1519,13 +1826,24 @@
"chercheur": {
"name": "Themen-Rechercheur",
"description": "Sucht nach tiefgehenden Informationen zu einem Thema und erstellt eine strukturierte Notiz mit Referenzen."
},
"slideGenerator": {
"name": "Foliengenerator",
"description": "Liest Notizen aus einem Notizbuch und erstellt automatisch eine strukturierte Präsentation."
},
"excalidrawGenerator": {
"name": "Diagrammgenerator",
"description": "Liest eine Notiz und generiert ein visuelles Diagramm im Excalidraw Lab."
}
},
"runLog": {
"title": "Verlauf",
"noHistory": "Noch keine Ausführungen",
"toolTrace": "{count} Werkzeugaufrufe",
"step": "Schritt {num}"
"step": "Schritt {num}",
"clearConfirm": "Sind Sie sicher, dass Sie den gesamten Verlauf für diesen Agenten löschen möchten?",
"cleared": "Verlauf gelöscht",
"clearHistory": "Klare Geschichte"
},
"tools": {
"title": "Agenten-Werkzeuge",
@@ -1536,6 +1854,9 @@
"noteCreate": "Notiz erstellen",
"urlFetch": "URL abrufen",
"memorySearch": "Gedächtnis",
"generatePptx": "PPTX-Folien",
"generateSlides": "HTML-Folien",
"generateExcalidraw": "Excalidraw-Diagramm",
"configNeeded": "Konfiguration",
"selected": "{count} ausgewählt",
"maxSteps": "Max. Iterationen"
@@ -1547,7 +1868,9 @@
"scraper": "Sie sind ein Überwachungsassistent. Fassen Sie Artikel von verschiedenen Websites zu einer klaren, strukturierten Zusammenfassung zusammen.",
"researcher": "Sie sind ein gründlicher Rechercheur. Erstellen Sie für das angeforderte Thema eine Forschungsnotiz mit Kontext, Kernpunkten, Debatten und Referenzen.",
"monitor": "Sie sind ein analytischer Assistent. Analysieren Sie die bereitgestellten Notizen und schlagen Sie Ansätze, Referenzen und Verbindungen zwischen Notizen vor.",
"custom": "Sie sind ein hilfreicher Assistent."
"custom": "Sie sind ein hilfreicher Assistent.",
"slideGenerator": "Sie sind ein Präsentationsersteller. Lesen Sie die bereitgestellten Inhalte und erstellen Sie strukturierte Folien mit Titeln, Kernpunkten und Zusammenfassungen.",
"excalidrawGenerator": "Sie sind ein Diagrammersteller. Analysieren Sie den bereitgestellten Inhalt und erstellen Sie ein klares, organisiertes visuelles Diagramm."
},
"help": {
"title": "Agenten-Leitfaden",
@@ -1581,7 +1904,10 @@
"frequency": "Wie oft der Agent automatisch läuft. Starten Sie mit Manuell zum Testen.",
"instructions": "Benutzerdefinierte Anweisungen, die den Standard-KI-Prompt ersetzen. Leer lassen für automatischen Prompt.",
"tools": "Wählen Sie die Werkzeuge, die der Agent verwenden kann. Jedes Werkzeug gibt dem Agent eine spezifische Fähigkeit.",
"maxSteps": "Maximale Anzahl von Denkachlägen. Mehr Schritte = tiefere Analyse, dauert aber länger."
"maxSteps": "Maximale Anzahl von Denkachlägen. Mehr Schritte = tiefere Analyse, dauert aber länger.",
"selectNotes": "Wählen Sie bestimmte Notizen zur Analyse aus. Wenn keine Option ausgewählt ist, verwendet der Agent alle Notizen aus dem Notizbuch.",
"slideTheme": "Wählen Sie eine Farbpalette für die Präsentation. Automatisch lässt die KI entscheiden.",
"slideStyle": "Der visuelle Stil beeinflusst den Eckenradius, den Abstand und die Informationsdichte."
}
}
},
@@ -1631,5 +1957,147 @@
"lab": {
"initializing": "Arbeitsbereich wird initialisiert",
"loadingIdeas": "Deine Ideen werden geladen..."
},
"richTextEditor": {
"slashHint": "↑↓ navigieren · Einfügung eingeben · Abschnitt zum Wechseln der Tabulatortaste",
"slashLoading": "KI-Denken...",
"slashTabAll": "Alle",
"slashCatBasic": "Grundblöcke",
"slashCatMedia": "Medien",
"slashCatFormatting": "Formatierung",
"slashCatAi": "AI-Hinweis",
"insertImage": "Bild einfügen",
"imageUrlPlaceholder": "https://example.com/image.png",
"preview": "Vorschau",
"cancel": "Stornieren",
"insert": "Einfügen",
"slashText": "Text",
"slashTextDesc": "Einfacher Absatz",
"slashH1": "Überschrift 1",
"slashH1Desc": "Große Abschnittsüberschrift",
"slashH2": "Überschrift 2",
"slashH2Desc": "Überschrift des mittleren Abschnitts",
"slashH3": "Überschrift 3",
"slashH3Desc": "Kleine Abschnittsüberschrift",
"slashBullet": "Aufzählungsliste",
"slashBulletDesc": "Ungeordnete Liste",
"slashNumbered": "Nummerierte Liste",
"slashNumberedDesc": "Geordnete nummerierte Liste",
"slashTodo": "Aufgabenliste",
"slashTodoDesc": "Checkbox-Aufgaben",
"slashQuote": "Zitat",
"slashQuoteDesc": "Erfassen Sie ein Angebot",
"slashCode": "Codeblock",
"slashCodeDesc": "Codeausschnitt",
"slashDivider": "Teiler",
"slashDividerDesc": "Horizontaler Trenner",
"slashTable": "Tisch",
"slashTableDesc": "Fügen Sie ein einfaches Raster ein",
"slashDiagram": "Diagramm",
"slashDiagramDesc": "Erstellen Sie einen Flow oder eine Mindmap",
"slashSlides": "Präsentation",
"slashSlidesDesc": "Erstellen Sie ein wunderschönes Foliendeck",
"slashImage": "Bild",
"slashImageDesc": "Betten Sie ein Bild von der URL ein",
"slashAlignLeft": "Links ausrichten",
"slashAlignLeftDesc": "Text linksbündig ausrichten",
"slashAlignCenter": "Center",
"slashAlignCenterDesc": "Zentrieren Sie den Text",
"slashAlignRight": "Rechts ausrichten",
"slashAlignRightDesc": "Text rechts ausrichten",
"slashSuperscript": "Hochgestellt",
"slashSuperscriptDesc": "Text über der Grundlinie",
"slashSubscript": "Index",
"slashSubscriptDesc": "Text unterhalb der Grundlinie",
"slashClarify": "Klären",
"slashClarifyDesc": "Machen Sie den Text klarer",
"slashShorten": "Verkürzen",
"slashShortenDesc": "Verdichten Sie den Text",
"slashImprove": "Verbessern",
"slashImproveDesc": "Verbessern Sie den Stil",
"slashExpand": "Expandieren",
"slashExpandDesc": "Verfeinern und bereichern Sie den Text",
"imageModalTitle": "Bild einfügen",
"imageModalPreview": "Vorschau",
"imageModalCancel": "Stornieren",
"imageModalInsert": "Einfügen",
"imageModalInvalidUrl": "Bitte geben Sie eine gültige URL ein",
"imageModalLoadFailed": "Bild konnte nicht geladen werden",
"linkPlaceholder": "Einen Link einfügen oder eingeben...",
"bold": "Deutlich",
"italic": "Kursiv",
"underline": "Unterstreichen",
"strike": "Durchgestrichen",
"code": "Code",
"highlight": "Hervorheben",
"superscript": "Hochgestellt",
"subscript": "Index",
"addBlock": "Block hinzufügen",
"placeholder": "Geben Sie „/“ für Befehle ein..."
},
"brainstorm": {
"title": "Waves of Thought",
"subtitle": "Unfold dimensions of potentiality",
"placeholder": "Enter a concept to unfold...",
"generating": "AI is harvesting seeds of thought...",
"newBrainstorm": "New Brainstorm",
"noSessions": "No brainstorms yet",
"startOne": "Start one",
"sessions": "Brainstorms",
"seedLabel": "Seed Idea",
"ideaPromptDetailed": "Geben Sie Ihre Idee, Frage oder Ihr Thema für ein Brainstorming ein ...",
"brainstormThisIdea": "Brainstorm this idea",
"startBrainstorm": "Start Brainstorm",
"spatialMode": "Spatial Exploration Mode",
"wave1": "Wave 1",
"wave2": "Wave 2",
"wave3": "Wave 3",
"export": "Export",
"exporting": "Exporting...",
"wave": "Wave",
"novelty": "Novelty",
"originConnection": "Origin connection",
"linkedNotes": "Linked notes",
"deepen": "Deepen",
"deepening": "Generating...",
"extract": "Create Note",
"converting": "Converting...",
"dismiss": "Not pertinent",
"noteCreated": "Note Created",
"ideas": "ideas",
"cancel": "Cancel",
"delete": "Delete",
"ideaOrigin": "Origin of the idea",
"noNoteLink": "Purely generative idea",
"derived_from": "Derived from",
"opposes": "In opposition with",
"extends": "Extends",
"synthesizes": "Synthesizes",
"transposes": "Transposes",
"none_found": "No note link",
"viewNote": "View note",
"addIdea": "Add idea",
"manualIdeaPrompt": "Title of your idea:",
"invite": "Invite",
"linkCopied": "Invite link copied!",
"activityTitle": "Aktivität",
"noActivity": "Noch keine Aktivität",
"justNow": "soeben",
"humanIdea": "Menschlich",
"aiIdea": "KI",
"respondsTo": "Reagiert auf",
"adding": "Hinzufügen...",
"manualIdeaDesc": "Teilen Sie Ihre Idee mit der Brainstorming-Leinwand",
"manualIdeaTitle": "Titel",
"manualIdeaTitlePlaceholder": "Ihre Idee in wenigen Worten...",
"manualIdeaDescLabel": "Beschreibung (optional)",
"manualIdeaDescPlaceholder": "Erläutern Sie Ihre Idee...",
"activity": {
"manual_idea": "eine Idee hinzugefügt",
"wave_generated": "eine Welle erzeugt",
"joined": "nahm an der Sitzung teil",
"idea_dismissed": "eine Idee verworfen",
"invite_created": "hat eine Einladung erstellt"
}
}
}
}

View File

@@ -51,7 +51,11 @@
"accountMenu": "Account menu",
"profile": "Profile",
"signOut": "Sign out",
"sortOrder": "Sort order"
"sortOrder": "Sort order",
"freezePinnedNotebook": "Pin notebook sidebar order",
"unfreezePinnedNotebook": "Unpin notebook sidebar order",
"newSubNotebook": "New sub-notebook",
"renameNotebook": "Rename"
},
"notes": {
"title": "Notes",
@@ -61,6 +65,12 @@
"placeholder": "Take a note...",
"markdownPlaceholder": "Take a note... (Markdown supported)",
"titlePlaceholder": "Title",
"noteTypes": {
"richtext": "Rich Text",
"markdown": "Markdown",
"text": "Plain text",
"checklist": "Checklist"
},
"listItem": "List item",
"addListItem": "+ List item",
"newChecklist": "New checklist",
@@ -217,6 +227,9 @@
"sort": "Sort",
"confirmDeleteTitle": "Delete note",
"leftShare": "Share removed",
"ideaOrigin": "Origin of the idea",
"noNoteLink": "Purely generative idea",
"dismiss": "Not pertinent",
"dismissed": "Note dismissed from recent",
"generalNotes": "General Notes",
"noteType": "Note type",
@@ -230,7 +243,23 @@
"switchTypeTitle": "Switch note type?",
"switchTypeWarning": "Some formatting may be lost when switching to {type}.",
"switchTypeContentPreserved": "Your content will be preserved as plain text.",
"switchType": "Switch to {type}"
"switchType": "Switch to {type}",
"saveNow": "Save now",
"backToCollection": "Back to collection",
"markdownEditingTitle": "Return to editing",
"markdownPreviewTitle": "Preview",
"brainstormThisIdea": "Brainstorm this idea",
"brainstormThisIdeaAria": "Brainstorm this idea",
"shareNoteTitle": "Share note",
"shareNoteAria": "Share note",
"saveNoteAria": "Save note",
"noChangesToSaveAria": "No changes to save",
"optionsMenuAria": "Options menu",
"deleteNoteConfirmItem": "Delete note",
"noteDeletedToast": "Note deleted.",
"deleteNoteFailedToast": "Could not delete.",
"documentInfoAria": "Document information",
"noModification": "No changes"
},
"pagination": {
"previous": "←",
@@ -312,7 +341,24 @@
"accessRevoked": "Access has been revoked",
"errorLoading": "Error loading collaborators",
"failedToAdd": "Failed to add collaborator",
"failedToRemove": "Failed to remove collaborator"
"failedToRemove": "Failed to remove collaborator",
"shareCompactTitle": "Share",
"inviteByEmailLabel": "Invite by email",
"accessReadCompact": "View",
"accessEditCompact": "Edit",
"sendInvitation": "Send invitation",
"invitationSentBadge": "Invitation sent",
"sharedAccessLabel": "Shared access",
"noCollaboratorsEmpty": "No collaborators yet.",
"removeAccessTitle": "Remove access",
"toastInviteSentTo": "Invitation sent to {email}",
"toastAccessRemoved": "Access removed for {target}",
"toastUserFallback": "the user",
"toastSharingError": "Sharing error",
"toastEmailNotFound": "No account found with this email.",
"toastAlreadySharedUser": "This note is already shared with this user.",
"toastRemoveAccessFailed": "Could not remove access.",
"userFallback": "User"
},
"ai": {
"analyzing": "AI analyzing...",
@@ -424,6 +470,15 @@
"chatTab": "Chat",
"noteActions": "Note Actions",
"askToStart": "Ask the Assistant something to get started.",
"chatPanelContext": "Context",
"chatPanelNotebookPlus": "+ Notebook",
"chatPanelWritingTone": "Writing tone",
"scopeAutoBadge": "Auto",
"chatNoteQuestionPlaceholder": "Ask a question about this note...",
"chatNotebookSelectPlaceholder": "Include a notebook...",
"assistantTabActions": "Actions",
"resourcePreviewAiTitle": "AI preview",
"resourcePreviewInjectFromChat": "Inject from chat",
"contextLabel": "Context",
"thisNote": "This note",
"allMyNotes": "All my notes",
@@ -435,6 +490,7 @@
"newLineHint": "Shift+Enter = new line",
"resultLabel": "Result",
"discardAction": "Discard",
"organization": "Organization",
"transformationsDesc": "Transformations — applied directly to the note",
"writeMinWordsAction": "Write at least 5 words to activate AI actions.",
"processingAction": "Processing...",
@@ -448,7 +504,8 @@
"describeImages": "Describe images",
"fixGrammar": "Fix Grammar",
"translate": "Translate",
"explain": "Explain"
"explain": "Explain",
"toRichText": "Convert to rich text"
},
"generate": {
"slides": "Generate Slides",
@@ -459,6 +516,8 @@
"themeMinimalSilk": "Minimal Silk",
"style": "Style",
"styleProfessional": "Professional",
"styleCreative": "Creative",
"styleBrutalist": "Brutalist",
"diagram": "Generate Diagram",
"diagramReadyHint": "Convert note into visual flow",
"diagramType": "Diagram Type",
@@ -540,7 +599,21 @@
},
"cancel": "Cancel",
"copied": "Copied",
"copy": "Copy"
"copy": "Copy",
"transformations": "Transformations",
"otherLanguage": "Another language",
"translateNow": "Translate now",
"generationTools": "Generation tools",
"generateSlidesLoading": "⏳ Generating presentation...",
"generateDiagramLoading": "⏳ Generating diagram...",
"errorShort": "Error",
"readyToast": "Ready!",
"downloadFailedToast": "Download failed",
"pptxDownloadButton": "Download .pptx",
"presentationReadyBadge": "Presentation ready",
"openInLabTitle": "Open in Lab",
"inlineSummaryMarkdown": "**Summary:**",
"networkErrorShort": "Network error."
},
"titleSuggestions": {
"available": "Title suggestions",
@@ -651,7 +724,14 @@
"openSlides": "Open presentation",
"canvasReady": "Diagram ready",
"pptxReady": "Slides ready",
"downloadPptx": "Download .pptx"
"downloadPptx": "Download .pptx",
"markAllRead": "Mark all read",
"agentSuccess": "Agent finished",
"agentFailed": "Agent failed",
"brainstormInvite": "Brainstorm",
"brainstormJoined": "Brainstorm",
"systemNotification": "System",
"downloadFailed": "Download failed"
},
"nav": {
"home": "Home",
@@ -743,7 +823,9 @@
"emailNotificationsDesc": "Receive important notifications by email",
"desktopNotifications": "Desktop notifications",
"desktopNotificationsDesc": "Receive notifications in your browser",
"notificationsDesc": "Manage your notification preferences"
"notificationsDesc": "Manage your notification preferences",
"autoSave": "Auto-save",
"autoSaveDesc": "Automatically save changes while typing"
},
"profile": {
"title": "Profile",
@@ -908,7 +990,11 @@
"confidence": "confidence",
"savingReminder": "Failed to save reminder",
"removingReminder": "Failed to remove reminder",
"generatingDescription": "Please wait..."
"generatingDescription": "Please wait...",
"pinnedFrozenTooltip": "Pinned notebook — order frozen",
"organizeNotebookWithAITooltip": "Organize this notebook with AI",
"assistantRequiredForSummarize": "Turn on AI Assistant in settings to summarize",
"createSubnotebook": "Add sub-notebook"
},
"notebookSuggestion": {
"title": "Move to {name}?",
@@ -1124,7 +1210,14 @@
"error": "Error:",
"testError": "Test Error: {error}",
"tipTitle": "Tip:",
"tipDescription": "Use the AI Test Panel to diagnose configuration issues before testing."
"tipDescription": "Use the AI Test Panel to diagnose configuration issues before testing.",
"chatTestTitle": "Chat assistant test",
"chatTestDescription": "Test the AI provider used by the chat assistant",
"chatGenerationTest": "💬 Chat assistant test:",
"chatStep1": "Sends a test message to the assistant",
"chatStep2": "Asks for a concise answer about what the assistant does",
"chatStep3": "Shows the model response",
"chatStep4": "Checks responsiveness and latency"
},
"sidebar": {
"dashboard": "Dashboard",
@@ -1399,6 +1492,69 @@
"organizeWithAI": "Organize with AI",
"organize": "Organize"
},
"organizeNotebook": {
"title": "Organize notebook",
"unknownError": "Unknown error",
"toastSuccess": "Notebook organized — {created} sub-notebook(s) created, {moved} note(s) moved",
"intro": "AI will analyze the notes in this notebook and propose a plan to reorganize them into thematic sub-notebooks.",
"bulletThemes": "Group notes by topic or theme",
"bulletSubfolders": "Create missing sub-notebooks",
"bulletPreview": "Full preview before any change",
"analyzingTitle": "Analyzing…",
"analyzingSubtitle": "AI is reading your notes and identifying themes",
"previewSummary": "{groups} group(s) · {notes} notes · {newSubs} new sub-notebook(s)",
"badgeNew": "New",
"untitledNote": "Untitled note",
"notesInGroup": "{count} notes",
"executingTitle": "Organizing…",
"executingSubtitle": "Creating sub-notebooks and moving notes",
"doneTitle": "Notebook organized!",
"doneStats": "{created} sub-notebook(s) created · {moved} note(s) moved",
"analyzeButton": "Analyze with AI",
"restart": "Start over",
"confirm": "Apply",
"closeButton": "Close"
},
"documentInfo": {
"tabInfo": "Info",
"tabVersions": "Versions",
"wordsLabel": "Words",
"charactersLabel": "Characters",
"notebookLabel": "Notebook",
"typeLabel": "Type",
"createdLabel": "Created",
"modifiedLabel": "Updated",
"labelsSection": "Labels",
"idLabel": "ID",
"historyDisabled": "History is not enabled for this note.",
"enableHistory": "Enable history",
"savedVersions": "Saved versions",
"savingEllipsis": "Saving…",
"versionSaved": "Version saved!",
"saveThisVersion": "Save this version",
"loading": "Loading…",
"noVersion": "No versions yet",
"restoreTooltip": "Restore",
"deleteTooltip": "Delete",
"comparisonMode": "Comparison mode",
"comparisonSubtitle": "Compare versions side by side",
"deleteVersionConfirm": "Delete this version?",
"latestBadge": "Latest"
},
"languages": {
"targets": {
"french": "French",
"english": "English",
"spanish": "Spanish",
"german": "German",
"persian": "Persian",
"portuguese": "Portuguese",
"italian": "Italian",
"chinese": "Chinese",
"japanese": "Japanese"
},
"customPlaceholder": "e.g. Arabic, Russian…"
},
"common": {
"unknown": "Unknown",
"notAvailable": "N/A",
@@ -1876,5 +2032,70 @@
"subscript": "Subscript",
"addBlock": "Add block",
"placeholder": "Type '/' for commands..."
},
"brainstorm": {
"title": "Waves of Thought",
"subtitle": "Unfold dimensions of potentiality",
"placeholder": "Enter a concept to unfold...",
"generating": "AI is harvesting seeds of thought...",
"newBrainstorm": "New Brainstorm",
"noSessions": "No brainstorms yet",
"startOne": "Start one",
"sessions": "Brainstorms",
"seedLabel": "Seed Idea",
"ideaPromptDetailed": "Enter your idea, question, or topic to brainstorm...",
"brainstormThisIdea": "Brainstorm this idea",
"startBrainstorm": "Start Brainstorm",
"spatialMode": "Spatial Exploration Mode",
"wave1": "Wave 1",
"wave2": "Wave 2",
"wave3": "Wave 3",
"export": "Export",
"exporting": "Exporting...",
"wave": "Wave",
"novelty": "Novelty",
"originConnection": "Origin connection",
"linkedNotes": "Linked notes",
"deepen": "Deepen",
"deepening": "Generating...",
"extract": "Create Note",
"converting": "Converting...",
"dismiss": "Not pertinent",
"noteCreated": "Note Created",
"ideas": "ideas",
"cancel": "Cancel",
"delete": "Delete",
"ideaOrigin": "Origin of the idea",
"noNoteLink": "Purely generative idea",
"derived_from": "Derived from",
"opposes": "In opposition with",
"extends": "Extends",
"synthesizes": "Synthesizes",
"transposes": "Transposes",
"none_found": "No note link",
"viewNote": "View note",
"addIdea": "Add idea",
"manualIdeaPrompt": "Title of your idea:",
"invite": "Invite",
"linkCopied": "Invite link copied!",
"activityTitle": "Activity",
"noActivity": "No activity yet",
"justNow": "just now",
"humanIdea": "Human",
"aiIdea": "AI",
"respondsTo": "Responds to",
"adding": "Adding...",
"manualIdeaDesc": "Share your idea with the brainstorm canvas",
"manualIdeaTitle": "Title",
"manualIdeaTitlePlaceholder": "Your idea in a few words...",
"manualIdeaDescLabel": "Description (optional)",
"manualIdeaDescPlaceholder": "Elaborate on your idea...",
"activity": {
"manual_idea": "added an idea",
"wave_generated": "generated a wave",
"joined": "joined the session",
"idea_dismissed": "dismissed an idea",
"invite_created": "created an invite"
}
}
}
}

View File

@@ -32,6 +32,8 @@
},
"sidebar": {
"notes": "Notes",
"recent": "Reciente",
"quickNav": "Navegación rápida",
"reminders": "Reminders",
"labels": "Labels",
"editLabels": "Edit labels",
@@ -40,15 +42,35 @@
"noLabelsInNotebook": "Sin etiquetas en este cuaderno",
"archive": "Archive",
"trash": "Trash",
"clearFilter": "Remove filter"
"clearFilter": "Remove filter",
"inbox": "Bandeja de entrada",
"sharedWithMe": "compartido conmigo",
"sortNewest": "Lo más nuevo primero",
"sortOldest": "El más viejo primero",
"sortAlpha": "A → Z",
"accountMenu": "Menú de cuenta",
"profile": "Perfil",
"signOut": "desconectar",
"sortOrder": "orden de clasificación",
"freezePinnedNotebook": "Orden de fijación de la barra lateral del cuaderno",
"unfreezePinnedNotebook": "Desanclar el orden de la barra lateral del cuaderno",
"newSubNotebook": "Nuevo subportátil",
"renameNotebook": "Rebautizar"
},
"notes": {
"title": "Notas",
"newNote": "Nueva nota",
"reorganize": "Reorganizar notas",
"untitled": "Sin título",
"placeholder": "Toma una nota...",
"markdownPlaceholder": "Toma una nota... (Markdown compatible)",
"titlePlaceholder": "Título",
"noteTypes": {
"richtext": "Texto enriquecido",
"markdown": "Reducción",
"text": "Texto sin formato",
"checklist": "Lista de verificación"
},
"listItem": "Elemento de lista",
"addListItem": "+ Elemento de lista",
"newChecklist": "Nueva lista de verificación",
@@ -58,6 +80,7 @@
"confirmDelete": "¿Estás seguro de que quieres eliminar esta nota?",
"confirmLeaveShare": "¿Estás seguro de que quieres abandonar esta nota compartida?",
"sharedBy": "Compartido por",
"sharedShort": "Compartido",
"leaveShare": "Abandonar",
"delete": "Eliminar",
"archive": "Archivar",
@@ -136,6 +159,8 @@
"dragToReorder": "Arrastra para reordenar",
"more": "Más",
"emptyState": "Sin notas",
"metadataPanel": "Detalles",
"metadataNotebook": "Computadora portátil",
"emptyStateTabs": "Sin notas en esta vista. Usa \"Nueva nota\" en la barra lateral (sugerencias de título IA en el compositor).",
"inNotebook": "En cuaderno",
"moveFailed": "Error al mover",
@@ -147,11 +172,6 @@
"unpinned": "Desfijadas",
"redoShortcut": "Rehacer (Ctrl+Y)",
"undoShortcut": "Deshacer (Ctrl+Z)",
"viewCards": "Vista tarjetas",
"viewCardsTooltip": "Cuadrícula de tarjetas con reorganización por arrastrar y soltar",
"viewTabs": "Vista lista",
"viewTabsTooltip": "Pestañas arriba, nota abajo — arrastra pestañas para reordenar",
"viewModeGroup": "Modo de visualización de notas",
"reorderTabs": "Reordenar pestaña",
"modified": "Modificada",
"created": "Creada",
@@ -160,15 +180,18 @@
"savedStatus": "Guardado",
"dirtyStatus": "Modificado",
"completedLabel": "Completados",
"notes.emptyNotebook": "Cuaderno vacío",
"notes.emptyNotebookDesc": "Este cuaderno no tiene notas. Haz clic en + para crear una.",
"notes.noNoteSelected": "Ninguna nota seleccionada",
"notes.selectOrCreateNote": "Selecciona una nota de la lista o crea una nueva.",
"notes": {
"emptyNotebook": "Cuaderno vacío",
"emptyNotebookDesc": "Este cuaderno no tiene notas. Haz clic en + para crear una.",
"noNoteSelected": "Ninguna nota seleccionada",
"selectOrCreateNote": "Selecciona una nota de la lista o crea una nueva."
},
"commitVersion": "Guardar versión",
"versionSaved": "Versión guardada",
"deleteVersion": "Eliminar esta versión",
"versionDeleted": "Versión eliminada",
"deleteVersionConfirm": "¿Eliminar esta versión permanentemente?",
"deleteVersionDesc": "Esta acción no se puede deshacer. La versión se eliminará permanentemente del historial.",
"historyMode": "Modo de historial",
"historyModeManual": "Manual (botón commit)",
"historyModeAuto": "Automático (inteligente)",
@@ -184,6 +207,10 @@
"enableHistory": "Activar historial",
"historyEmpty": "No hay versiones disponibles",
"historySelectVersion": "Selecciona una versión para previsualizar su contenido",
"currentVersion": "actual",
"compareVersions": "Comparar",
"diffTitle": "Comparación",
"diffSelectHint": "Haga clic en 2 versiones de la lista para compararlas",
"sortBy": "Ordenar por",
"sortDateDesc": "Fecha (reciente)",
"sortDateAsc": "Fecha (antigua)",
@@ -197,10 +224,14 @@
"createFailed": "Failed to create note",
"updateFailed": "Failed to update note",
"archived": "Note archived",
"unarchivedSuccess": "Nota eliminada del archivo",
"archiveFailed": "Failed to archive",
"sort": "Sort",
"confirmDeleteTitle": "Delete note",
"leftShare": "Share removed",
"ideaOrigin": "Origin of the idea",
"noNoteLink": "Purely generative idea",
"dismiss": "Not pertinent",
"dismissed": "Note dismissed from recent",
"generalNotes": "General Notes",
"noteType": "Tipo de nota",
@@ -214,7 +245,23 @@
"switchTypeTitle": "¿Cambiar tipo de nota?",
"switchTypeWarning": "Se puede perder formato al cambiar a {type}.",
"switchTypeContentPreserved": "Tu contenido se preservará como texto plano.",
"switchType": "Cambiar a {type}"
"switchType": "Cambiar a {type}",
"saveNow": "Ahorra ahora",
"backToCollection": "Volver a la colección",
"markdownEditingTitle": "Volver a editar",
"markdownPreviewTitle": "Avance",
"brainstormThisIdea": "Haz una lluvia de ideas sobre esta idea",
"brainstormThisIdeaAria": "Haz una lluvia de ideas sobre esta idea",
"shareNoteTitle": "Compartir nota",
"shareNoteAria": "Compartir nota",
"saveNoteAria": "guardar nota",
"noChangesToSaveAria": "No hay cambios para guardar",
"optionsMenuAria": "Menú de opciones",
"deleteNoteConfirmItem": "Eliminar nota",
"noteDeletedToast": "Nota eliminada.",
"deleteNoteFailedToast": "No se pudo eliminar.",
"documentInfoAria": "Información del documento",
"noModification": "Sin cambios"
},
"pagination": {
"previous": "←",
@@ -296,7 +343,24 @@
"accessRevoked": "El acceso ha sido revocado",
"errorLoading": "Error al cargar colaboradores",
"failedToAdd": "Error al agregar colaborador",
"failedToRemove": "Error al eliminar colaborador"
"failedToRemove": "Error al eliminar colaborador",
"shareCompactTitle": "Compartir",
"inviteByEmailLabel": "Invitar por correo electrónico",
"accessReadCompact": "Vista",
"accessEditCompact": "Editar",
"sendInvitation": "Enviar invitación",
"invitationSentBadge": "Invitación enviada",
"sharedAccessLabel": "Acceso compartido",
"noCollaboratorsEmpty": "Aún no hay colaboradores.",
"removeAccessTitle": "Quitar acceso",
"toastInviteSentTo": "Invitación enviada a {correo electrónico}",
"toastAccessRemoved": "Acceso eliminado para {target}",
"toastUserFallback": "el usuario",
"toastSharingError": "Error al compartir",
"toastEmailNotFound": "No se encontró ninguna cuenta con este correo electrónico.",
"toastAlreadySharedUser": "Esta nota ya está compartida con este usuario.",
"toastRemoveAccessFailed": "No se pudo eliminar el acceso.",
"userFallback": "Usuario"
},
"ai": {
"analyzing": "IA analizando...",
@@ -326,6 +390,8 @@
"transforming": "Transformando...",
"transformSuccess": "¡Texto transformado a Markdown exitosamente!",
"transformError": "Error durante la transformación",
"convertToRichtext": "Convertir a texto enriquecido",
"convertingToRichtext": "Mudado...",
"assistant": "Asistente IA",
"generating": "Generando...",
"generateTitles": "Generar títulos",
@@ -389,6 +455,8 @@
"undoAI": "Deshacer transformación de IA",
"undoApplied": "Texto original restaurado",
"minWordsError": "La nota debe contener al menos 5 palabras para usar acciones de IA.",
"wordCountMin": "Seleccione al menos {min} palabras para reformular (actualmente {current} palabras)",
"wordCountMax": "Seleccione como máximo {max} palabras para reformular (actualmente {current} palabras)",
"genericError": "Error de IA",
"actionError": "Error durante la acción de IA",
"appliedToNote": "Aplicado a la nota",
@@ -404,6 +472,15 @@
"chatTab": "Chat",
"noteActions": "Acciones de nota",
"askToStart": "Pregúntale algo al Asistente para empezar.",
"chatPanelContext": "Contexto",
"chatPanelNotebookPlus": "+ Cuaderno",
"chatPanelWritingTone": "Tono de escritura",
"scopeAutoBadge": "Auto",
"chatNoteQuestionPlaceholder": "Haga una pregunta sobre esta nota...",
"chatNotebookSelectPlaceholder": "Incluye un cuaderno...",
"assistantTabActions": "Comportamiento",
"resourcePreviewAiTitle": "Vista previa de IA",
"resourcePreviewInjectFromChat": "Inyectar desde el chat",
"contextLabel": "Contexto",
"thisNote": "Esta nota",
"allMyNotes": "Todas mis notas",
@@ -415,6 +492,7 @@
"newLineHint": "Shift+Enter = nueva línea",
"resultLabel": "Resultado",
"discardAction": "Descartar",
"organization": "Organización",
"transformationsDesc": "Transformaciones — aplicadas directamente a la nota",
"writeMinWordsAction": "Escribe al menos 5 palabras para activar las acciones de IA.",
"processingAction": "Procesando...",
@@ -425,7 +503,45 @@
"shorten": "Acortar",
"improve": "Mejorar",
"toMarkdown": "A Markdown",
"describeImages": "Describe images"
"describeImages": "Describe images",
"fixGrammar": "Arreglar gramática",
"translate": "Traducir",
"explain": "Explicar",
"toRichText": "Convertir a texto enriquecido"
},
"generate": {
"slides": "Generar diapositivas",
"sectionLabel": "Herramientas de generación",
"theme": "Tema",
"themeArchitecturalMono": "Mono arquitectónico",
"themeVibrantTech": "Tecnología vibrante",
"themeMinimalSilk": "Seda mínima",
"style": "Estilo",
"styleProfessional": "Profesional",
"styleCreative": "Creativo",
"styleBrutalist": "brutalista",
"diagram": "Generar diagrama",
"diagramReadyHint": "Convertir nota en flujo visual",
"diagramType": "Tipo de diagrama",
"typeAuto": "Detección automática",
"typeFlowchart": "Diagrama de flujo",
"typeMindMap": "Mapa mental",
"typeTimeline": "Línea de tiempo",
"typeOrgChart": "organigrama",
"typeArchitecture": "Arquitectura",
"typeProcessMap": "Mapa de procesos",
"styleSketchy": "Incompleto",
"styleSoft": "Suave",
"styleMinimal": "Mínimo",
"styleDraft": "Borrador",
"stylePolished": "Pulido",
"styleHandwritten": "Escrito",
"diagramReady": "¡El diagrama está listo!",
"openInExcalidraw": "Abierto en el laboratorio Excalidraw",
"insertDiagramInNote": "Insertar PNG en la nota actual",
"diagramImageAlt": "Diagrama generado por IA",
"insertedInNote": "Diagrama insertado en nota.",
"insertExportError": "Error al exportar/cargar el diagrama"
},
"openAssistant": "Abrir asistente IA",
"poweredByMomento": "Desarrollado por Momento AI",
@@ -442,7 +558,64 @@
"aiCopilot": "Copiañol IA",
"suggestTitle": "Sugerencia de título por IA",
"generateTitleFromImage": "Generate title from image",
"titleGenerated": "Title generated from image"
"titleGenerated": "Title generated from image",
"resourceTab": "Recurso",
"aiNoteTitle": "Nota de IA",
"injectReplace": "Reemplazar",
"injectReplaceTitle": "Reemplazar el contenido de la nota con este mensaje",
"injectComplete": "Completo",
"injectCompleteTitle": "Nota completa con este mensaje (AI)",
"injectMerge": "Unir",
"injectMergeTitle": "Fusionar con nota (AI)",
"imagesCount": "{contar} imágenes",
"resource": {
"failedToLoadUrl": "No se pudo cargar esta URL",
"pageLoaded": "Página cargada: {título}",
"pageLoadError": "Error al cargar la página",
"pasteOrUrlFirst": "Pegue el texto o cargue una URL primero",
"enrichError": "error de enriquecimiento",
"enrichErrorShort": "error de enriquecimiento",
"contentApplied": "Contenido aplicado a la nota ✓",
"fromChat": "💬 Del chat",
"replacement": "↓ Reemplazo",
"completedByAI": "✦ Completado por IA",
"mergedByAI": "⟳ Fusionado por IA",
"rendered": "Renderizado",
"cancel": "Cancelar",
"applyToNote": "Aplicar a la nota",
"urlLabel": "URL (opcional)",
"resourceText": "Texto de recurso",
"resourcePlaceholder": "Pegue su texto aquí (rebajas, HTML, texto sin formato…)",
"words": "palabras",
"integrationMode": "Modo de integración",
"modeReplace": "Reemplazar",
"modeReplaceDesc": "Directo, sin IA",
"modeComplete": "Completo",
"modeCompleteDesc": "Agrega sin reescribir",
"modeMerge": "Unir",
"modeMergeDesc": "Reescribe e integra",
"aiProcessing": "Procesamiento de IA...",
"preview": "Avance",
"generatePreview": "Generar vista previa",
"emptyNoteHint": "💡 La nota está vacía: el contenido del recurso se integrará directamente."
},
"cancel": "Cancelar",
"copied": "Copiado",
"copy": "Copiar",
"transformations": "Transformaciones",
"otherLanguage": "otro idioma",
"translateNow": "Traducir ahora",
"generationTools": "Herramientas de generación",
"generateSlidesLoading": "⏳ Generando presentación...",
"generateDiagramLoading": "⏳ Generando diagrama...",
"errorShort": "Error",
"readyToast": "¡Listo!",
"downloadFailedToast": "Descarga fallida",
"pptxDownloadButton": "Descargar .pptx",
"presentationReadyBadge": "Presentación lista",
"openInLabTitle": "Abrir en el laboratorio",
"inlineSummaryMarkdown": "**Resumen:**",
"networkErrorShort": "Error de red."
},
"titleSuggestions": {
"available": "Sugerencias de título",
@@ -548,7 +721,19 @@
"untitled": "Sin título",
"notifications": "Notificaciones",
"declined": "Uso compartido rechazado",
"removed": "Nota eliminada de la lista"
"removed": "Nota eliminada de la lista",
"slidesReady": "Presentación lista",
"openSlides": "Presentación abierta",
"canvasReady": "Diagrama listo",
"pptxReady": "Diapositivas listas",
"downloadPptx": "Descargar .pptx",
"markAllRead": "Marcar todo como leído",
"agentSuccess": "Agente terminado",
"agentFailed": "El agente falló",
"brainstormInvite": "Idea genial",
"brainstormJoined": "Idea genial",
"systemNotification": "Sistema",
"downloadFailed": "Descarga fallida"
},
"nav": {
"home": "Inicio",
@@ -597,6 +782,17 @@
"themeLight": "Claro",
"themeDark": "Oscuro",
"themeSystem": "Sistema",
"themeBaseGroup": "Base",
"themePalettesGroup": "Color palettes",
"themeSepia": "Sepia",
"themeMidnight": "Midnight",
"themeRose": "Rose",
"themeGreen": "Green",
"themeLavender": "Lavender",
"themeSand": "Sand",
"themeOcean": "Ocean",
"themeSunset": "Sunset",
"themeBlue": "Blue",
"notifications": "Notificaciones",
"language": "Idioma",
"selectLanguage": "Seleccionar idioma",
@@ -630,17 +826,8 @@
"desktopNotifications": "Notificaciones de escritorio",
"desktopNotificationsDesc": "Recibir notificaciones en el navegador",
"notificationsDesc": "Gestiona tus preferencias de notificaciones",
"themeBaseGroup": "Base",
"themePalettesGroup": "Color palettes",
"themeSepia": "Sepia",
"themeMidnight": "Midnight",
"themeRose": "Rose",
"themeGreen": "Green",
"themeLavender": "Lavender",
"themeSand": "Sand",
"themeOcean": "Ocean",
"themeSunset": "Sunset",
"themeBlue": "Blue"
"autoSave": "Guardar automáticamente",
"autoSaveDesc": "Guarda automáticamente los cambios mientras escribes"
},
"profile": {
"title": "Perfil",
@@ -707,7 +894,15 @@
"providerDesc": "Elige tu proveedor de IA preferido",
"providerAutoDesc": "Ollama si disponible, OpenAI como alternativa",
"providerOllamaDesc": "100% privado, se ejecuta localmente en tu máquina",
"providerOpenAIDesc": "Más preciso, requiere clave API"
"providerOpenAIDesc": "Más preciso, requiere clave API",
"aiNote": "Nota de IA",
"aiNoteDesc": "Habilite el botón de chat de IA y las herramientas de mejora de texto",
"languageDetection": "Detección de idioma",
"languageDetectionDesc": "Detecta automáticamente el idioma de tus notas",
"autoLabeling": "Sugerencias de etiquetas",
"autoLabelingDesc": "Sugiere y aplica etiquetas automáticamente a tus notas",
"noteHistory": "Historial de notas",
"noteHistoryDesc": "Habilitar instantáneas de versiones y restauración desde el Historial"
},
"general": {
"loading": "Cargando...",
@@ -764,7 +959,9 @@
"markDone": "Marcar como completado",
"markUndone": "Marcar como no completado",
"todayAt": "Hoy a las {time}",
"tomorrowAt": "Mañana a las {time}"
"tomorrowAt": "Mañana a las {time}",
"clearCompleted": "Borrar completado",
"viewAll": "Ver todos los recordatorios"
},
"notebook": {
"create": "Crear cuaderno",
@@ -795,7 +992,11 @@
"confidence": "confianza",
"savingReminder": "Error al guardar el recordatorio",
"removingReminder": "Error al eliminar el recordatorio",
"generatingDescription": "Please wait..."
"generatingDescription": "Please wait...",
"pinnedFrozenTooltip": "Cuaderno fijado: pedido congelado",
"organizeNotebookWithAITooltip": "Organiza este cuaderno con IA",
"assistantRequiredForSummarize": "Active AI Assistant en la configuración para resumir",
"createSubnotebook": "Agregar subcuaderno"
},
"notebookSuggestion": {
"title": "¿Mover a {name}?",
@@ -808,6 +1009,9 @@
},
"admin": {
"title": "Panel de administración",
"adminConsole": "Consola de administración",
"navSection": "Navegación",
"backToApp": "Volver a Recuerdo",
"userManagement": "Gestión de usuarios",
"chat": "AI Chat",
"lab": "The Lab",
@@ -850,6 +1054,11 @@
"providerEmbeddingRequired": "AI_PROVIDER_EMBEDDING es requerido",
"providerOllamaOption": "🦙 Ollama (Local & Free)",
"providerOpenAIOption": "🤖 OpenAI (GPT-5, GPT-4)",
"providerAnthropicOption": "🧠 Antrópico (Claude API)",
"providerAnthropicCustomOption": "🧩 Personalizado antrópico (API de mensajes - MiniMax, etc.)",
"anthropicModelHint": "Elija un ID de modelo de Claude de las sugerencias o ingrese uno manualmente (no hay una lista de modelos remotos para la API oficial).",
"anthropicCustomModelHint": "API de mensajes compatible con Anthropic (por ejemplo, MiniMax): URL base https://api.minimax.io/anthropic (China: https://api.minimaxi.com/anthropic), modelo MiniMax-M2.7. Incrustaciones: utilice el proveedor «Personalizado» + URL de OpenAI https://api.minimax.io/v1.",
"anthropicCustomNoModelList": "Esta puerta de enlace no expone una lista de modelos/estilo OpenAI: elija el modelo de las sugerencias o escríbalo (por ejemplo, MiniMax-M2.7).",
"providerCustomOption": "🔧 Custom OpenAI-Compatible",
"providerDeepSeekOption": "🔍 DeepSeek",
"providerOpenRouterOption": "🌐 OpenRouter",
@@ -1003,7 +1212,14 @@
"error": "Error:",
"testError": "Error de prueba: {error}",
"tipTitle": "Consejo:",
"tipDescription": "Usa el panel de pruebas de IA para diagnosticar problemas de configuración antes de probar."
"tipDescription": "Usa el panel de pruebas de IA para diagnosticar problemas de configuración antes de probar.",
"chatTestTitle": "Prueba de asistente de chat",
"chatTestDescription": "Pruebe el proveedor de IA utilizado por el asistente de chat",
"chatGenerationTest": "💬 Prueba de asistente de chat:",
"chatStep1": "Envía un mensaje de prueba al asistente.",
"chatStep2": "Pide una respuesta concisa sobre lo que hace el asistente.",
"chatStep3": "Muestra la respuesta del modelo.",
"chatStep4": "Comprueba la capacidad de respuesta y la latencia."
},
"sidebar": {
"dashboard": "Panel",
@@ -1194,6 +1410,7 @@
"notesViewLabel": "Vista de notas",
"notesViewTabs": "Pestañas (estilo OneNote)",
"notesViewMasonry": "Tarjetas (cuadrícula)",
"notesViewList": "Lista (revista)",
"selectTheme": "Select theme",
"fontFamilyLabel": "Familia de fuentes",
"fontFamilyDescription": "Elige la fuente utilizada en toda la aplicación",
@@ -1277,6 +1494,69 @@
"organizeWithAI": "Organizar con IA",
"organize": "Organizar"
},
"organizeNotebook": {
"title": "organizar cuaderno",
"unknownError": "Error desconocido",
"toastSuccess": "Cuaderno organizado: {created} subcuadernos creados, {moved} notas movidas",
"intro": "AI analizará las notas de este cuaderno y propondrá un plan para reorganizarlas en subcuadernos temáticos.",
"bulletThemes": "Agrupar notas por tema o tema",
"bulletSubfolders": "Crear subcuadernos faltantes",
"bulletPreview": "Vista previa completa antes de cualquier cambio",
"analyzingTitle": "Analizando…",
"analyzingSubtitle": "La IA lee tus notas e identifica temas",
"previewSummary": "{groups} grupo(s) · {notas} notas · {newSubs} nuevo(s) subcuaderno(s)",
"badgeNew": "Nuevo",
"untitledNote": "Nota sin título",
"notesInGroup": "{contar} notas",
"executingTitle": "Organizando…",
"executingSubtitle": "Crear subcuadernos y mover notas",
"doneTitle": "¡Cuaderno organizado!",
"doneStats": "{creado} subcuaderno(s) creado(s) · {movido} nota(s) movida(s)",
"analyzeButton": "Analizar con IA",
"restart": "Empezar de nuevo",
"confirm": "Aplicar",
"closeButton": "Cerca"
},
"documentInfo": {
"tabInfo": "Información",
"tabVersions": "Versiones",
"wordsLabel": "Palabras",
"charactersLabel": "Personajes",
"notebookLabel": "Computadora portátil",
"typeLabel": "Tipo",
"createdLabel": "Creado",
"modifiedLabel": "Actualizado",
"labelsSection": "Etiquetas",
"idLabel": "IDENTIFICACIÓN",
"historyDisabled": "El historial no está habilitado para esta nota.",
"enableHistory": "Habilitar historial",
"savedVersions": "Versiones guardadas",
"savingEllipsis": "Ahorro…",
"versionSaved": "¡Versión guardada!",
"saveThisVersion": "Guarde esta versión",
"loading": "Cargando…",
"noVersion": "Aún no hay versiones",
"restoreTooltip": "Restaurar",
"deleteTooltip": "Borrar",
"comparisonMode": "Modo de comparación",
"comparisonSubtitle": "Comparar versiones una al lado de la otra",
"deleteVersionConfirm": "¿Eliminar esta versión?",
"latestBadge": "El último"
},
"languages": {
"targets": {
"french": "Francés",
"english": "Inglés",
"spanish": "Español",
"german": "Alemán",
"persian": "persa",
"portuguese": "portugués",
"italian": "italiano",
"chinese": "Chino",
"japanese": "japonés"
},
"customPlaceholder": "p.ej. Árabe, ruso…"
},
"common": {
"unknown": "Desconocido",
"notAvailable": "No disponible",
@@ -1398,12 +1678,16 @@
"scraper": "Monitor",
"researcher": "Investigador",
"monitor": "Observador",
"slideGenerator": "Diapositivas",
"excalidrawGenerator": "Diagrama",
"custom": "Personalizado"
},
"typeDescriptions": {
"scraper": "Extrae contenido de múltiples sitios y crea un resumen",
"researcher": "Busca información sobre un tema",
"monitor": "Observa un cuaderno y analiza notas",
"slideGenerator": "Crea una presentación de PowerPoint a partir de notas.",
"excalidrawGenerator": "Crea un diagrama de Excalidraw a partir de notas.",
"custom": "Agente libre con tu propio prompt"
},
"form": {
@@ -1416,6 +1700,27 @@
"urlsOptional": "(opcional)",
"sourceNotebook": "Cuaderno a observar",
"selectNotebook": "Seleccionar un cuaderno...",
"selectNotes": "Notas para analizar",
"notesSelected": "{{count}} nota(s) seleccionada(s)",
"slideTheme": "Tema de presentación",
"slideThemeDefault": "Automático",
"slideStyle": "estilo visual",
"slideStyleSoft": "Suave (recomendado)",
"slideStyleSharp": "Afilado y denso",
"slideStyleRounded": "Redondeado y espacioso",
"slideStylePill": "Prima / Pastilla",
"excalidrawDiagramType": "Tipo de diagrama",
"excalidrawDiagramTypeAuto": "Auto (detección de dominio)",
"excalidrawDiagramTypeFlowchart": "Diagrama de flujo (proceso)",
"excalidrawDiagramTypeMindmap": "Mapa mental (ideas)",
"excalidrawDiagramTypeOrgChart": "Organigrama (equipos)",
"excalidrawDiagramTypeTimeline": "Cronograma/hoja de ruta",
"excalidrawDiagramTypeProcessMap": "Mapa de procesos (operaciones)",
"excalidrawDiagramTypeArchitectureCloud": "Arquitectura de nube (zonas/RG)",
"excalidrawDiagramStyle": "Estilo de diagrama Excalidraw",
"excalidrawDiagramStyleDefault": "Coloreado (Excalidraw)",
"excalidrawDiagramStyleSketchPlus": "Sketch+ (Excalidraw mejorado)",
"excalidrawDiagramStyleAustere": "Austero (mínimo)",
"targetNotebook": "Cuaderno de destino",
"inbox": "Bandeja de entrada",
"instructions": "Instrucciones de IA",
@@ -1485,6 +1790,8 @@
"updated": "Agente actualizado",
"deleted": "\"{name}\" eliminado",
"deleteError": "Error al eliminar",
"running": "Generación en progreso…",
"runningDesc": "La generación puede tardar unos minutos. Puedes navegar libremente.",
"runSuccess": "\"{name}\" ejecutado exitosamente",
"runError": "Error: {error}",
"runFailed": "Ejecución fallida",
@@ -1519,13 +1826,24 @@
"chercheur": {
"name": "Investigador de temas",
"description": "Busca información profunda sobre un tema y crea una nota estructurada con referencias."
},
"slideGenerator": {
"name": "Generador de diapositivas",
"description": "Lee notas de un cuaderno y genera una presentación estructurada automáticamente."
},
"excalidrawGenerator": {
"name": "Generador de diagramas",
"description": "Lee una nota y genera un diagrama visual en Excalidraw Lab."
}
},
"runLog": {
"title": "Historial",
"noHistory": "Aún no hay ejecuciones",
"toolTrace": "{count} llamadas de herramientas",
"step": "Paso {num}"
"step": "Paso {num}",
"clearConfirm": "¿Está seguro de que desea eliminar todo el historial de este agente?",
"cleared": "Historial eliminado",
"clearHistory": "Borrar historial"
},
"tools": {
"title": "Herramientas del Agente",
@@ -1536,6 +1854,9 @@
"noteCreate": "Crear Nota",
"urlFetch": "Obtener URL",
"memorySearch": "Memoria",
"generatePptx": "Diapositivas PPTX",
"generateSlides": "Diapositivas HTML",
"generateExcalidraw": "Diagrama de Excalidraw",
"configNeeded": "configuración",
"selected": "{count} seleccionadas",
"maxSteps": "Iteraciones máximas"
@@ -1547,7 +1868,9 @@
"scraper": "Eres un asistente de monitoreo. Sintetiza artículos de diferentes sitios web en un resumen claro y estructurado.",
"researcher": "Eres un investigador riguroso. Para el tema solicitado, produce una nota de investigación con contexto, puntos clave, debates y referencias.",
"monitor": "Eres un asistente analítico. Analiza las notas proporcionadas y sugiere pistas, referencias y conexiones entre notas.",
"custom": "Eres un asistente útil."
"custom": "Eres un asistente útil.",
"slideGenerator": "Eres un creador de presentaciones. Lea el contenido proporcionado y cree diapositivas estructuradas con títulos, puntos clave y resúmenes.",
"excalidrawGenerator": "Eres un creador de diagramas. Analice el contenido proporcionado y cree un diagrama visual claro y organizado."
},
"help": {
"title": "Guía de Agentes",
@@ -1581,7 +1904,10 @@
"frequency": "Con qué frecuencia se ejecuta el agente automáticamente. Comience con Manual para probar.",
"instructions": "Instrucciones personalizadas que reemplazan el prompt de IA predeterminado. Deje vacío para usar el automático.",
"tools": "Seleccione qué herramientas puede usar el agente. Cada herramienta da una capacidad específica.",
"maxSteps": "Número máximo de ciclos de razonamiento. Más pasos = análisis más profundo pero más lento."
"maxSteps": "Número máximo de ciclos de razonamiento. Más pasos = análisis más profundo pero más lento.",
"selectNotes": "Seleccione notas específicas para analizar. Si no se selecciona ninguno, el agente utilizará todas las notas del cuaderno.",
"slideTheme": "Elija una paleta de colores para la presentación. Automático deja que la IA decida.",
"slideStyle": "El estilo visual afecta el radio de las esquinas, el espaciado y la densidad de la información."
}
}
},
@@ -1631,5 +1957,147 @@
"lab": {
"initializing": "Inicializando espacio",
"loadingIdeas": "Cargando tus ideas..."
},
"richTextEditor": {
"slashHint": "↑↓ navegar · Ingresar insertar · Sección de cambio de pestaña",
"slashLoading": "La IA piensa...",
"slashTabAll": "Todo",
"slashCatBasic": "Bloques básicos",
"slashCatMedia": "Medios de comunicación",
"slashCatFormatting": "Formato",
"slashCatAi": "Nota de IA",
"insertImage": "Insertar imagen",
"imageUrlPlaceholder": "https://ejemplo.com/imagen.png",
"preview": "Avance",
"cancel": "Cancelar",
"insert": "Insertar",
"slashText": "Texto",
"slashTextDesc": "Párrafo sencillo",
"slashH1": "Título 1",
"slashH1Desc": "Encabezado de sección grande",
"slashH2": "Título 2",
"slashH2Desc": "Encabezado de sección media",
"slashH3": "Título 3",
"slashH3Desc": "Encabezado de sección pequeña",
"slashBullet": "Lista de viñetas",
"slashBulletDesc": "lista desordenada",
"slashNumbered": "Lista numerada",
"slashNumberedDesc": "Lista numerada ordenada",
"slashTodo": "Lista de tareas",
"slashTodoDesc": "Tareas de casilla de verificación",
"slashQuote": "Cita",
"slashQuoteDesc": "Capturar una cotización",
"slashCode": "Bloque de código",
"slashCodeDesc": "Fragmento de código",
"slashDivider": "Divisor",
"slashDividerDesc": "Separador horizontal",
"slashTable": "Mesa",
"slashTableDesc": "Insertar una cuadrícula simple",
"slashDiagram": "Diagrama",
"slashDiagramDesc": "Generar un flujo o mapa mental",
"slashSlides": "Presentación",
"slashSlidesDesc": "Genera una hermosa plataforma de diapositivas",
"slashImage": "Imagen",
"slashImageDesc": "Insertar una imagen desde la URL",
"slashAlignLeft": "Alinear a la izquierda",
"slashAlignLeftDesc": "Alinear texto a la izquierda",
"slashAlignCenter": "Centro",
"slashAlignCenterDesc": "Centrar el texto",
"slashAlignRight": "Alinear a la derecha",
"slashAlignRightDesc": "Alinear el texto a la derecha",
"slashSuperscript": "Sobrescrito",
"slashSuperscriptDesc": "Texto encima de la línea base",
"slashSubscript": "Subíndice",
"slashSubscriptDesc": "Texto debajo de la línea base",
"slashClarify": "Aclarar",
"slashClarifyDesc": "Haz el texto más claro",
"slashShorten": "Acortar",
"slashShortenDesc": "condensar el texto",
"slashImprove": "Mejorar",
"slashImproveDesc": "Realza el estilo",
"slashExpand": "Expandir",
"slashExpandDesc": "Elaborar y enriquecer el texto.",
"imageModalTitle": "Insertar imagen",
"imageModalPreview": "Avance",
"imageModalCancel": "Cancelar",
"imageModalInsert": "Insertar",
"imageModalInvalidUrl": "Por favor ingresa una URL válida",
"imageModalLoadFailed": "No se pudo cargar la imagen",
"linkPlaceholder": "Pega o escribe un enlace...",
"bold": "Atrevido",
"italic": "Itálico",
"underline": "Subrayar",
"strike": "Tachado",
"code": "Código",
"highlight": "Destacar",
"superscript": "Sobrescrito",
"subscript": "Subíndice",
"addBlock": "Agregar bloque",
"placeholder": "Escriba '/' para los comandos..."
},
"brainstorm": {
"title": "Waves of Thought",
"subtitle": "Unfold dimensions of potentiality",
"placeholder": "Enter a concept to unfold...",
"generating": "AI is harvesting seeds of thought...",
"newBrainstorm": "New Brainstorm",
"noSessions": "No brainstorms yet",
"startOne": "Start one",
"sessions": "Brainstorms",
"seedLabel": "Seed Idea",
"ideaPromptDetailed": "Ingrese su idea, pregunta o tema para realizar una lluvia de ideas...",
"brainstormThisIdea": "Brainstorm this idea",
"startBrainstorm": "Start Brainstorm",
"spatialMode": "Spatial Exploration Mode",
"wave1": "Wave 1",
"wave2": "Wave 2",
"wave3": "Wave 3",
"export": "Export",
"exporting": "Exporting...",
"wave": "Wave",
"novelty": "Novelty",
"originConnection": "Origin connection",
"linkedNotes": "Linked notes",
"deepen": "Deepen",
"deepening": "Generating...",
"extract": "Create Note",
"converting": "Converting...",
"dismiss": "Not pertinent",
"noteCreated": "Note Created",
"ideas": "ideas",
"cancel": "Cancel",
"delete": "Delete",
"ideaOrigin": "Origin of the idea",
"noNoteLink": "Purely generative idea",
"derived_from": "Derived from",
"opposes": "In opposition with",
"extends": "Extends",
"synthesizes": "Synthesizes",
"transposes": "Transposes",
"none_found": "No note link",
"viewNote": "View note",
"addIdea": "Add idea",
"manualIdeaPrompt": "Title of your idea:",
"invite": "Invite",
"linkCopied": "Invite link copied!",
"activityTitle": "Actividad",
"noActivity": "Aún no hay actividad",
"justNow": "En este momento",
"humanIdea": "Humano",
"aiIdea": "AI",
"respondsTo": "Responde a",
"adding": "Añadiendo...",
"manualIdeaDesc": "Comparte tu idea con el lienzo de lluvia de ideas.",
"manualIdeaTitle": "Título",
"manualIdeaTitlePlaceholder": "Tu idea en pocas palabras...",
"manualIdeaDescLabel": "Descripción (opcional)",
"manualIdeaDescPlaceholder": "Desarrolla tu idea...",
"activity": {
"manual_idea": "añadió una idea",
"wave_generated": "generó una ola",
"joined": "se unió a la sesión",
"idea_dismissed": "descartó una idea",
"invite_created": "creó una invitación"
}
}
}
}

View File

@@ -32,6 +32,8 @@
},
"sidebar": {
"notes": "یادداشت‌ها",
"recent": "اخیر",
"quickNav": "ناوبری سریع",
"reminders": "یادآورها",
"labels": "برچسب‌ها",
"editLabels": "ویرایش برچسب‌ها",
@@ -40,15 +42,35 @@
"noLabelsInNotebook": "هنوز برچسبی در این دفترچه وجود ندارد",
"archive": "بایگانی",
"trash": "زباله‌دان",
"clearFilter": "حذف فیلتر"
"clearFilter": "حذف فیلتر",
"inbox": "صندوق ورودی",
"sharedWithMe": "با من به اشتراک گذاشته شد",
"sortNewest": "اول جدیدترین",
"sortOldest": "اول قدیمی ترین",
"sortAlpha": "A → Z",
"accountMenu": "منوی حساب",
"profile": "نمایه",
"signOut": "از سیستم خارج شوید",
"sortOrder": "ترتیب مرتب سازی",
"freezePinnedNotebook": "پین کردن سفارش نوار کناری نوت بوک",
"unfreezePinnedNotebook": "پین کردن سفارش نوار کناری نوت بوک",
"newSubNotebook": "نوت بوک فرعی جدید",
"renameNotebook": "تغییر نام دهید"
},
"notes": {
"title": "یادداشت‌ها",
"newNote": "یادداشت جدید",
"reorganize": "سازماندهی مجدد یادداشت ها",
"untitled": "بدون عنوان",
"placeholder": "یادداشت بگیرید...",
"markdownPlaceholder": "یادداشت بگیرید... (Markdown پشتیبانی می‌شود)",
"titlePlaceholder": "عنوان",
"noteTypes": {
"richtext": "متن غنی",
"markdown": "مارک داون",
"text": "متن ساده",
"checklist": "چک لیست"
},
"listItem": "آیتم لیست",
"addListItem": "+ آیتم لیست",
"newChecklist": "لیست جدید",
@@ -58,6 +80,7 @@
"confirmDelete": "آیا مطمئن هستید که می‌خواهید این یادداشت را حذف کنید؟",
"confirmLeaveShare": "آیا مطمئن هستید که می‌خواهید این یادداشت اشتراکی را ترک کنید؟",
"sharedBy": "به اشتراک گذاشته توسط",
"sharedShort": "به اشتراک گذاشته شده است",
"leaveShare": "ترک",
"delete": "حذف",
"archive": "بایگانی",
@@ -136,6 +159,8 @@
"dragToReorder": "بکشید تا مرتب کنید",
"more": "بیشتر",
"emptyState": "یادداشتی نیست",
"metadataPanel": "جزئیات",
"metadataNotebook": "دفترچه یادداشت",
"emptyStateTabs": "هنوز یادداشتی اینجا نیست. از «یادداشت جدید» در نوار کناری استفاده کنید (پیشنهادات عنوان هوش مصنوعی در ویرایشگر نمایش داده می‌شود).",
"inNotebook": "در دفترچه",
"moveFailed": "انتقال شکست خورد",
@@ -147,11 +172,6 @@
"unpinned": "سنجاق نشده",
"redoShortcut": "انجام مجدد (Ctrl+Y)",
"undoShortcut": "بازگردانی (Ctrl+Z)",
"viewCards": "نمایش کارتی",
"viewCardsTooltip": "شبکه کارتی با مرتب‌سازی کشیدن و رها کردن",
"viewTabs": "نمایش لیستی",
"viewTabsTooltip": "زبانه‌ها در بالا، یادداشت در پایین — زبانه‌ها را بکشید تا مرتب شوند",
"viewModeGroup": "حالت نمایش یادداشت‌ها",
"reorderTabs": "مرتب‌سازی زبانه",
"modified": "ویرایش شده",
"created": "ایجاد شده",
@@ -160,15 +180,18 @@
"savedStatus": "ذخیره شد",
"dirtyStatus": "تغییر یافته",
"completedLabel": "تکمیل شده",
"notes.emptyNotebook": "دفترچه خالی",
"notes.emptyNotebookDesc": "این دفترچه یادداشتی ندارد. روی + کلیک کنید تا یکی بسازید.",
"notes.noNoteSelected": "یادداشتی انتخاب نشده",
"notes.selectOrCreateNote": ک یادداشت از لیست انتخاب کنید یا یکی جدید بسازید.",
"notes": {
"emptyNotebook": "دفترچه خالی",
"emptyNotebookDesc": "این دفترچه یادداشتی ندارد. روی + کلیک کنید تا یکی بسازید.",
"noNoteSelected": "یادداشتی انتخاب نشده",
"selectOrCreateNote": "یک یادداشت از لیست انتخاب کنید یا یکی جدید بسازید."
},
"commitVersion": "ذخیره نسخه",
"versionSaved": "نسخه ذخیره شد",
"deleteVersion": "حذف این نسخه",
"versionDeleted": "نسخه حذف شد",
"deleteVersionConfirm": "این نسخه برای همیشه حذف شود؟",
"deleteVersionDesc": "این عمل قابل بازگشت نیست. این نسخه برای همیشه از تاریخچه حذف خواهد شد.",
"historyMode": "حالت تاریخچه",
"historyModeManual": "دستی (دکمه ثبت)",
"historyModeAuto": "خودکار (هوشمند)",
@@ -184,6 +207,10 @@
"enableHistory": "فعال‌سازی تاریخچه",
"historyEmpty": "نسخه‌ای موجود نیست",
"historySelectVersion": "نسخه‌ای را برای پیش‌نمایش انتخاب کنید",
"currentVersion": "فعلی",
"compareVersions": "مقایسه",
"diffTitle": "مقایسه",
"diffSelectHint": "برای مقایسه روی ۲ نسخه در لیست کلیک کنید",
"sortBy": "مرتب‌سازی بر اساس",
"sortDateDesc": "تاریخ (جدیدترین)",
"sortDateAsc": "تاریخ (قدیمی‌ترین)",
@@ -197,10 +224,14 @@
"createFailed": "ایجاد یادداشت ناموفق بود",
"updateFailed": "به‌روزرسانی یادداشت ناموفق بود",
"archived": "یادداشت بایگانی شد",
"unarchivedSuccess": "یادداشت از بایگانی حذف شد",
"archiveFailed": "بایگانی ناموفق بود",
"sort": "مرتب‌سازی",
"confirmDeleteTitle": "حذف یادداشت",
"leftShare": "اشتراک‌گذاری حذف شد",
"ideaOrigin": "Origin of the idea",
"noNoteLink": "Purely generative idea",
"dismiss": "Not pertinent",
"dismissed": "یادداشت از اخیرها حذف شد",
"generalNotes": "یادداشت‌های عمومی",
"noteType": "نوع یادداشت",
@@ -215,11 +246,22 @@
"switchTypeWarning": "هنگام تغییر به {type} ممکن است برخی قالب‌بندی‌ها از بین بروند.",
"switchTypeContentPreserved": "محتوای شما به عنوان متن ساده حفظ می‌شود.",
"switchType": "تغییر به {type}",
"deleteVersionDesc": "این عمل قابل بازگشت نیست. این نسخه برای همیشه از تاریخچه حذف خواهد شد.",
"currentVersion": "فعلی",
"compareVersions": "مقایسه",
"diffTitle": "مقایسه",
"diffSelectHint": "برای مقایسه روی ۲ نسخه در لیست کلیک کنید"
"saveNow": "اکنون ذخیره کنید",
"backToCollection": "بازگشت به مجموعه",
"markdownEditingTitle": "بازگشت به ویرایش",
"markdownPreviewTitle": "پیش نمایش",
"brainstormThisIdea": "طوفان فکری این ایده",
"brainstormThisIdeaAria": "طوفان فکری این ایده",
"shareNoteTitle": "یادداشت را به اشتراک بگذارید",
"shareNoteAria": "یادداشت را به اشتراک بگذارید",
"saveNoteAria": "ذخیره یادداشت",
"noChangesToSaveAria": "هیچ تغییری برای ذخیره وجود ندارد",
"optionsMenuAria": "منوی گزینه ها",
"deleteNoteConfirmItem": "حذف یادداشت",
"noteDeletedToast": "یادداشت حذف شد",
"deleteNoteFailedToast": "حذف نشد.",
"documentInfoAria": "اطلاعات سند",
"noModification": "بدون تغییر"
},
"pagination": {
"previous": "←",
@@ -301,7 +343,24 @@
"accessRevoked": "دسترسی لغو شد",
"errorLoading": "خطا در بارگذاری همکاران",
"failedToAdd": "شکست در افزودن همکار",
"failedToRemove": "شکست در حذف همکار"
"failedToRemove": "شکست در حذف همکار",
"shareCompactTitle": "به اشتراک بگذارید",
"inviteByEmailLabel": "از طریق ایمیل دعوت کنید",
"accessReadCompact": "مشاهده کنید",
"accessEditCompact": "ویرایش کنید",
"sendInvitation": "ارسال دعوتنامه",
"invitationSentBadge": "دعوت نامه ارسال شد",
"sharedAccessLabel": "دسترسی مشترک",
"noCollaboratorsEmpty": "هنوز هیچ مشارکتی وجود ندارد.",
"removeAccessTitle": "حذف دسترسی",
"toastInviteSentTo": "دعوت نامه به {email} ارسال شد",
"toastAccessRemoved": "دسترسی برای {target} حذف شد",
"toastUserFallback": "کاربر",
"toastSharingError": "خطای اشتراک گذاری",
"toastEmailNotFound": "هیچ حساب کاربری با این ایمیل پیدا نشد.",
"toastAlreadySharedUser": "این یادداشت قبلاً با این کاربر به اشتراک گذاشته شده است.",
"toastRemoveAccessFailed": "دسترسی حذف نشد.",
"userFallback": "کاربر"
},
"ai": {
"analyzing": "در حال تحلیل هوش مصنوعی...",
@@ -331,6 +390,8 @@
"transforming": "در حال تبدیل...",
"transformSuccess": "متن با موفقیت به مارک‌داون تبدیل شد!",
"transformError": "خطا در تبدیل",
"convertToRichtext": "تبدیل به متن غنی",
"convertingToRichtext": "در حال تبدیل...",
"assistant": "دستیار هوش مصنوعی",
"generating": "در حال تولید...",
"generateTitles": "تولید عناوین",
@@ -394,6 +455,8 @@
"undoAI": "لغو تبدیل هوش مصنوعی",
"undoApplied": "متن اصلی بازگردانده شد",
"minWordsError": "یادداشت باید حداقل ۵ کلمه داشته باشد.",
"wordCountMin": "حداقل {min} کلمه برای بازنویسی انتخاب کنید (فعلاً {current} کلمه)",
"wordCountMax": "حداکثر {max} کلمه برای بازنویسی انتخاب کنید (فعلاً {current} کلمه)",
"genericError": "خطای هوش مصنوعی",
"actionError": "خطا در حین عمل هوش مصنوعی",
"appliedToNote": "در یادداشت اعمال شد",
@@ -409,6 +472,15 @@
"chatTab": "چت",
"noteActions": "عملیات یادداشت",
"askToStart": "برای شروع سوالی از دستیار بپرسید.",
"chatPanelContext": "زمینه",
"chatPanelNotebookPlus": "+ دفترچه یادداشت",
"chatPanelWritingTone": "لحن نوشتن",
"scopeAutoBadge": "خودکار",
"chatNoteQuestionPlaceholder": "در مورد این یادداشت سوال بپرسید...",
"chatNotebookSelectPlaceholder": "شامل یک دفترچه ...",
"assistantTabActions": "اقدامات",
"resourcePreviewAiTitle": "پیش نمایش هوش مصنوعی",
"resourcePreviewInjectFromChat": "تزریق از چت",
"contextLabel": "زمینه",
"thisNote": "این یادداشت",
"allMyNotes": "همه یادداشت‌های من",
@@ -420,6 +492,7 @@
"newLineHint": "Shift+Enter = خط جدید",
"resultLabel": "نتیجه",
"discardAction": "رد کردن",
"organization": "سازمان",
"transformationsDesc": "تبدیل‌ها — مستقیماً در یادداشت اعمال می‌شوند",
"writeMinWordsAction": "حداقل ۵ کلمه بنویسید تا عملیات هوش مصنوعی فعال شود.",
"processingAction": "در حال پردازش...",
@@ -433,7 +506,42 @@
"describeImages": "توصیف تصاویر",
"fixGrammar": "اصلاح گرامر",
"translate": "ترجمه",
"explain": "توضیح"
"explain": "توضیح",
"toRichText": "تبدیل به متن غنی"
},
"generate": {
"slides": "تولید اسلایدها",
"sectionLabel": "ابزارهای نسل",
"theme": "تم",
"themeArchitecturalMono": "مونو معماری",
"themeVibrantTech": "فناوری پر جنب و جوش",
"themeMinimalSilk": "حداقل ابریشم",
"style": "سبک",
"styleProfessional": "حرفه ای",
"styleCreative": "خلاق",
"styleBrutalist": "بروتالیست",
"diagram": "ایجاد نمودار",
"diagramReadyHint": "نت را به جریان بصری تبدیل کنید",
"diagramType": "نوع نمودار",
"typeAuto": "تشخیص خودکار",
"typeFlowchart": "فلوچارت",
"typeMindMap": "نقشه ذهنی",
"typeTimeline": "جدول زمانی",
"typeOrgChart": "نمودار سازمانی",
"typeArchitecture": "معماری",
"typeProcessMap": "نقشه فرآیند",
"styleSketchy": "طرح دار",
"styleSoft": "نرم",
"styleMinimal": "حداقل",
"styleDraft": "پیش نویس",
"stylePolished": "جلا داده شده",
"styleHandwritten": "دست نوشته",
"diagramReady": "نمودار آماده است!",
"openInExcalidraw": "در آزمایشگاه Excalidraw باز کنید",
"insertDiagramInNote": "PNG را در یادداشت فعلی جاسازی کنید",
"diagramImageAlt": "نمودار تولید شده هوش مصنوعی",
"insertedInNote": "نمودار در یادداشت درج شده است",
"insertExportError": "خطا در صادرات/بارگذاری نمودار"
},
"openAssistant": "باز کردن دستیار هوش مصنوعی",
"poweredByMomento": "پشتیبانی شده توسط Momento AI",
@@ -451,8 +559,6 @@
"suggestTitle": "پیشنهاد عنوان با هوش مصنوعی",
"generateTitleFromImage": "تولید عنوان از تصویر",
"titleGenerated": "عنوان از تصویر تولید شد",
"wordCountMin": "حداقل {min} کلمه برای بازنویسی انتخاب کنید (فعلاً {current} کلمه)",
"wordCountMax": "حداکثر {max} کلمه برای بازنویسی انتخاب کنید (فعلاً {current} کلمه)",
"resourceTab": "منبع",
"aiNoteTitle": "یادداشت هوش مصنوعی",
"injectReplace": "جایگزینی",
@@ -492,7 +598,24 @@
"preview": "پیش‌نمایش",
"generatePreview": "تولید پیش‌نمایش",
"emptyNoteHint": "💡 یادداشت خالی است — محتوای منبع مستقیماً یکپارچه خواهد شد."
}
},
"cancel": "لغو کنید",
"copied": "کپی شده",
"copy": "کپی کنید",
"transformations": "تحولات",
"otherLanguage": "زبان دیگر",
"translateNow": "همین الان ترجمه کن",
"generationTools": "ابزارهای نسل",
"generateSlidesLoading": "⏳ تولید ارائه...",
"generateDiagramLoading": "⏳ تولید نمودار...",
"errorShort": "خطا",
"readyToast": "آماده!",
"downloadFailedToast": "دانلود انجام نشد",
"pptxDownloadButton": "دانلود pptx",
"presentationReadyBadge": "ارائه آماده است",
"openInLabTitle": "در آزمایشگاه باز کنید",
"inlineSummaryMarkdown": "**خلاصه:**",
"networkErrorShort": "خطای شبکه"
},
"titleSuggestions": {
"available": "پیشنهادات عنوان",
@@ -598,7 +721,19 @@
"untitled": "بدون عنوان",
"notifications": "اعلان‌ها",
"declined": "اشتراک‌گذاری رد شد",
"removed": "یادداشت از لیست حذف شد"
"removed": "یادداشت از لیست حذف شد",
"slidesReady": "ارائه آماده است",
"openSlides": "ارائه را باز کنید",
"canvasReady": "نمودار آماده است",
"pptxReady": "اسلایدها آماده است",
"downloadPptx": "دانلود pptx",
"markAllRead": "علامت گذاری به عنوان خوانده شده",
"agentSuccess": "نماینده تمام شد",
"agentFailed": "عامل شکست خورد",
"brainstormInvite": "طوفان فکری",
"brainstormJoined": "طوفان فکری",
"systemNotification": "سیستم",
"downloadFailed": "دانلود انجام نشد"
},
"nav": {
"home": "خانه",
@@ -647,6 +782,17 @@
"themeLight": "روشن",
"themeDark": "تاریک",
"themeSystem": "سیستم",
"themeBaseGroup": "Base",
"themePalettesGroup": "Color palettes",
"themeSepia": "Sepia",
"themeMidnight": "Midnight",
"themeRose": "Rose",
"themeGreen": "Green",
"themeLavender": "Lavender",
"themeSand": "Sand",
"themeOcean": "Ocean",
"themeSunset": "Sunset",
"themeBlue": "Blue",
"notifications": "اعلان‌ها",
"language": "زبان",
"selectLanguage": "انتخاب زبان",
@@ -680,17 +826,8 @@
"desktopNotifications": "اعلان‌های مرورگر",
"desktopNotificationsDesc": "دریافت اعلان‌ها در مرورگر",
"notificationsDesc": "مدیریت تنظیمات اعلان",
"themeBaseGroup": "Base",
"themePalettesGroup": "Color palettes",
"themeSepia": "Sepia",
"themeMidnight": "Midnight",
"themeRose": "Rose",
"themeGreen": "Green",
"themeLavender": "Lavender",
"themeSand": "Sand",
"themeOcean": "Ocean",
"themeSunset": "Sunset",
"themeBlue": "Blue"
"autoSave": "ذخیره خودکار",
"autoSaveDesc": "هنگام تایپ کردن، تغییرات را به صورت خودکار ذخیره کنید"
},
"profile": {
"title": "پروفایل",
@@ -855,7 +992,11 @@
"confidence": "اطمینان",
"savingReminder": "شکست در ذخیره یادآوری",
"removingReminder": "شکست در حذف یادآوری",
"generatingDescription": "لطفاً صبر کنید..."
"generatingDescription": "لطفاً صبر کنید...",
"pinnedFrozenTooltip": "نوت بوک پین شده - سفارش منجمد شده",
"organizeNotebookWithAITooltip": "این نوت بوک را با هوش مصنوعی سازماندهی کنید",
"assistantRequiredForSummarize": "برای خلاصه کردن، دستیار هوش مصنوعی را در تنظیمات روشن کنید",
"createSubnotebook": "اضافه کردن نوت بوک فرعی"
},
"notebookSuggestion": {
"title": "انتقال به {name}؟",
@@ -868,6 +1009,9 @@
},
"admin": {
"title": "داشبورد مدیریت",
"adminConsole": "کنسول مدیریت",
"navSection": "ناوبری",
"backToApp": "بازگشت به Memento",
"userManagement": "مدیریت کاربران",
"chat": "چت هوش مصنوعی",
"lab": "آزمایشگاه",
@@ -910,6 +1054,11 @@
"providerEmbeddingRequired": "AI_PROVIDER_EMBEDDING الزامی است",
"providerOllamaOption": "🦙 Ollama (محلی و رایگان)",
"providerOpenAIOption": "🤖 OpenAI (GPT-5, GPT-4)",
"providerAnthropicOption": "🧠 Anthropic (Claude API)",
"providerAnthropicCustomOption": "🧩 سفارشی Anthropic (Messages API — MiniMax و غیره)",
"anthropicModelHint": "شناسه مدل Claude را از میان پیشنهادات انتخاب کنید یا به صورت دستی وارد کنید (لیست مدل از راه دور برای API رسمی وجود ندارد).",
"anthropicCustomModelHint": "API پیام‌های سازگار با Anthropic (به عنوان مثال MiniMax): URL پایه https://api.minimax.io/anthropic (چین: https://api.minimaxi.com/anthropic)، مدل MiniMax-M2.7. جاسازی‌ها: از ارائه‌دهنده «سفارشی» + URL OpenAI https://api.minimax.io/v1 استفاده کنید.",
"anthropicCustomNoModelList": "این دروازه یک لیست / مدل‌های به سبک OpenAI را نشان نمی‌دهد - مدل را از پیشنهادات انتخاب کنید یا آن را تایپ کنید (به عنوان مثال MiniMax-M2.7).",
"providerCustomOption": "🔧 سفارشی سازگار با OpenAI",
"providerDeepSeekOption": "🔍 DeepSeek",
"providerOpenRouterOption": "🌐 OpenRouter",
@@ -1063,7 +1212,14 @@
"error": "خطا:",
"testError": "خطای تست: {error}",
"tipTitle": "نکته:",
"tipDescription": "قبل از تست از پنل تست هوش مصنوعی برای تشخیص مشکلات پیکربندی استفاده کنید."
"tipDescription": "قبل از تست از پنل تست هوش مصنوعی برای تشخیص مشکلات پیکربندی استفاده کنید.",
"chatTestTitle": "تست دستیار چت",
"chatTestDescription": "ارائه دهنده هوش مصنوعی مورد استفاده دستیار چت را آزمایش کنید",
"chatGenerationTest": "💬 تست دستیار چت:",
"chatStep1": "یک پیام آزمایشی برای دستیار ارسال می کند",
"chatStep2": "در مورد کارهایی که دستیار انجام می دهد، پاسخی مختصر می خواهد",
"chatStep3": "پاسخ مدل را نشان می دهد",
"chatStep4": "پاسخگویی و تأخیر را بررسی می کند"
},
"sidebar": {
"dashboard": "داشبورد",
@@ -1254,6 +1410,7 @@
"notesViewLabel": "چیدمان یادداشت‌ها",
"notesViewTabs": "زبانه‌ها (سبک OneNote)",
"notesViewMasonry": "کارت‌ها (شبکه‌ای)",
"notesViewList": "فهرست (مجله)",
"selectTheme": "انتخاب تم",
"fontFamilyLabel": "خانواده فونت",
"fontFamilyDescription": "فونت استفاده شده در سراسر برنامه را انتخاب کنید",
@@ -1337,6 +1494,69 @@
"organizeWithAI": "سازماندهی با هوش مصنوعی",
"organize": "سازماندهی"
},
"organizeNotebook": {
"title": "نوت بوک را سازماندهی کنید",
"unknownError": "خطای ناشناخته",
"toastSuccess": "نوت بوک سازماندهی شده — {created} نوت بوک(های) فرعی ایجاد شده است، {Mobiled} یادداشت(ها) منتقل شده است",
"intro": "هوش مصنوعی یادداشت‌های موجود در این دفترچه را تجزیه و تحلیل می‌کند و طرحی را برای سازماندهی مجدد آن‌ها در دفترچه‌های فرعی موضوعی پیشنهاد می‌کند.",
"bulletThemes": "یادداشت ها را بر اساس موضوع یا موضوع گروه بندی کنید",
"bulletSubfolders": "نوت بوک های فرعی از دست رفته را ایجاد کنید",
"bulletPreview": "پیش نمایش کامل قبل از هر تغییری",
"analyzingTitle": "در حال تجزیه و تحلیل…",
"analyzingSubtitle": "هوش مصنوعی یادداشت های شما را می خواند و مضامین را شناسایی می کند",
"previewSummary": "{groups} گروه(ها) · {notes} یادداشت · {newSubs} دفتر(های) فرعی جدید",
"badgeNew": "جدید",
"untitledNote": "یادداشت بدون عنوان",
"notesInGroup": "{count} یادداشت",
"executingTitle": "سازماندهی…",
"executingSubtitle": "ایجاد دفترچه های فرعی و جابجایی یادداشت ها",
"doneTitle": "نوت بوک سازمان یافته!",
"doneStats": "{created} دفتر(های) فرعی ایجاد شد · {انتقال} یادداشت(ها) منتقل شد",
"analyzeButton": "با هوش مصنوعی تحلیل کنید",
"restart": "از نو شروع کن",
"confirm": "درخواست کنید",
"closeButton": "بستن"
},
"documentInfo": {
"tabInfo": "اطلاعات",
"tabVersions": "نسخه ها",
"wordsLabel": "کلمات",
"charactersLabel": "شخصیت ها",
"notebookLabel": "دفترچه یادداشت",
"typeLabel": "تایپ کنید",
"createdLabel": "ایجاد شد",
"modifiedLabel": "به روز شد",
"labelsSection": "برچسب ها",
"idLabel": "شناسه",
"historyDisabled": "سابقه برای این یادداشت فعال نیست.",
"enableHistory": "فعال کردن سابقه",
"savedVersions": "نسخه های ذخیره شده",
"savingEllipsis": "در حال ذخیره…",
"versionSaved": "نسخه ذخیره شد!",
"saveThisVersion": "این نسخه را ذخیره کنید",
"loading": "در حال بارگیری…",
"noVersion": "هنوز نسخه ای وجود ندارد",
"restoreTooltip": "بازیابی کنید",
"deleteTooltip": "حذف کنید",
"comparisonMode": "حالت مقایسه",
"comparisonSubtitle": "نسخه ها را در کنار هم مقایسه کنید",
"deleteVersionConfirm": "این نسخه حذف شود؟",
"latestBadge": "آخرین"
},
"languages": {
"targets": {
"french": "فرانسوی",
"english": "انگلیسی",
"spanish": "اسپانیایی",
"german": "آلمانی",
"persian": "فارسی",
"portuguese": "پرتغالی",
"italian": "ایتالیایی",
"chinese": "چینی",
"japanese": "ژاپنی"
},
"customPlaceholder": "به عنوان مثال عربی، روسی…"
},
"common": {
"unknown": "نامشخص",
"notAvailable": "در دسترس نیست",
@@ -1458,12 +1678,16 @@
"scraper": "پایشگر",
"researcher": "پژوهشگر",
"monitor": "ناظر",
"slideGenerator": "اسلایدها",
"excalidrawGenerator": "نمودار",
"custom": "سفارشی"
},
"typeDescriptions": {
"scraper": "چندین سایت را استخراج و خلاصه‌ای ایجاد می‌کند",
"researcher": "اطلاعاتی درباره یک موضوع جستجو می‌کند",
"monitor": "یک دفترچه را نظارت و یادداشت‌ها را تحلیل می‌کند",
"slideGenerator": "یک ارائه پاورپوینت از یادداشت ها ایجاد می کند",
"excalidrawGenerator": "یک نمودار Excalidraw از یادداشت ها ایجاد می کند",
"custom": "عامل آزاد با دستور سفارشی شما"
},
"form": {
@@ -1476,6 +1700,27 @@
"urlsOptional": "(اختیاری)",
"sourceNotebook": "دفترچه برای نظارت",
"selectNotebook": "یک دفترچه انتخاب کنید...",
"selectNotes": "یادداشت هایی برای تجزیه و تحلیل",
"notesSelected": "{{count}} یادداشت انتخاب شد",
"slideTheme": "موضوع ارائه",
"slideThemeDefault": "خودکار",
"slideStyle": "سبک بصری",
"slideStyleSoft": "نرم (توصیه می شود)",
"slideStyleSharp": "تیز و متراکم",
"slideStyleRounded": "گرد و جادار",
"slideStylePill": "حق بیمه / قرص",
"excalidrawDiagramType": "نوع نمودار",
"excalidrawDiagramTypeAuto": "خودکار (تشخیص دامنه)",
"excalidrawDiagramTypeFlowchart": "فلوچارت (فرایند)",
"excalidrawDiagramTypeMindmap": "نقشه ذهنی (ایده ها)",
"excalidrawDiagramTypeOrgChart": "نمودار سازمانی (تیم ها)",
"excalidrawDiagramTypeTimeline": "جدول زمانی / نقشه راه",
"excalidrawDiagramTypeProcessMap": "نقشه فرآیند (عملیات)",
"excalidrawDiagramTypeArchitectureCloud": "معماری ابری (مناطق/RG)",
"excalidrawDiagramStyle": "سبک نمودار Excalidraw",
"excalidrawDiagramStyleDefault": "رنگی (Excalidraw)",
"excalidrawDiagramStyleSketchPlus": "Sketch+ (Excalidraw پیشرفته)",
"excalidrawDiagramStyleAustere": "سخت (حداقل)",
"targetNotebook": "دفترچه مقصد",
"inbox": "صندوق ورودی",
"instructions": "دستورات هوش مصنوعی",
@@ -1545,6 +1790,8 @@
"updated": "عامل به‌روزرسانی شد",
"deleted": "\"{name}\" حذف شد",
"deleteError": "خطا در حذف",
"running": "نسل در حال پیشرفت…",
"runningDesc": "تولید ممکن است چند دقیقه طول بکشد. می توانید آزادانه پیمایش کنید.",
"runSuccess": "\"{name}\" با موفقیت اجرا شد",
"runError": "خطا: {error}",
"runFailed": "اجرای ناموفق",
@@ -1579,13 +1826,24 @@
"chercheur": {
"name": "پژوهشگر موضوع",
"description": "اطلاعات عمیق درباره یک موضوع جستجو و یادداشت ساختاریافته با منابع ایجاد می‌کند."
},
"slideGenerator": {
"name": "ژنراتور اسلاید",
"description": "یادداشت ها را از یک نوت بوک می خواند و به صورت خودکار یک ارائه ساختاریافته ایجاد می کند."
},
"excalidrawGenerator": {
"name": "ژنراتور نمودار",
"description": "یک یادداشت را می خواند و یک نمودار بصری در آزمایشگاه Excalidraw ایجاد می کند."
}
},
"runLog": {
"title": "تاریخچه",
"noHistory": "هنوز سابقه اجرایی وجود ندارد",
"toolTrace": "{count} فراخوانی ابزار",
"step": "مرحله {num}"
"step": "مرحله {num}",
"clearConfirm": "آیا مطمئن هستید که می خواهید تمام سابقه این نماینده را حذف کنید؟",
"cleared": "تاریخچه حذف شد",
"clearHistory": "پاک کردن تاریخچه"
},
"tools": {
"title": "ابزارهای عامل",
@@ -1596,6 +1854,9 @@
"noteCreate": "ایجاد یادداشت",
"urlFetch": "دریافت URL",
"memorySearch": "حافظه",
"generatePptx": "اسلایدهای PPTX",
"generateSlides": "اسلایدهای HTML",
"generateExcalidraw": "نمودار Excalidraw",
"configNeeded": "پیکربندی",
"selected": "{count} انتخاب شده",
"maxSteps": "حداکثر تکرار"
@@ -1607,7 +1868,9 @@
"scraper": "شما یک دستیار پایش هستید. مقالات وب‌سایت‌های مختلف را در یک خلاصه واضح و ساختاریافته ترکیب کنید.",
"researcher": "شما یک پژوهشگر دقیق هستید. برای موضوع درخواستی، یک یادداشت تحقیقاتی با زمینه، نکات کلیدی، بحث‌ها و منابع تولید کنید.",
"monitor": "شما یک دستیار تحلیلی هستید. یادداشت‌های ارائه شده را تحلیل و سرنخ‌ها، منابع و ارتباطات بین یادداشت‌ها را پیشنهاد دهید.",
"custom": "شما یک دستیار مفید هستید."
"custom": "شما یک دستیار مفید هستید.",
"slideGenerator": "شما یک خالق ارائه هستید. مطالب ارائه شده را بخوانید و اسلایدهای ساختاریافته با عنوان، نکات کلیدی و خلاصه ایجاد کنید.",
"excalidrawGenerator": "شما یک خالق نمودار هستید. محتوای ارائه شده را تجزیه و تحلیل کنید و یک نمودار بصری واضح و منظم ایجاد کنید."
},
"help": {
"title": "راهنمای عامل‌ها",
@@ -1641,7 +1904,10 @@
"frequency": "هر چند وقت یکبار عامل به صورت خودکار اجرا می‌شود. برای آزمایش با دستی شروع کنید.",
"instructions": "دستورالعمل‌های سفارشی که جایگزین دستور پیش‌فرض هوش مصنوعی می‌شوند. برای استفاده از خودکار خالی بگذارید.",
"tools": "ابزارهایی که عامل می‌تواند استفاده کند را انتخاب کنید. هر ابزار قابلیت خاصی به عامل می‌دهد.",
"maxSteps": "حداکثر چرخه‌های استدلال. مراحل بیشتر = تحلیل عمیق‌تر اما زمان بیشتر."
"maxSteps": "حداکثر چرخه‌های استدلال. مراحل بیشتر = تحلیل عمیق‌تر اما زمان بیشتر.",
"selectNotes": "یادداشت های خاصی را برای تجزیه و تحلیل انتخاب کنید. اگر هیچ کدام انتخاب نشد، نماینده از تمام یادداشت‌های دفترچه یادداشت استفاده می‌کند.",
"slideTheme": "یک پالت رنگی برای ارائه انتخاب کنید. خودکار به هوش مصنوعی اجازه می دهد تصمیم بگیرد.",
"slideStyle": "سبک بصری بر شعاع گوشه، فاصله و تراکم اطلاعات تأثیر می گذارد."
}
}
},
@@ -1725,6 +1991,12 @@
"slashCodeDesc": "قطعه کد",
"slashDivider": "جداکننده",
"slashDividerDesc": "جداکننده افقی",
"slashTable": "جدول",
"slashTableDesc": "یک شبکه ساده وارد کنید",
"slashDiagram": "نمودار",
"slashDiagramDesc": "یک جریان یا نقشه ذهنی ایجاد کنید",
"slashSlides": "ارائه",
"slashSlidesDesc": "یک عرشه اسلاید زیبا ایجاد کنید",
"slashImage": "تصویر",
"slashImageDesc": "درج تصویر از URL",
"slashAlignLeft": "تراز چپ",
@@ -1762,5 +2034,70 @@
"subscript": "زیرنویس",
"addBlock": "افزودن بلوک",
"placeholder": "برای دستورات '/' تایپ کنید..."
},
"brainstorm": {
"title": "Waves of Thought",
"subtitle": "Unfold dimensions of potentiality",
"placeholder": "Enter a concept to unfold...",
"generating": "AI is harvesting seeds of thought...",
"newBrainstorm": "New Brainstorm",
"noSessions": "No brainstorms yet",
"startOne": "Start one",
"sessions": "Brainstorms",
"seedLabel": "Seed Idea",
"ideaPromptDetailed": "ایده، سوال یا موضوع خود را برای طوفان فکری وارد کنید...",
"brainstormThisIdea": "Brainstorm this idea",
"startBrainstorm": "Start Brainstorm",
"spatialMode": "Spatial Exploration Mode",
"wave1": "Wave 1",
"wave2": "Wave 2",
"wave3": "Wave 3",
"export": "Export",
"exporting": "Exporting...",
"wave": "Wave",
"novelty": "Novelty",
"originConnection": "Origin connection",
"linkedNotes": "Linked notes",
"deepen": "Deepen",
"deepening": "Generating...",
"extract": "Create Note",
"converting": "Converting...",
"dismiss": "Not pertinent",
"noteCreated": "Note Created",
"ideas": "ideas",
"cancel": "Cancel",
"delete": "Delete",
"ideaOrigin": "Origin of the idea",
"noNoteLink": "Purely generative idea",
"derived_from": "Derived from",
"opposes": "In opposition with",
"extends": "Extends",
"synthesizes": "Synthesizes",
"transposes": "Transposes",
"none_found": "No note link",
"viewNote": "View note",
"addIdea": "Add idea",
"manualIdeaPrompt": "Title of your idea:",
"invite": "Invite",
"linkCopied": "Invite link copied!",
"activityTitle": "فعالیت",
"noActivity": "هنوز هیچ فعالیتی وجود ندارد",
"justNow": "همین الان",
"humanIdea": "انسان",
"aiIdea": "هوش مصنوعی",
"respondsTo": "پاسخ می دهد",
"adding": "در حال افزودن...",
"manualIdeaDesc": "ایده خود را با بوم طوفان فکری به اشتراک بگذارید",
"manualIdeaTitle": "عنوان",
"manualIdeaTitlePlaceholder": "ایده شما در چند کلمه ...",
"manualIdeaDescLabel": "توضیحات (اختیاری)",
"manualIdeaDescPlaceholder": "در مورد ایده خود توضیح دهید ...",
"activity": {
"manual_idea": "یک ایده اضافه کرد",
"wave_generated": "موجی ایجاد کرد",
"joined": "به جلسه پیوست",
"idea_dismissed": "یک ایده را رد کرد",
"invite_created": "دعوت ایجاد کرد"
}
}
}
}

View File

@@ -2,7 +2,7 @@
"auth": {
"signIn": "Connexion",
"signUp": "S'inscrire",
"email": "Email",
"email": "Courriel",
"password": "Mot de passe",
"name": "Nom",
"emailPlaceholder": "Entrez votre adresse email",
@@ -47,11 +47,15 @@
"sharedWithMe": "Partagées avec moi",
"sortNewest": "Plus récentes",
"sortOldest": "Plus anciennes",
"sortAlpha": "A → Z",
"sortAlpha": "Tri A → Z",
"accountMenu": "Menu du compte",
"profile": "Profil",
"signOut": "Se déconnecter",
"sortOrder": "Ordre de tri"
"sortOrder": "Ordre de tri",
"freezePinnedNotebook": "Figer l'état du carnet",
"unfreezePinnedNotebook": "Défiger l'état du carnet",
"newSubNotebook": "Nouveau sous-carnet",
"renameNotebook": "Renommer"
},
"notes": {
"title": "Notes",
@@ -61,6 +65,12 @@
"placeholder": "Prenez une note...",
"markdownPlaceholder": "Prenez une note... (Markdown supporté)",
"titlePlaceholder": "Titre",
"noteTypes": {
"richtext": "Texte enrichi",
"markdown": "Markdown",
"text": "Texte brut",
"checklist": "Liste de tâches"
},
"listItem": "Élément de liste",
"addListItem": "+ Élément de liste",
"newChecklist": "Nouvelle checklist",
@@ -170,10 +180,12 @@
"savedStatus": "Enregistré",
"dirtyStatus": "Modifié",
"completedLabel": "Terminé",
"notes.emptyNotebook": "Carnet vide",
"notes.emptyNotebookDesc": "Ce carnet n'a pas de notes. Cliquez sur + pour en créer une.",
"notes.noNoteSelected": "Aucune note sélectionnée",
"notes.selectOrCreateNote": "Sélectionnez une note dans la liste ou créez-en une nouvelle.",
"notes": {
"emptyNotebook": "Carnet vide",
"emptyNotebookDesc": "Ce carnet n'a pas de notes. Cliquez sur + pour en créer une.",
"noNoteSelected": "Aucune note sélectionnée",
"selectOrCreateNote": "Sélectionnez une note dans la liste ou créez-en une nouvelle."
},
"commitVersion": "Enregistrer la version",
"versionSaved": "Version enregistrée",
"deleteVersion": "Supprimer cette version",
@@ -217,6 +229,9 @@
"sort": "Trier",
"confirmDeleteTitle": "Supprimer la note",
"leftShare": "Partage retiré",
"ideaOrigin": "Origine de l'idée",
"noNoteLink": "Idée purement générative",
"dismiss": "Pas pertinent",
"dismissed": "Note retirée des récentes",
"generalNotes": "Notes générales",
"noteType": "Type de note",
@@ -231,11 +246,26 @@
"switchTypeWarning": "Certaines mises en forme peuvent être perdues lors du passage en {type}.",
"switchTypeContentPreserved": "Votre contenu sera préservé en texte brut.",
"switchType": "Passer en {type}",
"saveNow": "Enregistrer maintenant"
"saveNow": "Enregistrer maintenant",
"backToCollection": "Retour à la collection",
"markdownEditingTitle": "Revenir à l'édition",
"markdownPreviewTitle": "Aperçu",
"brainstormThisIdea": "Brainstormer cette idée",
"brainstormThisIdeaAria": "Brainstormer cette idée",
"shareNoteTitle": "Partager la note",
"shareNoteAria": "Partager la note",
"saveNoteAria": "Enregistrer la note",
"noChangesToSaveAria": "Aucune modification à enregistrer",
"optionsMenuAria": "Menu des options",
"deleteNoteConfirmItem": "Supprimer la note",
"noteDeletedToast": "Note supprimée.",
"deleteNoteFailedToast": "Impossible de supprimer.",
"documentInfoAria": "Informations du document",
"noModification": "Aucune modification"
},
"pagination": {
"previous": "←",
"pageInfo": "Page {currentPage} / {totalPages}",
"pageInfo": "Page {currentPage} sur {totalPages}",
"next": "→"
},
"labels": {
@@ -313,7 +343,24 @@
"accessRevoked": "L'accès a été révoqué",
"errorLoading": "Erreur lors du chargement des collaborateurs",
"failedToAdd": "Échec de l'ajout du collaborateur",
"failedToRemove": "Échec de la suppression du collaborateur"
"failedToRemove": "Échec de la suppression du collaborateur",
"shareCompactTitle": "Partager",
"inviteByEmailLabel": "Inviter par e-mail",
"accessReadCompact": "Lire",
"accessEditCompact": "Éditer",
"sendInvitation": "Envoyer l'invitation",
"invitationSentBadge": "Invitation envoyée",
"sharedAccessLabel": "Accès partagé",
"noCollaboratorsEmpty": "Aucun collaborateur pour l'instant.",
"removeAccessTitle": "Retirer l'accès",
"toastInviteSentTo": "Invitation envoyée à {email}",
"toastAccessRemoved": "Accès retiré pour {target}",
"toastUserFallback": "l'utilisateur",
"toastSharingError": "Erreur lors du partage",
"toastEmailNotFound": "Aucun compte trouvé avec cet e-mail.",
"toastAlreadySharedUser": "Cette note est déjà partagée avec cet utilisateur.",
"toastRemoveAccessFailed": "Impossible de retirer l'accès.",
"userFallback": "Utilisateur"
},
"ai": {
"analyzing": "Analyse IA en cours...",
@@ -415,10 +462,6 @@
"appliedToNote": "Appliqué à la note",
"applyToNote": "Appliquer à la note",
"undoLastAction": "Annuler la dernière action IA",
"transformations": "Transformations",
"otherLanguage": "Autre langue",
"translateNow": "Traduire maintenant",
"generationTools": "Outils de génération",
"selectContext": "Sélectionner le contexte...",
"selectNotebook": "Sélectionner un carnet",
"chatPlaceholder": "Demandez à l'IA de modifier, résumer ou rédiger...",
@@ -429,6 +472,15 @@
"chatTab": "Discussion",
"noteActions": "Actions sur la note",
"askToStart": "Posez une question à l'Assistant pour commencer.",
"chatPanelContext": "Contexte",
"chatPanelNotebookPlus": "+ Carnet",
"chatPanelWritingTone": "Ton d'écriture",
"scopeAutoBadge": "Auto",
"chatNoteQuestionPlaceholder": "Posez votre question sur cette note...",
"chatNotebookSelectPlaceholder": "Inclure un carnet...",
"assistantTabActions": "Actions",
"resourcePreviewAiTitle": "Aperçu IA",
"resourcePreviewInjectFromChat": "Injecter depuis Discussion",
"contextLabel": "Contexte",
"thisNote": "Cette note",
"allMyNotes": "Toutes mes notes",
@@ -440,6 +492,7 @@
"newLineHint": "Maj+Entrée = nouvelle ligne",
"resultLabel": "Résultat",
"discardAction": "Ignorer",
"organization": "Organisation",
"transformationsDesc": "Transformations — appliquées directement à la note",
"writeMinWordsAction": "Écrivez au moins 5 mots pour activer les actions IA.",
"processingAction": "Traitement en cours...",
@@ -450,21 +503,23 @@
"shorten": "Raccourcir",
"improve": "Améliorer",
"toMarkdown": "Convertir en Markdown",
"toRichText": "Convertir en texte enrichi",
"describeImages": "Décrire les images",
"fixGrammar": "Corriger les fautes",
"translate": "Traduire",
"explain": "Expliquer"
"explain": "Expliquer",
"toRichText": "Convertir en texte enrichi"
},
"generate": {
"slides": "Générer Slides",
"sectionLabel": "Outils de Génération",
"theme": "Thème",
"themeArchitecturalMono": "Architectural Mono",
"themeVibrantTech": "Vibrant Tech",
"themeMinimalSilk": "Minimal Silk",
"themeVibrantTech": "Tech vibrant",
"themeMinimalSilk": "Soie minimaliste",
"style": "Style",
"styleProfessional": "Professionnel",
"styleCreative": "Créatif",
"styleBrutalist": "Brutaliste",
"diagram": "Générer Diagramme",
"diagramReadyHint": "Convertir en flux visuel",
"diagramType": "Type de Diagramme",
@@ -499,7 +554,7 @@
"discussionContextLabel": "Contexte de discussion",
"webSearchNotConfigured": "Recherche web (non configurée)",
"historyTab": "Historique",
"insightsTab": "Insights",
"insightsTab": "Synthèses",
"aiCopilot": "Copilote IA",
"suggestTitle": "Suggestion de titre IA",
"generateTitleFromImage": "Générer un titre à partir de l'image",
@@ -546,84 +601,21 @@
},
"cancel": "Annuler",
"copied": "Copié",
"copy": "Copier"
},
"richTextEditor": {
"bold": "Gras",
"italic": "Italique",
"underline": "Souligné",
"strike": "Barré",
"code": "Code",
"highlight": "Surligner",
"superscript": "Exposant",
"subscript": "Indice",
"addBlock": "Ajouter un bloc",
"placeholder": "Tapez '/' pour voir les commandes...",
"slashHint": "↑↓ naviguer · Entrée insérer · Tab changer de section",
"slashLoading": "IA Note réfléchit...",
"slashTabAll": "Tout",
"slashCatBasic": "Blocs de base",
"slashCatMedia": "Médias",
"slashCatFormatting": "Mise en forme",
"slashCatAi": "IA Note",
"insertImage": "Insérer une image",
"imageUrlPlaceholder": "https://exemple.com/image.png",
"preview": "Aperçu",
"cancel": "Annuler",
"insert": "Insérer",
"slashText": "Texte",
"slashTextDesc": "Paragraphe simple",
"slashH1": "Titre 1",
"slashH1Desc": "Grand titre de section",
"slashH2": "Titre 2",
"slashH2Desc": "Titre de section moyen",
"slashH3": "Titre 3",
"slashH3Desc": "Petit titre de section",
"slashBullet": "Liste à puces",
"slashBulletDesc": "Liste non ordonnée",
"slashNumbered": "Liste numérotée",
"slashNumberedDesc": "Liste ordonnée numérotée",
"slashTodo": "Liste de tâches",
"slashTodoDesc": "Cases à cocher pour les tâches",
"slashQuote": "Citation",
"slashQuoteDesc": "Capturer une citation",
"slashCode": "Bloc de code",
"slashCodeDesc": "Extrait de code",
"slashDivider": "Séparateur",
"slashDividerDesc": "Séparateur horizontal",
"slashTable": "Tableau",
"slashTableDesc": "Insérer un tableau simple",
"slashDiagram": "Diagramme",
"slashDiagramDesc": "Générer un flux ou une carte mentale",
"slashSlides": "Présentation",
"slashSlidesDesc": "Générer un jeu de diapositives",
"slashImage": "Image",
"slashImageDesc": "Intégrer une image depuis une URL",
"slashAlignLeft": "Aligner à gauche",
"slashAlignLeftDesc": "Aligner le texte à gauche",
"slashAlignCenter": "Centrer",
"slashAlignCenterDesc": "Centrer le texte",
"slashAlignRight": "Aligner à droite",
"slashAlignRightDesc": "Aligner le texte à droite",
"slashSuperscript": "Exposant",
"slashSuperscriptDesc": "Sélectionner du texte d'abord",
"slashSubscript": "Indice",
"slashSubscriptDesc": "Sélectionner du texte d'abord",
"slashClarify": "Clarifier",
"slashClarifyDesc": "Rendre le texte plus clair",
"slashShorten": "Raccourcir",
"slashShortenDesc": "Condenser le texte",
"slashImprove": "Améliorer",
"slashImproveDesc": "Améliorer le style",
"slashExpand": "Développer",
"slashExpandDesc": "Élaborer et enrichir le texte",
"imageModalTitle": "Insérer une image",
"imageModalPreview": "Aperçu",
"imageModalCancel": "Annuler",
"imageModalInsert": "Insérer",
"imageModalInvalidUrl": "Veuillez entrer une URL valide",
"imageModalLoadFailed": "Échec du chargement de l'image",
"linkPlaceholder": "Collez ou tapez un lien..."
"copy": "Copier",
"transformations": "Transformations",
"otherLanguage": "Autre langue",
"translateNow": "Traduire maintenant",
"generationTools": "Outils de génération",
"generateSlidesLoading": "⏳ Génération de la présentation...",
"generateDiagramLoading": "⏳ Génération du diagramme...",
"errorShort": "Erreur",
"readyToast": "Prêt !",
"downloadFailedToast": "Échec du téléchargement",
"pptxDownloadButton": "Télécharger .pptx",
"presentationReadyBadge": "Présentation prête",
"openInLabTitle": "Ouvrir dans le Lab",
"inlineSummaryMarkdown": "**Résumé :**",
"networkErrorShort": "Erreur réseau."
},
"titleSuggestions": {
"available": "Suggestions de titre",
@@ -734,7 +726,14 @@
"openSlides": "Ouvrir la présentation",
"canvasReady": "Diagramme prêt",
"pptxReady": "Slides prêts",
"downloadPptx": "Télécharger .pptx"
"downloadPptx": "Télécharger .pptx",
"markAllRead": "Tout marquer comme lu",
"agentSuccess": "Agent terminé",
"agentFailed": "Agent en échec",
"brainstormInvite": "Brainstorm",
"brainstormJoined": "Brainstorm",
"systemNotification": "Système",
"downloadFailed": "Échec du téléchargement"
},
"nav": {
"home": "Accueil",
@@ -750,7 +749,7 @@
"adminDashboard": "Tableau de bord Admin",
"diagnostics": "Diagnostics",
"trash": "Corbeille",
"support": "Support Memento ☕",
"support": "Soutenir Memento ☕",
"reminders": "Rappels",
"userManagement": "Gestion des utilisateurs",
"accountSettings": "Paramètres du compte",
@@ -769,7 +768,7 @@
"myLibrary": "Ma bibliothèque",
"favorites": "Favoris",
"recent": "Récent",
"proPlan": "Pro Plan",
"proPlan": "Plan Pro",
"chat": "Chat IA",
"lab": "L'Atelier",
"agents": "Agents"
@@ -834,7 +833,7 @@
"title": "Profil",
"description": "Mettez à jour vos informations personnelles",
"displayName": "Nom d'affichage",
"email": "Email",
"email": "Courriel",
"changePassword": "Changer le mot de passe",
"changePasswordDescription": "Mettez à jour votre mot de passe. Vous aurez besoin de votre mot de passe actuel.",
"currentPassword": "Mot de passe actuel",
@@ -881,8 +880,8 @@
"features": "Fonctionnalités IA",
"provider": "Fournisseur IA",
"providerAuto": "Auto (Recommandé)",
"providerOllama": "Ollama (Local)",
"providerOpenAI": "OpenAI (Cloud)",
"providerOllama": "Ollama (local)",
"providerOpenAI": "OpenAI (cloud)",
"frequency": "Fréquence",
"frequencyDaily": "Quotidienne",
"frequencyWeekly": "Hebdomadaire",
@@ -993,7 +992,11 @@
"confidence": "confiance",
"savingReminder": "Erreur lors de la sauvegarde du rappel",
"removingReminder": "Erreur lors de la suppression du rappel",
"generatingDescription": "Veuillez patienter..."
"generatingDescription": "Veuillez patienter...",
"pinnedFrozenTooltip": "Carnet figé (ordre des sous-carnets verrouillé)",
"organizeNotebookWithAITooltip": "Organiser ce carnet avec l'IA",
"assistantRequiredForSummarize": "Activez l'assistant IA dans les paramètres pour résumer",
"createSubnotebook": "Ajouter un sous-carnet"
},
"notebookSuggestion": {
"title": "Déplacer vers {name} ?",
@@ -1061,7 +1064,7 @@
"providerOpenRouterOption": "🌐 OpenRouter",
"providerMistralOption": "🌀 Mistral AI",
"providerZAIOption": "✨ Z.AI",
"providerLMStudioOption": "🖥️ LM Studio (Local)",
"providerLMStudioOption": "🖥️ LM Studio (local)",
"bestValue": "Meilleur rapport qualité/prix",
"bestQuality": "Meilleure qualité",
"saved": "(Enregistré)",
@@ -1146,7 +1149,7 @@
"addUser": "Ajouter un utilisateur",
"createUserDescription": "Ajouter un nouvel utilisateur au système.",
"name": "Nom",
"email": "Email",
"email": "Courriel",
"password": "Mot de passe",
"role": "Rôle",
"createSuccess": "Utilisateur créé avec succès",
@@ -1160,14 +1163,14 @@
"confirmDelete": "Êtes-vous sûr ? Cette action est irréversible.",
"table": {
"name": "Nom",
"email": "Email",
"email": "Courriel",
"role": "Rôle",
"createdAt": "Créé le",
"actions": "Actions"
},
"roles": {
"user": "Utilisateur",
"admin": "Admin"
"admin": "Administrateur"
},
"title": "Utilisateurs",
"description": "Gérer les utilisateurs et les permissions"
@@ -1179,8 +1182,6 @@
"tagsTestDescription": "Testez le fournisseur IA responsable des suggestions d'étiquettes automatiques",
"embeddingsTestTitle": "Test d'embeddings",
"embeddingsTestDescription": "Testez le fournisseur IA responsable des embeddings de recherche sémantique",
"chatTestTitle": "Test de chat assistant",
"chatTestDescription": "Testez le fournisseur IA responsable de l'assistant de discussion",
"howItWorksTitle": "Fonctionnement des tests",
"tagsGenerationTest": "🏷️ Test de génération d'étiquettes :",
"tagsStep1": "Envoie une note exemple au fournisseur IA",
@@ -1192,11 +1193,6 @@
"embeddingsStep2": "Génère une représentation vectorielle (liste de nombres)",
"embeddingsStep3": "Affiche les dimensions de l'embedding et des exemples de valeurs",
"embeddingsStep4": "Vérifie que le vecteur est valide et correctement formaté",
"chatGenerationTest": "💬 Test de chat assistant :",
"chatStep1": "Envoie un message de test à l'assistant",
"chatStep2": "Demande une réponse concise sur le rôle de l'IA",
"chatStep3": "Affiche la réponse générée par le modèle",
"chatStep4": "Vérifie la fluidité et le temps de réponse",
"tipContent": "Vous pouvez utiliser différents fournisseurs pour les étiquettes et les embeddings ! Par exemple, utilisez Ollama (gratuit) pour les étiquettes et OpenAI (meilleure qualité) pour les embeddings afin d'optimiser les coûts et les performances.",
"provider": "Fournisseur :",
"model": "Modèle :",
@@ -1216,7 +1212,14 @@
"error": "Erreur :",
"testError": "Erreur de test : {error}",
"tipTitle": "Astuce :",
"tipDescription": "Utilisez le panneau de test IA pour diagnostiquer les problèmes de configuration avant de tester."
"tipDescription": "Utilisez le panneau de test IA pour diagnostiquer les problèmes de configuration avant de tester.",
"chatTestTitle": "Test de chat assistant",
"chatTestDescription": "Testez le fournisseur IA responsable de l'assistant de discussion",
"chatGenerationTest": "💬 Test de chat assistant :",
"chatStep1": "Envoie un message de test à l'assistant",
"chatStep2": "Demande une réponse concise sur le rôle de l'IA",
"chatStep3": "Affiche la réponse générée par le modèle",
"chatStep4": "Vérifie la fluidité et le temps de réponse"
},
"sidebar": {
"dashboard": "Tableau de bord",
@@ -1235,7 +1238,7 @@
"description": "Configurer les outils externes pour le tool-use des agents : recherche web, scraping et accès API.",
"searchProvider": "Fournisseur de recherche web",
"searxng": "SearXNG (Auto-hébergé)",
"brave": "Brave Search API",
"brave": "API Brave Search",
"both": "Les deux (SearXNG principal, Brave secours)",
"searxngUrl": "URL SearXNG",
"braveKey": "Clé API Brave Search",
@@ -1285,16 +1288,16 @@
"technology": {
"title": "Stack technologique",
"description": "Construit avec des technologies modernes",
"frontend": "Frontend",
"backend": "Backend",
"frontend": "Front-end",
"backend": "Back-end",
"database": "Base de données",
"authentication": "Authentification",
"ai": "IA",
"ui": "UI",
"ui": "Interface",
"testing": "Tests"
},
"support": {
"title": "Support",
"title": "Soutien",
"description": "Obtenez de l'aide et donnez votre avis",
"documentation": "Documentation",
"reportIssues": "Signaler des problèmes",
@@ -1491,9 +1494,72 @@
"organizeWithAI": "Organiser avec l'IA",
"organize": "Organiser"
},
"organizeNotebook": {
"title": "Organiser le carnet",
"unknownError": "Erreur inconnue",
"toastSuccess": "Carnet organisé — {created} sous-carnet(s) créé(s), {moved} note(s) déplacée(s)",
"intro": "L'IA va analyser les notes de ce carnet et vous proposer un plan de réorganisation en sous-carnets thématiques.",
"bulletThemes": "Regroupement par sujet ou thème",
"bulletSubfolders": "Création de sous-carnets manquants",
"bulletPreview": "Aperçu complet avant modification",
"analyzingTitle": "Analyse en cours…",
"analyzingSubtitle": "L'IA lit vos notes et identifie les thèmes",
"previewSummary": "{groups} groupe(s) · {notes} note(s) · {newSubs} nouveau(x) sous-carnet(s)",
"badgeNew": "Nouveau",
"untitledNote": "Note sans titre",
"notesInGroup": "{count} note(s)",
"executingTitle": "Organisation en cours…",
"executingSubtitle": "Création des sous-carnets et déplacement des notes",
"doneTitle": "Carnet organisé !",
"doneStats": "{created} sous-carnet(s) créé(s) · {moved} note(s) déplacée(s)",
"analyzeButton": "Analyser avec l'IA",
"restart": "Recommencer",
"confirm": "Valider",
"closeButton": "Fermer"
},
"documentInfo": {
"tabInfo": "Infos",
"tabVersions": "Versions",
"wordsLabel": "mots",
"charactersLabel": "caractères",
"notebookLabel": "Carnet",
"typeLabel": "Type",
"createdLabel": "Créée le",
"modifiedLabel": "Modifiée",
"labelsSection": "Étiquettes",
"idLabel": "ID",
"historyDisabled": "L'historique n'est pas activé pour cette note.",
"enableHistory": "Activer l'historique",
"savedVersions": "Versions sauvegardées",
"savingEllipsis": "Sauvegarde…",
"versionSaved": "Version sauvegardée !",
"saveThisVersion": "Sauvegarder cette version",
"loading": "Chargement...",
"noVersion": "Aucune version",
"restoreTooltip": "Restaurer",
"deleteTooltip": "Supprimer",
"comparisonMode": "Mode comparaison",
"comparisonSubtitle": "Comparer les versions côte à côte",
"deleteVersionConfirm": "Supprimer cette version ?",
"latestBadge": "Récent"
},
"languages": {
"targets": {
"french": "Français",
"english": "Anglais",
"spanish": "Espagnol",
"german": "Allemand",
"persian": "Persan",
"portuguese": "Portugais",
"italian": "Italien",
"chinese": "Chinois",
"japanese": "Japonais"
},
"customPlaceholder": "ex. : Arabe, Russe…"
},
"common": {
"unknown": "Inconnu",
"notAvailable": "N/A",
"notAvailable": "N/D",
"loading": "Chargement...",
"error": "Erreur",
"success": "Succès",
@@ -1557,7 +1623,7 @@
"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.",
"active": "Active",
"active": "Actif",
"revoked": "Révoquée",
"revoke": "Révoquer",
"delete": "Supprimer",
@@ -1612,7 +1678,7 @@
"scraper": "Veilleur",
"researcher": "Chercheur",
"monitor": "Surveillant",
"slideGenerator": "Slides",
"slideGenerator": "Diaporamas",
"excalidrawGenerator": "Diagramme",
"custom": "Personnalisé"
},
@@ -1648,7 +1714,7 @@
"excalidrawDiagramTypeFlowchart": "Flowchart (processus)",
"excalidrawDiagramTypeMindmap": "Mindmap (idées)",
"excalidrawDiagramTypeOrgChart": "Organigramme (équipes)",
"excalidrawDiagramTypeTimeline": "Timeline / roadmap",
"excalidrawDiagramTypeTimeline": "Chronologie / feuille de route",
"excalidrawDiagramTypeProcessMap": "Process map (opérations)",
"excalidrawDiagramTypeArchitectureCloud": "Architecture cloud (zones/RG)",
"excalidrawDiagramStyle": "Style du diagramme Excalidraw",
@@ -1786,12 +1852,12 @@
"noteSearch": "Recherche notes",
"noteRead": "Lire une note",
"noteCreate": "Créer une note",
"urlFetch": "Récupérer URL",
"urlFetch": "Requête URL",
"memorySearch": "Mémoire",
"generatePptx": "Slides PPTX",
"generateSlides": "Slides HTML",
"generateExcalidraw": "Diagramme Excalidraw",
"configNeeded": "config",
"configNeeded": "Configuration requise",
"selected": "{count} sélectionné(s)",
"maxSteps": "Itérations max"
},
@@ -1872,7 +1938,7 @@
},
"labHeader": {
"title": "L'Atelier",
"live": "Live",
"live": "Direct",
"currentProject": "Projet Actuel",
"choose": "Choisir...",
"yourSpaces": "Vos Espaces",
@@ -1891,5 +1957,147 @@
"lab": {
"initializing": "Initialisation de l'espace de travail",
"loadingIdeas": "Chargement de vos idées..."
},
"richTextEditor": {
"slashHint": "↑↓ naviguer · Entrée insérer · Tab changer de section",
"slashLoading": "IA Note réfléchit...",
"slashTabAll": "Tout",
"slashCatBasic": "Blocs de base",
"slashCatMedia": "Médias",
"slashCatFormatting": "Mise en forme",
"slashCatAi": "IA Note",
"insertImage": "Insérer une image",
"imageUrlPlaceholder": "https://exemple.com/image.png",
"preview": "Aperçu",
"cancel": "Annuler",
"insert": "Insérer",
"slashText": "Texte",
"slashTextDesc": "Paragraphe simple",
"slashH1": "Titre 1",
"slashH1Desc": "Grand titre de section",
"slashH2": "Titre 2",
"slashH2Desc": "Titre de section moyen",
"slashH3": "Titre 3",
"slashH3Desc": "Petit titre de section",
"slashBullet": "Liste à puces",
"slashBulletDesc": "Liste non ordonnée",
"slashNumbered": "Liste numérotée",
"slashNumberedDesc": "Liste ordonnée numérotée",
"slashTodo": "Liste de tâches",
"slashTodoDesc": "Cases à cocher pour les tâches",
"slashQuote": "Citation",
"slashQuoteDesc": "Capturer une citation",
"slashCode": "Bloc de code",
"slashCodeDesc": "Extrait de code",
"slashDivider": "Séparateur",
"slashDividerDesc": "Séparateur horizontal",
"slashTable": "Tableau",
"slashTableDesc": "Insérer un tableau simple",
"slashDiagram": "Diagramme",
"slashDiagramDesc": "Générer un flux ou une carte mentale",
"slashSlides": "Présentation",
"slashSlidesDesc": "Générer un jeu de diapositives",
"slashImage": "Image",
"slashImageDesc": "Intégrer une image depuis une URL",
"slashAlignLeft": "Aligner à gauche",
"slashAlignLeftDesc": "Aligner le texte à gauche",
"slashAlignCenter": "Centrer",
"slashAlignCenterDesc": "Centrer le texte",
"slashAlignRight": "Aligner à droite",
"slashAlignRightDesc": "Aligner le texte à droite",
"slashSuperscript": "Exposant",
"slashSuperscriptDesc": "Sélectionner du texte d'abord",
"slashSubscript": "Indice",
"slashSubscriptDesc": "Sélectionner du texte d'abord",
"slashClarify": "Clarifier",
"slashClarifyDesc": "Rendre le texte plus clair",
"slashShorten": "Raccourcir",
"slashShortenDesc": "Condenser le texte",
"slashImprove": "Améliorer",
"slashImproveDesc": "Améliorer le style",
"slashExpand": "Développer",
"slashExpandDesc": "Élaborer et enrichir le texte",
"imageModalTitle": "Insérer une image",
"imageModalPreview": "Aperçu",
"imageModalCancel": "Annuler",
"imageModalInsert": "Insérer",
"imageModalInvalidUrl": "Veuillez entrer une URL valide",
"imageModalLoadFailed": "Échec du chargement de l'image",
"linkPlaceholder": "Collez ou tapez un lien...",
"bold": "Gras",
"italic": "Italique",
"underline": "Souligné",
"strike": "Barré",
"code": "Code",
"highlight": "Surligner",
"superscript": "Exposant",
"subscript": "Indice",
"addBlock": "Ajouter un bloc",
"placeholder": "Tapez '/' pour voir les commandes..."
},
"brainstorm": {
"title": "Vagues de pensée",
"subtitle": "Déployer les dimensions du potentiel",
"placeholder": "Entrez un concept à explorer...",
"generating": "L'IA récolte des graines de pensée...",
"newBrainstorm": "Nouveau brainstorm",
"noSessions": "Pas encore de brainstorms",
"startOne": "Commencer",
"sessions": "Sessions de brainstorming",
"seedLabel": "Idée source",
"ideaPromptDetailed": "Saisissez votre idée, question ou sujet pour le brainstorming...",
"brainstormThisIdea": "Brainstormer cette idée",
"startBrainstorm": "Lancer le brainstorm",
"spatialMode": "Mode exploration spatiale",
"wave1": "Vague 1",
"wave2": "Vague 2",
"wave3": "Vague 3",
"export": "Exporter",
"exporting": "Export...",
"wave": "Vague",
"novelty": "Originalité",
"originConnection": "Lien avec l'origine",
"linkedNotes": "Notes liées",
"deepen": "Creuser",
"deepening": "Génération...",
"extract": "Créer une note",
"converting": "Conversion...",
"dismiss": "Pas pertinent",
"noteCreated": "Note créée",
"ideas": "idées",
"cancel": "Annuler",
"delete": "Supprimer",
"ideaOrigin": "Origine de l'idée",
"noNoteLink": "Idée purement générative",
"derived_from": "Dérivé de",
"opposes": "En opposition avec",
"extends": "Étend",
"synthesizes": "Synthétise",
"transposes": "Transpose",
"none_found": "Aucun lien",
"viewNote": "Voir la note",
"addIdea": "Ajouter une idée",
"manualIdeaPrompt": "Titre de votre idée :",
"invite": "Inviter",
"linkCopied": "Lien d'invitation copié !",
"activityTitle": "Activité",
"noActivity": "Pas encore d'activité",
"justNow": "à l'instant",
"humanIdea": "Humain",
"aiIdea": "IA",
"respondsTo": "Répond à",
"adding": "Ajout...",
"manualIdeaDesc": "Partagez votre idée sur le canvas",
"manualIdeaTitle": "Titre",
"manualIdeaTitlePlaceholder": "Votre idée en quelques mots...",
"manualIdeaDescLabel": "Description (optionnel)",
"manualIdeaDescPlaceholder": "Développez votre idée...",
"activity": {
"manual_idea": "a ajouté une idée",
"wave_generated": "a généré une vague",
"joined": "a rejoint la session",
"idea_dismissed": "a écarté une idée",
"invite_created": "a créé une invitation"
}
}
}
}

View File

@@ -32,6 +32,8 @@
},
"sidebar": {
"notes": "Notes",
"recent": "हाल ही का",
"quickNav": "त्वरित नेविगेशन",
"reminders": "Reminders",
"labels": "Labels",
"editLabels": "Edit labels",
@@ -40,15 +42,35 @@
"noLabelsInNotebook": "इस नोटबुक में कोई लेबल नहीं",
"archive": "Archive",
"trash": "Trash",
"clearFilter": "Remove filter"
"clearFilter": "Remove filter",
"inbox": "इनबॉक्स",
"sharedWithMe": "मेरे साथ साझा किया",
"sortNewest": "सबसे पहले नवीनतम",
"sortOldest": "सबसे पुराना पहले",
"sortAlpha": "ए → जेड",
"accountMenu": "खाता मेनू",
"profile": "प्रोफ़ाइल",
"signOut": "साइन आउट",
"sortOrder": "क्रमबद्ध करेन का आदेश",
"freezePinnedNotebook": "पिन नोटबुक साइडबार ऑर्डर",
"unfreezePinnedNotebook": "नोटबुक साइडबार ऑर्डर को अनपिन करें",
"newSubNotebook": "नई उप-नोटबुक",
"renameNotebook": "नाम बदलें"
},
"notes": {
"title": "नोट्स",
"newNote": "नया नोट",
"reorganize": "नोट्स को पुनर्व्यवस्थित करें",
"untitled": "शीर्षकहीन",
"placeholder": "नोट लें...",
"markdownPlaceholder": "नोट लें... (Markdown समर्थित)",
"titlePlaceholder": "शीर्षक",
"noteTypes": {
"richtext": "रिच पाठ",
"markdown": "markdown",
"text": "सादे पाठ",
"checklist": "जांच सूची"
},
"listItem": "सूची आइटम",
"addListItem": "+ सूची आइटम",
"newChecklist": "नई चेकलिस्ट",
@@ -58,6 +80,7 @@
"confirmDelete": "क्या आप वाकई इस नोट को हटाना चाहते हैं?",
"confirmLeaveShare": "क्या आप वाकई इस साझा नोट को छोड़ना चाहते हैं?",
"sharedBy": "द्वारा साझा किया गया",
"sharedShort": "साझा",
"leaveShare": "छोड़ें",
"delete": "हटाएं",
"archive": "संग्रहित करें",
@@ -136,6 +159,8 @@
"dragToReorder": "पुनर्व्यवस्थित करने के लिए खींचें",
"more": "अधिक",
"emptyState": "कोई नोट नहीं",
"metadataPanel": "विवरण",
"metadataNotebook": "स्मरण पुस्तक",
"emptyStateTabs": "इस दृश्य में कोई नोट नहीं। साइडबार में \"नया नोट\" का उपयोग करें (कंपोज़र में AI शीर्षक सुझाव उपलब्ध)।",
"inNotebook": "नोटबुक में",
"moveFailed": "ले जाने में विफल",
@@ -147,11 +172,6 @@
"unpinned": "अनपिन किया गया",
"redoShortcut": "फिर से करें (Ctrl+Y)",
"undoShortcut": "पूर्ववत करें (Ctrl+Z)",
"viewCards": "कार्ड दृश्य",
"viewCardsTooltip": "ड्रैग-एंड-ड्रॉप पुनर्व्यवस्था के साथ कार्ड ग्रिड",
"viewTabs": "सूची दृश्य",
"viewTabsTooltip": "ऊपर टैब, नीचे नोट — पुनर्व्यवस्थित करने के लिए टैब खींचें",
"viewModeGroup": "नोट्स प्रदर्शन मोड",
"reorderTabs": "टैब पुनर्व्यवस्थित करें",
"modified": "संशोधित",
"created": "बनाया गया",
@@ -160,15 +180,18 @@
"savedStatus": "सहेजा गया",
"dirtyStatus": "संशोधित",
"completedLabel": "पूर्ण",
"notes.emptyNotebook": "खाली नोटबुक",
"notes.emptyNotebookDesc": "इस नोटबुक में कोई नोट नहीं है। एक बनाने के लिए + पर क्लिक करें।",
"notes.noNoteSelected": "कोई नोट चुना नहीं गया",
"notes.selectOrCreateNote": "सूची से एक नोट चुनें या एक नया बनाएं।",
"notes": {
"emptyNotebook": "खाली नोटबुक",
"emptyNotebookDesc": "इस नोटबुक में कोई नोट नहीं है। एक बनाने के लिए + पर क्लिक करें।",
"noNoteSelected": "कोई नोट चुना नहीं गया",
"selectOrCreateNote": "सूची से एक नोट चुनें या एक नया बनाएं।"
},
"commitVersion": "संस्करण सहेजें",
"versionSaved": "संस्करण सहेजा गया",
"deleteVersion": "इस संस्करण को हटाएं",
"versionDeleted": "संस्करण हटाया गया",
"deleteVersionConfirm": "क्या आप इस संस्करण को स्थायी रूप से हटाना चाहते हैं?",
"deleteVersionDesc": "इस एक्शन को वापस नहीं किया जा सकता। संस्करण इतिहास से स्थायी रूप से हटा दिया जाएगा.",
"historyMode": "इतिहास मोड",
"historyModeManual": "मैनुअल (कमिट बटन)",
"historyModeAuto": "स्वचालित (स्मार्ट)",
@@ -184,6 +207,10 @@
"enableHistory": "इतिहास सक्षम करें",
"historyEmpty": "कोई संस्करण उपलब्ध नहीं",
"historySelectVersion": "पूर्वावलोकन के लिए एक संस्करण चुनें",
"currentVersion": "मौजूदा",
"compareVersions": "तुलना करना",
"diffTitle": "तुलना",
"diffSelectHint": "उनकी तुलना करने के लिए सूची में 2 संस्करणों पर क्लिक करें",
"sortBy": "इसके अनुसार क्रमबद्ध करें",
"sortDateDesc": "तिथि (नवीनतम)",
"sortDateAsc": "तिथि (पुराना)",
@@ -197,10 +224,14 @@
"createFailed": "Failed to create note",
"updateFailed": "Failed to update note",
"archived": "Note archived",
"unarchivedSuccess": "नोट संग्रह से हटा दिया गया",
"archiveFailed": "Failed to archive",
"sort": "Sort",
"confirmDeleteTitle": "Delete note",
"leftShare": "Share removed",
"ideaOrigin": "Origin of the idea",
"noNoteLink": "Purely generative idea",
"dismiss": "Not pertinent",
"dismissed": "Note dismissed from recent",
"generalNotes": "General Notes",
"noteType": "नोट प्रकार",
@@ -214,7 +245,23 @@
"switchTypeTitle": "नोट प्रकार बदलें?",
"switchTypeWarning": "{type} में बदलने पर कुछ फ़ॉर्मेटिंग खो सकती है।",
"switchTypeContentPreserved": "आपकी सामग्री सादे पाठ के रूप में संरक्षित रहेगी।",
"switchType": "{type} में बदलें"
"switchType": "{type} में बदलें",
"saveNow": "अब सहेजें",
"backToCollection": "संग्रह पर वापस जाएँ",
"markdownEditingTitle": "संपादन पर लौटें",
"markdownPreviewTitle": "पूर्व दर्शन",
"brainstormThisIdea": "इस विचार पर मंथन करें",
"brainstormThisIdeaAria": "इस विचार पर मंथन करें",
"shareNoteTitle": "नोट साझा करें",
"shareNoteAria": "नोट साझा करें",
"saveNoteAria": "नोट सहेजें",
"noChangesToSaveAria": "सहेजने के लिए कोई परिवर्तन नहीं",
"optionsMenuAria": "विकल्प मेनू",
"deleteNoteConfirmItem": "नोट हटाएँ",
"noteDeletedToast": "नोट हटा दिया गया.",
"deleteNoteFailedToast": "मिटाया नहीं जा सका.",
"documentInfoAria": "दस्तावेज़ जानकारी",
"noModification": "कोई परिवर्तन नहीं"
},
"pagination": {
"previous": "←",
@@ -296,7 +343,24 @@
"accessRevoked": "Access has been revoked",
"errorLoading": "Error loading collaborators",
"failedToAdd": "Failed to add collaborator",
"failedToRemove": "Failed to remove collaborator"
"failedToRemove": "Failed to remove collaborator",
"shareCompactTitle": "शेयर करना",
"inviteByEmailLabel": "ईमेल द्वारा आमंत्रित करें",
"accessReadCompact": "देखना",
"accessEditCompact": "संपादन करना",
"sendInvitation": "निमंत्रण भेजना",
"invitationSentBadge": "निमंत्रण भेजा गया",
"sharedAccessLabel": "साझा पहुंच",
"noCollaboratorsEmpty": "अभी तक कोई सहयोगी नहीं.",
"removeAccessTitle": "पहुंच हटाएं",
"toastInviteSentTo": "निमंत्रण {ईमेल} पर भेजा गया",
"toastAccessRemoved": "{लक्ष्य} के लिए पहुंच हटा दी गई",
"toastUserFallback": "प्रयोगकर्ता",
"toastSharingError": "साझा करने में त्रुटि",
"toastEmailNotFound": "इस ईमेल के साथ कोई खाता नहीं मिला.",
"toastAlreadySharedUser": "यह नोट पहले ही इस उपयोगकर्ता के साथ साझा किया जा चुका है.",
"toastRemoveAccessFailed": "पहुंच नहीं हटाई जा सकी.",
"userFallback": "उपयोगकर्ता"
},
"ai": {
"analyzing": "AI विश्लेषण जारी है...",
@@ -326,6 +390,8 @@
"transforming": "रूपांतरित हो रहा है...",
"transformSuccess": "पाठ सफलतापूर्वक Markdown में रूपांतरित हो गया!",
"transformError": "रूपांतरण के दौरान त्रुटि",
"convertToRichtext": "रिच टेक्स्ट में कनवर्ट करें",
"convertingToRichtext": "परिवर्तित किया जा रहा है...",
"assistant": "AI सहायक",
"generating": "उत्पन्न हो रहा है...",
"generateTitles": "शीर्षक उत्पन्न करें",
@@ -389,6 +455,8 @@
"undoAI": "AI परिवर्तन पूर्ववत करें",
"undoApplied": "मूल पाठ पुनर्स्थापित",
"minWordsError": "AI कार्रवाइयों का उपयोग करने के लिए नोट में कम से कम 5 शब्द होने चाहिए।",
"wordCountMin": "कृपया सुधार के लिए कम से कम {मिनट} शब्द चुनें (वर्तमान में {वर्तमान} शब्द)",
"wordCountMax": "कृपया सुधार के लिए अधिकतम {अधिकतम} शब्दों का चयन करें (वर्तमान में {वर्तमान} शब्द)",
"genericError": "AI त्रुटि",
"actionError": "AI कार्रवाई के दौरान त्रुटि",
"appliedToNote": "नोट में लागू किया गया",
@@ -404,6 +472,15 @@
"chatTab": "चैट",
"noteActions": "नोट कार्रवाई",
"askToStart": "शुरू करने के लिए सहायक से कुछ पूछें।",
"chatPanelContext": "प्रसंग",
"chatPanelNotebookPlus": "+ नोटबुक",
"chatPanelWritingTone": "लिखने का स्वर",
"scopeAutoBadge": "ऑटो",
"chatNoteQuestionPlaceholder": "इस नोट के बारे में एक प्रश्न पूछें...",
"chatNotebookSelectPlaceholder": "एक नोटबुक शामिल करें...",
"assistantTabActions": "कार्रवाई",
"resourcePreviewAiTitle": "एआई पूर्वावलोकन",
"resourcePreviewInjectFromChat": "चैट से इंजेक्ट करें",
"contextLabel": "संदर्भ",
"thisNote": "यह नोट",
"allMyNotes": "मेरी सभी नोट्स",
@@ -415,6 +492,7 @@
"newLineHint": "Shift+Enter = नई पंक्ति",
"resultLabel": "परिणाम",
"discardAction": "खारिज करें",
"organization": "संगठन",
"transformationsDesc": "रूपांतरण — सीधे नोट में लागू",
"writeMinWordsAction": "AI कार्रवाई सक्रिय करने के लिए कम से कम 5 शब्द लिखें।",
"processingAction": "प्रसंस्करण हो रहा है...",
@@ -425,7 +503,45 @@
"shorten": "छोटा करें",
"improve": "सुधार",
"toMarkdown": "Markdown में",
"describeImages": "Describe images"
"describeImages": "Describe images",
"fixGrammar": "व्याकरण ठीक करें",
"translate": "अनुवाद",
"explain": "व्याख्या करना",
"toRichText": "रिच टेक्स्ट में कनवर्ट करें"
},
"generate": {
"slides": "स्लाइड जनरेट करें",
"sectionLabel": "पीढ़ी के उपकरण",
"theme": "विषय",
"themeArchitecturalMono": "वास्तुशिल्प मोनो",
"themeVibrantTech": "वाइब्रेंट टेक",
"themeMinimalSilk": "न्यूनतम रेशम",
"style": "शैली",
"styleProfessional": "पेशेवर",
"styleCreative": "रचनात्मक",
"styleBrutalist": "क्रूरतावादी",
"diagram": "आरेख उत्पन्न करें",
"diagramReadyHint": "नोट को दृश्य प्रवाह में बदलें",
"diagramType": "आरेख प्रकार",
"typeAuto": "ऑटो का पता लगाने",
"typeFlowchart": "फ़्लोचार्ट",
"typeMindMap": "मन में नक्शे बनाना",
"typeTimeline": "समय",
"typeOrgChart": "संगठन चार्ट",
"typeArchitecture": "वास्तुकला",
"typeProcessMap": "नक्शे को संसाधित करें",
"styleSketchy": "अधूरा",
"styleSoft": "कोमल",
"styleMinimal": "न्यूनतम",
"styleDraft": "मसौदा",
"stylePolished": "पॉलिश",
"styleHandwritten": "हस्तलिखित",
"diagramReady": "आरेख तैयार है!",
"openInExcalidraw": "एक्सकैलिड्रॉ लैब में खोलें",
"insertDiagramInNote": "वर्तमान नोट में पीएनजी एम्बेड करें",
"diagramImageAlt": "एआई जनित आरेख",
"insertedInNote": "नोट में डायग्राम डाला गया",
"insertExportError": "आरेख निर्यात/अपलोड करने में त्रुटि"
},
"openAssistant": "AI सहायक खोलें",
"poweredByMomento": "Momento AI द्वारा संचालित",
@@ -442,7 +558,64 @@
"aiCopilot": "AI सह-पायलट",
"suggestTitle": "AI शीर्षक सुझाव",
"generateTitleFromImage": "Generate title from image",
"titleGenerated": "Title generated from image"
"titleGenerated": "Title generated from image",
"resourceTab": "संसाधन",
"aiNoteTitle": "एआई नोट",
"injectReplace": "प्रतिस्थापित करें",
"injectReplaceTitle": "नोट सामग्री को इस संदेश से बदलें",
"injectComplete": "पूरा",
"injectCompleteTitle": "इस संदेश के साथ पूरा नोट (एआई)",
"injectMerge": "मर्ज",
"injectMergeTitle": "नोट के साथ विलय (एआई)",
"imagesCount": "{गिनती} छवियाँ",
"resource": {
"failedToLoadUrl": "इस यूआरएल को लोड करने में विफल",
"pageLoaded": "पृष्ठ लोड किया गया: {शीर्षक}",
"pageLoadError": "पेज लोड करने में त्रुटि",
"pasteOrUrlFirst": "पहले टेक्स्ट चिपकाएँ या URL लोड करें",
"enrichError": "संवर्धन त्रुटि",
"enrichErrorShort": "संवर्धन त्रुटि",
"contentApplied": "नोट ✓ पर लागू सामग्री",
"fromChat": "💬 चैट से",
"replacement": "↓ प्रतिस्थापन",
"completedByAI": "✦ एआई द्वारा पूरा किया गया",
"mergedByAI": "⟳ एआई द्वारा विलय",
"rendered": "प्रतिपादन किया",
"cancel": "रद्द करना",
"applyToNote": "नोट करने के लिए आवेदन करें",
"urlLabel": "यूआरएल (वैकल्पिक)",
"resourceText": "संसाधन पाठ",
"resourcePlaceholder": "अपना टेक्स्ट यहां चिपकाएं (मार्कडाउन, HTML, सादा टेक्स्ट...)",
"words": "शब्द",
"integrationMode": "एकीकरण मोड",
"modeReplace": "प्रतिस्थापित करें",
"modeReplaceDesc": "प्रत्यक्ष, कोई एआई नहीं",
"modeComplete": "पूरा",
"modeCompleteDesc": "पुनः लिखे बिना जोड़ता है",
"modeMerge": "मर्ज",
"modeMergeDesc": "पुनः लिखता है और एकीकृत करता है",
"aiProcessing": "एआई प्रोसेसिंग...",
"preview": "पूर्व दर्शन",
"generatePreview": "पूर्वावलोकन उत्पन्न करें",
"emptyNoteHint": "💡 नोट खाली है - संसाधन सामग्री सीधे एकीकृत की जाएगी।"
},
"cancel": "रद्द करना",
"copied": "कॉपी किया गया",
"copy": "प्रतिलिपि",
"transformations": "परिवर्तनों",
"otherLanguage": "दूसरी भाषा",
"translateNow": "अभी अनुवाद करें",
"generationTools": "पीढ़ी के उपकरण",
"generateSlidesLoading": "⏳ प्रेजेंटेशन तैयार किया जा रहा है...",
"generateDiagramLoading": "⏳ आरेख जनरेट कर रहा है...",
"errorShort": "गलती",
"readyToast": "तैयार!",
"downloadFailedToast": "डाउनलोड विफल रहा",
"pptxDownloadButton": ".pptx डाउनलोड करें",
"presentationReadyBadge": "प्रेजेंटेशन तैयार",
"openInLabTitle": "लैब में खोलें",
"inlineSummaryMarkdown": "**सारांश:**",
"networkErrorShort": "नेटवर्क त्रुटि।"
},
"titleSuggestions": {
"available": "शीर्षक सुझाव",
@@ -548,7 +721,19 @@
"untitled": "शीर्षकहीन",
"notifications": "सूचनाएं",
"declined": "साझाकरण अस्वीकृत",
"removed": "सूची से नोट हटाया गया"
"removed": "सूची से नोट हटाया गया",
"slidesReady": "प्रेजेंटेशन तैयार",
"openSlides": "प्रस्तुति खोलें",
"canvasReady": "आरेख तैयार",
"pptxReady": "स्लाइड तैयार",
"downloadPptx": ".pptx डाउनलोड करें",
"markAllRead": "सभी को पढ़ा दिखाएं",
"agentSuccess": "एजेंट ख़त्म",
"agentFailed": "एजेंट असफल रहा",
"brainstormInvite": "मंथन",
"brainstormJoined": "मंथन",
"systemNotification": "प्रणाली",
"downloadFailed": "डाउनलोड विफल रहा"
},
"nav": {
"home": "होम",
@@ -597,6 +782,17 @@
"themeLight": "लाइट",
"themeDark": "डार्क",
"themeSystem": "सिस्टम",
"themeBaseGroup": "Base",
"themePalettesGroup": "Color palettes",
"themeSepia": "Sepia",
"themeMidnight": "Midnight",
"themeRose": "Rose",
"themeGreen": "Green",
"themeLavender": "Lavender",
"themeSand": "Sand",
"themeOcean": "Ocean",
"themeSunset": "Sunset",
"themeBlue": "Blue",
"notifications": "सूचनाएं",
"language": "भाषा",
"selectLanguage": "भाषा चुनें",
@@ -630,17 +826,8 @@
"desktopNotifications": "डेस्कटॉप सूचनाएं",
"desktopNotificationsDesc": "ब्राउज़र में सूचनाएं प्राप्त करें",
"notificationsDesc": "अपनी सूचना वरीयताएं प्रबंधित करें",
"themeBaseGroup": "Base",
"themePalettesGroup": "Color palettes",
"themeSepia": "Sepia",
"themeMidnight": "Midnight",
"themeRose": "Rose",
"themeGreen": "Green",
"themeLavender": "Lavender",
"themeSand": "Sand",
"themeOcean": "Ocean",
"themeSunset": "Sunset",
"themeBlue": "Blue"
"autoSave": "ऑटो को बचाने",
"autoSaveDesc": "टाइप करते समय परिवर्तन स्वचालित रूप से सहेजें"
},
"profile": {
"title": "प्रोफ़ाइल",
@@ -707,7 +894,15 @@
"providerDesc": "अपना पसंदीदा AI प्रदाता चुनें",
"providerAutoDesc": "Ollama जब उपलब्ध हो, OpenAI फॉलबैक",
"providerOllamaDesc": "100% निजी, स्थानीय रूप से चलता है",
"providerOpenAIDesc": "सबसे सटीक, API कुंजी की आवश्यकता है"
"providerOpenAIDesc": "सबसे सटीक, API कुंजी की आवश्यकता है",
"aiNote": "एआई नोट",
"aiNoteDesc": "एआई चैट बटन और टेक्स्ट सुधार उपकरण सक्षम करें",
"languageDetection": "भाषा का पता लगाना",
"languageDetectionDesc": "स्वचालित रूप से आपके नोट्स की भाषा का पता लगाता है",
"autoLabeling": "सुझावों को लेबल करें",
"autoLabelingDesc": "स्वचालित रूप से आपके नोट्स पर लेबल सुझाता है और लागू करता है",
"noteHistory": "इतिहास नोट करें",
"noteHistoryDesc": "इतिहास से संस्करण स्नैपशॉट और पुनर्स्थापना सक्षम करें"
},
"general": {
"loading": "लोड हो रहा है...",
@@ -764,7 +959,9 @@
"markDone": "पूर्ण चिह्नित करें",
"markUndone": "अपूर्ण चिह्नित करें",
"todayAt": "आज {time} बजे",
"tomorrowAt": "कल {time} बजे"
"tomorrowAt": "कल {time} बजे",
"clearCompleted": "स्पष्टतः पूरा",
"viewAll": "सभी अनुस्मारक देखें"
},
"notebook": {
"create": "नोटबुक बनाएं",
@@ -795,7 +992,11 @@
"confidence": "विश्वास",
"savingReminder": "रिमाइंडर सहेजने में त्रुटि",
"removingReminder": "रिमाइंडर हटाने में त्रुटि",
"generatingDescription": "Please wait..."
"generatingDescription": "Please wait...",
"pinnedFrozenTooltip": "पिन की गई नोटबुक - ऑर्डर फ़्रीज़ किया गया",
"organizeNotebookWithAITooltip": "इस नोटबुक को AI के साथ व्यवस्थित करें",
"assistantRequiredForSummarize": "संक्षेप में बताने के लिए सेटिंग्स में AI Assistant चालू करें",
"createSubnotebook": "उप-नोटबुक जोड़ें"
},
"notebookSuggestion": {
"title": "{name} में ले जाएं?",
@@ -808,6 +1009,9 @@
},
"admin": {
"title": "एडमिन डैशबोर्ड",
"adminConsole": "एडमिन कंसोल",
"navSection": "मार्गदर्शन",
"backToApp": "मेमेंटो को लौटें",
"userManagement": "उपयोगकर्ता प्रबंधन",
"chat": "AI Chat",
"lab": "The Lab",
@@ -850,6 +1054,11 @@
"providerEmbeddingRequired": "AI_PROVIDER_EMBEDDING आवश्यक है",
"providerOllamaOption": "🦙 Ollama (Local & Free)",
"providerOpenAIOption": "🤖 OpenAI (GPT-5, GPT-4)",
"providerAnthropicOption": "🧠 एंथ्रोपिक (क्लाउड एपीआई)",
"providerAnthropicCustomOption": "🧩 एंथ्रोपिक कस्टम (संदेश एपीआई - मिनीमैक्स, आदि)",
"anthropicModelHint": "सुझावों में से एक क्लाउड मॉडल आईडी चुनें या मैन्युअल रूप से एक दर्ज करें (आधिकारिक एपीआई के लिए कोई दूरस्थ मॉडल सूची नहीं)।",
"anthropicCustomModelHint": "एंथ्रोपिक-संगत संदेश एपीआई (जैसे मिनीमैक्स): बेस यूआरएल https://api.minimax.io/anthropic (चीन: https://api.minimaxi.com/anthropic), मॉडल MiniMax-M2.7। एंबेडिंग: प्रदाता «कस्टम» + ओपनएआई यूआरएल https://api.minimax.io/v1 का उपयोग करें।",
"anthropicCustomNoModelList": "यह गेटवे ओपनएआई-शैली/मॉडल सूची को उजागर नहीं करता है - सुझावों से मॉडल चुनें या इसे टाइप करें (उदाहरण के लिए मिनीमैक्स-एम2.7)।",
"providerCustomOption": "🔧 Custom OpenAI-Compatible",
"providerDeepSeekOption": "🔍 DeepSeek",
"providerOpenRouterOption": "🌐 OpenRouter",
@@ -1003,7 +1212,14 @@
"error": "त्रुटि:",
"testError": "परीक्षण त्रुटि: {error}",
"tipTitle": "सुझाव:",
"tipDescription": "परीक्षण से पहले कॉन्फ़िगरेशन समस्याओं का निदान करने के लिए AI परीक्षण पैनल का उपयोग करें।"
"tipDescription": "परीक्षण से पहले कॉन्फ़िगरेशन समस्याओं का निदान करने के लिए AI परीक्षण पैनल का उपयोग करें।",
"chatTestTitle": "चैट सहायक परीक्षण",
"chatTestDescription": "चैट सहायक द्वारा उपयोग किए गए AI प्रदाता का परीक्षण करें",
"chatGenerationTest": "💬 चैट सहायक परीक्षण:",
"chatStep1": "सहायक को एक परीक्षण संदेश भेजता है",
"chatStep2": "सहायक क्या करता है इसके बारे में संक्षिप्त उत्तर मांगता है",
"chatStep3": "मॉडल प्रतिक्रिया दिखाता है",
"chatStep4": "प्रतिक्रियाशीलता और विलंबता की जाँच करता है"
},
"sidebar": {
"dashboard": "डैशबोर्ड",
@@ -1194,6 +1410,7 @@
"notesViewLabel": "नोट्स दृश्य",
"notesViewTabs": "टैब (OneNote-शैली)",
"notesViewMasonry": "कार्ड (ग्रिड)",
"notesViewList": "सूची (पत्रिका)",
"selectTheme": "Select theme",
"fontFamilyLabel": "फ़ॉन्ट परिवार",
"fontFamilyDescription": "पूरे ऐप में उपयोग किए जाने वाले फ़ॉन्ट का चयन करें",
@@ -1277,6 +1494,69 @@
"organizeWithAI": "AI से व्यवस्थित करें",
"organize": "व्यवस्थित करें"
},
"organizeNotebook": {
"title": "नोटबुक व्यवस्थित करें",
"unknownError": "अज्ञात त्रुटि",
"toastSuccess": "नोटबुक व्यवस्थित - {बनाया गया} उप-नोटबुक बनाया गया, {स्थानांतरित} नोट स्थानांतरित किया गया",
"intro": "एआई इस नोटबुक में नोट्स का विश्लेषण करेगा और उन्हें विषयगत उप-नोटबुक में पुनर्गठित करने की योजना प्रस्तावित करेगा।",
"bulletThemes": "नोट्स को विषय या थीम के आधार पर समूहित करें",
"bulletSubfolders": "गुम उप-नोटबुक बनाएँ",
"bulletPreview": "किसी भी बदलाव से पहले पूर्ण पूर्वावलोकन",
"analyzingTitle": "विश्लेषण कर रहा हूँ...",
"analyzingSubtitle": "एआई आपके नोट्स पढ़ रहा है और थीम की पहचान कर रहा है",
"previewSummary": "{समूह} समूह · {नोट्स} नोट्स · {newSubs} नई उप-नोटबुक",
"badgeNew": "नया",
"untitledNote": "शीर्षक रहित नोट",
"notesInGroup": "{गिनती} नोट",
"executingTitle": "आयोजन...",
"executingSubtitle": "उप-नोटबुक बनाना और नोट्स चलाना",
"doneTitle": "नोटबुक व्यवस्थित!",
"doneStats": "{बनाया गया} उप-नोटबुक बनाया गया · {स्थानांतरित किया गया} नोट स्थानांतरित किया गया",
"analyzeButton": "एआई के साथ विश्लेषण करें",
"restart": "प्रारंभ करें",
"confirm": "आवेदन करना",
"closeButton": "बंद करना"
},
"documentInfo": {
"tabInfo": "जानकारी",
"tabVersions": "संस्करणों",
"wordsLabel": "शब्द",
"charactersLabel": "अक्षर",
"notebookLabel": "स्मरण पुस्तक",
"typeLabel": "प्रकार",
"createdLabel": "बनाया था",
"modifiedLabel": "अद्यतन",
"labelsSection": "लेबल",
"idLabel": "पहचान",
"historyDisabled": "इस नोट के लिए इतिहास सक्षम नहीं है.",
"enableHistory": "इतिहास सक्षम करें",
"savedVersions": "सहेजे गए संस्करण",
"savingEllipsis": "सहेजा जा रहा है...",
"versionSaved": "संस्करण सहेजा गया!",
"saveThisVersion": "इस संस्करण को सहेजें",
"loading": "लोड हो रहा है...",
"noVersion": "अभी तक कोई संस्करण नहीं",
"restoreTooltip": "पुनर्स्थापित करना",
"deleteTooltip": "मिटाना",
"comparisonMode": "तुलना विधा",
"comparisonSubtitle": "संस्करणों की साथ-साथ तुलना करें",
"deleteVersionConfirm": "यह संस्करण हटाएं?",
"latestBadge": "नवीनतम"
},
"languages": {
"targets": {
"french": "फ़्रेंच",
"english": "अंग्रेज़ी",
"spanish": "स्पैनिश",
"german": "जर्मन",
"persian": "फ़ारसी",
"portuguese": "पुर्तगाली",
"italian": "इतालवी",
"chinese": "चीनी",
"japanese": "जापानी"
},
"customPlaceholder": "जैसे अरबी, रूसी..."
},
"common": {
"unknown": "अज्ञात",
"notAvailable": "उपलब्ध नहीं",
@@ -1398,12 +1678,16 @@
"scraper": "मॉनिटर",
"researcher": "शोधकर्ता",
"monitor": "पर्यवेक्षक",
"slideGenerator": "स्लाइड्स",
"excalidrawGenerator": "आरेख",
"custom": "कस्टम"
},
"typeDescriptions": {
"scraper": "कई साइटों से डेटा एकत्र करता है और सारांश बनाता है",
"researcher": "किसी विषय पर जानकारी खोजता है",
"monitor": "नोटबुक की निगरानी करता है और नोट्स का विश्लेषण करता है",
"slideGenerator": "नोट्स से एक पावरपॉइंट प्रेजेंटेशन बनाता है",
"excalidrawGenerator": "नोट्स से एक एक्सालिड्रा आरेख बनाता है",
"custom": "अपने स्वयं के प्रॉम्प्ट के साथ मुक्त एजेंट"
},
"form": {
@@ -1416,6 +1700,27 @@
"urlsOptional": "(वैकल्पिक)",
"sourceNotebook": "निगरानी करने के लिए नोटबुक",
"selectNotebook": "नोटबुक चुनें...",
"selectNotes": "विश्लेषण करने के लिए नोट्स",
"notesSelected": "{{गिनती}} नोट चयनित",
"slideTheme": "प्रस्तुति विषय",
"slideThemeDefault": "स्वचालित",
"slideStyle": "दृश्य पद्धति",
"slideStyleSoft": "नरम (अनुशंसित)",
"slideStyleSharp": "तीखा और घना",
"slideStyleRounded": "गोल और विशाल",
"slideStylePill": "प्रीमियम/गोली",
"excalidrawDiagramType": "आरेख प्रकार",
"excalidrawDiagramTypeAuto": "स्वचालित (डोमेन पहचान)",
"excalidrawDiagramTypeFlowchart": "फ़्लोचार्ट (प्रक्रिया)",
"excalidrawDiagramTypeMindmap": "माइंडमैप (विचार)",
"excalidrawDiagramTypeOrgChart": "संगठन चार्ट (टीमें)",
"excalidrawDiagramTypeTimeline": "समयरेखा/रोडमैप",
"excalidrawDiagramTypeProcessMap": "प्रक्रिया मानचित्र (संचालन)",
"excalidrawDiagramTypeArchitectureCloud": "क्लाउड आर्किटेक्चर (क्षेत्र/आरजी)",
"excalidrawDiagramStyle": "एक्सालिड्रॉ आरेख शैली",
"excalidrawDiagramStyleDefault": "रंगीन (एक्सकैलिड्रॉ)",
"excalidrawDiagramStyleSketchPlus": "स्केच+ (उन्नत एक्सालिड्रॉ)",
"excalidrawDiagramStyleAustere": "ऑस्टेरे (न्यूनतम)",
"targetNotebook": "लक्ष्य नोटबुक",
"inbox": "इनबॉक्स",
"instructions": "AI निर्देश",
@@ -1485,6 +1790,8 @@
"updated": "एजेंट अपडेट किया गया",
"deleted": "\"{name}\" हटाया गया",
"deleteError": "हटाने में त्रुटि",
"running": "पीढ़ी प्रगति पर है...",
"runningDesc": "जनरेशन में कुछ मिनट लग सकते हैं. आप स्वतंत्र रूप से नेविगेट कर सकते हैं.",
"runSuccess": "\"{name}\" सफलतापूर्वक निष्पादित हुआ",
"runError": "त्रुटि: {error}",
"runFailed": "निष्पादन विफल",
@@ -1519,13 +1826,24 @@
"chercheur": {
"name": "विषय शोधकर्ता",
"description": "किसी विषय पर गहन जानकारी खोजता है और संदर्भों के साथ संरचित नोट बनाता है।"
},
"slideGenerator": {
"name": "स्लाइड जेनरेटर",
"description": "नोटबुक से नोट्स पढ़ता है और स्वचालित रूप से एक संरचित प्रस्तुति तैयार करता है।"
},
"excalidrawGenerator": {
"name": "आरेख जेनरेटर",
"description": "एक नोट पढ़ता है और एक्सकैलिड्रा लैब में एक दृश्य आरेख तैयार करता है।"
}
},
"runLog": {
"title": "इतिहास",
"noHistory": "अभी तक कोई निष्पादन नहीं",
"toolTrace": "{count} टूल कॉल",
"step": "चरण {num}"
"step": "चरण {num}",
"clearConfirm": "क्या आप वाकई इस एजेंट का सारा इतिहास हटाना चाहते हैं?",
"cleared": "इतिहास हटा दिया गया",
"clearHistory": "इतिहास मिटा दें"
},
"tools": {
"title": "एजेंट टूल",
@@ -1536,6 +1854,9 @@
"noteCreate": "नोट बनाएं",
"urlFetch": "URL प्राप्त करें",
"memorySearch": "मेमोरी",
"generatePptx": "पीपीटीएक्स स्लाइड्स",
"generateSlides": "HTML स्लाइड्स",
"generateExcalidraw": "एक्सालिड्रॉ आरेख",
"configNeeded": "कॉन्फ़िग",
"selected": "{count} चयनित",
"maxSteps": "अधिकतम पुनरावृत्तियाँ"
@@ -1547,7 +1868,9 @@
"scraper": "आप एक निगरानी सहायक हैं। विभिन्न वेबसाइटों के लेखों को एक स्पष्ट, संरचित सारांश में संश्लेषित करें।",
"researcher": "आप एक कट्टर शोधकर्ता हैं। अनुरोधित विषय के लिए संदर्भ, मुख्य बिंदु, बहस और संदर्भों के साथ एक अनुसंधान नोट तैयार करें।",
"monitor": "आप एक विश्लेषणात्मक सहायक हैं। प्रदान किए गए नोट्स का विश्लेषण करें और सुराग, संदर्भ और नोट्स के बीच कनेक्शन सुझाएं।",
"custom": "आप एक सहायक सहायक हैं।"
"custom": "आप एक सहायक सहायक हैं।",
"slideGenerator": "आप एक प्रेजेंटेशन निर्माता हैं. प्रदान की गई सामग्री पढ़ें और शीर्षकों, मुख्य बिंदुओं और सारांशों के साथ संरचित स्लाइड बनाएं।",
"excalidrawGenerator": "आप एक आरेख निर्माता हैं. प्रदान की गई सामग्री का विश्लेषण करें और एक स्पष्ट, व्यवस्थित दृश्य आरेख बनाएं।"
},
"help": {
"title": "एजेंट गाइड",
@@ -1581,7 +1904,10 @@
"frequency": "एजेंट कितनी बार स्वचालित रूप से चलता है। परीक्षण के लिए मैनुअल से शुरू करें।",
"instructions": "कस्टम निर्देश जो डिफ़ॉल्ट AI प्रॉम्प्ट को बदलते हैं। स्वचालित का उपयोग करने के लिए खाली छोड़ें।",
"tools": "चुनें कि एजेंट कौन से टूल उपयोग कर सकता है। प्रत्येक टूल एजेंट को एक विशिष्ट क्षमता देता है।",
"maxSteps": "अधिकतम तर्क चक्र। अधिक चरण = गहन विश्लेषण लेकिन अधिक समय।"
"maxSteps": "अधिकतम तर्क चक्र। अधिक चरण = गहन विश्लेषण लेकिन अधिक समय।",
"selectNotes": "विश्लेषण करने के लिए विशिष्ट नोट्स का चयन करें. यदि कोई भी चयनित नहीं है, तो एजेंट नोटबुक से सभी नोट्स का उपयोग करेगा।",
"slideTheme": "प्रस्तुतिकरण के लिए एक रंग पैलेट चुनें. स्वचालित AI को निर्णय लेने देता है।",
"slideStyle": "दृश्य शैली कोने की त्रिज्या, रिक्ति और सूचना घनत्व को प्रभावित करती है।"
}
}
},
@@ -1631,5 +1957,147 @@
"lab": {
"initializing": "कार्यक्षेत्र प्रारंभ हो रहा है",
"loadingIdeas": "आपके विचार लोड हो रहे हैं..."
},
"richTextEditor": {
"slashHint": "↑↓ नेविगेट करें · इंसर्ट दर्ज करें · टैब स्विच अनुभाग",
"slashLoading": "ऐ सोच...",
"slashTabAll": "सभी",
"slashCatBasic": "बुनियादी ब्लॉक",
"slashCatMedia": "मिडिया",
"slashCatFormatting": "का प्रारूपण",
"slashCatAi": "एआई नोट",
"insertImage": "चित्र डालें",
"imageUrlPlaceholder": "https://example.com/image.png",
"preview": "पूर्व दर्शन",
"cancel": "रद्द करना",
"insert": "डालना",
"slashText": "मूलपाठ",
"slashTextDesc": "सरल अनुच्छेद",
"slashH1": "शीर्षक 1",
"slashH1Desc": "बड़े अनुभाग का शीर्षक",
"slashH2": "शीर्षक 2",
"slashH2Desc": "मध्यम अनुभाग शीर्षक",
"slashH3": "शीर्षक 3",
"slashH3Desc": "लघु अनुभाग शीर्षक",
"slashBullet": "बुलेट सूची",
"slashBulletDesc": "अव्यवस्थित सूची",
"slashNumbered": "क्रमांकित सूची",
"slashNumberedDesc": "क्रमबद्ध क्रमांकित सूची",
"slashTodo": "कार्य सूची",
"slashTodoDesc": "चेकबॉक्स कार्य",
"slashQuote": "उद्धरण",
"slashQuoteDesc": "एक उद्धरण कैप्चर करें",
"slashCode": "कोड ब्लॉक",
"slashCodeDesc": "कोड स्निपेट",
"slashDivider": "डिवाइडर",
"slashDividerDesc": "क्षैतिज विभाजक",
"slashTable": "मेज़",
"slashTableDesc": "एक साधारण ग्रिड डालें",
"slashDiagram": "आरेख",
"slashDiagramDesc": "एक प्रवाह या माइंडमैप उत्पन्न करें",
"slashSlides": "प्रस्तुति",
"slashSlidesDesc": "एक सुंदर स्लाइड डेक बनाएं",
"slashImage": "छवि",
"slashImageDesc": "URL से एक छवि एम्बेड करें",
"slashAlignLeft": "बाएँ संरेखित करें",
"slashAlignLeftDesc": "पाठ को बाईं ओर संरेखित करें",
"slashAlignCenter": "केंद्र",
"slashAlignCenterDesc": "पाठ को केन्द्रित करें",
"slashAlignRight": "दाएँ संरेखित करें",
"slashAlignRightDesc": "टेक्स्ट को दाईं ओर संरेखित करें",
"slashSuperscript": "ऊपर की ओर लिखा हुआ",
"slashSuperscriptDesc": "आधार रेखा के ऊपर पाठ",
"slashSubscript": "सबस्क्रिप्ट",
"slashSubscriptDesc": "आधार रेखा के नीचे पाठ",
"slashClarify": "स्पष्ट करना",
"slashClarifyDesc": "पाठ को स्पष्ट बनाएं",
"slashShorten": "छोटा",
"slashShortenDesc": "पाठ को संक्षिप्त करें",
"slashImprove": "सुधार",
"slashImproveDesc": "शैली बढ़ाएँ",
"slashExpand": "बढ़ाना",
"slashExpandDesc": "पाठ को विस्तृत और समृद्ध करें",
"imageModalTitle": "चित्र डालें",
"imageModalPreview": "पूर्व दर्शन",
"imageModalCancel": "रद्द करना",
"imageModalInsert": "डालना",
"imageModalInvalidUrl": "क्रुपया मान्य यूआरएल दर्ज करें",
"imageModalLoadFailed": "छवि लोड करने में विफल",
"linkPlaceholder": "लिंक चिपकाएँ या टाइप करें...",
"bold": "बोल्ड",
"italic": "तिरछा",
"underline": "रेखांकन",
"strike": "स्ट्राइकथ्रू",
"code": "कोड",
"highlight": "प्रमुखता से दिखाना",
"superscript": "ऊपर की ओर लिखा हुआ",
"subscript": "सबस्क्रिप्ट",
"addBlock": "ब्लॉक जोड़ें",
"placeholder": "कमांड के लिए '/' टाइप करें..."
},
"brainstorm": {
"title": "Waves of Thought",
"subtitle": "Unfold dimensions of potentiality",
"placeholder": "Enter a concept to unfold...",
"generating": "AI is harvesting seeds of thought...",
"newBrainstorm": "New Brainstorm",
"noSessions": "No brainstorms yet",
"startOne": "Start one",
"sessions": "Brainstorms",
"seedLabel": "Seed Idea",
"ideaPromptDetailed": "विचार-मंथन के लिए अपना विचार, प्रश्न या विषय दर्ज करें...",
"brainstormThisIdea": "Brainstorm this idea",
"startBrainstorm": "Start Brainstorm",
"spatialMode": "Spatial Exploration Mode",
"wave1": "Wave 1",
"wave2": "Wave 2",
"wave3": "Wave 3",
"export": "Export",
"exporting": "Exporting...",
"wave": "Wave",
"novelty": "Novelty",
"originConnection": "Origin connection",
"linkedNotes": "Linked notes",
"deepen": "Deepen",
"deepening": "Generating...",
"extract": "Create Note",
"converting": "Converting...",
"dismiss": "Not pertinent",
"noteCreated": "Note Created",
"ideas": "ideas",
"cancel": "Cancel",
"delete": "Delete",
"ideaOrigin": "Origin of the idea",
"noNoteLink": "Purely generative idea",
"derived_from": "Derived from",
"opposes": "In opposition with",
"extends": "Extends",
"synthesizes": "Synthesizes",
"transposes": "Transposes",
"none_found": "No note link",
"viewNote": "View note",
"addIdea": "Add idea",
"manualIdeaPrompt": "Title of your idea:",
"invite": "Invite",
"linkCopied": "Invite link copied!",
"activityTitle": "गतिविधि",
"noActivity": "अभी तक कोई गतिविधि नहीं",
"justNow": "बस अब",
"humanIdea": "इंसान",
"aiIdea": "ऐ",
"respondsTo": "का जवाब देता है",
"adding": "जोड़ा जा रहा है...",
"manualIdeaDesc": "मंथन कैनवास के साथ अपना विचार साझा करें",
"manualIdeaTitle": "शीर्षक",
"manualIdeaTitlePlaceholder": "आपका विचार कुछ शब्दों में...",
"manualIdeaDescLabel": "विवरण (वैकल्पिक)",
"manualIdeaDescPlaceholder": "अपने विचार को विस्तार से बताएं...",
"activity": {
"manual_idea": "एक विचार जोड़ा",
"wave_generated": "एक लहर पैदा की",
"joined": "सत्र में शामिल हुए",
"idea_dismissed": "एक विचार को खारिज कर दिया",
"invite_created": "एक आमंत्रण बनाया"
}
}
}
}

View File

@@ -32,6 +32,8 @@
},
"sidebar": {
"notes": "Notes",
"recent": "Recente",
"quickNav": "Navigazione rapida",
"reminders": "Reminders",
"labels": "Labels",
"editLabels": "Edit labels",
@@ -40,15 +42,35 @@
"noLabelsInNotebook": "Nessuna etichetta in questo quaderno",
"archive": "Archive",
"trash": "Trash",
"clearFilter": "Remove filter"
"clearFilter": "Remove filter",
"inbox": "Posta in arrivo",
"sharedWithMe": "Condiviso con me",
"sortNewest": "Prima il più recente",
"sortOldest": "Prima il più vecchio",
"sortAlpha": "A→Z",
"accountMenu": "Menù conto",
"profile": "Profilo",
"signOut": "disconnessione",
"sortOrder": "Ordinamento",
"freezePinnedNotebook": "Blocca l'ordine della barra laterale del taccuino",
"unfreezePinnedNotebook": "Sblocca l'ordine della barra laterale del notebook",
"newSubNotebook": "Nuovo sub-notebook",
"renameNotebook": "Rinominare"
},
"notes": {
"title": "Note",
"newNote": "Nuova nota",
"reorganize": "Riorganizzare le note",
"untitled": "Senza titolo",
"placeholder": "Scrivi una nota...",
"markdownPlaceholder": "Scrivi una nota... (Markdown supportato)",
"titlePlaceholder": "Titolo",
"noteTypes": {
"richtext": "Testo ricco",
"markdown": "Ribasso",
"text": "Testo semplice",
"checklist": "Lista di controllo"
},
"listItem": "Elemento elenco",
"addListItem": "+ Elemento elenco",
"newChecklist": "Nuova checklist",
@@ -58,6 +80,7 @@
"confirmDelete": "Sei sicuro di voler eliminare questa nota?",
"confirmLeaveShare": "Sei sicuro di voler abbandonare questa nota condivisa?",
"sharedBy": "Condivisa da",
"sharedShort": "Condiviso",
"leaveShare": "Abbandona",
"delete": "Elimina",
"archive": "Archivia",
@@ -136,6 +159,8 @@
"dragToReorder": "Trascina per riordinare",
"more": "Altro",
"emptyState": "Nessuna nota qui",
"metadataPanel": "Dettagli",
"metadataNotebook": "Taccuino",
"emptyStateTabs": "Nessuna nota in questa vista. Usa \"Nuova nota\" nella barra laterale (suggerimenti titolo IA nel compositore).",
"inNotebook": "Nel notebook",
"moveFailed": "Spostamento non riuscito",
@@ -147,11 +172,6 @@
"unpinned": "Non fissato",
"redoShortcut": "Ripeti (Ctrl+Y)",
"undoShortcut": "Annulla (Ctrl+Z)",
"viewCards": "Vista schede",
"viewCardsTooltip": "Griglia di schede con riordino tramite trascinamento",
"viewTabs": "Vista elenco",
"viewTabsTooltip": "Schede in alto, nota sotto — trascina per riordinare",
"viewModeGroup": "Modalità di visualizzazione note",
"reorderTabs": "Riordina scheda",
"modified": "Modificata",
"created": "Creata",
@@ -160,15 +180,18 @@
"savedStatus": "Salvato",
"dirtyStatus": "Modificato",
"completedLabel": "Completati",
"notes.emptyNotebook": "Quaderno vuoto",
"notes.emptyNotebookDesc": "Questo quaderno non ha note. Clicca + per crearne una.",
"notes.noNoteSelected": "Nessuna nota selezionata",
"notes.selectOrCreateNote": "Seleziona una nota dalla lista o creane una nuova.",
"notes": {
"emptyNotebook": "Quaderno vuoto",
"emptyNotebookDesc": "Questo quaderno non ha note. Clicca + per crearne una.",
"noNoteSelected": "Nessuna nota selezionata",
"selectOrCreateNote": "Seleziona una nota dalla lista o creane una nuova."
},
"commitVersion": "Salva versione",
"versionSaved": "Versione salvata",
"deleteVersion": "Elimina questa versione",
"versionDeleted": "Versione eliminata",
"deleteVersionConfirm": "Eliminare questa versione definitivamente?",
"deleteVersionDesc": "Questa azione non può essere annullata. La versione verrà eliminata definitivamente dalla cronologia.",
"historyMode": "Modalità cronologia",
"historyModeManual": "Manuale (pulsante commit)",
"historyModeAuto": "Automatico (intelligente)",
@@ -184,6 +207,10 @@
"enableHistory": "Attiva cronologia",
"historyEmpty": "Nessuna versione disponibile",
"historySelectVersion": "Seleziona una versione per visualizzarne l'anteprima",
"currentVersion": "attuale",
"compareVersions": "Confrontare",
"diffTitle": "Confronto",
"diffSelectHint": "Fare clic su 2 versioni nell'elenco per confrontarle",
"sortBy": "Ordina per",
"sortDateDesc": "Data (recente)",
"sortDateAsc": "Data (meno recente)",
@@ -197,10 +224,14 @@
"createFailed": "Failed to create note",
"updateFailed": "Failed to update note",
"archived": "Note archived",
"unarchivedSuccess": "Nota rimossa dall'archivio",
"archiveFailed": "Failed to archive",
"sort": "Sort",
"confirmDeleteTitle": "Delete note",
"leftShare": "Share removed",
"ideaOrigin": "Origin of the idea",
"noNoteLink": "Purely generative idea",
"dismiss": "Not pertinent",
"dismissed": "Note dismissed from recent",
"generalNotes": "General Notes",
"noteType": "Tipo di nota",
@@ -214,7 +245,23 @@
"switchTypeTitle": "Cambiare tipo di nota?",
"switchTypeWarning": "Alcune formattazioni potrebbero andare perse con {type}.",
"switchTypeContentPreserved": "Il contenuto sarà preservato come testo semplice.",
"switchType": "Passa a {type}"
"switchType": "Passa a {type}",
"saveNow": "Risparmia ora",
"backToCollection": "Ritorno alla raccolta",
"markdownEditingTitle": "Ritorna alla modifica",
"markdownPreviewTitle": "Anteprima",
"brainstormThisIdea": "Raccogli questa idea",
"brainstormThisIdeaAria": "Raccogli questa idea",
"shareNoteTitle": "Condividi nota",
"shareNoteAria": "Condividi nota",
"saveNoteAria": "Salva nota",
"noChangesToSaveAria": "Nessuna modifica da salvare",
"optionsMenuAria": "Menù delle opzioni",
"deleteNoteConfirmItem": "Elimina nota",
"noteDeletedToast": "Nota cancellata.",
"deleteNoteFailedToast": "Impossibile eliminare.",
"documentInfoAria": "Informazioni sul documento",
"noModification": "Nessun cambiamento"
},
"pagination": {
"previous": "←",
@@ -296,7 +343,24 @@
"accessRevoked": "Laccesso è stato revocato",
"errorLoading": "Errore nel caricamento dei collaboratori",
"failedToAdd": "Impossibile aggiungere il collaboratore",
"failedToRemove": "Impossibile rimuovere il collaboratore"
"failedToRemove": "Impossibile rimuovere il collaboratore",
"shareCompactTitle": "Condividere",
"inviteByEmailLabel": "Invita tramite e-mail",
"accessReadCompact": "Visualizzazione",
"accessEditCompact": "Modificare",
"sendInvitation": "Invia invito",
"invitationSentBadge": "Invito inviato",
"sharedAccessLabel": "Accesso condiviso",
"noCollaboratorsEmpty": "Nessun collaboratore ancora.",
"removeAccessTitle": "Rimuovere l'accesso",
"toastInviteSentTo": "Invito inviato a {email}",
"toastAccessRemoved": "Accesso rimosso per {target}",
"toastUserFallback": "l'utente",
"toastSharingError": "Errore di condivisione",
"toastEmailNotFound": "Nessun account trovato con questa email.",
"toastAlreadySharedUser": "Questa nota è già condivisa con questo utente.",
"toastRemoveAccessFailed": "Impossibile rimuovere l'accesso.",
"userFallback": "Utente"
},
"ai": {
"analyzing": "AI analyzing...",
@@ -326,6 +390,8 @@
"transforming": "Transforming...",
"transformSuccess": "Text transformed to Markdown successfully!",
"transformError": "Error during transformation",
"convertToRichtext": "Converti in Rich Text",
"convertingToRichtext": "Conversione...",
"assistant": "AI Assistant",
"generating": "Generating...",
"generateTitles": "Generate titles",
@@ -389,6 +455,8 @@
"undoAI": "Annulla trasformazione IA",
"undoApplied": "Testo originale ripristinato",
"minWordsError": "La nota deve contenere almeno 5 parole per utilizzare le azioni IA.",
"wordCountMin": "Seleziona almeno {min} parole da riformulare (attualmente {current} parole)",
"wordCountMax": "Seleziona al massimo {max} parole da riformulare (attualmente {current} parole)",
"genericError": "Errore IA",
"actionError": "Errore durante l'azione IA",
"appliedToNote": "Applicato alla nota",
@@ -404,6 +472,15 @@
"chatTab": "Chat",
"noteActions": "Azioni nota",
"askToStart": "Chiedi qualcosa all'Assistente per iniziare.",
"chatPanelContext": "Contesto",
"chatPanelNotebookPlus": "+ Taccuino",
"chatPanelWritingTone": "Tono di scrittura",
"scopeAutoBadge": "Auto",
"chatNoteQuestionPlaceholder": "Fai una domanda su questa nota...",
"chatNotebookSelectPlaceholder": "Includere un quaderno...",
"assistantTabActions": "Azioni",
"resourcePreviewAiTitle": "Anteprima dell'IA",
"resourcePreviewInjectFromChat": "Iniettare dalla chat",
"contextLabel": "Contesto",
"thisNote": "Questa nota",
"allMyNotes": "Tutte le mie note",
@@ -415,6 +492,7 @@
"newLineHint": "Shift+Enter = nuova riga",
"resultLabel": "Risultato",
"discardAction": "Scarta",
"organization": "Organizzazione",
"transformationsDesc": "Trasformazioni — applicate direttamente alla nota",
"writeMinWordsAction": "Scrivi almeno 5 parole per attivare le azioni IA.",
"processingAction": "Elaborazione...",
@@ -425,7 +503,45 @@
"shorten": "Accorciare",
"improve": "Migliorare",
"toMarkdown": "In Markdown",
"describeImages": "Describe images"
"describeImages": "Describe images",
"fixGrammar": "Correggi la grammatica",
"translate": "Tradurre",
"explain": "Spiegare",
"toRichText": "Converti in formato RTF"
},
"generate": {
"slides": "Genera diapositive",
"sectionLabel": "Strumenti di generazione",
"theme": "Tema",
"themeArchitecturalMono": "Mono architettonico",
"themeVibrantTech": "Tecnologia vibrante",
"themeMinimalSilk": "Seta minima",
"style": "Stile",
"styleProfessional": "Professionale",
"styleCreative": "Creativo",
"styleBrutalist": "Brutalista",
"diagram": "Genera diagramma",
"diagramReadyHint": "Converti la nota in flusso visivo",
"diagramType": "Tipo di diagramma",
"typeAuto": "Rilevamento automatico",
"typeFlowchart": "Diagramma di flusso",
"typeMindMap": "Mappa mentale",
"typeTimeline": "Cronologia",
"typeOrgChart": "Organigramma",
"typeArchitecture": "Architettura",
"typeProcessMap": "Mappa dei processi",
"styleSketchy": "Abbozzato",
"styleSoft": "Morbido",
"styleMinimal": "Minimo",
"styleDraft": "Bozza",
"stylePolished": "Lucido",
"styleHandwritten": "Scritto a mano",
"diagramReady": "Il diagramma è pronto!",
"openInExcalidraw": "Apri in Excalidraw Lab",
"insertDiagramInNote": "Incorpora PNG nella nota corrente",
"diagramImageAlt": "Diagramma generato dall'intelligenza artificiale",
"insertedInNote": "Schema inserito in nota",
"insertExportError": "Errore durante l'esportazione/caricamento del diagramma"
},
"openAssistant": "Apri assistente IA",
"poweredByMomento": "Offerto da Momento AI",
@@ -442,7 +558,64 @@
"aiCopilot": "Copilot IA",
"suggestTitle": "Suggerimento titolo IA",
"generateTitleFromImage": "Generate title from image",
"titleGenerated": "Title generated from image"
"titleGenerated": "Title generated from image",
"resourceTab": "Risorsa",
"aiNoteTitle": "Nota dell'AI",
"injectReplace": "Sostituire",
"injectReplaceTitle": "Sostituisci il contenuto della nota con questo messaggio",
"injectComplete": "Completare",
"injectCompleteTitle": "Completa la nota con questo messaggio (AI)",
"injectMerge": "Unisci",
"injectMergeTitle": "Unisci con nota (AI)",
"imagesCount": "{count} immagini",
"resource": {
"failedToLoadUrl": "Impossibile caricare questo URL",
"pageLoaded": "Pagina caricata: {titolo}",
"pageLoadError": "Errore durante il caricamento della pagina",
"pasteOrUrlFirst": "Incolla prima il testo o carica un URL",
"enrichError": "Errore di arricchimento",
"enrichErrorShort": "Errore di arricchimento",
"contentApplied": "Contenuto applicato alla nota ✓",
"fromChat": "💬 Dalla chat",
"replacement": "↓ Sostituzione",
"completedByAI": "✦ Completato dall'IA",
"mergedByAI": "⟳ Uniti dall'AI",
"rendered": "Resi",
"cancel": "Cancellare",
"applyToNote": "Applicare alla nota",
"urlLabel": "URL (facoltativo)",
"resourceText": "Testo della risorsa",
"resourcePlaceholder": "Incolla qui il tuo testo (markdown, HTML, testo semplice...)",
"words": "parole",
"integrationMode": "Modalità di integrazione",
"modeReplace": "Sostituire",
"modeReplaceDesc": "Diretto, senza intelligenza artificiale",
"modeComplete": "Completare",
"modeCompleteDesc": "Aggiunge senza riscrivere",
"modeMerge": "Unisci",
"modeMergeDesc": "Riscrive e integra",
"aiProcessing": "Elaborazione dell'intelligenza artificiale...",
"preview": "Anteprima",
"generatePreview": "Genera anteprima",
"emptyNoteHint": "💡 La nota è vuota: il contenuto della risorsa verrà integrato direttamente."
},
"cancel": "Cancellare",
"copied": "Copiato",
"copy": "Copia",
"transformations": "Trasformazioni",
"otherLanguage": "Un'altra lingua",
"translateNow": "Traduci adesso",
"generationTools": "Strumenti di generazione",
"generateSlidesLoading": "⏳ Generazione della presentazione...",
"generateDiagramLoading": "⏳ Generazione del diagramma...",
"errorShort": "Errore",
"readyToast": "Pronto!",
"downloadFailedToast": "Download non riuscito",
"pptxDownloadButton": "Scarica .pptx",
"presentationReadyBadge": "Presentazione pronta",
"openInLabTitle": "Apri in laboratorio",
"inlineSummaryMarkdown": "**Riepilogo:**",
"networkErrorShort": "Errore di rete."
},
"titleSuggestions": {
"available": "Suggerimenti titolo",
@@ -548,7 +721,19 @@
"untitled": "Senza titolo",
"notifications": "Notifiche",
"declined": "Condivisione rifiutata",
"removed": "Nota rimossa dalla lista"
"removed": "Nota rimossa dalla lista",
"slidesReady": "Presentazione pronta",
"openSlides": "Presentazione aperta",
"canvasReady": "Diagramma pronto",
"pptxReady": "Diapositive pronte",
"downloadPptx": "Scarica .pptx",
"markAllRead": "Segna tutto letto",
"agentSuccess": "L'agente ha finito",
"agentFailed": "L'agente ha fallito",
"brainstormInvite": "Brainstorming",
"brainstormJoined": "Brainstorming",
"systemNotification": "Sistema",
"downloadFailed": "Download non riuscito"
},
"nav": {
"home": "Home",
@@ -597,6 +782,17 @@
"themeLight": "Light",
"themeDark": "Dark",
"themeSystem": "System",
"themeBaseGroup": "Base",
"themePalettesGroup": "Color palettes",
"themeSepia": "Sepia",
"themeMidnight": "Midnight",
"themeRose": "Rose",
"themeGreen": "Green",
"themeLavender": "Lavender",
"themeSand": "Sand",
"themeOcean": "Ocean",
"themeSunset": "Sunset",
"themeBlue": "Blue",
"notifications": "Notifications",
"language": "Language",
"selectLanguage": "Select language",
@@ -630,17 +826,8 @@
"desktopNotifications": "Notifiche desktop",
"desktopNotificationsDesc": "Ricevi notifiche nel browser",
"notificationsDesc": "Gestisci le preferenze di notifica",
"themeBaseGroup": "Base",
"themePalettesGroup": "Color palettes",
"themeSepia": "Sepia",
"themeMidnight": "Midnight",
"themeRose": "Rose",
"themeGreen": "Green",
"themeLavender": "Lavender",
"themeSand": "Sand",
"themeOcean": "Ocean",
"themeSunset": "Sunset",
"themeBlue": "Blue"
"autoSave": "Salvataggio automatico",
"autoSaveDesc": "Salva automaticamente le modifiche durante la digitazione"
},
"profile": {
"title": "Profilo",
@@ -707,7 +894,15 @@
"providerDesc": "Scegli il tuo provider AI preferito",
"providerAutoDesc": "Ollama se disponibile, altrimenti OpenAI",
"providerOllamaDesc": "100% privato, viene eseguito localmente sul tuo dispositivo",
"providerOpenAIDesc": "Più preciso, richiede chiave API"
"providerOpenAIDesc": "Più preciso, richiede chiave API",
"aiNote": "Nota dell'AI",
"aiNoteDesc": "Abilita il pulsante chat AI e gli strumenti di miglioramento del testo",
"languageDetection": "Rilevamento della lingua",
"languageDetectionDesc": "Rileva automaticamente la lingua delle tue note",
"autoLabeling": "Suggerimenti per le etichette",
"autoLabelingDesc": "Suggerisce e applica automaticamente le etichette alle tue note",
"noteHistory": "Nota la storia",
"noteHistoryDesc": "Abilita gli snapshot della versione e il ripristino dalla cronologia"
},
"general": {
"loading": "Caricamento...",
@@ -764,7 +959,9 @@
"markDone": "Segna come completato",
"markUndone": "Segna come non completato",
"todayAt": "Oggi alle {time}",
"tomorrowAt": "Domani alle {time}"
"tomorrowAt": "Domani alle {time}",
"clearCompleted": "Cancella completato",
"viewAll": "Visualizza tutti i promemoria"
},
"notebook": {
"create": "Crea notebook",
@@ -795,7 +992,11 @@
"confidence": "confidenza",
"savingReminder": "Errore nel salvataggio del promemoria",
"removingReminder": "Errore nella rimozione del promemoria",
"generatingDescription": "Please wait..."
"generatingDescription": "Please wait...",
"pinnedFrozenTooltip": "Taccuino appuntato: ordine congelato",
"organizeNotebookWithAITooltip": "Organizza questo taccuino con l'intelligenza artificiale",
"assistantRequiredForSummarize": "Attiva AI Assistant nelle impostazioni per riepilogare",
"createSubnotebook": "Aggiungi sub-notebook"
},
"notebookSuggestion": {
"title": "Spostare in {name}?",
@@ -808,6 +1009,9 @@
},
"admin": {
"title": "Admin Dashboard",
"adminConsole": "Console di amministrazione",
"navSection": "Navigazione",
"backToApp": "Torniamo a Memento",
"userManagement": "User Management",
"chat": "AI Chat",
"lab": "The Lab",
@@ -850,6 +1054,11 @@
"providerEmbeddingRequired": "AI_PROVIDER_EMBEDDING is required",
"providerOllamaOption": "🦙 Ollama (Local & Free)",
"providerOpenAIOption": "🤖 OpenAI (GPT-5, GPT-4)",
"providerAnthropicOption": "🧠 Antropico (Claude API)",
"providerAnthropicCustomOption": "🧩 Personalizzazione antropica (API Messaggi - MiniMax, ecc.)",
"anthropicModelHint": "Scegli un ID modello Claude dai suggerimenti o inseriscine uno manualmente (nessun elenco di modelli remoti per l'API ufficiale).",
"anthropicCustomModelHint": "API Messaggi compatibili con Anthropic (ad esempio MiniMax): URL di base https://api.minimax.io/anthropic (Cina: https://api.minimaxi.com/anthropic), modello MiniMax-M2.7. Incorporamenti: utilizza il provider «Personalizzato» + URL OpenAI https://api.minimax.io/v1.",
"anthropicCustomNoModelList": "Questo gateway non espone un elenco /modelli in stile OpenAI: scegli il modello dai suggerimenti o digitalo (ad esempio MiniMax-M2.7).",
"providerCustomOption": "🔧 Custom OpenAI-Compatible",
"providerDeepSeekOption": "🔍 DeepSeek",
"providerOpenRouterOption": "🌐 OpenRouter",
@@ -1003,7 +1212,14 @@
"error": "Error:",
"testError": "Test Error: {error}",
"tipTitle": "Tip:",
"tipDescription": "Use the AI Test Panel to diagnose configuration issues before testing."
"tipDescription": "Use the AI Test Panel to diagnose configuration issues before testing.",
"chatTestTitle": "Prova dell'assistente di chat",
"chatTestDescription": "Testare il provider AI utilizzato dall'assistente chat",
"chatGenerationTest": "💬 Test dell'assistente chat:",
"chatStep1": "Invia un messaggio di prova all'assistente",
"chatStep2": "Chiede una risposta concisa su cosa fa l'assistente",
"chatStep3": "Mostra la risposta del modello",
"chatStep4": "Controlla la reattività e la latenza"
},
"sidebar": {
"dashboard": "Dashboard",
@@ -1194,6 +1410,7 @@
"notesViewLabel": "Vista note",
"notesViewTabs": "Schede (stile OneNote)",
"notesViewMasonry": "Schede (griglia)",
"notesViewList": "Elenco (rivista)",
"selectTheme": "Select theme",
"fontFamilyLabel": "Famiglia di caratteri",
"fontFamilyDescription": "Scegli il carattere utilizzato in tutta l'app",
@@ -1277,6 +1494,69 @@
"organizeWithAI": "Organizza con AI",
"organize": "Organizza"
},
"organizeNotebook": {
"title": "Organizza il quaderno",
"unknownError": "Errore sconosciuto",
"toastSuccess": "Taccuino organizzato: {creato} sub-taccuino creato/i, {spostato} nota/e spostata/e",
"intro": "L'intelligenza artificiale analizzerà gli appunti di questo quaderno e proporrà un piano per riorganizzarli in sottoquaderni tematici.",
"bulletThemes": "Raggruppare le note per argomento o tema",
"bulletSubfolders": "Crea sotto-taccuini mancanti",
"bulletPreview": "Anteprima completa prima di qualsiasi modifica",
"analyzingTitle": "Analizzando...",
"analyzingSubtitle": "L'intelligenza artificiale legge i tuoi appunti e identifica i temi",
"previewSummary": "{groups} gruppo/i · {notes} note · {newSubs} nuovo/i sotto-taccuino/i",
"badgeNew": "Nuovo",
"untitledNote": "Nota senza titolo",
"notesInGroup": "{count} note",
"executingTitle": "Organizzazione…",
"executingSubtitle": "Creazione di sub-taccuini e spostamento di note",
"doneTitle": "Quaderno organizzato!",
"doneStats": "{creato} sub-taccuino creato/i · {spostato} nota/e spostata/e",
"analyzeButton": "Analizza con l'intelligenza artificiale",
"restart": "Ricominciare",
"confirm": "Fare domanda a",
"closeButton": "Vicino"
},
"documentInfo": {
"tabInfo": "Informazioni",
"tabVersions": "Versioni",
"wordsLabel": "Parole",
"charactersLabel": "Caratteri",
"notebookLabel": "Taccuino",
"typeLabel": "Tipo",
"createdLabel": "Creato",
"modifiedLabel": "Aggiornato",
"labelsSection": "Etichette",
"idLabel": "ID",
"historyDisabled": "La cronologia non è abilitata per questa nota.",
"enableHistory": "Abilita la cronologia",
"savedVersions": "Versioni salvate",
"savingEllipsis": "Risparmio…",
"versionSaved": "Versione salvata!",
"saveThisVersion": "Salva questa versione",
"loading": "Caricamento…",
"noVersion": "Nessuna versione ancora",
"restoreTooltip": "Ripristinare",
"deleteTooltip": "Eliminare",
"comparisonMode": "Modalità di confronto",
"comparisonSubtitle": "Confronta le versioni fianco a fianco",
"deleteVersionConfirm": "Eliminare questa versione?",
"latestBadge": "Ultimo"
},
"languages": {
"targets": {
"french": "francese",
"english": "Inglese",
"spanish": "spagnolo",
"german": "tedesco",
"persian": "persiano",
"portuguese": "portoghese",
"italian": "Italiano",
"chinese": "cinese",
"japanese": "giapponese"
},
"customPlaceholder": "per esempio. Arabo, russo…"
},
"common": {
"unknown": "Sconosciuto",
"notAvailable": "Non disponibile",
@@ -1398,12 +1678,16 @@
"scraper": "Monitor",
"researcher": "Ricercatore",
"monitor": "Osservatore",
"slideGenerator": "Diapositive",
"excalidrawGenerator": "Diagramma",
"custom": "Personalizzato"
},
"typeDescriptions": {
"scraper": "Estrae contenuti da più siti e crea un riepilogo",
"researcher": "Cerca informazioni su un argomento",
"monitor": "Osserva un quaderno e analizza le note",
"slideGenerator": "Crea una presentazione PowerPoint dalle note",
"excalidrawGenerator": "Crea un diagramma Excalidraw dalle note",
"custom": "Agente libero con il tuo prompt"
},
"form": {
@@ -1416,6 +1700,27 @@
"urlsOptional": "(opzionale)",
"sourceNotebook": "Quaderno da osservare",
"selectNotebook": "Seleziona un quaderno...",
"selectNotes": "Note da analizzare",
"notesSelected": "{{count}} nota/e selezionata/e",
"slideTheme": "Tema della presentazione",
"slideThemeDefault": "Automatico",
"slideStyle": "Stile visivo",
"slideStyleSoft": "Morbido (consigliato)",
"slideStyleSharp": "Affilato e denso",
"slideStyleRounded": "Arrotondato e spazioso",
"slideStylePill": "Premio/pillola",
"excalidrawDiagramType": "Tipo di diagramma",
"excalidrawDiagramTypeAuto": "Auto (rilevamento dominio)",
"excalidrawDiagramTypeFlowchart": "Diagramma di flusso (processo)",
"excalidrawDiagramTypeMindmap": "Mappa mentale (idee)",
"excalidrawDiagramTypeOrgChart": "Organigramma (team)",
"excalidrawDiagramTypeTimeline": "Cronologia/roadmap",
"excalidrawDiagramTypeProcessMap": "Mappa dei processi (operazioni)",
"excalidrawDiagramTypeArchitectureCloud": "Architettura cloud (zone/RG)",
"excalidrawDiagramStyle": "Stile diagramma Excalidraw",
"excalidrawDiagramStyleDefault": "Colorato (Excalidraw)",
"excalidrawDiagramStyleSketchPlus": "Sketch+ (Excalidraw migliorato)",
"excalidrawDiagramStyleAustere": "Austero (minimo)",
"targetNotebook": "Quaderno di destinazione",
"inbox": "In arrivo",
"instructions": "Istruzioni IA",
@@ -1485,6 +1790,8 @@
"updated": "Agente aggiornato",
"deleted": "\"{name}\" eliminato",
"deleteError": "Errore durante l'eliminazione",
"running": "Generazione in corso…",
"runningDesc": "La generazione potrebbe richiedere alcuni minuti. Puoi navigare liberamente.",
"runSuccess": "\"{name}\" eseguito con successo",
"runError": "Errore: {error}",
"runFailed": "Esecuzione fallita",
@@ -1519,13 +1826,24 @@
"chercheur": {
"name": "Ricercatore di argomenti",
"description": "Cerca informazioni approfondite su un argomento e crea una nota strutturata con riferimenti."
},
"slideGenerator": {
"name": "Generatore di diapositive",
"description": "Legge gli appunti da un taccuino e genera automaticamente una presentazione strutturata."
},
"excalidrawGenerator": {
"name": "Generatore di diagrammi",
"description": "Legge una nota e genera un diagramma visivo in Excalidraw Lab."
}
},
"runLog": {
"title": "Cronologia",
"noHistory": "Nessuna esecuzione ancora",
"toolTrace": "{count} chiamate strumento",
"step": "Passo {num}"
"step": "Passo {num}",
"clearConfirm": "Sei sicuro di voler eliminare tutta la cronologia per questo agente?",
"cleared": "Cronologia eliminata",
"clearHistory": "Cancella cronologia"
},
"tools": {
"title": "Strumenti Agente",
@@ -1536,6 +1854,9 @@
"noteCreate": "Crea Nota",
"urlFetch": "Recupera URL",
"memorySearch": "Memoria",
"generatePptx": "Diapositive PPTX",
"generateSlides": "Diapositive HTML",
"generateExcalidraw": "Diagramma Excalidraw",
"configNeeded": "config",
"selected": "{count} selezionati",
"maxSteps": "Iterazioni massime"
@@ -1547,7 +1868,9 @@
"scraper": "Sei un assistente di monitoraggio. Sintetizza gli articoli di diversi siti web in un riepilogo chiaro e strutturato.",
"researcher": "Sei un ricercatore rigoroso. Per l'argomento richiesto, produci una nota di ricerca con contesto, punti chiave, dibattiti e riferimenti.",
"monitor": "Sei un assistente analitico. Analizza le note fornite e suggerisci piste, riferimenti e connessioni tra le note.",
"custom": "Sei un assistente utile."
"custom": "Sei un assistente utile.",
"slideGenerator": "Sei un creatore di presentazioni. Leggi i contenuti forniti e crea diapositive strutturate con titoli, punti chiave e riepiloghi.",
"excalidrawGenerator": "Sei un creatore di diagrammi. Analizza il contenuto fornito e crea un diagramma visivo chiaro e organizzato."
},
"help": {
"title": "Guida agli Agenti",
@@ -1581,7 +1904,10 @@
"frequency": "Quanto spesso l'agente viene eseguito automaticamente. Inizia con Manuale per testare.",
"instructions": "Istruzioni personalizzate che sostituiscono il prompt IA predefinito. Lascia vuoto per usare quello automatico.",
"tools": "Seleziona quali strumenti può usare l'agente. Ogni strumento dà una capacità specifica all'agente.",
"maxSteps": "Numero massimo di cicli di ragionamento. Più passaggi = analisi più approfondita ma più lenta."
"maxSteps": "Numero massimo di cicli di ragionamento. Più passaggi = analisi più approfondita ma più lenta.",
"selectNotes": "Seleziona note specifiche da analizzare. Se non viene selezionato nessuno, l'agente utilizzerà tutte le note del taccuino.",
"slideTheme": "Scegli una tavolozza di colori per la presentazione. La modalità automatica lascia decidere all'intelligenza artificiale.",
"slideStyle": "Lo stile visivo influisce sul raggio dell'angolo, sulla spaziatura e sulla densità delle informazioni."
}
}
},
@@ -1631,5 +1957,147 @@
"lab": {
"initializing": "Inizializzazione spazio",
"loadingIdeas": "Caricamento delle tue idee..."
},
"richTextEditor": {
"slashHint": "↑↓ naviga · Inserisci inserisci · Sezione cambio scheda",
"slashLoading": "L'intelligenza artificiale pensa...",
"slashTabAll": "Tutto",
"slashCatBasic": "Blocchi fondamentali",
"slashCatMedia": "Media",
"slashCatFormatting": "Formattazione",
"slashCatAi": "Nota dell'AI",
"insertImage": "Inserisci immagine",
"imageUrlPlaceholder": "https://esempio.com/immagine.png",
"preview": "Anteprima",
"cancel": "Cancellare",
"insert": "Inserire",
"slashText": "Testo",
"slashTextDesc": "Paragrafo semplice",
"slashH1": "Rubrica 1",
"slashH1Desc": "Titolo di ampia sezione",
"slashH2": "Rubrica 2",
"slashH2Desc": "Titolo della sezione media",
"slashH3": "Rubrica 3",
"slashH3Desc": "Titolo di piccola sezione",
"slashBullet": "Elenco puntato",
"slashBulletDesc": "Elenco non ordinato",
"slashNumbered": "Elenco numerato",
"slashNumberedDesc": "Elenco numerato ordinato",
"slashTodo": "Elenco attività",
"slashTodoDesc": "Attività della casella di controllo",
"slashQuote": "Citazione",
"slashQuoteDesc": "Cattura una citazione",
"slashCode": "Blocco codice",
"slashCodeDesc": "Frammento di codice",
"slashDivider": "Divisore",
"slashDividerDesc": "Separatore orizzontale",
"slashTable": "Tavolo",
"slashTableDesc": "Inserisci una griglia semplice",
"slashDiagram": "Diagramma",
"slashDiagramDesc": "Genera un flusso o una mappa mentale",
"slashSlides": "Presentazione",
"slashSlidesDesc": "Genera un bellissimo mazzo di diapositive",
"slashImage": "Immagine",
"slashImageDesc": "Incorpora un'immagine dall'URL",
"slashAlignLeft": "Allinea a sinistra",
"slashAlignLeftDesc": "Allinea il testo a sinistra",
"slashAlignCenter": "Centro",
"slashAlignCenterDesc": "Centrare il testo",
"slashAlignRight": "Allinea a destra",
"slashAlignRightDesc": "Allinea il testo a destra",
"slashSuperscript": "Apice",
"slashSuperscriptDesc": "Testo sopra la linea di base",
"slashSubscript": "Pedice",
"slashSubscriptDesc": "Testo sotto la linea di base",
"slashClarify": "Chiarire",
"slashClarifyDesc": "Rendi il testo più chiaro",
"slashShorten": "Accorciare",
"slashShortenDesc": "Condensare il testo",
"slashImprove": "Migliorare",
"slashImproveDesc": "Migliora lo stile",
"slashExpand": "Espandere",
"slashExpandDesc": "Elaborare e arricchire il testo",
"imageModalTitle": "Inserisci immagine",
"imageModalPreview": "Anteprima",
"imageModalCancel": "Cancellare",
"imageModalInsert": "Inserire",
"imageModalInvalidUrl": "Inserisci un URL valido",
"imageModalLoadFailed": "Impossibile caricare l'immagine",
"linkPlaceholder": "Incolla o digita un collegamento...",
"bold": "Grassetto",
"italic": "Corsivo",
"underline": "Sottolineare",
"strike": "Barrato",
"code": "Codice",
"highlight": "Evidenziare",
"superscript": "Apice",
"subscript": "Pedice",
"addBlock": "Aggiungi blocco",
"placeholder": "Digita \"/\" per i comandi..."
},
"brainstorm": {
"title": "Waves of Thought",
"subtitle": "Unfold dimensions of potentiality",
"placeholder": "Enter a concept to unfold...",
"generating": "AI is harvesting seeds of thought...",
"newBrainstorm": "New Brainstorm",
"noSessions": "No brainstorms yet",
"startOne": "Start one",
"sessions": "Brainstorms",
"seedLabel": "Seed Idea",
"ideaPromptDetailed": "Inserisci la tua idea, domanda o argomento per il brainstorming...",
"brainstormThisIdea": "Brainstorm this idea",
"startBrainstorm": "Start Brainstorm",
"spatialMode": "Spatial Exploration Mode",
"wave1": "Wave 1",
"wave2": "Wave 2",
"wave3": "Wave 3",
"export": "Export",
"exporting": "Exporting...",
"wave": "Wave",
"novelty": "Novelty",
"originConnection": "Origin connection",
"linkedNotes": "Linked notes",
"deepen": "Deepen",
"deepening": "Generating...",
"extract": "Create Note",
"converting": "Converting...",
"dismiss": "Not pertinent",
"noteCreated": "Note Created",
"ideas": "ideas",
"cancel": "Cancel",
"delete": "Delete",
"ideaOrigin": "Origin of the idea",
"noNoteLink": "Purely generative idea",
"derived_from": "Derived from",
"opposes": "In opposition with",
"extends": "Extends",
"synthesizes": "Synthesizes",
"transposes": "Transposes",
"none_found": "No note link",
"viewNote": "View note",
"addIdea": "Add idea",
"manualIdeaPrompt": "Title of your idea:",
"invite": "Invite",
"linkCopied": "Invite link copied!",
"activityTitle": "Attività",
"noActivity": "Nessuna attività ancora",
"justNow": "proprio adesso",
"humanIdea": "Umano",
"aiIdea": "AI",
"respondsTo": "Risponde a",
"adding": "Aggiunta...",
"manualIdeaDesc": "Condividi la tua idea con la tela del brainstorming",
"manualIdeaTitle": "Titolo",
"manualIdeaTitlePlaceholder": "La tua idea in poche parole...",
"manualIdeaDescLabel": "Descrizione (facoltativa)",
"manualIdeaDescPlaceholder": "Elabora la tua idea...",
"activity": {
"manual_idea": "ha aggiunto un'idea",
"wave_generated": "generato un'onda",
"joined": "si è unito alla sessione",
"idea_dismissed": "scartò un'idea",
"invite_created": "creato un invito"
}
}
}
}

View File

@@ -32,6 +32,8 @@
},
"sidebar": {
"notes": "ノート",
"recent": "最近の",
"quickNav": "クイックナビゲーション",
"reminders": "リマインダー",
"labels": "ラベル",
"editLabels": "ラベルを編集",
@@ -40,15 +42,35 @@
"noLabelsInNotebook": "このノートブックにはまだラベルがありません",
"archive": "アーカイブ",
"trash": "ゴミ箱",
"clearFilter": "Remove filter"
"clearFilter": "Remove filter",
"inbox": "受信箱",
"sharedWithMe": "私と共有しました",
"sortNewest": "新しい順",
"sortOldest": "古い順",
"sortAlpha": "A→Z",
"accountMenu": "アカウントメニュー",
"profile": "プロフィール",
"signOut": "サインアウト",
"sortOrder": "並べ替え順序",
"freezePinnedNotebook": "ノートブックのサイドバーの順序を固定する",
"unfreezePinnedNotebook": "ノートブックのサイドバーの順序の固定を解除する",
"newSubNotebook": "新しいサブノート",
"renameNotebook": "名前の変更"
},
"notes": {
"title": "ノート",
"newNote": "新しいノート",
"reorganize": "メモを再整理する",
"untitled": "無題",
"placeholder": "ノートを作成...",
"markdownPlaceholder": "ノートを作成...Markdown対応",
"titlePlaceholder": "タイトル",
"noteTypes": {
"richtext": "リッチテキスト",
"markdown": "マークダウン",
"text": "プレーンテキスト",
"checklist": "チェックリスト"
},
"listItem": "リスト項目",
"addListItem": "+ リスト項目",
"newChecklist": "新しいチェックリスト",
@@ -58,6 +80,7 @@
"confirmDelete": "このノートを削除してもよろしいですか?",
"confirmLeaveShare": "この共有ノートを退出してもよろしいですか?",
"sharedBy": "共有者",
"sharedShort": "共有",
"leaveShare": "退出",
"delete": "削除",
"archive": "アーカイブ",
@@ -136,6 +159,8 @@
"dragToReorder": "ドラッグして並べ替え",
"more": "もっと見る",
"emptyState": "ノートがありません",
"metadataPanel": "詳細",
"metadataNotebook": "ノート",
"emptyStateTabs": "まだートがありません。サイドバーの「新しいート」を使って追加してくださいAIタイトル提案が作成画面に表示されます。",
"inNotebook": "ノートブック内",
"moveFailed": "移動に失敗しました",
@@ -147,11 +172,6 @@
"unpinned": "ピン留め解除",
"redoShortcut": "やり直し (Ctrl+Y)",
"undoShortcut": "元に戻す (Ctrl+Z)",
"viewCards": "カード表示",
"viewCardsTooltip": "ドラッグ&ドロップで並べ替え可能なカードグリッド",
"viewTabs": "リスト表示",
"viewTabsTooltip": "上部にタブ、下にノート — タブをドラッグで並べ替え",
"viewModeGroup": "ノートの表示モード",
"reorderTabs": "タブを並べ替え",
"modified": "更新日時",
"created": "作成日時",
@@ -160,15 +180,18 @@
"savedStatus": "保存済み",
"dirtyStatus": "変更済み",
"completedLabel": "完了",
"notes.emptyNotebook": "空のノートブック",
"notes.emptyNotebookDesc": "のノートブックにはノートがありません。+ をクリックして作成。",
"notes.noNoteSelected": "ノート未選択",
"notes.selectOrCreateNote": "リストからノート選択または新規作成してください。",
"notes": {
"emptyNotebook": "のノートブック",
"emptyNotebookDesc": "このノートブックにはノートがありません。+ をクリックして作成。",
"noNoteSelected": "ノート選択",
"selectOrCreateNote": "リストからノートを選択または新規作成してください。"
},
"commitVersion": "バージョンを保存",
"versionSaved": "バージョンを保存しました",
"deleteVersion": "このバージョンを削除",
"versionDeleted": "バージョンを削除しました",
"deleteVersionConfirm": "このバージョンを完全に削除しますか?",
"deleteVersionDesc": "この操作は元に戻すことができません。バージョンは履歴から完全に削除されます。",
"historyMode": "履歴モード",
"historyModeManual": "手動(コミットボタン)",
"historyModeAuto": "自動(スマート)",
@@ -184,6 +207,10 @@
"enableHistory": "履歴を有効にする",
"historyEmpty": "バージョンがありません",
"historySelectVersion": "プレビューするバージョンを選択してください",
"currentVersion": "現在",
"compareVersions": "比較する",
"diffTitle": "比較",
"diffSelectHint": "リスト内の 2 つのバージョンをクリックして比較します",
"sortBy": "並び替え",
"sortDateDesc": "日付(新しい)",
"sortDateAsc": "日付(古い)",
@@ -197,10 +224,14 @@
"createFailed": "Failed to create note",
"updateFailed": "Failed to update note",
"archived": "Note archived",
"unarchivedSuccess": "メモがアーカイブから削除されました",
"archiveFailed": "Failed to archive",
"sort": "Sort",
"confirmDeleteTitle": "Delete note",
"leftShare": "Share removed",
"ideaOrigin": "Origin of the idea",
"noNoteLink": "Purely generative idea",
"dismiss": "Not pertinent",
"dismissed": "Note dismissed from recent",
"generalNotes": "General Notes",
"noteType": "ノートタイプ",
@@ -214,7 +245,23 @@
"switchTypeTitle": "ノートタイプを切り替えますか?",
"switchTypeWarning": "{type} に切り替えると書式が失われる場合があります。",
"switchTypeContentPreserved": "内容はプレーンテキストとして保持されます。",
"switchType": "{type} に切り替え"
"switchType": "{type} に切り替え",
"saveNow": "今すぐ保存",
"backToCollection": "コレクションに戻る",
"markdownEditingTitle": "編集に戻る",
"markdownPreviewTitle": "プレビュー",
"brainstormThisIdea": "このアイデアについてブレインストーミングを行う",
"brainstormThisIdeaAria": "このアイデアについてブレインストーミングを行う",
"shareNoteTitle": "メモを共有する",
"shareNoteAria": "メモを共有する",
"saveNoteAria": "メモを保存する",
"noChangesToSaveAria": "保存する変更はありません",
"optionsMenuAria": "オプションメニュー",
"deleteNoteConfirmItem": "メモの削除",
"noteDeletedToast": "注は削除されました。",
"deleteNoteFailedToast": "削除できませんでした。",
"documentInfoAria": "文書情報",
"noModification": "変更なし"
},
"pagination": {
"previous": "←",
@@ -296,7 +343,24 @@
"accessRevoked": "アクセス権が取り消されました",
"errorLoading": "共同編集者の読み込みエラー",
"failedToAdd": "共同編集者の追加に失敗しました",
"failedToRemove": "共同編集者の削除に失敗しました"
"failedToRemove": "共同編集者の削除に失敗しました",
"shareCompactTitle": "共有",
"inviteByEmailLabel": "メールで招待する",
"accessReadCompact": "ビュー",
"accessEditCompact": "編集",
"sendInvitation": "招待状を送信する",
"invitationSentBadge": "招待状を送信しました",
"sharedAccessLabel": "共有アクセス",
"noCollaboratorsEmpty": "まだ協力者はいません。",
"removeAccessTitle": "アクセス権の削除",
"toastInviteSentTo": "招待状を {email} に送信しました",
"toastAccessRemoved": "{target} のアクセスが削除されました",
"toastUserFallback": "ユーザー",
"toastSharingError": "共有エラー",
"toastEmailNotFound": "このメールではアカウントが見つかりませんでした。",
"toastAlreadySharedUser": "このメモはすでにこのユーザーと共有されています。",
"toastRemoveAccessFailed": "アクセスを削除できませんでした。",
"userFallback": "ユーザー"
},
"ai": {
"analyzing": "AI分析中...",
@@ -326,6 +390,8 @@
"transforming": "変換中...",
"transformSuccess": "テキストをMarkdownに正常に変換しました",
"transformError": "変換中のエラー",
"convertToRichtext": "リッチテキストに変換",
"convertingToRichtext": "変換中...",
"assistant": "AIアシスタント",
"generating": "生成中...",
"generateTitles": "タイトルを生成",
@@ -389,6 +455,8 @@
"undoAI": "AI変換を取り消し",
"undoApplied": "元のテキストに戻しました",
"minWordsError": "AIアクションを使用するには、ートに5語以上が必要です。",
"wordCountMin": "再定式化するには少なくとも {min} 語を選択してください (現在 {current} 語)",
"wordCountMax": "再定式化するには最大 {max} 語を選択してください (現在は {current} 語)",
"genericError": "AIエラー",
"actionError": "AIアクション中にエラー",
"appliedToNote": "ノートに適用しました",
@@ -404,6 +472,15 @@
"chatTab": "チャット",
"noteActions": "ノートアクション",
"askToStart": "アシスタントに質問して始めましょう。",
"chatPanelContext": "コンテクスト",
"chatPanelNotebookPlus": "+ ノートブック",
"chatPanelWritingTone": "書き口調",
"scopeAutoBadge": "自動",
"chatNoteQuestionPlaceholder": "このノートについて質問する...",
"chatNotebookSelectPlaceholder": "ノートも入れて…",
"assistantTabActions": "アクション",
"resourcePreviewAiTitle": "AI プレビュー",
"resourcePreviewInjectFromChat": "チャットから注入",
"contextLabel": "コンテキスト",
"thisNote": "このノート",
"allMyNotes": "すべてのノート",
@@ -415,6 +492,7 @@
"newLineHint": "Shift+Enter = 改行",
"resultLabel": "結果",
"discardAction": "破棄",
"organization": "組織",
"transformationsDesc": "変換 — ノートに直接適用",
"writeMinWordsAction": "AIアクションを有効にするには5語以上書いてください。",
"processingAction": "処理中...",
@@ -425,7 +503,45 @@
"shorten": "短縮",
"improve": "改善",
"toMarkdown": "Markdownに",
"describeImages": "Describe images"
"describeImages": "Describe images",
"fixGrammar": "文法を修正する",
"translate": "翻訳する",
"explain": "説明する",
"toRichText": "リッチテキストに変換する"
},
"generate": {
"slides": "スライドの生成",
"sectionLabel": "生成ツール",
"theme": "テーマ",
"themeArchitecturalMono": "建築モノ",
"themeVibrantTech": "活気に満ちた技術",
"themeMinimalSilk": "ミニマルシルク",
"style": "スタイル",
"styleProfessional": "プロ",
"styleCreative": "クリエイティブ",
"styleBrutalist": "ブルータリスト",
"diagram": "ダイアグラムの生成",
"diagramReadyHint": "メモを視覚的なフローに変換する",
"diagramType": "図の種類",
"typeAuto": "自動検出",
"typeFlowchart": "フローチャート",
"typeMindMap": "マインドマップ",
"typeTimeline": "タイムライン",
"typeOrgChart": "組織図",
"typeArchitecture": "建築",
"typeProcessMap": "プロセスマップ",
"styleSketchy": "大ざっぱな",
"styleSoft": "柔らかい",
"styleMinimal": "最小限",
"styleDraft": "下書き",
"stylePolished": "ポリッシュ",
"styleHandwritten": "手書き",
"diagramReady": "図が完成しました!",
"openInExcalidraw": "Excalidraw Lab で開く",
"insertDiagramInNote": "現在のノートに PNG を埋め込む",
"diagramImageAlt": "AI が生成した図",
"insertedInNote": "メモに挿入された図",
"insertExportError": "図のエクスポート/アップロード中にエラーが発生しました"
},
"openAssistant": "AIアシスタントを開く",
"poweredByMomento": "Momento AI搭載",
@@ -442,7 +558,64 @@
"aiCopilot": "AIコパイロット",
"suggestTitle": "AIタイトル提案",
"generateTitleFromImage": "Generate title from image",
"titleGenerated": "Title generated from image"
"titleGenerated": "Title generated from image",
"resourceTab": "リソース",
"aiNoteTitle": "AIート",
"injectReplace": "交換する",
"injectReplaceTitle": "メモの内容をこのメッセージに置き換えます",
"injectComplete": "完了",
"injectCompleteTitle": "このメッセージをメモに記入してください (AI)",
"injectMerge": "マージ",
"injectMergeTitle": "メモと結合AI",
"imagesCount": "{count} 枚の画像",
"resource": {
"failedToLoadUrl": "この URL を読み込めませんでした",
"pageLoaded": "読み込まれたページ: {title}",
"pageLoadError": "ページの読み込みエラー",
"pasteOrUrlFirst": "テキストを貼り付けるか、最初に URL をロードしてください",
"enrichError": "エンリッチメントエラー",
"enrichErrorShort": "エンリッチメントエラー",
"contentApplied": "注記に適用される内容 ✓",
"fromChat": "💬チャットから",
"replacement": "↓ 交換",
"completedByAI": "✦ AIによって完成",
"mergedByAI": "⟳ AIによる統合",
"rendered": "レンダリング済み",
"cancel": "キャンセル",
"applyToNote": "ノートに応募する",
"urlLabel": "URL (オプション)",
"resourceText": "リソーステキスト",
"resourcePlaceholder": "ここにテキストを貼り付けます (マークダウン、HTML、プレーン テキストなど)。",
"words": "言葉",
"integrationMode": "統合モード",
"modeReplace": "交換する",
"modeReplaceDesc": "直接、AI なし",
"modeComplete": "完了",
"modeCompleteDesc": "書き換えずに追加します",
"modeMerge": "マージ",
"modeMergeDesc": "書き換えて統合する",
"aiProcessing": "AI処理…",
"preview": "プレビュー",
"generatePreview": "プレビューの生成",
"emptyNoteHint": "💡 メモは空です。リソースのコンテンツは直接統合されます。"
},
"cancel": "キャンセル",
"copied": "コピーされました",
"copy": "コピー",
"transformations": "変換",
"otherLanguage": "別の言語",
"translateNow": "今すぐ翻訳",
"generationTools": "生成ツール",
"generateSlidesLoading": "⏳ プレゼンテーションを生成中...",
"generateDiagramLoading": "⏳ ダイアグラムを生成中...",
"errorShort": "エラー",
"readyToast": "準備ができて!",
"downloadFailedToast": "ダウンロードに失敗しました",
"pptxDownloadButton": ".pptxをダウンロード",
"presentationReadyBadge": "プレゼンテーションの準備完了",
"openInLabTitle": "ラボで開く",
"inlineSummaryMarkdown": "**まとめ:**",
"networkErrorShort": "ネットワークエラー。"
},
"titleSuggestions": {
"available": "タイトルの提案",
@@ -548,7 +721,19 @@
"untitled": "無題",
"notifications": "通知",
"declined": "共有を拒否しました",
"removed": "リストからノートを削除しました"
"removed": "リストからノートを削除しました",
"slidesReady": "プレゼンテーションの準備完了",
"openSlides": "オープンプレゼンテーション",
"canvasReady": "図の準備ができました",
"pptxReady": "スライドの準備ができました",
"downloadPptx": ".pptxをダウンロード",
"markAllRead": "すべて既読としてマークする",
"agentSuccess": "エージェントが終了しました",
"agentFailed": "エージェントが失敗しました",
"brainstormInvite": "ブレーンストーミング",
"brainstormJoined": "ブレーンストーミング",
"systemNotification": "システム",
"downloadFailed": "ダウンロードに失敗しました"
},
"nav": {
"home": "ホーム",
@@ -597,6 +782,17 @@
"themeLight": "ライト",
"themeDark": "ダーク",
"themeSystem": "システム",
"themeBaseGroup": "Base",
"themePalettesGroup": "Color palettes",
"themeSepia": "Sepia",
"themeMidnight": "Midnight",
"themeRose": "Rose",
"themeGreen": "Green",
"themeLavender": "Lavender",
"themeSand": "Sand",
"themeOcean": "Ocean",
"themeSunset": "Sunset",
"themeBlue": "Blue",
"notifications": "通知",
"language": "言語",
"selectLanguage": "言語を選択",
@@ -630,17 +826,8 @@
"desktopNotifications": "デスクトップ通知",
"desktopNotificationsDesc": "ブラウザで通知を受け取ります",
"notificationsDesc": "通知設定を管理します",
"themeBaseGroup": "Base",
"themePalettesGroup": "Color palettes",
"themeSepia": "Sepia",
"themeMidnight": "Midnight",
"themeRose": "Rose",
"themeGreen": "Green",
"themeLavender": "Lavender",
"themeSand": "Sand",
"themeOcean": "Ocean",
"themeSunset": "Sunset",
"themeBlue": "Blue"
"autoSave": "自動保存",
"autoSaveDesc": "入力中に変更を自動的に保存する"
},
"profile": {
"title": "プロフィール",
@@ -707,7 +894,15 @@
"providerDesc": "お好みのAIプロバイダーを選択",
"providerAutoDesc": "Ollama優先、OpenAIフォールバック",
"providerOllamaDesc": "100%プライベート、ローカルで実行",
"providerOpenAIDesc": "最も正確、APIキーが必要"
"providerOpenAIDesc": "最も正確、APIキーが必要",
"aiNote": "AIート",
"aiNoteDesc": "AI チャット ボタンとテキスト改善ツールを有効にする",
"languageDetection": "言語検出",
"languageDetectionDesc": "メモの言語を自動的に検出します",
"autoLabeling": "ラベルの提案",
"autoLabelingDesc": "ラベルを自動的に提案してメモに適用します",
"noteHistory": "メモ履歴",
"noteHistoryDesc": "バージョンのスナップショットと履歴からの復元を有効にする"
},
"general": {
"loading": "読み込み中...",
@@ -764,7 +959,9 @@
"markDone": "完了にする",
"markUndone": "未完了にする",
"todayAt": "今日 {time}",
"tomorrowAt": "明日 {time}"
"tomorrowAt": "明日 {time}",
"clearCompleted": "クリア完了",
"viewAll": "すべてのリマインダーを表示"
},
"notebook": {
"create": "ノートブックを作成",
@@ -795,7 +992,11 @@
"confidence": "信頼度",
"savingReminder": "リマインダーの保存に失敗しました",
"removingReminder": "リマインダーの削除に失敗しました",
"generatingDescription": "Please wait..."
"generatingDescription": "Please wait...",
"pinnedFrozenTooltip": "固定されたノートブック — 注文は凍結されています",
"organizeNotebookWithAITooltip": "このノートを AI で整理する",
"assistantRequiredForSummarize": "設定で AI アシスタントをオンにして要約します",
"createSubnotebook": "サブノートブックを追加する"
},
"notebookSuggestion": {
"title": "{name}に移動しますか?",
@@ -808,6 +1009,9 @@
},
"admin": {
"title": "管理ダッシュボード",
"adminConsole": "管理コンソール",
"navSection": "ナビゲーション",
"backToApp": "メメントに戻る",
"userManagement": "ユーザー管理",
"chat": "AIチャット",
"lab": "ラボ",
@@ -850,6 +1054,11 @@
"providerEmbeddingRequired": "AI_PROVIDER_EMBEDDINGは必須です",
"providerOllamaOption": "🦙 Ollama (Local & Free)",
"providerOpenAIOption": "🤖 OpenAI (GPT-5, GPT-4)",
"providerAnthropicOption": "🧠 人間性 (クロード API)",
"providerAnthropicCustomOption": "🧩 Anthropic カスタム (メッセージ API — MiniMax など)",
"anthropicModelHint": "候補からクロード モデル ID を選択するか、手動で入力します (公式 API のリモート モデル リストはありません)。",
"anthropicCustomModelHint": "Anthropic 互換のメッセージ API (例: MiniMax): ベース URL https://api.minimax.io/anthropic (中国: https://api.minimaxi.com/anthropic)、モデル MiniMax-M2.7。埋め込み: プロバイダー「カスタム」+ OpenAI URL https://api.minimax.io/v1 を使用します。",
"anthropicCustomNoModelList": "このゲートウェイは OpenAI スタイルの /models リストを公開しません。候補からモデルを選択するか、モデルを入力します (例: MiniMax-M2.7)。",
"providerCustomOption": "🔧 Custom OpenAI-Compatible",
"providerDeepSeekOption": "🔍 DeepSeek",
"providerOpenRouterOption": "🌐 OpenRouter",
@@ -1003,7 +1212,14 @@
"error": "エラー:",
"testError": "テストエラー:{error}",
"tipTitle": "ヒント:",
"tipDescription": "テスト前にAIテストパネルを使用して設定の問題を診断してください。"
"tipDescription": "テスト前にAIテストパネルを使用して設定の問題を診断してください。",
"chatTestTitle": "チャットアシスタント試験",
"chatTestDescription": "チャット アシスタントで使用される AI プロバイダーをテストする",
"chatGenerationTest": "💬 チャットアシスタントテスト:",
"chatStep1": "アシスタントにテスト メッセージを送信します",
"chatStep2": "アシスタントの仕事について簡潔な回答を求めます",
"chatStep3": "モデル応答を表示します",
"chatStep4": "応答性と遅延をチェックします"
},
"sidebar": {
"dashboard": "ダッシュボード",
@@ -1194,6 +1410,7 @@
"notesViewLabel": "ノートのレイアウト",
"notesViewTabs": "タブOneNote風",
"notesViewMasonry": "カード(グリッド)",
"notesViewList": "一覧(雑誌)",
"selectTheme": "Select theme",
"fontFamilyLabel": "フォントファミリー",
"fontFamilyDescription": "アプリ全体で使用するフォントを選択してください",
@@ -1277,6 +1494,69 @@
"organizeWithAI": "AIで整理",
"organize": "整理"
},
"organizeNotebook": {
"title": "ノートを整理する",
"unknownError": "不明なエラー",
"toastSuccess": "ノートブックの整理 — {created} 個のサブノートブックが作成され、{moved} 個のノートが移動されました",
"intro": "AI はこのノートブック内のノートを分析し、テーマ別のサブノートブックに再編成する計画を提案します。",
"bulletThemes": "トピックまたはテーマごとにメモをグループ化する",
"bulletSubfolders": "不足しているサブノートブックを作成する",
"bulletPreview": "変更前の完全なプレビュー",
"analyzingTitle": "分析中…",
"analyzingSubtitle": "AI があなたのメモを読んでテーマを特定します",
"previewSummary": "{groups} 個のグループ · {notes} 個のノート · {newSubs} 個の新しいサブノートブック",
"badgeNew": "新しい",
"untitledNote": "無題のメモ",
"notesInGroup": "{count} 個の音符",
"executingTitle": "整理中…",
"executingSubtitle": "サブノートブックの作成とノートの移動",
"doneTitle": "ノート整理整頓!",
"doneStats": "{created} 個のサブノートブックが作成されました · {moved} 個のノートが移動されました",
"analyzeButton": "AIで分析する",
"restart": "やり直す",
"confirm": "適用する",
"closeButton": "近い"
},
"documentInfo": {
"tabInfo": "情報",
"tabVersions": "バージョン",
"wordsLabel": "言葉",
"charactersLabel": "キャラクター",
"notebookLabel": "ノート",
"typeLabel": "タイプ",
"createdLabel": "作成されました",
"modifiedLabel": "更新されました",
"labelsSection": "ラベル",
"idLabel": "ID",
"historyDisabled": "このメモの履歴は有効になっていません。",
"enableHistory": "履歴を有効にする",
"savedVersions": "保存されたバージョン",
"savingEllipsis": "保存中…",
"versionSaved": "バージョンが保存されました!",
"saveThisVersion": "このバージョンを保存する",
"loading": "読み込み中…",
"noVersion": "まだバージョンがありません",
"restoreTooltip": "復元する",
"deleteTooltip": "消去",
"comparisonMode": "比較モード",
"comparisonSubtitle": "バージョンを並べて比較する",
"deleteVersionConfirm": "このバージョンを削除しますか?",
"latestBadge": "最新"
},
"languages": {
"targets": {
"french": "フランス語",
"english": "英語",
"spanish": "スペイン語",
"german": "ドイツ語",
"persian": "ペルシア語",
"portuguese": "ポルトガル語",
"italian": "イタリア語",
"chinese": "中国語",
"japanese": "日本語"
},
"customPlaceholder": "例えばアラビア語、ロシア語…"
},
"common": {
"unknown": "不明",
"notAvailable": "利用不可",
@@ -1398,12 +1678,16 @@
"scraper": "モニター",
"researcher": "リサーチャー",
"monitor": "オブザーバー",
"slideGenerator": "スライド",
"excalidrawGenerator": "ダイアグラム",
"custom": "カスタム"
},
"typeDescriptions": {
"scraper": "複数のサイトをスクレイピングして要約を作成",
"researcher": "トピックに関する情報を検索",
"monitor": "ノートブックを監視しノートを分析",
"slideGenerator": "メモから PowerPoint プレゼンテーションを作成します",
"excalidrawGenerator": "メモから Excalidraw 図を作成します",
"custom": "独自のプロンプトを持つ自由エージェント"
},
"form": {
@@ -1416,6 +1700,27 @@
"urlsOptional": "(任意)",
"sourceNotebook": "監視するノートブック",
"selectNotebook": "ノートブックを選択...",
"selectNotes": "分析するためのメモ",
"notesSelected": "{{count}} 個のノートが選択されました",
"slideTheme": "発表テーマ",
"slideThemeDefault": "自動",
"slideStyle": "ビジュアルスタイル",
"slideStyleSoft": "ソフト(推奨)",
"slideStyleSharp": "シャープで緻密",
"slideStyleRounded": "丸みがあって広々とした",
"slideStylePill": "プレミアム/ピル",
"excalidrawDiagramType": "図の種類",
"excalidrawDiagramTypeAuto": "自動 (ドメイン検出)",
"excalidrawDiagramTypeFlowchart": "フローチャート(プロセス)",
"excalidrawDiagramTypeMindmap": "マインドマップ(アイデア)",
"excalidrawDiagramTypeOrgChart": "組織図 (チーム)",
"excalidrawDiagramTypeTimeline": "タイムライン/ロードマップ",
"excalidrawDiagramTypeProcessMap": "プロセスマップ(業務)",
"excalidrawDiagramTypeArchitectureCloud": "クラウド アーキテクチャ (ゾーン/RG)",
"excalidrawDiagramStyle": "Excalidraw ダイアグラム スタイル",
"excalidrawDiagramStyleDefault": "色付き (Excalidraw)",
"excalidrawDiagramStyleSketchPlus": "Sketch+ (拡張 Excalidraw)",
"excalidrawDiagramStyleAustere": "質素(最小限)",
"targetNotebook": "対象ノートブック",
"inbox": "受信箱",
"instructions": "AIの指示",
@@ -1485,6 +1790,8 @@
"updated": "エージェントを更新しました",
"deleted": "「{name}」を削除しました",
"deleteError": "削除エラー",
"running": "生成中…",
"runningDesc": "生成には数分かかる場合があります。自由にナビゲートできます。",
"runSuccess": "「{name}」が正常に実行されました",
"runError": "エラー:{error}",
"runFailed": "実行に失敗しました",
@@ -1519,13 +1826,24 @@
"chercheur": {
"name": "トピックリサーチャー",
"description": "トピックに関する詳細情報を検索し、参考文献付きの構造化ノートを作成します。"
},
"slideGenerator": {
"name": "スライドジェネレーター",
"description": "ノートブックからメモを読み取り、構造化されたプレゼンテーションを自動的に生成します。"
},
"excalidrawGenerator": {
"name": "ダイアグラムジェネレーター",
"description": "Excalidraw Lab でメモを読み、視覚的な図を生成します。"
}
},
"runLog": {
"title": "履歴",
"noHistory": "実行履歴なし",
"toolTrace": "{count}件のツール呼び出し",
"step": "ステップ {num}"
"step": "ステップ {num}",
"clearConfirm": "このエージェントの履歴をすべて削除してもよろしいですか?",
"cleared": "履歴が削除されました",
"clearHistory": "履歴をクリアする"
},
"tools": {
"title": "エージェントツール",
@@ -1536,6 +1854,9 @@
"noteCreate": "ノート作成",
"urlFetch": "URL取得",
"memorySearch": "メモリ",
"generatePptx": "PPTX スライド",
"generateSlides": "HTML スライド",
"generateExcalidraw": "Excalidraw の図",
"configNeeded": "設定",
"selected": "{count}件選択済み",
"maxSteps": "最大反復回数"
@@ -1547,7 +1868,9 @@
"scraper": "あなたは監視アシスタントです。複数のウェブサイトの記事を明確で構造化された要約にまとめてください。",
"researcher": "あなたは厳密な研究者です。要求されたトピックについて、背景、要点、議論、参考文献を含む調査ノートを作成してください。",
"monitor": "あなたは分析アシスタントです。提供されたノートを分析し、方向性、参考文献、ノート間の関連性を提案してください。",
"custom": "あなたは役立つアシスタントです。"
"custom": "あなたは役立つアシスタントです。",
"slideGenerator": "あなたはプレゼンテーション作成者です。提供されたコンテンツを読み、タイトル、要点、要約を含む構造化されたスライドを作成します。",
"excalidrawGenerator": "あなたは図の作成者です。提供されたコンテンツを分析し、明確で整理された視覚的な図を作成します。"
},
"help": {
"title": "エージェントガイド",
@@ -1581,7 +1904,10 @@
"frequency": "エージェントが自動実行される頻度。テストするには手動から始めてください。",
"instructions": "デフォルトのAIプロンプトを置き換えるカスタム指示。自動プロンプトを使用する場合は空のままにしてください。",
"tools": "エージェントが使用できるツールを選択してください。各ツールはエージェントに特定の機能を与えます。",
"maxSteps": "推論サイクルの最大数。ステップが多いほど深い分析ですが、時間がかかります。"
"maxSteps": "推論サイクルの最大数。ステップが多いほど深い分析ですが、時間がかかります。",
"selectNotes": "分析する特定のメモを選択します。何も選択されていない場合、エージェントはノートブックのすべてのメモを使用します。",
"slideTheme": "プレゼンテーションのカラー パレットを選択します。自動ではAIに判断させます。",
"slideStyle": "視覚的なスタイルは、角の半径、間隔、情報密度に影響します。"
}
}
},
@@ -1631,5 +1957,147 @@
"lab": {
"initializing": "ワークスペースを初期化中",
"loadingIdeas": "アイデアを読み込み中..."
},
"richTextEditor": {
"slashHint": "↑↓ナビゲート・挿入・タブ切り替えセクション",
"slashLoading": "AIの思考…",
"slashTabAll": "全て",
"slashCatBasic": "基本ブロック",
"slashCatMedia": "メディア",
"slashCatFormatting": "書式設定",
"slashCatAi": "AIート",
"insertImage": "画像の挿入",
"imageUrlPlaceholder": "https://example.com/image.png",
"preview": "プレビュー",
"cancel": "キャンセル",
"insert": "入れる",
"slashText": "文章",
"slashTextDesc": "単純な段落",
"slashH1": "見出し1",
"slashH1Desc": "大きなセクション見出し",
"slashH2": "見出し2",
"slashH2Desc": "中セクションの見出し",
"slashH3": "見出し 3",
"slashH3Desc": "小さなセクションの見出し",
"slashBullet": "箇条書きリスト",
"slashBulletDesc": "順序なしリスト",
"slashNumbered": "番号付きリスト",
"slashNumberedDesc": "順序付けられた番号付きリスト",
"slashTodo": "タスクリスト",
"slashTodoDesc": "チェックボックスタスク",
"slashQuote": "引用",
"slashQuoteDesc": "見積もりをキャプチャする",
"slashCode": "コードブロック",
"slashCodeDesc": "コードスニペット",
"slashDivider": "ディバイダー",
"slashDividerDesc": "水平セパレータ",
"slashTable": "テーブル",
"slashTableDesc": "単純なグリッドを挿入する",
"slashDiagram": "ダイアグラム",
"slashDiagramDesc": "フローまたはマインドマップを生成する",
"slashSlides": "プレゼンテーション",
"slashSlidesDesc": "美しいスライドデッキを生成する",
"slashImage": "画像",
"slashImageDesc": "URLから画像を埋め込む",
"slashAlignLeft": "左揃え",
"slashAlignLeftDesc": "テキストを左揃えにする",
"slashAlignCenter": "中心",
"slashAlignCenterDesc": "テキストを中央揃えにする",
"slashAlignRight": "右揃え",
"slashAlignRightDesc": "テキストを右揃えにする",
"slashSuperscript": "上付き文字",
"slashSuperscriptDesc": "ベースラインの上のテキスト",
"slashSubscript": "添字",
"slashSubscriptDesc": "ベースラインの下のテキスト",
"slashClarify": "明らかにする",
"slashClarifyDesc": "テキストをより明確にする",
"slashShorten": "短くする",
"slashShortenDesc": "テキストを凝縮する",
"slashImprove": "改善する",
"slashImproveDesc": "スタイルを高める",
"slashExpand": "拡大する",
"slashExpandDesc": "テキストを推敲して充実させる",
"imageModalTitle": "画像の挿入",
"imageModalPreview": "プレビュー",
"imageModalCancel": "キャンセル",
"imageModalInsert": "入れる",
"imageModalInvalidUrl": "有効な URL を入力してください",
"imageModalLoadFailed": "画像のロードに失敗しました",
"linkPlaceholder": "リンクを貼り付けるか入力してください...",
"bold": "大胆な",
"italic": "イタリック",
"underline": "下線",
"strike": "取り消し線",
"code": "コード",
"highlight": "ハイライト",
"superscript": "上付き文字",
"subscript": "添字",
"addBlock": "ブロックの追加",
"placeholder": "コマンドには「/」を入力します..."
},
"brainstorm": {
"title": "Waves of Thought",
"subtitle": "Unfold dimensions of potentiality",
"placeholder": "Enter a concept to unfold...",
"generating": "AI is harvesting seeds of thought...",
"newBrainstorm": "New Brainstorm",
"noSessions": "No brainstorms yet",
"startOne": "Start one",
"sessions": "Brainstorms",
"seedLabel": "Seed Idea",
"ideaPromptDetailed": "アイデア、質問、トピックを入力してブレインストーミングを行ってください...",
"brainstormThisIdea": "Brainstorm this idea",
"startBrainstorm": "Start Brainstorm",
"spatialMode": "Spatial Exploration Mode",
"wave1": "Wave 1",
"wave2": "Wave 2",
"wave3": "Wave 3",
"export": "Export",
"exporting": "Exporting...",
"wave": "Wave",
"novelty": "Novelty",
"originConnection": "Origin connection",
"linkedNotes": "Linked notes",
"deepen": "Deepen",
"deepening": "Generating...",
"extract": "Create Note",
"converting": "Converting...",
"dismiss": "Not pertinent",
"noteCreated": "Note Created",
"ideas": "ideas",
"cancel": "Cancel",
"delete": "Delete",
"ideaOrigin": "Origin of the idea",
"noNoteLink": "Purely generative idea",
"derived_from": "Derived from",
"opposes": "In opposition with",
"extends": "Extends",
"synthesizes": "Synthesizes",
"transposes": "Transposes",
"none_found": "No note link",
"viewNote": "View note",
"addIdea": "Add idea",
"manualIdeaPrompt": "Title of your idea:",
"invite": "Invite",
"linkCopied": "Invite link copied!",
"activityTitle": "活動",
"noActivity": "まだ活動はありません",
"justNow": "ちょうど今",
"humanIdea": "人間",
"aiIdea": "AI",
"respondsTo": "に応答します",
"adding": "追加中...",
"manualIdeaDesc": "ブレインストーミング キャンバスでアイデアを共有する",
"manualIdeaTitle": "タイトル",
"manualIdeaTitlePlaceholder": "あなたのアイデアを一言で言うと...",
"manualIdeaDescLabel": "説明 (オプション)",
"manualIdeaDescPlaceholder": "あなたのアイデアを詳しく説明してください...",
"activity": {
"manual_idea": "アイデアを追加しました",
"wave_generated": "波を起こした",
"joined": "セッションに参加しました",
"idea_dismissed": "アイデアを却下した",
"invite_created": "招待状を作成しました"
}
}
}
}

View File

@@ -32,6 +32,8 @@
},
"sidebar": {
"notes": "노트",
"recent": "최근의",
"quickNav": "빠른 탐색",
"reminders": "알림",
"labels": "라벨",
"editLabels": "라벨 편집",
@@ -40,15 +42,35 @@
"noLabelsInNotebook": "이 노트북에는 아직 라벨이 없습니다",
"archive": "보관함",
"trash": "휴지통",
"clearFilter": "Remove filter"
"clearFilter": "Remove filter",
"inbox": "받은편지함",
"sharedWithMe": "나와 공유됨",
"sortNewest": "최신순",
"sortOldest": "오래된 것부터",
"sortAlpha": "A → Z",
"accountMenu": "계정 메뉴",
"profile": "윤곽",
"signOut": "로그아웃",
"sortOrder": "정렬 순서",
"freezePinnedNotebook": "노트북 사이드바 순서 고정",
"unfreezePinnedNotebook": "노트북 사이드바 순서 고정 해제",
"newSubNotebook": "새 하위 노트북",
"renameNotebook": "이름 바꾸기"
},
"notes": {
"title": "메모",
"newNote": "새 메모",
"reorganize": "메모 재구성",
"untitled": "제목 없음",
"placeholder": "메모 작성...",
"markdownPlaceholder": "메모 작성... (Markdown 지원)",
"titlePlaceholder": "제목",
"noteTypes": {
"richtext": "리치 텍스트",
"markdown": "가격 인하",
"text": "일반 텍스트",
"checklist": "체크리스트"
},
"listItem": "목록 항목",
"addListItem": "+ 목록 항목",
"newChecklist": "새 체크리스트",
@@ -58,6 +80,7 @@
"confirmDelete": "이 메모를 삭제하시겠습니까?",
"confirmLeaveShare": "이 공유 메모를 나가시겠습니까?",
"sharedBy": "공유자",
"sharedShort": "공유됨",
"leaveShare": "나가기",
"delete": "삭제",
"archive": "보관",
@@ -136,6 +159,8 @@
"dragToReorder": "드래그하여 재정렬",
"more": "더 보기",
"emptyState": "메모가 없습니다",
"metadataPanel": "세부",
"metadataNotebook": "공책",
"emptyStateTabs": "아직 노트가 없습니다. 사이드바의 \"새 노트\"를 사용하여 추가하세요 (AI 제목 제안이 작성기에 나타납니다).",
"inNotebook": "노트북에서",
"moveFailed": "이동 실패",
@@ -147,11 +172,6 @@
"unpinned": "고정 해제됨",
"redoShortcut": "다시 실행 (Ctrl+Y)",
"undoShortcut": "실행 취소 (Ctrl+Z)",
"viewCards": "카드 보기",
"viewCardsTooltip": "드래그 앤 드롭으로 재정렬 가능한 카드 그리드",
"viewTabs": "리스트 보기",
"viewTabsTooltip": "상단에 탭, 하단에 노트 — 탭을 드래그하여 재정렬",
"viewModeGroup": "노트 표시 모드",
"reorderTabs": "탭 재정렬",
"modified": "수정됨",
"created": "생성됨",
@@ -160,15 +180,18 @@
"savedStatus": "저장됨",
"dirtyStatus": "수정됨",
"completedLabel": "완료",
"notes.emptyNotebook": "빈 노트북",
"notes.emptyNotebookDesc": " 노트북에 노트가 없습니다. +를 클릭하여 만드세요.",
"notes.noNoteSelected": "선택된 노트 없음",
"notes.selectOrCreateNote": "목록에서 노트를 선택하거나 새로 만드세요.",
"notes": {
"emptyNotebook": " 노트북",
"emptyNotebookDesc": "이 노트북에 노트가 없습니다. +를 클릭하여 만드세요.",
"noNoteSelected": "선택된 노트 없음",
"selectOrCreateNote": "목록에서 노트를 선택하거나 새로 만드세요."
},
"commitVersion": "버전 저장",
"versionSaved": "버전이 저장되었습니다",
"deleteVersion": "이 버전 삭제",
"versionDeleted": "버전이 삭제되었습니다",
"deleteVersionConfirm": "이 버전을 영구적으로 삭제하시겠습니까?",
"deleteVersionDesc": "이 작업은 취소할 수 없습니다. 해당 버전은 기록에서 영구적으로 삭제됩니다.",
"historyMode": "기록 모드",
"historyModeManual": "수동 (커밋 버튼)",
"historyModeAuto": "자동 (스마트)",
@@ -184,6 +207,10 @@
"enableHistory": "기록 활성화",
"historyEmpty": "사용 가능한 버전이 없습니다",
"historySelectVersion": "미리볼 버전을 선택하세요",
"currentVersion": "현재의",
"compareVersions": "비교하다",
"diffTitle": "비교",
"diffSelectHint": "비교하려면 목록에서 2개 버전을 클릭하세요.",
"sortBy": "정렬",
"sortDateDesc": "날짜 (최신)",
"sortDateAsc": "날짜 (오래된)",
@@ -197,10 +224,14 @@
"createFailed": "Failed to create note",
"updateFailed": "Failed to update note",
"archived": "Note archived",
"unarchivedSuccess": "보관 파일에서 메모가 삭제되었습니다.",
"archiveFailed": "Failed to archive",
"sort": "Sort",
"confirmDeleteTitle": "Delete note",
"leftShare": "Share removed",
"ideaOrigin": "Origin of the idea",
"noNoteLink": "Purely generative idea",
"dismiss": "Not pertinent",
"dismissed": "Note dismissed from recent",
"generalNotes": "General Notes",
"noteType": "노트 유형",
@@ -214,7 +245,23 @@
"switchTypeTitle": "노트 유형을 변경하시겠습니까?",
"switchTypeWarning": "{type}(으)로 전환하면 일부 서식이 손실될 수 있습니다.",
"switchTypeContentPreserved": "콘텐츠는 일반 텍스트로 보존됩니다.",
"switchType": "{type}(으)로 전환"
"switchType": "{type}(으)로 전환",
"saveNow": "지금 저장",
"backToCollection": "컬렉션으로 돌아가기",
"markdownEditingTitle": "편집으로 돌아가기",
"markdownPreviewTitle": "시사",
"brainstormThisIdea": "이 아이디어를 브레인스토밍하세요",
"brainstormThisIdeaAria": "이 아이디어를 브레인스토밍하세요",
"shareNoteTitle": "메모 공유",
"shareNoteAria": "메모 공유",
"saveNoteAria": "메모 저장",
"noChangesToSaveAria": "저장할 변경사항이 없습니다.",
"optionsMenuAria": "옵션 메뉴",
"deleteNoteConfirmItem": "메모 삭제",
"noteDeletedToast": "메모가 삭제되었습니다.",
"deleteNoteFailedToast": "삭제할 수 없습니다.",
"documentInfoAria": "문서정보",
"noModification": "변경사항 없음"
},
"pagination": {
"previous": "←",
@@ -296,7 +343,24 @@
"accessRevoked": "접근 권한이 취소되었습니다",
"errorLoading": "공동 작업자 로드 중 오류",
"failedToAdd": "공동 작업자 추가 실패",
"failedToRemove": "공동 작업자 제거 실패"
"failedToRemove": "공동 작업자 제거 실패",
"shareCompactTitle": "공유하다",
"inviteByEmailLabel": "이메일로 초대",
"accessReadCompact": "보다",
"accessEditCompact": "편집하다",
"sendInvitation": "초대장 보내기",
"invitationSentBadge": "초대장이 전송되었습니다",
"sharedAccessLabel": "공유 액세스",
"noCollaboratorsEmpty": "아직 공동작업자가 없습니다.",
"removeAccessTitle": "액세스 권한 삭제",
"toastInviteSentTo": "{email}(으)로 초대장을 보냈습니다.",
"toastAccessRemoved": "{target}에 대한 액세스가 제거되었습니다.",
"toastUserFallback": "사용자",
"toastSharingError": "공유 오류",
"toastEmailNotFound": "이 이메일에는 계정이 없습니다.",
"toastAlreadySharedUser": "이 메모는 이미 이 사용자와 공유되었습니다.",
"toastRemoveAccessFailed": "액세스 권한을 삭제할 수 없습니다.",
"userFallback": "사용자"
},
"ai": {
"analyzing": "AI 분석 중...",
@@ -326,6 +390,8 @@
"transforming": "변환 중...",
"transformSuccess": "텍스트가 Markdown으로 성공적으로 변환되었습니다!",
"transformError": "변환 중 오류",
"convertToRichtext": "서식 있는 텍스트로 변환",
"convertingToRichtext": "변환 중...",
"assistant": "AI 도우미",
"generating": "생성 중...",
"generateTitles": "제목 생성",
@@ -389,6 +455,8 @@
"undoAI": "AI 변환 실행 취소",
"undoApplied": "원본 텍스트가 복원되었습니다",
"minWordsError": "AI 작업을 사용하려면 노트에 최소 5단어가 필요합니다.",
"wordCountMin": "재구성할 최소 {min} 단어를 선택하십시오(현재 {current} 단어).",
"wordCountMax": "재구성하려면 최대 {max} 단어를 선택하십시오(현재 {현재} 단어).",
"genericError": "AI 오류",
"actionError": "AI 작업 중 오류",
"appliedToNote": "노트에 적용됨",
@@ -404,6 +472,15 @@
"chatTab": "채팅",
"noteActions": "노트 작업",
"askToStart": "시작하려면 어시스턴트에게 질문하세요.",
"chatPanelContext": "문맥",
"chatPanelNotebookPlus": "+ 노트북",
"chatPanelWritingTone": "글쓰기 톤",
"scopeAutoBadge": "자동",
"chatNoteQuestionPlaceholder": "이 메모에 대해 질문하세요...",
"chatNotebookSelectPlaceholder": "노트북을 포함하세요...",
"assistantTabActions": "행위",
"resourcePreviewAiTitle": "AI 미리보기",
"resourcePreviewInjectFromChat": "채팅에서 주입",
"contextLabel": "컨텍스트",
"thisNote": "이 노트",
"allMyNotes": "모든 노트",
@@ -415,6 +492,7 @@
"newLineHint": "Shift+Enter = 새 줄",
"resultLabel": "결과",
"discardAction": "취소",
"organization": "조직",
"transformationsDesc": "변환 — 노트에 직접 적용",
"writeMinWordsAction": "AI 작업을 활성화하려면 최소 5단어를 작성하세요.",
"processingAction": "처리 중...",
@@ -425,7 +503,45 @@
"shorten": "요약",
"improve": "개선",
"toMarkdown": "Markdown으로",
"describeImages": "Describe images"
"describeImages": "Describe images",
"fixGrammar": "문법 수정",
"translate": "번역하다",
"explain": "설명하다",
"toRichText": "서식 있는 텍스트로 변환"
},
"generate": {
"slides": "슬라이드 생성",
"sectionLabel": "생성 도구",
"theme": "주제",
"themeArchitecturalMono": "건축 모노",
"themeVibrantTech": "활기찬 기술",
"themeMinimalSilk": "미니멀 실크",
"style": "스타일",
"styleProfessional": "전문적인",
"styleCreative": "창의적인",
"styleBrutalist": "잔혹주의자",
"diagram": "다이어그램 생성",
"diagramReadyHint": "메모를 시각적 흐름으로 변환",
"diagramType": "다이어그램 유형",
"typeAuto": "자동 감지",
"typeFlowchart": "흐름도",
"typeMindMap": "마인드맵",
"typeTimeline": "타임라인",
"typeOrgChart": "조직도",
"typeArchitecture": "건축학",
"typeProcessMap": "프로세스 맵",
"styleSketchy": "스케치",
"styleSoft": "부드러운",
"styleMinimal": "최소",
"styleDraft": "초안",
"stylePolished": "우아한",
"styleHandwritten": "손으로 쓴",
"diagramReady": "다이어그램이 준비되었습니다!",
"openInExcalidraw": "Excalidraw Lab에서 열기",
"insertDiagramInNote": "현재 노트에 PNG 포함",
"diagramImageAlt": "AI 생성 다이어그램",
"insertedInNote": "노트에 삽입된 다이어그램",
"insertExportError": "다이어그램 내보내기/업로드 중 오류 발생"
},
"openAssistant": "AI 어시스턴트 열기",
"poweredByMomento": "Momento AI 제공",
@@ -442,7 +558,64 @@
"aiCopilot": "AI 코파일럿",
"suggestTitle": "AI 제목 제안",
"generateTitleFromImage": "Generate title from image",
"titleGenerated": "Title generated from image"
"titleGenerated": "Title generated from image",
"resourceTab": "의지",
"aiNoteTitle": "AI 노트",
"injectReplace": "바꾸다",
"injectReplaceTitle": "메모 내용을 이 메시지로 대체",
"injectComplete": "완벽한",
"injectCompleteTitle": "이 메시지로 메모를 작성하세요(AI)",
"injectMerge": "병합",
"injectMergeTitle": "노트와 병합(AI)",
"imagesCount": "{count} 이미지",
"resource": {
"failedToLoadUrl": "이 URL을 로드하지 못했습니다.",
"pageLoaded": "로드된 페이지: {제목}",
"pageLoadError": "페이지를 로드하는 중에 오류가 발생했습니다.",
"pasteOrUrlFirst": "텍스트를 붙여넣거나 먼저 URL을 로드하세요.",
"enrichError": "농축 오류",
"enrichErrorShort": "농축 오류",
"contentApplied": "노트에 적용된 내용 ✓",
"fromChat": "💬 채팅에서",
"replacement": "↓ 교체",
"completedByAI": "✦ AI로 완성",
"mergedByAI": "⟳ AI로 병합",
"rendered": "렌더링됨",
"cancel": "취소",
"applyToNote": "메모에 적용",
"urlLabel": "URL(선택사항)",
"resourceText": "리소스 텍스트",
"resourcePlaceholder": "여기에 텍스트를 붙여넣으세요(markdown, HTML, 일반 텍스트…).",
"words": "단어",
"integrationMode": "통합 모드",
"modeReplace": "바꾸다",
"modeReplaceDesc": "직접, AI 없음",
"modeComplete": "완벽한",
"modeCompleteDesc": "다시 쓰지 않고 추가",
"modeMerge": "병합",
"modeMergeDesc": "재작성 및 통합",
"aiProcessing": "AI 처리…",
"preview": "시사",
"generatePreview": "미리보기 생성",
"emptyNoteHint": "💡 메모가 비어 있습니다. 리소스 콘텐츠가 직접 통합됩니다."
},
"cancel": "취소",
"copied": "복사됨",
"copy": "복사",
"transformations": "변환",
"otherLanguage": "다른 언어",
"translateNow": "지금 번역하기",
"generationTools": "생성 도구",
"generateSlidesLoading": "⏳ 프레젠테이션 생성 중...",
"generateDiagramLoading": "⏳ 다이어그램 생성 중...",
"errorShort": "오류",
"readyToast": "준비가 된!",
"downloadFailedToast": "다운로드 실패",
"pptxDownloadButton": ".pptx 다운로드",
"presentationReadyBadge": "프레젠테이션 준비 완료",
"openInLabTitle": "실험실에서 열기",
"inlineSummaryMarkdown": "**요약:**",
"networkErrorShort": "네트워크 오류입니다."
},
"titleSuggestions": {
"available": "제목 제안",
@@ -548,7 +721,19 @@
"untitled": "제목 없음",
"notifications": "알림",
"declined": "공유가 거절되었습니다",
"removed": "목록에서 노트가 제거되었습니다"
"removed": "목록에서 노트가 제거되었습니다",
"slidesReady": "프레젠테이션 준비 완료",
"openSlides": "프레젠테이션 열기",
"canvasReady": "다이어그램 준비",
"pptxReady": "슬라이드 준비됨",
"downloadPptx": ".pptx 다운로드",
"markAllRead": "모두 읽은 것으로 표시",
"agentSuccess": "에이전트 완료",
"agentFailed": "에이전트 실패",
"brainstormInvite": "영감",
"brainstormJoined": "영감",
"systemNotification": "체계",
"downloadFailed": "다운로드 실패"
},
"nav": {
"home": "홈",
@@ -597,6 +782,17 @@
"themeLight": "밝게",
"themeDark": "어둡게",
"themeSystem": "시스템",
"themeBaseGroup": "Base",
"themePalettesGroup": "Color palettes",
"themeSepia": "Sepia",
"themeMidnight": "Midnight",
"themeRose": "Rose",
"themeGreen": "Green",
"themeLavender": "Lavender",
"themeSand": "Sand",
"themeOcean": "Ocean",
"themeSunset": "Sunset",
"themeBlue": "Blue",
"notifications": "알림",
"language": "언어",
"selectLanguage": "언어 선택",
@@ -630,17 +826,8 @@
"desktopNotifications": "데스크톱 알림",
"desktopNotificationsDesc": "브라우저에서 알림을 받습니다",
"notificationsDesc": "알림 환경설정을 관리합니다",
"themeBaseGroup": "Base",
"themePalettesGroup": "Color palettes",
"themeSepia": "Sepia",
"themeMidnight": "Midnight",
"themeRose": "Rose",
"themeGreen": "Green",
"themeLavender": "Lavender",
"themeSand": "Sand",
"themeOcean": "Ocean",
"themeSunset": "Sunset",
"themeBlue": "Blue"
"autoSave": "자동 저장",
"autoSaveDesc": "입력하는 동안 변경 사항을 자동으로 저장"
},
"profile": {
"title": "프로필",
@@ -707,7 +894,15 @@
"providerDesc": "선호하는 AI 공급자 선택",
"providerAutoDesc": "Ollama 우선, OpenAI 대체",
"providerOllamaDesc": "100% 프라이빗, 로컬에서 실행",
"providerOpenAIDesc": "가장 정확, API 키 필요"
"providerOpenAIDesc": "가장 정확, API 키 필요",
"aiNote": "AI 노트",
"aiNoteDesc": "AI 채팅 버튼 및 텍스트 개선 도구 활성화",
"languageDetection": "언어 감지",
"languageDetectionDesc": "메모의 언어를 자동으로 감지합니다.",
"autoLabeling": "라벨 제안",
"autoLabelingDesc": "노트에 라벨을 자동으로 제안하고 적용합니다.",
"noteHistory": "메모 기록",
"noteHistoryDesc": "버전 스냅샷 및 기록 복원 활성화"
},
"general": {
"loading": "로딩 중...",
@@ -764,7 +959,9 @@
"markDone": "완료로 표시",
"markUndone": "미완료로 표시",
"todayAt": "오늘 {time}",
"tomorrowAt": "내일 {time}"
"tomorrowAt": "내일 {time}",
"clearCompleted": "클리어 완료",
"viewAll": "모든 알림 보기"
},
"notebook": {
"create": "노트북 만들기",
@@ -795,7 +992,11 @@
"confidence": "신뢰도",
"savingReminder": "알림 저장 실패",
"removingReminder": "알림 제거 실패",
"generatingDescription": "Please wait..."
"generatingDescription": "Please wait...",
"pinnedFrozenTooltip": "고정된 노트북 - 주문이 동결됨",
"organizeNotebookWithAITooltip": "AI로 이 노트북을 정리하세요",
"assistantRequiredForSummarize": "요약하려면 설정에서 AI 도우미를 켜세요.",
"createSubnotebook": "하위 노트북 추가"
},
"notebookSuggestion": {
"title": "{name}(으)로 이동하시겠습니까?",
@@ -808,6 +1009,9 @@
},
"admin": {
"title": "관리자 대시보드",
"adminConsole": "관리 콘솔",
"navSection": "항해",
"backToApp": "메멘토로 돌아가기",
"userManagement": "사용자 관리",
"chat": "AI 채팅",
"lab": "랩",
@@ -850,6 +1054,11 @@
"providerEmbeddingRequired": "AI_PROVIDER_EMBEDDING이 필요합니다",
"providerOllamaOption": "🦙 Ollama (로컬 및 무료)",
"providerOpenAIOption": "🤖 OpenAI (GPT-5, GPT-4)",
"providerAnthropicOption": "🧠 인류학(Claude API)",
"providerAnthropicCustomOption": "🧩 Anthropic 사용자 정의(Messages API — MiniMax 등)",
"anthropicModelHint": "제안에서 Claude 모델 ID를 선택하거나 수동으로 입력하세요(공식 API의 경우 원격 모델 목록 없음).",
"anthropicCustomModelHint": "Anthropic 호환 메시지 API(예: MiniMax): 기본 URL https://api.minimax.io/anthropic(중국: https://api.minimaxi.com/anthropic), 모델 MiniMax-M2.7. 임베딩: 공급자 \"Custom\" + OpenAI URL https://api.minimax.io/v1을 사용합니다.",
"anthropicCustomNoModelList": "이 게이트웨이는 OpenAI 스타일 /models 목록을 노출하지 않습니다. 제안에서 모델을 선택하거나 입력하세요(예: MiniMax-M2.7).",
"providerCustomOption": "🔧 사용자 정의 OpenAI 호환",
"providerDeepSeekOption": "🔍 DeepSeek",
"providerOpenRouterOption": "🌐 OpenRouter",
@@ -1003,7 +1212,14 @@
"error": "오류:",
"testError": "테스트 오류: {error}",
"tipTitle": "팁:",
"tipDescription": "테스트 전에 AI 테스트 패널을 사용하여 구성 문제를 진단하세요."
"tipDescription": "테스트 전에 AI 테스트 패널을 사용하여 구성 문제를 진단하세요.",
"chatTestTitle": "채팅 도우미 테스트",
"chatTestDescription": "채팅 도우미가 사용하는 AI 제공자 테스트",
"chatGenerationTest": "💬 채팅 도우미 테스트:",
"chatStep1": "어시스턴트에게 테스트 메시지를 보냅니다.",
"chatStep2": "어시스턴트가 하는 일에 대해 간결한 답변을 요청합니다.",
"chatStep3": "모델 응답을 표시합니다.",
"chatStep4": "응답성과 대기 시간을 확인합니다."
},
"sidebar": {
"dashboard": "대시보드",
@@ -1194,6 +1410,7 @@
"notesViewLabel": "메모 레이아웃",
"notesViewTabs": "탭 (OneNote 스타일)",
"notesViewMasonry": "카드 (그리드)",
"notesViewList": "목록(잡지)",
"selectTheme": "Select theme",
"fontFamilyLabel": "글꼴 패밀리",
"fontFamilyDescription": "앱 전체에서 사용할 글꼴을 선택하세요",
@@ -1277,6 +1494,69 @@
"organizeWithAI": "AI로 정리하기",
"organize": "정리"
},
"organizeNotebook": {
"title": "노트북 정리",
"unknownError": "알 수 없는 오류",
"toastSuccess": "노트북 정리 — 하위 노트북 {created}개 생성, 노트 {moved}개 이동",
"intro": "AI는 이 노트에 담긴 노트를 분석해 주제별 하위 노트로 재구성하는 방안을 제안합니다.",
"bulletThemes": "주제 또는 주제별로 노트를 그룹화하세요.",
"bulletSubfolders": "누락된 하위 노트북 만들기",
"bulletPreview": "변경 전 전체 미리보기",
"analyzingTitle": "분석 중…",
"analyzingSubtitle": "AI가 메모를 읽고 주제를 식별합니다.",
"previewSummary": "{groups} 그룹 · {notes} 노트 · {newSubs} 새 하위 노트북",
"badgeNew": "새로운",
"untitledNote": "제목 없는 메모",
"notesInGroup": "{count}개의 메모",
"executingTitle": "정리 중…",
"executingSubtitle": "서브노트 생성 및 노트 이동",
"doneTitle": "수첩 정리!",
"doneStats": "{created}개의 하위 노트북이 생성됨 · {moved}개의 노트가 이동됨",
"analyzeButton": "AI로 분석",
"restart": "다시 시작하세요",
"confirm": "적용하다",
"closeButton": "닫다"
},
"documentInfo": {
"tabInfo": "정보",
"tabVersions": "버전",
"wordsLabel": "단어",
"charactersLabel": "캐릭터",
"notebookLabel": "공책",
"typeLabel": "유형",
"createdLabel": "생성됨",
"modifiedLabel": "업데이트됨",
"labelsSection": "라벨",
"idLabel": "ID",
"historyDisabled": "이 메모에 대한 기록이 활성화되어 있지 않습니다.",
"enableHistory": "기록 활성화",
"savedVersions": "저장된 버전",
"savingEllipsis": "절약…",
"versionSaved": "버전이 저장되었습니다!",
"saveThisVersion": "이 버전을 저장하세요",
"loading": "로드 중…",
"noVersion": "아직 버전이 없습니다.",
"restoreTooltip": "복원하다",
"deleteTooltip": "삭제",
"comparisonMode": "비교 모드",
"comparisonSubtitle": "버전을 나란히 비교",
"deleteVersionConfirm": "이 버전을 삭제하시겠습니까?",
"latestBadge": "최신"
},
"languages": {
"targets": {
"french": "프랑스 국민",
"english": "영어",
"spanish": "스페인 사람",
"german": "독일 사람",
"persian": "페르시아 인",
"portuguese": "포르투갈 인",
"italian": "이탈리아 사람",
"chinese": "중국인",
"japanese": "일본어"
},
"customPlaceholder": "예를 들어 아랍어, 러시아어…"
},
"common": {
"unknown": "알 수 없음",
"notAvailable": "사용 불가",
@@ -1398,12 +1678,16 @@
"scraper": "모니터",
"researcher": "리서처",
"monitor": "관찰자",
"slideGenerator": "슬라이드",
"excalidrawGenerator": "도표",
"custom": "사용자 정의"
},
"typeDescriptions": {
"scraper": "여러 사이트를 스크랩하고 요약을 생성합니다",
"researcher": "주제에 대한 정보를 검색합니다",
"monitor": "노트북을 감시하고 노트를 분석합니다",
"slideGenerator": "노트에서 PowerPoint 프레젠테이션을 만듭니다.",
"excalidrawGenerator": "노트에서 Excalidraw 다이어그램을 만듭니다.",
"custom": "직접 프롬프트를 작성하는 자유 에이전트"
},
"form": {
@@ -1416,6 +1700,27 @@
"urlsOptional": "(선택 사항)",
"sourceNotebook": "감시할 노트북",
"selectNotebook": "노트북을 선택하세요...",
"selectNotes": "분석할 참고사항",
"notesSelected": "{{count}}개의 메모가 선택되었습니다.",
"slideTheme": "발표 주제",
"slideThemeDefault": "오토매틱",
"slideStyle": "시각적 스타일",
"slideStyleSoft": "소프트(권장)",
"slideStyleSharp": "샤프하고 밀도가 높은",
"slideStyleRounded": "둥글고 넓음",
"slideStylePill": "프리미엄 / 알약",
"excalidrawDiagramType": "다이어그램 유형",
"excalidrawDiagramTypeAuto": "자동(도메인 감지)",
"excalidrawDiagramTypeFlowchart": "흐름도(프로세스)",
"excalidrawDiagramTypeMindmap": "마인드맵(아이디어)",
"excalidrawDiagramTypeOrgChart": "조직도(팀)",
"excalidrawDiagramTypeTimeline": "타임라인/로드맵",
"excalidrawDiagramTypeProcessMap": "프로세스 맵(운영)",
"excalidrawDiagramTypeArchitectureCloud": "클라우드 아키텍처(영역/RG)",
"excalidrawDiagramStyle": "Excalidraw 다이어그램 스타일",
"excalidrawDiagramStyleDefault": "컬러드(엑스칼리드로우)",
"excalidrawDiagramStyleSketchPlus": "Sketch+(향상된 Excalidraw)",
"excalidrawDiagramStyleAustere": "엄격함(최소)",
"targetNotebook": "대상 노트북",
"inbox": "받은편지함",
"instructions": "AI 지침",
@@ -1485,6 +1790,8 @@
"updated": "에이전트가 업데이트되었습니다",
"deleted": "\"{name}\"이(가) 삭제되었습니다",
"deleteError": "삭제 중 오류 발생",
"running": "세대 진행 중…",
"runningDesc": "생성하는 데 몇 분 정도 걸릴 수 있습니다. 자유롭게 탐색할 수 있습니다.",
"runSuccess": "\"{name}\"이(가) 성공적으로 실행되었습니다",
"runError": "오류: {error}",
"runFailed": "실행 실패",
@@ -1519,13 +1826,24 @@
"chercheur": {
"name": "주제 리서처",
"description": "주제에 대한 심층 정보를 검색하고 참조가 포함된 구조화된 노트를 만듭니다."
},
"slideGenerator": {
"name": "슬라이드 생성기",
"description": "노트북에서 메모를 읽고 구조화된 프레젠테이션을 자동으로 생성합니다."
},
"excalidrawGenerator": {
"name": "다이어그램 생성기",
"description": "Excalidraw Lab에서 메모를 읽고 시각적 다이어그램을 생성합니다."
}
},
"runLog": {
"title": "기록",
"noHistory": "아직 실행 기록이 없습니다",
"toolTrace": "{count}개 도구 호출",
"step": "{num}단계"
"step": "{num}단계",
"clearConfirm": "이 에이전트의 모든 기록을 삭제하시겠습니까?",
"cleared": "기록이 삭제되었습니다.",
"clearHistory": "기록 지우기"
},
"tools": {
"title": "에이전트 도구",
@@ -1536,6 +1854,9 @@
"noteCreate": "노트 만들기",
"urlFetch": "URL 가져오기",
"memorySearch": "메모리",
"generatePptx": "PPTX 슬라이드",
"generateSlides": "HTML 슬라이드",
"generateExcalidraw": "엑스칼리드로 다이어그램",
"configNeeded": "구성",
"selected": "{count}개 선택됨",
"maxSteps": "최대 반복 횟수"
@@ -1547,7 +1868,9 @@
"scraper": "당신은 모니터링 도우미입니다. 여러 웹사이트의 기사를 명확하고 구조화된 요약으로 종합하세요.",
"researcher": "당신은 철저한 연구원입니다. 요청된 주제에 대해 맥락, 핵심 포인트, 논쟁, 참조가 포함된 연구 노트를 작성하세요.",
"monitor": "당신은 분석 도우미입니다. 제공된 노트를 분석하고 단서, 참조 및 노트 간의 연결을 제안하세요.",
"custom": "당신은 도움이 되는 도우미입니다."
"custom": "당신은 도움이 되는 도우미입니다.",
"slideGenerator": "당신은 프레젠테이션 작성자입니다. 제공된 콘텐츠를 읽고 제목, 핵심 사항, 요약이 포함된 구조화된 슬라이드를 만드세요.",
"excalidrawGenerator": "당신은 다이어그램 작성자입니다. 제공된 콘텐츠를 분석하고 명확하고 체계적인 시각적 다이어그램을 만듭니다."
},
"help": {
"title": "에이전트 가이드",
@@ -1581,7 +1904,10 @@
"frequency": "에이전트가 자동으로 실행되는 빈도입니다. 테스트하려면 수동으로 시작하세요.",
"instructions": "기본 AI 프롬프트를 대체하는 사용자 지정 지침입니다. 자동 프롬프트를 사용하려면 비워두세요.",
"tools": "에이전트가 사용할 수 있는 도구를 선택하세요. 각 도구는 에이전트에게 특정 기능을 제공합니다.",
"maxSteps": "최대 추론 사이클 수입니다. 단계가 많을수록 분석이 깊어지지만 시간이 더 걸립니다."
"maxSteps": "최대 추론 사이클 수입니다. 단계가 많을수록 분석이 깊어지지만 시간이 더 걸립니다.",
"selectNotes": "분석할 특정 메모를 선택하세요. 아무것도 선택하지 않으면 상담원은 노트북의 모든 메모를 사용합니다.",
"slideTheme": "프레젠테이션의 색상 팔레트를 선택합니다. 자동으로 AI가 결정을 내릴 수 있습니다.",
"slideStyle": "시각적 스타일은 모서리 반경, 간격 및 정보 밀도에 영향을 미칩니다."
}
}
},
@@ -1631,5 +1957,147 @@
"lab": {
"initializing": "작업 공간 초기화 중",
"loadingIdeas": "아이디어 로딩 중..."
},
"richTextEditor": {
"slashHint": "↑↓ 탐색 · 삽입 입력 · 탭 스위치 섹션",
"slashLoading": "AI 생각...",
"slashTabAll": "모두",
"slashCatBasic": "기본 블록",
"slashCatMedia": "메디아",
"slashCatFormatting": "서식 지정",
"slashCatAi": "AI 노트",
"insertImage": "이미지 삽입",
"imageUrlPlaceholder": "https://example.com/image.png",
"preview": "시사",
"cancel": "취소",
"insert": "끼워 넣다",
"slashText": "텍스트",
"slashTextDesc": "간단한 단락",
"slashH1": "제목 1",
"slashH1Desc": "큰 섹션 제목",
"slashH2": "제목 2",
"slashH2Desc": "중간 섹션 제목",
"slashH3": "제목 3",
"slashH3Desc": "작은 섹션 제목",
"slashBullet": "글머리 기호 목록",
"slashBulletDesc": "순서가 없는 목록",
"slashNumbered": "번호 매기기 목록",
"slashNumberedDesc": "번호가 매겨진 목록",
"slashTodo": "작업 목록",
"slashTodoDesc": "체크박스 작업",
"slashQuote": "인용하다",
"slashQuoteDesc": "견적 캡처",
"slashCode": "코드 블록",
"slashCodeDesc": "코드 조각",
"slashDivider": "분할기",
"slashDividerDesc": "수평 분리기",
"slashTable": "테이블",
"slashTableDesc": "간단한 그리드 삽입",
"slashDiagram": "도표",
"slashDiagramDesc": "흐름 또는 마인드맵 생성",
"slashSlides": "프레젠테이션",
"slashSlidesDesc": "아름다운 슬라이드 데크 생성",
"slashImage": "영상",
"slashImageDesc": "URL에서 이미지 삽입",
"slashAlignLeft": "왼쪽 정렬",
"slashAlignLeftDesc": "텍스트를 왼쪽으로 정렬",
"slashAlignCenter": "센터",
"slashAlignCenterDesc": "텍스트를 중앙에 배치",
"slashAlignRight": "오른쪽 정렬",
"slashAlignRightDesc": "텍스트를 오른쪽으로 정렬",
"slashSuperscript": "어깨 기호",
"slashSuperscriptDesc": "기준선 위의 텍스트",
"slashSubscript": "아래첨자",
"slashSubscriptDesc": "기준선 아래의 텍스트",
"slashClarify": "밝히다",
"slashClarifyDesc": "텍스트를 더 명확하게 만들기",
"slashShorten": "줄이다",
"slashShortenDesc": "텍스트를 압축하세요",
"slashImprove": "개선하다",
"slashImproveDesc": "스타일을 향상시키세요",
"slashExpand": "확장하다",
"slashExpandDesc": "텍스트를 정교하고 풍부하게 만드세요.",
"imageModalTitle": "이미지 삽입",
"imageModalPreview": "시사",
"imageModalCancel": "취소",
"imageModalInsert": "끼워 넣다",
"imageModalInvalidUrl": "유효한 URL을 입력하세요.",
"imageModalLoadFailed": "이미지를 로드하지 못했습니다.",
"linkPlaceholder": "링크를 붙여넣거나 입력하세요...",
"bold": "용감한",
"italic": "이탤릭체",
"underline": "밑줄",
"strike": "취소선",
"code": "암호",
"highlight": "가장 밝은 부분",
"superscript": "어깨 기호",
"subscript": "아래첨자",
"addBlock": "블록 추가",
"placeholder": "명령에 '/'를 입력합니다..."
},
"brainstorm": {
"title": "Waves of Thought",
"subtitle": "Unfold dimensions of potentiality",
"placeholder": "Enter a concept to unfold...",
"generating": "AI is harvesting seeds of thought...",
"newBrainstorm": "New Brainstorm",
"noSessions": "No brainstorms yet",
"startOne": "Start one",
"sessions": "Brainstorms",
"seedLabel": "Seed Idea",
"ideaPromptDetailed": "브레인스토밍을 위한 아이디어, 질문 또는 주제를 입력하세요...",
"brainstormThisIdea": "Brainstorm this idea",
"startBrainstorm": "Start Brainstorm",
"spatialMode": "Spatial Exploration Mode",
"wave1": "Wave 1",
"wave2": "Wave 2",
"wave3": "Wave 3",
"export": "Export",
"exporting": "Exporting...",
"wave": "Wave",
"novelty": "Novelty",
"originConnection": "Origin connection",
"linkedNotes": "Linked notes",
"deepen": "Deepen",
"deepening": "Generating...",
"extract": "Create Note",
"converting": "Converting...",
"dismiss": "Not pertinent",
"noteCreated": "Note Created",
"ideas": "ideas",
"cancel": "Cancel",
"delete": "Delete",
"ideaOrigin": "Origin of the idea",
"noNoteLink": "Purely generative idea",
"derived_from": "Derived from",
"opposes": "In opposition with",
"extends": "Extends",
"synthesizes": "Synthesizes",
"transposes": "Transposes",
"none_found": "No note link",
"viewNote": "View note",
"addIdea": "Add idea",
"manualIdeaPrompt": "Title of your idea:",
"invite": "Invite",
"linkCopied": "Invite link copied!",
"activityTitle": "활동",
"noActivity": "아직 활동이 없습니다",
"justNow": "방금",
"humanIdea": "인간",
"aiIdea": "일체 포함",
"respondsTo": "응답하다",
"adding": "첨가...",
"manualIdeaDesc": "브레인스토밍 캔버스로 아이디어를 공유하세요",
"manualIdeaTitle": "제목",
"manualIdeaTitlePlaceholder": "당신의 생각을 한마디로...",
"manualIdeaDescLabel": "설명(선택사항)",
"manualIdeaDescPlaceholder": "당신의 아이디어를 자세히 설명하세요...",
"activity": {
"manual_idea": "아이디어를 추가했습니다",
"wave_generated": "파동을 일으켰다",
"joined": "세션에 참여했습니다",
"idea_dismissed": "아이디어를 기각했습니다",
"invite_created": "초대장을 만들었습니다"
}
}
}
}

View File

@@ -32,6 +32,8 @@
},
"sidebar": {
"notes": "Notities",
"recent": "Recent",
"quickNav": "Snelle navigatie",
"reminders": "Herinneringen",
"labels": "Labels",
"editLabels": "Labels bewerken",
@@ -40,15 +42,35 @@
"noLabelsInNotebook": "Nog geen labels in dit notitieboek",
"archive": "Archief",
"trash": "Prullenbak",
"clearFilter": "Remove filter"
"clearFilter": "Remove filter",
"inbox": "Postvak IN",
"sharedWithMe": "Gedeeld met mij",
"sortNewest": "Nieuwste eerst",
"sortOldest": "Oudste eerst",
"sortAlpha": "A → Z",
"accountMenu": "Accountmenu",
"profile": "Profiel",
"signOut": "Meld u af",
"sortOrder": "Sorteervolgorde",
"freezePinnedNotebook": "Zet de volgorde van de notitieboekjezijbalk vast",
"unfreezePinnedNotebook": "Maak de volgorde van de notitieblokzijbalk los",
"newSubNotebook": "Nieuw sub-notebook",
"renameNotebook": "Hernoemen"
},
"notes": {
"title": "Notities",
"newNote": "Nieuwe notitie",
"reorganize": "Notities reorganiseren",
"untitled": "Naamloos",
"placeholder": "Maak een notitie...",
"markdownPlaceholder": "Maak een notitie... (Markdown ondersteund)",
"titlePlaceholder": "Titel",
"noteTypes": {
"richtext": "Rijke tekst",
"markdown": "Afwaardering",
"text": "Platte tekst",
"checklist": "Controlelijst"
},
"listItem": "Lijstitem",
"addListItem": "+ Lijstitem",
"newChecklist": "Nieuwe checklist",
@@ -58,6 +80,7 @@
"confirmDelete": "Weet u zeker dat u deze notitie wilt verwijderen?",
"confirmLeaveShare": "Weet u zeker dat u deze gedeelde notitie wilt verlaten?",
"sharedBy": "Gedeeld door",
"sharedShort": "Gedeeld",
"leaveShare": "Verlaten",
"delete": "Verwijderen",
"archive": "Archiveren",
@@ -136,6 +159,8 @@
"dragToReorder": "Sleep om te herschikken",
"more": "Meer",
"emptyState": "Geen notities hier",
"metadataPanel": "Details",
"metadataNotebook": "Notitieboekje",
"emptyStateTabs": "Nog geen notities hier. Gebruik \"Nieuwe notitie\" in de zijbalk om er een toe te voegen (AI-titelsuggesties verschijnen in de composer).",
"inNotebook": "In notitieboek",
"moveFailed": "Verplaatsen mislukt",
@@ -147,11 +172,6 @@
"unpinned": "Losgemaakt",
"redoShortcut": "Opnieuw (Ctrl+Y)",
"undoShortcut": "Ongedaan maken (Ctrl+Z)",
"viewCards": "Kaartenweergave",
"viewCardsTooltip": "Kaartenraster met slepen-en-neerzetten herschikken",
"viewTabs": "Lijstweergave",
"viewTabsTooltip": "Tabbladen bovenaan, notitie eronder — sleep tabbladen om te herschikken",
"viewModeGroup": "Weergavemodus notities",
"reorderTabs": "Tabblad herschikken",
"modified": "Gewijzigd",
"created": "Aangemaakt",
@@ -160,15 +180,18 @@
"savedStatus": "Opgeslagen",
"dirtyStatus": "Gewijzigd",
"completedLabel": "Voltooid",
"notes.emptyNotebook": "Leeg notitieboek",
"notes.emptyNotebookDesc": "Dit notitieboek heeft geen notities. Klik op + om er een te maken.",
"notes.noNoteSelected": "Geen notitie geselecteerd",
"notes.selectOrCreateNote": "Selecteer een notitie uit de lijst of maak een nieuwe.",
"notes": {
"emptyNotebook": "Leeg notitieboek",
"emptyNotebookDesc": "Dit notitieboek heeft geen notities. Klik op + om er een te maken.",
"noNoteSelected": "Geen notitie geselecteerd",
"selectOrCreateNote": "Selecteer een notitie uit de lijst of maak een nieuwe."
},
"commitVersion": "Versie opslaan",
"versionSaved": "Versie opgeslagen",
"deleteVersion": "Deze versie verwijderen",
"versionDeleted": "Versie verwijderd",
"deleteVersionConfirm": "Deze versie definitief verwijderen?",
"deleteVersionDesc": "Deze actie kan niet ongedaan worden gemaakt. De versie wordt definitief uit de geschiedenis verwijderd.",
"historyMode": "Geschiedenismodus",
"historyModeManual": "Handmatig (commit-knop)",
"historyModeAuto": "Automatisch (slim)",
@@ -184,6 +207,10 @@
"enableHistory": "Geschiedenis inschakelen",
"historyEmpty": "Geen versies beschikbaar",
"historySelectVersion": "Selecteer een versie om de inhoud te bekijken",
"currentVersion": "huidig",
"compareVersions": "Vergelijken",
"diffTitle": "Vergelijking",
"diffSelectHint": "Klik op 2 versies in de lijst om ze te vergelijken",
"sortBy": "Sorteren op",
"sortDateDesc": "Datum (nieuwste)",
"sortDateAsc": "Datum (oudste)",
@@ -197,10 +224,14 @@
"createFailed": "Failed to create note",
"updateFailed": "Failed to update note",
"archived": "Note archived",
"unarchivedSuccess": "Opmerking verwijderd uit archief",
"archiveFailed": "Failed to archive",
"sort": "Sort",
"confirmDeleteTitle": "Delete note",
"leftShare": "Share removed",
"ideaOrigin": "Origin of the idea",
"noNoteLink": "Purely generative idea",
"dismiss": "Not pertinent",
"dismissed": "Note dismissed from recent",
"generalNotes": "General Notes",
"noteType": "Notitietype",
@@ -214,7 +245,23 @@
"switchTypeTitle": "Notitietype wijzigen?",
"switchTypeWarning": "Opmaak kan verloren gaan bij wijziging naar {type}.",
"switchTypeContentPreserved": "Je inhoud wordt bewaard als platte tekst.",
"switchType": "Wijzigen naar {type}"
"switchType": "Wijzigen naar {type}",
"saveNow": "Bespaar nu",
"backToCollection": "Terug naar collectie",
"markdownEditingTitle": "Terug naar bewerken",
"markdownPreviewTitle": "Voorbeeld",
"brainstormThisIdea": "Brainstorm over dit idee",
"brainstormThisIdeaAria": "Brainstorm over dit idee",
"shareNoteTitle": "Deel notitie",
"shareNoteAria": "Deel notitie",
"saveNoteAria": "Bewaar notitie",
"noChangesToSaveAria": "Geen wijzigingen om op te slaan",
"optionsMenuAria": "Optiemenu",
"deleteNoteConfirmItem": "Notitie verwijderen",
"noteDeletedToast": "Opmerking verwijderd.",
"deleteNoteFailedToast": "Kan niet verwijderen.",
"documentInfoAria": "Documentinformatie",
"noModification": "Geen wijzigingen"
},
"pagination": {
"previous": "←",
@@ -296,7 +343,24 @@
"accessRevoked": "Toegang ingetrokken",
"errorLoading": "Fout bij laden van medewerkers",
"failedToAdd": "Medewerker toevoegen mislukt",
"failedToRemove": "Medewerker verwijderen mislukt"
"failedToRemove": "Medewerker verwijderen mislukt",
"shareCompactTitle": "Deel",
"inviteByEmailLabel": "Uitnodigen per e-mail",
"accessReadCompact": "Weergave",
"accessEditCompact": "Bewerking",
"sendInvitation": "Uitnodiging versturen",
"invitationSentBadge": "Uitnodiging verzonden",
"sharedAccessLabel": "Gedeelde toegang",
"noCollaboratorsEmpty": "Nog geen medewerkers.",
"removeAccessTitle": "Toegang verwijderen",
"toastInviteSentTo": "Uitnodiging verzonden naar {email}",
"toastAccessRemoved": "Toegang verwijderd voor {target}",
"toastUserFallback": "de gebruiker",
"toastSharingError": "Fout bij delen",
"toastEmailNotFound": "Er is geen account gevonden met dit e-mailadres.",
"toastAlreadySharedUser": "Deze notitie is al gedeeld met deze gebruiker.",
"toastRemoveAccessFailed": "Kan de toegang niet verwijderen.",
"userFallback": "Gebruiker"
},
"ai": {
"analyzing": "AI analyseert...",
@@ -326,6 +390,8 @@
"transforming": "Transformeren...",
"transformSuccess": "Tekst succesvol naar Markdown getransformeerd!",
"transformError": "Fout bij transformeren",
"convertToRichtext": "Converteren naar Rich Text",
"convertingToRichtext": "Converteren...",
"assistant": "AI-assistent",
"generating": "Genereren...",
"generateTitles": "Titels genereren",
@@ -389,6 +455,8 @@
"undoAI": "AI-transformatie ongedaan maken",
"undoApplied": "Originele tekst hersteld",
"minWordsError": "De notitie moet minimaal 5 woorden bevatten om AI-acties te gebruiken.",
"wordCountMin": "Selecteer ten minste {min} woorden om te herformuleren (momenteel {huidige} woorden)",
"wordCountMax": "Selecteer maximaal {max} woorden om te herformuleren (momenteel {huidige} woorden)",
"genericError": "AI-fout",
"actionError": "Fout bij AI-actie",
"appliedToNote": "Toegepast op notitie",
@@ -404,6 +472,15 @@
"chatTab": "Chat",
"noteActions": "Notitie-acties",
"askToStart": "Stel de assistent een vraag om te beginnen.",
"chatPanelContext": "Context",
"chatPanelNotebookPlus": "+ Notitieboekje",
"chatPanelWritingTone": "Schrijftoon",
"scopeAutoBadge": "Auto",
"chatNoteQuestionPlaceholder": "Stel een vraag over deze notitie...",
"chatNotebookSelectPlaceholder": "Voeg een notitieboekje toe...",
"assistantTabActions": "Acties",
"resourcePreviewAiTitle": "AI-voorbeeld",
"resourcePreviewInjectFromChat": "Injecteren vanuit chat",
"contextLabel": "Context",
"thisNote": "Deze notitie",
"allMyNotes": "Al mijn notities",
@@ -415,6 +492,7 @@
"newLineHint": "Shift+Enter = nieuwe regel",
"resultLabel": "Resultaat",
"discardAction": "Negeren",
"organization": "Organisatie",
"transformationsDesc": "Transformaties — direct toegepast op de notitie",
"writeMinWordsAction": "Schrijf minimaal 5 woorden om AI-acties te activeren.",
"processingAction": "Verwerken...",
@@ -425,7 +503,45 @@
"shorten": "Inkorten",
"improve": "Verbeteren",
"toMarkdown": "Naar Markdown",
"describeImages": "Describe images"
"describeImages": "Describe images",
"fixGrammar": "Grammatica repareren",
"translate": "Vertalen",
"explain": "Uitleggen",
"toRichText": "Converteren naar rijke tekst"
},
"generate": {
"slides": "Genereer dia's",
"sectionLabel": "Generatiehulpmiddelen",
"theme": "Thema",
"themeArchitecturalMono": "Architectonisch Mono",
"themeVibrantTech": "Levendige technologie",
"themeMinimalSilk": "Minimale zijde",
"style": "Stijl",
"styleProfessional": "Professioneel",
"styleCreative": "Creatief",
"styleBrutalist": "Brutalistisch",
"diagram": "Diagram genereren",
"diagramReadyHint": "Zet notitie om in visuele stroom",
"diagramType": "Diagramtype",
"typeAuto": "Automatische detectie",
"typeFlowchart": "Stroomdiagram",
"typeMindMap": "Mindmap",
"typeTimeline": "Tijdlijn",
"typeOrgChart": "Organigram",
"typeArchitecture": "Architectuur",
"typeProcessMap": "Proceskaart",
"styleSketchy": "Schetsmatig",
"styleSoft": "Zacht",
"styleMinimal": "Minimaal",
"styleDraft": "Voorlopige versie",
"stylePolished": "Gepolijst",
"styleHandwritten": "Handgeschreven",
"diagramReady": "Diagram is klaar!",
"openInExcalidraw": "Openen in Excalidraw Lab",
"insertDiagramInNote": "Sluit PNG in de huidige notitie in",
"diagramImageAlt": "AI-gegenereerd diagram",
"insertedInNote": "Diagram ingevoegd in notitie",
"insertExportError": "Fout bij exporteren/uploaden van diagram"
},
"openAssistant": "AI-assistent openen",
"poweredByMomento": "Aangedreven door Momento AI",
@@ -442,7 +558,64 @@
"aiCopilot": "AI-copiloot",
"suggestTitle": "AI-titelsuggestie",
"generateTitleFromImage": "Generate title from image",
"titleGenerated": "Title generated from image"
"titleGenerated": "Title generated from image",
"resourceTab": "Bron",
"aiNoteTitle": "AI-opmerking",
"injectReplace": "Vervangen",
"injectReplaceTitle": "Vervang de inhoud van de notitie door dit bericht",
"injectComplete": "Compleet",
"injectCompleteTitle": "Vul de notitie in met dit bericht (AI)",
"injectMerge": "Samenvoegen",
"injectMergeTitle": "Samenvoegen met notitie (AI)",
"imagesCount": "{count} afbeeldingen",
"resource": {
"failedToLoadUrl": "Kan deze URL niet laden",
"pageLoaded": "Pagina geladen: {title}",
"pageLoadError": "Fout bij het laden van de pagina",
"pasteOrUrlFirst": "Plak tekst of laad eerst een URL",
"enrichError": "Verrijkingsfout",
"enrichErrorShort": "Verrijkingsfout",
"contentApplied": "Inhoud toegepast op noot ✓",
"fromChat": "💬 Vanuit chat",
"replacement": "↓ Vervanging",
"completedByAI": "✦ Voltooid door AI",
"mergedByAI": "⟳ Samengevoegd door AI",
"rendered": "Teruggegeven",
"cancel": "Annuleren",
"applyToNote": "Toepassen op notitie",
"urlLabel": "URL (optioneel)",
"resourceText": "Brontekst",
"resourcePlaceholder": "Plak hier uw tekst (markdown, HTML, platte tekst...)",
"words": "woorden",
"integrationMode": "Integratiemodus",
"modeReplace": "Vervangen",
"modeReplaceDesc": "Direct, geen AI",
"modeComplete": "Compleet",
"modeCompleteDesc": "Voegt toe zonder te herschrijven",
"modeMerge": "Samenvoegen",
"modeMergeDesc": "Herschrijft en integreert",
"aiProcessing": "AI-verwerking…",
"preview": "Voorbeeld",
"generatePreview": "Voorbeeld genereren",
"emptyNoteHint": "💡 De notitie is leeg: de broninhoud wordt direct geïntegreerd."
},
"cancel": "Annuleren",
"copied": "Gekopieerd",
"copy": "Kopiëren",
"transformations": "Transformaties",
"otherLanguage": "Een andere taal",
"translateNow": "Vertaal nu",
"generationTools": "Generatie-instrumenten",
"generateSlidesLoading": "⏳ Presentatie genereren...",
"generateDiagramLoading": "⏳ Diagram genereren...",
"errorShort": "Fout",
"readyToast": "Klaar!",
"downloadFailedToast": "Downloaden mislukt",
"pptxDownloadButton": "Download .pptx",
"presentationReadyBadge": "Presentatie klaar",
"openInLabTitle": "Openen in laboratorium",
"inlineSummaryMarkdown": "**Samenvatting:**",
"networkErrorShort": "Netwerkfout."
},
"titleSuggestions": {
"available": "Titelsuggesties",
@@ -548,7 +721,19 @@
"untitled": "Naamloos",
"notifications": "Meldingen",
"declined": "Delen geweigerd",
"removed": "Notitie verwijderd uit lijst"
"removed": "Notitie verwijderd uit lijst",
"slidesReady": "Presentatie klaar",
"openSlides": "Presentatie openen",
"canvasReady": "Diagram klaar",
"pptxReady": "Glijbanen klaar",
"downloadPptx": "Download .pptx",
"markAllRead": "Markeer alles als gelezen",
"agentSuccess": "Agent klaar",
"agentFailed": "Agent is mislukt",
"brainstormInvite": "Brainstormen",
"brainstormJoined": "Brainstormen",
"systemNotification": "Systeem",
"downloadFailed": "Downloaden mislukt"
},
"nav": {
"home": "Home",
@@ -597,6 +782,17 @@
"themeLight": "Licht",
"themeDark": "Donker",
"themeSystem": "Systeem",
"themeBaseGroup": "Base",
"themePalettesGroup": "Color palettes",
"themeSepia": "Sepia",
"themeMidnight": "Midnight",
"themeRose": "Rose",
"themeGreen": "Green",
"themeLavender": "Lavender",
"themeSand": "Sand",
"themeOcean": "Ocean",
"themeSunset": "Sunset",
"themeBlue": "Blue",
"notifications": "Meldingen",
"language": "Taal",
"selectLanguage": "Taal selecteren",
@@ -630,17 +826,8 @@
"desktopNotifications": "Bureaubladmeldingen",
"desktopNotificationsDesc": "Ontvang meldingen in uw browser",
"notificationsDesc": "Beheer uw meldingsvoorkeuren",
"themeBaseGroup": "Base",
"themePalettesGroup": "Color palettes",
"themeSepia": "Sepia",
"themeMidnight": "Midnight",
"themeRose": "Rose",
"themeGreen": "Green",
"themeLavender": "Lavender",
"themeSand": "Sand",
"themeOcean": "Ocean",
"themeSunset": "Sunset",
"themeBlue": "Blue"
"autoSave": "Automatisch opslaan",
"autoSaveDesc": "Sla wijzigingen automatisch op tijdens het typen"
},
"profile": {
"title": "Profiel",
@@ -707,7 +894,15 @@
"providerDesc": "Kies uw voorkeurs AI-provider",
"providerAutoDesc": "Ollama indien beschikbaar, OpenAI als terugval",
"providerOllamaDesc": "100% privé, draait lokaal op uw machine",
"providerOpenAIDesc": "Meest nauwkeurig, vereist API-sleutel"
"providerOpenAIDesc": "Meest nauwkeurig, vereist API-sleutel",
"aiNote": "AI-opmerking",
"aiNoteDesc": "Schakel AI-chatknop en tekstverbeteringstools in",
"languageDetection": "Taaldetectie",
"languageDetectionDesc": "Detecteert automatisch de taal van uw aantekeningen",
"autoLabeling": "Labelsuggesties",
"autoLabelingDesc": "Stelt automatisch labels voor en past deze toe op uw notities",
"noteHistory": "Let op de geschiedenis",
"noteHistoryDesc": "Schakel momentopnamen van versies en herstel vanuit de geschiedenis in"
},
"general": {
"loading": "Laden...",
@@ -764,7 +959,9 @@
"markDone": "Markeren als voltooid",
"markUndone": "Markeren als onvoltooid",
"todayAt": "Vandaag om {time}",
"tomorrowAt": "Morgen om {time}"
"tomorrowAt": "Morgen om {time}",
"clearCompleted": "Duidelijk voltooid",
"viewAll": "Bekijk alle herinneringen"
},
"notebook": {
"create": "Notitieboek maken",
@@ -795,7 +992,11 @@
"confidence": "betrouwbaarheid",
"savingReminder": "Herinnering opslaan mislukt",
"removingReminder": "Herinnering verwijderen mislukt",
"generatingDescription": "Please wait..."
"generatingDescription": "Please wait...",
"pinnedFrozenTooltip": "Vastgezet notitieboekje — bestelling bevroren",
"organizeNotebookWithAITooltip": "Organiseer dit notitieboekje met AI",
"assistantRequiredForSummarize": "Schakel AI Assistant in de instellingen in om samen te vatten",
"createSubnotebook": "Subnotitieblok toevoegen"
},
"notebookSuggestion": {
"title": "Verplaatsen naar {name}?",
@@ -808,6 +1009,9 @@
},
"admin": {
"title": "Beheerdashboard",
"adminConsole": "Beheerdersconsole",
"navSection": "Navigatie",
"backToApp": "Terug naar Herinnering",
"userManagement": "Gebruikersbeheer",
"chat": "AI Chat",
"lab": "Het Lab",
@@ -850,6 +1054,11 @@
"providerEmbeddingRequired": "AI_PROVIDER_EMBEDDING is vereist",
"providerOllamaOption": "🦙 Ollama (Local & Free)",
"providerOpenAIOption": "🤖 OpenAI (GPT-5, GPT-4)",
"providerAnthropicOption": "🧠 Antropisch (Claude API)",
"providerAnthropicCustomOption": "🧩 Antropisch aangepast (Berichten-API - MiniMax, etc.)",
"anthropicModelHint": "Kies een Claude-model-ID uit de suggesties of voer er handmatig een in (geen externe modellijst voor de officiële API).",
"anthropicCustomModelHint": "Anthropic-compatibele berichten-API (bijv. MiniMax): basis-URL https://api.minimax.io/anthropic (China: https://api.minimaxi.com/anthropic), model MiniMax-M2.7. Insluitingen: gebruik provider « Custom » + OpenAI URL https://api.minimax.io/v1.",
"anthropicCustomNoModelList": "Deze gateway geeft geen OpenAI-stijl /modellenlijst weer - kies het model uit de suggesties of typ het (bijvoorbeeld MiniMax-M2.7).",
"providerCustomOption": "🔧 Custom OpenAI-Compatible",
"providerDeepSeekOption": "🔍 DeepSeek",
"providerOpenRouterOption": "🌐 OpenRouter",
@@ -1003,7 +1212,14 @@
"error": "Fout:",
"testError": "Testfout: {error}",
"tipTitle": "Tip:",
"tipDescription": "Gebruik het AI-testpaneel om configuratieproblemen te diagnosticeren voordat u test."
"tipDescription": "Gebruik het AI-testpaneel om configuratieproblemen te diagnosticeren voordat u test.",
"chatTestTitle": "Chatassistent-test",
"chatTestDescription": "Test de AI-provider die door de chatassistent wordt gebruikt",
"chatGenerationTest": "💬 Chatassistent-test:",
"chatStep1": "Stuurt een testbericht naar de assistent",
"chatStep2": "Vraagt om een beknopt antwoord over wat de assistent doet",
"chatStep3": "Toont de modelreactie",
"chatStep4": "Controleert reactievermogen en latentie"
},
"sidebar": {
"dashboard": "Dashboard",
@@ -1194,6 +1410,7 @@
"notesViewLabel": "Notities weergave",
"notesViewTabs": "Tabbladen (OneNote-stijl)",
"notesViewMasonry": "Kaarten (raster)",
"notesViewList": "Lijst (tijdschrift)",
"selectTheme": "Select theme",
"fontFamilyLabel": "Lettertypefamilie",
"fontFamilyDescription": "Kies het lettertype dat in de hele app wordt gebruikt",
@@ -1277,6 +1494,69 @@
"organizeWithAI": "Organiseren met AI",
"organize": "Organiseren"
},
"organizeNotebook": {
"title": "Organiseer notitieboekje",
"unknownError": "Onbekende fout",
"toastSuccess": "Notitieboekje georganiseerd — {gemaakt} subnotitieboekje(s) gemaakt, {verplaatst} notitie(s) verplaatst",
"intro": "AI zal de aantekeningen in dit notitieboekje analyseren en een plan voorstellen om ze te reorganiseren in thematische subnotitieboekjes.",
"bulletThemes": "Groepeer notities op onderwerp of thema",
"bulletSubfolders": "Maak ontbrekende subnotitieboekjes aan",
"bulletPreview": "Volledige preview vóór elke wijziging",
"analyzingTitle": "Analyseren…",
"analyzingSubtitle": "AI leest uw aantekeningen en identificeert themas",
"previewSummary": "{groups} groep(en) · {notes} notities · {newSubs} nieuwe sub-notitieboekje(s)",
"badgeNew": "Nieuw",
"untitledNote": "Naamloze notitie",
"notesInGroup": "{count} notities",
"executingTitle": "Organiseren…",
"executingSubtitle": "Subnotitieboekjes maken en notities verplaatsen",
"doneTitle": "Notitieboekje georganiseerd!",
"doneStats": "{aangemaakt} subnotitieboekje(s) gemaakt · {verplaatst} notitie(s) verplaatst",
"analyzeButton": "Analyseer met AI",
"restart": "Begin opnieuw",
"confirm": "Toepassen",
"closeButton": "Dichtbij"
},
"documentInfo": {
"tabInfo": "Info",
"tabVersions": "Versies",
"wordsLabel": "Woorden",
"charactersLabel": "Karakters",
"notebookLabel": "Notitieboekje",
"typeLabel": "Type",
"createdLabel": "Gemaakt",
"modifiedLabel": "Bijgewerkt",
"labelsSection": "Etiketten",
"idLabel": "Identiteitskaart",
"historyDisabled": "Geschiedenis is niet ingeschakeld voor deze notitie.",
"enableHistory": "Geschiedenis inschakelen",
"savedVersions": "Opgeslagen versies",
"savingEllipsis": "Besparing…",
"versionSaved": "Versie opgeslagen!",
"saveThisVersion": "Bewaar deze versie",
"loading": "Laden…",
"noVersion": "Nog geen versies",
"restoreTooltip": "Herstellen",
"deleteTooltip": "Verwijderen",
"comparisonMode": "Vergelijkingsmodus",
"comparisonSubtitle": "Vergelijk versies naast elkaar",
"deleteVersionConfirm": "Deze versie verwijderen?",
"latestBadge": "Nieuwste"
},
"languages": {
"targets": {
"french": "Frans",
"english": "Engels",
"spanish": "Spaans",
"german": "Duits",
"persian": "Perzisch",
"portuguese": "Portugees",
"italian": "Italiaans",
"chinese": "Chinese",
"japanese": "Japanse"
},
"customPlaceholder": "bijv. Arabisch, Russisch…"
},
"common": {
"unknown": "Onbekend",
"notAvailable": "Niet beschikbaar",
@@ -1398,12 +1678,16 @@
"scraper": "Monitor",
"researcher": "Onderzoeker",
"monitor": "Waarnemer",
"slideGenerator": "Dia's",
"excalidrawGenerator": "Diagram",
"custom": "Aangepast"
},
"typeDescriptions": {
"scraper": "Schraapt meerdere sites en maakt een samenvatting",
"researcher": "Zoekt naar informatie over een onderwerp",
"monitor": "Bewaakt een notitieboek en analyseert notities",
"slideGenerator": "Creëert een PowerPoint-presentatie van notities",
"excalidrawGenerator": "Creëert een Excalidraw-diagram van notities",
"custom": "Vrije agent met uw eigen prompt"
},
"form": {
@@ -1416,6 +1700,27 @@
"urlsOptional": "(optioneel)",
"sourceNotebook": "Notitieboek om te bewaken",
"selectNotebook": "Selecteer een notitieboek...",
"selectNotes": "Opmerkingen om te analyseren",
"notesSelected": "{{count}} notitie(s) geselecteerd",
"slideTheme": "Presentatie thema",
"slideThemeDefault": "Automatisch",
"slideStyle": "Visuele stijl",
"slideStyleSoft": "Zacht (aanbevolen)",
"slideStyleSharp": "Scherp en compact",
"slideStyleRounded": "Rond en ruim",
"slideStylePill": "Premie / Pil",
"excalidrawDiagramType": "Diagramtype",
"excalidrawDiagramTypeAuto": "Automatisch (domeindetectie)",
"excalidrawDiagramTypeFlowchart": "Stroomdiagram (proces)",
"excalidrawDiagramTypeMindmap": "Mindmap (ideeën)",
"excalidrawDiagramTypeOrgChart": "Organigram (teams)",
"excalidrawDiagramTypeTimeline": "Tijdlijn / routekaart",
"excalidrawDiagramTypeProcessMap": "Proceskaart (operaties)",
"excalidrawDiagramTypeArchitectureCloud": "Cloudarchitectuur (zones/RG)",
"excalidrawDiagramStyle": "Excalidraw-diagramstijl",
"excalidrawDiagramStyleDefault": "Gekleurd (Excalidraw)",
"excalidrawDiagramStyleSketchPlus": "Sketch+ (verbeterde Excalidraw)",
"excalidrawDiagramStyleAustere": "Sober (minimaal)",
"targetNotebook": "Doelnotitieboek",
"inbox": "Inbox",
"instructions": "AI-instructies",
@@ -1485,6 +1790,8 @@
"updated": "Agent bijgewerkt",
"deleted": "\"{name}\" verwijderd",
"deleteError": "Fout bij verwijderen",
"running": "Generatie aan de gang…",
"runningDesc": "Het genereren kan enkele minuten duren. U kunt vrij navigeren.",
"runSuccess": "\"{name}\" succesvol uitgevoerd",
"runError": "Fout: {error}",
"runFailed": "Uitvoering mislukt",
@@ -1519,13 +1826,24 @@
"chercheur": {
"name": "Onderzoeker",
"description": "Zoekt naar diepgaande informatie over een onderwerp en maakt een gestructureerde notitie met referenties."
},
"slideGenerator": {
"name": "Diagenerator",
"description": "Leest notities uit een notitieboekje en genereert automatisch een gestructureerde presentatie."
},
"excalidrawGenerator": {
"name": "Diagramgenerator",
"description": "Leest een notitie en genereert een visueel diagram in het Excalidraw Lab."
}
},
"runLog": {
"title": "Geschiedenis",
"noHistory": "Nog geen uitvoeringen",
"toolTrace": "{count} tool-aanroepen",
"step": "Stap {num}"
"step": "Stap {num}",
"clearConfirm": "Weet u zeker dat u de volledige geschiedenis van deze agent wilt verwijderen?",
"cleared": "Geschiedenis verwijderd",
"clearHistory": "Geschiedenis wissen"
},
"tools": {
"title": "Agent Tools",
@@ -1536,6 +1854,9 @@
"noteCreate": "Notitie Maken",
"urlFetch": "URL Ophalen",
"memorySearch": "Geheugen",
"generatePptx": "PPTX-dia's",
"generateSlides": "HTML-dia's",
"generateExcalidraw": "Excalidraw-diagram",
"configNeeded": "config",
"selected": "{count} geselecteerd",
"maxSteps": "Max iteraties"
@@ -1547,7 +1868,9 @@
"scraper": "U bent een monitoring-assistent. Vat artikelen van verschillende websites samen in een duidelijke, gestructureerde samenvatting.",
"researcher": "U bent een grondig onderzoeker. Produceer voor het gevraagde onderwerp een onderzoeksnoot met context, kernpunten, debatten en referenties.",
"monitor": "U bent een analytische assistent. Analyseer de verstrekte notities en stel invalshoeken, referenties en verbanden tussen notities voor.",
"custom": "U bent een behulpzame assistent."
"custom": "U bent een behulpzame assistent.",
"slideGenerator": "Je bent een presentatiemaker. Lees de aangeboden inhoud en maak gestructureerde dia's met titels, kernpunten en samenvattingen.",
"excalidrawGenerator": "Je bent een diagrammaker. Analyseer de aangeboden inhoud en creëer een duidelijk, georganiseerd visueel diagram."
},
"help": {
"title": "Agentengids",
@@ -1581,7 +1904,10 @@
"frequency": "Hoe vaak de agent automatisch draait. Begin met Handmatig om te testen.",
"instructions": "Aangepaste instructies die de standaard AI-prompt vervangen. Laat leeg voor automatische prompt.",
"tools": "Selecteer welke tools de agent kan gebruiken. Elke tool geeft de agent een specifieke mogelijkheid.",
"maxSteps": "Maximaal aantal redeneercycli. Meer stappen = diepere analyse maar duurt langer."
"maxSteps": "Maximaal aantal redeneercycli. Meer stappen = diepere analyse maar duurt langer.",
"selectNotes": "Selecteer specifieke opmerkingen om te analyseren. Als er niets is geselecteerd, gebruikt de agent alle notities uit het notitieblok.",
"slideTheme": "Kies een kleurenpalet voor de presentatie. Automatisch laat de AI beslissen.",
"slideStyle": "De visuele stijl heeft invloed op de hoekradius, de afstand en de informatiedichtheid."
}
}
},
@@ -1631,5 +1957,147 @@
"lab": {
"initializing": "Werkruimte initialiseren",
"loadingIdeas": "Je ideeën laden..."
},
"richTextEditor": {
"slashHint": "↑↓ navigeren · Invoegen openen · Tabbladschakelaarsectie",
"slashLoading": "AI-denken...",
"slashTabAll": "Alle",
"slashCatBasic": "Basis blokken",
"slashCatMedia": "Media",
"slashCatFormatting": "Opmaak",
"slashCatAi": "AI-opmerking",
"insertImage": "Afbeelding invoegen",
"imageUrlPlaceholder": "https://example.com/image.png",
"preview": "Voorbeeld",
"cancel": "Annuleren",
"insert": "Invoegen",
"slashText": "Tekst",
"slashTextDesc": "Simpele paragraaf",
"slashH1": "Kop 1",
"slashH1Desc": "Grote sectiekop",
"slashH2": "Rubriek 2",
"slashH2Desc": "Middelgrote sectiekop",
"slashH3": "Rubriek 3",
"slashH3Desc": "Kleine sectiekop",
"slashBullet": "Lijst met opsommingstekens",
"slashBulletDesc": "Ongeordende lijst",
"slashNumbered": "Genummerde lijst",
"slashNumberedDesc": "Bestelde genummerde lijst",
"slashTodo": "Takenlijst",
"slashTodoDesc": "Taken met selectievakjes",
"slashQuote": "Citaat",
"slashQuoteDesc": "Leg een citaat vast",
"slashCode": "Codeblok",
"slashCodeDesc": "Codefragment",
"slashDivider": "Verdeler",
"slashDividerDesc": "Horizontale afscheider",
"slashTable": "Tafel",
"slashTableDesc": "Voeg een eenvoudig raster in",
"slashDiagram": "Diagram",
"slashDiagramDesc": "Genereer een flow of mindmap",
"slashSlides": "Presentatie",
"slashSlidesDesc": "Genereer een prachtig slide-deck",
"slashImage": "Afbeelding",
"slashImageDesc": "Sluit een afbeelding in via een URL",
"slashAlignLeft": "Links uitlijnen",
"slashAlignLeftDesc": "Tekst links uitlijnen",
"slashAlignCenter": "Centrum",
"slashAlignCenterDesc": "Centreer de tekst",
"slashAlignRight": "Rechts uitlijnen",
"slashAlignRightDesc": "Tekst rechts uitlijnen",
"slashSuperscript": "Superscript",
"slashSuperscriptDesc": "Tekst boven de basislijn",
"slashSubscript": "Abonnement",
"slashSubscriptDesc": "Tekst onder de basislijn",
"slashClarify": "Verduidelijken",
"slashClarifyDesc": "Maak de tekst duidelijker",
"slashShorten": "Verkorten",
"slashShortenDesc": "Verdicht de tekst",
"slashImprove": "Verbeteren",
"slashImproveDesc": "Verbeter de stijl",
"slashExpand": "Uitbreiden",
"slashExpandDesc": "Werk de tekst uit en verrijk deze",
"imageModalTitle": "Afbeelding invoegen",
"imageModalPreview": "Voorbeeld",
"imageModalCancel": "Annuleren",
"imageModalInsert": "Invoegen",
"imageModalInvalidUrl": "Voer een geldige URL in",
"imageModalLoadFailed": "Kan afbeelding niet laden",
"linkPlaceholder": "Plak of typ een link...",
"bold": "Vetgedrukt",
"italic": "Cursief",
"underline": "Onderstrepen",
"strike": "Doorhalen",
"code": "Code",
"highlight": "Hoogtepunt",
"superscript": "Superscript",
"subscript": "Abonnement",
"addBlock": "Blok toevoegen",
"placeholder": "Typ '/' voor opdrachten..."
},
"brainstorm": {
"title": "Waves of Thought",
"subtitle": "Unfold dimensions of potentiality",
"placeholder": "Enter a concept to unfold...",
"generating": "AI is harvesting seeds of thought...",
"newBrainstorm": "New Brainstorm",
"noSessions": "No brainstorms yet",
"startOne": "Start one",
"sessions": "Brainstorms",
"seedLabel": "Seed Idea",
"ideaPromptDetailed": "Voer uw idee, vraag of onderwerp in om te brainstormen...",
"brainstormThisIdea": "Brainstorm this idea",
"startBrainstorm": "Start Brainstorm",
"spatialMode": "Spatial Exploration Mode",
"wave1": "Wave 1",
"wave2": "Wave 2",
"wave3": "Wave 3",
"export": "Export",
"exporting": "Exporting...",
"wave": "Wave",
"novelty": "Novelty",
"originConnection": "Origin connection",
"linkedNotes": "Linked notes",
"deepen": "Deepen",
"deepening": "Generating...",
"extract": "Create Note",
"converting": "Converting...",
"dismiss": "Not pertinent",
"noteCreated": "Note Created",
"ideas": "ideas",
"cancel": "Cancel",
"delete": "Delete",
"ideaOrigin": "Origin of the idea",
"noNoteLink": "Purely generative idea",
"derived_from": "Derived from",
"opposes": "In opposition with",
"extends": "Extends",
"synthesizes": "Synthesizes",
"transposes": "Transposes",
"none_found": "No note link",
"viewNote": "View note",
"addIdea": "Add idea",
"manualIdeaPrompt": "Title of your idea:",
"invite": "Invite",
"linkCopied": "Invite link copied!",
"activityTitle": "Activiteit",
"noActivity": "Nog geen activiteit",
"justNow": "zojuist",
"humanIdea": "Menselijk",
"aiIdea": "AI",
"respondsTo": "Reageert op",
"adding": "Toevoegen...",
"manualIdeaDesc": "Deel uw idee met het brainstormcanvas",
"manualIdeaTitle": "Titel",
"manualIdeaTitlePlaceholder": "Jouw idee in een paar woorden...",
"manualIdeaDescLabel": "Beschrijving (optioneel)",
"manualIdeaDescPlaceholder": "Werk je idee verder uit...",
"activity": {
"manual_idea": "een idee toegevoegd",
"wave_generated": "een golf gegenereerd",
"joined": "heeft zich bij de sessie aangesloten",
"idea_dismissed": "een idee afgewezen",
"invite_created": "een uitnodiging aangemaakt"
}
}
}
}

View File

@@ -32,6 +32,8 @@
},
"sidebar": {
"notes": "Notatki",
"recent": "Ostatni",
"quickNav": "Szybka nawigacja",
"reminders": "Przypomnienia",
"labels": "Etykiety",
"editLabels": "Edytuj etykiety",
@@ -40,15 +42,35 @@
"noLabelsInNotebook": "Brak etykiet w tym notatniku",
"archive": "Archiwum",
"trash": "Kosz",
"clearFilter": "Remove filter"
"clearFilter": "Remove filter",
"inbox": "Skrzynka odbiorcza",
"sharedWithMe": "Udostępniono mi",
"sortNewest": "Najpierw najnowsze",
"sortOldest": "Najpierw najstarszy",
"sortAlpha": "A → Z",
"accountMenu": "Menu konta",
"profile": "Profil",
"signOut": "Wyloguj się",
"sortOrder": "Kolejność sortowania",
"freezePinnedNotebook": "Przypnij kolejność paska bocznego notatnika",
"unfreezePinnedNotebook": "Odepnij zamówienie na pasku bocznym notatnika",
"newSubNotebook": "Nowy subnotebook",
"renameNotebook": "Przemianować"
},
"notes": {
"title": "Notatki",
"newNote": "Nowa notatka",
"reorganize": "Reorganizuj notatki",
"untitled": "Bez tytułu",
"placeholder": "Zrób notatkę...",
"markdownPlaceholder": "Zrób notatkę... (Markdown obsługiwany)",
"titlePlaceholder": "Tytuł",
"noteTypes": {
"richtext": "Bogaty tekst",
"markdown": "Obniżka cen",
"text": "Zwykły tekst",
"checklist": "Lista kontrolna"
},
"listItem": "Element listy",
"addListItem": "+ Element listy",
"newChecklist": "Nowa lista kontrolna",
@@ -58,6 +80,7 @@
"confirmDelete": "Czy na pewno chcesz usunąć tę notatkę?",
"confirmLeaveShare": "Czy na pewno chcesz opuścić tę udostępnioną notatkę?",
"sharedBy": "Udostępnione przez",
"sharedShort": "Wspólny",
"leaveShare": "Opuść",
"delete": "Usuń",
"archive": "Archiwizuj",
@@ -136,6 +159,8 @@
"dragToReorder": "Przeciągnij, aby zmienić kolejność",
"more": "Więcej",
"emptyState": "Brak notatek tutaj",
"metadataPanel": "Bliższe dane",
"metadataNotebook": "Zeszyt",
"emptyStateTabs": "Brak notatek. Użyj \"Nowa notatka\" na pasku bocznym, aby dodać (sugestie tytułów AI pojawią się w edytorze).",
"inNotebook": "W notatniku",
"moveFailed": "Przenoszenie nie powiodło się",
@@ -147,11 +172,6 @@
"unpinned": "Odepnięta",
"redoShortcut": "Ponów (Ctrl+Y)",
"undoShortcut": "Cofnij (Ctrl+Z)",
"viewCards": "Widok kart",
"viewCardsTooltip": "Siatka kart z przeciąganiem i zmianą kolejności",
"viewTabs": "Widok listy",
"viewTabsTooltip": "Karty na górze, notatka poniżej — przeciągnij karty, aby zmienić kolejność",
"viewModeGroup": "Tryb wyświetlania notatek",
"reorderTabs": "Zmień kolejność kart",
"modified": "Zmodyfikowano",
"created": "Utworzono",
@@ -160,15 +180,18 @@
"savedStatus": "Zapisano",
"dirtyStatus": "Zmodyfikowano",
"completedLabel": "Ukończone",
"notes.emptyNotebook": "Pusty notatnik",
"notes.emptyNotebookDesc": "Ten notatnik nie ma notatek. Kliknij + aby utworzyć.",
"notes.noNoteSelected": "Nie wybrano notatki",
"notes.selectOrCreateNote": "Wybierz notatkę z listy lub utwórz nową.",
"notes": {
"emptyNotebook": "Pusty notatnik",
"emptyNotebookDesc": "Ten notatnik nie ma notatek. Kliknij + aby utworzyć.",
"noNoteSelected": "Nie wybrano notatki",
"selectOrCreateNote": "Wybierz notatkę z listy lub utwórz nową."
},
"commitVersion": "Zapisz wersję",
"versionSaved": "Wersja zapisana",
"deleteVersion": "Usuń tę wersję",
"versionDeleted": "Wersja usunięta",
"deleteVersionConfirm": "Usunąć tę wersję trwale?",
"deleteVersionDesc": "Tej akcji nie można cofnąć. Wersja zostanie trwale usunięta z historii.",
"historyMode": "Tryb historii",
"historyModeManual": "Ręczny (przycisk commit)",
"historyModeAuto": "Automatyczny (inteligentny)",
@@ -184,6 +207,10 @@
"enableHistory": "Włącz historię",
"historyEmpty": "Brak dostępnych wersji",
"historySelectVersion": "Wybierz wersję, aby zobaczyć podgląd",
"currentVersion": "aktualny",
"compareVersions": "Porównywać",
"diffTitle": "Porównanie",
"diffSelectHint": "Kliknij 2 wersje na liście, aby je porównać",
"sortBy": "Sortuj według",
"sortDateDesc": "Data (najnowsze)",
"sortDateAsc": "Data (najstarsze)",
@@ -197,10 +224,14 @@
"createFailed": "Failed to create note",
"updateFailed": "Failed to update note",
"archived": "Note archived",
"unarchivedSuccess": "Notatka usunięta z archiwum",
"archiveFailed": "Failed to archive",
"sort": "Sort",
"confirmDeleteTitle": "Delete note",
"leftShare": "Share removed",
"ideaOrigin": "Origin of the idea",
"noNoteLink": "Purely generative idea",
"dismiss": "Not pertinent",
"dismissed": "Note dismissed from recent",
"generalNotes": "General Notes",
"noteType": "Typ notatki",
@@ -214,7 +245,23 @@
"switchTypeTitle": "Zmienić typ notatki?",
"switchTypeWarning": "Niektóre formatowanie może zostać utracone przy zmianie na {type}.",
"switchTypeContentPreserved": "Twoja treść zostanie zachowana jako zwykły tekst.",
"switchType": "Zmień na {type}"
"switchType": "Zmień na {type}",
"saveNow": "Zapisz teraz",
"backToCollection": "Powrót do kolekcji",
"markdownEditingTitle": "Wróć do edycji",
"markdownPreviewTitle": "Zapowiedź",
"brainstormThisIdea": "Przemyśl ten pomysł",
"brainstormThisIdeaAria": "Przemyśl ten pomysł",
"shareNoteTitle": "Udostępnij notatkę",
"shareNoteAria": "Udostępnij notatkę",
"saveNoteAria": "Zapisz notatkę",
"noChangesToSaveAria": "Brak zmian do zapisania",
"optionsMenuAria": "Menu opcji",
"deleteNoteConfirmItem": "Usuń notatkę",
"noteDeletedToast": "Uwaga usunięta.",
"deleteNoteFailedToast": "Nie udało się usunąć.",
"documentInfoAria": "Informacje o dokumencie",
"noModification": "Żadnych zmian"
},
"pagination": {
"previous": "←",
@@ -296,7 +343,24 @@
"accessRevoked": "Dostęp został cofnięty",
"errorLoading": "Błąd ładowania współpracowników",
"failedToAdd": "Nie udało się dodać współpracownika",
"failedToRemove": "Nie udało się usunąć współpracownika"
"failedToRemove": "Nie udało się usunąć współpracownika",
"shareCompactTitle": "Udział",
"inviteByEmailLabel": "Zaproś e-mailem",
"accessReadCompact": "Pogląd",
"accessEditCompact": "Redagować",
"sendInvitation": "Wyślij zaproszenie",
"invitationSentBadge": "Zaproszenie wysłane",
"sharedAccessLabel": "Dostęp wspólny",
"noCollaboratorsEmpty": "Nie ma jeszcze współpracowników.",
"removeAccessTitle": "Usuń dostęp",
"toastInviteSentTo": "Zaproszenie wysłane na adres {email}",
"toastAccessRemoved": "Dostęp usunięty dla {target}",
"toastUserFallback": "użytkownik",
"toastSharingError": "Błąd udostępniania",
"toastEmailNotFound": "Nie znaleziono konta z tym adresem e-mail.",
"toastAlreadySharedUser": "Ta notatka została już udostępniona temu użytkownikowi.",
"toastRemoveAccessFailed": "Nie udało się usunąć dostępu.",
"userFallback": "Użytkownik"
},
"ai": {
"analyzing": "Analiza AI...",
@@ -326,6 +390,8 @@
"transforming": "Przekształcanie...",
"transformSuccess": "Tekst przekształcony do Markdown pomyślnie!",
"transformError": "Błąd podczas przekształcania",
"convertToRichtext": "Konwertuj na tekst sformatowany",
"convertingToRichtext": "Konwersja...",
"assistant": "Asystent AI",
"generating": "Generowanie...",
"generateTitles": "Generuj tytuły",
@@ -389,6 +455,8 @@
"undoAI": "Cofnij przekształcenie AI",
"undoApplied": "Oryginalny tekst przywrócony",
"minWordsError": "Notatka musi zawierać co najmniej 5 słów, aby używać akcji AI.",
"wordCountMin": "Proszę wybrać co najmniej {min} słów do przeformułowania (obecnie {current} słów)",
"wordCountMax": "Proszę wybrać maksymalnie {max} słów do przeformułowania (obecnie {current} słów)",
"genericError": "Błąd AI",
"actionError": "Błąd podczas akcji AI",
"appliedToNote": "Zastosowano w notatce",
@@ -404,6 +472,15 @@
"chatTab": "Chat",
"noteActions": "Akcje notatki",
"askToStart": "Zadaj asystentowi pytanie, aby rozpocząć.",
"chatPanelContext": "Kontekst",
"chatPanelNotebookPlus": "+ Notatnik",
"chatPanelWritingTone": "Ton pisania",
"scopeAutoBadge": "Automatyczny",
"chatNoteQuestionPlaceholder": "Zadaj pytanie dotyczące tej notatki...",
"chatNotebookSelectPlaceholder": "Dołącz notatnik...",
"assistantTabActions": "Działania",
"resourcePreviewAiTitle": "Podgląd sztucznej inteligencji",
"resourcePreviewInjectFromChat": "Wstrzyknij z czatu",
"contextLabel": "Kontekst",
"thisNote": "Ta notatka",
"allMyNotes": "Wszystkie notatki",
@@ -415,6 +492,7 @@
"newLineHint": "Shift+Enter = nowa linia",
"resultLabel": "Wynik",
"discardAction": "Odrzuć",
"organization": "Organizacja",
"transformationsDesc": "Transformacje — zastosowane bezpośrednio w notatce",
"writeMinWordsAction": "Napisz co najmniej 5 słów, aby aktywować akcje AI.",
"processingAction": "Przetwarzanie...",
@@ -425,7 +503,45 @@
"shorten": "Skróć",
"improve": "Popraw",
"toMarkdown": "Do Markdown",
"describeImages": "Describe images"
"describeImages": "Describe images",
"fixGrammar": "Napraw gramatykę",
"translate": "Tłumaczyć",
"explain": "Wyjaśnić",
"toRichText": "Konwertuj na tekst sformatowany"
},
"generate": {
"slides": "Generuj slajdy",
"sectionLabel": "Narzędzia generacji",
"theme": "Temat",
"themeArchitecturalMono": "Mono architektoniczne",
"themeVibrantTech": "Wibrująca technologia",
"themeMinimalSilk": "Minimalny jedwab",
"style": "Styl",
"styleProfessional": "Profesjonalny",
"styleCreative": "Twórczy",
"styleBrutalist": "Brutalista",
"diagram": "Wygeneruj diagram",
"diagramReadyHint": "Zamień notatkę na przepływ wizualny",
"diagramType": "Typ diagramu",
"typeAuto": "Automatyczne wykrywanie",
"typeFlowchart": "Schemat blokowy",
"typeMindMap": "Mapa myśli",
"typeTimeline": "Oś czasu",
"typeOrgChart": "Schemat organizacyjny",
"typeArchitecture": "Architektura",
"typeProcessMap": "Mapa procesu",
"styleSketchy": "Szkicowy",
"styleSoft": "Miękki",
"styleMinimal": "Minimalny",
"styleDraft": "Projekt",
"stylePolished": "Błyszczący",
"styleHandwritten": "Odręcznie",
"diagramReady": "Schemat jest gotowy!",
"openInExcalidraw": "Otwórz w laboratorium Excalidraw",
"insertDiagramInNote": "Osadź plik PNG w bieżącej notatce",
"diagramImageAlt": "Schemat wygenerowany przez sztuczną inteligencję",
"insertedInNote": "Schemat wstawiony w notatce",
"insertExportError": "Błąd podczas eksportowania/przesyłania diagramu"
},
"openAssistant": "Otwórz asystenta AI",
"poweredByMomento": "Napędzany przez Momento AI",
@@ -442,7 +558,64 @@
"aiCopilot": "AI Copilot",
"suggestTitle": "Sugestia tytułu AI",
"generateTitleFromImage": "Generate title from image",
"titleGenerated": "Title generated from image"
"titleGenerated": "Title generated from image",
"resourceTab": "Ratunek",
"aiNoteTitle": "Uwaga AI",
"injectReplace": "Zastępować",
"injectReplaceTitle": "Zastąp treść notatki tą wiadomością",
"injectComplete": "Kompletny",
"injectCompleteTitle": "Uzupełnij notatkę tą wiadomością (AI)",
"injectMerge": "Łączyć",
"injectMergeTitle": "Połącz z notatką (AI)",
"imagesCount": "Zdjęcia: {count}",
"resource": {
"failedToLoadUrl": "Nie udało się załadować tego adresu URL",
"pageLoaded": "Strona załadowana: {title}",
"pageLoadError": "Błąd ładowania strony",
"pasteOrUrlFirst": "Najpierw wklej tekst lub załaduj adres URL",
"enrichError": "Błąd wzbogacania",
"enrichErrorShort": "Błąd wzbogacania",
"contentApplied": "Treść zastosowana do notatki ✓",
"fromChat": "💬 Z czatu",
"replacement": "↓ Wymiana",
"completedByAI": "✦ Ukończone przez sztuczną inteligencję",
"mergedByAI": "⟳ Połączone przez sztuczną inteligencję",
"rendered": "Renderowane",
"cancel": "Anulować",
"applyToNote": "Zastosuj do notatki",
"urlLabel": "Adres URL (opcjonalnie)",
"resourceText": "Tekst źródłowy",
"resourcePlaceholder": "Wklej tutaj swój tekst (przecena, HTML, zwykły tekst…)",
"words": "słowa",
"integrationMode": "Tryb integracji",
"modeReplace": "Zastępować",
"modeReplaceDesc": "Bezpośrednio, bez sztucznej inteligencji",
"modeComplete": "Kompletny",
"modeCompleteDesc": "Dodaje bez przepisywania",
"modeMerge": "Łączyć",
"modeMergeDesc": "Przepisuje i integruje",
"aiProcessing": "Przetwarzanie sztucznej inteligencji…",
"preview": "Zapowiedź",
"generatePreview": "Wygeneruj podgląd",
"emptyNoteHint": "💡 Notatka jest pusta — zawartość zasobów zostanie zintegrowana bezpośrednio."
},
"cancel": "Anulować",
"copied": "Skopiowano",
"copy": "Kopia",
"transformations": "Transformacje",
"otherLanguage": "Inny język",
"translateNow": "Przetłumacz teraz",
"generationTools": "Narzędzia generacji",
"generateSlidesLoading": "⏳ Generowanie prezentacji...",
"generateDiagramLoading": "⏳ Generowanie diagramu...",
"errorShort": "Błąd",
"readyToast": "Gotowy!",
"downloadFailedToast": "Pobieranie nie powiodło się",
"pptxDownloadButton": "Pobierz .pptx",
"presentationReadyBadge": "Prezentacja gotowa",
"openInLabTitle": "Otwórz w laboratorium",
"inlineSummaryMarkdown": "**Streszczenie:**",
"networkErrorShort": "Błąd sieci."
},
"titleSuggestions": {
"available": "Sugestie tytułów",
@@ -548,7 +721,19 @@
"untitled": "Bez tytułu",
"notifications": "Powiadomienia",
"declined": "Udostępnienie odrzucone",
"removed": "Notatka usunięta z listy"
"removed": "Notatka usunięta z listy",
"slidesReady": "Prezentacja gotowa",
"openSlides": "Otwarta prezentacja",
"canvasReady": "Schemat gotowy",
"pptxReady": "Slajdy gotowe",
"downloadPptx": "Pobierz .pptx",
"markAllRead": "Zaznacz wszystkie jako przeczytane",
"agentSuccess": "Agent skończył",
"agentFailed": "Agent zawiódł",
"brainstormInvite": "Burza mózgów",
"brainstormJoined": "Burza mózgów",
"systemNotification": "System",
"downloadFailed": "Pobieranie nie powiodło się"
},
"nav": {
"home": "Strona główna",
@@ -597,6 +782,17 @@
"themeLight": "Jasny",
"themeDark": "Ciemny",
"themeSystem": "Systemowy",
"themeBaseGroup": "Base",
"themePalettesGroup": "Color palettes",
"themeSepia": "Sepia",
"themeMidnight": "Midnight",
"themeRose": "Rose",
"themeGreen": "Green",
"themeLavender": "Lavender",
"themeSand": "Sand",
"themeOcean": "Ocean",
"themeSunset": "Sunset",
"themeBlue": "Blue",
"notifications": "Powiadomienia",
"language": "Język",
"selectLanguage": "Wybierz język",
@@ -630,17 +826,8 @@
"desktopNotifications": "Powiadomienia na pulpicie",
"desktopNotificationsDesc": "Otrzymuj powiadomienia w przeglądarce",
"notificationsDesc": "Zarządzaj swoimi preferencjami powiadomień",
"themeBaseGroup": "Base",
"themePalettesGroup": "Color palettes",
"themeSepia": "Sepia",
"themeMidnight": "Midnight",
"themeRose": "Rose",
"themeGreen": "Green",
"themeLavender": "Lavender",
"themeSand": "Sand",
"themeOcean": "Ocean",
"themeSunset": "Sunset",
"themeBlue": "Blue"
"autoSave": "Automatyczne zapisywanie",
"autoSaveDesc": "Automatycznie zapisuj zmiany podczas pisania"
},
"profile": {
"title": "Profil",
@@ -707,7 +894,15 @@
"providerDesc": "Wybierz preferowanego dostawcę AI",
"providerAutoDesc": "Ollama gdy dostępny, OpenAI jako alternatywa",
"providerOllamaDesc": "100% prywatny, działa lokalnie na twoim urządzeniu",
"providerOpenAIDesc": "Najdokładniejszy, wymaga klucza API"
"providerOpenAIDesc": "Najdokładniejszy, wymaga klucza API",
"aiNote": "Uwaga AI",
"aiNoteDesc": "Włącz przycisk czatu AI i narzędzia do ulepszania tekstu",
"languageDetection": "Wykrywanie języka",
"languageDetectionDesc": "Automatycznie wykrywa język Twoich notatek",
"autoLabeling": "Sugestie dotyczące etykiet",
"autoLabelingDesc": "Automatycznie sugeruje i stosuje etykiety do notatek",
"noteHistory": "Uwaga na historię",
"noteHistoryDesc": "Włącz migawki wersji i przywracanie z Historii"
},
"general": {
"loading": "Ładowanie...",
@@ -764,7 +959,9 @@
"markDone": "Oznacz jako ukończone",
"markUndone": "Oznacz jako nieukończone",
"todayAt": "Dzisiaj o {time}",
"tomorrowAt": "Jutro o {time}"
"tomorrowAt": "Jutro o {time}",
"clearCompleted": "Wyczyść zakończone",
"viewAll": "Wyświetl wszystkie przypomnienia"
},
"notebook": {
"create": "Utwórz notatnik",
@@ -795,7 +992,11 @@
"confidence": "pewność",
"savingReminder": "Nie udało się zapisać przypomnienia",
"removingReminder": "Nie udało się usunąć przypomnienia",
"generatingDescription": "Please wait..."
"generatingDescription": "Please wait...",
"pinnedFrozenTooltip": "Przypięty notatnik — zamówienie zamrożone",
"organizeNotebookWithAITooltip": "Uporządkuj ten notatnik za pomocą sztucznej inteligencji",
"assistantRequiredForSummarize": "Aby podsumować, włącz AI Assistant w ustawieniach",
"createSubnotebook": "Dodaj podnotatnik"
},
"notebookSuggestion": {
"title": "Przenieść do {name}?",
@@ -808,6 +1009,9 @@
},
"admin": {
"title": "Panel administracyjny",
"adminConsole": "Konsola administracyjna",
"navSection": "Nawigacja",
"backToApp": "Wracając do Memento",
"userManagement": "Zarządzanie użytkownikami",
"chat": "Czat AI",
"lab": "Laboratorium",
@@ -850,6 +1054,11 @@
"providerEmbeddingRequired": "AI_PROVIDER_EMBEDDING jest wymagany",
"providerOllamaOption": "🦙 Ollama (lokalny i darmowy)",
"providerOpenAIOption": "🤖 OpenAI (GPT-5, GPT-4)",
"providerAnthropicOption": "🧠 Antropiczny (Claude API)",
"providerAnthropicCustomOption": "🧩 Niestandardowy antropiczny (Messages API — MiniMax itp.)",
"anthropicModelHint": "Wybierz identyfikator modelu Claude z sugestii lub wprowadź go ręcznie (nie ma zdalnej listy modeli dla oficjalnego API).",
"anthropicCustomModelHint": "API Messages kompatybilne z Anthropic (np. MiniMax): bazowy adres URL https://api.minimax.io/anthropic (Chiny: https://api.minimaxi.com/anthropic), model MiniMax-M2.7. Osadzanie: użyj dostawcy „Niestandardowy” + adres URL OpenAI https://api.minimax.io/v1.",
"anthropicCustomNoModelList": "Ta bramka nie udostępnia listy/modeli w stylu OpenAI — wybierz model z sugestii lub wpisz go (np. MiniMax-M2.7).",
"providerCustomOption": "🔧 Niestandardowy (kompatybilny z OpenAI)",
"providerDeepSeekOption": "🔍 DeepSeek",
"providerOpenRouterOption": "🌐 OpenRouter",
@@ -1003,7 +1212,14 @@
"error": "Błąd:",
"testError": "Błąd testu: {error}",
"tipTitle": "Wskazówka:",
"tipDescription": "Użyj panelu testowania AI, aby zdiagnozować problemy z konfiguracją przed testowaniem."
"tipDescription": "Użyj panelu testowania AI, aby zdiagnozować problemy z konfiguracją przed testowaniem.",
"chatTestTitle": "Test asystenta czatu",
"chatTestDescription": "Przetestuj dostawcę AI używanego przez asystenta czatu",
"chatGenerationTest": "💬 Test asystenta czatu:",
"chatStep1": "Wysyła wiadomość testową do asystenta",
"chatStep2": "Prosi o zwięzłą odpowiedź na temat tego, czym zajmuje się asystent",
"chatStep3": "Pokazuje odpowiedź modelu",
"chatStep4": "Sprawdza responsywność i opóźnienia"
},
"sidebar": {
"dashboard": "Panel główny",
@@ -1194,6 +1410,7 @@
"notesViewLabel": "Układ notatek",
"notesViewTabs": "Karty (styl OneNote)",
"notesViewMasonry": "Karty (siatka)",
"notesViewList": "Lista (magazyn)",
"selectTheme": "Select theme",
"fontFamilyLabel": "Rodzina czcionek",
"fontFamilyDescription": "Wybierz czcionkę używaną w całej aplikacji",
@@ -1277,6 +1494,69 @@
"organizeWithAI": "Organizuj z AI",
"organize": "Organizuj"
},
"organizeNotebook": {
"title": "Zorganizuj notatnik",
"unknownError": "Nieznany błąd",
"toastSuccess": "Notatnik uporządkowany — utworzono notes podrzędny: {created}, przeniesiono notatki: {moved}",
"intro": "AI przeanalizuje notatki w tym notatniku i zaproponuje plan ich reorganizacji w podnotatniki tematyczne.",
"bulletThemes": "Grupuj notatki według tematu lub motywu",
"bulletSubfolders": "Utwórz brakujące podnotatniki",
"bulletPreview": "Pełny podgląd przed jakąkolwiek zmianą",
"analyzingTitle": "Analizuję…",
"analyzingSubtitle": "AI czyta Twoje notatki i identyfikuje motywy",
"previewSummary": "{groups} grupy · {notes} notatki · {newSubs} nowy podnotatnik(y)",
"badgeNew": "Nowy",
"untitledNote": "Notatka bez tytułu",
"notesInGroup": "Liczba notatek",
"executingTitle": "Organizowanie…",
"executingSubtitle": "Tworzenie podnotatków i ruchomych notatek",
"doneTitle": "Notatnik zorganizowany!",
"doneStats": "Utworzono {created} podnotatków · Przeniesiono {moved} notatek",
"analyzeButton": "Analizuj za pomocą sztucznej inteligencji",
"restart": "Zacznij od nowa",
"confirm": "Stosować",
"closeButton": "Zamknąć"
},
"documentInfo": {
"tabInfo": "Informacje",
"tabVersions": "Wersje",
"wordsLabel": "Słowa",
"charactersLabel": "Pismo",
"notebookLabel": "Zeszyt",
"typeLabel": "Typ",
"createdLabel": "Stworzony",
"modifiedLabel": "Zaktualizowano",
"labelsSection": "Etykiety",
"idLabel": "ID",
"historyDisabled": "Historia nie jest włączona dla tej notatki.",
"enableHistory": "Włącz historię",
"savedVersions": "Zapisane wersje",
"savingEllipsis": "Oszczędność…",
"versionSaved": "Wersja zapisana!",
"saveThisVersion": "Zapisz tę wersję",
"loading": "Załadunek…",
"noVersion": "Nie ma jeszcze wersji",
"restoreTooltip": "Przywrócić",
"deleteTooltip": "Usuwać",
"comparisonMode": "Tryb porównania",
"comparisonSubtitle": "Porównaj wersje obok siebie",
"deleteVersionConfirm": "Usunąć tę wersję?",
"latestBadge": "Najnowszy"
},
"languages": {
"targets": {
"french": "francuski",
"english": "angielski",
"spanish": "hiszpański",
"german": "niemiecki",
"persian": "perski",
"portuguese": "portugalski",
"italian": "włoski",
"chinese": "chiński",
"japanese": "japoński"
},
"customPlaceholder": "np. Arabski, rosyjski…"
},
"common": {
"unknown": "Nieznany",
"notAvailable": "Niedostępne",
@@ -1398,12 +1678,16 @@
"scraper": "Monitor",
"researcher": "Badacz",
"monitor": "Obserwator",
"slideGenerator": "Slajdy",
"excalidrawGenerator": "Diagram",
"custom": "Niestandardowy"
},
"typeDescriptions": {
"scraper": "Pobiera dane z wielu stron i tworzy podsumowanie",
"researcher": "Wyszukuje informacje na dany temat",
"monitor": "Obserwuje notatnik i analizuje notatki",
"slideGenerator": "Tworzy prezentację programu PowerPoint z notatek",
"excalidrawGenerator": "Tworzy diagram Excalidraw na podstawie notatek",
"custom": "Swobodny agent z własnym promptem"
},
"form": {
@@ -1416,6 +1700,27 @@
"urlsOptional": "(opcjonalnie)",
"sourceNotebook": "Notatnik do obserwacji",
"selectNotebook": "Wybierz notatnik...",
"selectNotes": "Notatki do analizy",
"notesSelected": "Wybrano notatki: {{count}}",
"slideTheme": "Temat prezentacji",
"slideThemeDefault": "Automatyczny",
"slideStyle": "Styl wizualny",
"slideStyleSoft": "Miękkie (zalecane)",
"slideStyleSharp": "Ostry i gęsty",
"slideStyleRounded": "Zaokrąglony i przestronny",
"slideStylePill": "Premium / pigułka",
"excalidrawDiagramType": "Typ diagramu",
"excalidrawDiagramTypeAuto": "Auto (wykrywanie domeny)",
"excalidrawDiagramTypeFlowchart": "Schemat blokowy (proces)",
"excalidrawDiagramTypeMindmap": "Mapa myśli (pomysły)",
"excalidrawDiagramTypeOrgChart": "Schemat organizacyjny (zespoły)",
"excalidrawDiagramTypeTimeline": "Oś czasu / plan działania",
"excalidrawDiagramTypeProcessMap": "Mapa procesu (operacje)",
"excalidrawDiagramTypeArchitectureCloud": "Architektura chmurowa (strefy/RG)",
"excalidrawDiagramStyle": "Styl diagramu Excalidraw",
"excalidrawDiagramStyleDefault": "Kolorowe (Excalidraw)",
"excalidrawDiagramStyleSketchPlus": "Szkic+ (ulepszony Excalidraw)",
"excalidrawDiagramStyleAustere": "Surowy (minimalny)",
"targetNotebook": "Notatnik docelowy",
"inbox": "Skrzynka odbiorcza",
"instructions": "Instrukcje AI",
@@ -1485,6 +1790,8 @@
"updated": "Agent zaktualizowany",
"deleted": "\"{name}\" usunięty",
"deleteError": "Błąd usuwania",
"running": "Generowanie w toku…",
"runningDesc": "Generowanie może zająć kilka minut. Można swobodnie nawigować.",
"runSuccess": "\"{name}\" wykonany pomyślnie",
"runError": "Błąd: {error}",
"runFailed": "Wykonanie nie powiodło się",
@@ -1519,13 +1826,24 @@
"chercheur": {
"name": "Badacz tematów",
"description": "Wyszukuje szczegółowe informacje na dany temat i tworzy ustrukturyzowaną notatkę z odniesieniami."
},
"slideGenerator": {
"name": "Generator slajdów",
"description": "Czyta notatki z notatnika i automatycznie generuje uporządkowaną prezentację."
},
"excalidrawGenerator": {
"name": "Generator diagramów",
"description": "Czyta notatkę i generuje diagram wizualny w laboratorium Excalidraw."
}
},
"runLog": {
"title": "Historia",
"noHistory": "Brak historii wykonań",
"toolTrace": "{count} wywołań narzędzi",
"step": "Krok {num}"
"step": "Krok {num}",
"clearConfirm": "Czy na pewno chcesz usunąć całą historię tego agenta?",
"cleared": "Historia usunięta",
"clearHistory": "Wyczyść historię"
},
"tools": {
"title": "Narzędzia Agenta",
@@ -1536,6 +1854,9 @@
"noteCreate": "Utwórz Notatkę",
"urlFetch": "Pobierz URL",
"memorySearch": "Pamięć",
"generatePptx": "Slajdy PPTX",
"generateSlides": "Slajdy HTML",
"generateExcalidraw": "Schemat Excalidraw",
"configNeeded": "konfiguracja",
"selected": "{count} wybrano",
"maxSteps": "Maks. iteracji"
@@ -1547,7 +1868,9 @@
"scraper": "Jesteś asystentem monitorowania. Podsumuj artykuły z różnych stron w jasne, ustrukturyzowane podsumowanie.",
"researcher": "Jesteś rygorystycznym badaczem. Dla zadanego tematu przygotuj notatkę badawczą z kontekstem, kluczowymi punktami, dyskusjami i odniesieniami.",
"monitor": "Jesteś asystentem analitycznym. Przeanalizuj dostarczone notatki i zaproponuj kierunki, odniesienia i powiązania między notatkami.",
"custom": "Jesteś pomocnym asystentem."
"custom": "Jesteś pomocnym asystentem.",
"slideGenerator": "Jesteś twórcą prezentacji. Przeczytaj dostarczoną treść i utwórz uporządkowane slajdy z tytułami, kluczowymi punktami i podsumowaniami.",
"excalidrawGenerator": "Jesteś twórcą diagramów. Przeanalizuj dostarczoną treść i utwórz przejrzysty, zorganizowany diagram wizualny."
},
"help": {
"title": "Przewodnik po agentach",
@@ -1581,7 +1904,10 @@
"frequency": "Jak często agent uruchamia się automatycznie. Zacznij od Ręcznie, aby przetestować.",
"instructions": "Niestandardowe instrukcje zastępujące domyślny prompt AI. Zostaw puste, aby użyć automatycznego.",
"tools": "Wybierz, jakich narzędzi może używać agent. Każde narzędzie daje agentowi określoną zdolność.",
"maxSteps": "Maksymalna liczba cykli rozumowania. Więcej kroków = głębsza analiza, ale trwa dłużej."
"maxSteps": "Maksymalna liczba cykli rozumowania. Więcej kroków = głębsza analiza, ale trwa dłużej.",
"selectNotes": "Wybierz konkretne notatki do analizy. Jeśli żadna nie zostanie zaznaczona, agent użyje wszystkich notatek z notatnika.",
"slideTheme": "Wybierz paletę kolorów dla prezentacji. Automatycznie pozwala AI decydować.",
"slideStyle": "Styl wizualny wpływa na promień narożnika, odstępy i gęstość informacji."
}
}
},
@@ -1631,5 +1957,147 @@
"lab": {
"initializing": "Inicjalizacja przestrzeni",
"loadingIdeas": "Ładowanie twoich pomysłów..."
},
"richTextEditor": {
"slashHint": "↑↓ nawigacja · Wprowadź wstawkę · Sekcja przełączania kart",
"slashLoading": "AI myśli...",
"slashTabAll": "Wszystko",
"slashCatBasic": "Podstawowe bloki",
"slashCatMedia": "Głoska bezdźwięczna",
"slashCatFormatting": "Formatowanie",
"slashCatAi": "Uwaga AI",
"insertImage": "Wstaw obraz",
"imageUrlPlaceholder": "https://example.com/image.png",
"preview": "Zapowiedź",
"cancel": "Anulować",
"insert": "Wstawić",
"slashText": "Tekst",
"slashTextDesc": "Prosty akapit",
"slashH1": "Nagłówek 1",
"slashH1Desc": "Nagłówek dużej sekcji",
"slashH2": "Nagłówek 2",
"slashH2Desc": "Nagłówek sekcji średniej",
"slashH3": "Nagłówek 3",
"slashH3Desc": "Nagłówek małej sekcji",
"slashBullet": "Lista punktorów",
"slashBulletDesc": "Lista nieuporządkowana",
"slashNumbered": "Lista numerowana",
"slashNumberedDesc": "Zamówiona lista numerowana",
"slashTodo": "Lista zadań",
"slashTodoDesc": "Zadania pola wyboru",
"slashQuote": "Cytat",
"slashQuoteDesc": "Uchwyć cytat",
"slashCode": "Blok kodu",
"slashCodeDesc": "Fragment kodu",
"slashDivider": "Rozdzielacz",
"slashDividerDesc": "Separator poziomy",
"slashTable": "Tabela",
"slashTableDesc": "Wstaw prostą siatkę",
"slashDiagram": "Diagram",
"slashDiagramDesc": "Wygeneruj przepływ lub mapę myśli",
"slashSlides": "Prezentacja",
"slashSlidesDesc": "Wygeneruj piękną talię slajdów",
"slashImage": "Obraz",
"slashImageDesc": "Osadź obraz z adresu URL",
"slashAlignLeft": "Wyrównaj do lewej",
"slashAlignLeftDesc": "Wyrównaj tekst do lewej",
"slashAlignCenter": "Centrum",
"slashAlignCenterDesc": "Wyśrodkuj tekst",
"slashAlignRight": "Wyrównaj w prawo",
"slashAlignRightDesc": "Wyrównaj tekst do prawej",
"slashSuperscript": "Napisany u góry",
"slashSuperscriptDesc": "Tekst nad linią bazową",
"slashSubscript": "Indeks dolny",
"slashSubscriptDesc": "Tekst poniżej linii bazowej",
"slashClarify": "Wyjaśniać",
"slashClarifyDesc": "Spraw, aby tekst był wyraźniejszy",
"slashShorten": "Skracać",
"slashShortenDesc": "Skondensuj tekst",
"slashImprove": "Poprawić",
"slashImproveDesc": "Wzmocnij styl",
"slashExpand": "Zwiększać",
"slashExpandDesc": "Opracuj i wzbogacaj tekst",
"imageModalTitle": "Wstaw obraz",
"imageModalPreview": "Zapowiedź",
"imageModalCancel": "Anulować",
"imageModalInsert": "Wstawić",
"imageModalInvalidUrl": "Proszę wprowadzić prawidłowy adres URL",
"imageModalLoadFailed": "Nie udało się załadować obrazu",
"linkPlaceholder": "Wklej lub wpisz link...",
"bold": "Pogrubiony",
"italic": "italski",
"underline": "Podkreślać",
"strike": "Przekreślenie",
"code": "Kod",
"highlight": "Atrakcja",
"superscript": "Napisany u góry",
"subscript": "Indeks dolny",
"addBlock": "Dodaj blok",
"placeholder": "Wpisz „/”, aby uzyskać polecenia..."
},
"brainstorm": {
"title": "Waves of Thought",
"subtitle": "Unfold dimensions of potentiality",
"placeholder": "Enter a concept to unfold...",
"generating": "AI is harvesting seeds of thought...",
"newBrainstorm": "New Brainstorm",
"noSessions": "No brainstorms yet",
"startOne": "Start one",
"sessions": "Brainstorms",
"seedLabel": "Seed Idea",
"ideaPromptDetailed": "Wpisz swój pomysł, pytanie lub temat, aby przeprowadzić burzę mózgów...",
"brainstormThisIdea": "Brainstorm this idea",
"startBrainstorm": "Start Brainstorm",
"spatialMode": "Spatial Exploration Mode",
"wave1": "Wave 1",
"wave2": "Wave 2",
"wave3": "Wave 3",
"export": "Export",
"exporting": "Exporting...",
"wave": "Wave",
"novelty": "Novelty",
"originConnection": "Origin connection",
"linkedNotes": "Linked notes",
"deepen": "Deepen",
"deepening": "Generating...",
"extract": "Create Note",
"converting": "Converting...",
"dismiss": "Not pertinent",
"noteCreated": "Note Created",
"ideas": "ideas",
"cancel": "Cancel",
"delete": "Delete",
"ideaOrigin": "Origin of the idea",
"noNoteLink": "Purely generative idea",
"derived_from": "Derived from",
"opposes": "In opposition with",
"extends": "Extends",
"synthesizes": "Synthesizes",
"transposes": "Transposes",
"none_found": "No note link",
"viewNote": "View note",
"addIdea": "Add idea",
"manualIdeaPrompt": "Title of your idea:",
"invite": "Invite",
"linkCopied": "Invite link copied!",
"activityTitle": "Działalność",
"noActivity": "Brak aktywności",
"justNow": "właśnie",
"humanIdea": "Człowiek",
"aiIdea": "sztuczna inteligencja",
"respondsTo": "Odpowiada",
"adding": "Dodawanie...",
"manualIdeaDesc": "Podziel się swoim pomysłem na kanwie burzy mózgów",
"manualIdeaTitle": "Tytuł",
"manualIdeaTitlePlaceholder": "Twój pomysł w kilku słowach...",
"manualIdeaDescLabel": "Opis (opcjonalnie)",
"manualIdeaDescPlaceholder": "Opracuj swój pomysł...",
"activity": {
"manual_idea": "dodał pomysł",
"wave_generated": "wygenerował falę",
"joined": "dołączył do sesji",
"idea_dismissed": "odrzucił pomysł",
"invite_created": "utworzył zaproszenie"
}
}
}
}

View File

@@ -32,6 +32,8 @@
},
"sidebar": {
"notes": "Notas",
"recent": "Recente",
"quickNav": "Navegação rápida",
"reminders": "Lembretes",
"labels": "Etiquetas",
"editLabels": "Editar etiquetas",
@@ -40,15 +42,35 @@
"noLabelsInNotebook": "Nenhuma etiqueta neste caderno ainda",
"archive": "Arquivo",
"trash": "Lixeira",
"clearFilter": "Remove filter"
"clearFilter": "Remove filter",
"inbox": "Caixa de entrada",
"sharedWithMe": "Compartilhado comigo",
"sortNewest": "O mais novo primeiro",
"sortOldest": "Mais antigo primeiro",
"sortAlpha": "A → Z",
"accountMenu": "Menu da conta",
"profile": "Perfil",
"signOut": "sair",
"sortOrder": "Ordem de classificação",
"freezePinnedNotebook": "Fixar ordem da barra lateral do notebook",
"unfreezePinnedNotebook": "Liberar ordem da barra lateral do notebook",
"newSubNotebook": "Novo sub-notebook",
"renameNotebook": "Renomear"
},
"notes": {
"title": "Notas",
"newNote": "Nova nota",
"reorganize": "Reorganizar notas",
"untitled": "Sem título",
"placeholder": "Faça uma nota...",
"markdownPlaceholder": "Faça uma nota... (Markdown suportado)",
"titlePlaceholder": "Título",
"noteTypes": {
"richtext": "Texto rico",
"markdown": "Remarcação",
"text": "Texto simples",
"checklist": "Lista de verificação"
},
"listItem": "Item da lista",
"addListItem": "+ Item da lista",
"newChecklist": "Nova lista de verificação",
@@ -58,6 +80,7 @@
"confirmDelete": "Tem certeza de que deseja excluir esta nota?",
"confirmLeaveShare": "Tem certeza de que deseja sair desta nota compartilhada?",
"sharedBy": "Compartilhado por",
"sharedShort": "Compartilhado",
"leaveShare": "Sair",
"delete": "Excluir",
"archive": "Arquivar",
@@ -136,6 +159,8 @@
"dragToReorder": "Arraste para reordenar",
"more": "Mais",
"emptyState": "Nenhuma nota aqui",
"metadataPanel": "Detalhes",
"metadataNotebook": "Caderno",
"emptyStateTabs": "Nenhuma nota aqui ainda. Use \"Nova nota\" na barra lateral para adicionar uma (sugestões de título com IA aparecem no compositor).",
"inNotebook": "No caderno",
"moveFailed": "Falha ao mover",
@@ -147,11 +172,6 @@
"unpinned": "Desafixado",
"redoShortcut": "Refazer (Ctrl+Y)",
"undoShortcut": "Desfazer (Ctrl+Z)",
"viewCards": "Visualização em Cartões",
"viewCardsTooltip": "Grade de cartões com reordenação por arrastar e soltar",
"viewTabs": "Visualização em Lista",
"viewTabsTooltip": "Abas no topo, nota abaixo — arraste abas para reordenar",
"viewModeGroup": "Modo de exibição das notas",
"reorderTabs": "Reordenar aba",
"modified": "Modificado",
"created": "Criado",
@@ -160,15 +180,18 @@
"savedStatus": "Salvo",
"dirtyStatus": "Modificado",
"completedLabel": "Concluídos",
"notes.emptyNotebook": "Caderno vazio",
"notes.emptyNotebookDesc": "Este caderno não tem notas. Clique em + para criar uma.",
"notes.noNoteSelected": "Nenhuma nota selecionada",
"notes.selectOrCreateNote": "Selecione uma nota da lista ou crie uma nova.",
"notes": {
"emptyNotebook": "Caderno vazio",
"emptyNotebookDesc": "Este caderno não tem notas. Clique em + para criar uma.",
"noNoteSelected": "Nenhuma nota selecionada",
"selectOrCreateNote": "Selecione uma nota da lista ou crie uma nova."
},
"commitVersion": "Salvar versão",
"versionSaved": "Versão salva",
"deleteVersion": "Excluir esta versão",
"versionDeleted": "Versão excluída",
"deleteVersionConfirm": "Excluir esta versão permanentemente?",
"deleteVersionDesc": "Esta ação não pode ser desfeita. A versão será excluída permanentemente do histórico.",
"historyMode": "Modo de histórico",
"historyModeManual": "Manual (botão commit)",
"historyModeAuto": "Automático (inteligente)",
@@ -184,6 +207,10 @@
"enableHistory": "Ativar histórico",
"historyEmpty": "Nenhuma versão disponível",
"historySelectVersion": "Selecione uma versão para visualizar seu conteúdo",
"currentVersion": "atual",
"compareVersions": "Comparar",
"diffTitle": "Comparação",
"diffSelectHint": "Clique em 2 versões na lista para compará-las",
"sortBy": "Ordenar por",
"sortDateDesc": "Data (recente)",
"sortDateAsc": "Data (antiga)",
@@ -197,10 +224,14 @@
"createFailed": "Failed to create note",
"updateFailed": "Failed to update note",
"archived": "Note archived",
"unarchivedSuccess": "Nota removida do arquivo",
"archiveFailed": "Failed to archive",
"sort": "Sort",
"confirmDeleteTitle": "Delete note",
"leftShare": "Share removed",
"ideaOrigin": "Origin of the idea",
"noNoteLink": "Purely generative idea",
"dismiss": "Not pertinent",
"dismissed": "Note dismissed from recent",
"generalNotes": "General Notes",
"noteType": "Tipo de nota",
@@ -214,7 +245,23 @@
"switchTypeTitle": "Alterar tipo de nota?",
"switchTypeWarning": "Alguma formatação pode ser perdida ao mudar para {type}.",
"switchTypeContentPreserved": "Seu conteúdo será preservado como texto simples.",
"switchType": "Mudar para {type}"
"switchType": "Mudar para {type}",
"saveNow": "Salve agora",
"backToCollection": "Voltar à coleção",
"markdownEditingTitle": "Voltar para a edição",
"markdownPreviewTitle": "Visualização",
"brainstormThisIdea": "Pense nessa ideia",
"brainstormThisIdeaAria": "Pense nessa ideia",
"shareNoteTitle": "Compartilhar nota",
"shareNoteAria": "Compartilhar nota",
"saveNoteAria": "Salvar nota",
"noChangesToSaveAria": "Nenhuma alteração para salvar",
"optionsMenuAria": "Menu de opções",
"deleteNoteConfirmItem": "Excluir nota",
"noteDeletedToast": "Nota excluída.",
"deleteNoteFailedToast": "Não foi possível excluir.",
"documentInfoAria": "Informações do documento",
"noModification": "Sem alterações"
},
"pagination": {
"previous": "←",
@@ -296,7 +343,24 @@
"accessRevoked": "O acesso foi revogado",
"errorLoading": "Erro ao carregar colaboradores",
"failedToAdd": "Falha ao adicionar colaborador",
"failedToRemove": "Falha ao remover colaborador"
"failedToRemove": "Falha ao remover colaborador",
"shareCompactTitle": "Compartilhar",
"inviteByEmailLabel": "Convidar por e-mail",
"accessReadCompact": "Visualizar",
"accessEditCompact": "Editar",
"sendInvitation": "Enviar convite",
"invitationSentBadge": "Convite enviado",
"sharedAccessLabel": "Acesso compartilhado",
"noCollaboratorsEmpty": "Ainda não há colaboradores.",
"removeAccessTitle": "Remover acesso",
"toastInviteSentTo": "Convite enviado para {email}",
"toastAccessRemoved": "Acesso removido para {target}",
"toastUserFallback": "o usuário",
"toastSharingError": "Erro de compartilhamento",
"toastEmailNotFound": "Nenhuma conta encontrada com este e-mail.",
"toastAlreadySharedUser": "Esta nota já foi compartilhada com este usuário.",
"toastRemoveAccessFailed": "Não foi possível remover o acesso.",
"userFallback": "Usuário"
},
"ai": {
"analyzing": "IA analisando...",
@@ -326,6 +390,8 @@
"transforming": "Transformando...",
"transformSuccess": "Texto transformado para Markdown com sucesso!",
"transformError": "Erro durante a transformação",
"convertToRichtext": "Converter para Rich Text",
"convertingToRichtext": "Convertendo...",
"assistant": "Assistente IA",
"generating": "Gerando...",
"generateTitles": "Gerar títulos",
@@ -389,6 +455,8 @@
"undoAI": "Desfazer transformação da IA",
"undoApplied": "Texto original restaurado",
"minWordsError": "A nota deve conter pelo menos 5 palavras para usar ações de IA.",
"wordCountMin": "Selecione pelo menos {min} palavras para reformular (atualmente {current} palavras)",
"wordCountMax": "Selecione no máximo {max} palavras para reformular (atualmente {current} palavras)",
"genericError": "Erro de IA",
"actionError": "Erro durante ação de IA",
"appliedToNote": "Aplicado à nota",
@@ -404,6 +472,15 @@
"chatTab": "Chat",
"noteActions": "Ações da nota",
"askToStart": "Faça uma pergunta ao Assistente para começar.",
"chatPanelContext": "Contexto",
"chatPanelNotebookPlus": "+ Caderno",
"chatPanelWritingTone": "Tom de escrita",
"scopeAutoBadge": "Auto",
"chatNoteQuestionPlaceholder": "Faça uma pergunta sobre esta nota...",
"chatNotebookSelectPlaceholder": "Inclui um caderno...",
"assistantTabActions": "Ações",
"resourcePreviewAiTitle": "Visualização de IA",
"resourcePreviewInjectFromChat": "Injetar do bate-papo",
"contextLabel": "Contexto",
"thisNote": "Esta nota",
"allMyNotes": "Todas as minhas notas",
@@ -415,6 +492,7 @@
"newLineHint": "Shift+Enter = nova linha",
"resultLabel": "Resultado",
"discardAction": "Descartar",
"organization": "Organização",
"transformationsDesc": "Transformações — aplicadas diretamente à nota",
"writeMinWordsAction": "Escreva pelo menos 5 palavras para ativar ações de IA.",
"processingAction": "Processando...",
@@ -425,7 +503,45 @@
"shorten": "Encurtar",
"improve": "Melhorar",
"toMarkdown": "Para Markdown",
"describeImages": "Describe images"
"describeImages": "Describe images",
"fixGrammar": "Corrigir gramática",
"translate": "Traduzir",
"explain": "Explicar",
"toRichText": "Converter para rich text"
},
"generate": {
"slides": "Gerar slides",
"sectionLabel": "Ferramentas de geração",
"theme": "Tema",
"themeArchitecturalMono": "Mono arquitetônico",
"themeVibrantTech": "Tecnologia Vibrante",
"themeMinimalSilk": "Seda Mínima",
"style": "Estilo",
"styleProfessional": "Profissional",
"styleCreative": "Criativo",
"styleBrutalist": "Brutalista",
"diagram": "Gerar Diagrama",
"diagramReadyHint": "Converta notas em fluxo visual",
"diagramType": "Tipo de diagrama",
"typeAuto": "Detecção automática",
"typeFlowchart": "Fluxograma",
"typeMindMap": "Mapa Mental",
"typeTimeline": "Linha do tempo",
"typeOrgChart": "Organograma",
"typeArchitecture": "Arquitetura",
"typeProcessMap": "Mapa de Processo",
"styleSketchy": "Esboçado",
"styleSoft": "Macio",
"styleMinimal": "Mínimo",
"styleDraft": "Rascunho",
"stylePolished": "Polido",
"styleHandwritten": "Manuscrito",
"diagramReady": "O diagrama está pronto!",
"openInExcalidraw": "Abrir no Laboratório Excalidraw",
"insertDiagramInNote": "Incorporar PNG na nota atual",
"diagramImageAlt": "Diagrama gerado por IA",
"insertedInNote": "Diagrama inserido na nota",
"insertExportError": "Erro ao exportar/carregar diagrama"
},
"openAssistant": "Abrir assistente IA",
"poweredByMomento": "Desenvolvido por Momento AI",
@@ -442,7 +558,64 @@
"aiCopilot": "Copiloto IA",
"suggestTitle": "Sugestão de título por IA",
"generateTitleFromImage": "Generate title from image",
"titleGenerated": "Title generated from image"
"titleGenerated": "Title generated from image",
"resourceTab": "Recurso",
"aiNoteTitle": "Nota de IA",
"injectReplace": "Substituir",
"injectReplaceTitle": "Substitua o conteúdo da nota por esta mensagem",
"injectComplete": "Completo",
"injectCompleteTitle": "Complete a nota com esta mensagem (AI)",
"injectMerge": "Mesclar",
"injectMergeTitle": "Mesclar com nota (AI)",
"imagesCount": "{contar} imagens",
"resource": {
"failedToLoadUrl": "Falha ao carregar este URL",
"pageLoaded": "Página carregada: {title}",
"pageLoadError": "Erro ao carregar a página",
"pasteOrUrlFirst": "Cole o texto ou carregue um URL primeiro",
"enrichError": "Erro de enriquecimento",
"enrichErrorShort": "Erro de enriquecimento",
"contentApplied": "Conteúdo aplicado à nota ✓",
"fromChat": "💬 Do bate-papo",
"replacement": "↓ Substituição",
"completedByAI": "✦ Concluído pela IA",
"mergedByAI": "⟳ Fundido por IA",
"rendered": "Renderizado",
"cancel": "Cancelar",
"applyToNote": "Aplicar para nota",
"urlLabel": "URL (opcional)",
"resourceText": "Texto de recurso",
"resourcePlaceholder": "Cole seu texto aqui (markdown, HTML, texto simples…)",
"words": "palavras",
"integrationMode": "Modo de integração",
"modeReplace": "Substituir",
"modeReplaceDesc": "Direto, sem IA",
"modeComplete": "Completo",
"modeCompleteDesc": "Adiciona sem reescrever",
"modeMerge": "Mesclar",
"modeMergeDesc": "Reescreve e integra",
"aiProcessing": "Processamento de IA…",
"preview": "Visualização",
"generatePreview": "Gerar visualização",
"emptyNoteHint": "💡 A nota está vazia — o conteúdo do recurso será integrado diretamente."
},
"cancel": "Cancelar",
"copied": "Copiado",
"copy": "Cópia",
"transformations": "Transformações",
"otherLanguage": "Outro idioma",
"translateNow": "Traduzir agora",
"generationTools": "Ferramentas de geração",
"generateSlidesLoading": "⏳ Gerando apresentação...",
"generateDiagramLoading": "⏳ Gerando diagrama...",
"errorShort": "Erro",
"readyToast": "Preparar!",
"downloadFailedToast": "Falha no download",
"pptxDownloadButton": "Baixar .pptx",
"presentationReadyBadge": "Apresentação pronta",
"openInLabTitle": "Abrir no laboratório",
"inlineSummaryMarkdown": "**Resumo:**",
"networkErrorShort": "Erro de rede."
},
"titleSuggestions": {
"available": "Sugestões de título",
@@ -548,7 +721,19 @@
"untitled": "Sem título",
"notifications": "Notificações",
"declined": "Compartilhamento recusado",
"removed": "Nota removida da lista"
"removed": "Nota removida da lista",
"slidesReady": "Apresentação pronta",
"openSlides": "Apresentação aberta",
"canvasReady": "Diagrama pronto",
"pptxReady": "Slides prontos",
"downloadPptx": "Baixar .pptx",
"markAllRead": "Marcar tudo como lido",
"agentSuccess": "Agente terminou",
"agentFailed": "Falha no agente",
"brainstormInvite": "Brainstorming",
"brainstormJoined": "Brainstorming",
"systemNotification": "Sistema",
"downloadFailed": "Falha no download"
},
"nav": {
"home": "Início",
@@ -597,6 +782,17 @@
"themeLight": "Claro",
"themeDark": "Escuro",
"themeSystem": "Sistema",
"themeBaseGroup": "Base",
"themePalettesGroup": "Color palettes",
"themeSepia": "Sepia",
"themeMidnight": "Midnight",
"themeRose": "Rose",
"themeGreen": "Green",
"themeLavender": "Lavender",
"themeSand": "Sand",
"themeOcean": "Ocean",
"themeSunset": "Sunset",
"themeBlue": "Blue",
"notifications": "Notificações",
"language": "Idioma",
"selectLanguage": "Selecionar idioma",
@@ -630,17 +826,8 @@
"desktopNotifications": "Notificações na área de trabalho",
"desktopNotificationsDesc": "Receba notificações no seu navegador",
"notificationsDesc": "Gerencie suas preferências de notificação",
"themeBaseGroup": "Base",
"themePalettesGroup": "Color palettes",
"themeSepia": "Sepia",
"themeMidnight": "Midnight",
"themeRose": "Rose",
"themeGreen": "Green",
"themeLavender": "Lavender",
"themeSand": "Sand",
"themeOcean": "Ocean",
"themeSunset": "Sunset",
"themeBlue": "Blue"
"autoSave": "Salvar automaticamente",
"autoSaveDesc": "Salvar alterações automaticamente enquanto digita"
},
"profile": {
"title": "Perfil",
@@ -707,7 +894,15 @@
"providerDesc": "Escolha seu provedor de IA preferido",
"providerAutoDesc": "Ollama quando disponível, OpenAI como alternativa",
"providerOllamaDesc": "100% privado, roda localmente na sua máquina",
"providerOpenAIDesc": "Mais preciso, requer chave de API"
"providerOpenAIDesc": "Mais preciso, requer chave de API",
"aiNote": "Nota de IA",
"aiNoteDesc": "Habilite o botão de bate-papo AI e ferramentas de melhoria de texto",
"languageDetection": "Detecção de idioma",
"languageDetectionDesc": "Detecta automaticamente o idioma das suas notas",
"autoLabeling": "Sugestões de rótulos",
"autoLabelingDesc": "Sugere e aplica rótulos automaticamente às suas notas",
"noteHistory": "Histórico de notas",
"noteHistoryDesc": "Habilite snapshots de versão e restauração do histórico"
},
"general": {
"loading": "Carregando...",
@@ -764,7 +959,9 @@
"markDone": "Marcar como concluído",
"markUndone": "Marcar como não concluído",
"todayAt": "Hoje às {time}",
"tomorrowAt": "Amanhã às {time}"
"tomorrowAt": "Amanhã às {time}",
"clearCompleted": "Limpeza concluída",
"viewAll": "Ver todos os lembretes"
},
"notebook": {
"create": "Criar caderno",
@@ -795,7 +992,11 @@
"confidence": "confiança",
"savingReminder": "Falha ao salvar lembrete",
"removingReminder": "Falha ao remover lembrete",
"generatingDescription": "Please wait..."
"generatingDescription": "Please wait...",
"pinnedFrozenTooltip": "Caderno fixado pedido congelado",
"organizeNotebookWithAITooltip": "Organize este notebook com IA",
"assistantRequiredForSummarize": "Ative o AI Assistant nas configurações para resumir",
"createSubnotebook": "Adicionar sub-notebook"
},
"notebookSuggestion": {
"title": "Mover para {name}?",
@@ -808,6 +1009,9 @@
},
"admin": {
"title": "Painel de Administração",
"adminConsole": "Consola de administração",
"navSection": "Navegação",
"backToApp": "De volta à lembrança",
"userManagement": "Gerenciamento de Usuários",
"chat": "Chat IA",
"lab": "O Laboratório",
@@ -850,6 +1054,11 @@
"providerEmbeddingRequired": "AI_PROVIDER_EMBEDDING é obrigatório",
"providerOllamaOption": "🦙 Ollama (Local e Gratuito)",
"providerOpenAIOption": "🤖 OpenAI (GPT-5, GPT-4)",
"providerAnthropicOption": "🧠 Antrópico (Claude API)",
"providerAnthropicCustomOption": "🧩 Personalizado antrópico (API de mensagens - MiniMax, etc.)",
"anthropicModelHint": "Escolha um ID de modelo Claude nas sugestões ou insira um manualmente (não há lista de modelos remotos para a API oficial).",
"anthropicCustomModelHint": "API de mensagens compatíveis com Anthropic (por exemplo, MiniMax): URL base https://api.minimax.io/anthropic (China: https://api.minimaxi.com/anthropic), modelo MiniMax-M2.7. Embeddings: use provedor «Personalizado» + URL OpenAI https://api.minimax.io/v1.",
"anthropicCustomNoModelList": "Este gateway não expõe uma lista /models no estilo OpenAI - escolha o modelo nas sugestões ou digite-o (por exemplo, MiniMax-M2.7).",
"providerCustomOption": "🔧 Compatível com OpenAI (Personalizado)",
"providerDeepSeekOption": "🔍 DeepSeek",
"providerOpenRouterOption": "🌐 OpenRouter",
@@ -1003,7 +1212,14 @@
"error": "Erro:",
"testError": "Erro no Teste: {error}",
"tipTitle": "Dica:",
"tipDescription": "Use o Painel de Testes de IA para diagnosticar problemas de configuração antes de testar."
"tipDescription": "Use o Painel de Testes de IA para diagnosticar problemas de configuração antes de testar.",
"chatTestTitle": "Teste de assistente de bate-papo",
"chatTestDescription": "Teste o provedor de IA usado pelo assistente de chat",
"chatGenerationTest": "💬 Teste de assistente de bate-papo:",
"chatStep1": "Envia uma mensagem de teste para o assistente",
"chatStep2": "Pede uma resposta concisa sobre o que o assistente faz",
"chatStep3": "Mostra a resposta do modelo",
"chatStep4": "Verifica a capacidade de resposta e a latência"
},
"sidebar": {
"dashboard": "Painel",
@@ -1194,6 +1410,7 @@
"notesViewLabel": "Layout das notas",
"notesViewTabs": "Abas (estilo OneNote)",
"notesViewMasonry": "Cartões (grade)",
"notesViewList": "Lista (revista)",
"selectTheme": "Select theme",
"fontFamilyLabel": "Família de fontes",
"fontFamilyDescription": "Escolha a fonte usada em todo o aplicativo",
@@ -1277,6 +1494,69 @@
"organizeWithAI": "Organizar com IA",
"organize": "Organizar"
},
"organizeNotebook": {
"title": "Organizar caderno",
"unknownError": "Erro desconhecido",
"toastSuccess": "Caderno organizado — {criado} subcaderno(s) criado(s), {movido} nota(s) movida(s)",
"intro": "A AI analisará as notas deste caderno e proporá um plano para reorganizá-las em subcadernos temáticos.",
"bulletThemes": "Agrupe notas por tópico ou tema",
"bulletSubfolders": "Crie sub-notebooks ausentes",
"bulletPreview": "Visualização completa antes de qualquer alteração",
"analyzingTitle": "Analisando…",
"analyzingSubtitle": "A IA está lendo suas anotações e identificando temas",
"previewSummary": "{groups} grupo(s) · {notes} notas · {newSubs} novo(s) subcaderno(s)",
"badgeNew": "Novo",
"untitledNote": "Nota sem título",
"notesInGroup": "{contar} notas",
"executingTitle": "Organizando…",
"executingSubtitle": "Criação de subcadernos e notas móveis",
"doneTitle": "Caderno organizado!",
"doneStats": "{criado} subcaderno(s) criado(s) · {movido} nota(s) movida(s)",
"analyzeButton": "Analise com IA",
"restart": "Recomeçar",
"confirm": "Aplicar",
"closeButton": "Fechar"
},
"documentInfo": {
"tabInfo": "Informações",
"tabVersions": "Versões",
"wordsLabel": "Palavras",
"charactersLabel": "Personagens",
"notebookLabel": "Caderno",
"typeLabel": "Tipo",
"createdLabel": "Criado",
"modifiedLabel": "Atualizado",
"labelsSection": "Etiquetas",
"idLabel": "EU IA",
"historyDisabled": "O histórico não está habilitado para esta nota.",
"enableHistory": "Ativar histórico",
"savedVersions": "Versões salvas",
"savingEllipsis": "Salvando…",
"versionSaved": "Versão salva!",
"saveThisVersion": "Salve esta versão",
"loading": "Carregando…",
"noVersion": "Ainda não há versões",
"restoreTooltip": "Restaurar",
"deleteTooltip": "Excluir",
"comparisonMode": "Modo de comparação",
"comparisonSubtitle": "Compare versões lado a lado",
"deleteVersionConfirm": "Excluir esta versão?",
"latestBadge": "Mais recente"
},
"languages": {
"targets": {
"french": "Francês",
"english": "Inglês",
"spanish": "Espanhol",
"german": "Alemão",
"persian": "persa",
"portuguese": "Português",
"italian": "italiano",
"chinese": "chinês",
"japanese": "japonês"
},
"customPlaceholder": "por exemplo Árabe, russo…"
},
"common": {
"unknown": "Desconhecido",
"notAvailable": "Não disponível",
@@ -1398,12 +1678,16 @@
"scraper": "Monitor",
"researcher": "Pesquisador",
"monitor": "Observador",
"slideGenerator": "Apresentações",
"excalidrawGenerator": "Diagrama",
"custom": "Personalizado"
},
"typeDescriptions": {
"scraper": "Extrai conteúdo de vários sites e cria um resumo",
"researcher": "Busca informações sobre um tema",
"monitor": "Observa um caderno e analisa as notas",
"slideGenerator": "Cria uma apresentação do PowerPoint a partir de notas",
"excalidrawGenerator": "Cria um diagrama Excalidraw a partir de notas",
"custom": "Agente livre com seu próprio prompt"
},
"form": {
@@ -1416,6 +1700,27 @@
"urlsOptional": "(opcional)",
"sourceNotebook": "Caderno para observar",
"selectNotebook": "Selecione um caderno...",
"selectNotes": "Notas para analisar",
"notesSelected": "{{count}} notas selecionadas",
"slideTheme": "Tema de apresentação",
"slideThemeDefault": "Automático",
"slideStyle": "Estilo visual",
"slideStyleSoft": "Suave (recomendado)",
"slideStyleSharp": "Afiado e denso",
"slideStyleRounded": "Arredondado e espaçoso",
"slideStylePill": "Prêmio / Pílula",
"excalidrawDiagramType": "Tipo de diagrama",
"excalidrawDiagramTypeAuto": "Automático (detecção de domínio)",
"excalidrawDiagramTypeFlowchart": "Fluxograma (processo)",
"excalidrawDiagramTypeMindmap": "Mapa mental (ideias)",
"excalidrawDiagramTypeOrgChart": "Organograma (equipes)",
"excalidrawDiagramTypeTimeline": "Cronograma / roteiro",
"excalidrawDiagramTypeProcessMap": "Mapa de processos (operações)",
"excalidrawDiagramTypeArchitectureCloud": "Arquitetura de nuvem (zonas/RG)",
"excalidrawDiagramStyle": "Estilo de diagrama Excalidraw",
"excalidrawDiagramStyleDefault": "Colorido (Excalidraw)",
"excalidrawDiagramStyleSketchPlus": "Sketch+ (Excalidraw aprimorado)",
"excalidrawDiagramStyleAustere": "Austero (mínimo)",
"targetNotebook": "Caderno de destino",
"inbox": "Caixa de entrada",
"instructions": "Instruções da IA",
@@ -1485,6 +1790,8 @@
"updated": "Agente atualizado",
"deleted": "\"{name}\" excluído",
"deleteError": "Erro ao excluir",
"running": "Geração em andamento…",
"runningDesc": "A geração pode demorar alguns minutos. Você pode navegar livremente.",
"runSuccess": "\"{name}\" executado com sucesso",
"runError": "Erro: {error}",
"runFailed": "Execução falhou",
@@ -1519,13 +1826,24 @@
"chercheur": {
"name": "Pesquisador de temas",
"description": "Busca informações aprofundadas sobre um tema e cria uma nota estruturada com referências."
},
"slideGenerator": {
"name": "Gerador de slides",
"description": "Lê anotações de um caderno e gera automaticamente uma apresentação estruturada."
},
"excalidrawGenerator": {
"name": "Gerador de Diagrama",
"description": "Lê uma nota e gera um diagrama visual no Excalidraw Lab."
}
},
"runLog": {
"title": "Histórico",
"noHistory": "Nenhuma execução ainda",
"toolTrace": "{count} chamadas de ferramentas",
"step": "Passo {num}"
"step": "Passo {num}",
"clearConfirm": "Tem certeza de que deseja excluir todo o histórico deste agente?",
"cleared": "Histórico excluído",
"clearHistory": "Limpar histórico"
},
"tools": {
"title": "Ferramentas do Agente",
@@ -1536,6 +1854,9 @@
"noteCreate": "Criar Nota",
"urlFetch": "Buscar URL",
"memorySearch": "Memória",
"generatePptx": "Slides PPTX",
"generateSlides": "Apresentações HTML",
"generateExcalidraw": "Diagrama Excalidraw",
"configNeeded": "configuração",
"selected": "{count} selecionado(s)",
"maxSteps": "Máx. iterações"
@@ -1547,7 +1868,9 @@
"scraper": "Você é um assistente de monitoramento. Sintetize artigos de diferentes sites em um resumo claro e estruturado.",
"researcher": "Você é um pesquisador rigoroso. Para o tema solicitado, produza uma nota de pesquisa com contexto, pontos-chave, debates e referências.",
"monitor": "Você é um assistente analítico. Analise as notas fornecidas e sugira pistas, referências e conexões entre as notas.",
"custom": "Você é um assistente útil."
"custom": "Você é um assistente útil.",
"slideGenerator": "Você é um criador de apresentações. Leia o conteúdo fornecido e crie slides estruturados com títulos, pontos-chave e resumos.",
"excalidrawGenerator": "Você é um criador de diagramas. Analise o conteúdo fornecido e crie um diagrama visual claro e organizado."
},
"help": {
"title": "Guia de Agentes",
@@ -1581,7 +1904,10 @@
"frequency": "Com que frequência o agente é executado automaticamente. Comece com Manual para testar.",
"instructions": "Instruções personalizadas que substituem o prompt de IA padrão. Deixe vazio para usar o automático.",
"tools": "Selecione quais ferramentas o agente pode usar. Cada ferramenta dá uma capacidade específica ao agente.",
"maxSteps": "Número máximo de ciclos de raciocínio. Mais passos = análise mais profunda, mas mais lenta."
"maxSteps": "Número máximo de ciclos de raciocínio. Mais passos = análise mais profunda, mas mais lenta.",
"selectNotes": "Selecione notas específicas para analisar. Se nenhuma for selecionada, o agente utilizará todas as notas do caderno.",
"slideTheme": "Escolha uma paleta de cores para a apresentação. Automático permite que a IA decida.",
"slideStyle": "O estilo visual afeta o raio do canto, o espaçamento e a densidade da informação."
}
}
},
@@ -1631,5 +1957,147 @@
"lab": {
"initializing": "Inicializando espaço",
"loadingIdeas": "Carregando suas ideias..."
},
"richTextEditor": {
"slashHint": "↑↓ navegar · Entrar, inserir · Seção de troca de guia",
"slashLoading": "Pensamento de IA...",
"slashTabAll": "Todos",
"slashCatBasic": "Blocos básicos",
"slashCatMedia": "Mídia",
"slashCatFormatting": "Formatação",
"slashCatAi": "Nota de IA",
"insertImage": "Inserir imagem",
"imageUrlPlaceholder": "https://example.com/image.png",
"preview": "Visualização",
"cancel": "Cancelar",
"insert": "Inserir",
"slashText": "Texto",
"slashTextDesc": "Parágrafo simples",
"slashH1": "Título 1",
"slashH1Desc": "Título de seção grande",
"slashH2": "Título 2",
"slashH2Desc": "Título da seção média",
"slashH3": "Título 3",
"slashH3Desc": "Título de seção pequena",
"slashBullet": "Lista com marcadores",
"slashBulletDesc": "Lista não ordenada",
"slashNumbered": "Lista Numerada",
"slashNumberedDesc": "Lista numerada ordenada",
"slashTodo": "Lista de tarefas",
"slashTodoDesc": "Tarefas de caixa de seleção",
"slashQuote": "Citar",
"slashQuoteDesc": "Capture uma cotação",
"slashCode": "Bloco de código",
"slashCodeDesc": "Trecho de código",
"slashDivider": "Divisor",
"slashDividerDesc": "Separador horizontal",
"slashTable": "Mesa",
"slashTableDesc": "Insira uma grade simples",
"slashDiagram": "Diagrama",
"slashDiagramDesc": "Gere um fluxo ou mapa mental",
"slashSlides": "Apresentação",
"slashSlidesDesc": "Gere uma bela apresentação de slides",
"slashImage": "Imagem",
"slashImageDesc": "Incorporar uma imagem do URL",
"slashAlignLeft": "Alinhar à esquerda",
"slashAlignLeftDesc": "Alinhar o texto à esquerda",
"slashAlignCenter": "Centro",
"slashAlignCenterDesc": "Centralize o texto",
"slashAlignRight": "Alinhar à direita",
"slashAlignRightDesc": "Alinhar o texto à direita",
"slashSuperscript": "Sobrescrito",
"slashSuperscriptDesc": "Texto acima da linha de base",
"slashSubscript": "Subscrito",
"slashSubscriptDesc": "Texto abaixo da linha de base",
"slashClarify": "Esclarecer",
"slashClarifyDesc": "Deixe o texto mais claro",
"slashShorten": "Encurtar",
"slashShortenDesc": "Condense o texto",
"slashImprove": "Melhorar",
"slashImproveDesc": "Melhore o estilo",
"slashExpand": "Expandir",
"slashExpandDesc": "Elaborar e enriquecer o texto",
"imageModalTitle": "Inserir imagem",
"imageModalPreview": "Visualização",
"imageModalCancel": "Cancelar",
"imageModalInsert": "Inserir",
"imageModalInvalidUrl": "Insira um URL válido",
"imageModalLoadFailed": "Falha ao carregar imagem",
"linkPlaceholder": "Cole ou digite um link...",
"bold": "Audacioso",
"italic": "itálico",
"underline": "Sublinhado",
"strike": "Tachado",
"code": "Código",
"highlight": "Destaque",
"superscript": "Sobrescrito",
"subscript": "Subscrito",
"addBlock": "Adicionar bloco",
"placeholder": "Digite '/' para comandos..."
},
"brainstorm": {
"title": "Waves of Thought",
"subtitle": "Unfold dimensions of potentiality",
"placeholder": "Enter a concept to unfold...",
"generating": "AI is harvesting seeds of thought...",
"newBrainstorm": "New Brainstorm",
"noSessions": "No brainstorms yet",
"startOne": "Start one",
"sessions": "Brainstorms",
"seedLabel": "Seed Idea",
"ideaPromptDetailed": "Digite sua ideia, pergunta ou tópico para debater...",
"brainstormThisIdea": "Brainstorm this idea",
"startBrainstorm": "Start Brainstorm",
"spatialMode": "Spatial Exploration Mode",
"wave1": "Wave 1",
"wave2": "Wave 2",
"wave3": "Wave 3",
"export": "Export",
"exporting": "Exporting...",
"wave": "Wave",
"novelty": "Novelty",
"originConnection": "Origin connection",
"linkedNotes": "Linked notes",
"deepen": "Deepen",
"deepening": "Generating...",
"extract": "Create Note",
"converting": "Converting...",
"dismiss": "Not pertinent",
"noteCreated": "Note Created",
"ideas": "ideas",
"cancel": "Cancel",
"delete": "Delete",
"ideaOrigin": "Origin of the idea",
"noNoteLink": "Purely generative idea",
"derived_from": "Derived from",
"opposes": "In opposition with",
"extends": "Extends",
"synthesizes": "Synthesizes",
"transposes": "Transposes",
"none_found": "No note link",
"viewNote": "View note",
"addIdea": "Add idea",
"manualIdeaPrompt": "Title of your idea:",
"invite": "Invite",
"linkCopied": "Invite link copied!",
"activityTitle": "Atividade",
"noActivity": "Nenhuma atividade ainda",
"justNow": "agora mesmo",
"humanIdea": "Humano",
"aiIdea": "IA",
"respondsTo": "Responde a",
"adding": "Adicionando...",
"manualIdeaDesc": "Compartilhe sua ideia com a tela de brainstorming",
"manualIdeaTitle": "Título",
"manualIdeaTitlePlaceholder": "Sua ideia em poucas palavras...",
"manualIdeaDescLabel": "Descrição (opcional)",
"manualIdeaDescPlaceholder": "Elabore sua ideia...",
"activity": {
"manual_idea": "adicionei uma ideia",
"wave_generated": "gerou uma onda",
"joined": "entrou na sessão",
"idea_dismissed": "descartou uma ideia",
"invite_created": "criou um convite"
}
}
}
}

View File

@@ -32,6 +32,8 @@
},
"sidebar": {
"notes": "Заметки",
"recent": "Недавний",
"quickNav": "Быстрая навигация",
"reminders": "Напоминания",
"labels": "Метки",
"editLabels": "Редактировать метки",
@@ -40,15 +42,35 @@
"noLabelsInNotebook": "В этом блокноте пока нет меток",
"archive": "Архив",
"trash": "Корзина",
"clearFilter": "Remove filter"
"clearFilter": "Remove filter",
"inbox": "Входящие",
"sharedWithMe": "Поделились со мной",
"sortNewest": "Сначала самые новые",
"sortOldest": "Сначала самый старый",
"sortAlpha": "А → Я",
"accountMenu": "Меню аккаунта",
"profile": "Профиль",
"signOut": "выход",
"sortOrder": "Порядок сортировки",
"freezePinnedNotebook": "Закрепить порядок на боковой панели блокнота",
"unfreezePinnedNotebook": "Открепить порядок боковой панели блокнота",
"newSubNotebook": "Новый субноутбук",
"renameNotebook": "Переименовать"
},
"notes": {
"title": "Заметки",
"newNote": "Новая заметка",
"reorganize": "Реорганизация заметок",
"untitled": "Без названия",
"placeholder": "Сделайте заметку...",
"markdownPlaceholder": "Сделайте заметку... (Поддерживается Markdown)",
"titlePlaceholder": "Заголовок",
"noteTypes": {
"richtext": "Форматированный текст",
"markdown": "Уценка",
"text": "Обычный текст",
"checklist": "Контрольный список"
},
"listItem": "Элемент списка",
"addListItem": "+ Элемент списка",
"newChecklist": "Новый контрольный список",
@@ -58,6 +80,7 @@
"confirmDelete": "Вы уверены, что хотите удалить эту заметку?",
"confirmLeaveShare": "Вы уверены, что хотите покинуть эту общую заметку?",
"sharedBy": "Поделился",
"sharedShort": "Общий",
"leaveShare": "Покинуть",
"delete": "Удалить",
"archive": "Архивировать",
@@ -136,6 +159,8 @@
"dragToReorder": "Перетащите для изменения порядка",
"more": "Ещё",
"emptyState": "Здесь нет заметок",
"metadataPanel": "Подробности",
"metadataNotebook": "Блокнот",
"emptyStateTabs": "Здесь пока нет заметок. Используйте «Новая заметка» на боковой панели, чтобы добавить (предложения заголовков от ИИ появятся в редакторе).",
"inNotebook": "В блокноте",
"moveFailed": "Ошибка перемещения",
@@ -147,11 +172,6 @@
"unpinned": "Откреплённая",
"redoShortcut": "Повторить (Ctrl+Y)",
"undoShortcut": "Отменить (Ctrl+Z)",
"viewCards": "Вид карточек",
"viewCardsTooltip": "Сетка карточек с перетаскиванием для изменения порядка",
"viewTabs": "Список",
"viewTabsTooltip": "Вкладки сверху, заметка снизу — перетаскивайте вкладки для сортировки",
"viewModeGroup": "Режим отображения заметок",
"reorderTabs": "Изменить порядок вкладок",
"modified": "Изменено",
"created": "Создано",
@@ -160,15 +180,18 @@
"savedStatus": "Сохранено",
"dirtyStatus": "Изменено",
"completedLabel": "Завершено",
"notes.emptyNotebook": "Пустой блокнот",
"notes.emptyNotebookDesc": "В этом блокноте нет заметок. Нажмите +, чтобы создать.",
"notes.noNoteSelected": "Заметка не выбрана",
"notes.selectOrCreateNote": "Выберите заметку из списка или создайте новую.",
"notes": {
"emptyNotebook": "Пустой блокнот",
"emptyNotebookDesc": "В этом блокноте нет заметок. Нажмите +, чтобы создать.",
"noNoteSelected": "Заметка не выбрана",
"selectOrCreateNote": "Выберите заметку из списка или создайте новую."
},
"commitVersion": "Сохранить версию",
"versionSaved": "Версия сохранена",
"deleteVersion": "Удалить эту версию",
"versionDeleted": "Версия удалена",
"deleteVersionConfirm": "Удалить эту версию навсегда?",
"deleteVersionDesc": "Это действие невозможно отменить. Версия будет навсегда удалена из истории.",
"historyMode": "Режим истории",
"historyModeManual": "Ручной (кнопка фиксации)",
"historyModeAuto": "Автоматический (умный)",
@@ -184,6 +207,10 @@
"enableHistory": "Включить историю",
"historyEmpty": "Нет доступных версий",
"historySelectVersion": "Выберите версию для предпросмотра",
"currentVersion": "текущий",
"compareVersions": "Сравнивать",
"diffTitle": "Сравнение",
"diffSelectHint": "Нажмите на две версии в списке, чтобы сравнить их.",
"sortBy": "Сортировать по",
"sortDateDesc": "Дата (новые)",
"sortDateAsc": "Дата (старые)",
@@ -197,10 +224,14 @@
"createFailed": "Failed to create note",
"updateFailed": "Failed to update note",
"archived": "Note archived",
"unarchivedSuccess": "Заметка удалена из архива",
"archiveFailed": "Failed to archive",
"sort": "Sort",
"confirmDeleteTitle": "Delete note",
"leftShare": "Share removed",
"ideaOrigin": "Origin of the idea",
"noNoteLink": "Purely generative idea",
"dismiss": "Not pertinent",
"dismissed": "Note dismissed from recent",
"generalNotes": "General Notes",
"noteType": "Тип заметки",
@@ -214,7 +245,23 @@
"switchTypeTitle": "Сменить тип заметки?",
"switchTypeWarning": "Некоторое форматирование может быть потеряно при смене на {type}.",
"switchTypeContentPreserved": "Ваш контент будет сохранён как простой текст.",
"switchType": "Переключить на {type}"
"switchType": "Переключить на {type}",
"saveNow": "Сохранить сейчас",
"backToCollection": "Вернуться в коллекцию",
"markdownEditingTitle": "Вернуться к редактированию",
"markdownPreviewTitle": "Предварительный просмотр",
"brainstormThisIdea": "Продумайте эту идею",
"brainstormThisIdeaAria": "Продумайте эту идею",
"shareNoteTitle": "Поделиться заметкой",
"shareNoteAria": "Поделиться заметкой",
"saveNoteAria": "Сохранить заметку",
"noChangesToSaveAria": "Нет изменений для сохранения",
"optionsMenuAria": "Меню опций",
"deleteNoteConfirmItem": "Удалить заметку",
"noteDeletedToast": "Примечание удалено.",
"deleteNoteFailedToast": "Не удалось удалить.",
"documentInfoAria": "Информация о документе",
"noModification": "Никаких изменений"
},
"pagination": {
"previous": "←",
@@ -296,7 +343,24 @@
"accessRevoked": "Доступ был отозван",
"errorLoading": "Ошибка загрузки соавторов",
"failedToAdd": "Не удалось добавить соавтора",
"failedToRemove": "Не удалось удалить соавтора"
"failedToRemove": "Не удалось удалить соавтора",
"shareCompactTitle": "Делиться",
"inviteByEmailLabel": "Пригласить по электронной почте",
"accessReadCompact": "Вид",
"accessEditCompact": "Редактировать",
"sendInvitation": "Отправить приглашение",
"invitationSentBadge": "Приглашение отправлено",
"sharedAccessLabel": "Общий доступ",
"noCollaboratorsEmpty": "Соавторов пока нет.",
"removeAccessTitle": "Удалить доступ",
"toastInviteSentTo": "Приглашение отправлено на {email}",
"toastAccessRemoved": "Доступ закрыт для пользователя {target}",
"toastUserFallback": "пользователь",
"toastSharingError": "Ошибка доступа",
"toastEmailNotFound": "Аккаунт с этим адресом электронной почты не найден.",
"toastAlreadySharedUser": "Эта заметка уже доступна этому пользователю.",
"toastRemoveAccessFailed": "Не удалось удалить доступ.",
"userFallback": "Пользователь"
},
"ai": {
"analyzing": "ИИ анализирует...",
@@ -326,6 +390,8 @@
"transforming": "Преобразование...",
"transformSuccess": "Текст успешно преобразован в Markdown!",
"transformError": "Ошибка при преобразовании",
"convertToRichtext": "Преобразование в форматированный текст",
"convertingToRichtext": "Преобразование...",
"assistant": "ИИ-ассистент",
"generating": "Генерация...",
"generateTitles": "Сгенерировать заголовки",
@@ -389,6 +455,8 @@
"undoAI": "Отменить преобразование ИИ",
"undoApplied": "Оригинальный текст восстановлен",
"minWordsError": "Заметка должна содержать минимум 5 слов для использования действий ИИ.",
"wordCountMin": "Пожалуйста, выберите как минимум {min} слов для переформулирования (на данный момент {current} слов)",
"wordCountMax": "Пожалуйста, выберите не более {max} слов для переформулирования (на данный момент {current} слов)",
"genericError": "Ошибка ИИ",
"actionError": "Ошибка при выполнении действия ИИ",
"appliedToNote": "Применено к заметке",
@@ -404,6 +472,15 @@
"chatTab": "Чат",
"noteActions": "Действия с заметкой",
"askToStart": "Задайте вопрос ассистенту, чтобы начать.",
"chatPanelContext": "Контекст",
"chatPanelNotebookPlus": "+ Блокнот",
"chatPanelWritingTone": "Тон письма",
"scopeAutoBadge": "Авто",
"chatNoteQuestionPlaceholder": "Задайте вопрос по поводу этой заметки...",
"chatNotebookSelectPlaceholder": "Включите блокнот...",
"assistantTabActions": "Действия",
"resourcePreviewAiTitle": "Предварительный просмотр ИИ",
"resourcePreviewInjectFromChat": "Внедрить из чата",
"contextLabel": "Контекст",
"thisNote": "Эта заметка",
"allMyNotes": "Все мои заметки",
@@ -415,6 +492,7 @@
"newLineHint": "Shift+Enter = новая строка",
"resultLabel": "Результат",
"discardAction": "Отклонить",
"organization": "Организация",
"transformationsDesc": "Преобразования — применяются напрямую к заметке",
"writeMinWordsAction": "Напишите минимум 5 слов для активации действий ИИ.",
"processingAction": "Обработка...",
@@ -425,7 +503,45 @@
"shorten": "Сократить",
"improve": "Улучшить",
"toMarkdown": "В Markdown",
"describeImages": "Describe images"
"describeImages": "Describe images",
"fixGrammar": "Исправить грамматику",
"translate": "Переводить",
"explain": "Объяснять",
"toRichText": "Преобразование в форматированный текст"
},
"generate": {
"slides": "Создание слайдов",
"sectionLabel": "Инструменты генерации",
"theme": "Тема",
"themeArchitecturalMono": "Архитектурное моно",
"themeVibrantTech": "Яркие технологии",
"themeMinimalSilk": "Минимальный шелк",
"style": "Стиль",
"styleProfessional": "Профессиональный",
"styleCreative": "Креатив",
"styleBrutalist": "Бруталист",
"diagram": "Создать диаграмму",
"diagramReadyHint": "Преобразуйте заметку в визуальный поток",
"diagramType": "Тип диаграммы",
"typeAuto": "Автообнаружение",
"typeFlowchart": "Блок-схема",
"typeMindMap": "Карта разума",
"typeTimeline": "Хронология",
"typeOrgChart": "Организационная структура",
"typeArchitecture": "Архитектура",
"typeProcessMap": "Карта процесса",
"styleSketchy": "схематичный",
"styleSoft": "Мягкий",
"styleMinimal": "Минимальный",
"styleDraft": "Черновик",
"stylePolished": "Полированный",
"styleHandwritten": "Рукописный",
"diagramReady": "Схема готова!",
"openInExcalidraw": "Открыть в лаборатории Excalidraw",
"insertDiagramInNote": "Вставить PNG в текущую заметку",
"diagramImageAlt": "Диаграмма, созданная ИИ",
"insertedInNote": "Схема вставлена ​​в примечание",
"insertExportError": "Ошибка экспорта/загрузки диаграммы."
},
"openAssistant": "Открыть ИИ-ассистент",
"poweredByMomento": "На базе Momento AI",
@@ -442,7 +558,64 @@
"aiCopilot": "ИИ-копилот",
"suggestTitle": "Предложение заголовка ИИ",
"generateTitleFromImage": "Generate title from image",
"titleGenerated": "Title generated from image"
"titleGenerated": "Title generated from image",
"resourceTab": "Ресурс",
"aiNoteTitle": "Примечание ИИ",
"injectReplace": "Заменять",
"injectReplaceTitle": "Заменить содержание заметки этим сообщением",
"injectComplete": "Полный",
"injectCompleteTitle": "Полная заметка с этим сообщением (ИИ)",
"injectMerge": "Объединить",
"injectMergeTitle": "Объединить с заметкой (ИИ)",
"imagesCount": "{count} изображений",
"resource": {
"failedToLoadUrl": "Не удалось загрузить этот URL.",
"pageLoaded": "Страница загружена: {title}",
"pageLoadError": "Ошибка загрузки страницы",
"pasteOrUrlFirst": "Вставьте текст или сначала загрузите URL-адрес",
"enrichError": "Ошибка обогащения",
"enrichErrorShort": "Ошибка обогащения",
"contentApplied": "Содержимое применено к заметке ✓",
"fromChat": "💬 Из чата",
"replacement": "↓ Замена",
"completedByAI": "✦ Выполнено ИИ",
"mergedByAI": "⟳ Объединено ИИ",
"rendered": "Рендеринг",
"cancel": "Отмена",
"applyToNote": "Применить к заметке",
"urlLabel": "URL-адрес (необязательно)",
"resourceText": "Текст ресурса",
"resourcePlaceholder": "Вставьте сюда свой текст (уценка, HTML, обычный текст…)",
"words": "слова",
"integrationMode": "Режим интеграции",
"modeReplace": "Заменять",
"modeReplaceDesc": "Прямой, без ИИ",
"modeComplete": "Полный",
"modeCompleteDesc": "Добавляет без переписывания",
"modeMerge": "Объединить",
"modeMergeDesc": "Переписывает и интегрирует",
"aiProcessing": "ИИ-обработка…",
"preview": "Предварительный просмотр",
"generatePreview": "Создать предварительный просмотр",
"emptyNoteHint": "💡 Примечание пустое — содержимое ресурса будет интегрировано напрямую."
},
"cancel": "Отмена",
"copied": "Скопировано",
"copy": "Копировать",
"transformations": "Преобразования",
"otherLanguage": "Другой язык",
"translateNow": "Перевести сейчас",
"generationTools": "Инструменты генерации",
"generateSlidesLoading": "⏳ Создание презентации...",
"generateDiagramLoading": "⏳ Создание диаграммы...",
"errorShort": "Ошибка",
"readyToast": "Готовый!",
"downloadFailedToast": "Загрузка не удалась",
"pptxDownloadButton": "Скачать .pptx",
"presentationReadyBadge": "Презентация готова",
"openInLabTitle": "Открыть в лаборатории",
"inlineSummaryMarkdown": "**Краткое содержание:**",
"networkErrorShort": "Ошибка сети."
},
"titleSuggestions": {
"available": "Предложения заголовков",
@@ -548,7 +721,19 @@
"untitled": "Без названия",
"notifications": "Уведомления",
"declined": "В совместном доступе отказано",
"removed": "Заметка удалена из списка"
"removed": "Заметка удалена из списка",
"slidesReady": "Презентация готова",
"openSlides": "Открытая презентация",
"canvasReady": "Схема готова",
"pptxReady": "Слайды готовы",
"downloadPptx": "Скачать .pptx",
"markAllRead": "Отметить все прочитанными",
"agentSuccess": "Агент закончил",
"agentFailed": "Агент не удалось",
"brainstormInvite": "Мозговой штурм",
"brainstormJoined": "Мозговой штурм",
"systemNotification": "Система",
"downloadFailed": "Загрузка не удалась"
},
"nav": {
"home": "Главная",
@@ -597,6 +782,17 @@
"themeLight": "Светлая",
"themeDark": "Тёмная",
"themeSystem": "Системная",
"themeBaseGroup": "Base",
"themePalettesGroup": "Color palettes",
"themeSepia": "Sepia",
"themeMidnight": "Midnight",
"themeRose": "Rose",
"themeGreen": "Green",
"themeLavender": "Lavender",
"themeSand": "Sand",
"themeOcean": "Ocean",
"themeSunset": "Sunset",
"themeBlue": "Blue",
"notifications": "Уведомления",
"language": "Язык",
"selectLanguage": "Выберите язык",
@@ -630,17 +826,8 @@
"desktopNotifications": "Уведомления на рабочем столе",
"desktopNotificationsDesc": "Получать уведомления в браузере",
"notificationsDesc": "Управление настройками уведомлений",
"themeBaseGroup": "Base",
"themePalettesGroup": "Color palettes",
"themeSepia": "Sepia",
"themeMidnight": "Midnight",
"themeRose": "Rose",
"themeGreen": "Green",
"themeLavender": "Lavender",
"themeSand": "Sand",
"themeOcean": "Ocean",
"themeSunset": "Sunset",
"themeBlue": "Blue"
"autoSave": "Автосохранение",
"autoSaveDesc": "Автоматически сохранять изменения во время ввода"
},
"profile": {
"title": "Профиль",
@@ -707,7 +894,15 @@
"providerDesc": "Выберите предпочитаемого провайдера ИИ",
"providerAutoDesc": "Ollama при наличии, иначе OpenAI",
"providerOllamaDesc": "100% приватно, работает локально на вашем устройстве",
"providerOpenAIDesc": "Наиболее точно, требует API-ключ"
"providerOpenAIDesc": "Наиболее точно, требует API-ключ",
"aiNote": "Примечание ИИ",
"aiNoteDesc": "Включить кнопку чата AI и инструменты улучшения текста",
"languageDetection": "Распознавание языка",
"languageDetectionDesc": "Автоматически определяет язык ваших заметок",
"autoLabeling": "Предложения по ярлыкам",
"autoLabelingDesc": "Автоматически предлагает и применяет ярлыки к вашим заметкам.",
"noteHistory": "История заметок",
"noteHistoryDesc": "Включить снимки версий и восстановление из истории"
},
"general": {
"loading": "Загрузка...",
@@ -764,7 +959,9 @@
"markDone": "Отметить как выполненное",
"markUndone": "Отметить как невыполненное",
"todayAt": "Сегодня в {time}",
"tomorrowAt": "Завтра в {time}"
"tomorrowAt": "Завтра в {time}",
"clearCompleted": "Очистить завершено",
"viewAll": "Просмотреть все напоминания"
},
"notebook": {
"create": "Создать блокнот",
@@ -795,7 +992,11 @@
"confidence": "уверенность",
"savingReminder": "Не удалось сохранить напоминание",
"removingReminder": "Не удалось удалить напоминание",
"generatingDescription": "Please wait..."
"generatingDescription": "Please wait...",
"pinnedFrozenTooltip": "Прикрепленный блокнот — заказ заморожен",
"organizeNotebookWithAITooltip": "Организуйте этот блокнот с помощью ИИ",
"assistantRequiredForSummarize": "Включите AI Assistant в настройках, чтобы подводить итоги.",
"createSubnotebook": "Добавить субноутбук"
},
"notebookSuggestion": {
"title": "Переместить в {name}?",
@@ -808,6 +1009,9 @@
},
"admin": {
"title": "Панель администратора",
"adminConsole": "Консоль администратора",
"navSection": "Навигация",
"backToApp": "Вернуться к Мементо",
"userManagement": "Управление пользователями",
"chat": "ИИ-чат",
"lab": "Лаборатория",
@@ -850,6 +1054,11 @@
"providerEmbeddingRequired": "AI_PROVIDER_EMBEDDING обязателен",
"providerOllamaOption": "🦙 Ollama (Локальный и бесплатный)",
"providerOpenAIOption": "🤖 OpenAI (GPT-5, GPT-4)",
"providerAnthropicOption": "🧠 Антропный (Клод API)",
"providerAnthropicCustomOption": "🧩 Антропный кастом (API сообщений — MiniMax и т. д.)",
"anthropicModelHint": "Выберите идентификатор модели Claude из предложенных или введите его вручную (для официального API нет списка удаленных моделей).",
"anthropicCustomModelHint": "API сообщений, совместимый с Anthropic (например, MiniMax): базовый URL https://api.minimax.io/anthropic (Китай: https://api.minimaxi.com/anthropic), модель MiniMax-M2.7. Встраивания: используйте поставщика «Custom» + URL-адрес OpenAI https://api.minimax.io/v1.",
"anthropicCustomNoModelList": "Этот шлюз не предоставляет список моделей/моделей в стиле OpenAI — выберите модель из предложенных или введите ее (например, MiniMax-M2.7).",
"providerCustomOption": "🔧 Пользовательский (совместимый с OpenAI)",
"providerDeepSeekOption": "🔍 DeepSeek",
"providerOpenRouterOption": "🌐 OpenRouter",
@@ -1003,7 +1212,14 @@
"error": "Ошибка:",
"testError": "Ошибка теста: {error}",
"tipTitle": "Совет:",
"tipDescription": "Используйте панель тестирования ИИ для диагностики проблем конфигурации перед тестированием."
"tipDescription": "Используйте панель тестирования ИИ для диагностики проблем конфигурации перед тестированием.",
"chatTestTitle": "Тест чат-ассистента",
"chatTestDescription": "Проверьте поставщика искусственного интеллекта, используемого чат-помощником.",
"chatGenerationTest": "💬 Тест чат-помощника:",
"chatStep1": "Отправляет тестовое сообщение помощнику",
"chatStep2": "Просит дать краткий ответ о том, чем занимается помощник.",
"chatStep3": "Показывает реакцию модели",
"chatStep4": "Проверяет отзывчивость и задержку"
},
"sidebar": {
"dashboard": "Панель управления",
@@ -1194,6 +1410,7 @@
"notesViewLabel": "Макет заметок",
"notesViewTabs": "Вкладки (в стиле OneNote)",
"notesViewMasonry": "Карточки (сетка)",
"notesViewList": "Список (журнал)",
"selectTheme": "Select theme",
"fontFamilyLabel": "Семейство шрифтов",
"fontFamilyDescription": "Выберите шрифт, используемый во всём приложении",
@@ -1277,6 +1494,69 @@
"organizeWithAI": "Организовать с ИИ",
"organize": "Организовать"
},
"organizeNotebook": {
"title": "Организация блокнота",
"unknownError": "Неизвестная ошибка",
"toastSuccess": "Записная книжка организована — созданы дополнительные записные книжки {created}, перемещены заметки {moved}",
"intro": "ИИ проанализирует записи в этом блокноте и предложит план их реорганизации в тематические субблокноты.",
"bulletThemes": "Группировать заметки по теме или теме",
"bulletSubfolders": "Создать недостающие субноутбуки",
"bulletPreview": "Полный предварительный просмотр перед любыми изменениями",
"analyzingTitle": "Анализ…",
"analyzingSubtitle": "ИИ читает ваши заметки и определяет темы",
"previewSummary": "Группа(ы) {groups} · заметки {notes} · новые субноутбуки {newSubs}",
"badgeNew": "Новый",
"untitledNote": "Заметка без названия",
"notesInGroup": "{count} примечаний",
"executingTitle": "Организация…",
"executingSubtitle": "Создание субноутбуков и перемещение заметок",
"doneTitle": "Блокнот организован!",
"doneStats": "Субноутбуки: {created} созданы · заметки {moved} перемещены",
"analyzeButton": "Анализируйте с помощью ИИ",
"restart": "Начать сначала",
"confirm": "Применять",
"closeButton": "Закрывать"
},
"documentInfo": {
"tabInfo": "Информация",
"tabVersions": "Версии",
"wordsLabel": "Слова",
"charactersLabel": "Персонажи",
"notebookLabel": "Блокнот",
"typeLabel": "Тип",
"createdLabel": "Созданный",
"modifiedLabel": "Обновлено",
"labelsSection": "Этикетки",
"idLabel": "ИДЕНТИФИКАТОР",
"historyDisabled": "История для этой заметки не включена.",
"enableHistory": "Включить историю",
"savedVersions": "Сохраненные версии",
"savingEllipsis": "Сохранение…",
"versionSaved": "Версия сохранена!",
"saveThisVersion": "Сохранить эту версию",
"loading": "Загрузка…",
"noVersion": "Версий пока нет",
"restoreTooltip": "Восстановить",
"deleteTooltip": "Удалить",
"comparisonMode": "Режим сравнения",
"comparisonSubtitle": "Сравните версии рядом",
"deleteVersionConfirm": "Удалить эту версию?",
"latestBadge": "Последний"
},
"languages": {
"targets": {
"french": "Французский",
"english": "Английский",
"spanish": "испанский",
"german": "немецкий",
"persian": "персидский",
"portuguese": "португальский",
"italian": "итальянский",
"chinese": "китайский",
"japanese": "японский"
},
"customPlaceholder": "например арабский, русский…"
},
"common": {
"unknown": "Неизвестно",
"notAvailable": "Недоступно",
@@ -1398,12 +1678,16 @@
"scraper": "Монитор",
"researcher": "Исследователь",
"monitor": "Наблюдатель",
"slideGenerator": "Слайды",
"excalidrawGenerator": "Диаграмма",
"custom": "Пользовательский"
},
"typeDescriptions": {
"scraper": "Собирает данные с нескольких сайтов и создаёт сводку",
"researcher": "Ищет информацию по теме",
"monitor": "Следит за блокнотом и анализирует заметки",
"slideGenerator": "Создает презентацию PowerPoint из заметок.",
"excalidrawGenerator": "Создает диаграмму Excalidraw из заметок.",
"custom": "Свободный агент с вашим промптом"
},
"form": {
@@ -1416,6 +1700,27 @@
"urlsOptional": "(необязательно)",
"sourceNotebook": "Блокнот для наблюдения",
"selectNotebook": "Выберите блокнот...",
"selectNotes": "Примечания для анализа",
"notesSelected": "Выбрано заметок: {{count}}",
"slideTheme": "Тема презентации",
"slideThemeDefault": "Автоматический",
"slideStyle": "Визуальный стиль",
"slideStyleSoft": "Мягкий (рекомендуется)",
"slideStyleSharp": "Острый и плотный",
"slideStyleRounded": "Округлый и просторный",
"slideStylePill": "Премиум / Таблетки",
"excalidrawDiagramType": "Тип диаграммы",
"excalidrawDiagramTypeAuto": "Авто (обнаружение домена)",
"excalidrawDiagramTypeFlowchart": "Блок-схема (процесс)",
"excalidrawDiagramTypeMindmap": "Ментальная карта (идеи)",
"excalidrawDiagramTypeOrgChart": "Организационная структура (команды)",
"excalidrawDiagramTypeTimeline": "График/дорожная карта",
"excalidrawDiagramTypeProcessMap": "Карта процесса (операций)",
"excalidrawDiagramTypeArchitectureCloud": "Облачная архитектура (зоны/RG)",
"excalidrawDiagramStyle": "Стиль диаграммы Excalidraw",
"excalidrawDiagramStyleDefault": "Цветной (Excalidraw)",
"excalidrawDiagramStyleSketchPlus": "Sketch+ (улучшенный Excalidraw)",
"excalidrawDiagramStyleAustere": "Строгий (минимальный)",
"targetNotebook": "Целевой блокнот",
"inbox": "Входящие",
"instructions": "Инструкции для ИИ",
@@ -1485,6 +1790,8 @@
"updated": "Агент обновлён",
"deleted": "\"{name}\" удалён",
"deleteError": "Ошибка удаления",
"running": "Генерация в процессе…",
"runningDesc": "Генерация может занять несколько минут. Вы можете свободно перемещаться.",
"runSuccess": "\"{name}\" успешно выполнен",
"runError": "Ошибка: {error}",
"runFailed": "Выполнение не удалось",
@@ -1519,13 +1826,24 @@
"chercheur": {
"name": "Исследователь темы",
"description": "Ищет подробную информацию по теме и создаёт структурированную заметку с ссылками."
},
"slideGenerator": {
"name": "Генератор слайдов",
"description": "Читает заметки из блокнота и автоматически создает структурированную презентацию."
},
"excalidrawGenerator": {
"name": "Генератор диаграмм",
"description": "Читает заметку и создает визуальную диаграмму в лаборатории Excalidraw."
}
},
"runLog": {
"title": "История",
"noHistory": "Пока нет выполнений",
"toolTrace": "{count} вызовов инструментов",
"step": "Шаг {num}"
"step": "Шаг {num}",
"clearConfirm": "Вы уверены, что хотите удалить всю историю этого агента?",
"cleared": "История удалена",
"clearHistory": "Очистить историю"
},
"tools": {
"title": "Инструменты Агента",
@@ -1536,6 +1854,9 @@
"noteCreate": "Создать Заметку",
"urlFetch": "Получить URL",
"memorySearch": "Память",
"generatePptx": "PPTX-слайды",
"generateSlides": "HTML-слайды",
"generateExcalidraw": "Диаграмма Экскалидрайва",
"configNeeded": "конфигурация",
"selected": "{count} выбрано",
"maxSteps": "Макс. итераций"
@@ -1547,7 +1868,9 @@
"scraper": "Вы — ассистент мониторинга. Обобщите статьи с разных сайтов в ясную, структурированную сводку.",
"researcher": "Вы — тщательный исследователь. По запрошенной теме подготовьте исследовательскую заметку с контекстом, ключевыми моментами, дискуссиями и ссылками.",
"monitor": "Вы — аналитический ассистент. Проанализируйте предоставленные заметки и предложите направления, ссылки и связи между заметками.",
"custom": "Вы — полезный ассистент."
"custom": "Вы — полезный ассистент.",
"slideGenerator": "Вы создатель презентации. Прочитайте предоставленный контент и создайте структурированные слайды с заголовками, ключевыми моментами и резюме.",
"excalidrawGenerator": "Вы создатель диаграмм. Проанализируйте предоставленный контент и создайте четкую, организованную визуальную диаграмму."
},
"help": {
"title": "Руководство по агентам",
@@ -1581,7 +1904,10 @@
"frequency": "Как часто агент запускается автоматически. Начните с Вручную для тестирования.",
"instructions": "Пользовательские инструкции, заменяющие стандартный ИИ-промпт. Оставьте пустым для автоматического.",
"tools": "Выберите, какие инструменты может использовать агент. Каждый инструмент даёт агенту определённую способность.",
"maxSteps": "Максимальное количество циклов рассуждений. Больше шагов = более глубокий анализ, но дольше."
"maxSteps": "Максимальное количество циклов рассуждений. Больше шагов = более глубокий анализ, но дольше.",
"selectNotes": "Выберите конкретные заметки для анализа. Если ничего не выбрано, агент будет использовать все заметки из блокнота.",
"slideTheme": "Выберите цветовую палитру для презентации. Автоматически позволяет ИИ решать.",
"slideStyle": "Визуальный стиль влияет на радиус угла, расстояние и плотность информации."
}
}
},
@@ -1631,5 +1957,147 @@
"lab": {
"initializing": "Инициализация пространства",
"loadingIdeas": "Загрузка ваших идей..."
},
"richTextEditor": {
"slashHint": "↑↓ навигация · Ввод вставки · Раздел переключения вкладок",
"slashLoading": "ИИ думает...",
"slashTabAll": "Все",
"slashCatBasic": "Базовые блоки",
"slashCatMedia": "СМИ",
"slashCatFormatting": "Форматирование",
"slashCatAi": "Примечание ИИ",
"insertImage": "Вставить изображение",
"imageUrlPlaceholder": "https://example.com/image.png",
"preview": "Предварительный просмотр",
"cancel": "Отмена",
"insert": "Вставлять",
"slashText": "Текст",
"slashTextDesc": "Простой абзац",
"slashH1": "Заголовок 1",
"slashH1Desc": "Большой заголовок раздела",
"slashH2": "Заголовок 2",
"slashH2Desc": "Средний заголовок раздела",
"slashH3": "Заголовок 3",
"slashH3Desc": "Небольшой заголовок раздела",
"slashBullet": "Маркированный список",
"slashBulletDesc": "Неупорядоченный список",
"slashNumbered": "Нумерованный список",
"slashNumberedDesc": "Упорядоченный нумерованный список",
"slashTodo": "Список задач",
"slashTodoDesc": "Задачи с флажками",
"slashQuote": "Цитировать",
"slashQuoteDesc": "Запишите цитату",
"slashCode": "Кодовый блок",
"slashCodeDesc": "Фрагмент кода",
"slashDivider": "Разделитель",
"slashDividerDesc": "Горизонтальный сепаратор",
"slashTable": "Стол",
"slashTableDesc": "Вставка простой сетки",
"slashDiagram": "Диаграмма",
"slashDiagramDesc": "Создайте поток или карту мыслей",
"slashSlides": "Презентация",
"slashSlidesDesc": "Создайте красивую презентацию слайдов",
"slashImage": "Изображение",
"slashImageDesc": "Вставить изображение из URL",
"slashAlignLeft": "Выровнять по левому краю",
"slashAlignLeftDesc": "Выровнять текст по левому краю",
"slashAlignCenter": "Центр",
"slashAlignCenterDesc": "Центрировать текст",
"slashAlignRight": "Выровнять по правому краю",
"slashAlignRightDesc": "Выровнять текст по правому краю",
"slashSuperscript": "Надстрочный индекс",
"slashSuperscriptDesc": "Текст над базовой линией",
"slashSubscript": "Индекс",
"slashSubscriptDesc": "Текст под базовой линией",
"slashClarify": "Объяснить",
"slashClarifyDesc": "Сделайте текст более понятным",
"slashShorten": "Сократить",
"slashShortenDesc": "Сжать текст",
"slashImprove": "Улучшать",
"slashImproveDesc": "Улучшите стиль",
"slashExpand": "Расширять",
"slashExpandDesc": "Проработать и обогатить текст",
"imageModalTitle": "Вставить изображение",
"imageModalPreview": "Предварительный просмотр",
"imageModalCancel": "Отмена",
"imageModalInsert": "Вставлять",
"imageModalInvalidUrl": "Пожалуйста, введите действительный URL-адрес",
"imageModalLoadFailed": "Не удалось загрузить изображение",
"linkPlaceholder": "Вставьте или введите ссылку...",
"bold": "Смелый",
"italic": "Курсив",
"underline": "Подчеркнуть",
"strike": "Зачеркивание",
"code": "Код",
"highlight": "Выделять",
"superscript": "Надстрочный индекс",
"subscript": "Индекс",
"addBlock": "Добавить блок",
"placeholder": "Введите '/' для команд..."
},
"brainstorm": {
"title": "Waves of Thought",
"subtitle": "Unfold dimensions of potentiality",
"placeholder": "Enter a concept to unfold...",
"generating": "AI is harvesting seeds of thought...",
"newBrainstorm": "New Brainstorm",
"noSessions": "No brainstorms yet",
"startOne": "Start one",
"sessions": "Brainstorms",
"seedLabel": "Seed Idea",
"ideaPromptDetailed": "Введите свою идею, вопрос или тему для мозгового штурма...",
"brainstormThisIdea": "Brainstorm this idea",
"startBrainstorm": "Start Brainstorm",
"spatialMode": "Spatial Exploration Mode",
"wave1": "Wave 1",
"wave2": "Wave 2",
"wave3": "Wave 3",
"export": "Export",
"exporting": "Exporting...",
"wave": "Wave",
"novelty": "Novelty",
"originConnection": "Origin connection",
"linkedNotes": "Linked notes",
"deepen": "Deepen",
"deepening": "Generating...",
"extract": "Create Note",
"converting": "Converting...",
"dismiss": "Not pertinent",
"noteCreated": "Note Created",
"ideas": "ideas",
"cancel": "Cancel",
"delete": "Delete",
"ideaOrigin": "Origin of the idea",
"noNoteLink": "Purely generative idea",
"derived_from": "Derived from",
"opposes": "In opposition with",
"extends": "Extends",
"synthesizes": "Synthesizes",
"transposes": "Transposes",
"none_found": "No note link",
"viewNote": "View note",
"addIdea": "Add idea",
"manualIdeaPrompt": "Title of your idea:",
"invite": "Invite",
"linkCopied": "Invite link copied!",
"activityTitle": "Активность",
"noActivity": "Пока нет активности",
"justNow": "прямо сейчас",
"humanIdea": "Человек",
"aiIdea": "ИИ",
"respondsTo": "Отвечает на",
"adding": "Добавление...",
"manualIdeaDesc": "Поделитесь своей идеей с помощью холста мозгового штурма",
"manualIdeaTitle": "Заголовок",
"manualIdeaTitlePlaceholder": "Ваша идея в нескольких словах...",
"manualIdeaDescLabel": "Описание (необязательно)",
"manualIdeaDescPlaceholder": "Разработайте свою идею...",
"activity": {
"manual_idea": "добавил идею",
"wave_generated": "создал волну",
"joined": "присоединился к сессии",
"idea_dismissed": "отказался от идеи",
"invite_created": "создал приглашение"
}
}
}
}

View File

@@ -32,6 +32,8 @@
},
"sidebar": {
"notes": "笔记",
"recent": "最近的",
"quickNav": "快速导航",
"reminders": "提醒",
"labels": "标签",
"editLabels": "编辑标签",
@@ -40,15 +42,35 @@
"noLabelsInNotebook": "此笔记本中暂无标签",
"archive": "归档",
"trash": "回收站",
"clearFilter": "Remove filter"
"clearFilter": "Remove filter",
"inbox": "收件箱",
"sharedWithMe": "与我分享",
"sortNewest": "最新的优先",
"sortOldest": "最老的在前",
"sortAlpha": "A → Z",
"accountMenu": "账户菜单",
"profile": "轮廓",
"signOut": "登出",
"sortOrder": "排序顺序",
"freezePinnedNotebook": "固定笔记本侧边栏顺序",
"unfreezePinnedNotebook": "取消固定笔记本侧边栏顺序",
"newSubNotebook": "新亚笔记本",
"renameNotebook": "重命名"
},
"notes": {
"title": "笔记",
"newNote": "新建笔记",
"reorganize": "重新整理笔记",
"untitled": "无标题",
"placeholder": "记笔记...",
"markdownPlaceholder": "记笔记...(支持 Markdown",
"titlePlaceholder": "标题",
"noteTypes": {
"richtext": "富文本",
"markdown": "降价",
"text": "纯文本",
"checklist": "清单"
},
"listItem": "列表项",
"addListItem": "+ 列表项",
"newChecklist": "新建清单",
@@ -58,6 +80,7 @@
"confirmDelete": "确定要删除这条笔记吗?",
"confirmLeaveShare": "确定要离开这条共享笔记吗?",
"sharedBy": "共享者",
"sharedShort": "共享",
"leaveShare": "离开",
"delete": "删除",
"archive": "归档",
@@ -136,6 +159,8 @@
"dragToReorder": "拖动以重新排序",
"more": "更多",
"emptyState": "暂无笔记",
"metadataPanel": "细节",
"metadataNotebook": "笔记本",
"emptyStateTabs": "这里还没有笔记。使用侧边栏中的「新建笔记」来添加AI 标题建议会出现在编辑器中)。",
"inNotebook": "在笔记本中",
"moveFailed": "移动失败",
@@ -147,11 +172,6 @@
"unpinned": "未置顶",
"redoShortcut": "重做 (Ctrl+Y)",
"undoShortcut": "撤销 (Ctrl+Z)",
"viewCards": "卡片视图",
"viewCardsTooltip": "卡片网格,支持拖拽排序",
"viewTabs": "列表视图",
"viewTabsTooltip": "上方为标签页,下方为笔记 — 拖拽标签页可排序",
"viewModeGroup": "笔记显示模式",
"reorderTabs": "重新排序标签页",
"modified": "已修改",
"created": "已创建",
@@ -160,15 +180,18 @@
"savedStatus": "已保存",
"dirtyStatus": "已修改",
"completedLabel": "已完成",
"notes.emptyNotebook": "空笔记本",
"notes.emptyNotebookDesc": "笔记本没有笔记。点击 + 创建一个。",
"notes.noNoteSelected": "未选择笔记",
"notes.selectOrCreateNote": "从列表中选择笔记或创建新笔记。",
"notes": {
"emptyNotebook": "笔记本",
"emptyNotebookDesc": "此笔记本没有笔记。点击 + 创建一个。",
"noNoteSelected": "未选择笔记",
"selectOrCreateNote": "从列表中选择笔记或创建新笔记。"
},
"commitVersion": "保存版本",
"versionSaved": "版本已保存",
"deleteVersion": "删除此版本",
"versionDeleted": "版本已删除",
"deleteVersionConfirm": "确定永久删除此版本?",
"deleteVersionDesc": "此操作无法撤消。该版本将从历史记录中永久删除。",
"historyMode": "历史模式",
"historyModeManual": "手动(提交按钮)",
"historyModeAuto": "自动(智能)",
@@ -184,6 +207,10 @@
"enableHistory": "启用历史",
"historyEmpty": "暂无版本",
"historySelectVersion": "选择一个版本以预览其内容",
"currentVersion": "当前的",
"compareVersions": "比较",
"diffTitle": "比较",
"diffSelectHint": "单击列表中的 2 个版本进行比较",
"sortBy": "排序方式",
"sortDateDesc": "日期(最新)",
"sortDateAsc": "日期(最早)",
@@ -197,10 +224,14 @@
"createFailed": "Failed to create note",
"updateFailed": "Failed to update note",
"archived": "Note archived",
"unarchivedSuccess": "注释已从存档中删除",
"archiveFailed": "Failed to archive",
"sort": "Sort",
"confirmDeleteTitle": "Delete note",
"leftShare": "Share removed",
"ideaOrigin": "Origin of the idea",
"noNoteLink": "Purely generative idea",
"dismiss": "Not pertinent",
"dismissed": "Note dismissed from recent",
"generalNotes": "General Notes",
"noteType": "笔记类型",
@@ -214,7 +245,23 @@
"switchTypeTitle": "切换笔记类型?",
"switchTypeWarning": "切换到 {type} 可能会丢失部分格式。",
"switchTypeContentPreserved": "您的内容将保留为纯文本。",
"switchType": "切换到 {type}"
"switchType": "切换到 {type}",
"saveNow": "立即保存",
"backToCollection": "返回收藏",
"markdownEditingTitle": "返回编辑",
"markdownPreviewTitle": "预览",
"brainstormThisIdea": "集思广益这个想法",
"brainstormThisIdeaAria": "集思广益这个想法",
"shareNoteTitle": "分享笔记",
"shareNoteAria": "分享笔记",
"saveNoteAria": "保存备注",
"noChangesToSaveAria": "没有要保存的更改",
"optionsMenuAria": "选项菜单",
"deleteNoteConfirmItem": "删除注释",
"noteDeletedToast": "注释已删除。",
"deleteNoteFailedToast": "无法删除。",
"documentInfoAria": "文件信息",
"noModification": "没有变化"
},
"pagination": {
"previous": "←",
@@ -296,7 +343,24 @@
"accessRevoked": "访问权限已被撤销",
"errorLoading": "加载协作者时出错",
"failedToAdd": "添加协作者失败",
"failedToRemove": "移除协作者失败"
"failedToRemove": "移除协作者失败",
"shareCompactTitle": "分享",
"inviteByEmailLabel": "通过电子邮件邀请",
"accessReadCompact": "看法",
"accessEditCompact": "编辑",
"sendInvitation": "发送邀请",
"invitationSentBadge": "邀请已发送",
"sharedAccessLabel": "共享访问",
"noCollaboratorsEmpty": "还没有合作者。",
"removeAccessTitle": "删除访问权限",
"toastInviteSentTo": "邀请已发送至 {email}",
"toastAccessRemoved": "已移除 {target} 的访问权限",
"toastUserFallback": "用户",
"toastSharingError": "分享错误",
"toastEmailNotFound": "未找到此电子邮件的帐户。",
"toastAlreadySharedUser": "此注释已与该用户共享。",
"toastRemoveAccessFailed": "无法删除访问权限。",
"userFallback": "用户"
},
"ai": {
"analyzing": "AI 分析中...",
@@ -326,6 +390,8 @@
"transforming": "转换中...",
"transformSuccess": "文本已成功转换为 Markdown",
"transformError": "转换时出错",
"convertToRichtext": "转换为富文本",
"convertingToRichtext": "转换...",
"assistant": "AI 助手",
"generating": "生成中...",
"generateTitles": "生成标题",
@@ -389,6 +455,8 @@
"undoAI": "撤销 AI 转换",
"undoApplied": "已恢复原始文本",
"minWordsError": "笔记必须至少包含5个字才能使用AI操作。",
"wordCountMin": "请至少选择 {min} 个单词进行重新表述(当前为 {current} 个单词)",
"wordCountMax": "请最多选择 {max} 个单词进行重新表述(当前为 {current} 个单词)",
"genericError": "AI错误",
"actionError": "AI操作期间出错",
"appliedToNote": "已应用到笔记",
@@ -404,6 +472,15 @@
"chatTab": "聊天",
"noteActions": "笔记操作",
"askToStart": "向助手提问以开始。",
"chatPanelContext": "语境",
"chatPanelNotebookPlus": "+ 笔记本",
"chatPanelWritingTone": "书写语气",
"scopeAutoBadge": "汽车",
"chatNoteQuestionPlaceholder": "询问有关此注释的问题...",
"chatNotebookSelectPlaceholder": "附上笔记本...",
"assistantTabActions": "行动",
"resourcePreviewAiTitle": "人工智能预览",
"resourcePreviewInjectFromChat": "从聊天中注入",
"contextLabel": "上下文",
"thisNote": "此笔记",
"allMyNotes": "所有笔记",
@@ -415,6 +492,7 @@
"newLineHint": "Shift+Enter = 换行",
"resultLabel": "结果",
"discardAction": "丢弃",
"organization": "组织",
"transformationsDesc": "转换 — 直接应用到笔记",
"writeMinWordsAction": "至少写5个字以激活AI操作。",
"processingAction": "处理中...",
@@ -425,7 +503,45 @@
"shorten": "缩短",
"improve": "改进",
"toMarkdown": "转为Markdown",
"describeImages": "Describe images"
"describeImages": "Describe images",
"fixGrammar": "修复语法",
"translate": "翻译",
"explain": "解释",
"toRichText": "转换为富文本"
},
"generate": {
"slides": "生成幻灯片",
"sectionLabel": "生成工具",
"theme": "主题",
"themeArchitecturalMono": "建筑单声道",
"themeVibrantTech": "活力科技",
"themeMinimalSilk": "最小丝绸",
"style": "风格",
"styleProfessional": "专业的",
"styleCreative": "有创造力的",
"styleBrutalist": "野兽派",
"diagram": "生成图表",
"diagramReadyHint": "将笔记转化为视觉流",
"diagramType": "图表类型",
"typeAuto": "自动检测",
"typeFlowchart": "流程图",
"typeMindMap": "思维导图",
"typeTimeline": "时间轴",
"typeOrgChart": "组织结构图",
"typeArchitecture": "建筑学",
"typeProcessMap": "流程图",
"styleSketchy": "粗略",
"styleSoft": "柔软的",
"styleMinimal": "最小",
"styleDraft": "草稿",
"stylePolished": "抛光",
"styleHandwritten": "手写",
"diagramReady": "图表准备好了!",
"openInExcalidraw": "在 Excalidraw 实验室中打开",
"insertDiagramInNote": "在当前笔记中嵌入 PNG",
"diagramImageAlt": "人工智能生成图表",
"insertedInNote": "注释中插入图表",
"insertExportError": "导出/上传图表时出错"
},
"openAssistant": "打开AI助手",
"poweredByMomento": "由 Momento AI 提供支持",
@@ -442,7 +558,64 @@
"aiCopilot": "AI副驾驶",
"suggestTitle": "AI标题建议",
"generateTitleFromImage": "Generate title from image",
"titleGenerated": "Title generated from image"
"titleGenerated": "Title generated from image",
"resourceTab": "资源",
"aiNoteTitle": "人工智能笔记",
"injectReplace": "代替",
"injectReplaceTitle": "将注释内容替换为此消息",
"injectComplete": "完全的",
"injectCompleteTitle": "包含此消息的完整注释 (AI)",
"injectMerge": "合并",
"injectMergeTitle": "与注释合并AI",
"imagesCount": "{count} 张图片",
"resource": {
"failedToLoadUrl": "无法加载此网址",
"pageLoaded": "页面已加载:{标题}",
"pageLoadError": "加载页面时出错",
"pasteOrUrlFirst": "首先粘贴文本或加载 URL",
"enrichError": "富集错误",
"enrichErrorShort": "富集错误",
"contentApplied": "适用于注释的内容 ✓",
"fromChat": "💬 来自聊天",
"replacement": "↓ 更换",
"completedByAI": "✦ 由AI完成",
"mergedByAI": "⟳ AI 合并",
"rendered": "渲染的",
"cancel": "取消",
"applyToNote": "申请备注",
"urlLabel": "网址(可选)",
"resourceText": "资源文本",
"resourcePlaceholder": "将您的文本粘贴到此处markdown、HTML、纯文本...",
"words": "字",
"integrationMode": "整合模式",
"modeReplace": "代替",
"modeReplaceDesc": "直接,无人工智能",
"modeComplete": "完全的",
"modeCompleteDesc": "添加而不重写",
"modeMerge": "合并",
"modeMergeDesc": "重写并集成",
"aiProcessing": "人工智能处理...",
"preview": "预览",
"generatePreview": "生成预览",
"emptyNoteHint": "💡 备注为空——资源内容将直接整合。"
},
"cancel": "取消",
"copied": "已复制",
"copy": "复制",
"transformations": "转换",
"otherLanguage": "另一种语言",
"translateNow": "立即翻译",
"generationTools": "生成工具",
"generateSlidesLoading": "⏳ 正在生成演示文稿...",
"generateDiagramLoading": "⏳ 生成图表...",
"errorShort": "错误",
"readyToast": "准备好!",
"downloadFailedToast": "下载失败",
"pptxDownloadButton": "下载.pptx",
"presentationReadyBadge": "演示准备就绪",
"openInLabTitle": "在实验室中打开",
"inlineSummaryMarkdown": "**概括:**",
"networkErrorShort": "网络错误。"
},
"titleSuggestions": {
"available": "标题建议",
@@ -548,7 +721,19 @@
"untitled": "无标题",
"notifications": "通知",
"declined": "分享已拒绝",
"removed": "笔记已从列表中移除"
"removed": "笔记已从列表中移除",
"slidesReady": "演示准备就绪",
"openSlides": "开放演示",
"canvasReady": "图表准备好",
"pptxReady": "幻灯片准备好",
"downloadPptx": "下载.pptx",
"markAllRead": "标记全部已读",
"agentSuccess": "代理完毕",
"agentFailed": "代理失败",
"brainstormInvite": "头脑风暴",
"brainstormJoined": "头脑风暴",
"systemNotification": "系统",
"downloadFailed": "下载失败"
},
"nav": {
"home": "主页",
@@ -597,6 +782,17 @@
"themeLight": "浅色",
"themeDark": "深色",
"themeSystem": "跟随系统",
"themeBaseGroup": "Base",
"themePalettesGroup": "Color palettes",
"themeSepia": "Sepia",
"themeMidnight": "Midnight",
"themeRose": "Rose",
"themeGreen": "Green",
"themeLavender": "Lavender",
"themeSand": "Sand",
"themeOcean": "Ocean",
"themeSunset": "Sunset",
"themeBlue": "Blue",
"notifications": "通知",
"language": "语言",
"selectLanguage": "选择语言",
@@ -630,17 +826,8 @@
"desktopNotifications": "桌面通知",
"desktopNotificationsDesc": "在浏览器中接收通知",
"notificationsDesc": "管理您的通知偏好",
"themeBaseGroup": "Base",
"themePalettesGroup": "Color palettes",
"themeSepia": "Sepia",
"themeMidnight": "Midnight",
"themeRose": "Rose",
"themeGreen": "Green",
"themeLavender": "Lavender",
"themeSand": "Sand",
"themeOcean": "Ocean",
"themeSunset": "Sunset",
"themeBlue": "Blue"
"autoSave": "自动保存",
"autoSaveDesc": "键入时自动保存更改"
},
"profile": {
"title": "个人资料",
@@ -707,7 +894,15 @@
"providerDesc": "选择您偏好的 AI 提供商",
"providerAutoDesc": "优先使用 Ollama备用 OpenAI",
"providerOllamaDesc": "100% 私有,在本地运行",
"providerOpenAIDesc": "最准确,需要 API 密钥"
"providerOpenAIDesc": "最准确,需要 API 密钥",
"aiNote": "人工智能笔记",
"aiNoteDesc": "启用AI聊天按钮和文本改进工具",
"languageDetection": "语言检测",
"languageDetectionDesc": "自动检测笔记的语言",
"autoLabeling": "标签建议",
"autoLabelingDesc": "自动建议标签并将其应用到您的笔记",
"noteHistory": "注释历史记录",
"noteHistoryDesc": "启用版本快照和从历史记录恢复"
},
"general": {
"loading": "加载中...",
@@ -764,7 +959,9 @@
"markDone": "标记为已完成",
"markUndone": "标记为未完成",
"todayAt": "今天 {time}",
"tomorrowAt": "明天 {time}"
"tomorrowAt": "明天 {time}",
"clearCompleted": "清除完成",
"viewAll": "查看所有提醒"
},
"notebook": {
"create": "创建笔记本",
@@ -795,7 +992,11 @@
"confidence": "置信度",
"savingReminder": "保存提醒失败",
"removingReminder": "移除提醒失败",
"generatingDescription": "Please wait..."
"generatingDescription": "Please wait...",
"pinnedFrozenTooltip": "固定笔记本 — 订单冻结",
"organizeNotebookWithAITooltip": "用人工智能整理这个笔记本",
"assistantRequiredForSummarize": "设置中开启AI助手进行总结",
"createSubnotebook": "添加子笔记本"
},
"notebookSuggestion": {
"title": "移动到 {name}",
@@ -808,6 +1009,9 @@
},
"admin": {
"title": "管理后台",
"adminConsole": "管理控制台",
"navSection": "导航",
"backToApp": "返回《记忆碎片》",
"userManagement": "用户管理",
"chat": "AI 聊天",
"lab": "实验室",
@@ -850,6 +1054,11 @@
"providerEmbeddingRequired": "AI_PROVIDER_EMBEDDING 是必需的",
"providerOllamaOption": "🦙 Ollama (Local & Free)",
"providerOpenAIOption": "🤖 OpenAI (GPT-5, GPT-4)",
"providerAnthropicOption": "🧠 人类(克劳德 API",
"providerAnthropicCustomOption": "🧩 人为定制(消息 API — MiniMax 等)",
"anthropicModelHint": "从建议中选择一个 Claude 模型 ID 或手动输入一个(官方 API 没有远程模型列表)。",
"anthropicCustomModelHint": "Anthropic 兼容的消息 API例如 MiniMax基本 URL https://api.minimax.io/anthropic中国https://api.minimaxi.com/anthropic型号 MiniMax-M2.7。嵌入:使用提供商“自定义”+ OpenAI URL https://api.minimax.io/v1。",
"anthropicCustomNoModelList": "该网关不公开 OpenAI 风格的/模型列表 - 从建议中选择模型或输入模型(例如 MiniMax-M2.7)。",
"providerCustomOption": "🔧 Custom OpenAI-Compatible",
"providerDeepSeekOption": "🔍 DeepSeek",
"providerOpenRouterOption": "🌐 OpenRouter",
@@ -1003,7 +1212,14 @@
"error": "错误:",
"testError": "测试错误:{error}",
"tipTitle": "提示:",
"tipDescription": "在测试之前使用 AI 测试面板诊断配置问题。"
"tipDescription": "在测试之前使用 AI 测试面板诊断配置问题。",
"chatTestTitle": "聊天助手测试",
"chatTestDescription": "测试聊天助手使用的AI提供程序",
"chatGenerationTest": "💬 聊天助手测试:",
"chatStep1": "向助手发送测试消息",
"chatStep2": "要求简要回答助理的职责",
"chatStep3": "显示模型响应",
"chatStep4": "检查响应能力和延迟"
},
"sidebar": {
"dashboard": "仪表盘",
@@ -1194,6 +1410,7 @@
"notesViewLabel": "笔记布局",
"notesViewTabs": "标签页OneNote 风格)",
"notesViewMasonry": "卡片(网格)",
"notesViewList": "列表(杂志)",
"selectTheme": "Select theme",
"fontFamilyLabel": "字体系列",
"fontFamilyDescription": "选择应用程序中使用的字体",
@@ -1277,6 +1494,69 @@
"organizeWithAI": "用 AI 整理",
"organize": "整理"
},
"organizeNotebook": {
"title": "整理笔记本",
"unknownError": "未知错误",
"toastSuccess": "笔记本已整理 — 创建了 {created} 个子笔记本,已移动了 {moved} 个笔记",
"intro": "AI会分析这个笔记本中的笔记并提出将它们重新组织成主题子笔记本的计划。",
"bulletThemes": "按主题或主题对笔记进行分组",
"bulletSubfolders": "创建丢失的子笔记本",
"bulletPreview": "进行任何更改之前的完整预览",
"analyzingTitle": "正在分析……",
"analyzingSubtitle": "人工智能正在阅读你的笔记并识别主题",
"previewSummary": "{groups} 组 · {notes} 笔记 · {newSubs} 新子笔记本",
"badgeNew": "新的",
"untitledNote": "无标题笔记",
"notesInGroup": "{count} 条笔记",
"executingTitle": "组织…",
"executingSubtitle": "创建子笔记本和移动笔记",
"doneTitle": "笔记本整理好了!",
"doneStats": "{created} 子笔记本已创建 · {moved} 条笔记已移动",
"analyzeButton": "人工智能分析",
"restart": "重新开始",
"confirm": "申请",
"closeButton": "关闭"
},
"documentInfo": {
"tabInfo": "信息",
"tabVersions": "版本",
"wordsLabel": "字",
"charactersLabel": "人物",
"notebookLabel": "笔记本",
"typeLabel": "类型",
"createdLabel": "已创建",
"modifiedLabel": "已更新",
"labelsSection": "标签",
"idLabel": "ID",
"historyDisabled": "此笔记未启用历史记录。",
"enableHistory": "启用历史记录",
"savedVersions": "保存的版本",
"savingEllipsis": "保存…",
"versionSaved": "版本已保存!",
"saveThisVersion": "保存此版本",
"loading": "加载中…",
"noVersion": "还没有版本",
"restoreTooltip": "恢复",
"deleteTooltip": "删除",
"comparisonMode": "比较模式",
"comparisonSubtitle": "并排比较版本",
"deleteVersionConfirm": "删除这个版本?",
"latestBadge": "最新的"
},
"languages": {
"targets": {
"french": "法语",
"english": "英语",
"spanish": "西班牙语",
"german": "德语",
"persian": "波斯语",
"portuguese": "葡萄牙语",
"italian": "意大利语",
"chinese": "中国人",
"japanese": "日本人"
},
"customPlaceholder": "例如阿拉伯语、俄语……"
},
"common": {
"unknown": "未知",
"notAvailable": "不可用",
@@ -1398,12 +1678,16 @@
"scraper": "监控器",
"researcher": "研究员",
"monitor": "观察者",
"slideGenerator": "幻灯片",
"excalidrawGenerator": "图表",
"custom": "自定义"
},
"typeDescriptions": {
"scraper": "抓取多个网站并创建摘要",
"researcher": "搜索有关主题的信息",
"monitor": "监视笔记本并分析笔记",
"slideGenerator": "根据笔记创建 PowerPoint 演示文稿",
"excalidrawGenerator": "从笔记创建 Excalidraw 图表",
"custom": "使用自定义提示的自由代理"
},
"form": {
@@ -1416,6 +1700,27 @@
"urlsOptional": "(可选)",
"sourceNotebook": "要监视的笔记本",
"selectNotebook": "选择笔记本...",
"selectNotes": "分析注意事项",
"notesSelected": "已选择 {{count}} 条注释",
"slideTheme": "演讲主题",
"slideThemeDefault": "自动的",
"slideStyle": "视觉风格",
"slideStyleSoft": "软(推荐)",
"slideStyleSharp": "锐利而浓密",
"slideStyleRounded": "圆润宽敞",
"slideStylePill": "高级/药丸",
"excalidrawDiagramType": "图表类型",
"excalidrawDiagramTypeAuto": "自动(域检测)",
"excalidrawDiagramTypeFlowchart": "流程图(过程)",
"excalidrawDiagramTypeMindmap": "思维导图(想法)",
"excalidrawDiagramTypeOrgChart": "组织结构图(团队)",
"excalidrawDiagramTypeTimeline": "时间表/路线图",
"excalidrawDiagramTypeProcessMap": "流程图(操作)",
"excalidrawDiagramTypeArchitectureCloud": "云架构(区域/RG",
"excalidrawDiagramStyle": "Excalidraw 图表样式",
"excalidrawDiagramStyleDefault": "彩色Excalidraw",
"excalidrawDiagramStyleSketchPlus": "Sketch+(增强型 Excalidraw",
"excalidrawDiagramStyleAustere": "简朴(最小)",
"targetNotebook": "目标笔记本",
"inbox": "收件箱",
"instructions": "AI 指令",
@@ -1485,6 +1790,8 @@
"updated": "代理已更新",
"deleted": "\"{name}\" 已删除",
"deleteError": "删除时出错",
"running": "一代正在进行中……",
"runningDesc": "生成可能需要几分钟。您可以自由导航。",
"runSuccess": "\"{name}\" 执行成功",
"runError": "错误:{error}",
"runFailed": "执行失败",
@@ -1519,13 +1826,24 @@
"chercheur": {
"name": "主题研究员",
"description": "搜索有关主题的深入信息并创建带有参考的结构化笔记。"
},
"slideGenerator": {
"name": "幻灯片生成器",
"description": "从笔记本中读取笔记并自动生成结构化演示文稿。"
},
"excalidrawGenerator": {
"name": "图表生成器",
"description": "阅读笔记并在 Excalidraw 实验室中生成可视化图表。"
}
},
"runLog": {
"title": "历史记录",
"noHistory": "暂无执行记录",
"toolTrace": "{count} 次工具调用",
"step": "第 {num} 步"
"step": "第 {num} 步",
"clearConfirm": "您确定要删除该代理的所有历史记录吗?",
"cleared": "历史记录已删除",
"clearHistory": "清除历史记录"
},
"tools": {
"title": "代理工具",
@@ -1536,6 +1854,9 @@
"noteCreate": "创建笔记",
"urlFetch": "获取 URL",
"memorySearch": "记忆",
"generatePptx": "PPTX 幻灯片",
"generateSlides": "HTML 幻灯片",
"generateExcalidraw": "Excalidraw 图表",
"configNeeded": "配置",
"selected": "{count} 个已选择",
"maxSteps": "最大迭代次数"
@@ -1547,7 +1868,9 @@
"scraper": "您是一个监控助手。将不同网站的文章综合成清晰、结构化的摘要。",
"researcher": "您是一位严谨的研究员。针对请求的主题,制作包含背景、要点、争议和参考文献的研究笔记。",
"monitor": "您是一位分析助手。分析提供的笔记并建议线索、参考和笔记之间的联系。",
"custom": "您是一位有帮助的助手。"
"custom": "您是一位有帮助的助手。",
"slideGenerator": "您是演示文稿创建者。阅读提供的内容并创建包含标题、要点和摘要的结构化幻灯片。",
"excalidrawGenerator": "您是一名图表创建者。分析所提供的内容并创建清晰、有组织的可视化图表。"
},
"help": {
"title": "代理指南",
@@ -1581,7 +1904,10 @@
"frequency": "代理自动运行的频率。从手动开始测试。",
"instructions": "替换默认AI提示的自定义指令。留空则使用自动提示。",
"tools": "选择代理可以使用的工具。每个工具赋予代理特定能力。",
"maxSteps": "最大推理循环数。更多步骤 = 更深入的分析,但耗时更长。"
"maxSteps": "最大推理循环数。更多步骤 = 更深入的分析,但耗时更长。",
"selectNotes": "选择具体的笔记进行分析。如果未选择任何内容,代理将使用笔记本中的所有笔记。",
"slideTheme": "选择演示文稿的调色板。自动让人工智能决定。",
"slideStyle": "视觉风格影响角半径、间距和信息密度。"
}
}
},
@@ -1631,5 +1957,147 @@
"lab": {
"initializing": "初始化工作区",
"loadingIdeas": "加载你的想法..."
},
"richTextEditor": {
"slashHint": "↑↓导航·进入插入·Tab切换部分",
"slashLoading": "人工智能思维...",
"slashTabAll": "全部",
"slashCatBasic": "基本块",
"slashCatMedia": "媒体",
"slashCatFormatting": "格式化",
"slashCatAi": "人工智能笔记",
"insertImage": "插入图片",
"imageUrlPlaceholder": "https://example.com/image.png",
"preview": "预览",
"cancel": "取消",
"insert": "插入",
"slashText": "文本",
"slashTextDesc": "简单段落",
"slashH1": "标题 1",
"slashH1Desc": "大节标题",
"slashH2": "标题 2",
"slashH2Desc": "中节标题",
"slashH3": "标题 3",
"slashH3Desc": "小节标题",
"slashBullet": "项目符号列表",
"slashBulletDesc": "无序列表",
"slashNumbered": "编号列表",
"slashNumberedDesc": "有序编号列表",
"slashTodo": "任务清单",
"slashTodoDesc": "复选框任务",
"slashQuote": "引用",
"slashQuoteDesc": "捕获报价",
"slashCode": "代码块",
"slashCodeDesc": "代码片段",
"slashDivider": "分频器",
"slashDividerDesc": "卧式分离机",
"slashTable": "桌子",
"slashTableDesc": "插入一个简单的网格",
"slashDiagram": "图表",
"slashDiagramDesc": "生成流程或思维导图",
"slashSlides": "推介会",
"slashSlidesDesc": "生成漂亮的幻灯片",
"slashImage": "图像",
"slashImageDesc": "嵌入来自 URL 的图像",
"slashAlignLeft": "左对齐",
"slashAlignLeftDesc": "将文本左对齐",
"slashAlignCenter": "中心",
"slashAlignCenterDesc": "将文本居中",
"slashAlignRight": "右对齐",
"slashAlignRightDesc": "将文本右对齐",
"slashSuperscript": "上标",
"slashSuperscriptDesc": "文本高于基线",
"slashSubscript": "下标",
"slashSubscriptDesc": "文本低于基线",
"slashClarify": "阐明",
"slashClarifyDesc": "让文字更清晰",
"slashShorten": "缩短",
"slashShortenDesc": "压缩文本",
"slashImprove": "提升",
"slashImproveDesc": "提升风格",
"slashExpand": "扩张",
"slashExpandDesc": "阐述并丰富文本",
"imageModalTitle": "插入图片",
"imageModalPreview": "预览",
"imageModalCancel": "取消",
"imageModalInsert": "插入",
"imageModalInvalidUrl": "请输入有效的网址",
"imageModalLoadFailed": "加载图像失败",
"linkPlaceholder": "粘贴或输入链接...",
"bold": "大胆的",
"italic": "斜体",
"underline": "强调",
"strike": "删除线",
"code": "代码",
"highlight": "强调",
"superscript": "上标",
"subscript": "下标",
"addBlock": "添加区块",
"placeholder": "输入“/”作为命令..."
},
"brainstorm": {
"title": "Waves of Thought",
"subtitle": "Unfold dimensions of potentiality",
"placeholder": "Enter a concept to unfold...",
"generating": "AI is harvesting seeds of thought...",
"newBrainstorm": "New Brainstorm",
"noSessions": "No brainstorms yet",
"startOne": "Start one",
"sessions": "Brainstorms",
"seedLabel": "Seed Idea",
"ideaPromptDetailed": "输入您的想法、问题或主题进行头脑风暴...",
"brainstormThisIdea": "Brainstorm this idea",
"startBrainstorm": "Start Brainstorm",
"spatialMode": "Spatial Exploration Mode",
"wave1": "Wave 1",
"wave2": "Wave 2",
"wave3": "Wave 3",
"export": "Export",
"exporting": "Exporting...",
"wave": "Wave",
"novelty": "Novelty",
"originConnection": "Origin connection",
"linkedNotes": "Linked notes",
"deepen": "Deepen",
"deepening": "Generating...",
"extract": "Create Note",
"converting": "Converting...",
"dismiss": "Not pertinent",
"noteCreated": "Note Created",
"ideas": "ideas",
"cancel": "Cancel",
"delete": "Delete",
"ideaOrigin": "Origin of the idea",
"noNoteLink": "Purely generative idea",
"derived_from": "Derived from",
"opposes": "In opposition with",
"extends": "Extends",
"synthesizes": "Synthesizes",
"transposes": "Transposes",
"none_found": "No note link",
"viewNote": "View note",
"addIdea": "Add idea",
"manualIdeaPrompt": "Title of your idea:",
"invite": "Invite",
"linkCopied": "Invite link copied!",
"activityTitle": "活动",
"noActivity": "还没有活动",
"justNow": "现在",
"humanIdea": "人类",
"aiIdea": "人工智能",
"respondsTo": "回应",
"adding": "添加...",
"manualIdeaDesc": "通过头脑风暴画布分享您的想法",
"manualIdeaTitle": "标题",
"manualIdeaTitlePlaceholder": "用几句话来表达你的想法...",
"manualIdeaDescLabel": "描述(可选)",
"manualIdeaDescPlaceholder": "详细说明你的想法...",
"activity": {
"manual_idea": "添加了一个想法",
"wave_generated": "产生了波浪",
"joined": "加入会议",
"idea_dismissed": "驳回了一个想法",
"invite_created": "创建了邀请"
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More