diff --git a/memento-note/Dockerfile b/memento-note/Dockerfile index db4b13d..0ea236e 100644 --- a/memento-note/Dockerfile +++ b/memento-note/Dockerfile @@ -20,6 +20,10 @@ RUN npx prisma generate FROM node:22-bookworm-slim AS builder WORKDIR /app +RUN apt-get update && apt-get install -y --no-install-recommends \ + openssl \ + && rm -rf /var/lib/apt/lists/* + COPY --from=deps /app/node_modules ./node_modules COPY . . diff --git a/memento-note/app/(admin)/admin/ai-test/ai-tester.tsx b/memento-note/app/(admin)/admin/ai-test/ai-tester.tsx index 226c2bc..cd13a53 100644 --- a/memento-note/app/(admin)/admin/ai-test/ai-tester.tsx +++ b/memento-note/app/(admin)/admin/ai-test/ai-tester.tsx @@ -1,11 +1,10 @@ 'use client' import { Button } from '@/components/ui/button' -import { Card, CardContent } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' import { useState, useEffect } from 'react' import { toast } from 'sonner' -import { Loader2, CheckCircle2, XCircle, Clock, Zap, Info } from 'lucide-react' +import { Loader2, CheckCircle2, XCircle, Clock, Zap, Info, Shield, Brain, MessageSquare, Search } from 'lucide-react' import { useLanguage } from '@/lib/i18n' interface TestResult { @@ -65,14 +64,14 @@ export function AI_TESTER({ type }: { type: 'tags' | 'embeddings' | 'chat' }) { if (data.success) { toast.success( - `✅ ${t('admin.aiTest.testSuccessToast', { type: type === 'tags' ? 'Tags' : 'Embeddings' })}`, + `${t('admin.aiTest.testSuccessToast', { type: type === 'tags' ? 'Tags' : type === 'chat' ? 'Chat' : 'Embeddings' })}`, { description: `Provider: ${data.provider} | Time: ${endTime - startTime}ms` } ) } else { toast.error( - `❌ ${t('admin.aiTest.testFailedToast', { type: type === 'tags' ? 'Tags' : 'Embeddings' })}`, + `${t('admin.aiTest.testFailedToast', { type: type === 'tags' ? 'Tags' : type === 'chat' ? 'Chat' : 'Embeddings' })}`, { description: data.error || 'Unknown error' } @@ -93,7 +92,7 @@ export function AI_TESTER({ type }: { type: 'tags' | 'embeddings' | 'chat' }) { } const getProviderInfo = () => { - if (!config) return { provider: t('admin.aiTest.testing'), model: t('admin.aiTest.testing') } + if (!config) return { provider: '...', model: '...' } if (type === 'tags') { return { @@ -116,127 +115,164 @@ export function AI_TESTER({ type }: { type: 'tags' | 'embeddings' | 'chat' }) { const providerInfo = getProviderInfo() return ( -
- {/* Provider Info */} -
-
- {t('admin.aiTest.provider')} - - {providerInfo.provider.toUpperCase()} - +
+ {/* Current Config Summary */} +
+
+ {type === 'tags' ? : + type === 'embeddings' ? : + } + {t('admin.aiTest.provider')}
-
- {t('admin.aiTest.model')} - - {providerInfo.model} - +
+
+ {t('admin.aiTest.provider')} + + {providerInfo.provider} + +
+
+ {t('admin.aiTest.model')} + + {providerInfo.model} + +
- {/* Test Button */} + {/* Run Test Action */} - {/* Results */} + {/* Result Display */} {result && ( - - - {/* Status */} -
- {result.success ? ( - <> - - {t('admin.aiTest.testPassed')} - - ) : ( - <> - - {t('admin.aiTest.testFailed')} - +
+
+ {/* Status Header */} +
+
+ {result.success ? ( +
+ +
+ ) : ( +
+ +
+ )} + + {result.success ? t('admin.aiTest.testPassed') : t('admin.aiTest.testFailed')} + +
+ {result.responseTime && ( +
+ + {result.responseTime}ms +
)}
- {/* Response Time */} - {result.responseTime && ( -
- - {t('admin.aiTest.responseTime', { time: result.responseTime })} -
- )} - - {/* Tags Results */} + {/* Response Content - Tags */} {type === 'tags' && result.success && result.tags && ( -
-
- - {t('admin.aiTest.generatedTags')} +
+
+ + {t('admin.aiTest.generatedTags')}
{result.tags.map((tag, idx) => ( - - {tag.tag} - - ({Math.round(tag.confidence * 100)}%) + {tag.tag} + + {Math.round(tag.confidence * 100)}% - +
))}
)} - {/* Chat Response */} + {/* Response Content - Chat */} {type === 'chat' && result.success && result.chatResponse && ( -
-
- - Réponse du modèle +
+
+ + Modèle Réponse
-
-

"{result.chatResponse}"

+
+
+

+ "{result.chatResponse}" +

)} - {/* Embeddings Results */} + {/* Response Content - Embeddings */} {type === 'embeddings' && result.success && result.embeddingLength && ( -
-
- - {t('admin.aiTest.embeddingDimensions')} -
-
-
- {result.embeddingLength} +
+
+
+
+ {result.embeddingLength} +
+
+ {t('admin.aiTest.vectorDimensions')} +
-
- {t('admin.aiTest.vectorDimensions')} +
+
+
+ Active Vector +
+ {result.firstValues && result.firstValues.length > 0 && ( -
- {t('admin.aiTest.first5Values')} -
- [{result.firstValues.slice(0, 5).map((v, i) => v.toFixed(4)).join(', ')}] +
+
+ {t('admin.aiTest.first5Values')} +
+
+ {result.firstValues.slice(0, 5).map((v, i) => ( + + {v.toFixed(4)} + + ))}
)} @@ -245,32 +281,30 @@ export function AI_TESTER({ type }: { type: 'tags' | 'embeddings' | 'chat' }) { {/* Error Details */} {!result.success && result.error && ( -
-

{t('admin.aiTest.error')}

-

{result.error}

- {result.details && ( -
- - {t('admin.aiTest.technicalDetails')} - -
-                      {JSON.stringify(result.details, null, 2)}
-                    
-
- )} +
+
+
+ + Détails de l'erreur +
+

{result.error}

+ + {result.details && ( +
+ + {t('admin.aiTest.technicalDetails')} + +
+
+                          {JSON.stringify(result.details, null, 2)}
+                        
+
+
+ )} +
)} - - - )} - - {/* Loading State */} - {isLoading && ( -
- -

- {t('admin.aiTest.testingType', { type: type === 'tags' ? 'tags generation' : 'embeddings' })} -

+
)}
diff --git a/memento-note/app/(admin)/admin/ai-test/page.tsx b/memento-note/app/(admin)/admin/ai-test/page.tsx index 70b7304..976e350 100644 --- a/memento-note/app/(admin)/admin/ai-test/page.tsx +++ b/memento-note/app/(admin)/admin/ai-test/page.tsx @@ -1,8 +1,7 @@ 'use client' -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Button } from '@/components/ui/button' -import { ArrowLeft, TestTube } from 'lucide-react' +import { ArrowLeft, TestTube, Info, Sparkles, Zap, Brain, MessageSquare, Search } from 'lucide-react' import { AI_TESTER } from './ai-tester' import { useLanguage } from '@/lib/i18n' @@ -10,109 +9,140 @@ export default function AITestPage() { const { t } = useLanguage() return ( -
-
-
- - - -
-

- - {t('admin.aiTest.title')} -

-

- {t('admin.aiTest.description')} -

+
+
+
+ {/* Header - Even larger and more spaced */} +
+
+ + + +
+
+
+ +
+ Diagnostics Haute Précision +
+

+ {t('admin.aiTest.title')} +

+

+ {t('admin.aiTest.description')} +

+
+
+ +
+
+
+ Système Actif +
+

Latence: Optimisée

+
+
+ + {/* New Horizontal Section for each Test - Maximum Width Utilization */} +
+ {/* 1. Tags Test - Horizontal Layout */} +
+
+
+ +
+
+
+ 🏷️ +
+
+

{t('admin.aiTest.tagsTestTitle')}

+

{t('admin.aiTest.tagsTestDescription')}

+
+
+ Auto-Labeling + Suggestions +
+
+
+
+
+ +
+
+
+ + {/* 2. Embeddings Test - Horizontal Layout */} +
+
+
+ +
+
+
+ 🔍 +
+
+

{t('admin.aiTest.embeddingsTestTitle')}

+

{t('admin.aiTest.embeddingsTestDescription')}

+
+
+ Vector Store + Similarity +
+
+
+
+
+ +
+
+
+ + {/* 3. Chat Test - Horizontal Layout */} +
+
+
+ +
+
+
+ 💬 +
+
+

{t('admin.aiTest.chatTestTitle')}

+

{t('admin.aiTest.chatTestDescription')}

+
+
+ Conversational + Streaming +
+
+
+
+
+ +
+
+
+
+ + {/* Tips Section - Broad and visible */} +
+
+ 💡 +
+
+

{t('admin.aiTest.tipTitle')}

+

+ {t('admin.aiTest.tipContent')} +

+
- -
- {/* Tags Provider Test */} - - - - 🏷️ - {t('admin.aiTest.tagsTestTitle')} - - - {t('admin.aiTest.tagsTestDescription')} - - - - - - - - {/* Embeddings Provider Test */} - - - - 🔍 - {t('admin.aiTest.embeddingsTestTitle')} - - - {t('admin.aiTest.embeddingsTestDescription')} - - - - - - - - {/* Chat Provider Test */} - - - - 💬 - Fournisseur de chat - - - Testez le fournisseur IA responsable de l'assistant conversationnel - - - - - - -
- - - {/* Info Section */} - - - ℹ️ {t('admin.aiTest.howItWorksTitle')} - - -
-

{t('admin.aiTest.tagsGenerationTest')}

-
    -
  • {t('admin.aiTest.tagsStep1')}
  • -
  • {t('admin.aiTest.tagsStep2')}
  • -
  • {t('admin.aiTest.tagsStep3')}
  • -
  • {t('admin.aiTest.tagsStep4')}
  • -
-
-
-

{t('admin.aiTest.embeddingsTestLabel')}

-
    -
  • {t('admin.aiTest.embeddingsStep1')}
  • -
  • {t('admin.aiTest.embeddingsStep2')}
  • -
  • {t('admin.aiTest.embeddingsStep3')}
  • -
  • {t('admin.aiTest.embeddingsStep4')}
  • -
-
-
-

💡 {t('admin.aiTest.tipTitle')}

-

- {t('admin.aiTest.tipContent')} -

-
-
-
) } diff --git a/memento-note/components/ui/button.tsx b/memento-note/components/ui/button.tsx index 4d38506..fc680ba 100644 --- a/memento-note/components/ui/button.tsx +++ b/memento-note/components/ui/button.tsx @@ -5,7 +5,7 @@ import { Slot } from "radix-ui" import { cn } from "@/lib/utils" const buttonVariants = cva( - "inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + "inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-normal transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", { variants: { variant: { @@ -21,14 +21,14 @@ const buttonVariants = cva( link: "text-primary underline-offset-4 hover:underline", }, size: { - default: "h-9 px-4 py-2 has-[>svg]:px-3", - xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3", - sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5", - lg: "h-10 rounded-md px-6 has-[>svg]:px-4", - icon: "size-9", - "icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3", - "icon-sm": "size-8", - "icon-lg": "size-10", + default: "min-h-9 h-auto px-4 py-2 has-[>svg]:px-3", + xs: "min-h-6 h-auto gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3", + sm: "min-h-8 h-auto gap-1.5 rounded-md px-3 has-[>svg]:px-2.5", + lg: "min-h-10 h-auto rounded-md px-6 has-[>svg]:px-4", + icon: "size-9 shrink-0", + "icon-xs": "size-6 rounded-md shrink-0 [&_svg:not([class*='size-'])]:size-3", + "icon-sm": "size-8 shrink-0", + "icon-lg": "size-10 shrink-0", }, }, defaultVariants: { diff --git a/memento-note/fix-admin-settings.js b/memento-note/fix-admin-settings.js new file mode 100644 index 0000000..d1e1436 --- /dev/null +++ b/memento-note/fix-admin-settings.js @@ -0,0 +1,178 @@ +const fs = require('fs'); +const file = 'app/(admin)/admin/settings/admin-settings-form.tsx'; +let content = fs.readFileSync(file, 'utf-8'); + +// 1. Add icons +content = content.replace( + "import { TestTube, ExternalLink, RefreshCw } from 'lucide-react'", + "import { TestTube, ExternalLink, RefreshCw, Shield, Brain, Mail, Wrench } from 'lucide-react'" +); + +// 2. Change root wrapper to 2 columns +content = content.replace( + '
', + `
+ {/* Left Column */} +
` +); + +// 3. Close left column and open right column right before AI Providers +content = content.replace( + ' \n \n {t(\'admin.ai.title\')}', + `
+ {/* Right Column */} +
+ + + {t('admin.ai.title')}` +); + +// 4. Move Email from after AI to Left Column? No, let's keep the order and just split them. +// Wait, in my previous attempt I put Security + AI on left, and Email + Tools on right? +// Let's just do that to be safe. +// Let's put Security + AI in Left Column, and Email + Tools in Right Column. +// So: +// Left Column ends after AI Providers. +// Right Column starts before Email. +content = content.replace( + ' \n \n {t(\'admin.email.title\')}', + `
+ {/* Right Column */} +
+ + + {t('admin.email.title')}` +); + +// Add the closing div for Right Column at the very end +content = content.replace( + '
\n )\n}\n', + '
\n
\n )\n}\n' +); + +// Now let's replace all Card components with the custom design system +// Security Card +content = content.replace( + '\n \n {t(\'admin.security.title\')}\n {t(\'admin.security.description\')}\n ', + `
+
+
+ +
+
+

{t('admin.security.title')}

+

{t('admin.security.description')}

+
+
` +); +content = content.replace(//g, '
'); +content = content.replace(//g, '
'); +content = content.replace(/<\/CardFooter>/g, '
'); + +content = content.replace( + '\n
', + '\n
' +); // do it 4 times +content = content.replace('\n ', '\n
'); +content = content.replace('\n ', '\n
'); +content = content.replace('\n ', '\n
'); + +// AI Card +content = content.replace( + '\n \n {t(\'admin.ai.title\')}\n {t(\'admin.ai.description\')}\n ', + `
+
+
+ +
+
+

{t('admin.ai.title')}

+

{t('admin.ai.description')}

+
+
` +); + +// Email Card +content = content.replace( + '\n \n {t(\'admin.email.title\')}\n {t(\'admin.email.description\')}\n ', + `
+
+
+ +
+
+

{t('admin.email.title')}

+

{t('admin.email.description')}

+
+
` +); + +// Tools Card +content = content.replace( + '\n \n {t(\'admin.tools.title\')}\n {t(\'admin.tools.description\')}\n ', + `
+
+
+ +
+
+

{t('admin.tools.title')}

+

{t('admin.tools.description')}

+
+
` +); + +// Replace AI block styling +content = content.replace( + 'p-4 border rounded-lg bg-primary/5 dark:bg-primary/10', + 'p-4 border border-border/50 rounded-lg bg-muted/50' +); +content = content.replace( + 'p-4 border rounded-lg bg-green-50/50 dark:bg-green-950/20', + 'p-4 border border-border/50 rounded-lg bg-muted/50 mt-4' +); +content = content.replace( + 'p-4 border rounded-lg bg-blue-50/50 dark:bg-blue-950/20', + 'p-4 border border-border/50 rounded-lg bg-muted/50 mt-4' +); + +// Also replace dark text colors inside headers +content = content.replace( + '

\\n 🏷️', + '

\\n 🏷️' +); +content = content.replace( + '

\\n 🔍', + '

\\n 🔍' +); +content = content.replace( + '

\\n 💬', + '

\\n 💬' +); + +// Email / Search test result classes +content = content.replace( + /border-green-200 bg-green-50 text-green-800 dark:border-green-800 dark:bg-green-950\/30 dark:text-green-300/g, + 'border-green-500/20 bg-green-500/10 text-green-600' +); +content = content.replace( + /border-red-200 bg-red-50 text-red-800 dark:border-red-800 dark:bg-red-950\/30 dark:text-red-300/g, + 'border-red-500/20 bg-red-500/10 text-red-600' +); + +// CardFooter specific layout fixes since it became a div +content = content.replace( + '
', + '
' +); // Might not match exact, let's fix manually if needed. + +// Wait, the CardFooter replacement was: content.replace(//g, '
'); +// Then there was which became
... Need to make sure it has padding. +content = content.replace( + '
', + '
' +); + +fs.writeFileSync(file, content); +console.log("Success"); diff --git a/memento-note/fix-card-footer.js b/memento-note/fix-card-footer.js new file mode 100644 index 0000000..d133cba --- /dev/null +++ b/memento-note/fix-card-footer.js @@ -0,0 +1,11 @@ +const fs = require('fs'); +const file = 'app/(admin)/admin/settings/admin-settings-form.tsx'; +let content = fs.readFileSync(file, 'utf-8'); + +// Replace any leftover tags +content = content.replace(//g, '
'); +content = content.replace(//g, '
'); +content = content.replace(/<\/CardFooter>/g, '
'); + +fs.writeFileSync(file, content); +console.log("Fixed CardFooters"); diff --git a/memento-note/fix-mcp-settings.js b/memento-note/fix-mcp-settings.js new file mode 100644 index 0000000..1c80fe8 --- /dev/null +++ b/memento-note/fix-mcp-settings.js @@ -0,0 +1,192 @@ +const fs = require('fs'); +const file = 'components/mcp/mcp-settings-panel.tsx'; +let content = fs.readFileSync(file, 'utf-8'); + +// Replace standard with our styled headers +// Section 1 +content = content.replace( + ` +
+ +
+

{t('mcpSettings.whatIsMcp.title')}

`, + `
+
+
+ +
+
+

{t('mcpSettings.whatIsMcp.title')}

+
+
+
` +); + +// We need to close the p-6 div, wait, the ends with . So we need to replace carefully. +// Let's do it section by section or just globally replace with
since we added a new wrapping div. +// For Section 1, the end is +content = content.replace( + ` + +
+
+
`, + ` + +
+
` +); // The original has
+ +// Section 2 +content = content.replace( + ` +
+ +

{t('mcpSettings.serverStatus.title')}

+
`, + `
+
+
+ +
+
+

{t('mcpSettings.serverStatus.title')}

+
+
+
` +); +content = content.replace( + `
+ )} +
+
`, + `
+ )} +
+
+

` +); + + +// Section 3 +content = content.replace( + ` +
+
+ +
+

{t('mcpSettings.apiKeys.title')}

+

+ {t('mcpSettings.apiKeys.description')} +

+
+
`, + `
+
+
+
+ +
+
+

{t('mcpSettings.apiKeys.title')}

+

+ {t('mcpSettings.apiKeys.description')} +

+
+
` +); + +content = content.replace( + ` ))} +
+ )} + `, + ` ))} +
+ )} +
` +); +// Wait! I forgot to add `
` after the header for Section 3! +// The header ended at + +
`, + ` + +
+
` +); + +// Section 4 +// Wait, Section 4 is a subcomponent! +content = content.replace( + ` +
+ +
+

