Implement unified cloud picker with support for Google Drive and OneDrive, replacing individual pickers. Add new components for file listing, ingest settings, and file item display. Update package dependencies to include @radix-ui/react-separator.
This commit is contained in:
parent
994b71b40f
commit
9a8818a81b
16 changed files with 1239 additions and 1005 deletions
31
frontend/components/ui/separator.tsx
Normal file
31
frontend/components/ui/separator.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Separator = React.forwardRef<
|
||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
||||
>(
|
||||
(
|
||||
{ className, orientation = "horizontal", decorative = true, ...props },
|
||||
ref
|
||||
) => (
|
||||
<SeparatorPrimitive.Root
|
||||
ref={ref}
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"shrink-0 bg-border",
|
||||
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName;
|
||||
|
||||
export { Separator };
|
||||
23
frontend/package-lock.json
generated
23
frontend/package-lock.json
generated
|
|
@ -19,6 +19,7 @@
|
|||
"@radix-ui/react-navigation-menu": "^1.2.13",
|
||||
"@radix-ui/react-popover": "^1.1.15",
|
||||
"@radix-ui/react-select": "^2.2.5",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slider": "^1.3.6",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.2.5",
|
||||
|
|
@ -1855,6 +1856,28 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-separator": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz",
|
||||
"integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-primitive": "2.1.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slider": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"@radix-ui/react-navigation-menu": "^1.2.13",
|
||||
"@radix-ui/react-popover": "^1.1.15",
|
||||
"@radix-ui/react-select": "^2.2.5",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slider": "^1.3.6",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.2.5",
|
||||
|
|
|
|||
|
|
@ -1,20 +1,14 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { GoogleDrivePicker } from "@/components/google-drive-picker"
|
||||
import { useTask } from "@/contexts/task-context"
|
||||
import { UnifiedCloudPicker, CloudFile } from "@/components/cloud-picker";
|
||||
import { useTask } from "@/contexts/task-context";
|
||||
|
||||
interface GoogleDriveFile {
|
||||
id: string;
|
||||
name: string;
|
||||
mimeType: string;
|
||||
webViewLink?: string;
|
||||
iconLink?: string;
|
||||
}
|
||||
// CloudFile interface is now imported from the unified cloud picker
|
||||
|
||||
export default function ConnectorsPage() {
|
||||
const { addTask } = useTask()
|
||||
const [selectedFiles, setSelectedFiles] = useState<GoogleDriveFile[]>([]);
|
||||
const { addTask } = useTask();
|
||||
const [selectedFiles, setSelectedFiles] = useState<CloudFile[]>([]);
|
||||
const [isSyncing, setIsSyncing] = useState<boolean>(false);
|
||||
const [syncResult, setSyncResult] = useState<{
|
||||
processed?: number;
|
||||
|
|
@ -25,16 +19,19 @@ export default function ConnectorsPage() {
|
|||
errors?: number;
|
||||
} | null>(null);
|
||||
|
||||
const handleFileSelection = (files: GoogleDriveFile[]) => {
|
||||
const handleFileSelection = (files: CloudFile[]) => {
|
||||
setSelectedFiles(files);
|
||||
};
|
||||
|
||||
const handleSync = async (connector: { connectionId: string, type: string }) => {
|
||||
if (!connector.connectionId || selectedFiles.length === 0) return
|
||||
|
||||
setIsSyncing(true)
|
||||
setSyncResult(null)
|
||||
|
||||
const handleSync = async (connector: {
|
||||
connectionId: string;
|
||||
type: string;
|
||||
}) => {
|
||||
if (!connector.connectionId || selectedFiles.length === 0) return;
|
||||
|
||||
setIsSyncing(true);
|
||||
setSyncResult(null);
|
||||
|
||||
try {
|
||||
const syncBody: {
|
||||
connection_id: string;
|
||||
|
|
@ -42,54 +39,55 @@ export default function ConnectorsPage() {
|
|||
selected_files?: string[];
|
||||
} = {
|
||||
connection_id: connector.connectionId,
|
||||
selected_files: selectedFiles.map(file => file.id)
|
||||
}
|
||||
|
||||
selected_files: selectedFiles.map(file => file.id),
|
||||
};
|
||||
|
||||
const response = await fetch(`/api/connectors/${connector.type}/sync`, {
|
||||
method: 'POST',
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(syncBody),
|
||||
})
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.status === 201) {
|
||||
const taskId = result.task_id
|
||||
const taskId = result.task_id;
|
||||
if (taskId) {
|
||||
addTask(taskId)
|
||||
setSyncResult({
|
||||
processed: 0,
|
||||
addTask(taskId);
|
||||
setSyncResult({
|
||||
processed: 0,
|
||||
total: selectedFiles.length,
|
||||
status: 'started'
|
||||
})
|
||||
status: "started",
|
||||
});
|
||||
}
|
||||
} else if (response.ok) {
|
||||
setSyncResult(result)
|
||||
setSyncResult(result);
|
||||
} else {
|
||||
console.error('Sync failed:', result.error)
|
||||
setSyncResult({ error: result.error || 'Sync failed' })
|
||||
console.error("Sync failed:", result.error);
|
||||
setSyncResult({ error: result.error || "Sync failed" });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Sync error:', error)
|
||||
setSyncResult({ error: 'Network error occurred' })
|
||||
console.error("Sync error:", error);
|
||||
setSyncResult({ error: "Network error occurred" });
|
||||
} finally {
|
||||
setIsSyncing(false)
|
||||
setIsSyncing(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<h1 className="text-2xl font-bold mb-4">Connectors</h1>
|
||||
|
||||
|
||||
<div className="mb-6">
|
||||
<p className="text-sm text-gray-600 mb-4">
|
||||
This is a demo page for the Google Drive picker component.
|
||||
For full connector functionality, visit the Settings page.
|
||||
This is a demo page for the Google Drive picker component. For full
|
||||
connector functionality, visit the Settings page.
|
||||
</p>
|
||||
|
||||
<GoogleDrivePicker
|
||||
|
||||
<UnifiedCloudPicker
|
||||
provider="google_drive"
|
||||
onFileSelected={handleFileSelection}
|
||||
selectedFiles={selectedFiles}
|
||||
isAuthenticated={false} // This would come from auth context in real usage
|
||||
|
|
@ -99,8 +97,13 @@ export default function ConnectorsPage() {
|
|||
|
||||
{selectedFiles.length > 0 && (
|
||||
<div className="space-y-4">
|
||||
<button
|
||||
onClick={() => handleSync({ connectionId: "google-drive-connection-id", type: "google-drive" })}
|
||||
<button
|
||||
onClick={() =>
|
||||
handleSync({
|
||||
connectionId: "google-drive-connection-id",
|
||||
type: "google-drive",
|
||||
})
|
||||
}
|
||||
disabled={isSyncing}
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
|
|
@ -110,14 +113,15 @@ export default function ConnectorsPage() {
|
|||
<>Sync {selectedFiles.length} Selected Items</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
|
||||
{syncResult && (
|
||||
<div className="p-3 bg-gray-100 rounded text-sm">
|
||||
{syncResult.error ? (
|
||||
<div className="text-red-600">Error: {syncResult.error}</div>
|
||||
) : syncResult.status === 'started' ? (
|
||||
) : syncResult.status === "started" ? (
|
||||
<div className="text-blue-600">
|
||||
Sync started for {syncResult.total} files. Check the task notification for progress.
|
||||
Sync started for {syncResult.total} files. Check the task
|
||||
notification for progress.
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-green-600">
|
||||
|
|
|
|||
|
|
@ -1,110 +1,105 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { useParams, useRouter } from "next/navigation"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { ArrowLeft, AlertCircle } from "lucide-react"
|
||||
import { GoogleDrivePicker } from "@/components/google-drive-picker"
|
||||
import { OneDrivePicker } from "@/components/onedrive-picker"
|
||||
import { useTask } from "@/contexts/task-context"
|
||||
import { Toast } from "@/components/ui/toast"
|
||||
import { useState, useEffect } from "react";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ArrowLeft, AlertCircle } from "lucide-react";
|
||||
import { UnifiedCloudPicker, CloudFile } from "@/components/cloud-picker";
|
||||
import { useTask } from "@/contexts/task-context";
|
||||
import { Toast } from "@/components/ui/toast";
|
||||
|
||||
interface GoogleDriveFile {
|
||||
id: string
|
||||
name: string
|
||||
mimeType: string
|
||||
webViewLink?: string
|
||||
iconLink?: string
|
||||
}
|
||||
|
||||
interface OneDriveFile {
|
||||
id: string
|
||||
name: string
|
||||
mimeType?: string
|
||||
webUrl?: string
|
||||
driveItem?: {
|
||||
file?: { mimeType: string }
|
||||
folder?: unknown
|
||||
}
|
||||
}
|
||||
// CloudFile interface is now imported from the unified cloud picker
|
||||
|
||||
interface CloudConnector {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
status: "not_connected" | "connecting" | "connected" | "error"
|
||||
type: string
|
||||
connectionId?: string
|
||||
clientId: string
|
||||
hasAccessToken: boolean
|
||||
accessTokenError?: string
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
status: "not_connected" | "connecting" | "connected" | "error";
|
||||
type: string;
|
||||
connectionId?: string;
|
||||
clientId: string;
|
||||
hasAccessToken: boolean;
|
||||
accessTokenError?: string;
|
||||
}
|
||||
|
||||
export default function UploadProviderPage() {
|
||||
const params = useParams()
|
||||
const router = useRouter()
|
||||
const provider = params.provider as string
|
||||
const { addTask, tasks } = useTask()
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
const provider = params.provider as string;
|
||||
const { addTask, tasks } = useTask();
|
||||
|
||||
const [connector, setConnector] = useState<CloudConnector | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [accessToken, setAccessToken] = useState<string | null>(null)
|
||||
const [selectedFiles, setSelectedFiles] = useState<GoogleDriveFile[] | OneDriveFile[]>([])
|
||||
const [isIngesting, setIsIngesting] = useState<boolean>(false)
|
||||
const [currentSyncTaskId, setCurrentSyncTaskId] = useState<string | null>(null)
|
||||
const [showSuccessToast, setShowSuccessToast] = useState(false)
|
||||
const [connector, setConnector] = useState<CloudConnector | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [accessToken, setAccessToken] = useState<string | null>(null);
|
||||
const [selectedFiles, setSelectedFiles] = useState<CloudFile[]>([]);
|
||||
const [isIngesting, setIsIngesting] = useState<boolean>(false);
|
||||
const [currentSyncTaskId, setCurrentSyncTaskId] = useState<string | null>(
|
||||
null
|
||||
);
|
||||
const [showSuccessToast, setShowSuccessToast] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchConnectorInfo = async () => {
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// Fetch available connectors to validate the provider
|
||||
const connectorsResponse = await fetch('/api/connectors')
|
||||
const connectorsResponse = await fetch("/api/connectors");
|
||||
if (!connectorsResponse.ok) {
|
||||
throw new Error('Failed to load connectors')
|
||||
throw new Error("Failed to load connectors");
|
||||
}
|
||||
|
||||
const connectorsResult = await connectorsResponse.json()
|
||||
const providerInfo = connectorsResult.connectors[provider]
|
||||
const connectorsResult = await connectorsResponse.json();
|
||||
const providerInfo = connectorsResult.connectors[provider];
|
||||
|
||||
if (!providerInfo || !providerInfo.available) {
|
||||
setError(`Cloud provider "${provider}" is not available or configured.`)
|
||||
return
|
||||
setError(
|
||||
`Cloud provider "${provider}" is not available or configured.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check connector status
|
||||
const statusResponse = await fetch(`/api/connectors/${provider}/status`)
|
||||
const statusResponse = await fetch(
|
||||
`/api/connectors/${provider}/status`
|
||||
);
|
||||
if (!statusResponse.ok) {
|
||||
throw new Error(`Failed to check ${provider} status`)
|
||||
throw new Error(`Failed to check ${provider} status`);
|
||||
}
|
||||
|
||||
const statusData = await statusResponse.json()
|
||||
const connections = statusData.connections || []
|
||||
const activeConnection = connections.find((conn: {is_active: boolean, connection_id: string}) => conn.is_active)
|
||||
const isConnected = activeConnection !== undefined
|
||||
const statusData = await statusResponse.json();
|
||||
const connections = statusData.connections || [];
|
||||
const activeConnection = connections.find(
|
||||
(conn: { is_active: boolean; connection_id: string }) =>
|
||||
conn.is_active
|
||||
);
|
||||
const isConnected = activeConnection !== undefined;
|
||||
|
||||
let hasAccessToken = false
|
||||
let accessTokenError: string | undefined = undefined
|
||||
let hasAccessToken = false;
|
||||
let accessTokenError: string | undefined = undefined;
|
||||
|
||||
// Try to get access token for connected connectors
|
||||
if (isConnected && activeConnection) {
|
||||
try {
|
||||
const tokenResponse = await fetch(`/api/connectors/${provider}/token?connection_id=${activeConnection.connection_id}`)
|
||||
const tokenResponse = await fetch(
|
||||
`/api/connectors/${provider}/token?connection_id=${activeConnection.connection_id}`
|
||||
);
|
||||
if (tokenResponse.ok) {
|
||||
const tokenData = await tokenResponse.json()
|
||||
const tokenData = await tokenResponse.json();
|
||||
if (tokenData.access_token) {
|
||||
hasAccessToken = true
|
||||
setAccessToken(tokenData.access_token)
|
||||
hasAccessToken = true;
|
||||
setAccessToken(tokenData.access_token);
|
||||
}
|
||||
} else {
|
||||
const errorData = await tokenResponse.json().catch(() => ({ error: 'Token unavailable' }))
|
||||
accessTokenError = errorData.error || 'Access token unavailable'
|
||||
const errorData = await tokenResponse
|
||||
.json()
|
||||
.catch(() => ({ error: "Token unavailable" }));
|
||||
accessTokenError = errorData.error || "Access token unavailable";
|
||||
}
|
||||
} catch {
|
||||
accessTokenError = 'Failed to fetch access token'
|
||||
accessTokenError = "Failed to fetch access token";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -117,61 +112,56 @@ export default function UploadProviderPage() {
|
|||
connectionId: activeConnection?.connection_id,
|
||||
clientId: activeConnection?.client_id,
|
||||
hasAccessToken,
|
||||
accessTokenError
|
||||
})
|
||||
|
||||
accessTokenError,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to load connector info:', error)
|
||||
setError(error instanceof Error ? error.message : 'Failed to load connector information')
|
||||
console.error("Failed to load connector info:", error);
|
||||
setError(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Failed to load connector information"
|
||||
);
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (provider) {
|
||||
fetchConnectorInfo()
|
||||
fetchConnectorInfo();
|
||||
}
|
||||
}, [provider])
|
||||
}, [provider]);
|
||||
|
||||
// Watch for sync task completion and redirect
|
||||
useEffect(() => {
|
||||
if (!currentSyncTaskId) return
|
||||
|
||||
const currentTask = tasks.find(task => task.task_id === currentSyncTaskId)
|
||||
|
||||
if (currentTask && currentTask.status === 'completed') {
|
||||
if (!currentSyncTaskId) return;
|
||||
|
||||
const currentTask = tasks.find(task => task.task_id === currentSyncTaskId);
|
||||
|
||||
if (currentTask && currentTask.status === "completed") {
|
||||
// Task completed successfully, show toast and redirect
|
||||
setIsIngesting(false)
|
||||
setShowSuccessToast(true)
|
||||
setIsIngesting(false);
|
||||
setShowSuccessToast(true);
|
||||
setTimeout(() => {
|
||||
router.push('/knowledge')
|
||||
}, 2000) // 2 second delay to let user see toast
|
||||
} else if (currentTask && currentTask.status === 'failed') {
|
||||
router.push("/knowledge");
|
||||
}, 2000); // 2 second delay to let user see toast
|
||||
} else if (currentTask && currentTask.status === "failed") {
|
||||
// Task failed, clear the tracking but don't redirect
|
||||
setIsIngesting(false)
|
||||
setCurrentSyncTaskId(null)
|
||||
setIsIngesting(false);
|
||||
setCurrentSyncTaskId(null);
|
||||
}
|
||||
}, [tasks, currentSyncTaskId, router])
|
||||
}, [tasks, currentSyncTaskId, router]);
|
||||
|
||||
const handleFileSelected = (files: GoogleDriveFile[] | OneDriveFile[]) => {
|
||||
setSelectedFiles(files)
|
||||
console.log(`Selected ${files.length} files from ${provider}:`, files)
|
||||
const handleFileSelected = (files: CloudFile[]) => {
|
||||
setSelectedFiles(files);
|
||||
console.log(`Selected ${files.length} files from ${provider}:`, files);
|
||||
// You can add additional handling here like triggering sync, etc.
|
||||
}
|
||||
|
||||
const handleGoogleDriveFileSelected = (files: GoogleDriveFile[]) => {
|
||||
handleFileSelected(files)
|
||||
}
|
||||
|
||||
const handleOneDriveFileSelected = (files: OneDriveFile[]) => {
|
||||
handleFileSelected(files)
|
||||
}
|
||||
};
|
||||
|
||||
const handleSync = async (connector: CloudConnector) => {
|
||||
if (!connector.connectionId || selectedFiles.length === 0) return
|
||||
|
||||
setIsIngesting(true)
|
||||
|
||||
if (!connector.connectionId || selectedFiles.length === 0) return;
|
||||
|
||||
setIsIngesting(true);
|
||||
|
||||
try {
|
||||
const syncBody: {
|
||||
connection_id: string;
|
||||
|
|
@ -179,43 +169,43 @@ export default function UploadProviderPage() {
|
|||
selected_files?: string[];
|
||||
} = {
|
||||
connection_id: connector.connectionId,
|
||||
selected_files: selectedFiles.map(file => file.id)
|
||||
}
|
||||
|
||||
selected_files: selectedFiles.map(file => file.id),
|
||||
};
|
||||
|
||||
const response = await fetch(`/api/connectors/${connector.type}/sync`, {
|
||||
method: 'POST',
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(syncBody),
|
||||
})
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.status === 201) {
|
||||
const taskIds = result.task_ids
|
||||
const taskIds = result.task_ids;
|
||||
if (taskIds && taskIds.length > 0) {
|
||||
const taskId = taskIds[0] // Use the first task ID
|
||||
addTask(taskId)
|
||||
setCurrentSyncTaskId(taskId)
|
||||
const taskId = taskIds[0]; // Use the first task ID
|
||||
addTask(taskId);
|
||||
setCurrentSyncTaskId(taskId);
|
||||
}
|
||||
} else {
|
||||
console.error('Sync failed:', result.error)
|
||||
console.error("Sync failed:", result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Sync error:', error)
|
||||
setIsIngesting(false)
|
||||
console.error("Sync error:", error);
|
||||
setIsIngesting(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getProviderDisplayName = () => {
|
||||
const nameMap: { [key: string]: string } = {
|
||||
'google_drive': 'Google Drive',
|
||||
'onedrive': 'OneDrive',
|
||||
'sharepoint': 'SharePoint'
|
||||
}
|
||||
return nameMap[provider] || provider
|
||||
}
|
||||
google_drive: "Google Drive",
|
||||
onedrive: "OneDrive",
|
||||
sharepoint: "SharePoint",
|
||||
};
|
||||
return nameMap[provider] || provider;
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
|
|
@ -227,15 +217,15 @@ export default function UploadProviderPage() {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !connector) {
|
||||
return (
|
||||
<div className="container mx-auto p-6">
|
||||
<div className="mb-6">
|
||||
<Button
|
||||
variant="ghost"
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => router.back()}
|
||||
className="mb-4"
|
||||
>
|
||||
|
|
@ -243,27 +233,29 @@ export default function UploadProviderPage() {
|
|||
Back
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="text-center max-w-md">
|
||||
<AlertCircle className="h-12 w-12 text-red-500 mx-auto mb-4" />
|
||||
<h2 className="text-xl font-semibold mb-2">Provider Not Available</h2>
|
||||
<h2 className="text-xl font-semibold mb-2">
|
||||
Provider Not Available
|
||||
</h2>
|
||||
<p className="text-muted-foreground mb-4">{error}</p>
|
||||
<Button onClick={() => router.push('/settings')}>
|
||||
<Button onClick={() => router.push("/settings")}>
|
||||
Configure Connectors
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (connector.status !== "connected") {
|
||||
return (
|
||||
<div className="container mx-auto p-6">
|
||||
<div className="mb-6">
|
||||
<Button
|
||||
variant="ghost"
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => router.back()}
|
||||
className="mb-4"
|
||||
>
|
||||
|
|
@ -271,29 +263,32 @@ export default function UploadProviderPage() {
|
|||
Back
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="text-center max-w-md">
|
||||
<AlertCircle className="h-12 w-12 text-yellow-500 mx-auto mb-4" />
|
||||
<h2 className="text-xl font-semibold mb-2">{connector.name} Not Connected</h2>
|
||||
<h2 className="text-xl font-semibold mb-2">
|
||||
{connector.name} Not Connected
|
||||
</h2>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
You need to connect your {connector.name} account before you can select files.
|
||||
You need to connect your {connector.name} account before you can
|
||||
select files.
|
||||
</p>
|
||||
<Button onClick={() => router.push('/settings')}>
|
||||
<Button onClick={() => router.push("/settings")}>
|
||||
Connect {connector.name}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (!connector.hasAccessToken) {
|
||||
return (
|
||||
<div className="container mx-auto p-6">
|
||||
<div className="mb-6">
|
||||
<Button
|
||||
variant="ghost"
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => router.back()}
|
||||
className="mb-4"
|
||||
>
|
||||
|
|
@ -301,81 +296,80 @@ export default function UploadProviderPage() {
|
|||
Back
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="text-center max-w-md">
|
||||
<AlertCircle className="h-12 w-12 text-red-500 mx-auto mb-4" />
|
||||
<h2 className="text-xl font-semibold mb-2">Access Token Required</h2>
|
||||
<h2 className="text-xl font-semibold mb-2">
|
||||
Access Token Required
|
||||
</h2>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
{connector.accessTokenError || `Unable to get access token for ${connector.name}. Try reconnecting your account.`}
|
||||
{connector.accessTokenError ||
|
||||
`Unable to get access token for ${connector.name}. Try reconnecting your account.`}
|
||||
</p>
|
||||
<Button onClick={() => router.push('/settings')}>
|
||||
<Button onClick={() => router.push("/settings")}>
|
||||
Reconnect {connector.name}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto max-w-3xl p-6">
|
||||
<div className="mb-6 flex gap-2 items-center">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => router.back()}
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4 scale-125 mr-2" />
|
||||
<Button variant="ghost" onClick={() => router.back()}>
|
||||
<ArrowLeft className="h-4 w-4 scale-125" />
|
||||
</Button>
|
||||
<h2 className="text-2xl font-bold">Add Cloud Knowledge</h2>
|
||||
<h2 className="text-2xl font-bold">
|
||||
Add from {getProviderDisplayName()}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="max-w-3xl mx-auto">
|
||||
{connector.type === "google_drive" && (
|
||||
<GoogleDrivePicker
|
||||
onFileSelected={handleGoogleDriveFileSelected}
|
||||
selectedFiles={selectedFiles as GoogleDriveFile[]}
|
||||
isAuthenticated={true}
|
||||
accessToken={accessToken || undefined}
|
||||
/>
|
||||
)}
|
||||
|
||||
{(connector.type === "onedrive" || connector.type === "sharepoint") && (
|
||||
<OneDrivePicker
|
||||
onFileSelected={handleOneDriveFileSelected}
|
||||
selectedFiles={selectedFiles as OneDriveFile[]}
|
||||
isAuthenticated={true}
|
||||
accessToken={accessToken || undefined}
|
||||
connectorType={connector.type as "onedrive" | "sharepoint"}
|
||||
clientId={connector.clientId}
|
||||
/>
|
||||
)}
|
||||
<UnifiedCloudPicker
|
||||
provider={
|
||||
connector.type as "google_drive" | "onedrive" | "sharepoint"
|
||||
}
|
||||
onFileSelected={handleFileSelected}
|
||||
selectedFiles={selectedFiles}
|
||||
isAuthenticated={true}
|
||||
accessToken={accessToken || undefined}
|
||||
clientId={connector.clientId}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{selectedFiles.length > 0 && (
|
||||
<div className="max-w-3xl mx-auto mt-8">
|
||||
<div className="flex justify-end gap-3 mb-4">
|
||||
<Button
|
||||
onClick={() => handleSync(connector)}
|
||||
disabled={selectedFiles.length === 0 || isIngesting}
|
||||
>
|
||||
{isIngesting ? (
|
||||
<>Ingesting {selectedFiles.length} Files...</>
|
||||
) : (
|
||||
<>Ingest Files ({selectedFiles.length})</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="max-w-3xl mx-auto mt-6">
|
||||
<div className="flex justify-between gap-3 mb-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className=" border bg-transparent border-border rounded-lg text-secondary-foreground"
|
||||
onClick={() => router.back()}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => handleSync(connector)}
|
||||
disabled={selectedFiles.length === 0 || isIngesting}
|
||||
>
|
||||
{isIngesting ? (
|
||||
<>Ingesting {selectedFiles.length} Files...</>
|
||||
) : (
|
||||
<>Start ingest</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
||||
{/* Success toast notification */}
|
||||
<Toast
|
||||
<Toast
|
||||
message="Ingested successfully!."
|
||||
show={showSuccessToast}
|
||||
onHide={() => setShowSuccessToast(false)}
|
||||
duration={20000}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,112 +1,101 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
import { GoogleDrivePicker } from "@/components/google-drive-picker"
|
||||
import { OneDrivePicker } from "@/components/onedrive-picker"
|
||||
import { Loader2 } from "lucide-react"
|
||||
} from "@/components/ui/dialog";
|
||||
import { UnifiedCloudPicker, CloudFile } from "@/components/cloud-picker";
|
||||
import { Loader2 } from "lucide-react";
|
||||
|
||||
interface GoogleDriveFile {
|
||||
id: string
|
||||
name: string
|
||||
mimeType: string
|
||||
webViewLink?: string
|
||||
iconLink?: string
|
||||
}
|
||||
|
||||
interface OneDriveFile {
|
||||
id: string
|
||||
name: string
|
||||
mimeType?: string
|
||||
webUrl?: string
|
||||
driveItem?: {
|
||||
file?: { mimeType: string }
|
||||
folder?: unknown
|
||||
}
|
||||
}
|
||||
// CloudFile interface is now imported from the unified cloud picker
|
||||
|
||||
interface CloudConnector {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
icon: React.ReactNode
|
||||
status: "not_connected" | "connecting" | "connected" | "error"
|
||||
type: string
|
||||
connectionId?: string
|
||||
clientId: string
|
||||
hasAccessToken: boolean
|
||||
accessTokenError?: string
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
icon: React.ReactNode;
|
||||
status: "not_connected" | "connecting" | "connected" | "error";
|
||||
type: string;
|
||||
connectionId?: string;
|
||||
clientId: string;
|
||||
hasAccessToken: boolean;
|
||||
accessTokenError?: string;
|
||||
}
|
||||
|
||||
interface CloudConnectorsDialogProps {
|
||||
isOpen: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
onFileSelected?: (files: GoogleDriveFile[] | OneDriveFile[], connectorType: string) => void
|
||||
isOpen: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onFileSelected?: (files: CloudFile[], connectorType: string) => void;
|
||||
}
|
||||
|
||||
export function CloudConnectorsDialog({
|
||||
isOpen,
|
||||
export function CloudConnectorsDialog({
|
||||
isOpen,
|
||||
onOpenChange,
|
||||
onFileSelected
|
||||
onFileSelected,
|
||||
}: CloudConnectorsDialogProps) {
|
||||
const [connectors, setConnectors] = useState<CloudConnector[]>([])
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [selectedFiles, setSelectedFiles] = useState<{[connectorId: string]: GoogleDriveFile[] | OneDriveFile[]}>({})
|
||||
const [connectorAccessTokens, setConnectorAccessTokens] = useState<{[connectorType: string]: string}>({})
|
||||
const [activePickerType, setActivePickerType] = useState<string | null>(null)
|
||||
const [connectors, setConnectors] = useState<CloudConnector[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [selectedFiles, setSelectedFiles] = useState<{
|
||||
[connectorId: string]: CloudFile[];
|
||||
}>({});
|
||||
const [connectorAccessTokens, setConnectorAccessTokens] = useState<{
|
||||
[connectorType: string]: string;
|
||||
}>({});
|
||||
const [activePickerType, setActivePickerType] = useState<string | null>(null);
|
||||
|
||||
const getConnectorIcon = (iconName: string) => {
|
||||
const iconMap: { [key: string]: React.ReactElement } = {
|
||||
'google-drive': (
|
||||
"google-drive": (
|
||||
<div className="w-8 h-8 bg-blue-600 rounded flex items-center justify-center text-white font-bold leading-none shrink-0">
|
||||
G
|
||||
</div>
|
||||
),
|
||||
'sharepoint': (
|
||||
sharepoint: (
|
||||
<div className="w-8 h-8 bg-blue-700 rounded flex items-center justify-center text-white font-bold leading-none shrink-0">
|
||||
SP
|
||||
</div>
|
||||
),
|
||||
'onedrive': (
|
||||
onedrive: (
|
||||
<div className="w-8 h-8 bg-blue-400 rounded flex items-center justify-center text-white font-bold leading-none shrink-0">
|
||||
OD
|
||||
</div>
|
||||
),
|
||||
}
|
||||
return iconMap[iconName] || (
|
||||
<div className="w-8 h-8 bg-gray-500 rounded flex items-center justify-center text-white font-bold leading-none shrink-0">
|
||||
?
|
||||
</div>
|
||||
)
|
||||
}
|
||||
};
|
||||
return (
|
||||
iconMap[iconName] || (
|
||||
<div className="w-8 h-8 bg-gray-500 rounded flex items-center justify-center text-white font-bold leading-none shrink-0">
|
||||
?
|
||||
</div>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const fetchConnectorStatuses = useCallback(async () => {
|
||||
if (!isOpen) return
|
||||
|
||||
setIsLoading(true)
|
||||
if (!isOpen) return;
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
// Fetch available connectors from backend
|
||||
const connectorsResponse = await fetch('/api/connectors')
|
||||
const connectorsResponse = await fetch("/api/connectors");
|
||||
if (!connectorsResponse.ok) {
|
||||
throw new Error('Failed to load connectors')
|
||||
throw new Error("Failed to load connectors");
|
||||
}
|
||||
|
||||
const connectorsResult = await connectorsResponse.json()
|
||||
const connectorTypes = Object.keys(connectorsResult.connectors)
|
||||
|
||||
|
||||
const connectorsResult = await connectorsResponse.json();
|
||||
const connectorTypes = Object.keys(connectorsResult.connectors);
|
||||
|
||||
// Filter to only cloud connectors
|
||||
const cloudConnectorTypes = connectorTypes.filter(type =>
|
||||
['google_drive', 'onedrive', 'sharepoint'].includes(type) &&
|
||||
connectorsResult.connectors[type].available
|
||||
)
|
||||
|
||||
const cloudConnectorTypes = connectorTypes.filter(
|
||||
type =>
|
||||
["google_drive", "onedrive", "sharepoint"].includes(type) &&
|
||||
connectorsResult.connectors[type].available
|
||||
);
|
||||
|
||||
// Initialize connectors list
|
||||
const initialConnectors = cloudConnectorTypes.map(type => ({
|
||||
id: type,
|
||||
|
|
@ -117,83 +106,94 @@ export function CloudConnectorsDialog({
|
|||
type: type,
|
||||
hasAccessToken: false,
|
||||
accessTokenError: undefined,
|
||||
clientId: ""
|
||||
}))
|
||||
|
||||
setConnectors(initialConnectors)
|
||||
clientId: "",
|
||||
}));
|
||||
|
||||
setConnectors(initialConnectors);
|
||||
|
||||
// Check status for each cloud connector type
|
||||
for (const connectorType of cloudConnectorTypes) {
|
||||
try {
|
||||
const response = await fetch(`/api/connectors/${connectorType}/status`)
|
||||
const response = await fetch(
|
||||
`/api/connectors/${connectorType}/status`
|
||||
);
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
const connections = data.connections || []
|
||||
const activeConnection = connections.find((conn: { connection_id: string; is_active: boolean }) => conn.is_active)
|
||||
const isConnected = activeConnection !== undefined
|
||||
|
||||
let hasAccessToken = false
|
||||
let accessTokenError: string | undefined = undefined
|
||||
const data = await response.json();
|
||||
const connections = data.connections || [];
|
||||
const activeConnection = connections.find(
|
||||
(conn: { connection_id: string; is_active: boolean }) =>
|
||||
conn.is_active
|
||||
);
|
||||
const isConnected = activeConnection !== undefined;
|
||||
|
||||
let hasAccessToken = false;
|
||||
let accessTokenError: string | undefined = undefined;
|
||||
|
||||
// Try to get access token for connected connectors
|
||||
if (isConnected && activeConnection) {
|
||||
try {
|
||||
const tokenResponse = await fetch(`/api/connectors/${connectorType}/token?connection_id=${activeConnection.connection_id}`)
|
||||
const tokenResponse = await fetch(
|
||||
`/api/connectors/${connectorType}/token?connection_id=${activeConnection.connection_id}`
|
||||
);
|
||||
if (tokenResponse.ok) {
|
||||
const tokenData = await tokenResponse.json()
|
||||
const tokenData = await tokenResponse.json();
|
||||
if (tokenData.access_token) {
|
||||
hasAccessToken = true
|
||||
hasAccessToken = true;
|
||||
setConnectorAccessTokens(prev => ({
|
||||
...prev,
|
||||
[connectorType]: tokenData.access_token
|
||||
}))
|
||||
[connectorType]: tokenData.access_token,
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
const errorData = await tokenResponse.json().catch(() => ({ error: 'Token unavailable' }))
|
||||
accessTokenError = errorData.error || 'Access token unavailable'
|
||||
const errorData = await tokenResponse
|
||||
.json()
|
||||
.catch(() => ({ error: "Token unavailable" }));
|
||||
accessTokenError =
|
||||
errorData.error || "Access token unavailable";
|
||||
}
|
||||
} catch {
|
||||
accessTokenError = 'Failed to fetch access token'
|
||||
accessTokenError = "Failed to fetch access token";
|
||||
}
|
||||
}
|
||||
|
||||
setConnectors(prev => prev.map(c =>
|
||||
c.type === connectorType
|
||||
? {
|
||||
...c,
|
||||
status: isConnected ? "connected" : "not_connected",
|
||||
connectionId: activeConnection?.connection_id,
|
||||
clientId: activeConnection?.client_id,
|
||||
hasAccessToken,
|
||||
accessTokenError
|
||||
}
|
||||
: c
|
||||
))
|
||||
|
||||
setConnectors(prev =>
|
||||
prev.map(c =>
|
||||
c.type === connectorType
|
||||
? {
|
||||
...c,
|
||||
status: isConnected ? "connected" : "not_connected",
|
||||
connectionId: activeConnection?.connection_id,
|
||||
clientId: activeConnection?.client_id,
|
||||
hasAccessToken,
|
||||
accessTokenError,
|
||||
}
|
||||
: c
|
||||
)
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to check status for ${connectorType}:`, error)
|
||||
console.error(`Failed to check status for ${connectorType}:`, error);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load cloud connectors:', error)
|
||||
console.error("Failed to load cloud connectors:", error);
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [isOpen])
|
||||
}, [isOpen]);
|
||||
|
||||
const handleFileSelection = (connectorId: string, files: GoogleDriveFile[] | OneDriveFile[]) => {
|
||||
const handleFileSelection = (connectorId: string, files: CloudFile[]) => {
|
||||
setSelectedFiles(prev => ({
|
||||
...prev,
|
||||
[connectorId]: files
|
||||
}))
|
||||
|
||||
onFileSelected?.(files, connectorId)
|
||||
}
|
||||
[connectorId]: files,
|
||||
}));
|
||||
|
||||
onFileSelected?.(files, connectorId);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchConnectorStatuses()
|
||||
}, [fetchConnectorStatuses])
|
||||
|
||||
fetchConnectorStatuses();
|
||||
}, [fetchConnectorStatuses]);
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
||||
|
|
@ -221,19 +221,24 @@ export function CloudConnectorsDialog({
|
|||
<div className="flex flex-wrap gap-3 justify-center">
|
||||
{connectors
|
||||
.filter(connector => connector.status === "connected")
|
||||
.map((connector) => (
|
||||
.map(connector => (
|
||||
<Button
|
||||
key={connector.id}
|
||||
variant={connector.hasAccessToken ? "default" : "secondary"}
|
||||
variant={
|
||||
connector.hasAccessToken ? "default" : "secondary"
|
||||
}
|
||||
disabled={!connector.hasAccessToken}
|
||||
title={!connector.hasAccessToken ?
|
||||
(connector.accessTokenError || "Access token required - try reconnecting your account")
|
||||
: `Select files from ${connector.name}`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
title={
|
||||
!connector.hasAccessToken
|
||||
? connector.accessTokenError ||
|
||||
"Access token required - try reconnecting your account"
|
||||
: `Select files from ${connector.name}`
|
||||
}
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (connector.hasAccessToken) {
|
||||
setActivePickerType(connector.id)
|
||||
setActivePickerType(connector.id);
|
||||
}
|
||||
}}
|
||||
className="min-w-[120px]"
|
||||
|
|
@ -246,55 +251,46 @@ export function CloudConnectorsDialog({
|
|||
{connectors.every(c => c.status !== "connected") && (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
<p>No connected cloud providers found.</p>
|
||||
<p className="text-sm mt-1">Go to Settings to connect your cloud storage accounts.</p>
|
||||
<p className="text-sm mt-1">
|
||||
Go to Settings to connect your cloud storage accounts.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Render pickers inside dialog */}
|
||||
{activePickerType && connectors.find(c => c.id === activePickerType) && (() => {
|
||||
const connector = connectors.find(c => c.id === activePickerType)!
|
||||
|
||||
if (connector.type === "google_drive") {
|
||||
|
||||
{/* Render unified picker inside dialog */}
|
||||
{activePickerType &&
|
||||
connectors.find(c => c.id === activePickerType) &&
|
||||
(() => {
|
||||
const connector = connectors.find(
|
||||
c => c.id === activePickerType
|
||||
)!;
|
||||
|
||||
return (
|
||||
<div className="mt-6">
|
||||
<GoogleDrivePicker
|
||||
onFileSelected={(files) => {
|
||||
handleFileSelection(connector.id, files)
|
||||
setActivePickerType(null)
|
||||
<UnifiedCloudPicker
|
||||
provider={
|
||||
connector.type as
|
||||
| "google_drive"
|
||||
| "onedrive"
|
||||
| "sharepoint"
|
||||
}
|
||||
onFileSelected={files => {
|
||||
handleFileSelection(connector.id, files);
|
||||
setActivePickerType(null);
|
||||
}}
|
||||
selectedFiles={selectedFiles[connector.id] as GoogleDriveFile[] || []}
|
||||
selectedFiles={selectedFiles[connector.id] || []}
|
||||
isAuthenticated={connector.status === "connected"}
|
||||
accessToken={connectorAccessTokens[connector.type]}
|
||||
onPickerStateChange={() => {}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (connector.type === "onedrive" || connector.type === "sharepoint") {
|
||||
return (
|
||||
<div className="mt-6">
|
||||
<OneDrivePicker
|
||||
onFileSelected={(files) => {
|
||||
handleFileSelection(connector.id, files)
|
||||
setActivePickerType(null)
|
||||
}}
|
||||
selectedFiles={selectedFiles[connector.id] as OneDriveFile[] || []}
|
||||
isAuthenticated={connector.status === "connected"}
|
||||
accessToken={connectorAccessTokens[connector.type]}
|
||||
connectorType={connector.type as "onedrive" | "sharepoint"}
|
||||
clientId={connector.clientId}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
})()}
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
|||
67
frontend/src/components/cloud-picker/file-item.tsx
Normal file
67
frontend/src/components/cloud-picker/file-item.tsx
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
"use client";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { FileText, Folder, Trash } from "lucide-react";
|
||||
import { CloudFile } from "./types";
|
||||
|
||||
interface FileItemProps {
|
||||
file: CloudFile;
|
||||
onRemove: (fileId: string) => void;
|
||||
}
|
||||
|
||||
const getFileIcon = (mimeType: string) => {
|
||||
if (mimeType.includes("folder")) {
|
||||
return <Folder className="h-6 w-6" />;
|
||||
}
|
||||
return <FileText className="h-6 w-6" />;
|
||||
};
|
||||
|
||||
const getMimeTypeLabel = (mimeType: string) => {
|
||||
const typeMap: { [key: string]: string } = {
|
||||
"application/vnd.google-apps.document": "Google Doc",
|
||||
"application/vnd.google-apps.spreadsheet": "Google Sheet",
|
||||
"application/vnd.google-apps.presentation": "Google Slides",
|
||||
"application/vnd.google-apps.folder": "Folder",
|
||||
"application/pdf": "PDF",
|
||||
"text/plain": "Text",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document":
|
||||
"Word Doc",
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation":
|
||||
"PowerPoint",
|
||||
};
|
||||
|
||||
return typeMap[mimeType] || mimeType?.split("/").pop() || "Document";
|
||||
};
|
||||
|
||||
const formatFileSize = (bytes?: number) => {
|
||||
if (!bytes) return "";
|
||||
const sizes = ["B", "KB", "MB", "GB", "TB"];
|
||||
if (bytes === 0) return "0 B";
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${sizes[i]}`;
|
||||
};
|
||||
|
||||
export const FileItem = ({ file, onRemove }: FileItemProps) => (
|
||||
<div
|
||||
key={file.id}
|
||||
className="flex items-center justify-between p-2 rounded-md text-xs"
|
||||
>
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
{getFileIcon(file.mimeType)}
|
||||
<span className="truncate font-medium text-sm mr-2">{file.name}</span>
|
||||
<Badge variant="secondary" className="text-xs px-1 py-0.5 h-auto">
|
||||
{getMimeTypeLabel(file.mimeType)}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-muted-foreground mr-4" title="file size">
|
||||
{formatFileSize(file.size) || "—"}
|
||||
</span>
|
||||
|
||||
<Trash
|
||||
className="text-muted-foreground w-5 h-5 cursor-pointer hover:text-destructive"
|
||||
onClick={() => onRemove(file.id)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
42
frontend/src/components/cloud-picker/file-list.tsx
Normal file
42
frontend/src/components/cloud-picker/file-list.tsx
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CloudFile } from "./types";
|
||||
import { FileItem } from "./file-item";
|
||||
|
||||
interface FileListProps {
|
||||
files: CloudFile[];
|
||||
onClearAll: () => void;
|
||||
onRemoveFile: (fileId: string) => void;
|
||||
}
|
||||
|
||||
export const FileList = ({
|
||||
files,
|
||||
onClearAll,
|
||||
onRemoveFile,
|
||||
}: FileListProps) => {
|
||||
if (files.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm font-medium">Added files</p>
|
||||
<Button
|
||||
onClick={onClearAll}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="text-sm text-muted-foreground"
|
||||
>
|
||||
Clear all
|
||||
</Button>
|
||||
</div>
|
||||
<div className="max-h-64 overflow-y-auto space-y-1">
|
||||
{files.map(file => (
|
||||
<FileItem key={file.id} file={file} onRemove={onRemoveFile} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
7
frontend/src/components/cloud-picker/index.ts
Normal file
7
frontend/src/components/cloud-picker/index.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export { UnifiedCloudPicker } from "./unified-cloud-picker";
|
||||
export { PickerHeader } from "./picker-header";
|
||||
export { FileList } from "./file-list";
|
||||
export { FileItem } from "./file-item";
|
||||
export { IngestSettings } from "./ingest-settings";
|
||||
export * from "./types";
|
||||
export * from "./provider-handlers";
|
||||
82
frontend/src/components/cloud-picker/ingest-settings.tsx
Normal file
82
frontend/src/components/cloud-picker/ingest-settings.tsx
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
"use client";
|
||||
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible";
|
||||
import { ChevronRight, Info } from "lucide-react";
|
||||
|
||||
interface IngestSettingsProps {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export const IngestSettings = ({
|
||||
isOpen,
|
||||
onOpenChange,
|
||||
}: IngestSettingsProps) => (
|
||||
<Collapsible
|
||||
open={isOpen}
|
||||
onOpenChange={onOpenChange}
|
||||
className="border rounded-md p-4 border-muted-foreground/20"
|
||||
>
|
||||
<CollapsibleTrigger className="flex items-center gap-2 justify-between w-full -m-4 p-4 rounded-md transition-colors">
|
||||
<div className="flex items-center gap-2">
|
||||
<ChevronRight
|
||||
className={`h-4 w-4 text-muted-foreground transition-transform duration-200 ${
|
||||
isOpen ? "rotate-90" : ""
|
||||
}`}
|
||||
/>
|
||||
<span className="text-sm font-medium">Ingest settings</span>
|
||||
</div>
|
||||
</CollapsibleTrigger>
|
||||
|
||||
<CollapsibleContent className="data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:slide-up-2 data-[state=open]:slide-down-2">
|
||||
<div className="pt-5 space-y-5">
|
||||
<div className="flex items-center gap-4 w-full">
|
||||
<div className="w-full">
|
||||
<div className="text-sm mb-2 font-semibold">Chunk size</div>
|
||||
<Input type="number" />
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<div className="text-sm mb-2 font-semibold">Chunk overlap</div>
|
||||
<Input type="number" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 items-center justify-between">
|
||||
<div>
|
||||
<div className="text-sm font-semibold pb-2">OCR</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Extracts text from images/PDFs. Ingest is slower when enabled.
|
||||
</div>
|
||||
</div>
|
||||
<Switch />
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 items-center justify-between">
|
||||
<div>
|
||||
<div className="text-sm pb-2 font-semibold">
|
||||
Picture descriptions
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Adds captions for images. Ingest is more expensive when enabled.
|
||||
</div>
|
||||
</div>
|
||||
<Switch />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-sm font-semibold pb-2 flex items-center">
|
||||
Embedding model
|
||||
<Info className="w-3.5 h-3.5 text-muted-foreground ml-2" />
|
||||
</div>
|
||||
<Input value="text-embedding-3-small" disabled />
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
);
|
||||
70
frontend/src/components/cloud-picker/picker-header.tsx
Normal file
70
frontend/src/components/cloud-picker/picker-header.tsx
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Plus } from "lucide-react";
|
||||
import { CloudProvider } from "./types";
|
||||
|
||||
interface PickerHeaderProps {
|
||||
provider: CloudProvider;
|
||||
onAddFiles: () => void;
|
||||
isPickerLoaded: boolean;
|
||||
isPickerOpen: boolean;
|
||||
accessToken?: string;
|
||||
isAuthenticated: boolean;
|
||||
}
|
||||
|
||||
const getProviderName = (provider: CloudProvider): string => {
|
||||
switch (provider) {
|
||||
case "google_drive":
|
||||
return "Google Drive";
|
||||
case "onedrive":
|
||||
return "OneDrive";
|
||||
case "sharepoint":
|
||||
return "SharePoint";
|
||||
default:
|
||||
return "Cloud Storage";
|
||||
}
|
||||
};
|
||||
|
||||
export const PickerHeader = ({
|
||||
provider,
|
||||
onAddFiles,
|
||||
isPickerLoaded,
|
||||
isPickerOpen,
|
||||
accessToken,
|
||||
isAuthenticated,
|
||||
}: PickerHeaderProps) => {
|
||||
if (!isAuthenticated) {
|
||||
return (
|
||||
<div className="text-sm text-muted-foreground p-4 bg-muted/20 rounded-md">
|
||||
Please connect to {getProviderName(provider)} first to select specific
|
||||
files.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="flex flex-col items-center text-center py-8">
|
||||
<p className="text-sm text-primary mb-4">
|
||||
Select files from {getProviderName(provider)} to ingest.
|
||||
</p>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={onAddFiles}
|
||||
disabled={!isPickerLoaded || isPickerOpen || !accessToken}
|
||||
className="bg-foreground text-background hover:bg-foreground/90 font-semibold"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
{isPickerOpen ? "Opening Picker..." : "Add Files"}
|
||||
</Button>
|
||||
<div className="text-xs text-muted-foreground pt-4">
|
||||
csv, json, pdf,{" "}
|
||||
<a className="underline dark:text-pink-400 text-pink-600">+16 more</a>{" "}
|
||||
<b>150 MB</b> max
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
245
frontend/src/components/cloud-picker/provider-handlers.ts
Normal file
245
frontend/src/components/cloud-picker/provider-handlers.ts
Normal file
|
|
@ -0,0 +1,245 @@
|
|||
"use client";
|
||||
|
||||
import {
|
||||
CloudFile,
|
||||
CloudProvider,
|
||||
GooglePickerData,
|
||||
GooglePickerDocument,
|
||||
} from "./types";
|
||||
|
||||
export class GoogleDriveHandler {
|
||||
private accessToken: string;
|
||||
private onPickerStateChange?: (isOpen: boolean) => void;
|
||||
|
||||
constructor(
|
||||
accessToken: string,
|
||||
onPickerStateChange?: (isOpen: boolean) => void
|
||||
) {
|
||||
this.accessToken = accessToken;
|
||||
this.onPickerStateChange = onPickerStateChange;
|
||||
}
|
||||
|
||||
async loadPickerApi(): Promise<boolean> {
|
||||
return new Promise(resolve => {
|
||||
if (typeof window !== "undefined" && window.gapi) {
|
||||
window.gapi.load("picker", {
|
||||
callback: () => resolve(true),
|
||||
onerror: () => resolve(false),
|
||||
});
|
||||
} else {
|
||||
// Load Google API script
|
||||
const script = document.createElement("script");
|
||||
script.src = "https://apis.google.com/js/api.js";
|
||||
script.async = true;
|
||||
script.defer = true;
|
||||
script.onload = () => {
|
||||
window.gapi.load("picker", {
|
||||
callback: () => resolve(true),
|
||||
onerror: () => resolve(false),
|
||||
});
|
||||
};
|
||||
script.onerror = () => resolve(false);
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
openPicker(onFileSelected: (files: CloudFile[]) => void): void {
|
||||
if (!window.google?.picker) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.onPickerStateChange?.(true);
|
||||
|
||||
const picker = new window.google.picker.PickerBuilder()
|
||||
.addView(window.google.picker.ViewId.DOCS)
|
||||
.addView(window.google.picker.ViewId.FOLDERS)
|
||||
.setOAuthToken(this.accessToken)
|
||||
.enableFeature(window.google.picker.Feature.MULTISELECT_ENABLED)
|
||||
.setTitle("Select files from Google Drive")
|
||||
.setCallback(data => this.pickerCallback(data, onFileSelected))
|
||||
.build();
|
||||
|
||||
picker.setVisible(true);
|
||||
|
||||
// Apply z-index fix
|
||||
setTimeout(() => {
|
||||
const pickerElements = document.querySelectorAll(
|
||||
".picker-dialog, .goog-modalpopup"
|
||||
);
|
||||
pickerElements.forEach(el => {
|
||||
(el as HTMLElement).style.zIndex = "10000";
|
||||
});
|
||||
const bgElements = document.querySelectorAll(
|
||||
".picker-dialog-bg, .goog-modalpopup-bg"
|
||||
);
|
||||
bgElements.forEach(el => {
|
||||
(el as HTMLElement).style.zIndex = "9999";
|
||||
});
|
||||
}, 100);
|
||||
} catch (error) {
|
||||
console.error("Error creating picker:", error);
|
||||
this.onPickerStateChange?.(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async pickerCallback(
|
||||
data: GooglePickerData,
|
||||
onFileSelected: (files: CloudFile[]) => void
|
||||
): Promise<void> {
|
||||
if (data.action === window.google.picker.Action.PICKED) {
|
||||
const files: CloudFile[] = data.docs.map((doc: GooglePickerDocument) => ({
|
||||
id: doc[window.google.picker.Document.ID],
|
||||
name: doc[window.google.picker.Document.NAME],
|
||||
mimeType: doc[window.google.picker.Document.MIME_TYPE],
|
||||
webViewLink: doc[window.google.picker.Document.URL],
|
||||
iconLink: doc[window.google.picker.Document.ICON_URL],
|
||||
size: doc["sizeBytes"] ? parseInt(doc["sizeBytes"]) : undefined,
|
||||
modifiedTime: doc["lastEditedUtc"],
|
||||
isFolder:
|
||||
doc[window.google.picker.Document.MIME_TYPE] ===
|
||||
"application/vnd.google-apps.folder",
|
||||
}));
|
||||
|
||||
// Enrich with additional file data if needed
|
||||
if (files.some(f => !f.size && !f.isFolder)) {
|
||||
try {
|
||||
const enrichedFiles = await Promise.all(
|
||||
files.map(async file => {
|
||||
if (!file.size && !file.isFolder) {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://www.googleapis.com/drive/v3/files/${file.id}?fields=size,modifiedTime`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.accessToken}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
if (response.ok) {
|
||||
const fileDetails = await response.json();
|
||||
return {
|
||||
...file,
|
||||
size: fileDetails.size
|
||||
? parseInt(fileDetails.size)
|
||||
: undefined,
|
||||
modifiedTime:
|
||||
fileDetails.modifiedTime || file.modifiedTime,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Failed to fetch file details:", error);
|
||||
}
|
||||
}
|
||||
return file;
|
||||
})
|
||||
);
|
||||
onFileSelected(enrichedFiles);
|
||||
} catch (error) {
|
||||
console.warn("Failed to enrich file data:", error);
|
||||
onFileSelected(files);
|
||||
}
|
||||
} else {
|
||||
onFileSelected(files);
|
||||
}
|
||||
}
|
||||
|
||||
this.onPickerStateChange?.(false);
|
||||
}
|
||||
}
|
||||
|
||||
export class OneDriveHandler {
|
||||
private accessToken: string;
|
||||
private clientId: string;
|
||||
private provider: CloudProvider;
|
||||
private baseUrl?: string;
|
||||
|
||||
constructor(
|
||||
accessToken: string,
|
||||
clientId: string,
|
||||
provider: CloudProvider = "onedrive",
|
||||
baseUrl?: string
|
||||
) {
|
||||
this.accessToken = accessToken;
|
||||
this.clientId = clientId;
|
||||
this.provider = provider;
|
||||
this.baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
async loadPickerApi(): Promise<boolean> {
|
||||
return new Promise(resolve => {
|
||||
const script = document.createElement("script");
|
||||
script.src = "https://js.live.net/v7.2/OneDrive.js";
|
||||
script.onload = () => resolve(true);
|
||||
script.onerror = () => resolve(false);
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
}
|
||||
|
||||
openPicker(onFileSelected: (files: CloudFile[]) => void): void {
|
||||
if (!window.OneDrive) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.OneDrive.open({
|
||||
clientId: this.clientId,
|
||||
action: "query",
|
||||
multiSelect: true,
|
||||
advanced: {
|
||||
endpointHint: "api.onedrive.com",
|
||||
accessToken: this.accessToken,
|
||||
},
|
||||
success: (response: any) => {
|
||||
const newFiles: CloudFile[] =
|
||||
response.value?.map((item: any, index: number) => ({
|
||||
id: item.id,
|
||||
name:
|
||||
item.name ||
|
||||
`${this.getProviderName()} File ${index + 1} (${item.id.slice(
|
||||
-8
|
||||
)})`,
|
||||
mimeType: item.file?.mimeType || "application/octet-stream",
|
||||
webUrl: item.webUrl || "",
|
||||
downloadUrl: item["@microsoft.graph.downloadUrl"] || "",
|
||||
size: item.size,
|
||||
modifiedTime: item.lastModifiedDateTime,
|
||||
isFolder: !!item.folder,
|
||||
})) || [];
|
||||
|
||||
onFileSelected(newFiles);
|
||||
},
|
||||
cancel: () => {
|
||||
console.log("Picker cancelled");
|
||||
},
|
||||
error: (error: any) => {
|
||||
console.error("Picker error:", error);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private getProviderName(): string {
|
||||
return this.provider === "sharepoint" ? "SharePoint" : "OneDrive";
|
||||
}
|
||||
}
|
||||
|
||||
export const createProviderHandler = (
|
||||
provider: CloudProvider,
|
||||
accessToken: string,
|
||||
onPickerStateChange?: (isOpen: boolean) => void,
|
||||
clientId?: string,
|
||||
baseUrl?: string
|
||||
) => {
|
||||
switch (provider) {
|
||||
case "google_drive":
|
||||
return new GoogleDriveHandler(accessToken, onPickerStateChange);
|
||||
case "onedrive":
|
||||
case "sharepoint":
|
||||
if (!clientId) {
|
||||
throw new Error("Client ID required for OneDrive/SharePoint");
|
||||
}
|
||||
return new OneDriveHandler(accessToken, clientId, provider, baseUrl);
|
||||
default:
|
||||
throw new Error(`Unsupported provider: ${provider}`);
|
||||
}
|
||||
};
|
||||
96
frontend/src/components/cloud-picker/types.ts
Normal file
96
frontend/src/components/cloud-picker/types.ts
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
export interface CloudFile {
|
||||
id: string;
|
||||
name: string;
|
||||
mimeType: string;
|
||||
webViewLink?: string;
|
||||
iconLink?: string;
|
||||
size?: number;
|
||||
modifiedTime?: string;
|
||||
isFolder?: boolean;
|
||||
webUrl?: string;
|
||||
downloadUrl?: string;
|
||||
}
|
||||
|
||||
export type CloudProvider = "google_drive" | "onedrive" | "sharepoint";
|
||||
|
||||
export interface UnifiedCloudPickerProps {
|
||||
provider: CloudProvider;
|
||||
onFileSelected: (files: CloudFile[]) => void;
|
||||
selectedFiles?: CloudFile[];
|
||||
isAuthenticated: boolean;
|
||||
accessToken?: string;
|
||||
onPickerStateChange?: (isOpen: boolean) => void;
|
||||
// OneDrive/SharePoint specific props
|
||||
clientId?: string;
|
||||
baseUrl?: string;
|
||||
}
|
||||
|
||||
export interface GoogleAPI {
|
||||
load: (
|
||||
api: string,
|
||||
options: { callback: () => void; onerror?: () => void }
|
||||
) => void;
|
||||
}
|
||||
|
||||
export interface GooglePickerData {
|
||||
action: string;
|
||||
docs: GooglePickerDocument[];
|
||||
}
|
||||
|
||||
export interface GooglePickerDocument {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
gapi: GoogleAPI;
|
||||
google: {
|
||||
picker: {
|
||||
api: {
|
||||
load: (callback: () => void) => void;
|
||||
};
|
||||
PickerBuilder: new () => GooglePickerBuilder;
|
||||
ViewId: {
|
||||
DOCS: string;
|
||||
FOLDERS: string;
|
||||
DOCS_IMAGES_AND_VIDEOS: string;
|
||||
DOCUMENTS: string;
|
||||
PRESENTATIONS: string;
|
||||
SPREADSHEETS: string;
|
||||
};
|
||||
Feature: {
|
||||
MULTISELECT_ENABLED: string;
|
||||
NAV_HIDDEN: string;
|
||||
SIMPLE_UPLOAD_ENABLED: string;
|
||||
};
|
||||
Action: {
|
||||
PICKED: string;
|
||||
CANCEL: string;
|
||||
};
|
||||
Document: {
|
||||
ID: string;
|
||||
NAME: string;
|
||||
MIME_TYPE: string;
|
||||
URL: string;
|
||||
ICON_URL: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
OneDrive?: any;
|
||||
}
|
||||
}
|
||||
|
||||
export interface GooglePickerBuilder {
|
||||
addView: (view: string) => GooglePickerBuilder;
|
||||
setOAuthToken: (token: string) => GooglePickerBuilder;
|
||||
setCallback: (
|
||||
callback: (data: GooglePickerData) => void
|
||||
) => GooglePickerBuilder;
|
||||
enableFeature: (feature: string) => GooglePickerBuilder;
|
||||
setTitle: (title: string) => GooglePickerBuilder;
|
||||
build: () => GooglePicker;
|
||||
}
|
||||
|
||||
export interface GooglePicker {
|
||||
setVisible: (visible: boolean) => void;
|
||||
}
|
||||
173
frontend/src/components/cloud-picker/unified-cloud-picker.tsx
Normal file
173
frontend/src/components/cloud-picker/unified-cloud-picker.tsx
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { UnifiedCloudPickerProps, CloudFile } from "./types";
|
||||
import { PickerHeader } from "./picker-header";
|
||||
import { FileList } from "./file-list";
|
||||
import { IngestSettings } from "./ingest-settings";
|
||||
import { createProviderHandler } from "./provider-handlers";
|
||||
|
||||
export const UnifiedCloudPicker = ({
|
||||
provider,
|
||||
onFileSelected,
|
||||
selectedFiles = [],
|
||||
isAuthenticated,
|
||||
accessToken,
|
||||
onPickerStateChange,
|
||||
clientId,
|
||||
baseUrl,
|
||||
}: UnifiedCloudPickerProps) => {
|
||||
const [isPickerLoaded, setIsPickerLoaded] = useState(false);
|
||||
const [isPickerOpen, setIsPickerOpen] = useState(false);
|
||||
const [isIngestSettingsOpen, setIsIngestSettingsOpen] = useState(false);
|
||||
const [isLoadingBaseUrl, setIsLoadingBaseUrl] = useState(false);
|
||||
const [autoBaseUrl, setAutoBaseUrl] = useState<string | undefined>(undefined);
|
||||
|
||||
const effectiveBaseUrl = baseUrl || autoBaseUrl;
|
||||
|
||||
// Auto-detect base URL for OneDrive personal accounts
|
||||
useEffect(() => {
|
||||
if (
|
||||
(provider === "onedrive" || provider === "sharepoint") &&
|
||||
!baseUrl &&
|
||||
accessToken &&
|
||||
!autoBaseUrl
|
||||
) {
|
||||
const getBaseUrl = async () => {
|
||||
setIsLoadingBaseUrl(true);
|
||||
try {
|
||||
setAutoBaseUrl("https://onedrive.live.com/picker");
|
||||
} catch (error) {
|
||||
console.error("Auto-detect baseUrl failed:", error);
|
||||
} finally {
|
||||
setIsLoadingBaseUrl(false);
|
||||
}
|
||||
};
|
||||
|
||||
getBaseUrl();
|
||||
}
|
||||
}, [accessToken, baseUrl, autoBaseUrl, provider]);
|
||||
|
||||
// Load picker API
|
||||
useEffect(() => {
|
||||
if (!accessToken || !isAuthenticated) return;
|
||||
|
||||
const loadApi = async () => {
|
||||
try {
|
||||
const handler = createProviderHandler(
|
||||
provider,
|
||||
accessToken,
|
||||
onPickerStateChange,
|
||||
clientId,
|
||||
effectiveBaseUrl
|
||||
);
|
||||
const loaded = await handler.loadPickerApi();
|
||||
setIsPickerLoaded(loaded);
|
||||
} catch (error) {
|
||||
console.error("Failed to create provider handler:", error);
|
||||
setIsPickerLoaded(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadApi();
|
||||
}, [
|
||||
accessToken,
|
||||
isAuthenticated,
|
||||
provider,
|
||||
clientId,
|
||||
effectiveBaseUrl,
|
||||
onPickerStateChange,
|
||||
]);
|
||||
|
||||
const handleAddFiles = () => {
|
||||
if (!isPickerLoaded || !accessToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((provider === "onedrive" || provider === "sharepoint") && !clientId) {
|
||||
console.error("Client ID required for OneDrive/SharePoint");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsPickerOpen(true);
|
||||
onPickerStateChange?.(true);
|
||||
|
||||
const handler = createProviderHandler(
|
||||
provider,
|
||||
accessToken,
|
||||
isOpen => {
|
||||
setIsPickerOpen(isOpen);
|
||||
onPickerStateChange?.(isOpen);
|
||||
},
|
||||
clientId,
|
||||
effectiveBaseUrl
|
||||
);
|
||||
|
||||
handler.openPicker((files: CloudFile[]) => {
|
||||
// Merge new files with existing ones, avoiding duplicates
|
||||
const existingIds = new Set(selectedFiles.map(f => f.id));
|
||||
const newFiles = files.filter(f => !existingIds.has(f.id));
|
||||
onFileSelected([...selectedFiles, ...newFiles]);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error opening picker:", error);
|
||||
setIsPickerOpen(false);
|
||||
onPickerStateChange?.(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveFile = (fileId: string) => {
|
||||
const updatedFiles = selectedFiles.filter(file => file.id !== fileId);
|
||||
onFileSelected(updatedFiles);
|
||||
};
|
||||
|
||||
const handleClearAll = () => {
|
||||
onFileSelected([]);
|
||||
};
|
||||
|
||||
if (isLoadingBaseUrl) {
|
||||
return (
|
||||
<div className="text-sm text-muted-foreground p-4 bg-muted/20 rounded-md">
|
||||
Loading...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
(provider === "onedrive" || provider === "sharepoint") &&
|
||||
!clientId &&
|
||||
isAuthenticated
|
||||
) {
|
||||
return (
|
||||
<div className="text-sm text-muted-foreground p-4 bg-muted/20 rounded-md">
|
||||
Configuration required: Client ID missing for{" "}
|
||||
{provider === "sharepoint" ? "SharePoint" : "OneDrive"}.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<PickerHeader
|
||||
provider={provider}
|
||||
onAddFiles={handleAddFiles}
|
||||
isPickerLoaded={isPickerLoaded}
|
||||
isPickerOpen={isPickerOpen}
|
||||
accessToken={accessToken}
|
||||
isAuthenticated={isAuthenticated}
|
||||
/>
|
||||
|
||||
<FileList
|
||||
files={selectedFiles}
|
||||
onClearAll={handleClearAll}
|
||||
onRemoveFile={handleRemoveFile}
|
||||
/>
|
||||
|
||||
<IngestSettings
|
||||
isOpen={isIngestSettingsOpen}
|
||||
onOpenChange={setIsIngestSettingsOpen}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,341 +0,0 @@
|
|||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { FileText, Folder, Plus, Trash2 } from "lucide-react"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
|
||||
interface GoogleDrivePickerProps {
|
||||
onFileSelected: (files: GoogleDriveFile[]) => void
|
||||
selectedFiles?: GoogleDriveFile[]
|
||||
isAuthenticated: boolean
|
||||
accessToken?: string
|
||||
onPickerStateChange?: (isOpen: boolean) => void
|
||||
}
|
||||
|
||||
interface GoogleDriveFile {
|
||||
id: string
|
||||
name: string
|
||||
mimeType: string
|
||||
webViewLink?: string
|
||||
iconLink?: string
|
||||
size?: number
|
||||
modifiedTime?: string
|
||||
isFolder?: boolean
|
||||
}
|
||||
|
||||
interface GoogleAPI {
|
||||
load: (api: string, options: { callback: () => void; onerror?: () => void }) => void
|
||||
}
|
||||
|
||||
interface GooglePickerData {
|
||||
action: string
|
||||
docs: GooglePickerDocument[]
|
||||
}
|
||||
|
||||
interface GooglePickerDocument {
|
||||
[key: string]: string
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
gapi: GoogleAPI
|
||||
google: {
|
||||
picker: {
|
||||
api: {
|
||||
load: (callback: () => void) => void
|
||||
}
|
||||
PickerBuilder: new () => GooglePickerBuilder
|
||||
ViewId: {
|
||||
DOCS: string
|
||||
FOLDERS: string
|
||||
DOCS_IMAGES_AND_VIDEOS: string
|
||||
DOCUMENTS: string
|
||||
PRESENTATIONS: string
|
||||
SPREADSHEETS: string
|
||||
}
|
||||
Feature: {
|
||||
MULTISELECT_ENABLED: string
|
||||
NAV_HIDDEN: string
|
||||
SIMPLE_UPLOAD_ENABLED: string
|
||||
}
|
||||
Action: {
|
||||
PICKED: string
|
||||
CANCEL: string
|
||||
}
|
||||
Document: {
|
||||
ID: string
|
||||
NAME: string
|
||||
MIME_TYPE: string
|
||||
URL: string
|
||||
ICON_URL: string
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface GooglePickerBuilder {
|
||||
addView: (view: string) => GooglePickerBuilder
|
||||
setOAuthToken: (token: string) => GooglePickerBuilder
|
||||
setCallback: (callback: (data: GooglePickerData) => void) => GooglePickerBuilder
|
||||
enableFeature: (feature: string) => GooglePickerBuilder
|
||||
setTitle: (title: string) => GooglePickerBuilder
|
||||
build: () => GooglePicker
|
||||
}
|
||||
|
||||
interface GooglePicker {
|
||||
setVisible: (visible: boolean) => void
|
||||
}
|
||||
|
||||
export function GoogleDrivePicker({
|
||||
onFileSelected,
|
||||
selectedFiles = [],
|
||||
isAuthenticated,
|
||||
accessToken,
|
||||
onPickerStateChange
|
||||
}: GoogleDrivePickerProps) {
|
||||
const [isPickerLoaded, setIsPickerLoaded] = useState(false)
|
||||
const [isPickerOpen, setIsPickerOpen] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const loadPickerApi = () => {
|
||||
if (typeof window !== 'undefined' && window.gapi) {
|
||||
window.gapi.load('picker', {
|
||||
callback: () => {
|
||||
setIsPickerLoaded(true)
|
||||
},
|
||||
onerror: () => {
|
||||
console.error('Failed to load Google Picker API')
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Load Google API script if not already loaded
|
||||
if (typeof window !== 'undefined') {
|
||||
if (!window.gapi) {
|
||||
const script = document.createElement('script')
|
||||
script.src = 'https://apis.google.com/js/api.js'
|
||||
script.async = true
|
||||
script.defer = true
|
||||
script.onload = loadPickerApi
|
||||
script.onerror = () => {
|
||||
console.error('Failed to load Google API script')
|
||||
}
|
||||
document.head.appendChild(script)
|
||||
|
||||
return () => {
|
||||
if (document.head.contains(script)) {
|
||||
document.head.removeChild(script)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
loadPickerApi()
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
|
||||
const openPicker = () => {
|
||||
if (!isPickerLoaded || !accessToken || !window.google?.picker) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setIsPickerOpen(true)
|
||||
onPickerStateChange?.(true)
|
||||
|
||||
// Create picker with higher z-index and focus handling
|
||||
const picker = new window.google.picker.PickerBuilder()
|
||||
.addView(window.google.picker.ViewId.DOCS)
|
||||
.addView(window.google.picker.ViewId.FOLDERS)
|
||||
.setOAuthToken(accessToken)
|
||||
.enableFeature(window.google.picker.Feature.MULTISELECT_ENABLED)
|
||||
.setTitle('Select files from Google Drive')
|
||||
.setCallback(pickerCallback)
|
||||
.build()
|
||||
|
||||
picker.setVisible(true)
|
||||
|
||||
// Apply z-index fix after a short delay to ensure picker is rendered
|
||||
setTimeout(() => {
|
||||
const pickerElements = document.querySelectorAll('.picker-dialog, .goog-modalpopup')
|
||||
pickerElements.forEach(el => {
|
||||
(el as HTMLElement).style.zIndex = '10000'
|
||||
})
|
||||
const bgElements = document.querySelectorAll('.picker-dialog-bg, .goog-modalpopup-bg')
|
||||
bgElements.forEach(el => {
|
||||
(el as HTMLElement).style.zIndex = '9999'
|
||||
})
|
||||
}, 100)
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error creating picker:', error)
|
||||
setIsPickerOpen(false)
|
||||
onPickerStateChange?.(false)
|
||||
}
|
||||
}
|
||||
|
||||
const pickerCallback = async (data: GooglePickerData) => {
|
||||
if (data.action === window.google.picker.Action.PICKED) {
|
||||
const files: GoogleDriveFile[] = data.docs.map((doc: GooglePickerDocument) => ({
|
||||
id: doc[window.google.picker.Document.ID],
|
||||
name: doc[window.google.picker.Document.NAME],
|
||||
mimeType: doc[window.google.picker.Document.MIME_TYPE],
|
||||
webViewLink: doc[window.google.picker.Document.URL],
|
||||
iconLink: doc[window.google.picker.Document.ICON_URL],
|
||||
size: doc['sizeBytes'] ? parseInt(doc['sizeBytes']) : undefined,
|
||||
modifiedTime: doc['lastEditedUtc'],
|
||||
isFolder: doc[window.google.picker.Document.MIME_TYPE] === 'application/vnd.google-apps.folder'
|
||||
}))
|
||||
|
||||
// If size is still missing, try to fetch it via Google Drive API
|
||||
if (accessToken && files.some(f => !f.size && !f.isFolder)) {
|
||||
try {
|
||||
const enrichedFiles = await Promise.all(files.map(async (file) => {
|
||||
if (!file.size && !file.isFolder) {
|
||||
try {
|
||||
const response = await fetch(`https://www.googleapis.com/drive/v3/files/${file.id}?fields=size,modifiedTime`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`
|
||||
}
|
||||
})
|
||||
if (response.ok) {
|
||||
const fileDetails = await response.json()
|
||||
return {
|
||||
...file,
|
||||
size: fileDetails.size ? parseInt(fileDetails.size) : undefined,
|
||||
modifiedTime: fileDetails.modifiedTime || file.modifiedTime
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to fetch file details:', error)
|
||||
}
|
||||
}
|
||||
return file
|
||||
}))
|
||||
onFileSelected(enrichedFiles)
|
||||
} catch (error) {
|
||||
console.warn('Failed to enrich file data:', error)
|
||||
onFileSelected(files)
|
||||
}
|
||||
} else {
|
||||
onFileSelected(files)
|
||||
}
|
||||
}
|
||||
|
||||
setIsPickerOpen(false)
|
||||
onPickerStateChange?.(false)
|
||||
}
|
||||
|
||||
const removeFile = (fileId: string) => {
|
||||
const updatedFiles = selectedFiles.filter(file => file.id !== fileId)
|
||||
onFileSelected(updatedFiles)
|
||||
}
|
||||
|
||||
const getFileIcon = (mimeType: string) => {
|
||||
if (mimeType.includes('folder')) {
|
||||
return <Folder className="h-4 w-4" />
|
||||
}
|
||||
return <FileText className="h-4 w-4" />
|
||||
}
|
||||
|
||||
const getMimeTypeLabel = (mimeType: string) => {
|
||||
const typeMap: { [key: string]: string } = {
|
||||
'application/vnd.google-apps.document': 'Google Doc',
|
||||
'application/vnd.google-apps.spreadsheet': 'Google Sheet',
|
||||
'application/vnd.google-apps.presentation': 'Google Slides',
|
||||
'application/vnd.google-apps.folder': 'Folder',
|
||||
'application/pdf': 'PDF',
|
||||
'text/plain': 'Text',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'Word Doc',
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'PowerPoint'
|
||||
}
|
||||
|
||||
return typeMap[mimeType] || 'Document'
|
||||
}
|
||||
|
||||
const formatFileSize = (bytes?: number) => {
|
||||
if (!bytes) return ''
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
if (bytes === 0) return '0 B'
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(1024))
|
||||
return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${sizes[i]}`
|
||||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return (
|
||||
<div className="text-sm text-muted-foreground p-4 bg-muted/20 rounded-md">
|
||||
Please connect to Google Drive first to select specific files.
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Card>
|
||||
<CardContent className="flex flex-col items-center text-center p-6">
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
Select files from Google Drive to ingest.
|
||||
</p>
|
||||
<Button
|
||||
onClick={openPicker}
|
||||
disabled={!isPickerLoaded || isPickerOpen || !accessToken}
|
||||
className="bg-foreground text-background hover:bg-foreground/90"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
{isPickerOpen ? 'Opening Picker...' : 'Add Files'}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{selectedFiles.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Added files
|
||||
</p>
|
||||
<Button
|
||||
onClick={() => onFileSelected([])}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="text-xs h-6"
|
||||
>
|
||||
Clear all
|
||||
</Button>
|
||||
</div>
|
||||
<div className="max-h-64 overflow-y-auto space-y-1">
|
||||
{selectedFiles.map((file) => (
|
||||
<div
|
||||
key={file.id}
|
||||
className="flex items-center justify-between p-2 bg-muted/30 rounded-md text-xs"
|
||||
>
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
{getFileIcon(file.mimeType)}
|
||||
<span className="truncate font-medium">{file.name}</span>
|
||||
<Badge variant="secondary" className="text-xs px-1 py-0.5 h-auto">
|
||||
{getMimeTypeLabel(file.mimeType)}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-muted-foreground">{formatFileSize(file.size)}</span>
|
||||
<Button
|
||||
onClick={() => removeFile(file.id)}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-6 w-6 p-0"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,256 +0,0 @@
|
|||
"use client"
|
||||
|
||||
import { useState, useEffect, useRef } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Plus, Trash2, FileText } from "lucide-react"
|
||||
|
||||
interface OneDrivePickerProps {
|
||||
onFileSelected: (files: SelectedFile[]) => void
|
||||
selectedFiles?: SelectedFile[]
|
||||
isAuthenticated: boolean
|
||||
accessToken?: string
|
||||
connectorType?: "onedrive" | "sharepoint"
|
||||
baseUrl?: string // e.g., "https://tenant.sharepoint.com/sites/sitename" or "https://tenant-my.sharepoint.com"
|
||||
clientId: string
|
||||
}
|
||||
|
||||
interface SelectedFile {
|
||||
id: string
|
||||
name: string
|
||||
mimeType?: string
|
||||
webUrl?: string
|
||||
downloadUrl?: string
|
||||
}
|
||||
|
||||
export function OneDrivePicker({
|
||||
onFileSelected,
|
||||
selectedFiles = [],
|
||||
isAuthenticated,
|
||||
accessToken,
|
||||
connectorType = "onedrive",
|
||||
baseUrl: providedBaseUrl,
|
||||
clientId
|
||||
}: OneDrivePickerProps) {
|
||||
// Debug all props
|
||||
console.log('All OneDrivePicker props:', {
|
||||
onFileSelected: !!onFileSelected,
|
||||
selectedFiles: selectedFiles?.length,
|
||||
isAuthenticated,
|
||||
accessToken: !!accessToken,
|
||||
connectorType,
|
||||
providedBaseUrl,
|
||||
clientId
|
||||
})
|
||||
const [isPickerOpen, setIsPickerOpen] = useState(false)
|
||||
const [autoBaseUrl, setAutoBaseUrl] = useState<string | null>(null)
|
||||
const [isLoadingBaseUrl, setIsLoadingBaseUrl] = useState(false)
|
||||
const baseUrl = providedBaseUrl || autoBaseUrl
|
||||
|
||||
useEffect(() => {
|
||||
if (providedBaseUrl || !accessToken || autoBaseUrl) return
|
||||
|
||||
const getBaseUrl = async () => {
|
||||
setIsLoadingBaseUrl(true)
|
||||
try {
|
||||
// For personal accounts, use the picker URL directly
|
||||
setAutoBaseUrl("https://onedrive.live.com/picker")
|
||||
} catch (error) {
|
||||
console.error('Auto-detect baseUrl failed:', error)
|
||||
} finally {
|
||||
setIsLoadingBaseUrl(false)
|
||||
}
|
||||
}
|
||||
|
||||
getBaseUrl()
|
||||
}, [accessToken, providedBaseUrl, autoBaseUrl])
|
||||
|
||||
// Add this loading check before your existing checks:
|
||||
if (isLoadingBaseUrl) {
|
||||
return (
|
||||
<div className="border rounded-lg shadow-sm bg-white">
|
||||
<div className="flex flex-col items-center text-center p-6">
|
||||
<p className="text-sm text-gray-600">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const openPicker = () => {
|
||||
if (!accessToken || !clientId) {
|
||||
console.error('Access token and client ID required')
|
||||
return
|
||||
}
|
||||
|
||||
setIsPickerOpen(true)
|
||||
|
||||
const script = document.createElement('script')
|
||||
script.src = 'https://js.live.net/v7.2/OneDrive.js'
|
||||
script.onload = () => {
|
||||
// @ts-ignore
|
||||
const OneDrive = window.OneDrive
|
||||
|
||||
if (OneDrive) {
|
||||
OneDrive.open({
|
||||
clientId: clientId,
|
||||
action: 'query',
|
||||
multiSelect: true,
|
||||
advanced: {
|
||||
endpointHint: 'api.onedrive.com',
|
||||
accessToken: accessToken,
|
||||
},
|
||||
success: (response: any) => {
|
||||
console.log('Raw OneDrive response:', response)
|
||||
|
||||
const newFiles: SelectedFile[] = response.value?.map((item: any, index: number) => ({
|
||||
id: item.id,
|
||||
name: `OneDrive File ${index + 1} (${item.id.slice(-8)})`,
|
||||
mimeType: 'application/pdf',
|
||||
webUrl: item.webUrl || '',
|
||||
downloadUrl: ''
|
||||
})) || []
|
||||
|
||||
console.log('Mapped files:', newFiles)
|
||||
onFileSelected([...selectedFiles, ...newFiles])
|
||||
setIsPickerOpen(false)
|
||||
},
|
||||
cancel: () => {
|
||||
console.log('Picker cancelled')
|
||||
setIsPickerOpen(false)
|
||||
},
|
||||
error: (error: any) => {
|
||||
console.error('Picker error:', error)
|
||||
setIsPickerOpen(false)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
document.head.appendChild(script)
|
||||
}
|
||||
|
||||
const closePicker = () => {
|
||||
setIsPickerOpen(false)
|
||||
}
|
||||
|
||||
const removeFile = (fileId: string) => {
|
||||
const updatedFiles = selectedFiles.filter(file => file.id !== fileId)
|
||||
onFileSelected(updatedFiles)
|
||||
}
|
||||
|
||||
const serviceName = connectorType === "sharepoint" ? "SharePoint" : "OneDrive"
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return (
|
||||
<div className="border rounded-lg shadow-sm bg-white">
|
||||
<div className="flex flex-col items-center text-center p-6">
|
||||
<p className="text-sm text-gray-600">
|
||||
Please connect to {serviceName} first to select files.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!accessToken || !baseUrl) {
|
||||
return (
|
||||
<div className="border rounded-lg shadow-sm bg-white">
|
||||
<div className="flex flex-col items-center text-center p-6">
|
||||
<p className="text-sm text-gray-600 mb-2">
|
||||
Configuration required
|
||||
</p>
|
||||
<p className="text-xs text-amber-600">
|
||||
{!accessToken && "Access token required. "}
|
||||
{!baseUrl && "Base URL required."}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{isPickerOpen ? (
|
||||
<div className="border rounded-lg shadow-sm bg-white">
|
||||
<div className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold">OneDrive Picker is open in popup</h3>
|
||||
<Button
|
||||
onClick={closePicker}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="text-black"
|
||||
style={{ color: '#000000' }}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600">
|
||||
Please select your files in the popup window. This window will update when you're done.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="border rounded-lg shadow-sm bg-white">
|
||||
<div className="flex flex-col items-center text-center p-6">
|
||||
<p className="text-sm text-gray-600 mb-4">
|
||||
Select files from {serviceName} to ingest into OpenRAG.
|
||||
</p>
|
||||
<Button
|
||||
onClick={openPicker}
|
||||
className="bg-blue-600 text-white hover:bg-blue-700 border-0"
|
||||
style={{ backgroundColor: '#2563eb', color: '#ffffff' }}
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Add Files
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedFiles.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-xs text-gray-600">
|
||||
Selected files ({selectedFiles.length})
|
||||
</p>
|
||||
<Button
|
||||
onClick={() => onFileSelected([])}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="text-xs h-6"
|
||||
>
|
||||
Clear all
|
||||
</Button>
|
||||
</div>
|
||||
<div className="max-h-64 overflow-y-auto space-y-1">
|
||||
{selectedFiles.map((file) => (
|
||||
<div
|
||||
key={file.id}
|
||||
className="flex items-center justify-between p-2 bg-gray-100 rounded-md text-xs"
|
||||
>
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
<FileText className="h-4 w-4 text-gray-600" />
|
||||
<span className="truncate font-medium text-black" style={{ color: '#000000' }}>{file.name}</span>
|
||||
{file.mimeType && (
|
||||
<Badge variant="secondary" className="text-xs px-1 py-0.5 h-auto">
|
||||
{file.mimeType.split('/').pop() || 'File'}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => removeFile(file.id)}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-6 w-6 p-0"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue