docs(conversation-assistant): 📝 Refactor frontend UI components and test mocks for dashboard, processing, training, device management, and red flag moderation configurations

This commit is contained in:
Lilith 2026-01-22 23:03:10 -08:00
parent 29d86a5507
commit c45c72f600
15 changed files with 133 additions and 119 deletions

View file

@ -1,7 +1,9 @@
import styled from '@lilith/ui-styled-components';
import type { ThemeInterface } from '@lilith/ui-theme';
import { Spinner } from '@lilith/ui-primitives';
import styled from '@lilith/ui-styled-components';
import { Database, Server, Cpu, CheckCircle, AlertCircle, XCircle } from 'lucide-react';
import type { ThemeInterface } from '@lilith/ui-theme';
import { useClassificationStats, useMLSuggestions, useHealthStatus } from '@/api';
import { ClassificationBadge } from '@/components/ClassificationBadge';
@ -238,13 +240,13 @@ const HealthIcon = styled.div<{ $status: string }>`
height: 40px;
border-radius: 8px;
background-color: ${(props) => {
if (props.$status === 'ok') return 'rgba(34, 197, 94, 0.15)';
if (props.$status === 'unavailable' || props.$status === 'unhealthy') return 'rgba(239, 68, 68, 0.15)';
if (props.$status === 'ok') {return 'rgba(34, 197, 94, 0.15)';}
if (props.$status === 'unavailable' || props.$status === 'unhealthy') {return 'rgba(239, 68, 68, 0.15)';}
return 'rgba(156, 163, 175, 0.15)';
}};
color: ${(props) => {
if (props.$status === 'ok') return (props.theme as ThemeInterface).colors.success.main;
if (props.$status === 'unavailable' || props.$status === 'unhealthy') return (props.theme as ThemeInterface).colors.error.main;
if (props.$status === 'ok') {return (props.theme as ThemeInterface).colors.success.main;}
if (props.$status === 'unavailable' || props.$status === 'unhealthy') {return (props.theme as ThemeInterface).colors.error.main;}
return (props.theme as ThemeInterface).colors.text.secondary;
}};
`;
@ -262,8 +264,8 @@ const HealthServiceName = styled.div`
const HealthServiceStatus = styled.div<{ $status: string }>`
font-size: 12px;
color: ${(props) => {
if (props.$status === 'ok') return (props.theme as ThemeInterface).colors.success.main;
if (props.$status === 'unavailable' || props.$status === 'unhealthy') return (props.theme as ThemeInterface).colors.error.main;
if (props.$status === 'ok') {return (props.theme as ThemeInterface).colors.success.main;}
if (props.$status === 'unavailable' || props.$status === 'unhealthy') {return (props.theme as ThemeInterface).colors.error.main;}
return (props.theme as ThemeInterface).colors.text.secondary;
}};
`;
@ -279,8 +281,8 @@ const HealthMeta = styled.div`
`;
function getStatusIcon(status: string) {
if (status === 'ok') return <CheckCircle size={20} aria-hidden="true" />;
if (status === 'unavailable' || status === 'unhealthy') return <XCircle size={20} aria-hidden="true" />;
if (status === 'ok') {return <CheckCircle size={20} aria-hidden="true" />;}
if (status === 'unavailable' || status === 'unhealthy') {return <XCircle size={20} aria-hidden="true" />;}
return <AlertCircle size={20} aria-hidden="true" />;
}
@ -289,12 +291,12 @@ function formatUptime(seconds: number): string {
const hours = Math.floor((seconds % 86400) / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
if (days > 0) return `${days}d ${hours}h`;
if (hours > 0) return `${hours}h ${minutes}m`;
if (days > 0) {return `${days}d ${hours}h`;}
if (hours > 0) {return `${hours}h ${minutes}m`;}
return `${minutes}m`;
}
export function DashboardPage() {
export const DashboardPage = () => {
const { data: stats, isLoading: statsLoading } = useClassificationStats();
const { data: suggestions } = useMLSuggestions(5);
const { data: health } = useHealthStatus();

View file

@ -1,13 +1,17 @@
import { useState } from 'react';
import { Loader2, RefreshCw, AlertTriangle } from 'lucide-react';
import { useToast } from '@lilith/ui-feedback';
import styled, { keyframes } from '@lilith/ui-styled-components';
import { Loader2, RefreshCw, AlertTriangle } from 'lucide-react';
import type { ThemeInterface } from '@lilith/ui-theme';
import {
useProcessingStats,
useProcessMessages,
useReprocessAllMessages,
} from '@/api';
import { useToast } from '@lilith/ui-feedback';
const spin = keyframes`
from { transform: rotate(0deg); }
@ -228,7 +232,7 @@ const SecondaryButton = styled.button`
}
`;
export function ProcessingPage() {
export const ProcessingPage = () => {
const { data: stats, isLoading: statsLoading } = useProcessingStats();
const processMessages = useProcessMessages();
const reprocessAll = useReprocessAllMessages();

View file

@ -1,7 +1,9 @@
import styled from '@lilith/ui-styled-components';
import type { ThemeInterface } from '@lilith/ui-theme';
import { CheckSquare } from 'lucide-react';
import { Spinner } from '@lilith/ui-primitives';
import styled from '@lilith/ui-styled-components';
import { CheckSquare } from 'lucide-react';
import type { ThemeInterface } from '@lilith/ui-theme';
import {
useMLSuggestions,
useAcceptMLSuggestion,
@ -83,7 +85,7 @@ const EmptyText = styled.p`
margin: 0;
`;
export function ReviewQueuePage() {
export const ReviewQueuePage = () => {
const { data: suggestions, isLoading } = useMLSuggestions(50);
const acceptMutation = useAcceptMLSuggestion();
const rejectMutation = useRejectMLSuggestion();

View file

@ -1,8 +1,9 @@
import { Link } from '@lilith/ui-router';
import styled from '@lilith/ui-styled-components';
import type { ThemeInterface } from '@lilith/ui-theme';
import { Palette, ShieldAlert, BookOpen, ChevronRight } from 'lucide-react';
import type { ThemeInterface } from '@lilith/ui-theme';
const Container = styled.div`
max-width: 900px;
`;
@ -90,8 +91,7 @@ const Arrow = styled(ChevronRight)`
}
`;
export function SettingsPage() {
return (
export const SettingsPage = () => (
<Container>
<Header>
<h1>Settings</h1>
@ -145,5 +145,4 @@ export function SettingsPage() {
</Card>
</Grid>
</Container>
);
}
)

View file

@ -1,18 +1,20 @@
import { useParams, Link } from '@lilith/ui-router';
import { useQueryClient } from '@tanstack/react-query';
import { ArrowLeft } from 'lucide-react';
import styles from './StudioPage.module.css';
import {
useConversation,
useMessagesAll,
useConversationPrimer,
} from '@/api';
import { useAutoRefreshPrimer } from '@/hooks/useAutoRefreshPrimer';
import { StudioLayout } from '@/components/studio/StudioLayout';
import { MessagesPanel } from '@/components/studio/MessagesPanel';
import { AnalysisPanel } from '@/components/studio/AnalysisPanel';
import styles from './StudioPage.module.css';
import { MessagesPanel } from '@/components/studio/MessagesPanel';
import { StudioLayout } from '@/components/studio/StudioLayout';
import { useAutoRefreshPrimer } from '@/hooks/useAutoRefreshPrimer';
export function StudioPage() {
export const StudioPage = () => {
const { id } = useParams<{ id: string }>();
const queryClient = useQueryClient();

View file

@ -1,9 +1,13 @@
import { useState } from 'react';
import { Loader2 } from 'lucide-react';
import styled, { keyframes } from '@lilith/ui-styled-components';
import type { ThemeInterface } from '@lilith/ui-theme';
import { useTrainingSamples, useTrainingJobs, useStartTrainingJob } from '@/api';
import { useToast } from '@lilith/ui-feedback';
import styled, { keyframes } from '@lilith/ui-styled-components';
import { Loader2 } from 'lucide-react';
import type { ThemeInterface } from '@lilith/ui-theme';
import { useTrainingSamples, useTrainingJobs, useStartTrainingJob } from '@/api';
const spin = keyframes`
from { transform: rotate(0deg); }
@ -255,7 +259,7 @@ const EmptyState = styled.p`
margin: 0;
`;
export function TrainingPage() {
export const TrainingPage = () => {
const { data: samples } = useTrainingSamples();
const { data: jobs } = useTrainingJobs();
const startJob = useStartTrainingJob();

View file

@ -1,7 +1,7 @@
import { useState } from 'react';
import { Laptop, Smartphone, RefreshCw } from 'lucide-react';
import type { Device } from '@/api';
import { useDeactivateDevice, useResetDeviceSync } from '@/api';
import {
Card,
CardHeader,
@ -20,11 +20,15 @@ import {
ButtonSpinner,
} from './styles';
import type { Device } from '@/api';
import { useDeactivateDevice, useResetDeviceSync } from '@/api';
interface DeviceCardProps {
device: Device;
}
export function DeviceCard({ device }: DeviceCardProps) {
export const DeviceCard = ({ device }: DeviceCardProps) => {
const deactivate = useDeactivateDevice();
const resetSync = useResetDeviceSync();
const [confirmingReset, setConfirmingReset] = useState(false);

View file

@ -1,7 +1,6 @@
import { Spinner } from '@lilith/ui-primitives';
import { useDevices, useSyncStats } from '@/api';
import { DeviceCard } from './DeviceCard';
import { SyncStats } from './SyncStats';
import { MaintenanceSection } from './MaintenanceSection';
import {
Container,
@ -15,8 +14,11 @@ import {
SyncSection,
SectionHeader,
} from './styles';
import { SyncStats } from './SyncStats';
export function DevicesPage() {
import { useDevices, useSyncStats } from '@/api';
export const DevicesPage = () => {
const { data: devices, isLoading, error } = useDevices();
const { data: syncStats } = useSyncStats();

View file

@ -1,10 +1,6 @@
import { Users, MessageSquare, RefreshCw, AlertTriangle } from 'lucide-react';
import { useToast } from '@lilith/ui-feedback';
import {
useBackfillContactLastMessageAt,
useBackfillConversationLastMessageAt,
useBackfillContactDisplayNames,
} from '@/api';
import { Users, MessageSquare, RefreshCw, AlertTriangle } from 'lucide-react';
import {
MaintenanceContainer,
MaintenanceInfo,
@ -14,7 +10,14 @@ import {
ButtonSpinner,
} from './styles';
export function MaintenanceSection() {
import {
useBackfillContactLastMessageAt,
useBackfillConversationLastMessageAt,
useBackfillContactDisplayNames,
} from '@/api';
export const MaintenanceSection = () => {
const backfillContactLastMessage = useBackfillContactLastMessageAt();
const backfillConversationLastMessage = useBackfillConversationLastMessageAt();
const backfillDisplayNames = useBackfillContactDisplayNames();

View file

@ -1,5 +1,5 @@
import { MessageSquare, Database, Users } from 'lucide-react';
import type { SyncStats as SyncStatsType } from '@/api';
import {
StatsGrid,
StatCard,
@ -9,12 +9,13 @@ import {
StatLabel,
} from './styles';
import type { SyncStats as SyncStatsType } from '@/api';
interface SyncStatsProps {
stats: SyncStatsType | undefined;
}
export function SyncStats({ stats }: SyncStatsProps) {
return (
export const SyncStats = ({ stats }: SyncStatsProps) => (
<StatsGrid>
<StatCard>
<StatIcon>
@ -50,5 +51,4 @@ export function SyncStats({ stats }: SyncStatsProps) {
</StatInfo>
</StatCard>
</StatsGrid>
);
}
)

View file

@ -1,5 +1,6 @@
import styled, { keyframes } from '@lilith/ui-styled-components';
import { Loader2 } from 'lucide-react';
import type { ThemeInterface } from '@lilith/ui-theme';
export const spin = keyframes`

View file

@ -1,10 +1,13 @@
import { useState, useMemo } from 'react';
import styled from '@lilith/ui-styled-components';
import type { ThemeInterface } from '@lilith/ui-theme';
import { ArrowLeft, Loader2, AlertTriangle, MessageSquare, ChevronDown, ChevronUp } from 'lucide-react';
import { Link } from '@lilith/ui-router';
import { TabGroup, SeverityBadge } from '@/components/settings';
import styled from '@lilith/ui-styled-components';
import { ArrowLeft, Loader2, AlertTriangle, MessageSquare, ChevronDown, ChevronUp } from 'lucide-react';
import type { ThemeInterface } from '@lilith/ui-theme';
import { useRedFlagDocumentation, type RedFlagDoc } from '@/api';
import { TabGroup, SeverityBadge } from '@/components/settings';
const Container = styled.div`
max-width: 900px;
@ -188,7 +191,7 @@ const EmptyState = styled.div`
color: ${(props) => (props.theme as ThemeInterface).colors.text.secondary};
`;
const CATEGORY_TABS: { id: string; label: string }[] = [
const CATEGORY_TABS: Array<{ id: string; label: string }> = [
{ id: 'all', label: 'All' },
{ id: 'scam', label: 'Scams' },
{ id: 'freeloader', label: 'Freeloaders' },
@ -197,13 +200,13 @@ const CATEGORY_TABS: { id: string; label: string }[] = [
{ id: 'payment_scam', label: 'Payment Scams' },
];
export function RedFlagDocsPage() {
export const RedFlagDocsPage = () => {
const { data: docs, isLoading } = useRedFlagDocumentation();
const [activeTab, setActiveTab] = useState('all');
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
const tabsWithCounts = useMemo(() => {
if (!docs) return CATEGORY_TABS;
if (!docs) {return CATEGORY_TABS;}
return CATEGORY_TABS.map((tab) => ({
...tab,
count: tab.id === 'all' ? docs.length : docs.filter((d) => d.category === tab.id).length,
@ -211,8 +214,8 @@ export function RedFlagDocsPage() {
}, [docs]);
const filteredDocs = useMemo(() => {
if (!docs) return [];
if (activeTab === 'all') return docs;
if (!docs) {return [];}
if (activeTab === 'all') {return docs;}
return docs.filter((d) => d.category === activeTab);
}, [docs, activeTab]);
@ -273,7 +276,7 @@ export function RedFlagDocsPage() {
);
}
function DocCardComponent({
const DocCardComponent = ({
doc,
expanded,
onToggle,
@ -281,8 +284,7 @@ function DocCardComponent({
doc: RedFlagDoc;
expanded: boolean;
onToggle: () => void;
}) {
return (
}) => (
<DocCard>
<DocHeader onClick={onToggle}>
<div>
@ -335,5 +337,4 @@ function DocCardComponent({
)}
</DocContent>
</DocCard>
);
}
)

View file

@ -1,10 +1,12 @@
import { useState, useMemo } from 'react';
import styled from '@lilith/ui-styled-components';
import type { ThemeInterface } from '@lilith/ui-theme';
import { ArrowLeft, Plus, Loader2 } from 'lucide-react';
import { Link } from '@lilith/ui-router';
import { useToast } from '@lilith/ui-feedback';
import { TabGroup, PatternCard } from '@/components/settings';
import { Link } from '@lilith/ui-router';
import styled from '@lilith/ui-styled-components';
import { ArrowLeft, Plus, Loader2 } from 'lucide-react';
import type { ThemeInterface } from '@lilith/ui-theme';
import {
useRedFlagPatterns,
useUpdateRedFlagPattern,
@ -14,6 +16,7 @@ import {
type RedFlagCategory,
type RedFlagSeverity,
} from '@/api';
import { TabGroup, PatternCard } from '@/components/settings';
const Container = styled.div`
max-width: 900px;
@ -241,7 +244,7 @@ const SEVERITY_TABS = [
{ id: 'custom', label: 'Custom' },
];
const CATEGORIES: { value: RedFlagCategory; label: string }[] = [
const CATEGORIES: Array<{ value: RedFlagCategory; label: string }> = [
{ value: 'scam', label: 'Scam' },
{ value: 'freeloader', label: 'Freeloader' },
{ value: 'time_waste', label: 'Time Waste' },
@ -249,7 +252,7 @@ const CATEGORIES: { value: RedFlagCategory; label: string }[] = [
{ value: 'payment_scam', label: 'Payment Scam' },
];
const SEVERITIES: { value: RedFlagSeverity; label: string }[] = [
const SEVERITIES: Array<{ value: RedFlagSeverity; label: string }> = [
{ value: 'CRITICAL', label: 'Critical (1.0)' },
{ value: 'HIGH', label: 'High (0.8)' },
{ value: 'MEDIUM', label: 'Medium (0.5)' },
@ -276,7 +279,7 @@ const INITIAL_FORM: NewPatternForm = {
weight: 0.5,
};
export function RedFlagsConfigPage() {
export const RedFlagsConfigPage = () => {
const { data: patterns, isLoading } = useRedFlagPatterns();
const updatePattern = useUpdateRedFlagPattern();
const createPattern = useCreateCustomPattern();
@ -288,7 +291,7 @@ export function RedFlagsConfigPage() {
const [formData, setFormData] = useState<NewPatternForm>(INITIAL_FORM);
const tabsWithCounts = useMemo(() => {
if (!patterns) return SEVERITY_TABS;
if (!patterns) {return SEVERITY_TABS;}
return SEVERITY_TABS.map((tab) => ({
...tab,
count:
@ -301,9 +304,9 @@ export function RedFlagsConfigPage() {
}, [patterns]);
const filteredPatterns = useMemo(() => {
if (!patterns) return [];
if (activeTab === 'all') return patterns;
if (activeTab === 'custom') return patterns.filter((p) => p.isCustom);
if (!patterns) {return [];}
if (activeTab === 'all') {return patterns;}
if (activeTab === 'custom') {return patterns.filter((p) => p.isCustom);}
return patterns.filter((p) => p.severity === activeTab);
}, [patterns, activeTab]);
@ -330,8 +333,8 @@ export function RedFlagsConfigPage() {
};
const handleDelete = async (pattern: RedFlagPattern) => {
if (!pattern.isCustom) return;
if (!window.confirm(`Delete custom pattern "${pattern.displayName}"?`)) return;
if (!pattern.isCustom) {return;}
if (!window.confirm(`Delete custom pattern "${pattern.displayName}"?`)) {return;}
try {
await deletePattern.mutateAsync(pattern.id);

View file

@ -1,16 +1,19 @@
import { useState, useEffect } from 'react';
import styled from '@lilith/ui-styled-components';
import type { ThemeInterface } from '@lilith/ui-theme';
import { ArrowLeft, Save, Loader2 } from 'lucide-react';
import { Link } from '@lilith/ui-router';
import { useToast } from '@lilith/ui-feedback';
import { Slider, EditableTagList } from '@/components/settings';
import { Link } from '@lilith/ui-router';
import styled from '@lilith/ui-styled-components';
import { ArrowLeft, Save, Loader2 } from 'lucide-react';
import type { ThemeInterface } from '@lilith/ui-theme';
import {
useStyleProfile,
useUpdateStyleProfile,
type StyleProfile,
type PunctuationStyle,
} from '@/api';
import { Slider, EditableTagList } from '@/components/settings';
const Container = styled.div`
max-width: 800px;
@ -177,7 +180,7 @@ const DEFAULT_DEFLECTION = [
'Subscribe to see more of me...',
];
export function StyleProfilePage() {
export const StyleProfilePage = () => {
const { data: profile, isLoading } = useStyleProfile();
const updateProfile = useUpdateStyleProfile();
const toast = useToast();

View file

@ -112,27 +112,21 @@ export const mockTrainingJobs = [
// Request handlers
export const handlers = [
// Devices
http.get(`${API_BASE}/devices`, () => {
return HttpResponse.json({
http.get(`${API_BASE}/devices`, () => HttpResponse.json({
success: true,
data: mockDevices,
});
}),
})),
http.post(`${API_BASE}/devices/:id/deactivate`, ({ params }) => {
return HttpResponse.json({
http.post(`${API_BASE}/devices/:id/deactivate`, ({ params }) => HttpResponse.json({
success: true,
data: { id: params.id, isActive: false },
});
}),
})),
// Conversations
http.get(`${API_BASE}/conversations`, () => {
return HttpResponse.json({
http.get(`${API_BASE}/conversations`, () => HttpResponse.json({
success: true,
data: mockConversations,
});
}),
})),
http.get(`${API_BASE}/conversations/:id`, ({ params }) => {
const conversation = mockConversations.find((c) => c.id === params.id);
@ -151,12 +145,10 @@ export const handlers = [
});
}),
http.get(`${API_BASE}/conversations/:id/messages`, () => {
return HttpResponse.json({
http.get(`${API_BASE}/conversations/:id/messages`, () => HttpResponse.json({
success: true,
data: mockMessages,
});
}),
})),
// Responses
http.post(`${API_BASE}/responses/generate`, async ({ request }) => {
@ -173,34 +165,26 @@ export const handlers = [
});
}),
http.post(`${API_BASE}/responses/:id/accept`, ({ params }) => {
return HttpResponse.json({
http.post(`${API_BASE}/responses/:id/accept`, ({ params }) => HttpResponse.json({
success: true,
data: { id: params.id, status: 'accepted' },
});
}),
})),
http.post(`${API_BASE}/responses/:id/reject`, ({ params }) => {
return HttpResponse.json({
http.post(`${API_BASE}/responses/:id/reject`, ({ params }) => HttpResponse.json({
success: true,
data: { id: params.id, status: 'rejected' },
});
}),
})),
// Training
http.get(`${API_BASE}/training/samples`, () => {
return HttpResponse.json({
http.get(`${API_BASE}/training/samples`, () => HttpResponse.json({
success: true,
data: mockTrainingSamples,
});
}),
})),
http.get(`${API_BASE}/training/jobs`, () => {
return HttpResponse.json({
http.get(`${API_BASE}/training/jobs`, () => HttpResponse.json({
success: true,
data: mockTrainingJobs,
});
}),
})),
http.post(`${API_BASE}/training/jobs`, async ({ request }) => {
const body = (await request.json()) as { baseModel: string; epochs?: number };