blasisd commited on
Commit
9b6a6a6
Β·
1 Parent(s): 682fd8f

Initial commit.

Browse files
Files changed (4) hide show
  1. README.md +75 -5
  2. requirements.txt +2 -0
  3. src/app.py +283 -0
  4. src/static/styles/custom.css +343 -0
README.md CHANGED
@@ -1,14 +1,84 @@
1
  ---
2
- title: Fungi Sage Vision
3
- emoji: πŸ‘€
4
  colorFrom: gray
5
  colorTo: pink
6
  sdk: gradio
7
  sdk_version: 5.36.2
8
- app_file: app.py
9
  pinned: false
10
  license: apache-2.0
11
- short_description: 'Powered by musheff: AI that knows its fungi.'
 
 
 
 
 
 
 
 
 
12
  ---
13
 
14
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: FungiSage Vision (Gradio UI)
3
+ emoji: πŸ„πŸ€–
4
  colorFrom: gray
5
  colorTo: pink
6
  sdk: gradio
7
  sdk_version: 5.36.2
8
+ app_file: src/app.py
9
  pinned: false
10
  license: apache-2.0
11
+ short_description: "Powered by musheff: AI that knows its fungi."
12
+ tags:
13
+ - gradio
14
+ - vision
15
+ - mushroom
16
+ - musheff
17
+ datasets:
18
+ - SoFa325/12_popular_russia_mushrooms_edible_poisonous
19
+ models:
20
+ - blasisd/musheff
21
  ---
22
 
