openfree commited on
Commit
6d93c4e
Β·
verified Β·
1 Parent(s): 6f6b5f6

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +944 -51
app.py CHANGED
@@ -82,9 +82,30 @@ class NovelDatabase:
82
  )
83
  ''')
84
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  # Create indices
86
  cursor.execute('CREATE INDEX IF NOT EXISTS idx_session_id ON stages(session_id)')
87
  cursor.execute('CREATE INDEX IF NOT EXISTS idx_stage_number ON stages(stage_number)')
 
 
88
 
89
  conn.commit()
90
 
@@ -206,6 +227,119 @@ class NovelDatabase:
206
  ''', (final_novel, session_id))
207
  conn.commit()
208
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
209
  @staticmethod
210
  def get_active_sessions() -> List[Dict]:
211
  """Get all active sessions"""
@@ -235,7 +369,7 @@ class NovelWritingSystem:
235
 
236
  # Session management
237
  self.current_session_id = None
238
- self.auto_scroll = False # Auto-scroll control
239
 
240
  # μ†Œμ„€ μž‘μ„± μ§„ν–‰ μƒνƒœ
241
  self.novel_state = {
@@ -1568,6 +1702,9 @@ Last page content from Writer {writer_num}..."""
1568
  # Save final novel to DB
1569
  NovelDatabase.update_final_novel(self.current_session_id, final_novel)
1570
 
 
 
 
1571
  # Final yield
1572
  yield final_novel, stages
1573
 
@@ -1663,7 +1800,7 @@ Last page content from Writer {writer_num}..."""
1663
  return ""
1664
 
1665
  # Gradio Interface Functions
1666
- def process_query(query: str, language: str, session_id: str = None) -> Generator[Tuple[str, str, str], None, None]:
1667
  """Process query and yield updates"""
1668
  if not query.strip() and not session_id:
1669
  if language == "Korean":
@@ -1673,11 +1810,12 @@ def process_query(query: str, language: str, session_id: str = None) -> Generato
1673
  return
1674
 
1675
  system = NovelWritingSystem()
 
1676
 
1677
  try:
1678
  for final_novel, stages in system.process_novel_stream(query, language, session_id):
1679
  # Format stages for display with scroll control
1680
- stages_html = format_stages_html(stages, language, system.auto_scroll)
1681
 
1682
  status = "πŸ”„ Processing..." if not final_novel else "βœ… Complete!"
1683
 
@@ -1690,11 +1828,190 @@ def process_query(query: str, language: str, session_id: str = None) -> Generato
1690
  else:
1691
  yield "", "", f"❌ Error occurred: {str(e)}"
1692
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1693
  def format_stages_html(stages: List[Dict[str, str]], language: str, auto_scroll: bool = False) -> str:
1694
  """Format stages into HTML with scroll control"""
1695
- scroll_behavior = "auto" if auto_scroll else "manual"
1696
 
1697
- html = f'<div class="stages-container" data-scroll="{scroll_behavior}">'
 
 
 
1698
 
1699
  for idx, stage in enumerate(stages):
1700
  status_icon = "βœ…" if stage.get("status") == "complete" else ("⏳" if stage.get("status") == "active" else "❌")
@@ -1715,28 +2032,153 @@ def format_stages_html(stages: List[Dict[str, str]], language: str, auto_scroll:
1715
 
1716
  html += '</div>'
1717
 
1718
- # Add JavaScript for scroll control
1719
- if not auto_scroll:
1720
- html += '''
1721
- <script>
1722
- // Prevent auto-scrolling when content updates
1723
- const container = document.querySelector('.stages-container[data-scroll="manual"]');
1724
- if (container) {
1725
- let userScrolled = false;
1726
- container.addEventListener('scroll', () => {
1727
- userScrolled = true;
1728
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1729
 
1730
- // Override scroll behavior on content update
1731
- const observer = new MutationObserver(() => {
1732
- if (!userScrolled) {
1733
- // Don't auto-scroll
1734
- }
1735
- });
1736
- observer.observe(container, { childList: true, subtree: true });
1737
- }
1738
- </script>
1739
- '''
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1740
 
1741
  return html
1742
 
@@ -1758,13 +2200,13 @@ def get_active_sessions(language: str) -> List[Tuple[str, str, str]]:
1758
  logger.error(f"Error getting active sessions: {str(e)}")
1759
  return []
1760
 
1761
- def resume_session(session_id: str, language: str) -> Generator[Tuple[str, str, str], None, None]:
1762
  """Resume an existing session"""
1763
  if not session_id:
1764
  return
1765
 
1766
  # Process with existing session ID
1767
- yield from process_query("", language, session_id)
1768
 
1769
  def toggle_auto_scroll(current_state: bool) -> bool:
1770
  """Toggle auto-scroll state"""
@@ -1809,7 +2251,7 @@ def download_novel(novel_text: str, format: str, language: str) -> str:
1809
 
1810
  return filepath
1811
 
1812
- # Custom CSS with improved scroll handling
1813
  custom_css = """
1814
  .gradio-container {
1815
  background: linear-gradient(135deg, #1e3c72, #2a5298);
@@ -1819,17 +2261,28 @@ custom_css = """
1819
  .stages-container {
1820
  max-height: 600px;
1821
  overflow-y: auto;
 
1822
  padding: 20px;
1823
  background-color: rgba(255, 255, 255, 0.05);
1824
  border-radius: 12px;
1825
  backdrop-filter: blur(10px);
1826
- scroll-behavior: smooth;
1827
  position: relative;
1828
  }
1829
 
1830
- /* Disable auto-scroll when user has scrolled */
1831
  .stages-container[data-scroll="manual"] {
1832
- scroll-behavior: auto;
 
 
 
 
 
 
 
 
 
 
 
1833
  }
1834
 
1835
  .stage-section {
@@ -1848,6 +2301,7 @@ custom_css = """
1848
  align-items: center;
1849
  gap: 10px;
1850
  color: white;
 
1851
  }
1852
 
1853
  .stage-header:hover {
@@ -1862,18 +2316,28 @@ custom_css = """
1862
  padding: 15px;
1863
  max-height: 400px;
1864
  overflow-y: auto;
 
1865
  background-color: rgba(0, 0, 0, 0.3);
1866
  border-radius: 8px;
1867
  margin-top: 10px;
 
 
1868
  }
1869
 
1870
  .stage-content pre {
1871
  white-space: pre-wrap;
1872
  word-wrap: break-word;
 
1873
  color: #e0e0e0;
1874
  font-family: 'Courier New', monospace;
1875
  line-height: 1.6;
1876
  margin: 0;
 
 
 
 
 
 
1877
  }
1878
 
1879
  #novel-output {
@@ -1919,17 +2383,171 @@ custom_css = """
1919
  color: white;
1920
  }
1921
 
1922
- /* Scroll position indicator */
1923
- .scroll-indicator {
1924
- position: absolute;
1925
- right: 5px;
1926
- top: 5px;
1927
- background: rgba(255, 255, 255, 0.2);
1928
- padding: 5px 10px;
1929
- border-radius: 15px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1930
  font-size: 0.8em;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1931
  color: white;
1932
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1933
  """
1934
 
1935
  # Create Gradio Interface
@@ -1952,7 +2570,7 @@ def create_interface():
1952
  """)
1953
 
1954
  # State management
1955
- auto_scroll_state = gr.State(False)
1956
  current_session_id = gr.State(None)
1957
 
1958
  with gr.Row():
@@ -1992,11 +2610,22 @@ def create_interface():
1992
  refresh_btn = gr.Button("πŸ”„ Refresh / μƒˆλ‘œκ³ μΉ¨", scale=1)
1993
  resume_btn = gr.Button("▢️ Resume / 재개", variant="secondary", scale=1)
1994
 
1995
- # Scroll control
1996
- auto_scroll_checkbox = gr.Checkbox(
1997
- label="Auto-scroll / μžλ™ 슀크둀",
1998
- value=False
1999
- )
 
 
 
 
 
 
 
 
 
 
 
2000
 
2001
  gr.HTML("""
2002
  <div style="margin-top: 20px; padding: 15px; background: rgba(255,255,255,0.1); border-radius: 8px; color: white;">
@@ -2040,10 +2669,49 @@ def create_interface():
2040
  label="Downloaded File / λ‹€μš΄λ‘œλ“œλœ 파일",
2041
  visible=False
2042
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2043
 
2044
  # Hidden state for novel text
2045
  novel_text_state = gr.State("")
2046
 
 
 
 
 
2047
  # Examples
2048
  with gr.Row():
2049
  gr.Examples(
@@ -2059,6 +2727,14 @@ def create_interface():
2059
  label="πŸ’‘ Example Themes / 예제 주제"
2060
  )
2061
 
 
 
 
 
 
 
 
 
2062
  # Event handlers
2063
  def update_novel_state(process, novel, status):
2064
  return process, novel, status, novel
@@ -2071,9 +2747,60 @@ def create_interface():
2071
  logger.error(f"Error refreshing sessions: {str(e)}")
2072
  return gr.update(choices=[])
2073
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2074
  submit_btn.click(
2075
  fn=process_query,
2076
- inputs=[query_input, language_select, current_session_id],
2077
  outputs=[process_display, novel_output, status_text]
2078
  ).then(
2079
  fn=update_novel_state,
@@ -2087,7 +2814,7 @@ def create_interface():
2087
  outputs=[current_session_id]
2088
  ).then(
2089
  fn=resume_session,
2090
- inputs=[current_session_id, language_select],
2091
  outputs=[process_display, novel_output, status_text]
2092
  )
2093
 
@@ -2097,8 +2824,8 @@ def create_interface():
2097
  )
2098
 
2099
  clear_btn.click(
2100
- fn=lambda: ("", "", "πŸ”„ Ready", "", None),
2101
- outputs=[process_display, novel_output, status_text, novel_text_state, current_session_id]
2102
  )
2103
 
2104
  auto_scroll_checkbox.change(
@@ -2107,6 +2834,64 @@ def create_interface():
2107
  outputs=[auto_scroll_state]
2108
  )
2109
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2110
  def handle_download(novel_text, format_type, language):
2111
  if not novel_text:
2112
  return gr.update(visible=False)
@@ -2123,11 +2908,119 @@ def create_interface():
2123
  outputs=[download_file]
2124
  )
