File size: 42,962 Bytes
be096d1
3de9a41
 
6e9dc4d
 
44e10c6
99101f6
3de9a41
dc6db03
 
3de9a41
 
 
 
d3cd6fb
1420667
c66b983
7778425
 
99101f6
be096d1
3de9a41
 
8876c58
 
b6a1665
35509b3
 
99101f6
0110182
98d16bb
a90358f
 
 
 
 
 
 
 
3de9a41
6be08b8
 
 
 
 
 
 
44e10c6
 
 
 
 
dc6db03
44e10c6
 
dc6db03
44e10c6
 
 
a90358f
44e10c6
 
 
 
 
 
 
dc6db03
 
44e10c6
 
 
 
 
 
 
 
 
 
 
 
 
 
3de9a41
c22c6d5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72139b5
 
c22c6d5
 
 
 
6979d7b
c22c6d5
72139b5
c22c6d5
 
72139b5
 
c22c6d5
 
 
3f5ec44
 
c22c6d5
 
 
 
 
 
72139b5
 
c22c6d5
6979d7b
 
 
 
 
 
 
 
c22c6d5
72139b5
 
 
 
 
 
 
 
 
 
c22c6d5
 
72139b5
c22c6d5
72139b5
 
 
 
 
 
c22c6d5
 
72139b5
 
 
 
 
c22c6d5
72139b5
 
 
 
 
 
 
c22c6d5
72139b5
 
c22c6d5
72139b5
 
c22c6d5
6979d7b
 
 
72139b5
 
 
3f5ec44
6979d7b
 
 
 
 
 
 
 
 
 
 
72139b5
 
 
 
3f5ec44
c22c6d5
 
 
 
 
 
 
 
3f5ec44
c22c6d5
72139b5
c22c6d5
 
 
 
72139b5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e34f53c
 
 
72139b5
 
 
 
 
 
 
 
 
 
7778425
0110182
0284ff4
8df5c77
0284ff4
 
 
8df5c77
0284ff4
8df5c77
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0284ff4
 
 
 
f3ef5a8
0284ff4
 
 
 
 
f3ef5a8
 
 
 
0284ff4
f3ef5a8
 
0284ff4
975e2e8
f3ef5a8
 
0284ff4
 
64ee66a
0284ff4
 
64ee66a
0284ff4
 
 
 
 
 
 
f3ef5a8
0284ff4
 
64ee66a
 
 
0284ff4
64ee66a
 
 
 
0284ff4
 
64ee66a
0284ff4
 
 
 
 
64ee66a
0284ff4
 
 
343cfa2
0284ff4
 
 
46245e7
c66b983
 
0284ff4
 
32ffd18
6227cca
 
 
 
 
 
 
 
 
 
 
5c72cc4
6227cca
 
 
 
 
 
 
 
 
 
35509b3
5c72cc4
6227cca
 
dc6db03
44e10c6
6227cca
 
 
dc6db03
 
 
 
 
 
6227cca
 
 
 
dc6db03
 
 
 
 
3de9a41
 
 
 
13fbda5
6227cca
 
3de9a41
6227cca
 
 
 
dc6db03
3de9a41
7f90803
6227cca
 
 
 
7f90803
13fbda5
7f90803
 
6227cca
 
 
 
 
 
 
 
99101f6
7f90803
13fbda5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7f90803
 
 
 
 
13fbda5
 
8876c58
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3de9a41
dc6db03
99101f6
f87f523
 
3de9a41
 
 
 
 
 
44e10c6
3de9a41
 
 
44e10c6
3de9a41
 
 
 
 
44e10c6
3de9a41
 
 
 
 
44e10c6
3de9a41
46245e7
 
 
 
3de9a41
 
44e10c6
3de9a41
 
 
 
 
44e10c6
3de9a41
 
 
 
 
bb19da4
 
aaf54ac
bb19da4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46245e7
 
 
 
 
 
 
 
 
 
 
 
bb19da4
0110182
5775615
6b0fde6
3de9a41
eeb55bb
 
 
6227cca
 
 
 
91e02fa
 
677dd33
6227cca
677dd33
91e02fa
677dd33
 
 
dfac791
 
6227cca
677dd33
91e02fa
 
677dd33
91e02fa
 
6227cca
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91e02fa
eeb55bb
841b0f8
 
 
 
 
 
 
 
 
cf4340c
841b0f8
 
 
 
 
ebf300b
841b0f8
 
 
677dd33
841b0f8
677dd33
 
cf4340c
841b0f8
 
 
 
 
 
 
 
ebf300b
841b0f8
 
 
 
 
cf4340c
841b0f8
cf4340c
841b0f8
 
 
 
 
 
 
 
 
677dd33
841b0f8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cf4340c
3de9a41
7cba597
44e10c6
ba6db4d
bb19da4
 
6b0fde6
 
bb19da4
 
 
46245e7
 
 
bb19da4
 
 
 
677dd33
bb19da4
 
 
6b0fde6
677dd33
bb19da4
 
ea24b59
bb19da4
 
 
 
fb22341
 
7270a4d
fb22341
 
 
3de9a41
bb19da4
 
 
 
 
d27c9ff
 
 
 
 
 
 
104e918
 
d27c9ff
 
 
7a7ad06
d27c9ff
104e918
d27c9ff
 
 
 
7a7ad06
d27c9ff
7a7ad06
d27c9ff
7a7ad06
d27c9ff
 
7a7ad06
d27c9ff
7a7ad06
d27c9ff
7a7ad06
d27c9ff
 
7a7ad06
 
d27c9ff
7a7ad06
d27c9ff
7a7ad06
d27c9ff
 
7a7ad06
d27c9ff
7a7ad06
d27c9ff
 
 
1b7dac9
d27c9ff
2d99de1
bb19da4
 
6b0fde6
ba6db4d
bb19da4
 
 
 
 
 
 
 
 
 
 
4a50fa6
e3018df
6b0fde6
bb19da4
6b0fde6
6687e98
 
 
 
 
 
 
 
 
 
bb19da4
 
6227cca
 
 
 
 
 
 
 
 
 
 
 
bb19da4
6227cca
bb19da4
 
 
6b0fde6
bb19da4
 
6227cca
8bf8409
6227cca
 
 
 
 
 
 
 
 
bb19da4
 
 
 
 
 
 
 
 
 
 
ff0d30b
3de9a41
 
bb19da4
3de9a41
 
be096d1
39ee1aa
a08456e
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
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
import gradio as gr
import json
import os
import pandas as pd
import folium
from folium.plugins import MeasureControl, Fullscreen, MarkerCluster
from geopy.geocoders import Nominatim
from geopy.exc import GeocoderTimedOut, GeocoderServiceError
import time
import random
from typing import List, Tuple, Optional
import io
import tempfile
import warnings
import string
import spaces
from transformers import AutoModelForCausalLM, AutoTokenizer, AutoConfig
import torch

warnings.filterwarnings("ignore")

# Map Tile Providers with reliable sources
MAP_TILES = {
    "GreenMap": {
        "url": "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",
        "attr": "Esri"
    }
}

# Model configuration - corrected model name
MODEL_NAME = "numind/NuExtract-1.5"  # Fixed model name according to documentation
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
TORCH_DTYPE = torch.bfloat16 if DEVICE == "cuda" else torch.float32
MAX_INPUT_LENGTH = 20000  # For sliding window processing
MAX_NEW_TOKENS = 1000

# Global model variables
tokenizer = None
model = None

try:
    from transformers.models.qwen2.tokenization_qwen2 import Qwen2Tokenizer
    from transformers.models.qwen2.modeling_qwen2 import Qwen2ForCausalLM
    print("Qwen2 components successfully imported")
except ImportError:
    print("Could not import Qwen2 components directly")

class SafeGeocoder:
    def __init__(self):
        user_agent = f"location_mapper_v1_{random.randint(1000, 9999)}"
        self.geolocator = Nominatim(user_agent=user_agent, timeout=10)
        self.cache = {}
        self.last_request = 0
    
    def _respect_rate_limit(self):
        current_time = time.time()
        elapsed = current_time - self.last_request
        if elapsed < 1.0:
            time.sleep(1.0 - elapsed)
        self.last_request = current_time
    
    def get_coords(self, location: str):
        if not location or pd.isna(location):
            return None
            
        location = str(location).strip()
        
        if location in self.cache:
            return self.cache[location]
        
        try:
            self._respect_rate_limit()
            result = self.geolocator.geocode(location)
            if result:
                coords = (result.latitude, result.longitude)
                self.cache[location] = coords
                return coords
            self.cache[location] = None
            return None
        except Exception as e:
            print(f"Geocoding error for '{location}': {e}")
            self.cache[location] = None
            return None


def process_excel(file, places_column):
    if file is None:
        return None, "No file uploaded", None
    
    try:
        if hasattr(file, 'name'):
            df = pd.read_excel(file.name)
        elif isinstance(file, bytes):
            df = pd.read_excel(io.BytesIO(file))
        else:
            df = pd.read_excel(file)
        
        print(f"Spalten in der Excel-Tabelle: {list(df.columns)}")
        
        if places_column not in df.columns:
            return None, f"Spalte '{places_column}' wurde in der Excel-Datei nicht gefunden. Verfügbare Spalten: {', '.join(df.columns)}", None
        
        # Create a new DataFrame to store expanded rows
        expanded_rows = []
        
        geocoder = SafeGeocoder()
        coords = []
        processed_count = 0
        location_cache = {}  # Cache to ensure consistent coordinates
        
        # Process each row
        for idx, row in df.iterrows():
            if pd.isna(row[places_column]):
                # Keep rows with no location as-is
                expanded_rows.append(row.to_dict())
                continue
                
            location = str(row[places_column]).strip()
            
            try:
                locations = [loc.strip() for loc in location.split(',') if loc.strip()]
                if not locations:
                    locations = [location]
            except:
                locations = [location]
            
            # Process each location in the comma-separated list
            location_rows_added = False
            for loc in locations:
                # Use cached coordinates if available
                if loc in location_cache:
                    point = location_cache[loc]
                else:
                    point = geocoder.get_coords(loc)
                    if point:
                        location_cache[loc] = point  # Cache the result
                
                if point:
                    # Create a new row for this location
                    new_row = row.copy()
                    new_row_dict = new_row.to_dict()
                    new_row_dict[places_column] = loc  # Replace with just this location
                    new_row_dict['latitude'] = point[0]
                    new_row_dict['longitude'] = point[1]
                    
                    # Add the row to our expanded rows list
                    expanded_rows.append(new_row_dict)
                    
                    coords.append(point)
                    processed_count += 1
                    location_rows_added = True
            
            # If none of the locations could be geocoded, keep the original row
            if not location_rows_added:
                expanded_rows.append(row.to_dict())
        
        # Convert the list of dictionaries to a DataFrame
        expanded_df = pd.DataFrame(expanded_rows)
        
        # Create the map
        m = folium.Map(
            location=[20, 0],
            zoom_start=2, 
            control_scale=True
        )
        
        folium.TileLayer(
            tiles=MAP_TILES["GreenMap"]["url"],
            attr=MAP_TILES["GreenMap"]["attr"],
            name="GreenMap",
            overlay=False,
            control=False
        ).add_to(m)
        
        Fullscreen().add_to(m)
        MeasureControl(position='topright', primary_length_unit='kilometers').add_to(m)
        
        # Add markers directly here
        marker_cluster = MarkerCluster(name="Locations").add_to(m)
        
        # Track which coordinates we've already placed on the map
        seen_coords = {}
        
        for idx, row in expanded_df.iterrows():
            if 'latitude' in row and 'longitude' in row and not pd.isna(row['latitude']) and not pd.isna(row['longitude']):
                location = row[places_column] if not pd.isna(row[places_column]) else "Unknown"
                
                # Round coordinates to reduce small differences
                rounded_lat = round(row['latitude'], 5)
                rounded_lng = round(row['longitude'], 5)
                coord_key = f"{rounded_lat},{rounded_lng}"
                
                # Skip if we've already added a marker at this location
                if coord_key in seen_coords:
                    continue
                
                seen_coords[coord_key] = True
                
                additional_info = ""
                for col in expanded_df.columns:
                    if col not in [places_column, 'latitude', 'longitude'] and not pd.isna(row[col]):
                        additional_info += f"<br><b>{col}:</b> {row[col]}"
                
                popup_content = f"""
                <div style="min-width: 200px; max-width: 300px">
                    <h4 style="font-family: 'Source Sans Pro', sans-serif; margin-bottom: 5px;">{location}</h4>
                    <div style="font-family: 'Source Sans Pro', sans-serif; font-size: 14px;">
                        {additional_info}
                    </div>
                </div>
                """
                
                folium.Marker(
                    location=(row['latitude'], row['longitude']),
                    popup=folium.Popup(popup_content, max_width=300),
                    tooltip=location,
                    icon=folium.Icon(color="blue", icon="info-sign")
                ).add_to(marker_cluster)
        
        if coords:
            m.fit_bounds(coords)
        
        custom_css = """
        <style>
        @import url('https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@400;600&display=swap');
        .leaflet-container {
            font-family: 'Source Sans Pro', sans-serif;
        }
        .leaflet-popup-content {
            font-family: 'Source Sans Pro', sans-serif;
        }
        .leaflet-popup-content h4 {
            font-weight: 600;
            margin-bottom: 8px;
        }
        </style>
        """
        m.get_root().header.add_child(folium.Element(custom_css))
        
        # Save the expanded DataFrame to Excel
        with tempfile.NamedTemporaryFile(suffix=".xlsx", delete=False) as tmp:
            processed_path = tmp.name
            expanded_df.to_excel(processed_path, index=False)
        
        # Assuming places are comma-separated in the column
        all_places = df[places_column].str.split(',').explode().dropna()
        total_locations = len(all_places)
        success_rate = (processed_count / total_locations * 100) if total_locations > 0 else 0
        
        stats = f"Gefunden: {processed_count} von {total_locations} Orten ({success_rate:.1f}%)"
        
        return m._repr_html_(), stats, processed_path
    except Exception as e:
        import traceback
        trace = traceback.format_exc()
        print(f"Error processing file: {e}\n{trace}")
        return None, f"Fehler bei der Verarbeitung der Datei: {str(e)}", None

