mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-01 11:56:25 +02:00
Merge remote-tracking branch 'upstream/dev' into electon-desktop
This commit is contained in:
commit
ab3c636bcd
85 changed files with 4642 additions and 414 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@ import {
|
|||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import {
|
||||
Drawer,
|
||||
DrawerContent,
|
||||
|
|
@ -234,6 +233,7 @@ export function DocumentsTableShell({
|
|||
mentionedDocIds,
|
||||
onToggleChatMention,
|
||||
isSearchMode = false,
|
||||
onOpenInTab,
|
||||
}: {
|
||||
documents: Document[];
|
||||
loading: boolean;
|
||||
|
|
@ -253,6 +253,8 @@ export function DocumentsTableShell({
|
|||
onToggleChatMention?: (doc: Document, mentioned: boolean) => void;
|
||||
/** Whether results are filtered by a search query or type filters */
|
||||
isSearchMode?: boolean;
|
||||
/** When provided, desktop "Preview" opens a document tab instead of the popup dialog */
|
||||
onOpenInTab?: (doc: Document) => void;
|
||||
}) {
|
||||
const t = useTranslations("documents");
|
||||
const { openDialog } = useDocumentUploadDialog();
|
||||
|
|
@ -742,9 +744,9 @@ export function DocumentsTableShell({
|
|||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-48">
|
||||
<DropdownMenuItem onClick={() => handleViewDocument(doc)}>
|
||||
<DropdownMenuItem onClick={() => onOpenInTab ? onOpenInTab(doc) : handleViewDocument(doc)}>
|
||||
<Eye className="h-4 w-4" />
|
||||
Preview
|
||||
Open
|
||||
</DropdownMenuItem>
|
||||
{isEditable && (
|
||||
<DropdownMenuItem
|
||||
|
|
@ -923,26 +925,18 @@ export function DocumentsTableShell({
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* Document Content Viewer */}
|
||||
<Dialog open={!!viewingDoc} onOpenChange={(open) => !open && handleCloseViewer()}>
|
||||
<DialogContent className="max-w-4xl max-w-[92%] md:max-w-4xl max-h-[75vh] md:max-h-[80vh] flex flex-col overflow-hidden pb-0 p-3 md:p-6 gap-2 md:gap-4">
|
||||
<DialogHeader className="flex-shrink-0">
|
||||
<DialogTitle className="text-sm md:text-lg leading-tight pr-6">
|
||||
{/* Document Content Viewer (mobile drawer) */}
|
||||
<Drawer open={!!viewingDoc} onOpenChange={(open) => !open && handleCloseViewer()}>
|
||||
<DrawerContent className="max-h-[85vh] flex flex-col">
|
||||
<DrawerHandle />
|
||||
<DrawerHeader className="text-left shrink-0">
|
||||
<DrawerTitle className="text-base leading-tight break-words">
|
||||
{viewingDoc?.title}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
</DrawerTitle>
|
||||
</DrawerHeader>
|
||||
<div
|
||||
onScroll={handlePreviewScroll}
|
||||
className={[
|
||||
"overflow-y-auto flex-1 min-h-0 px-1 md:px-6 select-text",
|
||||
"max-md:text-xs",
|
||||
"max-md:[&_h1]:text-base! max-md:[&_h1]:mt-3!",
|
||||
"max-md:[&_h2]:text-sm! max-md:[&_h2]:mt-2!",
|
||||
"max-md:[&_h3]:text-xs! max-md:[&_h3]:mt-2!",
|
||||
"max-md:[&_h4]:text-xs!",
|
||||
"max-md:[&_td]:text-[11px]! max-md:[&_td]:px-2! max-md:[&_td]:py-1.5!",
|
||||
"max-md:[&_th]:text-[11px]! max-md:[&_th]:px-2! max-md:[&_th]:py-1.5!",
|
||||
].join(" ")}
|
||||
className="overflow-y-auto flex-1 min-h-0 px-4 pb-6 select-text text-xs [&_h1]:text-base! [&_h1]:mt-3! [&_h2]:text-sm! [&_h2]:mt-2! [&_h3]:text-xs! [&_h3]:mt-2! [&_h4]:text-xs! [&_td]:text-[11px]! [&_td]:px-2! [&_td]:py-1.5! [&_th]:text-[11px]! [&_th]:px-2! [&_th]:py-1.5!"
|
||||
style={{
|
||||
maskImage: `linear-gradient(to bottom, ${previewScrollPos === "top" ? "black" : "transparent"}, black 16px, black calc(100% - 16px), ${previewScrollPos === "bottom" ? "black" : "transparent"})`,
|
||||
WebkitMaskImage: `linear-gradient(to bottom, ${previewScrollPos === "top" ? "black" : "transparent"}, black 16px, black calc(100% - 16px), ${previewScrollPos === "bottom" ? "black" : "transparent"})`,
|
||||
|
|
@ -956,8 +950,8 @@ export function DocumentsTableShell({
|
|||
<MarkdownViewer content={viewingContent} />
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
{/* Document Metadata Viewer (Ctrl+Click) */}
|
||||
<JsonMetadataViewer
|
||||
|
|
@ -1027,7 +1021,7 @@ export function DocumentsTableShell({
|
|||
}}
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
Preview
|
||||
Open
|
||||
</Button>
|
||||
{mobileActionDoc &&
|
||||
EDITABLE_DOCUMENT_TYPES.includes(
|
||||
|
|
|
|||
|
|
@ -40,7 +40,6 @@ import { Thread } from "@/components/assistant-ui/thread";
|
|||
import { MobileEditorPanel } from "@/components/editor-panel/editor-panel";
|
||||
import { MobileHitlEditPanel } from "@/components/hitl-edit-panel/hitl-edit-panel";
|
||||
import { MobileReportPanel } from "@/components/report-panel/report-panel";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { useChatSessionStateSync } from "@/hooks/use-chat-session-state";
|
||||
import { useMessagesSync } from "@/hooks/use-messages-sync";
|
||||
import { documentsApiService } from "@/lib/apis/documents-api.service";
|
||||
|
|
@ -144,6 +143,8 @@ const TOOLS_WITH_UI = new Set([
|
|||
"delete_linear_issue",
|
||||
"create_google_drive_file",
|
||||
"delete_google_drive_file",
|
||||
"create_onedrive_file",
|
||||
"delete_onedrive_file",
|
||||
"create_calendar_event",
|
||||
"update_calendar_event",
|
||||
"delete_calendar_event",
|
||||
|
|
@ -902,6 +903,7 @@ export default function NewChatPage() {
|
|||
currentThread,
|
||||
currentUser,
|
||||
disabledTools,
|
||||
updateChatTabTitle,
|
||||
]
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -763,7 +763,7 @@ function CreateInviteDialog({
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter className="gap-3 sm:gap-2">
|
||||
<DialogFooter>
|
||||
<Button variant="secondary" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -181,6 +181,12 @@ export default function sitemap(): MetadataRoute.Sitemap {
|
|||
changeFrequency: "daily",
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: "https://www.surfsense.com/docs/connectors/microsoft-onedrive",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: "https://www.surfsense.com/docs/connectors/microsoft-teams",
|
||||
lastModified,
|
||||
|
|
|
|||
|
|
@ -40,6 +40,10 @@ import {
|
|||
CreateGoogleDriveFileToolUI,
|
||||
DeleteGoogleDriveFileToolUI,
|
||||
} from "@/components/tool-ui/google-drive";
|
||||
import {
|
||||
CreateOneDriveFileToolUI,
|
||||
DeleteOneDriveFileToolUI,
|
||||
} from "@/components/tool-ui/onedrive";
|
||||
import {
|
||||
CreateJiraIssueToolUI,
|
||||
DeleteJiraIssueToolUI,
|
||||
|
|
@ -97,6 +101,8 @@ const AssistantMessageInner: FC = () => {
|
|||
delete_linear_issue: DeleteLinearIssueToolUI,
|
||||
create_google_drive_file: CreateGoogleDriveFileToolUI,
|
||||
delete_google_drive_file: DeleteGoogleDriveFileToolUI,
|
||||
create_onedrive_file: CreateOneDriveFileToolUI,
|
||||
delete_onedrive_file: DeleteOneDriveFileToolUI,
|
||||
create_calendar_event: CreateCalendarEventToolUI,
|
||||
update_calendar_event: UpdateCalendarEventToolUI,
|
||||
delete_calendar_event: DeleteCalendarEventToolUI,
|
||||
|
|
|
|||
|
|
@ -340,10 +340,11 @@ export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, Connector
|
|||
onBack={handleBackFromEdit}
|
||||
onQuickIndex={(() => {
|
||||
const cfg = connectorConfig || editingConnector.config;
|
||||
const isDrive =
|
||||
editingConnector.connector_type === "GOOGLE_DRIVE_CONNECTOR" ||
|
||||
editingConnector.connector_type === "COMPOSIO_GOOGLE_DRIVE_CONNECTOR";
|
||||
const hasDriveItems = isDrive
|
||||
const isDriveOrOneDrive =
|
||||
editingConnector.connector_type === "GOOGLE_DRIVE_CONNECTOR" ||
|
||||
editingConnector.connector_type === "COMPOSIO_GOOGLE_DRIVE_CONNECTOR" ||
|
||||
editingConnector.connector_type === "ONEDRIVE_CONNECTOR";
|
||||
const hasDriveItems = isDriveOrOneDrive
|
||||
? ((cfg?.selected_folders as unknown[]) ?? []).length > 0 ||
|
||||
((cfg?.selected_files as unknown[]) ?? []).length > 0
|
||||
: true;
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import {
|
|||
} from "lucide-react";
|
||||
import type { FC } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { ComposioDriveFolderTree } from "@/components/connectors/composio-drive-folder-tree";
|
||||
import { DriveFolderTree, type SelectedFolder } from "@/components/connectors/drive-folder-tree";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
|
|
@ -23,13 +23,9 @@ import {
|
|||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { connectorsApiService } from "@/lib/apis/connectors-api.service";
|
||||
import type { ConnectorConfigProps } from "../index";
|
||||
|
||||
interface SelectedFolder {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface IndexingOptions {
|
||||
max_files_per_folder: number;
|
||||
incremental_sync: boolean;
|
||||
|
|
@ -102,6 +98,16 @@ export const ComposioDriveConfig: FC<ConnectorConfigProps> = ({ connector, onCon
|
|||
setAuthError(true);
|
||||
}, []);
|
||||
|
||||
const fetchItems = useCallback(
|
||||
async (parentId?: string) => {
|
||||
return connectorsApiService.listComposioDriveFolders({
|
||||
connector_id: connector.id,
|
||||
parent_id: parentId,
|
||||
});
|
||||
},
|
||||
[connector.id]
|
||||
);
|
||||
|
||||
const [isEditMode] = useState(() => existingFolders.length > 0 || existingFiles.length > 0);
|
||||
const [isFolderTreeOpen, setIsFolderTreeOpen] = useState(!isEditMode);
|
||||
|
||||
|
|
@ -255,24 +261,28 @@ export const ComposioDriveConfig: FC<ConnectorConfigProps> = ({ connector, onCon
|
|||
)}
|
||||
</button>
|
||||
{isFolderTreeOpen && (
|
||||
<ComposioDriveFolderTree
|
||||
connectorId={connector.id}
|
||||
<DriveFolderTree
|
||||
fetchItems={fetchItems}
|
||||
selectedFolders={selectedFolders}
|
||||
onSelectFolders={handleSelectFolders}
|
||||
selectedFiles={selectedFiles}
|
||||
onSelectFiles={handleSelectFiles}
|
||||
onAuthError={handleAuthError}
|
||||
rootLabel="My Drive"
|
||||
providerName="Google Drive"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<ComposioDriveFolderTree
|
||||
connectorId={connector.id}
|
||||
<DriveFolderTree
|
||||
fetchItems={fetchItems}
|
||||
selectedFolders={selectedFolders}
|
||||
onSelectFolders={handleSelectFolders}
|
||||
selectedFiles={selectedFiles}
|
||||
onSelectFiles={handleSelectFiles}
|
||||
onAuthError={handleAuthError}
|
||||
rootLabel="My Drive"
|
||||
providerName="Google Drive"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -242,8 +242,6 @@ export const GoogleDriveConfig: FC<ConnectorConfigProps> = ({ connector, onConfi
|
|||
{totalSelected > 0 ? "Change Selection" : "Select from Google Drive"}
|
||||
</Button>
|
||||
|
||||
{pickerError && !isAuthExpired && <p className="text-xs text-destructive">{pickerError}</p>}
|
||||
|
||||
{isAuthExpired && (
|
||||
<p className="text-xs text-amber-600 dark:text-amber-500">
|
||||
Your Google Drive authentication has expired. Please re-authenticate using the button
|
||||
|
|
|
|||
|
|
@ -0,0 +1,350 @@
|
|||
"use client";
|
||||
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
File,
|
||||
FileSpreadsheet,
|
||||
FileText,
|
||||
FolderClosed,
|
||||
Image,
|
||||
Presentation,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import type { FC } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { DriveFolderTree, type SelectedFolder } from "@/components/connectors/drive-folder-tree";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { connectorsApiService } from "@/lib/apis/connectors-api.service";
|
||||
import type { ConnectorConfigProps } from "../index";
|
||||
|
||||
interface IndexingOptions {
|
||||
max_files_per_folder: number;
|
||||
incremental_sync: boolean;
|
||||
include_subfolders: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_INDEXING_OPTIONS: IndexingOptions = {
|
||||
max_files_per_folder: 100,
|
||||
incremental_sync: true,
|
||||
include_subfolders: true,
|
||||
};
|
||||
|
||||
function getFileIconFromName(fileName: string, className: string = "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 existingFolders =
|
||||
(connector.config?.selected_folders as SelectedFolder[] | undefined) || [];
|
||||
const existingFiles = (connector.config?.selected_files as SelectedFolder[] | undefined) || [];
|
||||
const existingIndexingOptions =
|
||||
(connector.config?.indexing_options as IndexingOptions | undefined) || DEFAULT_INDEXING_OPTIONS;
|
||||
|
||||
const [selectedFolders, setSelectedFolders] = useState<SelectedFolder[]>(existingFolders);
|
||||
const [selectedFiles, setSelectedFiles] = useState<SelectedFolder[]>(existingFiles);
|
||||
const [indexingOptions, setIndexingOptions] = useState<IndexingOptions>(existingIndexingOptions);
|
||||
const [authError, setAuthError] = useState(false);
|
||||
|
||||
const isAuthExpired = connector.config?.auth_expired === true || authError;
|
||||
|
||||
const handleAuthError = useCallback(() => {
|
||||
setAuthError(true);
|
||||
}, []);
|
||||
|
||||
const fetchItems = useCallback(
|
||||
async (parentId?: string) => {
|
||||
return connectorsApiService.listOneDriveFolders({
|
||||
connector_id: connector.id,
|
||||
parent_id: parentId,
|
||||
});
|
||||
},
|
||||
[connector.id]
|
||||
);
|
||||
|
||||
const [isEditMode] = useState(() => existingFolders.length > 0 || existingFiles.length > 0);
|
||||
const [isFolderTreeOpen, setIsFolderTreeOpen] = useState(!isEditMode);
|
||||
|
||||
useEffect(() => {
|
||||
const folders = (connector.config?.selected_folders as SelectedFolder[] | undefined) || [];
|
||||
const files = (connector.config?.selected_files as SelectedFolder[] | undefined) || [];
|
||||
const options =
|
||||
(connector.config?.indexing_options as IndexingOptions | undefined) ||
|
||||
DEFAULT_INDEXING_OPTIONS;
|
||||
setSelectedFolders(folders);
|
||||
setSelectedFiles(files);
|
||||
setIndexingOptions(options);
|
||||
}, [connector.config]);
|
||||
|
||||
const updateConfig = (
|
||||
folders: SelectedFolder[],
|
||||
files: SelectedFolder[],
|
||||
options: IndexingOptions
|
||||
) => {
|
||||
if (onConfigChange) {
|
||||
onConfigChange({
|
||||
...connector.config,
|
||||
selected_folders: folders,
|
||||
selected_files: files,
|
||||
indexing_options: options,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectFolders = (folders: SelectedFolder[]) => {
|
||||
setSelectedFolders(folders);
|
||||
updateConfig(folders, selectedFiles, indexingOptions);
|
||||
};
|
||||
|
||||
const handleSelectFiles = (files: SelectedFolder[]) => {
|
||||
setSelectedFiles(files);
|
||||
updateConfig(selectedFolders, files, indexingOptions);
|
||||
};
|
||||
|
||||
const handleIndexingOptionChange = (key: keyof IndexingOptions, value: number | boolean) => {
|
||||
const newOptions = { ...indexingOptions, [key]: value };
|
||||
setIndexingOptions(newOptions);
|
||||
updateConfig(selectedFolders, selectedFiles, newOptions);
|
||||
};
|
||||
|
||||
const handleRemoveFolder = (folderId: string) => {
|
||||
const newFolders = selectedFolders.filter((folder) => folder.id !== folderId);
|
||||
setSelectedFolders(newFolders);
|
||||
updateConfig(newFolders, selectedFiles, indexingOptions);
|
||||
};
|
||||
|
||||
const handleRemoveFile = (fileId: string) => {
|
||||
const newFiles = selectedFiles.filter((file) => file.id !== fileId);
|
||||
setSelectedFiles(newFiles);
|
||||
updateConfig(selectedFolders, newFiles, indexingOptions);
|
||||
};
|
||||
|
||||
const totalSelected = selectedFolders.length + selectedFiles.length;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 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">
|
||||
Select specific folders and/or individual 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" : ""}: {(() => {
|
||||
const parts: string[] = [];
|
||||
if (selectedFolders.length > 0) {
|
||||
parts.push(
|
||||
`${selectedFolders.length} folder${selectedFolders.length > 1 ? "s" : ""}`
|
||||
);
|
||||
}
|
||||
if (selectedFiles.length > 0) {
|
||||
parts.push(`${selectedFiles.length} file${selectedFiles.length > 1 ? "s" : ""}`);
|
||||
}
|
||||
return parts.length > 0 ? `(${parts.join(", ")})` : "";
|
||||
})()}
|
||||
</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 sm:text-sm 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"
|
||||
aria-label={`Remove ${folder.name}`}
|
||||
>
|
||||
<X className="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
{selectedFiles.map((file) => (
|
||||
<div
|
||||
key={file.id}
|
||||
className="text-xs sm:text-sm 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"
|
||||
aria-label={`Remove ${file.name}`}
|
||||
>
|
||||
<X className="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</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>
|
||||
)}
|
||||
|
||||
{isEditMode ? (
|
||||
<div className="space-y-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsFolderTreeOpen(!isFolderTreeOpen)}
|
||||
className="flex items-center gap-2 text-xs sm:text-sm text-muted-foreground hover:text-foreground transition-colors w-fit"
|
||||
>
|
||||
Change Selection
|
||||
{isFolderTreeOpen ? (
|
||||
<ChevronDown className="size-4" />
|
||||
) : (
|
||||
<ChevronRight className="size-4" />
|
||||
)}
|
||||
</button>
|
||||
{isFolderTreeOpen && (
|
||||
<DriveFolderTree
|
||||
fetchItems={fetchItems}
|
||||
selectedFolders={selectedFolders}
|
||||
onSelectFolders={handleSelectFolders}
|
||||
selectedFiles={selectedFiles}
|
||||
onSelectFiles={handleSelectFiles}
|
||||
onAuthError={handleAuthError}
|
||||
rootLabel="OneDrive"
|
||||
providerName="OneDrive"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<DriveFolderTree
|
||||
fetchItems={fetchItems}
|
||||
selectedFolders={selectedFolders}
|
||||
onSelectFolders={handleSelectFolders}
|
||||
selectedFiles={selectedFiles}
|
||||
onSelectFiles={handleSelectFiles}
|
||||
onAuthError={handleAuthError}
|
||||
rootLabel="OneDrive"
|
||||
providerName="OneDrive"
|
||||
/>
|
||||
)}
|
||||
</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>
|
||||
|
||||
{/* Max files per folder */}
|
||||
<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" className="text-xs sm:text-sm">
|
||||
50 files
|
||||
</SelectItem>
|
||||
<SelectItem value="100" className="text-xs sm:text-sm">
|
||||
100 files
|
||||
</SelectItem>
|
||||
<SelectItem value="250" className="text-xs sm:text-sm">
|
||||
250 files
|
||||
</SelectItem>
|
||||
<SelectItem value="500" className="text-xs sm:text-sm">
|
||||
500 files
|
||||
</SelectItem>
|
||||
<SelectItem value="1000" className="text-xs sm:text-sm">
|
||||
1000 files
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Incremental sync toggle */}
|
||||
<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>
|
||||
|
||||
{/* Include subfolders toggle */}
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export const CONNECTOR_TO_DOCUMENT_TYPE: Record<string, string> = {
|
|||
// Direct mappings (connector type matches document type)
|
||||
SLACK_CONNECTOR: "SLACK_CONNECTOR",
|
||||
TEAMS_CONNECTOR: "TEAMS_CONNECTOR",
|
||||
ONEDRIVE_CONNECTOR: "ONEDRIVE_FILE",
|
||||
NOTION_CONNECTOR: "NOTION_CONNECTOR",
|
||||
GITHUB_CONNECTOR: "GITHUB_CONNECTOR",
|
||||
LINEAR_CONNECTOR: "LINEAR_CONNECTOR",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
};
|
||||
|
|
|
|||
|
|
@ -394,7 +394,7 @@ const defaultComponents = memoizeMarkdownComponents({
|
|||
if (!isCodeBlock) {
|
||||
return (
|
||||
<code
|
||||
className={cn("aui-md-inline-code rounded border bg-muted font-semibold", className)}
|
||||
className={cn("aui-md-inline-code rounded-md border bg-muted px-1.5 py-0.5 font-mono text-[0.9em] font-normal", className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -1267,6 +1267,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"],
|
||||
|
|
|
|||
|
|
@ -12,15 +12,13 @@ import {
|
|||
Image,
|
||||
Presentation,
|
||||
} from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { useComposioDriveFolders } from "@/hooks/use-composio-drive-folders";
|
||||
import { connectorsApiService } from "@/lib/apis/connectors-api.service";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface DriveItem {
|
||||
export interface DriveItem {
|
||||
id: string;
|
||||
name: string;
|
||||
mimeType: string;
|
||||
|
|
@ -32,73 +30,92 @@ interface DriveItem {
|
|||
|
||||
interface ItemTreeNode {
|
||||
item: DriveItem;
|
||||
children: DriveItem[] | null; // null = not loaded, [] = loaded but empty
|
||||
children: DriveItem[] | null;
|
||||
isExpanded: boolean;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
interface SelectedFolder {
|
||||
export interface SelectedFolder {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface ComposioDriveFolderTreeProps {
|
||||
connectorId: number;
|
||||
interface DriveFolderTreeProps {
|
||||
fetchItems: (parentId?: string) => Promise<{ items: DriveItem[] }>;
|
||||
selectedFolders: SelectedFolder[];
|
||||
onSelectFolders: (folders: SelectedFolder[]) => void;
|
||||
selectedFiles?: SelectedFolder[];
|
||||
onSelectFiles?: (files: SelectedFolder[]) => void;
|
||||
onAuthError?: (message: string) => void;
|
||||
rootLabel?: string;
|
||||
providerName?: string;
|
||||
}
|
||||
|
||||
// Helper to get appropriate icon for file type
|
||||
function getFileIcon(mimeType: string, className: string = "h-4 w-4") {
|
||||
if (mimeType.includes("spreadsheet") || mimeType.includes("excel")) {
|
||||
function getFileIcon(mimeType?: string, className: string = "h-4 w-4") {
|
||||
const type = mimeType ?? "";
|
||||
if (type.includes("spreadsheet") || type.includes("excel")) {
|
||||
return <FileSpreadsheet className={`${className} text-muted-foreground`} />;
|
||||
}
|
||||
if (mimeType.includes("presentation") || mimeType.includes("powerpoint")) {
|
||||
if (type.includes("presentation") || type.includes("powerpoint")) {
|
||||
return <Presentation className={`${className} text-muted-foreground`} />;
|
||||
}
|
||||
if (mimeType.includes("document") || mimeType.includes("word") || mimeType.includes("text")) {
|
||||
if (type.includes("document") || type.includes("word") || type.includes("text")) {
|
||||
return <FileText className={`${className} text-muted-foreground`} />;
|
||||
}
|
||||
if (mimeType.includes("image")) {
|
||||
if (type.includes("image")) {
|
||||
return <Image className={`${className} text-muted-foreground`} />;
|
||||
}
|
||||
return <File className={`${className} text-muted-foreground`} />;
|
||||
}
|
||||
|
||||
export function ComposioDriveFolderTree({
|
||||
connectorId,
|
||||
export function DriveFolderTree({
|
||||
fetchItems,
|
||||
selectedFolders,
|
||||
onSelectFolders,
|
||||
selectedFiles = [],
|
||||
onSelectFiles = () => {},
|
||||
onAuthError,
|
||||
}: ComposioDriveFolderTreeProps) {
|
||||
rootLabel = "My Drive",
|
||||
providerName = "Drive",
|
||||
}: DriveFolderTreeProps) {
|
||||
const [itemStates, setItemStates] = useState<Map<string, ItemTreeNode>>(new Map());
|
||||
|
||||
const {
|
||||
data: rootData,
|
||||
isLoading: isLoadingRoot,
|
||||
error: rootError,
|
||||
} = useComposioDriveFolders({
|
||||
connectorId,
|
||||
});
|
||||
const [rootItems, setRootItems] = useState<DriveItem[]>([]);
|
||||
const [isLoadingRoot, setIsLoadingRoot] = useState(true);
|
||||
const [rootError, setRootError] = useState<Error | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (rootError && onAuthError) {
|
||||
const msg = rootError instanceof Error ? rootError.message : String(rootError);
|
||||
if (
|
||||
msg.toLowerCase().includes("authentication expired") ||
|
||||
msg.toLowerCase().includes("re-authenticate")
|
||||
) {
|
||||
onAuthError(msg);
|
||||
}
|
||||
}
|
||||
}, [rootError, onAuthError]);
|
||||
let cancelled = false;
|
||||
setIsLoadingRoot(true);
|
||||
setRootError(null);
|
||||
|
||||
const rootItems = rootData?.items || [];
|
||||
fetchItems()
|
||||
.then((data) => {
|
||||
if (!cancelled) {
|
||||
setRootItems(data.items || []);
|
||||
setIsLoadingRoot(false);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
if (!cancelled) {
|
||||
const error = err instanceof Error ? err : new Error(String(err));
|
||||
setRootError(error);
|
||||
setIsLoadingRoot(false);
|
||||
if (onAuthError) {
|
||||
const msg = error.message;
|
||||
if (
|
||||
msg.toLowerCase().includes("authentication expired") ||
|
||||
msg.toLowerCase().includes("re-authenticate")
|
||||
) {
|
||||
onAuthError(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [fetchItems, onAuthError]);
|
||||
|
||||
const isFolderSelected = (folderId: string): boolean => {
|
||||
return selectedFolders.some((f) => f.id === folderId);
|
||||
|
|
@ -124,89 +141,81 @@ export function ComposioDriveFolderTree({
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Find an item by ID across all loaded items (root and nested).
|
||||
*/
|
||||
const findItem = (itemId: string): DriveItem | undefined => {
|
||||
const state = itemStates.get(itemId);
|
||||
if (state?.item) return state.item;
|
||||
const findItem = useCallback(
|
||||
(itemId: string): DriveItem | undefined => {
|
||||
const state = itemStates.get(itemId);
|
||||
if (state?.item) return state.item;
|
||||
|
||||
const rootItem = rootItems.find((item) => item.id === itemId);
|
||||
if (rootItem) return rootItem;
|
||||
const rootItem = rootItems.find((item) => item.id === itemId);
|
||||
if (rootItem) return rootItem;
|
||||
|
||||
for (const [, nodeState] of itemStates) {
|
||||
if (nodeState.children) {
|
||||
const found = nodeState.children.find((child) => child.id === itemId);
|
||||
if (found) return found;
|
||||
for (const [, nodeState] of itemStates) {
|
||||
if (nodeState.children) {
|
||||
const found = nodeState.children.find((child) => child.id === itemId);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
return undefined;
|
||||
},
|
||||
[itemStates, rootItems]
|
||||
);
|
||||
|
||||
const loadFolderContents = useCallback(
|
||||
async (folderId: string) => {
|
||||
try {
|
||||
setItemStates((prev) => {
|
||||
const newMap = new Map(prev);
|
||||
const existing = newMap.get(folderId);
|
||||
if (existing) {
|
||||
newMap.set(folderId, { ...existing, isLoading: true });
|
||||
} else {
|
||||
const item = findItem(folderId);
|
||||
if (item) {
|
||||
newMap.set(folderId, {
|
||||
item,
|
||||
children: null,
|
||||
isExpanded: false,
|
||||
isLoading: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
return newMap;
|
||||
});
|
||||
|
||||
const data = await fetchItems(folderId);
|
||||
const items = data.items || [];
|
||||
|
||||
setItemStates((prev) => {
|
||||
const newMap = new Map(prev);
|
||||
const existing = newMap.get(folderId);
|
||||
const item = existing?.item || findItem(folderId);
|
||||
|
||||
/**
|
||||
* Load and display contents of a specific folder.
|
||||
*/
|
||||
const loadFolderContents = async (folderId: string) => {
|
||||
try {
|
||||
setItemStates((prev) => {
|
||||
const newMap = new Map(prev);
|
||||
const existing = newMap.get(folderId);
|
||||
if (existing) {
|
||||
newMap.set(folderId, { ...existing, isLoading: true });
|
||||
} else {
|
||||
const item = findItem(folderId);
|
||||
if (item) {
|
||||
newMap.set(folderId, {
|
||||
item,
|
||||
children: null,
|
||||
isExpanded: false,
|
||||
isLoading: true,
|
||||
children: items,
|
||||
isExpanded: true,
|
||||
isLoading: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
return newMap;
|
||||
});
|
||||
return newMap;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error loading folder contents:", error);
|
||||
setItemStates((prev) => {
|
||||
const newMap = new Map(prev);
|
||||
const existing = newMap.get(folderId);
|
||||
if (existing) {
|
||||
newMap.set(folderId, { ...existing, isLoading: false });
|
||||
}
|
||||
return newMap;
|
||||
});
|
||||
}
|
||||
},
|
||||
[fetchItems, findItem]
|
||||
);
|
||||
|
||||
const data = await connectorsApiService.listComposioDriveFolders({
|
||||
connector_id: connectorId,
|
||||
parent_id: folderId,
|
||||
});
|
||||
const items = data.items || [];
|
||||
|
||||
setItemStates((prev) => {
|
||||
const newMap = new Map(prev);
|
||||
const existing = newMap.get(folderId);
|
||||
const item = existing?.item || findItem(folderId);
|
||||
|
||||
if (item) {
|
||||
newMap.set(folderId, {
|
||||
item,
|
||||
children: items,
|
||||
isExpanded: true,
|
||||
isLoading: false,
|
||||
});
|
||||
} else {
|
||||
console.error(`Could not find item for folderId: ${folderId}`);
|
||||
}
|
||||
return newMap;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error loading folder contents:", error);
|
||||
setItemStates((prev) => {
|
||||
const newMap = new Map(prev);
|
||||
const existing = newMap.get(folderId);
|
||||
if (existing) {
|
||||
newMap.set(folderId, { ...existing, isLoading: false });
|
||||
}
|
||||
return newMap;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggle folder expand/collapse state.
|
||||
*/
|
||||
const toggleFolder = async (item: DriveItem) => {
|
||||
if (!item.isFolder) return;
|
||||
|
||||
|
|
@ -226,9 +235,6 @@ export function ComposioDriveFolderTree({
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Render a single item (folder or file) with its children.
|
||||
*/
|
||||
const renderItem = (item: DriveItem, level: number = 0) => {
|
||||
const state = itemStates.get(item.id);
|
||||
const isExpanded = state?.isExpanded || false;
|
||||
|
|
@ -240,7 +246,7 @@ export function ComposioDriveFolderTree({
|
|||
const childFolders = children?.filter((c) => c.isFolder) || [];
|
||||
const childFiles = children?.filter((c) => !c.isFolder) || [];
|
||||
|
||||
const indentSize = 0.75; // Smaller indent for mobile
|
||||
const indentSize = 0.75;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -346,16 +352,16 @@ export function ComposioDriveFolderTree({
|
|||
<div className="flex items-center gap-1 sm:gap-2 h-auto py-1 sm:py-2 px-1 sm:px-2 rounded-md hover:bg-accent cursor-pointer">
|
||||
<Checkbox
|
||||
checked={isFolderSelected("root")}
|
||||
onCheckedChange={() => toggleFolderSelection("root", "My Drive")}
|
||||
onCheckedChange={() => toggleFolderSelection("root", rootLabel)}
|
||||
className="shrink-0 h-3.5 w-3.5 sm:h-4 sm:w-4 border-slate-400/20 dark:border-white/20"
|
||||
/>
|
||||
<HardDrive className="h-3 w-3 sm:h-4 sm:w-4 text-muted-foreground shrink-0" />
|
||||
<button
|
||||
type="button"
|
||||
className="font-semibold truncate text-xs sm:text-sm cursor-pointer bg-transparent border-0 p-0 text-left"
|
||||
onClick={() => toggleFolderSelection("root", "My Drive")}
|
||||
onClick={() => toggleFolderSelection("root", rootLabel)}
|
||||
>
|
||||
My Drive
|
||||
{rootLabel}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -372,17 +378,15 @@ export function ComposioDriveFolderTree({
|
|||
|
||||
{!isLoadingRoot && rootError && (
|
||||
<div className="text-center text-xs sm:text-sm text-amber-600 dark:text-amber-500 py-4 sm:py-8">
|
||||
{(rootError instanceof Error ? rootError.message : String(rootError)).includes(
|
||||
"authentication expired"
|
||||
)
|
||||
? "Google Drive authentication has expired. Please re-authenticate above."
|
||||
: "Failed to load Google Drive contents."}
|
||||
{rootError.message.includes("authentication expired")
|
||||
? `${providerName} authentication has expired. Please re-authenticate above.`
|
||||
: `Failed to load ${providerName} contents.`}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isLoadingRoot && !rootError && rootItems.length === 0 && (
|
||||
<div className="text-center text-xs sm:text-sm text-muted-foreground py-4 sm:py-8">
|
||||
No files or folders found in your Google Drive
|
||||
No files or folders found in your {providerName}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -84,7 +84,7 @@ export function CreateFolderDialog({
|
|||
/>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="flex-row justify-end gap-2 pt-2 sm:pt-3">
|
||||
<DialogFooter className="flex-row justify-end pt-2 sm:pt-3">
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
|
|
|
|||
|
|
@ -214,7 +214,7 @@ export const DocumentNode = React.memo(function DocumentNode({
|
|||
<MoreHorizontal className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-40">
|
||||
<DropdownMenuContent align="end" className="w-40" onClick={(e) => e.stopPropagation()}>
|
||||
<DropdownMenuItem onClick={() => onPreview(doc)}>
|
||||
<Eye className="mr-2 h-4 w-4" />
|
||||
Open
|
||||
|
|
@ -254,7 +254,7 @@ export const DocumentNode = React.memo(function DocumentNode({
|
|||
</ContextMenuTrigger>
|
||||
|
||||
{contextMenuOpen && (
|
||||
<ContextMenuContent className="w-40">
|
||||
<ContextMenuContent className="w-40" onClick={(e) => e.stopPropagation()}>
|
||||
<ContextMenuItem onClick={() => onPreview(doc)}>
|
||||
<Eye className="mr-2 h-4 w-4" />
|
||||
Open
|
||||
|
|
|
|||
|
|
@ -155,7 +155,7 @@ export function FolderPickerDialog({
|
|||
{renderPickerLevel(null, 1)}
|
||||
</div>
|
||||
|
||||
<DialogFooter className="flex-row justify-end gap-2 pt-2 sm:pt-3">
|
||||
<DialogFooter className="flex-row justify-end pt-2 sm:pt-3">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => onOpenChange(false)}
|
||||
|
|
|
|||
|
|
@ -238,9 +238,9 @@ export function FolderTreeView({
|
|||
|
||||
if (treeNodes.length === 0 && folders.length === 0 && documents.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-1 flex-col items-center justify-center gap-3 px-4 py-12 text-muted-foreground">
|
||||
<CirclePlus className="h-10 w-10 rotate-45" />
|
||||
<p className="text-sm">No documents yet</p>
|
||||
<div className="flex flex-1 flex-col items-center justify-center gap-1 px-4 py-12 text-muted-foreground">
|
||||
<p className="text-sm font-medium">No documents found</p>
|
||||
<p className="text-xs text-muted-foreground/70">Use the upload button or connect a source above</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -255,7 +255,7 @@ function MobileEditorDrawer() {
|
|||
shouldScaleBackground={false}
|
||||
>
|
||||
<DrawerContent
|
||||
className="h-[95vh] max-h-[95vh] z-80 bg-sidebar overflow-hidden"
|
||||
className="h-[90vh] max-h-[90vh] z-80 bg-sidebar overflow-hidden"
|
||||
overlayClassName="z-80"
|
||||
>
|
||||
<DrawerHandle />
|
||||
|
|
|
|||
|
|
@ -355,7 +355,7 @@ function MobileHitlEditDrawer() {
|
|||
shouldScaleBackground={false}
|
||||
>
|
||||
<DrawerContent
|
||||
className="h-[95vh] max-h-[95vh] z-80 bg-sidebar overflow-hidden"
|
||||
className="h-[90vh] max-h-[90vh] z-80 bg-sidebar overflow-hidden"
|
||||
overlayClassName="z-80"
|
||||
>
|
||||
<DrawerHandle />
|
||||
|
|
|
|||
|
|
@ -824,7 +824,7 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
|
|||
}
|
||||
}}
|
||||
/>
|
||||
<DialogFooter className="flex gap-2 sm:justify-end">
|
||||
<DialogFooter className="flex sm:justify-end">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => setShowRenameChatDialog(false)}
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@ export function CreateSearchSpaceDialog({ open, onOpenChange }: CreateSearchSpac
|
|||
)}
|
||||
/>
|
||||
|
||||
<DialogFooter className="flex-row justify-end gap-2 pt-2 sm:pt-3">
|
||||
<DialogFooter className="flex-row justify-end pt-2 sm:pt-3">
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
|
|
|
|||
|
|
@ -496,7 +496,7 @@ export function AllPrivateChatsSidebarContent({
|
|||
}
|
||||
}}
|
||||
/>
|
||||
<DialogFooter className="flex gap-2 sm:justify-end">
|
||||
<DialogFooter className="flex sm:justify-end">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => setShowRenameDialog(false)}
|
||||
|
|
|
|||
|
|
@ -496,7 +496,7 @@ export function AllSharedChatsSidebarContent({
|
|||
}
|
||||
}}
|
||||
/>
|
||||
<DialogFooter className="flex gap-2 sm:justify-end">
|
||||
<DialogFooter className="flex sm:justify-end">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => setShowRenameDialog(false)}
|
||||
|
|
|
|||
|
|
@ -7,12 +7,14 @@ import { useParams } from "next/navigation";
|
|||
import { useTranslations } from "next-intl";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { MarkdownViewer } from "@/components/markdown-viewer";
|
||||
import { DocumentsFilters } from "@/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsFilters";
|
||||
import { sidebarSelectedDocumentsAtom } from "@/atoms/chat/mentioned-documents.atom";
|
||||
import { connectorDialogOpenAtom } from "@/atoms/connector-dialog/connector-dialog.atoms";
|
||||
import { connectorsAtom } from "@/atoms/connectors/connector-query.atoms";
|
||||
import { deleteDocumentMutationAtom } from "@/atoms/documents/document-mutation.atoms";
|
||||
import { expandedFolderIdsAtom } from "@/atoms/documents/folder.atoms";
|
||||
import { rightPanelCollapsedAtom } from "@/atoms/layout/right-panel.atom";
|
||||
import { agentCreatedDocumentsAtom } from "@/atoms/documents/ui.atoms";
|
||||
import { openDocumentTabAtom } from "@/atoms/tabs/tabs.atom";
|
||||
import { CreateFolderDialog } from "@/components/documents/CreateFolderDialog";
|
||||
|
|
@ -33,13 +35,22 @@ import {
|
|||
} from "@/components/ui/alert-dialog";
|
||||
import { Avatar, AvatarFallback, AvatarGroup } from "@/components/ui/avatar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Drawer,
|
||||
DrawerContent,
|
||||
DrawerHandle,
|
||||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
} from "@/components/ui/drawer";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
||||
import type { DocumentTypeEnum } from "@/contracts/types/document.types";
|
||||
import { useDebouncedValue } from "@/hooks/use-debounced-value";
|
||||
import { useMediaQuery } from "@/hooks/use-media-query";
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
import { foldersApiService } from "@/lib/apis/folders-api.service";
|
||||
import { documentsApiService } from "@/lib/apis/documents-api.service";
|
||||
import { authenticatedFetch } from "@/lib/auth-utils";
|
||||
import { queries } from "@/zero/queries/index";
|
||||
import { SidebarSlideOutPanel } from "./SidebarSlideOutPanel";
|
||||
|
|
@ -83,10 +94,13 @@ export function DocumentsSidebar({
|
|||
const isMobile = !useMediaQuery("(min-width: 640px)");
|
||||
const searchSpaceId = Number(params.search_space_id);
|
||||
const setConnectorDialogOpen = useSetAtom(connectorDialogOpenAtom);
|
||||
const setRightPanelCollapsed = useSetAtom(rightPanelCollapsedAtom);
|
||||
const openDocumentTab = useSetAtom(openDocumentTabAtom);
|
||||
const { data: connectors } = useAtomValue(connectorsAtom);
|
||||
const connectorCount = connectors?.length ?? 0;
|
||||
|
||||
const isMobileLayout = useIsMobile();
|
||||
|
||||
const [search, setSearch] = useState("");
|
||||
const debouncedSearch = useDebouncedValue(search, 250);
|
||||
const [activeTypes, setActiveTypes] = useState<DocumentTypeEnum[]>([]);
|
||||
|
|
@ -360,6 +374,31 @@ export function DocumentsSidebar({
|
|||
[]
|
||||
);
|
||||
|
||||
// Document popup viewer state (for tree view "Open" and mobile preview)
|
||||
const [viewingDoc, setViewingDoc] = useState<DocumentNodeDoc | null>(null);
|
||||
const [viewingContent, setViewingContent] = useState<string>("");
|
||||
const [viewingLoading, setViewingLoading] = useState(false);
|
||||
|
||||
const handleViewDocumentPopup = useCallback(async (doc: DocumentNodeDoc) => {
|
||||
setViewingDoc(doc);
|
||||
setViewingLoading(true);
|
||||
try {
|
||||
const fullDoc = await documentsApiService.getDocument({ id: doc.id });
|
||||
setViewingContent(fullDoc.content);
|
||||
} catch (err) {
|
||||
console.error("[DocumentsSidebar] Failed to fetch document content:", err);
|
||||
setViewingContent("Failed to load document content.");
|
||||
} finally {
|
||||
setViewingLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleCloseViewer = useCallback(() => {
|
||||
setViewingDoc(null);
|
||||
setViewingContent("");
|
||||
setViewingLoading(false);
|
||||
}, []);
|
||||
|
||||
const handleToggleChatMention = useCallback(
|
||||
(doc: { id: number; title: string; document_type: string }, isMentioned: boolean) => {
|
||||
if (isMentioned) {
|
||||
|
|
@ -505,12 +544,16 @@ export function DocumentsSidebar({
|
|||
useEffect(() => {
|
||||
const handleEscape = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape" && open) {
|
||||
onOpenChange(false);
|
||||
if (isMobile) {
|
||||
onOpenChange(false);
|
||||
} else {
|
||||
setRightPanelCollapsed(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
document.addEventListener("keydown", handleEscape);
|
||||
return () => document.removeEventListener("keydown", handleEscape);
|
||||
}, [open, onOpenChange]);
|
||||
}, [open, onOpenChange, isMobile, setRightPanelCollapsed]);
|
||||
|
||||
const documentsContent = (
|
||||
<>
|
||||
|
|
@ -673,18 +716,24 @@ export function DocumentsSidebar({
|
|||
onCreateFolder={handleCreateFolder}
|
||||
searchQuery={debouncedSearch.trim() || undefined}
|
||||
onPreviewDocument={(doc) => {
|
||||
openDocumentTab({
|
||||
documentId: doc.id,
|
||||
searchSpaceId,
|
||||
title: doc.title,
|
||||
});
|
||||
if (isMobileLayout) {
|
||||
handleViewDocumentPopup(doc);
|
||||
} else {
|
||||
openDocumentTab({
|
||||
documentId: doc.id,
|
||||
searchSpaceId,
|
||||
title: doc.title,
|
||||
});
|
||||
}
|
||||
}}
|
||||
onEditDocument={(doc) => {
|
||||
openDocumentTab({
|
||||
documentId: doc.id,
|
||||
searchSpaceId,
|
||||
title: doc.title,
|
||||
});
|
||||
if (!isMobileLayout) {
|
||||
openDocumentTab({
|
||||
documentId: doc.id,
|
||||
searchSpaceId,
|
||||
title: doc.title,
|
||||
});
|
||||
}
|
||||
}}
|
||||
onDeleteDocument={(doc) => handleDeleteDocument(doc.id)}
|
||||
onMoveDocument={handleMoveDocument}
|
||||
|
|
@ -712,6 +761,26 @@ export function DocumentsSidebar({
|
|||
onConfirm={handleCreateFolderConfirm}
|
||||
/>
|
||||
|
||||
<Drawer open={!!viewingDoc} onOpenChange={(open) => !open && handleCloseViewer()}>
|
||||
<DrawerContent className="max-h-[85vh] flex flex-col">
|
||||
<DrawerHandle />
|
||||
<DrawerHeader className="text-left shrink-0">
|
||||
<DrawerTitle className="text-base leading-tight break-words">
|
||||
{viewingDoc?.title}
|
||||
</DrawerTitle>
|
||||
</DrawerHeader>
|
||||
<div className="overflow-y-auto flex-1 min-h-0 px-4 pb-6 select-text text-xs [&_h1]:text-base! [&_h1]:mt-3! [&_h2]:text-sm! [&_h2]:mt-2! [&_h3]:text-xs! [&_h3]:mt-2!">
|
||||
{viewingLoading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Spinner size="lg" className="text-muted-foreground" />
|
||||
</div>
|
||||
) : (
|
||||
<MarkdownViewer content={viewingContent} />
|
||||
)}
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
<AlertDialog
|
||||
open={bulkDeleteConfirmOpen}
|
||||
onOpenChange={(open) => !open && !isBulkDeleting && setBulkDeleteConfirmOpen(false)}
|
||||
|
|
|
|||
|
|
@ -455,7 +455,7 @@ function MobileReportDrawer() {
|
|||
shouldScaleBackground={false}
|
||||
>
|
||||
<DrawerContent
|
||||
className="h-[95vh] max-h-[95vh] z-80 bg-sidebar overflow-hidden"
|
||||
className="h-[90vh] max-h-[90vh] z-80 bg-sidebar overflow-hidden"
|
||||
overlayClassName="z-80"
|
||||
>
|
||||
<DrawerHandle />
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ export {
|
|||
export { GeneratePodcastToolUI } from "./generate-podcast";
|
||||
export { GenerateReportToolUI } from "./generate-report";
|
||||
export { CreateGoogleDriveFileToolUI, DeleteGoogleDriveFileToolUI } from "./google-drive";
|
||||
export { CreateOneDriveFileToolUI, DeleteOneDriveFileToolUI } from "./onedrive";
|
||||
export {
|
||||
Image,
|
||||
ImageErrorBoundary,
|
||||
|
|
|
|||
403
surfsense_web/components/tool-ui/onedrive/create-file.tsx
Normal file
403
surfsense_web/components/tool-ui/onedrive/create-file.tsx
Normal file
|
|
@ -0,0 +1,403 @@
|
|||
"use client";
|
||||
|
||||
import type { ToolCallMessagePartProps } from "@assistant-ui/react";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { CornerDownLeftIcon, FileIcon, Pen } from "lucide-react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
|
||||
import { PlateEditor } from "@/components/editor/plate-editor";
|
||||
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { useHitlPhase } from "@/hooks/use-hitl-phase";
|
||||
|
||||
interface OneDriveAccount {
|
||||
id: number;
|
||||
name: string;
|
||||
user_email?: string;
|
||||
auth_expired?: boolean;
|
||||
}
|
||||
|
||||
interface InterruptResult {
|
||||
__interrupt__: true;
|
||||
__decided__?: "approve" | "reject" | "edit";
|
||||
__completed__?: boolean;
|
||||
action_requests: Array<{ name: string; args: Record<string, unknown> }>;
|
||||
review_configs: Array<{
|
||||
action_name: string;
|
||||
allowed_decisions: Array<"approve" | "edit" | "reject">;
|
||||
}>;
|
||||
context?: {
|
||||
accounts?: OneDriveAccount[];
|
||||
parent_folders?: Record<number, Array<{ folder_id: string; name: string }>>;
|
||||
error?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface SuccessResult {
|
||||
status: "success";
|
||||
file_id: string;
|
||||
name: string;
|
||||
web_url?: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
interface ErrorResult {
|
||||
status: "error";
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface AuthErrorResult {
|
||||
status: "auth_error";
|
||||
message: string;
|
||||
connector_type?: string;
|
||||
}
|
||||
|
||||
type CreateOneDriveFileResult = InterruptResult | SuccessResult | ErrorResult | AuthErrorResult;
|
||||
|
||||
function isInterruptResult(result: unknown): result is InterruptResult {
|
||||
return (
|
||||
typeof result === "object" &&
|
||||
result !== null &&
|
||||
"__interrupt__" in result &&
|
||||
(result as InterruptResult).__interrupt__ === true
|
||||
);
|
||||
}
|
||||
|
||||
function isErrorResult(result: unknown): result is ErrorResult {
|
||||
return (
|
||||
typeof result === "object" &&
|
||||
result !== null &&
|
||||
"status" in result &&
|
||||
(result as ErrorResult).status === "error"
|
||||
);
|
||||
}
|
||||
|
||||
function isAuthErrorResult(result: unknown): result is AuthErrorResult {
|
||||
return (
|
||||
typeof result === "object" &&
|
||||
result !== null &&
|
||||
"status" in result &&
|
||||
(result as AuthErrorResult).status === "auth_error"
|
||||
);
|
||||
}
|
||||
|
||||
function ApprovalCard({
|
||||
args,
|
||||
interruptData,
|
||||
onDecision,
|
||||
}: {
|
||||
args: { name: string; content?: string };
|
||||
interruptData: InterruptResult;
|
||||
onDecision: (decision: {
|
||||
type: "approve" | "reject" | "edit";
|
||||
message?: string;
|
||||
edited_action?: { name: string; args: Record<string, unknown> };
|
||||
}) => void;
|
||||
}) {
|
||||
const { phase, setProcessing, setRejected } = useHitlPhase(interruptData);
|
||||
const [isPanelOpen, setIsPanelOpen] = useState(false);
|
||||
const openHitlEditPanel = useSetAtom(openHitlEditPanelAtom);
|
||||
const [pendingEdits, setPendingEdits] = useState<{ name: string; content: string } | null>(null);
|
||||
|
||||
const accounts = interruptData.context?.accounts ?? [];
|
||||
const validAccounts = accounts.filter((a) => !a.auth_expired);
|
||||
const expiredAccounts = accounts.filter((a) => a.auth_expired);
|
||||
|
||||
const defaultAccountId = useMemo(() => {
|
||||
if (validAccounts.length === 1) return String(validAccounts[0].id);
|
||||
return "";
|
||||
}, [validAccounts]);
|
||||
|
||||
const [selectedAccountId, setSelectedAccountId] = useState<string>(defaultAccountId);
|
||||
const [parentFolderId, setParentFolderId] = useState<string>("__root__");
|
||||
|
||||
const parentFolders = interruptData.context?.parent_folders ?? {};
|
||||
const availableParentFolders = useMemo(() => {
|
||||
if (!selectedAccountId) return [];
|
||||
return parentFolders[Number(selectedAccountId)] ?? [];
|
||||
}, [selectedAccountId, parentFolders]);
|
||||
|
||||
const handleAccountChange = useCallback((value: string) => {
|
||||
setSelectedAccountId(value);
|
||||
setParentFolderId("__root__");
|
||||
}, []);
|
||||
|
||||
const isNameValid = useMemo(() => {
|
||||
const name = pendingEdits?.name ?? args.name;
|
||||
return name && typeof name === "string" && name.trim().length > 0;
|
||||
}, [pendingEdits?.name, args.name]);
|
||||
|
||||
const canApprove = !!selectedAccountId && isNameValid;
|
||||
const reviewConfig = interruptData.review_configs?.[0];
|
||||
const allowedDecisions = reviewConfig?.allowed_decisions ?? ["approve", "reject"];
|
||||
const canEdit = allowedDecisions.includes("edit");
|
||||
|
||||
const handleApprove = useCallback(() => {
|
||||
if (phase !== "pending" || isPanelOpen || !canApprove) return;
|
||||
if (!allowedDecisions.includes("approve")) return;
|
||||
const isEdited = pendingEdits !== null;
|
||||
setProcessing();
|
||||
onDecision({
|
||||
type: isEdited ? "edit" : "approve",
|
||||
edited_action: {
|
||||
name: interruptData.action_requests[0].name,
|
||||
args: {
|
||||
...args,
|
||||
...(pendingEdits && { name: pendingEdits.name, content: pendingEdits.content }),
|
||||
connector_id: selectedAccountId ? Number(selectedAccountId) : null,
|
||||
parent_folder_id: parentFolderId === "__root__" ? null : parentFolderId,
|
||||
},
|
||||
},
|
||||
});
|
||||
}, [
|
||||
phase,
|
||||
setProcessing,
|
||||
isPanelOpen,
|
||||
canApprove,
|
||||
allowedDecisions,
|
||||
onDecision,
|
||||
interruptData,
|
||||
args,
|
||||
selectedAccountId,
|
||||
parentFolderId,
|
||||
pendingEdits,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
if (e.key === "Enter" && !e.shiftKey && !e.ctrlKey && !e.metaKey) handleApprove();
|
||||
};
|
||||
window.addEventListener("keydown", handler);
|
||||
return () => window.removeEventListener("keydown", handler);
|
||||
}, [handleApprove]);
|
||||
|
||||
return (
|
||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 transition-all duration-300">
|
||||
{/* Header */}
|
||||
<div className="flex items-start justify-between px-5 pt-5 pb-4 select-none">
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-foreground">
|
||||
{phase === "rejected"
|
||||
? "Word Document Rejected"
|
||||
: phase === "processing" || phase === "complete"
|
||||
? "Word Document Approved"
|
||||
: "Create Word Document"}
|
||||
</p>
|
||||
{phase === "processing" ? (
|
||||
<TextShimmerLoader
|
||||
text={pendingEdits ? "Creating file with your changes" : "Creating file"}
|
||||
size="sm"
|
||||
/>
|
||||
) : phase === "complete" ? (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{pendingEdits ? "File created with your changes" : "File created"}
|
||||
</p>
|
||||
) : phase === "rejected" ? (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">File creation was cancelled</p>
|
||||
) : (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
Requires your approval to proceed
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{phase === "pending" && canEdit && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="rounded-lg text-muted-foreground -mt-1 -mr-2"
|
||||
onClick={() => {
|
||||
setIsPanelOpen(true);
|
||||
openHitlEditPanel({
|
||||
title: pendingEdits?.name ?? args.name ?? "",
|
||||
content: pendingEdits?.content ?? args.content ?? "",
|
||||
toolName: "Word Document",
|
||||
onSave: (newName, newContent) => {
|
||||
setIsPanelOpen(false);
|
||||
setPendingEdits({ name: newName, content: newContent });
|
||||
},
|
||||
onClose: () => setIsPanelOpen(false),
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Pen className="size-3.5" />
|
||||
Edit
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Context section — pickers in pending */}
|
||||
{phase === "pending" && interruptData.context && (
|
||||
<>
|
||||
<div className="mx-5 h-px bg-border/50" />
|
||||
<div className="px-5 py-4 space-y-4 select-none">
|
||||
{interruptData.context.error ? (
|
||||
<p className="text-sm text-destructive">{interruptData.context.error}</p>
|
||||
) : (
|
||||
<>
|
||||
{accounts.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs font-medium text-muted-foreground">
|
||||
OneDrive Account <span className="text-destructive">*</span>
|
||||
</p>
|
||||
<Select value={selectedAccountId} onValueChange={handleAccountChange}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Select an account" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{validAccounts.map((account) => (
|
||||
<SelectItem key={account.id} value={String(account.id)}>
|
||||
{account.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
{expiredAccounts.map((a) => (
|
||||
<div
|
||||
key={a.id}
|
||||
className="relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 px-2 text-sm select-none opacity-50 pointer-events-none"
|
||||
>
|
||||
{a.name} (expired, retry after re-auth)
|
||||
</div>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs font-medium text-muted-foreground">
|
||||
File Type
|
||||
</p>
|
||||
<Select value="docx" disabled>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="docx">Word Document (.docx)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{selectedAccountId && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs font-medium text-muted-foreground">Parent Folder</p>
|
||||
<Select value={parentFolderId} onValueChange={setParentFolderId}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="OneDrive Root" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__root__">OneDrive Root</SelectItem>
|
||||
{availableParentFolders.map((folder) => (
|
||||
<SelectItem key={folder.folder_id} value={folder.folder_id}>
|
||||
{folder.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{availableParentFolders.length === 0 && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
No folders found. File will be created at OneDrive root.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="mx-5 h-px bg-border/50" />
|
||||
<div className="px-5 pt-3">
|
||||
{(pendingEdits?.name ?? args.name) != null && (
|
||||
<p className="text-sm font-medium text-foreground">{String(pendingEdits?.name ?? args.name)}</p>
|
||||
)}
|
||||
{(pendingEdits?.content ?? args.content) != null && (
|
||||
<div className="mt-2 max-h-[7rem] overflow-hidden text-sm" style={{ maskImage: "linear-gradient(to bottom, black 50%, transparent 100%)", WebkitMaskImage: "linear-gradient(to bottom, black 50%, transparent 100%)" }}>
|
||||
<PlateEditor markdown={String(pendingEdits?.content ?? args.content)} readOnly preset="readonly" editorVariant="none" className="h-auto [&_[data-slate-editor]]:!min-h-0 [&_[data-slate-editor]>*:first-child]:!mt-0" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{phase === "pending" && (
|
||||
<>
|
||||
<div className="mx-5 h-px bg-border/50" />
|
||||
<div className="px-5 py-4 flex items-center gap-2 select-none">
|
||||
{allowedDecisions.includes("approve") && (
|
||||
<Button size="sm" className="rounded-lg gap-1.5" onClick={handleApprove} disabled={!canApprove || isPanelOpen}>
|
||||
Approve <CornerDownLeftIcon className="size-3 opacity-60" />
|
||||
</Button>
|
||||
)}
|
||||
{allowedDecisions.includes("reject") && (
|
||||
<Button size="sm" variant="ghost" className="rounded-lg text-muted-foreground" disabled={isPanelOpen} onClick={() => { setRejected(); onDecision({ type: "reject", message: "User rejected the action." }); }}>
|
||||
Reject
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ErrorCard({ result }: { result: ErrorResult }) {
|
||||
return (
|
||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
||||
<div className="px-5 pt-5 pb-4">
|
||||
<p className="text-sm font-semibold text-destructive">Failed to create OneDrive file</p>
|
||||
</div>
|
||||
<div className="mx-5 h-px bg-border/50" />
|
||||
<div className="px-5 py-4"><p className="text-sm text-muted-foreground">{result.message}</p></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AuthErrorCard({ result }: { result: AuthErrorResult }) {
|
||||
return (
|
||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
||||
<div className="px-5 pt-5 pb-4">
|
||||
<p className="text-sm font-semibold text-destructive">OneDrive authentication expired</p>
|
||||
</div>
|
||||
<div className="mx-5 h-px bg-border/50" />
|
||||
<div className="px-5 py-4"><p className="text-sm text-muted-foreground">{result.message}</p></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SuccessCard({ result }: { result: SuccessResult }) {
|
||||
return (
|
||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
||||
<div className="px-5 pt-5 pb-4">
|
||||
<p className="text-sm font-semibold text-foreground">{result.message || "OneDrive file created successfully"}</p>
|
||||
</div>
|
||||
<div className="mx-5 h-px bg-border/50" />
|
||||
<div className="px-5 py-4 space-y-2 text-xs">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<FileIcon className="size-3.5 text-muted-foreground" />
|
||||
<span className="font-medium">{result.name}</span>
|
||||
</div>
|
||||
{result.web_url && (
|
||||
<div>
|
||||
<a href={result.web_url} target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">Open in OneDrive</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const CreateOneDriveFileToolUI = ({ args, result }: ToolCallMessagePartProps<{ name: string; content?: string }, CreateOneDriveFileResult>) => {
|
||||
if (!result) return null;
|
||||
if (isInterruptResult(result)) {
|
||||
return <ApprovalCard args={args} interruptData={result} onDecision={(decision) => { window.dispatchEvent(new CustomEvent("hitl-decision", { detail: { decisions: [decision] } })); }} />;
|
||||
}
|
||||
if (typeof result === "object" && result !== null && "status" in result && (result as { status: string }).status === "rejected") return null;
|
||||
if (isAuthErrorResult(result)) return <AuthErrorCard result={result} />;
|
||||
if (isErrorResult(result)) return <ErrorCard result={result} />;
|
||||
return <SuccessCard result={result as SuccessResult} />;
|
||||
};
|
||||
2
surfsense_web/components/tool-ui/onedrive/index.ts
Normal file
2
surfsense_web/components/tool-ui/onedrive/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export { CreateOneDriveFileToolUI } from "./create-file";
|
||||
export { DeleteOneDriveFileToolUI } from "./trash-file";
|
||||
219
surfsense_web/components/tool-ui/onedrive/trash-file.tsx
Normal file
219
surfsense_web/components/tool-ui/onedrive/trash-file.tsx
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
"use client";
|
||||
|
||||
import type { ToolCallMessagePartProps } from "@assistant-ui/react";
|
||||
import { CornerDownLeftIcon, InfoIcon } from "lucide-react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { useHitlPhase } from "@/hooks/use-hitl-phase";
|
||||
|
||||
interface OneDriveAccount {
|
||||
id: number;
|
||||
name: string;
|
||||
user_email?: string;
|
||||
auth_expired?: boolean;
|
||||
}
|
||||
|
||||
interface OneDriveFile {
|
||||
file_id: string;
|
||||
name: string;
|
||||
document_id?: number;
|
||||
web_url?: string;
|
||||
}
|
||||
|
||||
interface InterruptResult {
|
||||
__interrupt__: true;
|
||||
__decided__?: "approve" | "reject";
|
||||
__completed__?: boolean;
|
||||
action_requests: Array<{ name: string; args: Record<string, unknown> }>;
|
||||
review_configs: Array<{ action_name: string; allowed_decisions: Array<"approve" | "reject"> }>;
|
||||
context?: { account?: OneDriveAccount; file?: OneDriveFile; error?: string };
|
||||
}
|
||||
|
||||
interface SuccessResult { status: "success"; file_id: string; message?: string; deleted_from_kb?: boolean }
|
||||
interface ErrorResult { status: "error"; message: string }
|
||||
interface NotFoundResult { status: "not_found"; message: string }
|
||||
interface AuthErrorResult { status: "auth_error"; message: string; connector_type?: string }
|
||||
|
||||
type DeleteOneDriveFileResult = InterruptResult | SuccessResult | ErrorResult | NotFoundResult | AuthErrorResult;
|
||||
|
||||
function isInterruptResult(result: unknown): result is InterruptResult {
|
||||
return typeof result === "object" && result !== null && "__interrupt__" in result && (result as InterruptResult).__interrupt__ === true;
|
||||
}
|
||||
function isErrorResult(result: unknown): result is ErrorResult {
|
||||
return typeof result === "object" && result !== null && "status" in result && (result as ErrorResult).status === "error";
|
||||
}
|
||||
function isNotFoundResult(result: unknown): result is NotFoundResult {
|
||||
return typeof result === "object" && result !== null && "status" in result && (result as NotFoundResult).status === "not_found";
|
||||
}
|
||||
function isAuthErrorResult(result: unknown): result is AuthErrorResult {
|
||||
return typeof result === "object" && result !== null && "status" in result && (result as AuthErrorResult).status === "auth_error";
|
||||
}
|
||||
|
||||
function ApprovalCard({ interruptData, onDecision }: {
|
||||
interruptData: InterruptResult;
|
||||
onDecision: (decision: { type: "approve" | "reject"; message?: string; edited_action?: { name: string; args: Record<string, unknown> } }) => void;
|
||||
}) {
|
||||
const { phase, setProcessing, setRejected } = useHitlPhase(interruptData);
|
||||
const [deleteFromKb, setDeleteFromKb] = useState(false);
|
||||
|
||||
const context = interruptData.context;
|
||||
const account = context?.account;
|
||||
const file = context?.file;
|
||||
|
||||
const handleApprove = useCallback(() => {
|
||||
if (phase !== "pending") return;
|
||||
setProcessing();
|
||||
onDecision({
|
||||
type: "approve",
|
||||
edited_action: {
|
||||
name: interruptData.action_requests[0].name,
|
||||
args: { file_id: file?.file_id, connector_id: account?.id, delete_from_kb: deleteFromKb },
|
||||
},
|
||||
});
|
||||
}, [phase, setProcessing, onDecision, interruptData, file?.file_id, account?.id, deleteFromKb]);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
if (e.key === "Enter" && !e.shiftKey && !e.ctrlKey && !e.metaKey) handleApprove();
|
||||
};
|
||||
window.addEventListener("keydown", handler);
|
||||
return () => window.removeEventListener("keydown", handler);
|
||||
}, [handleApprove]);
|
||||
|
||||
return (
|
||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 transition-all duration-300">
|
||||
<div className="flex items-start justify-between px-5 pt-5 pb-4 select-none">
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-foreground">
|
||||
{phase === "rejected" ? "OneDrive File Deletion Rejected" : phase === "processing" || phase === "complete" ? "OneDrive File Deletion Approved" : "Delete OneDrive File"}
|
||||
</p>
|
||||
{phase === "processing" ? (
|
||||
<TextShimmerLoader text="Trashing file" size="sm" />
|
||||
) : phase === "complete" ? (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">File trashed</p>
|
||||
) : phase === "rejected" ? (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">File deletion was cancelled</p>
|
||||
) : (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">Requires your approval to proceed</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{phase !== "rejected" && context && (
|
||||
<>
|
||||
<div className="mx-5 h-px bg-border/50" />
|
||||
<div className="px-5 py-4 space-y-4 select-none">
|
||||
{context.error ? (
|
||||
<p className="text-sm text-destructive">{context.error}</p>
|
||||
) : (
|
||||
<>
|
||||
{account && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs font-medium text-muted-foreground">OneDrive Account</p>
|
||||
<div className="w-full rounded-md border border-input bg-muted/50 px-3 py-2 text-sm">{account.name}</div>
|
||||
</div>
|
||||
)}
|
||||
{file && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs font-medium text-muted-foreground">File to Delete</p>
|
||||
<div className="w-full rounded-md border border-input bg-muted/50 px-3 py-2 text-sm space-y-0.5">
|
||||
<div className="font-medium">{file.name}</div>
|
||||
{file.web_url && (
|
||||
<a href={file.web_url} target="_blank" rel="noopener noreferrer" className="text-xs text-primary hover:underline">Open in OneDrive</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{phase === "pending" && (
|
||||
<>
|
||||
<div className="mx-5 h-px bg-border/50" />
|
||||
<div className="px-5 py-4 space-y-3 select-none">
|
||||
<p className="text-xs text-muted-foreground">The file will be moved to the OneDrive recycle bin. You can restore it within 93 days.</p>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<Checkbox id="od-delete-from-kb" checked={deleteFromKb} onCheckedChange={(v) => setDeleteFromKb(v === true)} className="shrink-0" />
|
||||
<label htmlFor="od-delete-from-kb" className="flex-1 cursor-pointer">
|
||||
<span className="text-sm text-foreground">Also remove from knowledge base</span>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">This will permanently delete the file from your knowledge base</p>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{phase === "pending" && (
|
||||
<>
|
||||
<div className="mx-5 h-px bg-border/50" />
|
||||
<div className="px-5 py-4 flex items-center gap-2 select-none">
|
||||
<Button size="sm" className="rounded-lg gap-1.5" onClick={handleApprove}>Approve <CornerDownLeftIcon className="size-3 opacity-60" /></Button>
|
||||
<Button size="sm" variant="ghost" className="rounded-lg text-muted-foreground" onClick={() => { setRejected(); onDecision({ type: "reject", message: "User rejected the action." }); }}>Reject</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ErrorCard({ result }: { result: ErrorResult }) {
|
||||
return (
|
||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
||||
<div className="px-5 pt-5 pb-4"><p className="text-sm font-semibold text-destructive">Failed to delete file</p></div>
|
||||
<div className="mx-5 h-px bg-border/50" />
|
||||
<div className="px-5 py-4"><p className="text-sm text-muted-foreground">{result.message}</p></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NotFoundCard({ result }: { result: NotFoundResult }) {
|
||||
return (
|
||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
||||
<div className="flex items-start gap-3 px-5 py-4">
|
||||
<InfoIcon className="size-4 mt-0.5 shrink-0 text-muted-foreground" />
|
||||
<p className="text-sm text-muted-foreground">{result.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AuthErrorCard({ result }: { result: AuthErrorResult }) {
|
||||
return (
|
||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
||||
<div className="px-5 pt-5 pb-4"><p className="text-sm font-semibold text-destructive">OneDrive authentication expired</p></div>
|
||||
<div className="mx-5 h-px bg-border/50" />
|
||||
<div className="px-5 py-4"><p className="text-sm text-muted-foreground">{result.message}</p></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SuccessCard({ result }: { result: SuccessResult }) {
|
||||
return (
|
||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
||||
<div className="px-5 pt-5 pb-4"><p className="text-sm font-semibold text-foreground">{result.message || "File moved to recycle bin"}</p></div>
|
||||
{result.deleted_from_kb && (
|
||||
<>
|
||||
<div className="mx-5 h-px bg-border/50" />
|
||||
<div className="px-5 py-4 text-xs"><span className="text-green-600 dark:text-green-500">Also removed from knowledge base</span></div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const DeleteOneDriveFileToolUI = ({ result }: ToolCallMessagePartProps<{ file_name: string; delete_from_kb?: boolean }, DeleteOneDriveFileResult>) => {
|
||||
if (!result) return null;
|
||||
if (isInterruptResult(result)) {
|
||||
return <ApprovalCard interruptData={result} onDecision={(decision) => { window.dispatchEvent(new CustomEvent("hitl-decision", { detail: { decisions: [decision] } })); }} />;
|
||||
}
|
||||
if (typeof result === "object" && result !== null && "status" in result && (result as { status: string }).status === "rejected") return null;
|
||||
if (isAuthErrorResult(result)) return <AuthErrorCard result={result} />;
|
||||
if (isNotFoundResult(result)) return <NotFoundCard result={result} />;
|
||||
if (isErrorResult(result)) return <ErrorCard result={result} />;
|
||||
return <SuccessCard result={result as SuccessResult} />;
|
||||
};
|
||||
|
|
@ -68,7 +68,7 @@ function AlertDialogFooter({ className, ...props }: React.ComponentProps<"div">)
|
|||
return (
|
||||
<div
|
||||
data-slot="alert-dialog-footer"
|
||||
className={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
|
||||
className={cn("flex flex-col-reverse gap-3 sm:flex-row sm:justify-end", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ DialogHeader.displayName = "DialogHeader";
|
|||
|
||||
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
|
||||
className={cn("flex flex-col-reverse gap-3 sm:flex-row sm:justify-end", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -53,6 +53,11 @@ Connect SurfSense to your favorite tools and services. Browse the available inte
|
|||
description="Connect your Microsoft Teams to SurfSense"
|
||||
href="/docs/connectors/microsoft-teams"
|
||||
/>
|
||||
<Card
|
||||
title="Microsoft OneDrive"
|
||||
description="Connect your Microsoft OneDrive to SurfSense"
|
||||
href="/docs/connectors/microsoft-onedrive"
|
||||
/>
|
||||
<Card
|
||||
title="Confluence"
|
||||
description="Connect your Confluence spaces to SurfSense"
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
"jira",
|
||||
"linear",
|
||||
"microsoft-teams",
|
||||
"microsoft-onedrive",
|
||||
"confluence",
|
||||
"airtable",
|
||||
"clickup",
|
||||
|
|
|
|||
104
surfsense_web/content/docs/connectors/microsoft-onedrive.mdx
Normal file
104
surfsense_web/content/docs/connectors/microsoft-onedrive.mdx
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
---
|
||||
title: Microsoft OneDrive
|
||||
description: Connect your Microsoft OneDrive to SurfSense
|
||||
---
|
||||
|
||||
# Microsoft OneDrive OAuth Integration Setup Guide
|
||||
|
||||
This guide walks you through setting up a Microsoft OneDrive OAuth integration for SurfSense using Azure App Registration.
|
||||
|
||||
<Callout type="info">
|
||||
Microsoft OneDrive and [Microsoft Teams](/docs/connectors/microsoft-teams) share the same Azure App Registration. If you have already created an app for Teams, you can reuse the same Client ID and Client Secret. Just make sure both redirect URIs are added (see Step 3).
|
||||
</Callout>
|
||||
|
||||
## Step 1: Access Azure App Registrations
|
||||
|
||||
1. Navigate to [portal.azure.com](https://portal.azure.com)
|
||||
2. In the search bar, type **"app reg"**
|
||||
3. Select **"App registrations"** from the Services results
|
||||
|
||||
## Step 2: Create New Registration
|
||||
|
||||
1. On the **App registrations** page, click **"+ New registration"**
|
||||
|
||||
## Step 3: Register the Application
|
||||
|
||||
Fill in the application details:
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Name** | `SurfSense` |
|
||||
| **Supported account types** | Select **"Accounts in any organizational directory (Any Microsoft Entra ID tenant - Multitenant) and personal Microsoft accounts"** |
|
||||
| **Redirect URI** | Platform: `Web`, URI: `http://localhost:8000/api/v1/auth/onedrive/connector/callback` |
|
||||
|
||||
Click **"Register"**
|
||||
|
||||
After registration, add the Teams redirect URI as well (if you plan to use the Teams connector):
|
||||
|
||||
1. Go to **Authentication** in the left sidebar
|
||||
2. Under **Platform configurations** > **Web** > **Redirect URIs**, click **Add URI**
|
||||
3. Add: `http://localhost:8000/api/v1/auth/teams/connector/callback`
|
||||
4. Click **Save**
|
||||
|
||||
## Step 4: Get Application (Client) ID
|
||||
|
||||
After registration, you will be taken to the app's **Overview** page. Here you will find:
|
||||
|
||||
1. Copy the **Application (client) ID** - this is your Client ID
|
||||
2. Note the **Directory (tenant) ID** if needed
|
||||
|
||||
## Step 5: Create Client Secret
|
||||
|
||||
1. In the left sidebar under **Manage**, click **"Certificates & secrets"**
|
||||
2. Select the **"Client secrets"** tab
|
||||
3. Click **"+ New client secret"**
|
||||
4. Enter a description (e.g., `SurfSense`) and select an expiration period
|
||||
5. Click **"Add"**
|
||||
6. **Important**: Copy the secret **Value** immediately. It will not be shown again!
|
||||
|
||||
<Callout type="warn">
|
||||
Never share your client secret publicly or include it in code repositories.
|
||||
</Callout>
|
||||
|
||||
## Step 6: Configure API Permissions
|
||||
|
||||
1. In the left sidebar under **Manage**, click **"API permissions"**
|
||||
2. Click **"+ Add a permission"**
|
||||
3. Select **"Microsoft Graph"**
|
||||
4. Select **"Delegated permissions"**
|
||||
5. Add the following permissions:
|
||||
|
||||
| Permission | Type | Description | Admin Consent |
|
||||
|------------|------|-------------|---------------|
|
||||
| `Files.Read.All` | Delegated | Read all files the user can access | No |
|
||||
| `Files.ReadWrite.All` | Delegated | Read and write all files the user can access | No |
|
||||
| `offline_access` | Delegated | Maintain access to data you have given it access to | No |
|
||||
| `User.Read` | Delegated | Sign in and read user profile | No |
|
||||
|
||||
6. Click **"Add permissions"**
|
||||
|
||||
<Callout type="warn">
|
||||
All four permissions listed above are required. The connector will not authenticate successfully if any are missing.
|
||||
</Callout>
|
||||
|
||||
---
|
||||
|
||||
## Running SurfSense with Microsoft OneDrive Connector
|
||||
|
||||
Add the Microsoft OAuth credentials to your `.env` file (created during [Docker installation](/docs/docker-installation/docker-compose)):
|
||||
|
||||
```bash
|
||||
MICROSOFT_CLIENT_ID=your_microsoft_client_id
|
||||
MICROSOFT_CLIENT_SECRET=your_microsoft_client_secret
|
||||
ONEDRIVE_REDIRECT_URI=http://localhost:8000/api/v1/auth/onedrive/connector/callback
|
||||
```
|
||||
|
||||
<Callout type="info">
|
||||
The `MICROSOFT_CLIENT_ID` and `MICROSOFT_CLIENT_SECRET` are shared between the OneDrive and Teams connectors. You only need to set them once.
|
||||
</Callout>
|
||||
|
||||
Then restart the services:
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
|
@ -7,6 +7,10 @@ description: Connect your Microsoft Teams to SurfSense
|
|||
|
||||
This guide walks you through setting up a Microsoft Teams OAuth integration for SurfSense using Azure App Registration.
|
||||
|
||||
<Callout type="info">
|
||||
Microsoft Teams and [Microsoft OneDrive](/docs/connectors/microsoft-onedrive) share the same Azure App Registration. If you have already created an app for OneDrive, you can reuse the same Client ID and Client Secret. Just make sure both redirect URIs are added (see Step 3).
|
||||
</Callout>
|
||||
|
||||
## Step 1: Access Azure App Registrations
|
||||
|
||||
1. Navigate to [portal.azure.com](https://portal.azure.com)
|
||||
|
|
@ -33,11 +37,18 @@ Fill in the application details:
|
|||
|
||||
Click **"Register"**
|
||||
|
||||
After registration, add the OneDrive redirect URI as well:
|
||||
|
||||
1. Go to **Authentication** in the left sidebar
|
||||
2. Under **Platform configurations** > **Web** > **Redirect URIs**, click **Add URI**
|
||||
3. Add: `http://localhost:8000/api/v1/auth/onedrive/connector/callback`
|
||||
4. Click **Save**
|
||||
|
||||

|
||||
|
||||
## Step 4: Get Application (Client) ID
|
||||
|
||||
After registration, you'll be taken to the app's **Overview** page. Here you'll find:
|
||||
After registration, you will be taken to the app's **Overview** page. Here you will find:
|
||||
|
||||
1. Copy the **Application (client) ID** - this is your Client ID
|
||||
2. Note the **Directory (tenant) ID** if needed
|
||||
|
|
@ -54,7 +65,7 @@ After registration, you'll be taken to the app's **Overview** page. Here you'll
|
|||
|
||||

|
||||
|
||||
6. **Important**: Copy the secret **Value** immediately - it won't be shown again!
|
||||
6. **Important**: Copy the secret **Value** immediately. It will not be shown again!
|
||||
|
||||

|
||||
|
||||
|
|
@ -90,14 +101,18 @@ After registration, you'll be taken to the app's **Overview** page. Here you'll
|
|||
|
||||
## Running SurfSense with Microsoft Teams Connector
|
||||
|
||||
Add the Microsoft Teams credentials to your `.env` file (created during [Docker installation](/docs/docker-installation/docker-compose)):
|
||||
Add the Microsoft OAuth credentials to your `.env` file (created during [Docker installation](/docs/docker-installation/docker-compose)):
|
||||
|
||||
```bash
|
||||
TEAMS_CLIENT_ID=your_microsoft_client_id
|
||||
TEAMS_CLIENT_SECRET=your_microsoft_client_secret
|
||||
MICROSOFT_CLIENT_ID=your_microsoft_client_id
|
||||
MICROSOFT_CLIENT_SECRET=your_microsoft_client_secret
|
||||
TEAMS_REDIRECT_URI=http://localhost:8000/api/v1/auth/teams/connector/callback
|
||||
```
|
||||
|
||||
<Callout type="info">
|
||||
The `MICROSOFT_CLIENT_ID` and `MICROSOFT_CLIENT_SECRET` are shared between the Teams and OneDrive connectors. You only need to set them once.
|
||||
</Callout>
|
||||
|
||||
Then restart the services:
|
||||
|
||||
```bash
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ Uncomment the connectors you want to use. Redirect URIs follow the pattern `http
|
|||
| Linear | `LINEAR_CLIENT_ID`, `LINEAR_CLIENT_SECRET`, `LINEAR_REDIRECT_URI` |
|
||||
| ClickUp | `CLICKUP_CLIENT_ID`, `CLICKUP_CLIENT_SECRET`, `CLICKUP_REDIRECT_URI` |
|
||||
| Airtable | `AIRTABLE_CLIENT_ID`, `AIRTABLE_CLIENT_SECRET`, `AIRTABLE_REDIRECT_URI` |
|
||||
| Microsoft Teams | `TEAMS_CLIENT_ID`, `TEAMS_CLIENT_SECRET`, `TEAMS_REDIRECT_URI` |
|
||||
| Microsoft (Teams & OneDrive) | `MICROSOFT_CLIENT_ID`, `MICROSOFT_CLIENT_SECRET`, `TEAMS_REDIRECT_URI`, `ONEDRIVE_REDIRECT_URI` |
|
||||
|
||||
### Observability (optional)
|
||||
|
||||
|
|
|
|||
|
|
@ -127,9 +127,10 @@ Edit the `.env` file and set the following variables:
|
|||
| SLACK_CLIENT_ID | (Optional) Slack OAuth client ID |
|
||||
| SLACK_CLIENT_SECRET | (Optional) Slack OAuth client secret |
|
||||
| SLACK_REDIRECT_URI | (Optional) Redirect URI for Slack connector OAuth callback (e.g., `http://localhost:8000/api/v1/auth/slack/connector/callback`) |
|
||||
| TEAMS_CLIENT_ID | (Optional) Microsoft Teams OAuth client ID |
|
||||
| TEAMS_CLIENT_SECRET | (Optional) Microsoft Teams OAuth client secret |
|
||||
| MICROSOFT_CLIENT_ID | (Optional) Microsoft OAuth client ID (shared for Teams and OneDrive) |
|
||||
| MICROSOFT_CLIENT_SECRET | (Optional) Microsoft OAuth client secret (shared for Teams and OneDrive) |
|
||||
| TEAMS_REDIRECT_URI | (Optional) Redirect URI for Teams connector OAuth callback (e.g., `http://localhost:8000/api/v1/auth/teams/connector/callback`) |
|
||||
| ONEDRIVE_REDIRECT_URI | (Optional) Redirect URI for OneDrive connector OAuth callback (e.g., `http://localhost:8000/api/v1/auth/onedrive/connector/callback`) |
|
||||
|
||||
**(Optional) Backend LangSmith Observability:**
|
||||
| ENV VARIABLE | DESCRIPTION |
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ export const searchSourceConnectorTypeEnum = z.enum([
|
|||
"BAIDU_SEARCH_API",
|
||||
"SLACK_CONNECTOR",
|
||||
"TEAMS_CONNECTOR",
|
||||
"ONEDRIVE_CONNECTOR",
|
||||
"NOTION_CONNECTOR",
|
||||
"GITHUB_CONNECTOR",
|
||||
"LINEAR_CONNECTOR",
|
||||
|
|
@ -53,7 +54,7 @@ export const searchSourceConnector = z.object({
|
|||
export const googleDriveItem = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
mimeType: z.string(),
|
||||
mimeType: z.string().optional().default("application/octet-stream"),
|
||||
isFolder: z.boolean(),
|
||||
parents: z.array(z.string()).optional(),
|
||||
size: z.coerce.number().optional(),
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ export const documentTypeEnum = z.enum([
|
|||
"FILE",
|
||||
"SLACK_CONNECTOR",
|
||||
"TEAMS_CONNECTOR",
|
||||
"ONEDRIVE_FILE",
|
||||
"NOTION_CONNECTOR",
|
||||
"YOUTUBE_VIDEO",
|
||||
"GITHUB_CONNECTOR",
|
||||
|
|
|
|||
|
|
@ -1,28 +0,0 @@
|
|||
import { useQuery } from "@tanstack/react-query";
|
||||
import { connectorsApiService } from "@/lib/apis/connectors-api.service";
|
||||
import { cacheKeys } from "@/lib/query-client/cache-keys";
|
||||
|
||||
interface UseComposioDriveFoldersOptions {
|
||||
connectorId: number;
|
||||
parentId?: string;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export function useComposioDriveFolders({
|
||||
connectorId,
|
||||
parentId,
|
||||
enabled = true,
|
||||
}: UseComposioDriveFoldersOptions) {
|
||||
return useQuery({
|
||||
queryKey: cacheKeys.connectors.composioDrive.folders(connectorId, parentId),
|
||||
queryFn: async () => {
|
||||
return connectorsApiService.listComposioDriveFolders({
|
||||
connector_id: connectorId,
|
||||
parent_id: parentId,
|
||||
});
|
||||
},
|
||||
enabled: enabled && !!connectorId,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
retry: 2,
|
||||
});
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { connectorsApiService } from "@/lib/apis/connectors-api.service";
|
||||
|
||||
export interface PickerItem {
|
||||
|
|
@ -159,7 +160,9 @@ export function useGooglePicker({ connectorId, onPicked }: UseGooglePickerOption
|
|||
}
|
||||
|
||||
if (action === google.picker.Action.ERROR) {
|
||||
setError("Google Drive encountered an error. Please try again.");
|
||||
const msg = "Google Drive encountered an error. Please try again.";
|
||||
setError(msg);
|
||||
toast.error("Google Drive Picker failed", { description: msg });
|
||||
}
|
||||
|
||||
if (
|
||||
|
|
@ -180,6 +183,7 @@ export function useGooglePicker({ connectorId, onPicked }: UseGooglePickerOption
|
|||
openingRef.current = false;
|
||||
const msg = err instanceof Error ? err.message : "Failed to open Google Picker";
|
||||
setError(msg);
|
||||
toast.error("Google Drive Picker failed", { description: msg });
|
||||
console.error("Google Picker error:", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// =============================================================================
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -79,10 +79,6 @@ export const cacheKeys = {
|
|||
folders: (connectorId: number, parentId?: string) =>
|
||||
["connectors", "google-drive", connectorId, "folders", parentId] as const,
|
||||
},
|
||||
composioDrive: {
|
||||
folders: (connectorId: number, parentId?: string) =>
|
||||
["connectors", "composio-drive", connectorId, "folders", parentId] as const,
|
||||
},
|
||||
},
|
||||
comments: {
|
||||
byMessage: (messageId: number) => ["comments", "message", messageId] as const,
|
||||
|
|
|
|||
|
|
@ -1,155 +1 @@
|
|||
<?xml version="1.0" ?><svg style="enable-background:new 0 0 100 100;" version="1.1" viewBox="0 0 100 100" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
.st1{fill:#F5BB41;}
|
||||
.st2{fill:#2167D1;}
|
||||
.st3{fill:#3D84F3;}
|
||||
.st4{fill:#4CA853;}
|
||||
.st5{fill:#398039;}
|
||||
.st6{fill:#D74F3F;}
|
||||
.st7{fill:#D43C89;}
|
||||
.st8{fill:#B2005F;}
|
||||
.st9{fill:none;stroke:#000000;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
|
||||
|
||||
.st10{fill-rule:evenodd;clip-rule:evenodd;fill:none;stroke:#000000;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
|
||||
|
||||
.st11{fill-rule:evenodd;clip-rule:evenodd;fill:none;stroke:#040404;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
|
||||
.st12{fill-rule:evenodd;clip-rule:evenodd;}
|
||||
.st13{fill-rule:evenodd;clip-rule:evenodd;fill:#040404;}
|
||||
.st14{fill:url(#SVGID_1_);}
|
||||
.st15{fill:url(#SVGID_2_);}
|
||||
.st16{fill:url(#SVGID_3_);}
|
||||
.st17{fill:url(#SVGID_4_);}
|
||||
.st18{fill:url(#SVGID_5_);}
|
||||
.st19{fill:url(#SVGID_6_);}
|
||||
.st20{fill:url(#SVGID_7_);}
|
||||
.st21{fill:url(#SVGID_8_);}
|
||||
.st22{fill:url(#SVGID_9_);}
|
||||
.st23{fill:url(#SVGID_10_);}
|
||||
.st24{fill:url(#SVGID_11_);}
|
||||
.st25{fill:url(#SVGID_12_);}
|
||||
.st26{fill:url(#SVGID_13_);}
|
||||
.st27{fill:url(#SVGID_14_);}
|
||||
.st28{fill:url(#SVGID_15_);}
|
||||
.st29{fill:url(#SVGID_16_);}
|
||||
.st30{fill:url(#SVGID_17_);}
|
||||
.st31{fill:url(#SVGID_18_);}
|
||||
.st32{fill:url(#SVGID_19_);}
|
||||
.st33{fill:url(#SVGID_20_);}
|
||||
.st34{fill:url(#SVGID_21_);}
|
||||
.st35{fill:url(#SVGID_22_);}
|
||||
.st36{fill:url(#SVGID_23_);}
|
||||
.st37{fill:url(#SVGID_24_);}
|
||||
.st38{fill:url(#SVGID_25_);}
|
||||
.st39{fill:url(#SVGID_26_);}
|
||||
.st40{fill:url(#SVGID_27_);}
|
||||
.st41{fill:url(#SVGID_28_);}
|
||||
.st42{fill:url(#SVGID_29_);}
|
||||
.st43{fill:url(#SVGID_30_);}
|
||||
.st44{fill:url(#SVGID_31_);}
|
||||
.st45{fill:url(#SVGID_32_);}
|
||||
.st46{fill:url(#SVGID_33_);}
|
||||
.st47{fill:url(#SVGID_34_);}
|
||||
.st48{fill:url(#SVGID_35_);}
|
||||
.st49{fill:url(#SVGID_36_);}
|
||||
.st50{fill:url(#SVGID_37_);}
|
||||
.st51{fill:url(#SVGID_38_);}
|
||||
.st52{fill:url(#SVGID_39_);}
|
||||
.st53{fill:url(#SVGID_40_);}
|
||||
.st54{fill:url(#SVGID_41_);}
|
||||
.st55{fill:url(#SVGID_42_);}
|
||||
.st56{fill:url(#SVGID_43_);}
|
||||
.st57{fill:url(#SVGID_44_);}
|
||||
.st58{fill:url(#SVGID_45_);}
|
||||
.st59{fill:#040404;}
|
||||
.st60{fill:url(#SVGID_46_);}
|
||||
.st61{fill:url(#SVGID_47_);}
|
||||
.st62{fill:url(#SVGID_48_);}
|
||||
.st63{fill:url(#SVGID_49_);}
|
||||
.st64{fill:url(#SVGID_50_);}
|
||||
.st65{fill:url(#SVGID_51_);}
|
||||
.st66{fill:url(#SVGID_52_);}
|
||||
.st67{fill:url(#SVGID_53_);}
|
||||
.st68{fill:url(#SVGID_54_);}
|
||||
.st69{fill:url(#SVGID_55_);}
|
||||
.st70{fill:url(#SVGID_56_);}
|
||||
.st71{fill:url(#SVGID_57_);}
|
||||
.st72{fill:url(#SVGID_58_);}
|
||||
.st73{fill:url(#SVGID_59_);}
|
||||
.st74{fill:url(#SVGID_60_);}
|
||||
.st75{fill:url(#SVGID_61_);}
|
||||
.st76{fill:url(#SVGID_62_);}
|
||||
.st77{fill:none;stroke:#000000;stroke-width:3;stroke-miterlimit:10;}
|
||||
.st78{fill:none;stroke:#FFFFFF;stroke-miterlimit:10;}
|
||||
.st79{fill:#4BC9FF;}
|
||||
.st80{fill:#5500DD;}
|
||||
.st81{fill:#FF3A00;}
|
||||
.st82{fill:#E6162D;}
|
||||
.st83{fill:#F1F1F1;}
|
||||
.st84{fill:#FF9933;}
|
||||
.st85{fill:#B92B27;}
|
||||
.st86{fill:#00ACED;}
|
||||
.st87{fill:#BD2125;}
|
||||
.st88{fill:#1877F2;}
|
||||
.st89{fill:#6665D2;}
|
||||
.st90{fill:#CE3056;}
|
||||
.st91{fill:#5BB381;}
|
||||
.st92{fill:#61C3EC;}
|
||||
.st93{fill:#E4B34B;}
|
||||
.st94{fill:#181EF2;}
|
||||
.st95{fill:#FF0000;}
|
||||
.st96{fill:#FE466C;}
|
||||
.st97{fill:#FA4778;}
|
||||
.st98{fill:#FF7700;}
|
||||
.st99{fill-rule:evenodd;clip-rule:evenodd;fill:#1F6BF6;}
|
||||
.st100{fill:#520094;}
|
||||
.st101{fill:#4477E8;}
|
||||
.st102{fill:#3D1D1C;}
|
||||
.st103{fill:#FFE812;}
|
||||
.st104{fill:#344356;}
|
||||
.st105{fill:#00CC76;}
|
||||
.st106{fill-rule:evenodd;clip-rule:evenodd;fill:#345E90;}
|
||||
.st107{fill:#1F65D8;}
|
||||
.st108{fill:#EB3587;}
|
||||
.st109{fill-rule:evenodd;clip-rule:evenodd;fill:#603A88;}
|
||||
.st110{fill:#E3CE99;}
|
||||
.st111{fill:#783AF9;}
|
||||
.st112{fill:#FF515E;}
|
||||
.st113{fill:#FF4906;}
|
||||
.st114{fill:#503227;}
|
||||
.st115{fill:#4C7BD9;}
|
||||
.st116{fill:#69C9D0;}
|
||||
.st117{fill:#1B92D1;}
|
||||
.st118{fill:#EB4F4A;}
|
||||
.st119{fill:#513728;}
|
||||
.st120{fill:#FF6600;}
|
||||
.st121{fill-rule:evenodd;clip-rule:evenodd;fill:#B61438;}
|
||||
.st122{fill:#FFFC00;}
|
||||
.st123{fill:#141414;}
|
||||
.st124{fill:#94D137;}
|
||||
.st125{fill-rule:evenodd;clip-rule:evenodd;fill:#F1F1F1;}
|
||||
.st126{fill-rule:evenodd;clip-rule:evenodd;fill:#66E066;}
|
||||
.st127{fill:#2D8CFF;}
|
||||
.st128{fill:#F1A300;}
|
||||
.st129{fill:#4BA2F2;}
|
||||
.st130{fill:#1A5099;}
|
||||
.st131{fill:#EE6060;}
|
||||
.st132{fill-rule:evenodd;clip-rule:evenodd;fill:#F48120;}
|
||||
.st133{fill:#222222;}
|
||||
.st134{fill:url(#SVGID_63_);}
|
||||
.st135{fill:#0077B5;}
|
||||
.st136{fill:#FFCC00;}
|
||||
.st137{fill:#EB3352;}
|
||||
.st138{fill:#F9D265;}
|
||||
.st139{fill:#F5B955;}
|
||||
.st140{fill:#DD2A7B;}
|
||||
.st141{fill:#66E066;}
|
||||
.st142{fill:#EB4E00;}
|
||||
.st143{fill:#FFC794;}
|
||||
.st144{fill:#B5332A;}
|
||||
.st145{fill:#4E85EB;}
|
||||
.st146{fill:#58A45C;}
|
||||
.st147{fill:#F2BC42;}
|
||||
.st148{fill:#D85040;}
|
||||
.st149{fill:#464EB8;}
|
||||
.st150{fill:#7B83EB;}
|
||||
</style><g id="Layer_1"/><g id="Layer_2"><g><g><path class="st149" d="M84.025,35.881c5.797,0,10.513-4.729,10.513-10.54c-0.577-13.983-20.45-13.979-21.026,0 C73.512,31.152,78.229,35.881,84.025,35.881z"/><path class="st149" d="M90.958,38.71H72.193h-0.036c-0.005,0-0.003,0-0.009,0c-1.123,0-15.805,0-20.538,0v-3.68 c0.784,0.139,1.605,0.232,2.467,0.268c0.093,0.001,0.186-0.006,0.279-0.007c0.358-0.006,0.713-0.023,1.063-0.053 c0.12-0.011,0.239-0.021,0.357-0.035c0.403-0.045,0.801-0.104,1.193-0.181c0.024-0.005,0.05-0.008,0.074-0.012 c1.858-0.379,3.61-1.12,5.167-2.17c1.44-0.971,2.687-2.203,3.693-3.615c0.26-0.341,0.497-0.697,0.718-1.061 c0.021-0.036,0.044-0.07,0.065-0.107c0.17-0.287,0.32-0.584,0.466-0.884c0.064-0.13,0.13-0.26,0.19-0.392 c0.154-0.345,0.296-0.696,0.421-1.053c0.011-0.03,0.022-0.059,0.032-0.088c1.427-4.208,0.774-9.156-1.676-12.856 c-0.648-0.949-1.417-1.806-2.268-2.574c-0.176-0.153-0.344-0.314-0.529-0.457c-0.714-0.588-1.485-1.109-2.304-1.552 c-0.41-0.222-0.831-0.425-1.263-0.607c-0.434-0.192-0.887-0.35-1.347-0.493c-0.264-0.081-0.538-0.141-0.808-0.207 c-0.239-0.058-0.475-0.121-0.717-0.166c-0.2-0.038-0.405-0.062-0.607-0.092c-0.352-0.05-0.704-0.096-1.06-0.121 c-0.122-0.009-0.245-0.012-0.368-0.018C54.486,6.479,54.123,6.48,53.76,6.49c-2.08,0.121-3.926,0.558-5.543,1.24 c-0.33,0.149-0.664,0.294-0.975,0.47C44,9.966,41.52,12.972,40.375,16.493c-0.794,2.629-0.862,5.468-0.187,8.129 c0.007,0.025,0.013,0.051,0.02,0.076c0.032,0.115,0.065,0.23,0.097,0.345c0.039,0.137,0.085,0.273,0.128,0.409 c0.039,0.11,0.08,0.219,0.121,0.329H8.774c-2.846,0-5.162,2.316-5.162,5.162v37.672c0,2.847,2.316,5.162,5.162,5.162h20.122 c0.026,0.118,0.059,0.232,0.087,0.349C31.753,85.025,41.446,92.733,52.9,93.011c9.503-0.231,17.666-5.721,21.753-13.592 c0.061,0.022,0.124,0.038,0.185,0.059c10.182,3.851,21.752-4.229,21.546-15.131V44.122C96.385,41.138,93.95,38.71,90.958,38.71z"/><g><g><path class="st150" d="M77.444,44.232c0.069-2.971-2.287-5.448-5.251-5.521c-0.012,0-21.432,0-21.432,0 c-0.789,0-1.429,0.641-1.429,1.433v29.095c0,1.342-1.089,2.433-2.428,2.433H30.199c-0.429,0-0.836,0.194-1.107,0.527 c-0.271,0.334-0.379,0.772-0.292,1.194c2.367,11.561,12.248,19.837,24.1,20.126c13.856-0.34,24.866-11.914,24.544-25.767 L77.444,44.232z"/></g><path class="st150" d="M54.077,35.298c0.093,0.001,0.186-0.006,0.279-0.007c0.358-0.005,0.713-0.023,1.064-0.053 c0.12-0.011,0.239-0.021,0.357-0.035c0.402-0.045,0.801-0.104,1.193-0.181c0.024-0.005,0.05-0.008,0.074-0.013 c1.858-0.379,3.61-1.12,5.167-2.17c1.441-0.971,2.687-2.203,3.694-3.615c0.26-0.341,0.497-0.697,0.718-1.061 c0.021-0.036,0.044-0.07,0.065-0.107c0.17-0.287,0.32-0.585,0.466-0.884c0.064-0.13,0.13-0.259,0.19-0.392 c0.154-0.345,0.297-0.696,0.421-1.053c0.011-0.03,0.022-0.059,0.032-0.088c1.427-4.208,0.774-9.157-1.676-12.856 c-0.648-0.949-1.417-1.806-2.268-2.574c-0.176-0.153-0.344-0.314-0.529-0.457c-0.714-0.588-1.485-1.109-2.304-1.552 c-0.41-0.222-0.831-0.425-1.263-0.607c-0.434-0.192-0.887-0.35-1.347-0.493c-0.264-0.081-0.538-0.14-0.808-0.207 c-0.239-0.058-0.475-0.121-0.717-0.166c-0.2-0.038-0.404-0.062-0.607-0.092c-0.352-0.05-0.704-0.096-1.06-0.121 c-0.122-0.009-0.245-0.012-0.367-0.018c-0.362-0.016-0.725-0.015-1.088-0.005c-2.08,0.121-3.926,0.557-5.543,1.24 c-0.33,0.149-0.664,0.294-0.975,0.47c-3.242,1.767-5.723,4.773-6.867,8.294c-0.794,2.629-0.862,5.468-0.187,8.129 c0.007,0.025,0.013,0.051,0.02,0.076c0.032,0.115,0.065,0.23,0.097,0.345c0.039,0.137,0.085,0.273,0.128,0.409 c0.06,0.171,0.123,0.34,0.187,0.51h-0.027C42.371,30.941,46.864,34.993,54.077,35.298z"/></g><g><path class="st149" d="M46.448,25.783H8.774c-2.846,0-5.162,2.316-5.162,5.162v37.672c0,2.847,2.316,5.162,5.162,5.162h37.674 c2.846,0,5.161-2.316,5.161-5.162V30.945C51.61,28.099,49.295,25.783,46.448,25.783z"/><path class="st0" d="M37.109,36.271h-19.28c-0.771,0-1.395,0.625-1.395,1.396l0,3.514c0,0.771,0.624,1.396,1.395,1.396h6.22 l0,19.575c0,0.771,0.624,1.396,1.395,1.396h4.134c0.771,0,1.395-0.625,1.395-1.396l0-19.575h6.136 c0.771,0,1.395-0.625,1.395-1.396l0-3.514C38.504,36.896,37.88,36.271,37.109,36.271z"/></g></g></g></g></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="4 4 36 38"><path fill="url(#a)" d="M21.9999 20h12c3.3137 0 6 2.6863 6 6v10c0 3.3137-2.6863 6-6 6s-6-2.6863-6-6V26c0-3.3137-2.6863-6-6-6"/><path fill="url(#b)" d="M7.99988 24c0-3.3137 2.68632-6 6.00002-6h8c3.3137 0 6 2.6863 6 6v12c0 3.3137 2.6863 6 6 6l-16.0001-.0001c-5.5228 0-9.99992-4.4771-9.99992-10z"/><path fill="url(#c)" fill-opacity=".7" d="M7.99988 24c0-3.3137 2.68632-6 6.00002-6h8c3.3137 0 6 2.6863 6 6v12c0 3.3137 2.6863 6 6 6l-16.0001-.0001c-5.5228 0-9.99992-4.4771-9.99992-10z"/><path fill="url(#d)" fill-opacity=".7" d="M7.99988 24c0-3.3137 2.68632-6 6.00002-6h8c3.3137 0 6 2.6863 6 6v12c0 3.3137 2.6863 6 6 6l-16.0001-.0001c-5.5228 0-9.99992-4.4771-9.99992-10z"/><path fill="url(#e)" d="M32.9999 18c2.7614 0 5-2.2386 5-5s-2.2386-5-5-5-5 2.2386-5 5 2.2386 5 5 5"/><path fill="url(#f)" fill-opacity=".46" d="M32.9999 18c2.7614 0 5-2.2386 5-5s-2.2386-5-5-5-5 2.2386-5 5 2.2386 5 5 5"/><path fill="url(#g)" fill-opacity=".4" d="M32.9999 18c2.7614 0 5-2.2386 5-5s-2.2386-5-5-5-5 2.2386-5 5 2.2386 5 5 5"/><path fill="url(#h)" d="M17.9999 16c3.3137 0 6-2.6863 6-6 0-3.31371-2.6863-6-6-6s-6 2.68629-6 6c0 3.3137 2.6863 6 6 6"/><path fill="url(#i)" fill-opacity=".6" d="M17.9999 16c3.3137 0 6-2.6863 6-6 0-3.31371-2.6863-6-6-6s-6 2.68629-6 6c0 3.3137 2.6863 6 6 6"/><path fill="url(#j)" fill-opacity=".5" d="M17.9999 16c3.3137 0 6-2.6863 6-6 0-3.31371-2.6863-6-6-6s-6 2.68629-6 6c0 3.3137 2.6863 6 6 6"/><rect width="16" height="16" x="4" y="23" fill="url(#k)" rx="3.25"/><rect width="16" height="16" x="4" y="23" fill="url(#l)" fill-opacity=".7" rx="3.25"/><path fill="#fff" d="M15.4792 28.1054h-2.4471v7.466h-2.0648v-7.466H8.52014v-1.6768h6.95906z"/><defs><radialGradient id="a" cx="0" cy="0" r="1" gradientTransform="matrix(13.4784 0 0 33.2694 39.7967 22.1739)" gradientUnits="userSpaceOnUse"><stop stop-color="#a98aff"/><stop offset=".14" stop-color="#8c75ff"/><stop offset=".565" stop-color="#5f50e2"/><stop offset=".9" stop-color="#3c2cb8"/></radialGradient><radialGradient id="b" cx="0" cy="0" r="1" gradientTransform="rotate(68.1539 -7.71566095 14.71355834)scale(32.752 33.1231)" gradientUnits="userSpaceOnUse"><stop stop-color="#85c2ff"/><stop offset=".69" stop-color="#7588ff"/><stop offset="1" stop-color="#6459fe"/></radialGradient><radialGradient id="d" cx="0" cy="0" r="1" gradientTransform="rotate(113.326 8.09285255 17.64474501)scale(19.2186 15.4273)" gradientUnits="userSpaceOnUse"><stop stop-color="#bd96ff"/><stop offset=".686685" stop-color="#bd96ff" stop-opacity="0"/></radialGradient><radialGradient id="e" cx="0" cy="0" r="1" gradientTransform="matrix(0 -10 12.6216 0 32.9999 11.5714)" gradientUnits="userSpaceOnUse"><stop offset=".268201" stop-color="#6868f7"/><stop offset="1" stop-color="#3923b1"/></radialGradient><radialGradient id="f" cx="0" cy="0" r="1" gradientTransform="rotate(40.0516 -.03068196 44.8729095)scale(7.14629 10.3363)" gradientUnits="userSpaceOnUse"><stop offset=".270711" stop-color="#a1d3ff"/><stop offset=".813393" stop-color="#a1d3ff" stop-opacity="0"/></radialGradient><radialGradient id="g" cx="0" cy="0" r="1" gradientTransform="rotate(-41.6581 32.11799918 -43.41948423)scale(8.51275 20.8824)" gradientUnits="userSpaceOnUse"><stop stop-color="#e3acfd"/><stop offset=".816041" stop-color="#9fa2ff" stop-opacity="0"/></radialGradient><radialGradient id="h" cx="0" cy="0" r="1" gradientTransform="matrix(0 -12 15.146 0 17.9999 8.28571)" gradientUnits="userSpaceOnUse"><stop offset=".268201" stop-color="#8282ff"/><stop offset="1" stop-color="#3923b1"/></radialGradient><radialGradient id="i" cx="0" cy="0" r="1" gradientTransform="rotate(40.0516 -3.15465147 21.41641466)scale(8.57554 12.4035)" gradientUnits="userSpaceOnUse"><stop offset=".270711" stop-color="#a1d3ff"/><stop offset=".813393" stop-color="#a1d3ff" stop-opacity="0"/></radialGradient><radialGradient id="j" cx="0" cy="0" r="1" gradientTransform="rotate(-41.6581 20.38180375 -26.51566158)scale(10.2153 25.0589)" gradientUnits="userSpaceOnUse"><stop stop-color="#e3acfd"/><stop offset=".816041" stop-color="#9fa2ff" stop-opacity="0"/></radialGradient><radialGradient id="k" cx="0" cy="0" r="1" gradientTransform="rotate(45 -25.76345597 16.32842712)scale(22.6274)" gradientUnits="userSpaceOnUse"><stop offset=".046875" stop-color="#688eff"/><stop offset=".946875" stop-color="#230f94"/></radialGradient><radialGradient id="l" cx="0" cy="0" r="1" gradientTransform="matrix(0 11.2 -13.0702 0 12 32.6)" gradientUnits="userSpaceOnUse"><stop offset=".570647" stop-color="#6965f6" stop-opacity="0"/><stop offset="1" stop-color="#8f8fff"/></radialGradient><linearGradient id="c" x1="20.5936" x2="20.5936" y1="18" y2="42" gradientUnits="userSpaceOnUse"><stop offset=".801159" stop-color="#6864f6" stop-opacity="0"/><stop offset="1" stop-color="#5149de"/></linearGradient></defs></svg>
|
||||
|
Before Width: | Height: | Size: 8.8 KiB After Width: | Height: | Size: 4.7 KiB |
51
surfsense_web/public/connectors/onedrive.svg
Normal file
51
surfsense_web/public/connectors/onedrive.svg
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="35.98 139.2 648.03 430.85">
|
||||
<defs>
|
||||
<radialGradient id="radial0" gradientUnits="userSpaceOnUse" cx="0" cy="0" fx="0" fy="0" r="1" gradientTransform="matrix(130.864814,156.804864,-260.089994,217.063603,48.669602,228.766494)">
|
||||
<stop offset="0" style="stop-color:rgb(28.235294%,58.039216%,99.607843%);stop-opacity:1;"/>
|
||||
<stop offset="0.695072" style="stop-color:rgb(3.529412%,20.392157%,70.196078%);stop-opacity:1;"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="radial1" gradientUnits="userSpaceOnUse" cx="0" cy="0" fx="0" fy="0" r="1" gradientTransform="matrix(-575.289668,663.594003,-491.728488,-426.294267,596.956501,-6.380235)">
|
||||
<stop offset="0.165327" style="stop-color:rgb(13.72549%,75.294118%,99.607843%);stop-opacity:1;"/>
|
||||
<stop offset="0.534" style="stop-color:rgb(10.980392%,56.862745%,100%);stop-opacity:1;"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="radial2" gradientUnits="userSpaceOnUse" cx="0" cy="0" fx="0" fy="0" r="1" gradientTransform="matrix(-136.753383,-114.806698,262.816935,-313.057562,181.196995,240.395994)">
|
||||
<stop offset="0" style="stop-color:rgb(100%,100%,100%);stop-opacity:0.4;"/>
|
||||
<stop offset="0.660528" style="stop-color:rgb(67.843137%,75.294118%,100%);stop-opacity:0;"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="radial3" gradientUnits="userSpaceOnUse" cx="0" cy="0" fx="0" fy="0" r="1" gradientTransform="matrix(-153.638428,-130.000063,197.433014,-233.332948,375.353994,451.43549)">
|
||||
<stop offset="0" style="stop-color:rgb(1.176471%,22.745098%,80%);stop-opacity:1;"/>
|
||||
<stop offset="1" style="stop-color:rgb(21.176471%,55.686275%,100%);stop-opacity:0;"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="radial4" gradientUnits="userSpaceOnUse" cx="0" cy="0" fx="0" fy="0" r="1" gradientTransform="matrix(175.585899,405.198026,-437.434522,189.555055,169.378495,125.589294)">
|
||||
<stop offset="0.592618" style="stop-color:rgb(20.392157%,39.215686%,89.019608%);stop-opacity:0;"/>
|
||||
<stop offset="1" style="stop-color:rgb(1.176471%,22.745098%,80%);stop-opacity:0.6;"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="radial5" gradientUnits="userSpaceOnUse" cx="0" cy="0" fx="0" fy="0" r="1" gradientTransform="matrix(-459.329491,459.329491,-719.614455,-719.614455,589.876499,39.484649)">
|
||||
<stop offset="0" style="stop-color:rgb(29.411765%,99.215686%,90.980392%);stop-opacity:0.898039;"/>
|
||||
<stop offset="0.543937" style="stop-color:rgb(29.411765%,99.215686%,90.980392%);stop-opacity:0;"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="linear0" gradientUnits="userSpaceOnUse" x1="29.999701" y1="37.9823" x2="29.999701" y2="18.398199" gradientTransform="matrix(15,0,0,15,0,0)">
|
||||
<stop offset="0" style="stop-color:rgb(0%,52.54902%,100%);stop-opacity:1;"/>
|
||||
<stop offset="0.49" style="stop-color:rgb(0%,73.333333%,100%);stop-opacity:1;"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="radial6" gradientUnits="userSpaceOnUse" cx="0" cy="0" fx="0" fy="0" r="1" gradientTransform="matrix(273.622108,108.513684,-205.488428,518.148261,296.488495,307.441492)">
|
||||
<stop offset="0" style="stop-color:rgb(100%,100%,100%);stop-opacity:0.4;"/>
|
||||
<stop offset="0.785262" style="stop-color:rgb(100%,100%,100%);stop-opacity:0;"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="radial7" gradientUnits="userSpaceOnUse" cx="0" cy="0" fx="0" fy="0" r="1" gradientTransform="matrix(-305.683909,263.459223,-264.352324,-306.720147,674.845505,249.378004)">
|
||||
<stop offset="0" style="stop-color:rgb(29.411765%,99.215686%,90.980392%);stop-opacity:0.898039;"/>
|
||||
<stop offset="0.584724" style="stop-color:rgb(29.411765%,99.215686%,90.980392%);stop-opacity:0;"/>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
<g id="surface1">
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:url(#radial0);" d="M 215.078125 205.089844 C 116.011719 205.09375 41.957031 286.1875 36.382812 376.527344 C 39.835938 395.992188 51.175781 434.429688 68.941406 432.457031 C 91.144531 429.988281 147.066406 432.457031 194.765625 346.105469 C 229.609375 283.027344 301.285156 205.085938 215.078125 205.089844 Z M 215.078125 205.089844 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:url(#radial1);" d="M 192.171875 238.8125 C 158.871094 291.535156 114.042969 367.085938 98.914062 390.859375 C 80.929688 419.121094 33.304688 407.113281 37.25 366.609375 C 36.863281 369.894531 36.5625 373.210938 36.355469 376.546875 C 29.84375 481.933594 113.398438 569.453125 217.375 569.453125 C 331.96875 569.453125 605.269531 426.671875 577.609375 283.609375 C 548.457031 199.519531 466.523438 139.203125 373.664062 139.203125 C 280.808594 139.203125 221.296875 192.699219 192.171875 238.8125 Z M 192.171875 238.8125 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:url(#radial2);" d="M 192.171875 238.8125 C 158.871094 291.535156 114.042969 367.085938 98.914062 390.859375 C 80.929688 419.121094 33.304688 407.113281 37.25 366.609375 C 36.863281 369.894531 36.5625 373.210938 36.355469 376.546875 C 29.84375 481.933594 113.398438 569.453125 217.375 569.453125 C 331.96875 569.453125 605.269531 426.671875 577.609375 283.609375 C 548.457031 199.519531 466.523438 139.203125 373.664062 139.203125 C 280.808594 139.203125 221.296875 192.699219 192.171875 238.8125 Z M 192.171875 238.8125 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:url(#radial3);" d="M 192.171875 238.8125 C 158.871094 291.535156 114.042969 367.085938 98.914062 390.859375 C 80.929688 419.121094 33.304688 407.113281 37.25 366.609375 C 36.863281 369.894531 36.5625 373.210938 36.355469 376.546875 C 29.84375 481.933594 113.398438 569.453125 217.375 569.453125 C 331.96875 569.453125 605.269531 426.671875 577.609375 283.609375 C 548.457031 199.519531 466.523438 139.203125 373.664062 139.203125 C 280.808594 139.203125 221.296875 192.699219 192.171875 238.8125 Z M 192.171875 238.8125 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:url(#radial4);" d="M 192.171875 238.8125 C 158.871094 291.535156 114.042969 367.085938 98.914062 390.859375 C 80.929688 419.121094 33.304688 407.113281 37.25 366.609375 C 36.863281 369.894531 36.5625 373.210938 36.355469 376.546875 C 29.84375 481.933594 113.398438 569.453125 217.375 569.453125 C 331.96875 569.453125 605.269531 426.671875 577.609375 283.609375 C 548.457031 199.519531 466.523438 139.203125 373.664062 139.203125 C 280.808594 139.203125 221.296875 192.699219 192.171875 238.8125 Z M 192.171875 238.8125 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:url(#radial5);" d="M 192.171875 238.8125 C 158.871094 291.535156 114.042969 367.085938 98.914062 390.859375 C 80.929688 419.121094 33.304688 407.113281 37.25 366.609375 C 36.863281 369.894531 36.5625 373.210938 36.355469 376.546875 C 29.84375 481.933594 113.398438 569.453125 217.375 569.453125 C 331.96875 569.453125 605.269531 426.671875 577.609375 283.609375 C 548.457031 199.519531 466.523438 139.203125 373.664062 139.203125 C 280.808594 139.203125 221.296875 192.699219 192.171875 238.8125 Z M 192.171875 238.8125 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:url(#linear0);" d="M 215.699219 569.496094 C 215.699219 569.496094 489.320312 570.035156 535.734375 570.035156 C 619.960938 570.035156 684 501.273438 684 421.03125 C 684 340.789062 618.671875 272.445312 535.734375 272.445312 C 452.792969 272.445312 405.027344 334.492188 369.152344 402.226562 C 327.117188 481.59375 273.488281 568.546875 215.699219 569.496094 Z M 215.699219 569.496094 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:url(#radial6);" d="M 215.699219 569.496094 C 215.699219 569.496094 489.320312 570.035156 535.734375 570.035156 C 619.960938 570.035156 684 501.273438 684 421.03125 C 684 340.789062 618.671875 272.445312 535.734375 272.445312 C 452.792969 272.445312 405.027344 334.492188 369.152344 402.226562 C 327.117188 481.59375 273.488281 568.546875 215.699219 569.496094 Z M 215.699219 569.496094 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:url(#radial7);" d="M 215.699219 569.496094 C 215.699219 569.496094 489.320312 570.035156 535.734375 570.035156 C 619.960938 570.035156 684 501.273438 684 421.03125 C 684 340.789062 618.671875 272.445312 535.734375 272.445312 C 452.792969 272.445312 405.027344 334.492188 369.152344 402.226562 C 327.117188 481.59375 273.488281 568.546875 215.699219 569.496094 Z M 215.699219 569.496094 "/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8 KiB |
Loading…
Add table
Add a link
Reference in a new issue