Compare commits

...

29 Commits

Author SHA1 Message Date
Thuc Pham 3d3d94ca33 feat: limit display 2024-05-30 10:03:32 +07:00
Thuc Pham b0e25b3d7d fix lint 2024-05-29 08:03:11 +00:00
Thuc Pham ce2de2d0a8 update convert message content 2024-05-29 07:58:15 +00:00
Thuc Pham b3f3ce403f resolve conflict 2024-05-29 08:41:05 +07:00
Thuc Pham d473867a9a get resouces inside chat input 2024-05-29 08:26:17 +07:00
Thuc Pham 5068e28c8e Merge branch 'main' into feat/implement-csv-upload 2024-05-29 08:10:11 +07:00
Huu Le 0ad2207684 Merge pull request #98 from run-llama/feat/construct-resource-url-from-backend
feat: construct resource url from backend
2024-05-28 20:43:04 +07:00
Thuc Pham bfde30deed move logger to global scope 2024-05-28 18:42:46 +07:00
Thuc Pham 96fdb83abf use logger warning 2024-05-28 18:33:53 +07:00
Huu Le b7e0072c9c chore: always generate tools config if user selects agent mode (#102) 2024-05-28 14:35:36 +07:00
Thuc Pham 81bc340dda add warning when no file server url prefix 2024-05-27 18:21:32 +07:00
Thuc Pham ddf3aef7dc remove node path 2024-05-27 18:20:27 +07:00
Thuc Pham 04cc7ce451 suggest csv resources from frontend annotation data 2024-05-27 17:45:16 +07:00
Thuc Pham 1f5a26f3a8 Merge pull request #100 from run-llama/feat/code-interpreter-python
feat: add support for FastAPI in code interpreter tool
2024-05-27 16:58:32 +07:00
Thuc Pham bafb1d7d71 remove multi modal prop 2024-05-27 11:12:20 +07:00
Marcus Schiesser 2eb2803284 Create bright-turkeys-melt.md 2024-05-24 19:03:59 +07:00
Thuc Pham 48188ca3f9 feat: construct resource url from backend 2024-05-24 14:40:44 +07:00
Thuc Pham beaf4b4e90 add timeout for streaming 2024-05-24 10:54:52 +07:00
Thuc Pham c24e60a427 Update pre-commit 2024-05-24 09:15:28 +07:00
Thuc Pham 14c0b2ea87 Update .gitignore 2024-05-24 09:15:13 +07:00
Thuc Pham f3c1e54e97 fix lint 2024-05-24 09:12:55 +07:00
Thuc Pham 667c122eed update icon & fix code review 2024-05-24 09:09:00 +07:00
Thuc Pham cc45225a4a fix lint 2024-05-24 08:51:23 +07:00
Thuc Pham 5d190195b6 remove dead code 2024-05-24 08:49:16 +07:00
Thuc Pham 2e47af86ce feat: implement csv upload 2024-05-24 08:43:51 +07:00
Thuc Pham 778bdfaa0b fix: typo 2024-05-24 08:43:51 +07:00
Thuc Pham 2e43ba1615 refactor: use local file system, use absolute resource url 2024-05-24 08:42:46 +07:00
Thuc Pham ff8bed62c7 build tool system prompt 2024-05-24 08:39:38 +07:00
Thuc Pham a9d2177dc3 feat: implement interpreter tool 2024-05-24 08:39:00 +07:00
23 changed files with 601 additions and 157 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"create-llama": patch
---
Add CSV upload
+6
View File
@@ -276,6 +276,12 @@ const getEngineEnvs = (): EnvVar[] => {
"The number of similar embeddings to return when retrieving documents.", "The number of similar embeddings to return when retrieving documents.",
value: "3", value: "3",
}, },
{
name: "STREAM_TIMEOUT",
description:
"The time in milliseconds to wait for the stream to return a response.",
value: "60000",
},
]; ];
}; };
-1
View File
@@ -159,7 +159,6 @@ export const writeToolsConfig = async (
tools: Tool[] = [], tools: Tool[] = [],
type: ConfigFileType = ConfigFileType.YAML, type: ConfigFileType = ConfigFileType.YAML,
) => { ) => {
if (tools.length === 0) return; // no tools selected, no config need
const configContent: { const configContent: {
[key in ToolType]: Record<string, any>; [key in ToolType]: Record<string, any>;
} = { } = {
@@ -15,7 +15,7 @@ export type InterpreterToolParams = {
fileServerURLPrefix?: string; fileServerURLPrefix?: string;
}; };
export type InterpreterToolOuput = { export type InterpreterToolOutput = {
isError: boolean; isError: boolean;
logs: Logs; logs: Logs;
extraResult: InterpreterExtraResult[]; extraResult: InterpreterExtraResult[];
@@ -88,7 +88,7 @@ export class InterpreterTool implements BaseTool<InterpreterParameter> {
return this.codeInterpreter; return this.codeInterpreter;
} }
public async codeInterpret(code: string): Promise<InterpreterToolOuput> { public async codeInterpret(code: string): Promise<InterpreterToolOutput> {
console.log( console.log(
`\n${"=".repeat(50)}\n> Running following AI-generated code:\n${code}\n${"=".repeat(50)}`, `\n${"=".repeat(50)}\n> Running following AI-generated code:\n${code}\n${"=".repeat(50)}`,
); );
@@ -96,7 +96,7 @@ export class InterpreterTool implements BaseTool<InterpreterParameter> {
const exec = await interpreter.notebook.execCell(code); const exec = await interpreter.notebook.execCell(code);
if (exec.error) console.error("[Code Interpreter error]", exec.error); if (exec.error) console.error("[Code Interpreter error]", exec.error);
const extraResult = await this.getExtraResult(exec.results[0]); const extraResult = await this.getExtraResult(exec.results[0]);
const result: InterpreterToolOuput = { const result: InterpreterToolOutput = {
isError: !!exec.error, isError: !!exec.error,
logs: exec.logs, logs: exec.logs,
extraResult, extraResult,
@@ -104,7 +104,7 @@ export class InterpreterTool implements BaseTool<InterpreterParameter> {
return result; return result;
} }
async call(input: InterpreterParameter): Promise<InterpreterToolOuput> { async call(input: InterpreterParameter): Promise<InterpreterToolOutput> {
const result = await this.codeInterpret(input.code); const result = await this.codeInterpret(input.code);
await this.codeInterpreter?.close(); await this.codeInterpreter?.close();
return result; return result;
@@ -1,5 +1,7 @@
"use client"; "use client";
import { Message } from "./chat-messages";
export interface ChatInputProps { export interface ChatInputProps {
/** The current value of the input */ /** The current value of the input */
input?: string; input?: string;
@@ -13,6 +15,7 @@ export interface ChatInputProps {
handleSubmit: (e: React.FormEvent<HTMLFormElement>) => void; handleSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
isLoading: boolean; isLoading: boolean;
multiModal?: boolean; multiModal?: boolean;
messages: Message[];
} }
export default function ChatInput(props: ChatInputProps) { export default function ChatInput(props: ChatInputProps) {
@@ -1,32 +1,23 @@
import { Message, StreamData, streamToResponse } from "ai"; import { Message, StreamData, streamToResponse } from "ai";
import { Request, Response } from "express"; import { Request, Response } from "express";
import { ChatMessage, MessageContent, Settings } from "llamaindex"; import { ChatMessage, Settings } from "llamaindex";
import { createChatEngine } from "./engine/chat"; import { createChatEngine } from "./engine/chat";
import { LlamaIndexStream } from "./llamaindex-stream"; import {
import { createCallbackManager } from "./stream-helper"; DataParserOptions,
LlamaIndexStream,
const convertMessageContent = ( convertMessageContent,
textMessage: string, } from "./llamaindex-stream";
imageUrl: string | undefined, import { createCallbackManager, createStreamTimeout } from "./stream-helper";
): MessageContent => {
if (!imageUrl) return textMessage;
return [
{
type: "text",
text: textMessage,
},
{
type: "image_url",
image_url: {
url: imageUrl,
},
},
];
};
export const chat = async (req: Request, res: Response) => { export const chat = async (req: Request, res: Response) => {
// Init Vercel AI StreamData and timeout
const vercelStreamData = new StreamData();
const streamTimeout = createStreamTimeout(vercelStreamData);
try { try {
const { messages, data }: { messages: Message[]; data: any } = req.body; const {
messages,
data,
}: { messages: Message[]; data: DataParserOptions | undefined } = req.body;
const userMessage = messages.pop(); const userMessage = messages.pop();
if (!messages || !userMessage || userMessage.role !== "user") { if (!messages || !userMessage || userMessage.role !== "user") {
return res.status(400).json({ return res.status(400).json({
@@ -38,13 +29,7 @@ export const chat = async (req: Request, res: Response) => {
const chatEngine = await createChatEngine(); const chatEngine = await createChatEngine();
// Convert message content from Vercel/AI format to LlamaIndex/OpenAI format // Convert message content from Vercel/AI format to LlamaIndex/OpenAI format
const userMessageContent = convertMessageContent( const userMessageContent = convertMessageContent(userMessage.content, data);
userMessage.content,
data?.imageUrl,
);
// Init Vercel AI StreamData
const vercelStreamData = new StreamData();
// Setup callbacks // Setup callbacks
const callbackManager = createCallbackManager(vercelStreamData); const callbackManager = createCallbackManager(vercelStreamData);
@@ -61,7 +46,8 @@ export const chat = async (req: Request, res: Response) => {
// Return a stream, which can be consumed by the Vercel/AI client // Return a stream, which can be consumed by the Vercel/AI client
const stream = LlamaIndexStream(response, vercelStreamData, { const stream = LlamaIndexStream(response, vercelStreamData, {
parserOptions: { parserOptions: {
image_url: data?.imageUrl, imageUrl: data?.imageUrl,
uploadedCsv: data?.uploadedCsv,
}, },
}); });
@@ -71,5 +57,7 @@ export const chat = async (req: Request, res: Response) => {
return res.status(500).json({ return res.status(500).json({
detail: (error as Error).message, detail: (error as Error).message,
}); });
} finally {
clearTimeout(streamTimeout);
} }
}; };
@@ -6,6 +6,7 @@ import {
type AIStreamCallbacksAndOptions, type AIStreamCallbacksAndOptions,
} from "ai"; } from "ai";
import { import {
MessageContent,
Metadata, Metadata,
NodeWithScore, NodeWithScore,
Response, Response,
@@ -13,20 +14,61 @@ import {
} from "llamaindex"; } from "llamaindex";
import { AgentStreamChatResponse } from "llamaindex/agent/base"; import { AgentStreamChatResponse } from "llamaindex/agent/base";
import { appendImageData, appendSourceData } from "./stream-helper"; import {
UploadedCsv,
appendCsvData,
appendImageData,
appendSourceData,
} from "./stream-helper";
type LlamaIndexResponse = type LlamaIndexResponse =
| AgentStreamChatResponse<ToolCallLLMMessageOptions> | AgentStreamChatResponse<ToolCallLLMMessageOptions>
| Response; | Response;
type ParserOptions = { export type DataParserOptions = {
image_url?: string; imageUrl?: string;
uploadedCsv?: UploadedCsv;
};
export const convertMessageContent = (
textMessage: string,
additionalData?: DataParserOptions,
): MessageContent => {
if (!additionalData) return textMessage;
const content: MessageContent = [
{
type: "text",
text: textMessage,
},
];
if (additionalData?.imageUrl) {
content.push({
type: "image_url",
image_url: {
url: additionalData?.imageUrl,
},
});
}
if (additionalData?.uploadedCsv) {
const csvContent =
"Use the following CSV data:\n" +
"```csv\n" +
additionalData.uploadedCsv.content +
"\n```";
content.push({
type: "text",
text: `${csvContent}\n\n${textMessage}`,
});
}
return content;
}; };
function createParser( function createParser(
res: AsyncIterable<LlamaIndexResponse>, res: AsyncIterable<LlamaIndexResponse>,
data: StreamData, data: StreamData,
opts?: ParserOptions, opts?: DataParserOptions,
) { ) {
const it = res[Symbol.asyncIterator](); const it = res[Symbol.asyncIterator]();
const trimStartOfStream = trimStartOfStreamHelper(); const trimStartOfStream = trimStartOfStreamHelper();
@@ -34,7 +76,8 @@ function createParser(
let sourceNodes: NodeWithScore<Metadata>[] | undefined; let sourceNodes: NodeWithScore<Metadata>[] | undefined;
return new ReadableStream<string>({ return new ReadableStream<string>({
start() { start() {
appendImageData(data, opts?.image_url); appendImageData(data, opts?.imageUrl);
appendCsvData(data, opts?.uploadedCsv);
}, },
async pull(controller): Promise<void> { async pull(controller): Promise<void> {
const { value, done } = await it.next(); const { value, done } = await it.next();
@@ -72,7 +115,7 @@ export function LlamaIndexStream(
data: StreamData, data: StreamData,
opts?: { opts?: {
callbacks?: AIStreamCallbacksAndOptions; callbacks?: AIStreamCallbacksAndOptions;
parserOptions?: ParserOptions; parserOptions?: DataParserOptions;
}, },
): ReadableStream<Uint8Array> { ): ReadableStream<Uint8Array> {
return createParser(response, data, opts?.parserOptions) return createParser(response, data, opts?.parserOptions)
@@ -17,6 +17,22 @@ export function appendImageData(data: StreamData, imageUrl?: 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;
}
export function appendSourceData( export function appendSourceData(
data: StreamData, data: StreamData,
sourceNodes?: NodeWithScore<Metadata>[], sourceNodes?: NodeWithScore<Metadata>[],
@@ -29,6 +45,7 @@ export function appendSourceData(
...node.node.toMutableJSON(), ...node.node.toMutableJSON(),
id: node.node.id_, id: node.node.id_,
score: node.score ?? null, score: node.score ?? null,
url: getNodeUrl(node.node.metadata),
})), })),
}, },
}); });
@@ -65,6 +82,15 @@ export function appendToolData(
}); });
} }
export function createStreamTimeout(stream: StreamData) {
const timeout = Number(process.env.STREAM_TIMEOUT ?? 1000 * 60 * 5); // default to 5 minutes
const t = setTimeout(() => {
appendEventData(stream, `Stream timed out after ${timeout / 1000} seconds`);
stream.close();
}, timeout);
return t;
}
export function createCallbackManager(stream: StreamData) { export function createCallbackManager(stream: StreamData) {
const callbackManager = new CallbackManager(); const callbackManager = new CallbackManager();
@@ -95,3 +121,17 @@ export function createCallbackManager(stream: StreamData) {
return callbackManager; return callbackManager;
} }
export type UploadedCsv = {
content: string;
filename: string;
filesize: number;
};
export function appendCsvData(data: StreamData, uploadedCsv?: UploadedCsv) {
if (!uploadedCsv) return;
data.appendMessageAnnotation({
type: "csv",
data: uploadedCsv,
});
}
@@ -1,3 +1,5 @@
import os
import logging
from pydantic import BaseModel from pydantic import BaseModel
from typing import List, Any, Optional, Dict, Tuple from typing import List, Any, Optional, Dict, Tuple
from fastapi import APIRouter, Depends, HTTPException, Request, status from fastapi import APIRouter, Depends, HTTPException, Request, status
@@ -11,6 +13,7 @@ from aiostream import stream
chat_router = r = APIRouter() chat_router = r = APIRouter()
logger = logging.getLogger("uvicorn")
class _Message(BaseModel): class _Message(BaseModel):
role: MessageRole role: MessageRole
@@ -38,14 +41,27 @@ class _SourceNodes(BaseModel):
metadata: Dict[str, Any] metadata: Dict[str, Any]
score: Optional[float] score: Optional[float]
text: str text: str
url: Optional[str]
@classmethod @classmethod
def from_source_node(cls, source_node: NodeWithScore): def from_source_node(cls, source_node: NodeWithScore):
metadata = source_node.node.metadata
url = metadata.get("URL")
if not url:
file_name = metadata.get("file_name")
url_prefix = os.getenv("FILESERVER_URL_PREFIX")
if not url_prefix:
logger.warning("Warning: FILESERVER_URL_PREFIX not set in environment variables")
if file_name and url_prefix:
url = f"{url_prefix}/data/{file_name}"
return cls( return cls(
id=source_node.node.node_id, id=source_node.node.node_id,
metadata=source_node.node.metadata, metadata=metadata,
score=source_node.score, score=source_node.score,
text=source_node.node.text, # type: ignore text=source_node.node.text, # type: ignore
url=url
) )
@classmethod @classmethod
@@ -6,6 +6,7 @@ import {
type AIStreamCallbacksAndOptions, type AIStreamCallbacksAndOptions,
} from "ai"; } from "ai";
import { import {
MessageContent,
Metadata, Metadata,
NodeWithScore, NodeWithScore,
Response, Response,
@@ -13,20 +14,61 @@ import {
} from "llamaindex"; } from "llamaindex";
import { AgentStreamChatResponse } from "llamaindex/agent/base"; import { AgentStreamChatResponse } from "llamaindex/agent/base";
import { appendImageData, appendSourceData } from "./stream-helper"; import {
UploadedCsv,
appendCsvData,
appendImageData,
appendSourceData,
} from "./stream-helper";
type LlamaIndexResponse = type LlamaIndexResponse =
| AgentStreamChatResponse<ToolCallLLMMessageOptions> | AgentStreamChatResponse<ToolCallLLMMessageOptions>
| Response; | Response;
type ParserOptions = { export type DataParserOptions = {
image_url?: string; imageUrl?: string;
uploadedCsv?: UploadedCsv;
};
export const convertMessageContent = (
textMessage: string,
additionalData?: DataParserOptions,
): MessageContent => {
if (!additionalData) return textMessage;
const content: MessageContent = [
{
type: "text",
text: textMessage,
},
];
if (additionalData?.imageUrl) {
content.push({
type: "image_url",
image_url: {
url: additionalData?.imageUrl,
},
});
}
if (additionalData?.uploadedCsv) {
const csvContent =
"Use the following CSV data:\n" +
"```csv\n" +
additionalData.uploadedCsv.content +
"\n```";
content.push({
type: "text",
text: `${csvContent}\n\n${textMessage}`,
});
}
return content;
}; };
function createParser( function createParser(
res: AsyncIterable<LlamaIndexResponse>, res: AsyncIterable<LlamaIndexResponse>,
data: StreamData, data: StreamData,
opts?: ParserOptions, opts?: DataParserOptions,
) { ) {
const it = res[Symbol.asyncIterator](); const it = res[Symbol.asyncIterator]();
const trimStartOfStream = trimStartOfStreamHelper(); const trimStartOfStream = trimStartOfStreamHelper();
@@ -34,7 +76,8 @@ function createParser(
let sourceNodes: NodeWithScore<Metadata>[] | undefined; let sourceNodes: NodeWithScore<Metadata>[] | undefined;
return new ReadableStream<string>({ return new ReadableStream<string>({
start() { start() {
appendImageData(data, opts?.image_url); appendImageData(data, opts?.imageUrl);
appendCsvData(data, opts?.uploadedCsv);
}, },
async pull(controller): Promise<void> { async pull(controller): Promise<void> {
const { value, done } = await it.next(); const { value, done } = await it.next();
@@ -72,7 +115,7 @@ export function LlamaIndexStream(
data: StreamData, data: StreamData,
opts?: { opts?: {
callbacks?: AIStreamCallbacksAndOptions; callbacks?: AIStreamCallbacksAndOptions;
parserOptions?: ParserOptions; parserOptions?: DataParserOptions;
}, },
): ReadableStream<Uint8Array> { ): ReadableStream<Uint8Array> {
return createParser(response, data, opts?.parserOptions) return createParser(response, data, opts?.parserOptions)
@@ -1,11 +1,15 @@
import { initObservability } from "@/app/observability"; import { initObservability } from "@/app/observability";
import { Message, StreamData, StreamingTextResponse } from "ai"; import { Message, StreamData, StreamingTextResponse } from "ai";
import { ChatMessage, MessageContent, Settings } from "llamaindex"; import { ChatMessage, Settings } from "llamaindex";
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { createChatEngine } from "./engine/chat"; import { createChatEngine } from "./engine/chat";
import { initSettings } from "./engine/settings"; import { initSettings } from "./engine/settings";
import { LlamaIndexStream } from "./llamaindex-stream"; import {
import { createCallbackManager } from "./stream-helper"; DataParserOptions,
LlamaIndexStream,
convertMessageContent,
} from "./llamaindex-stream";
import { createCallbackManager, createStreamTimeout } from "./stream-helper";
initObservability(); initObservability();
initSettings(); initSettings();
@@ -13,29 +17,17 @@ initSettings();
export const runtime = "nodejs"; export const runtime = "nodejs";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
const convertMessageContent = (
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) { export async function POST(request: NextRequest) {
// Init Vercel AI StreamData and timeout
const vercelStreamData = new StreamData();
const streamTimeout = createStreamTimeout(vercelStreamData);
try { try {
const body = await request.json(); const body = await request.json();
const { messages, data }: { messages: Message[]; data: any } = body; const {
messages,
data,
}: { messages: Message[]; data: DataParserOptions | undefined } = body;
const userMessage = messages.pop(); const userMessage = messages.pop();
if (!messages || !userMessage || userMessage.role !== "user") { if (!messages || !userMessage || userMessage.role !== "user") {
return NextResponse.json( return NextResponse.json(
@@ -50,13 +42,7 @@ export async function POST(request: NextRequest) {
const chatEngine = await createChatEngine(); const chatEngine = await createChatEngine();
// Convert message content from Vercel/AI format to LlamaIndex/OpenAI format // Convert message content from Vercel/AI format to LlamaIndex/OpenAI format
const userMessageContent = convertMessageContent( const userMessageContent = convertMessageContent(userMessage.content, data);
userMessage.content,
data?.imageUrl,
);
// Init Vercel AI StreamData
const vercelStreamData = new StreamData();
// Setup callbacks // Setup callbacks
const callbackManager = createCallbackManager(vercelStreamData); const callbackManager = createCallbackManager(vercelStreamData);
@@ -73,7 +59,8 @@ export async function POST(request: NextRequest) {
// Transform LlamaIndex stream to Vercel/AI format // Transform LlamaIndex stream to Vercel/AI format
const stream = LlamaIndexStream(response, vercelStreamData, { const stream = LlamaIndexStream(response, vercelStreamData, {
parserOptions: { parserOptions: {
image_url: data?.imageUrl, imageUrl: data?.imageUrl,
uploadedCsv: data?.uploadedCsv,
}, },
}); });
@@ -89,5 +76,7 @@ export async function POST(request: NextRequest) {
status: 500, status: 500,
}, },
); );
} finally {
clearTimeout(streamTimeout);
} }
} }
@@ -17,6 +17,22 @@ export function appendImageData(data: StreamData, imageUrl?: 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;
}
export function appendSourceData( export function appendSourceData(
data: StreamData, data: StreamData,
sourceNodes?: NodeWithScore<Metadata>[], sourceNodes?: NodeWithScore<Metadata>[],
@@ -29,6 +45,7 @@ export function appendSourceData(
...node.node.toMutableJSON(), ...node.node.toMutableJSON(),
id: node.node.id_, id: node.node.id_,
score: node.score ?? null, score: node.score ?? null,
url: getNodeUrl(node.node.metadata),
})), })),
}, },
}); });
@@ -65,6 +82,15 @@ export function appendToolData(
}); });
} }
export function createStreamTimeout(stream: StreamData) {
const timeout = Number(process.env.STREAM_TIMEOUT ?? 1000 * 60 * 5); // default to 5 minutes
const t = setTimeout(() => {
appendEventData(stream, `Stream timed out after ${timeout / 1000} seconds`);
stream.close();
}, timeout);
return t;
}
export function createCallbackManager(stream: StreamData) { export function createCallbackManager(stream: StreamData) {
const callbackManager = new CallbackManager(); const callbackManager = new CallbackManager();
@@ -95,3 +121,17 @@ export function createCallbackManager(stream: StreamData) {
return callbackManager; return callbackManager;
} }
export type UploadedCsv = {
content: string;
filename: string;
filesize: number;
};
export function appendCsvData(data: StreamData, uploadedCsv?: UploadedCsv) {
if (!uploadedCsv) return;
data.appendMessageAnnotation({
type: "csv",
data: uploadedCsv,
});
}
@@ -37,7 +37,7 @@ export default function ChatSection() {
handleSubmit={handleSubmit} handleSubmit={handleSubmit}
handleInputChange={handleInputChange} handleInputChange={handleInputChange}
isLoading={isLoading} isLoading={isLoading}
multiModal={true} messages={messages}
/> />
</div> </div>
); );
@@ -1,8 +1,11 @@
import { useState } from "react"; import { useEffect, useState } from "react";
import { CsvData, getInputResources } from ".";
import { Button } from "../button"; import { Button } from "../button";
import FileUploader from "../file-uploader"; import FileUploader from "../file-uploader";
import { Input } from "../input"; import { Input } from "../input";
import UploadCsvPreview from "../upload-csv-preview";
import UploadImagePreview from "../upload-image-preview"; import UploadImagePreview from "../upload-image-preview";
import ChatResources from "./chat-resources";
import { ChatHandler } from "./chat.interface"; import { ChatHandler } from "./chat.interface";
export default function ChatInput( export default function ChatInput(
@@ -14,11 +17,21 @@ export default function ChatInput(
| "onFileError" | "onFileError"
| "handleSubmit" | "handleSubmit"
| "handleInputChange" | "handleInputChange"
> & { | "messages"
multiModal?: boolean; >,
},
) { ) {
const [imageUrl, setImageUrl] = useState<string | null>(null); 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 onSubmit = (e: React.FormEvent<HTMLFormElement>) => { const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
if (imageUrl) { if (imageUrl) {
@@ -28,6 +41,24 @@ export default function ChatInput(
setImageUrl(null); setImageUrl(null);
return; 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) {
props.handleSubmit(e, {
data: { uploadedCsv: attachCsv },
});
return;
}
props.handleSubmit(e); props.handleSubmit(e);
}; };
@@ -43,25 +74,62 @@ export default function ChatInput(
setImageUrl(base64); setImageUrl(base64);
}; };
const handleUploadCsvFile = async (file: File) => {
const content = await new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.readAsText(file);
reader.onload = () => resolve(reader.result as string);
reader.onerror = (error) => reject(error);
});
setUploadedCsv({
content,
filename: file.name,
filesize: file.size,
});
};
const handleUploadFile = async (file: File) => { const handleUploadFile = async (file: File) => {
try { try {
if (props.multiModal && file.type.startsWith("image/")) { if (file.type.startsWith("image/")) {
return await handleUploadImageFile(file); return await handleUploadImageFile(file);
} }
if (file.type === "text/csv") {
return await handleUploadCsvFile(file);
}
props.onFileUpload?.(file); props.onFileUpload?.(file);
} catch (error: any) { } catch (error: any) {
props.onFileError?.(error.message); props.onFileError?.(error.message);
} }
}; };
const removeResource = (index: number) => {
setInputResources((resources) => {
const newResources = [...resources];
newResources[index].selected = false;
return newResources;
});
};
return ( return (
<form <form
onSubmit={onSubmit} onSubmit={onSubmit}
className="rounded-xl bg-white p-4 shadow-xl space-y-4" className="rounded-xl bg-white p-4 shadow-xl space-y-4"
> >
<ChatResources
isLoading={props.isLoading}
resources={inputResources}
removeResource={removeResource}
/>
{imageUrl && ( {imageUrl && (
<UploadImagePreview url={imageUrl} onRemove={onRemovePreviewImage} /> <UploadImagePreview url={imageUrl} onRemove={onRemovePreviewImage} />
)} )}
{uploadedCsv && (
<UploadCsvPreview
filename={uploadedCsv.filename}
filesize={uploadedCsv.filesize}
onRemove={() => setUploadedCsv(undefined)}
/>
)}
<div className="flex w-full items-start justify-between gap-4 "> <div className="flex w-full items-start justify-between gap-4 ">
<Input <Input
autoFocus autoFocus
@@ -8,14 +8,16 @@ import { ChatEvents } from "./chat-events";
import { ChatImage } from "./chat-image"; import { ChatImage } from "./chat-image";
import { ChatSources } from "./chat-sources"; import { ChatSources } from "./chat-sources";
import ChatTools from "./chat-tools"; import ChatTools from "./chat-tools";
import CsvContent from "./csv-content";
import { import {
AnnotationData, CsvData,
EventData, EventData,
ImageData, ImageData,
MessageAnnotation, MessageAnnotation,
MessageAnnotationType, MessageAnnotationType,
SourceData, SourceData,
ToolData, ToolData,
getAnnotationData,
} from "./index"; } from "./index";
import Markdown from "./markdown"; import Markdown from "./markdown";
import { useCopyToClipboard } from "./use-copy-to-clipboard"; import { useCopyToClipboard } from "./use-copy-to-clipboard";
@@ -25,13 +27,6 @@ type ContentDisplayConfig = {
component: JSX.Element | null; component: JSX.Element | null;
}; };
function getAnnotationData<T extends AnnotationData>(
annotations: MessageAnnotation[],
type: MessageAnnotationType,
): T[] {
return annotations.filter((a) => a.type === type).map((a) => a.data as T);
}
function ChatMessageContent({ function ChatMessageContent({
message, message,
isLoading, isLoading,
@@ -46,6 +41,10 @@ function ChatMessageContent({
annotations, annotations,
MessageAnnotationType.IMAGE, MessageAnnotationType.IMAGE,
); );
const csvData = getAnnotationData<CsvData>(
annotations,
MessageAnnotationType.CSV,
);
const eventData = getAnnotationData<EventData>( const eventData = getAnnotationData<EventData>(
annotations, annotations,
MessageAnnotationType.EVENTS, MessageAnnotationType.EVENTS,
@@ -61,16 +60,20 @@ function ChatMessageContent({
const contents: ContentDisplayConfig[] = [ const contents: ContentDisplayConfig[] = [
{ {
order: -3, order: -4,
component: imageData[0] ? <ChatImage data={imageData[0]} /> : null, component: imageData[0] ? <ChatImage data={imageData[0]} /> : null,
}, },
{ {
order: -2, order: -3,
component: component:
eventData.length > 0 ? ( eventData.length > 0 ? (
<ChatEvents isLoading={isLoading} data={eventData} /> <ChatEvents isLoading={isLoading} data={eventData} />
) : null, ) : null,
}, },
{
order: -2,
component: csvData[0] ? <CsvContent data={csvData[0]} /> : null,
},
{ {
order: -1, order: -1,
component: toolData[0] ? <ChatTools data={toolData[0]} /> : null, component: toolData[0] ? <ChatTools data={toolData[0]} /> : null,
@@ -0,0 +1,48 @@
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>
);
}
@@ -2,12 +2,10 @@ import { Check, Copy } from "lucide-react";
import { useMemo } from "react"; import { useMemo } from "react";
import { Button } from "../button"; import { Button } from "../button";
import { HoverCard, HoverCardContent, HoverCardTrigger } from "../hover-card"; import { HoverCard, HoverCardContent, HoverCardTrigger } from "../hover-card";
import { getStaticFileDataUrl } from "../lib/url"; import { SourceData } from "./index";
import { SourceData, SourceNode } from "./index";
import { useCopyToClipboard } from "./use-copy-to-clipboard"; import { useCopyToClipboard } from "./use-copy-to-clipboard";
import PdfDialog from "./widgets/PdfDialog"; import PdfDialog from "./widgets/PdfDialog";
const DATA_SOURCE_FOLDER = "data";
const SCORE_THRESHOLD = 0.3; const SCORE_THRESHOLD = 0.3;
function SourceNumberButton({ index }: { index: number }) { function SourceNumberButton({ index }: { index: number }) {
@@ -18,46 +16,11 @@ function SourceNumberButton({ index }: { index: number }) {
); );
} }
enum NODE_TYPE {
URL,
FILE,
UNKNOWN,
}
type NodeInfo = { type NodeInfo = {
id: string; id: string;
type: NODE_TYPE;
path?: string;
url?: string; url?: string;
}; };
function getNodeInfo(node: SourceNode): NodeInfo {
if (typeof node.metadata["URL"] === "string") {
const url = node.metadata["URL"];
return {
id: node.id,
type: NODE_TYPE.URL,
path: url,
url,
};
}
if (typeof node.metadata["file_path"] === "string") {
const fileName = node.metadata["file_name"] as string;
const filePath = `${DATA_SOURCE_FOLDER}/${fileName}`;
return {
id: node.id,
type: NODE_TYPE.FILE,
path: node.metadata["file_path"],
url: getStaticFileDataUrl(filePath),
};
}
return {
id: node.id,
type: NODE_TYPE.UNKNOWN,
};
}
export function ChatSources({ data }: { data: SourceData }) { export function ChatSources({ data }: { data: SourceData }) {
const sources: NodeInfo[] = useMemo(() => { const sources: NodeInfo[] = useMemo(() => {
// aggregate nodes by url or file_path (get the highest one by score) // aggregate nodes by url or file_path (get the highest one by score)
@@ -67,8 +30,11 @@ export function ChatSources({ data }: { data: SourceData }) {
.filter((node) => (node.score ?? 1) > SCORE_THRESHOLD) .filter((node) => (node.score ?? 1) > SCORE_THRESHOLD)
.sort((a, b) => (b.score ?? 1) - (a.score ?? 1)) .sort((a, b) => (b.score ?? 1) - (a.score ?? 1))
.forEach((node) => { .forEach((node) => {
const nodeInfo = getNodeInfo(node); const nodeInfo = {
const key = nodeInfo.path ?? nodeInfo.id; // use id as key for UNKNOWN type id: node.id,
url: node.url,
};
const key = nodeInfo.url ?? nodeInfo.id; // use id as key for UNKNOWN type
if (!nodesByPath[key]) { if (!nodesByPath[key]) {
nodesByPath[key] = nodeInfo; nodesByPath[key] = nodeInfo;
} }
@@ -84,13 +50,12 @@ export function ChatSources({ data }: { data: SourceData }) {
<span className="font-semibold">Sources:</span> <span className="font-semibold">Sources:</span>
<div className="inline-flex gap-1 items-center"> <div className="inline-flex gap-1 items-center">
{sources.map((nodeInfo: NodeInfo, index: number) => { {sources.map((nodeInfo: NodeInfo, index: number) => {
if (nodeInfo.path?.endsWith(".pdf")) { if (nodeInfo.url?.endsWith(".pdf")) {
return ( return (
<PdfDialog <PdfDialog
key={nodeInfo.id} key={nodeInfo.id}
documentId={nodeInfo.id} documentId={nodeInfo.id}
url={nodeInfo.url!} url={nodeInfo.url!}
path={nodeInfo.path}
trigger={<SourceNumberButton index={index} />} trigger={<SourceNumberButton index={index} />}
/> />
); );
@@ -116,16 +81,16 @@ export function ChatSources({ data }: { data: SourceData }) {
function NodeInfo({ nodeInfo }: { nodeInfo: NodeInfo }) { function NodeInfo({ nodeInfo }: { nodeInfo: NodeInfo }) {
const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 1000 }); const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 1000 });
if (nodeInfo.type !== NODE_TYPE.UNKNOWN) { if (nodeInfo.url) {
// this is a node generated by the web loader or file loader, // this is a node generated by the web loader or file loader,
// add a link to view its URL and a button to copy the URL to the clipboard // add a link to view its URL and a button to copy the URL to the clipboard
return ( return (
<div className="flex items-center my-2"> <div className="flex items-center my-2">
<a className="hover:text-blue-900" href={nodeInfo.url} target="_blank"> <a className="hover:text-blue-900" href={nodeInfo.url} target="_blank">
<span>{nodeInfo.path}</span> <span>{nodeInfo.url}</span>
</a> </a>
<Button <Button
onClick={() => copyToClipboard(nodeInfo.path!)} onClick={() => copyToClipboard(nodeInfo.url!)}
size="icon" size="icon"
variant="ghost" variant="ghost"
className="h-12 w-12 shrink-0" className="h-12 w-12 shrink-0"
@@ -0,0 +1,15 @@
import { CsvData } from ".";
const LIMIT_DISPLAY = 100; // Limit the display of CSV content to 100 characters
export default function CsvContent({ data }: { data: CsvData }) {
const summaryContent = data.content.slice(0, LIMIT_DISPLAY) + "...";
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>
);
}
@@ -1,4 +1,4 @@
import { JSONValue } from "ai"; import { JSONValue, Message } from "ai";
import ChatInput from "./chat-input"; import ChatInput from "./chat-input";
import ChatMessages from "./chat-messages"; import ChatMessages from "./chat-messages";
@@ -6,6 +6,7 @@ export { type ChatHandler } from "./chat.interface";
export { ChatInput, ChatMessages }; export { ChatInput, ChatMessages };
export enum MessageAnnotationType { export enum MessageAnnotationType {
CSV = "csv",
IMAGE = "image", IMAGE = "image",
SOURCES = "sources", SOURCES = "sources",
EVENTS = "events", EVENTS = "events",
@@ -16,11 +17,18 @@ export type ImageData = {
url: string; url: string;
}; };
export type CsvData = {
content: string;
filename: string;
filesize: number;
};
export type SourceNode = { export type SourceNode = {
id: string; id: string;
metadata: Record<string, unknown>; metadata: Record<string, unknown>;
score?: number; score?: number;
text: string; text: string;
url?: string;
}; };
export type SourceData = { export type SourceData = {
@@ -46,9 +54,50 @@ export type ToolData = {
}; };
}; };
export type AnnotationData = ImageData | SourceData | EventData | ToolData; export type AnnotationData =
| ImageData
| CsvData
| SourceData
| EventData
| ToolData;
export type MessageAnnotation = { export type MessageAnnotation = {
type: MessageAnnotationType; type: MessageAnnotationType;
data: AnnotationData; data: AnnotationData;
}; };
export function getAnnotationData<T extends AnnotationData>(
annotations: MessageAnnotation[],
type: MessageAnnotationType,
): 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,
};
};
@@ -12,7 +12,6 @@ import {
export interface PdfDialogProps { export interface PdfDialogProps {
documentId: string; documentId: string;
path: string;
url: string; url: string;
trigger: React.ReactNode; trigger: React.ReactNode;
} }
@@ -26,13 +25,13 @@ export default function PdfDialog(props: PdfDialogProps) {
<div className="space-y-2"> <div className="space-y-2">
<DrawerTitle>PDF Content</DrawerTitle> <DrawerTitle>PDF Content</DrawerTitle>
<DrawerDescription> <DrawerDescription>
File path:{" "} File URL:{" "}
<a <a
className="hover:text-blue-900" className="hover:text-blue-900"
href={props.url} href={props.url}
target="_blank" target="_blank"
> >
{props.path} {props.url}
</a> </a>
</DrawerDescription> </DrawerDescription>
</div> </div>
@@ -0,0 +1,90 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="49px" height="67px" viewBox="0 0 49 67" version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Sheets-icon</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z" id="path-1"></path>
<path d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z" id="path-3"></path>
<path d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z" id="path-5"></path>
<linearGradient x1="50.0053945%" y1="8.58610612%" x2="50.0053945%" y2="100.013939%" id="linearGradient-7">
<stop stop-color="#263238" stop-opacity="0.2" offset="0%"></stop>
<stop stop-color="#263238" stop-opacity="0.02" offset="100%"></stop>
</linearGradient>
<path d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z" id="path-8"></path>
<path d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z" id="path-10"></path>
<path d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z" id="path-12"></path>
<path d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z" id="path-14"></path>
<radialGradient cx="3.16804688%" cy="2.71744318%" fx="3.16804688%" fy="2.71744318%" r="161.248516%" gradientTransform="translate(0.031680,0.027174),scale(1.000000,0.727273),translate(-0.031680,-0.027174)" id="radialGradient-16">
<stop stop-color="#FFFFFF" stop-opacity="0.1" offset="0%"></stop>
<stop stop-color="#FFFFFF" stop-opacity="0" offset="100%"></stop>
</radialGradient>
</defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Consumer-Apps-Sheets-Large-VD-R8-" transform="translate(-451.000000, -451.000000)">
<g id="Hero" transform="translate(0.000000, 63.000000)">
<g id="Personal" transform="translate(277.000000, 299.000000)">
<g id="Sheets-icon" transform="translate(174.833333, 89.958333)">
<g id="Group">
<g id="Clipped">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<g id="SVGID_1_"></g>
<path d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L36.9791667,10.3541667 L29.5833333,0 Z" id="Path" fill="#0F9D58" fill-rule="nonzero" mask="url(#mask-2)"></path>
</g>
<g id="Clipped">
<mask id="mask-4" fill="white">
<use xlink:href="#path-3"></use>
</mask>
<g id="SVGID_1_"></g>
<path d="M11.8333333,31.8020833 L11.8333333,53.25 L35.5,53.25 L35.5,31.8020833 L11.8333333,31.8020833 Z M22.1875,50.2916667 L14.7916667,50.2916667 L14.7916667,46.59375 L22.1875,46.59375 L22.1875,50.2916667 Z M22.1875,44.375 L14.7916667,44.375 L14.7916667,40.6770833 L22.1875,40.6770833 L22.1875,44.375 Z M22.1875,38.4583333 L14.7916667,38.4583333 L14.7916667,34.7604167 L22.1875,34.7604167 L22.1875,38.4583333 Z M32.5416667,50.2916667 L25.1458333,50.2916667 L25.1458333,46.59375 L32.5416667,46.59375 L32.5416667,50.2916667 Z M32.5416667,44.375 L25.1458333,44.375 L25.1458333,40.6770833 L32.5416667,40.6770833 L32.5416667,44.375 Z M32.5416667,38.4583333 L25.1458333,38.4583333 L25.1458333,34.7604167 L32.5416667,34.7604167 L32.5416667,38.4583333 Z" id="Shape" fill="#F1F1F1" fill-rule="nonzero" mask="url(#mask-4)"></path>
</g>
<g id="Clipped">
<mask id="mask-6" fill="white">
<use xlink:href="#path-5"></use>
</mask>
<g id="SVGID_1_"></g>
<polygon id="Path" fill="url(#linearGradient-7)" fill-rule="nonzero" mask="url(#mask-6)" points="30.8813021 16.4520313 47.3333333 32.9003646 47.3333333 17.75"></polygon>
</g>
<g id="Clipped">
<mask id="mask-9" fill="white">
<use xlink:href="#path-8"></use>
</mask>
<g id="SVGID_1_"></g>
<g id="Group" mask="url(#mask-9)">
<g transform="translate(26.625000, -2.958333)">
<path d="M2.95833333,2.95833333 L2.95833333,16.2708333 C2.95833333,18.7225521 4.94411458,20.7083333 7.39583333,20.7083333 L20.7083333,20.7083333 L2.95833333,2.95833333 Z" id="Path" fill="#87CEAC" fill-rule="nonzero"></path>
</g>
</g>
</g>
<g id="Clipped">
<mask id="mask-11" fill="white">
<use xlink:href="#path-10"></use>
</mask>
<g id="SVGID_1_"></g>
<path d="M4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,4.80729167 C0,2.36666667 1.996875,0.369791667 4.4375,0.369791667 L29.5833333,0.369791667 L29.5833333,0 L4.4375,0 Z" id="Path" fill-opacity="0.2" fill="#FFFFFF" fill-rule="nonzero" mask="url(#mask-11)"></path>
</g>
<g id="Clipped">
<mask id="mask-13" fill="white">
<use xlink:href="#path-12"></use>
</mask>
<g id="SVGID_1_"></g>
<path d="M42.8958333,64.7135417 L4.4375,64.7135417 C1.996875,64.7135417 0,62.7166667 0,60.2760417 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,60.2760417 C47.3333333,62.7166667 45.3364583,64.7135417 42.8958333,64.7135417 Z" id="Path" fill-opacity="0.2" fill="#263238" fill-rule="nonzero" mask="url(#mask-13)"></path>
</g>
<g id="Clipped">
<mask id="mask-15" fill="white">
<use xlink:href="#path-14"></use>
</mask>
<g id="SVGID_1_"></g>
<path d="M34.0208333,17.75 C31.5691146,17.75 29.5833333,15.7642188 29.5833333,13.3125 L29.5833333,13.6822917 C29.5833333,16.1340104 31.5691146,18.1197917 34.0208333,18.1197917 L47.3333333,18.1197917 L47.3333333,17.75 L34.0208333,17.75 Z" id="Path" fill-opacity="0.1" fill="#263238" fill-rule="nonzero" mask="url(#mask-15)"></path>
</g>
</g>
<path d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z" id="Path" fill="url(#radialGradient-16)" fill-rule="nonzero"></path>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.9 KiB

@@ -1,11 +0,0 @@
const staticFileAPI = "/api/files";
export const getStaticFileDataUrl = (filePath: string) => {
const isUsingBackend = !!process.env.NEXT_PUBLIC_CHAT_API;
const fileUrl = `${staticFileAPI}/${filePath}`;
if (isUsingBackend) {
const backendOrigin = new URL(process.env.NEXT_PUBLIC_CHAT_API!).origin;
return `${backendOrigin}${fileUrl}`;
}
return fileUrl;
};
@@ -0,0 +1,46 @@
import { XCircleIcon } from "lucide-react";
import Image from "next/image";
import SheetIcon from "../ui/icons/sheet.svg";
import { cn } from "./lib/utils";
export default function UploadCsvPreview({
filename,
filesize,
onRemove,
}: {
filename: string;
filesize: number;
onRemove: () => void;
}) {
const fileSizeInKB = Math.round((filesize / 1024) * 10) / 10;
return (
<div className="p-2 w-80 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
className="h-full w-auto"
priority
src={SheetIcon}
alt="SheetIcon"
/>
</div>
<div className="overflow-hidden">
<div className="truncate font-semibold">
{filename} ({fileSizeInKB} KB)
</div>
<div className="truncate text-token-text-tertiary">Spreadsheet</div>
</div>
</div>
<div
className={cn(
"absolute -top-2 -right-2 w-6 h-6 z-10 bg-gray-500 text-white rounded-full",
)}
>
<XCircleIcon
className="w-6 h-6 bg-gray-500 text-white rounded-full"
onClick={onRemove}
/>
</div>
</div>
);
}