File size: 21,521 Bytes
36f4290
 
 
 
 
693e307
 
36f4290
5889527
 
693e307
5889527
a6f3241
693e307
5889527
 
 
a6f3241
f1234a6
a6f3241
36f4290
 
693e307
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36f4290
 
 
5889527
 
 
 
 
 
a6f3241
 
693e307
927abad
 
 
 
 
693e307
 
 
927abad
693e307
927abad
 
 
693e307
 
 
 
36f4290
5222b98
36f4290
693e307
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
927abad
36f4290
693e307
 
36f4290
927abad
a6f3241
927abad
693e307
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42e443a
 
 
 
693e307
 
 
 
 
 
 
 
 
 
 
 
 
 
927abad
36f4290
 
 
 
 
 
5889527
42e443a
a6f3241
5222b98
36f4290
a6f3241
693e307
 
 
 
 
 
f5ecdb7
693e307
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5222b98
a6f3241
5222b98
 
5889527
36b70c2
5222b98
 
a6f3241
36f4290
693e307
 
 
 
 
5222b98
693e307
 
5889527
693e307
5889527
 
 
693e307
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36f4290
 
 
693e307
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36f4290
 
 
693e307
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36f4290
 
 
693e307
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
import streamlit as st
import pandas as pd
import numpy as np
from PIL import Image
import os
from huggingface_hub import hf_hub_download
import logic 

# --- CONSTANTES Y RUTAS DE RECURSOS ---
csv_path = "data/furniture_data.csv"
cache_path = "vectores_cache.pkl" 

# --- CONSTANTES DE DESCARGA DE MODELOS ---
MODEL_REPO_ID = "agerhund/DesignIA_models" 
MODEL_FILE_BERT = "bert_style_encoder.pth"
MODEL_FILE_HORIZON = "horizonnet_model.pth"

# --- CONFIGURACIÓN INICIAL DE LA PÁGINA ---
st.set_page_config(page_title="DesignIA - Recomendador de Muebles inteligente")

# --- CARGAR ESTILOS CSS ---
def cargar_estilo():
    """Define y aplica estilos CSS para la UI de Streamlit."""
    st.markdown("""
        <style>
        /* Ocultar elementos de sistema de Streamlit */
        #MainMenu {visibility: hidden;}
        footer {visibility: hidden;}
        header {visibility: hidden;}
        
        /* Estilo de Botones (Azul IKEA) */
        div.stButton > button:first-child {
            background-color: #0051ba; 
            color: white; 
            border-radius: 8px; 
            font-weight: bold; 
            border: none; 
            padding: 0.5rem 1rem;
        }
        div.stButton > button:first-child:hover { 
            background-color: #003e8f; 
            border: none; 
        }
        
        /* Estilo de las Tarjetas de Producto */
        .product-card {
            background-color: white; 
            padding: 15px; 
            border-radius: 10px;
            box-shadow: 0 2px 5px rgba(0,0,0,0.05); 
            margin-bottom: 15px; 
            border: 1px solid #eee;
            color: black !important; 
        }
        </style>
    """, unsafe_allow_html=True)

cargar_estilo()

# --- INICIALIZACIÓN DEL ESTADO DE SESIÓN ---
if 'stage' not in st.session_state: st.session_state.stage = 0
if 'room_data' not in st.session_state: st.session_state.room_data = None
if 'muebles_df' not in st.session_state: st.session_state.muebles_df = None
if 'data_manager' not in st.session_state: st.session_state.data_manager = None
if 'horizon_model_path' not in st.session_state: st.session_state.horizon_model_path = None
if 'source_file_path' not in st.session_state: st.session_state.source_file_path = None
if 'is_example' not in st.session_state: st.session_state.is_example = False
if 'display_files_list' not in st.session_state: st.session_state.display_files_list = []

# --- FUNCIONES DE CARGA CON CACHE ---

@st.cache_resource
def download_models():
    """Descarga ambos modelos grandes desde el Model Repository y devuelve las rutas locales temporales."""
    bert_path = hf_hub_download(repo_id=MODEL_REPO_ID, filename=MODEL_FILE_BERT)
    horizon_path = hf_hub_download(repo_id=MODEL_REPO_ID, filename=MODEL_FILE_HORIZON)

    return bert_path, horizon_path

@st.cache_resource
def init_backend(csv, cache, bert_model_path):
    """Inicializa DataManager y carga el DataFrame de muebles, usando la ruta local del modelo BERT."""
    dm = logic.DataManager(csv, cache, bert_model_path)
    df = dm.cargar_datos()
    return dm, df

