move to stand alone page

This commit is contained in:
Mike Fortman 2025-09-04 21:37:30 -05:00
parent 4573b056f6
commit 0ed98cb6e1
9 changed files with 1201 additions and 100 deletions

View file

@ -1,7 +1,6 @@
"use client"
import { useState, useEffect, useRef } from "react"
import { useRouter } from "next/navigation"
import { ChevronDown, Upload, FolderOpen, Cloud, PlugZap, Plus } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
@ -9,6 +8,7 @@ import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { cn } from "@/lib/utils"
import { useTask } from "@/contexts/task-context"
import { useRouter } from "next/navigation"
interface KnowledgeDropdownProps {
active?: boolean
@ -16,8 +16,8 @@ interface KnowledgeDropdownProps {
}
export function KnowledgeDropdown({ active, variant = 'navigation' }: KnowledgeDropdownProps) {
const router = useRouter()
const { addTask } = useTask()
const router = useRouter()
const [isOpen, setIsOpen] = useState(false)
const [showFolderDialog, setShowFolderDialog] = useState(false)
const [showS3Dialog, setShowS3Dialog] = useState(false)
@ -27,23 +27,76 @@ export function KnowledgeDropdown({ active, variant = 'navigation' }: KnowledgeD
const [folderLoading, setFolderLoading] = useState(false)
const [s3Loading, setS3Loading] = useState(false)
const [fileUploading, setFileUploading] = useState(false)
const [cloudConnectors, setCloudConnectors] = useState<{[key: string]: {name: string, available: boolean, connected: boolean, hasToken: boolean}}>({})
const fileInputRef = useRef<HTMLInputElement>(null)
const dropdownRef = useRef<HTMLDivElement>(null)
// Check AWS availability on mount
// Check AWS availability and cloud connectors on mount
useEffect(() => {
const checkAws = async () => {
const checkAvailability = async () => {
try {
const res = await fetch("/api/upload_options")
if (res.ok) {
const data = await res.json()
setAwsEnabled(Boolean(data.aws))
// Check AWS
const awsRes = await fetch("/api/upload_options")
if (awsRes.ok) {
const awsData = await awsRes.json()
setAwsEnabled(Boolean(awsData.aws))
}
// Check cloud connectors
const connectorsRes = await fetch('/api/connectors')
if (connectorsRes.ok) {
const connectorsResult = await connectorsRes.json()
const cloudConnectorTypes = ['google_drive', 'onedrive', 'sharepoint']
const connectorInfo: {[key: string]: {name: string, available: boolean, connected: boolean, hasToken: boolean}} = {}
for (const type of cloudConnectorTypes) {
if (connectorsResult.connectors[type]) {
connectorInfo[type] = {
name: connectorsResult.connectors[type].name,
available: connectorsResult.connectors[type].available,
connected: false,
hasToken: false
}
// Check connection status
try {
const statusRes = await fetch(`/api/connectors/${type}/status`)
if (statusRes.ok) {
const statusData = await statusRes.json()
const connections = statusData.connections || []
const activeConnection = connections.find((conn: {is_active: boolean, connection_id: string}) => conn.is_active)
const isConnected = activeConnection !== undefined
if (isConnected && activeConnection) {
connectorInfo[type].connected = true
// Check token availability
try {
const tokenRes = await fetch(`/api/connectors/${type}/token?connection_id=${activeConnection.connection_id}`)
if (tokenRes.ok) {
const tokenData = await tokenRes.json()
if (tokenData.access_token) {
connectorInfo[type].hasToken = true
}
}
} catch {
// Token check failed
}
}
}
} catch {
// Status check failed
}
}
}
setCloudConnectors(connectorInfo)
}
} catch (err) {
console.error("Failed to check AWS availability", err)
console.error("Failed to check availability", err)
}
}
checkAws()
checkAvailability()
}, [])
// Handle click outside to close dropdown
@ -194,6 +247,25 @@ export function KnowledgeDropdown({ active, variant = 'navigation' }: KnowledgeD
}
}
const cloudConnectorItems = Object.entries(cloudConnectors)
.filter(([, info]) => info.available)
.map(([type, info]) => ({
label: info.name,
icon: PlugZap,
onClick: () => {
setIsOpen(false)
if (info.connected && info.hasToken) {
router.push(`/upload/${type}`)
} else {
router.push('/settings')
}
},
disabled: !info.connected || !info.hasToken,
tooltip: !info.connected ? `Connect ${info.name} in Settings first` :
!info.hasToken ? `Reconnect ${info.name} - access token required` :
undefined
}))
const menuItems = [
{
label: "Add File",
@ -216,14 +288,7 @@ export function KnowledgeDropdown({ active, variant = 'navigation' }: KnowledgeD
setShowS3Dialog(true)
}
}] : []),
{
label: "Cloud Connectors",
icon: PlugZap,
onClick: () => {
setIsOpen(false)
router.push("/settings")
}
}
...cloudConnectorItems
]
return (
@ -265,7 +330,12 @@ export function KnowledgeDropdown({ active, variant = 'navigation' }: KnowledgeD
<button
key={index}
onClick={item.onClick}
className="w-full px-3 py-2 text-left text-sm hover:bg-accent hover:text-accent-foreground"
disabled={'disabled' in item ? item.disabled : false}
title={'tooltip' in item ? item.tooltip : undefined}
className={cn(
"w-full px-3 py-2 text-left text-sm hover:bg-accent hover:text-accent-foreground",
'disabled' in item && item.disabled && "opacity-50 cursor-not-allowed hover:bg-transparent hover:text-current"
)}
>
{item.label}
</button>
@ -364,6 +434,7 @@ export function KnowledgeDropdown({ active, variant = 'navigation' }: KnowledgeD
</div>
</DialogContent>
</Dialog>
</>
)
}

View file

@ -1,14 +1,60 @@
"use client"
import React, { useState } from "react";
import { GoogleDrivePicker, type DriveSelection } from "./GoogleDrivePicker"
import { GoogleDrivePicker } from "@/components/google-drive-picker"
const [driveSelection, setDriveSelection] = useState<DriveSelection>({ files: [], folders: [] });
interface GoogleDriveFile {
id: string;
name: string;
mimeType: string;
webViewLink?: string;
iconLink?: string;
}
// in JSX
<GoogleDrivePicker value={driveSelection} onChange={setDriveSelection} />
export default function ConnectorsPage() {
const [selectedFiles, setSelectedFiles] = useState<GoogleDriveFile[]>([]);
// when calling sync:
const body: { file_ids: string[]; folder_ids: string[]; recursive: boolean } = {
file_ids: driveSelection.files,
folder_ids: driveSelection.folders,
recursive: true,
};
const handleFileSelection = (files: GoogleDriveFile[]) => {
setSelectedFiles(files);
};
const handleSync = () => {
const fileIds = selectedFiles.map(file => file.id);
const body = {
file_ids: fileIds,
folder_ids: [], // Add folder handling if needed
recursive: true,
};
console.log('Syncing with:', body);
};
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.
</p>
<GoogleDrivePicker
onFileSelected={handleFileSelection}
selectedFiles={selectedFiles}
isAuthenticated={false} // This would come from auth context in real usage
accessToken={undefined} // This would come from connected account
/>
</div>
{selectedFiles.length > 0 && (
<button
onClick={handleSync}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
Sync {selectedFiles.length} Selected Items
</button>
)}
</div>
);
}

View file

@ -12,7 +12,6 @@ import { Loader2, PlugZap, RefreshCw } from "lucide-react"
import { ProtectedRoute } from "@/components/protected-route"
import { useTask } from "@/contexts/task-context"
import { useAuth } from "@/contexts/auth-context"
import { GoogleDrivePicker } from "@/components/google-drive-picker"
interface GoogleDriveFile {
@ -23,6 +22,17 @@ interface GoogleDriveFile {
iconLink?: string
}
interface OneDriveFile {
id: string
name: string
mimeType?: string
webUrl?: string
driveItem?: {
file?: { mimeType: string }
folder?: any
}
}
interface Connector {
id: string
name: string
@ -32,7 +42,7 @@ interface Connector {
type: string
connectionId?: string
access_token?: string
selectedFiles?: GoogleDriveFile[]
selectedFiles?: GoogleDriveFile[] | OneDriveFile[]
}
interface SyncResult {
@ -63,8 +73,6 @@ function KnowledgeSourcesPage() {
const [syncResults, setSyncResults] = useState<{[key: string]: SyncResult | null}>({})
const [maxFiles, setMaxFiles] = useState<number>(10)
const [syncAllFiles, setSyncAllFiles] = useState<boolean>(false)
const [selectedFiles, setSelectedFiles] = useState<{[connectorId: string]: GoogleDriveFile[]}>({})
const [connectorAccessTokens, setConnectorAccessTokens] = useState<{[connectorId: string]: string}>({})
// Settings state
// Note: backend internal Langflow URL is not needed on the frontend
@ -155,23 +163,6 @@ function KnowledgeSourcesPage() {
const activeConnection = connections.find((conn: Connection) => conn.is_active)
const isConnected = activeConnection !== undefined
// For Google Drive, try to get access token for the picker
if (connectorType === 'google_drive' && activeConnection) {
try {
const tokenResponse = await fetch(`/api/connectors/${connectorType}/token?connection_id=${activeConnection.connection_id}`)
if (tokenResponse.ok) {
const tokenData = await tokenResponse.json()
if (tokenData.access_token) {
setConnectorAccessTokens(prev => ({
...prev,
[connectorType]: tokenData.access_token
}))
}
}
} catch (e) {
console.log('Could not fetch access token for Google Drive picker:', e)
}
}
setConnectors(prev => prev.map(c =>
c.type === connectorType
@ -238,19 +229,6 @@ function KnowledgeSourcesPage() {
}
}
const handleFileSelection = (connectorId: string, files: GoogleDriveFile[]) => {
setSelectedFiles(prev => ({
...prev,
[connectorId]: files
}))
// Update the connector with selected files
setConnectors(prev => prev.map(c =>
c.id === connectorId
? { ...c, selectedFiles: files }
: c
))
}
const handleSync = async (connector: Connector) => {
if (!connector.connectionId) return
@ -268,10 +246,7 @@ function KnowledgeSourcesPage() {
max_files: syncAllFiles ? 0 : (maxFiles || undefined)
}
// Add selected files for Google Drive
if (connector.type === "google_drive" && selectedFiles[connector.id]?.length > 0) {
syncBody.selected_files = selectedFiles[connector.id].map(file => file.id)
}
// Note: File selection is now handled via the cloud connectors dialog
const response = await fetch(`/api/connectors/${connector.type}/sync`, {
method: 'POST',
@ -488,16 +463,6 @@ function KnowledgeSourcesPage() {
<CardContent className="flex-1 flex flex-col justify-end space-y-4">
{connector.status === "connected" ? (
<div className="space-y-3">
{/* Google Drive file picker */}
{connector.type === "google_drive" && (
<GoogleDrivePicker
onFileSelected={(files) => handleFileSelection(connector.id, files)}
selectedFiles={selectedFiles[connector.id] || []}
isAuthenticated={connector.status === "connected"}
accessToken={connectorAccessTokens[connector.type]}
/>
)}
<Button
onClick={() => handleSync(connector)}
disabled={isSyncing === connector.id}

View file

@ -0,0 +1,307 @@
"use client"
import { useState, useEffect } from "react"
import { useParams, useRouter } from "next/navigation"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { ArrowLeft, AlertCircle } from "lucide-react"
import { GoogleDrivePicker } from "@/components/google-drive-picker"
import { OneDrivePicker } from "@/components/onedrive-picker"
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?: object
}
}
interface CloudConnector {
id: string
name: string
description: string
status: "not_connected" | "connecting" | "connected" | "error"
type: string
connectionId?: string
hasAccessToken: boolean
accessTokenError?: string
}
export default function UploadProviderPage() {
const params = useParams()
const router = useRouter()
const provider = params.provider as string
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[]>([])
useEffect(() => {
const fetchConnectorInfo = async () => {
setIsLoading(true)
setError(null)
try {
// Fetch available connectors to validate the provider
const connectorsResponse = await fetch('/api/connectors')
if (!connectorsResponse.ok) {
throw new Error('Failed to load connectors')
}
const connectorsResult = await connectorsResponse.json()
const providerInfo = connectorsResult.connectors[provider]
if (!providerInfo || !providerInfo.available) {
setError(`Cloud provider "${provider}" is not available or configured.`)
return
}
// Check connector status
const statusResponse = await fetch(`/api/connectors/${provider}/status`)
if (!statusResponse.ok) {
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
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}`)
if (tokenResponse.ok) {
const tokenData = await tokenResponse.json()
if (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'
}
} catch {
accessTokenError = 'Failed to fetch access token'
}
}
setConnector({
id: provider,
name: providerInfo.name,
description: providerInfo.description,
status: isConnected ? "connected" : "not_connected",
type: provider,
connectionId: activeConnection?.connection_id,
hasAccessToken,
accessTokenError
})
} catch (error) {
console.error('Failed to load connector info:', error)
setError(error instanceof Error ? error.message : 'Failed to load connector information')
} finally {
setIsLoading(false)
}
}
if (provider) {
fetchConnectorInfo()
}
}, [provider])
const handleFileSelected = (files: GoogleDriveFile[] | OneDriveFile[]) => {
setSelectedFiles(files)
console.log(`Selected ${files.length} files from ${provider}:`, files)
// You can add additional handling here like triggering sync, etc.
}
const getProviderDisplayName = () => {
const nameMap: { [key: string]: string } = {
'google_drive': 'Google Drive',
'onedrive': 'OneDrive',
'sharepoint': 'SharePoint'
}
return nameMap[provider] || provider
}
if (isLoading) {
return (
<div className="container mx-auto p-6">
<div className="flex items-center justify-center py-12">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div>
<p>Loading {getProviderDisplayName()} connector...</p>
</div>
</div>
</div>
)
}
if (error || !connector) {
return (
<div className="container mx-auto p-6">
<div className="mb-6">
<Button
variant="ghost"
onClick={() => router.back()}
className="mb-4"
>
<ArrowLeft className="h-4 w-4 mr-2" />
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>
<p className="text-muted-foreground mb-4">{error}</p>
<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"
onClick={() => router.back()}
className="mb-4"
>
<ArrowLeft className="h-4 w-4 mr-2" />
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>
<p className="text-muted-foreground mb-4">
You need to connect your {connector.name} account before you can select files.
</p>
<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"
onClick={() => router.back()}
className="mb-4"
>
<ArrowLeft className="h-4 w-4 mr-2" />
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>
<p className="text-muted-foreground mb-4">
{connector.accessTokenError || `Unable to get access token for ${connector.name}. Try reconnecting your account.`}
</p>
<Button onClick={() => router.push('/settings')}>
Reconnect {connector.name}
</Button>
</div>
</div>
</div>
)
}
return (
<div className="container mx-auto p-6">
<div className="mb-6">
<Button
variant="ghost"
onClick={() => router.back()}
className="mb-4"
>
<ArrowLeft className="h-4 w-4 mr-2" />
Back
</Button>
<div className="mb-6 max-w-4xl mx-auto">
<h1 className="text-2xl font-bold mb-2">Select Files from {connector.name}</h1>
<p className="text-muted-foreground">
Choose specific files from your {connector.name} account to add to your knowledge base.
</p>
</div>
</div>
<div className="max-w-4xl mx-auto">
{connector.type === "google_drive" && (
<GoogleDrivePicker
onFileSelected={handleFileSelected}
selectedFiles={selectedFiles as GoogleDriveFile[]}
isAuthenticated={true}
accessToken={accessToken || undefined}
/>
)}
{(connector.type === "onedrive" || connector.type === "sharepoint") && (
<OneDrivePicker
onFileSelected={handleFileSelected}
selectedFiles={selectedFiles as OneDriveFile[]}
isAuthenticated={true}
accessToken={accessToken || undefined}
connectorType={connector.type as "onedrive" | "sharepoint"}
/>
)}
</div>
{selectedFiles.length > 0 && (
<div className="max-w-4xl mx-auto mt-8 flex justify-center gap-3">
<Button
onClick={() => {
// Handle sync/upload action here
console.log('Starting sync for selected files:', selectedFiles)
// You can add API call to trigger sync
}}
disabled={selectedFiles.length === 0}
>
Sync Selected Files ({selectedFiles.length})
</Button>
<Button
variant="outline"
onClick={() => setSelectedFiles([])}>
Clear Selection
</Button>
</div>
)}
</div>
)
}

View file

@ -0,0 +1,299 @@
"use client"
import { useState, useEffect, useCallback } from "react"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
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"
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?: any
}
}
interface CloudConnector {
id: string
name: string
description: string
icon: React.ReactNode
status: "not_connected" | "connecting" | "connected" | "error"
type: string
connectionId?: string
hasAccessToken: boolean
accessTokenError?: string
}
interface CloudConnectorsDialogProps {
isOpen: boolean
onOpenChange: (open: boolean) => void
onFileSelected?: (files: GoogleDriveFile[] | OneDriveFile[], connectorType: string) => void
}
export function CloudConnectorsDialog({
isOpen,
onOpenChange,
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 [isGooglePickerOpen, setIsGooglePickerOpen] = useState(false)
const getConnectorIcon = (iconName: string) => {
const iconMap: { [key: string]: React.ReactElement } = {
'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': (
<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': (
<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>
)
}
const fetchConnectorStatuses = useCallback(async () => {
if (!isOpen) return
setIsLoading(true)
try {
// Fetch available connectors from backend
const connectorsResponse = await fetch('/api/connectors')
if (!connectorsResponse.ok) {
throw new Error('Failed to load 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
)
// Initialize connectors list
const initialConnectors = cloudConnectorTypes.map(type => ({
id: type,
name: connectorsResult.connectors[type].name,
description: connectorsResult.connectors[type].description,
icon: getConnectorIcon(connectorsResult.connectors[type].icon),
status: "not_connected" as const,
type: type,
hasAccessToken: false,
accessTokenError: undefined
}))
setConnectors(initialConnectors)
// Check status for each cloud connector type
for (const connectorType of cloudConnectorTypes) {
try {
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: any) => 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}`)
if (tokenResponse.ok) {
const tokenData = await tokenResponse.json()
if (tokenData.access_token) {
hasAccessToken = true
setConnectorAccessTokens(prev => ({
...prev,
[connectorType]: tokenData.access_token
}))
}
} else {
const errorData = await tokenResponse.json().catch(() => ({ error: 'Token unavailable' }))
accessTokenError = errorData.error || 'Access token unavailable'
}
} catch (e) {
accessTokenError = 'Failed to fetch access token'
}
}
setConnectors(prev => prev.map(c =>
c.type === connectorType
? {
...c,
status: isConnected ? "connected" : "not_connected",
connectionId: activeConnection?.connection_id,
hasAccessToken,
accessTokenError
}
: c
))
}
} catch (error) {
console.error(`Failed to check status for ${connectorType}:`, error)
}
}
} catch (error) {
console.error('Failed to load cloud connectors:', error)
} finally {
setIsLoading(false)
}
}, [isOpen])
const handleFileSelection = (connectorId: string, files: GoogleDriveFile[] | OneDriveFile[]) => {
setSelectedFiles(prev => ({
...prev,
[connectorId]: files
}))
onFileSelected?.(files, connectorId)
}
useEffect(() => {
fetchConnectorStatuses()
}, [fetchConnectorStatuses])
return (
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-2xl max-h-[80vh] overflow-hidden">
<DialogHeader>
<DialogTitle>Cloud File Connectors</DialogTitle>
<DialogDescription>
Select files from your connected cloud storage providers
</DialogDescription>
</DialogHeader>
<div className="py-4">
{isLoading ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-6 w-6 animate-spin mr-2" />
Loading connectors...
</div>
) : connectors.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
No cloud connectors available. Configure them in Settings first.
</div>
) : (
<div className="space-y-6">
{/* Service Buttons Row */}
<div className="flex flex-wrap gap-3 justify-center">
{connectors
.filter(connector => connector.status === "connected")
.map((connector) => (
<Button
key={connector.id}
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()
if (connector.hasAccessToken) {
setActivePickerType(connector.id)
}
}}
className="min-w-[120px]"
>
{connector.name}
</Button>
))}
</div>
{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>
</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") {
return (
<div className="mt-6">
<GoogleDrivePicker
onFileSelected={(files) => {
handleFileSelection(connector.id, files)
setActivePickerType(null)
setIsGooglePickerOpen(false)
}}
selectedFiles={selectedFiles[connector.id] as GoogleDriveFile[] || []}
isAuthenticated={connector.status === "connected"}
accessToken={connectorAccessTokens[connector.type]}
onPickerStateChange={setIsGooglePickerOpen}
/>
</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"}
/>
</div>
)
}
return null
})()}
</div>
)}
</div>
</DialogContent>
</Dialog>
)
}

