mirror of
https://github.com/run-llama/create-llama.git
synced 2026-07-18 13:05:55 -04:00
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d22310d11c | |||
| 0a195f82bf | |||
| 948b1b61a9 | |||
| d07ffe95e5 | |||
| 4b66d29716 | |||
| afb54058dc | |||
| cca49c5462 | |||
| 28696d5415 | |||
| ee8cb008d8 | |||
| 689ad9b5b1 | |||
| 3ca2f65cbe | |||
| 7ff484398f | |||
| 9c8192dc63 | |||
| fa991f4b52 | |||
| 4ba4c64cf2 | |||
| 04a4dc9454 | |||
| 2d19560006 | |||
| f707c6fd18 | |||
| f4520276d7 | |||
| 035e96eefd | |||
| d458783c8a | |||
| d73ee43ac6 | |||
| 312b6d3b20 | |||
| 97af04e8f2 | |||
| 5a06cf9685 | |||
| 0d140f4105 | |||
| 8db540598d |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"create-llama": patch
|
||||
---
|
||||
|
||||
Support upload document files: pdf, docx, txt
|
||||
@@ -1,15 +1,20 @@
|
||||
import { BaseToolWithCall, OpenAIAgent, QueryEngineTool } from "llamaindex";
|
||||
import {
|
||||
BaseToolWithCall,
|
||||
OpenAIAgent,
|
||||
QueryEngineTool,
|
||||
TextNode,
|
||||
} from "llamaindex";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { getDataSource } from "./index";
|
||||
import { createTools } from "./tools";
|
||||
|
||||
export async function createChatEngine() {
|
||||
export async function createChatEngine(nodes?: TextNode[]) {
|
||||
const tools: BaseToolWithCall[] = [];
|
||||
|
||||
// Add a query engine tool if we have a data source
|
||||
// Delete this code if you don't have a data source
|
||||
const index = await getDataSource();
|
||||
const index = await getDataSource(nodes);
|
||||
if (index) {
|
||||
tools.push(
|
||||
new QueryEngineTool({
|
||||
|
||||
@@ -3,22 +3,20 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
export interface ChatConfig {
|
||||
chatAPI?: string;
|
||||
backend?: string;
|
||||
starterQuestions?: string[];
|
||||
}
|
||||
|
||||
export function useClientConfig() {
|
||||
const API_ROUTE = "/api/chat/config";
|
||||
export function useClientConfig(): ChatConfig {
|
||||
const chatAPI = process.env.NEXT_PUBLIC_CHAT_API;
|
||||
const [config, setConfig] = useState<ChatConfig>({
|
||||
chatAPI,
|
||||
});
|
||||
const [config, setConfig] = useState<ChatConfig>();
|
||||
|
||||
const configAPI = useMemo(() => {
|
||||
const backendOrigin = chatAPI ? new URL(chatAPI).origin : "";
|
||||
return `${backendOrigin}${API_ROUTE}`;
|
||||
const backendOrigin = useMemo(() => {
|
||||
return chatAPI ? new URL(chatAPI).origin : "";
|
||||
}, [chatAPI]);
|
||||
|
||||
const configAPI = `${backendOrigin}/api/chat/config`;
|
||||
|
||||
useEffect(() => {
|
||||
fetch(configAPI)
|
||||
.then((response) => response.json())
|
||||
@@ -26,5 +24,8 @@ export function useClientConfig() {
|
||||
.catch((error) => console.error("Error fetching config", error));
|
||||
}, [chatAPI, configAPI]);
|
||||
|
||||
return config;
|
||||
return {
|
||||
backend: backendOrigin,
|
||||
starterQuestions: config?.starterQuestions,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,8 +1,21 @@
|
||||
import { SimpleDocumentStore, VectorStoreIndex } from "llamaindex";
|
||||
import { SimpleDocumentStore, TextNode, VectorStoreIndex } from "llamaindex";
|
||||
import { storageContextFromDefaults } from "llamaindex/storage/StorageContext";
|
||||
import { STORAGE_CACHE_DIR } from "./shared";
|
||||
|
||||
export async function getDataSource() {
|
||||
export async function getDataSource(nodes?: TextNode[]) {
|
||||
if (nodes && nodes.length > 0) {
|
||||
// the user send some local nodes, we create an index using them and
|
||||
// prefer that index over the server side index.
|
||||
// TODO: merge indexes, currently we prefer nodes that are send by the user
|
||||
return await VectorStoreIndex.init({
|
||||
nodes,
|
||||
});
|
||||
} else {
|
||||
return await getServerDataSource();
|
||||
}
|
||||
}
|
||||
|
||||
async function getServerDataSource() {
|
||||
const storageContext = await storageContextFromDefaults({
|
||||
persistDir: `${STORAGE_CACHE_DIR}`,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Request, Response } from "express";
|
||||
import { readAndSplitDocument } from "./embeddings";
|
||||
|
||||
export const chatEmbed = async (req: Request, res: Response) => {
|
||||
const { base64 }: { base64: string } = req.body;
|
||||
if (!base64) {
|
||||
return res.status(400).json({
|
||||
error: "base64 is required in the request body",
|
||||
});
|
||||
}
|
||||
return res.status(200).json(await readAndSplitDocument(base64));
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
import {
|
||||
Document,
|
||||
IngestionPipeline,
|
||||
MetadataMode,
|
||||
Settings,
|
||||
SimpleNodeParser,
|
||||
TextNode,
|
||||
} from "llamaindex";
|
||||
import { DocxReader } from "llamaindex/readers/DocxReader";
|
||||
import { PDFReader } from "llamaindex/readers/PDFReader";
|
||||
import { TextFileReader } from "llamaindex/readers/TextFileReader";
|
||||
|
||||
export async function readAndSplitDocument(
|
||||
raw: string,
|
||||
): Promise<Pick<TextNode, "text" | "embedding">[]> {
|
||||
const [header, content] = raw.split(",");
|
||||
const mimeType = header.replace("data:", "").replace(";base64", "");
|
||||
const fileBuffer = Buffer.from(content, "base64");
|
||||
const documents = await loadDocuments(fileBuffer, mimeType);
|
||||
return await runPipeline(documents);
|
||||
}
|
||||
|
||||
async function runPipeline(
|
||||
documents: Document[],
|
||||
): Promise<Pick<TextNode, "text" | "embedding">[]> {
|
||||
const pipeline = new IngestionPipeline({
|
||||
transformations: [
|
||||
new SimpleNodeParser({
|
||||
chunkSize: Settings.chunkSize,
|
||||
chunkOverlap: Settings.chunkOverlap,
|
||||
}),
|
||||
Settings.embedModel,
|
||||
],
|
||||
});
|
||||
const nodes = await pipeline.run({ documents });
|
||||
// remove metadata from text nodes to reduce data send over the wire
|
||||
return nodes.map((node) => ({
|
||||
text: node.getContent(MetadataMode.NONE),
|
||||
embedding: node.embedding,
|
||||
}));
|
||||
}
|
||||
|
||||
async function loadDocuments(fileBuffer: Buffer, mimeType: string) {
|
||||
console.log(`Processing uploaded document of type: ${mimeType}`);
|
||||
switch (mimeType) {
|
||||
case "application/pdf":
|
||||
const pdfReader = new PDFReader();
|
||||
return await pdfReader.loadDataAsContent(new Uint8Array(fileBuffer));
|
||||
case "text/plain":
|
||||
const textReader = new TextFileReader();
|
||||
return await textReader.loadDataAsContent(fileBuffer);
|
||||
case "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
|
||||
const docxReader = new DocxReader();
|
||||
return await docxReader.loadDataAsContent(fileBuffer);
|
||||
default:
|
||||
throw new Error(`Unsupported document type: ${mimeType}`);
|
||||
}
|
||||
}
|
||||
@@ -11,8 +11,7 @@ import {
|
||||
MessageContent,
|
||||
MessageContentDetail,
|
||||
} from "llamaindex";
|
||||
|
||||
import { CsvFile } from "./stream-helper";
|
||||
import { DocumentFile } from "./stream-helper";
|
||||
|
||||
export const convertMessageContent = (
|
||||
content: string,
|
||||
@@ -60,17 +59,15 @@ const convertAnnotations = (
|
||||
},
|
||||
});
|
||||
}
|
||||
// convert CSV files to text
|
||||
if (type === "csv" && "csvFiles" in data && Array.isArray(data.csvFiles)) {
|
||||
const rawContents = data.csvFiles.map((csv) => {
|
||||
return "```csv\n" + (csv as CsvFile).content + "\n```";
|
||||
});
|
||||
const csvContent =
|
||||
"Use data from following CSV raw contents:\n" +
|
||||
rawContents.join("\n\n");
|
||||
// convert files to text
|
||||
if (
|
||||
type === "document_file" &&
|
||||
"files" in data &&
|
||||
Array.isArray(data.files)
|
||||
) {
|
||||
content.push({
|
||||
type: "text",
|
||||
text: csvContent,
|
||||
text: getDocFileContext(data.files as DocumentFile[]),
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -78,6 +75,19 @@ const convertAnnotations = (
|
||||
return content;
|
||||
};
|
||||
|
||||
// TODO: we must get here the relevant context based on the embeddings by comparing
|
||||
// it with the user's query
|
||||
function getDocFileContext(files: DocumentFile[]): string {
|
||||
const rawContents = files.map((file) => {
|
||||
const { content } = file;
|
||||
const context = Array.isArray(content)
|
||||
? content.map((node) => node.text).join("\n")
|
||||
: content;
|
||||
return "```" + `${context}\n` + "```";
|
||||
});
|
||||
return `Use the following context:\n` + rawContents.join("\n\n");
|
||||
}
|
||||
|
||||
function createParser(res: AsyncIterable<EngineResponse>, data: StreamData) {
|
||||
const it = res[Symbol.asyncIterator]();
|
||||
const trimStartOfStream = trimStartOfStreamHelper();
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
CallbackManager,
|
||||
Metadata,
|
||||
NodeWithScore,
|
||||
TextNode,
|
||||
ToolCall,
|
||||
ToolOutput,
|
||||
} from "llamaindex";
|
||||
@@ -113,9 +114,12 @@ export function createCallbackManager(stream: StreamData) {
|
||||
return callbackManager;
|
||||
}
|
||||
|
||||
export type CsvFile = {
|
||||
content: string;
|
||||
export type DocumentFileType = "csv" | "pdf" | "txt" | "docx";
|
||||
|
||||
export type DocumentFile = {
|
||||
id: string;
|
||||
filename: string;
|
||||
filesize: number;
|
||||
id: string;
|
||||
filetype: DocumentFileType;
|
||||
content: string | TextNode[];
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import express, { Router } from "express";
|
||||
import { chatConfig } from "../controllers/chat-config.controller";
|
||||
import { chatEmbed } from "../controllers/chat-embed.controller";
|
||||
import { chatRequest } from "../controllers/chat-request.controller";
|
||||
import { chat } from "../controllers/chat.controller";
|
||||
import { initSettings } from "../controllers/engine/settings";
|
||||
@@ -10,5 +11,6 @@ initSettings();
|
||||
llmRouter.route("/").post(chat);
|
||||
llmRouter.route("/request").post(chatRequest);
|
||||
llmRouter.route("/config").get(chatConfig);
|
||||
llmRouter.route("/embed").post(chatEmbed);
|
||||
|
||||
export default llmRouter;
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import {
|
||||
Document,
|
||||
IngestionPipeline,
|
||||
MetadataMode,
|
||||
Settings,
|
||||
SimpleNodeParser,
|
||||
TextNode,
|
||||
} from "llamaindex";
|
||||
import { DocxReader } from "llamaindex/readers/DocxReader";
|
||||
import { PDFReader } from "llamaindex/readers/PDFReader";
|
||||
import { TextFileReader } from "llamaindex/readers/TextFileReader";
|
||||
|
||||
type SimpleTextNode = Pick<TextNode, "text" | "embedding" | "metadata">;
|
||||
|
||||
export async function readAndSplitDocument(
|
||||
raw: string,
|
||||
): Promise<SimpleTextNode[]> {
|
||||
const [header, content] = raw.split(",");
|
||||
const mimeType = header.replace("data:", "").replace(";base64", "");
|
||||
const fileBuffer = Buffer.from(content, "base64");
|
||||
const documents = await loadDocuments(fileBuffer, mimeType);
|
||||
return await runPipeline(documents);
|
||||
}
|
||||
|
||||
async function runPipeline(documents: Document[]): Promise<SimpleTextNode[]> {
|
||||
const pipeline = new IngestionPipeline({
|
||||
transformations: [
|
||||
new SimpleNodeParser({
|
||||
chunkSize: Settings.chunkSize,
|
||||
chunkOverlap: Settings.chunkOverlap,
|
||||
}),
|
||||
Settings.embedModel,
|
||||
],
|
||||
});
|
||||
const nodes = await pipeline.run({ documents });
|
||||
// remove text nodes to reduce data send over the wire
|
||||
return nodes.map((node: TextNode) => ({
|
||||
text: node.getContent(MetadataMode.NONE),
|
||||
embedding: node.embedding,
|
||||
// TODO: to be able to view the source document, we need to store its URL in the metadata
|
||||
metadata: node.metadata,
|
||||
}));
|
||||
}
|
||||
|
||||
async function loadDocuments(fileBuffer: Buffer, mimeType: string) {
|
||||
console.log(`Processing uploaded document of type: ${mimeType}`);
|
||||
switch (mimeType) {
|
||||
case "application/pdf":
|
||||
const pdfReader = new PDFReader();
|
||||
return await pdfReader.loadDataAsContent(new Uint8Array(fileBuffer));
|
||||
case "text/plain":
|
||||
const textReader = new TextFileReader();
|
||||
return await textReader.loadDataAsContent(fileBuffer);
|
||||
case "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
|
||||
const docxReader = new DocxReader();
|
||||
return await docxReader.loadDataAsContent(fileBuffer);
|
||||
default:
|
||||
throw new Error(`Unsupported document type: ${mimeType}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { initSettings } from "../engine/settings";
|
||||
import { readAndSplitDocument } from "./embeddings";
|
||||
|
||||
initSettings();
|
||||
|
||||
export const runtime = "nodejs";
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { base64 }: { base64: string } = await request.json();
|
||||
if (!base64) {
|
||||
return NextResponse.json(
|
||||
{ error: "base64 is required in the request body" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
return NextResponse.json(await readAndSplitDocument(base64));
|
||||
} catch (error) {
|
||||
console.error("[Embed API]", error);
|
||||
return NextResponse.json(
|
||||
{ error: (error as Error).message },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
import {
|
||||
JSONValue,
|
||||
StreamData,
|
||||
createCallbacksTransformer,
|
||||
createStreamDataTransformer,
|
||||
trimStartOfStreamHelper,
|
||||
type AIStreamCallbacksAndOptions,
|
||||
} from "ai";
|
||||
import {
|
||||
EngineResponse,
|
||||
MessageContent,
|
||||
MessageContentDetail,
|
||||
} from "llamaindex";
|
||||
|
||||
import { CsvFile } from "./stream-helper";
|
||||
|
||||
export const convertMessageContent = (
|
||||
content: string,
|
||||
annotations?: JSONValue[],
|
||||
): MessageContent => {
|
||||
if (!annotations) return content;
|
||||
return [
|
||||
{
|
||||
type: "text",
|
||||
text: content,
|
||||
},
|
||||
...convertAnnotations(annotations),
|
||||
];
|
||||
};
|
||||
|
||||
const convertAnnotations = (
|
||||
annotations: JSONValue[],
|
||||
): MessageContentDetail[] => {
|
||||
const content: MessageContentDetail[] = [];
|
||||
annotations.forEach((annotation: JSONValue) => {
|
||||
// first skip invalid annotation
|
||||
if (
|
||||
!(
|
||||
annotation &&
|
||||
typeof annotation === "object" &&
|
||||
"type" in annotation &&
|
||||
"data" in annotation &&
|
||||
annotation.data &&
|
||||
typeof annotation.data === "object"
|
||||
)
|
||||
) {
|
||||
console.log(
|
||||
"Client sent invalid annotation. Missing data and type",
|
||||
annotation,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const { type, data } = annotation;
|
||||
// convert image
|
||||
if (type === "image" && "url" in data && typeof data.url === "string") {
|
||||
content.push({
|
||||
type: "image_url",
|
||||
image_url: {
|
||||
url: data.url,
|
||||
},
|
||||
});
|
||||
}
|
||||
// convert CSV files to text
|
||||
if (type === "csv" && "csvFiles" in data && Array.isArray(data.csvFiles)) {
|
||||
const rawContents = data.csvFiles.map((csv) => {
|
||||
return "```csv\n" + (csv as CsvFile).content + "\n```";
|
||||
});
|
||||
const csvContent =
|
||||
"Use data from following CSV raw contents:\n" +
|
||||
rawContents.join("\n\n");
|
||||
content.push({
|
||||
type: "text",
|
||||
text: csvContent,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return content;
|
||||
};
|
||||
|
||||
function createParser(res: AsyncIterable<EngineResponse>, data: StreamData) {
|
||||
const it = res[Symbol.asyncIterator]();
|
||||
const trimStartOfStream = trimStartOfStreamHelper();
|
||||
|
||||
return new ReadableStream<string>({
|
||||
async pull(controller): Promise<void> {
|
||||
const { value, done } = await it.next();
|
||||
if (done) {
|
||||
controller.close();
|
||||
data.close();
|
||||
return;
|
||||
}
|
||||
const text = trimStartOfStream(value.delta ?? "");
|
||||
if (text) {
|
||||
controller.enqueue(text);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function LlamaIndexStream(
|
||||
response: AsyncIterable<EngineResponse>,
|
||||
data: StreamData,
|
||||
opts?: {
|
||||
callbacks?: AIStreamCallbacksAndOptions;
|
||||
},
|
||||
): ReadableStream<Uint8Array> {
|
||||
return createParser(response, data)
|
||||
.pipeThrough(createCallbacksTransformer(opts?.callbacks))
|
||||
.pipeThrough(createStreamDataTransformer());
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import { JSONValue } from "ai";
|
||||
import { MessageContent, MessageContentDetail, TextNode } from "llamaindex";
|
||||
|
||||
export type DocumentFileType = "csv" | "pdf" | "txt" | "docx";
|
||||
|
||||
export type DocumentFile = {
|
||||
id: string;
|
||||
filename: string;
|
||||
filesize: number;
|
||||
filetype: DocumentFileType;
|
||||
content: string | TextNode[];
|
||||
};
|
||||
|
||||
type Annotation = {
|
||||
type: string;
|
||||
data: object;
|
||||
};
|
||||
|
||||
export function retrieveNodes(annotations?: JSONValue[]): TextNode[] {
|
||||
if (!annotations) return [];
|
||||
|
||||
const textNodes: TextNode[] = [];
|
||||
|
||||
annotations.forEach((annotation: JSONValue) => {
|
||||
const { type, data } = getValidAnnotation(annotation);
|
||||
if (
|
||||
type === "document_file" &&
|
||||
"files" in data &&
|
||||
Array.isArray(data.files)
|
||||
) {
|
||||
const files = data.files as DocumentFile[];
|
||||
files.forEach((file) => {
|
||||
if (Array.isArray(file.content)) {
|
||||
file.content.forEach((node) => {
|
||||
if (node.hasOwnProperty("text")) {
|
||||
textNodes.push(
|
||||
new TextNode({
|
||||
text: node.text,
|
||||
embedding: node.embedding,
|
||||
metadata: node.metadata,
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return textNodes;
|
||||
}
|
||||
|
||||
export function convertMessageContent(
|
||||
content: string,
|
||||
annotations?: JSONValue[],
|
||||
): MessageContent {
|
||||
if (!annotations) return content;
|
||||
return [
|
||||
{
|
||||
type: "text",
|
||||
text: content,
|
||||
},
|
||||
...convertAnnotations(annotations),
|
||||
];
|
||||
}
|
||||
|
||||
function convertAnnotations(annotations: JSONValue[]): MessageContentDetail[] {
|
||||
const content: MessageContentDetail[] = [];
|
||||
annotations.forEach((annotation: JSONValue) => {
|
||||
const { type, data } = getValidAnnotation(annotation);
|
||||
// convert image
|
||||
if (type === "image" && "url" in data && typeof data.url === "string") {
|
||||
content.push({
|
||||
type: "image_url",
|
||||
image_url: {
|
||||
url: data.url,
|
||||
},
|
||||
});
|
||||
}
|
||||
// convert the content of files to a text message
|
||||
if (
|
||||
type === "document_file" &&
|
||||
"files" in data &&
|
||||
Array.isArray(data.files)
|
||||
) {
|
||||
// get all CSV files and convert their whole content to one text message
|
||||
// currently CSV files are the only files where we send the whole content - we don't use an index
|
||||
const csvFiles = data.files.filter(
|
||||
(file: DocumentFile) => file.filetype === "csv",
|
||||
);
|
||||
if (csvFiles && csvFiles.length > 0) {
|
||||
const csvContents = csvFiles.map(
|
||||
(file: DocumentFile) => "```csv\n" + file.content + "\n```",
|
||||
);
|
||||
const text =
|
||||
"Use the following CSV content:\n" + csvContents.join("\n\n");
|
||||
content.push({
|
||||
type: "text",
|
||||
text,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
function getValidAnnotation(annotation: JSONValue): Annotation {
|
||||
if (
|
||||
!(
|
||||
annotation &&
|
||||
typeof annotation === "object" &&
|
||||
"type" in annotation &&
|
||||
typeof annotation.type === "string" &&
|
||||
"data" in annotation &&
|
||||
annotation.data &&
|
||||
typeof annotation.data === "object"
|
||||
)
|
||||
) {
|
||||
throw new Error("Client sent invalid annotation. Missing data and type");
|
||||
}
|
||||
return { type: annotation.type, data: annotation.data };
|
||||
}
|
||||
+15
-22
@@ -7,22 +7,6 @@ import {
|
||||
ToolOutput,
|
||||
} from "llamaindex";
|
||||
|
||||
function getNodeUrl(metadata: Metadata) {
|
||||
const url = metadata["URL"];
|
||||
if (url) return url;
|
||||
const fileName = metadata["file_name"];
|
||||
if (!process.env.FILESERVER_URL_PREFIX) {
|
||||
console.warn(
|
||||
"FILESERVER_URL_PREFIX is not set. File URLs will not be generated.",
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
if (fileName) {
|
||||
return `${process.env.FILESERVER_URL_PREFIX}/data/${fileName}`;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function appendSourceData(
|
||||
data: StreamData,
|
||||
sourceNodes?: NodeWithScore<Metadata>[],
|
||||
@@ -113,9 +97,18 @@ export function createCallbackManager(stream: StreamData) {
|
||||
return callbackManager;
|
||||
}
|
||||
|
||||
export type CsvFile = {
|
||||
content: string;
|
||||
filename: string;
|
||||
filesize: number;
|
||||
id: string;
|
||||
};
|
||||
function getNodeUrl(metadata: Metadata) {
|
||||
const url = metadata["URL"];
|
||||
if (url) return url;
|
||||
const fileName = metadata["file_name"];
|
||||
if (!process.env.FILESERVER_URL_PREFIX) {
|
||||
console.warn(
|
||||
"FILESERVER_URL_PREFIX is not set. File URLs will not be generated.",
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
if (fileName) {
|
||||
return `${process.env.FILESERVER_URL_PREFIX}/data/${fileName}`;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import {
|
||||
StreamData,
|
||||
createCallbacksTransformer,
|
||||
createStreamDataTransformer,
|
||||
trimStartOfStreamHelper,
|
||||
type AIStreamCallbacksAndOptions,
|
||||
} from "ai";
|
||||
import { EngineResponse } from "llamaindex";
|
||||
|
||||
export function LlamaIndexStream(
|
||||
response: AsyncIterable<EngineResponse>,
|
||||
data: StreamData,
|
||||
opts?: {
|
||||
callbacks?: AIStreamCallbacksAndOptions;
|
||||
},
|
||||
): ReadableStream<Uint8Array> {
|
||||
return createParser(response, data)
|
||||
.pipeThrough(createCallbacksTransformer(opts?.callbacks))
|
||||
.pipeThrough(createStreamDataTransformer());
|
||||
}
|
||||
|
||||
function createParser(res: AsyncIterable<EngineResponse>, data: StreamData) {
|
||||
const it = res[Symbol.asyncIterator]();
|
||||
const trimStartOfStream = trimStartOfStreamHelper();
|
||||
|
||||
return new ReadableStream<string>({
|
||||
async pull(controller): Promise<void> {
|
||||
const { value, done } = await it.next();
|
||||
if (done) {
|
||||
controller.close();
|
||||
data.close();
|
||||
return;
|
||||
}
|
||||
const text = trimStartOfStream(value.delta ?? "");
|
||||
if (text) {
|
||||
controller.enqueue(text);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -4,8 +4,12 @@ import { ChatMessage, Settings } from "llamaindex";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { createChatEngine } from "./engine/chat";
|
||||
import { initSettings } from "./engine/settings";
|
||||
import { LlamaIndexStream, convertMessageContent } from "./llamaindex-stream";
|
||||
import { createCallbackManager, createStreamTimeout } from "./stream-helper";
|
||||
import { convertMessageContent, retrieveNodes } from "./llamaindex/annotations";
|
||||
import {
|
||||
createCallbackManager,
|
||||
createStreamTimeout,
|
||||
} from "./llamaindex/events";
|
||||
import { LlamaIndexStream } from "./llamaindex/stream";
|
||||
|
||||
initObservability();
|
||||
initSettings();
|
||||
@@ -32,8 +36,6 @@ export async function POST(request: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
const chatEngine = await createChatEngine();
|
||||
|
||||
let annotations = userMessage.annotations;
|
||||
if (!annotations) {
|
||||
// the user didn't send any new annotations with the last message
|
||||
@@ -47,6 +49,10 @@ export async function POST(request: NextRequest) {
|
||||
)?.annotations;
|
||||
}
|
||||
|
||||
// retrieve nodes from annotations (if any) and create chat engine with index
|
||||
const nodes = retrieveNodes(annotations);
|
||||
const chatEngine = await createChatEngine(nodes);
|
||||
|
||||
// Convert message content from Vercel/AI format to LlamaIndex/OpenAI format
|
||||
const userMessageContent = convertMessageContent(
|
||||
userMessage.content,
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ChatInput, ChatMessages } from "./ui/chat";
|
||||
import { useClientConfig } from "./ui/chat/hooks/use-config";
|
||||
|
||||
export default function ChatSection() {
|
||||
const { chatAPI } = useClientConfig();
|
||||
const { backend } = useClientConfig();
|
||||
const {
|
||||
messages,
|
||||
input,
|
||||
@@ -17,7 +17,7 @@ export default function ChatSection() {
|
||||
append,
|
||||
setInput,
|
||||
} = useChat({
|
||||
api: chatAPI,
|
||||
api: `${backend}/api/chat`,
|
||||
headers: {
|
||||
"Content-Type": "application/json", // using JSON because of vercel/ai 2.2.26
|
||||
},
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { JSONValue } from "ai";
|
||||
import { useState } from "react";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { MessageAnnotation, MessageAnnotationType } from ".";
|
||||
import { Button } from "../button";
|
||||
import { DocumentPreview } from "../document-preview";
|
||||
import FileUploader from "../file-uploader";
|
||||
import { Input } from "../input";
|
||||
import UploadCsvPreview from "../upload-csv-preview";
|
||||
import UploadImagePreview from "../upload-image-preview";
|
||||
import { ChatHandler } from "./chat.interface";
|
||||
import { useCsv } from "./hooks/use-csv";
|
||||
import { useFile } from "./hooks/use-file";
|
||||
|
||||
const ALLOWED_EXTENSIONS = ["png", "jpg", "jpeg", "csv", "pdf", "txt", "docx"];
|
||||
|
||||
export default function ChatInput(
|
||||
props: Pick<
|
||||
@@ -24,33 +23,15 @@ export default function ChatInput(
|
||||
| "append"
|
||||
>,
|
||||
) {
|
||||
const [imageUrl, setImageUrl] = useState<string | null>(null);
|
||||
const { files: csvFiles, upload, remove, reset } = useCsv();
|
||||
|
||||
const getAnnotations = () => {
|
||||
if (!imageUrl && csvFiles.length === 0) return undefined;
|
||||
const annotations: MessageAnnotation[] = [];
|
||||
if (imageUrl) {
|
||||
annotations.push({
|
||||
type: MessageAnnotationType.IMAGE,
|
||||
data: { url: imageUrl },
|
||||
});
|
||||
}
|
||||
if (csvFiles.length > 0) {
|
||||
annotations.push({
|
||||
type: MessageAnnotationType.CSV,
|
||||
data: {
|
||||
csvFiles: csvFiles.map((file) => ({
|
||||
id: file.id,
|
||||
content: file.content,
|
||||
filename: file.filename,
|
||||
filesize: file.filesize,
|
||||
})),
|
||||
},
|
||||
});
|
||||
}
|
||||
return annotations as JSONValue[];
|
||||
};
|
||||
const {
|
||||
imageUrl,
|
||||
setImageUrl,
|
||||
uploadFile,
|
||||
files,
|
||||
removeDoc,
|
||||
reset,
|
||||
getAnnotations,
|
||||
} = useFile();
|
||||
|
||||
// default submit function does not handle including annotations in the message
|
||||
// so we need to use append function to submit new message with annotations
|
||||
@@ -70,61 +51,20 @@ export default function ChatInput(
|
||||
|
||||
const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
const annotations = getAnnotations();
|
||||
if (annotations) {
|
||||
if (annotations.length) {
|
||||
handleSubmitWithAnnotations(e, annotations);
|
||||
imageUrl && setImageUrl(null);
|
||||
csvFiles.length && reset();
|
||||
return;
|
||||
return reset();
|
||||
}
|
||||
props.handleSubmit(e);
|
||||
};
|
||||
|
||||
const onRemovePreviewImage = () => setImageUrl(null);
|
||||
|
||||
const readContent = async (file: File): Promise<string> => {
|
||||
const content = await new Promise<string>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
if (file.type.startsWith("image/")) {
|
||||
reader.readAsDataURL(file);
|
||||
} else {
|
||||
reader.readAsText(file);
|
||||
}
|
||||
reader.onload = () => resolve(reader.result as string);
|
||||
reader.onerror = (error) => reject(error);
|
||||
});
|
||||
return content;
|
||||
};
|
||||
|
||||
const handleUploadImageFile = async (file: File) => {
|
||||
const base64 = await readContent(file);
|
||||
setImageUrl(base64);
|
||||
};
|
||||
|
||||
const handleUploadCsvFile = async (file: File) => {
|
||||
const content = await readContent(file);
|
||||
const isSuccess = upload({
|
||||
id: uuidv4(),
|
||||
content,
|
||||
filename: file.name,
|
||||
filesize: file.size,
|
||||
});
|
||||
if (!isSuccess) {
|
||||
alert("File already exists in the list.");
|
||||
}
|
||||
};
|
||||
|
||||
const handleUploadFile = async (file: File) => {
|
||||
if (imageUrl || files.length > 0) {
|
||||
alert("You can only upload one file at a time.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (file.type.startsWith("image/")) {
|
||||
return await handleUploadImageFile(file);
|
||||
}
|
||||
if (file.type === "text/csv") {
|
||||
if (csvFiles.length > 0) {
|
||||
alert("You can only upload one csv file at a time.");
|
||||
return;
|
||||
}
|
||||
return await handleUploadCsvFile(file);
|
||||
}
|
||||
await uploadFile(file);
|
||||
props.onFileUpload?.(file);
|
||||
} catch (error: any) {
|
||||
props.onFileError?.(error.message);
|
||||
@@ -137,19 +77,17 @@ export default function ChatInput(
|
||||
className="rounded-xl bg-white p-4 shadow-xl space-y-4 shrink-0"
|
||||
>
|
||||
{imageUrl && (
|
||||
<UploadImagePreview url={imageUrl} onRemove={onRemovePreviewImage} />
|
||||
<UploadImagePreview url={imageUrl} onRemove={() => setImageUrl(null)} />
|
||||
)}
|
||||
{csvFiles.length > 0 && (
|
||||
{files.length > 0 && (
|
||||
<div className="flex gap-4 w-full overflow-auto py-2">
|
||||
{csvFiles.map((csv) => {
|
||||
return (
|
||||
<UploadCsvPreview
|
||||
key={csv.id}
|
||||
csv={csv}
|
||||
onRemove={() => remove(csv)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{files.map((file) => (
|
||||
<DocumentPreview
|
||||
key={file.id}
|
||||
file={file}
|
||||
onRemove={() => removeDoc(file)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex w-full items-start justify-between gap-4 ">
|
||||
@@ -164,6 +102,10 @@ export default function ChatInput(
|
||||
<FileUploader
|
||||
onFileUpload={handleUploadFile}
|
||||
onFileError={props.onFileError}
|
||||
config={{
|
||||
allowedExtensions: ALLOWED_EXTENSIONS,
|
||||
disabled: props.isLoading,
|
||||
}}
|
||||
/>
|
||||
<Button type="submit" disabled={props.isLoading || !props.input.trim()}>
|
||||
Send message
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { DocumentPreview } from "../../document-preview";
|
||||
import { DocumentFileData } from "../index";
|
||||
|
||||
export function ChatFiles({ data }: { data: DocumentFileData }) {
|
||||
if (!data.files.length) return null;
|
||||
return (
|
||||
<div className="flex gap-2 items-center">
|
||||
{data.files.map((file) => (
|
||||
<DocumentPreview key={file.id} file={file} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import UploadCsvPreview from "../../upload-csv-preview";
|
||||
import { CsvData } from "../index";
|
||||
|
||||
export default function CsvContent({ data }: { data: CsvData }) {
|
||||
if (!data.csvFiles.length) return null;
|
||||
return (
|
||||
<div className="flex gap-2 items-center">
|
||||
{data.csvFiles.map((csv, index) => (
|
||||
<UploadCsvPreview key={index} csv={csv} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import { Fragment } from "react";
|
||||
import { Button } from "../../button";
|
||||
import { useCopyToClipboard } from "../hooks/use-copy-to-clipboard";
|
||||
import {
|
||||
CsvData,
|
||||
DocumentFileData,
|
||||
EventData,
|
||||
ImageData,
|
||||
MessageAnnotation,
|
||||
@@ -16,10 +16,10 @@ import {
|
||||
} from "../index";
|
||||
import ChatAvatar from "./chat-avatar";
|
||||
import { ChatEvents } from "./chat-events";
|
||||
import { ChatFiles } from "./chat-files";
|
||||
import { ChatImage } from "./chat-image";
|
||||
import { ChatSources } from "./chat-sources";
|
||||
import ChatTools from "./chat-tools";
|
||||
import CsvContent from "./csv-content";
|
||||
import Markdown from "./markdown";
|
||||
|
||||
type ContentDisplayConfig = {
|
||||
@@ -41,9 +41,9 @@ function ChatMessageContent({
|
||||
annotations,
|
||||
MessageAnnotationType.IMAGE,
|
||||
);
|
||||
const csvData = getAnnotationData<CsvData>(
|
||||
const contentFileData = getAnnotationData<DocumentFileData>(
|
||||
annotations,
|
||||
MessageAnnotationType.CSV,
|
||||
MessageAnnotationType.DOCUMENT_FILE,
|
||||
);
|
||||
const eventData = getAnnotationData<EventData>(
|
||||
annotations,
|
||||
@@ -72,7 +72,9 @@ function ChatMessageContent({
|
||||
},
|
||||
{
|
||||
order: 2,
|
||||
component: csvData[0] ? <CsvContent data={csvData[0]} /> : null,
|
||||
component: contentFileData[0] ? (
|
||||
<ChatFiles data={contentFileData[0]} />
|
||||
) : null,
|
||||
},
|
||||
{
|
||||
order: -1,
|
||||
|
||||
@@ -3,22 +3,20 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
export interface ChatConfig {
|
||||
chatAPI?: string;
|
||||
backend?: string;
|
||||
starterQuestions?: string[];
|
||||
}
|
||||
|
||||
export function useClientConfig() {
|
||||
const API_ROUTE = "/api/chat/config";
|
||||
export function useClientConfig(): ChatConfig {
|
||||
const chatAPI = process.env.NEXT_PUBLIC_CHAT_API;
|
||||
const [config, setConfig] = useState<ChatConfig>({
|
||||
chatAPI,
|
||||
});
|
||||
const [config, setConfig] = useState<ChatConfig>();
|
||||
|
||||
const configAPI = useMemo(() => {
|
||||
const backendOrigin = chatAPI ? new URL(chatAPI).origin : "";
|
||||
return `${backendOrigin}${API_ROUTE}`;
|
||||
const backendOrigin = useMemo(() => {
|
||||
return chatAPI ? new URL(chatAPI).origin : "";
|
||||
}, [chatAPI]);
|
||||
|
||||
const configAPI = `${backendOrigin}/api/chat/config`;
|
||||
|
||||
useEffect(() => {
|
||||
fetch(configAPI)
|
||||
.then((response) => response.json())
|
||||
@@ -26,5 +24,8 @@ export function useClientConfig() {
|
||||
.catch((error) => console.error("Error fetching config", error));
|
||||
}, [chatAPI, configAPI]);
|
||||
|
||||
return config;
|
||||
return {
|
||||
backend: backendOrigin,
|
||||
starterQuestions: config?.starterQuestions,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { CsvFile } from "../index";
|
||||
|
||||
export function useCsv() {
|
||||
const [files, setFiles] = useState<CsvFile[]>([]);
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
const upload = (file: CsvFile) => {
|
||||
const existedCsv = files.find((f) => csvEqual(f, file));
|
||||
if (!existedCsv) {
|
||||
setFiles((prev) => [...prev, file]);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const remove = (file: CsvFile) => {
|
||||
setFiles((prev) => prev.filter((f) => f.id !== file.id));
|
||||
};
|
||||
|
||||
const reset = () => {
|
||||
setFiles([]);
|
||||
};
|
||||
|
||||
return { files, upload, remove, reset };
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
"use client";
|
||||
|
||||
import { JSONValue } from "llamaindex";
|
||||
import { useState } from "react";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import {
|
||||
DocumentFile,
|
||||
DocumentFileType,
|
||||
MessageAnnotation,
|
||||
MessageAnnotationType,
|
||||
TextNode,
|
||||
} from "..";
|
||||
import { useClientConfig } from "./use-config";
|
||||
|
||||
const docMineTypeMap: Record<string, DocumentFileType> = {
|
||||
"text/csv": "csv",
|
||||
"application/pdf": "pdf",
|
||||
"text/plain": "txt",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document":
|
||||
"docx",
|
||||
};
|
||||
|
||||
export function useFile() {
|
||||
const { backend } = useClientConfig();
|
||||
const [imageUrl, setImageUrl] = useState<string | null>(null);
|
||||
const [files, setFiles] = useState<DocumentFile[]>([]);
|
||||
|
||||
const docEqual = (a: DocumentFile, b: DocumentFile) => {
|
||||
if (a.id === b.id) return true;
|
||||
if (a.filename === b.filename && a.filesize === b.filesize) return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
const addDoc = (file: DocumentFile) => {
|
||||
const existedFile = files.find((f) => docEqual(f, file));
|
||||
if (!existedFile) {
|
||||
setFiles((prev) => [...prev, file]);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const removeDoc = (file: DocumentFile) => {
|
||||
setFiles((prev) => prev.filter((f) => f.id !== file.id));
|
||||
};
|
||||
|
||||
const reset = () => {
|
||||
imageUrl && setImageUrl(null);
|
||||
files.length && setFiles([]);
|
||||
};
|
||||
|
||||
const getTextNodes = async (base64: string): Promise<TextNode[]> => {
|
||||
const embedAPI = `${backend}/api/chat/embed`;
|
||||
const response = await fetch(embedAPI, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
base64,
|
||||
}),
|
||||
});
|
||||
if (!response.ok) throw new Error("Failed to get text nodes from file.");
|
||||
return await response.json();
|
||||
};
|
||||
|
||||
const getAnnotations = () => {
|
||||
const annotations: MessageAnnotation[] = [];
|
||||
if (imageUrl) {
|
||||
annotations.push({
|
||||
type: MessageAnnotationType.IMAGE,
|
||||
data: { url: imageUrl },
|
||||
});
|
||||
}
|
||||
if (files.length > 0) {
|
||||
annotations.push({
|
||||
type: MessageAnnotationType.DOCUMENT_FILE,
|
||||
data: { files },
|
||||
});
|
||||
}
|
||||
return annotations as JSONValue[];
|
||||
};
|
||||
|
||||
const readContent = async (input: {
|
||||
file: File;
|
||||
asUrl?: boolean;
|
||||
}): Promise<string> => {
|
||||
const { file, asUrl } = input;
|
||||
const content = await new Promise<string>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
if (asUrl) {
|
||||
reader.readAsDataURL(file);
|
||||
} else {
|
||||
reader.readAsText(file);
|
||||
}
|
||||
reader.onload = () => resolve(reader.result as string);
|
||||
reader.onerror = (error) => reject(error);
|
||||
});
|
||||
return content;
|
||||
};
|
||||
|
||||
const uploadFile = async (file: File) => {
|
||||
if (file.type.startsWith("image/")) {
|
||||
const base64 = await readContent({ file, asUrl: true });
|
||||
return setImageUrl(base64);
|
||||
}
|
||||
|
||||
const filetype = docMineTypeMap[file.type];
|
||||
if (!filetype) throw new Error("Unsupported document type.");
|
||||
const newDoc: DocumentFile = {
|
||||
id: uuidv4(),
|
||||
filetype,
|
||||
filename: file.name,
|
||||
filesize: file.size,
|
||||
content: "",
|
||||
};
|
||||
switch (file.type) {
|
||||
case "text/csv": {
|
||||
const content = await readContent({ file });
|
||||
return addDoc({ ...newDoc, content });
|
||||
}
|
||||
default: {
|
||||
const base64 = await readContent({ file, asUrl: true });
|
||||
const nodes = await getTextNodes(base64);
|
||||
return addDoc({ ...newDoc, content: nodes });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
imageUrl,
|
||||
setImageUrl,
|
||||
files,
|
||||
removeDoc,
|
||||
reset,
|
||||
getAnnotations,
|
||||
uploadFile,
|
||||
};
|
||||
}
|
||||
@@ -6,8 +6,8 @@ export { type ChatHandler } from "./chat.interface";
|
||||
export { ChatInput, ChatMessages };
|
||||
|
||||
export enum MessageAnnotationType {
|
||||
CSV = "csv",
|
||||
IMAGE = "image",
|
||||
DOCUMENT_FILE = "document_file",
|
||||
SOURCES = "sources",
|
||||
EVENTS = "events",
|
||||
TOOLS = "tools",
|
||||
@@ -17,15 +17,24 @@ export type ImageData = {
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type CsvFile = {
|
||||
content: string;
|
||||
filename: string;
|
||||
filesize: number;
|
||||
id: string;
|
||||
// this is subset of LlamaIndex's TextNode
|
||||
export type TextNode = {
|
||||
text: string;
|
||||
embedding: number[];
|
||||
};
|
||||
|
||||
export type CsvData = {
|
||||
csvFiles: CsvFile[];
|
||||
export type DocumentFileType = "csv" | "pdf" | "txt" | "docx";
|
||||
|
||||
export type DocumentFile = {
|
||||
id: string;
|
||||
filename: string;
|
||||
filesize: number;
|
||||
filetype: DocumentFileType;
|
||||
content: string | TextNode[];
|
||||
};
|
||||
|
||||
export type DocumentFileData = {
|
||||
files: DocumentFile[];
|
||||
};
|
||||
|
||||
export type SourceNode = {
|
||||
@@ -61,7 +70,7 @@ export type ToolData = {
|
||||
|
||||
export type AnnotationData =
|
||||
| ImageData
|
||||
| CsvData
|
||||
| DocumentFileData
|
||||
| SourceData
|
||||
| EventData
|
||||
| ToolData;
|
||||
|
||||
+28
-15
@@ -1,8 +1,11 @@
|
||||
import { XCircleIcon } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import DocxIcon from "../ui/icons/docx.svg";
|
||||
import PdfIcon from "../ui/icons/pdf.svg";
|
||||
import SheetIcon from "../ui/icons/sheet.svg";
|
||||
import TxtIcon from "../ui/icons/txt.svg";
|
||||
import { Button } from "./button";
|
||||
import { CsvFile } from "./chat";
|
||||
import { DocumentFile, DocumentFileType } from "./chat";
|
||||
import {
|
||||
Drawer,
|
||||
DrawerClose,
|
||||
@@ -14,24 +17,27 @@ import {
|
||||
} from "./drawer";
|
||||
import { cn } from "./lib/utils";
|
||||
|
||||
export interface UploadCsvPreviewProps {
|
||||
csv: CsvFile;
|
||||
export interface DocumentPreviewProps {
|
||||
file: DocumentFile;
|
||||
onRemove?: () => void;
|
||||
}
|
||||
|
||||
export default function UploadCsvPreview(props: UploadCsvPreviewProps) {
|
||||
const { filename, filesize, content } = props.csv;
|
||||
export function DocumentPreview(props: DocumentPreviewProps) {
|
||||
const { filename, filesize, content, filetype } = props.file;
|
||||
const docContent = Array.isArray(content)
|
||||
? content.map((n) => n.text).join("\n")
|
||||
: content;
|
||||
return (
|
||||
<Drawer direction="left">
|
||||
<DrawerTrigger asChild>
|
||||
<div>
|
||||
<CSVSummaryCard {...props} />
|
||||
<PreviewCard {...props} />
|
||||
</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>
|
||||
<DrawerTitle>{filetype.toUpperCase()} Raw Content</DrawerTitle>
|
||||
<DrawerDescription>
|
||||
{filename} ({inKB(filesize)} KB)
|
||||
</DrawerDescription>
|
||||
@@ -42,7 +48,7 @@ export default function UploadCsvPreview(props: UploadCsvPreviewProps) {
|
||||
</DrawerHeader>
|
||||
<div className="m-4 max-h-[80%] overflow-auto">
|
||||
<pre className="bg-secondary rounded-md p-4 block text-sm">
|
||||
{content}
|
||||
{docContent}
|
||||
</pre>
|
||||
</div>
|
||||
</DrawerContent>
|
||||
@@ -50,25 +56,32 @@ export default function UploadCsvPreview(props: UploadCsvPreviewProps) {
|
||||
);
|
||||
}
|
||||
|
||||
function CSVSummaryCard(props: UploadCsvPreviewProps) {
|
||||
const { onRemove, csv } = props;
|
||||
const FileIcon: Record<DocumentFileType, string> = {
|
||||
csv: SheetIcon,
|
||||
pdf: PdfIcon,
|
||||
docx: DocxIcon,
|
||||
txt: TxtIcon,
|
||||
};
|
||||
|
||||
function PreviewCard(props: DocumentPreviewProps) {
|
||||
const { onRemove, file } = props;
|
||||
return (
|
||||
<div className="p-2 w-60 max-w-60 bg-secondary rounded-lg text-sm relative cursor-pointer">
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<div className="relative h-10 w-10 shrink-0 overflow-hidden rounded-md">
|
||||
<div className="relative h-8 w-8 shrink-0 overflow-hidden rounded-md">
|
||||
<Image
|
||||
className="h-full w-auto"
|
||||
priority
|
||||
src={SheetIcon}
|
||||
alt="SheetIcon"
|
||||
src={FileIcon[file.filetype]}
|
||||
alt="Icon"
|
||||
/>
|
||||
</div>
|
||||
<div className="overflow-hidden">
|
||||
<div className="truncate font-semibold">
|
||||
{csv.filename} ({inKB(csv.filesize)} KB)
|
||||
{file.filename} ({inKB(file.filesize)} KB)
|
||||
</div>
|
||||
<div className="truncate text-token-text-tertiary flex items-center gap-2">
|
||||
<span>Spreadsheet</span>
|
||||
<span>{file.filetype.toUpperCase()} File</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="-4 0 64 64" xmlns="http://www.w3.org/2000/svg">
|
||||
|
||||
<g fill-rule="evenodd">
|
||||
|
||||
<path d="m5.11 0a5.07 5.07 0 0 0 -5.11 5v53.88a5.07 5.07 0 0 0 5.11 5.12h45.78a5.07 5.07 0 0 0 5.11-5.12v-38.6l-18.94-20.28z" fill="#107cad"/>
|
||||
|
||||
<path d="m56 20.35v1h-12.82s-6.31-1.26-6.13-6.71c0 0 .21 5.71 6 5.71z" fill="#084968"/>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 309.267 309.267" xml:space="preserve">
|
||||
<g>
|
||||
<path style="fill:#E2574C;" d="M38.658,0h164.23l87.049,86.711v203.227c0,10.679-8.659,19.329-19.329,19.329H38.658
|
||||
c-10.67,0-19.329-8.65-19.329-19.329V19.329C19.329,8.65,27.989,0,38.658,0z"/>
|
||||
<path style="fill:#B53629;" d="M289.658,86.981h-67.372c-10.67,0-19.329-8.659-19.329-19.329V0.193L289.658,86.981z"/>
|
||||
<path style="fill:#FFFFFF;" d="M217.434,146.544c3.238,0,4.823-2.822,4.823-5.557c0-2.832-1.653-5.567-4.823-5.567h-18.44
|
||||
c-3.605,0-5.615,2.986-5.615,6.282v45.317c0,4.04,2.3,6.282,5.412,6.282c3.093,0,5.403-2.242,5.403-6.282v-12.438h11.153
|
||||
c3.46,0,5.19-2.832,5.19-5.644c0-2.754-1.73-5.49-5.19-5.49h-11.153v-16.903C204.194,146.544,217.434,146.544,217.434,146.544z
|
||||
M155.107,135.42h-13.492c-3.663,0-6.263,2.513-6.263,6.243v45.395c0,4.629,3.74,6.079,6.417,6.079h14.159
|
||||
c16.758,0,27.824-11.027,27.824-28.047C183.743,147.095,173.325,135.42,155.107,135.42z M155.755,181.946h-8.225v-35.334h7.413
|
||||
c11.221,0,16.101,7.529,16.101,17.918C171.044,174.253,166.25,181.946,155.755,181.946z M106.33,135.42H92.964
|
||||
c-3.779,0-5.886,2.493-5.886,6.282v45.317c0,4.04,2.416,6.282,5.663,6.282s5.663-2.242,5.663-6.282v-13.231h8.379
|
||||
c10.341,0,18.875-7.326,18.875-19.107C125.659,143.152,117.425,135.42,106.33,135.42z M106.108,163.158h-7.703v-17.097h7.703
|
||||
c4.755,0,7.78,3.711,7.78,8.553C113.878,159.447,110.863,163.158,106.108,163.158z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 512 512" xml:space="preserve">
|
||||
<path style="fill:#E2E5E7;" d="M128,0c-17.6,0-32,14.4-32,32v448c0,17.6,14.4,32,32,32h320c17.6,0,32-14.4,32-32V128L352,0H128z"/>
|
||||
<path style="fill:#B0B7BD;" d="M384,128h96L352,0v96C352,113.6,366.4,128,384,128z"/>
|
||||
<polygon style="fill:#CAD1D8;" points="480,224 384,128 480,128 "/>
|
||||
<path style="fill:#576D7E;" d="M416,416c0,8.8-7.2,16-16,16H48c-8.8,0-16-7.2-16-16V256c0-8.8,7.2-16,16-16h352c8.8,0,16,7.2,16,16
|
||||
V416z"/>
|
||||
<g>
|
||||
<path style="fill:#FFFFFF;" d="M132.784,311.472H110.4c-11.136,0-11.136-16.368,0-16.368h60.512c11.392,0,11.392,16.368,0,16.368
|
||||
h-21.248v64.592c0,11.12-16.896,11.392-16.896,0v-64.592H132.784z"/>
|
||||
<path style="fill:#FFFFFF;" d="M224.416,326.176l22.272-27.888c6.656-8.688,19.568,2.432,12.288,10.752
|
||||
c-7.68,9.088-15.728,18.944-23.424,29.024l26.112,32.496c7.024,9.6-7.04,18.816-13.952,9.344l-23.536-30.192l-23.152,30.832
|
||||
c-6.528,9.328-20.992-1.152-13.68-9.856l25.696-32.624c-8.048-10.096-15.856-19.936-23.664-29.024
|
||||
c-8.064-9.6,6.912-19.44,12.784-10.48L224.416,326.176z"/>
|
||||
<path style="fill:#FFFFFF;" d="M298.288,311.472H275.92c-11.136,0-11.136-16.368,0-16.368h60.496c11.392,0,11.392,16.368,0,16.368
|
||||
h-21.232v64.592c0,11.12-16.896,11.392-16.896,0V311.472z"/>
|
||||
</g>
|
||||
<path style="fill:#CAD1D8;" d="M400,432H96v16h304c8.8,0,16-7.2,16-16v-16C416,424.8,408.8,432,400,432z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -3,7 +3,7 @@ import ChatSection from "./components/chat-section";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<main className="h-full w-full flex justify-center items-center background-gradient">
|
||||
<main className="h-screen w-screen flex justify-center items-center background-gradient">
|
||||
<div className="space-y-2 lg:space-y-10 w-[90%] lg:w-[60rem]">
|
||||
<Header />
|
||||
<div className="h-[65vh] flex">
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
"llamaindex": "0.4.6",
|
||||
"lucide-react": "^0.294.0",
|
||||
"next": "^14.2.4",
|
||||
"pdf2json": "3.0.5",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-markdown": "^8.0.7",
|
||||
|
||||
Reference in New Issue
Block a user