commit 101b3dd729e6ddaf2f5ecff2f5549edf8bb95262 Author: Rami Bitar Date: Sun Apr 12 00:22:26 2026 -0400 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5ef6a52 --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/README.md b/README.md new file mode 100644 index 0000000..e215bc4 --- /dev/null +++ b/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/app/favicon.ico b/app/favicon.ico new file mode 100644 index 0000000..718d6fe Binary files /dev/null and b/app/favicon.ico differ diff --git a/app/globals.css b/app/globals.css new file mode 100644 index 0000000..6d44e19 --- /dev/null +++ b/app/globals.css @@ -0,0 +1,129 @@ +@import "tailwindcss"; +@import "tw-animate-css"; +@import "shadcn/tailwind.css"; + +@custom-variant dark (&:is(.dark *)); + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --font-sans: var(--font-geist-sans); + --font-mono: var(--font-geist-mono); + --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); + --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); +} + +: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.577 0.245 27.325); + --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 { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 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); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } + html { + @apply font-sans; + } +} \ No newline at end of file diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..f7fa87e --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,34 @@ +import type { Metadata } from "next"; +import { Geist, Geist_Mono } from "next/font/google"; +import "./globals.css"; + +const geistSans = Geist({ + variable: "--font-geist-sans", + subsets: ["latin"], +}); + +const geistMono = Geist_Mono({ + variable: "--font-geist-mono", + subsets: ["latin"], +}); + +export const metadata: Metadata = { + title: "Create Next App", + description: "Generated by create next app", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + {children} + + + ); +} diff --git a/app/page.tsx b/app/page.tsx new file mode 100644 index 0000000..295f8fd --- /dev/null +++ b/app/page.tsx @@ -0,0 +1,65 @@ +import Image from "next/image"; + +export default function Home() { + return ( +
+
+ Next.js logo +
+

+ To get started, edit the page.tsx file. +

+

+ Looking for a starting point or more instructions? Head over to{" "} + + Templates + {" "} + or the{" "} + + Learning + {" "} + center. +

+
+
+ + Vercel logomark + Deploy Now + + + Documentation + +
+
+
+ ); +} diff --git a/components.json b/components.json new file mode 100644 index 0000000..02e61e0 --- /dev/null +++ b/components.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "radix-nova", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "app/globals.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": {} +} diff --git a/components/ai-elements/agent.tsx b/components/ai-elements/agent.tsx new file mode 100644 index 0000000..86dbcc0 --- /dev/null +++ b/components/ai-elements/agent.tsx @@ -0,0 +1,141 @@ +"use client"; + +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion"; +import { Badge } from "@/components/ui/badge"; +import { cn } from "@/lib/utils"; +import type { Tool } from "ai"; +import { BotIcon } from "lucide-react"; +import type { ComponentProps } from "react"; +import { memo } from "react"; + +import { CodeBlock } from "./code-block"; + +export type AgentProps = ComponentProps<"div">; + +export const Agent = memo(({ className, ...props }: AgentProps) => ( +
+)); + +export type AgentHeaderProps = ComponentProps<"div"> & { + name: string; + model?: string; +}; + +export const AgentHeader = memo( + ({ className, name, model, ...props }: AgentHeaderProps) => ( +
+
+ + {name} + {model && ( + + {model} + + )} +
+
+ ) +); + +export type AgentContentProps = ComponentProps<"div">; + +export const AgentContent = memo( + ({ className, ...props }: AgentContentProps) => ( +
+ ) +); + +export type AgentInstructionsProps = ComponentProps<"div"> & { + children: string; +}; + +export const AgentInstructions = memo( + ({ className, children, ...props }: AgentInstructionsProps) => ( +
+ + Instructions + +
+

{children}

+
+
+ ) +); + +export type AgentToolsProps = ComponentProps; + +export const AgentTools = memo(({ className, ...props }: AgentToolsProps) => ( +
+ Tools + +
+)); + +export type AgentToolProps = ComponentProps & { + tool: Tool; +}; + +export const AgentTool = memo( + ({ className, tool, value, ...props }: AgentToolProps) => { + const schema = + "jsonSchema" in tool && tool.jsonSchema + ? tool.jsonSchema + : tool.inputSchema; + + return ( + + + {tool.description ?? "No description"} + + +
+ +
+
+
+ ); + } +); + +export type AgentOutputProps = ComponentProps<"div"> & { + schema: string; +}; + +export const AgentOutput = memo( + ({ className, schema, ...props }: AgentOutputProps) => ( +
+ + Output Schema + +
+ +
+
+ ) +); + +Agent.displayName = "Agent"; +AgentHeader.displayName = "AgentHeader"; +AgentContent.displayName = "AgentContent"; +AgentInstructions.displayName = "AgentInstructions"; +AgentTools.displayName = "AgentTools"; +AgentTool.displayName = "AgentTool"; +AgentOutput.displayName = "AgentOutput"; diff --git a/components/ai-elements/artifact.tsx b/components/ai-elements/artifact.tsx new file mode 100644 index 0000000..94626e5 --- /dev/null +++ b/components/ai-elements/artifact.tsx @@ -0,0 +1,148 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { cn } from "@/lib/utils"; +import type { LucideIcon } from "lucide-react"; +import { XIcon } from "lucide-react"; +import type { ComponentProps, HTMLAttributes } from "react"; + +export type ArtifactProps = HTMLAttributes; + +export const Artifact = ({ className, ...props }: ArtifactProps) => ( +
+); + +export type ArtifactHeaderProps = HTMLAttributes; + +export const ArtifactHeader = ({ + className, + ...props +}: ArtifactHeaderProps) => ( +
+); + +export type ArtifactCloseProps = ComponentProps; + +export const ArtifactClose = ({ + className, + children, + size = "sm", + variant = "ghost", + ...props +}: ArtifactCloseProps) => ( + +); + +export type ArtifactTitleProps = HTMLAttributes; + +export const ArtifactTitle = ({ className, ...props }: ArtifactTitleProps) => ( +

+); + +export type ArtifactDescriptionProps = HTMLAttributes; + +export const ArtifactDescription = ({ + className, + ...props +}: ArtifactDescriptionProps) => ( +

+); + +export type ArtifactActionsProps = HTMLAttributes; + +export const ArtifactActions = ({ + className, + ...props +}: ArtifactActionsProps) => ( +

+); + +export type ArtifactActionProps = ComponentProps & { + tooltip?: string; + label?: string; + icon?: LucideIcon; +}; + +export const ArtifactAction = ({ + tooltip, + label, + icon: Icon, + children, + className, + size = "sm", + variant = "ghost", + ...props +}: ArtifactActionProps) => { + const button = ( + + ); + + if (tooltip) { + return ( + + + {button} + +

{tooltip}

+
+
+
+ ); + } + + return button; +}; + +export type ArtifactContentProps = HTMLAttributes; + +export const ArtifactContent = ({ + className, + ...props +}: ArtifactContentProps) => ( +
+); diff --git a/components/ai-elements/attachments.tsx b/components/ai-elements/attachments.tsx new file mode 100644 index 0000000..d4e4e34 --- /dev/null +++ b/components/ai-elements/attachments.tsx @@ -0,0 +1,426 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { + HoverCard, + HoverCardContent, + HoverCardTrigger, +} from "@/components/ui/hover-card"; +import { cn } from "@/lib/utils"; +import type { FileUIPart, SourceDocumentUIPart } from "ai"; +import { + FileTextIcon, + GlobeIcon, + ImageIcon, + Music2Icon, + PaperclipIcon, + VideoIcon, + XIcon, +} from "lucide-react"; +import type { ComponentProps, HTMLAttributes, ReactNode } from "react"; +import { createContext, useCallback, useContext, useMemo } from "react"; + +// ============================================================================ +// Types +// ============================================================================ + +export type AttachmentData = + | (FileUIPart & { id: string }) + | (SourceDocumentUIPart & { id: string }); + +export type AttachmentMediaCategory = + | "image" + | "video" + | "audio" + | "document" + | "source" + | "unknown"; + +export type AttachmentVariant = "grid" | "inline" | "list"; + +const mediaCategoryIcons: Record = { + audio: Music2Icon, + document: FileTextIcon, + image: ImageIcon, + source: GlobeIcon, + unknown: PaperclipIcon, + video: VideoIcon, +}; + +// ============================================================================ +// Utility Functions +// ============================================================================ + +export const getMediaCategory = ( + data: AttachmentData +): AttachmentMediaCategory => { + if (data.type === "source-document") { + return "source"; + } + + const mediaType = data.mediaType ?? ""; + + if (mediaType.startsWith("image/")) { + return "image"; + } + if (mediaType.startsWith("video/")) { + return "video"; + } + if (mediaType.startsWith("audio/")) { + return "audio"; + } + if (mediaType.startsWith("application/") || mediaType.startsWith("text/")) { + return "document"; + } + + return "unknown"; +}; + +export const getAttachmentLabel = (data: AttachmentData): string => { + if (data.type === "source-document") { + return data.title || data.filename || "Source"; + } + + const category = getMediaCategory(data); + return data.filename || (category === "image" ? "Image" : "Attachment"); +}; + +const renderAttachmentImage = ( + url: string, + filename: string | undefined, + isGrid: boolean +) => + isGrid ? ( + {filename + ) : ( + {filename + ); + +// ============================================================================ +// Contexts +// ============================================================================ + +interface AttachmentsContextValue { + variant: AttachmentVariant; +} + +const AttachmentsContext = createContext(null); + +interface AttachmentContextValue { + data: AttachmentData; + mediaCategory: AttachmentMediaCategory; + onRemove?: () => void; + variant: AttachmentVariant; +} + +const AttachmentContext = createContext(null); + +// ============================================================================ +// Hooks +// ============================================================================ + +export const useAttachmentsContext = () => + useContext(AttachmentsContext) ?? { variant: "grid" as const }; + +export const useAttachmentContext = () => { + const ctx = useContext(AttachmentContext); + if (!ctx) { + throw new Error("Attachment components must be used within "); + } + return ctx; +}; + +// ============================================================================ +// Attachments - Container +// ============================================================================ + +export type AttachmentsProps = HTMLAttributes & { + variant?: AttachmentVariant; +}; + +export const Attachments = ({ + variant = "grid", + className, + children, + ...props +}: AttachmentsProps) => { + const contextValue = useMemo(() => ({ variant }), [variant]); + + return ( + +
+ {children} +
+
+ ); +}; + +// ============================================================================ +// Attachment - Item +// ============================================================================ + +export type AttachmentProps = HTMLAttributes & { + data: AttachmentData; + onRemove?: () => void; +}; + +export const Attachment = ({ + data, + onRemove, + className, + children, + ...props +}: AttachmentProps) => { + const { variant } = useAttachmentsContext(); + const mediaCategory = getMediaCategory(data); + + const contextValue = useMemo( + () => ({ data, mediaCategory, onRemove, variant }), + [data, mediaCategory, onRemove, variant] + ); + + return ( + +
+ {children} +
+
+ ); +}; + +// ============================================================================ +// AttachmentPreview - Media preview +// ============================================================================ + +export type AttachmentPreviewProps = HTMLAttributes & { + fallbackIcon?: ReactNode; +}; + +export const AttachmentPreview = ({ + fallbackIcon, + className, + ...props +}: AttachmentPreviewProps) => { + const { data, mediaCategory, variant } = useAttachmentContext(); + + const iconSize = variant === "inline" ? "size-3" : "size-4"; + + const renderIcon = (Icon: typeof ImageIcon) => ( + + ); + + const renderContent = () => { + if (mediaCategory === "image" && data.type === "file" && data.url) { + return renderAttachmentImage(data.url, data.filename, variant === "grid"); + } + + if (mediaCategory === "video" && data.type === "file" && data.url) { + return