Merge pull request #653 from langflow-ai/knip-cleanup

knip cleanup for ui
This commit is contained in:
Sebastián Estévez 2025-12-12 10:16:56 -05:00 committed by GitHub
commit 6cfb5e2c7a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 482 additions and 5323 deletions

View file

@ -1,47 +0,0 @@
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
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

@ -1,103 +0,0 @@
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { 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

@ -1,47 +0,0 @@
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
import { Github } from "lucide-react";
import { useGitHubStars } from "@/hooks/use-github-stars";
import { formatCount } 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

@ -1,458 +0,0 @@
"use client";
import { useState, useEffect, useRef } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Card, CardContent } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import {
ChevronDown,
Filter,
Search,
X,
Loader2,
Plus,
Save,
} from "lucide-react";
import { cn } from "@/lib/utils";
interface KnowledgeFilter {
id: string;
name: string;
description: string;
query_data: string;
owner: string;
created_at: string;
updated_at: string;
}
interface ParsedQueryData {
query: string;
filters: {
data_sources: string[];
document_types: string[];
owners: string[];
};
limit: number;
scoreThreshold: number;
}
interface KnowledgeFilterDropdownProps {
selectedFilter: KnowledgeFilter | null;
onFilterSelect: (filter: KnowledgeFilter | null) => void;
}
export function KnowledgeFilterDropdown({
selectedFilter,
onFilterSelect,
}: KnowledgeFilterDropdownProps) {
const [isOpen, setIsOpen] = useState(false);
const [filters, setFilters] = useState<KnowledgeFilter[]>([]);
const [loading, setLoading] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const [showCreateModal, setShowCreateModal] = useState(false);
const [createName, setCreateName] = useState("");
const [createDescription, setCreateDescription] = useState("");
const [creating, setCreating] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const loadFilters = async (query = "") => {
setLoading(true);
try {
const response = await fetch("/api/knowledge-filter/search", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
query,
limit: 20, // Limit for dropdown
}),
});
const result = await response.json();
if (response.ok && result.success) {
setFilters(result.filters);
} else {
console.error("Failed to load knowledge filters:", result.error);
setFilters([]);
}
} catch (error) {
console.error("Error loading knowledge filters:", error);
setFilters([]);
} finally {
setLoading(false);
}
};
const deleteFilter = async (filterId: string, e: React.MouseEvent) => {
e.stopPropagation();
try {
const response = await fetch(`/api/knowledge-filter/${filterId}`, {
method: "DELETE",
});
if (response.ok) {
// Remove from local state
setFilters((prev) => prev.filter((f) => f.id !== filterId));
// If this was the selected filter, clear selection
if (selectedFilter?.id === filterId) {
onFilterSelect(null);
}
} else {
console.error("Failed to delete knowledge filter");
}
} catch (error) {
console.error("Error deleting knowledge filter:", error);
}
};
const handleFilterSelect = (filter: KnowledgeFilter) => {
onFilterSelect(filter);
setIsOpen(false);
};
const handleClearFilter = () => {
onFilterSelect(null);
setIsOpen(false);
};
const handleCreateNew = () => {
setIsOpen(false);
setShowCreateModal(true);
};
const handleCreateFilter = async () => {
if (!createName.trim()) return;
setCreating(true);
try {
// Create a basic filter with wildcards (match everything by default)
const defaultFilterData = {
query: "",
filters: {
data_sources: ["*"],
document_types: ["*"],
owners: ["*"],
},
limit: 10,
scoreThreshold: 0,
};
const response = await fetch("/api/knowledge-filter", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: createName.trim(),
description: createDescription.trim(),
queryData: JSON.stringify(defaultFilterData),
}),
});
const result = await response.json();
if (response.ok && result.success) {
// Create the new filter object
const newFilter: KnowledgeFilter = {
id: result.filter.id,
name: createName.trim(),
description: createDescription.trim(),
query_data: JSON.stringify(defaultFilterData),
owner: result.filter.owner,
created_at: result.filter.created_at,
updated_at: result.filter.updated_at,
};
// Add to local filters list
setFilters((prev) => [newFilter, ...prev]);
// Select the new filter
onFilterSelect(newFilter);
// Close modal and reset form
setShowCreateModal(false);
setCreateName("");
setCreateDescription("");
} else {
console.error("Failed to create knowledge filter:", result.error);
}
} catch (error) {
console.error("Error creating knowledge filter:", error);
} finally {
setCreating(false);
}
};
const handleCancelCreate = () => {
setShowCreateModal(false);
setCreateName("");
setCreateDescription("");
};
const getFilterSummary = (filter: KnowledgeFilter): string => {
try {
const parsed = JSON.parse(filter.query_data) as ParsedQueryData;
const parts = [];
if (parsed.query) parts.push(`"${parsed.query}"`);
if (parsed.filters.data_sources.length > 0)
parts.push(`${parsed.filters.data_sources.length} sources`);
if (parsed.filters.document_types.length > 0)
parts.push(`${parsed.filters.document_types.length} types`);
if (parsed.filters.owners.length > 0)
parts.push(`${parsed.filters.owners.length} owners`);
return parts.join(" • ") || "No filters";
} catch {
return "Invalid filter";
}
};
useEffect(() => {
if (isOpen) {
loadFilters();
}
}, [isOpen]);
useEffect(() => {
const timeoutId = setTimeout(() => {
if (isOpen) {
loadFilters(searchQuery);
}
}, 300);
return () => clearTimeout(timeoutId);
}, [searchQuery, isOpen]);
// Close dropdown when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node)
) {
setIsOpen(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
return (
<div className="relative" ref={dropdownRef}>
<Button
variant={selectedFilter ? "default" : "outline"}
size="sm"
onClick={() => setIsOpen(!isOpen)}
className={cn(
"flex items-center gap-2 h-8 px-3",
selectedFilter
? "hover:bg-primary hover:text-primary-foreground"
: "hover:bg-transparent hover:text-foreground hover:border-border",
)}
>
<Filter className="h-3 w-3" />
{selectedFilter ? (
<span className="max-w-32 truncate">{selectedFilter.name}</span>
) : (
<span>All Knowledge</span>
)}
<ChevronDown
className={cn("h-3 w-3 transition-transform", isOpen && "rotate-180")}
/>
</Button>
{isOpen && (
<Card className="absolute right-0 top-full mt-1 w-80 max-h-96 overflow-hidden z-50 shadow-lg border-border/50 bg-card/95 backdrop-blur-sm">
<CardContent className="p-0">
{/* Search Header */}
<div className="p-3 border-b border-border/50">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-3 w-3 text-muted-foreground" />
<Input
placeholder="Search filters..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9 h-8 text-sm"
/>
</div>
</div>
{/* Filter List */}
<div className="max-h-64 overflow-y-auto">
{/* Clear filter option */}
<div
onClick={handleClearFilter}
className={cn(
"flex items-center gap-3 p-3 hover:bg-accent hover:text-accent-foreground cursor-pointer border-b border-border/30 transition-colors",
!selectedFilter && "bg-accent text-accent-foreground",
)}
>
<div className="flex items-center gap-2 flex-1">
<Filter className="h-4 w-4 text-muted-foreground" />
<div>
<div className="text-sm font-medium">All Knowledge</div>
<div className="text-xs text-muted-foreground">
No filters applied
</div>
</div>
</div>
</div>
{loading ? (
<div className="flex items-center justify-center p-4">
<Loader2 className="h-4 w-4 animate-spin" />
<span className="ml-2 text-sm text-muted-foreground">
Loading...
</span>
</div>
) : filters.length === 0 ? (
<div className="p-4 text-center text-sm text-muted-foreground">
{searchQuery ? "No filters found" : "No saved filters"}
</div>
) : (
filters.map((filter) => (
<div
key={filter.id}
onClick={() => handleFilterSelect(filter)}
className={cn(
"flex items-center gap-3 p-3 hover:bg-accent hover:text-accent-foreground cursor-pointer group transition-colors",
selectedFilter?.id === filter.id &&
"bg-accent text-accent-foreground",
)}
>
<div className="flex items-center gap-2 flex-1 min-w-0">
<Filter className="h-4 w-4 text-muted-foreground group-hover:text-accent-foreground flex-shrink-0" />
<div className="min-w-0 flex-1">
<div className="text-sm font-medium truncate group-hover:text-accent-foreground">
{filter.name}
</div>
<div className="text-xs text-muted-foreground group-hover:text-accent-foreground/70 truncate">
{getFilterSummary(filter)}
</div>
</div>
</div>
<Button
variant="ghost"
size="sm"
onClick={(e) => deleteFilter(filter.id, e)}
className="opacity-0 group-hover:opacity-100 h-6 w-6 p-0 bg-transparent hover:bg-gray-700 hover:text-white transition-all duration-200 border border-transparent hover:border-gray-600"
>
<X className="h-3 w-3 text-gray-400 hover:text-white" />
</Button>
</div>
))
)}
</div>
{/* Create New Filter Option */}
<div className="border-t border-border/50">
<div
onClick={handleCreateNew}
className="flex items-center gap-3 p-3 hover:bg-accent hover:text-accent-foreground cursor-pointer transition-colors"
>
<Plus className="h-4 w-4 text-green-500" />
<div>
<div className="text-sm font-medium text-green-600">
Create New Filter
</div>
<div className="text-xs text-muted-foreground">
Save current search as filter
</div>
</div>
</div>
</div>
{/* Selected Filter Details */}
{selectedFilter && (
<div className="border-t border-border/50 p-3 bg-muted/20">
<div className="text-xs text-muted-foreground">
<strong>Selected:</strong> {selectedFilter.name}
</div>
{selectedFilter.description && (
<div className="text-xs text-muted-foreground mt-1 line-clamp-2">
{selectedFilter.description}
</div>
)}
</div>
)}
</CardContent>
</Card>
)}
{/* Create Filter Modal */}
{showCreateModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-card border border-border rounded-lg p-6 w-full max-w-md mx-4">
<h3 className="text-lg font-semibold mb-4">
Create New Knowledge Filter
</h3>
<div className="space-y-4">
<div>
<Label htmlFor="filter-name" className="font-medium">
Name <span className="text-red-400">*</span>
</Label>
<Input
id="filter-name"
type="text"
placeholder="Enter filter name"
value={createName}
onChange={(e) => setCreateName(e.target.value)}
className="mt-1"
/>
</div>
<div>
<Label htmlFor="filter-description" className="font-medium">
Description (optional)
</Label>
<Textarea
id="filter-description"
placeholder="Brief description of this filter"
value={createDescription}
onChange={(e) => setCreateDescription(e.target.value)}
className="mt-1"
rows={3}
/>
</div>
</div>
<div className="flex justify-end gap-2 mt-6">
<Button
variant="outline"
onClick={handleCancelCreate}
disabled={creating}
>
Cancel
</Button>
<Button
onClick={handleCreateFilter}
disabled={!createName.trim() || creating}
className="flex items-center gap-2"
>
{creating ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Creating...
</>
) : (
<>
<Save className="h-4 w-4" />
Create Filter
</>
)}
</Button>
</div>
</div>
</div>
)}
</div>
);
}

