feat: integrate OneDrive connector with UI components and configuration options

This commit is contained in:
Anish Sarkar 2026-03-28 17:00:52 +05:30
parent 028c88be72
commit 147061284b
13 changed files with 516 additions and 10 deletions

View file

@ -16,6 +16,7 @@ export function getDocumentTypeLabel(type: string): string {
FILE: "File",
SLACK_CONNECTOR: "Slack",
TEAMS_CONNECTOR: "Microsoft Teams",
ONEDRIVE_FILE: "OneDrive",
NOTION_CONNECTOR: "Notion",
YOUTUBE_VIDEO: "YouTube Video",
GITHUB_CONNECTOR: "GitHub",

View file

@ -0,0 +1,443 @@
"use client";
import {
ChevronRight,
File,
FileSpreadsheet,
FileText,
FolderClosed,
FolderOpen,
Image,
Presentation,
X,
} from "lucide-react";
import type { FC } from "react";
import { useCallback, useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Spinner } from "@/components/ui/spinner";
import { Switch } from "@/components/ui/switch";
import { useAuth } from "@/context/AuthContext";
import type { ConnectorConfigProps } from "../index";
interface SelectedItem {
id: string;
name: string;
}
interface IndexingOptions {
max_files_per_folder: number;
incremental_sync: boolean;
include_subfolders: boolean;
}
interface OneDriveItem {
id: string;
name: string;
isFolder: boolean;
size?: number;
lastModifiedDateTime?: string;
file?: { mimeType: string };
folder?: { childCount: number };
webUrl?: string;
}
const DEFAULT_INDEXING_OPTIONS: IndexingOptions = {
max_files_per_folder: 100,
incremental_sync: true,
include_subfolders: true,
};
function getFileIconFromName(fileName: string, className = "size-3.5 shrink-0") {
const lowerName = fileName.toLowerCase();
if (lowerName.endsWith(".xlsx") || lowerName.endsWith(".xls") || lowerName.endsWith(".csv")) {
return <FileSpreadsheet className={`${className} text-muted-foreground`} />;
}
if (lowerName.endsWith(".pptx") || lowerName.endsWith(".ppt")) {
return <Presentation className={`${className} text-muted-foreground`} />;
}
if (lowerName.endsWith(".docx") || lowerName.endsWith(".doc") || lowerName.endsWith(".txt")) {
return <FileText className={`${className} text-muted-foreground`} />;
}
if (/\.(png|jpe?g|gif|webp|svg)$/.test(lowerName)) {
return <Image className={`${className} text-muted-foreground`} />;
}
return <File className={`${className} text-muted-foreground`} />;
}
export const OneDriveConfig: FC<ConnectorConfigProps> = ({ connector, onConfigChange }) => {
const { authenticatedFetch } = useAuth();
const existingFolders = (connector.config?.selected_folders as SelectedItem[] | undefined) || [];
const existingFiles = (connector.config?.selected_files as SelectedItem[] | undefined) || [];
const existingIndexingOptions =
(connector.config?.indexing_options as IndexingOptions | undefined) || DEFAULT_INDEXING_OPTIONS;
const [selectedFolders, setSelectedFolders] = useState<SelectedItem[]>(existingFolders);
const [selectedFiles, setSelectedFiles] = useState<SelectedItem[]>(existingFiles);
const [indexingOptions, setIndexingOptions] = useState<IndexingOptions>(existingIndexingOptions);
const [browserOpen, setBrowserOpen] = useState(false);
const [browseItems, setBrowseItems] = useState<OneDriveItem[]>([]);
const [browseLoading, setBrowseLoading] = useState(false);
const [browseError, setBrowseError] = useState<string | null>(null);
const [breadcrumbs, setBreadcrumbs] = useState<{ id: string; name: string }[]>([
{ id: "root", name: "My files" },
]);
useEffect(() => {
const folders = (connector.config?.selected_folders as SelectedItem[] | undefined) || [];
const files = (connector.config?.selected_files as SelectedItem[] | undefined) || [];
const options =
(connector.config?.indexing_options as IndexingOptions | undefined) || DEFAULT_INDEXING_OPTIONS;
setSelectedFolders(folders);
setSelectedFiles(files);
setIndexingOptions(options);
}, [connector.config]);
const updateConfig = useCallback(
(folders: SelectedItem[], files: SelectedItem[], options: IndexingOptions) => {
if (onConfigChange) {
onConfigChange({
...connector.config,
selected_folders: folders,
selected_files: files,
indexing_options: options,
});
}
},
[onConfigChange, connector.config],
);
const fetchFolderContents = useCallback(
async (parentId: string) => {
setBrowseLoading(true);
setBrowseError(null);
try {
const backendUrl = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL || "http://localhost:8000";
const url = `${backendUrl}/api/v1/connectors/${connector.id}/onedrive/folders?parent_id=${encodeURIComponent(parentId)}`;
const response = await authenticatedFetch(url);
if (!response.ok) {
const data = await response.json().catch(() => ({}));
throw new Error(data.detail || `Failed to load folder contents (${response.status})`);
}
const data = await response.json();
setBrowseItems(data.items || []);
} catch (err: unknown) {
const message = err instanceof Error ? err.message : "Failed to load folder contents";
setBrowseError(message);
} finally {
setBrowseLoading(false);
}
},
[connector.id, authenticatedFetch],
);
const handleOpenBrowser = useCallback(() => {
setBrowserOpen(true);
setBreadcrumbs([{ id: "root", name: "My files" }]);
fetchFolderContents("root");
}, [fetchFolderContents]);
const handleNavigateFolder = useCallback(
(folderId: string, folderName: string) => {
setBreadcrumbs((prev) => [...prev, { id: folderId, name: folderName }]);
fetchFolderContents(folderId);
},
[fetchFolderContents],
);
const handleBreadcrumbClick = useCallback(
(index: number) => {
const newBreadcrumbs = breadcrumbs.slice(0, index + 1);
setBreadcrumbs(newBreadcrumbs);
fetchFolderContents(newBreadcrumbs[newBreadcrumbs.length - 1].id);
},
[breadcrumbs, fetchFolderContents],
);
const isItemSelected = useCallback(
(item: OneDriveItem) => {
if (item.isFolder) {
return selectedFolders.some((f) => f.id === item.id);
}
return selectedFiles.some((f) => f.id === item.id);
},
[selectedFolders, selectedFiles],
);
const handleToggleItem = useCallback(
(item: OneDriveItem) => {
if (item.isFolder) {
const exists = selectedFolders.some((f) => f.id === item.id);
const newFolders = exists
? selectedFolders.filter((f) => f.id !== item.id)
: [...selectedFolders, { id: item.id, name: item.name }];
setSelectedFolders(newFolders);
updateConfig(newFolders, selectedFiles, indexingOptions);
} else {
const exists = selectedFiles.some((f) => f.id === item.id);
const newFiles = exists
? selectedFiles.filter((f) => f.id !== item.id)
: [...selectedFiles, { id: item.id, name: item.name }];
setSelectedFiles(newFiles);
updateConfig(selectedFolders, newFiles, indexingOptions);
}
},
[selectedFolders, selectedFiles, indexingOptions, updateConfig],
);
const handleRemoveFolder = (folderId: string) => {
const newFolders = selectedFolders.filter((f) => f.id !== folderId);
setSelectedFolders(newFolders);
updateConfig(newFolders, selectedFiles, indexingOptions);
};
const handleRemoveFile = (fileId: string) => {
const newFiles = selectedFiles.filter((f) => f.id !== fileId);
setSelectedFiles(newFiles);
updateConfig(selectedFolders, newFiles, indexingOptions);
};
const handleIndexingOptionChange = (key: keyof IndexingOptions, value: number | boolean) => {
const newOptions = { ...indexingOptions, [key]: value };
setIndexingOptions(newOptions);
updateConfig(selectedFolders, selectedFiles, newOptions);
};
const isAuthExpired = connector.config?.auth_expired === true;
const totalSelected = selectedFolders.length + selectedFiles.length;
return (
<div className="space-y-4">
{/* Folder & File Selection */}
<div className="rounded-xl border border-border bg-slate-400/5 dark:bg-white/5 p-3 sm:p-6 space-y-3 sm:space-y-4">
<div className="space-y-1 sm:space-y-2">
<h3 className="font-medium text-sm sm:text-base">Folder & File Selection</h3>
<p className="text-xs sm:text-sm text-muted-foreground">
Browse and select specific folders and/or files to index from your OneDrive.
</p>
</div>
{totalSelected > 0 && (
<div className="p-2 sm:p-3 bg-muted rounded-lg text-xs sm:text-sm space-y-1 sm:space-y-2">
<p className="font-medium">
Selected {totalSelected} item{totalSelected > 1 ? "s" : ""}
</p>
<div className="max-h-20 sm:max-h-24 overflow-y-auto space-y-1">
{selectedFolders.map((folder) => (
<div
key={folder.id}
className="text-xs text-muted-foreground truncate flex items-center gap-1.5"
title={folder.name}
>
<FolderClosed className="size-3.5 shrink-0 text-muted-foreground" />
<span className="flex-1 truncate">{folder.name}</span>
<button
type="button"
onClick={() => handleRemoveFolder(folder.id)}
className="shrink-0 p-0.5 hover:bg-muted-foreground/20 rounded transition-colors"
>
<X className="size-3.5" />
</button>
</div>
))}
{selectedFiles.map((file) => (
<div
key={file.id}
className="text-xs text-muted-foreground truncate flex items-center gap-1.5"
title={file.name}
>
{getFileIconFromName(file.name)}
<span className="flex-1 truncate">{file.name}</span>
<button
type="button"
onClick={() => handleRemoveFile(file.id)}
className="shrink-0 p-0.5 hover:bg-muted-foreground/20 rounded transition-colors"
>
<X className="size-3.5" />
</button>
</div>
))}
</div>
</div>
)}
{!browserOpen ? (
<Button
type="button"
variant="outline"
onClick={handleOpenBrowser}
disabled={isAuthExpired}
className="bg-slate-400/5 dark:bg-white/5 border-slate-400/20 hover:bg-slate-400/10 dark:hover:bg-white/10 text-xs sm:text-sm h-8 sm:h-9"
>
{totalSelected > 0 ? "Change Selection" : "Browse OneDrive"}
</Button>
) : (
<div className="rounded-lg border border-border bg-background">
{/* Breadcrumbs */}
<div className="flex items-center gap-1 px-3 py-2 border-b border-border text-xs overflow-x-auto">
{breadcrumbs.map((crumb, index) => (
<span key={crumb.id} className="flex items-center gap-1 shrink-0">
{index > 0 && <ChevronRight className="size-3 text-muted-foreground" />}
<button
type="button"
onClick={() => handleBreadcrumbClick(index)}
className={`hover:underline ${
index === breadcrumbs.length - 1
? "font-medium text-foreground"
: "text-muted-foreground"
}`}
>
{crumb.name}
</button>
</span>
))}
</div>
{/* File list */}
<div className="max-h-48 overflow-y-auto">
{browseLoading ? (
<div className="flex items-center justify-center p-6">
<Spinner size="sm" />
</div>
) : browseError ? (
<div className="p-3 text-xs text-destructive">{browseError}</div>
) : browseItems.length === 0 ? (
<div className="p-3 text-xs text-muted-foreground">This folder is empty</div>
) : (
browseItems.map((item) => (
<div
key={item.id}
className="flex items-center gap-2 px-3 py-1.5 hover:bg-muted/50 text-xs"
>
<Checkbox
checked={isItemSelected(item)}
onCheckedChange={() => handleToggleItem(item)}
className="size-3.5"
/>
{item.isFolder ? (
<button
type="button"
className="flex items-center gap-1.5 flex-1 min-w-0 text-left"
onClick={() => handleNavigateFolder(item.id, item.name)}
>
<FolderOpen className="size-3.5 shrink-0 text-muted-foreground" />
<span className="truncate">{item.name}</span>
</button>
) : (
<div className="flex items-center gap-1.5 flex-1 min-w-0">
{getFileIconFromName(item.name)}
<span className="truncate">{item.name}</span>
</div>
)}
</div>
))
)}
</div>
<div className="px-3 py-2 border-t border-border flex justify-end">
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => setBrowserOpen(false)}
className="text-xs h-7"
>
Done
</Button>
</div>
</div>
)}
{isAuthExpired && (
<p className="text-xs text-amber-600 dark:text-amber-500">
Your OneDrive authentication has expired. Please re-authenticate using the button below.
</p>
)}
</div>
{/* Indexing Options */}
<div className="rounded-xl border border-border bg-slate-400/5 dark:bg-white/5 p-3 sm:p-6 space-y-4">
<div className="space-y-1 sm:space-y-2">
<h3 className="font-medium text-sm sm:text-base">Indexing Options</h3>
<p className="text-xs sm:text-sm text-muted-foreground">
Configure how files are indexed from your OneDrive.
</p>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="od-max-files" className="text-sm font-medium">
Max files per folder
</Label>
<p className="text-xs text-muted-foreground">
Maximum number of files to index from each folder
</p>
</div>
<Select
value={indexingOptions.max_files_per_folder.toString()}
onValueChange={(value) =>
handleIndexingOptionChange("max_files_per_folder", parseInt(value, 10))
}
>
<SelectTrigger
id="od-max-files"
className="w-[140px] bg-slate-400/5 dark:bg-slate-400/5 border-slate-400/20 text-xs sm:text-sm"
>
<SelectValue placeholder="Select limit" />
</SelectTrigger>
<SelectContent className="z-[100]">
<SelectItem value="50">50 files</SelectItem>
<SelectItem value="100">100 files</SelectItem>
<SelectItem value="250">250 files</SelectItem>
<SelectItem value="500">500 files</SelectItem>
<SelectItem value="1000">1000 files</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="flex items-center justify-between pt-2 border-t border-slate-400/20">
<div className="space-y-0.5">
<Label htmlFor="od-incremental-sync" className="text-sm font-medium">
Incremental sync
</Label>
<p className="text-xs text-muted-foreground">
Only sync changes since last index (faster). Disable for a full re-index.
</p>
</div>
<Switch
id="od-incremental-sync"
checked={indexingOptions.incremental_sync}
onCheckedChange={(checked) => handleIndexingOptionChange("incremental_sync", checked)}
/>
</div>
<div className="flex items-center justify-between pt-2 border-t border-slate-400/20">
<div className="space-y-0.5">
<Label htmlFor="od-include-subfolders" className="text-sm font-medium">
Include subfolders
</Label>
<p className="text-xs text-muted-foreground">
Recursively index files in subfolders of selected folders
</p>
</div>
<Switch
id="od-include-subfolders"
checked={indexingOptions.include_subfolders}
onCheckedChange={(checked) => handleIndexingOptionChange("include_subfolders", checked)}
/>
</div>
</div>
</div>
);
};

