This commit is contained in:
2026-04-01 17:28:05 +03:30
parent 1d4080a8f5
commit bde110868a
18 changed files with 2679 additions and 1002 deletions
+180 -68
View File
@@ -3,138 +3,250 @@
* @see CROP_ZONING_APIS.md
*/
import type { Feature, FeatureCollection, Polygon } from 'geojson'
import { apiClient } from '../client'
import type { Feature, FeatureCollection, Polygon } from "geojson";
import { apiClient } from "../client";
import type {
RecommendationTaskInitResponse,
RecommendationTaskStatus,
RecommendationTaskStatusResponse,
} from "./recommendationTask";
import { normalizeRecommendationTaskStatus } from "./recommendationTask";
const PREFIX = '/api/crop-zoning'
const PREFIX = "/api/crop-zoning";
export interface Product {
id: string
label: string
color: string
id: string;
label: string;
color: string;
}
export interface ZoneInitialData {
zoneId: string
geometry: Polygon
zoneId: string;
geometry: Polygon;
/** اگر null/خالی/uncultivable باشد، زون غیرقابل کشت و خاکستری نمایش داده می‌شود */
crop?: string | null
matchPercent?: number | null
waterNeed?: string | null
estimatedProfit?: string | null
crop?: string | null;
matchPercent?: number | null;
waterNeed?: string | null;
estimatedProfit?: string | null;
}
export interface ZonesInitialResponse {
total_area_hectares: number
total_area_sqm: number
zone_count: number
zones: ZoneInitialData[]
total_area_hectares: number;
total_area_sqm: number;
zone_count: number;
zones: ZoneInitialData[];
}
export interface AreaResponse {
area: Feature<Polygon>
area: Feature<Polygon> | null;
}
export interface CropZoningAreaTask {
status?:
| "IDLE"
| "PENDING"
| "PROCESSING"
| "SUCCESS"
| "FAILURE"
| "pending"
| "processing"
| "success"
| "completed"
| "failure"
| "failed";
stage?: string;
stage_label?: string;
area_uuid?: string;
total_zones?: number;
completed_zones?: number;
processing_zones?: number;
pending_zones?: number;
failed_zones?: number;
remaining_zones?: number;
progress_percent?: number;
message?: string;
failed_zone_errors?: string[];
cell_side_km?: number;
}
export interface CropZoningAreaResult extends AreaResponse {
status?: string;
task?: CropZoningAreaTask | null;
zones?: ZoneInitialData[];
}
export type CropZoningAreaResponse =
| CropZoningAreaResult
| RecommendationTaskInitResponse;
export interface ZoneDetailData {
zoneId: string
crop: string
matchPercent: number
waterNeed: string
estimatedProfit: string
reason: string
criteria: { name: string; value: number }[]
area_hectares?: number
zoneId: string;
crop: string;
matchPercent: number;
waterNeed: string;
estimatedProfit: string;
reason: string;
criteria: { name: string; value: number }[];
area_hectares?: number;
}
/** دیتای نیاز آبی هر زون — لایهٔ نیاز آبی */
export interface ZoneWaterNeedData {
zoneId: string
geometry: Polygon
level: 'low' | 'medium' | 'high'
value?: string
color: string
zoneId: string;
geometry: Polygon;
level: "low" | "medium" | "high";
value?: string;
color: string;
}
/** دیتای کیفیت خاک هر زون — لایهٔ کیفیت خاک */
export interface ZoneSoilQualityData {
zoneId: string
geometry: Polygon
level: 'low' | 'medium' | 'high'
score?: number
color: string
zoneId: string;
geometry: Polygon;
level: "low" | "medium" | "high";
score?: number;
color: string;
}
/** دیتای ریسک کشت هر زون — لایهٔ ریسک کشت */
export interface ZoneCultivationRiskData {
zoneId: string
geometry: Polygon
level: 'low' | 'medium' | 'high'
color: string
zoneId: string;
geometry: Polygon;
level: "low" | "medium" | "high";
color: string;
}
/** دادهٔ نمایشی هر زون روی نقشه — خروجی تبدیل از تمام لایه‌ها */
export interface ZoneMapData {
zoneId: string
geometry: Polygon
color: string
tooltipContent: string
cultivable: boolean
zoneInitialData?: ZoneInitialData
zoneId: string;
geometry: Polygon;
color: string;
tooltipContent: string;
cultivable: boolean;
zoneInitialData?: ZoneInitialData;
}
interface ApiResponse<T> {
status: string
data: T
status: string;
data: T;
}
async function unwrap<T>(promise: Promise<ApiResponse<T>>): Promise<T> {
const res = await promise
return res.data
const res = await promise;
return res.data;
}
function normalizeTaskInitResponse(
task: RecommendationTaskInitResponse,
): RecommendationTaskInitResponse {
return {
...task,
status: normalizeRecommendationTaskStatus(task.status),
};
}
function normalizeAreaResult(
result: CropZoningAreaResult,
): CropZoningAreaResult {
return {
...result,
task: result.task
? {
...result.task,
status: normalizeRecommendationTaskStatus(result.task.status),
}
: result.task,
};
}
export const cropZoningService = {
getProducts(): Promise<{ products: Product[] }> {
return unwrap(apiClient.get<ApiResponse<{ products: Product[] }>>(`${PREFIX}/products/`))
return unwrap(
apiClient.get<ApiResponse<{ products: Product[] }>>(
`${PREFIX}/products/`,
),
);
},
getZonesInitial(body: {
zones: FeatureCollection<Polygon>
products?: string[]
zones: FeatureCollection<Polygon>;
products?: string[];
}): Promise<ZonesInitialResponse> {
return unwrap(apiClient.post<ApiResponse<ZonesInitialResponse>>(`${PREFIX}/zones/initial/`, body))
return unwrap(
apiClient.post<ApiResponse<ZonesInitialResponse>>(
`${PREFIX}/zones/initial/`,
body,
),
);
},
getZoneDetails(zoneId: string): Promise<ZoneDetailData> {
return unwrap(apiClient.get<ApiResponse<ZoneDetailData>>(`${PREFIX}/zones/${zoneId}/details/`))
return unwrap(
apiClient.get<ApiResponse<ZoneDetailData>>(
`${PREFIX}/zones/${zoneId}/details/`,
),
);
},
getArea(): Promise<AreaResponse> {
return unwrap(apiClient.get<ApiResponse<AreaResponse>>(`${PREFIX}/area/`))
getArea(sensorUuid: string): Promise<CropZoningAreaResponse> {
return unwrap(
apiClient.get<ApiResponse<CropZoningAreaResponse>>(`${PREFIX}/area/?sensor_uuid=${sensorUuid}`),
).then((response) =>
"task_id" in response
? normalizeTaskInitResponse(response)
: normalizeAreaResult(response),
);
},
getAreaStatus(
taskId: string,
): Promise<RecommendationTaskStatusResponse<CropZoningAreaResult>> {
return unwrap(
apiClient.get<
ApiResponse<RecommendationTaskStatusResponse<CropZoningAreaResult>>
>(`${PREFIX}/area/status/${taskId}/`),
).then((response) => ({
...response,
status: normalizeRecommendationTaskStatus(response.status),
result: response.result
? normalizeAreaResult(response.result)
: undefined,
}));
},
/** نیاز آبی هر منطقه — برای لایهٔ نیاز آبی */
getZonesWaterNeed(body: { zones: FeatureCollection<Polygon> }): Promise<{ zones: ZoneWaterNeedData[] }> {
getZonesWaterNeed(body: {
zones: FeatureCollection<Polygon>;
}): Promise<{ zones: ZoneWaterNeedData[] }> {
return unwrap(
apiClient.post<ApiResponse<{ zones: ZoneWaterNeedData[] }>>(`${PREFIX}/zones/water-need/`, body)
)
apiClient.post<ApiResponse<{ zones: ZoneWaterNeedData[] }>>(
`${PREFIX}/zones/water-need/`,
body,
),
);
},
/** کیفیت خاک هر منطقه — برای لایهٔ کیفیت خاک */
getZonesSoilQuality(body: { zones: FeatureCollection<Polygon> }): Promise<{ zones: ZoneSoilQualityData[] }> {
getZonesSoilQuality(body: {
zones: FeatureCollection<Polygon>;
}): Promise<{ zones: ZoneSoilQualityData[] }> {
return unwrap(
apiClient.post<ApiResponse<{ zones: ZoneSoilQualityData[] }>>(`${PREFIX}/zones/soil-quality/`, body)
)
apiClient.post<ApiResponse<{ zones: ZoneSoilQualityData[] }>>(
`${PREFIX}/zones/soil-quality/`,
body,
),
);
},
/** ریسک کشت هر منطقه — برای لایهٔ ریسک کشت */
getZonesCultivationRisk(body: {
zones: FeatureCollection<Polygon>
zones: FeatureCollection<Polygon>;
}): Promise<{ zones: ZoneCultivationRiskData[] }> {
return unwrap(
apiClient.post<ApiResponse<{ zones: ZoneCultivationRiskData[] }>>(
`${PREFIX}/zones/cultivation-risk/`,
body
)
)
}
}
body,
),
);
},
};
+67 -20
View File
@@ -1,10 +1,5 @@
/**
* Farm AI Assistant API
* GET context (farm bar data), POST chat (user message + optional farm_context/images).
*/
import { apiClient } from '../client'
import type { FarmContext } from '@views/dashboards/farm/farmAiAssistant/farmAiAssistantTypes'
import type { ConversationSummary, FarmContext } from '@views/dashboards/farm/farmAiAssistant/farmAiAssistantTypes'
const PREFIX = '/api/farm-ai-assistant'
@@ -29,17 +24,57 @@ export interface ChatSection {
}
export interface ChatPayload {
content: string
farm_context?: FarmContext
content?: string
images?: string[]
conversation_id?: string
title?: string
farm_context?: Partial<FarmContext>
}
export interface ChatResponseData {
export interface ChatTaskInitResponse {
task_id: string
status: 'PENDING' | 'STARTED' | 'SUCCESS' | 'FAILURE'
status_url?: string
conversation_id: string
message_id: string
}
export interface ChatTaskStatusResponse {
task_id: string
status: 'PENDING' | 'STARTED' | 'SUCCESS' | 'FAILURE'
conversation_id: string
progress?: {
message?: string
}
result?: ChatMessageResponse
error?: string
}
export interface ChatMessageResponse {
message_id: string
conversation_id: string
role: 'user' | 'assistant'
content: string
sections: ChatSection[]
images?: string[]
created_at?: string
}
export interface ConversationMessagesResponse {
conversation_id: string
messages: ChatMessageResponse[]
}
export interface CreateConversationPayload {
title?: string
farm_context?: Partial<FarmContext>
}
export interface CreateConversationResponse {
id: string
message_count: number
title?: string
updated_at?: string
}
interface ApiResponse<T> {
@@ -52,21 +87,33 @@ function unwrap<T>(res: ApiResponse<T>): T {
}
export const farmAiAssistantService = {
/**
* Returns farm context for the context bar (soilType, waterEC, selectedCrop, growthStage, lastIrrigationStatus).
*/
getContext(): Promise<FarmContextResponse> {
return apiClient
.get<ApiResponse<FarmContextResponse>>(`${PREFIX}/context/`)
.then(unwrap)
return apiClient.get<ApiResponse<FarmContextResponse>>(`${PREFIX}/context/`).then(unwrap)
},
/**
* Send user message (and optional farm_context, images, conversation_id). Returns message with sections.
*/
chat(payload: ChatPayload): Promise<ChatResponseData> {
createChatTask(payload: ChatPayload): Promise<ChatTaskInitResponse> {
return apiClient.post<ApiResponse<ChatTaskInitResponse>>(`${PREFIX}/chat/task/`, payload).then(unwrap)
},
getChatTaskStatus(taskId: string): Promise<ChatTaskStatusResponse> {
return apiClient.get<ApiResponse<ChatTaskStatusResponse>>(`${PREFIX}/chat/task/${taskId}/status/`).then(unwrap)
},
getConversations(): Promise<ConversationSummary[]> {
return apiClient.get<ApiResponse<ConversationSummary[]>>(`${PREFIX}/chats/`).then(unwrap)
},
createConversation(payload?: CreateConversationPayload): Promise<CreateConversationResponse> {
return apiClient.post<ApiResponse<CreateConversationResponse>>(`${PREFIX}/chats/`, payload).then(unwrap)
},
deleteConversation(conversationId: string): Promise<{ conversation_id: string }> {
return apiClient.delete<ApiResponse<{ conversation_id: string }>>(`${PREFIX}/chats/${conversationId}/`).then(unwrap)
},
getConversationMessages(conversationId: string): Promise<ConversationMessagesResponse> {
return apiClient
.post<ApiResponse<ChatResponseData>>(`${PREFIX}/chat/`, payload)
.get<ApiResponse<ConversationMessagesResponse>>(`${PREFIX}/chats/${conversationId}/messages/`)
.then(unwrap)
}
}
@@ -3,65 +3,135 @@
* @see RECOMMENDATION_APIS.md
*/
import { apiClient } from '../client'
import { apiClient } from "../client";
import type {
RecommendationTaskInitResponse,
RecommendationTaskStatusResponse,
} from "./recommendationTask";
import { normalizeRecommendationTaskStatus } from "./recommendationTask";
const PREFIX = '/api/fertilization-recommendation'
const PREFIX = "/api/fertilization-recommendation";
export interface FarmData {
soilType: string
organicMatter: string
waterEC: string
soilType: string;
organicMatter: string;
waterEC: string;
}
export interface GrowthStage {
id: string
icon: string
id: string;
icon: string;
}
export interface CropOption {
id: string
labelKey: string
icon: string
id: string;
labelKey: string;
icon: string;
}
export interface FertilizationConfigResponse {
farmData: FarmData
growthStages: GrowthStage[]
cropOptions: CropOption[]
farmData: FarmData;
growthStages: GrowthStage[];
cropOptions: CropOption[];
}
export interface FertilizationPlan {
npkRatio: string
amountPerHectare: string
applicationMethod: string
applicationInterval: string
reasoning: string
npkRatio: string;
amountPerHectare: string;
applicationMethod: string;
applicationInterval: string;
reasoning: string;
}
export interface FertilizationRecommendPayload {
crop_id?: string
growth_stage?: string
soilType?: string
organicMatter?: string
waterEC?: string
crop_id?: string;
growth_stage?: string;
farm_data?: Partial<FarmData>;
soilType?: string;
organicMatter?: string;
waterEC?: string;
}
export interface FertilizationRecommendationResult {
plan: FertilizationPlan;
status?: string;
}
export type FertilizationRecommendResponse =
| FertilizationRecommendationResult
| RecommendationTaskInitResponse;
interface ApiResponse<T> {
status: string
data: T
status: string;
data: T;
}
async function unwrap<T>(promise: Promise<ApiResponse<T>>): Promise<T> {
const res = await promise
return res.data
const res = await promise;
return res.data;
}
function normalizeTaskInitResponse(
task: RecommendationTaskInitResponse,
): RecommendationTaskInitResponse {
return {
...task,
status: normalizeRecommendationTaskStatus(task.status),
};
}
function normalizeRecommendationResult(
result: FertilizationRecommendationResult,
): FertilizationRecommendationResult {
return result.status
? {
...result,
status: normalizeRecommendationTaskStatus(result.status),
}
: result;
}
export const fertilizationRecommendationService = {
getConfig(): Promise<FertilizationConfigResponse> {
return unwrap(apiClient.get<ApiResponse<FertilizationConfigResponse>>(`${PREFIX}/config/`))
return unwrap(
apiClient.get<ApiResponse<FertilizationConfigResponse>>(
`${PREFIX}/config/`,
),
);
},
recommend(payload?: FertilizationRecommendPayload): Promise<{ plan: FertilizationPlan }> {
return unwrap(apiClient.post<ApiResponse<{ plan: FertilizationPlan }>>(`${PREFIX}/recommend/`, payload ?? {}))
recommend(
payload?: FertilizationRecommendPayload,
): Promise<FertilizationRecommendResponse> {
return unwrap(
apiClient.post<ApiResponse<FertilizationRecommendResponse>>(
`${PREFIX}/recommend/`,
payload ?? {},
),
).then((response) =>
"task_id" in response
? normalizeTaskInitResponse(response)
: normalizeRecommendationResult(response),
);
},
}
getRecommendStatus(
taskId: string,
): Promise<
RecommendationTaskStatusResponse<FertilizationRecommendationResult>
> {
return unwrap(
apiClient.get<
ApiResponse<
RecommendationTaskStatusResponse<FertilizationRecommendationResult>
>
>(`${PREFIX}/recommend/status/${taskId}/`),
).then((response) => ({
...response,
status: normalizeRecommendationTaskStatus(response.status),
result: response.result
? normalizeRecommendationResult(response.result)
: undefined,
}));
},
};
@@ -3,58 +3,147 @@
* @see RECOMMENDATION_APIS.md
*/
import { apiClient } from '../client'
import { apiClient } from "../client";
import type {
RecommendationTaskInitResponse,
RecommendationTaskStatusResponse,
} from "./recommendationTask";
import { normalizeRecommendationTaskStatus } from "./recommendationTask";
const PREFIX = '/api/irrigation-recommendation'
const PREFIX = "/api/irrigation-recommendation";
export interface FarmInfo {
soilType: string
waterQuality: string
climateZone: string
soilType: string;
waterQuality: string;
climateZone: string;
}
export interface CropOption {
id: string
labelKey: string
icon: string
id: string;
labelKey: string;
icon: string;
}
export interface IrrigationConfigResponse {
farmInfo: FarmInfo
cropOptions: CropOption[]
farmInfo: FarmInfo;
cropOptions: CropOption[];
}
export interface IrrigationPlan {
frequencyPerWeek: number
durationMinutes: number
bestTimeOfDay: string
moistureLevel: number
warning?: string
frequencyPerWeek: number | string;
durationMinutes: number | string;
bestTimeOfDay: string;
moistureLevel: number | string;
warning?: string;
}
export interface IrrigationRecommendPayload {
crop_id?: string
soilType?: string
waterQuality?: string
climateZone?: string
crop_id?: string;
farm_data?: Partial<FarmInfo>;
soilType?: string;
waterQuality?: string;
climateZone?: string;
}
export interface WaterBalanceDailyEntry {
forecast_date: string;
et0_mm: number;
etc_mm: number;
effective_rainfall_mm: number;
gross_irrigation_mm: number;
irrigation_timing: string;
}
export interface WaterBalanceCropProfile {
kc_initial: number;
kc_mid: number;
kc_end: number;
}
export interface WaterBalance {
daily: WaterBalanceDailyEntry[];
crop_profile?: WaterBalanceCropProfile;
active_kc?: number;
}
export interface IrrigationRecommendationResult {
plan: IrrigationPlan;
raw_response?: string;
water_balance?: WaterBalance;
status?: string;
}
export type IrrigationRecommendResponse =
| IrrigationRecommendationResult
| RecommendationTaskInitResponse;
interface ApiResponse<T> {
status: string
data: T
status: string;
data: T;
}
async function unwrap<T>(promise: Promise<ApiResponse<T>>): Promise<T> {
const res = await promise
return res.data
const res = await promise;
return res.data;
}
function normalizeTaskInitResponse(
task: RecommendationTaskInitResponse,
): RecommendationTaskInitResponse {
return {
...task,
status: normalizeRecommendationTaskStatus(task.status),
};
}
function normalizeRecommendationResult(
result: IrrigationRecommendationResult,
): IrrigationRecommendationResult {
return result.status
? {
...result,
status: normalizeRecommendationTaskStatus(result.status),
}
: result;
}
export const irrigationRecommendationService = {
getConfig(): Promise<IrrigationConfigResponse> {
return unwrap(apiClient.get<ApiResponse<IrrigationConfigResponse>>(`${PREFIX}/config/`))
return unwrap(
apiClient.get<ApiResponse<IrrigationConfigResponse>>(`${PREFIX}/config/`),
);
},
recommend(payload?: IrrigationRecommendPayload): Promise<{ plan: IrrigationPlan }> {
return unwrap(apiClient.post<ApiResponse<{ plan: IrrigationPlan }>>(`${PREFIX}/recommend/`, payload ?? {}))
recommend(
payload?: IrrigationRecommendPayload,
): Promise<IrrigationRecommendResponse> {
return unwrap(
apiClient.post<ApiResponse<IrrigationRecommendResponse>>(
`${PREFIX}/recommend/`,
payload ?? {},
),
).then((response) =>
"task_id" in response
? normalizeTaskInitResponse(response)
: normalizeRecommendationResult(response),
);
},
}
getRecommendStatus(
taskId: string,
): Promise<RecommendationTaskStatusResponse<IrrigationRecommendationResult>> {
return unwrap(
apiClient.get<
ApiResponse<
RecommendationTaskStatusResponse<IrrigationRecommendationResult>
>
>(`${PREFIX}/recommend/status/${taskId}/`),
).then((response) => ({
...response,
status: normalizeRecommendationTaskStatus(response.status),
result: response.result
? normalizeRecommendationResult(response.result)
: undefined,
}));
},
};
@@ -0,0 +1,43 @@
export type RecommendationTaskStatus =
| "pending"
| "processing"
| "completed"
| "failed";
export interface RecommendationTaskInitResponse {
task_id: string;
status: RecommendationTaskStatus;
}
export interface RecommendationTaskProgress {
message?: string;
}
export interface RecommendationTaskStatusResponse<T> {
task_id: string;
status: RecommendationTaskStatus;
progress?: RecommendationTaskProgress;
result?: T;
error?: string;
}
export const normalizeRecommendationTaskStatus = (
status?: string,
): RecommendationTaskStatus => {
switch (status?.toLowerCase()) {
case "started":
case "processing":
return "processing";
case "success":
case "completed":
return "completed";
case "failure":
case "failed":
return "failed";
default:
return "pending";
}
};
export const isRecommendationTaskRunning = (status: RecommendationTaskStatus) =>
status === "pending" || status === "processing";