diff --git a/src-tauri/src/commands/file.rs b/src-tauri/src/commands/file.rs index 595df47..c1cde97 100644 --- a/src-tauri/src/commands/file.rs +++ b/src-tauri/src/commands/file.rs @@ -1,6 +1,8 @@ use crate::dto::{ImportFileResult, ImportSessionsResponse}; use crate::{AppState, OpenedFiles}; -use retrochat::services::ImportFileRequest; +use retrochat::models::provider::config::{ClaudeCodeConfig, CodexConfig, GeminiCliConfig}; +use retrochat::models::Provider; +use retrochat::services::{BatchImportRequest, ImportFileRequest}; use std::path::PathBuf; use std::sync::Arc; use tauri::{AppHandle, Emitter, Manager, State}; @@ -144,3 +146,202 @@ pub async fn import_sessions( results, }) } + +// Helper struct to track import stats +struct ImportStats { + results: Vec, + total_sessions_imported: i32, + total_messages_imported: i32, + successful_imports: i32, + failed_imports: i32, + total_files: i32, +} + +impl ImportStats { + fn new() -> Self { + Self { + results: Vec::new(), + total_sessions_imported: 0, + total_messages_imported: 0, + successful_imports: 0, + failed_imports: 0, + total_files: 0, + } + } +} + +// Command to import sessions from preset providers +#[tauri::command] +pub async fn import_from_provider( + state: State<'_, Arc>>, + provider: String, + overwrite: bool, +) -> Result { + log::info!( + "import_from_provider called with provider: {}, overwrite: {}", + provider, + overwrite + ); + + let state_guard = state.lock().await; + let import_service = &state_guard.import_service; + + let mut stats = ImportStats::new(); + + // Parse provider string to Provider enum + let providers = match provider.to_lowercase().as_str() { + "all" => vec![Provider::All], + "claude" => vec![Provider::ClaudeCode], + "gemini" => vec![Provider::GeminiCLI], + "codex" => vec![Provider::Codex], + _ => return Err(format!("Unknown provider: {}", provider)), + }; + + // Expand "All" to all specific providers + let expanded_providers = Provider::expand_all(providers); + + for prov in expanded_providers { + match prov { + Provider::All => { + // Should not happen due to expansion above + unreachable!("Provider::All should have been expanded") + } + Provider::ClaudeCode => { + log::info!("Importing from Claude Code directories..."); + if let Err(e) = import_provider_directories( + &ClaudeCodeConfig::create(), + import_service, + overwrite, + &mut stats, + ) + .await + { + log::error!("Error importing Claude directories: {}", e); + } + } + Provider::GeminiCLI => { + log::info!("Importing from Gemini directories..."); + if let Err(e) = import_provider_directories( + &GeminiCliConfig::create(), + import_service, + overwrite, + &mut stats, + ) + .await + { + log::error!("Error importing Gemini directories: {}", e); + } + } + Provider::Codex => { + log::info!("Importing from Codex directories..."); + if let Err(e) = import_provider_directories( + &CodexConfig::create(), + import_service, + overwrite, + &mut stats, + ) + .await + { + log::error!("Error importing Codex directories: {}", e); + } + } + Provider::Other(name) => { + log::error!("Unknown provider: {}", name); + return Err(format!("Unknown provider: {}", name)); + } + } + } + + log::info!( + "Provider import completed - {} successful, {} failed, total: {} sessions, {} messages", + stats.successful_imports, + stats.failed_imports, + stats.total_sessions_imported, + stats.total_messages_imported + ); + + Ok(ImportSessionsResponse { + total_files: stats.total_files, + successful_imports: stats.successful_imports, + failed_imports: stats.failed_imports, + total_sessions_imported: stats.total_sessions_imported, + total_messages_imported: stats.total_messages_imported, + results: stats.results, + }) +} + +// Helper function to import from a provider's directories +async fn import_provider_directories( + config: &retrochat::models::provider::config::ProviderConfig, + import_service: &retrochat::services::ImportService, + overwrite: bool, + stats: &mut ImportStats, +) -> Result<(), String> { + let directories = config.get_import_directories(); + + if directories.is_empty() { + log::info!("No directories found for provider: {}", config.name); + return Ok(()); + } + + for dir_path in directories { + let path = std::path::Path::new(&dir_path); + if !path.exists() { + log::warn!("Directory not found: {}", path.display()); + continue; + } + + log::info!("Importing from directory: {}", path.display()); + + let batch_request = BatchImportRequest { + directory_path: dir_path.clone(), + providers: None, + project_name: None, + overwrite_existing: Some(overwrite), + recursive: Some(true), + }; + + match import_service.import_batch(batch_request).await { + Ok(response) => { + log::info!( + "Successfully imported from directory '{}': {} sessions, {} messages", + dir_path, + response.total_sessions_imported, + response.total_messages_imported + ); + + stats.total_files += response.total_files_processed; + stats.successful_imports += response.successful_imports; + stats.failed_imports += response.failed_imports; + stats.total_sessions_imported += response.total_sessions_imported; + stats.total_messages_imported += response.total_messages_imported; + + // Add directory-level result + stats.results.push(ImportFileResult { + file_path: dir_path.clone(), + sessions_imported: response.total_sessions_imported, + messages_imported: response.total_messages_imported, + success: response.failed_imports == 0, + error: if response.errors.is_empty() { + None + } else { + Some(response.errors.join("; ")) + }, + }); + } + Err(e) => { + log::error!("Failed to import from directory '{}': {}", dir_path, e); + stats.failed_imports += 1; + stats.results.push(ImportFileResult { + file_path: dir_path.clone(), + sessions_imported: 0, + messages_imported: 0, + success: false, + error: Some(e.to_string()), + }); + } + } + } + + Ok(()) +} diff --git a/src-tauri/src/commands/session.rs b/src-tauri/src/commands/session.rs index c1fba5b..352c370 100644 --- a/src-tauri/src/commands/session.rs +++ b/src-tauri/src/commands/session.rs @@ -122,10 +122,8 @@ pub async fn get_session_detail( // Create a map of tool_operation_id -> tool_operation for efficient lookup log::debug!("Building tool operation lookup map"); - let tool_op_by_id: std::collections::HashMap<_, _> = tool_operations - .into_iter() - .map(|op| (op.id, op)) - .collect(); + let tool_op_by_id: std::collections::HashMap<_, _> = + tool_operations.into_iter().map(|op| (op.id, op)).collect(); // Create a map of message_id -> tool_operation log::debug!("Building message -> tool operation map"); diff --git a/src-tauri/src/dto.rs b/src-tauri/src/dto.rs index 6d9093c..cff9cb6 100644 --- a/src-tauri/src/dto.rs +++ b/src-tauri/src/dto.rs @@ -370,15 +370,15 @@ impl From for ToolUsageMetrics #[derive(Debug, Serialize, Deserialize)] pub struct HistogramRequest { - pub start_time: String, // RFC3339 timestamp - pub end_time: String, // RFC3339 timestamp - pub interval_minutes: i32, // 5, 15, 60, 360 + pub start_time: String, // RFC3339 timestamp + pub end_time: String, // RFC3339 timestamp + pub interval_minutes: i32, // 5, 15, 60, 360 } #[derive(Debug, Serialize, Deserialize)] pub struct HistogramBucket { - pub timestamp: String, // Bucket start time (RFC3339) - pub count: i32, // Count in this bucket + pub timestamp: String, // Bucket start time (RFC3339) + pub count: i32, // Count in this bucket } #[derive(Debug, Serialize, Deserialize)] diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index d948be1..f35104b 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -9,7 +9,10 @@ use commands::{ analyze_session, cancel_analysis, create_analysis, get_analysis_result, get_analysis_status, list_analyses, run_analysis, }, - file::{clear_opened_files, get_opened_files, handle_file_drop, import_sessions}, + file::{ + clear_opened_files, get_opened_files, handle_file_drop, import_from_provider, + import_sessions, + }, histogram::{get_session_activity_histogram, get_user_message_histogram}, session::{get_providers, get_session_detail, get_sessions, search_messages}, }; @@ -170,6 +173,7 @@ pub async fn run() -> anyhow::Result<()> { get_opened_files, clear_opened_files, import_sessions, + import_from_provider, get_session_activity_histogram, get_user_message_histogram, ]) diff --git a/ui-react/src/components/session-manager.tsx b/ui-react/src/components/session-manager.tsx index a0442af..4fb52c6 100644 --- a/ui-react/src/components/session-manager.tsx +++ b/ui-react/src/components/session-manager.tsx @@ -2,7 +2,7 @@ import { invoke } from '@tauri-apps/api/core' import { listen } from '@tauri-apps/api/event' import { open } from '@tauri-apps/plugin-dialog' import { stat } from '@tauri-apps/plugin-fs' -import { FileText, History, Upload } from 'lucide-react' +import { Database, FileText, FolderOpen, History, Upload } from 'lucide-react' import { useTheme } from 'next-themes' import { useCallback, useEffect, useState } from 'react' import { useHotkeys } from 'react-hotkeys-hook' @@ -38,10 +38,12 @@ export function SessionManager() { const [provider, setProvider] = useState(null) const [searchQuery, setSearchQuery] = useState('') const { theme, setTheme } = useTheme() + const [importMethodDialogOpen, setImportMethodDialogOpen] = useState(false) const [importDialogOpen, setImportDialogOpen] = useState(false) const [filesToImport, setFilesToImport] = useState([]) const [refreshTrigger, setRefreshTrigger] = useState(0) const [hasAnySessions, setHasAnySessions] = useState(false) + const [isImporting, setIsImporting] = useState(false) // Keyboard shortcuts useHotkeys('meta+1', () => setActiveTab('sessions'), { @@ -105,6 +107,13 @@ export function SessionManager() { }, [handleFilesImport]) const handleImport = async () => { + // Show import method selection dialog + setImportMethodDialogOpen(true) + } + + const handleFileImport = async () => { + setImportMethodDialogOpen(false) + try { // Open file dialog to select files const files = await open({ @@ -134,6 +143,73 @@ export function SessionManager() { } } + const handleProviderImport = async (providerName: string) => { + // Prevent concurrent imports + if (isImporting) { + toast.warning('Import already in progress') + return + } + + setIsImporting(true) + setImportMethodDialogOpen(false) + + console.log('Importing from provider:', providerName) + + // Use promise-based toast for better control + try { + await toast.promise( + invoke<{ + total_files: number + successful_imports: number + failed_imports: number + total_sessions_imported: number + total_messages_imported: number + results: Array<{ + file_path: string + sessions_imported: number + messages_imported: number + success: boolean + error?: string + }> + }>('import_from_provider', { provider: providerName, overwrite: false }), + { + loading: `Importing from ${providerName}...`, + success: async (response) => { + // Refresh the session list and select first session + setRefreshTrigger((prev) => prev + 1) + + // Fetch the first session and select it + try { + const { getSessions } = await import('@/lib/api') + const sessions = await getSessions(1, 1, provider) + if (sessions.length > 0) { + setSelectedSession(sessions[0].id) + } + } catch (error) { + console.error('Failed to fetch first session:', error) + } + + if (response.total_files === 0) { + return `No files found for ${providerName}` + } + + if (response.failed_imports > 0) { + return `Imported ${response.successful_imports}/${response.total_files} files (${response.total_sessions_imported} sessions, ${response.total_messages_imported} messages)` + } + + return `Successfully imported ${response.total_sessions_imported} sessions (${response.total_messages_imported} messages) from ${response.total_files} file(s)` + }, + error: (error) => { + console.error('Failed to import from provider:', error) + return `Failed to import: ${error}` + }, + } + ) + } finally { + setIsImporting(false) + } + } + const confirmImport = async () => { const filePaths = filesToImport.map((file) => file.path) console.log('Confirming import for:', filePaths) @@ -313,6 +389,120 @@ export function SessionManager() { + {/* Import Method Selection Dialog */} + + + + Choose Import Method + Select how you want to import chat sessions + + +
+ {/* File Import Option */} + + + {/* Provider Import Options */} +
+
+
+ OR IMPORT FROM PRESET +
+
+ + + +
+ + + + + +
+
+
+ + + + + +
+ {/* Import Files Dialog */} diff --git a/ui-react/src/components/ui/dialog.tsx b/ui-react/src/components/ui/dialog.tsx index 147842f..75f7fd2 100644 --- a/ui-react/src/components/ui/dialog.tsx +++ b/ui-react/src/components/ui/dialog.tsx @@ -35,7 +35,7 @@ const DialogContent = React.forwardRef<