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 (
+
+ );
+}
\ 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