This commit is contained in:
phact 2025-08-18 22:19:43 -04:00
parent 92408f3617
commit 961723856b
19 changed files with 1117 additions and 377 deletions

View file

@ -0,0 +1,42 @@
"use client"
import * as React from "react";
import { cn } from "@/lib/utils";
import { Users } from "lucide-react";
import { useDiscordMembers } from "@/hooks/use-discord-members";
import { formatCount } from "@/lib/format-count";
interface DiscordLinkProps {
inviteCode?: string;
className?: string;
}
const DiscordLink = React.forwardRef<HTMLAnchorElement, DiscordLinkProps>(
({ inviteCode = "EqksyE2EX9", className }, ref) => {
const { data, isLoading, error } = useDiscordMembers(inviteCode);
return (
<a
ref={ref}
href={`https://discord.gg/${inviteCode}`}
target="_blank"
rel="noopener noreferrer"
className={cn(
"inline-flex h-8 items-center justify-center rounded-md px-2 text-sm font-medium text-muted-foreground shadow-sm transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
>
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.120.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/>
</svg>
<span className="hidden sm:inline ml-2">
{isLoading ? "..." : error ? "--" : data ? formatCount(data.approximate_member_count) : "--"}
</span>
</a>
);
}
);
DiscordLink.displayName = "DiscordLink";
export { DiscordLink };

View file

@ -0,0 +1,103 @@
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Upload, FolderOpen, Loader2 } from "lucide-react"
interface FileUploadAreaProps {
onFileSelected?: (file: File) => void
isLoading?: boolean
className?: string
}
const FileUploadArea = React.forwardRef<HTMLDivElement, FileUploadAreaProps>(
({ onFileSelected, isLoading = false, className }, ref) => {
const [isDragging, setIsDragging] = React.useState(false)
const fileInputRef = React.useRef<HTMLInputElement>(null)
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault()
setIsDragging(true)
}
const handleDragLeave = (e: React.DragEvent) => {
e.preventDefault()
setIsDragging(false)
}
const handleDrop = (e: React.DragEvent) => {
e.preventDefault()
setIsDragging(false)
const files = Array.from(e.dataTransfer.files)
if (files.length > 0 && onFileSelected) {
onFileSelected(files[0])
}
}
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = Array.from(e.target.files || [])
if (files.length > 0 && onFileSelected) {
onFileSelected(files[0])
}
}
const handleClick = () => {
if (!isLoading) {
fileInputRef.current?.click()
}
}
return (
<div
ref={ref}
className={cn(
"relative flex min-h-[150px] w-full cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed border-border bg-background p-6 text-center transition-colors hover:bg-muted/50",
isDragging && "border-primary bg-primary/5",
isLoading && "cursor-not-allowed opacity-50",
className
)}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
onClick={handleClick}
>
<input
ref={fileInputRef}
type="file"
onChange={handleFileSelect}
className="hidden"
disabled={isLoading}
/>
<div className="flex flex-col items-center gap-4">
{isLoading && (
<div className="rounded-full bg-muted p-4">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
)}
<div className="space-y-2">
<h3 className="text-lg font-medium text-foreground">
{isLoading ? "Processing file..." : "Drop files here or click to upload"}
</h3>
<p className="text-sm text-muted-foreground">
{isLoading ? "Please wait while your file is being processed" : ""}
</p>
</div>
{!isLoading && (
<Button size="sm">
+ Upload
</Button>
)}
</div>
</div>
)
}
)
FileUploadArea.displayName = "FileUploadArea"
export { FileUploadArea }

View file

@ -0,0 +1,40 @@
"use client"
import * as React from "react";
import { cn } from "@/lib/utils";
import { Github, Star, TrendingUp } from "lucide-react";
import { useGitHubStars } from "@/hooks/use-github-stars";
import { formatCount, formatExactCount } from "@/lib/format-count";
interface GitHubStarButtonProps {
repo?: string;
className?: string;
}
const GitHubStarButton = React.forwardRef<HTMLAnchorElement, GitHubStarButtonProps>(
({ repo = "phact/openrag", className }, ref) => {
const { data, isLoading, error } = useGitHubStars(repo);
return (
<a
ref={ref}
href={`https://github.com/${repo}`}
target="_blank"
rel="noopener noreferrer"
className={cn(
"inline-flex h-8 items-center justify-center rounded-md px-2 text-sm font-medium text-muted-foreground shadow-sm transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
>
<Github className="h-4 w-4" />
<span className="hidden sm:inline ml-2">
{isLoading ? "..." : error ? "--" : data ? formatCount(data.stargazers_count) : "--"}
</span>
</a>
);
}
);
GitHubStarButton.displayName = "GitHubStarButton";
export { GitHubStarButton };

View file

@ -30,7 +30,7 @@ export function Navigation() {
] ]
return ( return (
<div className="space-y-4 py-4 flex flex-col h-full bg-card"> <div className="space-y-4 py-4 flex flex-col h-full bg-background">
<div className="px-3 py-2 flex-1"> <div className="px-3 py-2 flex-1">
<div className="space-y-1"> <div className="space-y-1">
{routes.map((route) => ( {routes.map((route) => (

View file

@ -1,59 +1,88 @@
import * as React from "react" import { Slot } from "@radix-ui/react-slot";
import { Slot } from "@radix-ui/react-slot" import { cva, type VariantProps } from "class-variance-authority";
import { cva, type VariantProps } from "class-variance-authority" import * as React from "react";
import { cn } from "@/lib/utils";
import { cn } from "@/lib/utils"
const buttonVariants = cva( 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", "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-70 disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{ {
variants: { variants: {
variant: { variant: {
default: default: "bg-primary text-primary-foreground hover:bg-primary-hover",
"bg-white text-black shadow-xs hover:bg-gray-100", destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
destructive: outline: "border border-input hover:bg-input hover:text-accent-foreground",
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", primary: "border bg-background text-secondary-foreground hover:bg-muted hover:shadow-sm",
outline: warning: "bg-warning-foreground text-warning-text hover:bg-warning-foreground/90 hover:shadow-sm",
"border border-border/40 bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:hover:bg-input/50", secondary: "border border-muted bg-muted text-secondary-foreground hover:bg-secondary-foreground/5",
secondary: ghost: "text-foreground hover:bg-accent hover:text-accent-foreground disabled:!bg-transparent",
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", ghostActive: "bg-muted text-foreground hover:bg-secondary-hover hover:text-accent-foreground",
ghost: link: "underline-offset-4 hover:underline text-primary",
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
}, },
size: { size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3", default: "h-10 py-2 px-4",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", md: "h-8 py-2 px-4",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4", sm: "h-9 px-3 rounded-md",
icon: "size-9", xs: "py-0.5 px-3 rounded-md",
lg: "h-11 px-8 rounded-md",
iconMd: "p-1.5 rounded-md",
icon: "p-1 rounded-md",
iconSm: "p-0.5 rounded-md",
}, },
}, },
defaultVariants: { defaultVariants: {
variant: "default", variant: "default",
size: "default", size: "default",
}, },
} },
) );
function Button({ function toTitleCase(text: string) {
className, return text
variant, ?.split(" ")
size, ?.map((word) => word?.charAt(0)?.toUpperCase() + word?.slice(1)?.toLowerCase())
asChild = false, ?.join(" ");
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
} }
export { Button, buttonVariants } export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
loading?: boolean;
ignoreTitleCase?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, loading, disabled, asChild = false, children, ignoreTitleCase = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
let newChildren = children;
if (typeof children === "string") {
newChildren = ignoreTitleCase ? children : toTitleCase(children);
}
return (
<Comp
className={buttonVariants({ variant, size, className })}
disabled={loading || disabled}
ref={ref}
{...props}
>
{loading ? (
<span className="relative flex items-center justify-center">
<span className="invisible flex items-center justify-center gap-2">
{newChildren}
</span>
<span className="absolute inset-0 flex items-center justify-center">
<div className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
</span>
</span>
) : (
newChildren
)}
</Comp>
);
}
);
Button.displayName = "Button";
export { Button, buttonVariants };

View file

@ -1,92 +1,58 @@
import * as React from "react" import * as React from "react";
import { cn } from "@/lib/utils";
import { cn } from "@/lib/utils" const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div <div
data-slot="card" ref={ref}
className={cn( className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border border-border/40 py-6 shadow-sm", "rounded-lg border border-border bg-card text-card-foreground shadow-sm",
className className,
)} )}
{...props} {...props}
/> />
) )
} );
function CardHeader({ className, ...props }: React.ComponentProps<"div">) { const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
return ( ({ className, ...props }, ref) => (
<div <div ref={ref} className={cn("flex flex-col space-y-1.5 p-4", className)} {...props} />
data-slot="card-header" )
className={cn( );
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
)} ({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn("text-base font-semibold leading-tight tracking-tight", className)}
{...props} {...props}
/> />
) )
} );
function CardTitle({ className, ...props }: React.ComponentProps<"div">) { const CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
return ( ({ className, ...props }, ref) => (
<div <div ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
{...props}
/>
) )
} );
function CardDescription({ className, ...props }: React.ComponentProps<"div">) { const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
return ( ({ className, ...props }, ref) => (
<div <div ref={ref} className={cn("p-4 pt-0", className)} {...props} />
data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
) )
} );
function CardAction({ className, ...props }: React.ComponentProps<"div">) { const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
return ( ({ className, ...props }, ref) => (
<div <div ref={ref} className={cn("flex items-center p-4 pt-0", className)} {...props} />
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
) )
} );
function CardContent({ className, ...props }: React.ComponentProps<"div">) { Card.displayName = "Card";
return ( CardHeader.displayName = "CardHeader";
<div CardTitle.displayName = "CardTitle";
data-slot="card-content" CardDescription.displayName = "CardDescription";
className={cn("px-6", className)} CardContent.displayName = "CardContent";
{...props} CardFooter.displayName = "CardFooter";
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) { export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle };
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}