2125
 
2126
- # Load sessions on startup
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2127
  interface.load(
2128
  fn=refresh_sessions,
2129
  outputs=[session_dropdown]
2130
  )
 
 
 
 
 
2131
 
2132
  return interface
2133
 
 
82
  )
83
  ''')
84
 
85
+ # Completed novels table for gallery
86
+ cursor.execute('''
87
+ CREATE TABLE IF NOT EXISTS completed_novels (
88
+ novel_id INTEGER PRIMARY KEY AUTOINCREMENT,
89
+ session_id TEXT UNIQUE NOT NULL,
90
+ title TEXT NOT NULL,
91
+ author_query TEXT NOT NULL,
92
+ language TEXT NOT NULL,
93
+ genre TEXT,
94
+ summary TEXT,
95
+ thumbnail_text TEXT,
96
+ full_content TEXT NOT NULL,
97
+ word_count INTEGER,
98
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
99
+ downloads INTEGER DEFAULT 0,
100
+ FOREIGN KEY (session_id) REFERENCES sessions(session_id)
101
+ )
102
+ ''')
103
+
104
  # Create indices
105
  cursor.execute('CREATE INDEX IF NOT EXISTS idx_session_id ON stages(session_id)')
106
  cursor.execute('CREATE INDEX IF NOT EXISTS idx_stage_number ON stages(stage_number)')
107
+ cursor.execute('CREATE INDEX IF NOT EXISTS idx_created_at ON completed_novels(created_at)')
108
+ cursor.execute('CREATE INDEX IF NOT EXISTS idx_language ON completed_novels(language)')
109
 
110
  conn.commit()
111
 
 
227
  ''', (final_novel, session_id))
228
  conn.commit()
229
 
