search-api-webui 0.1.10__py3-none-any.whl → 0.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
search_api_webui/app.py CHANGED
@@ -20,6 +20,8 @@
20
20
 
21
21
  import json
22
22
  import logging
23
+ import os
24
+ import platform
23
25
  import socket
24
26
  import sys
25
27
  import threading
@@ -39,9 +41,21 @@ except ImportError:
39
41
  WEBVIEW_AVAILABLE = False
40
42
 
41
43
 
42
- # Configure logging
44
+ # Auto-enable webview mode when running as packaged executable
45
+ # This replaces the need for a separate PyInstaller runtime hook
46
+ if (
47
+ getattr(sys, 'frozen', False)
48
+ and platform.system() in ('Windows', 'Darwin')
49
+ and '-w' not in sys.argv
50
+ and '--webview' not in sys.argv
51
+ ):
52
+ sys.argv.append('-w')
53
+
54
+
55
+ # Configure logging based on Flask debug mode or environment variable
56
+ log_level = logging.DEBUG if os.getenv('FLASK_DEBUG') or os.getenv('FLASK_ENV') == 'development' else logging.INFO
43
57
  logging.basicConfig(
44
- level=logging.INFO,
58
+ level=log_level,
45
59
  format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
46
60
  )
47
61
  logger = logging.getLogger(__name__)
@@ -133,6 +147,9 @@ def get_providers_list():
133
147
  'api_url': user_conf.get('api_url', ''),
134
148
  'limit': user_conf.get('limit', '10'),
135
149
  'language': user_conf.get('language'),
150
+ 'use_proxy': user_conf.get('use_proxy', False),
151
+ 'proxy_url': user_conf.get('proxy_url', ''),
152
+ 'skip_warmup': user_conf.get('skip_warmup', False),
136
153
  },
137
154
  },
138
155
  )
@@ -152,6 +169,9 @@ def update_config():
152
169
  api_url = data.get('api_url', '').strip()
153
170
  limit = data.get('limit', '10')
154
171
  language = data.get('language')
172
+ use_proxy = data.get('use_proxy', False)
173
+ proxy_url = data.get('proxy_url', '').strip()
174
+ skip_warmup = data.get('skip_warmup', False)
155
175
 
156
176
  all_config = get_stored_config()
157
177
 
@@ -178,6 +198,23 @@ def update_config():
178
198
  elif 'language' in all_config[provider_name]:
179
199
  del all_config[provider_name]['language']
180
200
 
201
+ # Save proxy settings
202
+ if use_proxy:
203
+ all_config[provider_name]['use_proxy'] = True
204
+ if proxy_url:
205
+ all_config[provider_name]['proxy_url'] = proxy_url
206
+ else:
207
+ if 'use_proxy' in all_config[provider_name]:
208
+ del all_config[provider_name]['use_proxy']
209
+ if 'proxy_url' in all_config[provider_name]:
210
+ del all_config[provider_name]['proxy_url']
211
+
212
+ # Save warmup settings
213
+ if skip_warmup:
214
+ all_config[provider_name]['skip_warmup'] = True
215
+ elif 'skip_warmup' in all_config[provider_name]:
216
+ del all_config[provider_name]['skip_warmup']
217
+
181
218
  # Only update api_key if explicitly provided
182
219
  if api_key is not None:
183
220
  all_config[provider_name]['api_key'] = api_key
@@ -221,6 +258,8 @@ def search_api():
221
258
  'api_url': provider_config.get('api_url'),
222
259
  'limit': provider_config.get('limit'),
223
260
  'language': provider_config.get('language'),
261
+ 'proxy_url': provider_config.get('proxy_url') if provider_config.get('use_proxy') else None,
262
+ 'skip_warmup': provider_config.get('skip_warmup', False),
224
263
  }
225
264
 
226
265
  result = provider.search(query, api_key, **search_kwargs)
@@ -20,6 +20,7 @@
20
20
 
21
21
  import html
22
22
  import logging
23
+ import os
23
24
  import time
24
25
 
25
26
  import jmespath
@@ -81,6 +82,9 @@ class GenericProvider(BaseProvider):
81
82
  except KeyError:
82
83
  # Return original string if a placeholder key is missing in kwargs
83
84
  return template_obj
85
+ elif isinstance(template_obj, list):
86
+ # Handle list/array - recursively process each element
87
+ return [self._fill_template(item, **kwargs) for item in template_obj]
84
88
  elif isinstance(template_obj, dict):
85
89
  result = {}
86
90
  for k, v in template_obj.items():
@@ -123,7 +127,7 @@ class GenericProvider(BaseProvider):
123
127
  Args:
124
128
  query: Search query string
125
129
  api_key: API key for authentication
126
- **kwargs: Additional parameters (limit, language, api_url)
130
+ **kwargs: Additional parameters (limit, language, api_url, proxy_url, skip_warmup)
127
131
 
