เพิ่ม AI Inference (Run) ที่ Frontend
This commit is contained in:
parent
7686d89fe0
commit
2eba6a099e
34
web/src/components/FileUploadAndSubmit.jsx
Normal file
34
web/src/components/FileUploadAndSubmit.jsx
Normal file
@ -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 (
|
||||
<form onSubmit={onSubmit} className="space-y-4">
|
||||
{/* File Upload */}
|
||||
<div>
|
||||
<label className="label">
|
||||
<span className="label-text">อัปโหลดไฟล์ DICOM / NIfTI</span>
|
||||
</label>
|
||||
<input
|
||||
type="file"
|
||||
className="file-input file-input-bordered w-full"
|
||||
accept=".nii,.nii.gz,.zip,.dcm"
|
||||
onChange={(e) => setFile(e.target.files[0])}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Submit */}
|
||||
<button
|
||||
type="submit"
|
||||
className={`btn btn-primary w-full ${isPending ? 'loading' : ''}`}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
{isPending ? 'กำลังประมวลผล...' : 'เริ่มประมวลผล'}
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
28
web/src/components/ModelSelector.jsx
Normal file
28
web/src/components/ModelSelector.jsx
Normal file
@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function ModelSelector({ activeModels, isLoading, selectedModelId, setSelectedModelId, isPending }) {
|
||||
return (
|
||||
<div>
|
||||
<label className="label">
|
||||
<span className="label-text">เลือก Model</span>
|
||||
</label>
|
||||
<select
|
||||
className="select select-bordered w-full"
|
||||
value={selectedModelId}
|
||||
onChange={(e) => setSelectedModelId(e.target.value)}
|
||||
disabled={isLoading || isPending}
|
||||
>
|
||||
<option value="">-- เลือก Model --</option>
|
||||
{isLoading ? (
|
||||
<option disabled>กำลังโหลด...</option>
|
||||
) : (
|
||||
activeModels?.map(model => (
|
||||
<option key={model.id} value={model.id}>
|
||||
{model.name} (v{model.model_version})
|
||||
</option>
|
||||
))
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
25
web/src/components/ResultDisplay.jsx
Normal file
25
web/src/components/ResultDisplay.jsx
Normal file
@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function ResultDisplay({ result }) {
|
||||
// หากไม่มีผลลัพธ์ (result เป็น null) ก็ไม่ต้องแสดงผลอะไร
|
||||
if (!result) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-6 alert alert-success shadow-lg">
|
||||
<div>
|
||||
<h3 className="font-bold">Inference Success!</h3>
|
||||
<p className="text-sm">ได้รับผลลัพธ์จากเซิร์ฟเวอร์แล้ว (JSON Payload)</p>
|
||||
</div>
|
||||
|
||||
{/* แสดงผลลัพธ์ JSON ในรูปแบบที่อ่านง่าย */}
|
||||
<pre className="text-sm mt-2 bg-base-100 p-2 rounded max-h-60 overflow-auto border border-base-300
|
||||
text-base-content whitespace-pre-wrap break-words">
|
||||
{/* text-base-content: เพื่อให้สีตัวอักษรเป็นสีพื้นฐานของ Theme (ไม่ใช่สีขาว) */}
|
||||
{/* whitespace-pre-wrap: บังคับให้ข้อความขึ้นบรรทัดใหม่แม้จะอยู่ใน <pre> */}
|
||||
{JSON.stringify(result, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
71
web/src/pages/data/InferenceRun.jsx
Normal file
71
web/src/pages/data/InferenceRun.jsx
Normal file
@ -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 (
|
||||
<div className="p-6 max-w-3xl mx-auto">
|
||||
<h2 className="text-2xl font-bold mb-4">AI Inference (Run)</h2>
|
||||
<p className="mb-6 text-gray-500">เลือก Model ที่ต้องการ และอัปโหลดไฟล์เพื่อสั่งรันการประมวลผล</p>
|
||||
|
||||
{/* Component 1: ตัวเลือก Model */}
|
||||
<ModelSelector
|
||||
activeModels={activeModels}
|
||||
isLoading={isLoading}
|
||||
selectedModelId={selectedModelId}
|
||||
setSelectedModelId={setSelectedModelId}
|
||||
isPending={isPending}
|
||||
/>
|
||||
|
||||
{/* Component 2: อัปโหลดไฟล์และปุ่ม Submit */}
|
||||
<FileUploadAndSubmit
|
||||
file={file}
|
||||
setFile={setFile}
|
||||
isPending={isPending}
|
||||
onSubmit={handleSubmit}
|
||||
selectedModelId={selectedModelId}
|
||||
/>
|
||||
|
||||
{/* Component 3: แสดงผลลัพธ์ */}
|
||||
<ResultDisplay result={result} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -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: <ModelRegistry />,
|
||||
},
|
||||
// --- AI Inference (Run) ---
|
||||
{
|
||||
// Path: /dashboard/inference-run
|
||||
path: 'inference-run',
|
||||
element: <InferenceRun />,
|
||||
},
|
||||
// Fallback สำหรับเส้นทางที่ไม่ตรงกับเมนูใดๆ
|
||||
{
|
||||
path: '*',
|
||||
|
||||
38
web/src/services/inferenceApi.js
Normal file
38
web/src/services/inferenceApi.js
Normal file
@ -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' }));
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -114,3 +114,37 @@ export const useUpdateModel = () => {
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
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 };
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user