File size: 11,519 Bytes
aa99dc9
 
 
 
 
 
c19a52b
35f6bff
aa99dc9
1fb428d
aa99dc9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c19a52b
0559ea0
681b7f8
 
c19a52b
 
 
 
0559ea0
c19a52b
681b7f8
1fb428d
681b7f8
0559ea0
 
681b7f8
 
 
0559ea0
 
c19a52b
0559ea0
 
 
c19a52b
 
 
 
 
681b7f8
 
0559ea0
b90b711
681b7f8
 
aab34d2
aa99dc9
b90b711
aa99dc9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c19a52b
aa99dc9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c19a52b
aa99dc9
 
 
 
 
 
 
c19a52b
aa99dc9
 
c19a52b
 
aa99dc9
 
 
 
c19a52b
aa99dc9
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
import re
from urllib.parse import urlparse
from datetime import datetime
from typing import List, Dict

import gradio as gr
import plotly.graph_objects as go
import numpy as np


# --------- Heuristic Scoring Logic ---------

SENTENCE_SPLIT_PATTERN = re.compile(r'(?<=[.!?])\s+')
URL_PATTERN = re.compile(r'(https?://[^\s]+)')
YEAR_PATTERN = re.compile(r'\b(19[5-9]\d|20[0-4]\d|2050)\b')

BIAS_WORDS = [
    "obviously", "definitely", "certainly", "undeniably",
    "everyone knows", "no doubt", "clearly", "always", "never"
]


def domain_quality(domain: str) -> int:
    """Very rough heuristic for domain quality."""
    if not domain:
        return 50
    domain = domain.lower()
    if domain.endswith(".edu") or domain.endswith(".ac.uk"):
        return 90
    if domain.endswith(".gov") or domain.endswith(".gov.uk"):
        return 90
    if domain.endswith(".org"):
        return 75
    if "wikipedia.org" in domain:
        return 70
    if "blog" in domain or "medium.com" in domain:
        return 55
    return 60  # default


def label_from_score(score: int) -> str:
    if score >= 80:
        return "High Trust"
    if score >= 60:
        return "Medium Trust"
    if score >= 40:
        return "Low–Medium Trust"
    return "Low Trust"

def make_gauge(score: int):
    # clamp score between 0–100
    score = max(0, min(100, score))

    fig = go.Figure(
        go.Indicator(
            mode="gauge+number",
            value=score,
            number={"suffix": "%", "font": {"size": 34}},
            gauge={
                "shape": "angular",
                "axis": {"range": [0, 100], "visible": False},
                "bar": {
                    "color": "#2563eb",   # solid blue fill for the active progress
                    "thickness": 0.35
                },
                "bgcolor": "white",
                "borderwidth": 0,

                # Three blue segments (background arc)
                "steps": [
                    {"range": [0, 33], "color": "#c7d2fe"},   # light blue
                    {"range": [33, 66], "color": "#60a5fa"},  # medium blue
                    {"range": [66, 100], "color": "#1d4ed8"}  # dark blue
                ],
            },
            domain={"x": [0, 1], "y": [0, 1]},
        )
    )

    fig.update_layout(
        margin=dict(t=30, b=0, l=20, r=20),
        height=220,
    )

    return fig