# --- 1. CARGA DE DATOS Y MODELOS (Cacheado y estabilizado) ---
try:
    if st.session_state.data_manager is None:
        
        # 1. Usar contenedor para el mensaje de descarga (Estabiliza el salto)
        status_message = st.empty()
        status_message.info("Descargando modelos grandes desde Hugging Face Hub (¡Solo la primera vez!)...")
        
        bert_path_downloaded, horizon_path_downloaded = download_models()
        
        # 2. Limpiar el placeholder de descarga y mostrar el spinner de carga de datos
        status_message.empty() 
        with st.spinner("Cargando base de datos de muebles y modelos IA..."):
            
            dm, df = init_backend(csv_path, cache_path, bert_path_downloaded)
            
            st.session_state.data_manager = dm
            st.session_state.muebles_df = df
            st.session_state.horizon_model_path = horizon_path_downloaded 
            
        st.toast("Modelos y datos cargados con éxito.", icon="✅") 
        
    st.sidebar.success(f"Base de datos cargada: {len(st.session_state.muebles_df)} items")

except Exception as e:
    st.error(f"Error cargando datos: {e}")
    st.stop()


# --- SIDEBAR: INFORMACIÓN DEL PROYECTO (SOLO INFORMACIÓN ESTÁTICA) ---
with st.sidebar:
    st.title("DesignIA - Asistente de Diseño")
    st.markdown("---")
    st.markdown("**Trabajo de fin de Máster**")
    st.caption("Máster de Data Science, Business Analytics y Big Data")
    st.caption("Universidad Complutense de Madrid")
    st.markdown("---")
    st.markdown("Desarrollado por **Andrés Gerlotti Slusnys**")
    st.markdown("© 2025")
    
    # Indicador de estado 
    with st.expander("Estado del Sistema", expanded=False):
        st.success("Motor Gráfico: Activo")
        st.success("Modelo NLP (BERT): Cargado")
        if 'horizon_model_path' in st.session_state:
            st.success("HorizonNet: Conectado (Remoto)")
        else:
            st.warning("HorizonNet: Pendiente de descarga/inicialización")

# --- FUNCIONES DE CALLBACK (Para estabilizar la UI) ---

def actualizar_estado_ejemplo():
    """Actualiza el estado de sesión SÓLO cuando cambia el selectbox."""
    selected_name = st.session_state.example_select_box
    display_files = st.session_state.display_files_list
    examples_dir = os.path.join(os.path.dirname(__file__), "examples")
    
    if selected_name != display_files[0]:
        file_path = os.path.join(examples_dir, selected_name)
        if st.session_state.get('source_file_path') != file_path:
            st.session_state.source_file_path = file_path
            st.session_state.is_example = True
    else:
        if 'source_file_path' in st.session_state:
            del st.session_state.source_file_path
            st.session_state.is_example = False

# --- INTERFAZ PRINCIPAL ---
st.title("Recomendador de Muebles Inteligente")
st.markdown("Sube una panorámica, detecta el espacio y obtén el diseño ideal según tu presupuesto.")

# --- PASO 1: CARGA DE IMAGEN Y DETECCIÓN ---
st.header("1. Escaneo de Habitación")

# --- LÓGICA DE SELECCIÓN DE EJEMPLOS ESTABLE ---
examples_dir = os.path.join(os.path.dirname(__file__), "examples")
image_placeholder = st.empty() # Placeholder para la imagen (Estabiliza el temblor)

if os.path.exists(examples_dir):
    example_files = [f for f in os.listdir(examples_dir) if f.endswith(('.jpg', '.png'))]
    example_files.sort()
    
    col_select, col_download = st.columns([3, 1])

    # Generar y almacenar la lista de opciones para el callback
    display_files = ["Cargar imagen manualmente"] + example_files 
    st.session_state.display_files_list = display_files
    
    with col_select:
        # El callback on_change maneja la lógica de estado de forma estable
        selected_example_name = st.selectbox(
            "O usa una imagen de ejemplo:", 
            display_files,
            key='example_select_box',
            on_change=actualizar_estado_ejemplo
        )

    # Manejo de la visualización del ejemplo (basado en el estado de sesión)
    if st.session_state.get('source_file_path') and st.session_state.is_example:
        file_path = st.session_state.source_file_path
        selected_example_name = file_path.split(os.sep)[-1]
        
        with col_download:
            st.markdown(" ") # Espacio para alinear el botón
            with open(file_path, "rb") as file:
                st.download_button(
                    label=f"⬇️ Descargar: {selected_example_name}",
                    data=file,
                    file_name=selected_example_name,
                    mime="image/jpeg"
                )
        
        with image_placeholder.container():
            st.image(file_path, caption="Vista previa del ejemplo", use_container_width=True)

    else:
        # Si no hay selección de ejemplo, el placeholder está vacío o se usará para la subida manual.
        pass


