diff --git a/packages/create-llama/create-app.ts b/packages/create-llama/create-app.ts index f37c4c40ad12dfe94443d54619a8c15ace24586c..ea5a0ac815f40b0529aefd30e0555fd4c11edbb2 100644 --- a/packages/create-llama/create-app.ts +++ b/packages/create-llama/create-app.ts @@ -12,6 +12,7 @@ import type { TemplateEngine, TemplateFramework, TemplateType, + TemplateUI, } from "./templates"; import { installTemplate } from "./templates"; @@ -19,6 +20,7 @@ export async function createApp({ template, framework, engine, + ui, appPath, packageManager, eslint, @@ -26,6 +28,7 @@ export async function createApp({ template: TemplateType; framework: TemplateFramework; engine: TemplateEngine; + ui: TemplateUI; appPath: string; packageManager: PackageManager; eslint: boolean; @@ -63,6 +66,7 @@ export async function createApp({ template, framework, engine, + ui, packageManager, isOnline, eslint, diff --git a/packages/create-llama/index.ts b/packages/create-llama/index.ts index ab80066569c681989a8cf3959a0ca9fbac7c9680..6aeb13ecba52916ffdbe2760b0620aed99a6dbf4 100644 --- a/packages/create-llama/index.ts +++ b/packages/create-llama/index.ts @@ -180,6 +180,7 @@ async function run(): Promise<void> { template: "simple", framework: "nextjs", engine: "simple", + ui: "html", eslint: true, }; const getPrefOrDefault = (field: string) => @@ -239,6 +240,35 @@ async function run(): Promise<void> { } } + if (program.framework === "nextjs") { + if (!program.ui) { + if (ciInfo.isCI) { + program.ui = getPrefOrDefault("ui"); + } else { + const { ui } = await prompts( + { + type: "select", + name: "ui", + message: "Which UI would you like to use?", + choices: [ + { title: "Just HTML", value: "html" }, + { title: "Shadcn", value: "shadcn" }, + ], + initial: 0, + }, + { + onCancel: () => { + console.error("Exiting."); + process.exit(1); + }, + }, + ); + program.ui = ui; + preferences.ui = ui; + } + } + } + if (program.framework === "express" || program.framework === "nextjs") { if (!program.engine) { if (ciInfo.isCI) { @@ -294,6 +324,7 @@ async function run(): Promise<void> { template: program.template, framework: program.framework, engine: program.engine, + ui: program.ui, appPath: resolvedProjectPath, packageManager, eslint: program.eslint, diff --git a/packages/create-llama/templates/index.ts b/packages/create-llama/templates/index.ts index 92641318f17d2460bb7852556e8b244c0ae95cce..5bf8a7d3b2e1fbea897c702c32db32ac2fad7737 100644 --- a/packages/create-llama/templates/index.ts +++ b/packages/create-llama/templates/index.ts @@ -20,6 +20,7 @@ export const installTemplate = async ({ template, framework, engine, + ui, eslint, }: InstallTemplateArgs) => { console.log(bold(`Using ${packageManager}.`)); @@ -81,6 +82,26 @@ export const installTemplate = async ({ await fs.writeFile(routeFile, newContent); } + /** + * Copy the selected UI files to the target directory and reference it. + */ + if (framework === "nextjs") { + console.log("\nUsing UI:", ui, "\n"); + const uiPath = path.join(__dirname, "ui", ui); + const componentsPath = path.join("app", "components"); + await copy("**", path.join(root, componentsPath, "ui"), { + parents: true, + cwd: uiPath, + }); + const chatSectionFile = path.join(root, componentsPath, "chat-section.tsx"); + const chatSectionFileContent = await fs.readFile(chatSectionFile, "utf8"); + const newContent = chatSectionFileContent.replace( + /^import { ChatInput, ChatMessages, Message }.*$/m, + 'import { ChatInput, ChatMessages, Message } from "./ui/chat"\n', + ); + await fs.writeFile(chatSectionFile, newContent); + } + /** * Update the package.json scripts. */ @@ -108,6 +129,17 @@ export const installTemplate = async ({ }; } + if (framework === "nextjs" && ui === "shadcn") { + // add shadcn dependencies to package.json + packageJson.dependencies = { + ...packageJson.dependencies, + "tailwind-merge": "^2", + "@radix-ui/react-slot": "^1", + "class-variance-authority": "^0.7", + "lucide-react": "^0.291", + }; + } + if (!eslint) { // Remove packages starting with "eslint" from devDependencies packageJson.devDependencies = Object.fromEntries( diff --git a/packages/create-llama/templates/simple/nextjs/app/components/chat-section.tsx b/packages/create-llama/templates/simple/nextjs/app/components/chat-section.tsx index 989bfcda4b29777235db1d29fdfaa483d047b1e5..0a3ea5b3d5135354745289fccd3e16f0db2179ae 100644 --- a/packages/create-llama/templates/simple/nextjs/app/components/chat-section.tsx +++ b/packages/create-llama/templates/simple/nextjs/app/components/chat-section.tsx @@ -2,8 +2,7 @@ import { nanoid } from "nanoid"; import { useState } from "react"; -import ChatInput from "./ui/chat-input"; -import ChatMessages, { Message } from "./ui/chat-messages"; +import { ChatInput, ChatMessages, Message } from "../../../../ui/html/chat"; export default function ChatSection() { const [messages, setMessages] = useState<Message[]>([]); @@ -37,7 +36,7 @@ export default function ChatSection() { setMessages(newMessages); setInput(""); const assistantMessage = await getAssistantMessage(newMessages); - setMessages([...newMessages, { ...assistantMessage }]); + setMessages([...newMessages, { ...assistantMessage, id: nanoid() }]); setLoading(false); } catch (error: any) { alert(JSON.stringify(error)); diff --git a/packages/create-llama/templates/simple/nextjs/app/globals.css b/packages/create-llama/templates/simple/nextjs/app/globals.css index d85e2eec9ab40d8bc2d8cbad401f414ae8cd0ab2..09b85ed2c912e25518ddebbfebaba69090f889f4 100644 --- a/packages/create-llama/templates/simple/nextjs/app/globals.css +++ b/packages/create-llama/templates/simple/nextjs/app/globals.css @@ -2,38 +2,93 @@ @tailwind components; @tailwind utilities; -:root { - --foreground-rgb: 0, 0, 0; - --background-start-rgb: 214, 219, 220; - --background-end-rgb: 255, 255, 255; -} - -@media (prefers-color-scheme: dark) { +@layer base { :root { - --foreground-rgb: 255, 255, 255; - --background-start-rgb: 0, 0, 0; - --background-end-rgb: 0, 0, 0; + --background: 0 0% 100%; + --foreground: 222.2 47.4% 11.2%; + + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + + --popover: 0 0% 100%; + --popover-foreground: 222.2 47.4% 11.2%; + + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + + --card: 0 0% 100%; + --card-foreground: 222.2 47.4% 11.2%; + + --primary: 222.2 47.4% 11.2%; + --primary-foreground: 210 40% 98%; + + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; + + --destructive: 0 100% 50%; + --destructive-foreground: 210 40% 98%; + + --ring: 215 20.2% 65.1%; + + --radius: 0.5rem; } -} -body { - color: rgb(var(--foreground-rgb)); - background: linear-gradient( - to bottom, - transparent, - rgb(var(--background-end-rgb)) - ) - rgb(var(--background-start-rgb)); + .dark { + --background: 224 71% 4%; + --foreground: 213 31% 91%; + + --muted: 223 47% 11%; + --muted-foreground: 215.4 16.3% 56.9%; + + --accent: 216 34% 17%; + --accent-foreground: 210 40% 98%; + + --popover: 224 71% 4%; + --popover-foreground: 215 20.2% 65.1%; + + --border: 216 34% 17%; + --input: 216 34% 17%; + + --card: 224 71% 4%; + --card-foreground: 213 31% 91%; + + --primary: 210 40% 98%; + --primary-foreground: 222.2 47.4% 1.2%; + + --secondary: 222.2 47.4% 11.2%; + --secondary-foreground: 210 40% 98%; + + --destructive: 0 63% 31%; + --destructive-foreground: 210 40% 98%; + + --ring: 216 34% 17%; + + --radius: 0.5rem; + } } -.background-gradient { - background-color: #fff; - background-image: radial-gradient( - at 21% 11%, - rgba(186, 186, 233, 0.53) 0, - transparent 50% - ), - radial-gradient(at 85% 0, hsla(46, 57%, 78%, 0.52) 0, transparent 50%), - radial-gradient(at 91% 36%, rgba(194, 213, 255, 0.68) 0, transparent 50%), - radial-gradient(at 8% 40%, rgba(251, 218, 239, 0.46) 0, transparent 50%); +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + font-feature-settings: + "rlig" 1, + "calt" 1; + } + .background-gradient { + background-color: #fff; + background-image: radial-gradient( + at 21% 11%, + rgba(186, 186, 233, 0.53) 0, + transparent 50% + ), + radial-gradient(at 85% 0, hsla(46, 57%, 78%, 0.52) 0, transparent 50%), + radial-gradient(at 91% 36%, rgba(194, 213, 255, 0.68) 0, transparent 50%), + radial-gradient(at 8% 40%, rgba(251, 218, 239, 0.46) 0, transparent 50%); + } } diff --git a/packages/create-llama/templates/simple/nextjs/tailwind.config.ts b/packages/create-llama/templates/simple/nextjs/tailwind.config.ts index 7e4bd91a03437328466a264489ce47e107635565..aa5580affac868255fedb5a8ddc0dde7a105c454 100644 --- a/packages/create-llama/templates/simple/nextjs/tailwind.config.ts +++ b/packages/create-llama/templates/simple/nextjs/tailwind.config.ts @@ -1,17 +1,75 @@ import type { Config } from "tailwindcss"; +import { fontFamily } from "tailwindcss/defaultTheme"; const config: Config = { - content: [ - "./pages/**/*.{js,ts,jsx,tsx,mdx}", - "./components/**/*.{js,ts,jsx,tsx,mdx}", - "./app/**/*.{js,ts,jsx,tsx,mdx}", - ], + darkMode: ["class"], + content: ["app/**/*.{ts,tsx}", "components/**/*.{ts,tsx}"], theme: { + container: { + center: true, + padding: "2rem", + screens: { + "2xl": "1400px", + }, + }, extend: { - backgroundImage: { - "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", - "gradient-conic": - "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", + colors: { + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", + primary: { + DEFAULT: "hsl(var(--primary))", + foreground: "hsl(var(--primary-foreground))", + }, + secondary: { + DEFAULT: "hsl(var(--secondary))", + foreground: "hsl(var(--secondary-foreground))", + }, + destructive: { + DEFAULT: "hsl(var(--destructive) / <alpha-value>)", + foreground: "hsl(var(--destructive-foreground) / <alpha-value>)", + }, + muted: { + DEFAULT: "hsl(var(--muted))", + foreground: "hsl(var(--muted-foreground))", + }, + accent: { + DEFAULT: "hsl(var(--accent))", + foreground: "hsl(var(--accent-foreground))", + }, + popover: { + DEFAULT: "hsl(var(--popover))", + foreground: "hsl(var(--popover-foreground))", + }, + card: { + DEFAULT: "hsl(var(--card))", + foreground: "hsl(var(--card-foreground))", + }, + }, + borderRadius: { + xl: `calc(var(--radius) + 4px)`, + lg: `var(--radius)`, + md: `calc(var(--radius) - 2px)`, + sm: "calc(var(--radius) - 4px)", + }, + fontFamily: { + sans: ["var(--font-sans)", ...fontFamily.sans], + }, + keyframes: { + "accordion-down": { + from: { height: "0" }, + to: { height: "var(--radix-accordion-content-height)" }, + }, + "accordion-up": { + from: { height: "var(--radix-accordion-content-height)" }, + to: { height: "0" }, + }, + }, + animation: { + "accordion-down": "accordion-down 0.2s ease-out", + "accordion-up": "accordion-up 0.2s ease-out", }, }, }, diff --git a/packages/create-llama/templates/streaming/nextjs/app/components/chat-section.tsx b/packages/create-llama/templates/streaming/nextjs/app/components/chat-section.tsx index 5ef09b3343ae357de8a88071306c6c39b638d425..0a5fcef939a304d351194b268bb84201f7865072 100644 --- a/packages/create-llama/templates/streaming/nextjs/app/components/chat-section.tsx +++ b/packages/create-llama/templates/streaming/nextjs/app/components/chat-section.tsx @@ -1,8 +1,7 @@ "use client"; -import ChatInput from "@/app/components/ui/chat-input"; import { useChat } from "ai/react"; -import ChatMessages from "./ui/chat-messages"; +import { ChatInput, ChatMessages, Message } from "../../../../ui/html/chat"; export default function ChatSection() { const { messages, input, isLoading, handleSubmit, handleInputChange } = diff --git a/packages/create-llama/templates/streaming/nextjs/app/components/ui/chat-avatar.tsx b/packages/create-llama/templates/streaming/nextjs/app/components/ui/chat-avatar.tsx deleted file mode 100644 index cd241104e4ef210c728aec47a1ab8b0161ad6538..0000000000000000000000000000000000000000 --- a/packages/create-llama/templates/streaming/nextjs/app/components/ui/chat-avatar.tsx +++ /dev/null @@ -1,34 +0,0 @@ -"use client"; - -import Image from "next/image"; -import { Message } from "./chat-messages"; - -export default function ChatAvatar(message: Message) { - if (message.role === "user") { - return ( - <div className="flex h-8 w-8 shrink-0 select-none items-center justify-center rounded-md border shadow bg-background"> - <svg - xmlns="http://www.w3.org/2000/svg" - viewBox="0 0 256 256" - fill="currentColor" - className="h-4 w-4" - > - <path d="M230.92 212c-15.23-26.33-38.7-45.21-66.09-54.16a72 72 0 1 0-73.66 0c-27.39 8.94-50.86 27.82-66.09 54.16a8 8 0 1 0 13.85 8c18.84-32.56 52.14-52 89.07-52s70.23 19.44 89.07 52a8 8 0 1 0 13.85-8ZM72 96a56 56 0 1 1 56 56 56.06 56.06 0 0 1-56-56Z"></path> - </svg> - </div> - ); - } - - return ( - <div className="flex h-8 w-8 shrink-0 select-none items-center justify-center rounded-md border bg-black text-white"> - <Image - className="rounded-md" - src="/llama.png" - alt="Llama Logo" - width={24} - height={24} - priority - /> - </div> - ); -} diff --git a/packages/create-llama/templates/streaming/nextjs/app/components/ui/chat-input.tsx b/packages/create-llama/templates/streaming/nextjs/app/components/ui/chat-input.tsx deleted file mode 100644 index 3eb979b02735943f3f11290c78b84f0e37709438..0000000000000000000000000000000000000000 --- a/packages/create-llama/templates/streaming/nextjs/app/components/ui/chat-input.tsx +++ /dev/null @@ -1,42 +0,0 @@ -"use client"; - -export interface ChatInputProps { - /** The current value of the input */ - input?: string; - /** An input/textarea-ready onChange handler to control the value of the input */ - handleInputChange?: ( - e: - | React.ChangeEvent<HTMLInputElement> - | React.ChangeEvent<HTMLTextAreaElement>, - ) => void; - /** Form submission handler to automatically reset input and append a user message */ - handleSubmit: (e: React.FormEvent<HTMLFormElement>) => void; - isLoading: boolean; -} - -export default function ChatInput(props: ChatInputProps) { - return ( - <> - <form - onSubmit={props.handleSubmit} - className="flex items-start justify-between w-full max-w-5xl p-4 bg-white rounded-xl shadow-xl gap-4" - > - <input - autoFocus - name="message" - placeholder="Type a message" - className="w-full p-4 rounded-xl shadow-inner flex-1" - value={props.input} - onChange={props.handleInputChange} - /> - <button - disabled={props.isLoading} - type="submit" - className="p-4 text-white rounded-xl shadow-xl bg-gradient-to-r from-cyan-500 to-sky-500 disabled:opacity-50 disabled:cursor-not-allowed" - > - Send message - </button> - </form> - </> - ); -} diff --git a/packages/create-llama/templates/streaming/nextjs/app/components/ui/chat-item.tsx b/packages/create-llama/templates/streaming/nextjs/app/components/ui/chat-item.tsx deleted file mode 100644 index 2244f729a8ba668121ab5ec0842963d22153ef92..0000000000000000000000000000000000000000 --- a/packages/create-llama/templates/streaming/nextjs/app/components/ui/chat-item.tsx +++ /dev/null @@ -1,13 +0,0 @@ -"use client"; - -import ChatAvatar from "./chat-avatar"; -import { Message } from "./chat-messages"; - -export default function ChatItem(message: Message) { - return ( - <div className="flex items-start gap-4 pt-5"> - <ChatAvatar {...message} /> - <p className="break-words">{message.content}</p> - </div> - ); -} diff --git a/packages/create-llama/templates/streaming/nextjs/app/globals.css b/packages/create-llama/templates/streaming/nextjs/app/globals.css index d85e2eec9ab40d8bc2d8cbad401f414ae8cd0ab2..09b85ed2c912e25518ddebbfebaba69090f889f4 100644 --- a/packages/create-llama/templates/streaming/nextjs/app/globals.css +++ b/packages/create-llama/templates/streaming/nextjs/app/globals.css @@ -2,38 +2,93 @@ @tailwind components; @tailwind utilities; -:root { - --foreground-rgb: 0, 0, 0; - --background-start-rgb: 214, 219, 220; - --background-end-rgb: 255, 255, 255; -} - -@media (prefers-color-scheme: dark) { +@layer base { :root { - --foreground-rgb: 255, 255, 255; - --background-start-rgb: 0, 0, 0; - --background-end-rgb: 0, 0, 0; + --background: 0 0% 100%; + --foreground: 222.2 47.4% 11.2%; + + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + + --popover: 0 0% 100%; + --popover-foreground: 222.2 47.4% 11.2%; + + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + + --card: 0 0% 100%; + --card-foreground: 222.2 47.4% 11.2%; + + --primary: 222.2 47.4% 11.2%; + --primary-foreground: 210 40% 98%; + + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; + + --destructive: 0 100% 50%; + --destructive-foreground: 210 40% 98%; + + --ring: 215 20.2% 65.1%; + + --radius: 0.5rem; } -} -body { - color: rgb(var(--foreground-rgb)); - background: linear-gradient( - to bottom, - transparent, - rgb(var(--background-end-rgb)) - ) - rgb(var(--background-start-rgb)); + .dark { + --background: 224 71% 4%; + --foreground: 213 31% 91%; + + --muted: 223 47% 11%; + --muted-foreground: 215.4 16.3% 56.9%; + + --accent: 216 34% 17%; + --accent-foreground: 210 40% 98%; + + --popover: 224 71% 4%; + --popover-foreground: 215 20.2% 65.1%; + + --border: 216 34% 17%; + --input: 216 34% 17%; + + --card: 224 71% 4%; + --card-foreground: 213 31% 91%; + + --primary: 210 40% 98%; + --primary-foreground: 222.2 47.4% 1.2%; + + --secondary: 222.2 47.4% 11.2%; + --secondary-foreground: 210 40% 98%; + + --destructive: 0 63% 31%; + --destructive-foreground: 210 40% 98%; + + --ring: 216 34% 17%; + + --radius: 0.5rem; + } } -.background-gradient { - background-color: #fff; - background-image: radial-gradient( - at 21% 11%, - rgba(186, 186, 233, 0.53) 0, - transparent 50% - ), - radial-gradient(at 85% 0, hsla(46, 57%, 78%, 0.52) 0, transparent 50%), - radial-gradient(at 91% 36%, rgba(194, 213, 255, 0.68) 0, transparent 50%), - radial-gradient(at 8% 40%, rgba(251, 218, 239, 0.46) 0, transparent 50%); +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + font-feature-settings: + "rlig" 1, + "calt" 1; + } + .background-gradient { + background-color: #fff; + background-image: radial-gradient( + at 21% 11%, + rgba(186, 186, 233, 0.53) 0, + transparent 50% + ), + radial-gradient(at 85% 0, hsla(46, 57%, 78%, 0.52) 0, transparent 50%), + radial-gradient(at 91% 36%, rgba(194, 213, 255, 0.68) 0, transparent 50%), + radial-gradient(at 8% 40%, rgba(251, 218, 239, 0.46) 0, transparent 50%); + } } diff --git a/packages/create-llama/templates/streaming/nextjs/tailwind.config.ts b/packages/create-llama/templates/streaming/nextjs/tailwind.config.ts index 7e4bd91a03437328466a264489ce47e107635565..aa5580affac868255fedb5a8ddc0dde7a105c454 100644 --- a/packages/create-llama/templates/streaming/nextjs/tailwind.config.ts +++ b/packages/create-llama/templates/streaming/nextjs/tailwind.config.ts @@ -1,17 +1,75 @@ import type { Config } from "tailwindcss"; +import { fontFamily } from "tailwindcss/defaultTheme"; const config: Config = { - content: [ - "./pages/**/*.{js,ts,jsx,tsx,mdx}", - "./components/**/*.{js,ts,jsx,tsx,mdx}", - "./app/**/*.{js,ts,jsx,tsx,mdx}", - ], + darkMode: ["class"], + content: ["app/**/*.{ts,tsx}", "components/**/*.{ts,tsx}"], theme: { + container: { + center: true, + padding: "2rem", + screens: { + "2xl": "1400px", + }, + }, extend: { - backgroundImage: { - "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", - "gradient-conic": - "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", + colors: { + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", + primary: { + DEFAULT: "hsl(var(--primary))", + foreground: "hsl(var(--primary-foreground))", + }, + secondary: { + DEFAULT: "hsl(var(--secondary))", + foreground: "hsl(var(--secondary-foreground))", + }, + destructive: { + DEFAULT: "hsl(var(--destructive) / <alpha-value>)", + foreground: "hsl(var(--destructive-foreground) / <alpha-value>)", + }, + muted: { + DEFAULT: "hsl(var(--muted))", + foreground: "hsl(var(--muted-foreground))", + }, + accent: { + DEFAULT: "hsl(var(--accent))", + foreground: "hsl(var(--accent-foreground))", + }, + popover: { + DEFAULT: "hsl(var(--popover))", + foreground: "hsl(var(--popover-foreground))", + }, + card: { + DEFAULT: "hsl(var(--card))", + foreground: "hsl(var(--card-foreground))", + }, + }, + borderRadius: { + xl: `calc(var(--radius) + 4px)`, + lg: `var(--radius)`, + md: `calc(var(--radius) - 2px)`, + sm: "calc(var(--radius) - 4px)", + }, + fontFamily: { + sans: ["var(--font-sans)", ...fontFamily.sans], + }, + keyframes: { + "accordion-down": { + from: { height: "0" }, + to: { height: "var(--radix-accordion-content-height)" }, + }, + "accordion-up": { + from: { height: "var(--radix-accordion-content-height)" }, + to: { height: "0" }, + }, + }, + animation: { + "accordion-down": "accordion-down 0.2s ease-out", + "accordion-up": "accordion-up 0.2s ease-out", }, }, }, diff --git a/packages/create-llama/templates/types.ts b/packages/create-llama/templates/types.ts index 4c9ad3c6538353d3ca1d74eb98f541d0ef6f9b34..e76cd7da17718c906b6c58dcd93c1c825d0aba9f 100644 --- a/packages/create-llama/templates/types.ts +++ b/packages/create-llama/templates/types.ts @@ -3,6 +3,7 @@ import { PackageManager } from "../helpers/get-pkg-manager"; export type TemplateType = "simple" | "streaming"; export type TemplateFramework = "nextjs" | "express"; export type TemplateEngine = "simple" | "context"; +export type TemplateUI = "html" | "shadcn"; export interface InstallTemplateArgs { appName: string; @@ -12,5 +13,6 @@ export interface InstallTemplateArgs { template: TemplateType; framework: TemplateFramework; engine: TemplateEngine; + ui: TemplateUI; eslint: boolean; } diff --git a/packages/create-llama/templates/simple/nextjs/app/components/ui/chat-avatar.tsx b/packages/create-llama/templates/ui/html/chat/chat-avatar.tsx similarity index 100% rename from packages/create-llama/templates/simple/nextjs/app/components/ui/chat-avatar.tsx rename to packages/create-llama/templates/ui/html/chat/chat-avatar.tsx diff --git a/packages/create-llama/templates/simple/nextjs/app/components/ui/chat-input.tsx b/packages/create-llama/templates/ui/html/chat/chat-input.tsx similarity index 100% rename from packages/create-llama/templates/simple/nextjs/app/components/ui/chat-input.tsx rename to packages/create-llama/templates/ui/html/chat/chat-input.tsx diff --git a/packages/create-llama/templates/simple/nextjs/app/components/ui/chat-item.tsx b/packages/create-llama/templates/ui/html/chat/chat-item.tsx similarity index 100% rename from packages/create-llama/templates/simple/nextjs/app/components/ui/chat-item.tsx rename to packages/create-llama/templates/ui/html/chat/chat-item.tsx diff --git a/packages/create-llama/templates/simple/nextjs/app/components/ui/chat-messages.tsx b/packages/create-llama/templates/ui/html/chat/chat-messages.tsx similarity index 100% rename from packages/create-llama/templates/simple/nextjs/app/components/ui/chat-messages.tsx rename to packages/create-llama/templates/ui/html/chat/chat-messages.tsx diff --git a/packages/create-llama/templates/ui/html/chat/index.ts b/packages/create-llama/templates/ui/html/chat/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..4ccc54926fbd450313f952747bc1ea720100f2df --- /dev/null +++ b/packages/create-llama/templates/ui/html/chat/index.ts @@ -0,0 +1,6 @@ +import ChatInput from "./chat-input"; +import ChatMessages from "./chat-messages"; + +export type { ChatInputProps } from "./chat-input"; +export type { Message } from "./chat-messages"; +export { ChatMessages, ChatInput }; diff --git a/packages/create-llama/templates/ui/shadcn/button.tsx b/packages/create-llama/templates/ui/shadcn/button.tsx new file mode 100644 index 0000000000000000000000000000000000000000..662b0404d83445eb7fca1ead6724e944610fdf25 --- /dev/null +++ b/packages/create-llama/templates/ui/shadcn/button.tsx @@ -0,0 +1,56 @@ +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; +import * as React from "react"; + +import { cn } from "./lib/utils"; + +const buttonVariants = cva( + "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: + "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-10 px-4 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-8", + icon: "h-10 w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + }, +); + +export interface ButtonProps + extends React.ButtonHTMLAttributes<HTMLButtonElement>, + VariantProps<typeof buttonVariants> { + asChild?: boolean; +} + +const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button"; + return ( + <Comp + className={cn(buttonVariants({ variant, size, className }))} + ref={ref} + {...props} + /> + ); + }, +); +Button.displayName = "Button"; + +export { Button, buttonVariants }; diff --git a/packages/create-llama/templates/ui/shadcn/chat/chat-avatar.tsx b/packages/create-llama/templates/ui/shadcn/chat/chat-avatar.tsx new file mode 100644 index 0000000000000000000000000000000000000000..ce04e306a7164e49e7ea6950a55c4f5cedc2ee2a --- /dev/null +++ b/packages/create-llama/templates/ui/shadcn/chat/chat-avatar.tsx @@ -0,0 +1,25 @@ +import { User2 } from "lucide-react"; +import Image from "next/image"; + +export default function ChatAvatar({ role }: { role: string }) { + if (role === "user") { + return ( + <div className="flex h-8 w-8 shrink-0 select-none items-center justify-center rounded-md border bg-background shadow"> + <User2 className="h-4 w-4" /> + </div> + ); + } + + return ( + <div className="flex h-8 w-8 shrink-0 select-none items-center justify-center rounded-md border bg-black text-white shadow"> + <Image + className="rounded-md" + src="/llama.png" + alt="Llama Logo" + width={24} + height={24} + priority + /> + </div> + ); +} diff --git a/packages/create-llama/templates/ui/shadcn/chat/chat-input.tsx b/packages/create-llama/templates/ui/shadcn/chat/chat-input.tsx new file mode 100644 index 0000000000000000000000000000000000000000..86b079e0bc4b14d5c72d3f24c3a7641fddd2e153 --- /dev/null +++ b/packages/create-llama/templates/ui/shadcn/chat/chat-input.tsx @@ -0,0 +1,32 @@ +import * as React from "react"; + +import { Button } from "../button"; +import { Input } from "../input"; + +export interface ChatInputProps { + handleSubmit: (e: React.FormEvent<HTMLFormElement>) => void; + handleInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void; + input: string; + isLoading: boolean; +} + +export default function ChatInput(props: ChatInputProps) { + return ( + <form + onSubmit={props.handleSubmit} + className="mx-auto flex w-full max-w-5xl items-start justify-between gap-4 rounded-xl bg-white p-4 shadow-xl" + > + <Input + autoFocus + name="message" + placeholder="Type a message" + className="flex-1" + value={props.input} + onChange={props.handleInputChange} + /> + <Button disabled={props.isLoading} type="submit"> + Send message + </Button> + </form> + ); +} diff --git a/packages/create-llama/templates/ui/shadcn/chat/chat-message.tsx b/packages/create-llama/templates/ui/shadcn/chat/chat-message.tsx new file mode 100644 index 0000000000000000000000000000000000000000..62f7e75dd64f8f95b753a69e6934a72e7ab64bca --- /dev/null +++ b/packages/create-llama/templates/ui/shadcn/chat/chat-message.tsx @@ -0,0 +1,44 @@ +import { Check, Copy } from "lucide-react"; +import { useState } from "react"; + +import { Button } from "../button"; +import ChatAvatar from "./chat-avatar"; + +export interface Message { + id: string; + content: string; + role: string; +} + +export default function ChatMessage(chatMessage: Message) { + const [isCopied, setIsCopied] = useState(false); + + const copyToClipboard = () => { + navigator.clipboard.writeText(chatMessage.content); + setIsCopied(true); + setTimeout(() => { + setIsCopied(false); + }, 2000); + }; + + return ( + <div className="flex items-start gap-4 pt-5"> + <ChatAvatar role={chatMessage.role} /> + <div className="group flex flex-1 justify-between gap-2"> + <p className="break-words">{chatMessage.content}</p> + <Button + onClick={copyToClipboard} + size="icon" + variant="ghost" + className="hidden h-8 w-8 group-hover:flex" + > + {isCopied ? ( + <Check className="h-4 w-4" /> + ) : ( + <Copy className="h-4 w-4" /> + )} + </Button> + </div> + </div> + ); +} diff --git a/packages/create-llama/templates/streaming/nextjs/app/components/ui/chat-messages.tsx b/packages/create-llama/templates/ui/shadcn/chat/chat-messages.tsx similarity index 62% rename from packages/create-llama/templates/streaming/nextjs/app/components/ui/chat-messages.tsx rename to packages/create-llama/templates/ui/shadcn/chat/chat-messages.tsx index 65eacabbfb1b7bc99950b43e7fd07ba56b3ecdb0..d5162768c7e1db4d9963a6a6e50131e1700d1a1e 100644 --- a/packages/create-llama/templates/streaming/nextjs/app/components/ui/chat-messages.tsx +++ b/packages/create-llama/templates/ui/shadcn/chat/chat-messages.tsx @@ -1,13 +1,6 @@ -"use client"; - import { useEffect, useRef } from "react"; -import ChatItem from "./chat-item"; -export interface Message { - id: string; - content: string; - role: string; -} +import ChatMessage, { Message } from "./chat-message"; export default function ChatMessages({ messages }: { messages: Message[] }) { const scrollableChatContainerRef = useRef<HTMLDivElement>(null); @@ -24,13 +17,13 @@ export default function ChatMessages({ messages }: { messages: Message[] }) { }, [messages.length]); return ( - <div className="w-full max-w-5xl p-4 bg-white rounded-xl shadow-xl"> + <div className="mx-auto w-full max-w-5xl rounded-xl bg-white p-4 shadow-xl"> <div - className="flex flex-col gap-5 divide-y h-[50vh] overflow-auto" + className="flex h-[50vh] flex-col gap-5 divide-y overflow-auto" ref={scrollableChatContainerRef} > - {messages.map((m: Message) => ( - <ChatItem key={m.id} {...m} /> + {messages.map((m) => ( + <ChatMessage key={m.id} {...m} /> ))} </div> </div> diff --git a/packages/create-llama/templates/ui/shadcn/chat/index.ts b/packages/create-llama/templates/ui/shadcn/chat/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..d50251ffd6184259c6916a74951d88e0f598c6c5 --- /dev/null +++ b/packages/create-llama/templates/ui/shadcn/chat/index.ts @@ -0,0 +1,6 @@ +import ChatInput from "./chat-input"; +import ChatMessages from "./chat-messages"; + +export type { ChatInputProps } from "./chat-input"; +export type { Message } from "./chat-message"; +export { ChatMessages, ChatInput }; diff --git a/packages/create-llama/templates/ui/shadcn/input.tsx b/packages/create-llama/templates/ui/shadcn/input.tsx new file mode 100644 index 0000000000000000000000000000000000000000..edfa129e623cca64fa706d7750e3635fecd1d628 --- /dev/null +++ b/packages/create-llama/templates/ui/shadcn/input.tsx @@ -0,0 +1,25 @@ +import * as React from "react"; + +import { cn } from "./lib/utils"; + +export interface InputProps + extends React.InputHTMLAttributes<HTMLInputElement> {} + +const Input = React.forwardRef<HTMLInputElement, InputProps>( + ({ className, type, ...props }, ref) => { + return ( + <input + type={type} + className={cn( + "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50", + className, + )} + ref={ref} + {...props} + /> + ); + }, +); +Input.displayName = "Input"; + +export { Input }; diff --git a/packages/create-llama/templates/ui/shadcn/lib/utils.ts b/packages/create-llama/templates/ui/shadcn/lib/utils.ts new file mode 100644 index 0000000000000000000000000000000000000000..a5ef193506d07d0459fec4f187af08283094d7c8 --- /dev/null +++ b/packages/create-llama/templates/ui/shadcn/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +}