690519:1631 224 to 226 AI #01
CI / CD Pipeline / build (push) Failing after 3m57s
CI / CD Pipeline / deploy (push) Has been skipped

This commit is contained in:
2026-05-19 16:31:50 +07:00
parent 3e25097470
commit ea5499123e
127 changed files with 12387 additions and 42 deletions
@@ -0,0 +1,178 @@
'use client';
// File: app/(admin)/admin/ai/intent-classification/[intentCode]/page.tsx
// Change Log
// - 2026-05-19: สร้างหน้า Intent Detail + Patterns (Admin, ADR-024).
import { useState } from 'react';
import { useParams, useRouter } from 'next/navigation';
import {
useIntentDefinition,
useUpdateIntentDefinition,
useIntentPatterns,
useCreateIntentPattern,
useDeleteIntentPattern,
} from '@/hooks/ai/use-intent-classification';
import { PatternForm } from '@/components/ai/intent-classification/pattern-form';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Switch } from '@/components/ui/switch';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { ArrowLeft, Plus, Trash2 } from 'lucide-react';
import type { PatternType, PatternLanguage } from '@/lib/services/ai-intent.service';
export default function IntentDetailPage() {
const params = useParams();
const router = useRouter();
const intentCode = params.intentCode as string;
const [showPatternForm, setShowPatternForm] = useState(false);
const { data: definition, isLoading: defLoading } = useIntentDefinition(intentCode);
const updateMutation = useUpdateIntentDefinition(intentCode);
const { data: patterns, isLoading: patternsLoading } = useIntentPatterns(intentCode);
const createPatternMutation = useCreateIntentPattern(intentCode);
const deletePatternMutation = useDeleteIntentPattern(intentCode);
const handleToggleActive = async (isActive: boolean) => {
await updateMutation.mutateAsync({ isActive });
};
const handleCreatePattern = async (data: {
patternType: PatternType;
patternValue: string;
language?: PatternLanguage;
priority?: number;
}) => {
await createPatternMutation.mutateAsync(data);
setShowPatternForm(false);
};
const handleDeletePattern = async (publicId: string) => {
if (!confirm('ต้องการลบ Pattern นี้?')) return;
await deletePatternMutation.mutateAsync(publicId);
};
if (defLoading) {
return <p className="text-center py-8 text-muted-foreground">...</p>;
}
if (!definition) {
return <p className="text-center py-8 text-destructive"> Intent: {intentCode}</p>;
}
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center gap-4">
<Button
variant="ghost"
size="icon"
onClick={() => router.push('/admin/ai/intent-classification')}
>
<ArrowLeft className="h-5 w-5" />
</Button>
<div className="flex-1">
<h1 className="text-2xl font-bold font-mono">{definition.intentCode}</h1>
<p className="text-muted-foreground">{definition.descriptionTh}</p>
<p className="text-sm text-muted-foreground">{definition.descriptionEn}</p>
</div>
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">Active</span>
<Switch
checked={definition.isActive}
onCheckedChange={handleToggleActive}
/>
</div>
</div>
{/* Info Card */}
<Card>
<CardContent className="pt-4 flex gap-4">
<Badge variant="secondary">{definition.category}</Badge>
<span className="text-sm text-muted-foreground">
: {new Date(definition.createdAt).toLocaleString('th-TH')}
</span>
</CardContent>
</Card>
{/* Patterns Table */}
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle>Patterns ({patterns?.length || 0})</CardTitle>
<Button size="sm" onClick={() => setShowPatternForm(true)}>
<Plus className="h-4 w-4 mr-1" />
Pattern
</Button>
</CardHeader>
<CardContent>
{patternsLoading ? (
<p className="text-center text-muted-foreground py-4">...</p>
) : patterns && patterns.length > 0 ? (
<Table>
<TableHeader>
<TableRow>
<TableHead>Type</TableHead>
<TableHead>Pattern Value</TableHead>
<TableHead>Language</TableHead>
<TableHead>Priority</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{patterns.map((p) => (
<TableRow key={p.publicId}>
<TableCell>
<Badge variant="outline">{p.patternType}</Badge>
</TableCell>
<TableCell className="font-mono text-sm max-w-[200px] truncate">
{p.patternValue}
</TableCell>
<TableCell>{p.language}</TableCell>
<TableCell>{p.priority}</TableCell>
<TableCell>
<Badge variant={p.isActive ? 'default' : 'destructive'}>
{p.isActive ? 'Active' : 'Inactive'}
</Badge>
</TableCell>
<TableCell>
<Button
variant="ghost"
size="icon"
onClick={() => handleDeletePattern(p.publicId)}
disabled={deletePatternMutation.isPending}
>
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
) : (
<p className="text-center text-muted-foreground py-4">
Pattern Pattern Matching
</p>
)}
</CardContent>
</Card>
{/* Create Pattern Form Dialog */}
<PatternForm
open={showPatternForm}
onClose={() => setShowPatternForm(false)}
onSubmit={handleCreatePattern}
isLoading={createPatternMutation.isPending}
/>
</div>
);
}
@@ -0,0 +1,96 @@
// File: app/(admin)/admin/ai/intent-classification/analytics/page.tsx
// Change Log
// - 2026-05-19: สร้างหน้า Analytics Dashboard สำหรับ Intent Classification (T037, US3).
'use client';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Skeleton } from '@/components/ui/skeleton';
import { useIntentAnalytics } from '@/hooks/ai/use-intent-classification';
import { AnalyticsSummaryCards } from '@/components/ai/intent-classification/analytics/analytics-summary-cards';
import { MethodBreakdownTable } from '@/components/ai/intent-classification/analytics/method-breakdown-table';
import { IntentBreakdownTable } from '@/components/ai/intent-classification/analytics/intent-breakdown-table';
import { RecalibrationPanel } from '@/components/ai/intent-classification/analytics/recalibration-panel';
/**
* หน้า Analytics Dashboard สำหรับ Intent Classification
* แสดง Summary Cards, Method Breakdown, Intent Breakdown, Recalibration
*/
export default function IntentAnalyticsPage() {
const { data, isLoading, isError, error } = useIntentAnalytics();
if (isLoading) {
return (
<div className="space-y-6 p-6">
<h1 className="text-2xl font-bold">Intent Classification Analytics</h1>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
{Array.from({ length: 4 }).map((_, i) => (
<Skeleton key={i} className="h-[120px]" />
))}
</div>
<Skeleton className="h-[300px]" />
</div>
);
}
if (isError) {
return (
<div className="p-6">
<h1 className="text-2xl font-bold mb-4">Intent Classification Analytics</h1>
<Card>
<CardContent className="p-6">
<p className="text-destructive">
: {error instanceof Error ? error.message : 'ไม่สามารถโหลดข้อมูลได้'}
</p>
</CardContent>
</Card>
</div>
);
}
if (!data) {
return null;
}
return (
<div className="space-y-6 p-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold">Intent Classification Analytics</h1>
<p className="text-sm text-muted-foreground"> 30 </p>
</div>
{/* Summary Cards */}
<AnalyticsSummaryCards data={data} />
{/* Method Breakdown */}
<Card>
<CardHeader>
<CardTitle>Classification Method Breakdown</CardTitle>
</CardHeader>
<CardContent>
<MethodBreakdownTable data={data.byMethod} />
</CardContent>
</Card>
{/* Intent Breakdown */}
<Card>
<CardHeader>
<CardTitle>Intent Code Breakdown</CardTitle>
</CardHeader>
<CardContent>
<IntentBreakdownTable data={data.byIntent} />
</CardContent>
</Card>
{/* Recalibration */}
<Card>
<CardHeader>
<CardTitle>Recalibration Recommendations</CardTitle>
</CardHeader>
<CardContent>
<RecalibrationPanel data={data.recalibration} />
</CardContent>
</Card>
</div>
);
}
@@ -0,0 +1,151 @@
'use client';
// File: app/(admin)/admin/ai/intent-classification/page.tsx
// Change Log
// - 2026-05-19: สร้างหน้า Intent Definitions List (Admin, ADR-024).
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import {
useIntentDefinitions,
useCreateIntentDefinition,
} from '@/hooks/ai/use-intent-classification';
import { IntentForm } from '@/components/ai/intent-classification/intent-form';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { Plus, Brain, TestTube } from 'lucide-react';
import type { IntentCategory } from '@/lib/services/ai-intent.service';
/** สีของ category badge */
const CATEGORY_COLORS: Record<IntentCategory, string> = {
read: 'bg-blue-100 text-blue-800',
suggest: 'bg-purple-100 text-purple-800',
utility: 'bg-gray-100 text-gray-800',
};
export default function IntentClassificationPage() {
const router = useRouter();
const [showForm, setShowForm] = useState(false);
const { data: definitions, isLoading } = useIntentDefinitions();
const createMutation = useCreateIntentDefinition();
const handleCreate = async (data: {
intentCode: string;
descriptionTh: string;
descriptionEn: string;
category: IntentCategory;
}) => {
await createMutation.mutateAsync(data);
setShowForm(false);
};
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold flex items-center gap-2">
<Brain className="h-6 w-6" />
Intent Classification
</h1>
<p className="text-muted-foreground mt-1">
Intent Definitions Patterns AI Chat
</p>
</div>
<div className="flex gap-2">
<Button
variant="outline"
onClick={() => router.push('/admin/ai/intent-classification/test-console')}
>
<TestTube className="h-4 w-4 mr-2" />
Test Console
</Button>
<Button onClick={() => setShowForm(true)}>
<Plus className="h-4 w-4 mr-2" />
Intent
</Button>
</div>
</div>
{/* Table */}
<Card>
<CardHeader>
<CardTitle>Intent Definitions ({definitions?.length || 0})</CardTitle>
</CardHeader>
<CardContent>
{isLoading ? (
<p className="text-center text-muted-foreground py-8">...</p>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>Intent Code</TableHead>
<TableHead></TableHead>
<TableHead>Category</TableHead>
<TableHead></TableHead>
<TableHead>Patterns</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{definitions?.map((def) => (
<TableRow
key={def.publicId}
className="cursor-pointer hover:bg-muted/50"
onClick={() =>
router.push(
`/admin/ai/intent-classification/${def.intentCode}`
)
}
>
<TableCell className="font-mono font-medium">
{def.intentCode}
</TableCell>
<TableCell>
<div className="text-sm">{def.descriptionTh}</div>
<div className="text-xs text-muted-foreground">
{def.descriptionEn}
</div>
</TableCell>
<TableCell>
<Badge
variant="secondary"
className={CATEGORY_COLORS[def.category]}
>
{def.category}
</Badge>
</TableCell>
<TableCell>
<Badge variant={def.isActive ? 'default' : 'destructive'}>
{def.isActive ? 'Active' : 'Inactive'}
</Badge>
</TableCell>
<TableCell className="text-center">
{def.patterns?.length || 0}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</CardContent>
</Card>
{/* Create Form Dialog */}
<IntentForm
open={showForm}
onClose={() => setShowForm(false)}
onSubmit={handleCreate}
isLoading={createMutation.isPending}
/>
</div>
);
}
@@ -0,0 +1,38 @@
'use client';
// File: app/(admin)/admin/ai/intent-classification/test-console/page.tsx
// Change Log
// - 2026-05-19: สร้างหน้า Test Console สำหรับทดสอบ Intent Classification (ADR-024).
import { useRouter } from 'next/navigation';
import { TestConsolePanel } from '@/components/ai/intent-classification/test-console-panel';
import { Button } from '@/components/ui/button';
import { ArrowLeft } from 'lucide-react';
export default function TestConsolePage() {
const router = useRouter();
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center gap-4">
<Button
variant="ghost"
size="icon"
onClick={() => router.push('/admin/ai/intent-classification')}
>
<ArrowLeft className="h-5 w-5" />
</Button>
<div>
<h1 className="text-2xl font-bold">Intent Test Console</h1>
<p className="text-muted-foreground">
Intent Classification Real-time
</p>
</div>
</div>
{/* Test Console */}
<TestConsolePanel />
</div>
);
}
@@ -18,6 +18,8 @@ import { contractDrawingService } from '@/lib/services/contract-drawing.service'
import { shopDrawingService } from '@/lib/services/shop-drawing.service';
import { asBuiltDrawingService } from '@/lib/services/asbuilt-drawing.service';
import { useUpdateContractDrawing, useUploadRevision } from '@/hooks/use-drawing';
import { AiChatToggle } from '@/components/ai/ai-chat-toggle';
import { AiChatPanel } from '@/components/ai/ai-chat-panel';
type DrawingType = 'CONTRACT' | 'SHOP' | 'AS_BUILT';
@@ -78,6 +80,7 @@ export default function DrawingDetailPage({ params }: { params: Promise<{ uuid:
const searchParams = useSearchParams();
const isEditMode = searchParams.get('edit') === 'true';
const isUploadMode = searchParams.get('upload') === 'true';
const [isChatOpen, setIsChatOpen] = useState(false);
const { data: drawing, isLoading } = useQuery({
queryKey: ['drawing-detail', uuid],
@@ -120,7 +123,8 @@ export default function DrawingDetailPage({ params }: { params: Promise<{ uuid:
const revisions = drawing.revisions || [];
return (
<div className="space-y-6">
<div className={`relative transition-all duration-300 ${isChatOpen ? 'lg:pr-[400px]' : ''}`}>
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
@@ -232,6 +236,14 @@ export default function DrawingDetailPage({ params }: { params: Promise<{ uuid:
</div>
</div>
)}
</div>
<AiChatToggle isOpen={isChatOpen} onClick={() => setIsChatOpen(!isChatOpen)} />
<AiChatPanel
context={{ type: 'drawing', publicId: uuid }}
isOpen={isChatOpen}
onClose={() => setIsChatOpen(false)}
onToggle={() => setIsChatOpen((prev) => !prev)}
/>
</div>
);
}
+13 -1
View File
@@ -12,6 +12,8 @@ import { Loader2 } from 'lucide-react';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import type { RFA } from '@/types/rfa';
import type { WorkflowAttachmentSummary } from '@/types/workflow';
import { AiChatToggle } from '@/components/ai/ai-chat-toggle';
import { AiChatPanel } from '@/components/ai/ai-chat-panel';
export default function RFADetailPage() {
const { uuid } = useParams();
@@ -32,6 +34,7 @@ export default function RFADetailPage() {
const [pendingAttachmentIds, setPendingAttachmentIds] = useState<string[]>([]);
// ADR-021 T041: ติดตาม publicIds ที่ Storage แจ้ง 404
const [unavailableIds, setUnavailableIds] = useState<string[]>([]);
const [isChatOpen, setIsChatOpen] = useState(false);
const handleUnavailable = (publicId: string) =>
setUnavailableIds((prev) => [...new Set([...prev, publicId])]);
@@ -56,7 +59,8 @@ export default function RFADetailPage() {
const status = currentRevision?.statusCode?.statusCode ?? '';
return (
<div className="space-y-4">
<div className={`relative transition-all duration-300 ${isChatOpen ? 'lg:pr-[400px]' : ''}`}>
<div className="space-y-4">
{/* ADR-021: Integrated Banner — เลขเอกสาร + สถานะ + ปุ่ม Action */}
<IntegratedBanner
docNo={docNo}
@@ -101,6 +105,14 @@ export default function RFADetailPage() {
onUnavailable={handleUnavailable}
/>
</WorkflowErrorBoundary>
</div>
<AiChatToggle isOpen={isChatOpen} onClick={() => setIsChatOpen(!isChatOpen)} />
<AiChatPanel
context={{ type: 'rfa', publicId: uuidStr }}
isOpen={isChatOpen}
onClose={() => setIsChatOpen(false)}
onToggle={() => setIsChatOpen((prev) => !prev)}
/>
</div>
);
}
+48
View File
@@ -0,0 +1,48 @@
// File: frontend/app/api/ai/chat/route.ts
// Change Log:
// - 2026-05-19: สร้าง API Proxy สำหรับ AI Document Chat
import { NextRequest, NextResponse } from 'next/server';
import { auth } from '@/lib/auth';
export async function POST(req: NextRequest) {
const session = await auth();
if (!session || !session.accessToken) {
return NextResponse.json({ error: { message: 'ไม่มีสิทธิ์เข้าถึงระบบ' } }, { status: 401 });
}
try {
const body = await req.json();
const { query, context } = body;
if (!query || !context || !context.type || !context.publicId) {
return NextResponse.json({ error: { message: 'ข้อมูลนำเข้าไม่ถูกต้อง' } }, { status: 400 });
}
const backendUrl = (process.env.INTERNAL_API_URL || process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001/api') + '/ai/chat';
const response = await fetch(backendUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${session.accessToken}`,
},
body: JSON.stringify({ query, context }),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
return NextResponse.json(errorData, { status: response.status });
}
const data = await response.json();
return NextResponse.json(data);
} catch (_error) {
return NextResponse.json(
{
error: {
type: 'INTERNAL_ERROR',
code: 'PROXY_ERROR',
message: 'เกิดข้อผิดพลาดในการประมวลผลคำขอ',
severity: 'HIGH',
timestamp: new Date().toISOString(),
},
},
{ status: 500 }
);
}
}