frontend
This commit is contained in:
parent
118965e0c3
commit
e3d416ff0e
30 changed files with 6864 additions and 0 deletions
46
frontend/README.md
Normal file
46
frontend/README.md
Normal file
|
@ -0,0 +1,46 @@
|
|||
# Project Genesis
|
||||
|
||||
A modern, responsive web application built with Vue.js. This project leverages a rich component library for a polished user interface and centralized state management for a robust and scalable architecture.
|
||||
|
||||
## Core Technologies
|
||||
|
||||
- **Framework:** [Vue.js](https://vuejs.org/)
|
||||
- **UI Components:** [PrimeVue](https://primevue.org/)
|
||||
- **State Management:** [Pinia](https://pinia.vuejs.org/)
|
||||
- **API Communication:** [Axios](https://axios-http.com/) (Managed within Pinia stores for centralized API/SPI calls)
|
||||
|
||||
## Getting Started
|
||||
|
||||
Follow these instructions to get a copy of the project up and running on your local machine for development and testing purposes.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
You will need [Node.js](https://nodejs.org/) and [npm](https://www.npmjs.com/) installed on your machine.
|
||||
|
||||
- Node.js (v18.x or higher recommended)
|
||||
- npm (v9.x or higher recommended)
|
||||
|
||||
### Installation
|
||||
|
||||
1. Clone the repository to your local machine:
|
||||
```sh
|
||||
git clone <repository-url>
|
||||
```
|
||||
2. Navigate to the project directory:
|
||||
```sh
|
||||
cd <project-directory>
|
||||
```
|
||||
3. Install the dependencies:
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
To start the local development server, run the following command:
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
The application will be available at `http://localhost:5173` (or the next available port). The development server supports hot-reloading, so any changes you make to the source code will be reflected in the browser automatically.
|
280
frontend/characteristics.json
Normal file
280
frontend/characteristics.json
Normal file
|
@ -0,0 +1,280 @@
|
|||
{
|
||||
"environmental_characteristics": {
|
||||
"atmosphere": [
|
||||
{
|
||||
"parameter": "air_temperature",
|
||||
"name": "Air Temperature",
|
||||
"unit": "Celsius",
|
||||
"description": "The ambient temperature surrounding the plant. Slider from -50 to 50 degrees Celsius"
|
||||
},
|
||||
{
|
||||
"parameter": "relative_humidity",
|
||||
"name": "Relative Humidity",
|
||||
"unit": "Percentage",
|
||||
"description": "The amount of water vapor in the air, expressed as a percentage of the maximum amount the air could hold. Slider from 0 to 100"
|
||||
},
|
||||
{
|
||||
"parameter": "co2_concentration",
|
||||
"name": "Carbon Dioxide Concentration",
|
||||
"unit": "ppm",
|
||||
"description": "Concentration of CO2 in the atmosphere, essential for photosynthesis. Slider from 0 to 5000 ppm"
|
||||
},
|
||||
{
|
||||
"parameter": "air_pressure",
|
||||
"name": "Atmospheric Pressure",
|
||||
"unit": "kPa",
|
||||
"description": "Atmospheric pressure, which can influence transpiration rates. Slider from 50 to 150 kPa"
|
||||
},
|
||||
{
|
||||
"parameter": "wind_speed",
|
||||
"name": "Wind Speed",
|
||||
"unit": "m/s",
|
||||
"description": "Speed of air movement, which affects transpiration and physical stress. Slider from 0 to 20 m/s"
|
||||
}
|
||||
],
|
||||
"soil": [
|
||||
{
|
||||
"parameter": "soil_temperature",
|
||||
"name": "Soil Temperature",
|
||||
"unit": "Celsius",
|
||||
"description": "The temperature of the soil, affecting root health and nutrient uptake. Slider from -50 to 50 degrees Celsius"
|
||||
},
|
||||
{
|
||||
"parameter": "soil_acidity",
|
||||
"name": "Soil Acidity (pH)",
|
||||
"unit": "pH",
|
||||
"description": "The acidity or alkalinity of the soil, critically affecting nutrient availability. Slider from 0 to 14"
|
||||
},
|
||||
{
|
||||
"parameter": "soil_moisture",
|
||||
"name": "Soil Moisture",
|
||||
"unit": "Percentage",
|
||||
"description": "The amount of water held in the soil, available to the plant roots. Slider from 0 to 100"
|
||||
},
|
||||
{
|
||||
"parameter": "nutrients",
|
||||
"name": "Nutrients",
|
||||
"description": "Essential minerals for plant growth, categorized into macro and micro. Multiselector with checkboxes",
|
||||
"types": {
|
||||
"macronutrients": ["Nitrogen (N)", "Phosphorus (P)", "Potassium (K)", "Calcium (Ca)", "Magnesium (Mg)", "Sulfur (S)"],
|
||||
}
|
||||
}
|
||||
],
|
||||
"light": [
|
||||
{
|
||||
"parameter": "light_intensity",
|
||||
"name": "Light Intensity",
|
||||
"unit": "PPFD (μmol/m²/s)",
|
||||
"description": "The amount of light available for photosynthesis. Slider from 0 to 2000 μmol/m²/s",
|
||||
},
|
||||
{
|
||||
"parameter": "photoperiod",
|
||||
"name": "Photoperiod",
|
||||
"unit": "Hours per day",
|
||||
"description": "The duration of light the plant receives in a 24-hour period. Slider from 0 to 24 hours"
|
||||
}
|
||||
],
|
||||
"water": [
|
||||
{
|
||||
"parameter": "watering_frequency",
|
||||
"name": "Watering Frequency",
|
||||
"unit": "Times per week",
|
||||
"description": "How often the plant is watered. Slider from 0 to 14 times per week"
|
||||
}
|
||||
]
|
||||
},
|
||||
"plant_characteristics": {
|
||||
"morphological": [
|
||||
{
|
||||
"name": "Plant type",
|
||||
"description": "Basil, rosemary etc.",
|
||||
"parameter": "plant_type",
|
||||
"unit": "N/A"
|
||||
},
|
||||
{
|
||||
"name": "Height",
|
||||
"description": "Height of the plant in cm",
|
||||
"parameter": "height",
|
||||
"unit": "cm"
|
||||
},
|
||||
{
|
||||
"name": "Leaf Color",
|
||||
"description": "Color of the leaves, e.g., green, yellow, purple, brown.",
|
||||
"parameter": "leaf_color",
|
||||
"unit": "N/A"
|
||||
},
|
||||
{
|
||||
"name": "Presence of Flowers",
|
||||
"description": "boolean",
|
||||
"parameter": "presence_of_flowers",
|
||||
"unit": "N/A"
|
||||
},
|
||||
{
|
||||
"name": "Fruit Development",
|
||||
"description": "boolean",
|
||||
"parameter": "fruit_development",
|
||||
"unit": "N/A"
|
||||
},
|
||||
{
|
||||
"name": "Root Health",
|
||||
"description": "Color and structure of roots, e.g., white and firm, brown and mushy.",
|
||||
"parameter": "root_health",
|
||||
"unit": "N/A"
|
||||
}
|
||||
],
|
||||
"physiological": [
|
||||
{
|
||||
"name": "Health Status",
|
||||
"description": "['Thriving', 'Healthy', 'Stressed', 'Wilting', 'Diseased', 'Dying', 'Dead']",
|
||||
"parameter": "health_status",
|
||||
"unit": "N/A"
|
||||
},
|
||||
{
|
||||
"name": "Growth Rate",
|
||||
"description": "cm per week",
|
||||
"parameter": "growth_rate",
|
||||
"unit": "cm/week"
|
||||
},
|
||||
{
|
||||
"name": "Photosynthesis Rate",
|
||||
"description": "Efficiency of converting light to energy in percentage.",
|
||||
"parameter": "photosynthesis_rate",
|
||||
"unit": "Percentage"
|
||||
},
|
||||
{
|
||||
"name": "Transpiration Rate",
|
||||
"description": "Rate of water movement and evaporation from leaves in percentage.",
|
||||
"parameter": "transpiration_rate",
|
||||
"unit": "Percentage"
|
||||
}
|
||||
]
|
||||
},
|
||||
"effects_on_plant": [
|
||||
{
|
||||
"effect_name": "Heat Stress",
|
||||
"cause": {
|
||||
"parameter": "air_temperature",
|
||||
"condition": "high",
|
||||
"description": "Temperature is significantly above the plant's optimal range."
|
||||
},
|
||||
"impacts": [
|
||||
{
|
||||
"characteristic_affected": "Leaf Shape",
|
||||
"symptom": "Leaves curl upwards and may develop brown, burnt spots on the edges."
|
||||
},
|
||||
{
|
||||
"characteristic_affected": "Health Status",
|
||||
"symptom": "Plant appears wilted even if the soil is moist due to high transpiration."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"effect_name": "Nitrogen Deficiency",
|
||||
"cause": {
|
||||
"parameter": "nutrients",
|
||||
"condition": "low",
|
||||
"description": "Insufficient Nitrogen (N) in the soil."
|
||||
},
|
||||
"impacts": [
|
||||
{
|
||||
"characteristic_affected": "Leaf Color",
|
||||
"symptom": "General chlorosis (yellowing) of older, lower leaves first, as nitrogen is mobile within the plant."
|
||||
},
|
||||
{
|
||||
"characteristic_affected": "Height",
|
||||
"symptom": "Stunted growth and reduced overall plant size."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"effect_name": "Low Soil pH",
|
||||
"cause": {
|
||||
"parameter": "soil_acidity",
|
||||
"condition": "low",
|
||||
"description": "Soil is too acidic (pH < 5.5), locking up nutrients like phosphorus and magnesium."
|
||||
},
|
||||
"impacts": [
|
||||
{
|
||||
"characteristic_affected": "Leaf Color",
|
||||
"symptom": "Can cause various nutrient deficiency symptoms, such as yellowing (magnesium) or purplish leaves (phosphorus)."
|
||||
},
|
||||
{
|
||||
"characteristic_affected": "Root Health",
|
||||
"symptom": "Can lead to aluminum toxicity, causing stunted, brown roots."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"effect_name": "Drought Stress",
|
||||
"cause": {
|
||||
"parameter": "soil_moisture",
|
||||
"condition": "low",
|
||||
"description": "Insufficient water available to the plant roots."
|
||||
},
|
||||
"impacts": [
|
||||
{
|
||||
"characteristic_affected": "Health Status",
|
||||
"symptom": "Progressive wilting of the entire plant, starting from the leaves."
|
||||
},
|
||||
{
|
||||
"characteristic_affected": "Leaf Shape",
|
||||
"symptom": "Leaves become dry, brittle, and may fall off."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"effect_name": "Poor Light",
|
||||
"cause": {
|
||||
"parameter": "light_intensity",
|
||||
"condition": "low",
|
||||
"description": "Insufficient light for photosynthesis."
|
||||
},
|
||||
"impacts": [
|
||||
{
|
||||
"characteristic_affected": "Stem Thickness",
|
||||
"symptom": "Plant becomes 'leggy' with thin, elongated stems as it stretches towards a light source."
|
||||
},
|
||||
{
|
||||
"characteristic_affected": "Leaf Color",
|
||||
"symptom": "Leaves may become pale green or yellow."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"effect_name": "CO2 Enrichment",
|
||||
"cause": {
|
||||
"parameter": "co2_concentration",
|
||||
"condition": "high",
|
||||
"description": "CO2 levels are elevated above ambient (e.g., > 1000 ppm)."
|
||||
},
|
||||
"impacts": [
|
||||
{
|
||||
"characteristic_affected": "Growth Rate",
|
||||
"symptom": "Increased growth rate and biomass production, assuming other factors are not limiting."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"effect_name": "Osmotic Stress / Chemical Contamination",
|
||||
"cause": {
|
||||
"parameter": "water_quality",
|
||||
"condition": "contaminated",
|
||||
"description": "Watering with a harmful, non-standard liquid (e.g., sugary, acidic, or salty solution like Coca-Cola)."
|
||||
},
|
||||
"impacts": [
|
||||
{
|
||||
"characteristic_affected": "Root Health",
|
||||
"symptom": "Roots are damaged by extreme pH and osmotic pressure, preventing water uptake and leading to decay."
|
||||
},
|
||||
{
|
||||
"characteristic_affected": "Health Status",
|
||||
"symptom": "Rapid and severe wilting, discoloration, and likely plant death within a short period."
|
||||
},
|
||||
{
|
||||
"characteristic_affected": "Leaf Color",
|
||||
"symptom": "Leaves may turn brown or black as cells die."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
24
frontend/eslint.config.js
Normal file
24
frontend/eslint.config.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||
import globals from 'globals'
|
||||
import js from '@eslint/js'
|
||||
import pluginVue from 'eslint-plugin-vue'
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
name: 'app/files-to-lint',
|
||||
files: ['**/*.{js,mjs,jsx,vue}'],
|
||||
},
|
||||
|
||||
globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']),
|
||||
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
js.configs.recommended,
|
||||
...pluginVue.configs['flat/essential'],
|
||||
])
|
13
frontend/index.html
Normal file
13
frontend/index.html
Normal file
|
@ -0,0 +1,13 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Vite App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
8
frontend/jsconfig.json
Normal file
8
frontend/jsconfig.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
4901
frontend/package-lock.json
generated
Normal file
4901
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
35
frontend/package.json
Normal file
35
frontend/package.json
Normal file
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"name": "plant",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint . --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@primeuix/themes": "^1.2.3",
|
||||
"axios": "^1.11.0",
|
||||
"pinia": "^3.0.3",
|
||||
"primeicons": "^7.0.0",
|
||||
"primevue": "^4.3.7",
|
||||
"scss": "^0.2.4",
|
||||
"vue": "^3.5.18",
|
||||
"vue-router": "^4.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.31.0",
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"eslint": "^9.31.0",
|
||||
"eslint-plugin-vue": "~10.3.0",
|
||||
"globals": "^16.3.0",
|
||||
"sass": "^1.89.2",
|
||||
"vite": "^7.0.6",
|
||||
"vite-plugin-vue-devtools": "^8.0.0"
|
||||
}
|
||||
}
|
BIN
frontend/public/favicon.ico
Normal file
BIN
frontend/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
49
frontend/src/App.vue
Normal file
49
frontend/src/App.vue
Normal file
|
@ -0,0 +1,49 @@
|
|||
<script setup>
|
||||
import PlantView from "@/views/PlantView.vue";
|
||||
import ConditionsVIew from "@/views/ConditionsVIew.vue";
|
||||
import { RouterView } from "vue-router";
|
||||
import Button from "primevue/button";
|
||||
import { useRoute } from 'vue-router';
|
||||
import {computed} from "vue";
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const showHomeButton = computed(() => route.path !== '/' && route.path !== '/loading');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="main-wrapper">
|
||||
<div class="inner">
|
||||
<Button v-if="showHomeButton" icon="pi pi-home" severity="secondary" aria-label="Home" @click="$router.push('/')"/>
|
||||
|
||||
|
||||
<RouterView></RouterView>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
* {
|
||||
font-family: Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
.main-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
padding: 3rem;
|
||||
}
|
||||
|
||||
.inner {
|
||||
max-width: 45rem;
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-direction: column;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
30
frontend/src/components/conditionDescription.vue
Normal file
30
frontend/src/components/conditionDescription.vue
Normal file
|
@ -0,0 +1,30 @@
|
|||
<template>
|
||||
|
||||
<div class="description-wrapper">
|
||||
<h3>What conditions are you going to put your plant in?</h3>
|
||||
<Textarea v-model="plantStateStore.conditionDataDescription" placeholder="Are you adding fertilizer? Are you watering it more?"/>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Textarea from 'primevue/textarea';
|
||||
import { usePlantStateStore
|
||||
} from "@/stores/plantState.js";
|
||||
|
||||
const plantStateStore = usePlantStateStore();
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
textarea {
|
||||
width: 100%;
|
||||
height: 10rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
286
frontend/src/components/conditionDetails.vue
Normal file
286
frontend/src/components/conditionDetails.vue
Normal file
|
@ -0,0 +1,286 @@
|
|||
<template>
|
||||
<main class="plant-form-container">
|
||||
<h3>If you would like to specify specific environment information you can do so, if not, we have some common defaults</h3>
|
||||
|
||||
<form @submit.prevent="submitForm" class="plant-form">
|
||||
<section class="environmental-section">
|
||||
<Tabs value="0">
|
||||
<TabList>
|
||||
<Tab header="Atmosphere" value="0">
|
||||
Atmosphere
|
||||
</Tab>
|
||||
<Tab header="Soil" value="1">
|
||||
Soil
|
||||
</Tab>
|
||||
<Tab header="Light" value="2">
|
||||
Light
|
||||
</Tab>
|
||||
<Tab header="Water" value="3">
|
||||
Water
|
||||
</Tab>
|
||||
</TabList>
|
||||
<TabPanels class="conditions-tabs" >
|
||||
<TabPanel header="Atmosphere" value="0">
|
||||
<Card class="condition-card">
|
||||
<template #content>
|
||||
<div class="field-grid">
|
||||
<div class="field">
|
||||
<label for="air_temperature">Air Temperature (°C)</label>
|
||||
<Slider v-model="formData.environmental_characteristics.atmosphere.air_temperature"
|
||||
:min="-50" :max="50"
|
||||
:step="1"
|
||||
class="w-full" />
|
||||
<InputNumber v-model="formData.environmental_characteristics.atmosphere.air_temperature"
|
||||
:min="-50" :max="50"
|
||||
class="mt-2" />
|
||||
<small>The ambient temperature surrounding the plant (-50 to 50°C)</small>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="relative_humidity">Relative Humidity (%)</label>
|
||||
<Slider v-model="formData.environmental_characteristics.atmosphere.relative_humidity"
|
||||
:min="0" :max="100"
|
||||
class="w-full" />
|
||||
<InputNumber v-model="formData.environmental_characteristics.atmosphere.relative_humidity"
|
||||
:min="0" :max="100"
|
||||
class="mt-2" />
|
||||
<small>The amount of water vapor in the air (0-100%)</small>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="co2_concentration">Carbon Dioxide Concentration (ppm)</label>
|
||||
<Slider v-model="formData.environmental_characteristics.atmosphere.co2_concentration"
|
||||
:min="0" :max="5000"
|
||||
:step="100"
|
||||
class="w-full" />
|
||||
<InputNumber v-model="formData.environmental_characteristics.atmosphere.co2_concentration"
|
||||
:min="0" :max="5000"
|
||||
class="mt-2" />
|
||||
<small>Concentration of CO₂ in the atmosphere (0-5000 ppm)</small>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="air_pressure">Atmospheric Pressure (kPa)</label>
|
||||
<Slider v-model="formData.environmental_characteristics.atmosphere.air_pressure"
|
||||
:min="50" :max="150"
|
||||
:step="1"
|
||||
class="w-full" />
|
||||
<InputNumber v-model="formData.environmental_characteristics.atmosphere.air_pressure"
|
||||
:min="50" :max="150"
|
||||
class="mt-2" />
|
||||
<small>Atmospheric pressure (50-150 kPa)</small>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="wind_speed">Wind Speed (m/s)</label>
|
||||
<Slider v-model="formData.environmental_characteristics.atmosphere.wind_speed"
|
||||
:min="0" :max="20"
|
||||
:step="0.1"
|
||||
class="w-full" />
|
||||
<InputNumber v-model="formData.environmental_characteristics.atmosphere.wind_speed"
|
||||
:min="0" :max="20"
|
||||
:step="0.1"
|
||||
class="mt-2" />
|
||||
<small>Speed of air movement (0-20 m/s)</small>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel header="Soil" value="1">
|
||||
<Card class="condition-card">
|
||||
<template #content>
|
||||
<div class="field-grid">
|
||||
<div class="field">
|
||||
<label for="soil_temperature">Soil Temperature (°C)</label>
|
||||
<Slider v-model="formData.environmental_characteristics.soil.soil_temperature"
|
||||
:min="-50" :max="50"
|
||||
class="w-full" />
|
||||
<InputNumber v-model="formData.environmental_characteristics.soil.soil_temperature"
|
||||
:min="-50" :max="50"
|
||||
class="mt-2" />
|
||||
<small>The temperature of the soil (-50 to 50°C)</small>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="soil_acidity">Soil Acidity (pH)</label>
|
||||
<Slider v-model="formData.environmental_characteristics.soil.soil_acidity"
|
||||
:min="0" :max="14"
|
||||
:step="0.1"
|
||||
class="w-full" />
|
||||
<InputNumber v-model="formData.environmental_characteristics.soil.soil_acidity"
|
||||
:min="0" :max="14"
|
||||
:step="0.1"
|
||||
class="mt-2" />
|
||||
<small>The acidity or alkalinity of the soil (0-14 pH)</small>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="soil_moisture">Soil Moisture (%)</label>
|
||||
<Slider v-model="formData.environmental_characteristics.soil.soil_moisture"
|
||||
:min="0" :max="100"
|
||||
class="w-full" />
|
||||
<InputNumber v-model="formData.environmental_characteristics.soil.soil_moisture"
|
||||
:min="0" :max="100"
|
||||
class="mt-2" />
|
||||
<small>The amount of water held in the soil (0-100%)</small>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="nutrients">Nutrients</label>
|
||||
<MultiSelect v-model="formData.environmental_characteristics.soil.nutrients"
|
||||
:options="nutrientOptions"
|
||||
display="chip"
|
||||
placeholder="Select Nutrients"
|
||||
class="w-full" />
|
||||
<small>Essential minerals for plant growth</small>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel header="Light" value="2">
|
||||
<Card class="condition-card">
|
||||
<template #content>
|
||||
<div class="field-grid">
|
||||
<div class="field">
|
||||
<label for="light_intensity">Light Intensity (PPFD)</label>
|
||||
<Slider v-model="formData.environmental_characteristics.light.light_intensity"
|
||||
:min="0" :max="2000"
|
||||
:step="50"
|
||||
class="w-full" />
|
||||
<InputNumber v-model="formData.environmental_characteristics.light.light_intensity"
|
||||
:min="0" :max="2000"
|
||||
class="mt-2" />
|
||||
<small>Amount of light available for photosynthesis (0-2000 μmol/m²/s)</small>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="photoperiod">Photoperiod (hours/day)</label>
|
||||
<Slider v-model="formData.environmental_characteristics.light.photoperiod"
|
||||
:min="0" :max="24"
|
||||
class="w-full" />
|
||||
<InputNumber v-model="formData.environmental_characteristics.light.photoperiod"
|
||||
:min="0" :max="24"
|
||||
class="mt-2" />
|
||||
<small>Duration of light the plant receives (0-24 hours)</small>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel header="Water" value="3">
|
||||
<Card class="condition-card">
|
||||
<template #content>
|
||||
<div class="field">
|
||||
<label for="watering_frequency">Watering Frequency (times/week)</label>
|
||||
<Slider v-model="formData.environmental_characteristics.water.watering_frequency"
|
||||
:min="0" :max="14"
|
||||
class="w-full" />
|
||||
<InputNumber v-model="formData.environmental_characteristics.water.watering_frequency"
|
||||
:min="0" :max="14"
|
||||
class="mt-2" />
|
||||
<small>How often the plant is watered (0-14 times/week)</small>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</section>
|
||||
</form>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { usePlantStateStore } from '@/stores/plantState';
|
||||
import TabView from 'primevue/tabview';
|
||||
import TabPanel from 'primevue/tabpanel';
|
||||
import Card from 'primevue/card';
|
||||
import Slider from 'primevue/slider';
|
||||
import InputNumber from 'primevue/inputnumber';
|
||||
import MultiSelect from 'primevue/multiselect';
|
||||
import Button from 'primevue/button';
|
||||
import Tab from "primevue/tab";
|
||||
import TabList from "primevue/tablist";
|
||||
import TabPanels from "primevue/tabpanels";
|
||||
import Tabs from "primevue/tabs";
|
||||
|
||||
|
||||
const plantStore = usePlantStateStore();
|
||||
|
||||
const nutrientOptions = [
|
||||
"Nitrogen (N)",
|
||||
"Phosphorus (P)",
|
||||
"Potassium (K)",
|
||||
"Calcium (Ca)",
|
||||
"Magnesium (Mg)",
|
||||
"Sulfur (S)"
|
||||
];
|
||||
|
||||
const formData = plantStore.conditionData
|
||||
|
||||
const submitForm = () => {
|
||||
plantStore.setConditionData(formData.value);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.plant-form-container {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.plant-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.conditions-tabs {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.condition-card {
|
||||
margin-top: 1rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.field-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
small {
|
||||
color: var(--text-color-secondary);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.form-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
label {
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
:deep(.p-slider) {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
</style>
|
41
frontend/src/components/imageScan.vue
Normal file
41
frontend/src/components/imageScan.vue
Normal file
|
@ -0,0 +1,41 @@
|
|||
<template>
|
||||
|
||||
<div class="image-wrapper">
|
||||
<FileUpload name="demo[]" @select="handleUpload($event)" :multiple="false" :auto="true" accept="image/*" :maxFileSize="110000">
|
||||
<template #empty>
|
||||
<span>Drag and drop files to here to upload.</span>
|
||||
</template>
|
||||
</FileUpload>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import FileUpload from 'primevue/fileupload';
|
||||
import { useToast } from "primevue/usetoast";
|
||||
import { usePlantStateStore } from "@/stores/plantState.js";
|
||||
const toast = useToast();
|
||||
import router from "@/router";
|
||||
|
||||
const plantStateStore = usePlantStateStore();
|
||||
|
||||
async function handleUpload(event) {
|
||||
console.log(event)
|
||||
const file = event.files[0]
|
||||
const reader = new FileReader()
|
||||
reader.onload = async () => {
|
||||
plantStateStore.setImage(reader.result)
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
|
||||
toast.add({ severity: 'success', summary: 'What a lovely plant you have!', detail: 'File Uploaded', life: 3000 });
|
||||
router.push('/help');
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.p-fileupload-upload-button) {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
213
frontend/src/components/plantDetails.vue
Normal file
213
frontend/src/components/plantDetails.vue
Normal file
|
@ -0,0 +1,213 @@
|
|||
<template>
|
||||
<main class="plant-form-container">
|
||||
<h3>Feel like being more precise? Insert additional information if you know it</h3>
|
||||
|
||||
<form @submit.prevent="submitForm" class="plant-form">
|
||||
<section class="plant-section">
|
||||
<Tabs value="0">
|
||||
<TabList>
|
||||
<Tab header="Morphological" value="0">
|
||||
Morphological Characteristics
|
||||
</Tab>
|
||||
<Tab header="Physiological" value="1">
|
||||
Physiological Characteristics
|
||||
</Tab>
|
||||
</TabList>
|
||||
<TabPanels class="plant-tabs">
|
||||
<TabPanel header="Morphological" value="0">
|
||||
<Card class="plant-card">
|
||||
<template #content>
|
||||
<div class="field-grid">
|
||||
<div class="field">
|
||||
<label for="plant_type">Plant Type</label>
|
||||
<InputText v-model="formData.plant_characteristics.morphological.plant_type"
|
||||
id="plant_type"
|
||||
class="w-full" />
|
||||
<small>Basil, rosemary etc.</small>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="height">Height (cm)</label>
|
||||
<InputNumber v-model="formData.plant_characteristics.morphological.height"
|
||||
id="height"
|
||||
:min="0"
|
||||
class="w-full" />
|
||||
<small>Height of the plant in cm</small>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="leaf_color">Leaf Color</label>
|
||||
<InputText v-model="formData.plant_characteristics.morphological.leaf_color"
|
||||
id="leaf_color"
|
||||
class="w-full" />
|
||||
<small>Color of the leaves (e.g., green, yellow, purple)</small>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="presence_of_flowers">Presence of Flowers</label>
|
||||
<InputSwitch v-model="formData.plant_characteristics.morphological.presence_of_flowers"
|
||||
id="presence_of_flowers" />
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="fruit_development">Fruit Development</label>
|
||||
<InputSwitch v-model="formData.plant_characteristics.morphological.fruit_development"
|
||||
id="fruit_development" />
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="root_health">Root Health</label>
|
||||
<InputText v-model="formData.plant_characteristics.morphological.root_health"
|
||||
id="root_health"
|
||||
class="w-full" />
|
||||
<small>Color and structure of roots (e.g., white and firm)</small>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel header="Physiological" value="1">
|
||||
<Card class="plant-card">
|
||||
<template #content>
|
||||
<div class="field-grid">
|
||||
<div class="field">
|
||||
<label for="health_status">Health Status</label>
|
||||
<Dropdown v-model="formData.plant_characteristics.physiological.health_status"
|
||||
:options="healthOptions"
|
||||
placeholder="Select Health Status"
|
||||
class="w-full" />
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="growth_rate">Growth Rate (cm/week)</label>
|
||||
<InputNumber v-model="formData.plant_characteristics.physiological.growth_rate"
|
||||
id="growth_rate"
|
||||
:min="0"
|
||||
:step="0.1"
|
||||
class="w-full" />
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="photosynthesis_rate">Photosynthesis Rate (%)</label>
|
||||
<Slider v-model="formData.plant_characteristics.physiological.photosynthesis_rate"
|
||||
:min="0"
|
||||
:max="100"
|
||||
class="w-full" />
|
||||
<InputNumber v-model="formData.plant_characteristics.physiological.photosynthesis_rate"
|
||||
:min="0"
|
||||
:max="100"
|
||||
class="mt-2" />
|
||||
<small>Efficiency of converting light to energy</small>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="transpiration_rate">Transpiration Rate (%)</label>
|
||||
<Slider v-model="formData.plant_characteristics.physiological.transpiration_rate"
|
||||
:min="0"
|
||||
:max="100"
|
||||
class="w-full" />
|
||||
<InputNumber v-model="formData.plant_characteristics.physiological.transpiration_rate"
|
||||
:min="0"
|
||||
:max="100"
|
||||
class="mt-2" />
|
||||
<small>Rate of water movement and evaporation from leaves</small>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</section>
|
||||
</form>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { usePlantStateStore } from '@/stores/plantState';
|
||||
import TabPanel from 'primevue/tabpanel';
|
||||
import Card from 'primevue/card';
|
||||
import InputNumber from 'primevue/inputnumber';
|
||||
import InputText from 'primevue/inputtext';
|
||||
import InputSwitch from 'primevue/inputswitch';
|
||||
import Dropdown from 'primevue/dropdown';
|
||||
import Button from 'primevue/button';
|
||||
import Slider from 'primevue/slider';
|
||||
import TabList from 'primevue/tablist';
|
||||
import Tab from 'primevue/tab';
|
||||
import Tabs from 'primevue/tabs';
|
||||
import TabPanels from 'primevue/tabpanels';
|
||||
|
||||
const plantStore = usePlantStateStore();
|
||||
|
||||
const healthOptions = [
|
||||
"Thriving",
|
||||
"Healthy",
|
||||
"Stressed",
|
||||
"Wilting",
|
||||
"Diseased",
|
||||
"Dying",
|
||||
"Dead"
|
||||
];
|
||||
|
||||
const formData = plantStore.plantData
|
||||
|
||||
const submitForm = () => {
|
||||
plantStore.setPlantData(formData.value);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.plant-form-container {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.plant-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.plant-tabs {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.plant-card {
|
||||
margin-top: 1rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.field-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
small {
|
||||
color: var(--text-color-secondary);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.form-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
label {
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
</style>
|
31
frontend/src/components/textDescription.vue
Normal file
31
frontend/src/components/textDescription.vue
Normal file
|
@ -0,0 +1,31 @@
|
|||
<template>
|
||||
|
||||
<div class="description-wrapper">
|
||||
<h3>Give us some more information, the more the better!</h3>
|
||||
<Textarea v-model="plantStateStore.plantDataDescription" placeholder="Does your plant have any known sicknesses? Is it large? Does your pet nibble on it?"/>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Textarea from 'primevue/textarea';
|
||||
import {usePlantStateStore} from "@/stores/plantState.js";
|
||||
|
||||
const plantStateStore = usePlantStateStore();
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
textarea {
|
||||
width: 100%;
|
||||
height: 10rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.description-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
</style>
|
24
frontend/src/main.js
Normal file
24
frontend/src/main.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import Aura from '@primeuix/themes/aura';
|
||||
import ToastService from 'primevue/toastservice'
|
||||
|
||||
import '../style/styles.css'
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
app.use(ToastService)
|
||||
|
||||
app.use(PrimeVue, {
|
||||
theme: {
|
||||
preset: Aura
|
||||
}
|
||||
})
|
||||
|
||||
app.mount('#app')
|
44
frontend/src/router/index.js
Normal file
44
frontend/src/router/index.js
Normal file
|
@ -0,0 +1,44 @@
|
|||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Upload',
|
||||
component: () => import('@/views/InitialUploadView.vue')
|
||||
},
|
||||
{
|
||||
path: '/help',
|
||||
name: 'How can we help you?',
|
||||
component: () => import('@/views/HelpView.vue')
|
||||
},
|
||||
{
|
||||
path: '/simulate',
|
||||
name: 'Simulate',
|
||||
component: () => import('@/views/PlantView.vue')
|
||||
},
|
||||
{
|
||||
path: '/setEnvironment',
|
||||
name: 'Set Environment',
|
||||
component: () => import('@/views/EnvironmentView.vue')
|
||||
},
|
||||
{
|
||||
path: '/loading',
|
||||
name: 'Loading',
|
||||
component: () => import('@/views/LoadingView.vue')
|
||||
},
|
||||
{
|
||||
path: '/results',
|
||||
name: 'Results',
|
||||
component: () => import('@/views/SimulationResultView.vue')
|
||||
},
|
||||
{
|
||||
path: '/cure',
|
||||
name: 'Cure Plant',
|
||||
component: () => import('@/views/PlantCureView.vue')
|
||||
}
|
||||
],
|
||||
})
|
||||
|
||||
export default router
|
12
frontend/src/stores/counter.js
Normal file
12
frontend/src/stores/counter.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { ref, computed } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useCounterStore = defineStore('counter', () => {
|
||||
const count = ref(0)
|
||||
const doubleCount = computed(() => count.value * 2)
|
||||
function increment() {
|
||||
count.value++
|
||||
}
|
||||
|
||||
return { count, doubleCount, increment }
|
||||
})
|
171
frontend/src/stores/plantState.js
Normal file
171
frontend/src/stores/plantState.js
Normal file
|
@ -0,0 +1,171 @@
|
|||
import { defineStore } from 'pinia';
|
||||
import router from '@/router'; // Adjust the import based on your router setup
|
||||
import axios from 'axios';
|
||||
|
||||
export const usePlantStateStore = defineStore('plantState', {
|
||||
state: () => ({
|
||||
imageResult: null,
|
||||
textResult: null,
|
||||
plantDataDescription: '',
|
||||
conditionDataDescription: '',
|
||||
imageBase64: null,
|
||||
plantHealthDescription: '',
|
||||
generatedImageBase64: null,
|
||||
plantData: {
|
||||
plant_characteristics: {
|
||||
morphological: {
|
||||
plant_type: '',
|
||||
height: 0,
|
||||
leaf_color: '',
|
||||
presence_of_flowers: false,
|
||||
fruit_development: false,
|
||||
root_health: ''
|
||||
},
|
||||
physiological: {
|
||||
health_status: '',
|
||||
growth_rate: 0,
|
||||
photosynthesis_rate: 0,
|
||||
transpiration_rate: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
conditionData: {
|
||||
environmental_characteristics: {
|
||||
atmosphere: {
|
||||
air_temperature: 20,
|
||||
relative_humidity: 50,
|
||||
co2_concentration: 400,
|
||||
air_pressure: 101.3,
|
||||
wind_speed: 0
|
||||
},
|
||||
soil: {
|
||||
soil_temperature: 20,
|
||||
soil_acidity: 6.5,
|
||||
soil_moisture: 50,
|
||||
nutrients: []
|
||||
},
|
||||
light: {
|
||||
light_intensity: 500,
|
||||
photoperiod: 12
|
||||
},
|
||||
water: {
|
||||
watering_frequency: 3
|
||||
}
|
||||
}
|
||||
},
|
||||
time_period: '',
|
||||
cure: '',
|
||||
text_response: '',
|
||||
health: {
|
||||
health_breakdown: {
|
||||
Dead: 0.0105,
|
||||
Dehydrated: 0.0421,
|
||||
Diseased: 0.0562,
|
||||
Healthy: 0.8912
|
||||
},
|
||||
health_status: "Healthy",
|
||||
}
|
||||
}),
|
||||
actions: {
|
||||
async healthCheck() {
|
||||
// Convert base64 to Blob (File is a specialized Blob)
|
||||
const blob = await fetch(this.imageBase64).then(res => res.blob());
|
||||
|
||||
// Create a File object from the Blob
|
||||
const imageFile = new File([blob], 'image', { type: 'image/jpeg' });
|
||||
|
||||
// Create FormData and append the file
|
||||
const formData = new FormData();
|
||||
formData.append('image', imageFile);
|
||||
|
||||
const res = await axios.post('/api/analyze', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
'Access-Control-Allow-Origin': '*'
|
||||
}
|
||||
});
|
||||
|
||||
function makePercentage(number) {
|
||||
// Ensure the number is between 0 and 1
|
||||
if (number < 0 || number > 1) {
|
||||
throw new Error("Number must be between 0 and 1");
|
||||
}
|
||||
|
||||
// Convert to percentage and return as a string
|
||||
return (number * 100).toFixed();
|
||||
}
|
||||
|
||||
this.health.health_breakdown.Dead = makePercentage(res.data.health_breakdown.Dead);
|
||||
this.health.health_breakdown.Dehydrated = makePercentage(res.data.health_breakdown.Dehydrated);
|
||||
this.health.health_breakdown.Diseased = makePercentage(res.data.health_breakdown.Diseased);
|
||||
this.health.health_breakdown.Healthy = makePercentage(res.data.health_breakdown.Healthy);
|
||||
this.health.health_status = res.data.health_status;
|
||||
|
||||
this.plantData.plant_characteristics.morphological.plant_type = res.data.plant_species;
|
||||
},
|
||||
setPlantData(data) {
|
||||
this.plantData = {
|
||||
plant_characteristics: {
|
||||
...this.plantData.plant_characteristics,
|
||||
...data.plant_characteristics
|
||||
}
|
||||
};
|
||||
},
|
||||
setConditionData(data) {
|
||||
this.conditionData = {
|
||||
environmental_characteristics: {
|
||||
...this.conditionData.environmental_characteristics,
|
||||
...data.environmental_characteristics
|
||||
}
|
||||
};
|
||||
},
|
||||
async sendRequest() {
|
||||
await router.push('/loading')
|
||||
|
||||
const data = {
|
||||
plant_state_description: this.plantDataDescription,
|
||||
plant_state_details: this.plantData,
|
||||
environmental_changes_description: this.conditionDataDescription,
|
||||
environmental_changes_details: this.conditionData,
|
||||
time_period: this.time_period
|
||||
}
|
||||
|
||||
const res = await axios.post('http://localhost:3000/analyze', JSON.stringify(data), {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const imageRes = await axios.post('http://localhost:3000/generate', JSON.stringify([res.data.result.image_generator_prompt, this.imageBase64]), {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Image generation prompt:', res.data.result.image_generator_prompt);
|
||||
|
||||
this.generatedImageBase64 = imageRes.data.result;
|
||||
|
||||
this.text_response = res.data.result.final_description_text;
|
||||
await router.push('/results');
|
||||
},
|
||||
async askForCure() {
|
||||
|
||||
const res = await axios.post('http://localhost:3000/cure', JSON.stringify({
|
||||
plant_state_description: this.plantDataDescription,
|
||||
plant_state_details: this.plantData,
|
||||
time_period: this.time_period
|
||||
}), {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
this.cure = res.data.result;
|
||||
},
|
||||
setImage(base64) {
|
||||
this.imageBase64 = base64
|
||||
localStorage.setItem('uploadedImage', base64)
|
||||
},
|
||||
}
|
||||
});
|
37
frontend/src/views/ConditionsVIew.vue
Normal file
37
frontend/src/views/ConditionsVIew.vue
Normal file
|
@ -0,0 +1,37 @@
|
|||
<script setup>
|
||||
import conditionDetails from '@/components/conditionDetails.vue'
|
||||
import conditionDescription from "@/components/conditionDescription.vue";
|
||||
import Button from "primevue/button";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="plant-wrapper">
|
||||
<div class="loose">
|
||||
<h2>Insert your conditions</h2>
|
||||
<condition-description></condition-description>
|
||||
<Button type="button" label="Upload" icon="pi pi-search" :loading="loading" @click="load" />
|
||||
</div>
|
||||
<div class="precise">
|
||||
<condition-details></condition-details>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.plant-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
justify-content: start;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.loose {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
width: 30rem;
|
||||
}
|
||||
</style>
|
13
frontend/src/views/CureResultsView.vue
Normal file
13
frontend/src/views/CureResultsView.vue
Normal file
|
@ -0,0 +1,13 @@
|
|||
<script>
|
||||
export default {
|
||||
name: "CureResultsView"
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
76
frontend/src/views/EnvironmentView.vue
Normal file
76
frontend/src/views/EnvironmentView.vue
Normal file
|
@ -0,0 +1,76 @@
|
|||
<template>
|
||||
|
||||
<div class="plant-wrapper">
|
||||
<img :src="plantStateStore.imageBase64" alt="">
|
||||
<div class="loose">
|
||||
<h2>What's going to happen to your lovely plant?</h2>
|
||||
<condition-description></condition-description>
|
||||
|
||||
<h3>How long is the condition going to last?</h3>
|
||||
<InputText placeholder="E.g. 5hrs, 7 days, a month" v-model="plantStateStore.time_period"></InputText>
|
||||
</div>
|
||||
<div class="precise">
|
||||
<condition-details></condition-details>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<Button icon="pi pi-check" label="Submit" aria-label="Home" @click="plantStateStore.sendRequest()"> </Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ImageScan from '@/components/imageScan.vue'
|
||||
import conditionDescription from "@/components/conditionDescription.vue";
|
||||
import conditionDetails from '@/components/conditionDetails.vue';
|
||||
import InputText from 'primevue/inputtext';
|
||||
import Button from 'primevue/button';
|
||||
import { usePlantStateStore} from "@/stores/plantState.js";
|
||||
|
||||
const plantStateStore = usePlantStateStore();
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Playwrite+HU:wght@100..400&family=Roboto+Flex:opsz,wght@8..144,100..1000&family=Rubik:ital,wght@0,300..900;1,300..900&display=swap');
|
||||
|
||||
|
||||
h2 {
|
||||
font-family: 'Playwrite HU', sans-serif;
|
||||
font-weight: 600;
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.plant-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
justify-content: start;
|
||||
gap: 2rem;
|
||||
overflow-y: scroll;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.loose {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
img {
|
||||
max-height: 5rem;
|
||||
border-radius: 4px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
</style>
|
153
frontend/src/views/HelpView.vue
Normal file
153
frontend/src/views/HelpView.vue
Normal file
|
@ -0,0 +1,153 @@
|
|||
<script setup>
|
||||
import Button from 'primevue/button';
|
||||
import { usePlantStateStore} from "@/stores/plantState.js";
|
||||
import { watchEffect } from 'vue';
|
||||
import Card from 'primevue/card';
|
||||
import ProgressSpinner from 'primevue/progressspinner';
|
||||
|
||||
const plantStateStore = usePlantStateStore();
|
||||
|
||||
watchEffect(() => {
|
||||
document.documentElement.style.setProperty('--background-image', 'url(' + plantStateStore.imageBase64 + ')');
|
||||
});
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
onMounted(() => {
|
||||
loadHealthCheck();
|
||||
});
|
||||
|
||||
const isLoading = ref(true);
|
||||
|
||||
function loadHealthCheck() {
|
||||
plantStateStore.healthCheck().finally(() => {
|
||||
isLoading.value = false;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="row">
|
||||
<div class="card">
|
||||
<div class="content">
|
||||
<h1>Here's how your plant is feeling today</h1>
|
||||
</div>
|
||||
</div>
|
||||
<Card v-if="!isLoading">
|
||||
<template #title>
|
||||
<h3>Recognized plant: {{ plantStateStore.plantData.plant_characteristics.morphological.plant_type }}</h3>
|
||||
</template>
|
||||
<template #subtitle>
|
||||
<h4>{{ plantStateStore.health.health_status }}</h4>
|
||||
</template>
|
||||
<template #content>
|
||||
<ul>
|
||||
<li>Healthy: {{ plantStateStore.health.health_breakdown.Healthy }}% </li>
|
||||
<li>Dying: {{ plantStateStore.health.health_breakdown.Dead }}% </li>
|
||||
<li>Diseased: {{ plantStateStore.health.health_breakdown.Diseased }}% </li>
|
||||
<li>Dehydrated : {{ plantStateStore.health.health_breakdown.Dehydrated }}% </li>
|
||||
</ul>
|
||||
</template>
|
||||
</Card>
|
||||
<ProgressSpinner v-else></ProgressSpinner>
|
||||
|
||||
</div>
|
||||
<div class="under">
|
||||
<Button icon="pi pi-arrow-right" label="Continue" aria-label="Home" @click="$router.push('/simulate')"> </Button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Playwrite+HU:wght@100..400&family=Roboto+Flex:opsz,wght@8..144,100..1000&family=Rubik:ital,wght@0,300..900;1,300..900&display=swap');
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
:deep(.p-card) {
|
||||
width: 100%;
|
||||
max-width: 35rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 0.7rem;
|
||||
margin-left: 1rem;
|
||||
font-size: 110%;
|
||||
}
|
||||
|
||||
.card {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 35rem;
|
||||
height: 40rem;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image: var(--background-image);
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.card::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(
|
||||
to top,
|
||||
rgba(0, 0, 0, 0.9) 0%,
|
||||
rgba(0, 0, 0, 0.7) 30%,
|
||||
rgba(0, 0, 0, 0.4) 70%,
|
||||
rgba(0, 0, 0, 0.2) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
padding: 2rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: 'Playwrite HU', sans-serif;
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 1rem;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.under {
|
||||
display: flex;
|
||||
justify-content: stretch;
|
||||
gap: 1rem;
|
||||
margin-top: 2rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.p-button) {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
:deep(.p-button-label) {
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
39
frontend/src/views/InitialUploadView.vue
Normal file
39
frontend/src/views/InitialUploadView.vue
Normal file
|
@ -0,0 +1,39 @@
|
|||
<script setup>
|
||||
import ImageScan from "@/components/imageScan.vue";
|
||||
import Card from "primevue/card";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="upload-wrapper">
|
||||
<Card>
|
||||
<template #header>
|
||||
<img src="../../style/moss.jpg" alt="">
|
||||
</template>
|
||||
<template #title>
|
||||
<h1>Hello! Share your plant with us and we'll tell you all about it!</h1>
|
||||
</template>
|
||||
<template #content>
|
||||
<image-scan></image-scan>
|
||||
</template>
|
||||
</Card>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Playwrite+HU:wght@100..400&family=Roboto+Flex:opsz,wght@8..144,100..1000&family=Rubik:ital,wght@0,300..900;1,300..900&display=swap');
|
||||
|
||||
|
||||
h1 {
|
||||
font-family: 'Playwrite HU', sans-serif;
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
img {
|
||||
max-height: 5rem;
|
||||
width: 45rem;
|
||||
object-fit: cover;
|
||||
border-radius: 5px 5px 0 0;
|
||||
}
|
||||
</style>
|
12
frontend/src/views/LoadingView.vue
Normal file
12
frontend/src/views/LoadingView.vue
Normal file
|
@ -0,0 +1,12 @@
|
|||
<script setup>
|
||||
import ProgressSpinner from 'primevue/progressspinner';
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ProgressSpinner></ProgressSpinner>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
98
frontend/src/views/PlantCureView.vue
Normal file
98
frontend/src/views/PlantCureView.vue
Normal file
|
@ -0,0 +1,98 @@
|
|||
<template>
|
||||
|
||||
<div class="plant-wrapper">
|
||||
<div class="loose">
|
||||
<h1>Plant not doing too well?</h1>
|
||||
|
||||
<img :src="plantStateStore.imageBase64" alt="">
|
||||
|
||||
<h3>Here are some tips on how to make it feel better!</h3>
|
||||
</div>
|
||||
<div class="precise">
|
||||
<ProgressBar v-if="isLoading" mode="indeterminate" style="width: 100%; height: 2rem; margin-top: 1rem;"></ProgressBar>
|
||||
<Card v-else class="cure-card">
|
||||
<template #content>
|
||||
<ul>
|
||||
<li class="list-item" v-for="(line, index) in textResponse" :key="index">{{ line }}</li>
|
||||
</ul>
|
||||
</template>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Button from 'primevue/button';
|
||||
|
||||
import { usePlantStateStore } from "@/stores/plantState.js";
|
||||
import Textarea from "primevue/textarea";
|
||||
const plantStateStore = usePlantStateStore();
|
||||
import ProgressBar from "primevue/progressbar";
|
||||
import {computed, ref} from 'vue';
|
||||
import Card from "primevue/card";
|
||||
|
||||
const isLoading = ref(false);
|
||||
import { onMounted } from 'vue';
|
||||
onMounted(() => {
|
||||
isLoading.value = true;
|
||||
plantStateStore.askForCure().finally(() => {
|
||||
isLoading.value = false;
|
||||
});
|
||||
});
|
||||
|
||||
const textResponse = computed(() =>
|
||||
plantStateStore.cure.split('.').filter(line => line.trim() !== '').map(line => line.trim())
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Playwrite+HU:wght@100..400&family=Roboto+Flex:opsz,wght@8..144,100..1000&family=Rubik:ital,wght@0,300..900;1,300..900&display=swap');
|
||||
|
||||
|
||||
h1 {
|
||||
font-family: 'Playwrite HU', sans-serif;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.list-item {
|
||||
margin-bottom: 1rem;
|
||||
margin-left: 1.5rem;
|
||||
font-size: 110%;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.plant-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
justify-content: start;
|
||||
gap: 2rem;
|
||||
overflow-y: scroll;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.loose {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-content: center;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
img {
|
||||
max-height: 40rem;
|
||||
width: 100%;
|
||||
border-radius: 4px;
|
||||
object-fit: contain;
|
||||
}
|
||||
</style>
|
70
frontend/src/views/PlantView.vue
Normal file
70
frontend/src/views/PlantView.vue
Normal file
|
@ -0,0 +1,70 @@
|
|||
<template>
|
||||
|
||||
<div class="plant-wrapper">
|
||||
<img :src="plantStateStore.imageBase64" alt="">
|
||||
<div class="loose">
|
||||
<h2>Talk to us about your plant</h2>
|
||||
<text-description></text-description>
|
||||
</div>
|
||||
<div class="precise">
|
||||
<plant-details></plant-details>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<Button label="Simulate your plant" icon="pi pi-chart-line" size="large" @click="$router.push('/setEnvironment')"/>
|
||||
<Button label="Get care suggestions" icon="pi pi-heart" size="large" @click="$router.push('/cure')"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ImageScan from '@/components/imageScan.vue'
|
||||
import textDescription from "@/components/textDescription.vue";
|
||||
import plantDetails from '@/components/plantDetails.vue';
|
||||
import Button from 'primevue/button';
|
||||
|
||||
import { usePlantStateStore } from "@/stores/plantState.js";
|
||||
const plantStateStore = usePlantStateStore();
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Playwrite+HU:wght@100..400&family=Roboto+Flex:opsz,wght@8..144,100..1000&family=Rubik:ital,wght@0,300..900;1,300..900&display=swap');
|
||||
|
||||
h2 {
|
||||
font-family: 'Playwrite HU', sans-serif;
|
||||
font-weight: 600;
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.plant-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
justify-content: start;
|
||||
gap: 2rem;
|
||||
overflow-y: scroll;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.loose {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: stretch;
|
||||
gap: 1rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
img {
|
||||
max-height: 5rem;
|
||||
border-radius: 4px;
|
||||
object-fit: cover;
|
||||
}
|
||||
</style>
|
115
frontend/src/views/SimulationResultView.vue
Normal file
115
frontend/src/views/SimulationResultView.vue
Normal file
|
@ -0,0 +1,115 @@
|
|||
<script setup>
|
||||
import { usePlantStateStore} from "@/stores/plantState.js";
|
||||
import Card from 'primevue/card';
|
||||
import Accordion from 'primevue/accordion';
|
||||
import AccordionPanel from 'primevue/accordionpanel';
|
||||
import AccordionHeader from 'primevue/accordionheader';
|
||||
import AccordionContent from 'primevue/accordioncontent';
|
||||
|
||||
const plantStore = usePlantStateStore();
|
||||
|
||||
const textResponse = plantStore.text_response.split('.').filter(line => line.trim() !== '').map(line => line.trim());
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="simulation-result-wrapper">
|
||||
<h1>Your plant has been successfully simulated!</h1>
|
||||
|
||||
<Card >
|
||||
<template #title><i class="pi pi-cog"></i><em> Here's what we think will happen</em></template>
|
||||
<template #content>
|
||||
<ul>
|
||||
<li class="list-item" v-for="(line, index) in textResponse" :key="index">{{ line }}</li>
|
||||
</ul>
|
||||
</template>
|
||||
<template #footer>
|
||||
<a class="gray" href="https://books.google.it/books/about/Horticultural_Crop_Response_to_Different.html?id=TPmnzgEACAAJ&source=kp_book_description&redir_esc=y">Source: Horticultural Crop Response to Different Environmental and Nutritional Stress - Stefano Marino</a>
|
||||
</template>
|
||||
</Card>
|
||||
<div class="image-container">
|
||||
<img :src="plantStore.imageBase64" class="image-first" alt="First image">
|
||||
<img :src="plantStore.generatedImageBase64" class="image-second" alt="Second image">
|
||||
</div>
|
||||
|
||||
|
||||
<Accordion value="1">
|
||||
<AccordionPanel value="0">
|
||||
<AccordionHeader>Old Image</AccordionHeader>
|
||||
<AccordionContent>
|
||||
<img :src="plantStore.imageBase64" alt="Original Plant Image">
|
||||
</AccordionContent>
|
||||
</AccordionPanel>
|
||||
</Accordion>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Playwrite+HU:wght@100..400&family=Roboto+Flex:opsz,wght@8..144,100..1000&family=Rubik:ital,wght@0,300..900;1,300..900&display=swap');
|
||||
|
||||
|
||||
@keyframes fadeOut {
|
||||
from { opacity: 1; }
|
||||
to { opacity: 0; }
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: 'Playwrite HU', sans-serif;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.list-item {
|
||||
margin-bottom: 1rem;
|
||||
margin-left: 1.5rem;
|
||||
font-size: 110%;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.gray {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
img {
|
||||
margin-top: 2rem;
|
||||
max-height: 30rem;
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.image-container {
|
||||
position: relative;
|
||||
width: 45rem;
|
||||
height: 30rem;
|
||||
margin-bottom: 10rem;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.image-container img {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.image-first {
|
||||
opacity: 1;
|
||||
animation: fadeOut 2s forwards;
|
||||
animation-delay: 1s; /* Optional delay before starting */
|
||||
}
|
||||
|
||||
.image-second {
|
||||
opacity: 0;
|
||||
animation: fadeIn 2s forwards;
|
||||
animation-delay: 1; /* Match delay with fadeOut */
|
||||
}
|
||||
</style>
|
BIN
frontend/style/moss.jpg
Normal file
BIN
frontend/style/moss.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 MiB |
16
frontend/style/styles.css
Normal file
16
frontend/style/styles.css
Normal file
|
@ -0,0 +1,16 @@
|
|||
@import 'primeicons/primeicons.css';
|
||||
|
||||
#app {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.p-button-label {
|
||||
display: block;
|
||||
padding-top: 4px;
|
||||
}
|
27
frontend/vite.config.js
Normal file
27
frontend/vite.config.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
vueDevTools(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
},
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://10.100.1.111:5000',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '')
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
Loading…
Add table
Add a link
Reference in a new issue