+ {/* Scrollable Container - scrollbar sits outside */}
+
+ {/* Document Container with border */}
+
+
+ {/* Document Image */}
+
+
+ {/* Highlights Overlay */}
+ {highlights.map((highlight) => (
+
+
+ {
+ e.stopPropagation();
+ handleHighlightClick(highlight, e);
+ }}
+ />
+
+ {highlight.label && (
+
+ {highlight.label}
+
+ )}
+
+ ))}
+
+
+
+
+ {/* Floating Zoom Controls - Positioned outside scrollable area */}
+
+
+
+
+
+
+ Zoom in
+
+
+
+
+
+
+
+
+ Zoom to fit
+
+
+
+
+
+
+
+
+ Zoom out
+
+
+
+
+
+ );
+}
diff --git a/apps/loan-qc/src/components/layout/Header.tsx b/apps/loan-qc/src/components/layout/Header.tsx
new file mode 100644
index 000000000..710967775
--- /dev/null
+++ b/apps/loan-qc/src/components/layout/Header.tsx
@@ -0,0 +1,21 @@
+"use client";
+
+interface HeaderProps {
+ title: string;
+ subtitle?: string;
+}
+
+export function Header({ title, subtitle }: HeaderProps) {
+ return (
+
+
+
+
{title}
+ {subtitle && (
+
{subtitle}
+ )}
+
+
+
+ );
+}
diff --git a/apps/loan-qc/src/components/layout/MinHeader.tsx b/apps/loan-qc/src/components/layout/MinHeader.tsx
new file mode 100644
index 000000000..6356a86e5
--- /dev/null
+++ b/apps/loan-qc/src/components/layout/MinHeader.tsx
@@ -0,0 +1,38 @@
+"use client";
+
+import { Box, Bell } from 'lucide-react';
+import { Avatar, AvatarFallback } from '@/components/ui/avatar';
+
+export function MinHeader() {
+ return (
+
+ );
+}
diff --git a/apps/loan-qc/src/components/layout/ObjectHeader.tsx b/apps/loan-qc/src/components/layout/ObjectHeader.tsx
new file mode 100644
index 000000000..f40e60179
--- /dev/null
+++ b/apps/loan-qc/src/components/layout/ObjectHeader.tsx
@@ -0,0 +1,27 @@
+"use client";
+
+import { ArrowLeft } from 'lucide-react';
+import { Button } from '@/components/ui/button';
+
+interface ObjectHeaderProps {
+ onBack?: () => void;
+}
+
+export function ObjectHeader({ onBack }: ObjectHeaderProps) {
+ return (
+
+
+
+
Agreement to provide insurance
+
CRE #00000000000-00
+
+
+ );
+}
diff --git a/apps/loan-qc/src/components/layout/SidebarFooter.tsx b/apps/loan-qc/src/components/layout/SidebarFooter.tsx
new file mode 100644
index 000000000..b34c5f175
--- /dev/null
+++ b/apps/loan-qc/src/components/layout/SidebarFooter.tsx
@@ -0,0 +1,49 @@
+"use client";
+
+import { ChevronLeft, ChevronRight } from 'lucide-react';
+import { Button } from '@/components/ui/button';
+
+interface SidebarFooterProps {
+ title: string;
+ currentIndex: number;
+ totalCount: number;
+ onPrevious?: () => void;
+ onNext?: () => void;
+}
+
+export function SidebarFooter({
+ title,
+ currentIndex,
+ totalCount,
+ onPrevious,
+ onNext,
+}: SidebarFooterProps) {
+ return (
+
+
+ {title}
+ {` ${currentIndex} of ${totalCount}`}
+
+
+
+
+
+
+ );
+}
diff --git a/apps/loan-qc/src/components/layout/SplitViewLayout.tsx b/apps/loan-qc/src/components/layout/SplitViewLayout.tsx
new file mode 100644
index 000000000..766adcc6f
--- /dev/null
+++ b/apps/loan-qc/src/components/layout/SplitViewLayout.tsx
@@ -0,0 +1,90 @@
+"use client";
+
+import {
+ ResizablePanelGroup,
+ ResizablePanel,
+ ResizableHandle,
+} from '@/components/ui/resizable';
+import type { ReactNode } from 'react';
+import { SidebarFooter } from './SidebarFooter';
+
+interface SplitViewLayoutProps {
+ validationPanel?: ReactNode;
+ documentArea?: ReactNode;
+ leftDocument?: ReactNode;
+ rightDocument?: ReactNode;
+}
+
+export function SplitViewLayout({
+ validationPanel,
+ documentArea,
+ leftDocument,
+ rightDocument,
+}: SplitViewLayoutProps) {
+ // If validationPanel is provided, render the main layout with sidebar
+ if (validationPanel) {
+ return (
+
+ {/* Decorative ellipse background */}
+
+
+
+ {/* Validation Checklist Panel */}
+
+
+
+ {validationPanel}
+
+
+
+
+
+
+
+ {/* Documents Area */}
+
+ {documentArea}
+
+
+
+ );
+ }
+
+ // Otherwise, render just the document split view
+ return (
+
+
+ {/* Left Document */}
+
+
+ {leftDocument}
+
+
+
+
+
+ {/* Right Document */}
+
+
+ {rightDocument}
+
+
+
+
+ );
+}
diff --git a/apps/loan-qc/src/components/ui/avatar.tsx b/apps/loan-qc/src/components/ui/avatar.tsx
new file mode 100644
index 000000000..1393f5097
--- /dev/null
+++ b/apps/loan-qc/src/components/ui/avatar.tsx
@@ -0,0 +1,53 @@
+"use client";
+
+import * as AvatarPrimitive from "@radix-ui/react-avatar";
+import * as React from "react";
+
+import { cn } from "@/lib/utils";
+
+function Avatar({
+ className,
+ ...props
+}: React.ComponentProps
) {
+ return (
+
+ );
+}
+
+function AvatarImage({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function AvatarFallback({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+export { Avatar, AvatarImage, AvatarFallback };
diff --git a/apps/loan-qc/src/components/ui/badge.tsx b/apps/loan-qc/src/components/ui/badge.tsx
new file mode 100644
index 000000000..ac9d712a8
--- /dev/null
+++ b/apps/loan-qc/src/components/ui/badge.tsx
@@ -0,0 +1,50 @@
+import { Slot } from "@radix-ui/react-slot";
+import { cva, type VariantProps } from "class-variance-authority";
+import * as React from "react";
+
+import { cn } from "@/lib/utils";
+
+const badgeVariants = cva(
+ "inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
+ {
+ variants: {
+ variant: {
+ default:
+ "border-transparent bg-primary/20 text-primary-foreground [a&]:hover:bg-primary/30",
+ secondary:
+ "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
+ destructive:
+ "border-transparent bg-destructive/20 text-destructive-foreground [a&]:hover:bg-destructive/30 dark:bg-destructive/30",
+ warning:
+ "border-transparent bg-warning/40 text-warning-foreground [a&]:hover:bg-warning/50",
+ review:
+ "border-transparent bg-review text-review-foreground [a&]:hover:bg-review/90",
+ outline:
+ "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ },
+);
+
+function Badge({
+ className,
+ variant,
+ asChild = false,
+ ...props
+}: React.ComponentProps<"span"> &
+ VariantProps & { asChild?: boolean }) {
+ const Comp = asChild ? Slot : "span";
+
+ return (
+
+ );
+}
+
+export { Badge, badgeVariants };
diff --git a/apps/loan-qc/src/components/ui/button.tsx b/apps/loan-qc/src/components/ui/button.tsx
new file mode 100644
index 000000000..087f24ff9
--- /dev/null
+++ b/apps/loan-qc/src/components/ui/button.tsx
@@ -0,0 +1,62 @@
+import { Slot } from "@radix-ui/react-slot";
+import { cva, type VariantProps } from "class-variance-authority";
+import * as React from "react";
+
+import { cn } from "@/lib/utils";
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
+ {
+ variants: {
+ variant: {
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
+ outline:
+ "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
+ secondary:
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ ghost:
+ "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
+ sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
+ icon: "size-9",
+ "icon-sm": "size-8",
+ "icon-lg": "size-10",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ },
+);
+
+function Button({
+ className,
+ variant = "default",
+ size = "default",
+ asChild = false,
+ ...props
+}: React.ComponentProps<"button"> &
+ VariantProps & {
+ asChild?: boolean;
+ }) {
+ const Comp = asChild ? Slot : "button";
+
+ return (
+
+ );
+}
+
+export { Button, buttonVariants };
diff --git a/apps/loan-qc/src/components/ui/popover.tsx b/apps/loan-qc/src/components/ui/popover.tsx
new file mode 100644
index 000000000..8b33a1821
--- /dev/null
+++ b/apps/loan-qc/src/components/ui/popover.tsx
@@ -0,0 +1,48 @@
+"use client";
+
+import * as PopoverPrimitive from "@radix-ui/react-popover";
+import * as React from "react";
+
+import { cn } from "@/lib/utils";
+
+function Popover({
+ ...props
+}: React.ComponentProps) {
+ return ;
+}
+
+function PopoverTrigger({
+ ...props
+}: React.ComponentProps) {
+ return ;
+}
+
+function PopoverContent({
+ className,
+ align = "center",
+ sideOffset = 4,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ );
+}
+
+function PopoverAnchor({
+ ...props
+}: React.ComponentProps) {
+ return ;
+}
+
+export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
diff --git a/apps/loan-qc/src/components/ui/resizable.tsx b/apps/loan-qc/src/components/ui/resizable.tsx
new file mode 100644
index 000000000..39caf3c40
--- /dev/null
+++ b/apps/loan-qc/src/components/ui/resizable.tsx
@@ -0,0 +1,56 @@
+"use client";
+
+import { GripVerticalIcon } from "lucide-react";
+import * as React from "react";
+import * as ResizablePrimitive from "react-resizable-panels";
+
+import { cn } from "@/lib/utils";
+
+function ResizablePanelGroup({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function ResizablePanel({
+ ...props
+}: React.ComponentProps) {
+ return ;
+}
+
+function ResizableHandle({
+ withHandle,
+ className,
+ ...props
+}: React.ComponentProps & {
+ withHandle?: boolean;
+}) {
+ return (
+ div]:rotate-90",
+ className,
+ )}
+ {...props}
+ >
+ {withHandle && (
+
+
+
+ )}
+
+ );
+}
+
+export { ResizablePanelGroup, ResizablePanel, ResizableHandle };
diff --git a/apps/loan-qc/src/components/ui/separator.tsx b/apps/loan-qc/src/components/ui/separator.tsx
new file mode 100644
index 000000000..ee7836062
--- /dev/null
+++ b/apps/loan-qc/src/components/ui/separator.tsx
@@ -0,0 +1,28 @@
+"use client";
+
+import * as SeparatorPrimitive from "@radix-ui/react-separator";
+import * as React from "react";
+
+import { cn } from "@/lib/utils";
+
+function Separator({
+ className,
+ orientation = "horizontal",
+ decorative = true,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+export { Separator };
diff --git a/apps/loan-qc/src/components/ui/textarea.tsx b/apps/loan-qc/src/components/ui/textarea.tsx
new file mode 100644
index 000000000..0735a8ca6
--- /dev/null
+++ b/apps/loan-qc/src/components/ui/textarea.tsx
@@ -0,0 +1,18 @@
+import * as React from "react";
+
+import { cn } from "@/lib/utils";
+
+function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
+ return (
+
+ );
+}
+
+export { Textarea };
diff --git a/apps/loan-qc/src/components/ui/toggle-group.tsx b/apps/loan-qc/src/components/ui/toggle-group.tsx
new file mode 100644
index 000000000..e995e268e
--- /dev/null
+++ b/apps/loan-qc/src/components/ui/toggle-group.tsx
@@ -0,0 +1,77 @@
+"use client";
+
+import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
+import { type VariantProps } from "class-variance-authority";
+import * as React from "react";
+
+import { cn } from "@/lib/utils";
+import { toggleVariants } from "@/components/ui/toggle";
+
+const ToggleGroupContext = React.createContext<
+ VariantProps & { spacing?: number }
+>({
+ size: "default",
+ variant: "default",
+ spacing: 0,
+});
+
+function ToggleGroup({
+ className,
+ variant,
+ size,
+ spacing = 0,
+ children,
+ ...props
+}: React.ComponentProps &
+ VariantProps & {
+ spacing?: number;
+ }) {
+ return (
+
+
+ {children}
+
+
+ );
+}
+
+function ToggleGroupItem({
+ className,
+ children,
+ variant,
+ size,
+ ...props
+}: React.ComponentProps &
+ VariantProps) {
+ const context = React.useContext(ToggleGroupContext);
+
+ return (
+
+ {children}
+
+ );
+}
+
+export { ToggleGroup, ToggleGroupItem };
diff --git a/apps/loan-qc/src/components/ui/toggle.tsx b/apps/loan-qc/src/components/ui/toggle.tsx
new file mode 100644
index 000000000..91c5d7e36
--- /dev/null
+++ b/apps/loan-qc/src/components/ui/toggle.tsx
@@ -0,0 +1,47 @@
+"use client";
+
+import * as TogglePrimitive from "@radix-ui/react-toggle";
+import { cva, type VariantProps } from "class-variance-authority";
+import * as React from "react";
+
+import { cn } from "@/lib/utils";
+
+const toggleVariants = cva(
+ "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
+ {
+ variants: {
+ variant: {
+ default: "bg-transparent",
+ outline:
+ "border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground",
+ },
+ size: {
+ default: "h-9 px-2 min-w-9",
+ sm: "h-8 px-1.5 min-w-8",
+ lg: "h-10 px-2.5 min-w-10",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ },
+);
+
+function Toggle({
+ className,
+ variant,
+ size,
+ ...props
+}: React.ComponentProps &
+ VariantProps) {
+ return (
+
+ );
+}
+
+export { Toggle, toggleVariants };
diff --git a/apps/loan-qc/src/components/ui/tooltip.tsx b/apps/loan-qc/src/components/ui/tooltip.tsx
new file mode 100644
index 000000000..6fdf76a17
--- /dev/null
+++ b/apps/loan-qc/src/components/ui/tooltip.tsx
@@ -0,0 +1,30 @@
+"use client";
+
+import * as React from "react";
+import * as TooltipPrimitive from "@radix-ui/react-tooltip";
+
+import { cn } from "@/lib/utils";
+
+const TooltipProvider = TooltipPrimitive.Provider;
+
+const Tooltip = TooltipPrimitive.Root;
+
+const TooltipTrigger = TooltipPrimitive.Trigger;
+
+const TooltipContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, sideOffset = 4, ...props }, ref) => (
+
+));
+TooltipContent.displayName = TooltipPrimitive.Content.displayName;
+
+export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
diff --git a/apps/loan-qc/src/components/validation/EvaluationBadges.tsx b/apps/loan-qc/src/components/validation/EvaluationBadges.tsx
new file mode 100644
index 000000000..89221a219
--- /dev/null
+++ b/apps/loan-qc/src/components/validation/EvaluationBadges.tsx
@@ -0,0 +1,64 @@
+"use client";
+
+import { BotMessageSquare, UserRound } from 'lucide-react';
+import type { AIEvaluationStatus, HumanEvaluationStatus } from '@/lib/types';
+import { Badge } from '@/components/ui/badge';
+
+interface EvaluationBadgesProps {
+ aiStatus: AIEvaluationStatus;
+ humanStatus: HumanEvaluationStatus;
+}
+
+export function EvaluationBadges({
+ aiStatus,
+ humanStatus,
+}: EvaluationBadgesProps) {
+ const aiBadgeText =
+ {
+ yes: 'Yes',
+ no: 'No',
+ inconclusive: 'Inconclusive',
+ pending: 'Pending',
+ }[aiStatus] || 'Pending';
+
+ const aiBadgeVariant = {
+ yes: 'default' as const,
+ no: 'destructive' as const,
+ inconclusive: 'warning' as const,
+ pending: 'secondary' as const,
+ }[aiStatus];
+
+ const humanBadgeText =
+ humanStatus === 'needs-review'
+ ? 'Needs review'
+ : humanStatus === 'yes'
+ ? 'Yes'
+ : humanStatus === 'no'
+ ? 'No'
+ : humanStatus === 'na'
+ ? 'N/A'
+ : '—';
+
+ const humanBadgeVariant = {
+ 'yes': 'default' as const,
+ 'no': 'destructive' as const,
+ 'needs-review': 'review' as const,
+ 'na': 'secondary' as const,
+ }[humanStatus || 'na'] || ('secondary' as const);
+
+ return (
+
+ {/* AI Badge */}
+
+
+ {aiBadgeText}
+
+
+ {/* Human Badge */}
+
+
+ {humanBadgeText}
+
+
+ );
+}
diff --git a/apps/loan-qc/src/components/validation/HumanEvaluationToggle.tsx b/apps/loan-qc/src/components/validation/HumanEvaluationToggle.tsx
new file mode 100644
index 000000000..f1d432ad4
--- /dev/null
+++ b/apps/loan-qc/src/components/validation/HumanEvaluationToggle.tsx
@@ -0,0 +1,60 @@
+"use client";
+
+import { MessageSquareText } from 'lucide-react';
+import type { HumanEvaluationStatus } from '@/lib/types';
+import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
+import { Button } from '@/components/ui/button';
+
+interface HumanEvaluationToggleProps {
+ value: HumanEvaluationStatus;
+ onChange: (value: HumanEvaluationStatus) => void;
+ onCommentClick?: () => void;
+}
+
+export function HumanEvaluationToggle({
+ value,
+ onChange,
+ onCommentClick,
+}: HumanEvaluationToggleProps) {
+ return (
+
+
+ Human evaluation
+
+
+
+ {/* Toggle Group */}
+ {
+ if (newValue) onChange(newValue as HumanEvaluationStatus);
+ }}
+ variant="outline"
+ spacing={0}
+ className="shadow-[0px_1px_2px_0px_var(--shadow-xs)]"
+ >
+
+ Yes
+
+
+ No
+
+
+ N/A
+
+
+
+ {/* Comment Button */}
+
+
+
+ );
+}
diff --git a/apps/loan-qc/src/components/validation/IdentifiedFields.tsx b/apps/loan-qc/src/components/validation/IdentifiedFields.tsx
new file mode 100644
index 000000000..9404e4a95
--- /dev/null
+++ b/apps/loan-qc/src/components/validation/IdentifiedFields.tsx
@@ -0,0 +1,51 @@
+import type { IdentifiedField } from '@/lib/types';
+
+interface IdentifiedFieldsProps {
+ fields: IdentifiedField[];
+}
+
+export function IdentifiedFields({ fields }: IdentifiedFieldsProps) {
+ if (!fields || fields.length === 0) {
+ return null;
+ }
+
+ return (
+
+ {fields.map((field, index) => {
+ // If field has sourceValue/targetValue, show comparison format
+ if (field.sourceValue !== undefined || field.targetValue !== undefined) {
+ const sourceLabel = 'Source';
+ const targetLabel = 'Target';
+
+ return (
+
+
{field.label}
+
+ {field.sourceValue !== undefined && (
+
+ {sourceLabel}:
+ {field.sourceValue}
+
+ )}
+ {field.targetValue !== undefined && (
+
+ {targetLabel}:
+ {field.targetValue}
+
+ )}
+
+
+ );
+ }
+
+ // Otherwise show simple format
+ return (
+
+ {field.label}:
+ {field.value}
+
+ );
+ })}
+
+ );
+}
diff --git a/apps/loan-qc/src/components/validation/ValidationChecklist.tsx b/apps/loan-qc/src/components/validation/ValidationChecklist.tsx
new file mode 100644
index 000000000..b93e41252
--- /dev/null
+++ b/apps/loan-qc/src/components/validation/ValidationChecklist.tsx
@@ -0,0 +1,40 @@
+"use client";
+
+import { ValidationItem } from './ValidationItem';
+import { Separator } from '@/components/ui/separator';
+import type { ValidationItem as ValidationItemType, HumanEvaluationStatus } from '@/lib/types';
+
+interface ValidationChecklistProps {
+ items: ValidationItemType[];
+ onUpdateEvaluation: (itemId: string, status: HumanEvaluationStatus) => void;
+ onAddComment: (itemId: string, text: string) => void;
+ expandedItemId?: string | null;
+ onItemClick?: (itemId: string) => void;
+}
+
+export function ValidationChecklist({
+ items,
+ onUpdateEvaluation,
+ onAddComment,
+ expandedItemId,
+ onItemClick,
+}: ValidationChecklistProps) {
+ return (
+
+ {items.map((item, index) => (
+
+ {/* Separator for non-first items */}
+ {index > 0 && }
+
+ onUpdateEvaluation(item.id, status)}
+ onAddComment={(text) => onAddComment(item.id, text)}
+ isExpanded={item.id === expandedItemId}
+ onClick={() => onItemClick?.(item.id)}
+ />
+
+ ))}
+
+ );
+}
diff --git a/apps/loan-qc/src/components/validation/ValidationItem.tsx b/apps/loan-qc/src/components/validation/ValidationItem.tsx
new file mode 100644
index 000000000..64a8fd513
--- /dev/null
+++ b/apps/loan-qc/src/components/validation/ValidationItem.tsx
@@ -0,0 +1,223 @@
+"use client";
+
+import { useState } from 'react';
+import { motion } from 'framer-motion';
+import { MessageSquareText } from 'lucide-react';
+import { EvaluationBadges } from './EvaluationBadges';
+import { IdentifiedFields } from './IdentifiedFields';
+import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
+import { Textarea } from '@/components/ui/textarea';
+import { Button } from '@/components/ui/button';
+import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
+import type { ValidationItem as ValidationItemType, HumanEvaluationStatus } from '@/lib/types';
+
+interface ValidationItemProps {
+ item: ValidationItemType;
+ onHumanEvaluation: (status: HumanEvaluationStatus) => void;
+ onAddComment: (text: string) => void;
+ isExpanded?: boolean;
+ onClick?: () => void;
+}
+
+const expandTransition = {
+ duration: 0.3,
+ ease: [0.4, 0, 0.2, 1],
+};
+
+export function ValidationItem({
+ item,
+ onHumanEvaluation,
+ onAddComment,
+ isExpanded = false,
+ onClick,
+}: ValidationItemProps) {
+ const [commentText, setCommentText] = useState('');
+ const [isPopoverOpen, setIsPopoverOpen] = useState(false);
+
+ const handleSaveComment = () => {
+ if (commentText.trim()) {
+ onAddComment(commentText.trim());
+ setCommentText('');
+ setIsPopoverOpen(false);
+ }
+ };
+
+ const handleCancelComment = () => {
+ setCommentText('');
+ setIsPopoverOpen(false);
+ };
+
+ return (
+
+ {/* Item Name - Always visible */}
+
+
+ {/* Badges - Always visible */}
+
+
+
+
+ {/* Expanded content - Animates in/out */}
+
+
+
+ {/* Agent Response */}
+
+
+ Agent evaluation: {item.aiEvaluation.status.toUpperCase()}
+
+
+ {item.details.agentReasoning}
+
+
+ {/* Identified Fields - New flexible pattern */}
+ {item.details.identifiedFields && item.details.identifiedFields.length > 0 && (
+
+ )}
+
+ {/* Legacy support for sourceText/targetText */}
+ {!item.details.identifiedFields && (item.details.sourceText || item.details.targetText) && (
+
+ {item.details.sourceText && (
+ <>
+ Grantor:
+ {item.details.sourceText}
+
+ >
+ )}
+ {item.details.targetText && (
+ <>
+ Borrower Name:
+ {item.details.targetText}
+ >
+ )}
+
+ )}
+
+
+ {/* Human Evaluation Section */}
+
e.stopPropagation()}
+ >
+
+
+ Human evaluation
+
+
+
+ {/* Toggle Group */}
+
{
+ // onValueChange fires with empty string when clicking selected item again
+ onHumanEvaluation(newValue === "" ? null : (newValue as HumanEvaluationStatus));
+ }}
+ variant="outline"
+ spacing={0}
+ className="shadow-[0px_1px_2px_0px_var(--shadow-xs)]"
+ >
+
+ Yes
+
+
+ No
+
+
+ N/A
+
+
+
+ {/* Comment Popover */}
+
+
+
+
+
+
+
+
+
+
+
+ {/* Display Comments */}
+ {item.comments.length > 0 && (
+
+ {item.comments.map((comment) => (
+
+
{comment.text}
+
+ {comment.author && `${comment.author} • `}
+ {new Date(comment.timestamp).toLocaleString('en-US', {
+ month: 'short',
+ day: 'numeric',
+ year: 'numeric',
+ hour: 'numeric',
+ minute: '2-digit',
+ })}
+
+
+ ))}
+
+ )}
+
+
+
+
+
+ );
+}
diff --git a/apps/loan-qc/src/hooks/useDocumentView.ts b/apps/loan-qc/src/hooks/useDocumentView.ts
new file mode 100644
index 000000000..97185140a
--- /dev/null
+++ b/apps/loan-qc/src/hooks/useDocumentView.ts
@@ -0,0 +1,43 @@
+"use client";
+
+import { useState, useCallback } from 'react';
+import type { ViewMode } from '@/lib/types';
+
+export function useDocumentView() {
+ const [viewMode, setViewMode] = useState('split');
+ const [activeDocument, setActiveDocument] = useState(null);
+
+ const enterImmersiveMode = useCallback((documentId: string) => {
+ setActiveDocument(documentId);
+ setViewMode(documentId === 'doc-1' ? 'immersive-left' : 'immersive-right');
+ }, []);
+
+ const exitImmersiveMode = useCallback(() => {
+ setViewMode('split');
+ setActiveDocument(null);
+ }, []);
+
+ const toggleDocumentView = useCallback(
+ (documentId: string) => {
+ if (activeDocument === documentId) {
+ exitImmersiveMode();
+ } else {
+ enterImmersiveMode(documentId);
+ }
+ },
+ [activeDocument, enterImmersiveMode, exitImmersiveMode],
+ );
+
+ const switchDocument = useCallback((documentId: string) => {
+ setActiveDocument(documentId);
+ }, []);
+
+ return {
+ viewMode,
+ activeDocument,
+ enterImmersiveMode,
+ exitImmersiveMode,
+ toggleDocumentView,
+ switchDocument,
+ };
+}
diff --git a/apps/loan-qc/src/hooks/useValidationState.ts b/apps/loan-qc/src/hooks/useValidationState.ts
new file mode 100644
index 000000000..77481bf13
--- /dev/null
+++ b/apps/loan-qc/src/hooks/useValidationState.ts
@@ -0,0 +1,60 @@
+"use client";
+
+import { useState, useCallback } from 'react';
+import type { ValidationItem, HumanEvaluationStatus, Comment } from '@/lib/types';
+import { mockValidationItems } from '@/lib/validation-data';
+
+export function useValidationState() {
+ const [items, setItems] = useState(mockValidationItems);
+ const [expandedItemId, setExpandedItemId] = useState(mockValidationItems[0]?.id || null);
+
+ const updateHumanEvaluation = useCallback(
+ (itemId: string, status: HumanEvaluationStatus) => {
+ setItems((prev) =>
+ prev.map((item) =>
+ item.id === itemId ? { ...item, humanEvaluation: status } : item,
+ ),
+ );
+ },
+ [],
+ );
+
+ const addComment = useCallback((itemId: string, commentText: string) => {
+ const newComment: Comment = {
+ id: Date.now().toString(),
+ text: commentText,
+ timestamp: new Date(),
+ author: 'Current User',
+ };
+
+ setItems((prev) =>
+ prev.map((item) =>
+ item.id === itemId
+ ? { ...item, comments: [...item.comments, newComment] }
+ : item,
+ ),
+ );
+ }, []);
+
+ const clearComment = useCallback((itemId: string, commentId: string) => {
+ setItems((prev) =>
+ prev.map((item) =>
+ item.id === itemId
+ ? {
+ ...item,
+ comments: item.comments.filter((c) => c.id !== commentId),
+ }
+ : item,
+ ),
+ );
+ }, []);
+
+ return {
+ items,
+ expandedItemId,
+ setExpandedItemId,
+ updateHumanEvaluation,
+ addComment,
+ clearComment,
+ };
+}
diff --git a/apps/loan-qc/src/lib/types.ts b/apps/loan-qc/src/lib/types.ts
new file mode 100644
index 000000000..291c598d3
--- /dev/null
+++ b/apps/loan-qc/src/lib/types.ts
@@ -0,0 +1,54 @@
+export type AIEvaluationStatus = 'yes' | 'no' | 'inconclusive' | 'pending';
+export type HumanEvaluationStatus = 'yes' | 'no' | 'na' | 'needs-review' | null;
+
+export interface Comment {
+ id: string;
+ text: string;
+ timestamp: Date;
+ author?: string;
+}
+
+export interface IdentifiedField {
+ label: string;
+ value?: string; // Single value (for simple comparisons)
+ sourceValue?: string; // Value from source document
+ targetValue?: string; // Value from target document
+ sourceLabel?: string; // Optional custom label for source (defaults to "Source")
+ targetLabel?: string; // Optional custom label for target (defaults to "Target")
+}
+
+export interface ValidationItem {
+ id: string;
+ title: string;
+ aiEvaluation: {
+ status: AIEvaluationStatus;
+ confidence?: number;
+ };
+ humanEvaluation: HumanEvaluationStatus;
+ details: {
+ agentReasoning: string;
+ identifiedFields?: IdentifiedField[];
+ // Legacy fields for backward compatibility
+ sourceText?: string;
+ targetText?: string;
+ };
+ comments: Comment[];
+ documentHighlights?: DocumentHighlight[];
+}
+
+export interface DocumentHighlight {
+ id: string;
+ documentId: string;
+ boundingBox: { x: number; y: number; width: number; height: number };
+ color: string;
+ label?: string; // Tooltip label to display on hover
+}
+
+export interface Document {
+ id: string;
+ name: string;
+ imageUrl: string;
+ type: 'agreement' | 'credit-memo';
+}
+
+export type ViewMode = 'split' | 'immersive-left' | 'immersive-right';
diff --git a/apps/loan-qc/src/lib/utils.ts b/apps/loan-qc/src/lib/utils.ts
new file mode 100644
index 000000000..365058ceb
--- /dev/null
+++ b/apps/loan-qc/src/lib/utils.ts
@@ -0,0 +1,6 @@
+import { type ClassValue, clsx } from "clsx";
+import { twMerge } from "tailwind-merge";
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs));
+}
diff --git a/apps/loan-qc/src/lib/validation-data.ts b/apps/loan-qc/src/lib/validation-data.ts
new file mode 100644
index 000000000..b0cfe40c3
--- /dev/null
+++ b/apps/loan-qc/src/lib/validation-data.ts
@@ -0,0 +1,157 @@
+import type { ValidationItem, Document, DocumentHighlight } from './types';
+
+// Document highlights based on Figma design
+const doc1Highlights: DocumentHighlight[] = [
+ {
+ id: 'hl-1',
+ documentId: 'doc-1',
+ boundingBox: { x: 13.6, y: 13.1, width: 18.8, height: 2.15 },
+ color: 'rgba(36, 175, 191, 0.3)',
+ label: 'Grantor Name',
+ },
+];
+
+const doc2Highlights: DocumentHighlight[] = [
+ {
+ id: 'hl-2',
+ documentId: 'doc-2',
+ boundingBox: { x: 20.3, y: 15.6, width: 19.3, height: 4.85 },
+ color: 'rgba(36, 175, 191, 0.3)',
+ label: 'Borrower Name',
+ },
+];
+
+export const mockValidationItems: ValidationItem[] = [
+ {
+ id: '1',
+ title: "Grantor's name matches Credit Memo",
+ aiEvaluation: { status: 'yes', confidence: 0.95 },
+ humanEvaluation: null,
+ details: {
+ agentReasoning:
+ 'Source (Grantor) name matches target (Borrower Name) after normalization.',
+ identifiedFields: [
+ {
+ label: 'Name',
+ sourceValue: 'Tilbrae Logistics Group LLC',
+ targetValue: 'TILBRAE LOGISTICS GROUP LLC',
+ sourceLabel: 'Grantor',
+ targetLabel: 'Borrower Name',
+ },
+ ],
+ },
+ comments: [],
+ documentHighlights: [...doc1Highlights, ...doc2Highlights],
+ },
+ {
+ id: '2',
+ title: 'Loan amount does not exceed approved amount',
+ aiEvaluation: { status: 'yes', confidence: 0.98 },
+ humanEvaluation: null,
+ details: {
+ agentReasoning:
+ 'Loan amount is within the approved limit.',
+ identifiedFields: [
+ { label: 'Loan Amount', value: '$500,000' },
+ { label: 'Approved Limit', value: '$750,000' },
+ ],
+ },
+ comments: [],
+ },
+ {
+ id: '3',
+ title: "Lender's address matches Credit Memo",
+ aiEvaluation: { status: 'no', confidence: 0.87 },
+ humanEvaluation: null,
+ details: {
+ agentReasoning:
+ 'Address mismatch detected. Source address differs from target.',
+ identifiedFields: [
+ {
+ label: 'Address',
+ sourceValue: '123 Main St, New York, NY 10001',
+ targetValue: '456 Oak Ave, Brooklyn, NY 11201',
+ sourceLabel: 'Agreement',
+ targetLabel: 'Credit Memo',
+ },
+ ],
+ },
+ comments: [],
+ },
+ {
+ id: '4',
+ title: "Borrower's name for signature matches ID",
+ aiEvaluation: { status: 'inconclusive', confidence: 0.42 },
+ humanEvaluation: 'needs-review',
+ details: {
+ agentReasoning:
+ 'Unable to conclusively match signature name with ID. Manual review required.',
+ identifiedFields: [
+ {
+ label: 'Name',
+ sourceValue: 'J. Smith',
+ targetValue: 'John P. Smith',
+ sourceLabel: 'Signature',
+ targetLabel: 'ID',
+ },
+ ],
+ },
+ comments: [],
+ },
+ {
+ id: '5',
+ title: 'Borrower information matches Credit Memo',
+ aiEvaluation: { status: 'yes', confidence: 0.92 },
+ humanEvaluation: null,
+ details: {
+ agentReasoning:
+ 'All borrower identifying information fields match between the application and Credit Memo.',
+ identifiedFields: [
+ {
+ label: 'Borrower Name',
+ sourceValue: 'TILBRAE LOGISTICS GROUP LLC',
+ targetValue: 'TILBRAE LOGISTICS GROUP LLC',
+ sourceLabel: 'Agreement',
+ targetLabel: 'Credit Memo',
+ },
+ {
+ label: 'Date of Birth',
+ sourceValue: 'N/A (Business Entity)',
+ targetValue: 'N/A (Business Entity)',
+ sourceLabel: 'Agreement',
+ targetLabel: 'Credit Memo',
+ },
+ {
+ label: 'Address',
+ sourceValue: '1234 Commerce Drive, Suite 200, Dallas, TX 75201',
+ targetValue: '1234 Commerce Drive, Suite 200, Dallas, TX 75201',
+ sourceLabel: 'Agreement',
+ targetLabel: 'Credit Memo',
+ },
+ {
+ label: 'SSN/TIN',
+ sourceValue: '**-***4567',
+ targetValue: '**-***4567',
+ sourceLabel: 'Agreement',
+ targetLabel: 'Credit Memo',
+ },
+ ],
+ },
+ comments: [],
+ },
+];
+
+export const mockDocuments: Document[] = [
+ {
+ id: 'doc-1',
+ name: 'Agreement to provide insurance',
+ imageUrl: '/documents/agreement-to-provide-insurance.png',
+ type: 'agreement',
+ },
+ {
+ id: 'doc-2',
+ name: 'Credit memo',
+ imageUrl: '/documents/credit-approval-memo.png',
+ type: 'credit-memo',
+ },
+];
diff --git a/apps/loan-qc/tsconfig.json b/apps/loan-qc/tsconfig.json
new file mode 100644
index 000000000..8b18f5c58
--- /dev/null
+++ b/apps/loan-qc/tsconfig.json
@@ -0,0 +1,43 @@
+{
+ "compilerOptions": {
+ "target": "esnext",
+ "lib": [
+ "dom",
+ "dom.iterable",
+ "esnext"
+ ],
+ "skipLibCheck": true,
+ "strict": true,
+ "noEmit": true,
+ "incremental": true,
+ "module": "esnext",
+ "esModuleInterop": true,
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "react-jsx",
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "baseUrl": ".",
+ "paths": {
+ "@/*": [
+ "./src/*"
+ ]
+ },
+ "allowJs": true
+ },
+ "include": [
+ "next-env.d.ts",
+ ".next/types/**/*.ts",
+ ".next/dev/types/**/*.ts",
+ "**/*.mts",
+ "**/*.ts",
+ "**/*.tsx"
+ ],
+ "exclude": [
+ "node_modules"
+ ]
+}
diff --git a/package.json b/package.json
index 76bb24114..677fd10a6 100644
--- a/package.json
+++ b/package.json
@@ -15,7 +15,7 @@
"build:apps": "turbo run build --filter='./apps/*'",
"build:analyze": "turbo run build:analyze",
"commit": "git-cz",
- "dev": "turbo run dev",
+ "dev": "turbo run dev --concurrency=20",
"dev:react-playground": "turbo run dev --filter=react-playground",
"dev:chat": "turbo run dev --filter=@uipath/ap-chat",
"dev:apollo-vertex": "turbo run dev --filter=apollo-vertex",
@@ -99,5 +99,8 @@
"lodash": "^4.17.23",
"lodash-es": "^4.17.23"
}
+ },
+ "dependencies": {
+ "agentation": "^1.3.2"
}
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 4d0624a2d..e41525d66 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -19,6 +19,10 @@ overrides:
importers:
.:
+ dependencies:
+ agentation:
+ specifier: ^1.3.2
+ version: 1.3.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
devDependencies:
'@biomejs/biome':
specifier: ^2.3.6
@@ -180,6 +184,9 @@ importers:
'@radix-ui/react-hover-card':
specifier: ^1.1.15
version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ '@radix-ui/react-id':
+ specifier: ^1.1.1
+ version: 1.1.1(@types/react@19.2.8)(react@19.2.3)
'@radix-ui/react-label':
specifier: ^2.1.8
version: 2.1.8(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
@@ -237,6 +244,9 @@ importers:
'@vercel/analytics':
specifier: ^1.5.0
version: 1.5.0(next@16.1.1(@babel/core@7.28.5)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.94.2))(react@19.2.3)
+ agentation:
+ specifier: ^1.3.2
+ version: 1.3.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
class-variance-authority:
specifier: ^0.7.1
version: 0.7.1
@@ -335,6 +345,82 @@ importers:
specifier: ^1.4.0
version: 1.4.0
+ apps/loan-qc:
+ dependencies:
+ '@radix-ui/react-avatar':
+ specifier: ^1.1.6
+ version: 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ '@radix-ui/react-popover':
+ specifier: ^1.1.15
+ version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ '@radix-ui/react-separator':
+ specifier: ^1.1.8
+ version: 1.1.8(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ '@radix-ui/react-slot':
+ specifier: ^1.2.4
+ version: 1.2.4(@types/react@19.2.8)(react@19.2.3)
+ '@radix-ui/react-toggle':
+ specifier: ^1.1.10
+ version: 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ '@radix-ui/react-toggle-group':
+ specifier: ^1.1.11
+ version: 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ '@radix-ui/react-tooltip':
+ specifier: ^1.2.8
+ version: 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ class-variance-authority:
+ specifier: ^0.7.1
+ version: 0.7.1
+ clsx:
+ specifier: ^2.1.1
+ version: 2.1.1
+ framer-motion:
+ specifier: ^11.18.2
+ version: 11.18.2(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ lucide-react:
+ specifier: ^0.468.0
+ version: 0.468.0(react@19.2.3)
+ next:
+ specifier: 16.1.1
+ version: 16.1.1(@babel/core@7.28.5)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.94.2)
+ react:
+ specifier: 19.2.3
+ version: 19.2.3
+ react-dom:
+ specifier: 19.2.3
+ version: 19.2.3(react@19.2.3)
+ react-resizable-panels:
+ specifier: ^3.0.6
+ version: 3.0.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ tailwind-merge:
+ specifier: ^2.6.0
+ version: 2.6.0
+ devDependencies:
+ '@biomejs/biome':
+ specifier: ^2.3.6
+ version: 2.3.6
+ '@tailwindcss/postcss':
+ specifier: ^4.1.17
+ version: 4.1.17
+ '@types/node':
+ specifier: ^24.10.1
+ version: 24.10.1
+ '@types/react':
+ specifier: ^19.2.6
+ version: 19.2.8
+ '@types/react-dom':
+ specifier: ^19.2.2
+ version: 19.2.3(@types/react@19.2.8)
+ postcss:
+ specifier: ^8.5.6
+ version: 8.5.6
+ tailwindcss:
+ specifier: ^4.1.17
+ version: 4.1.17
+ typescript:
+ specifier: ^5.9.3
+ version: 5.9.3
+
apps/react-playground:
dependencies:
'@emotion/react':
@@ -7011,6 +7097,12 @@ packages:
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
engines: {node: '>= 14'}
+ agentation@1.3.2:
+ resolution: {integrity: sha512-9yZ/3hTcNePr1asnMyipxAZU8nFdBibNfw7wTdLUd3ULTTQCp9QZX7Y5PTMzkYWX4fhqEI2LOjMCb3vkmZga9w==}
+ peerDependencies:
+ react: '>=18.0.0'
+ react-dom: '>=18.0.0'
+
aggregate-error@3.1.0:
resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==}
engines: {node: '>=8'}
@@ -8738,8 +8830,8 @@ packages:
fraction.js@5.3.4:
resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==}
- framer-motion@12.23.26:
- resolution: {integrity: sha512-cPcIhgR42xBn1Uj+PzOyheMtZ73H927+uWPDVhUMqxy8UHt6Okavb6xIz9J/phFUHUj0OncR6UvMfJTXoc/LKA==}
+ framer-motion@11.18.2:
+ resolution: {integrity: sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==}
peerDependencies:
'@emotion/is-prop-valid': '*'
react: ^18.0.0 || ^19.0.0
@@ -10464,14 +10556,14 @@ packages:
moo@0.5.2:
resolution: {integrity: sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==}
- motion-dom@12.23.23:
- resolution: {integrity: sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==}
+ motion-dom@11.18.1:
+ resolution: {integrity: sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==}
motion-dom@12.26.2:
resolution: {integrity: sha512-KLMT1BroY8oKNeliA3JMNJ+nbCIsTKg6hJpDb4jtRAJ7nCKnnpg/LTq/NGqG90Limitz3kdAnAVXecdFVGlWTw==}
- motion-utils@12.23.6:
- resolution: {integrity: sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==}
+ motion-utils@11.18.1:
+ resolution: {integrity: sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==}
motion-utils@12.24.10:
resolution: {integrity: sha512-x5TFgkCIP4pPsRLpKoI86jv/q8t8FQOiM/0E8QKBzfMozWHfkKap2gA1hOki+B5g3IsBNpxbUnfOum1+dgvYww==}
@@ -16961,7 +17053,7 @@ snapshots:
'@jest/schemas': 29.6.3
'@types/istanbul-lib-coverage': 2.0.6
'@types/istanbul-reports': 3.0.4
- '@types/node': 22.19.3
+ '@types/node': 24.10.1
'@types/yargs': 17.0.35
chalk: 4.1.2
@@ -20610,7 +20702,7 @@ snapshots:
'@types/webpack-bundle-analyzer@4.7.0(esbuild@0.27.0)':
dependencies:
- '@types/node': 22.19.3
+ '@types/node': 24.10.1
tapable: 2.3.0
webpack: 5.101.2(esbuild@0.27.0)
transitivePeerDependencies:
@@ -20984,6 +21076,11 @@ snapshots:
agent-base@7.1.4: {}
+ agentation@1.3.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
+ dependencies:
+ react: 19.2.3
+ react-dom: 19.2.3(react@19.2.3)
+
aggregate-error@3.1.0:
dependencies:
clean-stack: 2.2.0
@@ -22986,10 +23083,10 @@ snapshots:
fraction.js@5.3.4: {}
- framer-motion@12.23.26(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
+ framer-motion@11.18.2(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
dependencies:
- motion-dom: 12.23.23
- motion-utils: 12.23.6
+ motion-dom: 11.18.1
+ motion-utils: 11.18.1
tslib: 2.8.1
optionalDependencies:
'@emotion/is-prop-valid': 1.4.0
@@ -25094,21 +25191,21 @@ snapshots:
moo@0.5.2: {}
- motion-dom@12.23.23:
+ motion-dom@11.18.1:
dependencies:
- motion-utils: 12.23.6
+ motion-utils: 11.18.1
motion-dom@12.26.2:
dependencies:
motion-utils: 12.24.10
- motion-utils@12.23.6: {}
+ motion-utils@11.18.1: {}
motion-utils@12.24.10: {}
motion@12.23.26(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
dependencies:
- framer-motion: 12.23.26(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ framer-motion: 12.26.2(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
tslib: 2.8.1
optionalDependencies:
'@emotion/is-prop-valid': 1.4.0
diff --git a/turbo.json b/turbo.json
index d34aa7f62..f392615a2 100644
--- a/turbo.json
+++ b/turbo.json
@@ -1,6 +1,7 @@
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": ["**/.env.*local", "tsconfig.json", "pnpm-lock.json"],
+ "ui": "tui",
"tasks": {
"build": {
"dependsOn": ["^build"],