diff --git a/electron/main.ts b/electron/main.ts index c6fdc2d..8629fe7 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -865,6 +865,7 @@ ipcMain.handle('assistant:create', async (_event, profileId: string, params: { n } const assistantService = service.getAssistantService() const assistant = await assistantService.createAssistant(params) + track('assistant_created', { region: params.region || 'us' }) return { success: true, data: assistant } } catch (error) { const message = error instanceof Error ? error.message : 'Failed to create assistant' @@ -910,6 +911,7 @@ ipcMain.handle('assistant:delete', async (_event, profileId: string, name: strin } const assistantService = service.getAssistantService() await assistantService.deleteAssistant(name) + track('assistant_deleted') return { success: true } } catch (error) { const message = error instanceof Error ? error.message : 'Failed to delete assistant' @@ -959,6 +961,7 @@ ipcMain.handle('assistant:files:upload', async (_event, profileId: string, assis } const assistantService = service.getAssistantService() const file = await assistantService.uploadFile(assistantName, params) + track('file_uploaded', { multimodal: params.multimodal || false }) return { success: true, data: file } } catch (error) { const message = error instanceof Error ? error.message : 'Failed to upload file' @@ -974,6 +977,7 @@ ipcMain.handle('assistant:files:delete', async (_event, profileId: string, assis } const assistantService = service.getAssistantService() await assistantService.deleteFile(assistantName, fileId) + track('file_deleted') return { success: true } } catch (error) { const message = error instanceof Error ? error.message : 'Failed to delete file' @@ -996,6 +1000,7 @@ ipcMain.handle('assistant:chat', async (_event, profileId: string, assistantName } const assistantService = service.getAssistantService() const response = await assistantService.chat(assistantName, params) + track('chat_message_sent', { model: params.model, messageCount: params.messages.length }) return { success: true, data: response } } catch (error) { const message = error instanceof Error ? error.message : 'Failed to send chat message' @@ -1039,6 +1044,7 @@ ipcMain.handle('assistant:chat:stream:start', async (event, profileId: string, a activeChatStreams.delete(streamId) }) + track('chat_stream_started', { model: params.model }) return { success: true, data: { streamId } } } catch (error) { const message = error instanceof Error ? error.message : 'Failed to start chat stream' diff --git a/electron/preload.ts b/electron/preload.ts index a6951fa..24e08fb 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -219,6 +219,14 @@ contextBridge.exposeInMainWorld('electronAPI', { ipcRenderer.on('context-menu:assistant-action', handler) return () => ipcRenderer.removeListener('context-menu:assistant-action', handler) }, + showFileMenu: (assistantName: string, fileId: string, fileName: string): void => { + ipcRenderer.send('context-menu:show-file', assistantName, fileId, fileName) + }, + onFileAction: (callback: (action: { action: string; assistantName: string; fileId: string; fileName: string }) => void): (() => void) => { + const handler = (_event: any, data: { action: string; assistantName: string; fileId: string; fileName: string }) => callback(data) + ipcRenderer.on('context-menu:file-action', handler) + return () => ipcRenderer.removeListener('context-menu:file-action', handler) + }, }, profiles: { getAll: async (): Promise => { diff --git a/src/components/files/FilesPanel.tsx b/src/components/files/FilesPanel.tsx index e349a5c..31ef825 100644 --- a/src/components/files/FilesPanel.tsx +++ b/src/components/files/FilesPanel.tsx @@ -1,4 +1,4 @@ -import { useState, useMemo, useCallback } from 'react' +import { useState, useMemo, useCallback, useEffect } from 'react' import { FileText, Upload, Check, AlertCircle, Loader2, Trash2 } from 'lucide-react' import { usePinecone } from '../../providers/PineconeProvider' import { useAssistantSelection } from '../../context/AssistantSelectionContext' @@ -119,6 +119,51 @@ export function FilesPanel({ className }: FilesPanelProps) { setActiveFile(fileId) }, [setActiveFile]) + // Handle right-click context menu on file items + const handleFileContextMenu = useCallback((e: React.MouseEvent, file: AssistantFile) => { + e.preventDefault() + if (!activeAssistant) return + window.electronAPI.contextMenu.showFileMenu(activeAssistant, file.id, file.name) + }, [activeAssistant]) + + // Listen for file context menu actions + useEffect(() => { + if (!currentProfile?.id || !activeAssistant) return + + const cleanup = window.electronAPI.contextMenu.onFileAction(async (data) => { + if (data.assistantName !== activeAssistant) return + + if (data.action === 'delete') { + // Confirm deletion + const confirmed = window.confirm(`Delete "${data.fileName}"?\n\nThis action cannot be undone.`) + if (!confirmed) return + + try { + await window.electronAPI.assistant.files.delete(currentProfile.id, activeAssistant, data.fileId) + // Clear selection if deleted file was selected + if (activeFile === data.fileId) { + setActiveFile(null) + } + refetch() + } catch (err) { + console.error('Failed to delete file:', err) + } + } else if (data.action === 'download') { + // Get file details to get signed URL + try { + const file = await window.electronAPI.assistant.files.describe(currentProfile.id, activeAssistant, data.fileId) + if (file.signedUrl) { + await window.electronAPI.shell.openExternal(file.signedUrl) + } + } catch (err) { + console.error('Failed to download file:', err) + } + } + }) + + return cleanup + }, [currentProfile?.id, activeAssistant, activeFile, setActiveFile, refetch]) + // If no assistant is selected, show prompt if (!activeAssistant) { return ( @@ -254,6 +299,7 @@ export function FilesPanel({ className }: FilesPanelProps) {