230
+ @staticmethod
231
+ def save_completed_novel(session_id: str, final_novel: str):
232
+ """Save completed novel to gallery"""
233
+ try:
234
+ with NovelDatabase.get_db() as conn:
235
+ cursor = conn.cursor()
236
+
237
+ # Get session info
238
+ cursor.execute('SELECT user_query, language FROM sessions WHERE session_id = ?', (session_id,))
239
+ session = cursor.fetchone()
240
+ if not session:
241
+ return
242
+
243
+ # Extract title from novel content
244
+ title_match = re.search(r'#\s*\[?([^\]\n]+)\]?', final_novel)
245
+ title = title_match.group(1).strip() if title_match else f"Novel from {session['user_query'][:30]}..."
246
+
247
+ # Extract summary (first substantial paragraph after title)
248
+ lines = final_novel.split('\n')
249
+ summary = ""
250
+ for line in lines[10:]: # Skip headers
251
+ if len(line.strip()) > 50:
252
+ summary = line.strip()[:200] + "..."
253
+ break
254
+
255
+ # Create thumbnail text (first 500 characters of actual content)
256
+ content_start = final_novel.find("[Page 1]") if "[Page 1]" in final_novel else final_novel.find("[νŽ˜μ΄μ§€ 1]")
257
+ if content_start > 0:
258
+ thumbnail_text = final_novel[content_start:content_start+500].strip()
259
+ else:
260
+ thumbnail_text = final_novel[:500].strip()
261
+
262
+ # Count words
263
+ word_count = len(final_novel.split())
264
+
265
+ # Check if already exists
266
+ cursor.execute('SELECT novel_id FROM completed_novels WHERE session_id = ?', (session_id,))
267
+ existing = cursor.fetchone()
268
+
269
+ if existing:
270
+ # Update existing
271
+ cursor.execute('''
272
+ UPDATE completed_novels
273
+ SET title = ?, summary = ?, thumbnail_text = ?,
274
+ full_content = ?, word_count = ?
275
+ WHERE session_id = ?
276
+ ''', (title, summary, thumbnail_text, final_novel, word_count, session_id))
277
+ else:
278
+ # Insert new
279
+ cursor.execute('''
280
+ INSERT INTO completed_novels
281
+ (session_id, title, author_query, language, summary,
282
+ thumbnail_text, full_content, word_count)
283
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
284
+ ''', (session_id, title, session['user_query'], session['language'],
285
+ summary, thumbnail_text, final_novel, word_count))
286
+
287
+ conn.commit()
288
+ logger.info(f"Saved novel '{title}' to gallery")
289
+ except Exception as e:
290
+ logger.error(f"Error saving completed novel: {str(e)}")
291
+
292
+ @staticmethod
293
+ def get_gallery_novels(language: Optional[str] = None, limit: int = 50) -> List[Dict]:
294
+ """Get novels for gallery display"""
295
+ with NovelDatabase.get_db() as conn:
296
+ cursor = conn.cursor()
297
+
298
+ if language:
299
+ cursor.execute('''
300
+ SELECT novel_id, session_id, title, author_query, language,
301
+ genre, summary, thumbnail_text, word_count,
302
+ created_at, downloads
303
+ FROM completed_novels
304
+ WHERE language = ?
305
+ ORDER BY created_at DESC
306
+ LIMIT ?
307
+ ''', (language, limit))
308
+ else:
309
+ cursor.execute('''
310
+ SELECT novel_id, session_id, title, author_query, language,
311
+ genre, summary, thumbnail_text, word_count,
312
+ created_at, downloads
313
+ FROM completed_novels
314
+ ORDER BY created_at DESC
315
+ LIMIT ?
316
+ ''', (limit,))
317
+
318
+ return cursor.fetchall()
319
+
320
+ @staticmethod
321
+ def get_novel_content(novel_id: int) -> Optional[Dict]:
322
+ """Get full novel content"""
323
+ with NovelDatabase.get_db() as conn:
324
+ cursor = conn.cursor()
325
+ cursor.execute('''
326
+ SELECT * FROM completed_novels
327
+ WHERE novel_id = ?
328
+ ''', (novel_id,))
329
+ return cursor.fetchone()
330
+
331
+ @staticmethod
332
+ def increment_download_count(novel_id: int):
333
+ """Increment download counter"""
334
+ with NovelDatabase.get_db() as conn:
335
+ cursor = conn.cursor()
336
+ cursor.execute('''
337
+ UPDATE completed_novels
338
+ SET downloads = downloads + 1
339
+ WHERE novel_id = ?
340
+ ''', (novel_id,))
341
+ conn.commit()
342
+
343
  @staticmethod
344
  def get_active_sessions() -> List[Dict]:
345
  """Get all active sessions"""
 
369
 
370
  # Session management
371
  self.current_session_id = None
372
+ self.auto_scroll = False # Auto-scroll control (default: OFF)
373
 
374
  # μ†Œμ„€ μž‘μ„± μ§„ν–‰ μƒνƒœ