View file

@ -0,0 +1,77 @@
"use client"
import { useState } from "react"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { CloudConnectorsDialog } from "@/components/cloud-connectors-dialog"
import { Cloud, ChevronDown } 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?: any
}
}
interface CloudConnectorsDropdownProps {
onFileSelected?: (files: GoogleDriveFile[] | OneDriveFile[], connectorType: string) => void
buttonText?: string
variant?: "default" | "outline" | "secondary" | "ghost" | "link" | "destructive"
size?: "default" | "sm" | "lg" | "icon"
}
export function CloudConnectorsDropdown({
onFileSelected,
buttonText = "Cloud Files",
variant = "outline",
size = "default"
}: CloudConnectorsDropdownProps) {
const [isDialogOpen, setIsDialogOpen] = useState(false)
const handleOpenDialog = () => {
setIsDialogOpen(true)
}
return (
<>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant={variant} size={size}>
<Cloud className="mr-2 h-4 w-4" />
{buttonText}
<ChevronDown className="ml-2 h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
<DropdownMenuItem onClick={handleOpenDialog} className="cursor-pointer">
<Cloud className="mr-2 h-4 w-4" />
Select Cloud Files
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<CloudConnectorsDialog
isOpen={isDialogOpen}
onOpenChange={setIsDialogOpen}
onFileSelected={onFileSelected}
/>
</>
)
}