# 1. Selector de carga manual (Se mantiene aquí, pero solo para subir el archivo)
uploaded_file = st.file_uploader("Sube tu imagen panorámica (360)", type=['jpg', 'png', 'jpeg'], key='main_file_uploader')

# Determinar qué archivo usar (la carga manual tiene prioridad)
source_file_path = st.session_state.get('source_file_path', None)
is_example = st.session_state.get('is_example', False)

if uploaded_file is not None:
    source_file = uploaded_file
    file_caption = 'Imagen subida'
    is_example = False
    st.session_state.is_example = False # Resetear por seguridad
    image_placeholder.empty() # Limpiar el placeholder si hay una subida manual
elif source_file_path is not None and is_example == True:
    source_file = source_file_path
    file_caption = f'Imagen de ejemplo: {source_file_path.split(os.sep)[-1]}'
else:
    source_file = None


if source_file is not None:
    # Abrir la imagen
    if is_example:
        image = Image.open(source_file)
    else:
        image = Image.open(source_file)

    # Mostrar la imagen subida (solo si es subida, ya que el ejemplo se mostró arriba)
    if uploaded_file is not None:
        with image_placeholder.container():
             st.image(image, caption=file_caption, use_container_width=True)
    
    if st.button("Analizar la habitación"):
        with st.spinner("Detectando la geometría..."):
            
            # --- MANEJO DEL ARCHIVO TEMPORAL PARA EL DETECTOR ---
            if is_example:
                temp_file_path = source_file
            else:
                temp_file_path = "temp_pano.jpg"
                with open(temp_file_path, "wb") as f:
                    f.write(source_file.getbuffer())
            # --- FIN MANEJO ARCHIVO TEMPORAL ---


            # Instanciar y ejecutar el detector de layout (HorizonNet)
            try:
                detector = logic.RoomLayoutDetector(st.session_state.horizon_model_path) 
                room_data = detector.detect_layout(temp_file_path)
                
                # Validación de datos y manejo de fallos
                if room_data is None or not isinstance(room_data, dict) or 'width' not in room_data:
                    st.error("**Detección fallida.** El modelo de Computer Vision no pudo extraer las dimensiones ni los obstáculos. Asegúrate de que el modelo HorizonNet está configurado y funcionando correctamente.")
                    st.session_state.room_data = None
                    st.session_state.stage = 0
                else:
                    st.session_state.room_data = room_data
                    st.session_state.stage = 1
                    
                    st.toast("Análisis completado", icon="✅") 

                    # Mostrar resultado de la detección de HorizonNet
                    st.header("Resultado del análisis visual")
                    annotated_image = logic.dibujar_layout_sobre_imagen(temp_file_path, room_data)
                    st.image(annotated_image, caption='Análisis de HorizonNet (Vértices, Puertas y Ventanas)', use_container_width=True) 
                
            except Exception as e:
                st.error(f"Error en detección: {e}. Revisa la configuración del modelo HorizonNet.")
                st.session_state.stage = 0