+ {t('mcpSettings.configInstructions.title')} +

+

+ {t('mcpSettings.configInstructions.description')} +

+
+
`, + `
+
+
+ +
+
+

+ {t('mcpSettings.configInstructions.title')} +

+

+ {t('mcpSettings.configInstructions.description')} +

+
+
+
` +); + +content = content.replace( + ` ))} +
+ `, + ` ))} +
+
` +); +// In Section 4, remove `
` because I merged it into `
` +content = content.replace( + `
\n
`, + `
` +); + +// Fix colors +content = content.replace(/text-gray-500/g, 'text-muted-foreground'); +content = content.replace(/text-gray-600/g, 'text-muted-foreground'); +content = content.replace(/text-gray-400/g, 'text-muted-foreground'); +content = content.replace(/bg-gray-100 dark:bg-gray-800/g, 'bg-muted'); +content = content.replace(/bg-gray-50 dark:bg-gray-900/g, 'bg-muted/50'); +content = content.replace(/bg-gray-50/g, 'bg-muted/50'); +content = content.replace(/dark:hover:bg-gray-900/g, 'hover:bg-muted'); + +// Remove Card import +content = content.replace("import { Card } from '@/components/ui/card'", ""); + +// Grid layout for mcp-settings-panel root +content = content.replace( + '
', + '
' +); + +fs.writeFileSync(file, content); +console.log("Done"); diff --git a/memento-note/locales/fr.json b/memento-note/locales/fr.json index b86082f..163d0db 100644 --- a/memento-note/locales/fr.json +++ b/memento-note/locales/fr.json @@ -1092,6 +1092,8 @@ "tagsTestDescription": "Testez le fournisseur IA responsable des suggestions d\u0027étiquettes automatiques", "embeddingsTestTitle": "Test d\u0027embeddings", "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\u0027assistant de discussion", "howItWorksTitle": "Fonctionnement des tests", "tagsGenerationTest": "🏷️ Test de génération d\u0027étiquettes :", "tagsStep1": "Envoie une note exemple au fournisseur IA", @@ -1103,6 +1105,11 @@ "embeddingsStep2": "Génère une représentation vectorielle (liste de nombres)", "embeddingsStep3": "Affiche les dimensions de l\u0027embedding 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\u0027assistant", + "chatStep2": "Demande une réponse concise sur le rôle de l\u0027IA", + "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\u0027optimiser les coûts et les performances.", "provider": "Fournisseur :", "model": "Modèle :", diff --git a/memento-note/next.config.ts b/memento-note/next.config.ts index 9f91da0..3947b9d 100644 --- a/memento-note/next.config.ts +++ b/memento-note/next.config.ts @@ -26,11 +26,9 @@ const nextConfig: NextConfig = { // functions during concurrent transitions, amplifying timing issues. reactStrictMode: false, - // TEMP: disable Turbopack due React #310 loop on /admin routes in production builds. - // We keep webpack pipeline until upstream fix is confirmed. -}; -module.exports = { + // Allow development origins for HMR and Dev Server access + // @ts-ignore - Some NextConfig versions might require this in experimental allowedDevOrigins: ['192.168.1.83'], -} +}; export default nextConfig; diff --git a/memento-note/rewrite-ai-tabs.js b/memento-note/rewrite-ai-tabs.js new file mode 100644 index 0000000..36213f4 --- /dev/null +++ b/memento-note/rewrite-ai-tabs.js @@ -0,0 +1,71 @@ +const fs = require('fs'); +const file = 'app/(admin)/admin/settings/admin-settings-form.tsx'; +let content = fs.readFileSync(file, 'utf-8'); + +// 1. Add state variable +content = content.replace( + "const { t } = useLanguage()", + "const { t } = useLanguage()\n const [activeAiTab, setActiveAiTab] = useState<'tags' | 'embeddings' | 'chat'>('tags')" +); + +// 2. Add Tab Switcher UI and wrap each block +// Find the start of AI form +const aiFormStart = `
+ {/* Tags Generation Provider */} +
`; + +const newAiFormStart = `
+
+ + + +
+
+
+ {/* Tags Generation Provider */} +
`; + +content = content.replace(aiFormStart, newAiFormStart); + +// Now find Embeddings and Chat and wrap them in hidden condition +const embeddingsStart = `{/* Embeddings Provider */} +
`; +const newEmbeddingsStart = `{/* Embeddings Provider */} +
`; +content = content.replace(embeddingsStart, newEmbeddingsStart); + +const chatStart = `{/* Chat Provider */} +
`; +const newChatStart = `{/* Chat Provider */} +
`; +content = content.replace(chatStart, newChatStart); + +// 3. Fix button overflows everywhere. +// Find:
(AI) +content = content.replace( + '
', + '
' +); + +// Find:
(Email, Tools, Security?) +// Tools: +content = content.replace( + '
', + '
' +); +content = content.replace( + '
', + '
' +); + +// Email: +content = content.replace( + '
', + '
' +); + +// Security doesn't have justify-between, it just has
+ +// Write back +fs.writeFileSync(file, content); +console.log("Done");