ihaveaplan66 commited on
Commit
3eed4de
·
verified ·
1 Parent(s): 4a47976

Upload 10 files

Browse files
Files changed (10) hide show
  1. .gitignore +3 -0
  2. LICENSE +21 -0
  3. README.md +68 -11
  4. app.py +122 -0
  5. main.py +95 -0
  6. requirements.txt +8 -0
  7. static/script.js +32 -0
  8. static/style.css +117 -0
  9. templates/about.html +33 -0
  10. templates/index.html +57 -0
.gitignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ __pycache__/
2
+ .venv/
3
+ .idea/
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2025 ihaveaplan66
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
README.md CHANGED
@@ -1,11 +1,68 @@
1
- ---
2
- title: News Analyzer
3
- emoji: 🔥
4
- colorFrom: purple
5
- colorTo: purple
6
- sdk: docker
7
- pinned: false
8
- license: mit
9
- ---
10
-
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # NewsAnalyzer
2
+
3
+ [Live Demo](https://huggingface.co/spaces/YOUR_USERNAME/NewsAnalyzer)
4
+
5
+ NewsAnalyzer is a web application that analyzes news articles using natural language processing (NLP) techniques. It helps users quickly understand the sentiment, key topics, and overall trends in the latest news based on any search query.
6
+
7
+ ## Features
8
+
9
+ - Search for recent news articles using NewsAPI
10
+ - Perform sentiment analysis to determine whether each article is positive or negative
11
+ - Automatically categorize articles into topics such as business, technology, politics, and more
12
+ - Generate concise summaries for each article
13
+ - Extract the most frequently mentioned words and display them in a word cloud
14
+ - Visualize sentiment distribution and trending words in clear charts
15
+ - Cache recent searches for improved performance
16
+
17
+ ## Technologies
18
+
19
+ - **Flask** for backend logic and routing
20
+ - **Hugging Face Transformers** for NLP tasks:
21
+ - Sentiment analysis: [distilbert-base-uncased-finetuned-sst-2-english](https://huggingface.co/distilbert-base-uncased-finetuned-sst-2-english)
22
+ - Topic classification: [cardiffnlp/tweet-topic-21-multi](https://huggingface.co/cardiffnlp/tweet-topic-21-multi)
23
+ - Summarization: [facebook/bart-large-cnn](https://huggingface.co/facebook/bart-large-cnn)
24
+ - **NLTK** for tokenization and text preprocessing
25
+ - **Matplotlib** and **WordCloud** for visualizations
26
+ - **NewsAPI** for retrieving real-time news articles
27
+
28
+ ## Installation
29
+
30
+ 1. Clone the repository:
31
+ ```bash
32
+ git clone https://github.com/ihaveaplan66/news-analyzer.git
33
+ cd NewsAnalyzer
34
+ ```
35
+
36
+ 2. Create a virtual environment and activate it:
37
+ ```bash
38
+ python -m venv venv
39
+ source venv/bin/activate # Windows: venv\Scripts\activate
40
+ ```
41
+
42
+ 3. Install dependencies:
43
+ ```bash
44
+ pip install -r requirements.txt
45
+ ```
46
+
47
+ 4. Set your NewsAPI key in `app.py`:
48
+ ```python
49
+ api_key = "your_newsapi_key_here"
50
+ ```
51
+
52
+ 5. Run the application:
53
+ ```bash
54
+ python app.py
55
+ ```
56
+
57
+ 6. Open the application in your browser:
58
+ ```
59
+ http://localhost:5000
60
+ ```
61
+
62
+ ## License
63
+
64
+ This project is part of my personal portfolio and is provided under the MIT License.
65
+
66
+ ## Author
67
+
68
+ Created by Volodymyr Shereperov
app.py ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, render_template, request, Response, url_for
2
+ from main import analyze_news, extract_trending_words
3
+ import io
4
+ import time
5
+ import matplotlib.pyplot as plt
6
+ from wordcloud import WordCloud
7
+
8
+
9
+ app = Flask(__name__)
10
+ cache = {}
11
+ plt.switch_backend("Agg")
12
+
13
+ def get_cached_data(cache_key):
14
+ cached = cache.get(cache_key)
15
+ if cached and time.time() - cached["timestamp"] < 600:
16
+ print(f"Cache hit for {cache_key}")
17
+ return cached["results"], cached["trending_words"]
18
+ return None, None
19
+
20
+ @app.route("/", methods=["GET", "POST"])
21
+ def index():
22
+ results, trending_words = [], []
23
+ sentiment_chart = None
24
+ query = ""
25
+
26
+ if request.method == "POST":
27
+ query = request.form["query"]
28
+ num_articles = int(request.form["num_articles"])
29
+ api_key = "f80d8a6206cd472baeb21f04786b2626"
30
+
31
+ cache_key = f"{query}_{num_articles}"
32
+ results, trending_words = get_cached_data(cache_key)
33
+
34
+ if results is None:
35
+ print(f"No cache for {cache_key}, fetching new data...")
36
+ results = analyze_news(query, api_key, num_articles)
37
+ texts = [article["title"] + " " + article.get("summary", "") for article in results]
38
+ trending_words = extract_trending_words(texts)
39
+
40
+ cache[cache_key] = {
41
+ "results": results,
42
+ "trending_words": trending_words,
43
+ "timestamp": time.time()
44
+ }
45
+
46
+ if results:
47
+ sentiment_chart = url_for('sentiment_chart_route')
48
+
49
+ return render_template("index.html", results=results, sentiment_chart=sentiment_chart, query=query, trending_words=trending_words)
50
+
51
+ @app.route("/sentiment_chart")
52
+ def sentiment_chart_route():
53
+ if not cache:
54
+ return "No sentiment data", 404
55
+
56
+ last_query = list(cache.keys())[-1]
57
+ cached = cache.get(last_query)
58
+
59
+ if not cached:
60
+ return "No sentiment data", 404
61
+
62
+ results = cached["results"]
63
+ sentiments = [article["sentiment"] for article in results]
64
+
65
+ sentiment_counts = dict((x, sentiments.count(x)) for x in set(sentiments))
66
+
67
+ labels = list(sentiment_counts.keys())
68
+ values = list(sentiment_counts.values())
69
+
70
+ color_map = {
71
+ "POSITIVE": "#28a745",
72
+ "NEGATIVE": "#c82333"
73
+ }
74
+
75
+ colors = [color_map[label] for label in labels]
76
+ total = sum(values)
77
+
78
+ plt.figure(figsize=(3, 3))
79
+ plt.pie(values, autopct=lambda pct: f'{int(pct * total / 100)} ({pct:.1f}%)', startangle=140, colors=colors)
80
+ plt.axis('equal')
81
+
82
+ img = io.BytesIO()
83
+ plt.savefig(img, format="png", bbox_inches="tight", transparent = True)
84
+ plt.close()
85
+ img.seek(0)
86
+
87
+ return Response(img.getvalue(), mimetype="image/png")
88
+
89
+ @app.route("/wordcloud_chart")
90
+ def wordcloud_chart():
91
+ if not cache:
92
+ return "No word data", 404
93
+
94
+ last_query = list(cache.keys())[-1]
95
+ cached = cache.get(last_query)
96
+
97
+ if not cached:
98
+ return "No word data", 404
99
+
100
+ results = cached["results"]
101
+ texts = [article["title"] + " " + article.get("summary", "") for article in results]
102
+
103
+ text = " ".join(texts)
104
+
105
+ plt.figure(figsize=(16, 8), dpi=150)
106
+ wordcloud = WordCloud(width=1600, height=800, colormap="Blues", background_color='#222').generate(text)
107
+ plt.imshow(wordcloud, interpolation="bilinear")
108
+ plt.axis("off")
109
+
110
+ img = io.BytesIO()
111
+ plt.savefig(img, format="png", bbox_inches="tight", pad_inches=0, dpi=150)
112
+ plt.close()
113
+ img.seek(0)
114
+
115
+ return Response(img.getvalue(), mimetype="image/png")
116
+
117
+ @app.route("/about")
118
+ def about():
119
+ return render_template("about.html")
120
+
121
+ if __name__ == "__main__":
122
+ app.run(host="0.0.0.0", port=7860, debug=True)
main.py ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+ from collections import Counter
3
+ from transformers import pipeline
4
+ import nltk
5
+ from nltk.tokenize import word_tokenize
6
+ from nltk.corpus import stopwords
7
+ import string
8
+ from transformers import AutoModelForSequenceClassification, AutoTokenizer
9
+ import torch
10
+
11
+ nltk.download('punkt')
12
+ nltk.download('stopwords')
13
+ nltk.download('averaged_perceptron_tagger')
14
+ nltk.download('punkt_tab')
15
+
16
+
17
+ # 1. Function for getting news via NewsAPI
18
+ def get_news(query, api_key, num_articles=5):
19
+ url = f'https://newsapi.org/v2/everything?q={query}&apiKey={api_key}&language=en&pageSize={num_articles}'
20
+ response = requests.get(url)
21
+ if response.status_code == 200:
22
+ return response.json()['articles']
23
+ return []
24
+
25
+
26
+ # 2. Analyzing tone with Hugging Face
27
+ tone_analyzer = pipeline("sentiment-analysis", model="distilbert-base-uncased-finetuned-sst-2-english", revision="714eb0f")
28
+
29
+ def analyze_sentiment(text):
30
+ return tone_analyzer(text)[0]
31
+
32
+
33
+ # 3. Define category
34
+
35
+ category_model = AutoModelForSequenceClassification.from_pretrained("cardiffnlp/tweet-topic-21-multi")
36
+ category_tokenizer = AutoTokenizer.from_pretrained("cardiffnlp/tweet-topic-21-multi")
37
+ labels = ['art', 'business', 'entertainment', 'environment', 'fashion', 'finance', 'food',
38
+ 'health', 'law', 'media', 'military', 'music', 'politics', 'religion', 'sci/tech',
39
+ 'sports', 'travel', 'weather', 'world news', 'none']
40
+
41
+ def classify_category(text):
42
+ inputs = category_tokenizer(text, return_tensors="pt", truncation=True, padding=True)
43
+ outputs = category_model(**inputs)
44
+ predicted_class = torch.argmax(outputs.logits, dim=1).item()
45
+ return labels[predicted_class]
46
+
47
+
48
+ # 4. Summarization
49
+ summarizer = pipeline("summarization", model="facebook/bart-large-cnn")
50
+
51
+ def split_text(text, max_tokens=512):
52
+ words = text.split()
53
+ return [' '.join(words[i:i+max_tokens]) for i in range(0, len(words), max_tokens)]
54
+
55
+ def summarize_text(text):
56
+ chunks = split_text(text)
57
+ summaries = [summarizer(chunk, max_length=100, min_length=30, do_sample=False)[0]['summary_text'] for chunk in chunks]
58
+ return ' '.join(summaries)
59
+
60
+
61
+ # 5. Search for trending words
62
+ def extract_trending_words(texts):
63
+ text = ' '.join(texts).lower()
64
+ words = word_tokenize(text)
65
+ words = [word for word in words if word not in stopwords.words('english') and word not in string.punctuation and len(word) > 1]
66
+ word_freq = Counter(words)
67
+ return word_freq.most_common(10)
68
+
69
+ # 6. The main process of analyzing news
70
+ def analyze_news(query, api_key, num_articles=5):
71
+ articles = get_news(query, api_key, num_articles)
72
+
73
+ if not articles:
74
+ return []
75
+
76
+ news_results = []
77
+ for article in articles:
78
+ title = article.get('title', 'No Title')
79
+ description = article.get('description', '') or ''
80
+ url = article.get('url', '#')
81
+
82
+ sentiment = analyze_sentiment(title + " " + description)['label']
83
+ category = classify_category(title + " " + description)
84
+ summary = summarize_text(title + " " + description)
85
+
86
+ news_results.append({
87
+ "title": title,
88
+ "url": url,
89
+ "sentiment": sentiment,
90
+ "category": category,
91
+ "summary": summary
92
+ })
93
+
94
+ return news_results
95
+
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ Flask==3.0.0
2
+ transformers==4.39.1
3
+ torch==2.2.1
4
+ nltk==3.8.1
5
+ matplotlib==3.8.2
6
+ wordcloud==1.9.3
7
+ requests==2.31.0
8
+ pillow==10.2.0
static/script.js ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ let chartsLoaded = { sentiment: false, wordcloud: false };
2
+ let chartInterval = setInterval(checkCharts, 2000);
3
+
4
+ function checkCharts() {
5
+ fetch('/sentiment_chart')
6
+ .then(response => {
7
+ if (response.ok) {
8
+ document.getElementById('sentimentChart').src = '/sentiment_chart';
9
+ document.getElementById('sentimentChart').style.display = 'block';
10
+ document.getElementById('sentimentChartMessage').style.display = 'none';
11
+ chartsLoaded.sentiment = true;
12
+ stopIfChartsLoaded();
13
+ }
14
+ });
15
+
16
+ fetch('/wordcloud_chart')
17
+ .then(response => {
18
+ if (response.ok) {
19
+ document.getElementById('wordcloudChart').src = '/wordcloud_chart';
20
+ document.getElementById('wordcloudChart').style.display = 'block';
21
+ document.getElementById('wordcloudChartMessage').style.display = 'none';
22
+ chartsLoaded.wordcloud = true;
23
+ stopIfChartsLoaded();
24
+ }
25
+ });
26
+ }
27
+
28
+ function stopIfChartsLoaded() {
29
+ if (chartsLoaded.sentiment && chartsLoaded.wordcloud) {
30
+ clearInterval(chartInterval);
31
+ }
32
+ }
static/style.css ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ body {
2
+ font-family: Arial, sans-serif;
3
+ background-color: #181818;
4
+ color: #e0e0e0;
5
+ display: flex;
6
+ justify-content: center;
7
+ align-items: center;
8
+ flex-direction: column;
9
+ min-height: 100vh;
10
+ height: 100%;
11
+ margin: 0;
12
+ padding: 0;
13
+ }
14
+
15
+ .container {
16
+ background: #222;
17
+ padding: 20px;
18
+ border-radius: 10px;
19
+ box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.5);
20
+ text-align: center;
21
+ width: 90%;
22
+ max-width: 600px;
23
+ margin-bottom: 20px;
24
+ }
25
+
26
+ .first-container {
27
+ margin-top: 20px;
28
+ }
29
+
30
+ .res-container {
31
+ text-align: left;
32
+ }
33
+
34
+ h1 {
35
+ color: #f0f0f0;
36
+ }
37
+
38
+ h3 {
39
+ margin-top: 40px;
40
+ }
41
+
42
+ p.meta-info {
43
+ color: #bbb;
44
+ font-size: 14px;
45
+ line-height: 0
46
+ }
47
+
48
+ textarea, input {
49
+ width: 100%;
50
+ padding: 10px;
51
+ border: 1px solid #444;
52
+ border-radius: 5px;
53
+ background: #333;
54
+ color: #e0e0e0;
55
+ resize: none;
56
+ font-size: 16px;
57
+ outline: none;
58
+ box-sizing: border-box;
59
+ margin-bottom: 10px;
60
+ font-family: inherit;
61
+ }
62
+
63
+ button {
64
+ padding: 10px 15px;
65
+ border: none;
66
+ background: #007bff;
67
+ color: white;
68
+ font-size: 16px;
69
+ cursor: pointer;
70
+ border-radius: 5px;
71
+ transition: 0.3s;
72
+ width: 100%;
73
+ }
74
+
75
+ button:hover {
76
+ background: #0056b3;
77
+ }
78
+
79
+ input[type=number]::-webkit-inner-spin-button,
80
+ input[type=number]::-webkit-outer-spin-button {
81
+ -webkit-appearance: none;
82
+ margin: 0;
83
+ }
84
+
85
+ a {
86
+ color: inherit;
87
+ text-decoration: none;
88
+ }
89
+
90
+ a:hover {
91
+ text-decoration: underline;
92
+ }
93
+
94
+ img {
95
+ display: block;
96
+ margin: 0 auto;
97
+ }
98
+
99
+ .about-container {
100
+ margin-top: 20px;
101
+ }
102
+
103
+ .about-container li {
104
+ margin-bottom: 15px;
105
+ text-align: left;
106
+ }
107
+
108
+ .about-container p {
109
+ text-align: left;
110
+ }
111
+
112
+ footer {
113
+ text-align: center;
114
+ margin-bottom: 20px;
115
+ color: #bbb;
116
+ font-size: 14px;
117
+ }
templates/about.html ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>About - NewsAnalyzer</title>
7
+ <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
8
+ <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ctext y='80' font-size='80' %3E📰%3C/text%3E%3C/svg%3E">
9
+ </head>
10
+ <body>
11
+ <div class="container about-container">
12
+ <h1>About NewsAnalyzer</h1>
13
+ <p>
14
+ NewsAnalyzer is an AI-powered tool designed to help you quickly understand the latest news trends. Simply enter your search query, and NewsAnalyzer will gather and analyze articles to deliver valuable insights.
15
+ </p>
16
+ <p>Here's how it works:</p>
17
+ <ul>
18
+ <li><b>News Collection:</b> News articles are fetched from NewsAPI based on your search query.</li>
19
+ <li><b>Sentiment Analysis:</b> Each article is evaluated to determine whether its tone is positive or negative.</li>
20
+ <li><b>Category Classification:</b> Articles are automatically categorized into topics like business, technology, politics, and more.</li>
21
+ <li><b>Summarization:</b> Long descriptions are summarized to give you only the most important details.</li>
22
+ <li><b>Trending Words:</b> The most frequently mentioned words across articles are extracted to highlight key topics.</li>
23
+ <li><b>Charts:</b> Sentiment distribution and trending words are visualized with clear, easy-to-understand charts.</li>
24
+ </ul>
25
+ <p>Technologies:</p>
26
+ <p>NewsAnalyzer combines real-time data collection with machine learning models from Hugging Face, using Flask to seamlessly connect the data processing, analysis, and visualization into a single streamlined experience.</p>
27
+ <a href="/" style="color: #bbb;">← Back to Home</a>
28
+ </div>
29
+ </body>
30
+ <footer>
31
+ Created by Volodymyr Shereperov | <a href="https://github.com/ihaveaplan66/news-analyzer" target="_blank">Source Code on GitHub</a>
32
+ </footer>
33
+ </html>
templates/index.html ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>NewsAnalyzer</title>
7
+ <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
8
+ <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ctext y='80' font-size='80' %3E📰%3C/text%3E%3C/svg%3E">
9
+ </head>
10
+ <body>
11
+ <div class="container first-container">
12
+ <h1>NewsAnalyzer</h1>
13
+
14
+ <form method="POST">
15
+ <textarea name="query" placeholder="Enter search query (e.g., Tesla)" required></textarea>
16
+ <input type="number" name="num_articles" min="1" max="20" value="5" required placeholder="Number of articles (1-20)">
17
+ <button type="submit">Analyze</button>
18
+ </form>
19
+
20
+ <p style="margin-top: 10px; font-size: 14px; color: #aaa;">
21
+ <a href="/about" style="color: #bbb;">How does it work?</a>
22
+ </p>
23
+ </div>
24
+
25
+ {% if results %}
26
+ <div class="container res-container">
27
+ <h2 style="text-align: center">Analysis for '{{ query }}'</h2>
28
+ {% for article in results %}
29
+ <h3><b><a href="{{ article.url }}" target="_blank">{{ article.title }}</a></b></h3>
30
+ <p class="meta-info">{{ article.category }} · {{ article.sentiment }}</p>
31
+ <p>{{ article.summary }}</p>
32
+ {% endfor %}
33
+ </div>
34
+
35
+ <div class="container">
36
+ <h2>Sentiment Analysis</h2>
37
+ <img id="sentimentChart" src="" alt="Sentiment Analysis" style="display:none; max-width:100%;">
38
+ <p id="sentimentChartMessage" style="text-align: center; color: #aaa;">Charts are loading...</p>
39
+ </div>
40
+
41
+ <div class="container">
42
+ <h2>Trending Words</h2>
43
+ <ul style="list-style: none; padding: 0; text-align: center;">
44
+ {% for word, count in trending_words %}
45
+ <li style="display: inline-block; margin: 5px; color: #bbb;">{{ word }} ({{ count }})</li>
46
+ {% endfor %}
47
+ </ul>
48
+ <img id="wordcloudChart" src="" alt="Word Cloud" style="display:none; max-width:100%;">
49
+ <p id="wordcloudChartMessage" style="text-align: center; color: #aaa;">Charts are loading...</p>
50
+ </div>
51
+ <script src="{{ url_for('static', filename='script.js') }}"></script>
52
+ {% endif %}
53
+ </body>
54
+ <footer>
55
+ Created by Volodymyr Shereperov | <a href="https://github.com/ihaveaplan66/news-analyzer" target="_blank">Source Code on GitHub</a>
56
+ </footer>
57
+ </html>