feat(zopk): Panel admina bazy wiedzy, poprawa odpowiedzi AI, timeline
Priorytet 1 - Panel admina bazy wiedzy ZOPK: - /admin/zopk/knowledge - dashboard ze statystykami - /admin/zopk/knowledge/chunks - lista chunks z filtrowaniem - /admin/zopk/knowledge/facts - lista faktów z typami - /admin/zopk/knowledge/entities - lista encji z mentions - CRUD operacje: weryfikacja, usuwanie Priorytet 2 - Poprawa jakości odpowiedzi NordaGPT: - Linki markdown do źródeł w kontekście ZOPK - Ulepszone formatowanie (bold, listy, nagłówki) - Sekcja "Źródła" na końcu odpowiedzi - Instrukcje w system prompt dla lepszej prezentacji Priorytet 3 - Timeline ZOPK: - Model ZOPKMilestone w database.py - Migracja 016_zopk_milestones.sql z sample data - Sekcja "Roadmapa ZOPK" na stronie /zopk - Pionowa oś czasu z markerami lat - Statusy: completed, in_progress, planned, delayed Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
7cc5f033fe
commit
d0dda10bd7
302
app.py
302
app.py
@ -9989,9 +9989,9 @@ def release_notes():
|
||||
def zopk_index():
|
||||
"""
|
||||
Public knowledge base page for ZOPK.
|
||||
Shows projects, stakeholders, approved news, and resources.
|
||||
Shows projects, stakeholders, approved news, resources, and timeline.
|
||||
"""
|
||||
from database import ZOPKProject, ZOPKStakeholder, ZOPKNews, ZOPKResource
|
||||
from database import ZOPKProject, ZOPKStakeholder, ZOPKNews, ZOPKResource, ZOPKMilestone
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
@ -10000,6 +10000,11 @@ def zopk_index():
|
||||
ZOPKProject.is_active == True
|
||||
).order_by(ZOPKProject.sort_order, ZOPKProject.name).all()
|
||||
|
||||
# Get milestones for timeline (sorted by target_date)
|
||||
milestones = db.query(ZOPKMilestone).filter(
|
||||
ZOPKMilestone.is_verified == True # Only show verified milestones
|
||||
).order_by(ZOPKMilestone.target_date.asc()).all()
|
||||
|
||||
# Get active stakeholders
|
||||
stakeholders = db.query(ZOPKStakeholder).filter(
|
||||
ZOPKStakeholder.is_active == True
|
||||
@ -10056,7 +10061,8 @@ def zopk_index():
|
||||
news_items=news_items,
|
||||
resources=resources,
|
||||
stats=stats,
|
||||
news_stats=news_stats
|
||||
news_stats=news_stats,
|
||||
milestones=milestones
|
||||
)
|
||||
|
||||
finally:
|
||||
@ -11514,6 +11520,296 @@ def api_zopk_knowledge_search():
|
||||
db.close()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# ZOPK KNOWLEDGE BASE - ADMIN PANEL
|
||||
# ============================================================
|
||||
|
||||
@app.route('/admin/zopk/knowledge')
|
||||
@login_required
|
||||
def admin_zopk_knowledge_dashboard():
|
||||
"""
|
||||
Dashboard for ZOPK Knowledge Base management.
|
||||
Shows stats and links to chunks, facts, entities lists.
|
||||
"""
|
||||
if not current_user.is_admin:
|
||||
flash('Brak uprawnień do tej sekcji.', 'warning')
|
||||
return redirect(url_for('index'))
|
||||
|
||||
return render_template('admin/zopk_knowledge_dashboard.html')
|
||||
|
||||
|
||||
@app.route('/admin/zopk/knowledge/chunks')
|
||||
@login_required
|
||||
def admin_zopk_knowledge_chunks():
|
||||
"""
|
||||
List knowledge chunks with pagination and filtering.
|
||||
"""
|
||||
if not current_user.is_admin:
|
||||
flash('Brak uprawnień do tej sekcji.', 'warning')
|
||||
return redirect(url_for('index'))
|
||||
|
||||
from zopk_knowledge_service import list_chunks
|
||||
|
||||
# Get query params
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = request.args.get('per_page', 20, type=int)
|
||||
source_news_id = request.args.get('source_news_id', type=int)
|
||||
has_embedding = request.args.get('has_embedding')
|
||||
is_verified = request.args.get('is_verified')
|
||||
|
||||
# Convert string params to bool
|
||||
if has_embedding is not None:
|
||||
has_embedding = has_embedding.lower() == 'true'
|
||||
if is_verified is not None:
|
||||
is_verified = is_verified.lower() == 'true'
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
result = list_chunks(
|
||||
db,
|
||||
page=page,
|
||||
per_page=per_page,
|
||||
source_news_id=source_news_id,
|
||||
has_embedding=has_embedding,
|
||||
is_verified=is_verified
|
||||
)
|
||||
|
||||
return render_template(
|
||||
'admin/zopk_knowledge_chunks.html',
|
||||
chunks=result['chunks'],
|
||||
total=result['total'],
|
||||
page=result['page'],
|
||||
per_page=result['per_page'],
|
||||
pages=result['pages'],
|
||||
source_news_id=source_news_id,
|
||||
has_embedding=has_embedding,
|
||||
is_verified=is_verified
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@app.route('/admin/zopk/knowledge/facts')
|
||||
@login_required
|
||||
def admin_zopk_knowledge_facts():
|
||||
"""
|
||||
List knowledge facts with pagination and filtering.
|
||||
"""
|
||||
if not current_user.is_admin:
|
||||
flash('Brak uprawnień do tej sekcji.', 'warning')
|
||||
return redirect(url_for('index'))
|
||||
|
||||
from zopk_knowledge_service import list_facts
|
||||
|
||||
# Get query params
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = request.args.get('per_page', 20, type=int)
|
||||
fact_type = request.args.get('fact_type')
|
||||
source_news_id = request.args.get('source_news_id', type=int)
|
||||
is_verified = request.args.get('is_verified')
|
||||
|
||||
# Convert string param to bool
|
||||
if is_verified is not None:
|
||||
is_verified = is_verified.lower() == 'true'
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
result = list_facts(
|
||||
db,
|
||||
page=page,
|
||||
per_page=per_page,
|
||||
fact_type=fact_type,
|
||||
is_verified=is_verified,
|
||||
source_news_id=source_news_id
|
||||
)
|
||||
|
||||
return render_template(
|
||||
'admin/zopk_knowledge_facts.html',
|
||||
facts=result['facts'],
|
||||
total=result['total'],
|
||||
page=result['page'],
|
||||
per_page=result['per_page'],
|
||||
pages=result['pages'],
|
||||
fact_types=result['fact_types'],
|
||||
current_fact_type=fact_type,
|
||||
source_news_id=source_news_id,
|
||||
is_verified=is_verified
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@app.route('/admin/zopk/knowledge/entities')
|
||||
@login_required
|
||||
def admin_zopk_knowledge_entities():
|
||||
"""
|
||||
List knowledge entities with pagination and filtering.
|
||||
"""
|
||||
if not current_user.is_admin:
|
||||
flash('Brak uprawnień do tej sekcji.', 'warning')
|
||||
return redirect(url_for('index'))
|
||||
|
||||
from zopk_knowledge_service import list_entities
|
||||
|
||||
# Get query params
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = request.args.get('per_page', 20, type=int)
|
||||
entity_type = request.args.get('entity_type')
|
||||
min_mentions = request.args.get('min_mentions', type=int)
|
||||
is_verified = request.args.get('is_verified')
|
||||
|
||||
# Convert string param to bool
|
||||
if is_verified is not None:
|
||||
is_verified = is_verified.lower() == 'true'
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
result = list_entities(
|
||||
db,
|
||||
page=page,
|
||||
per_page=per_page,
|
||||
entity_type=entity_type,
|
||||
is_verified=is_verified,
|
||||
min_mentions=min_mentions
|
||||
)
|
||||
|
||||
return render_template(
|
||||
'admin/zopk_knowledge_entities.html',
|
||||
entities=result['entities'],
|
||||
total=result['total'],
|
||||
page=result['page'],
|
||||
per_page=result['per_page'],
|
||||
pages=result['pages'],
|
||||
entity_types=result['entity_types'],
|
||||
current_entity_type=entity_type,
|
||||
min_mentions=min_mentions,
|
||||
is_verified=is_verified
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@app.route('/api/zopk/knowledge/chunks/<int:chunk_id>')
|
||||
@login_required
|
||||
def api_zopk_chunk_detail(chunk_id):
|
||||
"""Get detailed information about a chunk."""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
from zopk_knowledge_service import get_chunk_detail
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
chunk = get_chunk_detail(db, chunk_id)
|
||||
if not chunk:
|
||||
return jsonify({'success': False, 'error': 'Chunk nie znaleziony'}), 404
|
||||
|
||||
return jsonify({'success': True, 'chunk': chunk})
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@app.route('/api/zopk/knowledge/chunks/<int:chunk_id>/verify', methods=['POST'])
|
||||
@login_required
|
||||
def api_zopk_chunk_verify(chunk_id):
|
||||
"""Toggle chunk verification status."""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
from zopk_knowledge_service import update_chunk_verification
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
data = request.get_json() or {}
|
||||
is_verified = data.get('is_verified', True)
|
||||
|
||||
success = update_chunk_verification(db, chunk_id, is_verified, current_user.id)
|
||||
if not success:
|
||||
return jsonify({'success': False, 'error': 'Chunk nie znaleziony'}), 404
|
||||
|
||||
return jsonify({'success': True, 'is_verified': is_verified})
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@app.route('/api/zopk/knowledge/facts/<int:fact_id>/verify', methods=['POST'])
|
||||
@login_required
|
||||
def api_zopk_fact_verify(fact_id):
|
||||
"""Toggle fact verification status."""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
from zopk_knowledge_service import update_fact_verification
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
data = request.get_json() or {}
|
||||
is_verified = data.get('is_verified', True)
|
||||
|
||||
success = update_fact_verification(db, fact_id, is_verified)
|
||||
if not success:
|
||||
return jsonify({'success': False, 'error': 'Fakt nie znaleziony'}), 404
|
||||
|
||||
return jsonify({'success': True, 'is_verified': is_verified})
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@app.route('/api/zopk/knowledge/entities/<int:entity_id>/verify', methods=['POST'])
|
||||
@login_required
|
||||
def api_zopk_entity_verify(entity_id):
|
||||
"""Toggle entity verification status."""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
from zopk_knowledge_service import update_entity_verification
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
data = request.get_json() or {}
|
||||
is_verified = data.get('is_verified', True)
|
||||
|
||||
success = update_entity_verification(db, entity_id, is_verified)
|
||||
if not success:
|
||||
return jsonify({'success': False, 'error': 'Encja nie znaleziona'}), 404
|
||||
|
||||
return jsonify({'success': True, 'is_verified': is_verified})
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@app.route('/api/zopk/knowledge/chunks/<int:chunk_id>', methods=['DELETE'])
|
||||
@login_required
|
||||
def api_zopk_chunk_delete(chunk_id):
|
||||
"""Delete a chunk and its associated data."""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
from zopk_knowledge_service import delete_chunk
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
success = delete_chunk(db, chunk_id)
|
||||
if not success:
|
||||
return jsonify({'success': False, 'error': 'Chunk nie znaleziony'}), 404
|
||||
|
||||
return jsonify({'success': True, 'message': 'Chunk usunięty'})
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# KRS AUDIT (Krajowy Rejestr Sądowy)
|
||||
# ============================================================
|
||||
|
||||
56
database.py
56
database.py
@ -1841,6 +1841,62 @@ class ZOPKStakeholderProject(Base):
|
||||
)
|
||||
|
||||
|
||||
class ZOPKMilestone(Base):
|
||||
"""
|
||||
Timeline milestones for ZOPK projects.
|
||||
Tracks key events: announcements, decisions, construction, completions.
|
||||
Used for public timeline visualization on /zopk page.
|
||||
"""
|
||||
__tablename__ = 'zopk_milestones'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
|
||||
# Basic info
|
||||
title = Column(String(255), nullable=False)
|
||||
description = Column(Text)
|
||||
|
||||
# Categorization
|
||||
# Types: announcement, decision, construction_start, construction_progress,
|
||||
# completion, investment, agreement, regulation
|
||||
milestone_type = Column(String(50), nullable=False, default='announcement')
|
||||
|
||||
# Project association
|
||||
project_id = Column(Integer, ForeignKey('zopk_projects.id', ondelete='SET NULL'))
|
||||
|
||||
# Timeline
|
||||
target_date = Column(Date) # Planned/expected date
|
||||
actual_date = Column(Date) # Actual completion date (if completed)
|
||||
date_precision = Column(String(20), default='exact') # exact, month, quarter, year
|
||||
|
||||
# Status: planned, in_progress, completed, delayed, cancelled
|
||||
status = Column(String(20), nullable=False, default='planned')
|
||||
|
||||
# Source linking
|
||||
source_news_id = Column(Integer, ForeignKey('zopk_news.id', ondelete='SET NULL'))
|
||||
source_fact_id = Column(Integer, ForeignKey('zopk_knowledge_facts.id', ondelete='SET NULL'))
|
||||
source_url = Column(String(1000))
|
||||
|
||||
# Display settings
|
||||
icon = Column(String(50), default='📌')
|
||||
color = Column(String(7), default='#059669')
|
||||
is_featured = Column(Boolean, default=False)
|
||||
display_order = Column(Integer, default=0)
|
||||
|
||||
# Verification
|
||||
is_verified = Column(Boolean, default=False)
|
||||
verified_by = Column(Integer, ForeignKey('users.id'))
|
||||
verified_at = Column(DateTime)
|
||||
|
||||
# Timestamps
|
||||
created_at = Column(DateTime, default=datetime.now)
|
||||
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
# Relationships
|
||||
project = relationship('ZOPKProject', backref='milestones')
|
||||
# Note: source_news relationship defined in ZOPKNews class via backref
|
||||
verifier = relationship('User', foreign_keys=[verified_by])
|
||||
|
||||
|
||||
class ZOPKNews(Base):
|
||||
"""
|
||||
News articles about ZOPK with approval workflow.
|
||||
|
||||
75
database/migrations/016_zopk_milestones.sql
Normal file
75
database/migrations/016_zopk_milestones.sql
Normal file
@ -0,0 +1,75 @@
|
||||
-- ============================================================
|
||||
-- MIGRATION 016: ZOPK Milestones (Timeline)
|
||||
-- ============================================================
|
||||
-- Created: 2026-01-17
|
||||
-- Purpose: Add milestones table for ZOPK timeline visualization
|
||||
-- ============================================================
|
||||
|
||||
-- Milestones table for timeline
|
||||
CREATE TABLE IF NOT EXISTS zopk_milestones (
|
||||
id SERIAL PRIMARY KEY,
|
||||
|
||||
-- Basic info
|
||||
title VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
|
||||
-- Categorization
|
||||
milestone_type VARCHAR(50) NOT NULL DEFAULT 'announcement',
|
||||
-- Types: announcement, decision, construction_start, construction_progress,
|
||||
-- completion, investment, agreement, regulation
|
||||
|
||||
-- Project association (optional)
|
||||
project_id INTEGER REFERENCES zopk_projects(id) ON DELETE SET NULL,
|
||||
|
||||
-- Timeline
|
||||
target_date DATE, -- Planned/expected date
|
||||
actual_date DATE, -- Actual completion date (if completed)
|
||||
date_precision VARCHAR(20) DEFAULT 'exact', -- exact, month, quarter, year
|
||||
|
||||
-- Status
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'planned',
|
||||
-- Status: planned, in_progress, completed, delayed, cancelled
|
||||
|
||||
-- Source linking
|
||||
source_news_id INTEGER REFERENCES zopk_news(id) ON DELETE SET NULL,
|
||||
source_fact_id INTEGER REFERENCES zopk_knowledge_facts(id) ON DELETE SET NULL,
|
||||
source_url VARCHAR(1000), -- External source URL
|
||||
|
||||
-- Display settings
|
||||
icon VARCHAR(50) DEFAULT '📌',
|
||||
color VARCHAR(7) DEFAULT '#059669', -- HEX color
|
||||
is_featured BOOLEAN DEFAULT FALSE,
|
||||
display_order INTEGER DEFAULT 0,
|
||||
|
||||
-- Verification
|
||||
is_verified BOOLEAN DEFAULT FALSE,
|
||||
verified_by INTEGER REFERENCES users(id),
|
||||
verified_at TIMESTAMP,
|
||||
|
||||
-- Timestamps
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX IF NOT EXISTS idx_milestones_project ON zopk_milestones(project_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_milestones_target_date ON zopk_milestones(target_date);
|
||||
CREATE INDEX IF NOT EXISTS idx_milestones_status ON zopk_milestones(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_milestones_type ON zopk_milestones(milestone_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_milestones_featured ON zopk_milestones(is_featured) WHERE is_featured = TRUE;
|
||||
|
||||
-- Grant permissions
|
||||
GRANT ALL ON TABLE zopk_milestones TO nordabiz_app;
|
||||
GRANT USAGE, SELECT ON SEQUENCE zopk_milestones_id_seq TO nordabiz_app;
|
||||
|
||||
-- Insert sample milestones from known ZOPK events (all verified for display)
|
||||
INSERT INTO zopk_milestones (title, description, milestone_type, target_date, status, icon, color, is_featured, is_verified, display_order) VALUES
|
||||
('Podpisanie porozumienia MON-Kongsberg', 'Podpisanie porozumienia o współpracy między MON a Kongsberg Defence & Aerospace w zakresie budowy fabryki w Rumi', 'agreement', '2024-03-15', 'completed', '📝', '#059669', TRUE, TRUE, 10),
|
||||
('Pozwolenie środowiskowe Baltic Power', 'Uzyskanie pozwolenia środowiskowego dla morskiej farmy wiatrowej Baltic Power', 'regulation', '2025-06-15', 'completed', '📋', '#10b981', FALSE, TRUE, 40),
|
||||
('Rozpoczęcie budowy fabryki Kongsberg w Rumi', 'Start prac budowlanych zakładu produkcji dronów morskich w Rumi Invest Park', 'construction_start', '2025-09-01', 'in_progress', '🏗️', '#f59e0b', TRUE, TRUE, 30),
|
||||
('Decyzja lokalizacyjna elektrowni jądrowej', 'Wydanie decyzji lokalizacyjnej dla elektrowni jądrowej w Lubiatowie-Kopalino', 'decision', '2026-03-01', 'planned', '⚖️', '#3b82f6', TRUE, TRUE, 20),
|
||||
('Uruchomienie pierwszego bloku jądrowego', 'Planowane uruchomienie pierwszego bloku elektrowni jądrowej w Lubiatowie', 'completion', '2033-12-01', 'planned', '⚡', '#8b5cf6', TRUE, TRUE, 100)
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Comment
|
||||
COMMENT ON TABLE zopk_milestones IS 'Timeline milestones for ZOPK projects - used for public timeline visualization';
|
||||
@ -710,20 +710,24 @@ class NordaBizChatEngine:
|
||||
).first()
|
||||
if news:
|
||||
chunk_data['source'] = news.source_name or news.source_domain or 'nieznane'
|
||||
chunk_data['source_url'] = news.url or ''
|
||||
if news.published_at:
|
||||
chunk_data['date'] = news.published_at.strftime('%Y-%m-%d')
|
||||
|
||||
context['chunks'].append(chunk_data)
|
||||
|
||||
# Get relevant facts
|
||||
# Get relevant facts with source information
|
||||
facts = get_relevant_facts(db, query=message, limit=5)
|
||||
context['facts'] = [
|
||||
{
|
||||
'fact': f['full_text'],
|
||||
'type': f['fact_type'],
|
||||
'confidence': f.get('confidence_score', 0),
|
||||
'confidence': f.get('confidence', 0),
|
||||
'value': f.get('numeric_value'),
|
||||
'unit': f.get('numeric_unit')
|
||||
'unit': f.get('numeric_unit'),
|
||||
'source_url': f.get('source_url', ''),
|
||||
'source_name': f.get('source_name', ''),
|
||||
'source_date': f.get('source_date', '')
|
||||
}
|
||||
for f in facts
|
||||
]
|
||||
@ -956,25 +960,53 @@ BŁĘDNIE (NIE RÓB - resetuje numerację):
|
||||
system_prompt += "\n\n🌍 BAZA WIEDZY ZOPK (Zielony Okręg Przemysłowy Kaszubia):\n"
|
||||
system_prompt += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
|
||||
|
||||
# Collect all sources for citations at the end
|
||||
sources_for_citation = []
|
||||
|
||||
# Add knowledge chunks (most relevant excerpts)
|
||||
if zopk.get('chunks'):
|
||||
system_prompt += "\n📄 FRAGMENTY WIEDZY (semantycznie dopasowane):\n"
|
||||
for i, chunk in enumerate(zopk['chunks'][:5], 1):
|
||||
source_name = chunk.get('source', 'nieznane')
|
||||
source_url = chunk.get('source_url', '')
|
||||
source_date = chunk.get('date', '')
|
||||
|
||||
system_prompt += f"\n[{i}] {chunk.get('summary', '')}\n"
|
||||
system_prompt += f" Źródło: {chunk.get('source', 'nieznane')} ({chunk.get('date', '')})\n"
|
||||
if source_url:
|
||||
system_prompt += f" Źródło: [{source_name}]({source_url}) ({source_date})\n"
|
||||
if source_url and source_name:
|
||||
sources_for_citation.append({
|
||||
'name': source_name,
|
||||
'url': source_url,
|
||||
'date': source_date
|
||||
})
|
||||
else:
|
||||
system_prompt += f" Źródło: {source_name} ({source_date})\n"
|
||||
|
||||
if chunk.get('content'):
|
||||
# Skrócona treść (max 300 znaków)
|
||||
content_preview = chunk['content'][:300]
|
||||
if len(chunk['content']) > 300:
|
||||
content_preview += "..."
|
||||
system_prompt += f" Treść: {content_preview}\n"
|
||||
|
||||
# Add verified facts
|
||||
# Add verified facts with source links
|
||||
if zopk.get('facts'):
|
||||
system_prompt += "\n📌 ZWERYFIKOWANE FAKTY:\n"
|
||||
for fact in zopk['facts'][:10]:
|
||||
confidence_stars = "★" * int(fact.get('confidence', 0) * 5)
|
||||
system_prompt += f"• {fact.get('fact', '')} [{confidence_stars}]\n"
|
||||
source_name = fact.get('source_name', '')
|
||||
source_url = fact.get('source_url', '')
|
||||
source_date = fact.get('source_date', '')
|
||||
|
||||
system_prompt += f"• {fact.get('fact', '')} [{confidence_stars}]"
|
||||
if source_name and source_url:
|
||||
system_prompt += f" ([{source_name}]({source_url}), {source_date})"
|
||||
sources_for_citation.append({
|
||||
'name': source_name,
|
||||
'url': source_url,
|
||||
'date': source_date
|
||||
})
|
||||
system_prompt += "\n"
|
||||
if fact.get('value') and fact.get('unit'):
|
||||
system_prompt += f" Wartość: {fact['value']} {fact['unit']}\n"
|
||||
|
||||
@ -987,6 +1019,7 @@ BŁĘDNIE (NIE RÓB - resetuje numerację):
|
||||
'company': '🏢',
|
||||
'person': '👤',
|
||||
'location': '📍',
|
||||
'place': '📍',
|
||||
'project': '🎯',
|
||||
'technology': '⚡'
|
||||
}.get(entity.get('type', ''), '•')
|
||||
@ -997,12 +1030,26 @@ BŁĘDNIE (NIE RÓB - resetuje numerację):
|
||||
system_prompt += f" [{entity['mentions']} wzmianek]"
|
||||
system_prompt += "\n"
|
||||
|
||||
# Add available sources for citation
|
||||
if sources_for_citation:
|
||||
# Deduplicate sources by URL
|
||||
unique_sources = {s['url']: s for s in sources_for_citation if s.get('url')}.values()
|
||||
system_prompt += "\n📚 DOSTĘPNE ŹRÓDŁA DO CYTOWANIA:\n"
|
||||
for src in list(unique_sources)[:5]:
|
||||
system_prompt += f"- [{src['name']}]({src['url']}) ({src['date']})\n"
|
||||
|
||||
system_prompt += "\n🎯 ZASADY ODPOWIEDZI O ZOPK:\n"
|
||||
system_prompt += "1. Odpowiadaj na podstawie bazy wiedzy (nie wymyślaj faktów)\n"
|
||||
system_prompt += "2. Cytuj źródła: \"Według [portal] z [data]...\"\n"
|
||||
system_prompt += "3. Podawaj konkretne daty i liczby gdy dostępne\n"
|
||||
system_prompt += "4. Wymieniaj organizacje i osoby zaangażowane\n"
|
||||
system_prompt += "5. Jeśli brak informacji w bazie - powiedz wprost\n"
|
||||
system_prompt += "1. Odpowiadaj na podstawie bazy wiedzy (NIE WYMYŚLAJ faktów)\n"
|
||||
system_prompt += "2. FORMATUJ odpowiedzi używając:\n"
|
||||
system_prompt += " - **Pogrubienia** dla kluczowych informacji\n"
|
||||
system_prompt += " - Listy punktowane dla wielu faktów\n"
|
||||
system_prompt += " - Nagłówki dla sekcji (## Inwestycje, ## Terminarz)\n"
|
||||
system_prompt += "3. CYTUJ źródła w tekście: \"Według [nazwa portalu](URL) z dnia RRRR-MM-DD...\"\n"
|
||||
system_prompt += "4. NA KOŃCU odpowiedzi DODAJ sekcję:\n"
|
||||
system_prompt += " 📚 **Źródła:**\n"
|
||||
system_prompt += " - [Nazwa portalu](URL) - krótki opis (data)\n"
|
||||
system_prompt += "5. Podawaj konkretne daty i liczby gdy dostępne\n"
|
||||
system_prompt += "6. Jeśli brak informacji w bazie - powiedz wprost: \"Nie mam tej informacji w bazie wiedzy ZOPK\"\n"
|
||||
system_prompt += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
|
||||
|
||||
# Add upcoming events (Etap 2)
|
||||
|
||||
@ -1342,6 +1342,13 @@
|
||||
<h3 class="stats-section-title">Najczęściej wymieniane encje</h3>
|
||||
<div id="top-entities-list" class="entity-pills"></div>
|
||||
</div>
|
||||
|
||||
<!-- Link to detailed dashboard -->
|
||||
<div class="stats-section" style="text-align: center; padding-top: var(--spacing-lg);">
|
||||
<a href="{{ url_for('admin_zopk_knowledge_dashboard') }}" class="btn btn-primary">
|
||||
📊 Szczegółowy panel bazy wiedzy →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="knowledge-stats-error" style="display: none;" class="alert alert-danger">
|
||||
|
||||
644
templates/admin/zopk_knowledge_chunks.html
Normal file
644
templates/admin/zopk_knowledge_chunks.html
Normal file
@ -0,0 +1,644 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Chunks - Baza Wiedzy ZOPK{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-xl);
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
font-size: var(--font-size-2xl);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.breadcrumb a {
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.breadcrumb a:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.filters {
|
||||
display: flex;
|
||||
gap: var(--spacing-md);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
background: var(--surface);
|
||||
padding: var(--spacing-md);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.filter-btn {
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
text-decoration: none;
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
transition: var(--transition);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.filter-btn:hover {
|
||||
background: var(--background);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.filter-btn.active {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
overflow-x: auto;
|
||||
background: var(--surface);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.data-table {
|
||||
width: 100%;
|
||||
min-width: 800px;
|
||||
}
|
||||
|
||||
.data-table th,
|
||||
.data-table td {
|
||||
padding: var(--spacing-md);
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.data-table th {
|
||||
background: var(--background);
|
||||
font-weight: 600;
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.data-table tr:hover {
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
.chunk-content {
|
||||
max-width: 400px;
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.chunk-summary {
|
||||
color: var(--text-secondary);
|
||||
font-style: italic;
|
||||
font-size: var(--font-size-xs);
|
||||
margin-top: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.chunk-source {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.chunk-source a {
|
||||
color: var(--primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.chunk-source a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-verified { background: #d1fae5; color: #065f46; }
|
||||
.status-pending { background: #fef3c7; color: #92400e; }
|
||||
.status-embedding { background: #dbeafe; color: #1e40af; }
|
||||
.status-no-embedding { background: #fee2e2; color: #991b1b; }
|
||||
|
||||
.importance-stars {
|
||||
color: #fbbf24;
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
border-radius: var(--radius-sm);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: var(--font-size-xs);
|
||||
transition: var(--transition);
|
||||
margin-right: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.action-btn-view {
|
||||
background: var(--background);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.action-btn-view:hover {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.action-btn-verify {
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.action-btn-verify:hover {
|
||||
background: #065f46;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.action-btn-delete {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.action-btn-delete:hover {
|
||||
background: #991b1b;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: var(--spacing-xs);
|
||||
padding: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.pagination a,
|
||||
.pagination span {
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
border-radius: var(--radius);
|
||||
text-decoration: none;
|
||||
font-size: var(--font-size-sm);
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.pagination a:hover {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.pagination .current {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.pagination .disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.stats-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--spacing-md);
|
||||
background: var(--background);
|
||||
border-radius: var(--radius);
|
||||
margin-bottom: var(--spacing-md);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
.modal-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0,0,0,0.5);
|
||||
z-index: 1000;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal-overlay.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background: var(--surface);
|
||||
border-radius: var(--radius-lg);
|
||||
max-width: 700px;
|
||||
width: 90%;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
padding: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.modal-section {
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.modal-section-title {
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.modal-content-box {
|
||||
background: var(--background);
|
||||
padding: var(--spacing-md);
|
||||
border-radius: var(--radius);
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.keyword-tag {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: var(--font-size-xs);
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
.fact-item, .entity-item {
|
||||
padding: var(--spacing-sm);
|
||||
background: var(--background);
|
||||
border-radius: var(--radius);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="breadcrumb">
|
||||
<a href="{{ url_for('admin_dashboard') }}">Panel Admina</a>
|
||||
<span>›</span>
|
||||
<a href="{{ url_for('admin_zopk_dashboard') }}">ZOP Kaszubia</a>
|
||||
<span>›</span>
|
||||
<a href="{{ url_for('admin_zopk_knowledge_dashboard') }}">Baza Wiedzy</a>
|
||||
<span>›</span>
|
||||
<span>Chunks</span>
|
||||
</div>
|
||||
|
||||
<div class="page-header">
|
||||
<h1>📄 Chunks (Fragmenty tekstu)</h1>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="filters">
|
||||
<span style="color: var(--text-secondary); font-size: var(--font-size-sm);">Filtruj:</span>
|
||||
<a href="{{ url_for('admin_zopk_knowledge_chunks') }}"
|
||||
class="filter-btn {{ 'active' if has_embedding is none and is_verified is none else '' }}">
|
||||
Wszystkie
|
||||
</a>
|
||||
<a href="{{ url_for('admin_zopk_knowledge_chunks', has_embedding='true') }}"
|
||||
class="filter-btn {{ 'active' if has_embedding == true else '' }}">
|
||||
✓ Z embeddingiem
|
||||
</a>
|
||||
<a href="{{ url_for('admin_zopk_knowledge_chunks', has_embedding='false') }}"
|
||||
class="filter-btn {{ 'active' if has_embedding == false else '' }}">
|
||||
✗ Bez embeddingu
|
||||
</a>
|
||||
<a href="{{ url_for('admin_zopk_knowledge_chunks', is_verified='true') }}"
|
||||
class="filter-btn {{ 'active' if is_verified == true else '' }}">
|
||||
✓ Zweryfikowane
|
||||
</a>
|
||||
<a href="{{ url_for('admin_zopk_knowledge_chunks', is_verified='false') }}"
|
||||
class="filter-btn {{ 'active' if is_verified == false else '' }}">
|
||||
⏳ Oczekujące
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Stats Bar -->
|
||||
<div class="stats-bar">
|
||||
<span>Pokazuję {{ chunks|length }} z {{ total }} chunków (strona {{ page }} z {{ pages }})</span>
|
||||
<span>
|
||||
{% if source_news_id %}
|
||||
Źródło: Artykuł #{{ source_news_id }}
|
||||
<a href="{{ url_for('admin_zopk_knowledge_chunks') }}" style="margin-left: 8px; color: var(--primary);">✕ usuń filtr</a>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="table-wrapper">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Treść</th>
|
||||
<th>Typ</th>
|
||||
<th>Ważność</th>
|
||||
<th>Status</th>
|
||||
<th>Źródło</th>
|
||||
<th>Akcje</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for chunk in chunks %}
|
||||
<tr id="chunk-row-{{ chunk.id }}">
|
||||
<td>#{{ chunk.id }}</td>
|
||||
<td class="chunk-content">
|
||||
{{ chunk.content }}
|
||||
{% if chunk.summary %}
|
||||
<div class="chunk-summary">{{ chunk.summary }}</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if chunk.chunk_type %}
|
||||
<span class="status-badge">{{ chunk.chunk_type }}</span>
|
||||
{% else %}
|
||||
<span style="color: var(--text-tertiary);">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if chunk.importance_score %}
|
||||
<span class="importance-stars">
|
||||
{% for i in range(chunk.importance_score) %}★{% endfor %}{% for i in range(5 - chunk.importance_score) %}☆{% endfor %}
|
||||
</span>
|
||||
{% else %}
|
||||
<span style="color: var(--text-tertiary);">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if chunk.is_verified %}
|
||||
<span class="status-badge status-verified">✓ Zweryfikowany</span>
|
||||
{% else %}
|
||||
<span class="status-badge status-pending">⏳ Oczekuje</span>
|
||||
{% endif %}
|
||||
<br>
|
||||
{% if chunk.has_embedding %}
|
||||
<span class="status-badge status-embedding">🧲 Embedding</span>
|
||||
{% else %}
|
||||
<span class="status-badge status-no-embedding">✗ Brak</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="chunk-source">
|
||||
{% if chunk.source_news_id %}
|
||||
<a href="{{ url_for('admin_zopk_knowledge_chunks', source_news_id=chunk.source_news_id) }}">
|
||||
Artykuł #{{ chunk.source_news_id }}
|
||||
</a>
|
||||
{% if chunk.source_title %}
|
||||
<br>{{ chunk.source_title[:50] }}...
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<button class="action-btn action-btn-view" onclick="viewChunk({{ chunk.id }})">👁️</button>
|
||||
<button class="action-btn action-btn-verify" onclick="toggleVerify({{ chunk.id }}, {{ 'false' if chunk.is_verified else 'true' }})">
|
||||
{{ '✗' if chunk.is_verified else '✓' }}
|
||||
</button>
|
||||
<button class="action-btn action-btn-delete" onclick="deleteChunk({{ chunk.id }})">🗑️</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="7" style="text-align: center; padding: var(--spacing-xl); color: var(--text-secondary);">
|
||||
Brak chunków do wyświetlenia
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
{% if pages > 1 %}
|
||||
<div class="pagination">
|
||||
{% if page > 1 %}
|
||||
<a href="{{ url_for('admin_zopk_knowledge_chunks', page=page-1, has_embedding=has_embedding, is_verified=is_verified, source_news_id=source_news_id) }}">‹ Poprzednia</a>
|
||||
{% else %}
|
||||
<span class="disabled">‹ Poprzednia</span>
|
||||
{% endif %}
|
||||
|
||||
{% for p in range(1, pages + 1) %}
|
||||
{% if p == page %}
|
||||
<span class="current">{{ p }}</span>
|
||||
{% elif p <= 3 or p >= pages - 2 or (p >= page - 1 and p <= page + 1) %}
|
||||
<a href="{{ url_for('admin_zopk_knowledge_chunks', page=p, has_embedding=has_embedding, is_verified=is_verified, source_news_id=source_news_id) }}">{{ p }}</a>
|
||||
{% elif p == 4 or p == pages - 3 %}
|
||||
<span>...</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if page < pages %}
|
||||
<a href="{{ url_for('admin_zopk_knowledge_chunks', page=page+1, has_embedding=has_embedding, is_verified=is_verified, source_news_id=source_news_id) }}">Następna ›</a>
|
||||
{% else %}
|
||||
<span class="disabled">Następna ›</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Detail Modal -->
|
||||
<div class="modal-overlay" id="chunkModal">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<div class="modal-title">📄 Szczegóły chunka</div>
|
||||
<button class="modal-close" onclick="closeModal()">×</button>
|
||||
</div>
|
||||
<div id="modalContent">
|
||||
<div style="text-align: center; padding: 20px;">Ładowanie...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
async function viewChunk(id) {
|
||||
document.getElementById('chunkModal').classList.add('active');
|
||||
document.getElementById('modalContent').innerHTML = '<div style="text-align: center; padding: 20px;">Ładowanie...</div>';
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/zopk/knowledge/chunks/${id}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
renderChunkDetail(data.chunk);
|
||||
} else {
|
||||
document.getElementById('modalContent').innerHTML = `<div style="color: red;">Błąd: ${data.error}</div>`;
|
||||
}
|
||||
} catch (error) {
|
||||
document.getElementById('modalContent').innerHTML = `<div style="color: red;">Błąd: ${error.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderChunkDetail(chunk) {
|
||||
let html = `
|
||||
<div class="modal-section">
|
||||
<div class="modal-section-title">Treść</div>
|
||||
<div class="modal-content-box">${chunk.content}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (chunk.summary) {
|
||||
html += `
|
||||
<div class="modal-section">
|
||||
<div class="modal-section-title">Podsumowanie</div>
|
||||
<div class="modal-content-box">${chunk.summary}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (chunk.keywords && chunk.keywords.length) {
|
||||
html += `
|
||||
<div class="modal-section">
|
||||
<div class="modal-section-title">Słowa kluczowe</div>
|
||||
<div>${chunk.keywords.map(k => `<span class="keyword-tag">${k}</span>`).join('')}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (chunk.facts && chunk.facts.length) {
|
||||
html += `
|
||||
<div class="modal-section">
|
||||
<div class="modal-section-title">Fakty (${chunk.facts.length})</div>
|
||||
${chunk.facts.map(f => `<div class="fact-item">${f.full_text}</div>`).join('')}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (chunk.entity_mentions && chunk.entity_mentions.length) {
|
||||
html += `
|
||||
<div class="modal-section">
|
||||
<div class="modal-section-title">Encje (${chunk.entity_mentions.length})</div>
|
||||
${chunk.entity_mentions.map(e => `<div class="entity-item">${e.entity_name} (${e.entity_type})</div>`).join('')}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
html += `
|
||||
<div class="modal-section">
|
||||
<div class="modal-section-title">Metadane</div>
|
||||
<div class="modal-content-box">
|
||||
<strong>Typ:</strong> ${chunk.chunk_type || 'brak'}<br>
|
||||
<strong>Ważność:</strong> ${chunk.importance_score || 'brak'}/5<br>
|
||||
<strong>Pewność:</strong> ${chunk.confidence_score ? (chunk.confidence_score * 100).toFixed(0) + '%' : 'brak'}<br>
|
||||
<strong>Tokeny:</strong> ${chunk.token_count || 'brak'}<br>
|
||||
<strong>Model:</strong> ${chunk.extraction_model || 'brak'}<br>
|
||||
<strong>Embedding:</strong> ${chunk.has_embedding ? 'Tak' : 'Nie'}<br>
|
||||
<strong>Zweryfikowany:</strong> ${chunk.is_verified ? 'Tak' : 'Nie'}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (chunk.source_news) {
|
||||
html += `
|
||||
<div class="modal-section">
|
||||
<div class="modal-section-title">Źródło</div>
|
||||
<div class="modal-content-box">
|
||||
<strong>Artykuł:</strong> ${chunk.source_news.title}<br>
|
||||
<strong>Portal:</strong> ${chunk.source_news.source_name || 'nieznany'}<br>
|
||||
<a href="${chunk.source_news.url}" target="_blank">Otwórz źródło →</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
document.getElementById('modalContent').innerHTML = html;
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
document.getElementById('chunkModal').classList.remove('active');
|
||||
}
|
||||
|
||||
document.getElementById('chunkModal').addEventListener('click', function(e) {
|
||||
if (e.target === this) closeModal();
|
||||
});
|
||||
|
||||
async function toggleVerify(id, newState) {
|
||||
try {
|
||||
const response = await fetch(`/api/zopk/knowledge/chunks/${id}/verify`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': '{{ csrf_token() }}'
|
||||
},
|
||||
body: JSON.stringify({ is_verified: newState })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Błąd: ' + data.error);
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Błąd: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteChunk(id) {
|
||||
if (!confirm('Czy na pewno usunąć ten chunk? Zostaną również usunięte powiązane fakty i wzmianki.')) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/zopk/knowledge/chunks/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'X-CSRFToken': '{{ csrf_token() }}'
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
document.getElementById('chunk-row-' + id).remove();
|
||||
} else {
|
||||
alert('Błąd: ' + data.error);
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Błąd: ' + error.message);
|
||||
}
|
||||
}
|
||||
{% endblock %}
|
||||
525
templates/admin/zopk_knowledge_dashboard.html
Normal file
525
templates/admin/zopk_knowledge_dashboard.html
Normal file
@ -0,0 +1,525 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Baza Wiedzy ZOPK - Panel Admina{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-xl);
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
font-size: var(--font-size-2xl);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: var(--spacing-lg);
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: var(--surface);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-lg);
|
||||
box-shadow: var(--shadow);
|
||||
text-align: center;
|
||||
transition: var(--transition);
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.stat-card.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: var(--font-size-2xl);
|
||||
font-weight: 700;
|
||||
color: var(--primary);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.stat-sublabel {
|
||||
color: var(--text-tertiary);
|
||||
font-size: var(--font-size-xs);
|
||||
margin-top: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.section {
|
||||
background: var(--surface);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-lg);
|
||||
box-shadow: var(--shadow);
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: 600;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.quick-links {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.quick-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
padding: var(--spacing-md);
|
||||
background: var(--background);
|
||||
border-radius: var(--radius);
|
||||
text-decoration: none;
|
||||
color: var(--text-primary);
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.quick-link:hover {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.quick-link-icon {
|
||||
font-size: 1.5rem;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--surface);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.quick-link:hover .quick-link-icon {
|
||||
background: rgba(255,255,255,0.2);
|
||||
}
|
||||
|
||||
.quick-link-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.quick-link-title {
|
||||
font-weight: 600;
|
||||
font-size: var(--font-size-base);
|
||||
}
|
||||
|
||||
.quick-link-desc {
|
||||
font-size: var(--font-size-xs);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.top-entities-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.entity-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
padding: var(--spacing-sm);
|
||||
background: var(--background);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.entity-type-badge {
|
||||
padding: 2px 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.entity-type-company { background: #dbeafe; color: #1e40af; }
|
||||
.entity-type-person { background: #fce7f3; color: #be185d; }
|
||||
.entity-type-place { background: #d1fae5; color: #065f46; }
|
||||
.entity-type-organization { background: #fef3c7; color: #92400e; }
|
||||
.entity-type-project { background: #e0e7ff; color: #3730a3; }
|
||||
.entity-type-technology { background: #f3e8ff; color: #7c3aed; }
|
||||
|
||||
.entity-mentions {
|
||||
margin-left: auto;
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--spacing-xl);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.loading::after {
|
||||
content: '';
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid var(--border);
|
||||
border-top-color: var(--primary);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-left: var(--spacing-sm);
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
border-radius: var(--radius);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 500;
|
||||
transition: var(--transition);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.action-btn-primary {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.action-btn-primary:hover {
|
||||
background: var(--primary-dark);
|
||||
}
|
||||
|
||||
.action-btn-secondary {
|
||||
background: var(--background);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.action-btn-secondary:hover {
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.pipeline-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
background: var(--background);
|
||||
border-radius: var(--radius);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.pipeline-status.running {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.pipeline-status.success {
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.breadcrumb a {
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.breadcrumb a:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="breadcrumb">
|
||||
<a href="{{ url_for('admin_dashboard') }}">Panel Admina</a>
|
||||
<span>›</span>
|
||||
<a href="{{ url_for('admin_zopk_dashboard') }}">ZOP Kaszubia</a>
|
||||
<span>›</span>
|
||||
<span>Baza Wiedzy</span>
|
||||
</div>
|
||||
|
||||
<div class="page-header">
|
||||
<h1>🧠 Baza Wiedzy ZOPK</h1>
|
||||
<div class="header-actions">
|
||||
<div id="pipelineStatus" class="pipeline-status">
|
||||
<span>⏳</span>
|
||||
<span>Ładowanie...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats Grid -->
|
||||
<div class="stats-grid" id="statsGrid">
|
||||
<div class="loading">Ładowanie statystyk...</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Links -->
|
||||
<div class="section">
|
||||
<h2 class="section-title">📋 Przegląd danych</h2>
|
||||
<div class="quick-links">
|
||||
<a href="{{ url_for('admin_zopk_knowledge_chunks') }}" class="quick-link">
|
||||
<div class="quick-link-icon">📄</div>
|
||||
<div class="quick-link-text">
|
||||
<div class="quick-link-title">Chunks (Fragmenty)</div>
|
||||
<div class="quick-link-desc">Fragmenty tekstu z embeddingami</div>
|
||||
</div>
|
||||
</a>
|
||||
<a href="{{ url_for('admin_zopk_knowledge_facts') }}" class="quick-link">
|
||||
<div class="quick-link-icon">📌</div>
|
||||
<div class="quick-link-text">
|
||||
<div class="quick-link-title">Fakty</div>
|
||||
<div class="quick-link-desc">Wyekstraktowane fakty strukturalne</div>
|
||||
</div>
|
||||
</a>
|
||||
<a href="{{ url_for('admin_zopk_knowledge_entities') }}" class="quick-link">
|
||||
<div class="quick-link-icon">🏢</div>
|
||||
<div class="quick-link-text">
|
||||
<div class="quick-link-title">Encje</div>
|
||||
<div class="quick-link-desc">Firmy, osoby, miejsca, projekty</div>
|
||||
</div>
|
||||
</a>
|
||||
<a href="{{ url_for('admin_zopk_news') }}" class="quick-link">
|
||||
<div class="quick-link-icon">📰</div>
|
||||
<div class="quick-link-text">
|
||||
<div class="quick-link-title">Źródłowe artykuły</div>
|
||||
<div class="quick-link-desc">Newsy ZOPK (źródło wiedzy)</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Top Entities -->
|
||||
<div class="section">
|
||||
<h2 class="section-title">🏆 Top 10 encji według wzmianek</h2>
|
||||
<div id="topEntities" class="top-entities-grid">
|
||||
<div class="loading">Ładowanie encji...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="section">
|
||||
<h2 class="section-title">⚡ Akcje</h2>
|
||||
<div class="quick-links">
|
||||
<div class="quick-link" onclick="runExtraction()" style="cursor: pointer;">
|
||||
<div class="quick-link-icon">🔄</div>
|
||||
<div class="quick-link-text">
|
||||
<div class="quick-link-title">Uruchom ekstrakcję</div>
|
||||
<div class="quick-link-desc">Przetwórz nowe artykuły</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="quick-link" onclick="generateEmbeddings()" style="cursor: pointer;">
|
||||
<div class="quick-link-icon">🧲</div>
|
||||
<div class="quick-link-text">
|
||||
<div class="quick-link-title">Generuj embeddingi</div>
|
||||
<div class="quick-link-desc">Wektory dla chunków bez embeddingów</div>
|
||||
</div>
|
||||
</div>
|
||||
<a href="{{ url_for('admin_zopk_dashboard') }}" class="quick-link">
|
||||
<div class="quick-link-icon">📊</div>
|
||||
<div class="quick-link-text">
|
||||
<div class="quick-link-title">Dashboard ZOPK</div>
|
||||
<div class="quick-link-desc">Powrót do głównego panelu</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
// Load stats on page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadStats();
|
||||
});
|
||||
|
||||
async function loadStats() {
|
||||
try {
|
||||
const response = await fetch('/admin/zopk/knowledge/stats');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
renderStats(data);
|
||||
renderTopEntities(data.top_entities || []);
|
||||
updatePipelineStatus(data);
|
||||
} else {
|
||||
document.getElementById('statsGrid').innerHTML = '<div class="loading">Błąd ładowania: ' + data.error + '</div>';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading stats:', error);
|
||||
document.getElementById('statsGrid').innerHTML = '<div class="loading">Błąd połączenia</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderStats(data) {
|
||||
const articles = data.articles || {};
|
||||
const kb = data.knowledge_base || {};
|
||||
|
||||
const statsHtml = `
|
||||
<a href="{{ url_for('admin_zopk_knowledge_chunks') }}" class="stat-card clickable">
|
||||
<div class="stat-icon">📄</div>
|
||||
<div class="stat-value">${kb.total_chunks || 0}</div>
|
||||
<div class="stat-label">Chunks</div>
|
||||
<div class="stat-sublabel">${kb.chunks_with_embeddings || 0} z embeddingami</div>
|
||||
</a>
|
||||
<a href="{{ url_for('admin_zopk_knowledge_facts') }}" class="stat-card clickable">
|
||||
<div class="stat-icon">📌</div>
|
||||
<div class="stat-value">${kb.total_facts || 0}</div>
|
||||
<div class="stat-label">Fakty</div>
|
||||
<div class="stat-sublabel">Wyekstraktowane informacje</div>
|
||||
</a>
|
||||
<a href="{{ url_for('admin_zopk_knowledge_entities') }}" class="stat-card clickable">
|
||||
<div class="stat-icon">🏢</div>
|
||||
<div class="stat-value">${kb.total_entities || 0}</div>
|
||||
<div class="stat-label">Encje</div>
|
||||
<div class="stat-sublabel">Firmy, osoby, miejsca</div>
|
||||
</a>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">🔗</div>
|
||||
<div class="stat-value">${kb.total_relations || 0}</div>
|
||||
<div class="stat-label">Relacje</div>
|
||||
<div class="stat-sublabel">Powiązania między encjami</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">📰</div>
|
||||
<div class="stat-value">${articles.extracted || 0}/${articles.scraped || 0}</div>
|
||||
<div class="stat-label">Artykuły przetworzone</div>
|
||||
<div class="stat-sublabel">${articles.pending_extract || 0} oczekuje</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">🧲</div>
|
||||
<div class="stat-value">${kb.chunks_with_embeddings || 0}/${kb.total_chunks || 0}</div>
|
||||
<div class="stat-label">Embeddingi</div>
|
||||
<div class="stat-sublabel">${kb.chunks_without_embeddings || 0} do wygenerowania</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById('statsGrid').innerHTML = statsHtml;
|
||||
}
|
||||
|
||||
function renderTopEntities(entities) {
|
||||
if (!entities.length) {
|
||||
document.getElementById('topEntities').innerHTML = '<div class="loading">Brak encji w bazie</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const html = entities.map(e => `
|
||||
<div class="entity-item">
|
||||
<span class="entity-type-badge entity-type-${e.type}">${e.type}</span>
|
||||
<span>${e.name}</span>
|
||||
<span class="entity-mentions">${e.mentions}×</span>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
document.getElementById('topEntities').innerHTML = html;
|
||||
}
|
||||
|
||||
function updatePipelineStatus(data) {
|
||||
const articles = data.articles || {};
|
||||
const kb = data.knowledge_base || {};
|
||||
|
||||
let status = 'success';
|
||||
let text = 'Pipeline OK';
|
||||
|
||||
if (articles.pending_extract > 0) {
|
||||
status = 'running';
|
||||
text = `${articles.pending_extract} artykułów czeka na ekstrakcję`;
|
||||
} else if (kb.chunks_without_embeddings > 0) {
|
||||
status = 'running';
|
||||
text = `${kb.chunks_without_embeddings} chunków bez embeddingów`;
|
||||
}
|
||||
|
||||
document.getElementById('pipelineStatus').className = 'pipeline-status ' + status;
|
||||
document.getElementById('pipelineStatus').innerHTML = `
|
||||
<span>${status === 'success' ? '✅' : '⏳'}</span>
|
||||
<span>${text}</span>
|
||||
`;
|
||||
}
|
||||
|
||||
async function runExtraction() {
|
||||
if (!confirm('Uruchomić ekstrakcję wiedzy z artykułów?')) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/admin/zopk/knowledge/extract', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': '{{ csrf_token() }}'
|
||||
},
|
||||
body: JSON.stringify({ limit: 50 })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
alert(data.message || data.error);
|
||||
loadStats();
|
||||
} catch (error) {
|
||||
alert('Błąd: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function generateEmbeddings() {
|
||||
if (!confirm('Wygenerować embeddingi dla chunków?')) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/admin/zopk/knowledge/embeddings', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': '{{ csrf_token() }}'
|
||||
},
|
||||
body: JSON.stringify({ limit: 100 })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
alert(data.message || data.error);
|
||||
loadStats();
|
||||
} catch (error) {
|
||||
alert('Błąd: ' + error.message);
|
||||
}
|
||||
}
|
||||
{% endblock %}
|
||||
446
templates/admin/zopk_knowledge_entities.html
Normal file
446
templates/admin/zopk_knowledge_entities.html
Normal file
@ -0,0 +1,446 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Encje - Baza Wiedzy ZOPK{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-xl);
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
font-size: var(--font-size-2xl);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.breadcrumb a {
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.breadcrumb a:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.filters {
|
||||
display: flex;
|
||||
gap: var(--spacing-md);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
background: var(--surface);
|
||||
padding: var(--spacing-md);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.filter-btn {
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
text-decoration: none;
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
transition: var(--transition);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.filter-btn:hover {
|
||||
background: var(--background);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.filter-btn.active {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
overflow-x: auto;
|
||||
background: var(--surface);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.data-table {
|
||||
width: 100%;
|
||||
min-width: 800px;
|
||||
}
|
||||
|
||||
.data-table th,
|
||||
.data-table td {
|
||||
padding: var(--spacing-md);
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.data-table th {
|
||||
background: var(--background);
|
||||
font-weight: 600;
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.data-table tr:hover {
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
.entity-name {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.entity-description {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--text-secondary);
|
||||
margin-top: var(--spacing-xs);
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.entity-aliases {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--text-tertiary);
|
||||
margin-top: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.entity-type-badge {
|
||||
padding: 2px 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.entity-type-company { background: #dbeafe; color: #1e40af; }
|
||||
.entity-type-person { background: #fce7f3; color: #be185d; }
|
||||
.entity-type-place { background: #d1fae5; color: #065f46; }
|
||||
.entity-type-organization { background: #fef3c7; color: #92400e; }
|
||||
.entity-type-project { background: #e0e7ff; color: #3730a3; }
|
||||
.entity-type-technology { background: #f3e8ff; color: #7c3aed; }
|
||||
.entity-type-event { background: #fce7f3; color: #be185d; }
|
||||
|
||||
.mentions-count {
|
||||
font-weight: 600;
|
||||
font-size: var(--font-size-lg);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.mentions-bar {
|
||||
width: 100px;
|
||||
height: 6px;
|
||||
background: var(--border);
|
||||
border-radius: 3px;
|
||||
margin-top: var(--spacing-xs);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mentions-bar-fill {
|
||||
height: 100%;
|
||||
background: var(--primary);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-verified { background: #d1fae5; color: #065f46; }
|
||||
.status-pending { background: #fef3c7; color: #92400e; }
|
||||
|
||||
.action-btn {
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
border-radius: var(--radius-sm);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: var(--font-size-xs);
|
||||
transition: var(--transition);
|
||||
margin-right: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.action-btn-verify {
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.action-btn-verify:hover {
|
||||
background: #065f46;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.action-btn-link {
|
||||
background: #dbeafe;
|
||||
color: #1e40af;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.action-btn-link:hover {
|
||||
background: #1e40af;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: var(--spacing-xs);
|
||||
padding: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.pagination a,
|
||||
.pagination span {
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
border-radius: var(--radius);
|
||||
text-decoration: none;
|
||||
font-size: var(--font-size-sm);
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.pagination a:hover {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.pagination .current {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.pagination .disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.stats-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--spacing-md);
|
||||
background: var(--background);
|
||||
border-radius: var(--radius);
|
||||
margin-bottom: var(--spacing-md);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.entity-dates {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="breadcrumb">
|
||||
<a href="{{ url_for('admin_dashboard') }}">Panel Admina</a>
|
||||
<span>›</span>
|
||||
<a href="{{ url_for('admin_zopk_dashboard') }}">ZOP Kaszubia</a>
|
||||
<span>›</span>
|
||||
<a href="{{ url_for('admin_zopk_knowledge_dashboard') }}">Baza Wiedzy</a>
|
||||
<span>›</span>
|
||||
<span>Encje</span>
|
||||
</div>
|
||||
|
||||
<div class="page-header">
|
||||
<h1>🏢 Encje (rozpoznane byty)</h1>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="filters">
|
||||
<span style="color: var(--text-secondary); font-size: var(--font-size-sm);">Typ encji:</span>
|
||||
<a href="{{ url_for('admin_zopk_knowledge_entities') }}"
|
||||
class="filter-btn {{ 'active' if current_entity_type is none else '' }}">
|
||||
Wszystkie
|
||||
</a>
|
||||
{% for etype in entity_types %}
|
||||
<a href="{{ url_for('admin_zopk_knowledge_entities', entity_type=etype) }}"
|
||||
class="filter-btn {{ 'active' if current_entity_type == etype else '' }}">
|
||||
{% if etype == 'company' %}🏢{% elif etype == 'person' %}👤{% elif etype == 'place' %}📍{% elif etype == 'organization' %}🏛️{% elif etype == 'project' %}🚀{% elif etype == 'technology' %}💻{% else %}📋{% endif %}
|
||||
{{ etype }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
|
||||
<span style="margin-left: 20px; color: var(--text-secondary); font-size: var(--font-size-sm);">Status:</span>
|
||||
<a href="{{ url_for('admin_zopk_knowledge_entities', is_verified='true', entity_type=current_entity_type) }}"
|
||||
class="filter-btn {{ 'active' if is_verified == true else '' }}">
|
||||
✓ Zweryfikowane
|
||||
</a>
|
||||
<a href="{{ url_for('admin_zopk_knowledge_entities', is_verified='false', entity_type=current_entity_type) }}"
|
||||
class="filter-btn {{ 'active' if is_verified == false else '' }}">
|
||||
⏳ Oczekujące
|
||||
</a>
|
||||
|
||||
<span style="margin-left: 20px; color: var(--text-secondary); font-size: var(--font-size-sm);">Min. wzmianek:</span>
|
||||
<a href="{{ url_for('admin_zopk_knowledge_entities', min_mentions=5, entity_type=current_entity_type) }}"
|
||||
class="filter-btn {{ 'active' if min_mentions == 5 else '' }}">
|
||||
5+
|
||||
</a>
|
||||
<a href="{{ url_for('admin_zopk_knowledge_entities', min_mentions=10, entity_type=current_entity_type) }}"
|
||||
class="filter-btn {{ 'active' if min_mentions == 10 else '' }}">
|
||||
10+
|
||||
</a>
|
||||
<a href="{{ url_for('admin_zopk_knowledge_entities', min_mentions=20, entity_type=current_entity_type) }}"
|
||||
class="filter-btn {{ 'active' if min_mentions == 20 else '' }}">
|
||||
20+
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Stats Bar -->
|
||||
<div class="stats-bar">
|
||||
<span>Pokazuję {{ entities|length }} z {{ total }} encji (strona {{ page }} z {{ pages }})</span>
|
||||
<span>Posortowane według liczby wzmianek (malejąco)</span>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="table-wrapper">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Nazwa</th>
|
||||
<th>Typ</th>
|
||||
<th>Wzmianki</th>
|
||||
<th>Status</th>
|
||||
<th>Daty</th>
|
||||
<th>Akcje</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% set max_mentions = entities[0].mentions_count if entities else 1 %}
|
||||
{% for entity in entities %}
|
||||
<tr id="entity-row-{{ entity.id }}">
|
||||
<td>#{{ entity.id }}</td>
|
||||
<td>
|
||||
<div class="entity-name">{{ entity.name }}</div>
|
||||
{% if entity.short_description %}
|
||||
<div class="entity-description">{{ entity.short_description }}</div>
|
||||
{% endif %}
|
||||
{% if entity.aliases and entity.aliases|length %}
|
||||
<div class="entity-aliases">
|
||||
Aliasy: {{ entity.aliases|join(', ') }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<span class="entity-type-badge entity-type-{{ entity.entity_type }}">
|
||||
{% if entity.entity_type == 'company' %}🏢{% elif entity.entity_type == 'person' %}👤{% elif entity.entity_type == 'place' %}📍{% elif entity.entity_type == 'organization' %}🏛️{% elif entity.entity_type == 'project' %}🚀{% elif entity.entity_type == 'technology' %}💻{% else %}📋{% endif %}
|
||||
{{ entity.entity_type }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="mentions-count">{{ entity.mentions_count }}</div>
|
||||
<div class="mentions-bar">
|
||||
<div class="mentions-bar-fill" style="width: {{ (entity.mentions_count / max_mentions * 100)|round }}%"></div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
{% if entity.is_verified %}
|
||||
<span class="status-badge status-verified">✓ Zweryfikowana</span>
|
||||
{% else %}
|
||||
<span class="status-badge status-pending">⏳ Oczekuje</span>
|
||||
{% endif %}
|
||||
{% if entity.company_id %}
|
||||
<br><a href="{{ url_for('company_detail', slug=entity.company_id) }}" class="action-btn-link" style="font-size: 10px;">→ Firma Norda</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="entity-dates">
|
||||
{% if entity.first_mentioned_at %}
|
||||
<div>Pierwsza: {{ entity.first_mentioned_at[:10] }}</div>
|
||||
{% endif %}
|
||||
{% if entity.last_mentioned_at %}
|
||||
<div>Ostatnia: {{ entity.last_mentioned_at[:10] }}</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<button class="action-btn action-btn-verify" onclick="toggleVerify({{ entity.id }}, {{ 'false' if entity.is_verified else 'true' }})">
|
||||
{{ '✗' if entity.is_verified else '✓' }}
|
||||
</button>
|
||||
{% if entity.external_url %}
|
||||
<a href="{{ entity.external_url }}" target="_blank" class="action-btn action-btn-link">🔗</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="7" style="text-align: center; padding: var(--spacing-xl); color: var(--text-secondary);">
|
||||
Brak encji do wyświetlenia
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
{% if pages > 1 %}
|
||||
<div class="pagination">
|
||||
{% if page > 1 %}
|
||||
<a href="{{ url_for('admin_zopk_knowledge_entities', page=page-1, entity_type=current_entity_type, is_verified=is_verified, min_mentions=min_mentions) }}">‹ Poprzednia</a>
|
||||
{% else %}
|
||||
<span class="disabled">‹ Poprzednia</span>
|
||||
{% endif %}
|
||||
|
||||
{% for p in range(1, pages + 1) %}
|
||||
{% if p == page %}
|
||||
<span class="current">{{ p }}</span>
|
||||
{% elif p <= 3 or p >= pages - 2 or (p >= page - 1 and p <= page + 1) %}
|
||||
<a href="{{ url_for('admin_zopk_knowledge_entities', page=p, entity_type=current_entity_type, is_verified=is_verified, min_mentions=min_mentions) }}">{{ p }}</a>
|
||||
{% elif p == 4 or p == pages - 3 %}
|
||||
<span>...</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if page < pages %}
|
||||
<a href="{{ url_for('admin_zopk_knowledge_entities', page=page+1, entity_type=current_entity_type, is_verified=is_verified, min_mentions=min_mentions) }}">Następna ›</a>
|
||||
{% else %}
|
||||
<span class="disabled">Następna ›</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
async function toggleVerify(id, newState) {
|
||||
try {
|
||||
const response = await fetch(`/api/zopk/knowledge/entities/${id}/verify`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': '{{ csrf_token() }}'
|
||||
},
|
||||
body: JSON.stringify({ is_verified: newState })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Błąd: ' + data.error);
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Błąd: ' + error.message);
|
||||
}
|
||||
}
|
||||
{% endblock %}
|
||||
441
templates/admin/zopk_knowledge_facts.html
Normal file
441
templates/admin/zopk_knowledge_facts.html
Normal file
@ -0,0 +1,441 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Fakty - Baza Wiedzy ZOPK{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-xl);
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
font-size: var(--font-size-2xl);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.breadcrumb a {
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.breadcrumb a:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.filters {
|
||||
display: flex;
|
||||
gap: var(--spacing-md);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
background: var(--surface);
|
||||
padding: var(--spacing-md);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.filter-btn {
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
text-decoration: none;
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
transition: var(--transition);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.filter-btn:hover {
|
||||
background: var(--background);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.filter-btn.active {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
overflow-x: auto;
|
||||
background: var(--surface);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.data-table {
|
||||
width: 100%;
|
||||
min-width: 900px;
|
||||
}
|
||||
|
||||
.data-table th,
|
||||
.data-table td {
|
||||
padding: var(--spacing-md);
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.data-table th {
|
||||
background: var(--background);
|
||||
font-weight: 600;
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.data-table tr:hover {
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
.fact-text {
|
||||
max-width: 400px;
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.fact-structure {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--text-secondary);
|
||||
margin-top: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.fact-structure strong {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.fact-source {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.fact-source a {
|
||||
color: var(--primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.fact-source a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-verified { background: #d1fae5; color: #065f46; }
|
||||
.status-pending { background: #fef3c7; color: #92400e; }
|
||||
|
||||
.fact-type-badge {
|
||||
padding: 2px 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.fact-type-statistic { background: #dbeafe; color: #1e40af; }
|
||||
.fact-type-event { background: #fce7f3; color: #be185d; }
|
||||
.fact-type-statement { background: #e0e7ff; color: #3730a3; }
|
||||
.fact-type-decision { background: #d1fae5; color: #065f46; }
|
||||
.fact-type-milestone { background: #fef3c7; color: #92400e; }
|
||||
.fact-type-financial { background: #fef3c7; color: #92400e; }
|
||||
.fact-type-date { background: #f3e8ff; color: #7c3aed; }
|
||||
|
||||
.numeric-value {
|
||||
font-weight: 600;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
border-radius: var(--radius-sm);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: var(--font-size-xs);
|
||||
transition: var(--transition);
|
||||
margin-right: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.action-btn-verify {
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.action-btn-verify:hover {
|
||||
background: #065f46;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: var(--spacing-xs);
|
||||
padding: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.pagination a,
|
||||
.pagination span {
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
border-radius: var(--radius);
|
||||
text-decoration: none;
|
||||
font-size: var(--font-size-sm);
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.pagination a:hover {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.pagination .current {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.pagination .disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.stats-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--spacing-md);
|
||||
background: var(--background);
|
||||
border-radius: var(--radius);
|
||||
margin-bottom: var(--spacing-md);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.entity-tag {
|
||||
display: inline-block;
|
||||
padding: 1px 6px;
|
||||
background: var(--background);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: var(--font-size-xs);
|
||||
margin: 1px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="breadcrumb">
|
||||
<a href="{{ url_for('admin_dashboard') }}">Panel Admina</a>
|
||||
<span>›</span>
|
||||
<a href="{{ url_for('admin_zopk_dashboard') }}">ZOP Kaszubia</a>
|
||||
<span>›</span>
|
||||
<a href="{{ url_for('admin_zopk_knowledge_dashboard') }}">Baza Wiedzy</a>
|
||||
<span>›</span>
|
||||
<span>Fakty</span>
|
||||
</div>
|
||||
|
||||
<div class="page-header">
|
||||
<h1>📌 Fakty (wyekstraktowane informacje)</h1>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="filters">
|
||||
<span style="color: var(--text-secondary); font-size: var(--font-size-sm);">Typ faktu:</span>
|
||||
<a href="{{ url_for('admin_zopk_knowledge_facts') }}"
|
||||
class="filter-btn {{ 'active' if current_fact_type is none else '' }}">
|
||||
Wszystkie
|
||||
</a>
|
||||
{% for ftype in fact_types %}
|
||||
<a href="{{ url_for('admin_zopk_knowledge_facts', fact_type=ftype) }}"
|
||||
class="filter-btn {{ 'active' if current_fact_type == ftype else '' }}">
|
||||
{{ ftype }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
|
||||
<span style="margin-left: 20px; color: var(--text-secondary); font-size: var(--font-size-sm);">Status:</span>
|
||||
<a href="{{ url_for('admin_zopk_knowledge_facts', is_verified='true', fact_type=current_fact_type) }}"
|
||||
class="filter-btn {{ 'active' if is_verified == true else '' }}">
|
||||
✓ Zweryfikowane
|
||||
</a>
|
||||
<a href="{{ url_for('admin_zopk_knowledge_facts', is_verified='false', fact_type=current_fact_type) }}"
|
||||
class="filter-btn {{ 'active' if is_verified == false else '' }}">
|
||||
⏳ Oczekujące
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Stats Bar -->
|
||||
<div class="stats-bar">
|
||||
<span>Pokazuję {{ facts|length }} z {{ total }} faktów (strona {{ page }} z {{ pages }})</span>
|
||||
<span>
|
||||
{% if source_news_id %}
|
||||
Źródło: Artykuł #{{ source_news_id }}
|
||||
<a href="{{ url_for('admin_zopk_knowledge_facts') }}" style="margin-left: 8px; color: var(--primary);">✕ usuń filtr</a>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="table-wrapper">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Fakt</th>
|
||||
<th>Typ</th>
|
||||
<th>Wartość</th>
|
||||
<th>Encje</th>
|
||||
<th>Status</th>
|
||||
<th>Źródło</th>
|
||||
<th>Akcje</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for fact in facts %}
|
||||
<tr id="fact-row-{{ fact.id }}">
|
||||
<td>#{{ fact.id }}</td>
|
||||
<td class="fact-text">
|
||||
{{ fact.full_text }}
|
||||
{% if fact.subject or fact.predicate or fact.object %}
|
||||
<div class="fact-structure">
|
||||
{% if fact.subject %}<strong>Podmiot:</strong> {{ fact.subject }}{% endif %}
|
||||
{% if fact.predicate %}<strong>Relacja:</strong> {{ fact.predicate }}{% endif %}
|
||||
{% if fact.object %}<strong>Obiekt:</strong> {{ fact.object[:50] }}{% if fact.object|length > 50 %}...{% endif %}{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if fact.fact_type %}
|
||||
<span class="fact-type-badge fact-type-{{ fact.fact_type }}">{{ fact.fact_type }}</span>
|
||||
{% else %}
|
||||
<span style="color: var(--text-tertiary);">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if fact.numeric_value %}
|
||||
<span class="numeric-value">{{ fact.numeric_value|round(2) }}</span>
|
||||
{% if fact.numeric_unit %}{{ fact.numeric_unit }}{% endif %}
|
||||
{% elif fact.date_value %}
|
||||
<span class="numeric-value">{{ fact.date_value }}</span>
|
||||
{% else %}
|
||||
<span style="color: var(--text-tertiary);">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if fact.entities_involved %}
|
||||
{% for e in fact.entities_involved[:3] %}
|
||||
<span class="entity-tag">{{ e.name if e.name else e }}</span>
|
||||
{% endfor %}
|
||||
{% if fact.entities_involved|length > 3 %}
|
||||
<span class="entity-tag">+{{ fact.entities_involved|length - 3 }}</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span style="color: var(--text-tertiary);">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if fact.is_verified %}
|
||||
<span class="status-badge status-verified">✓ Zweryfikowany</span>
|
||||
{% else %}
|
||||
<span class="status-badge status-pending">⏳ Oczekuje</span>
|
||||
{% endif %}
|
||||
{% if fact.confidence_score %}
|
||||
<br><small style="color: var(--text-tertiary);">{{ (fact.confidence_score * 100)|round }}% pewności</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="fact-source">
|
||||
{% if fact.source_news_id %}
|
||||
<a href="{{ url_for('admin_zopk_knowledge_facts', source_news_id=fact.source_news_id) }}">
|
||||
Art. #{{ fact.source_news_id }}
|
||||
</a>
|
||||
{% if fact.source_title %}
|
||||
<br>{{ fact.source_title[:40] }}...
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<button class="action-btn action-btn-verify" onclick="toggleVerify({{ fact.id }}, {{ 'false' if fact.is_verified else 'true' }})">
|
||||
{{ '✗' if fact.is_verified else '✓' }}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="8" style="text-align: center; padding: var(--spacing-xl); color: var(--text-secondary);">
|
||||
Brak faktów do wyświetlenia
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
{% if pages > 1 %}
|
||||
<div class="pagination">
|
||||
{% if page > 1 %}
|
||||
<a href="{{ url_for('admin_zopk_knowledge_facts', page=page-1, fact_type=current_fact_type, is_verified=is_verified, source_news_id=source_news_id) }}">‹ Poprzednia</a>
|
||||
{% else %}
|
||||
<span class="disabled">‹ Poprzednia</span>
|
||||
{% endif %}
|
||||
|
||||
{% for p in range(1, pages + 1) %}
|
||||
{% if p == page %}
|
||||
<span class="current">{{ p }}</span>
|
||||
{% elif p <= 3 or p >= pages - 2 or (p >= page - 1 and p <= page + 1) %}
|
||||
<a href="{{ url_for('admin_zopk_knowledge_facts', page=p, fact_type=current_fact_type, is_verified=is_verified, source_news_id=source_news_id) }}">{{ p }}</a>
|
||||
{% elif p == 4 or p == pages - 3 %}
|
||||
<span>...</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if page < pages %}
|
||||
<a href="{{ url_for('admin_zopk_knowledge_facts', page=page+1, fact_type=current_fact_type, is_verified=is_verified, source_news_id=source_news_id) }}">Następna ›</a>
|
||||
{% else %}
|
||||
<span class="disabled">Następna ›</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
async function toggleVerify(id, newState) {
|
||||
try {
|
||||
const response = await fetch(`/api/zopk/knowledge/facts/${id}/verify`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': '{{ csrf_token() }}'
|
||||
},
|
||||
body: JSON.stringify({ is_verified: newState })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Błąd: ' + data.error);
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Błąd: ' + error.message);
|
||||
}
|
||||
}
|
||||
{% endblock %}
|
||||
@ -332,6 +332,195 @@
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Timeline / Roadmapa */
|
||||
.timeline {
|
||||
position: relative;
|
||||
padding: 0 0 0 40px;
|
||||
margin-bottom: var(--spacing-2xl);
|
||||
}
|
||||
|
||||
.timeline::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 15px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 3px;
|
||||
background: linear-gradient(180deg, var(--primary) 0%, #10b981 50%, #6366f1 100%);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.timeline-item {
|
||||
position: relative;
|
||||
padding-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.timeline-item:last-child {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.timeline-icon {
|
||||
position: absolute;
|
||||
left: -40px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
z-index: 1;
|
||||
box-shadow: 0 0 0 4px var(--background);
|
||||
}
|
||||
|
||||
.timeline-icon.status-completed {
|
||||
background: #10b981;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.timeline-icon.status-in_progress {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.timeline-icon.status-planned {
|
||||
background: #f59e0b;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.timeline-icon.status-delayed {
|
||||
background: #ef4444;
|
||||
color: white;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { box-shadow: 0 0 0 4px var(--background); }
|
||||
50% { box-shadow: 0 0 0 8px rgba(59, 130, 246, 0.2); }
|
||||
}
|
||||
|
||||
.timeline-card {
|
||||
background: var(--surface);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-lg);
|
||||
box-shadow: var(--shadow);
|
||||
border-left: 4px solid var(--primary);
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.timeline-card:hover {
|
||||
box-shadow: var(--shadow-md);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.timeline-card.featured {
|
||||
border-left-width: 6px;
|
||||
background: linear-gradient(135deg, var(--surface) 0%, #f0fdf4 100%);
|
||||
}
|
||||
|
||||
.timeline-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-md);
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.timeline-card h4 {
|
||||
font-size: var(--font-size-lg);
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.timeline-date {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.timeline-date svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.timeline-card p {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.timeline-meta {
|
||||
display: flex;
|
||||
gap: var(--spacing-md);
|
||||
margin-top: var(--spacing-sm);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.timeline-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.badge-completed { background: #dcfce7; color: #166534; }
|
||||
.badge-in_progress { background: #dbeafe; color: #1e40af; }
|
||||
.badge-planned { background: #fef3c7; color: #92400e; }
|
||||
.badge-delayed { background: #fee2e2; color: #991b1b; }
|
||||
|
||||
.timeline-type {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.timeline-source {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.timeline-source:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.timeline-year-marker {
|
||||
position: relative;
|
||||
padding: var(--spacing-sm) 0 var(--spacing-lg) 0;
|
||||
}
|
||||
|
||||
.timeline-year-marker::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -40px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: var(--primary);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 0 0 4px var(--background);
|
||||
}
|
||||
|
||||
.timeline-year {
|
||||
display: inline-block;
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
padding: 4px 12px;
|
||||
border-radius: var(--radius);
|
||||
font-weight: 700;
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.zopk-hero {
|
||||
padding: var(--spacing-xl) var(--spacing-md);
|
||||
@ -354,6 +543,30 @@
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.timeline {
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
.timeline::before {
|
||||
left: 10px;
|
||||
}
|
||||
|
||||
.timeline-icon {
|
||||
left: -30px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.timeline-header {
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.timeline-card h4 {
|
||||
font-size: var(--font-size-base);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
@ -425,6 +638,85 @@
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<!-- Timeline / Roadmapa Section -->
|
||||
{% if milestones %}
|
||||
<section>
|
||||
<div class="section-header">
|
||||
<h2>🗺️ Roadmapa ZOPK</h2>
|
||||
</div>
|
||||
|
||||
<div class="timeline">
|
||||
{% set ns = namespace(current_year=None) %}
|
||||
{% for milestone in milestones %}
|
||||
{# Year marker when year changes #}
|
||||
{% if milestone.target_date %}
|
||||
{% set milestone_year = milestone.target_date.year %}
|
||||
{% if milestone_year != ns.current_year %}
|
||||
{% set ns.current_year = milestone_year %}
|
||||
<div class="timeline-year-marker">
|
||||
<span class="timeline-year">{{ milestone_year }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-icon status-{{ milestone.status }}" title="{{ milestone.status }}">
|
||||
{{ milestone.icon or '📌' }}
|
||||
</div>
|
||||
<div class="timeline-card{% if milestone.is_featured %} featured{% endif %}" style="border-left-color: {{ milestone.color or '#059669' }}">
|
||||
<div class="timeline-header">
|
||||
<h4>{{ milestone.title }}</h4>
|
||||
{% if milestone.target_date %}
|
||||
<span class="timeline-date">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect width="18" height="18" x="3" y="4" rx="2" ry="2"/><line x1="16" x2="16" y1="2" y2="6"/><line x1="8" x2="8" y1="2" y2="6"/><line x1="3" x2="21" y1="10" y2="10"/></svg>
|
||||
{% if milestone.date_precision == 'month' %}
|
||||
{{ milestone.target_date.strftime('%m/%Y') }}
|
||||
{% elif milestone.date_precision == 'quarter' %}
|
||||
Q{{ ((milestone.target_date.month - 1) // 3) + 1 }} {{ milestone.target_date.year }}
|
||||
{% elif milestone.date_precision == 'year' %}
|
||||
{{ milestone.target_date.year }}
|
||||
{% else %}
|
||||
{{ milestone.target_date.strftime('%d.%m.%Y') }}
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if milestone.description %}
|
||||
<p>{{ milestone.description[:200] }}{% if milestone.description|length > 200 %}...{% endif %}</p>
|
||||
{% endif %}
|
||||
<div class="timeline-meta">
|
||||
<span class="timeline-badge badge-{{ milestone.status }}">
|
||||
{% if milestone.status == 'completed' %}✅ Zrealizowano
|
||||
{% elif milestone.status == 'in_progress' %}🔄 W trakcie
|
||||
{% elif milestone.status == 'planned' %}⏳ Planowane
|
||||
{% elif milestone.status == 'delayed' %}⚠️ Opóźnione
|
||||
{% elif milestone.status == 'cancelled' %}❌ Anulowane
|
||||
{% else %}{{ milestone.status }}{% endif %}
|
||||
</span>
|
||||
<span class="timeline-type">
|
||||
{% if milestone.milestone_type == 'announcement' %}📢 Ogłoszenie
|
||||
{% elif milestone.milestone_type == 'decision' %}⚖️ Decyzja
|
||||
{% elif milestone.milestone_type == 'construction_start' %}🏗️ Start budowy
|
||||
{% elif milestone.milestone_type == 'construction_progress' %}🔨 Postęp budowy
|
||||
{% elif milestone.milestone_type == 'completion' %}🎉 Ukończenie
|
||||
{% elif milestone.milestone_type == 'investment' %}💰 Inwestycja
|
||||
{% elif milestone.milestone_type == 'agreement' %}📝 Porozumienie
|
||||
{% elif milestone.milestone_type == 'regulation' %}📋 Regulacja
|
||||
{% else %}{{ milestone.milestone_type }}{% endif %}
|
||||
</span>
|
||||
{% if milestone.source_url %}
|
||||
<a href="{{ milestone.source_url }}" target="_blank" rel="noopener" class="timeline-source">
|
||||
🔗 Źródło
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<!-- Stakeholders Section -->
|
||||
<section>
|
||||
<div class="section-header">
|
||||
|
||||
@ -1031,6 +1031,16 @@ def get_relevant_facts(
|
||||
score += 1
|
||||
|
||||
if score > 0:
|
||||
# Get source URL from related news
|
||||
source_url = ''
|
||||
source_name = ''
|
||||
source_date = ''
|
||||
if fact.source_news:
|
||||
source_url = fact.source_news.url or ''
|
||||
source_name = fact.source_news.source_name or fact.source_news.source_domain or ''
|
||||
if fact.source_news.published_at:
|
||||
source_date = fact.source_news.published_at.strftime('%Y-%m-%d')
|
||||
|
||||
results.append({
|
||||
'fact_id': fact.id,
|
||||
'fact_type': fact.fact_type,
|
||||
@ -1039,7 +1049,10 @@ def get_relevant_facts(
|
||||
'numeric_value': float(fact.numeric_value) if fact.numeric_value else None,
|
||||
'numeric_unit': fact.numeric_unit,
|
||||
'confidence': float(fact.confidence_score) if fact.confidence_score else None,
|
||||
'relevance_score': score
|
||||
'relevance_score': score,
|
||||
'source_url': source_url,
|
||||
'source_name': source_name,
|
||||
'source_date': source_date
|
||||
})
|
||||
|
||||
# Sort by relevance
|
||||
@ -1228,3 +1241,415 @@ def get_knowledge_stats(db_session) -> Dict:
|
||||
"""
|
||||
service = ZOPKKnowledgeService(db_session)
|
||||
return service.get_extraction_statistics()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# ADMIN PANEL - LIST FUNCTIONS
|
||||
# ============================================================
|
||||
|
||||
def list_chunks(
|
||||
db_session,
|
||||
page: int = 1,
|
||||
per_page: int = 20,
|
||||
source_news_id: Optional[int] = None,
|
||||
has_embedding: Optional[bool] = None,
|
||||
is_verified: Optional[bool] = None
|
||||
) -> Dict:
|
||||
"""
|
||||
List knowledge chunks with pagination and filtering.
|
||||
|
||||
Args:
|
||||
db_session: Database session
|
||||
page: Page number (1-based)
|
||||
per_page: Items per page
|
||||
source_news_id: Filter by source article
|
||||
has_embedding: Filter by embedding status
|
||||
is_verified: Filter by verification status
|
||||
|
||||
Returns:
|
||||
{
|
||||
'chunks': [...],
|
||||
'total': int,
|
||||
'page': int,
|
||||
'per_page': int,
|
||||
'pages': int
|
||||
}
|
||||
"""
|
||||
from sqlalchemy import func
|
||||
|
||||
query = db_session.query(ZOPKKnowledgeChunk)
|
||||
|
||||
# Apply filters
|
||||
if source_news_id:
|
||||
query = query.filter(ZOPKKnowledgeChunk.source_news_id == source_news_id)
|
||||
|
||||
if has_embedding is not None:
|
||||
if has_embedding:
|
||||
query = query.filter(ZOPKKnowledgeChunk.embedding.isnot(None))
|
||||
else:
|
||||
query = query.filter(ZOPKKnowledgeChunk.embedding.is_(None))
|
||||
|
||||
if is_verified is not None:
|
||||
query = query.filter(ZOPKKnowledgeChunk.is_verified == is_verified)
|
||||
|
||||
# Get total count
|
||||
total = query.count()
|
||||
|
||||
# Calculate pagination
|
||||
pages = (total + per_page - 1) // per_page
|
||||
offset = (page - 1) * per_page
|
||||
|
||||
# Get chunks with source news info
|
||||
chunks = query.order_by(
|
||||
ZOPKKnowledgeChunk.created_at.desc()
|
||||
).offset(offset).limit(per_page).all()
|
||||
|
||||
return {
|
||||
'chunks': [
|
||||
{
|
||||
'id': c.id,
|
||||
'content': c.content[:300] + '...' if len(c.content) > 300 else c.content,
|
||||
'full_content': c.content,
|
||||
'summary': c.summary,
|
||||
'chunk_type': c.chunk_type,
|
||||
'chunk_index': c.chunk_index,
|
||||
'token_count': c.token_count,
|
||||
'importance_score': c.importance_score,
|
||||
'confidence_score': float(c.confidence_score) if c.confidence_score else None,
|
||||
'has_embedding': c.embedding is not None,
|
||||
'is_verified': c.is_verified,
|
||||
'source_news_id': c.source_news_id,
|
||||
'source_title': c.source_news.title if c.source_news else None,
|
||||
'source_url': c.source_news.url if c.source_news else None,
|
||||
'created_at': c.created_at.isoformat() if c.created_at else None,
|
||||
'keywords': c.keywords if isinstance(c.keywords, list) else []
|
||||
}
|
||||
for c in chunks
|
||||
],
|
||||
'total': total,
|
||||
'page': page,
|
||||
'per_page': per_page,
|
||||
'pages': pages
|
||||
}
|
||||
|
||||
|
||||
def list_facts(
|
||||
db_session,
|
||||
page: int = 1,
|
||||
per_page: int = 20,
|
||||
fact_type: Optional[str] = None,
|
||||
is_verified: Optional[bool] = None,
|
||||
source_news_id: Optional[int] = None
|
||||
) -> Dict:
|
||||
"""
|
||||
List knowledge facts with pagination and filtering.
|
||||
|
||||
Args:
|
||||
db_session: Database session
|
||||
page: Page number (1-based)
|
||||
per_page: Items per page
|
||||
fact_type: Filter by fact type (statistic, event, statement, decision, milestone)
|
||||
is_verified: Filter by verification status
|
||||
source_news_id: Filter by source article
|
||||
|
||||
Returns:
|
||||
{
|
||||
'facts': [...],
|
||||
'total': int,
|
||||
'page': int,
|
||||
'per_page': int,
|
||||
'pages': int,
|
||||
'fact_types': [...] - available types for filtering
|
||||
}
|
||||
"""
|
||||
from sqlalchemy import func, distinct
|
||||
|
||||
query = db_session.query(ZOPKKnowledgeFact)
|
||||
|
||||
# Apply filters
|
||||
if fact_type:
|
||||
query = query.filter(ZOPKKnowledgeFact.fact_type == fact_type)
|
||||
|
||||
if is_verified is not None:
|
||||
query = query.filter(ZOPKKnowledgeFact.is_verified == is_verified)
|
||||
|
||||
if source_news_id:
|
||||
query = query.filter(ZOPKKnowledgeFact.source_news_id == source_news_id)
|
||||
|
||||
# Get total count
|
||||
total = query.count()
|
||||
|
||||
# Get available fact types
|
||||
fact_types = db_session.query(
|
||||
distinct(ZOPKKnowledgeFact.fact_type)
|
||||
).filter(
|
||||
ZOPKKnowledgeFact.fact_type.isnot(None)
|
||||
).all()
|
||||
fact_types = [f[0] for f in fact_types if f[0]]
|
||||
|
||||
# Calculate pagination
|
||||
pages = (total + per_page - 1) // per_page
|
||||
offset = (page - 1) * per_page
|
||||
|
||||
# Get facts
|
||||
facts = query.order_by(
|
||||
ZOPKKnowledgeFact.created_at.desc()
|
||||
).offset(offset).limit(per_page).all()
|
||||
|
||||
return {
|
||||
'facts': [
|
||||
{
|
||||
'id': f.id,
|
||||
'fact_type': f.fact_type,
|
||||
'subject': f.subject,
|
||||
'predicate': f.predicate,
|
||||
'object': f.object,
|
||||
'full_text': f.full_text,
|
||||
'numeric_value': float(f.numeric_value) if f.numeric_value else None,
|
||||
'numeric_unit': f.numeric_unit,
|
||||
'date_value': f.date_value.isoformat() if f.date_value else None,
|
||||
'confidence_score': float(f.confidence_score) if f.confidence_score else None,
|
||||
'is_verified': f.is_verified,
|
||||
'source_news_id': f.source_news_id,
|
||||
'source_chunk_id': f.source_chunk_id,
|
||||
'source_title': f.source_news.title if f.source_news else None,
|
||||
'source_url': f.source_news.url if f.source_news else None,
|
||||
'entities_involved': f.entities_involved if isinstance(f.entities_involved, list) else [],
|
||||
'created_at': f.created_at.isoformat() if f.created_at else None
|
||||
}
|
||||
for f in facts
|
||||
],
|
||||
'total': total,
|
||||
'page': page,
|
||||
'per_page': per_page,
|
||||
'pages': pages,
|
||||
'fact_types': fact_types
|
||||
}
|
||||
|
||||
|
||||
def list_entities(
|
||||
db_session,
|
||||
page: int = 1,
|
||||
per_page: int = 20,
|
||||
entity_type: Optional[str] = None,
|
||||
is_verified: Optional[bool] = None,
|
||||
min_mentions: Optional[int] = None
|
||||
) -> Dict:
|
||||
"""
|
||||
List knowledge entities with pagination and filtering.
|
||||
|
||||
Args:
|
||||
db_session: Database session
|
||||
page: Page number (1-based)
|
||||
per_page: Items per page
|
||||
entity_type: Filter by entity type (company, person, place, organization, project, technology)
|
||||
is_verified: Filter by verification status
|
||||
min_mentions: Filter by minimum mention count
|
||||
|
||||
Returns:
|
||||
{
|
||||
'entities': [...],
|
||||
'total': int,
|
||||
'page': int,
|
||||
'per_page': int,
|
||||
'pages': int,
|
||||
'entity_types': [...] - available types for filtering
|
||||
}
|
||||
"""
|
||||
from sqlalchemy import func, distinct
|
||||
|
||||
query = db_session.query(ZOPKKnowledgeEntity)
|
||||
|
||||
# Exclude merged entities
|
||||
query = query.filter(ZOPKKnowledgeEntity.merged_into_id.is_(None))
|
||||
|
||||
# Apply filters
|
||||
if entity_type:
|
||||
query = query.filter(ZOPKKnowledgeEntity.entity_type == entity_type)
|
||||
|
||||
if is_verified is not None:
|
||||
query = query.filter(ZOPKKnowledgeEntity.is_verified == is_verified)
|
||||
|
||||
if min_mentions:
|
||||
query = query.filter(ZOPKKnowledgeEntity.mentions_count >= min_mentions)
|
||||
|
||||
# Get total count
|
||||
total = query.count()
|
||||
|
||||
# Get available entity types
|
||||
entity_types = db_session.query(
|
||||
distinct(ZOPKKnowledgeEntity.entity_type)
|
||||
).filter(
|
||||
ZOPKKnowledgeEntity.entity_type.isnot(None)
|
||||
).all()
|
||||
entity_types = [e[0] for e in entity_types if e[0]]
|
||||
|
||||
# Calculate pagination
|
||||
pages = (total + per_page - 1) // per_page
|
||||
offset = (page - 1) * per_page
|
||||
|
||||
# Get entities sorted by mentions
|
||||
entities = query.order_by(
|
||||
ZOPKKnowledgeEntity.mentions_count.desc()
|
||||
).offset(offset).limit(per_page).all()
|
||||
|
||||
return {
|
||||
'entities': [
|
||||
{
|
||||
'id': e.id,
|
||||
'name': e.name,
|
||||
'normalized_name': e.normalized_name,
|
||||
'entity_type': e.entity_type,
|
||||
'description': e.description,
|
||||
'short_description': e.short_description,
|
||||
'aliases': e.aliases if isinstance(e.aliases, list) else [],
|
||||
'mentions_count': e.mentions_count or 0,
|
||||
'is_verified': e.is_verified,
|
||||
'company_id': e.company_id,
|
||||
'external_url': e.external_url,
|
||||
'first_mentioned_at': e.first_mentioned_at.isoformat() if e.first_mentioned_at else None,
|
||||
'last_mentioned_at': e.last_mentioned_at.isoformat() if e.last_mentioned_at else None,
|
||||
'created_at': e.created_at.isoformat() if e.created_at else None
|
||||
}
|
||||
for e in entities
|
||||
],
|
||||
'total': total,
|
||||
'page': page,
|
||||
'per_page': per_page,
|
||||
'pages': pages,
|
||||
'entity_types': entity_types
|
||||
}
|
||||
|
||||
|
||||
def get_chunk_detail(db_session, chunk_id: int) -> Optional[Dict]:
|
||||
"""Get detailed information about a single chunk."""
|
||||
chunk = db_session.query(ZOPKKnowledgeChunk).filter(
|
||||
ZOPKKnowledgeChunk.id == chunk_id
|
||||
).first()
|
||||
|
||||
if not chunk:
|
||||
return None
|
||||
|
||||
# Get facts from this chunk
|
||||
facts = db_session.query(ZOPKKnowledgeFact).filter(
|
||||
ZOPKKnowledgeFact.source_chunk_id == chunk_id
|
||||
).all()
|
||||
|
||||
# Get entity mentions
|
||||
mentions = db_session.query(ZOPKKnowledgeEntityMention).filter(
|
||||
ZOPKKnowledgeEntityMention.chunk_id == chunk_id
|
||||
).all()
|
||||
|
||||
return {
|
||||
'id': chunk.id,
|
||||
'content': chunk.content,
|
||||
'content_clean': chunk.content_clean,
|
||||
'summary': chunk.summary,
|
||||
'chunk_type': chunk.chunk_type,
|
||||
'chunk_index': chunk.chunk_index,
|
||||
'token_count': chunk.token_count,
|
||||
'importance_score': chunk.importance_score,
|
||||
'confidence_score': float(chunk.confidence_score) if chunk.confidence_score else None,
|
||||
'has_embedding': chunk.embedding is not None,
|
||||
'is_verified': chunk.is_verified,
|
||||
'keywords': chunk.keywords if isinstance(chunk.keywords, list) else [],
|
||||
'context_date': chunk.context_date.isoformat() if chunk.context_date else None,
|
||||
'context_location': chunk.context_location,
|
||||
'extraction_model': chunk.extraction_model,
|
||||
'extracted_at': chunk.extracted_at.isoformat() if chunk.extracted_at else None,
|
||||
'created_at': chunk.created_at.isoformat() if chunk.created_at else None,
|
||||
'source_news': {
|
||||
'id': chunk.source_news.id,
|
||||
'title': chunk.source_news.title,
|
||||
'url': chunk.source_news.url,
|
||||
'source_name': chunk.source_news.source_name
|
||||
} if chunk.source_news else None,
|
||||
'facts': [
|
||||
{
|
||||
'id': f.id,
|
||||
'fact_type': f.fact_type,
|
||||
'full_text': f.full_text,
|
||||
'is_verified': f.is_verified
|
||||
}
|
||||
for f in facts
|
||||
],
|
||||
'entity_mentions': [
|
||||
{
|
||||
'id': m.id,
|
||||
'entity_id': m.entity_id,
|
||||
'entity_name': m.entity.name if m.entity else None,
|
||||
'entity_type': m.entity.entity_type if m.entity else None,
|
||||
'mention_text': m.mention_text
|
||||
}
|
||||
for m in mentions
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def update_chunk_verification(db_session, chunk_id: int, is_verified: bool, user_id: int) -> bool:
|
||||
"""Update chunk verification status."""
|
||||
chunk = db_session.query(ZOPKKnowledgeChunk).filter(
|
||||
ZOPKKnowledgeChunk.id == chunk_id
|
||||
).first()
|
||||
|
||||
if not chunk:
|
||||
return False
|
||||
|
||||
chunk.is_verified = is_verified
|
||||
chunk.verified_by = user_id
|
||||
chunk.verified_at = datetime.now()
|
||||
db_session.commit()
|
||||
return True
|
||||
|
||||
|
||||
def update_fact_verification(db_session, fact_id: int, is_verified: bool) -> bool:
|
||||
"""Update fact verification status."""
|
||||
fact = db_session.query(ZOPKKnowledgeFact).filter(
|
||||
ZOPKKnowledgeFact.id == fact_id
|
||||
).first()
|
||||
|
||||
if not fact:
|
||||
return False
|
||||
|
||||
fact.is_verified = is_verified
|
||||
db_session.commit()
|
||||
return True
|
||||
|
||||
|
||||
def update_entity_verification(db_session, entity_id: int, is_verified: bool) -> bool:
|
||||
"""Update entity verification status."""
|
||||
entity = db_session.query(ZOPKKnowledgeEntity).filter(
|
||||
ZOPKKnowledgeEntity.id == entity_id
|
||||
).first()
|
||||
|
||||
if not entity:
|
||||
return False
|
||||
|
||||
entity.is_verified = is_verified
|
||||
db_session.commit()
|
||||
return True
|
||||
|
||||
|
||||
def delete_chunk(db_session, chunk_id: int) -> bool:
|
||||
"""Delete a chunk and its associated facts and mentions."""
|
||||
chunk = db_session.query(ZOPKKnowledgeChunk).filter(
|
||||
ZOPKKnowledgeChunk.id == chunk_id
|
||||
).first()
|
||||
|
||||
if not chunk:
|
||||
return False
|
||||
|
||||
# Delete associated facts
|
||||
db_session.query(ZOPKKnowledgeFact).filter(
|
||||
ZOPKKnowledgeFact.source_chunk_id == chunk_id
|
||||
).delete()
|
||||
|
||||
# Delete associated mentions
|
||||
db_session.query(ZOPKKnowledgeEntityMention).filter(
|
||||
ZOPKKnowledgeEntityMention.chunk_id == chunk_id
|
||||
).delete()
|
||||
|
||||
# Delete chunk
|
||||
db_session.delete(chunk)
|
||||
db_session.commit()
|
||||
return True
|
||||
|
||||
Loading…
Reference in New Issue
Block a user