major refactor
This commit is contained in:
@@ -55,7 +55,7 @@
|
|||||||
"tailwind-merge": "^3.0.2",
|
"tailwind-merge": "^3.0.2",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"use-stick-to-bottom": "^1.0.46",
|
"use-stick-to-bottom": "^1.0.46",
|
||||||
"uuid": "^11.0.5",
|
"uuid": "^11.1.0",
|
||||||
"zod": "^3.24.2"
|
"zod": "^3.24.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -63,10 +63,10 @@
|
|||||||
"@tailwindcss/postcss": "^4.0.13",
|
"@tailwindcss/postcss": "^4.0.13",
|
||||||
"@types/lodash": "^4.17.16",
|
"@types/lodash": "^4.17.16",
|
||||||
"@types/node": "^22.13.5",
|
"@types/node": "^22.13.5",
|
||||||
"@types/pdf-parse": "^1.1.5",
|
|
||||||
"@types/react": "^19.0.8",
|
"@types/react": "^19.0.8",
|
||||||
"@types/react-dom": "^19.0.3",
|
"@types/react-dom": "^19.0.3",
|
||||||
"@types/react-syntax-highlighter": "^15.5.13",
|
"@types/react-syntax-highlighter": "^15.5.13",
|
||||||
|
"@types/uuid": "^10.0.0",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
"eslint": "^9.19.0",
|
"eslint": "^9.19.0",
|
||||||
@@ -75,7 +75,6 @@
|
|||||||
"eslint-plugin-react-refresh": "^0.4.18",
|
"eslint-plugin-react-refresh": "^0.4.18",
|
||||||
"globals": "^15.14.0",
|
"globals": "^15.14.0",
|
||||||
"next": "^15.2.3",
|
"next": "^15.2.3",
|
||||||
"pdf-parse": "^1.1.1",
|
|
||||||
"postcss": "^8.5.3",
|
"postcss": "^8.5.3",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||||
|
|||||||
748
pnpm-lock.yaml
generated
748
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -37,14 +37,14 @@ import {
|
|||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "../ui/tooltip";
|
} from "../ui/tooltip";
|
||||||
import type { Base64ContentBlock } from "@/lib/pdf";
|
import {
|
||||||
|
fileToImageBlock,
|
||||||
type MessageContentType = Message["content"];
|
fileToPDFBlock,
|
||||||
interface UploadedBlock {
|
toOpenAIImageBlock,
|
||||||
id: string;
|
toOpenAIPDFBlock,
|
||||||
name: string;
|
} from "@/lib/multimodal-utils";
|
||||||
block: Base64ContentBlock;
|
import type { Base64ContentBlock } from "@langchain/core/messages";
|
||||||
}
|
import { convertToOpenAIImageBlock } from "@langchain/core/messages";
|
||||||
|
|
||||||
function StickyToBottomContent(props: {
|
function StickyToBottomContent(props: {
|
||||||
content: ReactNode;
|
content: ReactNode;
|
||||||
@@ -122,8 +122,8 @@ export function Thread() {
|
|||||||
parseAsBoolean.withDefault(false),
|
parseAsBoolean.withDefault(false),
|
||||||
);
|
);
|
||||||
const [input, setInput] = useState("");
|
const [input, setInput] = useState("");
|
||||||
const [imageUrlList, setImageUrlList] = useState<UploadedBlock[]>([]);
|
const [imageUrlList, setImageUrlList] = useState<Base64ContentBlock[]>([]);
|
||||||
const [pdfUrlList, setPdfUrlList] = useState<UploadedBlock[]>([]);
|
const [pdfUrlList, setPdfUrlList] = useState<Base64ContentBlock[]>([]);
|
||||||
const [firstTokenReceived, setFirstTokenReceived] = useState(false);
|
const [firstTokenReceived, setFirstTokenReceived] = useState(false);
|
||||||
const isLargeScreen = useMediaQuery("(min-width: 1024px)");
|
const isLargeScreen = useMediaQuery("(min-width: 1024px)");
|
||||||
|
|
||||||
@@ -182,16 +182,23 @@ export function Thread() {
|
|||||||
if (!input.trim() || isLoading) return;
|
if (!input.trim() || isLoading) return;
|
||||||
setFirstTokenReceived(false);
|
setFirstTokenReceived(false);
|
||||||
|
|
||||||
|
// TODO: check configurable object for modelname camelcase or snakecase else do openai format
|
||||||
|
const isOpenAI = true
|
||||||
|
|
||||||
|
const pdfBlocks = pdfUrlList.map(toOpenAIPDFBlock);
|
||||||
|
|
||||||
const newHumanMessage: Message = {
|
const newHumanMessage: Message = {
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
type: "human",
|
type: "human",
|
||||||
content: [
|
content: [
|
||||||
{ type: "text", text: input },
|
{ type: "text", text: input },
|
||||||
...imageUrlList.map((item) => item.block),
|
...imageUrlList.map(toOpenAIImageBlock),
|
||||||
...pdfUrlList.map((item) => item.block),
|
...pdfBlocks,
|
||||||
] as MessageContentType,
|
] as Message["content"],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log("Message content:", newHumanMessage.content);
|
||||||
|
|
||||||
const toolMessages = ensureToolCallsHaveResponses(stream.messages);
|
const toolMessages = ensureToolCallsHaveResponses(stream.messages);
|
||||||
stream.submit(
|
stream.submit(
|
||||||
{ messages: [...toolMessages, newHumanMessage] },
|
{ messages: [...toolMessages, newHumanMessage] },
|
||||||
@@ -210,37 +217,16 @@ export function Thread() {
|
|||||||
|
|
||||||
setInput("");
|
setInput("");
|
||||||
setImageUrlList([]);
|
setImageUrlList([]);
|
||||||
|
setPdfUrlList([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleImageUpload = async (e: ChangeEvent<HTMLInputElement>) => {
|
const handleImageUpload = async (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
const files = e.target.files;
|
const files = e.target.files;
|
||||||
if (files) {
|
if (files) {
|
||||||
const imageFiles: UploadedBlock[] = await Promise.all(
|
const imageBlocks = await Promise.all(
|
||||||
Array.from(files).map((file) => {
|
Array.from(files).map(fileToImageBlock)
|
||||||
return new Promise<UploadedBlock>((resolve) => {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onloadend = () => {
|
|
||||||
const result = reader.result as string;
|
|
||||||
const base64 = result.split(",")[1];
|
|
||||||
const match = result.match(/^data:(.*);base64/);
|
|
||||||
const mimeType = match && match[1] ? match[1] : file.type;
|
|
||||||
resolve({
|
|
||||||
id: uuidv4(),
|
|
||||||
name: file.name,
|
|
||||||
block: {
|
|
||||||
type: "image",
|
|
||||||
source_type: "base64",
|
|
||||||
data: base64,
|
|
||||||
mime_type: mimeType,
|
|
||||||
metadata: { name: file.name },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
setImageUrlList([...imageUrlList, ...imageFiles]);
|
setImageUrlList((prev) => [...prev, ...imageBlocks]);
|
||||||
}
|
}
|
||||||
e.target.value = "";
|
e.target.value = "";
|
||||||
};
|
};
|
||||||
@@ -248,33 +234,10 @@ export function Thread() {
|
|||||||
const handlePDFUpload = async (e: ChangeEvent<HTMLInputElement>) => {
|
const handlePDFUpload = async (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
const files = e.target.files;
|
const files = e.target.files;
|
||||||
if (files) {
|
if (files) {
|
||||||
const pdfFiles: UploadedBlock[] = await Promise.all(
|
const pdfBlocks = await Promise.all(
|
||||||
Array.from(files).map((file) => {
|
Array.from(files).map(fileToPDFBlock)
|
||||||
return new Promise<UploadedBlock>((resolve) => {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onloadend = () => {
|
|
||||||
const result = reader.result as string;
|
|
||||||
const base64 = result.split(",")[1];
|
|
||||||
const match = result.match(/^data:(.*);base64/);
|
|
||||||
const mimeType = match && match[1] ? match[1] : "application/pdf";
|
|
||||||
resolve({
|
|
||||||
id: uuidv4(),
|
|
||||||
name: file.name,
|
|
||||||
block: {
|
|
||||||
type: "file",
|
|
||||||
source_type: "base64",
|
|
||||||
data: base64,
|
|
||||||
mime_type: mimeType,
|
|
||||||
metadata: { name: file.name },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
console.log(pdfFiles[0]);
|
setPdfUrlList((prev) => [...prev, ...pdfBlocks]);
|
||||||
setPdfUrlList([...pdfUrlList, ...pdfFiles]);
|
|
||||||
}
|
}
|
||||||
e.target.value = "";
|
e.target.value = "";
|
||||||
};
|
};
|
||||||
@@ -312,87 +275,29 @@ export function Thread() {
|
|||||||
|
|
||||||
const files = Array.from(e.dataTransfer.files);
|
const files = Array.from(e.dataTransfer.files);
|
||||||
const imageFiles = files.filter((file) => file.type.startsWith("image/"));
|
const imageFiles = files.filter((file) => file.type.startsWith("image/"));
|
||||||
|
const pdfFiles = files.filter((file) => file.type === "application/pdf");
|
||||||
|
const invalidFiles = files.filter(
|
||||||
|
(file) => !file.type.startsWith("image/") && file.type !== "application/pdf"
|
||||||
|
);
|
||||||
|
|
||||||
if (
|
if (invalidFiles.length > 0) {
|
||||||
files.some(
|
|
||||||
(file) =>
|
|
||||||
!file.type.startsWith("image/") || file.type !== "application/pdf",
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
toast.error(
|
toast.error(
|
||||||
"You have uploaded invalid file type. Please upload an image or a PDF.",
|
"You have uploaded invalid file type. Please upload an image or a PDF."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* If there are any image files in the dropped files, this block reads each image file as a data URL,
|
|
||||||
* wraps it in a MessageContentImageWrapper object, and updates the imageUrlList state with the new images.
|
|
||||||
* This enables preview and later sending of uploaded images in the chat UI.
|
|
||||||
*/
|
|
||||||
if (imageFiles.length) {
|
if (imageFiles.length) {
|
||||||
const imageFilesData: UploadedBlock[] = await Promise.all(
|
const imageBlocks: Base64ContentBlock[] = await Promise.all(
|
||||||
Array.from(imageFiles).map((file) => {
|
imageFiles.map(fileToImageBlock)
|
||||||
return new Promise<UploadedBlock>((resolve) => {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onloadend = () => {
|
|
||||||
const result = reader.result as string;
|
|
||||||
const base64 = result.split(",")[1];
|
|
||||||
const match = result.match(/^data:(.*);base64/);
|
|
||||||
const mimeType = match && match[1] ? match[1] : file.type;
|
|
||||||
resolve({
|
|
||||||
id: uuidv4(),
|
|
||||||
name: file.name,
|
|
||||||
block: {
|
|
||||||
type: "image",
|
|
||||||
source_type: "base64",
|
|
||||||
data: base64,
|
|
||||||
mime_type: mimeType,
|
|
||||||
metadata: { name: file.name },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
setImageUrlList([...imageUrlList, ...imageFilesData]);
|
setImageUrlList((prev) => [...prev, ...imageBlocks]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
if (pdfFiles.length) {
|
||||||
* If there are any PDF files in the dropped files, this block previews the file name of each uploaded PDF
|
const pdfBlocks: Base64ContentBlock[] = await Promise.all(
|
||||||
* by rendering a list of file names above the input area, with a remove button for each.
|
pdfFiles.map(fileToPDFBlock)
|
||||||
*/
|
|
||||||
if (files.some((file) => file.type === "application/pdf")) {
|
|
||||||
const pdfFiles = files.filter(
|
|
||||||
(file) => file.type === "application/pdf",
|
|
||||||
);
|
);
|
||||||
const pdfFilesData: UploadedBlock[] = await Promise.all(
|
setPdfUrlList((prev) => [...prev, ...pdfBlocks]);
|
||||||
pdfFiles.map((file) => {
|
|
||||||
return new Promise<UploadedBlock>((resolve) => {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onloadend = () => {
|
|
||||||
const result = reader.result as string;
|
|
||||||
const base64 = result.split(",")[1];
|
|
||||||
const match = result.match(/^data:(.*);base64/);
|
|
||||||
const mimeType =
|
|
||||||
match && match[1] ? match[1] : "application/pdf";
|
|
||||||
resolve({
|
|
||||||
id: uuidv4(),
|
|
||||||
name: file.name,
|
|
||||||
block: {
|
|
||||||
type: "file",
|
|
||||||
source_type: "base64",
|
|
||||||
data: base64,
|
|
||||||
mime_type: mimeType,
|
|
||||||
metadata: { name: file.name },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
setPdfUrlList([...pdfUrlList, ...pdfFilesData]);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -613,13 +518,10 @@ export function Thread() {
|
|||||||
>
|
>
|
||||||
{imageUrlList.length > 0 && (
|
{imageUrlList.length > 0 && (
|
||||||
<div className="flex flex-wrap gap-2 p-3.5 pb-0">
|
<div className="flex flex-wrap gap-2 p-3.5 pb-0">
|
||||||
{imageUrlList.map((imageItemWrapper) => {
|
{imageUrlList.map((imageBlock, idx) => {
|
||||||
const imageUrlString = `data:${imageItemWrapper.block.mime_type};base64,${imageItemWrapper.block.data}`;
|
const imageUrlString = `data:${imageBlock.mime_type};base64,${imageBlock.data}`;
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="relative" key={idx}>
|
||||||
className="relative"
|
|
||||||
key={imageItemWrapper.id}
|
|
||||||
>
|
|
||||||
<img
|
<img
|
||||||
src={imageUrlString}
|
src={imageUrlString}
|
||||||
alt="uploaded"
|
alt="uploaded"
|
||||||
@@ -628,11 +530,7 @@ export function Thread() {
|
|||||||
<CircleX
|
<CircleX
|
||||||
className="absolute top-[2px] right-[2px] size-4 cursor-pointer rounded-full bg-gray-500 text-white"
|
className="absolute top-[2px] right-[2px] size-4 cursor-pointer rounded-full bg-gray-500 text-white"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setImageUrlList(
|
setImageUrlList(imageUrlList.filter((_, i) => i !== idx))
|
||||||
imageUrlList.filter(
|
|
||||||
(url) => url.id !== imageItemWrapper.id,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -642,20 +540,18 @@ export function Thread() {
|
|||||||
)}
|
)}
|
||||||
{pdfUrlList.length > 0 && (
|
{pdfUrlList.length > 0 && (
|
||||||
<div className="flex flex-wrap gap-2 p-3.5 pb-0">
|
<div className="flex flex-wrap gap-2 p-3.5 pb-0">
|
||||||
{pdfUrlList.map((pdf) => (
|
{pdfUrlList.map((pdfBlock, idx) => (
|
||||||
<div
|
<div
|
||||||
className="relative flex items-center gap-2 rounded rounded-md border-1 border-teal-700 bg-gray-100 bg-teal-900 px-2 py-1 py-2 text-white"
|
className="relative flex items-center gap-2 rounded rounded-md border-1 border-teal-700 bg-gray-100 bg-teal-900 px-2 py-1 py-2 text-white"
|
||||||
key={pdf.id}
|
key={idx}
|
||||||
>
|
>
|
||||||
<span className="max-w-xs truncate text-sm">
|
<span className="max-w-xs truncate text-sm">
|
||||||
{pdf.name}
|
{String(pdfBlock.metadata?.filename ?? pdfBlock.metadata?.name ?? "")}
|
||||||
</span>
|
</span>
|
||||||
<CircleX
|
<CircleX
|
||||||
className="size-4 cursor-pointer text-teal-600 hover:text-teal-500"
|
className="size-4 cursor-pointer text-teal-600 hover:text-teal-500"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setPdfUrlList(
|
setPdfUrlList(pdfUrlList.filter((_, i) => i !== idx))
|
||||||
pdfUrlList.filter((p) => p.id !== pdf.id),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,11 +1,30 @@
|
|||||||
import type { Message } from "@langchain/langgraph-sdk";
|
import type { Message } from "@langchain/langgraph-sdk";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts a string summary from a message's content, supporting multimodal (text, image, file, etc.).
|
||||||
|
* - If text is present, returns the joined text.
|
||||||
|
* - If not, returns a label for the first non-text modality (e.g., 'Image', 'Other').
|
||||||
|
* - If unknown, returns 'Multimodal message'.
|
||||||
|
*/
|
||||||
export function getContentString(content: Message["content"]): string {
|
export function getContentString(content: Message["content"]): string {
|
||||||
if (typeof content === "string") return content;
|
if (typeof content === "string") return content;
|
||||||
const texts = content
|
const texts = content
|
||||||
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
||||||
.map((c) => c.text);
|
.map((c) => c.text);
|
||||||
return texts.join(" ");
|
if (texts.length > 0) return texts.join(" ");
|
||||||
|
// Handle multimodal: fallback to first non-text type
|
||||||
|
if (Array.isArray(content) && content.length > 0) {
|
||||||
|
const first = content[0];
|
||||||
|
if (typeof first === "object" && first !== null && "type" in first) {
|
||||||
|
switch (first.type) {
|
||||||
|
case "image_url":
|
||||||
|
return "Image";
|
||||||
|
default:
|
||||||
|
return "Other";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "Multimodal message";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getContentImageUrls(content: Message["content"]): string[] {
|
export function getContentImageUrls(content: Message["content"]): string[] {
|
||||||
|
|||||||
31
src/index.ts
Normal file
31
src/index.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import {
|
||||||
|
Annotation,
|
||||||
|
MessagesAnnotation,
|
||||||
|
START,
|
||||||
|
StateGraph,
|
||||||
|
} from "@langchain/langgraph";
|
||||||
|
import { ChatOpenAI } from "@langchain/openai";
|
||||||
|
|
||||||
|
const ChatAgentAnnotation = Annotation.Root({
|
||||||
|
messages: MessagesAnnotation.spec["messages"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const graph = new StateGraph(ChatAgentAnnotation)
|
||||||
|
.addNode("chat", async (state) => {
|
||||||
|
const model = new ChatOpenAI({
|
||||||
|
model: "gpt-4o-mini",
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await model.invoke([
|
||||||
|
{ role: "system", content: "You are a helpful assistant." },
|
||||||
|
...state.messages,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
messages: response,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.addEdge(START, "chat");
|
||||||
|
|
||||||
|
export const agent = graph.compile();
|
||||||
|
agent.name = "Chat Agent";
|
||||||
67
src/lib/multimodal-utils.ts
Normal file
67
src/lib/multimodal-utils.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import type { Base64ContentBlock } from "@langchain/core/messages";
|
||||||
|
import { convertToOpenAIImageBlock } from "@langchain/core/messages";
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
|
// Returns a Promise of a typed multimodal block for images
|
||||||
|
export async function fileToImageBlock(file: File): Promise<Base64ContentBlock> {
|
||||||
|
const data = await fileToBase64(file);
|
||||||
|
return {
|
||||||
|
type: "image",
|
||||||
|
source_type: "base64",
|
||||||
|
mime_type: file.type,
|
||||||
|
data,
|
||||||
|
metadata: { name: file.name },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a Promise of a typed multimodal block for PDFs
|
||||||
|
export async function fileToPDFBlock(file: File): Promise<Base64ContentBlock>{
|
||||||
|
const data = await fileToBase64(file);
|
||||||
|
return {
|
||||||
|
type: "file",
|
||||||
|
source_type: "base64",
|
||||||
|
mime_type: "application/pdf",
|
||||||
|
data,
|
||||||
|
metadata: { name: file.name, filename: file.name },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// in lib/multimodal-utils.ts
|
||||||
|
export function toOpenAIPDFBlock(block: Base64ContentBlock) {
|
||||||
|
return {
|
||||||
|
type: "file",
|
||||||
|
file: {
|
||||||
|
source_type: "base64",
|
||||||
|
data: block.data,
|
||||||
|
mime_type: block.mime_type ?? "application/pdf",
|
||||||
|
filename: block.metadata?.name ?? block.metadata?.filename ?? "file.pdf",
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to convert File to base64 string
|
||||||
|
export async function fileToBase64(file: File): Promise<string> {
|
||||||
|
return new Promise<string>((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onloadend = () => {
|
||||||
|
const result = reader.result as string;
|
||||||
|
// Remove the data:...;base64, prefix
|
||||||
|
resolve(result.split(",")[1]);
|
||||||
|
};
|
||||||
|
reader.onerror = reject;
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Utility to convert base64 image blocks to OpenAI image_url format
|
||||||
|
export function toOpenAIImageBlock(block: Base64ContentBlock | any) {
|
||||||
|
if (block.type === "image" && block.source_type === "base64") {
|
||||||
|
return convertToOpenAIImageBlock(block);
|
||||||
|
}
|
||||||
|
return block;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanBase64 = (base64String: string): string => {
|
||||||
|
return base64String.replace(/^data:.*?;base64,/, "");
|
||||||
|
};
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
"use server";
|
|
||||||
import { MessageContentText } from "@langchain/core/messages";
|
|
||||||
import { WebPDFLoader } from "@langchain/community/document_loaders/web/pdf";
|
|
||||||
// import { Base64ContentBlock } from "@langchain/core/messages";
|
|
||||||
|
|
||||||
// switch local import with above import
|
|
||||||
export interface Base64ContentBlock {
|
|
||||||
data: string;
|
|
||||||
metadata?: Record<string, unknown>;
|
|
||||||
mime_type?: string;
|
|
||||||
source_type: "base64";
|
|
||||||
type: "image" | "audio" | "file";
|
|
||||||
}
|
|
||||||
|
|
||||||
export const extractPdfText = async (
|
|
||||||
file: File,
|
|
||||||
): Promise<MessageContentText> => {
|
|
||||||
const loader = new WebPDFLoader(file, { splitPages: false });
|
|
||||||
const docs = await loader.load();
|
|
||||||
return {
|
|
||||||
type: "text",
|
|
||||||
text: docs[0].pageContent,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const cleanBase64 = (base64String: string): string => {
|
|
||||||
return base64String.replace(/^data:.*?;base64,/, "");
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user