# --- PASO 2: VERIFICACIÓN Y EDICIÓN DE GEOMETRÍA/OBSTÁCULOS ---
if st.session_state.stage >= 1 and st.session_state.room_data:
    st.header("2. Verificación de Geometría")
    
    # Mostrar dimensiones detectadas
    w_m = st.session_state.room_data.get('width', 0.0)
    l_m = st.session_state.room_data.get('length', 0.0)
    
    col1, col2 = st.columns(2)
    with col1:
        st.metric("Ancho (m)", f"{w_m:.2f}")
    with col2:
        st.metric("Largo (m)", f"{l_m:.2f}")
    
    # Mostrar el diagrama de planta
    st.subheader("Planta de la habitación")
    floor_plan_fig = logic.generar_diagrama_planta(st.session_state.room_data)
    st.pyplot(floor_plan_fig) 
    
    # Formulario para añadir puertas y ventanas manualmente
    st.subheader("Añadir puertas y ventanas manualmente")
    
    polygon_points = st.session_state.room_data.get('polygon_points', [])
    num_walls = len(polygon_points) if polygon_points is not None else 0
    
    if num_walls > 0:
        col_a, col_b, col_c, col_d = st.columns(4)
        
        with col_a:
            wall_options = [f"Pared {i+1}" for i in range(num_walls)]
            selected_wall = st.selectbox("Seleccionar Pared", wall_options, key="wall_select")
            wall_idx = int(selected_wall.split()[1]) - 1
        
        with col_b:
            obstacle_type = st.radio("Tipo", ["Puerta", "Ventana"], key="obs_type")
        
        with col_c:
            # Posición normalizada [0.0, 1.0]
            position_pct = st.number_input("Posición (%)", min_value=0.0, max_value=100.0, value=50.0, step=5.0, key="obs_pos")
        
        with col_d:
            width_m = st.number_input("Ancho (m)", min_value=0.1, max_value=5.0, value=0.9, step=0.1, key="obs_width")
        
        if st.button("Añadir elemento"):
            # Los datos de centro están normalizados
            new_obstacle = {
                'center': [position_pct / 100.0, wall_idx / max(1, num_walls)],
                'width': width_m
            }
            
            if obstacle_type == "Puerta":
                st.session_state.room_data['doors'].append(new_obstacle)
            else:
                st.session_state.room_data['windows'].append(new_obstacle)
            
            st.success(f"{obstacle_type} añadida a {selected_wall}")
            st.rerun()
    else:
        st.warning("No se detectaron paredes en el polígono.")
    
    # Editor de datos para modificar obstáculos detectados/añadidos
    st.subheader("Editar elementos (puertas y ventanas)")
    st.info("Ajusta las coordenadas de los obstáculos. Los valores X/Y están normalizados [0.0, 1.0].")

    # Preparar datos para st.data_editor
    doors_data = []
    for i, d in enumerate(st.session_state.room_data.get('doors', [])):
        center_y = d['center'][1] if len(d['center']) > 1 else 0 
        doors_data.append({"ID": f"P{i}", "Tipo": "Puerta", "Centro X (Norm.)": d['center'][0], "Centro Y (Norm.)": center_y, "Ancho (m)": d['width']})
    
    windows_data = []
    for i, w in enumerate(st.session_state.room_data.get('windows', [])):
        center_y = w['center'][1] if len(w['center']) > 1 else 0
        windows_data.append({"ID": f"V{i}", "Tipo": "Ventana", "Centro X (Norm.)": w['center'][0], "Centro Y (Norm.)": center_y, "Ancho (m)": w['width']})

    all_obstacles = doors_data + windows_data
    df_obs = pd.DataFrame(all_obstacles)
    
    col_config = {
        "Centro X (Norm.)": st.column_config.NumberColumn("Centro X (Norm.)", help="Posición horizontal normalizada [0.0, 1.0]", format="%.2f"),
        "Centro Y (Norm.)": st.column_config.NumberColumn("Centro Y (Norm.)", help="Posición vertical normalizada [0.0, 1.0]", format="%.2f"),
        "Ancho (m)": st.column_config.NumberColumn("Ancho (m)", help="Ancho del obstáculo en metros", format="%.2f"),
    }
    
    edited_df = st.data_editor(df_obs, num_rows="dynamic", use_container_width=True, column_config=col_config)

    if st.button("Confirmar geometría"):
        # Reconstruir el diccionario room_data a partir del DataFrame editado
        new_doors = []
        new_windows = []
        for index, row in edited_df.iterrows():
            obj = {'center': [row['Centro X (Norm.)'], row['Centro Y (Norm.)']], 'width': row['Ancho (m)']}
            if row['Tipo'] == 'Puerta': new_doors.append(obj)
            else: new_windows.append(obj)
            
        st.session_state.room_data['doors'] = new_doors
        st.session_state.room_data['windows'] = new_windows
        st.session_state.stage = 2
        st.rerun()

