chore: clean up repo for public release
- Remove BMAD framework, IDE configs, dev screenshots, test files, internal docs, and backup files - Rename keep-notes/ to memento-note/ - Update all references from keep-notes to memento-note - Add Apache 2.0 license with Commons Clause (non-commercial restriction) - Add clean .gitignore and .env.docker.example
This commit is contained in:
122
memento-note/app/api/ai/auto-labels/route.ts
Normal file
122
memento-note/app/api/ai/auto-labels/route.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { autoLabelCreationService } from '@/lib/ai/services'
|
||||
|
||||
/**
|
||||
* POST /api/ai/auto-labels - Suggest new labels for a notebook
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const { notebookId, language = 'en' } = body
|
||||
|
||||
if (!notebookId || typeof notebookId !== 'string') {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Missing required field: notebookId' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Check if notebook belongs to user
|
||||
const { prisma } = await import('@/lib/prisma')
|
||||
const notebook = await prisma.notebook.findFirst({
|
||||
where: {
|
||||
id: notebookId,
|
||||
userId: session.user.id,
|
||||
},
|
||||
})
|
||||
|
||||
if (!notebook) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Notebook not found' },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
// Get label suggestions
|
||||
const suggestions = await autoLabelCreationService.suggestLabels(
|
||||
notebookId,
|
||||
session.user.id,
|
||||
language
|
||||
)
|
||||
|
||||
if (!suggestions) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: null,
|
||||
message: 'No suggestions available (notebook may have fewer than 15 notes)',
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: suggestions,
|
||||
})
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to get label suggestions',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT /api/ai/auto-labels - Create suggested labels
|
||||
*/
|
||||
export async function PUT(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const { suggestions, selectedLabels } = body
|
||||
|
||||
if (!suggestions || !Array.isArray(selectedLabels)) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Missing required fields: suggestions, selectedLabels' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Create labels
|
||||
const createdCount = await autoLabelCreationService.createLabels(
|
||||
suggestions.notebookId,
|
||||
session.user.id,
|
||||
suggestions,
|
||||
selectedLabels
|
||||
)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
createdCount,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to create labels',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
102
memento-note/app/api/ai/batch-organize/route.ts
Normal file
102
memento-note/app/api/ai/batch-organize/route.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { batchOrganizationService } from '@/lib/ai/services'
|
||||
|
||||
/**
|
||||
* POST /api/ai/batch-organize - Create organization plan for notes in Inbox
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
// Get language from request headers or body
|
||||
let language = 'en'
|
||||
try {
|
||||
const body = await request.json()
|
||||
if (body.language) {
|
||||
language = body.language
|
||||
}
|
||||
} catch (e) {
|
||||
// If no body or invalid json, check headers
|
||||
const acceptLanguage = request.headers.get('accept-language')
|
||||
if (acceptLanguage) {
|
||||
language = acceptLanguage.split(',')[0].split('-')[0]
|
||||
}
|
||||
}
|
||||
|
||||
// Create organization plan
|
||||
const plan = await batchOrganizationService.createOrganizationPlan(
|
||||
session.user.id,
|
||||
language
|
||||
)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: plan,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('[batch-organize POST] Error:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to create organization plan',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT /api/ai/batch-organize - Apply organization plan
|
||||
*/
|
||||
export async function PUT(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const { plan, selectedNoteIds } = body
|
||||
|
||||
if (!plan || !Array.isArray(selectedNoteIds)) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Missing required fields: plan, selectedNoteIds' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Apply organization plan
|
||||
const movedCount = await batchOrganizationService.applyOrganizationPlan(
|
||||
session.user.id,
|
||||
plan,
|
||||
selectedNoteIds
|
||||
)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
movedCount,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to apply organization plan',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
26
memento-note/app/api/ai/config/route.ts
Normal file
26
memento-note/app/api/ai/config/route.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getSystemConfig } from '@/lib/config'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const config = await getSystemConfig()
|
||||
|
||||
return NextResponse.json({
|
||||
AI_PROVIDER_TAGS: config.AI_PROVIDER_TAGS || 'not set',
|
||||
AI_MODEL_TAGS: config.AI_MODEL_TAGS || 'not set',
|
||||
AI_PROVIDER_EMBEDDING: config.AI_PROVIDER_EMBEDDING || 'not set',
|
||||
AI_MODEL_EMBEDDING: config.AI_MODEL_EMBEDDING || 'not set',
|
||||
OPENAI_API_KEY: config.OPENAI_API_KEY ? '***configured***' : '',
|
||||
CUSTOM_OPENAI_API_KEY: config.CUSTOM_OPENAI_API_KEY ? '***configured***' : '',
|
||||
CUSTOM_OPENAI_BASE_URL: config.CUSTOM_OPENAI_BASE_URL || '',
|
||||
OLLAMA_BASE_URL: config.OLLAMA_BASE_URL || 'not set'
|
||||
})
|
||||
} catch (error: any) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: error.message || 'Failed to fetch config'
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
85
memento-note/app/api/ai/echo/connections/route.ts
Normal file
85
memento-note/app/api/ai/echo/connections/route.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { memoryEchoService } from '@/lib/ai/services/memory-echo.service'
|
||||
|
||||
/**
|
||||
* GET /api/ai/echo/connections?noteId={id}&page={page}&limit={limit}
|
||||
* Fetch all connections for a specific note
|
||||
*/
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
// Get query parameters
|
||||
const { searchParams } = new URL(req.url)
|
||||
const noteId = searchParams.get('noteId')
|
||||
const page = parseInt(searchParams.get('page') || '1')
|
||||
const limit = parseInt(searchParams.get('limit') || '10')
|
||||
|
||||
// Validate noteId
|
||||
if (!noteId) {
|
||||
return NextResponse.json(
|
||||
{ error: 'noteId parameter is required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Validate pagination parameters
|
||||
if (page < 1 || limit < 1 || limit > 50) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid pagination parameters. page >= 1, limit between 1 and 50' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Get all connections for the note
|
||||
const allConnections = await memoryEchoService.getConnectionsForNote(noteId, session.user.id)
|
||||
|
||||
// Calculate pagination
|
||||
const total = allConnections.length
|
||||
const startIndex = (page - 1) * limit
|
||||
const endIndex = startIndex + limit
|
||||
const paginatedConnections = allConnections.slice(startIndex, endIndex)
|
||||
|
||||
// Format connections for response
|
||||
const connections = paginatedConnections.map(conn => {
|
||||
// Determine which note is the "other" note (not the target note)
|
||||
const isNote1Target = conn.note1.id === noteId
|
||||
const otherNote = isNote1Target ? conn.note2 : conn.note1
|
||||
|
||||
return {
|
||||
noteId: otherNote.id,
|
||||
title: otherNote.title,
|
||||
content: otherNote.content,
|
||||
createdAt: otherNote.createdAt,
|
||||
similarity: conn.similarityScore,
|
||||
daysApart: conn.daysApart
|
||||
}
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
connections,
|
||||
pagination: {
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
hasNext: endIndex < total,
|
||||
hasPrev: page > 1
|
||||
}
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch connections' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
60
memento-note/app/api/ai/echo/dismiss/route.ts
Normal file
60
memento-note/app/api/ai/echo/dismiss/route.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
/**
|
||||
* POST /api/ai/echo/dismiss
|
||||
* Dismiss a connection for a specific note
|
||||
* Body: { noteId, connectedNoteId }
|
||||
*/
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const body = await req.json()
|
||||
const { noteId, connectedNoteId } = body
|
||||
|
||||
if (!noteId || !connectedNoteId) {
|
||||
return NextResponse.json(
|
||||
{ error: 'noteId and connectedNoteId are required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Find and mark matching insights as dismissed
|
||||
// We need to find insights where (note1Id = noteId AND note2Id = connectedNoteId) OR (note1Id = connectedNoteId AND note2Id = noteId)
|
||||
await prisma.memoryEchoInsight.updateMany({
|
||||
where: {
|
||||
userId: session.user.id,
|
||||
OR: [
|
||||
{
|
||||
note1Id: noteId,
|
||||
note2Id: connectedNoteId
|
||||
},
|
||||
{
|
||||
note1Id: connectedNoteId,
|
||||
note2Id: noteId
|
||||
}
|
||||
]
|
||||
},
|
||||
data: {
|
||||
dismissed: true
|
||||
}
|
||||
})
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to dismiss connection' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
108
memento-note/app/api/ai/echo/fusion/route.ts
Normal file
108
memento-note/app/api/ai/echo/fusion/route.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { getChatProvider } from '@/lib/ai/factory'
|
||||
import { getSystemConfig } from '@/lib/config'
|
||||
import prisma from '@/lib/prisma'
|
||||
|
||||
/**
|
||||
* POST /api/ai/echo/fusion
|
||||
* Generate intelligent fusion of multiple notes
|
||||
*/
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const body = await req.json()
|
||||
const { noteIds, prompt } = body
|
||||
|
||||
if (!noteIds || !Array.isArray(noteIds) || noteIds.length < 2) {
|
||||
return NextResponse.json(
|
||||
{ error: 'At least 2 note IDs are required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Fetch the notes
|
||||
const notes = await prisma.note.findMany({
|
||||
where: {
|
||||
id: { in: noteIds },
|
||||
userId: session.user.id
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
content: true,
|
||||
createdAt: true
|
||||
}
|
||||
})
|
||||
|
||||
if (notes.length !== noteIds.length) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Some notes not found or access denied' },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
// Get AI provider
|
||||
const config = await getSystemConfig()
|
||||
const provider = getChatProvider(config)
|
||||
|
||||
// Build fusion prompt
|
||||
const notesDescriptions = notes.map((note, index) => {
|
||||
return `Note ${index + 1}: "${note.title || 'Untitled'}"
|
||||
${note.content}`
|
||||
}).join('\n\n')
|
||||
|
||||
const fusionPrompt = `You are an expert at synthesizing and merging information from multiple sources.
|
||||
|
||||
TASK: Create a unified, well-structured note by intelligently combining the following notes.
|
||||
|
||||
${prompt ? `ADDITIONAL INSTRUCTIONS: ${prompt}\n` : ''}
|
||||
|
||||
NOTES TO MERGE:
|
||||
${notesDescriptions}
|
||||
|
||||
REQUIREMENTS:
|
||||
1. Create a clear, descriptive title that captures the essence of all notes
|
||||
2. Merge and consolidate related information
|
||||
3. Remove duplicates while preserving unique details from each note
|
||||
4. Organize the content logically (use headers, bullet points, etc.)
|
||||
5. Maintain the important details and context from all notes
|
||||
6. Keep the tone and style consistent
|
||||
7. Use markdown formatting for better readability
|
||||
|
||||
Output format:
|
||||
# [Fused Title]
|
||||
|
||||
[Merged and organized content...]
|
||||
|
||||
Begin:`
|
||||
|
||||
try {
|
||||
const fusedContent = await provider.generateText(fusionPrompt)
|
||||
|
||||
return NextResponse.json({
|
||||
fusedNote: fusedContent,
|
||||
notesCount: notes.length
|
||||
})
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to generate fusion' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to process fusion request' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
92
memento-note/app/api/ai/echo/route.ts
Normal file
92
memento-note/app/api/ai/echo/route.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { memoryEchoService } from '@/lib/ai/services/memory-echo.service'
|
||||
|
||||
/**
|
||||
* GET /api/ai/echo
|
||||
* Fetch next Memory Echo insight for current user
|
||||
*/
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
// Get next insight (respects frequency limits)
|
||||
const insight = await memoryEchoService.getNextInsight(session.user.id)
|
||||
|
||||
if (!insight) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
insight: null,
|
||||
message: 'No new insights available at the moment. Memory Echo will notify you when we discover connections between your notes.'
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json({ insight })
|
||||
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch Memory Echo insight' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/ai/echo
|
||||
* Submit feedback or mark as viewed
|
||||
*/
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const body = await req.json()
|
||||
const { action, insightId, feedback } = body
|
||||
|
||||
if (action === 'view') {
|
||||
// Mark insight as viewed
|
||||
await memoryEchoService.markAsViewed(insightId)
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
|
||||
} else if (action === 'feedback') {
|
||||
// Submit feedback (thumbs_up or thumbs_down)
|
||||
if (!feedback || !['thumbs_up', 'thumbs_down'].includes(feedback)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid feedback. Must be thumbs_up or thumbs_down' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
await memoryEchoService.submitFeedback(insightId, feedback)
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
|
||||
} else {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid action. Must be "view" or "feedback"' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to process request' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
96
memento-note/app/api/ai/models/route.ts
Normal file
96
memento-note/app/api/ai/models/route.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getSystemConfig } from '@/lib/config'
|
||||
|
||||
// Modèles populaires pour chaque provider (2025)
|
||||
const PROVIDER_MODELS = {
|
||||
ollama: {
|
||||
tags: [
|
||||
'llama3:latest',
|
||||
'llama3.2:latest',
|
||||
'granite4:latest',
|
||||
'mistral:latest',
|
||||
'mixtral:latest',
|
||||
'phi3:latest',
|
||||
'gemma2:latest',
|
||||
'qwen2:latest'
|
||||
],
|
||||
embeddings: [
|
||||
'embeddinggemma:latest',
|
||||
'mxbai-embed-large:latest',
|
||||
'nomic-embed-text:latest'
|
||||
]
|
||||
},
|
||||
openai: {
|
||||
tags: [
|
||||
'gpt-4o',
|
||||
'gpt-4o-mini',
|
||||
'gpt-4-turbo',
|
||||
'gpt-4',
|
||||
'gpt-3.5-turbo'
|
||||
],
|
||||
embeddings: [
|
||||
'text-embedding-3-small',
|
||||
'text-embedding-3-large',
|
||||
'text-embedding-ada-002'
|
||||
]
|
||||
},
|
||||
custom: {
|
||||
tags: [], // Will be loaded dynamically
|
||||
embeddings: [] // Will be loaded dynamically
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const config = await getSystemConfig()
|
||||
const provider = (config.AI_PROVIDER || 'ollama').toLowerCase()
|
||||
|
||||
let models = PROVIDER_MODELS[provider as keyof typeof PROVIDER_MODELS] || { tags: [], embeddings: [] }
|
||||
|
||||
// Pour Ollama, essayer de récupérer la liste réelle depuis l'API locale
|
||||
if (provider === 'ollama') {
|
||||
try {
|
||||
const ollamaBaseUrl = config.OLLAMA_BASE_URL || process.env.OLLAMA_BASE_URL || 'http://localhost:11434'
|
||||
const response = await fetch(`${ollamaBaseUrl}/api/tags`, {
|
||||
method: 'GET',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
const allModels = data.models || []
|
||||
|
||||
// Séparer les modèles de tags et d'embeddings
|
||||
const tagModels = allModels
|
||||
.filter((m: any) => !m.name.includes('embed') && !m.name.includes('Embedding'))
|
||||
.map((m: any) => m.name)
|
||||
.slice(0, 20) // Limiter à 20 modèles
|
||||
|
||||
const embeddingModels = allModels
|
||||
.filter((m: any) => m.name.includes('embed') || m.name.includes('Embedding'))
|
||||
.map((m: any) => m.name)
|
||||
|
||||
models = {
|
||||
tags: tagModels.length > 0 ? tagModels : models.tags,
|
||||
embeddings: embeddingModels.length > 0 ? embeddingModels : models.embeddings
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Garder les modèles par défaut
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
provider,
|
||||
models: models || { tags: [], embeddings: [] }
|
||||
})
|
||||
} catch (error: any) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: error.message || 'Failed to fetch models',
|
||||
models: { tags: [], embeddings: [] }
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
73
memento-note/app/api/ai/notebook-summary/route.ts
Normal file
73
memento-note/app/api/ai/notebook-summary/route.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { notebookSummaryService } from '@/lib/ai/services'
|
||||
|
||||
/**
|
||||
* POST /api/ai/notebook-summary - Generate summary for a notebook
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const { notebookId, language = 'en' } = body
|
||||
|
||||
if (!notebookId || typeof notebookId !== 'string') {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Missing required field: notebookId' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Check if notebook belongs to user
|
||||
const { prisma } = await import('@/lib/prisma')
|
||||
const notebook = await prisma.notebook.findFirst({
|
||||
where: {
|
||||
id: notebookId,
|
||||
userId: session.user.id,
|
||||
},
|
||||
})
|
||||
|
||||
if (!notebook) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Notebook not found' },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
// Generate summary
|
||||
const summary = await notebookSummaryService.generateSummary(
|
||||
notebookId,
|
||||
session.user.id,
|
||||
language
|
||||
)
|
||||
|
||||
if (!summary) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: null,
|
||||
message: 'No summary available (notebook may be empty)',
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: summary,
|
||||
})
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to generate notebook summary',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
57
memento-note/app/api/ai/reformulate/route.ts
Normal file
57
memento-note/app/api/ai/reformulate/route.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { paragraphRefactorService } from '@/lib/ai/services/paragraph-refactor.service'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { text, option } = await request.json()
|
||||
|
||||
// Validation
|
||||
if (!text || typeof text !== 'string') {
|
||||
return NextResponse.json({ error: 'Text is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Map option to refactor mode
|
||||
const modeMap: Record<string, 'clarify' | 'shorten' | 'improveStyle'> = {
|
||||
'clarify': 'clarify',
|
||||
'shorten': 'shorten',
|
||||
'improve': 'improveStyle'
|
||||
}
|
||||
|
||||
const mode = modeMap[option]
|
||||
if (!mode) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid option. Use: clarify, shorten, or improve' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Validate word count
|
||||
const validation = paragraphRefactorService.validateWordCount(text)
|
||||
if (!validation.valid) {
|
||||
return NextResponse.json({ error: validation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
// Use the ParagraphRefactorService
|
||||
const result = await paragraphRefactorService.refactor(text, mode)
|
||||
|
||||
return NextResponse.json({
|
||||
originalText: result.original,
|
||||
reformulatedText: result.refactored,
|
||||
option: option,
|
||||
language: result.language,
|
||||
wordCountChange: result.wordCountChange
|
||||
})
|
||||
} catch (error: any) {
|
||||
return NextResponse.json(
|
||||
{ error: error.message || 'Failed to reformulate text' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
46
memento-note/app/api/ai/suggest-notebook/route.ts
Normal file
46
memento-note/app/api/ai/suggest-notebook/route.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { notebookSuggestionService } from '@/lib/ai/services/notebook-suggestion.service'
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await req.json()
|
||||
const { noteContent, language = 'en' } = body
|
||||
|
||||
if (!noteContent || typeof noteContent !== 'string') {
|
||||
return NextResponse.json({ error: 'noteContent is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Minimum content length for suggestion (20 words as per specs)
|
||||
const wordCount = noteContent.trim().split(/\s+/).length
|
||||
if (wordCount < 20) {
|
||||
return NextResponse.json({
|
||||
suggestion: null,
|
||||
reason: 'content_too_short',
|
||||
message: 'Note content too short for meaningful suggestion'
|
||||
})
|
||||
}
|
||||
|
||||
// Get suggestion from AI service
|
||||
const suggestedNotebook = await notebookSuggestionService.suggestNotebook(
|
||||
noteContent,
|
||||
session.user.id,
|
||||
language
|
||||
)
|
||||
|
||||
return NextResponse.json({
|
||||
suggestion: suggestedNotebook,
|
||||
confidence: suggestedNotebook ? 0.8 : 0 // Placeholder confidence score
|
||||
})
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to generate suggestion' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
62
memento-note/app/api/ai/tags/route.ts
Normal file
62
memento-note/app/api/ai/tags/route.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { auth } from '@/auth';
|
||||
import { contextualAutoTagService } from '@/lib/ai/services/contextual-auto-tag.service';
|
||||
import { getAIProvider } from '@/lib/ai/factory';
|
||||
import { getSystemConfig } from '@/lib/config';
|
||||
import { z } from 'zod';
|
||||
|
||||
const requestSchema = z.object({
|
||||
content: z.string().min(1, "Le contenu ne peut pas être vide"),
|
||||
notebookId: z.string().optional(),
|
||||
language: z.string().default('en'),
|
||||
});
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = await req.json();
|
||||
const { content, notebookId, language } = requestSchema.parse(body);
|
||||
|
||||
// If notebookId is provided, use contextual suggestions (IA2)
|
||||
if (notebookId) {
|
||||
const suggestions = await contextualAutoTagService.suggestLabels(
|
||||
content,
|
||||
notebookId,
|
||||
session.user.id,
|
||||
language
|
||||
);
|
||||
|
||||
// Convert label → tag to match TagSuggestion interface
|
||||
const convertedTags = suggestions.map(s => ({
|
||||
tag: s.label, // Convert label to tag
|
||||
confidence: s.confidence,
|
||||
// Keep additional properties for client-side use
|
||||
...(s.reasoning && { reasoning: s.reasoning }),
|
||||
...(s.isNewLabel !== undefined && { isNewLabel: s.isNewLabel })
|
||||
}));
|
||||
|
||||
return NextResponse.json({ tags: convertedTags });
|
||||
}
|
||||
|
||||
// Otherwise, use legacy auto-tagging (generates new tags)
|
||||
const config = await getSystemConfig();
|
||||
const provider = getAIProvider(config);
|
||||
const tags = await provider.generateTags(content, language);
|
||||
|
||||
return NextResponse.json({ tags });
|
||||
} catch (error: any) {
|
||||
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json({ error: error.issues }, { status: 400 });
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: error.message || 'Erreur lors de la génération des tags' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
90
memento-note/app/api/ai/test-embeddings/route.ts
Normal file
90
memento-note/app/api/ai/test-embeddings/route.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getEmbeddingsProvider } from '@/lib/ai/factory'
|
||||
import { getSystemConfig } from '@/lib/config'
|
||||
|
||||
function getProviderDetails(config: Record<string, string>, providerType: string) {
|
||||
const provider = providerType.toLowerCase()
|
||||
|
||||
switch (provider) {
|
||||
case 'ollama':
|
||||
return {
|
||||
provider: 'Ollama',
|
||||
baseUrl: config.OLLAMA_BASE_URL || 'http://localhost:11434',
|
||||
model: config.AI_MODEL_EMBEDDING || 'embeddinggemma:latest'
|
||||
}
|
||||
case 'openai':
|
||||
return {
|
||||
provider: 'OpenAI',
|
||||
baseUrl: 'https://api.openai.com/v1',
|
||||
model: config.AI_MODEL_EMBEDDING || 'text-embedding-3-small'
|
||||
}
|
||||
case 'custom':
|
||||
return {
|
||||
provider: 'Custom OpenAI',
|
||||
baseUrl: config.CUSTOM_OPENAI_BASE_URL || 'Not configured',
|
||||
model: config.AI_MODEL_EMBEDDING || 'text-embedding-3-small'
|
||||
}
|
||||
default:
|
||||
return {
|
||||
provider: provider,
|
||||
baseUrl: 'unknown',
|
||||
model: config.AI_MODEL_EMBEDDING || 'unknown'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const config = await getSystemConfig()
|
||||
const provider = getEmbeddingsProvider(config)
|
||||
|
||||
const testText = 'test'
|
||||
const startTime = Date.now()
|
||||
const embeddings = await provider.getEmbeddings(testText)
|
||||
const endTime = Date.now()
|
||||
|
||||
if (!embeddings || embeddings.length === 0) {
|
||||
const providerType = config.AI_PROVIDER_EMBEDDING || 'ollama'
|
||||
const details = getProviderDetails(config, providerType)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'No embeddings returned',
|
||||
provider: providerType,
|
||||
model: config.AI_MODEL_EMBEDDING || 'embeddinggemma:latest',
|
||||
details
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
const providerType = config.AI_PROVIDER_EMBEDDING || 'ollama'
|
||||
const details = getProviderDetails(config, providerType)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
provider: providerType,
|
||||
model: config.AI_MODEL_EMBEDDING || 'embeddinggemma:latest',
|
||||
embeddingLength: embeddings.length,
|
||||
firstValues: embeddings.slice(0, 5),
|
||||
responseTime: endTime - startTime,
|
||||
details
|
||||
})
|
||||
} catch (error: any) {
|
||||
const config = await getSystemConfig()
|
||||
const providerType = config.AI_PROVIDER_EMBEDDING || 'ollama'
|
||||
const details = getProviderDetails(config, providerType)
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error.message || 'Unknown error',
|
||||
provider: providerType,
|
||||
model: config.AI_MODEL_EMBEDDING || 'embeddinggemma:latest',
|
||||
details,
|
||||
stack: process.env.NODE_ENV === 'development' ? error.stack : undefined
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
49
memento-note/app/api/ai/test-tags/route.ts
Normal file
49
memento-note/app/api/ai/test-tags/route.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getTagsProvider } from '@/lib/ai/factory'
|
||||
import { getSystemConfig } from '@/lib/config'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const config = await getSystemConfig()
|
||||
const provider = getTagsProvider(config)
|
||||
|
||||
const testContent = "This is a test note about artificial intelligence and machine learning. It contains keywords like AI, ML, neural networks, and deep learning."
|
||||
|
||||
const startTime = Date.now()
|
||||
const tags = await provider.generateTags(testContent)
|
||||
const endTime = Date.now()
|
||||
|
||||
if (!tags || tags.length === 0) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'No tags generated',
|
||||
provider: config.AI_PROVIDER_TAGS || 'ollama',
|
||||
model: config.AI_MODEL_TAGS || 'granite4:latest'
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
provider: config.AI_PROVIDER_TAGS || 'ollama',
|
||||
model: config.AI_MODEL_TAGS || 'granite4:latest',
|
||||
tags: tags,
|
||||
responseTime: endTime - startTime
|
||||
})
|
||||
} catch (error: any) {
|
||||
const config = await getSystemConfig()
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error.message || 'Unknown error',
|
||||
provider: config.AI_PROVIDER_TAGS || 'ollama',
|
||||
model: config.AI_MODEL_TAGS || 'granite4:latest',
|
||||
stack: process.env.NODE_ENV === 'development' ? error.stack : undefined
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
89
memento-note/app/api/ai/test/route.ts
Normal file
89
memento-note/app/api/ai/test/route.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getTagsProvider, getEmbeddingsProvider } from '@/lib/ai/factory'
|
||||
import { getSystemConfig } from '@/lib/config'
|
||||
|
||||
function getProviderDetails(config: Record<string, string>, providerType: string) {
|
||||
const provider = providerType.toLowerCase()
|
||||
|
||||
switch (provider) {
|
||||
case 'ollama':
|
||||
return {
|
||||
provider: 'Ollama',
|
||||
baseUrl: config.OLLAMA_BASE_URL || process.env.OLLAMA_BASE_URL || 'http://localhost:11434',
|
||||
model: config.AI_MODEL_EMBEDDING || 'embeddinggemma:latest'
|
||||
}
|
||||
case 'openai':
|
||||
return {
|
||||
provider: 'OpenAI',
|
||||
baseUrl: 'https://api.openai.com/v1',
|
||||
model: config.AI_MODEL_EMBEDDING || 'text-embedding-3-small'
|
||||
}
|
||||
case 'custom':
|
||||
return {
|
||||
provider: 'Custom OpenAI',
|
||||
baseUrl: config.CUSTOM_OPENAI_BASE_URL || process.env.CUSTOM_OPENAI_BASE_URL || 'Not configured',
|
||||
model: config.AI_MODEL_EMBEDDING || 'text-embedding-3-small'
|
||||
}
|
||||
default:
|
||||
return {
|
||||
provider: provider,
|
||||
baseUrl: 'unknown',
|
||||
model: config.AI_MODEL_EMBEDDING || 'unknown'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const config = await getSystemConfig()
|
||||
const tagsProvider = getTagsProvider(config)
|
||||
const embeddingsProvider = getEmbeddingsProvider(config)
|
||||
|
||||
const testText = 'test'
|
||||
|
||||
// Test embeddings provider
|
||||
const embeddings = await embeddingsProvider.getEmbeddings(testText)
|
||||
|
||||
if (!embeddings || embeddings.length === 0) {
|
||||
const providerType = config.AI_PROVIDER_EMBEDDING || 'ollama'
|
||||
const details = getProviderDetails(config, providerType)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
tagsProvider: config.AI_PROVIDER_TAGS || 'ollama',
|
||||
embeddingsProvider: providerType,
|
||||
error: 'No embeddings returned',
|
||||
details
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
const tagsProviderType = config.AI_PROVIDER_TAGS || 'ollama'
|
||||
const embeddingsProviderType = config.AI_PROVIDER_EMBEDDING || 'ollama'
|
||||
const details = getProviderDetails(config, embeddingsProviderType)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
tagsProvider: tagsProviderType,
|
||||
embeddingsProvider: embeddingsProviderType,
|
||||
embeddingLength: embeddings.length,
|
||||
firstValues: embeddings.slice(0, 5),
|
||||
details
|
||||
})
|
||||
} catch (error: any) {
|
||||
const config = await getSystemConfig()
|
||||
const providerType = config.AI_PROVIDER_EMBEDDING || 'ollama'
|
||||
const details = getProviderDetails(config, providerType)
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error.message || 'Unknown error',
|
||||
stack: process.env.NODE_ENV === 'development' ? error.stack : undefined,
|
||||
details
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
107
memento-note/app/api/ai/title-suggestions/route.ts
Normal file
107
memento-note/app/api/ai/title-suggestions/route.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getAIProvider } from '@/lib/ai/factory'
|
||||
import { getSystemConfig } from '@/lib/config'
|
||||
import { z } from 'zod'
|
||||
|
||||
const requestSchema = z.object({
|
||||
content: z.string().min(1, "Le contenu ne peut pas être vide"),
|
||||
})
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const body = await req.json()
|
||||
const { content } = requestSchema.parse(body)
|
||||
|
||||
// Vérifier qu'il y a au moins 10 mots
|
||||
const wordCount = content.split(/\s+/).length
|
||||
|
||||
if (wordCount < 10) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Le contenu doit avoir au moins 10 mots' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const config = await getSystemConfig()
|
||||
const provider = getAIProvider(config)
|
||||
|
||||
// Détecter la langue du contenu (simple détection basée sur les caractères et mots)
|
||||
const hasNonLatinChars = /[\u0400-\u04FF\u0600-\u06FF\u4E00-\u9FFF\u0E00-\u0E7F]/.test(content)
|
||||
const isPersian = /[\u0600-\u06FF]/.test(content)
|
||||
const isChinese = /[\u4E00-\u9FFF]/.test(content)
|
||||
const isRussian = /[\u0400-\u04FF]/.test(content)
|
||||
const isArabic = /[\u0600-\u06FF]/.test(content)
|
||||
|
||||
// Détection du français par des mots et caractères caractéristiques
|
||||
const frenchWords = /\b(le|la|les|un|une|des|et|ou|mais|donc|pour|dans|sur|avec|sans|très|plus|moins|tout|tous|toute|toutes|ce|cette|ces|mon|ma|mes|ton|ta|tes|son|sa|ses|notre|nos|votre|vos|leur|leurs|je|tu|il|elle|nous|vous|ils|elles|est|sont|été|être|avoir|faire|aller|venir|voir|savoir|pouvoir|vouloir|falloir|comme|que|qui|dont|où|quand|pourquoi|comment|quel|quelle|quels|quelles)\b/i
|
||||
const frenchAccents = /[éèêàâôûùïüç]/i
|
||||
const isFrench = frenchWords.test(content) || frenchAccents.test(content)
|
||||
|
||||
// Déterminer la langue du prompt système
|
||||
let promptLanguage = 'en'
|
||||
let responseLanguage = 'English'
|
||||
|
||||
if (isFrench) {
|
||||
promptLanguage = 'fr' // Français
|
||||
responseLanguage = 'French'
|
||||
} else if (isPersian) {
|
||||
promptLanguage = 'fa' // Persan
|
||||
responseLanguage = 'Persian'
|
||||
} else if (isChinese) {
|
||||
promptLanguage = 'zh' // Chinois
|
||||
responseLanguage = 'Chinese'
|
||||
} else if (isRussian) {
|
||||
promptLanguage = 'ru' // Russe
|
||||
responseLanguage = 'Russian'
|
||||
} else if (isArabic) {
|
||||
promptLanguage = 'ar' // Arabe
|
||||
responseLanguage = 'Arabic'
|
||||
}
|
||||
|
||||
// Générer des titres appropriés basés sur le contenu
|
||||
const titlePrompt = promptLanguage === 'en'
|
||||
? `You are a title generator. Generate 3 concise, descriptive titles for the following content.
|
||||
|
||||
IMPORTANT INSTRUCTIONS:
|
||||
- Use ONLY the content provided below between the CONTENT_START and CONTENT_END markers
|
||||
- Do NOT use any external knowledge or training data
|
||||
- Focus on the main topics and themes in THIS SPECIFIC content
|
||||
- Be specific to what is actually discussed
|
||||
|
||||
CONTENT_START: ${content.substring(0, 500)} CONTENT_END
|
||||
|
||||
Respond ONLY with a JSON array: [{"title": "title1", "confidence": 0.95}, {"title": "title2", "confidence": 0.85}, {"title": "title3", "confidence": 0.75}]`
|
||||
: `Tu es un générateur de titres. Génère 3 titres concis et descriptifs pour le contenu suivant en ${responseLanguage}.
|
||||
|
||||
INSTRUCTIONS IMPORTANTES :
|
||||
- Utilise SEULEMENT le contenu fourni entre les marqueurs CONTENT_START et CONTENT_END
|
||||
- N'utilise AUCUNE connaissance externe ou données d'entraînement
|
||||
- Concentre-toi sur les sujets principaux et thèmes de CE CONTENU SPÉCIFIQUE
|
||||
- Sois spécifique à ce qui est réellement discuté
|
||||
|
||||
CONTENT_START: ${content.substring(0, 500)} CONTENT_END
|
||||
|
||||
Réponds SEULEMENT avec un tableau JSON: [{"title": "titre1", "confidence": 0.95}, {"title": "titre2", "confidence": 0.85}, {"title": "titre3", "confidence": 0.75}]`
|
||||
|
||||
const titles = await provider.generateTitles(titlePrompt)
|
||||
|
||||
// Créer les suggestions
|
||||
const suggestions = titles.map((t: any) => ({
|
||||
title: t.title,
|
||||
confidence: Math.round(t.confidence * 100),
|
||||
reasoning: `Basé sur le contenu`
|
||||
}))
|
||||
|
||||
return NextResponse.json({ suggestions })
|
||||
} catch (error: any) {
|
||||
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json({ error: error.issues }, { status: 400 })
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: error.message || 'Erreur lors de la génération des titres' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
90
memento-note/app/api/ai/transform-markdown/route.ts
Normal file
90
memento-note/app/api/ai/transform-markdown/route.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { getAIProvider } from '@/lib/ai/factory'
|
||||
import { getSystemConfig } from '@/lib/config'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { text } = await request.json()
|
||||
|
||||
// Validation
|
||||
if (!text || typeof text !== 'string') {
|
||||
return NextResponse.json({ error: 'Text is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Validate word count
|
||||
const wordCount = text.split(/\s+/).length
|
||||
if (wordCount < 10) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Text must have at least 10 words to transform' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
if (wordCount > 500) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Text must have maximum 500 words to transform' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const config = await getSystemConfig()
|
||||
const provider = getAIProvider(config)
|
||||
|
||||
// Detect language from text
|
||||
const hasFrench = /[àâäéèêëïîôùûüÿç]/i.test(text)
|
||||
const responseLanguage = hasFrench ? 'French' : 'English'
|
||||
|
||||
// Build prompt to transform text to Markdown
|
||||
const prompt = hasFrench
|
||||
? `Tu es un expert en Markdown. Transforme ce texte ${responseLanguage} en Markdown bien formaté.
|
||||
|
||||
IMPORTANT :
|
||||
- Ajoute des titres avec ## pour les sections principales
|
||||
- Utilise des listes à puces (-) ou numérotées (1.) quand approprié
|
||||
- Ajoute de l'emphase (gras **texte**, italique *texte*) pour les mots clés
|
||||
- Utilise des blocs de code pour le code ou les commandes
|
||||
- Présente l'information de manière claire et structurée
|
||||
- GARDE le même sens et le contenu, seul le format change
|
||||
|
||||
Texte à transformer :
|
||||
${text}
|
||||
|
||||
Réponds SEULEMENT avec le texte transformé en Markdown, sans explications.`
|
||||
: `You are a Markdown expert. Transform this ${responseLanguage} text into well-formatted Markdown.
|
||||
|
||||
IMPORTANT:
|
||||
- Add headings with ## for main sections
|
||||
- Use bullet lists (-) or numbered lists (1.) when appropriate
|
||||
- Add emphasis (bold **text**, italic *text*) for key terms
|
||||
- Use code blocks for code or commands
|
||||
- Present information clearly and structured
|
||||
- KEEP the same meaning and content, only change the format
|
||||
|
||||
Text to transform:
|
||||
${text}
|
||||
|
||||
Respond ONLY with the transformed Markdown text, no explanations.`
|
||||
|
||||
|
||||
const transformedText = await provider.generateText(prompt)
|
||||
|
||||
|
||||
return NextResponse.json({
|
||||
originalText: text,
|
||||
transformedText: transformedText,
|
||||
language: responseLanguage
|
||||
})
|
||||
} catch (error: any) {
|
||||
return NextResponse.json(
|
||||
{ error: error.message || 'Failed to transform text to Markdown' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
30
memento-note/app/api/ai/translate/route.ts
Normal file
30
memento-note/app/api/ai/translate/route.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { getTagsProvider } from '@/lib/ai/factory'
|
||||
import { getSystemConfig } from '@/lib/config'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { text, targetLanguage } = await request.json()
|
||||
|
||||
if (!text || !targetLanguage) {
|
||||
return NextResponse.json({ error: 'text and targetLanguage are required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const config = await getSystemConfig()
|
||||
const provider = getTagsProvider(config)
|
||||
|
||||
const prompt = `Translate the following text to ${targetLanguage}. Return ONLY the translated text, no explanation, no preamble, no quotes:\n\n${text}`
|
||||
|
||||
const translatedText = await provider.generateText(prompt)
|
||||
|
||||
return NextResponse.json({ translatedText: translatedText.trim() })
|
||||
} catch (error: any) {
|
||||
return NextResponse.json({ error: error.message || 'Translation failed' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user