mirror of
https://github.com/run-llama/create-llama.git
synced 2026-07-16 11:04:26 -04:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0a2c48fd50 | |||
| 508eebe39c | |||
| f9a4f7644a | |||
| 34f95bfa29 |
@@ -14,7 +14,6 @@ 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;
|
||||
messages: Message[];
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ export const chat = async (req: Request, res: Response) => {
|
||||
const stream = LlamaIndexStream(response, vercelStreamData, {
|
||||
parserOptions: {
|
||||
imageUrl: data?.imageUrl,
|
||||
uploadedCsv: data?.uploadedCsv,
|
||||
csvFiles: data?.csvFiles,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
|
||||
import { AgentStreamChatResponse } from "llamaindex/agent/base";
|
||||
import {
|
||||
UploadedCsv,
|
||||
CsvFile,
|
||||
appendCsvData,
|
||||
appendImageData,
|
||||
appendSourceData,
|
||||
@@ -27,7 +27,7 @@ type LlamaIndexResponse =
|
||||
|
||||
export type DataParserOptions = {
|
||||
imageUrl?: string;
|
||||
uploadedCsv?: UploadedCsv;
|
||||
csvFiles?: CsvFile[];
|
||||
};
|
||||
|
||||
export const convertMessageContent = (
|
||||
@@ -50,12 +50,12 @@ export const convertMessageContent = (
|
||||
});
|
||||
}
|
||||
|
||||
if (additionalData?.uploadedCsv) {
|
||||
if (additionalData?.csvFiles?.length) {
|
||||
const rawContents = additionalData.csvFiles.map((csv) => {
|
||||
return "```csv\n" + csv.content + "\n```";
|
||||
});
|
||||
const csvContent =
|
||||
"Use the following CSV data:\n" +
|
||||
"```csv\n" +
|
||||
additionalData.uploadedCsv.content +
|
||||
"\n```";
|
||||
"Use data from following CSV raw contents:\n" + rawContents.join("\n\n");
|
||||
content.push({
|
||||
type: "text",
|
||||
text: `${csvContent}\n\n${textMessage}`,
|
||||
@@ -77,7 +77,7 @@ function createParser(
|
||||
return new ReadableStream<string>({
|
||||
start() {
|
||||
appendImageData(data, opts?.imageUrl);
|
||||
appendCsvData(data, opts?.uploadedCsv);
|
||||
appendCsvData(data, opts?.csvFiles);
|
||||
},
|
||||
async pull(controller): Promise<void> {
|
||||
const { value, done } = await it.next();
|
||||
|
||||
@@ -122,16 +122,19 @@ export function createCallbackManager(stream: StreamData) {
|
||||
return callbackManager;
|
||||
}
|
||||
|
||||
export type UploadedCsv = {
|
||||
export type CsvFile = {
|
||||
content: string;
|
||||
filename: string;
|
||||
filesize: number;
|
||||
id: string;
|
||||
};
|
||||
|
||||
export function appendCsvData(data: StreamData, uploadedCsv?: UploadedCsv) {
|
||||
if (!uploadedCsv) return;
|
||||
export function appendCsvData(data: StreamData, csvFiles?: CsvFile[]) {
|
||||
if (!csvFiles) return;
|
||||
data.appendMessageAnnotation({
|
||||
type: "csv",
|
||||
data: uploadedCsv,
|
||||
data: {
|
||||
csvFiles,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
|
||||
import { AgentStreamChatResponse } from "llamaindex/agent/base";
|
||||
import {
|
||||
UploadedCsv,
|
||||
CsvFile,
|
||||
appendCsvData,
|
||||
appendImageData,
|
||||
appendSourceData,
|
||||
@@ -27,7 +27,7 @@ type LlamaIndexResponse =
|
||||
|
||||
export type DataParserOptions = {
|
||||
imageUrl?: string;
|
||||
uploadedCsv?: UploadedCsv;
|
||||
csvFiles?: CsvFile[];
|
||||
};
|
||||
|
||||
export const convertMessageContent = (
|
||||
@@ -50,12 +50,12 @@ export const convertMessageContent = (
|
||||
});
|
||||
}
|
||||
|
||||
if (additionalData?.uploadedCsv) {
|
||||
if (additionalData?.csvFiles?.length) {
|
||||
const rawContents = additionalData.csvFiles.map((csv) => {
|
||||
return "```csv\n" + csv.content + "\n```";
|
||||
});
|
||||
const csvContent =
|
||||
"Use the following CSV data:\n" +
|
||||
"```csv\n" +
|
||||
additionalData.uploadedCsv.content +
|
||||
"\n```";
|
||||
"Use data from following CSV raw contents:\n" + rawContents.join("\n\n");
|
||||
content.push({
|
||||
type: "text",
|
||||
text: `${csvContent}\n\n${textMessage}`,
|
||||
@@ -77,7 +77,7 @@ function createParser(
|
||||
return new ReadableStream<string>({
|
||||
start() {
|
||||
appendImageData(data, opts?.imageUrl);
|
||||
appendCsvData(data, opts?.uploadedCsv);
|
||||
appendCsvData(data, opts?.csvFiles);
|
||||
},
|
||||
async pull(controller): Promise<void> {
|
||||
const { value, done } = await it.next();
|
||||
|
||||
@@ -60,7 +60,7 @@ export async function POST(request: NextRequest) {
|
||||
const stream = LlamaIndexStream(response, vercelStreamData, {
|
||||
parserOptions: {
|
||||
imageUrl: data?.imageUrl,
|
||||
uploadedCsv: data?.uploadedCsv,
|
||||
csvFiles: data?.csvFiles,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -122,16 +122,19 @@ export function createCallbackManager(stream: StreamData) {
|
||||
return callbackManager;
|
||||
}
|
||||
|
||||
export type UploadedCsv = {
|
||||
export type CsvFile = {
|
||||
content: string;
|
||||
filename: string;
|
||||
filesize: number;
|
||||
id: string;
|
||||
};
|
||||
|
||||
export function appendCsvData(data: StreamData, uploadedCsv?: UploadedCsv) {
|
||||
if (!uploadedCsv) return;
|
||||
export function appendCsvData(data: StreamData, csvFiles?: CsvFile[]) {
|
||||
if (!csvFiles) return;
|
||||
data.appendMessageAnnotation({
|
||||
type: "csv",
|
||||
data: uploadedCsv,
|
||||
data: {
|
||||
csvFiles,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { CsvData, getInputResources } from ".";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { Button } from "../button";
|
||||
import FileUploader from "../file-uploader";
|
||||
import { Input } from "../input";
|
||||
import UploadCsvPreview from "../upload-csv-preview";
|
||||
import UploadImagePreview from "../upload-image-preview";
|
||||
import ChatResources from "./chat-resources";
|
||||
import { ChatHandler } from "./chat.interface";
|
||||
import { useCsv } from "./use-csv";
|
||||
|
||||
export default function ChatInput(
|
||||
props: Pick<
|
||||
@@ -21,17 +22,9 @@ export default function ChatInput(
|
||||
>,
|
||||
) {
|
||||
const [imageUrl, setImageUrl] = useState<string | null>(null);
|
||||
const [uploadedCsv, setUploadedCsv] = useState<CsvData>();
|
||||
const [inputResources, setInputResources] = useState<
|
||||
Array<CsvData & { selected: boolean }>
|
||||
>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const resources = getInputResources(props.messages);
|
||||
setInputResources(
|
||||
resources.csv.map((data) => ({ ...data, selected: true })),
|
||||
);
|
||||
}, [props.messages]);
|
||||
const { files, uploadNew, removeFile, resetUploadedFiles } = useCsv(
|
||||
props.messages,
|
||||
);
|
||||
|
||||
const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
if (imageUrl) {
|
||||
@@ -41,21 +34,12 @@ export default function ChatInput(
|
||||
setImageUrl(null);
|
||||
return;
|
||||
}
|
||||
// if users upload a new csv file, we will send it to backend
|
||||
if (uploadedCsv) {
|
||||
props.handleSubmit(e, {
|
||||
data: { uploadedCsv },
|
||||
});
|
||||
setUploadedCsv(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
// if users upload a new csv file, we can reuse provided csv resources
|
||||
const attachCsv = inputResources.filter((r) => r.selected)[0];
|
||||
if (attachCsv) {
|
||||
if (files.length > 0) {
|
||||
props.handleSubmit(e, {
|
||||
data: { uploadedCsv: attachCsv },
|
||||
data: { csvFiles: files },
|
||||
});
|
||||
resetUploadedFiles();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -81,11 +65,15 @@ export default function ChatInput(
|
||||
reader.onload = () => resolve(reader.result as string);
|
||||
reader.onerror = (error) => reject(error);
|
||||
});
|
||||
setUploadedCsv({
|
||||
const isSuccess = uploadNew({
|
||||
id: uuidv4(),
|
||||
content,
|
||||
filename: file.name,
|
||||
filesize: file.size,
|
||||
});
|
||||
if (!isSuccess) {
|
||||
alert("File already exists in the list.");
|
||||
}
|
||||
};
|
||||
|
||||
const handleUploadFile = async (file: File) => {
|
||||
@@ -102,33 +90,37 @@ export default function ChatInput(
|
||||
}
|
||||
};
|
||||
|
||||
const removeResource = (index: number) => {
|
||||
setInputResources((resources) => {
|
||||
const newResources = [...resources];
|
||||
newResources[index].selected = false;
|
||||
return newResources;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={onSubmit}
|
||||
className="rounded-xl bg-white p-4 shadow-xl space-y-4"
|
||||
>
|
||||
<ChatResources
|
||||
isLoading={props.isLoading}
|
||||
resources={inputResources}
|
||||
removeResource={removeResource}
|
||||
/>
|
||||
{imageUrl && (
|
||||
<UploadImagePreview url={imageUrl} onRemove={onRemovePreviewImage} />
|
||||
)}
|
||||
{uploadedCsv && (
|
||||
<UploadCsvPreview
|
||||
filename={uploadedCsv.filename}
|
||||
filesize={uploadedCsv.filesize}
|
||||
onRemove={() => setUploadedCsv(undefined)}
|
||||
/>
|
||||
{files.length > 0 && (
|
||||
<div className="flex gap-4 w-full overflow-auto py-2">
|
||||
{props.isLoading ? (
|
||||
<div className="flex gap-2 items-center">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />{" "}
|
||||
<span>Handling csv files...</span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{files.map((csv) => {
|
||||
return (
|
||||
<UploadCsvPreview
|
||||
key={csv.id}
|
||||
filename={csv.filename}
|
||||
filesize={csv.filesize}
|
||||
onRemove={() => removeFile(csv)}
|
||||
isNew={csv.type === "new_upload"}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex w-full items-start justify-between gap-4 ">
|
||||
<Input
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import { Loader2, XIcon } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import { CsvData } from ".";
|
||||
import SheetIcon from "../../ui/icons/sheet.svg";
|
||||
|
||||
export interface ChatResourcesProps {
|
||||
isLoading: boolean;
|
||||
resources: Array<CsvData & { selected: boolean }>;
|
||||
removeResource: (index: number) => void;
|
||||
}
|
||||
|
||||
export default function ChatResources(props: ChatResourcesProps) {
|
||||
if (!props.resources.length) return null;
|
||||
return (
|
||||
<div className="flex gap-4 text-sm">
|
||||
{props.resources.map((data, index) => {
|
||||
if (!data.selected) return null;
|
||||
const fileSizeInKB = Math.round((data.filesize / 1024) * 10) / 10;
|
||||
return (
|
||||
<div
|
||||
className="border-2 border-green-700 py-2 px-3 rounded-lg flex gap-2 items-center"
|
||||
key={data.filename}
|
||||
>
|
||||
<div className="h-4 w-4 shrink-0 rounded-md">
|
||||
<Image
|
||||
className="h-full w-auto"
|
||||
priority
|
||||
src={SheetIcon}
|
||||
alt="SheetIcon"
|
||||
/>
|
||||
</div>
|
||||
<span>
|
||||
{data.filename} - {fileSizeInKB} KB
|
||||
</span>
|
||||
{props.isLoading ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<XIcon
|
||||
className="w-4 h-4 cursor-pointer"
|
||||
onClick={() => props.removeResource(index)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +1,16 @@
|
||||
import { CsvData } from ".";
|
||||
|
||||
const LIMIT_DISPLAY = 100; // Limit the display of CSV content to 100 characters
|
||||
import CsvDialog from "./widgets/CsvDialog";
|
||||
|
||||
export default function CsvContent({ data }: { data: CsvData }) {
|
||||
const summaryContent = data.content.slice(0, LIMIT_DISPLAY) + "...";
|
||||
if (!data.csvFiles.length) return null;
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<h3 className="font-semibold">CSV Raw Content</h3>
|
||||
<pre className="bg-secondary max-h-[200px] overflow-auto rounded-md p-4 block text-sm">
|
||||
{summaryContent}
|
||||
</pre>
|
||||
<div>
|
||||
<p className="font-semibold mb-2">Using data from following CSV files:</p>
|
||||
<div className="flex gap-2 items-center">
|
||||
{data.csvFiles.map((csv, index) => (
|
||||
<CsvDialog key={index} csv={csv} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { JSONValue, Message } from "ai";
|
||||
import { JSONValue } from "ai";
|
||||
import ChatInput from "./chat-input";
|
||||
import ChatMessages from "./chat-messages";
|
||||
|
||||
@@ -17,10 +17,15 @@ export type ImageData = {
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type CsvData = {
|
||||
export type CsvFile = {
|
||||
content: string;
|
||||
filename: string;
|
||||
filesize: number;
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type CsvData = {
|
||||
csvFiles: CsvFile[];
|
||||
};
|
||||
|
||||
export type SourceNode = {
|
||||
@@ -72,32 +77,3 @@ export function getAnnotationData<T extends AnnotationData>(
|
||||
): T[] {
|
||||
return annotations.filter((a) => a.type === type).map((a) => a.data as T);
|
||||
}
|
||||
|
||||
// this function is used to get the additional resources for a message
|
||||
// it filters the annotations of a message and returns the unique resources
|
||||
// currently only CSV resources are supported
|
||||
export const getInputResources = (
|
||||
messages: Message[],
|
||||
): {
|
||||
csv: Array<CsvData>;
|
||||
} => {
|
||||
const csvResources: CsvData[] = [];
|
||||
messages.forEach((message) => {
|
||||
if (message.annotations) {
|
||||
const csvData = getAnnotationData<CsvData>(
|
||||
message.annotations as MessageAnnotation[],
|
||||
MessageAnnotationType.CSV,
|
||||
);
|
||||
csvData.forEach((data) => {
|
||||
if (
|
||||
csvResources.findIndex((r) => r.filename === data.filename) === -1
|
||||
) {
|
||||
csvResources.push(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return {
|
||||
csv: csvResources,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
"use client";
|
||||
|
||||
import { Message } from "ai";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
CsvData,
|
||||
CsvFile,
|
||||
MessageAnnotation,
|
||||
MessageAnnotationType,
|
||||
getAnnotationData,
|
||||
} from ".";
|
||||
|
||||
interface FrontendCSVData extends CsvFile {
|
||||
type: "available" | "new_upload";
|
||||
}
|
||||
|
||||
export function useCsv(messages: Message[]) {
|
||||
const [availableFiles, setAvailableFiles] = useState<FrontendCSVData[]>([]);
|
||||
const [uploadedFiles, setUploadedFiles] = useState<FrontendCSVData[]>([]);
|
||||
|
||||
const files = useMemo(() => {
|
||||
return [...availableFiles, ...uploadedFiles];
|
||||
}, [availableFiles, uploadedFiles]);
|
||||
|
||||
useEffect(() => {
|
||||
const items = getAvailableCsvFiles(messages);
|
||||
setAvailableFiles(items.map((data) => ({ ...data, type: "available" })));
|
||||
}, [messages]);
|
||||
|
||||
const csvEqual = (a: CsvFile, b: CsvFile) => {
|
||||
if (a.id === b.id) return true;
|
||||
if (a.filename === b.filename && a.filesize === b.filesize) return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
// Get available csv files from annotations chat history
|
||||
// returns the unique csv files by id
|
||||
const getAvailableCsvFiles = (messages: Message[]): Array<CsvFile> => {
|
||||
const docHash: Record<string, CsvFile> = {};
|
||||
messages.forEach((message) => {
|
||||
if (message.annotations) {
|
||||
const csvData = getAnnotationData<CsvData>(
|
||||
message.annotations as MessageAnnotation[],
|
||||
MessageAnnotationType.CSV,
|
||||
);
|
||||
csvData.forEach((data) => {
|
||||
data.csvFiles.forEach((file) => {
|
||||
if (!docHash[file.id]) {
|
||||
docHash[file.id] = file;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
return Object.values(docHash);
|
||||
};
|
||||
|
||||
const uploadNew = (file: CsvFile) => {
|
||||
const existedCsv = files.find((f) => csvEqual(f, file));
|
||||
if (!existedCsv) {
|
||||
setUploadedFiles((prev) => [...prev, { ...file, type: "new_upload" }]);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const removeFile = (file: FrontendCSVData) => {
|
||||
if (file.type === "new_upload") {
|
||||
setUploadedFiles((prev) => prev.filter((f) => f.id !== file.id));
|
||||
} else {
|
||||
setAvailableFiles((prev) => prev.filter((f) => f.id !== file.id));
|
||||
}
|
||||
};
|
||||
|
||||
const resetUploadedFiles = () => {
|
||||
setUploadedFiles([]);
|
||||
};
|
||||
|
||||
return {
|
||||
files,
|
||||
uploadNew,
|
||||
removeFile,
|
||||
resetUploadedFiles,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import Image from "next/image";
|
||||
import { CsvFile } from "..";
|
||||
import SheetIcon from "../../../ui/icons/sheet.svg";
|
||||
import { Button } from "../../button";
|
||||
import {
|
||||
Drawer,
|
||||
DrawerClose,
|
||||
DrawerContent,
|
||||
DrawerDescription,
|
||||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
DrawerTrigger,
|
||||
} from "../../drawer";
|
||||
|
||||
export interface CsvDialogProps {
|
||||
csv: CsvFile;
|
||||
}
|
||||
|
||||
export default function CsvDialog(props: CsvDialogProps) {
|
||||
const { filename, filesize, content } = props.csv;
|
||||
const fileSizeInKB = Math.round((filesize / 1024) * 10) / 10;
|
||||
return (
|
||||
<Drawer direction="left">
|
||||
<DrawerTrigger asChild>
|
||||
<div
|
||||
className="border-2 border-green-700 py-1.5 px-3 rounded-lg flex gap-2 items-center cursor-pointer text-sm hover:bg-green-700 hover:text-white transition-colors duration-200 ease-in-out"
|
||||
key={filename}
|
||||
>
|
||||
<div className="h-4 w-4 shrink-0 rounded-md">
|
||||
<Image
|
||||
className="h-full w-auto"
|
||||
priority
|
||||
src={SheetIcon}
|
||||
alt="SheetIcon"
|
||||
/>
|
||||
</div>
|
||||
<span>
|
||||
{filename} - {fileSizeInKB} KB
|
||||
</span>
|
||||
</div>
|
||||
</DrawerTrigger>
|
||||
<DrawerContent className="w-3/5 mt-24 h-full max-h-[96%] ">
|
||||
<DrawerHeader className="flex justify-between">
|
||||
<div className="space-y-2">
|
||||
<DrawerTitle>Csv Raw Content</DrawerTitle>
|
||||
<DrawerDescription>
|
||||
{filename} ({fileSizeInKB} KB)
|
||||
</DrawerDescription>
|
||||
</div>
|
||||
<DrawerClose asChild>
|
||||
<Button variant="outline">Close</Button>
|
||||
</DrawerClose>
|
||||
</DrawerHeader>
|
||||
<div className="m-4 max-h-[80%] overflow-auto">
|
||||
<pre className="bg-secondary rounded-md p-4 block text-sm">
|
||||
{content}
|
||||
</pre>
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
@@ -7,14 +7,16 @@ export default function UploadCsvPreview({
|
||||
filename,
|
||||
filesize,
|
||||
onRemove,
|
||||
isNew,
|
||||
}: {
|
||||
filename: string;
|
||||
filesize: number;
|
||||
onRemove: () => void;
|
||||
isNew?: boolean;
|
||||
}) {
|
||||
const fileSizeInKB = Math.round((filesize / 1024) * 10) / 10;
|
||||
return (
|
||||
<div className="p-2 w-80 bg-secondary rounded-lg text-sm relative">
|
||||
<div className="p-2 w-60 max-w-60 bg-secondary rounded-lg text-sm relative">
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<div className="relative h-10 w-10 shrink-0 overflow-hidden rounded-md">
|
||||
<Image
|
||||
@@ -28,7 +30,14 @@ export default function UploadCsvPreview({
|
||||
<div className="truncate font-semibold">
|
||||
{filename} ({fileSizeInKB} KB)
|
||||
</div>
|
||||
<div className="truncate text-token-text-tertiary">Spreadsheet</div>
|
||||
<div className="truncate text-token-text-tertiary flex items-center gap-2">
|
||||
<span>Spreadsheet</span>
|
||||
{isNew && (
|
||||
<span className="px-2 py-0.5 bg-red-400 text-white text-xs rounded-2xl">
|
||||
new
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -35,7 +35,8 @@
|
||||
"tailwind-merge": "^2.1.0",
|
||||
"vaul": "^0.9.1",
|
||||
"@llamaindex/pdf-viewer": "^1.1.1",
|
||||
"@e2b/code-interpreter": "^0.0.5"
|
||||
"@e2b/code-interpreter": "^0.0.5",
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.3",
|
||||
@@ -52,6 +53,7 @@
|
||||
"prettier-plugin-organize-imports": "^3.2.4",
|
||||
"tailwindcss": "^3.3.6",
|
||||
"tsx": "^4.7.2",
|
||||
"typescript": "^5.3.2"
|
||||
"typescript": "^5.3.2",
|
||||
"@types/uuid": "^9.0.8"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user