File size: 32,611 Bytes
702e195
 
 
121b6a5
8864088
702e195
 
ffd60ac
702e195
8864088
702e195
8864088
702e195
8864088
ffd60ac
8864088
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ffd60ac
8864088
ffd60ac
 
 
702e195
ffd60ac
702e195
 
ffd60ac
 
 
702e195
 
ffd60ac
702e195
 
ffd60ac
702e195
ffd60ac
702e195
ffd60ac
 
702e195
ffd60ac
702e195
 
 
 
ffd60ac
702e195
 
 
 
 
ffd60ac
702e195
ffd60ac
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
702e195
ffd60ac
 
 
 
 
 
 
 
 
 
 
 
702e195
ffd60ac
 
 
 
 
 
 
702e195
ffd60ac
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46c56e9
ffd60ac
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5356916
ffd60ac
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
702e195
ffd60ac
702e195
ffd60ac
 
 
 
702e195
ffd60ac
 
 
702e195
0ebdc64
702e195
0ebdc64
 
 
 
702e195
ffd60ac
702e195
 
ffd60ac
702e195
ffd60ac
702e195
 
 
 
 
 
ffd60ac
702e195
 
 
 
ffd60ac
702e195
 
 
 
 
 
 
 
 
ffd60ac
702e195
ffd60ac
 
 
 
 
 
 
 
 
 
 
 
 
 
 
702e195
ffd60ac
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
702e195
 
 
ffd60ac
 
 
 
 
 
 
 
 
 
702e195
ffd60ac
 
 
702e195
ffd60ac
 
702e195
ffd60ac
 
 
 
702e195
ffd60ac
 
 
 
702e195
ffd60ac
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
702e195
 
ffd60ac
 
 
 
 
702e195
 
 
 
ffd60ac
702e195
 
ffd60ac
702e195
 
ffd60ac
 
702e195
ffd60ac
 
 
702e195
 
ffd60ac
 
702e195
 
 
ffd60ac
702e195
 
 
 
 
 
 
 
ffd60ac
702e195
 
 
 
 
 
 
 
 
 
 
ffd60ac
5356916
ffd60ac
 
702e195
 
 
 
 
 
ffd60ac
 
702e195
 
 
 
 
 
 
 
 
 
ffd60ac
702e195
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ffd60ac
 
 
 
702e195
 
 
 
 
 
 
ffd60ac
 
702e195
 
 
 
 
 
 
 
ffd60ac
702e195
 
ffd60ac
 
 
 
 
 
 
 
 
 
 
702e195
ffd60ac
 
 
 
 
 
 
 
 
 
 
5356916
ffd60ac
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
702e195
ffd60ac
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
702e195
ffd60ac
 
 
702e195
ffd60ac
 
 
 
 
702e195
ffd60ac
 
 
702e195
ffd60ac
 
 
 
 
 
 
 
 
 
 
 
 
702e195
ffd60ac
 
 
 
 
 
 
 
 
 
 
5356916
ffd60ac
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
702e195
ffd60ac
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
702e195
ffd60ac
 
 
702e195
ffd60ac
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
702e195
ffd60ac
 
 
702e195
ffd60ac
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
702e195
 
 
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
import streamlit as st
import requests
from together import Together
import os
import json

# =============================================================================
# CONFIGURATION - Using Secrets Management
# =============================================================================
NOCODB_URL = "https://mtoft20-potm.hf.space".strip()  # Base URL, ensure no extra spaces

