feat: add reminders page, BMad skills upgrade, MCP server refactor
- Add reminders page with navigation support - Upgrade BMad builder module to skills-based architecture - Refactor MCP server: extract tools and auth into separate modules - Add connections cache, custom AI provider support - Update prisma schema and generated client - Various UI/UX improvements and i18n updates - Add service worker for PWA support Made-with: Cursor
This commit is contained in:
@@ -64,14 +64,14 @@ export function AI_TESTER({ type }: { type: 'tags' | 'embeddings' }) {
|
||||
|
||||
if (data.success) {
|
||||
toast.success(
|
||||
`✅ ${type === 'tags' ? 'Tags' : 'Embeddings'} Test Successful!`,
|
||||
`✅ ${t('admin.aiTest.testSuccessToast', { type: type === 'tags' ? 'Tags' : 'Embeddings' })}`,
|
||||
{
|
||||
description: `Provider: ${data.provider} | Time: ${endTime - startTime}ms`
|
||||
}
|
||||
)
|
||||
} else {
|
||||
toast.error(
|
||||
`❌ ${type === 'tags' ? 'Tags' : 'Embeddings'} Test Failed`,
|
||||
`❌ ${t('admin.aiTest.testFailedToast', { type: type === 'tags' ? 'Tags' : 'Embeddings' })}`,
|
||||
{
|
||||
description: data.error || 'Unknown error'
|
||||
}
|
||||
@@ -92,7 +92,7 @@ export function AI_TESTER({ type }: { type: 'tags' | 'embeddings' }) {
|
||||
}
|
||||
|
||||
const getProviderInfo = () => {
|
||||
if (!config) return { provider: 'Loading...', model: 'Loading...' }
|
||||
if (!config) return { provider: t('admin.aiTest.testing'), model: t('admin.aiTest.testing') }
|
||||
|
||||
if (type === 'tags') {
|
||||
return {
|
||||
@@ -232,7 +232,7 @@ export function AI_TESTER({ type }: { type: 'tags' | 'embeddings' }) {
|
||||
{result.details && (
|
||||
<details className="mt-2">
|
||||
<summary className="text-xs cursor-pointer text-red-600 dark:text-red-400">
|
||||
Technical details
|
||||
{t('admin.aiTest.technicalDetails')}
|
||||
</summary>
|
||||
<pre className="mt-2 text-xs overflow-auto p-2 bg-red-100 dark:bg-red-900/30 rounded">
|
||||
{JSON.stringify(result.details, null, 2)}
|
||||
@@ -250,7 +250,7 @@ export function AI_TESTER({ type }: { type: 'tags' | 'embeddings' }) {
|
||||
<div className="text-center py-4">
|
||||
<Loader2 className="h-8 w-8 animate-spin mx-auto text-primary" />
|
||||
<p className="text-sm text-muted-foreground mt-2">
|
||||
{t('admin.aiTest.testing')} {type === 'tags' ? 'tags generation' : 'embeddings'}...
|
||||
{t('admin.aiTest.testingType', { type: type === 'tags' ? 'tags generation' : 'embeddings' })}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
'use client'
|
||||
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { auth } from '@/auth'
|
||||
import { redirect } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
import { ArrowLeft, TestTube } from 'lucide-react'
|
||||
import { AI_TESTER } from './ai-tester'
|
||||
import { useLanguage } from '@/lib/i18n'
|
||||
|
||||
export default async function AITestPage() {
|
||||
const session = await auth()
|
||||
|
||||
if ((session?.user as any)?.role !== 'ADMIN') {
|
||||
redirect('/')
|
||||
}
|
||||
export default function AITestPage() {
|
||||
const { t } = useLanguage()
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-10 px-4 max-w-6xl">
|
||||
@@ -25,10 +22,10 @@ export default async function AITestPage() {
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold flex items-center gap-2">
|
||||
<TestTube className="h-8 w-8" />
|
||||
AI Provider Testing
|
||||
{t('admin.aiTest.title')}
|
||||
</h1>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
Test your AI providers for tag generation and semantic search embeddings
|
||||
{t('admin.aiTest.description')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -40,10 +37,10 @@ export default async function AITestPage() {
|
||||
<CardHeader className="bg-primary/5 dark:bg-primary/10">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<span className="text-2xl">🏷️</span>
|
||||
Tags Generation Test
|
||||
{t('admin.aiTest.tagsTestTitle')}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Test the AI provider responsible for automatic tag suggestions
|
||||
{t('admin.aiTest.tagsTestDescription')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-6">
|
||||
@@ -56,10 +53,10 @@ export default async function AITestPage() {
|
||||
<CardHeader className="bg-green-50/50 dark:bg-green-950/20">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<span className="text-2xl">🔍</span>
|
||||
Embeddings Test
|
||||
{t('admin.aiTest.embeddingsTestTitle')}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Test the AI provider responsible for semantic search embeddings
|
||||
{t('admin.aiTest.embeddingsTestDescription')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-6">
|
||||
@@ -71,32 +68,31 @@ export default async function AITestPage() {
|
||||
{/* Info Section */}
|
||||
<Card className="mt-6">
|
||||
<CardHeader>
|
||||
<CardTitle>ℹ️ How Testing Works</CardTitle>
|
||||
<CardTitle>ℹ️ {t('admin.aiTest.howItWorksTitle')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4 text-sm">
|
||||
<div>
|
||||
<h4 className="font-semibold mb-2">🏷️ Tags Generation Test:</h4>
|
||||
<h4 className="font-semibold mb-2">{t('admin.aiTest.tagsGenerationTest')}</h4>
|
||||
<ul className="list-disc list-inside space-y-1 text-muted-foreground">
|
||||
<li>Sends a sample note to the AI provider</li>
|
||||
<li>Requests 3-5 relevant tags based on the content</li>
|
||||
<li>Displays the generated tags with confidence scores</li>
|
||||
<li>Measures response time</li>
|
||||
<li>{t('admin.aiTest.tagsStep1')}</li>
|
||||
<li>{t('admin.aiTest.tagsStep2')}</li>
|
||||
<li>{t('admin.aiTest.tagsStep3')}</li>
|
||||
<li>{t('admin.aiTest.tagsStep4')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold mb-2">🔍 Embeddings Test:</h4>
|
||||
<h4 className="font-semibold mb-2">{t('admin.aiTest.embeddingsTestLabel')}</h4>
|
||||
<ul className="list-disc list-inside space-y-1 text-muted-foreground">
|
||||
<li>Sends a sample text to the embedding provider</li>
|
||||
<li>Generates a vector representation (list of numbers)</li>
|
||||
<li>Displays embedding dimensions and sample values</li>
|
||||
<li>Verifies the vector is valid and properly formatted</li>
|
||||
<li>{t('admin.aiTest.embeddingsStep1')}</li>
|
||||
<li>{t('admin.aiTest.embeddingsStep2')}</li>
|
||||
<li>{t('admin.aiTest.embeddingsStep3')}</li>
|
||||
<li>{t('admin.aiTest.embeddingsStep4')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="bg-amber-50 dark:bg-amber-950/20 p-4 rounded-lg border border-amber-200 dark:border-amber-900">
|
||||
<p className="font-semibold text-amber-900 dark:text-amber-100">💡 Tip:</p>
|
||||
<p className="font-semibold text-amber-900 dark:text-amber-100">💡 {t('admin.aiTest.tipTitle')}</p>
|
||||
<p className="text-amber-800 dark:text-amber-200 mt-1">
|
||||
You can use different providers for tags and embeddings! For example, use Ollama (free) for tags
|
||||
and OpenAI (best quality) for embeddings to optimize costs and performance.
|
||||
{t('admin.aiTest.tipContent')}
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -66,8 +66,8 @@ export function CreateUserDialog() {
|
||||
name="role"
|
||||
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
<option value="USER">User</option>
|
||||
<option value="ADMIN">Admin</option>
|
||||
<option value="USER">{t('admin.users.roles.user')}</option>
|
||||
<option value="ADMIN">{t('admin.users.roles.admin')}</option>
|
||||
</select>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { updateSystemConfig, testSMTP } from '@/app/actions/admin-settings'
|
||||
import { getOllamaModels } from '@/app/actions/ollama'
|
||||
import { getCustomModels, getCustomEmbeddingModels } from '@/app/actions/custom-provider'
|
||||
import { toast } from 'sonner'
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import Link from 'next/link'
|
||||
@@ -21,15 +22,10 @@ interface AvailableModels {
|
||||
}
|
||||
|
||||
const MODELS_2026 = {
|
||||
// Removed hardcoded Ollama models in favor of dynamic fetching
|
||||
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: ['gpt-4o-mini', 'gpt-4o', 'claude-3-haiku', 'claude-3-sonnet', 'llama-3.1-8b'],
|
||||
embeddings: ['text-embedding-3-small', 'text-embedding-3-large', 'text-embedding-ada-002']
|
||||
}
|
||||
}
|
||||
|
||||
export function AdminSettingsForm({ config }: { config: Record<string, string> }) {
|
||||
@@ -57,6 +53,14 @@ export function AdminSettingsForm({ config }: { config: Record<string, string> }
|
||||
const [isLoadingTagsModels, setIsLoadingTagsModels] = useState(false)
|
||||
const [isLoadingEmbeddingsModels, setIsLoadingEmbeddingsModels] = useState(false)
|
||||
|
||||
// Custom provider dynamic models
|
||||
const [customTagsModels, setCustomTagsModels] = useState<string[]>([])
|
||||
const [customEmbeddingsModels, setCustomEmbeddingsModels] = useState<string[]>([])
|
||||
const [isLoadingCustomTagsModels, setIsLoadingCustomTagsModels] = useState(false)
|
||||
const [isLoadingCustomEmbeddingsModels, setIsLoadingCustomEmbeddingsModels] = useState(false)
|
||||
const [customTagsSearch, setCustomTagsSearch] = useState('')
|
||||
const [customEmbeddingsSearch, setCustomEmbeddingsSearch] = useState('')
|
||||
|
||||
|
||||
// Fetch Ollama models
|
||||
const fetchOllamaModels = useCallback(async (type: 'tags' | 'embeddings', url: string) => {
|
||||
@@ -83,6 +87,33 @@ export function AdminSettingsForm({ config }: { config: Record<string, string> }
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Fetch Custom provider models — tags use /v1/models, embeddings use /v1/embeddings/models
|
||||
const fetchCustomModels = useCallback(async (type: 'tags' | 'embeddings', url: string, apiKey?: string) => {
|
||||
if (!url) return
|
||||
|
||||
if (type === 'tags') setIsLoadingCustomTagsModels(true)
|
||||
else setIsLoadingCustomEmbeddingsModels(true)
|
||||
|
||||
try {
|
||||
const result = type === 'embeddings'
|
||||
? await getCustomEmbeddingModels(url, apiKey)
|
||||
: await getCustomModels(url, apiKey)
|
||||
|
||||
if (result.success && result.models.length > 0) {
|
||||
if (type === 'tags') setCustomTagsModels(result.models)
|
||||
else setCustomEmbeddingsModels(result.models)
|
||||
} else {
|
||||
toast.error(`Impossible de récupérer les modèles : ${result.error}`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
toast.error('Erreur lors de la récupération des modèles')
|
||||
} finally {
|
||||
if (type === 'tags') setIsLoadingCustomTagsModels(false)
|
||||
else setIsLoadingCustomEmbeddingsModels(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Initial fetch for Ollama models if provider is selected
|
||||
useEffect(() => {
|
||||
if (tagsProvider === 'ollama') {
|
||||
@@ -96,10 +127,23 @@ export function AdminSettingsForm({ config }: { config: Record<string, string> }
|
||||
if (embeddingsProvider === 'ollama') {
|
||||
const url = config.OLLAMA_BASE_URL_EMBEDDING || config.OLLAMA_BASE_URL || 'http://localhost:11434'
|
||||
fetchOllamaModels('embeddings', url)
|
||||
} else if (embeddingsProvider === 'custom') {
|
||||
const url = config.CUSTOM_OPENAI_BASE_URL || ''
|
||||
const key = config.CUSTOM_OPENAI_API_KEY || ''
|
||||
if (url) fetchCustomModels('embeddings', url, key)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [embeddingsProvider])
|
||||
|
||||
useEffect(() => {
|
||||
if (tagsProvider === 'custom') {
|
||||
const url = config.CUSTOM_OPENAI_BASE_URL || ''
|
||||
const key = config.CUSTOM_OPENAI_API_KEY || ''
|
||||
if (url) fetchCustomModels('tags', url, key)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [tagsProvider])
|
||||
|
||||
const handleSaveSecurity = async (formData: FormData) => {
|
||||
setIsSaving(true)
|
||||
const data = {
|
||||
@@ -270,7 +314,7 @@ export function AdminSettingsForm({ config }: { config: Record<string, string> }
|
||||
const defaultModels: Record<string, string> = {
|
||||
ollama: '',
|
||||
openai: MODELS_2026.openai.tags[0],
|
||||
custom: MODELS_2026.custom.tags[0],
|
||||
custom: '',
|
||||
}
|
||||
setSelectedTagsModel(defaultModels[newProvider] || '')
|
||||
}}
|
||||
@@ -361,7 +405,28 @@ export function AdminSettingsForm({ config }: { config: Record<string, string> }
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="CUSTOM_OPENAI_BASE_URL_TAGS">{t('admin.ai.baseUrl')}</Label>
|
||||
<Input id="CUSTOM_OPENAI_BASE_URL_TAGS" name="CUSTOM_OPENAI_BASE_URL_TAGS" defaultValue={config.CUSTOM_OPENAI_BASE_URL || ''} placeholder="https://api.example.com/v1" />
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
id="CUSTOM_OPENAI_BASE_URL_TAGS"
|
||||
name="CUSTOM_OPENAI_BASE_URL_TAGS"
|
||||
defaultValue={config.CUSTOM_OPENAI_BASE_URL || ''}
|
||||
placeholder="https://api.example.com/v1"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
const urlInput = document.getElementById('CUSTOM_OPENAI_BASE_URL_TAGS') as HTMLInputElement
|
||||
const keyInput = document.getElementById('CUSTOM_OPENAI_API_KEY_TAGS') as HTMLInputElement
|
||||
fetchCustomModels('tags', urlInput.value, keyInput.value)
|
||||
}}
|
||||
disabled={isLoadingCustomTagsModels}
|
||||
title="Récupérer les modèles"
|
||||
>
|
||||
<RefreshCw className={`h-4 w-4 ${isLoadingCustomTagsModels ? 'animate-spin' : ''}`} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="CUSTOM_OPENAI_API_KEY_TAGS">{t('admin.ai.apiKey')}</Label>
|
||||
@@ -369,18 +434,41 @@ export function AdminSettingsForm({ config }: { config: Record<string, string> }
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="AI_MODEL_TAGS_CUSTOM">{t('admin.ai.model')}</Label>
|
||||
{customTagsModels.length > 0 && (
|
||||
<Input
|
||||
placeholder="Rechercher un modèle..."
|
||||
value={customTagsSearch}
|
||||
onChange={(e) => setCustomTagsSearch(e.target.value)}
|
||||
className="mb-1"
|
||||
/>
|
||||
)}
|
||||
<select
|
||||
id="AI_MODEL_TAGS_CUSTOM"
|
||||
name="AI_MODEL_TAGS_CUSTOM"
|
||||
value={selectedTagsModel}
|
||||
onChange={(e) => setSelectedTagsModel(e.target.value)}
|
||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||
className={`flex w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ${customTagsModels.length > 0 ? 'h-auto min-h-[180px]' : 'h-10'}`}
|
||||
size={customTagsModels.length > 0 ? Math.min(8, customTagsModels.filter(m => m.toLowerCase().includes(customTagsSearch.toLowerCase())).length) : 1}
|
||||
>
|
||||
{MODELS_2026.custom.tags.map((model) => (
|
||||
<option key={model} value={model}>{model}</option>
|
||||
))}
|
||||
{customTagsModels.length > 0 ? (
|
||||
customTagsModels
|
||||
.filter(m => m.toLowerCase().includes(customTagsSearch.toLowerCase()))
|
||||
.map((model) => (
|
||||
<option key={model} value={model}>{model}</option>
|
||||
))
|
||||
) : (
|
||||
selectedTagsModel
|
||||
? <option value={selectedTagsModel}>{selectedTagsModel}</option>
|
||||
: <option value="" disabled>Cliquez sur ↺ pour récupérer les modèles</option>
|
||||
)}
|
||||
</select>
|
||||
<p className="text-xs text-muted-foreground">{t('admin.ai.commonModelsDescription')}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{isLoadingCustomTagsModels
|
||||
? 'Récupération des modèles...'
|
||||
: customTagsModels.length > 0
|
||||
? `${customTagsModels.length} modèle(s) disponible(s)`
|
||||
: 'Renseignez l\'URL et cliquez sur ↺ pour charger les modèles'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -409,7 +497,7 @@ export function AdminSettingsForm({ config }: { config: Record<string, string> }
|
||||
const defaultModels: Record<string, string> = {
|
||||
ollama: '',
|
||||
openai: MODELS_2026.openai.embeddings[0],
|
||||
custom: MODELS_2026.custom.embeddings[0],
|
||||
custom: '',
|
||||
}
|
||||
setSelectedEmbeddingModel(defaultModels[newProvider] || '')
|
||||
}}
|
||||
@@ -503,7 +591,28 @@ export function AdminSettingsForm({ config }: { config: Record<string, string> }
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="CUSTOM_OPENAI_BASE_URL_EMBEDDING">{t('admin.ai.baseUrl')}</Label>
|
||||
<Input id="CUSTOM_OPENAI_BASE_URL_EMBEDDING" name="CUSTOM_OPENAI_BASE_URL_EMBEDDING" defaultValue={config.CUSTOM_OPENAI_BASE_URL || ''} placeholder="https://api.example.com/v1" />
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
id="CUSTOM_OPENAI_BASE_URL_EMBEDDING"
|
||||
name="CUSTOM_OPENAI_BASE_URL_EMBEDDING"
|
||||
defaultValue={config.CUSTOM_OPENAI_BASE_URL || ''}
|
||||
placeholder="https://api.example.com/v1"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
const urlInput = document.getElementById('CUSTOM_OPENAI_BASE_URL_EMBEDDING') as HTMLInputElement
|
||||
const keyInput = document.getElementById('CUSTOM_OPENAI_API_KEY_EMBEDDING') as HTMLInputElement
|
||||
fetchCustomModels('embeddings', urlInput.value, keyInput.value)
|
||||
}}
|
||||
disabled={isLoadingCustomEmbeddingsModels}
|
||||
title="Récupérer les modèles"
|
||||
>
|
||||
<RefreshCw className={`h-4 w-4 ${isLoadingCustomEmbeddingsModels ? 'animate-spin' : ''}`} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="CUSTOM_OPENAI_API_KEY_EMBEDDING">{t('admin.ai.apiKey')}</Label>
|
||||
@@ -511,18 +620,41 @@ export function AdminSettingsForm({ config }: { config: Record<string, string> }
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="AI_MODEL_EMBEDDING_CUSTOM">{t('admin.ai.model')}</Label>
|
||||
{customEmbeddingsModels.length > 0 && (
|
||||
<Input
|
||||
placeholder="Rechercher un modèle..."
|
||||
value={customEmbeddingsSearch}
|
||||
onChange={(e) => setCustomEmbeddingsSearch(e.target.value)}
|
||||
className="mb-1"
|
||||
/>
|
||||
)}
|
||||
<select
|
||||
id="AI_MODEL_EMBEDDING_CUSTOM"
|
||||
name="AI_MODEL_EMBEDDING_CUSTOM"
|
||||
value={selectedEmbeddingModel}
|
||||
onChange={(e) => setSelectedEmbeddingModel(e.target.value)}
|
||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||
className={`flex w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ${customEmbeddingsModels.length > 0 ? 'h-auto min-h-[180px]' : 'h-10'}`}
|
||||
size={customEmbeddingsModels.length > 0 ? Math.min(8, customEmbeddingsModels.filter(m => m.toLowerCase().includes(customEmbeddingsSearch.toLowerCase())).length) : 1}
|
||||
>
|
||||
{MODELS_2026.custom.embeddings.map((model) => (
|
||||
<option key={model} value={model}>{model}</option>
|
||||
))}
|
||||
{customEmbeddingsModels.length > 0 ? (
|
||||
customEmbeddingsModels
|
||||
.filter(m => m.toLowerCase().includes(customEmbeddingsSearch.toLowerCase()))
|
||||
.map((model) => (
|
||||
<option key={model} value={model}>{model}</option>
|
||||
))
|
||||
) : (
|
||||
selectedEmbeddingModel
|
||||
? <option value={selectedEmbeddingModel}>{selectedEmbeddingModel}</option>
|
||||
: <option value="" disabled>Cliquez sur ↺ pour récupérer les modèles</option>
|
||||
)}
|
||||
</select>
|
||||
<p className="text-xs text-muted-foreground">{t('admin.ai.commonEmbeddingModels')}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{isLoadingCustomEmbeddingsModels
|
||||
? 'Récupération des modèles...'
|
||||
: customEmbeddingsModels.length > 0
|
||||
? `${customEmbeddingsModels.length} modèle(s) disponible(s)`
|
||||
: 'Renseignez l\'URL et cliquez sur ↺ pour charger les modèles'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -602,7 +734,7 @@ export function AdminSettingsForm({ config }: { config: Record<string, string> }
|
||||
</label>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="flex justify-between">
|
||||
<CardFooter className="flex justify-between pt-6">
|
||||
<Button type="submit" disabled={isSaving}>{t('admin.smtp.saveSettings')}</Button>
|
||||
<Button type="button" variant="secondary" onClick={handleTestEmail} disabled={isTesting}>
|
||||
{isTesting ? t('admin.smtp.sending') : t('admin.smtp.testEmail')}
|
||||
|
||||
9
keep-notes/app/(main)/reminders/page.tsx
Normal file
9
keep-notes/app/(main)/reminders/page.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { getNotesWithReminders } from '@/app/actions/notes'
|
||||
import { RemindersPage } from '@/components/reminders-page'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export default async function RemindersRoute() {
|
||||
const notes = await getNotesWithReminders()
|
||||
return <RemindersPage notes={notes} />
|
||||
}
|
||||
Reference in New Issue
Block a user