Passa al contenuto principale

Applicazioni Frontend

Il layer frontend di Emblema è basato su Next.js 14 con App Router, utilizzando una libreria UI personalizzata costruita su Shadcn/UI e un sistema di state management distribuito con React Context e SWR per la gestione dei dati server.

Loading diagram...

www-emblema

L'applicazione principale è costruita con Next.js 14, utilizzando l'App Router per il routing avanzato e supporto SSR/SSG. Implementa un'architettura modulare con componenti riutilizzabili e pattern di design consistenti.

Architettura Next.js 14

// next.config.js - Configurazione ottimizzata
const nextConfig = {
reactStrictMode: true,
transpilePackages: ["@repo/ui"],
output: "standalone", // Docker-friendly build
experimental: {
serverComponentsExternalPackages: [
"sharp",
"onnxruntime-node",
"@repo/data-core",
],
outputFileTracingRoot: path.join(__dirname, "../../"),
},
webpack: (config, { isServer }) => {
// Ottimizzazioni server/client specifiche
if (isServer) {
config.resolve.alias["yjs"] = path.resolve(__dirname, "node_modules/yjs");
} else {
config.resolve.fallback = {
fs: false,
path: false,
child_process: false,
};
}
return config;
},
};

Struttura App Router

app/
├── [lang]/ # Internazionalizzazione
│ ├── layout.tsx # Layout principale con providers
│ ├── page.tsx # Homepage
│ ├── Document/ # Route documenti
│ │ ├── page.tsx
│ │ └── [id]/
│ │ ├── page.tsx
│ │ └── edit/page.tsx
│ ├── KnowledgeBase/ # Route knowledge bases
│ ├── Agent/ # Route agenti AI
│ └── Chat/ # Route chat interface
├── api/ # API Routes
│ ├── auth/ # NextAuth endpoints
│ ├── v1/ # REST API v1
│ │ └── [entityType]/
│ │ ├── route.ts # CRUD operations
│ │ └── [entityId]/
│ │ └── route.ts
│ ├── cron/ # Scheduled tasks
│ └── sse/ # Server-Sent Events
├── globals.css # Stili globali + Tailwind
└── layout.tsx # Root layout

Internazionalizzazione (i18n)

Emblema implementa un sistema i18n completo con supporto per italiano e inglese:

// lib/i18n.ts - Configurazione i18n
export const AVAILABLE_LANGUAGES = ["it", "en"] as const;
export type Language = (typeof AVAILABLE_LANGUAGES)[number];

// Hook per traduzioni
export const useTranslation = () => {
const params = useParams();
const lang = (params?.lang as Language) || "it";

return {
t: (key: string, variables?: Record<string, any>) => {
return getTranslation(key, lang, variables);
},
lang,
};
};

Struttura Flat Keys

Le traduzioni utilizzano una struttura flat per evitare collisioni:

// locales/it/translation.json
{
"common.loading": "Caricamento...",
"common.save": "Salva",
"action.create": "Crea",
"action.edit": "Modifica",
"document.upload.dragDropOrClick": "Trascina i file qui o clicca per selezionare",
"message.success.saved": "Salvato con successo",
"message.error.uploadFailed": "Errore durante il caricamento",
"form.document.name.label": "Nome documento",
"form.document.name.placeholder": "Inserisci il nome del documento"
}

Componenti e Pattern

Form System

Sistema di form unificato con validazione i18n:

// Pattern ref-based per forms
export const DocumentForm = forwardRef<FormRef<DocumentSchema>, DocumentFormProps>(
({ defaultValues, onSubmit, variant = 'page' }, ref) => {
const { t } = useTranslation();
const form = useForm({
resolver: zodResolver(createDocumentSchema(t)),
defaultValues,
});

useImperativeHandle(ref, () => ({
submit: async () => await form.handleSubmit(onSubmit)(),
reset: (values) => form.reset(values || defaultValues),
getValues: () => form.getValues(),
isDirty: () => form.formState.isDirty,
isValid: () => form.formState.isValid,
}));

return (
<Form {...form}>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>{t("form.document.name.label")}</FormLabel>
<FormControl>
<Input
placeholder={t("form.document.name.placeholder")}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</Form>
);
}
);

