Feat: Ingest UI

This commit is contained in:
Harshith Mullapudi 2025-06-11 21:40:35 +05:30
parent 02c7b90374
commit a9034fb448
30 changed files with 1329 additions and 202 deletions

View File

@ -0,0 +1,75 @@
import {
flexRender,
getCoreRowModel,
useReactTable,
} from "@tanstack/react-table";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "../ui/table";
import { type PersonalAccessToken, useTokensColumns } from "./columns";
export const APITable = ({
personalAccessTokens,
}: {
personalAccessTokens: PersonalAccessToken[];
}) => {
const columns = useTokensColumns();
const table = useReactTable({
data: personalAccessTokens,
columns,
getCoreRowModel: getCoreRowModel(),
});
return (
<div className="mt-6">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id} className="text-sm">
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id} className="w-[90%] py-1">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
></TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
);
};

View File

@ -0,0 +1,116 @@
import { useFetcher } from "@remix-run/react";
import { type ColumnDef } from "@tanstack/react-table";
import { format } from "date-fns";
import { Button } from "../ui";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "~/components/ui/dialog";
import React from "react";
export interface PersonalAccessToken {
name: string;
id: string;
obfuscatedToken: string;
lastAccessedAt: Date | null;
createdAt: Date;
}
export const useTokensColumns = (): Array<ColumnDef<PersonalAccessToken>> => {
const fetcher = useFetcher();
const [open, setOpen] = React.useState(false);
const onDelete = (id: string) => {
fetcher.submit({ id }, { method: "DELETE", action: "/home/api" });
};
return [
{
accessorKey: "name",
header: () => {
return <span>Name</span>;
},
cell: ({ row }) => {
return (
<div className="py-2capitalize flex items-center gap-1 py-2">
{row.original.name}
</div>
);
},
},
{
accessorKey: "obfuscatedToken",
header: () => {
return <span>Token</span>;
},
cell: ({ row }) => {
return (
<div className="flex items-center gap-1 text-sm">
{row.original.obfuscatedToken}
</div>
);
},
},
{
accessorKey: "lastAccessedAt",
header: () => {
return <span>Last accessed</span>;
},
cell: ({ row }) => {
return (
<div className="flex min-w-[200px] items-center gap-1">
{row.original.lastAccessedAt
? format(row.original.lastAccessedAt, "MMM d, yyyy")
: "Never"}
</div>
);
},
},
{
accessorKey: "actions",
header: () => {
return <span>Actions</span>;
},
cell: ({ row }) => {
return (
<Dialog onOpenChange={setOpen} open={open}>
<DialogTrigger asChild>
<Button variant="ghost">Delete</Button>
</DialogTrigger>
<DialogContent className="p-3">
<DialogHeader>
<DialogTitle>Are you sure?</DialogTitle>
<DialogDescription>
This action cannot be undone. This will permanently delete
your API token.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
variant="ghost"
onClick={() => {
setOpen(false);
}}
>
Cancel
</Button>
<Button
variant="destructive"
onClick={() => onDelete(row.original.id)}
>
Delete
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
},
},
];
};

View File

@ -0,0 +1 @@
export * from "./api-table";

View File

@ -1 +1,2 @@
export * from "./ingest";
export * from "./search";

View File

@ -1,9 +1,10 @@
import { PlusIcon } from "lucide-react";
import { PlusIcon, Loader2 } from "lucide-react";
import { Button } from "../ui";
import { Textarea } from "../ui/textarea";
import { useState } from "react";
import { z } from "zod";
import { EpisodeType } from "@core/types";
import { useFetcher } from "@remix-run/react";
export const IngestBodyRequest = z.object({
episodeBody: z.string(),
@ -16,10 +17,27 @@ export const IngestBodyRequest = z.object({
export const Ingest = () => {
const [text, setText] = useState("");
const fetcher = useFetcher();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
fetcher.submit(
{
episodeBody: text,
type: "TEXT",
referenceTime: new Date().toISOString(),
source: "local",
},
{ method: "POST", action: "/home/dashboard" },
);
};
const isLoading = fetcher.state === "submitting";
return (
<div className="flex flex-col">
<form method="POST" action="/home/dashboard" className="flex flex-col">
<form onSubmit={handleSubmit} className="flex flex-col">
<input type="hidden" name="type" value="TEXT" />
<input
type="hidden"
@ -27,18 +45,27 @@ export const Ingest = () => {
value={new Date().toISOString()}
/>
<input type="hidden" name="source" value="local" />
<Textarea
name="episodeBody"
value={text}
placeholder="Tell what you want to add"
onChange={(e) => setText(e.target.value)}
disabled={isLoading}
/>
<div className="mt-2 flex justify-end">
<Button type="submit" variant="secondary" className="gap-1">
<PlusIcon size={16} />
Add
<Button
type="submit"
variant="secondary"
className="gap-1"
disabled={isLoading}
>
{isLoading ? (
<Loader2 size={16} className="animate-spin" />
) : (
<PlusIcon size={16} />
)}
{isLoading ? "Adding..." : "Add"}
</Button>
</div>
</form>

View File

@ -0,0 +1,98 @@
import { PlusIcon, SearchIcon, Loader2 } from "lucide-react";
import { Button } from "../ui";
import { Textarea } from "../ui/textarea";
import { useState } from "react";
import { z } from "zod";
import { EpisodeType } from "@core/types";
import { useFetcher } from "@remix-run/react";
export const Search = () => {
const [text, setText] = useState("");
const fetcher = useFetcher<undefined | Record<string, string[]>>();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
fetcher.submit(
{ query: text },
{ method: "POST", action: "/home/dashboard" },
);
};
const searchResults = () => {
const data = fetcher.data as {
episodes?: string[];
facts?: string[];
};
if (
(!data.episodes || data.episodes.length === 0) &&
(!data.facts || data.facts.length === 0)
) {
return (
<div className="mt-4">
<p className="text-muted-foreground">No results found</p>
</div>
);
}
return (
<div className="mt-4">
{data.episodes && data.episodes.length > 0 && (
<div className="mb-4">
<h3 className="mb-2 text-lg font-semibold">Episodes</h3>
{data.episodes.map((episode, index) => (
<div key={index} className="bg-secondary mb-2 rounded-lg p-3">
{episode}
</div>
))}
</div>
)}
{data.facts && data.facts.length > 0 && (
<div>
<h3 className="mb-2 text-lg font-semibold">Facts</h3>
{data.facts.map((fact, index) => (
<div key={index} className="bg-secondary mb-2 rounded-lg p-3">
{fact}
</div>
))}
</div>
)}
</div>
);
};
const isLoading = fetcher.state === "submitting";
return (
<div className="flex flex-col">
<form onSubmit={handleSubmit} className="flex flex-col">
<Textarea
name="query"
value={text}
placeholder="What do you want to search"
onChange={(e) => setText(e.target.value)}
disabled={isLoading}
/>
<div className="mt-2 flex justify-end">
<Button
type="submit"
variant="secondary"
className="gap-1"
disabled={isLoading}
>
{isLoading ? (
<Loader2 size={16} className="animate-spin" />
) : (
<SearchIcon size={16} />
)}
{isLoading ? "Searching..." : "Search"}
</Button>
</div>
</form>
{fetcher?.data && searchResults()}
</div>
);
};

View File

@ -114,31 +114,8 @@ export function GraphPopovers({
)}
</div>
<div className="space-y-3">
<p className="text-muted-foreground text-sm break-all">
<span className="mr-2 text-sm font-medium text-black dark:text-white">
Name:
</span>
{nodePopupContent?.node.name || "Unknown"}
</p>
<p className="text-muted-foreground text-sm break-words">
<span className="mr-2 text-sm font-medium text-black dark:text-white">
UUID:
</span>
{nodePopupContent?.node.uuid || "Unknown"}
</p>
<p className="text-muted-foreground text-sm break-words">
<span className="mr-2 text-sm font-medium text-black dark:text-white">
Created:
</span>
{nodePopupContent?.node.created_at &&
formatDate(nodePopupContent?.node.created_at)}
</p>
{attributesToDisplay.length > 0 && (
<div className="border-border border-t pt-2">
<p className="mb-2 text-sm font-medium text-black dark:text-white">
Properties:
</p>
<div>
<div className="space-y-1.5">
{attributesToDisplay.map(({ key, value }) => (
<p key={key} className="text-sm">
@ -155,33 +132,6 @@ export function GraphPopovers({
</div>
</div>
)}
{nodePopupContent?.node.summary && (
<div className="border-border border-t pt-2">
<p className="mb-1 text-sm font-medium text-black dark:text-white">
Summary:
</p>
<div
className="relative max-h-[200px] overflow-y-auto"
style={{
scrollbarWidth: "thin",
scrollbarColor: "rgba(155, 155, 155, 0.5) transparent",
pointerEvents: "auto",
touchAction: "auto",
WebkitOverflowScrolling: "touch",
}}
onWheel={(e) => {
e.stopPropagation();
const target = e.currentTarget;
target.scrollTop += e.deltaY;
}}
>
<p className="text-muted-foreground pr-4 text-sm break-words">
{nodePopupContent.node.summary}
</p>
</div>
</div>
)}
</div>
</div>
</PopoverContent>
@ -200,11 +150,7 @@ export function GraphPopovers({
>
<div className="bg-grayAlpha-100 mb-4 rounded-md p-2">
<p className="text-sm break-all">
{edgePopupContent?.source.name || "Unknown"} {" "}
<span className="font-medium">
{edgePopupContent?.relation.name || "Unknown"}
</span>{" "}
{edgePopupContent?.target.name || "Unknown"}
Episode {edgePopupContent?.target.name || "Unknown"}
</p>
</div>
<div className="space-y-2">
@ -220,63 +166,14 @@ export function GraphPopovers({
<span className="mr-2 text-sm font-medium text-black dark:text-white">
Type:
</span>
{edgePopupContent?.relation.name || "Unknown"}
{edgePopupContent?.relation.type || "Unknown"}
</p>
{edgePopupContent?.relation.fact && (
<p className="text-muted-foreground text-sm break-all">
<span className="mr-2 text-sm font-medium text-black dark:text-white">
Fact:
</span>
{edgePopupContent.relation.fact}
</p>
)}
{edgePopupContent?.relation.episodes?.length ? (
<div>
<p className="text-sm font-medium text-black dark:text-white">
Episodes:
</p>
<div className="mt-1 flex gap-2">
{edgePopupContent.relation.episodes.map((episode) => (
<span
key={episode}
className="bg-muted rounded-md px-2 py-1 text-xs"
>
{episode}
</span>
))}
</div>
</div>
) : null}
<p className="text-muted-foreground text-sm break-all">
<span className="mr-2 text-sm font-medium text-black dark:text-white">
Created:
</span>
{formatDate(edgePopupContent?.relation.created_at)}
{formatDate(edgePopupContent?.relation.createdAt)}
</p>
{edgePopupContent?.relation.valid_at && (
<p className="text-muted-foreground text-sm break-all">
<span className="mr-2 text-sm font-medium text-black dark:text-white">
Valid From:
</span>
{formatDate(edgePopupContent.relation.valid_at)}
</p>
)}
{edgePopupContent?.relation.expired_at && (
<p className="text-muted-foreground text-sm break-all">
<span className="mr-2 text-sm font-medium text-black dark:text-white">
Expired At:
</span>
{formatDate(edgePopupContent.relation.expired_at)}
</p>
)}
{edgePopupContent?.relation.invalid_at && (
<p className="text-muted-foreground text-sm break-all">
<span className="mr-2 text-sm font-medium text-black dark:text-white">
Invalid At:
</span>
{formatDate(edgePopupContent.relation.invalid_at)}
</p>
)}
</div>
</div>
</PopoverContent>

View File

@ -4,8 +4,7 @@ export interface Node {
summary?: string;
labels?: string[];
attributes?: Record<string, any>;
created_at: string;
updated_at: string;
createdAt: string;
}
export interface Edge {
@ -13,14 +12,7 @@ export interface Edge {
source_node_uuid: string;
target_node_uuid: string;
type: string;
name: string;
fact?: string;
episodes?: string[];
created_at: string;
updated_at: string;
valid_at?: string;
expired_at?: string;
invalid_at?: string;
createdAt: string;
}
export interface RawTriplet {

View File

@ -16,8 +16,7 @@ export function toGraphNode(node: Node): GraphNode {
value: node.name,
uuid: node.uuid,
name: node.name,
created_at: node.created_at,
updated_at: node.updated_at,
createdAt: node.createdAt,
attributes: node.attributes,
summary: node.summary,
labels: node.labels,
@ -28,7 +27,7 @@ export function toGraphNode(node: Node): GraphNode {
export function toGraphEdge(edge: Edge): GraphEdge {
return {
id: edge.uuid,
value: edge.name,
value: edge.type,
...edge,
};
}
@ -90,9 +89,8 @@ export function createTriplets(edges: Edge[], nodes: Node[]): RawTriplet[] {
target_node_uuid: node.uuid,
// Use a special type that we can filter out in the Graph component
type: "_isolated_node_",
name: "", // Empty name so it doesn't show a label
created_at: node.created_at,
updated_at: node.updated_at,
createdAt: node.createdAt,
};
return {

View File

@ -0,0 +1 @@
export * from "./ingestion-logs-table";

View File

@ -0,0 +1,98 @@
import {
flexRender,
getCoreRowModel,
useReactTable,
type ColumnDef,
} from "@tanstack/react-table";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "../ui/table";
// Define the type for your ingestion log
export type IngestionLog = {
id: string;
createdAt: string;
// Add other fields as needed
};
const useIngestionLogsColumns = (): ColumnDef<IngestionLog>[] => [
{
accessorKey: "id",
header: "ID",
cell: (info) => info.getValue(),
},
{
accessorKey: "createdAt",
header: "Created At",
cell: (info) => new Date(info.getValue() as string).toLocaleString(),
},
{
accessorKey: "status",
header: "Status",
cell: (info) => info.getValue(),
},
// Add more columns as needed
];
export const IngestionLogsTable = ({
ingestionLogs,
}: {
ingestionLogs: IngestionLog[];
}) => {
const columns = useIngestionLogsColumns();
const table = useReactTable({
data: ingestionLogs,
columns,
getCoreRowModel: getCoreRowModel(),
});
return (
<div className="mt-6">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead key={header.id} className="text-sm">
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id} className="w-[90%] py-2">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No logs found.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
);
};

View File

@ -6,33 +6,32 @@ import {
SidebarFooter,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "../ui/sidebar";
import { DashboardIcon } from "@radix-ui/react-icons";
import { Code, LucideFileStack } from "lucide-react";
import { Code, Search } from "lucide-react";
import { NavMain } from "./nav-main";
import { useUser } from "~/hooks/useUser";
import { NavUser } from "./nav-user";
import { useWorkspace } from "~/hooks/useWorkspace";
const data = {
user: {
name: "shadcn",
email: "m@example.com",
avatar: "/avatars/shadcn.jpg",
},
navMain: [
{
title: "Dashboard",
url: "/",
url: "/home/dashboard",
icon: DashboardIcon,
},
{
title: "API",
url: "/api",
url: "/home/api",
icon: Code,
},
{
title: "Logs",
url: "/home/logs",
icon: Search,
},
],
};
@ -45,16 +44,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
<SidebarHeader>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton
asChild
className="data-[slot=sidebar-menu-button]:!p-1.5"
>
<a href="#">
<span className="text-base font-semibold">
{workspace.name}
</span>
</a>
</SidebarMenuButton>
<span className="text-base font-semibold">{workspace.name}</span>
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
@ -62,7 +52,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
<NavMain items={data.navMain} />
</SidebarContent>
<SidebarFooter>
<SidebarFooter className="p-0">
<NavUser user={user} />
</SidebarFooter>
</Sidebar>

View File

@ -1,14 +1,11 @@
import { useUser } from "~/hooks/useUser";
import {
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "../ui/sidebar";
import { NavUser } from "./nav-user";
import { useLocation } from "@remix-run/react";
import { useLocation, useNavigate } from "@remix-run/react";
export const NavMain = ({
items,
@ -20,6 +17,7 @@ export const NavMain = ({
}[];
}) => {
const location = useLocation();
const navigate = useNavigate();
return (
<SidebarGroup>
@ -30,6 +28,7 @@ export const NavMain = ({
<SidebarMenuButton
tooltip={item.title}
isActive={location.pathname.includes(item.url)}
onClick={() => navigate(item.url)}
>
{item.icon && <item.icon />}
<span>{item.title}</span>

View File

@ -1,22 +1,16 @@
import { DotIcon, LogOut, User as UserI } from "lucide-react";
import { Avatar, AvatarFallback, AvatarImage, AvatarText } from "../ui/avatar";
import { LogOut } from "lucide-react";
import { AvatarText } from "../ui/avatar";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "../ui/dropdown-menu";
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "../ui/sidebar";
import { SidebarMenu, SidebarMenuItem, useSidebar } from "../ui/sidebar";
import type { User } from "~/models/user.server";
import { useUser } from "~/hooks/useUser";
import { Button } from "../ui";
export function NavUser({ user }: { user: User }) {
const { isMobile } = useSidebar();
@ -26,9 +20,10 @@ export function NavUser({ user }: { user: User }) {
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton
<Button
size="lg"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
variant="link"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground ,b-2 mb-2 gap-2 px-2"
>
<AvatarText
text={user.name ?? "User"}
@ -40,21 +35,16 @@ export function NavUser({ user }: { user: User }) {
{user.email}
</span>
</div>
</SidebarMenuButton>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
side={isMobile ? "bottom" : "right"}
side={isMobile ? "bottom" : "top"}
align="end"
sideOffset={4}
>
<DropdownMenuLabel className="p-0 font-normal">
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<Avatar className="h-8 w-8 rounded-lg">
<AvatarFallback className="rounded-lg">
{user.name}
</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{user.name}</span>
<span className="text-muted-foreground truncate text-xs">
@ -64,15 +54,11 @@ export function NavUser({ user }: { user: User }) {
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<UserI />
Account
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem>
<LogOut />
<DropdownMenuItem
className="flex gap-2"
onClick={() => (window.location.href = "/logout")}
>
<LogOut size={16} />
Log out
</DropdownMenuItem>
</DropdownMenuContent>

View File

@ -0,0 +1,63 @@
import { cva, type VariantProps } from "class-variance-authority";
import React from "react";
import { cn } from "../../lib/utils";
const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
{
variants: {
variant: {
default: "bg-background text-foreground",
success:
"border-success/50 text-success dark:border-success [&>svg]:text-success bg-success/10",
warning:
"border-warning/50 text-warning dark:border-warning [&>svg]:text-warning bg-warning/10",
destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive bg-destructive/10",
},
},
defaultVariants: {
variant: "default",
},
},
);
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div
ref={ref}
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
));
Alert.displayName = "Alert";
const AlertTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn("mb-1 leading-none font-medium tracking-tight", className)}
{...props}
/>
));
AlertTitle.displayName = "AlertTitle";
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props}
/>
));
AlertDescription.displayName = "AlertDescription";
export { Alert, AlertTitle, AlertDescription };

View File

@ -0,0 +1,132 @@
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { Cross2Icon } from "@radix-ui/react-icons";
import React from "react";
import { cn } from "../../lib/utils";
const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close;
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"bg-grayAlpha-300 fixed inset-0 overflow-auto font-sans",
className,
)}
{...props}
/>
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
interface DialogContentProps
extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> {
closeIcon?: boolean;
}
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
DialogContentProps
>(({ className, children, closeIcon = true, ...props }, ref) => (
<DialogPortal>
<div className="fixed top-0 left-0 z-20 h-[100vh] w-[100vw]">
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"flex h-full !w-[100vw] !max-w-[100%] items-start justify-center p-[calc(0.07px_+_13vh)_12px_13vh] font-sans duration-200",
)}
{...props}
>
<div
className={cn(
"bg-background-2 shadow-1 border-border z-50 flex max-h-full min-w-[500px] flex-col gap-4 overflow-hidden sm:rounded-lg",
className,
)}
>
{children}
{closeIcon && (
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-none disabled:pointer-events-none">
<Cross2Icon className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</div>
</DialogPrimitive.Content>
</div>
</DialogPortal>
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center font-sans sm:text-left",
className,
)}
{...props}
/>
);
DialogHeader.displayName = "DialogHeader";
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse font-sans sm:flex-row sm:justify-end sm:space-x-2",
className,
)}
{...props}
/>
);
DialogFooter.displayName = "DialogFooter";
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn("font-sans text-lg leading-none tracking-tight", className)}
{...props}
/>
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-muted-foreground font-sans text-sm", className)}
{...props}
/>
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogTrigger,
DialogClose,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
};

View File

@ -0,0 +1,127 @@
import * as React from "react"
import {
ChevronLeftIcon,
ChevronRightIcon,
MoreHorizontalIcon,
} from "lucide-react"
import { cn } from "~/lib/utils"
import { Button, buttonVariants } from "~/components/ui/button"
function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
return (
<nav
role="navigation"
aria-label="pagination"
data-slot="pagination"
className={cn("mx-auto flex w-full justify-center", className)}
{...props}
/>
)
}
function PaginationContent({
className,
...props
}: React.ComponentProps<"ul">) {
return (
<ul
data-slot="pagination-content"
className={cn("flex flex-row items-center gap-1", className)}
{...props}
/>
)
}
function PaginationItem({ ...props }: React.ComponentProps<"li">) {
return <li data-slot="pagination-item" {...props} />
}
type PaginationLinkProps = {
isActive?: boolean
} & Pick<React.ComponentProps<typeof Button>, "size"> &
React.ComponentProps<"a">
function PaginationLink({
className,
isActive,
size = "icon",
...props
}: PaginationLinkProps) {
return (
<a
aria-current={isActive ? "page" : undefined}
data-slot="pagination-link"
data-active={isActive}
className={cn(
buttonVariants({
variant: isActive ? "outline" : "ghost",
size,
}),
className
)}
{...props}
/>
)
}
function PaginationPrevious({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) {
return (
<PaginationLink
aria-label="Go to previous page"
size="default"
className={cn("gap-1 px-2.5 sm:pl-2.5", className)}
{...props}
>
<ChevronLeftIcon />
<span className="hidden sm:block">Previous</span>
</PaginationLink>
)
}
function PaginationNext({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) {
return (
<PaginationLink
aria-label="Go to next page"
size="default"
className={cn("gap-1 px-2.5 sm:pr-2.5", className)}
{...props}
>
<span className="hidden sm:block">Next</span>
<ChevronRightIcon />
</PaginationLink>
)
}
function PaginationEllipsis({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
aria-hidden
data-slot="pagination-ellipsis"
className={cn("flex size-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontalIcon className="size-4" />
<span className="sr-only">More pages</span>
</span>
)
}
export {
Pagination,
PaginationContent,
PaginationLink,
PaginationItem,
PaginationPrevious,
PaginationNext,
PaginationEllipsis,
}

View File

@ -0,0 +1,125 @@
import React from "react";
import { cn } from "../../lib/utils";
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<table
ref={ref}
className={cn("w-full caption-bottom", className)}
{...props}
/>
));
Table.displayName = "Table";
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead
ref={ref}
className={cn(
"[&_tr]:border-border text-muted-foreground font-mono [&_tr]:border-b",
className,
)}
{...props}
/>
));
TableHeader.displayName = "TableHeader";
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
));
TableBody.displayName = "TableBody";
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn(
"border-border bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
className,
)}
{...props}
/>
));
TableFooter.displayName = "TableFooter";
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-border hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
className,
)}
{...props}
/>
));
TableRow.displayName = "TableRow";
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
"text-muted-foreground h-10 px-2 text-left align-middle font-medium [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className,
)}
{...props}
/>
));
TableHead.displayName = "TableHead";
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn(
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className,
)}
{...props}
/>
));
TableCell.displayName = "TableCell";
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("text-muted-foreground mt-4 text-sm", className)}
{...props}
/>
));
TableCaption.displayName = "TableCaption";
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
};

