diff --git a/.cursorrules b/.cursorrules
new file mode 100644
index 0000000..908e1c8
--- /dev/null
+++ b/.cursorrules
@@ -0,0 +1 @@
+To extend the provided rules to include usage of the `ai-sdk-rsc` library and integrate it with Vercel middleware and a KV database, here's an updated set of instructions tailored for use with Cursor IDE. These instructions are designed to help you effectively implement generative user interfaces using React Server Components (RSC) with the AI SDK.### Extended Rules for AI SDK RSC Integration with Vercel Middleware and KV Database**Environment and Tools**- You are an expert in TypeScript, Node.js, Next.js App Router, React, Shadcn UI, Radix UI, Tailwind, and Vercel middleware.- You are familiar with Vercel's KV database for managing stateful data.**Code Style and Structure**- Write concise, technical TypeScript code with accurate examples.- Use functional and declarative programming patterns; avoid classes.- Prefer iteration and modularization over code duplication.- Use descriptive variable names with auxiliary verbs (e.g., `isLoading`, `hasError`).- Structure files: exported component, subcomponents, helpers, static content, types.**Naming Conventions**- Use lowercase with dashes for directories (e.g., `components/auth-wizard`).- Favor named exports for components.**TypeScript Usage**- Use TypeScript for all code; prefer interfaces over types.- Avoid enums; use maps instead.- Use functional components with TypeScript interfaces.**Syntax and Formatting**- Use the `function` keyword for pure functions.- Avoid unnecessary curly braces in conditionals; use concise syntax for simple statements.- Use declarative JSX.**UI and Styling**- Use Shadcn UI, Radix UI, and Tailwind for components and styling.- Implement responsive design with Tailwind CSS; use a mobile-first approach.**Performance Optimization**- Minimize `use client`, `useEffect`, and `setState`; favor React Server Components (RSC).- Wrap client components in `Suspense` with fallback.- Use dynamic loading for non-critical components.- Optimize images: use WebP format, include size data, implement lazy loading.**Key Conventions**- Use `nuqs` for URL search parameter state management.- Optimize Web Vitals (LCP, CLS, FID).- Limit `use client`: - Favor server components and Next.js SSR. - Use only for Web API access in small components. - Avoid for data fetching or state management.- Follow Next.js docs for Data Fetching, Rendering, and Routing.**AI SDK RSC Integration**- **Setup and Installation**: Integrate `ai-sdk-rsc` into your Next.js project. - Install the library using `npm install ai-sdk-rsc` or `yarn add ai-sdk-rsc`. - Configure middleware in `middleware.ts` to manage requests and sessions using Vercel's KV database. - **Middleware Implementation**: Use Vercel middleware to handle incoming requests. - Create a middleware file in the `middleware` directory (e.g., `middleware/ai-middleware.ts`). - Use middleware to parse user input and manage sessions with the KV database. - Example: ```typescript import { NextRequest, NextResponse } from 'next/server'; import { kv } from '@vercel/kv'; export async function middleware(req: NextRequest) { const sessionId = req.cookies.get('session-id'); if (!sessionId) { const newSessionId = generateSessionId(); await kv.set(newSessionId, { state: {} }); // Initialize state in KV database const res = NextResponse.next(); res.cookies.set('session-id', newSessionId); return res; } // Fetch state from KV database const state = await kv.get(sessionId); req.nextUrl.searchParams.set('state', JSON.stringify(state)); return NextResponse.next(); } function generateSessionId() { return Math.random().toString(36).substring(2); } ```- **React Server Components (RSC) and AI SDK**: - Use `ai-sdk-rsc` hooks to manage state and stream generative content. - Example usage of AI SDK hooks in a React Server Component: ```typescript import { useAIStream } from 'ai-sdk-rsc'; import { FC } from 'react'; interface ChatProps { initialMessage: string; } const Chat: FC = ({ initialMessage }) => { const { messages, sendMessage } = useAIStream({ initialMessage, onMessage: (message) => console.log('New message:', message), }); return ( {msg.content} export default Chat; ```- **KV Database Integration**: - Use Vercel's KV database to store and retrieve session data. - Utilize `kv.set`, `kv.get`, and `kv.delete` to manage data. - Ensure the database operations are asynchronous to avoid blocking server-side rendering (SSR).- **Data Fetching and State Management**: - Use Next.js data fetching methods (`getServerSideProps`, `getStaticProps`) to manage server-side state. - Avoid client-side data fetching methods (`useEffect`, `fetch`) except for critical, non-blocking operations.- **Deployment Considerations**: - Ensure all environment variables (e.g., API keys, database credentials) are securely stored in Vercel's environment settings. - Configure Vercel's KV and other serverless functions correctly to handle scalability and performance needs.By following these extended rules, you'll be able to create a well-optimized, scalable, and efficient Next.js application that leverages `ai-sdk-rsc`, Vercel middleware, and KV database for building sophisticated AI-driven interfaces.
\ No newline at end of file
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..1e48203
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,10 @@
+# editorconfig.org
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+indent_size = 2
+indent_style = space
+insert_final_newline = true
+trim_trailing_whitespace = true
\ No newline at end of file
diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100644
index 0000000..ba93ef2
--- /dev/null
+++ b/.eslintrc.json
@@ -0,0 +1,32 @@
+{
+ "$schema": "https://json.schemastore.org/eslintrc",
+ "root": true,
+ "extends": [
+ "next/core-web-vitals",
+ "turbo",
+ "prettier",
+ "plugin:tailwindcss/recommended"
+ ],
+ "plugins": ["tailwindcss"],
+ "ignorePatterns": ["**/fixtures/**"],
+ "rules": {
+ "@next/next/no-html-link-for-pages": "off",
+ "tailwindcss/no-custom-classname": "off",
+ "tailwindcss/classnames-order": "error"
+ },
+ "settings": {
+ "tailwindcss": {
+ "callees": ["cn", "cva"],
+ "config": "tailwind.config.cjs"
+ },
+ "next": {
+ "rootDir": ["apps/*/"]
+ }
+ },
+ "overrides": [
+ {
+ "files": ["*.ts", "*.tsx"],
+ "parser": "@typescript-eslint/parser"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index c6bba59..af7a2b5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,130 +1,41 @@
-# Logs
-logs
-*.log
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-lerna-debug.log*
-.pnpm-debug.log*
-
-# Diagnostic reports (https://nodejs.org/api/report.html)
-report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
-# Runtime data
-pids
-*.pid
-*.seed
-*.pid.lock
+# dependencies
+node_modules
+.pnp
+.pnp.js
-# Directory for instrumented libs generated by jscoverage/JSCover
-lib-cov
-
-# Coverage directory used by tools like istanbul
+# testing
coverage
-*.lcov
-
-# nyc test coverage
-.nyc_output
-
-# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
-.grunt
-
-# Bower dependency directory (https://bower.io/)
-bower_components
-
-# node-waf configuration
-.lock-wscript
-
-# Compiled binary addons (https://nodejs.org/api/addons.html)
-build/Release
-
-# Dependency directories
-node_modules/
-jspm_packages/
-
-# Snowpack dependency directory (https://snowpack.dev/)
-web_modules/
-
-# TypeScript cache
-*.tsbuildinfo
-
-# Optional npm cache directory
-.npm
-
-# Optional eslint cache
-.eslintcache
-
-# Optional stylelint cache
-.stylelintcache
-# Microbundle cache
-.rpt2_cache/
-.rts2_cache_cjs/
-.rts2_cache_es/
-.rts2_cache_umd/
+# next.js
+.next/
+out/
+build
-# Optional REPL history
-.node_repl_history
+# misc
+.DS_Store
+*.pem
-# Output of 'npm pack'
-*.tgz
-
-# Yarn Integrity file
-.yarn-integrity
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+.pnpm-debug.log*
-# dotenv environment variable files
-.env
+# local env files
+.env.local
.env.development.local
.env.test.local
.env.production.local
-.env.local
-
-# parcel-bundler cache (https://parceljs.org/)
-.cache
-.parcel-cache
-
-# Next.js build output
-.next
-out
-
-# Nuxt.js build / generate output
-.nuxt
-dist
-
-# Gatsby files
-.cache/
-# Comment in the public line in if your project uses Gatsby and not Next.js
-# https://nextjs.org/blog/next-9-1#public-directory-support
-# public
-
-# vuepress build output
-.vuepress/dist
-
-# vuepress v2.x temp and cache directory
-.temp
-.cache
-
-# Docusaurus cache and generated files
-.docusaurus
-
-# Serverless directories
-.serverless/
-
-# FuseBox cache
-.fusebox/
-
-# DynamoDB Local files
-.dynamodb/
-# TernJS port file
-.tern-port
+# turbo
+.turbo
-# Stores VSCode versions used for testing VSCode extensions
-.vscode-test
+.contentlayer
+tsconfig.tsbuildinfo
-# yarn v2
-.yarn/cache
-.yarn/unplugged
-.yarn/build-state.yml
-.yarn/install-state.gz
-.pnp.*
+# ide
+.idea
+.fleet
+.vscode
\ No newline at end of file
diff --git a/.husky/pre-commit b/.husky/pre-commit
new file mode 100644
index 0000000..d0a7784
--- /dev/null
+++ b/.husky/pre-commit
@@ -0,0 +1 @@
+npx lint-staged
\ No newline at end of file
diff --git a/.npmrc b/.npmrc
new file mode 100644
index 0000000..f87a044
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1 @@
+auto-install-peers=true
\ No newline at end of file
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 0000000..80b8e06
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1,7 @@
+dist
+node_modules
+.next
+build
+.contentlayer
+apps/www/pages/api/registry.json
+**/fixtures
\ No newline at end of file
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 0000000..bfb0eec
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,5 @@
+{
+ "plugins": [
+ "prettier-plugin-tailwindcss"
+ ]
+}
\ No newline at end of file
diff --git a/app/(auth)/layout.tsx b/app/(auth)/layout.tsx
new file mode 100644
index 0000000..28049ce
--- /dev/null
+++ b/app/(auth)/layout.tsx
@@ -0,0 +1,54 @@
+import { cn } from "@/lib/utils";
+import { HorizontalGradient } from "@/components/marketing/horizontal-gradient";
+import Link from "next/link";
+import { buttonVariants } from "@/components/ui/button";
+import { ArrowLeftIcon } from "@radix-ui/react-icons";
+import { FileScriptIcon } from "hugeicons-react";
+
+export default function AuthLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return (
+ <>
+
+
+
+ Back
+
+
{children}
+
+
+
+
+ Generate & Ship UI with minimal effort
+
+
+ Synth UI allows you to generate User-Interfaces without writing a
+ single line of code.
+
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/app/(auth)/sign-in/[[...sign-in]]/page.tsx b/app/(auth)/sign-in/[[...sign-in]]/page.tsx
new file mode 100644
index 0000000..57cf97a
--- /dev/null
+++ b/app/(auth)/sign-in/[[...sign-in]]/page.tsx
@@ -0,0 +1,5 @@
+import { SignIn } from "@clerk/nextjs";
+
+export default function SignInPage() {
+ return ;
+}
diff --git a/app/(auth)/sign-up/[[...sign-up]]/page.tsx b/app/(auth)/sign-up/[[...sign-up]]/page.tsx
new file mode 100644
index 0000000..67d7c92
--- /dev/null
+++ b/app/(auth)/sign-up/[[...sign-up]]/page.tsx
@@ -0,0 +1,5 @@
+import { SignUp } from "@clerk/nextjs";
+
+export default function SignUpPage() {
+ return test
;
+}
diff --git a/app/(chat)/chat/[id]/page.tsx b/app/(chat)/chat/[id]/page.tsx
new file mode 100644
index 0000000..e525959
--- /dev/null
+++ b/app/(chat)/chat/[id]/page.tsx
@@ -0,0 +1,63 @@
+import { Chat } from "@/components/chat";
+import ChatHeader from "@/components/chat-header";
+import { getChat, getMissingKeys } from "@/lib/actions/chat";
+import { AI, getUIStateFromAIState } from "@/lib/ai/core";
+import { currentUser } from "@clerk/nextjs/server";
+import { Metadata } from "next";
+import { notFound, redirect } from "next/navigation";
+
+export interface ChatPageProps {
+ params: {
+ id: string;
+ };
+}
+
+export async function generateMetadata({
+ params,
+}: ChatPageProps): Promise {
+ const user = await currentUser();
+
+ if (!user) {
+ return {};
+ }
+
+ const chat = await getChat(params.id, user.id);
+
+ if (!chat || "error" in chat) {
+ redirect("/chat");
+ } else {
+ return {
+ title:
+ (chat?.title.toString().slice(0, 50) ?? "Untitled Chat") +
+ " - Synth UI",
+ };
+ }
+}
+
+export default async function ChatPage({ params }: ChatPageProps) {
+ const user = await currentUser();
+ const missingKeys = await getMissingKeys();
+
+ if (!user) {
+ redirect(`/login?next=/chat/${params.id}`);
+ }
+
+ const userId = user.id;
+ const chat = await getChat(params.id, userId);
+
+ if (!chat || "error" in chat) {
+ redirect("/chat");
+ } else {
+ if (chat?.userId !== user?.id) {
+ notFound();
+ }
+
+ const messages = getUIStateFromAIState(chat);
+
+ return (
+
+
+
+ );
+ }
+}
diff --git a/app/(chat)/chat/history/page.tsx b/app/(chat)/chat/history/page.tsx
new file mode 100644
index 0000000..528b602
--- /dev/null
+++ b/app/(chat)/chat/history/page.tsx
@@ -0,0 +1,60 @@
+import { Suspense } from "react";
+import ChatHistorySearchForm from "@/components/chat-history-search-form";
+import { getChats } from "@/lib/actions/chat";
+import { currentUser } from "@clerk/nextjs/server";
+import { redirect } from "next/navigation";
+import { ChatHistorySkeleton } from "@/components/chat-history-skeleton";
+import Link from "next/link";
+import { BubbleChatSearchIcon } from "hugeicons-react";
+import { Input } from "@/components/ui/input";
+import { cn } from "@/lib/utils";
+import { buttonVariants } from "@/components/ui/button";
+
+async function ChatHistoryContent() {
+ const user = await currentUser();
+
+ if (!user) {
+ redirect("/chat");
+ }
+
+ const chats = await getChats(user.id);
+
+ if (!chats || "error" in chats) {
+ return Error retrieving chats
;
+ }
+
+ return ;
+}
+
+export default function HistoryPage() {
+ return (
+
+
+
+
+ }
+ >
+
+
+ );
+}
diff --git a/app/(chat)/chat/layout.tsx b/app/(chat)/chat/layout.tsx
new file mode 100644
index 0000000..ae315bb
--- /dev/null
+++ b/app/(chat)/chat/layout.tsx
@@ -0,0 +1,14 @@
+import Sidebar from "@/components/sidebar";
+
+export default function ChatLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return (
+
+ );
+}
diff --git a/app/(chat)/chat/page.tsx b/app/(chat)/chat/page.tsx
new file mode 100644
index 0000000..593f4f6
--- /dev/null
+++ b/app/(chat)/chat/page.tsx
@@ -0,0 +1,19 @@
+import { Chat } from "@/components/chat";
+import { getMissingKeys } from "@/lib/actions/chat";
+import { AI } from "@/lib/ai/core";
+import { generateId } from "ai";
+
+export const metadata = {
+ title: "New Chat - Synth UI",
+};
+
+export default async function ChatPage() {
+ const id = generateId();
+ const missingKeys = await getMissingKeys();
+
+ return (
+
+
+
+ );
+}
diff --git a/app/(chat)/layout.tsx b/app/(chat)/layout.tsx
new file mode 100644
index 0000000..c00ae37
--- /dev/null
+++ b/app/(chat)/layout.tsx
@@ -0,0 +1,23 @@
+import ComponentPreviewPanel from "@/components/component-preview-panel";
+import Sidebar from "@/components/sidebar";
+import { ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
+
+export default function ChatWrapperLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return (
+
+
+
+ {children}
+
+
+
+
+ );
+}
diff --git a/app/(chat)/share/[id]/page.tsx b/app/(chat)/share/[id]/page.tsx
new file mode 100644
index 0000000..ac2d0b5
--- /dev/null
+++ b/app/(chat)/share/[id]/page.tsx
@@ -0,0 +1,55 @@
+import { type Metadata } from "next";
+import { notFound } from "next/navigation";
+import { formatDate } from "@/lib/utils";
+import { getSharedChat } from "@/lib/actions/chat";
+import { ChatList } from "@/components/chat-list";
+import { AI, UIState, getUIStateFromAIState } from "@/lib/ai/core";
+
+export const runtime = "edge";
+export const preferredRegion = "home";
+
+interface SharePageProps {
+ params: {
+ id: string;
+ };
+}
+
+export async function generateMetadata({
+ params,
+}: SharePageProps): Promise {
+ const chat = await getSharedChat(params.id);
+
+ return {
+ title: (chat?.title.slice(0, 50) ?? "Untitled") + " - Shared - Synth UI",
+ };
+}
+
+export default async function SharePage({ params }: SharePageProps) {
+ const chat = await getSharedChat(params.id);
+
+ if (!chat || !chat?.sharePath) {
+ notFound();
+ }
+
+ const uiState: UIState = getUIStateFromAIState(chat);
+
+ return (
+ <>
+
+
+
+
+
{chat.title}
+
+ {formatDate(chat.createdAt)} · {chat.messages.length} messages
+
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/app/(marketing)/layout.tsx b/app/(marketing)/layout.tsx
new file mode 100644
index 0000000..328407f
--- /dev/null
+++ b/app/(marketing)/layout.tsx
@@ -0,0 +1,18 @@
+import type { Metadata } from "next";
+import "@/styles/globals.css";
+import { NavBar } from "@/components/marketing/navbar";
+import { Footer } from "@/components/marketing/footer";
+
+export default function RootLayout({
+ children,
+}: Readonly<{
+ children: React.ReactNode;
+}>) {
+ return (
+
+
+ {children}
+
+
+ );
+}
diff --git a/app/(marketing)/page.tsx b/app/(marketing)/page.tsx
new file mode 100644
index 0000000..9c20fe0
--- /dev/null
+++ b/app/(marketing)/page.tsx
@@ -0,0 +1,28 @@
+import { Hero } from "@/components/marketing/hero";
+import { Background } from "@/components/marketing/background";
+import { Features } from "@/components/marketing/features";
+import { Companies } from "@/components/marketing/companies";
+import { GridFeatures } from "@/components/marketing/grid-features";
+import { CTA } from "@/components/marketing/cta";
+
+export default function Home() {
+ return (
+
+ );
+}
diff --git a/app/favicon.ico b/app/favicon.ico
new file mode 100644
index 0000000..bdaa5ad
Binary files /dev/null and b/app/favicon.ico differ
diff --git a/app/layout.tsx b/app/layout.tsx
new file mode 100644
index 0000000..ba79546
--- /dev/null
+++ b/app/layout.tsx
@@ -0,0 +1,62 @@
+import { GeistSans } from "geist/font/sans";
+
+import "@/styles/globals.css";
+
+import { cn } from "@/lib/utils";
+import { Metadata, Viewport } from "next";
+import { Providers } from "@/components/providers";
+import { Toaster } from "@/components/ui/sonner";
+
+const title = "Synth UI";
+const description = "Generative User Interfaces for the Web";
+
+export const metadata: Metadata = {
+ metadataBase: new URL("https://synthui.design"),
+ title,
+ description,
+ openGraph: {
+ title,
+ description,
+ },
+ twitter: {
+ title,
+ description,
+ card: "summary_large_image",
+ creator: "@julian-at",
+ },
+};
+
+export const viewport: Viewport = {
+ width: "device-width",
+ initialScale: 1,
+ minimumScale: 1,
+ maximumScale: 1,
+ themeColor: [
+ { media: "(prefers-color-scheme: light)", color: "white" },
+ { media: "(prefers-color-scheme: dark)", color: "black" },
+ ],
+};
+
+export default function RootLayout({
+ children,
+}: Readonly<{
+ children: React.ReactNode;
+}>) {
+ return (
+
+
+
+
+ {children}
+
+
+
+
+
+ );
+}
diff --git a/app/not-found.tsx b/app/not-found.tsx
new file mode 100644
index 0000000..58a3676
--- /dev/null
+++ b/app/not-found.tsx
@@ -0,0 +1,7 @@
+import { generateId } from "ai";
+
+export default function Home() {
+ const id = generateId();
+
+ return Not Found.
;
+}
diff --git a/components.json b/components.json
new file mode 100644
index 0000000..738daae
--- /dev/null
+++ b/components.json
@@ -0,0 +1,20 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "new-york",
+ "rsc": true,
+ "tsx": true,
+ "tailwind": {
+ "config": "tailwind.config.ts",
+ "css": "styles/globals.css",
+ "baseColor": "neutral",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/lib/utils",
+ "ui": "@/components/ui",
+ "lib": "@/lib",
+ "hooks": "@/hooks"
+ }
+}
diff --git a/components/chat-dropdown.tsx b/components/chat-dropdown.tsx
new file mode 100644
index 0000000..67da49e
--- /dev/null
+++ b/components/chat-dropdown.tsx
@@ -0,0 +1,62 @@
+import { TooltipButton } from "@/components/tooltip-button";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import {
+ Delete02Icon,
+ Edit02Icon,
+ MoreHorizontalIcon,
+ SentIcon,
+} from "hugeicons-react";
+import ChatRenameDialog from "@/components/chat-rename-dialog";
+import { ChatShareDialog } from "@/components/chat-share-dialog";
+
+export function ChatDropdown({ className }: { className?: string }) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Share Chat
+
+
+
+
+
+
+
+
+
+
+ Rename Chats
+
+
+
+
+
+
+
+ Delete Chat
+
+
+
+
+ );
+}
diff --git a/components/chat-header-skeleton.tsx b/components/chat-header-skeleton.tsx
new file mode 100644
index 0000000..4fa83d5
--- /dev/null
+++ b/components/chat-header-skeleton.tsx
@@ -0,0 +1,19 @@
+import { Skeleton } from "@/components/ui/skeleton";
+
+export default function ChatHeaderSkeleton() {
+ return (
+
+ );
+}
diff --git a/components/chat-header.tsx b/components/chat-header.tsx
new file mode 100644
index 0000000..fb09d0c
--- /dev/null
+++ b/components/chat-header.tsx
@@ -0,0 +1,86 @@
+"use client";
+
+import {
+ Menu01Icon,
+ SentIcon,
+ SquareLock02Icon,
+ SquareUnlock02Icon,
+} from "hugeicons-react";
+import { Button } from "@/components/ui/button";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from "@/components/ui/tooltip";
+import { useSidebar } from "@/components/ui/sidebar";
+import { TooltipButton } from "@/components/tooltip-button";
+import { ChatShareDialog } from "@/components/chat-share-dialog";
+import { ChatDropdown } from "@/components/chat-dropdown";
+import { useAppState } from "@/lib/hooks/use-app-state";
+import { usePathname } from "next/navigation";
+import ChatRenameDialog from "./chat-rename-dialog";
+
+export default function ChatHeader() {
+ const pathname = usePathname();
+ const { chat } = useAppState();
+ const { open, setOpen } = useSidebar();
+
+ console.log("detected change in chat", chat?.id, chat?.sharePath);
+
+ console.log("pathname", pathname);
+ console.log("chat.path", chat?.path);
+
+ if (!chat || pathname !== chat.path) {
+ return null;
+ }
+
+ return (
+
+
+
setOpen(!open)}
+ className="md:hidden"
+ >
+
+
+
+
+
+ {chat.title}
+
+
+ {chat.sharePath ? (
+
+
+
+
+
+ This chat is public
+
+
+ ) : (
+
+
+
+
+
+ This chat is private
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/components/chat-history-card-skeleton.tsx b/components/chat-history-card-skeleton.tsx
new file mode 100644
index 0000000..3a3b6b6
--- /dev/null
+++ b/components/chat-history-card-skeleton.tsx
@@ -0,0 +1,18 @@
+import { Card } from "@/components/ui/card";
+import { Separator } from "@/components/ui/separator";
+import { Skeleton } from "@/components/ui/skeleton";
+
+export default function ChatHistoryCardSkeleton() {
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/components/chat-history-card.tsx b/components/chat-history-card.tsx
new file mode 100644
index 0000000..75e1d6c
--- /dev/null
+++ b/components/chat-history-card.tsx
@@ -0,0 +1,40 @@
+import { Card } from "@/components/ui/card";
+import { ChatPublicBadge } from "@/components/chat-public-badge";
+import { Separator } from "@/components/ui/separator";
+import { ChatDropdown } from "@/components/chat-dropdown";
+import Link from "next/link";
+import moment from "moment";
+import { Chat } from "@/lib/types";
+
+export default function ChatHistoryCard({
+ id,
+ title,
+ description,
+ createdAt,
+ sharePath,
+ path,
+}: Chat) {
+ return (
+
+
+
+
{title}
+
+
+
+ {description}
+
+
+
+
+ Created {moment(createdAt).fromNow()}
+
+
+
+
+
+ );
+}
diff --git a/components/chat-history-search-form.tsx b/components/chat-history-search-form.tsx
new file mode 100644
index 0000000..002fc31
--- /dev/null
+++ b/components/chat-history-search-form.tsx
@@ -0,0 +1,151 @@
+"use client";
+
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useForm } from "react-hook-form";
+import ChatHistoryCard from "@/components/chat-history-card";
+import { buttonVariants } from "@/components/ui/button";
+import { ScrollArea } from "@/components/ui/scroll-area";
+import { cn } from "@/lib/utils";
+import { BubbleChatCancelIcon, BubbleChatSearchIcon } from "hugeicons-react";
+import Link from "next/link";
+import { z } from "zod";
+import { Form, FormControl, FormField } from "@/components/ui/form";
+import { Input } from "@/components/ui/input";
+import { Chat } from "@/lib/types";
+import debounce from "debounce";
+import { useState, useMemo, useCallback, useEffect } from "react";
+import ChatHistoryCardSkeleton from "@/components/chat-history-card-skeleton";
+import { ChatHistorySkeleton } from "@/components/chat-history-skeleton";
+
+const searchFormSchema = z.object({
+ query: z.string().min(0),
+});
+
+interface ChatHistorySearchFormProps {
+ chats: Chat[];
+}
+
+export default function ChatHistorySearchForm({
+ chats,
+}: ChatHistorySearchFormProps) {
+ const [isLoading, setIsLoading] = useState(false);
+ const [query, setQuery] = useState("");
+
+ const form = useForm>({
+ resolver: zodResolver(searchFormSchema),
+ defaultValues: {
+ query: "",
+ },
+ });
+
+ const filteredChats = useMemo(() => {
+ const lowercaseQuery = query.toLowerCase();
+ return chats.filter((chat) => {
+ const title = chat.title?.toLowerCase() || "";
+ const description =
+ chat.messages
+ .filter(
+ (message) =>
+ message.role === "assistant" &&
+ typeof message.content === "string",
+ )
+ .at(-1)
+ ?.content.toString()
+ .toLowerCase() || "";
+ return (
+ title.includes(lowercaseQuery) || description.includes(lowercaseQuery)
+ );
+ });
+ }, [chats, query]);
+
+ const debouncedHandleSearchChange = useCallback(
+ (value: string) => {
+ const debouncedSetQuery = debounce((v: string) => setQuery(v), 300);
+ debouncedSetQuery(value);
+ },
+ [setQuery],
+ );
+
+ useEffect(() => {
+ setIsLoading(true);
+ const timer = setTimeout(() => setIsLoading(false), 300);
+ return () => clearTimeout(timer);
+ }, [query]);
+
+ return (
+
+
+ );
+}
diff --git a/components/chat-history-skeleton.tsx b/components/chat-history-skeleton.tsx
new file mode 100644
index 0000000..aa2be33
--- /dev/null
+++ b/components/chat-history-skeleton.tsx
@@ -0,0 +1,14 @@
+import ChatHistoryCardSkeleton from "@/components/chat-history-card-skeleton";
+import { ScrollArea } from "@/components/ui/scroll-area";
+
+export function ChatHistorySkeleton() {
+ return (
+
+
+ {Array.from({ length: 9 }).map((_, index) => (
+
+ ))}
+
+
+ );
+}
diff --git a/components/chat-history-widget-form.tsx b/components/chat-history-widget-form.tsx
new file mode 100644
index 0000000..c0fa422
--- /dev/null
+++ b/components/chat-history-widget-form.tsx
@@ -0,0 +1,151 @@
+"use client";
+
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useForm } from "react-hook-form";
+import { z } from "zod";
+import { Form, FormControl, FormField } from "@/components/ui/form";
+import { Input } from "@/components/ui/input";
+import { Chat } from "@/lib/types";
+import debounce from "debounce";
+import { useState, useMemo, useCallback, useEffect } from "react";
+import { MessageSearch01Icon } from "hugeicons-react";
+import Link from "next/link";
+import { cn } from "@/lib/utils";
+import { buttonVariants } from "@/components/ui/button";
+import { Skeleton } from "@/components/ui/skeleton";
+import { Separator } from "@/components/ui/separator";
+import { ChatPublicBadge } from "@/components/chat-public-badge";
+
+const searchFormSchema = z.object({
+ query: z.string().min(0),
+});
+
+interface ChatHistoryWidgetFormProps {
+ chats: Chat[];
+}
+
+export default function ChatHistoryWidgetForm({
+ chats,
+}: ChatHistoryWidgetFormProps) {
+ const [isLoading, setIsLoading] = useState(false);
+ const [query, setQuery] = useState("");
+
+ const form = useForm>({
+ resolver: zodResolver(searchFormSchema),
+ defaultValues: {
+ query: "",
+ },
+ });
+
+ const filteredChats = useMemo(() => {
+ const lowercaseQuery = query.toLowerCase();
+ return chats.filter((chat) => {
+ const title = chat.title?.toLowerCase() || "";
+ const description =
+ chat.messages
+ .filter(
+ (message) =>
+ message.role === "assistant" &&
+ typeof message.content === "string",
+ )
+ .at(-1)
+ ?.content.toString()
+ .toLowerCase() || "";
+ return (
+ title.includes(lowercaseQuery) || description.includes(lowercaseQuery)
+ );
+ });
+ }, [chats, query]);
+
+ const debouncedHandleSearchChange = useCallback(
+ (value: string) => {
+ const debouncedSetQuery = debounce((v: string) => setQuery(v), 300);
+ debouncedSetQuery(value);
+ },
+ [setQuery],
+ );
+
+ useEffect(() => {
+ setIsLoading(true);
+ const timer = setTimeout(() => setIsLoading(false), 300);
+ return () => clearTimeout(timer);
+ }, [query]);
+
+ return (
+
+
+ );
+}
diff --git a/components/chat-history-widget.tsx b/components/chat-history-widget.tsx
new file mode 100644
index 0000000..118de2a
--- /dev/null
+++ b/components/chat-history-widget.tsx
@@ -0,0 +1,49 @@
+import { currentUser } from "@clerk/nextjs/server";
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@/components/ui/popover";
+import { Button } from "@/components/ui/button";
+import { Clock04Icon } from "hugeicons-react";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from "@/components/ui/tooltip";
+import ChatHistoryWidgetForm from "@/components/chat-history-widget-form";
+import { getChats } from "@/lib/actions/chat";
+
+export default async function ChatHistory() {
+ const user = await currentUser();
+ if (!user) return null;
+
+ const chats = await getChats(user.id);
+
+ if (!chats || "error" in chats) {
+ return null;
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ Chat History
+
+
+
+
+
+
+ );
+}
diff --git a/components/chat-list-skeleton.tsx b/components/chat-list-skeleton.tsx
new file mode 100644
index 0000000..8e2b298
--- /dev/null
+++ b/components/chat-list-skeleton.tsx
@@ -0,0 +1,36 @@
+import { Skeleton } from "@/components/ui/skeleton";
+import ChatHeaderSkeleton from "@/components/chat-header-skeleton";
+import ComponentCardSkeleton from "@/components/component-card-skeleton";
+import { Separator } from "@/components/ui/separator";
+
+export function ChatListSkeleton() {
+ return (
+
+
+
+
+ {Array.from({ length: 3 }).map((_, index) => (
+
+
+
+ {[75, 82, 89, 96, 85, 92, 79, 86, 88].map((width, index) =>
+ index === 5 ? (
+
+
+
+ ) : (
+
+ ),
+ )}
+ {index !== 2 &&
}
+
+
+ ))}
+
+
+ );
+}
diff --git a/components/chat-list.tsx b/components/chat-list.tsx
new file mode 100644
index 0000000..2ed60f7
--- /dev/null
+++ b/components/chat-list.tsx
@@ -0,0 +1,29 @@
+import { Separator } from "@/components/ui/separator";
+import { UIState } from "@/lib/ai/core";
+
+export interface ChatList {
+ messages: UIState;
+ isShared: boolean;
+}
+
+export function ChatList({ messages }: ChatList) {
+ if (!messages.length) {
+ return null;
+ }
+
+ return (
+
+ {messages
+ .filter((m) => m.display !== undefined)
+ .map((message, index) => (
+
+ {message.display}
+ {index < messages.length - 1 &&
+ messages[index + 1].id !== message.id && (
+
+ )}
+
+ ))}
+
+ );
+}
diff --git a/components/chat-message.tsx b/components/chat-message.tsx
new file mode 100644
index 0000000..451b46c
--- /dev/null
+++ b/components/chat-message.tsx
@@ -0,0 +1,185 @@
+"use client";
+
+import remarkGfm from "remark-gfm";
+import remarkMath from "remark-math";
+
+import { spinner } from "@/components/spinner";
+import { cn } from "@/lib/utils";
+import { CodeBlock } from "@/components/ui/codeblock";
+import { MemoizedReactMarkdown } from "@/components/markdown";
+import { StreamableValue } from "ai/rsc";
+import { useStreamableText } from "@/lib/hooks/use-streamable-text";
+import UserAvatar from "@/components/user-avatar";
+import Logo from "@/components/logo";
+
+export function MarkdownBlock({ content }: { content: string }) {
+ return (
+
+ {children};
+ },
+ h1({ children }) {
+ return {children} ;
+ },
+ h2({ children }) {
+ return {children} ;
+ },
+ h3({ children }) {
+ return {children} ;
+ },
+ h4({ children }) {
+ return {children} ;
+ },
+ h5({ children }) {
+ return {children} ;
+ },
+ h6({ children }) {
+ return {children} ;
+ },
+ code({ node, inline, className, children, ...props }) {
+ if (children.length) {
+ if (children[0] == "▍") {
+ return (
+ ▍
+ );
+ }
+
+ children[0] = (children[0] as string).replace("`▍`", "▍");
+ }
+
+ const match = /language-(\w+)/.exec(className || "");
+
+ if (inline) {
+ return (
+
+ {children}
+
+ );
+ }
+
+ return (
+
+ );
+ },
+ }}
+ >
+ {content}
+
+
+ );
+}
+
+export function UserMessage({ children }: { children: React.ReactNode }) {
+ return (
+
+ );
+}
+
+export function PlainMessage({
+ content,
+ indent = false,
+}: {
+ content: string | StreamableValue;
+ indent?: boolean;
+}) {
+ const text = useStreamableText(content);
+
+ return (
+
+
+
+ );
+}
+
+export function BotMessage({
+ content,
+ className,
+}: {
+ content: string | StreamableValue;
+ className?: string;
+}) {
+ const text = useStreamableText(content);
+
+ return (
+
+ );
+}
+
+export function BotCard({
+ children,
+ showAvatar = true,
+}: {
+ children: React.ReactNode;
+ showAvatar?: boolean;
+}) {
+ return (
+
+ );
+}
+
+export function SystemMessage({ children }: { children: React.ReactNode }) {
+ return (
+
+ );
+}
+
+export function SpinnerMessage() {
+ return (
+
+ );
+}
diff --git a/components/chat-public-badge.tsx b/components/chat-public-badge.tsx
new file mode 100644
index 0000000..1d124f5
--- /dev/null
+++ b/components/chat-public-badge.tsx
@@ -0,0 +1,30 @@
+"use client";
+
+import { SquareLock02Icon, SquareUnlock02Icon } from "hugeicons-react";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from "@/components/ui/tooltip";
+import { cn } from "@/lib/utils";
+
+interface ChatPublicBadgeProps extends React.HTMLAttributes {
+ isPublic: boolean;
+}
+
+export function ChatPublicBadge({ className, isPublic }: ChatPublicBadgeProps) {
+ return (
+
+
+ {isPublic ? (
+
+ ) : (
+
+ )}
+
+
+ {isPublic ? "This chat is public" : "This chat is private"}
+
+
+ );
+}
diff --git a/components/chat-rename-dialog.tsx b/components/chat-rename-dialog.tsx
new file mode 100644
index 0000000..0dc806f
--- /dev/null
+++ b/components/chat-rename-dialog.tsx
@@ -0,0 +1,86 @@
+"use client";
+
+import {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog";
+import { Button } from "@/components/ui/button";
+import { Loading03Icon, PencilEdit01Icon, Tick02Icon } from "hugeicons-react";
+import { renameChat } from "@/lib/actions/chat";
+import { useState } from "react";
+import { Input } from "@/components/ui/input";
+import { useAppState } from "@/lib/hooks/use-app-state";
+
+export default function ChatRenameDialog({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ const { chat, setChatName } = useAppState();
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const [name, setName] = useState(chat?.title || "");
+ const [hasChanged, setHasChanged] = useState(false);
+
+ if (!chat) {
+ return null;
+ }
+
+ const { id } = chat;
+
+ const handleSubmit = async () => {
+ setIsLoading(true);
+ const res = await renameChat(id, name);
+ if (!res || !res.success || "error" in res) {
+ setError(res.error || "An error occurred while renaming the chat");
+ }
+
+ setError(null);
+ setHasChanged(true);
+ setIsLoading(false);
+ setChatName(name);
+ };
+
+ return (
+
+ {children}
+
+ Rename Chat
+
+ setName(e.target.value)}
+ placeholder="Chat Name"
+ />
+ {error && (
+
+ {error}
+
+ )}
+
+
+ Cancel
+
+
+ {isLoading ? (
+
+ ) : hasChanged ? (
+
+ ) : (
+
+ )}
+ Confirm
+
+
+
+
+ );
+}
diff --git a/components/chat-share-dialog.tsx b/components/chat-share-dialog.tsx
new file mode 100644
index 0000000..004f646
--- /dev/null
+++ b/components/chat-share-dialog.tsx
@@ -0,0 +1,125 @@
+"use client";
+
+import { useState } from "react";
+import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog";
+import { Switch } from "@/components/ui/switch";
+import {
+ Globe02Icon,
+ Link04Icon,
+ Loading03Icon,
+ Tick02Icon,
+} from "hugeicons-react";
+import { shareChat, unshareChat } from "@/lib/actions/chat";
+import { toast } from "sonner";
+import { useCopyToClipboard } from "@/lib/hooks/use-copy-to-clipboard";
+import { useAppState } from "@/lib/hooks/use-app-state";
+
+export function ChatShareDialog({ children }: { children: React.ReactNode }) {
+ const { copyToClipboard, isCopied } = useCopyToClipboard({ timeout: 2000 });
+ const { chat, setSharePath } = useAppState();
+
+ if (!chat) {
+ return null;
+ }
+
+ const { id, sharePath } = chat;
+
+ const [isLoading, setIsLoading] = useState(false);
+
+ const handleShareToggle = async () => {
+ setIsLoading(true);
+
+ try {
+ if (!sharePath) {
+ const sharedChat = await shareChat(id);
+ if ("error" in sharedChat) {
+ throw new Error(sharedChat.error);
+ }
+
+ setSharePath(sharedChat.sharePath);
+ setIsLoading(false);
+ return;
+ } else {
+ const unsharedChat = await unshareChat(id);
+ if ("error" in unsharedChat) {
+ throw new Error(unsharedChat.error);
+ }
+
+ setSharePath(undefined);
+ setIsLoading(false);
+ return;
+ }
+ } catch (error: any) {
+ toast.error(error.message || "Something went wrong");
+ setIsLoading(false);
+ return;
+ }
+ };
+
+ const handleCopyLink = () => {
+ if (sharePath) {
+ copyToClipboard(`${process.env.NEXT_PUBLIC_URL}${sharePath}`);
+ }
+ };
+
+ return (
+
+ {children}
+
+ Share Chat
+
+ Chats are private by default, but can be shared via a link. By
+ sharing this chat, all of its messages and blocks will be visible.
+
+
+
+
+ {isLoading ? (
+
+ ) : (
+
+ )}
+
+
+
Public with link
+
+ Anyone with the link can view this chat and its Blocks
+
+
+
+
+
+
+
+ {isLoading ? (
+
+ ) : isCopied ? (
+
+ ) : (
+
+ )}
+ Copy Link
+
+
+
+
+ );
+}
diff --git a/components/chat.tsx b/components/chat.tsx
new file mode 100644
index 0000000..a1d3f0f
--- /dev/null
+++ b/components/chat.tsx
@@ -0,0 +1,153 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import { readStreamableValue, useAIState, useUIState } from "ai/rsc";
+import { useScrollAnchor } from "@/lib/hooks/use-scroll-anchor";
+import { cn } from "@/lib/utils";
+import { ChatList } from "@/components/chat-list";
+import { ScrollArea } from "@/components/ui/scroll-area";
+import EmptyScreen from "@/components/empty-screen";
+import EmptyScreenBackground from "@/components/empty-screen-background";
+import {
+ ResizableHandle,
+ ResizablePanel,
+ ResizablePanelGroup,
+} from "@/components/ui/resizable";
+import PromptForm from "@/components/prompt-form";
+
+import { AI } from "@/lib/ai/core";
+import { useComponentPreview } from "@/lib/hooks/use-component-preview";
+import ChatHeader from "@/components/chat-header";
+import { toast } from "sonner";
+import { ChatListSkeleton } from "@/components/chat-list-skeleton";
+import { useAppState } from "@/lib/hooks/use-app-state";
+import ComponentPreviewPanel from "./component-preview-panel";
+import { generateTitle } from "@/lib/ai/agents/title-generator";
+
+interface ChatProps extends React.HTMLAttributes {
+ missingKeys?: string[];
+}
+
+export function Chat({ className, missingKeys }: ChatProps) {
+ const [aiState] = useAIState();
+ const [messages] = useUIState();
+ const { setChat, setChatName } = useAppState();
+ const { isPreviewOpen, activeMessageId, closePreview, setComponentCards } =
+ useComponentPreview();
+ const [isMounted, setIsMounted] = useState(false);
+ const { messagesRef, scrollRef, visibilityRef } = useScrollAnchor();
+
+ useEffect(() => {
+ setIsMounted(true);
+ }, []);
+
+ useEffect(() => {
+ if (missingKeys) {
+ missingKeys.map((key) => {
+ toast.error(`Missing ${key} environment variable!`);
+ });
+ }
+ }, [missingKeys]);
+
+ useEffect(() => {
+ if (messages.length === 1) {
+ window.history.replaceState({}, "", `/chat/${aiState.id}`);
+
+ if (!aiState.title || aiState.title === "") {
+ (async () => {
+ const initialMessage = aiState.messages
+ .filter((m) => m.role === "user")
+ .find((m) => m.content)?.content;
+ console.log("initialMessage", initialMessage);
+ if (initialMessage) {
+ const newTitle = await generateTitle(initialMessage.toString());
+ console.log("newTitle", newTitle);
+ setChatName(newTitle);
+ }
+ })();
+ }
+ }
+ }, [messages, aiState.id]);
+
+ useEffect(() => {
+ if (Array.isArray(messages)) {
+ const filteredCards = messages.filter(async (m) => {
+ if (!m.isComponentCard) return;
+ let isComponentCard = false;
+ for await (const delta of readStreamableValue(m.isComponentCard)) {
+ console.log("delta", delta);
+ if (typeof delta === "boolean") {
+ isComponentCard = delta;
+ }
+ }
+ return isComponentCard;
+ });
+
+ if (isPreviewOpen && activeMessageId) {
+ const activeCardExists = filteredCards.some(
+ (card) => card.id === activeMessageId,
+ );
+ if (!activeCardExists) {
+ closePreview();
+ }
+ }
+ }
+ }, [
+ messages,
+ isPreviewOpen,
+ activeMessageId,
+ setComponentCards,
+ closePreview,
+ ]);
+
+ useEffect(() => {
+ setChat(aiState);
+ }, [aiState]);
+
+ if (!isMounted) {
+ return messages.length > 0 ? (
+
+ ) : (
+
+ );
+ }
+
+ return (
+
+ {messages.length ? (
+ <>
+
+
+
+
+
+ >
+ ) : (
+
+ )}
+
+ );
+}
diff --git a/components/component-card-skeleton.tsx b/components/component-card-skeleton.tsx
new file mode 100644
index 0000000..414f500
--- /dev/null
+++ b/components/component-card-skeleton.tsx
@@ -0,0 +1,28 @@
+import { Button } from "@/components/ui/button";
+import { Skeleton } from "@/components/ui/skeleton";
+import { cn } from "@/lib/utils";
+import React from "react";
+
+interface ComponentCardSkeletonProps
+ extends React.HTMLAttributes {}
+
+export default function ComponentCardSkeleton({
+ className,
+}: ComponentCardSkeletonProps) {
+ return (
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/components/component-card.tsx b/components/component-card.tsx
new file mode 100644
index 0000000..5e1347e
--- /dev/null
+++ b/components/component-card.tsx
@@ -0,0 +1,104 @@
+"use client";
+
+import { ReactIcon } from "hugeicons-react";
+import { Button } from "@/components/ui/button";
+import { useComponentPreview } from "@/lib/hooks/use-component-preview";
+import { useStreamableText } from "@/lib/hooks/use-streamable-text";
+import { useCallback, useEffect, useState } from "react";
+import { StreamableValue } from "ai/rsc";
+import { cn } from "@/lib/utils";
+
+export interface ComponentCardProps {
+ title?: string;
+ fileName?: string;
+ icon?: React.ReactNode;
+ code: string | StreamableValue;
+ messageId: string;
+}
+
+export default function ComponentCard({
+ title = "Untitled",
+ fileName = "untitled.tsx",
+ icon = ,
+ code,
+ messageId,
+}: ComponentCardProps) {
+ const {
+ isPreviewOpen,
+ togglePreview,
+ closePreview,
+ setPreviewCode,
+ activeMessageId,
+ } = useComponentPreview();
+ const streamedCode = useStreamableText(code);
+ const [isMounted, setIsMounted] = useState(false);
+
+ useEffect(() => {
+ setIsMounted(true);
+ }, []);
+
+ useEffect(() => {
+ if (isMounted) {
+ console.log(
+ "setting first preview",
+ messageId,
+ streamedCode,
+ title,
+ fileName,
+ );
+
+ togglePreview(messageId, streamedCode, title, fileName);
+ }
+ }, [isMounted]);
+
+ useEffect(() => {
+ setPreviewCode(streamedCode);
+ }, [streamedCode, setPreviewCode]);
+
+ const handleClick = useCallback(() => {
+ if (isPreviewOpen && activeMessageId === messageId) {
+ console.log("closing preview");
+ closePreview();
+ } else {
+ console.log("opening preview");
+ togglePreview(messageId, streamedCode, title, fileName);
+ }
+ }, [
+ isPreviewOpen,
+ closePreview,
+ togglePreview,
+ messageId,
+ activeMessageId,
+ streamedCode,
+ title,
+ fileName,
+ ]);
+
+ if (!isMounted) return null;
+
+ return (
+
+
+
+ {icon}
+
+
+
{title}
+
+ {fileName} · Click to
+ {isPreviewOpen ? " close" : " open"}
+
+
+
+
+ );
+}
diff --git a/components/component-editor-preview-header.tsx b/components/component-editor-preview-header.tsx
new file mode 100644
index 0000000..1ebec95
--- /dev/null
+++ b/components/component-editor-preview-header.tsx
@@ -0,0 +1,51 @@
+"use client";
+
+import { ArrowRightDoubleIcon, GitForkIcon } from "hugeicons-react";
+import { TooltipButton } from "@/components/tooltip-button";
+import { useComponentPreview } from "@/lib/hooks/use-component-preview";
+import NotImplementedDialog from "@/components/not-implemented-dialog";
+
+interface ComponentEditorPreviewHeaderProps {
+ title: string;
+}
+
+export default function ComponentEditorPreviewHeader({
+ title,
+}: ComponentEditorPreviewHeaderProps) {
+ const { closePreview } = useComponentPreview();
+
+ return (
+
+ );
+}
diff --git a/components/component-editor-preview.tsx b/components/component-editor-preview.tsx
new file mode 100644
index 0000000..19bf9d3
--- /dev/null
+++ b/components/component-editor-preview.tsx
@@ -0,0 +1,86 @@
+"use client";
+
+import { createRef, useEffect, useRef, useState } from "react";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import { BrowserIcon, CodeFolderIcon } from "hugeicons-react";
+import { ScrollArea } from "@/components/ui/scroll-area";
+import { useComponentPreview } from "@/lib/hooks/use-component-preview";
+import { MarkdownBlock } from "@/components/chat-message";
+
+export default function ComponentEditorPreview() {
+ const { previewCode, isPreviewOpen, activeMessageId, previewFileName } =
+ useComponentPreview();
+ const [isMounted, setIsMounted] = useState(false);
+
+ useEffect(() => {
+ setIsMounted(true);
+ }, []);
+
+ if (!isMounted || !isPreviewOpen || !activeMessageId) {
+ return null;
+ }
+
+ return (
+
+
+
+ );
+}
+
+function ComponentEditorPreviewContent({
+ code,
+ title,
+}: {
+ code: string;
+ title: string;
+}) {
+ const bottomRef = useRef(null);
+
+ useEffect(() => {
+ if (bottomRef.current) {
+ bottomRef.current.scrollIntoView({ behavior: "smooth" });
+ }
+ }, [code]);
+
+ return (
+
+
+
+ Preview
+
+
+ Code{" "}
+ ({title})
+
+
+
+ Not implemented yet
+
+
+
+
+ {/*
*/}
+
+
+
+ );
+}
diff --git a/components/component-preview-panel.tsx b/components/component-preview-panel.tsx
new file mode 100644
index 0000000..5312170
--- /dev/null
+++ b/components/component-preview-panel.tsx
@@ -0,0 +1,62 @@
+"use client";
+
+import { Drawer, DrawerContent } from "@/components/ui/drawer";
+import ComponentEditorPreviewHeader from "@/components/component-editor-preview-header";
+import ComponentEditorPreview from "@/components/component-editor-preview";
+import { ResizableHandle, ResizablePanel } from "@/components/ui/resizable";
+import { useComponentPreview } from "@/lib/hooks/use-component-preview";
+import { useEffect, useState } from "react";
+
+export default function ComponentPreviewPanel() {
+ const { isPreviewOpen, previewTitle, closePreview } = useComponentPreview();
+ const [width, setWidth] = useState(
+ typeof window !== "undefined" ? window.innerWidth : 0,
+ );
+ const [isMounted, setIsMounted] = useState(false);
+
+ useEffect(() => {
+ if (typeof window !== "undefined") {
+ const handleResize = () => {
+ setWidth(window.innerWidth);
+ };
+
+ window.addEventListener("resize", handleResize);
+ return () => window.removeEventListener("resize", handleResize);
+ }
+ }, []);
+
+ useEffect(() => {
+ setIsMounted(true);
+ }, []);
+
+ if (!isPreviewOpen || !isMounted) return null;
+
+ return (
+
+ {width >= 1280 ? (
+
+
+
+
+
+
+
+ ) : (
+
(!o ? closePreview() : null)}
+ >
+
+
+
+
+
+ )}
+
+ );
+}
diff --git a/components/empty-screen-background.tsx b/components/empty-screen-background.tsx
new file mode 100644
index 0000000..06441f2
--- /dev/null
+++ b/components/empty-screen-background.tsx
@@ -0,0 +1,67 @@
+import { cn } from "@/lib/utils";
+
+export default function EmptyScreenBackground() {
+ return (
+
+
+
+ );
+}
+
+const BackgroundGrids = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const GridLineVertical = ({
+ className,
+ offset,
+}: {
+ className?: string;
+ offset?: string;
+}) => {
+ return (
+
+ );
+};
diff --git a/components/empty-screen.tsx b/components/empty-screen.tsx
new file mode 100644
index 0000000..f267919
--- /dev/null
+++ b/components/empty-screen.tsx
@@ -0,0 +1,27 @@
+import PromptForm from "@/components/prompt-form";
+import ExamplePrompts from "@/components/example-prompts";
+import Balancer from "react-wrap-balancer";
+
+export default function EmptyScreen() {
+ return (
+
+
+
+
+ What can I help you build today?
+
+
+
+ Generate, Preview & Ship beautiful User Interfaces
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/components/error-card.tsx b/components/error-card.tsx
new file mode 100644
index 0000000..7d70c42
--- /dev/null
+++ b/components/error-card.tsx
@@ -0,0 +1,24 @@
+import { Alert02Icon } from "hugeicons-react";
+import { BotCard, BotMessage } from "@/components/chat-message";
+
+interface ErrorCardProps {
+ message: string;
+}
+
+export default function ErrorCard({ message }: ErrorCardProps) {
+ return (
+
+
+
+
+
+ Whooops! Something went wrong
+
+
{message}
+
+
+
+ );
+}
diff --git a/components/example-prompts.tsx b/components/example-prompts.tsx
new file mode 100644
index 0000000..7072e6a
--- /dev/null
+++ b/components/example-prompts.tsx
@@ -0,0 +1,66 @@
+"use client";
+
+import { ExamplePrompt } from "@/lib/types";
+import { Button } from "@/components/ui/button";
+import { ArrowUpRight01Icon } from "hugeicons-react";
+import { useActions, useUIState } from "ai/rsc";
+import { AI } from "@/lib/ai/core";
+import { generateId } from "ai";
+import { UserMessage } from "@/components/chat-message";
+import { cn } from "@/lib/utils";
+
+const EXAMPLE_PROMPTS: ExamplePrompt[] = [
+ {
+ label: "A Pricing Section",
+ prompt: "Generate a pricing section",
+ },
+ {
+ label: "An Ecommerce Dashboard",
+ prompt: "Create a dashboard for an e-commerce website",
+ },
+ {
+ label: "A Hero Section",
+ prompt: "Create a hero section for a website called 'Synth UI'",
+ },
+];
+
+interface ExamplePromptsProps extends React.HTMLAttributes {}
+
+export default function ExamplePrompts({ className }: ExamplePromptsProps) {
+ const [, setMessages] = useUIState();
+ const { submitUserMessage } = useActions();
+
+ return (
+
+ {EXAMPLE_PROMPTS.map((p: ExamplePrompt, index: number) => (
+
{
+ const query = p.prompt;
+
+ setMessages((currentMessages) => [
+ ...currentMessages,
+ {
+ id: generateId(),
+ display: {query} ,
+ },
+ ]);
+
+ const data = new FormData();
+ data.append("input", p.prompt);
+ const responseMessage = await submitUserMessage(data);
+ setMessages((currentMessages) => [
+ ...currentMessages,
+ responseMessage,
+ ]);
+ }}
+ >
+ {p.label}
+
+
+ ))}
+
+ );
+}
diff --git a/components/example/charts.tsx b/components/example/charts.tsx
new file mode 100644
index 0000000..7fcaa8a
--- /dev/null
+++ b/components/example/charts.tsx
@@ -0,0 +1,865 @@
+"use client";
+
+import {
+ Area,
+ AreaChart,
+ Bar,
+ BarChart,
+ CartesianGrid,
+ Label,
+ LabelList,
+ Line,
+ LineChart,
+ PolarAngleAxis,
+ RadialBar,
+ RadialBarChart,
+ Rectangle,
+ ReferenceLine,
+ XAxis,
+ YAxis,
+} from "recharts";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardFooter,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import {
+ ChartContainer,
+ ChartTooltip,
+ ChartTooltipContent,
+} from "@/components/ui/chart";
+import { Separator } from "@/components/ui/separator";
+export const description = "A collection of health charts.";
+
+export function Charts() {
+ return (
+
+
+
+
+ Today
+
+ 12,584{" "}
+
+ steps
+
+
+
+
+
+
+ }
+ />
+ {
+ return new Date(value).toLocaleDateString("en-US", {
+ weekday: "short",
+ });
+ }}
+ />
+ {
+ return new Date(value).toLocaleDateString("en-US", {
+ day: "numeric",
+ month: "long",
+ year: "numeric",
+ });
+ }}
+ />
+ }
+ cursor={false}
+ />
+
+
+
+
+
+
+
+
+
+ Over the past 7 days, you have walked{" "}
+ 53,305 steps.
+
+
+ You need{" "}
+ 12,584 more
+ steps to reach your goal.
+
+
+
+
+
+
+ Resting HR
+
+ 62
+
+ bpm
+
+
+
+
+ Variability
+
+ 35
+
+ ms
+
+
+
+
+
+
+
+
+
+ {
+ return new Date(value).toLocaleDateString("en-US", {
+ weekday: "short",
+ });
+ }}
+ />
+
+ {
+ return new Date(value).toLocaleDateString("en-US", {
+ day: "numeric",
+ month: "long",
+ year: "numeric",
+ });
+ }}
+ />
+ }
+ cursor={false}
+ />
+
+
+
+
+
+
+
+
+ Progress
+
+ You're average more steps a day this year than last year.
+
+
+
+
+
+ 12,453
+
+ steps/day
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 10,103
+
+ steps/day
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Walking Distance
+
+ Over the last 7 days, your distance walked and run was 12.5 miles
+ per day.
+
+
+
+
+ 12.5
+
+ miles/day
+
+
+
+
+ }
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Move
+
+ 562
+
+ kcal
+
+
+
+
+
+
Exercise
+
+ 73
+
+ min
+
+
+
+
+
+
Stand
+
+ 14
+
+ hr
+
+
+
+
+
+
+
+
+
+
+
+
+
Move
+
+ 562/600
+
+ kcal
+
+
+
+
+
Exercise
+
+ 73/120
+
+ min
+
+
+
+
+
Stand
+
+ 8/12
+
+ hr
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Active Energy
+
+ You're burning an average of 754 calories per day. Good job!
+
+
+
+
+ 1,254
+
+ kcal/day
+
+
+
+
+ }
+ />
+
+
+
+
+
+
+
+ Time in Bed
+
+ 8
+
+ hr
+
+ 35
+
+ min
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+ formatter={(value: any) => (
+
+ Time in bed
+
+ {value}
+
+ hr
+
+
+
+ )}
+ />
+
+
+
+
+
+
+ );
+}
diff --git a/components/logo.tsx b/components/logo.tsx
new file mode 100644
index 0000000..59cf246
--- /dev/null
+++ b/components/logo.tsx
@@ -0,0 +1,8 @@
+import { cn } from "@/lib/utils";
+import { SiriIcon } from "hugeicons-react";
+
+interface LogoProps extends React.HTMLAttributes {}
+
+export default function Logo({ className }: LogoProps) {
+ return ;
+}
diff --git a/components/markdown.tsx b/components/markdown.tsx
new file mode 100644
index 0000000..9207808
--- /dev/null
+++ b/components/markdown.tsx
@@ -0,0 +1,9 @@
+import { FC, memo } from "react";
+import ReactMarkdown, { Options } from "react-markdown";
+
+export const MemoizedReactMarkdown: FC = memo(
+ ReactMarkdown,
+ (prevProps, nextProps) =>
+ prevProps.children === nextProps.children &&
+ prevProps.className === nextProps.className,
+);
diff --git a/components/marketing/background.tsx b/components/marketing/background.tsx
new file mode 100644
index 0000000..d643816
--- /dev/null
+++ b/components/marketing/background.tsx
@@ -0,0 +1,130 @@
+"use client";
+
+import { cn } from "@/lib/utils";
+import { motion } from "framer-motion";
+import React, { useId } from "react";
+
+export const Background = () => {
+ return (
+
+
+ {Array.from({ length: 6 }).map((_, index) => (
+
+ {Array.from({ length: 10 }).map((_, index) => (
+
+ ))}
+
+ ))}
+
+ );
+};
+
+const GridBlock = () => {
+ return (
+
+ );
+};
+
+const Dot = () => {
+ return (
+
+ );
+};
+
+const SVGVertical = ({ className }: { className?: string }) => {
+ const width = 1;
+ const height = 140;
+
+ const id = useId();
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const SVG = ({ className }: { className?: string }) => {
+ const width = 300;
+ const height = 1;
+
+ const id = useId();
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+// Use the below rect to debug linear gradient
+{
+ /* */
+}
diff --git a/components/marketing/badge.tsx b/components/marketing/badge.tsx
new file mode 100644
index 0000000..fb6b6e5
--- /dev/null
+++ b/components/marketing/badge.tsx
@@ -0,0 +1,35 @@
+import React from "react";
+
+export const Badge: React.FC<
+ { children: React.ReactNode } & React.ComponentPropsWithoutRef<"button">
+> = ({ children, ...props }) => {
+ return (
+
+
+
+
+
+
+
+ );
+};
diff --git a/components/marketing/blur-image.tsx b/components/marketing/blur-image.tsx
new file mode 100644
index 0000000..e2680d7
--- /dev/null
+++ b/components/marketing/blur-image.tsx
@@ -0,0 +1,48 @@
+"use client";
+
+import clsx from "clsx";
+import Image from "next/image";
+import React, { useState } from "react";
+
+interface IBlurImage {
+ height?: any;
+ width?: any;
+ src?: string | any;
+ objectFit?: any;
+ className?: string | any;
+ alt?: string | undefined;
+ layout?: any;
+ [x: string]: any;
+}
+
+export const BlurImage = ({
+ height,
+ width,
+ src,
+ className,
+ objectFit,
+ alt,
+ layout,
+ ...rest
+}: IBlurImage) => {
+ const [isLoading, setLoading] = useState(true);
+ return (
+ setLoading(false)}
+ src={src}
+ width={width}
+ height={height}
+ loading="lazy"
+ decoding="async"
+ blurDataURL={src}
+ layout={layout}
+ alt={alt ? alt : "Avatar"}
+ {...rest}
+ />
+ );
+};
diff --git a/components/marketing/button.tsx b/components/marketing/button.tsx
new file mode 100644
index 0000000..d2df45d
--- /dev/null
+++ b/components/marketing/button.tsx
@@ -0,0 +1,41 @@
+import { cn } from "@/lib/utils";
+import React from "react";
+
+export const Button: React.FC<{
+ children?: React.ReactNode;
+ className?: string;
+ variant?: "simple" | "outline" | "primary";
+ as?: React.ElementType;
+ [x: string]: any;
+}> = ({
+ children,
+ className,
+ variant = "primary",
+ as: Tag = "button",
+ ...props
+}) => {
+ const variantClass =
+ variant === "simple"
+ ? "bg-black relative z-10 bg-transparent hover:bg-gray-100 border border-transparent text-black text-sm md:text-sm transition font-medium duration-200 rounded-full px-4 py-2 flex items-center justify-center dark:text-white dark:hover:bg-neutral-800 dark:hover:shadow-xl"
+ : variant === "outline"
+ ? "bg-white relative z-10 hover:bg-black/90 hover:shadow-xl text-black border border-black hover:text-white text-sm md:text-sm transition font-medium duration-200 rounded-full px-4 py-2 flex items-center justify-center"
+ : variant === "primary"
+ ? "bg-neutral-900 relative z-10 hover:bg-black/90 border border-transparent text-white text-sm md:text-sm transition font-medium duration-200 rounded-full px-4 py-2 flex items-center justify-center shadow-[0px_-1px_0px_0px_#FFFFFF40_inset,_0px_1px_0px_0px_#FFFFFF40_inset]"
+ : "";
+ return (
+
+ {children ?? `Get Started`}
+
+ );
+};
diff --git a/components/marketing/common.module.css b/components/marketing/common.module.css
new file mode 100644
index 0000000..14a1def
--- /dev/null
+++ b/components/marketing/common.module.css
@@ -0,0 +1,159 @@
+.gridLineHorizontal {
+ --background: #ffffff;
+ --color: rgba(0, 0, 0, 0.2);
+ --height: 1px;
+ --width: 5px;
+ --fade-stop: 90%;
+ /* Bleed in or out from the container */
+ --offset: -100px;
+
+ position: absolute;
+ width: calc(100% + var(--offset));
+ height: var(--height);
+ left: calc(var(--offset) / 2 * -1);
+
+ background: linear-gradient(
+ to right,
+ var(--color),
+ var(--color) 50%,
+ transparent 0,
+ transparent
+ );
+ background-size: var(--width) var(--height);
+
+ /* Fade out the edges */
+ mask: linear-gradient(
+ to left,
+ var(--background) var(--fade-stop),
+ transparent
+ ),
+ linear-gradient(to right, var(--background) var(--fade-stop), transparent),
+ linear-gradient(black, black);
+ -webkit-mask: linear-gradient(
+ to left,
+ var(--background) var(--fade-stop),
+ transparent
+ ),
+ linear-gradient(to right, var(--background) var(--fade-stop), transparent),
+ linear-gradient(black, black);
+ mask-composite: exclude;
+ -webkit-mask-composite: exclude;
+ z-index: 20;
+}
+
+.gridLineHorizontalDark {
+ --background: #ffffff;
+ --color: rgba(255, 255, 255, 0.2);
+ --height: 1px;
+ --width: 5px;
+ --fade-stop: 90%;
+ /* Bleed in or out from the container */
+ --offset: -100px;
+
+ position: absolute;
+ width: calc(100% + var(--offset));
+ height: var(--height);
+ left: calc(var(--offset) / 2 * -1);
+
+ background: linear-gradient(
+ to right,
+ var(--color),
+ var(--color) 50%,
+ transparent 0,
+ transparent
+ );
+ background-size: var(--width) var(--height);
+
+ /* Fade out the edges */
+ mask: linear-gradient(
+ to left,
+ var(--background) var(--fade-stop),
+ transparent
+ ),
+ linear-gradient(to right, var(--background) var(--fade-stop), transparent),
+ linear-gradient(black, black);
+ -webkit-mask: linear-gradient(
+ to left,
+ var(--background) var(--fade-stop),
+ transparent
+ ),
+ linear-gradient(to right, var(--background) var(--fade-stop), transparent),
+ linear-gradient(black, black);
+ mask-composite: exclude;
+ -webkit-mask-composite: exclude;
+ z-index: 20;
+}
+
+.gridLineVertical {
+ --background: #ffffff;
+ --color: rgba(0, 0, 0, 0.2);
+ --height: 5px;
+ --width: 1px;
+ --fade-stop: 90%;
+ --offset: -100px;
+
+ position: absolute;
+ height: calc(100% + var(--offset));
+ width: var(--width);
+ top: calc(var(--offset) / 2 * -1);
+
+ background: linear-gradient(
+ to bottom,
+ var(--color),
+ var(--color) 50%,
+ transparent 0,
+ transparent
+ );
+ background-size: var(--width) var(--height);
+
+ mask: linear-gradient(to top, var(--background) var(--fade-stop), transparent),
+ linear-gradient(to bottom, var(--background) var(--fade-stop), transparent),
+ linear-gradient(black, black);
+ -webkit-mask: linear-gradient(
+ to top,
+ var(--background) var(--fade-stop),
+ transparent
+ ),
+ linear-gradient(to bottom, var(--background) var(--fade-stop), transparent),
+ linear-gradient(black, black);
+ mask-composite: exclude;
+ -webkit-mask-composite: exclude;
+ z-index: 20;
+}
+
+.gridLineVerticalDark {
+ --background: #ffffff;
+ --color: rgba(255, 255, 255, 0.2);
+ --height: 5px;
+ --width: 1px;
+ --fade-stop: 90%;
+ --offset: -100px;
+
+ position: absolute;
+ height: calc(100% + var(--offset));
+ width: var(--width);
+ top: calc(var(--offset) / 2 * -1);
+
+ background: linear-gradient(
+ to bottom,
+ var(--color),
+ var(--color) 50%,
+ transparent 0,
+ transparent
+ );
+ background-size: var(--width) var(--height);
+
+ mask: linear-gradient(to top, var(--background) var(--fade-stop), transparent),
+ linear-gradient(to bottom, var(--background) var(--fade-stop), transparent),
+ linear-gradient(black, black);
+ -webkit-mask: linear-gradient(
+ to top,
+ var(--background) var(--fade-stop),
+ transparent
+ ),
+ linear-gradient(to bottom, var(--background) var(--fade-stop), transparent),
+ linear-gradient(black, black);
+ mask-composite: exclude;
+ -webkit-mask-composite: exclude;
+ z-index: 20;
+}
diff --git a/components/marketing/companies.tsx b/components/marketing/companies.tsx
new file mode 100644
index 0000000..c5fe34d
--- /dev/null
+++ b/components/marketing/companies.tsx
@@ -0,0 +1,161 @@
+"use client";
+import { useEffect, useState } from "react";
+import Image from "next/image";
+import { AnimatePresence, motion } from "framer-motion";
+
+interface Logo {
+ title: string;
+ src: string;
+}
+
+export const Companies = () => {
+ let [logos, setLogos] = useState([
+ [
+ {
+ title: "Raiffeisen Software",
+ src: "/assets/logos/ossbig/raiffeisen-software.png",
+ },
+ {
+ title: "Österreichische Nationalbank",
+ src: "/assets/logos/ossbig/oesterreichische-nationalbank.png",
+ },
+ {
+ title: "Twinformatics",
+ src: "/assets/logos/ossbig/twinformatics.png",
+ },
+ {
+ title: "Österreichische Lotterien",
+ src: "/assets/logos/ossbig/oesterreichische-lotterien.png",
+ },
+ {
+ title: "Styria Media IT",
+ src: "/assets/logos/ossbig/styria-media-group.png",
+ },
+ ],
+ [
+ {
+ title: "Bundeskanzleramt",
+ src: "/assets/logos/ossbig/bundeskanzleramt.png",
+ },
+ {
+ title: "BRZ",
+ src: "/assets/logos/ossbig/brz.png",
+ },
+ {
+ title: "ITSV",
+ src: "/assets/logos/ossbig/itsv.png",
+ },
+ {
+ title: "Magna Group",
+ src: "/assets/logos/ossbig/magna.png",
+ },
+ {
+ title: "Stadt Wien",
+ src: "/assets/logos/ossbig/stadt-wien.png",
+ },
+ ],
+ ]);
+ const [activeLogoSet, setActiveLogoSet] = useState(logos[0]);
+ const [isAnimating, setIsAnimating] = useState(false);
+
+ const flipLogos = () => {
+ setLogos((currentLogos) => {
+ const newLogos = [...currentLogos.slice(1), currentLogos[0]];
+ setActiveLogoSet(newLogos[0]);
+ setIsAnimating(true);
+ return newLogos;
+ });
+ };
+
+ useEffect(() => {
+ if (!isAnimating) {
+ const timer = setTimeout(() => {
+ flipLogos();
+ }, 3000);
+ return () => clearTimeout(timer); // Clear timeout if component unmounts or isAnimating changes
+ }
+ }, [isAnimating]);
+
+ return (
+
+
+
+ Task Force
+
+
+
+
+
+ "AI Assisted Software Development"
+
+
+
+
+ With participants from
+
+
+ Other participants of the Task Force "AI Assisted Software
+ Development"
+
+
+
+
{
+ setIsAnimating(false);
+ }}
+ >
+ {activeLogoSet.map((logo, idx) => (
+
+
+
+ ))}
+
+
+
+ );
+};
diff --git a/components/marketing/cta.tsx b/components/marketing/cta.tsx
new file mode 100644
index 0000000..483142a
--- /dev/null
+++ b/components/marketing/cta.tsx
@@ -0,0 +1,53 @@
+"use client";
+import React from "react";
+import Balancer from "react-wrap-balancer";
+import { Button } from "@/components/marketing/button";
+import { Link } from "next-view-transitions";
+
+export const CTA = () => {
+ return (
+
+
+
+
+
+
+
+
+
+ Ready to ship your project?
+
+
+
+ Synth UI is available now, signup and get instant access.
+
+
+
+
+
+ Get Started
+
+
+
+
+
+
+
+ );
+};
diff --git a/components/marketing/features.tsx b/components/marketing/features.tsx
new file mode 100644
index 0000000..9c5bdae
--- /dev/null
+++ b/components/marketing/features.tsx
@@ -0,0 +1,116 @@
+import React from "react";
+import { cn } from "@/lib/utils";
+import {
+ GridLineHorizontal,
+ GridLineVertical,
+} from "@/components/marketing/grid-lines";
+import { SkeletonOne } from "@/components/marketing/skeletons/first";
+import { SkeletonTwo } from "@/components/marketing/skeletons/second";
+import { SkeletonFour } from "@/components/marketing/skeletons/fourth";
+import { SkeletonThree } from "@/components/marketing/skeletons/third";
+
+export const Features = () => {
+ const features = [
+ {
+ title: "Generate User Interfaces with plain text",
+ description:
+ "Describe your UI in plain text and let the AI generate efficient, reusable, and customizable React Code.",
+ skeleton: ,
+ className:
+ "col-span-1 md:col-span-4 border-b border-r dark:border-neutral-800",
+ },
+ {
+ title: "Speak to Synth UI like to a Senior Developer",
+ description:
+ "Synth UI is designed to be a senior developer. It can help you with complex logic, design, and even write unit tests and documentation.",
+ skeleton: ,
+ className: "border-b col-span-1 md:col-span-2 dark:border-neutral-800",
+ },
+ {
+ title: "Synth UI supports almost all LLMs",
+ description:
+ "Wether it's OpenAI, GroQ or Your own custom LLM, we support (almost) everything.",
+ skeleton: ,
+ className: "col-span-1 md:col-span-3 border-r dark:border-neutral-800",
+ },
+ {
+ title: "Make use of established UI libraries",
+ description:
+ "Synth UI is designed to work with existing design systems. By default, Synth UI uses Shadcn/UI and TailwindCSS, but you can use any other library or custom components.",
+ skeleton: ,
+ className: "col-span-1 md:col-span-3",
+ },
+ ];
+ return (
+
+
Synth UI Features
+
+ Synth UI makes it easy to generate and ship UI with minimal effort.
+
+
+
+
+ {features.map((feature) => (
+
+ {feature.title}
+ {feature.description}
+ {feature.skeleton}
+
+ ))}
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const FeatureCard = ({
+ children,
+ className,
+}: {
+ children?: React.ReactNode;
+ className?: string;
+}) => {
+ return (
+
+ {children}
+
+ );
+};
+
+const FeatureTitle = ({ children }: { children?: React.ReactNode }) => {
+ return {children} ;
+};
+
+const FeatureDescription = ({ children }: { children?: React.ReactNode }) => {
+ return {children}
;
+};
diff --git a/components/marketing/footer.tsx b/components/marketing/footer.tsx
new file mode 100644
index 0000000..411f284
--- /dev/null
+++ b/components/marketing/footer.tsx
@@ -0,0 +1,52 @@
+import Link from "next/link";
+import React from "react";
+import Logo from "@/components/logo";
+
+export const Footer = () => {
+ const legal = [
+ {
+ name: "Privacy Policy",
+ href: "#",
+ },
+ {
+ name: "Terms of Service",
+ href: "#",
+ },
+ {
+ name: "Source Code",
+ href: "process.env.NEXT_PUBLIC_GITHUB_URL",
+ },
+ ];
+
+ return (
+
+
+
+
+
+
+
+
Copyright © 2024 Synth UI
+
All rights reserved
+
+
+
+ {legal.map((link) => (
+
+ {link.name}
+
+ ))}
+
+
+
+
+ {/*
+ SYNTH UI
+
*/}
+
+ );
+};
diff --git a/components/marketing/globe.tsx b/components/marketing/globe.tsx
new file mode 100644
index 0000000..e262835
--- /dev/null
+++ b/components/marketing/globe.tsx
@@ -0,0 +1,53 @@
+"use client";
+import createGlobe from "cobe";
+import { useEffect, useRef } from "react";
+
+// https://github.com/shuding/cobe
+export const Globe = ({ className }: { className?: string }) => {
+ const canvasRef = useRef(null);
+
+ useEffect(() => {
+ let phi = 0;
+
+ if (!canvasRef.current) return;
+
+ const globe = createGlobe(canvasRef.current, {
+ devicePixelRatio: 2,
+ width: 600 * 2,
+ height: 600 * 2,
+ phi: 0,
+ theta: 0,
+ dark: 0,
+ diffuse: 1.2,
+ mapSamples: 16000,
+ mapBrightness: 6,
+ baseColor: [0.9, 0.9, 0.9],
+ markerColor: [0.96, 0.96, 0.96],
+ glowColor: [0.88, 0.88, 0.88],
+ markers: [
+ // longitude latitude
+ { location: [37.7595, -122.4367], size: 0.03 },
+ { location: [40.7128, -74.006], size: 0.1 },
+ { location: [48.2081, 16.3713], size: 0.1 },
+ ],
+ onRender: (state) => {
+ // Called on every animation frame.
+ // `state` will be an empty object, return updated params.
+ state.phi = phi;
+ phi += 0.01;
+ },
+ });
+
+ return () => {
+ globe.destroy();
+ };
+ }, []);
+
+ return (
+
+ );
+};
diff --git a/components/marketing/grid-features.tsx b/components/marketing/grid-features.tsx
new file mode 100644
index 0000000..fc8a450
--- /dev/null
+++ b/components/marketing/grid-features.tsx
@@ -0,0 +1,105 @@
+import { cn } from "@/lib/utils";
+
+import {
+ SourceCodeIcon,
+ Dollar01Icon,
+ TradeUpIcon,
+ CloudIcon,
+ Compass01Icon,
+ HelpCircleIcon,
+ SquareLock01Icon,
+ FavouriteIcon,
+} from "hugeicons-react";
+
+export const GridFeatures = () => {
+ const features = [
+ {
+ title: "Built for developers",
+ description: "Built for engineers, developers, dreamers...",
+ icon: ,
+ },
+ {
+ title: "Ease of use",
+ description: "It's as easy as writing down your design dreams.",
+ icon: ,
+ },
+ {
+ title: "Pricing like no other",
+ description: "Open source, free forever. No strings attached.",
+ icon: ,
+ },
+ {
+ title: "Cloud Native",
+ description: "Built for the cloud, run anywhere.",
+ icon: ,
+ },
+ {
+ title: "Multi-tenant Architecture",
+ description: "Run multiple requests in parallel with only a single API.",
+ icon: ,
+ },
+ {
+ title: "24/7 Customer Support",
+ description:
+ "Synth UI support is available 24/7. Atleast our AI Agents are.",
+ icon: ,
+ },
+ {
+ title: "Secure by default",
+ description: "Bring along your own LLM, Databases, Proxies etc.",
+ icon: ,
+ },
+ {
+ title: "And everything else",
+ description:
+ "Synth UI's infrastructure is built for scalability and reliability.",
+ icon: ,
+ },
+ ];
+ return (
+
+ {features.map((feature, index) => (
+
+ ))}
+
+ );
+};
+
+const Feature = ({
+ title,
+ description,
+ icon,
+ index,
+}: {
+ title: string;
+ description: string;
+ icon: React.ReactNode;
+ index: number;
+}) => {
+ return (
+
+ {index < 4 && (
+
+ )}
+ {index >= 4 && (
+
+ )}
+
{icon}
+
+
+ {description}
+
+
+ );
+};
diff --git a/components/marketing/grid-lines.tsx b/components/marketing/grid-lines.tsx
new file mode 100644
index 0000000..49eedbe
--- /dev/null
+++ b/components/marketing/grid-lines.tsx
@@ -0,0 +1,32 @@
+import { cn } from "@/lib/utils";
+import styles from "./common.module.css";
+
+export const GridLineHorizontal = ({ ...props }) => {
+ return (
+ <>
+
+
+ >
+ );
+};
+
+export const GridLineVertical = ({ ...props }) => {
+ return (
+ <>
+
+
+ >
+ );
+};
diff --git a/components/marketing/hero.tsx b/components/marketing/hero.tsx
new file mode 100644
index 0000000..1aa82bc
--- /dev/null
+++ b/components/marketing/hero.tsx
@@ -0,0 +1,176 @@
+"use client";
+
+import Balancer from "react-wrap-balancer";
+import { Button } from "@/components/marketing/button";
+import { Badge } from "@/components/marketing/badge";
+import { motion } from "framer-motion";
+
+import Image from "next/image";
+import { Link } from "next-view-transitions";
+import { ArrowRight02Icon, GithubIcon } from "hugeicons-react";
+import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
+import { useEffect, useState } from "react";
+
+export const Hero = () => {
+ const [isMounted, setIsMounted] = useState(false);
+
+ useEffect(() => {
+ setIsMounted(true);
+ }, []);
+
+ return (
+
+
+
+
+
+
+ Proudly Open Source
+
+
+
+
+
+ Generate & Ship UI with minimal effort
+
+
+
+ Synth UI allows you to generate User-Interfaces without writing a
+ single line of code.
+
+
+
+
+ Get Started
+
+
+ Source Code
+
+
+
+
+
+
+ {isMounted && (
+
+
+ <>
+
+
+
+
+ Watch Video - 00:28
+
+
+ >
+
+
+
+
+
+
+
+ )}
+
+
+
+ );
+};
diff --git a/components/marketing/horizontal-gradient.tsx b/components/marketing/horizontal-gradient.tsx
new file mode 100644
index 0000000..ef3d50e
--- /dev/null
+++ b/components/marketing/horizontal-gradient.tsx
@@ -0,0 +1,49 @@
+"use client";
+import { cn } from "@/lib/utils";
+import { useId } from "react";
+
+export const HorizontalGradient = ({
+ className,
+ ...props
+}: {
+ className: string;
+ [x: string]: any;
+}) => {
+ const id = useId();
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/components/marketing/navbar/desktop-navbar.tsx b/components/marketing/navbar/desktop-navbar.tsx
new file mode 100644
index 0000000..4fc69ac
--- /dev/null
+++ b/components/marketing/navbar/desktop-navbar.tsx
@@ -0,0 +1,118 @@
+"use client";
+
+import Logo from "@/components/logo";
+import { Button } from "@/components/marketing/button";
+import { NavBarItem } from "@/components/marketing/navbar/navbar-item";
+import {
+ useMotionValueEvent,
+ useScroll,
+ motion,
+ AnimatePresence,
+} from "framer-motion";
+import { cn } from "@/lib/utils";
+import React, { useState } from "react";
+import { Link } from "next-view-transitions";
+import { SignedIn, SignedOut } from "@clerk/nextjs";
+import { ModeToggle } from "@/components/mode-toggle";
+import { GithubIcon } from "hugeicons-react";
+import { Separator } from "@/components/ui/separator";
+
+export interface NavItem {
+ icon?: React.ReactNode;
+ link: string;
+ title: string;
+ target?: "_blank";
+}
+
+type Props = {
+ navItems: NavItem[];
+};
+
+export const DesktopNavbar = ({ navItems }: Props) => {
+ const { scrollY } = useScroll();
+
+ const [showBackground, setShowBackground] = useState(false);
+
+ scrollY.on("change", (v) => {
+ console.log(v);
+ });
+
+ useMotionValueEvent(scrollY, "change", (value) => {
+ console.log(value);
+
+ if (value > 100) {
+ setShowBackground(true);
+ } else {
+ setShowBackground(false);
+ }
+ });
+
+ return (
+
+
+ {showBackground && (
+
+ )}
+
+
+
+
+ Synth UI
+
+
+ {navItems.map((item, index) => (
+ <>
+
+ {item.icon}
+ {item.title}
+
+ {index !== navItems.length - 1 && (
+
+ )}
+ >
+ ))}
+
+
+
+
+
+
+ Github
+
+
+
+ Dashboard
+
+
+
+
+ Login
+
+
+
+
+ );
+};
diff --git a/components/marketing/navbar/index.tsx b/components/marketing/navbar/index.tsx
new file mode 100644
index 0000000..90cf011
--- /dev/null
+++ b/components/marketing/navbar/index.tsx
@@ -0,0 +1,68 @@
+"use client";
+import {
+ DesktopNavbar,
+ NavItem,
+} from "@/components/marketing/navbar/desktop-navbar";
+import { MobileNavbar } from "@/components/marketing/navbar/mobile-navbar";
+import { motion } from "framer-motion";
+import { Blockchain01Icon, MagicWand01Icon, TableIcon } from "hugeicons-react";
+
+const navItems: NavItem[] = [
+ {
+ icon: ,
+ link: "#features",
+ title: "Features",
+ },
+ {
+ icon: ,
+ link: "process.env.NEXT_PUBLIC_GITHUB_URL",
+ title: "Synthui.v1 Model",
+ target: "_blank",
+ },
+ {
+ icon: ,
+ link: "https://huggingface.co/datasets/JulianAT/SynthUI-Code-2k-v1",
+ title: "v1 Dataset",
+ target: "_blank",
+ },
+ {
+ icon: ,
+ link: "https://huggingface.co/datasets/JulianAT/SynthUI-Code-Instruct-2k-v1",
+ title: "v1 Dataset (Instruct)",
+ target: "_blank",
+ },
+];
+
+export function NavBar() {
+ return (
+
+
+
+
+
+
+
+
+ );
+}
+
+{
+ /*
+
+
+
+
+
*/
+}
diff --git a/components/marketing/navbar/mobile-navbar.tsx b/components/marketing/navbar/mobile-navbar.tsx
new file mode 100644
index 0000000..8fffb8c
--- /dev/null
+++ b/components/marketing/navbar/mobile-navbar.tsx
@@ -0,0 +1,120 @@
+"use client";
+import { cn } from "@/lib/utils";
+import { Link } from "next-view-transitions";
+import { useState } from "react";
+import { Button } from "@/components/marketing/button";
+import Logo from "@/components/logo";
+import { useMotionValueEvent, useScroll, motion } from "framer-motion";
+import { Menu01Icon, Cancel01Icon, GithubIcon } from "hugeicons-react";
+import { SignedIn, SignedOut } from "@clerk/nextjs";
+import { ModeToggle } from "@/components/mode-toggle";
+
+export const MobileNavbar = ({ navItems }: any) => {
+ const [open, setOpen] = useState(false);
+ const [showBackground, setShowBackground] = useState(false);
+
+ const { scrollY } = useScroll();
+
+ useMotionValueEvent(scrollY, "change", (latest) => {
+ console.log(latest);
+
+ if (latest > 100) {
+ setShowBackground(true);
+ } else {
+ setShowBackground(false);
+ }
+ });
+
+ return (
+
+
+ setOpen(!open)}
+ />
+ {open && (
+
+
+
+
+
+ setOpen(!open)}
+ />
+
+
+
+ {navItems.map((navItem: any, idx: number) => (
+ <>
+ {navItem.children && navItem.children.length > 0 ? (
+ <>
+ {navItem.children.map((childNavItem: any, idx: number) => (
+
setOpen(false)}
+ className="relative max-w-[15rem] text-left text-2xl"
+ >
+
+ {childNavItem.title}
+
+
+ ))}
+ >
+ ) : (
+
setOpen(false)}
+ className="relative"
+ >
+
+
+ {navItem.icon}
+
+ {navItem.title}
+
+
+ )}
+ >
+ ))}
+
+
+
+
+ Dashboard
+
+
+
+
+ Login
+
+
+
+
+ Github
+
+
+
+ )}
+
+ );
+};
diff --git a/components/marketing/navbar/navbar-item.tsx b/components/marketing/navbar/navbar-item.tsx
new file mode 100644
index 0000000..0d37d80
--- /dev/null
+++ b/components/marketing/navbar/navbar-item.tsx
@@ -0,0 +1,39 @@
+"use client";
+
+import { cn } from "@/lib/utils";
+import { Link } from "next-view-transitions";
+import { ReactNode } from "react";
+import { usePathname } from "next/navigation";
+import { buttonVariants } from "@/components/ui/button";
+
+type Props = {
+ href: string;
+ children: ReactNode;
+ active?: boolean;
+ className?: string;
+ target?: "_blank";
+};
+
+export function NavBarItem({
+ children,
+ href,
+ active,
+ target,
+ className,
+}: Props) {
+ const pathname = usePathname();
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/components/marketing/skeletons/first.tsx b/components/marketing/skeletons/first.tsx
new file mode 100644
index 0000000..28b4e58
--- /dev/null
+++ b/components/marketing/skeletons/first.tsx
@@ -0,0 +1,334 @@
+"use client";
+import React from "react";
+import { motion } from "framer-motion";
+import Image from "next/image";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import Logo from "@/components/logo";
+import {
+ ChartContainer,
+ ChartTooltip,
+ ChartTooltipContent,
+} from "@/components/ui/chart";
+import {
+ Area,
+ AreaChart,
+ Bar,
+ BarChart,
+ LabelList,
+ XAxis,
+ YAxis,
+} from "recharts";
+
+export const SkeletonOne = () => {
+ return (
+
+
+
+
+ Hey! I want to make a landing page for a side project of mine
+
+
+ Certainly, I'm here to help you with that. What category does
+ your side project fall under?
+
+
+ My side project is a blog about AI. I write about AI and how it
+ affects our lives.
+
+
+ Sure, I can help you with that. What design do you want the website
+ to have?
+
+
+ It should be a simple, modern and sleek design. I want it to be
+ minimalistic and modern
+
+
+ I'll generate a design for you. Please give me a moment.
+
+
+
+
+
+
+
+
+ Productivity
+
+ Approximate number of lines of code written within a month
+
+
+
+
+
+ 20k
+
+ with Synth UI
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 7.5k
+
+ by hand
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Time saved with
+ Synth UI
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+ formatter={(value: any) => (
+
+ Time saved
+
+ {value}
+
+ hr
+
+
+
+ )}
+ />
+
+
+
+
+
+
+
+
+ );
+};
+
+const UserMessage = ({ children }: { children: React.ReactNode }) => {
+ const variants = {
+ initial: {
+ x: 0,
+ },
+ animate: {
+ x: 5,
+ transition: {
+ duration: 0.2,
+ },
+ },
+ };
+ return (
+
+
+ {children}
+
+ );
+};
+
+const AIMessage = ({ children }: { children: React.ReactNode }) => {
+ const variantsSecond = {
+ initial: {
+ x: 0,
+ },
+ animate: {
+ x: 10,
+ transition: {
+ duration: 0.2,
+ },
+ },
+ };
+ return (
+
+
+ {children}
+
+ );
+};
diff --git a/components/marketing/skeletons/fourth.tsx b/components/marketing/skeletons/fourth.tsx
new file mode 100644
index 0000000..738926a
--- /dev/null
+++ b/components/marketing/skeletons/fourth.tsx
@@ -0,0 +1,114 @@
+import React from "react";
+import { cn } from "@/lib/utils";
+import { InfiniteMovingCards } from "@/components/ui/infinite-moving-cards";
+import { Globe } from "@/components/marketing/globe";
+import Image from "next/image";
+export const SkeletonFour = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const MovingGrid = () => {
+ return (
+
+
+
+ Aceternity UI
+
+
+
+ NextUI
+
+
+
+ file_type_tailwind
+
+
+ Tailwind CSS
+
+
+
+ Framer Motion
+
+
+
+ Magic UI
+
+
+
+ shadcn/ui
+
+
+ );
+};
diff --git a/components/marketing/skeletons/second.tsx b/components/marketing/skeletons/second.tsx
new file mode 100644
index 0000000..fd63375
--- /dev/null
+++ b/components/marketing/skeletons/second.tsx
@@ -0,0 +1,71 @@
+"use client";
+import { stagger, useAnimate } from "framer-motion";
+import React, { useState } from "react";
+
+export const SkeletonTwo = () => {
+ const [scope, animate] = useAnimate();
+ const [animating, setAnimating] = useState(false);
+
+ const handleAnimation = async () => {
+ if (animating) return;
+
+ setAnimating(true);
+ await animate(
+ ".message",
+ {
+ opacity: [0, 1],
+ y: [20, 0],
+ },
+ {
+ delay: stagger(0.5),
+ },
+ );
+ setAnimating(false);
+ };
+ return (
+
+
+
+
+
+
+
+ Hello Synth UI! Can you help me build a login page?
+
+
+ Of course! What do you want the login page to look like?
+
+
+ Umm... I want it to be a simple login page with a username and
+ password input field.
+
+
And what should the submit button do?
+
+ The submit button should set a loading state and then redirect the
+ user to the home page.
+
+
+
+
+
+ );
+};
+
+const UserMessage = ({ children }: { children: React.ReactNode }) => {
+ return (
+
+ {children}
+
+ );
+};
+const AIMessage = ({ children }: { children: React.ReactNode }) => {
+ return (
+
+ {children}
+
+ );
+};
diff --git a/components/marketing/skeletons/third.tsx b/components/marketing/skeletons/third.tsx
new file mode 100644
index 0000000..253f889
--- /dev/null
+++ b/components/marketing/skeletons/third.tsx
@@ -0,0 +1,55 @@
+"use client";
+import React, { useState } from "react";
+import { Switch } from "@/components/ui/switch";
+import { MoreHorizontalIcon, PlusSignIcon } from "hugeicons-react";
+
+export const SkeletonThree = () => {
+ return (
+
+
+
+
+
+
Add LLM
+
+ {" "}
+ Add
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export const Row = ({
+ title,
+ updatedAt,
+ active = false,
+}: {
+ title: string;
+ updatedAt: string;
+ active?: boolean;
+}) => {
+ const [checked, setChecked] = useState(active);
+ return (
+
+
+
+ {title}
+
+
{updatedAt}
+
+
+
+
+
+
+ );
+};
diff --git a/components/mode-toggle.tsx b/components/mode-toggle.tsx
new file mode 100644
index 0000000..469b3fe
--- /dev/null
+++ b/components/mode-toggle.tsx
@@ -0,0 +1,72 @@
+"use client";
+
+import * as React from "react";
+import { useTheme } from "next-themes";
+import { motion } from "framer-motion";
+import { Moon02Icon, Sun02Icon } from "hugeicons-react";
+import { Button } from "@/components/ui/button";
+
+export function ModeToggle({ children }: { children?: React.ReactNode }) {
+ const { theme, setTheme } = useTheme();
+
+ const [isClient, setIsClient] = React.useState(false);
+
+ React.useEffect(() => {
+ setIsClient(true);
+ }, []);
+
+ return (
+ isClient && (
+ {
+ theme === "dark" ? setTheme("light") : setTheme("dark");
+ }}
+ className="flex w-full items-center justify-start gap-2 px-3"
+ >
+ {theme === "light" && (
+
+
+
+ )}
+
+ {theme === "dark" && (
+
+
+
+ )}
+
+ Toggle theme
+ {children}
+
+ )
+ );
+}
diff --git a/components/not-implemented-dialog.tsx b/components/not-implemented-dialog.tsx
new file mode 100644
index 0000000..7107af2
--- /dev/null
+++ b/components/not-implemented-dialog.tsx
@@ -0,0 +1,44 @@
+import Link from "next/link";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog";
+import { cn } from "@/lib/utils";
+import { buttonVariants } from "@/components/ui/button";
+import { GithubIcon } from "hugeicons-react";
+
+interface NotImplementedDialogProps
+ extends React.HTMLAttributes {}
+
+export default function NotImplementedDialog({
+ children,
+}: NotImplementedDialogProps) {
+ return (
+
+ {children}
+
+ This feature has not been implemented yet
+
+ If you have any suggestions for new features, or want to contribute to
+ the project, please visit the GitHub repository.
+
+
+
+
+ GitHub
+
+
+
+
+ );
+}
diff --git a/components/prompt-form.tsx b/components/prompt-form.tsx
new file mode 100644
index 0000000..66fe739
--- /dev/null
+++ b/components/prompt-form.tsx
@@ -0,0 +1,146 @@
+"use client";
+
+import { useEnterSubmit } from "@/lib/hooks/use-enter-submit";
+import Textarea from "react-textarea-autosize";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from "@/components/ui/tooltip";
+import { Button } from "@/components/ui/button";
+import { ArrowUp02Icon, Attachment01Icon } from "hugeicons-react";
+import { useEffect, useRef, useState } from "react";
+import { useActions, useUIState } from "ai/rsc";
+import { AI } from "@/lib/ai/core";
+import { generateId } from "ai";
+import { TooltipButton } from "@/components/tooltip-button";
+import { UserMessage } from "@/components/chat-message";
+import { cn } from "@/lib/utils";
+import NotImplementedDialog from "@/components/not-implemented-dialog";
+
+interface PromptFormProps extends React.HTMLAttributes {
+ query?: string;
+}
+
+export default function PromptForm({ query, className }: PromptFormProps) {
+ const [isMounted, setIsMounted] = useState(false);
+ const [input, setInput] = useState("");
+ const [, setMessages] = useUIState();
+ const [isGenerating, setIsGenerating] = useState(false);
+ const { submitUserMessage } = useActions();
+ const inputRef = useRef(null);
+ const isFirstRender = useRef(true);
+ const { onKeyDown, formRef } = useEnterSubmit();
+
+ useEffect(() => {
+ setIsMounted(true);
+ }, []);
+
+ async function handleQuerySubmit(query: string, formData?: FormData) {
+ setInput("");
+ setIsGenerating(true);
+
+ setMessages((currentMessages) => [
+ ...currentMessages,
+ {
+ id: generateId(),
+ display: {query} ,
+ },
+ ]);
+
+ // Submit and get response message
+ const data = formData || new FormData();
+ if (!formData) {
+ data.append("input", query);
+ }
+ const responseMessage = await submitUserMessage(data);
+ console.log("response", responseMessage);
+ setMessages((currentMessages) => [...currentMessages, responseMessage]);
+ }
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ const formData = new FormData(e.currentTarget);
+ formData.append("input", input);
+ await handleQuerySubmit(input, formData);
+ };
+
+ useEffect(() => {
+ if (isFirstRender.current && query && query.trim().length > 0) {
+ handleQuerySubmit(query);
+ isFirstRender.current = false;
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [query]);
+
+ useEffect(() => {
+ inputRef.current?.focus();
+ }, []);
+
+ if (query && query.trim().length > 0) {
+ return null;
+ }
+
+ return (
+
+ );
+}
diff --git a/components/providers.tsx b/components/providers.tsx
new file mode 100644
index 0000000..eb0304a
--- /dev/null
+++ b/components/providers.tsx
@@ -0,0 +1,31 @@
+"use client";
+
+import * as React from "react";
+import { ThemeProvider as NextThemesProvider, useTheme } from "next-themes";
+import { ThemeProviderProps } from "next-themes/dist/types";
+import { TooltipProvider } from "@/components/ui/tooltip";
+import { SidebarProvider } from "@/components/ui/sidebar";
+import { ClerkProvider } from "@clerk/nextjs";
+import { ComponentPreviewProvider } from "@/lib/hooks/use-component-preview";
+import { experimental__simple } from "@clerk/themes";
+import { AppStateProvider } from "@/lib/hooks/use-app-state";
+
+export function Providers({ children, ...props }: ThemeProviderProps) {
+ return (
+
+
+
+
+
+ {children}
+
+
+
+
+
+ );
+}
diff --git a/components/sidebar.tsx b/components/sidebar.tsx
new file mode 100644
index 0000000..d7ec625
--- /dev/null
+++ b/components/sidebar.tsx
@@ -0,0 +1,67 @@
+import {
+ Sidebar as AceternitySidebar,
+ SidebarBody,
+} from "@/components/ui/sidebar";
+import { PlusSignIcon, GithubIcon } from "hugeicons-react";
+import { buttonVariants } from "@/components/ui/button";
+import Link from "next/link";
+import { cn } from "@/lib/utils";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from "@/components/ui/tooltip";
+import UserPanel from "@/components/user-panel";
+import Logo from "@/components/logo";
+import ChatHistory from "@/components/chat-history-widget";
+
+export default function Sidebar() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ New Chat
+
+
+
+
+
+
+
+
+
+
+ Source Code
+
+
+ {/*
*/}
+
+
+
+
+
+ );
+}
diff --git a/components/spinner.tsx b/components/spinner.tsx
new file mode 100644
index 0000000..68e47cc
--- /dev/null
+++ b/components/spinner.tsx
@@ -0,0 +1,16 @@
+"use client";
+
+export const spinner = (
+
+
+
+);
diff --git a/components/tooltip-button.tsx b/components/tooltip-button.tsx
new file mode 100644
index 0000000..c59cc21
--- /dev/null
+++ b/components/tooltip-button.tsx
@@ -0,0 +1,37 @@
+import { VariantProps } from "class-variance-authority";
+import { Button, buttonVariants } from "@/components/ui/button";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from "@/components/ui/tooltip";
+
+interface TooltipButtonProps
+ extends React.ButtonHTMLAttributes,
+ VariantProps {
+ tooltip: string | React.ReactNode;
+ side?: "top" | "right" | "bottom" | "left";
+}
+
+export const TooltipButton = ({
+ children,
+ className,
+ tooltip,
+ variant,
+ size = "default",
+ side,
+ ...props
+}: TooltipButtonProps) => {
+ return (
+
+
+
+ {children}
+
+
+
+ {tooltip}
+
+
+ );
+};
diff --git a/components/ui/avatar.tsx b/components/ui/avatar.tsx
new file mode 100644
index 0000000..09cd14d
--- /dev/null
+++ b/components/ui/avatar.tsx
@@ -0,0 +1,50 @@
+"use client";
+
+import * as React from "react";
+import * as AvatarPrimitive from "@radix-ui/react-avatar";
+
+import { cn } from "@/lib/utils";
+
+const Avatar = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+Avatar.displayName = AvatarPrimitive.Root.displayName;
+
+const AvatarImage = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AvatarImage.displayName = AvatarPrimitive.Image.displayName;
+
+const AvatarFallback = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
+
+export { Avatar, AvatarImage, AvatarFallback };
diff --git a/components/ui/badge.tsx b/components/ui/badge.tsx
new file mode 100644
index 0000000..f795980
--- /dev/null
+++ b/components/ui/badge.tsx
@@ -0,0 +1,36 @@
+import * as React from "react";
+import { cva, type VariantProps } from "class-variance-authority";
+
+import { cn } from "@/lib/utils";
+
+const badgeVariants = cva(
+ "inline-flex items-center rounded-md border 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",
+ {
+ variants: {
+ variant: {
+ default:
+ "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
+ secondary:
+ "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ destructive:
+ "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
+ outline: "text-foreground",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ },
+);
+
+export interface BadgeProps
+ extends React.HTMLAttributes,
+ VariantProps {}
+
+function Badge({ className, variant, ...props }: BadgeProps) {
+ return (
+
+ );
+}
+
+export { Badge, badgeVariants };
diff --git a/components/ui/button.tsx b/components/ui/button.tsx
new file mode 100644
index 0000000..225e113
--- /dev/null
+++ b/components/ui/button.tsx
@@ -0,0 +1,57 @@
+import * as React from "react";
+import { Slot } from "@radix-ui/react-slot";
+import { cva, type VariantProps } from "class-variance-authority";
+
+import { cn } from "@/lib/utils";
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
+ {
+ variants: {
+ variant: {
+ default:
+ "bg-primary text-primary-foreground shadow hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
+ outline:
+ "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
+ secondary:
+ "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
+ ghost: "hover:bg-accent hover:text-accent-foreground",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-9 px-4 py-2",
+ sm: "h-8 rounded-md px-3 text-xs",
+ lg: "h-10 rounded-md px-8",
+ icon: "h-9 w-9",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ },
+);
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes,
+ VariantProps {
+ asChild?: boolean;
+}
+
+const Button = React.forwardRef(
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button";
+ return (
+
+ );
+ },
+);
+Button.displayName = "Button";
+
+export { Button, buttonVariants };
diff --git a/components/ui/calendar.tsx b/components/ui/calendar.tsx
new file mode 100644
index 0000000..018fe26
--- /dev/null
+++ b/components/ui/calendar.tsx
@@ -0,0 +1,72 @@
+"use client"
+
+import * as React from "react"
+import { ChevronLeftIcon, ChevronRightIcon } from "@radix-ui/react-icons"
+import { DayPicker } from "react-day-picker"
+
+import { cn } from "@/lib/utils"
+import { buttonVariants } from "@/components/ui/button"
+
+export type CalendarProps = React.ComponentProps
+
+function Calendar({
+ className,
+ classNames,
+ showOutsideDays = true,
+ ...props
+}: CalendarProps) {
+ return (
+ .day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
+ : "[&:has([aria-selected])]:rounded-md"
+ ),
+ day: cn(
+ buttonVariants({ variant: "ghost" }),
+ "h-8 w-8 p-0 font-normal aria-selected:opacity-100"
+ ),
+ day_range_start: "day-range-start",
+ day_range_end: "day-range-end",
+ day_selected:
+ "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
+ day_today: "bg-accent text-accent-foreground",
+ day_outside:
+ "day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30",
+ day_disabled: "text-muted-foreground opacity-50",
+ day_range_middle:
+ "aria-selected:bg-accent aria-selected:text-accent-foreground",
+ day_hidden: "invisible",
+ ...classNames,
+ }}
+ components={{
+ IconLeft: ({ ...props }) => ,
+ IconRight: ({ ...props }) => ,
+ }}
+ {...props}
+ />
+ )
+}
+Calendar.displayName = "Calendar"
+
+export { Calendar }
diff --git a/components/ui/card.tsx b/components/ui/card.tsx
new file mode 100644
index 0000000..77e9fb7
--- /dev/null
+++ b/components/ui/card.tsx
@@ -0,0 +1,76 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+const Card = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+Card.displayName = "Card"
+
+const CardHeader = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardHeader.displayName = "CardHeader"
+
+const CardTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardTitle.displayName = "CardTitle"
+
+const CardDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardDescription.displayName = "CardDescription"
+
+const CardContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardContent.displayName = "CardContent"
+
+const CardFooter = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardFooter.displayName = "CardFooter"
+
+export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
diff --git a/components/ui/chart.tsx b/components/ui/chart.tsx
new file mode 100644
index 0000000..0510f5b
--- /dev/null
+++ b/components/ui/chart.tsx
@@ -0,0 +1,370 @@
+"use client"
+
+import * as React from "react"
+import * as RechartsPrimitive from "recharts"
+import {
+ NameType,
+ Payload,
+ ValueType,
+} from "recharts/types/component/DefaultTooltipContent"
+
+import { cn } from "@/lib/utils"
+
+// Format: { THEME_NAME: CSS_SELECTOR }
+const THEMES = { light: "", dark: ".dark" } as const
+
+export type ChartConfig = {
+ [k in string]: {
+ label?: React.ReactNode
+ icon?: React.ComponentType
+ } & (
+ | { color?: string; theme?: never }
+ | { color?: never; theme: Record }
+ )
+}
+
+type ChartContextProps = {
+ config: ChartConfig
+}
+
+const ChartContext = React.createContext(null)
+
+function useChart() {
+ const context = React.useContext(ChartContext)
+
+ if (!context) {
+ throw new Error("useChart must be used within a ")
+ }
+
+ return context
+}
+
+const ChartContainer = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & {
+ config: ChartConfig
+ children: React.ComponentProps<
+ typeof RechartsPrimitive.ResponsiveContainer
+ >["children"]
+ }
+>(({ id, className, children, config, ...props }, ref) => {
+ const uniqueId = React.useId()
+ const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
+
+ return (
+
+
+
+
+ {children}
+
+
+
+ )
+})
+ChartContainer.displayName = "Chart"
+
+const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
+ const colorConfig = Object.entries(config).filter(
+ ([_, config]) => config.theme || config.color
+ )
+
+ if (!colorConfig.length) {
+ return null
+ }
+
+ return (
+