search-api-webui 0.1.8__py3-none-any.whl → 0.1.9__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.
search_api_webui/app.py CHANGED
@@ -19,6 +19,7 @@
19
19
  # DEALINGS IN THE SOFTWARE.
20
20
 
21
21
  import json
22
+ import logging
22
23
  import socket
23
24
  import sys
24
25
  import threading
@@ -38,6 +39,14 @@ except ImportError:
38
39
  WEBVIEW_AVAILABLE = False
39
40
 
40
41
 
42
+ # Configure logging
43
+ logging.basicConfig(
44
+ level=logging.INFO,
45
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
46
+ )
47
+ logger = logging.getLogger(__name__)
48
+
49
+
41
50
  def get_resource_path(relative_path):
42
51
  '''Get absolute path to resource, works for dev and for PyInstaller.'''
43
52
  try:
@@ -77,7 +86,7 @@ if not USER_CONFIG_DIR.exists():
77
86
  if PROVIDERS_YAML.exists():
78
87
  provider_map = load_providers(str(PROVIDERS_YAML))
79
88
  else:
80
- print(f'Error: Configuration file not found at {PROVIDERS_YAML}')
89
+ logger.error(f'Configuration file not found at {PROVIDERS_YAML}')
81
90
  provider_map = {}
82
91
 
83
92
 
@@ -88,7 +97,7 @@ def get_stored_config():
88
97
  with open(USER_CONFIG_JSON, encoding='utf-8') as f:
89
98
  return json.load(f)
90
99
  except Exception as e:
91
- print(f'Error reading config: {e}')
100
+ logger.error(f'Error reading config: {e}')
92
101
  return {}
93
102
 
94
103
 
@@ -97,7 +106,7 @@ def save_stored_config(config_dict):
97
106
  with open(USER_CONFIG_JSON, 'w', encoding='utf-8') as f:
98
107
  json.dump(config_dict, f, indent=2)
99
108
  except Exception as e:
100
- print(f'Error saving config: {e}')
109
+ logger.error(f'Error saving config: {e}')
101
110
 
102
111
 
103
112
  @app.route('/api/providers', methods=['GET'])
@@ -123,7 +132,7 @@ def get_providers_list():
123
132
  'user_settings': {
124
133
  'api_url': user_conf.get('api_url', ''),
125
134
  'limit': user_conf.get('limit', '10'),
126
- 'language': user_conf.get('language', 'en-US'),
135
+ 'language': user_conf.get('language'),
127
136
  },
128
137
  },
129
138
  )
@@ -138,31 +147,44 @@ def update_config():
138
147
  if not provider_name:
139
148
  return jsonify({'error': 'Provider name is required'}), 400
140
149
 
141
- if 'api_key' not in data:
142
- return jsonify({'error': 'API Key field is missing'}), 400
143
-
144
150
  api_key = data.get('api_key')
145
151
 
146
152
  api_url = data.get('api_url', '').strip()
147
153
  limit = data.get('limit', '10')
148
- language = data.get('language', 'en-US')
154
+ language = data.get('language')
149
155
 
150
156
  all_config = get_stored_config()
151
157
 
152
158
  if provider_name in all_config and isinstance(all_config[provider_name], str):
153
159
  all_config[provider_name] = {'api_key': all_config[provider_name]}
154
160
 
155
- if not api_key:
156
- if provider_name in all_config:
157
- all_config[provider_name]['api_key'] = ''
158
- else:
159
- if provider_name not in all_config:
160
- all_config[provider_name] = {}
161
+ # Initialize provider config if not exists
162
+ if provider_name not in all_config:
163
+ all_config[provider_name] = {}
161
164
 
162
- all_config[provider_name]['api_key'] = api_key
165
+ # Update advanced settings, skip empty values
166
+ if api_url:
163
167
  all_config[provider_name]['api_url'] = api_url
