Mega UI revamp

This commit is contained in:
akhisud3195 2025-03-27 18:52:17 +05:30 committed by Ramnique Singh
parent 650f481a96
commit bcb686a20d
94 changed files with 6984 additions and 3889 deletions

View file

@ -0,0 +1,145 @@
'use client';
import { Button, Spinner } from "@heroui/react";
import { useRef, useState, useEffect } from "react";
import { Textarea } from "@/components/ui/textarea";
// Add a type to support both message formats
type FlexibleMessage = {
role: 'user' | 'assistant' | 'system' | 'tool';
content: string | any;
version?: string;
chatId?: string;
createdAt?: string;
// Add any other optional fields that might be needed
};
export function ComposeBox({
minRows=3,
disabled=false,
loading=false,
handleUserMessage,
messages,
}: {
minRows?: number;
disabled?: boolean;
loading?: boolean;
handleUserMessage: (prompt: string) => void;
messages: FlexibleMessage[]; // Use the flexible message type
}) {
const [input, setInput] = useState('');
const [isFocused, setIsFocused] = useState(false);
const inputRef = useRef<HTMLTextAreaElement>(null);
function handleInput() {
const prompt = input.trim();
if (!prompt) {
return;
}
setInput('');
handleUserMessage(prompt);
}
function handleInputKeyDown(event: React.KeyboardEvent<HTMLTextAreaElement>) {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
handleInput();
}
}
// focus on the input field
useEffect(() => {
inputRef.current?.focus();
}, [messages]);
return (
<div className="relative group">
{/* Keyboard shortcut hint */}
<div className="absolute -top-6 right-0 text-xs text-gray-500 dark:text-gray-400 opacity-0
group-hover:opacity-100 transition-opacity">
Press + Enter to send
</div>
{/* Outer container with padding */}
<div className="rounded-2xl border-[1.5px] border-gray-200 dark:border-[#2a2d31] p-3 relative
bg-white dark:bg-[#1e2023] flex items-end gap-2">
{/* Textarea */}
<div className="flex-1">
<Textarea
ref={inputRef}
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleInputKeyDown}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
disabled={disabled || loading}
placeholder="Type a message..."
autoResize={true}
maxHeight={120}
className={`
!min-h-0
!border-0 !shadow-none !ring-0
bg-transparent
resize-none
overflow-y-auto
[&::-webkit-scrollbar]:w-1
[&::-webkit-scrollbar-track]:bg-transparent
[&::-webkit-scrollbar-thumb]:bg-gray-300
[&::-webkit-scrollbar-thumb]:dark:bg-[#2a2d31]
[&::-webkit-scrollbar-thumb]:rounded-full
placeholder:text-gray-500 dark:placeholder:text-gray-400
`}
/>
</div>
{/* Send button */}
<Button
size="sm"
isIconOnly
disabled={disabled || loading || !input.trim()}
onPress={handleInput}
className={`
transition-all duration-200
${input.trim()
? 'bg-indigo-50 hover:bg-indigo-100 text-indigo-700 dark:bg-indigo-900/50 dark:hover:bg-indigo-800/60 dark:text-indigo-300'
: 'bg-gray-100 dark:bg-gray-800 text-gray-400 dark:text-gray-500'
}
scale-100 hover:scale-105 active:scale-95
disabled:opacity-50 disabled:scale-95
hover:shadow-md dark:hover:shadow-indigo-950/10
mb-0.5
`}
>
{loading ? (
<Spinner size="sm" color={input.trim() ? "primary" : "default"} />
) : (
<SendIcon
size={16}
className={`transform transition-transform ${isFocused ? 'translate-x-0.5' : ''}`}
/>
)}
</Button>
</div>
</div>
);
}
// Custom SendIcon component for better visual alignment
function SendIcon({ size, className }: { size: number, className?: string }) {
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
<path d="M22 2L11 13" />
<path d="M22 2L15 22L11 13L2 9L22 2Z" />
</svg>
);
}

View file

@ -0,0 +1,11 @@
import { CopyButton } from "@/components/common/copy-button";
export function CopyAsJsonButton({ onCopy }: { onCopy: () => void }) {
return <div className="absolute top-0 right-0">
<CopyButton
onCopy={onCopy}
label="Copy as JSON"
successLabel="Copied"
/>
</div>
}

View file