View file

@ -1,48 +0,0 @@
"use client";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { useAuth } from "@/contexts/auth-context";
import { Lock, LogIn } from "lucide-react";
interface LoginRequiredProps {
title?: string;
description?: string;
feature?: string;
}
export function LoginRequired({
title = "Authentication Required",
description = "You need to sign in to access this feature",
feature,
}: LoginRequiredProps) {
const { login } = useAuth();
return (
<div className="flex items-center justify-center min-h-[400px]">
<Card className="max-w-md mx-auto">
<CardHeader className="text-center">
<div className="flex items-center justify-center w-12 h-12 bg-primary/10 rounded-full mx-auto mb-4">
<Lock className="h-6 w-6 text-primary" />
</div>
<CardTitle>{title}</CardTitle>
<CardDescription>
{feature ? `You need to sign in to access ${feature}` : description}
</CardDescription>
</CardHeader>
<CardContent className="text-center">
<Button onClick={login} className="w-full">
<LogIn className="h-4 w-4 mr-2" />
Sign In with Google
</Button>
</CardContent>
</Card>
</div>
);
}

View file

@ -1,23 +0,0 @@
"use client";
import * as React from "react";
import { Moon, Sun } from "lucide-react";
import { useTheme } from "next-themes";
import { Button } from "@/components/ui/button";
export function ModeToggle() {
const { theme, setTheme } = useTheme();
return (
<Button
variant="ghost"
size="icon"
onClick={() => setTheme(theme === "light" ? "dark" : "light")}
>
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
);
}

