เพิ่ม AI Inference (Run) ที่ Frontend

This commit is contained in:
Flook 2025-11-13 14:53:45 +07:00
parent 7686d89fe0
commit 2eba6a099e
7 changed files with 237 additions and 0 deletions

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View File

@ -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: '*',

View 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' }));
},
});
};

View File

@ -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 };
};