feat(zopk): Add admin-only knowledge display on homepage and /zopk
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
4 new features visible only to admin (role=='admin'): - Homepage: "Czy wiesz, że?" widget with 3 random high-confidence facts - /zopk: Knowledge stats, top entities, key numeric facts, fact type distribution - /zopk: Dated facts timeline (CSS-only vertical timeline) - /zopk: D3.js entity co-occurrence graph with slider control No migrations needed - read-only SELECT queries only. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
df8736297d
commit
30bb960a03
@ -113,6 +113,17 @@ def index():
|
||||
).first() is not None
|
||||
user_can_attend = next_event.can_user_attend(current_user)
|
||||
|
||||
# ZOPK Knowledge facts — admin only widget
|
||||
zopk_facts = []
|
||||
if current_user.role == 'admin':
|
||||
try:
|
||||
from database import ZOPKKnowledgeFact, ZOPKNews
|
||||
zopk_facts = db.query(ZOPKKnowledgeFact).join(ZOPKNews).filter(
|
||||
ZOPKKnowledgeFact.confidence_score >= 0.5
|
||||
).order_by(func.random()).limit(3).all()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Sprawdź czy użytkownik ma deklarację członkowską w toku
|
||||
pending_application = None
|
||||
if not current_user.is_norda_member and not current_user.company_id:
|
||||
@ -131,7 +142,8 @@ def index():
|
||||
next_event=next_event,
|
||||
user_registered=user_registered,
|
||||
user_can_attend=user_can_attend,
|
||||
pending_application=pending_application
|
||||
pending_application=pending_application,
|
||||
zopk_facts=zopk_facts
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@ -8,6 +8,8 @@ Contains public-facing routes for ZOPK (Zielony Okręg Przemysłowy Kaszubia).
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from flask import abort, render_template, request
|
||||
from flask_login import current_user
|
||||
from sqlalchemy import func
|
||||
|
||||
from database import (
|
||||
SessionLocal,
|
||||
@ -16,7 +18,10 @@ from database import (
|
||||
ZOPKNews,
|
||||
ZOPKResource,
|
||||
ZOPKMilestone,
|
||||
ZOPKCompanyLink
|
||||
ZOPKCompanyLink,
|
||||
ZOPKKnowledgeFact,
|
||||
ZOPKKnowledgeEntity,
|
||||
ZOPKKnowledgeChunk
|
||||
)
|
||||
from . import bp
|
||||
|
||||
@ -95,6 +100,34 @@ def zopk_index():
|
||||
'total_stakeholders': db.query(ZOPKStakeholder).filter(ZOPKStakeholder.is_active == True).count()
|
||||
}
|
||||
|
||||
# Knowledge data — admin only
|
||||
knowledge_data = None
|
||||
if current_user.is_authenticated and current_user.role == 'admin':
|
||||
knowledge_data = {
|
||||
'total_facts': db.query(func.count(ZOPKKnowledgeFact.id)).scalar(),
|
||||
'total_entities': db.query(func.count(ZOPKKnowledgeEntity.id)).filter(
|
||||
ZOPKKnowledgeEntity.merged_into_id.is_(None)
|
||||
).scalar(),
|
||||
'total_chunks': db.query(func.count(ZOPKKnowledgeChunk.id)).filter(
|
||||
ZOPKKnowledgeChunk.embedding.isnot(None)
|
||||
).scalar(),
|
||||
'fact_types': db.query(
|
||||
ZOPKKnowledgeFact.fact_type, func.count()
|
||||
).group_by(ZOPKKnowledgeFact.fact_type).all(),
|
||||
'top_entities': db.query(ZOPKKnowledgeEntity).filter(
|
||||
ZOPKKnowledgeEntity.merged_into_id.is_(None),
|
||||
ZOPKKnowledgeEntity.mentions_count >= 3
|
||||
).order_by(ZOPKKnowledgeEntity.mentions_count.desc()).limit(15).all(),
|
||||
'key_investments': db.query(ZOPKKnowledgeFact).filter(
|
||||
ZOPKKnowledgeFact.numeric_value.isnot(None),
|
||||
ZOPKKnowledgeFact.confidence_score >= 0.5
|
||||
).order_by(ZOPKKnowledgeFact.numeric_value.desc()).limit(5).all(),
|
||||
'dated_facts': db.query(ZOPKKnowledgeFact).join(ZOPKNews).filter(
|
||||
ZOPKKnowledgeFact.date_value.isnot(None),
|
||||
ZOPKKnowledgeFact.confidence_score >= 0.4
|
||||
).order_by(ZOPKKnowledgeFact.date_value.desc()).limit(20).all(),
|
||||
}
|
||||
|
||||
return render_template('zopk/index.html',
|
||||
projects=projects,
|
||||
stakeholders=stakeholders,
|
||||
@ -102,7 +135,8 @@ def zopk_index():
|
||||
resources=resources,
|
||||
stats=stats,
|
||||
news_stats=news_stats,
|
||||
milestones=milestones
|
||||
milestones=milestones,
|
||||
knowledge_data=knowledge_data
|
||||
)
|
||||
|
||||
finally:
|
||||
|
||||
@ -952,6 +952,38 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ZOPK Knowledge Widget — admin only -->
|
||||
{% if current_user.is_authenticated and current_user.role == 'admin' and zopk_facts %}
|
||||
<div style="background: linear-gradient(135deg, #059669 0%, #047857 50%, #065f46 100%); border-radius: var(--radius-lg); padding: var(--spacing-lg); margin-bottom: var(--spacing-xl); color: white;" data-animate="fadeIn">
|
||||
<div style="display: flex; align-items: center; gap: var(--spacing-sm); margin-bottom: var(--spacing-md);">
|
||||
<span style="font-size: 1.5rem;">💡</span>
|
||||
<h3 style="margin: 0; font-size: var(--font-size-lg); font-weight: 700;">Czy wiesz, że... <span style="font-weight: 400; font-size: var(--font-size-sm); opacity: 0.8;">(Baza Wiedzy ZOPK)</span></h3>
|
||||
</div>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: var(--spacing-md);">
|
||||
{% for fact in zopk_facts %}
|
||||
<div style="background: rgba(255,255,255,0.12); border-radius: var(--radius); padding: var(--spacing-md);">
|
||||
<span style="display: inline-block; padding: 1px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; margin-bottom: var(--spacing-xs);
|
||||
{% if fact.fact_type == 'investment' %}background: rgba(16,185,129,0.3);
|
||||
{% elif fact.fact_type == 'event' %}background: rgba(59,130,246,0.3);
|
||||
{% elif fact.fact_type == 'decision' %}background: rgba(245,158,11,0.3);
|
||||
{% elif fact.fact_type == 'milestone' %}background: rgba(139,92,246,0.3);
|
||||
{% else %}background: rgba(255,255,255,0.2);{% endif %}">
|
||||
{{ fact.fact_type or 'fakt' }}
|
||||
</span>
|
||||
<p style="font-size: var(--font-size-sm); line-height: 1.5; margin: 0; opacity: 0.95;">
|
||||
{{ fact.full_text[:200] }}{% if fact.full_text|length > 200 %}...{% endif %}
|
||||
</p>
|
||||
{% if fact.source_news %}
|
||||
<div style="font-size: 11px; margin-top: var(--spacing-xs); opacity: 0.7;">
|
||||
{{ fact.source_news.source_name or fact.source_news.source_domain }} • {{ fact.source_news.published_at.strftime('%d.%m.%Y') if fact.source_news.published_at else '' }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Search Section -->
|
||||
<div class="search-section" data-animate="fadeIn">
|
||||
<form action="{{ url_for('search') }}" method="GET" class="search-bar">
|
||||
|
||||
@ -568,6 +568,386 @@
|
||||
font-size: var(--font-size-base);
|
||||
}
|
||||
}
|
||||
|
||||
/* ====== Knowledge Section (admin only) ====== */
|
||||
.knowledge-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: var(--spacing-lg);
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.knowledge-stat-box {
|
||||
background: var(--surface);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-lg);
|
||||
box-shadow: var(--shadow);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.knowledge-stat-box .stat-number {
|
||||
font-size: var(--font-size-3xl);
|
||||
font-weight: 700;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.knowledge-stat-box .stat-label {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
margin-top: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.knowledge-entities-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.knowledge-entities-list li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--spacing-sm) 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.knowledge-entities-list li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.entity-type-badge {
|
||||
display: inline-block;
|
||||
padding: 1px 6px;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.entity-type-badge.company { background: #dbeafe; color: #1e40af; }
|
||||
.entity-type-badge.person { background: #fce7f3; color: #9d174d; }
|
||||
.entity-type-badge.place { background: #dcfce7; color: #166534; }
|
||||
.entity-type-badge.organization { background: #fef3c7; color: #92400e; }
|
||||
.entity-type-badge.project { background: #ede9fe; color: #5b21b6; }
|
||||
|
||||
.entity-mentions {
|
||||
background: var(--background);
|
||||
padding: 2px 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.fact-types-bar {
|
||||
display: flex;
|
||||
height: 24px;
|
||||
border-radius: var(--radius);
|
||||
overflow: hidden;
|
||||
margin-top: var(--spacing-md);
|
||||
}
|
||||
|
||||
.fact-type-segment {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
transition: flex 0.3s ease;
|
||||
min-width: 24px;
|
||||
}
|
||||
|
||||
.fact-type-segment.statistics { background: #3b82f6; }
|
||||
.fact-type-segment.investment { background: #10b981; }
|
||||
.fact-type-segment.event { background: #f59e0b; }
|
||||
.fact-type-segment.decision { background: #ef4444; }
|
||||
.fact-type-segment.milestone { background: #8b5cf6; }
|
||||
.fact-type-segment.other { background: #6b7280; }
|
||||
|
||||
.fact-types-legend {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-md);
|
||||
margin-top: var(--spacing-sm);
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.fact-types-legend span::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 2px;
|
||||
margin-right: 4px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.fact-types-legend .ft-statistics::before { background: #3b82f6; }
|
||||
.fact-types-legend .ft-investment::before { background: #10b981; }
|
||||
.fact-types-legend .ft-event::before { background: #f59e0b; }
|
||||
.fact-types-legend .ft-decision::before { background: #ef4444; }
|
||||
.fact-types-legend .ft-milestone::before { background: #8b5cf6; }
|
||||
.fact-types-legend .ft-other::before { background: #6b7280; }
|
||||
|
||||
.investment-card {
|
||||
background: var(--surface);
|
||||
padding: var(--spacing-md);
|
||||
border-radius: var(--radius);
|
||||
border-left: 3px solid #10b981;
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.investment-value {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: 700;
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
.investment-desc {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
margin-top: var(--spacing-xs);
|
||||
}
|
||||
|
||||
/* ====== Knowledge Timeline (admin only) ====== */
|
||||
.knowledge-timeline {
|
||||
position: relative;
|
||||
padding: 0 0 0 120px;
|
||||
margin-bottom: var(--spacing-2xl);
|
||||
}
|
||||
|
||||
.knowledge-timeline::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 110px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 3px;
|
||||
background: linear-gradient(180deg, #10b981 0%, #3b82f6 50%, #8b5cf6 100%);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.kt-item {
|
||||
position: relative;
|
||||
padding-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.kt-item:last-child { padding-bottom: 0; }
|
||||
|
||||
.kt-date {
|
||||
position: absolute;
|
||||
left: -120px;
|
||||
top: 4px;
|
||||
width: 100px;
|
||||
text-align: right;
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.kt-dot {
|
||||
position: absolute;
|
||||
left: -16px;
|
||||
top: 6px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
border: 3px solid white;
|
||||
box-shadow: 0 0 0 2px #059669;
|
||||
}
|
||||
|
||||
.kt-dot.investment { background: #10b981; box-shadow: 0 0 0 2px #10b981; }
|
||||
.kt-dot.event { background: #3b82f6; box-shadow: 0 0 0 2px #3b82f6; }
|
||||
.kt-dot.decision { background: #f59e0b; box-shadow: 0 0 0 2px #f59e0b; }
|
||||
.kt-dot.milestone { background: #8b5cf6; box-shadow: 0 0 0 2px #8b5cf6; }
|
||||
.kt-dot.statistics { background: #6b7280; box-shadow: 0 0 0 2px #6b7280; }
|
||||
|
||||
.kt-card {
|
||||
background: var(--surface);
|
||||
padding: var(--spacing-md);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
margin-left: var(--spacing-md);
|
||||
}
|
||||
|
||||
.kt-card .fact-type-tag {
|
||||
display: inline-block;
|
||||
padding: 1px 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 500;
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.kt-card .fact-type-tag.investment { background: #dcfce7; color: #166534; }
|
||||
.kt-card .fact-type-tag.event { background: #dbeafe; color: #1e40af; }
|
||||
.kt-card .fact-type-tag.decision { background: #fef3c7; color: #92400e; }
|
||||
.kt-card .fact-type-tag.milestone { background: #ede9fe; color: #5b21b6; }
|
||||
.kt-card .fact-type-tag.statistics { background: #f3f4f6; color: #374151; }
|
||||
|
||||
.kt-card p {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-primary);
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.kt-source {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--text-muted);
|
||||
margin-top: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.kt-source a {
|
||||
color: var(--primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.kt-source a:hover { text-decoration: underline; }
|
||||
|
||||
/* ====== Knowledge Graph (admin only) ====== */
|
||||
.knowledge-graph-container {
|
||||
position: relative;
|
||||
background: var(--surface);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow);
|
||||
overflow: hidden;
|
||||
margin-bottom: var(--spacing-2xl);
|
||||
}
|
||||
|
||||
.kg-controls {
|
||||
position: absolute;
|
||||
top: var(--spacing-md);
|
||||
left: var(--spacing-md);
|
||||
z-index: 10;
|
||||
background: rgba(255,255,255,0.95);
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.kg-controls label { color: var(--text-secondary); }
|
||||
|
||||
.kg-controls input[type="range"] {
|
||||
width: 100px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.kg-stats {
|
||||
position: absolute;
|
||||
top: var(--spacing-md);
|
||||
right: var(--spacing-md);
|
||||
z-index: 10;
|
||||
background: rgba(255,255,255,0.95);
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.kg-stats strong { color: var(--primary); }
|
||||
|
||||
#kg-svg {
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
#kg-svg:active { cursor: grabbing; }
|
||||
|
||||
.kg-node circle {
|
||||
stroke: #fff;
|
||||
stroke-width: 2px;
|
||||
}
|
||||
|
||||
.kg-node:hover circle {
|
||||
stroke-width: 4px;
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
|
||||
.kg-node text {
|
||||
font-size: 9px;
|
||||
fill: var(--text-primary);
|
||||
pointer-events: none;
|
||||
text-anchor: middle;
|
||||
}
|
||||
|
||||
.kg-link {
|
||||
stroke: #999;
|
||||
stroke-opacity: 0.4;
|
||||
}
|
||||
|
||||
.kg-legend {
|
||||
position: absolute;
|
||||
bottom: var(--spacing-md);
|
||||
left: var(--spacing-md);
|
||||
background: rgba(255,255,255,0.95);
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-md);
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
|
||||
.kg-legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.kg-legend-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.kg-tooltip {
|
||||
position: absolute;
|
||||
background: rgba(0,0,0,0.85);
|
||||
color: white;
|
||||
padding: 6px 10px;
|
||||
border-radius: var(--radius);
|
||||
font-size: var(--font-size-xs);
|
||||
pointer-events: none;
|
||||
z-index: 100;
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.knowledge-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.knowledge-timeline {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.knowledge-timeline::before {
|
||||
left: 6px;
|
||||
}
|
||||
|
||||
.kt-date {
|
||||
position: static;
|
||||
width: auto;
|
||||
text-align: left;
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.kt-dot {
|
||||
left: -20px;
|
||||
}
|
||||
|
||||
.kt-card {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
@ -598,6 +978,124 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Knowledge Section — admin only -->
|
||||
{% if current_user.is_authenticated and current_user.role == 'admin' and knowledge_data %}
|
||||
<section>
|
||||
<div class="section-header">
|
||||
<h2>Wiedza o regionie (baza wiedzy AI)</h2>
|
||||
</div>
|
||||
|
||||
<!-- Stats row -->
|
||||
<div class="knowledge-grid">
|
||||
<div class="knowledge-stat-box">
|
||||
<div class="stat-number">{{ knowledge_data.total_facts }}</div>
|
||||
<div class="stat-label">Wyekstrahowane fakty</div>
|
||||
</div>
|
||||
<div class="knowledge-stat-box">
|
||||
<div class="stat-number">{{ knowledge_data.total_entities }}</div>
|
||||
<div class="stat-label">Rozpoznane encje</div>
|
||||
</div>
|
||||
<div class="knowledge-stat-box">
|
||||
<div class="stat-number">{{ knowledge_data.total_chunks }}</div>
|
||||
<div class="stat-label">Chunki z embeddingami</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fact types bar -->
|
||||
{% if knowledge_data.fact_types %}
|
||||
{% set total_ft = knowledge_data.fact_types|sum(attribute='1') %}
|
||||
{% if total_ft > 0 %}
|
||||
<div class="fact-types-bar">
|
||||
{% for ft_name, ft_count in knowledge_data.fact_types %}
|
||||
<div class="fact-type-segment {{ ft_name or 'other' }}" style="flex: {{ ft_count }}" title="{{ ft_name or 'inne' }}: {{ ft_count }}">
|
||||
{% if ft_count * 100 / total_ft > 8 %}{{ ft_count }}{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="fact-types-legend">
|
||||
{% for ft_name, ft_count in knowledge_data.fact_types %}
|
||||
<span class="ft-{{ ft_name or 'other' }}">{{ ft_name or 'inne' }} ({{ ft_count }})</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<!-- Two columns: entities + investments -->
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: var(--spacing-lg); margin-top: var(--spacing-xl);">
|
||||
<!-- Top entities -->
|
||||
<div>
|
||||
<h3 style="font-size: var(--font-size-lg); margin-bottom: var(--spacing-md);">Najczęściej wymieniane encje</h3>
|
||||
<ul class="knowledge-entities-list">
|
||||
{% for entity in knowledge_data.top_entities %}
|
||||
<li>
|
||||
<span>
|
||||
<span class="entity-type-badge {{ entity.entity_type or 'other' }}">{{ entity.entity_type or '?' }}</span>
|
||||
{{ entity.canonical_name or entity.name }}
|
||||
</span>
|
||||
<span class="entity-mentions">{{ entity.mentions_count }}x</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Key investments / numeric facts -->
|
||||
<div>
|
||||
<h3 style="font-size: var(--font-size-lg); margin-bottom: var(--spacing-md);">Kluczowe dane liczbowe</h3>
|
||||
{% for fact in knowledge_data.key_investments %}
|
||||
<div class="investment-card">
|
||||
<div class="investment-value">
|
||||
{% if fact.numeric_value >= 1000000000 %}
|
||||
{{ "%.1f"|format(fact.numeric_value / 1000000000) }} mld {{ fact.numeric_unit or '' }}
|
||||
{% elif fact.numeric_value >= 1000000 %}
|
||||
{{ "%.1f"|format(fact.numeric_value / 1000000) }} mln {{ fact.numeric_unit or '' }}
|
||||
{% elif fact.numeric_value >= 1000 %}
|
||||
{{ "%.0f"|format(fact.numeric_value / 1000) }} tys. {{ fact.numeric_unit or '' }}
|
||||
{% else %}
|
||||
{{ "%.0f"|format(fact.numeric_value) }} {{ fact.numeric_unit or '' }}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="investment-desc">{{ fact.full_text[:200] }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Knowledge Timeline — admin only -->
|
||||
{% if knowledge_data.dated_facts %}
|
||||
<section>
|
||||
<div class="section-header">
|
||||
<h2>Oś czasu faktów</h2>
|
||||
</div>
|
||||
|
||||
<div class="knowledge-timeline">
|
||||
{% for fact in knowledge_data.dated_facts %}
|
||||
<div class="kt-item">
|
||||
<div class="kt-date">{{ fact.date_value.strftime('%d.%m.%Y') }}</div>
|
||||
<div class="kt-dot {{ fact.fact_type or 'statistics' }}"></div>
|
||||
<div class="kt-card">
|
||||
<span class="fact-type-tag {{ fact.fact_type or 'statistics' }}">
|
||||
{% if fact.fact_type == 'investment' %}Inwestycja
|
||||
{% elif fact.fact_type == 'event' %}Wydarzenie
|
||||
{% elif fact.fact_type == 'decision' %}Decyzja
|
||||
{% elif fact.fact_type == 'milestone' %}Kamień milowy
|
||||
{% else %}{{ fact.fact_type or 'Fakt' }}
|
||||
{% endif %}
|
||||
</span>
|
||||
<p>{{ fact.full_text[:300] }}</p>
|
||||
{% if fact.source_news %}
|
||||
<div class="kt-source">
|
||||
Źródło: <a href="{{ fact.source_news.url }}" target="_blank" rel="noopener">{{ fact.source_news.source_name or fact.source_news.source_domain }}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<!-- Projects Section -->
|
||||
<section>
|
||||
<div class="section-header">
|
||||
@ -845,4 +1343,141 @@
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<!-- Knowledge Graph — admin only -->
|
||||
{% if current_user.is_authenticated and current_user.role == 'admin' and knowledge_data %}
|
||||
<section>
|
||||
<div class="section-header">
|
||||
<h2>Graf współwystępowania encji</h2>
|
||||
</div>
|
||||
|
||||
<div class="knowledge-graph-container">
|
||||
<div class="kg-controls">
|
||||
<label>Min. współwystąpień:</label>
|
||||
<input type="range" id="kgMinCooccur" min="2" max="10" value="2"
|
||||
oninput="document.getElementById('kgCooccurVal').textContent=this.value"
|
||||
onchange="loadKnowledgeGraph()">
|
||||
<span id="kgCooccurVal">2</span>
|
||||
</div>
|
||||
<div class="kg-stats" id="kgStats">Ładowanie...</div>
|
||||
<svg id="kg-svg"></svg>
|
||||
<div class="kg-legend">
|
||||
<div class="kg-legend-item"><div class="kg-legend-dot" style="background:#3b82f6"></div><span>Firmy</span></div>
|
||||
<div class="kg-legend-item"><div class="kg-legend-dot" style="background:#ec4899"></div><span>Osoby</span></div>
|
||||
<div class="kg-legend-item"><div class="kg-legend-dot" style="background:#10b981"></div><span>Miejsca</span></div>
|
||||
<div class="kg-legend-item"><div class="kg-legend-dot" style="background:#f59e0b"></div><span>Organizacje</span></div>
|
||||
<div class="kg-legend-item"><div class="kg-legend-dot" style="background:#8b5cf6"></div><span>Projekty</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kg-tooltip" id="kgTooltip"></div>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
{% if current_user.is_authenticated and current_user.role == 'admin' and knowledge_data %}
|
||||
// Knowledge Graph — D3.js
|
||||
(function() {
|
||||
if (typeof d3 === 'undefined') {
|
||||
window.addEventListener('load', function() { if (typeof d3 !== 'undefined') initKG(); });
|
||||
} else {
|
||||
initKG();
|
||||
}
|
||||
|
||||
var kgSim, kgSvg, kgG, kgZoom;
|
||||
|
||||
function initKG() {
|
||||
kgSvg = d3.select('#kg-svg');
|
||||
if (!kgSvg.node()) return;
|
||||
|
||||
kgZoom = d3.zoom()
|
||||
.scaleExtent([0.1, 4])
|
||||
.on('zoom', function(event) { kgG.attr('transform', event.transform); });
|
||||
|
||||
kgSvg.call(kgZoom);
|
||||
kgG = kgSvg.append('g');
|
||||
kgG.append('g').attr('class', 'kg-links');
|
||||
kgG.append('g').attr('class', 'kg-nodes');
|
||||
|
||||
loadKnowledgeGraph();
|
||||
}
|
||||
|
||||
window.loadKnowledgeGraph = async function() {
|
||||
var minC = document.getElementById('kgMinCooccur').value;
|
||||
try {
|
||||
var resp = await fetch('/admin/zopk-api/knowledge/graph/data?min_cooccurrence=' + minC + '&limit=150');
|
||||
var data = await resp.json();
|
||||
if (data.success) {
|
||||
renderKG(data.nodes, data.links, data.stats);
|
||||
}
|
||||
} catch(e) {
|
||||
console.error('KG load error:', e);
|
||||
}
|
||||
};
|
||||
|
||||
var typeColors = {
|
||||
company: '#3b82f6', person: '#ec4899', place: '#10b981',
|
||||
organization: '#f59e0b', Organizacja: '#f59e0b',
|
||||
project: '#8b5cf6', Projekt: '#8b5cf6',
|
||||
Lokalizacja: '#10b981', event: '#ef4444'
|
||||
};
|
||||
|
||||
function nodeColor(type) { return typeColors[type] || '#6b7280'; }
|
||||
|
||||
function renderKG(nodes, links, stats) {
|
||||
var width = kgSvg.node().getBoundingClientRect().width;
|
||||
var height = 500;
|
||||
|
||||
if (kgSim) kgSim.stop();
|
||||
kgG.select('.kg-links').selectAll('*').remove();
|
||||
kgG.select('.kg-nodes').selectAll('*').remove();
|
||||
|
||||
document.getElementById('kgStats').innerHTML =
|
||||
'<strong>' + stats.total_nodes + '</strong> encji • <strong>' + stats.total_links + '</strong> połączeń';
|
||||
|
||||
var link = kgG.select('.kg-links').selectAll('line')
|
||||
.data(links).enter().append('line')
|
||||
.attr('class', 'kg-link')
|
||||
.attr('stroke-width', function(d) { return Math.sqrt(d.value); });
|
||||
|
||||
var node = kgG.select('.kg-nodes').selectAll('g')
|
||||
.data(nodes).enter().append('g')
|
||||
.attr('class', 'kg-node')
|
||||
.call(d3.drag()
|
||||
.on('start', function(ev, d) { if (!ev.active) kgSim.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; })
|
||||
.on('drag', function(ev, d) { d.fx = ev.x; d.fy = ev.y; })
|
||||
.on('end', function(ev, d) { if (!ev.active) kgSim.alphaTarget(0); d.fx = null; d.fy = null; }))
|
||||
.on('mouseover', function(ev, d) {
|
||||
var tt = document.getElementById('kgTooltip');
|
||||
tt.innerHTML = '<strong>' + d.name + '</strong><br>Typ: ' + d.type + ' | Wzmianki: ' + d.mentions;
|
||||
tt.style.display = 'block';
|
||||
tt.style.left = (ev.pageX + 10) + 'px';
|
||||
tt.style.top = (ev.pageY + 10) + 'px';
|
||||
})
|
||||
.on('mouseout', function() { document.getElementById('kgTooltip').style.display = 'none'; });
|
||||
|
||||
node.append('circle')
|
||||
.attr('r', function(d) { return Math.max(6, Math.min(24, Math.sqrt(d.mentions) * 2)); })
|
||||
.attr('fill', function(d) { return nodeColor(d.type); });
|
||||
|
||||
node.filter(function(d) { return d.mentions >= 8; })
|
||||
.append('text')
|
||||
.attr('dy', function(d) { return Math.max(6, Math.min(24, Math.sqrt(d.mentions) * 2)) + 10; })
|
||||
.text(function(d) { return d.name.length > 18 ? d.name.slice(0, 18) + '...' : d.name; });
|
||||
|
||||
kgSim = d3.forceSimulation(nodes)
|
||||
.force('link', d3.forceLink(links).id(function(d) { return d.id; }).distance(80).strength(function(d) { return Math.min(1, d.value / 10); }))
|
||||
.force('charge', d3.forceManyBody().strength(-150))
|
||||
.force('center', d3.forceCenter(width / 2, height / 2))
|
||||
.force('collision', d3.forceCollide().radius(function(d) { return Math.max(6, Math.sqrt(d.mentions) * 2) + 4; }))
|
||||
.on('tick', function() {
|
||||
link.attr('x1', function(d) { return d.source.x; })
|
||||
.attr('y1', function(d) { return d.source.y; })
|
||||
.attr('x2', function(d) { return d.target.x; })
|
||||
.attr('y2', function(d) { return d.target.y; });
|
||||
node.attr('transform', function(d) { return 'translate(' + d.x + ',' + d.y + ')'; });
|
||||
});
|
||||
}
|
||||
})();
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user