# Corrected model loading function based on official usage example

@spaces.GPU
def extract_info(template, text):
    global tokenizer, model
    
    # Load tokenizer if not loaded yet
    if tokenizer is None:
        print("Tokenizer not loaded yet, loading now...")
        try:
            try:
                from modelscope import AutoTokenizer as MSAutoTokenizer
                tokenizer = MSAutoTokenizer.from_pretrained(MODEL_NAME, trust_remote_code=True)
                print("Loaded tokenizer using modelscope AutoTokenizer")
            except:
                # Fall back to regular tokenizer
                tokenizer = AutoTokenizer.from_pretrained(
                    MODEL_NAME, 
                    trust_remote_code=True,
                    revision="main"
                )
                print("Loaded tokenizer using standard AutoTokenizer")
        except Exception as e:
            trace = traceback.format_exc()
            print(f"Error loading tokenizer: {e}\n{trace}")
            return "❌ Fehler beim Laden des Tokenizers", f"{str(e)}"
    
    try:
        # Load model if not loaded yet
        if model is None:
            print("Model not loaded yet, loading now...")
            try:
                model = AutoModelForCausalLM.from_pretrained(
                    MODEL_NAME,
                    torch_dtype=TORCH_DTYPE,
                    trust_remote_code=True,
                    revision="main",
                    device_map="auto"  # Let the model decide CUDA placement
                ).eval()
                print(f"✅ Model loaded successfully")
            except Exception as e:
                trace = traceback.format_exc()
                print(f"Error loading model: {e}\n{trace}")
                return f"❌ Fehler beim Laden des Modells: {str(e)}", "{}"
        
        print("Using model for inference...")
        
        # Format the template as proper JSON with indentation
        template_formatted = json.dumps(json.loads(template), indent=4)
        
        # Create prompt
        prompt = f"<|input|>\n### Template:\n{template_formatted}\n### Text:\n{text}\n\n<|output|>"
        
        # Tokenize with proper settings
        inputs = tokenizer(
            [prompt], 
            return_tensors="pt", 
            truncation=True, 
            padding=True,
            max_length=MAX_INPUT_LENGTH
        ).to(model.device)  # Use model's device
        
        # Generate output with torch.no_grad() for efficiency
        with torch.no_grad():
            outputs = model.generate(
                **inputs,
                max_new_tokens=MAX_NEW_TOKENS,
                temperature=0.0,
                do_sample=False
            )
        
        # Decode the result
        result_text = tokenizer.decode(outputs[0], skip_special_tokens=True)
        
        # Extract the output part
        if "<|output|>" in result_text:
            json_text = result_text.split("<|output|>")[1].strip()
        else:
            json_text = result_text
        
        # Try to parse as JSON
        try:
            extracted = json.loads(json_text)
            return "✅ Erfolgreich extrahiert", json.dumps(extracted, ensure_ascii=False, indent=2)
        except json.JSONDecodeError:
            return "❌ JSON Parsing Fehler", json_text
            
    except Exception as e:
        import traceback
        trace = traceback.format_exc()
        print(f"Error in extract_info: {e}\n{trace}")
        return f"❌ Fehler: {str(e)}", "{}"
