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 (
<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="space-y-1">
{routes.map((route) => (

View file

@ -1,59 +1,88 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
"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: {
variant: {
default:
"bg-white text-black shadow-xs hover:bg-gray-100",
destructive:
"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",
outline:
"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:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
default: "bg-primary text-primary-foreground hover:bg-primary-hover",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: "border border-input hover:bg-input hover:text-accent-foreground",
primary: "border bg-background text-secondary-foreground hover:bg-muted hover:shadow-sm",
warning: "bg-warning-foreground text-warning-text hover:bg-warning-foreground/90 hover:shadow-sm",
secondary: "border border-muted bg-muted text-secondary-foreground hover:bg-secondary-foreground/5",
ghost: "text-foreground hover:bg-accent hover:text-accent-foreground disabled:!bg-transparent",
ghostActive: "bg-muted text-foreground hover:bg-secondary-hover hover:text-accent-foreground",
link: "underline-offset-4 hover:underline text-primary",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
default: "h-10 py-2 px-4",
md: "h-8 py-2 px-4",
sm: "h-9 px-3 rounded-md",
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: {
variant: "default",
size: "default",
},
}
)
},
);
function Button({
className,
variant,
size,
asChild = false,
...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}
/>
)
function toTitleCase(text: string) {
return text
?.split(" ")
?.map((word) => word?.charAt(0)?.toUpperCase() + word?.slice(1)?.toLowerCase())
?.join(" ");
}
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"
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div
data-slot="card"
ref={ref}
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border border-border/40 py-6 shadow-sm",
className
"rounded-lg border border-border bg-card text-card-foreground shadow-sm",
className,
)}
{...props}
/>
)
}
);
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
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 CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex flex-col space-y-1.5 p-4", className)} {...props} />
)
);
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}
/>
)
}
);
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
{...props}
/>
const CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
)
}
);
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-4 pt-0", className)} {...props} />
)
}
);
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex items-center p-4 pt-0", className)} {...props} />
)
}
);
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
)
}
Card.displayName = "Card";
CardHeader.displayName = "CardHeader";
CardTitle.displayName = "CardTitle";
CardDescription.displayName = "CardDescription";
CardContent.displayName = "CardContent";
CardFooter.displayName = "CardFooter";
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
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,
}
export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle };

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"
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
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 interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
icon?: React.ReactNode;
inputClassName?: string;
}
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-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.5",
"@tailwindcss/forms": "^0.5.10",
"@tailwindcss/typography": "^0.5.16",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.525.0",
@ -1868,6 +1870,46 @@
"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": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz",
@ -5211,11 +5253,22 @@
"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": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"dev": true,
"license": "MIT"
},
"node_modules/loose-envify": {
@ -5278,6 +5331,15 @@
"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": {
"version": "3.1.2",
"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-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.5",
"@tailwindcss/forms": "^0.5.10",
"@tailwindcss/typography": "^0.5.16",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.525.0",

View file

@ -4,99 +4,317 @@
@layer base {
:root {
--font-sans: "Inter", sans-serif;
--font-mono: "JetBrains Mono", monospace;
--font-chivo: "Chivo", sans-serif;
/* Core Theme Colors */
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--foreground: 0 0% 0%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--card-foreground: 0 0% 0%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 217.2 91.2% 59.8%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 217.2 91.2% 59.8%;
--radius: 0.65rem;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--popover-foreground: 0 0% 0%;
--primary: 0 0% 0%;
--primary-foreground: 0 0% 100%;
--primary-hover: 240 4% 16%;
--secondary: 0 0% 100%;
--secondary-foreground: 240 4% 16%;
--secondary-hover: 240 6% 90%;
--muted: 240 5% 96%;
--muted-foreground: 240 4% 46%;
--accent: 240 5% 96%;
--accent-foreground: 0 0% 0%;
--destructive: 0 72% 51%;
--destructive-foreground: 0 0% 100%;
--border: 240 6% 90%;
--input: 240 6% 90%;
--ring: 0 0% 0%;
--placeholder-foreground: 240 5% 65%;
/* Status Colors */
--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 {
/* Main backgrounds - very dark charcoal */
--background: 0 0% 10%;
--foreground: 210 40% 98%;
/* Card backgrounds - slightly lighter dark gray */
--card: 0 0% 16%;
--card-foreground: 210 40% 98%;
/* Popover backgrounds */
--popover: 0 0% 16%;
--popover-foreground: 210 40% 98%;
/* Primary accent - bright teal/cyan */
--primary: 162 100% 42%;
--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%;
}
}
--background: 240 6% 10%;
--foreground: 0 0% 100%;
--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%;
/* Dark mode data type colors */
--datatype-blue: 211.7 96.4% 78.4%;
--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%;
--warning: 45.9 96.7% 64.5%;
--warning-foreground: 240 6% 10%;
}
* {
@apply border-border;
}
@layer base {
body {
@apply bg-background text-foreground;
font-feature-settings: "rlig" 1, "calt" 1;
font-family: Inter, system-ui, sans-serif;
}
}
@layer utilities {
/* Hide scrollbar for Chrome, Safari and Opera */
.scrollbar-hide::-webkit-scrollbar {
display: none;
@layer components {
.header-arrangement {
@apply flex w-full h-[53px] items-center justify-between border-b border-border;
}
/* Hide scrollbar for IE, Edge and Firefox */
.scrollbar-hide {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
.header-start-display {
@apply flex items-center gap-2;
}
.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 { useTask } from "@/contexts/task-context"
import { useAuth } from "@/contexts/auth-context"
import { FileUploadArea } from "@/components/file-upload-area"
type FacetBucket = { key: string; count: number }
@ -48,7 +49,6 @@ function KnowledgeSourcesPage() {
// File upload state
const [fileUploadLoading, setFileUploadLoading] = useState(false)
const [pathUploadLoading, setPathUploadLoading] = useState(false)
const [selectedFile, setSelectedFile] = useState<File | null>(null)
const [folderPath, setFolderPath] = useState("/app/documents/")
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)
// File upload handlers
const handleFileUpload = async (e: React.FormEvent) => {
e.preventDefault()
if (!selectedFile) return
const handleDirectFileUpload = async (file: File) => {
setFileUploadLoading(true)
setUploadStatus("")
try {
const formData = new FormData()
formData.append("file", selectedFile)
formData.append("file", file)
const response = await fetch("/api/upload", {
method: "POST",
@ -86,9 +83,6 @@ function KnowledgeSourcesPage() {
if (response.ok) {
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
fetchStats()
@ -331,7 +325,7 @@ function KnowledgeSourcesPage() {
case "error":
return <Badge variant="destructive">Error</Badge>
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">
{/* File Upload Card */}
<Card>
<Card className="flex flex-col">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Upload className="h-5 w-5" />
@ -517,40 +511,16 @@ function KnowledgeSourcesPage() {
Import a single document to be processed and indexed
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleFileUpload} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="file-input">File</Label>
<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 className="flex-1 flex flex-col justify-end">
<FileUploadArea
onFileSelected={handleDirectFileUpload}
isLoading={fileUploadLoading}
/>
</CardContent>
</Card>
{/* Folder Upload Card */}
<Card>
<Card className="flex flex-col">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<FolderOpen className="h-5 w-5" />
@ -560,7 +530,7 @@ function KnowledgeSourcesPage() {
Process all documents in a folder path
</CardDescription>
</CardHeader>
<CardContent>
<CardContent className="flex-1 flex flex-col justify-end">
<form onSubmit={handlePathUpload} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="folder-path">Folder Path</Label>
@ -626,22 +596,24 @@ function KnowledgeSourcesPage() {
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex items-center space-x-4">
<Label htmlFor="maxFiles" className="text-sm font-medium">
Max files per sync:
</Label>
<Input
id="maxFiles"
type="number"
value={maxFiles}
onChange={(e) => setMaxFiles(parseInt(e.target.value) || 10)}
className="w-24"
min="1"
max="100"
/>
<span className="text-sm text-muted-foreground">
(Leave blank or set to 0 for unlimited)
</span>
<div className="flex items-center text-sm">
<div className="flex items-center gap-3">
<Label htmlFor="maxFiles" className="font-medium whitespace-nowrap">
Max files per sync:
</Label>
<Input
id="maxFiles"
type="number"
value={maxFiles}
onChange={(e) => setMaxFiles(parseInt(e.target.value) || 10)}
className="w-16 min-w-16 max-w-16 flex-shrink-0"
min="1"
max="100"
/>
<span className="text-muted-foreground whitespace-nowrap">
(Leave blank or set to 0 for unlimited)
</span>
</div>
</div>
</div>
</CardContent>
@ -650,7 +622,7 @@ function KnowledgeSourcesPage() {
{/* Connectors Grid */}
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{connectors.map((connector) => (
<Card key={connector.id} className="relative">
<Card key={connector.id} className="relative flex flex-col">
<CardHeader>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
@ -665,7 +637,7 @@ function KnowledgeSourcesPage() {
{getStatusBadge(connector.status)}
</div>
</CardHeader>
<CardContent className="space-y-4">
<CardContent className="flex-1 flex flex-col justify-end space-y-4">
{connector.status === "connected" ? (
<div className="space-y-3">
<Button

View file

@ -1,5 +1,5 @@
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 { ThemeProvider } from "@/components/theme-provider";
import { AuthProvider } from "@/contexts/auth-context";
@ -8,13 +8,18 @@ import { KnowledgeFilterProvider } from "@/contexts/knowledge-filter-context";
import { LayoutWrapper } from "@/components/layout-wrapper";
import { Toaster } from "@/components/ui/sonner";
const geistSans = Geist({
variable: "--font-geist-sans",
const inter = Inter({
variable: "--font-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
const jetbrainsMono = JetBrains_Mono({
variable: "--font-mono",
subsets: ["latin"],
});
const chivo = Chivo({
variable: "--font-chivo",
subsets: ["latin"],
});
@ -30,8 +35,14 @@ export default function RootLayout({
}>) {
return (
<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
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
className={`${inter.variable} ${jetbrainsMono.variable} ${chivo.variable} antialiased`}
>
<ThemeProvider
attribute="class"

View file

@ -5,11 +5,12 @@ import { Bell, BellRing } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Navigation } from "@/components/navigation"
import { ModeToggle } from "@/components/mode-toggle"
import { UserNav } from "@/components/user-nav"
import { TaskNotificationMenu } from "@/components/task-notification-menu"
import { KnowledgeFilterDropdown } from "@/components/knowledge-filter-dropdown"
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 { 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 (
<div className="h-full relative">
<header className="sticky top-0 z-50 w-full border-b border-border/40 bg-background">
<div className="flex h-14 items-center px-4">
<div className="flex items-center">
<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">
<header className="header-arrangement bg-background sticky top-0 z-50">
<div className="header-start-display px-4">
{/* Logo/Title */}
<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="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>
</svg>
<h1 className="text-lg font-semibold tracking-tight text-white">
OpenRAG
</h1>
<span className="text-lg font-semibold">OpenRAG</span>
</div>
<div className="flex flex-1 items-center justify-end space-x-2">
<nav className="flex items-center space-x-2">
{/* Knowledge Filter Dropdown */}
<KnowledgeFilterDropdown
selectedFilter={selectedFilter}
onFilterSelect={setSelectedFilter}
/>
{/* Task Notification Bell */}
<Button
variant="ghost"
size="sm"
onClick={toggleMenu}
className="relative p-2"
>
{activeTasks.length > 0 ? (
<BellRing className="h-4 w-4 text-blue-500" />
) : (
<Bell className="h-4 w-4 text-muted-foreground" />
)}
{activeTasks.length > 0 && (
<Badge
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}
</Badge>
)}
</Button>
<UserNav />
<ModeToggle />
</nav>
</div>
<div className="header-end-division">
<div className="header-end-display">
{/* Knowledge Filter Dropdown */}
<KnowledgeFilterDropdown
selectedFilter={selectedFilter}
onFilterSelect={setSelectedFilter}
/>
{/* GitHub Star Button */}
<GitHubStarButton repo="phact/openrag" />
{/* Discord Link */}
<DiscordLink inviteCode="EqksyE2EX9" />
{/* Task Notification Bell */}
<Button
variant="ghost"
size="iconSm"
onClick={toggleMenu}
className="relative"
>
{activeTasks.length > 0 ? (
<BellRing className="h-4 w-4 text-blue-500" />
) : (
<Bell className="h-4 w-4 text-muted-foreground" />
)}
{activeTasks.length > 0 && (
<div className="header-notifications" />
)}
</Button>
{/* Separator */}
<div className="w-px h-6 bg-border" />
<UserNav />
</div>
</div>
</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 />
</div>
<main className={`md:pl-72 ${(isMenuOpen || isPanelOpen) ? 'md:pr-80' : ''}`}>
<div className="flex flex-col h-[calc(100vh-3.6rem)]">
<main className={`md:pl-72 md:pr-6 ${(isMenuOpen || isPanelOpen) ? 'md:pr-80' : ''}`}>
<div className="flex flex-col min-h-screen">
<div className="flex-1 overflow-y-auto scrollbar-hide">
<div className="container py-6 lg:py-8">
{children}

View file

@ -11,10 +11,12 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
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() {
const { user, isLoading, isAuthenticated, login, logout } = useAuth()
const { theme, setTheme } = useTheme()
if (isLoading) {
return (
@ -39,13 +41,14 @@ export function UserNav() {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="relative h-8 w-8 rounded-full">
<Avatar className="h-8 w-8">
<Button variant="ghost" className="flex items-center gap-1 h-8 px-1 rounded-full">
<Avatar className="h-6 w-6">
<AvatarImage src={user?.picture} alt={user?.name} />
<AvatarFallback>
{user?.name ? user.name.charAt(0).toUpperCase() : <User className="h-4 w-4" />}
<AvatarFallback className="text-xs">
{user?.name ? user.name.charAt(0).toUpperCase() : <User className="h-3 w-3" />}
</AvatarFallback>
</Avatar>
<ChevronsUpDown className="h-3 w-3 text-muted-foreground" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56" align="end" forceMount>
@ -58,6 +61,23 @@ export function UserNav() {
</div>
</DropdownMenuLabel>
<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">
<LogOut className="mr-2 h-4 w-4" />
<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"],
content: [
"./pages/**/*.{ts,tsx}",
@ -8,16 +13,47 @@ const config: Config = {
"./app/**/*.{ts,tsx}",
"./src/**/*.{ts,tsx}",
],
prefix: "",
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
"3xl": "1500px",
},
},
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: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
@ -27,10 +63,12 @@ const config: Config = {
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
hover: "hsl(var(--primary-hover))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
hover: "hsl(var(--secondary-hover))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
@ -52,37 +90,36 @@ const config: Config = {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
// Custom colors from the screenshot
teal: {
50: "#f0fdfa",
100: "#ccfbf1",
200: "#99f6e4",
300: "#5eead4",
400: "#2dd4bf",
500: "#14b8a6",
600: "#0d9488",
700: "#0f766e",
800: "#115e59",
900: "#134e4a",
DEFAULT: "#00D4AA", // Primary teal from screenshot
"status-blue": "var(--status-blue)",
"status-green": "var(--status-green)",
"status-red": "var(--status-red)",
"status-yellow": "var(--status-yellow)",
"component-icon": "var(--component-icon)",
"flow-icon": "var(--flow-icon)",
"placeholder-foreground": "hsl(var(--placeholder-foreground))",
"datatype-blue": {
DEFAULT: "hsl(var(--datatype-blue))",
foreground: "hsl(var(--datatype-blue-foreground))",
},
success: {
DEFAULT: "#10B981", // Success green from screenshot
foreground: "#ffffff",
"datatype-yellow": {
DEFAULT: "hsl(var(--datatype-yellow))",
foreground: "hsl(var(--datatype-yellow-foreground))",
},
// Additional grays matching the screenshot
gray: {
850: "#1a1a1a", // Very dark background
800: "#2a2a2a", // Card background
750: "#333333", // Slightly lighter card
700: "#374151", // Input background
600: "#4b5563", // Border color
500: "#6b7280", // Muted text
400: "#9ca3af", // Secondary text
300: "#d1d5db",
200: "#e5e7eb",
100: "#f3f4f6",
50: "#f8f9fa", // Primary text
"datatype-red": {
DEFAULT: "hsl(var(--datatype-red))",
foreground: "hsl(var(--datatype-red-foreground))",
},
"datatype-emerald": {
DEFAULT: "hsl(var(--datatype-emerald))",
foreground: "hsl(var(--datatype-emerald-foreground))",
},
"datatype-violet": {
DEFAULT: "hsl(var(--datatype-violet))",
foreground: "hsl(var(--datatype-violet-foreground))",
},
warning: {
DEFAULT: "hsl(var(--warning))",
foreground: "hsl(var(--warning-foreground))",
},
},
borderRadius: {
@ -90,23 +127,92 @@ const config: Config = {
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
fontFamily: {
sans: ["var(--font-sans)", ...fontFamily.sans],
mono: ["var(--font-mono)", ...fontFamily.mono],
chivo: ["var(--font-chivo)", ...fontFamily.sans],
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
fontSize: {
xxs: "11px",
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;