make it beauty and shiny

This commit is contained in:
2026-01-10 12:58:18 +01:00
parent f8986da1aa
commit afd3ae1cc4
2 changed files with 272 additions and 107 deletions

View File

@@ -6,4 +6,6 @@ sentence-transformers==2.2.2
transformers==4.28.1
torch==1.13.1
numpy==1.24.2
python-docx
python-docx
openpyxl
python-pptx

View File

@@ -7,11 +7,21 @@ import zipfile
import io
import traceback
# NEU: Word Support
# --- OPTIONALE IMPORTE ---
try:
import docx
except ImportError:
docx = None # Fallback, falls nicht installiert
docx = None
try:
import openpyxl
except ImportError:
openpyxl = None
try:
from pptx import Presentation
except ImportError:
Presentation = None
from sentence_transformers import SentenceTransformer, util
from rapidfuzz import process, fuzz
@@ -21,8 +31,8 @@ from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
QHBoxLayout, QLineEdit, QPushButton, QLabel,
QFileDialog, QProgressBar, QMessageBox,
QListWidget, QListWidgetItem, QSplitter, QFrame,
QSplashScreen, QScrollArea, QStyle)
from PyQt6.QtGui import QDesktopServices, QPixmap, QCursor, QAction
QSplashScreen, QScrollArea, QStyle, QGraphicsDropShadowEffect)
from PyQt6.QtGui import QDesktopServices, QPixmap, QCursor, QAction, QColor, QPalette, QFont
# --- 0. LOGGING & SETUP ---
@@ -51,12 +61,117 @@ class Logger(object):
sys.stdout = Logger()
sys.stderr = sys.stdout
print(f"--- START LOGGING ---")
print(f"Logfile: {log_file_path}")
# --- STYLESHEET ---
STYLESHEET = """
QMainWindow {
background-color: #f4f7f6;
}
# Check ob docx installiert ist und warnen im Log
if docx is None:
print("WARNUNG: 'python-docx' ist nicht installiert. .docx Dateien werden ignoriert.")
/* Sidebar Styles */
QFrame#Sidebar {
background-color: #2c3e50;
border: none;
}
QLabel#SidebarTitle {
color: #ecf0f1;
font-weight: bold;
font-size: 16px;
padding: 10px;
}
QListWidget {
background-color: #34495e;
color: #ecf0f1;
border: none;
outline: none;
font-size: 13px;
}
QListWidget::item {
padding: 8px;
border-bottom: 1px solid #2c3e50;
}
QListWidget::item:selected {
background-color: #1abc9c;
color: white;
}
QListWidget::item:hover {
background-color: #16a085;
}
/* Sidebar Buttons */
QPushButton#SidebarBtn {
background-color: #34495e;
color: #bdc3c7;
border: 1px solid #2c3e50;
padding: 8px;
text-align: left;
border-radius: 4px;
margin: 2px 10px;
}
QPushButton#SidebarBtn:hover {
background-color: #1abc9c;
color: white;
border: 1px solid #16a085;
}
QPushButton#CancelBtn {
background-color: #e74c3c;
color: white;
font-weight: bold;
border-radius: 4px;
margin: 10px;
padding: 8px;
}
/* Main Area */
QLineEdit {
padding: 10px;
border: 1px solid #bdc3c7;
border-radius: 20px;
font-size: 14px;
background-color: white;
selection-background-color: #3498db;
}
QLineEdit:focus {
border: 2px solid #3498db;
}
QPushButton#SearchBtn {
background-color: #3498db;
color: white;
font-weight: bold;
border-radius: 20px;
padding: 10px 20px;
font-size: 14px;
}
QPushButton#SearchBtn:hover {
background-color: #2980b9;
}
QPushButton#SearchBtn:pressed {
background-color: #1f618d;
}
/* Scroll Area & Results */
QScrollArea {
border: none;
background-color: transparent;
}
QWidget#ResultsContainer {
background-color: transparent;
}
QLabel#StatusLabel {
color: #7f8c8d;
font-size: 12px;
margin-left: 10px;
}
QProgressBar {
border: none;
background-color: #ecf0f1;
height: 4px;
text-align: center;
}
QProgressBar::chunk {
background-color: #1abc9c;
}
"""
def qt_message_handler(mode, context, message):
msg_lower = message.lower()
@@ -65,7 +180,6 @@ def qt_message_handler(mode, context, message):
"unable to create font", "fontbbox", "script"
]
if any(k in msg_lower for k in ignore_keywords): return
try:
sys.stdout.write(f"[Qt] {message}\n")
except: pass
@@ -74,45 +188,58 @@ qInstallMessageHandler(qt_message_handler)
os.environ["QT_LOGGING_RULES"] = "qt.text.font.db=false;qt.qpa.fonts=false"
# --- NEUE KOMPONENTE: Ein einzelnes Suchergebnis als Widget ---
# --- WIDGET: Modernes Suchergebnis (Fixed Tooltips) ---
class SearchResultItem(QFrame):
"""
Stellt ein einzelnes Suchergebnis als 'Karte' dar.
"""
def __init__(self, filename, filepath, snippet, parent=None):
super().__init__(parent)
self.filepath = filepath
# WICHTIG: Tooltip auf das gesamte Frame setzen, nicht nur auf Kinder
self.setToolTip(filepath)
# Design der Karte
self.setFrameShape(QFrame.Shape.StyledPanel)
self.setStyleSheet("""
SearchResultItem {
background-color: #ffffff;
border: 1px solid #ddd;
border-radius: 5px;
margin-bottom: 5px;
background-color: white;
border: 1px solid #e0e0e0;
border-radius: 8px;
}
SearchResultItem:hover {
background-color: #f0f8ff;
border: 1px solid #2980b9;
border: 1px solid #3498db;
background-color: #fbfbfb;
}
""")
# Schatten-Effekt
shadow = QGraphicsDropShadowEffect(self)
shadow.setBlurRadius(10)
shadow.setXOffset(0)
shadow.setYOffset(2)
shadow.setColor(QColor(0, 0, 0, 30))
self.setGraphicsEffect(shadow)
layout = QVBoxLayout(self)
layout.setContentsMargins(10, 10, 10, 10)
layout.setContentsMargins(15, 15, 15, 15)
layout.setSpacing(5)
# 1. Dateiname
# 1. Titel (Dateiname)
self.btn_title = QPushButton(filename)
self.btn_title.setCursor(Qt.CursorShape.PointingHandCursor)
# MouseTracking aktivieren hilft manchmal bei schnellen Bewegungen
self.btn_title.setMouseTracking(True)
self.btn_title.setStyleSheet("""
QPushButton {
text-align: left;
font-weight: bold;
font-size: 14pt;
color: #2980b9;
font-size: 16px;
color: #2c3e50;
border: none;
background: transparent;
padding: 0px;
}
QPushButton:hover {
color: #3498db;
text-decoration: underline;
}
""")
@@ -121,26 +248,30 @@ class SearchResultItem(QFrame):
# 2. Snippet
self.lbl_snippet = QLabel(snippet)
self.lbl_snippet.setWordWrap(True)
self.lbl_snippet.setStyleSheet("color: #444; font-size: 10pt; margin-top: 5px;")
self.lbl_snippet.setStyleSheet("color: #555; font-size: 13px; line-height: 1.4;")
# 3. Pfad (unten, klein)
path_layout = QHBoxLayout()
lbl_icon = QLabel("📄")
lbl_icon.setStyleSheet("font-size: 10px; color: #95a5a6;")
# 3. Pfad
self.lbl_path = QLabel(filepath)
self.lbl_path.setStyleSheet("color: #888; font-size: 8pt; margin-top: 5px;")
self.lbl_path.setStyleSheet("color: #95a5a6; font-size: 11px;")
path_layout.addWidget(lbl_icon)
path_layout.addWidget(self.lbl_path)
path_layout.addStretch()
layout.addWidget(self.btn_title)
layout.addWidget(self.lbl_snippet)
layout.addWidget(self.lbl_path)
layout.addLayout(path_layout)
def open_file(self):
print(f"Öffne Datei: {self.filepath}")
target_path = self.filepath
if " :: " in target_path:
target_path = target_path.split(" :: ")[0]
url = QUrl.fromLocalFile(target_path)
success = QDesktopServices.openUrl(url)
if not success:
print("Fehler: Konnte Datei nicht öffnen.")
QDesktopServices.openUrl(url)
# --- 1. DATENBANK MANAGER ---
@@ -191,7 +322,6 @@ class DatabaseHandler:
def search(self, query):
if not query.strip() or not self.model: return []
# 1. Semantik
q_vec = self.model.encode(query, convert_to_tensor=False)
conn = sqlite3.connect(self.db_name)
cursor = conn.cursor()
@@ -207,7 +337,6 @@ class DatabaseHandler:
scores = np.clip(scores, 0, 1)
sem_map = {did: float(s) for did, s in zip(doc_ids, scores)}
# 2. Lexikalisch
words = query.replace('"', '').split()
if not words: words = [query]
fts_query = " OR ".join([f'"{w}"*' for w in words])
@@ -222,7 +351,6 @@ class DatabaseHandler:
r2 = fuzz.partial_token_set_ratio(query.lower(), content[:5000].lower())
lex_map[did] = max(r1, r2) / 100.0
# 3. Hybrid
final = {}
ALPHA = 0.65
BETA = 0.35
@@ -233,7 +361,6 @@ class DatabaseHandler:
if s_score > 0.4 and l_score > 0.6: h_score += 0.1
final[did] = h_score
# 4. Fetch
sorted_ids = sorted(final.keys(), key=lambda x: final[x], reverse=True)[:50]
results = []
for did in sorted_ids:
@@ -269,7 +396,6 @@ class IndexerThread(QThread):
ext = os.path.splitext(filename)[1].lower()
text = ""
try:
# --- PDF ---
if ext == ".pdf":
try:
with pdfplumber.open(stream) as pdf:
@@ -277,17 +403,34 @@ class IndexerThread(QThread):
if t := p.extract_text(): text += t + "\n"
except: pass
# --- WORD / DOCX ---
elif ext == ".docx" and docx is not None:
try:
# python-docx kann file-like objects (BytesIO oder file) lesen
doc = docx.Document(stream)
for para in doc.paragraphs:
text += para.text + "\n"
except Exception as e:
print(f"Docx Error {filename}: {e}")
for para in doc.paragraphs: text += para.text + "\n"
except: pass
elif ext == ".xlsx" and openpyxl is not None:
try:
wb = openpyxl.load_workbook(stream, data_only=True, read_only=True)
for sheet in wb.worksheets:
text += f"\n--- {sheet.title} ---\n"
for row in sheet.iter_rows(values_only=True):
row_text = " ".join([str(c) for c in row if c is not None])
if row_text.strip(): text += row_text + "\n"
except: pass
elif ext == ".pptx" and Presentation is not None:
try:
prs = Presentation(stream)
for i, slide in enumerate(prs.slides):
text += f"\n--- Folie {i+1} ---\n"
for shape in slide.shapes:
if shape.has_text_frame:
for paragraph in shape.text_frame.paragraphs:
for run in paragraph.runs: text += run.text + " "
text += "\n"
except: pass
# --- PLAIN TEXT ---
elif ext in [".txt", ".md", ".py", ".json", ".csv", ".html", ".log", ".ini", ".xml"]:
try:
content = stream.read()
@@ -301,7 +444,6 @@ class IndexerThread(QThread):
conn = sqlite3.connect(self.db_name)
cursor = conn.cursor()
# Cleanup
cursor.execute("SELECT rowid FROM documents WHERE path LIKE ?", (f"{self.folder_path}%",))
ids = [r[0] for r in cursor.fetchall()]
if ids:
@@ -333,12 +475,16 @@ class IndexerThread(QThread):
indexed += 1
except: skipped += 1
else:
with open(path, "rb") as f:
content = self._extract_text(f, file)
if content and len(content.strip()) > 20:
self._save(cursor, file, path, content)
indexed += 1
else: skipped += 1
try:
with open(path, "rb") as f:
file_content = io.BytesIO(f.read())
content = self._extract_text(file_content, file)
if content and len(content.strip()) > 20:
self._save(cursor, file, path, content)
indexed += 1
else: skipped += 1
except: skipped += 1
if cancelled: break
conn.commit()
@@ -351,7 +497,7 @@ class IndexerThread(QThread):
vec = self.model.encode(content[:8000], convert_to_tensor=False).tobytes()
cursor.execute("INSERT INTO embeddings (doc_id, vec) VALUES (?, ?)", (did, vec))
# --- 3. UI ---
# --- 3. UI MAIN WINDOW ---
class UffWindow(QMainWindow):
def __init__(self, splash=None):
@@ -362,90 +508,111 @@ class UffWindow(QMainWindow):
self.load_saved_folders()
def initUI(self):
self.setWindowTitle("UFF Search")
self.resize(1000, 700)
self.setWindowTitle("UFF Search v7.2 (Stable Tooltips)")
self.resize(1100, 750)
self.setStyleSheet(STYLESHEET)
central = QWidget()
self.setCentralWidget(central)
main_layout = QHBoxLayout(central)
main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.setSpacing(0)
# -- LINKS (Sidebar) --
# -- SIDEBAR --
left_panel = QFrame()
left_panel.setFixedWidth(250)
left_panel.setStyleSheet("background-color: #f0f0f0; border-right: 1px solid #ccc;")
left_panel.setObjectName("Sidebar")
left_panel.setFixedWidth(260)
left_layout = QVBoxLayout(left_panel)
left_layout.setContentsMargins(0, 20, 0, 20)
lbl_title = QLabel(" UFF SEARCH")
lbl_title.setObjectName("SidebarTitle")
self.folder_list = QListWidget()
self.folder_list.setStyleSheet("border: 1px solid #ddd; background: white;")
btn_add = QPushButton(" + Ordner")
icon_add = self.style().standardIcon(QStyle.StandardPixmap.SP_FileDialogNewFolder)
icon_del = self.style().standardIcon(QStyle.StandardPixmap.SP_TrashIcon)
icon_refresh = self.style().standardIcon(QStyle.StandardPixmap.SP_BrowserReload)
icon_stop = self.style().standardIcon(QStyle.StandardPixmap.SP_DialogCancelButton)
btn_add = QPushButton(" Ordner hinzufügen")
btn_add.setObjectName("SidebarBtn")
btn_add.setIcon(icon_add)
btn_add.clicked.connect(self.add_new_folder)
btn_del = QPushButton(" - Löschen")
btn_del = QPushButton(" Ordner entfernen")
btn_del.setObjectName("SidebarBtn")
btn_del.setIcon(icon_del)
btn_del.clicked.connect(self.delete_selected_folder)
self.btn_rescan = QPushButton(" ↻ Scan")
self.btn_rescan = QPushButton(" Neu scannen")
self.btn_rescan.setObjectName("SidebarBtn")
self.btn_rescan.setIcon(icon_refresh)
self.btn_rescan.clicked.connect(self.rescan_selected_folder)
self.btn_cancel = QPushButton("🛑 Stop")
self.btn_cancel = QPushButton("STOPPEN")
self.btn_cancel.setObjectName("CancelBtn")
self.btn_cancel.setIcon(icon_stop)
self.btn_cancel.clicked.connect(self.cancel_indexing)
self.btn_cancel.hide()
self.btn_cancel.setStyleSheet("background-color: #ffcccc; color: red;")
left_layout.addWidget(QLabel("<b>📂 Indizierte Ordner</b>"))
left_layout.addWidget(lbl_title)
left_layout.addSpacing(10)
left_layout.addWidget(self.folder_list)
left_layout.addSpacing(10)
left_layout.addWidget(btn_add)
left_layout.addWidget(btn_del)
left_layout.addStretch()
left_layout.addWidget(self.btn_rescan)
left_layout.addWidget(self.btn_cancel)
# -- RECHTS (Suche & Ergebnisse) --
# -- RECHTS (Hauptbereich) --
right_panel = QWidget()
right_panel.setObjectName("MainArea")
right_layout = QVBoxLayout(right_panel)
right_layout.setContentsMargins(30, 30, 30, 30)
right_layout.setSpacing(15)
# Suchleiste
# Header
search_box = QHBoxLayout()
self.input_search = QLineEdit()
self.input_search.setPlaceholderText("Suchbegriff eingeben...")
self.input_search.setStyleSheet("padding: 8px; font-size: 14px;")
self.input_search.setPlaceholderText("Wonach suchst du heute?")
self.input_search.returnPressed.connect(self.perform_search)
self.btn_go = QPushButton("Suchen")
self.btn_go.setFixedWidth(100)
self.btn_go.setStyleSheet("background-color: #2980b9; color: white; padding: 8px; font-weight: bold;")
self.btn_go.setObjectName("SearchBtn")
self.btn_go.setCursor(Qt.CursorShape.PointingHandCursor)
self.btn_go.clicked.connect(self.perform_search)
search_box.addWidget(self.input_search)
search_box.addWidget(self.btn_go)
# Status & Progress
self.lbl_status = QLabel("Warte auf Modell...")
# Status
status_box = QHBoxLayout()
self.lbl_status = QLabel("Modell wird geladen...")
self.lbl_status.setObjectName("StatusLabel")
self.progress_bar = QProgressBar()
self.progress_bar.hide()
status_box.addWidget(self.lbl_status)
status_box.addWidget(self.progress_bar)
# ERGEBNIS-BEREICH (QScrollArea statt QTextBrowser)
# Ergebnisse
self.scroll_area = QScrollArea()
self.scroll_area.setWidgetResizable(True)
self.scroll_area.setStyleSheet("background-color: #fafafa; border: none;")
# Container Widget für die Ergebnisse
self.results_container = QWidget()
self.results_container.setStyleSheet("background-color: transparent;")
self.results_container.setObjectName("ResultsContainer")
self.results_layout = QVBoxLayout(self.results_container)
self.results_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
self.results_layout.setSpacing(10)
self.results_layout.setSpacing(15)
self.scroll_area.setWidget(self.results_container)
right_layout.addLayout(search_box)
right_layout.addWidget(self.lbl_status)
right_layout.addWidget(self.progress_bar)
right_layout.addLayout(status_box)
right_layout.addWidget(self.scroll_area)
# Splitter
splitter = QSplitter()
splitter.addWidget(left_panel)
splitter.addWidget(right_panel)
splitter.setSizes([250, 750])
main_layout.addWidget(splitter)
main_layout.addWidget(left_panel)
main_layout.addWidget(right_panel)
self.set_ui_enabled(False)
def set_ui_enabled(self, enabled):
@@ -465,44 +632,38 @@ class UffWindow(QMainWindow):
QMessageBox.critical(self, "Fehler", "Modell konnte nicht geladen werden.")
return
self.db.model = model
self.lbl_status.setText("Bereit.")
self.lbl_status.setText("Bereit für deine Suche.")
self.set_ui_enabled(True)
def perform_search(self):
query = self.input_search.text()
if not query: return
self.lbl_status.setText("Suche läuft...")
QApplication.processEvents()
# 1. Alte Ergebnisse löschen
while self.results_layout.count():
child = self.results_layout.takeAt(0)
if child.widget():
child.widget().deleteLater()
if child.widget(): child.widget().deleteLater()
# 2. Suchen
results = self.db.search(query)
self.lbl_status.setText(f"{len(results)} Treffer.")
self.lbl_status.setText(f"{len(results)} Treffer gefunden.")
# 3. Neue Ergebnisse als Widgets hinzufügen
if not results:
lbl = QLabel("Keine Ergebnisse gefunden.")
lbl.setStyleSheet("color: #777; font-size: 14pt; margin-top: 20px;")
lbl = QLabel("Leider keine Ergebnisse.")
lbl.setStyleSheet("color: #95a5a6; font-size: 18px; margin-top: 40px;")
lbl.setAlignment(Qt.AlignmentFlag.AlignHCenter)
self.results_layout.addWidget(lbl)
else:
for fname, fpath, snippet in results:
item = SearchResultItem(fname, fpath, snippet)
self.results_layout.addWidget(item)
self.results_layout.addWidget(SearchResultItem(fname, fpath, snippet))
self.results_layout.addStretch()
# --- Folder Management ---
def load_saved_folders(self):
self.folder_list.clear()
for f in self.db.get_folders():
self.folder_list.addItem(QListWidgetItem(f))
item = QListWidgetItem(self.style().standardIcon(QStyle.StandardPixmap.SP_DirIcon), f)
item.setToolTip(f)
self.folder_list.addItem(item)
def add_new_folder(self):
f = QFileDialog.getExistingDirectory(self, "Ordner wählen")
@@ -534,19 +695,21 @@ class UffWindow(QMainWindow):
def idx_done(self, n, s, c):
self.set_ui_enabled(True)
self.btn_cancel.hide(); self.btn_rescan.show(); self.progress_bar.hide()
msg = "Abgebrochen" if c else "Fertig"
msg = "Abgebrochen" if c else "Indexierung fertig"
self.lbl_status.setText(f"{msg}: {n} neu, {s} übersprungen.")
if __name__ == "__main__":
app = QApplication(sys.argv)
font = QFont("Segoe UI", 10)
app.setFont(font)
splash = None
try:
if os.path.exists("assets/uff_banner.jpeg"):
splash = QSplashScreen(QPixmap("assets/uff_banner.jpeg"))
splash.show()
except: pass
w = UffWindow(splash)
w.show()
w.start_model_loading()