Merge remote-tracking branch 'upstream/dev' into feat/whatsapp-gateway-integration

This commit is contained in:
Anish Sarkar 2026-06-02 00:29:32 +05:30
commit e3de7c4667
465 changed files with 29171 additions and 6994 deletions

View file

@ -13,8 +13,8 @@ import {
CheckIcon,
ClipboardPaste,
CopyIcon,
DownloadIcon,
Dot,
DownloadIcon,
ExternalLink,
Globe,
MessageCircleReply,

View file

@ -6,14 +6,13 @@ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { EnumConnectorName } from "@/contracts/enums/connector";
import { useApiKey } from "@/hooks/use-api-key";
import { BACKEND_URL } from "@/lib/env-config";
import { getConnectorBenefits } from "../connector-benefits";
import type { ConnectFormProps } from "../index";
import { BACKEND_URL } from "@/lib/env-config";
const PLUGIN_RELEASES_URL =
"https://github.com/MODSetter/SurfSense/releases?q=obsidian&expanded=true";
/**
* Obsidian connect form for the plugin-only architecture.
*

View file

@ -9,8 +9,8 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { authenticatedFetch } from "@/lib/auth-utils";
import type { ConnectorConfigProps } from "../index";
import { BACKEND_URL } from "@/lib/env-config";
import type { ConnectorConfigProps } from "../index";
export interface CirclebackConfigProps extends ConnectorConfigProps {
onNameChange?: (name: string) => void;
}

View file

@ -23,6 +23,7 @@ import { LIVE_CONNECTOR_TYPES } from "../../constants/connector-constants";
import { getConnectorDisplayName } from "../../tabs/all-connectors-tab";
import { MCPServiceConfig } from "../components/mcp-service-config";
import { getConnectorConfigComponent } from "../index";
const VISION_LLM_CONNECTOR_TYPES = new Set<SearchSourceConnector["connector_type"]>([
EnumConnectorName.GOOGLE_DRIVE_CONNECTOR,
EnumConnectorName.COMPOSIO_GOOGLE_DRIVE_CONNECTOR,

View file

@ -18,6 +18,7 @@ import { cn } from "@/lib/utils";
import { LIVE_CONNECTOR_TYPES } from "../constants/connector-constants";
import { useConnectorStatus } from "../hooks/use-connector-status";
import { getConnectorDisplayName } from "../tabs/all-connectors-tab";
interface ConnectorAccountsListViewProps {
connectorType: string;
connectorTitle: string;

View file

@ -1,16 +1,13 @@
"use client";
import { useQuery } from "@tanstack/react-query";
import { useSetAtom } from "jotai";
import { ExternalLink, FileText } from "lucide-react";
import dynamic from "next/dynamic";
import { FileText } from "lucide-react";
import type { FC } from "react";
import { useState } from "react";
import { openCitationPanelAtom } from "@/atoms/citation/citation-panel.atom";
import { useCitationMetadata } from "@/components/assistant-ui/citation-metadata-context";
import { CitationPanelContent } from "@/components/citation-panel/citation-panel";
import { Citation } from "@/components/tool-ui/citation";
import { CitationHoverPopover } from "@/components/tool-ui/citation/citation-hover-popover";
import { Button } from "@/components/ui/button";
import {
Drawer,
@ -19,21 +16,8 @@ import {
DrawerHeader,
DrawerTitle,
} from "@/components/ui/drawer";
import { Spinner } from "@/components/ui/spinner";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { useMediaQuery } from "@/hooks/use-media-query";
import { documentsApiService } from "@/lib/apis/documents-api.service";
import { cacheKeys } from "@/lib/query-client/cache-keys";
// Lazily load MarkdownViewer here to break the static import cycle:
// `markdown-viewer.tsx` → `citation-renderer.tsx` → `inline-citation.tsx`
// would otherwise pull `markdown-viewer.tsx` back in at module-init time.
// Only `SurfsenseDocCitation` (popover body) ever renders this viewer, so
// the lazy boundary is invisible to most call paths.
const MarkdownViewer = dynamic(
() => import("@/components/markdown-viewer").then((m) => m.MarkdownViewer),
{ ssr: false, loading: () => <Spinner size="xs" /> }
);
interface InlineCitationProps {
chunkId: number;
@ -41,9 +25,7 @@ interface InlineCitationProps {
}
/**
* Inline citation badge for knowledge-base chunks (numeric chunk IDs) and
* Surfsense documentation chunks (`isDocsChunk`). Negative chunk IDs render as
* a static "doc" pill (anonymous/synthetic uploads).
* Inline citation badge for knowledge-base chunks (numeric chunk IDs).
*
* Numeric KB chunks: clicking opens the citation panel in the right
* sidebar (alongside the chat does not replace it). The panel shows
@ -51,12 +33,13 @@ interface InlineCitationProps {
* `chunk_window`), with the cited one highlighted and an option to
* expand the window or jump into the full document via the editor panel.
*
* Surfsense docs chunks: rendered as a hover-controlled shadcn Popover that
* lazily fetches and previews the cited chunk inline, since those docs aren't
* indexed into the user's search space and have no tab to open.
* Negative chunk IDs and legacy SurfSense-docs chunks (`isDocsChunk`) render
* as a static, non-interactive "doc" pill. The SurfSense product-docs feature
* was removed, so those markers are inert (no fetch, no preview) they only
* survive in old persisted messages.
*/
export const InlineCitation: FC<InlineCitationProps> = ({ chunkId, isDocsChunk = false }) => {
if (chunkId < 0) {
if (chunkId < 0 || isDocsChunk) {
return (
<Tooltip>
<TooltipTrigger asChild>
@ -68,15 +51,13 @@ export const InlineCitation: FC<InlineCitationProps> = ({ chunkId, isDocsChunk =
doc
</span>
</TooltipTrigger>
<TooltipContent>Uploaded document</TooltipContent>
<TooltipContent>
{isDocsChunk ? "Documentation reference" : "Uploaded document"}
</TooltipContent>
</Tooltip>
);
}
if (isDocsChunk) {
return <SurfsenseDocCitation chunkId={chunkId} />;
}
return <NumericChunkCitation chunkId={chunkId} />;
};
@ -127,128 +108,6 @@ const NumericChunkCitation: FC<{ chunkId: number }> = ({ chunkId }) => {
);
};
const SurfsenseDocCitation: FC<{ chunkId: number }> = ({ chunkId }) => {
const isTouchLike = useMediaQuery("(hover: none), (pointer: coarse)");
const [mobilePreviewOpen, setMobilePreviewOpen] = useState(false);
const docQuery = useSurfsenseDocPreviewQuery(chunkId, mobilePreviewOpen);
const handleMobileClick = () => {
setMobilePreviewOpen(true);
};
return (
<>
<CitationHoverPopover
id={`doc-${chunkId}`}
contentClassName="w-96 max-w-[calc(100vw-2rem)] p-0"
align="start"
trigger={(hoverProps) => (
<Button
type="button"
variant="ghost"
size={null}
onClick={isTouchLike ? handleMobileClick : undefined}
className="ml-0.5 inline-flex h-5 min-w-5 items-center justify-center gap-0.5 rounded-md bg-popover px-1.5 text-[11px] font-medium text-popover-foreground/80 align-baseline"
aria-label={`Show Surfsense documentation chunk ${chunkId}`}
title="Surfsense documentation"
{...hoverProps}
>
<FileText className="size-3" />
doc
</Button>
)}
>
<SurfsenseDocPreview chunkId={chunkId} />
</CitationHoverPopover>
<Drawer
open={mobilePreviewOpen}
onOpenChange={setMobilePreviewOpen}
shouldScaleBackground={false}
>
<DrawerContent className="max-h-[85vh] z-80" overlayClassName="z-80">
<DrawerHandle />
<DrawerHeader className="pb-0">
<DrawerTitle>Surfsense documentation</DrawerTitle>
</DrawerHeader>
<SurfsenseDocPreviewContent
chunkId={chunkId}
query={docQuery}
contentClassName="max-h-[60vh]"
/>
</DrawerContent>
</Drawer>
</>
);
};
function useSurfsenseDocPreviewQuery(chunkId: number, enabled = true) {
return useQuery({
queryKey: cacheKeys.documents.byChunk(`doc-${chunkId}`),
queryFn: () => documentsApiService.getSurfsenseDocByChunk(chunkId),
staleTime: 5 * 60 * 1000,
enabled,
});
}
type SurfsenseDocPreviewQuery = ReturnType<typeof useSurfsenseDocPreviewQuery>;
const SurfsenseDocPreview: FC<{ chunkId: number }> = ({ chunkId }) => {
const query = useSurfsenseDocPreviewQuery(chunkId);
return <SurfsenseDocPreviewContent chunkId={chunkId} query={query} />;
};
const SurfsenseDocPreviewContent: FC<{
chunkId: number;
query: SurfsenseDocPreviewQuery;
contentClassName?: string;
}> = ({ chunkId, query, contentClassName = "max-h-72" }) => {
const { data, isLoading, error } = query;
const citedChunk = data?.chunks.find((c) => c.id === chunkId) ?? data?.chunks[0];
return (
<>
<div className="flex items-center justify-between gap-2 border-b px-3 py-2">
<div className="min-w-0">
<p className="truncate text-sm font-medium">{data?.title ?? "Surfsense documentation"}</p>
<p className="text-[11px] text-muted-foreground">Chunk #{chunkId}</p>
</div>
{data?.public_url && (
<a
href={data.public_url}
target="_blank"
rel="noopener noreferrer"
className="inline-flex shrink-0 items-center gap-1 rounded-md px-2 py-1 text-[11px] font-medium text-primary hover:bg-primary/10"
>
<ExternalLink className="size-3" />
Open
</a>
)}
</div>
<div className={`${contentClassName} overflow-auto px-3 py-2 text-sm`}>
{isLoading && (
<div className="flex items-center gap-2 py-4 text-muted-foreground">
<Spinner size="xs" />
<span className="text-xs">Loading</span>
</div>
)}
{error && (
<p className="py-4 text-xs text-destructive">
{error instanceof Error ? error.message : "Failed to load chunk"}
</p>
)}
{!isLoading && !error && citedChunk?.content && (
<MarkdownViewer content={citedChunk.content} maxLength={1500} enableCitations />
)}
{!isLoading && !error && !citedChunk?.content && (
<p className="py-4 text-xs text-muted-foreground">No content available.</p>
)}
</div>
</>
);
};
import { tryGetHostname } from "@/lib/url";
interface UrlCitationProps {

View file

@ -97,7 +97,12 @@ interface InlineMentionEditorProps {
onActionClose?: () => void;
onSubmit?: () => void;
onChange?: (text: string, docs: MentionedDocument[]) => void;
onDocumentRemove?: (docId: number, docType?: string, kind?: MentionKind, connectorType?: string) => void;
onDocumentRemove?: (
docId: number,
docType?: string,
kind?: MentionKind,
connectorType?: string
) => void;
onKeyDown?: (e: React.KeyboardEvent) => void;
disabled?: boolean;
className?: string;
@ -171,9 +176,10 @@ const MentionElement: FC<PlateElementProps<MentionElementNode>> = ({
{isFolder ? (
<FolderIcon className="h-3 w-3" />
) : isConnector ? (
getConnectorIcon(element.connector_type ?? element.document_type ?? "UNKNOWN", "h-3 w-3") ?? (
<PlugIcon className="h-3 w-3" />
)
(getConnectorIcon(
element.connector_type ?? element.document_type ?? "UNKNOWN",
"h-3 w-3"
) ?? <PlugIcon className="h-3 w-3" />)
) : (
getConnectorIcon(element.document_type ?? "UNKNOWN", "h-3 w-3")
)}
@ -357,7 +363,11 @@ function getSelectionAnchorRect(root: HTMLElement | null): SuggestionAnchorRect
const rect = range.getClientRects()[0] ?? range.getBoundingClientRect();
if (rect.width > 0 || rect.height > 0) return rectToAnchor(rect);
if (range.collapsed && range.startContainer.nodeType === Node.TEXT_NODE && range.startOffset > 0) {
if (
range.collapsed &&
range.startContainer.nodeType === Node.TEXT_NODE &&
range.startOffset > 0
) {
const fallbackRange = range.cloneRange();
fallbackRange.setStart(range.startContainer, range.startOffset - 1);
fallbackRange.setEnd(range.startContainer, range.startOffset);

View file

@ -67,12 +67,8 @@ import {
} from "@/components/assistant-ui/inline-mention-editor";
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
import { UserMessage } from "@/components/assistant-ui/user-message";
import { ChatExamplePrompts } from "@/components/new-chat/chat-example-prompts";
import { ComposerSuggestionPopoverContent } from "@/components/new-chat/composer-suggestion-popup";
import {
DocumentMentionPicker,
promoteRecentMention,
type DocumentMentionPickerRef,
} from "../new-chat/document-mention-picker";
import { PromptPicker, type PromptPickerRef } from "@/components/new-chat/prompt-picker";
import { Avatar, AvatarFallback, AvatarGroup } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
@ -112,6 +108,11 @@ import { captureDisplayToPngDataUrl } from "@/lib/chat/display-media-capture";
import { getMentionDocKey } from "@/lib/chat/mention-doc-key";
import { slideoutOpenedTickAtom } from "@/lib/layout-events";
import { cn } from "@/lib/utils";
import {
DocumentMentionPicker,
type DocumentMentionPickerRef,
promoteRecentMention,
} from "../new-chat/document-mention-picker";
const COMPOSER_PLACEHOLDER = "Ask anything, type / for prompts, type @ to mention docs";
@ -601,21 +602,24 @@ const Composer: FC = () => {
}
}, []);
const handleActionTrigger = useCallback((trigger: SuggestionTriggerInfo) => {
const anchorPoint = getComposerSuggestionAnchorPoint(
trigger.anchorRect,
clipboardInitialText ? "bottom" : "top"
);
if (!anchorPoint) {
setShowPromptPicker(false);
setActionQuery("");
setSuggestionAnchorPoint(null);
return;
}
setSuggestionAnchorPoint((current) => current ?? anchorPoint);
setShowPromptPicker(true);
setActionQuery(trigger.query);
}, [clipboardInitialText]);
const handleActionTrigger = useCallback(
(trigger: SuggestionTriggerInfo) => {
const anchorPoint = getComposerSuggestionAnchorPoint(
trigger.anchorRect,
clipboardInitialText ? "bottom" : "top"
);
if (!anchorPoint) {
setShowPromptPicker(false);
setActionQuery("");
setSuggestionAnchorPoint(null);
return;
}
setSuggestionAnchorPoint((current) => current ?? anchorPoint);
setShowPromptPicker(true);
setActionQuery(trigger.query);
},
[clipboardInitialText]
);
const handleActionClose = useCallback(() => {
if (showPromptPicker) {
@ -654,6 +658,15 @@ const Composer: FC = () => {
[actionQuery, aui]
);
const handleExampleSelect = useCallback(
(prompt: string) => {
editorRef.current?.setText(prompt);
aui.composer().setText(prompt);
editorRef.current?.focus();
},
[aui]
);
const handleQuickAskSelect = useCallback(
(action: { name: string; prompt: string; mode: "transform" | "explore" }) => {
if (!clipboardInitialText) return;
@ -754,7 +767,12 @@ const Composer: FC = () => {
]);
const handleDocumentRemove = useCallback(
(docId: number, docType?: string, kind?: "doc" | "folder" | "connector", connectorType?: string) => {
(
docId: number,
docType?: string,
kind?: "doc" | "folder" | "connector",
connectorType?: string
) => {
setMentionedDocuments((prev) => {
const removedKey = getMentionDocKey({
id: docId,
@ -768,27 +786,30 @@ const Composer: FC = () => {
[setMentionedDocuments]
);
const handleDocumentsMention = useCallback((mentions: MentionedDocumentInfo[]) => {
const parsedSearchSpaceId = Number(search_space_id);
const editorMentionedDocs = editorRef.current?.getMentionedDocuments() ?? [];
const editorDocKeys = new Set(editorMentionedDocs.map((doc) => getMentionDocKey(doc)));
const handleDocumentsMention = useCallback(
(mentions: MentionedDocumentInfo[]) => {
const parsedSearchSpaceId = Number(search_space_id);
const editorMentionedDocs = editorRef.current?.getMentionedDocuments() ?? [];
const editorDocKeys = new Set(editorMentionedDocs.map((doc) => getMentionDocKey(doc)));
for (const mention of mentions) {
const key = getMentionDocKey(mention);
if (editorDocKeys.has(key)) continue;
editorRef.current?.insertMentionChip(mention);
if (Number.isFinite(parsedSearchSpaceId)) {
promoteRecentMention(parsedSearchSpaceId, mention);
for (const mention of mentions) {
const key = getMentionDocKey(mention);
if (editorDocKeys.has(key)) continue;
editorRef.current?.insertMentionChip(mention);
if (Number.isFinite(parsedSearchSpaceId)) {
promoteRecentMention(parsedSearchSpaceId, mention);
}
// Track within the loop so a duplicate-in-batch can't double-insert.
editorDocKeys.add(key);
}
// Track within the loop so a duplicate-in-batch can't double-insert.
editorDocKeys.add(key);
}
// Atom is reconciled by ``handleEditorChange`` via the editor's
// onChange — no second write path here.
setMentionQuery("");
setSuggestionAnchorPoint(null);
}, [search_space_id]);
// Atom is reconciled by ``handleEditorChange`` via the editor's
// onChange — no second write path here.
setMentionQuery("");
setSuggestionAnchorPoint(null);
},
[search_space_id]
);
useEffect(() => {
const editor = editorRef.current;
@ -905,6 +926,7 @@ const Composer: FC = () => {
isThreadEmpty={isThreadEmpty}
onVisibleChange={setConnectToolsTrayVisible}
/>
{isThreadEmpty && <ChatExamplePrompts onSelect={handleExampleSelect} />}
</div>
</ComposerPrimitive.Root>
);
@ -1582,7 +1604,7 @@ interface ToolGroup {
const TOOL_GROUPS: ToolGroup[] = [
{
label: "Research",
tools: ["search_surfsense_docs", "scrape_webpage"],
tools: ["scrape_webpage"],
},
{
label: "Generate",

View file

@ -104,9 +104,9 @@ const UserTextPart: FC = () => {
const icon = isFolder ? (
<FolderIcon className="size-3.5" />
) : isConnector ? (
getConnectorIcon(segment.doc.connector_type, "size-3.5") ?? (
(getConnectorIcon(segment.doc.connector_type, "size-3.5") ?? (
<Plug className="size-3.5" />
)
))
) : (
getConnectorIcon(segment.doc.document_type ?? "UNKNOWN", "size-3.5")
);
@ -123,7 +123,9 @@ const UserTextPart: FC = () => {
: segment.doc.title
}
onClick={
isFolder || isConnector ? undefined : () => handleOpenDoc(segment.doc.id, segment.doc.title)
isFolder || isConnector
? undefined
: () => handleOpenDoc(segment.doc.id, segment.doc.title)
}
className="mx-0.5"
/>