Compare commits

...

6 Commits

Author SHA1 Message Date
Marcus Schiesser 61dfd74134 feat: removed non-streaming for nextjs 2023-11-24 18:04:14 +07:00
Marcus Schiesser 029ff83979 fix: set maxTokens to 4096 so vision model is not stopping too early (seems to have a lower default than other models) 2023-11-24 18:04:14 +07:00
Marcus Schiesser dbbc4cb2e1 feat: add multi-modal (file upload) and model selection to create-llama 2023-11-24 16:49:05 +07:00
Alex Yang 1cce21cdc2 feat: add loading indicator (#203) 2023-11-24 11:48:54 +08:00
yisding 8b786a51b3 create-llama 0.0.10 2023-11-23 10:56:43 -08:00
yisding ad7537dd84 llamaindex 0.0.37 2023-11-23 10:54:44 -08:00
50 changed files with 425 additions and 788 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"llamaindex": patch
---
Fixed errors (#225 and #226) Thanks @marcusschiesser
+7
View File
@@ -1,5 +1,12 @@
# mongodb-llamaindexts
## 0.0.3
### Patch Changes
- Updated dependencies [3bab231]
- llamaindex@0.0.37
## 0.0.2
### Patch Changes
+1 -1
View File
@@ -1,5 +1,5 @@
{
"version": "0.0.2",
"version": "0.0.3",
"private": true,
"name": "mongodb-llamaindexts",
"dependencies": {
+7
View File
@@ -1,5 +1,12 @@
# simple
## 0.0.35
### Patch Changes
- Updated dependencies [3bab231]
- llamaindex@0.0.37
## 0.0.34
### Patch Changes
+1 -1
View File
@@ -1,5 +1,5 @@
{
"version": "0.0.34",
"version": "0.0.35",
"private": true,
"name": "simple",
"dependencies": {
+6
View File
@@ -1,5 +1,11 @@
# llamaindex
## 0.0.37
### Patch Changes
- 3bab231: Fixed errors (#225 and #226) Thanks @marcusschiesser
## 0.0.36
### Patch Changes
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "llamaindex",
"version": "0.0.36",
"version": "0.0.37",
"license": "MIT",
"dependencies": {
"@anthropic-ai/sdk": "^0.9.1",
@@ -13,7 +13,7 @@
"md-utils-ts": "^2.0.0",
"mongodb": "^6.3.0",
"notion-md-crawler": "^0.0.2",
"openai": "^4.19.1",
"openai": "^4.20.0",
"papaparse": "^5.4.1",
"pdf-parse": "^1.1.1",
"pg": "^8.11.3",
+41 -20
View File
@@ -30,7 +30,7 @@ export interface ChatEngine {
T extends boolean | undefined = undefined,
R = T extends true ? AsyncGenerator<string, void, unknown> : Response,
>(
message: string,
message: MessageContent,
chatHistory?: ChatMessage[],
streaming?: T,
): Promise<R>;
@@ -56,7 +56,11 @@ export class SimpleChatEngine implements ChatEngine {
async chat<
T extends boolean | undefined = undefined,
R = T extends true ? AsyncGenerator<string, void, unknown> : Response,
>(message: string, chatHistory?: ChatMessage[], streaming?: T): Promise<R> {
>(
message: MessageContent,
chatHistory?: ChatMessage[],
streaming?: T,
): Promise<R> {
//Streaming option
if (streaming) {
return this.streamChat(message, chatHistory) as R;
@@ -72,7 +76,7 @@ export class SimpleChatEngine implements ChatEngine {
}
protected async *streamChat(
message: string,
message: MessageContent,
chatHistory?: ChatMessage[],
): AsyncGenerator<string, void, unknown> {
chatHistory = chatHistory ?? this.chatHistory;
@@ -144,14 +148,14 @@ export class CondenseQuestionChatEngine implements ChatEngine {
T extends boolean | undefined = undefined,
R = T extends true ? AsyncGenerator<string, void, unknown> : Response,
>(
message: string,
message: MessageContent,
chatHistory?: ChatMessage[] | undefined,
streaming?: T,
): Promise<R> {
chatHistory = chatHistory ?? this.chatHistory;
const condensedQuestion = (
await this.condenseQuestion(chatHistory, message)
await this.condenseQuestion(chatHistory, extractText(message))
).message.content;
const response = await this.queryEngine.query(condensedQuestion);
@@ -256,7 +260,7 @@ export class ContextChatEngine implements ChatEngine {
T extends boolean | undefined = undefined,
R = T extends true ? AsyncGenerator<string, void, unknown> : Response,
>(
message: string,
message: MessageContent,
chatHistory?: ChatMessage[] | undefined,
streaming?: T,
): Promise<R> {
@@ -272,7 +276,10 @@ export class ContextChatEngine implements ChatEngine {
type: "wrapper",
tags: ["final"],
};
const context = await this.contextGenerator.generate(message, parentEvent);
const context = await this.contextGenerator.generate(
extractText(message),
parentEvent,
);
chatHistory.push({ content: message, role: "user" });
@@ -291,7 +298,7 @@ export class ContextChatEngine implements ChatEngine {
}
protected async *streamChat(
message: string,
message: MessageContent,
chatHistory?: ChatMessage[] | undefined,
): AsyncGenerator<string, void, unknown> {
chatHistory = chatHistory ?? this.chatHistory;
@@ -301,7 +308,10 @@ export class ContextChatEngine implements ChatEngine {
type: "wrapper",
tags: ["final"],
};
const context = await this.contextGenerator.generate(message, parentEvent);
const context = await this.contextGenerator.generate(
extractText(message),
parentEvent,
);
chatHistory.push({ content: message, role: "user" });
@@ -330,8 +340,8 @@ export class ContextChatEngine implements ChatEngine {
export interface MessageContentDetail {
type: "text" | "image_url";
text: string;
image_url: { url: string };
text?: string;
image_url?: { url: string };
}
/**
@@ -339,6 +349,24 @@ export interface MessageContentDetail {
*/
export type MessageContent = string | MessageContentDetail[];
/**
* Extracts just the text from a multi-modal message or the message itself if it's just text.
*
* @param message The message to extract text from.
* @returns The extracted text
*/
function extractText(message: MessageContent): string {
if (Array.isArray(message)) {
// message is of type MessageContentDetail[] - retrieve just the text parts and concatenate them
// so we can pass them to the context generator
return (message as MessageContentDetail[])
.filter((c) => c.type === "text")
.map((c) => c.text)
.join("\n\n");
}
return message;
}
/**
* HistoryChatEngine is a ChatEngine that uses a `ChatHistory` object
* to keeps track of chat's message history.
@@ -413,15 +441,8 @@ export class HistoryChatEngine {
let requestMessages;
let context;
if (this.contextGenerator) {
if (Array.isArray(message)) {
// message is of type MessageContentDetail[] - retrieve just the text parts and concatenate them
// so we can pass them to the context generator
message = (message as MessageContentDetail[])
.filter((c) => c.type === "text")
.map((c) => c.text)
.join("\n\n");
}
context = await this.contextGenerator.generate(message);
const textOnly = extractText(message);
context = await this.contextGenerator.generate(textOnly);
}
requestMessages = await chatHistory.requestMessages(
context ? [context.message] : undefined,
+6
View File
@@ -1,5 +1,11 @@
# create-llama
## 0.0.10
### Patch Changes
- Bugfixes (thanks @marcusschiesser)
## 0.0.9
### Patch Changes
+11 -7
View File
@@ -12,6 +12,14 @@ import terminalLink from "terminal-link";
import type { InstallTemplateArgs } from "./templates";
import { installTemplate } from "./templates";
export type InstallAppArgs = Omit<
InstallTemplateArgs,
"appName" | "root" | "isOnline" | "customApiPath"
> & {
appPath: string;
frontend: boolean;
};
export async function createApp({
template,
framework,
@@ -22,13 +30,8 @@ export async function createApp({
eslint,
frontend,
openAIKey,
}: Omit<
InstallTemplateArgs,
"appName" | "root" | "isOnline" | "customApiPath"
> & {
appPath: string;
frontend: boolean;
}): Promise<void> {
model,
}: InstallAppArgs): Promise<void> {
const root = path.resolve(appPath);
if (!(await isWriteable(path.dirname(root)))) {
@@ -65,6 +68,7 @@ export async function createApp({
isOnline,
eslint,
openAIKey,
model,
};
if (frontend) {
+61 -30
View File
@@ -8,7 +8,7 @@ import path from "path";
import { blue, bold, cyan, green, red, yellow } from "picocolors";
import prompts from "prompts";
import checkForUpdate from "update-check";
import { createApp } from "./create-app";
import { InstallAppArgs, createApp } from "./create-app";
import { getPkgManager } from "./helpers/get-pkg-manager";
import { isFolderEmpty } from "./helpers/is-folder-empty";
import { validateNpmName } from "./helpers/validate-pkg";
@@ -155,21 +155,22 @@ async function run(): Promise<void> {
process.exit(1);
}
const preferences = (conf.get("preferences") || {}) as Record<
string,
boolean | string
>;
// TODO: use Args also for program
type Args = Omit<InstallAppArgs, "appPath" | "packageManager">;
const defaults: typeof preferences = {
template: "simple",
const preferences = (conf.get("preferences") || {}) as Args;
const defaults: Args = {
template: "streaming",
framework: "nextjs",
engine: "simple",
ui: "html",
eslint: true,
frontend: false,
openAIKey: "",
model: "gpt-3.5-turbo",
};
const getPrefOrDefault = (field: string) =>
const getPrefOrDefault = (field: keyof Args) =>
preferences[field] ?? defaults[field];
const handlers = {
@@ -179,28 +180,6 @@ async function run(): Promise<void> {
},
};
if (!program.template) {
if (ciInfo.isCI) {
program.template = getPrefOrDefault("template");
} else {
const { template } = await prompts(
{
type: "select",
name: "template",
message: "Which template would you like to use?",
choices: [
{ title: "Chat without streaming", value: "simple" },
{ title: "Chat with streaming", value: "streaming" },
],
initial: 1,
},
handlers,
);
program.template = template;
preferences.template = template;
}
}
if (!program.framework) {
if (ciInfo.isCI) {
program.framework = getPrefOrDefault("framework");
@@ -224,6 +203,31 @@ async function run(): Promise<void> {
}
}
if (program.framework === "nextjs") {
program.template = "streaming";
}
if (!program.template) {
if (ciInfo.isCI) {
program.template = getPrefOrDefault("template");
} else {
const { template } = await prompts(
{
type: "select",
name: "template",
message: "Which template would you like to use?",
choices: [
{ title: "Chat without streaming", value: "simple" },
{ title: "Chat with streaming", value: "streaming" },
],
initial: 1,
},
handlers,
);
program.template = template;
preferences.template = template;
}
}
if (program.framework === "express" || program.framework === "fastapi") {
// if a backend-only framework is selected, ask whether we should create a frontend
if (!program.frontend) {
@@ -277,6 +281,32 @@ async function run(): Promise<void> {
}
}
if (program.framework === "nextjs") {
if (!program.model) {
if (ciInfo.isCI) {
program.model = getPrefOrDefault("model");
} else {
const { model } = await prompts(
{
type: "select",
name: "model",
message: "Which model would you like to use?",
choices: [
{ title: "gpt-3.5-turbo", value: "gpt-3.5-turbo" },
{ title: "gpt-4", value: "gpt-4" },
{ title: "gpt-4-1106-preview", value: "gpt-4-1106-preview" },
{ title: "gpt-4-vision-preview", value: "gpt-4-vision-preview" },
],
initial: 0,
},
handlers,
);
program.model = model;
preferences.model = model;
}
}
}
if (program.framework === "express" || program.framework === "nextjs") {
if (!program.engine) {
if (ciInfo.isCI) {
@@ -350,6 +380,7 @@ async function run(): Promise<void> {
eslint: program.eslint,
frontend: program.frontend,
openAIKey: program.openAIKey,
model: program.model,
});
conf.set("preferences", preferences);
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "create-llama",
"version": "0.0.9",
"version": "0.0.10",
"keywords": [
"rag",
"llamaindex",
@@ -1,29 +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" | "handleSubmit" | "handleInputChange" | "input"
>,
| "isLoading"
| "input"
| "onFileUpload"
| "onFileError"
| "handleSubmit"
| "handleInputChange"
> & {
multiModal?: boolean;
},
) {
const [imageUrl, setImageUrl] = useState<string | null>(null);
const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
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<string>((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 (
<form
onSubmit={props.handleSubmit}
className="flex w-full items-start justify-between gap-4 rounded-xl bg-white p-4 shadow-xl"
onSubmit={onSubmit}
className="rounded-xl bg-white p-4 shadow-xl space-y-4"
>
<Input
autoFocus
name="message"
placeholder="Type a message"
className="flex-1"
value={props.input}
onChange={props.handleInputChange}
/>
<Button type="submit" disabled={props.isLoading}>
Send message
</Button>
{imageUrl && (
<UploadImagePreview url={imageUrl} onRemove={onRemovePreviewImage} />
)}
<div className="flex w-full items-start justify-between gap-4 ">
<Input
autoFocus
name="message"
placeholder="Type a message"
className="flex-1"
value={props.input}
onChange={props.handleInputChange}
/>
<FileUploader
onFileUpload={handleUploadFile}
onFileError={props.onFileError}
/>
<Button type="submit" disabled={props.isLoading}>
Send message
</Button>
</div>
</form>
);
}
@@ -1,4 +1,5 @@
import { useEffect, useRef } from "react";
import { Loader2 } from "lucide-react";
import ChatActions from "./chat-actions";
import ChatMessage from "./chat-message";
@@ -24,6 +25,11 @@ export default function ChatMessages(
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]);
@@ -37,6 +43,13 @@ export default function ChatMessages(
{props.messages.map((m) => (
<ChatMessage key={m.id} {...m} />
))}
{isPending && (
<div
className='flex justify-center items-center pt-10'
>
<Loader2 className="h-4 w-4 animate-spin"/>
</div>
)}
</div>
<div className="flex justify-end py-4">
<ChatActions
@@ -8,8 +8,15 @@ export interface ChatHandler {
messages: Message[];
input: string;
isLoading: boolean;
handleSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
handleSubmit: (
e: React.FormEvent<HTMLFormElement>,
ops?: {
data?: any;
},
) => void;
handleInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
reload?: () => void;
stop?: () => void;
onFileUpload?: (file: File) => Promise<void>;
onFileError?: (errMsg: string) => void;
}
@@ -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<void>;
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<HTMLInputElement>) => {
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 (
<div className="self-stretch">
<input
type="file"
id={inputId}
style={{ display: "none" }}
onChange={onFileChange}
accept={allowedExtensions?.join(",")}
disabled={config?.disabled || uploading}
/>
<label
htmlFor={inputId}
className={cn(
buttonVariants({ variant: "secondary", size: "icon" }),
"cursor-pointer",
uploading && "opacity-50",
)}
>
{uploading ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Paperclip className="-rotate-45 w-4 h-4" />
)}
</label>
</div>
);
}
@@ -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 (
<div className="relative w-20 h-20 group">
<Image
src={url}
alt="Uploaded image"
fill
className="object-cover w-full h-full rounded-xl hover:brightness-75"
/>
<div
className={cn(
"absolute -top-2 -right-2 w-6 h-6 z-10 bg-gray-500 text-white rounded-full hidden group-hover:block",
)}
>
<XCircleIcon
className="w-6 h-6 bg-gray-500 text-white rounded-full"
onClick={onRemove}
/>
</div>
</div>
);
}
+9
View File
@@ -101,6 +101,7 @@ const installTSTemplate = async ({
eslint,
customApiPath,
forBackend,
model,
}: InstallTemplateArgs) => {
console.log(bold(`Using ${packageManager}.`));
@@ -173,6 +174,14 @@ const installTSTemplate = async ({
});
}
if (framework === "nextjs") {
await fs.writeFile(
path.join(root, "constants.ts"),
`export const MODEL = "${model || "gpt-3.5-turbo"}";\n`,
);
console.log("\nUsing OpenAI model: ", model || "gpt-3.5-turbo", "\n");
}
/**
* Update the package.json scripts.
*/
+1
View File
@@ -18,4 +18,5 @@ export interface InstallTemplateArgs {
customApiPath?: string;
openAIKey?: string;
forBackend?: string;
model: string;
}
@@ -1,3 +0,0 @@
# Rename this file to `.env.local` to use environment variables locally with `next dev`
# https://nextjs.org/docs/pages/building-your-application/configuring/environment-variables
MY_HOST="example.com"
@@ -1,30 +0,0 @@
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).
## 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!
@@ -1,7 +0,0 @@
import { LLM, SimpleChatEngine } from "llamaindex";
export async function createChatEngine(llm: LLM) {
return new SimpleChatEngine({
llm,
});
}
@@ -1,47 +0,0 @@
import { ChatMessage, OpenAI } from "llamaindex";
import { NextRequest, NextResponse } from "next/server";
import { createChatEngine } from "./engine";
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { messages }: { messages: ChatMessage[] } = 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: "gpt-3.5-turbo",
});
const chatEngine = await createChatEngine(llm);
const response = await chatEngine.chat(lastMessage.content, messages);
const result: ChatMessage = {
role: "assistant",
content: response.response,
};
return NextResponse.json({ result });
} catch (error) {
console.error("[LlamaIndex]", error);
return NextResponse.json(
{
error: (error as Error).message,
},
{
status: 500,
},
);
}
}
@@ -1,80 +0,0 @@
"use client";
import { nanoid } from "nanoid";
import { useState } from "react";
import { ChatInput, ChatInputProps, ChatMessages, Message } from "./ui/chat";
function useChat(): ChatInputProps & { messages: Message[] } {
const [messages, setMessages] = useState<Message[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [input, setInput] = useState("");
const getAssistantMessage = async (messages: Message[]) => {
const response = await fetch(
process.env.NEXT_PUBLIC_CHAT_API ?? "/api/chat",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
messages,
}),
},
);
const data = await response.json();
const assistantMessage = data.result as Message;
return assistantMessage;
};
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!input) return;
setIsLoading(true);
try {
const newMessages = [
...messages,
{ id: nanoid(), content: input, role: "user" },
];
setMessages(newMessages);
setInput("");
const assistantMessage = await getAssistantMessage(newMessages);
setMessages([...newMessages, { ...assistantMessage, id: nanoid() }]);
} catch (error: any) {
console.log(error);
alert(error.message);
} finally {
setIsLoading(false);
}
};
const handleInputChange = (e: any): void => {
setInput(e.target.value);
};
return {
messages,
isLoading,
input,
handleSubmit,
handleInputChange,
};
}
export default function ChatSection() {
const { messages, isLoading, input, handleSubmit, handleInputChange } =
useChat();
return (
<div className="space-y-4 max-w-5xl w-full">
<ChatMessages messages={messages} />
<ChatInput
handleSubmit={handleSubmit}
isLoading={isLoading}
input={input}
handleInputChange={handleInputChange}
/>
</div>
);
}
@@ -1,28 +0,0 @@
import Image from "next/image";
export default function Header() {
return (
<div className="z-10 max-w-5xl w-full items-center justify-between font-mono text-sm lg:flex">
<p className="fixed left-0 top-0 flex w-full justify-center border-b border-gray-300 bg-gradient-to-b from-zinc-200 pb-6 pt-8 backdrop-blur-2xl dark:border-neutral-800 dark:bg-zinc-800/30 dark:from-inherit lg:static lg:w-auto lg:rounded-xl lg:border lg:bg-gray-200 lg:p-4 lg:dark:bg-zinc-800/30">
Get started by editing&nbsp;
<code className="font-mono font-bold">app/page.tsx</code>
</p>
<div className="fixed bottom-0 left-0 flex h-48 w-full items-end justify-center bg-gradient-to-t from-white via-white dark:from-black dark:via-black lg:static lg:h-auto lg:w-auto lg:bg-none">
<a
href="https://www.llamaindex.ai/"
className="flex items-center justify-center font-nunito text-lg font-bold gap-2"
>
<span>Built by LlamaIndex</span>
<Image
className="rounded-xl"
src="/llama.png"
alt="Llama Logo"
width={40}
height={40}
priority
/>
</a>
</div>
</div>
);
}
@@ -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>
);
}
@@ -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>
</>
);
}
@@ -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>
);
}
@@ -1,38 +0,0 @@
"use client";
import { useEffect, useRef } from "react";
import ChatItem from "./chat-item";
export interface Message {
id: string;
content: string;
role: string;
}
export default function ChatMessages({ messages }: { messages: Message[] }) {
const scrollableChatContainerRef = useRef<HTMLDivElement>(null);
const scrollToBottom = () => {
if (scrollableChatContainerRef.current) {
scrollableChatContainerRef.current.scrollTop =
scrollableChatContainerRef.current.scrollHeight;
}
};
useEffect(() => {
scrollToBottom();
}, [messages.length]);
return (
<div className="w-full max-w-5xl p-4 bg-white rounded-xl shadow-xl">
<div
className="flex flex-col gap-5 divide-y h-[50vh] overflow-auto"
ref={scrollableChatContainerRef}
>
{messages.map((m: Message) => (
<ChatItem key={m.id} {...m} />
))}
</div>
</div>
);
}
@@ -1,6 +0,0 @@
import ChatInput from "./chat-input";
import ChatMessages from "./chat-messages";
export type { ChatInputProps } from "./chat-input";
export type { Message } from "./chat-messages";
export { ChatInput, ChatMessages };
Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

@@ -1,94 +0,0 @@
@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%);
}
}
@@ -1,22 +0,0 @@
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 (
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
);
}
@@ -1,11 +0,0 @@
import ChatSection from "@/app/components/chat-section";
import Header from "@/app/components/header";
export default function Home() {
return (
<main className="flex min-h-screen flex-col items-center gap-10 p-24 background-gradient">
<Header />
<ChatSection />
</main>
);
}
@@ -1,3 +0,0 @@
{
"extends": "next/core-web-vitals"
}
@@ -1,35 +0,0 @@
# 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
@@ -1,5 +0,0 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
@@ -1,21 +0,0 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
webpack: (config) => {
// See https://webpack.js.org/configuration/resolve/#resolvealias
config.resolve.alias = {
...config.resolve.alias,
sharp$: false,
"onnxruntime-node$": false,
mongodb$: false,
};
return config;
},
experimental: {
serverComponentsExternalPackages: ["llamaindex"],
outputFileTracingIncludes: {
"/*": ["./cache/**/*"],
},
},
};
module.exports = nextConfig;
@@ -1,23 +0,0 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
output: "export",
images: { unoptimized: true },
webpack: (config) => {
// See https://webpack.js.org/configuration/resolve/#resolvealias
config.resolve.alias = {
...config.resolve.alias,
sharp$: false,
"onnxruntime-node$": false,
mongodb$: false,
};
return config;
},
experimental: {
serverComponentsExternalPackages: ["llamaindex"],
outputFileTracingIncludes: {
"/*": ["./cache/**/*"],
},
},
};
module.exports = nextConfig;
@@ -1,29 +0,0 @@
{
"name": "llama-index-nextjs",
"version": "1.0.0",
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"llamaindex": "0.0.31",
"dotenv": "^16.3.1",
"nanoid": "^5",
"next": "^13",
"react": "^18",
"react-dom": "^18"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"autoprefixer": "^10.1",
"eslint": "^8",
"eslint-config-next": "^13",
"postcss": "^8",
"tailwindcss": "^3.3",
"typescript": "^5"
}
}
@@ -1,6 +0,0 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

@@ -1,78 +0,0 @@
import type { Config } from "tailwindcss";
import { fontFamily } from "tailwindcss/defaultTheme";
const config: Config = {
darkMode: ["class"],
content: ["app/**/*.{ts,tsx}", "components/**/*.{ts,tsx}"],
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
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",
},
},
},
plugins: [],
};
export default config;
@@ -1,41 +0,0 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": [
"./*"
]
},
"forceConsistentCasingInFileNames": true,
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}
@@ -1,5 +1,6 @@
import { MODEL } from "@/constants";
import { Message, StreamingTextResponse } from "ai";
import { OpenAI } from "llamaindex";
import { MessageContent, OpenAI } from "llamaindex";
import { NextRequest, NextResponse } from "next/server";
import { createChatEngine } from "./engine";
import { LlamaIndexStream } from "./llamaindex-stream";
@@ -7,10 +8,29 @@ 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 }: { messages: Message[] } = body;
const { messages, data }: { messages: Message[]; data: any } = body;
const lastMessage = messages.pop();
if (!messages || !lastMessage || lastMessage.role !== "user") {
return NextResponse.json(
@@ -23,12 +43,22 @@ export async function POST(request: NextRequest) {
}
const llm = new OpenAI({
model: "gpt-3.5-turbo",
model: MODEL,
maxTokens: 4096,
});
const chatEngine = await createChatEngine(llm);
const response = await chatEngine.chat(lastMessage.content, messages, true);
const lastMessageContent = getLastMessageContent(
lastMessage.content,
data?.imageUrl,
);
const response = await chatEngine.chat(
lastMessageContent as MessageContent,
messages,
true,
);
// Transform the response into a readable stream
const stream = LlamaIndexStream(response);
@@ -1,5 +1,6 @@
"use client";
import { MODEL } from "@/constants";
import { useChat } from "ai/react";
import { ChatInput, ChatMessages } from "./ui/chat";
@@ -27,6 +28,7 @@ export default function ChatSection() {
handleSubmit={handleSubmit}
handleInputChange={handleInputChange}
isLoading={isLoading}
multiModal={MODEL === "gpt-4-vision-preview"}
/>
</div>
);
@@ -12,6 +12,7 @@ export interface ChatInputProps {
/** Form submission handler to automatically reset input and append a user message */
handleSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
isLoading: boolean;
multiModal?: boolean;
}
export default function ChatInput(props: ChatInputProps) {
@@ -0,0 +1 @@
export const MODEL = "gpt-4-vision-preview";
@@ -8,7 +8,7 @@
"lint": "next lint"
},
"dependencies": {
"ai": "^2.2.5",
"ai": "^2.2.25",
"llamaindex": "0.0.31",
"dotenv": "^16.3.1",
"next": "^13",
+4 -4
View File
@@ -178,8 +178,8 @@ importers:
specifier: ^0.0.2
version: 0.0.2
openai:
specifier: ^4.19.1
version: 4.19.1
specifier: ^4.20.0
version: 4.20.0
papaparse:
specifier: ^5.4.1
version: 5.4.1
@@ -11469,8 +11469,8 @@ packages:
is-wsl: 2.2.0
dev: false
/openai@4.19.1:
resolution: {integrity: sha512-9TddzuZBn2xxhghGGTHLZ4EeNBGTLs3xVzh266NiSJvtUsCsZQ5yVV6H5NhnhyAkKK8uUiZOUUlUAk3HdV+4xg==}
/openai@4.20.0:
resolution: {integrity: sha512-VbAYerNZFfIIeESS+OL9vgDkK8Mnri55n+jN0UN/HZeuM0ghGh6nDN6UGRZxslNgyJ7XmY/Ca9DO4YYyvrszGA==}
hasBin: true
dependencies:
'@types/node': 18.18.12