NimaKL commited on
Commit
21b2cd1
Β·
verified Β·
1 Parent(s): e9cabf8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +450 -440
app.py CHANGED
@@ -1,441 +1,451 @@
1
- import gradio as gr
2
- from neo4j import GraphDatabase
3
- import logging
4
- from typing import List, Dict, Tuple
5
- import pandas as pd
6
- from datetime import datetime
7
- import os
8
-
9
- # Set up logging
10
- logging.basicConfig(level=logging.INFO)
11
- logger = logging.getLogger(__name__)
12
-
13
- # Get Neo4j credentials from environment variables
14
- NEO4J_URL = os.environ['NEO4J_URL']
15
- NEO4J_USER = os.environ['NEO4J_USER']
16
- NEO4J_PASSWORD = os.environ['NEO4J_PASSWORD']
17
-
18
- def format_neo4j_datetime(dt) -> str:
19
- """Convert Neo4j datetime to string format."""
20
- if dt is None:
21
- return 'Unknown date'
22
- try:
23
- # Convert Neo4j datetime to Python datetime
24
- if hasattr(dt, 'to_native'):
25
- dt = dt.to_native()
26
- return dt.strftime('%Y-%m-%d')
27
- except Exception as e:
28
- logger.warning(f"Error formatting datetime: {e}")
29
- return 'Unknown date'
30
-
31
- def format_interest_list(interests: set, max_items: int = 10) -> str:
32
- """Format a list of interests with a limit and show count if truncated."""
33
- if not interests:
34
- return 'None'
35
- sorted_interests = sorted(interests)
36
- if len(sorted_interests) <= max_items:
37
- return ', '.join(sorted_interests)
38
- return f"{', '.join(sorted_interests[:max_items])} (+{len(sorted_interests) - max_items} more)"
39
-
40
- class QuestionRecommender:
41
- def __init__(self):
42
- self.driver = GraphDatabase.driver(
43
- NEO4J_URL,
44
- auth=(NEO4J_USER, NEO4J_PASSWORD)
45
- )
46
-
47
- def close(self):
48
- self.driver.close()
49
-
50
- def get_all_users(self) -> List[str]:
51
- """Get list of all users."""
52
- with self.driver.session() as session:
53
- result = session.run("""
54
- MATCH (u:User)
55
- RETURN DISTINCT u.name as username
56
- ORDER BY username
57
- """)
58
- return [record["username"] for record in result if record["username"]]
59
-
60
- def get_user_interests(self, username: str) -> Dict[str, set]:
61
- """Get keywords and topics a user is interested in."""
62
- with self.driver.session() as session:
63
- # Get keywords the user is interested in
64
- keyword_result = session.run("""
65
- MATCH (u:User {name: $username})-[:INTERESTED_IN_KEYWORD]->(k:Keyword)
66
- RETURN DISTINCT k.keyword as keyword
67
- """, username=username)
68
- keywords = {str(record["keyword"]) for record in keyword_result if record["keyword"]}
69
-
70
- # Get topics the user is interested in
71
- topic_result = session.run("""
72
- MATCH (u:User {name: $username})-[:INTERESTED_IN_TOPIC]->(t:Topic)
73
- RETURN DISTINCT t.topic as topic
74
- """, username=username)
75
- topics = {str(record["topic"]) for record in topic_result if record["topic"]}
76
-
77
- return {"keywords": keywords or set(), "topics": topics or set()}
78
-
79
- def find_common_questions(self, user1: str, user2: str, max_questions: int = 5) -> List[Dict]:
80
- """Find questions to recommend based on common interests."""
81
- with self.driver.session() as session:
82
- # First try to find questions with common keywords
83
- keyword_questions = session.run("""
84
- // Find keywords that both users are interested in
85
- MATCH (u1:User {name: $user1})-[:INTERESTED_IN_KEYWORD]->(k:Keyword)<-[:INTERESTED_IN_KEYWORD]-(u2:User {name: $user2})
86
-
87
- // Find questions with these common keywords
88
- WITH DISTINCT k
89
- MATCH (q:Question)-[:HAS_KEYWORD]->(k)
90
- WHERE q.author <> $user1 AND q.author <> $user2
91
-
92
- // Return questions with their details
93
- RETURN DISTINCT
94
- q.title as title,
95
- q.body as body,
96
- q.created_utc_ts as created_date,
97
- q.author as author,
98
- collect(k.keyword) as keywords
99
- ORDER BY q.created_utc_ts DESC
100
- LIMIT $limit
101
- """, user1=user1, user2=user2, limit=max_questions)
102
-
103
- questions = [dict(record) for record in keyword_questions]
104
-
105
- # If no questions found with common keywords, try topics
106
- if not questions:
107
- topic_questions = session.run("""
108
- // Find topics that both users are interested in
109
- MATCH (u1:User {name: $user1})-[:INTERESTED_IN_TOPIC]->(t:Topic)<-[:INTERESTED_IN_TOPIC]-(u2:User {name: $user2})
110
-
111
- // Find questions with these common topics
112
- WITH DISTINCT t
113
- MATCH (q:Question)-[:HAS_TOPIC]->(t)
114
- WHERE q.author <> $user1 AND q.author <> $user2
115
-
116
- // Return questions with their details
117
- RETURN DISTINCT
118
- q.title as title,
119
- q.body as body,
120
- q.created_utc_ts as created_date,
121
- q.author as author,
122
- collect(t.topic) as topics
123
- ORDER BY q.created_utc_ts DESC
124
- LIMIT $limit
125
- """, user1=user1, user2=user2, limit=max_questions)
126
-
127
- questions = [dict(record) for record in topic_questions]
128
-
129
- return questions
130
-
131
- def format_question(q: Dict) -> str:
132
- """Format a question for display."""
133
- created_date = format_neo4j_datetime(q.get('created_date'))
134
- keywords_or_topics = q.get('keywords', q.get('topics', []))
135
- interests = format_interest_list(set(k for k in keywords_or_topics if k is not None), max_items=5)
136
- author = q.get('author', 'Unknown author')
137
- title = q.get('title', 'Untitled')
138
- body = q.get('body', '')
139
-
140
- # Only show body section if there's actual content
141
- body_html = f"""
142
- <div class="question-body">
143
- {body[:300] + "... [truncated]" if body and len(body) > 300 else body}
144
- </div>
145
- """ if body else ""
146
-
147
- return f"""
148
- <div class="question-card">
149
- <h3>{title}</h3>
150
- <div class="question-meta">
151
- Posted by <span class="author">{author}</span> on <span class="date">{created_date}</span>
152
- </div>
153
- <div class="interests">
154
- Common Interests: <span class="interest-tags">{interests}</span>
155
- </div>{body_html}
156
- </div>
157
- """
158
-
159
- def recommend_questions(user1: str, user2: str) -> Tuple[str, str, str]:
160
- """Main function to get recommendations and user interests."""
161
- recommender = QuestionRecommender()
162
- try:
163
- # Get interests for both users
164
- user1_interests = recommender.get_user_interests(user1)
165
- user2_interests = recommender.get_user_interests(user2)
166
-
167
- # Find common interests
168
- common_keywords = user1_interests['keywords'] & user2_interests['keywords']
169
- common_topics = user1_interests['topics'] & user2_interests['topics']
170
-
171
- # Format interests summary
172
- interests_summary = f"""
173
- <div class="interests-summary">
174
- <div class="user-interests">
175
- <h3>{user1}'s Interests</h3>
176
- <div class="interest-section">
177
- <strong>Keywords:</strong> {format_interest_list(user1_interests['keywords'], max_items=8)}
178
- </div>
179
- <div class="interest-section">
180
- <strong>Topics:</strong> {format_interest_list(user1_interests['topics'], max_items=5)}
181
- </div>
182
- </div>
183
-
184
- <div class="user-interests">
185
- <h3>{user2}'s Interests</h3>
186
- <div class="interest-section">
187
- <strong>Keywords:</strong> {format_interest_list(user2_interests['keywords'], max_items=8)}
188
- </div>
189
- <div class="interest-section">
190
- <strong>Topics:</strong> {format_interest_list(user2_interests['topics'], max_items=5)}
191
- </div>
192
- </div>
193
-
194
- <div class="common-interests">
195
- <h3>Common Interests</h3>
196
- <div class="interest-section">
197
- <strong>Keywords:</strong> {format_interest_list(common_keywords, max_items=8)}
198
- </div>
199
- <div class="interest-section">
200
- <strong>Topics:</strong> {format_interest_list(common_topics, max_items=5)}
201
- </div>
202
- </div>
203
- </div>
204
- """
205
-
206
- # Get recommended questions
207
- questions = recommender.find_common_questions(user1, user2)
208
-
209
- if questions:
210
- questions_text = """<div class="questions-container">\n""" + \
211
- """\n""".join(format_question(q) for q in questions) + \
212
- """\n</div>"""
213
- recommendation_type = """<h2 class="recommendation-header">""" + \
214
- ("Recommendations Based on Common Keywords" if 'keywords' in questions[0]
215
- else "Recommendations Based on Common Topics") + \
216
- """</h2>"""
217
- else:
218
- questions_text = """<div class="no-questions">No questions found based on common interests.</div>"""
219
- recommendation_type = """<h2 class="recommendation-header">No Recommendations Available</h2>"""
220
-
221
- return interests_summary, recommendation_type, questions_text
222
-
223
- except Exception as e:
224
- logger.error(f"Error in recommend_questions: {str(e)}")
225
- return (
226
- """<div class="error">Error fetching user interests. Please try again.</div>""",
227
- """<h2 class="error-header">Error</h2>""",
228
- f"""<div class="error-message">An error occurred: {str(e)}</div>"""
229
- )
230
- finally:
231
- recommender.close()
232
-
233
- def loading_message() -> Tuple[str, str, str]:
234
- """Return loading message in proper HTML format."""
235
- loading_html = """
236
- <div class="loading-spinner">
237
- <div style="text-align: center;">
238
- <div style="border: 4px solid #60a5fa; border-top: 4px solid transparent; border-radius: 50%; width: 40px; height: 40px; animation: spin 1s linear infinite; margin: 20px auto;"></div>
239
- <div style="color: #60a5fa; margin-top: 10px;">Analyzing interests and finding recommendations...</div>
240
- </div>
241
- </div>
242
- """
243
- return loading_html, loading_html, loading_html
244
-
245
- # Update the custom CSS with dark theme compatible colors
246
- custom_css = """
247
- .gradio-container {
248
- max-width: 1200px !important;
249
- margin: auto !important;
250
- padding: 20px !important;
251
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif !important;
252
- }
253
-
254
- .interests-summary {
255
- background: rgba(30, 41, 59, 0.5);
256
- padding: 20px;
257
- border-radius: 10px;
258
- margin-bottom: 20px;
259
- border: 1px solid rgba(148, 163, 184, 0.1);
260
- }
261
-
262
- .user-interests, .common-interests {
263
- background: rgba(51, 65, 85, 0.5);
264
- padding: 15px;
265
- border-radius: 8px;
266
- margin: 10px 0;
267
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
268
- border: 1px solid rgba(148, 163, 184, 0.2);
269
- }
270
-
271
- .interest-section {
272
- margin: 10px 0;
273
- line-height: 1.5;
274
- color: #e2e8f0;
275
- }
276
-
277
- .question-card {
278
- background: rgba(51, 65, 85, 0.5);
279
- padding: 20px;
280
- border-radius: 8px;
281
- margin: 15px 0;
282
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
283
- border: 1px solid rgba(148, 163, 184, 0.2);
284
- transition: transform 0.2s ease;
285
- }
286
-
287
- .question-card:hover {
288
- transform: translateY(-2px);
289
- }
290
-
291
- .question-card h3 {
292
- color: #60a5fa;
293
- margin-top: 0;
294
- margin-bottom: 10px;
295
- font-size: 1.25rem;
296
- }
297
-
298
- .question-meta {
299
- font-size: 0.9em;
300
- color: #cbd5e1;
301
- margin-bottom: 10px;
302
- }
303
-
304
- .author {
305
- color: #93c5fd;
306
- font-weight: 500;
307
- }
308
-
309
- .date {
310
- color: #94a3b8;
311
- }
312
-
313
- .interests {
314
- margin: 10px 0;
315
- font-size: 0.9em;
316
- color: #cbd5e1;
317
- }
318
-
319
- .interest-tags {
320
- color: #93c5fd;
321
- font-weight: 500;
322
- }
323
-
324
- .question-body {
325
- line-height: 1.6;
326
- color: #e2e8f0;
327
- margin-top: 15px;
328
- white-space: pre-wrap;
329
- }
330
-
331
- .recommendation-header {
332
- color: #60a5fa;
333
- padding: 10px 0;
334
- margin: 20px 0;
335
- border-bottom: 2px solid #3b82f6;
336
- }
337
-
338
- .error {
339
- color: #fca5a5;
340
- padding: 15px;
341
- background: rgba(239, 68, 68, 0.2);
342
- border-radius: 8px;
343
- margin: 10px 0;
344
- border: 1px solid rgba(239, 68, 68, 0.3);
345
- }
346
-
347
- .error-header {
348
- color: #fca5a5;
349
- }
350
-
351
- .error-message {
352
- background: rgba(239, 68, 68, 0.2);
353
- padding: 15px;
354
- border-radius: 8px;
355
- color: #fca5a5;
356
- border: 1px solid rgba(239, 68, 68, 0.3);
357
- }
358
-
359
- .no-questions {
360
- padding: 20px;
361
- background: rgba(51, 65, 85, 0.5);
362
- border-radius: 8px;
363
- text-align: center;
364
- color: #94a3b8;
365
- border: 1px solid rgba(148, 163, 184, 0.2);
366
- }
367
-
368
- .loading-spinner {
369
- display: flex;
370
- justify-content: center;
371
- align-items: center;
372
- padding: 20px;
373
- color: #60a5fa;
374
- }
375
-
376
- h1, h2, h3 {
377
- color: #60a5fa !important;
378
- }
379
-
380
- strong {
381
- color: #93c5fd;
382
- }
383
-
384
- .user-interests h3, .common-interests h3 {
385
- color: #60a5fa;
386
- margin-top: 0;
387
- margin-bottom: 15px;
388
- font-size: 1.2rem;
389
- }
390
- """
391
-
392
- # Create Gradio interface
393
- recommender = QuestionRecommender()
394
- users = recommender.get_all_users()
395
- recommender.close()
396
-
397
- with gr.Blocks(title="Question Recommender", theme=gr.themes.Soft(), css=custom_css) as iface:
398
- gr.Markdown("""
399
- # 🀝 Question Recommender
400
- Find questions that two users might be interested in discussing together based on their common interests.
401
- """)
402
-
403
- with gr.Row(equal_height=True):
404
- with gr.Column(scale=1):
405
- user1_dropdown = gr.Dropdown(
406
- choices=users,
407
- label="πŸ‘€ First User",
408
- interactive=True
409
- )
410
- with gr.Column(scale=1):
411
- user2_dropdown = gr.Dropdown(
412
- choices=users,
413
- label="πŸ‘€ Second User",
414
- interactive=True
415
- )
416
-
417
- recommend_btn = gr.Button(
418
- "πŸ” Get Recommendations",
419
- variant="primary",
420
- size="lg"
421
- )
422
-
423
- with gr.Row():
424
- interests_output = gr.HTML(label="Common Interests")
425
-
426
- recommendation_type = gr.HTML()
427
- questions_output = gr.HTML()
428
-
429
- # Add loading state
430
- recommend_btn.click(
431
- fn=loading_message, # First show loading message
432
- outputs=[interests_output, recommendation_type, questions_output],
433
- queue=False # Don't queue this call
434
- ).then( # Then get the actual recommendations
435
- fn=recommend_questions,
436
- inputs=[user1_dropdown, user2_dropdown],
437
- outputs=[interests_output, recommendation_type, questions_output]
438
- )
439
-
440
- if __name__ == "__main__":
 
 
 
 
 
 
 
 
 
 
441
  iface.launch(share=True)
 
1
+ import gradio as gr
2
+ from neo4j import GraphDatabase
3
+ import logging
4
+ from typing import List, Dict, Tuple
5
+ import pandas as pd
6
+ from datetime import datetime
7
+ import os
8
+
9
+ # Set up logging
10
+ logging.basicConfig(level=logging.INFO)
11
+ logger = logging.getLogger(__name__)
12
+
13
+ # Get Neo4j credentials from environment variables
14
+ NEO4J_URL = os.environ['NEO4J_URL']
15
+ NEO4J_USER = os.environ['NEO4J_USER']
16
+ NEO4J_PASSWORD = os.environ['NEO4J_PASSWORD']
17
+
18
+ def format_neo4j_datetime(dt) -> str:
19
+ """Convert Neo4j datetime to string format."""
20
+ if dt is None:
21
+ return 'Unknown date'
22
+ try:
23
+ # Convert Neo4j datetime to Python datetime
24
+ if hasattr(dt, 'to_native'):
25
+ dt = dt.to_native()
26
+ return dt.strftime('%Y-%m-%d')
27
+ except Exception as e:
28
+ logger.warning(f"Error formatting datetime: {e}")
29
+ return 'Unknown date'
30
+
31
+ def format_interest_list(interests: set, max_items: int = 10) -> str:
32
+ """Format a list of interests with a limit and show count if truncated."""
33
+ if not interests:
34
+ return 'None'
35
+ sorted_interests = sorted(interests)
36
+ if len(sorted_interests) <= max_items:
37
+ return ', '.join(sorted_interests)
38
+ return f"{', '.join(sorted_interests[:max_items])} (+{len(sorted_interests) - max_items} more)"
39
+
40
+ class QuestionRecommender:
41
+ def __init__(self):
42
+ self.driver = GraphDatabase.driver(
43
+ NEO4J_URL,
44
+ auth=(NEO4J_USER, NEO4J_PASSWORD)
45
+ )
46
+
47
+ def close(self):
48
+ self.driver.close()
49
+
50
+ def get_all_users(self) -> List[str]:
51
+ """Get list of all users."""
52
+ with self.driver.session() as session:
53
+ result = session.run("""
54
+ MATCH (u:User)
55
+ RETURN DISTINCT u.name as username
56
+ ORDER BY username
57
+ """)
58
+ return [record["username"] for record in result if record["username"]]
59
+
60
+ def get_user_interests(self, username: str) -> Dict[str, set]:
61
+ """Get keywords and topics a user is interested in."""
62
+ with self.driver.session() as session:
63
+ # Get keywords the user is interested in
64
+ keyword_result = session.run("""
65
+ MATCH (u:User {name: $username})-[:INTERESTED_IN_KEYWORD]->(k:Keyword)
66
+ RETURN DISTINCT k.keyword as keyword
67
+ """, username=username)
68
+ keywords = {str(record["keyword"]) for record in keyword_result if record["keyword"]}
69
+
70
+ # Get topics the user is interested in
71
+ topic_result = session.run("""
72
+ MATCH (u:User {name: $username})-[:INTERESTED_IN_TOPIC]->(t:Topic)
73
+ RETURN DISTINCT t.topic as topic
74
+ """, username=username)
75
+ topics = {str(record["topic"]) for record in topic_result if record["topic"]}
76
+
77
+ return {"keywords": keywords or set(), "topics": topics or set()}
78
+
79
+ def find_common_questions(self, user1: str, user2: str, max_questions: int = 5) -> List[Dict]:
80
+ """Find questions to recommend based on common interests."""
81
+ with self.driver.session() as session:
82
+ # First try to find questions with common keywords
83
+ keyword_questions = session.run("""
84
+ // Find keywords that both users are interested in
85
+ MATCH (u1:User {name: $user1})-[:INTERESTED_IN_KEYWORD]->(k:Keyword)<-[:INTERESTED_IN_KEYWORD]-(u2:User {name: $user2})
86
+ WITH DISTINCT k
87
+
88
+ // Find questions with these common keywords
89
+ MATCH (q:Question)-[:HAS_KEYWORD]->(k)
90
+ WHERE q.author <> $user1 AND q.author <> $user2
91
+
92
+ // Count how many common keywords each question matches
93
+ WITH q, k, COLLECT(k.keyword) as matching_keywords
94
+ WITH q, matching_keywords, SIZE(matching_keywords) as relevance_score
95
+
96
+ // Return questions with their details
97
+ RETURN DISTINCT
98
+ q.title as title,
99
+ q.body as body,
100
+ q.created_utc_ts as created_date,
101
+ q.author as author,
102
+ matching_keywords as keywords,
103
+ relevance_score
104
+ ORDER BY relevance_score DESC, q.created_utc_ts DESC
105
+ LIMIT $limit
106
+ """, user1=user1, user2=user2, limit=max_questions)
107
+
108
+ questions = [dict(record) for record in keyword_questions]
109
+
110
+ # If no questions found with common keywords, try topics
111
+ if not questions:
112
+ topic_questions = session.run("""
113
+ // Find topics that both users are interested in
114
+ MATCH (u1:User {name: $user1})-[:INTERESTED_IN_TOPIC]->(t:Topic)<-[:INTERESTED_IN_TOPIC]-(u2:User {name: $user2})
115
+ WITH DISTINCT t
116
+
117
+ // Find questions with these common topics
118
+ MATCH (q:Question)-[:HAS_TOPIC]->(t)
119
+ WHERE q.author <> $user1 AND q.author <> $user2
120
+
121
+ // Count how many common topics each question matches
122
+ WITH q, t, COLLECT(t.topic) as matching_topics
123
+ WITH q, matching_topics, SIZE(matching_topics) as relevance_score
124
+
125
+ // Return questions with their details
126
+ RETURN DISTINCT
127
+ q.title as title,
128
+ q.body as body,
129
+ q.created_utc_ts as created_date,
130
+ q.author as author,
131
+ matching_topics as topics,
132
+ relevance_score
133
+ ORDER BY relevance_score DESC, q.created_utc_ts DESC
134
+ LIMIT $limit
135
+ """, user1=user1, user2=user2, limit=max_questions)
136
+
137
+ questions = [dict(record) for record in topic_questions]
138
+
139
+ return questions
140
+
141
+ def format_question(q: Dict) -> str:
142
+ """Format a question for display."""
143
+ created_date = format_neo4j_datetime(q.get('created_date'))
144
+ keywords_or_topics = q.get('keywords', q.get('topics', []))
145
+ interests = format_interest_list(set(k for k in keywords_or_topics if k is not None), max_items=5)
146
+ author = q.get('author', 'Unknown author')
147
+ title = q.get('title', 'Untitled')
148
+ body = q.get('body', '')
149
+
150
+ # Only show body section if there's actual content
151
+ body_html = f"""
152
+ <div class="question-body">
153
+ {body[:300] + "... [truncated]" if body and len(body) > 300 else body}
154
+ </div>
155
+ """ if body else ""
156
+
157
+ return f"""
158
+ <div class="question-card">
159
+ <h3>{title}</h3>
160
+ <div class="question-meta">
161
+ Posted by <span class="author">{author}</span> on <span class="date">{created_date}</span>
162
+ </div>
163
+ <div class="interests">
164
+ Common Interests: <span class="interest-tags">{interests}</span>
165
+ </div>{body_html}
166
+ </div>
167
+ """
168
+
169
+ def recommend_questions(user1: str, user2: str) -> Tuple[str, str, str]:
170
+ """Main function to get recommendations and user interests."""
171
+ recommender = QuestionRecommender()
172
+ try:
173
+ # Get interests for both users
174
+ user1_interests = recommender.get_user_interests(user1)
175
+ user2_interests = recommender.get_user_interests(user2)
176
+
177
+ # Find common interests
178
+ common_keywords = user1_interests['keywords'] & user2_interests['keywords']
179
+ common_topics = user1_interests['topics'] & user2_interests['topics']
180
+
181
+ # Format interests summary
182
+ interests_summary = f"""
183
+ <div class="interests-summary">
184
+ <div class="user-interests">
185
+ <h3>{user1}'s Interests</h3>
186
+ <div class="interest-section">
187
+ <strong>Keywords:</strong> {format_interest_list(user1_interests['keywords'], max_items=8)}
188
+ </div>
189
+ <div class="interest-section">
190
+ <strong>Topics:</strong> {format_interest_list(user1_interests['topics'], max_items=5)}
191
+ </div>
192
+ </div>
193
+
194
+ <div class="user-interests">
195
+ <h3>{user2}'s Interests</h3>
196
+ <div class="interest-section">
197
+ <strong>Keywords:</strong> {format_interest_list(user2_interests['keywords'], max_items=8)}
198
+ </div>
199
+ <div class="interest-section">
200
+ <strong>Topics:</strong> {format_interest_list(user2_interests['topics'], max_items=5)}
201
+ </div>
202
+ </div>
203
+
204
+ <div class="common-interests">
205
+ <h3>Common Interests</h3>
206
+ <div class="interest-section">
207
+ <strong>Keywords:</strong> {format_interest_list(common_keywords, max_items=8)}
208
+ </div>
209
+ <div class="interest-section">
210
+ <strong>Topics:</strong> {format_interest_list(common_topics, max_items=5)}
211
+ </div>
212
+ </div>
213
+ </div>
214
+ """
215
+
216
+ # Get recommended questions
217
+ questions = recommender.find_common_questions(user1, user2)
218
+
219
+ if questions:
220
+ questions_text = """<div class="questions-container">\n""" + \
221
+ """\n""".join(format_question(q) for q in questions) + \
222
+ """\n</div>"""
223
+ recommendation_type = """<h2 class="recommendation-header">""" + \
224
+ ("Recommendations Based on Common Keywords" if 'keywords' in questions[0]
225
+ else "Recommendations Based on Common Topics") + \
226
+ """</h2>"""
227
+ else:
228
+ questions_text = """<div class="no-questions">No questions found based on common interests.</div>"""
229
+ recommendation_type = """<h2 class="recommendation-header">No Recommendations Available</h2>"""
230
+
231
+ return interests_summary, recommendation_type, questions_text
232
+
233
+ except Exception as e:
234
+ logger.error(f"Error in recommend_questions: {str(e)}")
235
+ return (
236
+ """<div class="error">Error fetching user interests. Please try again.</div>""",
237
+ """<h2 class="error-header">Error</h2>""",
238
+ f"""<div class="error-message">An error occurred: {str(e)}</div>"""
239
+ )
240
+ finally:
241
+ recommender.close()
242
+
243
+ def loading_message() -> Tuple[str, str, str]:
244
+ """Return loading message in proper HTML format."""
245
+ loading_html = """
246
+ <div class="loading-spinner">
247
+ <div style="text-align: center;">
248
+ <div style="border: 4px solid #60a5fa; border-top: 4px solid transparent; border-radius: 50%; width: 40px; height: 40px; animation: spin 1s linear infinite; margin: 20px auto;"></div>
249
+ <div style="color: #60a5fa; margin-top: 10px;">Analyzing interests and finding recommendations...</div>
250
+ </div>
251
+ </div>
252
+ """
253
+ return loading_html, loading_html, loading_html
254
+
255
+ # Update the custom CSS with dark theme compatible colors
256
+ custom_css = """
257
+ .gradio-container {
258
+ max-width: 1200px !important;
259
+ margin: auto !important;
260
+ padding: 20px !important;
261
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif !important;
262
+ }
263
+
264
+ .interests-summary {
265
+ background: rgba(30, 41, 59, 0.5);
266
+ padding: 20px;
267
+ border-radius: 10px;
268
+ margin-bottom: 20px;
269
+ border: 1px solid rgba(148, 163, 184, 0.1);
270
+ }
271
+
272
+ .user-interests, .common-interests {
273
+ background: rgba(51, 65, 85, 0.5);
274
+ padding: 15px;
275
+ border-radius: 8px;
276
+ margin: 10px 0;
277
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
278
+ border: 1px solid rgba(148, 163, 184, 0.2);
279
+ }
280
+
281
+ .interest-section {
282
+ margin: 10px 0;
283
+ line-height: 1.5;
284
+ color: #e2e8f0;
285
+ }
286
+
287
+ .question-card {
288
+ background: rgba(51, 65, 85, 0.5);
289
+ padding: 20px;
290
+ border-radius: 8px;
291
+ margin: 15px 0;
292
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
293
+ border: 1px solid rgba(148, 163, 184, 0.2);
294
+ transition: transform 0.2s ease;
295
+ }
296
+
297
+ .question-card:hover {
298
+ transform: translateY(-2px);
299
+ }
300
+
301
+ .question-card h3 {
302
+ color: #60a5fa;
303
+ margin-top: 0;
304
+ margin-bottom: 10px;
305
+ font-size: 1.25rem;
306
+ }
307
+
308
+ .question-meta {
309
+ font-size: 0.9em;
310
+ color: #cbd5e1;
311
+ margin-bottom: 10px;
312
+ }
313
+
314
+ .author {
315
+ color: #93c5fd;
316
+ font-weight: 500;
317
+ }
318
+
319
+ .date {
320
+ color: #94a3b8;
321
+ }
322
+
323
+ .interests {
324
+ margin: 10px 0;
325
+ font-size: 0.9em;
326
+ color: #cbd5e1;
327
+ }
328
+
329
+ .interest-tags {
330
+ color: #93c5fd;
331
+ font-weight: 500;
332
+ }
333
+
334
+ .question-body {
335
+ line-height: 1.6;
336
+ color: #e2e8f0;
337
+ margin-top: 15px;
338
+ white-space: pre-wrap;
339
+ }
340
+
341
+ .recommendation-header {
342
+ color: #60a5fa;
343
+ padding: 10px 0;
344
+ margin: 20px 0;
345
+ border-bottom: 2px solid #3b82f6;
346
+ }
347
+
348
+ .error {
349
+ color: #fca5a5;
350
+ padding: 15px;
351
+ background: rgba(239, 68, 68, 0.2);
352
+ border-radius: 8px;
353
+ margin: 10px 0;
354
+ border: 1px solid rgba(239, 68, 68, 0.3);
355
+ }
356
+
357
+ .error-header {
358
+ color: #fca5a5;
359
+ }
360
+
361
+ .error-message {
362
+ background: rgba(239, 68, 68, 0.2);
363
+ padding: 15px;
364
+ border-radius: 8px;
365
+ color: #fca5a5;
366
+ border: 1px solid rgba(239, 68, 68, 0.3);
367
+ }
368
+
369
+ .no-questions {
370
+ padding: 20px;
371
+ background: rgba(51, 65, 85, 0.5);
372
+ border-radius: 8px;
373
+ text-align: center;
374
+ color: #94a3b8;
375
+ border: 1px solid rgba(148, 163, 184, 0.2);
376
+ }
377
+
378
+ .loading-spinner {
379
+ display: flex;
380
+ justify-content: center;
381
+ align-items: center;
382
+ padding: 20px;
383
+ color: #60a5fa;
384
+ }
385
+
386
+ h1, h2, h3 {
387
+ color: #60a5fa !important;
388
+ }
389
+
390
+ strong {
391
+ color: #93c5fd;
392
+ }
393
+
394
+ .user-interests h3, .common-interests h3 {
395
+ color: #60a5fa;
396
+ margin-top: 0;
397
+ margin-bottom: 15px;
398
+ font-size: 1.2rem;
399
+ }
400
+ """
401
+
402
+ # Create Gradio interface
403
+ recommender = QuestionRecommender()
404
+ users = recommender.get_all_users()
405
+ recommender.close()
406
+
407
+ with gr.Blocks(title="Question Recommender", theme=gr.themes.Soft(), css=custom_css) as iface:
408
+ gr.Markdown("""
409
+ # 🀝 Question Recommender
410
+ Find questions that two users might be interested in discussing together based on their common interests.
411
+ """)
412
+
413
+ with gr.Row(equal_height=True):
414
+ with gr.Column(scale=1):
415
+ user1_dropdown = gr.Dropdown(
416
+ choices=users,
417
+ label="πŸ‘€ First User",
418
+ interactive=True
419
+ )
420
+ with gr.Column(scale=1):
421
+ user2_dropdown = gr.Dropdown(
422
+ choices=users,
423
+ label="πŸ‘€ Second User",
424
+ interactive=True
425
+ )
426
+
427
+ recommend_btn = gr.Button(
428
+ "πŸ” Get Recommendations",
429
+ variant="primary",
430
+ size="lg"
431
+ )
432
+
433
+ with gr.Row():
434
+ interests_output = gr.HTML(label="Common Interests")
435
+
436
+ recommendation_type = gr.HTML()
437
+ questions_output = gr.HTML()
438
+
439
+ # Add loading state
440
+ recommend_btn.click(
441
+ fn=loading_message, # First show loading message
442
+ outputs=[interests_output, recommendation_type, questions_output],
443
+ queue=False # Don't queue this call
444
+ ).then( # Then get the actual recommendations
445
+ fn=recommend_questions,
446
+ inputs=[user1_dropdown, user2_dropdown],
447
+ outputs=[interests_output, recommendation_type, questions_output]
448
+ )
449
+
450
+ if __name__ == "__main__":
451
  iface.launch(share=True)