@ -0,0 +1,41 @@
'use client';
import { Button } from "@/components/ui/button";
import { CopyIcon, CheckIcon } from "lucide-react";
import { useState } from "react";
export function CopyButton({
onCopy,
label,
successLabel,
}: {
onCopy: () => void;
label: string;
successLabel: string;
}) {
const [showCopySuccess, setShowCopySuccess] = useState(false);
const handleCopy = () => {
onCopy();
setShowCopySuccess(true);
setTimeout(() => {
setShowCopySuccess(false);
}, 500);
}
return (
<Button
variant="secondary"
size="sm"
onClick={handleCopy}
className="gap-2"
showHoverContent
hoverContent={showCopySuccess ? successLabel : label}
>
{showCopySuccess ? (
<CheckIcon className="h-4 w-4" />
) : (
<CopyIcon className="h-4 w-4" />
)}
</Button>
);
}

View file

@ -0,0 +1,93 @@
import clsx from "clsx";
export function ActionButton({
icon = null,
children,
onClick = undefined,
disabled = false,
primary = false,
}: {
icon?: React.ReactNode;
children: React.ReactNode;
onClick?: () => void | undefined;
disabled?: boolean;
primary?: boolean;
}) {
const onClickProp = onClick ? { onClick } : {};
return <button
disabled={disabled}
className={clsx("rounded-md text-xs flex items-center gap-1 disabled:text-gray-300 dark:disabled:text-gray-600 hover:text-gray-600 dark:hover:text-gray-300", {
"text-blue-600 dark:text-blue-400": primary,
"text-gray-400 dark:text-gray-500": !primary,
})}
{...onClickProp}
>
{icon}
{children}
</button>;
}
interface PanelProps {
title: React.ReactNode;
rightActions?: React.ReactNode;
actions?: React.ReactNode;
children: React.ReactNode;
maxHeight?: string;
variant?: 'default' | 'copilot' | 'projects';
}
export function Panel({
title,
rightActions,
actions,
children,
maxHeight,
variant = 'default',
}: PanelProps) {
return <div className={clsx(
"flex flex-col overflow-hidden rounded-xl border",
"border-zinc-200 dark:border-zinc-800",
"bg-white dark:bg-zinc-900",
maxHeight ? "max-h-[var(--panel-height)]" : "h-full"
)}
style={{ '--panel-height': maxHeight } as React.CSSProperties}
>
<div className={clsx(
"shrink-0 border-b border-zinc-100 dark:border-zinc-800",
variant === 'projects' ? "flex flex-col gap-3 px-4 py-3" : "flex items-center justify-between px-4 py-3"
)}>
{variant === 'projects' ? (
<>
<div className="text-sm uppercase tracking-wide text-zinc-500 dark:text-zinc-400">
{title}
</div>
{actions && <div className="flex items-center gap-2">
{actions}
</div>}
</>
) : variant === 'copilot' ? (
<>
<div className="flex items-center gap-2">
{title}
</div>
{rightActions}
</>
) : (
<>
{title}
{rightActions}
</>
)}
</div>
<div className={clsx(
"min-h-0 flex-1 overflow-y-auto",
variant === 'projects' && "custom-scrollbar"
)}>
{variant === 'projects' ? (
<div className="px-3 py-2 pb-4">
{children}
</div>
) : children}
</div>
</div>;
}

View file

@ -0,0 +1,77 @@
import clsx from 'clsx';
import { ButtonHTMLAttributes, forwardRef } from "react";
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'tertiary';
size?: 'sm' | 'md' | 'lg';
startContent?: React.ReactNode;
endContent?: React.ReactNode;
isLoading?: boolean;
hoverContent?: React.ReactNode;
showHoverContent?: boolean;
}
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(({
className,
variant = 'primary',
size = 'md',
startContent,
endContent,
isLoading,
children,
disabled,
hoverContent,
showHoverContent = false,
...props
}, ref) => {
return (
<button
ref={ref}
disabled={isLoading || disabled}
className={clsx(
"inline-flex items-center justify-center rounded-full font-medium transition-all",
"focus-visible:outline-none transform hover:scale-[1.02] hover:shadow-md",
"disabled:pointer-events-none disabled:opacity-50",
{
'primary': "text-sm px-3 py-1.5 bg-indigo-50 hover:bg-indigo-100 text-indigo-700 dark:bg-indigo-950 dark:hover:bg-indigo-900 dark:text-indigo-400",
'secondary': "bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-100",
'tertiary': "bg-transparent hover:bg-gray-100 text-gray-700 dark:hover:bg-gray-800 dark:text-gray-300",
}[variant],
{
'sm': "min-h-[2rem] px-3 text-sm py-1",
'md': "min-h-[2.5rem] px-4 py-1",
'lg': "min-h-[3rem] px-4 py-2 text-sm",
}[size],
"group",
className
)}
{...props}
>
{isLoading && (
<svg className="animate-spin -ml-1 mr-2 h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
)}
{startContent && (
<span className={clsx(
"shrink-0",
children || hoverContent ? "mr-2" : ""
)}>
{startContent}
</span>
)}
<span className="truncate">
{showHoverContent ? (
<>
<span className="group-hover:hidden">{children}</span>
<span className="hidden group-hover:inline">{hoverContent}</span>
</>
) : children}
</span>
{endContent && <span className="ml-2 shrink-0">{endContent}</span>}
</button>
);
});
Button.displayName = "Button";