Data Table Pattern

Sistema di tabelle riutilizzabile con sorting, filtering e paginazione:

// Componente generico DataTable
export function DataTable<TData, TValue>({
columns,
data,
entityType,
loading,
pagination,
onPaginationChange,
sorting,
onSortingChange,
}: DataTableProps<TData, TValue>) {
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
onPaginationChange,
onSortingChange,
state: {
pagination,
sorting,
},
});

return (
<div className="space-y-4">
<div className="rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder ? null : (
<SortableHeader header={header} />
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{/* Render rows con skeleton loading */}
</TableBody>
</Table>
</div>
<DataTablePagination table={table} />
</div>
);
}

Performance Optimizations

SSR-Safe Components

Pattern per evitare errori hydration:

// Hook SSR-safe
export function useSSRSafeHook() {
const [isClient, setIsClient] = useState(false);

useEffect(() => {
setIsClient(true);
}, []);

if (!isClient) {
return {
/* safe default values */
};
}

return {
/* actual implementation */
};
}

// Componente con dynamic import per browser APIs
const DynamicComponent = dynamic(() => import("./BrowserOnlyComponent"), {
ssr: false,
});

Code Splitting

// Dynamic imports per features pesanti
const ChunkEditor = dynamic(() => import('./chunk/mineru-editor'), {
loading: () => <EditorSkeleton />,
ssr: false
});

const PDFViewer = dynamic(() => import('./pdf-viewer'), {
loading: () => <div>Caricamento PDF...</div>
});

UI Library

La UI library è basata su Shadcn/UI con un tema personalizzato "Emblema" e componenti estesi per le funzionalità specifiche della piattaforma.

Architettura UI Library

apps/ui/
├── registry/ # Registry dei componenti
│ ├── emblema/ # Tema Emblema
│ │ ├── ui/ # Componenti base
│ │ └── example/ # Esempi d'uso
│ ├── default/ # Tema default
│ └── new-york/ # Tema alternativo
├── components/ # Componenti documentazione
├── lib/ # Utilities e helpers
├── public/r/ # Registry pubblico
└── styles/ # Stili globali

Tema Emblema

/* Variabili CSS custom per il tema */
:root {
--primary: 190 62% 31%; /* Blu Emblema */
--primary-hover: 190 63% 23%;
--primary-active: 223 100% 31%;
--secondary: 0 0% 22%; /* Grigio scuro */
--danger: 357 76% 49%; /* Rosso */
--radius: 0.5rem; /* Border radius consistente */
}

/* Dark mode support */
.dark {
--primary: 190 62% 45%;
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
}

Componenti Personalizzati

File Upload Component

// Componente drag & drop avanzato
export function FileUpload({
onFilesSelected,
accept,
maxSize,
multiple = false,
disabled = false
}: FileUploadProps) {
const { t } = useTranslation();
const { maxFileSize } = useSettings();
const [isDragOver, setIsDragOver] = useState(false);

const handleDrop = useCallback((e: DragEvent) => {
e.preventDefault();
setIsDragOver(false);

const files = Array.from(e.dataTransfer?.files || []);
const validFiles = files.filter(file => {
// Validazione dimensione e tipo
if (file.size > maxFileSize) {
toast.error(t("message.error.fileTooLarge", {
maxSize: formatFileSize(maxFileSize)
}));
return false;
}
return true;
});

onFilesSelected(validFiles);
}, [maxFileSize, onFilesSelected, t]);

return (
<div
className={cn(
"border-2 border-dashed rounded-lg p-6 text-center transition-colors",
isDragOver && "border-primary bg-primary/5",
disabled && "opacity-50 cursor-not-allowed"
)}
onDrop={handleDrop}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
>
<Upload className="mx-auto h-12 w-12 text-muted-foreground" />
<p className="mt-2 text-sm text-muted-foreground">
{t("document.upload.dragDropOrClick")}
</p>
<p className="text-xs text-muted-foreground">
{t("document.upload.maxSize", { maxSize: formatFileSize(maxFileSize) })}
</p>
</div>
);
}

