valegro commited on
Commit
9401f73
·
verified ·
1 Parent(s): ca7ea29

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +268 -139
app.py CHANGED
@@ -5,16 +5,17 @@ import torch.nn.functional as F
5
  import pandas as pd
6
  import numpy as np
7
  from PIL import Image
 
8
 
9
- # =========================
10
- # 1) LIBRERIA OUTPUT: possibili nuovi usi
11
- # Ognuno ha un "mini embedding" e un'immagine di esempio
12
- # =========================
13
  output_libreria = {
14
  "ArmadioAnta": {
15
- "embedding": [1.0, 0.0], # fittizio
16
  "descrizione": "Riutilizzo come anta armadio",
17
- "img": "armadio.jpg" # se ce l'hai, o lascialo vuoto
18
  },
19
  "DockRigenerato": {
20
  "embedding": [0.5, 0.5],
@@ -27,50 +28,81 @@ output_libreria = {
27
  "img": "vetrina.jpg"
28
  }
29
  }
30
- # In un caso reale potresti mettere molte voci, con embeddings più ricchi
31
- # e parametri più precisi
32
-
33
- # =========================
34
- # 2) MODELLO AI: un MLP
35
- # Input: [volume, area, lunghezza, spessore, usura, (materiale cod?), + output_embedding(2)]
36
- # Output: punteggio compatibilità (float)
37
- # =========================
38
  class SimpleMLP(nn.Module):
39
  def __init__(self):
40
  super().__init__()
41
- self.fc1 = nn.Linear(8, 16)
42
- self.fc2 = nn.Linear(16, 8)
43
- self.fc3 = nn.Linear(8, 1)
44
 
45
  def forward(self, x):
46
  x = F.relu(self.fc1(x))
47
  x = F.relu(self.fc2(x))
48
- x = torch.sigmoid(self.fc3(x)) # punteggio tra 0 e 1
49
  return x
50
 
51
- # =========================
52
- # Salviamo il modello in session_state per poterlo usare da più sezioni
53
- # =========================
54
- if "model" not in st.session_state:
55
- st.session_state["model"] = SimpleMLP()
56
- if "trained" not in st.session_state:
57
- st.session_state["trained"] = False
58
-
59
- # Ottimizziamo i param in session
60
- optimizer = torch.optim.Adam(st.session_state["model"].parameters(), lr=1e-3)
61
-
62
- # ============== FUNZIONE DI TRAIN FAKE ================
63
- def train_fake_model(epochs=20, batch_size=32):
64
- """
65
- Genera un dataset fittizio di EoL + output => score
66
- e allena un MLP in session_state["model"].
67
- """
68
- # Generiamo un dataset random con 1000 esempi
69
- # EoL -> (vol, area, lunghezza, spessore, usura, materialeCod)
70
- # Output -> 2D embedding
71
- # Score -> random 0..1 ma correlato un po' a "logica" fittizia
72
- n = 1000
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  rng = np.random.default_rng(42)
 
74
  vol = rng.normal(50,15,n)
75
  area = rng.normal(20,5,n)
76
  lung = rng.normal(100,30,n)
@@ -78,135 +110,232 @@ def train_fake_model(epochs=20, batch_size=32):
78
  usura = rng.uniform(0,1,n)
79
  materiale = rng.integers(0,3,n).astype(float)
80
 
81
- # Scegli output a caso
82
  out_keys = list(output_libreria.keys())
83
- output_idx = rng.integers(0,len(out_keys),n)
84
- # costruiamo embedding 2D
85
- out_emb = []
86
- for i in output_idx:
87
- key = out_keys[i]
88
- out_emb.append(output_libreria[key]["embedding"])
89
- out_emb = np.array(out_emb)
90
 
91
  # Score fittizio
92
- # Esempio: se volume < 200 e usura < 0.5 => punteggio +
93
- # + un offset se output = "ArmadioAnta" e spess < 3 => punteggio alto
94
  base_score = 0.5 * np.ones(n)
95
  base_score -= (usura * 0.3)
96
  base_score -= ((vol - 200)/400)
97
- # se spess < 3 e out=Armadio => +0.2
98
- # se out=DockRigenerato => +0.1 random
99
  for i in range(n):
100
- if out_keys[output_idx[i]]=="ArmadioAnta" and spess[i]<3:
101
  base_score[i]+=0.2
102
- if out_keys[output_idx[i]]=="DockRigenerato":
103
- base_score[i]+=rng.uniform(0,0.1)
104
- # bounding punteggio
105
- base_score = np.clip(base_score,0,1)
106
 
107
- X = np.column_stack([vol,area,lung,spess,usura,materiale,out_emb])
108
  y = base_score.reshape(-1,1)
109
 
110
- # Torch
111
- X_t = torch.tensor(X, dtype=torch.float32)
112
- y_t = torch.tensor(y, dtype=torch.float32)
113
 
114
- dataset = torch.utils.data.TensorDataset(X_t, y_t)
115
- loader = torch.utils.data.DataLoader(dataset, batch_size=batch_size, shuffle=True)
 
116
 
117
- model = st.session_state["model"]
118
- # reinit param
119
- def weights_init(m):
120
  if isinstance(m, nn.Linear):
121
  nn.init.xavier_uniform_(m.weight)
122
  nn.init.zeros_(m.bias)
123
- model.apply(weights_init)
124
 
125
  lossf = nn.MSELoss()
126
  for ep in range(epochs):
127
- totloss=0
128
  for xb,yb in loader:
129
- optimizer.zero_grad()
130
  pred = model(xb)
131
  loss=lossf(pred,yb)
132
  loss.backward()
133
- optimizer.step()
134
- totloss+=loss.item()
135
- st.session_state["trained"] = True
136
- return totloss
137
-
138
- # ============== UI Streamlit ================
139
- st.title("WEEKO Flusso: EoL + Output-libreria + AI + Overlay Estetico")
140
-
141
- # TAB: Train AI + Avanzamento
142
- tab1, tab2, tab3 = st.tabs(["1) Train AI (finto)","2) Valuta EoL + Output","3) Overlay Estetico"])
143
-
144
- with tab1:
145
- st.subheader("Fase 1: Train fittizio del modello AI")
146
- st.write("Generiamo un dataset casuale simulando molte componenti EoL e possibili output.")
147
- if st.button("🏋️ Esegui training fittizio"):
148
- final_loss = train_fake_model(epochs=20)
149
- st.success(f"Training completato, loss={final_loss:.4f}")
150
- if st.session_state["trained"]:
151
- st.info("Modello AI pronto all'uso.")
152
- else:
153
- st.warning("Non hai ancora addestrato il modello fittizio, i punteggi saranno casuali.")
154
-
155
- with tab2:
156
- st.subheader("Fase 2: Inserisci EoL e scegli un Output")
157
-
158
- # Carico eventuale foto EoL
159
- file_eol = st.file_uploader("Foto EoL (opzionale)", type=["jpg","png"], key="eolFile2")
160
- if file_eol:
161
- st.image(file_eol, caption="Componente EoL", use_column_width=True)
162
-
163
- # Dati EoL
164
- vol_in = st.number_input("Volume (cm³)", 0.0, 1000.0, 100.0)
165
- area_in = st.number_input("Area (cm²)", 0.0, 500.0, 30.0)
166
- lung_in = st.number_input("Lunghezza (mm)", 0.0, 2000.0, 120.0)
167
- spess_in = st.number_input("Spessore (mm)", 0.5, 10.0, 2.0)
168
- usura_in = st.slider("Usura (0=nuovo,1=molto usurato)", 0.0,1.0,0.3)
169
- mat_in = st.selectbox("Materiale", ["Plastica","Metallo","PCB"])
170
- mat_code = {"Plastica":0.0,"Metallo":1.0,"PCB":2.0}[mat_in]
171
-
172
- # Scegli un output
173
- scelte = list(output_libreria.keys())
174
- out_sel = st.selectbox("Scegli l'output-libreria", scelte)
175
-
176
- if st.button("Calcola punteggio AI"):
177
- model = st.session_state["model"]
178
- embed = output_libreria[out_sel]["embedding"] # 2D
179
- x_np = np.array([[vol_in, area_in, lung_in, spess_in, usura_in, mat_code, embed[0], embed[1]]], dtype=np.float32)
180
- x_t = torch.from_numpy(x_np)
181
- with torch.no_grad():
182
- pred = model(x_t).item()
183
- st.info(f"Punteggio di compatibilità con '{out_sel}': {pred:.3f}")
184
- if pred>0.7:
185
- st.success("Ottima compatibilità! Procedi con la rigenerazione.")
186
- elif pred>0.4:
187
- st.warning("Compatibilità media: valuta modifiche/manuale rifinitura.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
  else:
189
- st.error("Compatibilità bassa, meglio cambiare output o scartare.")
 
 
 
 
 
 
 
 
 
 
 
 
190
 
191
- with tab3:
192
- st.subheader("Fase 3: Overlay Estetico")
193
- st.write("Carica la foto EoL e la foto dell'Output finale per un effetto wow. (Puramente estetico)")
 
 
194
 
195
  col1, col2 = st.columns(2)
196
  with col1:
197
- file_eol_3 = st.file_uploader("Foto EoL", type=["jpg","png"], key="uplEol3")
198
  with col2:
199
- file_out_3 = st.file_uploader("Foto Oggetto Finale (es. Armadio?)", type=["jpg","png"], key="uplOut3")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
200
 
201
- alpha_val = st.slider("Trasparenza EoL", 0.0,1.0,0.5)
 
 
 
 
202
 
203
- if file_eol_3 and file_out_3:
204
- eol_img = Image.open(file_eol_3).convert("RGBA")
205
- out_img = Image.open(file_out_3).convert("RGBA")
206
- # ridimensioniamo la EoL all'immagine dell'oggetto
207
- eol_img = eol_img.resize(out_img.size)
208
 
209
- blend = Image.blend(out_img, eol_img, alpha_val)
210
- st.image(blend, caption="Overlay EoL + Oggetto Finale", use_column_width=True)
 
 
 
211
  else:
212
- st.info("Attendi di caricare entrambe le immagini per vedere l'overlay.")
 
5
  import pandas as pd
6
  import numpy as np
7
  from PIL import Image
8
+ import os
9
 
10
+ # -------------------------------------------------------------------
11
+ # OUTPUT-LIBRERIA: possibili destinazioni d'uso
12
+ # con embedding fittizio e immagine di riferimento
13
+ # -------------------------------------------------------------------
14
  output_libreria = {
15
  "ArmadioAnta": {
16
+ "embedding": [1.0, 0.0],
17
  "descrizione": "Riutilizzo come anta armadio",
18
+ "img": "armadio.jpg"
19
  },
20
  "DockRigenerato": {
21
  "embedding": [0.5, 0.5],
 
28
  "img": "vetrina.jpg"
29
  }
30
  }
31
+
32
+ # -------------------------------------------------------------------
33
+ # MLP per compatibilità
34
+ # -------------------------------------------------------------------
 
 
 
 
35
  class SimpleMLP(nn.Module):
36
  def __init__(self):
37
  super().__init__()
38
+ self.fc1 = nn.Linear(8,16)
39
+ self.fc2 = nn.Linear(16,8)
40
+ self.fc3 = nn.Linear(8,1)
41
 
42
  def forward(self, x):
43
  x = F.relu(self.fc1(x))
44
  x = F.relu(self.fc2(x))
45
+ x = torch.sigmoid(self.fc3(x)) # punteggio 0..1
46
  return x
47
 
48
+ # -------------------------------------------------------------------
49
+ # VAE per "generative reuse" (mini, 4D)
50
+ # -------------------------------------------------------------------
51
+ class MiniVAE(nn.Module):
52
+ def __init__(self, input_dim=4, latent_dim=2):
53
+ super().__init__()
54
+ self.fc1 = nn.Linear(input_dim, 8)
55
+ self.fc21 = nn.Linear(8, latent_dim)
56
+ self.fc22 = nn.Linear(8, latent_dim)
57
+ self.fc3 = nn.Linear(latent_dim, 8)
58
+ self.fc4 = nn.Linear(8, input_dim)
59
+
60
+ def encode(self, x):
61
+ h = F.relu(self.fc1(x))
62
+ return self.fc21(h), self.fc22(h)
63
+
64
+ def reparameterize(self, mu, logvar):
65
+ std = torch.exp(0.5*logvar)
66
+ eps = torch.randn_like(std)
67
+ return mu + eps*std
68
+
69
+ def decode(self, z):
70
+ h = F.relu(self.fc3(z))
71
+ return self.fc4(h)
72
+
73
+ def forward(self, x):
74
+ mu, logvar = self.encode(x)
75
+ z = self.reparameterize(mu, logvar)
76
+ recon = self.decode(z)
77
+ return recon, mu, logvar
78
+
79
+ def vae_loss(recon_x, x, mu, logvar):
80
+ mse = F.mse_loss(recon_x, x, reduction='sum')
81
+ kld = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp())
82
+ return mse + kld
83
+
84
+ # -------------------------------------------------------------------
85
+ # Session state
86
+ # -------------------------------------------------------------------
87
+ if "mlp" not in st.session_state:
88
+ st.session_state["mlp"] = SimpleMLP()
89
+ if "trained_mlp" not in st.session_state:
90
+ st.session_state["trained_mlp"] = False
91
+
92
+ if "vae" not in st.session_state:
93
+ st.session_state["vae"] = MiniVAE()
94
+ if "trained_vae" not in st.session_state:
95
+ st.session_state["trained_vae"] = False
96
+
97
+ optimizer_mlp = torch.optim.Adam(st.session_state["mlp"].parameters(), lr=1e-3)
98
+ optimizer_vae = torch.optim.Adam(st.session_state["vae"].parameters(), lr=1e-3)
99
+
100
+ # -------------------------------------------------------------------
101
+ # Funzione training MLP (fittizio)
102
+ # -------------------------------------------------------------------
103
+ def train_fake_mlp(epochs=20, batch_size=32):
104
  rng = np.random.default_rng(42)
105
+ n = 1000
106
  vol = rng.normal(50,15,n)
107
  area = rng.normal(20,5,n)
108
  lung = rng.normal(100,30,n)
 
110
  usura = rng.uniform(0,1,n)
111
  materiale = rng.integers(0,3,n).astype(float)
112
 
 
113
  out_keys = list(output_libreria.keys())
114
+ out_idx = rng.integers(0,len(out_keys),n)
115
+ out_embed = []
116
+ for i in out_idx:
117
+ emb = output_libreria[out_keys[i]]["embedding"]
118
+ out_embed.append(emb)
119
+ out_embed = np.array(out_embed)
 
120
 
121
  # Score fittizio
 
 
122
  base_score = 0.5 * np.ones(n)
123
  base_score -= (usura * 0.3)
124
  base_score -= ((vol - 200)/400)
125
+ # Aggiustiamo se out=Armadio e spess < 3 => +0.2
 
126
  for i in range(n):
127
+ if out_keys[out_idx[i]]=="ArmadioAnta" and spess[i]<3:
128
  base_score[i]+=0.2
129
+ base_score = np.clip(base_score, 0, 1)
 
 
 
130
 
131
+ X = np.column_stack([vol,area,lung,spess,usura,materiale,out_embed])
132
  y = base_score.reshape(-1,1)
133
 
134
+ X_t = torch.tensor(X,dtype=torch.float32)
135
+ y_t = torch.tensor(y,dtype=torch.float32)
 
136
 
137
+ dataset = torch.utils.data.TensorDataset(X_t,y_t)
138
+ loader = torch.utils.data.DataLoader(dataset,batch_size=batch_size,shuffle=True)
139
+ model = st.session_state["mlp"]
140
 
141
+ def reinit(m):
 
 
142
  if isinstance(m, nn.Linear):
143
  nn.init.xavier_uniform_(m.weight)
144
  nn.init.zeros_(m.bias)
145
+ model.apply(reinit)
146
 
147
  lossf = nn.MSELoss()
148
  for ep in range(epochs):
149
+ tot=0
150
  for xb,yb in loader:
151
+ optimizer_mlp.zero_grad()
152
  pred = model(xb)
153
  loss=lossf(pred,yb)
154
  loss.backward()
155
+ optimizer_mlp.step()
156
+ tot+=loss.item()
157
+ st.session_state["trained_mlp"] = True
158
+ return tot
159
+
160
+ # -------------------------------------------------------------------
161
+ # Funzione training VAE (generative reuse)
162
+ # -------------------------------------------------------------------
163
+ def train_fake_vae(epochs=20, batch_size=32):
164
+ # mini VAE su 4 feature:
165
+ # es. [dim1, dim2, spessore, decorazione?] => generare "nuove proposte"
166
+ rng = np.random.default_rng(123)
167
+ n = 500
168
+ dim1 = rng.normal(50,10,n) # es. lung dimensione
169
+ dim2 = rng.normal(20,5,n) # es. larg dimensione
170
+ spess = rng.uniform(0.5,5,n)
171
+ dec = rng.uniform(0,1,n) # "livello decoro" fittizio
172
+
173
+ data = np.column_stack([dim1,dim2,spess,dec])
174
+ data_t = torch.tensor(data,dtype=torch.float32)
175
+ dataset = torch.utils.data.TensorDataset(data_t)
176
+ loader = torch.utils.data.DataLoader(dataset, batch_size=batch_size, shuffle=True)
177
+
178
+ vae = st.session_state["vae"]
179
+ def reinit_vae(m):
180
+ if isinstance(m,nn.Linear):
181
+ nn.init.xavier_uniform_(m.weight)
182
+ nn.init.zeros_(m.bias)
183
+ vae.apply(reinit_vae)
184
+
185
+ lossf = vae_loss
186
+ for ep in range(epochs):
187
+ total=0
188
+ for (xb,) in loader:
189
+ optimizer_vae.zero_grad()
190
+ recon, mu, logvar = vae(xb)
191
+ loss=lossf(recon, xb, mu, logvar)
192
+ loss.backward()
193
+ optimizer_vae.step()
194
+ total+=loss.item()
195
+ st.session_state["trained_vae"] = True
196
+ return total
197
+
198
+ # -------------------------------------------------------------------
199
+ # Funzione per salvare feedback
200
+ # -------------------------------------------------------------------
201
+ def salva_feedback(eol_desc, out_desc, punteggio, decisione):
202
+ # Scrive su un CSV fittizio
203
+ row = f"{eol_desc},{out_desc},{punteggio:.2f},{decisione}\n"
204
+ with open("feedback.csv","a") as f:
205
+ f.write(row)
206
+
207
+ # -------------------------------------------------------------------
208
+ # STREAMLIT APP
209
+ # -------------------------------------------------------------------
210
+ st.set_page_config(page_title="Weeko Incrementale", layout="wide")
211
+ st.title("WEEKO – App Incrementale")
212
+
213
+ tabs = st.tabs(["1) Train AI","2) Valuta EoL + Output","3) Overlay Estetico","4) Generative Reuse","5) Feedback"])
214
+
215
+ # ---------------------------
216
+ # TAB 1 - Train AI
217
+ # ---------------------------
218
+ with tabs[0]:
219
+ st.subheader("Fase 1: Train AI fittizio")
220
+ colA, colB = st.columns(2)
221
+ with colA:
222
+ if st.button("🏋️ Train MLP x compatibilità"):
223
+ final_loss = train_fake_mlp(epochs=20)
224
+ st.success(f"MLP trained, last loss={final_loss:.2f}")
225
+ with colB:
226
+ if st.button("🎨 Train VAE x generative reuse"):
227
+ final_loss2 = train_fake_vae(epochs=20)
228
+ st.success(f"VAE trained, last loss={final_loss2:.2f}")
229
+
230
+ st.write("**MLP** = calcolo compatibilità EoL → output. **VAE** = generazione nuove soluzioni (ingombri, spessore, decorazione).")
231
+ if os.path.exists("feedback.csv"):
232
+ st.write("Feedback attuale:")
233
+ st.code(open("feedback.csv","r").read())
234
+
235
+ # ---------------------------
236
+ # TAB 2 - Valuta EoL + Output
237
+ # ---------------------------
238
+ with tabs[1]:
239
+ st.subheader("Fase 2: Inserisci EoL e seleziona output-libreria (AI MLP)")
240
+
241
+ file_eol_2 = st.file_uploader("Foto EoL (opzionale)", type=["jpg","png"], key="Eol2")
242
+ if file_eol_2:
243
+ st.image(file_eol_2, caption="Componente EoL", use_column_width=True)
244
+
245
+ vol = st.number_input("Volume (cm³)",0.0,1000.0,100.0)
246
+ area= st.number_input("Area (cm²)",0.0,500.0,50.0)
247
+ lung= st.number_input("Lunghezza (mm)",0.0,2000.0,120.0)
248
+ spess= st.number_input("Spessore (mm)",0.5,10.0,2.0)
249
+ usura= st.slider("Usura",0.0,1.0,0.3)
250
+ mat = st.selectbox("Materiale",["Plastica","Metallo","PCB"])
251
+ mat_code = {"Plastica":0.0,"Metallo":1.0,"PCB":2.0}[mat]
252
+
253
+ out_choices = list(output_libreria.keys())
254
+ out_sel = st.selectbox("Scegli Output-libreria", out_choices)
255
+
256
+ if st.button("Calcola punteggio compatibilità MLP"):
257
+ if not st.session_state["trained_mlp"]:
258
+ st.warning("MLP non addestrato, premi su 'Train MLP' nel Tab 1")
259
  else:
260
+ model = st.session_state["mlp"]
261
+ emb = output_libreria[out_sel]["embedding"]
262
+ x_np = np.array([[vol,area,lung,spess,usura,mat_code,emb[0],emb[1]]],dtype=np.float32)
263
+ x_t = torch.from_numpy(x_np)
264
+ with torch.no_grad():
265
+ score = model(x_t).item()
266
+ st.info(f"Punteggio = {score:.3f}")
267
+ if score>0.7:
268
+ st.success("Alta compatibilità! Procedi pure.")
269
+ elif score>0.4:
270
+ st.warning("Compatibilità media, valuta modifiche.")
271
+ else:
272
+ st.error("Compatibilità bassa, magari scegli un altro output")
273
 
274
+ # ---------------------------
275
+ # TAB 3 - Overlay Estetico
276
+ # ---------------------------
277
+ with tabs[2]:
278
+ st.subheader("Fase 3: Overlay Estetico (effetto wow)")
279
 
280
  col1, col2 = st.columns(2)
281
  with col1:
282
+ eol_file = st.file_uploader("Foto EoL", type=["jpg","png"], key="overlayEol")
283
  with col2:
284
+ final_file = st.file_uploader("Foto Oggetto Finale (es. armadio, scaffale..)", type=["jpg","png"], key="overlayFinal")
285
+
286
+ alpha = st.slider("Trasparenza EoL",0.0,1.0,0.5)
287
+
288
+ if eol_file and final_file:
289
+ eol_img = Image.open(eol_file).convert("RGBA")
290
+ fin_img = Image.open(final_file).convert("RGBA")
291
+ eol_img = eol_img.resize(fin_img.size)
292
+ blend = Image.blend(fin_img,eol_img,alpha)
293
+ st.image(blend, caption="Overlay EoL + Oggetto", use_column_width=True)
294
+ else:
295
+ st.info("Carica entrambe le immagini per l'overlay")
296
+
297
+ # ---------------------------
298
+ # TAB 4 - Generative Reuse (VAE)
299
+ # ---------------------------
300
+ with tabs[3]:
301
+ st.subheader("Fase 4: Generative Reuse con VAE")
302
+
303
+ st.write("**Obiettivo**: generare 'idee' di configurazioni (dim1, dim2, spess, decoro) per nuovi usi.")
304
+ if not st.session_state["trained_vae"]:
305
+ st.warning("VAE non addestrato, premi su 'Train VAE' nel Tab 1.")
306
+ else:
307
+ if st.button("Genera 5 configurazioni random dal VAE"):
308
+ vae = st.session_state["vae"]
309
+ # generiamo 5 sample random in latent space
310
+ z = torch.randn(5,2) # latent_dim=2
311
+ with torch.no_grad():
312
+ samples = vae.decode(z)
313
+ arr = samples.numpy()
314
+ df = pd.DataFrame(arr, columns=["Dim1","Dim2","Spess","Decoro"])
315
+ st.dataframe(df.round(2))
316
+ st.info("Idee: potresti interpretare Dim1,Dim2 come lung/larg, Spess=spessore, Decoro=0..1 styling?")
317
+
318
+ # ---------------------------
319
+ # TAB 5 - Feedback
320
+ # ---------------------------
321
+ with tabs[4]:
322
+ st.subheader("Fase 5: Raccolta Feedback dell'utente")
323
+ st.write("Se hai calcolato un punteggio di compatibilità e vuoi salvare la tua decisione, fallo qui.")
324
 
325
+ # Input testuale su EoL, output, punteggio e decisione
326
+ eol_desc = st.text_input("Descrizione EoL (es. lamiera 100x50)","lamiera 100x50")
327
+ out_desc = st.text_input("Output scelto","ArmadioAnta")
328
+ punteggio = st.number_input("Punteggio (0..1)",0.0,1.0,0.7)
329
+ dec = st.selectbox("Decisione",["Approvato","Rifiutato","In attesa"])
330
 
331
+ if st.button("Salva feedback"):
332
+ salva_feedback(eol_desc, out_desc, punteggio, dec)
333
+ st.success("Feedback salvato in feedback.csv!")
 
 
334
 
335
+ # Mostra file di feedback
336
+ if os.path.exists("feedback.csv"):
337
+ st.write("Feedback attuale:")
338
+ txt = open("feedback.csv","r").read()
339
+ st.code(txt)
340
  else:
341
+ st.info("Nessun feedback.csv presente.")