View File

@ -40,15 +40,17 @@ async function processUserJob(userId: string, job: any) {
await prisma.ingestionQueue.update({
where: { id: job.data.queueId },
data: {
output: episodeDetails,
status: IngestionStatus.COMPLETED,
},
});
// your processing logic
} catch (err) {
} catch (err: any) {
await prisma.ingestionQueue.update({
where: { id: job.data.queueId },
data: {
error: err.message,
status: IngestionStatus.FAILED,
},
});
@ -86,12 +88,28 @@ export const addToQueue = async (
body: z.infer<typeof IngestBodyRequest>,
userId: string,
) => {
const user = await prisma.user.findFirst({
where: {
id: userId,
},
include: {
Workspace: true,
},
});
if (!user?.Workspace?.id) {
throw new Error(
"Workspace ID is required to create an ingestion queue entry.",
);
}
const queuePersist = await prisma.ingestionQueue.create({
data: {
spaceId: body.spaceId,
spaceId: body.spaceId ? body.spaceId : null,
data: body,
status: IngestionStatus.PENDING,
priority: 1,
workspaceId: user.Workspace.id,
},
});

View File

@ -78,25 +78,21 @@ export const getNodeLinks = async (userId: string) => {
labels: sourceNode.labels,
attributes: sourceNode.properties,
name: sourceNode.properties.name || "",
created_at: sourceNode.properties.created_at || "",
updated_at: sourceNode.properties.updated_at || "",
createdAt: sourceNode.properties.createdAt || "",
},
edge: {
uuid: edge.identity.toString(),
type: edge.type,
source_node_uuid: sourceNode.identity.toString(),
target_node_uuid: targetNode.identity.toString(),
name: edge.properties.name || "",
created_at: edge.properties.created_at || "",
updated_at: edge.properties.updated_at || "",
createdAt: edge.properties.createdAt || "",
},
targetNode: {
uuid: targetNode.identity.toString(),
labels: targetNode.labels,
attributes: targetNode.properties,
name: targetNode.properties.name || "",
created_at: targetNode.properties.created_at || "",
updated_at: targetNode.properties.updated_at || "",
createdAt: edge.properties.createdAt || "",
},
});
});