375
  self.novel_state = {
 
1702
  # Save final novel to DB
1703
  NovelDatabase.update_final_novel(self.current_session_id, final_novel)
1704
 
1705
+ # Save to gallery
1706
+ NovelDatabase.save_completed_novel(self.current_session_id, final_novel)
1707
+
1708
  # Final yield
1709
  yield final_novel, stages
1710
 
 
1800
  return ""
1801
 
1802
  # Gradio Interface Functions
1803
+ def process_query(query: str, language: str, session_id: str = None, auto_scroll: bool = False) -> Generator[Tuple[str, str, str], None, None]:
1804
  """Process query and yield updates"""
1805
  if not query.strip() and not session_id:
1806
  if language == "Korean":
 
1810
  return
1811
 
1812
  system = NovelWritingSystem()
1813
+ system.auto_scroll = auto_scroll # Set auto-scroll preference
1814
 
1815
  try:
1816
  for final_novel, stages in system.process_novel_stream(query, language, session_id):
1817
  # Format stages for display with scroll control
1818
+ stages_html = format_stages_html(stages, language, auto_scroll)
1819
 
1820
  status = "πŸ”„ Processing..." if not final_novel else "βœ… Complete!"
1821
 
 
1828
  else:
1829
  yield "", "", f"❌ Error occurred: {str(e)}"
1830
 
1831
+ def create_novel_thumbnail(novel_data: Dict) -> str:
1832
+ """Create HTML thumbnail for a novel"""
1833
+ novel_id = novel_data['novel_id']
1834
+ title = novel_data['title']
1835
+ summary = novel_data['summary'] or novel_data['thumbnail_text'][:150] + "..."
1836
+ word_count = novel_data['word_count']
1837
+ created = datetime.fromisoformat(novel_data['created_at'])
1838
+ date_str = created.strftime("%Y-%m-%d")
1839
+ downloads = novel_data['downloads']
1840
+ language = novel_data['language']
1841
+
1842
+ # Language-specific labels
1843
+ if language == "Korean":
1844
+ word_label = "단어"
1845
+ download_label = "λ‹€μš΄λ‘œλ“œ"
1846
+ else:
1847
+ word_label = "words"
1848
+ download_label = "downloads"
1849
+
1850
+ # Create color based on novel_id for variety
1851
+ colors = ['#667eea', '#f59e0b', '#10b981', '#ef4444', '#3b82f6', '#8b5cf6', '#ec4899', '#14b8a6']
1852
+ color = colors[novel_id % len(colors)]
1853
+
1854
+ return f'''
1855
+ <div class="novel-card" data-novel-id="{novel_id}">
1856
+ <div class="novel-card-header" style="background: linear-gradient(135deg, {color}, {color}dd);">
1857
+ <h3>{title}</h3>
1858
+ </div>
1859
+ <div class="novel-card-body">
1860
+ <p class="novel-summary">{summary}</p>
1861
+ <div class="novel-meta">
1862
+ <span>πŸ“… {date_str}</span>
1863
+ <span>πŸ“ {word_count:,} {word_label}</span>
1864
+ <span>⬇️ {downloads} {download_label}</span>
1865
+ </div>
1866
+ </div>
1867
+ <div class="novel-card-actions">
1868
+ <button class="view-btn" onclick="viewNovel({novel_id})">πŸ‘οΈ View</button>
1869
+ <button class="download-btn" onclick="downloadNovel({novel_id})">πŸ“₯ Download</button>
1870
+ </div>
1871
+ </div>
1872
+ '''
1873
+
1874
+ def get_gallery_html(language: Optional[str] = None) -> str:
1875
+ """Get gallery HTML with all novels"""
1876
+ try:
1877
+ novels = NovelDatabase.get_gallery_novels(language)
1878
+
1879
+ if not novels:
1880
+ if language == "Korean":
1881
+ return '<div class="gallery-empty">아직 μ™„μ„±λœ μ†Œμ„€μ΄ μ—†μŠ΅λ‹ˆλ‹€.</div>'
1882
+ else:
1883
+ return '<div class="gallery-empty">No completed novels yet.</div>'
1884
+
1885
+ gallery_html = '<div class="novel-gallery">'
1886
+ for novel in novels:
1887
+ gallery_html += create_novel_thumbnail(novel)
1888
+ gallery_html += '</div>'
1889
+
1890
+ # Add JavaScript for interactions
1891
+ gallery_html += '''
1892
+ <script>
1893
+ function viewNovel(novelId) {
1894
+ // Trigger view event
1895
+ const event = new CustomEvent('viewNovel', { detail: { novelId: novelId } });
1896
+ window.dispatchEvent(event);
1897
+ }
1898
+
1899
+ function downloadNovel(novelId) {
1900
+ // Trigger download event
1901
+ const event = new CustomEvent('downloadNovel', { detail: { novelId: novelId } });
1902
+ window.dispatchEvent(event);
1903
+ }
1904
+ </script>
1905
+ '''
1906
+
1907
+ return gallery_html
1908
+ except Exception as e:
1909
+ logger.error(f"Error getting gallery HTML: {str(e)}")
1910
+ return '<div class="gallery-empty">Error loading gallery.</div>'
1911
+
1912
+ def view_novel(novel_id: int, language: str) -> str:
1913
+ """View full novel content"""
1914
+ if not novel_id:
1915
+ return ""
1916
+
1917
+ novel = NovelDatabase.get_novel_content(novel_id)
1918
+ if not novel:
1919
+ if language == "Korean":
1920
+ return "μ†Œμ„€μ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€."
1921
+ else:
1922
+ return "Novel not found."
1923
+
1924
+ # Format novel for display
1925
+ content = novel['full_content']
1926
+
1927
+ # Add view header
1928
+ header = f"""
1929
+ <div class="novel-view-header">
1930
+ <h1>{novel['title']}</h1>
1931
+ <div class="novel-view-meta">
1932
+ <span>πŸ“… {novel['created_at']}</span>
1933
+ <span>πŸ“ {novel['word_count']:,} words</span>
1934
+ <span>🌐 {novel['language']}</span>
1935
+ </div>
1936
+ <div class="novel-view-query">
1937
+ <strong>Original prompt:</strong> {novel['author_query']}
1938
+ </div>
1939
+ </div>
1940
+ <hr>
1941
+ """
1942
+
1943
+ return header + content
1944
+
1945
+ def download_novel_from_gallery(novel_id: int, format: str) -> Optional[str]:
1946
+ """Download novel from gallery"""
1947
+ if not novel_id:
1948
+ return None
1949
+
1950
+ novel = NovelDatabase.get_novel_content(novel_id)
1951
+ if not novel:
1952
+ return None
1953
+
1954
+ # Increment download count
1955
+ NovelDatabase.increment_download_count(novel_id)
1956
+
1957
+ # Create file
1958
+ content = novel['full_content']
1959
+ title = re.sub(r'[^\w\s-]', '', novel['title']).strip()[:50]
1960
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
1961
+
1962
+ if format == "DOCX" and DOCX_AVAILABLE:
1963
+ # Create DOCX
1964
+ doc = Document()
1965
+
1966
+ # Add title
1967
+ title_para = doc.add_heading(novel['title'], 0)
1968
+ title_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
1969
+
1970
+ # Add metadata
1971
+ doc.add_paragraph(f"Created: {novel['created_at']}")
1972
+ doc.add_paragraph(f"Original prompt: {novel['author_query']}")
1973
+ doc.add_paragraph("")
1974
+
1975
+ # Parse and add content
1976
+ lines = content.split('\n')
1977
+ for line in lines:
1978
+ if line.startswith('#'):
1979
+ level = len(line.split()[0])
1980
+ text = line.lstrip('#').strip()
1981
+ if text: # Only add non-empty headings
1982
+ doc.add_heading(text, min(level, 3))
1983
+ elif line.strip():
1984
+ doc.add_paragraph(line)
1985
+
1986
+ # Save
1987
+ temp_dir = tempfile.gettempdir()
1988
+ filename = f"{title}_{timestamp}.docx"
1989
+ filepath = os.path.join(temp_dir, filename)
1990
+ doc.save(filepath)
1991
+
1992
+ return filepath
1993
+ else:
1994
+ # TXT format
1995
+ temp_dir = tempfile.gettempdir()
1996
+ filename = f"{title}_{timestamp}.txt"
1997
+ filepath = os.path.join(temp_dir, filename)
1998
+
1999
+ with open(filepath, 'w', encoding='utf-8') as f:
2000
+ f.write(f"Title: {novel['title']}\n")
2001
+ f.write(f"Created: {novel['created_at']}\n")
2002
+ f.write(f"Original prompt: {novel['author_query']}\n")
2003
+ f.write("\n" + "="*50 + "\n\n")
2004
+ f.write(content)
2005
+
2006
+ return filepath
2007
+
2008
  def format_stages_html(stages: List[Dict[str, str]], language: str, auto_scroll: bool = False) -> str:
2009
  """Format stages into HTML with scroll control"""
 
2010
 
2011
+ # Generate unique ID for this container
2012
+ container_id = f"stages-container-{int(time.time() * 1000)}"
2013
+
2014
+ html = f'<div id="{container_id}" class="stages-container" data-scroll="{"auto" if auto_scroll else "manual"}">'
2015
 
2016
  for idx, stage in enumerate(stages):
2017
  status_icon = "βœ…" if stage.get("status") == "complete" else ("⏳" if stage.get("status") == "active" else "❌")
 
2032
 
2033
  html += '</div>'
2034
 
2035
+ # Add powerful JavaScript for scroll control
2036
+ html += f'''
2037
+ <script>
2038
+ (function() {{
2039
+ const containerId = "{container_id}";
2040
+ const container = document.getElementById(containerId);
2041
+
2042
+ if (!container) return;
2043
+
2044
+ // Check if this is manual scroll mode
2045
+ const isManualMode = container.getAttribute('data-scroll') === 'manual';
2046
+
2047
+ // Get or create scroll controller
2048
+ if (!window.novelScrollController) {{
2049
+ window.novelScrollController = {{
2050
+ savedPosition: 0,
2051
+ userHasScrolled: false,
2052
+ isLocked: false
2053
+ }};
2054
+ }}
2055
+
2056
+ const scrollController = window.novelScrollController;
2057
+
2058
+ if (isManualMode) {{
2059
+ // Restore saved position immediately
2060
+ if (scrollController.userHasScrolled || scrollController.isLocked) {{
2061
+ container.scrollTop = scrollController.savedPosition;
2062
+ }}
2063
 
2064
+ // Save position on ANY scroll event
2065
+ let saveTimeout;
2066
+ const saveScrollPosition = () => {{
2067
+ clearTimeout(saveTimeout);
2068
+ saveTimeout = setTimeout(() => {{
2069
+ scrollController.savedPosition = container.scrollTop;
2070
+ scrollController.userHasScrolled = true;
2071
+ }}, 10);
2072
+ }};
2073
+
2074
+ container.addEventListener('scroll', saveScrollPosition, {{ passive: true }});
2075
+
2076
+ // Aggressively maintain scroll position during updates
2077
+ const lockScrollPosition = () => {{
2078
+ if (scrollController.userHasScrolled || scrollController.isLocked) {{
2079
+ container.scrollTop = scrollController.savedPosition;
2080
+ }}
2081
+ }};
2082
+
2083
+ // Multiple strategies to maintain scroll position
2084
+ // 1. MutationObserver
2085
+ const observer = new MutationObserver((mutations) => {{
2086
+ const shouldLock = scrollController.userHasScrolled || scrollController.isLocked;
2087
+ if (shouldLock) {{
2088
+ // Capture position before changes
2089
+ const beforeScroll = container.scrollTop;
2090
+ scrollController.savedPosition = beforeScroll;
2091
+
2092
+ // Restore after DOM settles
2093
+ requestAnimationFrame(() => {{
2094
+ container.scrollTop = scrollController.savedPosition;
2095
+
2096
+ // Double-check after a short delay
2097
+ setTimeout(() => {{
2098
+ if (container.scrollTop !== scrollController.savedPosition) {{
2099
+ container.scrollTop = scrollController.savedPosition;
2100
+ }}
2101
+ }}, 0);
2102
+ }});
2103
+ }}
2104
+ }});
2105
+
2106
+ observer.observe(container, {{
2107
+ childList: true,
2108
+ subtree: true,
2109
+ characterData: true
2110
+ }});
2111
+
2112
+ // 2. Periodic position check
2113
+ let checkInterval = setInterval(() => {{
2114
+ if ((scrollController.userHasScrolled || scrollController.isLocked) &&
2115
+ container.scrollTop !== scrollController.savedPosition) {{
2116
+ const diff = Math.abs(container.scrollTop - scrollController.savedPosition);
2117
+ // Only restore if difference is significant (not user scrolling)
2118
+ if (diff > 100) {{
2119
+ container.scrollTop = scrollController.savedPosition;
2120
+ }}
2121
+ }}
2122
+ }}, 100);
2123
+
2124
+ // 3. Intersection Observer for visible elements
2125
+ const visibleStages = new Set();
2126
+ const intersectionObserver = new IntersectionObserver((entries) => {{
2127
+ entries.forEach(entry => {{
2128
+ if (entry.isIntersecting) {{
2129
+ visibleStages.add(entry.target.id);
2130
+ }} else {{
2131
+ visibleStages.delete(entry.target.id);
2132
+ }}
2133
+ }});
2134
+
2135
+ // Save first visible stage
2136
+ if (visibleStages.size > 0) {{
2137
+ scrollController.firstVisibleStage = Array.from(visibleStages)[0];
2138
+ }}
2139
+ }}, {{ threshold: 0.1 }});
2140
+
2141
+ // Observe all stage sections
2142
+ container.querySelectorAll('.stage-section').forEach(stage => {{
2143
+ intersectionObserver.observe(stage);
2144
+ }});
2145
+
2146
+ // Clean up on container removal
2147
+ container.addEventListener('DOMNodeRemoved', () => {{
2148
+ clearInterval(checkInterval);
2149
+ observer.disconnect();
2150
+ intersectionObserver.disconnect();
2151
+ }});
2152
+
2153
+ }} else {{
2154
+ // Auto-scroll mode
2155
+ const scrollToBottom = () => {{
2156
+ requestAnimationFrame(() => {{
2157
+ container.scrollTop = container.scrollHeight;
2158
+ }});
2159
+ }};
2160
+
2161
+ const observer = new MutationObserver(scrollToBottom);
2162
+ observer.observe(container, {{ childList: true, subtree: true }});
2163
+
2164
+ scrollToBottom();
2165
+ }}
2166
+
2167
+ // Listen for lock command
2168
+ window.addEventListener('lockNovelScroll', () => {{
2169
+ scrollController.isLocked = true;
2170
+ scrollController.savedPosition = container.scrollTop;
2171
+ }});
2172
+
2173
+ // Listen for reset command
2174
+ window.addEventListener('novelScrollReset', () => {{
2175
+ scrollController.savedPosition = 0;
2176
+ scrollController.userHasScrolled = false;
2177
+ scrollController.isLocked = false;
2178
+ }});
2179
+ }})();
2180
+ </script>
2181
+ '''
2182
 
2183
  return html
2184
 
 
2200
  logger.error(f"Error getting active sessions: {str(e)}")
2201
  return []
2202
 
2203
+ def resume_session(session_id: str, language: str, auto_scroll: bool = False) -> Generator[Tuple[str, str, str], None, None]:
2204
  """Resume an existing session"""
2205
  if not session_id:
2206
  return
2207
 
2208
  # Process with existing session ID
2209
+ yield from process_query("", language, session_id, auto_scroll)
2210
 
2211
  def toggle_auto_scroll(current_state: bool) -> bool:
2212
  """Toggle auto-scroll state"""
 
2251
 
2252
  return filepath
2253
 
2254
+ # Custom CSS with improved scroll handling and gallery styles
2255
  custom_css = """
2256
  .gradio-container {
2257
  background: linear-gradient(135deg, #1e3c72, #2a5298);
 
2261
  .stages-container {
2262
  max-height: 600px;
2263
  overflow-y: auto;
2264
+ overflow-x: hidden;
2265
  padding: 20px;
2266
  background-color: rgba(255, 255, 255, 0.05);
2267
  border-radius: 12px;
2268
  backdrop-filter: blur(10px);
 
2269
  position: relative;
2270
  }
2271
 
2272
+ /* Disable smooth scrolling in manual mode to prevent unwanted behavior */
2273
  .stages-container[data-scroll="manual"] {
2274
+ scroll-behavior: auto !important;
2275
+ }
2276
+
2277
+ /* Auto-scroll mode */
2278
+ .stages-container[data-scroll="auto"] {
2279
+ scroll-behavior: smooth;
2280
+ }
2281
+
2282
+ /* Lock scroll position when user has scrolled */
2283
+ .stages-container.user-scrolled {
2284
+ overflow-y: auto !important;
2285
+ overscroll-behavior-y: contain;
2286
  }
2287
 
2288
  .stage-section {
 
2301
  align-items: center;
2302
  gap: 10px;
2303
  color: white;
2304
+ user-select: none;
2305
  }
2306
 
2307
  .stage-header:hover {
 
2316
  padding: 15px;
2317
  max-height: 400px;
2318
  overflow-y: auto;
2319
+ overflow-x: hidden;
2320
  background-color: rgba(0, 0, 0, 0.3);
2321
  border-radius: 8px;
2322
  margin-top: 10px;
2323
+ word-wrap: break-word;
2324
+ overflow-wrap: break-word;
2325
  }
2326
 
2327
  .stage-content pre {
2328
  white-space: pre-wrap;
2329
  word-wrap: break-word;
2330
+ overflow-wrap: break-word;
2331
  color: #e0e0e0;
2332
  font-family: 'Courier New', monospace;
2333
  line-height: 1.6;
2334
  margin: 0;
2335
+ max-width: 100%;
2336
+ }
2337
+
2338
+ /* Prevent layout shift during updates */
2339
+ .stages-container > * {
2340
+ transition: none !important;
2341
  }
2342
 
2343
  #novel-output {
 
2383
  color: white;
2384
  }
2385
 
2386
+ /* Scroll control indicator */
2387
+ .scroll-control-info {
2388
+ background-color: rgba(255, 255, 255, 0.2);
2389
+ color: white;
2390
+ padding: 8px 12px;
2391
+ border-radius: 4px;
2392
+ margin-bottom: 10px;
2393
+ text-align: center;
2394
+ font-size: 0.9em;
2395
+ }
2396
+
2397
+ /* Override Gradio's default scroll behavior */
2398
+ .generating .stages-container[data-scroll="manual"] {
2399
+ overflow-y: auto !important;
2400
+ }
2401
+
2402
+ /* Gallery Styles */
2403
+ .novel-gallery {
2404
+ display: grid;
2405
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
2406
+ gap: 20px;
2407
+ padding: 20px;
2408
+ max-height: 70vh;
2409
+ overflow-y: auto;
2410
+ }
2411
+
2412
+ .novel-card {
2413
+ background: rgba(255, 255, 255, 0.95);
2414
+ border-radius: 12px;
2415
+ overflow: hidden;
2416
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
2417
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
2418
+ cursor: pointer;
2419
+ }
2420
+
2421
+ .novel-card:hover {
2422
+ transform: translateY(-5px);
2423
+ box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2);
2424
+ }
2425
+
2426
+ .novel-card-header {
2427
+ padding: 15px;
2428
+ color: white;
2429
+ font-weight: bold;
2430
+ }
2431
+
2432
+ .novel-card-header h3 {
2433
+ margin: 0;
2434
+ font-size: 1.2em;
2435
+ overflow: hidden;
2436
+ text-overflow: ellipsis;
2437
+ white-space: nowrap;
2438
+ }
2439
+
2440
+ .novel-card-body {
2441
+ padding: 15px;
2442
+ }
2443
+
2444
+ .novel-summary {
2445
+ color: #333;
2446
+ font-size: 0.9em;
2447
+ line-height: 1.5;
2448
+ height: 80px;
2449
+ overflow: hidden;
2450
+ display: -webkit-box;
2451
+ -webkit-line-clamp: 4;
2452
+ -webkit-box-orient: vertical;
2453
+ }
2454
+
2455
+ .novel-meta {
2456
+ display: flex;
2457
+ justify-content: space-between;
2458
+ margin-top: 10px;
2459
  font-size: 0.8em;
2460
+ color: #666;
2461
+ }
2462
+
2463
+ .novel-card-actions {
2464
+ display: flex;
2465
+ gap: 10px;
2466
+ padding: 15px;
2467
+ background-color: rgba(0, 0, 0, 0.05);
2468
+ }
2469
+
2470
+ .novel-card-actions button {
2471
+ flex: 1;
2472
+ padding: 8px 16px;
2473
+ border: none;
2474
+ border-radius: 6px;
2475
+ cursor: pointer;
2476
+ font-weight: bold;
2477
+ transition: background-color 0.3s ease;
2478
+ }
2479
+
2480
+ .view-btn {
2481
+ background-color: #3b82f6;
2482
+ color: white;
2483
+ }
2484
+
2485
+ .view-btn:hover {
2486
+ background-color: #2563eb;
2487
+ }
2488
+
2489
+ .download-btn {
2490
+ background-color: #10b981;
2491
  color: white;
2492
  }
2493
+
2494
+ .download-btn:hover {
2495
+ background-color: #059669;
2496
+ }
2497
+
2498
+ .gallery-empty {
2499
+ text-align: center;
2500
+ padding: 50px;
2501
+ color: #666;
2502
+ font-size: 1.2em;
2503
+ }
2504
+
2505
+ /* Novel View Styles */
2506
+ .novel-view-header {
2507
+ background-color: rgba(255, 255, 255, 0.9);
2508
+ padding: 20px;
2509
+ border-radius: 8px;
2510
+ margin-bottom: 20px;
2511
+ }
2512
+
2513
+ .novel-view-header h1 {
2514
+ margin: 0 0 10px 0;
2515
+ color: #1e3c72;
2516
+ }
2517
+
2518
+ .novel-view-meta {
2519
+ display: flex;
2520
+ gap: 20px;
2521
+ color: #666;
2522
+ font-size: 0.9em;
2523
+ margin-bottom: 10px;
2524
+ }
2525
+
2526
+ .novel-view-query {
2527
+ color: #555;
2528
+ font-style: italic;
2529
+ padding: 10px;
2530
+ background-color: rgba(0, 0, 0, 0.05);
2531
+ border-radius: 4px;
2532
+ }
2533
+
2534
+ /* Gallery Filter Styles */
2535
+ .gallery-controls {
2536
+ background-color: rgba(255, 255, 255, 0.9);
2537
+ padding: 15px;
2538
+ border-radius: 8px;
2539
+ margin-bottom: 20px;
2540
+ display: flex;
2541
+ justify-content: space-between;
2542
+ align-items: center;
2543
+ }
2544
+
2545
+ .gallery-stats {
2546
+ display: flex;
2547
+ gap: 20px;
2548
+ font-size: 0.9em;
2549
+ color: #666;
2550
+ }
2551
  """
2552
 
2553
  # Create Gradio Interface
 
2570
  """)
2571
 
2572
  # State management
2573
+ auto_scroll_state = gr.State(False) # Default to manual scroll
2574
  current_session_id = gr.State(None)
2575
 
2576
  with gr.Row():
 
2610
  refresh_btn = gr.Button("πŸ”„ Refresh / μƒˆλ‘œκ³ μΉ¨", scale=1)
2611
  resume_btn = gr.Button("▢️ Resume / 재개", variant="secondary", scale=1)
2612
 
2613
+ # Scroll control with lock option
2614
+ with gr.Row():
2615
+ auto_scroll_checkbox = gr.Checkbox(
2616
+ label="Auto-scroll / μžλ™ 슀크둀",
2617
+ value=False
2618
+ )
2619
+ lock_scroll_btn = gr.Button("πŸ”’ Lock Scroll Position / 슀크둀 μœ„μΉ˜ κ³ μ •", scale=2)
2620
+
2621
+ gr.HTML("""
2622
+ <div class="scroll-control-info">
2623
+ <strong>Scroll Control:</strong><br>
2624
+ - Uncheck "Auto-scroll" to manually control scrolling<br>
2625
+ - Click "Lock Scroll Position" to force-lock current position<br>
2626
+ - Scroll will be preserved even during updates
2627
+ </div>
2628
+ """)
2629
 
2630
  gr.HTML("""
2631
  <div style="margin-top: 20px; padding: 15px; background: rgba(255,255,255,0.1); border-radius: 8px; color: white;">
 
2669
  label="Downloaded File / λ‹€μš΄λ‘œλ“œλœ 파일",
2670
  visible=False
2671
  )