View file

@ -0,0 +1,49 @@
import { Select, SelectItem, SelectProps } from "@heroui/react";
import { ReactNode, ChangeEvent } from "react";
export interface DropdownOption {
key: string;
label: string;
startContent?: ReactNode;
endContent?: ReactNode;
}
interface DropdownProps extends Omit<SelectProps, 'children' | 'onChange'> {
options: DropdownOption[];
value: string;
onChange: (value: string) => void;
className?: string;
width?: string | number;
containerClassName?: string;
}
export function Dropdown({
options,
value,
onChange,
className = "",
width = "100%",
containerClassName = "",
...selectProps
}: DropdownProps) {
return (
<div className={`${containerClassName}`} style={{ width }}>
<Select
{...selectProps}
selectedKeys={[value]}
onChange={(e: ChangeEvent<HTMLSelectElement>) => onChange(e.target.value)}
className={`${className}`}
>
{options.map((option) => (
<SelectItem
key={option.key}
startContent={option.startContent}
endContent={option.endContent}
>
{option.label}
</SelectItem>
))}
</Select>
</div>
);
}

View file

@ -0,0 +1,14 @@
import clsx from 'clsx';
interface HorizontalDividerProps {
className?: string;
}
export function HorizontalDivider({ className }: HorizontalDividerProps) {
return (
<div className={clsx(
"border-t border-gray-200 dark:border-gray-700",
className
)} />
);
}

View file

@ -0,0 +1,42 @@
import { clsx } from "clsx";
import { InputHTMLAttributes, forwardRef } from "react";
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
error?: string;
label?: string;
}
export const Input = forwardRef<HTMLInputElement, InputProps>(({
className,
error,
label,
...props
}, ref) => {
return (
<div className="space-y-2">
{label && (
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
{label}
</label>
)}
<input
ref={ref}
className={clsx(
"flex h-10 w-full rounded-md border border-gray-300 bg-white px-3 py-2",
"text-sm placeholder:text-gray-400",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400",
"disabled:cursor-not-allowed disabled:opacity-50",
"dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100",
error && "border-red-500 focus-visible:ring-red-500",
className
)}
{...props}
/>
{error && (
<p className="text-sm text-red-500">{error}</p>
)}
</div>
);
});
Input.displayName = "Input";

View file

@ -0,0 +1,32 @@
import clsx from 'clsx';
import { tokens } from "@/app/styles/design-tokens";
interface PageHeadingProps {
title: string;
description?: string;
}
export function PageHeading({ title, description }: PageHeadingProps) {
return (
<div>
<h1 className={clsx(
tokens.typography.weights.semibold,
tokens.typography.sizes["2xl"],
tokens.colors.light.text.primary,
tokens.colors.dark.text.primary
)}>
{title}
</h1>
{description && (
<p className={clsx(
"mt-2",
tokens.typography.sizes.base,
tokens.colors.light.text.secondary,
tokens.colors.dark.text.secondary
)}>
{description}
</p>
)}
</div>
);
}

View file

