Files
edr-platform/apps/edr-freight-web/backoffice/src/pages/ai/AiBookingMockTestPage.tsx

222 lines
6.6 KiB
TypeScript

import { useState } from "react";
import {
Alert,
Badge,
Button,
Card,
Code,
Group,
Stack,
Table,
Text,
Textarea,
Title,
} from "@mantine/core";
import axios from "axios";
import {
AiBookingExtractResult,
extractBookingFromText,
} from "@/services/ai.service";
const EXAMPLE_TEXT =
"Book 2x40ft containers from Djibouti to Indode. Cargo electronics. Customer ABC Logistics.";
const formatValue = (value: string | number | boolean | null): string => {
if (value === null) return "—";
if (typeof value === "boolean") return value ? "Yes" : "No";
return String(value);
};
const EXTRACTED_FIELD_LABELS: Array<{
key: keyof AiBookingExtractResult["extracted"];
label: string;
}> = [
{ key: "customerName", label: "Customer Name" },
{ key: "origin", label: "Origin" },
{ key: "destination", label: "Destination" },
{ key: "cargoType", label: "Cargo Type" },
{ key: "containerType", label: "Container Type" },
{ key: "quantity", label: "Quantity" },
{ key: "direction", label: "Direction" },
{ key: "weightKg", label: "Weight (kg)" },
{ key: "pickupRequired", label: "Pickup Required" },
{ key: "deliveryRequired", label: "Delivery Required" },
];
export default function AiBookingMockTestPage() {
const [text, setText] = useState("");
const [loading, setLoading] = useState(false);
const [result, setResult] = useState<AiBookingExtractResult | null>(null);
const [error, setError] = useState<string | null>(null);
const [showRawJson, setShowRawJson] = useState(false);
const handleTest = async () => {
setLoading(true);
setError(null);
setResult(null);
try {
setResult(await extractBookingFromText(text));
} catch (err) {
const backendMessage = axios.isAxiosError(err)
? (err.response?.data as { message?: string | string[] } | undefined)
?.message
: null;
setError(
backendMessage
? `Mock AI request failed: ${
Array.isArray(backendMessage)
? backendMessage.join(", ")
: backendMessage
}`
: "Mock AI request failed",
);
} finally {
setLoading(false);
}
};
const handleCreateDraftBooking = () => {
window.alert("Draft booking creation will be connected in the next step.");
};
const canCreateDraft = Boolean(result?.validation.valid);
return (
<Stack gap="lg" p="md" maw={860}>
<Title order={2}>Mock AI Booking Assistant</Title>
<Card withBorder radius="md" padding="lg">
<Stack gap="sm">
<Textarea
label="Customer booking request"
placeholder="Enter customer booking request..."
description={`Example: ${EXAMPLE_TEXT}`}
minRows={4}
autosize
value={text}
onChange={(event) => setText(event.currentTarget.value)}
/>
<Group>
<Button
onClick={handleTest}
loading={loading}
disabled={text.trim().length < 5}
>
{loading ? "Testing..." : "Test Mock AI"}
</Button>
<Button
variant="light"
color="gray"
onClick={() => setText(EXAMPLE_TEXT)}
>
Use example
</Button>
</Group>
</Stack>
</Card>
{error && (
<Alert color="red" title="Request failed">
{error}
</Alert>
)}
{result && (
<>
<Card withBorder radius="md" padding="lg">
<Stack gap="sm">
<Group justify="space-between">
<Title order={4}>Extracted Booking Data</Title>
<Badge variant="light" color="gray">
provider: {result.provider}
</Badge>
</Group>
<Table withTableBorder={false} verticalSpacing="xs">
<Table.Tbody>
{EXTRACTED_FIELD_LABELS.map(({ key, label }) => (
<Table.Tr key={key}>
<Table.Td w={200}>
<Text size="sm" c="dimmed">
{label}
</Text>
</Table.Td>
<Table.Td>
<Text size="sm" fw={500}>
{formatValue(result.extracted[key])}
</Text>
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</Stack>
</Card>
<Card withBorder radius="md" padding="lg">
<Stack gap="sm">
<Group>
<Title order={4}>Validation</Title>
<Badge color={result.validation.valid ? "green" : "red"}>
{result.validation.valid ? "Valid" : "Invalid"}
</Badge>
</Group>
{result.validation.errors.length > 0 && (
<Stack gap={4}>
{result.validation.errors.map((message) => (
<Text key={message} size="sm" c="red">
{message}
</Text>
))}
</Stack>
)}
</Stack>
</Card>
<Card withBorder radius="md" padding="lg">
<Stack gap="sm">
<Group>
<Title order={4}>Recommendation</Title>
<Badge
color={
result.recommendation.action === "CREATE_DRAFT_BOOKING"
? "green"
: "yellow"
}
>
{result.recommendation.action}
</Badge>
<Badge variant="light">
confidence {Math.round(result.recommendation.confidence * 100)}%
</Badge>
</Group>
<Text size="sm">{result.recommendation.message}</Text>
</Stack>
</Card>
<Group>
<Button
color="green"
disabled={!canCreateDraft}
onClick={handleCreateDraftBooking}
>
Create Draft Booking
</Button>
<Button
variant="subtle"
color="gray"
onClick={() => setShowRawJson((open) => !open)}
>
{showRawJson ? "Hide raw JSON" : "Show raw JSON"}
</Button>
</Group>
{showRawJson && (
<Code block>{JSON.stringify(result, null, 2)}</Code>
)}
</>
)}
</Stack>
);
}