2672
+
2673
+ with gr.Tab("🎨 Gallery / 가러리"):
2674
+ # Gallery controls
2675
+ with gr.Row():
2676
+ gallery_language = gr.Radio(
2677
+ choices=["All", "English", "Korean"],
2678
+ value="All",
2679
+ label="Filter by Language / 언어별 ν•„ν„°"
2680
+ )
2681
+ refresh_gallery_btn = gr.Button("πŸ”„ Refresh Gallery / 가러리 μƒˆλ‘œκ³ μΉ¨")
2682
+
2683
+ # Gallery display
2684
+ gallery_display = gr.HTML(
2685
+ value="<div class='gallery-empty'>Loading gallery...</div>",
2686
+ elem_id="novel-gallery-display"
2687
+ )
2688
+
2689
+ # Novel viewer (hidden by default)
2690
+ with gr.Group(visible=False) as novel_viewer:
2691
+ novel_view_content = gr.Markdown(
2692
+ value="",
2693
+ elem_id="novel-view-content"
2694
+ )
2695
+
2696
+ with gr.Row():
2697
+ close_viewer_btn = gr.Button("❌ Close / λ‹«κΈ°")
2698
+ download_viewed_btn = gr.Button("πŸ“₯ Download This Novel / 이 μ†Œμ„€ λ‹€μš΄λ‘œλ“œ")
2699
+
2700
+ download_viewed_file = gr.File(
2701
+ label="Downloaded File",
2702
+ visible=False
2703
+ )
2704
+
2705
+ # Hidden state for selected novel
2706
+ selected_novel_id = gr.State(None)
2707
 