Data Visualization Components

// Componente per visualizzazione chunks
export function ChunkViewer({
chunks,
highlightedChunk,
onChunkSelect
}: ChunkViewerProps) {
return (
<div className="space-y-2">
{chunks.map((chunk, index) => (
<Card
key={chunk.id}
className={cn(
"p-4 cursor-pointer transition-colors",
highlightedChunk === chunk.id && "border-primary bg-primary/5"
)}
onClick={() => onChunkSelect(chunk)}
>
<div className="flex items-start justify-between">
<div className="flex-1">
<p className="text-sm text-muted-foreground mb-2">
Chunk {index + 1} ({chunk.tokenCount} tokens)
</p>
<p className="text-sm leading-relaxed">
{chunk.text}
</p>
</div>
{chunk.embedding && (
<Badge variant="secondary" className="ml-2">
Embeddings
</Badge>
)}
</div>
</Card>
))}
</div>
);
}

Registry System

Sistema di registry per distribuzione componenti:

# Installazione componenti dal registry Emblema
export REGISTRY_URL="http://ui.emblema.ai/r"
pnpm dlx shadcn@latest add button
pnpm dlx shadcn@latest add data-table
pnpm dlx shadcn@latest add file-upload

Architecture

L'architettura frontend segue principi modulari con separazione chiara delle responsabilità e pattern scalabili.

Layered Architecture

Loading diagram...

Module Organization

// Struttura modulare per entità
export const DocumentModule = {
// Componenti UI
components: {
Form: DocumentForm,
List: DocumentList,
View: DocumentView,
DataTable: DocumentDataTable,
},

// Business Logic
hooks: {
useDocument: () => useEntityHook("Document"),
useDocumentPermissions: () => usePermission(),
useDocumentUpload: () => useChunkedUpload(),
},

// Schemi e validazione
schema: createDocumentSchema,

// Configurazione
config: {
entityType: "Document" as const,
routes: {
list: "/Document",
view: "/Document/[id]",
edit: "/Document/[id]/edit",
},
},
};

Error Boundaries

// Error boundary per gestione errori componenti
export class FeatureErrorBoundary extends Component<
PropsWithChildren<{ fallback?: ReactNode }>,
{ hasError: boolean; error?: Error }
> {
constructor(props: PropsWithChildren<{ fallback?: ReactNode }>) {
super(props);
this.state = { hasError: false };
}

static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}

componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('Feature Error:', error, errorInfo);
// Send to monitoring service
}

render() {
if (this.state.hasError) {
return this.props.fallback || (
<ErrorFallback
error={this.state.error}
resetError={() => this.setState({ hasError: false })}
/>
);
}

return this.props.children;
}
}

State Management

Emblema utilizza un approccio di state management distribuito combinando React Context per lo stato globale e SWR per la gestione dei dati server.

Global State Architecture

Loading diagram...

Settings Context

// Provider per configurazioni applicazione
export interface AppSettings {
backendUrl: string;
socketUrl: string;
applicationIdentifier: string;
minioUrl?: string;
debug?: boolean;
maxFileSize: number;
}

export const SettingProvider = ({ children, defaultSettings }: {
children: React.ReactNode;
defaultSettings: AppSettings;
}) => {
const [settings, setSettings] = useState<AppSettings>(defaultSettings);

return (
<SettingContext.Provider value={settings}>
{children}
</SettingContext.Provider>
);
};

// Hook per accesso settings
export const useSettings = () => {
const context = useContext(SettingContext);
if (!context) {
throw new Error('useSettings must be used within SettingProvider');
}
return context;
};

Server State con SWR

