From 961723856b10ec6ae29bc5a5b13969ff09e21ad1 Mon Sep 17 00:00:00 2001 From: phact Date: Mon, 18 Aug 2025 22:19:43 -0400 Subject: [PATCH] re-skin --- frontend/components/discord-link.tsx | 42 +++ frontend/components/file-upload-area.tsx | 103 ++++++ frontend/components/github-star-button.tsx | 40 +++ frontend/components/navigation.tsx | 2 +- frontend/components/ui/button.tsx | 115 +++--- frontend/components/ui/card.tsx | 112 ++---- frontend/components/ui/input.tsx | 61 +++- frontend/components/ui/scroll-area.tsx | 22 ++ frontend/hooks/use-discord-members.ts | 54 +++ frontend/hooks/use-github-stars.ts | 50 +++ frontend/lib/format-count.ts | 13 + frontend/package-lock.json | 64 +++- frontend/package.json | 2 + frontend/src/app/globals.css | 380 +++++++++++++++----- frontend/src/app/knowledge-sources/page.tsx | 92 ++--- frontend/src/app/layout.tsx | 23 +- frontend/src/components/layout-wrapper.tsx | 91 ++--- frontend/src/components/user-nav.tsx | 30 +- frontend/tailwind.config.ts | 198 +++++++--- 19 files changed, 1117 insertions(+), 377 deletions(-) create mode 100644 frontend/components/discord-link.tsx create mode 100644 frontend/components/file-upload-area.tsx create mode 100644 frontend/components/github-star-button.tsx create mode 100644 frontend/components/ui/scroll-area.tsx create mode 100644 frontend/hooks/use-discord-members.ts create mode 100644 frontend/hooks/use-github-stars.ts create mode 100644 frontend/lib/format-count.ts diff --git a/frontend/components/discord-link.tsx b/frontend/components/discord-link.tsx new file mode 100644 index 00000000..531e0161 --- /dev/null +++ b/frontend/components/discord-link.tsx @@ -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( + ({ inviteCode = "EqksyE2EX9", className }, ref) => { + const { data, isLoading, error } = useDiscordMembers(inviteCode); + + return ( + + + + + + {isLoading ? "..." : error ? "--" : data ? formatCount(data.approximate_member_count) : "--"} + + + ); + } +); + +DiscordLink.displayName = "DiscordLink"; + +export { DiscordLink }; \ No newline at end of file diff --git a/frontend/components/file-upload-area.tsx b/frontend/components/file-upload-area.tsx new file mode 100644 index 00000000..f0254941 --- /dev/null +++ b/frontend/components/file-upload-area.tsx @@ -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( + ({ onFileSelected, isLoading = false, className }, ref) => { + const [isDragging, setIsDragging] = React.useState(false) + const fileInputRef = React.useRef(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) => { + const files = Array.from(e.target.files || []) + if (files.length > 0 && onFileSelected) { + onFileSelected(files[0]) + } + } + + const handleClick = () => { + if (!isLoading) { + fileInputRef.current?.click() + } + } + + return ( +
+ + +
+ {isLoading && ( +
+ +
+ )} + +
+

+ {isLoading ? "Processing file..." : "Drop files here or click to upload"} +

+

+ {isLoading ? "Please wait while your file is being processed" : ""} +

+
+ + {!isLoading && ( + + )} +
+
+ ) + } +) + +FileUploadArea.displayName = "FileUploadArea" + +export { FileUploadArea } \ No newline at end of file diff --git a/frontend/components/github-star-button.tsx b/frontend/components/github-star-button.tsx new file mode 100644 index 00000000..33b5e2e9 --- /dev/null +++ b/frontend/components/github-star-button.tsx @@ -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( + ({ repo = "phact/openrag", className }, ref) => { + const { data, isLoading, error } = useGitHubStars(repo); + + return ( + + + + {isLoading ? "..." : error ? "--" : data ? formatCount(data.stargazers_count) : "--"} + + + ); + } +); + +GitHubStarButton.displayName = "GitHubStarButton"; + +export { GitHubStarButton }; \ No newline at end of file diff --git a/frontend/components/navigation.tsx b/frontend/components/navigation.tsx index 5cd483f3..4c4995e4 100644 --- a/frontend/components/navigation.tsx +++ b/frontend/components/navigation.tsx @@ -30,7 +30,7 @@ export function Navigation() { ] return ( -
+
{routes.map((route) => ( diff --git a/frontend/components/ui/button.tsx b/frontend/components/ui/button.tsx index 8671037d..deb22864 100644 --- a/frontend/components/ui/button.tsx +++ b/frontend/components/ui/button.tsx @@ -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 & { - asChild?: boolean - }) { - const Comp = asChild ? Slot : "button" - - return ( - - ) +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, + VariantProps { + asChild?: boolean; + loading?: boolean; + ignoreTitleCase?: boolean; +} + +const Button = React.forwardRef( + ({ 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 ( + + {loading ? ( + + + {newChildren} + + +
+ + + ) : ( + newChildren + )} + + ); + } +); + +Button.displayName = "Button"; + +export { Button, buttonVariants }; diff --git a/frontend/components/ui/card.tsx b/frontend/components/ui/card.tsx index cb6ea819..e70e64bb 100644 --- a/frontend/components/ui/card.tsx +++ b/frontend/components/ui/card.tsx @@ -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>( + ({ className, ...props }, ref) => (
) -} +); -function CardHeader({ className, ...props }: React.ComponentProps<"div">) { - return ( -
>( + ({ className, ...props }, ref) => ( +
+ ) +); + +const CardTitle = React.forwardRef>( + ({ className, ...props }, ref) => ( +

) -} +); -function CardTitle({ className, ...props }: React.ComponentProps<"div">) { - return ( -
+const CardDescription = React.forwardRef>( + ({ className, ...props }, ref) => ( +
) -} +); -function CardDescription({ className, ...props }: React.ComponentProps<"div">) { - return ( -
+const CardContent = React.forwardRef>( + ({ className, ...props }, ref) => ( +
) -} +); -function CardAction({ className, ...props }: React.ComponentProps<"div">) { - return ( -
+const CardFooter = React.forwardRef>( + ({ className, ...props }, ref) => ( +
) -} +); -function CardContent({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} +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 ( -
- ) -} - -export { - Card, - CardHeader, - CardFooter, - CardTitle, - CardAction, - CardDescription, - CardContent, -} +export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }; diff --git a/frontend/components/ui/input.tsx b/frontend/components/ui/input.tsx index 90e2ff5f..5c14e593 100644 --- a/frontend/components/ui/input.tsx +++ b/frontend/components/ui/input.tsx @@ -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 ( - - ) +export interface InputProps extends React.InputHTMLAttributes { + icon?: React.ReactNode; + inputClassName?: string; } -export { Input } +const Input = React.forwardRef( + ({ className, inputClassName, icon, type, placeholder, ...props }, ref) => { + return ( + + ); + }, +); + +Input.displayName = "Input"; + +export { Input }; diff --git a/frontend/components/ui/scroll-area.tsx b/frontend/components/ui/scroll-area.tsx new file mode 100644 index 00000000..0755b7c3 --- /dev/null +++ b/frontend/components/ui/scroll-area.tsx @@ -0,0 +1,22 @@ +import * as React from "react"; +import { cn } from "@/lib/utils"; + +export interface ScrollAreaProps extends React.HTMLAttributes {} + +const ScrollArea = React.forwardRef( + ({ className, children, ...props }, ref) => { + return ( +
+ {children} +
+ ); + } +); + +ScrollArea.displayName = "ScrollArea"; + +export { ScrollArea }; \ No newline at end of file diff --git a/frontend/hooks/use-discord-members.ts b/frontend/hooks/use-discord-members.ts new file mode 100644 index 00000000..d13fc512 --- /dev/null +++ b/frontend/hooks/use-discord-members.ts @@ -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(null); + const [isLoading, setIsLoading] = React.useState(true); + const [error, setError] = React.useState(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 }; +}; \ No newline at end of file diff --git a/frontend/hooks/use-github-stars.ts b/frontend/hooks/use-github-stars.ts new file mode 100644 index 00000000..80a92f9b --- /dev/null +++ b/frontend/hooks/use-github-stars.ts @@ -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(null); + const [isLoading, setIsLoading] = React.useState(true); + const [error, setError] = React.useState(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 }; +}; \ No newline at end of file diff --git a/frontend/lib/format-count.ts b/frontend/lib/format-count.ts new file mode 100644 index 00000000..e641a0b2 --- /dev/null +++ b/frontend/lib/format-count.ts @@ -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(); +}; \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 10046beb..1f30a27d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -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", diff --git a/frontend/package.json b/frontend/package.json index 0e1dc341..21ad6665 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 3b7a3c7a..4bf50edf 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -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; } } diff --git a/frontend/src/app/knowledge-sources/page.tsx b/frontend/src/app/knowledge-sources/page.tsx index efc68d45..89c79b5e 100644 --- a/frontend/src/app/knowledge-sources/page.tsx +++ b/frontend/src/app/knowledge-sources/page.tsx @@ -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(null) const [folderPath, setFolderPath] = useState("/app/documents/") const [uploadStatus, setUploadStatus] = useState("") @@ -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 Error default: - return Not Connected + return Not Connected } } @@ -507,7 +501,7 @@ function KnowledgeSourcesPage() {
{/* File Upload Card */} - + @@ -517,40 +511,16 @@ function KnowledgeSourcesPage() { Import a single document to be processed and indexed - -
-
- - setSelectedFile(e.target.files?.[0] || null)} - accept=".pdf,.docx,.txt,.md" - /> -
- -
+ +
{/* Folder Upload Card */} - + @@ -560,7 +530,7 @@ function KnowledgeSourcesPage() { Process all documents in a folder path - +
@@ -626,22 +596,24 @@ function KnowledgeSourcesPage() {
-
- - setMaxFiles(parseInt(e.target.value) || 10)} - className="w-24" - min="1" - max="100" - /> - - (Leave blank or set to 0 for unlimited) - +
+
+ + setMaxFiles(parseInt(e.target.value) || 10)} + className="w-16 min-w-16 max-w-16 flex-shrink-0" + min="1" + max="100" + /> + + (Leave blank or set to 0 for unlimited) + +
@@ -650,7 +622,7 @@ function KnowledgeSourcesPage() { {/* Connectors Grid */}
{connectors.map((connector) => ( - +
@@ -665,7 +637,7 @@ function KnowledgeSourcesPage() { {getStatusBadge(connector.status)}
- + {connector.status === "connected" ? (
- - - +
+
+
+ {/* Knowledge Filter Dropdown */} + + + {/* GitHub Star Button */} + + + {/* Discord Link */} + + + {/* Task Notification Bell */} + + + {/* Separator */} +
+ +
-
+
-
-
+
+
{children} diff --git a/frontend/src/components/user-nav.tsx b/frontend/src/components/user-nav.tsx index 07c899be..b818f06c 100644 --- a/frontend/src/components/user-nav.tsx +++ b/frontend/src/components/user-nav.tsx @@ -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 ( - @@ -58,6 +61,23 @@ export function UserNav() {
+ + + Profile + + + + Settings + + setTheme(theme === "light" ? "dark" : "light")}> + {theme === "light" ? ( + + ) : ( + + )} + Toggle Theme + + Log out diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts index 67d0d1e9..a07cef4f 100644 --- a/frontend/tailwind.config.ts +++ b/frontend/tailwind.config.ts @@ -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; \ No newline at end of file