feat/fix(frontend): update shadcn compnents, fix debug drawer layout and font sizes
This commit is contained in:
25
web/components.json
Normal file
25
web/components.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "base-nova",
|
||||||
|
"rsc": false,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "",
|
||||||
|
"css": "index.css",
|
||||||
|
"baseColor": "neutral",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"iconLibrary": "lucide",
|
||||||
|
"rtl": false,
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils",
|
||||||
|
"ui": "@/components/ui",
|
||||||
|
"lib": "@/lib",
|
||||||
|
"hooks": "@/hooks"
|
||||||
|
},
|
||||||
|
"menuColor": "default",
|
||||||
|
"menuAccent": "subtle",
|
||||||
|
"registries": {}
|
||||||
|
}
|
||||||
@@ -1,63 +1,37 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { X } from 'lucide-react';
|
import { X } from 'lucide-react';
|
||||||
|
|
||||||
// Button
|
// Shadcn UI Imports
|
||||||
|
import { Button as ShadcnButton } from './ui/button';
|
||||||
|
import { Input as ShadcnInput } from './ui/input';
|
||||||
|
import { Switch as ShadcnSwitch } from './ui/switch';
|
||||||
|
import { Card as ShadcnCard } from './ui/card';
|
||||||
|
import { Badge as ShadcnBadge } from './ui/badge';
|
||||||
|
import { TableHeader as ShadcnTableHeader, TableRow as ShadcnTableRow, TableHead as ShadcnTableHead, TableCell as ShadcnTableCell } from './ui/table';
|
||||||
|
import { Sheet, SheetContent, SheetHeader, SheetTitle } from './ui/sheet';
|
||||||
|
import { Dialog as ShadcnDialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from './ui/dialog';
|
||||||
|
|
||||||
|
// Button Wrapper to match old API
|
||||||
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'destructive';
|
variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'destructive';
|
||||||
size?: 'sm' | 'md' | 'lg' | 'icon';
|
size?: 'sm' | 'md' | 'lg' | 'icon';
|
||||||
}
|
}
|
||||||
|
export const Button: React.FC<ButtonProps> = ({ variant = 'primary', size = 'md', className, ...props }) => {
|
||||||
|
const vMap: any = { primary: 'default', secondary: 'secondary', outline: 'outline', ghost: 'ghost', destructive: 'destructive' };
|
||||||
|
const sMap: any = { sm: 'sm', md: 'default', lg: 'lg', icon: 'icon' };
|
||||||
|
return <ShadcnButton variant={vMap[variant] || 'default'} size={sMap[size] || 'default'} className={className} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
export const Button: React.FC<ButtonProps> = ({
|
// Input and Switch match seamlessly
|
||||||
className = '',
|
export const Input = ShadcnInput;
|
||||||
variant = 'primary',
|
export const Switch = ShadcnSwitch;
|
||||||
size = 'md',
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}) => {
|
|
||||||
const baseStyles = "inline-flex items-center justify-center rounded-md text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 active:scale-95";
|
|
||||||
|
|
||||||
const variants = {
|
|
||||||
// Primary: Glow effect
|
|
||||||
primary: "bg-primary text-primary-foreground shadow-[0_0_10px_rgba(6,182,212,0.5)] hover:bg-primary/90 hover:shadow-[0_0_15px_rgba(6,182,212,0.6)]",
|
|
||||||
secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
|
||||||
outline: "border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground hover:border-primary/50",
|
|
||||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
|
||||||
destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
|
||||||
};
|
|
||||||
|
|
||||||
const sizes = {
|
|
||||||
sm: "h-8 px-3 text-xs",
|
|
||||||
md: "h-9 px-4 py-2",
|
|
||||||
lg: "h-10 px-8",
|
|
||||||
icon: "h-9 w-9",
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button className={`${baseStyles} ${variants[variant]} ${sizes[size]} ${className}`} {...props}>
|
|
||||||
{children}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Input - Removed border, added subtle background
|
|
||||||
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
|
|
||||||
|
|
||||||
export const Input: React.FC<InputProps> = ({ className = '', ...props }) => {
|
|
||||||
return (
|
|
||||||
<input
|
|
||||||
className={`flex h-9 w-full rounded-md bg-white/5 px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 focus-visible:bg-white/10 disabled:cursor-not-allowed disabled:opacity-50 ${className}`}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface SelectProps extends React.SelectHTMLAttributes<HTMLSelectElement> {}
|
|
||||||
|
|
||||||
|
// Native Select Wrapper to avoid breaking consumers expecting <select><option></select>
|
||||||
|
interface SelectProps extends React.SelectHTMLAttributes<HTMLSelectElement> { }
|
||||||
export const Select: React.FC<SelectProps> = ({ className = '', children, ...props }) => {
|
export const Select: React.FC<SelectProps> = ({ className = '', children, ...props }) => {
|
||||||
return (
|
return (
|
||||||
<select
|
<select
|
||||||
className={`flex h-9 w-full rounded-md border-0 bg-white/5 px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 [&>option]:bg-card text-foreground disabled:cursor-not-allowed disabled:opacity-50 ${className}`}
|
className={`flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm [&>option]:bg-card [&>option]:text-foreground ${className}`}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@@ -65,143 +39,40 @@ export const Select: React.FC<SelectProps> = ({ className = '', children, ...pro
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
interface SwitchProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'onChange'> {
|
// Card Wrapper
|
||||||
checked: boolean;
|
interface CardProps extends React.HTMLAttributes<HTMLDivElement> { children: React.ReactNode; }
|
||||||
onCheckedChange: (checked: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Switch: React.FC<SwitchProps> = ({
|
|
||||||
checked,
|
|
||||||
onCheckedChange,
|
|
||||||
className = '',
|
|
||||||
disabled,
|
|
||||||
...props
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
role="switch"
|
|
||||||
aria-checked={checked}
|
|
||||||
disabled={disabled}
|
|
||||||
onClick={() => {
|
|
||||||
if (!disabled) onCheckedChange(!checked);
|
|
||||||
}}
|
|
||||||
className={`relative h-6 w-11 rounded-full transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/60 focus-visible:ring-offset-1 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 ${checked ? 'bg-emerald-500/80' : 'bg-white/20'} ${className}`}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={`absolute left-0.5 top-1/2 h-5 w-5 -translate-y-1/2 rounded-full bg-white shadow transition-transform ${checked ? 'translate-x-5' : 'translate-x-0'}`}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Card - Glassmorphism style, very subtle border
|
|
||||||
interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
||||||
children: React.ReactNode;
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
export const Card: React.FC<CardProps> = ({ children, className = '', ...props }) => (
|
export const Card: React.FC<CardProps> = ({ children, className = '', ...props }) => (
|
||||||
<div className={`rounded-xl border border-white/5 bg-card/40 backdrop-blur-md text-card-foreground shadow-sm ${className}`} {...props}>
|
<ShadcnCard className={`bg-card/40 backdrop-blur-md ${className}`} {...props}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</ShadcnCard>
|
||||||
);
|
);
|
||||||
|
|
||||||
// Badge
|
// Badge Wrapper for old variants
|
||||||
interface BadgeProps {
|
interface BadgeProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
variant?: 'default' | 'success' | 'warning' | 'outline';
|
variant?: 'default' | 'success' | 'warning' | 'outline';
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
export const Badge: React.FC<BadgeProps> = ({ children, variant = 'default', className = '' }) => {
|
export const Badge: React.FC<BadgeProps> = ({ children, variant = 'default', className = '' }) => {
|
||||||
const styles = {
|
let cName = className;
|
||||||
default: "border-transparent bg-primary/20 text-primary hover:bg-primary/30 border border-primary/20",
|
let shadcnVariant: any = variant === 'outline' ? 'outline' : 'default';
|
||||||
success: "border-transparent bg-green-500/20 text-green-400 border border-green-500/20",
|
|
||||||
warning: "border-transparent bg-yellow-500/20 text-yellow-400 border border-yellow-500/20",
|
if (variant === 'success') {
|
||||||
outline: "text-foreground border border-white/10 hover:bg-accent hover:text-accent-foreground",
|
cName += ' border-transparent bg-emerald-500/20 text-emerald-400 hover:bg-emerald-500/30';
|
||||||
};
|
} else if (variant === 'warning') {
|
||||||
return (
|
cName += ' border-transparent bg-yellow-500/20 text-yellow-400 hover:bg-yellow-500/30';
|
||||||
<div className={`inline-flex items-center rounded-md px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 ${styles[variant]} ${className}`}>
|
}
|
||||||
{children}
|
|
||||||
</div>
|
return <ShadcnBadge variant={shadcnVariant} className={cName}>{children}</ShadcnBadge>;
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Table - Subtle borders
|
// Table Exports
|
||||||
export const TableHeader: React.FC<{ children: React.ReactNode }> = ({ children }) => <thead className="[&_tr]:border-b [&_tr]:border-white/5">{children}</thead>;
|
export const TableHeader = ShadcnTableHeader;
|
||||||
|
export const TableRow = ShadcnTableRow;
|
||||||
|
export const TableHead = ShadcnTableHead;
|
||||||
|
export const TableCell = ShadcnTableCell;
|
||||||
|
|
||||||
interface TableRowProps extends React.HTMLAttributes<HTMLTableRowElement> {
|
// Drawer (Side Sheet Wrapper)
|
||||||
children: React.ReactNode;
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
export const TableRow: React.FC<TableRowProps> = ({ children, className = '', ...props }) => <tr className={`border-b border-white/5 transition-colors hover:bg-white/5 data-[state=selected]:bg-muted ${className}`} {...props}>{children}</tr>;
|
|
||||||
|
|
||||||
interface TableHeadProps extends React.ThHTMLAttributes<HTMLTableCellElement> {
|
|
||||||
children: React.ReactNode;
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
export const TableHead: React.FC<TableHeadProps> = ({ children, className = '', ...props }) => <th className={`h-10 px-4 text-left align-middle text-sm font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 ${className}`} {...props}>{children}</th>;
|
|
||||||
|
|
||||||
interface TableCellProps extends React.TdHTMLAttributes<HTMLTableCellElement> {
|
|
||||||
children: React.ReactNode;
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
export const TableCell: React.FC<TableCellProps> = ({ children, className = '', ...props }) => <td className={`p-4 align-middle text-sm [&:has([role=checkbox])]:pr-0 ${className}`} {...props}>{children}</td>;
|
|
||||||
|
|
||||||
interface LibraryPageShellProps {
|
|
||||||
title: string;
|
|
||||||
primaryAction: React.ReactNode;
|
|
||||||
filterBar: React.ReactNode;
|
|
||||||
children: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const LibraryPageShell: React.FC<LibraryPageShellProps> = ({ title, primaryAction, filterBar, children }) => {
|
|
||||||
return (
|
|
||||||
<div className="space-y-6 animate-in fade-in py-4 pb-10">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<h1 className="text-2xl font-bold tracking-tight text-white">{title}</h1>
|
|
||||||
{primaryAction}
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 bg-card/50 p-4 rounded-lg border border-white/5 shadow-sm">
|
|
||||||
{filterBar}
|
|
||||||
</div>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface TableStatusRowProps {
|
|
||||||
colSpan: number;
|
|
||||||
text: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TableStatusRow: React.FC<TableStatusRowProps> = ({ colSpan, text }) => {
|
|
||||||
return (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell colSpan={colSpan} className="text-center py-8 text-muted-foreground">
|
|
||||||
{text}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface LibraryActionCellProps {
|
|
||||||
previewAction?: React.ReactNode;
|
|
||||||
editAction: React.ReactNode;
|
|
||||||
deleteAction: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const LibraryActionCell: React.FC<LibraryActionCellProps> = ({ previewAction, editAction, deleteAction }) => {
|
|
||||||
return (
|
|
||||||
<TableCell className="text-right">
|
|
||||||
{previewAction}
|
|
||||||
{editAction}
|
|
||||||
{deleteAction}
|
|
||||||
</TableCell>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Drawer (Side Sheet)
|
|
||||||
interface DrawerProps {
|
interface DrawerProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
@@ -209,32 +80,25 @@ interface DrawerProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Drawer: React.FC<DrawerProps> = ({ isOpen, onClose, title, className, children }) => {
|
export const Drawer: React.FC<DrawerProps> = ({ isOpen, onClose, title, className, children }) => {
|
||||||
if (!isOpen) return null;
|
// Pass `!w-[85vw]` logic directly down from the parent to naturally override Shadcn specificities safely.
|
||||||
|
const containerClasses = className || 'w-full max-w-md sm:max-w-lg';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 flex">
|
<Sheet open={isOpen} onOpenChange={(open) => { if (!open) onClose(); }}>
|
||||||
{/* Backdrop */}
|
<SheetContent className={`flex flex-col p-6 bg-background/95 backdrop-blur-md border-l border-white/10 shadow-2xl [&>button]:top-5 [&>button]:right-5 ${containerClasses}`}>
|
||||||
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm transition-opacity" onClick={onClose} />
|
<SheetHeader className="mb-2 shrink-0 p-0 text-left">
|
||||||
|
<SheetTitle className="text-lg font-semibold">{title}</SheetTitle>
|
||||||
{/* Drawer Content */}
|
</SheetHeader>
|
||||||
<div className={`relative ml-auto flex h-full w-full flex-col bg-background/95 border-l border-white/10 p-6 shadow-2xl animate-in slide-in-from-right ${className || 'max-w-md sm:max-w-lg'}`}>
|
<div className="flex-1 min-h-0 overflow-y-auto custom-scrollbar flex flex-col">
|
||||||
<div className="flex items-center justify-between mb-4 shrink-0">
|
|
||||||
<h2 className="text-lg font-semibold text-foreground">{title}</h2>
|
|
||||||
<Button variant="ghost" size="icon" onClick={onClose}>
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 min-h-0 overflow-y-auto">
|
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</SheetContent>
|
||||||
</div>
|
</Sheet>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Dialog (Modal)
|
// Dialog (Modal Wrapper)
|
||||||
interface DialogProps {
|
interface DialogProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
@@ -243,33 +107,66 @@ interface DialogProps {
|
|||||||
footer?: React.ReactNode;
|
footer?: React.ReactNode;
|
||||||
contentClassName?: string;
|
contentClassName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Dialog: React.FC<DialogProps> = ({ isOpen, onClose, title, children, footer, contentClassName }) => {
|
export const Dialog: React.FC<DialogProps> = ({ isOpen, onClose, title, children, footer, contentClassName }) => {
|
||||||
if (!isOpen) return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
<ShadcnDialog open={isOpen} onOpenChange={(open) => { if (!open) onClose(); }}>
|
||||||
<div className="fixed inset-0 bg-black/80 backdrop-blur-sm transition-opacity animate-in fade-in" onClick={onClose} />
|
<DialogContent className={`max-h-[95vh] flex flex-col ${contentClassName || ''}`}>
|
||||||
<div className={`relative z-50 w-full max-w-lg rounded-xl border border-white/10 bg-card p-6 shadow-2xl animate-in zoom-in-95 duration-200 ${contentClassName || ''}`}>
|
<DialogHeader>
|
||||||
<div className="flex flex-col space-y-1.5 text-center sm:text-left mb-4">
|
<DialogTitle>{title}</DialogTitle>
|
||||||
<h2 className="text-lg font-semibold leading-none tracking-tight">{title}</h2>
|
</DialogHeader>
|
||||||
</div>
|
<div className="py-2 flex-1 min-h-0 overflow-y-auto pr-2 custom-scrollbar">
|
||||||
<div className="py-4">
|
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
{footer && (
|
{footer && <DialogFooter>{footer}</DialogFooter>}
|
||||||
<div className="flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2 mt-2">
|
</DialogContent>
|
||||||
{footer}
|
</ShadcnDialog>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
onClick={onClose}
|
|
||||||
className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"
|
|
||||||
>
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
<span className="sr-only">Close</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------
|
||||||
|
// Custom Application Layout Components
|
||||||
|
// ---------------------------------------------
|
||||||
|
interface LibraryPageShellProps {
|
||||||
|
title: string;
|
||||||
|
primaryAction: React.ReactNode;
|
||||||
|
filterBar: React.ReactNode;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
export const LibraryPageShell: React.FC<LibraryPageShellProps> = ({ title, primaryAction, filterBar, children }) => (
|
||||||
|
<div className="space-y-6 animate-in fade-in py-4 pb-10">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h1 className="text-2xl font-bold tracking-tight text-foreground">{title}</h1>
|
||||||
|
{primaryAction}
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 bg-card/50 p-4 rounded-lg border border-border shadow-sm">
|
||||||
|
{filterBar}
|
||||||
|
</div>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
interface TableStatusRowProps {
|
||||||
|
colSpan: number;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
export const TableStatusRow: React.FC<TableStatusRowProps> = ({ colSpan, text }) => (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={colSpan} className="text-center py-8 text-muted-foreground">
|
||||||
|
{text}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
|
||||||
|
interface LibraryActionCellProps {
|
||||||
|
previewAction?: React.ReactNode;
|
||||||
|
editAction: React.ReactNode;
|
||||||
|
deleteAction: React.ReactNode;
|
||||||
|
}
|
||||||
|
export const LibraryActionCell: React.FC<LibraryActionCellProps> = ({ previewAction, editAction, deleteAction }) => (
|
||||||
|
<TableCell className="text-right whitespace-nowrap">
|
||||||
|
{previewAction}
|
||||||
|
{editAction}
|
||||||
|
{deleteAction}
|
||||||
|
</TableCell>
|
||||||
|
);
|
||||||
|
|||||||
52
web/components/ui/badge.tsx
Normal file
52
web/components/ui/badge.tsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { mergeProps } from "@base-ui/react/merge-props"
|
||||||
|
import { useRender } from "@base-ui/react/use-render"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const badgeVariants = cva(
|
||||||
|
"group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
|
||||||
|
outline:
|
||||||
|
"border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
|
||||||
|
ghost:
|
||||||
|
"hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function Badge({
|
||||||
|
className,
|
||||||
|
variant = "default",
|
||||||
|
render,
|
||||||
|
...props
|
||||||
|
}: useRender.ComponentProps<"span"> & VariantProps<typeof badgeVariants>) {
|
||||||
|
return useRender({
|
||||||
|
defaultTagName: "span",
|
||||||
|
props: mergeProps<"span">(
|
||||||
|
{
|
||||||
|
className: cn(badgeVariants({ variant }), className),
|
||||||
|
},
|
||||||
|
props
|
||||||
|
),
|
||||||
|
render,
|
||||||
|
state: {
|
||||||
|
slot: "badge",
|
||||||
|
variant,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Badge, badgeVariants }
|
||||||
58
web/components/ui/button.tsx
Normal file
58
web/components/ui/button.tsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { Button as ButtonPrimitive } from "@base-ui/react/button"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
||||||
|
outline:
|
||||||
|
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
||||||
|
ghost:
|
||||||
|
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default:
|
||||||
|
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
||||||
|
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
||||||
|
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
|
||||||
|
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
|
||||||
|
icon: "size-8",
|
||||||
|
"icon-xs":
|
||||||
|
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
|
||||||
|
"icon-sm":
|
||||||
|
"size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
|
||||||
|
"icon-lg": "size-9",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function Button({
|
||||||
|
className,
|
||||||
|
variant = "default",
|
||||||
|
size = "default",
|
||||||
|
...props
|
||||||
|
}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
|
||||||
|
return (
|
||||||
|
<ButtonPrimitive
|
||||||
|
data-slot="button"
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Button, buttonVariants }
|
||||||
103
web/components/ui/card.tsx
Normal file
103
web/components/ui/card.tsx
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Card({
|
||||||
|
className,
|
||||||
|
size = "default",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card"
|
||||||
|
data-size={size}
|
||||||
|
className={cn(
|
||||||
|
"group/card flex flex-col gap-4 overflow-hidden rounded-xl bg-card py-4 text-sm text-card-foreground ring-1 ring-foreground/10 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-header"
|
||||||
|
className={cn(
|
||||||
|
"group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-title"
|
||||||
|
className={cn(
|
||||||
|
"text-base leading-snug font-medium group-data-[size=sm]/card:text-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-description"
|
||||||
|
className={cn("text-sm text-muted-foreground", 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}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-content"
|
||||||
|
className={cn("px-4 group-data-[size=sm]/card:px-3", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-footer"
|
||||||
|
className={cn(
|
||||||
|
"flex items-center rounded-b-xl border-t bg-muted/50 p-4 group-data-[size=sm]/card:p-3",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
CardFooter,
|
||||||
|
CardTitle,
|
||||||
|
CardAction,
|
||||||
|
CardDescription,
|
||||||
|
CardContent,
|
||||||
|
}
|
||||||
157
web/components/ui/dialog.tsx
Normal file
157
web/components/ui/dialog.tsx
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { Dialog as DialogPrimitive } from "@base-ui/react/dialog"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { XIcon } from "lucide-react"
|
||||||
|
|
||||||
|
function Dialog({ ...props }: DialogPrimitive.Root.Props) {
|
||||||
|
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogTrigger({ ...props }: DialogPrimitive.Trigger.Props) {
|
||||||
|
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogPortal({ ...props }: DialogPrimitive.Portal.Props) {
|
||||||
|
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogClose({ ...props }: DialogPrimitive.Close.Props) {
|
||||||
|
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogOverlay({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: DialogPrimitive.Backdrop.Props) {
|
||||||
|
return (
|
||||||
|
<DialogPrimitive.Backdrop
|
||||||
|
data-slot="dialog-overlay"
|
||||||
|
className={cn(
|
||||||
|
"fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
showCloseButton = true,
|
||||||
|
...props
|
||||||
|
}: DialogPrimitive.Popup.Props & {
|
||||||
|
showCloseButton?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DialogPortal>
|
||||||
|
<DialogOverlay />
|
||||||
|
<DialogPrimitive.Popup
|
||||||
|
data-slot="dialog-content"
|
||||||
|
className={cn(
|
||||||
|
"fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-background p-4 text-sm ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{showCloseButton && (
|
||||||
|
<DialogPrimitive.Close
|
||||||
|
data-slot="dialog-close"
|
||||||
|
render={
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="absolute top-2 right-2"
|
||||||
|
size="icon-sm"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<XIcon
|
||||||
|
/>
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
</DialogPrimitive.Close>
|
||||||
|
)}
|
||||||
|
</DialogPrimitive.Popup>
|
||||||
|
</DialogPortal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="dialog-header"
|
||||||
|
className={cn("flex flex-col gap-2", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogFooter({
|
||||||
|
className,
|
||||||
|
showCloseButton = false,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & {
|
||||||
|
showCloseButton?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="dialog-footer"
|
||||||
|
className={cn(
|
||||||
|
"-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t bg-muted/50 p-4 sm:flex-row sm:justify-end",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{showCloseButton && (
|
||||||
|
<DialogPrimitive.Close render={<Button variant="outline" />}>
|
||||||
|
Close
|
||||||
|
</DialogPrimitive.Close>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) {
|
||||||
|
return (
|
||||||
|
<DialogPrimitive.Title
|
||||||
|
data-slot="dialog-title"
|
||||||
|
className={cn("text-base leading-none font-medium", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogDescription({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: DialogPrimitive.Description.Props) {
|
||||||
|
return (
|
||||||
|
<DialogPrimitive.Description
|
||||||
|
data-slot="dialog-description"
|
||||||
|
className={cn(
|
||||||
|
"text-sm text-muted-foreground *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogOverlay,
|
||||||
|
DialogPortal,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
}
|
||||||
20
web/components/ui/input.tsx
Normal file
20
web/components/ui/input.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { Input as InputPrimitive } from "@base-ui/react/input"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||||
|
return (
|
||||||
|
<InputPrimitive
|
||||||
|
type={type}
|
||||||
|
data-slot="input"
|
||||||
|
className={cn(
|
||||||
|
"h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Input }
|
||||||
201
web/components/ui/select.tsx
Normal file
201
web/components/ui/select.tsx
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { Select as SelectPrimitive } from "@base-ui/react/select"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react"
|
||||||
|
|
||||||
|
const Select = SelectPrimitive.Root
|
||||||
|
|
||||||
|
function SelectGroup({ className, ...props }: SelectPrimitive.Group.Props) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Group
|
||||||
|
data-slot="select-group"
|
||||||
|
className={cn("scroll-my-1 p-1", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectValue({ className, ...props }: SelectPrimitive.Value.Props) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Value
|
||||||
|
data-slot="select-value"
|
||||||
|
className={cn("flex flex-1 text-left", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectTrigger({
|
||||||
|
className,
|
||||||
|
size = "default",
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: SelectPrimitive.Trigger.Props & {
|
||||||
|
size?: "sm" | "default"
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Trigger
|
||||||
|
data-slot="select-trigger"
|
||||||
|
data-size={size}
|
||||||
|
className={cn(
|
||||||
|
"flex w-fit items-center justify-between gap-1.5 rounded-lg border border-input bg-transparent py-2 pr-2 pl-2.5 text-sm whitespace-nowrap transition-colors outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<SelectPrimitive.Icon
|
||||||
|
render={
|
||||||
|
<ChevronDownIcon className="pointer-events-none size-4 text-muted-foreground" />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</SelectPrimitive.Trigger>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
side = "bottom",
|
||||||
|
sideOffset = 4,
|
||||||
|
align = "center",
|
||||||
|
alignOffset = 0,
|
||||||
|
alignItemWithTrigger = true,
|
||||||
|
...props
|
||||||
|
}: SelectPrimitive.Popup.Props &
|
||||||
|
Pick<
|
||||||
|
SelectPrimitive.Positioner.Props,
|
||||||
|
"align" | "alignOffset" | "side" | "sideOffset" | "alignItemWithTrigger"
|
||||||
|
>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Portal>
|
||||||
|
<SelectPrimitive.Positioner
|
||||||
|
side={side}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
align={align}
|
||||||
|
alignOffset={alignOffset}
|
||||||
|
alignItemWithTrigger={alignItemWithTrigger}
|
||||||
|
className="isolate z-50"
|
||||||
|
>
|
||||||
|
<SelectPrimitive.Popup
|
||||||
|
data-slot="select-content"
|
||||||
|
data-align-trigger={alignItemWithTrigger}
|
||||||
|
className={cn("relative isolate z-50 max-h-(--available-height) w-(--anchor-width) min-w-36 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className )}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SelectScrollUpButton />
|
||||||
|
<SelectPrimitive.List>{children}</SelectPrimitive.List>
|
||||||
|
<SelectScrollDownButton />
|
||||||
|
</SelectPrimitive.Popup>
|
||||||
|
</SelectPrimitive.Positioner>
|
||||||
|
</SelectPrimitive.Portal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectLabel({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: SelectPrimitive.GroupLabel.Props) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.GroupLabel
|
||||||
|
data-slot="select-label"
|
||||||
|
className={cn("px-1.5 py-1 text-xs text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: SelectPrimitive.Item.Props) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Item
|
||||||
|
data-slot="select-item"
|
||||||
|
className={cn(
|
||||||
|
"relative flex w-full cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SelectPrimitive.ItemText className="flex flex-1 shrink-0 gap-2 whitespace-nowrap">
|
||||||
|
{children}
|
||||||
|
</SelectPrimitive.ItemText>
|
||||||
|
<SelectPrimitive.ItemIndicator
|
||||||
|
render={
|
||||||
|
<span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center" />
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<CheckIcon className="pointer-events-none" />
|
||||||
|
</SelectPrimitive.ItemIndicator>
|
||||||
|
</SelectPrimitive.Item>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectSeparator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: SelectPrimitive.Separator.Props) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Separator
|
||||||
|
data-slot="select-separator"
|
||||||
|
className={cn("pointer-events-none -mx-1 my-1 h-px bg-border", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectScrollUpButton({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpArrow>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.ScrollUpArrow
|
||||||
|
data-slot="select-scroll-up-button"
|
||||||
|
className={cn(
|
||||||
|
"top-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronUpIcon
|
||||||
|
/>
|
||||||
|
</SelectPrimitive.ScrollUpArrow>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectScrollDownButton({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownArrow>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.ScrollDownArrow
|
||||||
|
data-slot="select-scroll-down-button"
|
||||||
|
className={cn(
|
||||||
|
"bottom-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronDownIcon
|
||||||
|
/>
|
||||||
|
</SelectPrimitive.ScrollDownArrow>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
|
SelectScrollDownButton,
|
||||||
|
SelectScrollUpButton,
|
||||||
|
SelectSeparator,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
}
|
||||||
133
web/components/ui/sheet.tsx
Normal file
133
web/components/ui/sheet.tsx
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { Dialog as SheetPrimitive } from "@base-ui/react/dialog"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { XIcon } from "lucide-react"
|
||||||
|
|
||||||
|
function Sheet({ ...props }: SheetPrimitive.Root.Props) {
|
||||||
|
return <SheetPrimitive.Root data-slot="sheet" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetTrigger({ ...props }: SheetPrimitive.Trigger.Props) {
|
||||||
|
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetClose({ ...props }: SheetPrimitive.Close.Props) {
|
||||||
|
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetPortal({ ...props }: SheetPrimitive.Portal.Props) {
|
||||||
|
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetOverlay({ className, ...props }: SheetPrimitive.Backdrop.Props) {
|
||||||
|
return (
|
||||||
|
<SheetPrimitive.Backdrop
|
||||||
|
data-slot="sheet-overlay"
|
||||||
|
className={cn(
|
||||||
|
"fixed inset-0 z-50 bg-black/10 transition-opacity duration-150 data-ending-style:opacity-0 data-starting-style:opacity-0 supports-backdrop-filter:backdrop-blur-xs",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
side = "right",
|
||||||
|
showCloseButton = true,
|
||||||
|
...props
|
||||||
|
}: SheetPrimitive.Popup.Props & {
|
||||||
|
side?: "top" | "right" | "bottom" | "left"
|
||||||
|
showCloseButton?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<SheetPortal>
|
||||||
|
<SheetOverlay />
|
||||||
|
<SheetPrimitive.Popup
|
||||||
|
data-slot="sheet-content"
|
||||||
|
data-side={side}
|
||||||
|
className={cn(
|
||||||
|
"fixed z-50 flex flex-col gap-4 bg-background bg-clip-padding text-sm shadow-lg transition duration-200 ease-in-out data-ending-style:opacity-0 data-starting-style:opacity-0 data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t data-[side=bottom]:data-ending-style:translate-y-[2.5rem] data-[side=bottom]:data-starting-style:translate-y-[2.5rem] data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r data-[side=left]:data-ending-style:translate-x-[-2.5rem] data-[side=left]:data-starting-style:translate-x-[-2.5rem] data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-l data-[side=right]:data-ending-style:translate-x-[2.5rem] data-[side=right]:data-starting-style:translate-x-[2.5rem] data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=top]:data-ending-style:translate-y-[-2.5rem] data-[side=top]:data-starting-style:translate-y-[-2.5rem] data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{showCloseButton && (
|
||||||
|
<SheetPrimitive.Close
|
||||||
|
data-slot="sheet-close"
|
||||||
|
render={
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="absolute top-3 right-3"
|
||||||
|
size="icon-sm"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<XIcon
|
||||||
|
/>
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
</SheetPrimitive.Close>
|
||||||
|
)}
|
||||||
|
</SheetPrimitive.Popup>
|
||||||
|
</SheetPortal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="sheet-header"
|
||||||
|
className={cn("flex flex-col gap-0.5 p-4", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="sheet-footer"
|
||||||
|
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetTitle({ className, ...props }: SheetPrimitive.Title.Props) {
|
||||||
|
return (
|
||||||
|
<SheetPrimitive.Title
|
||||||
|
data-slot="sheet-title"
|
||||||
|
className={cn("text-base font-medium text-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetDescription({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: SheetPrimitive.Description.Props) {
|
||||||
|
return (
|
||||||
|
<SheetPrimitive.Description
|
||||||
|
data-slot="sheet-description"
|
||||||
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Sheet,
|
||||||
|
SheetTrigger,
|
||||||
|
SheetClose,
|
||||||
|
SheetContent,
|
||||||
|
SheetHeader,
|
||||||
|
SheetFooter,
|
||||||
|
SheetTitle,
|
||||||
|
SheetDescription,
|
||||||
|
}
|
||||||
30
web/components/ui/switch.tsx
Normal file
30
web/components/ui/switch.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { Switch as SwitchPrimitive } from "@base-ui/react/switch"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Switch({
|
||||||
|
className,
|
||||||
|
size = "default",
|
||||||
|
...props
|
||||||
|
}: SwitchPrimitive.Root.Props & {
|
||||||
|
size?: "sm" | "default"
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<SwitchPrimitive.Root
|
||||||
|
data-slot="switch"
|
||||||
|
data-size={size}
|
||||||
|
className={cn(
|
||||||
|
"peer group/switch relative inline-flex shrink-0 items-center rounded-full border border-transparent transition-all outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-[size=default]:h-[18.4px] data-[size=default]:w-[32px] data-[size=sm]:h-[14px] data-[size=sm]:w-[24px] dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:bg-primary data-unchecked:bg-input dark:data-unchecked:bg-input/80 data-disabled:cursor-not-allowed data-disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SwitchPrimitive.Thumb
|
||||||
|
data-slot="switch-thumb"
|
||||||
|
className="pointer-events-none block rounded-full bg-background ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 group-data-[size=default]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=sm]/switch:data-checked:translate-x-[calc(100%-2px)] dark:data-checked:bg-primary-foreground group-data-[size=default]/switch:data-unchecked:translate-x-0 group-data-[size=sm]/switch:data-unchecked:translate-x-0 dark:data-unchecked:bg-foreground"
|
||||||
|
/>
|
||||||
|
</SwitchPrimitive.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Switch }
|
||||||
116
web/components/ui/table.tsx
Normal file
116
web/components/ui/table.tsx
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Table({ className, ...props }: React.ComponentProps<"table">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="table-container"
|
||||||
|
className="relative w-full overflow-x-auto"
|
||||||
|
>
|
||||||
|
<table
|
||||||
|
data-slot="table"
|
||||||
|
className={cn("w-full caption-bottom text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
||||||
|
return (
|
||||||
|
<thead
|
||||||
|
data-slot="table-header"
|
||||||
|
className={cn("[&_tr]:border-b", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
||||||
|
return (
|
||||||
|
<tbody
|
||||||
|
data-slot="table-body"
|
||||||
|
className={cn("[&_tr:last-child]:border-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
||||||
|
return (
|
||||||
|
<tfoot
|
||||||
|
data-slot="table-footer"
|
||||||
|
className={cn(
|
||||||
|
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
data-slot="table-row"
|
||||||
|
className={cn(
|
||||||
|
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
||||||
|
return (
|
||||||
|
<th
|
||||||
|
data-slot="table-head"
|
||||||
|
className={cn(
|
||||||
|
"h-10 px-2 text-left align-middle font-medium whitespace-nowrap text-foreground [&:has([role=checkbox])]:pr-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
||||||
|
return (
|
||||||
|
<td
|
||||||
|
data-slot="table-cell"
|
||||||
|
className={cn(
|
||||||
|
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableCaption({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"caption">) {
|
||||||
|
return (
|
||||||
|
<caption
|
||||||
|
data-slot="table-caption"
|
||||||
|
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Table,
|
||||||
|
TableHeader,
|
||||||
|
TableBody,
|
||||||
|
TableFooter,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
TableCell,
|
||||||
|
TableCaption,
|
||||||
|
}
|
||||||
158
web/index.css
Normal file
158
web/index.css
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
@import "tw-animate-css";
|
||||||
|
@import "shadcn/tailwind.css";
|
||||||
|
@import "@fontsource-variable/geist";
|
||||||
|
|
||||||
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--background: oklch(1 0 0);
|
||||||
|
--foreground: oklch(0.145 0 0);
|
||||||
|
--card: oklch(1 0 0);
|
||||||
|
--card-foreground: oklch(0.145 0 0);
|
||||||
|
--popover: oklch(1 0 0);
|
||||||
|
--popover-foreground: oklch(0.145 0 0);
|
||||||
|
--primary: oklch(0.205 0 0);
|
||||||
|
--primary-foreground: oklch(0.985 0 0);
|
||||||
|
--secondary: oklch(0.97 0 0);
|
||||||
|
--secondary-foreground: oklch(0.205 0 0);
|
||||||
|
--muted: oklch(0.97 0 0);
|
||||||
|
--muted-foreground: oklch(0.556 0 0);
|
||||||
|
--accent: oklch(0.97 0 0);
|
||||||
|
--accent-foreground: oklch(0.205 0 0);
|
||||||
|
--destructive: oklch(0.58 0.22 27);
|
||||||
|
--border: oklch(0.922 0 0);
|
||||||
|
--input: oklch(0.922 0 0);
|
||||||
|
--ring: oklch(0.708 0 0);
|
||||||
|
--chart-1: oklch(0.809 0.105 251.813);
|
||||||
|
--chart-2: oklch(0.623 0.214 259.815);
|
||||||
|
--chart-3: oklch(0.546 0.245 262.881);
|
||||||
|
--chart-4: oklch(0.488 0.243 264.376);
|
||||||
|
--chart-5: oklch(0.424 0.199 265.638);
|
||||||
|
--radius: 0.625rem;
|
||||||
|
--sidebar: oklch(0.985 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.145 0 0);
|
||||||
|
--sidebar-primary: oklch(0.205 0 0);
|
||||||
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-accent: oklch(0.97 0 0);
|
||||||
|
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||||
|
--sidebar-border: oklch(0.922 0 0);
|
||||||
|
--sidebar-ring: oklch(0.708 0 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
/* Using the exact colors from your previous custom theme */
|
||||||
|
--background: hsl(224, 71%, 4%);
|
||||||
|
--foreground: hsl(210, 40%, 98%);
|
||||||
|
--card: hsl(224, 71%, 5%);
|
||||||
|
--card-foreground: hsl(210, 40%, 98%);
|
||||||
|
--popover: hsl(224, 71%, 4%);
|
||||||
|
--popover-foreground: hsl(210, 40%, 98%);
|
||||||
|
--primary: hsl(196, 100%, 50%);
|
||||||
|
--primary-foreground: hsl(222.2, 47.4%, 11.2%);
|
||||||
|
--secondary: hsl(217.2, 32.6%, 17.5%);
|
||||||
|
--secondary-foreground: hsl(210, 40%, 98%);
|
||||||
|
--muted: hsl(217.2, 32.6%, 17.5%);
|
||||||
|
--muted-foreground: hsl(215, 20.2%, 65.1%);
|
||||||
|
--accent: hsl(217.2, 32.6%, 17.5%);
|
||||||
|
--accent-foreground: hsl(210, 40%, 98%);
|
||||||
|
--destructive: hsl(0, 62.8%, 30.6%);
|
||||||
|
--destructive-foreground: hsl(210, 40%, 98%);
|
||||||
|
--border: hsl(217.2, 32.6%, 17.5%);
|
||||||
|
--input: hsl(217.2, 32.6%, 17.5%);
|
||||||
|
--ring: hsl(196, 100%, 50%);
|
||||||
|
--chart-1: oklch(0.809 0.105 251.813);
|
||||||
|
--chart-2: oklch(0.623 0.214 259.815);
|
||||||
|
--chart-3: oklch(0.546 0.245 262.881);
|
||||||
|
--chart-4: oklch(0.488 0.243 264.376);
|
||||||
|
--chart-5: oklch(0.424 0.199 265.638);
|
||||||
|
--sidebar: hsl(224, 71%, 5%);
|
||||||
|
--sidebar-foreground: hsl(210, 40%, 98%);
|
||||||
|
--sidebar-primary: hsl(196, 100%, 50%);
|
||||||
|
--sidebar-primary-foreground: hsl(222.2, 47.4%, 11.2%);
|
||||||
|
--sidebar-accent: hsl(217.2, 32.6%, 17.5%);
|
||||||
|
--sidebar-accent-foreground: hsl(210, 40%, 98%);
|
||||||
|
--sidebar-border: hsl(217.2, 32.6%, 17.5%);
|
||||||
|
--sidebar-ring: hsl(196, 100%, 50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--font-sans: 'Geist Variable', sans-serif;
|
||||||
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
|
--color-sidebar: var(--sidebar);
|
||||||
|
--color-chart-5: var(--chart-5);
|
||||||
|
--color-chart-4: var(--chart-4);
|
||||||
|
--color-chart-3: var(--chart-3);
|
||||||
|
--color-chart-2: var(--chart-2);
|
||||||
|
--color-chart-1: var(--chart-1);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
--color-card: var(--card);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--color-background: var(--background);
|
||||||
|
--radius-sm: calc(var(--radius) * 0.6);
|
||||||
|
--radius-md: calc(var(--radius) * 0.8);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) * 1.4);
|
||||||
|
--radius-2xl: calc(var(--radius) * 1.8);
|
||||||
|
--radius-3xl: calc(var(--radius) * 2.2);
|
||||||
|
--radius-4xl: calc(var(--radius) * 2.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border outline-ring/50;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
/* Subtle Grid Pattern */
|
||||||
|
background-image:
|
||||||
|
linear-gradient(rgba(255, 255, 255, 0.03) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, rgba(255, 255, 255, 0.03) 1px, transparent 1px);
|
||||||
|
background-size: 40px 40px;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
@apply font-sans;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Scrollbar styled for dark mode */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--muted);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--primary);
|
||||||
|
}
|
||||||
113
web/index.html
113
web/index.html
@@ -1,122 +1,11 @@
|
|||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en" class="dark">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>AI视频助手</title>
|
<title>AI视频助手</title>
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
|
||||||
<script>
|
|
||||||
tailwind.config = {
|
|
||||||
darkMode: 'class',
|
|
||||||
theme: {
|
|
||||||
extend: {
|
|
||||||
colors: {
|
|
||||||
border: "hsl(var(--border))",
|
|
||||||
input: "hsl(var(--input))",
|
|
||||||
ring: "hsl(var(--ring))",
|
|
||||||
background: "hsl(var(--background))",
|
|
||||||
foreground: "hsl(var(--foreground))",
|
|
||||||
primary: {
|
|
||||||
DEFAULT: "hsl(var(--primary))",
|
|
||||||
foreground: "hsl(var(--primary-foreground))",
|
|
||||||
},
|
|
||||||
secondary: {
|
|
||||||
DEFAULT: "hsl(var(--secondary))",
|
|
||||||
foreground: "hsl(var(--secondary-foreground))",
|
|
||||||
},
|
|
||||||
destructive: {
|
|
||||||
DEFAULT: "hsl(var(--destructive))",
|
|
||||||
foreground: "hsl(var(--destructive-foreground))",
|
|
||||||
},
|
|
||||||
muted: {
|
|
||||||
DEFAULT: "hsl(var(--muted))",
|
|
||||||
foreground: "hsl(var(--muted-foreground))",
|
|
||||||
},
|
|
||||||
accent: {
|
|
||||||
DEFAULT: "hsl(var(--accent))",
|
|
||||||
foreground: "hsl(var(--accent-foreground))",
|
|
||||||
},
|
|
||||||
card: {
|
|
||||||
DEFAULT: "hsl(var(--card))",
|
|
||||||
foreground: "hsl(var(--card-foreground))",
|
|
||||||
},
|
|
||||||
popover: {
|
|
||||||
DEFAULT: "hsl(var(--popover))",
|
|
||||||
foreground: "hsl(var(--popover-foreground))",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
borderRadius: {
|
|
||||||
lg: "var(--radius)",
|
|
||||||
md: "calc(var(--radius) - 2px)",
|
|
||||||
sm: "calc(var(--radius) - 4px)",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<style>
|
|
||||||
:root {
|
|
||||||
/* Tech/Dark Theme Palette */
|
|
||||||
--background: 224 71% 4%; /* Very Dark Blue #020817 */
|
|
||||||
--foreground: 210 40% 98%; /* Light Gray/White */
|
|
||||||
|
|
||||||
--card: 224 71% 5%; /* Slightly lighter than bg, will use transparency */
|
|
||||||
--card-foreground: 210 40% 98%;
|
|
||||||
|
|
||||||
--popover: 224 71% 4%;
|
|
||||||
--popover-foreground: 210 40% 98%;
|
|
||||||
|
|
||||||
--primary: 196 100% 50%; /* Cyan/Electric Blue for high tech feel */
|
|
||||||
--primary-foreground: 222.2 47.4% 11.2%;
|
|
||||||
|
|
||||||
--secondary: 217.2 32.6% 17.5%;
|
|
||||||
--secondary-foreground: 210 40% 98%;
|
|
||||||
|
|
||||||
--muted: 217.2 32.6% 17.5%;
|
|
||||||
--muted-foreground: 215 20.2% 65.1%;
|
|
||||||
|
|
||||||
--accent: 217.2 32.6% 17.5%;
|
|
||||||
--accent-foreground: 210 40% 98%;
|
|
||||||
|
|
||||||
--destructive: 0 62.8% 30.6%;
|
|
||||||
--destructive-foreground: 210 40% 98%;
|
|
||||||
|
|
||||||
--border: 217.2 32.6% 17.5%;
|
|
||||||
--input: 217.2 32.6% 17.5%;
|
|
||||||
--ring: 196 100% 50%;
|
|
||||||
|
|
||||||
--radius: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
||||||
background-color: hsl(var(--background));
|
|
||||||
color: hsl(var(--foreground));
|
|
||||||
/* Subtle Grid Pattern */
|
|
||||||
background-image:
|
|
||||||
linear-gradient(rgba(255, 255, 255, 0.03) 1px, transparent 1px),
|
|
||||||
linear-gradient(90deg, rgba(255, 255, 255, 0.03) 1px, transparent 1px);
|
|
||||||
background-size: 40px 40px;
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Custom Scrollbar */
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
}
|
|
||||||
::-webkit-scrollbar-track {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
background: hsl(var(--muted));
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: hsl(var(--primary));
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<script type="importmap">
|
<script type="importmap">
|
||||||
{
|
{
|
||||||
"imports": {
|
"imports": {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import ReactDOM from 'react-dom/client';
|
|||||||
import { QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClientProvider } from '@tanstack/react-query';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
import { queryClient } from './services/queryClient';
|
import { queryClient } from './services/queryClient';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
const rootElement = document.getElementById('root');
|
const rootElement = document.getElementById('root');
|
||||||
if (!rootElement) {
|
if (!rootElement) {
|
||||||
|
|||||||
6
web/lib/utils.ts
Normal file
6
web/lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { clsx, type ClassValue } from "clsx"
|
||||||
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs))
|
||||||
|
}
|
||||||
BIN
web/old_ui.txt
Normal file
BIN
web/old_ui.txt
Normal file
Binary file not shown.
4398
web/package-lock.json
generated
4398
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -9,17 +9,28 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/react-query": "^5.90.2",
|
"@base-ui/react": "^1.2.0",
|
||||||
"lucide-react": "^0.563.0",
|
"@fontsource-variable/geist": "^5.2.8",
|
||||||
"zustand": "^5.0.8",
|
|
||||||
"react-router-dom": "^7.13.0",
|
|
||||||
"@google/genai": "^1.39.0",
|
"@google/genai": "^1.39.0",
|
||||||
|
"@tailwindcss/vite": "^4.2.1",
|
||||||
|
"@tanstack/react-query": "^5.90.2",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"lucide-react": "^0.563.0",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.4"
|
"react-dom": "^19.2.4",
|
||||||
|
"react-router-dom": "^7.13.0",
|
||||||
|
"shadcn": "^4.0.2",
|
||||||
|
"tailwind-merge": "^3.5.0",
|
||||||
|
"tw-animate-css": "^1.4.0",
|
||||||
|
"zustand": "^5.0.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.14.0",
|
"@types/node": "^22.14.0",
|
||||||
"@vitejs/plugin-react": "^5.0.0",
|
"@vitejs/plugin-react": "^5.0.0",
|
||||||
|
"autoprefixer": "^10.4.27",
|
||||||
|
"postcss": "^8.5.8",
|
||||||
|
"tailwindcss": "^4.2.1",
|
||||||
"typescript": "~5.8.2",
|
"typescript": "~5.8.2",
|
||||||
"vite": "^6.2.0"
|
"vite": "^6.2.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -645,9 +645,9 @@ export const AssistantsPage: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="flex h-full min-h-0 gap-6 animate-in fade-in">
|
<div className="flex h-full min-h-0 gap-6 animate-in fade-in">
|
||||||
{/* LEFT COLUMN: List */}
|
{/* LEFT COLUMN: List */}
|
||||||
<div className="w-80 flex flex-col gap-4 shrink-0">
|
<div className="w-80 flex flex-col gap-4 shrink-0 pt-4">
|
||||||
<div className="flex items-center justify-between px-1 text-white">
|
<div className="flex items-center justify-between px-1 text-white">
|
||||||
<h2 className="text-xl font-bold tracking-tight text-white">小助手列表</h2>
|
<h2 className="text-lg font-bold tracking-tight text-white">小助手列表</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
@@ -670,8 +670,7 @@ export const AssistantsPage: React.FC = () => {
|
|||||||
<div
|
<div
|
||||||
key={assistant.id}
|
key={assistant.id}
|
||||||
onClick={() => setSelectedId(assistant.id)}
|
onClick={() => setSelectedId(assistant.id)}
|
||||||
className={`group relative flex flex-col p-4 rounded-xl border transition-all cursor-pointer ${
|
className={`group relative flex flex-col p-4 rounded-xl border transition-all cursor-pointer ${selectedId === assistant.id
|
||||||
selectedId === assistant.id
|
|
||||||
? 'bg-primary/10 border-primary/40 shadow-[0_0_15px_rgba(6,182,212,0.15)]'
|
? 'bg-primary/10 border-primary/40 shadow-[0_0_15px_rgba(6,182,212,0.15)]'
|
||||||
: 'bg-card/30 border-white/5 hover:bg-white/5 hover:border-white/10'
|
: 'bg-card/30 border-white/5 hover:bg-white/5 hover:border-white/10'
|
||||||
}`}
|
}`}
|
||||||
@@ -684,8 +683,7 @@ export const AssistantsPage: React.FC = () => {
|
|||||||
<div className="flex">
|
<div className="flex">
|
||||||
<Badge
|
<Badge
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className={`text-[9px] uppercase tracking-tighter shrink-0 opacity-70 border-white/10 ${
|
className={`text-[9px] uppercase tracking-tighter shrink-0 opacity-70 border-white/10 ${assistant.configMode === 'platform' ? 'text-cyan-400 bg-cyan-400/5' :
|
||||||
assistant.configMode === 'platform' ? 'text-cyan-400 bg-cyan-400/5' :
|
|
||||||
assistant.configMode === 'dify' ? 'text-blue-400 bg-blue-400/5' :
|
assistant.configMode === 'dify' ? 'text-blue-400 bg-blue-400/5' :
|
||||||
'text-purple-400 bg-purple-400/5'
|
'text-purple-400 bg-purple-400/5'
|
||||||
}`}
|
}`}
|
||||||
@@ -733,9 +731,9 @@ export const AssistantsPage: React.FC = () => {
|
|||||||
<div className="flex items-start justify-between gap-4">
|
<div className="flex items-start justify-between gap-4">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<label className="text-[10px] text-muted-foreground font-black tracking-widest uppercase ml-1">小助手名称</label>
|
<label className="text-xs text-muted-foreground font-bold tracking-widest uppercase ml-1">小助手名称</label>
|
||||||
<div className="flex items-center gap-1.5 px-2 py-0.5 rounded-md bg-white/5 border border-white/10 group/id transition-all hover:bg-white/10">
|
<div className="flex items-center gap-1.5 px-2 py-0.5 rounded-md bg-white/5 border border-white/10 group/id transition-all hover:bg-white/10">
|
||||||
<span className="text-[10px] font-mono text-muted-foreground/60 select-all tracking-tight">UUID: {selectedAssistant.id}</span>
|
<span className="text-xs font-mono text-muted-foreground/60 select-all tracking-tight">UUID: {selectedAssistant.id}</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleCopyId(selectedAssistant.id)}
|
onClick={() => handleCopyId(selectedAssistant.id)}
|
||||||
className="text-muted-foreground hover:text-primary transition-colors flex items-center"
|
className="text-muted-foreground hover:text-primary transition-colors flex items-center"
|
||||||
@@ -748,7 +746,7 @@ export const AssistantsPage: React.FC = () => {
|
|||||||
<Input
|
<Input
|
||||||
value={selectedAssistant.name}
|
value={selectedAssistant.name}
|
||||||
onChange={(e) => updateAssistant('name', e.target.value)}
|
onChange={(e) => updateAssistant('name', e.target.value)}
|
||||||
className="font-bold bg-white/5 border-white/10 focus:border-primary/50 text-base"
|
className="font-bold bg-white/5 border-white/10 focus:border-primary/50 text-lg"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2 pt-6">
|
<div className="flex items-center space-x-2 pt-6">
|
||||||
@@ -777,7 +775,7 @@ export const AssistantsPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-[10px] text-muted-foreground font-black tracking-widest uppercase ml-1">配置方式</label>
|
<label className="text-xs text-muted-foreground font-bold tracking-widest uppercase ml-1">配置方式</label>
|
||||||
<div className="relative group w-full">
|
<div className="relative group w-full">
|
||||||
<select
|
<select
|
||||||
className="flex h-10 w-full rounded-md border border-white/10 bg-white/5 px-3 py-1 text-sm shadow-sm transition-all focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 text-foreground appearance-none cursor-pointer [&>option]:bg-card"
|
className="flex h-10 w-full rounded-md border border-white/10 bg-white/5 px-3 py-1 text-sm shadow-sm transition-all focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 text-foreground appearance-none cursor-pointer [&>option]:bg-card"
|
||||||
@@ -866,7 +864,7 @@ export const AssistantsPage: React.FC = () => {
|
|||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-medium text-white flex items-center">
|
<label className="text-sm font-medium text-white flex items-center">
|
||||||
<Globe className="w-4 h-4 mr-2 text-primary"/> 接口地址 (API URL)
|
<Globe className="w-4 h-4 mr-2 text-primary" /> 接口地址 (API URL)
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
value={selectedAssistant.apiUrl || ''}
|
value={selectedAssistant.apiUrl || ''}
|
||||||
@@ -878,7 +876,7 @@ export const AssistantsPage: React.FC = () => {
|
|||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-medium text-white flex items-center">
|
<label className="text-sm font-medium text-white flex items-center">
|
||||||
<Terminal className="w-4 h-4 mr-2 text-primary"/> 密钥 (API KEY)
|
<Terminal className="w-4 h-4 mr-2 text-primary" /> 密钥 (API KEY)
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
type="password"
|
type="password"
|
||||||
@@ -895,7 +893,7 @@ export const AssistantsPage: React.FC = () => {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-medium text-white flex items-center">
|
<label className="text-sm font-medium text-white flex items-center">
|
||||||
<BrainCircuit className="w-4 h-4 mr-2 text-primary"/> 大模型 (LLM Model)
|
<BrainCircuit className="w-4 h-4 mr-2 text-primary" /> 大模型 (LLM Model)
|
||||||
</label>
|
</label>
|
||||||
<div className="relative group">
|
<div className="relative group">
|
||||||
<select
|
<select
|
||||||
@@ -915,7 +913,7 @@ export const AssistantsPage: React.FC = () => {
|
|||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-medium text-white flex items-center">
|
<label className="text-sm font-medium text-white flex items-center">
|
||||||
<BotIcon className="w-4 h-4 mr-2 text-primary"/> 提示词 (Prompt)
|
<BotIcon className="w-4 h-4 mr-2 text-primary" /> 提示词 (Prompt)
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<textarea
|
<textarea
|
||||||
@@ -976,14 +974,13 @@ export const AssistantsPage: React.FC = () => {
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<label className="text-sm font-medium text-white flex items-center">
|
<label className="text-sm font-medium text-white flex items-center">
|
||||||
<PhoneCall className="w-4 h-4 mr-2 text-primary"/> 首轮发言方
|
<PhoneCall className="w-4 h-4 mr-2 text-primary" /> 首轮发言方
|
||||||
</label>
|
</label>
|
||||||
<div className="inline-flex rounded-lg border border-white/10 bg-white/5 p-1">
|
<div className="inline-flex rounded-lg border border-white/10 bg-white/5 p-1">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => updateAssistant('firstTurnMode', 'bot_first')}
|
onClick={() => updateAssistant('firstTurnMode', 'bot_first')}
|
||||||
className={`px-3 py-1 text-xs rounded-md transition-colors ${
|
className={`px-3 py-1 text-xs rounded-md transition-colors ${isBotFirstTurn
|
||||||
isBotFirstTurn
|
|
||||||
? 'bg-primary text-primary-foreground shadow-sm'
|
? 'bg-primary text-primary-foreground shadow-sm'
|
||||||
: 'text-muted-foreground hover:text-foreground'
|
: 'text-muted-foreground hover:text-foreground'
|
||||||
}`}
|
}`}
|
||||||
@@ -993,8 +990,7 @@ export const AssistantsPage: React.FC = () => {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => updateAssistant('firstTurnMode', 'user_first')}
|
onClick={() => updateAssistant('firstTurnMode', 'user_first')}
|
||||||
className={`px-3 py-1 text-xs rounded-md transition-colors ${
|
className={`px-3 py-1 text-xs rounded-md transition-colors ${isBotFirstTurn
|
||||||
isBotFirstTurn
|
|
||||||
? 'text-muted-foreground hover:text-foreground'
|
? 'text-muted-foreground hover:text-foreground'
|
||||||
: 'bg-primary text-primary-foreground shadow-sm'
|
: 'bg-primary text-primary-foreground shadow-sm'
|
||||||
}`}
|
}`}
|
||||||
@@ -1012,14 +1008,13 @@ export const AssistantsPage: React.FC = () => {
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<label className="text-sm font-medium flex items-center text-white">
|
<label className="text-sm font-medium flex items-center text-white">
|
||||||
<MessageSquare className="w-4 h-4 mr-2 text-primary"/> 开场白
|
<MessageSquare className="w-4 h-4 mr-2 text-primary" /> 开场白
|
||||||
</label>
|
</label>
|
||||||
<div className="inline-flex rounded-lg border border-white/10 bg-white/5 p-1">
|
<div className="inline-flex rounded-lg border border-white/10 bg-white/5 p-1">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => updateAssistant('generatedOpenerEnabled', false)}
|
onClick={() => updateAssistant('generatedOpenerEnabled', false)}
|
||||||
className={`px-3 py-1 text-xs rounded-md transition-colors ${
|
className={`px-3 py-1 text-xs rounded-md transition-colors ${selectedAssistant.generatedOpenerEnabled === true
|
||||||
selectedAssistant.generatedOpenerEnabled === true
|
|
||||||
? 'text-muted-foreground hover:text-foreground'
|
? 'text-muted-foreground hover:text-foreground'
|
||||||
: 'bg-primary text-primary-foreground shadow-sm'
|
: 'bg-primary text-primary-foreground shadow-sm'
|
||||||
}`}
|
}`}
|
||||||
@@ -1029,8 +1024,7 @@ export const AssistantsPage: React.FC = () => {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => updateAssistant('generatedOpenerEnabled', true)}
|
onClick={() => updateAssistant('generatedOpenerEnabled', true)}
|
||||||
className={`px-3 py-1 text-xs rounded-md transition-colors ${
|
className={`px-3 py-1 text-xs rounded-md transition-colors ${selectedAssistant.generatedOpenerEnabled === true
|
||||||
selectedAssistant.generatedOpenerEnabled === true
|
|
||||||
? 'bg-primary text-primary-foreground shadow-sm'
|
? 'bg-primary text-primary-foreground shadow-sm'
|
||||||
: 'text-muted-foreground hover:text-foreground'
|
: 'text-muted-foreground hover:text-foreground'
|
||||||
}`}
|
}`}
|
||||||
@@ -1205,8 +1199,7 @@ export const AssistantsPage: React.FC = () => {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => updateAssistant('openerAudioEnabled', false)}
|
onClick={() => updateAssistant('openerAudioEnabled', false)}
|
||||||
className={`px-3 py-1 text-xs rounded-md transition-colors ${
|
className={`px-3 py-1 text-xs rounded-md transition-colors ${selectedAssistant.openerAudioEnabled === true
|
||||||
selectedAssistant.openerAudioEnabled === true
|
|
||||||
? 'text-muted-foreground hover:text-foreground'
|
? 'text-muted-foreground hover:text-foreground'
|
||||||
: 'bg-primary text-primary-foreground shadow-sm'
|
: 'bg-primary text-primary-foreground shadow-sm'
|
||||||
}`}
|
}`}
|
||||||
@@ -1216,8 +1209,7 @@ export const AssistantsPage: React.FC = () => {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => updateAssistant('openerAudioEnabled', true)}
|
onClick={() => updateAssistant('openerAudioEnabled', true)}
|
||||||
className={`px-3 py-1 text-xs rounded-md transition-colors ${
|
className={`px-3 py-1 text-xs rounded-md transition-colors ${selectedAssistant.openerAudioEnabled === true
|
||||||
selectedAssistant.openerAudioEnabled === true
|
|
||||||
? 'bg-primary text-primary-foreground shadow-sm'
|
? 'bg-primary text-primary-foreground shadow-sm'
|
||||||
: 'text-muted-foreground hover:text-foreground'
|
: 'text-muted-foreground hover:text-foreground'
|
||||||
}`}
|
}`}
|
||||||
@@ -1273,7 +1265,7 @@ export const AssistantsPage: React.FC = () => {
|
|||||||
<div className="space-y-8 animate-in fade-in">
|
<div className="space-y-8 animate-in fade-in">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-medium text-white flex items-center">
|
<label className="text-sm font-medium text-white flex items-center">
|
||||||
<BrainCircuit className="w-4 h-4 mr-2 text-primary"/> 嵌入模型 (Embedding Model)
|
<BrainCircuit className="w-4 h-4 mr-2 text-primary" /> 嵌入模型 (Embedding Model)
|
||||||
</label>
|
</label>
|
||||||
<div className="relative group">
|
<div className="relative group">
|
||||||
<select
|
<select
|
||||||
@@ -1293,7 +1285,7 @@ export const AssistantsPage: React.FC = () => {
|
|||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-medium text-white flex items-center">
|
<label className="text-sm font-medium text-white flex items-center">
|
||||||
<Filter className="w-4 h-4 mr-2 text-primary"/> 重排模型 (Rerank Model)
|
<Filter className="w-4 h-4 mr-2 text-primary" /> 重排模型 (Rerank Model)
|
||||||
</label>
|
</label>
|
||||||
<div className="relative group">
|
<div className="relative group">
|
||||||
<select
|
<select
|
||||||
@@ -1313,7 +1305,7 @@ export const AssistantsPage: React.FC = () => {
|
|||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-medium text-white flex items-center">
|
<label className="text-sm font-medium text-white flex items-center">
|
||||||
<Book className="w-4 h-4 mr-2 text-primary"/> 知识库挂载 (Knowledge Base)
|
<Book className="w-4 h-4 mr-2 text-primary" /> 知识库挂载 (Knowledge Base)
|
||||||
</label>
|
</label>
|
||||||
<div className="relative group">
|
<div className="relative group">
|
||||||
<select
|
<select
|
||||||
@@ -1337,7 +1329,7 @@ export const AssistantsPage: React.FC = () => {
|
|||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-medium text-white flex items-center">
|
<label className="text-sm font-medium text-white flex items-center">
|
||||||
<Ear className="w-4 h-4 mr-2 text-primary"/> 语音识别 (ASR Model)
|
<Ear className="w-4 h-4 mr-2 text-primary" /> 语音识别 (ASR Model)
|
||||||
</label>
|
</label>
|
||||||
<div className="relative group">
|
<div className="relative group">
|
||||||
<select
|
<select
|
||||||
@@ -1362,14 +1354,13 @@ export const AssistantsPage: React.FC = () => {
|
|||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<label className="text-sm font-medium text-white flex items-center">
|
<label className="text-sm font-medium text-white flex items-center">
|
||||||
<Mic className="w-4 h-4 mr-2 text-primary"/> 离线 ASR 中间结果
|
<Mic className="w-4 h-4 mr-2 text-primary" /> 离线 ASR 中间结果
|
||||||
</label>
|
</label>
|
||||||
<div className="inline-flex rounded-lg border border-white/10 bg-white/5 p-1">
|
<div className="inline-flex rounded-lg border border-white/10 bg-white/5 p-1">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => updateAssistant('asrInterimEnabled', false)}
|
onClick={() => updateAssistant('asrInterimEnabled', false)}
|
||||||
className={`px-3 py-1 text-xs rounded-md transition-colors ${
|
className={`px-3 py-1 text-xs rounded-md transition-colors ${selectedAssistant.asrInterimEnabled === true
|
||||||
selectedAssistant.asrInterimEnabled === true
|
|
||||||
? 'text-muted-foreground hover:text-foreground'
|
? 'text-muted-foreground hover:text-foreground'
|
||||||
: 'bg-primary text-primary-foreground shadow-sm'
|
: 'bg-primary text-primary-foreground shadow-sm'
|
||||||
}`}
|
}`}
|
||||||
@@ -1379,8 +1370,7 @@ export const AssistantsPage: React.FC = () => {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => updateAssistant('asrInterimEnabled', true)}
|
onClick={() => updateAssistant('asrInterimEnabled', true)}
|
||||||
className={`px-3 py-1 text-xs rounded-md transition-colors ${
|
className={`px-3 py-1 text-xs rounded-md transition-colors ${selectedAssistant.asrInterimEnabled === true
|
||||||
selectedAssistant.asrInterimEnabled === true
|
|
||||||
? 'bg-primary text-primary-foreground shadow-sm'
|
? 'bg-primary text-primary-foreground shadow-sm'
|
||||||
: 'text-muted-foreground hover:text-foreground'
|
: 'text-muted-foreground hover:text-foreground'
|
||||||
}`}
|
}`}
|
||||||
@@ -1397,14 +1387,13 @@ export const AssistantsPage: React.FC = () => {
|
|||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<label className="text-sm font-medium text-white flex items-center">
|
<label className="text-sm font-medium text-white flex items-center">
|
||||||
<Volume2 className="w-4 h-4 mr-2 text-primary"/> 语音输出
|
<Volume2 className="w-4 h-4 mr-2 text-primary" /> 语音输出
|
||||||
</label>
|
</label>
|
||||||
<div className="inline-flex rounded-lg border border-white/10 bg-white/5 p-1">
|
<div className="inline-flex rounded-lg border border-white/10 bg-white/5 p-1">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => updateAssistant('voiceOutputEnabled', false)}
|
onClick={() => updateAssistant('voiceOutputEnabled', false)}
|
||||||
className={`px-3 py-1 text-xs rounded-md transition-colors ${
|
className={`px-3 py-1 text-xs rounded-md transition-colors ${selectedAssistant.voiceOutputEnabled !== false
|
||||||
selectedAssistant.voiceOutputEnabled !== false
|
|
||||||
? 'text-muted-foreground hover:text-foreground'
|
? 'text-muted-foreground hover:text-foreground'
|
||||||
: 'bg-primary text-primary-foreground shadow-sm'
|
: 'bg-primary text-primary-foreground shadow-sm'
|
||||||
}`}
|
}`}
|
||||||
@@ -1414,8 +1403,7 @@ export const AssistantsPage: React.FC = () => {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => updateAssistant('voiceOutputEnabled', true)}
|
onClick={() => updateAssistant('voiceOutputEnabled', true)}
|
||||||
className={`px-3 py-1 text-xs rounded-md transition-colors ${
|
className={`px-3 py-1 text-xs rounded-md transition-colors ${selectedAssistant.voiceOutputEnabled !== false
|
||||||
selectedAssistant.voiceOutputEnabled !== false
|
|
||||||
? 'bg-primary text-primary-foreground shadow-sm'
|
? 'bg-primary text-primary-foreground shadow-sm'
|
||||||
: 'text-muted-foreground hover:text-foreground'
|
: 'text-muted-foreground hover:text-foreground'
|
||||||
}`}
|
}`}
|
||||||
@@ -1458,8 +1446,7 @@ export const AssistantsPage: React.FC = () => {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => updateAssistant('botCannotBeInterrupted', false)}
|
onClick={() => updateAssistant('botCannotBeInterrupted', false)}
|
||||||
className={`px-3 py-1 text-xs rounded-md transition-colors ${
|
className={`px-3 py-1 text-xs rounded-md transition-colors ${selectedAssistant.botCannotBeInterrupted === true
|
||||||
selectedAssistant.botCannotBeInterrupted === true
|
|
||||||
? 'text-muted-foreground hover:text-foreground'
|
? 'text-muted-foreground hover:text-foreground'
|
||||||
: 'bg-primary text-primary-foreground shadow-sm'
|
: 'bg-primary text-primary-foreground shadow-sm'
|
||||||
}`}
|
}`}
|
||||||
@@ -1469,8 +1456,7 @@ export const AssistantsPage: React.FC = () => {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => updateAssistant('botCannotBeInterrupted', true)}
|
onClick={() => updateAssistant('botCannotBeInterrupted', true)}
|
||||||
className={`px-3 py-1 text-xs rounded-md transition-colors ${
|
className={`px-3 py-1 text-xs rounded-md transition-colors ${selectedAssistant.botCannotBeInterrupted === true
|
||||||
selectedAssistant.botCannotBeInterrupted === true
|
|
||||||
? 'bg-primary text-primary-foreground shadow-sm'
|
? 'bg-primary text-primary-foreground shadow-sm'
|
||||||
: 'text-muted-foreground hover:text-foreground'
|
: 'text-muted-foreground hover:text-foreground'
|
||||||
}`}
|
}`}
|
||||||
@@ -1484,7 +1470,7 @@ export const AssistantsPage: React.FC = () => {
|
|||||||
<>
|
<>
|
||||||
<div className="flex justify-between items-center mb-1">
|
<div className="flex justify-between items-center mb-1">
|
||||||
<label className="text-sm font-medium flex items-center text-white">
|
<label className="text-sm font-medium flex items-center text-white">
|
||||||
<Timer className="w-4 h-4 mr-2 text-primary"/> 打断灵敏度
|
<Timer className="w-4 h-4 mr-2 text-primary" /> 打断灵敏度
|
||||||
</label>
|
</label>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
@@ -1523,7 +1509,7 @@ export const AssistantsPage: React.FC = () => {
|
|||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<label className="text-sm font-medium text-white flex items-center">
|
<label className="text-sm font-medium text-white flex items-center">
|
||||||
<Mic className="w-4 h-4 mr-2 text-primary"/> ASR 热词优化 (Hotwords)
|
<Mic className="w-4 h-4 mr-2 text-primary" /> ASR 热词优化 (Hotwords)
|
||||||
</label>
|
</label>
|
||||||
<div className="flex space-x-2">
|
<div className="flex space-x-2">
|
||||||
<Input
|
<Input
|
||||||
@@ -1890,7 +1876,7 @@ export const AssistantsPage: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Icon helper
|
// Icon helper
|
||||||
const BotIcon = ({className}: {className?: string}) => (
|
const BotIcon = ({ className }: { className?: string }) => (
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
|
||||||
<path d="M12 8V4H8" />
|
<path d="M12 8V4H8" />
|
||||||
<rect width="16" height="12" x="4" y="8" rx="2" />
|
<rect width="16" height="12" x="4" y="8" rx="2" />
|
||||||
@@ -4386,7 +4372,7 @@ export const DebugDrawer: React.FC<{
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Drawer isOpen={isOpen} onClose={() => { handleHangup(); onClose(); }} title={`调试: ${assistant.name}`} className="w-[90vw] sm:w-[85vw] max-w-none">
|
<Drawer isOpen={isOpen} onClose={() => { handleHangup(); onClose(); }} title={`调试: ${assistant.name}`} className="!w-[90vw] sm:!w-[85vw] !max-w-none">
|
||||||
<div className="relative flex h-full min-h-0 overflow-hidden gap-6">
|
<div className="relative flex h-full min-h-0 overflow-hidden gap-6">
|
||||||
|
|
||||||
{/* Left Column: Call Interface */}
|
{/* Left Column: Call Interface */}
|
||||||
@@ -4522,19 +4508,16 @@ export const DebugDrawer: React.FC<{
|
|||||||
</h3>
|
</h3>
|
||||||
<div className="flex items-center justify-center gap-2">
|
<div className="flex items-center justify-center gap-2">
|
||||||
<span className="relative flex h-2.5 w-2.5">
|
<span className="relative flex h-2.5 w-2.5">
|
||||||
<span className={`animate-ping absolute inline-flex h-full w-full rounded-full opacity-75 ${
|
<span className={`animate-ping absolute inline-flex h-full w-full rounded-full opacity-75 ${agentState === 'thinking' ? 'bg-yellow-400' :
|
||||||
agentState === 'thinking' ? 'bg-yellow-400' :
|
|
||||||
agentState === 'speaking' ? 'bg-green-400' :
|
agentState === 'speaking' ? 'bg-green-400' :
|
||||||
'bg-blue-400'
|
'bg-blue-400'
|
||||||
}`}></span>
|
}`}></span>
|
||||||
<span className={`relative inline-flex rounded-full h-2.5 w-2.5 ${
|
<span className={`relative inline-flex rounded-full h-2.5 w-2.5 ${agentState === 'thinking' ? 'bg-yellow-500' :
|
||||||
agentState === 'thinking' ? 'bg-yellow-500' :
|
|
||||||
agentState === 'speaking' ? 'bg-green-500' :
|
agentState === 'speaking' ? 'bg-green-500' :
|
||||||
'bg-blue-500'
|
'bg-blue-500'
|
||||||
}`}></span>
|
}`}></span>
|
||||||
</span>
|
</span>
|
||||||
<p className={`text-sm font-medium ${
|
<p className={`text-sm font-medium ${agentState === 'thinking' ? 'text-yellow-400' :
|
||||||
agentState === 'thinking' ? 'text-yellow-400' :
|
|
||||||
agentState === 'speaking' ? 'text-green-400' :
|
agentState === 'speaking' ? 'text-green-400' :
|
||||||
'text-blue-400'
|
'text-blue-400'
|
||||||
}`}>
|
}`}>
|
||||||
@@ -4728,13 +4711,11 @@ export const DebugDrawer: React.FC<{
|
|||||||
isOpen={settingsDrawerOpen}
|
isOpen={settingsDrawerOpen}
|
||||||
onClose={() => setSettingsDrawerOpen(false)}
|
onClose={() => setSettingsDrawerOpen(false)}
|
||||||
title="调试设置"
|
title="调试设置"
|
||||||
contentClassName="max-w-[90vw] md:max-w-[70vw] lg:max-w-2xl h-[85vh] flex flex-col"
|
contentClassName="max-w-[90vw] md:max-w-[70vw] lg:max-w-2xl"
|
||||||
>
|
>
|
||||||
<div className="flex-1 min-h-0 overflow-y-auto">
|
|
||||||
{settingsPanel}
|
{settingsPanel}
|
||||||
</div>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { defineConfig, loadEnv } from 'vite';
|
import { defineConfig, loadEnv } from 'vite';
|
||||||
import react from '@vitejs/plugin-react';
|
import react from '@vitejs/plugin-react';
|
||||||
|
import tailwindcss from '@tailwindcss/vite';
|
||||||
|
|
||||||
export default defineConfig(({ mode }) => {
|
export default defineConfig(({ mode }) => {
|
||||||
const env = loadEnv(mode, '.', '');
|
const env = loadEnv(mode, '.', '');
|
||||||
@@ -9,7 +10,7 @@ export default defineConfig(({ mode }) => {
|
|||||||
port: 3000,
|
port: 3000,
|
||||||
host: '0.0.0.0',
|
host: '0.0.0.0',
|
||||||
},
|
},
|
||||||
plugins: [react()],
|
plugins: [react(), tailwindcss()],
|
||||||
define: {
|
define: {
|
||||||
'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY),
|
'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY),
|
||||||
'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY)
|
'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY)
|
||||||
|
|||||||
Reference in New Issue
Block a user