diff --git a/web/src/components/FileUploadAndSubmit.jsx b/web/src/components/FileUploadAndSubmit.jsx new file mode 100644 index 0000000..39a9b15 --- /dev/null +++ b/web/src/components/FileUploadAndSubmit.jsx @@ -0,0 +1,34 @@ +import React from 'react'; + +// ใน component นี้ เราจะจัดการแค่ UI/Input/Button และเรียก onSubmit() ที่มาจาก parent +export default function FileUploadAndSubmit({ file, setFile, isPending, onSubmit, selectedModelId }) { + + // กำหนดว่าปุ่มควรถูก Disable หรือไม่ + const isDisabled = isPending || !selectedModelId || !file; + + return ( +
+ {/* File Upload */} +
+ + setFile(e.target.files[0])} + /> +
+ + {/* Submit */} + +
+ ); +} \ No newline at end of file diff --git a/web/src/components/ModelSelector.jsx b/web/src/components/ModelSelector.jsx new file mode 100644 index 0000000..36846b1 --- /dev/null +++ b/web/src/components/ModelSelector.jsx @@ -0,0 +1,28 @@ +import React from 'react'; + +export default function ModelSelector({ activeModels, isLoading, selectedModelId, setSelectedModelId, isPending }) { + return ( +
+ + +
+ ); +} \ No newline at end of file diff --git a/web/src/components/ResultDisplay.jsx b/web/src/components/ResultDisplay.jsx new file mode 100644 index 0000000..22a28fd --- /dev/null +++ b/web/src/components/ResultDisplay.jsx @@ -0,0 +1,25 @@ +import React from 'react'; + +export default function ResultDisplay({ result }) { + // หากไม่มีผลลัพธ์ (result เป็น null) ก็ไม่ต้องแสดงผลอะไร + if (!result) { + return null; + } + + return ( +
+
+

Inference Success!

+

ได้รับผลลัพธ์จากเซิร์ฟเวอร์แล้ว (JSON Payload)

+
+ + {/* แสดงผลลัพธ์ JSON ในรูปแบบที่อ่านง่าย */} +
+                {/* text-base-content: เพื่อให้สีตัวอักษรเป็นสีพื้นฐานของ Theme (ไม่ใช่สีขาว) */}
+                {/* whitespace-pre-wrap: บังคับให้ข้อความขึ้นบรรทัดใหม่แม้จะอยู่ใน 
 */}
+                {JSON.stringify(result, null, 2)}
+            
+
+ ); +} \ No newline at end of file diff --git a/web/src/pages/data/InferenceRun.jsx b/web/src/pages/data/InferenceRun.jsx new file mode 100644 index 0000000..3dcf8a6 --- /dev/null +++ b/web/src/pages/data/InferenceRun.jsx @@ -0,0 +1,71 @@ +import React, { useState } from 'react'; +// Import Components ย่อยจากโฟลเดอร์ components/ +import ModelSelector from '../../components/ModelSelector'; +import FileUploadAndSubmit from '../../components/FileUploadAndSubmit'; +import ResultDisplay from '../../components/ResultDisplay'; + +// Import Hooks จากโฟลเดอร์ services/ +import { useActiveModelList } from '../../services/modelRegistryApi'; +import { useRunInferenceMutation } from '../../services/inferenceApi'; + +export default function InferenceRun() { + // 1. Hooks สำหรับดึงข้อมูลและเรียกใช้งาน AI + const { data: activeModels, isLoading } = useActiveModelList(); + const { mutateAsync: runInference, isPending } = useRunInferenceMutation(); + + // 2. State สำหรับ Component + const [selectedModelId, setSelectedModelId] = useState(''); + const [file, setFile] = useState(null); + const [result, setResult] = useState(null); + + // 3. Logic เมื่อกดปุ่ม Submit + const handleSubmit = async (e) => { + e.preventDefault(); + setResult(null); // เคลียร์ผลลัพธ์เก่า + + if (!selectedModelId || !file) return alert('กรุณาเลือก Model และอัปโหลดไฟล์'); + + const formData = new FormData(); + formData.append('file', file); + + try { + // เรียก Mutation Hook: ส่ง modelId แทน targetUrl + const res = await runInference({ + formData, + modelId: selectedModelId // Backend คาดหวัง String ใน URL Path + }); + setResult(res); + } catch (error) { + // Error handling ถูกจัดการด้วย Toast ใน Hook แล้ว + console.error('Inference run failed in page component:', error); + } + }; + + return ( +
+

AI Inference (Run)

+

เลือก Model ที่ต้องการ และอัปโหลดไฟล์เพื่อสั่งรันการประมวลผล

