Feat: UI fixes in graph
@ -1,13 +1,6 @@
|
||||
import { useFetcher, useNavigate } from "@remix-run/react";
|
||||
import { useEffect, useState, useCallback, useRef } from "react";
|
||||
import {
|
||||
List,
|
||||
AutoSizer,
|
||||
InfiniteLoader,
|
||||
type ListRowRenderer,
|
||||
} from "react-virtualized";
|
||||
import { format } from "date-fns";
|
||||
import { MessageSquare, Clock, Plus } from "lucide-react";
|
||||
import { AutoSizer, List, type ListRowRenderer } from "react-virtualized";
|
||||
import { cn } from "~/lib/utils";
|
||||
import { Button } from "../ui";
|
||||
|
||||
@ -40,10 +33,8 @@ type ConversationListResponse = {
|
||||
|
||||
export const ConversationList = ({
|
||||
currentConversationId,
|
||||
showNewConversationCTA,
|
||||
}: {
|
||||
currentConversationId?: string;
|
||||
showNewConversationCTA?: boolean;
|
||||
}) => {
|
||||
const fetcher = useFetcher<ConversationListResponse>();
|
||||
const navigate = useNavigate();
|
||||
@ -51,8 +42,9 @@ export const ConversationList = ({
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [hasNextPage, setHasNextPage] = useState(true);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
// const [searchTerm, setSearchTerm] = useState("");
|
||||
// const searchTimeoutRef = useRef<NodeJS.Timeout>();
|
||||
|
||||
// Prevent duplicate conversations when paginating
|
||||
const loadedConversationIds = useRef<Set<string>>(new Set());
|
||||
|
||||
const loadMoreConversations = useCallback(
|
||||
(page: number) => {
|
||||
@ -61,89 +53,75 @@ export const ConversationList = ({
|
||||
setIsLoading(true);
|
||||
const searchParams = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
limit: "25",
|
||||
limit: "5", // Increased for better density
|
||||
});
|
||||
|
||||
fetcher.load(`/api/v1/conversations?${searchParams}`, {
|
||||
flushSync: true,
|
||||
});
|
||||
},
|
||||
[isLoading, fetcher, currentPage],
|
||||
[isLoading, fetcher],
|
||||
);
|
||||
|
||||
// Initial load
|
||||
useEffect(() => {
|
||||
loadMoreConversations(1);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// Handle fetcher response
|
||||
useEffect(() => {
|
||||
if (fetcher.data && fetcher.state === "idle") {
|
||||
setIsLoading(false);
|
||||
const response = fetcher.data;
|
||||
|
||||
if (currentPage === 1) {
|
||||
setConversations(response.conversations);
|
||||
} else {
|
||||
setConversations((prev) => [...prev, ...response.conversations]);
|
||||
}
|
||||
// Prevent duplicate conversations
|
||||
const newConversations = response.conversations.filter(
|
||||
(c) => !loadedConversationIds.current.has(c.id),
|
||||
);
|
||||
newConversations.forEach((c) => loadedConversationIds.current.add(c.id));
|
||||
|
||||
setConversations((prev) => [...prev, ...newConversations]);
|
||||
setHasNextPage(response.pagination.hasNext);
|
||||
setCurrentPage(response.pagination.page);
|
||||
}
|
||||
}, [fetcher.data, fetcher.state, currentPage]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [fetcher.data, fetcher.state]);
|
||||
|
||||
// const handleSearch = useCallback(
|
||||
// (term: string) => {
|
||||
// setSearchTerm(term);
|
||||
// setCurrentPage(1);
|
||||
// setConversations([]);
|
||||
// setHasNextPage(true);
|
||||
|
||||
// if (searchTimeoutRef.current) {
|
||||
// clearTimeout(searchTimeoutRef.current);
|
||||
// }
|
||||
|
||||
// searchTimeoutRef.current = setTimeout(() => {
|
||||
// loadMoreConversations(1);
|
||||
// }, 300);
|
||||
// },
|
||||
// [loadMoreConversations],
|
||||
// );
|
||||
|
||||
const isRowLoaded = useCallback(
|
||||
({ index }: { index: number }) => {
|
||||
return !!conversations[index];
|
||||
},
|
||||
[conversations],
|
||||
);
|
||||
|
||||
const loadMoreRows = useCallback(() => {
|
||||
if (!hasNextPage || isLoading) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return new Promise<void>((resolve) => {
|
||||
if (conversations.length === 25) {
|
||||
const nextPage = currentPage + 1;
|
||||
loadMoreConversations(nextPage);
|
||||
const checkLoaded = () => {
|
||||
if (!isLoading) {
|
||||
resolve();
|
||||
} else {
|
||||
setTimeout(checkLoaded, 100);
|
||||
}
|
||||
};
|
||||
checkLoaded();
|
||||
}
|
||||
});
|
||||
}, [
|
||||
hasNextPage,
|
||||
isLoading,
|
||||
currentPage,
|
||||
loadMoreConversations,
|
||||
conversations,
|
||||
]);
|
||||
// The row count is conversations.length + 1 if hasNextPage, else just conversations.length
|
||||
const rowCount = hasNextPage
|
||||
? conversations.length + 1
|
||||
: conversations.length;
|
||||
|
||||
const rowRenderer: ListRowRenderer = useCallback(
|
||||
({ index, key, style }) => {
|
||||
// If this is the last row and hasNextPage, show the Load More button
|
||||
if (hasNextPage && index === conversations.length) {
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
style={style}
|
||||
className="-ml-1 flex items-center justify-start p-2 py-0 text-sm"
|
||||
>
|
||||
<Button
|
||||
variant="link"
|
||||
onClick={() => loadMoreConversations(currentPage + 1)}
|
||||
disabled={isLoading}
|
||||
className="w-fit underline underline-offset-4"
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<div className="border-primary mr-2 h-4 w-4 animate-spin rounded-full border-2 border-t-transparent" />
|
||||
Loading...
|
||||
</>
|
||||
) : (
|
||||
"Load More"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const conversation = conversations[index];
|
||||
|
||||
if (!conversation) {
|
||||
@ -158,16 +136,21 @@ export const ConversationList = ({
|
||||
|
||||
return (
|
||||
<div key={key} style={style}>
|
||||
<div className="p-1">
|
||||
<div className="px-1 pr-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
"border-border h-auto w-full justify-start p-2 text-left",
|
||||
currentConversationId === conversation.id && "bg-grayAlpha-100",
|
||||
"border-border h-auto w-full justify-start rounded p-2 py-1 text-left",
|
||||
currentConversationId === conversation.id &&
|
||||
"bg-accent text-accent-foreground font-semibold",
|
||||
)}
|
||||
onClick={() => {
|
||||
window.location.href = `/home/conversation/${conversation.id}`;
|
||||
navigate(`/home/conversation/${conversation.id}`);
|
||||
}}
|
||||
tabIndex={0}
|
||||
aria-current={
|
||||
currentConversationId === conversation.id ? "page" : undefined
|
||||
}
|
||||
>
|
||||
<div className="flex w-full items-start space-x-3">
|
||||
<div className="min-w-0 flex-1">
|
||||
@ -175,11 +158,6 @@ export const ConversationList = ({
|
||||
<p className={cn("truncate font-normal")}>
|
||||
{conversation.title || "Untitled Conversation"}
|
||||
</p>
|
||||
<div className="text-muted-foreground flex items-center space-x-1 text-xs">
|
||||
<span>
|
||||
{format(new Date(conversation.updatedAt), "MMM d")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -188,62 +166,32 @@ export const ConversationList = ({
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[conversations],
|
||||
[
|
||||
conversations,
|
||||
currentConversationId,
|
||||
hasNextPage,
|
||||
isLoading,
|
||||
currentPage,
|
||||
loadMoreConversations,
|
||||
navigate,
|
||||
],
|
||||
);
|
||||
|
||||
const rowCount = hasNextPage
|
||||
? conversations.length + 1
|
||||
: conversations.length;
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
{showNewConversationCTA && (
|
||||
<div className="flex items-center justify-start p-1 pb-0">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="w-full justify-start gap-2 py-4"
|
||||
onClick={() => {
|
||||
navigate("/home/conversation");
|
||||
}}
|
||||
>
|
||||
<Plus size={14} /> New conversation
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{/* <div className="border-b">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search conversations..."
|
||||
className="focus:ring-primary w-full rounded-none px-3 py-2 focus:ring-2 focus:outline-none"
|
||||
value={searchTerm}
|
||||
onChange={(e) => handleSearch(e.target.value)}
|
||||
/>
|
||||
</div> */}
|
||||
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<InfiniteLoader
|
||||
isRowLoaded={isRowLoaded}
|
||||
loadMoreRows={loadMoreRows}
|
||||
rowCount={rowCount}
|
||||
threshold={5}
|
||||
>
|
||||
{({ onRowsRendered, registerChild }) => (
|
||||
<AutoSizer>
|
||||
{({ height, width }) => (
|
||||
<List
|
||||
ref={registerChild}
|
||||
height={height}
|
||||
width={width}
|
||||
rowCount={rowCount}
|
||||
rowHeight={40}
|
||||
onRowsRendered={onRowsRendered}
|
||||
rowRenderer={rowRenderer}
|
||||
overscanRowCount={5}
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
<div className="flex h-full flex-col pt-1 pl-1">
|
||||
<div className="grow overflow-hidden">
|
||||
<AutoSizer>
|
||||
{({ height, width }) => (
|
||||
<List
|
||||
height={height}
|
||||
width={width}
|
||||
rowCount={rowCount}
|
||||
rowHeight={36} // Slightly taller for better click area
|
||||
rowRenderer={rowRenderer}
|
||||
overscanRowCount={5}
|
||||
/>
|
||||
)}
|
||||
</InfiniteLoader>
|
||||
</AutoSizer>
|
||||
</div>
|
||||
|
||||
{isLoading && conversations.length === 0 && (
|
||||
|
||||
@ -8,7 +8,7 @@ import { EditorContent, Placeholder, EditorRoot } from "novel";
|
||||
import { useCallback, useState } from "react";
|
||||
import { cn } from "~/lib/utils";
|
||||
import { Button } from "../ui";
|
||||
import { Loader } from "lucide-react";
|
||||
import { Loader, LoaderCircle } from "lucide-react";
|
||||
import { Form, useSubmit } from "@remix-run/react";
|
||||
|
||||
interface ConversationTextareaProps {
|
||||
@ -159,7 +159,7 @@ export function ConversationTextarea({
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Loader size={18} className="mr-1 animate-spin" />
|
||||
<LoaderCircle size={18} className="mr-1 animate-spin" />
|
||||
Stop
|
||||
</>
|
||||
) : (
|
||||
|
||||
@ -46,17 +46,7 @@ export const ConversationNew = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<ResizablePanelGroup direction="horizontal" className="bg-background-2">
|
||||
<ResizablePanel
|
||||
maxSize={50}
|
||||
defaultSize={16}
|
||||
minSize={16}
|
||||
collapsible
|
||||
collapsedSize={16}
|
||||
className="border-border h-[calc(100vh_-_60px)] min-w-[200px] border-r-1"
|
||||
>
|
||||
<ConversationList />
|
||||
</ResizablePanel>
|
||||
<ResizablePanelGroup direction="horizontal" className="rounded-md">
|
||||
<ResizableHandle className="w-1" />
|
||||
|
||||
<ResizablePanel
|
||||
@ -70,7 +60,7 @@ export const ConversationNew = ({
|
||||
onSubmit={(e) => submitForm(e)}
|
||||
className="pt-2"
|
||||
>
|
||||
<div className={cn("flex h-[calc(100vh_-_60px)] flex-col")}>
|
||||
<div className={cn("flex h-[calc(100vh_-_56px)] flex-col")}>
|
||||
<div className="flex h-full w-full flex-col items-start justify-start overflow-y-auto p-4">
|
||||
<div className="flex w-full flex-col items-center">
|
||||
<div className="w-full max-w-[90ch]">
|
||||
|
||||
@ -134,8 +134,6 @@ export const extensionsForConversation = [
|
||||
tiptapLink,
|
||||
horizontalRule,
|
||||
heading,
|
||||
AIHighlight,
|
||||
HighlightExtension,
|
||||
Table.configure({
|
||||
resizable: true,
|
||||
}),
|
||||
|
||||
@ -310,7 +310,7 @@ export const Graph = forwardRef<GraphRef, GraphProps>(
|
||||
...settings,
|
||||
barnesHutOptimize: true,
|
||||
strongGravityMode: false,
|
||||
gravity: 0.05,
|
||||
gravity: 0.1,
|
||||
scalingRatio: 10,
|
||||
slowDown: 5,
|
||||
},
|
||||
|
||||
@ -13,6 +13,7 @@ import { NavMain } from "./nav-main";
|
||||
import { useUser } from "~/hooks/useUser";
|
||||
import { NavUser } from "./nav-user";
|
||||
import Logo from "../logo/logo";
|
||||
import { ConversationList } from "../conversation";
|
||||
|
||||
const data = {
|
||||
navMain: [
|
||||
@ -44,24 +45,29 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
|
||||
return (
|
||||
<Sidebar
|
||||
collapsible="none"
|
||||
variant="inset"
|
||||
{...props}
|
||||
className="bg-background h-[100vh] w-[calc(var(--sidebar-width-icon)+1px)]! py-2"
|
||||
className="bg-background h-[100vh] py-2"
|
||||
>
|
||||
<SidebarHeader>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<div className="mt-1 flex w-full items-center justify-center">
|
||||
<div className="mt-1 flex w-full items-center justify-start gap-2">
|
||||
<Logo width={20} height={20} />
|
||||
C.O.R.E.
|
||||
</div>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
<NavMain items={data.navMain} />
|
||||
<div className="mt-4 flex h-full flex-col">
|
||||
<h2 className="text-muted-foreground px-4 text-sm"> History </h2>
|
||||
<ConversationList />
|
||||
</div>
|
||||
</SidebarContent>
|
||||
|
||||
<SidebarFooter className="p-0">
|
||||
<SidebarFooter className="px-2">
|
||||
<NavUser user={user} />
|
||||
</SidebarFooter>
|
||||
</Sidebar>
|
||||
|
||||
@ -7,6 +7,7 @@ import {
|
||||
SidebarMenuItem,
|
||||
} from "../ui/sidebar";
|
||||
import { useLocation, useNavigate } from "@remix-run/react";
|
||||
import { Button } from "../ui";
|
||||
|
||||
export const NavMain = ({
|
||||
items,
|
||||
@ -23,20 +24,22 @@ export const NavMain = ({
|
||||
return (
|
||||
<SidebarGroup>
|
||||
<SidebarGroupContent className="flex flex-col gap-2">
|
||||
<SidebarMenu>
|
||||
<SidebarMenu className="gap-0.5">
|
||||
{items.map((item) => (
|
||||
<SidebarMenuItem key={item.title}>
|
||||
<SidebarMenuButton
|
||||
tooltip={item.title}
|
||||
<Button
|
||||
isActive={location.pathname.includes(item.url)}
|
||||
className={cn(
|
||||
"bg-grayAlpha-100 w-fit gap-1 !rounded-md",
|
||||
location.pathname.includes(item.url) &&
|
||||
"!bg-grayAlpha-100 hover:bg-grayAlpha-100!",
|
||||
"!bg-accent !text-accent-foreground",
|
||||
)}
|
||||
onClick={() => navigate(item.url)}
|
||||
variant="ghost"
|
||||
>
|
||||
{item.icon && <item.icon />}
|
||||
</SidebarMenuButton>
|
||||
{item.icon && <item.icon size={16} />}
|
||||
{item.title}
|
||||
</Button>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
|
||||
@ -26,26 +26,10 @@ export function NavUser({ user }: { user: User }) {
|
||||
|
||||
return (
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem className="mb-2 flex justify-center">
|
||||
<Button
|
||||
variant="ghost"
|
||||
isActive={location.pathname.includes("settings")}
|
||||
className={cn(
|
||||
location.pathname.includes("settings") &&
|
||||
"!bg-grayAlpha-100 hover:bg-grayAlpha-100!",
|
||||
)}
|
||||
onClick={() => navigate("/settings")}
|
||||
>
|
||||
<Settings size={18} />
|
||||
</Button>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuItem className="flex justify-between">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="link"
|
||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground mb-2 gap-2 px-3"
|
||||
>
|
||||
<Button variant="link" className="mb-2 ml-2 gap-2 px-0">
|
||||
<AvatarText
|
||||
text={user.name ?? "User"}
|
||||
className="h-6 w-6 rounded"
|
||||
@ -55,7 +39,7 @@ export function NavUser({ user }: { user: User }) {
|
||||
<DropdownMenuContent
|
||||
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
|
||||
side={isMobile ? "bottom" : "top"}
|
||||
align="end"
|
||||
align="start"
|
||||
sideOffset={4}
|
||||
>
|
||||
<DropdownMenuLabel className="p-0 font-normal">
|
||||
@ -71,7 +55,14 @@ export function NavUser({ user }: { user: User }) {
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
className="flex gap-2"
|
||||
onClick={() => (window.location.href = "/logout")}
|
||||
onClick={() => navigate("/settings")}
|
||||
>
|
||||
<Settings size={16} />
|
||||
Settings
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="flex gap-2"
|
||||
onClick={() => navigate("/logout")}
|
||||
>
|
||||
<LogOut size={16} />
|
||||
Log out
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
import { useLocation } from "@remix-run/react";
|
||||
import { useLocation, useNavigate } from "@remix-run/react";
|
||||
import { Button } from "./button";
|
||||
import { Plus } from "lucide-react";
|
||||
import { SidebarTrigger } from "./sidebar";
|
||||
|
||||
const PAGE_TITLES: Record<string, string> = {
|
||||
"/home/dashboard": "Memory graph",
|
||||
@ -18,14 +21,37 @@ function getHeaderTitle(pathname: string): string {
|
||||
return "Documents";
|
||||
}
|
||||
|
||||
function isConversationDetail(pathname: string): boolean {
|
||||
// Matches /home/conversation/<something> but not /home/conversation exactly
|
||||
return /^\/home\/conversation\/[^/]+$/.test(pathname);
|
||||
}
|
||||
|
||||
export function SiteHeader() {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const title = getHeaderTitle(location.pathname);
|
||||
|
||||
const showNewConversationButton = isConversationDetail(location.pathname);
|
||||
|
||||
return (
|
||||
<header className="flex h-(--header-height) shrink-0 items-center gap-2 border-b transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-(--header-height)">
|
||||
<div className="flex w-full items-center gap-1 px-4 lg:gap-2">
|
||||
<h1 className="text-base">{title}</h1>
|
||||
<header className="border-border flex h-(--header-height) shrink-0 items-center gap-2 border-b transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-(--header-height)">
|
||||
<div className="flex w-full items-center justify-between gap-1 px-4 pr-2 lg:gap-2">
|
||||
<div className="flex items-center gap-1">
|
||||
<SidebarTrigger className="-ml-1" />
|
||||
<h1 className="text-base">{title}</h1>
|
||||
</div>
|
||||
<div>
|
||||
{showNewConversationButton && (
|
||||
<Button
|
||||
onClick={() => navigate("/home/conversation")}
|
||||
variant="secondary"
|
||||
className="gap-2"
|
||||
>
|
||||
<Plus size={14} />
|
||||
New conversation
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
|
||||
@ -227,7 +227,7 @@ function Sidebar({
|
||||
<div
|
||||
data-slot="sidebar-container"
|
||||
className={cn(
|
||||
"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex",
|
||||
"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) !px-0 transition-[left,right,width] duration-200 ease-linear md:flex",
|
||||
side === "left"
|
||||
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
|
||||
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
|
||||
@ -514,7 +514,11 @@ function SidebarMenuButton({
|
||||
data-sidebar="menu-button"
|
||||
data-size={size}
|
||||
data-active={isActive}
|
||||
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
|
||||
className={cn(
|
||||
sidebarMenuButtonVariants({ variant, size }),
|
||||
className,
|
||||
"rounded-md",
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -129,29 +129,16 @@ export default function SingleConversation() {
|
||||
}
|
||||
|
||||
return (
|
||||
<ResizablePanelGroup direction="horizontal" className="bg-background-2">
|
||||
<ResizablePanel
|
||||
maxSize={50}
|
||||
defaultSize={16}
|
||||
minSize={16}
|
||||
collapsible
|
||||
collapsedSize={16}
|
||||
className="border-border h-[calc(100vh_-_60px)] min-w-[200px] border-r-1"
|
||||
>
|
||||
<ConversationList
|
||||
currentConversationId={conversationId}
|
||||
showNewConversationCTA
|
||||
/>
|
||||
</ResizablePanel>
|
||||
<ResizablePanelGroup direction="horizontal" className="!rounded-md">
|
||||
<ResizableHandle className="w-1" />
|
||||
|
||||
<ResizablePanel
|
||||
collapsible
|
||||
collapsedSize={0}
|
||||
className="flex h-[calc(100vh_-_24px)] w-full flex-col"
|
||||
className="flex w-full flex-col"
|
||||
>
|
||||
<div className="relative flex h-[calc(100vh_-_70px)] w-full flex-col items-center justify-center overflow-auto">
|
||||
<div className="flex h-[calc(100vh_-_60px)] w-full flex-col justify-end overflow-hidden">
|
||||
<div className="flex h-[calc(100vh_-_56px)] w-full flex-col justify-end overflow-hidden">
|
||||
<ScrollAreaWithAutoScroll>
|
||||
{getConversations()}
|
||||
{conversationResponse && (
|
||||
|
||||
@ -13,6 +13,7 @@ import { useTypedLoaderData } from "remix-typedjson";
|
||||
import { SearchBodyRequest } from "./search";
|
||||
import { SearchService } from "~/services/search.server";
|
||||
import { GraphVisualizationClient } from "~/components/graph/graph-client";
|
||||
import { LoaderCircle } from "lucide-react";
|
||||
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
const userId = await requireUserId(request);
|
||||
@ -83,11 +84,11 @@ export default function Dashboard() {
|
||||
}, [userId]);
|
||||
|
||||
return (
|
||||
<div className="home flex h-[calc(100vh_-_60px)] flex-col overflow-y-auto p-3 text-base">
|
||||
<div className="home flex h-[calc(100vh_-_56px)] flex-col overflow-y-auto p-3 text-base">
|
||||
<div className="flex grow items-center justify-center rounded">
|
||||
{loading ? (
|
||||
<div className="flex h-full w-full flex-col items-center justify-center">
|
||||
<div className="mb-2 h-8 w-8 animate-spin rounded-full border-b-2 border-gray-400" />
|
||||
<LoaderCircle size={18} className="mr-1 animate-spin" />
|
||||
<span className="text-muted-foreground">Loading graph...</span>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@ -33,19 +33,17 @@ export default function Home() {
|
||||
{
|
||||
"--sidebar-width": "calc(var(--spacing) * 54)",
|
||||
"--header-height": "calc(var(--spacing) * 12)",
|
||||
background: "var(--background-2)",
|
||||
background: "var(--background)",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<AppSidebar variant="inset" />
|
||||
<SidebarInset className="bg-background h-[100vh] py-2 pr-2">
|
||||
<div className="bg-background-2 h-full rounded-md">
|
||||
<SiteHeader />
|
||||
<div className="flex h-[calc(100vh_-_60px)] flex-col">
|
||||
<div className="@container/main flex h-full flex-col gap-2">
|
||||
<div className="flex h-full flex-col">
|
||||
<Outlet />
|
||||
</div>
|
||||
<SidebarInset className="bg-background-2 h-full rounded pr-0">
|
||||
<SiteHeader />
|
||||
<div className="flex h-[calc(100vh_-_56px)] flex-col rounded">
|
||||
<div className="@container/main flex h-full flex-col gap-2">
|
||||
<div className="flex h-full flex-col">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -322,7 +322,7 @@
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
--header-height: 44px;
|
||||
--header-height: 40px;
|
||||
}
|
||||
body {
|
||||
@apply bg-background-2 text-foreground text-base;
|
||||
@ -467,3 +467,74 @@
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 10' style='fill: rgba(255,255,255, 0.5)'%3E%3Cpath d='M3,2 C2.44771525,2 2,1.55228475 2,1 C2,0.44771525 2.44771525,0 3,0 C3.55228475,0 4,0.44771525 4,1 C4,1.55228475 3.55228475,2 3,2 Z M3,6 C2.44771525,6 2,5.55228475 2,5 C2,4.44771525 2.44771525,4 3,4 C3.55228475,4 4,4.44771525 4,5 C4,5.55228475 3.55228475,6 3,6 Z M3,10 C2.44771525,10 2,9.55228475 2,9 C2,8.44771525 2.44771525,8 3,8 C3.55228475,8 4,8.44771525 4,9 C4,9.55228475 3.55228475,10 3,10 Z M7,2 C6.44771525,2 6,1.55228475 6,1 C6,0.44771525 6.44771525,0 7,0 C7.55228475,0 8,0.44771525 8,1 C8,1.55228475 7.55228475,2 7,2 Z M7,6 C6.44771525,6 6,5.55228475 6,5 C6,4.44771525 6.44771525,4 7,4 C7.55228475,4 8,4.44771525 8,5 C8,5.55228475 7.55228475,6 7,6 Z M7,10 C6.44771525,10 6,9.55228475 6,9 C6,8.44771525 6.44771525,8 7,8 C7.55228475,8 8,8.44771525 8,9 C8,9.55228475 7.55228475,10 7,10 Z'%3E%3C/path%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
@layer base {
|
||||
.tiptap {
|
||||
:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
pre {
|
||||
@apply bg-grayAlpha-100 text-foreground p-4 rounded-md w-fit;
|
||||
margin: 1.5rem 0;
|
||||
|
||||
code {
|
||||
background: none;
|
||||
color: inherit;
|
||||
font-size: 0.8rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Code styling */
|
||||
.hljs-comment,
|
||||
.hljs-quote {
|
||||
@apply text-muted-foreground;
|
||||
}
|
||||
|
||||
.hljs-variable,
|
||||
.hljs-template-variable,
|
||||
.hljs-attribute,
|
||||
.hljs-tag,
|
||||
.hljs-regexp,
|
||||
.hljs-link,
|
||||
.hljs-name,
|
||||
.hljs-selector-id,
|
||||
.hljs-selector-class {
|
||||
color: var(--custom-color-4); /* #886dbc */
|
||||
}
|
||||
|
||||
.hljs-number,
|
||||
.hljs-meta,
|
||||
.hljs-built_in,
|
||||
.hljs-builtin-name,
|
||||
.hljs-literal,
|
||||
.hljs-type,
|
||||
.hljs-params {
|
||||
color: var(--custom-color-2); /* #7b8a34 */
|
||||
}
|
||||
|
||||
.hljs-string,
|
||||
.hljs-symbol,
|
||||
.hljs-bullet {
|
||||
color: var(--custom-color-3); /* #1c91a8 */
|
||||
}
|
||||
|
||||
.hljs-title,
|
||||
.hljs-section {
|
||||
color: var(--custom-color-1); /* #b56455 */
|
||||
}
|
||||
|
||||
.hljs-keyword,
|
||||
.hljs-selector-tag {
|
||||
color: var(--custom-color-5); /* #ad6e30 */
|
||||
}
|
||||
|
||||
.hljs-emphasis {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hljs-strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 79 KiB |
BIN
apps/webapp/public/logo.png
Normal file
|
After Width: | Height: | Size: 79 KiB |