2026-04-30 04:00:19 +03:30
|
|
|
|
"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;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-02 06:23:34 +03:30
|
|
|
|
type FertilizationPlanParserPageProps = {
|
|
|
|
|
|
initialTab?: ParserTabKey;
|
|
|
|
|
|
enabledTabs?: ParserTabKey[];
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-04-30 04:00:19 +03:30
|
|
|
|
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);
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-02 06:23:34 +03:30
|
|
|
|
const FertilizationPlanParserPage = ({
|
|
|
|
|
|
initialTab = "fertilization",
|
|
|
|
|
|
enabledTabs = ["fertilization", "irrigation"],
|
|
|
|
|
|
}: FertilizationPlanParserPageProps) => {
|
2026-04-30 04:00:19 +03:30
|
|
|
|
const theme = useTheme();
|
|
|
|
|
|
const { farmHub } = useFarmHub();
|
|
|
|
|
|
const farmUuid = farmHub?.farm_uuid;
|
2026-05-02 06:23:34 +03:30
|
|
|
|
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;
|
2026-04-30 04:00:19 +03:30
|
|
|
|
|
2026-05-02 06:23:34 +03:30
|
|
|
|
const [activeTab, setActiveTab] = useState<ParserTabKey>(resolvedInitialTab);
|
2026-04-30 04:00:19 +03:30
|
|
|
|
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 }}
|
|
|
|
|
|
>
|
2026-05-02 06:23:34 +03:30
|
|
|
|
{singleTabMode
|
|
|
|
|
|
? `جریان هوشمند ${config.label}`
|
|
|
|
|
|
: "دو جریان هوشمند، یک صفحه واحد"}
|
2026-04-30 04:00:19 +03:30
|
|
|
|
</Typography>
|
|
|
|
|
|
<Typography color="text.secondary">
|
2026-05-02 06:23:34 +03:30
|
|
|
|
{singleTabMode
|
|
|
|
|
|
? `ورودی متنی برنامه ${config.label} را بفرست، ابهام ها را کامل کن و خروجی JSON نهایی بگیر.`
|
|
|
|
|
|
: "بین تب آبیاری و تب کودهی جابه جا شو و هر کدام را با همان flow سوال های تکمیلی تا JSON نهایی پیش ببر."}
|
2026-04-30 04:00:19 +03:30
|
|
|
|
</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>
|
2026-05-02 06:23:34 +03:30
|
|
|
|
{!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}
|
2026-04-30 04:00:19 +03:30
|
|
|
|
</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;
|