View File

@ -62,7 +62,6 @@ export async function action({ request }: ActionFunctionArgs) {
export default function ConfirmBasicDetails() {
const lastSubmission = useActionData<typeof action>();
const [selectedApps, setSelectedApps] = useState<string[]>([]);
const [form, fields] = useForm({
lastSubmission: lastSubmission as any,

View File

@ -0,0 +1,184 @@
import {
type LoaderFunctionArgs,
type ActionFunctionArgs,
} from "@remix-run/server-runtime";
import { Plus, Copy } from "lucide-react";
import { Button } from "~/components/ui";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "~/components/ui/dialog";
import { useFetcher } from "@remix-run/react";
import { Input } from "~/components/ui/input";
import { useState } from "react";
import { parse } from "@conform-to/zod";
import { json } from "@remix-run/node";
import { z } from "zod";
import {
createPersonalAccessToken,
getValidPersonalAccessTokens,
revokePersonalAccessToken,
} from "~/services/personalAccessToken.server";
import { requireUserId } from "~/services/session.server";
import { useTypedLoaderData } from "remix-typedjson";
import { APITable } from "~/components/api";
export const APIKeyBodyRequest = z.object({
name: z.string(),
});
export const APIKeyDeleteBodyRequest = z.object({
id: z.string(),
});
export async function action({ request }: ActionFunctionArgs) {
const userId = await requireUserId(request);
if (request.method === "DELETE") {
const formData = await request.formData();
const submission = parse(formData, {
schema: APIKeyDeleteBodyRequest,
});
if (!submission.value || submission.intent !== "submit") {
return json(submission);
}
const results = await revokePersonalAccessToken(submission.value.id);
return json(results);
}
const formData = await request.formData();
const submission = parse(formData, {
schema: APIKeyBodyRequest,
});
if (!submission.value || submission.intent !== "submit") {
return json(submission);
}
const results = await createPersonalAccessToken({
name: submission.value.name,
userId,
});
return json(results);
}
export async function loader({ request }: LoaderFunctionArgs) {
const userId = await requireUserId(request);
const personalAccessTokens = await getValidPersonalAccessTokens(userId);
return personalAccessTokens;
}
export default function API() {
const personalAccessTokens = useTypedLoaderData<typeof loader>();
const [open, setOpen] = useState(false);
const [showToken, setShowToken] = useState(false);
const fetcher = useFetcher<{ token: string }>();
const isSubmitting = fetcher.state !== "idle";
const [name, setName] = useState("");
const onSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
fetcher.submit({ name }, { method: "POST", action: "/home/api" });
setOpen(false);
setShowToken(true);
};
const copyToClipboard = (text: string | undefined) => {
text && navigator.clipboard.writeText(text);
};
return (
<div className="home flex h-full flex-col overflow-y-auto p-3">
<div className="flex items-center justify-between">
<div className="space-y-1 text-base">
<h2 className="text-lg font-semibold">API Keys</h2>
<p className="text-muted-foreground">
Create and manage API keys to access your data programmatically. API
keys allow secure access to your workspace's data and functionality
through our REST API.
</p>
</div>
<div>
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button
className="inline-flex items-center justify-center gap-1"
variant="secondary"
>
<Plus size={16} />
Create
</Button>
</DialogTrigger>
<DialogContent className="p-3">
<DialogHeader>
<DialogTitle>Create API Key</DialogTitle>
</DialogHeader>
<fetcher.Form
method="post"
onSubmit={onSubmit}
className="space-y-4"
>
<div>
<Input
id="name"
onChange={(e) => setName(e.target.value)}
name="name"
placeholder="Enter API key name"
className="mt-1"
required
/>
</div>
<div className="flex justify-end">
<Button
type="submit"
variant="secondary"
disabled={isSubmitting}
>
{isSubmitting ? "Creating..." : "Create API Key"}
</Button>
</div>
</fetcher.Form>
</DialogContent>
</Dialog>
<Dialog open={showToken} onOpenChange={setShowToken}>
<DialogContent className="p-3">
<DialogHeader>
<DialogTitle>Your New API Key</DialogTitle>
</DialogHeader>
<div className="space-y-2">
<p className="text-muted-foreground text-sm">
Make sure to copy your API key now. You won't be able to see
it again!
</p>
<div className="flex items-center gap-2 rounded-md border p-3">
<code className="flex-1 text-sm break-all">
{fetcher.data?.token}
</code>
<Button
variant="ghost"
size="sm"
onClick={() => copyToClipboard(fetcher.data?.token)}
>
<Copy size={16} />
</Button>
</div>
</div>
</DialogContent>
</Dialog>
</div>
</div>
<APITable personalAccessTokens={personalAccessTokens} />
</div>
);
}

View File

@ -1,4 +1,3 @@
import { useLocalCommonState } from "~/hooks/use-local-state";
import {
ResizableHandle,
ResizablePanel,
@ -15,17 +14,34 @@ import {
type ActionFunctionArgs,
} from "@remix-run/server-runtime";
import { requireUserId } from "~/services/session.server";
import { useActionData } from "@remix-run/react";
import { addToQueue, IngestBodyRequest } from "~/lib/ingest.server";
import { getNodeLinks } from "~/lib/neo4j.server";
import { useTypedLoaderData } from "remix-typedjson";
import { GraphVisualization } from "~/components/graph/graph-visualization";
import { Search } from "~/components/dashboard";
import { SearchBodyRequest } from "./search";
import { SearchService } from "~/services/search.server";
export async function action({ request }: ActionFunctionArgs) {
const userId = await requireUserId(request);
const formData = await request.formData();
// Check if this is a search request by looking for query parameter
if (formData.has("query")) {
// Handle ingest request
const submission = parse(formData, { schema: SearchBodyRequest });
const searchService = new SearchService();
if (!submission.value || submission.intent !== "submit") {
return json(submission);
}
const results = await searchService.search(submission.value.query, userId);
return json(results);
}
// Handle ingest request
const submission = parse(formData, { schema: IngestBodyRequest });
if (!submission.value || submission.intent !== "submit") {
@ -45,8 +61,6 @@ export async function loader({ request }: LoaderFunctionArgs) {
export default function Dashboard() {
const nodeLinks = useTypedLoaderData<typeof loader>();
const actionData = useActionData<typeof action>();
const [size, setSize] = useState(15);
return (
@ -57,8 +71,8 @@ export default function Dashboard() {
order={1}
id="home"
>
<div className="home flex h-full flex-col overflow-y-auto p-3">
<h2 className="text-xl"> Graph </h2>
<div className="home flex h-full flex-col overflow-y-auto p-3 text-base">
<h3 className="text-lg font-medium">Graph</h3>
<p className="text-muted-foreground"> Your memory graph </p>
<div className="bg-background-3 mt-2 grow rounded">
@ -86,7 +100,10 @@ export default function Dashboard() {
<TabsTrigger value="retrieve">Retrieve</TabsTrigger>
</TabsList>
<TabsContent value="ingest">
<Ingest actionData={actionData} />
<Ingest />
</TabsContent>
<TabsContent value="retrieve">
<Search />
</TabsContent>
</Tabs>
</ResizablePanel>

View File

@ -0,0 +1,52 @@
import { json } from "@remix-run/node";
import { Link, useLoaderData } from "@remix-run/react";
import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
import { IngestionLogsTable } from "~/components/logs";
import { getIngestionLogs } from "~/services/ingestionLogs.server";
import { requireUserId } from "~/services/session.server";
export async function loader({ request }: LoaderFunctionArgs) {
const userId = await requireUserId(request);
const url = new URL(request.url);
const page = Number(url.searchParams.get("page") || 1);
const { ingestionLogs, pagination } = await getIngestionLogs(userId, page);
return json({ ingestionLogs, pagination });
}
export default function Logs() {
const { ingestionLogs, pagination } = useLoaderData<typeof loader>();
return (
<div className="home flex h-full flex-col overflow-y-auto p-3">
<div className="flex items-center justify-between">
<div className="space-y-1 text-base">
<h2 className="text-lg font-semibold">Logs</h2>
<p className="text-muted-foreground">
View and monitor your data ingestion logs. These logs show the
history of data being loaded into memory, helping you track and
debug the ingestion process.
</p>
</div>
</div>
<IngestionLogsTable ingestionLogs={ingestionLogs} />
<div className="mt-4">
{Array.from({ length: pagination.pages }, (_, i) => (
<Link
key={i + 1}
to={`?page=${i + 1}`}
className={`mx-1 rounded border px-2 py-1 ${
pagination.currentPage === i + 1
? "bg-gray-200 font-bold"
: "bg-white"
}`}
>
{i + 1}
</Link>
))}
</div>
</div>
);
}

View File

@ -0,0 +1,46 @@
import { prisma } from "~/db.server";
export async function getIngestionLogs(
userId: string,
page: number = 1,
limit: number = 10,
) {
const user = await prisma.user.findUnique({
where: {
id: userId,
},
include: {
Workspace: true,
},
});
const skip = (page - 1) * limit;
const [ingestionLogs, total] = await Promise.all([
prisma.ingestionQueue.findMany({
where: {
workspaceId: user?.Workspace?.id,
},
skip,
take: limit,
orderBy: {
createdAt: "desc",
},
}),
prisma.ingestionQueue.count({
where: {
workspaceId: user?.Workspace?.id,
},
}),
]);
return {
ingestionLogs,
pagination: {
total,
pages: Math.ceil(total / limit),
currentPage: page,
limit,
},
};
}

View File

@ -324,6 +324,6 @@
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
@apply bg-background text-foreground text-base;
}
}

View File

@ -44,6 +44,7 @@
"@remix-run/server-runtime": "2.16.7",
"@remix-run/v1-meta": "^0.1.3",
"@remixicon/react": "^4.2.0",
"@tanstack/react-table": "^8.13.2",
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/postcss": "^4.1.7",
"ai": "4.3.14",
@ -54,6 +55,7 @@
"cross-env": "^7.0.3",
"d3": "^7.9.0",
"dayjs": "^1.11.10",
"date-fns": "^4.1.0",
"express": "^4.18.1",
"ioredis": "^5.6.1",
"isbot": "^4.1.0",
@ -72,6 +74,7 @@
"remix-typedjson": "0.3.1",
"remix-utils": "^7.7.0",
"react-resizable-panels": "^1.0.9",
"react-virtualized": "^9.22.6",
"tailwind-merge": "^2.6.0",
"tailwind-scrollbar-hide": "^2.0.0",
"tailwindcss-animate": "^1.0.7",

View File

@ -0,0 +1,12 @@
/*
Warnings:
- Added the required column `workspaceId` to the `IngestionQueue` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "IngestionQueue" ADD COLUMN "output" JSONB,
ADD COLUMN "workspaceId" TEXT NOT NULL;
-- AddForeignKey
ALTER TABLE "IngestionQueue" ADD CONSTRAINT "IngestionQueue_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "Workspace"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@ -54,8 +54,9 @@ model Workspace {
integrations String[]
userId String? @unique
user User? @relation(fields: [userId], references: [id])
userId String? @unique
user User? @relation(fields: [userId], references: [id])
IngestionQueue IngestionQueue[]
}
enum AuthenticationMethod {
@ -174,9 +175,13 @@ model IngestionQueue {
// Queue metadata
data Json // The actual data to be processed
output Json? // The processed output data
status IngestionStatus
priority Int @default(0)
workspaceId String
workspace Workspace @relation(fields: [workspaceId], references: [id])
// Error handling
error String?
retryCount Int @default(0)

69
pnpm-lock.yaml generated
View File

@ -135,6 +135,9 @@ importers:
'@tailwindcss/postcss':
specifier: ^4.1.7
version: 4.1.7
'@tanstack/react-table':
specifier: ^8.13.2
version: 8.21.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
ai:
specifier: 4.3.14
version: 4.3.14(react@18.3.1)(zod@3.23.8)
@ -156,6 +159,9 @@ importers:
d3:
specifier: ^7.9.0
version: 7.9.0
date-fns:
specifier: ^4.1.0
version: 4.1.0
dayjs:
specifier: ^1.11.10
version: 1.11.13
@ -198,6 +204,9 @@ importers:
react-resizable-panels:
specifier: ^1.0.9
version: 1.0.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-virtualized:
specifier: ^9.22.6
version: 9.22.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
remix-auth:
specifier: ^4.2.0
version: 4.2.0
@ -2173,6 +2182,17 @@ packages:
peerDependencies:
vite: ^5.2.0 || ^6
'@tanstack/react-table@8.21.3':
resolution: {integrity: sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==}
engines: {node: '>=12'}
peerDependencies:
react: '>=16.8'
react-dom: '>=16.8'
'@tanstack/table-core@8.21.3':
resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==}
engines: {node: '>=12'}
'@testing-library/dom@8.20.1':
resolution: {integrity: sha512-/DiOQ5xBxgdYRC8LNk7U+RWat0S3qRLeIw3ZIkMQ9kkVlRmwD/Eg8k8CqIpD6GW7u20JIUOfMKbxtiLutpjQ4g==}
engines: {node: '>=12'}
@ -2969,6 +2989,10 @@ packages:
resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==}
engines: {node: '>=0.8'}
clsx@1.2.1:
resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==}
engines: {node: '>=6'}
clsx@2.1.1:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
@ -3274,6 +3298,9 @@ packages:
dataloader@1.4.0:
resolution: {integrity: sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==}
date-fns@4.1.0:
resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
dayjs@1.11.13:
resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==}
@ -3406,6 +3433,9 @@ packages:
dom-accessibility-api@0.5.16:
resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==}
dom-helpers@5.2.1:
resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
dotenv-cli@7.4.4:
resolution: {integrity: sha512-XkBYCG0tPIes+YZr4SpfFv76SQrV/LeCE8CI7JSEMi3VR9MvTihCGTOtbIexD6i2mXF+6px7trb1imVCXSNMDw==}
hasBin: true
@ -5635,6 +5665,9 @@ packages:
react-is@17.0.2:
resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
react-lifecycles-compat@3.0.4:
resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==}
react-refresh@0.14.2:
resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==}
engines: {node: '>=0.10.0'}
@ -5688,6 +5721,12 @@ packages:
'@types/react':
optional: true
react-virtualized@9.22.6:
resolution: {integrity: sha512-U5j7KuUQt3AaMatlMJ0UJddqSiX+Km0YJxSqbAzIiGw5EmNz0khMyqP2hzgu4+QUtm+QPIrxzUX4raJxmVJnHg==}
peerDependencies:
react: ^16.3.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.3.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react@18.3.1:
resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
engines: {node: '>=0.10.0'}
@ -8662,6 +8701,14 @@ snapshots:
tailwindcss: 4.1.7
vite: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(yaml@2.8.0)
'@tanstack/react-table@8.21.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@tanstack/table-core': 8.21.3
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
'@tanstack/table-core@8.21.3': {}
'@testing-library/dom@8.20.1':
dependencies:
'@babel/code-frame': 7.27.1
@ -9612,6 +9659,8 @@ snapshots:
clone@1.0.4: {}
clsx@1.2.1: {}
clsx@2.1.1: {}
cluster-key-slot@1.1.2: {}
@ -9925,6 +9974,8 @@ snapshots:
dataloader@1.4.0: {}
date-fns@4.1.0: {}
dayjs@1.11.13: {}
debug@2.6.9:
@ -10039,6 +10090,11 @@ snapshots:
dom-accessibility-api@0.5.16: {}
dom-helpers@5.2.1:
dependencies:
'@babel/runtime': 7.27.3
csstype: 3.1.3
dotenv-cli@7.4.4:
dependencies:
cross-spawn: 7.0.6
@ -12617,6 +12673,8 @@ snapshots:
react-is@17.0.2: {}
react-lifecycles-compat@3.0.4: {}
react-refresh@0.14.2: {}
react-remove-scroll-bar@2.3.8(@types/react@18.3.23)(react@18.3.1):
@ -12663,6 +12721,17 @@ snapshots:
optionalDependencies:
'@types/react': 18.3.23
react-virtualized@9.22.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
'@babel/runtime': 7.27.3
clsx: 1.2.1
dom-helpers: 5.2.1
loose-envify: 1.4.0
prop-types: 15.8.1
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-lifecycles-compat: 3.0.4
react@18.3.1:
dependencies:
loose-envify: 1.4.0