From 9976fef5a30cde150b8978ab54f0304aab715f82 Mon Sep 17 00:00:00 2001 From: Per Stark Date: Wed, 1 Jan 2025 23:26:41 +0100 Subject: [PATCH] feat: refactored error handling --- assets/style.css | 2 +- src/error.rs | 300 +++++++++++++----- src/ingress/analysis/ingress_analyser.rs | 28 +- .../analysis/types/llm_analysis_result.rs | 23 +- src/ingress/content_processor.rs | 12 +- src/ingress/types/ingress_input.rs | 42 +-- src/ingress/types/ingress_object.rs | 25 +- src/rabbitmq/mod.rs | 4 - src/retrieval/mod.rs | 6 +- src/retrieval/query_helper.rs | 32 +- src/retrieval/vector.rs | 4 +- src/server/middleware_api_auth.rs | 8 +- src/server/routes/api/file.rs | 10 +- src/server/routes/api/ingress.rs | 4 +- src/server/routes/api/queue_length.rs | 17 +- src/server/routes/html/account.rs | 40 ++- src/server/routes/html/index.rs | 19 +- src/server/routes/html/ingress.rs | 25 +- src/server/routes/html/search_result.rs | 30 +- src/server/routes/html/signin.rs | 23 +- src/server/routes/html/signup.rs | 22 +- src/storage/types/knowledge_relationship.rs | 7 +- src/storage/types/user.rs | 26 +- src/utils/embedding.rs | 9 +- templates/errors/error.html | 14 + 25 files changed, 439 insertions(+), 293 deletions(-) create mode 100644 templates/errors/error.html diff --git a/assets/style.css b/assets/style.css index 226493a..8dd90f4 100644 --- a/assets/style.css +++ b/assets/style.css @@ -1 +1 @@ -*,:after,:before{--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:rgba(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:rgba(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: }/*! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com*/*,:after,:before{border:0 solid #e5e7eb;box-sizing:border-box}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;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;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-tap-highlight-color:transparent}body{line-height:inherit;margin:0}hr{border-top-width:1px;color:inherit;height:0}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,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-size:1em;font-variation-settings:normal}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}button,input,optgroup,select,textarea{color:inherit;font-family:inherit;font-feature-settings:inherit;font-size:100%;font-variation-settings:inherit;font-weight:inherit;letter-spacing:inherit;line-height: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,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{color:#9ca3af;opacity:1}input::placeholder,textarea::placeholder{color:#9ca3af;opacity:1}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}[hidden]:where(:not([hidden=until-found])){display:none}:root,[data-theme]{background-color:var(--fallback-b1,oklch(var(--b1)/1));color:var(--fallback-bc,oklch(var(--bc)/1))}@supports not (color:oklch(0% 0 0)){:root{color-scheme:light;--fallback-p:#491eff;--fallback-pc:#d4dbff;--fallback-s:#ff41c7;--fallback-sc:#fff9fc;--fallback-a:#00cfbd;--fallback-ac:#00100d;--fallback-n:#2b3440;--fallback-nc:#d7dde4;--fallback-b1:#fff;--fallback-b2:#e5e6e6;--fallback-b3:#e5e6e6;--fallback-bc:#1f2937;--fallback-in:#00b3f0;--fallback-inc:#000;--fallback-su:#00ca92;--fallback-suc:#000;--fallback-wa:#ffc22d;--fallback-wac:#000;--fallback-er:#ff6f70;--fallback-erc:#000}@media (prefers-color-scheme:dark){:root{color-scheme:dark;--fallback-p:#7582ff;--fallback-pc:#050617;--fallback-s:#ff71cf;--fallback-sc:#190211;--fallback-a:#00c7b5;--fallback-ac:#000e0c;--fallback-n:#2a323c;--fallback-nc:#a6adbb;--fallback-b1:#1d232a;--fallback-b2:#191e24;--fallback-b3:#15191e;--fallback-bc:#a6adbb;--fallback-in:#00b3f0;--fallback-inc:#000;--fallback-su:#00ca92;--fallback-suc:#000;--fallback-wa:#ffc22d;--fallback-wac:#000;--fallback-er:#ff6f70;--fallback-erc:#000}}}html{-webkit-tap-highlight-color:transparent}*{scrollbar-color:color-mix(in oklch,currentColor 35%,transparent) transparent}:hover{scrollbar-color:color-mix(in oklch,currentColor 60%,transparent) transparent}:root{color-scheme:light;--in:72.06% 0.191 231.6;--su:64.8% 0.150 160;--wa:84.71% 0.199 83.87;--er:71.76% 0.221 22.18;--pc:89.824% 0.06192 275.75;--ac:15.352% 0.0368 183.61;--inc:0% 0 0;--suc:0% 0 0;--wac:0% 0 0;--erc:0% 0 0;--rounded-box:1rem;--rounded-btn:0.5rem;--rounded-badge:1.9rem;--animation-btn:0.25s;--animation-input:.2s;--btn-focus-scale:0.95;--border-btn:1px;--tab-border:1px;--tab-radius:0.5rem;--p:49.12% 0.3096 275.75;--s:69.71% 0.329 342.55;--sc:98.71% 0.0106 342.55;--a:76.76% 0.184 183.61;--n:32.1785% 0.02476 255.701624;--nc:89.4994% 0.011585 252.096176;--b1:100% 0 0;--b2:96.1151% 0 0;--b3:92.4169% 0.00108 197.137559;--bc:27.8078% 0.029596 256.847952}@media (prefers-color-scheme:dark){:root{color-scheme:dark;--in:72.06% 0.191 231.6;--su:64.8% 0.150 160;--wa:84.71% 0.199 83.87;--er:71.76% 0.221 22.18;--pc:13.138% 0.0392 275.75;--sc:14.96% 0.052 342.55;--ac:14.902% 0.0334 183.61;--inc:0% 0 0;--suc:0% 0 0;--wac:0% 0 0;--erc:0% 0 0;--rounded-box:1rem;--rounded-btn:0.5rem;--rounded-badge:1.9rem;--animation-btn:0.25s;--animation-input:.2s;--btn-focus-scale:0.95;--border-btn:1px;--tab-border:1px;--tab-radius:0.5rem;--p:65.69% 0.196 275.75;--s:74.8% 0.26 342.55;--a:74.51% 0.167 183.61;--n:31.3815% 0.021108 254.139175;--nc:74.6477% 0.0216 264.435964;--b1:25.3267% 0.015896 252.417568;--b2:23.2607% 0.013807 253.100675;--b3:21.1484% 0.01165 254.087939;--bc:74.6477% 0.0216 264.435964}}[data-theme=light]{color-scheme:light;--in:72.06% 0.191 231.6;--su:64.8% 0.150 160;--wa:84.71% 0.199 83.87;--er:71.76% 0.221 22.18;--pc:89.824% 0.06192 275.75;--ac:15.352% 0.0368 183.61;--inc:0% 0 0;--suc:0% 0 0;--wac:0% 0 0;--erc:0% 0 0;--rounded-box:1rem;--rounded-btn:0.5rem;--rounded-badge:1.9rem;--animation-btn:0.25s;--animation-input:.2s;--btn-focus-scale:0.95;--border-btn:1px;--tab-border:1px;--tab-radius:0.5rem;--p:49.12% 0.3096 275.75;--s:69.71% 0.329 342.55;--sc:98.71% 0.0106 342.55;--a:76.76% 0.184 183.61;--n:32.1785% 0.02476 255.701624;--nc:89.4994% 0.011585 252.096176;--b1:100% 0 0;--b2:96.1151% 0 0;--b3:92.4169% 0.00108 197.137559;--bc:27.8078% 0.029596 256.847952}[data-theme=dark]{color-scheme:dark;--in:72.06% 0.191 231.6;--su:64.8% 0.150 160;--wa:84.71% 0.199 83.87;--er:71.76% 0.221 22.18;--pc:13.138% 0.0392 275.75;--sc:14.96% 0.052 342.55;--ac:14.902% 0.0334 183.61;--inc:0% 0 0;--suc:0% 0 0;--wac:0% 0 0;--erc:0% 0 0;--rounded-box:1rem;--rounded-btn:0.5rem;--rounded-badge:1.9rem;--animation-btn:0.25s;--animation-input:.2s;--btn-focus-scale:0.95;--border-btn:1px;--tab-border:1px;--tab-radius:0.5rem;--p:65.69% 0.196 275.75;--s:74.8% 0.26 342.55;--a:74.51% 0.167 183.61;--n:31.3815% 0.021108 254.139175;--nc:74.6477% 0.0216 264.435964;--b1:25.3267% 0.015896 252.417568;--b2:23.2607% 0.013807 253.100675;--b3:21.1484% 0.01165 254.087939;--bc:74.6477% 0.0216 264.435964}.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.avatar.placeholder>div{align-items:center;display:flex;justify-content:center}@media (hover:hover){.checkbox-primary:hover{--tw-border-opacity:1;border-color:var(--fallback-p,oklch(var(--p)/var(--tw-border-opacity)))}.label a:hover{--tw-text-opacity:1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)))}.menu li>:not(ul,.menu-title,details,.btn).active,.menu li>:not(ul,.menu-title,details,.btn):active,.menu li>details>summary:active{--tw-bg-opacity:1;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));--tw-text-opacity:1;color:var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity)))}}.btn{align-items:center;border-color:transparent;border-color:oklch(var(--btn-color,var(--b2))/var(--tw-border-opacity));border-radius:var(--rounded-btn,.5rem);border-width:var(--border-btn,1px);cursor:pointer;display:inline-flex;flex-shrink:0;flex-wrap:wrap;font-size:.875rem;font-weight:600;gap:.5rem;height:3rem;justify-content:center;line-height:1em;min-height:3rem;padding-left:1rem;padding-right:1rem;text-align:center;text-decoration-line:none;transition-duration:.2s;transition-property:color,background-color,border-color,opacity,box-shadow,transform;transition-timing-function:cubic-bezier(0,0,.2,1);-webkit-user-select:none;-moz-user-select:none;user-select:none;--tw-text-opacity:1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));--tw-shadow:0 1px 2px 0 rgba(0,0,0,.05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color);background-color:oklch(var(--btn-color,var(--b2))/var(--tw-bg-opacity));box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);outline-color:var(--fallback-bc,oklch(var(--bc)/1));--tw-bg-opacity:1;--tw-border-opacity:1}.btn-disabled,.btn:disabled,.btn[disabled]{pointer-events:none}:where(.btn:is(input[type=checkbox])),:where(.btn:is(input[type=radio])){-webkit-appearance:none;-moz-appearance:none;appearance:none;width:auto}.btn:is(input[type=checkbox]):after,.btn:is(input[type=radio]):after{--tw-content:attr(aria-label);content:var(--tw-content)}.card{border-radius:var(--rounded-box,1rem);display:flex;flex-direction:column;position:relative}.card:focus{outline:2px solid transparent;outline-offset:2px}.card-body{display:flex;flex:1 1 auto;flex-direction:column;gap:.5rem;padding:var(--padding-card,2rem)}.card-body :where(p){flex-grow:1}.card figure{align-items:center;display:flex;justify-content:center}.card.image-full{display:grid}.card.image-full:before{border-radius:var(--rounded-box,1rem);content:"";position:relative;z-index:10;--tw-bg-opacity:1;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));opacity:.75}.card.image-full:before,.card.image-full>*{grid-column-start:1;grid-row-start:1}.card.image-full>figure img{height:100%;-o-object-fit:cover;object-fit:cover}.card.image-full>.card-body{position:relative;z-index:20;--tw-text-opacity:1;color:var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity)))}.chat{-moz-column-gap:.75rem;column-gap:.75rem;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));padding-bottom:.25rem;padding-top:.25rem}.chat-bubble{border-radius:var(--rounded-box,1rem);display:block;max-width:90%;min-height:2.75rem;min-width:2.75rem;padding:.5rem 1rem;position:relative;width:-moz-fit-content;width:fit-content;--tw-bg-opacity:1;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));--tw-text-opacity:1;color:var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity)))}.chat-bubble:before{background-color:inherit;bottom:0;content:"";height:.75rem;-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;position:absolute;width:.75rem}.chat-start{grid-template-columns:auto 1fr;place-items:start}.chat-start .chat-footer,.chat-start .chat-header{grid-column-start:2}.chat-start .chat-image{grid-column-start:1}.chat-start .chat-bubble{border-end-start-radius:0;grid-column-start:2}.chat-start .chat-bubble:before{inset-inline-start:-.749rem;-webkit-mask-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='3' height='3'%3E%3Cpath d='M0 3h3V0c0 1-2 3-3 3'/%3E%3C/svg%3E");mask-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='3' height='3'%3E%3Cpath d='M0 3h3V0c0 1-2 3-3 3'/%3E%3C/svg%3E")}[dir=rtl] .chat-start .chat-bubble:before{-webkit-mask-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='3' height='3'%3E%3Cpath d='M0 3h3C2 3 0 1 0 0'/%3E%3C/svg%3E");mask-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='3' height='3'%3E%3Cpath d='M0 3h3C2 3 0 1 0 0'/%3E%3C/svg%3E")}.chat-end .chat-bubble{border-end-end-radius:0;grid-column-start:1}.chat-end .chat-bubble:before{inset-inline-start:99.9%;-webkit-mask-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='3' height='3'%3E%3Cpath d='M0 3h3C2 3 0 1 0 0'/%3E%3C/svg%3E");mask-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='3' height='3'%3E%3Cpath d='M0 3h3C2 3 0 1 0 0'/%3E%3C/svg%3E")}[dir=rtl] .chat-end .chat-bubble:before{-webkit-mask-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='3' height='3'%3E%3Cpath d='M0 3h3V0c0 1-2 3-3 3'/%3E%3C/svg%3E");mask-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='3' height='3'%3E%3Cpath d='M0 3h3V0c0 1-2 3-3 3'/%3E%3C/svg%3E")}.checkbox{flex-shrink:0;--chkbg:var(--fallback-bc,oklch(var(--bc)/1));--chkfg:var(--fallback-b1,oklch(var(--b1)/1));-webkit-appearance:none;-moz-appearance:none;appearance:none;border-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-border-opacity)));border-radius:var(--rounded-btn,.5rem);border-width:1px;cursor:pointer;height:1.5rem;width:1.5rem;--tw-border-opacity:0.2}.divider{align-items:center;align-self:stretch;display:flex;flex-direction:row;height:1rem;margin-bottom:1rem;margin-top:1rem;white-space:nowrap}.divider:after,.divider:before{flex-grow:1;height:.125rem;width:100%;--tw-content:"";background-color:var(--fallback-bc,oklch(var(--bc)/.1));content:var(--tw-content)}@media (hover:hover){.btm-nav>.disabled:hover,.btm-nav>[disabled]:hover{pointer-events:none;--tw-border-opacity:0;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));--tw-bg-opacity:0.1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));--tw-text-opacity:0.2}.btn:hover{--tw-border-opacity:1;border-color:var(--fallback-b3,oklch(var(--b3)/var(--tw-border-opacity)));--tw-bg-opacity:1;background-color:var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity)))}@supports (color:color-mix(in oklab,black,black)){.btn:hover{background-color:color-mix(in oklab,oklch(var(--btn-color,var(--b2))/var(--tw-bg-opacity,1)) 90%,#000);border-color:color-mix(in oklab,oklch(var(--btn-color,var(--b2))/var(--tw-border-opacity,1)) 90%,#000)}}@supports not (color:oklch(0% 0 0)){.btn:hover{background-color:var(--btn-color,var(--fallback-b2));border-color:var(--btn-color,var(--fallback-b2))}}.btn.glass:hover{--glass-opacity:25%;--glass-border-opacity:15%}.btn-outline:hover{--tw-border-opacity:1;border-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-border-opacity)));--tw-bg-opacity:1;background-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)));--tw-text-opacity:1;color:var(--fallback-b1,oklch(var(--b1)/var(--tw-text-opacity)))}.btn-outline.btn-primary:hover{--tw-text-opacity:1;color:var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity)))}@supports (color:color-mix(in oklab,black,black)){.btn-outline.btn-primary:hover{background-color:color-mix(in oklab,var(--fallback-p,oklch(var(--p)/1)) 90%,#000);border-color:color-mix(in oklab,var(--fallback-p,oklch(var(--p)/1)) 90%,#000)}}.btn-outline.btn-secondary:hover{--tw-text-opacity:1;color:var(--fallback-sc,oklch(var(--sc)/var(--tw-text-opacity)))}@supports (color:color-mix(in oklab,black,black)){.btn-outline.btn-secondary:hover{background-color:color-mix(in oklab,var(--fallback-s,oklch(var(--s)/1)) 90%,#000);border-color:color-mix(in oklab,var(--fallback-s,oklch(var(--s)/1)) 90%,#000)}}.btn-outline.btn-accent:hover{--tw-text-opacity:1;color:var(--fallback-ac,oklch(var(--ac)/var(--tw-text-opacity)))}@supports (color:color-mix(in oklab,black,black)){.btn-outline.btn-accent:hover{background-color:color-mix(in oklab,var(--fallback-a,oklch(var(--a)/1)) 90%,#000);border-color:color-mix(in oklab,var(--fallback-a,oklch(var(--a)/1)) 90%,#000)}}.btn-outline.btn-success:hover{--tw-text-opacity:1;color:var(--fallback-suc,oklch(var(--suc)/var(--tw-text-opacity)))}@supports (color:color-mix(in oklab,black,black)){.btn-outline.btn-success:hover{background-color:color-mix(in oklab,var(--fallback-su,oklch(var(--su)/1)) 90%,#000);border-color:color-mix(in oklab,var(--fallback-su,oklch(var(--su)/1)) 90%,#000)}}.btn-outline.btn-info:hover{--tw-text-opacity:1;color:var(--fallback-inc,oklch(var(--inc)/var(--tw-text-opacity)))}@supports (color:color-mix(in oklab,black,black)){.btn-outline.btn-info:hover{background-color:color-mix(in oklab,var(--fallback-in,oklch(var(--in)/1)) 90%,#000);border-color:color-mix(in oklab,var(--fallback-in,oklch(var(--in)/1)) 90%,#000)}}.btn-outline.btn-warning:hover{--tw-text-opacity:1;color:var(--fallback-wac,oklch(var(--wac)/var(--tw-text-opacity)))}@supports (color:color-mix(in oklab,black,black)){.btn-outline.btn-warning:hover{background-color:color-mix(in oklab,var(--fallback-wa,oklch(var(--wa)/1)) 90%,#000);border-color:color-mix(in oklab,var(--fallback-wa,oklch(var(--wa)/1)) 90%,#000)}}.btn-outline.btn-error:hover{--tw-text-opacity:1;color:var(--fallback-erc,oklch(var(--erc)/var(--tw-text-opacity)))}@supports (color:color-mix(in oklab,black,black)){.btn-outline.btn-error:hover{background-color:color-mix(in oklab,var(--fallback-er,oklch(var(--er)/1)) 90%,#000);border-color:color-mix(in oklab,var(--fallback-er,oklch(var(--er)/1)) 90%,#000)}}.btn-disabled:hover,.btn:disabled:hover,.btn[disabled]:hover{--tw-border-opacity:0;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));--tw-bg-opacity:0.2;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));--tw-text-opacity:0.2}@supports (color:color-mix(in oklab,black,black)){.btn:is(input[type=checkbox]:checked):hover,.btn:is(input[type=radio]:checked):hover{background-color:color-mix(in oklab,var(--fallback-p,oklch(var(--p)/1)) 90%,#000);border-color:color-mix(in oklab,var(--fallback-p,oklch(var(--p)/1)) 90%,#000)}}:where(.menu li:not(.menu-title,.disabled)>:not(ul,details,.menu-title)):not(.active,.btn):hover,:where(.menu li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.active,.btn):hover{cursor:pointer;outline:2px solid transparent;outline-offset:2px}@supports (color:oklch(0% 0 0)){:where(.menu li:not(.menu-title,.disabled)>:not(ul,details,.menu-title)):not(.active,.btn):hover,:where(.menu li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.active,.btn):hover{background-color:var(--fallback-bc,oklch(var(--bc)/.1))}}}.file-input{border-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-border-opacity)));border-radius:var(--rounded-btn,.5rem);border-width:1px;flex-shrink:1;font-size:1rem;height:3rem;line-height:2;line-height:1.5rem;overflow:hidden;padding-inline-end:1rem;--tw-border-opacity:0;--tw-bg-opacity:1;background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)))}.file-input::file-selector-button{align-items:center;border-style:solid;cursor:pointer;display:inline-flex;flex-shrink:0;flex-wrap:wrap;font-size:.875rem;height:100%;justify-content:center;line-height:1.25rem;line-height:1em;margin-inline-end:1rem;padding-left:1rem;padding-right:1rem;text-align:center;transition-duration:.2s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-timing-function:cubic-bezier(0,0,.2,1);-webkit-user-select:none;-moz-user-select:none;user-select:none;--tw-border-opacity:1;border-color:var(--fallback-n,oklch(var(--n)/var(--tw-border-opacity)));--tw-bg-opacity:1;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));font-weight:600;text-transform:uppercase;--tw-text-opacity:1;animation:button-pop var(--animation-btn,.25s) ease-out;border-width:var(--border-btn,1px);color:var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity)));text-decoration-line:none}.form-control{flex-direction:column}.form-control,.label{display:flex}.label{align-items:center;justify-content:space-between;padding:.5rem .25rem;-webkit-user-select:none;-moz-user-select:none;user-select:none}.hero{background-position:50%;background-size:cover;display:grid;place-items:center;width:100%}.hero>*{grid-column-start:1;grid-row-start:1}.hero-content{align-items:center;display:flex;gap:1rem;justify-content:center;max-width:80rem;padding:1rem;z-index:0}.input{-webkit-appearance:none;-moz-appearance:none;appearance:none;border-color:transparent;border-radius:var(--rounded-btn,.5rem);border-width:1px;flex-shrink:1;font-size:1rem;height:3rem;line-height:2;line-height:1.5rem;padding-left:1rem;padding-right:1rem;--tw-bg-opacity:1;background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)))}.input-md[type=number]::-webkit-inner-spin-button,.input[type=number]::-webkit-inner-spin-button{margin-bottom:-1rem;margin-top:-1rem;margin-inline-end:-1rem}.link{cursor:pointer;text-decoration-line:underline}.menu{display:flex;flex-direction:column;flex-wrap:wrap;font-size:.875rem;line-height:1.25rem;padding:.5rem}.menu :where(li ul){margin-inline-start:1rem;padding-inline-start:.5rem;position:relative;white-space:nowrap}.menu :where(li:not(.menu-title)>:not(ul,details,.menu-title,.btn)),.menu :where(li:not(.menu-title)>details>summary:not(.menu-title)){align-content:flex-start;align-items:center;display:grid;gap:.5rem;grid-auto-columns:minmax(auto,max-content) auto max-content;grid-auto-flow:column;-webkit-user-select:none;-moz-user-select:none;user-select:none}.menu li.disabled{color:var(--fallback-bc,oklch(var(--bc)/.3));cursor:not-allowed;-webkit-user-select:none;-moz-user-select:none;user-select:none}.menu :where(li>.menu-dropdown:not(.menu-dropdown-show)){display:none}:where(.menu li){align-items:stretch;display:flex;flex-direction:column;flex-shrink:0;flex-wrap:wrap;position:relative}:where(.menu li) .badge{justify-self:end}.navbar{align-items:center;display:flex;min-height:4rem;padding:var(--navbar-padding,.5rem);width:100%}:where(.navbar>:not(script,style)){align-items:center;display:inline-flex}.textarea{border-color:transparent;border-radius:var(--rounded-btn,.5rem);border-width:1px;flex-shrink:1;font-size:.875rem;line-height:1.25rem;line-height:2;min-height:3rem;padding:.5rem 1rem;--tw-bg-opacity:1;background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)))}.btm-nav>.disabled,.btm-nav>[disabled]{pointer-events:none;--tw-border-opacity:0;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));--tw-bg-opacity:0.1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));--tw-text-opacity:0.2}.btm-nav>* .label{font-size:1rem;line-height:1.5rem}@media (prefers-reduced-motion:no-preference){.btn{animation:button-pop var(--animation-btn,.25s) ease-out}}.btn:active:focus,.btn:active:hover{animation:button-pop 0s ease-out;transform:scale(var(--btn-focus-scale,.97))}@supports not (color:oklch(0% 0 0)){.btn{background-color:var(--btn-color,var(--fallback-b2));border-color:var(--btn-color,var(--fallback-b2))}.btn-primary{--btn-color:var(--fallback-p)}.btn-secondary{--btn-color:var(--fallback-s)}.btn-error{--btn-color:var(--fallback-er)}}@supports (color:color-mix(in oklab,black,black)){.btn-outline.btn-primary.btn-active{background-color:color-mix(in oklab,var(--fallback-p,oklch(var(--p)/1)) 90%,#000);border-color:color-mix(in oklab,var(--fallback-p,oklch(var(--p)/1)) 90%,#000)}.btn-outline.btn-secondary.btn-active{background-color:color-mix(in oklab,var(--fallback-s,oklch(var(--s)/1)) 90%,#000);border-color:color-mix(in oklab,var(--fallback-s,oklch(var(--s)/1)) 90%,#000)}.btn-outline.btn-accent.btn-active{background-color:color-mix(in oklab,var(--fallback-a,oklch(var(--a)/1)) 90%,#000);border-color:color-mix(in oklab,var(--fallback-a,oklch(var(--a)/1)) 90%,#000)}.btn-outline.btn-success.btn-active{background-color:color-mix(in oklab,var(--fallback-su,oklch(var(--su)/1)) 90%,#000);border-color:color-mix(in oklab,var(--fallback-su,oklch(var(--su)/1)) 90%,#000)}.btn-outline.btn-info.btn-active{background-color:color-mix(in oklab,var(--fallback-in,oklch(var(--in)/1)) 90%,#000);border-color:color-mix(in oklab,var(--fallback-in,oklch(var(--in)/1)) 90%,#000)}.btn-outline.btn-warning.btn-active{background-color:color-mix(in oklab,var(--fallback-wa,oklch(var(--wa)/1)) 90%,#000);border-color:color-mix(in oklab,var(--fallback-wa,oklch(var(--wa)/1)) 90%,#000)}.btn-outline.btn-error.btn-active{background-color:color-mix(in oklab,var(--fallback-er,oklch(var(--er)/1)) 90%,#000);border-color:color-mix(in oklab,var(--fallback-er,oklch(var(--er)/1)) 90%,#000)}}.btn:focus-visible{outline-offset:2px;outline-style:solid;outline-width:2px}.btn-primary{--tw-text-opacity:1;color:var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity)));outline-color:var(--fallback-p,oklch(var(--p)/1))}@supports (color:oklch(0% 0 0)){.btn-primary{--btn-color:var(--p)}.btn-secondary{--btn-color:var(--s)}.btn-error{--btn-color:var(--er)}}.btn-secondary{--tw-text-opacity:1;color:var(--fallback-sc,oklch(var(--sc)/var(--tw-text-opacity)));outline-color:var(--fallback-s,oklch(var(--s)/1))}.btn-error{--tw-text-opacity:1;color:var(--fallback-erc,oklch(var(--erc)/var(--tw-text-opacity)));outline-color:var(--fallback-er,oklch(var(--er)/1))}.btn.glass{--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);outline-color:currentColor}.btn.glass.btn-active{--glass-opacity:25%;--glass-border-opacity:15%}.btn-outline{background-color:transparent;border-color:currentColor;--tw-text-opacity:1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.btn-outline.btn-active{--tw-border-opacity:1;border-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-border-opacity)));--tw-bg-opacity:1;background-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)));--tw-text-opacity:1;color:var(--fallback-b1,oklch(var(--b1)/var(--tw-text-opacity)))}.btn-outline.btn-primary{--tw-text-opacity:1;color:var(--fallback-p,oklch(var(--p)/var(--tw-text-opacity)))}.btn-outline.btn-primary.btn-active{--tw-text-opacity:1;color:var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity)))}.btn-outline.btn-secondary{--tw-text-opacity:1;color:var(--fallback-s,oklch(var(--s)/var(--tw-text-opacity)))}.btn-outline.btn-secondary.btn-active{--tw-text-opacity:1;color:var(--fallback-sc,oklch(var(--sc)/var(--tw-text-opacity)))}.btn-outline.btn-accent{--tw-text-opacity:1;color:var(--fallback-a,oklch(var(--a)/var(--tw-text-opacity)))}.btn-outline.btn-accent.btn-active{--tw-text-opacity:1;color:var(--fallback-ac,oklch(var(--ac)/var(--tw-text-opacity)))}.btn-outline.btn-success{--tw-text-opacity:1;color:var(--fallback-su,oklch(var(--su)/var(--tw-text-opacity)))}.btn-outline.btn-success.btn-active{--tw-text-opacity:1;color:var(--fallback-suc,oklch(var(--suc)/var(--tw-text-opacity)))}.btn-outline.btn-info{--tw-text-opacity:1;color:var(--fallback-in,oklch(var(--in)/var(--tw-text-opacity)))}.btn-outline.btn-info.btn-active{--tw-text-opacity:1;color:var(--fallback-inc,oklch(var(--inc)/var(--tw-text-opacity)))}.btn-outline.btn-warning{--tw-text-opacity:1;color:var(--fallback-wa,oklch(var(--wa)/var(--tw-text-opacity)))}.btn-outline.btn-warning.btn-active{--tw-text-opacity:1;color:var(--fallback-wac,oklch(var(--wac)/var(--tw-text-opacity)))}.btn-outline.btn-error{--tw-text-opacity:1;color:var(--fallback-er,oklch(var(--er)/var(--tw-text-opacity)))}.btn-outline.btn-error.btn-active{--tw-text-opacity:1;color:var(--fallback-erc,oklch(var(--erc)/var(--tw-text-opacity)))}.btn.btn-disabled,.btn:disabled,.btn[disabled]{--tw-border-opacity:0;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));--tw-bg-opacity:0.2;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));--tw-text-opacity:0.2}.btn:is(input[type=checkbox]:checked),.btn:is(input[type=radio]:checked){--tw-border-opacity:1;border-color:var(--fallback-p,oklch(var(--p)/var(--tw-border-opacity)));--tw-bg-opacity:1;background-color:var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity)));--tw-text-opacity:1;color:var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity)))}.btn:is(input[type=checkbox]:checked):focus-visible,.btn:is(input[type=radio]:checked):focus-visible{outline-color:var(--fallback-p,oklch(var(--p)/1))}@keyframes button-pop{0%{transform:scale(var(--btn-focus-scale,.98))}40%{transform:scale(1.02)}to{transform:scale(1)}}.card :where(figure:first-child){border-end-end-radius:unset;border-end-start-radius:unset;border-start-end-radius:inherit;border-start-start-radius:inherit;overflow:hidden}.card :where(figure:last-child){border-end-end-radius:inherit;border-end-start-radius:inherit;border-start-end-radius:unset;border-start-start-radius:unset;overflow:hidden}.card:focus-visible{outline:2px solid currentColor;outline-offset:2px}.card.bordered{border-width:1px;--tw-border-opacity:1;border-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)))}.card.compact .card-body{font-size:.875rem;line-height:1.25rem;padding:1rem}.card-title{align-items:center;display:flex;font-size:1.25rem;font-weight:600;gap:.5rem;line-height:1.75rem}.card.image-full :where(figure){border-radius:inherit;overflow:hidden}.checkbox:focus{box-shadow:none}.checkbox:focus-visible{outline-color:var(--fallback-bc,oklch(var(--bc)/1));outline-offset:2px;outline-style:solid;outline-width:2px}.checkbox:disabled{border-color:transparent;border-width:0;cursor:not-allowed;--tw-bg-opacity:1;background-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)));opacity:.2}.checkbox:checked,.checkbox[aria-checked=true]{animation:checkmark var(--animation-input,.2s) ease-out;background-color:var(--chkbg);background-image:linear-gradient(-45deg,transparent 65%,var(--chkbg) 65.99%),linear-gradient(45deg,transparent 75%,var(--chkbg) 75.99%),linear-gradient(-45deg,var(--chkbg) 40%,transparent 40.99%),linear-gradient(45deg,var(--chkbg) 30%,var(--chkfg) 30.99%,var(--chkfg) 40%,transparent 40.99%),linear-gradient(-45deg,var(--chkfg) 50%,var(--chkbg) 50.99%);background-repeat:no-repeat}.checkbox:indeterminate{--tw-bg-opacity:1;animation:checkmark var(--animation-input,.2s) ease-out;background-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)));background-image:linear-gradient(90deg,transparent 80%,var(--chkbg) 80%),linear-gradient(-90deg,transparent 80%,var(--chkbg) 80%),linear-gradient(0deg,var(--chkbg) 43%,var(--chkfg) 43%,var(--chkfg) 57%,var(--chkbg) 57%);background-repeat:no-repeat}.checkbox-primary{--chkbg:var(--fallback-p,oklch(var(--p)/1));--chkfg:var(--fallback-pc,oklch(var(--pc)/1));--tw-border-opacity:1;border-color:var(--fallback-p,oklch(var(--p)/var(--tw-border-opacity)))}.checkbox-primary:focus-visible{outline-color:var(--fallback-p,oklch(var(--p)/1))}.checkbox-primary:checked,.checkbox-primary[aria-checked=true]{--tw-border-opacity:1;border-color:var(--fallback-p,oklch(var(--p)/var(--tw-border-opacity)));--tw-bg-opacity:1;background-color:var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity)));--tw-text-opacity:1;color:var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity)))}@keyframes checkmark{0%{background-position-y:5px}50%{background-position-y:-2px}to{background-position-y:0}}.divider:not(:empty){gap:1rem}.file-input-bordered{--tw-border-opacity:0.2}.file-input:focus{outline-color:var(--fallback-bc,oklch(var(--bc)/.2));outline-offset:2px;outline-style:solid;outline-width:2px}.file-input-disabled,.file-input[disabled]{cursor:not-allowed;--tw-border-opacity:1;border-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)));--tw-bg-opacity:1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));--tw-text-opacity:0.2}.file-input-disabled::-moz-placeholder,.file-input[disabled]::-moz-placeholder{color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)));--tw-placeholder-opacity:0.2}.file-input-disabled::placeholder,.file-input[disabled]::placeholder{color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)));--tw-placeholder-opacity:0.2}.file-input-disabled::file-selector-button,.file-input[disabled]::file-selector-button{--tw-border-opacity:0;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));--tw-bg-opacity:0.2;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));--tw-text-opacity:0.2}.label-text{font-size:.875rem;line-height:1.25rem;--tw-text-opacity:1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)))}.input input{--tw-bg-opacity:1;background-color:var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity)));background-color:transparent}.input input:focus{outline:2px solid transparent;outline-offset:2px}.input[list]::-webkit-calendar-picker-indicator{line-height:1em}.input-bordered{border-color:var(--fallback-bc,oklch(var(--bc)/.2))}.input:focus,.input:focus-within{border-color:var(--fallback-bc,oklch(var(--bc)/.2));box-shadow:none;outline-color:var(--fallback-bc,oklch(var(--bc)/.2));outline-offset:2px;outline-style:solid;outline-width:2px}.input-disabled,.input:disabled,.input:has(>input[disabled]),.input[disabled]{cursor:not-allowed;--tw-border-opacity:1;border-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)));--tw-bg-opacity:1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));color:var(--fallback-bc,oklch(var(--bc)/.4))}.input-disabled::-moz-placeholder,.input:disabled::-moz-placeholder,.input:has(>input[disabled])::-moz-placeholder,.input[disabled]::-moz-placeholder{color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)));--tw-placeholder-opacity:0.2}.input-disabled::placeholder,.input:disabled::placeholder,.input:has(>input[disabled])::placeholder,.input[disabled]::placeholder{color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)));--tw-placeholder-opacity:0.2}.input:has(>input[disabled])>input[disabled]{cursor:not-allowed}.input::-webkit-date-and-time-value{text-align:inherit}.join>:where(:not(:first-child)):is(.btn){margin-inline-start:calc(var(--border-btn)*-1)}.link-primary{--tw-text-opacity:1;color:var(--fallback-p,oklch(var(--p)/var(--tw-text-opacity)))}@supports (color:color-mix(in oklab,black,black)){@media (hover:hover){.link-primary:hover{color:color-mix(in oklab,var(--fallback-p,oklch(var(--p)/1)) 80%,#000)}}}.link:focus{outline:2px solid transparent;outline-offset:2px}.link:focus-visible{outline:2px solid currentColor;outline-offset:2px}:where(.menu li:empty){--tw-bg-opacity:1;background-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)));height:1px;margin:.5rem 1rem;opacity:.1}.menu :where(li ul):before{bottom:.75rem;inset-inline-start:0;position:absolute;top:.75rem;width:1px;--tw-bg-opacity:1;background-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)));content:"";opacity:.1}.menu :where(li:not(.menu-title)>:not(ul,details,.menu-title,.btn)),.menu :where(li:not(.menu-title)>details>summary:not(.menu-title)){border-radius:var(--rounded-btn,.5rem);padding:.5rem 1rem;text-align:start;text-wrap:balance;transition-duration:.2s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-timing-function:cubic-bezier(0,0,.2,1)}:where(.menu li:not(.menu-title,.disabled)>:not(ul,details,.menu-title)):is(summary):not(.active,.btn):focus-visible,:where(.menu li:not(.menu-title,.disabled)>:not(ul,details,.menu-title)):not(summary,.active,.btn).focus,:where(.menu li:not(.menu-title,.disabled)>:not(ul,details,.menu-title)):not(summary,.active,.btn):focus,:where(.menu li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):is(summary):not(.active,.btn):focus-visible,:where(.menu li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(summary,.active,.btn).focus,:where(.menu li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(summary,.active,.btn):focus{background-color:var(--fallback-bc,oklch(var(--bc)/.1));cursor:pointer;--tw-text-opacity:1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));outline:2px solid transparent;outline-offset:2px}.menu li>:not(ul,.menu-title,details,.btn).active,.menu li>:not(ul,.menu-title,details,.btn):active,.menu li>details>summary:active{--tw-bg-opacity:1;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));--tw-text-opacity:1;color:var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity)))}.menu :where(li>details>summary)::-webkit-details-marker{display:none}.menu :where(li>.menu-dropdown-toggle):after,.menu :where(li>details>summary):after{box-shadow:2px 2px;content:"";display:block;height:.5rem;justify-self:end;margin-top:-.5rem;pointer-events:none;transform:rotate(45deg);transform-origin:75% 75%;transition-duration:.3s;transition-property:transform,margin-top;transition-timing-function:cubic-bezier(.4,0,.2,1);width:.5rem}.menu :where(li>.menu-dropdown-toggle.menu-dropdown-show):after,.menu :where(li>details[open]>summary):after{margin-top:0;transform:rotate(225deg)}.mockup-browser .mockup-browser-toolbar .input{display:block;height:1.75rem;margin-left:auto;margin-right:auto;overflow:hidden;position:relative;text-overflow:ellipsis;white-space:nowrap;width:24rem;--tw-bg-opacity:1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));direction:ltr;padding-left:2rem}.mockup-browser .mockup-browser-toolbar .input:before{aspect-ratio:1/1;height:.75rem;left:.5rem;--tw-translate-y:-50%;border-color:currentColor;border-radius:9999px;border-width:2px}.mockup-browser .mockup-browser-toolbar .input:after,.mockup-browser .mockup-browser-toolbar .input:before{content:"";opacity:.6;position:absolute;top:50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.mockup-browser .mockup-browser-toolbar .input:after{height:.5rem;left:1.25rem;--tw-translate-y:25%;--tw-rotate:-45deg;border-color:currentColor;border-radius:9999px;border-width:1px}@keyframes modal-pop{0%{opacity:0}}@keyframes progress-loading{50%{background-position-x:-115%}}@keyframes radiomark{0%{box-shadow:0 0 0 12px var(--fallback-b1,oklch(var(--b1)/1)) inset,0 0 0 12px var(--fallback-b1,oklch(var(--b1)/1)) inset}50%{box-shadow:0 0 0 3px var(--fallback-b1,oklch(var(--b1)/1)) inset,0 0 0 3px var(--fallback-b1,oklch(var(--b1)/1)) inset}to{box-shadow:0 0 0 4px var(--fallback-b1,oklch(var(--b1)/1)) inset,0 0 0 4px var(--fallback-b1,oklch(var(--b1)/1)) inset}}@keyframes rating-pop{0%{transform:translateY(-.125em)}40%{transform:translateY(-.125em)}to{transform:translateY(0)}}.skeleton{border-radius:var(--rounded-box,1rem);--tw-bg-opacity:1;animation:skeleton 1.8s ease-in-out infinite;background-color:var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity)));background-image:linear-gradient(105deg,transparent 0,transparent 40%,var(--fallback-b1,oklch(var(--b1)/1)) 50%,transparent 60%,transparent 100%);background-position-x:-50%;background-repeat:no-repeat;background-size:200% auto;will-change:background-position}@media (prefers-reduced-motion){.skeleton{animation-duration:15s}}@keyframes skeleton{0%{background-position:150%}to{background-position:-50%}}.textarea:focus{border-color:var(--fallback-bc,oklch(var(--bc)/.2));box-shadow:none;outline-color:var(--fallback-bc,oklch(var(--bc)/.2));outline-offset:2px;outline-style:solid;outline-width:2px}.textarea-disabled,.textarea:disabled,.textarea[disabled]{cursor:not-allowed;--tw-border-opacity:1;border-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)));--tw-bg-opacity:1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));color:var(--fallback-bc,oklch(var(--bc)/.4))}.textarea-disabled::-moz-placeholder,.textarea:disabled::-moz-placeholder,.textarea[disabled]::-moz-placeholder{color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)));--tw-placeholder-opacity:0.2}.textarea-disabled::placeholder,.textarea:disabled::placeholder,.textarea[disabled]::placeholder{color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)));--tw-placeholder-opacity:0.2}@keyframes toast-pop{0%{opacity:0;transform:scale(.9)}to{opacity:1;transform:scale(1)}}.btn-lg{font-size:1.125rem;height:4rem;min-height:4rem;padding-left:1.5rem;padding-right:1.5rem}.btn-square:where(.btn-lg){height:4rem;padding:0;width:4rem}.btn-circle:where(.btn-lg){border-radius:9999px;height:4rem;padding:0;width:4rem}.menu-horizontal{display:inline-flex;flex-direction:row}.menu-horizontal>li:not(.menu-title)>details>ul{position:absolute}.card-compact .card-body{font-size:.875rem;line-height:1.25rem;padding:1rem}.card-compact .card-title{margin-bottom:.25rem}.card-normal .card-body{font-size:1rem;line-height:1.5rem;padding:var(--padding-card,2rem)}.card-normal .card-title{margin-bottom:.75rem}.join.join-vertical>:where(:not(:first-child)):is(.btn){margin-top:calc(var(--border-btn)*-1)}.join.join-horizontal>:where(:not(:first-child)):is(.btn){margin-inline-start:calc(var(--border-btn)*-1);margin-top:0}.menu-horizontal>li:not(.menu-title)>details>ul{margin-inline-start:0;margin-top:1rem;padding-bottom:.5rem;padding-inline-end:.5rem;padding-top:.5rem}.menu-horizontal>li>details>ul:before{content:none}:where(.menu-horizontal>li:not(.menu-title)>details>ul){border-radius:var(--rounded-box,1rem);--tw-bg-opacity:1;background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)));--tw-shadow:0 20px 25px -5px rgba(0,0,0,.1),0 8px 10px -6px rgba(0,0,0,.1);--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color),0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.mx-auto{margin-left:auto;margin-right:auto}.my-12{margin-bottom:3rem;margin-top:3rem}.mb-8{margin-bottom:2rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.mt-12{margin-top:3rem}.ml-5{margin-left:1.25rem}.ml-4{margin-left:1rem}.block{display:block}.flex{display:flex}.grid{display:grid}.h-32{height:8rem}.min-h-\[80vh\]{min-height:80vh}.min-h-screen{min-height:100vh}.min-h-\[90vh\]{min-height:90vh}.w-full{width:100%}.w-32{width:8rem}.max-w-2xl{max-width:42rem}.max-w-lg{max-width:32rem}.max-w-md{max-width:28rem}.max-w-4xl{max-width:56rem}.max-w-screen-lg{max-width:1024px}.flex-1{flex:1 1 0%}.flex-none{flex:none}.flex-grow{flex-grow:1}.cursor-pointer{cursor:pointer}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-start{justify-content:flex-start}.justify-center{justify-content:center}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.gap-8{gap:2rem}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1rem*var(--tw-space-y-reverse));margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)))}.space-y-8>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(2rem*var(--tw-space-y-reverse));margin-top:calc(2rem*(1 - var(--tw-space-y-reverse)))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(1rem*var(--tw-space-x-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1.5rem*var(--tw-space-y-reverse));margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.5rem*var(--tw-space-y-reverse));margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)))}.rounded-full{border-radius:9999px}.border-transparent{border-color:transparent}.bg-base-200{--tw-bg-opacity:1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity,1)))}.bg-base-100{--tw-bg-opacity:1;background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity,1)))}.bg-gradient-to-r{background-image:linear-gradient(to right,var(--tw-gradient-stops))}.from-blue-400{--tw-gradient-from:#60a5fa var(--tw-gradient-from-position);--tw-gradient-to:rgba(96,165,250,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-primary{--tw-gradient-from:var(--fallback-p,oklch(var(--p)/1)) var(--tw-gradient-from-position);--tw-gradient-to:var(--fallback-p,oklch(var(--p)/0)) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.via-purple-500{--tw-gradient-to:rgba(168,85,247,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),#a855f7 var(--tw-gradient-via-position),var(--tw-gradient-to)}.to-pink-500{--tw-gradient-to:#ec4899 var(--tw-gradient-to-position)}.to-secondary{--tw-gradient-to:var(--fallback-s,oklch(var(--s)/1)) var(--tw-gradient-to-position)}.bg-clip-text{-webkit-background-clip:text;background-clip:text}.p-4{padding:1rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-4{padding-left:1rem;padding-right:1rem}.py-8{padding-bottom:2rem;padding-top:2rem}.py-4{padding-top:1rem}.pb-4,.py-4{padding-bottom:1rem}.pt-10{padding-top:2.5rem}.pb-5{padding-bottom:1.25rem}.text-center{text-align:center}.text-2xl{font-size:1.5rem;line-height:2rem}.text-5xl{font-size:3rem;line-height:1}.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-6xl{font-size:3.75rem;line-height:1}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.font-bold{font-weight:700}.font-semibold{font-weight:600}.text-blue-950{--tw-text-opacity:1;color:rgb(23 37 84/var(--tw-text-opacity,1))}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity,1))}.text-transparent{color:transparent}.text-base-content\/60{color:var(--fallback-bc,oklch(var(--bc)/.6))}.text-base-content\/70{color:var(--fallback-bc,oklch(var(--bc)/.7))}.shadow-xl{--tw-shadow:0 20px 25px -5px rgba(0,0,0,.1),0 8px 10px -6px rgba(0,0,0,.1);--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color),0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}@media (min-width:640px){.sm\:max-w-md{max-width:28rem}.sm\:px-0{padding-left:0;padding-right:0}.sm\:text-6xl{font-size:3.75rem;line-height:1}}@media (min-width:768px){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}} \ No newline at end of file +*,:after,:before{--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:rgba(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:rgba(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: }/*! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com*/*,:after,:before{border:0 solid #e5e7eb;box-sizing:border-box}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;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;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-tap-highlight-color:transparent}body{line-height:inherit;margin:0}hr{border-top-width:1px;color:inherit;height:0}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,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-size:1em;font-variation-settings:normal}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}button,input,optgroup,select,textarea{color:inherit;font-family:inherit;font-feature-settings:inherit;font-size:100%;font-variation-settings:inherit;font-weight:inherit;letter-spacing:inherit;line-height: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,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{color:#9ca3af;opacity:1}input::placeholder,textarea::placeholder{color:#9ca3af;opacity:1}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}[hidden]:where(:not([hidden=until-found])){display:none}:root,[data-theme]{background-color:var(--fallback-b1,oklch(var(--b1)/1));color:var(--fallback-bc,oklch(var(--bc)/1))}@supports not (color:oklch(0% 0 0)){:root{color-scheme:light;--fallback-p:#491eff;--fallback-pc:#d4dbff;--fallback-s:#ff41c7;--fallback-sc:#fff9fc;--fallback-a:#00cfbd;--fallback-ac:#00100d;--fallback-n:#2b3440;--fallback-nc:#d7dde4;--fallback-b1:#fff;--fallback-b2:#e5e6e6;--fallback-b3:#e5e6e6;--fallback-bc:#1f2937;--fallback-in:#00b3f0;--fallback-inc:#000;--fallback-su:#00ca92;--fallback-suc:#000;--fallback-wa:#ffc22d;--fallback-wac:#000;--fallback-er:#ff6f70;--fallback-erc:#000}@media (prefers-color-scheme:dark){:root{color-scheme:dark;--fallback-p:#7582ff;--fallback-pc:#050617;--fallback-s:#ff71cf;--fallback-sc:#190211;--fallback-a:#00c7b5;--fallback-ac:#000e0c;--fallback-n:#2a323c;--fallback-nc:#a6adbb;--fallback-b1:#1d232a;--fallback-b2:#191e24;--fallback-b3:#15191e;--fallback-bc:#a6adbb;--fallback-in:#00b3f0;--fallback-inc:#000;--fallback-su:#00ca92;--fallback-suc:#000;--fallback-wa:#ffc22d;--fallback-wac:#000;--fallback-er:#ff6f70;--fallback-erc:#000}}}html{-webkit-tap-highlight-color:transparent}*{scrollbar-color:color-mix(in oklch,currentColor 35%,transparent) transparent}:hover{scrollbar-color:color-mix(in oklch,currentColor 60%,transparent) transparent}:root{color-scheme:light;--in:72.06% 0.191 231.6;--su:64.8% 0.150 160;--wa:84.71% 0.199 83.87;--er:71.76% 0.221 22.18;--pc:89.824% 0.06192 275.75;--ac:15.352% 0.0368 183.61;--inc:0% 0 0;--suc:0% 0 0;--wac:0% 0 0;--erc:0% 0 0;--rounded-box:1rem;--rounded-btn:0.5rem;--rounded-badge:1.9rem;--animation-btn:0.25s;--animation-input:.2s;--btn-focus-scale:0.95;--border-btn:1px;--tab-border:1px;--tab-radius:0.5rem;--p:49.12% 0.3096 275.75;--s:69.71% 0.329 342.55;--sc:98.71% 0.0106 342.55;--a:76.76% 0.184 183.61;--n:32.1785% 0.02476 255.701624;--nc:89.4994% 0.011585 252.096176;--b1:100% 0 0;--b2:96.1151% 0 0;--b3:92.4169% 0.00108 197.137559;--bc:27.8078% 0.029596 256.847952}@media (prefers-color-scheme:dark){:root{color-scheme:dark;--in:72.06% 0.191 231.6;--su:64.8% 0.150 160;--wa:84.71% 0.199 83.87;--er:71.76% 0.221 22.18;--pc:13.138% 0.0392 275.75;--sc:14.96% 0.052 342.55;--ac:14.902% 0.0334 183.61;--inc:0% 0 0;--suc:0% 0 0;--wac:0% 0 0;--erc:0% 0 0;--rounded-box:1rem;--rounded-btn:0.5rem;--rounded-badge:1.9rem;--animation-btn:0.25s;--animation-input:.2s;--btn-focus-scale:0.95;--border-btn:1px;--tab-border:1px;--tab-radius:0.5rem;--p:65.69% 0.196 275.75;--s:74.8% 0.26 342.55;--a:74.51% 0.167 183.61;--n:31.3815% 0.021108 254.139175;--nc:74.6477% 0.0216 264.435964;--b1:25.3267% 0.015896 252.417568;--b2:23.2607% 0.013807 253.100675;--b3:21.1484% 0.01165 254.087939;--bc:74.6477% 0.0216 264.435964}}[data-theme=light]{color-scheme:light;--in:72.06% 0.191 231.6;--su:64.8% 0.150 160;--wa:84.71% 0.199 83.87;--er:71.76% 0.221 22.18;--pc:89.824% 0.06192 275.75;--ac:15.352% 0.0368 183.61;--inc:0% 0 0;--suc:0% 0 0;--wac:0% 0 0;--erc:0% 0 0;--rounded-box:1rem;--rounded-btn:0.5rem;--rounded-badge:1.9rem;--animation-btn:0.25s;--animation-input:.2s;--btn-focus-scale:0.95;--border-btn:1px;--tab-border:1px;--tab-radius:0.5rem;--p:49.12% 0.3096 275.75;--s:69.71% 0.329 342.55;--sc:98.71% 0.0106 342.55;--a:76.76% 0.184 183.61;--n:32.1785% 0.02476 255.701624;--nc:89.4994% 0.011585 252.096176;--b1:100% 0 0;--b2:96.1151% 0 0;--b3:92.4169% 0.00108 197.137559;--bc:27.8078% 0.029596 256.847952}[data-theme=dark]{color-scheme:dark;--in:72.06% 0.191 231.6;--su:64.8% 0.150 160;--wa:84.71% 0.199 83.87;--er:71.76% 0.221 22.18;--pc:13.138% 0.0392 275.75;--sc:14.96% 0.052 342.55;--ac:14.902% 0.0334 183.61;--inc:0% 0 0;--suc:0% 0 0;--wac:0% 0 0;--erc:0% 0 0;--rounded-box:1rem;--rounded-btn:0.5rem;--rounded-badge:1.9rem;--animation-btn:0.25s;--animation-input:.2s;--btn-focus-scale:0.95;--border-btn:1px;--tab-border:1px;--tab-radius:0.5rem;--p:65.69% 0.196 275.75;--s:74.8% 0.26 342.55;--a:74.51% 0.167 183.61;--n:31.3815% 0.021108 254.139175;--nc:74.6477% 0.0216 264.435964;--b1:25.3267% 0.015896 252.417568;--b2:23.2607% 0.013807 253.100675;--b3:21.1484% 0.01165 254.087939;--bc:74.6477% 0.0216 264.435964}.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.avatar.placeholder>div{align-items:center;display:flex;justify-content:center}@media (hover:hover){.checkbox-primary:hover{--tw-border-opacity:1;border-color:var(--fallback-p,oklch(var(--p)/var(--tw-border-opacity)))}.label a:hover{--tw-text-opacity:1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)))}.menu li>:not(ul,.menu-title,details,.btn).active,.menu li>:not(ul,.menu-title,details,.btn):active,.menu li>details>summary:active{--tw-bg-opacity:1;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));--tw-text-opacity:1;color:var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity)))}}.btn{align-items:center;border-color:transparent;border-color:oklch(var(--btn-color,var(--b2))/var(--tw-border-opacity));border-radius:var(--rounded-btn,.5rem);border-width:var(--border-btn,1px);cursor:pointer;display:inline-flex;flex-shrink:0;flex-wrap:wrap;font-size:.875rem;font-weight:600;gap:.5rem;height:3rem;justify-content:center;line-height:1em;min-height:3rem;padding-left:1rem;padding-right:1rem;text-align:center;text-decoration-line:none;transition-duration:.2s;transition-property:color,background-color,border-color,opacity,box-shadow,transform;transition-timing-function:cubic-bezier(0,0,.2,1);-webkit-user-select:none;-moz-user-select:none;user-select:none;--tw-text-opacity:1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));--tw-shadow:0 1px 2px 0 rgba(0,0,0,.05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color);background-color:oklch(var(--btn-color,var(--b2))/var(--tw-bg-opacity));box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);outline-color:var(--fallback-bc,oklch(var(--bc)/1));--tw-bg-opacity:1;--tw-border-opacity:1}.btn-disabled,.btn:disabled,.btn[disabled]{pointer-events:none}:where(.btn:is(input[type=checkbox])),:where(.btn:is(input[type=radio])){-webkit-appearance:none;-moz-appearance:none;appearance:none;width:auto}.btn:is(input[type=checkbox]):after,.btn:is(input[type=radio]):after{--tw-content:attr(aria-label);content:var(--tw-content)}.card{border-radius:var(--rounded-box,1rem);display:flex;flex-direction:column;position:relative}.card:focus{outline:2px solid transparent;outline-offset:2px}.card-body{display:flex;flex:1 1 auto;flex-direction:column;gap:.5rem;padding:var(--padding-card,2rem)}.card-body :where(p){flex-grow:1}.card figure{align-items:center;display:flex;justify-content:center}.card.image-full{display:grid}.card.image-full:before{border-radius:var(--rounded-box,1rem);content:"";position:relative;z-index:10;--tw-bg-opacity:1;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));opacity:.75}.card.image-full:before,.card.image-full>*{grid-column-start:1;grid-row-start:1}.card.image-full>figure img{height:100%;-o-object-fit:cover;object-fit:cover}.card.image-full>.card-body{position:relative;z-index:20;--tw-text-opacity:1;color:var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity)))}.chat{-moz-column-gap:.75rem;column-gap:.75rem;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));padding-bottom:.25rem;padding-top:.25rem}.chat-bubble{border-radius:var(--rounded-box,1rem);display:block;max-width:90%;min-height:2.75rem;min-width:2.75rem;padding:.5rem 1rem;position:relative;width:-moz-fit-content;width:fit-content;--tw-bg-opacity:1;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));--tw-text-opacity:1;color:var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity)))}.chat-bubble:before{background-color:inherit;bottom:0;content:"";height:.75rem;-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;position:absolute;width:.75rem}.chat-start{grid-template-columns:auto 1fr;place-items:start}.chat-start .chat-footer,.chat-start .chat-header{grid-column-start:2}.chat-start .chat-image{grid-column-start:1}.chat-start .chat-bubble{border-end-start-radius:0;grid-column-start:2}.chat-start .chat-bubble:before{inset-inline-start:-.749rem;-webkit-mask-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='3' height='3'%3E%3Cpath d='M0 3h3V0c0 1-2 3-3 3'/%3E%3C/svg%3E");mask-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='3' height='3'%3E%3Cpath d='M0 3h3V0c0 1-2 3-3 3'/%3E%3C/svg%3E")}[dir=rtl] .chat-start .chat-bubble:before{-webkit-mask-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='3' height='3'%3E%3Cpath d='M0 3h3C2 3 0 1 0 0'/%3E%3C/svg%3E");mask-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='3' height='3'%3E%3Cpath d='M0 3h3C2 3 0 1 0 0'/%3E%3C/svg%3E")}.chat-end .chat-bubble{border-end-end-radius:0;grid-column-start:1}.chat-end .chat-bubble:before{inset-inline-start:99.9%;-webkit-mask-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='3' height='3'%3E%3Cpath d='M0 3h3C2 3 0 1 0 0'/%3E%3C/svg%3E");mask-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='3' height='3'%3E%3Cpath d='M0 3h3C2 3 0 1 0 0'/%3E%3C/svg%3E")}[dir=rtl] .chat-end .chat-bubble:before{-webkit-mask-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='3' height='3'%3E%3Cpath d='M0 3h3V0c0 1-2 3-3 3'/%3E%3C/svg%3E");mask-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='3' height='3'%3E%3Cpath d='M0 3h3V0c0 1-2 3-3 3'/%3E%3C/svg%3E")}.checkbox{flex-shrink:0;--chkbg:var(--fallback-bc,oklch(var(--bc)/1));--chkfg:var(--fallback-b1,oklch(var(--b1)/1));-webkit-appearance:none;-moz-appearance:none;appearance:none;border-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-border-opacity)));border-radius:var(--rounded-btn,.5rem);border-width:1px;cursor:pointer;height:1.5rem;width:1.5rem;--tw-border-opacity:0.2}.divider{align-items:center;align-self:stretch;display:flex;flex-direction:row;height:1rem;margin-bottom:1rem;margin-top:1rem;white-space:nowrap}.divider:after,.divider:before{flex-grow:1;height:.125rem;width:100%;--tw-content:"";background-color:var(--fallback-bc,oklch(var(--bc)/.1));content:var(--tw-content)}@media (hover:hover){.btm-nav>.disabled:hover,.btm-nav>[disabled]:hover{pointer-events:none;--tw-border-opacity:0;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));--tw-bg-opacity:0.1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));--tw-text-opacity:0.2}.btn:hover{--tw-border-opacity:1;border-color:var(--fallback-b3,oklch(var(--b3)/var(--tw-border-opacity)));--tw-bg-opacity:1;background-color:var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity)))}@supports (color:color-mix(in oklab,black,black)){.btn:hover{background-color:color-mix(in oklab,oklch(var(--btn-color,var(--b2))/var(--tw-bg-opacity,1)) 90%,#000);border-color:color-mix(in oklab,oklch(var(--btn-color,var(--b2))/var(--tw-border-opacity,1)) 90%,#000)}}@supports not (color:oklch(0% 0 0)){.btn:hover{background-color:var(--btn-color,var(--fallback-b2));border-color:var(--btn-color,var(--fallback-b2))}}.btn.glass:hover{--glass-opacity:25%;--glass-border-opacity:15%}.btn-outline:hover{--tw-border-opacity:1;border-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-border-opacity)));--tw-bg-opacity:1;background-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)));--tw-text-opacity:1;color:var(--fallback-b1,oklch(var(--b1)/var(--tw-text-opacity)))}.btn-outline.btn-primary:hover{--tw-text-opacity:1;color:var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity)))}@supports (color:color-mix(in oklab,black,black)){.btn-outline.btn-primary:hover{background-color:color-mix(in oklab,var(--fallback-p,oklch(var(--p)/1)) 90%,#000);border-color:color-mix(in oklab,var(--fallback-p,oklch(var(--p)/1)) 90%,#000)}}.btn-outline.btn-secondary:hover{--tw-text-opacity:1;color:var(--fallback-sc,oklch(var(--sc)/var(--tw-text-opacity)))}@supports (color:color-mix(in oklab,black,black)){.btn-outline.btn-secondary:hover{background-color:color-mix(in oklab,var(--fallback-s,oklch(var(--s)/1)) 90%,#000);border-color:color-mix(in oklab,var(--fallback-s,oklch(var(--s)/1)) 90%,#000)}}.btn-outline.btn-accent:hover{--tw-text-opacity:1;color:var(--fallback-ac,oklch(var(--ac)/var(--tw-text-opacity)))}@supports (color:color-mix(in oklab,black,black)){.btn-outline.btn-accent:hover{background-color:color-mix(in oklab,var(--fallback-a,oklch(var(--a)/1)) 90%,#000);border-color:color-mix(in oklab,var(--fallback-a,oklch(var(--a)/1)) 90%,#000)}}.btn-outline.btn-success:hover{--tw-text-opacity:1;color:var(--fallback-suc,oklch(var(--suc)/var(--tw-text-opacity)))}@supports (color:color-mix(in oklab,black,black)){.btn-outline.btn-success:hover{background-color:color-mix(in oklab,var(--fallback-su,oklch(var(--su)/1)) 90%,#000);border-color:color-mix(in oklab,var(--fallback-su,oklch(var(--su)/1)) 90%,#000)}}.btn-outline.btn-info:hover{--tw-text-opacity:1;color:var(--fallback-inc,oklch(var(--inc)/var(--tw-text-opacity)))}@supports (color:color-mix(in oklab,black,black)){.btn-outline.btn-info:hover{background-color:color-mix(in oklab,var(--fallback-in,oklch(var(--in)/1)) 90%,#000);border-color:color-mix(in oklab,var(--fallback-in,oklch(var(--in)/1)) 90%,#000)}}.btn-outline.btn-warning:hover{--tw-text-opacity:1;color:var(--fallback-wac,oklch(var(--wac)/var(--tw-text-opacity)))}@supports (color:color-mix(in oklab,black,black)){.btn-outline.btn-warning:hover{background-color:color-mix(in oklab,var(--fallback-wa,oklch(var(--wa)/1)) 90%,#000);border-color:color-mix(in oklab,var(--fallback-wa,oklch(var(--wa)/1)) 90%,#000)}}.btn-outline.btn-error:hover{--tw-text-opacity:1;color:var(--fallback-erc,oklch(var(--erc)/var(--tw-text-opacity)))}@supports (color:color-mix(in oklab,black,black)){.btn-outline.btn-error:hover{background-color:color-mix(in oklab,var(--fallback-er,oklch(var(--er)/1)) 90%,#000);border-color:color-mix(in oklab,var(--fallback-er,oklch(var(--er)/1)) 90%,#000)}}.btn-disabled:hover,.btn:disabled:hover,.btn[disabled]:hover{--tw-border-opacity:0;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));--tw-bg-opacity:0.2;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));--tw-text-opacity:0.2}@supports (color:color-mix(in oklab,black,black)){.btn:is(input[type=checkbox]:checked):hover,.btn:is(input[type=radio]:checked):hover{background-color:color-mix(in oklab,var(--fallback-p,oklch(var(--p)/1)) 90%,#000);border-color:color-mix(in oklab,var(--fallback-p,oklch(var(--p)/1)) 90%,#000)}}:where(.menu li:not(.menu-title,.disabled)>:not(ul,details,.menu-title)):not(.active,.btn):hover,:where(.menu li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.active,.btn):hover{cursor:pointer;outline:2px solid transparent;outline-offset:2px}@supports (color:oklch(0% 0 0)){:where(.menu li:not(.menu-title,.disabled)>:not(ul,details,.menu-title)):not(.active,.btn):hover,:where(.menu li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.active,.btn):hover{background-color:var(--fallback-bc,oklch(var(--bc)/.1))}}}.file-input{border-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-border-opacity)));border-radius:var(--rounded-btn,.5rem);border-width:1px;flex-shrink:1;font-size:1rem;height:3rem;line-height:2;line-height:1.5rem;overflow:hidden;padding-inline-end:1rem;--tw-border-opacity:0;--tw-bg-opacity:1;background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)))}.file-input::file-selector-button{align-items:center;border-style:solid;cursor:pointer;display:inline-flex;flex-shrink:0;flex-wrap:wrap;font-size:.875rem;height:100%;justify-content:center;line-height:1.25rem;line-height:1em;margin-inline-end:1rem;padding-left:1rem;padding-right:1rem;text-align:center;transition-duration:.2s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-timing-function:cubic-bezier(0,0,.2,1);-webkit-user-select:none;-moz-user-select:none;user-select:none;--tw-border-opacity:1;border-color:var(--fallback-n,oklch(var(--n)/var(--tw-border-opacity)));--tw-bg-opacity:1;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));font-weight:600;text-transform:uppercase;--tw-text-opacity:1;animation:button-pop var(--animation-btn,.25s) ease-out;border-width:var(--border-btn,1px);color:var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity)));text-decoration-line:none}.form-control{flex-direction:column}.form-control,.label{display:flex}.label{align-items:center;justify-content:space-between;padding:.5rem .25rem;-webkit-user-select:none;-moz-user-select:none;user-select:none}.hero{background-position:50%;background-size:cover;display:grid;place-items:center;width:100%}.hero>*{grid-column-start:1;grid-row-start:1}.hero-content{align-items:center;display:flex;gap:1rem;justify-content:center;max-width:80rem;padding:1rem;z-index:0}.input{-webkit-appearance:none;-moz-appearance:none;appearance:none;border-color:transparent;border-radius:var(--rounded-btn,.5rem);border-width:1px;flex-shrink:1;font-size:1rem;height:3rem;line-height:2;line-height:1.5rem;padding-left:1rem;padding-right:1rem;--tw-bg-opacity:1;background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)))}.input-md[type=number]::-webkit-inner-spin-button,.input[type=number]::-webkit-inner-spin-button{margin-bottom:-1rem;margin-top:-1rem;margin-inline-end:-1rem}.link{cursor:pointer;text-decoration-line:underline}.menu{display:flex;flex-direction:column;flex-wrap:wrap;font-size:.875rem;line-height:1.25rem;padding:.5rem}.menu :where(li ul){margin-inline-start:1rem;padding-inline-start:.5rem;position:relative;white-space:nowrap}.menu :where(li:not(.menu-title)>:not(ul,details,.menu-title,.btn)),.menu :where(li:not(.menu-title)>details>summary:not(.menu-title)){align-content:flex-start;align-items:center;display:grid;gap:.5rem;grid-auto-columns:minmax(auto,max-content) auto max-content;grid-auto-flow:column;-webkit-user-select:none;-moz-user-select:none;user-select:none}.menu li.disabled{color:var(--fallback-bc,oklch(var(--bc)/.3));cursor:not-allowed;-webkit-user-select:none;-moz-user-select:none;user-select:none}.menu :where(li>.menu-dropdown:not(.menu-dropdown-show)){display:none}:where(.menu li){align-items:stretch;display:flex;flex-direction:column;flex-shrink:0;flex-wrap:wrap;position:relative}:where(.menu li) .badge{justify-self:end}.navbar{align-items:center;display:flex;min-height:4rem;padding:var(--navbar-padding,.5rem);width:100%}:where(.navbar>:not(script,style)){align-items:center;display:inline-flex}.textarea{border-color:transparent;border-radius:var(--rounded-btn,.5rem);border-width:1px;flex-shrink:1;font-size:.875rem;line-height:1.25rem;line-height:2;min-height:3rem;padding:.5rem 1rem;--tw-bg-opacity:1;background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)))}.btm-nav>.disabled,.btm-nav>[disabled]{pointer-events:none;--tw-border-opacity:0;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));--tw-bg-opacity:0.1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));--tw-text-opacity:0.2}.btm-nav>* .label{font-size:1rem;line-height:1.5rem}@media (prefers-reduced-motion:no-preference){.btn{animation:button-pop var(--animation-btn,.25s) ease-out}}.btn:active:focus,.btn:active:hover{animation:button-pop 0s ease-out;transform:scale(var(--btn-focus-scale,.97))}@supports not (color:oklch(0% 0 0)){.btn{background-color:var(--btn-color,var(--fallback-b2));border-color:var(--btn-color,var(--fallback-b2))}.btn-primary{--btn-color:var(--fallback-p)}.btn-secondary{--btn-color:var(--fallback-s)}.btn-error{--btn-color:var(--fallback-er)}}@supports (color:color-mix(in oklab,black,black)){.btn-outline.btn-primary.btn-active{background-color:color-mix(in oklab,var(--fallback-p,oklch(var(--p)/1)) 90%,#000);border-color:color-mix(in oklab,var(--fallback-p,oklch(var(--p)/1)) 90%,#000)}.btn-outline.btn-secondary.btn-active{background-color:color-mix(in oklab,var(--fallback-s,oklch(var(--s)/1)) 90%,#000);border-color:color-mix(in oklab,var(--fallback-s,oklch(var(--s)/1)) 90%,#000)}.btn-outline.btn-accent.btn-active{background-color:color-mix(in oklab,var(--fallback-a,oklch(var(--a)/1)) 90%,#000);border-color:color-mix(in oklab,var(--fallback-a,oklch(var(--a)/1)) 90%,#000)}.btn-outline.btn-success.btn-active{background-color:color-mix(in oklab,var(--fallback-su,oklch(var(--su)/1)) 90%,#000);border-color:color-mix(in oklab,var(--fallback-su,oklch(var(--su)/1)) 90%,#000)}.btn-outline.btn-info.btn-active{background-color:color-mix(in oklab,var(--fallback-in,oklch(var(--in)/1)) 90%,#000);border-color:color-mix(in oklab,var(--fallback-in,oklch(var(--in)/1)) 90%,#000)}.btn-outline.btn-warning.btn-active{background-color:color-mix(in oklab,var(--fallback-wa,oklch(var(--wa)/1)) 90%,#000);border-color:color-mix(in oklab,var(--fallback-wa,oklch(var(--wa)/1)) 90%,#000)}.btn-outline.btn-error.btn-active{background-color:color-mix(in oklab,var(--fallback-er,oklch(var(--er)/1)) 90%,#000);border-color:color-mix(in oklab,var(--fallback-er,oklch(var(--er)/1)) 90%,#000)}}.btn:focus-visible{outline-offset:2px;outline-style:solid;outline-width:2px}.btn-primary{--tw-text-opacity:1;color:var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity)));outline-color:var(--fallback-p,oklch(var(--p)/1))}@supports (color:oklch(0% 0 0)){.btn-primary{--btn-color:var(--p)}.btn-secondary{--btn-color:var(--s)}.btn-error{--btn-color:var(--er)}}.btn-secondary{--tw-text-opacity:1;color:var(--fallback-sc,oklch(var(--sc)/var(--tw-text-opacity)));outline-color:var(--fallback-s,oklch(var(--s)/1))}.btn-error{--tw-text-opacity:1;color:var(--fallback-erc,oklch(var(--erc)/var(--tw-text-opacity)));outline-color:var(--fallback-er,oklch(var(--er)/1))}.btn.glass{--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);outline-color:currentColor}.btn.glass.btn-active{--glass-opacity:25%;--glass-border-opacity:15%}.btn-outline{background-color:transparent;border-color:currentColor;--tw-text-opacity:1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.btn-outline.btn-active{--tw-border-opacity:1;border-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-border-opacity)));--tw-bg-opacity:1;background-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)));--tw-text-opacity:1;color:var(--fallback-b1,oklch(var(--b1)/var(--tw-text-opacity)))}.btn-outline.btn-primary{--tw-text-opacity:1;color:var(--fallback-p,oklch(var(--p)/var(--tw-text-opacity)))}.btn-outline.btn-primary.btn-active{--tw-text-opacity:1;color:var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity)))}.btn-outline.btn-secondary{--tw-text-opacity:1;color:var(--fallback-s,oklch(var(--s)/var(--tw-text-opacity)))}.btn-outline.btn-secondary.btn-active{--tw-text-opacity:1;color:var(--fallback-sc,oklch(var(--sc)/var(--tw-text-opacity)))}.btn-outline.btn-accent{--tw-text-opacity:1;color:var(--fallback-a,oklch(var(--a)/var(--tw-text-opacity)))}.btn-outline.btn-accent.btn-active{--tw-text-opacity:1;color:var(--fallback-ac,oklch(var(--ac)/var(--tw-text-opacity)))}.btn-outline.btn-success{--tw-text-opacity:1;color:var(--fallback-su,oklch(var(--su)/var(--tw-text-opacity)))}.btn-outline.btn-success.btn-active{--tw-text-opacity:1;color:var(--fallback-suc,oklch(var(--suc)/var(--tw-text-opacity)))}.btn-outline.btn-info{--tw-text-opacity:1;color:var(--fallback-in,oklch(var(--in)/var(--tw-text-opacity)))}.btn-outline.btn-info.btn-active{--tw-text-opacity:1;color:var(--fallback-inc,oklch(var(--inc)/var(--tw-text-opacity)))}.btn-outline.btn-warning{--tw-text-opacity:1;color:var(--fallback-wa,oklch(var(--wa)/var(--tw-text-opacity)))}.btn-outline.btn-warning.btn-active{--tw-text-opacity:1;color:var(--fallback-wac,oklch(var(--wac)/var(--tw-text-opacity)))}.btn-outline.btn-error{--tw-text-opacity:1;color:var(--fallback-er,oklch(var(--er)/var(--tw-text-opacity)))}.btn-outline.btn-error.btn-active{--tw-text-opacity:1;color:var(--fallback-erc,oklch(var(--erc)/var(--tw-text-opacity)))}.btn.btn-disabled,.btn:disabled,.btn[disabled]{--tw-border-opacity:0;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));--tw-bg-opacity:0.2;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));--tw-text-opacity:0.2}.btn:is(input[type=checkbox]:checked),.btn:is(input[type=radio]:checked){--tw-border-opacity:1;border-color:var(--fallback-p,oklch(var(--p)/var(--tw-border-opacity)));--tw-bg-opacity:1;background-color:var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity)));--tw-text-opacity:1;color:var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity)))}.btn:is(input[type=checkbox]:checked):focus-visible,.btn:is(input[type=radio]:checked):focus-visible{outline-color:var(--fallback-p,oklch(var(--p)/1))}@keyframes button-pop{0%{transform:scale(var(--btn-focus-scale,.98))}40%{transform:scale(1.02)}to{transform:scale(1)}}.card :where(figure:first-child){border-end-end-radius:unset;border-end-start-radius:unset;border-start-end-radius:inherit;border-start-start-radius:inherit;overflow:hidden}.card :where(figure:last-child){border-end-end-radius:inherit;border-end-start-radius:inherit;border-start-end-radius:unset;border-start-start-radius:unset;overflow:hidden}.card:focus-visible{outline:2px solid currentColor;outline-offset:2px}.card.bordered{border-width:1px;--tw-border-opacity:1;border-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)))}.card.compact .card-body{font-size:.875rem;line-height:1.25rem;padding:1rem}.card-title{align-items:center;display:flex;font-size:1.25rem;font-weight:600;gap:.5rem;line-height:1.75rem}.card.image-full :where(figure){border-radius:inherit;overflow:hidden}.checkbox:focus{box-shadow:none}.checkbox:focus-visible{outline-color:var(--fallback-bc,oklch(var(--bc)/1));outline-offset:2px;outline-style:solid;outline-width:2px}.checkbox:disabled{border-color:transparent;border-width:0;cursor:not-allowed;--tw-bg-opacity:1;background-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)));opacity:.2}.checkbox:checked,.checkbox[aria-checked=true]{animation:checkmark var(--animation-input,.2s) ease-out;background-color:var(--chkbg);background-image:linear-gradient(-45deg,transparent 65%,var(--chkbg) 65.99%),linear-gradient(45deg,transparent 75%,var(--chkbg) 75.99%),linear-gradient(-45deg,var(--chkbg) 40%,transparent 40.99%),linear-gradient(45deg,var(--chkbg) 30%,var(--chkfg) 30.99%,var(--chkfg) 40%,transparent 40.99%),linear-gradient(-45deg,var(--chkfg) 50%,var(--chkbg) 50.99%);background-repeat:no-repeat}.checkbox:indeterminate{--tw-bg-opacity:1;animation:checkmark var(--animation-input,.2s) ease-out;background-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)));background-image:linear-gradient(90deg,transparent 80%,var(--chkbg) 80%),linear-gradient(-90deg,transparent 80%,var(--chkbg) 80%),linear-gradient(0deg,var(--chkbg) 43%,var(--chkfg) 43%,var(--chkfg) 57%,var(--chkbg) 57%);background-repeat:no-repeat}.checkbox-primary{--chkbg:var(--fallback-p,oklch(var(--p)/1));--chkfg:var(--fallback-pc,oklch(var(--pc)/1));--tw-border-opacity:1;border-color:var(--fallback-p,oklch(var(--p)/var(--tw-border-opacity)))}.checkbox-primary:focus-visible{outline-color:var(--fallback-p,oklch(var(--p)/1))}.checkbox-primary:checked,.checkbox-primary[aria-checked=true]{--tw-border-opacity:1;border-color:var(--fallback-p,oklch(var(--p)/var(--tw-border-opacity)));--tw-bg-opacity:1;background-color:var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity)));--tw-text-opacity:1;color:var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity)))}@keyframes checkmark{0%{background-position-y:5px}50%{background-position-y:-2px}to{background-position-y:0}}.divider:not(:empty){gap:1rem}.file-input-bordered{--tw-border-opacity:0.2}.file-input:focus{outline-color:var(--fallback-bc,oklch(var(--bc)/.2));outline-offset:2px;outline-style:solid;outline-width:2px}.file-input-disabled,.file-input[disabled]{cursor:not-allowed;--tw-border-opacity:1;border-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)));--tw-bg-opacity:1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));--tw-text-opacity:0.2}.file-input-disabled::-moz-placeholder,.file-input[disabled]::-moz-placeholder{color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)));--tw-placeholder-opacity:0.2}.file-input-disabled::placeholder,.file-input[disabled]::placeholder{color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)));--tw-placeholder-opacity:0.2}.file-input-disabled::file-selector-button,.file-input[disabled]::file-selector-button{--tw-border-opacity:0;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));--tw-bg-opacity:0.2;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));--tw-text-opacity:0.2}.label-text{font-size:.875rem;line-height:1.25rem;--tw-text-opacity:1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)))}.input input{--tw-bg-opacity:1;background-color:var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity)));background-color:transparent}.input input:focus{outline:2px solid transparent;outline-offset:2px}.input[list]::-webkit-calendar-picker-indicator{line-height:1em}.input-bordered{border-color:var(--fallback-bc,oklch(var(--bc)/.2))}.input:focus,.input:focus-within{border-color:var(--fallback-bc,oklch(var(--bc)/.2));box-shadow:none;outline-color:var(--fallback-bc,oklch(var(--bc)/.2));outline-offset:2px;outline-style:solid;outline-width:2px}.input-disabled,.input:disabled,.input:has(>input[disabled]),.input[disabled]{cursor:not-allowed;--tw-border-opacity:1;border-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)));--tw-bg-opacity:1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));color:var(--fallback-bc,oklch(var(--bc)/.4))}.input-disabled::-moz-placeholder,.input:disabled::-moz-placeholder,.input:has(>input[disabled])::-moz-placeholder,.input[disabled]::-moz-placeholder{color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)));--tw-placeholder-opacity:0.2}.input-disabled::placeholder,.input:disabled::placeholder,.input:has(>input[disabled])::placeholder,.input[disabled]::placeholder{color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)));--tw-placeholder-opacity:0.2}.input:has(>input[disabled])>input[disabled]{cursor:not-allowed}.input::-webkit-date-and-time-value{text-align:inherit}.join>:where(:not(:first-child)):is(.btn){margin-inline-start:calc(var(--border-btn)*-1)}.link-primary{--tw-text-opacity:1;color:var(--fallback-p,oklch(var(--p)/var(--tw-text-opacity)))}@supports (color:color-mix(in oklab,black,black)){@media (hover:hover){.link-primary:hover{color:color-mix(in oklab,var(--fallback-p,oklch(var(--p)/1)) 80%,#000)}}}.link:focus{outline:2px solid transparent;outline-offset:2px}.link:focus-visible{outline:2px solid currentColor;outline-offset:2px}:where(.menu li:empty){--tw-bg-opacity:1;background-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)));height:1px;margin:.5rem 1rem;opacity:.1}.menu :where(li ul):before{bottom:.75rem;inset-inline-start:0;position:absolute;top:.75rem;width:1px;--tw-bg-opacity:1;background-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)));content:"";opacity:.1}.menu :where(li:not(.menu-title)>:not(ul,details,.menu-title,.btn)),.menu :where(li:not(.menu-title)>details>summary:not(.menu-title)){border-radius:var(--rounded-btn,.5rem);padding:.5rem 1rem;text-align:start;text-wrap:balance;transition-duration:.2s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-timing-function:cubic-bezier(0,0,.2,1)}:where(.menu li:not(.menu-title,.disabled)>:not(ul,details,.menu-title)):is(summary):not(.active,.btn):focus-visible,:where(.menu li:not(.menu-title,.disabled)>:not(ul,details,.menu-title)):not(summary,.active,.btn).focus,:where(.menu li:not(.menu-title,.disabled)>:not(ul,details,.menu-title)):not(summary,.active,.btn):focus,:where(.menu li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):is(summary):not(.active,.btn):focus-visible,:where(.menu li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(summary,.active,.btn).focus,:where(.menu li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(summary,.active,.btn):focus{background-color:var(--fallback-bc,oklch(var(--bc)/.1));cursor:pointer;--tw-text-opacity:1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));outline:2px solid transparent;outline-offset:2px}.menu li>:not(ul,.menu-title,details,.btn).active,.menu li>:not(ul,.menu-title,details,.btn):active,.menu li>details>summary:active{--tw-bg-opacity:1;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));--tw-text-opacity:1;color:var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity)))}.menu :where(li>details>summary)::-webkit-details-marker{display:none}.menu :where(li>.menu-dropdown-toggle):after,.menu :where(li>details>summary):after{box-shadow:2px 2px;content:"";display:block;height:.5rem;justify-self:end;margin-top:-.5rem;pointer-events:none;transform:rotate(45deg);transform-origin:75% 75%;transition-duration:.3s;transition-property:transform,margin-top;transition-timing-function:cubic-bezier(.4,0,.2,1);width:.5rem}.menu :where(li>.menu-dropdown-toggle.menu-dropdown-show):after,.menu :where(li>details[open]>summary):after{margin-top:0;transform:rotate(225deg)}.mockup-browser .mockup-browser-toolbar .input{display:block;height:1.75rem;margin-left:auto;margin-right:auto;overflow:hidden;position:relative;text-overflow:ellipsis;white-space:nowrap;width:24rem;--tw-bg-opacity:1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));direction:ltr;padding-left:2rem}.mockup-browser .mockup-browser-toolbar .input:before{aspect-ratio:1/1;height:.75rem;left:.5rem;--tw-translate-y:-50%;border-color:currentColor;border-radius:9999px;border-width:2px}.mockup-browser .mockup-browser-toolbar .input:after,.mockup-browser .mockup-browser-toolbar .input:before{content:"";opacity:.6;position:absolute;top:50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.mockup-browser .mockup-browser-toolbar .input:after{height:.5rem;left:1.25rem;--tw-translate-y:25%;--tw-rotate:-45deg;border-color:currentColor;border-radius:9999px;border-width:1px}@keyframes modal-pop{0%{opacity:0}}@keyframes progress-loading{50%{background-position-x:-115%}}@keyframes radiomark{0%{box-shadow:0 0 0 12px var(--fallback-b1,oklch(var(--b1)/1)) inset,0 0 0 12px var(--fallback-b1,oklch(var(--b1)/1)) inset}50%{box-shadow:0 0 0 3px var(--fallback-b1,oklch(var(--b1)/1)) inset,0 0 0 3px var(--fallback-b1,oklch(var(--b1)/1)) inset}to{box-shadow:0 0 0 4px var(--fallback-b1,oklch(var(--b1)/1)) inset,0 0 0 4px var(--fallback-b1,oklch(var(--b1)/1)) inset}}@keyframes rating-pop{0%{transform:translateY(-.125em)}40%{transform:translateY(-.125em)}to{transform:translateY(0)}}.skeleton{border-radius:var(--rounded-box,1rem);--tw-bg-opacity:1;animation:skeleton 1.8s ease-in-out infinite;background-color:var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity)));background-image:linear-gradient(105deg,transparent 0,transparent 40%,var(--fallback-b1,oklch(var(--b1)/1)) 50%,transparent 60%,transparent 100%);background-position-x:-50%;background-repeat:no-repeat;background-size:200% auto;will-change:background-position}@media (prefers-reduced-motion){.skeleton{animation-duration:15s}}@keyframes skeleton{0%{background-position:150%}to{background-position:-50%}}.textarea:focus{border-color:var(--fallback-bc,oklch(var(--bc)/.2));box-shadow:none;outline-color:var(--fallback-bc,oklch(var(--bc)/.2));outline-offset:2px;outline-style:solid;outline-width:2px}.textarea-disabled,.textarea:disabled,.textarea[disabled]{cursor:not-allowed;--tw-border-opacity:1;border-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)));--tw-bg-opacity:1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));color:var(--fallback-bc,oklch(var(--bc)/.4))}.textarea-disabled::-moz-placeholder,.textarea:disabled::-moz-placeholder,.textarea[disabled]::-moz-placeholder{color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)));--tw-placeholder-opacity:0.2}.textarea-disabled::placeholder,.textarea:disabled::placeholder,.textarea[disabled]::placeholder{color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)));--tw-placeholder-opacity:0.2}@keyframes toast-pop{0%{opacity:0;transform:scale(.9)}to{opacity:1;transform:scale(1)}}.btn-lg{font-size:1.125rem;height:4rem;min-height:4rem;padding-left:1.5rem;padding-right:1.5rem}.btn-square:where(.btn-lg){height:4rem;padding:0;width:4rem}.btn-circle:where(.btn-lg){border-radius:9999px;height:4rem;padding:0;width:4rem}.menu-horizontal{display:inline-flex;flex-direction:row}.menu-horizontal>li:not(.menu-title)>details>ul{position:absolute}.card-compact .card-body{font-size:.875rem;line-height:1.25rem;padding:1rem}.card-compact .card-title{margin-bottom:.25rem}.card-normal .card-body{font-size:1rem;line-height:1.5rem;padding:var(--padding-card,2rem)}.card-normal .card-title{margin-bottom:.75rem}.join.join-vertical>:where(:not(:first-child)):is(.btn){margin-top:calc(var(--border-btn)*-1)}.join.join-horizontal>:where(:not(:first-child)):is(.btn){margin-inline-start:calc(var(--border-btn)*-1);margin-top:0}.menu-horizontal>li:not(.menu-title)>details>ul{margin-inline-start:0;margin-top:1rem;padding-bottom:.5rem;padding-inline-end:.5rem;padding-top:.5rem}.menu-horizontal>li>details>ul:before{content:none}:where(.menu-horizontal>li:not(.menu-title)>details>ul){border-radius:var(--rounded-box,1rem);--tw-bg-opacity:1;background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)));--tw-shadow:0 20px 25px -5px rgba(0,0,0,.1),0 8px 10px -6px rgba(0,0,0,.1);--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color),0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.mx-auto{margin-left:auto;margin-right:auto}.my-12{margin-bottom:3rem;margin-top:3rem}.my-4{margin-bottom:1rem;margin-top:1rem}.mb-8{margin-bottom:2rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.mt-12{margin-top:3rem}.ml-5{margin-left:1.25rem}.ml-4{margin-left:1rem}.mt-2{margin-top:.5rem}.block{display:block}.flex{display:flex}.grid{display:grid}.h-32{height:8rem}.min-h-\[80vh\]{min-height:80vh}.min-h-screen{min-height:100vh}.min-h-\[90vh\]{min-height:90vh}.w-full{width:100%}.w-32{width:8rem}.max-w-2xl{max-width:42rem}.max-w-lg{max-width:32rem}.max-w-md{max-width:28rem}.max-w-4xl{max-width:56rem}.max-w-screen-lg{max-width:1024px}.flex-1{flex:1 1 0%}.flex-none{flex:none}.flex-grow{flex-grow:1}.cursor-pointer{cursor:pointer}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-start{justify-content:flex-start}.justify-center{justify-content:center}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.gap-8{gap:2rem}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1rem*var(--tw-space-y-reverse));margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)))}.space-y-8>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(2rem*var(--tw-space-y-reverse));margin-top:calc(2rem*(1 - var(--tw-space-y-reverse)))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(1rem*var(--tw-space-x-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1.5rem*var(--tw-space-y-reverse));margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.5rem*var(--tw-space-y-reverse));margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)))}.rounded-full{border-radius:9999px}.border-transparent{border-color:transparent}.bg-base-200{--tw-bg-opacity:1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity,1)))}.bg-base-100{--tw-bg-opacity:1;background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity,1)))}.bg-gradient-to-r{background-image:linear-gradient(to right,var(--tw-gradient-stops))}.from-blue-400{--tw-gradient-from:#60a5fa var(--tw-gradient-from-position);--tw-gradient-to:rgba(96,165,250,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-primary{--tw-gradient-from:var(--fallback-p,oklch(var(--p)/1)) var(--tw-gradient-from-position);--tw-gradient-to:var(--fallback-p,oklch(var(--p)/0)) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.via-purple-500{--tw-gradient-to:rgba(168,85,247,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),#a855f7 var(--tw-gradient-via-position),var(--tw-gradient-to)}.to-pink-500{--tw-gradient-to:#ec4899 var(--tw-gradient-to-position)}.to-secondary{--tw-gradient-to:var(--fallback-s,oklch(var(--s)/1)) var(--tw-gradient-to-position)}.bg-clip-text{-webkit-background-clip:text;background-clip:text}.p-4{padding:1rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-4{padding-left:1rem;padding-right:1rem}.py-8{padding-bottom:2rem;padding-top:2rem}.py-4{padding-top:1rem}.pb-4,.py-4{padding-bottom:1rem}.pt-10{padding-top:2.5rem}.pb-5{padding-bottom:1.25rem}.text-center{text-align:center}.text-2xl{font-size:1.5rem;line-height:2rem}.text-5xl{font-size:3rem;line-height:1}.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-6xl{font-size:3.75rem;line-height:1}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.font-bold{font-weight:700}.font-semibold{font-weight:600}.text-blue-950{--tw-text-opacity:1;color:rgb(23 37 84/var(--tw-text-opacity,1))}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity,1))}.text-transparent{color:transparent}.text-base-content\/60{color:var(--fallback-bc,oklch(var(--bc)/.6))}.text-base-content\/70{color:var(--fallback-bc,oklch(var(--bc)/.7))}.text-error{--tw-text-opacity:1;color:var(--fallback-er,oklch(var(--er)/var(--tw-text-opacity,1)))}.text-primary{--tw-text-opacity:1;color:var(--fallback-p,oklch(var(--p)/var(--tw-text-opacity,1)))}.text-base-content{--tw-text-opacity:1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity,1)))}.shadow-xl{--tw-shadow:0 20px 25px -5px rgba(0,0,0,.1),0 8px 10px -6px rgba(0,0,0,.1);--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color),0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}@media (min-width:640px){.sm\:max-w-md{max-width:28rem}.sm\:px-0{padding-left:0;padding-right:0}.sm\:text-6xl{font-size:3.75rem;line-height:1}}@media (min-width:768px){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}} \ No newline at end of file diff --git a/src/error.rs b/src/error.rs index 3400ba1..6a7e1fb 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,107 +1,233 @@ +use std::sync::Arc; + use async_openai::error::OpenAIError; -use axum::{http::StatusCode, response::IntoResponse, Json}; +use axum::{ + http::StatusCode, + response::{Html, IntoResponse, Response}, + Json, +}; +use minijinja::context; +use minijinja_autoreload::AutoReloader; +use serde::Serialize; use serde_json::json; use thiserror::Error; use tokio::task::JoinError; use crate::{ - ingress::types::ingress_input::IngressContentError, rabbitmq::RabbitMQError, - storage::types::file_info::FileError, utils::mailer::EmailError, + rabbitmq::RabbitMQError, storage::types::file_info::FileError, utils::mailer::EmailError, }; +// Core internal errors #[derive(Error, Debug)] -pub enum ProcessingError { - #[error("SurrealDb error: {0}")] - SurrealDbError(#[from] surrealdb::Error), - - #[error("LLM processing error: {0}")] - OpenAIerror(#[from] OpenAIError), - - #[error("Embedding processing error: {0}")] - EmbeddingError(String), - - #[error("Graph processing error: {0}")] - GraphProcessingError(String), - - #[error("LLM parsing error: {0}")] - LLMParsingError(String), - - #[error("Task join error: {0}")] - JoinError(#[from] JoinError), -} - -#[derive(Error, Debug)] -pub enum IngressConsumerError { +pub enum AppError { + #[error("Database error: {0}")] + Database(#[from] surrealdb::Error), + #[error("OpenAI error: {0}")] + OpenAI(#[from] OpenAIError), #[error("RabbitMQ error: {0}")] RabbitMQ(#[from] RabbitMQError), - - #[error("Processing error: {0}")] - Processing(#[from] ProcessingError), - - #[error("Ingress content error: {0}")] - IngressContent(#[from] IngressContentError), + #[error("File error: {0}")] + File(#[from] FileError), + #[error("Email error: {0}")] + Email(#[from] EmailError), + #[error("Not found: {0}")] + NotFound(String), + #[error("Validation error: {0}")] + Validation(String), + #[error("Authorization error: {0}")] + Auth(String), + #[error("LLM parsing error: {0}")] + LLMParsing(String), + #[error("Task join error: {0}")] + Join(#[from] JoinError), + #[error("Graph mapper error: {0}")] + GraphMapper(String), + #[error("IoError: {0}")] + Io(#[from] std::io::Error), + #[error("Minijina error: {0}")] + MiniJinja(#[from] minijinja::Error), } -#[derive(Error, Debug)] +// API-specific errors +#[derive(Debug, Serialize)] pub enum ApiError { - #[error("Processing error: {0}")] - ProcessingError(#[from] ProcessingError), - #[error("Ingress content error: {0}")] - IngressContentError(#[from] IngressContentError), - #[error("Publishing error: {0}")] - PublishingError(String), - #[error("Database error: {0}")] - DatabaseError(String), - #[error("Query error: {0}")] - QueryError(String), - #[error("RabbitMQ error: {0}")] - RabbitMQError(#[from] RabbitMQError), - #[error("LLM processing error: {0}")] - OpenAIerror(#[from] OpenAIError), - #[error("File error: {0}")] - FileError(#[from] FileError), - #[error("SurrealDb error: {0}")] - SurrealDbError(#[from] surrealdb::Error), - #[error("User already exists")] - UserAlreadyExists, - #[error("User was not found")] - UserNotFound, - #[error("You must provide valid credentials")] - AuthRequired, - #[error("Templating error: {0}")] - TemplatingError(#[from] minijinja::Error), - #[error("Mail error: {0}")] - EmailError(#[from] EmailError), + InternalError(String), + ValidationError(String), + NotFound(String), + Unauthorized(String), +} + +impl From for ApiError { + fn from(err: AppError) -> Self { + match err { + AppError::Database(_) | AppError::OpenAI(_) | AppError::Email(_) => { + tracing::error!("Internal error: {:?}", err); + ApiError::InternalError("Internal server error".to_string()) + } + AppError::NotFound(msg) => ApiError::NotFound(msg), + AppError::Validation(msg) => ApiError::ValidationError(msg), + AppError::Auth(msg) => ApiError::Unauthorized(msg), + _ => ApiError::InternalError("Internal server error".to_string()), + } + } } impl IntoResponse for ApiError { - fn into_response(self) -> axum::response::Response { - let (status, error_message) = match &self { - ApiError::ProcessingError(_) => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()), - ApiError::SurrealDbError(_) => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()), - ApiError::PublishingError(_) => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()), - ApiError::DatabaseError(_) => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()), - ApiError::OpenAIerror(_) => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()), - ApiError::QueryError(_) => (StatusCode::BAD_REQUEST, self.to_string()), - ApiError::UserAlreadyExists => (StatusCode::BAD_REQUEST, self.to_string()), - ApiError::AuthRequired => (StatusCode::BAD_REQUEST, self.to_string()), - ApiError::UserNotFound => (StatusCode::BAD_REQUEST, self.to_string()), - ApiError::IngressContentError(_) => { - (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()) - } - ApiError::RabbitMQError(_) => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()), - ApiError::FileError(_) => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()), - ApiError::TemplatingError(_) => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()), - ApiError::EmailError(_) => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()), - }; - - ( - status, - Json(json!({ - "error": error_message, + fn into_response(self) -> Response { + let (status, body) = match self { + ApiError::InternalError(message) => ( + StatusCode::INTERNAL_SERVER_ERROR, + json!({ + "error": message, "status": "error" - })), - ) - .into_response() + }), + ), + ApiError::ValidationError(message) => ( + StatusCode::BAD_REQUEST, + json!({ + "error": message, + "status": "error" + }), + ), + ApiError::NotFound(message) => ( + StatusCode::NOT_FOUND, + json!({ + "error": message, + "status": "error" + }), + ), + ApiError::Unauthorized(message) => ( + StatusCode::UNAUTHORIZED, + json!({ + "error": message, + "status": "error" + }), + ), // ... other matches + }; + (status, Json(body)).into_response() + } +} +#[derive(Clone)] +pub struct ErrorContext { + #[allow(dead_code)] + templates: Arc, +} + +impl ErrorContext { + pub fn new(templates: Arc) -> Self { + Self { templates } + } +} + +pub enum HtmlError { + ServerError(Arc), + NotFound(Arc), + Unauthorized(Arc), + BadRequest(String, Arc), + Template(String, Arc), +} + +// Implement From for HtmlError +impl HtmlError { + pub fn new(error: AppError, templates: Arc) -> Self { + match error { + AppError::NotFound(_msg) => HtmlError::NotFound(templates), + AppError::Auth(_msg) => HtmlError::Unauthorized(templates), + AppError::Validation(msg) => HtmlError::BadRequest(msg, templates), + _ => { + tracing::error!("Internal error: {:?}", error); + HtmlError::ServerError(templates) + } + } + } + + pub fn from_template_error(error: minijinja::Error, templates: Arc) -> Self { + tracing::error!("Template error: {:?}", error); + HtmlError::Template(error.to_string(), templates) + } +} + +impl IntoResponse for HtmlError { + fn into_response(self) -> Response { + let (status, context, templates) = match self { + HtmlError::ServerError(templates) | HtmlError::Template(_, templates) => ( + StatusCode::INTERNAL_SERVER_ERROR, + context! { + status_code => 500, + title => "Internal Server Error", + error => "Internal Server Error", + description => "Something went wrong on our end." + }, + templates, + ), + HtmlError::NotFound(templates) => ( + StatusCode::NOT_FOUND, + context! { + status_code => 404, + title => "Page Not Found", + error => "Not Found", + description => "The page you're looking for doesn't exist or was removed." + }, + templates, + ), + HtmlError::Unauthorized(templates) => ( + StatusCode::UNAUTHORIZED, + context! { + status_code => 401, + title => "Unauthorized", + error => "Access Denied", + description => "You need to be logged in to access this page." + }, + templates, + ), + HtmlError::BadRequest(msg, templates) => ( + StatusCode::BAD_REQUEST, + context! { + status_code => 400, + title => "Bad Request", + error => "Bad Request", + description => msg + }, + templates, + ), + }; + + let html = match templates.acquire_env() { + Ok(env) => match env.get_template("errors/error.html") { + Ok(tmpl) => match tmpl.render(context) { + Ok(output) => output, + Err(e) => { + tracing::error!("Template render error: {:?}", e); + Self::fallback_html() + } + }, + Err(e) => { + tracing::error!("Template get error: {:?}", e); + Self::fallback_html() + } + }, + Err(e) => { + tracing::error!("Environment acquire error: {:?}", e); + Self::fallback_html() + } + }; + + (status, Html(html)).into_response() + } +} + +impl HtmlError { + fn fallback_html() -> String { + r#" + + +
+

Error

+

Sorry, something went wrong displaying this page.

+
+ + + "# + .to_string() } } diff --git a/src/ingress/analysis/ingress_analyser.rs b/src/ingress/analysis/ingress_analyser.rs index f10752c..7647606 100644 --- a/src/ingress/analysis/ingress_analyser.rs +++ b/src/ingress/analysis/ingress_analyser.rs @@ -1,13 +1,16 @@ use crate::{ - error::ProcessingError, + error::AppError, ingress::analysis::prompt::{get_ingress_analysis_schema, INGRESS_ANALYSIS_SYSTEM_MESSAGE}, retrieval::combined_knowledge_entity_retrieval, storage::types::knowledge_entity::KnowledgeEntity, }; -use async_openai::types::{ - ChatCompletionRequestSystemMessage, ChatCompletionRequestUserMessage, - CreateChatCompletionRequest, CreateChatCompletionRequestArgs, ResponseFormat, - ResponseFormatJsonSchema, +use async_openai::{ + error::OpenAIError, + types::{ + ChatCompletionRequestSystemMessage, ChatCompletionRequestUserMessage, + CreateChatCompletionRequest, CreateChatCompletionRequestArgs, ResponseFormat, + ResponseFormatJsonSchema, + }, }; use serde_json::json; use surrealdb::engine::any::Any; @@ -38,7 +41,7 @@ impl<'a> IngressAnalyzer<'a> { instructions: &str, text: &str, user_id: &str, - ) -> Result { + ) -> Result { let similar_entities = self .find_similar_entities(category, instructions, text, user_id) .await?; @@ -53,7 +56,7 @@ impl<'a> IngressAnalyzer<'a> { instructions: &str, text: &str, user_id: &str, - ) -> Result, ProcessingError> { + ) -> Result, AppError> { let input_text = format!( "content: {}, category: {}, user_instructions: {}", text, category, instructions @@ -74,7 +77,7 @@ impl<'a> IngressAnalyzer<'a> { instructions: &str, text: &str, similar_entities: &[KnowledgeEntity], - ) -> Result { + ) -> Result { let entities_json = json!(similar_entities .iter() .map(|entity| { @@ -114,13 +117,12 @@ impl<'a> IngressAnalyzer<'a> { ]) .response_format(response_format) .build() - .map_err(|e| ProcessingError::LLMParsingError(e.to_string())) } async fn perform_analysis( &self, request: CreateChatCompletionRequest, - ) -> Result { + ) -> Result { let response = self.openai_client.chat().create(request).await?; debug!("Received LLM response: {:?}", response); @@ -128,12 +130,12 @@ impl<'a> IngressAnalyzer<'a> { .choices .first() .and_then(|choice| choice.message.content.as_ref()) - .ok_or(ProcessingError::LLMParsingError( - "No content found in LLM response".into(), + .ok_or(AppError::LLMParsing( + "No content found in LLM response".to_string(), )) .and_then(|content| { serde_json::from_str(content).map_err(|e| { - ProcessingError::LLMParsingError(format!( + AppError::LLMParsing(format!( "Failed to parse LLM response into analysis: {}", e )) diff --git a/src/ingress/analysis/types/llm_analysis_result.rs b/src/ingress/analysis/types/llm_analysis_result.rs index b8e3401..8b1aee5 100644 --- a/src/ingress/analysis/types/llm_analysis_result.rs +++ b/src/ingress/analysis/types/llm_analysis_result.rs @@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize}; use tokio::task; use crate::{ - error::ProcessingError, + error::AppError, storage::types::{ knowledge_entity::{KnowledgeEntity, KnowledgeEntityType}, knowledge_relationship::KnowledgeRelationship, @@ -49,13 +49,13 @@ impl LLMGraphAnalysisResult { /// /// # Returns /// - /// * `Result<(Vec, Vec), ProcessingError>` - A tuple containing vectors of `KnowledgeEntity` and `KnowledgeRelationship`. + /// * `Result<(Vec, Vec), AppError>` - A tuple containing vectors of `KnowledgeEntity` and `KnowledgeRelationship`. pub async fn to_database_entities( &self, source_id: &str, user_id: &str, openai_client: &async_openai::Client, - ) -> Result<(Vec, Vec), ProcessingError> { + ) -> Result<(Vec, Vec), AppError> { // Create mapper and pre-assign IDs let mapper = Arc::new(Mutex::new(self.create_mapper()?)); @@ -70,7 +70,7 @@ impl LLMGraphAnalysisResult { Ok((entities, relationships)) } - fn create_mapper(&self) -> Result { + fn create_mapper(&self) -> Result { let mut mapper = GraphMapper::new(); // Pre-assign all IDs @@ -87,7 +87,7 @@ impl LLMGraphAnalysisResult { user_id: &str, mapper: Arc>, openai_client: &async_openai::Client, - ) -> Result, ProcessingError> { + ) -> Result, AppError> { let futures: Vec<_> = self .knowledge_entities .iter() @@ -116,10 +116,10 @@ impl LLMGraphAnalysisResult { fn process_relationships( &self, mapper: Arc>, - ) -> Result, ProcessingError> { + ) -> Result, AppError> { let mut mapper_guard = mapper .lock() - .map_err(|_| ProcessingError::GraphProcessingError("Failed to lock mapper".into()))?; + .map_err(|_| AppError::GraphMapper("Failed to lock mapper".into()))?; self.relationships .iter() .map(|rel| { @@ -142,18 +142,15 @@ async fn create_single_entity( user_id: &str, mapper: Arc>, openai_client: &async_openai::Client, -) -> Result { +) -> Result { let assigned_id = { let mapper = mapper .lock() - .map_err(|_| ProcessingError::GraphProcessingError("Failed to lock mapper".into()))?; + .map_err(|_| AppError::GraphMapper("Failed to lock mapper".into()))?; mapper .get_id(&llm_entity.key) .ok_or_else(|| { - ProcessingError::GraphProcessingError(format!( - "ID not found for key: {}", - llm_entity.key - )) + AppError::GraphMapper(format!("ID not found for key: {}", llm_entity.key)) })? .to_string() }; diff --git a/src/ingress/content_processor.rs b/src/ingress/content_processor.rs index d38c194..963611a 100644 --- a/src/ingress/content_processor.rs +++ b/src/ingress/content_processor.rs @@ -4,7 +4,7 @@ use text_splitter::TextSplitter; use tracing::{debug, info}; use crate::{ - error::ProcessingError, + error::AppError, storage::{ db::{store_item, SurrealDbClient}, types::{ @@ -25,7 +25,7 @@ pub struct ContentProcessor { } impl ContentProcessor { - pub async fn new(app_config: &AppConfig) -> Result { + pub async fn new(app_config: &AppConfig) -> Result { Ok(Self { db_client: SurrealDbClient::new( &app_config.surrealdb_address, @@ -39,7 +39,7 @@ impl ContentProcessor { }) } - pub async fn process(&self, content: &TextContent) -> Result<(), ProcessingError> { + pub async fn process(&self, content: &TextContent) -> Result<(), AppError> { // Store original content store_item(&self.db_client, content.clone()).await?; @@ -72,7 +72,7 @@ impl ContentProcessor { async fn perform_semantic_analysis( &self, content: &TextContent, - ) -> Result { + ) -> Result { let analyser = IngressAnalyzer::new(&self.db_client, &self.openai_client); analyser .analyze_content( @@ -88,7 +88,7 @@ impl ContentProcessor { &self, entities: Vec, relationships: Vec, - ) -> Result<(), ProcessingError> { + ) -> Result<(), AppError> { for entity in &entities { debug!("Storing entity: {:?}", entity); store_item(&self.db_client, entity.clone()).await?; @@ -107,7 +107,7 @@ impl ContentProcessor { Ok(()) } - async fn store_vector_chunks(&self, content: &TextContent) -> Result<(), ProcessingError> { + async fn store_vector_chunks(&self, content: &TextContent) -> Result<(), AppError> { let splitter = TextSplitter::new(500..2000); let chunks = splitter.chunks(&content.text); diff --git a/src/ingress/types/ingress_input.rs b/src/ingress/types/ingress_input.rs index ed09cfa..37288bf 100644 --- a/src/ingress/types/ingress_input.rs +++ b/src/ingress/types/ingress_input.rs @@ -1,10 +1,12 @@ use super::ingress_object::IngressObject; -use crate::storage::{ - db::{get_item, SurrealDbClient}, - types::file_info::FileInfo, +use crate::{ + error::AppError, + storage::{ + db::{get_item, SurrealDbClient}, + types::file_info::FileInfo, + }, }; use serde::{Deserialize, Serialize}; -use thiserror::Error; use tracing::info; use url::Url; @@ -17,34 +19,6 @@ pub struct IngressInput { pub files: Option>, } -/// Error types for processing ingress content. -#[derive(Error, Debug)] -pub enum IngressContentError { - #[error("IO error occurred: {0}")] - Io(#[from] std::io::Error), - - #[error("UTF-8 conversion error: {0}")] - Utf8(#[from] std::string::FromUtf8Error), - - #[error("SurrealDb error: {0}")] - SurrealDbError(#[from] surrealdb::Error), - - #[error("MIME type detection failed for input: {0}")] - MimeDetection(String), - - #[error("Unsupported MIME type: {0}")] - UnsupportedMime(String), - - #[error("URL parse error: {0}")] - UrlParse(#[from] url::ParseError), - - #[error("UUID parse error: {0}")] - UuidParse(#[from] uuid::Error), - - #[error("Redis error: {0}")] - RedisError(String), -} - /// Function to create ingress objects from input. /// /// # Arguments @@ -57,7 +31,7 @@ pub async fn create_ingress_objects( input: IngressInput, db_client: &SurrealDbClient, user_id: &str, -) -> Result, IngressContentError> { +) -> Result, AppError> { // Initialize list let mut object_list = Vec::new(); @@ -103,7 +77,7 @@ pub async fn create_ingress_objects( // If no objects are constructed, we return Err if object_list.is_empty() { - return Err(IngressContentError::MimeDetection( + return Err(AppError::NotFound( "No valid content or files provided".into(), )); } diff --git a/src/ingress/types/ingress_object.rs b/src/ingress/types/ingress_object.rs index 0c35f45..e7dc55b 100644 --- a/src/ingress/types/ingress_object.rs +++ b/src/ingress/types/ingress_object.rs @@ -1,8 +1,9 @@ -use crate::storage::types::{file_info::FileInfo, text_content::TextContent}; +use crate::{ + error::AppError, + storage::types::{file_info::FileInfo, text_content::TextContent}, +}; use serde::{Deserialize, Serialize}; -use super::ingress_input::IngressContentError; - /// Knowledge object type, containing the content or reference to it, as well as metadata #[derive(Debug, Serialize, Deserialize, Clone)] pub enum IngressObject { @@ -34,7 +35,7 @@ impl IngressObject { /// /// # Returns /// `TextContent` - An object containing a text representation of the object, could be a scraped URL, parsed PDF, etc. - pub async fn to_text_content(&self) -> Result { + pub async fn to_text_content(&self) -> Result { match self { IngressObject::Url { url, @@ -82,12 +83,12 @@ impl IngressObject { } /// Fetches and extracts text from a URL. - async fn fetch_text_from_url(_url: &str) -> Result { + async fn fetch_text_from_url(_url: &str) -> Result { unimplemented!() } /// Extracts text from a file based on its MIME type. - async fn extract_text_from_file(file_info: &FileInfo) -> Result { + async fn extract_text_from_file(file_info: &FileInfo) -> Result { match file_info.mime_type.as_str() { "text/plain" => { // Read the file and return its content @@ -101,15 +102,11 @@ impl IngressObject { } "application/pdf" => { // TODO: Implement PDF text extraction using a crate like `pdf-extract` or `lopdf` - Err(IngressContentError::UnsupportedMime( - file_info.mime_type.clone(), - )) + Err(AppError::NotFound(file_info.mime_type.clone())) } "image/png" | "image/jpeg" => { // TODO: Implement OCR on image using a crate like `tesseract` - Err(IngressContentError::UnsupportedMime( - file_info.mime_type.clone(), - )) + Err(AppError::NotFound(file_info.mime_type.clone())) } "application/octet-stream" => { let content = tokio::fs::read_to_string(&file_info.path).await?; @@ -120,9 +117,7 @@ impl IngressObject { Ok(content) } // Handle other MIME types as needed - _ => Err(IngressContentError::UnsupportedMime( - file_info.mime_type.clone(), - )), + _ => Err(AppError::NotFound(file_info.mime_type.clone())), } } } diff --git a/src/rabbitmq/mod.rs b/src/rabbitmq/mod.rs index 38f0e89..5000ee2 100644 --- a/src/rabbitmq/mod.rs +++ b/src/rabbitmq/mod.rs @@ -9,8 +9,6 @@ use lapin::{ use thiserror::Error; use tracing::debug; -use crate::error::ProcessingError; - /// Possible errors related to RabbitMQ operations. #[derive(Error, Debug)] pub enum RabbitMQError { @@ -28,8 +26,6 @@ pub enum RabbitMQError { InitializeConsumerError(String), #[error("Queue error: {0}")] QueueError(String), - #[error("Processing error: {0}")] - ProcessingError(#[from] ProcessingError), } /// Struct containing the information required to set up a client and connection. diff --git a/src/retrieval/mod.rs b/src/retrieval/mod.rs index 9a79d32..ca0f620 100644 --- a/src/retrieval/mod.rs +++ b/src/retrieval/mod.rs @@ -4,7 +4,7 @@ pub mod query_helper_prompt; pub mod vector; use crate::{ - error::ProcessingError, + error::AppError, retrieval::{ graph::{find_entities_by_relationship_by_id, find_entities_by_source_ids}, vector::find_items_by_vector_similarity, @@ -34,14 +34,14 @@ use surrealdb::{engine::any::Any, Surreal}; /// * 'user_id' - The user id of the current user /// /// # Returns -/// * `Result, ProcessingError>` - A deduplicated vector of relevant +/// * `Result, AppError>` - A deduplicated vector of relevant /// knowledge entities, or an error if the retrieval process fails pub async fn combined_knowledge_entity_retrieval( db_client: &Surreal, openai_client: &async_openai::Client, query: &str, user_id: &str, -) -> Result, ProcessingError> { +) -> Result, AppError> { // info!("Received input: {:?}", query); let (items_from_knowledge_entity_similarity, closest_chunks) = try_join( diff --git a/src/retrieval/query_helper.rs b/src/retrieval/query_helper.rs index 1c83f19..56f95ab 100644 --- a/src/retrieval/query_helper.rs +++ b/src/retrieval/query_helper.rs @@ -1,14 +1,17 @@ -use async_openai::types::{ - ChatCompletionRequestSystemMessage, ChatCompletionRequestUserMessage, - CreateChatCompletionRequest, CreateChatCompletionRequestArgs, CreateChatCompletionResponse, - ResponseFormat, ResponseFormatJsonSchema, +use async_openai::{ + error::OpenAIError, + types::{ + ChatCompletionRequestSystemMessage, ChatCompletionRequestUserMessage, + CreateChatCompletionRequest, CreateChatCompletionRequestArgs, CreateChatCompletionResponse, + ResponseFormat, ResponseFormatJsonSchema, + }, }; use serde::Deserialize; use serde_json::{json, Value}; use tracing::debug; use crate::{ - error::ApiError, + error::AppError, retrieval::combined_knowledge_entity_retrieval, storage::{db::SurrealDbClient, types::knowledge_entity::KnowledgeEntity}, }; @@ -94,7 +97,7 @@ pub async fn get_answer_with_references( openai_client: &async_openai::Client, query: &str, user_id: &str, -) -> Result { +) -> Result { let entities = combined_knowledge_entity_retrieval(surreal_db_client, openai_client, query, user_id) .await?; @@ -104,11 +107,7 @@ pub async fn get_answer_with_references( let user_message = create_user_message(&entities_json, query); let request = create_chat_request(user_message)?; - let response = openai_client - .chat() - .create(request) - .await - .map_err(|e| ApiError::QueryError(e.to_string()))?; + let response = openai_client.chat().create(request).await?; let llm_response = process_llm_response(response).await?; @@ -152,7 +151,9 @@ pub fn create_user_message(entities_json: &Value, query: &str) -> String { ) } -pub fn create_chat_request(user_message: String) -> Result { +pub fn create_chat_request( + user_message: String, +) -> Result { let response_format = ResponseFormat::JsonSchema { json_schema: ResponseFormatJsonSchema { description: Some("Query answering AI".into()), @@ -172,22 +173,21 @@ pub fn create_chat_request(user_message: String) -> Result Result { +) -> Result { response .choices .first() .and_then(|choice| choice.message.content.as_ref()) - .ok_or(ApiError::QueryError( + .ok_or(AppError::LLMParsing( "No content found in LLM response".into(), )) .and_then(|content| { serde_json::from_str::(content).map_err(|e| { - ApiError::QueryError(format!("Failed to parse LLM response into analysis: {}", e)) + AppError::LLMParsing(format!("Failed to parse LLM response into analysis: {}", e)) }) }) } diff --git a/src/retrieval/vector.rs b/src/retrieval/vector.rs index 38ee87d..fd4efde 100644 --- a/src/retrieval/vector.rs +++ b/src/retrieval/vector.rs @@ -1,6 +1,6 @@ use surrealdb::{engine::any::Any, Surreal}; -use crate::{error::ProcessingError, utils::embedding::generate_embedding}; +use crate::{error::AppError, utils::embedding::generate_embedding}; /// Compares vectors and retrieves a number of items from the specified table. /// @@ -30,7 +30,7 @@ pub async fn find_items_by_vector_similarity( table: String, openai_client: &async_openai::Client, user_id: &str, -) -> Result, ProcessingError> +) -> Result, AppError> where T: for<'de> serde::Deserialize<'de>, { diff --git a/src/server/middleware_api_auth.rs b/src/server/middleware_api_auth.rs index 324ba07..6f05823 100644 --- a/src/server/middleware_api_auth.rs +++ b/src/server/middleware_api_auth.rs @@ -13,10 +13,14 @@ pub async fn api_auth( mut request: Request, next: Next, ) -> Result { - let api_key = extract_api_key(&request).ok_or(ApiError::AuthRequired)?; + let api_key = extract_api_key(&request).ok_or(ApiError::Unauthorized( + "You have to be authenticated".to_string(), + ))?; let user = User::find_by_api_key(&api_key, &state.surreal_db_client).await?; - let user = user.ok_or(ApiError::UserNotFound)?; + let user = user.ok_or(ApiError::Unauthorized( + "You have to be authenticated".to_string(), + ))?; request.extensions_mut().insert(user); diff --git a/src/server/routes/api/file.rs b/src/server/routes/api/file.rs index 8b6fb15..f3a406a 100644 --- a/src/server/routes/api/file.rs +++ b/src/server/routes/api/file.rs @@ -1,4 +1,8 @@ -use crate::{error::ApiError, server::AppState, storage::types::file_info::FileInfo}; +use crate::{ + error::{ApiError, AppError}, + server::AppState, + storage::types::file_info::FileInfo, +}; use axum::{extract::State, response::IntoResponse, Json}; use axum_typed_multipart::{FieldData, TryFromMultipart, TypedMultipart}; use serde_json::json; @@ -21,7 +25,9 @@ pub async fn upload_handler( info!("Received an upload request"); // Process the file upload - let file_info = FileInfo::new(input.file, &state.surreal_db_client).await?; + let file_info = FileInfo::new(input.file, &state.surreal_db_client) + .await + .map_err(AppError::from)?; // Prepare the response JSON let response = json!({ diff --git a/src/server/routes/api/ingress.rs b/src/server/routes/api/ingress.rs index eb33f25..0ed434c 100644 --- a/src/server/routes/api/ingress.rs +++ b/src/server/routes/api/ingress.rs @@ -1,5 +1,5 @@ use crate::{ - error::ApiError, + error::{ApiError, AppError}, ingress::types::ingress_input::{create_ingress_objects, IngressInput}, server::AppState, storage::types::user::User, @@ -22,7 +22,7 @@ pub async fn ingress_handler( .map(|object| state.rabbitmq_producer.publish(object)) .collect(); - try_join_all(futures).await?; + try_join_all(futures).await.map_err(AppError::from)?; Ok(StatusCode::OK) } diff --git a/src/server/routes/api/queue_length.rs b/src/server/routes/api/queue_length.rs index d28db94..d205de0 100644 --- a/src/server/routes/api/queue_length.rs +++ b/src/server/routes/api/queue_length.rs @@ -1,21 +1,28 @@ use axum::{extract::State, http::StatusCode, response::IntoResponse}; -use minijinja::context; -use tracing::{info, Instrument}; +use tracing::info; -use crate::{error::ApiError, server::AppState}; +use crate::{ + error::{ApiError, AppError}, + server::AppState, +}; pub async fn queue_length_handler( State(state): State, ) -> Result { info!("Getting queue length"); - let queue_length = state.rabbitmq_consumer.get_queue_length().await?; + let queue_length = state + .rabbitmq_consumer + .get_queue_length() + .await + .map_err(AppError::from)?; info!("Queue length: {}", queue_length); state .mailer - .send_email_verification("per@starks.cloud", "1001010", &state.templates)?; + .send_email_verification("per@starks.cloud", "1001010", &state.templates) + .map_err(AppError::from)?; // Return the queue length with a 200 OK status Ok((StatusCode::OK, queue_length.to_string())) diff --git a/src/server/routes/html/account.rs b/src/server/routes/html/account.rs index e25cff3..8545ec4 100644 --- a/src/server/routes/html/account.rs +++ b/src/server/routes/html/account.rs @@ -1,7 +1,7 @@ use axum::{ extract::State, - http::{Response, StatusCode, Uri}, - response::{Html, IntoResponse, Redirect}, + http::{StatusCode, Uri}, + response::{IntoResponse, Redirect}, }; use axum_htmx::HxRedirect; use axum_session_auth::AuthSession; @@ -9,7 +9,7 @@ use axum_session_surreal::SessionSurrealPool; use surrealdb::{engine::any::Any, Surreal}; use crate::{ - error::ApiError, + error::{AppError, HtmlError}, page_data, server::{routes::html::render_template, AppState}, storage::{db::delete_item, types::user::User}, @@ -24,7 +24,7 @@ page_data!(AccountData, "auth/account.html", { pub async fn show_account_page( State(state): State, auth: AuthSession, Surreal>, -) -> Result { +) -> Result { // Early return if the user is not authenticated let user = match auth.current_user { Some(user) => user, @@ -34,8 +34,9 @@ pub async fn show_account_page( let output = render_template( AccountData::template_name(), AccountData { user }, - state.templates, - )?; + state.templates.clone(), + ) + .map_err(|e| HtmlError::new(AppError::from(e), state.templates.clone()))?; Ok(output.into_response()) } @@ -43,12 +44,17 @@ pub async fn show_account_page( pub async fn set_api_key( State(state): State, auth: AuthSession, Surreal>, -) -> Result { +) -> Result { // Early return if the user is not authenticated - let user = auth.current_user.as_ref().ok_or(ApiError::AuthRequired)?; + let user = match &auth.current_user { + Some(user) => user, + None => return Ok(Redirect::to("/").into_response()), + }; // Generate and set the API key - let api_key = User::set_api_key(&user.id, &state.surreal_db_client).await?; + let api_key = User::set_api_key(&user.id, &state.surreal_db_client) + .await + .map_err(|e| HtmlError::new(e, state.templates.clone()))?; auth.cache_clear_user(user.id.to_string()); @@ -63,8 +69,9 @@ pub async fn set_api_key( AccountData::template_name(), "api_key_section", AccountData { user: updated_user }, - state.templates, - )?; + state.templates.clone(), + ) + .map_err(|e| HtmlError::new(AppError::from(e), state.templates.clone()))?; Ok(output.into_response()) } @@ -72,11 +79,16 @@ pub async fn set_api_key( pub async fn delete_account( State(state): State, auth: AuthSession, Surreal>, -) -> Result { +) -> Result { // Early return if the user is not authenticated - let user = auth.current_user.as_ref().ok_or(ApiError::AuthRequired)?; + let user = match &auth.current_user { + Some(user) => user, + None => return Ok(Redirect::to("/").into_response()), + }; - delete_item::(&state.surreal_db_client, &user.id).await?; + delete_item::(&state.surreal_db_client, &user.id) + .await + .map_err(|e| HtmlError::new(AppError::from(e), state.templates.clone()))?; auth.logout_user(); diff --git a/src/server/routes/html/index.rs b/src/server/routes/html/index.rs index d639879..6272976 100644 --- a/src/server/routes/html/index.rs +++ b/src/server/routes/html/index.rs @@ -1,11 +1,11 @@ -use axum::{extract::State, response::Html}; +use axum::{extract::State, response::IntoResponse}; use axum_session_auth::AuthSession; use axum_session_surreal::SessionSurrealPool; use surrealdb::{engine::any::Any, Surreal}; use tracing::info; use crate::{ - error::ApiError, + error::{AppError, HtmlError}, page_data, server::{routes::html::render_template, AppState}, storage::types::user::User, @@ -19,10 +19,14 @@ page_data!(IndexData, "index/index.html", { pub async fn index_handler( State(state): State, auth: AuthSession, Surreal>, -) -> Result, ApiError> { +) -> Result { info!("Displaying index page"); - let queue_length = state.rabbitmq_consumer.get_queue_length().await?; + let queue_length = state + .rabbitmq_consumer + .get_queue_length() + .await + .map_err(|e| HtmlError::new(AppError::from(e), state.templates.clone()))?; // let knowledge_entities = User::get_knowledge_entities( // &auth.current_user.clone().unwrap().id, @@ -38,8 +42,9 @@ pub async fn index_handler( queue_length, user: auth.current_user, }, - state.templates, - )?; + state.templates.clone(), + ) + .map_err(|e| HtmlError::new(AppError::from(e), state.templates.clone()))?; - Ok(output) + Ok(output.into_response()) } diff --git a/src/server/routes/html/ingress.rs b/src/server/routes/html/ingress.rs index 433ed58..dcae272 100644 --- a/src/server/routes/html/ingress.rs +++ b/src/server/routes/html/ingress.rs @@ -1,25 +1,22 @@ use axum::{ extract::State, - http::{StatusCode, Uri}, response::{Html, IntoResponse, Redirect}, - Form, }; -use axum_htmx::{HxBoosted, HxRedirect}; use axum_session_auth::AuthSession; use axum_session_surreal::SessionSurrealPool; use axum_typed_multipart::{FieldData, TryFromMultipart, TypedMultipart}; -use serde::{Deserialize, Serialize}; +use serde::Serialize; use surrealdb::{engine::any::Any, Surreal}; use tempfile::NamedTempFile; use tracing::info; use crate::{ - error::ApiError, + error::{AppError, HtmlError}, server::AppState, storage::types::{file_info::FileInfo, user::User}, }; -use super::{render_block, render_template}; +use super::render_template; #[derive(Serialize)] struct PageData { @@ -29,13 +26,17 @@ struct PageData { pub async fn show_ingress_form( State(state): State, auth: AuthSession, Surreal>, -) -> Result { +) -> Result { if !auth.is_authenticated() { return Ok(Redirect::to("/").into_response()); } - Ok(render_template("ingress_form.html", PageData {}, state.templates)?.into_response()) + let output = render_template("ingress_form.html", PageData {}, state.templates.clone()) + .map_err(|e| HtmlError::new(AppError::from(e), state.templates.clone()))?; + + Ok(output.into_response()) } + #[derive(Debug, TryFromMultipart)] pub struct IngressParams { pub content: Option, @@ -49,8 +50,8 @@ pub async fn process_ingress_form( State(state): State, auth: AuthSession, Surreal>, TypedMultipart(input): TypedMultipart, -) -> Result { - let user = match auth.current_user { +) -> Result { + let _user = match auth.current_user { Some(user) => user, None => return Ok(Redirect::to("/").into_response()), }; @@ -60,7 +61,9 @@ pub async fn process_ingress_form( // Process files and create FileInfo objects let mut file_infos = Vec::new(); for file in input.files { - let file_info = FileInfo::new(file, &state.surreal_db_client).await?; + let file_info = FileInfo::new(file, &state.surreal_db_client) + .await + .map_err(|e| HtmlError::new(AppError::from(e), state.templates.clone()))?; file_infos.push(file_info); } diff --git a/src/server/routes/html/search_result.rs b/src/server/routes/html/search_result.rs index 76cb90d..8d5ef0d 100644 --- a/src/server/routes/html/search_result.rs +++ b/src/server/routes/html/search_result.rs @@ -1,6 +1,6 @@ use axum::{ extract::{Query, State}, - response::Html, + response::{Html, IntoResponse, Redirect}, }; use axum_session_auth::AuthSession; use axum_session_surreal::SessionSurrealPool; @@ -9,7 +9,7 @@ use surrealdb::{engine::any::Any, Surreal}; use tracing::info; use crate::{ - error::ApiError, retrieval::query_helper::get_answer_with_references, server::AppState, + error::HtmlError, retrieval::query_helper::get_answer_with_references, server::AppState, storage::types::user::User, }; #[derive(Deserialize)] @@ -21,30 +21,22 @@ pub async fn search_result_handler( State(state): State, Query(query): Query, auth: AuthSession, Surreal>, -) -> Result, ApiError> { +) -> Result { info!("Displaying search results"); - let user_id = auth.current_user.ok_or_else(|| ApiError::AuthRequired)?.id; + let user = match auth.current_user { + Some(user) => user, + None => return Ok(Redirect::to("/").into_response()), + }; let answer = get_answer_with_references( &state.surreal_db_client, &state.openai_client, &query.query, - &user_id, + &user.id, ) - .await?; + .await + .map_err(|e| HtmlError::new(e, state.templates.clone()))?; - Ok(Html(answer.content)) - // let output = state - // .tera - // .render( - // "search_result.html", - // &Context::from_value( - // json!({"result": answer.content, "references": answer.references}), - // ) - // .unwrap(), - // ) - // .unwrap(); - - // Ok(output.into()) + Ok(Html(answer.content).into_response()) } diff --git a/src/server/routes/html/signin.rs b/src/server/routes/html/signin.rs index 37de68f..d7dc774 100644 --- a/src/server/routes/html/signin.rs +++ b/src/server/routes/html/signin.rs @@ -9,7 +9,12 @@ use axum_session_auth::AuthSession; use axum_session_surreal::SessionSurrealPool; use surrealdb::{engine::any::Any, Surreal}; -use crate::{error::ApiError, page_data, server::AppState, storage::types::user::User}; +use crate::{ + error::{AppError, HtmlError}, + page_data, + server::AppState, + storage::types::user::User, +}; use super::{render_block, render_template}; @@ -26,7 +31,7 @@ pub async fn show_signin_form( State(state): State, auth: AuthSession, Surreal>, HxBoosted(boosted): HxBoosted, -) -> Result { +) -> Result { if auth.is_authenticated() { return Ok(Redirect::to("/").into_response()); } @@ -35,13 +40,15 @@ pub async fn show_signin_form( ShowSignInForm::template_name(), "body", ShowSignInForm {}, - state.templates, - )?, + state.templates.clone(), + ) + .map_err(|e| HtmlError::new(AppError::from(e), state.templates.clone()))?, false => render_template( ShowSignInForm::template_name(), ShowSignInForm {}, - state.templates, - )?, + state.templates.clone(), + ) + .map_err(|e| HtmlError::new(AppError::from(e), state.templates.clone()))?, }; Ok(output.into_response()) @@ -51,11 +58,11 @@ pub async fn authenticate_user( State(state): State, auth: AuthSession, Surreal>, Form(form): Form, -) -> Result { +) -> Result { let user = match User::authenticate(form.email, form.password, &state.surreal_db_client).await { Ok(user) => user, Err(_) => { - return Ok(Html("

Invalid email or password.

").into_response()); + return Ok(Html("

Incorrect email or password

").into_response()); } }; diff --git a/src/server/routes/html/signup.rs b/src/server/routes/html/signup.rs index 5534886..73cc0f6 100644 --- a/src/server/routes/html/signup.rs +++ b/src/server/routes/html/signup.rs @@ -10,7 +10,11 @@ use axum_session_surreal::SessionSurrealPool; use serde::{Deserialize, Serialize}; use surrealdb::{engine::any::Any, Surreal}; -use crate::{error::ApiError, server::AppState, storage::types::user::User}; +use crate::{ + error::{AppError, HtmlError}, + server::AppState, + storage::types::user::User, +}; use super::{render_block, render_template}; @@ -29,7 +33,7 @@ pub async fn show_signup_form( State(state): State, auth: AuthSession, Surreal>, HxBoosted(boosted): HxBoosted, -) -> Result { +) -> Result { if auth.is_authenticated() { return Ok(Redirect::to("/").into_response()); } @@ -38,9 +42,15 @@ pub async fn show_signup_form( "auth/signup_form.html", "body", PageData {}, - state.templates, - )?, - false => render_template("auth/signup_form.html", PageData {}, state.templates)?, + state.templates.clone(), + ) + .map_err(|e| HtmlError::new(AppError::from(e), state.templates.clone()))?, + false => render_template( + "auth/signup_form.html", + PageData {}, + state.templates.clone(), + ) + .map_err(|e| HtmlError::new(AppError::from(e), state.templates.clone()))?, }; Ok(output.into_response()) @@ -50,7 +60,7 @@ pub async fn process_signup_and_show_verification( State(state): State, auth: AuthSession, Surreal>, Form(form): Form, -) -> Result { +) -> Result { let user = match User::create_new(form.email, form.password, &state.surreal_db_client).await { Ok(user) => user, Err(_) => { diff --git a/src/storage/types/knowledge_relationship.rs b/src/storage/types/knowledge_relationship.rs index a81c19a..093c28d 100644 --- a/src/storage/types/knowledge_relationship.rs +++ b/src/storage/types/knowledge_relationship.rs @@ -1,4 +1,4 @@ -use crate::{error::ProcessingError, stored_object}; +use crate::{error::AppError, stored_object}; use surrealdb::{engine::any::Any, Surreal}; use tracing::debug; use uuid::Uuid; @@ -26,10 +26,7 @@ impl KnowledgeRelationship { metadata, } } - pub async fn store_relationship( - &self, - db_client: &Surreal, - ) -> Result<(), ProcessingError> { + pub async fn store_relationship(&self, db_client: &Surreal) -> Result<(), AppError> { let query = format!( "RELATE knowledge_entity:`{}` -> relates_to -> knowledge_entity:`{}`", self.in_, self.out diff --git a/src/storage/types/user.rs b/src/storage/types/user.rs index 82b1958..a200bab 100644 --- a/src/storage/types/user.rs +++ b/src/storage/types/user.rs @@ -1,5 +1,5 @@ use crate::{ - error::ApiError, + error::AppError, storage::db::{get_item, SurrealDbClient}, stored_object, }; @@ -41,10 +41,10 @@ impl User { email: String, password: String, db: &SurrealDbClient, - ) -> Result { + ) -> Result { // Check if user exists if (Self::find_by_email(&email, db).await?).is_some() { - return Err(ApiError::UserAlreadyExists); + return Err(AppError::Auth("User already exists".into())); } let id = Uuid::new_v4().to_string(); @@ -62,14 +62,14 @@ impl User { .await? .take(0)?; - user.ok_or(ApiError::UserAlreadyExists) + user.ok_or(AppError::Auth("User failed to create".into())) } pub async fn authenticate( email: String, password: String, db: &SurrealDbClient, - ) -> Result { + ) -> Result { let user: Option = db .client .query( @@ -81,13 +81,13 @@ impl User { .bind(("password", password)) .await? .take(0)?; - user.ok_or(ApiError::UserAlreadyExists) + user.ok_or(AppError::Auth("User failed to authenticate".into())) } pub async fn find_by_email( email: &str, db: &SurrealDbClient, - ) -> Result, ApiError> { + ) -> Result, AppError> { let user: Option = db .client .query("SELECT * FROM user WHERE email = $email LIMIT 1") @@ -101,7 +101,7 @@ impl User { pub async fn find_by_api_key( api_key: &str, db: &SurrealDbClient, - ) -> Result, ApiError> { + ) -> Result, AppError> { let user: Option = db .client .query("SELECT * FROM user WHERE api_key = $api_key LIMIT 1") @@ -112,7 +112,7 @@ impl User { Ok(user) } - pub async fn set_api_key(id: &str, db: &SurrealDbClient) -> Result { + pub async fn set_api_key(id: &str, db: &SurrealDbClient) -> Result { // Generate a secure random API key let api_key = format!("sk_{}", Uuid::new_v4().to_string().replace("-", "")); @@ -133,11 +133,11 @@ impl User { if user.is_some() { Ok(api_key) } else { - Err(ApiError::UserNotFound) + Err(AppError::Auth("User not found".into())) } } - pub async fn revoke_api_key(id: &str, db: &SurrealDbClient) -> Result<(), ApiError> { + pub async fn revoke_api_key(id: &str, db: &SurrealDbClient) -> Result<(), AppError> { let user: Option = db .client .query( @@ -152,14 +152,14 @@ impl User { if user.is_some() { Ok(()) } else { - Err(ApiError::UserNotFound) + Err(AppError::Auth("User was not found".into())) } } pub async fn get_knowledge_entities( id: &str, db: &SurrealDbClient, - ) -> Result, ApiError> { + ) -> Result, AppError> { let entities: Vec = db .client .query("SELECT * FROM knowledge_entity WHERE user_id = $user_id") diff --git a/src/utils/embedding.rs b/src/utils/embedding.rs index ca7e486..d1a0cdd 100644 --- a/src/utils/embedding.rs +++ b/src/utils/embedding.rs @@ -1,7 +1,6 @@ use async_openai::types::CreateEmbeddingRequestArgs; -use crate::error::ProcessingError; - +use crate::error::AppError; /// Generates an embedding vector for the given input text using OpenAI's embedding model. /// /// This function takes a text input and converts it into a numerical vector representation (embedding) @@ -21,14 +20,14 @@ use crate::error::ProcessingError; /// /// # Errors /// -/// This function can return a `ProcessingError` in the following cases: +/// This function can return a `AppError` in the following cases: /// * If the OpenAI API request fails /// * If the request building fails /// * If no embedding data is received in the response pub async fn generate_embedding( client: &async_openai::Client, input: &str, -) -> Result, ProcessingError> { +) -> Result, AppError> { let request = CreateEmbeddingRequestArgs::default() .model("text-embedding-3-small") .input([input]) @@ -41,7 +40,7 @@ pub async fn generate_embedding( let embedding: Vec = response .data .first() - .ok_or_else(|| ProcessingError::EmbeddingError("No embedding data received".into()))? + .ok_or_else(|| AppError::LLMParsing("No embedding data received".into()))? .embedding .clone(); diff --git a/templates/errors/error.html b/templates/errors/error.html new file mode 100644 index 0000000..b1add1b --- /dev/null +++ b/templates/errors/error.html @@ -0,0 +1,14 @@ +{% extends "body_base.html" %} + +{% block main %} +
+
+

+ {{ status_code }} +

+

{{ error }}

+

{{ description }}

+ Go Home +
+
+{% endblock %} \ No newline at end of file