// File: frontend/components/admin/ai/RuntimeParametersPanel.tsx // Change Log: // - 2026-06-14: Created RuntimeParametersPanel component for managing sandbox parameters (conforming to task T048) import React, { useState, useEffect, useCallback } from 'react'; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Label } from '@/components/ui/label'; import { Input } from '@/components/ui/input'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { adminAiService, SandboxProfileParams } from '@/lib/services/admin-ai.service'; import { toast } from 'sonner'; import { Save, RefreshCw, CheckCircle, Sliders } from 'lucide-react'; import { v7 as uuidv7 } from 'uuid'; interface RuntimeParametersPanelProps { onProfileChange?: (params: SandboxProfileParams) => void; } const PROFILE_OPTIONS = [ { value: 'standard', label: 'มาตรฐาน (Standard)' }, { value: 'quality', label: 'คุณภาพสูง (Quality)' }, { value: 'interactive', label: 'โต้ตอบเร็ว (Interactive)' }, { value: 'deep-analysis', label: 'วิเคราะห์เชิงลึก (Deep Analysis)' }, { value: 'ocr-extract', label: 'สกัด OCR (OCR Extract)' }, ]; export default function RuntimeParametersPanel({ onProfileChange }: RuntimeParametersPanelProps) { const [selectedProfile, setSelectedProfile] = useState('standard'); const [params, setParams] = useState(null); const [isLoading, setIsLoading] = useState(false); const [isSaving, setIsSaving] = useState(false); const [isResetting, setIsResetting] = useState(false); const [isApplying, setIsApplying] = useState(false); const fetchProfileParams = useCallback(async (profileName: string) => { setIsLoading(true); try { const data = await adminAiService.getSandboxProfile(profileName); setParams(data); if (onProfileChange) { onProfileChange(data); } } catch (_err) { toast.error('ไม่สามารถดึงค่าพารามิเตอร์ Sandbox ได้'); } finally { setIsLoading(false); } }, [onProfileChange]); useEffect(() => { fetchProfileParams(selectedProfile); }, [selectedProfile, fetchProfileParams]); const handleSliderChange = (field: keyof SandboxProfileParams, val: number) => { if (!params) return; setParams({ ...params, [field]: val, }); }; const handleInputChange = (field: keyof SandboxProfileParams, val: string) => { if (!params) return; const parsed = val === '' ? null : Number(val); setParams({ ...params, [field]: parsed, }); }; const handleSaveDraft = async () => { if (!params) return; setIsSaving(true); try { const key = uuidv7(); const res = await adminAiService.saveSandboxProfile(selectedProfile, params, key); setParams(res); toast.success('บันทึกแบบร่าง Sandbox สำเร็จ'); if (onProfileChange) { onProfileChange(res); } } catch (_err) { toast.error('ไม่สามารถบันทึกแบบร่างได้'); } finally { setIsSaving(false); } }; const handleResetDraft = async () => { setIsResetting(true); try { const res = await adminAiService.resetSandboxProfile(selectedProfile); setParams(res); toast.success('รีเซ็ตแบบร่างเป็นค่าเริ่มต้นแล้ว'); if (onProfileChange) { onProfileChange(res); } } catch (_err) { toast.error('ไม่สามารถรีเซ็ตแบบร่างได้'); } finally { setIsResetting(false); } }; const handleApplyToProduction = async () => { setIsApplying(true); try { const key = uuidv7(); await adminAiService.applyProfile(selectedProfile, key); toast.success('ปรับใช้พารามิเตอร์จริงสำเร็จ'); } catch (_err) { toast.error('ไม่สามารถปรับใช้พารามิเตอร์จริงได้'); } finally { setIsApplying(false); } }; if (isLoading || !params) { return (
กำลังโหลดพารามิเตอร์...
); } return (
จัดการพารามิเตอร์รันไทม์ (Runtime Parameters) ปรับเปลี่ยนพารามิเตอร์การทำงานของโมเดล AI ในระบบทดสอบ Sandbox
{params.temperature.toFixed(2)}
handleSliderChange('temperature', Number(e.target.value))} className="w-full h-1.5 bg-secondary rounded-lg appearance-none cursor-pointer accent-primary" />

ค่ายิ่งสูงโมเดลยิ่งตอบอย่างอิสระและมีความคิดสร้างสรรค์ (Temperature สูงเหมาะกับการเขียน) ค่ายิ่งต่ำยิ่งมั่นใจในความถูกต้อง (Temperature ต่ำเหมาะกับการสกัดข้อความ)

{params.topP.toFixed(2)}
handleSliderChange('topP', Number(e.target.value))} className="w-full h-1.5 bg-secondary rounded-lg appearance-none cursor-pointer accent-primary" />

กำหนดขอบเขตของคำที่เป็นไปได้ในการเลือกคำถัดไป แนะนำให้ตั้งไว้ที่ 0.8 - 0.95

{params.repeatPenalty.toFixed(2)}
handleSliderChange('repeatPenalty', Number(e.target.value))} className="w-full h-1.5 bg-secondary rounded-lg appearance-none cursor-pointer accent-primary" />

ลดโอกาสที่โมเดลจะสร้างคำที่เคยพูดไปแล้วซ้ำๆ ค่ายิ่งสูงยิ่งช่วยลดปัญหาคำซ้ำ

handleInputChange('maxTokens', e.target.value)} className="bg-background/50 border-border/50 h-8 text-xs font-mono" />
handleInputChange('numCtx', e.target.value)} className="bg-background/50 border-border/50 h-8 text-xs font-mono" />
handleInputChange('keepAliveSeconds', e.target.value)} className="bg-background/50 border-border/50 h-8 text-xs font-mono" />

ระยะเวลาที่โมเดลจะค้างอยู่ใน VRAM หลังจากสิ้นสุดการขอข้อมูลก่อนระบบจะเคลียร์ VRAM

{params.canonicalModel} ระบบเปลี่ยนให้อัตโนมัติ
); }