feat(web): add connector display definitions and enhance composer suggestion components

This commit is contained in:
Anish Sarkar 2026-05-26 22:40:22 +05:30
parent 2d134439ec
commit 79f5e8f88c
3 changed files with 105 additions and 72 deletions

View file

@ -1,5 +1,4 @@
import { EnumConnectorName } from "@/contracts/enums/connector"; import { EnumConnectorName } from "@/contracts/enums/connector";
import type { SearchSourceConnector } from "@/contracts/types/connector.types";
/** /**
* Connectors that operate in real time (no background indexing). * Connectors that operate in real time (no background indexing).
@ -230,6 +229,20 @@ export const COMPOSIO_CONNECTORS = [
}, },
] as const; ] as const;
export const CONNECTOR_DISPLAY_DEFINITIONS = [
...OAUTH_CONNECTORS,
...CRAWLERS,
...OTHER_CONNECTORS,
...COMPOSIO_CONNECTORS,
] as const;
export function getConnectorTitle(connectorType: string): string {
return (
CONNECTOR_DISPLAY_DEFINITIONS.find((connector) => connector.connectorType === connectorType)
?.title ?? connectorType
);
}
// Composio Toolkits (available integrations via Composio) // Composio Toolkits (available integrations via Composio)
export const COMPOSIO_TOOLKITS = [ export const COMPOSIO_TOOLKITS = [
{ {

View file

@ -31,7 +31,7 @@ function ComposerSuggestionPopoverContent({
onCloseAutoFocus?.(event); onCloseAutoFocus?.(event);
}} }}
className={cn( className={cn(
"w-[280px] overflow-hidden rounded-md border border-popover-border bg-popover p-0 text-popover-foreground shadow-md sm:w-[320px]", "w-[256px] overflow-hidden rounded-md border border-popover-border bg-popover p-0 text-popover-foreground shadow-md sm:w-[288px]",
"data-[state=open]:!animate-none data-[state=closed]:!animate-none data-[state=open]:!duration-0 data-[state=closed]:!duration-0", "data-[state=open]:!animate-none data-[state=closed]:!animate-none data-[state=open]:!duration-0 data-[state=closed]:!duration-0",
className className
)} )}
@ -47,14 +47,14 @@ const ComposerSuggestionList = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<div <div
ref={ref} ref={ref}
className={cn("max-h-[180px] overflow-y-auto sm:max-h-[280px]", className)} className={cn("max-h-[160px] overflow-y-auto sm:max-h-[232px]", className)}
{...props} {...props}
/> />
)); ));
ComposerSuggestionList.displayName = "ComposerSuggestionList"; ComposerSuggestionList.displayName = "ComposerSuggestionList";
function ComposerSuggestionGroup({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) { function ComposerSuggestionGroup({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return <div className={cn("px-2 py-1", className)} {...props} />; return <div className={cn("px-1.5 py-2", className)} {...props} />;
} }
function ComposerSuggestionGroupHeading({ function ComposerSuggestionGroupHeading({
@ -63,12 +63,32 @@ function ComposerSuggestionGroupHeading({
}: React.HTMLAttributes<HTMLDivElement>) { }: React.HTMLAttributes<HTMLDivElement>) {
return ( return (
<div <div
className={cn("px-3 py-2 text-xs font-bold text-muted-foreground/55", className)} className={cn("px-2.5 py-1.5 text-xs font-bold text-muted-foreground/55", className)}
{...props} {...props}
/> />
); );
} }
function ComposerSuggestionHeader({
className,
icon,
children,
...props
}: React.HTMLAttributes<HTMLDivElement> & { icon?: React.ReactNode }) {
return (
<div
className={cn(
"flex items-center gap-2 px-2 py-1.5 text-sm font-semibold text-muted-foreground",
className
)}
{...props}
>
{icon ? <span className="shrink-0 text-muted-foreground">{icon}</span> : null}
{children}
</div>
);
}
const ComposerSuggestionItem = React.forwardRef< const ComposerSuggestionItem = React.forwardRef<
HTMLButtonElement, HTMLButtonElement,
Omit<React.ComponentProps<typeof Button>, "variant"> & { Omit<React.ComponentProps<typeof Button>, "variant"> & {
@ -83,7 +103,7 @@ const ComposerSuggestionItem = React.forwardRef<
variant="ghost" variant="ghost"
disabled={disabled} disabled={disabled}
className={cn( className={cn(
"h-auto w-full justify-start gap-2 rounded-md px-3 py-2 text-left text-sm font-normal transition-colors", "h-auto w-full justify-start gap-2 rounded-md px-2.5 py-1.5 text-left text-sm font-normal transition-colors",
disabled ? "cursor-not-allowed opacity-50" : "cursor-pointer", disabled ? "cursor-not-allowed opacity-50" : "cursor-pointer",
muted && !selected && "text-muted-foreground hover:bg-accent hover:text-accent-foreground", muted && !selected && "text-muted-foreground hover:bg-accent hover:text-accent-foreground",
selected && "bg-accent text-accent-foreground", selected && "bg-accent text-accent-foreground",
@ -99,7 +119,7 @@ ComposerSuggestionItem.displayName = "ComposerSuggestionItem";
function ComposerSuggestionSeparator({ className, ...props }: React.ComponentProps<typeof Separator>) { function ComposerSuggestionSeparator({ className, ...props }: React.ComponentProps<typeof Separator>) {
return ( return (
<div className={cn("my-1 px-4", className)}> <div className={cn("my-1 px-3", className)}>
<Separator className="bg-popover-border" {...props} /> <Separator className="bg-popover-border" {...props} />
</div> </div>
); );
@ -111,10 +131,10 @@ function ComposerSuggestionMessage({
variant = "muted", variant = "muted",
}: React.HTMLAttributes<HTMLParagraphElement> & { variant?: "muted" | "destructive" }) { }: React.HTMLAttributes<HTMLParagraphElement> & { variant?: "muted" | "destructive" }) {
return ( return (
<div className="px-2 py-1"> <div className="px-1.5 py-1">
<p <p
className={cn( className={cn(
"px-3 py-2 text-xs", "px-2.5 py-1.5 text-xs",
variant === "destructive" ? "text-destructive" : "text-muted-foreground", variant === "destructive" ? "text-destructive" : "text-muted-foreground",
className className
)} )}
@ -127,15 +147,15 @@ function ComposerSuggestionMessage({
function ComposerSuggestionSkeleton() { function ComposerSuggestionSkeleton() {
return ( return (
<div className="px-2 py-1"> <div className="px-1.5 py-1">
<div className="px-3 py-2"> <div className="px-2.5 py-1.5">
<Skeleton className="h-[16px] w-24" /> <Skeleton className="h-[16px] w-24" />
</div> </div>
{["a", "b", "c", "d", "e"].map((id, index) => ( {["a", "b", "c", "d", "e"].map((id, index) => (
<div <div
key={id} key={id}
className={cn( className={cn(
"flex w-full items-center gap-2 rounded-md px-3 py-2 text-left", "flex w-full items-center gap-2 rounded-md px-2.5 py-1.5 text-left",
index >= 3 && "hidden sm:flex" index >= 3 && "hidden sm:flex"
)} )}
> >
@ -156,6 +176,7 @@ export {
ComposerSuggestionList, ComposerSuggestionList,
ComposerSuggestionGroup, ComposerSuggestionGroup,
ComposerSuggestionGroupHeading, ComposerSuggestionGroupHeading,
ComposerSuggestionHeader,
ComposerSuggestionItem, ComposerSuggestionItem,
ComposerSuggestionSeparator, ComposerSuggestionSeparator,
ComposerSuggestionMessage, ComposerSuggestionMessage,

View file

@ -8,7 +8,7 @@ import {
ChevronRight, ChevronRight,
Files, Files,
Folder as FolderIcon, Folder as FolderIcon,
Plug, Unplug,
} from "lucide-react"; } from "lucide-react";
import { import {
forwardRef, forwardRef,
@ -23,14 +23,12 @@ import type * as React from "react";
import type { MentionedDocumentInfo } from "@/atoms/chat/mentioned-documents.atom"; import type { MentionedDocumentInfo } from "@/atoms/chat/mentioned-documents.atom";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { connectorsAtom } from "@/atoms/connectors/connector-query.atoms"; import { connectorsAtom } from "@/atoms/connectors/connector-query.atoms";
import { import { getConnectorTitle } from "@/components/assistant-ui/connector-popup/constants/connector-constants";
COMPOSIO_CONNECTORS,
OAUTH_CONNECTORS,
} from "@/components/assistant-ui/connector-popup/constants/connector-constants";
import { getConnectorDisplayName } from "@/components/assistant-ui/connector-popup/tabs/all-connectors-tab"; import { getConnectorDisplayName } from "@/components/assistant-ui/connector-popup/tabs/all-connectors-tab";
import { import {
ComposerSuggestionGroup, ComposerSuggestionGroup,
ComposerSuggestionGroupHeading, ComposerSuggestionGroupHeading,
ComposerSuggestionHeader,
ComposerSuggestionItem, ComposerSuggestionItem,
ComposerSuggestionList, ComposerSuggestionList,
ComposerSuggestionMessage, ComposerSuggestionMessage,
@ -94,19 +92,6 @@ function useDebounced<T>(value: T, delay = DEBOUNCE_MS) {
return debounced; return debounced;
} }
function titleForConnectorType(connectorType: string) {
const configured =
OAUTH_CONNECTORS.find((c) => c.connectorType === connectorType) ||
COMPOSIO_CONNECTORS.find((c) => c.connectorType === connectorType);
return (
configured?.title ||
connectorType
.replace(/_/g, " ")
.replace(/connector/gi, "")
.trim()
);
}
function makeDocMention(doc: Pick<Document, "id" | "title" | "document_type">): MentionedDocumentInfo { function makeDocMention(doc: Pick<Document, "id" | "title" | "document_type">): MentionedDocumentInfo {
return { return {
id: doc.id, id: doc.id,
@ -130,7 +115,7 @@ function makeConnectorMention(
connector: SearchSourceConnector connector: SearchSourceConnector
): Extract<MentionedDocumentInfo, { kind: "connector" }> { ): Extract<MentionedDocumentInfo, { kind: "connector" }> {
const accountName = getConnectorDisplayName(connector.name); const accountName = getConnectorDisplayName(connector.name);
const connectorTitle = titleForConnectorType(connector.connector_type); const connectorTitle = getConnectorTitle(connector.connector_type);
return { return {
id: connector.id, id: connector.id,
title: `${connectorTitle}: ${accountName}`, title: `${connectorTitle}: ${accountName}`,
@ -319,6 +304,7 @@ export const DocumentMentionPicker = forwardRef<
() => new Set(initialSelectedDocuments.map((d) => getMentionDocKey(d))), () => new Set(initialSelectedDocuments.map((d) => getMentionDocKey(d))),
[initialSelectedDocuments] [initialSelectedDocuments]
); );
const showSurfsenseDocsRootRef = useRef((surfsenseDocs?.items?.length ?? 0) > 0);
const selectMention = useCallback( const selectMention = useCallback(
(mention: MentionedDocumentInfo) => { (mention: MentionedDocumentInfo) => {
@ -329,35 +315,41 @@ export const DocumentMentionPicker = forwardRef<
); );
const rootNodes = useMemo<ComposerSuggestionNode<ResourceNodeValue>[]>( const rootNodes = useMemo<ComposerSuggestionNode<ResourceNodeValue>[]>(
() => [ () => {
{ const nodes: ComposerSuggestionNode<ResourceNodeValue>[] = [];
id: "surfsense-docs", if (showSurfsenseDocsRootRef.current) {
label: "SurfSense Docs", nodes.push({
subtitle: "Browse product documentation", id: "surfsense-docs",
icon: <BookOpen className="size-4" />, label: "SurfSense Docs",
type: "branch", subtitle: "Browse product documentation",
value: { kind: "view", view: { kind: "surfsense-docs" } }, icon: <BookOpen className="size-4" />,
}, type: "branch",
{ value: { kind: "view", view: { kind: "surfsense-docs" } },
id: "files-folders", });
label: "Files & Folders", }
subtitle: "Browse your knowledge base", nodes.push(
icon: <Files className="size-4" />, {
type: "branch", id: "files-folders",
value: { kind: "view", view: { kind: "files-folders" } }, label: "Files & Folders",
}, subtitle: "Browse your knowledge base",
{ icon: <Files className="size-4" />,
id: "connectors", type: "branch",
label: "Connectors", value: { kind: "view", view: { kind: "files-folders" } },
subtitle: activeConnectors.length },
? "Choose the exact account for tool use" {
: "No connected accounts yet", id: "connectors",
icon: <Plug className="size-4" />, label: "Connectors",
type: "branch", subtitle: activeConnectors.length
disabled: activeConnectors.length === 0, ? "Choose the exact account for tool use"
value: { kind: "view", view: { kind: "connectors" } }, : "No connected accounts yet",
}, icon: <Unplug className="size-4" />,
], type: "branch",
disabled: activeConnectors.length === 0,
value: { kind: "view", view: { kind: "connectors" } },
}
);
return nodes;
},
[activeConnectors.length] [activeConnectors.length]
); );
@ -389,7 +381,7 @@ export const DocumentMentionPicker = forwardRef<
id: getMentionDocKey(mention), id: getMentionDocKey(mention),
label: mention.title, label: mention.title,
subtitle: "Connector account", subtitle: "Connector account",
icon: getConnectorIcon(mention.connector_type, "size-4") ?? <Plug className="size-4" />, icon: getConnectorIcon(mention.connector_type, "size-4") ?? <Unplug className="size-4" />,
type: "item" as const, type: "item" as const,
disabled: selectedKeys.has(getMentionDocKey(mention)), disabled: selectedKeys.has(getMentionDocKey(mention)),
value: { kind: "mention" as const, mention }, value: { kind: "mention" as const, mention },
@ -414,7 +406,7 @@ export const DocumentMentionPicker = forwardRef<
byType.set(connector.connector_type, list); byType.set(connector.connector_type, list);
} }
return Array.from(byType.entries()).sort(([a], [b]) => return Array.from(byType.entries()).sort(([a], [b]) =>
titleForConnectorType(a).localeCompare(titleForConnectorType(b)) getConnectorTitle(a).localeCompare(getConnectorTitle(b))
); );
}, [activeConnectors]); }, [activeConnectors]);
@ -459,16 +451,16 @@ export const DocumentMentionPicker = forwardRef<
if (view.kind === "connectors") { if (view.kind === "connectors") {
return connectorTypeEntries.map(([connectorType, typeConnectors]) => ({ return connectorTypeEntries.map(([connectorType, typeConnectors]) => ({
id: `connector-type:${connectorType}`, id: `connector-type:${connectorType}`,
label: titleForConnectorType(connectorType), label: getConnectorTitle(connectorType),
subtitle: `${typeConnectors.length} ${typeConnectors.length === 1 ? "account" : "accounts"}`, subtitle: `${typeConnectors.length} ${typeConnectors.length === 1 ? "account" : "accounts"}`,
icon: getConnectorIcon(connectorType, "size-4") ?? <Plug className="size-4" />, icon: getConnectorIcon(connectorType, "size-4") ?? <Unplug className="size-4" />,
type: "branch" as const, type: "branch" as const,
value: { value: {
kind: "view" as const, kind: "view" as const,
view: { view: {
kind: "connector-type" as const, kind: "connector-type" as const,
connectorType, connectorType,
title: titleForConnectorType(connectorType), title: getConnectorTitle(connectorType),
}, },
}, },
})); }));
@ -481,7 +473,7 @@ export const DocumentMentionPicker = forwardRef<
id: getMentionDocKey(mention), id: getMentionDocKey(mention),
label: getConnectorDisplayName(connector.name), label: getConnectorDisplayName(connector.name),
subtitle: `${view.title} account`, subtitle: `${view.title} account`,
icon: getConnectorIcon(connector.connector_type, "size-4") ?? <Plug className="size-4" />, icon: getConnectorIcon(connector.connector_type, "size-4") ?? <Unplug className="size-4" />,
type: "item" as const, type: "item" as const,
disabled: selectedKeys.has(getMentionDocKey(mention)), disabled: selectedKeys.has(getMentionDocKey(mention)),
value: { kind: "mention" as const, mention }, value: { kind: "mention" as const, mention },
@ -571,13 +563,20 @@ export const DocumentMentionPicker = forwardRef<
<ComposerSuggestionGroup> <ComposerSuggestionGroup>
{title ? ( {title ? (
<> <>
<ComposerSuggestionItem <ComposerSuggestionHeader
icon={<ChevronLeft className="size-4" />} icon={
muted <button
onClick={handleBack} type="button"
onClick={handleBack}
aria-label="Back"
className="-ml-0.5 flex size-5 items-center justify-center rounded-sm text-muted-foreground transition-colors hover:text-foreground focus-visible:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
>
<ChevronLeft className="size-4" />
</button>
}
> >
<span className="flex-1 truncate text-sm">{title}</span> <span className="flex-1 truncate">{title}</span>
</ComposerSuggestionItem> </ComposerSuggestionHeader>
<ComposerSuggestionSeparator /> <ComposerSuggestionSeparator />
</> </>
) : null} ) : null}