View file

@ -1,81 +0,0 @@
"use client";
import { usePathname } from "next/navigation";
import {
useGetConversationsQuery,
type ChatConversation,
} from "@/app/api/queries/useGetConversationsQuery";
import { KnowledgeFilterDropdown } from "@/components/knowledge-filter-dropdown";
import { ModeToggle } from "@/components/mode-toggle";
import { Navigation } from "@/components/navigation";
import { useAuth } from "@/contexts/auth-context";
import { useChat } from "@/contexts/chat-context";
import { useKnowledgeFilter } from "@/contexts/knowledge-filter-context";
interface NavigationLayoutProps {
children: React.ReactNode;
}
export function NavigationLayout({ children }: NavigationLayoutProps) {
const { selectedFilter, setSelectedFilter } = useKnowledgeFilter();
const pathname = usePathname();
const { isAuthenticated, isNoAuthMode } = useAuth();
const {
endpoint,
refreshTrigger,
refreshConversations,
startNewConversation,
} = useChat();
// Only fetch conversations on chat page
const isOnChatPage = pathname === "/" || pathname === "/chat";
const { data: conversations = [], isLoading: isConversationsLoading } =
useGetConversationsQuery(endpoint, refreshTrigger, {
enabled: isOnChatPage && (isAuthenticated || isNoAuthMode),
}) as { data: ChatConversation[]; isLoading: boolean };
const handleNewConversation = () => {
refreshConversations();
startNewConversation();
};
return (
<div className="h-full relative">
<div className="hidden h-full md:flex md:w-72 md:flex-col md:fixed md:inset-y-0 z-[80] border-r border-border/40">
<Navigation
conversations={conversations}
isConversationsLoading={isConversationsLoading}
onNewConversation={handleNewConversation}
/>
</div>
<main className="md:pl-72">
<div className="flex flex-col min-h-screen">
<header className="sticky top-0 z-40 w-full border-b border-border/40 bg-background">
<div className="container flex h-14 max-w-screen-2xl items-center">
<div className="mr-4 hidden md:flex">
<h1 className="text-lg font-semibold tracking-tight">
OpenRAG
</h1>
</div>
<div className="flex flex-1 items-center justify-between space-x-2 md:justify-end">
<div className="w-full flex-1 md:w-auto md:flex-none">
{/* Search component could go here */}
</div>
<nav className="flex items-center space-x-2">
<KnowledgeFilterDropdown
selectedFilter={selectedFilter}
onFilterSelect={setSelectedFilter}
/>
<ModeToggle />
</nav>
</div>
</div>
</header>
<div className="flex-1">
<div className="container py-6 lg:py-8">{children}</div>
</div>
</div>
</main>
</div>
);
}