def analyze_text(text: str, is_ai_generated: bool):
    text = text.strip()
    if not text:
        return (
            "—",
            "No input",
            "Please paste some content to analyze.",
            "- No issues (no text)\n",
            "- Add some content to see suggestions.\n",
            "_No sentences to analyze._",
        )

    # --- Sentence splitting ---
    sentences = [s.strip() for s in SENTENCE_SPLIT_PATTERN.split(text) if s.strip()]

    urls = URL_PATTERN.findall(text)
    domains = []
    for url in urls:
        try:
            parsed = urlparse(url)
            domains.append(parsed.netloc)
        except Exception:
            continue
    unique_domains = set(domains)

    # Sentence-level metadata
    sentence_data: List[Dict] = []
    sentences_with_citation = 0
    sentences_with_bias = 0

    for s in sentences:
        lower_s = s.lower()
        has_citation = bool(
            URL_PATTERN.search(s)
            or re.search(r'\[\d+\]', s)
            or "according to" in lower_s
        )
        has_bias = any(bias in lower_s for bias in BIAS_WORDS)
        years = YEAR_PATTERN.findall(s)

        sentence_data.append(
            {
                "text": s,
                "has_citation": has_citation,
                "has_bias_indicator": has_bias,
                "years": years,
            }
        )

        if has_citation:
            sentences_with_citation += 1
        if has_bias:
            sentences_with_bias += 1

    total_sentences = len(sentences)

    # ---- SQS: Source Quality ----
    if urls:
        domain_scores = [domain_quality(d) for d in unique_domains]
        avg_domain_score = sum(domain_scores) / len(domain_scores)
        sqs = int(avg_domain_score)
        sqs_rationale = f"Based on {len(unique_domains)} cited domain(s); average domain quality ≈ {avg_domain_score:.1f}."
    else:
        sqs = 55
        sqs_rationale = "No explicit sources detected; defaulting to moderate source quality."

    # ---- CSS: Claim Support ----
    citation_ratio = sentences_with_citation / total_sentences if total_sentences else 0
    if citation_ratio == 0:
        css = 25
        css_rationale = "No citations detected for any sentences."
    else:
        css = int(40 + 60 * citation_ratio)  # 40–100
        css_rationale = f"{sentences_with_citation}/{total_sentences} sentence(s) appear to have citations or references."

    # ---- BDS: Bias & Diversity ----
    bias_ratio = sentences_with_bias / total_sentences if total_sentences else 0
    diversity_bonus = min(len(unique_domains) * 5, 20)  # up to +20 for multiple domains

    base_bds = 70 + diversity_bonus
    bias_penalty = int(bias_ratio * 40)  # up to -40
    bds = max(20, min(100, base_bds - bias_penalty))

    bds_rationale_parts = []
    if unique_domains:
        bds_rationale_parts.append(f"{len(unique_domains)} unique source domain(s) detected.")
    if sentences_with_bias:
        bds_rationale_parts.append(
            f"{sentences_with_bias}/{total_sentences} sentence(s) with bias-indicating phrases."
        )
    if not bds_rationale_parts:
        bds_rationale_parts.append("No citations or explicit bias indicators detected.")
    bds_rationale = " ".join(bds_rationale_parts)

    # ---- RCS: Recency & Context ----
    all_years = YEAR_PATTERN.findall(text)
    current_year = datetime.now().year
    if not all_years:
        rcs = 70
        rcs_rationale = "No specific years detected; assuming moderate recency for educational content."
    else:
        years_int = [int(y) for y in all_years]
        newest = max(years_int)
        age = current_year - newest
        if age <= 5:
            rcs = 90
            rcs_rationale = f"Newest year mentioned is {newest}, which is recent."
        elif age <= 15:
            rcs = 70
            rcs_rationale = f"Newest year mentioned is {newest}, which is moderately old."
        else:
            rcs = 50
            rcs_rationale = f"Newest year mentioned is {newest}, which appears quite old for some topics."

    # ---- MRS: Model Reliability ----
    mrs = 80 if not is_ai_generated else 75
    mrs_rationale = "Assuming competent human or general-purpose AI author; can be refined later."

    # ---- Final weighted score ----
    weight_sqs = 0.30
    weight_css = 0.30
    weight_bds = 0.15
    weight_mrs = 0.15
    weight_rcs = 0.10

    overall = (
        sqs * weight_sqs
        + css * weight_css
        + bds * weight_bds
        + mrs * weight_mrs
        + rcs * weight_rcs
    )
    overall_int = int(round(overall))
    label = label_from_score(overall_int)

    # ---- Build markdown outputs ----

    component_md = f"""
### Component Breakdown
| Component | Weight | Score | Rationale |
|----------|--------|-------|-----------|
| SQS (Source Quality) | 30% | {sqs} | {sqs_rationale} |
| CSS (Claim Support)  | 30% | {css} | {css_rationale} |
| BDS (Bias & Diversity) | 15% | {bds} | {bds_rationale} |
| MRS (Model Reliability) | 15% | {mrs} | {mrs_rationale} |
| RCS (Recency & Context) | 10% | {rcs} | {rcs_rationale} |
"""

    issues: List[str] = []
    suggestions: List[str] = []

    if sentences_with_citation == 0:
        issues.append("No explicit citations or references detected.")
        suggestions.append("Add at least 2–3 citations from reliable educational or academic sources.")
    elif citation_ratio < 0.3:
        issues.append("Only a small portion of statements appear to be supported by citations.")
        suggestions.append("Support more factual claims with references to textbooks, encyclopedias, or academic articles.")

    if sentences_with_bias > 0:
        issues.append("Some sentences use strong or absolute language that may indicate bias.")
        suggestions.append("Rephrase strongly worded claims into more neutral, evidence-based statements.")

    if rcs <= 60:
        issues.append("The information relies on relatively old dates, which may be outdated for some topics.")
        suggestions.append("Check if there are more recent sources or data to support your claims.")

    if not issues:
        issues.append("No major credibility issues detected by the heuristic checks.")
        suggestions.append("You can still improve credibility by adding more high-quality sources and clarifying complex claims.")

    issues_md = "\n".join(f"- {i}" for i in issues)
    suggestions_md = "\n".join(f"- {s}" for s in suggestions)

    sentence_lines = []
    for s in sentence_data:
        tags = []
        if s["has_citation"]:
            tags.append("📚 citation")
        if s["has_bias_indicator"]:
            tags.append("⚠️ possible bias")
        if s["years"]:
            tags.append("📅 years: " + ", ".join(s["years"]))
        tag_str = " — " + ", ".join(tags) if tags else ""
        sentence_lines.append(f"- {s['text']}{tag_str}")

    sentences_md = "\n".join(sentence_lines) if sentence_lines else "_No sentences found._"

    overall_md = f"""
# Trust Score: **{overall_int}/100**  
Label: **{label}**
"""

    return overall_int, overall_md, label, component_md, issues_md, suggestions_md, sentences_md


# --------- Gradio UI ---------


with gr.Blocks(title="Educational Content Credibility Checker") as demo:
    gr.Markdown(
        """
# 📚 Educational Content Credibility Checker (MVP)

Paste any student essay or AI-generated explanation.  
This tool estimates a **Trust Score (0–100)** based on:
- Source quality  
- Claim support (citations)  
- Bias & diversity  
- Model reliability  
- Recency & context  

> ⚠️ MVP: This is a **heuristic prototype**, not a perfect fact-checker.
"""
    )

    with gr.Row():
        with gr.Column():
            text_input = gr.Textbox(
                label="1. Paste content here",
                lines=15,
                placeholder="Paste student content or AI-generated text...",
            )
            is_ai_checkbox = gr.Checkbox(
                label="Content is AI-generated", value=False
            )
            analyze_btn = gr.Button("Analyze Credibility", variant="primary")

        with gr.Column():
            gauge_plot = gr.Plot(label="trust.AI Score")
            overall_out = gr.Markdown(label="Overall Trust Score")
            components_out = gr.Markdown(label="Component Breakdown")
            issues_out = gr.Markdown(label="Issues Detected")
            suggestions_out = gr.Markdown(label="Suggestions for Improvement")
            sentences_out = gr.Markdown(label="Sentence-level Analysis")

    def gradio_wrapper(text, is_ai_generated):
        overall_int, overall_md, label, component_md, issues_md, suggestions_md, sentences_md = analyze_text(
            text, is_ai_generated
        )
        fig = make_gauge(overall_int)
        return fig, overall_md, component_md, issues_md, suggestions_md, sentences_md

    analyze_btn.click(
        fn=gradio_wrapper,
        inputs=[text_input, is_ai_checkbox],
        outputs=[gauge_plot, overall_out, components_out, issues_out, suggestions_out, sentences_out],
    )

if __name__ == "__main__":
    demo.launch()