128
132
  Returns:
129
133
  dict: Search results with 'results' and 'metrics' keys
@@ -132,13 +136,46 @@ class GenericProvider(BaseProvider):
132
136
  limit = kwargs.get('limit', '10')
133
137
  language = kwargs.get('language')
134
138
  custom_url = kwargs.get('api_url')
139
+ proxy_url = kwargs.get('proxy_url')
140
+ skip_warmup = kwargs.get('skip_warmup', False)
141
+
142
+ # 2. Configure proxy for this request if provided
143
+ if proxy_url:
144
+ self.session.proxies = {
145
+ 'http': proxy_url,
146
+ 'https': proxy_url,
147
+ }
148
+ logger.info('Using proxy: %s', proxy_url)
149
+
150
+ # Disable SSL verification when using proxy
151
+ self.session.verify = False
152
+ # Suppress InsecureRequestWarning
153
+ import urllib3
154
+ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
155
+ else:
156
+ # Check environment variables if no user proxy configured
157
+ env_proxy = os.environ.get('https_proxy') or os.environ.get('HTTPS_PROXY') or \
158
+ os.environ.get('http_proxy') or os.environ.get('HTTP_PROXY')
159
+ if env_proxy:
160
+ self.session.proxies = {
161
+ 'http': env_proxy,
162
+ 'https': env_proxy,
163
+ }
164
+ logger.info('Using proxy from environment: %s', env_proxy)
165
+ self.session.verify = False
166
+ import urllib3
167
+ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
168
+ else:
169
+ # No proxy, ensure normal SSL verification
170
+ self.session.proxies = {}
171
+ self.session.verify = True
135
172
 
136
- # 2. Determine configuration
173
+ # 3. Determine configuration
137
174
  # Use custom api_url if provided, otherwise fallback to config url
138
175
  url = custom_url.strip() if custom_url else self.config.get('url')
139
176
  method = self.config.get('method', 'GET')
140
177
 