// Hook generico per entità CRUD
export function useEntity<T>(
entityType: EntityType,
entityId?: string,
options?: SWRConfiguration,
) {
const { data, error, mutate, isLoading } = useSWR<ListResult<T>>(
entityId ? `/api/v1/${entityType}/${entityId}` : `/api/v1/${entityType}`,
fetcher,
{
revalidateOnFocus: false,
revalidateOnReconnect: true,
...options,
},
);

const createEntity = useCallback(
async (payload: Partial<T>) => {
const response = await api.post(`/api/v1/${entityType}`, payload);
mutate(); // Revalidate cache
return response.data;
},
[entityType, mutate],
);

const updateEntity = useCallback(
async (id: string, payload: Partial<T>) => {
const response = await api.put(`/api/v1/${entityType}/${id}`, payload);
mutate(); // Revalidate cache
return response.data;
},
[entityType, mutate],
);

return {
data: data?.data || [],
total: data?.total || 0,
error,
isLoading,
mutate,
createEntity,
updateEntity,
};
}

Real-time Updates

// Hook per Server-Sent Events
export function useServerSentEvents(endpoint: string) {
const [data, setData] = useState(null);
const [isConnected, setIsConnected] = useState(false);

useEffect(() => {
const eventSource = new EventSource(endpoint);

eventSource.onopen = () => {
setIsConnected(true);
};

eventSource.onmessage = (event) => {
const eventData = JSON.parse(event.data);
setData(eventData);
};

eventSource.onerror = () => {
setIsConnected(false);
};

return () => {
eventSource.close();
};
}, [endpoint]);

return { data, isConnected };
}

// Hook per task progress monitoring
export function useTaskProgress(taskId: string) {
const { data } = useServerSentEvents(`/api/sse/task/${taskId}`);

return {
progress: data?.progress || 0,
status: data?.status || "PENDING",
isComplete: data?.status === "COMPLETED",
error: data?.error,
};
}

Form State Management

// Schema factory per validazione i18n
export const createSchemaFactory = (namespace: string) => {
return (t: TFunction) => {
const key = (field: string, rule: string) =>
`schema.${namespace}.${field}.${rule}`;

return {
string: (field: string) => ({
required: () => t(key(field, "required")),
minLength: (min: number) => t(key(field, "minLength"), { min }),
email: () => t(key(field, "email")),
}),
};
};
};

// Hook per gestione form unificata
export function useFormWithSchema<T extends z.ZodType>(
schemaFactory: (t: TFunction) => T,
defaultValues?: z.infer<T>,
) {
const { t } = useTranslation();

const form = useForm<z.infer<T>>({
resolver: zodResolver(schemaFactory(t)),
defaultValues,
});

const handleSubmit = useCallback(
async (onSubmit: (data: z.infer<T>) => Promise<void> | void) => {
try {
await form.handleSubmit(onSubmit)();
} catch (error) {
console.error("Form submission error:", error);
}
},
[form],
);

return {
form,
handleSubmit,
isValid: form.formState.isValid,
isDirty: form.formState.isDirty,
errors: form.formState.errors,
};
}

Caching Strategy

// Cache configuration per diversi tipi di dati
export const cacheConfig = {
// Dati statici - cache lunga
staticData: {
dedupingInterval: 60 * 60 * 1000, // 1 ora
focusThrottleInterval: 30 * 60 * 1000, // 30 minuti
},

// Dati dinamici - cache breve
dynamicData: {
dedupingInterval: 5 * 60 * 1000, // 5 minuti
focusThrottleInterval: 2 * 60 * 1000, // 2 minuti
},

// Real-time data - no cache
realTimeData: {
dedupingInterval: 0,
revalidateOnFocus: true,
refreshInterval: 1000, // 1 secondo
},
};

// Hook con cache intelligente
export function useIntelligentCache<T>(
key: string,
fetcher: () => Promise<T>,
type: keyof typeof cacheConfig = "dynamicData",
) {
return useSWR(key, fetcher, cacheConfig[type]);
}

Riferimenti

Prossimi Passi

  1. Esplora Backend Services: Servizi Python e architettura microservizi
  2. Comprendi AI Pipeline: Sistema di processing documenti e chunking
  3. Analizza Task System: Background jobs e processing asincrono
  4. Approfondisci Monitoring: Sistema di logging e metriche

Questa pagina ti è stata utile?