cleanup
This commit is contained in:
parent
19f969c18c
commit
4bb7d214e8
4 changed files with 139 additions and 196 deletions
|
|
@ -10,11 +10,11 @@
|
|||
2. **Derived State (`$derived`)**:
|
||||
- Use camelCase.
|
||||
- No special prefixes are needed as `$derived` already marks them as reactive derived state.
|
||||
- Example: `let inputLat = $derived(...)`
|
||||
- Example: `let currentPoint = $derived(...)`
|
||||
|
||||
3. **Component Instance References**:
|
||||
- Use camelCase and suffix with `Ref`.
|
||||
- Example: `let PointEditorRef: PointEditor | null = null;`
|
||||
- Example: `let pointEditorRef: PointEditor | null = null;`
|
||||
|
||||
4. **Event Handlers**:
|
||||
- Use `handle<EventName>` or `handle<Element><Event>` naming.
|
||||
|
|
@ -34,7 +34,7 @@
|
|||
- Example: `import { SavedPointsStore } from '$lib/stores';`
|
||||
- The reactive Svelte store prefix `$` is used as standard.
|
||||
*/
|
||||
import { onMount } from "svelte";
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
|
|
@ -53,8 +53,14 @@
|
|||
import PointEditor from "$lib/components/PointEditor.svelte";
|
||||
import SelectSearchable from "$lib/components/SelectSearchable.svelte";
|
||||
import { getForecast } from "$lib/prediction";
|
||||
import { FlightParametersStore, SavedPointsStore, writeLocalStorage } from "$lib/stores";
|
||||
import { PROFILE_MAP, type FlightParameters, type ProfileName, type SavedPoint } from "$lib/types";
|
||||
import {
|
||||
FlightParametersStore,
|
||||
SavedPointsStore,
|
||||
writeLocalStorage,
|
||||
readLocalStorage,
|
||||
flightParametersDefaults,
|
||||
} from "$lib/stores";
|
||||
import { PROFILE_MAP, type FlightParameters, type SavedPoint } from "$lib/types";
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
|
|
@ -64,37 +70,30 @@
|
|||
|
||||
// State
|
||||
let isCollapsed = $state(false);
|
||||
let isPointDirty = $state(false);
|
||||
const now = new Date();
|
||||
let startDate = $state(now.toISOString().split("T")[0]);
|
||||
let startTime = $state(now.toISOString().split("T")[1].split(".")[0]);
|
||||
let selectedPointId = $state($FlightParametersStore.start_point);
|
||||
let startDate = $state(readLocalStorage<string>("startDate", new Date().toISOString().split("T")[0]));
|
||||
let startTime = $state(readLocalStorage<string>("startTime", "12:00:00"));
|
||||
let selectedPointId = $state($FlightParametersStore.start_point || -1);
|
||||
|
||||
// Component References
|
||||
let PointEditorRef: PointEditor | null = null;
|
||||
let pointEditorRef: PointEditor | null = null;
|
||||
|
||||
// Derived State
|
||||
let inputLat = $derived(
|
||||
$FlightParametersStore.start_point === -1
|
||||
? $FlightParametersStore.launch_latitude.toFixed(6)
|
||||
: $SavedPointsStore.find((point) => point.id === $FlightParametersStore.start_point)?.lat.toFixed(6) ||
|
||||
"0.000000",
|
||||
);
|
||||
let inputLng = $derived(
|
||||
$FlightParametersStore.start_point === -1
|
||||
? $FlightParametersStore.launch_longitude.toFixed(6)
|
||||
: $SavedPointsStore.find((point) => point.id === $FlightParametersStore.start_point)?.lon.toFixed(6) ||
|
||||
"0.000000",
|
||||
);
|
||||
let inputAlt = $derived(
|
||||
$FlightParametersStore.start_point === -1
|
||||
? $FlightParametersStore.launch_altitude.toFixed(2)
|
||||
: $SavedPointsStore.find((point) => point.id === $FlightParametersStore.start_point)?.alt.toFixed(2) ||
|
||||
"0.00",
|
||||
);
|
||||
let currentPoint = $derived($SavedPointsStore.find((p) => p.id === selectedPointId) || null);
|
||||
let isPointDirty = $derived(() => {
|
||||
if (!currentPoint) return false; // Not dirty if no point is selected
|
||||
const latMatch = $FlightParametersStore.launch_latitude.toFixed(6) === currentPoint.lat.toFixed(6);
|
||||
const lonMatch = $FlightParametersStore.launch_longitude.toFixed(6) === currentPoint.lon.toFixed(6);
|
||||
const altMatch = $FlightParametersStore.launch_altitude.toFixed(2) === currentPoint.alt.toFixed(2);
|
||||
return !(latMatch && lonMatch && altMatch);
|
||||
});
|
||||
|
||||
// Lifecycle Hooks
|
||||
onMount(() => {
|
||||
// NOTE: Consider moving localStorage logic into the store itself for better encapsulation.
|
||||
$FlightParametersStore =
|
||||
readLocalStorage<FlightParameters>("flightParameters", flightParametersDefaults) || $FlightParametersStore;
|
||||
selectedPointId = $FlightParametersStore.start_point || -1;
|
||||
|
||||
getSavedPoints()
|
||||
.then((points) => SavedPointsStore.set(points))
|
||||
.catch((error) => {
|
||||
|
|
@ -103,102 +102,91 @@
|
|||
body: `Failed to load saved points: ${error.message}`,
|
||||
color: "danger",
|
||||
});
|
||||
return [];
|
||||
});
|
||||
selectedPointId = $FlightParametersStore.start_point;
|
||||
});
|
||||
|
||||
function handleApplySelectedPoint() {
|
||||
if (selectedPointId && selectedPointId !== -1) {
|
||||
$FlightParametersStore.start_point = selectedPointId;
|
||||
isPointDirty = false;
|
||||
onDestroy(() => {
|
||||
writeLocalStorage<FlightParameters>("flightParameters", $FlightParametersStore);
|
||||
writeLocalStorage<string>("startDate", startDate);
|
||||
writeLocalStorage<string>("startTime", startTime);
|
||||
});
|
||||
|
||||
// Event Handlers
|
||||
function handlePointSelection(newPointId: number) {
|
||||
console.log("Point selection changed:", newPointId);
|
||||
selectedPointId = newPointId;
|
||||
const point = $SavedPointsStore.find((p) => p.id === newPointId);
|
||||
|
||||
if (point) {
|
||||
console.log("Selected point:", point);
|
||||
$FlightParametersStore.start_point = point.id;
|
||||
$FlightParametersStore.launch_latitude = point.lat;
|
||||
$FlightParametersStore.launch_longitude = point.lon;
|
||||
$FlightParametersStore.launch_altitude = point.alt;
|
||||
} else if (newPointId === -1) {
|
||||
$FlightParametersStore.start_point = -1;
|
||||
// When clearing the selection, we can reset to defaults or leave as is.
|
||||
// For now, we'll just update the ID. The user can manually edit coordinates.
|
||||
}
|
||||
}
|
||||
|
||||
export function handleSelectPointInModal(point: SavedPoint) {
|
||||
console.log("Selected point from modal:", point);
|
||||
selectedPointId = point.id;
|
||||
$FlightParametersStore.start_point = selectedPointId;
|
||||
isPointDirty = false;
|
||||
}
|
||||
|
||||
function handleSaveCurrentPoint() {
|
||||
if (selectedPointId !== -1) {
|
||||
const point = $SavedPointsStore.find((p) => p.id === selectedPointId);
|
||||
if (point) {
|
||||
point.lat = parseFloat(inputLat);
|
||||
point.lon = parseFloat(inputLng);
|
||||
point.alt = parseFloat(inputAlt);
|
||||
updatePoint(point)
|
||||
.then((updatedPoint) => {
|
||||
$SavedPointsStore = $SavedPointsStore.map((p) => (p.id === updatedPoint.id ? updatedPoint : p));
|
||||
SavedPointsStore.set($SavedPointsStore);
|
||||
addToast({
|
||||
header: "Точка обновлена",
|
||||
body: `Точка "${updatedPoint.name}" успешно обновлена.`,
|
||||
color: "success",
|
||||
});
|
||||
isPointDirty = false;
|
||||
})
|
||||
.catch((error) => {
|
||||
addToast({
|
||||
header: "Ошибка обновления точки",
|
||||
body: `Ошибка при обновлении точки: ${error.message}`,
|
||||
color: "danger",
|
||||
});
|
||||
console.error("Ошибка при обновлении точки:", error);
|
||||
if (currentPoint) {
|
||||
// Update existing point
|
||||
const updatedPointData = {
|
||||
...currentPoint,
|
||||
lat: $FlightParametersStore.launch_latitude,
|
||||
lon: $FlightParametersStore.launch_longitude,
|
||||
alt: $FlightParametersStore.launch_altitude,
|
||||
};
|
||||
updatePoint(updatedPointData)
|
||||
.then((savedPoint) => {
|
||||
SavedPointsStore.update((points) => points.map((p) => (p.id === savedPoint.id ? savedPoint : p)));
|
||||
addToast({
|
||||
header: "Point Updated",
|
||||
body: `Point "${savedPoint.name}" was successfully updated.`,
|
||||
color: "success",
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
addToast({
|
||||
header: "Update Error",
|
||||
body: `Failed to update point: ${error.message}`,
|
||||
color: "danger",
|
||||
});
|
||||
});
|
||||
} else {
|
||||
PointEditorRef?.openModalAndCreate(
|
||||
// Create new point
|
||||
pointEditorRef?.openModalAndCreate(
|
||||
null,
|
||||
{
|
||||
id: 0,
|
||||
name: `Новая точка ${new Date().toLocaleString()}`,
|
||||
lat: parseFloat(inputLat),
|
||||
lon: parseFloat(inputLng),
|
||||
alt: parseFloat(inputAlt),
|
||||
name: `New Point ${new Date().toLocaleString()}`,
|
||||
lat: $FlightParametersStore.launch_latitude,
|
||||
lon: $FlightParametersStore.launch_longitude,
|
||||
alt: $FlightParametersStore.launch_altitude,
|
||||
},
|
||||
true,
|
||||
false,
|
||||
handleModalSave,
|
||||
(savedPoint) => {
|
||||
if (savedPoint) {
|
||||
handlePointSelection(savedPoint.id);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function handleModalSave(savedPoint: SavedPoint) {
|
||||
if (savedPoint) {
|
||||
$FlightParametersStore.start_point = savedPoint.id;
|
||||
selectedPointId = savedPoint.id;
|
||||
isPointDirty = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handlePointEditorOpen() {
|
||||
PointEditorRef?.openModal(true);
|
||||
}
|
||||
|
||||
async function handlePredictionRequest() {
|
||||
$FlightParametersStore.launch_latitude = parseFloat(inputLat);
|
||||
$FlightParametersStore.launch_longitude = parseFloat(inputLng);
|
||||
$FlightParametersStore.launch_altitude = parseFloat(inputAlt);
|
||||
// Persist current parameters before running prediction
|
||||
writeLocalStorage<FlightParameters>("flightParameters", $FlightParametersStore);
|
||||
|
||||
try {
|
||||
const data = await getForecast($FlightParametersStore, `${startDate}T${startTime}Z`);
|
||||
console.log("Forecast request successful:", data);
|
||||
addToast({
|
||||
header: "Forecast Request",
|
||||
body: "Forecast request successful!",
|
||||
color: "success",
|
||||
});
|
||||
addToast({ header: "Forecast Request", body: "Forecast request successful!", color: "success" });
|
||||
} catch (error: any) {
|
||||
console.error("Error getting forecast:", error);
|
||||
addToast({
|
||||
header: "Forecast Error",
|
||||
body: `Error getting forecast: ${error.message}`,
|
||||
color: "danger",
|
||||
});
|
||||
addToast({ header: "Forecast Error", body: `Error getting forecast: ${error.message}`, color: "danger" });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -210,25 +198,23 @@
|
|||
export function updateLaunchPosition(lat: number, lng: number) {
|
||||
$FlightParametersStore.launch_latitude = lat;
|
||||
$FlightParametersStore.launch_longitude = lng;
|
||||
isPointDirty = true;
|
||||
}
|
||||
|
||||
export function getSelectedProfile() {
|
||||
return $FlightParametersStore.profile;
|
||||
export function loadFlightParameters(params: FlightParameters) {
|
||||
$FlightParametersStore = params;
|
||||
selectedPointId = params.start_point || -1;
|
||||
}
|
||||
|
||||
export function selectProfile(profile: ProfileName) {
|
||||
$FlightParametersStore.profile = profile;
|
||||
export function getFlightParameters(): FlightParameters {
|
||||
return $FlightParametersStore;
|
||||
}
|
||||
|
||||
export function collapsePanel() {
|
||||
isCollapsed = true;
|
||||
}
|
||||
|
||||
export function expandPanel() {
|
||||
isCollapsed = false;
|
||||
}
|
||||
|
||||
export function togglePanel() {
|
||||
isCollapsed = !isCollapsed;
|
||||
}
|
||||
|
|
@ -238,22 +224,12 @@
|
|||
<CardHeader
|
||||
class="bg-primary text-white d-flex justify-content-between align-items-center card-header p-1 px-3"
|
||||
style="cursor:pointer;"
|
||||
onclick={handleToggleCollapse}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="d-flex w-100 justify-content-between align-items-center bg-transparent border-0 p-0"
|
||||
aria-label="Свернуть/развернуть условия прогнозирования"
|
||||
onclick={handleToggleCollapse}
|
||||
>
|
||||
<b class="card-title mb-0 text-white p-0">Условия прогнозирования</b>
|
||||
<Button class="p-0" size="sm" color="primary" onclick={handleToggleCollapse}>
|
||||
{#if isCollapsed}
|
||||
<Icon name="caret-left-fill" class="text-white" />
|
||||
{:else}
|
||||
<Icon name="caret-down-fill" class="text-white" />
|
||||
{/if}
|
||||
</Button>
|
||||
</button>
|
||||
<b class="card-title mb-0 text-white p-0">Условия прогнозирования</b>
|
||||
<Button class="p-0" size="sm" color="primary" aria-label="Свернуть/развернуть условия прогнозирования">
|
||||
<Icon name={isCollapsed ? "caret-left-fill" : "caret-down-fill"} class="text-white" />
|
||||
</Button>
|
||||
</CardHeader>
|
||||
|
||||
{#if !isCollapsed}
|
||||
|
|
@ -273,8 +249,8 @@
|
|||
<Label for="cp-flight-profile" class="form-label">Профиль полета:</Label>
|
||||
<InputGroup size="sm">
|
||||
<Input type="select" id="cp-flight-profile" bind:value={$FlightParametersStore.profile}>
|
||||
{#each Object.keys(PROFILE_MAP) as profileName}
|
||||
<option value={PROFILE_MAP[profileName as ProfileName]}>{profileName}</option>
|
||||
{#each Object.entries(PROFILE_MAP) as [name, value]}
|
||||
<option {value}>{name}</option>
|
||||
{/each}
|
||||
</Input>
|
||||
</InputGroup>
|
||||
|
|
@ -287,46 +263,37 @@
|
|||
<SelectSearchable
|
||||
style="min-height: calc(1.5em + .5rem + 2px); padding: .25rem .5rem; font-size: .875rem;"
|
||||
id="cp-start-point"
|
||||
bind:selected={selectedPointId}
|
||||
selected={selectedPointId}
|
||||
onChange={(e) => handlePointSelection(e)}
|
||||
options={$SavedPointsStore.map((point) => ({
|
||||
value: point.id,
|
||||
label:
|
||||
point.name +
|
||||
`${point.id == $FlightParametersStore.start_point && isPointDirty ? " (изменено)" : ""}`,
|
||||
label: `${point.name} ${point.id === selectedPointId && isPointDirty() ? "(изменено)" : ""}`,
|
||||
}))}
|
||||
placeholder="Новая точка..."
|
||||
searchPlaceholder="Поиск по точкам..."
|
||||
on:change={() => {
|
||||
if (!isPointDirty) {
|
||||
$FlightParametersStore.start_point = selectedPointId;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
color="white"
|
||||
class="position-absolute top-50 end-0 translate-middle-y rounded-circle d-flex align-items-center justify-content-center"
|
||||
style="width: 16px; height: 16px; border: none; background: var(--bs-secondary); color: var(--bs-white); z-index: 10; margin-right: 2rem;"
|
||||
on:click={() => {
|
||||
selectedPointId = -1;
|
||||
$FlightParametersStore.start_point = -1;
|
||||
}}
|
||||
disabled={selectedPointId === -1}
|
||||
>
|
||||
<Icon name="x" style="font-size: 16px;" />
|
||||
</Button>
|
||||
{#if selectedPointId !== -1}
|
||||
<Button
|
||||
size="sm"
|
||||
color="white"
|
||||
class="position-absolute top-50 end-0 translate-middle-y rounded-circle d-flex align-items-center justify-content-center"
|
||||
style="width: 16px; height: 16px; border: none; background: var(--bs-secondary); color: var(--bs-white); z-index: 10; margin-right: 2rem;"
|
||||
on:click={() => handlePointSelection(-1)}
|
||||
title="Clear selection"
|
||||
>
|
||||
<Icon name="x" style="font-size: 16px;" />
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
<Button color="success" size="sm" onclick={handleApplySelectedPoint} title="Apply Coordinates"
|
||||
>✓</Button
|
||||
>
|
||||
</InputGroup>
|
||||
</FormGroup>
|
||||
|
||||
<div class="d-flex gap-2 mb-2">
|
||||
<Button
|
||||
color="secondary flex-fill"
|
||||
color="secondary"
|
||||
class="flex-fill"
|
||||
size="sm"
|
||||
onclick={handlePointEditorOpen}
|
||||
onclick={() => pointEditorRef?.openModal(true)}
|
||||
title="Открыть список точек"
|
||||
>
|
||||
Все точки
|
||||
|
|
@ -334,7 +301,8 @@
|
|||
</Button>
|
||||
|
||||
<Button
|
||||
color="primary flex-fill"
|
||||
color="primary"
|
||||
class="flex-fill"
|
||||
size="sm"
|
||||
onclick={handleSaveCurrentPoint}
|
||||
title="Сохранить текущие координаты"
|
||||
|
|
@ -350,28 +318,25 @@
|
|||
<InputGroup size="sm">
|
||||
<Input
|
||||
id="cp-latitude"
|
||||
type="text"
|
||||
bind:value={inputLat}
|
||||
type="number"
|
||||
step="0.000001"
|
||||
bind:value={$FlightParametersStore.launch_latitude}
|
||||
placeholder="Latitude"
|
||||
on:change={() => {
|
||||
isPointDirty = true;
|
||||
}}
|
||||
/>
|
||||
<InputGroupText>/</InputGroupText>
|
||||
<Input
|
||||
id="cp-longitude"
|
||||
type="text"
|
||||
bind:value={inputLng}
|
||||
type="number"
|
||||
step="0.000001"
|
||||
bind:value={$FlightParametersStore.launch_longitude}
|
||||
placeholder="Longitude"
|
||||
on:change={() => {
|
||||
isPointDirty = true;
|
||||
}}
|
||||
/>
|
||||
<Button color="secondary" size="sm" onclick={onSelectOnMapClick}>
|
||||
<Icon name="geo-alt-fill" />
|
||||
</Button>
|
||||
</InputGroup>
|
||||
</FormGroup>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<FormGroup class="flex-fill w-50" spacing="mb-2">
|
||||
<Label for="cp-start-height" class="form-label">Высота старта (м):</Label>
|
||||
|
|
@ -379,10 +344,7 @@
|
|||
type="number"
|
||||
id="cp-start-height"
|
||||
class="form-control-sm"
|
||||
on:change={() => {
|
||||
isPointDirty = true;
|
||||
}}
|
||||
bind:value={inputAlt}
|
||||
bind:value={$FlightParametersStore.launch_altitude}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup class="flex-fill w-50" spacing="mb-2">
|
||||
|
|
@ -418,34 +380,8 @@
|
|||
</FormGroup>
|
||||
</div>
|
||||
{:else}
|
||||
<FormGroup spacing="mb-2">
|
||||
<Label for="cp-flight-profile" class="form-label">Пользовательский профиль:</Label>
|
||||
<InputGroup size="sm">
|
||||
<SelectSearchable
|
||||
id="cp-flight-profile"
|
||||
bind:selected={$FlightParametersStore.profile}
|
||||
options={Object.keys(PROFILE_MAP).map((profileName) => ({
|
||||
// stub, replace with actual profiles
|
||||
value: PROFILE_MAP[profileName as ProfileName],
|
||||
label: profileName,
|
||||
}))}
|
||||
placeholder="Выберите профиль..."
|
||||
searchPlaceholder="Поиск профилей..."
|
||||
on:change={() => {
|
||||
$FlightParametersStore.profile = $FlightParametersStore.profile;
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
color="secondary"
|
||||
size="sm"
|
||||
title="Edit profile"
|
||||
disabled={$FlightParametersStore.profile !== "custom_profile"}
|
||||
>
|
||||
<span>Редакт.</span>
|
||||
<Icon name="gear-fill" />
|
||||
</Button>
|
||||
</InputGroup>
|
||||
</FormGroup>
|
||||
<!-- NOTE: Custom profile UI to be implemented -->
|
||||
<p class="text-muted text-center small my-3">Custom profile editor is not yet implemented.</p>
|
||||
{/if}
|
||||
|
||||
<div class="d-grid gap-1">
|
||||
|
|
@ -454,4 +390,4 @@
|
|||
</CardBody>
|
||||
{/if}
|
||||
</Card>
|
||||
<PointEditor bind:this={PointEditorRef} onSelectPoint={handleSelectPointInModal} />
|
||||
<PointEditor bind:this={pointEditorRef} onSelectPoint={(point: SavedPoint) => handlePointSelection(point.id)} />
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@
|
|||
const savedFlightParameters = savedData.flight_parameters;
|
||||
|
||||
// Compare flight parameters excluding launch_datetime
|
||||
console.log("Comparing flight parameters:", flightParameters, savedFlightParameters);
|
||||
return JSON.stringify(flightParameters) !== JSON.stringify(savedFlightParameters);
|
||||
}
|
||||
|
||||
|
|
@ -112,6 +113,7 @@
|
|||
$FlightParametersStore = selectedScenario.template_data.flight_parameters;
|
||||
scenarioUnsaved = false;
|
||||
writeLocalStorage("scenario", $ScenarioStore);
|
||||
writeLocalStorage("flightParameters", $FlightParametersStore);
|
||||
if (showToast) {
|
||||
addToast({
|
||||
header: "Сценарий применен",
|
||||
|
|
|
|||
|
|
@ -52,6 +52,11 @@ export const getForecast = async (
|
|||
flightParameters.dataset = getLatestDataset();
|
||||
flightParameters.launch_datetime = launchDateTime;
|
||||
|
||||
if (flightParameters.start_point === -1) {
|
||||
// remove start_point if it is -1
|
||||
delete flightParameters.start_point;
|
||||
}
|
||||
|
||||
console.log("Sending request:", flightParameters);
|
||||
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ export const clearLocalStorage = (key: string): void => {
|
|||
}
|
||||
}
|
||||
|
||||
const flightParametersDefaults: FlightParameters = {
|
||||
export const flightParametersDefaults: FlightParameters = {
|
||||
ascent_rate: 5.0,
|
||||
burst_altitude: 30000.0,
|
||||
dataset: "",
|
||||
|
|
@ -52,7 +52,7 @@ export const FlightParametersStore = writable<FlightParameters>(
|
|||
readLocalStorage<FlightParameters>("flightParameters", flightParametersDefaults)
|
||||
);
|
||||
|
||||
const templateDataDefaults: TemplateData = {
|
||||
export const templateDataDefaults: TemplateData = {
|
||||
description: "",
|
||||
prediction_mode: "",
|
||||
model: "",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue