1632 lines
61 KiB
TypeScript
1632 lines
61 KiB
TypeScript
"use client";
|
||
|
||
import { useMemo, useState } from "react";
|
||
|
||
import Alert from "@mui/material/Alert";
|
||
import Box from "@mui/material/Box";
|
||
import Button from "@mui/material/Button";
|
||
import Card from "@mui/material/Card";
|
||
import CardContent from "@mui/material/CardContent";
|
||
import Chip from "@mui/material/Chip";
|
||
import Divider from "@mui/material/Divider";
|
||
import Grid from "@mui/material/Grid2";
|
||
import LinearProgress from "@mui/material/LinearProgress";
|
||
import Stack from "@mui/material/Stack";
|
||
import Step from "@mui/material/Step";
|
||
import StepLabel from "@mui/material/StepLabel";
|
||
import Stepper from "@mui/material/Stepper";
|
||
import Tab from "@mui/material/Tab";
|
||
import Tabs from "@mui/material/Tabs";
|
||
import Typography from "@mui/material/Typography";
|
||
import { alpha, useTheme } from "@mui/material/styles";
|
||
|
||
import type { ThemeColor } from "@core/types";
|
||
|
||
import HorizontalWithAvatar from "@/components/card-statistics/HorizontalWithAvatar";
|
||
import { useFarmHub } from "@/hooks/useFarmHub";
|
||
import {
|
||
fertilizationPlanParserService,
|
||
type FertilizationPlanAnswerValue,
|
||
type FertilizationPlanApplication,
|
||
type FertilizationPlanData,
|
||
type FertilizationPlanParserResult,
|
||
} from "@/libs/api/services/fertilizationPlanParserService";
|
||
import {
|
||
irrigationPlanParserService,
|
||
type IrrigationPlanAnswerValue,
|
||
type IrrigationPlanData,
|
||
type IrrigationPlanParserResult,
|
||
} from "@/libs/api/services/irrigationPlanParserService";
|
||
import OptionMenu from "@core/components/option-menu";
|
||
import CustomAvatar from "@core/components/mui/Avatar";
|
||
import CustomTextField from "@core/components/mui/TextField";
|
||
|
||
type ParserTabKey = "fertilization" | "irrigation";
|
||
type ParserAnswerValue =
|
||
| FertilizationPlanAnswerValue
|
||
| IrrigationPlanAnswerValue;
|
||
|
||
type ParserQuestion = {
|
||
id: string;
|
||
field: string;
|
||
question: string;
|
||
rationale: string;
|
||
};
|
||
|
||
type ParserResponse =
|
||
| FertilizationPlanParserResult
|
||
| IrrigationPlanParserResult;
|
||
|
||
type TabState = {
|
||
message: string;
|
||
response: ParserResponse | null;
|
||
answers: Record<string, string>;
|
||
activeQuestion: number;
|
||
requestError: string | null;
|
||
statusNote: string | null;
|
||
loading: boolean;
|
||
};
|
||
|
||
type FieldMeta = {
|
||
key: string;
|
||
label: string;
|
||
value: string | null;
|
||
};
|
||
|
||
type ParserConfig = {
|
||
key: ParserTabKey;
|
||
label: string;
|
||
badge: string;
|
||
icon: string;
|
||
heroTitle: string;
|
||
heroDescription: string;
|
||
panelTitle: string;
|
||
panelDescription: string;
|
||
inputLabel: string;
|
||
inputPlaceholder: string;
|
||
samplePrompts: string[];
|
||
fieldLabels: Record<string, string>;
|
||
previewOrder: string[];
|
||
itemCountTitle: string;
|
||
primaryCardTitle: string;
|
||
primaryFallbackTitle: string;
|
||
finalCardTitle: string;
|
||
finalCardDescription: string;
|
||
buildPayload: (payload: {
|
||
message?: string;
|
||
answers?: Record<string, ParserAnswerValue>;
|
||
partialPlan?: unknown;
|
||
farmUuid?: string;
|
||
}) => Record<string, unknown>;
|
||
submit: (payload: Record<string, unknown>) => Promise<ParserResponse>;
|
||
getFieldValue: (plan: unknown, field: string) => ParserAnswerValue;
|
||
formatFieldValue: (field: string, value: ParserAnswerValue) => string;
|
||
getPrimaryItem: (plan: unknown) => unknown | null;
|
||
getPrimaryMeta: (item: unknown) => FieldMeta[];
|
||
getPrimaryHeadline: (item: unknown) => string;
|
||
getItemCount: (plan: unknown) => number;
|
||
};
|
||
|
||
type FertilizationPlanParserPageProps = {
|
||
initialTab?: ParserTabKey;
|
||
enabledTabs?: ParserTabKey[];
|
||
};
|
||
|
||
const createInitialTabState = (): TabState => ({
|
||
message: "",
|
||
response: null,
|
||
answers: {},
|
||
activeQuestion: 0,
|
||
requestError: null,
|
||
statusNote: null,
|
||
loading: false,
|
||
});
|
||
|
||
const formatNumber = (value: number) =>
|
||
new Intl.NumberFormat("fa-IR").format(value);
|
||
|
||
const defaultFormatFieldValue = (value: ParserAnswerValue) => {
|
||
if (value === null || value === undefined || value === "") {
|
||
return "—";
|
||
}
|
||
|
||
if (typeof value === "number") {
|
||
return formatNumber(value);
|
||
}
|
||
|
||
if (typeof value === "boolean") {
|
||
return value ? "بله" : "خیر";
|
||
}
|
||
|
||
return value;
|
||
};
|
||
|
||
const formatNumberWithUnit = (
|
||
value: number | null | undefined,
|
||
unit: string,
|
||
) => {
|
||
if (value === null || value === undefined) {
|
||
return null;
|
||
}
|
||
|
||
return `${formatNumber(value)} ${unit}`;
|
||
};
|
||
|
||
const getStatusMeta = (status?: ParserResponse["status"]) => {
|
||
if (status === "completed") {
|
||
return {
|
||
label: "آماده اجرا",
|
||
color: "success" as const,
|
||
icon: "tabler-rosette-discount-check",
|
||
};
|
||
}
|
||
|
||
if (status === "needs_clarification") {
|
||
return {
|
||
label: "نیازمند تکمیل",
|
||
color: "warning" as const,
|
||
icon: "tabler-help-hexagon",
|
||
};
|
||
}
|
||
|
||
return {
|
||
label: "در انتظار تحلیل",
|
||
color: "secondary" as const,
|
||
icon: "tabler-sparkles",
|
||
};
|
||
};
|
||
|
||
const extractErrorMessage = (error: unknown, fallback: string) => {
|
||
if (typeof error !== "object" || error === null) {
|
||
return fallback;
|
||
}
|
||
|
||
if (
|
||
"details" in error &&
|
||
error.details &&
|
||
typeof error.details === "object" &&
|
||
"data" in error.details &&
|
||
error.details.data &&
|
||
typeof error.details.data === "object"
|
||
) {
|
||
const data = error.details.data as Record<string, unknown>;
|
||
const nonFieldErrors = data.non_field_errors;
|
||
|
||
if (
|
||
Array.isArray(nonFieldErrors) &&
|
||
typeof nonFieldErrors[0] === "string"
|
||
) {
|
||
return nonFieldErrors[0];
|
||
}
|
||
}
|
||
|
||
if ("message" in error && typeof error.message === "string") {
|
||
return error.message;
|
||
}
|
||
|
||
return fallback;
|
||
};
|
||
|
||
const createJsonSnapshot = (plan: unknown) => {
|
||
if (!plan) {
|
||
return "{}";
|
||
}
|
||
|
||
return JSON.stringify(plan, null, 2);
|
||
};
|
||
|
||
const getFertilizationFieldValue = (plan: unknown, field: string) => {
|
||
const data = plan as FertilizationPlanData | null | undefined;
|
||
|
||
if (!data) return null;
|
||
if (field === "crop_name") return data.crop_name;
|
||
if (field === "growth_stage") return data.growth_stage;
|
||
if (field === "objective") return data.objective;
|
||
|
||
const application = data.applications?.[0];
|
||
|
||
if (!application) return null;
|
||
if (field === "fertilizer_name") return application.fertilizer_name;
|
||
if (field === "formula") return application.formula;
|
||
if (field === "amount") return application.amount;
|
||
if (field === "application_method") return application.application_method;
|
||
if (field === "timing") return application.timing;
|
||
if (field === "interval_days") return application.interval_days;
|
||
if (field === "purpose") return application.purpose;
|
||
|
||
return null;
|
||
};
|
||
|
||
const getIrrigationFieldValue = (plan: unknown, field: string) => {
|
||
const data = plan as IrrigationPlanData | null | undefined;
|
||
|
||
if (!data) return null;
|
||
if (field === "crop_name") return data.crop_name;
|
||
if (field === "growth_stage") return data.growth_stage;
|
||
if (field === "irrigation_method") return data.irrigation_method;
|
||
if (field === "water_amount_per_event") return data.water_amount_per_event;
|
||
if (field === "duration_minutes") return data.duration_minutes;
|
||
if (field === "frequency_text") return data.frequency_text;
|
||
if (field === "interval_days") return data.interval_days;
|
||
if (field === "preferred_time_of_day") return data.preferred_time_of_day;
|
||
if (field === "start_date") return data.start_date;
|
||
if (field === "target_area") return data.target_area;
|
||
|
||
return null;
|
||
};
|
||
|
||
const buildNextAnswersState = (
|
||
response: ParserResponse,
|
||
previousAnswers: Record<string, string>,
|
||
config: ParserConfig,
|
||
) => {
|
||
return response.questions.reduce<Record<string, string>>((acc, question) => {
|
||
const previousValue = previousAnswers[question.field];
|
||
|
||
if (previousValue) {
|
||
acc[question.field] = previousValue;
|
||
return acc;
|
||
}
|
||
|
||
const extractedValue = config.getFieldValue(
|
||
response.collected_data,
|
||
question.field,
|
||
);
|
||
|
||
if (extractedValue !== null && extractedValue !== undefined) {
|
||
acc[question.field] = String(extractedValue);
|
||
}
|
||
|
||
return acc;
|
||
}, {});
|
||
};
|
||
|
||
const normalizeAnswers = (
|
||
questions: ParserQuestion[],
|
||
answers: Record<string, string>,
|
||
) => {
|
||
return questions.reduce<Record<string, ParserAnswerValue>>(
|
||
(acc, question) => {
|
||
const rawValue = answers[question.field]?.trim();
|
||
|
||
if (!rawValue) {
|
||
return acc;
|
||
}
|
||
|
||
if (
|
||
question.field === "interval_days" ||
|
||
question.field === "duration_minutes"
|
||
) {
|
||
const parsed = Number(rawValue);
|
||
acc[question.field] = Number.isFinite(parsed) ? parsed : rawValue;
|
||
return acc;
|
||
}
|
||
|
||
acc[question.field] = rawValue;
|
||
return acc;
|
||
},
|
||
{},
|
||
);
|
||
};
|
||
|
||
const FERTILIZATION_FIELD_LABELS: Record<string, string> = {
|
||
crop_name: "محصول",
|
||
growth_stage: "مرحله رشد",
|
||
objective: "هدف برنامه",
|
||
fertilizer_name: "نام کود",
|
||
formula: "فرمول کود",
|
||
amount: "مقدار مصرف",
|
||
application_method: "روش مصرف",
|
||
timing: "زمان بندی",
|
||
interval_days: "فاصله نوبت ها",
|
||
purpose: "هدف هر نوبت",
|
||
};
|
||
|
||
const IRRIGATION_FIELD_LABELS: Record<string, string> = {
|
||
crop_name: "محصول",
|
||
growth_stage: "مرحله رشد",
|
||
irrigation_method: "روش آبیاری",
|
||
water_amount_per_event: "مقدار آب هر نوبت",
|
||
duration_minutes: "مدت هر نوبت",
|
||
frequency_text: "تناوب آبیاری",
|
||
interval_days: "فاصله آبیاری",
|
||
preferred_time_of_day: "زمان مناسب اجرا",
|
||
start_date: "زمان شروع",
|
||
target_area: "محدوده اجرا",
|
||
};
|
||
|
||
const PARSER_CONFIGS: Record<ParserTabKey, ParserConfig> = {
|
||
fertilization: {
|
||
key: "fertilization",
|
||
label: "کودهی",
|
||
badge: "Fertilization AI Planner",
|
||
icon: "tabler-flask-2",
|
||
heroTitle: "برنامه کودهی با ورودی متنی و خروجی JSON آماده اجرا",
|
||
heroDescription:
|
||
"متن آزاد کشاورز را بگیر، اگر داده ناقص بود سوال تکمیلی بپرس و در نهایت یک نسخه ساختاریافته و خوش خوان از برنامه کودهی تحویل بده.",
|
||
panelTitle: "اتاق فرمان برنامه کودهی",
|
||
panelDescription:
|
||
"هر چقدر متن طبیعی تر و نزدیک به زبان خود کشاورز باشد، API بهتر می تواند ساختار نهایی را استخراج کند.",
|
||
inputLabel: "متن آزاد برنامه کودهی",
|
||
inputPlaceholder:
|
||
"مثلا: برای گندم در مرحله پنجه زنی هر 12 روز یک بار کود 20-20-20 به مقدار 35 کیلوگرم در هکتار از طریق کودآبیاری می دهم.",
|
||
samplePrompts: [
|
||
"برای گندم در مرحله پنجه زنی هر 12 روز یک بار کود 20-20-20 به مقدار 35 کیلوگرم در هکتار از طریق کودآبیاری می دهم.",
|
||
"برای ذرت از کود کامل استفاده می کنم اما فاصله بین نوبت ها را دقیق نمی دانم و می خواهم برنامه ام کامل شود.",
|
||
"برای گوجه فرنگی در شروع گلدهی یک برنامه کودهی دقیق با کود NPK و مصرف مرحله ای می خواهم.",
|
||
],
|
||
fieldLabels: FERTILIZATION_FIELD_LABELS,
|
||
previewOrder: [
|
||
"crop_name",
|
||
"growth_stage",
|
||
"objective",
|
||
"fertilizer_name",
|
||
"formula",
|
||
"amount",
|
||
"application_method",
|
||
"timing",
|
||
"interval_days",
|
||
"purpose",
|
||
],
|
||
itemCountTitle: "نوبت های کودی",
|
||
primaryCardTitle: "نسخه پیشنهادی برای اولین نوبت کودی",
|
||
primaryFallbackTitle: "نام کود هنوز مشخص نیست",
|
||
finalCardTitle: "خروجی نهایی کودهی آماده تحویل",
|
||
finalCardDescription:
|
||
"اگر لازم بود همین JSON را برای ذخیره، اشتراک گذاری یا مرحله بعدی workflow استفاده کن.",
|
||
buildPayload: ({ message, answers, partialPlan, farmUuid }) => ({
|
||
...(message ? { message } : {}),
|
||
...(answers ? { answers } : {}),
|
||
...(partialPlan ? { partial_plan: partialPlan } : {}),
|
||
...(farmUuid ? { farm_uuid: farmUuid } : {}),
|
||
}),
|
||
submit: (payload) =>
|
||
fertilizationPlanParserService.parseFromText(
|
||
payload as Parameters<
|
||
typeof fertilizationPlanParserService.parseFromText
|
||
>[0],
|
||
),
|
||
getFieldValue: getFertilizationFieldValue,
|
||
formatFieldValue: (field, value) => {
|
||
if (field === "interval_days" && typeof value === "number") {
|
||
return `${formatNumber(value)} روز`;
|
||
}
|
||
|
||
return defaultFormatFieldValue(value);
|
||
},
|
||
getPrimaryItem: (plan) =>
|
||
(plan as FertilizationPlanData | null | undefined)?.applications?.[0] ??
|
||
null,
|
||
getPrimaryMeta: (item) => {
|
||
const application = item as FertilizationPlanApplication | null;
|
||
|
||
if (!application) return [];
|
||
|
||
return [
|
||
{
|
||
key: "formula",
|
||
label: FERTILIZATION_FIELD_LABELS.formula,
|
||
value: application.formula,
|
||
},
|
||
{
|
||
key: "amount",
|
||
label: FERTILIZATION_FIELD_LABELS.amount,
|
||
value: application.amount,
|
||
},
|
||
{
|
||
key: "application_method",
|
||
label: FERTILIZATION_FIELD_LABELS.application_method,
|
||
value: application.application_method,
|
||
},
|
||
{
|
||
key: "timing",
|
||
label: FERTILIZATION_FIELD_LABELS.timing,
|
||
value: application.timing,
|
||
},
|
||
{
|
||
key: "interval_days",
|
||
label: FERTILIZATION_FIELD_LABELS.interval_days,
|
||
value: formatNumberWithUnit(application.interval_days, "روز"),
|
||
},
|
||
{
|
||
key: "purpose",
|
||
label: FERTILIZATION_FIELD_LABELS.purpose,
|
||
value: application.purpose,
|
||
},
|
||
];
|
||
},
|
||
getPrimaryHeadline: (item) =>
|
||
(item as FertilizationPlanApplication | null)?.fertilizer_name ||
|
||
"نام کود هنوز مشخص نیست",
|
||
getItemCount: (plan) =>
|
||
(plan as FertilizationPlanData | null | undefined)?.applications
|
||
?.length ?? 0,
|
||
},
|
||
irrigation: {
|
||
key: "irrigation",
|
||
label: "آبیاری",
|
||
badge: "Irrigation AI Planner",
|
||
icon: "tabler-droplet-half-2",
|
||
heroTitle: "برنامه آبیاری با ورودی متنی و خروجی JSON آماده اجرا",
|
||
heroDescription:
|
||
"برای آبیاری هم همان flow هوشمند را داشته باش: متن آزاد را بفرست، ابهام ها را کامل کن و یک برنامه ساختاریافته و قابل اجرا بگیر.",
|
||
panelTitle: "اتاق فرمان برنامه آبیاری",
|
||
panelDescription:
|
||
"جزئیات روش آبیاری، زمان بندی و مقدار آب را با زبان طبیعی بنویس تا API آن را به برنامه دقیق تبدیل کند.",
|
||
inputLabel: "متن آزاد برنامه آبیاری",
|
||
inputPlaceholder:
|
||
"مثلا: برای گوجه فرنگی با آبیاری قطره ای هر سه روز یک بار صبح زود 25 دقیقه آبیاری می کنم و حدود 18 لیتر برای هر بوته می دهم.",
|
||
samplePrompts: [
|
||
"برای گوجه فرنگی با آبیاری قطره ای هر سه روز یک بار صبح زود 25 دقیقه آبیاری می کنم و حدود 18 لیتر برای هر بوته می دهم.",
|
||
"برای هندوانه هر دو روز یک بار آبیاری می کنم اما زمان شروع برنامه و محدوده اجرا را هنوز مشخص نکرده ام.",
|
||
"برای خیار گلخانه ای با تیپ آبیاری می کنم و می خواهم برنامه ام بر اساس زمان و حجم آب کامل شود.",
|
||
],
|
||
fieldLabels: IRRIGATION_FIELD_LABELS,
|
||
previewOrder: [
|
||
"crop_name",
|
||
"growth_stage",
|
||
"irrigation_method",
|
||
"water_amount_per_event",
|
||
"duration_minutes",
|
||
"frequency_text",
|
||
"interval_days",
|
||
"preferred_time_of_day",
|
||
"start_date",
|
||
"target_area",
|
||
],
|
||
itemCountTitle: "بخش های آبیاری",
|
||
primaryCardTitle: "چکیده اجرای برنامه آبیاری",
|
||
primaryFallbackTitle: "روش آبیاری هنوز مشخص نیست",
|
||
finalCardTitle: "خروجی نهایی آبیاری آماده تحویل",
|
||
finalCardDescription:
|
||
"از همین JSON می توانی برای نمایش، ذخیره یا ادامه workflow آبیاری استفاده کنی.",
|
||
buildPayload: ({ message, answers, partialPlan, farmUuid }) => ({
|
||
...(message ? { message } : {}),
|
||
...(answers ? { answers } : {}),
|
||
...(partialPlan ? { partial_plan: partialPlan } : {}),
|
||
...(farmUuid ? { farm_uuid: farmUuid } : {}),
|
||
}),
|
||
submit: (payload) =>
|
||
irrigationPlanParserService.parseFromText(
|
||
payload as Parameters<
|
||
typeof irrigationPlanParserService.parseFromText
|
||
>[0],
|
||
),
|
||
getFieldValue: getIrrigationFieldValue,
|
||
formatFieldValue: (field, value) => {
|
||
if (field === "duration_minutes" && typeof value === "number") {
|
||
return `${formatNumber(value)} دقیقه`;
|
||
}
|
||
|
||
if (field === "interval_days" && typeof value === "number") {
|
||
return `${formatNumber(value)} روز`;
|
||
}
|
||
|
||
return defaultFormatFieldValue(value);
|
||
},
|
||
getPrimaryItem: (plan) => plan ?? null,
|
||
getPrimaryMeta: (item) => {
|
||
const data = item as IrrigationPlanData | null;
|
||
|
||
if (!data) return [];
|
||
|
||
return [
|
||
{
|
||
key: "irrigation_method",
|
||
label: IRRIGATION_FIELD_LABELS.irrigation_method,
|
||
value: data.irrigation_method,
|
||
},
|
||
{
|
||
key: "water_amount_per_event",
|
||
label: IRRIGATION_FIELD_LABELS.water_amount_per_event,
|
||
value: data.water_amount_per_event,
|
||
},
|
||
{
|
||
key: "duration_minutes",
|
||
label: IRRIGATION_FIELD_LABELS.duration_minutes,
|
||
value: formatNumberWithUnit(data.duration_minutes, "دقیقه"),
|
||
},
|
||
{
|
||
key: "frequency_text",
|
||
label: IRRIGATION_FIELD_LABELS.frequency_text,
|
||
value: data.frequency_text,
|
||
},
|
||
{
|
||
key: "preferred_time_of_day",
|
||
label: IRRIGATION_FIELD_LABELS.preferred_time_of_day,
|
||
value: data.preferred_time_of_day,
|
||
},
|
||
{
|
||
key: "target_area",
|
||
label: IRRIGATION_FIELD_LABELS.target_area,
|
||
value: data.target_area,
|
||
},
|
||
];
|
||
},
|
||
getPrimaryHeadline: (item) =>
|
||
(item as IrrigationPlanData | null)?.irrigation_method ||
|
||
"روش آبیاری هنوز مشخص نیست",
|
||
getItemCount: (plan) => {
|
||
const data = plan as IrrigationPlanData | null | undefined;
|
||
|
||
if (!data) return 0;
|
||
|
||
return 1 + (data.trigger_conditions?.length ?? 0);
|
||
},
|
||
},
|
||
};
|
||
|
||
const FertilizationPlanParserPage = ({
|
||
initialTab = "fertilization",
|
||
enabledTabs = ["fertilization", "irrigation"],
|
||
}: FertilizationPlanParserPageProps) => {
|
||
const theme = useTheme();
|
||
const { farmHub } = useFarmHub();
|
||
const farmUuid = farmHub?.farm_uuid;
|
||
const availableTabs = enabledTabs.filter(
|
||
(tab): tab is ParserTabKey => tab in PARSER_CONFIGS,
|
||
);
|
||
const resolvedInitialTab = availableTabs.includes(initialTab)
|
||
? initialTab
|
||
: availableTabs[0] ?? "fertilization";
|
||
const singleTabMode = availableTabs.length === 1;
|
||
|
||
const [activeTab, setActiveTab] = useState<ParserTabKey>(resolvedInitialTab);
|
||
const [tabStates, setTabStates] = useState<Record<ParserTabKey, TabState>>({
|
||
fertilization: createInitialTabState(),
|
||
irrigation: createInitialTabState(),
|
||
});
|
||
|
||
const config = PARSER_CONFIGS[activeTab];
|
||
const currentState = tabStates[activeTab];
|
||
const statusMeta = getStatusMeta(currentState.response?.status);
|
||
const planPreview =
|
||
currentState.response?.final_plan ??
|
||
currentState.response?.collected_data ??
|
||
null;
|
||
const primaryItem = config.getPrimaryItem(planPreview);
|
||
const currentQuestion =
|
||
currentState.response?.questions[currentState.activeQuestion] ?? null;
|
||
|
||
const completionValue = currentState.response
|
||
? Math.max(
|
||
20,
|
||
Math.round(
|
||
((config.previewOrder.length -
|
||
currentState.response.missing_fields.length) /
|
||
config.previewOrder.length) *
|
||
100,
|
||
),
|
||
)
|
||
: 18;
|
||
|
||
const statCards = useMemo(
|
||
() => [
|
||
{
|
||
stats: currentState.response?.status_fa ?? "شروع نشده",
|
||
title: "وضعیت پردازش",
|
||
avatarIcon: statusMeta.icon,
|
||
avatarColor: statusMeta.color as ThemeColor,
|
||
},
|
||
{
|
||
stats: formatNumber(currentState.response?.missing_fields.length ?? 0),
|
||
title: "فیلدهای ناقص",
|
||
avatarIcon: "tabler-help-circle",
|
||
avatarColor: "warning" as ThemeColor,
|
||
},
|
||
{
|
||
stats: formatNumber(config.getItemCount(planPreview)),
|
||
title: config.itemCountTitle,
|
||
avatarIcon: config.icon,
|
||
avatarColor: "success" as ThemeColor,
|
||
},
|
||
],
|
||
[
|
||
config,
|
||
currentState.response?.missing_fields.length,
|
||
currentState.response?.status_fa,
|
||
planPreview,
|
||
statusMeta.color,
|
||
statusMeta.icon,
|
||
],
|
||
);
|
||
|
||
const updateTabState = (
|
||
tab: ParserTabKey,
|
||
updater: Partial<TabState> | ((previous: TabState) => TabState),
|
||
) => {
|
||
setTabStates((previous) => ({
|
||
...previous,
|
||
[tab]:
|
||
typeof updater === "function"
|
||
? updater(previous[tab])
|
||
: { ...previous[tab], ...updater },
|
||
}));
|
||
};
|
||
|
||
const handleReset = (tab: ParserTabKey) => {
|
||
updateTabState(tab, createInitialTabState());
|
||
};
|
||
|
||
const submitRequest = async (
|
||
tab: ParserTabKey,
|
||
payload: Record<string, unknown>,
|
||
) => {
|
||
const tabConfig = PARSER_CONFIGS[tab];
|
||
|
||
updateTabState(tab, (previous) => ({
|
||
...previous,
|
||
loading: true,
|
||
requestError: null,
|
||
statusNote: null,
|
||
}));
|
||
|
||
try {
|
||
const nextResponse = await tabConfig.submit(payload);
|
||
|
||
updateTabState(tab, (previous) => ({
|
||
...previous,
|
||
response: nextResponse,
|
||
answers: buildNextAnswersState(
|
||
nextResponse,
|
||
previous.answers,
|
||
tabConfig,
|
||
),
|
||
activeQuestion: 0,
|
||
statusNote:
|
||
nextResponse.status === "completed"
|
||
? `برنامه ${tabConfig.label} نهایی آماده شد و می توانی آن را با تیم مزرعه به اشتراک بگذاری.`
|
||
: "سیستم چند ابهام پیدا کرده؛ جواب ها را کامل کن تا JSON نهایی ساخته شود.",
|
||
}));
|
||
} catch (error) {
|
||
updateTabState(tab, (previous) => ({
|
||
...previous,
|
||
requestError: extractErrorMessage(
|
||
error,
|
||
`در ساخت برنامه ${tabConfig.label} مشکلی پیش آمد. دوباره تلاش کن.`,
|
||
),
|
||
}));
|
||
} finally {
|
||
updateTabState(tab, (previous) => ({
|
||
...previous,
|
||
loading: false,
|
||
}));
|
||
}
|
||
};
|
||
|
||
const handleGeneratePlan = async () => {
|
||
const trimmedMessage = currentState.message.trim();
|
||
|
||
if (!trimmedMessage) {
|
||
updateTabState(activeTab, (previous) => ({
|
||
...previous,
|
||
requestError: `اول متن برنامه ${config.label} را بنویس تا تحلیل را شروع کنیم.`,
|
||
}));
|
||
return;
|
||
}
|
||
|
||
await submitRequest(
|
||
activeTab,
|
||
config.buildPayload({
|
||
message: trimmedMessage,
|
||
farmUuid,
|
||
}),
|
||
);
|
||
};
|
||
|
||
const handleSubmitAnswers = async () => {
|
||
if (!currentState.response) {
|
||
return;
|
||
}
|
||
|
||
const normalizedAnswers = normalizeAnswers(
|
||
currentState.response.questions,
|
||
currentState.answers,
|
||
);
|
||
|
||
const unansweredQuestion = currentState.response.questions.find(
|
||
(question) => {
|
||
const value = normalizedAnswers[question.field];
|
||
|
||
return value === undefined || value === null || value === "";
|
||
},
|
||
);
|
||
|
||
if (unansweredQuestion) {
|
||
updateTabState(activeTab, (previous) => ({
|
||
...previous,
|
||
requestError: `پاسخ سوال «${config.fieldLabels[unansweredQuestion.field] ?? unansweredQuestion.question}» هنوز کامل نشده است.`,
|
||
}));
|
||
return;
|
||
}
|
||
|
||
await submitRequest(
|
||
activeTab,
|
||
config.buildPayload({
|
||
answers: normalizedAnswers,
|
||
partialPlan: currentState.response.collected_data,
|
||
farmUuid,
|
||
}),
|
||
);
|
||
};
|
||
|
||
const handleCopyJson = async () => {
|
||
if (
|
||
!planPreview ||
|
||
typeof navigator === "undefined" ||
|
||
!navigator.clipboard
|
||
) {
|
||
updateTabState(activeTab, (previous) => ({
|
||
...previous,
|
||
statusNote: "کپی خودکار روی این مرورگر در دسترس نیست.",
|
||
}));
|
||
return;
|
||
}
|
||
|
||
await navigator.clipboard.writeText(createJsonSnapshot(planPreview));
|
||
|
||
updateTabState(activeTab, (previous) => ({
|
||
...previous,
|
||
statusNote: `نسخه JSON برنامه ${config.label} در کلیپ بورد کپی شد.`,
|
||
}));
|
||
};
|
||
|
||
return (
|
||
<Box>
|
||
<Grid container spacing={6}>
|
||
<Grid size={12}>
|
||
<Card sx={{ overflow: "hidden" }}>
|
||
<CardContent
|
||
sx={{
|
||
position: "relative",
|
||
overflow: "hidden",
|
||
p: { xs: 4, md: 6 },
|
||
color: "common.white",
|
||
background: `linear-gradient(135deg, ${theme.palette.success.dark} 0%, ${theme.palette.primary.main} 42%, ${theme.palette.info.main} 100%)`,
|
||
}}
|
||
>
|
||
<Box
|
||
sx={{
|
||
position: "absolute",
|
||
insetInlineEnd: -54,
|
||
insetBlockStart: -60,
|
||
inlineSize: 220,
|
||
blockSize: 220,
|
||
borderRadius: "40% 60% 70% 30% / 50% 45% 55% 50%",
|
||
backgroundColor: alpha(theme.palette.common.white, 0.12),
|
||
}}
|
||
/>
|
||
<Box
|
||
sx={{
|
||
position: "absolute",
|
||
insetInlineStart: "-8%",
|
||
insetBlockEnd: "-65%",
|
||
inlineSize: "46%",
|
||
blockSize: 260,
|
||
borderRadius: "50%",
|
||
backgroundColor: alpha(theme.palette.common.white, 0.08),
|
||
}}
|
||
/>
|
||
<Grid
|
||
container
|
||
spacing={5}
|
||
sx={{ position: "relative", zIndex: 1, alignItems: "center" }}
|
||
>
|
||
<Grid size={{ xs: 12, lg: 7 }}>
|
||
<Stack spacing={3}>
|
||
<Chip
|
||
label={config.badge}
|
||
size="small"
|
||
sx={{
|
||
width: "fit-content",
|
||
color: "common.white",
|
||
border: `1px solid ${alpha(theme.palette.common.white, 0.26)}`,
|
||
backgroundColor: alpha(
|
||
theme.palette.common.white,
|
||
0.12,
|
||
),
|
||
}}
|
||
/>
|
||
<Box>
|
||
<Typography
|
||
variant="h3"
|
||
sx={{ color: "common.white", fontWeight: 800, mb: 2 }}
|
||
>
|
||
{config.heroTitle}
|
||
</Typography>
|
||
<Typography
|
||
sx={{
|
||
maxWidth: 760,
|
||
color: alpha(theme.palette.common.white, 0.84),
|
||
lineHeight: 1.9,
|
||
}}
|
||
>
|
||
{config.heroDescription}
|
||
</Typography>
|
||
</Box>
|
||
<Stack
|
||
direction={{ xs: "column", sm: "row" }}
|
||
spacing={2}
|
||
flexWrap="wrap"
|
||
>
|
||
<Box
|
||
sx={{
|
||
px: 2.5,
|
||
py: 1.75,
|
||
borderRadius: 4,
|
||
backdropFilter: "blur(10px)",
|
||
backgroundColor: alpha(
|
||
theme.palette.common.white,
|
||
0.12,
|
||
),
|
||
border: `1px solid ${alpha(theme.palette.common.white, 0.16)}`,
|
||
}}
|
||
>
|
||
<Typography
|
||
sx={{
|
||
color: alpha(theme.palette.common.white, 0.72),
|
||
mb: 0.5,
|
||
}}
|
||
>
|
||
موتور تحلیل
|
||
</Typography>
|
||
<Typography
|
||
sx={{ color: "common.white", fontWeight: 700 }}
|
||
>
|
||
Free-text parser + clarification flow
|
||
</Typography>
|
||
</Box>
|
||
<Box
|
||
sx={{
|
||
px: 2.5,
|
||
py: 1.75,
|
||
borderRadius: 4,
|
||
backdropFilter: "blur(10px)",
|
||
backgroundColor: alpha(
|
||
theme.palette.common.white,
|
||
0.12,
|
||
),
|
||
border: `1px solid ${alpha(theme.palette.common.white, 0.16)}`,
|
||
}}
|
||
>
|
||
<Typography
|
||
sx={{
|
||
color: alpha(theme.palette.common.white, 0.72),
|
||
mb: 0.5,
|
||
}}
|
||
>
|
||
مزرعه فعال
|
||
</Typography>
|
||
<Typography
|
||
sx={{ color: "common.white", fontWeight: 700 }}
|
||
>
|
||
{farmHub?.name || farmUuid || "بدون مزرعه فعال"}
|
||
</Typography>
|
||
</Box>
|
||
</Stack>
|
||
</Stack>
|
||
</Grid>
|
||
<Grid size={{ xs: 12, lg: 5 }}>
|
||
<Card
|
||
sx={{
|
||
borderRadius: 5,
|
||
color: "text.primary",
|
||
backgroundColor: alpha(theme.palette.common.white, 0.96),
|
||
boxShadow: `0 24px 60px ${alpha(theme.palette.common.black, 0.16)}`,
|
||
}}
|
||
>
|
||
<CardContent>
|
||
<Stack spacing={3}>
|
||
<Box>
|
||
<Typography
|
||
variant="h5"
|
||
sx={{ fontWeight: 800, mb: 1.5 }}
|
||
>
|
||
{singleTabMode
|
||
? `جریان هوشمند ${config.label}`
|
||
: "دو جریان هوشمند، یک صفحه واحد"}
|
||
</Typography>
|
||
<Typography color="text.secondary">
|
||
{singleTabMode
|
||
? `ورودی متنی برنامه ${config.label} را بفرست، ابهام ها را کامل کن و خروجی JSON نهایی بگیر.`
|
||
: "بین تب آبیاری و تب کودهی جابه جا شو و هر کدام را با همان flow سوال های تکمیلی تا JSON نهایی پیش ببر."}
|
||
</Typography>
|
||
</Box>
|
||
<Stepper
|
||
activeStep={
|
||
currentState.response?.status === "completed"
|
||
? 2
|
||
: currentState.response
|
||
? 1
|
||
: 0
|
||
}
|
||
alternativeLabel
|
||
>
|
||
<Step>
|
||
<StepLabel>متن آزاد</StepLabel>
|
||
</Step>
|
||
<Step>
|
||
<StepLabel>تکمیل ابهام ها</StepLabel>
|
||
</Step>
|
||
<Step>
|
||
<StepLabel>برنامه نهایی</StepLabel>
|
||
</Step>
|
||
</Stepper>
|
||
</Stack>
|
||
</CardContent>
|
||
</Card>
|
||
</Grid>
|
||
</Grid>
|
||
</CardContent>
|
||
</Card>
|
||
</Grid>
|
||
|
||
<Grid size={{ xs: 12, lg: 7 }}>
|
||
<Card sx={{ borderRadius: 5 }}>
|
||
<CardContent sx={{ p: { xs: 4, md: 5 } }}>
|
||
<Stack spacing={4}>
|
||
<Box>
|
||
{!singleTabMode ? (
|
||
<Tabs
|
||
value={activeTab}
|
||
onChange={(_, value: ParserTabKey) => setActiveTab(value)}
|
||
variant="fullWidth"
|
||
sx={{
|
||
p: 1,
|
||
borderRadius: 4,
|
||
backgroundColor: alpha(theme.palette.primary.main, 0.05),
|
||
minHeight: 64,
|
||
"& .MuiTabs-indicator": { display: "none" },
|
||
}}
|
||
>
|
||
{availableTabs.map((tabKey) => {
|
||
const tabConfig = PARSER_CONFIGS[tabKey];
|
||
|
||
return (
|
||
<Tab
|
||
key={tabConfig.key}
|
||
value={tabConfig.key}
|
||
icon={<i className={tabConfig.icon} />}
|
||
iconPosition="start"
|
||
label={tabConfig.label}
|
||
sx={{
|
||
minHeight: 54,
|
||
borderRadius: 3,
|
||
fontWeight: 700,
|
||
transition: "all 0.2s ease",
|
||
"&.Mui-selected": {
|
||
color: theme.palette.common.white,
|
||
background: `linear-gradient(135deg, ${theme.palette.primary.main} 0%, ${theme.palette.info.main} 100%)`,
|
||
boxShadow: `0 14px 32px ${alpha(theme.palette.primary.main, 0.24)}`,
|
||
},
|
||
}}
|
||
/>
|
||
);
|
||
})}
|
||
</Tabs>
|
||
) : null}
|
||
</Box>
|
||
|
||
<Box
|
||
display="flex"
|
||
justifyContent="space-between"
|
||
alignItems="flex-start"
|
||
gap={3}
|
||
flexWrap="wrap"
|
||
>
|
||
<Box>
|
||
<Typography variant="h4" sx={{ fontWeight: 800, mb: 1 }}>
|
||
{config.panelTitle}
|
||
</Typography>
|
||
<Typography color="text.secondary" sx={{ maxWidth: 700 }}>
|
||
{config.panelDescription}
|
||
</Typography>
|
||
</Box>
|
||
<OptionMenu
|
||
icon="tabler-dots"
|
||
options={[
|
||
{
|
||
text: `پر کردن نمونه ${config.label}`,
|
||
icon: config.icon,
|
||
menuItemProps: {
|
||
onClick: () =>
|
||
updateTabState(activeTab, (previous) => ({
|
||
...previous,
|
||
message: config.samplePrompts[0],
|
||
})),
|
||
},
|
||
},
|
||
{
|
||
text: "کپی JSON",
|
||
icon: "tabler-copy",
|
||
menuItemProps: {
|
||
onClick: handleCopyJson,
|
||
},
|
||
},
|
||
{
|
||
text: "شروع دوباره",
|
||
icon: "tabler-rotate-clockwise-2",
|
||
menuItemProps: {
|
||
onClick: () => handleReset(activeTab),
|
||
},
|
||
},
|
||
]}
|
||
/>
|
||
</Box>
|
||
|
||
{!farmUuid && (
|
||
<Alert severity="warning">
|
||
مزرعه فعالی پیدا نشد. صفحه هنوز کار می کند، اما اگر
|
||
`farm_uuid` فعال باشد پاسخ API دقیق تر می شود.
|
||
</Alert>
|
||
)}
|
||
|
||
<CustomTextField
|
||
fullWidth
|
||
multiline
|
||
minRows={7}
|
||
label={config.inputLabel}
|
||
placeholder={config.inputPlaceholder}
|
||
value={currentState.message}
|
||
onChange={(event) =>
|
||
updateTabState(activeTab, (previous) => ({
|
||
...previous,
|
||
message: event.target.value,
|
||
}))
|
||
}
|
||
/>
|
||
|
||
<Box>
|
||
<Typography
|
||
variant="body2"
|
||
sx={{ mb: 2, color: "text.secondary" }}
|
||
>
|
||
نمونه های آماده برای شروع سریع
|
||
</Typography>
|
||
<Stack
|
||
direction="row"
|
||
spacing={1.5}
|
||
useFlexGap
|
||
flexWrap="wrap"
|
||
>
|
||
{config.samplePrompts.map((prompt) => (
|
||
<Chip
|
||
key={prompt}
|
||
label={prompt}
|
||
onClick={() =>
|
||
updateTabState(activeTab, (previous) => ({
|
||
...previous,
|
||
message: prompt,
|
||
}))
|
||
}
|
||
variant="outlined"
|
||
sx={{
|
||
justifyContent: "flex-start",
|
||
px: 1,
|
||
py: 2.75,
|
||
maxWidth: "100%",
|
||
height: "auto",
|
||
borderRadius: 3,
|
||
backgroundColor: alpha(
|
||
theme.palette.success.main,
|
||
0.05,
|
||
),
|
||
"& .MuiChip-label": {
|
||
display: "block",
|
||
whiteSpace: "normal",
|
||
},
|
||
}}
|
||
/>
|
||
))}
|
||
</Stack>
|
||
</Box>
|
||
|
||
<Stack direction={{ xs: "column", sm: "row" }} spacing={2}>
|
||
<Button
|
||
variant="contained"
|
||
size="large"
|
||
onClick={handleGeneratePlan}
|
||
disabled={currentState.loading}
|
||
startIcon={<i className="tabler-sparkles" />}
|
||
sx={{ px: 5, py: 1.8, borderRadius: 3 }}
|
||
>
|
||
تحلیل متن و ساخت برنامه
|
||
</Button>
|
||
<Button
|
||
variant="tonal"
|
||
size="large"
|
||
onClick={() => handleReset(activeTab)}
|
||
disabled={currentState.loading}
|
||
startIcon={<i className="tabler-refresh" />}
|
||
sx={{ px: 4, py: 1.8, borderRadius: 3 }}
|
||
>
|
||
پاک کردن جریان
|
||
</Button>
|
||
</Stack>
|
||
|
||
{currentState.loading && (
|
||
<LinearProgress sx={{ borderRadius: 999 }} />
|
||
)}
|
||
|
||
{currentState.requestError && (
|
||
<Alert severity="error">{currentState.requestError}</Alert>
|
||
)}
|
||
{currentState.statusNote && (
|
||
<Alert severity="success">{currentState.statusNote}</Alert>
|
||
)}
|
||
|
||
{currentState.response && (
|
||
<Card
|
||
variant="outlined"
|
||
sx={{
|
||
borderRadius: 4,
|
||
borderColor: alpha(theme.palette.primary.main, 0.16),
|
||
background: `linear-gradient(180deg, ${alpha(theme.palette.primary.main, 0.04)} 0%, ${theme.palette.background.paper} 100%)`,
|
||
}}
|
||
>
|
||
<CardContent>
|
||
<Stack spacing={3}>
|
||
<Box
|
||
display="flex"
|
||
alignItems="center"
|
||
justifyContent="space-between"
|
||
gap={2}
|
||
flexWrap="wrap"
|
||
>
|
||
<Box display="flex" alignItems="center" gap={2}>
|
||
<CustomAvatar
|
||
skin="light-static"
|
||
color={statusMeta.color}
|
||
size={48}
|
||
>
|
||
<i className={statusMeta.icon} />
|
||
</CustomAvatar>
|
||
<Box>
|
||
<Typography variant="h6" sx={{ fontWeight: 800 }}>
|
||
{currentState.response.status_fa}
|
||
</Typography>
|
||
<Typography color="text.secondary">
|
||
{currentState.response.summary}
|
||
</Typography>
|
||
</Box>
|
||
</Box>
|
||
<Chip
|
||
label={statusMeta.label}
|
||
color={statusMeta.color}
|
||
variant="tonal"
|
||
/>
|
||
</Box>
|
||
|
||
{currentState.response.status ===
|
||
"needs_clarification" &&
|
||
currentState.response.questions.length > 0 &&
|
||
currentQuestion && (
|
||
<Stack spacing={3}>
|
||
<Divider />
|
||
<Box>
|
||
<Typography
|
||
variant="h5"
|
||
sx={{ fontWeight: 800, mb: 1 }}
|
||
>
|
||
مرحله تکمیل ابهام ها
|
||
</Typography>
|
||
<Typography color="text.secondary">
|
||
سوال ها را یکی یکی جلو ببر؛ بعد از تکمیل، همان
|
||
endpoint با `answers` و `partial_plan` دوباره
|
||
صدا زده می شود.
|
||
</Typography>
|
||
</Box>
|
||
<Stepper
|
||
activeStep={currentState.activeQuestion}
|
||
alternativeLabel
|
||
>
|
||
{currentState.response.questions.map(
|
||
(question) => (
|
||
<Step key={question.id}>
|
||
<StepLabel>
|
||
{config.fieldLabels[question.field] ??
|
||
question.field}
|
||
</StepLabel>
|
||
</Step>
|
||
),
|
||
)}
|
||
</Stepper>
|
||
<Card
|
||
sx={{
|
||
borderRadius: 4,
|
||
border: `1px solid ${alpha(theme.palette.warning.main, 0.24)}`,
|
||
backgroundColor: alpha(
|
||
theme.palette.warning.main,
|
||
0.06,
|
||
),
|
||
}}
|
||
>
|
||
<CardContent>
|
||
<Stack spacing={3}>
|
||
<Box
|
||
display="flex"
|
||
alignItems="center"
|
||
gap={2}
|
||
>
|
||
<CustomAvatar
|
||
color="warning"
|
||
skin="light-static"
|
||
size={42}
|
||
>
|
||
<i className="tabler-message-question" />
|
||
</CustomAvatar>
|
||
<Box>
|
||
<Typography sx={{ fontWeight: 700 }}>
|
||
سوال{" "}
|
||
{formatNumber(
|
||
currentState.activeQuestion + 1,
|
||
)}{" "}
|
||
از{" "}
|
||
{formatNumber(
|
||
currentState.response.questions
|
||
.length,
|
||
)}
|
||
</Typography>
|
||
<Typography color="text.secondary">
|
||
{config.fieldLabels[
|
||
currentQuestion.field
|
||
] ?? currentQuestion.field}
|
||
</Typography>
|
||
</Box>
|
||
</Box>
|
||
<Box>
|
||
<Typography
|
||
variant="h5"
|
||
sx={{ fontWeight: 800, mb: 1.5 }}
|
||
>
|
||
{currentQuestion.question}
|
||
</Typography>
|
||
<Typography color="text.secondary">
|
||
{currentQuestion.rationale}
|
||
</Typography>
|
||
</Box>
|
||
<CustomTextField
|
||
fullWidth
|
||
label="پاسخ کاربر"
|
||
value={
|
||
currentState.answers[
|
||
currentQuestion.field
|
||
] ?? ""
|
||
}
|
||
onChange={(event) =>
|
||
updateTabState(
|
||
activeTab,
|
||
(previous) => ({
|
||
...previous,
|
||
answers: {
|
||
...previous.answers,
|
||
[currentQuestion.field]:
|
||
event.target.value,
|
||
},
|
||
}),
|
||
)
|
||
}
|
||
placeholder="پاسخ را اینجا بنویس..."
|
||
/>
|
||
<Stack
|
||
direction={{ xs: "column", sm: "row" }}
|
||
spacing={2}
|
||
justifyContent="space-between"
|
||
>
|
||
<Button
|
||
variant="tonal"
|
||
disabled={
|
||
currentState.activeQuestion === 0
|
||
}
|
||
onClick={() =>
|
||
updateTabState(
|
||
activeTab,
|
||
(previous) => ({
|
||
...previous,
|
||
activeQuestion: Math.max(
|
||
previous.activeQuestion - 1,
|
||
0,
|
||
),
|
||
}),
|
||
)
|
||
}
|
||
startIcon={
|
||
<i className="tabler-arrow-right" />
|
||
}
|
||
sx={{ borderRadius: 3 }}
|
||
>
|
||
سوال قبل
|
||
</Button>
|
||
<Stack
|
||
direction={{ xs: "column", sm: "row" }}
|
||
spacing={2}
|
||
>
|
||
<Button
|
||
variant="outlined"
|
||
disabled={
|
||
currentState.activeQuestion >=
|
||
currentState.response.questions
|
||
.length -
|
||
1
|
||
}
|
||
onClick={() =>
|
||
updateTabState(
|
||
activeTab,
|
||
(previous) => ({
|
||
...previous,
|
||
activeQuestion: Math.min(
|
||
previous.activeQuestion + 1,
|
||
currentState.response
|
||
?.questions.length
|
||
? currentState.response
|
||
.questions.length - 1
|
||
: 0,
|
||
),
|
||
}),
|
||
)
|
||
}
|
||
endIcon={
|
||
<i className="tabler-arrow-left" />
|
||
}
|
||
sx={{ borderRadius: 3 }}
|
||
>
|
||
سوال بعد
|
||
</Button>
|
||
<Button
|
||
variant="contained"
|
||
onClick={handleSubmitAnswers}
|
||
disabled={currentState.loading}
|
||
endIcon={
|
||
<i className="tabler-send" />
|
||
}
|
||
sx={{ borderRadius: 3 }}
|
||
>
|
||
ارسال پاسخ ها
|
||
</Button>
|
||
</Stack>
|
||
</Stack>
|
||
</Stack>
|
||
</CardContent>
|
||
</Card>
|
||
</Stack>
|
||
)}
|
||
</Stack>
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
</Stack>
|
||
</CardContent>
|
||
</Card>
|
||
</Grid>
|
||
|
||
<Grid size={{ xs: 12, lg: 5 }}>
|
||
<Stack spacing={4}>
|
||
<Grid container spacing={3}>
|
||
{statCards.map((card) => (
|
||
<Grid key={card.title} size={{ xs: 12, sm: 4, lg: 12 }}>
|
||
<HorizontalWithAvatar
|
||
stats={card.stats}
|
||
title={card.title}
|
||
avatarIcon={card.avatarIcon}
|
||
avatarColor={card.avatarColor}
|
||
avatarSkin="light"
|
||
avatarVariant="rounded"
|
||
avatarSize={42}
|
||
/>
|
||
</Grid>
|
||
))}
|
||
</Grid>
|
||
|
||
<Card sx={{ borderRadius: 5, overflow: "hidden" }}>
|
||
<CardContent sx={{ p: 0 }}>
|
||
<Box
|
||
sx={{
|
||
px: 4,
|
||
py: 3,
|
||
color: "common.white",
|
||
background: `linear-gradient(135deg, ${theme.palette.secondary.dark} 0%, ${theme.palette.primary.main} 100%)`,
|
||
}}
|
||
>
|
||
<Typography variant="h5" sx={{ fontWeight: 800, mb: 1 }}>
|
||
پیش نمایش زنده داده ساختاریافته
|
||
</Typography>
|
||
<Typography
|
||
sx={{ color: alpha(theme.palette.common.white, 0.78) }}
|
||
>
|
||
این بخش نشان می دهد سیستم تا این لحظه چه چیزهایی را فهمیده
|
||
است.
|
||
</Typography>
|
||
</Box>
|
||
<Box sx={{ px: 4, py: 3 }}>
|
||
<Box sx={{ mb: 3 }}>
|
||
<Box
|
||
display="flex"
|
||
justifyContent="space-between"
|
||
alignItems="center"
|
||
mb={1.5}
|
||
>
|
||
<Typography sx={{ fontWeight: 700 }}>
|
||
درصد تکمیل برنامه
|
||
</Typography>
|
||
<Typography color="text.secondary">
|
||
{formatNumber(completionValue)}٪
|
||
</Typography>
|
||
</Box>
|
||
<LinearProgress
|
||
variant="determinate"
|
||
value={completionValue}
|
||
sx={{ height: 10, borderRadius: 999 }}
|
||
/>
|
||
</Box>
|
||
|
||
{planPreview ? (
|
||
<Stack spacing={2}>
|
||
{config.previewOrder.map((field) => {
|
||
const value = config.getFieldValue(planPreview, field);
|
||
|
||
return (
|
||
<Box
|
||
key={field}
|
||
sx={{
|
||
p: 2.5,
|
||
borderRadius: 3,
|
||
border: `1px solid ${alpha(theme.palette.divider, 0.8)}`,
|
||
backgroundColor: alpha(
|
||
theme.palette.background.default,
|
||
0.55,
|
||
),
|
||
}}
|
||
>
|
||
<Typography
|
||
variant="body2"
|
||
color="text.secondary"
|
||
sx={{ mb: 0.75 }}
|
||
>
|
||
{config.fieldLabels[field] ?? field}
|
||
</Typography>
|
||
<Typography sx={{ fontWeight: 700 }}>
|
||
{config.formatFieldValue(field, value)}
|
||
</Typography>
|
||
</Box>
|
||
);
|
||
})}
|
||
</Stack>
|
||
) : (
|
||
<Alert severity="info">
|
||
هنوز خروجی ای نداریم. متن را ارسال کن تا collected_data و
|
||
سپس final_plan اینجا شکل بگیرد.
|
||
</Alert>
|
||
)}
|
||
</Box>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{primaryItem !== null && primaryItem !== undefined && (
|
||
<Card sx={{ borderRadius: 5 }}>
|
||
<CardContent sx={{ p: { xs: 4, md: 4.5 } }}>
|
||
<Stack spacing={3}>
|
||
<Box
|
||
display="flex"
|
||
justifyContent="space-between"
|
||
alignItems="flex-start"
|
||
gap={2}
|
||
>
|
||
<Box>
|
||
<Typography
|
||
variant="h5"
|
||
sx={{ fontWeight: 800, mb: 1 }}
|
||
>
|
||
{config.primaryCardTitle}
|
||
</Typography>
|
||
<Typography color="text.secondary">
|
||
{config.getPrimaryHeadline(primaryItem) ||
|
||
config.primaryFallbackTitle}
|
||
</Typography>
|
||
</Box>
|
||
<Chip
|
||
label={
|
||
currentState.response?.status === "completed"
|
||
? "قابل اجرا"
|
||
: "در حال تکمیل"
|
||
}
|
||
color={
|
||
currentState.response?.status === "completed"
|
||
? "success"
|
||
: "warning"
|
||
}
|
||
variant="tonal"
|
||
/>
|
||
</Box>
|
||
|
||
<Grid container spacing={2}>
|
||
{config.getPrimaryMeta(primaryItem).map((item) => (
|
||
<Grid key={item.key} size={{ xs: 12, sm: 6 }}>
|
||
<Box
|
||
sx={{
|
||
p: 2.5,
|
||
borderRadius: 4,
|
||
backgroundColor: alpha(
|
||
theme.palette.success.main,
|
||
0.06,
|
||
),
|
||
border: `1px solid ${alpha(theme.palette.success.main, 0.15)}`,
|
||
}}
|
||
>
|
||
<Typography
|
||
variant="body2"
|
||
color="text.secondary"
|
||
sx={{ mb: 0.75 }}
|
||
>
|
||
{item.label}
|
||
</Typography>
|
||
<Typography sx={{ fontWeight: 700 }}>
|
||
{item.value || "—"}
|
||
</Typography>
|
||
</Box>
|
||
</Grid>
|
||
))}
|
||
</Grid>
|
||
</Stack>
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
|
||
{currentState.response?.final_plan && (
|
||
<Card sx={{ borderRadius: 5 }}>
|
||
<CardContent sx={{ p: { xs: 4, md: 4.5 } }}>
|
||
<Stack spacing={3}>
|
||
<Box
|
||
display="flex"
|
||
justifyContent="space-between"
|
||
alignItems="center"
|
||
gap={2}
|
||
flexWrap="wrap"
|
||
>
|
||
<Box>
|
||
<Typography
|
||
variant="h5"
|
||
sx={{ fontWeight: 800, mb: 1 }}
|
||
>
|
||
{config.finalCardTitle}
|
||
</Typography>
|
||
<Typography color="text.secondary">
|
||
{config.finalCardDescription}
|
||
</Typography>
|
||
</Box>
|
||
<Button
|
||
variant="outlined"
|
||
onClick={handleCopyJson}
|
||
startIcon={<i className="tabler-copy" />}
|
||
sx={{ borderRadius: 3 }}
|
||
>
|
||
کپی JSON
|
||
</Button>
|
||
</Box>
|
||
<Box
|
||
component="pre"
|
||
sx={{
|
||
m: 0,
|
||
p: 3,
|
||
overflow: "auto",
|
||
borderRadius: 4,
|
||
fontSize: 13,
|
||
lineHeight: 1.9,
|
||
color: theme.palette.grey[100],
|
||
backgroundColor: theme.palette.grey[900],
|
||
}}
|
||
>
|
||
{createJsonSnapshot(currentState.response.final_plan)}
|
||
</Box>
|
||
</Stack>
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
</Stack>
|
||
</Grid>
|
||
</Grid>
|
||
</Box>
|
||
);
|
||
};
|
||
|
||
export default FertilizationPlanParserPage;
|