File size: 26,784 Bytes
71b065c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
import gradio as gr
import chromadb
import google.generativeai as genai
import os
from dotenv import load_dotenv
import logging
import functools
from collections import defaultdict

# --- Configuration ---
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

# Load environment variables (for API Key)
load_dotenv()
API_KEY = os.getenv("GEMINI_API_KEY")
if not API_KEY:
    logging.error("GEMINI_API_KEY not found in environment variables.")
else:
    try:
        genai.configure(api_key=API_KEY)
        logging.info("Gemini API configured successfully.")
    except Exception as e:
        logging.error(f"Error configuring Gemini API: {e}")

# Chroma DB Configuration
CHROMA_DB_PATH = "./chroma"
COLLECTION_NAME = "phil_de"

# Gemini Embedding Model Configuration
# Make sure this matches the model used to create the DB (expecting 3072 dims based on past errors)
EMBEDDING_MODEL = "models/gemini-embedding-exp-03-07"
logging.info(f"Using embedding model: {EMBEDDING_MODEL}")

# --- Constants ---
MAX_RESULTS = 20

# --- ChromaDB Connection and Author Fetching ---
collection = None
unique_authors = []
try:
    client = chromadb.PersistentClient(path=CHROMA_DB_PATH)
    collection = client.get_collection(name=COLLECTION_NAME)
    logging.info(f"Successfully connected to ChromaDB collection '{COLLECTION_NAME}'. Collection count: {collection.count()}")

    logging.info("Fetching all metadata to extract unique authors...")
    all_metadata = collection.get(include=['metadatas'])
    if all_metadata and 'metadatas' in all_metadata and all_metadata['metadatas']:
        authors_set = set()
        for meta in all_metadata['metadatas']:
            if meta and 'author' in meta and meta['author']:
                 authors_set.add(meta['author'])
        unique_authors = sorted(list(authors_set))
        logging.info(f"Found {len(unique_authors)} unique authors.")
    else:
        logging.warning("Could not retrieve metadata or no metadata found to extract authors.")

except Exception as e:
    logging.critical(f"FATAL: Could not connect to Chroma DB or fetch authors: {e}", exc_info=True)
    unique_authors = []

# --- Embedding Function ---
def get_embedding(text, task="RETRIEVAL_QUERY"):
    if not API_KEY:
        logging.error("Cannot generate embedding: API key not configured.")
        return None
    if not text:
        logging.warning("Embedding requested for empty text.")
        return None
    try:
        logging.info(f"Generating embedding for task: {task}")
        result = genai.embed_content(
            model=EMBEDDING_MODEL,
            content=text,
            task_type=task
        )
        logging.info("Embedding generated successfully.")
        return result['embedding']
    except Exception as e:
        logging.error(f"Error generating Gemini embedding: {e}", exc_info=True)
        if "model" in str(e).lower() and ("not found" in str(e).lower() or "permission" in str(e).lower()):
             logging.error(f"The configured embedding model '{EMBEDDING_MODEL}' might be incorrect, unavailable, or lack permissions.")
        elif "dimension" in str(e).lower():
             logging.error(f"Potential dimension mismatch issue with model '{EMBEDDING_MODEL}'.")
        return None


# --- Helper: Format Single Result (for top display area) ---
def format_single_result(result_data, index, total_results):
    """Formats the data for a single result into Markdown for the top preview area."""
    if not result_data:
        return "No result data available."

    metadata = result_data.get('metadata', {})
    doc = result_data.get('document', "N/A")
    distance = result_data.get('distance', float('inf'))

    author = metadata.get('author', 'N/A')
    book = metadata.get('book', 'N/A')
    section = metadata.get('section', 'N/A')

    md_content = ""
    md_content += f"* **Author:** {author}\n"
    md_content += f"* **Book:** {book}\n"
    if section not in ['Unknown', 'N/A', None]:
        md_content += f"* **Section:** {section}\n"
    md_content += f"* **Distance:** {distance:.4f}\n\n"
    md_content += f"> {doc}\n\n"
    return md_content

# --- Helper: Format Reading Passage (Deprecated - formatting now done in format_context_markdown) ---
# def format_reading_passage(passage_data): # No longer needed as separate function
#     ...

# --- Context Formatting Helper ---
def format_context_markdown(passages):
    """
    Formats a list of passage dictionaries into a seamless Markdown string
    for the reading area, *without* a header.
    """
    if not passages:
        return ""

    valid_passages = [p for p in passages if p and p.get('id') is not None]
    valid_passages.sort(key=lambda p: int(p.get('id', -1)))

    if not valid_passages:
        return ""

    # Combine Passage Texts
    full_text = ""
    for i, passage in enumerate(valid_passages):
        doc = passage.get('doc', '_Passage text missing_')
        role = passage.get('role', 'context') # Includes 'current_reading', 'prev', 'next'

        if role == 'missing':
            continue # Skip placeholders like "Beginning/End of document"

        full_text += doc

        # Add separator if not the last passage and next isn't missing
        if i < len(valid_passages) - 1:
            if valid_passages[i+1].get('role') != 'missing':
                 full_text += "\n\n"

    return full_text

# --- Search Function (Complete) ---
def search_philosophical_texts(query, selected_authors):
    """
    Performs search, stores all results in state, displays the first result.
    Returns updates for multiple components and state variables.
    """
    # Initialize updates dictionary with default states
    updates = {
        full_search_results_state: [],
        current_result_index_state: 0,
        single_result_group: gr.Group(visible=False),
        result_index_indicator_md: gr.Markdown(""),
        single_result_display_md: gr.Markdown(""),
        previous_result_button: gr.Button(visible=False),
        next_result_button: gr.Button(visible=False),
        weiterlesen_button: gr.Button(visible=False), # Default to hidden
        context_display: gr.Markdown(""),
        displayed_context_passages: [],
        load_previous_button: gr.Button(visible=False),
        load_next_button: gr.Button(visible=False),
    }

    # --- Pre-computation Checks ---
    if collection is None:
        logging.error("Search attempted but ChromaDB collection is not available.")
        updates[single_result_display_md] = gr.Markdown("Error: Database connection failed.")
        updates[single_result_group] = gr.Group(visible=True) # Show group to display error
        return updates

    if not query:
        logging.warning("Empty query received.")
        updates[single_result_display_md] = gr.Markdown("Please enter a query.")
        updates[single_result_group] = gr.Group(visible=True) # Show group to display message
        return updates

    logging.info(f"Received query: '{query[:50]}...'")
    logging.info(f"Selected Authors for filtering: {selected_authors}")

    # --- Embedding ---
    query_embedding = get_embedding(query, task="RETRIEVAL_QUERY")
    if query_embedding is None:
        logging.error("Failed to generate query embedding.")
        updates[single_result_display_md] = gr.Markdown("Error: Failed to generate query embedding.")
        updates[single_result_group] = gr.Group(visible=True)
        return updates

    # --- Filtering ---
    where_filter = None
    if selected_authors:
        where_filter = {"author": {"$in": selected_authors}}
        logging.info(f"Applying WHERE filter: {where_filter}")

    # --- Query Execution and Result Processing ---
    try:
        logging.info(f"Querying collection '{COLLECTION_NAME}' for top {MAX_RESULTS} results.")

        # --->>> ACTUAL QUERY CALL <<<---
        results = collection.query(
            query_embeddings=[query_embedding],
            n_results=MAX_RESULTS,
            where=where_filter,
            include=['documents', 'metadatas', 'distances'] # IDs are included by default
        )
        # --->>> END QUERY CALL <<<---

        # Process results if found
        all_results_data = []
        if results and results.get('ids') and results['ids'][0]:
            num_found = len(results['ids'][0])
            logging.info(f"Query successful. Found {num_found} results.")

            ids_list = results['ids'][0]
            docs_list = results['documents'][0]
            metadatas_list = results['metadatas'][0]
            distances_list = results['distances'][0]

            # --->>> ACTUAL RESULT PROCESSING LOOP <<<---
            for i in range(num_found):
                 # Validate ID conversion (just in case)
                try:
                    _ = int(ids_list[i]) # Check if convertible
                except ValueError:
                    logging.warning(f"Skipping result with non-integer ID: {ids_list[i]}")
                    continue

                all_results_data.append({
                    "id": ids_list[i],
                    "document": docs_list[i],
                    "metadata": metadatas_list[i],
                    "distance": distances_list[i]
                })
            # --->>> END RESULT PROCESSING LOOP <<<---

            if all_results_data:
                # Results found and processed successfully
                updates[full_search_results_state] = all_results_data
                updates[current_result_index_state] = 0
                first_result_md = format_single_result(all_results_data[0], 0, len(all_results_data))
                updates[single_result_display_md] = gr.Markdown(first_result_md)
                updates[single_result_group] = gr.Group(visible=True) # Show group
                updates[result_index_indicator_md] = gr.Markdown(f"Result **1** of **{len(all_results_data)}**")
                updates[previous_result_button] = gr.Button(visible=True, interactive=False)
                updates[next_result_button] = gr.Button(visible=True, interactive=(len(all_results_data) > 1))
                updates[weiterlesen_button] = gr.Button(visible=True) # Show this button
            else:
                 # Query returned results, but none were valid after processing
                 logging.info("No valid results found after filtering/validation.")
                 updates[single_result_display_md] = gr.Markdown("No results found matching your query and filters.")
                 updates[single_result_group] = gr.Group(visible=True) # Show message
                 updates[weiterlesen_button] = gr.Button(visible=False) # Hide button

        else:
            # Query returned no results
            logging.info("No results found for the query (or matching the filter).")
            updates[single_result_display_md] = gr.Markdown("No results found matching your query and filters.")
            updates[single_result_group] = gr.Group(visible=True) # Show message
            updates[weiterlesen_button] = gr.Button(visible=False) # Hide button

        return updates

    # --->>> ACTUAL EXCEPTION HANDLING <<<---
    except Exception as e:
        logging.error(f"Error querying ChromaDB or processing results: {e}", exc_info=True)

        # Define error_msg based on the exception
        if "dimension" in str(e).lower():
             error_msg = "**Error:** Database search failed due to embedding mismatch. Please check configuration."
        else:
             # Display the actual error message type from the exception
             error_msg = f"**Error:** An unexpected error occurred during search. See logs for details. ({type(e).__name__})"

        # Update the UI to show the error message
        updates[single_result_display_md] = gr.Markdown(error_msg)
        updates[single_result_group] = gr.Group(visible=True) # Show the group to display the error
        # Reset state on error
        updates[full_search_results_state] = []
        updates[current_result_index_state] = 0
        updates[weiterlesen_button] = gr.Button(visible=False)
        updates[previous_result_button] = gr.Button(visible=False)
        updates[next_result_button] = gr.Button(visible=False)
        updates[result_index_indicator_md] = gr.Markdown("")
        updates[context_display] = gr.Markdown("")
        updates[displayed_context_passages] = []
        updates[load_previous_button] = gr.Button(visible=False)
        updates[load_next_button] = gr.Button(visible=False)

        return updates
    # --->>> END EXCEPTION HANDLING <<<---


# --- Result Navigation Function ---
def navigate_results(direction, current_index, full_results):
    """Handles moving between search results in the top display area."""
    updates = {}
    if not full_results:
        logging.warning("Navigate called with no results in state.")
        return { current_result_index_state: 0 }

    total_results = len(full_results)
    new_index = current_index

    if direction == 'previous':
        new_index = max(0, current_index - 1)
    elif direction == 'next':
        new_index = min(total_results - 1, current_index + 1)

    # Only update display if the index actually changed
    if new_index != current_index:
        logging.info(f"Navigating from result index {current_index} to {new_index}")
        result_data = full_results[new_index]
        result_md = format_single_result(result_data, new_index, total_results)
        updates[single_result_display_md] = gr.Markdown(result_md)
        updates[current_result_index_state] = new_index
        updates[result_index_indicator_md] = gr.Markdown(f"Result **{new_index + 1}** of **{total_results}**")
        updates[context_display] = gr.Markdown("") # Clear reading area
        updates[displayed_context_passages] = []
        updates[load_previous_button] = gr.Button(visible=False)
        updates[load_next_button] = gr.Button(visible=False)
        updates[weiterlesen_button] = gr.Button(visible=True) # Make visible again

    # Update navigation button interactivity based on the *new* index
    updates[previous_result_button] = gr.Button(interactive=(new_index > 0))
    updates[next_result_button] = gr.Button(interactive=(new_index < total_results - 1))

    # If index didn't change, ensure button states are still returned correctly
    if new_index == current_index:
         # Ensure weiterlesen visibility is returned if index didn't change
         # (it should already be visible unless user clicked at boundary where it was hidden)
         # Let's explicitly set it visible for safety upon any nav click if results exist
         if total_results > 0:
             updates[weiterlesen_button] = gr.Button(visible=True)

    return updates


# --- Fetch Single Passage Helper ---
def fetch_passage_data(passage_id_int):
    """Fetches a single passage dictionary from ChromaDB by its integer ID."""
    if collection is None or passage_id_int < 0:
        return None
    try:
        passage_id_str = str(passage_id_int)
        result = collection.get(ids=[passage_id_str], include=['documents', 'metadatas'])
        if result and result.get('ids') and result['ids']:
            return {
                'id': result['ids'][0],
                'doc': result['documents'][0] if result.get('documents') else "N/A",
                'meta': result['metadatas'][0] if result.get('metadatas') else {},
            }
        else:
            logging.info(f"Passage ID {passage_id_str} not found in collection.")
            return None
    except Exception as e:
        logging.error(f"Error fetching passage ID {passage_id_int} from ChromaDB: {e}", exc_info=True)
        return None


# --- Move Passage to Reading Area ---
def move_to_reading_area(current_index, full_results):
    """
    Moves the selected result passage's text to the reading area below,
    hides the 'weiterlesen' button, and enables context loading buttons.
    Keeps the metadata preview in the top area.
    """
    updates = {
        # Keep top preview area unchanged
        # Prepare context/reading area
        context_display: gr.Markdown("_Loading reading passage..._"),
        displayed_context_passages: [],
        load_previous_button: gr.Button(visible=False),
        load_next_button: gr.Button(visible=False),
        weiterlesen_button: gr.Button(visible=False) # Hide this button
    }

    if not full_results or current_index < 0 or current_index >= len(full_results):
        logging.warning(f"Attempted to move passage with invalid state or index. Index: {current_index}, Results Count: {len(full_results)}")
        updates[context_display] = gr.Markdown("Error: Could not load passage reference.")
        updates[weiterlesen_button] = gr.Button(visible=False)
        return updates

    try:
        target_result_data = full_results[current_index]
        reading_passage_state_data = {
            'id': target_result_data.get('id'),
            'doc': target_result_data.get('document'),
            'meta': target_result_data.get('metadata'),
            'role': 'current_reading'
        }

        if not reading_passage_state_data['id'] or not reading_passage_state_data['doc']:
             logging.error(f"Cannot move passage: Missing ID or document in result at index {current_index}.")
             updates[context_display] = gr.Markdown("Error: Selected passage data is incomplete.")
             updates[weiterlesen_button] = gr.Button(visible=False)
             return updates

        formatted_passage_md = format_context_markdown([reading_passage_state_data])

        updates[context_display] = gr.Markdown(formatted_passage_md)
        updates[displayed_context_passages] = [reading_passage_state_data]
        updates[load_previous_button] = gr.Button(visible=True)
        updates[load_next_button] = gr.Button(visible=True)

        logging.info(f"Moved passage ID {reading_passage_state_data['id']} to reading area.")
        return updates

    except Exception as e:
        logging.error(f"Error moving passage for result index {current_index}: {e}", exc_info=True)
        updates[context_display] = gr.Markdown(f"Error moving passage to reading area: {e}")
        updates[weiterlesen_button] = gr.Button(visible=False)
        return updates


# --- Load More Context Function ---
def load_more_context(direction, current_passages_state):
    """
    Loads one more passage either before or after the passages in the reading/context area.
    Updates the Markdown display and the context state list.
    """
    if collection is None:
        return "Error: Database connection failed.", current_passages_state
    if not current_passages_state:
        logging.warning("Load more context called with empty state.")
        return "_No reading passage loaded yet._", []

    current_passages_state.sort(key=lambda p: int(p.get('id', -1)))
    updated_passages = list(current_passages_state)

    try:
        if direction == 'previous':
            earliest_id_str = updated_passages[0].get('id')
            if earliest_id_str is None: return format_context_markdown(updated_passages), updated_passages
            earliest_id_int = int(earliest_id_str)
            id_to_fetch = earliest_id_int - 1

            if id_to_fetch < 0:
                if not (updated_passages[0].get('role') == 'missing' and updated_passages[0].get('id') == '-1'):
                     if updated_passages[0].get('role') == 'missing': updated_passages.pop(0)
                     updated_passages.insert(0, {'id': '-1', 'role': 'missing', 'doc': '_(Beginning of document reached)_'})
            else:
                new_passage_data = fetch_passage_data(id_to_fetch)
                if new_passage_data:
                    new_passage_data['role'] = 'prev'
                    if updated_passages[0].get('role') == 'missing' and updated_passages[0].get('id') == str(id_to_fetch + 1):
                        updated_passages.pop(0)
                    updated_passages.insert(0, new_passage_data)
                else:
                     if not (updated_passages[0].get('role') == 'missing' and updated_passages[0].get('id') == str(id_to_fetch)):
                        if updated_passages[0].get('role') == 'missing': updated_passages.pop(0)
                        updated_passages.insert(0, {'id': str(id_to_fetch), 'role': 'missing', 'doc': '_(Beginning of document reached)_'})

        elif direction == 'next':
            latest_id_str = updated_passages[-1].get('id')
            if latest_id_str is None: return format_context_markdown(updated_passages), updated_passages
            latest_id_int = int(latest_id_str)
            id_to_fetch = latest_id_int + 1

            new_passage_data = fetch_passage_data(id_to_fetch)
            if new_passage_data:
                new_passage_data['role'] = 'next'
                if updated_passages[-1].get('role') == 'missing' and updated_passages[-1].get('id') == str(id_to_fetch -1):
                    updated_passages.pop(-1)
                updated_passages.append(new_passage_data)
            else:
                if not (updated_passages[-1].get('role') == 'missing' and updated_passages[-1].get('id') == str(id_to_fetch)):
                    if updated_passages[-1].get('role') == 'missing': updated_passages.pop(-1)
                    updated_passages.append({'id': str(id_to_fetch), 'role': 'missing', 'doc': '_(End of document reached)_'})

        context_md = format_context_markdown(updated_passages)
        return context_md, updated_passages

    except ValueError:
         logging.error(f"Error converting passage ID to integer in load_more_context. State: {current_passages_state}", exc_info=True)
         error_message = format_context_markdown(current_passages_state) + "\n\n**Error processing context expansion.**"
         return error_message, current_passages_state
    except Exception as e:
        logging.error(f"Error loading more context (direction: {direction}): {e}", exc_info=True)
        error_message = format_context_markdown(current_passages_state) + f"\n\n**Error loading passage: {e}**"
        return error_message, current_passages_state


# --- Gradio UI Definition ---
with gr.Blocks(theme=gr.themes.Default()) as demo:
    gr.Markdown("# Philosophical Text Search & Context Explorer")

    # --- State Variables ---
    full_search_results_state = gr.State([])
    current_result_index_state = gr.State(0)
    displayed_context_passages = gr.State([])

    # --- Search Input Row ---
    with gr.Row():
        query_input = gr.Textbox(label="Enter query", placeholder="z. B. 'Was ist der Unterschied zwischen Herstellen und Handeln?'", lines=2, scale=3)
        author_dropdown = gr.Dropdown(
            label="Filter by Author(s) (Optional)",
            choices=unique_authors,
            multiselect=True,
            scale=2
        )
        search_button = gr.Button("Search", variant="primary", scale=1)

    # --- Result Navigation Row (MOVED HERE) ---
    with gr.Row():
            previous_result_button = gr.Button("⬅️", visible=False)
            next_result_button = gr.Button("➡️", visible=False)

    gr.Markdown("---") # Separator after search and navigation

    # --- Single Result Display Area ---
    # Contains the preview text and the "weiterlesen" button
    with gr.Column(visible=True) as results_area:
        with gr.Group(visible=False) as single_result_group:
            result_index_indicator_md = gr.Markdown("Result 0 of 0")
            single_result_display_md = gr.Markdown("...") # Shows the preview
            # "weiterlesen" button remains at the end of the preview group
            weiterlesen_button = gr.Button("weiterlesen", variant="secondary", visible=True)

    gr.Markdown("---") # Separator before reading area

    # --- Context / Reading Area ---
    with gr.Column(visible=True) as context_area:
        load_previous_button = gr.Button("⬆️", variant="secondary", visible=False)
        context_display = gr.Markdown(label="Reading Area")
        load_next_button = gr.Button("⬇️", variant="secondary", visible=False)


    # --- Event Handlers (Wiring remains the same) ---

    # Search Button Action
    search_outputs = [
        full_search_results_state, current_result_index_state, single_result_group,
        result_index_indicator_md, single_result_display_md, previous_result_button,
        next_result_button, weiterlesen_button, context_display,
        displayed_context_passages, load_previous_button, load_next_button,
    ]
    search_button.click(
        fn=search_philosophical_texts,
        inputs=[query_input, author_dropdown],
        outputs=search_outputs
    )

    # Previous/Next Result Button Actions
    nav_outputs = [ # Combined list for prev/next
        single_result_display_md, current_result_index_state, result_index_indicator_md,
        previous_result_button, next_result_button, weiterlesen_button,
        context_display, displayed_context_passages,
        load_previous_button, load_next_button,
    ]
    previous_result_button.click(
        fn=navigate_results,
        inputs=[gr.State('previous'), current_result_index_state, full_search_results_state],
        outputs=nav_outputs
    )
    next_result_button.click(
        fn=navigate_results,
        inputs=[gr.State('next'), current_result_index_state, full_search_results_state],
        outputs=nav_outputs
    )

    # "weiterlesen" Button Action
    weiterlesen_outputs = [
        context_display, displayed_context_passages,
        load_previous_button, load_next_button,
        weiterlesen_button # Target button itself to control visibility
    ]
    weiterlesen_button.click(
        fn=move_to_reading_area,
        inputs=[current_result_index_state, full_search_results_state],
        outputs=weiterlesen_outputs
    )

    # Load More Context Buttons
    load_previous_button.click(
        fn=load_more_context,
        inputs=[gr.State('previous'), displayed_context_passages],
        outputs=[context_display, displayed_context_passages]
    )
    load_next_button.click(
        fn=load_more_context,
        inputs=[gr.State('next'), displayed_context_passages],
        outputs=[context_display, displayed_context_passages]
    )

# --- Launch the Application ---
if __name__ == "__main__":
    if collection is None:
        print("\n--- ERROR: ChromaDB collection failed to load. UI might not function correctly. Check logs. ---\n")
    elif not unique_authors:
         print("\n--- WARNING: No unique authors found in DB metadata. Author filter will be empty. ---\n")

    print("Launching Gradio Interface...")
    # Make sure debug=True is helpful during testing
    demo.launch(server_name="0.0.0.0", share=False, debug=True)