# --- PASO 3: PRESUPUESTO Y GENERACIÓN DE LAYOUT/RECOMENDACIÓN ---
if st.session_state.stage >= 2:
    st.header("3. Presupuesto y generación")
    
    presupuesto = st.number_input("Presupuesto Máximo (€)", min_value=100.0, value=1000.0, step=100.0)
    
    if st.button("Generar diseño"):
        with st.spinner("Calculando distribución óptima y seleccionando muebles..."):
            # Convertir dimensiones de m a cm para el LayoutEngine
            w_cm = st.session_state.room_data.get('width', 0.0) * 100
            l_cm = st.session_state.room_data.get('length', 0.0) * 100
            
            if w_cm < 200 or l_cm < 200:
                st.error("Las dimensiones de la habitación son demasiado pequeñas (mínimo 2x2m) o no fueron capturadas correctamente.")
            else:
                # 1. Inicializar motores
                layout_engine = logic.LayoutEngine(st.session_state.data_manager.dimensiones_promedio)
                recommender = logic.Recommender(st.session_state.muebles_df)
                
                # 2. Sugerir el pack de muebles base
                pack_sugerido = layout_engine.sugerir_pack(w_cm, l_cm)
                
                # 3. Convertir obstáculos a polígonos para el motor
                obs_layout = layout_engine.convertir_obstaculos(
                    st.session_state.room_data, 
                    w_cm, l_cm, 
                    polygon_points=st.session_state.room_data.get('polygon_points')
                )
                
                # 4. Generar el Layout
                layout_plan, constraints, log_msgs = layout_engine.generar_layout(
                    w_cm, l_cm, 
                    pack_sugerido, 
                    obs_layout,
                    polygon_points=st.session_state.room_data.get('polygon_points')
                )

                # Mostrar Log de Generación
                with st.expander("📝 Detalles de la Generación del Layout", expanded=False):
                    for msg in log_msgs:
                        if "✅" in msg: st.success(msg)
                        elif "❌" in msg: st.error(msg)
                        elif "⚠️" in msg: st.warning(msg)
                        else: st.text(msg)
                
                if not layout_plan:
                    st.error("No se pudo generar una distribución válida para este espacio (demasiado pequeño o muchos obstáculos).")
                else:
                    # 5. Recomendar productos (Knapsack para optimización de precio/estilo)
                    best_combo = recommender.buscar_combinacion(constraints, presupuesto, top_n=1)
                    
                    if not best_combo:
                        st.error("No se encontraron muebles que se ajusten al presupuesto y restricciones.")
                    else:
                        st.session_state.result_layout = layout_plan
                        st.session_state.result_items = best_combo[0]['items']
                        st.session_state.result_total = best_combo[0]['precio_total']
                        st.session_state.result_score = best_combo[0]['score']
                        st.session_state.stage = 3

# --- PASO 4: RESULTADOS Y VISUALIZACIÓN FINAL ---
if st.session_state.stage == 3:
    st.divider()
    st.header("Tu salón ideal")
    
    # --- VISUALIZACIÓN 3D Interactiva (Plotly) ---
    st.subheader("Visualización 3D Interactiva")
    
    # Generar la figura 3D
    fig_plotly = logic.generar_figura_3d_plotly(
        st.session_state.result_layout, 
        st.session_state.room_data,
        st.session_state.result_items
    )
    
    # Renderizar la figura de Plotly
    st.plotly_chart(fig_plotly, use_container_width=True, theme="streamlit") 
    
    st.info("💡 Usa el ratón: Clic izquierdo para rotar, rueda para zoom.")
        
    st.divider()

    # --- LISTA DE COMPRA ---
    st.subheader("Lista de Compra")
    
    # Totales y Score de Diseño
    c_tot1, c_tot2 = st.columns([2, 1])
    with c_tot1:
        st.markdown("### Total Estimado")
        st.caption(f"Score de Diseño (Estilo + Puntuación Base): {st.session_state.result_score:.2f}/1.0")
    with c_tot2:
        st.markdown(f"### {st.session_state.result_total:.2f}€")
    
    st.markdown("---")
    
    # Listado de productos
    for item in st.session_state.result_items:
        with st.container():
            c_img, c_info, c_price, c_link = st.columns([1, 2, 1, 1])
            
            url = f"https://www.ikea.com/es/es/p/{item.get('Enlace_producto', '')}-{item.get('ID', '')}"
            img_src = item.get('Imagen_principal', '')
            nombre = item['Nombre']
            tipo = item['Tipo_mueble']
            precio = float(item['Precio'])
            
            with c_img:
                if img_src:
                    st.image(img_src, width=150)
                else:
                    st.text("Sin imagen")
            
            with c_info:
                st.subheader(nombre)
                st.caption(tipo)
                st.text(item.get('Descripcion', '')[:100] + '...')
            
            with c_price:
                st.markdown(f"### {precio:.2f} €")
            
            with c_link:
                st.link_button("Ver en IKEA", url)
            
            st.divider()
    
    if st.button("Reiniciar"):
        for key in ['room_data', 'result_layout', 'result_items', 'result_total', 'result_score']:
            if key in st.session_state:
                del st.session_state[key]
        st.session_state.stage = 0
        st.rerun()