From 943ae1b3a328b4776f956df6fcb2d661f22cbe9e Mon Sep 17 00:00:00 2001 From: Darko Gjorgjijoski Date: Wed, 5 Aug 2026 20:47:42 +0200 Subject: [PATCH 1/3] feat: extract the AI assistant module --- .github/workflows/ci.yml | 47 + .github/workflows/release.yml | 16 + .gitignore | 9 + AGENTS.md | 10 + LICENSE | 661 ++++ README.md | 26 + app/Application/AiAssistantService.php | 322 ++ app/Application/AiConfigurationService.php | 169 + app/Application/AiTextGenerationService.php | 89 + app/Application/AiToolRegistry.php | 55 + app/Application/Tools/AiTool.php | 129 + .../Tools/Concerns/ResolvesPeriod.php | 30 + app/Application/Tools/GetCompanyStatsTool.php | 49 + app/Application/Tools/GetCustomerTool.php | 39 + app/Application/Tools/GetInvoiceTool.php | 39 + .../Tools/ListExpenseCategoriesTool.php | 37 + .../Tools/ListOverdueInvoicesTool.php | 39 + .../Tools/ListRecentPaymentsTool.php | 50 + .../Tools/RankExpenseCategoriesTool.php | 54 + .../Tools/RankTopCustomersTool.php | 61 + app/Application/Tools/RankTopItemsTool.php | 61 + app/Application/Tools/SearchCustomersTool.php | 43 + app/Application/Tools/SearchInvoicesTool.php | 43 + app/Application/Tools/SearchItemsTool.php | 43 + app/Drivers/AiDriverFactory.php | 30 + app/Drivers/OpenRouterDriver.php | 234 ++ app/Http/Admin/AiConfigurationController.php | 46 + app/Http/Admin/CapabilitiesController.php | 23 + app/Http/AiConfigurationController.php | 51 + app/Http/Company/CapabilitiesController.php | 30 + app/Http/Company/ChatController.php | 41 + .../CompanyAiConfigurationController.php | 41 + app/Http/Company/ConversationController.php | 51 + app/Http/Company/GenerationController.php | 29 + app/Http/ModuleController.php | 16 + app/Http/Requests/AiConfigurationRequest.php | 34 + app/Http/Requests/ChatRequest.php | 20 + app/Http/Requests/ConnectionTestRequest.php | 23 + app/Http/Requests/GenerationRequest.php | 20 + .../Requests/RenameConversationRequest.php | 20 + app/Lifecycle/DataCleanup.php | 24 + app/Models/AiConversation.php | 28 + app/Models/AiMessage.php | 53 + app/Prompting/PromptLoader.php | 55 + app/Providers/AiAssistantServiceProvider.php | 99 + app/Rules/PublicHttpUrl.php | 19 + app/Support/Net/BlockedUrlException.php | 9 + app/Support/Net/PrivateNetworkGuard.php | 128 + composer.json | 49 + ...e_ai_conversations_and_messages_tables.php | 59 + dist/init.js | 3059 +++++++++++++++++ dist/style.css | 3 + eslint.config.js | 22 + module.json | 39 + package.json | 27 + phpunit.xml.dist | 12 + pnpm-lock.yaml | 2228 ++++++++++++ resources/ai/prompts/chat-system.md | 11 + resources/ai/prompts/text-generation.md | 4 + resources/css/module.css | 28 + resources/js/components/AiChatOverlay.vue | 171 + resources/js/components/AiHeaderAction.vue | 12 + resources/js/components/AiTextAction.vue | 94 + resources/js/composables/useAiChat.ts | 125 + resources/js/global.d.ts | 13 + resources/js/init.ts | 92 + resources/js/pages/AiConfigurationPage.vue | 180 + resources/js/shims-css.d.ts | 1 + resources/js/shims-vue.d.ts | 7 + resources/js/types/ai.ts | 61 + routes/api.php | 33 + tests/Feature/AiAssistantFlowTest.php | 415 +++ tests/Unit/AiConfigurationServiceTest.php | 66 + tests/Unit/AiDriverFactoryAndRegistryTest.php | 76 + tests/Unit/DataCleanupTest.php | 58 + tests/Unit/ModuleRoutesTest.php | 51 + tests/Unit/OpenRouterDriverTest.php | 39 + tests/Unit/PrivateNetworkGuardTest.php | 59 + tests/Unit/PromptLoaderTest.php | 19 + tests/Unit/ToolDelegationTest.php | 125 + tests/bootstrap.php | 19 + tsconfig.json | 16 + vite.config.js | 64 + 83 files changed, 10582 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100644 AGENTS.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 app/Application/AiAssistantService.php create mode 100644 app/Application/AiConfigurationService.php create mode 100644 app/Application/AiTextGenerationService.php create mode 100644 app/Application/AiToolRegistry.php create mode 100644 app/Application/Tools/AiTool.php create mode 100644 app/Application/Tools/Concerns/ResolvesPeriod.php create mode 100644 app/Application/Tools/GetCompanyStatsTool.php create mode 100644 app/Application/Tools/GetCustomerTool.php create mode 100644 app/Application/Tools/GetInvoiceTool.php create mode 100644 app/Application/Tools/ListExpenseCategoriesTool.php create mode 100644 app/Application/Tools/ListOverdueInvoicesTool.php create mode 100644 app/Application/Tools/ListRecentPaymentsTool.php create mode 100644 app/Application/Tools/RankExpenseCategoriesTool.php create mode 100644 app/Application/Tools/RankTopCustomersTool.php create mode 100644 app/Application/Tools/RankTopItemsTool.php create mode 100644 app/Application/Tools/SearchCustomersTool.php create mode 100644 app/Application/Tools/SearchInvoicesTool.php create mode 100644 app/Application/Tools/SearchItemsTool.php create mode 100644 app/Drivers/AiDriverFactory.php create mode 100644 app/Drivers/OpenRouterDriver.php create mode 100644 app/Http/Admin/AiConfigurationController.php create mode 100644 app/Http/Admin/CapabilitiesController.php create mode 100644 app/Http/AiConfigurationController.php create mode 100644 app/Http/Company/CapabilitiesController.php create mode 100644 app/Http/Company/ChatController.php create mode 100644 app/Http/Company/CompanyAiConfigurationController.php create mode 100644 app/Http/Company/ConversationController.php create mode 100644 app/Http/Company/GenerationController.php create mode 100644 app/Http/ModuleController.php create mode 100644 app/Http/Requests/AiConfigurationRequest.php create mode 100644 app/Http/Requests/ChatRequest.php create mode 100644 app/Http/Requests/ConnectionTestRequest.php create mode 100644 app/Http/Requests/GenerationRequest.php create mode 100644 app/Http/Requests/RenameConversationRequest.php create mode 100644 app/Lifecycle/DataCleanup.php create mode 100644 app/Models/AiConversation.php create mode 100644 app/Models/AiMessage.php create mode 100644 app/Prompting/PromptLoader.php create mode 100644 app/Providers/AiAssistantServiceProvider.php create mode 100644 app/Rules/PublicHttpUrl.php create mode 100644 app/Support/Net/BlockedUrlException.php create mode 100644 app/Support/Net/PrivateNetworkGuard.php create mode 100644 composer.json create mode 100644 database/migrations/2026_04_11_154445_create_ai_conversations_and_messages_tables.php create mode 100644 dist/init.js create mode 100644 dist/style.css create mode 100644 eslint.config.js create mode 100644 module.json create mode 100644 package.json create mode 100644 phpunit.xml.dist create mode 100644 pnpm-lock.yaml create mode 100644 resources/ai/prompts/chat-system.md create mode 100644 resources/ai/prompts/text-generation.md create mode 100644 resources/css/module.css create mode 100644 resources/js/components/AiChatOverlay.vue create mode 100644 resources/js/components/AiHeaderAction.vue create mode 100644 resources/js/components/AiTextAction.vue create mode 100644 resources/js/composables/useAiChat.ts create mode 100644 resources/js/global.d.ts create mode 100644 resources/js/init.ts create mode 100644 resources/js/pages/AiConfigurationPage.vue create mode 100644 resources/js/shims-css.d.ts create mode 100644 resources/js/shims-vue.d.ts create mode 100644 resources/js/types/ai.ts create mode 100644 routes/api.php create mode 100644 tests/Feature/AiAssistantFlowTest.php create mode 100644 tests/Unit/AiConfigurationServiceTest.php create mode 100644 tests/Unit/AiDriverFactoryAndRegistryTest.php create mode 100644 tests/Unit/DataCleanupTest.php create mode 100644 tests/Unit/ModuleRoutesTest.php create mode 100644 tests/Unit/OpenRouterDriverTest.php create mode 100644 tests/Unit/PrivateNetworkGuardTest.php create mode 100644 tests/Unit/PromptLoaderTest.php create mode 100644 tests/Unit/ToolDelegationTest.php create mode 100644 tests/bootstrap.php create mode 100644 tsconfig.json create mode 100644 vite.config.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..44f7ed3 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,47 @@ +name: CI + +on: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: shivammathur/setup-php@v2 + with: + php-version: '8.4' + extensions: sodium + coverage: none + + - uses: pnpm/action-setup@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: pnpm + + - name: Install PHP dependencies + run: composer install --no-interaction --prefer-dist + + - name: Verify PHP and package contracts + run: | + composer run lint + composer run test + vendor/bin/invoiceshelf-module validate-module module.json + vendor/bin/invoiceshelf-module validate-package . + + - name: Verify frontend + run: | + pnpm install --frozen-lockfile + pnpm run lint + pnpm exec tsc --noEmit + pnpm run build + git diff --exit-code -- dist diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..00208a0 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,16 @@ +name: Release module + +on: + push: + tags: + - '*' + +permissions: + contents: read + +jobs: + release: + uses: InvoiceShelf/modules/.github/workflows/module-release.yml@3.3.0 + with: + channel: stable + secrets: inherit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5fdea0c --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +/vendor/ +/node_modules/ +/.phpunit.cache/ +/coverage/ +/*.zip +/release-manifest.json +/release-manifest.canonical.json +/release-manifest.sig + diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..de9d026 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,10 @@ +# AGENTS.md + +This is the official InvoiceShelf AI Assistant module. Read the parent devenv `AGENTS.md` and the InvoiceShelf 3.x `AGENTS.md` before making changes. + +- PHP 8.4, Laravel 13, PHPUnit 12, Vue 3, TypeScript, and Vite. +- Keep business queries behind `InvoiceShelf\\Modules\\Contracts\\Host` interfaces; module code must not import InvoiceShelf Eloquent models. +- Use the SDK AI driver ABI under `InvoiceShelf\\Modules\\Ai`. +- Keep the module optional: all UI must register through the host extension API and all backend routes must disappear when disabled. +- Preserve the existing `ai_conversations` and `ai_messages` schema and migration filename. +- Run `composer run lint`, `composer run test`, `pnpm run build`, and package validation before release. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0ad25db --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..344e4cc --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# InvoiceShelf AI Assistant + +The official free AI Assistant module for InvoiceShelf 3.x. It provides the in-app assistant, read-only business-data tools, and rich-text generation while keeping AI dependencies out of the core application. + +Users supply and fund their own OpenRouter API key. The module itself is free and open source under AGPL-3.0-only. + +## Development + +```bash +composer install +pnpm install --frozen-lockfile +composer run lint +composer run test +pnpm run build +vendor/bin/invoiceshelf-module validate-package . +``` + +Compiled assets in `dist/` are committed because InvoiceShelf installs immutable packages without running Composer or Node package managers. + +## Data lifecycle + +Disabling or uninstalling the module normally preserves conversations and configuration. If an administrator explicitly selects **Remove module data**, the cleanup hook removes legacy settings and the reversible migration removes the AI conversation tables. + +## Releases + +Push an exact SemVer tag matching `module.json`, such as `1.0.0`. The release workflow builds and validates a deterministic signed package before registering it with the InvoiceShelf marketplace. diff --git a/app/Application/AiAssistantService.php b/app/Application/AiAssistantService.php new file mode 100644 index 0000000..c93abb3 --- /dev/null +++ b/app/Application/AiAssistantService.php @@ -0,0 +1,322 @@ + $companyId, + 'user_id' => $userId, + 'title' => $firstMessage !== null ? $this->titleFromMessage($firstMessage) : null, + ]); + } + + /** + * Process one user message within an existing conversation. + * + * Returns the final assistant AiMessage that should be shown to the user. + * + * @throws AiException When AI is disabled or the driver call fails unrecoverably. + */ + public function chat(AiConversation $conversation, string $userMessage): AiMessage + { + $driver = $this->aiConfiguration->makeDriver($conversation->company_id); + + if ($driver === null) { + throw new AiException('AI is not enabled for this company', 'ai_disabled'); + } + + $resolved = $this->aiConfiguration->resolveForCompany($conversation->company_id); + if (empty($resolved['chat_enabled'])) { + throw new AiException('Chat is not enabled for this company', 'chat_disabled'); + } + + $model = (string) ($resolved['ai_chat_model'] ?? ''); + if ($model === '') { + throw new AiException('No chat model configured', 'missing_model'); + } + + // Auto-title on first message and remember the model used for the thread. + if ($conversation->title === null) { + $conversation->title = $this->titleFromMessage($userMessage); + } + + if ($conversation->model === null) { + $conversation->model = $model; + } + + // Persist the user's message first so it shows even if the LLM call fails. + $userRow = AiMessage::create([ + 'conversation_id' => $conversation->id, + 'role' => AiMessage::ROLE_USER, + 'content' => $userMessage, + ]); + + $conversation->touch(); // bump updated_at for "recent" ordering + + $messages = $this->buildMessagesPayload($conversation); + $tools = $this->toolRegistry->schemas($conversation->user_id, $conversation->company_id); + + for ($iteration = 0; $iteration < self::MAX_TOOL_ITERATIONS; $iteration++) { + try { + $response = $driver->chatCompletion($messages, $model, $tools); + } catch (AiException $e) { + // Persist the error as an assistant message so the UI can render it. + return AiMessage::create([ + 'conversation_id' => $conversation->id, + 'role' => AiMessage::ROLE_ASSISTANT, + 'content' => "Error: {$e->getMessage()}", + 'model' => $model, + ]); + } + + // Tool calls requested → execute each, append their results, loop. + if ($response->hasToolCalls()) { + $this->persistAssistantToolCallTurn($conversation, $response, $model); + $messages[] = $this->assistantToolCallMessage($response); + + foreach ($response->toolCalls as $call) { + $toolResult = $this->safelyExecuteTool( + name: $call['name'], + arguments: $call['arguments'] ?? [], + companyId: $conversation->company_id, + userId: $conversation->user_id, + ); + + $resultJson = json_encode($toolResult, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + + AiMessage::create([ + 'conversation_id' => $conversation->id, + 'role' => AiMessage::ROLE_TOOL, + 'content' => $resultJson, + 'tool_call_id' => $call['id'] ?? null, + ]); + + $messages[] = [ + 'role' => 'tool', + 'tool_call_id' => $call['id'] ?? '', + 'content' => $resultJson, + ]; + } + + // Loop — the LLM will receive the tool results on the next iteration. + continue; + } + + // Plain text response — persist and return. + return AiMessage::create([ + 'conversation_id' => $conversation->id, + 'role' => AiMessage::ROLE_ASSISTANT, + 'content' => $response->message ?? '', + 'model' => $model, + 'tokens_in' => $response->usage['tokens_in'] ?? null, + 'tokens_out' => $response->usage['tokens_out'] ?? null, + ]); + } + + // Exceeded the loop cap. + return AiMessage::create([ + 'conversation_id' => $conversation->id, + 'role' => AiMessage::ROLE_ASSISTANT, + 'content' => 'The assistant could not complete this request within the tool-call budget. Please try rephrasing.', + 'model' => $model, + ]); + } + + /** + * Build the OpenAI-format messages payload for the next LLM call. + * + * Starts with a fresh system prompt, then the recent message history + * trimmed to HISTORY_WINDOW items, then the message(s) that will drive + * the upcoming round-trip (already persisted by chat()). + * + * @return array> + */ + protected function buildMessagesPayload(AiConversation $conversation): array + { + $payload = [ + [ + 'role' => 'system', + 'content' => $this->buildSystemPrompt($conversation), + ], + ]; + + $history = AiMessage::query() + ->where('conversation_id', $conversation->id) + ->orderByDesc('created_at') + ->limit(self::HISTORY_WINDOW) + ->get() + ->reverse() + ->values(); + + foreach ($history as $msg) { + $payload[] = $this->formatMessageForDriver($msg); + } + + return $payload; + } + + /** + * @return array + */ + protected function formatMessageForDriver(AiMessage $msg): array + { + $base = ['role' => $msg->role]; + + if ($msg->role === AiMessage::ROLE_TOOL) { + $base['tool_call_id'] = $msg->tool_call_id ?? ''; + $base['content'] = $msg->content ?? ''; + + return $base; + } + + if ($msg->role === AiMessage::ROLE_ASSISTANT && $msg->tool_calls) { + $base['content'] = $msg->content; + $base['tool_calls'] = array_map(function (array $call): array { + return [ + 'id' => $call['id'] ?? '', + 'type' => 'function', + 'function' => [ + 'name' => $call['name'] ?? '', + 'arguments' => json_encode($call['arguments'] ?? [], JSON_UNESCAPED_UNICODE), + ], + ]; + }, $msg->tool_calls); + + return $base; + } + + $base['content'] = $msg->content ?? ''; + + return $base; + } + + /** + * Compose the system prompt with company context. + * + * The template itself lives at resources/ai/prompts/chat-system.md so + * it can be edited without touching this class. See PromptLoader for + * the substitution rules. + */ + protected function buildSystemPrompt(AiConversation $conversation): string + { + return PromptLoader::load('chat-system', [ + 'user_name' => 'the current user', + 'company_name' => 'the current company', + 'today' => Carbon::now()->toDateString(), + ]); + } + + /** + * Persist the assistant's tool_calls-turn message (the one that requested tools). + */ + protected function persistAssistantToolCallTurn( + AiConversation $conversation, + AiChatResponse $response, + string $model, + ): void { + AiMessage::create([ + 'conversation_id' => $conversation->id, + 'role' => AiMessage::ROLE_ASSISTANT, + 'content' => $response->message, // usually null on a tool_calls turn + 'tool_calls' => $response->toolCalls, + 'model' => $model, + 'tokens_in' => $response->usage['tokens_in'] ?? null, + 'tokens_out' => $response->usage['tokens_out'] ?? null, + ]); + } + + /** + * Format the assistant tool-call turn for the NEXT driver call, in OpenAI format. + * + * @return array + */ + protected function assistantToolCallMessage(AiChatResponse $response): array + { + return [ + 'role' => 'assistant', + 'content' => $response->message, + 'tool_calls' => array_map(function (array $call): array { + return [ + 'id' => $call['id'] ?? '', + 'type' => 'function', + 'function' => [ + 'name' => $call['name'] ?? '', + 'arguments' => json_encode($call['arguments'] ?? [], JSON_UNESCAPED_UNICODE), + ], + ]; + }, $response->toolCalls), + ]; + } + + /** + * Call a tool and trap any exception — we want tool failures to be visible + * to the LLM as structured errors, not to crash the whole turn. + * + * @param array $arguments + */ + protected function safelyExecuteTool( + string $name, + array $arguments, + int $companyId, + int $userId, + ): mixed { + try { + return $this->toolRegistry->execute($name, $arguments, $companyId, $userId); + } catch (InvalidArgumentException $e) { + return ['error' => 'unknown_tool', 'message' => $e->getMessage()]; + } catch (Throwable $e) { + return ['error' => 'tool_execution_failed', 'message' => $e->getMessage()]; + } + } + + /** + * Derive a short conversation title from the first user message. + */ + protected function titleFromMessage(string $message): string + { + $trimmed = trim(preg_replace('/\s+/', ' ', $message) ?? ''); + if (mb_strlen($trimmed) <= 60) { + return $trimmed; + } + + return rtrim(mb_substr($trimmed, 0, 57)).'...'; + } +} diff --git a/app/Application/AiConfigurationService.php b/app/Application/AiConfigurationService.php new file mode 100644 index 0000000..7e0c748 --- /dev/null +++ b/app/Application/AiConfigurationService.php @@ -0,0 +1,169 @@ + */ + public const SETTING_KEYS = [ + 'ai_enabled', 'ai_driver', 'ai_api_key', 'ai_base_url', 'ai_chat_enabled', + 'ai_chat_model', 'ai_text_generation_enabled', 'ai_text_generation_model', + ]; + + public function __construct(private readonly SettingsStore $settings) {} + + /** @return array */ + public function getGlobalConfig(): array + { + return $this->hydrateDefaults($this->decrypt($this->readGlobal())); + } + + /** @return array */ + public function getCompanyConfig(int $companyId): array + { + $config = []; + foreach (self::SETTING_KEYS as $key) { + $config[$key] = $this->settings->getCompany($companyId, 'company_'.$key); + } + + return array_merge([ + 'use_custom_ai_config' => $this->settings->getCompany($companyId, 'use_custom_ai_config', 'NO'), + ], $this->hydrateDefaults($this->decrypt($config))); + } + + /** @param array $payload */ + public function saveGlobalConfig(array $payload): void + { + foreach ($this->storageValues($payload) as $key => $value) { + $this->settings->putGlobal($key, $value); + } + } + + /** @param array $payload */ + public function saveCompanyConfig(int $companyId, array $payload): void + { + if (($payload['use_custom_ai_config'] ?? 'NO') !== 'YES') { + $this->settings->putCompany($companyId, 'use_custom_ai_config', 'NO'); + + return; + } + + foreach ($this->storageValues($payload) as $key => $value) { + $this->settings->putCompany($companyId, 'company_'.$key, $value); + } + $this->settings->putCompany($companyId, 'use_custom_ai_config', 'YES'); + } + + /** @return array|null */ + public function resolveForCompany(int $companyId): ?array + { + $global = $this->getGlobalConfig(); + if (($global['ai_enabled'] ?? 'NO') !== 'YES') { + return null; + } + + $company = $this->getCompanyConfig($companyId); + $resolved = ($company['use_custom_ai_config'] ?? 'NO') === 'YES' ? $company : $global; + if (($resolved['ai_enabled'] ?? 'NO') !== 'YES') { + return null; + } + + $resolved['chat_enabled'] = ($resolved['ai_chat_enabled'] ?? 'NO') === 'YES'; + $resolved['text_generation_enabled'] = ($resolved['ai_text_generation_enabled'] ?? 'NO') === 'YES'; + + return $resolved; + } + + public function makeDriver(int $companyId): ?AiDriver + { + $config = $this->resolveForCompany($companyId); + if ($config === null || empty($config['ai_api_key']) || empty($config['ai_driver'])) { + return null; + } + + return AiDriverFactory::make((string) $config['ai_driver'], (string) $config['ai_api_key'], [ + 'base_url' => $config['ai_base_url'] ?? null, + ]); + } + + /** @return array> */ + public function listDrivers(): array + { + return collect(Registry::allDrivers('ai'))->map(fn (array $meta, string $name): array => [ + 'value' => $name, + 'label' => $meta['label'], + 'website' => $meta['website'] ?? '', + 'default_base_url' => $meta['default_base_url'] ?? '', + 'supported_roles' => $meta['supported_roles'], + 'suggested_models' => $meta['suggested_models'], + 'config_fields' => $meta['config_fields'], + ])->values()->all(); + } + + /** @return array */ + private function readGlobal(): array + { + $values = []; + foreach (self::SETTING_KEYS as $key) { + $values[$key] = $this->settings->getGlobal($key); + } + + return $values; + } + + /** @param array $settings @return array */ + private function decrypt(array $settings): array + { + if (! empty($settings['ai_api_key'])) { + try { + $settings['ai_api_key'] = Crypt::decryptString((string) $settings['ai_api_key']); + } catch (\Throwable) { + // Older core installations stored the key before encryption. + } + } + + return $settings; + } + + /** @param array $settings @return array */ + private function hydrateDefaults(array $settings): array + { + return array_merge([ + 'ai_enabled' => 'NO', 'ai_driver' => 'openrouter', 'ai_api_key' => '', 'ai_base_url' => '', + 'ai_chat_enabled' => 'NO', 'ai_chat_model' => 'anthropic/claude-sonnet-4.6', + 'ai_text_generation_enabled' => 'NO', 'ai_text_generation_model' => 'anthropic/claude-haiku-4.5', + ], array_filter($settings, static fn (mixed $value): bool => $value !== null)); + } + + /** @param array $payload @return array */ + private function storageValues(array $payload): array + { + $values = []; + foreach (self::SETTING_KEYS as $key) { + if (! array_key_exists($key, $payload)) { + continue; + } + $value = $payload[$key]; + $values[$key] = $key === 'ai_api_key' && is_string($value) && $value !== '' + ? Crypt::encryptString($value) + : $value; + } + + return $values; + } +} diff --git a/app/Application/AiTextGenerationService.php b/app/Application/AiTextGenerationService.php new file mode 100644 index 0000000..4353306 --- /dev/null +++ b/app/Application/AiTextGenerationService.php @@ -0,0 +1,89 @@ +aiConfiguration->makeDriver($companyId); + + if ($driver === null) { + throw new AiException('AI is not enabled for this company', 'ai_disabled'); + } + + $resolved = $this->aiConfiguration->resolveForCompany($companyId); + if (empty($resolved['text_generation_enabled'])) { + throw new AiException('Text generation is not enabled for this company', 'text_generation_disabled'); + } + + $model = (string) ($resolved['ai_text_generation_model'] ?? ''); + if ($model === '') { + throw new AiException('No text generation model configured', 'missing_model'); + } + + $fullPrompt = $this->buildPrompt($prompt, $context); + + return trim($driver->textCompletion($fullPrompt, $model)); + } + + /** + * Compose the final prompt sent to the model. + * + * Keep the framing terse — text-generation output should not be padded + * with "Here is the text you requested:" preambles. The instruction is + * always placed last so the model gives it the most weight. + * + * The static preamble lives at resources/ai/prompts/text-generation.md. + * The conditional context + instruction appending stays here because it + * has different structure depending on whether `context` is present. + */ + protected function buildPrompt(string $prompt, ?string $context): string + { + $system = PromptLoader::load('text-generation'); + + if ($context !== null && trim($context) !== '') { + return $system."\n\n" + ."Context (current content the user is working with):\n" + .trim($context) + ."\n\n" + ."Instruction: {$prompt}"; + } + + return $system."\n\nInstruction: {$prompt}"; + } +} diff --git a/app/Application/AiToolRegistry.php b/app/Application/AiToolRegistry.php new file mode 100644 index 0000000..8220d52 --- /dev/null +++ b/app/Application/AiToolRegistry.php @@ -0,0 +1,55 @@ + */ + private array $tools = []; + + /** @param list $tools */ + public function __construct(private readonly ModuleAuthorization $authorization, array $tools = []) + { + foreach ($tools as $tool) { + $this->register($tool); + } + } + + public function register(AiTool $tool): void + { + $this->tools[$tool->name()] = $tool; + } + + /** @return list> */ + public function schemas(int $userId, int $companyId): array + { + return array_values(array_map( + static fn (AiTool $tool): array => $tool->toOpenAiToolSchema(), + array_filter($this->tools, fn (AiTool $tool): bool => $this->allowed($tool, $userId, $companyId)), + )); + } + + /** @param array $arguments */ + public function execute(string $name, array $arguments, int $companyId, int $userId): mixed + { + $tool = $this->tools[$name] ?? throw new InvalidArgumentException("Unknown AI tool: {$name}"); + if (! $this->allowed($tool, $userId, $companyId)) { + return ['error' => 'unauthorized', 'message' => 'You do not have permission to access this data.']; + } + + return $tool->execute($arguments, $companyId, $userId); + } + + private function allowed(AiTool $tool, int $userId, int $companyId): bool + { + $required = $tool->requiredAbility(); + + return $required === null || $this->authorization->allows($userId, $companyId, $required[0], $required[1]); + } +} diff --git a/app/Application/Tools/AiTool.php b/app/Application/Tools/AiTool.php new file mode 100644 index 0000000..a3ec5e3 --- /dev/null +++ b/app/Application/Tools/AiTool.php @@ -0,0 +1,129 @@ + 'object', 'properties' => (object) []]`. + * + * Critical: do NOT include `company_id` or `user_id` in the schema. + * Scoping is the host's responsibility and is injected at execute time. + * + * @return array + */ + abstract public function parameterSchema(): array; + + /** + * Run the tool with the given arguments, scoped to the caller's session. + * + * Implementations MUST query within the given `$companyId` and never trust + * any company/user identifier the LLM tries to sneak into `$arguments`. + * + * @param array $arguments Parsed from the LLM's tool_call JSON + * @param int $companyId Injected from the current session — authoritative + * @param int $userId Injected from the current session + * @return mixed Anything JSON-encodable; will be serialized and sent back to the LLM + */ + abstract public function execute(array $arguments, int $companyId, int $userId): mixed; + + /** + * The host ability a caller must hold to use this tool, as an + * `[ability, stable-resource-name]` pair — or null if no ability beyond `use ai`. + * + * The registry checks this against the session user before exposing the tool + * to the LLM and again before executing it, so the assistant honours the same + * per-user permissions as the rest of the app. The model element may be null + * for gate-style abilities that take no model (e.g. `dashboard`). + * + * @return array{0: string, 1: string|null}|null + */ + abstract public function requiredAbility(): ?array; + + /** + * Convert this tool into an OpenAI-style tools array entry. + * + * @return array + */ + public function toOpenAiToolSchema(): array + { + return [ + 'type' => 'function', + 'function' => [ + 'name' => $this->name(), + 'description' => $this->description(), + 'parameters' => $this->parameterSchema(), + ], + ]; + } + + /** + * Normalize a model date field (which may be a string OR a Carbon instance + * depending on model casts) to a YYYY-MM-DD string for tool output. + * + * Many InvoiceShelf models store dates as raw strings; others cast to Carbon. + * Centralizing this avoids cast-guessing in every tool. + */ + protected function asDate(mixed $value): ?string + { + if ($value === null || $value === '') { + return null; + } + + if (is_object($value) && method_exists($value, 'toDateString')) { + return $value->toDateString(); + } + + // String like '2026-04-11' or '2026-04-11 00:00:00' — keep the date part only. + if (is_string($value)) { + return substr($value, 0, 10); + } + + return null; + } +} diff --git a/app/Application/Tools/Concerns/ResolvesPeriod.php b/app/Application/Tools/Concerns/ResolvesPeriod.php new file mode 100644 index 0000000..45f1d0f --- /dev/null +++ b/app/Application/Tools/Concerns/ResolvesPeriod.php @@ -0,0 +1,30 @@ + null, + 'today' => [$now->copy()->startOfDay(), $now->copy()->endOfDay()], + 'this_week' => [$now->copy()->startOfWeek(), $now->copy()->endOfWeek()], + 'this_month' => [$now->copy()->startOfMonth(), $now->copy()->endOfMonth()], + 'last_month' => [$now->copy()->subMonthNoOverflow()->startOfMonth(), $now->copy()->subMonthNoOverflow()->endOfMonth()], + 'this_quarter' => [$now->copy()->startOfQuarter(), $now->copy()->endOfQuarter()], + 'this_year' => [$now->copy()->startOfYear(), $now->copy()->endOfYear()], + 'last_year' => [$now->copy()->subYearNoOverflow()->startOfYear(), $now->copy()->subYearNoOverflow()->endOfYear()], + default => null, + }; + } +} diff --git a/app/Application/Tools/GetCompanyStatsTool.php b/app/Application/Tools/GetCompanyStatsTool.php new file mode 100644 index 0000000..974c3aa --- /dev/null +++ b/app/Application/Tools/GetCompanyStatsTool.php @@ -0,0 +1,49 @@ + 'object', 'properties' => ['period' => ['type' => 'string', 'enum' => self::PERIODS, 'description' => 'Named time window.']], 'required' => ['period']]; + } + + public function requiredAbility(): ?array + { + return ['dashboard', null]; + } + + public function execute(array $arguments, int $companyId, int $userId): mixed + { + $period = (string) ($arguments['period'] ?? 'this_month'); + if (! in_array($period, self::PERIODS, true)) { + return ['error' => 'invalid_period', 'valid' => self::PERIODS]; + } + + [$start, $end] = $this->rangeFor($period); + + return ['period' => $period, 'start' => $start->toDateString(), 'end' => $end->toDateString(), ...$this->data->companyStats($companyId, $start->toDateString(), $end->toDateString())]; + } +} diff --git a/app/Application/Tools/GetCustomerTool.php b/app/Application/Tools/GetCustomerTool.php new file mode 100644 index 0000000..2430e23 --- /dev/null +++ b/app/Application/Tools/GetCustomerTool.php @@ -0,0 +1,39 @@ + 'object', 'properties' => ['customer_id' => ['type' => 'integer', 'description' => 'The customer ID.']], 'required' => ['customer_id']]; + } + + public function requiredAbility(): ?array + { + return ['view-customer', 'customer']; + } + + public function execute(array $arguments, int $companyId, int $userId): mixed + { + $customer = $this->data->findCustomer($companyId, (int) ($arguments['customer_id'] ?? 0)); + + return $customer === null ? ['error' => 'customer_not_found'] : ['customer' => $customer]; + } +} diff --git a/app/Application/Tools/GetInvoiceTool.php b/app/Application/Tools/GetInvoiceTool.php new file mode 100644 index 0000000..492b0d9 --- /dev/null +++ b/app/Application/Tools/GetInvoiceTool.php @@ -0,0 +1,39 @@ + 'object', 'properties' => ['invoice_number' => ['type' => 'string', 'description' => 'The invoice_number to look up (e.g. "INV-000001").']], 'required' => ['invoice_number']]; + } + + public function requiredAbility(): ?array + { + return ['view-invoice', 'invoice']; + } + + public function execute(array $arguments, int $companyId, int $userId): mixed + { + $invoice = $this->data->findInvoice($companyId, (string) ($arguments['invoice_number'] ?? '')); + + return $invoice === null ? ['error' => 'invoice_not_found'] : ['invoice' => $invoice]; + } +} diff --git a/app/Application/Tools/ListExpenseCategoriesTool.php b/app/Application/Tools/ListExpenseCategoriesTool.php new file mode 100644 index 0000000..d5f5a12 --- /dev/null +++ b/app/Application/Tools/ListExpenseCategoriesTool.php @@ -0,0 +1,37 @@ + 'object', 'properties' => (object) [], 'required' => []]; + } + + public function requiredAbility(): ?array + { + return ['view-expense', 'expense']; + } + + public function execute(array $arguments, int $companyId, int $userId): mixed + { + return ['categories' => $this->data->expenseCategories($companyId)]; + } +} diff --git a/app/Application/Tools/ListOverdueInvoicesTool.php b/app/Application/Tools/ListOverdueInvoicesTool.php new file mode 100644 index 0000000..646a654 --- /dev/null +++ b/app/Application/Tools/ListOverdueInvoicesTool.php @@ -0,0 +1,39 @@ + 'object', 'properties' => (object) [], 'required' => []]; + } + + public function requiredAbility(): ?array + { + return ['view-invoice', 'invoice']; + } + + public function execute(array $arguments, int $companyId, int $userId): mixed + { + $invoices = $this->data->overdueInvoices($companyId, 100); + + return ['count' => count($invoices), 'total_outstanding' => (float) array_sum(array_map(static fn (array $invoice): float => (float) ($invoice['due_amount'] ?? 0), $invoices)), 'invoices' => $invoices]; + } +} diff --git a/app/Application/Tools/ListRecentPaymentsTool.php b/app/Application/Tools/ListRecentPaymentsTool.php new file mode 100644 index 0000000..c8c8cf2 --- /dev/null +++ b/app/Application/Tools/ListRecentPaymentsTool.php @@ -0,0 +1,50 @@ + 'object', 'properties' => ['days' => ['type' => 'integer', 'minimum' => 1, 'maximum' => self::MAX_DAYS, 'description' => 'How many days back to look (default 30, max 365).'], 'limit' => ['type' => 'integer', 'minimum' => 1, 'maximum' => self::MAX_LIMIT, 'description' => 'Max rows to return (default 20, max 100).']], 'required' => []]; + } + + public function requiredAbility(): ?array + { + return ['view-payment', 'payment']; + } + + public function execute(array $arguments, int $companyId, int $userId): mixed + { + $days = min((int) ($arguments['days'] ?? self::DEFAULT_DAYS), self::MAX_DAYS); + $limit = min((int) ($arguments['limit'] ?? self::DEFAULT_LIMIT), self::MAX_LIMIT); + $since = Carbon::now()->subDays($days)->startOfDay()->toDateString(); + + return ['since' => $since, 'payments' => $this->data->recentPayments($companyId, $since, $limit)]; + } +} diff --git a/app/Application/Tools/RankExpenseCategoriesTool.php b/app/Application/Tools/RankExpenseCategoriesTool.php new file mode 100644 index 0000000..e38b478 --- /dev/null +++ b/app/Application/Tools/RankExpenseCategoriesTool.php @@ -0,0 +1,54 @@ + 'object', 'properties' => ['period' => ['type' => 'string', 'enum' => self::ALL_PERIODS, 'description' => 'Named time window. Use all_time for lifetime totals.'], 'limit' => ['type' => 'integer', 'minimum' => 1, 'maximum' => self::MAX_LIMIT, 'description' => 'Max number of categories to return. Default 10.']], 'required' => []]; + } + + public function requiredAbility(): ?array + { + return ['view-expense', 'expense']; + } + + public function execute(array $arguments, int $companyId, int $userId): mixed + { + $period = (string) ($arguments['period'] ?? 'all_time'); + if (! in_array($period, self::ALL_PERIODS, true)) { + return ['error' => 'invalid_period', 'valid' => self::ALL_PERIODS]; + } + + $limit = min(max((int) ($arguments['limit'] ?? self::DEFAULT_LIMIT), 1), self::MAX_LIMIT); + $range = $this->rangeFor($period); + $start = $range === null ? null : $range[0]->toDateString(); + $end = $range === null ? null : $range[1]->toDateString(); + + return ['period' => $period, 'categories' => $this->data->rankExpenseCategories($companyId, $start, $end, $limit)]; + } +} diff --git a/app/Application/Tools/RankTopCustomersTool.php b/app/Application/Tools/RankTopCustomersTool.php new file mode 100644 index 0000000..f0e847f --- /dev/null +++ b/app/Application/Tools/RankTopCustomersTool.php @@ -0,0 +1,61 @@ + 'object', 'properties' => ['metric' => ['type' => 'string', 'enum' => self::METRICS, 'description' => 'Which metric to rank by.'], 'period' => ['type' => 'string', 'enum' => self::ALL_PERIODS, 'description' => 'Named time window. Use all_time for lifetime rankings. Ignored for outstanding_balance (always current).'], 'limit' => ['type' => 'integer', 'minimum' => 1, 'maximum' => self::MAX_LIMIT, 'description' => 'Max number of customers to return. Default 5.']], 'required' => ['metric']]; + } + + public function requiredAbility(): ?array + { + return ['view-customer', 'customer']; + } + + public function execute(array $arguments, int $companyId, int $userId): mixed + { + $metric = (string) ($arguments['metric'] ?? 'invoiced_total'); + if (! in_array($metric, self::METRICS, true)) { + return ['error' => 'invalid_metric', 'valid' => self::METRICS]; + } + + $period = (string) ($arguments['period'] ?? 'all_time'); + if (! in_array($period, self::ALL_PERIODS, true)) { + return ['error' => 'invalid_period', 'valid' => self::ALL_PERIODS]; + } + + $limit = min(max((int) ($arguments['limit'] ?? self::DEFAULT_LIMIT), 1), self::MAX_LIMIT); + $range = $metric === 'outstanding_balance' ? null : $this->rangeFor($period); + $start = $range === null ? null : $range[0]->toDateString(); + $end = $range === null ? null : $range[1]->toDateString(); + + return ['metric' => $metric, 'period' => $metric === 'outstanding_balance' ? 'current' : $period, 'customers' => $this->data->rankCustomers($companyId, $metric, $start, $end, $limit)]; + } +} diff --git a/app/Application/Tools/RankTopItemsTool.php b/app/Application/Tools/RankTopItemsTool.php new file mode 100644 index 0000000..27fa718 --- /dev/null +++ b/app/Application/Tools/RankTopItemsTool.php @@ -0,0 +1,61 @@ + 'object', 'properties' => ['metric' => ['type' => 'string', 'enum' => self::METRICS, 'description' => 'Which dimension to rank by.'], 'period' => ['type' => 'string', 'enum' => self::ALL_PERIODS, 'description' => 'Named time window. Use all_time for lifetime rankings.'], 'limit' => ['type' => 'integer', 'minimum' => 1, 'maximum' => self::MAX_LIMIT, 'description' => 'Max number of items to return. Default 5.']], 'required' => ['metric']]; + } + + public function requiredAbility(): ?array + { + return ['view-item', 'item']; + } + + public function execute(array $arguments, int $companyId, int $userId): mixed + { + $metric = (string) ($arguments['metric'] ?? 'revenue'); + if (! in_array($metric, self::METRICS, true)) { + return ['error' => 'invalid_metric', 'valid' => self::METRICS]; + } + + $period = (string) ($arguments['period'] ?? 'all_time'); + if (! in_array($period, self::ALL_PERIODS, true)) { + return ['error' => 'invalid_period', 'valid' => self::ALL_PERIODS]; + } + + $limit = min(max((int) ($arguments['limit'] ?? self::DEFAULT_LIMIT), 1), self::MAX_LIMIT); + $range = $this->rangeFor($period); + $start = $range === null ? null : $range[0]->toDateString(); + $end = $range === null ? null : $range[1]->toDateString(); + + return ['metric' => $metric, 'period' => $period, 'items' => $this->data->rankItems($companyId, $metric, $start, $end, $limit)]; + } +} diff --git a/app/Application/Tools/SearchCustomersTool.php b/app/Application/Tools/SearchCustomersTool.php new file mode 100644 index 0000000..3843f75 --- /dev/null +++ b/app/Application/Tools/SearchCustomersTool.php @@ -0,0 +1,43 @@ + 'object', 'properties' => ['query' => ['type' => 'string', 'description' => 'Free-text search against name, email, and related fields.'], 'limit' => ['type' => 'integer', 'minimum' => 1, 'maximum' => self::MAX_LIMIT]], 'required' => []]; + } + + public function requiredAbility(): ?array + { + return ['view-customer', 'customer']; + } + + public function execute(array $arguments, int $companyId, int $userId): mixed + { + $limit = min((int) ($arguments['limit'] ?? self::DEFAULT_LIMIT), self::MAX_LIMIT); + + return ['customers' => $this->data->searchCustomers($companyId, empty($arguments['query']) ? null : (string) $arguments['query'], $limit)]; + } +} diff --git a/app/Application/Tools/SearchInvoicesTool.php b/app/Application/Tools/SearchInvoicesTool.php new file mode 100644 index 0000000..3be4ff3 --- /dev/null +++ b/app/Application/Tools/SearchInvoicesTool.php @@ -0,0 +1,43 @@ + 'object', 'properties' => ['query' => ['type' => 'string', 'description' => 'Optional free-text search against invoice_number and reference_number.'], 'status' => ['type' => 'string', 'enum' => ['DRAFT', 'SENT', 'VIEWED', 'COMPLETED', 'UNPAID', 'PARTIALLY_PAID', 'PAID', 'OVERDUE'], 'description' => 'Optional status filter.'], 'customer_id' => ['type' => 'integer', 'description' => 'Optional customer ID to restrict to a specific customer.'], 'limit' => ['type' => 'integer', 'minimum' => 1, 'maximum' => self::MAX_LIMIT, 'description' => 'Max rows to return (default 10, max 50).']], 'required' => []]; + } + + public function requiredAbility(): ?array + { + return ['view-invoice', 'invoice']; + } + + public function execute(array $arguments, int $companyId, int $userId): mixed + { + $limit = min((int) ($arguments['limit'] ?? self::DEFAULT_LIMIT), self::MAX_LIMIT); + + return ['invoices' => $this->data->searchInvoices($companyId, empty($arguments['query']) ? null : (string) $arguments['query'], empty($arguments['status']) ? null : (string) $arguments['status'], empty($arguments['customer_id']) ? null : (int) $arguments['customer_id'], $limit)]; + } +} diff --git a/app/Application/Tools/SearchItemsTool.php b/app/Application/Tools/SearchItemsTool.php new file mode 100644 index 0000000..fec8684 --- /dev/null +++ b/app/Application/Tools/SearchItemsTool.php @@ -0,0 +1,43 @@ + 'object', 'properties' => ['query' => ['type' => 'string', 'description' => 'Free-text search against name and description.'], 'limit' => ['type' => 'integer', 'minimum' => 1, 'maximum' => self::MAX_LIMIT]], 'required' => []]; + } + + public function requiredAbility(): ?array + { + return ['view-item', 'item']; + } + + public function execute(array $arguments, int $companyId, int $userId): mixed + { + $limit = min((int) ($arguments['limit'] ?? self::DEFAULT_LIMIT), self::MAX_LIMIT); + + return ['items' => $this->data->searchItems($companyId, empty($arguments['query']) ? null : (string) $arguments['query'], $limit)]; + } +} diff --git a/app/Drivers/AiDriverFactory.php b/app/Drivers/AiDriverFactory.php new file mode 100644 index 0000000..d19203d --- /dev/null +++ b/app/Drivers/AiDriverFactory.php @@ -0,0 +1,30 @@ + $config */ + public static function make(string $driver, string $apiKey, array $config = []): AiDriver + { + $meta = Registry::driverMeta('ai', $driver); + $class = $meta['class'] ?? null; + if (! is_string($class) || ! is_a($class, AiDriver::class, true)) { + throw new InvalidArgumentException("Unknown AI driver: {$driver}"); + } + + return new $class($apiKey, $config); + } + + /** @return list */ + public static function availableDrivers(): array + { + return array_keys(Registry::allDrivers('ai')); + } +} diff --git a/app/Drivers/OpenRouterDriver.php b/app/Drivers/OpenRouterDriver.php new file mode 100644 index 0000000..7c19608 --- /dev/null +++ b/app/Drivers/OpenRouterDriver.php @@ -0,0 +1,234 @@ +getBaseUrl().'/chat/completions'; + + $payload = array_filter([ + 'model' => $model, + 'messages' => $messages, + 'tools' => $tools !== [] ? $tools : null, + 'tool_choice' => $tools !== [] ? ($options['tool_choice'] ?? 'auto') : null, + 'temperature' => $options['temperature'] ?? null, + 'max_tokens' => $options['max_tokens'] ?? null, + ], fn ($v) => $v !== null); + + try { + $response = Http::withToken($this->apiKey) + ->timeout(self::TIMEOUT_SECONDS) + ->acceptJson() + ->asJson() + ->post($endpoint, $payload); + } catch (Throwable $e) { + throw new AiException( + 'OpenRouter request failed: '.$e->getMessage(), + 'server_error', + 0, + $e, + ); + } + + if ($response->status() === 401) { + throw new AiException('Invalid OpenRouter API key', 'invalid_key'); + } + + if ($response->status() === 429) { + throw new AiException('OpenRouter rate limit exceeded', 'rate_limited'); + } + + if (! $response->successful()) { + $errorBody = $response->json('error.message') ?? $response->body(); + throw new AiException( + 'OpenRouter returned '.$response->status().': '.$errorBody, + 'server_error', + ); + } + + return $this->parseChatResponse($response->json()); + } + + public function textCompletion(string $prompt, string $model, array $options = []): string + { + $response = $this->chatCompletion( + [['role' => 'user', 'content' => $prompt]], + $model, + [], + $options, + ); + + return $response->message ?? ''; + } + + public function validateConnection(): array + { + // Resolve (and SSRF-validate) the URL before the try so a blocked base + // URL surfaces as `invalid_base_url`, not a generic `server_error`. + $endpoint = $this->getBaseUrl().'/models'; + + try { + $response = Http::withToken($this->apiKey) + ->timeout(30) + ->acceptJson() + ->get($endpoint); + } catch (Throwable $e) { + throw new AiException( + 'Unable to reach OpenRouter: '.$e->getMessage(), + 'server_error', + 0, + $e, + ); + } + + if ($response->status() === 401) { + throw new AiException('Invalid OpenRouter API key', 'invalid_key'); + } + + if (! $response->successful()) { + throw new AiException( + 'OpenRouter validation failed with status '.$response->status(), + 'server_error', + ); + } + + $data = $response->json('data', []); + + return [ + 'ok' => true, + 'model_count' => is_array($data) ? count($data) : 0, + ]; + } + + public function listModels(): array + { + try { + $response = Http::withToken($this->apiKey) + ->timeout(30) + ->acceptJson() + ->get($this->getBaseUrl().'/models'); + } catch (Throwable) { + return []; + } + + if (! $response->successful()) { + return []; + } + + $models = $response->json('data', []); + + if (! is_array($models)) { + return []; + } + + return array_map( + fn (array $m): array => [ + 'value' => $m['id'] ?? '', + 'label' => $m['name'] ?? ($m['id'] ?? ''), + ], + $models, + ); + } + + /** + * @param array|null $body + */ + protected function parseChatResponse(?array $body): AiChatResponse + { + $choice = $body['choices'][0] ?? []; + $message = $choice['message'] ?? []; + + $text = $message['content'] ?? null; + $finishReason = $choice['finish_reason'] ?? 'stop'; + + // Normalize OpenAI's tool_calls shape — each entry has id, type='function', + // and function.{name,arguments} where arguments is a JSON string we need to decode. + $toolCalls = []; + foreach ($message['tool_calls'] ?? [] as $call) { + $name = $call['function']['name'] ?? null; + $rawArgs = $call['function']['arguments'] ?? '{}'; + $args = is_string($rawArgs) ? (json_decode($rawArgs, true) ?: []) : (array) $rawArgs; + + if ($name === null) { + continue; + } + + $toolCalls[] = [ + 'id' => $call['id'] ?? '', + 'name' => $name, + 'arguments' => $args, + ]; + } + + $usage = []; + if (isset($body['usage'])) { + $usage = [ + 'tokens_in' => (int) ($body['usage']['prompt_tokens'] ?? 0), + 'tokens_out' => (int) ($body['usage']['completion_tokens'] ?? 0), + ]; + } + + return new AiChatResponse( + message: $text, + toolCalls: $toolCalls, + finishReason: $finishReason, + usage: $usage, + model: $body['model'] ?? null, + ); + } + + protected function getBaseUrl(): string + { + if ($this->validatedBaseUrl !== null) { + return $this->validatedBaseUrl; + } + + $configured = (string) ($this->config['base_url'] ?? ''); + $url = rtrim($configured !== '' ? $configured : self::DEFAULT_BASE_URL, '/'); + + // SSRF guard: never let an admin/owner-supplied base URL point the + // server (with the bearer token attached) at a private/reserved host. + try { + PrivateNetworkGuard::assertAllowed($url); + } catch (BlockedUrlException $exception) { + throw new AiException('Invalid AI base URL: '.$exception->getMessage(), 'invalid_base_url', 0, $exception); + } + + return $this->validatedBaseUrl = $url; + } +} diff --git a/app/Http/Admin/AiConfigurationController.php b/app/Http/Admin/AiConfigurationController.php new file mode 100644 index 0000000..7364a57 --- /dev/null +++ b/app/Http/Admin/AiConfigurationController.php @@ -0,0 +1,46 @@ +authorizeAbility('manage ai config'); + + return response()->json($this->masked($this->configuration->getGlobalConfig())); + } + + public function saveConfig(AiConfigurationRequest $request): JsonResponse + { + $this->authorizeAbility('manage ai config'); + $values = $request->validated(); + if (in_array($values['ai_api_key'] ?? '', ['', '********'], true)) { + $values['ai_api_key'] = $this->configuration->getGlobalConfig()['ai_api_key']; + } + $this->configuration->saveGlobalConfig($values); + + return response()->json(['success' => true]); + } + + public function getDrivers(): JsonResponse + { + $this->authorizeAbility('manage ai config'); + + return response()->json(['ai_drivers' => $this->configuration->listDrivers()]); + } + + public function testConnection(ConnectionTestRequest $request): JsonResponse + { + $this->authorizeAbility('manage ai config'); + + return $this->testConfiguration($request->validated(), $this->configuration->getGlobalConfig()); + } +} diff --git a/app/Http/Admin/CapabilitiesController.php b/app/Http/Admin/CapabilitiesController.php new file mode 100644 index 0000000..2c180ba --- /dev/null +++ b/app/Http/Admin/CapabilitiesController.php @@ -0,0 +1,23 @@ +json([ + 'enabled' => false, + 'chat' => false, + 'text_generation' => false, + 'can_manage_company' => false, + 'can_manage_global' => Gate::allows('manage ai config'), + ]); + } +} diff --git a/app/Http/AiConfigurationController.php b/app/Http/AiConfigurationController.php new file mode 100644 index 0000000..b37ded2 --- /dev/null +++ b/app/Http/AiConfigurationController.php @@ -0,0 +1,51 @@ + $values @param array $stored */ + protected function testConfiguration(array $values, array $stored): JsonResponse + { + $key = $values['ai_api_key'] ?? ''; + $key = in_array($key, ['', '********'], true) ? ($stored['ai_api_key'] ?? '') : $key; + + if ($key === '') { + return response()->json(['error' => 'missing_api_key'], 422); + } + + try { + $details = AiDriverFactory::make( + (string) $values['ai_driver'], + (string) $key, + ['base_url' => $values['ai_base_url'] ?? null], + )->validateConnection(); + } catch (AiException $exception) { + return response()->json([ + 'error' => $exception->errorKey, + 'message' => $exception->getMessage(), + ], 422); + } + + return response()->json(['success' => true, 'details' => $details]); + } + + /** @param array $config @return array */ + protected function masked(array $config): array + { + if (! empty($config['ai_api_key'])) { + $config['ai_api_key'] = '********'; + } + + return $config; + } +} diff --git a/app/Http/Company/CapabilitiesController.php b/app/Http/Company/CapabilitiesController.php new file mode 100644 index 0000000..2c5d762 --- /dev/null +++ b/app/Http/Company/CapabilitiesController.php @@ -0,0 +1,30 @@ +authorizeAbility('use ai'); + $config = $this->configuration->resolveForCompany((int) $request->header('company')); + + return response()->json([ + 'enabled' => $config !== null, + 'chat' => (bool) ($config['chat_enabled'] ?? false), + 'text_generation' => (bool) ($config['text_generation_enabled'] ?? false), + 'can_manage_company' => Gate::allows('owner only'), + 'can_manage_global' => Gate::allows('manage ai config'), + ]); + } +} diff --git a/app/Http/Company/ChatController.php b/app/Http/Company/ChatController.php new file mode 100644 index 0000000..817f298 --- /dev/null +++ b/app/Http/Company/ChatController.php @@ -0,0 +1,41 @@ +authorizeAbility('use ai'); + $values = $request->validated(); + $companyId = (int) $request->header('company'); + $userId = (int) $request->user()->getAuthIdentifier(); + $conversation = $this->conversation($values['conversation_id'] ?? null, $companyId, $userId, $values['message']); + try { + $message = $this->assistant->chat($conversation, $values['message']); + } catch (AiException $exception) { + return response()->json(['error' => $exception->errorKey, 'message' => $exception->getMessage()], 422); + } + $conversation->refresh(); + + return response()->json(['conversation' => $conversation->only(['id', 'title', 'model', 'updated_at']), 'message' => $message->only(['id', 'role', 'content', 'created_at'])]); + } + + private function conversation(?int $id, int $companyId, int $userId, string $message): AiConversation + { + $existing = $id === null ? null : AiConversation::query()->whereKey($id)->where('company_id', $companyId)->where('user_id', $userId)->first(); + + return $existing ?? $this->assistant->startConversation($companyId, $userId, $message); + } +} diff --git a/app/Http/Company/CompanyAiConfigurationController.php b/app/Http/Company/CompanyAiConfigurationController.php new file mode 100644 index 0000000..1c9ca79 --- /dev/null +++ b/app/Http/Company/CompanyAiConfigurationController.php @@ -0,0 +1,41 @@ +authorizeAbility('owner only'); + + return response()->json($this->masked($this->configuration->getCompanyConfig((int) $request->header('company')))); + } + + public function saveConfig(AiConfigurationRequest $request): JsonResponse + { + $this->authorizeAbility('owner only'); + $companyId = (int) $request->header('company'); + $values = $request->validated(); + if (in_array($values['ai_api_key'] ?? '', ['', '********'], true)) { + $values['ai_api_key'] = $this->configuration->getCompanyConfig($companyId)['ai_api_key']; + } + $this->configuration->saveCompanyConfig($companyId, $values); + + return response()->json(['success' => true]); + } + + public function testConnection(ConnectionTestRequest $request): JsonResponse + { + $this->authorizeAbility('owner only'); + + return $this->testConfiguration($request->validated(), $this->configuration->getCompanyConfig((int) $request->header('company'))); + } +} diff --git a/app/Http/Company/ConversationController.php b/app/Http/Company/ConversationController.php new file mode 100644 index 0000000..8def793 --- /dev/null +++ b/app/Http/Company/ConversationController.php @@ -0,0 +1,51 @@ +authorizeAbility('use ai'); + + return response()->json(['conversations' => $this->owned($request)->latest('updated_at')->limit(50)->get(['id', 'title', 'model', 'updated_at', 'created_at'])]); + } + + public function show(Request $request, int $id): JsonResponse + { + $this->authorizeAbility('use ai'); + $conversation = $this->owned($request)->findOrFail($id); + + return response()->json(['conversation' => $conversation->only(['id', 'title', 'model', 'created_at', 'updated_at']), 'messages' => $conversation->messages()->whereIn('role', ['user', 'assistant'])->get(['id', 'role', 'content', 'created_at'])]); + } + + public function update(RenameConversationRequest $request, int $id): JsonResponse + { + $this->authorizeAbility('use ai'); + $this->owned($request)->findOrFail($id)->update(['title' => $request->validated('title')]); + + return response()->json(['success' => true]); + } + + public function destroy(Request $request, int $id): JsonResponse + { + $this->authorizeAbility('use ai'); + $this->owned($request)->findOrFail($id)->delete(); + + return response()->json(['success' => true]); + } + + private function owned(Request $request): Builder + { + return AiConversation::query()->where('company_id', (int) $request->header('company'))->where('user_id', (int) $request->user()->getAuthIdentifier()); + } +} diff --git a/app/Http/Company/GenerationController.php b/app/Http/Company/GenerationController.php new file mode 100644 index 0000000..2783b3a --- /dev/null +++ b/app/Http/Company/GenerationController.php @@ -0,0 +1,29 @@ +authorizeAbility('use ai'); + $values = $request->validated(); + try { + $text = $this->generator->generate((int) $request->header('company'), $values['prompt'], $values['context'] ?? null); + } catch (AiException $exception) { + return response()->json(['error' => $exception->errorKey, 'message' => $exception->getMessage()], 422); + } + + return response()->json(['text' => $text]); + } +} diff --git a/app/Http/ModuleController.php b/app/Http/ModuleController.php new file mode 100644 index 0000000..22e8bf8 --- /dev/null +++ b/app/Http/ModuleController.php @@ -0,0 +1,16 @@ +> */ + public function rules(): array + { + return [ + 'use_custom_ai_config' => ['nullable', Rule::in(['YES', 'NO'])], + 'ai_enabled' => ['nullable', Rule::in(['YES', 'NO'])], + 'ai_driver' => ['required_if:ai_enabled,YES', 'nullable', 'string', Rule::in(AiDriverFactory::availableDrivers())], + 'ai_api_key' => ['required_if:ai_enabled,YES', 'nullable', 'string', 'max:1000'], + 'ai_base_url' => ['nullable', 'url', 'max:500', new PublicHttpUrl], + 'ai_chat_enabled' => ['nullable', Rule::in(['YES', 'NO'])], + 'ai_chat_model' => ['nullable', 'string', 'max:200'], + 'ai_text_generation_enabled' => ['nullable', Rule::in(['YES', 'NO'])], + 'ai_text_generation_model' => ['nullable', 'string', 'max:200'], + ]; + } +} diff --git a/app/Http/Requests/ChatRequest.php b/app/Http/Requests/ChatRequest.php new file mode 100644 index 0000000..5dd5570 --- /dev/null +++ b/app/Http/Requests/ChatRequest.php @@ -0,0 +1,20 @@ + ['nullable', 'integer'], 'message' => ['required', 'string', 'max:10000']]; + } +} diff --git a/app/Http/Requests/ConnectionTestRequest.php b/app/Http/Requests/ConnectionTestRequest.php new file mode 100644 index 0000000..e142476 --- /dev/null +++ b/app/Http/Requests/ConnectionTestRequest.php @@ -0,0 +1,23 @@ + ['required', 'string', Rule::in(AiDriverFactory::availableDrivers())], 'ai_api_key' => ['nullable', 'string', 'max:1000'], 'ai_base_url' => ['nullable', 'url', 'max:500', new PublicHttpUrl]]; + } +} diff --git a/app/Http/Requests/GenerationRequest.php b/app/Http/Requests/GenerationRequest.php new file mode 100644 index 0000000..b8690ac --- /dev/null +++ b/app/Http/Requests/GenerationRequest.php @@ -0,0 +1,20 @@ + ['required', 'string', 'max:4000'], 'context' => ['nullable', 'string', 'max:20000']]; + } +} diff --git a/app/Http/Requests/RenameConversationRequest.php b/app/Http/Requests/RenameConversationRequest.php new file mode 100644 index 0000000..37cd202 --- /dev/null +++ b/app/Http/Requests/RenameConversationRequest.php @@ -0,0 +1,20 @@ + ['required', 'string', 'max:255']]; + } +} diff --git a/app/Lifecycle/DataCleanup.php b/app/Lifecycle/DataCleanup.php new file mode 100644 index 0000000..2cafe41 --- /dev/null +++ b/app/Lifecycle/DataCleanup.php @@ -0,0 +1,24 @@ +settings->deleteGlobal($key); + $this->settings->deleteCompanyForAll('company_'.$key); + } + $this->settings->deleteCompanyForAll('use_custom_ai_config'); + } +} diff --git a/app/Models/AiConversation.php b/app/Models/AiConversation.php new file mode 100644 index 0000000..aed7910 --- /dev/null +++ b/app/Models/AiConversation.php @@ -0,0 +1,28 @@ +hasMany(AiMessage::class, 'conversation_id')->orderBy('created_at'); + } +} diff --git a/app/Models/AiMessage.php b/app/Models/AiMessage.php new file mode 100644 index 0000000..2dc8f5c --- /dev/null +++ b/app/Models/AiMessage.php @@ -0,0 +1,53 @@ + 'array', + 'tokens_in' => 'integer', + 'tokens_out' => 'integer', + 'created_at' => 'datetime', + ]; + } + + public function conversation(): BelongsTo + { + return $this->belongsTo(AiConversation::class, 'conversation_id'); + } +} diff --git a/app/Prompting/PromptLoader.php b/app/Prompting/PromptLoader.php new file mode 100644 index 0000000..e33c398 --- /dev/null +++ b/app/Prompting/PromptLoader.php @@ -0,0 +1,55 @@ + $vars Placeholder => value map. + * + * @throws RuntimeException when the template file does not exist. + */ + public static function load(string $name, array $vars = []): string + { + $path = dirname(__DIR__, 2)."/resources/ai/prompts/{$name}.md"; + + if (! is_file($path)) { + throw new RuntimeException("Missing AI prompt template: {$name} (expected at {$path})"); + } + + $template = (string) file_get_contents($path); + + if ($vars === []) { + return trim($template); + } + + $replacements = []; + foreach ($vars as $key => $value) { + $replacements['{{'.$key.'}}'] = (string) $value; + } + + return trim(strtr($template, $replacements)); + } +} diff --git a/app/Providers/AiAssistantServiceProvider.php b/app/Providers/AiAssistantServiceProvider.php new file mode 100644 index 0000000..8dfc1dc --- /dev/null +++ b/app/Providers/AiAssistantServiceProvider.php @@ -0,0 +1,99 @@ +app->singleton(AiToolRegistry::class, function (Application $app): AiToolRegistry { + return new AiToolRegistry($app->make(ModuleAuthorization::class), [ + $app->make(SearchInvoicesTool::class), $app->make(GetInvoiceTool::class), + $app->make(ListOverdueInvoicesTool::class), $app->make(SearchCustomersTool::class), + $app->make(GetCustomerTool::class), $app->make(ListRecentPaymentsTool::class), + $app->make(SearchItemsTool::class), $app->make(ListExpenseCategoriesTool::class), + $app->make(GetCompanyStatsTool::class), $app->make(RankTopCustomersTool::class), + $app->make(RankTopItemsTool::class), $app->make(RankExpenseCategoriesTool::class), + ]); + }); + } + + public function boot(): void + { + parent::boot(); + + $modulePath = module_path($this->name); + Registry::registerScript('ai-assistant', $modulePath.'/dist/init.js'); + Registry::registerStyle('ai-assistant', $modulePath.'/dist/style.css'); + Registry::registerAiDriver('openrouter', [ + 'class' => OpenRouterDriver::class, + 'label' => 'OpenRouter', + 'website' => 'https://openrouter.ai', + 'default_base_url' => 'https://openrouter.ai/api/v1', + 'supported_roles' => ['chat', 'text_generation'], + 'suggested_models' => [ + ['value' => 'anthropic/claude-sonnet-4.6', 'label' => 'Anthropic Claude Sonnet 4.6'], + ['value' => 'anthropic/claude-haiku-4.5', 'label' => 'Anthropic Claude Haiku 4.5'], + ['value' => 'anthropic/claude-opus-4.6', 'label' => 'Anthropic Claude Opus 4.6'], + ['value' => 'openai/gpt-5.4', 'label' => 'OpenAI GPT-5.4'], + ['value' => 'openai/gpt-5.4-mini', 'label' => 'OpenAI GPT-5.4 mini'], + ['value' => 'google/gemini-3.1-pro-preview', 'label' => 'Google Gemini 3.1 Pro (preview)'], + ['value' => 'google/gemini-3.1-flash-lite-preview', 'label' => 'Google Gemini 3.1 Flash Lite (preview)'], + ['value' => 'z-ai/glm-5.1', 'label' => 'Z.AI GLM 5.1'], + ['value' => 'z-ai/glm-4.7-flash', 'label' => 'Z.AI GLM 4.7 Flash'], + ], + 'config_fields' => [[ + 'key' => 'base_url', + 'type' => 'text', + 'label' => 'Base URL', + 'default' => 'https://openrouter.ai/api/v1', + ]], + ]); + $this->app->bind(DataCleanup::class, fn (Application $app): DataCleanup => new DataCleanup( + $app->make(SettingsStore::class), + )); + + Gate::define('manage ai config', static fn (mixed $user): bool => is_object($user) + && method_exists($user, 'isSuperAdmin') && $user->isSuperAdmin()); + Gate::define('use ai', static fn (): bool => true); + RateLimiter::for('ai-assistant', static function (Request $request): Limit { + $userId = $request->user()?->getAuthIdentifier() ?? $request->ip(); + + return Limit::perMinute(30)->by($userId.':'.$request->header('company', 'none')); + }); + $this->loadRoutesFrom($modulePath.'/routes/api.php'); + } +} diff --git a/app/Rules/PublicHttpUrl.php b/app/Rules/PublicHttpUrl.php new file mode 100644 index 0000000..1dac58f --- /dev/null +++ b/app/Rules/PublicHttpUrl.php @@ -0,0 +1,19 @@ + */ + private const BLOCKED_IPV4 = [ + '0.0.0.0/8', '10.0.0.0/8', '100.64.0.0/10', '127.0.0.0/8', '169.254.0.0/16', + '172.16.0.0/12', '192.0.0.0/24', '192.0.2.0/24', '192.168.0.0/16', + '198.18.0.0/15', '198.51.100.0/24', '203.0.113.0/24', '240.0.0.0/4', + ]; + + /** @var list */ + private const BLOCKED_IPV6 = ['::1/128', '::/128', 'fc00::/7', 'fe80::/10', '64:ff9b::/96', '2001:db8::/32']; + + public static function blockedReason(string $url): ?string + { + $url = trim($url); + if ($url === '') { + return null; + } + $parts = parse_url($url); + if ($parts === false || ! isset($parts['scheme'], $parts['host'])) { + return 'URL must include a scheme and host'; + } + if (! in_array(strtolower($parts['scheme']), ['http', 'https'], true)) { + return 'URL scheme must be http or https'; + } + $host = trim($parts['host'], '[]'); + if ($host === '') { + return 'URL must include a host'; + } + if (filter_var($host, FILTER_VALIDATE_IP) !== false) { + return self::ipIsBlocked($host) ? "URL host {$host} is a private or reserved address" : null; + } + foreach (self::resolveHost($host) as $ip) { + if (self::ipIsBlocked($ip)) { + return "URL host {$host} resolves to a private or reserved address ({$ip})"; + } + } + + return null; + } + + public static function assertAllowed(string $url): void + { + $reason = self::blockedReason($url); + if ($reason !== null) { + throw new BlockedUrlException($reason); + } + } + + public static function ipIsBlocked(string $ip): bool + { + $mapped = self::extractMappedIpv4($ip); + if ($mapped !== null) { + return self::ipIsBlocked($mapped); + } + $blocks = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false ? self::BLOCKED_IPV4 : self::BLOCKED_IPV6; + foreach ($blocks as $cidr) { + if (self::ipInCidr($ip, $cidr)) { + return true; + } + } + + return false; + } + + public static function ipInCidr(string $ip, string $cidr): bool + { + [$subnet, $bits] = array_pad(explode('/', $cidr, 2), 2, null); + $ipBin = @inet_pton($ip); + $subnetBin = @inet_pton((string) $subnet); + if ($ipBin === false || $subnetBin === false || strlen($ipBin) !== strlen($subnetBin)) { + return false; + } + $wholeBytes = intdiv((int) $bits, 8); + $remainder = (int) $bits % 8; + if ($wholeBytes > 0 && strncmp($ipBin, $subnetBin, $wholeBytes) !== 0) { + return false; + } + if ($remainder === 0) { + return true; + } + $mask = (~((1 << (8 - $remainder)) - 1)) & 0xFF; + + return (ord($ipBin[$wholeBytes]) & $mask) === (ord($subnetBin[$wholeBytes]) & $mask); + } + + /** @return list */ + private static function resolveHost(string $host): array + { + $ips = []; + if (function_exists('gethostbynamel')) { + $v4 = @gethostbynamel($host); + if (is_array($v4)) { + $ips = $v4; + } + } + if (function_exists('dns_get_record')) { + $records = @dns_get_record($host, DNS_AAAA); + if (is_array($records)) { + foreach ($records as $record) { + if (isset($record['ipv6'])) { + $ips[] = $record['ipv6']; + } + } + } + } + + return array_values(array_unique($ips)); + } + + private static function extractMappedIpv4(string $ip): ?string + { + $bin = @inet_pton($ip); + if ($bin === false || strlen($bin) !== 16 || strncmp($bin, str_repeat("\x00", 10)."\xff\xff", 12) !== 0) { + return null; + } + $ipv4 = inet_ntop(substr($bin, 12, 4)); + + return $ipv4 === false ? null : $ipv4; + } +} diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..a15168c --- /dev/null +++ b/composer.json @@ -0,0 +1,49 @@ +{ + "name": "invoiceshelf/module-ai-assistant", + "description": "Official free AI Assistant module for InvoiceShelf", + "license": "AGPL-3.0-only", + "authors": [ + { + "name": "InvoiceShelf", + "email": "hello@invoiceshelf.com" + } + ], + "require": { + "php": "^8.4", + "ext-json": "*", + "invoiceshelf/modules": "^3.3" + }, + "require-dev": { + "laravel/pint": "^1.26", + "orchestra/testbench": "^11.0", + "phpunit/phpunit": "^12.0" + }, + "autoload": { + "psr-4": { + "Modules\\AiAssistant\\": "app/" + } + }, + "autoload-dev": { + "psr-4": { + "Modules\\AiAssistant\\Tests\\": "tests/" + } + }, + "scripts": { + "test": "vendor/bin/phpunit", + "lint": "vendor/bin/pint --test" + }, + "config": { + "allow-plugins": { + "wikimedia/composer-merge-plugin": true + }, + "sort-packages": true + }, + "minimum-stability": "stable", + "prefer-stable": true, + "repositories": { + "invoiceshelf-modules": { + "type": "vcs", + "url": "https://github.com/InvoiceShelf/modules.git" + } + } +} diff --git a/database/migrations/2026_04_11_154445_create_ai_conversations_and_messages_tables.php b/database/migrations/2026_04_11_154445_create_ai_conversations_and_messages_tables.php new file mode 100644 index 0000000..75c9267 --- /dev/null +++ b/database/migrations/2026_04_11_154445_create_ai_conversations_and_messages_tables.php @@ -0,0 +1,59 @@ +id(); + $table->unsignedInteger('company_id'); + $table->unsignedInteger('user_id'); + $table->string('title')->nullable(); + $table->string('model', 100)->nullable(); + $table->timestamps(); + + // List my conversations, most recently updated first + $table->index(['company_id', 'user_id', 'updated_at']); + }); + + Schema::create('ai_messages', function (Blueprint $table) { + $table->id(); + $table->foreignId('conversation_id') + ->constrained('ai_conversations') + ->cascadeOnDelete(); + + // OpenAI chat message roles. Persisted as string (not enum) so future + // roles don't require a migration — the application layer validates. + $table->string('role', 20); + + $table->longText('content')->nullable(); + + // For role=tool messages: which tool_call_id from the assistant turn this answers. + $table->string('tool_call_id')->nullable(); + + // For role=assistant messages that requested tool execution: the parsed tool_calls array. + $table->json('tool_calls')->nullable(); + + // Which model produced this turn (nullable for user/tool messages). + $table->string('model', 100)->nullable(); + + // For future cost tracking dashboards. + $table->unsignedInteger('tokens_in')->nullable(); + $table->unsignedInteger('tokens_out')->nullable(); + + $table->timestamp('created_at')->useCurrent(); + + $table->index(['conversation_id', 'created_at']); + }); + } + + public function down(): void + { + Schema::dropIfExists('ai_messages'); + Schema::dropIfExists('ai_conversations'); + } +}; diff --git a/dist/init.js b/dist/init.js new file mode 100644 index 0000000..bba79b8 --- /dev/null +++ b/dist/init.js @@ -0,0 +1,3059 @@ +const { Fragment: e, Teleport: t, computed: n, createBlock: r, createCommentVNode: i, createElementBlock: a, createElementVNode: o, createTextVNode: s, createVNode: c, defineComponent: l, h: u, mergeModels: d, nextTick: f, normalizeClass: p, onMounted: m, openBlock: h, reactive: g, ref: _, renderList: v, resolveComponent: y, toDisplayString: b, unref: x, useModel: S, vModelCheckbox: C, vModelDynamic: w, vModelSelect: ee, vModelText: T, watch: te, withCtx: E, withDirectives: ne, withKeys: re, withModifiers: ie } = window.__invoiceshelf_vue; +//#region node_modules/.pnpm/dompurify@3.4.13/node_modules/dompurify/dist/purify.es.mjs +function D(e, t) { + (t == null || t > e.length) && (t = e.length); + for (var n = 0, r = Array(t); n < t; n++) r[n] = e[n]; + return r; +} +function ae(e) { + if (Array.isArray(e)) return e; +} +function oe(e, t) { + var n = e == null ? null : typeof Symbol < "u" && e[Symbol.iterator] || e["@@iterator"]; + if (n != null) { + var r, i, a, o, s = [], c = !0, l = !1; + try { + if (a = (n = n.call(e)).next, t !== 0) for (; !(c = (r = a.call(n)).done) && (s.push(r.value), s.length !== t); c = !0); + } catch (e) { + l = !0, i = e; + } finally { + try { + if (!c && n.return != null && (o = n.return(), Object(o) !== o)) return; + } finally { + if (l) throw i; + } + } + return s; + } +} +function se() { + throw TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); +} +function O(e, t) { + return ae(e) || oe(e, t) || k(e, t) || se(); +} +function k(e, t) { + if (e) { + if (typeof e == "string") return D(e, t); + var n = {}.toString.call(e).slice(8, -1); + return n === "Object" && e.constructor && (n = e.constructor.name), n === "Map" || n === "Set" ? Array.from(e) : n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n) ? D(e, t) : void 0; + } +} +var ce = Object.entries, le = Object.setPrototypeOf, ue = Object.isFrozen, de = Object.getPrototypeOf, fe = Object.getOwnPropertyDescriptor, A = Object.freeze, j = Object.seal, pe = Object.create, me = typeof Reflect < "u" && Reflect, he = me.apply, ge = me.construct; +A ||= function(e) { + return e; +}, j ||= function(e) { + return e; +}, he ||= function(e, t) { + var n = [...arguments].slice(2); + return e.apply(t, n); +}, ge ||= function(e) { + return new e(...[...arguments].slice(1)); +}; +var _e = I(Array.prototype.forEach), ve = I(Array.prototype.lastIndexOf), ye = I(Array.prototype.pop), be = I(Array.prototype.push), xe = I(Array.prototype.splice), Se = Array.isArray, Ce = I(String.prototype.toLowerCase), we = I(String.prototype.toString), Te = I(String.prototype.match), Ee = I(String.prototype.replace), De = I(String.prototype.indexOf), Oe = I(String.prototype.trim), ke = I(Number.prototype.toString), Ae = I(Boolean.prototype.toString), M = typeof BigInt > "u" ? null : I(BigInt.prototype.toString), je = typeof Symbol > "u" ? null : I(Symbol.prototype.toString), N = I(Object.prototype.hasOwnProperty), P = I(Object.prototype.toString), F = I(RegExp.prototype.test), Me = L(TypeError); +function I(e) { + return function(t) { + t instanceof RegExp && (t.lastIndex = 0); + var n = [...arguments].slice(1); + return he(e, t, n); + }; +} +function L(e) { + return function() { + return ge(e, [...arguments]); + }; +} +function R(e, t) { + let n = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : Ce; + if (le && le(e, null), !Se(t)) return e; + let r = t.length; + for (; r--;) { + let i = t[r]; + if (typeof i == "string") { + let e = n(i); + e !== i && (ue(t) || (t[r] = e), i = e); + } + e[i] = !0; + } + return e; +} +function Ne(e) { + for (let t = 0; t < e.length; t++) N(e, t) || (e[t] = null); + return e; +} +function z(e) { + let t = pe(null); + for (let r of ce(e)) { + var n = O(r, 2); + let i = n[0], a = n[1]; + N(e, i) && (t[i] = Se(a) ? Ne(a) : a && typeof a == "object" && a.constructor === Object ? z(a) : a); + } + return t; +} +function Pe(e) { + switch (typeof e) { + case "string": return e; + case "number": return ke(e); + case "boolean": return Ae(e); + case "bigint": return M ? M(e) : "0"; + case "symbol": return je ? je(e) : "Symbol()"; + case "undefined": return P(e); + case "function": + case "object": { + if (e === null) return P(e); + let t = e, n = B(t, "toString"); + if (typeof n == "function") { + let e = n(t); + return typeof e == "string" ? e : P(e); + } + return P(e); + } + default: return P(e); + } +} +function B(e, t) { + for (; e !== null;) { + let n = fe(e, t); + if (n) { + if (n.get) return I(n.get); + if (typeof n.value == "function") return I(n.value); + } + e = de(e); + } + function n() { + return null; + } + return n; +} +function Fe(e) { + try { + return F(e, ""), !0; + } catch { + return !1; + } +} +var Ie = A(/* @__PURE__ */ "a.abbr.acronym.address.area.article.aside.audio.b.bdi.bdo.big.blink.blockquote.body.br.button.canvas.caption.center.cite.code.col.colgroup.content.data.datalist.dd.decorator.del.details.dfn.dialog.dir.div.dl.dt.element.em.fieldset.figcaption.figure.font.footer.form.h1.h2.h3.h4.h5.h6.head.header.hgroup.hr.html.i.img.input.ins.kbd.label.legend.li.main.map.mark.marquee.menu.menuitem.meter.nav.nobr.ol.optgroup.option.output.p.picture.pre.progress.q.rp.rt.ruby.s.samp.search.section.select.shadow.slot.small.source.spacer.span.strike.strong.style.sub.summary.sup.table.tbody.td.template.textarea.tfoot.th.thead.time.tr.track.tt.u.ul.var.video.wbr".split(".")), Le = A(/* @__PURE__ */ "svg.a.altglyph.altglyphdef.altglyphitem.animatecolor.animatemotion.animatetransform.circle.clippath.defs.desc.ellipse.enterkeyhint.exportparts.filter.font.g.glyph.glyphref.hkern.image.inputmode.line.lineargradient.marker.mask.metadata.mpath.part.path.pattern.polygon.polyline.radialgradient.rect.stop.style.switch.symbol.text.textpath.title.tref.tspan.view.vkern".split(".")), Re = A([ + "feBlend", + "feColorMatrix", + "feComponentTransfer", + "feComposite", + "feConvolveMatrix", + "feDiffuseLighting", + "feDisplacementMap", + "feDistantLight", + "feDropShadow", + "feFlood", + "feFuncA", + "feFuncB", + "feFuncG", + "feFuncR", + "feGaussianBlur", + "feImage", + "feMerge", + "feMergeNode", + "feMorphology", + "feOffset", + "fePointLight", + "feSpecularLighting", + "feSpotLight", + "feTile", + "feTurbulence" +]), ze = A([ + "animate", + "color-profile", + "cursor", + "discard", + "font-face", + "font-face-format", + "font-face-name", + "font-face-src", + "font-face-uri", + "foreignobject", + "hatch", + "hatchpath", + "mesh", + "meshgradient", + "meshpatch", + "meshrow", + "missing-glyph", + "script", + "set", + "solidcolor", + "unknown", + "use" +]), Be = A(/* @__PURE__ */ "math.menclose.merror.mfenced.mfrac.mglyph.mi.mlabeledtr.mmultiscripts.mn.mo.mover.mpadded.mphantom.mroot.mrow.ms.mspace.msqrt.mstyle.msub.msup.msubsup.mtable.mtd.mtext.mtr.munder.munderover.mprescripts".split(".")), Ve = A([ + "maction", + "maligngroup", + "malignmark", + "mlongdiv", + "mscarries", + "mscarry", + "msgroup", + "mstack", + "msline", + "msrow", + "semantics", + "annotation", + "annotation-xml", + "mprescripts", + "none" +]), He = A(["#text"]), Ue = A(/* @__PURE__ */ "accept.action.align.alt.autocapitalize.autocomplete.autopictureinpicture.autoplay.background.bgcolor.border.capture.cellpadding.cellspacing.checked.cite.class.clear.color.cols.colspan.command.commandfor.controls.controlslist.coords.crossorigin.datetime.decoding.default.dir.disabled.disablepictureinpicture.disableremoteplayback.download.draggable.enctype.enterkeyhint.exportparts.face.for.headers.height.hidden.high.href.hreflang.id.inert.inputmode.integrity.ismap.kind.label.lang.list.loading.loop.low.max.maxlength.media.method.min.minlength.multiple.muted.name.nonce.noshade.novalidate.nowrap.open.optimum.part.pattern.placeholder.playsinline.popover.popovertarget.popovertargetaction.poster.preload.pubdate.radiogroup.readonly.rel.required.rev.reversed.role.rows.rowspan.spellcheck.scope.selected.shape.size.sizes.slot.span.srclang.start.src.srcset.step.style.summary.tabindex.title.translate.type.usemap.valign.value.width.wrap.xmlns".split(".")), We = A(/* @__PURE__ */ "accent-height.accumulate.additive.alignment-baseline.amplitude.ascent.attributename.attributetype.azimuth.basefrequency.baseline-shift.begin.bias.by.class.clip.clippathunits.clip-path.clip-rule.color.color-interpolation.color-interpolation-filters.color-profile.color-rendering.cx.cy.d.dx.dy.diffuseconstant.direction.display.divisor.dominant-baseline.dur.edgemode.elevation.end.exponent.fill.fill-opacity.fill-rule.filter.filterunits.flood-color.flood-opacity.font-family.font-size.font-size-adjust.font-stretch.font-style.font-variant.font-weight.fx.fy.g1.g2.glyph-name.glyphref.gradientunits.gradienttransform.height.href.id.image-rendering.in.in2.intercept.k.k1.k2.k3.k4.kerning.keypoints.keysplines.keytimes.lang.lengthadjust.letter-spacing.kernelmatrix.kernelunitlength.lighting-color.local.marker-end.marker-mid.marker-start.markerheight.markerunits.markerwidth.maskcontentunits.maskunits.max.mask.mask-type.media.method.mode.min.name.numoctaves.offset.operator.opacity.order.orient.orientation.origin.overflow.paint-order.path.pathlength.patterncontentunits.patterntransform.patternunits.points.preservealpha.preserveaspectratio.primitiveunits.r.rx.ry.radius.refx.refy.repeatcount.repeatdur.restart.result.rotate.scale.seed.shape-rendering.slope.specularconstant.specularexponent.spreadmethod.startoffset.stddeviation.stitchtiles.stop-color.stop-opacity.stroke-dasharray.stroke-dashoffset.stroke-linecap.stroke-linejoin.stroke-miterlimit.stroke-opacity.stroke.stroke-width.style.surfacescale.systemlanguage.tabindex.tablevalues.targetx.targety.transform.transform-origin.text-anchor.text-decoration.text-orientation.text-rendering.textlength.type.u1.u2.unicode.values.viewbox.visibility.version.vert-adv-y.vert-origin-x.vert-origin-y.width.word-spacing.wrap.writing-mode.xchannelselector.ychannelselector.x.x1.x2.xmlns.y.y1.y2.z.zoomandpan".split(".")), Ge = A(/* @__PURE__ */ "accent.accentunder.align.bevelled.close.columnalign.columnlines.columnspacing.columnspan.denomalign.depth.dir.display.displaystyle.encoding.fence.frame.height.href.id.largeop.length.linethickness.lquote.lspace.mathbackground.mathcolor.mathsize.mathvariant.maxsize.minsize.movablelimits.notation.numalign.open.rowalign.rowlines.rowspacing.rowspan.rspace.rquote.scriptlevel.scriptminsize.scriptsizemultiplier.selection.separator.separators.stretchy.subscriptshift.supscriptshift.symmetric.voffset.width.xmlns".split(".")), Ke = A([ + "xlink:href", + "xml:id", + "xlink:title", + "xml:space", + "xmlns:xlink" +]), qe = j(/{{[\w\W]*|^[\w\W]*}}/g), Je = j(/<%[\w\W]*|^[\w\W]*%>/g), Ye = j(/\${[\w\W]*/g), Xe = j(/^data-[\-\w.\u00B7-\uFFFF]+$/), Ze = j(/^aria-[\-\w]+$/), Qe = j(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i), $e = j(/^(?:\w+script|data):/i), et = j(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g), tt = j(/^html$/i), nt = j(/^[a-z][.\w]*(-[.\w]+)+$/i), rt = j(/<[/\w!]/g), it = j(/<[/\w]/g), at = j(/<\/no(script|embed|frames)/i), ot = j(/\/>/i), V = { + element: 1, + attribute: 2, + text: 3, + cdataSection: 4, + entityReference: 5, + entityNode: 6, + processingInstruction: 7, + comment: 8, + document: 9, + documentType: 10, + documentFragment: 11, + notation: 12 +}, st = function() { + return typeof window > "u" ? null : window; +}, ct = function(e, t) { + if (typeof e != "object" || typeof e.createPolicy != "function") return null; + let n = null, r = "data-tt-policy-suffix"; + t && t.hasAttribute(r) && (n = t.getAttribute(r)); + let i = "dompurify" + (n ? "#" + n : ""); + try { + return e.createPolicy(i, { + createHTML(e) { + return e; + }, + createScriptURL(e) { + return e; + } + }); + } catch { + return console.warn("TrustedTypes policy " + i + " could not be created."), null; + } +}, lt = function() { + return { + afterSanitizeAttributes: [], + afterSanitizeElements: [], + afterSanitizeShadowDOM: [], + beforeSanitizeAttributes: [], + beforeSanitizeElements: [], + beforeSanitizeShadowDOM: [], + uponSanitizeAttribute: [], + uponSanitizeElement: [], + uponSanitizeShadowNode: [] + }; +}, ut = function(e, t, n, r) { + return N(e, t) && Se(e[t]) ? R(r.base ? z(r.base) : {}, e[t], r.transform) : n; +}; +function dt() { + let e = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : st(), t = (e) => dt(e); + if (t.version = "3.4.13", t.removed = [], !e || !e.document || e.document.nodeType !== V.document || !e.Element) return t.isSupported = !1, t; + let n = e.document, r = n, i = r.currentScript; + e.DocumentFragment; + let a = e.HTMLTemplateElement, o = e.Node, s = e.Element, c = e.NodeFilter; + e.NamedNodeMap === void 0 && (e.NamedNodeMap || e.MozNamedAttrMap), e.HTMLFormElement; + let l = e.DOMParser, u = e.trustedTypes, d = s.prototype, f = B(d, "cloneNode"), p = B(d, "remove"), m = B(d, "nextSibling"), h = B(d, "childNodes"), g = B(d, "parentNode"), _ = B(d, "shadowRoot"), v = B(d, "attributes"), y = o && o.prototype ? B(o.prototype, "nodeType") : null, b = o && o.prototype ? B(o.prototype, "nodeName") : null, x = o && o.prototype ? B(o.prototype, "ownerDocument") : null; + if (typeof a == "function") { + let e = n.createElement("template"); + e.content && e.content.ownerDocument && (n = e.content.ownerDocument); + } + let S, C = "", w, ee = !1, T = 0, te = function() { + if (T > 0) throw Me("A configured TRUSTED_TYPES_POLICY callback (createHTML or createScriptURL) must not call DOMPurify.sanitize, as that causes infinite recursion. Do not pass a policy whose callbacks wrap DOMPurify as TRUSTED_TYPES_POLICY; see the \"DOMPurify and Trusted Types\" section of the README."); + }, E = function(e) { + te(), T++; + try { + return S.createHTML(e); + } finally { + T--; + } + }, ne = function(e) { + te(), T++; + try { + return S.createScriptURL(e); + } finally { + T--; + } + }, re = function() { + return ee ||= (w = ct(u, i), !0), w; + }, ie = n, D = ie.implementation, ae = ie.createNodeIterator, oe = ie.createDocumentFragment, se = ie.getElementsByTagName, O = r.importNode, k = lt(); + t.isSupported = typeof ce == "function" && typeof g == "function" && D && D.createHTMLDocument !== void 0; + let le = qe, ue = Je, de = Ye, fe = Xe, me = Ze, he = $e, ge = et, ke = nt, Ae = Qe, M = null, je = R({}, [ + ...Ie, + ...Le, + ...Re, + ...Be, + ...He + ]), P = null, I = R({}, [ + ...Ue, + ...We, + ...Ge, + ...Ke + ]), L = Object.seal(pe(null, { + tagNameCheck: { + writable: !0, + configurable: !1, + enumerable: !0, + value: null + }, + attributeNameCheck: { + writable: !0, + configurable: !1, + enumerable: !0, + value: null + }, + allowCustomizedBuiltInElements: { + writable: !0, + configurable: !1, + enumerable: !0, + value: !1 + } + })), Ne = null, ft = null, H = Object.seal(pe(null, { + tagCheck: { + writable: !0, + configurable: !1, + enumerable: !0, + value: null + }, + attributeCheck: { + writable: !0, + configurable: !1, + enumerable: !0, + value: null + } + })), pt = !0, mt = !0, ht = !1, gt = !0, U = !1, _t = !0, W = !1, vt = !1, yt = null, bt = null, xt = !1, St = !1, Ct = !1, wt = !1, Tt = !0, Et = !1, Dt = "user-content-", Ot = !0, kt = !1, At = {}, G = null, jt = R({}, /* @__PURE__ */ "annotation-xml.audio.colgroup.desc.foreignobject.head.iframe.math.mi.mn.mo.ms.mtext.noembed.noframes.noscript.plaintext.script.selectedcontent.style.svg.template.thead.title.video.xmp".split(".")), Mt = null, Nt = R({}, [ + "audio", + "video", + "img", + "source", + "image", + "track" + ]), Pt = null, Ft = R({}, [ + "alt", + "class", + "for", + "id", + "label", + "name", + "pattern", + "placeholder", + "role", + "summary", + "title", + "value", + "style", + "xmlns" + ]), It = "http://www.w3.org/1998/Math/MathML", Lt = "http://www.w3.org/2000/svg", K = "http://www.w3.org/1999/xhtml", Rt = K, zt = !1, Bt = null, Vt = R({}, [ + It, + Lt, + K + ], we), Ht = A([ + "mi", + "mo", + "mn", + "ms", + "mtext" + ]), Ut = R({}, Ht), q = A(["annotation-xml"]), Wt = R({}, q), Gt = R({}, [ + "title", + "style", + "font", + "a", + "script" + ]), Kt = null, qt = ["application/xhtml+xml", "text/html"], J = null, Jt = null, Yt = n.createElement("form"), Xt = function(e) { + return e instanceof RegExp || e instanceof Function; + }, Zt = function() { + let e = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : {}; + if (Jt && Jt === e) return; + (!e || typeof e != "object") && (e = {}), e = z(e), Kt = qt.indexOf(e.PARSER_MEDIA_TYPE) === -1 ? "text/html" : e.PARSER_MEDIA_TYPE, J = Kt === "application/xhtml+xml" ? we : Ce, M = ut(e, "ALLOWED_TAGS", je, { transform: J }), P = ut(e, "ALLOWED_ATTR", I, { transform: J }), Bt = ut(e, "ALLOWED_NAMESPACES", Vt, { transform: we }), Pt = ut(e, "ADD_URI_SAFE_ATTR", Ft, { + transform: J, + base: Ft + }), Mt = ut(e, "ADD_DATA_URI_TAGS", Nt, { + transform: J, + base: Nt + }), G = ut(e, "FORBID_CONTENTS", jt, { transform: J }), Ne = ut(e, "FORBID_TAGS", z({}), { transform: J }), ft = ut(e, "FORBID_ATTR", z({}), { transform: J }), At = N(e, "USE_PROFILES") ? e.USE_PROFILES && typeof e.USE_PROFILES == "object" ? z(e.USE_PROFILES) : e.USE_PROFILES : !1, pt = e.ALLOW_ARIA_ATTR !== !1, mt = e.ALLOW_DATA_ATTR !== !1, ht = e.ALLOW_UNKNOWN_PROTOCOLS || !1, gt = e.ALLOW_SELF_CLOSE_IN_ATTR !== !1, U = e.SAFE_FOR_TEMPLATES || !1, _t = e.SAFE_FOR_XML !== !1, W = e.WHOLE_DOCUMENT || !1, St = e.RETURN_DOM || !1, Ct = e.RETURN_DOM_FRAGMENT || !1, wt = e.RETURN_TRUSTED_TYPE || !1, xt = e.FORCE_BODY || !1, Tt = e.SANITIZE_DOM !== !1, Et = e.SANITIZE_NAMED_PROPS || !1, Ot = e.KEEP_CONTENT !== !1, kt = e.IN_PLACE || !1, Ae = Fe(e.ALLOWED_URI_REGEXP) ? e.ALLOWED_URI_REGEXP : Qe, Rt = typeof e.NAMESPACE == "string" ? e.NAMESPACE : K, Ut = N(e, "MATHML_TEXT_INTEGRATION_POINTS") && e.MATHML_TEXT_INTEGRATION_POINTS && typeof e.MATHML_TEXT_INTEGRATION_POINTS == "object" ? z(e.MATHML_TEXT_INTEGRATION_POINTS) : R({}, Ht), Wt = N(e, "HTML_INTEGRATION_POINTS") && e.HTML_INTEGRATION_POINTS && typeof e.HTML_INTEGRATION_POINTS == "object" ? z(e.HTML_INTEGRATION_POINTS) : R({}, q); + let t = N(e, "CUSTOM_ELEMENT_HANDLING") && e.CUSTOM_ELEMENT_HANDLING && typeof e.CUSTOM_ELEMENT_HANDLING == "object" ? z(e.CUSTOM_ELEMENT_HANDLING) : pe(null); + if (L = pe(null), N(t, "tagNameCheck") && Xt(t.tagNameCheck) && (L.tagNameCheck = t.tagNameCheck), N(t, "attributeNameCheck") && Xt(t.attributeNameCheck) && (L.attributeNameCheck = t.attributeNameCheck), N(t, "allowCustomizedBuiltInElements") && typeof t.allowCustomizedBuiltInElements == "boolean" && (L.allowCustomizedBuiltInElements = t.allowCustomizedBuiltInElements), j(L), U && (mt = !1), Ct && (St = !0), At && (M = R({}, He), P = pe(null), At.html === !0 && (R(M, Ie), R(P, Ue)), At.svg === !0 && (R(M, Le), R(P, We), R(P, Ke)), At.svgFilters === !0 && (R(M, Re), R(P, We), R(P, Ke)), At.mathMl === !0 && (R(M, Be), R(P, Ge), R(P, Ke))), H.tagCheck = null, H.attributeCheck = null, N(e, "ADD_TAGS") && (typeof e.ADD_TAGS == "function" ? H.tagCheck = e.ADD_TAGS : Se(e.ADD_TAGS) && (M === je && (M = z(M)), R(M, e.ADD_TAGS, J))), N(e, "ADD_ATTR") && (typeof e.ADD_ATTR == "function" ? H.attributeCheck = e.ADD_ATTR : Se(e.ADD_ATTR) && (P === I && (P = z(P)), R(P, e.ADD_ATTR, J))), N(e, "ADD_URI_SAFE_ATTR") && Se(e.ADD_URI_SAFE_ATTR) && R(Pt, e.ADD_URI_SAFE_ATTR, J), N(e, "FORBID_CONTENTS") && Se(e.FORBID_CONTENTS) && (G === jt && (G = z(G)), R(G, e.FORBID_CONTENTS, J)), N(e, "ADD_FORBID_CONTENTS") && Se(e.ADD_FORBID_CONTENTS) && (G === jt && (G = z(G)), R(G, e.ADD_FORBID_CONTENTS, J)), Ot && (M["#text"] = !0), W && R(M, [ + "html", + "head", + "body" + ]), M.table && (R(M, ["tbody"]), delete Ne.tbody), e.TRUSTED_TYPES_POLICY) { + if (typeof e.TRUSTED_TYPES_POLICY.createHTML != "function") throw Me("TRUSTED_TYPES_POLICY configuration option must provide a \"createHTML\" hook."); + if (typeof e.TRUSTED_TYPES_POLICY.createScriptURL != "function") throw Me("TRUSTED_TYPES_POLICY configuration option must provide a \"createScriptURL\" hook."); + let t = S; + S = e.TRUSTED_TYPES_POLICY; + try { + C = E(""); + } catch (e) { + throw S = t, e; + } + } else e.TRUSTED_TYPES_POLICY === null ? (S = void 0, C = "") : (S === void 0 && (S = re()), S && typeof C == "string" && (C = E(""))); + A && A(e), Jt = e; + }, Qt = R({}, [ + ...Le, + ...Re, + ...ze + ]), $t = R({}, [...Be, ...Ve]), en = function(e, t, n) { + return t.namespaceURI === K ? e === "svg" : t.namespaceURI === It ? e === "svg" && (n === "annotation-xml" || Ut[n]) : !!Qt[e]; + }, tn = function(e, t, n) { + return t.namespaceURI === K ? e === "math" : t.namespaceURI === Lt ? e === "math" && Wt[n] : !!$t[e]; + }, nn = function(e, t, n) { + return t.namespaceURI === Lt && !Wt[n] || t.namespaceURI === It && !Ut[n] ? !1 : !$t[e] && (Gt[e] || !Qt[e]); + }, rn = function(e) { + let t = g(e); + (!t || !t.tagName) && (t = { + namespaceURI: Rt, + tagName: "template" + }); + let n = Ce(e.tagName), r = Ce(t.tagName); + return Bt[e.namespaceURI] ? e.namespaceURI === Lt ? en(n, t, r) : e.namespaceURI === It ? tn(n, t, r) : e.namespaceURI === K ? nn(n, t, r) : !!(Kt === "application/xhtml+xml" && Bt[e.namespaceURI]) : !1; + }, an = function(e) { + be(t.removed, { element: e }); + try { + g(e).removeChild(e); + } catch { + if (p(e), !g(e)) throw Me("a node selected for removal could not be detached from its tree and cannot be safely returned; refusing to sanitize in place"); + } + }, on = function(e) { + ln(e); + let t = h(e); + if (t) { + let e = []; + _e(t, (t) => { + be(e, t); + }), _e(e, (e) => { + try { + p(e); + } catch {} + }); + } + let n = v(e); + if (n) for (let t = n.length - 1; t >= 0; --t) { + let r = n[t], i = r && r.name; + if (typeof i == "string") try { + e.removeAttribute(i); + } catch {} + } + }, sn = function(e, n) { + try { + be(t.removed, { + attribute: n.getAttributeNode(e), + from: n + }); + } catch { + be(t.removed, { + attribute: null, + from: n + }); + } + if (n.removeAttribute(e), e === "is") if (St || Ct) try { + an(n); + } catch {} + else try { + n.setAttribute(e, ""); + } catch {} + }, cn = function(e) { + let t = v(e); + if (t) for (let n = t.length - 1; n >= 0; --n) { + let r = t[n], i = r && r.name; + if (!(typeof i != "string" || P[J(i)])) try { + e.removeAttribute(i); + } catch {} + } + }, ln = function(e) { + let t = [e]; + for (; t.length > 0;) { + let e = t.pop(); + (y ? y(e) : e.nodeType) === V.element && cn(e); + let n = h(e); + if (n) for (let e = n.length - 1; e >= 0; --e) t.push(n[e]); + } + }, un = function(e) { + if (!_t) return; + let t = [e]; + for (; t.length > 0;) { + let e = t.pop(), n = y ? y(e) : e.nodeType; + if (n === V.processingInstruction || n === V.comment && F(it, e.data)) { + try { + p(e); + } catch {} + continue; + } + if (n === V.element) { + let t = e, n = J(b ? b(e) : e.nodeName); + try { + t.hasAttribute && t.hasAttribute("patchsrc") && t.removeAttribute("patchsrc"), t.hasAttribute && t.hasAttribute("for") && n !== "label" && n !== "output" && t.removeAttribute("for"); + } catch {} + } + let r = h(e); + if (r) for (let e = r.length - 1; e >= 0; --e) t.push(r[e]); + } + }, dn = function(e) { + let t = null, r = null; + if (xt) e = "" + e; + else { + let t = Te(e, /^[\r\n\t ]+/); + r = t && t[0]; + } + Kt === "application/xhtml+xml" && Rt === K && (e = "" + e + ""); + let i = S ? E(e) : e; + if (Rt === K) try { + t = new l().parseFromString(i, Kt); + } catch {} + if (!t || !t.documentElement) { + t = D.createDocument(Rt, "template", null); + try { + t.documentElement.innerHTML = zt ? C : i; + } catch {} + } + let a = t.body || t.documentElement; + return e && r && a.insertBefore(n.createTextNode(r), a.childNodes[0] || null), Rt === K ? se.call(t, W ? "html" : "body")[0] : W ? t.documentElement : a; + }, fn = function(e) { + let t = x ? x(e) : e.ownerDocument; + return ae.call(t || e, e, c.SHOW_ELEMENT | c.SHOW_COMMENT | c.SHOW_TEXT | c.SHOW_PROCESSING_INSTRUCTION | c.SHOW_CDATA_SECTION, null); + }, pn = function(e) { + return e = Ee(e, le, " "), e = Ee(e, ue, " "), e = Ee(e, de, " "), e; + }, mn = function(e) { + e.normalize(); + let t = x ? x(e) : e.ownerDocument, n = ae.call(t || e, e, c.SHOW_TEXT | c.SHOW_COMMENT | c.SHOW_CDATA_SECTION | c.SHOW_PROCESSING_INSTRUCTION, null), r = n.nextNode(); + for (; r;) r.data = pn(r.data), r = n.nextNode(); + let i = e.querySelectorAll?.call(e, "template"); + i && _e(i, (e) => { + gn(e.content) && mn(e.content); + }); + }, hn = function(e) { + let t = b ? b(e) : null; + return typeof t != "string" || J(t) !== "form" ? !1 : typeof e.nodeName != "string" || typeof e.textContent != "string" || typeof e.removeChild != "function" || e.attributes !== v(e) || typeof e.removeAttribute != "function" || typeof e.setAttribute != "function" || typeof e.namespaceURI != "string" || typeof e.insertBefore != "function" || typeof e.hasChildNodes != "function" || e.nodeType !== y(e) || e.childNodes !== h(e); + }, gn = function(e) { + if (!y || typeof e != "object" || !e) return !1; + try { + return y(e) === V.documentFragment; + } catch { + return !1; + } + }, _n = function(e) { + if (!y || typeof e != "object" || !e) return !1; + try { + return typeof y(e) == "number"; + } catch { + return !1; + } + }; + function Y(e, n, r) { + e.length !== 0 && _e(e, (e) => { + e.call(t, n, r, Jt); + }); + } + let vn = function(e, t) { + return !!(_t && e.hasChildNodes() && !_n(e.firstElementChild) && F(rt, e.textContent) && F(rt, e.innerHTML) || _t && e.namespaceURI === K && t === "style" && _n(e.firstElementChild) || e.nodeType === V.processingInstruction || _t && e.nodeType === V.comment && F(it, e.data)); + }, yn = function(e, t, n) { + if (!Ne[t] && wn(t) && (L.tagNameCheck instanceof RegExp && F(L.tagNameCheck, t) || L.tagNameCheck instanceof Function && L.tagNameCheck(t))) return !1; + if (Ot && !G[t]) { + let t = g(e), r = h(e); + if (r && t) { + let i = r.length; + for (let a = i - 1; a >= 0; --a) { + let i = e === n ? f(r[a], !0) : r[a]; + t.insertBefore(i, m(e)); + } + } + } + return an(e), !0; + }, bn = function(e, t, n, r) { + return e.length === 0 ? t : t === n || t === r ? z(t) : t; + }, xn = function(e, n) { + if (Y(k.beforeSanitizeElements, e, null), e !== n && g(e) === null) return kt && ln(e), !0; + if (hn(e)) return an(e), !0; + let r = J(b ? b(e) : e.nodeName); + if (M = bn(k.uponSanitizeElement, M, je, yt), Y(k.uponSanitizeElement, e, { + tagName: r, + allowedTags: M + }), e !== n && g(e) === null) return kt && ln(e), !0; + if (vn(e, r)) return an(e), !0; + if (Ne[r] || !(H.tagCheck instanceof Function && H.tagCheck(r)) && !M[r]) { + let t = yn(e, r, n); + return t === !1 && Y(k.afterSanitizeElements, e, null), t; + } + if ((y ? y(e) : e.nodeType) === V.element && !rn(e) || (r === "noscript" || r === "noembed" || r === "noframes") && F(at, e.innerHTML)) return an(e), !0; + if (U && e.nodeType === V.text) { + let n = pn(e.textContent); + e.textContent !== n && (be(t.removed, { element: e.cloneNode() }), e.textContent = n); + } + return Y(k.afterSanitizeElements, e, null), !1; + }, Sn = function(e, t, r) { + if (ft[t] || _t && t === "patchsrc" || _t && t === "for" && e !== "label" && e !== "output" || Tt && (t === "id" || t === "name") && (r in n || r in Yt)) return !1; + let i = P[t] || H.attributeCheck instanceof Function && H.attributeCheck(t, e); + if (!(mt && F(fe, t)) && !(pt && F(me, t))) { + if (!i) { + if (!(wn(e) && (L.tagNameCheck instanceof RegExp && F(L.tagNameCheck, e) || L.tagNameCheck instanceof Function && L.tagNameCheck(e)) && (L.attributeNameCheck instanceof RegExp && F(L.attributeNameCheck, t) || L.attributeNameCheck instanceof Function && L.attributeNameCheck(t, e)) || t === "is" && L.allowCustomizedBuiltInElements && (L.tagNameCheck instanceof RegExp && F(L.tagNameCheck, r) || L.tagNameCheck instanceof Function && L.tagNameCheck(r)))) return !1; + } else if (!Pt[t] && !F(Ae, Ee(r, ge, "")) && !((t === "src" || t === "xlink:href" || t === "href") && e !== "script" && De(r, "data:") === 0 && Mt[e]) && !(ht && !F(he, Ee(r, ge, ""))) && r) return !1; + } + return !0; + }, Cn = R({}, [ + "annotation-xml", + "color-profile", + "font-face", + "font-face-format", + "font-face-name", + "font-face-src", + "font-face-uri", + "missing-glyph" + ]), wn = function(e) { + return !Cn[Ce(e)] && F(ke, e); + }, Tn = function(e, t, n, r) { + if (S && typeof u == "object" && typeof u.getAttributeType == "function" && !n) switch (u.getAttributeType(e, t)) { + case "TrustedHTML": return E(r); + case "TrustedScriptURL": return ne(r); + } + return r; + }, En = function(e, n, r, i) { + try { + r ? e.setAttributeNS(r, n, i) : e.setAttribute(n, i), hn(e) ? an(e) : ye(t.removed); + } catch { + sn(n, e); + } + }, X = function(e) { + Y(k.beforeSanitizeAttributes, e, null); + let t = e.attributes; + if (!t || hn(e)) return; + P = bn(k.uponSanitizeAttribute, P, I, bt); + let n = { + attrName: "", + attrValue: "", + keepAttr: !0, + allowedAttributes: P, + forceKeepAttr: void 0 + }, r = t.length, i = J(e.nodeName); + for (; r--;) { + let a = t[r], o = a.name, s = a.namespaceURI, c = a.value, l = J(o), u = c, d = o === "value" ? u : Oe(u); + if (n.attrName = l, n.attrValue = d, n.keepAttr = !0, n.forceKeepAttr = void 0, Y(k.uponSanitizeAttribute, e, n), d = n.attrValue, Et && (l === "id" || l === "name") && De(d, Dt) !== 0 && (sn(o, e), d = Dt + d), _t && F(/((--!?|])>)|<\/(style|script|title|xmp|textarea|noscript|iframe|noembed|noframes)/i, d)) { + sn(o, e); + continue; + } + if (l === "attributename" && Te(d, "href")) { + sn(o, e); + continue; + } + if (!n.forceKeepAttr) { + if (!n.keepAttr) { + sn(o, e); + continue; + } + if (!gt && F(ot, d)) { + sn(o, e); + continue; + } + if (U && (d = pn(d)), !Sn(i, l, d)) { + sn(o, e); + continue; + } + d = Tn(i, l, s, d), d !== u && En(e, o, s, d); + } + } + Y(k.afterSanitizeAttributes, e, null); + }, Dn = function(e) { + let t = null, n = fn(e); + for (Y(k.beforeSanitizeShadowDOM, e, null); t = n.nextNode();) if (Y(k.uponSanitizeShadowNode, t, null), xn(t, e), X(t), gn(t.content) && Dn(t.content), (y ? y(t) : t.nodeType) === V.element) { + let e = _(t); + gn(e) && (On(e), Dn(e)); + } + Y(k.afterSanitizeShadowDOM, e, null); + }, On = function(e) { + let t = [{ + node: e, + shadow: null + }]; + for (; t.length > 0;) { + let e = t.pop(); + if (e.shadow) { + Dn(e.shadow); + continue; + } + let n = e.node, r = (y ? y(n) : n.nodeType) === V.element, i = h(n); + if (i) for (let e = i.length - 1; e >= 0; --e) t.push({ + node: i[e], + shadow: null + }); + if (r) { + let e = b ? b(n) : null; + if (typeof e == "string" && J(e) === "template") { + let e = n.content; + gn(e) && t.push({ + node: e, + shadow: null + }); + } + } + if (r) { + let e = _(n); + gn(e) && t.push({ + node: null, + shadow: e + }, { + node: e, + shadow: null + }); + } + } + }; + return t.sanitize = function(e) { + let n = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : {}, i = null, a = null, o = null, s = null; + if (zt = !e, zt && (e = ""), typeof e != "string" && !_n(e) && (e = Pe(e), typeof e != "string")) throw Me("dirty is not a string, aborting"); + if (!t.isSupported) return e; + vt ? (M = yt, P = bt) : Zt(n), (k.uponSanitizeElement.length > 0 || k.uponSanitizeAttribute.length > 0) && (M = z(M)), k.uponSanitizeAttribute.length > 0 && (P = z(P)), t.removed = []; + let c = kt && typeof e != "string" && _n(e); + if (c) { + un(e); + let t = b ? b(e) : e.nodeName; + if (typeof t == "string") { + let n = J(t); + if (!M[n] || Ne[n]) throw on(e), Me("root node is forbidden and cannot be sanitized in-place"); + } + if (hn(e)) throw on(e), Me("root node is clobbered and cannot be sanitized in-place"); + try { + On(e); + } catch (t) { + throw on(e), t; + } + } else if (_n(e)) i = dn(""), a = i.ownerDocument.importNode(e, !0), a.nodeType === V.element && a.nodeName === "BODY" || a.nodeName === "HTML" ? i = a : i.appendChild(a), On(a); + else { + if (!St && !U && !W && e.indexOf("<") === -1) return S && wt ? E(e) : e; + if (i = dn(e), !i) return St ? null : wt ? C : ""; + } + i && xt && an(i.firstChild); + let l = c ? e : i; + try { + let e = fn(l); + for (; o = e.nextNode();) xn(o, l), X(o), gn(o.content) && Dn(o.content); + } catch (n) { + throw c && (on(e), _e(t.removed, (e) => { + e.element && ln(e.element); + })), n; + } + if (c) return _e(t.removed, (e) => { + e.element && ln(e.element); + }), U && mn(e), e; + if (St) { + if (U && mn(i), Ct) for (s = oe.call(i.ownerDocument); i.firstChild;) s.appendChild(i.firstChild); + else s = i; + return (P.shadowroot || P.shadowrootmode) && (s = O.call(r, s, !0)), s; + } + let u = W ? i.outerHTML : i.innerHTML; + return W && M["!doctype"] && i.ownerDocument && i.ownerDocument.doctype && i.ownerDocument.doctype.name && F(tt, i.ownerDocument.doctype.name) && (u = "\n" + u), U && (u = pn(u)), S && wt ? E(u) : u; + }, t.setConfig = function() { + let e = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : {}; + Zt(e), vt = !0, yt = M, bt = P; + }, t.clearConfig = function() { + Jt = null, vt = !1, yt = null, bt = null, S = w, C = ""; + }, t.isValidAttribute = function(e, t, n) { + Jt || Zt({}); + let r = J(e), i = J(t); + return Sn(r, i, n); + }, t.addHook = function(e, t) { + typeof t == "function" && N(k, e) && be(k[e], t); + }, t.removeHook = function(e, t) { + if (N(k, e)) { + if (t !== void 0) { + let n = ve(k[e], t); + return n === -1 ? void 0 : xe(k[e], n, 1)[0]; + } + return ye(k[e]); + } + }, t.removeHooks = function(e) { + N(k, e) && (k[e] = []); + }, t.removeAllHooks = function() { + k = lt(); + }, t; +} +var ft = dt(); +//#endregion +//#region node_modules/.pnpm/marked@18.0.9/node_modules/marked/lib/marked.esm.js +function H() { + return { + async: !1, + breaks: !1, + extensions: null, + gfm: !0, + hooks: null, + pedantic: !1, + renderer: null, + silent: !1, + tokenizer: null, + walkTokens: null + }; +} +var pt = H(); +function mt(e) { + pt = e; +} +var ht = { exec: () => null }; +function gt(e) { + let t = []; + return (n) => { + let r = Math.max(0, Math.min(3, n - 1)), i = t[r]; + return i || (i = e(r), t[r] = i), i; + }; +} +function U(e, t = "") { + let n = typeof e == "string" ? e : e.source, r = { + replace: (e, t) => { + let i = typeof t == "string" ? t : t.source; + return i = i.replace(W.caret, "$1"), n = n.replace(e, i), r; + }, + getRegex: () => new RegExp(n, t) + }; + return r; +} +var _t = ((e = "") => { + try { + return !!RegExp("(?<=1)(?/, + blockquoteSetextReplace: /\n {0,3}((?:=+|-+) *)(?=\n|$)/g, + blockquoteSetextReplace2: /^ {0,3}>[ \t]?/gm, + listReplaceNesting: /^ {1,4}(?=( {4})*[^ ])/g, + listIsTask: /^\[[ xX]\] +\S/, + listReplaceTask: /^\[[ xX]\] +/, + listTaskCheckbox: /\[[ xX]\]/, + anyLine: /\n.*\n/, + hrefBrackets: /^<(.*)>$/, + tableDelimiter: /[:|]/, + tableAlignChars: /^\||\| *$/g, + tableRowBlankLine: /\n[ \t]*$/, + tableAlignRight: /^ *-+: *$/, + tableAlignCenter: /^ *:-+: *$/, + tableAlignLeft: /^ *:-+ *$/, + startATag: /^/i, + startPreScriptTag: /^<(pre|code|kbd|script)(\s|>)/i, + endPreScriptTag: /^<\/(pre|code|kbd|script)(\s|>)/i, + startAngleBracket: /^$/, + pedanticHrefTitle: /^([^'"]*[^\s])\s+(['"])(.*)\2/, + unicodeAlphaNumeric: /[\p{L}\p{N}]/u, + escapeTest: /[&<>"']/, + escapeReplace: /[&<>"']/g, + escapeTestNoEncode: /[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/, + escapeReplaceNoEncode: /[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/g, + caret: /(^|[^\[])\^/g, + percentDecode: /%25/g, + findPipe: /\|/g, + splitPipe: / \|/, + slashPipe: /\\\|/g, + carriageReturn: /\r\n|\r/g, + spaceLine: /^ +$/gm, + notSpaceStart: /^\S*/, + endingNewline: /\n$/, + listItemRegex: (e) => RegExp(`^( {0,3}${e})((?:[ ][^\\n]*)?(?:\\n|$))`), + nextBulletRegex: gt((e) => RegExp(`^ {0,${e}}(?:[*+-]|\\d{1,9}[.)])((?:[ ][^\\n]*)?(?:\\n|$))`)), + hrRegex: gt((e) => RegExp(`^ {0,${e}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`)), + fencesBeginRegex: gt((e) => RegExp(`^ {0,${e}}(?:\`\`\`|~~~)`)), + headingBeginRegex: gt((e) => RegExp(`^ {0,${e}}#`)), + htmlBeginRegex: gt((e) => RegExp(`^ {0,${e}}<(?:[a-z].*>|!--)`, "i")), + blockquoteBeginRegex: gt((e) => RegExp(`^ {0,${e}}>`)) +}, vt = /^(?:[ \t]*(?:\n|$))+/, yt = /^((?: {4}| {0,3}\t)[^\n]+(?:\n(?:[ \t]*(?:\n|$))*)?)+/, bt = /^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/, xt = /^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/, St = /^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/, Ct = / {0,3}(?:[*+-]|\d{1,9}[.)])/, wt = /^(?!bull |blockCode|fences|blockquote|heading|html|table)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html|table))+?)\n {0,3}(=+|-+) *(?:\n+|$)/, Tt = U(wt).replace(/bull/g, Ct).replace(/blockCode/g, /(?: {4}| {0,3}\t)/).replace(/fences/g, / {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g, / {0,3}>/).replace(/heading/g, / {0,3}#{1,6}(?:\s|$)/).replace(/html/g, / {0,3}<[^\n>]+>\n/).replace(/\|table/g, "").getRegex(), Et = U(wt).replace(/bull/g, Ct).replace(/blockCode/g, /(?: {4}| {0,3}\t)/).replace(/fences/g, / {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g, / {0,3}>/).replace(/heading/g, / {0,3}#{1,6}(?:\s|$)/).replace(/html/g, / {0,3}<[^\n>]+>\n/).replace(/table/g, / {0,3}\|?(?:[:\- ]*\|)+[\:\- ]*\n/).getRegex(), Dt = /^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table|[ \t]+\n)[^\n]+)*)/, Ot = /^[^\n]+/, kt = /(?!\s*\])(?:\\[\s\S]|[^\[\]\\])+/, At = U(/^ {0,3}\[(label)\]: *(?:\n[ \t]*)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n[ \t]*)?| *\n[ \t]*)(title))? *(?:\n+|$)/).replace("label", kt).replace("title", /(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/).getRegex(), G = U(/^(bull)([ \t][^\n]*?)?(?:\n|$)/).replace(/bull/g, Ct).getRegex(), jt = "address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul", Mt = /|$))/, Nt = U("^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n*|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>[^\\n]*\\n*|$)|[^\\n]*\\n*|$)|[^\\n]*\\n*|$)|)[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$)|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$))", "i").replace("comment", Mt).replace("tag", jt).replace("attribute", / +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(), Pt = (e) => U(Dt).replace("hr", xt).replace("heading", " {0,3}#{1,6}(?:\\s|$)").replace("|lheading", "").replace("|table", "").replace("blockquote", " {0,3}>").replace("fences", " {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~~~)[^\\n]*\\n").replace("list", e).replace("html", ")|<(?:script|pre|style|textarea|!--)").replace("tag", jt).getRegex(), Ft = Pt(/ {0,3}(?:[*+-]|1[.)])[ \t]+[^ \t\n]/), It = Pt(/ {0,3}(?:[*+-]|\d{1,9}[.)])(?:[ \t]|\n|$)/), Lt = { + blockquote: U(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/).replace("paragraph", It).getRegex(), + code: yt, + def: At, + fences: bt, + heading: St, + hr: xt, + html: Nt, + lheading: Tt, + list: G, + newline: vt, + paragraph: Ft, + table: ht, + text: Ot +}, K = U("^ *([^\\n ].*)\\n {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)").replace("hr", xt).replace("heading", " {0,3}#{1,6}(?:\\s|$)").replace("blockquote", " {0,3}>").replace("code", "(?: {4}| {0,3} )[^\\n]").replace("fences", " {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~~~)[^\\n]*\\n").replace("list", " {0,3}(?:[*+-]|1[.)])[ \\t]").replace("html", ")|<(?:script|pre|style|textarea|!--)").replace("tag", jt).getRegex(), Rt = { + ...Lt, + lheading: Et, + table: K, + paragraph: U(Dt).replace("hr", xt).replace("heading", " {0,3}#{1,6}(?:\\s|$)").replace("|lheading", "").replace("table", K).replace("blockquote", " {0,3}>").replace("fences", " {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~~~)[^\\n]*\\n").replace("list", " {0,3}(?:[*+-]|1[.)])[ \\t]+[^ \\t\\n]").replace("html", ")|<(?:script|pre|style|textarea|!--)").replace("tag", jt).getRegex() +}, zt = { + ...Lt, + html: U("^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))").replace("comment", Mt).replace(/tag/g, "(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(), + def: /^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/, + heading: /^(#{1,6})(.*)(?:\n+|$)/, + fences: ht, + lheading: /^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/, + paragraph: U(Dt).replace("hr", xt).replace("heading", " *#{1,6} *[^\n]").replace("lheading", Tt).replace("|table", "").replace("blockquote", " {0,3}>").replace("|fences", "").replace("|list", "").replace("|html", "").replace("|tag", "").getRegex() +}, Bt = /^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/, Vt = /^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/, Ht = /^( {2,}|\\)\n(?!\s*$)/, Ut = /^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\`+)[^`]+\k(?!`))*?\]\((?:\\[\s\S]|[^\\\(\)]|\((?:\\[\s\S]|[^\\\(\)])*\))*\)/).replace("precode-", _t ? "(?`+)[^`]+\k(?!`)/).replace("html", /<(?! )[^<>]*?>/).getRegex(), Zt = /^(?:\*+(?:((?!\*)punct)|([^\s*]))?)|^_+(?:((?!_)punct)|([^\s_]))?/, Qt = U(Zt, "u").replace(/punct/g, q).getRegex(), $t = U(Zt, "u").replace(/punct/g, J).getRegex(), en = U(/^(?:\*+(?:((?!\*)(?!openQuote)punct)|([^\s*]))?)|^_+(?:((?!_)(?!openQuote)punct)|([^\s_]))?/, "u").replace(/openQuote/g, qt).replace(/punct/g, q).getRegex(), tn = "^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\*)punct(\\*+)(?=[\\s]|$)|notPunctSpace(\\*+)(?!\\*)(?=punctSpace|$)|(?!\\*)punctSpace(\\*+)(?=notPunctSpace)|[\\s](\\*+)(?!\\*)(?=punct)|(?!\\*)punct(\\*+)(?!\\*)(?=punct)|notPunctSpace(\\*+)(?=notPunctSpace)", nn = U(tn, "gu").replace(/notPunctSpace/g, Gt).replace(/punctSpace/g, Wt).replace(/punct/g, q).getRegex(), rn = U(tn, "gu").replace(/notPunctSpace/g, Yt).replace(/punctSpace/g, Jt).replace(/punct/g, J).getRegex(), an = U("^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\*)punct(\\*+)(?=[\\s]|$)|notPunctSpace(\\*+)(?!\\*)(?=punctSpace|$)|(?!\\*)[\\s](\\*+)(?=notPunctSpace)|[\\s](\\*+)(?!\\*)(?=punct)|(?!\\*)punct(\\*+)(?!\\*)(?=punct)|(?:(?!\\*)punct|notPunctSpace)(\\*+)(?!\\*)(?=notPunctSpace)", "gu").replace(/notPunctSpace/g, Gt).replace(/punctSpace/g, Wt).replace(/punct/g, q).getRegex(), on = U("^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)|[^_]+(?=[^_])|(?!_)punct(_+)(?=[\\s]|$)|notPunctSpace(_+)(?!_)(?=punctSpace|$)|(?!_)punctSpace(_+)(?=notPunctSpace)|[\\s](_+)(?!_)(?=punct)|(?!_)punct(_+)(?!_)(?=punct)", "gu").replace(/notPunctSpace/g, Gt).replace(/punctSpace/g, Wt).replace(/punct/g, q).getRegex(), sn = U("^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)|[^_]+(?=[^_])|(?!_)punct(_+)(?=[\\s]|$)|notPunctSpace(_+)(?!_)(?=punctSpace|$)|(?!_)[\\s](_+)(?=notPunctSpace)|[\\s](_+)(?!_)(?=punct)|(?!_)punct(_+)(?!_)(?=punct)|(?:(?!_)punct|notPunctSpace)(_+)(?!_)(?=notPunctSpace)", "gu").replace(/notPunctSpace/g, Gt).replace(/punctSpace/g, Wt).replace(/punct/g, q).getRegex(), cn = U(/^~~?(?:((?!~)punct)|[^\s~])/, "u").replace(/punct/g, q).getRegex(), ln = U("^[^~]+(?=[^~])|(?!~)punct(~~?)(?=[\\s]|$)|notPunctSpace(~~?)(?!~)(?=punctSpace|$)|(?!~)punctSpace(~~?)(?=notPunctSpace)|[\\s](~~?)(?!~)(?=punct)|(?!~)punct(~~?)(?!~)(?=punct)|notPunctSpace(~~?)(?=notPunctSpace)", "gu").replace(/notPunctSpace/g, Gt).replace(/punctSpace/g, Wt).replace(/punct/g, q).getRegex(), un = U(/\\(punct)/, "gu").replace(/punct/g, q).getRegex(), dn = U(/^<(scheme:[^\s\x00-\x1f<>]*|email)>/).replace("scheme", /[a-zA-Z][a-zA-Z0-9+.-]{1,31}/).replace("email", /[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/).getRegex(), fn = U(Mt).replace("(?:-->|$)", "-->").getRegex(), pn = U("^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^").replace("comment", fn).replace("attribute", /\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/).getRegex(), mn = /(?:\[(?:\\[\s\S]|[^\[\]\\])*\]|\\[\s\S]|`+(?!`)[^`]*?`+(?!`)|``+(?=\])|[^\[\]\\`])*?/, hn = U(/^!?\[(label)\]\(\s*(href)(?:(?:[ \t]+(?:\n[ \t]*)?|\n[ \t]*)(title))?\s*\)/).replace("label", mn).replace("href", /<(?:\\.|[^\n<>\\])+>|[^ \t\n\x00-\x1f]+|(?=\))/).replace("title", /"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/).getRegex(), gn = U(/^!?\[(label)\]\[(ref)\]/).replace("label", mn).replace("ref", kt).getRegex(), _n = U(/^!?\[(ref)\](?:\[\])?/).replace("ref", kt).getRegex(), Y = U("reflink|nolink(?!\\()", "g").replace("reflink", gn).replace("nolink", _n).getRegex(), vn = /[hH][tT][tT][pP][sS]?|[fF][tT][pP]/, yn = { + _backpedal: ht, + anyPunctuation: un, + autolink: dn, + blockSkip: Xt, + br: Ht, + code: Vt, + del: ht, + delLDelim: ht, + delRDelim: ht, + emStrongLDelim: Qt, + emStrongRDelimAst: nn, + emStrongRDelimUnd: on, + escape: Bt, + link: hn, + nolink: _n, + punctuation: Kt, + reflink: gn, + reflinkSearch: Y, + tag: pn, + text: Ut, + url: ht +}, bn = { + ...yn, + emStrongLDelim: en, + emStrongRDelimAst: an, + emStrongRDelimUnd: sn, + link: U(/^!?\[(label)\]\((.*?)\)/).replace("label", mn).getRegex(), + reflink: U(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label", mn).getRegex() +}, xn = { + ...yn, + emStrongRDelimAst: rn, + emStrongLDelim: $t, + delLDelim: cn, + delRDelim: ln, + url: U(/^((?:protocol):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/).replace("protocol", vn).replace("email", /[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/).getRegex(), + _backpedal: /(?:[^?!.,:;*_'"~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_'"~)]+(?!$))+/, + del: /^(~~?)(?=[^\s~])((?:\\[\s\S]|[^\\])*?(?:\\[\s\S]|[^\s~\\]))\1(?=[^~]|$)/, + text: U(/^(`+|~+|[^`~])(?:(?=[`~])|(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\": ">", + "\"": """, + "'": "'" +}, En = (e) => Tn[e]; +function X(e, t) { + if (t) { + if (W.escapeTest.test(e)) return e.replace(W.escapeReplace, En); + } else if (W.escapeTestNoEncode.test(e)) return e.replace(W.escapeReplaceNoEncode, En); + return e; +} +function Dn(e) { + try { + e = encodeURI(e).replace(W.percentDecode, "%"); + } catch { + return null; + } + return e; +} +function On(e, t) { + let n = e.replace(W.findPipe, (e, t, n) => { + let r = !1, i = t; + for (; --i >= 0 && n[i] === "\\";) r = !r; + return r ? "|" : " |"; + }).split(W.splitPipe), r = 0; + if (n[0].trim() || n.shift(), n.length > 0 && !n.at(-1)?.trim() && n.pop(), t) if (n.length > t) n.splice(t); + else for (; n.length < t;) n.push(""); + for (; r < n.length; r++) n[r] = n[r].trim().replace(W.slashPipe, "|"); + return n; +} +function kn(e, t, n) { + let r = e.length; + if (r === 0) return ""; + let i = 0; + for (; i < r;) { + let a = e.charAt(r - i - 1); + if (a === t && !n) i++; + else if (a !== t && n) i++; + else break; + } + return e.slice(0, r - i); +} +function An(e) { + let t = e.split("\n"), n = t.length - 1; + for (; n >= 0 && W.blankLine.test(t[n]);) n--; + return t.length - n <= 2 ? e : t.slice(0, n + 1).join("\n"); +} +function jn(e, t) { + if (e.indexOf(t[1]) === -1) return -1; + let n = 0; + for (let r = 0; r < e.length; r++) if (e[r] === "\\") r++; + else if (e[r] === t[0]) n++; + else if (e[r] === t[1] && (n--, n < 0)) return r; + return n > 0 ? -2 : -1; +} +function Mn(e, t = 0) { + let n = t, r = ""; + for (let t of e) if (t === " ") { + let e = 4 - n % 4; + r += " ".repeat(e), n += e; + } else r += t, n++; + return r; +} +function Nn(e, t, n, r, i) { + let a = t.href, o = t.title || null, s = e[1].replace(i.other.outputLinkReplace, "$1"); + r.state.inLink = !0; + let c = { + type: e[0].charAt(0) === "!" ? "image" : "link", + raw: n, + href: a, + title: o, + text: s, + tokens: r.inlineTokens(s) + }; + return r.state.inLink = !1, c; +} +function Pn(e, t, n) { + let r = e.match(n.other.indentCodeCompensation); + if (r === null) return t; + let i = r[1]; + return t.split("\n").map((e) => { + let t = e.match(n.other.beginningSpace); + if (t === null) return e; + let [r] = t; + return r.length >= i.length ? e.slice(i.length) : e; + }).join("\n"); +} +var Fn = class { + options; + rules; + lexer; + constructor(e) { + this.options = e || pt; + } + space(e) { + let t = this.rules.block.newline.exec(e); + if (t && t[0].length > 0) return { + type: "space", + raw: t[0] + }; + } + code(e) { + let t = this.rules.block.code.exec(e); + if (t) { + let e = this.options.pedantic ? t[0] : An(t[0]); + return { + type: "code", + raw: e, + codeBlockStyle: "indented", + text: e.replace(this.rules.other.codeRemoveIndent, "") + }; + } + } + fences(e) { + let t = this.rules.block.fences.exec(e); + if (t) { + let e = t[0], n = Pn(e, t[3] || "", this.rules); + return { + type: "code", + raw: e, + lang: t[2] ? t[2].trim().replace(this.rules.inline.anyPunctuation, "$1") : t[2], + text: n + }; + } + } + heading(e) { + let t = this.rules.block.heading.exec(e); + if (t) { + let e = t[2].trim(); + if (this.rules.other.endingHash.test(e)) { + let t = kn(e, "#"); + (this.options.pedantic || !t || this.rules.other.endingSpaceChar.test(t)) && (e = t.trim()); + } + return { + type: "heading", + raw: kn(t[0], "\n"), + depth: t[1].length, + text: e, + tokens: this.lexer.inline(e) + }; + } + } + hr(e) { + let t = this.rules.block.hr.exec(e); + if (t) return { + type: "hr", + raw: kn(t[0], "\n") + }; + } + blockquote(e) { + let t = this.rules.block.blockquote.exec(e); + if (t) { + let e = kn(t[0], "\n").split("\n"), n = "", r = "", i = []; + for (; e.length > 0;) { + let t = !1, a = [], o; + for (o = 0; o < e.length; o++) if (this.rules.other.blockquoteStart.test(e[o])) a.push(e[o]), t = !0; + else if (!t) a.push(e[o]); + else break; + e = e.slice(o); + let s = a.join("\n"), c = s.replace(this.rules.other.blockquoteSetextReplace, "\n $1").replace(this.rules.other.blockquoteSetextReplace2, ""); + n = n ? `${n} +${s}` : s, r = r ? `${r} +${c}` : c; + let l = this.lexer.state.top; + if (this.lexer.state.top = !0, this.lexer.blockTokens(c, i, !0), this.lexer.state.top = l, e.length === 0) break; + let u = i.at(-1); + if (u?.type === "code") break; + if (u?.type === "blockquote") { + let t = u, a = e.join("\n"), o = t.raw + "\n" + a.replace(this.rules.other.blockquoteSetextReplace2, ""), s = this.blockquote(o); + i[i.length - 1] = s, n = `${n} +${a}`, r = r.substring(0, r.length - t.text.length) + s.text; + break; + } + if (u?.type === "list") { + let t = u, a = t.raw + "\n" + e.join("\n"), o = this.list(a); + i[i.length - 1] = o, n = n.substring(0, n.length - u.raw.length) + o.raw, r = r.substring(0, r.length - t.raw.length) + o.raw, e = a.substring(i.at(-1).raw.length).split("\n"); + continue; + } + } + return { + type: "blockquote", + raw: n, + tokens: i, + text: r + }; + } + } + list(e) { + let t = this.rules.block.list.exec(e); + if (t) { + let n = t[1].trim(), r = n.length > 1, i = { + type: "list", + raw: "", + ordered: r, + start: r ? +n.slice(0, -1) : "", + loose: !1, + items: [] + }; + n = r ? `\\d{1,9}\\${n.slice(-1)}` : `\\${n}`, this.options.pedantic && (n = r ? n : "[*+-]"); + let a = this.rules.other.listItemRegex(n), o = !1; + for (; e;) { + let n = !1, r = "", s = ""; + if (!(t = a.exec(e)) || this.rules.block.hr.test(e)) break; + r = t[0], e = e.substring(r.length); + let c = Mn(t[2].split("\n", 1)[0], t[1].length), l = e.split("\n", 1)[0], u = !c.trim(), d = 0; + if (this.options.pedantic ? (d = 2, s = c.trimStart()) : u ? d = t[1].length + 1 : (d = c.search(this.rules.other.nonSpaceChar), d = d > 4 ? 1 : d, s = c.slice(d), d += t[1].length), u && this.rules.other.blankLine.test(l) && (r += l + "\n", e = e.substring(l.length + 1), n = !0), !n) { + let t = this.rules.other.nextBulletRegex(d), n = this.rules.other.hrRegex(d), i = this.rules.other.fencesBeginRegex(d), a = this.rules.other.headingBeginRegex(d), o = this.rules.other.htmlBeginRegex(d), f = this.rules.other.blockquoteBeginRegex(d); + for (; e;) { + let p = e.split("\n", 1)[0], m; + if (l = p, this.options.pedantic ? (l = l.replace(this.rules.other.listReplaceNesting, " "), m = l) : m = l.replace(this.rules.other.tabCharGlobal, " "), i.test(l) || a.test(l) || o.test(l) || f.test(l) || t.test(l) || n.test(l)) break; + if (m.search(this.rules.other.nonSpaceChar) >= d || !l.trim()) s += "\n" + m.slice(d); + else { + if (u || c.replace(this.rules.other.tabCharGlobal, " ").search(this.rules.other.nonSpaceChar) >= 4 || i.test(c) || a.test(c) || n.test(c)) break; + s += "\n" + l; + } + u = !l.trim(), r += p + "\n", e = e.substring(p.length + 1), c = m.slice(d); + } + } + i.loose || (o ? i.loose = !0 : this.rules.other.doubleBlankLine.test(r) && (o = !0)), i.items.push({ + type: "list_item", + raw: r, + task: !!this.options.gfm && this.rules.other.listIsTask.test(s), + loose: !1, + text: s, + tokens: [] + }), i.raw += r; + } + let s = i.items.at(-1); + if (s) s.raw = s.raw.trimEnd(), s.text = s.text.trimEnd(); + else return; + i.raw = i.raw.trimEnd(); + for (let e of i.items) { + this.lexer.state.top = !1, e.tokens = this.lexer.blockTokens(e.text, []); + let t = e.tokens[0]; + if (e.task && (t?.type === "text" || t?.type === "paragraph")) { + e.text = e.text.replace(this.rules.other.listReplaceTask, ""), t.raw = t.raw.replace(this.rules.other.listReplaceTask, ""), t.text = t.text.replace(this.rules.other.listReplaceTask, ""); + for (let e = this.lexer.inlineQueue.length - 1; e >= 0; e--) if (this.rules.other.listIsTask.test(this.lexer.inlineQueue[e].src)) { + this.lexer.inlineQueue[e].src = this.lexer.inlineQueue[e].src.replace(this.rules.other.listReplaceTask, ""); + break; + } + let n = this.rules.other.listTaskCheckbox.exec(e.raw); + if (n) { + let t = { + type: "checkbox", + raw: n[0] + " ", + checked: n[0] !== "[ ]" + }; + e.checked = t.checked, i.loose ? e.tokens[0] && ["paragraph", "text"].includes(e.tokens[0].type) && "tokens" in e.tokens[0] && e.tokens[0].tokens ? (e.tokens[0].raw = t.raw + e.tokens[0].raw, e.tokens[0].text = t.raw + e.tokens[0].text, e.tokens[0].tokens.unshift(t)) : e.tokens.unshift({ + type: "paragraph", + raw: t.raw, + text: t.raw, + tokens: [t] + }) : e.tokens.unshift(t); + } + } else e.task &&= !1; + if (!i.loose) { + let t = e.tokens.filter((e) => e.type === "space"); + i.loose = t.length > 0 && t.some((e) => this.rules.other.anyLine.test(e.raw)); + } + } + if (i.loose) for (let e of i.items) { + e.loose = !0; + for (let t of e.tokens) t.type === "text" && (t.type = "paragraph"); + } + return i; + } + } + html(e) { + let t = this.rules.block.html.exec(e); + if (t) { + let e = An(t[0]); + return { + type: "html", + block: !0, + raw: e, + pre: t[1] === "pre" || t[1] === "script" || t[1] === "style", + text: e + }; + } + } + def(e) { + let t = this.rules.block.def.exec(e); + if (t) { + let e = t[1].toLowerCase().replace(this.rules.other.multipleSpaceGlobal, " "), n = t[2] ? t[2].replace(this.rules.other.hrefBrackets, "$1").replace(this.rules.inline.anyPunctuation, "$1") : "", r = t[3] ? t[3].substring(1, t[3].length - 1).replace(this.rules.inline.anyPunctuation, "$1") : t[3]; + return { + type: "def", + tag: e, + raw: kn(t[0], "\n"), + href: n, + title: r + }; + } + } + table(e) { + let t = this.rules.block.table.exec(e); + if (!t || !this.rules.other.tableDelimiter.test(t[2])) return; + let n = On(t[1]), r = t[2].replace(this.rules.other.tableAlignChars, "").split("|"), i = t[3]?.trim() ? t[3].replace(this.rules.other.tableRowBlankLine, "").split("\n") : [], a = { + type: "table", + raw: kn(t[0], "\n"), + header: [], + align: [], + rows: [] + }; + if (n.length === r.length) { + for (let e of r) this.rules.other.tableAlignRight.test(e) ? a.align.push("right") : this.rules.other.tableAlignCenter.test(e) ? a.align.push("center") : this.rules.other.tableAlignLeft.test(e) ? a.align.push("left") : a.align.push(null); + for (let e = 0; e < n.length; e++) a.header.push({ + text: n[e], + tokens: this.lexer.inline(n[e]), + header: !0, + align: a.align[e] + }); + for (let e of i) a.rows.push(On(e, a.header.length).map((e, t) => ({ + text: e, + tokens: this.lexer.inline(e), + header: !1, + align: a.align[t] + }))); + return a; + } + } + lheading(e) { + let t = this.rules.block.lheading.exec(e); + if (t) { + let e = t[1].trim(); + return { + type: "heading", + raw: kn(t[0], "\n"), + depth: t[2].charAt(0) === "=" ? 1 : 2, + text: e, + tokens: this.lexer.inline(e) + }; + } + } + paragraph(e) { + let t = this.rules.block.paragraph.exec(e); + if (t) { + let e = t[1].charAt(t[1].length - 1) === "\n" ? t[1].slice(0, -1) : t[1]; + return { + type: "paragraph", + raw: t[0], + text: e, + tokens: this.lexer.inline(e) + }; + } + } + text(e) { + let t = this.rules.block.text.exec(e); + if (t) return { + type: "text", + raw: t[0], + text: t[0], + tokens: this.lexer.inline(t[0]) + }; + } + escape(e) { + let t = this.rules.inline.escape.exec(e); + if (t) return { + type: "escape", + raw: t[0], + text: t[1] + }; + } + tag(e) { + let t = this.rules.inline.tag.exec(e); + if (t) return !this.lexer.state.inLink && this.rules.other.startATag.test(t[0]) ? this.lexer.state.inLink = !0 : this.lexer.state.inLink && this.rules.other.endATag.test(t[0]) && (this.lexer.state.inLink = !1), !this.lexer.state.inRawBlock && this.rules.other.startPreScriptTag.test(t[0]) ? this.lexer.state.inRawBlock = !0 : this.lexer.state.inRawBlock && this.rules.other.endPreScriptTag.test(t[0]) && (this.lexer.state.inRawBlock = !1), { + type: "html", + raw: t[0], + inLink: this.lexer.state.inLink, + inRawBlock: this.lexer.state.inRawBlock, + block: !1, + text: t[0] + }; + } + link(e) { + let t = this.rules.inline.link.exec(e); + if (t) { + let e = t[2].trim(); + if (!this.options.pedantic && this.rules.other.startAngleBracket.test(e)) { + if (!this.rules.other.endAngleBracket.test(e)) return; + let t = kn(e.slice(0, -1), "\\"); + if ((e.length - t.length) % 2 == 0) return; + } else { + let e = jn(t[2], "()"); + if (e === -2) return; + if (e > -1) { + let n = (t[0].indexOf("!") === 0 ? 5 : 4) + t[1].length + e; + t[2] = t[2].substring(0, e), t[0] = t[0].substring(0, n).trim(), t[3] = ""; + } + } + let n = t[2], r = ""; + if (this.options.pedantic) { + let e = this.rules.other.pedanticHrefTitle.exec(n); + e && (n = e[1], r = e[3]); + } else r = t[3] ? t[3].slice(1, -1) : ""; + return n = n.trim(), this.rules.other.startAngleBracket.test(n) && (n = this.options.pedantic && !this.rules.other.endAngleBracket.test(e) ? n.slice(1) : n.slice(1, -1)), Nn(t, { + href: n && n.replace(this.rules.inline.anyPunctuation, "$1"), + title: r && r.replace(this.rules.inline.anyPunctuation, "$1") + }, t[0], this.lexer, this.rules); + } + } + reflink(e, t) { + let n; + if ((n = this.rules.inline.reflink.exec(e)) || (n = this.rules.inline.nolink.exec(e))) { + let e = t[(n[2] || n[1]).replace(this.rules.other.multipleSpaceGlobal, " ").toLowerCase()]; + if (!e) { + let e = n[0].charAt(0); + return { + type: "text", + raw: e, + text: e + }; + } + return Nn(n, e, n[0], this.lexer, this.rules); + } + } + emStrong(e, t, n = "") { + let r = this.rules.inline.emStrongLDelim.exec(e); + if (!(!r || !r[1] && !r[2] && !r[3] && !r[4] || r[4] && n.match(this.rules.other.unicodeAlphaNumeric)) && (!(r[1] || r[3]) || !n || this.rules.inline.punctuation.exec(n))) { + let i = [...r[0]].length - 1, a, o, s = i, c = 0, l = r[0][0], u = n === l, d = l === "*" ? this.rules.inline.emStrongRDelimAst : this.rules.inline.emStrongRDelimUnd; + for (d.lastIndex = 0, t = t.slice(-1 * e.length + i); (r = d.exec(t)) !== null;) { + if (a = r[1] || r[2] || r[3] || r[4] || r[5] || r[6], !a) continue; + if (o = [...a].length, r[3] || r[4]) { + s += o; + continue; + } + if (r[5] || r[6]) { + if (i % 3 && !((i + o) % 3)) { + c += o; + continue; + } + if (u) break; + } + if (s -= o, s > 0) continue; + o = Math.min(o, o + s + c); + let t = [...r[0]][0].length, n = e.slice(0, i + r.index + t + o); + if (Math.min(i, o) % 2) { + let e = n.slice(1, -1); + return { + type: "em", + raw: n, + text: e, + tokens: this.lexer.inlineTokens(e) + }; + } + let l = n.slice(2, -2); + return { + type: "strong", + raw: n, + text: l, + tokens: this.lexer.inlineTokens(l) + }; + } + } + } + codespan(e) { + let t = this.rules.inline.code.exec(e); + if (t) { + let e = t[2].replace(this.rules.other.newLineCharGlobal, " "), n = this.rules.other.nonSpaceChar.test(e), r = this.rules.other.startingSpaceChar.test(e) && this.rules.other.endingSpaceChar.test(e); + return n && r && (e = e.substring(1, e.length - 1)), { + type: "codespan", + raw: t[0], + text: e + }; + } + } + br(e) { + let t = this.rules.inline.br.exec(e); + if (t) return { + type: "br", + raw: t[0] + }; + } + del(e, t, n = "") { + let r = this.rules.inline.delLDelim.exec(e); + if (r && (!r[1] || !n || this.rules.inline.punctuation.exec(n))) { + let n = [...r[0]].length - 1, i, a, o = n, s = this.rules.inline.delRDelim; + for (s.lastIndex = 0, t = t.slice(-1 * e.length + n); (r = s.exec(t)) !== null;) { + if (i = r[1] || r[2] || r[3] || r[4] || r[5] || r[6], !i || (a = [...i].length, a !== n)) continue; + if (r[3] || r[4]) { + o += a; + continue; + } + if (o -= a, o > 0) continue; + a = Math.min(a, a + o); + let t = [...r[0]][0].length, s = e.slice(0, n + r.index + t + a), c = s.slice(n, -n); + return { + type: "del", + raw: s, + text: c, + tokens: this.lexer.inlineTokens(c) + }; + } + } + } + autolink(e) { + let t = this.rules.inline.autolink.exec(e); + if (t) { + let e, n; + return t[2] === "@" ? (e = t[1], n = "mailto:" + e) : (e = t[1], n = e), { + type: "link", + raw: t[0], + text: e, + href: n, + tokens: [{ + type: "text", + raw: e, + text: e + }] + }; + } + } + url(e) { + let t; + if (t = this.rules.inline.url.exec(e)) { + let e, n; + if (t[2] === "@") e = t[0], n = "mailto:" + e; + else { + let r; + do + r = t[0], t[0] = this.rules.inline._backpedal.exec(t[0])?.[0] ?? ""; + while (r !== t[0]); + e = t[0], n = t[1] === "www." ? "http://" + t[0] : t[0]; + } + return { + type: "link", + raw: t[0], + text: e, + href: n, + tokens: [{ + type: "text", + raw: e, + text: e + }] + }; + } + } + inlineText(e) { + let t = this.rules.inline.text.exec(e); + if (t) { + let e = this.lexer.state.inRawBlock; + return { + type: "text", + raw: t[0], + text: t[0], + escaped: e + }; + } + } +}, Z = class e { + tokens; + options; + state; + inlineQueue; + tokenizer; + constructor(e) { + this.tokens = [], this.tokens.links = Object.create(null), this.options = e || pt, this.options.tokenizer = this.options.tokenizer || new Fn(), this.tokenizer = this.options.tokenizer, this.tokenizer.options = this.options, this.tokenizer.lexer = this, this.inlineQueue = [], this.state = { + inLink: !1, + inRawBlock: !1, + top: !0 + }; + let t = { + other: W, + block: Cn.normal, + inline: wn.normal + }; + this.options.pedantic ? (t.block = Cn.pedantic, t.inline = wn.pedantic) : this.options.gfm && (t.block = Cn.gfm, t.inline = this.options.breaks ? wn.breaks : wn.gfm), this.tokenizer.rules = t; + } + static get rules() { + return { + block: Cn, + inline: wn + }; + } + static lex(t, n) { + return new e(n).lex(t); + } + static lexInline(t, n) { + return new e(n).inlineTokens(t); + } + lex(e) { + e = e.replace(W.carriageReturn, "\n"), this.blockTokens(e, this.tokens); + for (let e = 0; e < this.inlineQueue.length; e++) { + let t = this.inlineQueue[e]; + this.inlineTokens(t.src, t.tokens); + } + return this.inlineQueue = [], this.tokens; + } + blockTokens(e, t = [], n = !1) { + this.tokenizer.lexer = this, this.options.pedantic && (e = e.replace(W.tabCharGlobal, " ").replace(W.spaceLine, "")); + let r = Infinity; + for (; e;) { + if (e.length < r) r = e.length; + else { + this.infiniteLoopError(e.charCodeAt(0)); + break; + } + let i; + if (this.options.extensions?.block?.some((n) => (i = n.call({ lexer: this }, e, t)) ? (e = e.substring(i.raw.length), t.push(i), !0) : !1)) continue; + if (i = this.tokenizer.space(e)) { + e = e.substring(i.raw.length); + let n = t.at(-1); + i.raw.length === 1 && n !== void 0 ? n.raw += "\n" : t.push(i); + continue; + } + if (i = this.tokenizer.code(e)) { + e = e.substring(i.raw.length); + let n = t.at(-1); + n?.type === "paragraph" || n?.type === "text" ? (n.raw += (n.raw.endsWith("\n") ? "" : "\n") + i.raw, n.text += "\n" + i.text, this.inlineQueue.at(-1).src = n.text) : t.push(i); + continue; + } + if (i = this.tokenizer.fences(e)) { + e = e.substring(i.raw.length), t.push(i); + continue; + } + if (i = this.tokenizer.heading(e)) { + e = e.substring(i.raw.length), t.push(i); + continue; + } + if (i = this.tokenizer.hr(e)) { + e = e.substring(i.raw.length), t.push(i); + continue; + } + if (i = this.tokenizer.blockquote(e)) { + e = e.substring(i.raw.length), t.push(i); + continue; + } + if (i = this.tokenizer.list(e)) { + e = e.substring(i.raw.length), t.push(i); + continue; + } + if (i = this.tokenizer.html(e)) { + e = e.substring(i.raw.length), t.push(i); + continue; + } + if (i = this.tokenizer.def(e)) { + e = e.substring(i.raw.length); + let n = t.at(-1); + n?.type === "paragraph" || n?.type === "text" ? (n.raw += (n.raw.endsWith("\n") ? "" : "\n") + i.raw, n.text += "\n" + i.raw, this.inlineQueue.at(-1).src = n.text) : this.tokens.links[i.tag] || (this.tokens.links[i.tag] = { + href: i.href, + title: i.title + }, t.push(i)); + continue; + } + if (i = this.tokenizer.table(e)) { + e = e.substring(i.raw.length), t.push(i); + continue; + } + if (i = this.tokenizer.lheading(e)) { + e = e.substring(i.raw.length), t.push(i); + continue; + } + let a = e; + if (this.options.extensions?.startBlock) { + let t = Infinity, n = e.slice(1), r; + this.options.extensions.startBlock.forEach((e) => { + r = e.call({ lexer: this }, n), typeof r == "number" && r >= 0 && (t = Math.min(t, r)); + }), t < Infinity && t >= 0 && (a = e.substring(0, t + 1)); + } + if (this.state.top && (i = this.tokenizer.paragraph(a))) { + let r = t.at(-1); + n && r?.type === "paragraph" ? (r.raw += (r.raw.endsWith("\n") ? "" : "\n") + i.raw, r.text += "\n" + i.text, this.inlineQueue.pop(), this.inlineQueue.at(-1).src = r.text) : t.push(i), n = a.length !== e.length, e = e.substring(i.raw.length); + continue; + } + if (i = this.tokenizer.text(e)) { + e = e.substring(i.raw.length); + let n = t.at(-1); + n?.type === "text" ? (n.raw += (n.raw.endsWith("\n") ? "" : "\n") + i.raw, n.text += "\n" + i.text, this.inlineQueue.pop(), this.inlineQueue.at(-1).src = n.text) : t.push(i); + continue; + } + if (e) { + this.infiniteLoopError(e.charCodeAt(0)); + break; + } + } + return this.state.top = !0, t; + } + inline(e, t = []) { + return this.inlineQueue.push({ + src: e, + tokens: t + }), t; + } + inlineTokens(e, t = []) { + this.tokenizer.lexer = this; + let n = e; + if (this.tokens.links) { + let e = Object.keys(this.tokens.links); + e.length > 0 && (n = n.replace(this.tokenizer.rules.inline.reflinkSearch, (t) => e.includes(t.slice(t.lastIndexOf("[") + 1, -1)) ? "[" + "a".repeat(t.length - 2) + "]" : t)); + } + n = n.replace(this.tokenizer.rules.inline.anyPunctuation, "++"), n = n.replace(this.tokenizer.rules.inline.blockSkip, (e, t, n) => { + let r = n ? n.length : 0; + return e.slice(0, r) + "[" + "a".repeat(e.length - r - 2) + "]"; + }), n = this.options.hooks?.emStrongMask?.call({ lexer: this }, n) ?? n; + let r = !1, i = "", a = Infinity; + for (; e;) { + if (e.length < a) a = e.length; + else { + this.infiniteLoopError(e.charCodeAt(0)); + break; + } + r || (i = ""), r = !1; + let o; + if (this.options.extensions?.inline?.some((n) => (o = n.call({ lexer: this }, e, t)) ? (e = e.substring(o.raw.length), t.push(o), !0) : !1)) continue; + if (o = this.tokenizer.escape(e)) { + e = e.substring(o.raw.length), t.push(o); + continue; + } + if (o = this.tokenizer.tag(e)) { + e = e.substring(o.raw.length), t.push(o); + continue; + } + if (o = this.tokenizer.link(e)) { + e = e.substring(o.raw.length), t.push(o); + continue; + } + if (o = this.tokenizer.reflink(e, this.tokens.links)) { + e = e.substring(o.raw.length); + let n = t.at(-1); + o.type === "text" && n?.type === "text" ? (n.raw += o.raw, n.text += o.text) : t.push(o); + continue; + } + if (o = this.tokenizer.emStrong(e, n, i)) { + e = e.substring(o.raw.length), t.push(o); + continue; + } + if (o = this.tokenizer.codespan(e)) { + e = e.substring(o.raw.length), t.push(o); + continue; + } + if (o = this.tokenizer.br(e)) { + e = e.substring(o.raw.length), t.push(o); + continue; + } + if (o = this.tokenizer.del(e, n, i)) { + e = e.substring(o.raw.length), t.push(o); + continue; + } + if (o = this.tokenizer.autolink(e)) { + e = e.substring(o.raw.length), t.push(o); + continue; + } + if (!this.state.inLink && (o = this.tokenizer.url(e))) { + e = e.substring(o.raw.length), t.push(o); + continue; + } + let s = e; + if (this.options.extensions?.startInline) { + let t = Infinity, n = e.slice(1), r; + this.options.extensions.startInline.forEach((e) => { + r = e.call({ lexer: this }, n), typeof r == "number" && r >= 0 && (t = Math.min(t, r)); + }), t < Infinity && t >= 0 && (s = e.substring(0, t + 1)); + } + if (o = this.tokenizer.inlineText(s)) { + e = e.substring(o.raw.length), o.raw.slice(-1) !== "_" && (i = o.raw.slice(-1)), r = !0; + let n = t.at(-1); + n?.type === "text" ? (n.raw += o.raw, n.text += o.text) : t.push(o); + continue; + } + if (e) { + this.infiniteLoopError(e.charCodeAt(0)); + break; + } + } + return t; + } + infiniteLoopError(e) { + let t = "Infinite loop on byte: " + e; + if (this.options.silent) console.error(t); + else throw Error(t); + } +}, In = class { + options; + parser; + constructor(e) { + this.options = e || pt; + } + space(e) { + return ""; + } + code({ text: e, lang: t, escaped: n }) { + let r = (t || "").match(W.notSpaceStart)?.[0], i = e.replace(W.endingNewline, "") + "\n"; + return r ? "
" + (n ? i : X(i, !0)) + "
\n" : "
" + (n ? i : X(i, !0)) + "
\n"; + } + blockquote({ tokens: e }) { + return `
+${this.parser.parse(e)}
+`; + } + html({ text: e }) { + return e; + } + def(e) { + return ""; + } + heading({ tokens: e, depth: t }) { + return `${this.parser.parseInline(e)} +`; + } + hr(e) { + return "
\n"; + } + list(e) { + let t = e.ordered, n = e.start, r = ""; + for (let t = 0; t < e.items.length; t++) { + let n = e.items[t]; + r += this.listitem(n); + } + let i = t ? "ol" : "ul", a = t && n !== 1 ? " start=\"" + n + "\"" : ""; + return "<" + i + a + ">\n" + r + "\n"; + } + listitem(e) { + return `
  • ${this.parser.parse(e.tokens)}
  • +`; + } + checkbox({ checked: e }) { + return " "; + } + paragraph({ tokens: e }) { + return `

    ${this.parser.parseInline(e)}

    +`; + } + table(e) { + let t = "", n = ""; + for (let t = 0; t < e.header.length; t++) n += this.tablecell(e.header[t]); + t += this.tablerow({ text: n }); + let r = ""; + for (let t = 0; t < e.rows.length; t++) { + let i = e.rows[t]; + n = ""; + for (let e = 0; e < i.length; e++) n += this.tablecell(i[e]); + r += this.tablerow({ text: n }); + } + return r &&= `${r}`, "\n\n" + t + "\n" + r + "
    \n"; + } + tablerow({ text: e }) { + return ` +${e} +`; + } + tablecell(e) { + let t = this.parser.parseInline(e.tokens), n = e.header ? "th" : "td"; + return (e.align ? `<${n} align="${e.align}">` : `<${n}>`) + t + ` +`; + } + strong({ tokens: e }) { + return `${this.parser.parseInline(e)}`; + } + em({ tokens: e }) { + return `${this.parser.parseInline(e)}`; + } + codespan({ text: e }) { + return `${X(e, !0)}`; + } + br(e) { + return "
    "; + } + del({ tokens: e }) { + return `${this.parser.parseInline(e)}`; + } + link({ href: e, title: t, tokens: n }) { + let r = this.parser.parseInline(n), i = Dn(e); + if (i === null) return r; + e = i; + let a = "
    ", a; + } + image({ href: e, title: t, text: n, tokens: r }) { + r && (n = this.parser.parseInline(r, this.parser.textRenderer)); + let i = Dn(e); + if (i === null) return X(n); + e = i; + let a = `${X(n)} { + let i = e[r].flat(Infinity); + n = n.concat(this.walkTokens(i, t)); + }) : e.tokens && (n = n.concat(this.walkTokens(e.tokens, t))); + } + } + return n; + } + use(...e) { + let t = this.defaults.extensions || { + renderers: {}, + childTokens: {} + }; + return e.forEach((e) => { + let n = { ...e }; + if (n.async = this.defaults.async || n.async || !1, e.extensions && (e.extensions.forEach((e) => { + if (!e.name) throw Error("extension name required"); + if ("renderer" in e) { + let n = t.renderers[e.name]; + n ? t.renderers[e.name] = function(...t) { + let r = e.renderer.apply(this, t); + return r === !1 && (r = n.apply(this, t)), r; + } : t.renderers[e.name] = e.renderer; + } + if ("tokenizer" in e) { + if (!e.level || e.level !== "block" && e.level !== "inline") throw Error("extension level must be 'block' or 'inline'"); + let n = t[e.level]; + n ? n.unshift(e.tokenizer) : t[e.level] = [e.tokenizer], e.start && (e.level === "block" ? t.startBlock ? t.startBlock.push(e.start) : t.startBlock = [e.start] : e.level === "inline" && (t.startInline ? t.startInline.push(e.start) : t.startInline = [e.start])); + } + "childTokens" in e && e.childTokens && (t.childTokens[e.name] = e.childTokens); + }), n.extensions = t), e.renderer) { + let t = this.defaults.renderer || new In(this.defaults); + for (let n in e.renderer) { + if (!(n in t)) throw Error(`renderer '${n}' does not exist`); + if (["options", "parser"].includes(n)) continue; + let r = n, i = e.renderer[r], a = t[r]; + t[r] = (...e) => { + let n = i.apply(t, e); + return n === !1 && (n = a.apply(t, e)), n || ""; + }; + } + n.renderer = t; + } + if (e.tokenizer) { + let t = this.defaults.tokenizer || new Fn(this.defaults); + for (let n in e.tokenizer) { + if (!(n in t)) throw Error(`tokenizer '${n}' does not exist`); + if ([ + "options", + "rules", + "lexer" + ].includes(n)) continue; + let r = n, i = e.tokenizer[r], a = t[r]; + t[r] = (...e) => { + let n = i.apply(t, e); + return n === !1 && (n = a.apply(t, e)), n; + }; + } + n.tokenizer = t; + } + if (e.hooks) { + let t = this.defaults.hooks || new Rn(); + for (let n in e.hooks) { + if (!(n in t)) throw Error(`hook '${n}' does not exist`); + if (["options", "block"].includes(n)) continue; + let r = n, i = e.hooks[r], a = t[r]; + t[r] = Rn.passThroughHooks.has(n) ? (e) => { + if (this.defaults.async && Rn.passThroughHooksRespectAsync.has(n)) return (async () => { + let n = await i.call(t, e); + return a.call(t, n); + })(); + let r = i.call(t, e); + return a.call(t, r); + } : (...e) => { + if (this.defaults.async) return (async () => { + let n = await i.apply(t, e); + return n === !1 && (n = await a.apply(t, e)), n; + })(); + let n = i.apply(t, e); + return n === !1 && (n = a.apply(t, e)), n; + }; + } + n.hooks = t; + } + if (e.walkTokens) { + let t = this.defaults.walkTokens, r = e.walkTokens; + n.walkTokens = function(e) { + let n = []; + return n.push(r.call(this, e)), t && (n = n.concat(t.call(this, e))), n; + }; + } + this.defaults = { + ...this.defaults, + ...n + }; + }), this; + } + setOptions(e) { + return this.defaults = { + ...this.defaults, + ...e + }, this; + } + lexer(e, t) { + return Z.lex(e, t ?? this.defaults); + } + parser(e, t) { + return Q.parse(e, t ?? this.defaults); + } + parseMarkdown(e) { + return (t, n) => { + let r = { ...n }, i = { + ...this.defaults, + ...r + }, a = this.onError(!!i.silent, !!i.async); + if (this.defaults.async === !0 && r.async === !1) return a(/* @__PURE__ */ Error("marked(): The async option was set to true by an extension. Remove async: false from the parse options object to return a Promise.")); + if (typeof t > "u" || t === null) return a(/* @__PURE__ */ Error("marked(): input parameter is undefined or null")); + if (typeof t != "string") return a(/* @__PURE__ */ Error("marked(): input parameter is of type " + Object.prototype.toString.call(t) + ", string expected")); + if (i.hooks && (i.hooks.options = i, i.hooks.block = e), i.async) return (async () => { + let n = i.hooks ? await i.hooks.preprocess(t) : t, r = await (i.hooks ? await i.hooks.provideLexer(e) : e ? Z.lex : Z.lexInline)(n, i), a = i.hooks ? await i.hooks.processAllTokens(r) : r; + i.walkTokens && await Promise.all(this.walkTokens(a, i.walkTokens)); + let o = await (i.hooks ? await i.hooks.provideParser(e) : e ? Q.parse : Q.parseInline)(a, i); + return i.hooks ? await i.hooks.postprocess(o) : o; + })().catch(a); + try { + i.hooks && (t = i.hooks.preprocess(t)); + let n = (i.hooks ? i.hooks.provideLexer(e) : e ? Z.lex : Z.lexInline)(t, i); + i.hooks && (n = i.hooks.processAllTokens(n)), i.walkTokens && this.walkTokens(n, i.walkTokens); + let r = (i.hooks ? i.hooks.provideParser(e) : e ? Q.parse : Q.parseInline)(n, i); + return i.hooks && (r = i.hooks.postprocess(r)), r; + } catch (e) { + return a(e); + } + }; + } + onError(e, t) { + return (n) => { + if (n.message += "\nPlease report this to https://github.com/markedjs/marked.", e) { + let e = "

    An error occurred:

    " + X(n.message + "", !0) + "
    "; + return t ? Promise.resolve(e) : e; + } + if (t) return Promise.reject(n); + throw n; + }; + } +}(); +function $(e, t) { + return zn.parse(e, t); +} +$.options = $.setOptions = function(e) { + return zn.setOptions(e), $.defaults = zn.defaults, mt($.defaults), $; +}, $.getDefaults = H, $.defaults = pt; +function Bn(...e) { + return zn.use(...e), $.defaults = zn.defaults, mt($.defaults), $; +} +$.use = Bn, $.walkTokens = function(e, t) { + return zn.walkTokens(e, t); +}, $.parseInline = zn.parseInline, $.Parser = Q, $.parser = Q.parse, $.Renderer = In, $.TextRenderer = Ln, $.Lexer = Z, $.lexer = Z.lex, $.Tokenizer = Fn, $.Hooks = Rn, $.parse = $, $.options, $.setOptions, $.walkTokens, $.parseInline, Q.parse, Z.lex; +//#endregion +//#region resources/js/composables/useAiChat.ts +function Vn(e, t) { + let r = _(null), i = _([]), a = _([]), o = _(!1), s = _(!1), c = _(!1), l = _(null), u = n(() => r.value !== null); + function d() { + r.value = null, i.value = [], a.value = [], o.value = !1, s.value = !1, c.value = !1, l.value = null; + } + function f() { + r.value = null, i.value = [], l.value = null; + } + async function p() { + o.value = !0; + try { + let { data: t } = await e.get("/ai/conversations"); + a.value = t.conversations; + } catch {} finally { + o.value = !1; + } + } + async function m(n) { + s.value = !0, l.value = null; + try { + let { data: t } = await e.get(`/ai/conversations/${n}`); + r.value = t.conversation.id, i.value = t.messages; + } catch (e) { + let n = Hn(e, "Unable to load this conversation."); + l.value = n, t("error", n); + } finally { + s.value = !1; + } + } + async function h(t) { + let n = t.trim(); + if (!n || c.value) return; + l.value = null, c.value = !0; + let a = { + id: -Date.now(), + role: "user", + content: n, + created_at: (/* @__PURE__ */ new Date()).toISOString() + }; + i.value.push(a); + try { + let { data: t } = await e.post("/ai/chat", { + conversation_id: r.value, + message: n + }); + r.value = t.conversation.id, i.value = i.value.map((e) => e.id === a.id ? { + ...a, + id: a.id + } : e), i.value.push(t.message), p(); + } catch (e) { + i.value = i.value.filter((e) => e.id !== a.id), l.value = Hn(e, "The assistant could not respond. Please try again."); + } finally { + c.value = !1; + } + } + async function g(n, r) { + let i = r.trim(); + if (i) try { + await e.patch(`/ai/conversations/${n}`, { title: i }); + let t = a.value.find((e) => e.id === n); + t && (t.title = i); + } catch (e) { + t("error", Hn(e, "Unable to rename this conversation.")); + } + } + async function v(n) { + try { + await e.delete(`/ai/conversations/${n}`), a.value = a.value.filter((e) => e.id !== n), r.value === n && f(); + } catch (e) { + t("error", Hn(e, "Unable to delete this conversation.")); + } + } + return { + currentConversationId: r, + messages: i, + conversations: a, + isLoadingConversations: o, + isLoadingConversation: s, + isSending: c, + lastError: l, + hasActiveConversation: u, + reset: d, + newConversation: f, + refreshConversations: p, + loadConversation: m, + sendMessage: h, + renameConversation: g, + deleteConversation: v + }; +} +function Hn(e, t) { + if (typeof e == "object" && e && "response" in e) { + let t = e.response; + if (typeof t?.data?.message == "string") return t.data.message; + } + return e instanceof Error && e.message ? e.message : t; +} +//#endregion +//#region resources/js/components/AiChatOverlay.vue?vue&type=script&setup=true&lang.ts +var Un = { + key: 1, + class: "fixed inset-y-0 right-0 z-50 flex w-full bg-surface shadow-2xl sm:w-[480px] lg:w-[640px]", + "aria-label": "AI Assistant" +}, Wn = { class: "hidden w-52 shrink-0 border-r border-line-default bg-surface-secondary sm:flex sm:flex-col" }, Gn = { class: "flex-1 overflow-y-auto p-2" }, Kn = { + key: 0, + class: "p-2 text-xs text-muted" +}, qn = { + key: 1, + class: "p-2 text-xs text-muted" +}, Jn = { + key: 2, + class: "space-y-1" +}, Yn = ["onClick"], Xn = { class: "hidden shrink-0 group-hover:flex" }, Zn = ["onClick"], Qn = ["onClick"], $n = { class: "flex min-w-0 flex-1 flex-col" }, er = { class: "flex h-14 items-center justify-between border-b border-line-default px-3" }, tr = { class: "flex items-center gap-2" }, nr = { class: "flex items-center gap-1" }, rr = { + key: 0, + class: "border-b border-line-default bg-surface-secondary p-2 sm:hidden" +}, ir = { class: "max-h-48 overflow-y-auto" }, ar = ["onClick"], or = { + key: 0, + class: "p-2 text-xs text-muted" +}, sr = { + key: 0, + class: "py-12 text-center text-sm text-muted" +}, cr = { + key: 1, + class: "mx-auto mt-16 max-w-xs text-center text-sm text-muted" +}, lr = { + key: 0, + class: "max-w-[85%] whitespace-pre-wrap break-words rounded-lg bg-primary-500 px-4 py-2 text-sm text-white" +}, ur = ["innerHTML"], dr = { + key: 2, + class: "inline-flex rounded-lg bg-surface-tertiary px-4 py-2 text-sm italic text-muted" +}, fr = { + key: 3, + class: "rounded bg-alert-error-bg p-3 text-xs text-alert-error-text" +}, pr = ["disabled"], mr = ["disabled"], hr = /* @__PURE__ */ l({ + __name: "AiChatOverlay", + props: /*@__PURE__*/ d({ + client: { type: [Function, Object] }, + enabled: { type: Boolean }, + notify: { type: Function } + }, { + open: { + type: Boolean, + default: !1 + }, + openModifiers: {} + }), + emits: ["update:open"], + setup(n) { + let s = n, l = S(n, "open"), u = Vn(s.client, s.notify), d = _(""), m = _(null), g = _(!1), C = _(null), w = _(""); + te(l, (e) => { + e && s.enabled && u.refreshConversations(); + }), te(() => s.enabled, (e) => { + e || (l.value = !1, u.reset()); + }), te(() => u.messages.value.length, async () => { + await f(), m.value && (m.value.scrollTop = m.value.scrollHeight); + }); + function ee() { + l.value = !1, g.value = !1; + } + async function E(e) { + await u.loadConversation(e.id), g.value = !1; + } + async function D() { + let e = d.value; + d.value = "", await u.sendMessage(e); + } + function ae(e, t) { + t.stopPropagation(), C.value = e.id, w.value = e.title ?? ""; + } + async function oe() { + C.value !== null && await u.renameConversation(C.value, w.value), C.value = null, w.value = ""; + } + async function se(e, t) { + t.stopPropagation(), window.confirm(`Delete “${e.title ?? "Untitled conversation"}”?`) && await u.deleteConversation(e.id); + } + function O() { + u.newConversation(), g.value = !1; + } + function k(e) { + e.key === "Enter" && !e.shiftKey && (e.preventDefault(), D()); + } + function ce(e) { + return ft.sanitize($.parse(e ?? "", { async: !1 })); + } + return (s, f) => { + let _ = y("BaseIcon"); + return h(), r(t, { to: "body" }, [l.value ? (h(), a("button", { + key: 0, + type: "button", + class: "fixed inset-0 z-40 cursor-default bg-black/20", + "aria-label": "Close AI Assistant", + onClick: ee + })) : i("", !0), l.value ? (h(), a("aside", Un, [o("section", Wn, [o("div", { class: "border-b border-line-default p-3" }, [o("button", { + type: "button", + class: "w-full rounded-md bg-btn-primary px-3 py-2 text-xs font-medium text-white hover:bg-btn-primary-hover", + onClick: O + }, " + New conversation ")]), o("div", Gn, [x(u).isLoadingConversations.value && !x(u).conversations.value.length ? (h(), a("p", Kn, "Loading history…")) : x(u).conversations.value.length ? (h(), a("ul", Jn, [(h(!0), a(e, null, v(x(u).conversations.value, (e) => (h(), a("li", { key: e.id }, [C.value === e.id ? (h(), a("form", { + key: 0, + class: "flex gap-1 p-1", + onSubmit: ie(oe, ["prevent"]) + }, [ne(o("input", { + "onUpdate:modelValue": f[0] ||= (e) => w.value = e, + class: "min-w-0 flex-1 rounded border border-line-default bg-surface px-2 py-1 text-xs text-body", + "aria-label": "Conversation title", + autofocus: "", + onKeydown: f[1] ||= re((e) => C.value = null, ["esc"]) + }, null, 544), [[T, w.value]]), f[4] ||= o("button", { + type: "submit", + class: "rounded px-1 text-xs text-primary-500 hover:bg-hover", + "aria-label": "Save conversation title" + }, "Save", -1)], 32)) : (h(), a("div", { + key: 1, + class: p(["group flex items-center gap-1 rounded text-sm hover:bg-hover", x(u).currentConversationId.value === e.id ? "bg-hover-strong font-semibold text-heading" : "text-body"]) + }, [o("button", { + type: "button", + class: "min-w-0 flex-1 truncate px-2 py-2 text-left", + onClick: (t) => E(e) + }, b(e.title || "Untitled conversation"), 9, Yn), o("span", Xn, [o("button", { + type: "button", + class: "px-1 text-muted hover:text-heading", + "aria-label": "Rename conversation", + onClick: (t) => ae(e, t) + }, "✎", 8, Zn), o("button", { + type: "button", + class: "px-1 text-muted hover:text-alert-error-text", + "aria-label": "Delete conversation", + onClick: (t) => se(e, t) + }, "×", 8, Qn)])], 2))]))), 128))])) : (h(), a("p", qn, "No conversations yet."))])]), o("section", $n, [ + o("header", er, [o("div", tr, [ + o("button", { + type: "button", + class: "rounded p-1 text-muted hover:bg-hover hover:text-heading sm:hidden", + "aria-label": "Conversation history", + onClick: f[2] ||= (e) => g.value = !g.value + }, [c(_, { + name: "Bars3Icon", + class: "h-5 w-5" + })]), + c(_, { + name: "SparklesIcon", + class: "h-5 w-5 text-primary-500" + }), + f[5] ||= o("h2", { class: "text-sm font-semibold text-heading" }, "AI Assistant", -1) + ]), o("div", nr, [o("button", { + type: "button", + class: "rounded p-2 text-muted hover:bg-hover hover:text-heading", + "aria-label": "New conversation", + title: "New conversation", + onClick: O + }, [c(_, { + name: "PlusIcon", + class: "h-4 w-4" + })]), o("button", { + type: "button", + class: "rounded p-2 text-muted hover:bg-hover hover:text-heading", + "aria-label": "Close AI Assistant", + onClick: ee + }, [c(_, { + name: "XMarkIcon", + class: "h-5 w-5" + })])])]), + g.value ? (h(), a("div", rr, [o("button", { + type: "button", + class: "mb-2 w-full rounded-md bg-btn-primary px-3 py-2 text-xs font-medium text-white", + onClick: O + }, "+ New conversation"), o("div", ir, [(h(!0), a(e, null, v(x(u).conversations.value, (e) => (h(), a("button", { + key: e.id, + type: "button", + class: "block w-full truncate rounded px-2 py-2 text-left text-sm text-body hover:bg-hover", + onClick: (t) => E(e) + }, b(e.title || "Untitled conversation"), 9, ar))), 128)), x(u).conversations.value.length ? i("", !0) : (h(), a("p", or, "No conversations yet."))])])) : i("", !0), + o("main", { + ref_key: "messagesEl", + ref: m, + class: "flex-1 space-y-3 overflow-y-auto p-4", + "aria-live": "polite" + }, [ + x(u).isLoadingConversation.value ? (h(), a("div", sr, "Loading conversation…")) : x(u).messages.value.length ? i("", !0) : (h(), a("div", cr, [c(_, { + name: "SparklesIcon", + class: "mx-auto mb-3 h-10 w-10 text-subtle" + }), f[6] ||= o("p", null, "Ask for help with invoices, customers, or your business.", -1)])), + (h(!0), a(e, null, v(x(u).messages.value, (e) => (h(), a("article", { + key: e.id, + class: p(["flex", e.role === "user" ? "justify-end" : "justify-start"]) + }, [e.role === "user" ? (h(), a("p", lr, b(e.content), 1)) : (h(), a("div", { + key: 1, + class: "max-w-[85%] break-words rounded-lg bg-surface-tertiary px-4 py-2 text-sm text-body [&_a]:text-primary-500 [&_a]:underline [&_ol]:mt-3 [&_ol]:list-decimal [&_ol]:pl-5 [&_p+p]:mt-3 [&_pre]:mt-3 [&_pre]:overflow-x-auto [&_pre]:rounded [&_pre]:bg-surface [&_pre]:p-3 [&_ul]:mt-3 [&_ul]:list-disc [&_ul]:pl-5", + innerHTML: ce(e.content) + }, null, 8, ur))], 2))), 128)), + x(u).isSending.value ? (h(), a("div", dr, "Thinking…")) : i("", !0), + x(u).lastError.value ? (h(), a("p", fr, b(x(u).lastError.value), 1)) : i("", !0) + ], 512), + o("form", { + class: "flex items-end gap-2 border-t border-line-default p-3", + onSubmit: ie(D, ["prevent"]) + }, [ne(o("textarea", { + "onUpdate:modelValue": f[3] ||= (e) => d.value = e, + rows: "2", + class: "min-h-11 flex-1 resize-none rounded-md border border-line-default bg-surface px-3 py-2 text-sm text-body outline-none focus:ring-1 focus:ring-primary-500", + placeholder: "Ask about your business…", + disabled: !n.enabled || x(u).isSending.value, + onKeydown: k + }, null, 40, pr), [[T, d.value]]), o("button", { + type: "submit", + class: "rounded-md bg-btn-primary px-3 py-2 text-sm font-medium text-white hover:bg-btn-primary-hover disabled:cursor-not-allowed disabled:opacity-50", + disabled: !n.enabled || x(u).isSending.value || !d.value.trim() + }, b(x(u).isSending.value ? "Sending…" : "Send"), 9, mr)], 32) + ])])) : i("", !0)]); + }; + } +}), gr = /* @__PURE__ */ l({ + __name: "AiHeaderAction", + emits: ["open"], + setup(e) { + return (e, t) => { + let n = y("BaseIcon"); + return h(), a("li", null, [o("button", { + type: "button", + class: "inline-flex items-center gap-1 rounded-md px-2 py-1 text-sm text-body hover:bg-hover", + title: "AI Assistant", + "aria-label": "Open AI Assistant", + onClick: t[0] ||= (t) => e.$emit("open") + }, [c(n, { + name: "SparklesIcon", + class: "h-5 w-5 text-primary-500" + }), t[1] ||= o("span", { class: "hidden lg:inline" }, "AI", -1)])]); + }; + } +}), _r = ["disabled"], vr = { + class: "w-full max-w-xl overflow-hidden rounded-lg bg-surface shadow-2xl", + role: "dialog", + "aria-modal": "true", + "aria-labelledby": "ai-generate-title" +}, yr = { class: "flex items-center justify-between border-b border-line-default px-5 py-4" }, br = { class: "flex items-center gap-2" }, xr = ["disabled"], Sr = ["disabled"], Cr = { + key: 0, + class: "flex cursor-pointer items-start gap-2 text-sm text-body" +}, wr = { + key: 1, + class: "rounded bg-alert-error-bg p-3 text-sm text-alert-error-text" +}, Tr = { + key: 2, + class: "rounded-md border border-line-default bg-surface-secondary p-3" +}, Er = { class: "max-h-48 overflow-y-auto whitespace-pre-wrap break-words text-sm text-body" }, Dr = { class: "flex flex-wrap justify-end gap-2 border-t border-line-default p-4" }, Or = ["disabled"], kr = ["disabled"], Ar = ["disabled"], jr = ["disabled"], Mr = ["disabled"], Nr = /* @__PURE__ */ l({ + __name: "AiTextAction", + props: { + client: { type: [Function, Object] }, + context: {}, + enabled: { type: Boolean }, + notify: { type: Function } + }, + setup(l) { + let u = l, d = _(!1), f = _(""), p = _(!1), m = _(""), g = _(!1), v = _(""), x = n(() => m.value.trim().length > 0); + te(d, (e) => { + e || (f.value = "", p.value = !1, m.value = "", v.value = ""); + }); + async function S() { + if (!(!f.value.trim() || g.value)) { + g.value = !0, v.value = "", m.value = ""; + try { + let { data: e } = await u.client.post("/ai/generate", { + prompt: f.value.trim(), + context: p.value ? u.context.getHtml() : void 0 + }); + e.text ? m.value = e.text : v.value = e.message ?? e.error ?? "The assistant could not generate text."; + } catch (e) { + v.value = Hn(e, "The assistant could not generate text."); + } finally { + g.value = !1; + } + } + } + function w() { + u.context.insertContent(m.value), u.notify("success", "AI text inserted."), d.value = !1; + } + function ee() { + u.context.replaceContent(m.value), u.notify("success", "Editor content replaced with AI text."), d.value = !1; + } + return (n, u) => { + let _ = y("BaseIcon"); + return h(), a(e, null, [o("button", { + type: "button", + class: "inline-flex items-center gap-1 rounded px-2 py-1 text-xs text-body hover:bg-hover disabled:cursor-not-allowed disabled:opacity-50", + disabled: !l.enabled, + title: "Generate with AI", + onClick: u[0] ||= (e) => d.value = !0 + }, [c(_, { + name: "SparklesIcon", + class: "h-4 w-4 text-primary-500" + }), u[6] ||= s(" AI write ", -1)], 8, _r), (h(), r(t, { to: "body" }, [d.value ? (h(), a("div", { + key: 0, + class: "fixed inset-0 z-50 grid place-items-center bg-black/30 p-4", + role: "presentation", + onMousedown: u[5] ||= ie((e) => d.value = !1, ["self"]) + }, [o("section", vr, [ + o("header", yr, [o("div", br, [c(_, { + name: "SparklesIcon", + class: "h-5 w-5 text-primary-500" + }), u[7] ||= o("h2", { + id: "ai-generate-title", + class: "font-semibold text-heading" + }, "Generate text", -1)]), o("button", { + type: "button", + class: "rounded p-1 text-muted hover:bg-hover hover:text-heading", + "aria-label": "Close", + disabled: g.value, + onClick: u[1] ||= (e) => d.value = !1 + }, [c(_, { + name: "XMarkIcon", + class: "h-5 w-5" + })], 8, xr)]), + o("form", { + class: "space-y-4 p-5", + onSubmit: ie(S, ["prevent"]) + }, [ + u[11] ||= o("label", { + class: "block text-sm font-medium text-heading", + for: "ai-prompt" + }, "What would you like to write?", -1), + ne(o("textarea", { + id: "ai-prompt", + "onUpdate:modelValue": u[2] ||= (e) => f.value = e, + rows: "3", + class: "w-full rounded-md border border-line-default bg-surface px-3 py-2 text-sm text-body outline-none focus:ring-1 focus:ring-primary-500", + placeholder: "For example: Write a polite payment reminder", + disabled: g.value, + autofocus: "" + }, null, 8, Sr), [[T, f.value]]), + l.context.getHtml() ? (h(), a("label", Cr, [ + ne(o("input", { + "onUpdate:modelValue": u[3] ||= (e) => p.value = e, + type: "checkbox", + class: "mt-1" + }, null, 512), [[C, p.value]]), + u[8] ||= s(), + u[9] ||= o("span", null, [ + s("Use the current editor content as context"), + o("br"), + o("span", { class: "text-xs text-muted" }, "The content helps the assistant match the document’s tone and details.") + ], -1) + ])) : i("", !0), + v.value ? (h(), a("p", wr, b(v.value), 1)) : i("", !0), + m.value ? (h(), a("section", Tr, [u[10] ||= o("p", { class: "mb-2 text-xs font-medium text-muted" }, "Preview", -1), o("p", Er, b(m.value), 1)])) : i("", !0) + ], 32), + o("footer", Dr, [ + o("button", { + type: "button", + class: "rounded px-3 py-2 text-sm text-body hover:bg-hover", + disabled: g.value, + onClick: u[4] ||= (e) => d.value = !1 + }, "Cancel", 8, Or), + x.value ? (h(), a("button", { + key: 0, + type: "button", + class: "rounded border border-line-default px-3 py-2 text-sm text-body hover:bg-hover", + disabled: g.value, + onClick: ee + }, "Replace", 8, kr)) : i("", !0), + x.value ? (h(), a("button", { + key: 1, + type: "button", + class: "rounded border border-line-default px-3 py-2 text-sm text-body hover:bg-hover", + disabled: g.value, + onClick: S + }, "Regenerate", 8, Ar)) : i("", !0), + x.value ? (h(), a("button", { + key: 2, + type: "button", + class: "rounded bg-btn-primary px-3 py-2 text-sm font-medium text-white hover:bg-btn-primary-hover", + disabled: g.value, + onClick: w + }, "Insert", 8, jr)) : (h(), a("button", { + key: 3, + type: "button", + class: "rounded bg-btn-primary px-3 py-2 text-sm font-medium text-white hover:bg-btn-primary-hover disabled:cursor-not-allowed disabled:opacity-50", + disabled: g.value || !f.value.trim(), + onClick: S + }, b(g.value ? "Generating…" : "Generate"), 9, Mr)) + ]) + ])], 32)) : i("", !0)]))], 64); + }; + } +}), Pr = { + key: 0, + class: "p-6 text-sm text-muted" +}, Fr = { + key: 1, + class: "space-y-3 p-6" +}, Ir = { class: "rounded bg-alert-error-bg p-3 text-sm text-alert-error-text" }, Lr = { + key: 0, + class: "border-b border-line-default pb-6" +}, Rr = { class: "flex cursor-pointer items-start gap-3" }, zr = ["checked", "disabled"], Br = { + key: 0, + class: "mt-4 rounded bg-alert-success-bg p-3 text-sm text-alert-success-text" +}, Vr = { + key: 1, + class: "space-y-6" +}, Hr = { class: "flex cursor-pointer items-start gap-3" }, Ur = ["checked"], Wr = { + key: 0, + class: "space-y-5" +}, Gr = { class: "block text-sm font-medium text-heading" }, Kr = ["value"], qr = { + key: 0, + class: "-mt-3 text-xs text-muted" +}, Jr = ["href"], Yr = { class: "block text-sm font-medium text-heading" }, Xr = { class: "mt-1 flex gap-2" }, Zr = ["type", "placeholder"], Qr = [ + "value", + "placeholder", + "onInput" +], $r = ["value", "onChange"], ei = ["value"], ti = { class: "border-t border-line-default pt-5" }, ni = { class: "mt-5 space-y-5" }, ri = { class: "flex cursor-pointer gap-3" }, ii = ["checked"], ai = { + key: 0, + class: "mt-3 block text-sm text-body" +}, oi = { class: "flex cursor-pointer gap-3" }, si = ["checked"], ci = { + key: 0, + class: "mt-3 block text-sm text-body" +}, li = { id: "ai-models" }, ui = ["value"], di = { + key: 2, + class: "flex flex-wrap items-center gap-3 border-t border-line-default pt-5" +}, fi = ["disabled"], pi = ["disabled"], mi = /* @__PURE__ */ l({ + __name: "AiConfigurationPage", + props: { + client: { type: [Function, Object] }, + scope: {}, + notify: { type: Function } + }, + setup(t) { + let l = t, u = { + ai_enabled: "NO", + ai_driver: "openrouter", + ai_api_key: "", + ai_base_url: "", + ai_chat_enabled: "NO", + ai_chat_model: "anthropic/claude-sonnet-4.6", + ai_text_generation_enabled: "NO", + ai_text_generation_model: "anthropic/claude-haiku-4.5" + }, d = g({ + ...u, + use_custom_ai_config: "NO" + }), f = _([]), p = _(!0), x = _(!1), S = _(!1), C = _(!1), re = _(""), D = n(() => l.scope === "admin" ? "/ai/config" : "/company/ai/config"), ae = n(() => l.scope === "admin" ? "/ai/test" : "/company/ai/test"), oe = n(() => d.use_custom_ai_config === "YES"), se = n(() => d.ai_enabled === "YES"), O = n(() => f.value.find((e) => e.value === d.ai_driver)), k = n(() => O.value?.suggested_models ?? []), ce = n(() => O.value?.config_fields ?? []), le = n(() => /[•*]/.test(d.ai_api_key)), ue = n(() => l.scope === "admin" || oe.value); + te(() => d.ai_driver, (e) => { + let t = f.value.find((t) => t.value === e); + t?.default_base_url && !d.ai_base_url && (d.ai_base_url = t.default_base_url); + }), m(() => { + de(); + }); + async function de() { + p.value = !0, re.value = ""; + try { + let [{ data: e }, { data: t }] = await Promise.all([l.client.get("/ai/drivers"), l.client.get(D.value)]); + f.value = e.ai_drivers, Object.assign(d, u, t); + } catch (e) { + re.value = Hn(e, "Unable to load AI configuration."), l.notify("error", re.value); + } finally { + p.value = !1; + } + } + function fe() { + return { ...d }; + } + function A() { + if (!se.value) return null; + if (!d.ai_driver) return "Choose an AI provider."; + if (!d.ai_api_key && !le.value) return "Enter an API key."; + if (d.ai_base_url) try { + new URL(d.ai_base_url); + } catch { + return "Enter a valid base URL."; + } + return d.ai_chat_enabled === "YES" && !d.ai_chat_model.trim() ? "Choose a chat model." : d.ai_text_generation_enabled === "YES" && !d.ai_text_generation_model.trim() ? "Choose a text generation model." : null; + } + async function j() { + let e = A(); + if (e) { + l.notify("error", e); + return; + } + x.value = !0; + try { + await l.client.post(D.value, fe()), l.notify("success", "AI settings saved."); + } catch (e) { + l.notify("error", Hn(e, "Unable to save AI settings.")); + } finally { + x.value = !1; + } + } + async function pe(e) { + let t = e.target.checked; + if (d.use_custom_ai_config = t ? "YES" : "NO", !t) { + x.value = !0; + try { + await l.client.post(D.value, { use_custom_ai_config: "NO" }), l.notify("success", "This company now uses the global AI configuration."); + } catch (e) { + d.use_custom_ai_config = "YES", l.notify("error", Hn(e, "Unable to update the company configuration.")); + } finally { + x.value = !1; + } + } + } + async function me() { + if (se.value) { + S.value = !0; + try { + let { data: e } = await l.client.post(ae.value, { + ai_driver: d.ai_driver, + ai_api_key: le.value ? void 0 : d.ai_api_key, + ai_base_url: d.ai_base_url + }); + e.success ? l.notify("success", e.message ?? "Connection successful.") : l.notify("error", e.message ?? e.error ?? "The connection test failed."); + } catch (e) { + l.notify("error", Hn(e, "The connection test failed.")); + } finally { + S.value = !1; + } + } + } + function he(e, t) { + d[e] = t.target.checked ? "YES" : "NO"; + } + function ge(e, t) { + d[`ai_${e}`] = t.target.value; + } + return (n, l) => { + let u = y("BasePageHeader"), m = y("BaseCard"), g = y("BasePage"); + return h(), r(g, null, { + default: E(() => [c(u, { title: t.scope === "admin" ? "AI Assistant" : "AI Assistant configuration" }, null, 8, ["title"]), c(m, { class: "max-w-3xl" }, { + default: E(() => [p.value ? (h(), a("div", Pr, "Loading AI configuration…")) : re.value ? (h(), a("div", Fr, [o("p", Ir, b(re.value), 1), o("button", { + type: "button", + class: "rounded bg-btn-primary px-3 py-2 text-sm text-white", + onClick: de + }, "Try again")])) : (h(), a("form", { + key: 2, + class: "space-y-6 p-6", + onSubmit: ie(j, ["prevent"]) + }, [ + t.scope === "company" ? (h(), a("section", Lr, [o("label", Rr, [o("input", { + type: "checkbox", + checked: oe.value, + disabled: x.value, + onChange: pe + }, null, 40, zr), l[8] ||= o("span", null, [o("span", { class: "block text-sm font-medium text-heading" }, "Use a company-specific AI configuration"), o("span", { class: "mt-1 block text-xs text-muted" }, "Override the global provider and models for this company.")], -1)]), oe.value ? i("", !0) : (h(), a("p", Br, "This company is using the global AI configuration."))])) : i("", !0), + ue.value ? (h(), a("div", Vr, [o("section", null, [o("label", Hr, [o("input", { + type: "checkbox", + checked: se.value, + onChange: l[0] ||= (e) => he("ai_enabled", e) + }, null, 40, Ur), l[9] ||= o("span", null, [o("span", { class: "block text-sm font-medium text-heading" }, "Enable AI Assistant"), o("span", { class: "mt-1 block text-xs text-muted" }, "Allow this provider to power chat and editor text generation.")], -1)])]), se.value ? (h(), a("div", Wr, [ + o("label", Gr, [l[10] ||= s("Provider ", -1), ne(o("select", { + "onUpdate:modelValue": l[1] ||= (e) => d.ai_driver = e, + class: "mt-1 w-full rounded-md border border-line-default bg-surface px-3 py-2 text-body" + }, [(h(!0), a(e, null, v(f.value, (e) => (h(), a("option", { + key: e.value, + value: e.value + }, b(e.label), 9, Kr))), 128))], 512), [[ee, d.ai_driver]])]), + O.value?.website ? (h(), a("p", qr, [ + l[11] ||= s("Get an API key from ", -1), + o("a", { + class: "text-primary-500 underline", + href: O.value.website, + target: "_blank", + rel: "noopener noreferrer" + }, b(O.value.label), 9, Jr), + l[12] ||= s(".", -1) + ])) : i("", !0), + o("label", Yr, [ + l[13] ||= s("API key ", -1), + o("span", Xr, [ne(o("input", { + "onUpdate:modelValue": l[2] ||= (e) => d.ai_api_key = e, + type: C.value ? "text" : "password", + autocomplete: "new-password", + class: "min-w-0 flex-1 rounded-md border border-line-default bg-surface px-3 py-2 text-body", + placeholder: le.value ? "Stored securely — enter a new key to replace it" : "" + }, null, 8, Zr), [[w, d.ai_api_key]]), o("button", { + type: "button", + class: "rounded border border-line-default px-3 text-xs text-body hover:bg-hover", + onClick: l[3] ||= (e) => C.value = !C.value + }, b(C.value ? "Hide" : "Show"), 1)]), + l[14] ||= o("span", { class: "mt-1 block text-xs font-normal text-muted" }, "Keys are stored securely. Leave the masked value unchanged to keep the current key.", -1) + ]), + (h(!0), a(e, null, v(ce.value, (t) => (h(), a("label", { + key: t.key, + class: "block text-sm font-medium text-heading" + }, [s(b(t.label) + " ", 1), t.type === "text" ? (h(), a("input", { + key: 0, + value: d[`ai_${t.key}`] ?? "", + placeholder: t.default, + class: "mt-1 w-full rounded-md border border-line-default bg-surface px-3 py-2 text-body", + onInput: (e) => ge(t.key, e) + }, null, 40, Qr)) : (h(), a("select", { + key: 1, + value: d[`ai_${t.key}`] ?? "", + class: "mt-1 w-full rounded-md border border-line-default bg-surface px-3 py-2 text-body", + onChange: (e) => ge(t.key, e) + }, [(h(!0), a(e, null, v(t.options, (e) => (h(), a("option", { + key: e.value, + value: e.value + }, b(e.label), 9, ei))), 128))], 40, $r))]))), 128)), + o("section", ti, [ + l[19] ||= o("h2", { class: "text-sm font-semibold text-heading" }, "AI capabilities", -1), + l[20] ||= o("p", { class: "mt-1 text-xs text-muted" }, "Enable only the AI tools your team needs, and choose a model for each one.", -1), + o("div", ni, [o("div", null, [o("label", ri, [o("input", { + type: "checkbox", + checked: d.ai_chat_enabled === "YES", + onChange: l[4] ||= (e) => he("ai_chat_enabled", e) + }, null, 40, ii), l[15] ||= o("span", { class: "text-sm font-medium text-heading" }, [s("Assistant chat"), o("span", { class: "mt-1 block text-xs font-normal text-muted" }, "Show the conversational AI drawer in company pages.")], -1)]), d.ai_chat_enabled === "YES" ? (h(), a("label", ai, [l[16] ||= s("Chat model", -1), ne(o("input", { + "onUpdate:modelValue": l[5] ||= (e) => d.ai_chat_model = e, + list: "ai-models", + class: "mt-1 w-full rounded-md border border-line-default bg-surface px-3 py-2" + }, null, 512), [[T, d.ai_chat_model]])])) : i("", !0)]), o("div", null, [o("label", oi, [o("input", { + type: "checkbox", + checked: d.ai_text_generation_enabled === "YES", + onChange: l[6] ||= (e) => he("ai_text_generation_enabled", e) + }, null, 40, si), l[17] ||= o("span", { class: "text-sm font-medium text-heading" }, [s("Editor text generation"), o("span", { class: "mt-1 block text-xs font-normal text-muted" }, "Add AI writing tools to rich-text editors.")], -1)]), d.ai_text_generation_enabled === "YES" ? (h(), a("label", ci, [l[18] ||= s("Text generation model", -1), ne(o("input", { + "onUpdate:modelValue": l[7] ||= (e) => d.ai_text_generation_model = e, + list: "ai-models", + class: "mt-1 w-full rounded-md border border-line-default bg-surface px-3 py-2" + }, null, 512), [[T, d.ai_text_generation_model]])])) : i("", !0)])]), + o("datalist", li, [(h(!0), a(e, null, v(k.value, (e) => (h(), a("option", { + key: e.value, + value: e.value + }, b(e.label), 9, ui))), 128))]) + ]) + ])) : i("", !0)])) : i("", !0), + ue.value ? (h(), a("footer", di, [o("button", { + type: "submit", + class: "rounded bg-btn-primary px-4 py-2 text-sm font-medium text-white hover:bg-btn-primary-hover disabled:opacity-50", + disabled: x.value + }, b(x.value ? "Saving…" : "Save settings"), 9, fi), se.value ? (h(), a("button", { + key: 0, + type: "button", + class: "rounded border border-line-default px-4 py-2 text-sm text-body hover:bg-hover disabled:opacity-50", + disabled: x.value || S.value, + onClick: me + }, b(S.value ? "Testing…" : "Test connection"), 9, pi)) : i("", !0)])) : i("", !0) + ], 32))]), + _: 1 + })]), + _: 1 + }); + }; + } +}); +//#endregion +//#region resources/js/init.ts +window.InvoiceShelf.booting((e, t, n) => { + let r = _(!1), i = _(!1), a = _(!1), o = _(!1), s = _(!1), c = _(0), d = (e, t) => { + n.notify(e, t); + }, f = async (e = !1) => { + try { + let t = e ? "/ai/admin-capabilities" : "/ai/capabilities", { data: s } = await n.client.get(t); + r.value = !!s.chat, i.value = !!s.text_generation, a.value = !!s.can_manage_company, o.value = !!s.can_manage_global; + } catch { + r.value = !1, i.value = !1, a.value = !1, o.value = !1; + } + }; + n.addMessages({ en: { ai_assistant: { + title: "AI Assistant", + chat: { + new_conversation: "New conversation", + empty: "No conversations yet.", + thinking: "Thinking…" + }, + settings: { + saved: "AI settings saved.", + connection_success: "Connection successful." + } + } } }), n.on("bootstrap:completed", ({ adminMode: e }) => { + f(e); + }), n.on("company:changing", () => { + s.value = !1, c.value += 1, r.value = !1, i.value = !1; + }), n.on("company:changed", ({ companyId: e }) => { + f(e === null); + }), n.registerHeaderAction({ + id: "ai-assistant.header", + priority: 20, + visible: () => r.value, + component: l({ setup: () => () => u(gr, { onOpen: () => { + s.value = !0; + } }) }) + }), n.registerCompanyLayoutOverlay({ + id: "ai-assistant.overlay", + component: l({ setup: () => () => u(hr, { + key: c.value, + open: s.value, + enabled: r.value, + client: n.client, + notify: d, + "onUpdate:open": (e) => { + s.value = e; + } + }) }) + }), n.registerRichEditorToolbarAction({ + id: "ai-assistant.editor", + visible: () => i.value, + component: l({ + props: { context: { + type: Object, + required: !0 + } }, + setup: (e) => () => u(Nr, { + context: e.context, + enabled: i.value, + client: n.client, + notify: d + }) + }) + }), hi(n, "admin", o, d), hi(n, "company", a, d); +}); +function hi(e, t, n, r) { + let i = { + id: `ai-assistant.${t}-settings`, + title: "AI Assistant", + icon: "SparklesIcon", + path: "ai-assistant", + priority: 80, + visible: () => n.value, + component: l({ setup: () => () => u(mi, { + client: e.client, + scope: t, + notify: r + }) }) + }; + t === "admin" ? e.registerAdminSettingsPage(i) : e.registerCompanySettingsPage(i); +} +//#endregion diff --git a/dist/style.css b/dist/style.css new file mode 100644 index 0000000..a3d801b --- /dev/null +++ b/dist/style.css @@ -0,0 +1,3 @@ +/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */ +@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-border-style:solid;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000}}}@layer utilities{.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.static{position:static}.inset-0{inset:0}.inset-y-0{inset-block:0}.right-0{right:0}.z-40{z-index:40}.z-50{z-index:50}.mx-auto{margin-inline:auto}.-mt-3{margin-top:calc(var(--spacing,.25rem) * -3)}.mt-1{margin-top:var(--spacing,.25rem)}.mt-3{margin-top:calc(var(--spacing,.25rem) * 3)}.mt-4{margin-top:calc(var(--spacing,.25rem) * 4)}.mt-5{margin-top:calc(var(--spacing,.25rem) * 5)}.mt-16{margin-top:calc(var(--spacing,.25rem) * 16)}.mb-2{margin-bottom:calc(var(--spacing,.25rem) * 2)}.mb-3{margin-bottom:calc(var(--spacing,.25rem) * 3)}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-flex{display:inline-flex}.table{display:table}.h-4{height:calc(var(--spacing,.25rem) * 4)}.h-5{height:calc(var(--spacing,.25rem) * 5)}.h-10{height:calc(var(--spacing,.25rem) * 10)}.h-14{height:calc(var(--spacing,.25rem) * 14)}.max-h-48{max-height:calc(var(--spacing,.25rem) * 48)}.min-h-11{min-height:calc(var(--spacing,.25rem) * 11)}.w-4{width:calc(var(--spacing,.25rem) * 4)}.w-5{width:calc(var(--spacing,.25rem) * 5)}.w-10{width:calc(var(--spacing,.25rem) * 10)}.w-52{width:calc(var(--spacing,.25rem) * 52)}.w-full{width:100%}.max-w-3xl{max-width:var(--container-3xl,48rem)}.max-w-\[85\%\]{max-width:85%}.max-w-xl{max-width:var(--container-xl,36rem)}.max-w-xs{max-width:var(--container-xs,20rem)}.min-w-0{min-width:0}.flex-1{flex:1}.shrink-0{flex-shrink:0}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.cursor-default{cursor:default}.cursor-pointer{cursor:pointer}.resize-none{resize:none}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.place-items-center{place-items:center}.items-center{align-items:center}.items-end{align-items:flex-end}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-end{justify-content:flex-end}.justify-start{justify-content:flex-start}.gap-1{gap:var(--spacing,.25rem)}.gap-2{gap:calc(var(--spacing,.25rem) * 2)}.gap-3{gap:calc(var(--spacing,.25rem) * 3)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(var(--spacing,.25rem) * var(--tw-space-y-reverse));margin-block-end:calc(var(--spacing,.25rem) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing,.25rem) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing,.25rem) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing,.25rem) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing,.25rem) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing,.25rem) * 5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing,.25rem) * 5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing,.25rem) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing,.25rem) * 6) * calc(1 - var(--tw-space-y-reverse)))}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-lg{border-radius:var(--radius-lg,.5rem)}.rounded-md{border-radius:var(--radius-md,.375rem)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-line-default{border-color:var(--color-line-default)}.bg-alert-error-bg{background-color:var(--color-alert-error-bg)}.bg-alert-success-bg{background-color:var(--color-alert-success-bg)}.bg-black\/20{background-color:#0003}@supports (color:color-mix(in lab, red, red)){.bg-black\/20{background-color:color-mix(in oklab, var(--color-black,#000) 20%, transparent)}}.bg-black\/30{background-color:#0000004d}@supports (color:color-mix(in lab, red, red)){.bg-black\/30{background-color:color-mix(in oklab, var(--color-black,#000) 30%, transparent)}}.bg-btn-primary{background-color:var(--color-btn-primary)}.bg-hover-strong{background-color:var(--color-hover-strong)}.bg-primary-500{background-color:var(--color-primary-500)}.bg-surface{background-color:var(--color-surface)}.bg-surface-secondary{background-color:var(--color-surface-secondary)}.bg-surface-tertiary{background-color:var(--color-surface-tertiary)}.p-1{padding:var(--spacing,.25rem)}.p-2{padding:calc(var(--spacing,.25rem) * 2)}.p-3{padding:calc(var(--spacing,.25rem) * 3)}.p-4{padding:calc(var(--spacing,.25rem) * 4)}.p-5{padding:calc(var(--spacing,.25rem) * 5)}.p-6{padding:calc(var(--spacing,.25rem) * 6)}.px-1{padding-inline:var(--spacing,.25rem)}.px-2{padding-inline:calc(var(--spacing,.25rem) * 2)}.px-3{padding-inline:calc(var(--spacing,.25rem) * 3)}.px-4{padding-inline:calc(var(--spacing,.25rem) * 4)}.px-5{padding-inline:calc(var(--spacing,.25rem) * 5)}.py-1{padding-block:var(--spacing,.25rem)}.py-2{padding-block:calc(var(--spacing,.25rem) * 2)}.py-4{padding-block:calc(var(--spacing,.25rem) * 4)}.py-12{padding-block:calc(var(--spacing,.25rem) * 12)}.pt-5{padding-top:calc(var(--spacing,.25rem) * 5)}.pb-6{padding-bottom:calc(var(--spacing,.25rem) * 6)}.text-center{text-align:center}.text-left{text-align:left}.text-sm{font-size:var(--text-sm,.875rem);line-height:var(--tw-leading,var(--text-sm--line-height,calc(1.25 / .875)))}.text-xs{font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)))}.font-medium{--tw-font-weight:var(--font-weight-medium,500);font-weight:var(--font-weight-medium,500)}.font-normal{--tw-font-weight:var(--font-weight-normal,400);font-weight:var(--font-weight-normal,400)}.font-semibold{--tw-font-weight:var(--font-weight-semibold,600);font-weight:var(--font-weight-semibold,600)}.break-words{overflow-wrap:break-word}.whitespace-pre-wrap{white-space:pre-wrap}.text-alert-error-text{color:var(--color-alert-error-text)}.text-alert-success-text{color:var(--color-alert-success-text)}.text-body{color:var(--color-body)}.text-heading{color:var(--color-heading)}.text-muted{color:var(--color-muted)}.text-primary-500{color:var(--color-primary-500)}.text-subtle{color:var(--color-subtle)}.text-white{color:var(--color-white,#fff)}.italic{font-style:italic}.underline{text-decoration-line:underline}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.outline-none{--tw-outline-style:none;outline-style:none}@media (hover:hover){.group-hover\:flex:is(:where(.group):hover *){display:flex}.hover\:bg-btn-primary-hover:hover{background-color:var(--color-btn-primary-hover)}.hover\:bg-hover:hover{background-color:var(--color-hover)}.hover\:text-alert-error-text:hover{color:var(--color-alert-error-text)}.hover\:text-heading:hover{color:var(--color-heading)}}.focus\:ring-1:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus\:ring-primary-500:focus{--tw-ring-color:var(--color-primary-500)}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}@media (width>=40rem){.sm\:flex{display:flex}.sm\:hidden{display:none}.sm\:w-\[480px\]{width:480px}.sm\:flex-col{flex-direction:column}}@media (width>=64rem){.lg\:inline{display:inline}.lg\:w-\[640px\]{width:640px}}.\[\&_a\]\:text-primary-500 a{color:var(--color-primary-500)}.\[\&_a\]\:underline a{text-decoration-line:underline}.\[\&_ol\]\:mt-3 ol{margin-top:calc(var(--spacing,.25rem) * 3)}.\[\&_ol\]\:list-decimal ol{list-style-type:decimal}.\[\&_ol\]\:pl-5 ol{padding-left:calc(var(--spacing,.25rem) * 5)}.\[\&_p\+p\]\:mt-3 p+p,.\[\&_pre\]\:mt-3 pre{margin-top:calc(var(--spacing,.25rem) * 3)}.\[\&_pre\]\:overflow-x-auto pre{overflow-x:auto}.\[\&_pre\]\:rounded pre{border-radius:.25rem}.\[\&_pre\]\:bg-surface pre{background-color:var(--color-surface)}.\[\&_pre\]\:p-3 pre{padding:calc(var(--spacing,.25rem) * 3)}.\[\&_ul\]\:mt-3 ul{margin-top:calc(var(--spacing,.25rem) * 3)}.\[\&_ul\]\:list-disc ul{list-style-type:disc}.\[\&_ul\]\:pl-5 ul{padding-left:calc(var(--spacing,.25rem) * 5)}}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000} +/*$vite$:1*/ \ No newline at end of file diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..42fb2b5 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,22 @@ +import pluginVue from 'eslint-plugin-vue' +import tseslint from 'typescript-eslint' + +export default [ + ...tseslint.configs.recommended, + ...pluginVue.configs['flat/recommended'], + { + files: ['**/*.vue'], + languageOptions: { parserOptions: { parser: tseslint.parser } }, + }, + { + rules: { + 'vue/multi-word-component-names': 'off', + 'vue/require-default-prop': 'off', + 'vue/one-component-per-file': 'off', + 'vue/require-prop-types': 'off', + 'vue/max-attributes-per-line': 'off', + 'vue/singleline-html-element-content-newline': 'off', + 'vue/multiline-html-element-content-newline': 'off', + }, + }, +] diff --git a/module.json b/module.json new file mode 100644 index 0000000..52f69d8 --- /dev/null +++ b/module.json @@ -0,0 +1,39 @@ +{ + "name": "AiAssistant", + "alias": "aiassistant", + "description": "The official free AI assistant for InvoiceShelf.", + "keywords": [ + "ai", + "assistant", + "openrouter" + ], + "priority": 0, + "providers": [ + "Modules\\AiAssistant\\Providers\\AiAssistantServiceProvider" + ], + "aliases": {}, + "files": [], + "requires": {}, + "schema_version": 2, + "slug": "ai-assistant", + "version": "1.0.0", + "license": "AGPL-3.0-only", + "compatibility": { + "invoiceshelf": ">=3.0.0-alpha.2 <4.0.0", + "module_api": "^1.2.0", + "php": "^8.4.0", + "extensions": [ + "ext-json" + ] + }, + "module_dependencies": {}, + "migration_policy": "reversible", + "uninstall": { + "data_cleanup": "Modules\\AiAssistant\\Lifecycle\\DataCleanup" + }, + "dependency_policy": "host-provided-only", + "assets": [ + "dist/init.js", + "dist/style.css" + ] +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..90eeb65 --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "name": "@invoiceshelf/module-ai-assistant", + "version": "1.0.0", + "private": true, + "type": "module", + "packageManager": "pnpm@11.6.0", + "scripts": { + "build": "vite build", + "lint": "eslint resources/js --ext .ts,.vue --max-warnings 0" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.0.0", + "@vitejs/plugin-vue": "^6.0.5", + "axios": "^1.13.6", + "eslint": "^9.39.2", + "eslint-plugin-vue": "^10.6.2", + "tailwindcss": "^4.0.0", + "typescript-eslint": "^8.57.0", + "vite": "^8.0.5", + "vue": "^3.5.31", + "vue-router": "^4.6.4" + }, + "dependencies": { + "dompurify": "^3.4.13", + "marked": "^18.0.9" + } +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..14e6423 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,12 @@ + + + + + tests + + + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..0469db8 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,2228 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + dompurify: + specifier: ^3.4.13 + version: 3.4.13 + marked: + specifier: ^18.0.9 + version: 18.0.9 + devDependencies: + '@tailwindcss/vite': + specifier: ^4.0.0 + version: 4.3.3(vite@8.2.0(jiti@2.7.0)) + '@vitejs/plugin-vue': + specifier: ^6.0.5 + version: 6.0.8(vite@8.2.0(jiti@2.7.0))(vue@3.5.40(typescript@6.0.3)) + axios: + specifier: ^1.13.6 + version: 1.19.0 + eslint: + specifier: ^9.39.2 + version: 9.39.5(jiti@2.7.0) + eslint-plugin-vue: + specifier: ^10.6.2 + version: 10.10.0(@typescript-eslint/parser@8.66.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0))(vue-eslint-parser@10.4.1(eslint@9.39.5(jiti@2.7.0))) + tailwindcss: + specifier: ^4.0.0 + version: 4.3.3 + typescript-eslint: + specifier: ^8.57.0 + version: 8.66.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3) + vite: + specifier: ^8.0.5 + version: 8.2.0(jiti@2.7.0) + vue: + specifier: ^3.5.31 + version: 3.5.40(typescript@6.0.3) + vue-router: + specifier: ^4.6.4 + version: 4.6.4(vue@3.5.40(typescript@6.0.3)) + +packages: + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.8': + resolution: {integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.29.8': + resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} + engines: {node: '>=6.9.0'} + + '@eslint-community/eslint-utils@4.10.1': + resolution: {integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.2': + resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.6': + resolution: {integrity: sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.39.5': + resolution: {integrity: sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@oxc-project/types@0.142.0': + resolution: {integrity: sha512-7W+2q5AKQVU36fkaryontrHn3YDt1RyUYXatw9i5H8ocYe2sPKSFB6eS8WNPeRKiN1qAWWZUPm7gwFzJGrccqQ==} + + '@rolldown/binding-android-arm64@1.2.2': + resolution: {integrity: sha512-l7x215OGvo1s52JWmR8U/DAVzEDWBCIbTm28aeJV/WDTSHgcKXaZTuBT0hJMs5NggilfJTW3clZVvd24yfKJxA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.2.2': + resolution: {integrity: sha512-9u9Xv6c1AJZT0FfwH5vrMG5Jjcwhc1MlyrPu0XfTqkzsmqfks2M6W/o5XwAJgVVN/jHpqqngC1WevHKKTIUtIA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.2.2': + resolution: {integrity: sha512-9W1mbGZAfW3oqd85bhBkmpyHCCzL1TeG/zFFP3vg7b0rlly8cxOcre5nXwz+LHazCwac2MNWgPdPCHndABjpWQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.2.2': + resolution: {integrity: sha512-0p1lhiCSCyaerFwtrdZQUx7NqGk6LQnaRKWX7tFQqwQgvX0rjM15cIkm3pax1UpEakK14C4mOxx/jSqCBdBRqQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.2.2': + resolution: {integrity: sha512-e+cOJXrJ2L3zx6YzqPg+f6Wbk3V1cKB8bOhbaYdVYN3DdquzNdRAmrbETz1qnt5yp/c7JNlNjmITiA2cVneQ7w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.2.2': + resolution: {integrity: sha512-JsSMsj6sNat/MuhG5fnBD7QgbtpHKVe30x5/bAVirDHdhoQRXJkF6xc0Jqk8O4fiCUQAzMOoH9wZi3m60c8wtg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.2.2': + resolution: {integrity: sha512-B5G/zJdHaoJn9vD50eGHWkiWfmq8Uhi3IiLPJTzmZTrAalk1bztUikSXo0qga18ibE0IXboyeMUnhPjhAJ45wQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.2.2': + resolution: {integrity: sha512-6mC/awzKka8W6EoekjegpfGkjz8jXWDX63pqu/HYVpyKtZfu65Jsh4QAH3Kej3CAv/c1oGX7psTmFEbr0mDxLA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.2.2': + resolution: {integrity: sha512-412MX9fJLdA1IK28EZnc8jYv2HRTleOZgfLQumJ5zy7OeJLZlg/CETwFaXjNmGVxG51cFHpKLqb5LKvBC+HsHA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.2.2': + resolution: {integrity: sha512-Q/+HI/ToJafZ1iCqGgVQXUEkIjufHCTF0gBQ2a5o3cg7GJ2h0qyq3nvvSmU+bGda2/7ygXpTY4TM6gO9OhQ0ZA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.2.2': + resolution: {integrity: sha512-ZKp/w41n6wCvxzxQHtQSbuphfX3Y4cCvbjkKHusrLx4lh+JWLTU7StSltO/DKARISzbj368d+qUaCjI8K2wzXw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.2.2': + resolution: {integrity: sha512-pxE6xD4KS3eAROkKK5yrhB9/3+vhlhVGMvlQLbdpzrBGDbKrnzx3RLwPaHvasLo6jgaiBLn7e04Df9C4tYhjmA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-win32-arm64-msvc@1.2.2': + resolution: {integrity: sha512-4MqEue5re+xIZzAWsB8sj0P1kqZySWqIuN4t6QaIO/YA6SFwySOLruvWQFzfmqk8LBK2P30KCSJwf6mJCZZ5/A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.2.2': + resolution: {integrity: sha512-NweNxxD0Nf9t8v7kodun45Ijp3EIwYY+uydPP6qBEYvfBqhIjN6dZMzlQja3tqX/aLs3F3Uz+AxDpKgRhpOZQg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@tailwindcss/node@4.3.3': + resolution: {integrity: sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==} + + '@tailwindcss/oxide-android-arm64@4.3.3': + resolution: {integrity: sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.3.3': + resolution: {integrity: sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.3.3': + resolution: {integrity: sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.3.3': + resolution: {integrity: sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': + resolution: {integrity: sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': + resolution: {integrity: sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-arm64-musl@4.3.3': + resolution: {integrity: sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-x64-gnu@4.3.3': + resolution: {integrity: sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-x64-musl@4.3.3': + resolution: {integrity: sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-wasm32-wasi@4.3.3': + resolution: {integrity: sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': + resolution: {integrity: sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.3.3': + resolution: {integrity: sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.3.3': + resolution: {integrity: sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==} + engines: {node: '>= 20'} + + '@tailwindcss/vite@4.3.3': + resolution: {integrity: sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 || ^8 + + '@types/esrecurse@4.3.1': + resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + + '@typescript-eslint/eslint-plugin@8.66.0': + resolution: {integrity: sha512-p088eaGrzYz1s+7cov0aMOCkNGTJlVxF4jgubf28c8L0Cv9Rloj8YBHnv4hXLq6IIEE1AsjNWavO+k+8kP2Y0A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.66.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.66.0': + resolution: {integrity: sha512-X6ypGChaWYk6PBtUg2BwuTZEFFcHJAtGTVJ9/lCTOufhZ4i9fNolQNnktq+kkMCwMj7V8Svsq7+TxSDslmhE0g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.66.0': + resolution: {integrity: sha512-7MthGPTt4BP69lSryqpqq8HQqxuzynssckL/jyDyk3+TNMQ3y2jFWkptCrktWvBrP+EH787Nl5N5Qpw7WZg+5g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.66.0': + resolution: {integrity: sha512-8TGcH25j9zqJ/IULB/ppyhRvxA8QYfFEZ7nfbg6/BN9spDgb8fPWQXlE5l8TWBL50EtUx007uZ1o9VOwrq2/9g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.66.0': + resolution: {integrity: sha512-9D5gLYZG4rOjcoag8MQ/fWI8WqA9wcPDyOGyWtWFhvM1lHRbliqUSPIY5J3zqCU1tvSwzXxnnjhQhz5Ne7mJ4g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.66.0': + resolution: {integrity: sha512-LG2dWfjZQQp0ADtAu/EWJVayefGL2UEZ3CDeI44D9v3rXB/WYUqE/jpO28KrEKul5AySrmI+Zh1v6v+xW2U9+g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.66.0': + resolution: {integrity: sha512-H6gcYaSDOyvL3AD/jHUtUFo2jqGgn/F6nuyuZSu0QTesxL+cP4dQoIMrODRofuJC09g64+WgZ6tE19Y1N2YIFQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.66.0': + resolution: {integrity: sha512-8/x4INiiQb10jGgXYD7116/zQ+OL84ZIFn0za68wwFHCanT/VLbBEroWht8RV8fn0/ZCAoazHLQgwUC0UQcDfg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.66.0': + resolution: {integrity: sha512-jasearZPolBw5NJNYGMwxzHMF83niVWmMU1VdHzG1CyfI2VS7f7nZltnKtHcg20hW+7Uo5GfK4MeDPoU3qI8EA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.66.0': + resolution: {integrity: sha512-dkKR8q+lKciskj1Y3vthHktl+3cMLWGyVUP23bRiPZ5O9BRT++4EqDDV+TVeIKBL1VXVEqrJlz8MYbcnvJcAlg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@vitejs/plugin-vue@6.0.8': + resolution: {integrity: sha512-0ZjgOg7oO6farnNGup7yvoM/YXZV84OZxHAwtflItNa/6zzQyVb5LNxyea3FEKEX2XlagIKzrlH7wwxkKgtiew==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + vue: ^3.2.25 + + '@vue/compiler-core@3.5.40': + resolution: {integrity: sha512-39E8IgOhTbVDnoJFMKc2DvYnypcZwUqgUhQkccva/0m6FUwtIKSGV7n1hpVmYcFaoRAwf9pBcwnKlCEsN63ZEQ==} + + '@vue/compiler-dom@3.5.40': + resolution: {integrity: sha512-pwkx4vqlqOspFstrcmzwkKLePVMD3PT65imRzLhanU2V1Fj4K13g6OXjanOyzw3aTAuRk84BOmY8f3rEHqPaVA==} + + '@vue/compiler-sfc@3.5.40': + resolution: {integrity: sha512-gIf497P4kpuALcvs5n3AEg1Vdn0pSY4XbjASIfHNYF1/MP3T2Mf2STERTubysBxCRxzJGJYtF/O7vwJrxFB3Vw==} + + '@vue/compiler-ssr@3.5.40': + resolution: {integrity: sha512-rrE5xiXG663+vHCHa3J9p2z5OcBRjXmoqenprJxAFQxg5pSshzeBiCE6pu46axapRJ2Adk0YDA2BRZVjiHXnhg==} + + '@vue/devtools-api@6.6.4': + resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==} + + '@vue/reactivity@3.5.40': + resolution: {integrity: sha512-B7ot9UlUZOi1zbq61/LvE88ZLTV8IlajTdiZTAEiDQgrnIMIZoPr9kGw0Zw46ObW62O9+H/Be3kMbfb7kYPQZA==} + + '@vue/runtime-core@3.5.40': + resolution: {integrity: sha512-KAZLweuZ6uUJPK1PMSQPgBU5gCjgrrfjUhSglmU9NhH+Zjepa8cnwSydPWDWHDwOgY4g3VcZ+PljbiHlURNCbw==} + + '@vue/runtime-dom@3.5.40': + resolution: {integrity: sha512-ZfrX8ssZQds900L9pr8AuK05ddnMsR4MPMZr8cPN9GoqoPWcXLhjvvbIA2SMv+7a97sJ1vv9pj/zxK0Cq/eEFQ==} + + '@vue/server-renderer@3.5.40': + resolution: {integrity: sha512-XNJym9WpevhTVt1HuwOrCRJ5Q+9z4BjTMrDtjTrvx74SmUll8spNTw6whWJa9mEkO4PKn5TihI/bm/8ds2QVJw==} + + '@vue/shared@3.5.40': + resolution: {integrity: sha512-WxnBtruIqOoV3rA4jeKDWzrYI5h7Cp4+pjwDi8kWGHz+IslhiN+wguLVVhtv2l8VoU02rzDCVfDjgCl1lNpZVg==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.18.0: + resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + axios@1.19.0: + resolution: {integrity: sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + brace-expansion@1.1.18: + resolution: {integrity: sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==} + + brace-expansion@5.0.9: + resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} + engines: {node: 20 || >=22} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + dompurify@3.4.13: + resolution: {integrity: sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + enhanced-resolve@5.24.5: + resolution: {integrity: sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==} + engines: {node: '>=10.13.0'} + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-plugin-vue@10.10.0: + resolution: {integrity: sha512-dL9x9rBHqqNcByWiLOHK6L0SB97V82/NC0cZRn9cXPjM7pCuWlpQQP9bFH4vjBv80ej1ZpzAkuD8zWH1o9bZbA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@stylistic/eslint-plugin': ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 + '@typescript-eslint/parser': ^7.0.0 || ^8.0.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + vue-eslint-parser: ^10.3.0 + peerDependenciesMeta: + '@stylistic/eslint-plugin': + optional: true + '@typescript-eslint/parser': + optional: true + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-scope@9.1.2: + resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@9.39.5: + resolution: {integrity: sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + espree@11.2.0: + resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.4.4: + resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==} + + follow-redirects@1.16.0: + resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.6: + resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} + engines: {node: '>= 6'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.6: + resolution: {integrity: sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + + js-yaml@4.3.1: + resolution: {integrity: sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} + engines: {node: '>= 12.0.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + marked@18.0.9: + resolution: {integrity: sha512-/Sa4qiiHZxf0/FQdBBowr9q4r10krCwMvpK48FUBdXdUXScDxiQGR9zCPrFgRVR5LU3iySOiIjy09ZQvADir1w==} + engines: {node: '>= 20'} + hasBin: true + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + minimatch@10.2.6: + resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} + engines: {node: 18 || 20 || >=22} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.17: + resolution: {integrity: sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + + postcss-selector-parser@7.1.4: + resolution: {integrity: sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==} + engines: {node: '>=4'} + + postcss@8.5.25: + resolution: {integrity: sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + proxy-from-env@2.1.0: + resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} + engines: {node: '>=10'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + rolldown@1.2.2: + resolution: {integrity: sha512-opwpo1tQBAcpSUJDt94B7hhLNGOKjCdE//XXjeLrnx9b83bjnw45tXdg1b09yEw/VLFBJGZpwRULMmOZo7ol+A==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + tailwindcss@4.3.3: + resolution: {integrity: sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==} + + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + typescript-eslint@8.66.0: + resolution: {integrity: sha512-QlEbBPz/RuJ1XUHj29nm3t0F/O/cSlEnntozqPOYHnnTGAXFamnMBu5i9Vn6vhUPHGAjR+Vl+5J8vPN/BMUrJw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vite@8.2.0: + resolution: {integrity: sha512-pn+CFpM0lwDeKwmOq1ZaBK/9sjorZcgqxki6MbY/jPEVd9vichIlmlD4HmQ5wdP5EgqQCFRaACBxMC7uEGc6lQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.4.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vue-eslint-parser@10.4.1: + resolution: {integrity: sha512-Gk6gRDj0n/fkRa3C3l0bBheoBckUq/Rs0F/TvMWIS6nzzx67amAViMe9CkNgsP2tXyQONvGiHQESHwFtZ3aYDA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + + vue-router@4.6.4: + resolution: {integrity: sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==} + peerDependencies: + vue: ^3.5.0 + + vue@3.5.40: + resolution: {integrity: sha512-+8PJ4SJXdn/cHGImF4CKdxlWHIN5Dkt7DoufRREM6h6uVCx2m7QxgcEQmmzyOK8A9mcafg7sFbJFYsdFVubTig==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + +snapshots: + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/parser@7.29.8': + dependencies: + '@babel/types': 7.29.8 + + '@babel/types@7.29.8': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@eslint-community/eslint-utils@4.10.1(eslint@9.39.5(jiti@2.7.0))': + dependencies: + eslint: 9.39.5(jiti@2.7.0) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.21.2': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.6': + dependencies: + ajv: 6.15.0 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.3.1 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.39.5': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@oxc-project/types@0.142.0': {} + + '@rolldown/binding-android-arm64@1.2.2': + optional: true + + '@rolldown/binding-darwin-arm64@1.2.2': + optional: true + + '@rolldown/binding-darwin-x64@1.2.2': + optional: true + + '@rolldown/binding-freebsd-x64@1.2.2': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.2.2': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.2.2': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.2.2': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.2.2': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.2.2': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.2.2': + optional: true + + '@rolldown/binding-linux-x64-musl@1.2.2': + optional: true + + '@rolldown/binding-openharmony-arm64@1.2.2': + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.2.2': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.2.2': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@tailwindcss/node@4.3.3': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.24.5 + jiti: 2.7.0 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.3.3 + + '@tailwindcss/oxide-android-arm64@4.3.3': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.3.3': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.3.3': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.3.3': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.3.3': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.3.3': + optional: true + + '@tailwindcss/oxide@4.3.3': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.3.3 + '@tailwindcss/oxide-darwin-arm64': 4.3.3 + '@tailwindcss/oxide-darwin-x64': 4.3.3 + '@tailwindcss/oxide-freebsd-x64': 4.3.3 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.3 + '@tailwindcss/oxide-linux-arm64-gnu': 4.3.3 + '@tailwindcss/oxide-linux-arm64-musl': 4.3.3 + '@tailwindcss/oxide-linux-x64-gnu': 4.3.3 + '@tailwindcss/oxide-linux-x64-musl': 4.3.3 + '@tailwindcss/oxide-wasm32-wasi': 4.3.3 + '@tailwindcss/oxide-win32-arm64-msvc': 4.3.3 + '@tailwindcss/oxide-win32-x64-msvc': 4.3.3 + + '@tailwindcss/vite@4.3.3(vite@8.2.0(jiti@2.7.0))': + dependencies: + '@tailwindcss/node': 4.3.3 + '@tailwindcss/oxide': 4.3.3 + tailwindcss: 4.3.3 + vite: 8.2.0(jiti@2.7.0) + + '@types/esrecurse@4.3.1': {} + + '@types/estree@1.0.9': {} + + '@types/json-schema@7.0.15': {} + + '@types/trusted-types@2.0.7': + optional: true + + '@typescript-eslint/eslint-plugin@8.66.0(@typescript-eslint/parser@8.66.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.66.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.66.0 + '@typescript-eslint/type-utils': 8.66.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/utils': 8.66.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.66.0 + eslint: 9.39.5(jiti@2.7.0) + ignore: 7.0.6 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.66.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.66.0 + '@typescript-eslint/types': 8.66.0 + '@typescript-eslint/typescript-estree': 8.66.0(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.66.0 + debug: 4.4.3 + eslint: 9.39.5(jiti@2.7.0) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.66.0(typescript@6.0.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.66.0(typescript@6.0.3) + '@typescript-eslint/types': 8.66.0 + debug: 4.4.3 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.66.0': + dependencies: + '@typescript-eslint/types': 8.66.0 + '@typescript-eslint/visitor-keys': 8.66.0 + + '@typescript-eslint/tsconfig-utils@8.66.0(typescript@6.0.3)': + dependencies: + typescript: 6.0.3 + + '@typescript-eslint/type-utils@8.66.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3)': + dependencies: + '@typescript-eslint/types': 8.66.0 + '@typescript-eslint/typescript-estree': 8.66.0(typescript@6.0.3) + '@typescript-eslint/utils': 8.66.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3) + debug: 4.4.3 + eslint: 9.39.5(jiti@2.7.0) + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.66.0': {} + + '@typescript-eslint/typescript-estree@8.66.0(typescript@6.0.3)': + dependencies: + '@typescript-eslint/project-service': 8.66.0(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.66.0(typescript@6.0.3) + '@typescript-eslint/types': 8.66.0 + '@typescript-eslint/visitor-keys': 8.66.0 + debug: 4.4.3 + minimatch: 10.2.6 + semver: 7.8.5 + tinyglobby: 0.2.17 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.66.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3)': + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5(jiti@2.7.0)) + '@typescript-eslint/scope-manager': 8.66.0 + '@typescript-eslint/types': 8.66.0 + '@typescript-eslint/typescript-estree': 8.66.0(typescript@6.0.3) + eslint: 9.39.5(jiti@2.7.0) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.66.0': + dependencies: + '@typescript-eslint/types': 8.66.0 + eslint-visitor-keys: 5.0.1 + + '@vitejs/plugin-vue@6.0.8(vite@8.2.0(jiti@2.7.0))(vue@3.5.40(typescript@6.0.3))': + dependencies: + '@rolldown/pluginutils': 1.0.1 + vite: 8.2.0(jiti@2.7.0) + vue: 3.5.40(typescript@6.0.3) + + '@vue/compiler-core@3.5.40': + dependencies: + '@babel/parser': 7.29.8 + '@vue/shared': 3.5.40 + entities: 7.0.1 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.40': + dependencies: + '@vue/compiler-core': 3.5.40 + '@vue/shared': 3.5.40 + + '@vue/compiler-sfc@3.5.40': + dependencies: + '@babel/parser': 7.29.8 + '@vue/compiler-core': 3.5.40 + '@vue/compiler-dom': 3.5.40 + '@vue/compiler-ssr': 3.5.40 + '@vue/shared': 3.5.40 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.25 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.40': + dependencies: + '@vue/compiler-dom': 3.5.40 + '@vue/shared': 3.5.40 + + '@vue/devtools-api@6.6.4': {} + + '@vue/reactivity@3.5.40': + dependencies: + '@vue/shared': 3.5.40 + + '@vue/runtime-core@3.5.40': + dependencies: + '@vue/reactivity': 3.5.40 + '@vue/shared': 3.5.40 + + '@vue/runtime-dom@3.5.40': + dependencies: + '@vue/reactivity': 3.5.40 + '@vue/runtime-core': 3.5.40 + '@vue/shared': 3.5.40 + csstype: 3.2.3 + + '@vue/server-renderer@3.5.40': + dependencies: + '@vue/compiler-ssr': 3.5.40 + '@vue/runtime-dom': 3.5.40 + '@vue/shared': 3.5.40 + + '@vue/shared@3.5.40': {} + + acorn-jsx@5.3.2(acorn@8.18.0): + dependencies: + acorn: 8.18.0 + + acorn@8.18.0: {} + + agent-base@6.0.2: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + argparse@2.0.1: {} + + asynckit@0.4.0: {} + + axios@1.19.0: + dependencies: + follow-redirects: 1.16.0 + form-data: 4.0.6 + https-proxy-agent: 5.0.1 + proxy-from-env: 2.1.0 + transitivePeerDependencies: + - debug + - supports-color + + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + + boolbase@1.0.0: {} + + brace-expansion@1.1.18: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@5.0.9: + dependencies: + balanced-match: 4.0.4 + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + callsites@3.1.0: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + concat-map@0.0.1: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + cssesc@3.0.0: {} + + csstype@3.2.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-is@0.1.4: {} + + delayed-stream@1.0.0: {} + + detect-libc@2.1.2: {} + + dompurify@3.4.13: + optionalDependencies: + '@types/trusted-types': 2.0.7 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + enhanced-resolve@5.24.5: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + + entities@7.0.1: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + + escape-string-regexp@4.0.0: {} + + eslint-plugin-vue@10.10.0(@typescript-eslint/parser@8.66.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0))(vue-eslint-parser@10.4.1(eslint@9.39.5(jiti@2.7.0))): + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5(jiti@2.7.0)) + eslint: 9.39.5(jiti@2.7.0) + natural-compare: 1.4.0 + nth-check: 2.1.1 + postcss-selector-parser: 7.1.4 + semver: 7.8.5 + vue-eslint-parser: 10.4.1(eslint@9.39.5(jiti@2.7.0)) + xml-name-validator: 5.0.0 + optionalDependencies: + '@typescript-eslint/parser': 8.66.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3) + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-scope@9.1.2: + dependencies: + '@types/esrecurse': 4.3.1 + '@types/estree': 1.0.9 + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@9.39.5(jiti@2.7.0): + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5(jiti@2.7.0)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.2 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.6 + '@eslint/js': 9.39.5 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.9 + ajv: 6.15.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.7.0 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.18.0 + acorn-jsx: 5.3.2(acorn@8.18.0) + eslint-visitor-keys: 4.2.1 + + espree@11.2.0: + dependencies: + acorn: 8.18.0 + acorn-jsx: 5.3.2(acorn@8.18.0) + eslint-visitor-keys: 5.0.1 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@2.0.2: {} + + esutils@2.0.3: {} + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fdir@6.5.0(picomatch@4.0.5): + optionalDependencies: + picomatch: 4.0.5 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.4 + keyv: 4.5.4 + + flatted@3.4.4: {} + + follow-redirects@1.16.0: {} + + form-data@4.0.6: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.4 + mime-types: 2.1.35 + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@14.0.0: {} + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + has-flag@4.0.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + ignore@5.3.2: {} + + ignore@7.0.6: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + isexe@2.0.0: {} + + jiti@2.7.0: {} + + js-yaml@4.3.1: + dependencies: + argparse: 2.0.1 + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-android-arm64@1.33.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.33.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.33.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.33.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.33.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.33.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.33.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.33.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.33.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.33.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + lightningcss@1.33.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + marked@18.0.9: {} + + math-intrinsics@1.1.0: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + minimatch@10.2.6: + dependencies: + brace-expansion: 5.0.9 + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.18 + + ms@2.1.3: {} + + nanoid@3.3.17: {} + + natural-compare@1.4.0: {} + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + picocolors@1.1.1: {} + + picomatch@4.0.5: {} + + postcss-selector-parser@7.1.4: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss@8.5.25: + dependencies: + nanoid: 3.3.17 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + proxy-from-env@2.1.0: {} + + punycode@2.3.1: {} + + resolve-from@4.0.0: {} + + rolldown@1.2.2: + dependencies: + '@oxc-project/types': 0.142.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.2.2 + '@rolldown/binding-darwin-arm64': 1.2.2 + '@rolldown/binding-darwin-x64': 1.2.2 + '@rolldown/binding-freebsd-x64': 1.2.2 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.2 + '@rolldown/binding-linux-arm64-gnu': 1.2.2 + '@rolldown/binding-linux-arm64-musl': 1.2.2 + '@rolldown/binding-linux-ppc64-gnu': 1.2.2 + '@rolldown/binding-linux-s390x-gnu': 1.2.2 + '@rolldown/binding-linux-x64-gnu': 1.2.2 + '@rolldown/binding-linux-x64-musl': 1.2.2 + '@rolldown/binding-openharmony-arm64': 1.2.2 + '@rolldown/binding-win32-arm64-msvc': 1.2.2 + '@rolldown/binding-win32-x64-msvc': 1.2.2 + + semver@7.8.5: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + source-map-js@1.2.1: {} + + strip-json-comments@3.1.1: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + tailwindcss@4.3.3: {} + + tapable@2.3.3: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + + ts-api-utils@2.5.0(typescript@6.0.3): + dependencies: + typescript: 6.0.3 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typescript-eslint@8.66.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.66.0(@typescript-eslint/parser@8.66.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/parser': 8.66.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.66.0(typescript@6.0.3) + '@typescript-eslint/utils': 8.66.0(eslint@9.39.5(jiti@2.7.0))(typescript@6.0.3) + eslint: 9.39.5(jiti@2.7.0) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + typescript@6.0.3: {} + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + util-deprecate@1.0.2: {} + + vite@8.2.0(jiti@2.7.0): + dependencies: + lightningcss: 1.33.0 + picomatch: 4.0.5 + postcss: 8.5.25 + rolldown: 1.2.2 + tinyglobby: 0.2.17 + optionalDependencies: + fsevents: 2.3.3 + jiti: 2.7.0 + + vue-eslint-parser@10.4.1(eslint@9.39.5(jiti@2.7.0)): + dependencies: + debug: 4.4.3 + eslint: 9.39.5(jiti@2.7.0) + eslint-scope: 9.1.2 + eslint-visitor-keys: 5.0.1 + espree: 11.2.0 + esquery: 1.7.0 + semver: 7.8.5 + transitivePeerDependencies: + - supports-color + + vue-router@4.6.4(vue@3.5.40(typescript@6.0.3)): + dependencies: + '@vue/devtools-api': 6.6.4 + vue: 3.5.40(typescript@6.0.3) + + vue@3.5.40(typescript@6.0.3): + dependencies: + '@vue/compiler-dom': 3.5.40 + '@vue/compiler-sfc': 3.5.40 + '@vue/runtime-dom': 3.5.40 + '@vue/server-renderer': 3.5.40 + '@vue/shared': 3.5.40 + optionalDependencies: + typescript: 6.0.3 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + xml-name-validator@5.0.0: {} + + yocto-queue@0.1.0: {} diff --git a/resources/ai/prompts/chat-system.md b/resources/ai/prompts/chat-system.md new file mode 100644 index 0000000..4abbdcc --- /dev/null +++ b/resources/ai/prompts/chat-system.md @@ -0,0 +1,11 @@ +You are the InvoiceShelf assistant, embedded in an invoicing application. You help {{user_name}} answer questions about **{{company_name}}**'s data: invoices, estimates, payments, expenses, customers, and items. + +Today is {{today}}. + +Rules: +- Use the provided tools whenever the user asks about specific records. Do not invent data. +- Only answer questions about {{company_name}}'s data. If asked about another company or unrelated topics, politely decline. +- You cannot modify data — you are read-only. If the user asks you to create/update/delete something, explain that they need to do it in the UI. +- Be concise. Format responses in Markdown (headings, **bold**, bullet lists, tables when comparing multiple records). +- Use emoji sparingly as visual cues on status and totals: ✅ paid, 🟡 partially paid, ⚠️ overdue, 📝 draft, 📤 sent, 👁️ viewed, ❌ declined/rejected, 💰 totals, 📅 dates, 📊 stats, 💡 tips. One emoji per bullet is plenty — don't decorate every sentence. +- If a tool returns an error or empty result, say so plainly and suggest a next step. diff --git a/resources/ai/prompts/text-generation.md b/resources/ai/prompts/text-generation.md new file mode 100644 index 0000000..7de959e --- /dev/null +++ b/resources/ai/prompts/text-generation.md @@ -0,0 +1,4 @@ +You are a helpful writing assistant embedded in an invoicing application. Generate text based on the user's instruction. Rules: +- Return only the requested text. No preamble, no explanation, no markdown code fences. +- Match the tone and language implied by the instruction. +- Be concise unless explicitly asked for length. diff --git a/resources/css/module.css b/resources/css/module.css new file mode 100644 index 0000000..1908a9b --- /dev/null +++ b/resources/css/module.css @@ -0,0 +1,28 @@ +@reference "tailwindcss/theme.css"; +@import "tailwindcss/utilities" layer(utilities); + +@source "../js/**/*.vue"; +@source "../js/**/*.ts"; + +/* Map module utilities to the active InvoiceShelf theme. */ +@theme inline reference { + --color-primary-50: var(--color-primary-50); + --color-primary-500: var(--color-primary-500); + --color-primary-600: var(--color-primary-600); + --color-surface: var(--color-surface); + --color-surface-secondary: var(--color-surface-secondary); + --color-surface-tertiary: var(--color-surface-tertiary); + --color-heading: var(--color-heading); + --color-body: var(--color-body); + --color-muted: var(--color-muted); + --color-subtle: var(--color-subtle); + --color-line-default: var(--color-line-default); + --color-hover: var(--color-hover); + --color-hover-strong: var(--color-hover-strong); + --color-btn-primary: var(--color-btn-primary); + --color-btn-primary-hover: var(--color-btn-primary-hover); + --color-alert-error-bg: var(--color-alert-error-bg); + --color-alert-error-text: var(--color-alert-error-text); + --color-alert-success-bg: var(--color-alert-success-bg); + --color-alert-success-text: var(--color-alert-success-text); +} diff --git a/resources/js/components/AiChatOverlay.vue b/resources/js/components/AiChatOverlay.vue new file mode 100644 index 0000000..e37725b --- /dev/null +++ b/resources/js/components/AiChatOverlay.vue @@ -0,0 +1,171 @@ + + +