View file

@ -1,30 +0,0 @@
"use client";
import * as React from "react";
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import { Check } from "lucide-react";
import { cn } from "@/lib/utils";
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className,
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
));
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
export { Checkbox };

View file

@ -1,158 +0,0 @@
"use client";
import { motion } from "motion/react";
import type React from "react";
import { useEffect, useId, useRef, useState } from "react";
import { cn } from "@/lib/utils";
/**
* DotPattern Component Props
*
* @param {number} [width=16] - The horizontal spacing between dots
* @param {number} [height=16] - The vertical spacing between dots
* @param {number} [x=0] - The x-offset of the entire pattern
* @param {number} [y=0] - The y-offset of the entire pattern
* @param {number} [cx=1] - The x-offset of individual dots
* @param {number} [cy=1] - The y-offset of individual dots
* @param {number} [cr=1] - The radius of each dot
* @param {string} [className] - Additional CSS classes to apply to the SVG container
* @param {boolean} [glow=false] - Whether dots should have a glowing animation effect
*/
interface DotPatternProps extends React.SVGProps<SVGSVGElement> {
width?: number;
height?: number;
x?: number;
y?: number;
cx?: number;
cy?: number;
cr?: number;
className?: string;
glow?: boolean;
[key: string]: unknown;
}
/**
* DotPattern Component
*
* A React component that creates an animated or static dot pattern background using SVG.
* The pattern automatically adjusts to fill its container and can optionally display glowing dots.
*
* @component
*
* @see DotPatternProps for the props interface.
*
* @example
* // Basic usage
* <DotPattern />
*
* // With glowing effect and custom spacing
* <DotPattern
* width={20}
* height={20}
* glow={true}
* className="opacity-50"
* />
*
* @notes
* - The component is client-side only ("use client")
* - Automatically responds to container size changes
* - When glow is enabled, dots will animate with random delays and durations
* - Uses Motion for animations
* - Dots color can be controlled via the text color utility classes
*/
export function DotPattern({
width = 16,
height = 16,
x = 0,
y = 0,
cx = 1,
cy = 1,
cr = 1,
className,
glow = false,
...props
}: DotPatternProps) {
const id = useId();
const containerRef = useRef<SVGSVGElement>(null);
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
useEffect(() => {
const updateDimensions = () => {
if (containerRef.current) {
const { width, height } = containerRef.current.getBoundingClientRect();
setDimensions({ width, height });
}
};
updateDimensions();
window.addEventListener("resize", updateDimensions);
return () => window.removeEventListener("resize", updateDimensions);
}, []);
const dots = Array.from(
{
length:
Math.ceil(dimensions.width / width) *
Math.ceil(dimensions.height / height),
},
(_, i) => {
const col = i % Math.ceil(dimensions.width / width);
const row = Math.floor(i / Math.ceil(dimensions.width / width));
return {
x: col * width + cx,
y: row * height + cy,
delay: Math.random() * 5,
duration: Math.random() * 3 + 2,
};
},
);
return (
<svg
ref={containerRef}
aria-hidden="true"
className={cn(
"pointer-events-none absolute inset-0 h-full w-full text-neutral-400/80",
className,
)}
{...props}
>
<defs>
<radialGradient id={`${id}-gradient`}>
<stop offset="0%" stopColor="currentColor" stopOpacity="1" />
<stop offset="100%" stopColor="currentColor" stopOpacity="0" />
</radialGradient>
</defs>
{dots.map((dot, index) => (
<motion.circle
key={`${dot.x}-${dot.y}`}
cx={dot.x}
cy={dot.y}
r={cr}
fill={glow ? `url(#${id}-gradient)` : "currentColor"}
initial={glow ? { opacity: 0.4, scale: 1 } : {}}
animate={
glow
? {
opacity: [0.4, 1, 0.4],
scale: [1, 1.5, 1],
}
: {}
}
transition={
glow
? {
duration: dot.duration,
repeat: Infinity,
repeatType: "reverse",
delay: dot.delay,
ease: "easeInOut",
}
: {}
}
/>
))}
</svg>
);
}

View file

@ -1,67 +0,0 @@
import {
Select,
SelectContent,
SelectTrigger,
SelectValue,
} from "@radix-ui/react-select";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@radix-ui/react-tooltip";
import type { ModelOption } from "@/app/api/queries/useGetModelsQuery";
import {
getFallbackModels,
type ModelProvider,
} from "@/app/settings/_helpers/model-helpers";
import { ModelSelectItems } from "@/app/settings/_helpers/model-select-item";
import { LabelWrapper } from "@/components/label-wrapper";
interface EmbeddingModelInputProps {
disabled?: boolean;
value: string;
onChange: (value: string) => void;
modelsData?: {
embedding_models: ModelOption[];
};
currentProvider?: ModelProvider;
}
export const EmbeddingModelInput = ({
disabled,
value,
onChange,
modelsData,
currentProvider = "openai",
}: EmbeddingModelInputProps) => {
const isDisabled = Boolean(disabled);
const tooltipMessage = isDisabled
? "Locked to keep embeddings consistent"
: "Choose the embedding model for ingest and retrieval";
return (
<LabelWrapper
helperText="Model used for knowledge ingest and retrieval"
id="embedding-model-select"
label="Embedding model"
>
<Select disabled={isDisabled} value={value} onValueChange={onChange}>
<Tooltip delayDuration={0}>
<TooltipTrigger asChild>
<SelectTrigger disabled={isDisabled} id="embedding-model-select">
<SelectValue placeholder="Select an embedding model" />
</SelectTrigger>
</TooltipTrigger>
<TooltipContent>{tooltipMessage}</TooltipContent>
</Tooltip>
<SelectContent>
<ModelSelectItems
models={modelsData?.embedding_models || []}
fallbackModels={getFallbackModels(currentProvider).embedding || []}
provider={currentProvider}
/>
</SelectContent>
</Select>
</LabelWrapper>
);
};

View file

