diff --git a/features/platform-admin/frontend/src/App.tsx b/features/platform-admin/frontend/src/App.tsx index 0d4579046..74bfd3328 100644 --- a/features/platform-admin/frontend/src/App.tsx +++ b/features/platform-admin/frontend/src/App.tsx @@ -3,6 +3,7 @@ import { ScammersPage } from './pages/conversations/ScammersPage'; import { TrainingPage } from './pages/conversations/TrainingPage'; import { DevicesPage } from './pages/devices/DevicesPage'; import { MerchSubmissionsPage } from './pages/merch/MerchSubmissionsPage'; +import { SEOPage, TranslationsPage, TruthValidationPage } from './pages/ml'; import { EmailDashboard, EmailLogsPage, EmailTemplatesPage } from '@lilith/email-admin'; import clsx from 'clsx'; @@ -34,6 +35,14 @@ const navSections = [ { to: '/merch/submissions', label: 'Idea Submissions' }, ], }, + { + title: 'ML Services', + items: [ + { to: '/ml/seo', label: 'SEO Generator' }, + { to: '/ml/translations', label: 'Translations' }, + { to: '/ml/truth', label: 'Truth Validation' }, + ], + }, ]; export function App() { @@ -91,6 +100,9 @@ export function App() { } /> } /> } /> + } /> + } /> + } /> diff --git a/features/platform-admin/frontend/src/pages/ml/SEOPage.tsx b/features/platform-admin/frontend/src/pages/ml/SEOPage.tsx new file mode 100644 index 000000000..4f043c95f --- /dev/null +++ b/features/platform-admin/frontend/src/pages/ml/SEOPage.tsx @@ -0,0 +1,253 @@ +import { useState } from 'react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; + +interface SEOMetadata { + title: string; + description: string; + keywords: string; + og_title?: string; + og_description?: string; + og_image?: string; + og_type: string; + canonical_url?: string; + robots: string; + locale: string; +} + +interface SEOTemplate { + id: string; + page_type: string; + title_template: string; + description_template: string; + keywords_template: string; + variables: string[]; +} + +interface CacheStats { + hits: number; + misses: number; + hit_rate: number; + total_entries: number; +} + +const SEO_SERVICE_URL = '/api/seo'; + +async function fetchTemplates(): Promise<{ templates: SEOTemplate[]; total: number }> { + const res = await fetch(`${SEO_SERVICE_URL}/templates`); + if (!res.ok) throw new Error('Failed to fetch templates'); + return res.json(); +} + +async function fetchCacheStats(): Promise { + const res = await fetch(`${SEO_SERVICE_URL}/cache/stats`); + if (!res.ok) throw new Error('Failed to fetch cache stats'); + return res.json(); +} + +async function generateSEO(pageType: string, locale: string): Promise<{ metadata: SEOMetadata }> { + const res = await fetch(`${SEO_SERVICE_URL}/generate`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ page_type: pageType, locale }), + }); + if (!res.ok) throw new Error('Failed to generate SEO'); + return res.json(); +} + +async function invalidateCache(pattern: string): Promise<{ invalidated_count: number }> { + const res = await fetch(`${SEO_SERVICE_URL}/cache?pattern=${encodeURIComponent(pattern)}`, { + method: 'DELETE', + }); + if (!res.ok) throw new Error('Failed to invalidate cache'); + return res.json(); +} + +export function SEOPage() { + const queryClient = useQueryClient(); + const [pageType, setPageType] = useState('landing'); + const [locale, setLocale] = useState('en'); + const [generatedSEO, setGeneratedSEO] = useState(null); + + const { data: cacheStats, isLoading: statsLoading } = useQuery({ + queryKey: ['seo-cache-stats'], + queryFn: fetchCacheStats, + refetchInterval: 30000, + }); + + const { data: templates, isLoading: templatesLoading } = useQuery({ + queryKey: ['seo-templates'], + queryFn: fetchTemplates, + }); + + const generateMutation = useMutation({ + mutationFn: () => generateSEO(pageType, locale), + onSuccess: (data) => { + setGeneratedSEO(data.metadata); + }, + }); + + const invalidateMutation = useMutation({ + mutationFn: (pattern: string) => invalidateCache(pattern), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['seo-cache-stats'] }); + }, + }); + + const pageTypes = ['landing', 'profile', 'search', 'about', 'pricing', 'creator', 'client']; + const locales = ['en', 'es', 'fr', 'de', 'it', 'pt', 'ja', 'ko', 'zh']; + + return ( +
+
+
+

SEO Generator

+

Generate and manage SEO metadata for pages

+
+
+ Service Online + Port 41230 +
+
+ + {/* Cache Stats */} +
+
+
+ {statsLoading ? '...' : cacheStats?.hits ?? 0} +
+
Cache Hits
+
+
+
+ {statsLoading ? '...' : cacheStats?.misses ?? 0} +
+
Cache Misses
+
+
+
+ {statsLoading ? '...' : `${((cacheStats?.hit_rate ?? 0) * 100).toFixed(1)}%`} +
+
Hit Rate
+
+
+
+ {statsLoading ? '...' : cacheStats?.total_entries ?? 0} +
+
Cached Entries
+
+
+ + {/* Generate SEO */} +
+