@ -3,14 +3,14 @@
import { GripVertical } from "lucide-react"
import * as ResizablePrimitive from "react-resizable-panels"
import { cn } from "../../lib/utils"
import clsx from 'clsx';
const ResizablePanelGroup = ({
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => (
<ResizablePrimitive.PanelGroup
className={cn(
className={clsx(
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
className
)}
@ -28,7 +28,7 @@ const ResizableHandle = ({
withHandle?: boolean
}) => (
<ResizablePrimitive.PanelResizeHandle
className={cn(
className={clsx(
"relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
className
)}

View file

@ -0,0 +1,44 @@
'use client';
import { Input } from "@/components/ui/input";
import { SearchIcon, XIcon } from "lucide-react";
import { InputHTMLAttributes } from "react";
import clsx from 'clsx';
interface SearchBarProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'onChange'> {
value: string;
onChange: (value: string) => void;
onClear?: () => void;
}
export function SearchBar({
value,
onChange,
onClear,
className,
...props
}: SearchBarProps) {
return (
<div className="relative">
<SearchIcon
size={16}
className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400"
/>
<Input
type="text"
value={value}
onChange={(e) => onChange(e.target.value)}
className={clsx("pl-9 pr-8 bg-transparent", className)}
{...props}
/>
{value && (
<button
type="button"
onClick={onClear}
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<XIcon size={14} />
</button>
)}
</div>
);
}

View file

@ -0,0 +1,32 @@
import clsx from 'clsx';
import { tokens } from "@/app/styles/design-tokens";
interface SectionHeadingProps {
children: React.ReactNode;
subheading?: React.ReactNode;
}
export function SectionHeading({ children, subheading }: SectionHeadingProps) {
return (
<div className="space-y-1">
<div className={clsx(
tokens.typography.weights.medium,
tokens.typography.sizes.lg,
tokens.colors.light.text.primary,
tokens.colors.dark.text.primary
)}>
{children}
</div>
{subheading && (
<p className={clsx(
tokens.typography.sizes.sm,
tokens.typography.weights.normal,
tokens.colors.light.text.secondary,
tokens.colors.dark.text.secondary
)}>
{subheading}
</p>
)}
</div>
);
}

View file

@ -0,0 +1,151 @@
import clsx from 'clsx';
import { TextareaHTMLAttributes, forwardRef, useEffect, useRef, useState } from "react";
interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {
label?: string;
autoResize?: boolean;
maxHeight?: number;
useValidation?: boolean;
validate?: (value: string) => { valid: boolean; errorMessage?: string };
onValidatedChange?: (value: string) => void;
updateOnBlur?: boolean;
}
export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(({
className,
label,
autoResize = false,
maxHeight = 120, // default max height (roughly 5 lines)
value: propValue,
onChange,
// New validation props
useValidation = false,
validate,
onValidatedChange,
updateOnBlur = false,
onBlur,
onKeyDown,
...props
}, ref) => {
const internalRef = useRef<HTMLTextAreaElement>(null);
const textareaRef = (ref as any) || internalRef;
// Local state for validation mode
const [localValue, setLocalValue] = useState(propValue as string);
const [validationError, setValidationError] = useState<string | undefined>();
const [isEditing, setIsEditing] = useState(false);
// Sync local state with prop value when not editing
useEffect(() => {
if (!isEditing) {
setLocalValue(propValue as string);
}
}, [propValue, isEditing]);
useEffect(() => {
if (!autoResize) return;
const textarea = textareaRef.current;
if (!textarea) return;
const adjustHeight = () => {
textarea.style.height = 'auto';
const scrollHeight = textarea.scrollHeight;
textarea.style.height = `${Math.min(scrollHeight, maxHeight)}px`;
// Add scrolling if content exceeds maxHeight
textarea.style.overflowY = scrollHeight > maxHeight ? 'auto' : 'hidden';
};
adjustHeight();
// Add window resize listener
window.addEventListener('resize', adjustHeight);
return () => window.removeEventListener('resize', adjustHeight);
}, [localValue, autoResize, maxHeight, textareaRef]);
const validateAndUpdate = (value: string) => {
if (validate) {
const result = validate(value);
setValidationError(result.errorMessage);
if (result.valid && onValidatedChange) {
onValidatedChange(value);
return true;
}
return false;
} else if (onValidatedChange) {
onValidatedChange(value);
return true;
}
return false;
};
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const newValue = e.target.value;
setLocalValue(newValue);
setIsEditing(true);
if (!updateOnBlur) {
if (useValidation) {
validateAndUpdate(newValue);
} else {
onChange?.(e);
}
}
};
const handleBlur = (e: React.FocusEvent<HTMLTextAreaElement>) => {
setIsEditing(false);
if (updateOnBlur) {
if (useValidation) {
validateAndUpdate(localValue);
} else {
const syntheticEvent = {
...e,
target: { ...e.target, value: localValue },
currentTarget: { ...e.currentTarget, value: localValue }
};
onChange?.(syntheticEvent as any);
}
}
onBlur?.(e);
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (updateOnBlur && e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
textareaRef.current?.blur();
}
onKeyDown?.(e);
};
return (
<div className="space-y-2">
{label && (
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
{label}
</label>
)}
<textarea
ref={textareaRef}
onChange={handleChange}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
value={localValue}
className={clsx(
"flex w-full text-sm focus-visible:outline-none",
"disabled:cursor-not-allowed disabled:opacity-50",
"transition-colors",
className
)}
style={{
...props.style,
minHeight: autoResize ? '24px' : undefined,
}}
{...props}
/>
</div>
);
});
Textarea.displayName = "Textarea";