2708
  # Hidden state for novel text
2709
  novel_text_state = gr.State("")
2710
 
2711
+ # Hidden HTML components for JavaScript injection
2712
+ js_injector = gr.HTML(value="", visible=False)
2713
+ gallery_js = gr.HTML(value="", visible=False)
2714
+
2715
  # Examples
2716
  with gr.Row():
2717
  gr.Examples(
 
2727
  label="πŸ’‘ Example Themes / 예제 주제"
2728
  )
2729
 
2730
+ # Footer
2731
+ gr.HTML("""
2732
+ <div style="text-align: center; padding: 20px; color: #ccc;">
2733
+ <p>All generated novels are automatically saved to the gallery for future reference.</p>
2734
+ <p>λͺ¨λ“  μƒμ„±λœ μ†Œμ„€μ€ κ°€λŸ¬λ¦¬μ— μžλ™μœΌλ‘œ μ €μž₯λ˜μ–΄ λ‚˜μ€‘μ— μ—΄λžŒν•  수 μžˆμŠ΅λ‹ˆλ‹€.</p>
2735
+ </div>
2736
+ """)
2737
+
2738
  # Event handlers
2739
  def update_novel_state(process, novel, status):
2740
  return process, novel, status, novel
 
2747
  logger.error(f"Error refreshing sessions: {str(e)}")
2748
  return gr.update(choices=[])
2749
 
2750
+ def toggle_scroll_mode(checked):
2751
+ """Toggle scroll mode and reset tracking"""
2752
+ # Inject JavaScript to reset scroll tracking
2753
+ reset_script = """
2754
+ <script>
2755
+ window.dispatchEvent(new Event('novelScrollReset'));
2756
+ </script>
2757
+ """
2758
+ return checked, gr.update(value=reset_script)
2759
+
2760
+ # Gallery interactions
2761
+ def refresh_gallery(language_filter):
2762
+ """Refresh gallery display"""
2763
+ lang = None if language_filter == "All" else language_filter
2764
+ return get_gallery_html(lang)
2765
+
2766
+ # Gallery-specific handlers
2767
+ def view_novel_handler(novel_id):
2768
+ """Handle viewing a novel from gallery"""
2769
+ if not novel_id:
2770
+ return gr.update(visible=False), "", None
2771
+
2772
+ try:
2773
+ novel_id = int(novel_id)
2774
+ content = view_novel(novel_id, language_select.value)
2775
+ return gr.update(visible=True), content, novel_id
2776
+ except Exception as e:
2777
+ logger.error(f"Error viewing novel: {str(e)}")
2778
+ return gr.update(visible=False), "", None
2779
+
2780
+ def close_viewer():
2781
+ """Close novel viewer"""
2782
+ return gr.update(visible=False), ""
2783
+
2784
+ def download_viewed_novel(novel_id, format_type):
2785
+ """Download the currently viewed novel"""
2786
+ if not novel_id:
2787
+ return gr.update(visible=False)
2788
+
2789
+ try:
2790
+ file_path = download_novel_from_gallery(novel_id, format_type)
2791
+ if file_path:
2792
+ return gr.update(value=file_path, visible=True)
2793
+ except Exception as e:
2794
+ logger.error(f"Error downloading novel: {str(e)}")
2795
+
2796
+ return gr.update(visible=False)
2797
+
2798
+ # Add HTML component for JavaScript injection
2799
+ js_injector = gr.HTML(value="", visible=False)
2800
+
2801
  submit_btn.click(
2802
  fn=process_query,
2803
+ inputs=[query_input, language_select, current_session_id, auto_scroll_checkbox],
2804
  outputs=[process_display, novel_output, status_text]
2805
  ).then(
2806
  fn=update_novel_state,
 
2814
  outputs=[current_session_id]
2815
  ).then(
2816
  fn=resume_session,
2817
+ inputs=[current_session_id, language_select, auto_scroll_checkbox],
2818
  outputs=[process_display, novel_output, status_text]
2819
  )
2820
 
 
2824
  )
2825
 
2826
  clear_btn.click(
2827
+ fn=lambda: ("", "", "πŸ”„ Ready", "", None, '<script>window.dispatchEvent(new Event("novelScrollReset"));</script>'),
2828
+ outputs=[process_display, novel_output, status_text, novel_text_state, current_session_id, js_injector]
2829
  )
2830
 
2831
  auto_scroll_checkbox.change(
 
2834
  outputs=[auto_scroll_state]
2835
  )
2836
 
2837
+ def lock_scroll_position():
2838
+ """Force lock the current scroll position"""
2839
+ lock_script = """
2840
+ <div id="scroll-lock-script">
2841
+ <script>
2842
+ (function() {
2843
+ // Trigger lock event
2844
+ window.dispatchEvent(new Event('lockNovelScroll'));
2845
+
2846
+ // Visual feedback
2847
+ const containers = document.querySelectorAll('.stages-container');
2848
+ containers.forEach(container => {
2849
+ // Flash border to indicate lock
2850
+ container.style.border = '3px solid #4CAF50';
2851
+ container.style.boxShadow = '0 0 10px rgba(76, 175, 80, 0.5)';
2852
+
2853
+ setTimeout(() => {
2854
+ container.style.border = '';
2855
+ container.style.boxShadow = '';
2856
+ }, 1500);
2857
+ });
2858
+
2859
+ // Show confirmation message
2860
+ const msg = document.createElement('div');
2861
+ msg.innerHTML = '<strong>πŸ”’ Scroll Position Locked!</strong><br>The view will stay at current position';
2862
+ msg.style.cssText = `
2863
+ position: fixed;
2864
+ top: 50%;
2865
+ left: 50%;
2866
+ transform: translate(-50%, -50%);
2867
+ background: #4CAF50;
2868
+ color: white;
2869
+ padding: 20px 30px;
2870
+ border-radius: 8px;
2871
+ z-index: 9999;
2872
+ font-size: 16px;
2873
+ box-shadow: 0 4px 20px rgba(0,0,0,0.3);
2874
+ text-align: center;
2875
+ `;
2876
+ document.body.appendChild(msg);
2877
+
2878
+ // Fade out animation
2879
+ msg.style.transition = 'opacity 0.5s ease';
2880
+ setTimeout(() => {
2881
+ msg.style.opacity = '0';
2882
+ setTimeout(() => msg.remove(), 500);
2883
+ }, 2000);
2884
+ })();
2885
+ </script>
2886
+ </div>
2887
+ """
2888
+ return gr.update(value=lock_script)
2889
+
2890
+ lock_scroll_btn.click(
2891
+ fn=lock_scroll_position,
2892
+ outputs=[js_injector]
2893
+ )
2894
+
2895
  def handle_download(novel_text, format_type, language):
2896
  if not novel_text:
2897
  return gr.update(visible=False)
 
2908
  outputs=[download_file]
2909
  )
2910
 
2911
+ # Gallery-specific handlers
2912
+ def view_novel_handler(novel_id):
2913
+ """Handle viewing a novel from gallery"""
2914
+ if not novel_id:
2915
+ return gr.update(visible=False), "", None
2916
+
2917
+ try:
2918
+ novel_id = int(novel_id)
2919
+ content = view_novel(novel_id, language_select.value)
2920
+ return gr.update(visible=True), content, novel_id
2921
+ except:
2922
+ return gr.update(visible=False), "", None
2923
+
2924
+ def close_viewer():
2925
+ """Close novel viewer"""
2926
+ return gr.update(visible=False), ""
2927
+
2928
+ def download_viewed_novel(novel_id, format_type):
2929
+ """Download the currently viewed novel"""
2930
+ if not novel_id:
2931
+ return gr.update(visible=False)
2932
+
2933
+ try:
2934
+ file_path = download_novel_from_gallery(novel_id, format_type)
2935
+ if file_path:
2936
+ return gr.update(value=file_path, visible=True)
2937
+ except Exception as e:
2938
+ logger.error(f"Error downloading novel: {str(e)}")
2939
+
2940
+ return gr.update(visible=False)
2941
+
2942
+ # Gallery event handlers
2943
+ refresh_gallery_btn.click(
2944
+ fn=refresh_gallery,
2945
+ inputs=[gallery_language],
2946
+ outputs=[gallery_display]
2947
+ )
2948
+
2949
+ gallery_language.change(
2950
+ fn=refresh_gallery,
2951
+ inputs=[gallery_language],
2952
+ outputs=[gallery_display]
2953
+ )
2954
+
2955
+ # Novel viewer interactions
2956
+ selected_novel_id.change(
2957
+ fn=view_novel_handler,
2958
+ inputs=[selected_novel_id],
2959
+ outputs=[novel_viewer, novel_view_content, selected_novel_id]
2960
+ )
2961
+
2962
+ close_viewer_btn.click(
2963
+ fn=close_viewer,
2964
+ outputs=[novel_viewer, novel_view_content]
2965
+ )
2966
+
2967
+ download_viewed_btn.click(
2968
+ fn=download_viewed_novel,
2969
+ inputs=[selected_novel_id, format_select],
2970
+ outputs=[download_viewed_file]
2971
+ )
2972
+
2973
+ # Add JavaScript for gallery interactions
2974
+ interface.load(
2975
+ fn=lambda: gr.update(value="""
2976
+ <script>
2977
+ // Listen for view novel events
2978
+ window.addEventListener('viewNovel', function(e) {
2979
+ const novelId = e.detail.novelId;
2980
+ // Find the hidden input for selected_novel_id and update it
2981
+ const inputs = document.querySelectorAll('input[type="hidden"]');
2982
+ for (let input of inputs) {
2983
+ if (input.value === '' || !isNaN(input.value)) {
2984
+ input.value = novelId;
2985
+ input.dispatchEvent(new Event('input', { bubbles: true }));
2986
+ break;
2987
+ }
2988
+ }
2989
+ });
2990
+
2991
+ // Listen for download novel events
2992
+ window.addEventListener('downloadNovel', function(e) {
2993
+ const novelId = e.detail.novelId;
2994
+ // Update selected novel and trigger download
2995
+ const inputs = document.querySelectorAll('input[type="hidden"]');
2996
+ for (let input of inputs) {
2997
+ if (input.value === '' || !isNaN(input.value)) {
2998
+ input.value = novelId;
2999
+ input.dispatchEvent(new Event('input', { bubbles: true }));
3000
+ setTimeout(() => {
3001
+ // Click download button
3002
+ const downloadBtn = document.querySelector('#download-viewed-btn');
3003
+ if (downloadBtn) downloadBtn.click();
3004
+ }, 100);
3005
+ break;
3006
+ }
3007
+ }
3008
+ });
3009
+ </script>
3010
+ """),
3011
+ outputs=[gallery_js]
3012
+ )
3013
+
3014
+ # Load sessions and gallery on startup
3015
  interface.load(
3016
  fn=refresh_sessions,
3017
  outputs=[session_dropdown]
3018
  )
3019
+
3020
+ interface.load(
3021
+ fn=lambda: get_gallery_html(None),
3022
+ outputs=[gallery_display]
3023
+ )
3024
 
3025
  return interface
3026