# Get sensitive data from Streamlit secrets or environment variables
def get_api_credentials():
    """Get API credentials from secrets or environment"""
    try:
        # Try Streamlit secrets first (for Hugging Face Spaces)
        api_token = st.secrets.get("NOCODB_API_TOKEN", os.environ.get("NOCODB_API_TOKEN", "")).strip()
        together_key = st.secrets.get("TOGETHER_API_KEY", os.environ.get("TOGETHER_API_KEY", "")).strip()
        
        # Get endpoints for content and similarities
        content_endpoint = st.secrets.get("NOCODB_CONTENT_ENDPOINT", os.environ.get("NOCODB_CONTENT_ENDPOINT", "")).strip()
        similarity_endpoints = [endpoint.strip() for endpoint in 
                             st.secrets.get("NOCODB_SIMILARITY_ENDPOINTS", 
                                          os.environ.get("NOCODB_SIMILARITY_ENDPOINTS", "")).split(",")
                             if endpoint.strip()]
        
        return api_token, together_key, content_endpoint, similarity_endpoints
    except:
        # Fallback to environment variables
        api_token = os.environ.get("NOCODB_API_TOKEN", "").strip()
        together_key = os.environ.get("TOGETHER_API_KEY", "").strip()
        content_endpoint = os.environ.get("NOCODB_CONTENT_ENDPOINT", "").strip()
        similarity_endpoints = [endpoint.strip() for endpoint in 
                             os.environ.get("NOCODB_SIMILARITY_ENDPOINTS", "").split(",")
                             if endpoint.strip()]
        
        return api_token, together_key, content_endpoint, similarity_endpoints

# Initialize Together AI client
@st.cache_resource
def get_ai_client():
    """Initialize Together AI client"""
    _, together_key, _, _ = get_api_credentials()
    if not together_key:
        st.error("Together AI API key not found. Please configure it in the secrets.")
        return None
    return Together(api_key=together_key)

# =============================================================================
# HELPER FUNCTIONS
# =============================================================================
@st.cache_data(ttl=300)  # Cache for 5 minutes
def get_streaming_content():
    """Fetch streaming content from NocoDB with pagination"""
    api_token, _, content_endpoint, _ = get_api_credentials()
    
    if not api_token or not content_endpoint:
        st.error("NocoDB credentials not configured. Please set up your secrets.")
        return []
    
    headers = {
        "xc-token": api_token,
        "accept": "application/json"
    }
    
    all_content = []
    page = 1
    page_size = 1000  # NocoDB default page size
    
    try:
        while True:
            offset = (page - 1) * page_size
            url = f"{NOCODB_URL.strip()}{content_endpoint.strip()}?limit={page_size}&offset={offset}"
            
            response = requests.get(url, headers=headers)
            
            if response.status_code == 200:
                data = response.json()
                current_page_data = data.get('list', [])
                
                if not current_page_data:  # No more data to fetch
                    break
                    
                all_content.extend(current_page_data)
                
                # Check if this is the last page
                page_info = data.get('pageInfo', {})
                if page_info.get('isLastPage', True):
                    break
                
                page += 1
            else:
                st.error(f"Failed to fetch data: {response.status_code}")
                if not all_content:  # Only return [] if we haven't fetched any data
                    return []
                break  # If we have some data, return what we've got
                
        return all_content
            
    except Exception as e:
        st.error(f"Error connecting to database: {str(e)}")
        st.write("Full error details:", e)
        return []

def filter_content(content_list, filters):
    """Apply filters to streaming content list"""
    filtered = []
    
    for content in content_list:
        if not content or not isinstance(content, dict):
            continue
            
        matches_all_filters = True
        
        # Streaming service filter
        if filters['streaming_services']:
            if content.get('streaming_service') not in filters['streaming_services']:
                matches_all_filters = False
                continue
        
        # Type filter - only apply if not "All"
        if filters['content_type']:
            if content.get('type') != filters['content_type']:
                matches_all_filters = False
                continue
            
        # Genre filter - check if ALL selected genres are in the content's genres
        if filters['genres']:
            content_genres = set(g.strip().lower() for g in str(content.get('listed_in', '')).split(','))
            selected_genres = set(g.strip().lower() for g in filters['genres'])
            
            if not selected_genres.issubset(content_genres):
                matches_all_filters = False
                continue
            
        # Rating filter
        if filters['ratings']:
            rating = (content.get('rating') or '').strip()
            # Only compare if rating is a valid string and not a duration
            if not rating or not isinstance(rating, str) or rating.endswith('min'):
                matches_all_filters = False
                continue
            if rating not in filters['ratings']:
                matches_all_filters = False
                continue

        # Release year filter
        try:
            release_year = int(content.get('release_year', 0))
            if release_year < filters['year_range'][0] or release_year > filters['year_range'][1]:
                matches_all_filters = False
                continue
        except (ValueError, TypeError):
            matches_all_filters = False
            continue
            
        # Duration filter (different handling for movies)
        if filters['content_type'] == 'Movie':
            duration = str(content.get('duration', ''))
            if 'min' in duration:
                try:
                    minutes = int(duration.split()[0])
                    if minutes < filters['duration_range'][0] or minutes > filters['duration_range'][1]:
                        matches_all_filters = False
                        continue
                except (ValueError, IndexError):
                    matches_all_filters = False
                    continue

        # Director filter (optional)
        if filters['director']:
            director = str(content.get('director', '')).lower()
            if not any(name.strip().lower() in director for name in filters['director'].split(',')):
                matches_all_filters = False
                continue

        # Cast filter (optional)
        if filters['cast']:
            cast = str(content.get('cast', '')).lower()
            if not any(name.strip().lower() in cast for name in filters['cast'].split(',')):
                matches_all_filters = False
                continue
        
        if matches_all_filters:
            filtered.append(content)
    
    return filtered

def create_content_context(content_list):
    """Create context string about current content for AI"""
    if not content_list:
        return "No content matches the current filters."
    
    total = len(content_list)
    movies = sum(1 for c in content_list if c.get('type') == 'Movie')
    shows = sum(1 for c in content_list if c.get('type') == 'TV Show')
    
    context = f"""Currently showing {total} titles ({movies} movies and {shows} TV shows) """
    
    # Add streaming services info
    services = set(c.get('streaming_service') for c in content_list if c.get('streaming_service'))
    if services:
        context += f"available on {', '.join(services)}. "
    
    return context

def get_ai_response(client, question, context, model_name):
    """Get response from Together AI"""
    try:
        prompt = f"""You are a helpful streaming content expert. Based on the current content data, please answer the user's question accurately and helpfully.

Current Content Data Context:
{context}

User Question: {question}

Please provide a helpful, accurate response based on the data provided. Keep your answer concise but informative."""

        response = client.chat.completions.create(
            model=model_name,
            messages=[
                {"role": "system", "content": "You are a helpful content expert with deep knowledge of movies and TV shows."},
                {"role": "user", "content": prompt}
            ],
            max_tokens=300,
            temperature=0.7,
        )
        
        return response.choices[0].message.content
        
    except Exception as e:
        raise Exception(f"Together AI Error: {str(e)}")

def extract_unique_names(content_list, field):
    """Extract unique names from a comma-separated field in content list"""
    unique_names = set()
    for content in content_list:
        names = content.get(field, '')
        if names:
            # Split by comma and clean each name
            for name in names.split(','):
                cleaned_name = name.strip()
                if cleaned_name:  # Only add non-empty names
                    unique_names.add(cleaned_name)
    return sorted(list(unique_names))

def get_similar_content(content, n_recommendations=5):
    """Get pre-computed similar content from database"""
    try:
        # Get database credentials
        api_token, _, content_endpoint, similarity_endpoints = get_api_credentials()
        
        if not api_token or not similarity_endpoints:
            st.error("NocoDB credentials not configured properly.")
            return []
            
        headers = {
            "xc-token": api_token,
            "accept": "application/json"
        }
        
        title = content.get('title', '')
        show_id = content.get('show_id', '')
        
        # Try finding by both show_id and title
        query = f'where=(show_id,eq,{show_id})~and(title,eq,{title})'
        params = {
            "where": query
        }
        
        for endpoint in similarity_endpoints:
            if not endpoint.strip():  # Skip empty endpoints
                continue
                
            try:
                url = f"{NOCODB_URL.strip()}{endpoint.strip()}"
                response = requests.get(url, headers=headers, params=params)
                
                if response.status_code == 200:
                    data = response.json()
                    if data.get('list'):
                        for entry in data['list']:
                            try:
                                similar_items = json.loads(entry['similar_items'])
                                
                                # Get full content details for each similar item
                                similar_content = []
                                for item in similar_items[:n_recommendations]:
                                    show_id = item.get('show_id', '')
                                    query = f'where=(show_id,eq,{show_id})'
                                    content_params = {
                                        "where": query
                                    }
                                    content_url = f"{NOCODB_URL.strip()}{content_endpoint.strip()}"
                                    content_response = requests.get(content_url, headers=headers, params=content_params)
                                    
                                    if content_response.status_code == 200:
                                        content_data = content_response.json()
                                        if content_data and len(content_data.get('list', [])) > 0:
                                            content_dict = content_data['list'][0]
                                            content_dict['similarity'] = f"{item['similarity']:.2%}"
                                            similar_content.append(content_dict)
                                
                                return similar_content[:n_recommendations]
                            except Exception as parse_error:
                                continue
            except Exception as e:
                continue
        
        return []
    except Exception as e:
        return []

# =============================================================================
# MAIN APP
# =============================================================================
def main():
    # Page config
    st.set_page_config(
        page_title="StreamButler - Your Personal Streaming Concierge",
        page_icon="🎩",
        layout="wide"
    )
    
    # Header with butler theme
    st.title("🎩 StreamButler")
    st.write("*At your service! Allow me to curate the perfect streaming entertainment for you.*")
    
    # Check API credentials
    api_token, together_key, content_endpoint, similarity_endpoints = get_api_credentials()
    
    if not together_key:
        st.error("⚠️ Together AI API key not configured!")
        st.info("Please set your TOGETHER_API_KEY in the Hugging Face Spaces secrets.")
        st.stop()
    
    if not api_token or not content_endpoint:
        st.error("⚠️ NocoDB credentials not configured!")
        st.info("Please set NOCODB_API_TOKEN and NOCODB_CONTENT_ENDPOINT in the Hugging Face Spaces secrets.")
        st.stop()
    
    # Initialize AI client
    try:
        client = get_ai_client()
        if not client:
            st.stop()
    except Exception as e:
        st.error(f"Failed to initialize Together AI client: {e}")
        st.stop()
    
    # Load all content first
    with st.spinner("Loading streaming content..."):
        all_content = get_streaming_content()
    
    if not all_content:
        st.error("Could not load streaming content. Please check your NocoDB connection.")
        st.stop()
    
    # Extract unique values for filters
    all_ratings = sorted(list(set(
        c.get('rating') for c in all_content 
        if c and isinstance(c, dict) 
        and c.get('rating') 
        and isinstance(c.get('rating'), str)
        and not c.get('rating').endswith('min')  # Exclude duration values
        and c.get('rating').strip()  # Exclude empty strings
    )))
    all_genres = sorted(list(set(
        genre.strip()
        for c in all_content
        for genre in c.get('listed_in', '').split(',')
        if genre.strip()
    )))
    all_streaming_services = sorted(list(set([c.get('streaming_service') for c in all_content if c.get('streaming_service')])))
    
    # Extract unique directors and cast members
    all_directors = extract_unique_names(all_content, 'director')
    all_cast_members = extract_unique_names(all_content, 'cast')
    
    # Sidebar filters
    st.sidebar.header("πŸ” Filter Content")

    with st.sidebar.form("filter_form"):
        st.subheader("Streaming Services")
        
        # Streaming service selection (required)
        selected_services = st.multiselect(
            "Select Your Streaming Services",
            options=all_streaming_services,
            default=all_streaming_services[:1],  # Default to first service
            help="Select the streaming services you have access to",
            key="streaming_services"
        )
        
        if not selected_services:
            st.warning("Please select at least one streaming service")
        
        st.subheader("Content Filters")
        
        content_type = st.selectbox(
            "Content Type",
            options=["All", "Movie", "TV Show"],
            index=0
        )
        
        selected_genres = st.multiselect(
            "Genres",
            options=all_genres,
            default=[]
        )

        st.subheader("Optional Filters")
        
        # Rating filter
        selected_ratings = st.multiselect(
            "Ratings",
            options=all_ratings,
            default=[],
            help="Filter by content rating"
        )

        # Year range slider
        years = [int(c.get('release_year', 0)) for c in all_content if c.get('release_year')]
        min_year, max_year = min(years), max(years)
        year_range = st.slider(
            "Release Year",
            min_value=min_year,
            max_value=max_year,
            value=(min_year, max_year),
            help="Filter by release year range"
        )
        
        # Duration range slider (for movies only)
        movie_durations = [
            int(str(c.get('duration', '0 min')).split()[0])
            for c in all_content
            if c and c.get('type') == 'Movie' and 'min' in str(c.get('duration', ''))
        ]
        
        if movie_durations:
            min_duration = min(d for d in movie_durations if d > 0)
            max_duration = max(movie_durations)
            duration_range = st.slider(
                "Movie Duration (minutes)",
                min_value=min_duration,
                max_value=max_duration,
                value=(min_duration, max_duration),
                help="This filter only applies to movies"
            )
        else:
            duration_range = (0, 1000)  # Fallback values

        # Director filter with autocomplete
        selected_directors = st.multiselect(
            "Directors",
            options=all_directors,
            default=[],
            help="Select one or more directors (searchable)",
            placeholder="Start typing to search directors..."
        )

        # Cast filter with autocomplete
        selected_cast = st.multiselect(
            "Cast Members",
            options=all_cast_members,
            default=[],
            help="Select one or more cast members (searchable)",
            placeholder="Start typing to search cast members..."
        )
        
        # Submit button
        apply_filters = st.form_submit_button("πŸ” Apply Filters", type="primary")
    
    # Create filter dictionary
    filters = {
        'streaming_services': selected_services,
        'content_type': content_type if content_type != "All" else None,
        'ratings': selected_ratings,
        'genres': selected_genres,
        'year_range': year_range,
        'duration_range': duration_range,
        'director': ','.join(selected_directors) if selected_directors else '',
        'cast': ','.join(selected_cast) if selected_cast else ''
    }
    
    # Only apply filters when the button is clicked
    if apply_filters:
        filtered_content = filter_content(all_content, filters)
        st.session_state.filtered_content = filtered_content
    else:
        # Initialize filtered content if not exists
        if 'filtered_content' not in st.session_state:
            st.session_state.filtered_content = all_content

    # Main content area
    col1, col2 = st.columns([2, 1])
    
    with col1:
        # Content listings
        filtered_count = len(st.session_state.filtered_content)
        if filtered_count == 0:
            st.subheader("πŸ“‹ No Titles Found")
        else:
            # Header with count and page info
            st.subheader(f"πŸ“‹ Found {filtered_count:,} Title{'s' if filtered_count != 1 else ''}")
        
        if st.session_state.filtered_content:
            # Active Filters section with better formatting
            if any([filters['content_type'], filters['genres'], filters['ratings'], 
                   filters['director'], filters['cast']]):
                with st.expander("πŸ” Active Filters", expanded=True):
                    filter_cols = st.columns(2)
                    with filter_cols[0]:
                        if filters['content_type']:
                            st.write(f"**Type:** {filters['content_type']}")
                        if filters['genres']:
                            st.write(f"**Genres:** {', '.join(filters['genres'])}")
                        if filters['ratings']:
                            st.write(f"**Ratings:** {', '.join(filters['ratings'])}")
                    with filter_cols[1]:
                        if filters['director']:
                            st.write(f"**Director:** {filters['director']}")
                        if filters['cast']:
                            st.write(f"**Cast:** {filters['cast']}")
            st.write("---")

            # Pagination setup
            items_per_page = 10
            total_pages = (filtered_count + items_per_page - 1) // items_per_page

            # Initialize page number in session state if not exists
            if 'current_page' not in st.session_state:
                st.session_state.current_page = 1

            # Calculate slice indices for current page
            start_idx = (st.session_state.current_page - 1) * items_per_page
            end_idx = min(start_idx + items_per_page, filtered_count)
            
            # Display current range info
            st.write(f"Showing {start_idx + 1}-{end_idx} of {filtered_count:,} titles")
            
            # Show items for current page
            for i, content in enumerate(st.session_state.filtered_content[start_idx:end_idx], start=start_idx):
                with st.container():
                    st.write(f"### {content.get('title', 'N/A')} ({content.get('release_year', 'N/A')})")
                    
                    # Content details in columns
                    detail_col1, detail_col2 = st.columns(2)
                    
                    with detail_col1:
                        st.write(f"**πŸ“Ί Available on:** {content.get('streaming_service', 'N/A')}")
                        st.write(f"**🎭 Type:** {content.get('type', 'N/A')}")
                        st.write(f"**⭐ Rating:** {content.get('rating', 'N/A')}")
                        st.write(f"**⏱️ Duration:** {content.get('duration', 'N/A')}")
                    
                    with detail_col2:
                        st.write(f"**🎬 Genres:** {content.get('listed_in', 'N/A')}")
                        cast = content.get('cast')
                        cast_display = cast[:100] + "..." if cast and len(cast) > 100 else cast if cast else "N/A"
                        st.write(f"**πŸ‘₯ Cast:** {cast_display}")
                        st.write(f"**πŸ“ Director:** {content.get('director', 'N/A')}")
                    
                    # Description
                    st.write(f"**πŸ“– Description:**")
                    st.write(content.get('description', 'N/A'))
                    
                    # Add Find Similar button with loading state
                    similar_button = st.button(f"πŸ” Find Similar Content", key=f"similar_{i}")
                    if similar_button:
                        with st.spinner("Finding similar content..."):
                            similar_content = get_similar_content(content, n_recommendations=5)
                            
                            if similar_content:
                                # Create tabs for different aspects of recommendations
                                sim_tab1, sim_tab2 = st.tabs(["πŸ“Ί Similar Titles", "πŸ” Why These Recommendations"])
                                
                                with sim_tab1:
                                    for sim_content in similar_content[:5]:  # Show top 5 similar items
                                        with st.container():
                                            col1, col2 = st.columns([3, 1])
                                            with col1:
                                                st.write(f"**{sim_content.get('title')}** ({sim_content.get('type')}, {sim_content.get('release_year')})")
                                                st.write(f"*Available on:* {sim_content.get('streaming_service')}")
                                                st.write(f"*Genres:* {sim_content.get('listed_in')}")
                                                st.write(f"*Cast:* {sim_content.get('cast')}")
                                                st.write(f"*Director:* {sim_content.get('director')}")
                                                st.write(f"*Description:* {sim_content.get('description')}")
                                            with col2:
                                                st.write(f"**Match:** {sim_content.get('similarity', 'N/A')}")
                                            st.write("---")
                                
                                with sim_tab2:
                                    st.write("**Why these recommendations?**")
                                    st.write("""
                                    These recommendations are based on multiple factors:
                                    - Genre and theme matching
                                    - Plot similarity analysis
                                    - Cast and director relationships
                                    - Release year proximity
                                    
                                    The percentage match indicates how similar each title is to your selection.
                                    """)
                            else:
                                st.info("No similar content found.")
                    st.write("---")
            
            # Bottom pagination controls with better layout
            st.write("---")
            page_cols = st.columns([1, 2, 1, 2, 1])
            
            # Previous button
            with page_cols[0]:
                if st.button("← Previous", disabled=st.session_state.current_page == 1, use_container_width=True):
                    st.session_state.current_page -= 1
                    st.rerun()
            
            # Spacer
            with page_cols[1]:
                st.write("")
            
            # Page input
            with page_cols[2]:
                page_input = st.number_input(
                    f"Page (of {total_pages})",
                    min_value=1,
                    max_value=total_pages,
                    value=st.session_state.current_page,
                    key="page_number",
                    help=f"Enter a page number between 1 and {total_pages}"
                )
                if page_input != st.session_state.current_page:
                    st.session_state.current_page = page_input
                    st.rerun()
            
            # Spacer
            with page_cols[3]:
                st.write("")
            
            # Next button
            with page_cols[4]:
                if st.button("Next β†’", disabled=st.session_state.current_page == total_pages, use_container_width=True):
                    st.session_state.current_page += 1
                    st.rerun()
        else:
            st.info("No content matches your current filters. Try adjusting the criteria.")
    
    with col2:
        # AI Chat Section
        st.subheader("🎩 Your Personal Butler")
        st.write("How may I be of assistance in finding your perfect entertainment today?")
        
        # Model selection for Together AI
        model_choice = st.selectbox(
            "Select Your Butler's Expertise Level:",
            [
                "google/gemma-2b-it",
                "google/gemma-2-27b-it",
                "mistralai/Mistral-7B-Instruct-v0.1",
                "NousResearch/Nous-Hermes-2-Mixtral-8x7B-DPO",
                "mistralai/Mixtral-8x7B-Instruct-v0.1"
            ],
            help="Select your butler's level of expertise in making recommendations"
        )
        
        # Example questions
        with st.expander("πŸ’‘ How to Address Your Butler"):
            st.write("""
            Your butler understands requests like:
            β€’ "My good sir, I seek an action film that would also please my companion who favors comedies."
            β€’ "Would you be so kind as to suggest a family-friendly show in the spirit of Stranger Things, but less frightening?"
            β€’ "I've quite enjoyed The Crown and Downton Abbey. Might you recommend similar period dramas?"
            β€’ "The weather is rather gloomy today. Perhaps a charming romantic comedy or musical?"
            β€’ "I'm in search of enlightening documentaries about technology or artificial intelligence."
            β€’ "We're hosting a gathering this evening. What entertainment would you suggest for a group?"
            """)
        
        user_question = st.text_area(
            "How May I Assist You?",
            placeholder="Tell me your preferences, and I shall curate the perfect selection...",
            height=100
        )
        
        if st.button("🎩 Request Recommendations", type="primary"):
            if user_question:
                with st.spinner("Your butler is carefully selecting the perfect entertainment..."):
                    # Create context from current filtered data
                    context = create_content_context(st.session_state.filtered_content)
                    
                    try:
                        # Get AI response
                        ai_response = get_ai_response(client, user_question, context, model_choice)
                        
                        st.success("**🎩 Your Curated Selection:**")
                        st.write(ai_response)
                        
                        # Show streaming availability
                        with st.expander("🎩 Butler's Note"):
                            st.write("""
                            To access your selected entertainment:
                            1. Kindly select your preferred streaming services above
                            2. Locate your chosen title in the curated list
                            3. For similar recommendations, simply request "Find Similar Content"
                            
                            *Is there anything else I can assist you with?*
                            """)
                        
                    except Exception as e:
                        st.error("My sincerest apologies, but I seem to be unable to process your request at the moment. Might we try again?")
                        
            else:
                st.warning("How may I be of assistance? Please share your entertainment preferences.")
    
    # Footer stats with butler theme
    st.markdown("---")
    if all_content:
        total_items = len(all_content)
        filtered_items = len(st.session_state.filtered_content)
        
        st.markdown("### 🎩 Your Entertainment Library")
        
        # Create columns for stats with better spacing
        stat_cols = st.columns(len(selected_services) + 3)
        
        # Basic stats with improved formatting
        with stat_cols[0]:
            st.metric("πŸ“š Complete Collection", f"{total_items:,}")
        
        with stat_cols[1]:
            st.metric("🎯 Curated Selection", f"{filtered_items:,}")
        
        with stat_cols[2]:
            movies = sum(1 for c in st.session_state.filtered_content if c.get('type') == 'Movie')
            shows = sum(1 for c in st.session_state.filtered_content if c.get('type') == 'TV Show')
            st.metric("🎬 Films / πŸ“Ί Series", f"{movies:,} / {shows:,}")
        
        # Streaming service breakdown with icons
        service_icons = {
            "Netflix": "πŸ”΄",
            "Amazon Prime": "πŸ”΅",
            "Hulu": "🟒",
            "Disney+": "🟣"
        }
        
        for i, service in enumerate(selected_services, 3):
            if i < len(stat_cols):
                service_count = sum(1 for c in st.session_state.filtered_content if c.get('streaming_service') == service)
                icon = service_icons.get(service, "πŸ“Ί")
                with stat_cols[i]:
                    st.metric(f"{icon} {service}", f"{service_count:,}")

if __name__ == "__main__":
    main()