chemistryService->healthCheck(); return response()->json(new ChemistryResponseResource($result)); } // ==================== Threads ==================== public function createThread(): JsonResponse { $user = Auth::user(); $result = $this->chemistryService->createThread(); if (!$result['success']) { return response()->json(new ChemistryResponseResource($result), 500); } $thread = ChemistryThread::create([ 'user_id' => $user->id, 'thread_id' => $result['data']['thread_id'], 'title' => 'New Conversation', 'status' => 'active', 'last_used_at' => now(), ]); return response()->json([ 'success' => true, 'data' => [ 'thread_id' => $thread->thread_id, 'id' => $thread->id, 'created_at' => $thread->created_at, ], ]); } public function listThreads(): JsonResponse { $threads = Auth::user()->chemistryThreads() ->where('status', 'active') ->orderBy('last_used_at', 'desc') ->get(['id', 'thread_id', 'title', 'last_used_at', 'created_at']); return response()->json([ 'success' => true, 'data' => $threads, ]); } // ==================== Chat (معدل: title من أول سؤال) ==================== public function chat(ChemistryChatRequest $request): JsonResponse { $user = Auth::user(); $threadId = $request->input('thread_id'); if ($threadId) { $thread = ChemistryThread::where('user_id', $user->id) ->where('thread_id', $threadId) ->first(); if (!$thread) { return response()->json([ 'success' => false, 'error' => 'Thread not found or access denied', ], 403); } // ← لو الـ title لسه "New Conversation"، غيريه لأول سؤال if ($thread->title === 'New Conversation') { $message = $request->input('message'); $newTitle = mb_strlen($message) > 50 ? mb_substr($message, 0, 50) . '...' : $message; $thread->update([ 'title' => $newTitle, 'last_used_at' => now(), ]); } else { $thread->update(['last_used_at' => now()]); } } $result = $this->chemistryService->chat( $request->input('message'), $threadId ); if (!$result['success']) { return response()->json(new ChemistryResponseResource($result), $result['status']); } ChemistryAnalysis::create([ 'user_id' => $user->id, 'chemistry_thread_id' => $thread?->id, 'type' => 'chat', 'input_data' => $request->input('message'), 'response' => $result['data']['reply'] ?? null, 'processing_time_ms' => $result['data']['processing_time_ms'] ?? null, 'status' => 'success', ]); return response()->json(new ChemistryResponseResource($result)); } // ==================== Analyze SMILES ==================== public function analyzeSmiles(Request $request): JsonResponse { $user = Auth::user(); $request->validate([ 'smiles' => 'required|string|max:1000', 'thread_id' => 'nullable|string|max:255', ]); $threadId = $request->input('thread_id'); $thread = $this->validateThread($user->id, $threadId); $smiles = $request->input('smiles'); $result = $this->chemistryService->analyzeSmiles($smiles, $threadId); return $this->saveAndRespond($result, $user->id, 'smiles', $smiles, $thread?->id); } // ==================== Compare ==================== public function compare(ChemistryCompareRequest $request): JsonResponse { $user = Auth::user(); $threadId = $request->input('thread_id'); $thread = $this->validateThread($user->id, $threadId); $result = $this->chemistryService->compareMolecules( $request->input('smiles'), $threadId ); $input = implode(', ', $request->input('smiles')); return $this->saveAndRespond($result, $user->id, 'compare', $input, $thread?->id); } // ==================== Docking ==================== public function docking(ChemistryDockingRequest $request): JsonResponse { $user = Auth::user(); $threadId = $request->input('thread_id'); $thread = $this->validateThread($user->id, $threadId); $result = $this->chemistryService->analyzeDocking( $request->input('docking_data'), $threadId ); return $this->saveAndRespond($result, $user->id, 'docking', $request->input('docking_data'), $thread?->id); } // ==================== CSV (معدل: required thread_id) ==================== public function uploadCsv(Request $request): JsonResponse { $user = Auth::user(); $request->validate([ 'file' => 'required|file|mimes:csv,txt|max:2048', 'analysis_type' => 'nullable|in:full,quick,admet,classify', 'thread_id' => 'required|string|max:255', ]); $file = $request->file('file'); $analysisType = $request->input('analysis_type', 'full'); $threadId = $request->input('thread_id'); // Validate thread belongs to user $thread = ChemistryThread::where('user_id', $user->id) ->where('thread_id', $threadId) ->first(); if (!$thread) { return response()->json([ 'success' => false, 'error' => 'Thread not found or access denied', ], 403); } $result = $this->chemistryService->uploadCsv($file, $analysisType); if (!$result['success']) { return response()->json(new ChemistryResponseResource($result), $result['status']); } $apiJobId = $result['data']['job_id'] ?? null; $totalRows = $result['data']['total_rows'] ?? 0; if (!$apiJobId) { return response()->json([ 'success' => false, 'error' => 'Failed to get job ID from API', ], 500); } $job = ChemistryCsvJob::create([ 'user_id' => $user->id, 'chemistry_thread_id' => $thread->id, 'job_id' => $apiJobId, 'filename' => $file->getClientOriginalName(), 'analysis_type' => $analysisType, 'total_rows' => $totalRows, 'status' => 'queued', ]); // Update thread last_used_at $thread->update(['last_used_at' => now()]); return response()->json([ 'success' => true, 'data' => [ 'job_id' => $job->job_id, 'id' => $job->id, 'status' => $job->status, 'total_rows' => $job->total_rows, ], ]); } public function csvStatus(string $jobId): JsonResponse { $user = Auth::user(); $job = ChemistryCsvJob::where('user_id', $user->id) ->where('job_id', $jobId) ->first(); if (!$job) { return response()->json([ 'success' => false, 'error' => 'Job not found or access denied', ], 404); } $result = $this->chemistryService->getCsvStatus($jobId); if ($result['success'] && isset($result['data'])) { $apiData = $result['data']; $job->update([ 'status' => $apiData['status'] ?? $job->status, 'completed_rows' => $apiData['completed'] ?? $job->completed_rows, 'failed_rows' => $apiData['failed_rows'] ?? $job->failed_rows, 'progress_percent' => $apiData['progress_percent'] ?? $job->progress_percent, ]); if ($apiData['status'] === 'done' && !$job->completed_at) { $job->update(['completed_at' => now(), 'status' => 'done']); } } return response()->json([ 'success' => true, 'data' => [ 'job_id' => $job->job_id, 'status' => $job->status, 'total' => $job->total_rows, 'completed' => $job->completed_rows, 'failed' => $job->failed_rows, 'progress_percent' => $job->progress_percent, ], ]); } public function csvResults(string $jobId) { $user = Auth::user(); $job = ChemistryCsvJob::where('user_id', $user->id) ->where('job_id', $jobId) ->first(); if (!$job) { return response()->json([ 'success' => false, 'error' => 'Job not found or access denied', ], 404); } if ($job->status !== 'done') { return response()->json([ 'success' => false, 'error' => 'Job not completed yet', ], 400); } // ← Return content from DB if available if ($job->result_content) { return response($job->result_content) ->header('Content-Type', 'text/csv') ->header('Content-Disposition', 'attachment; filename="results_' . $jobId . '.csv"'); } $result = $this->chemistryService->getCsvResults($jobId); if (is_string($result)) { // ← Store content in DB instead of file path $job->update([ 'result_content' => $result, 'result_file_path' => null, ]); return response($result) ->header('Content-Type', 'text/csv') ->header('Content-Disposition', 'attachment; filename="results_' . $jobId . '.csv"'); } return response()->json(new ChemistryResponseResource($result)); } public function listJobs(Request $request): JsonResponse { $user = Auth::user(); $query = $user->chemistryCsvJobs() ->orderBy('created_at', 'desc'); // Optional: filter by thread_id if ($request->has('thread_id')) { $thread = ChemistryThread::where('user_id', $user->id) ->where('thread_id', $request->input('thread_id')) ->first(); if ($thread) { $query->where('chemistry_thread_id', $thread->id); } } $jobs = $query->get([ 'id', 'job_id', 'chemistry_thread_id', 'filename', 'analysis_type', 'status', 'total_rows', 'completed_rows', 'failed_rows', 'progress_percent', 'created_at' ]); return response()->json([ 'success' => true, 'data' => $jobs, ]); } public function deleteJob(string $jobId): JsonResponse { $user = Auth::user(); $job = ChemistryCsvJob::where('user_id', $user->id) ->where('job_id', $jobId) ->first(); if (!$job) { return response()->json([ 'success' => false, 'error' => 'Job not found or access denied', ], 404); } $result = $this->chemistryService->deleteCsvJob($jobId); if ($job->result_file_path && Storage::exists($job->result_file_path)) { Storage::delete($job->result_file_path); } $job->delete(); return response()->json([ 'success' => true, 'message' => 'Job deleted successfully', ]); } // ==================== User History ==================== public function userHistory(Request $request): JsonResponse { $user = Auth::user(); $type = $request->input('type'); $query = $user->chemistryAnalyses() ->with('thread:id,thread_id,title') ->orderBy('created_at', 'desc'); if ($type) { $query->where('type', $type); } $history = $query->paginate(20); return response()->json([ 'success' => true, 'data' => $history, ]); } // ==================== Thread Messages (جديد) ==================== public function threadMessages(Request $request): JsonResponse { $user = Auth::user(); $request->validate([ 'thread_id' => 'required|string|max:255', ]); $threadId = $request->input('thread_id'); $thread = ChemistryThread::where('user_id', $user->id) ->where('thread_id', $threadId) ->first(); if (!$thread) { return response()->json([ 'success' => false, 'error' => 'Thread not found or access denied', ], 403); } // Chat/analysis messages $messages = ChemistryAnalysis::where('user_id', $user->id) ->where('chemistry_thread_id', $thread->id) ->orderBy('created_at', 'asc') ->get(['id', 'type', 'input_data', 'response', 'status', 'created_at']); // CSV jobs related to this thread $csvJobs = ChemistryCsvJob::where('user_id', $user->id) ->where('chemistry_thread_id', $thread->id) ->orderBy('created_at', 'asc') ->get([ 'id', 'job_id', 'filename', 'analysis_type', 'status', 'total_rows', 'completed_rows', 'failed_rows', 'progress_percent', 'result_content', 'created_at', 'completed_at' ]); return response()->json([ 'success' => true, 'data' => [ 'thread_id' => $thread->thread_id, 'title' => $thread->title, 'messages' => $messages, 'csv_jobs' => $csvJobs, ], ]); } // ==================== Helper Methods ==================== private function validateThread(int $userId, ?string $threadId): ?ChemistryThread { if (!$threadId) return null; return ChemistryThread::where('user_id', $userId) ->where('thread_id', $threadId) ->first(); } private function saveAndRespond(array $result, int $userId, string $type, string $input, ?int $threadId = null): JsonResponse { if (!$result['success']) { ChemistryAnalysis::create([ 'user_id' => $userId, 'chemistry_thread_id' => $threadId, 'type' => $type, 'input_data' => $input, 'status' => 'failed', 'error_message' => is_string($result['error']) ? $result['error'] : json_encode($result['error']), ]); return response()->json(new ChemistryResponseResource($result), $result['status']); } ChemistryAnalysis::create([ 'user_id' => $userId, 'chemistry_thread_id' => $threadId, 'type' => $type, 'input_data' => $input, 'response' => $result['data']['reply'] ?? null, 'processing_time_ms' => $result['data']['processing_time_ms'] ?? null, 'status' => 'success', ]); return response()->json(new ChemistryResponseResource($result)); } }