From bdd1497322a08a36b1b0a72d0493a6cd3bb19f77 Mon Sep 17 00:00:00 2001
From: Marcus Schiesser
Date: Mon, 25 Dec 2023 12:32:54 +0700
Subject: [PATCH] feat: add xmas demo
---
.gitignore | 3 +
nextjs-multi-modal-xmas/.eslintrc.json | 3 +
nextjs-multi-modal-xmas/.gitignore | 35 +
nextjs-multi-modal-xmas/README.md | 41 +
.../app/api/chat/engine/index.ts | 7 +
.../app/api/chat/llamaindex-stream.ts | 81 +
nextjs-multi-modal-xmas/app/api/chat/route.ts | 84 +
.../app/components/chat-section.tsx | 44 +
.../app/components/header.tsx | 30 +
.../app/components/transform.ts | 36 +
.../app/components/ui/README.md | 1 +
.../app/components/ui/button.tsx | 56 +
.../app/components/ui/chat/chat-actions.tsx | 28 +
.../app/components/ui/chat/chat-avatar.tsx | 25 +
.../app/components/ui/chat/chat-input.tsx | 84 +
.../app/components/ui/chat/chat-message.tsx | 71 +
.../app/components/ui/chat/chat-messages.tsx | 62 +
.../app/components/ui/chat/chat.interface.ts | 35 +
.../app/components/ui/chat/codeblock.tsx | 139 +
.../app/components/ui/chat/index.ts | 10 +
.../app/components/ui/chat/markdown.tsx | 59 +
.../ui/chat/use-copy-to-clipboard.tsx | 33 +
.../app/components/ui/file-uploader.tsx | 105 +
.../app/components/ui/input.tsx | 25 +
.../app/components/ui/lib/utils.ts | 6 +
.../components/ui/upload-image-preview.tsx | 32 +
nextjs-multi-modal-xmas/app/favicon.ico | Bin 0 -> 15406 bytes
nextjs-multi-modal-xmas/app/globals.css | 94 +
nextjs-multi-modal-xmas/app/layout.tsx | 22 +
nextjs-multi-modal-xmas/app/page.tsx | 11 +
nextjs-multi-modal-xmas/constants.ts | 1 +
nextjs-multi-modal-xmas/data/1.jpg | Bin 0 -> 16769 bytes
nextjs-multi-modal-xmas/data/3.gif | Bin 0 -> 26616 bytes
nextjs-multi-modal-xmas/data/4.webp | Bin 0 -> 9980 bytes
nextjs-multi-modal-xmas/next.config.js | 21 +
nextjs-multi-modal-xmas/package-lock.json | 8281 +++++++++++++++++
nextjs-multi-modal-xmas/package.json | 45 +
nextjs-multi-modal-xmas/pnpm-lock.yaml | 5120 ++++++++++
nextjs-multi-modal-xmas/postcss.config.js | 6 +
nextjs-multi-modal-xmas/public/llama.png | Bin 0 -> 36985 bytes
nextjs-multi-modal-xmas/tailwind.config.ts | 78 +
nextjs-multi-modal-xmas/tsconfig.json | 41 +
42 files changed, 14855 insertions(+)
create mode 100644 nextjs-multi-modal-xmas/.eslintrc.json
create mode 100644 nextjs-multi-modal-xmas/.gitignore
create mode 100644 nextjs-multi-modal-xmas/README.md
create mode 100644 nextjs-multi-modal-xmas/app/api/chat/engine/index.ts
create mode 100644 nextjs-multi-modal-xmas/app/api/chat/llamaindex-stream.ts
create mode 100644 nextjs-multi-modal-xmas/app/api/chat/route.ts
create mode 100644 nextjs-multi-modal-xmas/app/components/chat-section.tsx
create mode 100644 nextjs-multi-modal-xmas/app/components/header.tsx
create mode 100644 nextjs-multi-modal-xmas/app/components/transform.ts
create mode 100644 nextjs-multi-modal-xmas/app/components/ui/README.md
create mode 100644 nextjs-multi-modal-xmas/app/components/ui/button.tsx
create mode 100644 nextjs-multi-modal-xmas/app/components/ui/chat/chat-actions.tsx
create mode 100644 nextjs-multi-modal-xmas/app/components/ui/chat/chat-avatar.tsx
create mode 100644 nextjs-multi-modal-xmas/app/components/ui/chat/chat-input.tsx
create mode 100644 nextjs-multi-modal-xmas/app/components/ui/chat/chat-message.tsx
create mode 100644 nextjs-multi-modal-xmas/app/components/ui/chat/chat-messages.tsx
create mode 100644 nextjs-multi-modal-xmas/app/components/ui/chat/chat.interface.ts
create mode 100644 nextjs-multi-modal-xmas/app/components/ui/chat/codeblock.tsx
create mode 100644 nextjs-multi-modal-xmas/app/components/ui/chat/index.ts
create mode 100644 nextjs-multi-modal-xmas/app/components/ui/chat/markdown.tsx
create mode 100644 nextjs-multi-modal-xmas/app/components/ui/chat/use-copy-to-clipboard.tsx
create mode 100644 nextjs-multi-modal-xmas/app/components/ui/file-uploader.tsx
create mode 100644 nextjs-multi-modal-xmas/app/components/ui/input.tsx
create mode 100644 nextjs-multi-modal-xmas/app/components/ui/lib/utils.ts
create mode 100644 nextjs-multi-modal-xmas/app/components/ui/upload-image-preview.tsx
create mode 100644 nextjs-multi-modal-xmas/app/favicon.ico
create mode 100644 nextjs-multi-modal-xmas/app/globals.css
create mode 100644 nextjs-multi-modal-xmas/app/layout.tsx
create mode 100644 nextjs-multi-modal-xmas/app/page.tsx
create mode 100644 nextjs-multi-modal-xmas/constants.ts
create mode 100644 nextjs-multi-modal-xmas/data/1.jpg
create mode 100644 nextjs-multi-modal-xmas/data/3.gif
create mode 100644 nextjs-multi-modal-xmas/data/4.webp
create mode 100644 nextjs-multi-modal-xmas/next.config.js
create mode 100644 nextjs-multi-modal-xmas/package-lock.json
create mode 100644 nextjs-multi-modal-xmas/package.json
create mode 100644 nextjs-multi-modal-xmas/pnpm-lock.yaml
create mode 100644 nextjs-multi-modal-xmas/postcss.config.js
create mode 100644 nextjs-multi-modal-xmas/public/llama.png
create mode 100644 nextjs-multi-modal-xmas/tailwind.config.ts
create mode 100644 nextjs-multi-modal-xmas/tsconfig.json
diff --git a/.gitignore b/.gitignore
index 8cd1283..0020033 100644
--- a/.gitignore
+++ b/.gitignore
@@ -146,3 +146,6 @@ Pipfile.lock
# pyright
pyrightconfig.json
+
+# vscode
+.vscode/
\ No newline at end of file
diff --git a/nextjs-multi-modal-xmas/.eslintrc.json b/nextjs-multi-modal-xmas/.eslintrc.json
new file mode 100644
index 0000000..bffb357
--- /dev/null
+++ b/nextjs-multi-modal-xmas/.eslintrc.json
@@ -0,0 +1,3 @@
+{
+ "extends": "next/core-web-vitals"
+}
diff --git a/nextjs-multi-modal-xmas/.gitignore b/nextjs-multi-modal-xmas/.gitignore
new file mode 100644
index 0000000..8f322f0
--- /dev/null
+++ b/nextjs-multi-modal-xmas/.gitignore
@@ -0,0 +1,35 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# local env files
+.env*.local
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
diff --git a/nextjs-multi-modal-xmas/README.md b/nextjs-multi-modal-xmas/README.md
new file mode 100644
index 0000000..080b0e5
--- /dev/null
+++ b/nextjs-multi-modal-xmas/README.md
@@ -0,0 +1,41 @@
+This is a [LlamaIndex](https://www.llamaindex.ai/) project using [Next.js](https://nextjs.org/) bootstrapped with [`create-llama`](https://github.com/run-llama/LlamaIndexTS/tree/main/packages/create-llama).
+
+## Introduction
+
+This example allows you to have a chat using the [GPT4 Vision model](https://platform.openai.com/docs/guides/vision) from OpenAI. You can upload files and ask the model to describe them.
+
+To keep the example simple, we are not using a database or any other kind of storage for the images.
+Instead, they are sent to the model in base64 encoding. This is not very efficient and only works for small images
+like the ones in the `./data` folder.
+
+We recommended implementing a server upload and sending just the URL of the image instead.
+A straightforward way is to use [Vercel Blob](https://vercel.com/docs/storage/vercel-blob/quickstart#server-uploads) which is a file storage service that is easy to integrate with Next.js.
+
+## Getting Started
+
+First, install the dependencies:
+
+```
+npm install
+```
+
+Second, run the development server:
+
+```
+npm run dev
+```
+
+Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
+
+You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
+
+This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
+
+## Learn More
+
+To learn more about LlamaIndex, take a look at the following resources:
+
+- [LlamaIndex Documentation](https://docs.llamaindex.ai) - learn about LlamaIndex (Python features).
+- [LlamaIndexTS Documentation](https://ts.llamaindex.ai) - learn about LlamaIndex (Typescript features).
+
+You can check out [the LlamaIndexTS GitHub repository](https://github.com/run-llama/LlamaIndexTS) - your feedback and contributions are welcome!
diff --git a/nextjs-multi-modal-xmas/app/api/chat/engine/index.ts b/nextjs-multi-modal-xmas/app/api/chat/engine/index.ts
new file mode 100644
index 0000000..abb02e9
--- /dev/null
+++ b/nextjs-multi-modal-xmas/app/api/chat/engine/index.ts
@@ -0,0 +1,7 @@
+import { LLM, SimpleChatEngine } from "llamaindex";
+
+export async function createChatEngine(llm: LLM) {
+ return new SimpleChatEngine({
+ llm,
+ });
+}
diff --git a/nextjs-multi-modal-xmas/app/api/chat/llamaindex-stream.ts b/nextjs-multi-modal-xmas/app/api/chat/llamaindex-stream.ts
new file mode 100644
index 0000000..dde9a7f
--- /dev/null
+++ b/nextjs-multi-modal-xmas/app/api/chat/llamaindex-stream.ts
@@ -0,0 +1,81 @@
+import {
+ createCallbacksTransformer,
+ createStreamDataTransformer,
+ trimStartOfStreamHelper,
+ type AIStreamCallbacksAndOptions,
+ experimental_StreamData,
+ JSONValue,
+} from "ai";
+import OpenAI from 'openai';
+
+type ParserOptions = {
+ image_url?: string;
+};
+
+function wrapUrl(image_url: string | undefined): JSONValue {
+ if (image_url) {
+ // if image_url is provided, send it via the data stream
+ return {
+ type: "image_url",
+ image_url: {
+ url: image_url,
+ },
+ }
+ }
+ return {}; // send an empty image response for the user's message
+}
+
+function createParser(
+ res: AsyncGenerator,
+ data: experimental_StreamData,
+ opts?: ParserOptions,
+) {
+ const openai = new OpenAI();
+ const trimStartOfStream = trimStartOfStreamHelper();
+ let prompt = "";
+ return new ReadableStream({
+ start() {
+ data.append(wrapUrl(opts?.image_url));
+ },
+ async pull(controller): Promise {
+ const {value, done} = await res.next();
+ if (done) {
+ const response = await openai.images.generate({
+ model: "dall-e-3",
+ prompt,
+ n: 1,
+ size: "1024x1024",
+ });
+ const imageUrl = response.data.at(0)?.url;
+ controller.close();
+ data.append(wrapUrl(imageUrl)); // send an empty image response for the assistant's message
+ await data.close();
+ return;
+ }
+
+ const text = trimStartOfStream(value ?? "");
+ if (text) {
+ prompt = prompt + text;
+ controller.enqueue(text);
+ }
+ },
+ }
+ )
+ ;
+}
+
+export function LlamaIndexStream(
+ res: AsyncGenerator,
+ opts?: {
+ callbacks?: AIStreamCallbacksAndOptions;
+ parserOptions?: ParserOptions;
+ },
+): { stream: ReadableStream; data: experimental_StreamData } {
+ const data = new experimental_StreamData();
+ return {
+ stream: createParser(res, data, opts?.parserOptions)
+ .pipeThrough(createCallbacksTransformer(opts?.callbacks))
+ .pipeThrough(createStreamDataTransformer(true)),
+ data,
+ };
+}
diff --git a/nextjs-multi-modal-xmas/app/api/chat/route.ts b/nextjs-multi-modal-xmas/app/api/chat/route.ts
new file mode 100644
index 0000000..b5c6429
--- /dev/null
+++ b/nextjs-multi-modal-xmas/app/api/chat/route.ts
@@ -0,0 +1,84 @@
+import { MODEL } from "@/constants";
+import { Message, StreamingTextResponse } from "ai";
+import { ChatMessage, MessageContent, OpenAI } from "llamaindex";
+import { NextRequest, NextResponse } from "next/server";
+import { createChatEngine } from "./engine";
+import { LlamaIndexStream } from "./llamaindex-stream";
+
+export const runtime = "nodejs";
+export const dynamic = "force-dynamic";
+
+const getLastMessageContent = (
+ textMessage: string,
+ imageUrl: string | undefined,
+): MessageContent => {
+ if (!imageUrl) return textMessage;
+ return [
+ {
+ type: "text",
+ text: textMessage,
+ },
+ {
+ type: "image_url",
+ image_url: {
+ url: imageUrl,
+ },
+ },
+ ];
+};
+
+export async function POST(request: NextRequest) {
+ try {
+ const body = await request.json();
+ const { messages, data }: { messages: Message[]; data: any } = body;
+ const lastMessage = messages.pop();
+ if (!messages || !lastMessage || lastMessage.role !== "user") {
+ return NextResponse.json(
+ {
+ error:
+ "messages are required in the request body and the last message must be from the user",
+ },
+ { status: 400 },
+ );
+ }
+
+ const llm = new OpenAI({
+ model: MODEL,
+ maxTokens: 512,
+ });
+
+ const chatEngine = await createChatEngine(llm);
+
+ const prompt="Please give me a cute and vivid description of this image. Modify the description so it has a christmas theme.";
+ const lastMessageContent = getLastMessageContent(
+ prompt,
+ data?.imageUrl,
+ );
+
+ const response = await chatEngine.chat(
+ lastMessageContent as MessageContent,
+ messages as ChatMessage[],
+ true,
+ );
+
+ // Transform the response into a readable stream
+ const stream = LlamaIndexStream(response, {
+ parserOptions: {
+ image_url: data?.imageUrl,
+ },
+ });
+
+ // Return a StreamingTextResponse, which can be consumed by the client
+ return new StreamingTextResponse(stream.stream, {}, stream.data);
+ } catch (error) {
+ console.error("[LlamaIndex]", error);
+ return NextResponse.json(
+ {
+ error: (error as Error).message,
+ },
+ {
+ status: 500,
+ },
+ );
+ }
+}
diff --git a/nextjs-multi-modal-xmas/app/components/chat-section.tsx b/nextjs-multi-modal-xmas/app/components/chat-section.tsx
new file mode 100644
index 0000000..d40b083
--- /dev/null
+++ b/nextjs-multi-modal-xmas/app/components/chat-section.tsx
@@ -0,0 +1,44 @@
+"use client";
+
+import { MODEL } from "@/constants";
+import { useChat } from "ai/react";
+import { ChatInput, ChatMessages } from "./ui/chat";
+import { useMemo } from "react";
+import { transformMessages } from "./transform";
+
+export default function ChatSection() {
+ const {
+ messages,
+ input,
+ isLoading,
+ handleSubmit,
+ handleInputChange,
+ reload,
+ stop,
+ data,
+ } = useChat({
+ api: process.env.NEXT_PUBLIC_CHAT_API,
+ });
+
+ const transformedMessages = useMemo(() => {
+ return transformMessages(messages, data);
+ }, [messages, data]);
+
+ return (
+
+
+
+
+ );
+}
diff --git a/nextjs-multi-modal-xmas/app/components/header.tsx b/nextjs-multi-modal-xmas/app/components/header.tsx
new file mode 100644
index 0000000..64115f2
--- /dev/null
+++ b/nextjs-multi-modal-xmas/app/components/header.tsx
@@ -0,0 +1,30 @@
+import Image from "next/image";
+
+export default function Header() {
+ return (
+
+
+ Multi-Modal Chat Demo
+
+
+ Upload images and ask questions related to the images!
+
+
+
+ );
+}
diff --git a/nextjs-multi-modal-xmas/app/components/transform.ts b/nextjs-multi-modal-xmas/app/components/transform.ts
new file mode 100644
index 0000000..9c246a9
--- /dev/null
+++ b/nextjs-multi-modal-xmas/app/components/transform.ts
@@ -0,0 +1,36 @@
+import { JSONValue } from "ai";
+import { MessageContentDetail, RawMessage, Message } from "./ui/chat/index";
+
+const transformMessage = (
+ message: RawMessage,
+ data: JSONValue | undefined,
+): Message => {
+ const msg = {
+ ...message,
+ content: [
+ {
+ type: "text",
+ text: message.content,
+ },
+ ],
+ } as Message;
+ if (data && typeof data === "object" && Object.keys(data).length > 0) {
+ // if the server sends an non-empty data object, it must be of type MessageContentDetail
+ // add it to the message's content
+ const content = data as unknown as MessageContentDetail;
+ if (content["type"] === "image_url") {
+ msg.content.push(content);
+ }
+ }
+ return msg;
+};
+
+export const transformMessages = (
+ messages: RawMessage[],
+ data: JSONValue[] | undefined,
+) => {
+ const result = messages.map((message, index) =>
+ transformMessage(message, data?.at(index)),
+ );
+ return result;
+};
diff --git a/nextjs-multi-modal-xmas/app/components/ui/README.md b/nextjs-multi-modal-xmas/app/components/ui/README.md
new file mode 100644
index 0000000..ebfcf48
--- /dev/null
+++ b/nextjs-multi-modal-xmas/app/components/ui/README.md
@@ -0,0 +1 @@
+Using the chat component from https://github.com/marcusschiesser/ui (based on https://ui.shadcn.com/)
diff --git a/nextjs-multi-modal-xmas/app/components/ui/button.tsx b/nextjs-multi-modal-xmas/app/components/ui/button.tsx
new file mode 100644
index 0000000..662b040
--- /dev/null
+++ b/nextjs-multi-modal-xmas/app/components/ui/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,
+ 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/nextjs-multi-modal-xmas/app/components/ui/chat/chat-actions.tsx b/nextjs-multi-modal-xmas/app/components/ui/chat/chat-actions.tsx
new file mode 100644
index 0000000..151ef61
--- /dev/null
+++ b/nextjs-multi-modal-xmas/app/components/ui/chat/chat-actions.tsx
@@ -0,0 +1,28 @@
+import { PauseCircle, RefreshCw } from "lucide-react";
+
+import { Button } from "../button";
+import { ChatHandler } from "./chat.interface";
+
+export default function ChatActions(
+ props: Pick & {
+ showReload?: boolean;
+ showStop?: boolean;
+ },
+) {
+ return (
+
+ {props.showStop && (
+
+ )}
+ {props.showReload && (
+
+ )}
+
+ );
+}
diff --git a/nextjs-multi-modal-xmas/app/components/ui/chat/chat-avatar.tsx b/nextjs-multi-modal-xmas/app/components/ui/chat/chat-avatar.tsx
new file mode 100644
index 0000000..baac2f9
--- /dev/null
+++ b/nextjs-multi-modal-xmas/app/components/ui/chat/chat-avatar.tsx
@@ -0,0 +1,25 @@
+import { Shell, User2 } from "lucide-react";
+import Image from "next/image";
+
+export default function ChatAvatar({ role }: { role: string }) {
+ if (role === "user") {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ );
+}
diff --git a/nextjs-multi-modal-xmas/app/components/ui/chat/chat-input.tsx b/nextjs-multi-modal-xmas/app/components/ui/chat/chat-input.tsx
new file mode 100644
index 0000000..435637e
--- /dev/null
+++ b/nextjs-multi-modal-xmas/app/components/ui/chat/chat-input.tsx
@@ -0,0 +1,84 @@
+import { useState } from "react";
+import { Button } from "../button";
+import FileUploader from "../file-uploader";
+import { Input } from "../input";
+import UploadImagePreview from "../upload-image-preview";
+import { ChatHandler } from "./chat.interface";
+
+export default function ChatInput(
+ props: Pick<
+ ChatHandler,
+ | "isLoading"
+ | "input"
+ | "onFileUpload"
+ | "onFileError"
+ | "handleSubmit"
+ | "handleInputChange"
+ > & {
+ multiModal?: boolean;
+ },
+) {
+ const [imageUrl, setImageUrl] = useState(null);
+
+ const onSubmit = (e: React.FormEvent) => {
+ if (imageUrl) {
+ props.handleSubmit(e, {
+ data: { imageUrl: imageUrl },
+ });
+ setImageUrl(null);
+ return;
+ }
+ props.handleSubmit(e);
+ };
+
+ const onRemovePreviewImage = () => setImageUrl(null);
+
+ const handleUploadImageFile = async (file: File) => {
+ const base64 = await new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.readAsDataURL(file);
+ reader.onload = () => resolve(reader.result as string);
+ reader.onerror = (error) => reject(error);
+ });
+ setImageUrl(base64);
+ };
+
+ const handleUploadFile = async (file: File) => {
+ try {
+ if (props.multiModal && file.type.startsWith("image/")) {
+ return await handleUploadImageFile(file);
+ }
+ props.onFileUpload?.(file);
+ } catch (error: any) {
+ props.onFileError?.(error.message);
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/nextjs-multi-modal-xmas/app/components/ui/chat/chat-message.tsx b/nextjs-multi-modal-xmas/app/components/ui/chat/chat-message.tsx
new file mode 100644
index 0000000..a28d043
--- /dev/null
+++ b/nextjs-multi-modal-xmas/app/components/ui/chat/chat-message.tsx
@@ -0,0 +1,71 @@
+/* eslint-disable @next/next/no-img-element */
+import { Check, Copy } from "lucide-react";
+
+import { Button } from "../button";
+import ChatAvatar from "./chat-avatar";
+import { Message, MessageContentDetail } from "./chat.interface";
+import Markdown from "./markdown";
+import { useCopyToClipboard } from "./use-copy-to-clipboard";
+
+function ChatMessageContents({
+ contents,
+}: {
+ contents: MessageContentDetail[];
+}) {
+ const mediaContents = contents.filter(
+ (c) => c.type === "image_url" && c.image_url?.url,
+ );
+ const textContent = contents.find((c) => c.type === "text");
+
+ return (
+ <>
+ {mediaContents.length > 0 && (
+
+ {mediaContents.map((content, index) => {
+ const image_url = content.image_url?.url;
+ return (
+
+

+
+ );
+ })}
+
+ )}
+ {textContent && }
+ >
+ );
+}
+
+export default function ChatMessage(chatMessage: Message) {
+ const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 });
+ const onCopy = () => {
+ const pureText = chatMessage.content.find((c) => c.text)?.text;
+ if (pureText) copyToClipboard(pureText);
+ };
+ return (
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/nextjs-multi-modal-xmas/app/components/ui/chat/chat-messages.tsx b/nextjs-multi-modal-xmas/app/components/ui/chat/chat-messages.tsx
new file mode 100644
index 0000000..abc3e52
--- /dev/null
+++ b/nextjs-multi-modal-xmas/app/components/ui/chat/chat-messages.tsx
@@ -0,0 +1,62 @@
+import { Loader2 } from "lucide-react";
+import { useEffect, useRef } from "react";
+
+import ChatActions from "./chat-actions";
+import ChatMessage from "./chat-message";
+import { ChatHandler } from "./chat.interface";
+
+export default function ChatMessages(
+ props: Pick,
+) {
+ const scrollableChatContainerRef = useRef(null);
+ const messageLength = props.messages.length;
+ const lastMessage = props.messages[messageLength - 1];
+
+ const scrollToBottom = () => {
+ if (scrollableChatContainerRef.current) {
+ scrollableChatContainerRef.current.scrollTop =
+ scrollableChatContainerRef.current.scrollHeight;
+ }
+ };
+
+ const isLastMessageFromAssistant =
+ messageLength > 0 && lastMessage?.role !== "user";
+ const showReload =
+ props.reload && !props.isLoading && isLastMessageFromAssistant;
+ const showStop = props.stop && props.isLoading;
+
+ // `isPending` indicate
+ // that stream response is not yet received from the server,
+ // so we show a loading indicator to give a better UX.
+ const isPending = props.isLoading && !isLastMessageFromAssistant;
+
+ useEffect(() => {
+ scrollToBottom();
+ }, [messageLength, lastMessage]);
+
+ return (
+
+
+ {props.messages.map((m) => (
+
+ ))}
+ {isPending && (
+
+
+
+ )}
+
+
+
+
+
+ );
+}
diff --git a/nextjs-multi-modal-xmas/app/components/ui/chat/chat.interface.ts b/nextjs-multi-modal-xmas/app/components/ui/chat/chat.interface.ts
new file mode 100644
index 0000000..496feed
--- /dev/null
+++ b/nextjs-multi-modal-xmas/app/components/ui/chat/chat.interface.ts
@@ -0,0 +1,35 @@
+export interface MessageContentDetail {
+ type: "text" | "image_url";
+ text?: string;
+ image_url?: { url: string };
+ role?: "user" | "assistant";
+}
+
+export interface RawMessage {
+ id: string;
+ content: string;
+ role: string;
+}
+
+export interface Message {
+ id: string;
+ role: string;
+ content: MessageContentDetail[];
+}
+
+export interface ChatHandler {
+ messages: Message[];
+ input: string;
+ isLoading: boolean;
+ handleSubmit: (
+ e: React.FormEvent,
+ ops?: {
+ data?: any;
+ },
+ ) => void;
+ handleInputChange: (e: React.ChangeEvent) => void;
+ reload?: () => void;
+ stop?: () => void;
+ onFileUpload?: (file: File) => Promise;
+ onFileError?: (errMsg: string) => void;
+}
diff --git a/nextjs-multi-modal-xmas/app/components/ui/chat/codeblock.tsx b/nextjs-multi-modal-xmas/app/components/ui/chat/codeblock.tsx
new file mode 100644
index 0000000..014a0fc
--- /dev/null
+++ b/nextjs-multi-modal-xmas/app/components/ui/chat/codeblock.tsx
@@ -0,0 +1,139 @@
+"use client";
+
+import { Check, Copy, Download } from "lucide-react";
+import { FC, memo } from "react";
+import { Prism, SyntaxHighlighterProps } from "react-syntax-highlighter";
+import { coldarkDark } from "react-syntax-highlighter/dist/cjs/styles/prism";
+
+import { Button } from "../button";
+import { useCopyToClipboard } from "./use-copy-to-clipboard";
+
+// TODO: Remove this when @type/react-syntax-highlighter is updated
+const SyntaxHighlighter = Prism as unknown as FC;
+
+interface Props {
+ language: string;
+ value: string;
+}
+
+interface languageMap {
+ [key: string]: string | undefined;
+}
+
+export const programmingLanguages: languageMap = {
+ javascript: ".js",
+ python: ".py",
+ java: ".java",
+ c: ".c",
+ cpp: ".cpp",
+ "c++": ".cpp",
+ "c#": ".cs",
+ ruby: ".rb",
+ php: ".php",
+ swift: ".swift",
+ "objective-c": ".m",
+ kotlin: ".kt",
+ typescript: ".ts",
+ go: ".go",
+ perl: ".pl",
+ rust: ".rs",
+ scala: ".scala",
+ haskell: ".hs",
+ lua: ".lua",
+ shell: ".sh",
+ sql: ".sql",
+ html: ".html",
+ css: ".css",
+ // add more file extensions here, make sure the key is same as language prop in CodeBlock.tsx component
+};
+
+export const generateRandomString = (length: number, lowercase = false) => {
+ const chars = "ABCDEFGHJKLMNPQRSTUVWXY3456789"; // excluding similar looking characters like Z, 2, I, 1, O, 0
+ let result = "";
+ for (let i = 0; i < length; i++) {
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
+ }
+ return lowercase ? result.toLowerCase() : result;
+};
+
+const CodeBlock: FC = memo(({ language, value }) => {
+ const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 });
+
+ const downloadAsFile = () => {
+ if (typeof window === "undefined") {
+ return;
+ }
+ const fileExtension = programmingLanguages[language] || ".file";
+ const suggestedFileName = `file-${generateRandomString(
+ 3,
+ true,
+ )}${fileExtension}`;
+ const fileName = window.prompt("Enter file name" || "", suggestedFileName);
+
+ if (!fileName) {
+ // User pressed cancel on prompt.
+ return;
+ }
+
+ const blob = new Blob([value], { type: "text/plain" });
+ const url = URL.createObjectURL(blob);
+ const link = document.createElement("a");
+ link.download = fileName;
+ link.href = url;
+ link.style.display = "none";
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ URL.revokeObjectURL(url);
+ };
+
+ const onCopy = () => {
+ if (isCopied) return;
+ copyToClipboard(value);
+ };
+
+ return (
+
+
+
{language}
+
+
+
+
+
+
+ {value}
+
+
+ );
+});
+CodeBlock.displayName = "CodeBlock";
+
+export { CodeBlock };
diff --git a/nextjs-multi-modal-xmas/app/components/ui/chat/index.ts b/nextjs-multi-modal-xmas/app/components/ui/chat/index.ts
new file mode 100644
index 0000000..e5d51ce
--- /dev/null
+++ b/nextjs-multi-modal-xmas/app/components/ui/chat/index.ts
@@ -0,0 +1,10 @@
+import ChatInput from "./chat-input";
+import ChatMessages from "./chat-messages";
+
+export {
+ type ChatHandler,
+ type Message,
+ type RawMessage,
+ type MessageContentDetail,
+} from "./chat.interface";
+export { ChatInput, ChatMessages };
diff --git a/nextjs-multi-modal-xmas/app/components/ui/chat/markdown.tsx b/nextjs-multi-modal-xmas/app/components/ui/chat/markdown.tsx
new file mode 100644
index 0000000..3ca5380
--- /dev/null
+++ b/nextjs-multi-modal-xmas/app/components/ui/chat/markdown.tsx
@@ -0,0 +1,59 @@
+import { FC, memo } from "react";
+import ReactMarkdown, { Options } from "react-markdown";
+import remarkGfm from "remark-gfm";
+import remarkMath from "remark-math";
+
+import { CodeBlock } from "./codeblock";
+
+const MemoizedReactMarkdown: FC = memo(
+ ReactMarkdown,
+ (prevProps, nextProps) =>
+ prevProps.children === nextProps.children &&
+ prevProps.className === nextProps.className,
+);
+
+export default function Markdown({ content }: { content: string }) {
+ 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}
+
+ );
+}
diff --git a/nextjs-multi-modal-xmas/app/components/ui/chat/use-copy-to-clipboard.tsx b/nextjs-multi-modal-xmas/app/components/ui/chat/use-copy-to-clipboard.tsx
new file mode 100644
index 0000000..e011d69
--- /dev/null
+++ b/nextjs-multi-modal-xmas/app/components/ui/chat/use-copy-to-clipboard.tsx
@@ -0,0 +1,33 @@
+"use client";
+
+import * as React from "react";
+
+export interface useCopyToClipboardProps {
+ timeout?: number;
+}
+
+export function useCopyToClipboard({
+ timeout = 2000,
+}: useCopyToClipboardProps) {
+ const [isCopied, setIsCopied] = React.useState(false);
+
+ const copyToClipboard = (value: string) => {
+ if (typeof window === "undefined" || !navigator.clipboard?.writeText) {
+ return;
+ }
+
+ if (!value) {
+ return;
+ }
+
+ navigator.clipboard.writeText(value).then(() => {
+ setIsCopied(true);
+
+ setTimeout(() => {
+ setIsCopied(false);
+ }, timeout);
+ });
+ };
+
+ return { isCopied, copyToClipboard };
+}
diff --git a/nextjs-multi-modal-xmas/app/components/ui/file-uploader.tsx b/nextjs-multi-modal-xmas/app/components/ui/file-uploader.tsx
new file mode 100644
index 0000000..e42a267
--- /dev/null
+++ b/nextjs-multi-modal-xmas/app/components/ui/file-uploader.tsx
@@ -0,0 +1,105 @@
+"use client";
+
+import { Loader2, Paperclip } from "lucide-react";
+import { ChangeEvent, useState } from "react";
+import { buttonVariants } from "./button";
+import { cn } from "./lib/utils";
+
+export interface FileUploaderProps {
+ config?: {
+ inputId?: string;
+ fileSizeLimit?: number;
+ allowedExtensions?: string[];
+ checkExtension?: (extension: string) => string | null;
+ disabled: boolean;
+ };
+ onFileUpload: (file: File) => Promise;
+ onFileError?: (errMsg: string) => void;
+}
+
+const DEFAULT_INPUT_ID = "fileInput";
+const DEFAULT_FILE_SIZE_LIMIT = 1024 * 1024 * 50; // 50 MB
+
+export default function FileUploader({
+ config,
+ onFileUpload,
+ onFileError,
+}: FileUploaderProps) {
+ const [uploading, setUploading] = useState(false);
+
+ const inputId = config?.inputId || DEFAULT_INPUT_ID;
+ const fileSizeLimit = config?.fileSizeLimit || DEFAULT_FILE_SIZE_LIMIT;
+ const allowedExtensions = config?.allowedExtensions;
+ const defaultCheckExtension = (extension: string) => {
+ if (allowedExtensions && !allowedExtensions.includes(extension)) {
+ return `Invalid file type. Please select a file with one of these formats: ${allowedExtensions!.join(
+ ",",
+ )}`;
+ }
+ return null;
+ };
+ const checkExtension = config?.checkExtension ?? defaultCheckExtension;
+
+ const isFileSizeExceeded = (file: File) => {
+ return file.size > fileSizeLimit;
+ };
+
+ const resetInput = () => {
+ const fileInput = document.getElementById(inputId) as HTMLInputElement;
+ fileInput.value = "";
+ };
+
+ const onFileChange = async (e: ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (!file) return;
+
+ setUploading(true);
+ await handleUpload(file);
+ resetInput();
+ setUploading(false);
+ };
+
+ const handleUpload = async (file: File) => {
+ const onFileUploadError = onFileError || window.alert;
+ const fileExtension = file.name.split(".").pop() || "";
+ const extensionFileError = checkExtension(fileExtension);
+ if (extensionFileError) {
+ return onFileUploadError(extensionFileError);
+ }
+
+ if (isFileSizeExceeded(file)) {
+ return onFileUploadError(
+ `File size exceeded. Limit is ${fileSizeLimit / 1024 / 1024} MB`,
+ );
+ }
+
+ await onFileUpload(file);
+ };
+
+ return (
+
+
+
+
+ );
+}
diff --git a/nextjs-multi-modal-xmas/app/components/ui/input.tsx b/nextjs-multi-modal-xmas/app/components/ui/input.tsx
new file mode 100644
index 0000000..edfa129
--- /dev/null
+++ b/nextjs-multi-modal-xmas/app/components/ui/input.tsx
@@ -0,0 +1,25 @@
+import * as React from "react";
+
+import { cn } from "./lib/utils";
+
+export interface InputProps
+ extends React.InputHTMLAttributes {}
+
+const Input = React.forwardRef(
+ ({ className, type, ...props }, ref) => {
+ return (
+
+ );
+ },
+);
+Input.displayName = "Input";
+
+export { Input };
diff --git a/nextjs-multi-modal-xmas/app/components/ui/lib/utils.ts b/nextjs-multi-modal-xmas/app/components/ui/lib/utils.ts
new file mode 100644
index 0000000..a5ef193
--- /dev/null
+++ b/nextjs-multi-modal-xmas/app/components/ui/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));
+}
diff --git a/nextjs-multi-modal-xmas/app/components/ui/upload-image-preview.tsx b/nextjs-multi-modal-xmas/app/components/ui/upload-image-preview.tsx
new file mode 100644
index 0000000..55ef6e9
--- /dev/null
+++ b/nextjs-multi-modal-xmas/app/components/ui/upload-image-preview.tsx
@@ -0,0 +1,32 @@
+import { XCircleIcon } from "lucide-react";
+import Image from "next/image";
+import { cn } from "./lib/utils";
+
+export default function UploadImagePreview({
+ url,
+ onRemove,
+}: {
+ url: string;
+ onRemove: () => void;
+}) {
+ return (
+
+ );
+}
diff --git a/nextjs-multi-modal-xmas/app/favicon.ico b/nextjs-multi-modal-xmas/app/favicon.ico
new file mode 100644
index 0000000000000000000000000000000000000000..a1eaef62f2dfa895f1bbffc6595bb53d9604963e
GIT binary patch
literal 15406
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
KfA0a-1OEqKLE94m
literal 0
HcmV?d00001
diff --git a/nextjs-multi-modal-xmas/app/globals.css b/nextjs-multi-modal-xmas/app/globals.css
new file mode 100644
index 0000000..09b85ed
--- /dev/null
+++ b/nextjs-multi-modal-xmas/app/globals.css
@@ -0,0 +1,94 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer base {
+ :root {
+ --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;
+ }
+
+ .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;
+ }
+}
+
+@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/nextjs-multi-modal-xmas/app/layout.tsx b/nextjs-multi-modal-xmas/app/layout.tsx
new file mode 100644
index 0000000..fb09770
--- /dev/null
+++ b/nextjs-multi-modal-xmas/app/layout.tsx
@@ -0,0 +1,22 @@
+import type { Metadata } from "next";
+import { Inter } from "next/font/google";
+import "./globals.css";
+
+const inter = Inter({ subsets: ["latin"] });
+
+export const metadata: Metadata = {
+ title: "Create Llama App",
+ description: "Generated by create-llama",
+};
+
+export default function RootLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/nextjs-multi-modal-xmas/app/page.tsx b/nextjs-multi-modal-xmas/app/page.tsx
new file mode 100644
index 0000000..ef00262
--- /dev/null
+++ b/nextjs-multi-modal-xmas/app/page.tsx
@@ -0,0 +1,11 @@
+import Header from "@/app/components/header";
+import ChatSection from "./components/chat-section";
+
+export default function Home() {
+ return (
+
+
+
+
+ );
+}
diff --git a/nextjs-multi-modal-xmas/constants.ts b/nextjs-multi-modal-xmas/constants.ts
new file mode 100644
index 0000000..0959a5f
--- /dev/null
+++ b/nextjs-multi-modal-xmas/constants.ts
@@ -0,0 +1 @@
+export const MODEL = "gpt-4-vision-preview";
diff --git a/nextjs-multi-modal-xmas/data/1.jpg b/nextjs-multi-modal-xmas/data/1.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..89128a34f6414ca002a18d62e21974c26d5f88c0
GIT binary patch
literal 16769
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
literal 0
HcmV?d00001
diff --git a/nextjs-multi-modal-xmas/data/3.gif b/nextjs-multi-modal-xmas/data/3.gif
new file mode 100644
index 0000000000000000000000000000000000000000..82a95442c3a55bd4285649ed78ffbea069d679b3
GIT binary patch
literal 26616
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