View file

@ -21,6 +21,7 @@ import { MCPConfig } from "./components/mcp-config";
import { ObsidianConfig } from "./components/obsidian-config";
import { SlackConfig } from "./components/slack-config";
import { TavilyApiConfig } from "./components/tavily-api-config";
import { OneDriveConfig } from "./components/onedrive-config";
import { TeamsConfig } from "./components/teams-config";
import { WebcrawlerConfig } from "./components/webcrawler-config";
@ -58,6 +59,8 @@ export function getConnectorConfigComponent(
return DiscordConfig;
case "TEAMS_CONNECTOR":
return TeamsConfig;
case "ONEDRIVE_CONNECTOR":
return OneDriveConfig;
case "CONFLUENCE_CONNECTOR":
return ConfluenceConfig;
case "BOOKSTACK_CONNECTOR":

View file

@ -27,6 +27,7 @@ const REAUTH_ENDPOINTS: Partial<Record<string, string>> = {
[EnumConnectorName.COMPOSIO_GOOGLE_DRIVE_CONNECTOR]: "/api/v1/auth/composio/connector/reauth",
[EnumConnectorName.COMPOSIO_GMAIL_CONNECTOR]: "/api/v1/auth/composio/connector/reauth",
[EnumConnectorName.COMPOSIO_GOOGLE_CALENDAR_CONNECTOR]: "/api/v1/auth/composio/connector/reauth",
[EnumConnectorName.ONEDRIVE_CONNECTOR]: "/api/v1/auth/onedrive/connector/reauth",
};
interface ConnectorEditViewProps {

View file

@ -61,6 +61,13 @@ export const OAUTH_CONNECTORS = [
connectorType: EnumConnectorName.TEAMS_CONNECTOR,
authEndpoint: "/api/v1/auth/teams/connector/add/",
},
{
id: "onedrive-connector",
title: "OneDrive",
description: "Search your OneDrive files",
connectorType: EnumConnectorName.ONEDRIVE_CONNECTOR,
authEndpoint: "/api/v1/auth/onedrive/connector/add/",
},
{
id: "discord-connector",
title: "Discord",

View file

@ -729,10 +729,11 @@ export const useConnectorDialog = () => {
async (refreshConnectors: () => void) => {
if (!indexingConfig || !searchSpaceId) return;
// Validate date range (skip for Google Drive, Composio Drive, and Webcrawler)
// Validate date range (skip for Google Drive, Composio Drive, OneDrive, and Webcrawler)
if (
indexingConfig.connectorType !== "GOOGLE_DRIVE_CONNECTOR" &&
indexingConfig.connectorType !== "COMPOSIO_GOOGLE_DRIVE_CONNECTOR" &&
indexingConfig.connectorType !== "ONEDRIVE_CONNECTOR" &&
indexingConfig.connectorType !== "WEBCRAWLER_CONNECTOR"
) {
const dateRangeValidation = dateRangeSchema.safeParse({ startDate, endDate });
@ -778,10 +779,11 @@ export const useConnectorDialog = () => {
});
}
// Handle Google Drive folder selection (regular and Composio)
if (
(indexingConfig.connectorType === "GOOGLE_DRIVE_CONNECTOR" ||
indexingConfig.connectorType === "COMPOSIO_GOOGLE_DRIVE_CONNECTOR") &&
// Handle Google Drive / OneDrive folder selection (regular and Composio)
if (
(indexingConfig.connectorType === "GOOGLE_DRIVE_CONNECTOR" ||
indexingConfig.connectorType === "COMPOSIO_GOOGLE_DRIVE_CONNECTOR" ||
indexingConfig.connectorType === "ONEDRIVE_CONNECTOR") &&
indexingConnectorConfig
) {
const selectedFolders = indexingConnectorConfig.selected_folders as
@ -967,10 +969,11 @@ export const useConnectorDialog = () => {
async (refreshConnectors: () => void) => {
if (!editingConnector || !searchSpaceId || isSaving) return;
// Validate date range (skip for Google Drive which uses folder selection, Webcrawler which uses config, and non-indexable connectors)
// Validate date range (skip for Google Drive/OneDrive which uses folder selection, Webcrawler which uses config, and non-indexable connectors)
if (
editingConnector.is_indexable &&
editingConnector.connector_type !== "GOOGLE_DRIVE_CONNECTOR" &&
editingConnector.connector_type !== "ONEDRIVE_CONNECTOR" &&
editingConnector.connector_type !== "WEBCRAWLER_CONNECTOR"
) {
const dateRangeValidation = dateRangeSchema.safeParse({ startDate, endDate });
@ -986,11 +989,12 @@ export const useConnectorDialog = () => {
return;
}
// Prevent periodic indexing for Google Drive (regular or Composio) without folders/files selected
// Prevent periodic indexing for Google Drive / OneDrive (regular or Composio) without folders/files selected
if (
periodicEnabled &&
(editingConnector.connector_type === "GOOGLE_DRIVE_CONNECTOR" ||
editingConnector.connector_type === "COMPOSIO_GOOGLE_DRIVE_CONNECTOR")
editingConnector.connector_type === "COMPOSIO_GOOGLE_DRIVE_CONNECTOR" ||
editingConnector.connector_type === "ONEDRIVE_CONNECTOR")
) {
const selectedFolders = (connectorConfig || editingConnector.config)?.selected_folders as
| Array<{ id: string; name: string }>
@ -1043,7 +1047,8 @@ export const useConnectorDialog = () => {
indexingDescription = "Settings saved.";
} else if (
editingConnector.connector_type === "GOOGLE_DRIVE_CONNECTOR" ||
editingConnector.connector_type === "COMPOSIO_GOOGLE_DRIVE_CONNECTOR"
editingConnector.connector_type === "COMPOSIO_GOOGLE_DRIVE_CONNECTOR" ||
editingConnector.connector_type === "ONEDRIVE_CONNECTOR"
) {
// Google Drive (both regular and Composio) uses folder selection from config, not date ranges
const selectedFolders = (connectorConfig || editingConnector.config)?.selected_folders as

View file

@ -25,6 +25,7 @@ const REAUTH_ENDPOINTS: Partial<Record<string, string>> = {
[EnumConnectorName.COMPOSIO_GOOGLE_DRIVE_CONNECTOR]: "/api/v1/auth/composio/connector/reauth",
[EnumConnectorName.COMPOSIO_GMAIL_CONNECTOR]: "/api/v1/auth/composio/connector/reauth",
[EnumConnectorName.COMPOSIO_GOOGLE_CALENDAR_CONNECTOR]: "/api/v1/auth/composio/connector/reauth",
[EnumConnectorName.ONEDRIVE_CONNECTOR]: "/api/v1/auth/onedrive/connector/reauth",
[EnumConnectorName.JIRA_CONNECTOR]: "/api/v1/auth/jira/connector/reauth",
[EnumConnectorName.CONFLUENCE_CONNECTOR]: "/api/v1/auth/confluence/connector/reauth",
};

View file

@ -1090,6 +1090,12 @@ const TOOL_GROUPS: ToolGroup[] = [
connectorIcon: "google_drive",
tooltip: "Create and delete files in Google Drive.",
},
{
label: "OneDrive",
tools: ["create_onedrive_file", "delete_onedrive_file"],
connectorIcon: "onedrive",
tooltip: "Create and delete files in OneDrive.",
},
{
label: "Notion",
tools: ["create_notion_page", "update_notion_page", "delete_notion_page"],

View file

@ -6,6 +6,7 @@ export enum EnumConnectorName {
BAIDU_SEARCH_API = "BAIDU_SEARCH_API",
SLACK_CONNECTOR = "SLACK_CONNECTOR",
TEAMS_CONNECTOR = "TEAMS_CONNECTOR",
ONEDRIVE_CONNECTOR = "ONEDRIVE_CONNECTOR",
NOTION_CONNECTOR = "NOTION_CONNECTOR",
GITHUB_CONNECTOR = "GITHUB_CONNECTOR",
LINEAR_CONNECTOR = "LINEAR_CONNECTOR",

View file

@ -39,6 +39,8 @@ export const getConnectorIcon = (connectorType: EnumConnectorName | string, clas
return <Image src="/connectors/slack.svg" alt="Slack" {...imgProps} />;
case EnumConnectorName.TEAMS_CONNECTOR:
return <Image src="/connectors/microsoft-teams.svg" alt="Microsoft Teams" {...imgProps} />;
case EnumConnectorName.ONEDRIVE_CONNECTOR:
return <Image src="/connectors/onedrive.svg" alt="OneDrive" {...imgProps} />;
case EnumConnectorName.NOTION_CONNECTOR:
return <Image src="/connectors/notion.svg" alt="Notion" {...imgProps} />;
case EnumConnectorName.DISCORD_CONNECTOR:
@ -98,6 +100,9 @@ export const getConnectorIcon = (connectorType: EnumConnectorName | string, clas
return <File {...iconProps} />;
case "GOOGLE_DRIVE_FILE":
return <Image src="/connectors/google-drive.svg" alt="Google Drive" {...imgProps} />;
case "ONEDRIVE_FILE":
case "ONEDRIVE_CONNECTOR":
return <Image src="/connectors/onedrive.svg" alt="OneDrive" {...imgProps} />;
case "COMPOSIO_GOOGLE_DRIVE_CONNECTOR":
return <Image src="/connectors/google-drive.svg" alt="Google Drive" {...imgProps} />;
case "COMPOSIO_GMAIL_CONNECTOR":

View file

@ -277,6 +277,19 @@ class ConnectorsApiService {
}>(`/api/v1/connectors/${connectorId}/drive-picker-token`);
};
/**
* List OneDrive folders and files
*/
listOneDriveFolders = async (request: { connector_id: number; parent_id?: string }) => {
const queryParams = request.parent_id
? `?parent_id=${encodeURIComponent(request.parent_id)}`
: "";
return baseApiService.get(
`/api/v1/connectors/${request.connector_id}/onedrive/folders${queryParams}`,
listGoogleDriveFoldersResponse
);
};
// =============================================================================
// MCP Connector Methods
// =============================================================================

View file

@ -132,11 +132,30 @@ export function buildContentForPersistence(
return parts.length > 0 ? parts : [{ type: "text", text: "" }];
}
export type SSEEvent =
| { type: "text-delta"; delta: string }
| { type: "tool-input-start"; toolCallId: string; toolName: string }
| {
type: "tool-input-available";
toolCallId: string;
toolName: string;
input: Record<string, unknown>;
}
| {
type: "tool-output-available";
toolCallId: string;
output: Record<string, unknown>;
}
| { type: "data-thinking-step"; data: ThinkingStepData }
| { type: "data-thread-title-update"; data: { threadId: number; title: string } }
| { type: "data-interrupt-request"; data: Record<string, unknown> }
| { type: "error"; errorText: string };
/**
* Async generator that reads an SSE stream and yields parsed JSON objects.
* Handles buffering, event splitting, and skips malformed JSON / [DONE] lines.
*/
export async function* readSSEStream(response: Response): AsyncGenerator<unknown> {
export async function* readSSEStream(response: Response): AsyncGenerator<SSEEvent> {
if (!response.body) {
throw new Error("No response body");
}

View file

@ -8,6 +8,7 @@ export const getConnectorTypeDisplay = (type: string): string => {
BAIDU_SEARCH_API: "Baidu Search",
SLACK_CONNECTOR: "Slack",
TEAMS_CONNECTOR: "Microsoft Teams",
ONEDRIVE_CONNECTOR: "OneDrive",
NOTION_CONNECTOR: "Notion",
GITHUB_CONNECTOR: "GitHub",
LINEAR_CONNECTOR: "Linear",