View file

@ -1,21 +1,46 @@
import * as React from "react" import * as React from "react";
import { cn } from "@/lib/utils";
import { cn } from "@/lib/utils" export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
icon?: React.ReactNode;
function Input({ className, type, ...props }: React.ComponentProps<"input">) { inputClassName?: string;
return (
<input
type={type}
data-slot="input"
className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-border/40 flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"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",
className
)}
{...props}
/>
)
} }
export { Input } const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, inputClassName, icon, type, placeholder, ...props }, ref) => {
return (
<label className={cn("relative block h-fit w-full text-sm", icon ? className : "")}>
{icon && (
<div className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 transform text-muted-foreground">
{icon}
</div>
)}
<input
autoComplete="off"
type={type}
placeholder={placeholder}
className={cn(
"primary-input !placeholder-transparent",
icon && "pl-9",
icon ? inputClassName : className,
)}
ref={ref}
{...props}
/>
<span
className={cn(
"pointer-events-none absolute top-1/2 -translate-y-1/2 pl-px text-placeholder-foreground",
icon ? "left-9" : "left-3",
props.value && "hidden",
)}
>
{placeholder}
</span>
</label>
);
},
);
Input.displayName = "Input";
export { Input };

View file

@ -0,0 +1,22 @@
import * as React from "react";
import { cn } from "@/lib/utils";
export interface ScrollAreaProps extends React.HTMLAttributes<HTMLDivElement> {}
const ScrollArea = React.forwardRef<HTMLDivElement, ScrollAreaProps>(
({ className, children, ...props }, ref) => {
return (
<div
ref={ref}
className={cn("overflow-auto scrollbar-hide", className)}
{...props}
>
{children}
</div>
);
}
);
ScrollArea.displayName = "ScrollArea";
export { ScrollArea };

View file

@ -0,0 +1,54 @@
import * as React from "react";
interface DiscordData {
approximate_member_count: number;
approximate_presence_count: number;
guild: {
name: string;
icon: string;
};
}
export const useDiscordMembers = (inviteCode: string) => {
const [data, setData] = React.useState<DiscordData | null>(null);
const [isLoading, setIsLoading] = React.useState(true);
const [error, setError] = React.useState<string | null>(null);
React.useEffect(() => {
const fetchDiscordData = async () => {
try {
setIsLoading(true);
setError(null);
const response = await fetch(
`https://discord.com/api/v10/invites/${inviteCode}?with_counts=true&with_expiration=true`,
{
headers: {
'Content-Type': 'application/json',
},
}
);
if (!response.ok) {
throw new Error(`Discord API error: ${response.status}`);
}
const discordData = await response.json();
setData(discordData);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to fetch Discord data');
console.error('Discord API Error:', err);
} finally {
setIsLoading(false);
}
};
fetchDiscordData();
// Refresh every 10 minutes
const interval = setInterval(fetchDiscordData, 10 * 60 * 1000);
return () => clearInterval(interval);
}, [inviteCode]);
return { data, isLoading, error };
};

View file

@ -0,0 +1,50 @@
import * as React from "react";
interface GitHubData {
stargazers_count: number;
forks_count: number;
open_issues_count: number;
}
export const useGitHubStars = (repo: string) => {
const [data, setData] = React.useState<GitHubData | null>(null);
const [isLoading, setIsLoading] = React.useState(true);
const [error, setError] = React.useState<string | null>(null);
React.useEffect(() => {
const fetchGitHubData = async () => {
try {
setIsLoading(true);
setError(null);
const response = await fetch(`https://api.github.com/repos/${repo}`, {
headers: {
'Accept': 'application/vnd.github.v3+json',
// Optional: Add your GitHub token for higher rate limits
// 'Authorization': `Bearer ${process.env.NEXT_PUBLIC_GITHUB_TOKEN}`,
},
});
if (!response.ok) {
throw new Error(`GitHub API error: ${response.status}`);
}
const repoData = await response.json();
setData(repoData);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to fetch GitHub data');
console.error('GitHub API Error:', err);
} finally {
setIsLoading(false);
}
};
fetchGitHubData();
// Refresh every 5 minutes
const interval = setInterval(fetchGitHubData, 5 * 60 * 1000);
return () => clearInterval(interval);
}, [repo]);
return { data, isLoading, error };
};

View file

@ -0,0 +1,13 @@
export const formatCount = (count: number): string => {
if (count >= 1_000_000) {
return `${(count / 1_000_000).toFixed(1)}M`;
}
if (count >= 1_000) {
return `${(count / 1_000).toFixed(1)}k`;
}
return count.toLocaleString();
};
export const formatExactCount = (count: number): string => {
return count.toLocaleString();
};

View file

@ -17,6 +17,8 @@
"@radix-ui/react-select": "^2.2.5", "@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.5", "@radix-ui/react-switch": "^1.2.5",
"@tailwindcss/forms": "^0.5.10",
"@tailwindcss/typography": "^0.5.16",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"lucide-react": "^0.525.0", "lucide-react": "^0.525.0",
@ -1868,6 +1870,46 @@
"tslib": "^2.8.0" "tslib": "^2.8.0"
} }
}, },
"node_modules/@tailwindcss/forms": {
"version": "0.5.10",
"resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.10.tgz",
"integrity": "sha512-utI1ONF6uf/pPNO68kmN1b8rEwNXv3czukalo8VtJH8ksIkZXr3Q3VYudZLkCsDd4Wku120uF02hYK25XGPorw==",
"license": "MIT",
"dependencies": {
"mini-svg-data-uri": "^1.2.3"
},
"peerDependencies": {
"tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1"
}
},
"node_modules/@tailwindcss/typography": {
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.16.tgz",
"integrity": "sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==",
"license": "MIT",
"dependencies": {
"lodash.castarray": "^4.4.0",
"lodash.isplainobject": "^4.0.6",
"lodash.merge": "^4.6.2",
"postcss-selector-parser": "6.0.10"
},
"peerDependencies": {
"tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1"
}
},
"node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": {
"version": "6.0.10",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
"integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
"license": "MIT",
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@tybys/wasm-util": { "node_modules/@tybys/wasm-util": {
"version": "0.10.0", "version": "0.10.0",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz",
@ -5211,11 +5253,22 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/lodash.castarray": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz",
"integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==",
"license": "MIT"
},
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
"license": "MIT"
},
"node_modules/lodash.merge": { "node_modules/lodash.merge": {
"version": "4.6.2", "version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/loose-envify": { "node_modules/loose-envify": {
@ -5278,6 +5331,15 @@
"node": ">=8.6" "node": ">=8.6"
} }
}, },
"node_modules/mini-svg-data-uri": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz",
"integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==",
"license": "MIT",
"bin": {
"mini-svg-data-uri": "cli.js"
}
},
"node_modules/minimatch": { "node_modules/minimatch": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",

View file

@ -18,6 +18,8 @@
"@radix-ui/react-select": "^2.2.5", "@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.5", "@radix-ui/react-switch": "^1.2.5",
"@tailwindcss/forms": "^0.5.10",
"@tailwindcss/typography": "^0.5.16",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"lucide-react": "^0.525.0", "lucide-react": "^0.525.0",

View file

@ -4,99 +4,317 @@
@layer base { @layer base {
:root { :root {
--font-sans: "Inter", sans-serif;
--font-mono: "JetBrains Mono", monospace;
--font-chivo: "Chivo", sans-serif;
/* Core Theme Colors */
--background: 0 0% 100%; --background: 0 0% 100%;
--foreground: 222.2 84% 4.9%; --foreground: 0 0% 0%;
--card: 0 0% 100%; --card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%; --card-foreground: 0 0% 0%;
--popover: 0 0% 100%; --popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%; --popover-foreground: 0 0% 0%;
--primary: 217.2 91.2% 59.8%; --primary: 0 0% 0%;
--primary-foreground: 210 40% 98%; --primary-foreground: 0 0% 100%;
--secondary: 210 40% 96.1%; --primary-hover: 240 4% 16%;
--secondary-foreground: 222.2 47.4% 11.2%; --secondary: 0 0% 100%;
--muted: 210 40% 96.1%; --secondary-foreground: 240 4% 16%;
--muted-foreground: 215.4 16.3% 46.9%; --secondary-hover: 240 6% 90%;
--accent: 210 40% 96.1%; --muted: 240 5% 96%;
--accent-foreground: 222.2 47.4% 11.2%; --muted-foreground: 240 4% 46%;
--destructive: 0 84.2% 60.2%; --accent: 240 5% 96%;
--destructive-foreground: 210 40% 98%; --accent-foreground: 0 0% 0%;
--border: 214.3 31.8% 91.4%; --destructive: 0 72% 51%;
--input: 214.3 31.8% 91.4%; --destructive-foreground: 0 0% 100%;
--ring: 217.2 91.2% 59.8%; --border: 240 6% 90%;
--radius: 0.65rem; --input: 240 6% 90%;
--chart-1: 12 76% 61%; --ring: 0 0% 0%;
--chart-2: 173 58% 39%; --placeholder-foreground: 240 5% 65%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%; /* Status Colors */
--chart-5: 27 87% 67%; --status-red: #ef4444;
--status-yellow: #eab308;
--status-green: #4ade80;
--status-blue: #2563eb;
/* Component Colors */
--component-icon: #d8598a;
--flow-icon: #2f67d0;
/* Data Type Colors */
--datatype-blue: 221.2 83.2% 53.3%;
--datatype-blue-foreground: 214.3 94.6% 92.7%;
--datatype-yellow: 40.6 96.1% 40.4%;
--datatype-yellow-foreground: 54.9 96.7% 88%;
--datatype-red: 0 72.2% 50.6%;
--datatype-red-foreground: 0 93.3% 94.1%;
--datatype-emerald: 161.4 93.5% 30.4%;
--datatype-emerald-foreground: 149.3 80.4% 90%;
--datatype-violet: 262.1 83.3% 57.8%;
--datatype-violet-foreground: 251.4 91.3% 95.5%;
/* Warning */
--warning: 48 96.6% 76.7%;
--warning-foreground: 240 6% 10%;
--radius: 0.5rem;
} }
.dark { .dark {
/* Main backgrounds - very dark charcoal */ --background: 240 6% 10%;
--background: 0 0% 10%; --foreground: 0 0% 100%;
--foreground: 210 40% 98%; --card: 240 6% 10%;
--card-foreground: 0 0% 100%;
--popover: 240 6% 10%;
--popover-foreground: 0 0% 100%;
--primary: 0 0% 100%;
--primary-foreground: 0 0% 0%;
--primary-hover: 240 6% 90%;
--secondary: 0 0% 0%;
--secondary-foreground: 240 6% 90%;
--secondary-hover: 240 4% 16%;
--muted: 240 4% 16%;
--muted-foreground: 240 5% 65%;
--accent: 240 4% 16%;
--accent-foreground: 0 0% 100%;
--destructive: 0 84% 60%;
--destructive-foreground: 0 0% 100%;
--border: 240 5% 26%;
--input: 240 5% 34%;
--ring: 0 0% 100%;
--placeholder-foreground: 240 4% 46%;
/* Card backgrounds - slightly lighter dark gray */ /* Dark mode data type colors */
--card: 0 0% 16%; --datatype-blue: 211.7 96.4% 78.4%;
--card-foreground: 210 40% 98%; --datatype-blue-foreground: 221.2 83.2% 53.3%;
--datatype-yellow: 50.4 97.8% 63.5%;
--datatype-yellow-foreground: 40.6 96.1% 40.4%;
--datatype-red: 0 93.5% 81.8%;
--datatype-red-foreground: 0 72.2% 50.6%;
--datatype-emerald: 156.2 71.6% 66.9%;
--datatype-emerald-foreground: 161.4 93.5% 30.4%;
--datatype-violet: 252.5 94.7% 85.1%;
--datatype-violet-foreground: 262.1 83.3% 57.8%;
/* Popover backgrounds */ --warning: 45.9 96.7% 64.5%;
--popover: 0 0% 16%; --warning-foreground: 240 6% 10%;
--popover-foreground: 210 40% 98%; }
/* Primary accent - bright teal/cyan */ * {
--primary: 162 100% 42%; @apply border-border;
--primary-foreground: 0 0% 10%;
/* Secondary elements - medium gray */
--secondary: 215 25% 27%;
--secondary-foreground: 210 40% 98%;
/* Muted elements - darker gray */
--muted: 215 25% 27%;
--muted-foreground: 215 20% 65%;
/* Accent elements - bright blue */
--accent: 0 0% 100%;
--accent-foreground: 0 0% 10%;
/* Destructive/error - red */
--destructive: 0 63% 31%;
--destructive-foreground: 210 40% 98%;
/* Borders - subtle gray */
--border: 215 25% 35%;
/* Input backgrounds - darker gray */
--input: 215 25% 27%;
/* Ring/focus colors - teal accent */
--ring: 162 100% 42%;
/* Chart colors - adjusted for dark theme */
--chart-1: 162 100% 42%;
--chart-2: 142 76% 36%;
--chart-3: 217 91% 60%;
--chart-4: 45 93% 58%;
--chart-5: 340 75% 55%;
} }
}
@layer base {
body { body {
@apply bg-background text-foreground; @apply bg-background text-foreground;
font-feature-settings: "rlig" 1, "calt" 1;
font-family: Inter, system-ui, sans-serif;
} }
} }
@layer utilities { @layer components {
/* Hide scrollbar for Chrome, Safari and Opera */ .header-arrangement {
.scrollbar-hide::-webkit-scrollbar { @apply flex w-full h-[53px] items-center justify-between border-b border-border;
display: none;
} }
/* Hide scrollbar for IE, Edge and Firefox */ .header-start-display {
.scrollbar-hide { @apply flex items-center gap-2;
-ms-overflow-style: none; /* IE and Edge */ }
scrollbar-width: none; /* Firefox */
.header-end-division {
@apply flex justify-end px-2;
}
.header-end-display {
@apply ml-auto mr-2 flex items-center gap-5;
}
.header-github-link {
@apply inline-flex h-8 items-center justify-center rounded-md border border-input px-2 text-sm font-medium text-muted-foreground shadow-sm ring-offset-background transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50;
}
.header-notifications {
@apply absolute right-[3px] h-1.5 w-1.5 rounded-full bg-destructive;
}
.header-menu-bar {
@apply flex items-center rounded-md py-1 text-sm font-medium;
}
.header-menu-bar-display {
@apply flex max-w-[110px] cursor-pointer items-center gap-2 lg:max-w-[150px];
}
.header-menu-flow-name {
@apply flex-1 truncate;
}
.header-menu-options {
@apply mr-2 h-4 w-4;
}
.side-bar-arrangement {
@apply flex h-full w-[14.5rem] flex-col overflow-hidden border-r scrollbar-hide;
}
.side-bar-search-div-placement {
@apply relative mx-auto flex items-center py-3;
}
.side-bar-components-icon {
@apply h-6 w-4 text-ring;
}
.side-bar-components-text {
@apply w-full truncate pr-1 text-xs text-foreground;
}
.side-bar-components-div-form {
@apply flex w-full items-center justify-between rounded-md rounded-l-none border border-l-0 border-dashed border-ring px-3 py-1 text-sm;
}
.side-bar-components-border {
@apply cursor-grab rounded-l-md border-l-8;
}
.side-bar-components-gap {
@apply flex flex-col gap-2 p-2;
}
.side-bar-components-div-arrangement {
@apply w-full overflow-auto pb-10 scrollbar-hide;
}
.side-bar-button-size {
@apply h-5 w-5;
}
.side-bar-button-size:hover {
@apply hover:text-accent-foreground;
}
.side-bar-buttons-arrangement {
@apply mb-2 mt-2 flex w-full items-center justify-between gap-2 px-2;
}
.side-bar-button {
@apply flex w-full;
}
.components-disclosure-arrangement {
@apply -mt-px flex w-full select-none items-center justify-between border-y border-y-input bg-primary-foreground px-3 py-2;
}
.components-disclosure-title {
@apply flex items-center text-sm text-primary;
}
.toolbar-wrapper {
@apply flex h-10 items-center gap-1 rounded-xl border border-border bg-background p-1 shadow-sm;
}
.playground-btn-flow-toolbar {
@apply relative inline-flex h-8 w-full items-center justify-center gap-1.5 rounded px-3 py-1.5 text-sm font-normal transition-all duration-500 ease-in-out;
}
.input-search {
@apply primary-input mx-2 pr-7;
}
/* GitHub Star Button */
.github-star-link {
@apply inline-flex h-8 items-center rounded-md text-sm font-medium text-muted-foreground shadow-sm transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2;
}
.github-star-content {
@apply flex items-center px-2 pr-1;
}
.github-star-icon {
@apply h-4 w-4;
}
.github-star-text {
@apply ml-1;
}
.github-star-count-section {
@apply border-l bg-muted/80 px-2 py-1;
}
.github-star-display {
@apply flex items-center gap-1;
}
.github-star-count-icon {
@apply h-3 w-3 fill-current;
}
.github-star-count {
@apply text-xs font-medium text-foreground;
}
.github-star-loading {
@apply flex items-center justify-center;
}
.github-star-skeleton {
@apply h-3 w-8 animate-pulse bg-muted-foreground/20 rounded;
}
.github-star-error {
@apply text-xs text-muted-foreground;
}
/* Discord Link */
.discord-link {
@apply inline-flex h-8 items-center rounded-md text-sm font-medium shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2;
background-color: #5865f2;
border-color: #5865f2;
color: white;
}
.discord-link:hover {
@apply opacity-90;
}
.discord-content {
@apply flex items-center px-2 pr-1;
}
.discord-icon {
@apply flex items-center justify-center;
}
.discord-text {
@apply ml-1;
}
.discord-member-section {
@apply border-l border-white/20 bg-black/10 px-2 py-1;
}
.discord-member-display {
@apply flex items-center gap-1;
}
.discord-member-icon {
@apply h-3 w-3;
}
.discord-member-count {
@apply text-xs font-medium;
}
.discord-loading {
@apply flex items-center justify-center;
}
.discord-skeleton {
@apply h-3 w-8 animate-pulse bg-white/20 rounded;
}
.discord-error {
@apply text-xs opacity-70;
} }
} }

View file

@ -11,6 +11,7 @@ import { Upload, FolderOpen, Loader2, PlugZap, RefreshCw, Download } from "lucid
import { ProtectedRoute } from "@/components/protected-route" import { ProtectedRoute } from "@/components/protected-route"
import { useTask } from "@/contexts/task-context" import { useTask } from "@/contexts/task-context"
import { useAuth } from "@/contexts/auth-context" import { useAuth } from "@/contexts/auth-context"
import { FileUploadArea } from "@/components/file-upload-area"
type FacetBucket = { key: string; count: number } type FacetBucket = { key: string; count: number }
@ -48,7 +49,6 @@ function KnowledgeSourcesPage() {
// File upload state // File upload state
const [fileUploadLoading, setFileUploadLoading] = useState(false) const [fileUploadLoading, setFileUploadLoading] = useState(false)
const [pathUploadLoading, setPathUploadLoading] = useState(false) const [pathUploadLoading, setPathUploadLoading] = useState(false)
const [selectedFile, setSelectedFile] = useState<File | null>(null)
const [folderPath, setFolderPath] = useState("/app/documents/") const [folderPath, setFolderPath] = useState("/app/documents/")
const [uploadStatus, setUploadStatus] = useState<string>("") const [uploadStatus, setUploadStatus] = useState<string>("")
@ -66,16 +66,13 @@ function KnowledgeSourcesPage() {
const [facetStats, setFacetStats] = useState<{ data_sources: FacetBucket[]; document_types: FacetBucket[]; owners: FacetBucket[] } | null>(null) const [facetStats, setFacetStats] = useState<{ data_sources: FacetBucket[]; document_types: FacetBucket[]; owners: FacetBucket[] } | null>(null)
// File upload handlers // File upload handlers
const handleFileUpload = async (e: React.FormEvent) => { const handleDirectFileUpload = async (file: File) => {
e.preventDefault()
if (!selectedFile) return
setFileUploadLoading(true) setFileUploadLoading(true)
setUploadStatus("") setUploadStatus("")
try { try {
const formData = new FormData() const formData = new FormData()
formData.append("file", selectedFile) formData.append("file", file)
const response = await fetch("/api/upload", { const response = await fetch("/api/upload", {
method: "POST", method: "POST",
@ -86,9 +83,6 @@ function KnowledgeSourcesPage() {
if (response.ok) { if (response.ok) {
setUploadStatus(`File processed successfully! ID: ${result.id}`) setUploadStatus(`File processed successfully! ID: ${result.id}`)
setSelectedFile(null)
const fileInput = document.getElementById("file-input") as HTMLInputElement
if (fileInput) fileInput.value = ""
// Refresh stats after successful file upload // Refresh stats after successful file upload
fetchStats() fetchStats()
@ -331,7 +325,7 @@ function KnowledgeSourcesPage() {
case "error": case "error":
return <Badge variant="destructive">Error</Badge> return <Badge variant="destructive">Error</Badge>
default: default:
return <Badge variant="outline" className="bg-muted/20 text-muted-foreground border-muted">Not Connected</Badge> return <Badge variant="outline" className="bg-muted/20 text-muted-foreground border-muted whitespace-nowrap">Not Connected</Badge>
} }
} }
@ -507,7 +501,7 @@ function KnowledgeSourcesPage() {
<div className="grid gap-6 md:grid-cols-2"> <div className="grid gap-6 md:grid-cols-2">
{/* File Upload Card */} {/* File Upload Card */}
<Card> <Card className="flex flex-col">
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
<Upload className="h-5 w-5" /> <Upload className="h-5 w-5" />
@ -517,40 +511,16 @@ function KnowledgeSourcesPage() {
Import a single document to be processed and indexed Import a single document to be processed and indexed
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent className="flex-1 flex flex-col justify-end">
<form onSubmit={handleFileUpload} className="space-y-4"> <FileUploadArea
<div className="space-y-2"> onFileSelected={handleDirectFileUpload}
<Label htmlFor="file-input">File</Label> isLoading={fileUploadLoading}
<Input />
id="file-input"
type="file"
onChange={(e) => setSelectedFile(e.target.files?.[0] || null)}
accept=".pdf,.docx,.txt,.md"
/>
</div>
<Button
type="submit"
disabled={!selectedFile || fileUploadLoading}
className="w-full"
>
{fileUploadLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Processing...
</>
) : (
<>
<Upload className="mr-2 h-4 w-4" />
Add File
</>
)}
</Button>
</form>
</CardContent> </CardContent>
</Card> </Card>
{/* Folder Upload Card */} {/* Folder Upload Card */}
<Card> <Card className="flex flex-col">
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
<FolderOpen className="h-5 w-5" /> <FolderOpen className="h-5 w-5" />
@ -560,7 +530,7 @@ function KnowledgeSourcesPage() {
Process all documents in a folder path Process all documents in a folder path
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent className="flex-1 flex flex-col justify-end">
<form onSubmit={handlePathUpload} className="space-y-4"> <form onSubmit={handlePathUpload} className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="folder-path">Folder Path</Label> <Label htmlFor="folder-path">Folder Path</Label>
@ -626,22 +596,24 @@ function KnowledgeSourcesPage() {
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center space-x-4"> <div className="flex items-center text-sm">
<Label htmlFor="maxFiles" className="text-sm font-medium"> <div className="flex items-center gap-3">
Max files per sync: <Label htmlFor="maxFiles" className="font-medium whitespace-nowrap">
</Label> Max files per sync:
<Input </Label>
id="maxFiles" <Input
type="number" id="maxFiles"
value={maxFiles} type="number"
onChange={(e) => setMaxFiles(parseInt(e.target.value) || 10)} value={maxFiles}
className="w-24" onChange={(e) => setMaxFiles(parseInt(e.target.value) || 10)}
min="1" className="w-16 min-w-16 max-w-16 flex-shrink-0"
max="100" min="1"
/> max="100"
<span className="text-sm text-muted-foreground"> />
(Leave blank or set to 0 for unlimited) <span className="text-muted-foreground whitespace-nowrap">
</span> (Leave blank or set to 0 for unlimited)
</span>
</div>
</div> </div>
</div> </div>
</CardContent> </CardContent>
@ -650,7 +622,7 @@ function KnowledgeSourcesPage() {
{/* Connectors Grid */} {/* Connectors Grid */}
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3"> <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{connectors.map((connector) => ( {connectors.map((connector) => (
<Card key={connector.id} className="relative"> <Card key={connector.id} className="relative flex flex-col">
<CardHeader> <CardHeader>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
@ -665,7 +637,7 @@ function KnowledgeSourcesPage() {
{getStatusBadge(connector.status)} {getStatusBadge(connector.status)}
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="flex-1 flex flex-col justify-end space-y-4">
{connector.status === "connected" ? ( {connector.status === "connected" ? (
<div className="space-y-3"> <div className="space-y-3">
<Button <Button

View file

@ -1,5 +1,5 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google"; import { Inter, JetBrains_Mono, Chivo } from "next/font/google";
import "./globals.css"; import "./globals.css";
import { ThemeProvider } from "@/components/theme-provider"; import { ThemeProvider } from "@/components/theme-provider";
import { AuthProvider } from "@/contexts/auth-context"; import { AuthProvider } from "@/contexts/auth-context";
@ -8,13 +8,18 @@ import { KnowledgeFilterProvider } from "@/contexts/knowledge-filter-context";
import { LayoutWrapper } from "@/components/layout-wrapper"; import { LayoutWrapper } from "@/components/layout-wrapper";
import { Toaster } from "@/components/ui/sonner"; import { Toaster } from "@/components/ui/sonner";
const geistSans = Geist({ const inter = Inter({
variable: "--font-geist-sans", variable: "--font-sans",
subsets: ["latin"], subsets: ["latin"],
}); });
const geistMono = Geist_Mono({ const jetbrainsMono = JetBrains_Mono({
variable: "--font-geist-mono", variable: "--font-mono",
subsets: ["latin"],
});
const chivo = Chivo({
variable: "--font-chivo",
subsets: ["latin"], subsets: ["latin"],
}); });
@ -30,8 +35,14 @@ export default function RootLayout({
}>) { }>) {
return ( return (
<html lang="en" suppressHydrationWarning> <html lang="en" suppressHydrationWarning>
<head>
<link
href="https://fonts.googleapis.com/css2?family=Chivo:ital,wght@0,100..900;1,100..900&family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap"
rel="stylesheet"
/>
</head>
<body <body
className={`${geistSans.variable} ${geistMono.variable} antialiased`} className={`${inter.variable} ${jetbrainsMono.variable} ${chivo.variable} antialiased`}
> >
<ThemeProvider <ThemeProvider
attribute="class" attribute="class"

View file

@ -5,11 +5,12 @@ import { Bell, BellRing } from "lucide-react"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
import { Navigation } from "@/components/navigation" import { Navigation } from "@/components/navigation"
import { ModeToggle } from "@/components/mode-toggle"
import { UserNav } from "@/components/user-nav" import { UserNav } from "@/components/user-nav"
import { TaskNotificationMenu } from "@/components/task-notification-menu" import { TaskNotificationMenu } from "@/components/task-notification-menu"
import { KnowledgeFilterDropdown } from "@/components/knowledge-filter-dropdown" import { KnowledgeFilterDropdown } from "@/components/knowledge-filter-dropdown"
import { KnowledgeFilterPanel } from "@/components/knowledge-filter-panel" import { KnowledgeFilterPanel } from "@/components/knowledge-filter-panel"
import { GitHubStarButton } from "@/components/github-star-button"
import { DiscordLink } from "@/components/discord-link"
import { useTask } from "@/contexts/task-context" import { useTask } from "@/contexts/task-context"
import { useKnowledgeFilter } from "@/contexts/knowledge-filter-context" import { useKnowledgeFilter } from "@/contexts/knowledge-filter-context"
@ -36,60 +37,64 @@ export function LayoutWrapper({ children }: { children: React.ReactNode }) {
) )
} }
// For all other pages, render with full navigation and task menu // For all other pages, render with Langflow-styled navigation and task menu
return ( return (
<div className="h-full relative"> <div className="h-full relative">
<header className="sticky top-0 z-50 w-full border-b border-border/40 bg-background"> <header className="header-arrangement bg-background sticky top-0 z-50">
<div className="flex h-14 items-center px-4"> <div className="header-start-display px-4">
<div className="flex items-center"> {/* Logo/Title */}
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="22" viewBox="0 0 24 22" fill="currentColor" className="h-6 w-6 mr-2 text-white"> <div className="flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="22" viewBox="0 0 24 22" fill="currentColor" className="h-6 w-6 text-primary">
<path d="M13.0486 0.462158H9.75399C9.44371 0.462158 9.14614 0.586082 8.92674 0.806667L4.03751 5.72232C3.81811 5.9429 3.52054 6.06682 3.21026 6.06682H1.16992C0.511975 6.06682 -0.0165756 6.61212 0.000397655 7.2734L0.0515933 9.26798C0.0679586 9.90556 0.586745 10.4139 1.22111 10.4139H3.59097C3.90124 10.4139 4.19881 10.2899 4.41821 10.0694L9.34823 5.11269C9.56763 4.89211 9.8652 4.76818 10.1755 4.76818H13.0486C13.6947 4.76818 14.2185 4.24157 14.2185 3.59195V1.63839C14.2185 0.988773 13.6947 0.462158 13.0486 0.462158Z"></path> <path d="M13.0486 0.462158H9.75399C9.44371 0.462158 9.14614 0.586082 8.92674 0.806667L4.03751 5.72232C3.81811 5.9429 3.52054 6.06682 3.21026 6.06682H1.16992C0.511975 6.06682 -0.0165756 6.61212 0.000397655 7.2734L0.0515933 9.26798C0.0679586 9.90556 0.586745 10.4139 1.22111 10.4139H3.59097C3.90124 10.4139 4.19881 10.2899 4.41821 10.0694L9.34823 5.11269C9.56763 4.89211 9.8652 4.76818 10.1755 4.76818H13.0486C13.6947 4.76818 14.2185 4.24157 14.2185 3.59195V1.63839C14.2185 0.988773 13.6947 0.462158 13.0486 0.462158Z"></path>
<path d="M19.5355 11.5862H22.8301C23.4762 11.5862 24 12.1128 24 12.7624V14.716C24 15.3656 23.4762 15.8922 22.8301 15.8922H19.957C19.6467 15.8922 19.3491 16.0161 19.1297 16.2367L14.1997 21.1934C13.9803 21.414 13.6827 21.5379 13.3725 21.5379H11.0026C10.3682 21.5379 9.84945 21.0296 9.83309 20.392L9.78189 18.3974C9.76492 17.7361 10.2935 17.1908 10.9514 17.1908H12.9918C13.302 17.1908 13.5996 17.0669 13.819 16.8463L18.7082 11.9307C18.9276 11.7101 19.2252 11.5862 19.5355 11.5862Z"></path> <path d="M19.5355 11.5862H22.8301C23.4762 11.5862 24 12.1128 24 12.7624V14.716C24 15.3656 23.4762 15.8922 22.8301 15.8922H19.957C19.6467 15.8922 19.3491 16.0161 19.1297 16.2367L14.1997 21.1934C13.9803 21.414 13.6827 21.5379 13.3725 21.5379H11.0026C10.3682 21.5379 9.84945 21.0296 9.83309 20.392L9.78189 18.3974C9.76492 17.7361 10.2935 17.1908 10.9514 17.1908H12.9918C13.302 17.1908 13.5996 17.0669 13.819 16.8463L18.7082 11.9307C18.9276 11.7101 19.2252 11.5862 19.5355 11.5862Z"></path>
<path d="M19.5355 2.9796L22.8301 2.9796C23.4762 2.9796 24 3.50622 24 4.15583V6.1094C24 6.75901 23.4762 7.28563 22.8301 7.28563H19.957C19.6467 7.28563 19.3491 7.40955 19.1297 7.63014L14.1997 12.5868C13.9803 12.8074 13.6827 12.9313 13.3725 12.9313H10.493C10.1913 12.9313 9.90126 13.0485 9.68346 13.2583L4.14867 18.5917C3.93087 18.8016 3.64085 18.9187 3.33917 18.9187H1.32174C0.675616 18.9187 0.151832 18.3921 0.151832 17.7425V15.7343C0.151832 15.0846 0.675616 14.558 1.32174 14.558H3.32468C3.63496 14.558 3.93253 14.4341 4.15193 14.2135L9.40827 8.92878C9.62767 8.70819 9.92524 8.58427 10.2355 8.58427H12.9918C13.302 8.58427 13.5996 8.46034 13.819 8.23976L18.7082 3.32411C18.9276 3.10353 19.2252 2.9796 19.5355 2.9796Z"></path> <path d="M19.5355 2.9796L22.8301 2.9796C23.4762 2.9796 24 3.50622 24 4.15583V6.1094C24 6.75901 23.4762 7.28563 22.8301 7.28563H19.957C19.6467 7.28563 19.3491 7.40955 19.1297 7.63014L14.1997 12.5868C13.9803 12.8074 13.6827 12.9313 13.3725 12.9313H10.493C10.1913 12.9313 9.90126 13.0485 9.68346 13.2583L4.14867 18.5917C3.93087 18.8016 3.64085 18.9187 3.33917 18.9187H1.32174C0.675616 18.9187 0.151832 18.3921 0.151832 17.7425V15.7343C0.151832 15.0846 0.675616 14.558 1.32174 14.558H3.32468C3.63496 14.558 3.93253 14.4341 4.15193 14.2135L9.40827 8.92878C9.62767 8.70819 9.92524 8.58427 10.2355 8.58427H12.9918C13.302 8.58427 13.5996 8.46034 13.819 8.23976L18.7082 3.32411C18.9276 3.10353 19.2252 2.9796 19.5355 2.9796Z"></path>
</svg> </svg>
<h1 className="text-lg font-semibold tracking-tight text-white"> <span className="text-lg font-semibold">OpenRAG</span>
OpenRAG
</h1>
</div> </div>
<div className="flex flex-1 items-center justify-end space-x-2"> </div>
<nav className="flex items-center space-x-2"> <div className="header-end-division">
{/* Knowledge Filter Dropdown */} <div className="header-end-display">
<KnowledgeFilterDropdown {/* Knowledge Filter Dropdown */}
selectedFilter={selectedFilter} <KnowledgeFilterDropdown
onFilterSelect={setSelectedFilter} selectedFilter={selectedFilter}
/> onFilterSelect={setSelectedFilter}
{/* Task Notification Bell */} />
<Button
variant="ghost" {/* GitHub Star Button */}
size="sm" <GitHubStarButton repo="phact/openrag" />
onClick={toggleMenu}
className="relative p-2" {/* Discord Link */}
> <DiscordLink inviteCode="EqksyE2EX9" />
{activeTasks.length > 0 ? (
<BellRing className="h-4 w-4 text-blue-500" /> {/* Task Notification Bell */}
) : ( <Button
<Bell className="h-4 w-4 text-muted-foreground" /> variant="ghost"
)} size="iconSm"
{activeTasks.length > 0 && ( onClick={toggleMenu}
<Badge className="relative"
variant="secondary" >
className="absolute -top-1 -right-1 h-5 w-5 flex items-center justify-center p-0 text-xs bg-blue-500 text-white border-0" {activeTasks.length > 0 ? (
> <BellRing className="h-4 w-4 text-blue-500" />
{activeTasks.length} ) : (
</Badge> <Bell className="h-4 w-4 text-muted-foreground" />
)} )}
</Button> {activeTasks.length > 0 && (
<UserNav /> <div className="header-notifications" />
<ModeToggle /> )}
</nav> </Button>
{/* Separator */}
<div className="w-px h-6 bg-border" />
<UserNav />
</div> </div>
</div> </div>
</header> </header>
<div className="hidden md:flex md:w-72 md:flex-col md:fixed md:top-14 md:bottom-0 md:left-0 z-[80] border-r border-border/40"> <div className="side-bar-arrangement bg-background fixed left-0 top-[53px] bottom-0 md:flex hidden">
<Navigation /> <Navigation />
</div> </div>
<main className={`md:pl-72 ${(isMenuOpen || isPanelOpen) ? 'md:pr-80' : ''}`}> <main className={`md:pl-72 md:pr-6 ${(isMenuOpen || isPanelOpen) ? 'md:pr-80' : ''}`}>
<div className="flex flex-col h-[calc(100vh-3.6rem)]"> <div className="flex flex-col min-h-screen">
<div className="flex-1 overflow-y-auto scrollbar-hide"> <div className="flex-1 overflow-y-auto scrollbar-hide">
<div className="container py-6 lg:py-8"> <div className="container py-6 lg:py-8">
{children} {children}

View file

@ -11,10 +11,12 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu" } from "@/components/ui/dropdown-menu"
import { useAuth } from "@/contexts/auth-context" import { useAuth } from "@/contexts/auth-context"
import { LogIn, LogOut, User } from "lucide-react" import { LogIn, LogOut, User, Moon, Sun, Settings, ChevronsUpDown } from "lucide-react"
import { useTheme } from "next-themes"
export function UserNav() { export function UserNav() {
const { user, isLoading, isAuthenticated, login, logout } = useAuth() const { user, isLoading, isAuthenticated, login, logout } = useAuth()
const { theme, setTheme } = useTheme()
if (isLoading) { if (isLoading) {
return ( return (
@ -39,13 +41,14 @@ export function UserNav() {
return ( return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="ghost" className="relative h-8 w-8 rounded-full"> <Button variant="ghost" className="flex items-center gap-1 h-8 px-1 rounded-full">
<Avatar className="h-8 w-8"> <Avatar className="h-6 w-6">
<AvatarImage src={user?.picture} alt={user?.name} /> <AvatarImage src={user?.picture} alt={user?.name} />
<AvatarFallback> <AvatarFallback className="text-xs">
{user?.name ? user.name.charAt(0).toUpperCase() : <User className="h-4 w-4" />} {user?.name ? user.name.charAt(0).toUpperCase() : <User className="h-3 w-3" />}
</AvatarFallback> </AvatarFallback>
</Avatar> </Avatar>
<ChevronsUpDown className="h-3 w-3 text-muted-foreground" />
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent className="w-56" align="end" forceMount> <DropdownMenuContent className="w-56" align="end" forceMount>
@ -58,6 +61,23 @@ export function UserNav() {
</div> </div>
</DropdownMenuLabel> </DropdownMenuLabel>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem>
<User className="mr-2 h-4 w-4" />
Profile
</DropdownMenuItem>
<DropdownMenuItem>
<Settings className="mr-2 h-4 w-4" />
<span>Settings</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
{theme === "light" ? (
<Moon className="mr-2 h-4 w-4" />
) : (
<Sun className="mr-2 h-4 w-4" />
)}
<span>Toggle Theme</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={logout} className="text-red-600 focus:text-red-600"> <DropdownMenuItem onClick={logout} className="text-red-600 focus:text-red-600">
<LogOut className="mr-2 h-4 w-4" /> <LogOut className="mr-2 h-4 w-4" />
<span>Log out</span> <span>Log out</span>

View file

@ -1,6 +1,11 @@
import type { Config } from "tailwindcss"; /** @type {import('tailwindcss').Config} */
import tailwindcssForms from "@tailwindcss/forms";
import tailwindcssTypography from "@tailwindcss/typography";
import { fontFamily } from "tailwindcss/defaultTheme";
import plugin from "tailwindcss/plugin";
import tailwindcssAnimate from "tailwindcss-animate";
const config: Config = { const config = {
darkMode: ["class"], darkMode: ["class"],
content: [ content: [
"./pages/**/*.{ts,tsx}", "./pages/**/*.{ts,tsx}",
@ -8,16 +13,47 @@ const config: Config = {
"./app/**/*.{ts,tsx}", "./app/**/*.{ts,tsx}",
"./src/**/*.{ts,tsx}", "./src/**/*.{ts,tsx}",
], ],
prefix: "",
theme: { theme: {
container: { container: {
center: true, center: true,
padding: "2rem",
screens: { screens: {
"2xl": "1400px", "2xl": "1400px",
"3xl": "1500px",
}, },
}, },
extend: { extend: {
screens: {
xl: "1200px",
"2xl": "1400px",
"3xl": "1500px",
},
keyframes: {
overlayShow: {
from: { opacity: 0 },
to: { opacity: 1 },
},
contentShow: {
from: {
opacity: 0,
transform: "translate(-50%, -50%) scale(0.95)",
clipPath: "inset(50% 0)",
},
to: {
opacity: 1,
transform: "translate(-50%, -50%) scale(1)",
clipPath: "inset(0% 0)",
},
},
wiggle: {
"0%, 100%": { transform: "scale(100%)" },
"50%": { transform: "scale(120%)" },
},
},
animation: {
overlayShow: "overlayShow 400ms cubic-bezier(0.16, 1, 0.3, 1)",
contentShow: "contentShow 400ms cubic-bezier(0.16, 1, 0.3, 1)",
wiggle: "wiggle 150ms ease-in-out 1",
},
colors: { colors: {
border: "hsl(var(--border))", border: "hsl(var(--border))",
input: "hsl(var(--input))", input: "hsl(var(--input))",
@ -27,10 +63,12 @@ const config: Config = {
primary: { primary: {
DEFAULT: "hsl(var(--primary))", DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))", foreground: "hsl(var(--primary-foreground))",
hover: "hsl(var(--primary-hover))",
}, },
secondary: { secondary: {
DEFAULT: "hsl(var(--secondary))", DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))", foreground: "hsl(var(--secondary-foreground))",
hover: "hsl(var(--secondary-hover))",
}, },
destructive: { destructive: {
DEFAULT: "hsl(var(--destructive))", DEFAULT: "hsl(var(--destructive))",
@ -52,37 +90,36 @@ const config: Config = {
DEFAULT: "hsl(var(--card))", DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))", foreground: "hsl(var(--card-foreground))",
}, },
// Custom colors from the screenshot "status-blue": "var(--status-blue)",
teal: { "status-green": "var(--status-green)",
50: "#f0fdfa", "status-red": "var(--status-red)",
100: "#ccfbf1", "status-yellow": "var(--status-yellow)",
200: "#99f6e4", "component-icon": "var(--component-icon)",
300: "#5eead4", "flow-icon": "var(--flow-icon)",
400: "#2dd4bf", "placeholder-foreground": "hsl(var(--placeholder-foreground))",
500: "#14b8a6", "datatype-blue": {
600: "#0d9488", DEFAULT: "hsl(var(--datatype-blue))",
700: "#0f766e", foreground: "hsl(var(--datatype-blue-foreground))",
800: "#115e59",
900: "#134e4a",
DEFAULT: "#00D4AA", // Primary teal from screenshot
}, },
success: { "datatype-yellow": {
DEFAULT: "#10B981", // Success green from screenshot DEFAULT: "hsl(var(--datatype-yellow))",
foreground: "#ffffff", foreground: "hsl(var(--datatype-yellow-foreground))",
}, },
// Additional grays matching the screenshot "datatype-red": {
gray: { DEFAULT: "hsl(var(--datatype-red))",
850: "#1a1a1a", // Very dark background foreground: "hsl(var(--datatype-red-foreground))",
800: "#2a2a2a", // Card background },
750: "#333333", // Slightly lighter card "datatype-emerald": {
700: "#374151", // Input background DEFAULT: "hsl(var(--datatype-emerald))",
600: "#4b5563", // Border color foreground: "hsl(var(--datatype-emerald-foreground))",
500: "#6b7280", // Muted text },
400: "#9ca3af", // Secondary text "datatype-violet": {
300: "#d1d5db", DEFAULT: "hsl(var(--datatype-violet))",
200: "#e5e7eb", foreground: "hsl(var(--datatype-violet-foreground))",
100: "#f3f4f6", },
50: "#f8f9fa", // Primary text warning: {
DEFAULT: "hsl(var(--warning))",
foreground: "hsl(var(--warning-foreground))",
}, },
}, },
borderRadius: { borderRadius: {
@ -90,23 +127,92 @@ const config: Config = {
md: "calc(var(--radius) - 2px)", md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)", sm: "calc(var(--radius) - 4px)",
}, },
keyframes: { fontFamily: {
"accordion-down": { sans: ["var(--font-sans)", ...fontFamily.sans],
from: { height: "0" }, mono: ["var(--font-mono)", ...fontFamily.mono],
to: { height: "var(--radix-accordion-content-height)" }, chivo: ["var(--font-chivo)", ...fontFamily.sans],
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
}, },
animation: { fontSize: {
"accordion-down": "accordion-down 0.2s ease-out", xxs: "11px",
"accordion-up": "accordion-up 0.2s ease-out", mmd: "13px",
}, },
}, },
}, },
plugins: [require("tailwindcss-animate")], plugins: [
tailwindcssAnimate,
tailwindcssForms({
strategy: "class",
}),
plugin(({ addUtilities }) => {
addUtilities({
".scrollbar-hide": {
"-ms-overflow-style": "none",
"scrollbar-width": "none",
"&::-webkit-scrollbar": {
display: "none",
},
},
".truncate-multiline": {
display: "-webkit-box",
"-webkit-line-clamp": "3",
"-webkit-box-orient": "vertical",
overflow: "hidden",
"text-overflow": "ellipsis",
},
".custom-scroll": {
"&::-webkit-scrollbar": {
width: "8px",
height: "8px",
},
"&::-webkit-scrollbar-track": {
backgroundColor: "hsl(var(--muted))",
},
"&::-webkit-scrollbar-thumb": {
backgroundColor: "hsl(var(--border))",
borderRadius: "999px",
},
"&::-webkit-scrollbar-thumb:hover": {
backgroundColor: "hsl(var(--placeholder-foreground))",
},
},
".primary-input": {
display: "block",
width: "100%",
borderRadius: "0.375rem",
border: "1px solid hsl(var(--border))",
backgroundColor: "hsl(var(--background))",
paddingLeft: "0.75rem",
paddingRight: "0.75rem",
paddingTop: "0.5rem",
paddingBottom: "0.5rem",
fontSize: "0.875rem",
textAlign: "left",
textOverflow: "ellipsis",
"&::placeholder": {
color: "hsl(var(--muted-foreground))",
},
"&:hover": {
borderColor: "hsl(var(--muted-foreground))",
},
"&:focus": {
borderColor: "hsl(var(--foreground))",
outline: "none",
boxShadow: "none",
"&::placeholder": {
color: "transparent",
},
},
"&:disabled": {
pointerEvents: "none",
cursor: "not-allowed",
backgroundColor: "hsl(var(--muted))",
color: "hsl(var(--muted-foreground))",
},
},
});
}),
tailwindcssTypography,
],
}; };
export default config; export default config;