View file

@ -10,6 +10,7 @@ interface GoogleDrivePickerProps {
selectedFiles?: GoogleDriveFile[]
isAuthenticated: boolean
accessToken?: string
onPickerStateChange?: (isOpen: boolean) => void
}
interface GoogleDriveFile {
@ -88,7 +89,8 @@ export function GoogleDrivePicker({
onFileSelected,
selectedFiles = [],
isAuthenticated,
accessToken
accessToken,
onPickerStateChange
}: GoogleDrivePickerProps) {
const [isPickerLoaded, setIsPickerLoaded] = useState(false)
const [isPickerOpen, setIsPickerOpen] = useState(false)
@ -131,6 +133,7 @@ export function GoogleDrivePicker({
}
}, [])
const openPicker = () => {
if (!isPickerLoaded || !accessToken || !window.google?.picker) {
return
@ -138,7 +141,9 @@ export function GoogleDrivePicker({
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)
@ -149,9 +154,23 @@ export function GoogleDrivePicker({
.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)
}
}
@ -169,6 +188,7 @@ export function GoogleDrivePicker({
}
setIsPickerOpen(false)
onPickerStateChange?.(false)
}
const removeFile = (fileId: string) => {

View file

@ -0,0 +1,322 @@
"use client"
import { useState, useEffect } from "react"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { FileText, Folder, X } from "lucide-react"
interface OneDrivePickerProps {
onFileSelected: (files: OneDriveFile[]) => void
selectedFiles?: OneDriveFile[]
isAuthenticated: boolean
accessToken?: string
connectorType?: "onedrive" | "sharepoint"
onPickerStateChange?: (isOpen: boolean) => void
}
interface OneDriveFile {
id: string
name: string
mimeType?: string
webUrl?: string
driveItem?: {
file?: { mimeType: string }
folder?: any
}
}
interface GraphResponse {
value: OneDriveFile[]
}
declare global {
interface Window {
mgt?: {
Providers: {
globalProvider: any
}
}
}
}
export function OneDrivePicker({
onFileSelected,
selectedFiles = [],
isAuthenticated,
accessToken,
connectorType = "onedrive",
onPickerStateChange
}: OneDrivePickerProps) {
const [isLoading, setIsLoading] = useState(false)
const [files, setFiles] = useState<OneDriveFile[]>([])
const [isPickerOpen, setIsPickerOpen] = useState(false)
const [currentPath, setCurrentPath] = useState<string>(
connectorType === "sharepoint" ? 'sites?search=' : 'me/drive/root/children'
)
const [breadcrumbs, setBreadcrumbs] = useState<{id: string, name: string}[]>([
{id: 'root', name: connectorType === "sharepoint" ? 'SharePoint' : 'OneDrive'}
])
useEffect(() => {
const loadMGT = async () => {
if (typeof window !== 'undefined' && !window.mgt) {
try {
const mgtModule = await import('@microsoft/mgt-components')
const mgtProvider = await import('@microsoft/mgt-msal2-provider')
// Initialize provider if needed
if (!window.mgt?.Providers?.globalProvider && accessToken) {
// For simplicity, we'll use direct Graph API calls instead of MGT components
}
} catch (error) {
console.warn('MGT not available, falling back to direct API calls')
}
}
}
loadMGT()
}, [accessToken])
const fetchFiles = async (path: string = currentPath) => {
if (!accessToken) return
setIsLoading(true)
try {
const response = await fetch(`https://graph.microsoft.com/v1.0/${path}`, {
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
}
})
if (response.ok) {
const data: GraphResponse = await response.json()
setFiles(data.value || [])
} else {
console.error('Failed to fetch OneDrive files:', response.statusText)
}
} catch (error) {
console.error('Error fetching OneDrive files:', error)
} finally {
setIsLoading(false)
}
}
const openPicker = () => {
if (!accessToken) return
setIsPickerOpen(true)
onPickerStateChange?.(true)
fetchFiles()
}
const closePicker = () => {
setIsPickerOpen(false)
onPickerStateChange?.(false)
setFiles([])
setCurrentPath(
connectorType === "sharepoint" ? 'sites?search=' : 'me/drive/root/children'
)
setBreadcrumbs([
{id: 'root', name: connectorType === "sharepoint" ? 'SharePoint' : 'OneDrive'}
])
}
const handleFileClick = (file: OneDriveFile) => {
if (file.driveItem?.folder) {
// Navigate to folder
const newPath = `me/drive/items/${file.id}/children`
setCurrentPath(newPath)
setBreadcrumbs([...breadcrumbs, {id: file.id, name: file.name}])
fetchFiles(newPath)
} else {
// Select file
const isAlreadySelected = selectedFiles.some(f => f.id === file.id)
if (!isAlreadySelected) {
onFileSelected([...selectedFiles, file])
}
}
}
const navigateToBreadcrumb = (index: number) => {
if (index === 0) {
setCurrentPath('me/drive/root/children')
setBreadcrumbs([{id: 'root', name: 'OneDrive'}])
fetchFiles('me/drive/root/children')
} else {
const targetCrumb = breadcrumbs[index]
const newPath = `me/drive/items/${targetCrumb.id}/children`
setCurrentPath(newPath)
setBreadcrumbs(breadcrumbs.slice(0, index + 1))
fetchFiles(newPath)
}
}
const removeFile = (fileId: string) => {
const updatedFiles = selectedFiles.filter(file => file.id !== fileId)
onFileSelected(updatedFiles)
}
const getFileIcon = (file: OneDriveFile) => {
if (file.driveItem?.folder) {
return <Folder className="h-4 w-4" />
}
return <FileText className="h-4 w-4" />
}
const getMimeTypeLabel = (file: OneDriveFile) => {
const mimeType = file.driveItem?.file?.mimeType || file.mimeType || ''
const typeMap: { [key: string]: string } = {
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'Word Doc',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'Excel',
'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'PowerPoint',
'application/pdf': 'PDF',
'text/plain': 'Text',
'image/jpeg': 'Image',
'image/png': 'Image',
}
if (file.driveItem?.folder) return 'Folder'
return typeMap[mimeType] || 'Document'
}
const serviceName = connectorType === "sharepoint" ? "SharePoint" : "OneDrive"
if (!isAuthenticated) {
return (
<div className="text-sm text-muted-foreground p-4 bg-muted/20 rounded-md">
Please connect to {serviceName} first to select specific files.
</div>
)
}
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<h4 className="text-sm font-medium">{serviceName} File Selection</h4>
<p className="text-xs text-muted-foreground">
Choose specific files to sync instead of syncing everything
</p>
</div>
<Button
onClick={openPicker}
disabled={!accessToken}
size="sm"
variant="outline"
title={!accessToken ? `Access token required - try disconnecting and reconnecting ${serviceName}` : ""}
>
{!accessToken ? "No Access Token" : "Select Files"}
</Button>
</div>
{/* Status message when access token is missing */}
{isAuthenticated && !accessToken && (
<div className="text-xs text-amber-600 bg-amber-50 p-3 rounded-md border border-amber-200">
<div className="font-medium mb-1">Access token unavailable</div>
<div>The file picker requires an access token. Try disconnecting and reconnecting your {serviceName} account.</div>
</div>
)}
{/* File Picker Modal */}
{isPickerOpen && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[100]">
<div className="bg-white rounded-lg p-6 max-w-2xl w-full max-h-[80vh] overflow-hidden flex flex-col">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold">Select Files from {serviceName}</h3>
<Button onClick={closePicker} size="sm" variant="ghost">
<X className="h-4 w-4" />
</Button>
</div>
{/* Breadcrumbs */}
<div className="flex items-center space-x-2 mb-4 text-sm">
{breadcrumbs.map((crumb, index) => (
<div key={crumb.id} className="flex items-center">
{index > 0 && <span className="mx-2 text-gray-400">/</span>}
<button
onClick={() => navigateToBreadcrumb(index)}
className="text-blue-600 hover:underline"
>
{crumb.name}
</button>
</div>
))}
</div>
{/* File List */}
<div className="flex-1 overflow-y-auto border rounded-md">
{isLoading ? (
<div className="p-4 text-center text-muted-foreground">Loading...</div>
) : files.length === 0 ? (
<div className="p-4 text-center text-muted-foreground">No files found</div>
) : (
<div className="divide-y">
{files.map((file) => (
<div
key={file.id}
className="flex items-center p-3 hover:bg-gray-50 cursor-pointer"
onClick={() => handleFileClick(file)}
>
<div className="flex items-center gap-3 flex-1">
{getFileIcon(file)}
<span className="font-medium">{file.name}</span>
<Badge variant="secondary" className="text-xs">
{getMimeTypeLabel(file)}
</Badge>
</div>
{selectedFiles.some(f => f.id === file.id) && (
<Badge variant="default" className="text-xs">Selected</Badge>
)}
</div>
))}
</div>
)}
</div>
</div>
</div>
)}
{selectedFiles.length > 0 && (
<div className="space-y-2">
<p className="text-xs text-muted-foreground">
Selected files ({selectedFiles.length}):
</p>
<div className="max-h-32 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)}
<span className="truncate font-medium">{file.name}</span>
<Badge variant="secondary" className="text-xs px-1 py-0.5 h-auto">
{getMimeTypeLabel(file)}
</Badge>
</div>
<Button
onClick={() => removeFile(file.id)}
size="sm"
variant="ghost"
className="h-6 w-6 p-0"
>
<X className="h-3 w-3" />
</Button>
</div>
))}
</div>
<Button
onClick={() => onFileSelected([])}
size="sm"
variant="ghost"
className="text-xs h-6"
>
Clear all
</Button>
</div>
)}
</div>
)
}