@spaces.GPU
@spaces.GPU
def create_map(df, location_col):
    # Start a simple log to track execution
    with open("map_debug.log", "w") as log:
        log.write(f"Starting map creation at {time.strftime('%Y-%m-%d %H:%M:%S')}\n")
    
    m = folium.Map(
        location=[20, 0],
        zoom_start=2, 
        control_scale=True
    )
    
    folium.TileLayer(
        tiles=MAP_TILES["GreenMap"]["url"],
        attr=MAP_TILES["GreenMap"]["attr"],
        name="GreenMap",
        overlay=False,
        control=False
    ).add_to(m)
    
    Fullscreen().add_to(m)
    MeasureControl(position='topright', primary_length_unit='kilometers').add_to(m)
    
    geocoder = SafeGeocoder()
    coords = []
    marker_cluster = MarkerCluster(name="Locations").add_to(m)
    processed_count = 0
    
    with open("map_debug.log", "a") as log:
        log.write(f"Processing {len(df)} rows from dataframe\n")
    
    for idx, row in df.iterrows():
        if pd.isna(row[location_col]):
            continue
            
        location = str(row[location_col]).strip()
        
        # Log the location being processed
        with open("map_debug.log", "a") as log:
            log.write(f"Processing location: {location}\n")
        
        additional_info = ""
        for col in df.columns:
            if col != location_col and not pd.isna(row[col]):
                additional_info += f"<br><b>{col}:</b> {row[col]}"
        
        try:
            locations = [loc.strip() for loc in location.split(',') if loc.strip()]
            if not locations:
                locations = [location]
        except Exception as e:
            with open("map_debug.log", "a") as log:
                log.write(f"Error splitting location '{location}': {str(e)}\n")
            locations = [location]
        
        # Log the parsed locations
        with open("map_debug.log", "a") as log:
            log.write(f"Split into locations: {locations}\n")
            
        for loc in locations:
            try:
                # Log the current location
                with open("map_debug.log", "a") as log:
                    log.write(f"Getting coordinates for: {loc}\n")
                
                point = geocoder.get_coords(loc)
                
                    
            except Exception as e:
                # Log any errors in processing this location
                with open("map_debug.log", "a") as log:
                    log.write(f"Error processing {loc}: {str(e)}\n")
                    log.write(traceback.format_exc() + "\n")
    
    # Fit map bounds if we have coordinates
    if coords:
        m.fit_bounds(coords)
    
    # Custom CSS for map
    custom_css = """
    <style>
    @import url('https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@400;600&display=swap');
    .leaflet-container {
        font-family: 'Source Sans Pro', sans-serif;
    }
    .leaflet-popup-content {
        font-family: 'Source Sans Pro', sans-serif;
    }
    .leaflet-popup-content h4 {
        font-weight: 600;
        margin-bottom: 8px;
    }
    </style>
    """
    m.get_root().header.add_child(folium.Element(custom_css))
    
    # Log completion
    with open("map_debug.log", "a") as log:
        log.write(f"Map creation completed at {time.strftime('%Y-%m-%d %H:%M:%S')}\n")
        log.write(f"Processed {processed_count} locations\n")
    
    return m._repr_html_(), processed_count
    
    custom_css = """
    <style>
    @import url('https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@400;600&display=swap');
    .leaflet-container {
        font-family: 'Source Sans Pro', sans-serif;
    }
    .leaflet-popup-content {
        font-family: 'Source Sans Pro', sans-serif;
    }
    .leaflet-popup-content h4 {
        font-weight: 600;
        margin-bottom: 8px;
    }
    </style>
    """
    m.get_root().header.add_child(folium.Element(custom_css))
    
    return m._repr_html_(), processed_count


 

custom_css = """
<style>
@import url('https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@300;400;600;700&display=swap');
body, .gradio-container {
    font-family: 'Source Sans Pro', sans-serif !important;
    color: #333333;
}
h1 {
    font-weight: 700 !important;
    color: #2c6bb3 !important;
    font-size: 2.5rem !important;
    margin-bottom: 1rem !important;
}
h2 {
    font-weight: 600 !important;
    color: #4e8fd1 !important;
    font-size: 1.5rem !important;
    margin-top: 1rem !important;
    margin-bottom: 0.75rem !important;
}
.gradio-button.primary {
    background-color: #ff7518 !important;
}
.gradio-button.secondary {
    background-color: #5a87ca !important;
    color: white !important;
}
.info-box {
    background-color: #e8f4fd;
    border-left: 4px solid #2c6bb3;
    padding: 15px;
    margin: 15px 0;
    border-radius: 4px;
}
.file-upload-box {
    border: 2px dashed #e0e0e0;
    border-radius: 8px;
    padding: 20px;
    text-align: center;
    transition: all 0.3s ease;
}
/* Fix for map container spacing */
#map-container {
    height: 20vh !important;
    margin-bottom: 0 !important;
    padding-bottom: 0 !important;
}
/* Stats box styling */
.stats-box {
    margin-top: 10px !important;
    margin-bottom: 0 !important;
    padding: 10px;
    background: #f8f9fa;
    border-radius: 4px;
}
/* Remove extra space around components */
.gr-box {
    margin-bottom: 0 !important;
}
/* Model status styling */
.model-status {
    padding: 10px;
    border-radius: 4px;
    margin-bottom: 15px;
    background-color: #f8f9fa;
    font-size: 14px;
}
.separator {
    margin: 20px 0;
    border-top: 1px solid #eaeaea;
}
</style>
"""
 
