690519:1631 224 to 226 AI #01
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
// File: components/ai/intent-classification/analytics/analytics-summary-cards.tsx
|
||||
// Change Log
|
||||
// - 2026-05-19: สร้าง Summary Cards สำหรับ Analytics Dashboard (T036, US3).
|
||||
|
||||
'use client';
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import type { ClassificationAnalytics } from '@/lib/services/ai-intent.service';
|
||||
|
||||
interface AnalyticsSummaryCardsProps {
|
||||
data: ClassificationAnalytics;
|
||||
}
|
||||
|
||||
/**
|
||||
* แสดงสรุปสถิติหลักในรูปแบบ Cards
|
||||
* Total Requests, Pattern Hit Rate, Avg Confidence, Avg Latency
|
||||
*/
|
||||
export function AnalyticsSummaryCards({ data }: AnalyticsSummaryCardsProps) {
|
||||
const cards = [
|
||||
{
|
||||
title: 'Total Requests',
|
||||
value: data.totalRequests.toLocaleString(),
|
||||
subtitle: `${data.successCount} สำเร็จ / ${data.failedCount} ล้มเหลว`,
|
||||
color: 'text-blue-600',
|
||||
},
|
||||
{
|
||||
title: 'Pattern Hit Rate',
|
||||
value: `${data.patternHitRate}%`,
|
||||
subtitle: 'เป้าหมาย: 70-80%',
|
||||
color: data.patternHitRate >= 70 ? 'text-green-600' : 'text-amber-600',
|
||||
},
|
||||
{
|
||||
title: 'Avg Confidence',
|
||||
value: data.avgConfidence.toFixed(2),
|
||||
subtitle: 'เป้าหมาย: ≥ 0.70',
|
||||
color: data.avgConfidence >= 0.7 ? 'text-green-600' : 'text-amber-600',
|
||||
},
|
||||
{
|
||||
title: 'Avg Latency',
|
||||
value: `${data.avgLatencyMs.toFixed(1)}ms`,
|
||||
subtitle: 'Pattern < 10ms, LLM < 2000ms',
|
||||
color: data.avgLatencyMs < 100 ? 'text-green-600' : 'text-amber-600',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{cards.map((card) => (
|
||||
<Card key={card.title}>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||
{card.title}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className={`text-2xl font-bold ${card.color}`}>
|
||||
{card.value}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{card.subtitle}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// File: components/ai/intent-classification/analytics/intent-breakdown-table.tsx
|
||||
// Change Log
|
||||
// - 2026-05-19: สร้าง Intent Breakdown Table สำหรับ Analytics Dashboard (T036, US3).
|
||||
|
||||
'use client';
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
import type { IntentStats } from '@/lib/services/ai-intent.service';
|
||||
|
||||
interface IntentBreakdownTableProps {
|
||||
data: IntentStats[];
|
||||
}
|
||||
|
||||
/**
|
||||
* ตารางแสดงสถิติแยกตาม intent code พร้อม bar แสดง pattern vs llm
|
||||
*/
|
||||
export function IntentBreakdownTable({ data }: IntentBreakdownTableProps) {
|
||||
if (data.length === 0) {
|
||||
return <p className="text-sm text-muted-foreground">ยังไม่มีข้อมูล</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Intent Code</TableHead>
|
||||
<TableHead className="text-right">Total</TableHead>
|
||||
<TableHead className="text-right">Pattern</TableHead>
|
||||
<TableHead className="text-right">LLM</TableHead>
|
||||
<TableHead className="text-right">Avg Confidence</TableHead>
|
||||
<TableHead className="w-[120px]">Pattern Rate</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data.map((row) => {
|
||||
const patternRate =
|
||||
row.count > 0 ? (row.patternHits / row.count) * 100 : 0;
|
||||
return (
|
||||
<TableRow key={row.intentCode}>
|
||||
<TableCell className="font-mono text-sm">
|
||||
{row.intentCode}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">{row.count}</TableCell>
|
||||
<TableCell className="text-right">{row.patternHits}</TableCell>
|
||||
<TableCell className="text-right">{row.llmHits}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{row.avgConfidence.toFixed(2)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<Progress value={patternRate} className="h-2" />
|
||||
<span className="text-xs text-muted-foreground w-10 text-right">
|
||||
{patternRate.toFixed(0)}%
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// File: components/ai/intent-classification/analytics/method-breakdown-table.tsx
|
||||
// Change Log
|
||||
// - 2026-05-19: สร้าง Method Breakdown Table สำหรับ Analytics Dashboard (T036, US3).
|
||||
|
||||
'use client';
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import type { MethodStats } from '@/lib/services/ai-intent.service';
|
||||
|
||||
interface MethodBreakdownTableProps {
|
||||
data: MethodStats[];
|
||||
}
|
||||
|
||||
/** แปลงชื่อ method เป็น label + สี */
|
||||
function methodBadge(method: string) {
|
||||
switch (method) {
|
||||
case 'pattern':
|
||||
return <Badge variant="default">Pattern Match</Badge>;
|
||||
case 'llm_fallback':
|
||||
return <Badge variant="secondary">LLM Fallback</Badge>;
|
||||
case 'semaphore_overflow':
|
||||
return <Badge variant="destructive">Semaphore Overflow</Badge>;
|
||||
case 'llm_error':
|
||||
return <Badge variant="destructive">LLM Error</Badge>;
|
||||
default:
|
||||
return <Badge variant="outline">{method}</Badge>;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ตารางแสดงสถิติแยกตาม method (pattern, llm_fallback, etc.)
|
||||
*/
|
||||
export function MethodBreakdownTable({ data }: MethodBreakdownTableProps) {
|
||||
if (data.length === 0) {
|
||||
return <p className="text-sm text-muted-foreground">ยังไม่มีข้อมูล</p>;
|
||||
}
|
||||
|
||||
const total = data.reduce((sum, d) => sum + d.count, 0);
|
||||
|
||||
return (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Method</TableHead>
|
||||
<TableHead className="text-right">Count</TableHead>
|
||||
<TableHead className="text-right">%</TableHead>
|
||||
<TableHead className="text-right">Avg Confidence</TableHead>
|
||||
<TableHead className="text-right">Avg Latency</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data.map((row) => (
|
||||
<TableRow key={row.method}>
|
||||
<TableCell>{methodBadge(row.method)}</TableCell>
|
||||
<TableCell className="text-right">{row.count}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{total > 0 ? ((row.count / total) * 100).toFixed(1) : 0}%
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{row.avgConfidence.toFixed(2)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{row.avgLatencyMs.toFixed(1)}ms
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// File: components/ai/intent-classification/analytics/recalibration-panel.tsx
|
||||
// Change Log
|
||||
// - 2026-05-19: สร้าง Recalibration Panel สำหรับ Analytics Dashboard (T036, US3).
|
||||
|
||||
'use client';
|
||||
|
||||
import { AlertTriangle } from 'lucide-react';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import type { RecalibrationRecommendation } from '@/lib/services/ai-intent.service';
|
||||
|
||||
interface RecalibrationPanelProps {
|
||||
data: RecalibrationRecommendation[];
|
||||
}
|
||||
|
||||
/**
|
||||
* แสดงคำแนะนำ Intent ที่ควรเพิ่ม pattern เพื่อลด LLM Calls
|
||||
* ตาม SC-001: เป้าหมาย Pattern Hit Rate 70-80%
|
||||
*/
|
||||
export function RecalibrationPanel({ data }: RecalibrationPanelProps) {
|
||||
if (data.length === 0) {
|
||||
return (
|
||||
<Alert>
|
||||
<AlertTitle>ไม่มีคำแนะนำ</AlertTitle>
|
||||
<AlertDescription>
|
||||
ยังไม่มี Intent ที่ต้องเพิ่ม Pattern — Pattern Hit Rate อยู่ในระดับดี
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<Alert variant="destructive">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertTitle>ควรเพิ่ม Pattern</AlertTitle>
|
||||
<AlertDescription>
|
||||
Intent ด้านล่างถูก classify ด้วย LLM บ่อย — การเพิ่ม keyword/regex pattern
|
||||
จะช่วยลดภาระ LLM และเพิ่มความเร็ว
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Intent Code</TableHead>
|
||||
<TableHead className="text-right">LLM Calls</TableHead>
|
||||
<TableHead className="text-right">Avg Confidence</TableHead>
|
||||
<TableHead className="text-right">Priority</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data.map((row) => (
|
||||
<TableRow key={row.intentCode}>
|
||||
<TableCell className="font-mono text-sm">
|
||||
{row.intentCode}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">{row.llmCallCount}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{row.avgConfidence.toFixed(2)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right font-medium text-amber-600">
|
||||
{row.priority}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
'use client';
|
||||
|
||||
// File: components/ai/intent-classification/classification-result-card.tsx
|
||||
// Change Log
|
||||
// - 2026-05-19: สร้าง Classification Result Card component (ADR-024).
|
||||
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import type { ClassificationResult } from '@/lib/services/ai-intent.service';
|
||||
|
||||
interface ClassificationResultCardProps {
|
||||
query: string;
|
||||
result: ClassificationResult;
|
||||
}
|
||||
|
||||
/** สีของ method badge */
|
||||
const METHOD_COLORS: Record<string, string> = {
|
||||
pattern: 'bg-green-100 text-green-800 border-green-200',
|
||||
llm_fallback: 'bg-blue-100 text-blue-800 border-blue-200',
|
||||
semaphore_overflow: 'bg-yellow-100 text-yellow-800 border-yellow-200',
|
||||
llm_error: 'bg-red-100 text-red-800 border-red-200',
|
||||
};
|
||||
|
||||
/** สีของ confidence bar */
|
||||
function getConfidenceColor(confidence: number): string {
|
||||
if (confidence >= 0.9) return 'bg-green-500';
|
||||
if (confidence >= 0.7) return 'bg-blue-500';
|
||||
if (confidence >= 0.5) return 'bg-yellow-500';
|
||||
return 'bg-red-500';
|
||||
}
|
||||
|
||||
/**
|
||||
* Card แสดงผลลัพธ์การจำแนก Intent
|
||||
* แสดง intentCode, confidence, method, latency
|
||||
*/
|
||||
export function ClassificationResultCard({
|
||||
query,
|
||||
result,
|
||||
}: ClassificationResultCardProps) {
|
||||
const confidencePercent = Math.round(result.confidence * 100);
|
||||
|
||||
return (
|
||||
<Card className="border-l-4 border-l-primary/20">
|
||||
<CardContent className="pt-4 pb-3 space-y-2">
|
||||
{/* Query */}
|
||||
<p className="text-sm font-medium text-muted-foreground">
|
||||
"{query}"
|
||||
</p>
|
||||
|
||||
{/* Intent Code + Method */}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-mono text-base font-semibold">
|
||||
{result.intentCode}
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={METHOD_COLORS[result.method] || ''}
|
||||
>
|
||||
{result.method}
|
||||
</Badge>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{result.latencyMs}ms
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Confidence Bar */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1 h-2 bg-muted rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full rounded-full transition-all ${getConfidenceColor(result.confidence)}`}
|
||||
style={{ width: `${confidencePercent}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs font-mono text-muted-foreground w-10 text-right">
|
||||
{confidencePercent}%
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Params (ถ้ามี) */}
|
||||
{result.params && Object.keys(result.params).length > 0 && (
|
||||
<pre className="text-xs bg-muted p-2 rounded overflow-x-auto">
|
||||
{JSON.stringify(result.params, null, 2)}
|
||||
</pre>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
'use client';
|
||||
|
||||
// File: components/ai/intent-classification/intent-form.tsx
|
||||
// Change Log
|
||||
// - 2026-05-19: สร้าง Intent Definition Form (Create/Update) (ADR-024).
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from '@/components/ui/dialog';
|
||||
import type {
|
||||
IntentDefinition,
|
||||
IntentCategory,
|
||||
} from '@/lib/services/ai-intent.service';
|
||||
|
||||
interface IntentFormProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (data: {
|
||||
intentCode: string;
|
||||
descriptionTh: string;
|
||||
descriptionEn: string;
|
||||
category: IntentCategory;
|
||||
}) => void;
|
||||
/** ถ้ามี = edit mode */
|
||||
initial?: IntentDefinition;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dialog Form สำหรับสร้าง/แก้ไข Intent Definition
|
||||
*/
|
||||
export function IntentForm({
|
||||
open,
|
||||
onClose,
|
||||
onSubmit,
|
||||
initial,
|
||||
isLoading,
|
||||
}: IntentFormProps) {
|
||||
const isEdit = !!initial;
|
||||
const [intentCode, setIntentCode] = useState(initial?.intentCode || '');
|
||||
const [descriptionTh, setDescriptionTh] = useState(initial?.descriptionTh || '');
|
||||
const [descriptionEn, setDescriptionEn] = useState(initial?.descriptionEn || '');
|
||||
const [category, setCategory] = useState<IntentCategory>(initial?.category || 'read');
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
onSubmit({ intentCode, descriptionTh, descriptionEn, category });
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(v) => !v && onClose()}>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{isEdit ? `แก้ไข ${initial.intentCode}` : 'สร้าง Intent ใหม่'}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{/* Intent Code */}
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="intentCode">Intent Code</Label>
|
||||
<Input
|
||||
id="intentCode"
|
||||
value={intentCode}
|
||||
onChange={(e) => setIntentCode(e.target.value.toUpperCase())}
|
||||
placeholder="GET_RFA"
|
||||
pattern="^[A-Z][A-Z0-9_]*$"
|
||||
maxLength={50}
|
||||
disabled={isEdit}
|
||||
required
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
UPPERCASE_SNAKE_CASE เช่น GET_RFA, SUMMARIZE_DOCUMENT
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Description TH */}
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="descriptionTh">คำอธิบาย (ไทย)</Label>
|
||||
<Input
|
||||
id="descriptionTh"
|
||||
value={descriptionTh}
|
||||
onChange={(e) => setDescriptionTh(e.target.value)}
|
||||
placeholder="ดึง RFA ตาม filter"
|
||||
maxLength={255}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Description EN */}
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="descriptionEn">Description (EN)</Label>
|
||||
<Input
|
||||
id="descriptionEn"
|
||||
value={descriptionEn}
|
||||
onChange={(e) => setDescriptionEn(e.target.value)}
|
||||
placeholder="Get RFA by filters"
|
||||
maxLength={255}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Category */}
|
||||
<div className="space-y-1">
|
||||
<Label>Category</Label>
|
||||
<Select
|
||||
value={category}
|
||||
onValueChange={(v) => setCategory(v as IntentCategory)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="read">Read (ดึงข้อมูล)</SelectItem>
|
||||
<SelectItem value="suggest">Suggest (แนะนำ)</SelectItem>
|
||||
<SelectItem value="utility">Utility (อื่น ๆ)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" onClick={onClose}>
|
||||
ยกเลิก
|
||||
</Button>
|
||||
<Button type="submit" disabled={isLoading}>
|
||||
{isEdit ? 'บันทึก' : 'สร้าง'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
'use client';
|
||||
|
||||
// File: components/ai/intent-classification/pattern-form.tsx
|
||||
// Change Log
|
||||
// - 2026-05-19: สร้าง Pattern Form (Create/Update) (ADR-024).
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from '@/components/ui/dialog';
|
||||
import type {
|
||||
IntentPattern,
|
||||
PatternType,
|
||||
PatternLanguage,
|
||||
} from '@/lib/services/ai-intent.service';
|
||||
|
||||
interface PatternFormProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (data: {
|
||||
patternType: PatternType;
|
||||
patternValue: string;
|
||||
language?: PatternLanguage;
|
||||
priority?: number;
|
||||
}) => void;
|
||||
/** ถ้ามี = edit mode */
|
||||
initial?: IntentPattern;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dialog Form สำหรับสร้าง/แก้ไข Intent Pattern
|
||||
*/
|
||||
export function PatternForm({
|
||||
open,
|
||||
onClose,
|
||||
onSubmit,
|
||||
initial,
|
||||
isLoading,
|
||||
}: PatternFormProps) {
|
||||
const isEdit = !!initial;
|
||||
const [patternType, setPatternType] = useState<PatternType>(
|
||||
initial?.patternType || 'keyword'
|
||||
);
|
||||
const [patternValue, setPatternValue] = useState(initial?.patternValue || '');
|
||||
const [language, setLanguage] = useState<PatternLanguage>(
|
||||
initial?.language || 'any'
|
||||
);
|
||||
const [priority, setPriority] = useState<number>(initial?.priority || 100);
|
||||
const [regexError, setRegexError] = useState<string | null>(null);
|
||||
|
||||
/** Validate regex ใน frontend ก่อนส่ง */
|
||||
const validateRegex = (value: string): boolean => {
|
||||
if (patternType !== 'regex') return true;
|
||||
try {
|
||||
new RegExp(value);
|
||||
setRegexError(null);
|
||||
return true;
|
||||
} catch (err) {
|
||||
setRegexError(err instanceof Error ? err.message : 'Invalid regex');
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!validateRegex(patternValue)) return;
|
||||
onSubmit({ patternType, patternValue, language, priority });
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(v) => !v && onClose()}>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{isEdit ? 'แก้ไข Pattern' : 'เพิ่ม Pattern ใหม่'}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{/* Pattern Type */}
|
||||
<div className="space-y-1">
|
||||
<Label>ชนิด Pattern</Label>
|
||||
<Select
|
||||
value={patternType}
|
||||
onValueChange={(v) => {
|
||||
setPatternType(v as PatternType);
|
||||
setRegexError(null);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="keyword">Keyword (includes)</SelectItem>
|
||||
<SelectItem value="regex">Regex (RegExp)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Pattern Value */}
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="patternValue">
|
||||
ค่า Pattern {patternType === 'regex' && '(Regular Expression)'}
|
||||
</Label>
|
||||
<Input
|
||||
id="patternValue"
|
||||
value={patternValue}
|
||||
onChange={(e) => {
|
||||
setPatternValue(e.target.value);
|
||||
if (patternType === 'regex') validateRegex(e.target.value);
|
||||
}}
|
||||
placeholder={
|
||||
patternType === 'keyword'
|
||||
? 'สรุป, drawing, rfa'
|
||||
: '\\brfa\\b'
|
||||
}
|
||||
maxLength={255}
|
||||
required
|
||||
/>
|
||||
{regexError && (
|
||||
<p className="text-xs text-destructive">{regexError}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Language */}
|
||||
<div className="space-y-1">
|
||||
<Label>ภาษา</Label>
|
||||
<Select
|
||||
value={language}
|
||||
onValueChange={(v) => setLanguage(v as PatternLanguage)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="any">Any (ทุกภาษา)</SelectItem>
|
||||
<SelectItem value="th">Thai (ภาษาไทย)</SelectItem>
|
||||
<SelectItem value="en">English (ภาษาอังกฤษ)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Priority */}
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="priority">Priority (ต่ำ = สำคัญกว่า)</Label>
|
||||
<Input
|
||||
id="priority"
|
||||
type="number"
|
||||
value={priority}
|
||||
onChange={(e) => setPriority(Number(e.target.value))}
|
||||
min={1}
|
||||
max={9999}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" onClick={onClose}>
|
||||
ยกเลิก
|
||||
</Button>
|
||||
<Button type="submit" disabled={isLoading || !!regexError}>
|
||||
{isEdit ? 'บันทึก' : 'เพิ่ม'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
'use client';
|
||||
|
||||
// File: components/ai/intent-classification/test-console-panel.tsx
|
||||
// Change Log
|
||||
// - 2026-05-19: สร้าง Test Console Panel สำหรับทดสอบ Intent Classification (ADR-024).
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useClassifyIntent } from '@/hooks/ai/use-intent-classification';
|
||||
import { ClassificationResultCard } from './classification-result-card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Loader2, Send } from 'lucide-react';
|
||||
import type { ClassificationResult } from '@/lib/services/ai-intent.service';
|
||||
|
||||
/**
|
||||
* Test Console Panel — Admin/Developer ทดสอบ classification แบบ real-time
|
||||
*/
|
||||
export function TestConsolePanel() {
|
||||
const [query, setQuery] = useState('');
|
||||
const [results, setResults] = useState<
|
||||
Array<{ query: string; result: ClassificationResult; timestamp: Date }>
|
||||
>([]);
|
||||
|
||||
const classifyMutation = useClassifyIntent();
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const trimmed = query.trim();
|
||||
if (!trimmed) return;
|
||||
|
||||
try {
|
||||
const result = await classifyMutation.mutateAsync({ query: trimmed });
|
||||
setResults((prev) => [
|
||||
{ query: trimmed, result, timestamp: new Date() },
|
||||
...prev,
|
||||
]);
|
||||
setQuery('');
|
||||
} catch {
|
||||
// Error state จัดการโดย TanStack Query (classifyMutation.isError)
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Send className="h-5 w-5" />
|
||||
Test Console
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* Input Form */}
|
||||
<form onSubmit={handleSubmit} className="flex gap-2">
|
||||
<Input
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="พิมพ์คำถามเพื่อทดสอบ เช่น 'สรุปเอกสารนี้' หรือ 'show me RFA'"
|
||||
maxLength={200}
|
||||
disabled={classifyMutation.isPending}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={!query.trim() || classifyMutation.isPending}
|
||||
>
|
||||
{classifyMutation.isPending ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Send className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
{/* Error Display */}
|
||||
{classifyMutation.isError && (
|
||||
<p className="text-sm text-destructive">
|
||||
เกิดข้อผิดพลาด: ไม่สามารถเชื่อมต่อ AI ได้ กรุณาลองใหม่
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Results List */}
|
||||
<div className="space-y-3 max-h-[60vh] overflow-y-auto">
|
||||
{results.length === 0 && (
|
||||
<p className="text-muted-foreground text-sm text-center py-8">
|
||||
พิมพ์คำถามด้านบนเพื่อทดสอบ Intent Classification
|
||||
</p>
|
||||
)}
|
||||
{results.map((item, idx) => (
|
||||
<ClassificationResultCard
|
||||
key={`${item.timestamp.getTime()}-${idx}`}
|
||||
query={item.query}
|
||||
result={item.result}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user