168
+ elif 'api_url' in all_config[provider_name]:
169
+ del all_config[provider_name]['api_url']
170
+
171
+ if limit:
164
172
  all_config[provider_name]['limit'] = limit
173
+ elif 'limit' in all_config[provider_name]:
174
+ del all_config[provider_name]['limit']
175
+
176
+ if language:
165
177
  all_config[provider_name]['language'] = language
178
+ elif 'language' in all_config[provider_name]:
179
+ del all_config[provider_name]['language']
180
+
181
+ # Only update api_key if explicitly provided
182
+ if api_key is not None:
183
+ all_config[provider_name]['api_key'] = api_key
184
+
185
+ # Clean up empty provider config
186
+ if not all_config[provider_name]:
187
+ del all_config[provider_name]
166
188
 
167
189
  save_stored_config(all_config)
168
190
  return jsonify({'status': 'success'})
@@ -231,20 +253,20 @@ def main():
231
253
 
232
254
  parser = argparse.ArgumentParser(description='Search API WebUI')
233
255
  parser.add_argument('--port', type=int, default=8889, help='Port to run the server on')
234
- parser.add_argument('--host', type=str, default='127.0.0.1', help='Host to run the server on')
256
+ parser.add_argument('--host', type=str, default='localhost', help='Host to run the server on')
235
257
  parser.add_argument('-w', '--webview', action='store_true', help='Use webview to open the application')
236
258
  args = parser.parse_args()
237
259
 
238
260
  url = f'http://{args.host}:{args.port}'
239
- print('Starting Search API WebUI...')
240
- print(f' - Config Storage: {USER_CONFIG_JSON}')
241
- print(f' - Serving on: {url}')
261
+ logger.info('Starting Search API WebUI...')
262
+ logger.info(f' - Config Storage: {USER_CONFIG_JSON}')
263
+ logger.info(f' - Serving on: {url}')
242
264
  if args.webview:
243
- print(' - Mode: webview')
265
+ logger.info(' - Mode: webview')
244
266
 
245
267
  if args.webview:
246
268
  if not WEBVIEW_AVAILABLE:
247
- print('Warning: webview library not installed. Falling back to webbrowser.')
269
+ logger.warning('webview library not installed. Falling back to webbrowser.')
248
270
  # Start server in background thread and wait for it to be ready
