Compare commits
10 Commits
595d4803e0
...
8b9382501e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b9382501e | ||
|
|
9234a380fc | ||
|
|
51bafbab98 | ||
|
|
93f848efad | ||
|
|
ef6454a157 | ||
|
|
8b4845494a | ||
|
|
be73c9424e | ||
|
|
3b01d7293b | ||
|
|
4472616d05 | ||
|
|
87bdf5be01 |
@@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import type { Base64ContentBlock } from "@langchain/core/messages";
|
import type { Base64ContentBlock } from "@langchain/core/messages";
|
||||||
import { MultimodalPreview } from "../ui/MultimodalPreview";
|
import { MultimodalPreview } from "./MultimodalPreview";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
interface ContentBlocksPreviewProps {
|
interface ContentBlocksPreviewProps {
|
||||||
|
|||||||
@@ -18,15 +18,6 @@ export const MultimodalPreview: React.FC<MultimodalPreviewProps> = ({
|
|||||||
className,
|
className,
|
||||||
size = "md",
|
size = "md",
|
||||||
}) => {
|
}) => {
|
||||||
// Sizing
|
|
||||||
const sizeMap = {
|
|
||||||
sm: "h-10 w-10 text-base",
|
|
||||||
md: "h-16 w-16 text-lg",
|
|
||||||
lg: "h-24 w-24 text-xl",
|
|
||||||
};
|
|
||||||
const iconSize: string =
|
|
||||||
typeof sizeMap[size] === "string" ? sizeMap[size] : sizeMap["md"];
|
|
||||||
|
|
||||||
// Image block
|
// Image block
|
||||||
if (
|
if (
|
||||||
block.type === "image" &&
|
block.type === "image" &&
|
||||||
@@ -72,28 +63,28 @@ export const MultimodalPreview: React.FC<MultimodalPreviewProps> = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex items-center gap-2 rounded-md border bg-gray-100 px-3 py-2",
|
"relative flex items-start gap-2 rounded-md border bg-gray-100 px-3 py-2",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<File
|
<div className="flex flex-shrink-0 flex-col items-start justify-start">
|
||||||
className={cn(
|
<File
|
||||||
"flex-shrink-0 text-teal-700",
|
className={cn(
|
||||||
size === "sm" ? "h-5 w-5" : "h-7 w-7",
|
"text-teal-700",
|
||||||
)}
|
size === "sm" ? "h-5 w-5" : "h-7 w-7",
|
||||||
/>
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn("min-w-0 flex-1 text-sm break-all text-gray-800")}
|
||||||
"truncate text-sm text-gray-800",
|
style={{ wordBreak: "break-all", whiteSpace: "pre-wrap" }}
|
||||||
size === "sm" ? "max-w-[80px]" : "max-w-[160px]",
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
{String(filename)}
|
{String(filename)}
|
||||||
</span>
|
</span>
|
||||||
{removable && (
|
{removable && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="ml-2 rounded-full bg-gray-200 p-1 text-teal-700 hover:bg-gray-300"
|
className="ml-2 self-start rounded-full bg-gray-200 p-1 text-teal-700 hover:bg-gray-300"
|
||||||
onClick={onRemove}
|
onClick={onRemove}
|
||||||
aria-label="Remove PDF"
|
aria-label="Remove PDF"
|
||||||
>
|
>
|
||||||
@@ -445,17 +445,12 @@ export function Thread() {
|
|||||||
<div
|
<div
|
||||||
ref={dropRef}
|
ref={dropRef}
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-muted relative z-10 mx-auto mb-8 w-full max-w-3xl rounded-2xl border shadow-xs transition-all",
|
"bg-muted relative z-10 mx-auto mb-8 w-full max-w-3xl rounded-2xl shadow-xs transition-all",
|
||||||
dragOver ? "border-primary border-2 border-dotted" : "",
|
dragOver
|
||||||
|
? "border-primary border-2 border-dotted"
|
||||||
|
: "border border-solid",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{dragOver && (
|
|
||||||
<div className="pointer-events-none absolute inset-0 z-20 flex items-center justify-center rounded-2xl bg-black/40">
|
|
||||||
<span className="text-lg font-semibold text-white">
|
|
||||||
Drop file here
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
className="mx-auto grid max-w-3xl grid-rows-[1fr_auto] gap-2"
|
className="mx-auto grid max-w-3xl grid-rows-[1fr_auto] gap-2"
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ function Interrupt({
|
|||||||
)}
|
)}
|
||||||
{interruptValue &&
|
{interruptValue &&
|
||||||
!isAgentInboxInterruptSchema(interruptValue) &&
|
!isAgentInboxInterruptSchema(interruptValue) &&
|
||||||
isLastMessage ? (
|
(isLastMessage || hasNoAIOrToolMessages) ? (
|
||||||
<GenericInterruptView interrupt={interruptValue} />
|
<GenericInterruptView interrupt={interruptValue} />
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -6,6 +6,39 @@ function isComplexValue(value: any): boolean {
|
|||||||
return Array.isArray(value) || (typeof value === "object" && value !== null);
|
return Array.isArray(value) || (typeof value === "object" && value !== null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isUrl(value: any): boolean {
|
||||||
|
if (typeof value !== "string") return false;
|
||||||
|
try {
|
||||||
|
new URL(value);
|
||||||
|
return value.startsWith("http://") || value.startsWith("https://");
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderInterruptStateItem(value: any): React.ReactNode {
|
||||||
|
if (isComplexValue(value)) {
|
||||||
|
return (
|
||||||
|
<code className="rounded bg-gray-50 px-2 py-1 font-mono text-sm">
|
||||||
|
{JSON.stringify(value, null, 2)}
|
||||||
|
</code>
|
||||||
|
);
|
||||||
|
} else if (isUrl(value)) {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href={value}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="break-all text-blue-600 underline hover:text-blue-800"
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function GenericInterruptView({
|
export function GenericInterruptView({
|
||||||
interrupt,
|
interrupt,
|
||||||
}: {
|
}: {
|
||||||
@@ -17,9 +50,13 @@ export function GenericInterruptView({
|
|||||||
const contentLines = contentStr.split("\n");
|
const contentLines = contentStr.split("\n");
|
||||||
const shouldTruncate = contentLines.length > 4 || contentStr.length > 500;
|
const shouldTruncate = contentLines.length > 4 || contentStr.length > 500;
|
||||||
|
|
||||||
// Function to truncate long string values
|
// Function to truncate long string values (but preserve URLs)
|
||||||
const truncateValue = (value: any): any => {
|
const truncateValue = (value: any): any => {
|
||||||
if (typeof value === "string" && value.length > 100) {
|
if (typeof value === "string" && value.length > 100) {
|
||||||
|
// Don't truncate URLs so they remain clickable
|
||||||
|
if (isUrl(value)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
return value.substring(0, 100) + "...";
|
return value.substring(0, 100) + "...";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,13 +132,7 @@ export function GenericInterruptView({
|
|||||||
{key}
|
{key}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-2 text-sm text-gray-500">
|
<td className="px-4 py-2 text-sm text-gray-500">
|
||||||
{isComplexValue(value) ? (
|
{renderInterruptStateItem(value)}
|
||||||
<code className="rounded bg-gray-50 px-2 py-1 font-mono text-sm">
|
|
||||||
{JSON.stringify(value, null, 2)}
|
|
||||||
</code>
|
|
||||||
) : (
|
|
||||||
String(value)
|
|
||||||
)}
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import { getContentString } from "../utils";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { BranchSwitcher, CommandBar } from "./shared";
|
import { BranchSwitcher, CommandBar } from "./shared";
|
||||||
import { MultimodalPreview } from "@/components/ui/MultimodalPreview";
|
import { MultimodalPreview } from "@/components/thread/MultimodalPreview";
|
||||||
import type { Base64ContentBlock } from "@langchain/core/messages";
|
import { isBase64ContentBlock } from "@/lib/multimodal-utils";
|
||||||
|
|
||||||
function EditableContent({
|
function EditableContent({
|
||||||
value,
|
value,
|
||||||
@@ -34,36 +34,6 @@ function EditableContent({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Type guard for Base64ContentBlock
|
|
||||||
function isBase64ContentBlock(block: unknown): block is Base64ContentBlock {
|
|
||||||
if (typeof block !== "object" || block === null || !("type" in block))
|
|
||||||
return false;
|
|
||||||
// file type (legacy)
|
|
||||||
if (
|
|
||||||
(block as { type: unknown }).type === "file" &&
|
|
||||||
"source_type" in block &&
|
|
||||||
(block as { source_type: unknown }).source_type === "base64" &&
|
|
||||||
"mime_type" in block &&
|
|
||||||
typeof (block as { mime_type?: unknown }).mime_type === "string" &&
|
|
||||||
((block as { mime_type: string }).mime_type.startsWith("image/") ||
|
|
||||||
(block as { mime_type: string }).mime_type === "application/pdf")
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// image type (new)
|
|
||||||
if (
|
|
||||||
(block as { type: unknown }).type === "image" &&
|
|
||||||
"source_type" in block &&
|
|
||||||
(block as { source_type: unknown }).source_type === "base64" &&
|
|
||||||
"mime_type" in block &&
|
|
||||||
typeof (block as { mime_type?: unknown }).mime_type === "string" &&
|
|
||||||
(block as { mime_type: string }).mime_type.startsWith("image/")
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function HumanMessage({
|
export function HumanMessage({
|
||||||
message,
|
message,
|
||||||
isLoading,
|
isLoading,
|
||||||
@@ -119,7 +89,7 @@ export function HumanMessage({
|
|||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
{/* Render images and files if no text */}
|
{/* Render images and files if no text */}
|
||||||
{Array.isArray(message.content) && message.content.length > 0 && (
|
{Array.isArray(message.content) && message.content.length > 0 && (
|
||||||
<div className="flex flex-col items-end gap-2">
|
<div className="flex flex-wrap items-end justify-end gap-2">
|
||||||
{message.content.reduce<React.ReactNode[]>(
|
{message.content.reduce<React.ReactNode[]>(
|
||||||
(acc, block, idx) => {
|
(acc, block, idx) => {
|
||||||
if (isBase64ContentBlock(block)) {
|
if (isBase64ContentBlock(block)) {
|
||||||
|
|||||||
@@ -86,41 +86,23 @@ export function useFileUpload({
|
|||||||
// Global drag events with counter for robust dragOver state
|
// Global drag events with counter for robust dragOver state
|
||||||
const handleWindowDragEnter = (e: DragEvent) => {
|
const handleWindowDragEnter = (e: DragEvent) => {
|
||||||
if (e.dataTransfer?.types?.includes("Files")) {
|
if (e.dataTransfer?.types?.includes("Files")) {
|
||||||
dragCounter.current++;
|
dragCounter.current += 1;
|
||||||
setDragOver(true);
|
setDragOver(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const handleWindowDragLeave = (e: DragEvent) => {
|
const handleWindowDragLeave = (e: DragEvent) => {
|
||||||
if (e.dataTransfer?.types?.includes("Files")) {
|
if (e.dataTransfer?.types?.includes("Files")) {
|
||||||
dragCounter.current--;
|
dragCounter.current -= 1;
|
||||||
if (dragCounter.current <= 0) {
|
if (dragCounter.current <= 0) {
|
||||||
setDragOver(false);
|
setDragOver(false);
|
||||||
dragCounter.current = 0;
|
dragCounter.current = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const handleWindowDrop = (e: DragEvent) => {
|
const handleWindowDrop = async (e: DragEvent) => {
|
||||||
dragCounter.current = 0;
|
|
||||||
setDragOver(false);
|
|
||||||
};
|
|
||||||
const handleWindowDragEnd = (e: DragEvent) => {
|
|
||||||
dragCounter.current = 0;
|
|
||||||
setDragOver(false);
|
|
||||||
};
|
|
||||||
window.addEventListener("dragenter", handleWindowDragEnter);
|
|
||||||
window.addEventListener("dragleave", handleWindowDragLeave);
|
|
||||||
window.addEventListener("drop", handleWindowDrop);
|
|
||||||
window.addEventListener("dragend", handleWindowDragEnd);
|
|
||||||
|
|
||||||
const handleDragOver = (e: DragEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
setDragOver(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDrop = async (e: DragEvent) => {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
dragCounter.current = 0;
|
||||||
setDragOver(false);
|
setDragOver(false);
|
||||||
|
|
||||||
if (!e.dataTransfer) return;
|
if (!e.dataTransfer) return;
|
||||||
@@ -155,34 +137,52 @@ export function useFileUpload({
|
|||||||
: [];
|
: [];
|
||||||
setContentBlocks((prev) => [...prev, ...newBlocks]);
|
setContentBlocks((prev) => [...prev, ...newBlocks]);
|
||||||
};
|
};
|
||||||
|
const handleWindowDragEnd = (e: DragEvent) => {
|
||||||
|
dragCounter.current = 0;
|
||||||
|
setDragOver(false);
|
||||||
|
};
|
||||||
|
window.addEventListener("dragenter", handleWindowDragEnter);
|
||||||
|
window.addEventListener("dragleave", handleWindowDragLeave);
|
||||||
|
window.addEventListener("drop", handleWindowDrop);
|
||||||
|
window.addEventListener("dragend", handleWindowDragEnd);
|
||||||
|
|
||||||
|
// Prevent default browser behavior for dragover globally
|
||||||
|
const handleWindowDragOver = (e: DragEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
};
|
||||||
|
window.addEventListener("dragover", handleWindowDragOver);
|
||||||
|
|
||||||
|
// Remove element-specific drop event (handled globally)
|
||||||
|
const handleDragOver = (e: DragEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setDragOver(true);
|
||||||
|
};
|
||||||
const handleDragEnter = (e: DragEvent) => {
|
const handleDragEnter = (e: DragEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setDragOver(true);
|
setDragOver(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDragLeave = (e: DragEvent) => {
|
const handleDragLeave = (e: DragEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setDragOver(false);
|
setDragOver(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const element = dropRef.current;
|
const element = dropRef.current;
|
||||||
element.addEventListener("dragover", handleDragOver);
|
element.addEventListener("dragover", handleDragOver);
|
||||||
element.addEventListener("drop", handleDrop);
|
|
||||||
element.addEventListener("dragenter", handleDragEnter);
|
element.addEventListener("dragenter", handleDragEnter);
|
||||||
element.addEventListener("dragleave", handleDragLeave);
|
element.addEventListener("dragleave", handleDragLeave);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
element.removeEventListener("dragover", handleDragOver);
|
element.removeEventListener("dragover", handleDragOver);
|
||||||
element.removeEventListener("drop", handleDrop);
|
|
||||||
element.removeEventListener("dragenter", handleDragEnter);
|
element.removeEventListener("dragenter", handleDragEnter);
|
||||||
element.removeEventListener("dragleave", handleDragLeave);
|
element.removeEventListener("dragleave", handleDragLeave);
|
||||||
window.removeEventListener("dragenter", handleWindowDragEnter);
|
window.removeEventListener("dragenter", handleWindowDragEnter);
|
||||||
window.removeEventListener("dragleave", handleWindowDragLeave);
|
window.removeEventListener("dragleave", handleWindowDragLeave);
|
||||||
window.removeEventListener("drop", handleWindowDrop);
|
window.removeEventListener("drop", handleWindowDrop);
|
||||||
window.removeEventListener("dragend", handleWindowDragEnd);
|
window.removeEventListener("dragend", handleWindowDragEnd);
|
||||||
|
window.removeEventListener("dragover", handleWindowDragOver);
|
||||||
dragCounter.current = 0;
|
dragCounter.current = 0;
|
||||||
};
|
};
|
||||||
}, [contentBlocks]);
|
}, [contentBlocks]);
|
||||||
@@ -203,14 +203,17 @@ export function useFileUpload({
|
|||||||
const items = e.clipboardData.items;
|
const items = e.clipboardData.items;
|
||||||
if (!items) return;
|
if (!items) return;
|
||||||
const files: File[] = [];
|
const files: File[] = [];
|
||||||
for (let i = 0; i < items.length; i++) {
|
for (let i = 0; i < items.length; i += 1) {
|
||||||
const item = items[i];
|
const item = items[i];
|
||||||
if (item.kind === "file") {
|
if (item.kind === "file") {
|
||||||
const file = item.getAsFile();
|
const file = item.getAsFile();
|
||||||
if (file) files.push(file);
|
if (file) files.push(file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (files.length === 0) return;
|
if (files.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
const validFiles = files.filter((file) =>
|
const validFiles = files.filter((file) =>
|
||||||
SUPPORTED_FILE_TYPES.includes(file.type),
|
SUPPORTED_FILE_TYPES.includes(file.type),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -55,3 +55,35 @@ export async function fileToBase64(file: File): Promise<string> {
|
|||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Type guard for Base64ContentBlock
|
||||||
|
export function isBase64ContentBlock(
|
||||||
|
block: unknown,
|
||||||
|
): block is Base64ContentBlock {
|
||||||
|
if (typeof block !== "object" || block === null || !("type" in block))
|
||||||
|
return false;
|
||||||
|
// file type (legacy)
|
||||||
|
if (
|
||||||
|
(block as { type: unknown }).type === "file" &&
|
||||||
|
"source_type" in block &&
|
||||||
|
(block as { source_type: unknown }).source_type === "base64" &&
|
||||||
|
"mime_type" in block &&
|
||||||
|
typeof (block as { mime_type?: unknown }).mime_type === "string" &&
|
||||||
|
((block as { mime_type: string }).mime_type.startsWith("image/") ||
|
||||||
|
(block as { mime_type: string }).mime_type === "application/pdf")
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// image type (new)
|
||||||
|
if (
|
||||||
|
(block as { type: unknown }).type === "image" &&
|
||||||
|
"source_type" in block &&
|
||||||
|
(block as { source_type: unknown }).source_type === "base64" &&
|
||||||
|
"mime_type" in block &&
|
||||||
|
typeof (block as { mime_type?: unknown }).mime_type === "string" &&
|
||||||
|
(block as { mime_type: string }).mime_type.startsWith("image/")
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user