View file

@ -24,11 +24,8 @@ async def connector_sync(request: Request, connector_service, session_manager):
max_files = data.get("max_files")
try:
logger.debug("Starting connector sync", connector_type=connector_type, max_files=max_files)
user = request.state.user
jwt_token = request.state.jwt_token
logger.debug("User authenticated", user_id=user.user_id)
# Get all active connections for this connector type and user
connections = await connector_service.connection_manager.list_connections(
@ -45,18 +42,15 @@ async def connector_sync(request: Request, connector_service, session_manager):
# Start sync tasks for all active connections
task_ids = []
for connection in active_connections:
logger.debug("About to call sync_connector_files for connection", connection_id=connection.connection_id)
task_id = await connector_service.sync_connector_files(
connection.connection_id,
user.user_id,
max_files,
jwt_token=jwt_token,
# NEW: thread picker selections through
selected_files=data.get("selected_files"),
selected_folders=data.get("selected_folders"),
)
task_ids.append(task_id)
logger.debug("Got task ID", task_id=task_id)
return JSONResponse(
{
@ -69,14 +63,7 @@ async def connector_sync(request: Request, connector_service, session_manager):
)
except Exception as e:
import sys
import traceback
error_msg = f"[ERROR] Connector sync failed: {str(e)}"
logger.error(error_msg)
traceback.print_exc(file=sys.stderr)
sys.stderr.flush()
logger.error("Connector sync failed", error=str(e))
return JSONResponse({"error": f"Sync failed: {str(e)}"}, status_code=500)
@ -247,9 +234,6 @@ async def connector_webhook(request: Request, connector_service, session_manager
except Exception as e:
logger.error("Failed to process webhook for connection", connection_id=connection.connection_id, error=str(e))
import traceback
traceback.print_exc()
return JSONResponse(
{
"status": "error",
@ -261,10 +245,7 @@ async def connector_webhook(request: Request, connector_service, session_manager
)
except Exception as e:
import traceback
logger.error("Webhook processing failed", error=str(e))
traceback.print_exc()
return JSONResponse(
{"error": f"Webhook processing failed: {str(e)}"}, status_code=500
)
@ -288,7 +269,7 @@ async def connector_token(request: Request, connector_service, session_manager):
# Get the connector instance
connector = await connector_service._get_connector(connection_id)
if not connector:
return JSONResponse({"error": "Connector not available"}, status_code=404)
return JSONResponse({"error": f"Connector not available - authentication may have failed for {connector_type}"}, status_code=404)
# For Google Drive, get the access token
if connector_type == "google_drive" and hasattr(connector, 'oauth'):
@ -301,9 +282,22 @@ async def connector_token(request: Request, connector_service, session_manager):
})
else:
return JSONResponse({"error": "Invalid or expired credentials"}, status_code=401)
# For OneDrive and SharePoint, get the access token
elif connector_type in ["onedrive", "sharepoint"] and hasattr(connector, 'oauth'):
try:
access_token = connector.oauth.get_access_token()
return JSONResponse({
"access_token": access_token,
"expires_in": None # MSAL handles token expiry internally
})
except ValueError as e:
return JSONResponse({"error": f"Failed to get access token: {str(e)}"}, status_code=401)
except Exception as e:
return JSONResponse({"error": f"Authentication error: {str(e)}"}, status_code=500)
return JSONResponse({"error": "Token not available for this connector type"}, status_code=400)
except Exception as e:
print(f"Error getting connector token: {e}")
logger.error("Error getting connector token", error=str(e))
return JSONResponse({"error": str(e)}, status_code=500)