{
"tool_response": {
"type": "text",
"file": {
"filePath": "\/var\/www\/dev.campus.systemische-tools.de\/src\/Controller\/SemanticExplorerController.php",
"content": "<?php\n\nnamespace Controller;\n\nuse Framework\\Controller;\n\n\/**\n * SemanticExplorerController - Nutzdaten Explorer\n *\n * Zeigt Dokumente und Chunks aus Nextcloud (documents, chunks Tabellen).\n * Für Endnutzer - Coaching-Materialien, PDFs, später Mails.\n *\/\nclass SemanticExplorerController extends Controller\n{\n private \\PDO $db;\n\n public function __construct()\n {\n $this->db = new \\PDO(\n 'mysql:host=localhost;dbname=ki_content;charset=utf8mb4',\n 'root',\n $this->getPassword(),\n [\n \\PDO::ATTR_ERRMODE => \\PDO::ERRMODE_EXCEPTION,\n \\PDO::ATTR_DEFAULT_FETCH_MODE => \\PDO::FETCH_ASSOC,\n ]\n );\n }\n\n private function getPassword(): string\n {\n $file = '\/var\/www\/docs\/credentials\/credentials.md';\n $content = file_get_contents($file);\n foreach (explode(\"\\n\", $content) as $line) {\n if (str_contains($line, 'MariaDB') && str_contains($line, 'root')) {\n $parts = explode('|', $line);\n if (count($parts) >= 4) {\n return trim($parts[3]);\n }\n }\n }\n\n return '';\n }\n\n \/**\n * GET \/semantic-explorer\n * Dashboard mit Statistiken\n *\/\n public function index(): void\n {\n \/\/ Dokument-Statistiken\n $docStats = $this->db->query(\n 'SELECT\n COUNT(*) as total,\n SUM(CASE WHEN status = \"done\" THEN 1 ELSE 0 END) as processed,\n SUM(CASE WHEN status = \"error\" THEN 1 ELSE 0 END) as errors\n FROM documents'\n )->fetch();\n\n \/\/ Chunk-Statistiken\n $chunkStats = $this->db->query(\n 'SELECT\n COUNT(*) as total,\n COALESCE(SUM(token_count), 0) as tokens,\n SUM(CASE WHEN qdrant_id IS NOT NULL THEN 1 ELSE 0 END) as embedded\n FROM chunks'\n )->fetch();\n\n \/\/ Dokumente\n $documents = $this->db->query(\n 'SELECT d.id, d.filename, d.folder_path, d.mime_type, d.file_size,\n d.status, d.imported_at, d.processed_at,\n (SELECT COUNT(*) FROM chunks WHERE document_id = d.id) as chunk_count\n FROM documents d\n ORDER BY d.imported_at DESC'\n )->fetchAll();\n\n \/\/ Neueste Chunks\n $recentChunks = $this->db->query(\n 'SELECT c.id, c.content, c.token_count, c.created_at, c.qdrant_id,\n d.filename\n FROM chunks c\n JOIN documents d ON c.document_id = d.id\n ORDER BY c.created_at DESC\n LIMIT 5'\n )->fetchAll();\n\n $this->view('semantic-explorer.index', [\n 'title' => 'Semantic Explorer',\n 'docStats' => $docStats,\n 'chunkStats' => $chunkStats,\n 'documents' => $documents,\n 'recentChunks' => $recentChunks,\n ]);\n }\n\n \/**\n * GET \/semantic-explorer\/dokumente\n * Liste aller Dokumente\n *\/\n public function dokumente(): void\n {\n $status = $_GET['status'] ?? '';\n $search = $_GET['search'] ?? '';\n\n $sql = 'SELECT d.id, d.filename, d.folder_path, d.source_path, d.mime_type,\n d.file_size, d.status, d.imported_at, d.processed_at, d.error_message,\n (SELECT COUNT(*) FROM chunks WHERE document_id = d.id) as chunk_count,\n (SELECT COALESCE(SUM(token_count), 0) FROM chunks WHERE document_id = d.id) as token_count\n FROM documents d\n WHERE 1=1';\n\n $params = [];\n\n if ($status !== '') {\n $sql .= ' AND d.status = :status';\n $params['status'] = $status;\n }\n\n if ($search !== '') {\n $sql .= ' AND (d.filename LIKE :search OR d.source_path LIKE :search2)';\n $params['search'] = '%' . $search . '%';\n $params['search2'] = '%' . $search . '%';\n }\n\n $sql .= ' ORDER BY d.imported_at DESC';\n\n $stmt = $this->db->prepare($sql);\n $stmt->execute($params);\n $documents = $stmt->fetchAll();\n\n $this->view('semantic-explorer.dokumente.index', [\n 'title' => 'Dokumente',\n 'documents' => $documents,\n 'currentStatus' => $status,\n 'currentSearch' => $search,\n ]);\n }\n\n \/**\n * GET \/semantic-explorer\/dokumente\/{id}\n * Dokument-Details mit Chunks\n *\/\n public function dokumentShow(int $id): void\n {\n $stmt = $this->db->prepare('SELECT * FROM documents WHERE id = :id');\n $stmt->execute(['id' => $id]);\n $document = $stmt->fetch();\n\n if ($document === false) {\n http_response_code(404);\n echo '404 - Dokument nicht gefunden';\n\n return;\n }\n\n \/\/ Chunks dieses Dokuments\n $stmt = $this->db->prepare(\n 'SELECT c.id, c.chunk_index, c.content, c.token_count, c.heading_path,\n c.metadata, c.qdrant_id, c.created_at\n FROM chunks c\n WHERE c.document_id = :id\n ORDER BY c.chunk_index'\n );\n $stmt->execute(['id' => $id]);\n $chunks = $stmt->fetchAll();\n\n \/\/ Heading-Paths dekodieren\n foreach ($chunks as &$chunk) {\n $chunk['heading_path_decoded'] = json_decode($chunk['heading_path'] ?? '[]', true) ?: [];\n $chunk['metadata_decoded'] = json_decode($chunk['metadata'] ?? '{}', true) ?: [];\n }\n\n $this->view('semantic-explorer.dokumente.show', [\n 'title' => $document['filename'],\n 'document' => $document,\n 'chunks' => $chunks,\n ]);\n }\n\n \/**\n * GET \/semantic-explorer\/chunks\n * Liste aller Chunks\n *\/\n public function chunks(): void\n {\n $search = $_GET['search'] ?? '';\n $embedded = $_GET['embedded'] ?? '';\n $page = max(1, (int) ($_GET['page'] ?? 1));\n $limit = 50;\n $offset = ($page - 1) * $limit;\n\n $sql = 'SELECT c.id, c.chunk_index, c.content, c.token_count, c.qdrant_id, c.created_at,\n d.filename, d.id as document_id\n FROM chunks c\n JOIN documents d ON c.document_id = d.id\n WHERE 1=1';\n\n $countSql = 'SELECT COUNT(*) FROM chunks c\n JOIN documents d ON c.document_id = d.id\n WHERE 1=1';\n\n $params = [];\n\n if ($search !== '') {\n $sql .= ' AND c.content LIKE :search';\n $countSql .= ' AND c.content LIKE :search';\n $params['search'] = '%' . $search . '%';\n }\n\n if ($embedded === 'yes') {\n $sql .= ' AND c.qdrant_id IS NOT NULL';\n $countSql .= ' AND c.qdrant_id IS NOT NULL';\n } elseif ($embedded === 'no') {\n $sql .= ' AND c.qdrant_id IS NULL';\n $countSql .= ' AND c.qdrant_id IS NULL';\n }\n\n \/\/ Count total\n $countStmt = $this->db->prepare($countSql);\n $countStmt->execute($params);\n $totalCount = $countStmt->fetchColumn();\n\n $sql .= ' ORDER BY c.created_at DESC LIMIT ' . $limit . ' OFFSET ' . $offset;\n\n $stmt = $this->db->prepare($sql);\n $stmt->execute($params);\n $chunks = $stmt->fetchAll();\n\n $this->view('semantic-explorer.chunks.index', [\n 'title' => 'Chunks',\n 'chunks' => $chunks,\n 'currentSearch' => $search,\n 'currentEmbedded' => $embedded,\n 'currentPage' => $page,\n 'totalCount' => $totalCount,\n 'totalPages' => ceil($totalCount \/ $limit),\n ]);\n }\n\n \/**\n * GET \/semantic-explorer\/chunks\/{id}\n * Chunk-Details\n *\/\n public function chunkShow(int $id): void\n {\n $stmt = $this->db->prepare(\n 'SELECT c.*, d.filename, d.source_path, d.id as document_id\n FROM chunks c\n JOIN documents d ON c.document_id = d.id\n WHERE c.id = :id'\n );\n $stmt->execute(['id' => $id]);\n $chunk = $stmt->fetch();\n\n if ($chunk === false) {\n http_response_code(404);\n echo '404 - Chunk nicht gefunden';\n\n return;\n }\n\n \/\/ JSON-Felder dekodieren\n $chunk['heading_path_decoded'] = json_decode($chunk['heading_path'] ?? '[]', true) ?: [];\n $chunk['metadata_decoded'] = json_decode($chunk['metadata'] ?? '{}', true) ?: [];\n\n \/\/ Nachbar-Chunks\n $stmt = $this->db->prepare(\n 'SELECT id, chunk_index FROM chunks\n WHERE document_id = :doc_id AND chunk_index = :prev'\n );\n $stmt->execute(['doc_id' => $chunk['document_id'], 'prev' => $chunk['chunk_index'] - 1]);\n $prevChunk = $stmt->fetch();\n\n $stmt = $this->db->prepare(\n 'SELECT id, chunk_index FROM chunks\n WHERE document_id = :doc_id AND chunk_index = :next'\n );\n $stmt->execute(['doc_id' => $chunk['document_id'], 'next' => $chunk['chunk_index'] + 1]);\n $nextChunk = $stmt->fetch();\n\n $this->view('semantic-explorer.chunks.show', [\n 'title' => 'Chunk #' . $chunk['id'],\n 'chunk' => $chunk,\n 'prevChunk' => $prevChunk,\n 'nextChunk' => $nextChunk,\n ]);\n }\n\n \/**\n * GET \/semantic-explorer\/suche\n * Semantische Suche in Nutzdaten\n *\/\n public function suche(): void\n {\n $query = $_GET['q'] ?? '';\n $limit = min(20, max(1, (int) ($_GET['limit'] ?? 10)));\n\n $results = [];\n\n if ($query !== '') {\n \/\/ Vektor-Suche via Qdrant\n $results = $this->vectorSearch($query, $limit);\n }\n\n $this->view('semantic-explorer.suche', [\n 'title' => 'Semantische Suche',\n 'query' => $query,\n 'results' => $results,\n 'limit' => $limit,\n ]);\n }\n\n \/**\n * Vektor-Suche in... [TRUNCATED-c09ecc2cb5a8fff7]",
"numLines": 745,
"startLine": 1,
"totalLines": 745
}
}
}