141
- # 3. Prepare context for template injection
178
+ # 4. Prepare context for template injection
142
179
  context = {
143
180
  'query': query,
144
181
  'api_key': api_key,
@@ -148,7 +185,7 @@ class GenericProvider(BaseProvider):
148
185
  if language:
149
186
  context['language'] = language
150
187
 
151
- # 4. construct request components
188
+ # 5. construct request components
152
189
  headers = self._fill_template(self.config.get('headers', {}), **context)
153
190
  params = self._fill_template(self.config.get('params', {}), **context)
154
191
  json_body = self._fill_template(self.config.get('payload', {}), **context)
@@ -158,15 +195,13 @@ class GenericProvider(BaseProvider):
158
195
 
159
196
  # Ensure connection is pre-warmed (use HEAD request to verify availability)
160
197
  # Pre-warming is not counted in request latency, only verifies connection
161
- try:
162
- self._ensure_connection(url, headers)
163
- except Exception as e:
164
- logger.error('Connection Warm-up Error: %s', e)
165
- return {
166
- 'error': f'Connection failed: {str(e)}',
167
- 'results': [],
168
- 'metrics': {'latency_ms': 0, 'server_latency_ms': None, 'size_bytes': 0},
169
- }
198
+ # Skip connection warm-up if disabled in config or by user
199
+ if not skip_warmup and not self.config.get('skip_connection_warmup', False):
200
+ try:
201
+ self._ensure_connection(url, headers)
202
+ except Exception as e:
203
+ logger.warning('Connection Warm-up Warning: %s (continuing anyway)', e)
204
+ # Don't return error, continue with the actual request
170
205
 
171
206
  try:
172
207
  req_args = {'headers': headers, 'timeout': 30}
@@ -233,8 +268,8 @@ class GenericProvider(BaseProvider):
233
268
  if isinstance(item, dict):
234
269
  for key, value in item.items():
235
270
  # 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)):
271
+ # Include all unmapped fields, including nested objects and arrays
272
+ if key not in mapped_paths and value is not None:
238
273
  snippet_fields[key] = value
239
274
 
240
275
  # Store snippet fields as JSON structure in snippet
@@ -242,7 +277,7 @@ class GenericProvider(BaseProvider):
242
277
  entry['snippet'] = snippet_fields
243
278
  else:
244
279
  entry['snippet'] = ''
245
-
280
+ logger.debug('snippet_fields: %s', snippet_fields)
246
281
  normalized_results.append(entry)
247
282
 
248
283
  # Post-process: extract domain from URL if site_name is empty
@@ -12,8 +12,8 @@ querit:
12
12
  root_path: "results.result"
13
13
  server_latency_path: "took"
14
14
  fields:
15
- title: "title"
16
15
  url: "url"
16
+ title: "title"
17
17
  site_name: "site_name"
18
18
  site_icon: "site_icon"
19
19
  page_age: "page_age"
@@ -31,11 +31,93 @@ you:
31
31
  root_path: "results.web"
32
32
  server_latency_path: "metadata.latency"
33
33
  fields:
34
- title: "title"
35
34
  url: "url"
35
+ title: "title"
36
36
  site_icon: "favicon_url"
37
37
  page_age: "page_age"
38
38
 
39
+ tavily:
40
+ url: "https://api.tavily.com/search"
41
+ method: "POST"
42
+ headers:
43
+ "Accept": "application/json"
44
+ "Content-Type": "application/json"
45
+ payload:
46
+ api_key: "{api_key}"
47
+ query: "{query}"
48
+ max_results: "{limit}"
49
+ search_depth: "basic"
50
+ include_favicon: "true"
51
+ response_mapping:
52
+ root_path: "results"
53
+ server_latency_path: "response_time"
54
+ fields:
55
+ url: "url"
56
+ title: "title"
57
+ page_age: "published_date"
58
+ site_icon: "favicon"
59
+
60
+ exa:
61
+ url: "https://api.exa.ai/search"
62
+ method: "POST"
63
+ headers:
64
+ "Content-Type": "application/json"
65
+ "x-api-key": "{api_key}"
66
+ params: {}
67
+ payload:
68
+ query: "{query}"
69
+ numResults: "{limit}"
70
+ type: "auto"
71
+ response_mapping:
72
+ root_path: "results"
73
+ fields:
74
+ url: "url"
75
+ title: "title"
76
+ site_icon: "favicon"
77
+ page_age: "publishedDate"
78
+
79
+ parallel:
80
+ url: "https://api.parallel.ai/v1beta/search"
81
+ method: "POST"
82
+ headers:
83
+ "Content-Type": "application/json"
84
+ "x-api-key": "{api_key}"
85
+ "parallel-beta": "search-extract-2025-10-10"
86
+ payload:
87
+ mode: "one-shot"
88
+ max_results: "{limit}"
89
+ objective: "{query}"
90
+ response_mapping:
91
+ root_path: "results"
92
+ fields:
93
+ url: "url"
94
+ title: "title"
95
+ page_age: "publish_date"
96
+
97
+ baidu:
98
+ url: "https://qianfan.baidubce.com/v2/ai_search/web_search"
99
+ method: "POST"
100
+ headers:
101
+ "Accept": "application/json"
102
+ "Authorization": "Bearer {api_key}"
103
+ "Content-Type": "application/json"
104
+ payload:
105
+ messages:
106
+ - content: "{query}"
107
+ role: "user"
108
+ search_source: "baidu_search_v2"
109
+ resource_type_filter:
110
+ - type: "web"
111
+ top_k: "{limit}"
112
+ response_mapping:
113
+ root_path: "references"
114
+ fields:
115
+ url: "url"
116
+ title: "title"
117
+ site_name: "website"
118
+ site_icon: "icon"
119
+ page_age: "date"
120
+
39
121
  brave:
40
122
  url: "https://api.search.brave.com/res/v1/web/search"
41
123
  method: "GET"
@@ -49,27 +131,26 @@ brave:
49
131
  response_mapping:
50
132
  root_path: "web.results"
51
133
  fields:
52
- title: "title"
53
134
  url: "url"
135
+ title: "title"
54
136
  page_age: "page_age"
55
137
 
56
- exa:
57
- url: "https://api.exa.ai/search"
138
+ serper:
139
+ url: "https://google.serper.dev/search"
58
140
  method: "POST"
59
141
  headers:
142
+ "X-API-KEY": "{api_key}"
60
143
  "Content-Type": "application/json"
61
- "x-api-key": "{api_key}"
62
144
  params: {}
63
145
  payload:
64
- query: "{query}"
65
- numResults: "{limit}"
146
+ q: "{query}"
147
+ num: "{limit}"
66
148
  response_mapping:
67
- root_path: "results"
149
+ root_path: "organic"
68
150
  fields:
151
+ url: "link"
69
152
  title: "title"
70
- url: "url"
71
- page_age: "publishedDate"
72
- site_icon: "favicon"
153
+ snippet: "snippet"
73
154
 
74
155
  querit_sdk:
75
156
  type: "querit_sdk"
Binary file
@@ -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}.ml-4{margin-left:1rem}.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}.block{display:block}.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-20{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-auto{width:auto}.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}.flex-wrap{flex-wrap:wrap}.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{border-radius:.25rem}.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-l-2{border-left-width:2px}.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}.object-cover{-o-object-fit:cover;object-fit:cover}.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}.pl-3{padding-left:.75rem}.pl-6{padding-left:1.5rem}.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\:border-blue-400:hover{--tw-border-opacity: 1;border-color:rgb(96 165 250 / var(--tw-border-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-500:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity, 1))}.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}}