mirror of
https://github.com/run-llama/create-llama.git
synced 2026-07-10 19:33:22 -04:00
Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3d3d94ca33 | |||
| b0e25b3d7d | |||
| ce2de2d0a8 | |||
| b3f3ce403f | |||
| d473867a9a | |||
| 5068e28c8e | |||
| 0ad2207684 | |||
| bfde30deed | |||
| 96fdb83abf | |||
| b7e0072c9c | |||
| 81bc340dda | |||
| ddf3aef7dc | |||
| 04cc7ce451 | |||
| 1f5a26f3a8 | |||
| bafb1d7d71 | |||
| 2eb2803284 | |||
| 48188ca3f9 | |||
| beaf4b4e90 | |||
| c24e60a427 | |||
| 14c0b2ea87 | |||
| f3c1e54e97 | |||
| 667c122eed | |||
| cc45225a4a | |||
| 5d190195b6 | |||
| 2e47af86ce | |||
| 778bdfaa0b | |||
| 2e43ba1615 | |||
| ff8bed62c7 | |||
| a9d2177dc3 |
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"create-llama": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Add CSV upload
|
||||||
@@ -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",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user