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.
- local_deep_research/__init__.py +23 -22
- local_deep_research/__main__.py +16 -0
- local_deep_research/advanced_search_system/__init__.py +7 -0
- local_deep_research/advanced_search_system/filters/__init__.py +8 -0
- local_deep_research/advanced_search_system/filters/base_filter.py +38 -0
- local_deep_research/advanced_search_system/filters/cross_engine_filter.py +200 -0
- local_deep_research/advanced_search_system/findings/base_findings.py +81 -0
- local_deep_research/advanced_search_system/findings/repository.py +452 -0
- local_deep_research/advanced_search_system/knowledge/__init__.py +1 -0
- local_deep_research/advanced_search_system/knowledge/base_knowledge.py +151 -0
- local_deep_research/advanced_search_system/knowledge/standard_knowledge.py +159 -0
- local_deep_research/advanced_search_system/questions/__init__.py +1 -0
- local_deep_research/advanced_search_system/questions/base_question.py +64 -0
- local_deep_research/advanced_search_system/questions/decomposition_question.py +445 -0
- local_deep_research/advanced_search_system/questions/standard_question.py +119 -0
- local_deep_research/advanced_search_system/repositories/__init__.py +7 -0
- local_deep_research/advanced_search_system/strategies/__init__.py +1 -0
- local_deep_research/advanced_search_system/strategies/base_strategy.py +118 -0
- local_deep_research/advanced_search_system/strategies/iterdrag_strategy.py +450 -0
- local_deep_research/advanced_search_system/strategies/parallel_search_strategy.py +312 -0
- local_deep_research/advanced_search_system/strategies/rapid_search_strategy.py +270 -0
- local_deep_research/advanced_search_system/strategies/standard_strategy.py +300 -0
- local_deep_research/advanced_search_system/tools/__init__.py +1 -0
- local_deep_research/advanced_search_system/tools/base_tool.py +100 -0
- local_deep_research/advanced_search_system/tools/knowledge_tools/__init__.py +1 -0
- local_deep_research/advanced_search_system/tools/question_tools/__init__.py +1 -0
- local_deep_research/advanced_search_system/tools/search_tools/__init__.py +1 -0
- local_deep_research/api/__init__.py +5 -5
- local_deep_research/api/research_functions.py +96 -84
- local_deep_research/app.py +8 -0
- local_deep_research/citation_handler.py +25 -16
- local_deep_research/{config.py → config/config_files.py} +102 -110
- local_deep_research/config/llm_config.py +472 -0
- local_deep_research/config/search_config.py +77 -0
- local_deep_research/defaults/__init__.py +10 -5
- local_deep_research/defaults/main.toml +2 -2
- local_deep_research/defaults/search_engines.toml +60 -34
- local_deep_research/main.py +121 -19
- local_deep_research/migrate_db.py +147 -0
- local_deep_research/report_generator.py +72 -44
- local_deep_research/search_system.py +147 -283
- local_deep_research/setup_data_dir.py +35 -0
- local_deep_research/test_migration.py +178 -0
- local_deep_research/utilities/__init__.py +0 -0
- local_deep_research/utilities/db_utils.py +49 -0
- local_deep_research/{utilties → utilities}/enums.py +2 -2
- local_deep_research/{utilties → utilities}/llm_utils.py +63 -29
- local_deep_research/utilities/search_utilities.py +242 -0
- local_deep_research/{utilties → utilities}/setup_utils.py +4 -2
- local_deep_research/web/__init__.py +0 -1
- local_deep_research/web/app.py +86 -1709
- local_deep_research/web/app_factory.py +289 -0
- local_deep_research/web/database/README.md +70 -0
- local_deep_research/web/database/migrate_to_ldr_db.py +289 -0
- local_deep_research/web/database/migrations.py +447 -0
- local_deep_research/web/database/models.py +117 -0
- local_deep_research/web/database/schema_upgrade.py +107 -0
- local_deep_research/web/models/database.py +294 -0
- local_deep_research/web/models/settings.py +94 -0
- local_deep_research/web/routes/api_routes.py +559 -0
- local_deep_research/web/routes/history_routes.py +354 -0
- local_deep_research/web/routes/research_routes.py +715 -0
- local_deep_research/web/routes/settings_routes.py +1592 -0
- local_deep_research/web/services/research_service.py +947 -0
- local_deep_research/web/services/resource_service.py +149 -0
- local_deep_research/web/services/settings_manager.py +669 -0
- local_deep_research/web/services/settings_service.py +187 -0
- local_deep_research/web/services/socket_service.py +210 -0
- local_deep_research/web/static/css/custom_dropdown.css +277 -0
- local_deep_research/web/static/css/settings.css +1223 -0
- local_deep_research/web/static/css/styles.css +525 -48
- local_deep_research/web/static/js/components/custom_dropdown.js +428 -0
- local_deep_research/web/static/js/components/detail.js +348 -0
- local_deep_research/web/static/js/components/fallback/formatting.js +122 -0
- local_deep_research/web/static/js/components/fallback/ui.js +215 -0
- local_deep_research/web/static/js/components/history.js +487 -0
- local_deep_research/web/static/js/components/logpanel.js +949 -0
- local_deep_research/web/static/js/components/progress.js +1107 -0
- local_deep_research/web/static/js/components/research.js +1865 -0
- local_deep_research/web/static/js/components/results.js +766 -0
- local_deep_research/web/static/js/components/settings.js +3981 -0
- local_deep_research/web/static/js/components/settings_sync.js +106 -0
- local_deep_research/web/static/js/main.js +226 -0
- local_deep_research/web/static/js/services/api.js +253 -0
- local_deep_research/web/static/js/services/audio.js +31 -0
- local_deep_research/web/static/js/services/formatting.js +119 -0
- local_deep_research/web/static/js/services/pdf.js +622 -0
- local_deep_research/web/static/js/services/socket.js +882 -0
- local_deep_research/web/static/js/services/ui.js +546 -0
- local_deep_research/web/templates/base.html +72 -0
- local_deep_research/web/templates/components/custom_dropdown.html +47 -0
- local_deep_research/web/templates/components/log_panel.html +32 -0
- local_deep_research/web/templates/components/mobile_nav.html +22 -0
- local_deep_research/web/templates/components/settings_form.html +299 -0
- local_deep_research/web/templates/components/sidebar.html +21 -0
- local_deep_research/web/templates/pages/details.html +73 -0
- local_deep_research/web/templates/pages/history.html +51 -0
- local_deep_research/web/templates/pages/progress.html +57 -0
- local_deep_research/web/templates/pages/research.html +139 -0
- local_deep_research/web/templates/pages/results.html +59 -0
- local_deep_research/web/templates/settings_dashboard.html +78 -192
- local_deep_research/web/utils/__init__.py +0 -0
- local_deep_research/web/utils/formatters.py +76 -0
- local_deep_research/web_search_engines/engines/full_search.py +18 -16
- local_deep_research/web_search_engines/engines/meta_search_engine.py +182 -131
- local_deep_research/web_search_engines/engines/search_engine_arxiv.py +224 -139
- local_deep_research/web_search_engines/engines/search_engine_brave.py +88 -71
- local_deep_research/web_search_engines/engines/search_engine_ddg.py +48 -39
- local_deep_research/web_search_engines/engines/search_engine_github.py +415 -204
- local_deep_research/web_search_engines/engines/search_engine_google_pse.py +123 -90
- local_deep_research/web_search_engines/engines/search_engine_guardian.py +210 -157
- local_deep_research/web_search_engines/engines/search_engine_local.py +532 -369
- local_deep_research/web_search_engines/engines/search_engine_local_all.py +42 -36
- local_deep_research/web_search_engines/engines/search_engine_pubmed.py +358 -266
- local_deep_research/web_search_engines/engines/search_engine_searxng.py +211 -159
- local_deep_research/web_search_engines/engines/search_engine_semantic_scholar.py +213 -170
- local_deep_research/web_search_engines/engines/search_engine_serpapi.py +84 -68
- local_deep_research/web_search_engines/engines/search_engine_wayback.py +186 -154
- local_deep_research/web_search_engines/engines/search_engine_wikipedia.py +115 -77
- local_deep_research/web_search_engines/search_engine_base.py +174 -99
- local_deep_research/web_search_engines/search_engine_factory.py +192 -102
- local_deep_research/web_search_engines/search_engines_config.py +22 -15
- {local_deep_research-0.1.26.dist-info → local_deep_research-0.2.0.dist-info}/METADATA +177 -97
- local_deep_research-0.2.0.dist-info/RECORD +135 -0
- {local_deep_research-0.1.26.dist-info → local_deep_research-0.2.0.dist-info}/WHEEL +1 -2
- {local_deep_research-0.1.26.dist-info → local_deep_research-0.2.0.dist-info}/entry_points.txt +3 -0
- local_deep_research/defaults/llm_config.py +0 -338
- local_deep_research/utilties/search_utilities.py +0 -114
- local_deep_research/web/static/js/app.js +0 -3763
- local_deep_research/web/templates/api_keys_config.html +0 -82
- local_deep_research/web/templates/collections_config.html +0 -90
- local_deep_research/web/templates/index.html +0 -348
- local_deep_research/web/templates/llm_config.html +0 -120
- local_deep_research/web/templates/main_config.html +0 -89
- local_deep_research/web/templates/search_engines_config.html +0 -154
- local_deep_research/web/templates/settings.html +0 -519
- local_deep_research-0.1.26.dist-info/RECORD +0 -61
- local_deep_research-0.1.26.dist-info/top_level.txt +0 -1
- /local_deep_research/{utilties → config}/__init__.py +0 -0
- {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
|
+
}
|