deepcode-hku 1.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. cli/__init__.py +18 -0
  2. cli/cli_app.py +296 -0
  3. cli/cli_interface.py +744 -0
  4. cli/cli_launcher.py +155 -0
  5. cli/main_cli.py +243 -0
  6. cli/workflows/__init__.py +11 -0
  7. cli/workflows/cli_workflow_adapter.py +336 -0
  8. deepcode.py +219 -0
  9. deepcode_hku-1.0.1.dist-info/METADATA +695 -0
  10. deepcode_hku-1.0.1.dist-info/RECORD +44 -0
  11. deepcode_hku-1.0.1.dist-info/WHEEL +5 -0
  12. deepcode_hku-1.0.1.dist-info/entry_points.txt +2 -0
  13. deepcode_hku-1.0.1.dist-info/licenses/LICENSE +21 -0
  14. deepcode_hku-1.0.1.dist-info/top_level.txt +6 -0
  15. tools/__init__.py +0 -0
  16. tools/code_implementation_server.py +1045 -0
  17. tools/code_indexer.py +1657 -0
  18. tools/code_reference_indexer.py +486 -0
  19. tools/command_executor.py +324 -0
  20. tools/git_command.py +356 -0
  21. tools/pdf_converter.py +640 -0
  22. tools/pdf_downloader.py +1370 -0
  23. tools/pdf_utils.py +52 -0
  24. ui/__init__.py +43 -0
  25. ui/app.py +13 -0
  26. ui/components.py +1450 -0
  27. ui/handlers.py +773 -0
  28. ui/layout.py +106 -0
  29. ui/streamlit_app.py +38 -0
  30. ui/styles.py +2116 -0
  31. utils/__init__.py +17 -0
  32. utils/cli_interface.py +459 -0
  33. utils/dialogue_logger.py +671 -0
  34. utils/file_processor.py +426 -0
  35. utils/simple_llm_logger.py +198 -0
  36. workflows/__init__.py +31 -0
  37. workflows/agent_orchestration_engine.py +1371 -0
  38. workflows/agents/__init__.py +13 -0
  39. workflows/agents/code_implementation_agent.py +1093 -0
  40. workflows/agents/memory_agent_concise.py +923 -0
  41. workflows/agents/memory_agent_concise_index.py +935 -0
  42. workflows/code_implementation_workflow.py +924 -0
  43. workflows/code_implementation_workflow_index.py +931 -0
  44. workflows/codebase_index_workflow.py +726 -0
utils/__init__.py ADDED
@@ -0,0 +1,17 @@
1
+ """
2
+ Utils package for paper processing tools.
3
+ """
4
+
5
+ from .file_processor import FileProcessor
6
+ from .dialogue_logger import (
7
+ DialogueLogger,
8
+ create_dialogue_logger,
9
+ extract_paper_id_from_path,
10
+ )
11
+
12
+ __all__ = [
13
+ "FileProcessor",
14
+ "DialogueLogger",
15
+ "create_dialogue_logger",
16
+ "extract_paper_id_from_path",
17
+ ]
utils/cli_interface.py ADDED
@@ -0,0 +1,459 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Professional CLI Interface Module
4
+ 专业CLI界面模块 - 包含logo、颜色定义和界面组件
5
+ """
6
+
7
+ import os
8
+ import time
9
+ import platform
10
+ from pathlib import Path
11
+ from typing import Optional
12
+ import tkinter as tk
13
+ from tkinter import filedialog
14
+
15
+
16
+ class Colors:
17
+ """ANSI color codes for terminal styling"""
18
+
19
+ HEADER = "\033[95m"
20
+ OKBLUE = "\033[94m"
21
+ OKCYAN = "\033[96m"
22
+ OKGREEN = "\033[92m"
23
+ WARNING = "\033[93m"
24
+ FAIL = "\033[91m"
25
+ ENDC = "\033[0m"
26
+ BOLD = "\033[1m"
27
+ UNDERLINE = "\033[4m"
28
+
29
+ # Gradient colors
30
+ PURPLE = "\033[35m"
31
+ MAGENTA = "\033[95m"
32
+ BLUE = "\033[34m"
33
+ CYAN = "\033[36m"
34
+ GREEN = "\033[32m"
35
+ YELLOW = "\033[33m"
36
+
37
+
38
+ class CLIInterface:
39
+ """Professional CLI interface with modern styling"""
40
+
41
+ def __init__(self):
42
+ self.uploaded_file = None
43
+ self.is_running = True
44
+
45
+ # Check tkinter availability
46
+ self.tkinter_available = True
47
+ try:
48
+ import tkinter as tk
49
+
50
+ # Test if tkinter can create a window (some systems have tkinter but no display)
51
+ test_root = tk.Tk()
52
+ test_root.withdraw()
53
+ test_root.destroy()
54
+ except Exception:
55
+ self.tkinter_available = False
56
+
57
+ def clear_screen(self):
58
+ """Clear terminal screen"""
59
+ os.system("cls" if os.name == "nt" else "clear")
60
+
61
+ def print_logo(self):
62
+ """Print a beautiful ASCII logo with gradient colors and tech elements"""
63
+ # 确保每行总共79个字符(不包括颜色代码),边框完美对齐
64
+ logo = f"""
65
+ {Colors.CYAN}╔═══════════════════════════════════════════════════════════════════════════════╗
66
+ ║ ║
67
+ ║ {Colors.BOLD}{Colors.MAGENTA}██████╗ ███████╗██████╗ ██████╗ ██████╗ █████╗ ██╗{Colors.CYAN} ║
68
+ ║ {Colors.BOLD}{Colors.PURPLE}██╔══██╗ ██╔════╝██╔══██╗██╔══██╗██╔═══██╗ ██╔══██╗██║{Colors.CYAN} ║
69
+ ║ {Colors.BOLD}{Colors.BLUE}██████╔╝ █████╗ ██████╔╝██████╔╝██║ ██║ ███████║██║{Colors.CYAN} ║
70
+ ║ {Colors.BOLD}{Colors.OKBLUE}██╔══██╗ ██╔══╝ ██╔═══╝ ██╔══██╗██║ ██║ ██╔══██║██║{Colors.CYAN} ║
71
+ ║ {Colors.BOLD}{Colors.OKCYAN}██║ ██║ ███████╗██║ ██║ ██║╚██████╔╝ ██║ ██║██║{Colors.CYAN} ║
72
+ ║ {Colors.BOLD}{Colors.GREEN}╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝{Colors.CYAN} ║
73
+ ║ ║
74
+ ║ {Colors.BOLD}{Colors.YELLOW}┌─────────────────────────────────────────────────────────────────────────┐{Colors.CYAN} ║
75
+ ║ {Colors.BOLD}{Colors.YELLOW}│ 🤖 AI-POWERED RESEARCH PAPER REPRODUCTION ENGINE 🚀 │{Colors.CYAN} ║
76
+ ║ {Colors.BOLD}{Colors.YELLOW}│ ⚡ INTELLIGENT • AUTOMATED • CUTTING-EDGE ⚡ │{Colors.CYAN} ║
77
+ ║ {Colors.BOLD}{Colors.YELLOW}└─────────────────────────────────────────────────────────────────────────┘{Colors.CYAN} ║
78
+ ║ ║
79
+ ║ {Colors.BOLD}{Colors.GREEN}💎 CORE CAPABILITIES:{Colors.ENDC} {Colors.CYAN}║
80
+ ║ {Colors.BOLD}{Colors.OKCYAN}▶ Neural PDF Analysis & Code Extraction {Colors.CYAN}║
81
+ ║ {Colors.BOLD}{Colors.OKCYAN}▶ Advanced Document Processing Engine {Colors.CYAN}║
82
+ ║ {Colors.BOLD}{Colors.OKCYAN}▶ Multi-Format Support (PDF•DOCX•PPTX•HTML) {Colors.CYAN}║
83
+ ║ {Colors.BOLD}{Colors.OKCYAN}▶ Smart File Upload Interface {Colors.CYAN}║
84
+ ║ {Colors.BOLD}{Colors.OKCYAN}▶ Automated Repository Management {Colors.CYAN}║
85
+ ║ ║
86
+ ║ {Colors.BOLD}{Colors.PURPLE}🔬 TECH STACK: Python•AI•MCP•Docling•LLM {Colors.CYAN}║
87
+ ║ ║
88
+ ╚═══════════════════════════════════════════════════════════════════════════════╝{Colors.ENDC}
89
+ """
90
+ print(logo)
91
+
92
+ def print_welcome_banner(self):
93
+ """Print welcome banner with version info"""
94
+ banner = f"""
95
+ {Colors.BOLD}{Colors.CYAN}╔═══════════════════════════════════════════════════════════════════════════════╗
96
+ ║ WELCOME TO ReproAI ║
97
+ ╠═══════════════════════════════════════════════════════════════════════════════╣
98
+ ║ ║
99
+ ║ {Colors.YELLOW}Version: 2.0.0 | Build: Professional Edition {Colors.CYAN}║
100
+ ║ {Colors.GREEN}Status: Ready | Engine: Initialized {Colors.CYAN}║
101
+ ║ {Colors.PURPLE}Author: AI Research Team | License: MIT {Colors.CYAN}║
102
+ ║ ║
103
+ ╚═══════════════════════════════════════════════════════════════════════════════╝{Colors.ENDC}
104
+ """
105
+ print(banner)
106
+
107
+ def print_separator(self, char="═", length=79, color=Colors.CYAN):
108
+ """Print a styled separator line"""
109
+ print(f"{color}{char * length}{Colors.ENDC}")
110
+
111
+ def print_status(self, message: str, status_type: str = "info"):
112
+ """Print status message with appropriate styling"""
113
+ status_styles = {
114
+ "success": f"{Colors.OKGREEN}✅",
115
+ "error": f"{Colors.FAIL}❌",
116
+ "warning": f"{Colors.WARNING}⚠️ ",
117
+ "info": f"{Colors.OKBLUE}ℹ️ ",
118
+ "processing": f"{Colors.YELLOW}⏳",
119
+ "upload": f"{Colors.PURPLE}📁",
120
+ "download": f"{Colors.CYAN}📥",
121
+ "analysis": f"{Colors.MAGENTA}🔍",
122
+ }
123
+
124
+ icon = status_styles.get(status_type, status_styles["info"])
125
+ print(f"{icon} {Colors.BOLD}{message}{Colors.ENDC}")
126
+
127
+ def create_menu(self):
128
+ """Create an interactive menu"""
129
+ menu = f"""
130
+ {Colors.BOLD}{Colors.CYAN}╔═══════════════════════════════════════════════════════════════════════════════╗
131
+ ║ MAIN MENU ║
132
+ ╠═══════════════════════════════════════════════════════════════════════════════╣
133
+ ║ ║
134
+ ║ {Colors.OKGREEN}🌐 [U] Process URL {Colors.CYAN}│ {Colors.PURPLE}📁 [F] Upload File {Colors.CYAN}│ {Colors.FAIL}❌ [Q] Quit{Colors.CYAN} ║
135
+ ║ ║
136
+ ║ {Colors.YELLOW}📝 Enter a research paper URL (arXiv, IEEE, ACM, etc.) {Colors.CYAN}║
137
+ ║ {Colors.YELLOW} or upload a PDF/DOC file for intelligent analysis {Colors.CYAN}║
138
+ ║ ║
139
+ ║ {Colors.OKCYAN}💡 Tip: Press 'F' to open file browser or 'U' to enter URL manually {Colors.CYAN}║
140
+ ║ ║
141
+ ╚═══════════════════════════════════════════════════════════════════════════════╝{Colors.ENDC}
142
+ """
143
+ print(menu)
144
+
145
+ def get_user_input(self):
146
+ """Get user input with styled prompt"""
147
+ print(f"\n{Colors.BOLD}{Colors.OKCYAN}➤ Your choice: {Colors.ENDC}", end="")
148
+ return input().strip().lower()
149
+
150
+ def upload_file_gui(self) -> Optional[str]:
151
+ """Modern file upload interface using tkinter with cross-platform compatibility"""
152
+ # Check if tkinter is available
153
+ if not self.tkinter_available:
154
+ self.print_status("GUI file dialog not available on this system", "warning")
155
+ self.print_status("Using manual file path input instead", "info")
156
+ return self._get_manual_file_path()
157
+
158
+ def select_file():
159
+ try:
160
+ # Create a hidden root window
161
+ root = tk.Tk()
162
+ root.withdraw() # Hide the main window
163
+
164
+ # Platform-specific configurations
165
+ system = platform.system()
166
+
167
+ if system == "Darwin": # macOS
168
+ # macOS specific settings
169
+ try:
170
+ root.call("wm", "attributes", ".", "-topmost", True)
171
+ except Exception:
172
+ pass
173
+
174
+ # macOS compatible file types
175
+ file_types = [
176
+ ("PDF Files", ".pdf"),
177
+ ("Word Documents", ".docx .doc"),
178
+ ("PowerPoint Files", ".pptx .ppt"),
179
+ ("HTML Files", ".html .htm"),
180
+ ("Text Files", ".txt .md"),
181
+ ("All Files", ".*"),
182
+ ]
183
+ else:
184
+ # Windows and Linux
185
+ root.attributes("-topmost", True)
186
+
187
+ # Windows/Linux compatible file types
188
+ file_types = [
189
+ ("PDF Files", "*.pdf"),
190
+ ("Word Documents", "*.docx;*.doc"),
191
+ ("PowerPoint Files", "*.pptx;*.ppt"),
192
+ ("HTML Files", "*.html;*.htm"),
193
+ ("Text Files", "*.txt;*.md"),
194
+ ("All Files", "*.*"),
195
+ ]
196
+
197
+ # Set window title
198
+ root.title("Repro-AI - File Selector")
199
+
200
+ try:
201
+ # Open file dialog with platform-appropriate settings
202
+ file_path = filedialog.askopenfilename(
203
+ title="Select Research Paper File",
204
+ filetypes=file_types,
205
+ initialdir=os.getcwd(),
206
+ )
207
+ except Exception as e:
208
+ self.print_status(f"File dialog error: {str(e)}", "error")
209
+ return None
210
+ finally:
211
+ # Clean up
212
+ try:
213
+ root.destroy()
214
+ except Exception:
215
+ pass
216
+
217
+ return file_path
218
+
219
+ except Exception as e:
220
+ # Fallback: destroy root if it exists
221
+ try:
222
+ if "root" in locals():
223
+ root.destroy()
224
+ except Exception:
225
+ pass
226
+
227
+ # Print error and suggest alternative
228
+ self.print_status(f"GUI file dialog failed: {str(e)}", "error")
229
+ self.print_status(
230
+ "Please use manual file path input instead", "warning"
231
+ )
232
+ return self._get_manual_file_path()
233
+
234
+ self.print_status("Opening file browser dialog...", "upload")
235
+ file_path = select_file()
236
+
237
+ if file_path:
238
+ # Validate file
239
+ if not os.path.exists(file_path):
240
+ self.print_status("File not found!", "error")
241
+ return None
242
+
243
+ file_size = os.path.getsize(file_path) / (1024 * 1024) # Size in MB
244
+ file_ext = Path(file_path).suffix.lower()
245
+
246
+ # Display file info with beautiful formatting
247
+ file_name = Path(file_path).name
248
+ directory = str(Path(file_path).parent)
249
+
250
+ # Truncate long paths for display
251
+ if len(file_name) > 50:
252
+ display_name = file_name[:47] + "..."
253
+ else:
254
+ display_name = file_name
255
+
256
+ if len(directory) > 49:
257
+ display_dir = "..." + directory[-46:]
258
+ else:
259
+ display_dir = directory
260
+
261
+ print(f"""
262
+ {Colors.OKGREEN}╔═══════════════════════════════════════════════════════════════════════════════╗
263
+ ║ FILE SELECTED ║
264
+ ╠═══════════════════════════════════════════════════════════════════════════════╣
265
+ ║ ║
266
+ ║ {Colors.BOLD}📄 File Name:{Colors.ENDC} {Colors.CYAN}{display_name:<50}{Colors.OKGREEN}║
267
+ ║ {Colors.BOLD}📁 Directory:{Colors.ENDC} {Colors.YELLOW}{display_dir:<49}{Colors.OKGREEN}║
268
+ ║ {Colors.BOLD}📊 File Size:{Colors.ENDC} {Colors.PURPLE}{file_size:.2f} MB{Colors.OKGREEN} ║
269
+ ║ {Colors.BOLD}🔖 File Type:{Colors.ENDC} {Colors.MAGENTA}{file_ext.upper():<50}{Colors.OKGREEN}║
270
+ ║ ║
271
+ ╚═══════════════════════════════════════════════════════════════════════════════╝{Colors.ENDC}
272
+ """)
273
+
274
+ self.print_status(f"File successfully selected: {file_name}", "success")
275
+ return file_path
276
+ else:
277
+ self.print_status("No file selected", "warning")
278
+ return None
279
+
280
+ def _get_manual_file_path(self) -> Optional[str]:
281
+ """Fallback method for manual file path input when GUI fails"""
282
+ print(
283
+ f"\n{Colors.BOLD}{Colors.CYAN}╔═══════════════════════════════════════════════════════════════════════════════╗"
284
+ )
285
+ print(
286
+ "║ MANUAL FILE INPUT ║"
287
+ )
288
+ print(
289
+ f"╚═══════════════════════════════════════════════════════════════════════════════╝{Colors.ENDC}"
290
+ )
291
+
292
+ print(f"\n{Colors.YELLOW}📝 Supported file types:{Colors.ENDC}")
293
+ print(f" {Colors.CYAN}• PDF files (.pdf)")
294
+ print(f" {Colors.CYAN}• Word documents (.docx, .doc)")
295
+ print(f" {Colors.CYAN}• PowerPoint files (.pptx, .ppt)")
296
+ print(f" {Colors.CYAN}• HTML files (.html, .htm)")
297
+ print(f" {Colors.CYAN}• Text files (.txt, .md){Colors.ENDC}")
298
+
299
+ print(
300
+ f"\n{Colors.BOLD}{Colors.OKCYAN}📁 Enter file path (or drag & drop): {Colors.ENDC}",
301
+ end="",
302
+ )
303
+ file_path = input().strip()
304
+
305
+ # Clean up the path (remove quotes if present)
306
+ file_path = file_path.strip("\"'")
307
+
308
+ if file_path:
309
+ # Expand user directory if needed
310
+ file_path = os.path.expanduser(file_path)
311
+
312
+ # Check if file exists
313
+ if os.path.exists(file_path):
314
+ self.print_status(
315
+ f"File found: {os.path.basename(file_path)}", "success"
316
+ )
317
+ return file_path
318
+ else:
319
+ self.print_status("File not found at the specified path", "error")
320
+ return None
321
+ else:
322
+ self.print_status("No file path provided", "warning")
323
+ return None
324
+
325
+ def get_url_input(self) -> str:
326
+ """Get URL input with validation and examples"""
327
+ print(
328
+ f"\n{Colors.BOLD}{Colors.CYAN}╔═══════════════════════════════════════════════════════════════════════════════╗"
329
+ )
330
+ print(
331
+ "║ URL INPUT ║"
332
+ )
333
+ print(
334
+ f"╚═══════════════════════════════════════════════════════════════════════════════╝{Colors.ENDC}"
335
+ )
336
+
337
+ print(f"\n{Colors.YELLOW}📝 Supported URL Examples:{Colors.ENDC}")
338
+ print(f" {Colors.CYAN}• arXiv: https://arxiv.org/pdf/2403.00813")
339
+ print(f" {Colors.CYAN}• arXiv: @https://arxiv.org/pdf/2403.00813")
340
+ print(f" {Colors.CYAN}• IEEE: https://ieeexplore.ieee.org/document/...")
341
+ print(f" {Colors.CYAN}• ACM: https://dl.acm.org/doi/...")
342
+ print(
343
+ f" {Colors.CYAN}• Direct PDF: https://example.com/paper.pdf{Colors.ENDC}"
344
+ )
345
+
346
+ print(
347
+ f"\n{Colors.BOLD}{Colors.OKCYAN}🌐 Enter paper URL: {Colors.ENDC}", end=""
348
+ )
349
+ url = input().strip()
350
+
351
+ if url:
352
+ # Basic URL validation
353
+ if any(
354
+ domain in url.lower()
355
+ for domain in ["arxiv.org", "ieee", "acm.org", ".pdf", "researchgate"]
356
+ ):
357
+ self.print_status(f"URL received: {url}", "success")
358
+ return url
359
+ else:
360
+ self.print_status("URL appears valid, proceeding...", "info")
361
+ return url
362
+ else:
363
+ self.print_status("No URL provided", "warning")
364
+ return ""
365
+
366
+ def show_progress_bar(self, message: str, duration: float = 2.0):
367
+ """Show a progress animation with enhanced styling"""
368
+ print(f"\n{Colors.YELLOW}{message}{Colors.ENDC}")
369
+
370
+ # Progress bar animation with different styles
371
+ bar_length = 50
372
+ for i in range(bar_length + 1):
373
+ percent = (i / bar_length) * 100
374
+ filled = "█" * i
375
+ empty = "░" * (bar_length - i)
376
+
377
+ # Color gradient effect
378
+ if percent < 33:
379
+ color = Colors.FAIL
380
+ elif percent < 66:
381
+ color = Colors.WARNING
382
+ else:
383
+ color = Colors.OKGREEN
384
+
385
+ print(
386
+ f"\r{color}[{filled}{empty}] {percent:6.1f}%{Colors.ENDC}",
387
+ end="",
388
+ flush=True,
389
+ )
390
+ time.sleep(duration / bar_length)
391
+
392
+ print(f"\n{Colors.OKGREEN}✅ {message} completed!{Colors.ENDC}\n")
393
+
394
+ def show_spinner(self, message: str, duration: float = 1.0):
395
+ """Show a spinner animation"""
396
+ spinner_chars = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
397
+ end_time = time.time() + duration
398
+
399
+ while time.time() < end_time:
400
+ for char in spinner_chars:
401
+ print(
402
+ f"\r{Colors.CYAN}{char} {Colors.BOLD}{message}{Colors.ENDC}",
403
+ end="",
404
+ flush=True,
405
+ )
406
+ time.sleep(0.1)
407
+ if time.time() >= end_time:
408
+ break
409
+
410
+ print(f"\r{Colors.OKGREEN}✅ {Colors.BOLD}{message} - Done!{Colors.ENDC}")
411
+
412
+ def print_results_header(self):
413
+ """Print results section header"""
414
+ header = f"""
415
+ {Colors.OKGREEN}╔═══════════════════════════════════════════════════════════════════════════════╗
416
+ ║ PROCESSING RESULTS ║
417
+ ╚═══════════════════════════════════════════════════════════════════════════════╝{Colors.ENDC}
418
+ """
419
+ print(header)
420
+
421
+ def print_error_box(self, title: str, error_msg: str):
422
+ """Print error message in a styled box"""
423
+ print(f"""
424
+ {Colors.FAIL}╔═══════════════════════════════════════════════════════════════════════════════╗
425
+ ║ ERROR ║
426
+ ╠═══════════════════════════════════════════════════════════════════════════════╣
427
+ ║ ║
428
+ ║ {Colors.BOLD}Title: {title:<66}{Colors.FAIL}║
429
+ ║ {Colors.BOLD}Error: {error_msg:<66}{Colors.FAIL}║
430
+ ║ ║
431
+ ╚═══════════════════════════════════════════════════════════════════════════════╝{Colors.ENDC}
432
+ """)
433
+
434
+ def print_goodbye(self):
435
+ """Print goodbye message"""
436
+ goodbye = f"""
437
+ {Colors.BOLD}{Colors.YELLOW}╔═══════════════════════════════════════════════════════════════════════════════╗
438
+ ║ GOODBYE! ║
439
+ ╠═══════════════════════════════════════════════════════════════════════════════╣
440
+ ║ ║
441
+ ║ {Colors.CYAN}Thank you for using ReproAI! {Colors.YELLOW}║
442
+ ║ {Colors.GREEN}🌟 Star us on GitHub: https://github.com/your-repo {Colors.YELLOW}║
443
+ ║ {Colors.PURPLE}📧 Contact: support@reproai.com {Colors.YELLOW}║
444
+ ║ {Colors.MAGENTA}🐛 Report issues: https://github.com/your-repo/issues {Colors.YELLOW}║
445
+ ║ ║
446
+ ║ {Colors.OKGREEN}✨ Happy coding! See you next time! ✨ {Colors.YELLOW}║
447
+ ║ ║
448
+ ╚═══════════════════════════════════════════════════════════════════════════════╝{Colors.ENDC}
449
+ """
450
+ print(goodbye)
451
+
452
+ def ask_continue(self) -> bool:
453
+ """Ask user if they want to continue"""
454
+ print(
455
+ f"\n{Colors.BOLD}{Colors.CYAN}Press Enter to continue or 'q' to quit: {Colors.ENDC}",
456
+ end="",
457
+ )
458
+ choice = input().strip().lower()
459
+ return choice not in ["q", "quit", "exit"]