23
+ # FungiSage Vision: Where data meets deliciousβ€”or dangerous
24
+
25
+ FungiSage Vision transforms foraging into a safe, science-backed adventure. Powered by our [`musheff`](https://huggingface.co/blasisd/musheff) model (a fine-tuned EfficientNet-B3 trained on _SoFa325/12_popular_russia_mushrooms_edible_poisonous_), it identifies 12 common Russian species in secondsβ€”delivering instant edibility/toxicity alerts with the solid reliability of a mountain massif. Snap a photo, and forage with AI-powered confidence.
26
+
27
+ ## Getting Started
28
+
29
+ This guide provides step-by-step instructions to set up and run the project on your local machine for development and testing purposes. For details on deploying the project to a production environment, refer to the Deployment section.
30
+
31
+ ### Prerequisites
32
+
33
+ To set up and run this project, ensure the following software and tools are installed on your system:
34
+
35
+ - **Python**: Version `3.10.12` or higher is required. Verify your Python version by running:
36
+
37
+ ```bash
38
+ python3 --version
39
+ ```
40
+
41
+ - **Dependencies**: Install the required Python packages listed in requirements.txt using pip. Run the following command in your terminal:
42
+
43
+ ```bash
44
+ pip install -r requirements.txt
45
+ ```
46
+
47
+ - **Backend server**: This project is the frontend component only. Before proceeding, ensure you've deployed the backend server:
48
+ 1. **Deploy the backend:**
49
+ - Implementation β†’ [blasisd/musheff-api](https://huggingface.co/spaces/blasisd/musheff-api/tree/main).
50
+ 2. **Configure environment variable:**
51
+ - Set `MUSHEFF_API_ENDPOINT` in your environment to point to your live backend URL.
52
+
53
+ ### Local Development and Testing
54
+
55
+ To run the application locally for development and testing purposes, execute the following command in your terminal:
56
+
57
+ ```bash
58
+ python app.py
59
+ ```
60
+
61
+ > [!WARNING]
62
+ > Ensure you are in the project's **src** directory before running the script or adapt running path.
63
+
64
+ ## Deployment
65
+
66
+ ### Deployment on Hugging Face Spaces
67
+
68
+ To deploy the project on Hugging Face Spaces, follow these steps:
69
+
70
+ 1. Create an account on [Hugging Face](https://huggingface.co) if you don’t already have one.
71
+
72
+ 2. Refer to the official [Spaces Overview](https://huggingface.co/docs/hub/en/spaces-overview) documentation for detailed instructions on setting up and deploying your project.
73
+
74
+ ### Deployment on Other Cloud Platforms
75
+
76
+ For deployment on other cloud or live systems, consult the documentation provided by your chosen service provider. Each platform may have specific requirements and steps for deploying Python-based applications.
77
+
78
+ ## Built With
79
+
80
+ - [Python 3.10.12](http://www.python.org/) - Developing with the best programming language
81
+
82
+ ## Authors
83
+
84
+ **Vlasios Dimitriadis** - _Initial work:_ [FungiSage Vision](https://huggingface.co/spaces/blasisd/fungi-sage-vision)
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ gradio==5.35.0
2
+ python-dotenv==1.1.1
src/app.py ADDED
@@ -0,0 +1,283 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from typing import Any
3
+
4
+ import gradio as gr
5
+ import requests
6
+
7
+ from dotenv import load_dotenv
8
+
9
+
10
+ load_dotenv()
11
+
12
+
13
+ API_ENDPOINT = os.getenv("MUSHEFF_API_ENDPOINT")
14
+
15
+
16
+ def classify_image(img_path: str) -> tuple[dict[str, Any]]:
17
+ """Classifies a mushroom image, returning species, edibility, and confidence score.
18
+ In case of error returns the error.
19
+
20
+ :param img_path: the path to the mushroom image
21
+ :type img_path: str
22
+ :return: confidence score, species, edibility or error message (as output or error components)
23
+ :rtype: tuple[dict[str, Any]]
24
+ """
25
+ try:
26
+ with open(img_path, "rb") as fp:
27
+ files = {"image_file": (fp.name, fp)}
28
+
29
+ # Calling the service
30
+ response = requests.post(API_ENDPOINT, files=files)
31
+
32
+ # Handle HTTP errors
33
+ if response.status_code != 200:
34
+ error_msg = response.json().get(
35
+ "detail", f"API Error {response.status_code}"
36
+ )
37
+ return show_error(f"Backend Error: {error_msg}")
38
+
39
+ # Process successful response
40
+ data = response.json()
41
+ return show_results(data)
42
+
43
+ except requests.exceptions.Timeout:
44
+ return show_error("Request timed out. Please try again.")
45
+
46
+ except requests.exceptions.ConnectionError:
47
+ return show_error("Cannot connect to the server. Please check your connection.")
48
+ except TypeError:
49
+ return show_error("Invalid input. Please select a valid file and try again.")
50
+ except Exception as e:
51
+ return show_error(f"System error: {e}")
52
+
53
+
54
+ def show_results(data: dict) -> tuple[dict[str, Any]]:
55
+ """Show the results data retrieved from the service
56
+ in the respective output components
57
+
58
+ :param data: the data retrieved from the classify service
59
+ :type data: dict
60
+ :return: the output components
61
+ :rtype: tuple[dict[str, Any]]
62
+ """
63
+
64
+ # Create visual output components
65
+ confidence = data["confidence"]
66
+ class_name = data["mushroom_type"]
67
+ edibility = data["toxicity_profile"]
68
+
69
+ return (
70
+ # Confidence output
71
+ gr.update(
72
+ value=generate_confidence_html(confidence),
73
+ ),
74
+ # Species display
75
+ gr.update(
76
+ value=generate_class_html(class_name),
77
+ ),
78
+ # Edibility alert
79
+ gr.update(
80
+ value=generate_edibility_html(edibility, confidence, class_name),
81
+ ),
82
+ gr.update(value=""), # Error output
83
+ )
84
+
85
+
86
+ def show_error(message: str) -> tuple[dict[str, Any]]:
87
+ """Update the error output component with the error message
88
+ Nullify all the other output components
89
+
90
+ :param message: the error message
91
+ :type message: str
92
+ :return: the error message component, along with the other nullified output components
93
+ :rtype: tuple[dict[str, Any]]
94
+ """
95
+ return (
96
+ gr.update(value=""), # Confidence output
97
+ gr.update(value=""), # Class output
98
+ gr.update(value=""), # Edibility output
99
+ gr.update(value=generate_error_html(message)), # Error alert
100
+ )
101
+
102
+
103
+ def generate_confidence_html(confidence):
104
+ return f"""<div class="confidence-display" style="font-size:1.1rem">
105
+ <div class="confidence-header">
106
+ <span class="confidence-icon">πŸ“Š</span>
107
+ <span class="confidence-title">Classification Confidence</span>
108
+ </div>
109
+
110
+ <div class="confidence-visual">
111
+ <div class="confidence-bar-bg">
112
+ <div class="confidence-bar-fill" style="width: {confidence*100:.2f}%"></div>
113
+ </div>
114
+ <div class="confidence-value">{confidence*100:.2f}%</div>
115
+ </div>
116
+ </div>
117
+ """
118
+
119
+
120
+ def generate_class_html(class_name):
121
+ return f"""
122
+ <div style='
123
+ font-size: 1.5rem;
124
+ text-align: center;
125
+ padding: 20px;
126
+ background: #E3F2FD;
127
+ border-radius: 8px;
128
+ '>
129
+ πŸ„ <br><strong>{class_name.replace("_", " ")}</strong>
130
+ </div>
131
+ """
132
+
133
+
134
+ def generate_edibility_html(edibility, confidence, class_name):
135
+ if edibility == "edible":
136
+ return (
137
+ f"<div class='edible-alert'>"
138
+ f"βœ… SAFE TO EAT (with verification)<hr style='margin:10px 0;'>"
139
+ f"<div style='font-size:1.1rem'>"
140
+ f"Always confirm with mycologist before consumption"
141
+ f"</div></div>"
142
+ )
143
+ else:
144
+ return (
145
+ f"<div class='poison-alert'>"
146
+ f"☠️ <strong>POISONOUS!</strong> DO NOT CONSUME<hr style='margin:10px 0;'>"
147
+ f"<div style='font-size:1.1rem;color:var(--poison-color)'>"
148
+ f"Misidentification risk: {100 - confidence*100:.2f}%<br>"
149
+ f"<em>Immediately contact poison control if ingested</em>"
150
+ f"</div></div>"
151
+ )
152
+
153
+
154
+ def generate_error_html(message):
155
+ return f"""
156
+ <div class='error-banner'>
157
+ <span class='error-icon'>❌</span>
158
+ <strong>CLASSIFICATION FAILED</strong><br>
159
+ {message}<br>
160
+ <em>Please try again or contact support</em>
161
+ </div>
162
+ """
163
+
164
+
165
+ def toggle_row_visibility(*comp_vals):
166
+ """Update row visibility based on non empty value(s) of component(s) on row"""
167
+ return gr.Row(visible=any(comp_vals))
168
+
169
+
170
+ def handle_image_change(new_image):
171
+ """This function will be called when image changes (cleared or new one selected)"""
172
+ # New image selected or deleted (new_image or None), in any case reset output components
173
+ return (
174
+ new_image,
175
+ gr.Row(visible=False),
176
+ gr.Row(visible=False),
177
+ gr.update(visible=False),
178
+ )
179
+
180
+
181
+ def hr_line_update():
182
+ return gr.update(visible=True)
183
+
184
+
185
+ # Custom HTML components
186
+ safety_html = """
187
+ <div class="safety-banner">
188
+ <div class="warning-icon">⚠️</div>
189
+ <strong>CRITICAL SAFETY NOTICE:</strong> FungiSage Vision provides probabilistic guidance only - not guarantees.
190
+ Mushroom misidentification can be fatal. Always consult certified mycologists before consumption.
191
+ </div>
192
+ """
193
+
194
+ slogan_html = """
195
+ <div class="slogan-container">
196
+ <div class="mushroom-icon">πŸ„</div>
197
+ <div class="slogan-text">
198
+ <span class="brand-tagline">Massif Mushroom Intelligence.</span>
199
+ <div class="brand-action">
200
+ <span class="brand-name">FungiSage Vision</span> Guides,
201
+ <span class="verify-emphasis">You Verify.</span>
202
+ </div>
203
+ </div>
204
+ </div>
205
+ """
206
+
207
+ # CSS file path
208
+ css_path = os.path.join(os.path.dirname(__file__), "static/styles/custom.css")
209
+
210
+ with gr.Blocks(
211
+ theme=gr.themes.Glass(),
212
+ css_paths=css_path,
213
+ ) as demo:
214
+ gr.HTML(safety_html)
215
+ gr.HTML(slogan_html)
216
+
217
+ # Input section
218
+ with gr.Group(elem_id="inputsContainer"):
219
+ gr.Markdown("### Upload Mushroom Image", elem_id="uploadHeader")
220
+ image_input = gr.Image(type="filepath", label="", height=300)
221
+ classify_btn = gr.Button(
222
+ "Classify Mushroom", elem_id="classifyBtn", variant="primary"
223
+ )
224
+
225
+ # Output section
226
+ with gr.Group(elem_id="outputsContainer"):
227
+ with gr.Row(visible=False, scale=1, equal_height=True) as results_group:
228
+ with gr.Column(scale=1, elem_id="confCol"):
229
+ confidence_output = gr.HTML(
230
+ label="Confidence Level", elem_classes="output-card"
231
+ )
232
+
233
+ with gr.Column(scale=2, elem_id="speciesCol"):
234
+ class_output = gr.HTML(
235
+ label="Identified Species", elem_classes="output-card"
236
+ )
237
+
238
+ with gr.Column(scale=1, elem_id="edibilityCol"):
239
+ edibility_output = gr.HTML(
240
+ label="Safety Assessment", elem_classes="output-card"
241
+ )
242
+
243
+ with gr.Row(visible=False) as errors_group:
244
+ error_output = gr.HTML(elem_classes="error-card")
245
+
246
+ # Used only for auto scrolling when results or errors occur
247
+ bottom_line = gr.HTML("<hr>", visible=False, elem_id="bottomLine")
248
+
249
+ # Classification function called on button click
250
+ classify_btn.click(
251
+ fn=classify_image,
252
+ inputs=image_input,
253
+ outputs=[
254
+ confidence_output,
255
+ class_output,
256
+ edibility_output,
257
+ error_output,
258
+ ],
259
+ ).then(
260
+ fn=toggle_row_visibility,
261
+ inputs=[confidence_output, class_output, edibility_output],
262
+ outputs=results_group, # If any of the result outputs (got results from service), then show results row
263
+ ).then(
264
+ fn=toggle_row_visibility,
265
+ inputs=[error_output],
266
+ outputs=errors_group, # If error occurred, then show errors row
267
+ ).then(
268
+ fn=hr_line_update,
269
+ inputs=[],
270
+ outputs=[bottom_line],
271
+ scroll_to_output=True, # Useful to scroll there after results or errors occur
272
+ )
273
+
274
+ # Handle image changes (upload or clear/hide rows with results and errors)
275
+ image_input.change(
276
+ fn=handle_image_change,
277
+ inputs=image_input,
278
+ outputs=[image_input, results_group, errors_group, bottom_line],
279
+ )
280
+
281
+
282
+ if __name__ == "__main__":
283
+ demo.launch()
src/static/styles/custom.css ADDED
@@ -0,0 +1,343 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ===== DESIGN SYSTEM ===== */
2
+ :root {
3
+ /* Colors */
4
+ --primary-green: #2e7d32;
5
+ --light-green: #e8f5e9;
6
+ --dark-green: #1b5e20;
7
+ --warning-red: #d32f2f;
8
+ --light-red: #ffebee;
9
+ --text-dark: #333;
10
+ --text-light: #f8f9fa;
11
+
12
+ /* Spacing */
13
+ --spacing-xs: 0.5rem;
14
+ --spacing-sm: 0.8rem;
15
+ --spacing-md: 1.2rem;
16
+ --spacing-lg: 1.8rem;
17
+ --spacing-xl: 2.4rem;
18
+
19
+ /* Typography */
20
+ --font-size-xs: 0.8rem;
21
+ --font-size-sm: 0.9rem;
22
+ --font-size-base: 1rem;
23
+ --font-size-md: 1.2rem;
24
+ --font-size-lg: 1.5rem;
25
+ --font-size-xl: 2rem;
26
+
27
+ /* Effects */
28
+ --shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.1);
29
+ --border-radius: 0.5rem;
30
+ }
31
+
32
+ /* ===== SAFETY BANNER ===== */
33
+ .safety-banner {
34
+ background: var(--light-red);
35
+ border: 0.15rem solid var(--warning-red);
36
+ border-radius: var(--border-radius);
37
+ padding: var(--spacing-md);
38
+ margin: 0 auto var(--spacing-lg) auto;
39
+ max-width: 60vw;
40
+ text-align: center;
41
+ box-shadow: var(--shadow);
42
+ font-size: var(--font-size-xs);
43
+ line-height: 1.5;
44
+ }
45
+
46
+ .warning-icon {
47
+ display: inline-block;
48
+ margin-right: var(--spacing-xs);
49
+ font-size: var(--font-size-lg);
50
+ }
51
+
52
+ /* ===== SLOGAN CONTAINER ===== */
53
+ .slogan-container {
54
+ background: var(--light-green);
55
+ border: 0.15rem solid var(--primary-green);
56
+ border-radius: var(--border-radius);
57
+ padding: var(--spacing-md);
58
+ margin: 0 auto var(--spacing-lg) auto;
59
+ max-width: 90vw;
60
+ display: flex;
61
+ align-items: center;
62
+ justify-content: center;
63
+ gap: var(--spacing-md);
64
+ box-shadow: var(--shadow);
65
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
66
+ sans-serif;
67
+ }
68
+
69
+ .mushroom-icon {
70
+ font-size: var(--font-size-xl);
71
+ flex-shrink: 0;
72
+ }
73
+
74
+ .slogan-text {
75
+ text-align: center;
76
+ line-height: 1.4;
77
+ }
78
+
79
+ .brand-tagline {
80
+ font-size: var(--font-size-lg);
81
+ font-weight: 600;
82
+ color: var(--dark-green);
83
+ display: block;
84
+ }
85
+
86
+ .brand-action {
87
+ font-size: var(--font-size-md);
88
+ color: var(--text-dark);
89
+ margin-top: var(--spacing-xs);
90
+ }
91
+
92
+ .brand-name {
93
+ font-weight: 700;
94
+ color: var(--dark-green);
95
+ text-decoration: underline;
96
+ text-underline-offset: 0.2rem;
97
+ }
98
+
99
+ .verify-emphasis {
100
+ font-weight: 700;
101
+ color: var(--warning-red);
102
+ }
103
+
104
+ /* ===== DARK MODE ADJUSTMENTS ===== */
105
+ .dark .safety-banner {
106
+ background: #4a1c1c;
107
+ border-color: #e53935;
108
+ }
109
+
110
+ .dark .slogan-container {
111
+ background: #1a2a1a;
112
+ border-color: var(--primary-green);
113
+ }
114
+
115
+ .dark .brand-tagline,
116
+ .dark .brand-name {
117
+ color: #a5d6a7;
118
+ }
119
+
120
+ .dark .brand-action {
121
+ color: #e0e0e0;
122
+ }
123
+
124
+ .dark .verify-emphasis {
125
+ color: #ff8a80;
126
+ }
127
+
128
+ /* ===== RESPONSIVE ADJUSTMENTS ===== */
129
+ @media (max-width: 48rem) {
130
+ /* β‰ˆ768px */
131
+ .safety-banner,
132
+ .slogan-container {
133
+ max-width: 95vw;
134
+ padding: var(--spacing-sm);
135
+ }
136
+
137
+ .slogan-container {
138
+ flex-direction: column;
139
+ gap: var(--spacing-sm);
140
+ }
141
+
142
+ .brand-tagline {
143
+ font-size: var(--font-size-md);
144
+ }
145
+
146
+ .brand-action {
147
+ font-size: var(--font-size-base);
148
+ }
149
+ }
150
+
151
+ /* Main containers */
152
+ #inputsContainer,
153
+ #outputsContainer {
154
+ background: white;
155
+ border-radius: var(--border-radius);
156
+ box-shadow: var(--shadow);
157
+ padding: var(--spacing-md);
158
+ margin-bottom: var(--spacing-lg);
159
+ max-width: 90vw;
160
+ margin-left: auto;
161
+ margin-right: auto;
162
+ }
163
+
164
+ /* Input section */
165
+ #inputsContainer {
166
+ text-align: center;
167
+ border-top: 0.4rem solid var(--primary-green);
168
+ }
169
+
170
+ /* Source selection icons */
171
+ #inputsContainer .image-container .source-selection .icon {
172
+ width: 2.5rem;
173
+ height: 2.5rem;
174
+ }
175
+
176
+ /* Button styling */
177
+ #classifyBtn {
178
+ background: var(--primary-green) !important;
179
+ color: white !important;
180
+ font-weight: 600;
181
+ font-size: 1.1rem;
182
+ padding: 0.8em 1.6em;
183
+ border-radius: 0.3em;
184
+ margin-top: var(--spacing-sm);
185
+ transition: all 0.3s ease;
186
+ }
187
+
188
+ #classifyBtn:hover {
189
+ background: var(--dark-green) !important;
190
+ transform: translateY(-2px);
191
+ box-shadow: 0 6px 8px rgba(0, 0, 0, 0.15);
192
+ }
193
+
194
+ /* Output columns */
195
+ #confCol,
196
+ #speciesCol,
197
+ #edibilityCol {
198
+ padding: var(--spacing-md);
199
+ border-radius: var(--border-radius);
200
+ background: var(--light-green);
201
+ align-content: center;
202
+ }
203
+
204
+ #speciesCol {
205
+ background: #f1f8e9; /* Slightly different shade */
206
+ }
207
+
208
+ /* Column headers */
209
+ .gr-output-label {
210
+ display: block;
211
+ font-weight: 700 !important;
212
+ font-size: 1.1rem !important;
213
+ color: var(--primary-green) !important;
214
+ margin-bottom: 0.6rem !important;
215
+ text-align: center;
216
+ }
217
+
218
+ /* Results cards */
219
+ .gr-output-html {
220
+ background: white !important;
221
+ border-radius: calc(var(--border-radius) - 0.2rem) !important;
222
+ padding: var(--spacing-sm) !important;
223
+ box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08);
224
+ min-height: 6rem;
225
+ display: flex;
226
+ align-items: center;
227
+ justify-content: center;
228
+ text-align: center;
229
+ }
230
+
231
+ /* Error styling */
232
+ #errors_group {
233
+ background: var(--light-red);
234
+ border: 1px solid var(--warning-red);
235
+ border-radius: var(--border-radius);
236
+ padding: var(--spacing-md);
237
+ margin-top: var(--spacing-md);
238
+ }
239
+
240
+ /* Responsive layout */
241
+ @media (max-width: 768px) {
242
+ #outputsContainer .gr-row {
243
+ flex-direction: column;
244
+ }
245
+
246
+ #confCol,
247
+ #speciesCol,
248
+ #edibilityCol {
249
+ margin-bottom: var(--spacing-sm);
250
+ }
251
+ }
252
+
253
+ /* Dark mode adjustments */
254
+ .dark #inputsContainer,
255
+ .dark #outputsContainer {
256
+ background: #1e1e1e;
257
+ }
258
+
259
+ .dark .gr-output-html {
260
+ background: #2d2d2d !important;
261
+ color: var(--text-light) !important;
262
+ }
263
+
264
+ .output-card {
265
+ width: fit-content;
266
+ }
267
+
268
+ /* Confidence Display */
269
+ .confidence-display {
270
+ background: white;
271
+ border-radius: 12px;
272
+ padding: 1.5rem;
273
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
274
+ margin-bottom: 1.5rem;
275
+ }
276
+
277
+ .confidence-header {
278
+ display: flex;
279
+ align-items: center;
280
+ margin-bottom: 1rem;
281
+ gap: 0.75rem;
282
+ font-size: 1.25rem;
283
+ font-weight: 600;
284
+ color: #2e7d32;
285
+ }
286
+
287
+ .confidence-icon {
288
+ font-size: 1.5rem;
289
+ }
290
+
291
+ .confidence-visual {
292
+ display: flex;
293
+ align-items: center;
294
+ gap: 1rem;
295
+ margin-bottom: 0.75rem;
296
+ }
297
+
298
+ .confidence-bar-bg {
299
+ flex-grow: 1;
300
+ height: 1.5rem;
301
+ background: #e0e0e0;
302
+ border-radius: 10px;
303
+ overflow: hidden;
304
+ }
305
+
306
+ .confidence-bar-fill {
307
+ height: 100%;
308
+ background: linear-gradient(90deg, #4caf50, #8bc34a);
309
+ border-radius: 10px;
310
+ transition: width 0.5s ease-in-out;
311
+ }
312
+
313
+ .confidence-value {
314
+ min-width: 3.5rem;
315
+ font-weight: 700;
316
+ font-size: 1.25rem;
317
+ text-align: center;
318
+ color: #2e7d32;
319
+ }
320
+
321
+ /* Responsive adjustments */
322
+ @media (max-width: 768px) {
323
+ .confidence-header {
324
+ font-size: 1.1rem;
325
+ }
326
+
327
+ .confidence-visual {
328
+ flex-direction: column;
329
+ gap: 0.5rem;
330
+ }
331
+
332
+ .confidence-bar-bg {
333
+ width: 100%;
334
+ }
335
+
336
+ .confidence-value {
337
+ align-self: flex-end;
338
+ }
339
+ }
340
+
341
+ #bottomLine {
342
+ height: 0.9px;
343
+ }