mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-29 19:35:20 +02:00
feat(web): add connector display definitions and enhance composer suggestion components
This commit is contained in:
parent
2d134439ec
commit
79f5e8f88c
3 changed files with 105 additions and 72 deletions
|
|
@ -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 = [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,15 +315,19 @@ export const DocumentMentionPicker = forwardRef<
|
||||||
);
|
);
|
||||||
|
|
||||||
const rootNodes = useMemo<ComposerSuggestionNode<ResourceNodeValue>[]>(
|
const rootNodes = useMemo<ComposerSuggestionNode<ResourceNodeValue>[]>(
|
||||||
() => [
|
() => {
|
||||||
{
|
const nodes: ComposerSuggestionNode<ResourceNodeValue>[] = [];
|
||||||
|
if (showSurfsenseDocsRootRef.current) {
|
||||||
|
nodes.push({
|
||||||
id: "surfsense-docs",
|
id: "surfsense-docs",
|
||||||
label: "SurfSense Docs",
|
label: "SurfSense Docs",
|
||||||
subtitle: "Browse product documentation",
|
subtitle: "Browse product documentation",
|
||||||
icon: <BookOpen className="size-4" />,
|
icon: <BookOpen className="size-4" />,
|
||||||
type: "branch",
|
type: "branch",
|
||||||
value: { kind: "view", view: { kind: "surfsense-docs" } },
|
value: { kind: "view", view: { kind: "surfsense-docs" } },
|
||||||
},
|
});
|
||||||
|
}
|
||||||
|
nodes.push(
|
||||||
{
|
{
|
||||||
id: "files-folders",
|
id: "files-folders",
|
||||||
label: "Files & Folders",
|
label: "Files & Folders",
|
||||||
|
|
@ -352,12 +342,14 @@ export const DocumentMentionPicker = forwardRef<
|
||||||
subtitle: activeConnectors.length
|
subtitle: activeConnectors.length
|
||||||
? "Choose the exact account for tool use"
|
? "Choose the exact account for tool use"
|
||||||
: "No connected accounts yet",
|
: "No connected accounts yet",
|
||||||
icon: <Plug className="size-4" />,
|
icon: <Unplug className="size-4" />,
|
||||||
type: "branch",
|
type: "branch",
|
||||||
disabled: activeConnectors.length === 0,
|
disabled: activeConnectors.length === 0,
|
||||||
value: { kind: "view", view: { kind: "connectors" } },
|
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
|
||||||
|
type="button"
|
||||||
onClick={handleBack}
|
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"
|
||||||
>
|
>
|
||||||
<span className="flex-1 truncate text-sm">{title}</span>
|
<ChevronLeft className="size-4" />
|
||||||
</ComposerSuggestionItem>
|
</button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span className="flex-1 truncate">{title}</span>
|
||||||
|
</ComposerSuggestionHeader>
|
||||||
<ComposerSuggestionSeparator />
|
<ComposerSuggestionSeparator />
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue