local-deep-research 0.1.26__py3-none-any.whl → 0.2.0__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 (140) hide show
  1. local_deep_research/__init__.py +23 -22
  2. local_deep_research/__main__.py +16 -0
  3. local_deep_research/advanced_search_system/__init__.py +7 -0
  4. local_deep_research/advanced_search_system/filters/__init__.py +8 -0
  5. local_deep_research/advanced_search_system/filters/base_filter.py +38 -0
  6. local_deep_research/advanced_search_system/filters/cross_engine_filter.py +200 -0
  7. local_deep_research/advanced_search_system/findings/base_findings.py +81 -0
  8. local_deep_research/advanced_search_system/findings/repository.py +452 -0
  9. local_deep_research/advanced_search_system/knowledge/__init__.py +1 -0
  10. local_deep_research/advanced_search_system/knowledge/base_knowledge.py +151 -0
  11. local_deep_research/advanced_search_system/knowledge/standard_knowledge.py +159 -0
  12. local_deep_research/advanced_search_system/questions/__init__.py +1 -0
  13. local_deep_research/advanced_search_system/questions/base_question.py +64 -0
  14. local_deep_research/advanced_search_system/questions/decomposition_question.py +445 -0
  15. local_deep_research/advanced_search_system/questions/standard_question.py +119 -0
  16. local_deep_research/advanced_search_system/repositories/__init__.py +7 -0
  17. local_deep_research/advanced_search_system/strategies/__init__.py +1 -0
  18. local_deep_research/advanced_search_system/strategies/base_strategy.py +118 -0
  19. local_deep_research/advanced_search_system/strategies/iterdrag_strategy.py +450 -0
  20. local_deep_research/advanced_search_system/strategies/parallel_search_strategy.py +312 -0
  21. local_deep_research/advanced_search_system/strategies/rapid_search_strategy.py +270 -0
  22. local_deep_research/advanced_search_system/strategies/standard_strategy.py +300 -0
  23. local_deep_research/advanced_search_system/tools/__init__.py +1 -0
  24. local_deep_research/advanced_search_system/tools/base_tool.py +100 -0
  25. local_deep_research/advanced_search_system/tools/knowledge_tools/__init__.py +1 -0
  26. local_deep_research/advanced_search_system/tools/question_tools/__init__.py +1 -0
  27. local_deep_research/advanced_search_system/tools/search_tools/__init__.py +1 -0
  28. local_deep_research/api/__init__.py +5 -5
  29. local_deep_research/api/research_functions.py +96 -84
  30. local_deep_research/app.py +8 -0
  31. local_deep_research/citation_handler.py +25 -16
  32. local_deep_research/{config.py → config/config_files.py} +102 -110
  33. local_deep_research/config/llm_config.py +472 -0
  34. local_deep_research/config/search_config.py +77 -0
  35. local_deep_research/defaults/__init__.py +10 -5
  36. local_deep_research/defaults/main.toml +2 -2
  37. local_deep_research/defaults/search_engines.toml +60 -34
  38. local_deep_research/main.py +121 -19
  39. local_deep_research/migrate_db.py +147 -0
  40. local_deep_research/report_generator.py +72 -44
  41. local_deep_research/search_system.py +147 -283
  42. local_deep_research/setup_data_dir.py +35 -0
  43. local_deep_research/test_migration.py +178 -0
  44. local_deep_research/utilities/__init__.py +0 -0
  45. local_deep_research/utilities/db_utils.py +49 -0
  46. local_deep_research/{utilties → utilities}/enums.py +2 -2
  47. local_deep_research/{utilties → utilities}/llm_utils.py +63 -29
  48. local_deep_research/utilities/search_utilities.py +242 -0
  49. local_deep_research/{utilties → utilities}/setup_utils.py +4 -2
  50. local_deep_research/web/__init__.py +0 -1
  51. local_deep_research/web/app.py +86 -1709
  52. local_deep_research/web/app_factory.py +289 -0
  53. local_deep_research/web/database/README.md +70 -0
  54. local_deep_research/web/database/migrate_to_ldr_db.py +289 -0
  55. local_deep_research/web/database/migrations.py +447 -0
  56. local_deep_research/web/database/models.py +117 -0
  57. local_deep_research/web/database/schema_upgrade.py +107 -0
  58. local_deep_research/web/models/database.py +294 -0
  59. local_deep_research/web/models/settings.py +94 -0
  60. local_deep_research/web/routes/api_routes.py +559 -0
  61. local_deep_research/web/routes/history_routes.py +354 -0
  62. local_deep_research/web/routes/research_routes.py +715 -0
  63. local_deep_research/web/routes/settings_routes.py +1592 -0
  64. local_deep_research/web/services/research_service.py +947 -0
  65. local_deep_research/web/services/resource_service.py +149 -0
  66. local_deep_research/web/services/settings_manager.py +669 -0
  67. local_deep_research/web/services/settings_service.py +187 -0
  68. local_deep_research/web/services/socket_service.py +210 -0
  69. local_deep_research/web/static/css/custom_dropdown.css +277 -0
  70. local_deep_research/web/static/css/settings.css +1223 -0
  71. local_deep_research/web/static/css/styles.css +525 -48
  72. local_deep_research/web/static/js/components/custom_dropdown.js +428 -0
  73. local_deep_research/web/static/js/components/detail.js +348 -0
  74. local_deep_research/web/static/js/components/fallback/formatting.js +122 -0
  75. local_deep_research/web/static/js/components/fallback/ui.js +215 -0
  76. local_deep_research/web/static/js/components/history.js +487 -0
  77. local_deep_research/web/static/js/components/logpanel.js +949 -0
  78. local_deep_research/web/static/js/components/progress.js +1107 -0
  79. local_deep_research/web/static/js/components/research.js +1865 -0
  80. local_deep_research/web/static/js/components/results.js +766 -0
  81. local_deep_research/web/static/js/components/settings.js +3981 -0
  82. local_deep_research/web/static/js/components/settings_sync.js +106 -0
  83. local_deep_research/web/static/js/main.js +226 -0
  84. local_deep_research/web/static/js/services/api.js +253 -0
  85. local_deep_research/web/static/js/services/audio.js +31 -0
  86. local_deep_research/web/static/js/services/formatting.js +119 -0
  87. local_deep_research/web/static/js/services/pdf.js +622 -0
  88. local_deep_research/web/static/js/services/socket.js +882 -0
  89. local_deep_research/web/static/js/services/ui.js +546 -0
  90. local_deep_research/web/templates/base.html +72 -0
  91. local_deep_research/web/templates/components/custom_dropdown.html +47 -0
  92. local_deep_research/web/templates/components/log_panel.html +32 -0
  93. local_deep_research/web/templates/components/mobile_nav.html +22 -0
  94. local_deep_research/web/templates/components/settings_form.html +299 -0
  95. local_deep_research/web/templates/components/sidebar.html +21 -0
  96. local_deep_research/web/templates/pages/details.html +73 -0
  97. local_deep_research/web/templates/pages/history.html +51 -0
  98. local_deep_research/web/templates/pages/progress.html +57 -0
  99. local_deep_research/web/templates/pages/research.html +139 -0
  100. local_deep_research/web/templates/pages/results.html +59 -0
  101. local_deep_research/web/templates/settings_dashboard.html +78 -192
  102. local_deep_research/web/utils/__init__.py +0 -0
  103. local_deep_research/web/utils/formatters.py +76 -0
  104. local_deep_research/web_search_engines/engines/full_search.py +18 -16
  105. local_deep_research/web_search_engines/engines/meta_search_engine.py +182 -131
  106. local_deep_research/web_search_engines/engines/search_engine_arxiv.py +224 -139
  107. local_deep_research/web_search_engines/engines/search_engine_brave.py +88 -71
  108. local_deep_research/web_search_engines/engines/search_engine_ddg.py +48 -39
  109. local_deep_research/web_search_engines/engines/search_engine_github.py +415 -204
  110. local_deep_research/web_search_engines/engines/search_engine_google_pse.py +123 -90
  111. local_deep_research/web_search_engines/engines/search_engine_guardian.py +210 -157
  112. local_deep_research/web_search_engines/engines/search_engine_local.py +532 -369
  113. local_deep_research/web_search_engines/engines/search_engine_local_all.py +42 -36
  114. local_deep_research/web_search_engines/engines/search_engine_pubmed.py +358 -266
  115. local_deep_research/web_search_engines/engines/search_engine_searxng.py +211 -159
  116. local_deep_research/web_search_engines/engines/search_engine_semantic_scholar.py +213 -170
  117. local_deep_research/web_search_engines/engines/search_engine_serpapi.py +84 -68
  118. local_deep_research/web_search_engines/engines/search_engine_wayback.py +186 -154
  119. local_deep_research/web_search_engines/engines/search_engine_wikipedia.py +115 -77
  120. local_deep_research/web_search_engines/search_engine_base.py +174 -99
  121. local_deep_research/web_search_engines/search_engine_factory.py +192 -102
  122. local_deep_research/web_search_engines/search_engines_config.py +22 -15
  123. {local_deep_research-0.1.26.dist-info → local_deep_research-0.2.0.dist-info}/METADATA +177 -97
  124. local_deep_research-0.2.0.dist-info/RECORD +135 -0
  125. {local_deep_research-0.1.26.dist-info → local_deep_research-0.2.0.dist-info}/WHEEL +1 -2
  126. {local_deep_research-0.1.26.dist-info → local_deep_research-0.2.0.dist-info}/entry_points.txt +3 -0
  127. local_deep_research/defaults/llm_config.py +0 -338
  128. local_deep_research/utilties/search_utilities.py +0 -114
  129. local_deep_research/web/static/js/app.js +0 -3763
  130. local_deep_research/web/templates/api_keys_config.html +0 -82
  131. local_deep_research/web/templates/collections_config.html +0 -90
  132. local_deep_research/web/templates/index.html +0 -348
  133. local_deep_research/web/templates/llm_config.html +0 -120
  134. local_deep_research/web/templates/main_config.html +0 -89
  135. local_deep_research/web/templates/search_engines_config.html +0 -154
  136. local_deep_research/web/templates/settings.html +0 -519
  137. local_deep_research-0.1.26.dist-info/RECORD +0 -61
  138. local_deep_research-0.1.26.dist-info/top_level.txt +0 -1
  139. /local_deep_research/{utilties → config}/__init__.py +0 -0
  140. {local_deep_research-0.1.26.dist-info → local_deep_research-0.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,187 @@
1
+ import logging
2
+ from pathlib import Path
3
+ from typing import Any, Dict, Optional, Union
4
+
5
+ from ..database.models import Setting, SettingType
6
+ from .settings_manager import SettingsManager
7
+
8
+ # Initialize logger
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ def get_settings_manager(db_session=None):
13
+ """
14
+ Get or create the settings manager instance.
15
+
16
+ Args:
17
+ db_session: Optional database session to use
18
+
19
+ Returns:
20
+ SettingsManager: The settings manager instance
21
+ """
22
+ return SettingsManager.get_instance(db_session)
23
+
24
+
25
+ def get_setting(key: str, default: Any = None, db_session=None) -> Any:
26
+ """
27
+ Get a setting value by key
28
+
29
+ Args:
30
+ key: Setting key
31
+ default: Default value if setting not found
32
+ db_session: Optional database session to use
33
+
34
+ Returns:
35
+ Any: The setting value
36
+ """
37
+ manager = get_settings_manager(db_session)
38
+ return manager.get_setting(key, default)
39
+
40
+
41
+ def set_setting(key: str, value: Any, commit: bool = True, db_session=None) -> bool:
42
+ """
43
+ Set a setting value
44
+
45
+ Args:
46
+ key: Setting key
47
+ value: Setting value
48
+ commit: Whether to commit the change
49
+ db_session: Optional database session
50
+
51
+ Returns:
52
+ bool: True if successful
53
+ """
54
+ manager = get_settings_manager(db_session)
55
+ return manager.set_setting(key, value, commit)
56
+
57
+
58
+ def get_all_settings(
59
+ setting_type: Optional[SettingType] = None, db_session=None
60
+ ) -> Dict[str, Any]:
61
+ """
62
+ Get all settings, optionally filtered by type
63
+
64
+ Args:
65
+ setting_type: Optional filter by type
66
+ db_session: Optional database session
67
+
68
+ Returns:
69
+ Dict[str, Any]: Dictionary of settings
70
+ """
71
+ manager = get_settings_manager(db_session)
72
+ return manager.get_all_settings(setting_type)
73
+
74
+
75
+ def get_all_settings_as_dict(db_session=None) -> Dict[str, Dict[str, Any]]:
76
+ """
77
+ Get all settings as a structured dictionary
78
+
79
+ Args:
80
+ db_session: Optional database session
81
+
82
+ Returns:
83
+ Dict[str, Dict[str, Any]]: Dictionary of settings grouped by type
84
+ """
85
+ # Get settings manager
86
+ manager = get_settings_manager(db_session)
87
+
88
+ # Get all settings
89
+ all_settings = {}
90
+
91
+ for setting_type in SettingType:
92
+ type_key = setting_type.value.lower()
93
+ type_settings = manager.get_all_settings(setting_type)
94
+ if type_settings:
95
+ all_settings[type_key] = type_settings
96
+
97
+ return all_settings
98
+
99
+
100
+ def create_or_update_setting(
101
+ setting: Union[Dict[str, Any], Setting], commit: bool = True, db_session=None
102
+ ) -> Optional[Setting]:
103
+ """
104
+ Create or update a setting
105
+
106
+ Args:
107
+ setting: Setting dictionary or object
108
+ commit: Whether to commit the change
109
+ db_session: Optional database session
110
+
111
+ Returns:
112
+ Optional[Setting]: The setting object if successful
113
+ """
114
+ manager = get_settings_manager(db_session)
115
+ return manager.create_or_update_setting(setting, commit)
116
+
117
+
118
+ def bulk_update_settings(
119
+ settings_dict: Dict[str, Any], commit: bool = True, db_session=None
120
+ ) -> bool:
121
+ """
122
+ Update multiple settings from a dictionary
123
+
124
+ Args:
125
+ settings_dict: Dictionary of setting keys and values
126
+ commit: Whether to commit the changes
127
+ db_session: Optional database session
128
+
129
+ Returns:
130
+ bool: True if all updates were successful
131
+ """
132
+ manager = get_settings_manager(db_session)
133
+ success = True
134
+
135
+ for key, value in settings_dict.items():
136
+ if not manager.set_setting(key, value, commit=False):
137
+ success = False
138
+
139
+ if commit and success and manager.db_session:
140
+ try:
141
+ manager.db_session.commit()
142
+ except Exception as e:
143
+ logger.error(f"Error committing bulk settings update: {e}")
144
+ manager.db_session.rollback()
145
+ success = False
146
+
147
+ return success
148
+
149
+
150
+ def import_settings_from_file(
151
+ main_settings_file: Union[str, Path],
152
+ search_engines_file: Union[str, Path],
153
+ collections_file: Union[str, Path],
154
+ db_session=None,
155
+ ) -> bool:
156
+ """
157
+ Import settings from default configuration files
158
+
159
+ Args:
160
+ main_settings_file: Path to the main settings file
161
+ search_engines_file: Path to the search engines file
162
+ collections_file: Path to the collections file
163
+ db_session: Optional database session
164
+
165
+ Returns:
166
+ bool: True if import was successful
167
+ """
168
+ manager = get_settings_manager(db_session)
169
+ return manager.import_default_settings(
170
+ main_settings_file, search_engines_file, collections_file
171
+ )
172
+
173
+
174
+ def validate_setting(setting: Setting, value: Any) -> tuple[bool, Optional[str]]:
175
+ """
176
+ Validate a setting value based on its type and constraints
177
+
178
+ Args:
179
+ setting: The Setting object to validate against
180
+ value: The value to validate
181
+
182
+ Returns:
183
+ tuple[bool, Optional[str]]: (is_valid, error_message)
184
+ """
185
+ from ..routes.settings_routes import validate_setting as routes_validate_setting
186
+
187
+ return routes_validate_setting(setting, value)
@@ -0,0 +1,210 @@
1
+ import logging
2
+
3
+ # Initialize logger
4
+ logger = logging.getLogger(__name__)
5
+
6
+ # Make this a module variable to be set by the Flask app on initialization
7
+ socketio = None
8
+ # Socket subscription tracking
9
+ socket_subscriptions = {}
10
+
11
+
12
+ def set_socketio(socket_instance):
13
+ """Set the Socket.IO instance for the service."""
14
+ global socketio
15
+ socketio = socket_instance
16
+ logger.info("Socket.IO instance attached to socket service")
17
+
18
+
19
+ def emit_socket_event(event, data, room=None):
20
+ """
21
+ Emit a socket event to clients.
22
+
23
+ Args:
24
+ event: The event name to emit
25
+ data: The data to send with the event
26
+ room: Optional room ID to send to specific client
27
+
28
+ Returns:
29
+ bool: True if emission was successful, False otherwise
30
+ """
31
+ global socketio
32
+
33
+ if not socketio:
34
+ logger.error("Socket.IO not initialized when attempting to emit event")
35
+ return False
36
+
37
+ try:
38
+ # If room is specified, only emit to that room
39
+ if room:
40
+ socketio.emit(event, data, room=room)
41
+ else:
42
+ # Otherwise broadcast to all
43
+ socketio.emit(event, data)
44
+ return True
45
+ except Exception as e:
46
+ logger.error(f"Error emitting socket event {event}: {str(e)}")
47
+ return False
48
+
49
+
50
+ def emit_to_subscribers(event_base, research_id, data):
51
+ """
52
+ Emit an event to all subscribers of a specific research.
53
+
54
+ Args:
55
+ event_base: Base event name (will be formatted with research_id)
56
+ research_id: ID of the research
57
+ data: The data to send with the event
58
+
59
+ Returns:
60
+ bool: True if emission was successful, False otherwise
61
+ """
62
+ global socketio
63
+
64
+ if not socketio:
65
+ logger.error("Socket.IO not initialized when attempting to emit to subscribers")
66
+ return False
67
+
68
+ try:
69
+ # Emit to the general channel for the research
70
+ full_event = f"{event_base}_{research_id}"
71
+ socketio.emit(full_event, data)
72
+
73
+ # Emit to specific subscribers
74
+ if research_id in socket_subscriptions and socket_subscriptions[research_id]:
75
+ for sid in socket_subscriptions[research_id]:
76
+ try:
77
+ socketio.emit(full_event, data, room=sid)
78
+ except Exception as sub_err:
79
+ logger.error(f"Error emitting to subscriber {sid}: {str(sub_err)}")
80
+
81
+ return True
82
+ except Exception as e:
83
+ logger.error(
84
+ f"Error emitting to subscribers for research {research_id}: {str(e)}"
85
+ )
86
+ return False
87
+
88
+
89
+ # Socket event handlers moved from app.py
90
+ def handle_connect(request):
91
+ """Handle client connection"""
92
+ logger.info(f"Client connected: {request.sid}")
93
+
94
+
95
+ def handle_disconnect(request):
96
+ """Handle client disconnection"""
97
+ try:
98
+ logger.info(f"Client disconnected: {request.sid}")
99
+ # Clean up subscriptions for this client
100
+ global socket_subscriptions
101
+ for research_id, subscribers in list(socket_subscriptions.items()):
102
+ if request.sid in subscribers:
103
+ subscribers.remove(request.sid)
104
+ if not subscribers:
105
+ socket_subscriptions.pop(research_id, None)
106
+ logger.info(f"Removed empty subscription for research {research_id}")
107
+ except Exception as e:
108
+ logger.error(f"Error handling disconnect: {e}")
109
+
110
+
111
+ def handle_subscribe(data, request, active_research=None):
112
+ """Handle client subscription to research updates"""
113
+ from datetime import datetime
114
+
115
+ from ..models.database import get_db_connection
116
+
117
+ research_id = data.get("research_id")
118
+ if research_id:
119
+ # First check if this research is still active
120
+ conn = get_db_connection()
121
+ cursor = conn.cursor()
122
+ cursor.execute(
123
+ "SELECT status FROM research_history WHERE id = ?", (research_id,)
124
+ )
125
+ result = cursor.fetchone()
126
+ conn.close()
127
+
128
+ # Only allow subscription to valid research
129
+ if result:
130
+ status = result[0]
131
+
132
+ # Initialize subscription set if needed
133
+ global socket_subscriptions
134
+ if research_id not in socket_subscriptions:
135
+ socket_subscriptions[research_id] = set()
136
+
137
+ # Add this client to the subscribers
138
+ socket_subscriptions[research_id].add(request.sid)
139
+ logger.info(f"Client {request.sid} subscribed to research {research_id}")
140
+
141
+ # Send current status immediately if available
142
+ if active_research and research_id in active_research:
143
+ progress = active_research[research_id]["progress"]
144
+ latest_log = (
145
+ active_research[research_id]["log"][-1]
146
+ if active_research[research_id]["log"]
147
+ else None
148
+ )
149
+
150
+ if latest_log:
151
+ emit_socket_event(
152
+ f"research_progress_{research_id}",
153
+ {
154
+ "progress": progress,
155
+ "message": latest_log.get("message", "Processing..."),
156
+ "status": "in_progress",
157
+ "log_entry": latest_log,
158
+ },
159
+ room=request.sid,
160
+ )
161
+ elif status in ["completed", "failed", "suspended"]:
162
+ # Send final status for completed research
163
+ emit_socket_event(
164
+ f"research_progress_{research_id}",
165
+ {
166
+ "progress": 100 if status == "completed" else 0,
167
+ "message": (
168
+ "Research completed successfully"
169
+ if status == "completed"
170
+ else (
171
+ "Research failed"
172
+ if status == "failed"
173
+ else "Research was suspended"
174
+ )
175
+ ),
176
+ "status": status,
177
+ "log_entry": {
178
+ "time": datetime.utcnow().isoformat(),
179
+ "message": f"Research is {status}",
180
+ "progress": 100 if status == "completed" else 0,
181
+ "metadata": {
182
+ "phase": (
183
+ "complete" if status == "completed" else "error"
184
+ )
185
+ },
186
+ },
187
+ },
188
+ room=request.sid,
189
+ )
190
+ else:
191
+ # Research not found
192
+ emit_socket_event(
193
+ "error",
194
+ {"message": f"Research ID {research_id} not found"},
195
+ room=request.sid,
196
+ )
197
+
198
+
199
+ def handle_socket_error(e):
200
+ """Handle Socket.IO errors"""
201
+ logger.error(f"Socket.IO error: {str(e)}")
202
+ # Don't propagate exceptions to avoid crashing the server
203
+ return False
204
+
205
+
206
+ def handle_default_error(e):
207
+ """Handle unhandled Socket.IO errors"""
208
+ logger.error(f"Unhandled Socket.IO error: {str(e)}")
209
+ # Don't propagate exceptions to avoid crashing the server
210
+ return False
@@ -0,0 +1,277 @@
1
+ /**
2
+ * Custom Dropdown Component
3
+ * Styling for reusable dropdown component
4
+ */
5
+
6
+ .custom-dropdown {
7
+ position: relative;
8
+ width: 100%;
9
+ z-index: 5; /* Base z-index for the dropdown container */
10
+ }
11
+
12
+ .custom-dropdown-input {
13
+ width: 100%;
14
+ padding: 10px;
15
+ border-radius: 6px;
16
+ border: 1px solid var(--border-color, #343452);
17
+ background-color: var(--bg-secondary, #1e1e2d);
18
+ color: var(--text-primary, #f5f5f5);
19
+ font-size: 0.9rem;
20
+ cursor: pointer;
21
+ position: relative;
22
+ z-index: 6; /* Higher than container for stacking context */
23
+ }
24
+
25
+ .custom-dropdown-input:focus {
26
+ outline: none;
27
+ border-color: var(--accent-primary, #6e4ff6);
28
+ box-shadow: 0 0 0 3px rgba(110, 79, 246, 0.15);
29
+ }
30
+
31
+ .custom-dropdown-list {
32
+ position: absolute;
33
+ width: 100%;
34
+ max-height: 250px;
35
+ overflow-y: auto;
36
+ z-index: 9999; /* High z-index for default state */
37
+ background-color: var(--bg-tertiary, #2a2a3a);
38
+ border: 1px solid var(--border-color, #343452);
39
+ border-radius: 6px;
40
+ margin-top: 5px;
41
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.4);
42
+ display: none;
43
+ }
44
+
45
+ /* Styles for when the dropdown is active */
46
+ .custom-dropdown-list.dropdown-active {
47
+ z-index: 99999 !important; /* Extremely high z-index */
48
+ margin-top: 0 !important; /* Reset margin as top/left are calculated */
49
+ max-height: 40vh !important; /* Cap at 40% of viewport height */
50
+ overflow-y: auto !important;
51
+ box-shadow: 0 6px 20px rgba(0, 0, 0, 0.5) !important; /* Stronger shadow */
52
+ visibility: visible !important;
53
+ opacity: 1 !important;
54
+ clip: auto !important;
55
+ clip-path: none !important;
56
+ transform: none !important;
57
+ pointer-events: auto !important;
58
+
59
+ /* Ensure contrast with background */
60
+ background-color: var(--bg-tertiary, #2a2a3a) !important;
61
+ color: var(--text-primary, #f5f5f5) !important;
62
+ border: 2px solid var(--accent-primary, #6e4ff6) !important; /* More visible border */
63
+
64
+ /* Width, top, left are set by JS */
65
+ }
66
+
67
+ /* Explicitly override any ancestor overflow properties - more targeted to avoid side effects */
68
+ body * {
69
+ overflow: visible !important;
70
+ }
71
+
72
+ /* Immediately revert the blanket override for essential elements */
73
+ html, body {
74
+ overflow: auto !important;
75
+ }
76
+ .custom-dropdown-list,
77
+ .custom-dropdown-list.dropdown-fixed {
78
+ overflow-y: auto !important;
79
+ }
80
+
81
+ /* Add a special class to the body when a dropdown is active */
82
+ body.dropdown-active {
83
+ /* Apply a higher z-index to ensure body doesn't create a stacking context that traps our dropdown */
84
+ z-index: auto !important;
85
+ position: relative !important;
86
+ }
87
+
88
+ .custom-dropdown-item {
89
+ padding: 10px 15px;
90
+ cursor: pointer;
91
+ color: var(--text-primary, #f5f5f5);
92
+ transition: background-color 0.2s;
93
+ }
94
+
95
+ .custom-dropdown-item:hover,
96
+ .custom-dropdown-item.active {
97
+ background-color: rgba(110, 79, 246, 0.1);
98
+ }
99
+
100
+ .custom-dropdown-item .highlight {
101
+ background-color: rgba(110, 79, 246, 0.3);
102
+ border-radius: 2px;
103
+ padding: 0 2px;
104
+ }
105
+
106
+ .custom-dropdown-no-results {
107
+ padding: 10px 15px;
108
+ color: var(--text-secondary, #c0c0cc);
109
+ font-style: italic;
110
+ }
111
+
112
+ .custom-dropdown-footer {
113
+ padding: 10px 15px;
114
+ border-top: 1px solid var(--border-color, #343452);
115
+ color: var(--accent-tertiary, #40bfff);
116
+ font-size: 0.85rem;
117
+ background-color: rgba(64, 191, 255, 0.08);
118
+ }
119
+
120
+ /* Custom dropdown with refresh button */
121
+ .custom-dropdown-with-refresh {
122
+ display: flex;
123
+ align-items: center; /* Center items vertically */
124
+ gap: 8px;
125
+ width: 100%;
126
+ }
127
+
128
+ .custom-dropdown-with-refresh .custom-dropdown {
129
+ flex: 1;
130
+ }
131
+
132
+ .custom-dropdown-refresh-btn {
133
+ display: flex;
134
+ align-items: center;
135
+ justify-content: center;
136
+ width: 38px;
137
+ height: 38px;
138
+ background-color: var(--bg-tertiary, #2a2a3a);
139
+ border: 1px solid var(--border-color, #343452);
140
+ border-radius: 6px;
141
+ color: var(--text-secondary, #c0c0cc);
142
+ cursor: pointer;
143
+ transition: all 0.2s;
144
+ flex-shrink: 0; /* Prevent button from shrinking */
145
+ }
146
+
147
+ .custom-dropdown-refresh-btn:hover {
148
+ background-color: var(--bg-secondary, #1e1e2d);
149
+ color: var(--accent-primary, #6e4ff6);
150
+ border-color: var(--accent-primary, #6e4ff6);
151
+ }
152
+
153
+ .custom-dropdown-refresh-btn.loading {
154
+ pointer-events: none;
155
+ }
156
+
157
+ /* Remove the inner dropdown loader - we'll use JS to only show it in the button */
158
+ .dropdown-loading-indicator {
159
+ display: none;
160
+ }
161
+
162
+ /* Only show loader inside button when needed */
163
+ .custom-dropdown-refresh-btn.loading i {
164
+ display: none;
165
+ }
166
+
167
+ .custom-dropdown-refresh-btn.loading:before {
168
+ content: "";
169
+ width: 16px;
170
+ height: 16px;
171
+ border: 2px solid rgba(255, 255, 255, 0.1);
172
+ border-radius: 50%;
173
+ border-top-color: var(--accent-primary, #6e4ff6);
174
+ animation: spin 0.8s linear infinite;
175
+ display: block;
176
+ }
177
+
178
+ @keyframes spin {
179
+ 0% { transform: rotate(0deg); }
180
+ 100% { transform: rotate(360deg); }
181
+ }
182
+
183
+ /* Advanced Options Panel Styles */
184
+ .advanced-options-toggle {
185
+ display: flex;
186
+ align-items: center;
187
+ justify-content: space-between;
188
+ margin-bottom: 1.5rem;
189
+ padding: 0.8rem 1rem;
190
+ background-color: var(--bg-tertiary, rgba(40, 45, 60, 0.7));
191
+ border-radius: 6px;
192
+ cursor: pointer;
193
+ transition: background-color 0.2s;
194
+ border: 1px solid var(--border-color, rgba(255, 255, 255, 0.1));
195
+ }
196
+
197
+ .advanced-options-toggle:hover {
198
+ background-color: rgba(50, 55, 70, 0.8);
199
+ }
200
+
201
+ .advanced-options-toggle.open {
202
+ margin-bottom: 0.5rem;
203
+ }
204
+
205
+ .toggle-text {
206
+ font-weight: 600;
207
+ color: var(--text-primary, #e1e2e4);
208
+ }
209
+
210
+ .advanced-options-panel {
211
+ position: relative;
212
+ z-index: 1; /* Lower than dropdowns */
213
+ transition: max-height 0.3s ease-in-out, opacity 0.3s ease-in-out, margin 0.3s ease-in-out;
214
+ max-height: 0;
215
+ overflow: hidden; /* Keep hidden when collapsed */
216
+ opacity: 0;
217
+ margin-top: 0;
218
+ margin-bottom: 0;
219
+ display: block !important; /* Always display but use max-height/opacity for animation */
220
+ }
221
+
222
+ .advanced-options-panel.expanded {
223
+ max-height: 1000px; /* Large enough to fit all content */
224
+ opacity: 1;
225
+ margin-bottom: 1.5rem;
226
+ margin-top: 0.5rem;
227
+ overflow: visible; /* Allow dropdowns to overflow when expanded */
228
+ }
229
+
230
+ /* Make dropdowns in advanced panel appear more quickly when expanded */
231
+ .advanced-options-panel.expanded .form-group {
232
+ animation: fadeIn 0.3s ease forwards;
233
+ }
234
+
235
+ /* Stagger animation for form groups */
236
+ .advanced-options-panel.expanded .form-row:nth-child(1) .form-group {
237
+ animation-delay: 0.05s;
238
+ }
239
+
240
+ .advanced-options-panel.expanded .form-row:nth-child(2) .form-group {
241
+ animation-delay: 0.1s;
242
+ }
243
+
244
+ .advanced-options-panel.expanded .form-row:nth-child(3) .form-group {
245
+ animation-delay: 0.15s;
246
+ }
247
+
248
+ @keyframes fadeIn {
249
+ from { opacity: 0; /* transform: translateY(-5px); REMOVED */ }
250
+ to { opacity: 1; /* transform: translateY(0); REMOVED */ }
251
+ }
252
+
253
+ .advanced-options-toggle i {
254
+ transition: transform 0.3s ease;
255
+ }
256
+
257
+ .advanced-options-toggle.open i {
258
+ transform: rotate(180deg);
259
+ }
260
+
261
+ /* Loading state for dropdowns */
262
+ .custom-dropdown.loading input,
263
+ .form-group.loading input {
264
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath fill='%236e4ff6' d='M12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2zm0 18a8 8 0 1 1 8-8 8 8 0 0 1-8 8z' opacity='0.3'/%3E%3Cpath fill='%236e4ff6' d='M20 12h2A10 10 0 0 0 12 2v2a8 8 0 0 1 8 8z'%3E%3CanimateTransform attributeName='transform' dur='1s' from='0 12 12' repeatCount='indefinite' to='360 12 12' type='rotate'/%3E%3C/path%3E%3C/svg%3E");
265
+ background-position: right 10px center;
266
+ background-repeat: no-repeat;
267
+ background-size: 16px 16px;
268
+ }
269
+
270
+ /* Add special handling for dropdowns in the advanced panel */
271
+ /* .advanced-options-panel .custom-dropdown {
272
+ z-index: 100;
273
+ } */
274
+
275
+ .advanced-options-panel .custom-dropdown-list {
276
+ z-index: 10000; /* Even higher z-index for dropdowns in the advanced panel */
277
+ }