Ippo987 commited on
Commit
e487cc6
·
verified ·
1 Parent(s): c9911a1

Yup this is it

Browse files
TrendAnalysis.py ADDED
@@ -0,0 +1,1044 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from motor.motor_asyncio import AsyncIOMotorClient
2
+ import pandas as pd
3
+ import numpy as np
4
+ import re
5
+ import json
6
+ import umap
7
+ import plotly.io as pio
8
+ import hdbscan
9
+ from bertopic import BERTopic
10
+ from sklearn.feature_extraction.text import ENGLISH_STOP_WORDS
11
+ from skopt import gp_minimize
12
+ from sentence_transformers import SentenceTransformer
13
+ import torch
14
+ import random
15
+ import multiprocessing
16
+ from sklearn.feature_extraction.text import CountVectorizer
17
+ from bertopic.vectorizers import ClassTfidfTransformer
18
+ from bertopic.representation import KeyBERTInspired
19
+ import optuna
20
+ import pandas as pd
21
+ import dash
22
+ from dash import dcc, html, Input, Output, State
23
+ import plotly.graph_objects as go
24
+ import plotly.express as px
25
+ import numpy as np
26
+ import dash_bootstrap_components as dbc
27
+ from fastapi import HTTPException, APIRouter, Request
28
+ from pydantic import BaseModel
29
+ import threading
30
+ import time
31
+ import webbrowser
32
+ import asyncio
33
+
34
+
35
+ # Set seed for reproducibility
36
+ def set_seed(seed=42):
37
+ random.seed(seed)
38
+ np.random.seed(seed)
39
+ torch.manual_seed(seed)
40
+ torch.cuda.manual_seed_all(seed)
41
+ torch.backends.cudnn.deterministic = True
42
+ torch.backends.cudnn.benchmark = False
43
+
44
+
45
+ if __name__ == "__main__":
46
+ set_seed(42)
47
+ multiprocessing.freeze_support()
48
+
49
+ global TitleName
50
+ TitleName = "Dashboard"
51
+ router = APIRouter()
52
+
53
+
54
+ class TrendAnalysisRequest(BaseModel):
55
+ userId: str
56
+ topic: str
57
+ year: str = None
58
+ page: int = 0
59
+
60
+
61
+ async def fetch_papers_with_pagination(request: Request, userId: str, topic: str, year: str = None, page: int = 0):
62
+ # Build the query filter
63
+ query_filter = {"userId": userId, "topic": topic}
64
+ if year:
65
+ query_filter["year"] = year
66
+
67
+ # Count total matching documents
68
+ count_pipeline = [
69
+ {"$match": query_filter},
70
+ {"$unwind": "$papers"},
71
+ {"$count": "total_papers"}
72
+ ]
73
+ collection = request.app.state.collection
74
+ count_result = await collection.aggregate(count_pipeline).to_list(length=1)
75
+ total_papers = count_result[0]['total_papers'] if count_result else 0
76
+
77
+ print(f"Total papers matching criteria: {total_papers}")
78
+
79
+ # If no papers found, return empty result
80
+ if total_papers == 0:
81
+ return pd.DataFrame(), 0, 0, 0, 0
82
+
83
+ # Define pagination constants
84
+ papers_per_page = 200
85
+ min_papers_last_page = 50
86
+
87
+ # Calculate basic pagination
88
+ if total_papers <= papers_per_page:
89
+ # Simple case: all papers fit in one page
90
+ total_pages = 1
91
+ else:
92
+ # Multiple pages case
93
+ full_pages = total_papers // papers_per_page
94
+ remaining = total_papers % papers_per_page
95
+
96
+ if remaining >= min_papers_last_page:
97
+ # If remaining papers meet minimum threshold, create a separate page
98
+ total_pages = full_pages + 1
99
+ else:
100
+ # Otherwise, we'll have exactly 'full_pages' pages
101
+ # The remaining papers will be added to the last page
102
+ total_pages = full_pages
103
+
104
+ # Ensure page is within valid range
105
+ if page >= total_pages:
106
+ return pd.DataFrame(), 0, total_pages, 0, total_papers
107
+
108
+ # Calculate skip and limit based on page number
109
+ if total_pages == 1:
110
+ # Only one page - return all papers
111
+ skip = 0
112
+ limit = total_papers
113
+ elif page < total_pages - 1:
114
+ # Regular full page
115
+ skip = page * papers_per_page
116
+ limit = papers_per_page
117
+ else:
118
+ # Last page - might include remaining papers
119
+ remaining = total_papers % papers_per_page
120
+
121
+ if remaining >= min_papers_last_page or remaining == 0:
122
+ # Last page with either enough remaining papers or perfectly divided
123
+ skip = page * papers_per_page
124
+ limit = remaining if remaining > 0 else papers_per_page
125
+ else:
126
+ # Last page with remaining papers that don't meet minimum threshold
127
+ # We distribute by adding them to the last page
128
+ skip = (total_pages - 1) * papers_per_page
129
+ limit = papers_per_page + remaining
130
+
131
+ print(f"Pagination: Page {page + 1} of {total_pages}, Skip {skip}, Limit {limit}")
132
+
133
+ # MongoDB aggregation pipeline
134
+ pipeline = [
135
+ {"$match": query_filter},
136
+ {"$unwind": "$papers"},
137
+ {"$replaceRoot": {"newRoot": "$papers"}},
138
+ {"$project": {
139
+ "_id": 0,
140
+ "paperId": 1,
141
+ "url": 1,
142
+ "title": 1,
143
+ "abstract": 1,
144
+ "citationCount": 1,
145
+ "influentialCitationCount": 1,
146
+ "embedding": 1,
147
+ "publicationDate": 1,
148
+ "authors": 1
149
+ }},
150
+ {"$sort": {"publicationDate": 1}},
151
+ {"$skip": skip},
152
+ {"$limit": limit}
153
+ ]
154
+
155
+ # Execute the aggregation pipeline
156
+ cursor = collection.aggregate(pipeline)
157
+ papers = await cursor.to_list(None)
158
+
159
+ papers_count = len(papers)
160
+ print(f"Papers Retrieved: {papers_count}")
161
+
162
+ # Convert to DataFrame
163
+ df = pd.DataFrame(papers)
164
+ df = df.sort_values(by="publicationDate")
165
+ print(df[["paperId", "publicationDate"]].head(10))
166
+
167
+ return df, page, total_pages, papers_count, total_papers
168
+
169
+
170
+ # Preprocessing function
171
+ def clean_text(text):
172
+ text = str(text).lower()
173
+ text = re.sub(r"[^a-zA-Z0-9\s]", "", text)
174
+ return ' '.join([word for word in text.split() if word not in ENGLISH_STOP_WORDS])
175
+
176
+
177
+ # Adaptive clustering and topic modeling
178
+ def perform_trend_analysis(df):
179
+ # Convert embeddings
180
+ def convert_embedding(embedding):
181
+ return np.array(embedding["vector"], dtype=np.float64) if isinstance(embedding,
182
+ dict) and "vector" in embedding else None
183
+
184
+ df["embedding"] = df["embedding"].apply(convert_embedding)
185
+ df = df.dropna(subset=["embedding"])
186
+
187
+ if df.empty:
188
+ return df, {}
189
+
190
+ df["clean_text"] = (df["abstract"].fillna("")).apply(clean_text)
191
+
192
+ def objective(trial):
193
+ umap_n_components = trial.suggest_int("umap_n_components", 1, 12)
194
+ umap_min_dist = trial.suggest_float("umap_min_dist", 0.1, 0.8)
195
+ umap_n_neighbors = trial.suggest_int("umap_n_neighbors", 2, 12)
196
+ hdbscan_min_cluster_size = trial.suggest_int("hdbscan_min_cluster_size", 2, 10)
197
+ hdbscan_min_samples = trial.suggest_int("hdbscan_min_samples", 1, 10)
198
+ hdbscan_cluster_selection_epsilon = trial.suggest_float("hdbscan_cluster_selection_epsilon", 0.2, 0.8)
199
+ hdbscan_cluster_selection_method = trial.suggest_categorical("hdbscan_cluster_selection_method",
200
+ ["eom", "leaf"])
201
+
202
+ reducer_high_dim = umap.UMAP(
203
+ n_components=umap_n_components,
204
+ random_state=42,
205
+ min_dist=umap_min_dist,
206
+ n_neighbors=umap_n_neighbors,
207
+ metric="cosine"
208
+ )
209
+ reduced_embeddings_high_dim = reducer_high_dim.fit_transform(np.vstack(df["embedding"].values)).astype(
210
+ np.float64)
211
+
212
+ clusterer = hdbscan.HDBSCAN(
213
+ min_cluster_size=hdbscan_min_cluster_size,
214
+ min_samples=hdbscan_min_samples,
215
+ cluster_selection_epsilon=hdbscan_cluster_selection_epsilon,
216
+ cluster_selection_method=hdbscan_cluster_selection_method,
217
+ prediction_data=True,
218
+ core_dist_n_jobs=1
219
+ )
220
+ labels = clusterer.fit_predict(reduced_embeddings_high_dim)
221
+
222
+ if len(set(labels)) > 1:
223
+ dbcv_score = hdbscan.validity.validity_index(reduced_embeddings_high_dim, labels)
224
+ else:
225
+ dbcv_score = -np.inf
226
+
227
+ return dbcv_score
228
+
229
+ study = optuna.create_study(
230
+ direction="maximize",
231
+ sampler=optuna.samplers.TPESampler(seed=42))
232
+ study.optimize(objective, n_trials=100)
233
+
234
+ best_params = study.best_params
235
+ umap_model = umap.UMAP(
236
+ n_components=best_params["umap_n_components"],
237
+ random_state=42,
238
+ min_dist=best_params["umap_min_dist"],
239
+ n_neighbors=best_params["umap_n_neighbors"],
240
+ metric="cosine"
241
+ )
242
+ hdbscan_model = hdbscan.HDBSCAN(
243
+ min_cluster_size=best_params["hdbscan_min_cluster_size"],
244
+ min_samples=best_params["hdbscan_min_samples"],
245
+ cluster_selection_epsilon=best_params["hdbscan_cluster_selection_epsilon"],
246
+ cluster_selection_method=best_params["hdbscan_cluster_selection_method"],
247
+ prediction_data=True,
248
+ core_dist_n_jobs=1
249
+ )
250
+
251
+ vectorizer = CountVectorizer(
252
+ stop_words=list(ENGLISH_STOP_WORDS),
253
+ ngram_range=(2, 3)
254
+ )
255
+
256
+ representation_model = KeyBERTInspired()
257
+ embedding_model = SentenceTransformer("allenai/specter")
258
+ topic_model = BERTopic(
259
+ vectorizer_model=vectorizer,
260
+ umap_model=umap_model,
261
+ hdbscan_model=hdbscan_model,
262
+ embedding_model=embedding_model,
263
+ nr_topics='auto',
264
+ top_n_words=8,
265
+ representation_model=representation_model,
266
+ ctfidf_model=ClassTfidfTransformer(reduce_frequent_words=False, bm25_weighting=True)
267
+ )
268
+
269
+ topics, _ = topic_model.fit_transform(df["clean_text"], np.vstack(df["embedding"].values))
270
+ df["topic"] = topics
271
+ topic_labels = {t: " | ".join([word for word, _ in topic_model.get_topic(t)][:8]) for t in set(topics)}
272
+
273
+ reduced_embeddings_2d = umap.UMAP(n_components=2, random_state=42).fit_transform(
274
+ np.vstack(df["embedding"].values)).astype(np.float64)
275
+ df["x"] = reduced_embeddings_2d[:, 0]
276
+ df["y"] = reduced_embeddings_2d[:, 1]
277
+ df["topic_label"] = df["topic"].map(topic_labels)
278
+
279
+ return df, topic_labels
280
+
281
+
282
+ def build_dashboard(df, titleNm, topic_year):
283
+ TitleName = titleNm + "_" + topic_year
284
+ color_palette = px.colors.qualitative.Vivid
285
+ unique_topics = sorted(df["topic"].unique())
286
+ color_map = {topic: color_palette[i % len(color_palette)] for i, topic in enumerate(unique_topics)}
287
+
288
+ # Map colors to topics
289
+ df["color"] = df["topic"].map(color_map)
290
+
291
+ # Calculate the number of papers in each cluster
292
+ cluster_sizes = df.groupby("topic").size().reset_index(name="paper_count")
293
+ df = df.merge(cluster_sizes, on="topic", how="left")
294
+
295
+ # Improved marker scaling with a better range
296
+ min_size = 50
297
+ max_size = 140
298
+ df["marker_size"] = ((df["paper_count"] - df["paper_count"].min()) /
299
+ (df["paper_count"].max() - df["paper_count"].min())) * (max_size - min_size) + min_size
300
+
301
+ # Add log-transformed citation and influence columns
302
+ df["log_citation"] = np.log1p(df["citationCount"])
303
+ df["log_influence"] = np.log1p(df["influentialCitationCount"])
304
+
305
+ # Bayesian shrinkage for citations and influence
306
+ global_median_citation = df["log_citation"].median()
307
+ global_median_influence = df["log_influence"].median()
308
+ C = 10 # Shrinkage constant
309
+
310
+ def bayesian_shrinkage(group, global_median, C):
311
+ return (group.sum() + C * global_median) / (len(group) + C)
312
+
313
+ adjusted_citations = df.groupby("topic")["log_citation"].apply(
314
+ lambda x: bayesian_shrinkage(x, global_median_citation, C))
315
+ adjusted_influence = df.groupby("topic")["log_influence"].apply(
316
+ lambda x: bayesian_shrinkage(x, global_median_influence, C))
317
+
318
+ # Merge adjusted metrics back into the dataframe
319
+ df = df.merge(adjusted_citations.rename("adjusted_citation"), on="topic")
320
+ df = df.merge(adjusted_influence.rename("adjusted_influence"), on="topic")
321
+
322
+ # Calculate global percentiles for thresholds
323
+ citation_25th = df["adjusted_citation"].quantile(0.25)
324
+ citation_75th = df["adjusted_citation"].quantile(0.75)
325
+ influence_25th = df["adjusted_influence"].quantile(0.25)
326
+ influence_75th = df["adjusted_influence"].quantile(0.75)
327
+
328
+ # Enhanced theme classification with more distinct emojis
329
+ def classify_theme(row):
330
+ if row["adjusted_citation"] >= citation_75th and row["adjusted_influence"] >= influence_75th:
331
+ return "🔥 Hot Topic"
332
+ elif row["adjusted_citation"] <= citation_25th and row["adjusted_influence"] >= influence_75th:
333
+ return "💎 Gap Opportunity"
334
+ elif row["adjusted_citation"] >= citation_75th and row["adjusted_influence"] <= influence_25th:
335
+ return "⚠️ Risky Theme"
336
+ else:
337
+ return "🔄 Neutral"
338
+
339
+ df["theme"] = df.apply(classify_theme, axis=1)
340
+
341
+ # Initialize the Dash app with an improved Bootstrap theme
342
+ app = dash.Dash(__name__, external_stylesheets=[dbc.themes.DARKLY]) # DARKLY for a sleek dark theme
343
+
344
+ # Create a more visually appealing figure
345
+ fig = go.Figure()
346
+
347
+ # Add subtle grid lines for reference
348
+ fig.update_xaxes(
349
+ showgrid=True,
350
+ gridwidth=0.1,
351
+ gridcolor='rgba(255, 255, 255, 0.05)',
352
+ zeroline=False
353
+ )
354
+ fig.update_yaxes(
355
+ showgrid=True,
356
+ gridwidth=0.1,
357
+ gridcolor='rgba(255, 255, 255, 0.05)',
358
+ zeroline=False
359
+ )
360
+
361
+ for topic in unique_topics:
362
+ topic_data = df[df["topic"] == topic]
363
+
364
+ # Get cluster center
365
+ center_x = topic_data["x"].mean()
366
+ center_y = topic_data["y"].mean()
367
+
368
+ # Get label
369
+ full_topic_formatted = topic_data['topic_label'].iloc[
370
+ 0] if 'topic_label' in topic_data.columns else f"Cluster {topic}"
371
+
372
+ # Add a subtle glow effect with a larger outer circle
373
+ fig.add_trace(
374
+ go.Scatter(
375
+ x=[center_x],
376
+ y=[center_y],
377
+ mode="markers",
378
+ marker=dict(
379
+ color=color_map[topic],
380
+ size=topic_data["marker_size"].iloc[0] * 1.2, # Slightly larger for glow effect
381
+ opacity=0.3,
382
+ line=dict(width=0),
383
+ symbol="circle",
384
+ ),
385
+ showlegend=False,
386
+ hoverinfo="none",
387
+ )
388
+ )
389
+
390
+ # Add main cluster circle with enhanced styling
391
+ fig.add_trace(
392
+ go.Scatter(
393
+ x=[center_x],
394
+ y=[center_y],
395
+ mode="markers+text",
396
+ marker=dict(
397
+ color=color_map[topic],
398
+ size=topic_data["marker_size"].iloc[0],
399
+ opacity=0.85,
400
+ line=dict(width=2, color="white"),
401
+ symbol="circle",
402
+ ),
403
+ text=[f"{topic}"],
404
+ textposition="middle center",
405
+ textfont=dict(
406
+ family="Arial Black",
407
+ size=16,
408
+ color="white"
409
+ ),
410
+ name=f"{topic}",
411
+ hovertemplate=(
412
+ "<b>Cluster ID:</b> %{text}<br>" +
413
+ "<b>Name:</b><br>" + full_topic_formatted + "<br>" +
414
+ "<b>Papers:</b> " + str(topic_data["paper_count"].iloc[0]) + "<br>" +
415
+ "<b>Popularity:</b> " + (
416
+ "🔼 High" if topic_data["adjusted_citation"].iloc[0] >= citation_75th else "🔽 Low") +
417
+ f" (Adjusted Citation: {topic_data['adjusted_citation'].iloc[0]:.2f})<br>" +
418
+ "<b>Impactfulness:</b> " + (
419
+ "🔼 High" if topic_data["adjusted_influence"].iloc[0] >= influence_75th else "🔽 Low") +
420
+ f" (Adjusted Influence: {topic_data['adjusted_influence'].iloc[0]:.2f})<br>" +
421
+ "<b>Theme:</b> " + topic_data["theme"].iloc[0] +
422
+ "<extra></extra>"
423
+ ),
424
+ customdata=[[topic]],
425
+ )
426
+ )
427
+
428
+ # Add an aesthetic background with gradient
429
+ fig.update_layout(
430
+ shapes=[
431
+ # Improved gradient background
432
+ dict(
433
+ type="rect",
434
+ xref="paper",
435
+ yref="paper",
436
+ x0=0,
437
+ y0=0,
438
+ x1=1,
439
+ y1=1,
440
+ fillcolor="rgba(0, 0, 40, 0.95)",
441
+ line_width=0,
442
+ layer="below"
443
+ ),
444
+ # Add a subtle radial gradient effect
445
+ dict(
446
+ type="circle",
447
+ xref="paper",
448
+ yref="paper",
449
+ x0=0.3,
450
+ y0=0.3,
451
+ x1=0.7,
452
+ y1=0.7,
453
+ fillcolor="rgba(50, 50, 120, 0.2)",
454
+ line_width=0,
455
+ layer="below"
456
+ )
457
+ ],
458
+ template="plotly_dark",
459
+ title={
460
+ 'text': f"<b>{TitleName.title()}</b>",
461
+ 'y': 0.97,
462
+ 'x': 0.5,
463
+ 'xanchor': 'center',
464
+ 'yanchor': 'top',
465
+ 'font': dict(
466
+ family="Arial Black",
467
+ size=28,
468
+ color="white",
469
+ ),
470
+ 'xref': 'paper',
471
+ 'yref': 'paper',
472
+ },
473
+ margin=dict(l=40, r=40, b=150, t=100),
474
+ hovermode="closest",
475
+ xaxis=dict(showticklabels=False),
476
+ yaxis=dict(showticklabels=False),
477
+ paper_bgcolor="rgba(0,0,0,0)",
478
+ plot_bgcolor="rgba(0,0,0,0)",
479
+ dragmode="pan",
480
+ legend=dict(
481
+ orientation="h",
482
+ yanchor="bottom",
483
+ y=-0.15,
484
+ xanchor="center",
485
+ x=0.5,
486
+ bgcolor="rgba(30,30,60,0.5)",
487
+ bordercolor="rgba(255,255,255,0.2)",
488
+ borderwidth=1
489
+ ),
490
+ )
491
+
492
+ # Add subtle animation options
493
+ fig.update_layout(
494
+ updatemenus=[
495
+ dict(
496
+ type="buttons",
497
+ showactive=False,
498
+ buttons=[
499
+ dict(
500
+ label="Reset View",
501
+ method="relayout",
502
+ args=[{"xaxis.range": None, "yaxis.range": None}]
503
+ ),
504
+ ],
505
+ x=0.05,
506
+ y=0.05,
507
+ xanchor="left",
508
+ yanchor="bottom",
509
+ bgcolor="rgba(50,50,80,0.7)",
510
+ bordercolor="rgba(255,255,255,0.2)",
511
+ )
512
+ ]
513
+ )
514
+
515
+ # Enhanced app layout with modern design elements
516
+ app.layout = dbc.Container(
517
+ fluid=True,
518
+ style={
519
+ "backgroundColor": "#111122",
520
+ "minHeight": "100vh",
521
+ "height": "100%",
522
+ "width": "100%",
523
+ "backgroundImage": "linear-gradient(135deg, #111122 0%, #15162c 100%)",
524
+ "padding": "20px"
525
+ },
526
+ children=[
527
+ dbc.Row([
528
+ dbc.Col(html.H1(
529
+ "Trend Analysis Dashboard ",
530
+ style={
531
+ "textAlign": "center",
532
+ "color": "white",
533
+ "marginBottom": "5px",
534
+ "fontFamily": "Arial Black",
535
+ "textShadow": "2px 2px 8px rgba(0,0,0,0.7)",
536
+ "letterSpacing": "2px",
537
+ "fontSize": "42px",
538
+ "background": "linear-gradient(135deg, #790091 0%, #565cd5 100%)",
539
+ "WebkitBackgroundClip": "text",
540
+ "WebkitTextFillColor": "transparent",
541
+ "paddingTop": "10px"
542
+ }
543
+ ), width=10),
544
+
545
+ dbc.Col([
546
+ html.Button(
547
+ [
548
+ html.I(className="fas fa-download mr-2"),
549
+ " Save Dashboard"
550
+ ],
551
+ id="download-button",
552
+ className="btn btn-outline-light",
553
+ style={
554
+ "marginTop": "10px",
555
+ "backgroundColor": "rgba(80, 80, 150, 0.4)",
556
+ "border": "1px solid rgba(100, 100, 200, 0.5)",
557
+ "borderRadius": "8px",
558
+ "padding": "8px 15px",
559
+ "boxShadow": "0px 4px 8px rgba(0, 0, 0, 0.3)",
560
+ "transition": "all 0.3s ease",
561
+ "fontSize": "14px",
562
+ "fontWeight": "bold"
563
+ }
564
+ ),
565
+ # Add the download component
566
+ dcc.Download(id="download-dashboard")
567
+ ], width=2),
568
+
569
+ dbc.Col(html.P(
570
+ "Interactive visualization of research topics and their relationships",
571
+ style={
572
+ "textAlign": "center",
573
+ "color": "#aaddff",
574
+ "marginBottom": "15px",
575
+ "fontStyle": "italic",
576
+ "fontSize": "16px",
577
+ "fontWeight": "300",
578
+ "letterSpacing": "0.5px",
579
+ "textShadow": "1px 1px 3px rgba(0,0,0,0.5)",
580
+ }
581
+ ), width=12),
582
+ ]),
583
+
584
+ dbc.Row([
585
+ dbc.Col(
586
+ dbc.Card(
587
+ dbc.CardBody([
588
+ dcc.Graph(
589
+ id="cluster-graph",
590
+ figure=fig,
591
+ config={
592
+ "scrollZoom": True,
593
+ "displayModeBar": True,
594
+ "modeBarButtonsToRemove": ["select2d", "lasso2d"]
595
+ }, style={"height": "80vh", "min-height": "800px"}
596
+ )
597
+ ], style={"height": "80vh", "min-height": "800px"}),
598
+ style={
599
+ "backgroundColor": "rgba(20, 20, 40, 0.7)",
600
+ "borderRadius": "15px",
601
+ "boxShadow": "0px 10px 30px rgba(0, 0, 0, 0.5)",
602
+ "border": "1px solid rgba(100, 100, 200, 0.3)",
603
+ "height": "80vh",
604
+ "min-height": "800px" # Ensure minimum height
605
+ }
606
+ ),
607
+ width=9
608
+ ),
609
+
610
+ dbc.Col(
611
+ dbc.Card(
612
+ dbc.CardBody([
613
+ html.H3("Paper List", style={
614
+ "textAlign": "center",
615
+ "marginBottom": "15px",
616
+ "color": "#ffffff",
617
+ "fontFamily": "Arial",
618
+ "fontWeight": "bold",
619
+ "textShadow": "1px 1px 3px rgba(0,0,0,0.3)"
620
+ }),
621
+ html.Hr(style={"borderColor": "rgba(100, 100, 200, 0.3)", "margin": "10px 0 20px 0"}),
622
+ html.Div(
623
+ id="paper-list",
624
+ style={
625
+ "overflowY": "auto",
626
+ "height": "700px",
627
+ "padding": "5px"
628
+ },
629
+ children=html.Div([
630
+ html.Div(
631
+ html.I(className="fas fa-mouse-pointer", style={"marginRight": "10px"}),
632
+ style={"textAlign": "center", "fontSize": "24px", "marginBottom": "10px",
633
+ "color": "#7f8fa6"}
634
+ ),
635
+ html.P("Click on a cluster to view its papers",
636
+ style={"textAlign": "center", "color": "#7f8fa6"})
637
+ ])
638
+ ),
639
+ ],
640
+ style={
641
+ "backgroundColor": "rgba(30, 30, 50, 0.8)",
642
+ "borderRadius": "15px",
643
+ "padding": "20px",
644
+ "height": "100%"
645
+ }),
646
+ style={
647
+ "height": "800px",
648
+ "boxShadow": "0px 10px 30px rgba(0, 0, 0, 0.5)",
649
+ "border": "1px solid rgba(100, 100, 200, 0.3)",
650
+ "borderRadius": "15px"
651
+ }
652
+ ),
653
+ width=3
654
+ ),
655
+ ], style={"marginTop": "20px"}),
656
+
657
+ # Add a footer with theme legend
658
+ dbc.Row([
659
+ dbc.Col(
660
+ dbc.Card(
661
+ dbc.CardBody([
662
+ html.H5("Theme Legend", style={"textAlign": "center", "marginBottom": "15px"}),
663
+ dbc.Row([
664
+ dbc.Col(html.Div([
665
+ html.Span("🔥", style={"fontSize": "20px", "marginRight": "10px"}),
666
+ "Hot Topic: High citations & high influence"
667
+ ]), width=3),
668
+ dbc.Col(html.Div([
669
+ html.Span("💎", style={"fontSize": "20px", "marginRight": "10px"}),
670
+ "Gap Opportunity: Low citations but high influence"
671
+ ]), width=3),
672
+ dbc.Col(html.Div([
673
+ html.Span("⚠️", style={"fontSize": "20px", "marginRight": "10px"}),
674
+ "Risky Theme: High citations but low influence"
675
+ ]), width=3),
676
+ dbc.Col(html.Div([
677
+ html.Span("🔄", style={"fontSize": "20px", "marginRight": "10px"}),
678
+ "Neutral: Average citations and influence"
679
+ ]), width=3),
680
+ ])
681
+ ]),
682
+ style={
683
+ "backgroundColor": "rgba(30, 30, 50, 0.8)",
684
+ "borderRadius": "15px",
685
+ "marginTop": "20px",
686
+ "boxShadow": "0px 5px 15px rgba(0, 0, 0, 0.3)",
687
+ "border": "1px solid rgba(100, 100, 200, 0.3)"
688
+ }
689
+ ),
690
+ width=12
691
+ ),
692
+
693
+ ]),
694
+
695
+ dcc.Store(id="stored-figure", data=fig)
696
+ ]
697
+
698
+ )
699
+
700
+ @app.callback(
701
+ Output("download-dashboard", "data"),
702
+ Input("download-button", "n_clicks"),
703
+ State("cluster-graph", "figure"),
704
+ prevent_initial_call=True
705
+ )
706
+ def download_dashboard(n_clicks, figure):
707
+ if n_clicks is None:
708
+ return None
709
+
710
+ # Save the figure as HTML with full plotly.js included
711
+ dashboard_html = pio.to_html(
712
+ figure,
713
+ full_html=True,
714
+ include_plotlyjs='cdn',
715
+ config={'responsive': True}
716
+ )
717
+
718
+ # Return the dashboard as an HTML file
719
+ return dict(
720
+ content=dashboard_html,
721
+ filename="research_dashboard.html",
722
+ type="text/html",
723
+ )
724
+
725
+ # Enhanced callback to update paper list with better styling
726
+ # Enhanced callback to update paper list with better styling
727
+ @app.callback(
728
+ Output("paper-list", "children"),
729
+ [Input("cluster-graph", "clickData")]
730
+ )
731
+ def update_paper_list(clickData):
732
+ if clickData is None:
733
+ return html.Div([
734
+ html.Div(
735
+ html.I(className="fas fa-mouse-pointer", style={"marginRight": "10px"}),
736
+ style={"textAlign": "center", "fontSize": "24px", "marginBottom": "10px", "color": "#7f8fa6"}
737
+ ),
738
+ html.P("Click on a cluster to view its papers",
739
+ style={"textAlign": "center", "color": "#7f8fa6"})
740
+ ])
741
+
742
+ # Extract the clicked cluster ID
743
+ try:
744
+ clicked_topic = clickData["points"][0]["customdata"][0]
745
+
746
+ # Get the color for this topic for styling consistency
747
+ topic_color = color_map[clicked_topic]
748
+
749
+ # Get the theme for this topic
750
+ topic_theme = df[df["topic"] == clicked_topic]["theme"].iloc[0]
751
+
752
+ except (KeyError, IndexError):
753
+ return html.Div("Error retrieving cluster data.", style={"textAlign": "center", "marginTop": "20px"})
754
+
755
+ # Filter papers in the clicked cluster - UPDATED to include titles AND urls
756
+ papers_in_cluster = df[df["topic"] == clicked_topic][["title", "url", "paperId"]]
757
+
758
+ if papers_in_cluster.empty:
759
+ return html.Div(f"No papers found for Cluster {clicked_topic}.",
760
+ style={"textAlign": "center", "marginTop": "20px"})
761
+
762
+ # Get topic label
763
+ topic_label = df[df["topic"] == clicked_topic]['topic_label'].iloc[
764
+ 0] if 'topic_label' in df.columns else f"Cluster {clicked_topic}"
765
+
766
+ # Create an enhanced styled list of paper titles - UPDATED to make clickable
767
+ paper_list = []
768
+ for i, (_, paper) in enumerate(papers_in_cluster.iterrows()):
769
+ paper_url = paper["url"]
770
+ paper_title = paper["title"]
771
+
772
+ paper_list.append(
773
+ dbc.Card(
774
+ dbc.CardBody([
775
+ html.A(
776
+ html.H6(
777
+ f"{i + 1}. {paper_title}",
778
+ className="card-title",
779
+ style={
780
+ "fontSize": "14px",
781
+ "margin": "5px 0",
782
+ "fontWeight": "normal",
783
+ "lineHeight": "1.4",
784
+ "color": "#aaccff", # Blue color to indicate clickable link
785
+ "cursor": "pointer"
786
+ }
787
+ ),
788
+ href=paper_url,
789
+ target="_blank", # Open in new tab
790
+ style={"textDecoration": "none"}
791
+ ),
792
+ ], style={"padding": "12px"}),
793
+ style={
794
+ "marginBottom": "10px",
795
+ "backgroundColor": "rgba(40, 45, 60, 0.8)",
796
+ "borderRadius": "8px",
797
+ "borderLeft": f"4px solid {topic_color}",
798
+ "boxShadow": "0px 3px 8px rgba(0, 0, 0, 0.2)",
799
+ "transition": "transform 0.2s",
800
+ ":hover": {
801
+ "transform": "translateY(-2px)",
802
+ "boxShadow": "0px 5px 10px rgba(0, 0, 0, 0.3)"
803
+ }
804
+ },
805
+ className="paper-card"
806
+ )
807
+ )
808
+
809
+ return html.Div([
810
+ html.Div([
811
+ html.H4(
812
+ f"Cluster {clicked_topic}",
813
+ style={
814
+ "textAlign": "center",
815
+ "marginBottom": "5px",
816
+ "color": topic_color,
817
+ "fontWeight": "bold"
818
+ }
819
+ ),
820
+ html.H5(
821
+ topic_label,
822
+ style={
823
+ "textAlign": "center",
824
+ "marginBottom": "5px",
825
+ "color": "#aaaacc",
826
+ "fontStyle": "italic",
827
+ "fontWeight": "normal"
828
+ }
829
+ ),
830
+ html.Div(
831
+ topic_theme,
832
+ style={
833
+ "textAlign": "center",
834
+ "marginBottom": "15px",
835
+ "fontSize": "16px",
836
+ "fontWeight": "bold"
837
+ }
838
+ ),
839
+ html.Hr(style={"borderColor": "rgba(100, 100, 200, 0.3)", "margin": "10px 0 20px 0"}),
840
+ html.H5(
841
+ f"Papers ({len(papers_in_cluster)})",
842
+ style={
843
+ "textAlign": "left",
844
+ "marginBottom": "15px",
845
+ "color": "#ffffff",
846
+ "fontWeight": "bold"
847
+ }
848
+ ),
849
+ ]),
850
+ html.Div(
851
+ paper_list,
852
+ style={"paddingRight": "10px"},
853
+ )
854
+ ])
855
+
856
+ # Add custom CSS for hover effects
857
+ app.index_string = '''
858
+ <!DOCTYPE html>
859
+ <html>
860
+ <head>
861
+ {%metas%}
862
+ <title>Trend Analysis Clusters Dashboard</title>
863
+ {%favicon%}
864
+ {%css%}
865
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
866
+ <style>
867
+ .paper-card:hover {
868
+ transform: translateY(-2px);
869
+ box-shadow: 0px 5px 10px rgba(0, 0, 0, 0.3);
870
+ background-color: rgba(50, 55, 70, 0.8) !important;
871
+ }
872
+ a h6:hover {
873
+ color: #ffffff !important;
874
+ text-decoration: underline;
875
+ }
876
+ /* Add subtle scroll bar styling */
877
+ ::-webkit-scrollbar {
878
+ width: 8px;
879
+ }
880
+ ::-webkit-scrollbar-track {
881
+ background: rgba(30, 30, 50, 0.3);
882
+ border-radius: 10px;
883
+ }
884
+ ::-webkit-scrollbar-thumb {
885
+ background: rgba(100, 100, 200, 0.5);
886
+ border-radius: 10px;
887
+ }
888
+ ::-webkit-scrollbar-thumb:hover {
889
+ background: rgba(120, 120, 220, 0.7);
890
+ }
891
+ </style>
892
+ </head>
893
+ <body>
894
+ {%app_entry%}
895
+ <footer>
896
+ {%config%}
897
+ {%scripts%}
898
+ {%renderer%}
899
+ </footer>
900
+ </body>
901
+ </html>
902
+ '''
903
+ return app
904
+
905
+
906
+ # Global variables to track Dash app state
907
+ dash_thread = None
908
+ dash_app = None
909
+ DASH_PORT = 7050
910
+
911
+
912
+ # Simplified shutdown function that doesn't rely on request or psutil connections
913
+ def shutdown_dash_app():
914
+ global dash_thread, dash_app
915
+
916
+ if dash_app is not None:
917
+ try:
918
+ print("Shutting down previous Dash app...")
919
+
920
+ # If we have a Dash app with a server
921
+ if hasattr(dash_app, 'server'):
922
+ # Set a shutdown flag
923
+ dash_app._shutdown = True
924
+
925
+ # Force the thread to terminate
926
+ if dash_thread and dash_thread.is_alive():
927
+ import ctypes
928
+ ctypes.pythonapi.PyThreadState_SetAsyncExc(
929
+ ctypes.c_long(dash_thread.ident),
930
+ ctypes.py_object(SystemExit)
931
+ )
932
+ dash_thread.join(timeout=2)
933
+
934
+ # Try to find and kill the process using the port
935
+ try:
936
+ import psutil
937
+ import os
938
+ import signal
939
+
940
+ for proc in psutil.process_iter(['pid']):
941
+ try:
942
+ for conn in proc.connections(kind='inet'):
943
+ if conn.laddr.port == DASH_PORT:
944
+ print(f"Killing process {proc.pid} using port {DASH_PORT}")
945
+ os.kill(proc.pid, signal.SIGTERM)
946
+ except:
947
+ pass
948
+ except:
949
+ print("Could not find process using port")
950
+
951
+ # Clear references
952
+ dash_app = None
953
+ print("Previous Dash app successfully shut down")
954
+ return True
955
+
956
+ except Exception as e:
957
+ print(f"Error shutting down Dash app: {e}")
958
+ # Even if there were errors, reset the state
959
+ dash_app = None
960
+ return True
961
+
962
+ return True # No app to shut down
963
+
964
+
965
+ # Updated function to run Dash with error handling
966
+ def run_dash(df, titleNm, Topic_year):
967
+ global dash_app
968
+
969
+ try:
970
+ # Build the dashboard
971
+ dash_app = build_dashboard(df, titleNm, Topic_year)
972
+
973
+ # Run the server
974
+ dash_app.run_server(debug=False, port=DASH_PORT, use_reloader=False)
975
+ except Exception as e:
976
+ print(f"Error running Dash app: {e}")
977
+ dash_app = None
978
+
979
+
980
+ # Update your endpoint - removed request parameter from shutdown_dash_app
981
+ @router.post("/analyze-trends/")
982
+ async def analyze_trends(request: Request, data_request: TrendAnalysisRequest):
983
+ global dash_thread
984
+ TitleName = data_request.topic
985
+ Topic_year = data_request.year
986
+ # First, ensure any existing dashboard is properly shut down
987
+ shutdown_dash_app()
988
+
989
+ # Short delay to ensure port is freed
990
+ import time
991
+ time.sleep(1)
992
+
993
+ # Fetch and process data
994
+ df, current_page, total_pages, papers_count, total_papers = await fetch_papers_with_pagination(
995
+ request, data_request.userId, data_request.topic, data_request.year, data_request.page
996
+ )
997
+
998
+ if df.empty and total_papers > 0:
999
+ raise HTTPException(
1000
+ status_code=404,
1001
+ detail=f"No papers found for page {data_request.page + 1}. Valid pages are 1 to {total_pages}."
1002
+ )
1003
+ elif df.empty:
1004
+ raise HTTPException(
1005
+ status_code=404,
1006
+ detail=f"No papers found for userId '{data_request.userId}', topic '{data_request.topic}'" +
1007
+ (f", and year '{data_request.year}'" if data_request.year else "")
1008
+ )
1009
+
1010
+ # Perform the trend analysis
1011
+ df, topic_labels = perform_trend_analysis(df)
1012
+
1013
+ if df.empty:
1014
+ raise HTTPException(status_code=500, detail="Failed to process embeddings for trend analysis")
1015
+
1016
+ # Create cluster statistics
1017
+ cluster_sizes = df.groupby("topic").size().to_dict()
1018
+
1019
+ # Create and start a new thread for the dashboard
1020
+ dash_thread = threading.Thread(target=run_dash, args=(df, TitleName, Topic_year))
1021
+ dash_thread.daemon = True
1022
+ dash_thread.start()
1023
+
1024
+ # Open browser automatically
1025
+ browser_thread = threading.Thread(target=open_browser)
1026
+ browser_thread.daemon = True
1027
+ browser_thread.start()
1028
+
1029
+ return {
1030
+ "message": f"Trend analysis completed for papers (page {current_page + 1} of {total_pages})",
1031
+ "current_page": current_page,
1032
+ "total_pages": total_pages,
1033
+ "papers_count": papers_count,
1034
+ "total_papers": total_papers,
1035
+ "cluster_sizes": cluster_sizes,
1036
+ "cluster_titles": topic_labels,
1037
+ "dashboard_url": f"http://localhost:{DASH_PORT}"
1038
+ }
1039
+
1040
+
1041
+ # Function to open browser after a short delay
1042
+ def open_browser():
1043
+ time.sleep(2) # Wait for servers to start
1044
+ webbrowser.open_new(f"http://localhost:{DASH_PORT}")
app.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, HTTPException, APIRouter, Request
2
+ from fastapi.responses import HTMLResponse
3
+ import uvicorn
4
+ from dataApi import router as dataAPI_router
5
+ from TrendAnalysis import router as trend_analysis_process
6
+ from datacite import router as citation_analysis_process
7
+ from fastapi.middleware.cors import CORSMiddleware
8
+ from dbconnect import db_Connect
9
+ from fastapi.templating import Jinja2Templates
10
+ from fastapi.staticfiles import StaticFiles
11
+ from venuAnalysis import router as venue_analysis_process
12
+ from venuedata import router as venuedata_router
13
+
14
+
15
+
16
+ # Initialize FastAPI app
17
+ app = FastAPI()
18
+
19
+ app.mount("/static", StaticFiles(directory="static"), name="static")
20
+ app.mount("/assets", StaticFiles(directory="assets"), name="assets")
21
+
22
+ templates = Jinja2Templates(directory="templates")
23
+ templates.env.auto_reload = True
24
+
25
+ app.add_middleware(
26
+ CORSMiddleware,
27
+ allow_origins=["*"], # For development - restrict this in production
28
+ allow_credentials=True,
29
+ allow_methods=["*"],
30
+ allow_headers=["*"],
31
+ )
32
+
33
+
34
+
35
+
36
+ # # Include routers
37
+ app.include_router(dataAPI_router)
38
+ app.include_router(trend_analysis_process)
39
+ app.include_router(citation_analysis_process)
40
+ app.include_router(venue_analysis_process)
41
+ app.include_router(venuedata_router)
42
+
43
+
44
+ collection,collection1,collection2 = db_Connect()
45
+ app.state.collection = collection
46
+ app.state.collection1 = collection1
47
+ app.state.collection2 = collection2
48
+
49
+
50
+
51
+
52
+ # Root endpoint
53
+ @app.get("/")
54
+ async def root():
55
+ return {"message": "Welcome to the Science Mapping Tool!"}
56
+
57
+
58
+
59
+ @app.get("/home", response_class=HTMLResponse)
60
+ async def home(request: Request):
61
+ return templates.TemplateResponse("homepage.html", {"request": request})
62
+
63
+ @app.get("/login", response_class=HTMLResponse)
64
+ async def login(request: Request):
65
+ return templates.TemplateResponse("loginpage.html", {"request": request})
66
+
67
+ @app.get("/contact", response_class=HTMLResponse)
68
+ async def login(request: Request):
69
+ return templates.TemplateResponse("contactBoard.html", {"request": request})
70
+
71
+ @app.get("/feedback", response_class=HTMLResponse)
72
+ async def login(request: Request):
73
+ return templates.TemplateResponse("feedback.html", {"request": request})
74
+
75
+
76
+ # uvicorn app:app --host 0.0.0.0 --port 3000 --reload
77
+
dataApi.py ADDED
@@ -0,0 +1,245 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, HTTPException, APIRouter, Request
2
+ from motor.motor_asyncio import AsyncIOMotorClient
3
+ from pydantic import BaseModel
4
+ import requests
5
+ import spacy
6
+ import time
7
+ import torch
8
+ from transformers import AutoTokenizer
9
+ from adapters import AutoAdapterModel
10
+ from typing import Optional
11
+
12
+ router = APIRouter()
13
+
14
+
15
+ # Load spaCy model
16
+ nlp = spacy.load("en_core_sci_sm")
17
+
18
+ # Pydantic models for request/responsecl
19
+ class ResearchQuery(BaseModel):
20
+ userId: str # User ID
21
+ topic: str # Research topic
22
+ year: str # Year of publication
23
+
24
+
25
+ class PaperMetadata(BaseModel):
26
+ paperId: str
27
+ title: str
28
+ abstract: str
29
+ citationCount: int
30
+ influentialCitationCount: int
31
+ publicationDate: str
32
+ url: str
33
+
34
+
35
+ # Extract keywords from the research topic
36
+ def extract_keywords(text):
37
+ doc = nlp(text.lower()) # Normalize to lowercase
38
+
39
+ # Step 1: Extract noun chunks (compound phrases)
40
+ noun_chunks = [chunk.text for chunk in doc.noun_chunks]
41
+
42
+ # Step 2: Extract individual tokens (nouns and verbs)
43
+ individual_tokens = [
44
+ token.text
45
+ for token in doc
46
+ if token.pos_ in ["NOUN", "VERB"] and not token.is_stop
47
+ ]
48
+
49
+ # Step 3: Combine noun chunks and individual tokens
50
+ keywords = set(noun_chunks + individual_tokens)
51
+ cleaned_keywords = set()
52
+ for keyword in keywords:
53
+ # Check if the keyword is part of any larger noun chunk
54
+ if not any(keyword in chunk for chunk in noun_chunks if keyword != chunk):
55
+ cleaned_keywords.add(keyword)
56
+
57
+ return sorted(list(cleaned_keywords))
58
+
59
+
60
+ # Construct query based on keywords
61
+ def construct_query(keywords):
62
+ query = " + ".join(keywords)
63
+ return query
64
+
65
+
66
+ # Fetch Paper IDs using Semantic Scholar Bulk Search API
67
+ def fetch_paper_ids(query, year):
68
+ search_url = "https://api.semanticscholar.org/graph/v1/paper/search/bulk"
69
+ search_params = {
70
+ "query": query,
71
+ "year": year,
72
+ "fields": "paperId",
73
+ }
74
+ response = requests.get(search_url, params=search_params)
75
+
76
+ if response.status_code == 200:
77
+ data = response.json()
78
+ papers = data.get("data", [])
79
+ paper_ids = [paper["paperId"] for paper in papers]
80
+ return paper_ids
81
+ else:
82
+ raise HTTPException(status_code=response.status_code, detail="Error fetching paper IDs")
83
+
84
+
85
+ # Fetch metadata using Semantic Scholar Graph API
86
+ def fetch_metadata(batch_ids):
87
+ graph_url = "https://api.semanticscholar.org/graph/v1/paper/batch"
88
+ metadata_params = {
89
+ "fields": "title,abstract,citationCount,influentialCitationCount,publicationDate,url"
90
+ }
91
+
92
+ attempt = 0
93
+ max_retries = 20
94
+
95
+ while attempt < max_retries:
96
+ response = requests.post(graph_url, json={"ids": batch_ids}, params=metadata_params)
97
+
98
+ if response.status_code == 200:
99
+ return response.json()
100
+
101
+ elif response.status_code == 429:
102
+ wait_time = 5
103
+ print(f"429 Too Many Requests. Retrying in {wait_time} seconds...")
104
+ time.sleep(wait_time)
105
+ attempt += 1
106
+
107
+ else:
108
+ raise HTTPException(status_code=response.status_code, detail="Error fetching metadata")
109
+
110
+ raise HTTPException(status_code=500, detail="Max retries reached while fetching metadata")
111
+
112
+
113
+ # Clean and process metadata using SPECTER2 embeddings
114
+ def clean_and_process_metadata(metadata_list):
115
+ # Load tokenizer and base model
116
+ tokenizer = AutoTokenizer.from_pretrained("allenai/specter2_base")
117
+ model = AutoAdapterModel.from_pretrained("allenai/specter2_base")
118
+
119
+ # Load and activate the proximity adapter
120
+ model.load_adapter("allenai/specter2", source="hf", load_as="proximity", set_active=True)
121
+
122
+ # Move model to GPU if available
123
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
124
+ model.to(device)
125
+
126
+ cleaned_metadata = []
127
+ invalid_papers = []
128
+
129
+ for paper in metadata_list:
130
+ paper_id = paper.get("paperId")
131
+ title = paper.get("title")
132
+ abstract = paper.get("abstract")
133
+
134
+ # Case 1: Paper lacks sufficient content (title + abstract or just abstract)
135
+ if not (title and abstract) and not abstract:
136
+ invalid_papers.append(paper_id)
137
+ continue
138
+
139
+ # Prepare text for embedding generation
140
+ text = title + tokenizer.sep_token + abstract if title and abstract else abstract
141
+
142
+ # Tokenize and encode
143
+ inputs = tokenizer(text, padding=True, truncation=True, return_tensors="pt", max_length=512)
144
+ inputs = {k: v.to(device) for k, v in inputs.items()} # Move inputs to device
145
+
146
+ with torch.no_grad():
147
+ output = model(**inputs)
148
+ embedding = output.last_hidden_state[:, 0, :].cpu().numpy().tolist()
149
+
150
+ # Add embedding to the paper metadata
151
+ paper["embedding"] = {"model": "specter2_proximity", "vector": embedding}
152
+
153
+ # Add the cleaned paper to the list
154
+ cleaned_metadata.append(paper)
155
+
156
+ return cleaned_metadata, invalid_papers
157
+
158
+
159
+ # Save metadata to MongoDB under username and topic
160
+ async def save_to_mongodb(userId: str, topic: str, year:str , metadata_list, request:Request):
161
+ # Create or update the document for the user-topic combination
162
+ collection = request.app.state.collection
163
+
164
+ await collection.update_one(
165
+ {"userId": userId, "topic": topic ,"year": year}, # Filter by user and topic
166
+ {"$set": {"papers": metadata_list}}, # Update the papers field
167
+ upsert=True # Create the document if it doesn't exist
168
+ )
169
+
170
+ print(f"Saved {len(metadata_list)} papers for user '{userId}' and topic '{topic}'")
171
+
172
+
173
+ @router.get("/test")
174
+ def greet():
175
+ return {
176
+ "message":"helllo jessi"
177
+ }
178
+
179
+
180
+ # Endpoint to process user input and fetch data
181
+ @router.post("/analyze")
182
+ async def analyze(query: ResearchQuery,requests:Request):
183
+ # Extract keywords and construct query
184
+ keywords = extract_keywords(query.topic)
185
+ refined_query = construct_query(keywords)
186
+
187
+ print(f"\n🔍 Refined Query: {refined_query}")
188
+
189
+ # Fetch paper IDs
190
+ paper_ids = fetch_paper_ids(refined_query, query.year)
191
+ print(f"\nFound {len(paper_ids)} papers for '{query.topic}' in {query.year}\n")
192
+
193
+ # Fetch metadata in batches
194
+ metadata_list = []
195
+ batch_size = 100
196
+
197
+ for i in range(0, len(paper_ids), batch_size):
198
+ batch_ids = paper_ids[i : i + batch_size]
199
+ metadata = fetch_metadata(batch_ids)
200
+ metadata_list.extend(metadata)
201
+ print(f"Retrieved {len(batch_ids)} papers' metadata...")
202
+
203
+ # Clean and process metadata
204
+ cleaned_metadata, invalid_papers = clean_and_process_metadata(metadata_list)
205
+
206
+ # Save cleaned metadata to MongoDB under the username and topic
207
+ await save_to_mongodb(query.userId, query.topic, query.year, cleaned_metadata,requests )
208
+
209
+ return {
210
+ "message": f"Processed {len(cleaned_metadata)} papers and cleaned data.",
211
+ "invalid_papers_removed": len(invalid_papers),
212
+ }
213
+
214
+
215
+
216
+ # Pydantic model for request data
217
+ class CheckDataRequest(BaseModel):
218
+ userId: str
219
+ topic: str
220
+ year: Optional[str] = None
221
+
222
+ # Endpoint to check if the combination of userId, topic, and year exists
223
+
224
+ @router.post("/check-data-exists/")
225
+ async def check_data_exists(request_data: CheckDataRequest, request:Request):
226
+ # Create a query to check if the data exists
227
+ query = {
228
+ "userId": request_data.userId,
229
+ "topic": request_data.topic
230
+ }
231
+
232
+ # Add year to query if it's provided
233
+ if request_data.year:
234
+ query["year"] = request_data.year
235
+
236
+ collection = request.app.state.collection
237
+ # Check if a document matching the query exists
238
+ document = await collection.find_one(query) # Await the async operation
239
+
240
+ # Return result
241
+ return {
242
+ "exists": document is not None,
243
+ "message": "Data found" if document else "Data not found"
244
+ }
245
+
datacite.py ADDED
@@ -0,0 +1,294 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import spacy
2
+ import requests
3
+ from time import sleep
4
+ from tqdm import tqdm
5
+ from fastapi import HTTPException
6
+ from pydantic import BaseModel
7
+ from concurrent.futures import ThreadPoolExecutor
8
+ from datetime import datetime
9
+ from fastapi import APIRouter, Request
10
+ from fastapi.templating import Jinja2Templates
11
+ from fastapi.responses import HTMLResponse
12
+ import json
13
+
14
+ # Load spaCy model
15
+ nlp = spacy.load("en_core_sci_sm")
16
+ templates = Jinja2Templates(directory="templates")
17
+ # Constants
18
+ OPENALEX_API_URL = "https://api.openalex.org/works"
19
+ PER_PAGE = 100
20
+ REQUEST_DELAY = 0.5
21
+ MAX_RESULTS = 1000
22
+
23
+ router = APIRouter()
24
+
25
+
26
+ # Pydantic Models
27
+ class CitationAnalysisRequest(BaseModel):
28
+ userId: str
29
+ topic: str
30
+ year: int
31
+
32
+
33
+ # 1. Keyword Extraction
34
+ def extract_keywords(text):
35
+ doc = nlp(text.lower())
36
+ noun_chunks = [chunk.text for chunk in doc.noun_chunks]
37
+ individual_tokens = [token.text for token in doc if token.pos_ in ["NOUN", "VERB"] and not token.is_stop]
38
+ keywords = set(noun_chunks + individual_tokens)
39
+
40
+ cleaned_keywords = set()
41
+ for keyword in keywords:
42
+ if not any(keyword in chunk for chunk in noun_chunks if keyword != chunk):
43
+ cleaned_keywords.add(keyword)
44
+ return sorted(list(cleaned_keywords))
45
+
46
+
47
+ # 2. Citation Batch Fetching
48
+ def fetch_citing_papers_for_batch(paper_ids):
49
+ citing_map = {pid: [] for pid in paper_ids}
50
+ batch_size = 100
51
+
52
+ def fetch_batch(batch_ids):
53
+ filter_query = "|".join(batch_ids)
54
+ cursor = "*"
55
+ local_citing_map = {pid: [] for pid in batch_ids}
56
+
57
+ while True:
58
+ params = {
59
+ "filter": f"cites:{filter_query}",
60
+ "per_page": 200,
61
+ "cursor": cursor
62
+ }
63
+
64
+ try:
65
+ response = requests.get(OPENALEX_API_URL, params=params)
66
+ if response.status_code != 200:
67
+ print(f"❌ Error while fetching citing papers: {response.status_code}")
68
+ break
69
+
70
+ data = response.json()
71
+ for citing_work in data.get("results", []):
72
+ refs = citing_work.get("referenced_works", [])
73
+ for ref_id in refs:
74
+ if ref_id in local_citing_map:
75
+ local_citing_map[ref_id].append(citing_work["id"])
76
+
77
+ next_cursor = data.get("meta", {}).get("next_cursor")
78
+ if not next_cursor:
79
+ break
80
+ cursor = next_cursor
81
+ sleep(REQUEST_DELAY)
82
+
83
+ except Exception as e:
84
+ print(f"❌ Exception: {e}")
85
+ break
86
+
87
+ return local_citing_map
88
+
89
+ print("\n🔗 Fetching citing papers for all topic papers...")
90
+ with ThreadPoolExecutor(max_workers=10) as executor:
91
+ futures = []
92
+ for i in range(0, len(paper_ids), batch_size):
93
+ batch_ids = paper_ids[i:i + batch_size]
94
+ futures.append(executor.submit(fetch_batch, batch_ids))
95
+
96
+ for future in tqdm(futures, desc="ParallelGroup"):
97
+ local_citing_map = future.result()
98
+ citing_map.update(local_citing_map)
99
+
100
+ return citing_map
101
+
102
+
103
+ # 3. Fetch topic papers
104
+ def fetch_papers_with_citations(keywords, year, concept_threshold=0.8, max_results=MAX_RESULTS):
105
+ query = " + ".join(keywords)
106
+ print(f"\n🔍 Final OpenAlex Query: {query}")
107
+ params = {
108
+ "filter": f"title_and_abstract.search:{query},publication_year:{year}",
109
+ "per_page": PER_PAGE,
110
+ "cursor": "*"
111
+ }
112
+
113
+ all_papers = []
114
+ paper_id_map = {}
115
+ print("\n🚀 Fetching topic papers from OpenAlex...")
116
+
117
+ while len(all_papers) < max_results:
118
+ response = requests.get(OPENALEX_API_URL, params=params)
119
+ if response.status_code != 200:
120
+ print(f"❌ Error: {response.status_code}")
121
+ break
122
+
123
+ data = response.json()
124
+ results = data.get("results", [])
125
+
126
+ for paper in tqdm(results, desc="📄 Collecting papers"):
127
+ paper_id = paper.get("id", "")
128
+ concepts = [
129
+ c["display_name"]
130
+ for c in paper.get("concepts", [])
131
+ if c.get("score", 0) >= concept_threshold
132
+ ]
133
+
134
+ paper_data = {
135
+ "id": paper_id,
136
+ "title": paper.get("title", "No title"),
137
+ "cited_by_count": paper.get("cited_by_count", 0),
138
+ "publication_date": paper.get("publication_date", ""),
139
+ "referenced_works": paper.get("referenced_works", []),
140
+ "concepts": concepts,
141
+ "cited_by_ids": [] # to be filled later
142
+ }
143
+ paper_id_map[paper_id] = paper_data
144
+ all_papers.append(paper_data)
145
+
146
+ next_cursor = data.get("meta", {}).get("next_cursor")
147
+ if not next_cursor:
148
+ break
149
+
150
+ params["cursor"] = next_cursor
151
+ sleep(REQUEST_DELAY)
152
+
153
+ all_ids = list(paper_id_map.keys())
154
+ citing_map = fetch_citing_papers_for_batch(all_ids)
155
+
156
+ for pid, citing_ids in citing_map.items():
157
+ paper_id_map[pid]["cited_by_ids"] = citing_ids
158
+
159
+ cleaned_papers = []
160
+ for paper in all_papers:
161
+ if paper.get("referenced_works") or paper.get("cited_by_ids"):
162
+ cleaned_papers.append(paper)
163
+
164
+ print(f"\n🧹 Removed {len(all_papers) - len(cleaned_papers)} papers without references or citations.")
165
+ return cleaned_papers[:max_results]
166
+
167
+
168
+ # 4. Save to MongoDB
169
+ async def save_to_mongodb(userId, topic, year, papers, request: Request):
170
+ metadata = {
171
+ "userId": userId,
172
+ "topic": topic,
173
+ "year": year,
174
+ "scraped_on": datetime.utcnow().isoformat() + "Z",
175
+ "papers": papers
176
+ }
177
+ collection = request.app.state.collection1
178
+ result = await collection.insert_one(metadata)
179
+ print(f"\n✅ Saved metadata to MongoDB with ID: {result.inserted_id}")
180
+ return str(result.inserted_id)
181
+
182
+
183
+ # 5. FastAPI Endpoints
184
+ @router.post("/save")
185
+ async def save_data(data_request: CitationAnalysisRequest, saveRequest: Request):
186
+ userId = data_request.userId
187
+ topic = data_request.topic
188
+ year = data_request.year
189
+
190
+ keywords = extract_keywords(topic)
191
+ print("\n🔑 Extracted Keywords:")
192
+ print(keywords)
193
+
194
+ if not keywords:
195
+ raise HTTPException(status_code=400, detail="No keywords extracted. Please provide a valid topic.")
196
+
197
+ papers = fetch_papers_with_citations(keywords, year)
198
+ if not papers:
199
+ raise HTTPException(status_code=404, detail="No papers retrieved for the given topic and year.")
200
+
201
+ document_id = await save_to_mongodb(userId, topic, year, papers, saveRequest)
202
+ return {"message": "Data saved successfully!", "document_id": document_id}
203
+
204
+
205
+ # NEW ENDPOINT: Get citation data from MongoDB
206
+ # @router.post("/citation-data",response_class=HTMLResponse)
207
+ # async def get_citation_data(data_request:CitationAnalysisRequest , saveRequest: Request):
208
+ # # Build the query based on provided parameters
209
+ # query = {"userId": data_request.userId}
210
+
211
+ # if data_request.topic:
212
+ # query["topic"] = data_request.topic
213
+
214
+ # if data_request.year:
215
+ # query["year"] = data_request.year
216
+
217
+ # collection = saveRequest.app.state.collection1
218
+ # result = await collection.find_one(
219
+ # query,
220
+ # sort=[("scraped_on", 1)] # Sort by scraped_on in descending order to get the most recent
221
+ # )
222
+
223
+ # if not result:
224
+ # raise HTTPException(status_code=404, detail="No data found for the specified criteria")
225
+
226
+ # # Convert ObjectId to string for JSON serialization
227
+ # result["_id"] = str(result["_id"])
228
+
229
+ # data = result["papers"]
230
+ # data_json = json.dumps(data)
231
+ # print(" i am here")
232
+ # return templates.TemplateResponse("gra.html", {"request": saveRequest,"data":data_json})
233
+
234
+
235
+ @router.get("/citation-data", response_class=HTMLResponse)
236
+ async def get_citation_data(request: Request):
237
+ # Extract query parameters
238
+ user_id = request.query_params.get("userId")
239
+ topic = request.query_params.get("topic")
240
+ year = request.query_params.get("year")
241
+
242
+ if not user_id or not topic or not year:
243
+ raise HTTPException(status_code=400, detail="Missing required query parameters.")
244
+
245
+ # Build the query based on provided parameters
246
+ query = {"userId": user_id, "topic": topic, "year": int(year)}
247
+
248
+ collection = request.app.state.collection1
249
+ result = await collection.find_one(
250
+ query,
251
+ sort=[("scraped_on", -1)] # Sort by scraped_on in descending order to get the most recent
252
+ )
253
+
254
+ if not result:
255
+ raise HTTPException(status_code=404, detail="No data found for the specified criteria")
256
+
257
+ # Convert ObjectId to string for JSON serialization
258
+ result["_id"] = str(result["_id"])
259
+
260
+ data = result["papers"]
261
+ if not data:
262
+ raise HTTPException(status_code=404, detail="No papers found in the database.")
263
+
264
+ data_json = json.dumps(data)
265
+
266
+ return templates.TemplateResponse("gra.html", {"request": request, "data": data_json})
267
+
268
+
269
+ @router.post("/check-data-exists-citation/")
270
+ async def check_data_exists(data_request: CitationAnalysisRequest, saveRequest: Request):
271
+ # Build the query based on provided parameters
272
+ query = {
273
+ "userId": data_request.userId,
274
+ "topic": data_request.topic,
275
+ "year": data_request.year
276
+ }
277
+
278
+ # Access the MongoDB collection from the app state
279
+ collection = saveRequest.app.state.collection1
280
+
281
+ # Check if a document matching the query exists
282
+ document = await collection.find_one(query)
283
+
284
+ # Return the result
285
+ if document:
286
+ return {
287
+ "exists": True,
288
+ "message": "Data found for the given userId, topic, and year."
289
+ }
290
+ else:
291
+ return {
292
+ "exists": False,
293
+ "message": "No data found for the given userId, topic, and year."
294
+ }
dbconnect.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dotenv import load_dotenv
2
+ import os
3
+ from motor.motor_asyncio import AsyncIOMotorClient
4
+ load_dotenv()
5
+
6
+
7
+ def db_Connect():
8
+ MONGO_URI = os.getenv("MONGO_URI")
9
+ DATABASE_NAME = "PaperLens_Db"
10
+ COLLECTION_NAME = "Trend_Analysis_Data"
11
+ COLLECTION_NAME1 = "Citation_Analysis_Data"
12
+ COLLECTION_NAME2 = "Venue_Analysis_Data"
13
+ client = AsyncIOMotorClient(MONGO_URI)
14
+ db = client[DATABASE_NAME]
15
+ collection = db[COLLECTION_NAME]
16
+ collection1 = db[COLLECTION_NAME1]
17
+ collection2 = db[COLLECTION_NAME2]
18
+ return collection,collection1,collection2
19
+
20
+
21
+
22
+
23
+
static/css/dashboard.css ADDED
File without changes
static/css/gra.css ADDED
@@ -0,0 +1,548 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root{
2
+ --secondary-color: #fcee0a;
3
+ --text-color: #333;
4
+ --bg:#4dd9ca;
5
+ }
6
+ body {
7
+ font-family: Arial, sans-serif;
8
+ margin: 0;
9
+ padding: 0;
10
+ background-color:var( --text-color);
11
+ }
12
+ .concept-label {
13
+ font-size: 14px;
14
+ color: #555;
15
+ margin-bottom: 5px;
16
+ font-weight: 500;
17
+ }
18
+
19
+ #container {
20
+ width: 100%;
21
+ height: 100vh;
22
+ overflow: hidden;
23
+ }
24
+
25
+ svg {
26
+ width: 100%;
27
+ height: 100%;
28
+ }
29
+
30
+ .node {
31
+ cursor: pointer;
32
+ }
33
+
34
+ .link {
35
+ stroke-opacity: 0.6;
36
+ }
37
+
38
+ .tooltip {
39
+ position: absolute;
40
+ padding: 10px;
41
+ background-color: rgba(255, 255, 255, 0.95);
42
+ border: 1px solid #ddd;
43
+ border-radius: 5px;
44
+ pointer-events: none;
45
+ font-size: 14px;
46
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
47
+ max-width: 300px;
48
+ line-height: 1.4;
49
+ z-index: 1000;
50
+ }
51
+
52
+ .tooltip strong {
53
+ display: block;
54
+ margin-bottom: 5px;
55
+ font-size: 15px;
56
+ color: #333;
57
+ }
58
+
59
+ .tooltip em {
60
+ color: #4285f4;
61
+ font-style: italic;
62
+ }
63
+
64
+ #paperConcepts {
65
+ margin-top: 15px;
66
+ display: flex;
67
+ flex-wrap: wrap;
68
+ gap: 5px;
69
+ }
70
+
71
+ .controls {
72
+ position: absolute;
73
+ top: 20px;
74
+ left: 20px;
75
+ background-color: var(--bg);
76
+ padding: 15px;
77
+ border-radius: 5px;
78
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
79
+ display: flex;
80
+ flex-direction: column;
81
+ gap: 10px;
82
+ z-index: 100;
83
+ max-height: 80vh;
84
+ overflow-y: auto;
85
+ border: 8px solid #000; /* Add this line for a black border */
86
+ }
87
+
88
+ .control-section {
89
+ border-top: 1px solid #eee;
90
+ padding-top: 10px;
91
+ margin-top: 10px;
92
+ }
93
+
94
+ .control-section h4 {
95
+ margin-top: 0;
96
+ margin-bottom: 10px;
97
+ color: #333;
98
+ font-weight: bolder;
99
+ font-family: Georgia, 'Times New Roman', Times, serif;
100
+ }
101
+
102
+ .controls label {
103
+ display: flex;
104
+ align-items: center;
105
+ gap: 10px;
106
+ margin-bottom: 8px;
107
+ font-style: italic;
108
+ font-family:'Times New Roman', Times, serif;
109
+ }
110
+
111
+ .controls input {
112
+ width: 100px;
113
+ }
114
+
115
+ button {
116
+ margin-top: 10px;
117
+ padding: 8px 12px;
118
+ background-color: black;
119
+ color: white;
120
+ border: none;
121
+ border-radius: 4px;
122
+ cursor: pointer;
123
+ }
124
+
125
+ button:hover {
126
+ background-color: #3b78e7;
127
+ }
128
+
129
+ /* Improved legend styling */
130
+ .legend {
131
+ position: absolute;
132
+ right: 20px;
133
+ top: 20px;
134
+ background-color: var(--bg);
135
+ padding: 15px;
136
+ border-radius: 5px;
137
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
138
+ z-index: 100;
139
+ max-width: 250px;
140
+ border: 8px solid #000; /* Add this line for a black border */
141
+ }
142
+
143
+ .legend h3 {
144
+ margin-top: 0;
145
+ margin-bottom: 12px;
146
+ font-family: Georgia, 'Times New Roman', Times, serif;
147
+ }
148
+
149
+
150
+ .legend-item {
151
+ display: flex;
152
+ align-items: center;
153
+ margin-bottom: 10px;
154
+ height: 24px; /* Standardize height for all items */
155
+ font-family: 'Times New Roman', Times, serif;
156
+ }
157
+
158
+ /* Standardize all legend indicators */
159
+ .legend-indicator {
160
+ width: 32px; /* Increased from 24px */
161
+ height: 32px; /* Increased from 24px */
162
+ margin-right: 10px;
163
+ display: flex;
164
+ align-items: center;
165
+ justify-content: center;
166
+ flex-shrink: 0;
167
+ }
168
+
169
+ /* Specific styles for different shapes */
170
+ .legend-circle {
171
+ border-radius: 50%;
172
+ }
173
+
174
+ .legend-circle.main {
175
+ width: 16px;
176
+ height: 16px;
177
+ background-color: #4285f4;
178
+ }
179
+
180
+ .legend-circle.referenced {
181
+ width: 12px;
182
+ height: 12px;
183
+ background-color: #34a853;
184
+ }
185
+
186
+ .legend-circle.citing {
187
+ width: 12px;
188
+ height: 12px;
189
+ background-color: #ea4335;
190
+ }
191
+
192
+ .legend-diamond {
193
+ width: 12px;
194
+ height: 12px;
195
+ background-color: #fbbc05;
196
+ transform: rotate(45deg);
197
+ }
198
+
199
+ .legend-star svg {
200
+ width: 20px;
201
+ height: 20px;
202
+ }
203
+
204
+ .legend-arrow {
205
+ width: 50px;
206
+ height: 20px;
207
+ }
208
+
209
+ .legend-info {
210
+ font-weight: bold;
211
+ color: #555;
212
+ margin-top: 10px;
213
+ padding-top: 10px;
214
+ border-top: 1px solid #eee;
215
+ font-family: 'Gill Sans', 'Gill Sans MT', Calibri, 'Trebuchet MS', sans-serif;
216
+ }
217
+
218
+ .legend-info p {
219
+ margin: 5px 0;
220
+ }
221
+
222
+
223
+ #loadingIndicator {
224
+ position: absolute;
225
+ top: 50%;
226
+ left: 50%;
227
+ transform: translate(-50%, -50%);
228
+ background-color: rgba(255, 255, 255, 0.9);
229
+ padding: 20px;
230
+ border-radius: 5px;
231
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
232
+ z-index: 1000;
233
+ display: none;
234
+ }
235
+
236
+ /* New styles for paper details panel */
237
+ .paper-details-panel {
238
+ position: absolute;
239
+ top: 20px;
240
+ right: 20px;
241
+ background-color: white;
242
+ width: 350px;
243
+ max-height: 80vh;
244
+ border-radius: 5px;
245
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
246
+ z-index: 200;
247
+ display: none;
248
+ overflow: hidden;
249
+ transition: transform 0.3s ease;
250
+ }
251
+
252
+ .panel-header {
253
+ display: flex;
254
+ justify-content: space-between;
255
+ align-items: center;
256
+ padding: 12px 15px;
257
+ background-color: #4285f4;
258
+ color: white;
259
+ }
260
+
261
+ .panel-header h3 {
262
+ margin: 0;
263
+ font-size: 16px;
264
+ }
265
+
266
+ .close-button {
267
+ background: none;
268
+ border: none;
269
+ color: white;
270
+ font-size: 24px;
271
+ cursor: pointer;
272
+ margin: 0;
273
+ padding: 0;
274
+ }
275
+
276
+ .panel-content {
277
+ padding: 15px;
278
+ overflow-y: auto;
279
+ max-height: calc(80vh - 50px);
280
+ }
281
+
282
+ #paperInfo {
283
+ margin-bottom: 20px;
284
+ padding-bottom: 15px;
285
+ border-bottom: 1px solid #eee;
286
+ }
287
+
288
+ #paperTitle {
289
+ margin-top: 0;
290
+ margin-bottom: 10px;
291
+ font-size: 16px;
292
+ color: #333;
293
+ }
294
+
295
+ #paperType, #paperCitations {
296
+ margin: 5px 0;
297
+ font-size: 14px;
298
+ color: #555;
299
+ }
300
+
301
+ #paperConcepts {
302
+ margin-top: 10px;
303
+ }
304
+
305
+ .concept-tag {
306
+ display: inline-block;
307
+ padding: 4px 8px;
308
+ margin: 3px;
309
+ background-color: #e8f0fe;
310
+ color: #4285f4;
311
+ border-radius: 12px;
312
+ font-size: 12px;
313
+ }
314
+
315
+ .paper-link {
316
+ margin-top: 15px;
317
+ }
318
+
319
+ .paper-link a {
320
+ color: #4285f4;
321
+ text-decoration: none;
322
+ font-weight: bold;
323
+ }
324
+
325
+ .paper-link a:hover {
326
+ text-decoration: underline;
327
+ }
328
+
329
+ .paper-lists {
330
+ display: flex;
331
+ flex-direction: column;
332
+ gap: 20px;
333
+ }
334
+
335
+ .paper-list h5 {
336
+ margin-top: 0;
337
+ margin-bottom: 10px;
338
+ font-size: 14px;
339
+ color: #333;
340
+ }
341
+
342
+ .paper-list ul {
343
+ margin: 0;
344
+ padding: 0;
345
+ list-style: none;
346
+ max-height: 200px;
347
+ overflow-y: auto;
348
+ }
349
+
350
+ .paper-list li {
351
+ margin-bottom: 8px;
352
+ padding-bottom: 8px;
353
+ border-bottom: 1px solid #f0f0f0;
354
+ }
355
+
356
+ .paper-list li:last-child {
357
+ border-bottom: none;
358
+ }
359
+
360
+ .paper-list a {
361
+ color: #4285f4;
362
+ text-decoration: none;
363
+ font-size: 13px;
364
+ display: block;
365
+ }
366
+
367
+ .paper-list a:hover {
368
+ text-decoration: underline;
369
+ }
370
+
371
+ .paper-type-label {
372
+ display: inline-block;
373
+ padding: 2px 6px;
374
+ margin-right: 5px;
375
+ border-radius: 3px;
376
+ font-size: 11px;
377
+ color: white;
378
+ }
379
+
380
+ .paper-type-main {
381
+ background-color: #4285f4;
382
+ }
383
+
384
+ .paper-type-reference {
385
+ background-color: #34a853;
386
+ }
387
+
388
+ .paper-type-citation {
389
+ background-color: #ea4335;
390
+ }
391
+
392
+ .paper-type-bridge {
393
+ background-color: #fbbc05;
394
+ }
395
+
396
+ /* Make the legend move up when details panel is open */
397
+ .paper-details-panel.active + .legend {
398
+ top: calc(80vh + 30px);
399
+ }
400
+
401
+ /* Selected node styling */
402
+ .node.selected circle,
403
+ .node.selected rect,
404
+ .node.selected path {
405
+ stroke: #000;
406
+ stroke-width: 2px;
407
+ }
408
+
409
+ /* Responsive design for smaller screens */
410
+ @media (max-width: 768px) {
411
+ .paper-details-panel {
412
+ width: 100%;
413
+ max-width: none;
414
+ top: auto;
415
+ bottom: 0;
416
+ right: 0;
417
+ max-height: 50vh;
418
+ border-radius: 5px 5px 0 0;
419
+ transform: translateY(100%);
420
+ }
421
+
422
+ .paper-details-panel.active {
423
+ transform: translateY(0);
424
+ }
425
+
426
+ .controls {
427
+ max-width: 250px;
428
+ }
429
+
430
+ .legend {
431
+ max-width: 200px;
432
+ }
433
+ }
434
+ /*
435
+ /* Add to gra.css */
436
+ /* .loading-section {
437
+ position: absolute;
438
+ top: 20px;
439
+ left: 20px;
440
+ background-color: white;
441
+ padding: 15px;
442
+ border-radius: 5px;
443
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
444
+ z-index: 100;
445
+ width: 300px;
446
+ } */
447
+
448
+ #statusMessage {
449
+ margin-bottom: 10px;
450
+ font-weight: bold;
451
+ }
452
+
453
+ .progress-container {
454
+ width: 100%;
455
+ height: 10px;
456
+ background-color: #f0f0f0;
457
+ border-radius: 5px;
458
+ overflow: hidden;
459
+ }
460
+
461
+ .progress-bar {
462
+ height: 100%;
463
+ width: 0%;
464
+ background-color: #4285f4;
465
+ transition: width 0.3s ease;
466
+ }
467
+
468
+
469
+
470
+
471
+
472
+ .time-navigation {
473
+ position: absolute;
474
+ bottom: 20px;
475
+ left: 50%;
476
+ transform: translateX(-50%);
477
+ background-color: var(--bg);
478
+ padding: 12px 15px;
479
+ border-radius: 5px;
480
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
481
+ display: flex;
482
+ align-items: center;
483
+ z-index: 100;
484
+ border: 8px solid #000; /* Add this line for a black border */
485
+ }
486
+
487
+ .time-navigation button {
488
+ margin: 0 10px;
489
+ padding: 8px 12px;
490
+ background-color: black;
491
+ color: white;
492
+ border: none;
493
+ border-radius: 4px;
494
+ cursor: pointer;
495
+ }
496
+
497
+ .time-navigation button:disabled {
498
+ background-color: black;
499
+ cursor: not-allowed;
500
+ }
501
+
502
+ .time-indicator {
503
+ font-weight: bold;
504
+ margin: 0 15px;
505
+ }
506
+
507
+ .time-slider-container {
508
+ display: flex;
509
+ flex-direction: column;
510
+ width: 400px;
511
+ }
512
+
513
+ .time-slider {
514
+ width: 100%;
515
+ margin: 5px 0;
516
+ }
517
+
518
+ .time-labels {
519
+ display: flex;
520
+ justify-content: space-between;
521
+ font-size: 12px;
522
+ color: #666;
523
+ }
524
+
525
+ .time-loader {
526
+ height: 3px;
527
+ background-color: #4285f4;
528
+ width: 0%;
529
+ transition: width 0.3s;
530
+ margin-top: 5px;
531
+ }
532
+
533
+ /* Animation for transitions */
534
+ .node.exiting {
535
+ opacity: 0;
536
+ transition: opacity 0.5s;
537
+ }
538
+
539
+ .node.entering {
540
+ opacity: 0;
541
+ animation: fadeIn 0.5s forwards;
542
+ animation-delay: 0.3s;
543
+ }
544
+
545
+ @keyframes fadeIn {
546
+ from { opacity: 0; }
547
+ to { opacity: 1; }
548
+ }
static/css/homestyle.css ADDED
@@ -0,0 +1,1967 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import url('https://fonts.googleapis.com/css2?family=Bungee+Shade&display=swap');
2
+ :root {
3
+ --primary-color: #4a90e2;
4
+ --secondary-color: #fcee0a;
5
+ --third-color: #6f42c1;
6
+ --text-color: #333;
7
+ --bg-color: #f5f7fa;
8
+ --white: #ffffff;
9
+ --forth-color: #00BFFF;
10
+ }
11
+
12
+ * {
13
+ margin: 0;
14
+ padding: 0;
15
+ box-sizing: border-box;
16
+ }
17
+
18
+ body {
19
+ font-family: 'Poppins', sans-serif;
20
+ line-height: 1.6;
21
+ background: var(--secondary-color);
22
+ color: var(--text-color);
23
+ }
24
+
25
+ header {
26
+ color: var(--secondary-color);
27
+ position: fixed;
28
+ width: 100%;
29
+ top: 0;
30
+ z-index: 1000;
31
+ display: flex;
32
+ justify-content: space-between;
33
+ align-items: center;
34
+ padding: 1rem 2%;
35
+ backdrop-filter: blur(10px); /* Optional: nice blur effect */
36
+ }
37
+
38
+ .header-content {
39
+ display: flex;
40
+ justify-content: space-between;
41
+ align-items: center;
42
+ width: 100%;
43
+ max-width: 1200px;
44
+ margin: 0 auto;
45
+ /* background-color: #fff; */
46
+ flex-wrap: wrap; /* Allow wrapping on smaller screens */
47
+
48
+ }
49
+
50
+
51
+ .logo-container {
52
+ display: flex;
53
+ align-items: center;
54
+ gap: 10px;
55
+ border-radius: 30px;
56
+ background-color: rgb(7, 7, 7);
57
+ padding: 10px 30px;
58
+ width: fit-content;
59
+ position: relative;
60
+ }
61
+
62
+ .logo-container::before {
63
+ content: "";
64
+ position: absolute;
65
+ top: 13px;
66
+ left: 13px;
67
+ right: 13px;
68
+ bottom: 13px;
69
+ border-radius: 26px;
70
+ border: 8px solid #faf8f8;
71
+ pointer-events: none;
72
+ }
73
+
74
+
75
+ #appname {
76
+ display: flex;
77
+ align-items: center;
78
+ font-size: clamp(1.2rem, 3vw, 2rem); /* Responsive font size */
79
+ white-space: nowrap;
80
+ margin-right: auto; /* Push other elements to the right */
81
+ color: #00bcd4;
82
+
83
+ }
84
+
85
+ /* Ensure header text remains visible */
86
+ .header-content h1#appname {
87
+ position: relative;
88
+ z-index: 2;
89
+ }
90
+
91
+ nav {
92
+ display: flex;
93
+ gap: clamp(1rem, 3vw, 3rem); /* Wider gaps between items */
94
+ align-items: center;
95
+ clip-path: polygon(0 0, 95% 0, 100% 100%, 5% 100%); /* More dramatic angle */
96
+ background-color: #0b0b0b; /* Olive color like in Image 2 */
97
+ padding: 0.4rem 2rem; /* Balanced padding */
98
+ height: 38px; /* Slimmer height */
99
+ width: fit-content; /* Only as wide as needed */
100
+ }
101
+
102
+
103
+ nav a:hover {
104
+ color: rgba(255, 255, 255, 0.8);
105
+ transform: translateY(-2px);
106
+ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
107
+ }
108
+
109
+ nav a {
110
+ color: white;
111
+ text-decoration: none;
112
+ font-weight: bold;
113
+ text-transform: uppercase; /* Match the all-caps style in Image 2 */
114
+ font-size: 0.85rem;
115
+ letter-spacing: 0.5px;
116
+ height: 100%;
117
+ display: flex;
118
+ align-items: center;
119
+ }
120
+
121
+
122
+ nav a::after {
123
+ content: '';
124
+ position: absolute;
125
+ bottom: 0;
126
+ left: 0;
127
+ width: 0;
128
+ height: 2px;
129
+ background: var(--white);
130
+
131
+ transition: width 0.3s ease;
132
+ }
133
+
134
+ nav a:hover::after {
135
+ width: 100%;
136
+ background-color: #00bcd4;
137
+ }
138
+
139
+
140
+ .sign-in-btn {
141
+ background: rgb(35, 204, 216);
142
+ color: white;
143
+ border: none;
144
+ padding: 0.2rem 1.5rem;
145
+ border-radius: 25px;
146
+ cursor: pointer;
147
+ transition: all 0.3s ease;
148
+ font-size: 0.95rem;
149
+ letter-spacing: 0.5px;
150
+ box-shadow: 0 4px 10px rgba(35, 204, 216, 0.3);
151
+ position: relative;
152
+ overflow: hidden;
153
+ text-transform: none;
154
+ bottom: 5px;
155
+ }
156
+
157
+ .sign-in-btn:hover {
158
+ background: rgb(28, 175, 185);
159
+ transform: translateY(-2px);
160
+ box-shadow: 0 6px 12px rgba(174, 35, 216, 0.4);
161
+ }
162
+
163
+ /* Active/click effect */
164
+ .sign-in-btn:active {
165
+ transform: translateY(1px); /* Push down slightly when clicked */
166
+ box-shadow: 0 2px 8px rgba(201, 35, 216, 0.4); /* Reduced shadow */
167
+ }
168
+
169
+ /* Optional: Add a subtle shine/gleam effect */
170
+ .sign-in-btn::before {
171
+ content: '';
172
+ position: absolute;
173
+ top: -50%;
174
+ left: -50%;
175
+ width: 200%;
176
+ height: 200%;
177
+ background: rgba(255, 255, 255, 0.1);
178
+ transform: rotate(45deg);
179
+ opacity: 0;
180
+ transition: opacity 0.5s;
181
+ }
182
+
183
+ .sign-in-btn:hover::before {
184
+ opacity: 1;
185
+ animation: shine 1.5s ease-out;
186
+ }
187
+
188
+ @keyframes shine {
189
+ 0% {
190
+ transform: translateX(-100%) rotate(45deg);
191
+ }
192
+ 100% {
193
+ transform: translateX(100%) rotate(45deg);
194
+ }
195
+ }
196
+
197
+ #signInBtn {
198
+ display: block;
199
+ }
200
+
201
+
202
+
203
+
204
+ /* Hero Section */
205
+ .hero {
206
+ position: relative;
207
+ padding: 8rem 5% 4rem;
208
+ text-align: center;
209
+ background: linear-gradient(135deg, var(--secondary-color), var(--secondary-color));
210
+ color: var(--white);
211
+ overflow: hidden;
212
+ }
213
+
214
+ .hero h2 {
215
+ font-size: 2.5rem;
216
+ margin-bottom: 1rem;
217
+ font-family: 'Bungee Shade', cursive;
218
+ font-weight: 700;
219
+ color: #0b0b0b;
220
+ }
221
+
222
+ /* Lower text with paper effect */
223
+ .liquid-container {
224
+ position: relative;
225
+ height: 150px;
226
+ margin: 0 auto;
227
+ perspective: 1000px;
228
+ }
229
+
230
+ .liquid-word {
231
+ position: absolute;
232
+ width: 100%;
233
+ top: 0;
234
+ left: 0;
235
+ opacity: 0;
236
+ font-size: 0; /* Start with 0 size */
237
+ text-align: center;
238
+ transform-style: preserve-3d;
239
+ transition: opacity 0.5s ease;
240
+ }
241
+
242
+ .liquid-word.active {
243
+ opacity: 1;
244
+ }
245
+
246
+ .liquid-char {
247
+ display: inline-block;
248
+ font-size: 55px;
249
+ color: #00CED1; /* Base color */
250
+ -webkit-text-stroke: 4px #008B8B; /* Outline */
251
+ text-shadow:
252
+ -2px -2px 0 #fff,
253
+ 2px -2px 0 #fff,
254
+ -2px 2px 0 #fff,
255
+ 2px 2px 0 #fff,
256
+ 0px 4px 0 #006666, /* Inner shadow */
257
+ 4px 4px 10px rgba(0,0,0,0.3); /* Outer shadow */
258
+ transform: translateY(100px) scale(0);
259
+ opacity: 0;
260
+ position: relative;
261
+ }
262
+
263
+ /* Paper pieces/confetti */
264
+ .paper-piece {
265
+ position: absolute;
266
+ background: #fff;
267
+ box-shadow: 0 2px 4px rgba(0,0,0,0.2);
268
+ opacity: 0;
269
+ z-index: -1;
270
+ transform-origin: center;
271
+ transition: all 0.3s ease;
272
+ }
273
+
274
+ /* Paper drop effect (instead of drip) */
275
+ .paper-drop {
276
+ position: absolute;
277
+ bottom: -20px;
278
+ left: 50%;
279
+ width: 15px;
280
+ height: 20px;
281
+ background: #fff;
282
+ transform: translateX(-50%) scale(0);
283
+ transform-origin: top center;
284
+ box-shadow: 0 2px 4px rgba(0,0,0,0.2);
285
+ clip-path: polygon(0% 0%, 100% 0%, 100% 70%, 50% 100%, 0% 70%);
286
+ }
287
+
288
+ /* Paper splash container */
289
+ .splash-container {
290
+ position: absolute;
291
+ width: 100%;
292
+ height: 100%;
293
+ pointer-events: none;
294
+ z-index: -1;
295
+ }
296
+
297
+ /* Animations */
298
+ @keyframes paperDrop {
299
+ 0% { transform: translateX(-50%) scale(0); }
300
+ 40% { transform: translateX(-50%) scale(1.2); }
301
+ 60% { transform: translateX(-50%) scale(0.8); }
302
+ 80% { transform: translateX(-50%) scale(1.1); }
303
+ 100% { transform: translateX(-50%) scale(1); }
304
+ }
305
+
306
+ @keyframes paperSplash {
307
+ 0% { transform: scale(0) rotate(0deg); opacity: 0.9; }
308
+ 50% { opacity: 0.7; }
309
+ 100% { transform: scale(1.5) rotate(var(--rotation)); opacity: 0; }
310
+ }
311
+
312
+ @keyframes paperIn {
313
+ 0% { transform: translateY(100px) scale(0) rotate(10deg); opacity: 0; }
314
+ 40% { transform: translateY(-20px) scale(1.2) rotate(-5deg); opacity: 1; }
315
+ 60% { transform: translateY(10px) scale(0.9) rotate(3deg); opacity: 1; }
316
+ 80% { transform: translateY(-5px) scale(1.05) rotate(-2deg); opacity: 1; }
317
+ 100% { transform: translateY(0) scale(1) rotate(0deg); opacity: 1; }
318
+ }
319
+
320
+ @keyframes paperOut {
321
+ 0% { transform: translateY(0) scale(1) rotate(0deg); opacity: 1; }
322
+ 20% { transform: translateY(-10px) scale(1.1) rotate(-5deg); opacity: 0.8; }
323
+ 100% { transform: translateY(-100px) scale(0) rotate(10deg); opacity: 0; }
324
+ }
325
+
326
+ @keyframes wobble {
327
+ 0%, 100% { transform: translateY(0) rotate(0deg); }
328
+ 25% { transform: translateY(-5px) rotate(-2deg); }
329
+ 75% { transform: translateY(3px) rotate(2deg); }
330
+ }
331
+
332
+ .title-char {
333
+ display: inline-block;
334
+ animation: wobble 4s ease-in-out infinite;
335
+ animation-delay: calc(var(--i) * 0.1s);
336
+ }
337
+
338
+ /* Search Container */
339
+ .search-container {
340
+ max-width: 600px;
341
+ margin: 2rem auto;
342
+ width: fit-content;
343
+ border-radius: 40px;
344
+ display: flex;
345
+ justify-content: center;
346
+ align-items: center;
347
+ gap: 1rem;
348
+ background: var(--white);
349
+ padding: 0.5rem;
350
+ box-shadow: 0 8px 15px rgba(0, 0, 0, 0.1);
351
+ overflow: hidden;
352
+ transition: all 0.3s ease;
353
+ position: relative;
354
+ }
355
+
356
+ .search-container .searchBox {
357
+ margin: 0;
358
+ }
359
+
360
+ .searchBox {
361
+ position: relative;
362
+ display: flex;
363
+ background: var(--secondary-color);
364
+ height: 60px;
365
+ border-radius: 40px;
366
+ padding: 10px;
367
+ margin: 0 auto;
368
+ width: fit-content;
369
+ z-index: 10;
370
+ }
371
+
372
+ .searchBox:hover > .searchInput {
373
+ width: 290px;
374
+ padding: 0 6px;
375
+ color: #0b0b0b;
376
+ font-weight: bold;
377
+ }
378
+
379
+ .searchBox:hover > .searchButton {
380
+ background: white;
381
+ color: #2f3640;
382
+ }
383
+
384
+ #searchbtn {
385
+ color: white;
386
+ width: 40px;
387
+ height: 40px;
388
+ border-radius: 50%;
389
+ background: rgb(13, 223, 220);
390
+ display: flex;
391
+ justify-content: center;
392
+ align-items: center;
393
+ transition: 0.4s;
394
+ cursor: pointer;
395
+ margin-top: -2px;
396
+ font-size: 14px;
397
+
398
+ }
399
+
400
+ .searchInput {
401
+ border: none;
402
+ background: none;
403
+ outline: none;
404
+ float: left;
405
+ padding: 0;
406
+ color: white;
407
+ font-size: 16px;
408
+ transition: 0.4s;
409
+ line-height: 40px;
410
+ width: 0px;
411
+ }
412
+
413
+ /* Year Filter Visibility */
414
+ .filter-container {
415
+ display: none; /* Hidden by default */
416
+ transition: opacity 0.3s ease, visibility 0.3s ease;
417
+ }
418
+
419
+ .searchBox:hover .filter-container {
420
+ display: flex; /* Visible on hover */
421
+ opacity: 1;
422
+ visibility: visible;
423
+ }
424
+
425
+ /* Base Styling for Year Filter */
426
+ .year-filter {
427
+ position: relative;
428
+ height: 40px;
429
+ padding: 0.6rem 1.5rem;
430
+ border: none;
431
+ background: #f5f5f5;
432
+ color: #333;
433
+ font-family: 'Poppins', sans-serif;
434
+ font-size: 14px;
435
+ border-radius: 40px;
436
+ outline: none;
437
+ cursor: pointer;
438
+ transition: all 0.3s ease;
439
+ appearance: none; /* Remove default arrow */
440
+ -webkit-appearance: none;
441
+ -moz-appearance: none;
442
+ background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="12" height="6" viewBox="0 0 12 6"><path fill="%234a4a4a" d="M0 0h12L6 6z"/></svg>');
443
+ background-repeat: no-repeat;
444
+ background-position: right 1rem center;
445
+ }
446
+
447
+ .year-filter:hover,
448
+ .year-filter:focus {
449
+ background-color: #e0e0e0;
450
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
451
+ }
452
+
453
+ @media screen and (max-width: 620px) {
454
+ .searchBox:hover > .searchInput {
455
+ width: 150px;
456
+ padding: 0 6px;
457
+ }
458
+ }
459
+ /* Submit Button */
460
+ .search-container button {
461
+ background: var(--secondary-color);
462
+ color: var(--white);
463
+ border: none;
464
+ padding: 0.8rem 2rem;
465
+ border-radius: 5px;
466
+ cursor: pointer;
467
+ font-size: 1rem;
468
+ transition: all 0.3s ease;
469
+ position: relative;
470
+ overflow: hidden;
471
+ }
472
+
473
+ /* Hover effect for the submit button */
474
+ .search-container button::before {
475
+ content: '';
476
+ position: absolute;
477
+ top: 0;
478
+ left: -100%;
479
+ width: 100%;
480
+ height: 100%;
481
+ background: rgba(255, 255, 255, 0.2);
482
+ transition: left 0.4s ease;
483
+ }
484
+
485
+ .search-container button:hover::before {
486
+ left: 100%;
487
+ }
488
+
489
+ .search-container button:hover {
490
+ background: #5a37a8;
491
+ transform: scale(1.05);
492
+ }
493
+
494
+ /* Responsive Design */
495
+ @media (max-width: 768px) {
496
+ .search-container {
497
+ flex-direction: column;
498
+ gap: 0.5rem;
499
+ }
500
+
501
+ .filter-container {
502
+ display: none;
503
+ transition: opacity 0.3s ease, visibility 0.3s ease;
504
+ width: 100%;
505
+ margin: 0.5rem 0;
506
+ }
507
+
508
+ .year-filter {
509
+ width: 100%;
510
+ }
511
+
512
+ .search-container button {
513
+ width: 100%;
514
+ }
515
+ }
516
+
517
+
518
+
519
+
520
+
521
+ /* architecture Section
522
+ .architecture {
523
+ background: linear-gradient(135deg, var( --text-color), var( --text-color));
524
+ color: var(--white);
525
+ padding: 4rem 5%;
526
+ } */
527
+
528
+ .architecture {
529
+ background: linear-gradient(135deg, var(--text-color), var(--text-color));
530
+ color: var(--white);
531
+ padding: 4rem 5%;
532
+ border: 8px solid black;
533
+ border-radius: 10px; /* Adding some border radius for a nicer look */
534
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
535
+ position: relative;
536
+ top: 20px;
537
+ margin-left: 20px;
538
+ margin-right: 10px;
539
+ width: calc(100% - 40px); /* Accounting for the left and right margins */
540
+ }
541
+
542
+
543
+
544
+
545
+
546
+ #contacts {
547
+ padding-top: 40px;
548
+ margin-left: 20px;
549
+ margin-right: 20px;
550
+ position: relative;
551
+
552
+ }
553
+ #feedback {
554
+ margin-left: 20px;
555
+ margin-right: 20px;
556
+ position: relative;
557
+
558
+ }
559
+ /* Footer */
560
+ footer {
561
+ background: #2c3e50;
562
+ color: var(--white);
563
+ text-align: center;
564
+ padding: 2rem;
565
+ }
566
+
567
+
568
+ /* /* Profile Styling */
569
+ /* .profile-container {
570
+ display: flex;
571
+ align-items: center;
572
+ cursor: pointer;
573
+ position: relative;
574
+ margin-left: auto;
575
+ } */
576
+
577
+ .profile-pic {
578
+ width: 40px;
579
+ height: 40px;
580
+ border-radius: 50%;
581
+ margin-left: 10px;
582
+ }
583
+
584
+ .username {
585
+ color: white;
586
+ font-weight: bold;
587
+ }
588
+
589
+
590
+ .logout-btn {
591
+ background: red;
592
+ color: white;
593
+ padding: 5px 10px;
594
+ border: none;
595
+ cursor: pointer;
596
+ margin-top: 10px;
597
+ }
598
+
599
+
600
+
601
+ .orb {
602
+ position: absolute;
603
+ border-radius: 10px; /* Slightly rounded corners for a paper-like effect */
604
+ background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); /* Gradient using theme colors */
605
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1), 0 1px 3px rgba(0, 0, 0, 0.06); /* Subtle shadow for depth */
606
+ animation: floatOrb 10s ease-in-out infinite;
607
+ z-index: 0;
608
+ opacity: 0.3;
609
+ }
610
+
611
+ .orb:nth-child(1) {
612
+ width: 200px;
613
+ height: 200px;
614
+ top: 30%;
615
+ left: 10%;
616
+ animation-delay: 0s;
617
+ background: var(--forth-color); /* Gradient for a slight texture */
618
+ }
619
+
620
+ .orb:nth-child(2) {
621
+ width: 150px;
622
+ height: 150px;
623
+ top: 60%;
624
+ right: 15%;
625
+ animation-delay: -2s;
626
+ background: var(--third-color); /* Slightly different gradient */
627
+ }
628
+
629
+ .orb:nth-child(3) {
630
+ width: 100px;
631
+ height: 100px;
632
+ top: 30%;
633
+ right: 25%;
634
+ animation-delay: -4s;
635
+ background: var(--secondary-color); /* Another variation of theme texture */
636
+ }
637
+
638
+
639
+ /* Orb Styles for Features Section */
640
+ .orb1 {
641
+ position: absolute;
642
+ border-radius: 10px; /* Slightly rounded corners for a paper-like effect */
643
+ background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); /* Gradient using theme colors */
644
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1), 0 1px 3px rgba(0, 0, 0, 0.06); /* Subtle shadow for depth */
645
+ animation: floatOrb 10s ease-in-out infinite;
646
+ z-index: 0;
647
+ opacity: 0.3;
648
+ }
649
+
650
+ .orb1:nth-child(1) {
651
+ width: 150px;
652
+ height: 150px;
653
+ top: 10%;
654
+ left: 30%;
655
+ animation-delay: 0s;
656
+ background: var(--third-color); /* Gradient for a slight texture */
657
+ }
658
+
659
+ .orb1:nth-child(2) {
660
+ width: 100px;
661
+ height: 100px;
662
+ top: 60%;
663
+ right: 15%;
664
+ animation-delay: -2s;
665
+ background: var(--forth-color); /* Slightly different gradient */
666
+ }
667
+
668
+
669
+ @keyframes floatOrb {
670
+ 0%, 100% { transform: translate(0, 0); }
671
+ 50% { transform: translate(-20px, -20px); }
672
+ }
673
+
674
+ @keyframes floatOrb {
675
+ 0%, 100% { transform: translate(0, 0); }
676
+ 50% { transform: translate(-20px, -20px); }
677
+ }
678
+
679
+ .hero-content {
680
+ position: relative; /* Ensure content is above orbs */
681
+ z-index: 1; /* Content should be above orbs */
682
+ }
683
+
684
+
685
+ .paper {
686
+ position: fixed;
687
+ pointer-events: none;
688
+ transform-origin: center center;
689
+ filter: blur(1px);
690
+ mix-blend-mode: screen;
691
+ background: white;
692
+ top: 0;
693
+ left: 0;
694
+ }
695
+
696
+ .glow-wrapper {
697
+ position: relative;
698
+ flex: 1;
699
+ }
700
+
701
+ .glow {
702
+ position: absolute;
703
+ top: -2px;
704
+ left: -2px;
705
+ right: -2px;
706
+ bottom: -2px;
707
+ background: linear-gradient(90deg,
708
+ var(--primary-color), var(--secondary-color), var(--third-color), var(--forth-color),
709
+ var(--primary-color), var(--secondary-color), var(--third-color));
710
+ border-radius: 0px; /* Make it square */
711
+ z-index: 0;
712
+ background-size: 400%;
713
+ animation: animate 20s linear infinite;
714
+ }
715
+
716
+ .glow::before {
717
+ content: '';
718
+ position: absolute;
719
+ top: 2px;
720
+ left: 2px;
721
+ right: 2px;
722
+ bottom: 2px;
723
+ background: #f0f0f0;
724
+ border-radius: 0px; /* Ensure it's square */
725
+ z-index: 1;
726
+ transition: background-color 0.3s ease;
727
+ }
728
+
729
+
730
+
731
+ /* .search-input {
732
+ position: relative;
733
+ z-index: 2;
734
+ width: 100%;
735
+ border: none;
736
+ padding: 0.8rem;
737
+ border-radius: 25px;
738
+ outline: none;
739
+ background: transparent;
740
+ } */
741
+
742
+ .glow-wrapper:focus-within .glow::before {
743
+ background: var(--white);
744
+ }
745
+
746
+ @keyframes animate {
747
+ 0% { background-position: 0 0; }
748
+ 50% { background-position: 400% 0; }
749
+ 100% { background-position: 0 0; }
750
+ }
751
+
752
+ /* Remove the original box-shadow focus effect */
753
+ .search-container input:focus {
754
+ box-shadow: none;
755
+ }
756
+
757
+
758
+ .beam-container {
759
+ position: relative;
760
+ display: flex;
761
+ height: 500px;
762
+ width: 90vw;
763
+ max-width: 800px;
764
+ align-items: center;
765
+ justify-content: center;
766
+ overflow: hidden;
767
+ padding: 10px;
768
+ }
769
+
770
+ .content-wrapper {
771
+ display: flex;
772
+ width: 100%;
773
+ max-width: 32rem; /* equivalent to max-w-lg */
774
+ flex-direction: row;
775
+ align-items: center;
776
+ justify-content: space-between;
777
+ gap: 2.5rem; /* equivalent to gap-10 */
778
+ }
779
+
780
+ .left-column, .middle-column, .right-column {
781
+ display: flex;
782
+ flex-direction: column;
783
+ align-items: center;
784
+ }
785
+
786
+ .left-column {
787
+ gap: 0.5rem; /* equivalent to gap-2 */
788
+ }
789
+
790
+ .circle {
791
+ z-index: 10;
792
+ display: flex;
793
+ width: 3rem; /* size-12 */
794
+ height: 3rem;
795
+ align-items: center;
796
+ justify-content: center;
797
+ border-radius: 9999px;
798
+ border: 2px solid #e2e8f0;
799
+ background-color: white;
800
+ padding: 0.75rem;
801
+ box-shadow: 0 0 20px -12px rgba(0, 0, 0, 0.8);
802
+ }
803
+
804
+ .large-circle {
805
+ width: 4rem; /* size-16 */
806
+ height: 4rem;
807
+ }
808
+
809
+ .circle img {
810
+ max-width: 100%;
811
+ max-height: 100%;
812
+ }
813
+
814
+ /* SVG and beam styling */
815
+ .beams-svg {
816
+ position: absolute;
817
+ top: 0;
818
+ left: 0;
819
+ width: 100%;
820
+ height: 100%;
821
+ pointer-events: none;
822
+ }
823
+
824
+ .beam {
825
+ stroke: #e2e8f0;
826
+ stroke-width: 2;
827
+ stroke-dasharray: 3 3;
828
+ animation: dashOffset 5s linear infinite;
829
+ opacity: 0.7;
830
+ }
831
+
832
+ @keyframes dashOffset {
833
+ from {
834
+ stroke-dashoffset: 0;
835
+ }
836
+ to {
837
+ stroke-dashoffset: 100;
838
+ }
839
+ }
840
+
841
+ .beam-glow {
842
+ stroke: #60a5fa;
843
+ stroke-width: 4;
844
+ filter: blur(4px);
845
+ opacity: 0.5;
846
+ animation: pulse 2s ease-in-out infinite;
847
+ }
848
+
849
+ @keyframes pulse {
850
+ 0%, 100% {
851
+ opacity: 0.5;
852
+ }
853
+ 50% {
854
+ opacity: 0.2;
855
+ }
856
+ }
857
+
858
+ .container {
859
+ position: relative;
860
+ width: 800px;
861
+ height: 400px;
862
+ }
863
+
864
+
865
+ .center-box {
866
+ position: absolute;
867
+ left: 50%;
868
+ top: 50%;
869
+ transform: translate(-50%, -50%);
870
+ width: 80px;
871
+ height: 80px;
872
+ background: white; /* White background */
873
+ border-radius: 12px;
874
+ display: flex;
875
+ justify-content: center;
876
+ align-items: center;
877
+ z-index: 2;
878
+ box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.2);
879
+ }
880
+
881
+ .settings-icon-wrapper {
882
+ width: 40px;
883
+ height: 40px;
884
+ animation: rotate 8s linear infinite;
885
+ display: flex;
886
+ justify-content: center;
887
+ align-items: center;
888
+ }
889
+
890
+ .settings-icon-wrapper img {
891
+ width: 40px;
892
+ height: 40px;
893
+ }
894
+
895
+
896
+ @keyframes rotate {
897
+ from { transform: rotate(0deg); }
898
+ to { transform: rotate(360deg); }
899
+ }
900
+
901
+ .paper-container {
902
+ position: absolute;
903
+ display: flex;
904
+ align-items: center;
905
+ gap: 8px;
906
+ color: var(--white);
907
+ font-size: medium;
908
+ font-weight: bold;
909
+ transform: translateY(-50%);
910
+ z-index: 2;
911
+ }
912
+
913
+ .paper-icon {
914
+ width: 20px;
915
+ height: 24px;
916
+ position: relative;
917
+ background: white;
918
+ border-radius: 2px;
919
+ display: flex;
920
+ justify-content: center;
921
+ align-items: center;
922
+ font-size: 9px;
923
+ font-weight: bold;
924
+ color: #555;
925
+ }
926
+
927
+ .paper-icon:before {
928
+ content: '';
929
+ position: absolute;
930
+ top: 0;
931
+ right: 0;
932
+ border-width: 0 10px 10px 0;
933
+ border-style: solid;
934
+ border-color: #ddd #7f56d9;
935
+ }
936
+
937
+ .paths-container {
938
+ position: absolute;
939
+ top: 0;
940
+ left: 0;
941
+ width: 100%;
942
+ height: 100%;
943
+ z-index: 1;
944
+ }
945
+
946
+ .path {
947
+ fill: none;
948
+ stroke: rgba(255, 255, 255, 0.6);
949
+ stroke-width: 1;
950
+ stroke-opacity: 0; /* Make lines invisible initially */
951
+ stroke-dasharray: 0, 1000; /* Hide stroke */
952
+ transition: stroke-opacity 0.3s ease-in-out;
953
+ }
954
+
955
+
956
+ .moving-paper {
957
+ position: absolute;
958
+ width: 20px;
959
+ height: 24px;
960
+ background: white;
961
+ border-radius: 2px;
962
+ z-index: 2;
963
+ transform: translate(-50%, -50%);
964
+ display: flex;
965
+ justify-content: center;
966
+ align-items: center;
967
+ font-size: 9px;
968
+ font-weight: bold;
969
+ color: #555;
970
+ }
971
+
972
+ .moving-paper:before {
973
+ content: '';
974
+ position: absolute;
975
+ top: 0;
976
+ right: 0;
977
+ border-width: 0 10px 10px 0;
978
+ border-style: solid;
979
+ border-color: #ddd #7f56d9;
980
+ }
981
+
982
+ .moving-paper {
983
+ z-index: 1;
984
+ }
985
+
986
+ .moving-dot {
987
+ position: absolute;
988
+ width: 8px;
989
+ height: 8px;
990
+ background: #4a90e2;
991
+ border-radius: 50%;
992
+ z-index: 1;
993
+ transform: translate(-50%, -50%);
994
+ }
995
+
996
+ .output-box {
997
+ position: absolute;
998
+ width: 35px;
999
+ height: 35px;
1000
+ background: rgba(255, 255, 255, 0.05);
1001
+ border-radius: 8px;
1002
+ border: 0.5px solid rgba(255, 255, 255, 0.1);
1003
+ z-index: 2;
1004
+ right: 80px;
1005
+ transform: translateY(-50%);
1006
+ display: flex;
1007
+ justify-content: center;
1008
+ align-items: center;
1009
+ }
1010
+
1011
+ .output-box.highlight {
1012
+ box-shadow: 0 0 15px #4a90e2;
1013
+ background: rgba(255, 255, 255, 0.1);
1014
+ }
1015
+
1016
+ .output-logo {
1017
+ border-radius: 8px;
1018
+ width: 35px;
1019
+ height: 35px;
1020
+ object-fit: fill;
1021
+ }
1022
+
1023
+ .box-title {
1024
+ position: absolute;
1025
+ bottom: -25px;
1026
+ margin-left: -25px;
1027
+ font-size: medium;
1028
+ font-weight: bold;
1029
+ color: var(--white);
1030
+ white-space: nowrap;
1031
+ text-align: center;
1032
+ width: 100%;
1033
+ }
1034
+
1035
+ .output-box {
1036
+ display: none;
1037
+ }
1038
+
1039
+ .center-box {
1040
+ transition: transform 0.3s ease-in-out;
1041
+ }
1042
+
1043
+ .center-box.pulse {
1044
+ transform: translate(-50%, -50%) scale(1.1);
1045
+ }
1046
+
1047
+ .edit-img {
1048
+ width: 20px; /* Adjust size */
1049
+ height: 20px;
1050
+ cursor: pointer;
1051
+ transition: transform 0.2s ease-in-out;
1052
+ }
1053
+
1054
+ .edit-img:hover {
1055
+ transform: scale(1.1);
1056
+ }
1057
+
1058
+
1059
+ /* Add these styles to your CSS file */
1060
+ .filter-container {
1061
+ position: relative;
1062
+ margin: 0 0.5rem;
1063
+ }
1064
+
1065
+ .year-filter {
1066
+ height: 100%;
1067
+ padding: 0.8rem;
1068
+ border: none;
1069
+ background: #f0f0f0;
1070
+ color: var(--text-color);
1071
+ font-family: 'Poppins', sans-serif;
1072
+ outline: none;
1073
+ cursor: pointer;
1074
+ transition: background-color 0.3s ease;
1075
+ appearance: none; /* Remove default arrow */
1076
+ -webkit-appearance: none;
1077
+ -moz-appearance: none;
1078
+ background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="12" height="6" viewBox="0 0 12 6"><path fill="%234a4a4a" d="M0 0h12L6 6z"/></svg>');
1079
+ background-repeat: no-repeat;
1080
+ background-position: right 0.8rem center;
1081
+ padding-right: 2rem;
1082
+ min-width: 120px;
1083
+ }
1084
+
1085
+ .year-filter:hover, .year-filter:focus {
1086
+ background-color: #e0e0e0;
1087
+ }
1088
+
1089
+ /* Adjust search container for the new filter */
1090
+ .search-container {
1091
+ align-items: center;
1092
+ }
1093
+
1094
+ /* Make the search container responsive */
1095
+ .logo-image {
1096
+ height: auto;
1097
+ max-height: 40px; /* Adjust based on your desired size */
1098
+ width: auto;
1099
+ margin-right: 1rem; /* Add space between logo and text */
1100
+ transition: all 0.3s ease; /* Smooth scaling */
1101
+ }
1102
+
1103
+ /* Make logo responsive */
1104
+ @media (max-width: 768px) {
1105
+ .logo-image {
1106
+ max-height: 30px; /* Smaller on mobile devices */
1107
+ }
1108
+ }
1109
+ @media (max-width: 768px) {
1110
+ .search-container {
1111
+ flex-direction: column;
1112
+ gap: 0.5rem;
1113
+ }
1114
+
1115
+ .filter-container {
1116
+ width: 100%;
1117
+ margin: 0.5rem 0;
1118
+ }
1119
+
1120
+ .year-filter {
1121
+ width: 100%;
1122
+ }
1123
+
1124
+ .search-container button {
1125
+ width: 100%;
1126
+ }
1127
+ }
1128
+
1129
+ /* Search Results Styling */
1130
+ .search-results {
1131
+ padding: 2rem 5%;
1132
+ background: var(--bg-color);
1133
+ min-height: 300px;
1134
+ }
1135
+
1136
+ .search-results h2 {
1137
+ color: var(--secondary-color);
1138
+ margin-bottom: 1.5rem;
1139
+ text-align: center;
1140
+ }
1141
+
1142
+ .paper-card {
1143
+ background: var(--white);
1144
+ border-radius: 10px;
1145
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
1146
+ padding: 1.5rem;
1147
+ margin-bottom: 1.5rem;
1148
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
1149
+ }
1150
+
1151
+ .paper-card:hover {
1152
+ transform: translateY(-5px);
1153
+ box-shadow: 0 10px 15px rgba(0, 0, 0, 0.1);
1154
+ }
1155
+
1156
+ .paper-card h3 {
1157
+ color: var(--primary-color);
1158
+ margin-bottom: 0.5rem;
1159
+ }
1160
+
1161
+ .paper-authors {
1162
+ color: var(--text-color);
1163
+ font-style: italic;
1164
+ margin-bottom: 0.5rem;
1165
+ }
1166
+
1167
+ .paper-year {
1168
+ display: inline-block;
1169
+ background: var(--secondary-color);
1170
+ color: white;
1171
+ padding: 0.2rem 0.5rem;
1172
+ border-radius: 4px;
1173
+ font-size: 0.9rem;
1174
+ margin-bottom: 1rem;
1175
+ }
1176
+
1177
+ .paper-abstract {
1178
+ color: var(--text-color);
1179
+ margin-bottom: 1rem;
1180
+ line-height: 1.6;
1181
+ }
1182
+
1183
+ .view-paper-btn {
1184
+ background: var(--primary-color);
1185
+ color: white;
1186
+ border: none;
1187
+ padding: 0.5rem 1rem;
1188
+ border-radius: 4px;
1189
+ cursor: pointer;
1190
+ transition: background-color 0.3s ease;
1191
+ }
1192
+
1193
+ .view-paper-btn:hover {
1194
+ background: var(--secondary-color);
1195
+ }
1196
+
1197
+ #input-paper {
1198
+ margin-bottom: 20px;
1199
+ }
1200
+
1201
+
1202
+
1203
+ .container {
1204
+ position: relative;
1205
+ width: 800px;
1206
+ height: 400px;
1207
+ margin: 0 auto; /* Add this to center the container horizontally */
1208
+ display: flex;
1209
+ flex-direction: column;
1210
+ align-items: center; /* This will center child elements horizontally */
1211
+ }
1212
+
1213
+
1214
+ .architecture-description {
1215
+ max-width: 800px;
1216
+ margin: 0 auto 40px auto;
1217
+ padding: 25px;
1218
+ background: var(--text-color);
1219
+ border-radius: 10px;
1220
+ backdrop-filter: blur(5px);
1221
+ border: 1px solid rgba(255, 255, 255, 0.2);
1222
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
1223
+ }
1224
+
1225
+ .architecture-description h3 {
1226
+ color: var(--secondary-color);
1227
+ font-size: 1.8rem;
1228
+ margin-bottom: 15px;
1229
+ text-shadow: 0 0 10px var(--third-color);
1230
+ font-family: 'Bungee Shade', cursive;
1231
+ text-align: center; /* Ensures the text inside the h3 is centered */
1232
+ }
1233
+ .architecture-description p {
1234
+ line-height: 1.7;
1235
+ margin-bottom: 15px;
1236
+ font-size: 1.05rem;
1237
+ text-transform: capitalize;
1238
+ font-style: italic;
1239
+ font-weight: 200;
1240
+ color:white;
1241
+ text-align: center;
1242
+ font-family: serif;
1243
+ }
1244
+
1245
+ .architecture-features {
1246
+ display: flex;
1247
+ justify-content: space-between;
1248
+ margin-top: 25px;
1249
+ flex-wrap: wrap;
1250
+ gap: 20px;
1251
+ }
1252
+
1253
+ .architecture-feature {
1254
+ flex: 1;
1255
+ min-width: 200px;
1256
+ background: rgba(111, 66, 193, 0.2);
1257
+ padding: 15px;
1258
+ border-radius: 8px;
1259
+ border-left: 3px solid var(--forth-color);
1260
+ }
1261
+
1262
+ .architecture-feature h4 {
1263
+ color: white;
1264
+ font-size: 1.2rem;
1265
+ margin-bottom: 10px;
1266
+ }
1267
+
1268
+ .architecture-feature p {
1269
+ color: rgba(255, 255, 255, 0.8);
1270
+ font-size: 0.95rem;
1271
+ }
1272
+
1273
+
1274
+ .feature-selection-container {
1275
+ display: flex;
1276
+ flex-direction: column;
1277
+ align-items: center;
1278
+ }
1279
+
1280
+ .feature-selection {
1281
+ position: relative;
1282
+ display: flex;
1283
+ justify-content: center;
1284
+ align-items: center;
1285
+ margin-top: 50px;
1286
+ }
1287
+
1288
+ .feature-selection .glass {
1289
+ position: relative;
1290
+ width: 180px;
1291
+ height: 200px;
1292
+ background: linear-gradient(var(--text-color), var(--text-color));
1293
+ border: 1px solid rgba(255, 255, 255, 0.1);
1294
+ box-shadow: 0 25px 25px rgba(0, 0, 0, 0.25);
1295
+ display: flex;
1296
+ justify-content: center;
1297
+ align-items: center;
1298
+ transition: 0.5s;
1299
+ border-radius: 10px;
1300
+ margin: 0 -45px;
1301
+ backdrop-filter: blur(10px);
1302
+ transform: rotate(calc(var(--r) * 1deg));
1303
+ }
1304
+
1305
+ .feature-selection:hover .glass {
1306
+ transform: rotate(0deg);
1307
+ margin: 0 10px;
1308
+ }
1309
+
1310
+ .feature-selection .glass::before {
1311
+ content: attr(data-text);
1312
+ position: absolute;
1313
+ bottom: 0;
1314
+ width: 100%;
1315
+ height: 60px;
1316
+ background: rgba(255, 255, 255, 0.05);
1317
+ display: flex;
1318
+ justify-content: center;
1319
+ align-items: center;
1320
+ color: #fff;
1321
+ }
1322
+
1323
+
1324
+
1325
+ .feature-selection .glass svg {
1326
+ font-size: 2.5em;
1327
+ fill: #fff;
1328
+ }
1329
+
1330
+
1331
+ .selection-message {
1332
+ text-align: center;
1333
+ margin-top: 30px;
1334
+ padding: 12px;
1335
+ background: var(--text-color);
1336
+ border-radius: 8px;
1337
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
1338
+ color: var(--secondary-color);
1339
+ font-weight: 500;
1340
+ opacity: 0;
1341
+ transform: translateY(20px);
1342
+ transition: all 0.4s ease;
1343
+ }
1344
+
1345
+ .selection-message.visible {
1346
+ opacity: 1;
1347
+ transform: translateY(0);
1348
+ }
1349
+
1350
+ .glass.selected {
1351
+ outline: 3px solid var(--secondary-color, #00bcd4);
1352
+ box-shadow: 0 0 15px rgba(0, 188, 212, 0.6);
1353
+ }
1354
+
1355
+
1356
+ .glass-icon {
1357
+ width: 60px; /* or whatever size fits */
1358
+ height: 60px;
1359
+ object-fit: contain;
1360
+ }
1361
+
1362
+
1363
+
1364
+
1365
+
1366
+
1367
+ /* Add this to your existing CSS file or in a <style> tag in the <head> */
1368
+ .loading-overlay {
1369
+ position: fixed;
1370
+ top: 0;
1371
+ left: 0;
1372
+ width: 100%;
1373
+ height: 100%;
1374
+ background-color: rgba(255, 255, 255, 0.7);
1375
+ backdrop-filter: blur(8px);
1376
+ display: flex;
1377
+ justify-content: center;
1378
+ align-items: center;
1379
+ z-index: 9999;
1380
+ opacity: 0;
1381
+ visibility: hidden;
1382
+ transition: opacity 0.3s, visibility 0.3s;
1383
+ }
1384
+
1385
+ .loading-overlay.active {
1386
+ opacity: 1;
1387
+ visibility: visible;
1388
+ }
1389
+
1390
+ .loading-container {
1391
+ position: relative;
1392
+ width: 300px;
1393
+ height: 500px;
1394
+ display: flex;
1395
+ flex-direction: column;
1396
+ align-items: center;
1397
+ justify-content: center;
1398
+ }
1399
+
1400
+ .cyclone {
1401
+ position: absolute;
1402
+ width: 200px;
1403
+ height: 400px;
1404
+ top: 50px;
1405
+ }
1406
+
1407
+ .paper {
1408
+ position: absolute;
1409
+ background-color: white;
1410
+ border: 1px solid #ddd;
1411
+ transform-origin: center;
1412
+ box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.1);
1413
+ z-index: 10;
1414
+ }
1415
+
1416
+ .loading-text {
1417
+ position: absolute;
1418
+ bottom: 30px;
1419
+ font-size: 24px;
1420
+ color: #555;
1421
+ font-weight: bold;
1422
+ }
1423
+
1424
+ .ground {
1425
+ position: absolute;
1426
+ bottom: 70px;
1427
+ width: 120px;
1428
+ height: 10px;
1429
+ background: radial-gradient(ellipse at center, #eee, transparent 70%);
1430
+ border-radius: 50%;
1431
+ opacity: 0.3;
1432
+ }
1433
+
1434
+
1435
+ .paper-alert {
1436
+ background: #fff;
1437
+ border-radius: 2px;
1438
+ box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23),
1439
+ 0 0 0 1px rgba(0,0,0,0.05);
1440
+ min-width: 320px;
1441
+ max-width: 400px;
1442
+ position: fixed;
1443
+ right: 16px;
1444
+ top: 16px;
1445
+ overflow: hidden;
1446
+ opacity: 0;
1447
+ pointer-events: none;
1448
+ z-index: 10000;
1449
+ transform-origin: top right;
1450
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
1451
+ }
1452
+
1453
+ /* Torn paper effect */
1454
+ .paper-alert::before {
1455
+ content: '';
1456
+ position: absolute;
1457
+ left: 0;
1458
+ bottom: -2px;
1459
+ width: 100%;
1460
+ height: 4px;
1461
+ background-color: #fff;
1462
+ background-image:
1463
+ radial-gradient(circle at 2px 3px, transparent 2px, #fff 2px, #fff 3px, transparent 3px),
1464
+ radial-gradient(circle at 10px 3px, transparent 2px, #fff 2px, #fff 3px, transparent 3px),
1465
+ radial-gradient(circle at 18px 3px, transparent 2px, #fff 2px, #fff 3px, transparent 3px),
1466
+ radial-gradient(circle at 26px 3px, transparent 2px, #fff 2px, #fff 3px, transparent 3px),
1467
+ radial-gradient(circle at 34px 3px, transparent 2px, #fff 2px, #fff 3px, transparent 3px),
1468
+ radial-gradient(circle at 42px 3px, transparent 2px, #fff 2px, #fff 3px, transparent 3px);
1469
+ background-size: 50px 10px;
1470
+ z-index: 2;
1471
+ }
1472
+
1473
+ /* Paper texture */
1474
+ .paper-alert::after {
1475
+ content: '';
1476
+ position: absolute;
1477
+ left: 0;
1478
+ top: 0;
1479
+ width: 100%;
1480
+ height: 100%;
1481
+ background-image: url("data:image/svg+xml,%3Csvg width='100' height='100' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise' x='0' y='0'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3CfeBlend mode='screen'/%3E%3C/filter%3E%3Crect width='100' height='100' filter='url(%23noise)' opacity='0.08'/%3E%3C/svg%3E");
1482
+ pointer-events: none;
1483
+ z-index: 1;
1484
+ }
1485
+
1486
+ .paper-alert.showAlert {
1487
+ opacity: 1;
1488
+ pointer-events: auto;
1489
+ }
1490
+
1491
+ .paper-alert.show {
1492
+ animation: paper_enter 0.7s cubic-bezier(0.68, -0.55, 0.27, 1.55) forwards;
1493
+ }
1494
+
1495
+ @keyframes paper_enter {
1496
+ 0% { transform: translateX(100%) rotate(5deg); }
1497
+ 50% { transform: translateX(-10%) rotate(-2deg); }
1498
+ 75% { transform: translateX(5%) rotate(1deg); }
1499
+ 100% { transform: translateX(0) rotate(0); }
1500
+ }
1501
+
1502
+ .paper-alert.hide {
1503
+ animation: paper_exit 0.5s cubic-bezier(0.68, -0.55, 0.27, 1.55) forwards;
1504
+ }
1505
+
1506
+ @keyframes paper_exit {
1507
+ 0% { transform: translateX(0) rotate(0); }
1508
+ 30% { transform: translateX(5%) rotate(1deg); }
1509
+ 100% { transform: translateX(110%) rotate(5deg); }
1510
+ }
1511
+
1512
+ .paper-alert-content {
1513
+ position: relative;
1514
+ padding: 18px 15px;
1515
+ z-index: 3;
1516
+ }
1517
+
1518
+ .paper-pin {
1519
+ position: absolute;
1520
+ width: 12px;
1521
+ height: 12px;
1522
+ background: #f44336;
1523
+ border-radius: 50%;
1524
+ top: 10px;
1525
+ left: 20px;
1526
+ box-shadow: inset 0 0 0 2px rgba(0,0,0,0.2), 0 1px 2px rgba(0,0,0,0.2);
1527
+ }
1528
+
1529
+ .paper-pin:after {
1530
+ content: '';
1531
+ position: absolute;
1532
+ width: 6px;
1533
+ height: 6px;
1534
+ background: rgba(255,255,255,0.4);
1535
+ border-radius: 50%;
1536
+ top: 2px;
1537
+ left: 2px;
1538
+ }
1539
+
1540
+ .paper-clip {
1541
+ position: absolute;
1542
+ top: -8px;
1543
+ right: 30px;
1544
+ width: 30px;
1545
+ height: 30px;
1546
+ border: 2px solid #5d9cec;
1547
+ border-radius: 0 0 0 10px;
1548
+ border-top: none;
1549
+ border-right: none;
1550
+ transform: rotate(45deg);
1551
+ z-index: 10;
1552
+ }
1553
+
1554
+ .paper-clip:before {
1555
+ content: '';
1556
+ position: absolute;
1557
+ width: 15px;
1558
+ height: 10px;
1559
+ border: 2px solid #5d9cec;
1560
+ border-radius: 0 5px 0 5px;
1561
+ border-top: none;
1562
+ border-right: none;
1563
+ top: 0;
1564
+ left: 0;
1565
+ transform: rotate(0deg);
1566
+ }
1567
+
1568
+ .alert-icon {
1569
+ display: flex;
1570
+ align-items: center;
1571
+ margin-bottom: 10px;
1572
+ }
1573
+
1574
+ .alert-icon i {
1575
+ font-size: 22px;
1576
+ color: #f44336;
1577
+ margin-right: 10px;
1578
+ }
1579
+
1580
+ .alert-title {
1581
+ font-weight: 600;
1582
+ font-size: 16px;
1583
+ color: #333;
1584
+ margin: 0;
1585
+ }
1586
+
1587
+ .paper-alert .msg {
1588
+ margin-top: 6px;
1589
+ margin-left: 32px;
1590
+ font-size: 14px;
1591
+ color: #555;
1592
+ line-height: 1.4;
1593
+ }
1594
+
1595
+ .paper-alert .close-btn {
1596
+ position: absolute;
1597
+ top: 10px;
1598
+ right: 10px;
1599
+ width: 24px;
1600
+ height: 24px;
1601
+ display: flex;
1602
+ align-items: center;
1603
+ justify-content: center;
1604
+ cursor: pointer;
1605
+ background: rgba(0,0,0,0.05);
1606
+ border-radius: 50%;
1607
+ color: #666;
1608
+ transition: all 0.2s;
1609
+ z-index: 5;
1610
+ }
1611
+
1612
+ .paper-alert .close-btn:hover {
1613
+ background: rgba(0,0,0,0.1);
1614
+ color: #333;
1615
+ transform: rotate(90deg);
1616
+ }
1617
+
1618
+ .alert-btn {
1619
+ position: fixed;
1620
+ top: 7rem;
1621
+ right: 2.5rem;
1622
+ z-index: 1201;
1623
+ font-size: 0.9rem;
1624
+ padding: 0.7rem 1.5rem;
1625
+ border-radius: 5px;
1626
+ background: linear-gradient(135deg, #ff6b6b, #ff8e8e);
1627
+ color: white;
1628
+ border: none;
1629
+ box-shadow: 0 4px 15px rgba(255, 107, 107, 0.4);
1630
+ cursor: pointer;
1631
+ font-family: 'Segoe UI', sans-serif;
1632
+ font-weight: 600;
1633
+ letter-spacing: 0.5px;
1634
+ transition: all 0.3s;
1635
+ }
1636
+
1637
+ .alert-btn:hover {
1638
+ transform: translateY(-3px) scale(1.03);
1639
+ box-shadow: 0 6px 20px rgba(255, 107, 107, 0.5);
1640
+ }
1641
+
1642
+ .paper-lines {
1643
+ position: absolute;
1644
+ left: 0;
1645
+ top: 0;
1646
+ width: 100%;
1647
+ height: 100%;
1648
+ background-image:
1649
+ linear-gradient(to bottom, transparent 0%, transparent 98%, #e0e0e0 99%, transparent 100%);
1650
+ background-size: 100% 24px;
1651
+ z-index: 0;
1652
+ pointer-events: none;
1653
+ }
1654
+
1655
+ .handwritten {
1656
+ font-family: 'Segoe Script', 'Bradley Hand', cursive;
1657
+ transform: rotate(-1deg);
1658
+ color: #2c3e50;
1659
+ }
1660
+
1661
+
1662
+ #nextBtn {
1663
+ background: var(--secondary-color);
1664
+ color: var(--white);
1665
+ border: none;
1666
+ padding: 0.8rem 2rem;
1667
+ border-radius: 15px;
1668
+ cursor: pointer;
1669
+ transition: background-color 0.3s ease;
1670
+ bottom: 7px;
1671
+ }
1672
+
1673
+ #nextBtn:hover {
1674
+ background: #5a37a8;
1675
+ }
1676
+
1677
+ .header-title {
1678
+ text-align: center;
1679
+ margin: 0 auto;
1680
+ width: 100%;
1681
+ position: relative;
1682
+ top: 50%;
1683
+ font-family: 'Bungee Shade', cursive;
1684
+ transform: translateY(35%);
1685
+ }
1686
+
1687
+ .interactive-book {
1688
+ max-height: fit-content;
1689
+ margin: 0;
1690
+ padding: 10px;
1691
+ background: linear-gradient(90deg, var(--secondary-color), var(--secondary-color));
1692
+ position: relative;
1693
+ }
1694
+
1695
+ .interactive-book::after {
1696
+ content: '';
1697
+ position: absolute;
1698
+ top: 10px;
1699
+ left: 20px;
1700
+ right: 20px;
1701
+ bottom: 20px;
1702
+ border: 8px solid black;
1703
+ pointer-events: none;
1704
+ border-radius: 12px;
1705
+ }
1706
+
1707
+ .carousel-container {
1708
+ position: relative;
1709
+ margin-top: -2px;
1710
+ width: 80%;
1711
+ height: 55vh;
1712
+ perspective: 1000px;
1713
+ display: flex;
1714
+ flex-direction: column;
1715
+ justify-content: space-between;
1716
+ align-items: center;
1717
+ margin-left: 10%;
1718
+ transform: scale(0.6);
1719
+
1720
+ }
1721
+
1722
+ .carousel {
1723
+ margin-top: 60px;
1724
+ position: absolute;
1725
+ width: 100%;
1726
+ height: 100%;
1727
+ transform-style: preserve-3d;
1728
+ transition: transform 1s ease;
1729
+ }
1730
+
1731
+ .slide {
1732
+ position: absolute;
1733
+ width: 60%;
1734
+ height: 80%;
1735
+ left: 20%;
1736
+ top: 10%;
1737
+ background: var(--text-color);
1738
+ border-radius: 10px;
1739
+ box-shadow: 0 10px 30px rgba(0,0,0,0.2);
1740
+ display: flex;
1741
+ flex-direction: column;
1742
+ justify-content: center;
1743
+ align-items: center;
1744
+ padding: 20px;
1745
+ box-sizing: border-box;
1746
+ text-align: center;
1747
+ backface-visibility: hidden;
1748
+ transition: opacity 0.5s;
1749
+ }
1750
+
1751
+
1752
+ .slide video {
1753
+ width: 100%;
1754
+ height: 100%;
1755
+ object-fit: cover;
1756
+ border-radius: 3px;
1757
+ }
1758
+
1759
+ .slide h2 {
1760
+ font-size: 24px;
1761
+ margin-bottom: 15px;
1762
+ color: #333;
1763
+ }
1764
+
1765
+ .slide p {
1766
+ font-size: 16px;
1767
+ line-height: 1.6;
1768
+ color: #666;
1769
+ max-width: 80%;
1770
+ }
1771
+
1772
+ /* Refined Open Book Section */
1773
+ .book-container {
1774
+ position: relative;
1775
+ width: 300px;
1776
+ margin-top: 60px;
1777
+ height: 150px;
1778
+ margin-bottom: 30px;
1779
+ margin-left: 40%;
1780
+ }
1781
+
1782
+ .book {
1783
+ position: absolute;
1784
+ width: 100%;
1785
+ height: 100%;
1786
+ transform-style: preserve-3d;
1787
+ transform: rotateX(20deg);
1788
+ }
1789
+
1790
+ .book-center {
1791
+ position: absolute;
1792
+ width: 2px;
1793
+ height: 100%;
1794
+ background: #999;
1795
+ left: 50%;
1796
+ transform: translateX(20px) ;
1797
+ box-shadow: 0 0 5px rgba(0,0,0,0.3);
1798
+ z-index: 1;
1799
+ }
1800
+
1801
+ .book-left, .book-right {
1802
+ position: absolute;
1803
+ width: 50%;
1804
+ height: 100%;
1805
+ top: 0;
1806
+ box-sizing: border-box;
1807
+ background: #fff;
1808
+ border: 1px solid #ddd;
1809
+ box-shadow: 0 5px 15px rgba(0,0,0,0.2);
1810
+ overflow: hidden;
1811
+ }
1812
+
1813
+ .book-left {
1814
+ left: 0;
1815
+ border-radius: 5px 0 0 5px;
1816
+ transform-origin: right center;
1817
+ z-index: 1;
1818
+ background: linear-gradient(to right, #f9f9f9, #fff);
1819
+ }
1820
+
1821
+ .book-right {
1822
+ right: 0;
1823
+ border-radius: 0 5px 5px 0;
1824
+ transform-origin: left center;
1825
+ z-index: 1;
1826
+ background: linear-gradient(to left, #f9f9f9, #fff);
1827
+ }
1828
+
1829
+ .page {
1830
+ position: absolute;
1831
+ width: 50%;
1832
+ height: 100%;
1833
+ top: 0;
1834
+ right: 0;
1835
+ background: #fff;
1836
+ border-left: 1px solid #eee;
1837
+ transform-origin: left center;
1838
+ transition: transform 0.6s ease, z-index 0s 0.3s;
1839
+ transform-style: preserve-3d;
1840
+ z-index: 1;
1841
+ border-radius: 0 5px 5px 0;
1842
+ }
1843
+
1844
+ .page-front, .page-back {
1845
+ position: absolute;
1846
+ width: 100%;
1847
+ height: 100%;
1848
+ top: 0;
1849
+ left: 0;
1850
+ backface-visibility: hidden;
1851
+ background: #fff;
1852
+ box-sizing: border-box;
1853
+ padding: 10px;
1854
+ background: linear-gradient(to left, #f9f9f9, #fff);
1855
+ }
1856
+
1857
+ .page-back {
1858
+ transform: rotateY(180deg);
1859
+ background: linear-gradient(to right, #f9f9f9, #fff);
1860
+ border-radius: 5px 0 0 5px;
1861
+ }
1862
+
1863
+ .page-fold {
1864
+ position: absolute;
1865
+ width: 30px;
1866
+ height: 100%;
1867
+ top: 0;
1868
+ left: 0;
1869
+ background: linear-gradient(to right, rgba(0,0,0,0.05), rgba(0,0,0,0));
1870
+ z-index: 2;
1871
+ }
1872
+
1873
+ .page-shadow {
1874
+ position: absolute;
1875
+ width: 100%;
1876
+ height: 100%;
1877
+ top: 0;
1878
+ left: 0;
1879
+ background: linear-gradient(to right, rgba(0,0,0,0.1), rgba(0,0,0,0));
1880
+ opacity: 0;
1881
+ transition: opacity 0.6s;
1882
+ pointer-events: none;
1883
+ }
1884
+
1885
+ .book-shadow {
1886
+ position: absolute;
1887
+ width: 100%;
1888
+ height: 20px;
1889
+ bottom: -20px;
1890
+ left: 0;
1891
+ background: radial-gradient(ellipse at center, rgba(0,0,0,0.2) 0%, rgba(0,0,0,0) 70%);
1892
+ transform: rotateX(90deg);
1893
+ z-index: 0;
1894
+ }
1895
+
1896
+ .page-content {
1897
+ width: 100%;
1898
+ height: 100%;
1899
+ display: flex;
1900
+ justify-content: center;
1901
+ align-items: center;
1902
+ }
1903
+
1904
+
1905
+ .page-number {
1906
+ position: absolute;
1907
+ bottom: 10px;
1908
+ font-size: 12px;
1909
+ color: #999;
1910
+ }
1911
+
1912
+ .page-number-left {
1913
+ left: 10px;
1914
+ }
1915
+
1916
+ .page-number-right {
1917
+ right: 10px;
1918
+ }
1919
+
1920
+ .page-line {
1921
+ width: 80%;
1922
+ height: 1px;
1923
+ background: #eee;
1924
+ margin: 10px 0;
1925
+ }
1926
+
1927
+ .navigation {
1928
+ display: flex;
1929
+ gap: 8px;
1930
+ margin-top: 20px;
1931
+ justify-content: space-between;
1932
+ }
1933
+
1934
+ button {
1935
+ padding: 10px 20px;
1936
+ background: #3498db;
1937
+ color: white;
1938
+ border: none;
1939
+ margin-top: 10px;
1940
+ border-radius: 5px;
1941
+ cursor: pointer;
1942
+ transition: background 0.3s;
1943
+ font-size: 14px;
1944
+ }
1945
+
1946
+ button:hover {
1947
+ background: #2980b9;
1948
+ }
1949
+
1950
+ .pagination {
1951
+ display: flex;
1952
+ justify-content: center;
1953
+ gap: 10px;
1954
+ margin-top: 15px;
1955
+ }
1956
+
1957
+ .dot {
1958
+ width: 10px;
1959
+ height: 10px;
1960
+ background: #ccc;
1961
+ border-radius: 50%;
1962
+ cursor: pointer;
1963
+ }
1964
+
1965
+ .dot.active {
1966
+ background: #3498db;
1967
+ }
static/css/loginstyle.css ADDED
@@ -0,0 +1,460 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap');
2
+
3
+ * {
4
+ margin: 0;
5
+ padding: 0;
6
+ box-sizing: border-box;
7
+ font-family: 'Poppins', sans-serif;
8
+ }
9
+
10
+ :root {
11
+ --mainColor1: #4a90e2;
12
+ --mainColor2: #7f56d9;
13
+ --whiteColor: #ffffff;
14
+ --titleColor: #555555;
15
+ --labelColor: #333333;
16
+ --text-color: #333;
17
+ --secondary-color: #fcee0a;
18
+ --aqua: #47f5f2;
19
+ }
20
+
21
+ html {
22
+ font-size: 62.5%;
23
+ scroll-behavior: smooth;
24
+ }
25
+
26
+ body {
27
+ background: linear-gradient(to right, var(--secondary-color), var(--secondary-color));
28
+ font-weight: 400;
29
+ min-height: 100vh;
30
+ display: grid;
31
+ place-content: center;
32
+ overflow: hidden;
33
+ }
34
+
35
+
36
+ .paper-fall {
37
+ position: absolute;
38
+ width: 100%;
39
+ height: 100%;
40
+ overflow: hidden;
41
+ }
42
+
43
+ .paper {
44
+ position: absolute;
45
+ width: 50px;
46
+ height: 50px;
47
+ background-color: #fff;
48
+ border-radius: 2px;
49
+ box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
50
+ animation: fall linear infinite;
51
+ }
52
+
53
+ @keyframes fall {
54
+ 0% {
55
+ transform: translateY(-100%);
56
+ opacity: 0;
57
+ }
58
+ 10% {
59
+ opacity: 1;
60
+ }
61
+ 100% {
62
+ transform: translateY(100vh);
63
+ opacity: 0;
64
+ }
65
+ }
66
+
67
+
68
+ .wrapper {
69
+ position: relative;
70
+ width: 35rem;
71
+ height: 50rem;
72
+ }
73
+
74
+ @media(min-width: 540px) {
75
+ .wrapper {
76
+ width: 40rem;
77
+ }
78
+ }
79
+
80
+ .form-container {
81
+ position: absolute;
82
+ top: 0;
83
+ left: 0;
84
+ display: flex;
85
+ align-items: center;
86
+ justify-content: center;
87
+ width: 100%;
88
+ height: 100%;
89
+ background: var(--text-color);
90
+ border-radius: 0.5rem;
91
+ box-shadow: 0 0 1rem rgba(0, 0, 0, 0.2);
92
+ }
93
+
94
+ .form-container h2 {
95
+ font-size: 3rem;
96
+ text-align: center;
97
+ text-transform: capitalize;
98
+ color: var(--whiteColor);
99
+ }
100
+
101
+ .form-group {
102
+ position: relative;
103
+ width: 32rem;
104
+ margin: 3rem 0;
105
+ }
106
+
107
+ .form-group i,
108
+ .form-group label {
109
+ position: absolute;
110
+ top: 50%;
111
+ transform: translateY(-50%);
112
+ font-size: 1.6rem;
113
+ color: var(--labelColor);
114
+ padding: 0 0.5rem;
115
+ transition: all 0.5s ease;
116
+ pointer-events: none;
117
+ }
118
+
119
+ .form-group i {
120
+ left: 0.5rem;
121
+ }
122
+
123
+ .form-group label {
124
+ left: 2.5rem;
125
+ }
126
+
127
+ .form-group input {
128
+ width: 100%;
129
+ height: 4rem;
130
+ padding: 0 1rem;
131
+ border-radius: 0.5rem;
132
+ border: 0.1rem solid var(--whiteColor);
133
+ font-size: 1.6rem;
134
+ color: var(--labelColor);
135
+ background: white;
136
+ outline: none;
137
+ }
138
+
139
+ .form-group input:focus ~ label,
140
+ .form-group input:valid ~ label,
141
+ .form-group input:focus ~ i,
142
+ .form-group input:valid ~ i {
143
+ top: 0 !important;
144
+ font-size: 1.2rem !important;
145
+ background: var(--whiteColor);
146
+ }
147
+
148
+ .forgot-pass {
149
+ margin: -1.5rem 0 1.5rem;
150
+ text-align: center;
151
+ }
152
+
153
+ .forgot-pass a {
154
+ color: var(--whiteColor);
155
+ text-decoration: none;
156
+ font-size: 1.4rem;
157
+ transition: all 0.5s ease;
158
+ }
159
+
160
+ .forgot-pass a:hover {
161
+ color: var(--mainColor1);
162
+ }
163
+
164
+
165
+ .wrapper .form-container.forgot-password {
166
+ position: absolute;
167
+ top: 0;
168
+ left: 0;
169
+ width: 100%;
170
+ height: 100%;
171
+ background-color: var (-text-color);
172
+ border-radius: .5rem;
173
+ box-shadow: 0 0 1rem 0 rgba(0, 0, 0, 0.2);
174
+ transform: rotate(7deg);
175
+ }
176
+
177
+ /* Animation for Forgot Password */
178
+ .wrapper.animated-forgot .form-container.forgot-password {
179
+ animation: forgot-flip 1s ease-in-out forwards;
180
+ z-index: 3;
181
+ }
182
+
183
+ @keyframes forgot-flip {
184
+ 0% {
185
+ transform: translateX(0);
186
+ }
187
+ 50% {
188
+ transform: translateX(-50rem) scale(1.1);
189
+ }
190
+ 100% {
191
+ transform: translateX(0) rotate(7deg) scale(1);
192
+ }
193
+ }
194
+
195
+ .wrapper.animated-forgot .form-container.sign-in,
196
+ .wrapper.animated-forgot .form-container.sign-up {
197
+ animation: rotatecard 0.7s ease-in-out forwards;
198
+ animation-delay: 0.3s;
199
+ }
200
+
201
+
202
+
203
+
204
+ .btn {
205
+ background: linear-gradient(to right, var(--aqua), var(--aqua));
206
+ color: var(--whiteColor);
207
+ width: 100%;
208
+ height: 4rem;
209
+ font-size: 1.6rem;
210
+ font-weight: 500;
211
+ border: none;
212
+ border-radius: 0.5rem;
213
+ cursor: pointer;
214
+ box-shadow: 0 0.2rem 1rem rgba(0, 0, 0, 0.4);
215
+ }
216
+
217
+ .link {
218
+ text-align: center;
219
+ font-size: 1.4rem;
220
+ color: var(--labelColor);
221
+ margin: 2.5rem 0;
222
+ }
223
+
224
+ .link a {
225
+ color: var(--mainColor1);
226
+ text-decoration: none;
227
+ font-weight: 600;
228
+ transition: all 0.5s ease;
229
+ }
230
+
231
+ .link a:hover {
232
+ color: #da4453;
233
+ }
234
+
235
+ .sign-up {
236
+ transform: rotate(7deg);
237
+ }
238
+
239
+ .animated-signin .sign-in {
240
+ animation: signin-flip 1s ease-in-out forwards;
241
+ }
242
+
243
+ @keyframes signin-flip {
244
+ 0% {
245
+ transform: translateX(0);
246
+ }
247
+ 50% {
248
+ transform: translateX(-50rem) scale(1.1);
249
+ }
250
+ 100% {
251
+ transform: translateX(0) rotate(7deg) scale(1);
252
+ }
253
+ }
254
+
255
+ .animated-signin .sign-up {
256
+ animation: rotatecard 0.7s ease-in-out forwards;
257
+ }
258
+
259
+ .animated-signup .sign-up {
260
+ animation: signup-flip 1s ease-in-out forwards;
261
+ }
262
+
263
+ @keyframes signup-flip {
264
+ 0% {
265
+ transform: translateX(0);
266
+ z-index: 1;
267
+ }
268
+ 50% {
269
+ transform: translateX(50rem) scale(1.1);
270
+ }
271
+ 100% {
272
+ transform: translateX(0) rotate(7deg) scale(1);
273
+ }
274
+ }
275
+
276
+ .animated-signup .sign-in {
277
+ transform: rotate(7deg);
278
+ animation: rotatecard 0.7s ease-in-out forwards;
279
+ animation-delay: 0.3s;
280
+ }
281
+
282
+ @keyframes rotatecard {
283
+ 0% {
284
+ transform: rotate(7deg);
285
+ }
286
+ 100% {
287
+ transform: rotate(0);
288
+ z-index: 1;
289
+ }
290
+ }
291
+
292
+ /*
293
+ .success-animation {
294
+ position: fixed;
295
+ top: 50%;
296
+ left: 50%;
297
+ transform: translate(-50%, -50%);
298
+ z-index: 1000;
299
+ text-align: center;
300
+ }
301
+
302
+ .checkmark {
303
+ width: 80px;
304
+ height: 80px;
305
+ border-radius: 50%;
306
+ background: #7f56d9;
307
+ margin: 0 auto;
308
+ position: relative;
309
+ animation: scaleIn 0.3s ease-out;
310
+ }
311
+
312
+ .checkmark::after {
313
+ content: '';
314
+ position: absolute;
315
+ left: 25px;
316
+ top: 40px;
317
+ width: 25px;
318
+ height: 5px;
319
+ background: #fff;
320
+ transform: rotate(45deg);
321
+ animation: checkmark 0.3s ease-out;
322
+ }
323
+
324
+ .checkmark::before {
325
+ content: '';
326
+ position: absolute;
327
+ left: 15px;
328
+ top: 30px;
329
+ width: 40px;
330
+ height: 5px;
331
+ background: #fff;
332
+ transform: rotate(-45deg);
333
+ animation: checkmark 0.4s ease-out;
334
+ }
335
+
336
+ .success-text {
337
+ color: #7f56d9;
338
+ font-size: 2.4rem;
339
+ margin-top: 20px;
340
+ opacity: 0;
341
+ animation: fadeInUp 0.5s ease-out 0.3s forwards;
342
+ }
343
+
344
+ @keyframes scaleIn {
345
+ from {
346
+ transform: scale(0);
347
+ }
348
+ to {
349
+ transform: scale(1);
350
+ }
351
+ }
352
+
353
+ @keyframes checkmark {
354
+ from {
355
+ width: 0;
356
+ height: 0;
357
+ }
358
+ to {
359
+ width: 25px;
360
+ height: 5px;
361
+ }
362
+ }
363
+
364
+ @keyframes fadeInUp {
365
+ from {
366
+ opacity: 0;
367
+ transform: translateY(20px);
368
+ }
369
+ to {
370
+ opacity: 1;
371
+ transform: translateY(0);
372
+ }
373
+ }
374
+
375
+ .success-overlay {
376
+ position: fixed;
377
+ top: 0;
378
+ left: 0;
379
+ width: 100%;
380
+ height: 100%;
381
+ background: rgba(255, 255, 255, 0.9);
382
+ z-index: 999;
383
+ }
384
+ */
385
+
386
+
387
+
388
+ /* Paper Animation Styles */
389
+
390
+ .page {
391
+ position: absolute;
392
+ width: 70px;
393
+ height: 90px;
394
+ background-color: var(--mainColor1);
395
+ border: 1px solid #ccc;
396
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
397
+ animation: scatter 3s ease-in-out forwards;
398
+ z-index: 1000;
399
+ }
400
+
401
+ .tear-left,
402
+ .tear-right {
403
+ position: absolute;
404
+ width: 75px;
405
+ height: 200px;
406
+ background-color: white;
407
+ border: 1px solid #ccc;
408
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
409
+ z-index: 1000;
410
+ }
411
+
412
+ .tear-left {
413
+ clip-path: polygon(0 0, 100% 0, 70% 100%, 0% 100%);
414
+ animation: tearLeft 2s ease-in-out forwards;
415
+ }
416
+
417
+ .tear-right {
418
+ clip-path: polygon(30% 0, 100% 0, 100% 100%, 0% 100%);
419
+ animation: tearRight 2s ease-in-out forwards;
420
+ }
421
+
422
+ @keyframes scatter {
423
+ 0% {
424
+ transform: translate(0, 0) rotate(0deg);
425
+ opacity: 1;
426
+ }
427
+ 100% {
428
+ transform: translate(var(--x), var(--y)) rotate(var(--rotate));
429
+ opacity: 0;
430
+ }
431
+ }
432
+
433
+ @keyframes tearLeft {
434
+ 0% {
435
+ transform: translateX(0) rotate(0deg);
436
+ }
437
+ 100% {
438
+ transform: translateX(-150px) rotate(-20deg);
439
+ }
440
+ }
441
+
442
+ @keyframes tearRight {
443
+ 0% {
444
+ transform: translateX(0) rotate(0deg);
445
+ }
446
+ 100% {
447
+ transform: translateX(150px) rotate(20deg);
448
+ }
449
+ }
450
+
451
+ .paper {
452
+ position: fixed;
453
+ pointer-events: none;
454
+ transform-origin: center center;
455
+ filter: blur(1px);
456
+ mix-blend-mode: screen;
457
+ background: white;
458
+ top: 0;
459
+ left: 0;
460
+ }
static/css/profilepage.css ADDED
@@ -0,0 +1,654 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --hprimary-color: #2c3e50;
3
+ --hsecondary-color: #1a2530;
4
+ --accent-color: #3498db;
5
+ --accent-hover: #2980b9;
6
+ --danger-color: #e74c3c;
7
+ --danger-hover: #c0392b;
8
+ --success-color: #2ecc71;
9
+ --warning-color: #f39c12;
10
+ --white: #ffffff;
11
+ --light-gray: #f5f5f5;
12
+ --gray: #95a5a6;
13
+ --yellow-color: #fcee0a;
14
+ --dark-gray: #7f8c8d;
15
+ --text-color: #2c3e50;
16
+ --card-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
17
+ --transition-speed: 0.3s;
18
+ }
19
+
20
+ /* Profile Trigger */
21
+ .profile-container {
22
+ position: relative;
23
+ margin-left: 20px;
24
+ }
25
+
26
+ .profile-trigger {
27
+ display: flex;
28
+ align-items: center;
29
+ cursor: pointer;
30
+ padding: 5px 10px;
31
+ border-radius: 50px;
32
+ background: rgba(255, 255, 255, 0.1);
33
+ transition: all var(--transition-speed) ease;
34
+ color: var(--white);
35
+ text-decoration: none;
36
+ }
37
+
38
+ .profile-trigger:hover {
39
+ background: rgba(255, 255, 255, 0.2);
40
+ }
41
+
42
+ .profile-trigger span {
43
+ margin-right: 8px;
44
+ font-weight: 500;
45
+ }
46
+
47
+ .profile-trigger i {
48
+ margin-left: 5px;
49
+ font-size: 12px;
50
+ transition: transform var(--transition-speed) ease;
51
+ }
52
+
53
+ .profile-trigger.active i {
54
+ transform: rotate(180deg);
55
+ }
56
+
57
+ .profile-pic {
58
+ width: 35px;
59
+ height: 35px;
60
+ border-radius: 50%;
61
+ object-fit: cover;
62
+ border: 2px solid var(--accent-color);
63
+ transition: all var(--transition-speed) ease;
64
+ margin-left: 8px;
65
+ }
66
+
67
+ .profile-pic:hover {
68
+ border-color: var(--accent-hover);
69
+ transform: scale(1.05);
70
+ }
71
+
72
+ /* Profile Dropdown */
73
+ .profile-overlay {
74
+ position: fixed;
75
+ top: 0;
76
+ left: 0;
77
+ width: 100%;
78
+ height: 100%;
79
+ background: rgba(0, 0, 0, 0.6);
80
+ backdrop-filter: blur(5px);
81
+ display: none;
82
+ z-index: 1000;
83
+ transition: opacity var(--transition-speed) ease;
84
+ }
85
+
86
+ /* Fix the transition variable */
87
+ .profile-dropdown {
88
+ position: fixed; /* Change from relative to fixed */
89
+ top: 0;
90
+ right: -450px;
91
+ width: 450px;
92
+ height: 100vh; /* Use viewport height instead */
93
+ background: linear-gradient(145deg, var(--hprimary-color), var(--hsecondary-color));
94
+ color: var(--white);
95
+ box-shadow: -5px 0 25px rgba(0, 0, 0, 0.3);
96
+ transition: right var(--transition-speed) ease-in-out; /* Fix variable name */
97
+ z-index: 1001;
98
+ overflow-y: auto;
99
+ scrollbar-width: thin;
100
+ scrollbar-color: var(--accent-color) var(--hsecondary-color);
101
+ }
102
+
103
+ /* Prevent body scrolling when profile is open */
104
+ body.profile-open {
105
+ overflow: hidden;
106
+ }
107
+
108
+ /* Show profile when active */
109
+ .profile-dropdown.active {
110
+ right: 0;
111
+ }
112
+
113
+ .profile-overlay.active {
114
+ display: block;
115
+ }
116
+
117
+ /* Profile Header */
118
+ .profile-header {
119
+ position: relative;
120
+ padding-bottom: 20px;
121
+ text-align: center;
122
+ }
123
+
124
+ .profile-cover-photo {
125
+ height: 120px;
126
+ background: linear-gradient(120deg,var(--yellow-color));
127
+ background-size: 300% 300%;
128
+ position: relative;
129
+ }
130
+
131
+ .profile-avatar-container {
132
+ position: relative;
133
+ margin-top: -50px;
134
+ display: inline-block;
135
+ }
136
+
137
+ #profileAvatar {
138
+ width: 100px;
139
+ height: 100px;
140
+ border-radius: 50%;
141
+ object-fit: cover;
142
+ border: 4px solid var(--white);
143
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
144
+ background: var(--white);
145
+ transition: all var(--transition-speed) ease;
146
+ }
147
+
148
+ .avatar-upload {
149
+ position: absolute;
150
+ bottom: 0;
151
+ right: 5px;
152
+ background: var(--accent-color);
153
+ width: 30px;
154
+ height: 30px;
155
+ border-radius: 50%;
156
+ display: flex;
157
+ align-items: center;
158
+ justify-content: center;
159
+ cursor: pointer;
160
+ box-shadow: 0 3px 8px rgba(0, 0, 0, 0.2);
161
+ transition: all var(--transition-speed) ease;
162
+ }
163
+
164
+ .avatar-upload:hover {
165
+ background: var(--accent-hover);
166
+ transform: scale(1.1);
167
+ }
168
+
169
+ .avatar-upload i {
170
+ color: var(--white);
171
+ font-size: 14px;
172
+ }
173
+
174
+ .avatar-upload input {
175
+ display: none;
176
+ }
177
+
178
+ #profileHeaderName {
179
+ margin: 15px 0 5px;
180
+ font-size: 22px;
181
+ font-weight: 600;
182
+ }
183
+
184
+ .membership-status {
185
+ display: inline-block;
186
+ padding: 4px 12px;
187
+ background: rgba(243, 156, 18, 0.2);
188
+ border-radius: 20px;
189
+ font-size: 14px;
190
+ margin: 0;
191
+ color: var(--warning-color);
192
+ }
193
+
194
+ .membership-status i {
195
+ margin-right: 5px;
196
+ }
197
+
198
+ /* Tabs */
199
+ .profile-tabs {
200
+ display: flex;
201
+ background: rgba(0, 0, 0, 0.2);
202
+ margin: 20px 0;
203
+ border-radius: 8px;
204
+ padding: 5px;
205
+ }
206
+
207
+ .tab-btn {
208
+ flex: 1;
209
+ background: transparent;
210
+ border: none;
211
+ padding: 12px;
212
+ color: var(--white);
213
+ font-weight: 500;
214
+ font-size: 14px;
215
+ cursor: pointer;
216
+ border-radius: 5px;
217
+ transition: all var(--transition-speed) ease;
218
+ }
219
+
220
+ .tab-btn:hover {
221
+ background: rgba(255, 255, 255, 0.1);
222
+ }
223
+
224
+ .tab-btn.active {
225
+ background: var(--accent-color);
226
+ color: var(--white);
227
+ }
228
+
229
+ .tab-content {
230
+ display: none;
231
+ padding: 0 20px;
232
+ }
233
+
234
+ .tab-content.active {
235
+ display: block;
236
+ animation: fadeIn 0.5s ease;
237
+ }
238
+
239
+ @keyframes fadeIn {
240
+ from { opacity: 0; transform: translateY(10px); }
241
+ to { opacity: 1; transform: translateY(0); }
242
+ }
243
+
244
+ /* Profile Fields */
245
+ .profile-field {
246
+ position: relative;
247
+ background: rgba(255, 255, 255, 0.1);
248
+ padding: 15px 20px;
249
+ border-radius: 12px;
250
+ margin-bottom: 20px;
251
+ transition: all var(--transition-speed) ease;
252
+ }
253
+
254
+ .profile-field:hover {
255
+ background: rgba(255, 255, 255, 0.15);
256
+ transform: translateY(-2px);
257
+ }
258
+
259
+ .profile-field.active {
260
+ background: rgba(255, 255, 255, 0.2);
261
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
262
+ }
263
+
264
+ .profile-field label {
265
+ display: block;
266
+ margin-bottom: 8px;
267
+ font-weight: 500;
268
+ font-size: 14px;
269
+ color: var(--light-gray);
270
+ }
271
+
272
+ .profile-field label i {
273
+ margin-right: 8px;
274
+ color: var(--accent-color);
275
+ }
276
+
277
+ .profile-field input {
278
+ width: 100%;
279
+ padding: 12px 15px;
280
+ background: rgba(0, 0, 0, 0.2);
281
+ border: 1px solid rgba(255, 255, 255, 0.1);
282
+ border-radius: 8px;
283
+ color: var(--white);
284
+ font-size: 15px;
285
+ transition: all var(--transition-speed) ease;
286
+ font-family: inherit;
287
+ }
288
+
289
+ .profile-field input:focus {
290
+ background: rgba(0, 0, 0, 0.3);
291
+ border-color: var(--accent-color);
292
+ outline: none;
293
+ box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.2);
294
+ }
295
+
296
+ .profile-field input:disabled {
297
+ opacity: 0.8;
298
+ cursor: not-allowed;
299
+ }
300
+
301
+ .edit-icon {
302
+ position: absolute;
303
+ top: 15px;
304
+ right: 15px;
305
+ width: 30px;
306
+ height: 30px;
307
+ background: rgba(255, 255, 255, 0.15);
308
+ border-radius: 50%;
309
+ display: flex;
310
+ align-items: center;
311
+ justify-content: center;
312
+ cursor: pointer;
313
+ transition: all var(--transition-speed) ease;
314
+ }
315
+
316
+ .edit-icon:hover {
317
+ background: var(--accent-color);
318
+ transform: rotate(15deg);
319
+ }
320
+
321
+ .edit-icon i {
322
+ color: var(--white);
323
+ font-size: 12px;
324
+ }
325
+
326
+ .verification-badge {
327
+ position: absolute;
328
+ top: 15px;
329
+ right: 15px;
330
+ padding: 4px 10px;
331
+ background: rgba(46, 204, 113, 0.2);
332
+ color: var(--success-color);
333
+ border-radius: 20px;
334
+ font-size: 12px;
335
+ font-weight: 500;
336
+ }
337
+
338
+ .verification-badge i {
339
+ margin-right: 4px;
340
+ }
341
+
342
+ /* Toggle Switches */
343
+ .toggle-field {
344
+ display: flex;
345
+ justify-content: space-between;
346
+ align-items: center;
347
+ padding: 15px 20px;
348
+ background: rgba(255, 255, 255, 0.1);
349
+ border-radius: 12px;
350
+ margin-bottom: 15px;
351
+ transition: all var(--transition-speed) ease;
352
+ }
353
+
354
+ .toggle-field:hover {
355
+ background: rgba(255, 255, 255, 0.15);
356
+ transform: translateY(-2px);
357
+ }
358
+
359
+ .toggle-field span {
360
+ font-weight: 500;
361
+ }
362
+
363
+ .toggle-field span i {
364
+ margin-right: 8px;
365
+ color: var(--accent-color);
366
+ }
367
+
368
+ .switch {
369
+ position: relative;
370
+ display: inline-block;
371
+ width: 50px;
372
+ height: 26px;
373
+ }
374
+
375
+ .switch input {
376
+ opacity: 0;
377
+ width: 0;
378
+ height: 0;
379
+ }
380
+
381
+ .slider {
382
+ position: absolute;
383
+ cursor: pointer;
384
+ top: 0;
385
+ left: 0;
386
+ right: 0;
387
+ bottom: 0;
388
+ background-color: rgba(0, 0, 0, 0.3);
389
+ transition: .4s;
390
+ border-radius: 34px;
391
+ }
392
+
393
+ .slider:before {
394
+ position: absolute;
395
+ content: "";
396
+ height: 18px;
397
+ width: 18px;
398
+ left: 4px;
399
+ bottom: 4px;
400
+ background-color: white;
401
+ transition: .4s;
402
+ border-radius: 50%;
403
+ }
404
+
405
+ input:checked + .slider {
406
+ background-color: var(--accent-color);
407
+ }
408
+
409
+ input:focus + .slider {
410
+ box-shadow: 0 0 1px var(--accent-color);
411
+ }
412
+
413
+ input:checked + .slider:before {
414
+ transform: translateX(24px);
415
+ }
416
+
417
+ /* Select Field */
418
+ .select-field {
419
+ padding: 15px 20px;
420
+ background: rgba(255, 255, 255, 0.1);
421
+ border-radius: 12px;
422
+ margin-bottom: 15px;
423
+ }
424
+
425
+ .select-field label {
426
+ display: block;
427
+ margin-bottom: 10px;
428
+ font-weight: 500;
429
+ }
430
+
431
+ .select-field select {
432
+ width: 100%;
433
+ padding: 12px;
434
+ background: rgba(0, 0, 0, 0.2);
435
+ border: 1px solid rgba(255, 255, 255, 0.1);
436
+ border-radius: 8px;
437
+ color: var(--white);
438
+ appearance: none;
439
+ background-image: url('data:image/svg+xml;utf8,<svg fill="white" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M7 10l5 5 5-5z"/><path d="M0 0h24v24H0z" fill="none"/></svg>');
440
+ background-repeat: no-repeat;
441
+ background-position: right 10px top 50%;
442
+ }
443
+
444
+ /* Activity Tab */
445
+ .activity-summary {
446
+ display: flex;
447
+ justify-content: space-between;
448
+ margin-bottom: 25px;
449
+ }
450
+
451
+ .activity-stat {
452
+ flex: 1;
453
+ text-align: center;
454
+ padding: 15px;
455
+ background: rgba(255, 255, 255, 0.1);
456
+ border-radius: 12px;
457
+ margin: 0 5px;
458
+ transition: all var(--transition-speed) ease;
459
+ }
460
+
461
+ .activity-stat:hover {
462
+ background: rgba(255, 255, 255, 0.2);
463
+ transform: translateY(-5px);
464
+ }
465
+
466
+ .activity-stat i {
467
+ font-size: 24px;
468
+ color: var(--accent-color);
469
+ margin-bottom: 8px;
470
+ }
471
+
472
+ .stat-value {
473
+ display: block;
474
+ font-size: 24px;
475
+ font-weight: 700;
476
+ margin-bottom: 5px;
477
+ }
478
+
479
+ .stat-label {
480
+ display: block;
481
+ font-size: 12px;
482
+ color: var(--gray);
483
+ text-transform: uppercase;
484
+ letter-spacing: 1px;
485
+ }
486
+
487
+ .section-title {
488
+ font-size: 18px;
489
+ margin: 20px 0 15px;
490
+ padding-bottom: 10px;
491
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
492
+ }
493
+
494
+ /* Timeline */
495
+ .timeline {
496
+ position: relative;
497
+ padding-left: 30px;
498
+ margin-bottom: 20px;
499
+ }
500
+
501
+ .timeline:before {
502
+ content: '';
503
+ position: absolute;
504
+ left: 7.5px;
505
+ top: 5px;
506
+ height: calc(100% - 10px);
507
+ width: 2px;
508
+ background: rgba(255, 255, 255, 0.2);
509
+ }
510
+
511
+ .timeline-item {
512
+ position: relative;
513
+ padding-bottom: 20px;
514
+ }
515
+
516
+ .timeline-dot {
517
+ position: absolute;
518
+ left: -30px;
519
+ top: 5px;
520
+ width: 15px;
521
+ height: 15px;
522
+ background: var(--accent-color);
523
+ border-radius: 50%;
524
+ z-index: 1;
525
+ }
526
+
527
+ .timeline-content {
528
+ background: rgba(255, 255, 255, 0.1);
529
+ padding: 15px;
530
+ border-radius: 8px;
531
+ margin-bottom: 5px;
532
+ transition: all var(--transition-speed) ease;
533
+ }
534
+
535
+ .timeline-content:hover {
536
+ background: rgba(255, 255, 255, 0.15);
537
+ transform: translateX(5px);
538
+ }
539
+
540
+ .timeline-content h4 {
541
+ margin: 0 0 5px;
542
+ font-size: 16px;
543
+ }
544
+
545
+ .timeline-content p {
546
+ margin: 0 0 10px;
547
+ color: var(--light-gray);
548
+ font-size: 14px;
549
+ }
550
+
551
+ .timeline-date {
552
+ font-size: 12px;
553
+ color: var(--gray);
554
+ }
555
+
556
+ .timeline-date i {
557
+ margin-right: 5px;
558
+ }
559
+
560
+ /* Action Buttons */
561
+ .action-buttons {
562
+ padding: 20px;
563
+ display: flex;
564
+ gap: 10px;
565
+ }
566
+
567
+ .save-btn, .logout-btn {
568
+ flex: 1;
569
+ padding: 14px;
570
+ border: none;
571
+ border-radius: 8px;
572
+ font-size: 15px;
573
+ font-weight: 600;
574
+ cursor: pointer;
575
+ transition: all var(--transition-speed) ease;
576
+ display: flex;
577
+ align-items: center;
578
+ justify-content: center;
579
+ }
580
+
581
+ .save-btn i, .logout-btn i {
582
+ margin-right: 8px;
583
+ font-size: 16px;
584
+ }
585
+
586
+ .save-btn {
587
+ background: var(--accent-color);
588
+ color: var(--white);
589
+ }
590
+
591
+ .save-btn:hover {
592
+ background: var(--accent-hover);
593
+ transform: translateY(-2px);
594
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
595
+ }
596
+
597
+ .logout-btn {
598
+ background: rgba(255, 255, 255, 0.1);
599
+ color: var(--danger-color);
600
+ }
601
+
602
+ .logout-btn:hover {
603
+ background: var(--danger-color);
604
+ color: var(--white);
605
+ transform: translateY(-2px);
606
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
607
+ }
608
+
609
+ /* Custom Scrollbar */
610
+ .profile-dropdown::-webkit-scrollbar {
611
+ width: 6px;
612
+ }
613
+
614
+ .profile-dropdown::-webkit-scrollbar-track {
615
+ background: rgba(0, 0, 0, 0.1);
616
+ }
617
+
618
+ .profile-dropdown::-webkit-scrollbar-thumb {
619
+ background: rgba(52, 152, 219, 0.5);
620
+ border-radius: 10px;
621
+ }
622
+
623
+ .profile-dropdown::-webkit-scrollbar-thumb:hover {
624
+ background: rgba(52, 152, 219, 0.8);
625
+ }
626
+
627
+ /* Responsive Adjustments */
628
+ @media (max-width: 600px) {
629
+ .profile-dropdown {
630
+ width: 100%;
631
+ right: -100%;
632
+ }
633
+
634
+ .activity-summary {
635
+ flex-direction: column;
636
+ gap: 10px;
637
+ }
638
+
639
+ .activity-stat {
640
+ margin: 0;
641
+ }
642
+ }
643
+
644
+ /* Animation for the dot pulse effect */
645
+ @keyframes pulse {
646
+ 0% { transform: scale(1); opacity: 1; }
647
+ 50% { transform: scale(1.5); opacity: 0.5; }
648
+ 100% { transform: scale(1); opacity: 1; }
649
+ }
650
+
651
+ .timeline-dot.pulse {
652
+ animation: pulse 2s infinite;
653
+ }
654
+
static/js/.env ADDED
@@ -0,0 +1 @@
 
 
1
+ MONGO_URL= "mongodb+srv://darahasviit:<IrYaA4lePAABb0e7>@paperlens.q42bk.mongodb.net/"
static/js/gra.js ADDED
@@ -0,0 +1,1080 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Initialization
2
+ let svg = d3.select("#container").append("svg")
3
+ .attr("width", "100%")
4
+ .attr("height", "100%"),
5
+ width = window.innerWidth,
6
+ height = window.innerHeight,
7
+ mainGroup = svg.append("g").attr("class", "main-container"),
8
+ simulation = null,
9
+ linkDistance = 80,
10
+ chargeStrength = -120,
11
+ gravityStrength = 0.1;
12
+
13
+ // Size scaling controls
14
+ let mainNodeScale = 1.0; // Scale factor for main paper nodes
15
+ let bridgeNodeScale = 1.0; // Scale factor for bridge paper nodes
16
+ let normalNodeScale = 1.0; // Scale factor for normal nodes (cited/referenced)
17
+
18
+ // Keep track of the original data for reference
19
+ let originalData = null;
20
+ let selectedNode = null;
21
+ let nodes = [], links = [];
22
+
23
+ // Define arrow markers for directed links
24
+ svg.append("defs").append("marker")
25
+ .attr("id", "arrow")
26
+ .attr("viewBox", "0 -5 10 10")
27
+ .attr("refX", 20)
28
+ .attr("refY", 0)
29
+ .attr("markerWidth", 6)
30
+ .attr("markerHeight", 6)
31
+ .attr("orient", "auto")
32
+ .append("path")
33
+ .attr("d", "M0,-5L10,0L0,5")
34
+ .attr("fill", "#999");
35
+
36
+ // Define star shape for main papers that are bridges
37
+ svg.append("defs")
38
+ .append("path")
39
+ .attr("id", "star")
40
+ .attr("d", d3.symbol().type(d3.symbolStar).size(300)());
41
+
42
+ // Performance optimization: Set up requestAnimationFrame for simulation updates
43
+ let rafId = null;
44
+ let isSimulationRunning = false;
45
+
46
+ function startSimulationLoop() {
47
+ if (!isSimulationRunning && simulation) {
48
+ isSimulationRunning = true;
49
+ rafId = requestAnimationFrame(simulationTick);
50
+ }
51
+ }
52
+
53
+ function stopSimulationLoop() {
54
+ isSimulationRunning = false;
55
+ if (rafId !== null) {
56
+ cancelAnimationFrame(rafId);
57
+ rafId = null;
58
+ }
59
+ }
60
+
61
+ // Optimized tick function using requestAnimationFrame
62
+ function simulationTick() {
63
+ if (isSimulationRunning && simulation && simulation.alpha() > 0.001) {
64
+ simulation.tick();
65
+ updateVisualElements();
66
+ rafId = requestAnimationFrame(simulationTick);
67
+ } else {
68
+ stopSimulationLoop();
69
+ }
70
+ }
71
+
72
+ // Update visual elements without recalculating forces
73
+ function updateVisualElements() {
74
+ // Performance optimization: Use d3.select once and then update directly
75
+ const linkElements = mainGroup.selectAll(".link");
76
+ const nodeElements = mainGroup.selectAll(".node");
77
+
78
+ // Update links - faster than using attr for each property separately
79
+ linkElements.each(function(d) {
80
+ d3.select(this)
81
+ .attr("x1", d.source.x)
82
+ .attr("y1", d.source.y)
83
+ .attr("x2", d.target.x)
84
+ .attr("y2", d.target.y);
85
+ });
86
+
87
+ // Update nodes - faster transform operation
88
+ nodeElements.attr("transform", d => `translate(${d.x}, ${d.y})`);
89
+ }
90
+
91
+ // Debounce function to improve performance
92
+ function debounce(func, wait) {
93
+ let timeout;
94
+ return function(...args) {
95
+ const context = this;
96
+ clearTimeout(timeout);
97
+ timeout = setTimeout(() => func.apply(context, args), wait);
98
+ };
99
+ }
100
+
101
+ // Cache for node radius calculations
102
+ const nodeRadiusCache = new Map();
103
+
104
+ // Event listeners for controls - optimized with debouncing
105
+ d3.select("#linkDistanceSlider").on("input", debounce(function() {
106
+ linkDistance = +this.value;
107
+ d3.select("#linkDistanceValue").text(linkDistance);
108
+ if (simulation) {
109
+ simulation.force("link").distance(linkDistance);
110
+ simulation.alpha(0.3).restart();
111
+ startSimulationLoop();
112
+ }
113
+ }, 50));
114
+
115
+ d3.select("#chargeSlider").on("input", debounce(function() {
116
+ chargeStrength = +this.value;
117
+ d3.select("#chargeValue").text(chargeStrength);
118
+ if (simulation) {
119
+ simulation.force("charge").strength(chargeStrength);
120
+ simulation.alpha(0.3).restart();
121
+ startSimulationLoop();
122
+ }
123
+ }, 50));
124
+
125
+ d3.select("#gravitySlider").on("input", debounce(function() {
126
+ gravityStrength = +this.value;
127
+ d3.select("#gravityValue").text(gravityStrength);
128
+ if (simulation) {
129
+ simulation.force("gravity", d3.forceRadial(0, width / 2, height / 2).strength(gravityStrength));
130
+ simulation.alpha(0.3).restart();
131
+ startSimulationLoop();
132
+ }
133
+ }, 50));
134
+
135
+ // Paper details panel controls
136
+ d3.select("#closePanelButton").on("click", function() {
137
+ closeDetailsPanel();
138
+ });
139
+
140
+
141
+
142
+
143
+
144
+
145
+
146
+
147
+
148
+
149
+
150
+
151
+
152
+ // Time period tracking
153
+ let allPapers = []; // Store all papers from API
154
+ let currentTimeIndex = 0;
155
+ let timeChunks = []; // Will store our time chunks once we process the data
156
+ let isLoadingTimeChunk = false;
157
+
158
+ // New time navigation event listeners
159
+ d3.select("#prevTimeButton").on("click", function() {
160
+ if (currentTimeIndex > 0 && !isLoadingTimeChunk) {
161
+ currentTimeIndex--;
162
+ loadTimeChunk(currentTimeIndex);
163
+ updateTimeControls();
164
+ }
165
+ });
166
+
167
+ d3.select("#nextTimeButton").on("click", function() {
168
+ if (currentTimeIndex < timeChunks.length - 1 && !isLoadingTimeChunk) {
169
+ currentTimeIndex++;
170
+ loadTimeChunk(currentTimeIndex);
171
+ updateTimeControls();
172
+ }
173
+ });
174
+
175
+ d3.select("#timeSlider").on("input", function() {
176
+ if (!isLoadingTimeChunk) {
177
+ currentTimeIndex = +this.value;
178
+ loadTimeChunk(currentTimeIndex);
179
+ updateTimeControls();
180
+ }
181
+ });
182
+
183
+ // Function to update time navigation controls
184
+ function updateTimeControls() {
185
+ // Update button states
186
+ d3.select("#prevTimeButton").property("disabled", currentTimeIndex === 0);
187
+ d3.select("#nextTimeButton").property("disabled", currentTimeIndex === timeChunks.length - 1);
188
+
189
+ // Update time slider
190
+ d3.select("#timeSlider")
191
+ .property("value", currentTimeIndex)
192
+ .property("max", timeChunks.length - 1);
193
+
194
+ // Update time period indicator
195
+ if (timeChunks.length > 0) {
196
+ const currentChunk = timeChunks[currentTimeIndex];
197
+ d3.select("#currentTimePeriod")
198
+ .text(`${formatDate(currentChunk.startDate)} - ${formatDate(currentChunk.endDate)}`);
199
+
200
+ // Update start and end labels for the entire range
201
+ if (timeChunks.length > 0) {
202
+ d3.select("#startTimeLabel").text(formatDate(timeChunks[0].startDate));
203
+ d3.select("#endTimeLabel").text(formatDate(timeChunks[timeChunks.length - 1].endDate));
204
+ }
205
+ }
206
+ }
207
+
208
+
209
+
210
+ // Function to close the details panel
211
+ function closeDetailsPanel() {
212
+ d3.select("#paperDetailsPanel").style("display", "none");
213
+
214
+ // Deselect any selected node
215
+ if (selectedNode) {
216
+ d3.selectAll(".node").classed("selected", false);
217
+ selectedNode = null;
218
+ }
219
+ }
220
+
221
+ // Function to update node sizes when scale sliders change - optimized
222
+ function updateNodeSizes() {
223
+ if (!simulation) return;
224
+
225
+ // Performance optimization: Batch DOM updates
226
+ // Create a temporary object to store updates
227
+ const updates = [];
228
+
229
+ // Prepare all updates without touching the DOM
230
+ mainGroup.selectAll(".node").each(function(d) {
231
+ const element = d3.select(this);
232
+ const radius = getNodeRadius(d);
233
+
234
+ updates.push({
235
+ element,
236
+ d,
237
+ radius
238
+ });
239
+ });
240
+
241
+ // Now apply all updates in a batch
242
+ updates.forEach(update => {
243
+ const { element, d, radius } = update;
244
+
245
+ if (d.type === 'main' && d.isBridge) {
246
+ // Main bridge papers are stars - scale the star symbol
247
+ const starSize = radius * radius * 3;
248
+ element.select("path")
249
+ .attr("d", d3.symbol().type(d3.symbolStar).size(starSize)());
250
+ } else if (d.isBridge) {
251
+ // Other bridge papers remain diamonds
252
+ element.select("rect")
253
+ .attr("width", radius * 2)
254
+ .attr("height", radius * 2)
255
+ .attr("transform", `rotate(45) translate(${-radius}, ${-radius})`);
256
+ } else {
257
+ // Regular papers stay as circles
258
+ element.select("circle")
259
+ .attr("r", radius);
260
+ }
261
+ });
262
+
263
+ // Update the collision detection radius
264
+ simulation.force("collide").radius(d => getNodeRadius(d) * 1.5);
265
+
266
+ // Use a lower alpha value to make the transition smoother
267
+ simulation.alpha(0.1).restart();
268
+ startSimulationLoop();
269
+ }
270
+
271
+ // Debounced version of updateNodeSizes for better performance
272
+ const debouncedUpdateNodeSizes = debounce(updateNodeSizes, 200);
273
+
274
+ // Add slider event listeners for node scaling with debouncing
275
+ d3.select("#mainNodeScaleSlider").on("input", debounce(function() {
276
+ mainNodeScale = +this.value;
277
+ d3.select("#mainNodeScaleValue").text(mainNodeScale.toFixed(1));
278
+ nodeRadiusCache.clear(); // Clear cache when scale changes
279
+ debouncedUpdateNodeSizes();
280
+ }, 100));
281
+
282
+ d3.select("#bridgeNodeScaleSlider").on("input", debounce(function() {
283
+ bridgeNodeScale = +this.value;
284
+ d3.select("#bridgeNodeScaleValue").text(bridgeNodeScale.toFixed(1));
285
+ nodeRadiusCache.clear(); // Clear cache when scale changes
286
+ debouncedUpdateNodeSizes();
287
+ }, 100));
288
+
289
+ d3.select("#normalNodeScaleSlider").on("input", debounce(function() {
290
+ normalNodeScale = +this.value;
291
+ d3.select("#normalNodeScaleValue").text(normalNodeScale.toFixed(1));
292
+ nodeRadiusCache.clear(); // Clear cache when scale changes
293
+ debouncedUpdateNodeSizes();
294
+ }, 100));
295
+
296
+ d3.select("#resetButton").on("click", function() {
297
+ if (simulation) {
298
+ // Reset node positions to random positions near the center
299
+ simulation.nodes().forEach(node => {
300
+ node.x = width / 2 + (Math.random() - 0.5) * 100;
301
+ node.y = height / 2 + (Math.random() - 0.5) * 100;
302
+ node.vx = 0;
303
+ node.vy = 0;
304
+ });
305
+ simulation.alpha(1).restart();
306
+ startSimulationLoop();
307
+ }
308
+ });
309
+
310
+ // Create zoom behavior early to avoid recreating it
311
+ const zoom = d3.zoom()
312
+ .scaleExtent([0.1, 8])
313
+ .on("zoom", (event) => {
314
+ // Performance optimization: Use transform attribute instead of attr
315
+ mainGroup.attr("transform", event.transform);
316
+ });
317
+
318
+ svg.call(zoom);
319
+
320
+ d3.select("#centerViewButton").on("click", function() {
321
+ // Reset zoom and center the view
322
+ svg.transition()
323
+ .duration(750)
324
+ .call(zoom.transform, d3.zoomIdentity.translate(width/2, height/2).scale(1));
325
+ });
326
+
327
+ // Optimize resize handler with debounce
328
+ const handleResize = debounce(function() {
329
+ width = window.innerWidth;
330
+ height = window.innerHeight;
331
+ svg.attr("width", width).attr("height", height);
332
+ if (simulation) {
333
+ simulation.force("center", d3.forceCenter(width / 2, height / 2));
334
+ simulation.alpha(0.3).restart();
335
+ startSimulationLoop();
336
+ }
337
+ }, 100);
338
+
339
+ // Handle window resize
340
+ window.addEventListener('resize', handleResize);
341
+
342
+ // File input handling
343
+ async function loadCitationData() {
344
+ const progressBar = d3.select("#loadingProgress");
345
+ const statusMessage = d3.select("#statusMessage");
346
+
347
+ try {
348
+ statusMessage.text("Fetching citation data...");
349
+ progressBar.style("width", "0%");
350
+
351
+ // Simulate progress updates
352
+ const interval = setInterval(() => {
353
+ const currentWidth = parseFloat(progressBar.style("width")) || 0;
354
+ if (currentWidth < 90) {
355
+ progressBar.style("width", `${currentWidth + 10}%`);
356
+ }
357
+ }, 200);
358
+
359
+ // // Fetch all data from the backend
360
+ // const response = await fetch('http://localhost:8000/citation-data?userId=abc123&topic=ai in phishing&year=2023');
361
+ // if (!response.ok) {
362
+ // throw new Error(`Failed to load data: ${response.status}`);
363
+ // }
364
+
365
+ // const data = await response.json();
366
+ clearInterval(interval);
367
+ progressBar.style("width", "100%");
368
+
369
+ statusMessage.text("Processing data...");
370
+
371
+ const dataFromBackend = window.__INITIAL_DATA__;
372
+
373
+
374
+ console.log(dataFromBackend , " here data")
375
+ allPapers=dataFromBackend
376
+
377
+ // Process papers into time chunks (3-month periods)
378
+ processTimeChunks(allPapers);
379
+
380
+ // Load the first time chunk
381
+ if (timeChunks.length > 0) {
382
+ loadTimeChunk(0);
383
+ updateTimeControls();
384
+ }
385
+
386
+ statusMessage.text("Citation network loaded successfully!");
387
+ setTimeout(() => {
388
+ progressBar.style("display", "none");
389
+ statusMessage.style("display", "none");
390
+ d3.select(".loading-section").style("display", "none");
391
+ }, 1000);
392
+
393
+ } catch (error) {
394
+ console.error("Error loading citation data:", error);
395
+ statusMessage.text("Failed to load citation network.");
396
+ progressBar.style("width", "0%");
397
+ }
398
+ }
399
+
400
+ // Process papers into 3-month time chunks
401
+ function processTimeChunks(papers) {
402
+ // Sort papers by publication date
403
+ papers.sort((a, b) => {
404
+ const dateA = a.publication_date ? new Date(a.publication_date) : new Date(0);
405
+ const dateB = b.publication_date ? new Date(b.publication_date) : new Date(0);
406
+ return dateA - dateB;
407
+ });
408
+
409
+ // Find min and max dates
410
+ let minDate = new Date();
411
+ let maxDate = new Date(0);
412
+
413
+ papers.forEach(paper => {
414
+ if (paper.publication_date) {
415
+ const date = new Date(paper.publication_date);
416
+ if (date < minDate) minDate = new Date(date);
417
+ if (date > maxDate) maxDate = new Date(date);
418
+ }
419
+ });
420
+
421
+ // Create 3-month chunks
422
+ const chunks = [];
423
+ let currentStart = new Date(minDate);
424
+
425
+ while (currentStart < maxDate) {
426
+ // Calculate end date (3 months later)
427
+ const endDate = new Date(currentStart);
428
+ endDate.setMonth(endDate.getMonth() + 3);
429
+
430
+ // Create the chunk
431
+ chunks.push({
432
+ startDate: new Date(currentStart),
433
+ endDate: new Date(endDate),
434
+ papers: []
435
+ });
436
+
437
+ // Move to next period
438
+ currentStart = new Date(endDate);
439
+ }
440
+
441
+ // Assign papers to chunks
442
+ papers.forEach(paper => {
443
+ if (paper.publication_date) {
444
+ const pubDate = new Date(paper.publication_date);
445
+
446
+ for (let i = 0; i < chunks.length; i++) {
447
+ const chunk = chunks[i];
448
+ if (pubDate >= chunk.startDate && pubDate < chunk.endDate) {
449
+ chunk.papers.push(paper);
450
+ break;
451
+ }
452
+ }
453
+ }
454
+ });
455
+
456
+ // Remove empty chunks
457
+ timeChunks = chunks.filter(chunk => chunk.papers.length > 0);
458
+
459
+ // Update the slider max value
460
+ d3.select("#timeSlider").property("max", timeChunks.length - 1);
461
+ }
462
+
463
+ // Load a specific time chunk
464
+ function loadTimeChunk(index) {
465
+ if (isLoadingTimeChunk || index < 0 || index >= timeChunks.length) return;
466
+
467
+ isLoadingTimeChunk = true;
468
+
469
+ // Show loading indicator
470
+ d3.select("#timeLoader").style("width", "0%");
471
+
472
+ // Animate loading
473
+ const loadingInterval = setInterval(() => {
474
+ const currentWidth = parseFloat(d3.select("#timeLoader").style("width")) || 0;
475
+ if (currentWidth < 90) {
476
+ d3.select("#timeLoader").style("width", `${currentWidth + 10}%`);
477
+ }
478
+ }, 50);
479
+
480
+ // Use setTimeout to allow the UI to update before processing
481
+ setTimeout(() => {
482
+ // Process the current chunk
483
+ const chunkData = timeChunks[index].papers;
484
+ processData(chunkData);
485
+
486
+ // Complete loading
487
+ clearInterval(loadingInterval);
488
+ d3.select("#timeLoader").style("width", "100%");
489
+
490
+ // Hide loader after transition
491
+ setTimeout(() => {
492
+ d3.select("#timeLoader").style("width", "0%");
493
+ isLoadingTimeChunk = false;
494
+ }, 500);
495
+
496
+ }, 100);
497
+ }
498
+
499
+
500
+
501
+ document.addEventListener("DOMContentLoaded", function() {
502
+ loadCitationData();
503
+ });
504
+
505
+ function processData(data) {
506
+ // Stop any running simulation
507
+ if (simulation) {
508
+ simulation.stop();
509
+ stopSimulationLoop();
510
+ }
511
+
512
+ // Reset the SVG content but keep the main structure
513
+ mainGroup.selectAll("*").remove();
514
+ svg.select("defs").remove();
515
+
516
+ // Close any open panels
517
+ closeDetailsPanel();
518
+
519
+ // Redefine markers and star shape
520
+ svg.append("defs").append("marker")
521
+ .attr("id", "arrow")
522
+ .attr("viewBox", "0 -5 10 10")
523
+ .attr("refX", 20)
524
+ .attr("refY", 0)
525
+ .attr("markerWidth", 6)
526
+ .attr("markerHeight", 6)
527
+ .attr("orient", "auto")
528
+ .append("path")
529
+ .attr("d", "M0,-5L10,0L0,5")
530
+ .attr("fill", "#999");
531
+
532
+ svg.append("defs")
533
+ .append("path")
534
+ .attr("id", "star")
535
+ .attr("d", d3.symbol().type(d3.symbolStar).size(300)());
536
+
537
+ // Clear cache when loading new data
538
+ nodeRadiusCache.clear();
539
+
540
+ // Process the data to create nodes and links
541
+ nodes = [];
542
+ links = [];
543
+ const nodeMap = new Map();
544
+
545
+ // Count occurrences of each paper ID to identify bridge papers
546
+ const paperOccurrences = new Map();
547
+
548
+ // Performance optimization: Pre-allocate array size when possible
549
+ // First pass: Count occurrences of papers in references and citations
550
+ data.forEach(paper => {
551
+ // Count the main paper
552
+ incrementPaperCount(paperOccurrences, paper.id);
553
+
554
+ // Count referenced papers
555
+ if (paper.referenced_works) {
556
+ paper.referenced_works.forEach(ref => {
557
+ incrementPaperCount(paperOccurrences, ref);
558
+ });
559
+ }
560
+
561
+ // Count citing papers
562
+ if (paper.cited_by_ids) {
563
+ paper.cited_by_ids.forEach(cite => {
564
+ incrementPaperCount(paperOccurrences, cite);
565
+ });
566
+ }
567
+ });
568
+
569
+ // Performance optimization: Estimate total node and link count
570
+ let estimatedNodeCount = data.length;
571
+ let estimatedLinkCount = 0;
572
+
573
+ data.forEach(paper => {
574
+ if (paper.referenced_works) estimatedNodeCount += paper.referenced_works.length;
575
+ if (paper.cited_by_ids) estimatedNodeCount += paper.cited_by_ids.length;
576
+
577
+ if (paper.referenced_works) estimatedLinkCount += paper.referenced_works.length;
578
+ if (paper.cited_by_ids) estimatedLinkCount += paper.cited_by_ids.length;
579
+ });
580
+
581
+ // Pre-allocate arrays
582
+ nodes = new Array(estimatedNodeCount);
583
+ links = new Array(estimatedLinkCount);
584
+ let nodeIndex = 0;
585
+ let linkIndex = 0;
586
+
587
+ // Second pass: Create nodes and links
588
+ data.forEach(paper => {
589
+ // Add main paper node if not already in the map
590
+ if (!nodeMap.has(paper.id)) {
591
+ const node = {
592
+ id: paper.id,
593
+ title: paper.title || "Unknown Title",
594
+ cited_by_count: paper.cited_by_count || 0,
595
+ concepts: paper.concepts || [],
596
+ publication_date: paper.publication_date || null, // Add publication date
597
+ referenced_works: paper.referenced_works || [],
598
+ cited_by_ids: paper.cited_by_ids || [],
599
+ type: 'main',
600
+ isBridge: paperOccurrences.get(paper.id) > 1,
601
+ // Start main papers near the center
602
+ x: width / 2 + (Math.random() - 0.5) * 100,
603
+ y: height / 2 + (Math.random() - 0.5) * 100
604
+ };
605
+ nodes[nodeIndex++] = node;
606
+ nodeMap.set(paper.id, node);
607
+ }
608
+
609
+ // Add referenced paper nodes and create links
610
+ if (paper.referenced_works) {
611
+ paper.referenced_works.forEach(ref => {
612
+ if (!nodeMap.has(ref)) {
613
+ const node = {
614
+ id: ref,
615
+ title: "Referenced Paper",
616
+ type: 'reference',
617
+ isBridge: paperOccurrences.get(ref) > 1,
618
+ // Start near their respective main paper
619
+ x: nodeMap.get(paper.id).x + (Math.random() - 0.5) * 50,
620
+ y: nodeMap.get(paper.id).y + (Math.random() - 0.5) * 50
621
+ };
622
+ nodes[nodeIndex++] = node;
623
+ nodeMap.set(ref, node);
624
+ }
625
+
626
+ links[linkIndex++] = {
627
+ source: paper.id,
628
+ target: ref,
629
+ direction: 'outgoing'
630
+ };
631
+ });
632
+ }
633
+
634
+ // Add citing paper nodes and create links
635
+ if (paper.cited_by_ids) {
636
+ paper.cited_by_ids.forEach(cite => {
637
+ if (!nodeMap.has(cite)) {
638
+ const node = {
639
+ id: cite,
640
+ title: "Citing Paper",
641
+ type: 'citation',
642
+ isBridge: paperOccurrences.get(cite) > 1,
643
+ // Start near their respective main paper
644
+ x: nodeMap.get(paper.id).x + (Math.random() - 0.5) * 50,
645
+ y: nodeMap.get(paper.id).y + (Math.random() - 0.5) * 50
646
+ };
647
+ nodes[nodeIndex++] = node;
648
+ nodeMap.set(cite, node);
649
+ }
650
+
651
+ links[linkIndex++] = {
652
+ source: cite,
653
+ target: paper.id,
654
+ direction: 'incoming'
655
+ };
656
+ });
657
+ }
658
+ });
659
+
660
+ // Trim arrays to actual size
661
+ nodes = nodes.slice(0, nodeIndex);
662
+ links = links.slice(0, linkIndex);
663
+
664
+ createVisualization(nodes, links);
665
+ }
666
+
667
+ function incrementPaperCount(map, id) {
668
+ if (!map.has(id)) {
669
+ map.set(id, 1);
670
+ } else {
671
+ map.set(id, map.get(id) + 1);
672
+ }
673
+ }
674
+
675
+ function createVisualization(nodes, links) {
676
+ // Performance optimization: Use quadtree for faster force calculation
677
+ simulation = d3.forceSimulation(nodes)
678
+ .force("link", d3.forceLink(links).id(d => d.id).distance(linkDistance))
679
+ .force("charge", d3.forceManyBody().strength(chargeStrength).theta(0.8)) // Higher theta = better performance
680
+ .force("center", d3.forceCenter(width / 2, height / 2))
681
+ .force("gravity", d3.forceRadial(0, width / 2, height / 2).strength(gravityStrength))
682
+ // Add collision detection to prevent node overlap
683
+ .force("collide", d3.forceCollide().radius(d => getNodeRadius(d) * 1.5).strength(0.7)); // Reduced strength for better performance
684
+
685
+ // Performance: Lower alpha decay for smoother but faster convergence
686
+ simulation.alphaDecay(0.02);
687
+
688
+ // Performance: Remove automatic ticking and handle it via requestAnimationFrame
689
+ simulation.on("tick", null);
690
+
691
+ // Create link group first (rendered below nodes)
692
+ const linkGroup = mainGroup.append("g").attr("class", "links");
693
+
694
+ // Performance optimization: Use object pooling for links
695
+ const link = linkGroup.selectAll("line")
696
+ .data(links)
697
+ .enter().append("line")
698
+ .attr("class", "link")
699
+ .attr("stroke", "#999")
700
+ .attr("stroke-width", 1)
701
+ .attr("marker-end", "url(#arrow)");
702
+
703
+ // Create node group
704
+ const nodeGroup = mainGroup.append("g").attr("class", "nodes");
705
+
706
+ // Create a more efficient node container
707
+ const node = nodeGroup.selectAll(".node")
708
+ .data(nodes)
709
+ .enter().append("g")
710
+ .attr("class", "node")
711
+ .call(d3.drag()
712
+ .on("start", dragstarted)
713
+ .on("drag", dragged)
714
+ .on("end", dragended)
715
+ )
716
+ .on("click", nodeClicked);
717
+
718
+ // Performance optimization: Batch DOM operations
719
+ // Add different shapes based on node type
720
+ node.each(function(d) {
721
+ const element = d3.select(this);
722
+ const radius = getNodeRadius(d);
723
+
724
+ if (d.type === 'main' && d.isBridge) {
725
+ // Main bridge papers are stars
726
+ const starSize = radius * radius * 3;
727
+ element.append("path")
728
+ .attr("d", d3.symbol().type(d3.symbolStar).size(starSize)())
729
+ .attr("fill", "#4285f4");
730
+ } else if (d.isBridge) {
731
+ // Bridge papers are diamonds
732
+ element.append("rect")
733
+ .attr("width", radius * 2)
734
+ .attr("height", radius * 2)
735
+ .attr("transform", `rotate(45) translate(${-radius}, ${-radius})`)
736
+ .attr("fill", "#fbbc05");
737
+ } else if (d.type === 'main') {
738
+ // Main papers are blue circles
739
+ element.append("circle")
740
+ .attr("r", radius)
741
+ .attr("fill", "#4285f4");
742
+ } else if (d.type === 'reference') {
743
+ // Referenced papers are green circles
744
+ element.append("circle")
745
+ .attr("r", radius)
746
+ .attr("fill", "#34a853");
747
+ } else if (d.type === 'citation') {
748
+ // Citing papers are red circles
749
+ element.append("circle")
750
+ .attr("r", radius)
751
+ .attr("fill", "#ea4335");
752
+ } else {
753
+ // Default case - grey circles
754
+ element.append("circle")
755
+ .attr("r", radius)
756
+ .attr("fill", "#999");
757
+ }
758
+ });
759
+
760
+ // Optimized tooltip handling
761
+ const tooltip = d3.select(".tooltip");
762
+
763
+ // Performance optimization: Use event delegation
764
+ // Update inside the mouseoverEvent handler in the createVisualization function
765
+ nodeGroup.on("mouseover", function(event) {
766
+ const target = event.target;
767
+ if (target.closest(".node")) {
768
+ const d = d3.select(target.closest(".node")).datum();
769
+
770
+ // Prepare tooltip content
771
+ let tooltipContent = `<strong>${d.title}</strong>`;
772
+
773
+ // Add publication date if available
774
+ if (d.publication_date) {
775
+ tooltipContent += `<br>Published: ${formatDate(d.publication_date)}`;
776
+ }
777
+
778
+ // Add citation count if available
779
+ if (d.cited_by_count) {
780
+ tooltipContent += `<br>Citations: ${d.cited_by_count}`;
781
+ }
782
+
783
+ // Add concepts if available
784
+ if (d.concepts && d.concepts.length > 0) {
785
+ tooltipContent += `<br>Concepts: ${formatConcepts(d.concepts)}`;
786
+ }
787
+
788
+ // Add bridge paper info if applicable
789
+ if (d.isBridge) {
790
+ tooltipContent += "<br><em>Bridge Paper</em>";
791
+ }
792
+
793
+ // Show tooltip with enhanced paper information
794
+ tooltip.style("display", "block")
795
+ .style("left", (event.pageX + 15) + "px")
796
+ .style("top", (event.pageY - 30) + "px")
797
+ .html(tooltipContent);
798
+ }
799
+ })
800
+ .on("mouseout", function(event) {
801
+ const target = event.target;
802
+ if (target.closest(".node")) {
803
+ tooltip.style("display", "none");
804
+ }
805
+ })
806
+ .on("mousemove", function(event) {
807
+ tooltip.style("left", (event.pageX + 15) + "px")
808
+ .style("top", (event.pageY - 30) + "px");
809
+ });
810
+
811
+ // Start simulation loop using requestAnimationFrame
812
+ startSimulationLoop();
813
+ }
814
+
815
+ // Calculate node radius based on node type and importance
816
+ function getNodeRadius(d) {
817
+ // Return from cache if already calculated
818
+ if (nodeRadiusCache.has(d.id)) {
819
+ return nodeRadiusCache.get(d.id);
820
+ }
821
+
822
+ let radius;
823
+
824
+ if (d.type === 'main') {
825
+ // Main papers: size based on citation count and scaled by mainNodeScale
826
+ const baseCitationSize = Math.max(5, Math.min(15, 5 + Math.log(d.cited_by_count + 1)));
827
+ radius = baseCitationSize * mainNodeScale;
828
+ } else if (d.isBridge) {
829
+ // Bridge papers: slightly larger than normal papers and scaled by bridgeNodeScale
830
+ radius = 6 * bridgeNodeScale;
831
+ } else {
832
+ // Referenced or citing papers: normal size and scaled by normalNodeScale
833
+ radius = 4 * normalNodeScale;
834
+ }
835
+
836
+ // Cache the result
837
+ nodeRadiusCache.set(d.id, radius);
838
+ return radius;
839
+ }
840
+
841
+ // Drag functions for nodes - optimized
842
+ function dragstarted(event, d) {
843
+ stopSimulationLoop(); // Stop animation loop during drag
844
+ if (!event.active) simulation.alphaTarget(0.3);
845
+ d.fx = d.x;
846
+ d.fy = d.y;
847
+ }
848
+
849
+ function dragged(event, d) {
850
+ d.fx = event.x;
851
+ d.fy = event.y;
852
+
853
+ // Update position immediately for smoother dragging
854
+ d3.select(event.sourceEvent.target.closest(".node"))
855
+ .attr("transform", `translate(${d.fx}, ${d.fy})`);
856
+
857
+ // Update connected links for immediate feedback
858
+ mainGroup.selectAll(".link").each(function(linkData) {
859
+ if (linkData.source === d || linkData.target === d) {
860
+ d3.select(this)
861
+ .attr("x1", linkData.source.x)
862
+ .attr("y1", linkData.source.y)
863
+ .attr("x2", linkData.target.x)
864
+ .attr("y2", linkData.target.y);
865
+ }
866
+ });
867
+ }
868
+
869
+ function dragended(event, d) {
870
+ if (!event.active) simulation.alphaTarget(0);
871
+ // Comment these out to keep the node fixed at the position where it was dragged
872
+ // d.fx = null;
873
+ // d.fy = null;
874
+
875
+ // Restart simulation loop
876
+ startSimulationLoop();
877
+ }
878
+
879
+ // Helper function to format publication date
880
+ function formatDate(dateString) {
881
+ if (!dateString) return "Unknown";
882
+
883
+ try {
884
+ const date = new Date(dateString);
885
+ // Check if date is valid
886
+ if (isNaN(date.getTime())) return dateString;
887
+
888
+ return date.toLocaleDateString('en-US', {
889
+ year: 'numeric',
890
+ month: 'short',
891
+ day: 'numeric'
892
+ });
893
+ } catch (e) {
894
+ return dateString;
895
+ }
896
+ }
897
+
898
+ // Helper function to format concepts
899
+ function formatConcepts(concepts) {
900
+ if (!concepts || concepts.length === 0) return "None";
901
+
902
+ // Handle both string concepts and concept objects
903
+ const conceptNames = concepts.map(concept => {
904
+ if (typeof concept === 'string') return concept;
905
+ return concept.display_name || concept.name || "Unknown concept";
906
+ });
907
+
908
+ // Return first 3 concepts to keep tooltip compact
909
+ if (conceptNames.length > 3) {
910
+ return conceptNames.slice(0, 3).join(", ") + ` +${conceptNames.length - 3} more`;
911
+ }
912
+
913
+ return conceptNames.join(", ");
914
+ }
915
+
916
+ // Handle node click to show paper details - optimized
917
+ // Update the nodeClicked function
918
+ function nodeClicked(event, d) {
919
+ event.stopPropagation(); // Prevent bubbling to SVG
920
+
921
+ // Set as selected node
922
+ selectedNode = d;
923
+
924
+ // Update visual selection - optimized to only change classes for relevant nodes
925
+ d3.selectAll(".node.selected").classed("selected", false);
926
+ d3.select(this).classed("selected", true);
927
+
928
+ // Get paper details from the original data if it's a main paper
929
+ let paperDetails = d;
930
+ if (d.type === 'main' && originalData) {
931
+ paperDetails = originalData.find(paper => paper.id === d.id) || d;
932
+ }
933
+
934
+ // Update paper info panel - batch DOM updates
935
+ const paperInfoUpdates = {
936
+ "#paperTitle": paperDetails.title || "Unknown Title",
937
+ "#paperCitations": paperDetails.cited_by_count !== undefined ? `Citations: ${paperDetails.cited_by_count}` : ""
938
+ };
939
+
940
+ // Add publication date if available
941
+ if (paperDetails.publication_date) {
942
+ paperInfoUpdates["#paperType"] = `${d.type === 'main' ? "Main Paper" :
943
+ d.type === 'reference' ? "Referenced Paper" :
944
+ d.type === 'citation' ? "Citing Paper" : "Paper"}${d.isBridge ? " (Bridge)" : ""} · Published: ${formatDate(paperDetails.publication_date)}`;
945
+ } else {
946
+ // Set paper type without date
947
+ let typeText = "";
948
+ if (d.type === 'main') typeText = "Main Paper";
949
+ else if (d.type === 'reference') typeText = "Referenced Paper";
950
+ else if (d.type === 'citation') typeText = "Citing Paper";
951
+
952
+ if (d.isBridge) typeText += " (Bridge)";
953
+ paperInfoUpdates["#paperType"] = typeText;
954
+ }
955
+
956
+ // Apply all text updates in one batch
957
+ Object.entries(paperInfoUpdates).forEach(([selector, text]) => {
958
+ d3.select(selector).text(text);
959
+ });
960
+
961
+ // Set concepts if available - use document fragment for better performance
962
+ const conceptsDiv = d3.select("#paperConcepts");
963
+ conceptsDiv.html("");
964
+
965
+ if (paperDetails.concepts && paperDetails.concepts.length > 0) {
966
+ // Performance: Create document fragment for batch append
967
+ const fragment = document.createDocumentFragment();
968
+
969
+ // Add a small label for concepts
970
+ const conceptLabel = document.createElement("div");
971
+ conceptLabel.className = "concept-label";
972
+ conceptLabel.textContent = "Concepts:";
973
+ fragment.appendChild(conceptLabel);
974
+
975
+ paperDetails.concepts.forEach(concept => {
976
+ const span = document.createElement("span");
977
+ span.className = "concept-tag";
978
+ span.textContent = typeof concept === 'string' ? concept : (concept.display_name || concept.name || "Unknown");
979
+ fragment.appendChild(span);
980
+ });
981
+
982
+ conceptsDiv.node().appendChild(fragment);
983
+ }
984
+
985
+ // Set OpenAlex link
986
+ d3.select("#openAlexLink")
987
+ .attr("href", `${d.id}`);
988
+
989
+ // Performance optimization: Batch DOM operations for related papers lists
990
+ // Use setTimeout to defer heavy operations and keep UI responsive
991
+ setTimeout(() => {
992
+ populateRelatedPapersList(paperDetails, "citing");
993
+ populateRelatedPapersList(paperDetails, "referenced");
994
+
995
+ // Show the panel
996
+ d3.select("#paperDetailsPanel").style("display", "block");
997
+ }, 0);
998
+ }
999
+
1000
+ // Populate the lists of citing and referenced papers - optimized
1001
+ function populateRelatedPapersList(paperDetails, listType) {
1002
+ const listElement = listType === "citing" ?
1003
+ d3.select("#citingPapersList") :
1004
+ d3.select("#referencedPapersList");
1005
+
1006
+ listElement.html("");
1007
+
1008
+ const paperIds = listType === "citing" ?
1009
+ paperDetails.cited_by_ids :
1010
+ paperDetails.referenced_works;
1011
+
1012
+ if (!paperIds || paperIds.length === 0) {
1013
+ listElement.append("li")
1014
+ .text("No papers available");
1015
+ return;
1016
+ }
1017
+
1018
+ // Performance optimization: Use document fragment for batch DOM operations
1019
+ const fragment = document.createDocumentFragment();
1020
+
1021
+ // Get paper details if available in original data
1022
+ paperIds.forEach(id => {
1023
+ const li = document.createElement("li");
1024
+
1025
+ // Try to find paper details in original data
1026
+ let paperInfo = null;
1027
+ if (originalData) {
1028
+ paperInfo = originalData.find(paper => paper.id === id);
1029
+ }
1030
+
1031
+ const a = document.createElement("a");
1032
+ if (paperInfo) {
1033
+ a.href = "#";
1034
+ a.setAttribute("data-id", id);
1035
+ a.textContent = paperInfo.title || id;
1036
+ a.addEventListener("click", function(event) {
1037
+ event.preventDefault();
1038
+ const paperId = this.getAttribute("data-id");
1039
+ const nodeElement = mainGroup.selectAll(".node").filter(d => d.id === paperId);
1040
+ if (!nodeElement.empty()) {
1041
+ nodeClicked.call(nodeElement.node(), event, nodeElement.datum());
1042
+ }
1043
+ });
1044
+ } else {
1045
+ a.href = `${id}`;
1046
+ a.target = "_blank";
1047
+ a.textContent = id;
1048
+ }
1049
+
1050
+ li.appendChild(a);
1051
+ fragment.appendChild(li);
1052
+ });
1053
+
1054
+ listElement.node().appendChild(fragment);
1055
+ }
1056
+
1057
+ // Close details panel when clicking on the background
1058
+ svg.on("click", function() {
1059
+ closeDetailsPanel();
1060
+ });
1061
+
1062
+ // Clean up resources when window is closed or component is unmounted
1063
+ window.addEventListener('beforeunload', function() {
1064
+ if (simulation) {
1065
+ simulation.stop();
1066
+ }
1067
+ stopSimulationLoop();
1068
+ });
1069
+
1070
+ // Load sample data or wait for user upload
1071
+ document.addEventListener("DOMContentLoaded", function() {
1072
+ // Optional: Load sample data automatically
1073
+ // fetch('sample_data.json')
1074
+ // .then(response => response.json())
1075
+ // .then(data => {
1076
+ // originalData = data;
1077
+ // processData(data);
1078
+ // })
1079
+ // .catch(error => console.error('Error loading sample data:', error));
1080
+ });
static/js/home.js ADDED
@@ -0,0 +1,1627 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const getLocalUser = localStorage.getItem("username");
2
+ if (getLocalUser) {
3
+ document.getElementById("userProfile").innerHTML = `${getLocalUser}`;
4
+ } else {
5
+ document.getElementById("userProfile").innerHTML = "Hei";
6
+ }
7
+
8
+ function populateYearDropdown(startYear, endYear) {
9
+ const dropdown = document.getElementById('year-filter');
10
+
11
+ // Clear existing options (except the first default option)
12
+ while (dropdown.options.length > 1) {
13
+ dropdown.remove(1);
14
+ }
15
+
16
+ // Add years dynamically
17
+ for (let year = endYear; year >= startYear; year--) {
18
+ const option = document.createElement('option');
19
+ option.value = year;
20
+ option.textContent = year;
21
+ dropdown.appendChild(option);
22
+ }
23
+ }
24
+
25
+ // Populate the dropdown with years from 1900 to 2025
26
+ populateYearDropdown(1900, 2025);
27
+
28
+ const profileContainer = document.getElementById("profileContainer");
29
+ const signInBtn = document.getElementById("signInBtn");
30
+ const storedUsername = localStorage.getItem("username");
31
+ const storedUserId = localStorage.getItem("userId")
32
+
33
+ if (storedUsername) {
34
+ profileContainer.style.display = "flex";
35
+ signInBtn.style.display = "none";
36
+ } else {
37
+ profileContainer.style.display = "none";
38
+ signInBtn.style.display = "block";
39
+ }
40
+
41
+ // Elements for profile dropdown and overlay
42
+ const userProfile = document.getElementById("userProfile");
43
+ const profileDropdown = document.getElementById("profileDropdown");
44
+ const profileOverlay = document.getElementById("profileOverlay");
45
+ const logoutBtn = document.getElementById("logoutBtn");
46
+
47
+ // Toggle dropdown on clicking profile trigger
48
+ userProfile.addEventListener("click", () => {
49
+ profileDropdown.classList.add("active");
50
+ profileOverlay.classList.add("active");
51
+ });
52
+
53
+ // Close dropdown when clicking outside
54
+ profileOverlay.addEventListener("click", () => {
55
+ profileDropdown.classList.remove("active");
56
+ profileOverlay.classList.remove("active");
57
+ });
58
+
59
+ async function saveSearchToFirebase(query, year, feature) {
60
+ const auth = firebase.auth();
61
+ const db = firebase.firestore();
62
+ const user = auth.currentUser;
63
+ if (!user) {
64
+ console.log("User not logged in, cannot save search history");
65
+ return;
66
+ }
67
+
68
+ try {
69
+ // Create search activity object
70
+ const searchData = {
71
+ type: "search",
72
+ query: query,
73
+ year: year || "All Years",
74
+ feature: feature,
75
+ timestamp: firebase.firestore.FieldValue.serverTimestamp()
76
+ };
77
+
78
+ // Add to Firestore
79
+ await db.collection("users")
80
+ .doc(user.uid)
81
+ .collection("activities")
82
+ .add(searchData);
83
+
84
+ console.log("Search activity saved successfully");
85
+ } catch (error) {
86
+ console.error("Error saving search activity:", error);
87
+ }
88
+ }
89
+
90
+
91
+ document.addEventListener("DOMContentLoaded", async () => {
92
+ // Input fields for personal info
93
+ const profileName = document.getElementById("profileName");
94
+ const profileEmail = document.getElementById("profileEmail");
95
+ const profilePhone = document.getElementById("profilePhone");
96
+ const profileAddress = document.getElementById("profileAddress");
97
+ const profileProfession = document.getElementById("profileProfession");
98
+ const profileHeaderName = document.getElementById("profileHeaderName");
99
+
100
+ // Firebase Auth & Firestore setup
101
+ const auth = firebase.auth();
102
+ const db = firebase.firestore();
103
+
104
+ auth.onAuthStateChanged(async (user) => {
105
+ if (user) {
106
+ const userId = user.uid;
107
+
108
+ // Load saved personal info from localStorage
109
+ profileName.value = localStorage.getItem("username") || "";
110
+ profilePhone.value = localStorage.getItem("phone") || "";
111
+ profileAddress.value = localStorage.getItem("address") || "";
112
+ profileProfession.value = localStorage.getItem("profession") || "";
113
+ profileHeaderName.innerHTML = `Welcome, ${storedUsername}`;
114
+
115
+
116
+
117
+ try {
118
+ // Fetch additional user details from Firestore
119
+ const userDoc = await db.collection("users").doc(userId).get();
120
+ if (userDoc.exists) {
121
+ const userData = userDoc.data();
122
+ if (!profileName.value) profileName.value = userData.name || "";
123
+ if (!profilePhone.value) profilePhone.value = userData.phone || "";
124
+ if (!profileAddress.value) profileAddress.value = userData.address || "";
125
+ if (!profileProfession.value) profileProfession.value = userData.profession || "";
126
+ }
127
+ } catch (error) {
128
+ console.error("Error fetching user data:", error);
129
+ }
130
+ }
131
+ });
132
+
133
+ // Enable editing for profile fields
134
+ window.enableEdit = (fieldId) => {
135
+ const inputField = document.getElementById(fieldId);
136
+ inputField.removeAttribute("disabled");
137
+ inputField.focus();
138
+ inputField.addEventListener("blur", () => {
139
+ // When leaving, set back to readonly (or disabled if you prefer)
140
+ inputField.setAttribute("disabled", true);
141
+ });
142
+ };
143
+
144
+ // Tab switching for profile dropdown
145
+ const tabButtons = document.querySelectorAll(".tab-btn");
146
+ const tabContents = document.querySelectorAll(".tab-content");
147
+
148
+ tabButtons.forEach((btn) => {
149
+ btn.addEventListener("click", () => {
150
+ const tab = btn.getAttribute("data-tab");
151
+ tabButtons.forEach((b) => b.classList.remove("active"));
152
+ tabContents.forEach((c) => c.classList.remove("active"));
153
+ btn.classList.add("active");
154
+ document.getElementById(`${tab}-tab`).classList.add("active");
155
+ });
156
+ });
157
+
158
+ // Avatar upload preview
159
+ const avatarUpload = document.getElementById("avatarUpload");
160
+ const profileAvatar = document.getElementById("profileAvatar");
161
+
162
+ if (avatarUpload) {
163
+ avatarUpload.addEventListener("change", (e) => {
164
+ const file = e.target.files[0];
165
+ if (file) {
166
+ const reader = new FileReader();
167
+ reader.onload = (event) => {
168
+ profileAvatar.src = event.target.result;
169
+ // (Optional) Save the avatar image to your storage if needed
170
+ };
171
+ reader.readAsDataURL(file);
172
+ }
173
+ });
174
+ }
175
+
176
+ // Save profile details and preferences
177
+ const saveBtn = document.getElementById("saveBtn");
178
+ saveBtn.addEventListener("click", async () => {
179
+ const user = auth.currentUser;
180
+ if (!user) {
181
+ alert("No user logged in!");
182
+ return;
183
+ }
184
+ const userId = user.uid;
185
+
186
+ // Gather updated personal info
187
+ const updatedName = profileName.value.trim();
188
+ const updatedPhone = profilePhone.value.trim();
189
+ const updatedAddress = profileAddress.value.trim();
190
+ const updatedProfession = profileProfession.value.trim();
191
+
192
+ // Save personal info in localStorage
193
+ localStorage.setItem("username", updatedName);
194
+ localStorage.setItem("phone", updatedPhone);
195
+ localStorage.setItem("address", updatedAddress);
196
+ localStorage.setItem("profession", updatedProfession);
197
+
198
+ // Store personal info in Firestore
199
+ try {
200
+ await db.collection("users").doc(userId).set(
201
+ {
202
+ name: updatedName,
203
+ phone: updatedPhone,
204
+ address: updatedAddress,
205
+ profession: updatedProfession,
206
+
207
+ },
208
+ { merge: true }
209
+ );
210
+ userProfile.textContent = updatedName; // Update navbar username
211
+ showPaperAlert(
212
+ "Successfully Updated!",
213
+ "Profile Updation Status",
214
+ "fa-exclamation-circle",
215
+ 4000
216
+ );
217
+ } catch (error) {
218
+ console.error("Error updating profile:", error);
219
+ showPaperAlert(
220
+ "Error in Updation",
221
+ "Profile Updation Status",
222
+ "fa-exclamation-circle",
223
+ 4000
224
+ );
225
+ }
226
+ });
227
+
228
+ // Logout functionality
229
+ logoutBtn.addEventListener("click", async () => {
230
+ try {
231
+ await auth.signOut();
232
+ localStorage.clear();
233
+ window.location.href = "/home";
234
+ } catch (error) {
235
+ console.error("Logout error:", error);
236
+ }
237
+ });
238
+ });
239
+ // Paper animation class
240
+ class Paper {
241
+ constructor(x, y) {
242
+ this.x = x;
243
+ this.y = y;
244
+ this.size = Math.random() * 15 + 5;
245
+ this.rotation = Math.random() * 360;
246
+ this.speedX = Math.random() * 3 - 1.5;
247
+ this.speedY = Math.random() * 3 - 1.5;
248
+ this.rotationSpeed = Math.random() * 2 - 1;
249
+ this.opacity = 1;
250
+ this.life = 100;
251
+ this.element = document.createElement('div');
252
+ this.element.className = 'paper';
253
+ this.element.style.width = `${this.size}px`;
254
+ this.element.style.height = `${this.size}px`;
255
+ this.element.style.left = `${this.x}px`;
256
+ this.element.style.top = `${this.y}px`;
257
+ this.element.style.transform = `rotate(${this.rotation}deg)`;
258
+ this.element.style.opacity = this.opacity;
259
+ document.body.appendChild(this.element);
260
+ }
261
+
262
+ update() {
263
+ this.life--;
264
+ this.x += this.speedX;
265
+ this.y += this.speedY;
266
+ this.rotation += this.rotationSpeed;
267
+ this.opacity = this.life / 100;
268
+
269
+ this.element.style.left = `${this.x}px`;
270
+ this.element.style.top = `${this.y}px`;
271
+ this.element.style.transform = `rotate(${this.rotation}deg)`;
272
+ this.element.style.opacity = this.opacity;
273
+
274
+ if (this.life <= 0) {
275
+ this.element.remove();
276
+ return false;
277
+ }
278
+ return true;
279
+ }
280
+ }
281
+
282
+ // Paper animation
283
+ let papers = [];
284
+ let frameCount = 0;
285
+
286
+ document.addEventListener('mousemove', (e) => {
287
+ if (frameCount % 2 === 0) {
288
+ papers.push(new Paper(e.clientX, e.clientY));
289
+ }
290
+ });
291
+
292
+ function animate() {
293
+ frameCount++;
294
+ papers = papers.filter(paper => paper.update());
295
+ requestAnimationFrame(animate);
296
+ }
297
+
298
+ animate();
299
+
300
+ // Beam connections animation
301
+ document.addEventListener('DOMContentLoaded', () => {
302
+ // Get references to all circles
303
+ const container = document.getElementById('beam-container');
304
+ const circle1 = document.getElementById('circle1');
305
+ const circle2 = document.getElementById('circle2');
306
+ const circle3 = document.getElementById('circle3');
307
+ const circle4 = document.getElementById('circle4');
308
+ const circle5 = document.getElementById('circle5');
309
+ const circle6 = document.getElementById('circle6');
310
+ const circle7 = document.getElementById('circle7');
311
+
312
+ // Get SVG container
313
+ const svgContainer = document.getElementById('beams-svg');
314
+
315
+ // Create beam connections
316
+ createBeam(circle1, circle6);
317
+ createBeam(circle2, circle6);
318
+ createBeam(circle3, circle6);
319
+ createBeam(circle4, circle6);
320
+ createBeam(circle5, circle6);
321
+ createBeam(circle6, circle7);
322
+
323
+ // Handle window resize
324
+ window.addEventListener('resize', () => {
325
+ // Clear existing beams
326
+ svgContainer.innerHTML = '';
327
+
328
+ // Recreate beams
329
+ createBeam(circle1, circle6);
330
+ createBeam(circle2, circle6);
331
+ createBeam(circle3, circle6);
332
+ createBeam(circle4, circle6);
333
+ createBeam(circle5, circle6);
334
+ createBeam(circle6, circle7);
335
+ });
336
+
337
+ // Function to create an animated beam between two elements
338
+ function createBeam(fromElement, toElement) {
339
+ // Get positions
340
+ const fromRect = fromElement.getBoundingClientRect();
341
+ const toRect = toElement.getBoundingClientRect();
342
+ const containerRect = container.getBoundingClientRect();
343
+
344
+ // Calculate center points relative to the container
345
+ const fromX = fromRect.left + fromRect.width / 2 - containerRect.left;
346
+ const fromY = fromRect.top + fromRect.height / 2 - containerRect.top;
347
+ const toX = toRect.left + toRect.width / 2 - containerRect.left;
348
+ const toY = toRect.top + toRect.height / 2 - containerRect.top;
349
+
350
+ // Create the base beam line
351
+ const beam = document.createElementNS('http://www.w3.org/2000/svg', 'line');
352
+ beam.setAttribute('x1', fromX);
353
+ beam.setAttribute('y1', fromY);
354
+ beam.setAttribute('x2', toX);
355
+ beam.setAttribute('y2', toY);
356
+ beam.classList.add('beam');
357
+ svgContainer.appendChild(beam);
358
+
359
+ // Create the glowing beam
360
+ const beamGlow = document.createElementNS('http://www.w3.org/2000/svg', 'line');
361
+ beamGlow.setAttribute('x1', fromX);
362
+ beamGlow.setAttribute('y1', fromY);
363
+ beamGlow.setAttribute('x2', toX);
364
+ beamGlow.setAttribute('y2', toY);
365
+ beamGlow.classList.add('beam-glow');
366
+ svgContainer.appendChild(beamGlow);
367
+ }
368
+ });
369
+
370
+ // Data flow animation
371
+ const inputFiles = [
372
+ { y: 120, ext: 'ieee' },
373
+ { y: 160, ext: 'arxiv' },
374
+ { y: 200, ext: 'PMC' },
375
+ { y: 240, ext: 'jstor' },
376
+ { y: 280, ext: 'plos' }
377
+ ];
378
+
379
+ const outputFiles = [
380
+ { label: 'Trend Analysis', y: 100, logoSrc: '/assets/c1.png' },
381
+ { label: 'Citation Network', y: 180, logoSrc: '/assets/c2.png' },
382
+ { label: 'Venue & Publisher', y: 260, logoSrc: '/assets/c3.png' },
383
+ ];
384
+
385
+ const svgContainer = document.querySelector('.paths-container');
386
+ const centerBox = document.querySelector('.center-box');
387
+
388
+ // Animation state
389
+ let isAnimating = false;
390
+
391
+ function createInputPath(startY) {
392
+ const startX = 100;
393
+ const endX = 400;
394
+ const endY = 200;
395
+ const controlX = (startX + endX) / 2;
396
+ return `M ${startX} ${startY} C ${controlX} ${startY}, ${controlX} ${endY}, ${endX} ${endY}`;
397
+ }
398
+
399
+ function createOutputPath(endY) {
400
+ const startX = 400;
401
+ const startY = 200;
402
+ const endX = 680;
403
+ const controlX = (startX + endX) / 2;
404
+ return `M ${startX} ${startY} C ${controlX} ${startY}, ${controlX} ${endY}, ${endX} ${endY}`;
405
+ }
406
+
407
+ function createInputPaper(data) {
408
+ const container = document.createElement('div');
409
+ container.className = 'paper-container';
410
+
411
+ const paperIcon = document.createElement('div');
412
+ paperIcon.className = 'paper-icon';
413
+ paperIcon.textContent = data.ext;
414
+
415
+ const label = document.createElement('span');
416
+ label.textContent = data.label || data.ext;
417
+
418
+ const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
419
+ path.setAttribute('class', 'path');
420
+ path.setAttribute('d', createInputPath(data.y));
421
+ svgContainer.appendChild(path);
422
+
423
+ container.appendChild(paperIcon);
424
+ container.appendChild(label);
425
+ document.querySelector('.container').appendChild(container);
426
+
427
+ container.style.left = '70px';
428
+ container.style.top = `${data.y}px`;
429
+
430
+ return { container, paperIcon, path, ext: data.ext };
431
+ }
432
+
433
+ function createOutputBox(data) {
434
+ const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
435
+ path.setAttribute('class', 'path');
436
+ path.setAttribute('d', createOutputPath(data.y));
437
+ svgContainer.appendChild(path);
438
+
439
+ // Create output box
440
+ const outputBox = document.createElement('div');
441
+ outputBox.className = 'output-box';
442
+ document.querySelector('.container').appendChild(outputBox);
443
+ outputBox.style.top = `${data.y}px`;
444
+
445
+ // Create logo image element
446
+ const logo = document.createElement('img');
447
+ logo.className = 'output-logo';
448
+ logo.src = data.logoSrc;
449
+ logo.alt = data.label + ' logo';
450
+ outputBox.appendChild(logo);
451
+
452
+ // Add title below the box
453
+ const title = document.createElement('div');
454
+ title.className = 'box-title';
455
+ title.textContent = data.label;
456
+ outputBox.appendChild(title);
457
+
458
+ return { path, outputBox };
459
+ }
460
+
461
+ function animatePaperAlongPath(path, ext, onComplete) {
462
+ const movingPaper = document.createElement('div');
463
+ movingPaper.className = 'moving-paper';
464
+ movingPaper.textContent = ext;
465
+ document.querySelector('.container').appendChild(movingPaper);
466
+
467
+ const length = path.getTotalLength();
468
+ let start = null;
469
+ const duration = 1200;
470
+
471
+ path.style.strokeOpacity = "1"; // Make line visible when animation starts
472
+ path.style.strokeDasharray = `${length}, ${length}`;
473
+ path.style.strokeDashoffset = length;
474
+
475
+ function animate(timestamp) {
476
+ if (!start) start = timestamp;
477
+ const progress = (timestamp - start) / duration;
478
+
479
+ if (progress <= 1) {
480
+ const point = path.getPointAtLength(length * progress);
481
+ movingPaper.style.left = `${point.x}px`;
482
+ movingPaper.style.top = `${point.y}px`;
483
+
484
+ // Reveal only the traveled portion of the path
485
+ path.style.strokeDashoffset = length * (1 - progress);
486
+
487
+ requestAnimationFrame(animate);
488
+ } else {
489
+ movingPaper.remove();
490
+ path.style.strokeOpacity = "0"; // Hide line when animation completes
491
+ if (onComplete) onComplete();
492
+ }
493
+ }
494
+
495
+ requestAnimationFrame(animate);
496
+ }
497
+
498
+
499
+ function animateDotAlongPath(path, outputBox, onComplete) {
500
+ const movingDot = document.createElement('div');
501
+ movingDot.className = 'moving-dot';
502
+ document.querySelector('.container').appendChild(movingDot);
503
+
504
+ const length = path.getTotalLength();
505
+ let start = null;
506
+ const duration = 1000;
507
+
508
+ path.style.strokeOpacity = "1"; // Make line visible when animation starts
509
+ path.style.strokeDasharray = `${length}, ${length}`;
510
+ path.style.strokeDashoffset = length;
511
+
512
+ function animate(timestamp) {
513
+ if (!start) start = timestamp;
514
+ const progress = (timestamp - start) / duration;
515
+
516
+ if (progress <= 1) {
517
+ const point = path.getPointAtLength(length * progress);
518
+ movingDot.style.left = `${point.x}px`;
519
+ movingDot.style.top = `${point.y}px`;
520
+
521
+ // Reveal only the traveled portion of the path
522
+ path.style.strokeDashoffset = length * (1 - progress);
523
+
524
+ requestAnimationFrame(animate);
525
+ } else {
526
+ movingDot.remove();
527
+ path.style.strokeOpacity = "0"; // Hide line when animation completes
528
+
529
+ outputBox.style.display = 'flex'; // Show output box
530
+ outputBox.classList.add('highlight');
531
+ setTimeout(() => outputBox.classList.remove('highlight'), 500);
532
+
533
+ if (onComplete) onComplete();
534
+ }
535
+ }
536
+
537
+ requestAnimationFrame(animate);
538
+ }
539
+
540
+ const inputs = inputFiles.map(createInputPaper);
541
+ const outputs = outputFiles.map(createOutputBox);
542
+
543
+ function pulseBox() {
544
+ centerBox.classList.add('pulse');
545
+ setTimeout(() => centerBox.classList.remove('pulse'), 300);
546
+ }
547
+
548
+ async function animateInputs() {
549
+ // Hide output boxes when input animation starts
550
+ outputs.forEach(output => {
551
+ output.outputBox.style.opacity = '0';
552
+ });
553
+
554
+ // Animate all inputs (papers) to the center sequentially
555
+ for (let i = 0; i < inputs.length; i++) {
556
+ await new Promise(resolve => {
557
+ animatePaperAlongPath(inputs[i].path, inputs[i].ext, () => {
558
+ pulseBox();
559
+ resolve();
560
+ });
561
+ });
562
+ // Small delay between input animations
563
+ await new Promise(resolve => setTimeout(resolve, 200));
564
+ }
565
+
566
+ pulseBox(); // Final pulse after all inputs are processed
567
+ return true;
568
+ }
569
+
570
+ async function animateOutputs() {
571
+ // Animate all outputs sequentially
572
+ for (let i = 0; i < outputs.length; i++) {
573
+ await new Promise(resolve => {
574
+ animateDotAlongPath(outputs[i].path, outputs[i].outputBox, () => {
575
+ outputs[i].outputBox.style.opacity = '1'; // Show output box when dot reaches destination
576
+ resolve();
577
+ });
578
+ });
579
+
580
+ // Wait a bit before starting the next output animation
581
+ await new Promise(resolve => setTimeout(resolve, 300));
582
+ }
583
+
584
+ // Keep outputs visible for a moment
585
+ await new Promise(resolve => setTimeout(resolve, 1500));
586
+
587
+ // Before next cycle, fade out output boxes
588
+ outputs.forEach(output => {
589
+ output.outputBox.style.opacity = '0';
590
+ });
591
+
592
+ // Small pause before starting next cycle
593
+ await new Promise(resolve => setTimeout(resolve, 500));
594
+ return true;
595
+ }
596
+
597
+ async function completeAnimationCycle() {
598
+ if (isAnimating) return;
599
+ isAnimating = true;
600
+
601
+ try {
602
+ await animateInputs();
603
+ await new Promise(resolve => setTimeout(resolve, 800));
604
+ await animateOutputs();
605
+ isAnimating = false;
606
+ setTimeout(completeAnimationCycle, 500);
607
+ } catch (error) {
608
+ console.error("Animation error:", error);
609
+ isAnimating = false;
610
+ }
611
+ }
612
+
613
+ setTimeout(completeAnimationCycle, 1000);
614
+
615
+ function showPaperAlert(message, title = "Alert", icon = "fa-exclamation-circle", duration = 7000) {
616
+ const paperAlert = document.getElementById('paperAlert');
617
+ const alertMessage = document.getElementById('alertMessage');
618
+ const alertTitle = document.getElementById('alertTitle');
619
+ const alertIcon = document.getElementById('alertIcon');
620
+
621
+ // Set custom message, title and icon
622
+ alertMessage.textContent = message;
623
+ alertTitle.textContent = title;
624
+
625
+ // Update the icon class
626
+ alertIcon.className = ''; // Clear existing classes
627
+ alertIcon.classList.add('fas', icon);
628
+
629
+ // Show the alert
630
+ paperAlert.classList.remove("hide");
631
+ paperAlert.classList.add("show", "showAlert");
632
+
633
+ // Auto-hide after specified duration
634
+ setTimeout(() => {
635
+ hideAlert();
636
+ }, duration);
637
+ }
638
+
639
+ // Function to hide the alert with animation
640
+ function hideAlert() {
641
+ const paperAlert = document.getElementById('paperAlert');
642
+ paperAlert.classList.remove("show");
643
+ paperAlert.classList.add("hide");
644
+ setTimeout(() => {
645
+ paperAlert.classList.remove("showAlert");
646
+ }, 500);
647
+ }
648
+
649
+ // Set up close button functionality
650
+ document.querySelector('#paperAlert .close-btn').addEventListener('click', function() {
651
+ hideAlert();
652
+ });
653
+
654
+ const featureCards = document.querySelectorAll('.feature-selection .glass');
655
+ const selectionMessage = document.getElementById('selectionMessage');
656
+ const selectedFeatureText = document.getElementById('selectedFeature');
657
+ const selectedFeature = {"feature":null}
658
+
659
+ let isLoggedIn = localStorage.getItem("username"); // Replace with your actual login check
660
+ let selectedCard = null;
661
+
662
+ featureCards.forEach(card => {
663
+ card.addEventListener('click', () => {
664
+ if (!isLoggedIn) {
665
+
666
+ showPaperAlert(
667
+ "You must be Logged In to continue!",
668
+ "Authentication Required",
669
+ "fa-exclamation-circle",
670
+ 7000
671
+ );
672
+ return;
673
+ }
674
+ const isAlreadySelected = card === selectedCard;
675
+
676
+ if (isAlreadySelected) {
677
+ card.classList.remove('selected');
678
+ selectedCard = null;
679
+ selectionMessage.classList.remove('visible');
680
+ } else {
681
+ if (selectedCard) {
682
+ selectedCard.classList.remove('selected');
683
+ }
684
+
685
+ card.classList.add('selected');
686
+ selectedCard = card;
687
+
688
+ selectedFeature.feature = card.getAttribute('data-feature');
689
+
690
+ selectedFeatureText.textContent = card.getAttribute('data-text');
691
+ selectionMessage.classList.add('visible');
692
+ }
693
+ });
694
+ });
695
+
696
+
697
+
698
+ const searchBtn = document.getElementById("searchbtn");
699
+ searchBtn.addEventListener("click", function () {
700
+ console.log("Search button clicked");
701
+
702
+
703
+
704
+ const isLoggedIn = localStorage.getItem("username");
705
+ if (!isLoggedIn) {
706
+ alert("You must be signed in to perform a search.");
707
+ return;
708
+ }
709
+
710
+ const searchInput = document.querySelector('#searchy');
711
+ console.log("searchInput" , searchInput)
712
+ if (!searchInput) {
713
+ console.error("Search input element not found");
714
+ alert("Search input not found. Please check the page structure.");
715
+ return;
716
+ }
717
+
718
+ const searchTopic = searchInput.value.trim();
719
+ const yearFilter = document.getElementById('year-filter');
720
+ const searchYear = yearFilter ? yearFilter.value : "";
721
+ const userId = localStorage.getItem("userId");
722
+
723
+ saveSearchToFirebase(searchTopic, searchYear, selectedFeature )
724
+ // alert(" saved the activity")
725
+
726
+ // Validate inputs
727
+ if (!searchTopic) {
728
+ alert("Please enter a search topic");
729
+ return;
730
+ }
731
+
732
+ console.log(`Searching for: ${searchTopic}, Year: ${searchYear}, Feature: ${selectedFeature}`);
733
+
734
+
735
+ let currentPage = 0;
736
+ const nextBtn = document.getElementById("nextBtn");
737
+
738
+ nextBtn.addEventListener("click", function () {
739
+ currentPage+=1
740
+ alert("just checking bro " +currentPage );
741
+ trendanalysis(currentPage);
742
+ });
743
+
744
+
745
+
746
+ function trendanalysis(pageNo){
747
+ const originalText = searchBtn.textContent;
748
+ searchBtn.textContent = "Processing...";
749
+ searchBtn.disabled = true;
750
+ nextBtn.style.display = "inline-block";
751
+
752
+
753
+ fetch('/check-data-exists/', {
754
+ method: 'POST',
755
+ headers: {
756
+ 'Content-Type': 'application/json',
757
+ },
758
+ body: JSON.stringify({
759
+ userId: userId,
760
+ topic: searchTopic,
761
+ year: searchYear
762
+ })
763
+ })
764
+ .then(response => {
765
+ if (!response.ok) {
766
+ throw new Error(`Network response was not ok: ${response.status}`);
767
+ }
768
+ return response.json();
769
+ })
770
+ .then(data => {
771
+ if (data.exists) {
772
+ console.log('Data exists, skipping analysis step');
773
+ showPaperAlert(
774
+ "Data exists",
775
+ "Trend Analysis.......",
776
+ "fa-exclamation-circle",
777
+ 7000
778
+ );
779
+ return fetch('/analyze-trends/', {
780
+ method: 'POST',
781
+ headers: {
782
+ 'Content-Type': 'application/json',
783
+ },
784
+ body: JSON.stringify({
785
+ userId: userId,
786
+ topic: searchTopic,
787
+ year: searchYear,
788
+ page: pageNo
789
+ })
790
+ });
791
+ } else {
792
+ console.log('Data does not exist, starting full analysis');
793
+ showPaperAlert(
794
+ "Starting full analysis",
795
+ "ATrend Analysis.......",
796
+ "fa-exclamation-circle",
797
+ 7000
798
+ );
799
+ return fetch('/analyze/', {
800
+ method: 'POST',
801
+ headers: {
802
+ 'Content-Type': 'application/json',
803
+ },
804
+ body: JSON.stringify({
805
+ userId: userId,
806
+ topic: searchTopic,
807
+ year: searchYear
808
+ })
809
+ })
810
+ .then(response => {
811
+ if (!response.ok) {
812
+ throw new Error(`Analysis failed: ${response.status}`);
813
+ }
814
+ return response.json();
815
+ })
816
+ .then(analysisData => {
817
+ console.log('Analysis complete:', analysisData);
818
+ // Now proceed to analyze-trends
819
+ showPaperAlert(
820
+ "Analysis complete",
821
+ "Trend Analysis.......",
822
+ "fa-exclamation-circle",
823
+ 7000
824
+ );
825
+ return fetch('/analyze-trends/', {
826
+ method: 'POST',
827
+ headers: {
828
+ 'Content-Type': 'application/json',
829
+ },
830
+ body: JSON.stringify({
831
+ userId: userId,
832
+ topic: searchTopic,
833
+ year: searchYear,
834
+ page: pageNo
835
+ })
836
+ });
837
+ });
838
+ }
839
+ })
840
+ .then(response => {
841
+ if (!response.ok) {
842
+ throw new Error(`Trend analysis failed: ${response.status}`);
843
+ }
844
+ return response.json();
845
+ })
846
+ .then(trendData => {
847
+ console.log('Trend analysis complete:', trendData);
848
+ showPaperAlert(
849
+ "Analysis complete",
850
+ "Trend Analysis.......",
851
+ "fa-exclamation-circle",
852
+ 7000
853
+ );
854
+ loadingOverlay.hide()
855
+ // Handle redirection to results page or display results
856
+ })
857
+ .catch(error => {
858
+ console.error('Error:', error);
859
+ alert("There was an error processing your request. Please try again: " + error.message);
860
+ showPaperAlert(
861
+ `There was an error processing your request. Please try again:` + error.message,
862
+ "Trend Analysis.......",
863
+ "fa-exclamation-circle",
864
+ 7000
865
+ );
866
+ loadingOverlay.hide()
867
+ })
868
+ .finally(() => {
869
+ // Always reset button state
870
+ searchBtn.textContent = originalText;
871
+ searchBtn.disabled = false;
872
+ });
873
+ }
874
+
875
+
876
+
877
+ if (selectedFeature.feature === 'trends') {
878
+ console.log("here here")
879
+ loadingOverlay.show("Loading Trend Analysis")
880
+ trendanalysis(0);
881
+ }
882
+ else if (selectedFeature.feature === 'citations') {
883
+ loadingOverlay.show("Loading Citation Network Analysis")
884
+ nextBtn.style.display = "none";
885
+ currentPage = 0;
886
+ showPaperAlert(
887
+ "Analysis Started",
888
+ "Citation Network Analysis.......",
889
+ "fa-exclamation-circle",
890
+ 7000
891
+ );
892
+
893
+ const originalText = this.textContent;
894
+ this.textContent = "Processing...";
895
+ this.disabled = true;
896
+
897
+ // First check if data exists in MongoDB
898
+ fetch('/check-data-exists-citation/', {
899
+ method: 'POST',
900
+ headers: {
901
+ 'Content-Type': 'application/json',
902
+ },
903
+ body: JSON.stringify({
904
+ userId: userId,
905
+ topic: searchTopic,
906
+ year: searchYear
907
+ })
908
+ })
909
+ .then(response => {
910
+ if (!response.ok) {
911
+ throw new Error(`Network response was not ok: ${response.status}`);
912
+ }
913
+ return response.json();
914
+ })
915
+ .then(data => {
916
+ if (data.exists) {
917
+ console.log('Data exists, fetching citation data...');
918
+ loadingOverlay.hide()
919
+ window.open(`/citation-data?userId=${encodeURIComponent(userId)}&topic=${encodeURIComponent(searchTopic)}&year=${encodeURIComponent(searchYear)}`, '_blank');
920
+ } else {
921
+ console.log('Data does not exist, starting full analysis...');
922
+ showPaperAlert(
923
+ "Data Collecting....",
924
+ "Citation Network Analysis.......",
925
+ "fa-exclamation-circle",
926
+ 7000
927
+ );
928
+ return fetch('/save', {
929
+ method: 'POST',
930
+ headers: {
931
+ 'Content-Type': 'application/json',
932
+ },
933
+ body: JSON.stringify({
934
+ userId: userId,
935
+ topic: searchTopic,
936
+ year: searchYear
937
+ })
938
+ })
939
+ .then(response => {
940
+ if (!response.ok) {
941
+ throw new Error(`Save failed: ${response.status}`);
942
+ }
943
+ return response.json();
944
+ })
945
+ .then(saveData => {
946
+ console.log('Data saved successfully:', saveData);
947
+ loadingOverlay.hide()
948
+ window.open(`/citation-data?userId=${encodeURIComponent(userId)}&topic=${encodeURIComponent(searchTopic)}&year=${encodeURIComponent(searchYear)}`, '_blank');
949
+ });
950
+ }
951
+ })
952
+ .catch(error => {
953
+ console.error('Error:', error);
954
+ showPaperAlert(
955
+ "There was an error processing your request. Please try again: " + error.message,
956
+ "Citation Network Analysis.......",
957
+ "fa-exclamation-circle",
958
+ 7000
959
+ );
960
+ loadingOverlay.hide()
961
+ })
962
+ .finally(() => {
963
+ // Always reset button state
964
+ this.textContent = originalText;
965
+ this.disabled = false;
966
+ });
967
+ }
968
+
969
+ else if (selectedFeature.feature === 'venues') {
970
+ nextBtn.style.display = "none";
971
+ currentPage = 0;
972
+ const originalText = this.textContent;
973
+ this.textContent = "Processing...";
974
+ this.disabled = true;
975
+ console.log(" here here venue")
976
+ showPaperAlert(
977
+ "Analysis Started",
978
+ "Venue/Publisher Analysis.......",
979
+ "fa-exclamation-circle",
980
+ 7000
981
+ );
982
+ loadingOverlay.show("Started Loading.....")
983
+ fetch('/check-data-exists-venue/', {
984
+ method: 'POST',
985
+ headers: {
986
+ 'Content-Type': 'application/json',
987
+ },
988
+ body: JSON.stringify({
989
+ userId: userId,
990
+ topic: searchTopic,
991
+ year: searchYear
992
+ })
993
+ })
994
+ .then(response => {
995
+ if (!response.ok) {
996
+
997
+ throw new Error(`Network response was not ok: ${response.status}`);
998
+
999
+ }
1000
+
1001
+ return response.json();
1002
+ })
1003
+ .then(data => {
1004
+ if (data.exists) {
1005
+ console.log('Data exists, loading dashboard directly');
1006
+ // If data exists, go directly to load_and_display_dashboard
1007
+
1008
+ return fetch('/load_and_display_dashboard/', {
1009
+ method: 'POST',
1010
+ headers: {
1011
+ 'Content-Type': 'application/json',
1012
+ },
1013
+ body: JSON.stringify({
1014
+ userId: userId,
1015
+ topic: searchTopic,
1016
+ year: searchYear
1017
+ })
1018
+ });
1019
+ } else {
1020
+ console.log('Data does not exist, starting search process');
1021
+ showPaperAlert(
1022
+ "Data Collectiong....",
1023
+ "Venue/Publisher Analysis.......",
1024
+ "fa-exclamation-circle",
1025
+ 7000
1026
+ );
1027
+ return fetch('/search/', {
1028
+ method: 'POST',
1029
+ headers: {
1030
+ 'Content-Type': 'application/json',
1031
+ },
1032
+ body: JSON.stringify({
1033
+ userId: userId,
1034
+ topic: searchTopic,
1035
+ year: searchYear
1036
+ })
1037
+ })
1038
+ .then(response => {
1039
+ if (!response.ok) {
1040
+ throw new Error(`Search failed: ${response.status}`);
1041
+ }
1042
+ return response.json();
1043
+ })
1044
+ .then(searchData => {
1045
+ console.log('Search complete:', searchData);
1046
+
1047
+ return fetch('/load_and_display_dashboard/', {
1048
+ method: 'POST',
1049
+ headers: {
1050
+ 'Content-Type': 'application/json',
1051
+ },
1052
+ body: JSON.stringify({
1053
+ userId: userId,
1054
+ topic: searchTopic,
1055
+ year: searchYear
1056
+ })
1057
+ });
1058
+ });
1059
+ }
1060
+ })
1061
+ .then(response => {
1062
+ if (!response.ok) {
1063
+ throw new Error(`Dashboard loading failed: ${response.status}`);
1064
+ }
1065
+ return response.json();
1066
+ })
1067
+ .then(dashboardData => {
1068
+
1069
+ console.log('Dashboard loaded successfully:', dashboardData);
1070
+ loadingOverlay.hide();
1071
+ //
1072
+ })
1073
+ .catch(error => {
1074
+ console.error('Error:', error);
1075
+ showPaperAlert(
1076
+ "There was an error processing your request. Please try again: " + error.message,
1077
+ "Venue/Publisher Analysis.......",
1078
+ "fa-exclamation-circle",
1079
+ 7000
1080
+ );
1081
+ })
1082
+ .finally(() => {
1083
+ this.textContent = originalText;
1084
+ this.disabled = false;
1085
+ });
1086
+
1087
+ } else {
1088
+ alert("Invalid feature selected. Please choose a valid feature.");
1089
+ }
1090
+ });
1091
+
1092
+
1093
+ const loadingOverlay = {
1094
+ overlay: document.getElementById('loading-overlay'),
1095
+ cyclone: null,
1096
+ loadingText: null,
1097
+ animationInterval: null,
1098
+ paperInterval: null,
1099
+
1100
+ initialize() {
1101
+ this.cyclone = document.getElementById('cyclone');
1102
+ this.loadingText = document.getElementById('loading-text');
1103
+ this.setupAnimation();
1104
+
1105
+ // Make the background whiter by changing the overlay background
1106
+ this.overlay.style.backgroundColor = 'rgba(137, 138, 183, 0.9)'; // More white, slightly transparent
1107
+ },
1108
+
1109
+ setupAnimation() {
1110
+ // Pre-setup animation elements so they're ready when needed
1111
+ for (let i = 0; i < 10; i++) {
1112
+ this.createPaper(i);
1113
+ }
1114
+ },
1115
+
1116
+ show(message = 'Processing...') {
1117
+ this.loadingText.textContent = message;
1118
+ this.overlay.classList.add('active');
1119
+
1120
+ // Start creating papers
1121
+ this.paperInterval = setInterval(() => {
1122
+ this.createPaper();
1123
+ }, 120);
1124
+
1125
+ // Animate loading text
1126
+ let dots = 0;
1127
+ this.animationInterval = setInterval(() => {
1128
+ dots = (dots + 1) % 4;
1129
+ this.loadingText.textContent = message + '.'.repeat(dots);
1130
+ }, 500);
1131
+ },
1132
+
1133
+ hide() {
1134
+ this.overlay.classList.remove('active');
1135
+
1136
+ // Clear intervals
1137
+ clearInterval(this.paperInterval);
1138
+ clearInterval(this.animationInterval);
1139
+
1140
+ // Clear all papers after animation ends
1141
+ setTimeout(() => {
1142
+ while (this.cyclone.firstChild) {
1143
+ this.cyclone.removeChild(this.cyclone.firstChild);
1144
+ }
1145
+ }, 300);
1146
+ },
1147
+
1148
+ createPaper(index) {
1149
+ const paper = document.createElement('div');
1150
+ paper.className = 'paper';
1151
+
1152
+ // Randomize paper appearance for more realistic effect
1153
+ const size = 8 + Math.random() * 12;
1154
+ paper.style.width = `${size}px`;
1155
+ paper.style.height = `${size * 1.3}px`;
1156
+
1157
+ // Change paper color to grayish (varying shades of gray)
1158
+ const grayness = 65 + Math.random() * 20; // Range from 65-85% gray (darker)
1159
+ paper.style.backgroundColor = `hsl(0, 0%, ${grayness}%)`;
1160
+
1161
+ // Give it a slight skew to look more like paper
1162
+ const skewX = Math.random() * 10 - 5;
1163
+ const skewY = Math.random() * 10 - 5;
1164
+ paper.style.transform = `skew(${skewX}deg, ${skewY}deg)`;
1165
+
1166
+ // Start position - at the top of the cyclone
1167
+ const startAngle = Math.random() * Math.PI * 2;
1168
+ const startRadius = 60 + Math.random() * 20;
1169
+ const startX = 100 + startRadius * Math.cos(startAngle);
1170
+ const startY = -20; // Start above the visible area
1171
+
1172
+ paper.style.left = `${startX}px`;
1173
+ paper.style.top = `${startY}px`;
1174
+
1175
+ this.cyclone.appendChild(paper);
1176
+
1177
+ // Animate the paper with a specific starting point in the spiral
1178
+ this.animatePaperInCyclone(paper, startAngle);
1179
+ },
1180
+
1181
+ animatePaperInCyclone(paper, initialAngle) {
1182
+ // Animation duration
1183
+ const duration = 6000 + Math.random() * 2000;
1184
+ const startTime = Date.now();
1185
+
1186
+ // Initial state
1187
+ let angle = initialAngle || 0;
1188
+ let height = -20; // Start above the visible area
1189
+ const maxHeight = 350; // End height
1190
+ const rotationSpeed = 1.0 + Math.random() * 0.5; // More consistent rotation speed
1191
+
1192
+ const animateFrame = () => {
1193
+ if (!this.overlay.classList.contains('active')) {
1194
+ if (paper.parentNode === this.cyclone) {
1195
+ this.cyclone.removeChild(paper);
1196
+ }
1197
+ return;
1198
+ }
1199
+
1200
+ const now = Date.now();
1201
+ const elapsed = now - startTime;
1202
+ const progress = elapsed / duration;
1203
+
1204
+ if (progress < 1) {
1205
+ // Calculate new height with a clear downward flow
1206
+ // Move from top to bottom with easing
1207
+ height = -20 + (maxHeight + 20) * Math.min(1, progress * 1.2);
1208
+
1209
+ // Calculate spiral radius - wider at top, narrower at bottom
1210
+ // Creates a funnel/cyclone shape
1211
+ const heightProgress = height / maxHeight;
1212
+ const currentRadius = Math.max(10, 80 - 60 * heightProgress);
1213
+
1214
+ // Rotate faster as it gets closer to the bottom
1215
+ const adjustedRotation = rotationSpeed * (1 + heightProgress * 0.5);
1216
+ angle += adjustedRotation * 0.03;
1217
+
1218
+ // Calculate position
1219
+ const x = 100 + currentRadius * Math.cos(angle);
1220
+ const y = height;
1221
+
1222
+ // Apply position
1223
+ paper.style.left = `${x}px`;
1224
+ paper.style.top = `${y}px`;
1225
+
1226
+ // Apply rotation and scaling
1227
+ const rotation = angle * (180 / Math.PI);
1228
+ // Increase fluttering effect as paper falls
1229
+ const flutter = Math.sin(elapsed * 0.01) * (5 + heightProgress * 10);
1230
+ paper.style.transform = `rotate(${rotation + flutter}deg) scale(${0.8 + Math.sin(progress * Math.PI * 4) * 0.1})`;
1231
+
1232
+ // Adjust opacity near the end for a fading effect
1233
+ if (progress > 0.8) {
1234
+ paper.style.opacity = (1 - progress) * 5;
1235
+ }
1236
+
1237
+ requestAnimationFrame(animateFrame);
1238
+ } else {
1239
+ // Remove paper when animation completes
1240
+ if (paper.parentNode === this.cyclone) {
1241
+ this.cyclone.removeChild(paper);
1242
+ }
1243
+ }
1244
+ };
1245
+
1246
+ requestAnimationFrame(animateFrame);
1247
+ }
1248
+ };
1249
+ // Initialize the loading overlay when the DOM is loaded
1250
+ document.addEventListener('DOMContentLoaded', () => {
1251
+ loadingOverlay.initialize();
1252
+ });
1253
+ const words = document.querySelectorAll('.liquid-word');
1254
+ const splashContainer = document.getElementById('splashContainer');
1255
+ let currentIndex = 0;
1256
+
1257
+ // Prepare each word with character spans and paper drops
1258
+ words.forEach(word => {
1259
+ const letters = word.textContent.split('');
1260
+ word.innerHTML = '';
1261
+
1262
+ letters.forEach(letter => {
1263
+ const charSpan = document.createElement('span');
1264
+ charSpan.className = 'liquid-char';
1265
+ charSpan.textContent = letter;
1266
+ word.appendChild(charSpan);
1267
+
1268
+ // Add paper drop to each character
1269
+ const paperDrop = document.createElement('div');
1270
+ paperDrop.className = 'paper-drop';
1271
+ charSpan.appendChild(paperDrop);
1272
+
1273
+ // Add paper background to each character
1274
+ for (let i = 0; i < 3; i++) {
1275
+ const paperBg = document.createElement('div');
1276
+ paperBg.className = 'paper-piece';
1277
+ paperBg.style.width = `${Math.random() * 20 + 20}px`;
1278
+ paperBg.style.height = `${Math.random() * 15 + 15}px`;
1279
+ paperBg.style.transform = `rotate(${Math.random() * 40 - 20}deg)`;
1280
+
1281
+ // Random position around the character
1282
+ const offsetX = (Math.random() - 0.5) * 30;
1283
+ const offsetY = (Math.random() - 0.5) * 30;
1284
+ paperBg.style.left = `calc(50% + ${offsetX}px)`;
1285
+ paperBg.style.top = `calc(50% + ${offsetY}px)`;
1286
+
1287
+ // Random colors (paper-like)
1288
+ const colors = ['#ffffff', '#f8f8f8', '#f0f0f0', '#fffafa', '#f5f5f5'];
1289
+ paperBg.style.backgroundColor = colors[Math.floor(Math.random() * colors.length)];
1290
+
1291
+ charSpan.appendChild(paperBg);
1292
+ }
1293
+ });
1294
+ });
1295
+
1296
+ // Function to create paper splashes
1297
+ function createPaperSplashes(centerX, centerY) {
1298
+ for (let i = 0; i < 15; i++) {
1299
+ setTimeout(() => {
1300
+ const paper = document.createElement('div');
1301
+ paper.className = 'paper-piece';
1302
+
1303
+ // Random size and shape
1304
+ const size = Math.random() * 20 + 10;
1305
+ paper.style.width = `${size}px`;
1306
+ paper.style.height = `${size * (Math.random() * 0.5 + 0.5)}px`;
1307
+
1308
+ // Random rotation
1309
+ const rotation = Math.random() * 360;
1310
+ paper.style.setProperty('--rotation', `${rotation}deg`);
1311
+
1312
+ // Random position around center
1313
+ const angle = Math.random() * Math.PI * 2;
1314
+ const distance = Math.random() * 100 + 20;
1315
+ const x = centerX + Math.cos(angle) * distance;
1316
+ const y = centerY + Math.sin(angle) * distance;
1317
+
1318
+ paper.style.left = `${x}px`;
1319
+ paper.style.top = `${y}px`;
1320
+
1321
+ // Random paper colors
1322
+ const colors = ['#ffffff', '#f8f8f8', '#f0f0f0', '#fffafa', '#f5f5f5'];
1323
+ paper.style.backgroundColor = colors[Math.floor(Math.random() * colors.length)];
1324
+
1325
+ // Add a slight border on some pieces
1326
+ if (Math.random() > 0.7) {
1327
+ paper.style.border = '1px solid rgba(0,0,0,0.05)';
1328
+ }
1329
+
1330
+ // Animation
1331
+ paper.style.animation = `paperSplash ${Math.random() * 0.8 + 0.6}s forwards`;
1332
+
1333
+ splashContainer.appendChild(paper);
1334
+
1335
+ // Remove after animation
1336
+ setTimeout(() => paper.remove(), 1500);
1337
+ }, i * 40);
1338
+ }
1339
+ }
1340
+
1341
+ // Function to animate letters in
1342
+ function animateLettersIn(word) {
1343
+ const chars = word.querySelectorAll('.liquid-char');
1344
+
1345
+ chars.forEach((char, i) => {
1346
+ setTimeout(() => {
1347
+ // Animate the character
1348
+ char.style.animation = 'paperIn 0.7s forwards';
1349
+ char.style.opacity = '1';
1350
+
1351
+ // Animate paper drop
1352
+ const paperDrop = char.querySelector('.paper-drop');
1353
+ setTimeout(() => {
1354
+ paperDrop.style.animation = 'paperDrop 0.5s forwards';
1355
+ }, 350);
1356
+
1357
+ // Animate paper pieces
1358
+ const paperPieces = char.querySelectorAll('.paper-piece');
1359
+ paperPieces.forEach((piece, j) => {
1360
+ setTimeout(() => {
1361
+ piece.style.opacity = '1';
1362
+ piece.style.transform += ' scale(1)';
1363
+ }, j * 100 + 300);
1364
+ });
1365
+
1366
+ }, i * 100);
1367
+ });
1368
+ }
1369
+
1370
+ // Function to animate letters out
1371
+ function animateLettersOut(word) {
1372
+ const chars = word.querySelectorAll('.liquid-char');
1373
+
1374
+ chars.forEach((char, i) => {
1375
+ setTimeout(() => {
1376
+ char.style.animation = 'paperOut 0.5s forwards';
1377
+
1378
+ // Hide paper pieces
1379
+ const paperPieces = char.querySelectorAll('.paper-piece');
1380
+ paperPieces.forEach(piece => {
1381
+ piece.style.opacity = '0';
1382
+ });
1383
+ }, i * 60);
1384
+ });
1385
+ }
1386
+
1387
+ // Function for word transition
1388
+ function transitionWords() {
1389
+ // Get current word and calculate center position
1390
+ const currentWord = words[currentIndex];
1391
+ const rect = currentWord.getBoundingClientRect();
1392
+ const centerX = rect.width / 2;
1393
+ const centerY = rect.height / 2;
1394
+
1395
+ // Animate current word out
1396
+ animateLettersOut(currentWord);
1397
+
1398
+ setTimeout(() => {
1399
+ // Hide current word
1400
+ currentWord.classList.remove('active');
1401
+
1402
+ // Go to next word
1403
+ currentIndex = (currentIndex + 1) % words.length;
1404
+ const nextWord = words[currentIndex];
1405
+
1406
+ // Show next word
1407
+ nextWord.classList.add('active');
1408
+
1409
+ // Create paper splash effect
1410
+ createPaperSplashes(centerX, centerY);
1411
+
1412
+ // Animate next word in
1413
+ setTimeout(() => {
1414
+ animateLettersIn(nextWord);
1415
+ }, 200);
1416
+
1417
+ }, 500);
1418
+ }
1419
+
1420
+ // Initial animation
1421
+ setTimeout(() => {
1422
+ animateLettersIn(words[0]);
1423
+ }, 500);
1424
+
1425
+ // Start word transition cycle
1426
+ setInterval(transitionWords, 3500);
1427
+
1428
+
1429
+ const carousel = document.getElementById('carousel');
1430
+ const pages = document.querySelectorAll('.page');
1431
+ const nextBtn2 = document.getElementById('nextBtn2');
1432
+ const prevBtn = document.getElementById('prevBtn');
1433
+ const dots = document.querySelectorAll('.dot');
1434
+ const pageShadows = document.querySelectorAll('.page-shadow');
1435
+
1436
+ let currentPage = 0;
1437
+ let totalSlides = 6; // Confirm we have 6 total slides
1438
+
1439
+ // Initialize pages
1440
+ pages.forEach((page, index) => {
1441
+ page.style.transform = 'rotateY(0deg)';
1442
+ page.style.zIndex = 6 - index;
1443
+ });
1444
+
1445
+ // Function to turn to next page and rotate carousel
1446
+ function nextPage() {
1447
+ if (currentPage < pages.length) {
1448
+ // Apply shadow during turn
1449
+ if (currentPage > 0) {
1450
+ pageShadows[currentPage-1].style.opacity = '0';
1451
+ }
1452
+
1453
+ pages[currentPage].style.transform = 'rotateY(-180deg)';
1454
+ pages[currentPage].style.zIndex = 10 + currentPage;
1455
+
1456
+ if (currentPage < pages.length - 1) {
1457
+ pageShadows[currentPage].style.opacity = '1';
1458
+ }
1459
+
1460
+ currentPage++;
1461
+
1462
+ // Rotate carousel - Fixed rotation calculation for 6 slides (360 degrees)
1463
+ const newRotationDegree = (currentPage % totalSlides) * (360 / totalSlides);
1464
+ carousel.style.transform = `rotateY(${-newRotationDegree}deg)`;
1465
+
1466
+ // Update pagination dots
1467
+ updatePagination(currentPage % totalSlides);
1468
+ }
1469
+ }
1470
+
1471
+ // Function to turn to previous page and rotate carousel
1472
+ function prevPage() {
1473
+ if (currentPage > 0) {
1474
+ currentPage--;
1475
+
1476
+ // Apply shadow during turn
1477
+ if (currentPage > 0) {
1478
+ pageShadows[currentPage-1].style.opacity = '1';
1479
+ }
1480
+
1481
+ pages[currentPage].style.transform = 'rotateY(0deg)';
1482
+ setTimeout(() => {
1483
+ pages[currentPage].style.zIndex = 10 - currentPage;
1484
+ }, 300);
1485
+
1486
+ pageShadows[currentPage].style.opacity = '0';
1487
+
1488
+ // Rotate carousel - Fixed rotation calculation
1489
+ const newRotationDegree = (currentPage % totalSlides) * (360 / totalSlides);
1490
+ carousel.style.transform = `rotateY(${-newRotationDegree}deg)`;
1491
+
1492
+ // Update pagination dots
1493
+ updatePagination(currentPage % totalSlides);
1494
+ }
1495
+ }
1496
+
1497
+ // Function to reset book to beginning
1498
+ function resetBook() {
1499
+ pages.forEach((page, index) => {
1500
+ page.style.transition = 'none';
1501
+ page.style.transform = 'rotateY(0deg)';
1502
+ setTimeout(() => {
1503
+ page.style.zIndex = 5 - index;
1504
+ page.style.transition = 'transform 0.6s ease, z-index 0s 0.3s';
1505
+ }, 50);
1506
+ });
1507
+
1508
+ pageShadows.forEach(shadow => {
1509
+ shadow.style.opacity = '0';
1510
+ });
1511
+
1512
+ currentPage = 0;
1513
+
1514
+ // Reset carousel and pagination
1515
+ carousel.style.transform = 'rotateY(0deg)';
1516
+ updatePagination(0);
1517
+ }
1518
+
1519
+ // Function to update pagination dots
1520
+ function updatePagination(index) {
1521
+ dots.forEach((dot, i) => {
1522
+ if (i === index) {
1523
+ dot.classList.add('active');
1524
+ } else {
1525
+ dot.classList.remove('active');
1526
+ }
1527
+ });
1528
+ }
1529
+
1530
+ // Event listeners
1531
+ nextBtn2.addEventListener('click', nextPage);
1532
+ prevBtn.addEventListener('click', prevPage);
1533
+
1534
+ // Make pagination dots clickable
1535
+ dots.forEach((dot, index) => {
1536
+ dot.addEventListener('click', () => {
1537
+ // Find the current actual position in the book
1538
+ const currentInCycle = currentPage % totalSlides;
1539
+
1540
+ // Calculate how many pages to move
1541
+ let pagesToMove;
1542
+
1543
+ if (index > currentInCycle) {
1544
+ pagesToMove = index - currentInCycle;
1545
+ } else if (index < currentInCycle) {
1546
+ pagesToMove = totalSlides - currentInCycle + index;
1547
+ } else {
1548
+ return; // Already on this page
1549
+ }
1550
+
1551
+ // Move the pages
1552
+ for (let i = 0; i < pagesToMove; i++) {
1553
+ setTimeout(() => nextPage(), i * 400);
1554
+ }
1555
+ });
1556
+ });
1557
+
1558
+ // Allow clicking on pages to turn them
1559
+ pages.forEach((page, index) => {
1560
+ page.addEventListener('click', () => {
1561
+ if (index === currentPage - 1) {
1562
+ prevPage();
1563
+ } else if (index === currentPage) {
1564
+ nextPage();
1565
+ }
1566
+ });
1567
+ });
1568
+
1569
+ const canvas = document.getElementById('bgParticles');
1570
+ if (canvas) {
1571
+ const ctx = canvas.getContext('2d');
1572
+ let particles = [];
1573
+ const colors = ['#ff6ec4', '#7873f5', '#1fd1f9', '#43e97b'];
1574
+ let width = window.innerWidth;
1575
+ let height = window.innerHeight;
1576
+ canvas.width = width;
1577
+ canvas.height = height;
1578
+
1579
+ function randomBetween(a, b) { return a + Math.random() * (b - a); }
1580
+
1581
+ function createParticles(num) {
1582
+ particles = [];
1583
+ for (let i = 0; i < num; i++) {
1584
+ particles.push({
1585
+ x: randomBetween(0, width),
1586
+ y: randomBetween(0, height),
1587
+ r: randomBetween(2, 5),
1588
+ color: colors[Math.floor(Math.random() * colors.length)],
1589
+ dx: randomBetween(-0.7, 0.7),
1590
+ dy: randomBetween(-0.7, 0.7),
1591
+ alpha: randomBetween(0.3, 0.8)
1592
+ });
1593
+ }
1594
+ }
1595
+
1596
+ function drawParticles() {
1597
+ ctx.clearRect(0, 0, width, height);
1598
+ for (let p of particles) {
1599
+ ctx.save();
1600
+ ctx.globalAlpha = p.alpha;
1601
+ ctx.beginPath();
1602
+ ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
1603
+ ctx.fillStyle = p.color;
1604
+ ctx.shadowColor = p.color;
1605
+ ctx.shadowBlur = 16;
1606
+ ctx.fill();
1607
+ ctx.restore();
1608
+
1609
+ p.x += p.dx;
1610
+ p.y += p.dy;
1611
+ if (p.x < 0 || p.x > width) p.dx *= -1;
1612
+ if (p.y < 0 || p.y > height) p.dy *= -1;
1613
+ }
1614
+ requestAnimationFrame(drawParticles);
1615
+ }
1616
+
1617
+ createParticles(48);
1618
+ drawParticles();
1619
+
1620
+ window.addEventListener('resize', () => {
1621
+ width = window.innerWidth;
1622
+ height = window.innerHeight;
1623
+ canvas.width = width;
1624
+ canvas.height = height;
1625
+ createParticles(48);
1626
+ });
1627
+ }
static/js/login.js ADDED
@@ -0,0 +1,216 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Firebase configuration
2
+ const firebaseConfig = {
3
+ apiKey: "AIzaSyA__OFybLoWSA6x_8lIyo0Z1Cb2ozhbHoA",
4
+ authDomain: "capstone-project-5f445.firebaseapp.com",
5
+ projectId: "capstone-project-5f445",
6
+ storageBucket: "capstone-project-5f445.firebasestorage.app",
7
+ messagingSenderId: "701976065811",
8
+ appId: "1:701976065811:web:494fa4ea19f04f1dd00ce6",
9
+ measurementId: "G-EVZTJ9X538"
10
+ };
11
+
12
+ // Initialize Firebase
13
+ const app = firebase.initializeApp(firebaseConfig);
14
+ const auth = firebase.auth();
15
+ const db = firebase.firestore();
16
+
17
+ // Form Elements
18
+ const signUpForm = document.querySelector('.sign-up');
19
+ const signInForm = document.querySelector('.sign-in');
20
+ const forgotForm = document.querySelector('.forgot-password');
21
+
22
+ // Sign Up Handler
23
+ signUpForm.addEventListener('submit', async (event) => {
24
+ event.preventDefault();
25
+
26
+ const name = document.querySelector('.sign-up input[type="text"]').value;
27
+ const email = document.querySelector('.sign-up input[type="email"]').value;
28
+ const password = document.querySelectorAll('.sign-up input[type="password"]')[0].value;
29
+ const confirmPassword = document.querySelectorAll('.sign-up input[type="password"]')[1].value;
30
+
31
+ if (password !== confirmPassword) {
32
+ alert("Passwords don't match!");
33
+ return;
34
+ }
35
+
36
+ try {
37
+ const userCredential = await auth.createUserWithEmailAndPassword(email, password);
38
+ const user = userCredential.user;
39
+
40
+ // Save to Firestore
41
+ await db.collection('users').doc(user.uid).set({
42
+ name: name,
43
+ email: email,
44
+ createdAt: firebase.firestore.FieldValue.serverTimestamp()
45
+ });
46
+
47
+ // Show success message and switch forms
48
+ alert('Account created successfully! Redirecting to login...');
49
+
50
+ // Trigger sign-in form animation
51
+ wrapper.classList.add('animated-signup');
52
+ wrapper.classList.remove('animated-signin', 'animated-forgot');
53
+
54
+ } catch (error) {
55
+ alert(error.message);
56
+ console.error("Signup error:", error);
57
+ }
58
+ });
59
+
60
+ // Animation utility functions
61
+ function scatterPapers() {
62
+ // Create paper particles
63
+ for (let i = 0; i < 10; i++) {
64
+ const page = document.createElement('div');
65
+ page.classList.add('page');
66
+ page.style.left = '50%';
67
+ page.style.top = '50%';
68
+ page.style.transform = 'translate(-50%, -50%)';
69
+ page.style.setProperty('--x', `${Math.random() * 800 - 400}px`);
70
+ page.style.setProperty('--y', `${Math.random() * -600}px`);
71
+ page.style.setProperty('--rotate', `${Math.random() * 720 - 360}deg`);
72
+ document.body.appendChild(page);
73
+
74
+ setTimeout(() => page.remove(), 3000);
75
+ }
76
+ }
77
+
78
+ function tearPaper() {
79
+ const leftPiece = document.createElement('div');
80
+ leftPiece.classList.add('tear-left');
81
+ leftPiece.style.left = 'calc(50% - 75px)';
82
+ leftPiece.style.top = '50%';
83
+ leftPiece.style.transform = 'translateY(-50%)';
84
+ document.body.appendChild(leftPiece);
85
+
86
+ const rightPiece = document.createElement('div');
87
+ rightPiece.classList.add('tear-right');
88
+ rightPiece.style.left = '50%';
89
+ rightPiece.style.top = '50%';
90
+ rightPiece.style.transform = 'translateY(-50%)';
91
+ document.body.appendChild(rightPiece);
92
+
93
+ setTimeout(() => {
94
+ leftPiece.remove();
95
+ rightPiece.remove();
96
+ }, 7000);
97
+ }
98
+
99
+ signInForm.addEventListener('submit', async (event) => {
100
+ event.preventDefault();
101
+
102
+ const email = document.querySelector('.sign-in input[type="email"]').value;
103
+ const password = document.querySelector('.sign-in input[type="password"]').value;
104
+
105
+ try {
106
+ const userCredential = await auth.signInWithEmailAndPassword(email, password);
107
+ const user = userCredential.user;
108
+
109
+ // Fetch username from Firestore
110
+ const userDoc = await db.collection('users').doc(user.uid).get();
111
+ let username = "User";
112
+ if (userDoc.exists) {
113
+ username = userDoc.data().name;
114
+ }
115
+
116
+ // Store username and userId in localStorage
117
+ localStorage.setItem("username", username);
118
+ localStorage.setItem("userId", user.uid);
119
+
120
+ // Trigger success animation
121
+ scatterPapers();
122
+
123
+ // Redirect to home page after animation
124
+ setTimeout(() => {
125
+ window.location.href = "/home";
126
+ }, 2000);
127
+
128
+ } catch (error) {
129
+ // Trigger failure animation
130
+ tearPaper();
131
+ console.error("Login error:", error);
132
+
133
+ }
134
+ });
135
+
136
+ // Password Reset Handler
137
+ forgotForm.addEventListener('submit', async (event) => {
138
+ event.preventDefault();
139
+ const email = document.querySelector('.forgot-password input[type="email"]').value;
140
+
141
+ try {
142
+ await auth.sendPasswordResetEmail(email);
143
+ alert('Password reset email sent! Check your inbox.');
144
+ } catch (error) {
145
+ alert(error.message);
146
+ console.error("Password reset error:", error);
147
+ }
148
+ });
149
+
150
+ // Form navigation handlers
151
+ let wrapper = document.querySelector('.wrapper');
152
+ const signUpLink = document.querySelector('.signup-link');
153
+ const signInLink = document.querySelector('.signin-link');
154
+ const forgotPassLink = document.querySelector('.forgot-pass a');
155
+ const backToLoginLink = document.querySelector('.forget-link');
156
+
157
+ // Set initial animation state
158
+ wrapper.classList.add('animated-signup');
159
+
160
+ // Form switching event listeners
161
+ signUpLink.addEventListener('click', () => {
162
+ wrapper.classList.add('animated-signin');
163
+ wrapper.classList.remove('animated-signup', 'animated-forgot');
164
+ });
165
+
166
+ signInLink.addEventListener('click', () => {
167
+ wrapper.classList.add('animated-signup');
168
+ wrapper.classList.remove('animated-signin', 'animated-forgot');
169
+ });
170
+
171
+ forgotPassLink.addEventListener('click', () => {
172
+ wrapper.classList.add('animated-forgot');
173
+ wrapper.classList.remove('animated-signin', 'animated-signup');
174
+ });
175
+
176
+ backToLoginLink.addEventListener('click', () => {
177
+ wrapper.classList.add('animated-signup');
178
+ wrapper.classList.remove('animated-forgot');
179
+ });
180
+
181
+ // Paper falling animation
182
+ document.addEventListener("DOMContentLoaded", () => {
183
+ const paperFallContainer = document.querySelector('.paper-fall');
184
+ function createPaper() {
185
+ const paper = document.createElement('div');
186
+ paper.classList.add('paper');
187
+
188
+ // Randomize the size of the paper
189
+ const size = Math.random() * 30 + 20; // Between 20px and 50px
190
+ paper.style.width = `${size}px`;
191
+ paper.style.height = `${size}px`;
192
+
193
+ // Randomize the position horizontally
194
+ const randomX = Math.random() * window.innerWidth;
195
+ paper.style.left = `${randomX}px`;
196
+
197
+ // Randomize the animation duration
198
+ const duration = Math.random() * 5 + 5; // Between 5s and 10s
199
+ paper.style.animationDuration = `${duration}s`;
200
+
201
+ // Randomize the rotation
202
+ const rotation = Math.random() * 360;
203
+ paper.style.transform = `rotate(${rotation}deg)`;
204
+
205
+ // Append the paper to the container
206
+ paperFallContainer.appendChild(paper);
207
+
208
+ // Remove the paper after it falls out of view
209
+ setTimeout(() => {
210
+ paper.remove();
211
+ }, duration * 1000);
212
+ }
213
+
214
+ // Create a new paper every 100ms
215
+ setInterval(createPaper, 100);
216
+ });
static/js/useractivity.js ADDED
@@ -0,0 +1,251 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ document.addEventListener("DOMContentLoaded", async () => {
2
+ // Firebase Auth & Firestore setup - reference from existing code
3
+ const auth = firebase.auth();
4
+ const db = firebase.firestore();
5
+
6
+ // Activity tab elements
7
+ const activityTab = document.getElementById("activity-tab");
8
+ const searchCountElement = activityTab.querySelector(".stat-value");
9
+ const timelineContainer = activityTab.querySelector(".timeline");
10
+
11
+ // Function to format relative time (e.g., "2 days ago")
12
+ function getRelativeTimeString(timestamp) {
13
+ const now = new Date();
14
+ const searchTime = timestamp.toDate();
15
+ const diffInMs = now - searchTime;
16
+ const diffInDays = Math.floor(diffInMs / (1000 * 60 * 60 * 24));
17
+
18
+ if (diffInDays === 0) {
19
+ const diffInHours = Math.floor(diffInMs / (1000 * 60 * 60));
20
+ if (diffInHours === 0) {
21
+ const diffInMinutes = Math.floor(diffInMs / (1000 * 60));
22
+ return diffInMinutes === 1 ? "1 minute ago" : `${diffInMinutes} minutes ago`;
23
+ }
24
+ return diffInHours === 1 ? "1 hour ago" : `${diffInHours} hours ago`;
25
+ } else if (diffInDays === 1) {
26
+ return "Yesterday";
27
+ } else if (diffInDays < 7) {
28
+ return `${diffInDays} days ago`;
29
+ } else if (diffInDays < 30) {
30
+ const diffInWeeks = Math.floor(diffInDays / 7);
31
+ return diffInWeeks === 1 ? "1 week ago" : `${diffInWeeks} weeks ago`;
32
+ } else {
33
+ return searchTime.toLocaleDateString();
34
+ }
35
+ }
36
+
37
+ // Function to create a timeline item
38
+ function createTimelineItem(activity) {
39
+ const timelineItem = document.createElement("div");
40
+ timelineItem.className = "timeline-item";
41
+
42
+ const timelineDot = document.createElement("div");
43
+ timelineDot.className = "timeline-dot";
44
+
45
+ const timelineContent = document.createElement("div");
46
+ timelineContent.className = "timeline-content";
47
+
48
+ const title = document.createElement("h4");
49
+ title.textContent = activity.type === "search" ? activity.query : activity.title;
50
+
51
+ const description = document.createElement("p");
52
+ if (activity.type === "search") {
53
+ description.textContent = `You searched for "${activity.query}"`;
54
+ } else if (activity.type === "save") {
55
+ description.textContent = `You saved "${activity.title}"`;
56
+ } else if (activity.type === "profile") {
57
+ description.textContent = "You updated your profile information";
58
+ }
59
+
60
+ const timelineDate = document.createElement("span");
61
+ timelineDate.className = "timeline-date";
62
+
63
+ const clockIcon = document.createElement("i");
64
+ clockIcon.className = "fas fa-clock";
65
+
66
+ timelineDate.appendChild(clockIcon);
67
+ timelineDate.append(` ${getRelativeTimeString(activity.timestamp)}`);
68
+
69
+ timelineContent.appendChild(title);
70
+ timelineContent.appendChild(description);
71
+ timelineContent.appendChild(timelineDate);
72
+
73
+ timelineItem.appendChild(timelineDot);
74
+ timelineItem.appendChild(timelineContent);
75
+
76
+ return timelineItem;
77
+ }
78
+
79
+ // Function to load and display user activity
80
+ async function loadUserActivity() {
81
+ const user = auth.currentUser;
82
+ if (!user) return;
83
+
84
+ try {
85
+ // Get user activity from Firestore
86
+ const activitiesSnapshot = await db.collection("users")
87
+ .doc(user.uid)
88
+ .collection("activities")
89
+ .orderBy("timestamp", "desc")
90
+ .limit(10)
91
+ .get();
92
+
93
+ // Clear existing timeline content
94
+ timelineContainer.innerHTML = "";
95
+
96
+ // Track search count
97
+ let searchCount = 0;
98
+
99
+ if (!activitiesSnapshot.empty) {
100
+ activitiesSnapshot.forEach(doc => {
101
+ const activity = doc.data();
102
+
103
+ // Count searches for the stat display
104
+ if (activity.type === "search") {
105
+ searchCount++;
106
+ }
107
+
108
+ // Create and append timeline item
109
+ const timelineItem = createTimelineItem(activity);
110
+ timelineContainer.appendChild(timelineItem);
111
+ });
112
+
113
+ // Update search count display
114
+ searchCountElement.textContent = searchCount;
115
+ } else {
116
+ // No activities found
117
+ searchCountElement.textContent = "0";
118
+
119
+ // Display a message when no activities are found
120
+ const noActivitiesMessage = document.createElement("div");
121
+ noActivitiesMessage.className = "empty-state";
122
+ noActivitiesMessage.innerHTML = `
123
+ <i class="fas fa-history"></i>
124
+ <p>No activity history found. Your recent searches and actions will appear here.</p>
125
+ `;
126
+ timelineContainer.appendChild(noActivitiesMessage);
127
+ }
128
+ } catch (error) {
129
+ console.error("Error loading user activity:", error);
130
+ timelineContainer.innerHTML = `<div class="error-message">Failed to load activity history.</div>`;
131
+ }
132
+ }
133
+
134
+ // Function to save search activity
135
+ window.saveSearchActivity = async (query) => {
136
+ const user = auth.currentUser;
137
+ if (!user || !query) return;
138
+
139
+ try {
140
+ // Create activity object
141
+ const activity = {
142
+ type: "search",
143
+ query: query,
144
+ timestamp: firebase.firestore.FieldValue.serverTimestamp()
145
+ };
146
+
147
+ // Save to Firestore
148
+ await db.collection("users")
149
+ .doc(user.uid)
150
+ .collection("activities")
151
+ .add(activity);
152
+
153
+ console.log("Search activity saved successfully");
154
+ } catch (error) {
155
+ console.error("Error saving search activity:", error);
156
+ }
157
+ };
158
+
159
+ // Function to save profile update activity
160
+ window.saveProfileUpdateActivity = async () => {
161
+ const user = auth.currentUser;
162
+ if (!user) return;
163
+
164
+ try {
165
+ // Create activity object
166
+ const activity = {
167
+ type: "profile",
168
+ title: "Profile Updated",
169
+ timestamp: firebase.firestore.FieldValue.serverTimestamp()
170
+ };
171
+
172
+ // Save to Firestore
173
+ await db.collection("users")
174
+ .doc(user.uid)
175
+ .collection("activities")
176
+ .add(activity);
177
+
178
+ console.log("Profile update activity saved successfully");
179
+ } catch (error) {
180
+ console.error("Error saving profile update activity:", error);
181
+ }
182
+ };
183
+
184
+ // Function to save article activity
185
+ window.saveArticleActivity = async (title, articleId) => {
186
+ const user = auth.currentUser;
187
+ if (!user || !title) return;
188
+
189
+ try {
190
+ // Create activity object
191
+ const activity = {
192
+ type: "save",
193
+ title: title,
194
+ articleId: articleId || null,
195
+ timestamp: firebase.firestore.FieldValue.serverTimestamp()
196
+ };
197
+
198
+ // Save to Firestore
199
+ await db.collection("users")
200
+ .doc(user.uid)
201
+ .collection("activities")
202
+ .add(activity);
203
+
204
+ console.log("Article save activity recorded successfully");
205
+ } catch (error) {
206
+ console.error("Error saving article activity:", error);
207
+ }
208
+ };
209
+
210
+ // Load activity when tab is clicked
211
+ const activityTabBtn = document.querySelector('[data-tab="activity"]');
212
+ if (activityTabBtn) {
213
+ activityTabBtn.addEventListener("click", loadUserActivity);
214
+ }
215
+
216
+ // Update the save profile button to also record the activity
217
+ const saveBtn = document.getElementById("saveBtn");
218
+ const originalClickHandler = saveBtn.onclick;
219
+
220
+ saveBtn.addEventListener("click", async () => {
221
+ // Call the original handler first
222
+ if (typeof originalClickHandler === "function") {
223
+ originalClickHandler();
224
+ }
225
+
226
+ // Add activity tracking
227
+ await saveProfileUpdateActivity();
228
+ });
229
+
230
+ // Hook this up to your search functionality
231
+ // Example: If you have a search form
232
+ const searchForm = document.getElementById("searchForm");
233
+ if (searchForm) {
234
+ searchForm.addEventListener("submit", (e) => {
235
+ const searchInput = searchForm.querySelector("input[type=search]");
236
+ if (searchInput && searchInput.value.trim()) {
237
+ saveSearchActivity(searchInput.value.trim());
238
+ }
239
+ });
240
+ }
241
+
242
+ // Load activity data when the page loads if user is on activity tab
243
+ auth.onAuthStateChanged(user => {
244
+ if (user && activityTab.classList.contains("active")) {
245
+ loadUserActivity();
246
+ }
247
+ });
248
+ });
249
+
250
+
251
+
templates/contactBoard.html ADDED
@@ -0,0 +1,451 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Team Member Contact </title>
7
+ <style>
8
+ @import url('https://fonts.googleapis.com/css2?family=Bungee+Shade&display=swap');
9
+ :root {
10
+ --primary-color: #4a90e2;
11
+ --secondary-color: #fcee0a;
12
+ --third-color: #6f42c1;
13
+ --text-color: #333;
14
+ --bg-color: #f5f7fa;
15
+ --white: #ffffff;
16
+ --forth-color: #00BFFF;
17
+ --aqua:#28cbb0;
18
+ }
19
+
20
+ * {
21
+ margin: 0;
22
+ padding: 0;
23
+ box-sizing: border-box;
24
+ font-family: 'Poppins', sans-serif;
25
+ }
26
+
27
+ body {
28
+ background-color: var(--secondary-color);
29
+ padding: 40px 20px;
30
+ }
31
+
32
+ .container {
33
+ max-width: 1200px;
34
+ margin: 0 auto;
35
+ padding: 20px;
36
+ }
37
+
38
+ .header {
39
+ text-align: center;
40
+ margin-bottom: 50px;
41
+ }
42
+
43
+ .header h1 {
44
+ font-size: 2.5rem;
45
+ color: var(--text-color);
46
+ margin-bottom: 15px;
47
+ position: relative;
48
+ display: inline-block;
49
+ font-family: 'Bungee Shade';
50
+ }
51
+
52
+ .header h1::after {
53
+ content: '';
54
+ position: absolute;
55
+ bottom: -10px;
56
+ left: 50%;
57
+ transform: translateX(-50%);
58
+ width: 80px;
59
+ height: 4px;
60
+ background: var(--forth-color);
61
+ border-radius: 2px;
62
+ }
63
+
64
+ .header p {
65
+
66
+ font-size: 1.1rem;
67
+ max-width: 700px;
68
+ margin: 0 auto;
69
+ font-family: 'Bungee Shade';
70
+ color: black;
71
+ }
72
+
73
+ .cards-container {
74
+ display: flex;
75
+ flex-wrap: wrap;
76
+ justify-content: center;
77
+ gap: 30px;
78
+ }
79
+
80
+ .card {
81
+ flex: 1;
82
+ min-width: 300px;
83
+ max-width: 350px;
84
+ background: var(--text-color);
85
+ border-radius: 15px;
86
+ overflow: hidden;
87
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
88
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
89
+ position: relative;
90
+ padding-top: 40px;
91
+ }
92
+
93
+ .card:hover {
94
+ transform: translateY(-10px);
95
+ box-shadow: 0 15px 40px rgba(0, 0, 0, 0.15);
96
+ }
97
+
98
+ .card-number {
99
+ position: absolute;
100
+ top: 20px;
101
+ left: 20px;
102
+ width: 40px;
103
+ height: 40px;
104
+ background: var(--secondary-color);
105
+ color: var(--text-color);
106
+ border-radius: 50%;
107
+ display: flex;
108
+ justify-content: center;
109
+ align-items: center;
110
+ font-weight: bold;
111
+ font-size: 1.2rem;
112
+ box-shadow: 0 5px 15px rgba(252, 238, 10, 0.3);
113
+ z-index: 2;
114
+ }
115
+
116
+ .card-title {
117
+ text-align: center;
118
+ padding: 0 20px;
119
+ margin-bottom: 20px;
120
+ }
121
+
122
+ .card-title h3 {
123
+ font-size: 1.5rem;
124
+ color: var(--text-color);
125
+ margin-bottom: 5px;
126
+ }
127
+
128
+ .card-title p {
129
+ font-size: 0.9rem;
130
+ color: #666;
131
+ }
132
+
133
+ .member-photo {
134
+ width: 120px;
135
+ height: 120px;
136
+ border-radius: 50%;
137
+ border: 5px solid white;
138
+ object-fit: cover;
139
+ margin: 0 auto 20px;
140
+ display: block;
141
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
142
+ background-color: #f0f0f0;
143
+ }
144
+
145
+ .card-body {
146
+ padding: 0 30px 30px;
147
+ }
148
+
149
+ .contact-info {
150
+ margin-bottom: 25px;
151
+ }
152
+
153
+ .contact-item {
154
+ display: flex;
155
+ align-items: center;
156
+ margin-bottom: 15px;
157
+ }
158
+
159
+ .contact-icon {
160
+ width: 35px;
161
+ height: 35px;
162
+ background: var(--forth-color);
163
+ border-radius: 50%;
164
+ display: flex;
165
+ justify-content: center;
166
+ align-items: center;
167
+ margin-right: 15px;
168
+ color: white;
169
+ font-size: 16px;
170
+ flex-shrink: 0;
171
+ }
172
+
173
+ .contact-text {
174
+ font-size: 0.9rem;
175
+ color: var(--secondary-color);
176
+ font-weight: bolder;
177
+ }
178
+
179
+
180
+ .contact-text strong {
181
+ color: var(--text-color);
182
+ display: block;
183
+ margin-bottom: 3px;
184
+ }
185
+
186
+ .social-links {
187
+ display: flex;
188
+ justify-content: center;
189
+ gap: 15px;
190
+ margin-top: 30px;
191
+ }
192
+
193
+ .social-link {
194
+ display: flex;
195
+ justify-content: center;
196
+ align-items: center;
197
+ width: 40px;
198
+ height: 40px;
199
+ border-radius: 50%;
200
+ background: var(--white);
201
+ color: var(--forth-color);
202
+ font-size: 18px;
203
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
204
+ transition: all 0.3s ease;
205
+ }
206
+
207
+ .social-link:hover {
208
+ transform: translateY(-5px) rotate(15deg);
209
+ background: var(--forth-color);
210
+ color: var(--white);
211
+ box-shadow: 0 8px 20px rgba(0, 191, 255, 0.3);
212
+ }
213
+
214
+ @media (max-width: 768px) {
215
+ .cards-container {
216
+ flex-direction: column;
217
+ align-items: center;
218
+ }
219
+
220
+ .card {
221
+ max-width: 100%;
222
+ }
223
+ }
224
+
225
+ </style>
226
+ </head>
227
+ <body>
228
+ <div class="container">
229
+ <div class="header">
230
+ <h1>Our Team Members</h1>
231
+ <p>Get in touch with our dedicated team who will provide you with quality service</p>
232
+ </div>
233
+
234
+ <div class="cards-container">
235
+ <!-- Card 1 -->
236
+ <div class="card">
237
+ <div class="card-number">01</div>
238
+ <img src="/assets/dev1.png" alt="Member Photo" class="member-photo">
239
+ <div class="card-body">
240
+ <div class="contact-info">
241
+ <div class="contact-item">
242
+ <div class="contact-icon">
243
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
244
+ <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
245
+ <circle cx="12" cy="7" r="4"></circle>
246
+ </svg>
247
+ </div>
248
+ <div class="contact-text">
249
+ <strong style="color: #fcee0a;">Name: </strong>
250
+ <sub style="margin-left: 10px;">Javvadi Jaswant Sri Akhilesh</sub>
251
+
252
+ </div>
253
+ </div>
254
+ <div class="contact-item">
255
+ <div class="contact-icon">
256
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
257
+ <path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path>
258
+ </svg>
259
+ </div>
260
+ <div class="contact-text">
261
+ <strong style="color: #fcee0a;">Phone</strong>
262
+ <sub style="margin-left: 10px;">+91-9632587415</sub>
263
+ </div>
264
+ </div>
265
+ <div class="contact-item">
266
+ <div class="contact-icon">
267
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
268
+ <path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path>
269
+ <polyline points="22,6 12,13 2,6"></polyline>
270
+ </svg>
271
+ </div>
272
+ <div class="contact-text">
273
+ <strong style="color: #fcee0a;">Email</strong>
274
+ <sub style="margin-left: 10px;">[email protected]</sub>
275
+ </div>
276
+ </div>
277
+ </div>
278
+ <div class="social-links">
279
+ <a href="#" class="social-link">
280
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
281
+ <path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"></path>
282
+ </svg>
283
+ </a>
284
+ <a href="#" class="social-link">
285
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
286
+ <rect x="2" y="2" width="20" height="20" rx="5" ry="5"></rect>
287
+ <path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z"></path>
288
+ <line x1="17.5" y1="6.5" x2="17.51" y2="6.5"></line>
289
+ </svg>
290
+ </a>
291
+ <a href="#" class="social-link">
292
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
293
+ <path d="M23 3a10.9 10.9 0 0 1-3.14 1.53 4.48 4.48 0 0 0-7.86 3v1A10.66 10.66 0 0 1 3 4s-4 9 5 13a11.64 11.64 0 0 1-7 2c9 5 20 0 20-11.5a4.5 4.5 0 0 0-.08-.83A7.72 7.72 0 0 0 23 3z"></path>
294
+ </svg>
295
+ </a>
296
+ <a href="#" class="social-link">
297
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
298
+ <path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"></path>
299
+ <rect x="2" y="9" width="4" height="12"></rect>
300
+ <circle cx="4" cy="4" r="2"></circle>
301
+ </svg>
302
+ </a>
303
+ </div>
304
+ </div>
305
+ </div>
306
+
307
+ <!-- Card 2 -->
308
+ <div class="card">
309
+ <div class="card-number">02</div>
310
+ <img src="/assets/dev2.png" alt="Member Photo" class="member-photo">
311
+ <div class="card-body">
312
+ <div class="contact-info">
313
+ <div class="contact-item">
314
+ <div class="contact-icon">
315
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
316
+ <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
317
+ <circle cx="12" cy="7" r="4"></circle>
318
+ </svg>
319
+ </div>
320
+ <div class="contact-text">
321
+ <strong style="color: #fcee0a;">Name: </strong>
322
+ <sub style="margin-left: 10px;">Sai Darahas</sub>
323
+ </div>
324
+ </div>
325
+ <div class="contact-item">
326
+ <div class="contact-icon">
327
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
328
+ <path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path>
329
+ </svg>
330
+ </div>
331
+ <div class="contact-text">
332
+ <strong style="color: #fcee0a;">Phone</strong>
333
+ <sub style="margin-left: 10px;">+91-852147963</sub>
334
+ </div>
335
+ </div>
336
+ <div class="contact-item">
337
+ <div class="contact-icon">
338
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
339
+ <path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path>
340
+ <polyline points="22,6 12,13 2,6"></polyline>
341
+ </svg>
342
+ </div>
343
+ <div class="contact-text">
344
+ <strong style="color: #fcee0a;">Email</strong>
345
+ <sub style="margin-left: 10px;">[email protected]</sub>
346
+ </div>
347
+ </div>
348
+ </div>
349
+ <div class="social-links">
350
+ <a href="#" class="social-link">
351
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
352
+ <path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"></path>
353
+ </svg>
354
+ </a>
355
+ <a href="#" class="social-link">
356
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
357
+ <rect x="2" y="2" width="20" height="20" rx="5" ry="5"></rect>
358
+ <path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z"></path>
359
+ <line x1="17.5" y1="6.5" x2="17.51" y2="6.5"></line>
360
+ </svg>
361
+ </a>
362
+ <a href="#" class="social-link">
363
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
364
+ <path d="M23 3a10.9 10.9 0 0 1-3.14 1.53 4.48 4.48 0 0 0-7.86 3v1A10.66 10.66 0 0 1 3 4s-4 9 5 13a11.64 11.64 0 0 1-7 2c9 5 20 0 20-11.5a4.5 4.5 0 0 0-.08-.83A7.72 7.72 0 0 0 23 3z"></path>
365
+ </svg>
366
+ </a>
367
+ <a href="#" class="social-link">
368
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
369
+ <path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"></path>
370
+ <rect x="2" y="9" width="4" height="12"></rect>
371
+ <circle cx="4" cy="4" r="2"></circle>
372
+ </svg>
373
+ </a>
374
+ </div>
375
+ </div>
376
+ </div>
377
+
378
+ <!-- Card 3 -->
379
+ <div class="card">
380
+ <div class="card-number">03</div>
381
+ <img src="/assets/dev3.png" alt="Member Photo" class="member-photo">
382
+ <div class="card-body">
383
+ <div class="contact-info">
384
+ <div class="contact-item">
385
+ <div class="contact-icon">
386
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
387
+ <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
388
+ <circle cx="12" cy="7" r="4"></circle>
389
+ </svg>
390
+ </div>
391
+ <div class="contact-text">
392
+ <strong style="color: #fcee0a;">Name: </strong>
393
+ <sub style="margin-left: 10px;">Thota Abilash Reddy</sub>
394
+ </div>
395
+ </div>
396
+ <div class="contact-item">
397
+ <div class="contact-icon">
398
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
399
+ <path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path>
400
+ </svg>
401
+ </div>
402
+ <div class="contact-text">
403
+ <strong style="color: #fcee0a;">Phone</strong>
404
+ <sub style="margin-left: 10px;">+91-7412589635</sub>
405
+ </div>
406
+ </div>
407
+ <div class="contact-item">
408
+ <div class="contact-icon">
409
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
410
+ <path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path>
411
+ <polyline points="22,6 12,13 2,6"></polyline>
412
+ </svg>
413
+ </div>
414
+ <div class="contact-text">
415
+ <strong style="color: #fcee0a;">Email</strong>
416
+ <sub style="margin-left: 10px;">[email protected]</sub>
417
+ </div>
418
+ </div>
419
+ </div>
420
+ <div class="social-links">
421
+ <a href="#" class="social-link">
422
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
423
+ <path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"></path>
424
+ </svg>
425
+ </a>
426
+ <a href="#" class="social-link">
427
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
428
+ <rect x="2" y="2" width="20" height="20" rx="5" ry="5"></rect>
429
+ <path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z"></path>
430
+ <line x1="17.5" y1="6.5" x2="17.51" y2="6.5"></line>
431
+ </svg>
432
+ </a>
433
+ <a href="#" class="social-link">
434
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
435
+ <path d="M23 3a10.9 10.9 0 0 1-3.14 1.53 4.48 4.48 0 0 0-7.86 3v1A10.66 10.66 0 0 1 3 4s-4 9 5 13a11.64 11.64 0 0 1-7 2c9 5 20 0 20-11.5a4.5 4.5 0 0 0-.08-.83A7.72 7.72 0 0 0 23 3z"></path>
436
+ </svg>
437
+ </a>
438
+ <a href="#" class="social-link">
439
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
440
+ <path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"></path>
441
+ <rect x="2" y="9" width="4" height="12"></rect>
442
+ <circle cx="4" cy="4" r="2"></circle>
443
+ </svg>
444
+ </a>
445
+ </div>
446
+ </div>
447
+ </div>
448
+ </div>
449
+ </div>
450
+ </body>
451
+ </html>
templates/feedback.html ADDED
@@ -0,0 +1,566 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Feedback Form</title>
7
+ <!-- Added FontAwesome library for icons -->
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
9
+ <style>
10
+ @import url('https://fonts.googleapis.com/css2?family=Bungee+Shade&display=swap');
11
+ /* Global styles */
12
+ * {
13
+ margin: 0;
14
+ padding: 0;
15
+ box-sizing: border-box;
16
+ font-family: 'Poppins', 'Segoe UI', Arial, sans-serif;
17
+ }
18
+
19
+ body {
20
+ background-color: #333333;
21
+ color: #ffffff;
22
+ padding: 20px;
23
+ }
24
+
25
+ /* Header styles */
26
+ .header {
27
+ background-color: #ffed00;
28
+ padding: 30px 20px;
29
+ text-align: center;
30
+ color: #333333;
31
+
32
+ }
33
+
34
+ .header h1 {
35
+ font-size: 2.5rem;
36
+ font-weight: 800;
37
+ text-transform: uppercase;
38
+ letter-spacing: 1px;
39
+ text-shadow: 3px 3px 0 #333333;
40
+ margin-bottom: 10px;
41
+ font-family: 'Bungee Shade';
42
+ }
43
+
44
+ .header p {
45
+ font-size: 1rem;
46
+ margin-top: 15px;
47
+ font-weight: 500;
48
+ text-transform: uppercase;
49
+ letter-spacing: 1px;
50
+ font-family:'Courier New', Courier, monospace;
51
+ font-weight: bolder;
52
+ }
53
+
54
+ .header-underline {
55
+ width: 150px;
56
+ height: 5px;
57
+ background-color: #00b7ff;
58
+ margin: 5px auto 15px;
59
+ border-radius: 5px;
60
+ }
61
+
62
+ /* Form styles */
63
+ .form-container {
64
+ padding: 30px;
65
+ }
66
+
67
+ .form-group {
68
+ margin-bottom: 25px;
69
+ }
70
+
71
+ .form-group label {
72
+ display: block;
73
+ margin-bottom: 8px;
74
+ font-weight: 600;
75
+ color: #ffed00;
76
+ }
77
+
78
+ .form-control {
79
+ width: 100%;
80
+ padding: 12px 15px;
81
+ background-color: #3a3a3a;
82
+ border: 2px solid #ffed00;
83
+ border-radius: 8px;
84
+ color: #ffffff;
85
+ font-size: 16px;
86
+ }
87
+
88
+ .form-control:focus {
89
+ outline: none;
90
+ border-color: #00b7ff;
91
+ box-shadow: 0 0 5px rgba(0, 183, 255, 0.5);
92
+ }
93
+
94
+ textarea.form-control {
95
+ min-height: 120px;
96
+ resize: vertical;
97
+ }
98
+
99
+ .rating {
100
+ display: flex;
101
+ gap: 5px;
102
+ margin-top: 10px;
103
+ }
104
+
105
+ .rating input[type="radio"] {
106
+ display: none;
107
+ }
108
+
109
+ .rating label {
110
+ cursor: pointer;
111
+ width: 30px;
112
+ height: 30px;
113
+ background-color: #3a3a3a;
114
+ border: 2px solid #ffed00;
115
+ border-radius: 50%;
116
+ display: flex;
117
+ align-items: center;
118
+ justify-content: center;
119
+ color: #ffed00;
120
+ font-weight: bold;
121
+ transition: all 0.2s ease;
122
+ }
123
+
124
+ .rating input[type="radio"]:checked + label {
125
+ background-color: #ffed00;
126
+ color: #333333;
127
+ transform: scale(1.1);
128
+ }
129
+
130
+ .submit-btn {
131
+ display: block;
132
+ width: 100%;
133
+ padding: 15px;
134
+ background-color: #ffed00;
135
+ color: #333333;
136
+ border: none;
137
+ border-radius: 8px;
138
+ font-size: 18px;
139
+ font-weight: 700;
140
+ text-transform: uppercase;
141
+ cursor: pointer;
142
+ transition: all 0.3s ease;
143
+ margin-top: 10px;
144
+ }
145
+
146
+
147
+
148
+ .submit-btn:hover {
149
+ background-color: #00b7ff;
150
+ transform: translateY(-3px);
151
+ }
152
+
153
+
154
+
155
+ .form-section-title {
156
+ display: flex;
157
+ align-items: center;
158
+ margin-bottom: 20px;
159
+ }
160
+
161
+ .section-number {
162
+ width: 40px;
163
+ height: 40px;
164
+ background-color: #ffed00;
165
+ color: #333333;
166
+ border-radius: 50%;
167
+ display: flex;
168
+ align-items: center;
169
+ justify-content: center;
170
+ font-weight: bold;
171
+ font-size: 18px;
172
+ margin-right: 15px;
173
+ }
174
+
175
+ .section-title {
176
+ font-size: 1.3rem;
177
+ color: #ffed00;
178
+ }
179
+
180
+
181
+ .paper-alert {
182
+ background: #fff;
183
+ border-radius: 2px;
184
+ box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23),
185
+ 0 0 0 1px rgba(0,0,0,0.05);
186
+ min-width: 320px;
187
+ max-width: 400px;
188
+ position: fixed;
189
+ right: 16px;
190
+ top: 16px;
191
+ overflow: hidden;
192
+ opacity: 0;
193
+ pointer-events: none;
194
+ z-index: 10000;
195
+ transform-origin: top right;
196
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
197
+ }
198
+
199
+ /* Torn paper effect */
200
+ .paper-alert::before {
201
+ content: '';
202
+ position: absolute;
203
+ left: 0;
204
+ bottom: -2px;
205
+ width: 100%;
206
+ height: 4px;
207
+ background-color: #fff;
208
+ background-image:
209
+ radial-gradient(circle at 2px 3px, transparent 2px, #fff 2px, #fff 3px, transparent 3px),
210
+ radial-gradient(circle at 10px 3px, transparent 2px, #fff 2px, #fff 3px, transparent 3px),
211
+ radial-gradient(circle at 18px 3px, transparent 2px, #fff 2px, #fff 3px, transparent 3px),
212
+ radial-gradient(circle at 26px 3px, transparent 2px, #fff 2px, #fff 3px, transparent 3px),
213
+ radial-gradient(circle at 34px 3px, transparent 2px, #fff 2px, #fff 3px, transparent 3px),
214
+ radial-gradient(circle at 42px 3px, transparent 2px, #fff 2px, #fff 3px, transparent 3px);
215
+ background-size: 50px 10px;
216
+ z-index: 2;
217
+ }
218
+
219
+ /* Paper texture */
220
+ .paper-alert::after {
221
+ content: '';
222
+ position: absolute;
223
+ left: 0;
224
+ top: 0;
225
+ width: 100%;
226
+ height: 100%;
227
+ background-image: url("data:image/svg+xml,%3Csvg width='100' height='100' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise' x='0' y='0'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3CfeBlend mode='screen'/%3E%3C/filter%3E%3Crect width='100' height='100' filter='url(%23noise)' opacity='0.08'/%3E%3C/svg%3E");
228
+ pointer-events: none;
229
+ z-index: 1;
230
+ }
231
+
232
+ .paper-alert.showAlert {
233
+ opacity: 1;
234
+ pointer-events: auto;
235
+ }
236
+
237
+ .paper-alert.show {
238
+ animation: paper_enter 0.7s cubic-bezier(0.68, -0.55, 0.27, 1.55) forwards;
239
+ }
240
+
241
+ @keyframes paper_enter {
242
+ 0% { transform: translateX(100%) rotate(5deg); }
243
+ 50% { transform: translateX(-10%) rotate(-2deg); }
244
+ 75% { transform: translateX(5%) rotate(1deg); }
245
+ 100% { transform: translateX(0) rotate(0); }
246
+ }
247
+
248
+ .paper-alert.hide {
249
+ animation: paper_exit 0.5s cubic-bezier(0.68, -0.55, 0.27, 1.55) forwards;
250
+ }
251
+
252
+ @keyframes paper_exit {
253
+ 0% { transform: translateX(0) rotate(0); }
254
+ 30% { transform: translateX(5%) rotate(1deg); }
255
+ 100% { transform: translateX(110%) rotate(5deg); }
256
+ }
257
+
258
+ .paper-alert-content {
259
+ position: relative;
260
+ padding: 18px 15px;
261
+ z-index: 3;
262
+ }
263
+
264
+ .paper-pin {
265
+ position: absolute;
266
+ width: 12px;
267
+ height: 12px;
268
+ background: #f44336;
269
+ border-radius: 50%;
270
+ top: 10px;
271
+ left: 20px;
272
+ box-shadow: inset 0 0 0 2px rgba(0,0,0,0.2), 0 1px 2px rgba(0,0,0,0.2);
273
+ }
274
+
275
+ .paper-pin:after {
276
+ content: '';
277
+ position: absolute;
278
+ width: 6px;
279
+ height: 6px;
280
+ background: rgba(255,255,255,0.4);
281
+ border-radius: 50%;
282
+ top: 2px;
283
+ left: 2px;
284
+ }
285
+
286
+ .paper-clip {
287
+ position: absolute;
288
+ top: -8px;
289
+ right: 30px;
290
+ width: 30px;
291
+ height: 30px;
292
+ border: 2px solid #5d9cec;
293
+ border-radius: 0 0 0 10px;
294
+ border-top: none;
295
+ border-right: none;
296
+ transform: rotate(45deg);
297
+ z-index: 10;
298
+ }
299
+
300
+ .paper-clip:before {
301
+ content: '';
302
+ position: absolute;
303
+ width: 15px;
304
+ height: 10px;
305
+ border: 2px solid #5d9cec;
306
+ border-radius: 0 5px 0 5px;
307
+ border-top: none;
308
+ border-right: none;
309
+ top: 0;
310
+ left: 0;
311
+ transform: rotate(0deg);
312
+ }
313
+
314
+ .alert-icon {
315
+ display: flex;
316
+ align-items: center;
317
+ margin-bottom: 10px;
318
+ }
319
+
320
+ .alert-icon i {
321
+ font-size: 22px;
322
+ color: #f44336;
323
+ margin-right: 10px;
324
+ }
325
+
326
+ .alert-title {
327
+ font-weight: 600;
328
+ font-size: 16px;
329
+ color: #333;
330
+ margin: 0;
331
+ }
332
+
333
+ .paper-alert .msg {
334
+ margin-top: 6px;
335
+ margin-left: 32px;
336
+ font-size: 14px;
337
+ color: #555;
338
+ line-height: 1.4;
339
+ }
340
+
341
+ .paper-alert .close-btn {
342
+ position: absolute;
343
+ top: 10px;
344
+ right: 10px;
345
+ width: 24px;
346
+ height: 24px;
347
+ display: flex;
348
+ align-items: center;
349
+ justify-content: center;
350
+ cursor: pointer;
351
+ background: rgba(0,0,0,0.05);
352
+ border-radius: 50%;
353
+ color: #666;
354
+ transition: all 0.2s;
355
+ z-index: 5;
356
+ }
357
+
358
+ .paper-alert .close-btn:hover {
359
+ background: rgba(0,0,0,0.1);
360
+ color: #333;
361
+ transform: rotate(90deg);
362
+ }
363
+
364
+ .alert-btn {
365
+ position: fixed;
366
+ top: 7rem;
367
+ right: 2.5rem;
368
+ z-index: 1201;
369
+ font-size: 0.9rem;
370
+ padding: 0.7rem 1.5rem;
371
+ border-radius: 5px;
372
+ background: linear-gradient(135deg, #ff6b6b, #ff8e8e);
373
+ color: white;
374
+ border: none;
375
+ box-shadow: 0 4px 15px rgba(255, 107, 107, 0.4);
376
+ cursor: pointer;
377
+ font-family: 'Segoe UI', sans-serif;
378
+ font-weight: 600;
379
+ letter-spacing: 0.5px;
380
+ transition: all 0.3s;
381
+ }
382
+
383
+ .alert-btn:hover {
384
+ transform: translateY(-3px) scale(1.03);
385
+ box-shadow: 0 6px 20px rgba(255, 107, 107, 0.5);
386
+ }
387
+
388
+ .paper-lines {
389
+ position: absolute;
390
+ left: 0;
391
+ top: 0;
392
+ width: 100%;
393
+ height: 100%;
394
+ background-image:
395
+ linear-gradient(to bottom, transparent 0%, transparent 98%, #e0e0e0 99%, transparent 100%);
396
+ background-size: 100% 24px;
397
+ z-index: 0;
398
+ pointer-events: none;
399
+ }
400
+
401
+ .handwritten {
402
+ font-family: 'Segoe Script', 'Bradley Hand', cursive;
403
+ transform: rotate(-1deg);
404
+ color: #2c3e50;
405
+ }
406
+ </style>
407
+ </head>
408
+ <body>
409
+ <div class="paper-alert hide" id="paperAlert">
410
+ <div class="paper-pin"></div>
411
+ <div class="paper-clip"></div>
412
+ <div class="paper-lines"></div>
413
+ <div class="paper-alert-content">
414
+ <div class="alert-icon">
415
+ <i class="fas fa-exclamation-circle" id="alertIcon"></i>
416
+ <h4 class="alert-title" id="alertTitle">Authentication Required</h4>
417
+ </div>
418
+ <div class="msg handwritten" id="alertMessage">You must be Logged In to continue!</div>
419
+ <div class="close-btn">
420
+ <i class="fas fa-times"></i>
421
+ </div>
422
+ </div>
423
+ </div>
424
+
425
+ <div class="header">
426
+ <h1>Customer Feedback</h1>
427
+ <div class="header-underline"></div>
428
+ <p>Help us improve our service by sharing your experience</p>
429
+ </div>
430
+
431
+ <form class="form-container" id="feedbackForm">
432
+ <div class="form-section-title">
433
+ <div class="section-number">01</div>
434
+ <h2 class="section-title">Your Information</h2>
435
+ </div>
436
+
437
+ <div class="form-group">
438
+ <label for="name">Full Name</label>
439
+ <input type="text" id="name" class="form-control" placeholder="Enter your name" required>
440
+ </div>
441
+
442
+ <div class="form-group">
443
+ <label for="email">Email Address</label>
444
+ <input type="email" id="email" class="form-control" placeholder="Enter your email" required>
445
+ </div>
446
+
447
+ <div class="form-group">
448
+ <label for="phone">Phone Number</label>
449
+ <input type="tel" id="phone" class="form-control" placeholder="Enter your phone number">
450
+ </div>
451
+
452
+ <div class="form-section-title">
453
+ <div class="section-number">02</div>
454
+ <h2 class="section-title">Your Experience</h2>
455
+ </div>
456
+
457
+ <div class="form-group">
458
+ <label>How would you rate your overall experience?</label>
459
+ <div class="rating">
460
+ <input type="radio" name="rating" id="star1" value="1">
461
+ <label for="star1">1</label>
462
+
463
+ <input type="radio" name="rating" id="star2" value="2">
464
+ <label for="star2">2</label>
465
+
466
+ <input type="radio" name="rating" id="star3" value="3">
467
+ <label for="star3">3</label>
468
+
469
+ <input type="radio" name="rating" id="star4" value="4">
470
+ <label for="star4">4</label>
471
+
472
+ <input type="radio" name="rating" id="star5" value="5">
473
+ <label for="star5">5</label>
474
+ </div>
475
+ </div>
476
+
477
+ <div class="form-section-title">
478
+ <div class="section-number">03</div>
479
+ <h2 class="section-title">Your Feedback</h2>
480
+ </div>
481
+
482
+ <div class="form-group">
483
+ <label for="comments">Please share your thoughts or suggestions</label>
484
+ <textarea id="comments" class="form-control" placeholder="Your feedback is valuable to us..." required></textarea>
485
+ </div>
486
+
487
+ <div class="form-group">
488
+ <label for="improvements">How can we improve our service?</label>
489
+ <textarea id="improvements" class="form-control" placeholder="Any suggestions for improvement..."></textarea>
490
+ </div>
491
+
492
+ <button type="submit" class="submit-btn" id="submitBtn">Submit Feedback</button>
493
+ </form>
494
+
495
+
496
+ <script>
497
+ // Wait for DOM to be fully loaded
498
+ document.addEventListener('DOMContentLoaded', function() {
499
+ console.log("DOM fully loaded");
500
+
501
+ // Get the form element with ID
502
+ const form = document.getElementById('feedbackForm');
503
+ console.log("Form element:", form);
504
+
505
+ // Function to show the paper alert
506
+ function showPaperAlert(message, title = "Alert", icon = "fa-exclamation-circle", duration = 7000) {
507
+ console.log("Showing alert:", message);
508
+ const paperAlert = document.getElementById('paperAlert');
509
+ const alertMessage = document.getElementById('alertMessage');
510
+ const alertTitle = document.getElementById('alertTitle');
511
+ const alertIcon = document.getElementById('alertIcon');
512
+
513
+ // Set custom message, title and icon
514
+ alertMessage.textContent = message;
515
+ alertTitle.textContent = title;
516
+
517
+ // Update the icon class
518
+ alertIcon.className = ''; // Clear existing classes
519
+ alertIcon.classList.add('fas', icon);
520
+
521
+ // Show the alert
522
+ paperAlert.classList.remove("hide");
523
+ paperAlert.classList.add("show", "showAlert");
524
+
525
+ // Auto-hide after specified duration
526
+ setTimeout(() => {
527
+ hideAlert();
528
+ }, duration);
529
+ }
530
+
531
+ // Function to hide the alert with animation
532
+ function hideAlert() {
533
+ console.log("Hiding alert");
534
+ const paperAlert = document.getElementById('paperAlert');
535
+ paperAlert.classList.remove("show");
536
+ paperAlert.classList.add("hide");
537
+ setTimeout(() => {
538
+ paperAlert.classList.remove("showAlert");
539
+ }, 500);
540
+ }
541
+
542
+ // Set up close button functionality
543
+ document.querySelector('#paperAlert .close-btn').addEventListener('click', function() {
544
+ hideAlert();
545
+ });
546
+
547
+
548
+ // Add event listener for form submission
549
+ if (form) {
550
+ form.addEventListener('submit', function(event) {
551
+ event.preventDefault();
552
+ console.log("Form submitted");
553
+ showPaperAlert(
554
+ "Thank you for your feedback!",
555
+ "Submission Successful",
556
+ "fa-check-circle",
557
+ 7000
558
+ );
559
+ });
560
+ } else {
561
+ console.error("Form element not found!");
562
+ }
563
+ });
564
+ </script>
565
+ </body>
566
+ </html>
templates/gra.html ADDED
@@ -0,0 +1,179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Interactive Citation Network</title>
7
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js"></script>
8
+ <link rel="stylesheet" href="/static/css/gra.css">
9
+ </head>
10
+ <body>
11
+ <div id="container"></div>
12
+
13
+ <!-- Replace the file-input div with this loading section -->
14
+ <div class="loading-section">
15
+ <div id="statusMessage">Loading citation network...</div>
16
+ <div class="progress-container">
17
+ <div id="loadingProgress" class="progress-bar"></div>
18
+ </div>
19
+ </div>
20
+
21
+ <div class="time-navigation">
22
+ <button id="prevTimeButton" disabled>← Previous</button>
23
+ <div class="time-slider-container">
24
+ <input type="range" id="timeSlider" class="time-slider" min="0" max="0" value="0" step="1">
25
+ <div class="time-labels">
26
+ <span id="startTimeLabel">Start Date</span>
27
+ <span id="endTimeLabel">End Date</span>
28
+ </div>
29
+ <div id="timeLoader" class="time-loader"></div>
30
+ </div>
31
+ <div class="time-indicator" id="currentTimePeriod">Loading time range...</div>
32
+ <button id="nextTimeButton">Next →</button>
33
+ </div>
34
+
35
+
36
+ <div class="controls">
37
+ <div class="control-section">
38
+ <h4>Network Physics</h4>
39
+ <label>
40
+ Link Distance:
41
+ <input type="range" id="linkDistanceSlider" min="20" max="200" value="80">
42
+ <span id="linkDistanceValue">80</span>
43
+ </label>
44
+ <label>
45
+ Charge Strength:
46
+ <input type="range" id="chargeSlider" min="-500" max="-10" value="-120">
47
+ <span id="chargeValue">-120</span>
48
+ </label>
49
+ <label>
50
+ Network Compactness:
51
+ <input type="range" id="gravitySlider" min="0.01" max="0.5" step="0.01" value="0.1">
52
+ <span id="gravityValue">0.1</span>
53
+ </label>
54
+ </div>
55
+
56
+ <div class="control-section">
57
+ <h4>Node Sizing</h4>
58
+ <label>
59
+ Main Papers:
60
+ <input type="range" id="mainNodeScaleSlider" min="0.5" max="3.0" step="0.1" value="1.0">
61
+ <span id="mainNodeScaleValue">1.0</span>
62
+ </label>
63
+ <label>
64
+ Bridge Papers:
65
+ <input type="range" id="bridgeNodeScaleSlider" min="0.5" max="3.0" step="0.1" value="1.0">
66
+ <span id="bridgeNodeScaleValue">1.0</span>
67
+ </label>
68
+ <label>
69
+ Normal Papers:
70
+ <input type="range" id="normalNodeScaleSlider" min="0.5" max="3.0" step="0.1" value="1.0">
71
+ <span id="normalNodeScaleValue">1.0</span>
72
+ </label>
73
+ </div>
74
+
75
+ <div class="control-section">
76
+ <h4>View Controls</h4>
77
+ <button id="resetButton">Reset View</button>
78
+ <button id="centerViewButton">Center View</button>
79
+ </div>
80
+ </div>
81
+ </div>
82
+
83
+
84
+ <div class="legend">
85
+ <h3>Legend</h3>
86
+ <div class="legend-item">
87
+ <div class="legend-indicator">
88
+ <div class="legend-circle main"></div>
89
+ </div>
90
+ <span>Main Paper (Circle)</span>
91
+ </div>
92
+ <div class="legend-item">
93
+ <div class="legend-indicator legend-star">
94
+ <svg width="20" height="20" viewBox="0 0 24 24">
95
+ <path d="M12,5 L13.25,9.25 L17.5,9.25 L14.12,11.75 L15.25,16 L12,13.5 L8.75,16 L9.88,11.75 L6.5,9.25 L10.75,9.25 Z" fill="#4285f4"></path>
96
+ </svg>
97
+ </div>
98
+ <span>Main Bridge Paper (Star)</span>
99
+ </div>
100
+ <div class="legend-item">
101
+ <div class="legend-indicator">
102
+ <div class="legend-circle referenced"></div>
103
+ </div>
104
+ <span>Referenced Paper (Circle)</span>
105
+ </div>
106
+ <div class="legend-item">
107
+ <div class="legend-indicator">
108
+ <div class="legend-circle citing"></div>
109
+ </div>
110
+ <span>Citing Paper (Circle)</span>
111
+ </div>
112
+ <div class="legend-item">
113
+ <div class="legend-indicator">
114
+ <div class="legend-diamond"></div>
115
+ </div>
116
+ <span>Bridge Paper (Diamond)</span>
117
+ </div>
118
+ <div class="legend-info">
119
+ <p>Node size also reflects importance:</p>
120
+ <p>• Main papers: Size may increase with citation count</p>
121
+ <p>• All node types can be scaled using the controls</p>
122
+ </div>
123
+ <div class="legend-item">
124
+ <div class="legend-indicator legend-arrow">
125
+ <svg width="50" height="20">
126
+ <defs>
127
+ <marker id="legend-arrow" viewBox="0 -5 10 10" refX="8" refY="0"
128
+ markerWidth="6" markerHeight="6" orient="auto">
129
+ <path d="M0,-5L10,0L0,5" fill="#999"></path>
130
+ </marker>
131
+ </defs>
132
+ <line x1="5" y1="10" x2="45" y2="10" stroke="#999" stroke-width="2" marker-end="url(#legend-arrow)"></line>
133
+ </svg>
134
+ </div>
135
+ <span>Citation Direction</span>
136
+ </div>
137
+ </div>
138
+
139
+ <!-- New Paper Details Sidebar -->
140
+ <div id="paperDetailsPanel" class="paper-details-panel">
141
+ <div class="panel-header">
142
+ <h3>Paper Details</h3>
143
+ <button id="closePanelButton" class="close-button">×</button>
144
+ </div>
145
+ <div class="panel-content">
146
+ <div id="paperInfo">
147
+ <h4 id="paperTitle">Select a paper to view details</h4>
148
+ <p id="paperType"></p>
149
+ <p id="paperCitations"></p>
150
+ <div id="paperConcepts"></div>
151
+ <div class="paper-link">
152
+ <a id="openAlexLink" href="#" target="_blank">View on OpenAlex</a>
153
+ </div>
154
+ </div>
155
+
156
+ <div class="paper-lists">
157
+ <div class="paper-list">
158
+ <h5>Citing Papers</h5>
159
+ <ul id="citingPapersList"></ul>
160
+ </div>
161
+ <div class="paper-list">
162
+ <h5>Referenced Papers</h5>
163
+ <ul id="referencedPapersList"></ul>
164
+ </div>
165
+ </div>
166
+ </div>
167
+ </div>
168
+
169
+ <!-- <div id="loadingIndicator">Loading data...</div> -->
170
+
171
+ <div class="tooltip" style="display: none;"></div>
172
+
173
+ <script type="text/javascript">
174
+ window.__INITIAL_DATA__= JSON.parse(`{{ data | safe }}`);
175
+ </script>
176
+ <script src="/static/js/gra.js"></script>
177
+
178
+ </body>
179
+ </html>
templates/homepage.html ADDED
@@ -0,0 +1,475 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>PaperLens</title>
7
+ <link rel="stylesheet" href="/static/css/homestyle.css">
8
+ <link rel="stylesheet" href="/static/css/profilepage.css">
9
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
10
+
11
+
12
+ </head>
13
+ <body>
14
+ <div id="loading-overlay" class="loading-overlay">
15
+ <div class="loading-container">
16
+ <div class="cyclone" id="cyclone"></div>
17
+ <div class="ground"></div>
18
+ <div class="loading-text" id="loading-text">Loading...</div>
19
+ </div>
20
+ </div>
21
+ <div class="paper-alert hide" id="paperAlert">
22
+ <div class="paper-pin"></div>
23
+ <div class="paper-clip"></div>
24
+ <div class="paper-lines"></div>
25
+ <div class="paper-alert-content">
26
+ <div class="alert-icon">
27
+ <i class="fas fa-exclamation-circle" id="alertIcon"></i>
28
+ <h4 class="alert-title" id="alertTitle">Authentication Required</h4>
29
+ </div>
30
+ <div class="msg handwritten" id="alertMessage">You must be Logged In to continue!</div>
31
+ <div class="close-btn">
32
+ <i class="fas fa-times"></i>
33
+ </div>
34
+ </div>
35
+ </div>
36
+ <header>
37
+ <div class="header-content">
38
+ <div class="logo-container">
39
+ <img src="/assets/logo.svg" style="height: 70px; width: auto;" alt="PaperLens Logo">
40
+ <h1 id="appname">PaperLens</h1>
41
+ </div>
42
+ <nav>
43
+ <a href="#interactive-book">Features</a>
44
+ <a href="#architecture ">Solutions</a>
45
+ <a href="#contacts">Contacts</a>
46
+ <a href="#feedback">Feedback</a>
47
+ <a href="/login"><button class="sign-in-btn" id="signInBtn">Sign In</button></a>
48
+ <a id="userProfile" class="profile-trigger">
49
+ <span id="userName">dara</span>
50
+ <img src="/assets/userPhoto.png" alt="Profile Picture" class="profile-pic" id="profile-pic">
51
+ <i class="fas fa-chevron-down"></i>
52
+ </a>
53
+ </nav>
54
+ </div>
55
+ </header>
56
+
57
+ <div class="profile-container" id="profileContainer">
58
+ <div class="profile-overlay" id="profileOverlay"></div>
59
+ <div class="profile-dropdown" id="profileDropdown">
60
+ <div class="profile-header">
61
+ <div class="profile-cover-photo"></div>
62
+ <div class="profile-avatar-container">
63
+ <img src="/assets/default-user.jpg" alt="Profile Picture" id="profileAvatar">
64
+ </div>
65
+ <h2 id="profileHeaderName">Welcome, Dara</h2>
66
+ </div>
67
+
68
+ <div class="profile-tabs">
69
+ <button class="tab-btn active" data-tab="personal">Personal Info</button>
70
+ <button class="tab-btn" data-tab="activity">Activity</button>
71
+ </div>
72
+
73
+ <div class="tab-content active" id="personal-tab">
74
+ <div class="profile-field">
75
+ <label><i class="fas fa-user"></i> Name</label>
76
+ <input type="text" id="profileName" placeholder="Enter your name" disabled>
77
+ <span class="edit-icon" onclick="enableEdit('profileName')" >
78
+ <img src="/assets/editicon.png" alt="Edit" class="edit-img"></span>
79
+ </div>
80
+
81
+ <div class="profile-field">
82
+ <label><i class="fas fa-envelope"></i> Email</label>
83
+ <input type="email" id="profileEmail" disabled>
84
+ <div class="verification-badge" id="emailVerified">
85
+ <i class="fas fa-check-circle"></i> Verified
86
+ </div>
87
+ </div>
88
+
89
+ <div class="profile-field">
90
+ <label><i class="fas fa-phone"></i> Phone No</label>
91
+ <input type="text" id="profilePhone" placeholder="Enter phone number" disabled>
92
+ <span class="edit-icon" onclick="enableEdit('profilePhone')">
93
+ <img src="/assets/editicon.png" alt="Edit" class="edit-img"></span>
94
+ </div>
95
+
96
+ <div class="profile-field">
97
+ <label><i class="fas fa-map-marker-alt"></i> Address</label>
98
+ <input type="text" id="profileAddress" placeholder="Enter address" disabled>
99
+ <span class="edit-icon" onclick="enableEdit('profileAddress')">
100
+ <img src="/assets/editicon.png" alt="Edit" class="edit-img">
101
+ </span>
102
+ </div>
103
+
104
+ <div class="profile-field">
105
+ <label><i class="fas fa-briefcase"></i> Profession</label>
106
+ <input type="text" id="profileProfession" placeholder="Enter profession" disabled>
107
+ <span class="edit-icon" onclick="enableEdit('profileProfession')">
108
+ <img src="/assets/editicon.png" alt="Edit" class="edit-img"></span>
109
+ </div>
110
+ </div>
111
+
112
+
113
+
114
+ <div class="tab-content" id="activity-tab">
115
+ <div class="activity-summary">
116
+ <div class="activity-stat">
117
+ <i class="fas fa-search"></i>
118
+ <span class="stat-value">32</span>
119
+ <span class="stat-label">Searches</span>
120
+ </div>
121
+ </div>
122
+
123
+
124
+
125
+ <h3 class="section-title">Recent Activity</h3>
126
+ <div class="timeline">
127
+ <div class="timeline-item">
128
+ <div class="timeline-dot"></div>
129
+ <div class="timeline-content">
130
+ <h4>Research on Neural Networks</h4>
131
+ <p>You searched for papers on neural networks in medicine</p>
132
+ <span class="timeline-date"><i class="fas fa-clock"></i> 2 days ago</span>
133
+ </div>
134
+ </div>
135
+ <div class="timeline-item">
136
+ <div class="timeline-dot"></div>
137
+ <div class="timeline-content">
138
+ <h4>Saved Article</h4>
139
+ <p>You saved "Emerging Trends in AI Ethics"</p>
140
+ <span class="timeline-date"><i class="fas fa-clock"></i> 1 week ago</span>
141
+ </div>
142
+ </div>
143
+ <div class="timeline-item">
144
+ <div class="timeline-dot"></div>
145
+ <div class="timeline-content">
146
+ <h4>Profile Updated</h4>
147
+ <p>You updated your professional information</p>
148
+ <span class="timeline-date"><i class="fas fa-clock"></i> 2 weeks ago</span>
149
+ </div>
150
+ </div>
151
+ </div>
152
+ </div>
153
+
154
+ <div class="action-buttons">
155
+ <button class="save-btn" id="saveBtn"><i class="fas fa-save"></i> Save Changes</button>
156
+ <button class="logout-btn" id="logoutBtn"><i class="fas fa-sign-out-alt"></i> Logout</button>
157
+ </div>
158
+ </div>
159
+ </div>
160
+
161
+
162
+
163
+
164
+ <section class="hero">
165
+ <div class="orb"></div>
166
+ <div class="orb"></div>
167
+ <div class="orb"></div>
168
+ <h2> Charting Research Trajectories and
169
+ Academic Linkages</h2>
170
+ <div class="liquid-container">
171
+ <div class="liquid-word active" id="word1">Innovative</div>
172
+ <div class="liquid-word" id="word2">Dynamic</div>
173
+ <div class="liquid-word" id="word3">Effortless</div>
174
+ <div class="splash-container" id="splashContainer"></div>
175
+ </div>
176
+ <div class="search-container">
177
+ <div class="searchBox">
178
+ <input id="searchy" class="searchInput"type="text" name="" placeholder="Search">
179
+ <div class="filter-container">
180
+ <select id="year-filter" class="year-filter">
181
+ <option value="">Year</option>
182
+ </select>
183
+ </div>
184
+ <img id="searchbtn" src="/assets/searchy.png" >
185
+ </div>
186
+
187
+ <!-- <button id="searchbtn">Search </button> -->
188
+ <button id="nextBtn" style="display: none;" >Next</button>
189
+ </div>
190
+ <div class="feature-selection-container">
191
+ <div class="feature-selection">
192
+ <div data-text="Trend Analysis" data-feature="trends" style="--r:-15;" class="glass">
193
+ <img src="/assets/trenzkj.png" alt="Trend Analysis" class="glass-icon" />
194
+ </div>
195
+
196
+
197
+ <div data-text="Citation Network" data-feature="citations" style="--r:-15;" class="glass">
198
+ <img src="/assets/icons8-hub-100.png" alt="Citation Network Analysis" class="glass-icon" />
199
+ </div>
200
+ <div data-text="Venue & Publisher Analysis" data-feature="venues" style="--r:-15;" class="glass">
201
+ <img src="/assets/trenzkj.png" alt="Venue & Publisher Analysis" class="glass-icon" />
202
+ </div>
203
+ </div>
204
+ <div class="selection-message" id="selectionMessage">
205
+ You've selected <span id="selectedFeature">Trend Analysis</span>. Click Search to continue.
206
+ </div>
207
+ </div>
208
+ </section>
209
+
210
+ <section id="interactive-book" class="interactive-book">
211
+ <h2 class="header-title">Features</h2>
212
+ <div class="carousel-container" id="carousel-container">
213
+ <!-- HTML Fix - Corrected slide 6 transform -->
214
+ <div class="carousel" id="carousel">
215
+ <div class="slide" style="transform: rotateY(0deg) translateZ(630px);">
216
+ <h4 class="card-title" style="color: aqua;font-family: 'Bungee Shade';">What Is Trend Analysis?</h4>
217
+ <p class="card-text" style="color:white; font:bolder">Trend Analysis via Topic Modeling in our project involves identifying and visualizing patterns in research papers for the given topic to uncover underlying themes and trends</p>
218
+ <h4 style="color: aqua;font-family: 'Bungee Shade';">What Does It Answers?</h4>
219
+ <p style="color: white;">Underlying themes thorugh clusters and themes and the system categorizes these topics into three distant importance.
220
+ - 🔥Hot topics ,🔄Neutraltopics ,💎Gap topics.
221
+ </p>
222
+ </div>
223
+
224
+ <div class="slide" style="transform: rotateY(60deg) translateZ(630px);">
225
+ <video src="/assets/d1.mp4" autoplay loop muted playsinline>
226
+ </div>
227
+
228
+ <div class="slide" style="transform: rotateY(120deg) translateZ(630px);">
229
+ <h4 style="color: aqua;font-family: 'Bungee Shade';">What is Citation Network</h4>
230
+ <p style="color: white;">This network maps relationships between research papers based on citations,
231
+ revealing influential works, clusters of related studies, and knowledge flow within a field.<br></p>
232
+ <h4 style="color: aqua;font-family: 'Bungee Shade';">What Does It Answers?</h4>
233
+ <ul style="color: white;">
234
+ <li>Identify Main Papers</li>
235
+ <li>Indentify Bridge Papers</li>
236
+ <li>Filter Reference And Cited Papers</li>
237
+ </ul>
238
+ </div>
239
+
240
+ <div class="slide" style="transform: rotateY(180deg) translateZ(630px);">
241
+ <video src="/assets/d2.mp4" autoplay loop muted playsinline>
242
+ </div>
243
+
244
+ <div class="slide" style="transform: rotateY(240deg) translateZ(630px);">
245
+ <h4 style="color: aqua;font-family: 'Bungee Shade';">What is Host/Publisher Analysis</h4>
246
+ <p style="color: white;">Venue and publisher analysis identifies key players in the research landscape,
247
+ such as institutions and journals, by visualizing clusters of host organizations or
248
+ publication venues based on their publication patterns and influence.
249
+ </p>
250
+ <h4 style="color: aqua;font-family: 'Bungee Shade';">What Does It Answers?</h4>
251
+ <ul style="display: flex; gap: 20px; padding: 0; color:white;">
252
+ <li>Status of Open To Access papers</li>
253
+ <li>Types of papers</li>
254
+ <li>Host and Publication Patterns</li>
255
+ </ul>
256
+ </div>
257
+
258
+
259
+ <div class="slide" style="transform: rotateY(300deg) translateZ(630px);">
260
+ <video src="/assets/d3.mp4" autoplay loop muted playsinline>
261
+ </div>
262
+ </div>
263
+
264
+
265
+
266
+ <div class="pagination">
267
+ <div class="dot active" data-index="0"></div>
268
+ <div class="dot" data-index="1"></div>
269
+ <div class="dot" data-index="2"></div>
270
+ <div class="dot" data-index="3"></div>
271
+ <div class="dot" data-index="4"></div>
272
+ <div class="dot" data-index="5"></div>
273
+ <div class="dot" data-index="5"></div>
274
+ </div>
275
+ </div>
276
+
277
+ <!-- Refined Open Book -->
278
+ <div class="book-container">
279
+ <div class="book">
280
+ <div class="book-left">
281
+ <div class="page-content">
282
+ <div class="page-line"></div>
283
+ <div class="page-line"></div>
284
+ <div class="page-line"></div>
285
+ </div>
286
+ <div class="page-fold"></div>
287
+ <div class="page-number page-number-left">1</div>
288
+ </div>
289
+
290
+ <div class="book-center"></div>
291
+
292
+ <div class="page" id="page1">
293
+ <div class="page-front">
294
+ <div class="page-content">
295
+ <div class="page-line"></div>
296
+ <div class="page-line"></div>
297
+ <div class="page-line"></div>
298
+ </div>
299
+ <div class="page-fold"></div>
300
+ <div class="page-number page-number-right">2</div>
301
+ </div>
302
+ <div class="page-back">
303
+ <div class="page-content">
304
+ <div class="page-line"></div>
305
+ <div class="page-line"></div>
306
+ <div class="page-line"></div>
307
+ </div>
308
+ <div class="page-fold"></div>
309
+ <div class="page-number page-number-left">3</div>
310
+ </div>
311
+ <div class="page-shadow"></div>
312
+ </div>
313
+
314
+ <div class="page" id="page2">
315
+ <div class="page-front">
316
+ <div class="page-content">
317
+ <div class="page-line"></div>
318
+ <div class="page-line"></div>
319
+ <div class="page-line"></div>
320
+ </div>
321
+ <div class="page-fold"></div>
322
+ <div class="page-number page-number-right">4</div>
323
+ </div>
324
+ <div class="page-back">
325
+ <div class="page-content">
326
+ <div class="page-line"></div>
327
+ <div class="page-line"></div>
328
+ <div class="page-line"></div>
329
+ </div>
330
+ <div class="page-fold"></div>
331
+ <div class="page-number page-number-left">5</div>
332
+ </div>
333
+ <div class="page-shadow"></div>
334
+ </div>
335
+
336
+ <div class="page" id="page3">
337
+ <div class="page-front">
338
+ <div class="page-content">
339
+ <div class="page-line"></div>
340
+ <div class="page-line"></div>
341
+ <div class="page-line"></div>
342
+ </div>
343
+ <div class="page-fold"></div>
344
+ <div class="page-number page-number-right">6</div>
345
+ </div>
346
+ <div class="page-back">
347
+ <div class="page-content">
348
+ <div class="page-line"></div>
349
+ <div class="page-line"></div>
350
+ <div class="page-line"></div>
351
+ </div>
352
+ <div class="page-fold"></div>
353
+ <div class="page-number page-number-left">7</div>
354
+ </div>
355
+ <div class="page-shadow"></div>
356
+ </div>
357
+
358
+ <div class="page" id="page4">
359
+ <div class="page-front">
360
+ <div class="page-content">
361
+ <div class="page-line"></div>
362
+ <div class="page-line"></div>
363
+ <div class="page-line"></div>
364
+ </div>
365
+ <div class="page-fold"></div>
366
+ <div class="page-number page-number-right">8</div>
367
+ </div>
368
+ <div class="page-back">
369
+ <div class="page-content">
370
+ <div class="page-line"></div>
371
+ <div class="page-line"></div>
372
+ <div class="page-line"></div>
373
+ </div>
374
+ <div class="page-fold"></div>
375
+ <div class="page-number page-number-left">9</div>
376
+ </div>
377
+ <div class="page-shadow"></div>
378
+ </div>
379
+
380
+ <div class="page" id="page5">
381
+ <div class="page-front">
382
+ <div class="page-content">
383
+ <div class="page-line"></div>
384
+ <div class="page-line"></div>
385
+ <div class="page-line"></div>
386
+ </div>
387
+ <div class="page-fold"></div>
388
+ <div class="page-number page-number-right">10</div>
389
+ </div>
390
+ <div class="page-back">
391
+ <div class="page-content">
392
+ <div class="page-line"></div>
393
+ <div class="page-line"></div>
394
+ <div class="page-line"></div>
395
+ </div>
396
+ <div class="page-fold"></div>
397
+ <div class="page-number page-number-left">11</div>
398
+ </div>
399
+ <div class="page-shadow"></div>
400
+ </div>
401
+
402
+
403
+ <div class="book-right">
404
+ <div class="page-content">
405
+ <div class="page-line"></div>
406
+ <div class="page-line"></div>
407
+ <div class="page-line"></div>
408
+ </div>
409
+ <div class="page-fold"></div>
410
+ <div class="page-number page-number-right">12</div>
411
+ </div>
412
+
413
+ <div class="book-shadow"></div>
414
+ </div>
415
+
416
+ <div class="navigation">
417
+ <button id="prevBtn">Previous</button>
418
+ <button id="nextBtn2">Next</button>
419
+ </div>
420
+ </div>
421
+ </section>
422
+
423
+ <section id="architecture" class="architecture">
424
+ <div class="architecture-description">
425
+ <h3>Our Innovative Research Architecture</h3>
426
+ <p>PaperLens architecture integrates data scraping from Semantic API and OpenAlex, followed by robust data cleaning, to enable three key features ,Trend Analysis, Citation Networks, and Host/venue insights through interactive, user-friendly dashboards.</p>
427
+
428
+ <div class="architecture-features">
429
+ <div class="architecture-feature">
430
+ <h4 style="text-align: center;">Dynamic Data Collection</h4>
431
+ <p>Continuously harvests research papers from IEEE, ArXiv, PMC, JSTOR, and PLOS databases by using semantic scholar and openAlex api's to ensure comprehensive coverage across disciplines. Say no to manual data loading.</p>
432
+ </div>
433
+ <div class="architecture-feature">
434
+ <h4 style="text-align: center;">Integrated Solution</h4>
435
+ <p>PaperLens integrates advanced science mapping features like trend analysis and citation networks into an intuitive platform, bridging gaps left by traditional tools that often require complex configurations or lack interactivity.</p>
436
+ </div>
437
+ <div class="architecture-feature">
438
+ <h4 style="text-align: center;">Ease Of Use </h4>
439
+ <p>Designed for simplicity, our tool allows users to type and search effortlessly, eliminating the need to learn heavy concepts, unlike many existing science mapping tools that demand extensive expertise for basic tasks.</p>
440
+ </div>
441
+ </div>
442
+ </div>
443
+ <div class="container">
444
+ <h2 style="margin-top: 30px;font-family: 'Bungee Shade', cursive;color:aqua">PaperLens</h2>
445
+ <svg class="paths-container">
446
+ </svg>
447
+ <div class="center-box">
448
+ <div class="settings-icon-wrapper">
449
+ <img src="/assets/setting.png" alt="Settings">
450
+ </div>
451
+ </div>
452
+ </div>
453
+ </section>
454
+ <section id="contacts" class="contacts">
455
+ <iframe src="/contact" frameborder="1" width="100%" height="800px" style="border: 8px solid #333; border-radius: 8px; margin: 20px 0;"></iframe>
456
+ </section>
457
+
458
+ <section id="feedback" class="feedback">
459
+ <iframe src="/feedback" frameborder="1" width="100%" height="1300px" style="border: 8px solid #333; border-radius: 8px; "></iframe>
460
+ </section>
461
+
462
+ <footer>
463
+ <p>&copy; 2025 PaperLens. All rights reserved.</p>
464
+ </footer>
465
+
466
+
467
+ <script src="https://www.gstatic.com/firebasejs/9.0.2/firebase-app-compat.js"></script>
468
+ <script src="https://www.gstatic.com/firebasejs/9.0.2/firebase-auth-compat.js"></script>
469
+ <script src="https://www.gstatic.com/firebasejs/9.0.2/firebase-firestore-compat.js"></script>
470
+ <script type="module" src="/static/js/home.js"></script>
471
+ <script type="module" src="/static/js/login.js"></script>
472
+ <script type="module" src="/static/js/useractivity.js"></script>
473
+ </body>
474
+
475
+ </html>
templates/loginpage.html ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Login & Signup</title>
7
+ <link rel="stylesheet" href="/static/css/loginstyle.css">
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
9
+ </head>
10
+ <body>
11
+ <div class="paper-fall"></div>
12
+ <div class="wrapper">
13
+ <div class="form-container sign-up" id="seperate">
14
+ <form action="#">
15
+ <h2>sign up</h2>
16
+ <div class="form-group">
17
+ <input type="text" required>
18
+ <i class="fas fa-user"></i>
19
+ <label for="">name</label>
20
+ </div>
21
+ <div class="form-group">
22
+ <input type="email" required>
23
+ <i class="fas fa-at"></i>
24
+ <label for="">email</label>
25
+ </div>
26
+ <div class="form-group">
27
+ <input type="password" required>
28
+ <i class="fas fa-lock"></i>
29
+ <label for="">password</label>
30
+ </div>
31
+ <div class="form-group">
32
+ <input type="password" required>
33
+ <i class="fas fa-lock"></i>
34
+ <label for="">confirm password</label>
35
+ </div>
36
+ <button type="submit" class="btn">sign up</button>
37
+ <div class="link">
38
+ <p style="color: white;">You already have an account?<a href="#" class="signin-link"> sign in</a></p>
39
+ </div>
40
+ </form>
41
+ </div>
42
+ <div class="form-container sign-in">
43
+ <form>
44
+ <h2>login</h2>
45
+ <div class="form-group">
46
+ <input type="email" required>
47
+ <i class="fas fa-at"></i>
48
+ <label for="">email</label>
49
+ </div>
50
+ <div class="form-group">
51
+ <input type="password" required>
52
+ <i class="fas fa-lock"></i>
53
+ <label for="">password</label>
54
+ </div>
55
+ <div class="forgot-pass">
56
+ <a href="#">forgot password?</a>
57
+ </div>
58
+ <button type="submit" class="btn">login</button>
59
+ <div class="link">
60
+ <p style="color: white;">Don't have an account?<a href="#" class="signup-link"> sign up</a></p>
61
+ </div>
62
+ </form>
63
+ </div>
64
+ <div class="form-container forgot-password">
65
+ <form action="#">
66
+ <h2>forgot password</h2>
67
+ <div class="form-group">
68
+ <input type="email" required>
69
+ <i class="fas fa-at"></i>
70
+ <label for="">email</label>
71
+ </div>
72
+ <button type="submit" class="btn">submit</button>
73
+ <div class="link">
74
+ <p style="color: white;">Go to Login Page ? <a href="#" class="forget-link">login</a></p>
75
+ </div>
76
+ </form>
77
+ </div>
78
+ </div>
79
+
80
+ <!-- Firebase Scripts -->
81
+ <script src="https://www.gstatic.com/firebasejs/9.0.2/firebase-app-compat.js"></script>
82
+ <script src="https://www.gstatic.com/firebasejs/9.0.2/firebase-auth-compat.js"></script>
83
+ <script src="https://www.gstatic.com/firebasejs/9.0.2/firebase-firestore-compat.js"></script>
84
+ <script src="/static/js/login.js"></script>
85
+ </body>
86
+ </html>
venuAnalysis.py ADDED
@@ -0,0 +1,837 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import plotly.express as px
3
+ import plotly.graph_objects as go
4
+ from dash import Dash, dcc, html, Input, Output, State
5
+ import numpy as np
6
+ import random
7
+ import math
8
+ from collections import defaultdict
9
+ import colorsys
10
+ from fastapi import HTTPException
11
+ from pydantic import BaseModel
12
+ import threading
13
+ import webbrowser
14
+ import os
15
+ import psutil
16
+ import socket
17
+ from fastapi import HTTPException, APIRouter, Request
18
+ router = APIRouter()
19
+
20
+ # Global variables to track dashboard state
21
+ dashboard_port = 8050
22
+ dashboard_process = None
23
+
24
+ # MongoDB connection and data loader function
25
+ async def load_data_from_mongodb(userId, topic, year, request:Request):
26
+ query = {
27
+ "userId": userId,
28
+ "topic": topic,
29
+ "year": year
30
+ }
31
+ collection = request.app.state.collection2
32
+ document = await collection.find_one(query)
33
+ if not document:
34
+ raise ValueError(f"No data found for userId={userId}, topic={topic}, year={year}")
35
+ # Extract metadata and convert to DataFrame
36
+ metadata = document.get("metadata", [])
37
+ df = pd.DataFrame(metadata)
38
+ df['publication_date'] = pd.to_datetime(df['publication_date'])
39
+ return df
40
+
41
+ # Common functions (unchanged)
42
+ def filter_by_date_range(dataframe, start_idx, end_idx):
43
+ start_date = date_range[start_idx]
44
+ end_date = date_range[end_idx]
45
+ return dataframe[(dataframe['publication_date'] >= start_date) &
46
+ (dataframe['publication_date'] <= end_date)]
47
+
48
+ def generate_vibrant_colors(n):
49
+ base_colors = []
50
+ for i in range(n):
51
+ hue = (i / n) % 1.0
52
+ saturation = random.uniform(0.7, 0.9)
53
+ value = random.uniform(0.7, 0.9)
54
+ r, g, b = colorsys.hsv_to_rgb(hue, saturation, value)
55
+ vibrant_color = '#{:02x}{:02x}{:02x}'.format(
56
+ int(r * 255),
57
+ int(g * 255),
58
+ int(b * 255)
59
+ )
60
+ end_color_r = min(255, int(r * 255 * 1.1))
61
+ end_color_g = min(255, int(g * 255 * 1.1))
62
+ end_color_b = min(255, int(b * 255 * 1.1))
63
+ gradient_end = '#{:02x}{:02x}{:02x}'.format(end_color_r, end_color_g, end_color_b)
64
+ base_colors.append({
65
+ 'start': vibrant_color,
66
+ 'end': gradient_end
67
+ })
68
+ extended_colors = base_colors * math.ceil(n/10)
69
+ final_colors = []
70
+ for i in range(n):
71
+ color = extended_colors[i]
72
+ jitter = random.uniform(0.9, 1.1)
73
+ def jitter_color(hex_color):
74
+ r, g, b = [min(255, max(0, int(int(hex_color[j:j+2], 16) * jitter))) for j in (1, 3, 5)]
75
+ return f'rgba({r}, {g}, {b}, 0.9)'
76
+ final_colors.append({
77
+ 'start': jitter_color(color['start']),
78
+ 'end': jitter_color(color['end']).replace('0.9', '0.8')
79
+ })
80
+ return final_colors
81
+
82
+ # Knowledge map creator function (unchanged)
83
+ def create_knowledge_map(filtered_df, view_type='host'):
84
+ color_palette = {
85
+ 'background': '#1E1E1E', # Dark background (almost black)
86
+ 'card_bg': '#1A2238', # Bluish-black for cards (from your image)
87
+ 'accent1': '#FF6A3D', # Orange for headings (keeping from original)
88
+ 'accent2': '#4ECCA3', # Keeping teal for secondary elements
89
+ 'accent3': '#9D84B7', # Keeping lavender for tertiary elements
90
+ 'text_light': '#FFFFFF', # White text
91
+ 'text_dark': '#E0E0E0', # Light grey text for dark backgrounds
92
+ }
93
+
94
+ if view_type == 'host':
95
+ group_col = 'host_organization_name'
96
+ id_col = 'host_organization_id'
97
+ title = "Host Organization Clusters"
98
+ else:
99
+ group_col = 'venue'
100
+ id_col = 'venue_id'
101
+ title = "Publication Venue Clusters"
102
+ summary = filtered_df.groupby(group_col).agg(
103
+ paper_count=('id', 'count'),
104
+ is_oa=('is_oa', 'mean'),
105
+ oa_status=('oa_status', lambda x: x.mode()[0] if not x.mode().empty else None),
106
+ entity_id=(id_col, 'first')
107
+ ).reset_index()
108
+ paper_count_groups = defaultdict(list)
109
+ for _, row in summary.iterrows():
110
+ paper_count_groups[row['paper_count']].append(row)
111
+ knowledge_map_fig = go.Figure()
112
+ sorted_counts = sorted(paper_count_groups.keys(), reverse=True)
113
+ vibrant_colors = generate_vibrant_colors(len(sorted_counts))
114
+ golden_angle = np.pi * (3 - np.sqrt(5))
115
+ spiral_coef = 150
116
+ cluster_metadata = {}
117
+ max_x, max_y = 500, 500
118
+ for i, count in enumerate(sorted_counts):
119
+ radius = np.sqrt(i) * spiral_coef
120
+ theta = golden_angle * i
121
+ cluster_x, cluster_y = radius * np.cos(theta), radius * np.sin(theta)
122
+ label_offset_angle = theta + np.pi/4
123
+ label_offset_distance = 80 + 4 * np.sqrt(len(paper_count_groups[count]))
124
+ label_x = cluster_x + label_offset_distance * np.cos(label_offset_angle)
125
+ label_y = cluster_y + label_offset_distance * np.sin(label_offset_angle)
126
+ cluster_metadata[count] = {
127
+ 'center_x': cluster_x,
128
+ 'center_y': cluster_y,
129
+ 'entities': paper_count_groups[count],
130
+ 'color': vibrant_colors[i]
131
+ }
132
+ entities = paper_count_groups[count]
133
+ num_entities = len(entities)
134
+ cluster_size = min(200, max(80, 40 + 8 * np.sqrt(num_entities)))
135
+ color = vibrant_colors[i]
136
+ knowledge_map_fig.add_shape(
137
+ type="circle",
138
+ x0=cluster_x - cluster_size/2, y0=cluster_y - cluster_size/2,
139
+ x1=cluster_x + cluster_size/2, y1=cluster_y + cluster_size/2,
140
+ fillcolor=color['end'].replace("0.8", "0.15"),
141
+ line=dict(color=color['start'], width=1.5),
142
+ opacity=0.7
143
+ )
144
+ knowledge_map_fig.add_trace(go.Scatter(
145
+ x=[cluster_x], y=[cluster_y],
146
+ mode='markers',
147
+ marker=dict(size=cluster_size, color=color['start'], opacity=0.3),
148
+ customdata=[[count, "cluster"]],
149
+ hoverinfo='skip'
150
+ ))
151
+ knowledge_map_fig.add_trace(go.Scatter(
152
+ x=[cluster_x, label_x], y=[cluster_y, label_y],
153
+ mode='lines',
154
+ line=dict(color=color['start'], width=1, dash='dot'),
155
+ hoverinfo='skip'
156
+ ))
157
+ knowledge_map_fig.add_annotation(
158
+ x=label_x, y=label_y,
159
+ text=f"{count} papers<br>{num_entities} {'orgs' if view_type == 'host' else 'venues'}",
160
+ showarrow=False,
161
+ font=dict(size=11, color='white'),
162
+ bgcolor=color['start'],
163
+ bordercolor='white',
164
+ borderwidth=1,
165
+ opacity=0.9
166
+ )
167
+ entities_sorted = sorted(entities, key=lambda x: x[group_col])
168
+ inner_spiral_coef = 0.4
169
+ for j, entity_data in enumerate(entities_sorted):
170
+ spiral_radius = np.sqrt(j) * cluster_size * inner_spiral_coef / np.sqrt(num_entities + 1)
171
+ spiral_angle = golden_angle * j
172
+ jitter_radius = random.uniform(0.9, 1.1) * spiral_radius
173
+ jitter_angle = spiral_angle + random.uniform(-0.1, 0.1)
174
+ entity_x = cluster_x + jitter_radius * np.cos(jitter_angle)
175
+ entity_y = cluster_y + jitter_radius * np.sin(jitter_angle)
176
+ node_size = min(18, max(8, np.sqrt(entity_data['paper_count']) * 1.5))
177
+ knowledge_map_fig.add_trace(go.Scatter(
178
+ x=[entity_x], y=[entity_y],
179
+ mode='markers',
180
+ marker=dict(
181
+ size=node_size,
182
+ color=color['start'],
183
+ line=dict(color='rgba(255, 255, 255, 0.9)', width=1.5)
184
+ ),
185
+ customdata=[[
186
+ entity_data[group_col],
187
+ entity_data['paper_count'],
188
+ entity_data['is_oa'],
189
+ entity_data['entity_id'],
190
+ count,
191
+ "entity"
192
+ ]],
193
+ hovertemplate=(
194
+ f"<b>{entity_data[group_col]}</b><br>"
195
+ f"Papers: {entity_data['paper_count']}<br>"
196
+ f"Open Access: {entity_data['is_oa']:.1%}<extra></extra>"
197
+ )
198
+ ))
199
+ max_x = max([abs(cluster['center_x']) for cluster in cluster_metadata.values()]) + 150 if cluster_metadata else 500
200
+ max_y = max([abs(cluster['center_y']) for cluster in cluster_metadata.values()]) + 150 if cluster_metadata else 500
201
+ # Update knowledge_map_fig layout
202
+ knowledge_map_fig.update_layout(
203
+ title=dict(
204
+ text=title,
205
+ font=dict(size=22, family='"Poppins", sans-serif', color=color_palette['accent1']) # Orange title
206
+ ),
207
+ plot_bgcolor='rgba(26, 34, 56, 1)', # Bluish-black background
208
+ paper_bgcolor='rgba(26, 34, 56, 0.7)',
209
+ xaxis=dict(range=[-max(700, max_x), max(700, max_x)], showticklabels=False, showgrid=False),
210
+ yaxis=dict(range=[-max(500, max_y), max(500, max_y)], showticklabels=False, showgrid=False),
211
+ margin=dict(l=10, r=10, t=60, b=10),
212
+ height=700,
213
+ hovermode='closest',
214
+ showlegend=False,
215
+ font=dict(family='"Poppins", sans-serif', color=color_palette['text_light']), # Light text
216
+ )
217
+ return knowledge_map_fig, cluster_metadata
218
+
219
+ # Other chart functions (unchanged)
220
+ def create_oa_pie_fig(filtered_df):
221
+ color_palette = {
222
+ 'background': '#1A2238', # Dark blue background
223
+ 'card_bg': '#1A2238', # Changed to match the other chart
224
+ 'accent1': '#FF6A3D', # Vibrant orange for highlights
225
+ 'accent2': '#4ECCA3', # Teal for secondary elements
226
+ 'accent3': '#9D84B7', # Lavender for tertiary elements
227
+ 'text_light': '#FFFFFF', # White text
228
+ 'text_dark': '#FFFFFF', # Changed to white for better contrast
229
+ }
230
+
231
+ fig = px.pie(
232
+ filtered_df, names='is_oa', title="Overall Open Access Status",
233
+ labels={True: "Open Access", False: "Not Open Access"},
234
+ color_discrete_sequence=[color_palette['accent2'], color_palette['accent1']]
235
+ )
236
+
237
+ fig.update_traces(
238
+ textinfo='label+percent',
239
+ textfont=dict(size=14, family='"Poppins", sans-serif'),
240
+ marker=dict(line=dict(color='#1A2238', width=2)) # Match background color
241
+ )
242
+
243
+ fig.update_layout(
244
+ title=dict(
245
+ text="Overall Open Access Status",
246
+ font=dict(size=18, family='"Poppins", sans-serif', color=color_palette['accent1']) # Orange title
247
+ ),
248
+ font=dict(family='"Poppins", sans-serif', color=color_palette['text_light']),
249
+ paper_bgcolor=color_palette['background'], # Dark background
250
+ plot_bgcolor=color_palette['background'], # Dark background
251
+ margin=dict(t=50, b=20, l=20, r=20),
252
+ legend=dict(
253
+ orientation="h",
254
+ yanchor="bottom",
255
+ y=-0.2,
256
+ xanchor="center",
257
+ x=0.5,
258
+ font=dict(size=12, color=color_palette['text_light'])
259
+ )
260
+ )
261
+
262
+ return fig
263
+ def create_oa_status_pie_fig(filtered_df):
264
+ custom_colors = [
265
+ "#9D84B7",
266
+ '#4DADFF',
267
+ '#FFD166',
268
+ '#06D6A0',
269
+ '#EF476F'
270
+ ]
271
+ fig = px.pie(
272
+ filtered_df,
273
+ names='oa_status',
274
+ title="Open Access Status Distribution",
275
+ color_discrete_sequence=custom_colors
276
+ )
277
+ fig.update_traces(
278
+ textinfo='label+percent',
279
+ insidetextorientation='radial',
280
+ textfont=dict(size=14, family='"Poppins", sans-serif'),
281
+ marker=dict(line=dict(color='#FFFFFF', width=2))
282
+ )
283
+ fig.update_layout(
284
+ title=dict(
285
+ text="Open Access Status Distribution",
286
+ font=dict(size=18, family='"Poppins", sans-serif', color="#FF6A3D")
287
+ ),
288
+ font=dict(family='"Poppins", sans-serif', color='#FFFFFF'),
289
+ paper_bgcolor='#1A2238', # Bluish-black background
290
+ plot_bgcolor='#1A2238',
291
+ margin=dict(t=50, b=20, l=20, r=20),
292
+ legend=dict(
293
+ orientation="h",
294
+ yanchor="bottom",
295
+ y=-0.2,
296
+ xanchor="center",
297
+ x=0.5,
298
+ font=dict(size=12, color='#FFFFFF')
299
+ )
300
+ )
301
+ return fig
302
+ def create_type_bar_fig(filtered_df):
303
+ type_counts = filtered_df['type'].value_counts()
304
+ vibrant_colors = [
305
+ '#4361EE', '#3A0CA3', '#4CC9F0',
306
+ '#F72585', '#7209B7', '#B5179E',
307
+ '#480CA8', '#560BAD', '#F77F00'
308
+ ]
309
+ fig = px.bar(
310
+ type_counts,
311
+ title="Publication Types",
312
+ labels={'value': 'Count', 'index': 'Type'},
313
+ color=type_counts.index,
314
+ color_discrete_sequence=vibrant_colors[:len(type_counts)]
315
+ )
316
+ fig.update_layout(
317
+ title=dict(
318
+ text="Publication Types",
319
+ font=dict(size=20, family='"Poppins", sans-serif', color="#FF6A3D") # Larger font size
320
+ ),
321
+ xaxis_title="Type",
322
+ yaxis_title="Count",
323
+ font=dict(family='"Poppins", sans-serif', color="#FFFFFF", size=14), # Increased font size
324
+ paper_bgcolor='#1A2238', # Consistent dark background
325
+ plot_bgcolor='#1A2238', # Consistent dark background
326
+ margin=dict(t=70, b=60, l=60, r=40), # Increased margins
327
+ xaxis=dict(
328
+ tickfont=dict(size=14, color="#FFFFFF"), # Increased tick font size
329
+ tickangle=-45,
330
+ gridcolor='rgba(255, 255, 255, 0.1)' # Lighter grid lines
331
+ ),
332
+ yaxis=dict(
333
+ tickfont=dict(size=14, color="#FFFFFF"), # Increased tick font size
334
+ gridcolor='rgba(255, 255, 255, 0.1)' # Lighter grid lines
335
+ ),
336
+ bargap=0.3, # Increased bar gap
337
+ )
338
+ fig.update_traces(
339
+ marker_line_width=1,
340
+ marker_line_color='rgba(0, 0, 0, 0.5)',
341
+ opacity=0.9,
342
+ hovertemplate='%{y} publications<extra></extra>',
343
+ texttemplate='%{y}', # Add text labels
344
+ textposition='outside', # Position labels outside bars
345
+ textfont=dict(size=14, color='white') # Text label formatting
346
+ )
347
+ return fig
348
+
349
+ # Function to check if port is in use
350
+ def is_port_in_use(port):
351
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
352
+ return s.connect_ex(('localhost', port)) == 0
353
+
354
+ # Function to find a free port
355
+ def find_free_port(start_port=8050):
356
+ port = start_port
357
+ while is_port_in_use(port):
358
+ port += 1
359
+ return port
360
+
361
+ # Function to shutdown any existing dashboard
362
+ def shutdown_existing_dashboard():
363
+ global dashboard_process
364
+
365
+ # First, check if our port is in use
366
+ if is_port_in_use(dashboard_port):
367
+ try:
368
+ # Kill processes using the port
369
+ for proc in psutil.process_iter(['pid', 'name', 'connections']):
370
+ try:
371
+ for conn in proc.connections():
372
+ if conn.laddr.port == dashboard_port:
373
+ print(f"Terminating process {proc.pid} using port {dashboard_port}")
374
+ proc.terminate()
375
+ proc.wait(timeout=3)
376
+ except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
377
+ pass
378
+ except Exception as e:
379
+ print(f"Error freeing port {dashboard_port}: {e}")
380
+
381
+ # If we're tracking a dashboard process, try to terminate it
382
+ if dashboard_process is not None:
383
+ try:
384
+ # Kill the process if it's still running
385
+ if dashboard_process.is_alive():
386
+ parent = psutil.Process(os.getpid())
387
+ children = parent.children(recursive=True)
388
+ for process in children:
389
+ try:
390
+ process.terminate()
391
+ except:
392
+ pass
393
+ dashboard_process = None
394
+ except Exception as e:
395
+ print(f"Error terminating dashboard process: {e}")
396
+ dashboard_process = None # Reset the reference anyway
397
+
398
+ # Pydantic model for request validation
399
+ class DashboardRequest(BaseModel):
400
+ userId: str
401
+ topic: str
402
+ year: int
403
+
404
+ @router.post("/load_and_display_dashboard/")
405
+ async def load_and_display_dashboard(request: DashboardRequest, req:Request):
406
+ global dashboard_process, dashboard_port
407
+
408
+ # Make sure any existing dashboard is shut down
409
+ shutdown_existing_dashboard()
410
+
411
+ # Find a free port
412
+ dashboard_port = find_free_port()
413
+
414
+ try:
415
+ # Load data from MongoDB
416
+ df = await load_data_from_mongodb(request.userId, request.topic, request.year, req)
417
+
418
+ # Get date range for the slider
419
+ global min_date, max_date, date_range, date_marks
420
+ min_date = df['publication_date'].min()
421
+ max_date = df['publication_date'].max()
422
+ date_range = pd.date_range(start=min_date, end=max_date, freq='MS')
423
+ date_marks = {i: date.strftime('%b %Y') for i, date in enumerate(date_range)}
424
+
425
+ # Function to create and run the dashboard
426
+ def create_and_run_dashboard():
427
+ # Create a new app instance
428
+ app = Dash(__name__, suppress_callback_exceptions=True)
429
+ app.cluster_metadata = {}
430
+ color_palette = {
431
+ 'background': '#1A2238', # Dark blue background
432
+ 'card_bg': '#F8F8FF', # Off-white for cards
433
+ 'accent1': '#FF6A3D', # Vibrant orange for highlights
434
+ 'accent2': '#4ECCA3', # Teal for secondary elements
435
+ 'accent3': '#9D84B7', # Lavender for tertiary elements
436
+ 'text_light': '#FFFFFF', # White text
437
+ 'text_dark': '#2D3748', # Dark gray text
438
+ }
439
+
440
+ # Define modern styling for containers
441
+ container_style = {
442
+ 'padding': '5px',
443
+ 'backgroundColor': color_palette['text_dark'],
444
+ 'borderRadius': '12px',
445
+ 'boxShadow': '0 4px 12px rgba(0, 0, 0, 0.15)',
446
+ 'marginBottom': '25px',
447
+ 'border': f'1px solid rgba(255, 255, 255, 0.2)',
448
+
449
+ }
450
+
451
+ hidden_style = {**container_style, 'display': 'none'}
452
+ visible_style = {**container_style}
453
+
454
+ # Create a modern, attractive layout
455
+ app.layout = html.Div([
456
+ # Header section with gradient background
457
+ html.Div([
458
+ html.H1(request.topic.capitalize() + " Analytics Dashboard", style={
459
+ 'textAlign': 'center',
460
+ 'marginBottom': '10px',
461
+ 'color': color_palette['accent1'],
462
+ 'fontSize': '2.5rem',
463
+ 'fontWeight': '700',
464
+ 'letterSpacing': '0.5px',
465
+ }),
466
+ html.Div([
467
+ html.P("Research Publication Analysis & Knowledge Mapping", style={
468
+ 'textAlign': 'center',
469
+ 'color': color_palette['text_light'],
470
+ 'opacity': '0.8',
471
+ 'fontSize': '1.2rem',
472
+ 'marginTop': '0',
473
+ })
474
+ ])
475
+ ], style={
476
+ 'background': f'linear-gradient(135deg, {color_palette["background"]}, #364156)',
477
+ 'padding': '30px 20px',
478
+ 'borderRadius': '12px',
479
+ 'marginBottom': '25px',
480
+ 'boxShadow': '0 4px 20px rgba(0, 0, 0, 0.2)',
481
+ }),
482
+
483
+ # Controls section
484
+ html.Div([
485
+ html.Div([
486
+ html.Button(
487
+ id='view-toggle',
488
+ children='Switch to Venue View',
489
+ style={
490
+ 'padding': '12px 20px',
491
+ 'fontSize': '1rem',
492
+ 'borderRadius': '8px',
493
+ 'border': 'none',
494
+ 'backgroundColor': color_palette['accent1'],
495
+ 'color': 'white',
496
+ 'cursor': 'pointer',
497
+ 'boxShadow': '0 2px 5px rgba(0, 0, 0, 0.1)',
498
+ 'transition': 'all 0.3s ease',
499
+ 'marginRight': '20px',
500
+ 'fontWeight': '500',
501
+ }
502
+ ),
503
+ html.H3("Filter by Publication Date", style={
504
+ 'marginBottom': '15px',
505
+ 'color': color_palette['text_dark'],
506
+ 'fontSize': '1.3rem',
507
+ 'fontWeight': '600',
508
+ }),
509
+ ], style={'display': 'flex', 'alignItems': 'center', 'marginBottom': '15px'}),
510
+
511
+ dcc.RangeSlider(
512
+ id='date-slider',
513
+ min=0,
514
+ max=len(date_range) - 1,
515
+ value=[0, len(date_range) - 1],
516
+ marks=date_marks if len(date_marks) <= 12 else {
517
+ i: date_marks[i] for i in range(0, len(date_range), max(1, len(date_range) // 12))
518
+ },
519
+ step=1,
520
+ tooltip={"placement": "bottom", "always_visible": True},
521
+ updatemode='mouseup'
522
+ ),
523
+ html.Div(id='date-range-display', style={
524
+ 'textAlign': 'center',
525
+ 'marginTop': '12px',
526
+ 'fontSize': '1.1rem',
527
+ 'fontWeight': '500',
528
+ 'color': color_palette['accent1'],
529
+ })
530
+ ], style={**container_style, 'marginBottom': '25px'}),
531
+
532
+ # Knowledge map - main visualization
533
+ html.Div([
534
+ dcc.Graph(
535
+ id='knowledge-map',
536
+ style={'width': '100%', 'height': '700px'},
537
+ config={'scrollZoom': True, 'displayModeBar': True, 'responsive': True}
538
+ )
539
+ ], style={
540
+ **container_style,
541
+ 'height': '750px',
542
+ 'marginBottom': '25px',
543
+ 'background': f'linear-gradient(to bottom right, {color_palette["card_bg"]}, #F0F0F8)',
544
+ }),
545
+
546
+ # Details container - appears when clicking elements
547
+ html.Div([
548
+ html.H3(id='details-title', style={
549
+ 'marginBottom': '15px',
550
+ 'color': color_palette['accent1'],
551
+ 'fontSize': '1.4rem',
552
+ 'fontWeight': '600',
553
+ }),
554
+ html.Div(id='details-content', style={
555
+ 'maxHeight': '350px',
556
+ 'overflowY': 'auto',
557
+ 'padding': '10px',
558
+ 'borderRadius': '8px',
559
+ 'backgroundColor': 'rgba(255, 255, 255, 0.7)',
560
+ })
561
+ ], id='details-container', style=hidden_style),
562
+
563
+ # Charts in flex container
564
+ html.Div([
565
+ html.Div([
566
+ dcc.Graph(
567
+ id='oa-pie-chart',
568
+ style={'width': '100%', 'height': '350px'},
569
+ config={'displayModeBar': False, 'responsive': True}
570
+ )
571
+ ], style={
572
+ 'flex': 1,
573
+ **container_style,
574
+ 'margin': '0 10px',
575
+ 'height': '400px',
576
+ 'transition': 'transform 0.3s ease',
577
+ ':hover': {'transform': 'translateY(-5px)'},
578
+ }),
579
+ html.Div([
580
+ dcc.Graph(
581
+ id='oa-status-pie-chart',
582
+ style={'width': '100%', 'height': '350px'},
583
+ config={'displayModeBar': False, 'responsive': True}
584
+ )
585
+ ], style={
586
+ 'flex': 1,
587
+ **container_style,
588
+ 'margin': '0 10px',
589
+ 'height': '400px',
590
+ 'transition': 'transform 0.3s ease',
591
+ ':hover': {'transform': 'translateY(-5px)'},
592
+ })
593
+ ], style={'display': 'flex', 'marginBottom': '25px', 'height': '420px'}),
594
+
595
+ # Bar chart container
596
+ # Increase bar chart height and improve visibility
597
+ html.Div([
598
+ dcc.Graph(
599
+ id='type-bar-chart',
600
+ style={'width': '100%', 'height': '50vh'}, # Reduced from 60vh
601
+ config={'displayModeBar': False, 'responsive': True}
602
+ )
603
+ ], style={
604
+ **container_style,
605
+ 'height': '500px', # Decreased from 650px
606
+ 'background': 'rgba(26, 34, 56, 1)',
607
+ 'marginBottom': '10px', # Added smaller bottom margin
608
+ }),
609
+ # Store components for state
610
+ dcc.Store(id='filtered-df-info'),
611
+ dcc.Store(id='current-view', data='host'),
612
+ html.Div(id='load-trigger', children='trigger-initial-load', style={'display': 'none'})
613
+ ], style={
614
+ 'fontFamily': '"Poppins", "Segoe UI", Arial, sans-serif',
615
+ 'backgroundColor': '#121212', # Dark background
616
+ 'backgroundImage': 'none', # Remove gradient
617
+ 'padding': '30px',
618
+ 'maxWidth': '1800px',
619
+ 'margin': '0 auto',
620
+ 'minHeight': '100vh',
621
+ 'color': color_palette['text_light'],
622
+ 'paddingBottom': '10px',
623
+ })
624
+
625
+
626
+
627
+ @app.callback(
628
+ [Output('current-view', 'data'),
629
+ Output('view-toggle', 'children')],
630
+ [Input('view-toggle', 'n_clicks')],
631
+ [State('current-view', 'data')]
632
+ )
633
+ def toggle_view(n_clicks, current_view):
634
+ if not n_clicks:
635
+ return current_view, 'Switch to Venue View' if current_view == 'host' else 'Switch to Host View'
636
+ new_view = 'venue' if current_view == 'host' else 'host'
637
+ new_button_text = 'Switch to Host View' if new_view == 'venue' else 'Switch to Venue View'
638
+ return new_view, new_button_text
639
+
640
+ @app.callback(
641
+ Output('date-range-display', 'children'),
642
+ [Input('date-slider', 'value')]
643
+ )
644
+ def update_date_range_display(date_range_indices):
645
+ start_date = date_range[date_range_indices[0]]
646
+ end_date = date_range[date_range_indices[1]]
647
+ return f"Selected period: {start_date.strftime('%b %Y')} to {end_date.strftime('%b %Y')}"
648
+
649
+ @app.callback(
650
+ [Output('knowledge-map', 'figure'),
651
+ Output('oa-pie-chart', 'figure'),
652
+ Output('oa-status-pie-chart', 'figure'),
653
+ Output('type-bar-chart', 'figure'),
654
+ Output('filtered-df-info', 'data'),
655
+ Output('details-container', 'style')],
656
+ [Input('date-slider', 'value'),
657
+ Input('current-view', 'data'),
658
+ Input('load-trigger', 'children')] # Added trigger
659
+ )
660
+ def update_visualizations(date_range_indices, current_view, _):
661
+ filtered_df = filter_by_date_range(df, date_range_indices[0], date_range_indices[1])
662
+ knowledge_map_fig, cluster_metadata = create_knowledge_map(filtered_df, current_view)
663
+ app.cluster_metadata = cluster_metadata
664
+ filtered_info = {
665
+ 'start_idx': date_range_indices[0],
666
+ 'end_idx': date_range_indices[1],
667
+ 'start_date': date_range[date_range_indices[0]].strftime('%Y-%m-%d'),
668
+ 'end_date': date_range[date_range_indices[1]].strftime('%Y-%m-%d'),
669
+ 'record_count': len(filtered_df),
670
+ 'view_type': current_view
671
+ }
672
+ return (
673
+ knowledge_map_fig,
674
+ create_oa_pie_fig(filtered_df),
675
+ create_oa_status_pie_fig(filtered_df),
676
+ create_type_bar_fig(filtered_df),
677
+ filtered_info,
678
+ hidden_style
679
+ )
680
+
681
+ @app.callback(
682
+ [Output('details-container', 'style', allow_duplicate=True),
683
+ Output('details-title', 'children'),
684
+ Output('details-content', 'children')],
685
+ [Input('knowledge-map', 'clickData')],
686
+ [State('filtered-df-info', 'data')],
687
+ prevent_initial_call=True
688
+ )
689
+ def display_details(clickData, filtered_info):
690
+ if not clickData or not filtered_info:
691
+ return hidden_style, "", []
692
+ customdata = clickData['points'][0]['customdata']
693
+ view_type = filtered_info['view_type']
694
+ entity_type = "Organization" if view_type == 'host' else "Venue"
695
+ if len(customdata) >= 2 and customdata[-1] == "cluster":
696
+ count = customdata[0]
697
+ if count not in app.cluster_metadata:
698
+ return hidden_style, "", []
699
+ entities = app.cluster_metadata[count]['entities']
700
+ color = app.cluster_metadata[count]['color']['start']
701
+ table_header = [
702
+ html.Thead(html.Tr([
703
+ html.Th(f"{entity_type} Name", style={'padding': '8px'}),
704
+ html.Th(f"{entity_type} ID", style={'padding': '8px'}),
705
+ html.Th("Papers", style={'padding': '8px', 'textAlign': 'center'}),
706
+ html.Th("Open Access %", style={'padding': '8px', 'textAlign': 'center'})
707
+ ], style={'backgroundColor': color_palette['accent1'], 'color': 'white'}))
708
+ ]
709
+
710
+ # Update row styles
711
+ row_style = {'backgroundColor': '#232D42'} if i % 2 == 0 else {'backgroundColor': '#1A2238'}
712
+ rows = []
713
+ for i, entity in enumerate(sorted(entities, key=lambda x: x['paper_count'], reverse=True)):
714
+ row_style = {'backgroundColor': '#f9f9f9'} if i % 2 == 0 else {'backgroundColor': 'white'}
715
+ entity_name_link = html.A(
716
+ entity[f"{view_type}_organization_name" if view_type == 'host' else "venue"],
717
+ href=entity['entity_id'],
718
+ target="_blank",
719
+ style={'color': color, 'textDecoration': 'underline'}
720
+ )
721
+ entity_id_link = html.A(
722
+ entity['entity_id'].split('/')[-1],
723
+ href=entity['entity_id'],
724
+ target="_blank",
725
+ style={'color': color, 'textDecoration': 'underline'}
726
+ )
727
+ rows.append(html.Tr([
728
+ html.Td(entity_name_link, style={'padding': '8px'}),
729
+ html.Td(entity_id_link, style={'padding': '8px'}),
730
+ html.Td(entity['paper_count'], style={'padding': '8px', 'textAlign': 'center'}),
731
+ html.Td(f"{entity['is_oa']:.1%}", style={'padding': '8px', 'textAlign': 'center'})
732
+ ], style=row_style))
733
+ table = html.Table(table_header + [html.Tbody(rows)], style={
734
+ 'width': '100%',
735
+ 'borderCollapse': 'collapse',
736
+ 'boxShadow': '0 1px 3px rgba(0,0,0,0.1)'
737
+ })
738
+ return (
739
+ visible_style,
740
+ f"{entity_type}s with {count} papers",
741
+ [html.P(f"Showing {len(entities)} {entity_type.lower()}s during selected period"), table]
742
+ )
743
+ elif len(customdata) >= 6 and customdata[-1] == "entity":
744
+ entity_name = customdata[0]
745
+ entity_id = customdata[3]
746
+ cluster_count = customdata[4]
747
+ color = app.cluster_metadata[cluster_count]['color']['start']
748
+ if view_type == 'host':
749
+ entity_papers = df[df['host_organization_name'] == entity_name].copy()
750
+ else:
751
+ entity_papers = df[df['venue'] == entity_name].copy()
752
+ entity_papers = entity_papers[
753
+ (entity_papers['publication_date'] >= pd.to_datetime(filtered_info['start_date'])) &
754
+ (entity_papers['publication_date'] <= pd.to_datetime(filtered_info['end_date']))
755
+ ]
756
+ entity_name_link = html.A(
757
+ entity_name,
758
+ href=entity_id,
759
+ target="_blank",
760
+ style={'color': color, 'textDecoration': 'underline', 'fontSize': '1.2em'}
761
+ )
762
+ entity_id_link = html.A(
763
+ entity_id.split('/')[-1],
764
+ href=entity_id,
765
+ target="_blank",
766
+ style={'color': color, 'textDecoration': 'underline'}
767
+ )
768
+ header = [
769
+ html.Div([
770
+ html.Span("Name: ", style={'fontWeight': 'bold'}),
771
+ entity_name_link
772
+ ], style={'marginBottom': '10px'}),
773
+ html.Div([
774
+ html.Span("ID: ", style={'fontWeight': 'bold'}),
775
+ entity_id_link
776
+ ], style={'marginBottom': '10px'}),
777
+ html.Div([
778
+ html.Span(f"Papers: {len(entity_papers)}", style={'marginRight': '20px'}),
779
+ ], style={'marginBottom': '20px'})
780
+ ]
781
+ table_header = [
782
+ html.Thead(html.Tr([
783
+ html.Th("Paper ID", style={'padding': '8px'}),
784
+ html.Th("Type", style={'padding': '8px'}),
785
+ html.Th("OA Status", style={'padding': '8px', 'textAlign': 'center'}),
786
+ html.Th("Publication Date", style={'padding': '8px', 'textAlign': 'center'})
787
+ ], style={'backgroundColor': color, 'color': 'white'}))
788
+ ]
789
+ rows = []
790
+ for i, (_, paper) in enumerate(entity_papers.sort_values('publication_date', ascending=False).iterrows()):
791
+ row_style = {'backgroundColor': '#232D42'} if i % 2 == 0 else {'backgroundColor': '#1A2238'}
792
+ paper_link = html.A(
793
+ paper['id'],
794
+ href=paper['id'],
795
+ target="_blank",
796
+ style={'color': color, 'textDecoration': 'underline'}
797
+ )
798
+ rows.append(html.Tr([
799
+ html.Td(paper_link, style={'padding': '8px'}),
800
+ html.Td(paper['type'], style={'padding': '8px'}),
801
+ html.Td(paper['oa_status'], style={'padding': '8px', 'textAlign': 'center'}),
802
+ html.Td(paper['publication_date'].strftime('%Y-%m-%d'), style={'padding': '8px', 'textAlign': 'center'})
803
+ ], style=row_style))
804
+ table = html.Table(table_header + [html.Tbody(rows)], style={
805
+ 'width': '100%',
806
+ 'borderCollapse': 'collapse',
807
+ 'boxShadow': '0 1px 3px rgba(0,0,0,0.1)'
808
+ })
809
+ with open("dashboard.html", "w") as f:
810
+ f.write(app.index())
811
+ print("yup saved!!")
812
+ return visible_style, f"{entity_type} Papers", header + [table]
813
+ return hidden_style, "", []
814
+
815
+ # Start the Dash app
816
+ app.run_server(debug=False, port=dashboard_port, use_reloader=False)
817
+
818
+ # Run the dashboard in a separate process
819
+ dashboard_process = threading.Thread(target=create_and_run_dashboard)
820
+ dashboard_process.daemon = True
821
+ dashboard_process.start()
822
+
823
+ # Open the browser after a delay
824
+ def open_browser():
825
+ try:
826
+ webbrowser.open_new(f"http://127.0.0.1:{dashboard_port}/")
827
+ except:
828
+ pass
829
+
830
+ threading.Timer(1.5, open_browser).start()
831
+
832
+ return {"status": "success", "message": f"Dashboard loaded successfully on port {dashboard_port}."}
833
+
834
+ except Exception as e:
835
+ # Clean up in case of failure
836
+ shutdown_existing_dashboard()
837
+ raise HTTPException(status_code=400, detail=str(e))
venuedata.py ADDED
@@ -0,0 +1,211 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import HTTPException, APIRouter, Request
2
+ from pydantic import BaseModel
3
+ import requests
4
+ from time import sleep
5
+ import spacy
6
+ from pymongo import MongoClient
7
+
8
+ # Load SciSpacy model
9
+ nlp = spacy.load("en_core_sci_sm")
10
+
11
+ router = APIRouter()
12
+
13
+ # Pydantic model for request body
14
+ class SearchRequest(BaseModel):
15
+ topic: str
16
+ year: int
17
+ userId: str # Add userId to the request body
18
+
19
+
20
+
21
+ # Function to extract keywords from user topic
22
+ def extract_keywords(text):
23
+ doc = nlp(text.lower())
24
+ noun_chunks = [chunk.text.strip() for chunk in doc.noun_chunks]
25
+ individual_tokens = [
26
+ token.text.strip() for token in doc
27
+ if token.pos_ in ["NOUN", "VERB"] and not token.is_stop
28
+ ]
29
+ keywords = set(noun_chunks + individual_tokens)
30
+
31
+ cleaned_keywords = set()
32
+ for keyword in keywords:
33
+ if not any(keyword in chunk and keyword != chunk for chunk in noun_chunks):
34
+ cleaned_keywords.add(keyword)
35
+ return sorted(list(cleaned_keywords))
36
+
37
+ # Fetch works from OpenAlex based on refined topic
38
+ def fetch_works(refined_query, year, per_page=200):
39
+ OPENALEX_API_URL = f"https://api.openalex.org/works"
40
+ params = {
41
+ "filter": f"title_and_abstract.search:{refined_query},publication_year:{year}",
42
+ "per_page": per_page,
43
+ "cursor": "*"
44
+ }
45
+ all_results = []
46
+
47
+ while True:
48
+ response = requests.get(OPENALEX_API_URL, params=params)
49
+ if response.status_code != 200:
50
+ raise HTTPException(status_code=response.status_code, detail="Error fetching data from OpenAlex")
51
+
52
+ data = response.json()
53
+ all_results.extend(data.get("results", []))
54
+
55
+ next_cursor = data.get("meta", {}).get("next_cursor")
56
+ if next_cursor:
57
+ params["cursor"] = next_cursor
58
+ else:
59
+ break
60
+
61
+ sleep(0.2)
62
+
63
+ return all_results
64
+
65
+ # Batch fetch host organization details for sources
66
+ def batch_fetch_host_org_details(source_ids):
67
+ host_org_map = {}
68
+ batch_size = 100
69
+ for i in range(0, len(source_ids), batch_size):
70
+ batch = source_ids[i:i + batch_size]
71
+ openalex_ids = [src_id.split("/")[-1] for src_id in batch]
72
+ filter_query = "|".join(openalex_ids)
73
+ url = f"https://api.openalex.org/sources?filter=openalex_id:{filter_query}"
74
+ response = requests.get(url)
75
+ if response.status_code == 200:
76
+ sources = response.json().get("results", [])
77
+ for source in sources:
78
+ source_id = source.get("id")
79
+ # Handle the case where host_organization is a string (URL)
80
+ host_org_id = source.get("host_organization", "unknown")
81
+ host_org_name = source.get("host_organization_name", "unknown")
82
+
83
+ host_org_map[source_id] = {
84
+ "host_organization_name": host_org_name,
85
+ "host_organization_id": host_org_id
86
+ }
87
+ sleep(0.1)
88
+ else:
89
+ raise HTTPException(status_code=response.status_code, detail="Error fetching host organization details")
90
+ return host_org_map
91
+
92
+ # Extract metadata from works
93
+ def extract_metadata(works):
94
+ source_ids = list({
95
+ work["primary_location"]["source"]["id"]
96
+ for work in works
97
+ if work.get("primary_location") and work["primary_location"].get("source")
98
+ })
99
+
100
+ # Fetch host organization details (name and ID)
101
+ host_org_map = batch_fetch_host_org_details(source_ids)
102
+
103
+ metadata = []
104
+ for work in works:
105
+ primary_location = work.get("primary_location", {}) or {}
106
+ source = primary_location.get("source", {}) or {}
107
+
108
+ source_id = source.get("id")
109
+ host_org_details = host_org_map.get(source_id, {"host_organization_name": "Unknown", "host_organization_id": "Unknown"})
110
+
111
+ # Extract type field
112
+ work_type = (
113
+ work.get("type") or # Check the top-level "type" field
114
+ primary_location.get("type") or # Check "type" in primary_location
115
+ source.get("type") or # Check "type" in source
116
+ "unknown" # Fallback value
117
+ )
118
+
119
+ metadata.append({
120
+ "id": work.get("id"),
121
+ "is_oa": work.get("open_access", {}).get("is_oa", False),
122
+ "oa_status": work.get("open_access", {}).get("oa_status", "unknown"),
123
+ "venue": source.get("display_name", "unknown"),
124
+ "venue_id": source_id or "unknown",
125
+ "host_organization_name": host_org_details["host_organization_name"],
126
+ "host_organization_id": host_org_details["host_organization_id"],
127
+ "type": work_type,
128
+ "publication_date": work.get("publication_date", "unknown"),
129
+ })
130
+ return metadata
131
+
132
+ # Clean metadata by removing entries with unknown venue and host organization name
133
+ def clean_metadata(metadata):
134
+ initial_count = len(metadata)
135
+ cleaned_metadata = [
136
+ entry for entry in metadata
137
+ if not (entry["venue"] == "unknown" and entry["host_organization_name"] == "Unknown")
138
+ ]
139
+ final_count = len(cleaned_metadata)
140
+ print(f"Total papers before cleaning: {initial_count}")
141
+ print(f"Total papers after cleaning: {final_count}")
142
+ return cleaned_metadata
143
+
144
+ # Save metadata to MongoDB
145
+ async def save_to_mongodb(userId, topic, year, metadata,request:Request):
146
+ document = {
147
+ "userId": userId,
148
+ "topic": topic,
149
+ "year": year,
150
+ "metadata": metadata
151
+ }
152
+ collection = request.app.state.collection2
153
+ await collection.update_one(
154
+ {"userId": userId, "topic": topic, "year": year},
155
+ {"$set": document},
156
+ upsert=True
157
+ )
158
+ print(f"Data saved to MongoDB for userId: {userId}, topic: {topic}, year: {year}")
159
+
160
+ # FastAPI endpoint
161
+ @router.post("/search/")
162
+ async def search(data:SearchRequest,request: Request):
163
+ userId = data.userId
164
+ topic = data.topic
165
+ year = data.year
166
+
167
+ # Extract keywords and refine query
168
+ keywords = extract_keywords(topic)
169
+ refined_query = "+".join(keywords)
170
+ print(f"Using refined search query: {refined_query}")
171
+
172
+ # Fetch works from OpenAlex
173
+ works = fetch_works(refined_query, year)
174
+ if not works:
175
+ raise HTTPException(status_code=404, detail="No works found for the given query")
176
+
177
+ # Extract metadata
178
+ metadata = extract_metadata(works)
179
+
180
+ # Clean metadata
181
+ cleaned_metadata = clean_metadata(metadata)
182
+
183
+ # Save metadata to MongoDB
184
+ await save_to_mongodb(userId, topic, year, cleaned_metadata,request)
185
+
186
+ # Return metadata as JSON response
187
+ return {"message": f"Data saved to MongoDB for userId: {userId}, topic: {topic}, year: {year}", "metadata": cleaned_metadata}
188
+
189
+ @router.post("/check-data-exists-venue/")
190
+ async def check_data_exists(request_data: SearchRequest, request:Request):
191
+ # Create a query to check if the data exists
192
+ query = {
193
+ "userId": request_data.userId,
194
+ "topic": request_data.topic
195
+ }
196
+
197
+ # Add year to query if it's provided
198
+ if request_data.year:
199
+ query["year"] = request_data.year
200
+
201
+ collection = request.app.state.collection2
202
+ # Check if a document matching the query exists
203
+ document = await collection.find_one(query) # Await the async operation
204
+
205
+ # Return result
206
+ return {
207
+ "exists": document is not None,
208
+ "message": "Data found" if document else "Data not found"
209
+ }
210
+
211
+