249
271
  server_thread = threading.Thread(
250
272
  target=lambda: app.run(
@@ -254,10 +276,10 @@ def main():
254
276
  )
255
277
  server_thread.start()
256
278
  if wait_for_server_ready(args.host, args.port):
257
- print(f'Server is ready! Opening browser: {url}')
279
+ logger.info(f'Server is ready! Opening browser: {url}')
258
280
  webbrowser.open(url)
259
281
  else:
260
- print('Error: Server took too long to start. Browser not opened.')
282
+ logger.error('Server took too long to start. Browser not opened.')
261
283
  else:
262
284
  # Start server in background thread and wait for it to be ready, then start webview
263
285
  server_thread = threading.Thread(
@@ -268,19 +290,19 @@ def main():
268
290
  )
269
291
  server_thread.start()
270
292
  if wait_for_server_ready(args.host, args.port):
271
- print('Server is ready! Using webview mode...')
293
+ logger.info('Server is ready! Using webview mode...')
272
294
  webview.create_window('Search API WebUI', url, width=1200, height=800)
273
295
  webview.start()
274
296
  else:
275
- print('Error: Server took too long to start. Webview not opened.')
297
+ logger.error('Server took too long to start. Webview not opened.')
276
298
  else:
277
299
  # Start a background thread to check server status and open the browser automatically
278
300
  def open_browser():
279
301
  if wait_for_server_ready(args.host, args.port):
280
- print(f'Server is ready! Opening browser: {url}')
302
+ logger.info(f'Server is ready! Opening browser: {url}')
281
303
  webbrowser.open(url)
282
304
  else:
283
- print('Error: Server took too long to start. Browser not opened.')
305
+ logger.error('Server took too long to start. Browser not opened.')
284
306
  threading.Thread(target=open_browser, daemon=True).start()
285
307
  app.run(host=args.host, port=args.port)
286
308
 
@@ -18,6 +18,7 @@
18
18
  # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
19
19
  # DEALINGS IN THE SOFTWARE.
20
20
 
21
+ import logging
21
22
  import os
22
23
 
23
24
  import yaml
@@ -25,6 +26,8 @@ import yaml
25
26
  from .generic import GenericProvider
26
27
  from .querit import QueritSdkProvider
27
28
 
29
+ logger = logging.getLogger(__name__)
30
+
28
31
 
29
32
  def load_providers(file_path='providers.yaml'):
30
33
  '''
@@ -37,7 +40,7 @@ def load_providers(file_path='providers.yaml'):
37
40
  dict: A dictionary mapping provider names to their initialized instances.
38
41
  '''
39
42
  if not os.path.exists(file_path):
40
- print(f'Warning: Provider config file not found at {file_path}')
43
+ logger.warning(f'Provider config file not found at {file_path}')
41
44
  return {}
42
45
 
43
46
  with open(file_path, encoding='utf-8') as f:
@@ -19,6 +19,39 @@
19
19
  # DEALINGS IN THE SOFTWARE.
20
20
 
21
21
  from abc import ABC, abstractmethod
22
+ from urllib.parse import urlparse
23
+
24
+
25
+ def extract_domain_from_url(url):
26
+ '''
27
+ Extract domain name from URL, removing 'www.' prefix.
28
+
29
+ Args:
30
+ url (str): The URL to extract domain from.
31
+
32
+ Returns:
33
+ str | None: The domain name without 'www.' prefix
34
+ (e.g., 'example.com' from 'https://www.example.com/path'),
35
+ or None if the URL is invalid or has no netloc.
36
+
37
+ Examples:
38
+ >>> extract_domain_from_url('https://www.example.com/path')
39
+ 'example.com'
40
+ >>> extract_domain_from_url('http://example.com')
41
+ 'example.com'
42
+ >>> extract_domain_from_url('invalid-url')
43
+ None
44
+ '''
45
+ if not url or not isinstance(url, str):
46
+ return None
47
+ try:
48
+ parsed = urlparse(url)
49
+ domain = parsed.netloc if parsed.netloc else None
50
+ if domain and domain.startswith('www.'):
51
+ domain = domain[4:]
52
+ return domain
53
+ except Exception:
54
+ return None
22
55
 
23
56
 
24
57
  def parse_server_latency(latency_value):
@@ -25,7 +25,7 @@ import time
25
25
  import jmespath
26
26
  import requests
27
27
 
28
- from .base import BaseProvider, parse_server_latency
28
+ from .base import BaseProvider, extract_domain_from_url, parse_server_latency
29
29
 
30
30
  logger = logging.getLogger(__name__)
31
31
 
@@ -59,17 +59,36 @@ class GenericProvider(BaseProvider):
59
59
 
60
60
  Returns:
61
61
  The structure with placeholders replaced by actual values.
62
+ Dict entries with empty values are removed.
63
+ Pure numeric placeholders (e.g., "{limit}") are converted to int/float.
62
64
  '''
63
65
  if isinstance(template_obj, str):
64
66
  # Treat None values as empty strings to prevent "None" appearing in URLs
65
67
  safe_kwargs = {k: (v if v is not None else '') for k, v in kwargs.items()}
66
68
  try:
67
- return template_obj.format(**safe_kwargs)
69
+ result = template_obj.format(**safe_kwargs)
70
+ # Convert to number if the result is a numeric string
71
+ # and the original template was a pure placeholder like "{limit}"
72
+ if template_obj.strip().startswith('{') and template_obj.strip().endswith('}'):
73
+ try:
74
+ # Try int first, then float
75
+ if '.' in result:
76
+ return float(result)
77
+ return int(result)
78
+ except (ValueError, AttributeError):
79
+ pass
80
+ return result
68
81
  except KeyError:
69
82
  # Return original string if a placeholder key is missing in kwargs
70
83
  return template_obj
71
84
  elif isinstance(template_obj, dict):
72
- return {k: self._fill_template(v, **kwargs) for k, v in template_obj.items()}
85
+ result = {}
86
+ for k, v in template_obj.items():
87
+ filled = self._fill_template(v, **kwargs)
88
+ # Skip entries with empty values (e.g., empty strings after template fill)
89
+ if filled != '':
90
+ result[k] = filled
91
+ return result
73
92
  return template_obj
74
93
 
75
94
  def _ensure_connection(self, url, headers):
@@ -111,11 +130,12 @@ class GenericProvider(BaseProvider):
111
130
  '''
112
131
  # 1. Extract parameters with defaults
113
132
  limit = kwargs.get('limit', '10')
114
- language = kwargs.get('language', 'en-US')
115
- custom_url = kwargs.get('api_url', '').strip()
133
+ language = kwargs.get('language')
134
+ custom_url = kwargs.get('api_url')
116
135
 
117
136
  # 2. Determine configuration
118
- url = custom_url if custom_url else self.config.get('url')
137
+ # Use custom api_url if provided, otherwise fallback to config url
138
+ url = custom_url.strip() if custom_url else self.config.get('url')
119
139
  method = self.config.get('method', 'GET')
120
140
 
121
141
  # 3. Prepare context for template injection
@@ -123,8 +143,10 @@ class GenericProvider(BaseProvider):
123
143
  'query': query,
124
144
  'api_key': api_key,
125
145
  'limit': limit,
126
- 'language': language,
127
146
  }
147
+ # Only add language to context if provided
148
+ if language:
149
+ context['language'] = language
128
150
 
129
151
  # 4. construct request components
130
152
  headers = self._fill_template(self.config.get('headers', {}), **context)
@@ -163,7 +185,7 @@ class GenericProvider(BaseProvider):
163
185
 
164
186
  response.raise_for_status()
165
187
  except Exception as e:
166
- logger.error('Request Error: %s', e)
188
+ logger.error('Request Error: %s', e, f"args: {req_args}")
167
189
  return {
168
190
  'error': str(e),
169
191
  'results': [],
@@ -185,18 +207,49 @@ class GenericProvider(BaseProvider):
185
207
 
186
208
  normalized_results = []
187
209
  field_map = mapping.get('fields', {})
210
+ # Define common fields that should be extracted as-is
211
+ common_fields = {'title', 'url', 'site_name', 'site_icon', 'page_age'}
212
+
213
+ # Collect all JMESPath source paths that are already mapped
214
+ mapped_paths = set(field_map.values())
188
215
 
189
216
  for item in root_list:
190
217
  entry = {}
218
+ snippet_fields = {} # Collect unmapped fields from raw API response
219
+
191
220
  # Map specific fields (title, url, etc.) based on config
192
221
  for std_key, source_path in field_map.items():
193
222
  val = jmespath.search(source_path, item)
194
223
  # Decode HTML entities for site_name
195
224
  if std_key == 'site_name' and val:
196
225
  val = html.unescape(val)
197
- entry[std_key] = val if val else ''
226
+
227
+ # Only store common fields in entry
228
+ if std_key in common_fields:
229
+ entry[std_key] = val if val else ''
230
+
231
+ # Find all unmapped fields in the raw item
232
+ # These are fields that exist in the API response but are not in field_map
233
+ if isinstance(item, dict):
234
+ for key, value in item.items():
235
+ # Check if this key is already mapped in config
236
+ # Skip common nested objects and arrays for cleaner output
237
+ if key not in mapped_paths and value and not isinstance(value, (dict, list)):
238
+ snippet_fields[key] = value
239
+
240
+ # Store snippet fields as JSON structure in snippet
241
+ if snippet_fields:
242
+ entry['snippet'] = snippet_fields
243
+ else:
244
+ entry['snippet'] = ''
245
+
198
246
  normalized_results.append(entry)
199
247
 
248
+ # Post-process: extract domain from URL if site_name is empty
249
+ for entry in normalized_results:
250
+ if not entry.get('site_name') and entry.get('url'):
251
+ entry['site_name'] = extract_domain_from_url(entry['url']) or ''
252
+
200
253
  # Extract server latency from response if configured
201
254
  server_latency_path = mapping.get('server_latency_path')
202
255
  server_latency_ms = parse_server_latency(
@@ -19,6 +19,7 @@
19
19
  # DEALINGS IN THE SOFTWARE.
20
20
 
21
21
  import json
22
+ import logging
22
23
  import time
23
24
 
24
25
  from querit import QueritClient
@@ -27,6 +28,8 @@ from querit.models.request import SearchRequest
27
28
 
28
29
  from .base import BaseProvider, parse_server_latency
29
30
 
31
+ logger = logging.getLogger(__name__)
32
+
30
33
 
31
34
  class QueritSdkProvider(BaseProvider):
32
35
  '''
@@ -52,7 +55,7 @@ class QueritSdkProvider(BaseProvider):
52
55
  count=limit,
53
56
  )
54
57
 
55
- print(f'[Querit SDK] Searching: {query} (Limit: {limit})')
58
+ logger.debug(f'[Querit SDK] Searching: {query} (Limit: {limit})')
56
59
 
57
60
  start_time = time.time()
58
61
 
@@ -94,14 +97,14 @@ class QueritSdkProvider(BaseProvider):
94
97
  }
95
98
 
96
99
  except QueritError as e:
97
- print(f'Querit SDK Error: {e}')
100
+ logger.error(f'Querit SDK Error: {e}')
98
101
  return {
99
102
  'error': f'Querit SDK Error: {str(e)}',
100
103
  'results': [],
101
104
  'metrics': {'latency_ms': 0, 'server_latency_ms': None, 'size_bytes': 0},
102
105
  }
103
106
  except Exception as e:
104
- print(f'Unexpected Error: {e}')
107
+ logger.exception(f'Unexpected Error: {e}')
105
108
  return {
106
109
  'error': f'Error: {str(e)}',
107
110
  'results': [],
@@ -7,13 +7,13 @@ querit:
7
7
  "Content-Type": "application/json"
8
8
  payload:
9
9
  query: "{query}"
10
+ count: "{limit}"
10
11
  response_mapping:
11
12
  root_path: "results.result"
12
13
  server_latency_path: "took"
13
14
  fields:
14
15
  title: "title"
15
16
  url: "url"
16
- snippet: "snippet"
17
17
  site_name: "site_name"
18
18
  site_icon: "site_icon"
19
19
  page_age: "page_age"
@@ -25,6 +25,7 @@ you:
25
25
  X-API-Key: "{api_key}"
26
26
  params:
27
27
  query: "{query}"
28
+ count: "{limit}"
28
29
  payload: {}
29
30
  response_mapping:
30
31
  root_path: "results.web"
@@ -32,7 +33,6 @@ you:
32
33
  fields:
33
34
  title: "title"
34
35
  url: "url"
35
- snippet: "snippets[0] || description"
36
36
  site_icon: "favicon_url"
37
37
  page_age: "page_age"
38
38
 
@@ -44,13 +44,13 @@ brave:
44
44
  Accept: "application/json"
45
45
  params:
46
46
  q: "{query}"
47
+ count: "{limit}"
47
48
  payload: {}
48
49
  response_mapping:
49
50
  root_path: "web.results"
50
51
  fields:
51
52
  title: "title"
52
53
  url: "url"
53
- snippet: "description"
54
54
  page_age: "page_age"
55
55
 
56
56
  exa:
@@ -62,12 +62,12 @@ exa:
62
62
  params: {}
63
63
  payload:
64
64
  query: "{query}"
65
+ numResults: "{limit}"
65
66
  response_mapping:
66
67
  root_path: "results"
67
68
  fields:
68
69
  title: "title"
69
70
  url: "url"
70
- snippet: "text"
71
71
  page_age: "publishedDate"
72
72
  site_icon: "favicon"
73
73
 
@@ -0,0 +1 @@
1
+ *,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.pointer-events-none{pointer-events:none}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.right-3{right:.75rem}.top-0{top:0}.top-3\.5{top:.875rem}.z-10{z-index:10}.col-span-2{grid-column:span 2 / span 2}.mx-4{margin-left:1rem;margin-right:1rem}.mx-auto{margin-left:auto;margin-right:auto}.my-4{margin-top:1rem;margin-bottom:1rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.ml-2{margin-left:.5rem}.mr-1{margin-right:.25rem}.mr-2{margin-right:.5rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.flex{display:flex}.inline-flex{display:inline-flex}.grid{display:grid}.h-10{height:2.5rem}.h-12{height:3rem}.h-16{height:4rem}.h-2{height:.5rem}.h-3{height:.75rem}.h-3\.5{height:.875rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-8{height:2rem}.h-9{height:2.25rem}.h-full{height:100%}.min-h-\[500px\]{min-height:500px}.min-h-screen{min-height:100vh}.w-10{width:2.5rem}.w-12{width:3rem}.w-3{width:.75rem}.w-3\.5{width:.875rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-8{width:2rem}.w-9{width:2.25rem}.w-full{width:100%}.max-w-2xl{max-width:42rem}.max-w-4xl{max-width:56rem}.max-w-7xl{max-width:80rem}.max-w-full{max-width:100%}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:spin 1s linear infinite}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-col{flex-direction:column}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-0\.5{gap:.125rem}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.space-y-8>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(2rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(2rem * var(--tw-space-y-reverse))}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.break-words{overflow-wrap:break-word}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.border{border-width:1px}.border-0{border-width:0px}.border-b{border-bottom-width:1px}.border-t{border-top-width:1px}.border-t-4{border-top-width:4px}.border-dashed{border-style:dashed}.border-gray-100{--tw-border-opacity: 1;border-color:rgb(243 244 246 / var(--tw-border-opacity, 1))}.border-gray-200{--tw-border-opacity: 1;border-color:rgb(229 231 235 / var(--tw-border-opacity, 1))}.border-gray-300{--tw-border-opacity: 1;border-color:rgb(209 213 219 / var(--tw-border-opacity, 1))}.border-green-200{--tw-border-opacity: 1;border-color:rgb(187 247 208 / var(--tw-border-opacity, 1))}.border-purple-200{--tw-border-opacity: 1;border-color:rgb(233 213 255 / var(--tw-border-opacity, 1))}.border-red-200{--tw-border-opacity: 1;border-color:rgb(254 202 202 / var(--tw-border-opacity, 1))}.border-transparent{border-color:transparent}.border-t-transparent{border-top-color:transparent}.bg-blue-600{--tw-bg-opacity: 1;background-color:rgb(37 99 235 / var(--tw-bg-opacity, 1))}.bg-gray-100{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity, 1))}.bg-gray-100\/50{background-color:#f3f4f680}.bg-gray-400{--tw-bg-opacity: 1;background-color:rgb(156 163 175 / var(--tw-bg-opacity, 1))}.bg-gray-50{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity, 1))}.bg-gray-900{--tw-bg-opacity: 1;background-color:rgb(17 24 39 / var(--tw-bg-opacity, 1))}.bg-green-100{--tw-bg-opacity: 1;background-color:rgb(220 252 231 / var(--tw-bg-opacity, 1))}.bg-green-200{--tw-bg-opacity: 1;background-color:rgb(187 247 208 / var(--tw-bg-opacity, 1))}.bg-green-50{--tw-bg-opacity: 1;background-color:rgb(240 253 244 / var(--tw-bg-opacity, 1))}.bg-green-500{--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity, 1))}.bg-purple-600{--tw-bg-opacity: 1;background-color:rgb(147 51 234 / var(--tw-bg-opacity, 1))}.bg-red-100{--tw-bg-opacity: 1;background-color:rgb(254 226 226 / var(--tw-bg-opacity, 1))}.bg-red-50{--tw-bg-opacity: 1;background-color:rgb(254 242 242 / var(--tw-bg-opacity, 1))}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.bg-yellow-100{--tw-bg-opacity: 1;background-color:rgb(254 249 195 / var(--tw-bg-opacity, 1))}.bg-gradient-to-br{background-image:linear-gradient(to bottom right,var(--tw-gradient-stops))}.bg-gradient-to-r{background-image:linear-gradient(to right,var(--tw-gradient-stops))}.from-blue-600{--tw-gradient-from: #2563eb var(--tw-gradient-from-position);--tw-gradient-to: rgb(37 99 235 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.from-gray-50{--tw-gradient-from: #f9fafb var(--tw-gradient-from-position);--tw-gradient-to: rgb(249 250 251 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.from-purple-600{--tw-gradient-from: #9333ea var(--tw-gradient-from-position);--tw-gradient-to: rgb(147 51 234 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.to-blue-50{--tw-gradient-to: #eff6ff var(--tw-gradient-to-position)}.to-blue-600{--tw-gradient-to: #2563eb var(--tw-gradient-to-position)}.to-indigo-600{--tw-gradient-to: #4f46e5 var(--tw-gradient-to-position)}.bg-clip-text{-webkit-background-clip:text;background-clip:text}.p-2{padding:.5rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-8{padding-left:2rem;padding-right:2rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-12{padding-top:3rem;padding-bottom:3rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.pb-2{padding-bottom:.5rem}.pl-0{padding-left:0}.pt-4{padding-top:1rem}.pt-6{padding-top:1.5rem}.text-center{text-align:center}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.leading-relaxed{line-height:1.625}.tracking-widest{letter-spacing:.1em}.text-blue-500{--tw-text-opacity: 1;color:rgb(59 130 246 / var(--tw-text-opacity, 1))}.text-blue-600{--tw-text-opacity: 1;color:rgb(37 99 235 / var(--tw-text-opacity, 1))}.text-gray-300{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity, 1))}.text-gray-700{--tw-text-opacity: 1;color:rgb(55 65 81 / var(--tw-text-opacity, 1))}.text-gray-800{--tw-text-opacity: 1;color:rgb(31 41 55 / var(--tw-text-opacity, 1))}.text-gray-900{--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity, 1))}.text-gray-950{--tw-text-opacity: 1;color:rgb(3 7 18 / var(--tw-text-opacity, 1))}.text-green-600{--tw-text-opacity: 1;color:rgb(22 163 74 / var(--tw-text-opacity, 1))}.text-green-700{--tw-text-opacity: 1;color:rgb(21 128 61 / var(--tw-text-opacity, 1))}.text-green-800{--tw-text-opacity: 1;color:rgb(22 101 52 / var(--tw-text-opacity, 1))}.text-purple-600{--tw-text-opacity: 1;color:rgb(147 51 234 / var(--tw-text-opacity, 1))}.text-red-600{--tw-text-opacity: 1;color:rgb(220 38 38 / var(--tw-text-opacity, 1))}.text-red-700{--tw-text-opacity: 1;color:rgb(185 28 28 / var(--tw-text-opacity, 1))}.text-red-800{--tw-text-opacity: 1;color:rgb(153 27 27 / var(--tw-text-opacity, 1))}.text-transparent{color:transparent}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.text-yellow-800{--tw-text-opacity: 1;color:rgb(133 77 14 / var(--tw-text-opacity, 1))}.underline-offset-4{text-underline-offset:4px}.opacity-0{opacity:0}.opacity-50{opacity:.5}.shadow-inner{--tw-shadow: inset 0 2px 4px 0 rgb(0 0 0 / .05);--tw-shadow-colored: inset 0 2px 4px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.outline{outline-style:solid}.ring-1{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-gray-200{--tw-ring-opacity: 1;--tw-ring-color: rgb(229 231 235 / var(--tw-ring-opacity, 1))}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-shadow{transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-500{transition-duration:.5s}body{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity, 1));--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity, 1))}.file\:border-0::file-selector-button{border-width:0px}.file\:bg-transparent::file-selector-button{background-color:transparent}.file\:text-sm::file-selector-button{font-size:.875rem;line-height:1.25rem}.file\:font-medium::file-selector-button{font-weight:500}.placeholder\:text-gray-500::-moz-placeholder{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.placeholder\:text-gray-500::placeholder{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.hover\:bg-blue-700:hover{--tw-bg-opacity: 1;background-color:rgb(29 78 216 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-100:hover{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-200:hover{--tw-bg-opacity: 1;background-color:rgb(229 231 235 / var(--tw-bg-opacity, 1))}.hover\:bg-green-300:hover{--tw-bg-opacity: 1;background-color:rgb(134 239 172 / var(--tw-bg-opacity, 1))}.hover\:bg-purple-50:hover{--tw-bg-opacity: 1;background-color:rgb(250 245 255 / var(--tw-bg-opacity, 1))}.hover\:bg-purple-700:hover{--tw-bg-opacity: 1;background-color:rgb(126 34 206 / var(--tw-bg-opacity, 1))}.hover\:bg-red-50:hover{--tw-bg-opacity: 1;background-color:rgb(254 242 242 / var(--tw-bg-opacity, 1))}.hover\:text-blue-800:hover{--tw-text-opacity: 1;color:rgb(30 64 175 / var(--tw-text-opacity, 1))}.hover\:text-gray-900:hover{--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity, 1))}.hover\:text-purple-700:hover{--tw-text-opacity: 1;color:rgb(126 34 206 / var(--tw-text-opacity, 1))}.hover\:text-red-700:hover{--tw-text-opacity: 1;color:rgb(185 28 28 / var(--tw-text-opacity, 1))}.hover\:underline:hover{text-decoration-line:underline}.hover\:shadow-md:hover{--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-2:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-blue-600:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(37 99 235 / var(--tw-ring-opacity, 1))}.focus\:ring-purple-500:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(168 85 247 / var(--tw-ring-opacity, 1))}.focus\:ring-offset-2:focus{--tw-ring-offset-width: 2px}.focus-visible\:outline-none:focus-visible{outline:2px solid transparent;outline-offset:2px}.focus-visible\:ring-2:focus-visible{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus-visible\:ring-blue-600:focus-visible{--tw-ring-opacity: 1;--tw-ring-color: rgb(37 99 235 / var(--tw-ring-opacity, 1))}.focus-visible\:ring-offset-2:focus-visible{--tw-ring-offset-width: 2px}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}.group:hover .group-hover\:underline{text-decoration-line:underline}.group:hover .group-hover\:opacity-50{opacity:.5}.data-\[side\=Left\]\:border-t-blue-500[data-side=Left]{--tw-border-opacity: 1;border-top-color:rgb(59 130 246 / var(--tw-border-opacity, 1))}.data-\[side\=Right\]\:border-t-orange-500[data-side=Right]{--tw-border-opacity: 1;border-top-color:rgb(249 115 22 / var(--tw-border-opacity, 1))}@media(min-width:768px){.md\:w-64{width:16rem}.md\:w-auto{width:auto}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:flex-row{flex-direction:row}}