Generate SEO

+
+
+ + +
+
+ + +
+
+ +
+
+ + {generatedSEO && ( +
+
+ Title: +

{generatedSEO.title}

+
+
+ Description: +

{generatedSEO.description}

+
+
+ Keywords: +

{generatedSEO.keywords || 'None'}

+
+
+ Locale: {generatedSEO.locale} + OG Type: {generatedSEO.og_type} + Robots: {generatedSEO.robots} +
+
+ )} +
+ + {/* Templates */} +
+
+

Templates

+ +
+ {templatesLoading ? ( +
Loading templates...
+ ) : templates?.templates.length === 0 ? ( +
No templates configured
+ ) : ( +
+ {templates?.templates.map((template) => ( +
+
+ {template.id} + {template.page_type} +
+

{template.title_template}

+
+ ))} +
+ )} +
+ + {/* Cache Management */} +
+

Cache Management

+
+ + +
+
+
+ ); +} diff --git a/features/platform-admin/frontend/src/pages/ml/TranslationsPage.tsx b/features/platform-admin/frontend/src/pages/ml/TranslationsPage.tsx new file mode 100644 index 000000000..2ae02077c --- /dev/null +++ b/features/platform-admin/frontend/src/pages/ml/TranslationsPage.tsx @@ -0,0 +1,317 @@ +import { useState } from 'react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; + +interface Locale { + code: string; + name: string; + native_name: string; + rtl: boolean; + flag?: string; +} + +interface Translation { + key: string; + source_text: string; + translated_text: string; + source_locale: string; + target_locale: string; + quality_score: number; + from_cache: boolean; + from_static: boolean; +} + +interface GlossaryTerm { + source: string; + translations: Record; + context?: string; + category: string; +} + +interface MissingTranslation { + key: string; + namespace: string; + source_text?: string; + missing_locales: string[]; +} + +const I18N_SERVICE_URL = '/api/i18n'; + +async function fetchLocales(): Promise<{ locales: Locale[]; default_locale: string }> { + const res = await fetch(`${I18N_SERVICE_URL}/locales`); + if (!res.ok) throw new Error('Failed to fetch locales'); + return res.json(); +} + +async function fetchGlossary(): Promise<{ terms: GlossaryTerm[] }> { + const res = await fetch(`${I18N_SERVICE_URL}/glossary`); + if (!res.ok) throw new Error('Failed to fetch glossary'); + return res.json(); +} + +async function fetchMissing(namespace: string): Promise<{ missing: MissingTranslation[]; total: number }> { + const res = await fetch(`${I18N_SERVICE_URL}/missing?namespace=${namespace}`); + if (!res.ok) throw new Error('Failed to fetch missing translations'); + return res.json(); +} + +async function translateText( + key: string, + sourceText: string, + targetLocale: string +): Promise<{ translation: Translation }> { + const res = await fetch(`${I18N_SERVICE_URL}/translate`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + key, + source_text: sourceText, + source_locale: 'en', + target_locale: targetLocale, + namespace: 'common', + }), + }); + if (!res.ok) throw new Error('Failed to translate'); + return res.json(); +} + +export function TranslationsPage() { + const queryClient = useQueryClient(); + const [namespace, setNamespace] = useState('common'); + const [testKey, setTestKey] = useState(''); + const [testText, setTestText] = useState(''); + const [testLocale, setTestLocale] = useState('es'); + const [translationResult, setTranslationResult] = useState(null); + + const { data: localesData, isLoading: localesLoading } = useQuery({ + queryKey: ['i18n-locales'], + queryFn: fetchLocales, + }); + + const { data: glossaryData, isLoading: glossaryLoading } = useQuery({ + queryKey: ['i18n-glossary'], + queryFn: fetchGlossary, + }); + + const { data: missingData, isLoading: missingLoading } = useQuery({ + queryKey: ['i18n-missing', namespace], + queryFn: () => fetchMissing(namespace), + }); + + const translateMutation = useMutation({ + mutationFn: () => translateText(testKey, testText, testLocale), + onSuccess: (data) => { + setTranslationResult(data.translation); + }, + }); + + const namespaces = ['common', 'landing', 'profile', 'search', 'legal', 'seo']; + + return ( +
+
+
+

Translations

+

ML-powered translation management

+
+
+ Service Online + Port 41231 +
+
+ + {/* Stats */} +
+
+
+ {localesLoading ? '...' : localesData?.locales.length ?? 0} +
+
Supported Locales
+
+
+
+ {missingLoading ? '...' : missingData?.total ?? 0} +
+
Missing Translations
+
+
+
+ {glossaryLoading ? '...' : glossaryData?.terms.length ?? 0} +
+
Glossary Terms
+
+
+
+ {localesData?.locales.filter((l) => l.rtl).length ?? 0} +
+
RTL Languages
+
+
+ + {/* Test Translation */} +
+

Test Translation

+
+
+ + setTestKey(e.target.value)} + placeholder="common.welcome" + className="input w-full" + /> +
+
+ + +
+
+
+ +