mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-28 02:23:53 +02:00
Merge remote-tracking branch 'upstream/dev' into feat/unified-etl-pipeline
This commit is contained in:
commit
63a75052ca
76 changed files with 3041 additions and 376 deletions
|
|
@ -34,9 +34,12 @@ export const CirclebackConfig: FC<CirclebackConfigProps> = ({ connector, onNameC
|
|||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
// Fetch webhook info
|
||||
// Fetch webhook info
|
||||
useEffect(() => {
|
||||
const fetchWebhookInfo = async () => {
|
||||
const controller = new AbortController();
|
||||
|
||||
const doFetch = async () => {
|
||||
if (!connector.search_space_id) return;
|
||||
|
||||
const baseUrl = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL;
|
||||
|
|
@ -49,8 +52,11 @@ export const CirclebackConfig: FC<CirclebackConfigProps> = ({ connector, onNameC
|
|||
setIsLoading(true);
|
||||
try {
|
||||
const response = await authenticatedFetch(
|
||||
`${baseUrl}/api/v1/webhooks/circleback/${connector.search_space_id}/info`
|
||||
`${baseUrl}/api/v1/webhooks/circleback/${connector.search_space_id}/info`,
|
||||
{ signal: controller.signal }
|
||||
);
|
||||
if (controller.signal.aborted) return;
|
||||
|
||||
if (response.ok) {
|
||||
const data: unknown = await response.json();
|
||||
// Runtime validation with zod schema
|
||||
|
|
@ -59,16 +65,18 @@ export const CirclebackConfig: FC<CirclebackConfigProps> = ({ connector, onNameC
|
|||
setWebhookUrl(validatedData.webhook_url);
|
||||
}
|
||||
} catch (error) {
|
||||
if (controller.signal.aborted) return;
|
||||
console.error("Failed to fetch webhook info:", error);
|
||||
// Reset state on error
|
||||
setWebhookInfo(null);
|
||||
setWebhookUrl("");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
if (!controller.signal.aborted) setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchWebhookInfo();
|
||||
doFetch().catch(() => {});
|
||||
return () => controller.abort();
|
||||
}, [connector.search_space_id]);
|
||||
|
||||
const handleNameChange = (value: string) => {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { ImageIcon, ImageOffIcon } from "lucide-react";
|
|||
import { memo, type PropsWithChildren, useEffect, useRef, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { cn } from "@/lib/utils";
|
||||
import NextImage from 'next/image';
|
||||
|
||||
const imageVariants = cva("aui-image-root relative overflow-hidden rounded-lg", {
|
||||
variants: {
|
||||
|
|
@ -86,23 +87,57 @@ function ImagePreview({
|
|||
>
|
||||
<ImageOffIcon className="size-8 text-muted-foreground" />
|
||||
</div>
|
||||
) : (
|
||||
) : isDataOrBlobUrl(src) ? (
|
||||
// biome-ignore lint/performance/noImgElement: data/blob URLs need plain img
|
||||
<img
|
||||
ref={imgRef}
|
||||
src={src}
|
||||
alt={alt}
|
||||
className={cn("block h-auto w-full object-contain", !loaded && "invisible", className)}
|
||||
onLoad={(e) => {
|
||||
if (typeof src === "string") setLoadedSrc(src);
|
||||
onLoad?.(e);
|
||||
}}
|
||||
onError={(e) => {
|
||||
if (typeof src === "string") setErrorSrc(src);
|
||||
onError?.(e);
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
) : (
|
||||
// biome-ignore lint/performance/noImgElement: intentional for dynamic external URLs
|
||||
<img
|
||||
ref={imgRef}
|
||||
src={src}
|
||||
alt={alt}
|
||||
className={cn("block h-auto w-full object-contain", !loaded && "invisible", className)}
|
||||
onLoad={(e) => {
|
||||
if (typeof src === "string") setLoadedSrc(src);
|
||||
onLoad?.(e);
|
||||
}}
|
||||
onError={(e) => {
|
||||
if (typeof src === "string") setErrorSrc(src);
|
||||
onError?.(e);
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
// <img
|
||||
// ref={imgRef}
|
||||
// src={src}
|
||||
// alt={alt}
|
||||
// className={cn("block h-auto w-full object-contain", !loaded && "invisible", className)}
|
||||
// onLoad={(e) => {
|
||||
// if (typeof src === "string") setLoadedSrc(src);
|
||||
// onLoad?.(e);
|
||||
// }}
|
||||
// onError={(e) => {
|
||||
// if (typeof src === "string") setErrorSrc(src);
|
||||
// onError?.(e);
|
||||
// }}
|
||||
// {...props}
|
||||
// />
|
||||
<NextImage
|
||||
fill
|
||||
src={src || ""}
|
||||
alt={alt}
|
||||
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 60vw"
|
||||
className={cn("block object-contain", !loaded && "invisible", className)}
|
||||
onLoad={() => {
|
||||
if (typeof src === "string") setLoadedSrc(src);
|
||||
onLoad?.();
|
||||
}}
|
||||
onError={() => {
|
||||
if (typeof src === "string") setErrorSrc(src);
|
||||
onError?.();
|
||||
}}
|
||||
unoptimized={false}
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
@ -126,7 +161,10 @@ type ImageZoomProps = PropsWithChildren<{
|
|||
src: string;
|
||||
alt?: string;
|
||||
}>;
|
||||
|
||||
function isDataOrBlobUrl(src: string | undefined): boolean {
|
||||
if (!src || typeof src !== "string") return false;
|
||||
return src.startsWith("data:") || src.startsWith("blob:");
|
||||
}
|
||||
function ImageZoom({ src, alt = "Image preview", children }: ImageZoomProps) {
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
|
@ -177,22 +215,39 @@ function ImageZoom({ src, alt = "Image preview", children }: ImageZoomProps) {
|
|||
aria-label="Close zoomed image"
|
||||
>
|
||||
{/** biome-ignore lint/performance/noImgElement: <explanation> */}
|
||||
<img
|
||||
data-slot="image-zoom-content"
|
||||
src={src}
|
||||
alt={alt}
|
||||
className="aui-image-zoom-content fade-in zoom-in-95 max-h-[90vh] max-w-[90vw] animate-in object-contain duration-200"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleClose();
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.stopPropagation();
|
||||
handleClose();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{isDataOrBlobUrl(src) ? (
|
||||
// biome-ignore lint/performance/noImgElement: data/blob URLs need plain img
|
||||
<img
|
||||
data-slot="image-zoom-content"
|
||||
src={src}
|
||||
alt={alt}
|
||||
className="aui-image-zoom-content fade-in zoom-in-95 max-h-[90vh] max-w-[90vw] animate-in object-contain duration-200"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleClose();
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.stopPropagation();
|
||||
handleClose();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<NextImage
|
||||
data-slot="image-zoom-content"
|
||||
fill
|
||||
src={src}
|
||||
alt={alt}
|
||||
sizes="90vw"
|
||||
className="aui-image-zoom-content fade-in zoom-in-95 object-contain duration-200"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleClose();
|
||||
}}
|
||||
unoptimized={false}
|
||||
/>
|
||||
)}
|
||||
</button>,
|
||||
document.body
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import {
|
|||
TrashIcon,
|
||||
} from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { memo, useCallback, useEffect, useState } from "react";
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
|
|
@ -224,6 +224,11 @@ const ThreadListItemComponent = memo(function ThreadListItemComponent({
|
|||
onUnarchive,
|
||||
onDelete,
|
||||
}: ThreadListItemComponentProps) {
|
||||
const relativeTime = useMemo(
|
||||
() => formatRelativeTime(new Date(thread.updatedAt)),
|
||||
[thread.updatedAt]
|
||||
);
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -237,7 +242,7 @@ const ThreadListItemComponent = memo(function ThreadListItemComponent({
|
|||
<div className="flex-1 min-w-0">
|
||||
<p className="truncate text-sm font-medium">{thread.title || "New Chat"}</p>
|
||||
<p className="truncate text-xs text-muted-foreground">
|
||||
{formatRelativeTime(new Date(thread.updatedAt))}
|
||||
{relativeTime}
|
||||
</p>
|
||||
</div>
|
||||
<DropdownMenu>
|
||||
|
|
|
|||
|
|
@ -817,12 +817,23 @@ const ComposerAction: FC<ComposerActionProps> = ({ isBlockedByOtherUser = false
|
|||
const isDesktop = useMediaQuery("(min-width: 640px)");
|
||||
const { openDialog: openUploadDialog } = useDocumentUploadDialog();
|
||||
const [toolsScrollPos, setToolsScrollPos] = useState<"top" | "middle" | "bottom">("top");
|
||||
const toolsRafRef = useRef<number>();
|
||||
const handleToolsScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => {
|
||||
const el = e.currentTarget;
|
||||
const atTop = el.scrollTop <= 2;
|
||||
const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight <= 2;
|
||||
setToolsScrollPos(atTop ? "top" : atBottom ? "bottom" : "middle");
|
||||
if (toolsRafRef.current) return;
|
||||
toolsRafRef.current = requestAnimationFrame(() => {
|
||||
const atTop = el.scrollTop <= 2;
|
||||
const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight <= 2;
|
||||
setToolsScrollPos(atTop ? "top" : atBottom ? "bottom" : "middle");
|
||||
toolsRafRef.current = undefined;
|
||||
});
|
||||
}, []);
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (toolsRafRef.current) cancelAnimationFrame(toolsRafRef.current);
|
||||
},
|
||||
[]
|
||||
);
|
||||
const isComposerTextEmpty = useAuiState(({ composer }) => {
|
||||
const text = composer.text?.trim() || "";
|
||||
return text.length === 0;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { ToolCallMessagePartComponent } from "@assistant-ui/react";
|
||||
import { CheckIcon, ChevronDownIcon, ChevronUpIcon, XCircleIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { getToolIcon } from "@/contracts/enums/toolIcons";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
|
|
@ -19,17 +19,28 @@ export const ToolFallback: ToolCallMessagePartComponent = ({
|
|||
const isCancelled = status?.type === "incomplete" && status.reason === "cancelled";
|
||||
const isError = status?.type === "incomplete" && status.reason === "error";
|
||||
const isRunning = status?.type === "running" || status?.type === "requires-action";
|
||||
const errorData = status?.type === "incomplete" ? status.error : undefined;
|
||||
const serializedError = useMemo(
|
||||
() => (errorData && typeof errorData !== "string" ? JSON.stringify(errorData) : null),
|
||||
[errorData]
|
||||
);
|
||||
|
||||
const serializedResult = useMemo(
|
||||
() => (result !== undefined && typeof result !== "string" ? JSON.stringify(result, null, 2) : null),
|
||||
[result]
|
||||
);
|
||||
|
||||
const cancelledReason =
|
||||
isCancelled && status.error
|
||||
? typeof status.error === "string"
|
||||
? status.error
|
||||
: JSON.stringify(status.error)
|
||||
: serializedError
|
||||
: null;
|
||||
const errorReason =
|
||||
isError && status.error
|
||||
? typeof status.error === "string"
|
||||
? status.error
|
||||
: JSON.stringify(status.error)
|
||||
: serializedError
|
||||
: null;
|
||||
|
||||
const Icon = getToolIcon(toolName);
|
||||
|
|
@ -122,7 +133,7 @@ export const ToolFallback: ToolCallMessagePartComponent = ({
|
|||
<div>
|
||||
<p className="text-xs font-medium text-muted-foreground mb-1">Result</p>
|
||||
<pre className="text-xs text-foreground/80 whitespace-pre-wrap break-all">
|
||||
{typeof result === "string" ? result : JSON.stringify(result, null, 2)}
|
||||
{typeof result === "string" ? result : serializedResult}
|
||||
</pre>
|
||||
</div>
|
||||
</>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue