mtoft20 commited on
Commit
ffd60ac
Β·
verified Β·
1 Parent(s): 5356916

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +588 -460
src/streamlit_app.py CHANGED
@@ -6,161 +6,232 @@ import os
6
  import json
7
 
8
  # =============================================================================
9
- # CONFIGURATION
10
  # =============================================================================
11
- NOCODB_URL = "https://mtoft20-potm.hf.space".strip()
12
 
13
- # =============================================================================
14
- # API CLIENTS & CREDENTIALS
15
- # =============================================================================
16
  def get_api_credentials():
17
- """Get API credentials and endpoints from secrets or environment."""
18
- def get_secret(key):
19
- """Helper to get and clean secret values."""
20
- return st.secrets.get(key, os.environ.get(key, "")).strip()
21
-
22
- def get_endpoints(key):
23
- """Helper to get and clean endpoint lists."""
24
- return [ep.strip() for ep in get_secret(key).split(",") if ep.strip()]
25
-
26
- return {
27
- 'api_token': get_secret("NOCODB_API_TOKEN"),
28
- 'together_key': get_secret("TOGETHER_API_KEY"),
29
- 'content_endpoint': get_secret("NOCODB_CONTENT_ENDPOINT"),
30
- 'similarity_endpoints': get_endpoints("NOCODB_SIMILARITY_ENDPOINTS")
31
- }
 
 
 
 
 
 
 
 
 
32
 
 
33
  @st.cache_resource
34
  def get_ai_client():
35
- """Initialize Together AI client."""
36
- creds = get_api_credentials()
37
- if not creds['together_key']:
38
  st.error("Together AI API key not found. Please configure it in the secrets.")
39
  return None
40
- return Together(api_key=creds['together_key'])
41
 
42
  # =============================================================================
43
- # DATABASE OPERATIONS
44
  # =============================================================================
45
- def make_api_request(url, headers, params=None):
46
- """Make API request with error handling."""
47
- try:
48
- response = requests.get(url, headers=headers, params=params)
49
- response.raise_for_status()
50
- return response.json()
51
- except requests.exceptions.RequestException as e:
52
- st.error(f"API request failed: {str(e)}")
53
- return None
54
-
55
- @st.cache_data(ttl=300)
56
  def get_streaming_content():
57
- """Fetch streaming content from NocoDB with pagination."""
58
- creds = get_api_credentials()
59
 
60
- if not creds['api_token'] or not creds['content_endpoint']:
61
  st.error("NocoDB credentials not configured. Please set up your secrets.")
62
  return []
63
 
64
  headers = {
65
- "xc-token": creds['api_token'],
66
  "accept": "application/json"
67
  }
68
 
69
  all_content = []
70
  page = 1
71
- page_size = 1000
72
 
73
- while True:
74
- url = f"{NOCODB_URL}{creds['content_endpoint']}?limit={page_size}&offset={(page-1)*page_size}"
75
- data = make_api_request(url, headers)
76
-
77
- if not data:
78
- break
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
 
80
- current_page = data.get('list', [])
81
- if not current_page:
82
- break
 
 
 
 
 
 
 
 
 
83
 
84
- all_content.extend(current_page)
 
 
 
 
 
 
85
 
86
- if data.get('pageInfo', {}).get('isLastPage', True):
87
- break
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
 
89
- page += 1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
 
91
- return all_content
92
 
93
- def get_similar_content(content, n_recommendations=5):
94
- """Get pre-computed similar content from database."""
95
- creds = get_api_credentials()
 
96
 
97
- if not creds['api_token'] or not creds['similarity_endpoints']:
98
- st.error("NocoDB credentials not configured properly.")
99
- return []
100
 
101
- headers = {
102
- "xc-token": creds['api_token'],
103
- "accept": "application/json"
104
- }
105
 
106
- # Build query parameters
107
- query = {
108
- "where": f'(show_id,eq,{content.get("show_id", "")})~and(title,eq,{content.get("title", "")})'
109
- }
 
110
 
111
- # Search through similarity endpoints
112
- for endpoint in creds['similarity_endpoints']:
113
- url = f"{NOCODB_URL}{endpoint}"
114
- data = make_api_request(url, headers, query)
115
-
116
- if not data or not data.get('list'):
117
- continue
118
-
119
- # Process similar items
120
- for entry in data['list']:
121
- try:
122
- similar_items = json.loads(entry['similar_items'])
123
- similar_content = []
124
-
125
- # Get details for each similar item
126
- for item in similar_items[:n_recommendations]:
127
- content_query = {
128
- "where": f'(show_id,eq,{item.get("show_id", "")})'
129
- }
130
- content_url = f"{NOCODB_URL}{creds['content_endpoint']}"
131
- content_data = make_api_request(content_url, headers, content_query)
132
-
133
- if content_data and content_data.get('list'):
134
- content_dict = content_data['list'][0]
135
- content_dict['similarity'] = f"{item['similarity']:.2%}"
136
- similar_content.append(content_dict)
137
-
138
- return similar_content[:n_recommendations]
139
- except (json.JSONDecodeError, KeyError, IndexError):
140
- continue
141
 
142
- return []
143
 
144
- # =============================================================================
145
- # AI OPERATIONS
146
- # =============================================================================
147
  def get_ai_response(client, question, context, model_name):
148
- """Get AI response for user questions."""
149
  try:
150
- prompt = f"""You are a sophisticated butler and streaming content expert. Based on the current content data,
151
- please assist the user with their entertainment needs.
152
 
153
  Current Content Data Context:
154
  {context}
155
 
156
  User Question: {question}
157
 
158
- Please provide a refined and helpful response, keeping it concise yet informative."""
159
 
160
  response = client.chat.completions.create(
161
  model=model_name,
162
  messages=[
163
- {"role": "system", "content": "You are StreamButler, a sophisticated entertainment concierge with extensive knowledge of films and television."},
164
  {"role": "user", "content": prompt}
165
  ],
166
  max_tokens=300,
@@ -170,101 +241,165 @@ Please provide a refined and helpful response, keeping it concise yet informativ
170
  return response.choices[0].message.content
171
 
172
  except Exception as e:
173
- raise Exception(f"AI Service Error: {str(e)}")
174
 
175
- # =============================================================================
176
- # DATA PROCESSING
177
- # =============================================================================
178
- def filter_content(content_list, filters):
179
- """Apply filters to content list with error handling."""
 
 
 
 
 
 
 
 
 
 
180
  try:
181
- return [
182
- content for content in content_list
183
- if all([
184
- content.get('streaming_service') in filters['streaming_services'],
185
- (not filters['content_type'] or content.get('type') == filters['content_type']),
186
- (not filters['genres'] or all(g.strip().lower() in str(content.get('listed_in', '')).lower() for g in filters['genres'])),
187
- (not filters['ratings'] or content.get('rating', '').strip() in filters['ratings']),
188
- filters['year_range'][0] <= int(content.get('release_year', 0)) <= filters['year_range'][1],
189
- (not filters['director'] or any(name.strip().lower() in str(content.get('director', '')).lower() for name in filters['director'].split(','))),
190
- (not filters['cast'] or any(name.strip().lower() in str(content.get('cast', '')).lower() for name in filters['cast'].split(',')))
191
- ])
192
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
  except Exception as e:
194
- st.error(f"Error filtering content: {str(e)}")
195
  return []
196
 
197
- def create_content_context(content_list):
198
- """Create context string about current content."""
199
- if not content_list:
200
- return "No content matches the current filters."
 
 
 
 
 
 
201
 
202
- total = len(content_list)
203
- movies = sum(1 for c in content_list if c.get('type') == 'Movie')
204
- shows = sum(1 for c in content_list if c.get('type') == 'TV Show')
205
 
206
- genres = pd.Series([
207
- g.strip()
208
- for c in content_list[:20]
209
- for g in str(c.get('listed_in', '')).split(',')
210
- ]).value_counts()
211
 
212
- context = f"""Currently showing {total} titles ({movies} movies and {shows} TV shows). """
213
- if not genres.empty:
214
- context += f"Popular genres include: {', '.join(genres.head(5).index)}. "
 
215
 
216
- return context
217
-
218
- # =============================================================================
219
- # UI COMPONENTS
220
- # =============================================================================
221
- def render_filters_sidebar(all_content):
222
- """Render the filters sidebar."""
223
- st.sidebar.header("πŸ” Filter Content")
224
 
225
- # Extract filter options
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
226
  all_ratings = sorted(list(set(
227
  c.get('rating') for c in all_content
228
- if c and isinstance(c.get('rating'), str)
229
- and not c.get('rating').endswith('min')
230
- and c.get('rating').strip()
 
 
231
  )))
232
-
233
  all_genres = sorted(list(set(
234
  genre.strip()
235
  for c in all_content
236
- for genre in str(c.get('listed_in', '')).split(',')
237
  if genre.strip()
238
  )))
239
-
240
- all_streaming_services = sorted(list(set(
241
- c.get('streaming_service')
242
- for c in all_content
243
- if c.get('streaming_service')
244
- )))
245
 
246
  # Extract unique directors and cast members
247
- all_directors = sorted(list(set(
248
- name.strip()
249
- for c in all_content
250
- for name in str(c.get('director', '')).split(',')
251
- if name.strip()
252
- )))
253
-
254
- all_cast_members = sorted(list(set(
255
- name.strip()
256
- for c in all_content
257
- for name in str(c.get('cast', '')).split(',')
258
- if name.strip()
259
- )))
260
 
 
 
 
261
  with st.sidebar.form("filter_form"):
262
- # Streaming Services
263
  st.subheader("Streaming Services")
 
 
264
  selected_services = st.multiselect(
265
  "Select Your Streaming Services",
266
  options=all_streaming_services,
267
- default=all_streaming_services[:1],
268
  help="Select the streaming services you have access to",
269
  key="streaming_services"
270
  )
@@ -272,31 +407,31 @@ def render_filters_sidebar(all_content):
272
  if not selected_services:
273
  st.warning("Please select at least one streaming service")
274
 
275
- # Content Type
276
  st.subheader("Content Filters")
 
277
  content_type = st.selectbox(
278
  "Content Type",
279
  options=["All", "Movie", "TV Show"],
280
  index=0
281
  )
282
 
283
- # Genres
284
  selected_genres = st.multiselect(
285
  "Genres",
286
  options=all_genres,
287
  default=[]
288
  )
289
-
290
- # Optional Filters
291
  st.subheader("Optional Filters")
 
 
292
  selected_ratings = st.multiselect(
293
  "Ratings",
294
  options=all_ratings,
295
  default=[],
296
  help="Filter by content rating"
297
  )
298
-
299
- # Year range
300
  years = [int(c.get('release_year', 0)) for c in all_content if c.get('release_year')]
301
  min_year, max_year = min(years), max(years)
302
  year_range = st.slider(
@@ -307,14 +442,13 @@ def render_filters_sidebar(all_content):
307
  help="Filter by release year range"
308
  )
309
 
310
- # Duration range for movies
311
  movie_durations = [
312
  int(str(c.get('duration', '0 min')).split()[0])
313
  for c in all_content
314
  if c and c.get('type') == 'Movie' and 'min' in str(c.get('duration', ''))
315
  ]
316
 
317
- duration_range = (0, 1000)
318
  if movie_durations:
319
  min_duration = min(d for d in movie_durations if d > 0)
320
  max_duration = max(movie_durations)
@@ -325,8 +459,10 @@ def render_filters_sidebar(all_content):
325
  value=(min_duration, max_duration),
326
  help="This filter only applies to movies"
327
  )
328
-
329
- # Director and Cast filters
 
 
330
  selected_directors = st.multiselect(
331
  "Directors",
332
  options=all_directors,
@@ -334,7 +470,8 @@ def render_filters_sidebar(all_content):
334
  help="Select one or more directors (searchable)",
335
  placeholder="Start typing to search directors..."
336
  )
337
-
 
338
  selected_cast = st.multiselect(
339
  "Cast Members",
340
  options=all_cast_members,
@@ -343,294 +480,285 @@ def render_filters_sidebar(all_content):
343
  placeholder="Start typing to search cast members..."
344
  )
345
 
 
346
  apply_filters = st.form_submit_button("πŸ” Apply Filters", type="primary")
347
-
348
- return {
349
- 'streaming_services': selected_services,
350
- 'content_type': content_type if content_type != "All" else None,
351
- 'ratings': selected_ratings,
352
- 'genres': selected_genres,
353
- 'year_range': year_range,
354
- 'duration_range': duration_range,
355
- 'director': ','.join(selected_directors) if selected_directors else '',
356
- 'cast': ','.join(selected_cast) if selected_cast else '',
357
- 'apply_filters': apply_filters
358
- }
359
-
360
- def render_content_list(filtered_content, page, items_per_page=10):
361
- """Render the content list with pagination."""
362
- if not filtered_content:
363
- st.subheader("πŸ“‹ No Titles Found")
364
- return
365
-
366
- filtered_count = len(filtered_content)
367
- st.subheader(f"πŸ“‹ Found {filtered_count:,} Title{'s' if filtered_count != 1 else ''}")
368
 
369
- # Calculate page info
370
- total_pages = (filtered_count + items_per_page - 1) // items_per_page
371
- start_idx = (page - 1) * items_per_page
372
- end_idx = min(start_idx + items_per_page, filtered_count)
 
 
 
 
 
 
 
373
 
374
- st.write(f"Showing {start_idx + 1}-{end_idx} of {filtered_count:,} titles")
 
 
 
 
 
 
 
 
 
 
375
 
376
- # Display content items
377
- for i, content in enumerate(filtered_content[start_idx:end_idx], start=start_idx):
378
- with st.container():
379
- st.write(f"### {content.get('title', 'N/A')} ({content.get('release_year', 'N/A')})")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
380
 
381
- detail_col1, detail_col2 = st.columns(2)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
382
 
383
- with detail_col1:
384
- st.write(f"**πŸ“Ί Available on:** {content.get('streaming_service', 'N/A')}")
385
- st.write(f"**🎭 Type:** {content.get('type', 'N/A')}")
386
- st.write(f"**⭐ Rating:** {content.get('rating', 'N/A')}")
387
- st.write(f"**⏱️ Duration:** {content.get('duration', 'N/A')}")
388
 
389
- with detail_col2:
390
- st.write(f"**🎬 Genres:** {content.get('listed_in', 'N/A')}")
391
- cast = content.get('cast')
392
- cast_display = cast[:100] + "..." if cast and len(cast) > 100 else cast if cast else "N/A"
393
- st.write(f"**πŸ‘₯ Cast:** {cast_display}")
394
- st.write(f"**πŸ“ Director:** {content.get('director', 'N/A')}")
395
 
396
- st.write(f"**πŸ“– Description:**")
397
- st.write(content.get('description', 'N/A'))
 
398
 
399
- render_similar_content_section(content)
400
- st.write("---")
401
-
402
- render_pagination_controls(page, total_pages)
403
-
404
- def render_similar_content_section(content):
405
- """Render the similar content section for a content item."""
406
- similar_button = st.button(f"πŸ” Find Similar Content", key=f"similar_{content.get('show_id')}")
407
- if similar_button:
408
- with st.spinner("Finding similar content..."):
409
- similar_content = get_similar_content(content, n_recommendations=5)
 
 
410
 
411
- if similar_content:
412
- sim_tab1, sim_tab2 = st.tabs(["πŸ“Ί Similar Titles", "πŸ” Why These Recommendations"])
413
-
414
- with sim_tab1:
415
- for sim_content in similar_content:
416
- with st.container():
417
- col1, col2 = st.columns([3, 1])
418
- with col1:
419
- st.write(f"**{sim_content.get('title')}** ({sim_content.get('type')}, {sim_content.get('release_year')})")
420
- st.write(f"*Available on:* {sim_content.get('streaming_service')}")
421
- st.write(f"*Genres:* {sim_content.get('listed_in')}")
422
- st.write(f"*Cast:* {sim_content.get('cast')}")
423
- st.write(f"*Director:* {sim_content.get('director')}")
424
- st.write(f"*Description:* {sim_content.get('description')}")
425
- with col2:
426
- st.write(f"**Match:** {sim_content.get('similarity', 'N/A')}")
427
- st.write("---")
428
-
429
- with sim_tab2:
430
- st.write("**Why these recommendations?**")
431
- st.write("""
432
- These recommendations are based on multiple factors:
433
- - Genre and theme matching
434
- - Plot similarity analysis
435
- - Cast and director relationships
436
- - Release year proximity
437
-
438
- The percentage match indicates how similar each title is to your selection.
439
- """)
440
- else:
441
- st.info("No similar content found.")
442
-
443
- def render_pagination_controls(current_page, total_pages):
444
- """Render pagination controls."""
445
- st.write("---")
446
- page_cols = st.columns([1, 2, 1, 2, 1])
447
-
448
- with page_cols[0]:
449
- if st.button("← Previous", disabled=current_page == 1, use_container_width=True):
450
- st.session_state.current_page = max(1, current_page - 1)
451
- st.rerun()
452
 
453
- with page_cols[2]:
454
- page_input = st.number_input(
455
- f"Page (of {total_pages})",
456
- min_value=1,
457
- max_value=total_pages,
458
- value=current_page,
459
- key="page_number",
460
- help=f"Enter a page number between 1 and {total_pages}"
 
 
 
 
 
 
 
 
461
  )
462
- if page_input != current_page:
463
- st.session_state.current_page = page_input
464
- st.rerun()
465
-
466
- with page_cols[4]:
467
- if st.button("Next β†’", disabled=current_page == total_pages, use_container_width=True):
468
- st.session_state.current_page = min(total_pages, current_page + 1)
469
- st.rerun()
470
-
471
- def render_ai_chat_section(client, filtered_content):
472
- """Render the AI chat section."""
473
- st.subheader("🎩 Your Personal Butler")
474
- st.write("How may I be of assistance in finding your perfect entertainment today?")
475
-
476
- model_choice = st.selectbox(
477
- "Select Your Butler's Expertise Level:",
478
- [
479
- "google/gemma-2b-it",
480
- "google/gemma-2-27b-it",
481
- "mistralai/Mistral-7B-Instruct-v0.1",
482
- "NousResearch/Nous-Hermes-2-Mixtral-8x7B-DPO",
483
- "mistralai/Mixtral-8x7B-Instruct-v0.1"
484
- ],
485
- help="Select your butler's level of expertise in making recommendations"
486
- )
487
-
488
- with st.expander("πŸ’‘ How to Address Your Butler"):
489
- st.write("""
490
- Your butler understands requests like:
491
- β€’ "My good sir, I seek an action film that would also please my companion who favors comedies."
492
- β€’ "Would you be so kind as to suggest a family-friendly show in the spirit of Stranger Things, but less frightening?"
493
- β€’ "I've quite enjoyed The Crown and Downton Abbey. Might you recommend similar period dramas?"
494
- β€’ "The weather is rather gloomy today. Perhaps a charming romantic comedy or musical?"
495
- β€’ "I'm in search of enlightening documentaries about technology or artificial intelligence."
496
- β€’ "We're hosting a gathering this evening. What entertainment would you suggest for a group?"
497
- """)
498
-
499
- user_question = st.text_area(
500
- "How May I Assist You?",
501
- placeholder="Tell me your preferences, and I shall curate the perfect selection...",
502
- height=100
503
- )
504
-
505
- if st.button("🎩 Request Recommendations", type="primary"):
506
- if user_question:
507
- with st.spinner("Your butler is carefully selecting the perfect entertainment..."):
508
- context = create_content_context(filtered_content)
509
- try:
510
- ai_response = get_ai_response(client, user_question, context, model_choice)
511
- st.success("**🎩 Your Curated Selection:**")
512
- st.write(ai_response)
513
 
514
- with st.expander("🎩 Butler's Note"):
515
- st.write("""
516
- To access your selected entertainment:
517
- 1. Kindly select your preferred streaming services above
518
- 2. Locate your chosen title in the curated list
519
- 3. For similar recommendations, simply request "Find Similar Content"
520
 
521
- *Is there anything else I can assist you with?*
522
- """)
523
- except Exception as e:
524
- st.error("My sincerest apologies, but I seem to be unable to process your request at the moment. Might we try again?")
525
- else:
526
- st.warning("How may I be of assistance? Please share your entertainment preferences.")
527
-
528
- def render_footer_stats(all_content, filtered_content, selected_services):
529
- """Render footer statistics."""
 
 
 
 
 
 
 
 
 
 
 
 
530
  st.markdown("---")
531
- if not all_content:
532
- return
 
533
 
534
- total_items = len(all_content)
535
- filtered_items = len(filtered_content)
536
-
537
- st.markdown("### 🎩 Your Entertainment Library")
538
-
539
- stat_cols = st.columns(len(selected_services) + 3)
540
-
541
- with stat_cols[0]:
542
- st.metric("πŸ“š Complete Collection", f"{total_items:,}")
543
-
544
- with stat_cols[1]:
545
- st.metric("🎯 Curated Selection", f"{filtered_items:,}")
546
-
547
- with stat_cols[2]:
548
- movies = sum(1 for c in filtered_content if c.get('type') == 'Movie')
549
- shows = sum(1 for c in filtered_content if c.get('type') == 'TV Show')
550
- st.metric("🎬 Films / πŸ“Ί Series", f"{movies:,} / {shows:,}")
551
-
552
- service_icons = {
553
- "Netflix": "πŸ”΄",
554
- "Amazon Prime": "πŸ”΅",
555
- "Hulu": "🟒",
556
- "Disney+": "🟣"
557
- }
558
-
559
- for i, service in enumerate(selected_services, 3):
560
- if i < len(stat_cols):
561
- service_count = sum(1 for c in filtered_content if c.get('streaming_service') == service)
562
- icon = service_icons.get(service, "πŸ“Ί")
563
- with stat_cols[i]:
564
- st.metric(f"{icon} {service}", f"{service_count:,}")
565
-
566
- def main():
567
- """Main application entry point."""
568
- # Page config
569
- st.set_page_config(
570
- page_title="StreamButler - Your Personal Streaming Concierge",
571
- page_icon="🎩",
572
- layout="wide"
573
- )
574
-
575
- # Header
576
- st.title("🎩 StreamButler")
577
- st.write("*At your service! Allow me to curate the perfect streaming entertainment for you.*")
578
-
579
- # Check API credentials
580
- creds = get_api_credentials()
581
- if not creds['together_key']:
582
- st.error("⚠️ Together AI API key not configured!")
583
- st.info("Please set your TOGETHER_API_KEY in the Hugging Face Spaces secrets.")
584
- st.stop()
585
-
586
- if not creds['api_token'] or not creds['content_endpoint']:
587
- st.error("⚠️ NocoDB credentials not configured!")
588
- st.info("Please set NOCODB_API_TOKEN and NOCODB_CONTENT_ENDPOINT in the Hugging Face Spaces secrets.")
589
- st.stop()
590
-
591
- # Initialize AI client
592
- try:
593
- client = get_ai_client()
594
- if not client:
595
- st.stop()
596
- except Exception as e:
597
- st.error(f"Failed to initialize Together AI client: {e}")
598
- st.stop()
599
-
600
- # Load content
601
- with st.spinner("Loading streaming content..."):
602
- all_content = get_streaming_content()
603
-
604
- if not all_content:
605
- st.error("Could not load streaming content. Please check your NocoDB connection.")
606
- st.stop()
607
-
608
- # Render filters sidebar
609
- filters = render_filters_sidebar(all_content)
610
-
611
- # Apply filters
612
- if filters['apply_filters']:
613
- filtered_content = filter_content(all_content, filters)
614
- st.session_state.filtered_content = filtered_content
615
- else:
616
- if 'filtered_content' not in st.session_state:
617
- st.session_state.filtered_content = all_content
618
-
619
- # Initialize page number
620
- if 'current_page' not in st.session_state:
621
- st.session_state.current_page = 1
622
-
623
- # Main content layout
624
- col1, col2 = st.columns([2, 1])
625
-
626
- with col1:
627
- render_content_list(st.session_state.filtered_content, st.session_state.current_page)
628
-
629
- with col2:
630
- render_ai_chat_section(client, st.session_state.filtered_content)
631
-
632
- # Footer stats
633
- render_footer_stats(all_content, st.session_state.filtered_content, filters['streaming_services'])
634
 
635
  if __name__ == "__main__":
636
  main()
 
6
  import json
7
 
8
  # =============================================================================
9
+ # CONFIGURATION - Using Secrets Management
10
  # =============================================================================
11
+ NOCODB_URL = "https://mtoft20-potm.hf.space".strip() # Base URL, ensure no extra spaces
12
 
13
+ # Get sensitive data from Streamlit secrets or environment variables
 
 
14
  def get_api_credentials():
15
+ """Get API credentials from secrets or environment"""
16
+ try:
17
+ # Try Streamlit secrets first (for Hugging Face Spaces)
18
+ api_token = st.secrets.get("NOCODB_API_TOKEN", os.environ.get("NOCODB_API_TOKEN", "")).strip()
19
+ together_key = st.secrets.get("TOGETHER_API_KEY", os.environ.get("TOGETHER_API_KEY", "")).strip()
20
+
21
+ # Get endpoints for content and similarities
22
+ content_endpoint = st.secrets.get("NOCODB_CONTENT_ENDPOINT", os.environ.get("NOCODB_CONTENT_ENDPOINT", "")).strip()
23
+ similarity_endpoints = [endpoint.strip() for endpoint in
24
+ st.secrets.get("NOCODB_SIMILARITY_ENDPOINTS",
25
+ os.environ.get("NOCODB_SIMILARITY_ENDPOINTS", "")).split(",")
26
+ if endpoint.strip()]
27
+
28
+ return api_token, together_key, content_endpoint, similarity_endpoints
29
+ except:
30
+ # Fallback to environment variables
31
+ api_token = os.environ.get("NOCODB_API_TOKEN", "").strip()
32
+ together_key = os.environ.get("TOGETHER_API_KEY", "").strip()
33
+ content_endpoint = os.environ.get("NOCODB_CONTENT_ENDPOINT", "").strip()
34
+ similarity_endpoints = [endpoint.strip() for endpoint in
35
+ os.environ.get("NOCODB_SIMILARITY_ENDPOINTS", "").split(",")
36
+ if endpoint.strip()]
37
+
38
+ return api_token, together_key, content_endpoint, similarity_endpoints
39
 
40
+ # Initialize Together AI client
41
  @st.cache_resource
42
  def get_ai_client():
43
+ """Initialize Together AI client"""
44
+ _, together_key, _, _ = get_api_credentials()
45
+ if not together_key:
46
  st.error("Together AI API key not found. Please configure it in the secrets.")
47
  return None
48
+ return Together(api_key=together_key)
49
 
50
  # =============================================================================
51
+ # HELPER FUNCTIONS
52
  # =============================================================================
53
+ @st.cache_data(ttl=300) # Cache for 5 minutes
 
 
 
 
 
 
 
 
 
 
54
  def get_streaming_content():
55
+ """Fetch streaming content from NocoDB with pagination"""
56
+ api_token, _, content_endpoint, _ = get_api_credentials()
57
 
58
+ if not api_token or not content_endpoint:
59
  st.error("NocoDB credentials not configured. Please set up your secrets.")
60
  return []
61
 
62
  headers = {
63
+ "xc-token": api_token,
64
  "accept": "application/json"
65
  }
66
 
67
  all_content = []
68
  page = 1
69
+ page_size = 1000 # NocoDB default page size
70
 
71
+ try:
72
+ while True:
73
+ offset = (page - 1) * page_size
74
+ url = f"{NOCODB_URL.strip()}{content_endpoint.strip()}?limit={page_size}&offset={offset}"
75
+
76
+ response = requests.get(url, headers=headers)
77
+
78
+ if response.status_code == 200:
79
+ data = response.json()
80
+ current_page_data = data.get('list', [])
81
+
82
+ if not current_page_data: # No more data to fetch
83
+ break
84
+
85
+ all_content.extend(current_page_data)
86
+
87
+ # Check if this is the last page
88
+ page_info = data.get('pageInfo', {})
89
+ if page_info.get('isLastPage', True):
90
+ break
91
+
92
+ page += 1
93
+ else:
94
+ st.error(f"Failed to fetch data: {response.status_code}")
95
+ if not all_content: # Only return [] if we haven't fetched any data
96
+ return []
97
+ break # If we have some data, return what we've got
98
+
99
+ return all_content
100
 
101
+ except Exception as e:
102
+ st.error(f"Error connecting to database: {str(e)}")
103
+ st.write("Full error details:", e)
104
+ return []
105
+
106
+ def filter_content(content_list, filters):
107
+ """Apply filters to streaming content list"""
108
+ filtered = []
109
+
110
+ for content in content_list:
111
+ if not content or not isinstance(content, dict):
112
+ continue
113
 
114
+ matches_all_filters = True
115
+
116
+ # Streaming service filter
117
+ if filters['streaming_services']:
118
+ if content.get('streaming_service') not in filters['streaming_services']:
119
+ matches_all_filters = False
120
+ continue
121
 
122
+ # Type filter - only apply if not "All"
123
+ if filters['content_type']:
124
+ if content.get('type') != filters['content_type']:
125
+ matches_all_filters = False
126
+ continue
127
+
128
+ # Genre filter - check if ALL selected genres are in the content's genres
129
+ if filters['genres']:
130
+ content_genres = set(g.strip().lower() for g in str(content.get('listed_in', '')).split(','))
131
+ selected_genres = set(g.strip().lower() for g in filters['genres'])
132
+
133
+ if not selected_genres.issubset(content_genres):
134
+ matches_all_filters = False
135
+ continue
136
+
137
+ # Rating filter
138
+ if filters['ratings']:
139
+ rating = content.get('rating', '').strip()
140
+ # Only compare if rating is a valid string and not a duration
141
+ if not rating or not isinstance(rating, str) or rating.endswith('min'):
142
+ matches_all_filters = False
143
+ continue
144
+ if rating not in filters['ratings']:
145
+ matches_all_filters = False
146
+ continue
147
+
148
+ # Release year filter
149
+ try:
150
+ release_year = int(content.get('release_year', 0))
151
+ if release_year < filters['year_range'][0] or release_year > filters['year_range'][1]:
152
+ matches_all_filters = False
153
+ continue
154
+ except (ValueError, TypeError):
155
+ matches_all_filters = False
156
+ continue
157
 
158
+ # Duration filter (different handling for movies)
159
+ if filters['content_type'] == 'Movie':
160
+ duration = str(content.get('duration', ''))
161
+ if 'min' in duration:
162
+ try:
163
+ minutes = int(duration.split()[0])
164
+ if minutes < filters['duration_range'][0] or minutes > filters['duration_range'][1]:
165
+ matches_all_filters = False
166
+ continue
167
+ except (ValueError, IndexError):
168
+ matches_all_filters = False
169
+ continue
170
+
171
+ # Director filter (optional)
172
+ if filters['director']:
173
+ director = str(content.get('director', '')).lower()
174
+ if not any(name.strip().lower() in director for name in filters['director'].split(',')):
175
+ matches_all_filters = False
176
+ continue
177
+
178
+ # Cast filter (optional)
179
+ if filters['cast']:
180
+ cast = str(content.get('cast', '')).lower()
181
+ if not any(name.strip().lower() in cast for name in filters['cast'].split(',')):
182
+ matches_all_filters = False
183
+ continue
184
+
185
+ if matches_all_filters:
186
+ filtered.append(content)
187
 
188
+ return filtered
189
 
190
+ def create_content_context(content_list):
191
+ """Create context string about current content for AI"""
192
+ if not content_list:
193
+ return "No content matches the current filters."
194
 
195
+ total = len(content_list)
196
+ movies = sum(1 for c in content_list if c.get('type') == 'Movie')
197
+ shows = sum(1 for c in content_list if c.get('type') == 'TV Show')
198
 
199
+ context = f"""Currently showing {total} titles ({movies} movies and {shows} TV shows). """
 
 
 
200
 
201
+ # Add some genre info
202
+ all_genres = []
203
+ for content in content_list[:20]: # Sample from first 20 items
204
+ genres = content.get('listed_in', '').split(', ')
205
+ all_genres.extend(genres)
206
 
207
+ if all_genres:
208
+ genre_counts = pd.Series(all_genres).value_counts()
209
+ top_genres = genre_counts.head(5).index.tolist()
210
+ context += f"Top genres include: {', '.join(top_genres)}. "
211
+
212
+ # Add year range
213
+ years = [int(c.get('release_year', 0)) for c in content_list if c.get('release_year')]
214
+ if years:
215
+ context += f"Release years range from {min(years)} to {max(years)}."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
 
217
+ return context
218
 
 
 
 
219
  def get_ai_response(client, question, context, model_name):
220
+ """Get response from Together AI"""
221
  try:
222
+ prompt = f"""You are a helpful streaming content expert. Based on the current content data, please answer the user's question accurately and helpfully.
 
223
 
224
  Current Content Data Context:
225
  {context}
226
 
227
  User Question: {question}
228
 
229
+ Please provide a helpful, accurate response based on the data provided. Keep your answer concise but informative."""
230
 
231
  response = client.chat.completions.create(
232
  model=model_name,
233
  messages=[
234
+ {"role": "system", "content": "You are a helpful content expert with deep knowledge of movies and TV shows."},
235
  {"role": "user", "content": prompt}
236
  ],
237
  max_tokens=300,
 
241
  return response.choices[0].message.content
242
 
243
  except Exception as e:
244
+ raise Exception(f"Together AI Error: {str(e)}")
245
 
246
+ def extract_unique_names(content_list, field):
247
+ """Extract unique names from a comma-separated field in content list"""
248
+ unique_names = set()
249
+ for content in content_list:
250
+ names = content.get(field, '')
251
+ if names:
252
+ # Split by comma and clean each name
253
+ for name in names.split(','):
254
+ cleaned_name = name.strip()
255
+ if cleaned_name: # Only add non-empty names
256
+ unique_names.add(cleaned_name)
257
+ return sorted(list(unique_names))
258
+
259
+ def get_similar_content(content, n_recommendations=5):
260
+ """Get pre-computed similar content from database"""
261
  try:
262
+ # Get database credentials
263
+ api_token, _, content_endpoint, similarity_endpoints = get_api_credentials()
264
+
265
+ if not api_token or not similarity_endpoints:
266
+ st.error("NocoDB credentials not configured properly.")
267
+ return []
268
+
269
+ headers = {
270
+ "xc-token": api_token,
271
+ "accept": "application/json"
272
+ }
273
+
274
+ title = content.get('title', '')
275
+ show_id = content.get('show_id', '')
276
+
277
+ # Try finding by both show_id and title
278
+ query = f'where=(show_id,eq,{show_id})~and(title,eq,{title})'
279
+ params = {
280
+ "where": query
281
+ }
282
+
283
+ for endpoint in similarity_endpoints:
284
+ if not endpoint.strip(): # Skip empty endpoints
285
+ continue
286
+
287
+ try:
288
+ url = f"{NOCODB_URL.strip()}{endpoint.strip()}"
289
+ response = requests.get(url, headers=headers, params=params)
290
+
291
+ if response.status_code == 200:
292
+ data = response.json()
293
+ if data.get('list'):
294
+ for entry in data['list']:
295
+ try:
296
+ similar_items = json.loads(entry['similar_items'])
297
+
298
+ # Get full content details for each similar item
299
+ similar_content = []
300
+ for item in similar_items[:n_recommendations]:
301
+ show_id = item.get('show_id', '')
302
+ query = f'where=(show_id,eq,{show_id})'
303
+ content_params = {
304
+ "where": query
305
+ }
306
+ content_url = f"{NOCODB_URL.strip()}{content_endpoint.strip()}"
307
+ content_response = requests.get(content_url, headers=headers, params=content_params)
308
+
309
+ if content_response.status_code == 200:
310
+ content_data = content_response.json()
311
+ if content_data and len(content_data.get('list', [])) > 0:
312
+ content_dict = content_data['list'][0]
313
+ content_dict['similarity'] = f"{item['similarity']:.2%}"
314
+ similar_content.append(content_dict)
315
+
316
+ return similar_content[:n_recommendations]
317
+ except Exception as parse_error:
318
+ continue
319
+ except Exception as e:
320
+ continue
321
+
322
+ return []
323
  except Exception as e:
 
324
  return []
325
 
326
+ # =============================================================================
327
+ # MAIN APP
328
+ # =============================================================================
329
+ def main():
330
+ # Page config
331
+ st.set_page_config(
332
+ page_title="StreamButler - Your Personal Streaming Concierge",
333
+ page_icon="🎩",
334
+ layout="wide"
335
+ )
336
 
337
+ # Header with butler theme
338
+ st.title("🎩 StreamButler")
339
+ st.write("*At your service! Allow me to curate the perfect streaming entertainment for you.*")
340
 
341
+ # Check API credentials
342
+ api_token, together_key, content_endpoint, similarity_endpoints = get_api_credentials()
 
 
 
343
 
344
+ if not together_key:
345
+ st.error("⚠️ Together AI API key not configured!")
346
+ st.info("Please set your TOGETHER_API_KEY in the Hugging Face Spaces secrets.")
347
+ st.stop()
348
 
349
+ if not api_token or not content_endpoint:
350
+ st.error("⚠️ NocoDB credentials not configured!")
351
+ st.info("Please set NOCODB_API_TOKEN and NOCODB_CONTENT_ENDPOINT in the Hugging Face Spaces secrets.")
352
+ st.stop()
 
 
 
 
353
 
354
+ # Initialize AI client
355
+ try:
356
+ client = get_ai_client()
357
+ if not client:
358
+ st.stop()
359
+ except Exception as e:
360
+ st.error(f"Failed to initialize Together AI client: {e}")
361
+ st.stop()
362
+
363
+ # Load all content first
364
+ with st.spinner("Loading streaming content..."):
365
+ all_content = get_streaming_content()
366
+
367
+ if not all_content:
368
+ st.error("Could not load streaming content. Please check your NocoDB connection.")
369
+ st.stop()
370
+
371
+ # Extract unique values for filters
372
  all_ratings = sorted(list(set(
373
  c.get('rating') for c in all_content
374
+ if c and isinstance(c, dict)
375
+ and c.get('rating')
376
+ and isinstance(c.get('rating'), str)
377
+ and not c.get('rating').endswith('min') # Exclude duration values
378
+ and c.get('rating').strip() # Exclude empty strings
379
  )))
 
380
  all_genres = sorted(list(set(
381
  genre.strip()
382
  for c in all_content
383
+ for genre in c.get('listed_in', '').split(',')
384
  if genre.strip()
385
  )))
386
+ all_streaming_services = sorted(list(set([c.get('streaming_service') for c in all_content if c.get('streaming_service')])))
 
 
 
 
 
387
 
388
  # Extract unique directors and cast members
389
+ all_directors = extract_unique_names(all_content, 'director')
390
+ all_cast_members = extract_unique_names(all_content, 'cast')
 
 
 
 
 
 
 
 
 
 
 
391
 
392
+ # Sidebar filters
393
+ st.sidebar.header("πŸ” Filter Content")
394
+
395
  with st.sidebar.form("filter_form"):
 
396
  st.subheader("Streaming Services")
397
+
398
+ # Streaming service selection (required)
399
  selected_services = st.multiselect(
400
  "Select Your Streaming Services",
401
  options=all_streaming_services,
402
+ default=all_streaming_services[:1], # Default to first service
403
  help="Select the streaming services you have access to",
404
  key="streaming_services"
405
  )
 
407
  if not selected_services:
408
  st.warning("Please select at least one streaming service")
409
 
 
410
  st.subheader("Content Filters")
411
+
412
  content_type = st.selectbox(
413
  "Content Type",
414
  options=["All", "Movie", "TV Show"],
415
  index=0
416
  )
417
 
 
418
  selected_genres = st.multiselect(
419
  "Genres",
420
  options=all_genres,
421
  default=[]
422
  )
423
+
 
424
  st.subheader("Optional Filters")
425
+
426
+ # Rating filter
427
  selected_ratings = st.multiselect(
428
  "Ratings",
429
  options=all_ratings,
430
  default=[],
431
  help="Filter by content rating"
432
  )
433
+
434
+ # Year range slider
435
  years = [int(c.get('release_year', 0)) for c in all_content if c.get('release_year')]
436
  min_year, max_year = min(years), max(years)
437
  year_range = st.slider(
 
442
  help="Filter by release year range"
443
  )
444
 
445
+ # Duration range slider (for movies only)
446
  movie_durations = [
447
  int(str(c.get('duration', '0 min')).split()[0])
448
  for c in all_content
449
  if c and c.get('type') == 'Movie' and 'min' in str(c.get('duration', ''))
450
  ]
451
 
 
452
  if movie_durations:
453
  min_duration = min(d for d in movie_durations if d > 0)
454
  max_duration = max(movie_durations)
 
459
  value=(min_duration, max_duration),
460
  help="This filter only applies to movies"
461
  )
462
+ else:
463
+ duration_range = (0, 1000) # Fallback values
464
+
465
+ # Director filter with autocomplete
466
  selected_directors = st.multiselect(
467
  "Directors",
468
  options=all_directors,
 
470
  help="Select one or more directors (searchable)",
471
  placeholder="Start typing to search directors..."
472
  )
473
+
474
+ # Cast filter with autocomplete
475
  selected_cast = st.multiselect(
476
  "Cast Members",
477
  options=all_cast_members,
 
480
  placeholder="Start typing to search cast members..."
481
  )
482
 
483
+ # Submit button
484
  apply_filters = st.form_submit_button("πŸ” Apply Filters", type="primary")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
485
 
486
+ # Create filter dictionary
487
+ filters = {
488
+ 'streaming_services': selected_services,
489
+ 'content_type': content_type if content_type != "All" else None,
490
+ 'ratings': selected_ratings,
491
+ 'genres': selected_genres,
492
+ 'year_range': year_range,
493
+ 'duration_range': duration_range,
494
+ 'director': ','.join(selected_directors) if selected_directors else '',
495
+ 'cast': ','.join(selected_cast) if selected_cast else ''
496
+ }
497
 
498
+ # Only apply filters when the button is clicked
499
+ if apply_filters:
500
+ filtered_content = filter_content(all_content, filters)
501
+ st.session_state.filtered_content = filtered_content
502
+ else:
503
+ # Initialize filtered content if not exists
504
+ if 'filtered_content' not in st.session_state:
505
+ st.session_state.filtered_content = all_content
506
+
507
+ # Main content area
508
+ col1, col2 = st.columns([2, 1])
509
 
510
+ with col1:
511
+ # Content listings
512
+ filtered_count = len(st.session_state.filtered_content)
513
+ if filtered_count == 0:
514
+ st.subheader("πŸ“‹ No Titles Found")
515
+ else:
516
+ # Header with count and page info
517
+ st.subheader(f"πŸ“‹ Found {filtered_count:,} Title{'s' if filtered_count != 1 else ''}")
518
+
519
+ if st.session_state.filtered_content:
520
+ # Active Filters section with better formatting
521
+ if any([filters['content_type'], filters['genres'], filters['ratings'],
522
+ filters['director'], filters['cast']]):
523
+ with st.expander("πŸ” Active Filters", expanded=True):
524
+ filter_cols = st.columns(2)
525
+ with filter_cols[0]:
526
+ if filters['content_type']:
527
+ st.write(f"**Type:** {filters['content_type']}")
528
+ if filters['genres']:
529
+ st.write(f"**Genres:** {', '.join(filters['genres'])}")
530
+ if filters['ratings']:
531
+ st.write(f"**Ratings:** {', '.join(filters['ratings'])}")
532
+ with filter_cols[1]:
533
+ if filters['director']:
534
+ st.write(f"**Director:** {filters['director']}")
535
+ if filters['cast']:
536
+ st.write(f"**Cast:** {filters['cast']}")
537
+ st.write("---")
538
+
539
+ # Pagination setup
540
+ items_per_page = 10
541
+ total_pages = (filtered_count + items_per_page - 1) // items_per_page
542
+
543
+ # Initialize page number in session state if not exists
544
+ if 'current_page' not in st.session_state:
545
+ st.session_state.current_page = 1
546
+
547
+ # Calculate slice indices for current page
548
+ start_idx = (st.session_state.current_page - 1) * items_per_page
549
+ end_idx = min(start_idx + items_per_page, filtered_count)
550
+
551
+ # Display current range info
552
+ st.write(f"Showing {start_idx + 1}-{end_idx} of {filtered_count:,} titles")
553
 
554
+ # Show items for current page
555
+ for i, content in enumerate(st.session_state.filtered_content[start_idx:end_idx], start=start_idx):
556
+ with st.container():
557
+ st.write(f"### {content.get('title', 'N/A')} ({content.get('release_year', 'N/A')})")
558
+
559
+ # Content details in columns
560
+ detail_col1, detail_col2 = st.columns(2)
561
+
562
+ with detail_col1:
563
+ st.write(f"**πŸ“Ί Available on:** {content.get('streaming_service', 'N/A')}")
564
+ st.write(f"**🎭 Type:** {content.get('type', 'N/A')}")
565
+ st.write(f"**⭐ Rating:** {content.get('rating', 'N/A')}")
566
+ st.write(f"**⏱️ Duration:** {content.get('duration', 'N/A')}")
567
+
568
+ with detail_col2:
569
+ st.write(f"**🎬 Genres:** {content.get('listed_in', 'N/A')}")
570
+ cast = content.get('cast')
571
+ cast_display = cast[:100] + "..." if cast and len(cast) > 100 else cast if cast else "N/A"
572
+ st.write(f"**πŸ‘₯ Cast:** {cast_display}")
573
+ st.write(f"**πŸ“ Director:** {content.get('director', 'N/A')}")
574
+
575
+ # Description
576
+ st.write(f"**πŸ“– Description:**")
577
+ st.write(content.get('description', 'N/A'))
578
+
579
+ # Add Find Similar button with loading state
580
+ similar_button = st.button(f"πŸ” Find Similar Content", key=f"similar_{i}")
581
+ if similar_button:
582
+ with st.spinner("Finding similar content..."):
583
+ similar_content = get_similar_content(content, n_recommendations=5)
584
+
585
+ if similar_content:
586
+ # Create tabs for different aspects of recommendations
587
+ sim_tab1, sim_tab2 = st.tabs(["πŸ“Ί Similar Titles", "πŸ” Why These Recommendations"])
588
+
589
+ with sim_tab1:
590
+ for sim_content in similar_content[:5]: # Show top 5 similar items
591
+ with st.container():
592
+ col1, col2 = st.columns([3, 1])
593
+ with col1:
594
+ st.write(f"**{sim_content.get('title')}** ({sim_content.get('type')}, {sim_content.get('release_year')})")
595
+ st.write(f"*Available on:* {sim_content.get('streaming_service')}")
596
+ st.write(f"*Genres:* {sim_content.get('listed_in')}")
597
+ st.write(f"*Cast:* {sim_content.get('cast')}")
598
+ st.write(f"*Director:* {sim_content.get('director')}")
599
+ st.write(f"*Description:* {sim_content.get('description')}")
600
+ with col2:
601
+ st.write(f"**Match:** {sim_content.get('similarity', 'N/A')}")
602
+ st.write("---")
603
+
604
+ with sim_tab2:
605
+ st.write("**Why these recommendations?**")
606
+ st.write("""
607
+ These recommendations are based on multiple factors:
608
+ - Genre and theme matching
609
+ - Plot similarity analysis
610
+ - Cast and director relationships
611
+ - Release year proximity
612
+
613
+ The percentage match indicates how similar each title is to your selection.
614
+ """)
615
+ else:
616
+ st.info("No similar content found.")
617
+ st.write("---")
618
 
619
+ # Bottom pagination controls with better layout
620
+ st.write("---")
621
+ page_cols = st.columns([1, 2, 1, 2, 1])
 
 
622
 
623
+ # Previous button
624
+ with page_cols[0]:
625
+ if st.button("← Previous", disabled=st.session_state.current_page == 1, use_container_width=True):
626
+ st.session_state.current_page -= 1
627
+ st.rerun()
 
628
 
629
+ # Spacer
630
+ with page_cols[1]:
631
+ st.write("")
632
 
633
+ # Page input
634
+ with page_cols[2]:
635
+ page_input = st.number_input(
636
+ f"Page (of {total_pages})",
637
+ min_value=1,
638
+ max_value=total_pages,
639
+ value=st.session_state.current_page,
640
+ key="page_number",
641
+ help=f"Enter a page number between 1 and {total_pages}"
642
+ )
643
+ if page_input != st.session_state.current_page:
644
+ st.session_state.current_page = page_input
645
+ st.rerun()
646
 
647
+ # Spacer
648
+ with page_cols[3]:
649
+ st.write("")
650
+
651
+ # Next button
652
+ with page_cols[4]:
653
+ if st.button("Next β†’", disabled=st.session_state.current_page == total_pages, use_container_width=True):
654
+ st.session_state.current_page += 1
655
+ st.rerun()
656
+ else:
657
+ st.info("No content matches your current filters. Try adjusting the criteria.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
658
 
659
+ with col2:
660
+ # AI Chat Section
661
+ st.subheader("🎩 Your Personal Butler")
662
+ st.write("How may I be of assistance in finding your perfect entertainment today?")
663
+
664
+ # Model selection for Together AI
665
+ model_choice = st.selectbox(
666
+ "Select Your Butler's Expertise Level:",
667
+ [
668
+ "google/gemma-2b-it",
669
+ "google/gemma-2-27b-it",
670
+ "mistralai/Mistral-7B-Instruct-v0.1",
671
+ "NousResearch/Nous-Hermes-2-Mixtral-8x7B-DPO",
672
+ "mistralai/Mixtral-8x7B-Instruct-v0.1"
673
+ ],
674
+ help="Select your butler's level of expertise in making recommendations"
675
  )
676
+
677
+ # Example questions
678
+ with st.expander("πŸ’‘ How to Address Your Butler"):
679
+ st.write("""
680
+ Your butler understands requests like:
681
+ β€’ "My good sir, I seek an action film that would also please my companion who favors comedies."
682
+ β€’ "Would you be so kind as to suggest a family-friendly show in the spirit of Stranger Things, but less frightening?"
683
+ β€’ "I've quite enjoyed The Crown and Downton Abbey. Might you recommend similar period dramas?"
684
+ β€’ "The weather is rather gloomy today. Perhaps a charming romantic comedy or musical?"
685
+ β€’ "I'm in search of enlightening documentaries about technology or artificial intelligence."
686
+ β€’ "We're hosting a gathering this evening. What entertainment would you suggest for a group?"
687
+ """)
688
+
689
+ user_question = st.text_area(
690
+ "How May I Assist You?",
691
+ placeholder="Tell me your preferences, and I shall curate the perfect selection...",
692
+ height=100
693
+ )
694
+
695
+ if st.button("🎩 Request Recommendations", type="primary"):
696
+ if user_question:
697
+ with st.spinner("Your butler is carefully selecting the perfect entertainment..."):
698
+ # Create context from current filtered data
699
+ context = create_content_context(st.session_state.filtered_content)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
700
 
701
+ try:
702
+ # Get AI response
703
+ ai_response = get_ai_response(client, user_question, context, model_choice)
 
 
 
704
 
705
+ st.success("**🎩 Your Curated Selection:**")
706
+ st.write(ai_response)
707
+
708
+ # Show streaming availability
709
+ with st.expander("🎩 Butler's Note"):
710
+ st.write("""
711
+ To access your selected entertainment:
712
+ 1. Kindly select your preferred streaming services above
713
+ 2. Locate your chosen title in the curated list
714
+ 3. For similar recommendations, simply request "Find Similar Content"
715
+
716
+ *Is there anything else I can assist you with?*
717
+ """)
718
+
719
+ except Exception as e:
720
+ st.error("My sincerest apologies, but I seem to be unable to process your request at the moment. Might we try again?")
721
+
722
+ else:
723
+ st.warning("How may I be of assistance? Please share your entertainment preferences.")
724
+
725
+ # Footer stats with butler theme
726
  st.markdown("---")
727
+ if all_content:
728
+ total_items = len(all_content)
729
+ filtered_items = len(st.session_state.filtered_content)
730
 
731
+ st.markdown("### 🎩 Your Entertainment Library")
732
+
733
+ # Create columns for stats with better spacing
734
+ stat_cols = st.columns(len(selected_services) + 3)
735
+
736
+ # Basic stats with improved formatting
737
+ with stat_cols[0]:
738
+ st.metric("πŸ“š Complete Collection", f"{total_items:,}")
739
+
740
+ with stat_cols[1]:
741
+ st.metric("🎯 Curated Selection", f"{filtered_items:,}")
742
+
743
+ with stat_cols[2]:
744
+ movies = sum(1 for c in st.session_state.filtered_content if c.get('type') == 'Movie')
745
+ shows = sum(1 for c in st.session_state.filtered_content if c.get('type') == 'TV Show')
746
+ st.metric("🎬 Films / πŸ“Ί Series", f"{movies:,} / {shows:,}")
747
+
748
+ # Streaming service breakdown with icons
749
+ service_icons = {
750
+ "Netflix": "πŸ”΄",
751
+ "Amazon Prime": "πŸ”΅",
752
+ "Hulu": "🟒",
753
+ "Disney+": "🟣"
754
+ }
755
+
756
+ for i, service in enumerate(selected_services, 3):
757
+ if i < len(stat_cols):
758
+ service_count = sum(1 for c in st.session_state.filtered_content if c.get('streaming_service') == service)
759
+ icon = service_icons.get(service, "πŸ“Ί")
760
+ with stat_cols[i]:
761
+ st.metric(f"{icon} {service}", f"{service_count:,}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
762
 
763
  if __name__ == "__main__":
764
  main()