@ -1,128 +0,0 @@
import * as React from "react";
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
import { cva } from "class-variance-authority";
import { ChevronDown } from "lucide-react";
import { cn } from "@/lib/utils";
const NavigationMenu = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Root
ref={ref}
className={cn(
"relative z-10 flex max-w-max flex-1 items-center justify-center",
className,
)}
{...props}
>
{children}
<NavigationMenuViewport />
</NavigationMenuPrimitive.Root>
));
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName;
const NavigationMenuList = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.List>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.List
ref={ref}
className={cn(
"group flex flex-1 list-none items-center justify-center space-x-1",
className,
)}
{...props}
/>
));
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName;
const NavigationMenuItem = NavigationMenuPrimitive.Item;
const navigationMenuTriggerStyle = cva(
"group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[state=open]:text-accent-foreground data-[state=open]:bg-accent/50 data-[state=open]:hover:bg-accent data-[state=open]:focus:bg-accent",
);
const NavigationMenuTrigger = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Trigger
ref={ref}
className={cn(navigationMenuTriggerStyle(), "group", className)}
{...props}
>
{children}{" "}
<ChevronDown
className="relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180"
aria-hidden="true"
/>
</NavigationMenuPrimitive.Trigger>
));
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName;
const NavigationMenuContent = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Content
ref={ref}
className={cn(
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ",
className,
)}
{...props}
/>
));
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName;
const NavigationMenuLink = NavigationMenuPrimitive.Link;
const NavigationMenuViewport = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
>(({ className, ...props }, ref) => (
<div className={cn("absolute left-0 top-full flex justify-center")}>
<NavigationMenuPrimitive.Viewport
className={cn(
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
className,
)}
ref={ref}
{...props}
/>
</div>
));
NavigationMenuViewport.displayName =
NavigationMenuPrimitive.Viewport.displayName;
const NavigationMenuIndicator = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Indicator
ref={ref}
className={cn(
"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
className,
)}
{...props}
>
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
</NavigationMenuPrimitive.Indicator>
));
NavigationMenuIndicator.displayName =
NavigationMenuPrimitive.Indicator.displayName;
export {
navigationMenuTriggerStyle,
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuContent,
NavigationMenuTrigger,
NavigationMenuLink,
NavigationMenuIndicator,
NavigationMenuViewport,
};

View file

@ -1,44 +0,0 @@
"use client";
import * as React from "react";
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
import { Circle } from "lucide-react";
import { cn } from "@/lib/utils";
const RadioGroup = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Root
className={cn("grid gap-2", className)}
{...props}
ref={ref}
/>
);
});
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
const RadioGroupItem = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Item
ref={ref}
className={cn(
"aspect-square h-4 w-4 rounded-full border border-input text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
>
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
<Circle className="h-2.5 w-2.5 fill-current text-current" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
);
});
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
export { RadioGroup, RadioGroupItem };

View file

@ -1,31 +0,0 @@
"use client";
import * as SeparatorPrimitive from "@radix-ui/react-separator";
import * as React from "react";
import { cn } from "@/lib/utils";
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref,
) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className,
)}
{...props}
/>
),
);
Separator.displayName = SeparatorPrimitive.Root.displayName;
export { Separator };

View file

@ -1,56 +0,0 @@
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

@ -1,52 +0,0 @@
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 };
};

18
frontend/knip.config.ts Normal file
View file

@ -0,0 +1,18 @@
import type { KnipConfig } from 'knip';
const config: KnipConfig = {
entry: [
'app/**/*.{ts,tsx}',
'next.config.ts',
],
project: ['**/*.{ts,tsx}'],
ignore: [
'**/*.d.ts',
'**/node_modules/**',
'.next/**',
'public/**',
],
};
export default config;

View file

@ -1,13 +0,0 @@
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();
};

File diff suppressed because it is too large Load diff

View file

@ -8,23 +8,18 @@
"start": "next start",
"lint": "next lint",
"check-format": "npx @biomejs/biome check",
"format": "npx @biomejs/biome format --write"
"format": "npx @biomejs/biome format --write",
"knip": "knip"
},
"dependencies": {
"@microsoft/mgt-components": "^4.6.0",
"@microsoft/mgt-msal2-provider": "^4.6.0",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-checkbox": "^1.3.2",
"@radix-ui/react-collapsible": "^1.1.11",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.15",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-navigation-menu": "^1.2.13",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-radio-group": "^1.3.8",
"@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slider": "^1.3.6",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.5",
@ -47,7 +42,6 @@
"react-dom": "^19.0.0",
"react-dropzone": "^14.3.8",
"react-hook-form": "^7.65.0",
"react-icons": "^5.5.0",
"react-markdown": "^10.1.0",
"react-syntax-highlighter": "^15.6.1",
"react-textarea-autosize": "^8.5.9",
@ -68,8 +62,7 @@
"@types/react-dom": "^19",
"@types/react-syntax-highlighter": "^15.5.13",
"autoprefixer": "^10.4.21",
"eslint": "^9",
"eslint-config-next": "15.3.5",
"knip": "^5.73.1",
"postcss": "^8.5.6",
"tailwindcss": "^3.4.17",
"typescript": "^5"