with gr.Blocks(css=custom_css, title="Daten Strukturieren und Analysieren") as demo:
    gr.HTML("""
    <div style="text-align: center; margin-bottom: 1rem">
        <h1>Strukturierung und Visualisierung von historischen Daten</h1>
    </div>
      
      <!-- Quick Summary Box -->
      <div style="background-color: #f8f9fa; padding: 1.5rem; border-radius: 8px; margin-bottom: 2rem; border-left: 5px solid #4e8fd1; box-shadow: 0 2px 10px rgba(0,0,0,0.05);">
        <h3 style="color: #2c6bb3; margin-top: 0; margin-bottom: 15px; font-size: 1.4rem;">Kurz erklärt: Was macht diese Anwendung?</h3>
        
        <ol style="padding-left: 20px; margin-bottom: 0;">

          <li style="margin-bottom: 15px; font-size: 1.1rem; line-height: 1.5;">
            <strong style="color: #2c6bb3;">Textanalyse:</strong> Finden Sie automatisch wichtige Informationen in historische Zeitungsartikeln mit Hilfe eines kleinen Sprachmodells.
          </li>

          <li style="margin-bottom: 0; font-size: 1.1rem; line-height: 1.5;">
            <strong style="color: #2c6bb3;">Textorganisaton:</strong> Organisieren Sie historische Daten zu strukturierten, analysierbaren Informationen.
          </li>   
          
          <li style="margin-bottom: 15px; font-size: 1.1rem; line-height: 1.5;">
            <strong style="color: #2c6bb3;">Visualisierung:</strong> Visualisieren Sie gefunden Orte auf einer Karte.
          </li>
          
          
        </ol>
      </div>
      
      <!-- Main Explanation Section -->
      <div style="line-height: 1.7; font-size: 1.15rem; margin-bottom: 2rem;">
        <p style="font-size: 1.2rem; margin-bottom: 1.5rem; color: #2c3e50; font-weight: 400; padding: 0 1rem; line-height: 1.8;">
          In dieser Unterrichtseinheit befassen wir uns mit der Strukturierung unstrukturierter historischer Texte und der Visualisierung von extrahierten Daten auf Karten. Die systematische Strukturierung von Daten wird mit einem für Informationsextrahierung trainiertem Sprachmodell durchgeführt, das auf der Question-Answering-Methode basiert. Diese Methode erlaubt es, Informationen mit Hilfe einer Frage zu extrahieren, wie etwa „Wo fand das Erdbeben statt"? Dies ermöglicht die Extrahierung des Ortes, an dem ein Erdbeben stattfand, auch wenn im Text selbst noch andere Orte genannt werden.
        </p>
      </div>
      
      <!-- Example Section with Improved Styling -->
      <div style="line-height: 1.7; font-size: 1.15rem; background: #f8f9fa; padding: 1.5rem; border-radius: 8px; margin: 2rem 0; box-shadow: 0 2px 10px rgba(0,0,0,0.05); border: 1px solid #e8e8e8;">
        <h3 style="color: #2c6bb3; margin-top: 0; margin-bottom: 15px;">Beispiel einer Textanalyse</h3>
        
        <div style="background-color: white; padding: 1.5rem; border-radius: 6px; font-family: 'Source Sans Pro', sans-serif; line-height: 1.7;">
          Die Katastrophe in <span style="background-color: #a8e6cf; font-weight: bold; padding: 2px 5px; border-radius: 3px;" title="Earthquake Location">Japan</span> — 3 Millionen Tote. Mtb. <span style="background-color: #ffdfba; font-weight: bold; padding: 2px 5px; border-radius: 3px;" title="Non-Earthquake Location">London</span>, 4. Sept. (Drahtbericht.) Zu dem Unglück in <span style="background-color: #a8e6cf; font-weight: bold; padding: 2px 5px; border-radius: 3px;" title="Earthquake Location">Japan</span> liegen noch folgende Nachrichten vor: Wie die japanische Gesandtschaft in <span style="background-color: #ffdfba; font-weight: bold; padding: 2px 5px; border-radius: 3px;" title="Non-Earthquake Location">Peking</span> meldet, sind Unterhandlungen mit <span style="background-color: #ffdfba; font-weight: bold; padding: 2px 5px; border-radius: 3px;" title="Non-Earthquake Location">China</span> über die sofortige Lieferung von Lebensmitteln ausgenommen worden. Von <span style="background-color: #ffdfba; font-weight: bold; padding: 2px 5px; border-radius: 3px;" title="Non-Earthquake Location">Peking</span> seien amerikanische, englische und italienische Schiffe mit Lebensmitteln nach <span style="background-color: #a8e6cf; font-weight: bold; padding: 2px 5px; border-radius: 3px;" title="Earthquake Location">Japan</span> abgegangen.
        </div>
        
        <!-- Legend with Improved Layout -->
        <div style="display: flex; margin-top: 20px; flex-wrap: wrap; justify-content: flex-start;">
          <div style="display: flex; align-items: center; margin-right: 30px; margin-bottom: 10px;">
            <div style="width: 20px; height: 20px; background-color: #a8e6cf; margin-right: 10px; border-radius: 3px;"></div>
            <span style="font-weight: 600;">Ort des Erdbebens: Japan</span>
          </div>
          <div style="display: flex; align-items: center;">
            <div style="width: 20px; height: 20px; background-color: #ffdfba; margin-right: 10px; border-radius: 3px;"></div>
            <span style="font-weight: 600;">Andere Orte: London, Peking, China</span>
          </div>
        </div>
      </div>
    </div>
    
  <div style="background: #f8f9fa; padding: 2rem; border-radius: 10px; margin-bottom: 2.5rem; border-left: 5px solid #3498db;">
    <h3 style="margin-top: 0; color: #2c3e50; border-bottom: 2px solid #eee; padding-bottom: 0.8rem; font-size: 1.5rem;">
      Methodik: Vom unstrukturierten Text zur strukturierten Information
    </h3>
    
    <p style="margin-bottom: 1.8rem; font-size: 1.2rem;">
      Die grundlegende Herausforderung bei der Arbeit mit historischen Quellen ist, dass relevante Informationen in langen 
      Fließtexten eingebettet sind und manuell mühsam extrahiert werden müssen. Dieser Ansatz automatisiert diesen Prozess.
    </p>
    
    <h4 style="color: #2980b9; margin-top: 2rem; font-size: 1.35rem;">Wie funktioniert die Informationsextraktion?</h4>
    
    <ol style="padding-left: 2rem; font-size: 1.15rem;">
      <li style="margin-bottom: 1.5rem;">
        <strong style="color: #2c3e50; font-size: 1.2rem;">Template-Definition</strong>: Sie definieren ein JSON-Template mit den Informationstypen, die Sie extrahieren möchten:
        <pre style="background: #f5f5f5; padding: 1.2rem; border-radius: 6px; overflow-x: auto; margin: 1rem 0 1.5rem; font-size: 1.1rem;"><code>{"What are the earthquake locations": "", "What is the place name in the dateline": ""}</code></pre>
      </li>
      
      <li style="margin-bottom: 1.5rem;">
        <strong style="color: #2c3e50; font-size: 1.2rem;">Question-Answering-Methode</strong>: Das Sprachmodell füllt die Lehren Felder mit den Antwortn auf die Frage:
        <ul style="margin-top: 1rem; padding-left: 2rem; font-size: 1.15rem;">
          <li style="margin-bottom: 0.8rem;"><code style="background: #f0f0f0; padding: 0.3rem 0.5rem; border-radius: 4px; font-size: 1.1rem;">"What are the earthquake locations": ""</code> → "Japan"</li>
          <li style="margin-bottom: 0.8rem;"><code style="background: #f0f0f0; padding: 0.3rem 0.5rem; border-radius: 4px; font-size: 1.1rem;">"What is the place name in the dateline": ""</code> → "Paris"</li>
        </ul>
      </li>
      
      <li style="margin-bottom: 1.5rem;">
        <strong style="color: #2c3e50; font-size: 1.2rem;">Sprachmodell-Verarbeitung</strong>: Das NuExtract-1.5 Modell (ein Sequence-to-Sequence Transformer) analysiert den Text vollständig und identifiziert die relevanten Informationen für jedes Template-Feld.
      </li>
      
      <li style="margin-bottom: 1rem;">
        <strong style="color: #2c3e50; font-size: 1.2rem;">Strukturierte Ausgabe</strong>: Das Modell füllt das Template mit den extrahierten Informationen:
        <pre style="background: #f5f5f5; padding: 1.2rem; border-radius: 6px; overflow-x: auto; margin: 1rem 0 1.5rem; font-size: 1.1rem;"><code>{"What are the earthquake locations": "Japan, Yokohama", "What is the place name in the dateline": "Tokio"}</code></pre>
      </li>
    </ol>
  </div>
  <div style="background: #f8f9fa; padding: 2rem; border-radius: 10px; margin-bottom: 2.5rem; border-left: 5px solid #9b59b6;">
    <h4 style="color: #2980b9; margin-top: 0; font-size: 1.35rem;">Technische Funktionsweise des Sprachmodells</h4>
    
    <p style="font-size: 1.2rem;">Intern erfolgt die Verarbeitung in mehreren Schritten:</p>
    
    <ol style="padding-left: 2rem; font-size: 1.15rem;">
      <li style="margin-bottom: 1rem;"><strong style="color: #2c3e50; font-size: 1.2rem;">Tokenisierung</strong>: Der Text wird in bearbeitbare Einheiten zerlegt.</li>
      <li style="margin-bottom: 1rem;"><strong style="color: #2c3e50; font-size: 1.2rem;">Kontextuelle Analyse</strong>: Der Transformer-Mechanismus ermöglicht die Analyse von Beziehungen zwischen allen Textteilen gleichzeitig.</li>
      <li style="margin-bottom: 1rem;"><strong style="color: #2c3e50; font-size: 1.2rem;">Selektive Aufmerksamkeit</strong>: Das Modell fokussiert sich auf Textpassagen, die Antworten auf die impliziten Fragen enthalten könnten.</li>
      <li style="margin-bottom: 1rem;"><strong style="color: #2c3e50; font-size: 1.2rem;">Generierung</strong>: Die erkannten Informationen werden in das vorgegebene Template eingefügt.</li>
    </ol>
  </div>
  <div style="background: #f8f9fa; padding: 2rem; border-radius: 10px; margin-bottom: 2.5rem; border-left: 5px solid #27ae60;">
    <h3 style="margin-top: 0; color: #2c3e50; border-bottom: 2px solid #eee; padding-bottom: 0.8rem; font-size: 1.5rem;">
      Mapping
    </h3>
    
    <p style="margin-bottom: 1.8rem; font-size: 1.2rem;">
      Nach der Extraktion der Ortsangaben ermöglicht unsere Anwendung die automatische Visualisierung dieser Daten auf einer interaktiven Karte:
    </p>
    
    <ol style="padding-left: 2rem; font-size: 1.15rem;">
      <li style="margin-bottom: 1.2rem;">
        <strong style="color: #2c3e50; font-size: 1.2rem;">Geokodierung</strong>: Die extrahierten Ortsnamen werden mittels eines geografischen Dienstes in geografische Koordinaten (Längen- und Breitengrade) umgewandelt.
      </li>
      
      <li style="margin-bottom: 1.2rem;">
        <strong style="color: #2c3e50; font-size: 1.2rem;">Kartenerstellung</strong>: Die Koordinaten werden auf einer interaktiven Karte platziert, wobei jeder Ort durch einen Marker dargestellt wird.
      </li>
      
      <li style="margin-bottom: 1.2rem;">
        <strong style="color: #2c3e50; font-size: 1.2rem;">Kontextinformationen</strong>: Beim Klick auf einen Marker werden zusätzliche Informationen aus dem Originaltext angezeigt.
      </li>
      
      <li style="margin-bottom: 1rem;">
        <strong style="color: #2c3e50; font-size: 1.2rem;">Räumliche Analyse</strong>: Die Karte ermöglicht die visuelle Analyse der räumlichen Verteilung historischer Ereignisse.
      </li>
    </ol>
    
    <p style="font-size: 1.2rem; margin-top: 1.5rem;">
      Dieser kombinierte Ansatz aus Textextraktion und geografischer Visualisierung eröffnet neue Möglichkeiten für die räumliche Analyse historischer Quellen und erlaubt es, geografische Muster zu erkennen, die in den reinen Textdaten nicht unmittelbar sichtbar wären.
    </p>
  </div>
  <div style="margin-top: 2.5rem; padding: 1.5rem; background: #e8f4fd; border-radius: 10px; text-align: center; font-size: 1.1rem;">
    <p style="margin: 0;">Diese Methode ermöglicht die effiziente Extraktion und Visualisierung historischer Daten aus unstrukturierten Quellen.</p>
  </div>
</div>
    """)
    
    with gr.Tabs() as tabs:
        with gr.TabItem("🔍 Text Extrahierung"):
            gr.HTML("""
            <div class="info-box">
                <h3 style="margin-top: 0;">Extrahieren Sie strukturierte Daten aus unstrukturiertem Text</h3>
                <p>Verwenden Sie das Sprachmodell NuExtract-1.5 um automatisch Informationen zu extrahieren.</p>
            </div>
            """)
            
            # Add model loading button and status at the top
            
            
            with gr.Row():
                with gr.Column():
                    template = gr.Textbox(
                        label="JSON Template", 
                        value='{"What are the earthquake locations": "", "What is the place name in the dateline": "", "What is the source of information": "", "What is the communication form": ""}',
                        lines=5
                    )
                    text = gr.Textbox(
                        label="Hier unstrukturierten Text einfügen",
                        value="Die Zahl der Erdbebenopfer in Japan. Paris, 12. Sept. Der japanische Konsul in Marseille veröffentlicht nachstehendes offizielles Telegramm, das er heute aus Japan erhalten hat: „Die Zahl der Toten beträgt in Tokio laut einer von der Polizei vorgenommenen Zählung mehr als 60000. Die Zahl der Verwundeten beläuft sich auf ungefähr 500000. In Jokohama beträgt die Zahl der Opfer 110 000, was ungefähr ein Viertel der gesamten Bevölkerung dieser Stadt ausmacht. In den Bezirken von Chiba und Kanagama ist die Zahl der Opfer gleichfalls beträchtlich, doch wurde die Zählung noch nicht zu Ende geführt",
                        lines=8
                    )
                    extract_btn = gr.Button("Extrahieren Sie Informationen", variant="primary")
                
                with gr.Column():
                    status = gr.Textbox(label="Status")
                    output = gr.Textbox(label="Output", lines=10)
                    excel_download_file = gr.File(
                        label="Excel-Vorlage herunterladen",
                        value="Earthquake_Japan_Places.xlsx",  # Replace with the actual path to your Excel file
                        visible=True,
                        interactive=False
                    )
            
            extract_btn.click(
                fn=extract_info,
                inputs=[template, text],
                outputs=[status, output]
            )

            
            # Add dateline examples section below the extraction
            # Note: This HTML block must be properly indented to be within the first tab
            with gr.Row():
                gr.HTML("""
                <div style="margin-top: 30px; border-top: 1px solid #eaeaea; padding-top: 20px;">
                    <h3 style="color: #4e8fd1; margin-bottom: 15px;">Zeitungsartikel</h3>
        
                    
                    <div style="margin-bottom: 20px; padding: 15px; background-color: white; border-left: 4px solid #0066cc; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
                        <div style="color: #444; line-height: 1.5;">
                            WTB Tokio, 7. Sept. Nachdem infolge Verkündigung des Kriegsrechts General Fukuda den Beschluss über die Stadt übernommen hat, ist die Ordnung im Innern wieder so gut wie hergestellt. Lebensmittelvorräte treffen aus den verschiedensten Gegenden ein. Plünderer werden auf der Stelle standrechtlich erschossen — Die Land- und Seezone, die von dem Erdbeben heimgesucht wurde, hat Jokohama zum Zentrum. Sie erstreckt sich über 130 Kilometer von Norden nach Süden und in der Richtung von Osten nach Westen über 450 Kilometer. Im ganzen sind etwa 500000 Quadratkilometer javanischen Bodens vom Erdbeben betroffen worden. Dieses Gebiet umfaßt 5 Großstädte, 10 Landratsämter, sowie 132 Unterverwaltungen. Ihre Bewohnerzahl ist 9 Millionen. 70 Prozent der Städte, Ortschaften und Dörfer sind verwüstet. 
                        </div>

                    </div>
                    
                    <div style="margin-bottom: 20px; padding: 15px; background-color: white; border-left: 4px solid #0066cc; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
                        <div style="color: #444; line-height: 1.5;">
                            Erdbeben und Politik (Von unserem ständigen Pariser Vertreter) —t. Paris, 4, Sept. 3n Pariser maßgebenden Kreisen beschäftigt man sich gegen wärtig neben der durch Italiens Ansprüche hervorgerufenen Kri sis des Völkerbundes, die schon in ihrem heutigen Stadium Nicht ohne dauernde Foiligen zu bleiben scheint, in erster Linie mit den politischen Auswirkungen der entsetzlichen Naturkatastrophe, die die Weltgeschichte kennt: des Erdbebens in Japan. Die An sicht dieser Kreise, die besonders über die Vorgänge in London stets genau unterrichtet zu sein pflögen, läßt sich ungefähr in folgen der Weise wiedergeben: «Es ist eine bekannte Tatsache, daß Japan eines der ersten Opfer wurde, als die Wirtschafts- und Finanzkrisis nach der Hoch konjunktur des Krieges mit voller Wucht auf der ganzen Erde, und gerade bei den bis jetzt bevorzugten Staaten, einsehte. Wer die M- rüstungskonferenz in Washington miterlobt hat, dem sind gewisse Dinge l>ekannt, die damals in Japan viel böses Mut erregten, ob schon sie nur die direkte, politische Auswirkung wetten der finan ziellen Schwierigkeiten, -die dem Inselreich bereits einmal die Früchte eines teuer erkauften Sieges entrissen hatten
                        </div>

                    </div>

                    <div style="margin-bottom: 20px; padding: 15px; background-color: white; border-left: 4px solid #0066cc; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
                        <div style="color: #444; line-height: 1.5;">
                            Neues Erdbeben in Japan. w. London, 16. Januar. (Drahtdertcht.) Reuter mewet aus Osaka: Die telephonische und telegra» phische Verbindung zwischen Tokio und Osaka ist gestern morgen 5.45 Uhr durch ein Erdbeben unterbrochen worden. Die Trambahn in Tokio liegt still. Der Eisenbahnverkehr zwischen Tokio und Yokohama ist unterbrochen. Die königliche Familie ist in Sicherheit. In Lugamo, einer Dor« stadt Tokios, sind Brände ausgebrochen. Eia Elsenbahnzug siürzle in einen Flutz. Aus Tokio wird weiter gemeldet, daß bei dem Erdbeben in Osaka sechs Personen getötet und 22 verwundet, in Tokio vier getötet und 20 verletzt wurden. In Yokohama sind 600 Häuser zerstört worden.
                        </div>

                    </div>

                    <div style="margin-bottom: 20px; padding: 15px; background-color: white; border-left: 4px solid #0066cc; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
                        <div style="color: #444; line-height: 1.5;">
                             Englische Zeitungen sagen in einer Besprechung der Folgen des japanischen Erdbebens für Japan, daß Japan durch diese Katastrophe ein armes Land geworden ist; die Aufschwung im wirtschaftlichen Leben der Völker, seine beginnende Verdrängung der amerikanischen und englischen Konkurrenz, sind nicht mehr. Auch die Ausdehnungsbemühungen Japans gegenüber China, die in den letzten Jahren unterblieben, dann Japan wird alle Kräfte zum Wiederaufbau seines Landes brauchen. 13 große Städte sind zerstört. Alle großen Industrieanlagen um Tokyo sind vernichtet. Ein paar Minuten genügten, um Japan aus den Reihen der Großmächte der Welt auszuschalten. Ein einziger Ruck ging durch die Erde und Japans Weltstellung erfuhr einen bedeutenden Stoß. Japan steht mit einem Male wieder vor der Lehre des Jahres 1914 zurückgeworfen. Es scheidet als Konkurrent auf dem Weltmarkt für gemume Zeit aus und büßt, was noch erheblich schwerer wiegt, die finanzielle Unabhängigkeit, die es sich im Kriege erlangt hatte, vollkommen ein.

                        </div>

                    </div>

                    <div style="margin-bottom: 20px; padding: 15px; background-color: white; border-left: 4px solid #0066cc; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
                        <div style="color: #444; line-height: 1.5;">
                            Die politischen Folgen der japanischen Erdbebenkatastrophe. Rückwirkungen auf di « ganz « Welt. Paris, 5. Sept. Die französische Presse hat es bisher vermieden, auf die politischen Folgen der japanischen Erdbebenkatastrophe hinzuweisen. Heute berührt die Iournee Industrielle diese wichtige Frag «. Das Blatt führt aus: Die ganze Welt werde die wirtschaftlichen und politischen Rückwirkungen des Anglücks spüren. Japan werde durch den Wiederaufbau gezwungen sein, die ganz « Kraft auf dag eigene Land zu konzentrieren
                        </div>

                    </div>
                </div>
                """)
        
    
        with gr.TabItem("📍 Visualisierung von strukturierten Daten"):
            gr.HTML("""
            <div class="info-box">
                <h3 style="margin-top: 0;">Visualisieren Sie Daten auf Karten</h3>
                <p>Laden Sie eine Excel-Tabelle hoch und erstelle eine interaktive Karte.</p>
            </div>
            """)
            
            with gr.Row():
                with gr.Column():
                    excel_file = gr.File(
                        label="Upload Excel File",
                        file_types=[".xlsx", ".xls"],
                        elem_classes="file-upload-box"
                    )
                    places_column = gr.Textbox(
                        label="Name der Tabellenspalte",
                        value="earthquake_locations",
                        placeholder="Füge den Namen der Spalte mit den Orten ein"
                    )
                    process_btn = gr.Button("Erstellen Sie die Karte", variant="primary")
                    stats_output = gr.Textbox(
                        label="Status",
                        lines=2,
                        elem_classes="stats-box"
                    )
                    processed_file = gr.File(
                        label="Bearbeitete Daten herunterladen",
                        visible=True,
                        interactive=False
                    )
                
                with gr.Column():
                    map_output = gr.HTML(
                        label="Interaktive Karte",
                        value="""
                        <div style="text-align:center; height:20vh; width:100%; display:flex; align-items:center; justify-content:center; 
                                background-color:#f5f5f5; border:1px solid #e0e0e0; border-radius:8px;">
                            <div>
                                <img src="https://cdn-icons-png.flaticon.com/512/854/854878.png" width="100">
                                <p style="margin-top:20px; color:#666;">Your map will appear here after processing</p>
                            </div>
                        </div>
                        """,
                        elem_id="map-container"
                    )
                    
            
            def process_and_map(file, column):
                if file is None:
                    return None, "Hier bitte die Excel-Tabelle hochladen", None
                
                try:
                    map_html, stats, processed_path = process_excel(file, column)
                    
                    if map_html and processed_path:
                        responsive_html = f"""
                        <div style="width:100%; height:20vh; margin:0; padding:0; border:1px solid #e0e0e0; border-radius:8px; overflow:hidden;">
                            {map_html}
                        </div>
                        """
                        return responsive_html, stats, processed_path
                    else:
                        return None, stats, None
                except Exception as e:
                    import traceback
                    trace = traceback.format_exc()
                    print(f"Error in process_and_map: {e}\n{trace}")
                    return None, f"Error: {str(e)}", None
            
            process_btn.click(
                fn=process_and_map,
                inputs=[excel_file, places_column],
                outputs=[map_output, stats_output, processed_file]
            )
    
    gr.HTML("""
    <div style="text-align: center; margin-top: 2rem; padding-top: 1rem; border-top: 1px solid #eee; font-size: 0.9rem; color: #666;">
        <p>Made with <span style="color: #e25555;">❤</span> for historical research</p>
    </div>
    """)

if __name__ == "__main__":
    demo.launch(share=True)