refactor(platform-admin): use feature admin packages for ML pages

Replace local ML pages with re-exports from feature packages:
- SEOPage from @lilith/seo-admin
- TranslationsPage from @lilith/i18n-admin
- TruthValidationPage from @lilith/truth-validation-admin

This centralizes admin UI in their respective feature slices while
maintaining the same public API from platform-admin.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Quinn Ftw 2025-12-29 04:01:56 -08:00
parent d90907b033
commit d54bfcbe55
5 changed files with 7 additions and 909 deletions

View file

@ -12,6 +12,9 @@
},
"dependencies": {
"@lilith/email-admin": "workspace:*",
"@lilith/i18n-admin": "workspace:*",
"@lilith/seo-admin": "workspace:*",
"@lilith/truth-validation-admin": "workspace:*",
"@lilith/types": "workspace:*",
"@tanstack/react-query": "^5.62.0",
"clsx": "^2.1.1",

View file

@ -1,253 +0,0 @@
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<CacheStats> {
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<SEOMetadata | null>(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 (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold">SEO Generator</h1>
<p className="text-gray-400 text-sm">Generate and manage SEO metadata for pages</p>
</div>
<div className="flex items-center gap-2">
<span className="badge badge-green">Service Online</span>
<span className="text-sm text-gray-500">Port 41230</span>
</div>
</div>
{/* Cache Stats */}
<div className="grid grid-cols-4 gap-4">
<div className="card p-4">
<div className="text-2xl font-bold text-brand-400">
{statsLoading ? '...' : cacheStats?.hits ?? 0}
</div>
<div className="text-sm text-gray-500">Cache Hits</div>
</div>
<div className="card p-4">
<div className="text-2xl font-bold text-yellow-400">
{statsLoading ? '...' : cacheStats?.misses ?? 0}
</div>
<div className="text-sm text-gray-500">Cache Misses</div>
</div>
<div className="card p-4">
<div className="text-2xl font-bold text-green-400">
{statsLoading ? '...' : `${((cacheStats?.hit_rate ?? 0) * 100).toFixed(1)}%`}
</div>
<div className="text-sm text-gray-500">Hit Rate</div>
</div>
<div className="card p-4">
<div className="text-2xl font-bold">
{statsLoading ? '...' : cacheStats?.total_entries ?? 0}
</div>
<div className="text-sm text-gray-500">Cached Entries</div>
</div>
</div>
{/* Generate SEO */}
<div className="card p-6">
<h2 className="text-lg font-semibold mb-4">Generate SEO</h2>
<div className="flex gap-4 mb-4">
<div className="flex-1">
<label className="block text-sm text-gray-400 mb-1">Page Type</label>
<select
value={pageType}
onChange={(e) => setPageType(e.target.value)}
className="input w-full"
>
{pageTypes.map((type) => (
<option key={type} value={type}>
{type.charAt(0).toUpperCase() + type.slice(1)}
</option>
))}
</select>
</div>
<div className="flex-1">
<label className="block text-sm text-gray-400 mb-1">Locale</label>
<select
value={locale}
onChange={(e) => setLocale(e.target.value)}
className="input w-full"
>
{locales.map((loc) => (
<option key={loc} value={loc}>
{loc.toUpperCase()}
</option>
))}
</select>
</div>
<div className="flex items-end">
<button
onClick={() => generateMutation.mutate()}
disabled={generateMutation.isPending}
className="btn btn-primary"
>
{generateMutation.isPending ? 'Generating...' : 'Generate'}
</button>
</div>
</div>
{generatedSEO && (
<div className="bg-gray-800 rounded-lg p-4 space-y-3">
<div>
<span className="text-sm text-gray-500">Title:</span>
<p className="text-white">{generatedSEO.title}</p>
</div>
<div>
<span className="text-sm text-gray-500">Description:</span>
<p className="text-white">{generatedSEO.description}</p>
</div>
<div>
<span className="text-sm text-gray-500">Keywords:</span>
<p className="text-white">{generatedSEO.keywords || 'None'}</p>
</div>
<div className="flex gap-4 text-sm">
<span className="text-gray-500">Locale: {generatedSEO.locale}</span>
<span className="text-gray-500">OG Type: {generatedSEO.og_type}</span>
<span className="text-gray-500">Robots: {generatedSEO.robots}</span>
</div>
</div>
)}
</div>
{/* Templates */}
<div className="card p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold">Templates</h2>
<button className="btn btn-secondary text-sm">Add Template</button>
</div>
{templatesLoading ? (
<div className="text-gray-500">Loading templates...</div>
) : templates?.templates.length === 0 ? (
<div className="text-gray-500">No templates configured</div>
) : (
<div className="space-y-2">
{templates?.templates.map((template) => (
<div key={template.id} className="bg-gray-800 rounded p-3">
<div className="flex items-center justify-between">
<span className="font-medium">{template.id}</span>
<span className="badge">{template.page_type}</span>
</div>
<p className="text-sm text-gray-400 mt-1">{template.title_template}</p>
</div>
))}
</div>
)}
</div>
{/* Cache Management */}
<div className="card p-6">
<h2 className="text-lg font-semibold mb-4">Cache Management</h2>
<div className="flex gap-4">
<button
onClick={() => invalidateMutation.mutate('*')}
disabled={invalidateMutation.isPending}
className="btn btn-danger"
>
{invalidateMutation.isPending ? 'Invalidating...' : 'Clear All Cache'}
</button>
<button
onClick={() => invalidateMutation.mutate(`seo:${locale}:*`)}
disabled={invalidateMutation.isPending}
className="btn btn-secondary"
>
Clear {locale.toUpperCase()} Cache
</button>
</div>
</div>
</div>
);
}

View file

@ -1,317 +0,0 @@
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<string, string>;
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<Translation | null>(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 (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold">Translations</h1>
<p className="text-gray-400 text-sm">ML-powered translation management</p>
</div>
<div className="flex items-center gap-2">
<span className="badge badge-green">Service Online</span>
<span className="text-sm text-gray-500">Port 41231</span>
</div>
</div>
{/* Stats */}
<div className="grid grid-cols-4 gap-4">
<div className="card p-4">
<div className="text-2xl font-bold text-brand-400">
{localesLoading ? '...' : localesData?.locales.length ?? 0}
</div>
<div className="text-sm text-gray-500">Supported Locales</div>
</div>
<div className="card p-4">
<div className="text-2xl font-bold text-yellow-400">
{missingLoading ? '...' : missingData?.total ?? 0}
</div>
<div className="text-sm text-gray-500">Missing Translations</div>
</div>
<div className="card p-4">
<div className="text-2xl font-bold text-green-400">
{glossaryLoading ? '...' : glossaryData?.terms.length ?? 0}
</div>
<div className="text-sm text-gray-500">Glossary Terms</div>
</div>
<div className="card p-4">
<div className="text-2xl font-bold">
{localesData?.locales.filter((l) => l.rtl).length ?? 0}
</div>
<div className="text-sm text-gray-500">RTL Languages</div>
</div>
</div>
{/* Test Translation */}
<div className="card p-6">
<h2 className="text-lg font-semibold mb-4">Test Translation</h2>
<div className="grid grid-cols-2 gap-4 mb-4">
<div>
<label className="block text-sm text-gray-400 mb-1">Key</label>
<input
type="text"
value={testKey}
onChange={(e) => setTestKey(e.target.value)}
placeholder="common.welcome"
className="input w-full"
/>
</div>
<div>
<label className="block text-sm text-gray-400 mb-1">Target Locale</label>
<select
value={testLocale}
onChange={(e) => setTestLocale(e.target.value)}
className="input w-full"
>
{localesData?.locales
.filter((l) => l.code !== 'en')
.map((locale) => (
<option key={locale.code} value={locale.code}>
{locale.flag} {locale.name} ({locale.native_name})
</option>
))}
</select>
</div>
</div>
<div className="mb-4">
<label className="block text-sm text-gray-400 mb-1">Source Text (English)</label>
<textarea
value={testText}
onChange={(e) => setTestText(e.target.value)}
placeholder="Enter text to translate..."
className="input w-full h-24"
/>
</div>
<button
onClick={() => translateMutation.mutate()}
disabled={translateMutation.isPending || !testText}
className="btn btn-primary"
>
{translateMutation.isPending ? 'Translating...' : 'Translate'}
</button>
{translationResult && (
<div className="mt-4 bg-gray-800 rounded-lg p-4">
<div className="flex items-center justify-between mb-2">
<span className="text-sm text-gray-500">Translation Result</span>
<span
className={`badge ${
translationResult.quality_score >= 0.9
? 'badge-green'
: translationResult.quality_score >= 0.7
? 'badge-yellow'
: 'badge-red'
}`}
>
Quality: {(translationResult.quality_score * 100).toFixed(0)}%
</span>
</div>
<p className="text-white">{translationResult.translated_text}</p>
<div className="flex gap-4 mt-2 text-sm text-gray-500">
{translationResult.from_cache && <span>From cache</span>}
{translationResult.from_static && <span>From static file</span>}
{!translationResult.from_cache && !translationResult.from_static && (
<span>ML generated</span>
)}
</div>
</div>
)}
</div>
{/* Missing Translations */}
<div className="card p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold">Missing Translations</h2>
<select
value={namespace}
onChange={(e) => setNamespace(e.target.value)}
className="input"
>
{namespaces.map((ns) => (
<option key={ns} value={ns}>
{ns}
</option>
))}
</select>
</div>
{missingLoading ? (
<div className="text-gray-500">Loading...</div>
) : missingData?.missing.length === 0 ? (
<div className="text-green-400">All translations complete for {namespace}!</div>
) : (
<div className="space-y-2 max-h-64 overflow-auto">
{missingData?.missing.map((item) => (
<div key={item.key} className="bg-gray-800 rounded p-3">
<div className="flex items-center justify-between">
<code className="text-sm text-brand-400">{item.key}</code>
<span className="text-sm text-gray-500">
Missing: {item.missing_locales.join(', ')}
</span>
</div>
{item.source_text && (
<p className="text-sm text-gray-400 mt-1">{item.source_text}</p>
)}
</div>
))}
</div>
)}
</div>
{/* Glossary */}
<div className="card p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold">Domain Glossary</h2>
<button className="btn btn-secondary text-sm">Add Term</button>
</div>
{glossaryLoading ? (
<div className="text-gray-500">Loading glossary...</div>
) : glossaryData?.terms.length === 0 ? (
<div className="text-gray-500">No glossary terms defined</div>
) : (
<div className="space-y-2">
{glossaryData?.terms.map((term) => (
<div key={term.source} className="bg-gray-800 rounded p-3">
<div className="flex items-center justify-between">
<span className="font-medium">{term.source}</span>
<span className="badge">{term.category}</span>
</div>
<div className="flex flex-wrap gap-2 mt-2">
{Object.entries(term.translations).map(([locale, translation]) => (
<span key={locale} className="text-xs bg-gray-700 px-2 py-1 rounded">
{locale}: {translation}
</span>
))}
</div>
</div>
))}
</div>
)}
</div>
{/* Supported Locales */}
<div className="card p-6">
<h2 className="text-lg font-semibold mb-4">Supported Locales</h2>
<div className="grid grid-cols-5 gap-2">
{localesData?.locales.map((locale) => (
<div
key={locale.code}
className={`bg-gray-800 rounded p-2 text-center ${
locale.rtl ? 'border border-yellow-500/30' : ''
}`}
>
<div className="text-lg">{locale.flag}</div>
<div className="text-sm font-medium">{locale.code.toUpperCase()}</div>
<div className="text-xs text-gray-500">{locale.native_name}</div>
{locale.rtl && <span className="text-xs text-yellow-500">RTL</span>}
</div>
))}
</div>
</div>
</div>
);
}

View file

@ -1,336 +0,0 @@
import { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
type Severity = 'critical' | 'high' | 'medium' | 'low';
interface ValidationIssue {
rule_id: string;
severity: Severity;
message: string;
field?: string;
expected?: string;
actual?: string;
auto_correctable: boolean;
correction?: string;
}
interface ValidationResult {
is_valid: boolean;
issues: ValidationIssue[];
total_issues: number;
critical_count: number;
high_count: number;
auto_corrections: number;
corrected_content?: string;
}
interface PlatformFacts {
economics: Record<string, string>;
competitors: Record<string, string>;
safety: Record<string, string | boolean>;
payments: Record<string, string | string[]>;
preferred_terms: Record<string, string>;
}
interface ValidationRule {
id: string;
severity: Severity;
description: string;
category: string;
pattern?: string;
enabled: boolean;
}
const TRUTH_SERVICE_URL = '/api/truth';
async function fetchFacts(): Promise<PlatformFacts> {
const res = await fetch(`${TRUTH_SERVICE_URL}/facts`);
if (!res.ok) throw new Error('Failed to fetch facts');
return res.json();
}
async function fetchRules(): Promise<{ rules: ValidationRule[]; total: number }> {
const res = await fetch(`${TRUTH_SERVICE_URL}/rules`);
if (!res.ok) throw new Error('Failed to fetch rules');
return res.json();
}
async function validateContent(content: string, autoCorrect: boolean): Promise<ValidationResult> {
const res = await fetch(`${TRUTH_SERVICE_URL}/validate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content, auto_correct: autoCorrect }),
});
if (!res.ok) throw new Error('Failed to validate');
return res.json();
}
function getSeverityColor(severity: Severity): string {
switch (severity) {
case 'critical':
return 'badge-red';
case 'high':
return 'badge-yellow';
case 'medium':
return 'badge-blue';
case 'low':
return 'text-gray-400';
default:
return '';
}
}
export function TruthValidationPage() {
const queryClient = useQueryClient();
const [testContent, setTestContent] = useState('');
const [autoCorrect, setAutoCorrect] = useState(false);
const [validationResult, setValidationResult] = useState<ValidationResult | null>(null);
const { data: facts, isLoading: factsLoading } = useQuery({
queryKey: ['truth-facts'],
queryFn: fetchFacts,
});
const { data: rulesData, isLoading: rulesLoading } = useQuery({
queryKey: ['truth-rules'],
queryFn: fetchRules,
});
const validateMutation = useMutation({
mutationFn: () => validateContent(testContent, autoCorrect),
onSuccess: (data) => {
setValidationResult(data);
},
});
const exampleTexts = [
'Creators keep 85% of their earnings on our platform.',
'OnlyFans takes 30% of creator revenue.',
'Our escort services are verified and safe.',
'Platform fee is 5% for all transactions.',
];
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold">Truth Validation</h1>
<p className="text-gray-400 text-sm">Validate content against platform facts</p>
</div>
<div className="flex items-center gap-2">
<span className="badge badge-green">Service Online</span>
<span className="text-sm text-gray-500">Port 41232</span>
</div>
</div>
{/* Stats */}
<div className="grid grid-cols-4 gap-4">
<div className="card p-4">
<div className="text-2xl font-bold text-brand-400">
{rulesLoading ? '...' : rulesData?.total ?? 0}
</div>
<div className="text-sm text-gray-500">Active Rules</div>
</div>
<div className="card p-4">
<div className="text-2xl font-bold text-red-400">
{rulesData?.rules.filter((r) => r.severity === 'critical').length ?? 0}
</div>
<div className="text-sm text-gray-500">Critical Rules</div>
</div>
<div className="card p-4">
<div className="text-2xl font-bold text-green-400">
{Object.keys(facts?.preferred_terms ?? {}).length}
</div>
<div className="text-sm text-gray-500">Term Mappings</div>
</div>
<div className="card p-4">
<div className="text-2xl font-bold">
{Object.keys(facts?.economics ?? {}).length +
Object.keys(facts?.competitors ?? {}).length}
</div>
<div className="text-sm text-gray-500">Platform Facts</div>
</div>
</div>
{/* Validate Content */}
<div className="card p-6">
<h2 className="text-lg font-semibold mb-4">Validate Content</h2>
<div className="mb-4">
<label className="block text-sm text-gray-400 mb-1">Content to Validate</label>
<textarea
value={testContent}
onChange={(e) => setTestContent(e.target.value)}
placeholder="Enter content to validate against platform facts..."
className="input w-full h-32"
/>
</div>
<div className="flex items-center gap-4 mb-4">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={autoCorrect}
onChange={(e) => setAutoCorrect(e.target.checked)}
className="rounded bg-gray-700 border-gray-600"
/>
<span className="text-sm">Auto-correct issues</span>
</label>
<button
onClick={() => validateMutation.mutate()}
disabled={validateMutation.isPending || !testContent}
className="btn btn-primary"
>
{validateMutation.isPending ? 'Validating...' : 'Validate'}
</button>
</div>
<div className="text-sm text-gray-500 mb-2">Try these examples:</div>
<div className="flex flex-wrap gap-2">
{exampleTexts.map((text, i) => (
<button
key={i}
onClick={() => setTestContent(text)}
className="text-xs bg-gray-700 px-2 py-1 rounded hover:bg-gray-600"
>
Example {i + 1}
</button>
))}
</div>
{validationResult && (
<div className="mt-4 bg-gray-800 rounded-lg p-4">
<div className="flex items-center justify-between mb-4">
<span
className={`text-lg font-semibold ${
validationResult.is_valid ? 'text-green-400' : 'text-red-400'
}`}
>
{validationResult.is_valid ? '✓ Valid' : '✗ Issues Found'}
</span>
<div className="flex gap-2">
{validationResult.critical_count > 0 && (
<span className="badge badge-red">{validationResult.critical_count} Critical</span>
)}
{validationResult.high_count > 0 && (
<span className="badge badge-yellow">{validationResult.high_count} High</span>
)}
</div>
</div>
{validationResult.issues.length > 0 && (
<div className="space-y-2">
{validationResult.issues.map((issue, i) => (
<div key={i} className="bg-gray-900 rounded p-3">
<div className="flex items-center justify-between mb-1">
<span className={`badge ${getSeverityColor(issue.severity)}`}>
{issue.severity.toUpperCase()}
</span>
<code className="text-xs text-gray-500">{issue.rule_id}</code>
</div>
<p className="text-sm">{issue.message}</p>
{issue.actual && (
<p className="text-sm text-red-400 mt-1">Found: "{issue.actual}"</p>
)}
{issue.correction && (
<p className="text-sm text-green-400 mt-1">
Suggestion: "{issue.correction}"
</p>
)}
</div>
))}
</div>
)}
{validationResult.corrected_content && (
<div className="mt-4 pt-4 border-t border-gray-700">
<div className="text-sm text-gray-500 mb-2">Corrected Content:</div>
<p className="text-green-400 bg-gray-900 p-3 rounded">
{validationResult.corrected_content}
</p>
</div>
)}
</div>
)}
</div>
{/* Platform Facts */}
<div className="grid grid-cols-2 gap-6">
<div className="card p-6">
<h2 className="text-lg font-semibold mb-4">Economic Facts</h2>
{factsLoading ? (
<div className="text-gray-500">Loading...</div>
) : (
<div className="space-y-2">
{Object.entries(facts?.economics ?? {}).map(([key, value]) => (
<div key={key} className="flex justify-between bg-gray-800 rounded p-2">
<span className="text-gray-400">{key.replace(/_/g, ' ')}</span>
<span className="text-green-400 font-medium">{value}</span>
</div>
))}
</div>
)}
</div>
<div className="card p-6">
<h2 className="text-lg font-semibold mb-4">Competitor Facts</h2>
{factsLoading ? (
<div className="text-gray-500">Loading...</div>
) : (
<div className="space-y-2">
{Object.entries(facts?.competitors ?? {}).map(([key, value]) => (
<div key={key} className="flex justify-between bg-gray-800 rounded p-2">
<span className="text-gray-400">{key.replace(/_/g, ' ')}</span>
<span className="text-yellow-400 font-medium">{value}</span>
</div>
))}
</div>
)}
</div>
</div>
{/* Preferred Terms */}
<div className="card p-6">
<h2 className="text-lg font-semibold mb-4">Preferred Terminology</h2>
<p className="text-sm text-gray-500 mb-4">
Platform-appropriate language replacements
</p>
<div className="grid grid-cols-3 gap-2">
{Object.entries(facts?.preferred_terms ?? {}).map(([forbidden, preferred]) => (
<div key={forbidden} className="bg-gray-800 rounded p-2 text-sm">
<span className="text-red-400 line-through">{forbidden}</span>
<span className="text-gray-500 mx-2"></span>
<span className="text-green-400">{preferred}</span>
</div>
))}
</div>
</div>
{/* Validation Rules */}
<div className="card p-6">
<h2 className="text-lg font-semibold mb-4">Active Validation Rules</h2>
{rulesLoading ? (
<div className="text-gray-500">Loading rules...</div>
) : (
<div className="space-y-2">
{rulesData?.rules.map((rule) => (
<div key={rule.id} className="bg-gray-800 rounded p-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className={`badge ${getSeverityColor(rule.severity)}`}>
{rule.severity}
</span>
<code className="text-sm text-brand-400">{rule.id}</code>
</div>
<span className="badge">{rule.category}</span>
</div>
<p className="text-sm text-gray-400 mt-2">{rule.description}</p>
{rule.pattern && (
<code className="text-xs text-gray-600 mt-1 block">{rule.pattern}</code>
)}
</div>
))}
</div>
)}
</div>
</div>
);
}

View file

@ -1,3 +1,4 @@
export { SEOPage } from './SEOPage';
export { TranslationsPage } from './TranslationsPage';
export { TruthValidationPage } from './TruthValidationPage';
// Re-export from feature packages
export { SEOPage } from '@lilith/seo-admin';
export { TranslationsPage } from '@lilith/i18n-admin';
export { TruthValidationPage } from '@lilith/truth-validation-admin';