+ + {/* Component 1: ตัวเลือก Model */} + + + {/* Component 2: อัปโหลดไฟล์และปุ่ม Submit */} + + + {/* Component 3: แสดงผลลัพธ์ */} + +
+ ); +} \ No newline at end of file diff --git a/web/src/routes/pageRoutes.jsx b/web/src/routes/pageRoutes.jsx index 8c4aab3..d5a0b34 100644 --- a/web/src/routes/pageRoutes.jsx +++ b/web/src/routes/pageRoutes.jsx @@ -1,6 +1,7 @@ import Dashboard from '../pages/Dashboard'; import BlankPage from '../pages/BlankPage'; import ModelRegistry from '../pages/data/ModelRegistry'; +import InferenceRun from '../pages/data/InferenceRun'; // Array ของเส้นทางย่อยภายใต้ /dashboard/ const pageRoutes = [ @@ -15,6 +16,12 @@ const pageRoutes = [ path: 'model-registry', element: , }, + // --- AI Inference (Run) --- + { + // Path: /dashboard/inference-run + path: 'inference-run', + element: , + }, // Fallback สำหรับเส้นทางที่ไม่ตรงกับเมนูใดๆ { path: '*', diff --git a/web/src/services/inferenceApi.js b/web/src/services/inferenceApi.js new file mode 100644 index 0000000..cb1ca91 --- /dev/null +++ b/web/src/services/inferenceApi.js @@ -0,0 +1,38 @@ +import { useMutation } from '@tanstack/react-query'; +import axiosClient from './axiosClient'; +import { useDispatch } from 'react-redux'; +import { addToast } from '../features/toast/toastSlice'; + +/** + * Hook สำหรับสั่งรัน AI Inference ผ่าน Django DRF Proxy + * Endpoint: POST /api/v1/models/{modelId}/run-inference/ ⬅️ (ใหม่!) + */ +export const useRunInferenceMutation = () => { + const dispatch = useDispatch(); + + return useMutation({ + mutationFn: async ({ formData, modelId }) => { + + // ใช้ Template String เพื่อฝัง modelId เข้าไปใน URL + const response = await axiosClient.post( + `/api/v1/models/${modelId}/run-inference/`, + formData, + { + // ต้องระบุ Content-Type สำหรับการอัปโหลดไฟล์ + headers: { 'Content-Type': 'multipart/form-data' }, + // ตั้งค่า Timeout เผื่อการประมวลผล AI ที่นาน (10 นาที) + timeout: 600000, + } + ); + + return response.data; + }, + onSuccess: () => { + dispatch(addToast({ message: 'เริ่มกระบวนการ Inference แล้ว!', type: 'success' })); + }, + onError: (error) => { + const msg = error.response?.data?.detail || 'การประมวลผลล้มเหลว'; + dispatch(addToast({ message: `Inference Failed: ${msg}`, type: 'error' })); + }, + }); +}; \ No newline at end of file diff --git a/web/src/services/modelRegistryApi.js b/web/src/services/modelRegistryApi.js index 5a46462..cfa689a 100644 --- a/web/src/services/modelRegistryApi.js +++ b/web/src/services/modelRegistryApi.js @@ -113,4 +113,38 @@ export const useUpdateModel = () => { dispatch(addToast({ message: `การแก้ไขล้มเหลว: ${errorMessage}`, type: 'error' })); } }); +}; + +export const useActiveModelList = () => { + // Note: useModelList() จะส่ง data: undefined หรือ error มาเมื่อ 401 + const { data, isLoading, isError } = useModelList(); + + const activeModels = data + // การตรวจสอบ data?. เพื่อความปลอดภัยก่อนเข้าสู่ filter + ?.filter(model => model.status === 'ACTIVE') + ?.map(model => { + + // การตรวจสอบ Optional Chaining (?. ) ก่อนเรียกใช้ properties + // และเพิ่มการตรวจสอบค่าว่าง (|| '') เพื่อป้องกัน undefined/null + const base_url = model.base_url || ''; + const endpoint_path = model.endpoint_path || ''; + + // 1. ลบ / ท้ายสุดของ base_url ออก (ถ้ามี) + const baseUrl = base_url.replace(/\/+$/, ''); + + // 2. ลบ / หน้าสุดของ endpoint_path ออก (ถ้ามี) + // Error: Cannot read properties of undefined (reading 'startsWith') เกิดที่นี่ + const endpointPath = endpoint_path.startsWith('/') + ? endpoint_path.substring(1) + : endpoint_path; + + const fullInferenceUrl = `${baseUrl}/${endpointPath}`; + + return { + ...model, + fullInferenceUrl, + }; + }) || []; // ถ้า data เป็น undefined/null ให้คืนค่า Array ว่าง + + return { data: activeModels, isLoading, isError }; }; \ No newline at end of file