diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..1d6d4c66 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,57 @@ +name: Release +on: + workflow_dispatch: + +jobs: + release: + permissions: + contents: write + strategy: + fail-fast: false + matrix: + platform: [macos-latest, ubuntu-22.04, windows-latest] + runs-on: ${{ matrix.platform }} + environment: Build Release + env: + VITE_ONEDOCS_API_URL: ${{ secrets.ONEDOCS_API_URL }} + VITE_ONEDOCS_API_KEY: ${{ secrets.ONEDOCS_API_KEY }} + VITE_SILICONFLOW_TOKEN: ${{ secrets.SILICONFLOW_TOKEN }} + VITE_BUILD_TARGET: esnext + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install dependencies (Ubuntu only) + if: matrix.platform == 'ubuntu-22.04' + run: | + sudo apt-get update + sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 'lts/*' + + - name: Install Rust stable + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }} + + - name: Install frontend dependencies + run: npm ci + + - name: Build the app + run: npm run tauri build + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: OneDocs-${{ matrix.platform }} + path: | + src-tauri/target/release/bundle/msi/*.msi + src-tauri/target/release/bundle/nsis/*.exe + src-tauri/target/release/bundle/dmg/*.dmg + src-tauri/target/release/bundle/deb/*.deb + src-tauri/target/release/bundle/appimage/*.AppImage + if-no-files-found: warn diff --git a/.gitignore b/.gitignore index c83ac4bc..ca813ab3 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ dist-ssr package-lock.json src-tauri/Cargo.toml src-tauri/Cargo.toml +.env diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..0c400b92 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,41 @@ +# Repository Guidelines + +## Project Structure & Module Organization +- `src/`: React + TypeScript front end (core app). Key areas include `components/`, `pages/`, `services/`, `store/`, `hooks/`, `utils/`, and `styles/`. +- `src-tauri/`: Tauri (Rust) desktop shell and configuration. +- `docs/`: Static docs/site assets. +- `dist/`: Build output (generated). +- `index.html`, `vite.config.ts`, `tsconfig*.json`: Vite and TypeScript setup. + +## Build, Test, and Development Commands +- `npm install`: Install Node dependencies. +- `npm run dev`: Start the Vite web dev server. +- `npm run build`: Type-check (`tsc`) and build the web app. +- `npm run preview`: Preview the production web build. +- `npm run tauri:dev`: Run the desktop app in dev mode (requires Rust + Tauri toolchain). +- `npm run tauri:build`: Build desktop app bundles. + +## Coding Style & Naming Conventions +- TypeScript + React with `.tsx` for UI, `.ts` for logic. +- Use 2-space indentation, single quotes, and semicolons (match existing files like `src/main.tsx`). +- Prefer `PascalCase` for React components and `camelCase` for functions/variables. +- Keep CSS in `src/styles/` and reuse existing class naming patterns. + +## Testing Guidelines +- No automated test framework is configured in this repo. +- If you add tests, document the framework and add a script in `package.json`. +- For now, validate changes manually: + - Web UI: `npm run dev` + - Desktop: `npm run tauri:dev` + +## Commit & Pull Request Guidelines +- Commit history shows a mix of `feat:`-style prefixes and simple imperatives (e.g., “Update README”). Either is acceptable; keep messages short and specific. +- PRs should include: + - A clear summary of changes and the motivation. + - Steps to validate (commands and expected results). + - Screenshots or screen recordings for UI changes. + - Linked issues or feature requests when applicable. + +## Configuration & Secrets +- API keys are entered in the app UI; do not hardcode secrets. +- If new env vars are added, document them in `README_EN.md` and keep defaults safe. diff --git a/LICENSE b/LICENSE index 261eeb9e..f288702d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,201 +1,674 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 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 General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is 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. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + 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. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + 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 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. Use with the GNU Affero General Public License. + + 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 Affero 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 special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU 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 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 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 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 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + 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 GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md index e74ef3ce..b608a41b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,14 @@ ![OneDocs](https://socialify.git.ci/LYOfficial/OneDocs/image?description=1&font=KoHo&forks=1&issues=1&language=1&logo=https%3A%2F%2Foss.1n.hk%2Flyofficial%2Fimages%2Ficon.png&name=1&owner=1&pattern=Plus&pulls=1&stargazers=1&theme=Auto) +

+ 中文简体 | English +

+ +

+milestone2 OneDocs - A single text, all is known. | Product Hunt +

+ + # OneDocs > A Single Text, All is Known. @@ -13,13 +22,28 @@ **文章千卷,一览而知。智慧之器,助君析文明理。** OneDocs者,一文亦闻也,乃集诸多智能提示之力,助君速览文档精髓,无论新闻要览、数据解析,抑或学科要点,皆可一键明了。 -## 测试截图 -![介绍页](https://oss.1n.hk/image/up/02d537a812ec13ab1afe9c7934a81e5a.png) +## 项目介绍 +当今数据时代,各行各业有各种文档需要我们阅读和分析,有每日新鲜的新闻报告、工作场景的数据表格、学习生活中的课件文档……各种文档中**有精华亦有糟粕**,人力阅读和筛选会**占用大量的时间**,有没有什么工具能**将那些无用内容筛去**,像学霸笔记那样**将精华内容整理分析**方便我们阅读和学习呢? + +于是我做了这个AI工具,**OneDocs**,给它起了个好听的中文名「**一文亦闻**」,结合文件分析、大模型应用和输出格式规范化,对用户上传的文件进行解构分析,去除无用内容,整理成知识手册。希望用户能通过大模型的力量,通过一个个简单的文档了解事情的本质,学习和进步。 + +目前软件支持**PDF**文档格式,支持**40+模型**选择,支持**Windows/macOS/Linux跨平台**使用,基本满足各类用户使用需求。且软件为本地下载使用,无文件上传,不会造成文件和API Key泄露。 + +软件功能主要有四种: + +- **要闻概览**——新闻要点梳理 +- **罗森析数**——数据内容分析 +- **理工速知**——理工课件整理 +- **文采丰呈**——文科课件整理 -![应用页](https://oss.1n.hk/image/up/8fa2905efe5922ce320d6744d734ec8d.png) +分析后的结果可以预览、复制Markdown源码和导出下载,亦可以同时分析多文件进行合并下载。 -![源码页](https://oss.1n.hk/image/up/3c639cb78d434110f31b450f57de117d.png) +## 软件截图 + +OneDocsOneDocs +OneDocsOneDocs +OneDocsOneDocs ## 使用方法 @@ -60,31 +84,186 @@ OneDocs者,一文亦闻也,乃集诸多智能提示之力,助君速览文 要参与开发和部署这个项目,请先克隆本仓库: ```bash - git clone https://github.com/LYOfficial/OneDocs.git +git clone https://github.com/LYOfficial/OneDocs.git ``` -安装Rust: https://rust-lang.org/zh-CN/tools/install/ +### 环境要求 + +- Node.js(推荐 LTS)+ npm +- Rust + Tauri CLI(桌面端开发必须) +- macOS 用户可用以下方式安装 Rust:https://rust-lang.org/zh-CN/tools/install/ + ```bash -# MacOS 用户选择 curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh ``` +### Tauri 版本说明(重要) + +在部分镜像源环境(如 `ustc`)下,Tauri Rust crate 可用版本会落后于 npm 包最新版本。 +为避免 `tauri-runtime` / `tauri-runtime-wry` 版本冲突,请固定使用当前仓库已验证可运行的版本组合,并使用**精确版本**(不要使用 `^`): + +- npm: + - `@tauri-apps/api = 2.6.0` + - `@tauri-apps/cli = 2.6.0` + - `@tauri-apps/plugin-dialog = 2.3.0` + - `@tauri-apps/plugin-fs = 2.4.0` + - `@tauri-apps/plugin-shell = 2.3.0` +- Rust(`src-tauri/Cargo.toml`): + - `tauri = =2.6.0` + - `tauri-build = =2.6.0` + - `tauri-runtime = =2.9.2` + - `tauri-plugin-dialog = =2.3.0` + - `tauri-plugin-fs = =2.4.0` + - `tauri-plugin-shell = =2.3.0` + +如需升级 Tauri,请保持 npm 与 Rust 端**同一代版本**并重新验证 `npm run tauri:dev`。 -启动开发服务器: +### 启动与构建 ```bash - npm install - npm run tauri dev +npm install + +# Web 前端(Vite) +npm run dev + +# Desktop(Tauri) +npm run tauri:dev ``` -构建: +构建发行版: ```bash - npm run tauri build +# Web 前端 +npm run build + +# Desktop(Tauri) +npm run tauri:build ``` +### 项目结构(核心) + +- src/:React + TypeScript 前端主应用 + - components/:通用组件(上传、结果、模型选择、设置面板等) + - pages/:页面入口(Landing / Analysis / AnalysisResult / Settings) + - hooks/:核心业务 Hook(析文流程) + - services/:API 与 Tauri 调用封装 + - store/:全局状态(Zustand + persist) + - utils/:文档解析、Markdown/LaTeX 渲染 + - config/:模型提供商、提示词配置、功能信息 + - i18n/:多语言资源 + - styles/:全局与组件样式 +- src-tauri/:Tauri 桌面端(Rust) + - src/main.rs:Tauri 入口与命令注册 + - src/lib.rs:请求转发与 API 调用 +- docs/:静态站点资源 +- dist/:构建输出(自动生成) + +### 运行流程与构成 + +1. 用户选择文档与功能(FunctionSelector / FileUpload) +2. DocumentProcessor 将文件解析为纯文本 +3. useAnalysis 组装提示词,调用 APIService +4. APIService 通过 Tauri invoke 调用 Rust 层请求 +5. Rust 使用 reqwest 访问模型 API,将结果返回前端 +6. Zustand 保存结果并由 ResultDisplay 渲染、导出或复制 + +### 功能与提示词 + +提示词配置位于 config/prompts/index.ts,对应四类功能: + +- 要闻概览(news) +- 罗森析数(data) +- 理工速知(science,支持可选格式复查) +- 文采丰呈(liberal) + +### 模型与提供商(当前内置) + +模型配置位于 config/providers.ts,支持官方与第三方 API、以及本地模型服务: + +- OneDocs:Qwen2.5-7B、Qwen3-8B、GLM-4-9B、GLM-Z1-9B 0414、GLM-4.1V-9B Thinking、GLM-4-9B 0414、DeepSeek-R1 Qwen3-8B、GLM-4-Flash +- OpenAI:gpt-4o、gpt-4o-mini、gpt-4、gpt-3.5-turbo +- Anthropic:Claude 3.5 Sonnet/Haiku、Claude 3 Opus/Sonnet +- Google Gemini:gemini-3-pro、2.5-pro、2.5-flash、1.5-pro、1.5-flash +- Moonshot:moonshot-v1-8k/32k/128k +- 智谱 GLM:glm-4-flash/flashx/plus/0520/long/4v-plus +- DeepSeek:deepseek-chat、deepseek-reasoner +- Ollama(本地):llama3.2、qwen2.5、gemma2、mistral +- LM Studio(本地):local-model +- 兼容平台:优云智算、302.AI、小马算力、硅基流动、星河大模型、PPIO、ModelScope、OneAPI + +> 也可在设置中添加自定义模型(Custom Provider + 自定义模型名)。 + +### 技术栈 + +| 层级 | 技术 | +|------|------| +| 前端框架 | React 19 + TypeScript | +| 桌面端 | Tauri v2(Rust) | +| 构建工具 | Vite 7 | +| 状态管理 | Zustand 5 | +| PDF 解析 | pdfjs-dist v5(前端文本提取)+ lopdf(Rust 端图片提取) | +| 渲染 | marked(Markdown)+ KaTeX(LaTeX) | +| 国际化 | i18next + react-i18next | + +### 文档解析与限制 + +- 支持:PDF +- 最大文件体积:30 MB(FILE_SIZE_LIMIT) +- 解析位于 utils/documentProcessor.ts(基于 pdfjs),图片提取位于 Rust 端(lopdf) + +## 贡献指南 + +我们热烈欢迎每一位开发者的贡献! + +- 在开始前,请确保已阅读 [开发](#开发) 部分的环境要求与版本说明。 +- 欢迎通过 [Pull Request](https://github.com/LYOfficial/OneDocs/pulls) 或 [GitHub Issues](https://github.com/LYOfficial/OneDocs/issues) 分享您的想法与改进。 +- 提交 PR 时,请包含:变更摘要、验证步骤、UI 变更的截图或录屏。 +- Commit 信息请保持简明扼要,可使用 `feat:` / `fix:` / `docs:` 等前缀。 + +## 版权声明 + +版权所有 © 2024-2026 LYOfficial。 + +本项目基于 [GNU 通用公共许可证 v3.0](https://github.com/LYOfficial/OneDocs/blob/main/LICENSE) 发布。 + +## 鸣谢 + +衷心感谢以下开源项目,OneDocs 的开发离不开它们: + +- [Tauri](https://github.com/tauri-apps/tauri) — 跨平台桌面应用框架 +- [lopdf](https://github.com/J-F-Liu/lopdf) — Rust PDF 处理库,用于图片提取 +- [pdfjs-dist](https://github.com/nicbarker/pdfjs-dist) — PDF 文本解析 +- [React](https://github.com/facebook/react) — 用户界面构建库 +- [marked](https://github.com/markedjs/marked) — Markdown 解析器 +- [KaTeX](https://github.com/KaTeX/KaTeX) — LaTeX 数学公式渲染 +- [Zustand](https://github.com/pmndrs/zustand) — 轻量状态管理 +- [i18next](https://github.com/i18next/i18next) — 国际化框架 + +## 联系我们 + +- 邮箱:coldregion@qq.com +- GitHub Issues:[提交问题](https://github.com/LYOfficial/OneDocs/issues) +- Wiki:[OneDocs Wiki](https://github.com/LYOfficial/OneDocs/wiki) + +### 环境变量(可选) + +如果要内置 OneDocs 服务的 Base URL / API Key,可设置: + +- VITE_ONEDOCS_API_URL +- VITE_ONEDOCS_API_KEY + ## 作者 - [@LYOfficial ](https://github.com/LYOfficial/) 主要开发,项目主管。 - [@JHL-HK](https://github.com/JHL-HK) 部分重构,图床提供。 + +## 特别鸣谢 + +> 糖方科技提供苹果软件签名 + +![candyrectangle](https://github.com/LYOfficial/OneDocs/blob/f4bbdea5317f82ac7f734599fc54346ea4b85e1b/candyrectangle.png) + +## Star 里程图 + +[![Star History Chart](https://api.star-history.com/svg?repos=LYOfficial/OneDocs&type=date&legend=top-left)](https://www.star-history.com/#LYOfficial/OneDocs&type=date&legend=top-left) diff --git a/README_EN.md b/README_EN.md new file mode 100644 index 00000000..31fa0ea7 --- /dev/null +++ b/README_EN.md @@ -0,0 +1,262 @@ +![OneDocs](https://socialify.git.ci/LYOfficial/OneDocs/image?description=1&font=KoHo&forks=1&issues=1&language=1&logo=https%3A%2F%2Foss.1n.hk%2Flyofficial%2Fimages%2Ficon.png&name=1&owner=1&pattern=Plus&pulls=1&stargazers=1&theme=Auto) + +

+ 中文简体 | English +

+ +

+milestone1 OneDocs - A single text, all is known. | Product Hunt +

+ + +# OneDocs + +> A Single Text, All is Known. +> +> OneDocs, an intelligent document analysis tool. + +If you find this project useful, please **give it a star** in the top right corner. Your support is greatly appreciated. + +[![Tauri](https://img.shields.io/badge/Tauri-v2-FFC131?style=for-the-badge&logo=tauri&logoColor=white&labelColor=24C8DB)](https://tauri.app/) ![React Badge](https://img.shields.io/badge/React-61DAFB?style=for-the-badge&logo=react&logoColor=black) ![TypeScript Badge](https://img.shields.io/badge/TypeScript-3178C6?style=for-the-badge&logo=typescript&logoColor=white) ![Markdown Badge](https://img.shields.io/badge/Markdown-000000?logo=markdown&logoColor=white&style=for-the-badge) ![LaTeX Badge](https://img.shields.io/badge/LaTeX-008080?logo=latex&logoColor=white&style=for-the-badge) + +**Thousands of articles, known at a glance. A tool of wisdom to help you analyze and understand.** + +OneDocs gathers the power of intelligent prompts to help you quickly grasp the essence of documents. Whether it's news summaries, data analysis, or academic key points, everything becomes clear with just one click. + +## Introduction + +In today's data era, we need to read and analyze various documents in every industry: daily news reports, data spreadsheets in work scenarios, courseware documents in study life... These documents contain **both essence and dross**. Reading and screening manually **takes up a lot of time**. Is there a tool that can **filter out useless content** and **organize and analyze the essence** like a top student's notes for our reading and learning? + +So I created this AI tool, **OneDocs**, and gave it a nice Chinese name "Yi Wen Yi Wen" (一文亦闻). Combining file analysis, large model applications, and output format standardization, it deconstructs and analyzes files uploaded by users, removes useless content, and organizes them into knowledge handbooks. I hope users can understand the essence of things, learn, and progress through simple documents with the power of large models. + +Currently, the software supports **PDF** document format, supports **40+ models**, and supports **Windows/macOS/Linux cross-platform** use, basically meeting the needs of various users. Moreover, the software is for local download and use, with no file uploads, so it will not cause file and API Key leaks. + +The software has four main functions: + +- **News Overview** - Summary of news highlights +- **Data Analysis** - Analysis of data content +- **STEM Quick Know** - Organization of STEM courseware +- **Liberal Arts Richness** - Organization of Liberal Arts courseware + +The analyzed results can be previewed, Markdown source code copied, and exported for download. It also supports analyzing multiple files simultaneously for merged downloads. + +## Screenshots + +OneDocsOneDocs +OneDocsOneDocs +OneDocsOneDocs + + +## Usage + +**1 Download the Release** + +Find the latest version on the project release page: https://github.com/LYOfficial/OneDocs/releases/latest + +Find the software suitable for your system and download it. + +**2 Launch** + +After launching the software, click "Start with One Text" (始于一文) to enter the function page. + +**3 Fill in API KEY** + +Click the gear icon "Settings" in the upper right corner, fill in the API Base URL and OpenAI API Key. The default API Base URL is the official OpenAI address. If you have a third-party API service address, please replace it. + +After filling in, select the appropriate model for subsequent analysis operations. + +> Currently, only some models are supported. More model interfaces will be provided in the future. + +**4 Upload and Analyze** + +Click the "Click to Select Document" box to upload documents, and select the appropriate function on the left according to your analysis needs. + +After uploading and selecting the function, click the "Start Analysis" (开始析文) button to analyze. + +**5 Post-processing** + +The software comes with Markdown and LaTeX format rendering. If there is a rendering error, you can copy the Markdown format text completely to an external viewer for viewing, or click the "Export" button to export a PDF file. + +For more detailed instructions, please read the Wiki: [OneDocs Wiki](https://github.com/LYOfficial/OneDocs/wiki) + +## Development + +To participate in the development and deployment of this project, please clone this repository first: + +```bash +git clone https://github.com/LYOfficial/OneDocs.git +``` + +### Requirements + +- Node.js (LTS recommended) + npm +- Rust + Tauri CLI (required for desktop builds) +- macOS users can install Rust here: https://rust-lang.org/tools/install + +```bash +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +``` + +### Tauri Version Notes (Important) + +In some mirrored registry environments (such as `ustc`), available Tauri Rust crate versions may lag behind the latest npm packages. +To avoid `tauri-runtime` / `tauri-runtime-wry` conflicts, keep the currently validated version set and use **exact versions** (do not use `^`): + +- npm: + - `@tauri-apps/api = 2.6.0` + - `@tauri-apps/cli = 2.6.0` + - `@tauri-apps/plugin-dialog = 2.3.0` + - `@tauri-apps/plugin-fs = 2.4.0` + - `@tauri-apps/plugin-shell = 2.3.0` +- Rust (`src-tauri/Cargo.toml`): + - `tauri = =2.6.0` + - `tauri-build = =2.6.0` + - `tauri-runtime = =2.9.2` + - `tauri-plugin-dialog = =2.3.0` + - `tauri-plugin-fs = =2.4.0` + - `tauri-plugin-shell = =2.3.0` + +If you upgrade Tauri, keep npm and Rust sides on the same generation and re-verify with `npm run tauri:dev`. + +### Run and Build + +```bash +npm install + +# Web (Vite) +npm run dev + +# Desktop (Tauri) +npm run tauri:dev +``` + +Build releases: + +```bash +# Web +npm run build + +# Desktop (Tauri) +npm run tauri:build +``` + +### Project Structure (Core) + +- src/: React + TypeScript front end + - components/: reusable UI (upload, results, model selection, settings) + - pages/: page entry points (Landing / Analysis / AnalysisResult / Settings) + - hooks/: business hooks (analysis workflow) + - services/: API + Tauri wrappers + - store/: global state (Zustand + persist) + - utils/: document parsing, Markdown/LaTeX rendering + - config/: providers, prompt configs, function metadata + - i18n/: localization resources + - styles/: global and component styles +- src-tauri/: Tauri desktop shell (Rust) + - src/main.rs: Tauri entry + command registration + - src/lib.rs: request forwarding + API calls +- docs/: static site assets +- dist/: build output (generated) + +### Data Flow + +1. User picks files and a function (FunctionSelector / FileUpload) +2. DocumentProcessor extracts plain text +3. useAnalysis builds prompts and calls APIService +4. APIService invokes Rust through Tauri +5. Rust uses reqwest to call the model API and returns the result +6. Zustand stores results, ResultDisplay renders/export/copies + +### Functions and Prompts + +Prompt configs live in config/prompts/index.ts and map to four modes: + +- News Overview (news) +- Data Analysis (data) +- STEM Quick Know (science, optional format review) +- Liberal Arts Richness (liberal) + +### Models and Providers (Built-in) + +Provider configuration is in config/providers.ts and supports hosted APIs and local servers: + +- OneDocs: Qwen2.5-7B, Qwen3-8B, GLM-4-9B, GLM-Z1-9B 0414, GLM-4.1V-9B Thinking, GLM-4-9B 0414, DeepSeek-R1 Qwen3-8B, GLM-4-Flash +- OpenAI: gpt-4o, gpt-4o-mini, gpt-4, gpt-3.5-turbo +- Anthropic: Claude 3.5 Sonnet/Haiku, Claude 3 Opus/Sonnet +- Google Gemini: gemini-3-pro, 2.5-pro, 2.5-flash, 1.5-pro, 1.5-flash +- Moonshot: moonshot-v1-8k/32k/128k +- Zhipu GLM: glm-4-flash/flashx/plus/0520/long/4v-plus +- DeepSeek: deepseek-chat, deepseek-reasoner +- Ollama (local): llama3.2, qwen2.5, gemma2, mistral +- LM Studio (local): local-model +- Compatible platforms: CompShare, 302.AI, TokenPony, SiliconFlow, Xinghe, PPIO, ModelScope, OneAPI + +> You can also add a custom provider and custom model name in Settings. + +### Tech Stack + +| Layer | Technology | +|-------|-----------| +| Frontend | React 19 + TypeScript | +| Desktop | Tauri v2 (Rust) | +| Build Tool | Vite 7 | +| State Management | Zustand 5 | +| PDF Parsing | pdfjs-dist v5 (frontend text extraction) + lopdf (Rust image extraction) | +| Rendering | marked (Markdown) + KaTeX (LaTeX) | +| i18n | i18next + react-i18next | + +### Document Parsing and Limits + +- Supported: PDF +- Max file size: 30 MB (FILE_SIZE_LIMIT) +- Parsing in utils/documentProcessor.ts (pdfjs), image extraction in Rust (lopdf) + +## Contributing + +We warmly welcome contributions from every developer! + +- Before starting, make sure you've read the [Development](#development) section for requirements and version notes. +- Feel free to share your ideas and improvements via [Pull Request](https://github.com/LYOfficial/OneDocs/pulls) or [GitHub Issues](https://github.com/LYOfficial/OneDocs/issues). +- When submitting a PR, please include: a summary of changes, verification steps, and screenshots or recordings for UI changes. +- Keep commit messages concise; you may use prefixes like `feat:` / `fix:` / `docs:`. + +## License + +Copyright © 2024-2026 LYOfficial. + +This project is licensed under the [GNU General Public License v3.0](https://github.com/LYOfficial/OneDocs/blob/main/LICENSE). + +## Acknowledgments + +We sincerely thank the following open-source projects — OneDocs would not be possible without them: + +- [Tauri](https://github.com/tauri-apps/tauri) — Cross-platform desktop application framework +- [lopdf](https://github.com/J-F-Liu/lopdf) — Rust PDF processing library for image extraction +- [pdfjs-dist](https://github.com/nicbarker/pdfjs-dist) — PDF text parsing +- [React](https://github.com/facebook/react) — UI building library +- [marked](https://github.com/markedjs/marked) — Markdown parser +- [KaTeX](https://github.com/KaTeX/KaTeX) — LaTeX math formula rendering +- [Zustand](https://github.com/pmndrs/zustand) — Lightweight state management +- [i18next](https://github.com/i18next/i18next) — Internationalization framework + +## Contact + +- Email: coldregion@qq.com +- GitHub Issues: [Submit an issue](https://github.com/LYOfficial/OneDocs/issues) +- Wiki: [OneDocs Wiki](https://github.com/LYOfficial/OneDocs/wiki) + +### Environment Variables (Optional) + +To provide managed OneDocs credentials, set: + +- VITE_ONEDOCS_API_URL +- VITE_ONEDOCS_API_KEY + +## Authors + +- [@LYOfficial ](https://github.com/LYOfficial/) Lead Developer, Project Manager. +- [@JHL-HK](https://github.com/JHL-HK) Partial Refactoring, Image Hosting Provider. + +## Star History + +[![Star History Chart](https://api.star-history.com/svg?repos=LYOfficial/OneDocs&type=date&legend=top-left)](https://www.star-history.com/#LYOfficial/OneDocs&type=date&legend=top-left) diff --git a/candyrectangle.png b/candyrectangle.png new file mode 100644 index 00000000..80ad20b4 Binary files /dev/null and b/candyrectangle.png differ diff --git a/docs/CNAME b/docs/CNAME index 811e1489..176ae51f 100644 --- a/docs/CNAME +++ b/docs/CNAME @@ -1 +1 @@ -www.onedocs.cn \ No newline at end of file +onedocs.ijune.cn \ No newline at end of file diff --git a/docs/index.html b/docs/index.html index e6fefc8a..59bc1c73 100644 --- a/docs/index.html +++ b/docs/index.html @@ -7,21 +7,21 @@ - + > - + - + - + - +
- +
@@ -81,7 +81,7 @@

文章千卷,一览而知

- +

核心功能

@@ -123,7 +123,7 @@

文采丰呈

- +

支持格式

@@ -134,38 +134,35 @@

支持格式

PDF
-
- - Word -
-
- - PPT -
-
- - TXT -
- 支持最大 50MB 文件大小 + 支持最大 30MB 文件大小
- +

立即下载

选择适合您操作系统的版本

- +
如果下载被浏览器拦截,请点击"保留"继续下载
+ + +
+ +
@@ -174,13 +171,18 @@

立即下载

Windows

适用于 Windows 10 及以上版本

- +
+ + +
- 版本: 1.1.0 - 大小: 12.2 MB + 版本: 1.4.0 + 大小: ~4 MB
@@ -190,13 +192,13 @@

Windows

macOS

适用于 macOS 10.15 及以上版本

-
- 版本: 1.1.0 - 大小: 5.59 MB + 版本: 1.4.0 + 大小: ~6 MB
@@ -206,13 +208,18 @@

macOS

Linux

适用于主流 Linux 发行版

- +
+ + +
- 版本: 1.0.0 - 即将发布 + 版本: 1.4.0 + 大小: ~80 MB
@@ -226,7 +233,7 @@

Linux

- +

关于 OneDocs

@@ -267,7 +274,7 @@

多模型支持

-
0
+
0
MB 最大文件
@@ -275,7 +282,7 @@

多模型支持

种分析模式
-
0
+
0
+ AI 模型
@@ -283,7 +290,50 @@

多模型支持

- + +
+ +
+ +
- + - + \ No newline at end of file diff --git a/docs/script.js b/docs/script.js index abb774bf..cc6b2f2b 100644 --- a/docs/script.js +++ b/docs/script.js @@ -1,29 +1,22 @@ -// 导航栏功能 document.addEventListener('DOMContentLoaded', function() { const navToggle = document.querySelector('.nav-toggle'); const navMenu = document.querySelector('.nav-menu'); const navLinks = document.querySelectorAll('.nav-link'); const backToTop = document.getElementById('backToTop'); - // 移动端菜单切换 navToggle.addEventListener('click', function() { navMenu.classList.toggle('active'); navToggle.classList.toggle('active'); }); - // 导航链接点击事件 navLinks.forEach(link => { link.addEventListener('click', function(e) { - // 移除所有活动状态 navLinks.forEach(l => l.classList.remove('active')); - // 添加当前活动状态 this.classList.add('active'); - // 移动端关闭菜单 navMenu.classList.remove('active'); navToggle.classList.remove('active'); - // 平滑滚动到目标区域 const targetId = this.getAttribute('href'); if (targetId.startsWith('#')) { e.preventDefault(); @@ -39,11 +32,9 @@ document.addEventListener('DOMContentLoaded', function() { }); }); - // 滚动事件处理 window.addEventListener('scroll', function() { const scrollTop = window.pageYOffset || document.documentElement.scrollTop; - // 导航栏透明度控制 const navbar = document.querySelector('.navbar'); if (scrollTop > 100) { navbar.style.background = 'rgba(255, 255, 255, 0.98)'; @@ -53,18 +44,15 @@ document.addEventListener('DOMContentLoaded', function() { navbar.style.boxShadow = 'none'; } - // 返回顶部按钮显示/隐藏 if (scrollTop > 500) { backToTop.classList.add('visible'); } else { backToTop.classList.remove('visible'); } - // 活动导航项高亮 updateActiveNavLink(); }); - // 返回顶部功能 backToTop.addEventListener('click', function() { window.scrollTo({ top: 0, @@ -72,7 +60,6 @@ document.addEventListener('DOMContentLoaded', function() { }); }); - // 更新活动导航链接 function updateActiveNavLink() { const sections = ['home', 'features', 'download', 'about']; const scrollTop = window.pageYOffset || document.documentElement.scrollTop; @@ -91,7 +78,6 @@ document.addEventListener('DOMContentLoaded', function() { } }); - // 更新导航链接活动状态 navLinks.forEach(link => { link.classList.remove('active'); if (link.getAttribute('href') === `#${currentSection}`) { @@ -100,7 +86,6 @@ document.addEventListener('DOMContentLoaded', function() { }); } - // 数字动画 function animateNumbers() { const statNumbers = document.querySelectorAll('.stat-number'); @@ -121,7 +106,6 @@ document.addEventListener('DOMContentLoaded', function() { }); } - // 交叉观察器用于触发动画 const observerOptions = { threshold: 0.1, rootMargin: '0px 0px -50px 0px' @@ -132,7 +116,6 @@ document.addEventListener('DOMContentLoaded', function() { if (entry.isIntersecting) { entry.target.classList.add('animate'); - // 如果是统计数字区域,启动数字动画 if (entry.target.classList.contains('about-stats')) { animateNumbers(); } @@ -140,11 +123,9 @@ document.addEventListener('DOMContentLoaded', function() { }); }, observerOptions); - // 观察需要动画的元素 const animateElements = document.querySelectorAll('.feature-card, .download-card, .about-stats'); animateElements.forEach(el => observer.observe(el)); - // 功能卡片交互效果 const featureCards = document.querySelectorAll('.feature-card'); featureCards.forEach(card => { card.addEventListener('mouseenter', function() { @@ -156,7 +137,6 @@ document.addEventListener('DOMContentLoaded', function() { }); }); - // 格式项目悬停效果 const formatItems = document.querySelectorAll('.format-item'); formatItems.forEach(item => { item.addEventListener('mouseenter', function() { @@ -173,43 +153,59 @@ document.addEventListener('DOMContentLoaded', function() { }); }); -// 下载功能 -function downloadApp(platform) { - let downloadUrl = ''; +function downloadApp(platform, type) { + const useProxy = document.getElementById('useProxy').checked; + const baseUrl = 'https://github.com/LYOfficial/OneDocs/releases/download/v1.4.0/'; + const proxyUrl = 'https://gh-proxy.com/'; + + let filename = ''; let platformName = ''; switch(platform) { case 'windows': - downloadUrl = 'https://gh-proxy.com/https://github.com/LYOfficial/OneDocs/releases/download/v1.1.0/OneDocs-1.1.0.exe'; platformName = 'Windows'; + if (type === 'msi') { + filename = 'onedocs_1.4.0_x64_en-US.msi'; + } else { + filename = 'onedocs_1.4.0_x64-setup.exe'; + } break; case 'macos': - downloadUrl = 'https://gh-proxy.com/https://github.com/LYOfficial/OneDocs/releases/download/v1.1.0/OneDocs-macos-1.1.0.zip'; platformName = 'macOS'; + filename = 'onedocs_1.4.0_aarch64.dmg'; + break; + case 'linux': + platformName = 'Linux'; + if (type === 'deb') { + filename = 'onedocs_1.4.0_amd64.deb'; + } else { + filename = 'onedocs_1.4.0_amd64.AppImage'; + } break; default: showToast('该平台版本暂未发布', 'warning'); return; } - // 显示下载提示 - showToast(`正在下载 ${platformName} 版本...如果被浏览器拦截,请点击"保留"`, 'info', 4000); + let downloadUrl = baseUrl + filename; + if (useProxy) { + downloadUrl = proxyUrl + downloadUrl; + } + + showToast(`正在下载 ${platformName} ${type ? type : ''} 版本...如果被浏览器拦截,请点击"保留"`, 'info', 4000); - // 创建隐藏的下载链接 const link = document.createElement('a'); link.href = downloadUrl; link.download = ''; link.style.display = 'none'; document.body.appendChild(link); - // 触发下载 setTimeout(() => { link.click(); document.body.removeChild(link); }, 500); } -// 使用说明功能 function openWiki() { showToast('正在跳转到使用说明...'); setTimeout(() => { @@ -217,7 +213,6 @@ function openWiki() { }, 500); } -// 滚动到下载区域 function scrollToDownload() { const downloadSection = document.getElementById('download'); const offsetTop = downloadSection.offsetTop - 70; @@ -227,29 +222,23 @@ function scrollToDownload() { }); } -// 显示提示信息 function showToast(message, type = 'info', duration = 3000) { - // 移除现有的 toast const existingToast = document.querySelector('.toast'); if (existingToast) { existingToast.remove(); } - // 创建新的 toast const toast = document.createElement('div'); toast.className = `toast toast-${type}`; toast.textContent = message; - // 添加到页面 document.body.appendChild(toast); - // 显示动画 setTimeout(() => { toast.style.opacity = '1'; toast.style.transform = 'translateX(-50%) translateY(0)'; }, 100); - // 自动隐藏 setTimeout(() => { toast.style.opacity = '0'; toast.style.transform = 'translateX(-50%) translateY(-30px)'; @@ -261,14 +250,12 @@ function showToast(message, type = 'info', duration = 3000) { }, duration); } -// 复制文本到剪贴板 function copyToClipboard(text) { if (navigator.clipboard) { navigator.clipboard.writeText(text).then(() => { showToast('已复制到剪贴板', 'success'); }); } else { - // 降级方案 const textArea = document.createElement('textarea'); textArea.value = text; document.body.appendChild(textArea); @@ -283,24 +270,18 @@ function copyToClipboard(text) { } } -// 页面加载完成后的初始化 window.addEventListener('load', function() { - // 移除加载状态 document.body.classList.remove('loading'); - // 添加入场动画 const heroContent = document.querySelector('.hero-content'); if (heroContent) { heroContent.classList.add('animate'); } - // 预加载关键资源 preloadResources(); }); -// 预加载资源 function preloadResources() { - // 预加载字体 const fonts = [ 'https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@300;400;500;600;700&display=swap' ]; @@ -314,22 +295,16 @@ function preloadResources() { }); } -// 错误处理 window.addEventListener('error', function(e) { console.error('页面发生错误:', e.error); - // 可以在这里添加错误报告逻辑 }); -// 键盘快捷键 document.addEventListener('keydown', function(e) { - // Ctrl/Cmd + K 快速搜索 if ((e.ctrlKey || e.metaKey) && e.key === 'k') { e.preventDefault(); - // 这里可以添加搜索功能 showToast('搜索功能即将上线'); } - // ESC 关闭菜单 if (e.key === 'Escape') { const navMenu = document.querySelector('.nav-menu'); const navToggle = document.querySelector('.nav-toggle'); @@ -340,7 +315,6 @@ document.addEventListener('keydown', function(e) { } }); -// 性能监控 if ('performance' in window) { window.addEventListener('load', function() { setTimeout(() => { @@ -348,7 +322,6 @@ if ('performance' in window) { const loadTime = perfData.loadEventEnd - perfData.navigationStart; console.log(`页面加载时间: ${loadTime}ms`); - // 如果加载时间过长,显示提示 if (loadTime > 3000) { console.warn('页面加载时间较长,建议优化'); } @@ -356,7 +329,6 @@ if ('performance' in window) { }); } -// 主题切换功能(预留) function toggleTheme() { const body = document.body; const currentTheme = body.getAttribute('data-theme'); @@ -368,7 +340,6 @@ function toggleTheme() { showToast(`已切换到${newTheme === 'dark' ? '深色' : '浅色'}模式`); } -// 恢复主题设置 function restoreTheme() { const savedTheme = localStorage.getItem('theme'); if (savedTheme) { @@ -376,22 +347,18 @@ function restoreTheme() { } } -// 页面可见性变化处理 document.addEventListener('visibilitychange', function() { if (document.hidden) { - // 页面隐藏时暂停动画 document.querySelectorAll('.feature-icon').forEach(icon => { icon.style.animationPlayState = 'paused'; }); } else { - // 页面显示时恢复动画 document.querySelectorAll('.feature-icon').forEach(icon => { icon.style.animationPlayState = 'running'; }); } }); -// 响应式图片懒加载 function lazyLoadImages() { const images = document.querySelectorAll('img[data-src]'); @@ -409,5 +376,4 @@ function lazyLoadImages() { images.forEach(img => imageObserver.observe(img)); } -// 初始化懒加载 -document.addEventListener('DOMContentLoaded', lazyLoadImages); \ No newline at end of file +document.addEventListener('DOMContentLoaded', lazyLoadImages); diff --git a/docs/styles.css b/docs/styles.css index f4d711d9..d4bef784 100644 --- a/docs/styles.css +++ b/docs/styles.css @@ -1,4 +1,4 @@ -/* 基础样式重置 */ + * { margin: 0; padding: 0; @@ -39,7 +39,7 @@ body { overflow-x: hidden; } -/* 导航栏样式 */ + .navbar { position: fixed; top: 0; @@ -133,12 +133,12 @@ body { transition: var(--transition); } -/* 主容器 */ + .main-container { margin-top: 70px; } -/* 头部英雄区域 */ + .header { min-height: 90vh; display: flex; @@ -281,7 +281,7 @@ body { transform: translateX(3px); } -/* 功能区域 */ + .features-section { padding: 5rem 0; background: var(--background-color); @@ -423,7 +423,7 @@ body { font-weight: bold; } -/* 支持格式区域 */ + .formats-section { padding: 4rem 0; background: var(--surface-color); @@ -479,7 +479,7 @@ body { color: var(--accent-color); } -/* 下载区域 */ + .download-section { padding: 5rem 0; background: var(--background-color); @@ -487,7 +487,7 @@ body { -/* 下载提示 */ + .download-notice { background: linear-gradient(135deg, rgba(59, 130, 246, 0.1) 0%, rgba(37, 99, 235, 0.1) 100%); border: 1px solid rgba(59, 130, 246, 0.2); @@ -608,7 +608,7 @@ body { color: var(--accent-color); } -/* 关于区域 */ + .about-section { padding: 5rem 0; background: var(--surface-color); @@ -687,7 +687,48 @@ body { letter-spacing: 0.05em; } -/* 页脚 */ +.acknowledgments-section { + padding: 4rem 0; + background: var(--surface-color); +} + +.ack-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1.5rem; + margin-top: 2rem; +} + +.ack-card { + display: block; + background: var(--background-color); + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + padding: 1.5rem; + text-decoration: none; + color: var(--primary-color); + transition: var(--transition); +} + +.ack-card:hover { + transform: translateY(-4px); + box-shadow: var(--shadow-medium); + border-color: var(--accent-color); +} + +.ack-card h4 { + font-size: 1.1rem; + margin-bottom: 0.4rem; + color: var(--accent-color); +} + +.ack-card p { + font-size: 0.9rem; + color: var(--secondary-color); + margin: 0; +} + + .footer { background: var(--primary-color); color: white; @@ -771,7 +812,7 @@ body { color: white; } -/* 返回顶部按钮 */ + .back-to-top { position: fixed; bottom: 2rem; @@ -799,7 +840,7 @@ body { box-shadow: var(--shadow-large); } -/* 动画 */ + @keyframes fadeInUp { from { opacity: 0; @@ -842,7 +883,7 @@ body { } } -/* 响应式设计 */ + @media (max-width: 768px) { .nav-menu { position: fixed; @@ -992,7 +1033,7 @@ body { } } -/* 滚动条样式 */ + ::-webkit-scrollbar { width: 8px; } @@ -1010,26 +1051,26 @@ body { background: var(--accent-hover); } -/* 选择文本样式 */ + ::selection { background: rgba(59, 130, 246, 0.2); color: var(--primary-color); } -/* 焦点样式 */ + button:focus, a:focus { outline: 2px solid var(--accent-color); outline-offset: 2px; } -/* 加载动画 */ + .loading { opacity: 0.6; pointer-events: none; } -/* 工具提示 */ + .tooltip { position: relative; } @@ -1055,4 +1096,78 @@ a:focus { .tooltip:hover::before { opacity: 1; visibility: visible; -} \ No newline at end of file +} + +.download-options { + display: flex; + justify-content: center; + margin-bottom: 2rem; +} + +.source-toggle { + display: flex; + align-items: center; + gap: 1rem; + cursor: pointer; + user-select: none; +} + +.source-toggle input { + display: none; +} + +.source-toggle .slider { + position: relative; + width: 50px; + height: 26px; + background-color: var(--secondary-color); + border-radius: 20px; + transition: var(--transition); +} + +.source-toggle .slider::before { + content: ''; + position: absolute; + height: 20px; + width: 20px; + left: 3px; + bottom: 3px; + background-color: white; + border-radius: 50%; + transition: var(--transition); +} + +.source-toggle input:checked + .slider { + background-color: var(--accent-color); +} + +.source-toggle input:checked + .slider::before { + transform: translateX(24px); +} + +.source-toggle .label-text { + font-size: 1rem; + color: var(--primary-color); +} + + +.button-group { + display: flex; + flex-direction: column; + gap: 0.5rem; + width: 100%; + align-items: center; +} + +.download-btn.secondary { + background: transparent; + border: 1px solid var(--accent-color); + color: var(--accent-color); + box-shadow: none; +} + +.download-btn.secondary:hover { + background: rgba(59, 130, 246, 0.1); + transform: translateY(-2px); +} + diff --git a/index.html b/index.html index 1150a7ff..a93358b2 100644 --- a/index.html +++ b/index.html @@ -3,19 +3,45 @@ + OneDocs - 一文亦闻 + + - + - + =6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.3", - "resolved": "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", - "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.28.3" + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -164,9 +166,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", "dev": true, "license": "MIT", "engines": { @@ -175,7 +177,7 @@ }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", - "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true, "license": "MIT", @@ -184,9 +186,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "dev": true, "license": "MIT", "engines": { @@ -195,7 +197,7 @@ }, "node_modules/@babel/helper-validator-option": { "version": "7.27.1", - "resolved": "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", "dev": true, "license": "MIT", @@ -204,27 +206,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.28.4", - "resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.28.4.tgz", - "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4" + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.28.4", - "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.28.4.tgz", - "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.4" + "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -235,7 +237,7 @@ }, "node_modules/@babel/plugin-transform-react-jsx-self": { "version": "7.27.1", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", "dev": true, "license": "MIT", @@ -251,7 +253,7 @@ }, "node_modules/@babel/plugin-transform-react-jsx-source": { "version": "7.27.1", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", "dev": true, "license": "MIT", @@ -265,34 +267,43 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.28.4", - "resolved": "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.28.4.tgz", - "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.3", + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.4", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", "debug": "^4.3.1" }, "engines": { @@ -300,23 +311,23 @@ } }, "node_modules/@babel/types": { - "version": "7.28.4", - "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.28.4.tgz", - "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", "cpu": [ "ppc64" ], @@ -327,13 +338,13 @@ "aix" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", "cpu": [ "arm" ], @@ -344,13 +355,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", "cpu": [ "arm64" ], @@ -361,13 +372,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", "cpu": [ "x64" ], @@ -378,13 +389,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", "cpu": [ "arm64" ], @@ -395,13 +406,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", "cpu": [ "x64" ], @@ -412,13 +423,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", "cpu": [ "arm64" ], @@ -429,13 +440,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", "cpu": [ "x64" ], @@ -446,13 +457,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", "cpu": [ "arm" ], @@ -463,13 +474,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", "cpu": [ "arm64" ], @@ -480,13 +491,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", "cpu": [ "ia32" ], @@ -497,13 +508,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", "cpu": [ "loong64" ], @@ -514,13 +525,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", "cpu": [ "mips64el" ], @@ -531,13 +542,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", "cpu": [ "ppc64" ], @@ -548,13 +559,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", "cpu": [ "riscv64" ], @@ -565,13 +576,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", "cpu": [ "s390x" ], @@ -582,13 +593,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", "cpu": [ "x64" ], @@ -599,13 +610,30 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", "cpu": [ "x64" ], @@ -616,13 +644,30 @@ "netbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", "cpu": [ "x64" ], @@ -633,13 +678,30 @@ "openbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", "cpu": [ "x64" ], @@ -650,13 +712,13 @@ "sunos" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", "cpu": [ "arm64" ], @@ -667,13 +729,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", "cpu": [ "ia32" ], @@ -684,13 +746,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", "cpu": [ "x64" ], @@ -701,12 +763,12 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", - "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, "license": "MIT", @@ -717,7 +779,7 @@ }, "node_modules/@jridgewell/remapping": { "version": "2.3.5", - "resolved": "https://registry.npmmirror.com/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", "dev": true, "license": "MIT", @@ -728,7 +790,7 @@ }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", - "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, "license": "MIT", @@ -738,14 +800,14 @@ }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", - "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", - "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, "license": "MIT", @@ -754,10 +816,16 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@lobehub/icons-static-png": { + "version": "1.88.0", + "resolved": "https://registry.npmjs.org/@lobehub/icons-static-png/-/icons-static-png-1.88.0.tgz", + "integrity": "sha512-fg5KHOMFjKZ9pneZNZi0XHyDin/wESDK7Wb5QFB1i76GshT3Fc4vTC0f5FHk5qxaQ2WwgFA2/hUBQIKIu7svPg==", + "license": "MIT" + }, "node_modules/@napi-rs/canvas": { - "version": "0.1.80", - "resolved": "https://registry.npmmirror.com/@napi-rs/canvas/-/canvas-0.1.80.tgz", - "integrity": "sha512-DxuT1ClnIPts1kQx8FBmkk4BQDTfI5kIzywAaMjQSXfNnra5UFU9PwurXrl+Je3bJ6BGsp/zmshVVFbCmyI+ww==", + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.100.tgz", + "integrity": "sha512-xglYA6q3XO5P3BNJYxVZ1IV7DLVjp1Py6nwag88YntrS+3vKHyYcMqXVS4ZztJmwz2uGvz1FWhI/4LgbR5uQDA==", "license": "MIT", "optional": true, "workspaces": [ @@ -766,23 +834,28 @@ "engines": { "node": ">= 10" }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, "optionalDependencies": { - "@napi-rs/canvas-android-arm64": "0.1.80", - "@napi-rs/canvas-darwin-arm64": "0.1.80", - "@napi-rs/canvas-darwin-x64": "0.1.80", - "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.80", - "@napi-rs/canvas-linux-arm64-gnu": "0.1.80", - "@napi-rs/canvas-linux-arm64-musl": "0.1.80", - "@napi-rs/canvas-linux-riscv64-gnu": "0.1.80", - "@napi-rs/canvas-linux-x64-gnu": "0.1.80", - "@napi-rs/canvas-linux-x64-musl": "0.1.80", - "@napi-rs/canvas-win32-x64-msvc": "0.1.80" + "@napi-rs/canvas-android-arm64": "0.1.100", + "@napi-rs/canvas-darwin-arm64": "0.1.100", + "@napi-rs/canvas-darwin-x64": "0.1.100", + "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.100", + "@napi-rs/canvas-linux-arm64-gnu": "0.1.100", + "@napi-rs/canvas-linux-arm64-musl": "0.1.100", + "@napi-rs/canvas-linux-riscv64-gnu": "0.1.100", + "@napi-rs/canvas-linux-x64-gnu": "0.1.100", + "@napi-rs/canvas-linux-x64-musl": "0.1.100", + "@napi-rs/canvas-win32-arm64-msvc": "0.1.100", + "@napi-rs/canvas-win32-x64-msvc": "0.1.100" } }, "node_modules/@napi-rs/canvas-android-arm64": { - "version": "0.1.80", - "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.80.tgz", - "integrity": "sha512-sk7xhN/MoXeuExlggf91pNziBxLPVUqF2CAVnB57KLG/pz7+U5TKG8eXdc3pm0d7Od0WreB6ZKLj37sX9muGOQ==", + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.100.tgz", + "integrity": "sha512-hjhCKhntPv9+t4ckHymdx0phYNcVW+GKQR6Lzw2zE+pOVjOplSmtx9nNNknTjbEDLcuLZqA1y8ufKg1XfgftzQ==", "cpu": [ "arm64" ], @@ -793,12 +866,16 @@ ], "engines": { "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" } }, "node_modules/@napi-rs/canvas-darwin-arm64": { - "version": "0.1.80", - "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.80.tgz", - "integrity": "sha512-O64APRTXRUiAz0P8gErkfEr3lipLJgM6pjATwavZ22ebhjYl/SUbpgM0xcWPQBNMP1n29afAC/Us5PX1vg+JNQ==", + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.100.tgz", + "integrity": "sha512-2PcswRaC7Ly645DGt88///zuFDhJxJYdKAs1uU3mfk1atYkXufgcgLfBpk6Tm12nCQBaNt1wpybuPZ4qOhTo8A==", "cpu": [ "arm64" ], @@ -809,12 +886,16 @@ ], "engines": { "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" } }, "node_modules/@napi-rs/canvas-darwin-x64": { - "version": "0.1.80", - "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.80.tgz", - "integrity": "sha512-FqqSU7qFce0Cp3pwnTjVkKjjOtxMqRe6lmINxpIZYaZNnVI0H5FtsaraZJ36SiTHNjZlUB69/HhxNDT1Aaa9vA==", + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.100.tgz", + "integrity": "sha512-ePNZtj7pNIva/siZMg+HmbeozkIjqUIYdoymH8HaA3qK7LfzFN4WMBM8G6HQ9ZC+H3+Dnn5pqtiXpgLykaPOhw==", "cpu": [ "x64" ], @@ -825,12 +906,16 @@ ], "engines": { "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" } }, "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { - "version": "0.1.80", - "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.80.tgz", - "integrity": "sha512-eyWz0ddBDQc7/JbAtY4OtZ5SpK8tR4JsCYEZjCE3dI8pqoWUC8oMwYSBGCYfsx2w47cQgQCgMVRVTFiiO38hHQ==", + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.100.tgz", + "integrity": "sha512-d5cDB48oWFGU8/XPhUOFAlySgb/VAu7D+s8fi55K1Pcfg8aPplHWqMgibhVLU8ky7Pyg/fuiVLz4Nf3JrSTuUA==", "cpu": [ "arm" ], @@ -841,12 +926,16 @@ ], "engines": { "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" } }, "node_modules/@napi-rs/canvas-linux-arm64-gnu": { - "version": "0.1.80", - "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.80.tgz", - "integrity": "sha512-qwA63t8A86bnxhuA/GwOkK3jvb+XTQaTiVML0vAWoHyoZYTjNs7BzoOONDgTnNtr8/yHrq64XXzUoLqDzU+Uuw==", + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.100.tgz", + "integrity": "sha512-rDxgxRu69RvDlX/bh9o22DxLsGr8EqsNgotL9+RwQE1S0b0cqeatqsw6aW45mukm0B42DIAaAacKaYQ8cqS1nw==", "cpu": [ "arm64" ], @@ -857,12 +946,16 @@ ], "engines": { "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" } }, "node_modules/@napi-rs/canvas-linux-arm64-musl": { - "version": "0.1.80", - "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.80.tgz", - "integrity": "sha512-1XbCOz/ymhj24lFaIXtWnwv/6eFHXDrjP0jYkc6iHQ9q8oXKzUX1Lc6bu+wuGiLhGh2GS/2JlfORC5ZcXimRcg==", + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.100.tgz", + "integrity": "sha512-K3mDW66N+xT2/V439u1alFANiBUjdEx2gLiNYnCmUsva5jZMxWTjafBYwTzYK+EMFMHrUoabuU+T1BIP5CgbYQ==", "cpu": [ "arm64" ], @@ -873,12 +966,16 @@ ], "engines": { "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" } }, "node_modules/@napi-rs/canvas-linux-riscv64-gnu": { - "version": "0.1.80", - "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.80.tgz", - "integrity": "sha512-XTzR125w5ZMs0lJcxRlS1K3P5RaZ9RmUsPtd1uGt+EfDyYMu4c6SEROYsxyatbbu/2+lPe7MPHOO/0a0x7L/gw==", + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.100.tgz", + "integrity": "sha512-mooqUBTIsccZpnoQC4NgrC1v6C1vof39etLNMnBwCY+p0gajWJvAHLGQ6g/gGyS5YrpDW+GefSN4+Cvcr08UWw==", "cpu": [ "riscv64" ], @@ -889,12 +986,16 @@ ], "engines": { "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" } }, "node_modules/@napi-rs/canvas-linux-x64-gnu": { - "version": "0.1.80", - "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.80.tgz", - "integrity": "sha512-BeXAmhKg1kX3UCrJsYbdQd3hIMDH/K6HnP/pG2LuITaXhXBiNdh//TVVVVCBbJzVQaV5gK/4ZOCMrQW9mvuTqA==", + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.100.tgz", + "integrity": "sha512-1eCvkDCazm7FFhsT7DfGOdSaHgZVK3bt/dSBl5EWHOWmnz+I7j8tPseJqqD81NF+MH21jKUK4wQSDjN0mdhnTg==", "cpu": [ "x64" ], @@ -905,12 +1006,16 @@ ], "engines": { "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" } }, "node_modules/@napi-rs/canvas-linux-x64-musl": { - "version": "0.1.80", - "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.80.tgz", - "integrity": "sha512-x0XvZWdHbkgdgucJsRxprX/4o4sEed7qo9rCQA9ugiS9qE2QvP0RIiEugtZhfLH3cyI+jIRFJHV4Fuz+1BHHMg==", + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.100.tgz", + "integrity": "sha512-20arT6lnI19S68qNlii73TSEDbECNgzMz2EpldC1V3mZFuRkeujXkcebRk0LRJe9SEUAooYiLokfMViY8IX7yA==", "cpu": [ "x64" ], @@ -921,12 +1026,36 @@ ], "engines": { "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-win32-arm64-msvc": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-0.1.100.tgz", + "integrity": "sha512-DZFFT1wIAg37LJw37yhMRFfjATd3vTQzjZ1Yki8u2vhO6Hi5VE6BVaGQ1aaDu7xb4iMErz+9EOwjpS7xcxFeBw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" } }, "node_modules/@napi-rs/canvas-win32-x64-msvc": { - "version": "0.1.80", - "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.80.tgz", - "integrity": "sha512-Z8jPsM6df5V8B1HrCHB05+bDiCxjE9QA//3YrkKIdVDEwn5RKaqOxCJDRJkl48cJbylcrJbW4HxZbTte8juuPg==", + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.100.tgz", + "integrity": "sha512-MyT1j3mHC2+Lu4pBi9mKyMJhtP6U7k7EldY7sj/uS5gJA65gTXt8MefJQXLJo5d/vZbuWmfxzkEUNc/urV3pHA==", "cpu": [ "x64" ], @@ -937,19 +1066,23 @@ ], "engines": { "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" } }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.27", - "resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", "dev": true, "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.52.4", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.4.tgz", - "integrity": "sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz", + "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==", "cpu": [ "arm" ], @@ -961,9 +1094,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.52.4", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.4.tgz", - "integrity": "sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz", + "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==", "cpu": [ "arm64" ], @@ -975,9 +1108,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.52.4", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.4.tgz", - "integrity": "sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz", + "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==", "cpu": [ "arm64" ], @@ -989,9 +1122,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.52.4", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.4.tgz", - "integrity": "sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz", + "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==", "cpu": [ "x64" ], @@ -1003,9 +1136,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.52.4", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.4.tgz", - "integrity": "sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz", + "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==", "cpu": [ "arm64" ], @@ -1017,9 +1150,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.52.4", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.4.tgz", - "integrity": "sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz", + "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==", "cpu": [ "x64" ], @@ -1031,9 +1164,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.52.4", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.4.tgz", - "integrity": "sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz", + "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==", "cpu": [ "arm" ], @@ -1045,9 +1178,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.52.4", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.4.tgz", - "integrity": "sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz", + "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==", "cpu": [ "arm" ], @@ -1059,9 +1192,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.52.4", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.4.tgz", - "integrity": "sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz", + "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==", "cpu": [ "arm64" ], @@ -1073,9 +1206,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.52.4", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.4.tgz", - "integrity": "sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz", + "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==", "cpu": [ "arm64" ], @@ -1087,9 +1220,23 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.52.4", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.4.tgz", - "integrity": "sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz", + "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz", + "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==", "cpu": [ "loong64" ], @@ -1101,9 +1248,23 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.52.4", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.4.tgz", - "integrity": "sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz", + "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz", + "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==", "cpu": [ "ppc64" ], @@ -1115,9 +1276,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.52.4", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.4.tgz", - "integrity": "sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz", + "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==", "cpu": [ "riscv64" ], @@ -1129,9 +1290,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.52.4", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.4.tgz", - "integrity": "sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz", + "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==", "cpu": [ "riscv64" ], @@ -1143,9 +1304,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.52.4", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.4.tgz", - "integrity": "sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz", + "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==", "cpu": [ "s390x" ], @@ -1157,9 +1318,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.52.4", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.4.tgz", - "integrity": "sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz", + "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==", "cpu": [ "x64" ], @@ -1171,9 +1332,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.52.4", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.4.tgz", - "integrity": "sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz", + "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==", "cpu": [ "x64" ], @@ -1184,10 +1345,24 @@ "linux" ] }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz", + "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.52.4", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.4.tgz", - "integrity": "sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz", + "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==", "cpu": [ "arm64" ], @@ -1199,9 +1374,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.52.4", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.4.tgz", - "integrity": "sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz", + "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==", "cpu": [ "arm64" ], @@ -1213,9 +1388,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.52.4", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.4.tgz", - "integrity": "sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz", + "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==", "cpu": [ "ia32" ], @@ -1227,9 +1402,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.52.4", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.4.tgz", - "integrity": "sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz", + "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==", "cpu": [ "x64" ], @@ -1241,9 +1416,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.52.4", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.4.tgz", - "integrity": "sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz", + "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==", "cpu": [ "x64" ], @@ -1255,9 +1430,9 @@ ] }, "node_modules/@tauri-apps/api": { - "version": "2.8.0", - "resolved": "https://registry.npmmirror.com/@tauri-apps/api/-/api-2.8.0.tgz", - "integrity": "sha512-ga7zdhbS2GXOMTIZRT0mYjKJtR9fivsXzsyq5U3vjDL0s6DTMwYRm0UHNjzTY5dh4+LSC68Sm/7WEiimbQNYlw==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.6.0.tgz", + "integrity": "sha512-hRNcdercfgpzgFrMXWwNDBN0B7vNzOzRepy6ZAmhxi5mDLVPNrTpo9MGg2tN/F7JRugj4d2aF7E1rtPXAHaetg==", "license": "Apache-2.0 OR MIT", "funding": { "type": "opencollective", @@ -1265,9 +1440,9 @@ } }, "node_modules/@tauri-apps/cli": { - "version": "2.8.4", - "resolved": "https://registry.npmmirror.com/@tauri-apps/cli/-/cli-2.8.4.tgz", - "integrity": "sha512-ejUZBzuQRcjFV+v/gdj/DcbyX/6T4unZQjMSBZwLzP/CymEjKcc2+Fc8xTORThebHDUvqoXMdsCZt8r+hyN15g==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.6.0.tgz", + "integrity": "sha512-JoGKkzNGi7Y1X8IKviSBW7EQO5dLqCDeDPITtugBdbfIuoYtJxQh3St/ABQiX1lbkCSDsMEjQ/EIMo+R/qH8gw==", "dev": true, "license": "Apache-2.0 OR MIT", "bin": { @@ -1281,23 +1456,23 @@ "url": "https://opencollective.com/tauri" }, "optionalDependencies": { - "@tauri-apps/cli-darwin-arm64": "2.8.4", - "@tauri-apps/cli-darwin-x64": "2.8.4", - "@tauri-apps/cli-linux-arm-gnueabihf": "2.8.4", - "@tauri-apps/cli-linux-arm64-gnu": "2.8.4", - "@tauri-apps/cli-linux-arm64-musl": "2.8.4", - "@tauri-apps/cli-linux-riscv64-gnu": "2.8.4", - "@tauri-apps/cli-linux-x64-gnu": "2.8.4", - "@tauri-apps/cli-linux-x64-musl": "2.8.4", - "@tauri-apps/cli-win32-arm64-msvc": "2.8.4", - "@tauri-apps/cli-win32-ia32-msvc": "2.8.4", - "@tauri-apps/cli-win32-x64-msvc": "2.8.4" + "@tauri-apps/cli-darwin-arm64": "2.6.0", + "@tauri-apps/cli-darwin-x64": "2.6.0", + "@tauri-apps/cli-linux-arm-gnueabihf": "2.6.0", + "@tauri-apps/cli-linux-arm64-gnu": "2.6.0", + "@tauri-apps/cli-linux-arm64-musl": "2.6.0", + "@tauri-apps/cli-linux-riscv64-gnu": "2.6.0", + "@tauri-apps/cli-linux-x64-gnu": "2.6.0", + "@tauri-apps/cli-linux-x64-musl": "2.6.0", + "@tauri-apps/cli-win32-arm64-msvc": "2.6.0", + "@tauri-apps/cli-win32-ia32-msvc": "2.6.0", + "@tauri-apps/cli-win32-x64-msvc": "2.6.0" } }, "node_modules/@tauri-apps/cli-darwin-arm64": { - "version": "2.8.4", - "resolved": "https://registry.npmmirror.com/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.8.4.tgz", - "integrity": "sha512-BKu8HRkYV01SMTa7r4fLx+wjgtRK8Vep7lmBdHDioP6b8XH3q2KgsAyPWfEZaZIkZ2LY4SqqGARaE9oilNe0oA==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.6.0.tgz", + "integrity": "sha512-peSc1/dzdQ5n6LinMFBzcQVV6LTB6NjIm4X3fozijq1yJ1ZEODEB7D0+/fjGdortWpEgoV5e8YcsxU3GTjQAew==", "cpu": [ "arm64" ], @@ -1312,9 +1487,9 @@ } }, "node_modules/@tauri-apps/cli-darwin-x64": { - "version": "2.8.4", - "resolved": "https://registry.npmmirror.com/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.8.4.tgz", - "integrity": "sha512-imb9PfSd/7G6VAO7v1bQ2A3ZH4NOCbhGJFLchxzepGcXf9NKkfun157JH9mko29K6sqAwuJ88qtzbKCbWJTH9g==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.6.0.tgz", + "integrity": "sha512-6pitNZQ5toCScBDsdxQJQZlCvalBTc3JYNfKzljF7kYFZXVFTIdrRtGzWreMIwGnNsY41jJ9kcZzj/D6oXk71Q==", "cpu": [ "x64" ], @@ -1329,9 +1504,9 @@ } }, "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { - "version": "2.8.4", - "resolved": "https://registry.npmmirror.com/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.8.4.tgz", - "integrity": "sha512-Ml215UnDdl7/fpOrF1CNovym/KjtUbCuPgrcZ4IhqUCnhZdXuphud/JT3E8X97Y03TZ40Sjz8raXYI2ET0exzw==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.6.0.tgz", + "integrity": "sha512-H/OLy5TLV6+UjN2tVXCB5KDXH/i/2mzYSoltjO2Fxb5n7FdntpHnyPxKF5KY3ZK8WivwhazxUx7NYP/Y/eZF3A==", "cpu": [ "arm" ], @@ -1346,9 +1521,9 @@ } }, "node_modules/@tauri-apps/cli-linux-arm64-gnu": { - "version": "2.8.4", - "resolved": "https://registry.npmmirror.com/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.8.4.tgz", - "integrity": "sha512-pbcgBpMyI90C83CxE5REZ9ODyIlmmAPkkJXtV398X3SgZEIYy5TACYqlyyv2z5yKgD8F8WH4/2fek7+jH+ZXAw==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.6.0.tgz", + "integrity": "sha512-CypYdnywFW3upu0uNeZs2aExOuLactj5ZmiTe5mRLMitcfzkPnnAm5VfAeMPlEDrqJQvAHziJiLSyQ0EHYW1cg==", "cpu": [ "arm64" ], @@ -1363,9 +1538,9 @@ } }, "node_modules/@tauri-apps/cli-linux-arm64-musl": { - "version": "2.8.4", - "resolved": "https://registry.npmmirror.com/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.8.4.tgz", - "integrity": "sha512-zumFeaU1Ws5Ay872FTyIm7z8kfzEHu8NcIn8M6TxbJs0a7GRV21KBdpW1zNj2qy7HynnpQCqjAYXTUUmm9JAOw==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.6.0.tgz", + "integrity": "sha512-lAEzj40VGrn/dPtly9E2dfng82t1xuEdoq0fwjEk6J4oeoKJwD/Kss7vysQHSHq36nKGFoO7gpKa5mxisBGFog==", "cpu": [ "arm64" ], @@ -1380,9 +1555,9 @@ } }, "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { - "version": "2.8.4", - "resolved": "https://registry.npmmirror.com/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.8.4.tgz", - "integrity": "sha512-qiqbB3Zz6IyO201f+1ojxLj65WYj8mixL5cOMo63nlg8CIzsP23cPYUrx1YaDPsCLszKZo7tVs14pc7BWf+/aQ==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.6.0.tgz", + "integrity": "sha512-IWaMX0tQQHFzOudJ0OyC3wamFUr/Qje3qwPSu2okSiHmP1+5enSN3WZZJV5vZ979n/Mak2TFQ8+ivPeSW2fn6g==", "cpu": [ "riscv64" ], @@ -1397,9 +1572,9 @@ } }, "node_modules/@tauri-apps/cli-linux-x64-gnu": { - "version": "2.8.4", - "resolved": "https://registry.npmmirror.com/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.8.4.tgz", - "integrity": "sha512-TaqaDd9Oy6k45Hotx3pOf+pkbsxLaApv4rGd9mLuRM1k6YS/aw81YrsMryYPThrxrScEIUcmNIHaHsLiU4GMkw==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.6.0.tgz", + "integrity": "sha512-w/zlc2H3c9D4GEjAQOOh/DT701SRT9L8IvuhEMwdWWG6BQEH/+u5tgYR3sVJNDv9WXGNxTCiFV0bd/6pgreC1Q==", "cpu": [ "x64" ], @@ -1414,9 +1589,9 @@ } }, "node_modules/@tauri-apps/cli-linux-x64-musl": { - "version": "2.8.4", - "resolved": "https://registry.npmmirror.com/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.8.4.tgz", - "integrity": "sha512-ot9STAwyezN8w+bBHZ+bqSQIJ0qPZFlz/AyscpGqB/JnJQVDFQcRDmUPFEaAtt2UUHSWzN3GoTJ5ypqLBp2WQA==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.6.0.tgz", + "integrity": "sha512-XSeuj7AWuP6Abg532wvzcgIXLlkZLhFXNDRISp7Zeb/ZNHrYTd4BcO88IPb5LVw5vVmvV5r6iTxDd9oVw/FrmA==", "cpu": [ "x64" ], @@ -1431,9 +1606,9 @@ } }, "node_modules/@tauri-apps/cli-win32-arm64-msvc": { - "version": "2.8.4", - "resolved": "https://registry.npmmirror.com/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.8.4.tgz", - "integrity": "sha512-+2aJ/g90dhLiOLFSD1PbElXX3SoMdpO7HFPAZB+xot3CWlAZD1tReUFy7xe0L5GAR16ZmrxpIDM9v9gn5xRy/w==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.6.0.tgz", + "integrity": "sha512-gvmpQkj2vsU3fvJNgSzrX2mw/0DRnkfrg+1GH3wHsCoZq0/xVXG1mXZAxDg4r6e4yCeVuM6Xv2OHpF+Az9+cXA==", "cpu": [ "arm64" ], @@ -1448,9 +1623,9 @@ } }, "node_modules/@tauri-apps/cli-win32-ia32-msvc": { - "version": "2.8.4", - "resolved": "https://registry.npmmirror.com/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.8.4.tgz", - "integrity": "sha512-yj7WDxkL1t9Uzr2gufQ1Hl7hrHuFKTNEOyascbc109EoiAqCp0tgZ2IykQqOZmZOHU884UAWI1pVMqBhS/BfhA==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.6.0.tgz", + "integrity": "sha512-TLeE1HHejeroBeT23MLgjlizSBdAt8QpldLEea4y4WUHHQBT+lx/ey4+aCLhKbB4OEKkdzU9os76f9iNOAsZJw==", "cpu": [ "ia32" ], @@ -1465,9 +1640,9 @@ } }, "node_modules/@tauri-apps/cli-win32-x64-msvc": { - "version": "2.8.4", - "resolved": "https://registry.npmmirror.com/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.8.4.tgz", - "integrity": "sha512-XuvGB4ehBdd7QhMZ9qbj/8icGEatDuBNxyYHbLKsTYh90ggUlPa/AtaqcC1Fo69lGkTmq9BOKrs1aWSi7xDonA==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.6.0.tgz", + "integrity": "sha512-QJ7KHh6//xEob7yr0hG7m3rha0yEAnCvO5ackQGUbbUy0M5ILq5tceNEb4FkKd9OLiFyFIFuA8z7BkrU0hJQFg==", "cpu": [ "x64" ], @@ -1482,26 +1657,65 @@ } }, "node_modules/@tauri-apps/plugin-dialog": { - "version": "2.4.0", - "resolved": "https://registry.npmmirror.com/@tauri-apps/plugin-dialog/-/plugin-dialog-2.4.0.tgz", - "integrity": "sha512-OvXkrEBfWwtd8tzVCEXIvRfNEX87qs2jv6SqmVPiHcJjBhSF/GUvjqUNIDmKByb5N8nvDqVUM7+g1sXwdC/S9w==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.3.0.tgz", + "integrity": "sha512-ylSBvYYShpGlKKh732ZuaHyJ5Ie1JR71QCXewCtsRLqGdc8Is4xWdz6t43rzXyvkItM9syNPMvFVcvjgEy+/GA==", "license": "MIT OR Apache-2.0", "dependencies": { - "@tauri-apps/api": "^2.8.0" + "@tauri-apps/api": "^2.6.0" + } + }, + "node_modules/@tauri-apps/plugin-dialog/node_modules/@tauri-apps/api": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.11.0.tgz", + "integrity": "sha512-7CinYODhky9lmO23xHnUFv0Xt43fbtWMyxZcLcRBlFkcgXKuEirBvHpmtJ89YMhyeGcq20Wuc47Fa4XjyniywA==", + "license": "Apache-2.0 OR MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" } }, "node_modules/@tauri-apps/plugin-fs": { - "version": "2.4.2", - "resolved": "https://registry.npmmirror.com/@tauri-apps/plugin-fs/-/plugin-fs-2.4.2.tgz", - "integrity": "sha512-YGhmYuTgXGsi6AjoV+5mh2NvicgWBfVJHHheuck6oHD+HC9bVWPaHvCP0/Aw4pHDejwrvT8hE3+zZAaWf+hrig==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-fs/-/plugin-fs-2.4.0.tgz", + "integrity": "sha512-Sp8AdDcbyXyk6LD6Pmdx44SH3LPeNAvxR2TFfq/8CwqzfO1yOyV+RzT8fov0NNN7d9nvW7O7MtMAptJ42YXA5g==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.6.0" + } + }, + "node_modules/@tauri-apps/plugin-fs/node_modules/@tauri-apps/api": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.11.0.tgz", + "integrity": "sha512-7CinYODhky9lmO23xHnUFv0Xt43fbtWMyxZcLcRBlFkcgXKuEirBvHpmtJ89YMhyeGcq20Wuc47Fa4XjyniywA==", + "license": "Apache-2.0 OR MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + } + }, + "node_modules/@tauri-apps/plugin-opener": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.5.4.tgz", + "integrity": "sha512-1HnPkb+AmgO29HBazm4uPLKB+r7zzcTBW1d0fyYp1uP+jwtpoiNDGKMMzz58SFp49nOIrxdE3aUJtT57lfO9CQ==", "license": "MIT OR Apache-2.0", "dependencies": { - "@tauri-apps/api": "^2.8.0" + "@tauri-apps/api": "^2.11.0" + } + }, + "node_modules/@tauri-apps/plugin-opener/node_modules/@tauri-apps/api": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.11.0.tgz", + "integrity": "sha512-7CinYODhky9lmO23xHnUFv0Xt43fbtWMyxZcLcRBlFkcgXKuEirBvHpmtJ89YMhyeGcq20Wuc47Fa4XjyniywA==", + "license": "Apache-2.0 OR MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" } }, "node_modules/@types/babel__core": { "version": "7.20.5", - "resolved": "https://registry.npmmirror.com/@types/babel__core/-/babel__core-7.20.5.tgz", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", "dev": true, "license": "MIT", @@ -1515,7 +1729,7 @@ }, "node_modules/@types/babel__generator": { "version": "7.27.0", - "resolved": "https://registry.npmmirror.com/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", "dev": true, "license": "MIT", @@ -1525,7 +1739,7 @@ }, "node_modules/@types/babel__template": { "version": "7.4.4", - "resolved": "https://registry.npmmirror.com/@types/babel__template/-/babel__template-7.4.4.tgz", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", "dev": true, "license": "MIT", @@ -1536,7 +1750,7 @@ }, "node_modules/@types/babel__traverse": { "version": "7.28.0", - "resolved": "https://registry.npmmirror.com/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", "dev": true, "license": "MIT", @@ -1546,60 +1760,42 @@ }, "node_modules/@types/estree": { "version": "1.0.8", - "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, "license": "MIT" }, "node_modules/@types/katex": { - "version": "0.16.7", - "resolved": "https://registry.npmmirror.com/@types/katex/-/katex-0.16.7.tgz", - "integrity": "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/marked": { - "version": "6.0.0", - "resolved": "https://registry.npmmirror.com/@types/marked/-/marked-6.0.0.tgz", - "integrity": "sha512-jmjpa4BwUsmhxcfsgUit/7A9KbrC48Q0q8KvnY107ogcjGgTFDlIL3RpihNpx2Mu1hM4mdFQjoVc4O6JoGKHsA==", - "deprecated": "This is a stub types definition. marked provides its own type definitions, so you do not need this installed.", + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.8.tgz", + "integrity": "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==", "dev": true, - "license": "MIT", - "dependencies": { - "marked": "*" - } - }, - "node_modules/@types/prop-types": { - "version": "15.7.15", - "resolved": "https://registry.npmmirror.com/@types/prop-types/-/prop-types-15.7.15.tgz", - "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "devOptional": true, "license": "MIT" }, "node_modules/@types/react": { - "version": "18.3.26", - "resolved": "https://registry.npmmirror.com/@types/react/-/react-18.3.26.tgz", - "integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==", + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.0.2" + "csstype": "^3.2.2" } }, "node_modules/@types/react-dom": { - "version": "18.3.7", - "resolved": "https://registry.npmmirror.com/@types/react-dom/-/react-dom-18.3.7.tgz", - "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "dev": true, "license": "MIT", "peerDependencies": { - "@types/react": "^18.0.0" + "@types/react": "^19.2.0" } }, "node_modules/@vitejs/plugin-react": { "version": "4.7.0", - "resolved": "https://registry.npmmirror.com/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", "dev": true, "license": "MIT", @@ -1618,64 +1814,23 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, - "node_modules/@xmldom/xmldom": { - "version": "0.8.11", - "resolved": "https://registry.npmmirror.com/@xmldom/xmldom/-/xmldom-0.8.11.tgz", - "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmmirror.com/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/baseline-browser-mapping": { - "version": "2.8.16", - "resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.8.16.tgz", - "integrity": "sha512-OMu3BGQ4E7P1ErFsIPpbJh0qvDudM/UuJeHgkAvfWe+0HFJCXh+t/l8L6fVLR55RI/UbKrVLnAXZSVwd9ysWYw==", + "version": "2.10.25", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.25.tgz", + "integrity": "sha512-QO/VHsXCQdnzADMfmkeOPvHdIAkoB7i0/rGjINPJEetLx75hNttVWGQ/jycHUDP9zZ9rupbm60WRxcwViB0MiA==", "dev": true, "license": "Apache-2.0", "bin": { - "baseline-browser-mapping": "dist/cli.js" + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" } }, - "node_modules/bluebird": { - "version": "3.4.7", - "resolved": "https://registry.npmmirror.com/bluebird/-/bluebird-3.4.7.tgz", - "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==", - "license": "MIT" - }, "node_modules/browserslist": { - "version": "4.26.3", - "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.26.3.tgz", - "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==", + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", "dev": true, "funding": [ { @@ -1692,12 +1847,13 @@ } ], "license": "MIT", + "peer": true, "dependencies": { - "baseline-browser-mapping": "^2.8.9", - "caniuse-lite": "^1.0.30001746", - "electron-to-chromium": "^1.5.227", - "node-releases": "^2.0.21", - "update-browserslist-db": "^1.1.3" + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" @@ -1707,9 +1863,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001750", - "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001750.tgz", - "integrity": "sha512-cuom0g5sdX6rw00qOoLNSFCJ9/mYIsuSOA+yzpDw8eopiFqcVwQvZHqov0vmEighRxX++cfC0Vg1G+1Iy/mSpQ==", + "version": "1.0.30001791", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz", + "integrity": "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==", "dev": true, "funding": [ { @@ -1729,7 +1885,7 @@ }, "node_modules/commander": { "version": "8.3.0", - "resolved": "https://registry.npmmirror.com/commander/-/commander-8.3.0.tgz", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", "license": "MIT", "engines": { @@ -1738,27 +1894,21 @@ }, "node_modules/convert-source-map": { "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true, "license": "MIT" }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "license": "MIT" - }, "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "devOptional": true, "license": "MIT" }, "node_modules/debug": { "version": "4.4.3", - "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", @@ -1774,32 +1924,17 @@ } } }, - "node_modules/dingbat-to-unicode": { - "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/dingbat-to-unicode/-/dingbat-to-unicode-1.0.1.tgz", - "integrity": "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==", - "license": "BSD-2-Clause" - }, - "node_modules/duck": { - "version": "0.1.12", - "resolved": "https://registry.npmmirror.com/duck/-/duck-0.1.12.tgz", - "integrity": "sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==", - "license": "BSD", - "dependencies": { - "underscore": "^1.13.1" - } - }, "node_modules/electron-to-chromium": { - "version": "1.5.234", - "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.234.tgz", - "integrity": "sha512-RXfEp2x+VRYn8jbKfQlRImzoJU01kyDvVPBmG39eU2iuRVhuS6vQNocB8J0/8GrIMLnPzgz4eW6WiRnJkTuNWg==", + "version": "1.5.349", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.349.tgz", + "integrity": "sha512-QsWVGyRuY07Aqb234QytTfwd5d9AJlfNIQ5wIOl1L+PZDzI9d9+Fn0FRale/QYlFxt/bUnB0/nLd1jFPGxGK1A==", "dev": true, "license": "ISC" }, "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1807,37 +1942,40 @@ "esbuild": "bin/esbuild" }, "engines": { - "node": ">=12" + "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" } }, "node_modules/escalade": { "version": "3.2.0", - "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, "license": "MIT", @@ -1845,9 +1983,27 @@ "node": ">=6" } }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/fsevents": { "version": "2.3.3", - "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, @@ -1862,7 +2018,7 @@ }, "node_modules/gensync": { "version": "1.0.0-beta.2", - "resolved": "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true, "license": "MIT", @@ -1870,33 +2026,57 @@ "node": ">=6.9.0" } }, - "node_modules/immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmmirror.com/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", - "license": "MIT" - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "license": "MIT" + "node_modules/i18next": { + "version": "25.10.10", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.10.10.tgz", + "integrity": "sha512-cqUW2Z3EkRx7NqSyywjkgCLK7KLCL6IFVFcONG7nVYIJ3ekZ1/N5jUsihHV6Bq37NfhgtczxJcxduELtjTwkuQ==", + "funding": [ + { + "type": "individual", + "url": "https://www.locize.com/i18next" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + }, + { + "type": "individual", + "url": "https://www.locize.com" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/runtime": "^7.29.2" + }, + "peerDependencies": { + "typescript": "^5 || ^6" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } }, "node_modules/js-tokens": { "version": "4.0.0", - "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, "license": "MIT" }, "node_modules/jsesc": { "version": "3.1.0", - "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "dev": true, "license": "MIT", @@ -1909,7 +2089,7 @@ }, "node_modules/json5": { "version": "2.2.3", - "resolved": "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, "license": "MIT", @@ -1920,22 +2100,10 @@ "node": ">=6" } }, - "node_modules/jszip": { - "version": "3.10.1", - "resolved": "https://registry.npmmirror.com/jszip/-/jszip-3.10.1.tgz", - "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", - "license": "(MIT OR GPL-3.0-or-later)", - "dependencies": { - "lie": "~3.3.0", - "pako": "~1.0.2", - "readable-stream": "~2.3.6", - "setimmediate": "^1.0.5" - } - }, "node_modules/katex": { - "version": "0.16.25", - "resolved": "https://registry.npmmirror.com/katex/-/katex-0.16.25.tgz", - "integrity": "sha512-woHRUZ/iF23GBP1dkDQMh1QBad9dmr8/PAwNA54VrSOVYgI12MAcE14TqnDdQOdzyEonGzMepYnqBMYdsoAr8Q==", + "version": "0.16.45", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.45.tgz", + "integrity": "sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA==", "funding": [ "https://opencollective.com/katex", "https://github.com/sponsors/katex" @@ -1948,41 +2116,9 @@ "katex": "cli.js" } }, - "node_modules/lie": { - "version": "3.3.0", - "resolved": "https://registry.npmmirror.com/lie/-/lie-3.3.0.tgz", - "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", - "license": "MIT", - "dependencies": { - "immediate": "~3.0.5" - } - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/lop": { - "version": "0.4.2", - "resolved": "https://registry.npmmirror.com/lop/-/lop-0.4.2.tgz", - "integrity": "sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw==", - "license": "BSD-2-Clause", - "dependencies": { - "duck": "^0.1.12", - "option": "~0.2.1", - "underscore": "^1.13.1" - } - }, "node_modules/lru-cache": { "version": "5.1.1", - "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "dev": true, "license": "ISC", @@ -1990,34 +2126,10 @@ "yallist": "^3.0.2" } }, - "node_modules/mammoth": { - "version": "1.11.0", - "resolved": "https://registry.npmmirror.com/mammoth/-/mammoth-1.11.0.tgz", - "integrity": "sha512-BcEqqY/BOwIcI1iR5tqyVlqc3KIaMRa4egSoK83YAVrBf6+yqdAAbtUcFDCWX8Zef8/fgNZ6rl4VUv+vVX8ddQ==", - "license": "BSD-2-Clause", - "dependencies": { - "@xmldom/xmldom": "^0.8.6", - "argparse": "~1.0.3", - "base64-js": "^1.5.1", - "bluebird": "~3.4.0", - "dingbat-to-unicode": "^1.0.1", - "jszip": "^3.7.1", - "lop": "^0.4.2", - "path-is-absolute": "^1.0.0", - "underscore": "^1.13.1", - "xmlbuilder": "^10.0.0" - }, - "bin": { - "mammoth": "bin/mammoth" - }, - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/marked": { - "version": "12.0.2", - "resolved": "https://registry.npmmirror.com/marked/-/marked-12.0.2.tgz", - "integrity": "sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q==", + "version": "15.0.12", + "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", + "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", "license": "MIT", "bin": { "marked": "bin/marked.js" @@ -2028,15 +2140,15 @@ }, "node_modules/ms": { "version": "2.1.3", - "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true, "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", "dev": true, "funding": [ { @@ -2053,56 +2165,49 @@ } }, "node_modules/node-releases": { - "version": "2.0.23", - "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.23.tgz", - "integrity": "sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==", + "version": "2.0.38", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", + "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", "dev": true, "license": "MIT" }, - "node_modules/option": { - "version": "0.2.4", - "resolved": "https://registry.npmmirror.com/option/-/option-0.2.4.tgz", - "integrity": "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==", - "license": "BSD-2-Clause" - }, - "node_modules/pako": { - "version": "1.0.11", - "resolved": "https://registry.npmmirror.com/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "license": "(MIT AND Zlib)" - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/pdfjs-dist": { - "version": "4.10.38", - "resolved": "https://registry.npmmirror.com/pdfjs-dist/-/pdfjs-dist-4.10.38.tgz", - "integrity": "sha512-/Y3fcFrXEAsMjJXeL9J8+ZG9U01LbuWaYypvDW2ycW1jL269L3js3DVBjDJ0Up9Np1uqDXsDrRihHANhZOlwdQ==", + "version": "5.7.284", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.7.284.tgz", + "integrity": "sha512-h4EdYQczmGhbOlqc3PPZwxevn7ApdWPbovAuWXOB/DjIyigSnwfy2oze7c6mRcSr9XgLp3eN3EeL4DyySTPMFw==", "license": "Apache-2.0", "engines": { - "node": ">=20" + "node": ">=22.13.0 || >=24" }, "optionalDependencies": { - "@napi-rs/canvas": "^0.1.65" + "@napi-rs/canvas": "^0.1.100" } }, "node_modules/picocolors": { "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true, "license": "ISC" }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.13", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.13.tgz", + "integrity": "sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag==", "dev": true, "funding": [ { @@ -2128,40 +2233,57 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "license": "MIT" - }, "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmmirror.com/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", + "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - }, + "peer": true, "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", + "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", "license": "MIT", "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" + "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^18.3.1" + "react": "^19.2.5" + } + }, + "node_modules/react-i18next": { + "version": "15.7.4", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.7.4.tgz", + "integrity": "sha512-nyU8iKNrI5uDJch0z9+Y5XEr34b0wkyYj3Rp+tfbahxtlswxSCjcUL9H0nqXo9IR3/t5Y5PKIA3fx3MfUyR9Xw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.6", + "html-parse-stringify": "^3.0.1" + }, + "peerDependencies": { + "i18next": ">= 23.4.0", + "react": ">= 16.8.0", + "typescript": "^5" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } } }, "node_modules/react-refresh": { "version": "0.17.0", - "resolved": "https://registry.npmmirror.com/react-refresh/-/react-refresh-0.17.0.tgz", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", "dev": true, "license": "MIT", @@ -2169,25 +2291,10 @@ "node": ">=0.10.0" } }, - "node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, "node_modules/rollup": { - "version": "4.52.4", - "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.52.4.tgz", - "integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", + "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2201,49 +2308,43 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.52.4", - "@rollup/rollup-android-arm64": "4.52.4", - "@rollup/rollup-darwin-arm64": "4.52.4", - "@rollup/rollup-darwin-x64": "4.52.4", - "@rollup/rollup-freebsd-arm64": "4.52.4", - "@rollup/rollup-freebsd-x64": "4.52.4", - "@rollup/rollup-linux-arm-gnueabihf": "4.52.4", - "@rollup/rollup-linux-arm-musleabihf": "4.52.4", - "@rollup/rollup-linux-arm64-gnu": "4.52.4", - "@rollup/rollup-linux-arm64-musl": "4.52.4", - "@rollup/rollup-linux-loong64-gnu": "4.52.4", - "@rollup/rollup-linux-ppc64-gnu": "4.52.4", - "@rollup/rollup-linux-riscv64-gnu": "4.52.4", - "@rollup/rollup-linux-riscv64-musl": "4.52.4", - "@rollup/rollup-linux-s390x-gnu": "4.52.4", - "@rollup/rollup-linux-x64-gnu": "4.52.4", - "@rollup/rollup-linux-x64-musl": "4.52.4", - "@rollup/rollup-openharmony-arm64": "4.52.4", - "@rollup/rollup-win32-arm64-msvc": "4.52.4", - "@rollup/rollup-win32-ia32-msvc": "4.52.4", - "@rollup/rollup-win32-x64-gnu": "4.52.4", - "@rollup/rollup-win32-x64-msvc": "4.52.4", + "@rollup/rollup-android-arm-eabi": "4.60.2", + "@rollup/rollup-android-arm64": "4.60.2", + "@rollup/rollup-darwin-arm64": "4.60.2", + "@rollup/rollup-darwin-x64": "4.60.2", + "@rollup/rollup-freebsd-arm64": "4.60.2", + "@rollup/rollup-freebsd-x64": "4.60.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", + "@rollup/rollup-linux-arm-musleabihf": "4.60.2", + "@rollup/rollup-linux-arm64-gnu": "4.60.2", + "@rollup/rollup-linux-arm64-musl": "4.60.2", + "@rollup/rollup-linux-loong64-gnu": "4.60.2", + "@rollup/rollup-linux-loong64-musl": "4.60.2", + "@rollup/rollup-linux-ppc64-gnu": "4.60.2", + "@rollup/rollup-linux-ppc64-musl": "4.60.2", + "@rollup/rollup-linux-riscv64-gnu": "4.60.2", + "@rollup/rollup-linux-riscv64-musl": "4.60.2", + "@rollup/rollup-linux-s390x-gnu": "4.60.2", + "@rollup/rollup-linux-x64-gnu": "4.60.2", + "@rollup/rollup-linux-x64-musl": "4.60.2", + "@rollup/rollup-openbsd-x64": "4.60.2", + "@rollup/rollup-openharmony-arm64": "4.60.2", + "@rollup/rollup-win32-arm64-msvc": "4.60.2", + "@rollup/rollup-win32-ia32-msvc": "4.60.2", + "@rollup/rollup-win32-x64-gnu": "4.60.2", + "@rollup/rollup-win32-x64-msvc": "4.60.2", "fsevents": "~2.3.2" } }, - "node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - } + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" }, "node_modules/semver": { "version": "6.3.1", - "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "license": "ISC", @@ -2251,15 +2352,9 @@ "semver": "bin/semver.js" } }, - "node_modules/setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmmirror.com/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", - "license": "MIT" - }, "node_modules/source-map-js": { "version": "1.2.1", - "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, "license": "BSD-3-Clause", @@ -2267,27 +2362,30 @@ "node": ">=0.10.0" } }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmmirror.com/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "license": "BSD-3-Clause" - }, - "node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, "license": "MIT", "dependencies": { - "safe-buffer": "~5.1.0" + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" } }, "node_modules/typescript": { "version": "5.9.3", - "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -2296,16 +2394,10 @@ "node": ">=14.17" } }, - "node_modules/underscore": { - "version": "1.13.7", - "resolved": "https://registry.npmmirror.com/underscore/-/underscore-1.13.7.tgz", - "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==", - "license": "MIT" - }, "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "dev": true, "funding": [ { @@ -2333,37 +2425,26 @@ "browserslist": ">= 4.21.0" } }, - "node_modules/use-sync-external-store": { - "version": "1.6.0", - "resolved": "https://registry.npmmirror.com/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", - "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, "node_modules/vite": { - "version": "5.4.20", - "resolved": "https://registry.npmmirror.com/vite/-/vite-5.4.20.tgz", - "integrity": "sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g==", + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", + "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^20.19.0 || >=22.12.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -2372,19 +2453,25 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.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 }, + "jiti": { + "optional": true + }, "less": { "optional": true }, @@ -2405,40 +2492,44 @@ }, "terser": { "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true } } }, - "node_modules/xmlbuilder": { - "version": "10.1.1", - "resolved": "https://registry.npmmirror.com/xmlbuilder/-/xmlbuilder-10.1.1.tgz", - "integrity": "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==", + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", "license": "MIT", "engines": { - "node": ">=4.0" + "node": ">=0.10.0" } }, "node_modules/yallist": { "version": "3.1.1", - "resolved": "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true, "license": "ISC" }, "node_modules/zustand": { - "version": "4.5.7", - "resolved": "https://registry.npmmirror.com/zustand/-/zustand-4.5.7.tgz", - "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.12.tgz", + "integrity": "sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==", "license": "MIT", - "dependencies": { - "use-sync-external-store": "^1.2.2" - }, "engines": { - "node": ">=12.7.0" + "node": ">=12.20.0" }, "peerDependencies": { - "@types/react": ">=16.8", + "@types/react": ">=18.0.0", "immer": ">=9.0.6", - "react": ">=16.8" + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" }, "peerDependenciesMeta": { "@types/react": { @@ -2449,6 +2540,9 @@ }, "react": { "optional": true + }, + "use-sync-external-store": { + "optional": true } } } diff --git a/package.json b/package.json index daff91d6..3cb8b44c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "onedocs", "private": true, - "version": "1.2.0", + "version": "1.4.0", "type": "module", "scripts": { "dev": "vite", @@ -12,26 +12,30 @@ "tauri:build": "tauri build" }, "dependencies": { - "@tauri-apps/api": "^2.8.0", - "@tauri-apps/plugin-dialog": "^2.4.0", - "@tauri-apps/plugin-fs": "^2.4.2", - "jszip": "^3.10.1", - "katex": "^0.16.11", - "mammoth": "^1.8.0", - "marked": "^12.0.2", - "pdfjs-dist": "^4.0.379", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "zustand": "^4.4.0" + "@lobehub/icons-static-png": "^1.88.0", + "@tauri-apps/api": "2.6.0", + "@tauri-apps/plugin-dialog": "2.3.0", + "@tauri-apps/plugin-fs": "2.4.0", + "@tauri-apps/plugin-opener": "^2.5.4", + "i18next": "^25.0.0", + "katex": "^0.16.21", + "marked": "^15.0.0", + "pdfjs-dist": "^5.0.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-i18next": "^15.0.0", + "zustand": "^5.0.0" }, "devDependencies": { - "@tauri-apps/cli": "^2", + "@tauri-apps/cli": "2.6.0", "@types/katex": "^0.16.7", - "@types/marked": "^6.0.0", - "@types/react": "^18.3.12", - "@types/react-dom": "^18.3.1", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", "@vitejs/plugin-react": "^4.3.3", "typescript": "^5.6.3", - "vite": "^5.4.10" + "vite": "^7.3.1" + }, + "overrides": { + "uuid": "^14.0.0" } } diff --git a/scripts/pdf-smoke-test.mjs b/scripts/pdf-smoke-test.mjs new file mode 100644 index 00000000..d7c59025 --- /dev/null +++ b/scripts/pdf-smoke-test.mjs @@ -0,0 +1,32 @@ +import { readFile } from "node:fs/promises"; +import process from "node:process"; +import { getDocument } from "pdfjs-dist/legacy/build/pdf.mjs"; + +const pdfPath = process.env.PDF_PATH; + +if (!pdfPath) { + console.error("Missing PDF_PATH env var."); + process.exit(1); +} + +const data = new Uint8Array(await readFile(pdfPath)); +const pdf = await getDocument({ + data, + disableWorker: true, + useSystemFonts: true, + disableAutoFetch: true, +}).promise; + +console.log(`Pages: ${pdf.numPages}`); + +const maxPages = Math.min(pdf.numPages, 2); +let previewText = ""; + +for (let pageNum = 1; pageNum <= maxPages; pageNum++) { + const page = await pdf.getPage(pageNum); + const textContent = await page.getTextContent(); + const pageText = textContent.items.map((item) => item.str).join(" ").trim(); + previewText += `\n--- Page ${pageNum} ---\n${pageText}\n`; +} + +console.log(previewText || "(no text detected)"); diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 3615bc28..ac06d418 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2,15 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "addr2line" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" -dependencies = [ - "gimli", -] - [[package]] name = "adler2" version = "2.0.1" @@ -19,13 +10,31 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] +[[package]] +name = "aligned" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" +dependencies = [ + "as-slice", +] + +[[package]] +name = "aligned-vec" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +dependencies = [ + "equator", +] + [[package]] name = "alloc-no-stdlib" version = "2.0.4" @@ -52,20 +61,52 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "as-slice" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" +dependencies = [ + "stable_deref_trait", +] [[package]] name = "ashpd" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cbdf310d77fd3aaee6ea2093db7011dc2d35d2eb3481e5607f1f8d942ed99df" +checksum = "d2f3f79755c74fd155000314eb349864caa787c6592eace6c6882dad873d9c39" dependencies = [ "enumflags2", "futures-channel", "futures-util", - "rand 0.9.2", + "rand 0.9.4", "raw-window-handle", "serde", "serde_repr", @@ -89,6 +130,79 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", +] + [[package]] name = "async-recursion" version = "1.1.1" @@ -97,9 +211,33 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + [[package]] name = "async-trait" version = "0.1.89" @@ -108,7 +246,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -147,18 +285,46 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] -name = "backtrace" -version = "0.3.76" +name = "av-scenechange" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-link 0.2.1", + "aligned", + "anyhow", + "arg_enum_proc_macro", + "arrayvec", + "log", + "num-rational", + "num-traits", + "pastey", + "rayon", + "thiserror 2.0.18", + "v_frame", + "y4m", +] + +[[package]] +name = "av1-grain" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom 8.0.0", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7178fe5f7d460b13895ebb9dcb28a3a6216d2df2574a0806cb51b555d297f38" +dependencies = [ + "arrayvec", ] [[package]] @@ -173,6 +339,27 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" + [[package]] name = "bitflags" version = "1.3.2" @@ -181,11 +368,20 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.4" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" dependencies = [ - "serde", + "serde_core", +] + +[[package]] +name = "bitstream-io" +version = "4.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eff00be299a18769011411c9def0d827e8f2d7bf0c3dbf53633147a8867fd1f" +dependencies = [ + "no_std_io2", ] [[package]] @@ -199,20 +395,24 @@ dependencies = [ [[package]] name = "block2" -version = "0.5.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" dependencies = [ - "objc2 0.5.2", + "objc2", ] [[package]] -name = "block2" -version = "0.6.2" +name = "blocking" +version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" dependencies = [ - "objc2 0.6.3", + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", ] [[package]] @@ -236,17 +436,34 @@ dependencies = [ "alloc-stdlib", ] +[[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + +[[package]] +name = "built" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64" + [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "bytemuck" -version = "1.24.0" +version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" [[package]] name = "byteorder" @@ -254,11 +471,17 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" -version = "1.10.1" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" dependencies = [ "serde", ] @@ -269,7 +492,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.1", "cairo-sys-rs", "glib", "libc", @@ -290,9 +513,9 @@ dependencies = [ [[package]] name = "camino" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "276a59bf2b2c967788139340c9f0c5b12d7fd6630315c15c217e559de85d2609" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" dependencies = [ "serde_core", ] @@ -317,7 +540,7 @@ dependencies = [ "semver", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -327,16 +550,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" dependencies = [ "serde", - "toml 0.9.7", + "toml 0.9.12+spec-1.1.0", ] [[package]] name = "cc" -version = "1.2.40" +version = "1.2.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d05d92f4b1fd76aad469d46cdd858ca761576082cd37df81416691e50199fb" +checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -369,21 +594,15 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" - -[[package]] -name = "cfg_aliases" -version = "0.2.1" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chrono" -version = "0.4.42" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "num-traits", @@ -391,6 +610,12 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "combine" version = "4.6.7" @@ -454,11 +679,11 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "core-graphics" -version = "0.24.0" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.1", "core-foundation 0.10.1", "core-graphics-types", "foreign-types 0.5.0", @@ -471,7 +696,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.1", "core-foundation 0.10.1", "libc", ] @@ -503,17 +728,42 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "typenum", @@ -536,6 +786,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "cssparser" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf 0.13.1", + "smallvec", +] + [[package]] name = "cssparser-macros" version = "0.6.1" @@ -543,24 +806,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "ctor" -version = "0.2.9" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" dependencies = [ - "quote", - "syn 2.0.106", + "ctor-proc-macro", + "dtor", ] +[[package]] +name = "ctor-proc-macro" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + [[package]] name = "darling" -version = "0.21.3" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" dependencies = [ "darling_core", "darling_macro", @@ -568,34 +837,33 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.21.3" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" dependencies = [ - "fnv", "ident_case", "proc-macro2", "quote", "strsim", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "darling_macro" -version = "0.21.3" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "deranged" -version = "0.5.4" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", "serde_core", @@ -611,7 +879,28 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.106", + "syn 2.0.117", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", ] [[package]] @@ -645,22 +934,16 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "dispatch" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" - [[package]] name = "dispatch2" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags 2.9.4", - "block2 0.6.2", + "bitflags 2.11.1", + "block2", "libc", - "objc2 0.6.3", + "objc2", ] [[package]] @@ -671,23 +954,23 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "dlib" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" dependencies = [ - "libloading", + "libloading 0.8.9", ] [[package]] name = "dlopen2" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b54f373ccf864bf587a89e880fb7610f8d73f3045f13580948ccbcaff26febff" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" dependencies = [ "dlopen2_derive", "libc", @@ -697,13 +980,28 @@ dependencies = [ [[package]] name = "dlopen2_derive" -version = "0.4.1" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "788160fb30de9cdd857af31c6a2675904b16ece8fc2737b2c7127ba368c9d0f4" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", +] + +[[package]] +name = "dom_query" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" +dependencies = [ + "bit-set", + "cssparser 0.36.0", + "foldhash 0.2.0", + "html5ever 0.38.0", + "precomputed-hash", + "selectors 0.36.1", + "tendril 0.5.0", ] [[package]] @@ -723,9 +1021,9 @@ dependencies = [ [[package]] name = "dtoa" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" [[package]] name = "dtoa-short" @@ -736,6 +1034,21 @@ dependencies = [ "dtoa", ] +[[package]] +name = "dtor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + [[package]] name = "dunce" version = "1.0.5" @@ -748,16 +1061,22 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "embed-resource" -version = "3.0.6" +version = "3.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55a075fc573c64510038d7ee9abc7990635863992f83ebc52c8b433b8411a02e" +checksum = "c31a88c8d26de40ed18fe748c547845aa39de1db3afd958f8cb91579f3644bcb" dependencies = [ "cc", "memchr", "rustc_version", - "toml 0.9.7", + "toml 1.1.2+spec-1.1.0", "vswhom", "winreg", ] @@ -779,9 +1098,9 @@ dependencies = [ [[package]] name = "endi" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" [[package]] name = "enumflags2" @@ -801,7 +1120,27 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", +] + +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] @@ -812,9 +1151,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "erased-serde" -version = "0.4.8" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "259d404d09818dec19332e31d94558aeb442fea04c817006456c24b5460bbd4b" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" dependencies = [ "serde", "serde_core", @@ -852,11 +1191,32 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "exr" +version = "1.74.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + [[package]] name = "fastrand" -version = "2.3.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "fax" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" [[package]] name = "fdeflate" @@ -879,15 +1239,15 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.3" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0399f9d26e5191ce32c498bebd31e7a3ceabc2745f0ac54af3f335126c3f24b3" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "flate2" -version = "1.1.4" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc5a4e564e38c699f2880d3fda590bedc2e69f3f84cd48b457bd892ce61d0aa9" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", @@ -899,6 +1259,18 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "foreign-types" version = "0.3.2" @@ -926,7 +1298,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -962,24 +1334,24 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", ] [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -988,9 +1360,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-lite" @@ -1007,32 +1379,32 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-core", "futures-io", @@ -1041,7 +1413,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -1176,9 +1547,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "libc", @@ -1187,21 +1558,38 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.3.3" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", - "r-efi", - "wasi 0.14.7+wasi-0.2.4", + "r-efi 6.0.0", + "wasip2", + "wasip3", ] [[package]] -name = "gimli" -version = "0.32.3" +name = "gif" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159" +dependencies = [ + "color_quant", + "weezl", +] [[package]] name = "gio" @@ -1241,7 +1629,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.1", "futures-channel", "futures-core", "futures-executor", @@ -1269,7 +1657,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -1348,14 +1736,14 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "h2" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" dependencies = [ "atomic-waker", "bytes", @@ -1363,13 +1751,24 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.11.4", + "indexmap 2.14.0", "slab", "tokio", "tokio-util", "tracing", ] +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -1378,9 +1777,18 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.16.0" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" [[package]] name = "heck" @@ -1394,6 +1802,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex" version = "0.4.3" @@ -1408,18 +1822,27 @@ checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" dependencies = [ "log", "mac", - "markup5ever", + "markup5ever 0.14.1", "match_token", ] +[[package]] +name = "html5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" +dependencies = [ + "log", + "markup5ever 0.38.0", +] + [[package]] name = "http" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" dependencies = [ "bytes", - "fnv", "itoa", ] @@ -1446,6 +1869,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "http-range" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" + [[package]] name = "httparse" version = "1.10.1" @@ -1454,9 +1883,9 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hyper" -version = "1.7.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" dependencies = [ "atomic-waker", "bytes", @@ -1468,7 +1897,6 @@ dependencies = [ "httparse", "itoa", "pin-project-lite", - "pin-utils", "smallvec", "tokio", "want", @@ -1476,15 +1904,14 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.7" +version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ "http", "hyper", "hyper-util", "rustls", - "rustls-pki-types", "tokio", "tokio-rustls", "tower-service", @@ -1508,14 +1935,13 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.17" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ "base64 0.22.1", "bytes", "futures-channel", - "futures-core", "futures-util", "http", "http-body", @@ -1534,9 +1960,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.64" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1558,22 +1984,23 @@ dependencies = [ [[package]] name = "ico" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc50b891e4acf8fe0e71ef88ec43ad82ee07b3810ad09de10f1d01f072ed4b98" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" dependencies = [ "byteorder", - "png", + "png 0.17.16", ] [[package]] name = "icu_collections" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", @@ -1581,9 +2008,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -1594,11 +2021,10 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", @@ -1609,42 +2035,38 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "2.0.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ - "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", - "potential_utf", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "2.0.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", - "stable_deref_trait", - "tinystr", "writeable", "yoke", "zerofrom", @@ -1652,6 +2074,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" @@ -1671,14 +2099,54 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ "icu_normalizer", "icu_properties", ] +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "exr", + "gif", + "image-webp", + "moxcms", + "num-traits", + "png 0.18.1", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "imgref" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40fac9d56ed6437b198fddba683305e8e2d651aa42647f00f5ae542e7f5c94a2" + [[package]] name = "indexmap" version = "1.9.3" @@ -1692,12 +2160,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.11.4" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.0", + "hashbrown 0.17.0", "serde", "serde_core", ] @@ -1712,37 +2180,65 @@ dependencies = [ ] [[package]] -name = "io-uring" -version = "0.7.10" +name = "interpolate_name" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ - "bitflags 2.9.4", - "cfg-if", - "libc", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] name = "ipnet" -version = "2.11.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "iri-string" -version = "0.7.8" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" dependencies = [ "memchr", "serde", ] +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "javascriptcore-rs" @@ -1776,7 +2272,7 @@ dependencies = [ "cesu8", "cfg-if", "combine", - "jni-sys", + "jni-sys 0.3.1", "log", "thiserror 1.0.69", "walkdir", @@ -1785,16 +2281,50 @@ dependencies = [ [[package]] name = "jni-sys" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] [[package]] name = "js-sys" -version = "0.3.81" +version = "0.3.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" +checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] @@ -1827,7 +2357,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.1", "serde", "unicode-segmentation", ] @@ -1838,17 +2368,23 @@ version = "0.8.8-speedreader" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" dependencies = [ - "cssparser", - "html5ever", - "indexmap 2.11.4", - "selectors", + "cssparser 0.29.6", + "html5ever 0.29.1", + "indexmap 2.14.0", + "selectors 0.24.0", ] [[package]] -name = "lazy_static" -version = "1.5.0" +name = "leb128fmt" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "lebe" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" [[package]] name = "libappindicator" @@ -1870,15 +2406,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" dependencies = [ "gtk-sys", - "libloading", + "libloading 0.7.4", "once_cell", ] [[package]] name = "libc" -version = "0.2.176" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" +checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d" +dependencies = [ + "arbitrary", + "cc", +] [[package]] name = "libloading" @@ -1890,27 +2436,42 @@ dependencies = [ "winapi", ] +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link 0.2.1", +] + [[package]] name = "libredox" -version = "0.1.10" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" dependencies = [ - "bitflags 2.9.4", "libc", ] +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "lock_api" @@ -1923,9 +2484,55 @@ dependencies = [ [[package]] name = "log" -version = "0.4.28" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + +[[package]] +name = "lopdf" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07c8e1b6184b1b32ea5f72f572ebdc40e5da1d2921fa469947ff7c480ad1f85a" +dependencies = [ + "encoding_rs", + "flate2", + "itoa", + "linked-hash-map", + "log", + "md5", + "pom", + "time", + "weezl", +] + +[[package]] +name = "lopdf" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "c5c8ecfc6c72051981c0459f75ccc585e7ff67c70829560cda8e647882a9abff" +dependencies = [ + "chrono", + "encoding_rs", + "flate2", + "indexmap 2.14.0", + "itoa", + "log", + "md-5", + "nom 7.1.3", + "rangemap", + "rayon", + "time", + "weezl", +] [[package]] name = "mac" @@ -1942,9 +2549,20 @@ dependencies = [ "log", "phf 0.11.3", "phf_codegen 0.11.3", - "string_cache", - "string_cache_codegen", - "tendril", + "string_cache 0.8.9", + "string_cache_codegen 0.5.4", + "tendril 0.4.3", +] + +[[package]] +name = "markup5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" +dependencies = [ + "log", + "tendril 0.5.0", + "web_atoms", ] [[package]] @@ -1955,7 +2573,7 @@ checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -1964,11 +2582,37 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "memoffset" @@ -1985,6 +2629,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -1997,41 +2647,51 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.4" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.59.0", + "windows-sys 0.61.2", +] + +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", ] [[package]] name = "muda" -version = "0.17.1" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01c1738382f66ed56b3b9c8119e794a2e23148ac8ea214eda86622d4cb9d415a" +checksum = "4de14a9b5d569ca68d7c891d613b390cf5ab4f851c77aaa2f9e435555d3d9492" dependencies = [ "crossbeam-channel", "dpi", "gtk", "keyboard-types", - "objc2 0.6.3", + "objc2", "objc2-app-kit", "objc2-core-foundation", - "objc2-foundation 0.3.2", + "objc2-foundation", "once_cell", - "png", + "png 0.17.16", "serde", - "thiserror 2.0.17", - "windows-sys 0.60.2", + "thiserror 2.0.18", + "windows-sys 0.59.0", ] [[package]] name = "native-tls" -version = "0.2.14" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" dependencies = [ "libc", "log", @@ -2050,8 +2710,8 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.9.4", - "jni-sys", + "bitflags 2.11.1", + "jni-sys 0.3.1", "log", "ndk-sys", "num_enum", @@ -2071,7 +2731,7 @@ version = "0.6.0+11769913" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" dependencies = [ - "jni-sys", + "jni-sys 0.3.1", ] [[package]] @@ -2081,16 +2741,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" [[package]] -name = "nix" -version = "0.30.1" +name = "no_std_io2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +checksum = "418abd1b6d34fbf6cae440dc874771b0525a604428704c76e48b29a5e67b8003" dependencies = [ - "bitflags 2.9.4", - "cfg-if", - "cfg_aliases", - "libc", - "memoffset", + "memchr", ] [[package]] @@ -2099,11 +2755,77 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] [[package]] name = "num-traits" @@ -2116,9 +2838,9 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" dependencies = [ "num_enum_derive", "rustversion", @@ -2126,37 +2848,21 @@ dependencies = [ [[package]] name = "num_enum_derive" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" dependencies = [ - "proc-macro-crate 3.4.0", + "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn 2.0.106", -] - -[[package]] -name = "objc-sys" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" - -[[package]] -name = "objc2" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" -dependencies = [ - "objc-sys", - "objc2-encode", + "syn 2.0.117", ] [[package]] name = "objc2" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" dependencies = [ "objc2-encode", "objc2-exception-helper", @@ -2168,10 +2874,10 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ - "bitflags 2.9.4", - "block2 0.6.2", + "bitflags 2.11.1", + "block2", "libc", - "objc2 0.6.3", + "objc2", "objc2-cloud-kit", "objc2-core-data", "objc2-core-foundation", @@ -2179,8 +2885,8 @@ dependencies = [ "objc2-core-image", "objc2-core-text", "objc2-core-video", - "objc2-foundation 0.3.2", - "objc2-quartz-core 0.3.2", + "objc2-foundation", + "objc2-quartz-core", ] [[package]] @@ -2189,9 +2895,9 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" dependencies = [ - "bitflags 2.9.4", - "objc2 0.6.3", - "objc2-foundation 0.3.2", + "bitflags 2.11.1", + "objc2", + "objc2-foundation", ] [[package]] @@ -2200,9 +2906,9 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" dependencies = [ - "bitflags 2.9.4", - "objc2 0.6.3", - "objc2-foundation 0.3.2", + "bitflags 2.11.1", + "objc2", + "objc2-foundation", ] [[package]] @@ -2211,9 +2917,9 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.1", "dispatch2", - "objc2 0.6.3", + "objc2", ] [[package]] @@ -2222,9 +2928,9 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.1", "dispatch2", - "objc2 0.6.3", + "objc2", "objc2-core-foundation", "objc2-io-surface", ] @@ -2235,8 +2941,8 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" dependencies = [ - "objc2 0.6.3", - "objc2-foundation 0.3.2", + "objc2", + "objc2-foundation", ] [[package]] @@ -2245,8 +2951,8 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" dependencies = [ - "bitflags 2.9.4", - "objc2 0.6.3", + "bitflags 2.11.1", + "objc2", "objc2-core-foundation", "objc2-core-graphics", ] @@ -2257,8 +2963,8 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" dependencies = [ - "bitflags 2.9.4", - "objc2 0.6.3", + "bitflags 2.11.1", + "objc2", "objc2-core-foundation", "objc2-core-graphics", "objc2-io-surface", @@ -2279,28 +2985,16 @@ dependencies = [ "cc", ] -[[package]] -name = "objc2-foundation" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" -dependencies = [ - "bitflags 2.9.4", - "block2 0.5.1", - "libc", - "objc2 0.5.2", -] - [[package]] name = "objc2-foundation" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.9.4", - "block2 0.6.2", + "bitflags 2.11.1", + "block2", "libc", - "objc2 0.6.3", + "objc2", "objc2-core-foundation", ] @@ -2310,8 +3004,8 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ - "bitflags 2.9.4", - "objc2 0.6.3", + "bitflags 2.11.1", + "objc2", "objc2-core-foundation", ] @@ -2321,44 +3015,20 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a1e6550c4caed348956ce3370c9ffeca70bb1dbed4fa96112e7c6170e074586" dependencies = [ - "objc2 0.6.3", + "objc2", "objc2-core-foundation", ] -[[package]] -name = "objc2-metal" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" -dependencies = [ - "bitflags 2.9.4", - "block2 0.5.1", - "objc2 0.5.2", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "objc2-quartz-core" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" -dependencies = [ - "bitflags 2.9.4", - "block2 0.5.1", - "objc2 0.5.2", - "objc2-foundation 0.2.2", - "objc2-metal", -] - [[package]] name = "objc2-quartz-core" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ - "bitflags 2.9.4", - "objc2 0.6.3", - "objc2-foundation 0.3.2", + "bitflags 2.11.1", + "objc2", + "objc2-core-foundation", + "objc2-foundation", ] [[package]] @@ -2367,8 +3037,8 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a" dependencies = [ - "bitflags 2.9.4", - "objc2 0.6.3", + "bitflags 2.11.1", + "objc2", "objc2-core-foundation", ] @@ -2378,10 +3048,10 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" dependencies = [ - "bitflags 2.9.4", - "objc2 0.6.3", + "bitflags 2.11.1", + "objc2", "objc2-core-foundation", - "objc2-foundation 0.3.2", + "objc2-foundation", ] [[package]] @@ -2390,36 +3060,31 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" dependencies = [ - "bitflags 2.9.4", - "block2 0.6.2", - "objc2 0.6.3", + "bitflags 2.11.1", + "block2", + "objc2", "objc2-app-kit", "objc2-core-foundation", - "objc2-foundation 0.3.2", + "objc2-foundation", "objc2-javascript-core", "objc2-security", ] -[[package]] -name = "object" -version = "0.37.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" -dependencies = [ - "memchr", -] - [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "onedocs" -version = "1.2.0" +version = "1.4.0" dependencies = [ "anyhow", + "base64 0.22.1", + "image", + "lopdf 0.34.0", + "printpdf", "reqwest", "serde", "serde_json", @@ -2427,16 +3092,30 @@ dependencies = [ "tauri-build", "tauri-plugin-dialog", "tauri-plugin-fs", + "tauri-plugin-opener", + "tauri-runtime", "tokio", ] +[[package]] +name = "open" +version = "5.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fbaa89d2ddc8473c78a3adf69eea8cffa28c483b8e02a971ef31527cd0fc92c" +dependencies = [ + "dunce", + "is-wsl", + "libc", + "pathdiff", +] + [[package]] name = "openssl" -version = "0.10.73" +version = "0.10.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +checksum = "f38c4372413cdaaf3cc79dd92d29d7d9f5ab09b51b10dded508fb90bb70b9222" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.1", "cfg-if", "foreign-types 0.3.2", "libc", @@ -2453,20 +3132,20 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "openssl-probe" -version = "0.1.6" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "openssl-sys" -version = "0.9.109" +version = "0.9.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +checksum = "13ce1245cd07fcc4cfdb438f7507b0c7e4f3849a69fd84d52374c66d83741bb6" dependencies = [ "cc", "libc", @@ -2490,6 +3169,15 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "owned_ttf_parser" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "706de7e2214113d63a8238d1910463cfce781129a6f263d13fdb09ff64355ba4" +dependencies = [ + "ttf-parser", +] + [[package]] name = "pango" version = "0.18.3" @@ -2544,6 +3232,24 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pastey" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + [[package]] name = "percent-encoding" version = "2.3.2" @@ -2580,6 +3286,17 @@ dependencies = [ "phf_shared 0.11.3", ] +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros 0.13.1", + "phf_shared 0.13.1", + "serde", +] + [[package]] name = "phf_codegen" version = "0.8.0" @@ -2600,6 +3317,16 @@ dependencies = [ "phf_shared 0.11.3", ] +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", +] + [[package]] name = "phf_generator" version = "0.8.0" @@ -2617,7 +3344,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" dependencies = [ "phf_shared 0.10.0", - "rand 0.8.5", + "rand 0.8.6", ] [[package]] @@ -2627,7 +3354,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared 0.11.3", - "rand 0.8.5", + "rand 0.8.6", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared 0.13.1", ] [[package]] @@ -2654,7 +3391,20 @@ dependencies = [ "phf_shared 0.11.3", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] @@ -2681,36 +3431,50 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" dependencies = [ - "siphasher 1.0.1", + "siphasher 1.0.2", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher 1.0.2", ] [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] -name = "pin-utils" -version = "0.1.0" +name = "piper" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "plist" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" +checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1" dependencies = [ "base64 0.22.1", - "indexmap 2.11.4", - "quick-xml 0.38.3", + "indexmap 2.14.0", + "quick-xml", "serde", "time", ] @@ -2728,11 +3492,47 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.11.1", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "pom" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c972d8f86e943ad532d0b04e8965a749ad1d18bb981a9c7b3ae72fe7fd7744b" +dependencies = [ + "bstr", +] + [[package]] name = "potential_utf" -version = "0.1.3" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ "zerovec", ] @@ -2758,6 +3558,28 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[package]] +name = "printpdf" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c30a4cc87c3ca9a98f4970db158a7153f8d1ec8076e005751173c57836380b1d" +dependencies = [ + "js-sys", + "lopdf 0.31.0", + "owned_ttf_parser", + "time", +] + [[package]] name = "proc-macro-crate" version = "1.3.1" @@ -2780,11 +3602,11 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit 0.23.6", + "toml_edit 0.25.11+spec-1.1.0", ] [[package]] @@ -2819,36 +3641,67 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" [[package]] name = "proc-macro2" -version = "1.0.101" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] -name = "quick-xml" -version = "0.37.5" +name = "profiling" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" dependencies = [ - "memchr", + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4488a4a36b9a4ba6b9334a32a39971f77c1436ec82c38707bce707699cc3bbcb" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "pxfm" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f" + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", ] +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quick-xml" -version = "0.38.3" +version = "0.39.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89" +checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d" dependencies = [ "memchr", ] [[package]] name = "quote" -version = "1.0.41" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -2859,6 +3712,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rand" version = "0.7.3" @@ -2875,9 +3734,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", "rand_chacha 0.3.1", @@ -2886,12 +3745,12 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ "rand_chacha 0.9.0", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -2921,7 +3780,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -2939,16 +3798,16 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", ] [[package]] name = "rand_core" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", ] [[package]] @@ -2969,19 +3828,95 @@ dependencies = [ "rand_core 0.5.1", ] +[[package]] +name = "rangemap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "973443cf09a9c8656b574a866ab68dfa19f0867d0340648c7d2f6a71b8a8ea68" + +[[package]] +name = "rav1e" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" +dependencies = [ + "aligned-vec", + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av-scenechange", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "paste", + "profiling", + "rand 0.9.4", + "rand_chacha 0.9.0", + "simd_helpers", + "thiserror 2.0.18", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", +] + [[package]] name = "raw-window-handle" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.1", ] [[package]] @@ -2990,9 +3925,9 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", "libredox", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -3012,14 +3947,14 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "regex" -version = "1.11.3" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -3029,9 +3964,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.11" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -3040,15 +3975,15 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.6" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "reqwest" -version = "0.12.23" +version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64 0.22.1", "bytes", @@ -3094,17 +4029,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed" dependencies = [ "ashpd", - "block2 0.6.2", + "block2", "dispatch2", "glib-sys", "gobject-sys", "gtk-sys", "js-sys", "log", - "objc2 0.6.3", + "objc2", "objc2-app-kit", "objc2-core-foundation", - "objc2-foundation 0.3.2", + "objc2-foundation", "raw-window-handle", "wasm-bindgen", "wasm-bindgen-futures", @@ -3112,6 +4047,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "rgb" +version = "0.8.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" + [[package]] name = "ring" version = "0.17.14" @@ -3120,17 +4061,17 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.16", + "getrandom 0.2.17", "libc", "untrusted", "windows-sys 0.52.0", ] [[package]] -name = "rustc-demangle" -version = "0.1.26" +name = "rustc-hash" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" [[package]] name = "rustc_version" @@ -3143,11 +4084,11 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.1", "errno", "libc", "linux-raw-sys", @@ -3156,9 +4097,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.32" +version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ "once_cell", "rustls-pki-types", @@ -3169,18 +4110,18 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.12.0" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ "zeroize", ] [[package]] name = "rustls-webpki" -version = "0.103.7" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ "ring", "rustls-pki-types", @@ -3195,9 +4136,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "same-file" @@ -3210,9 +4151,9 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ "windows-sys 0.61.2", ] @@ -3246,9 +4187,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.0.4" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" dependencies = [ "dyn-clone", "ref-cast", @@ -3265,7 +4206,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -3282,12 +4223,12 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "security-framework" -version = "2.11.1" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.9.4", - "core-foundation 0.9.4", + "bitflags 2.11.1", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -3295,9 +4236,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.15.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ "core-foundation-sys", "libc", @@ -3310,22 +4251,41 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416" dependencies = [ "bitflags 1.3.2", - "cssparser", - "derive_more", + "cssparser 0.29.6", + "derive_more 0.99.20", "fxhash", "log", "phf 0.8.0", "phf_codegen 0.8.0", "precomputed-hash", - "servo_arc", + "servo_arc 0.2.0", + "smallvec", +] + +[[package]] +name = "selectors" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" +dependencies = [ + "bitflags 2.11.1", + "cssparser 0.36.0", + "derive_more 2.1.1", + "log", + "new_debug_unreachable", + "phf 0.13.1", + "phf_codegen 0.13.1", + "precomputed-hash", + "rustc-hash", + "servo_arc 0.4.3", "smallvec", ] [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" dependencies = [ "serde", "serde_core", @@ -3370,7 +4330,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -3381,20 +4341,20 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", ] [[package]] @@ -3405,7 +4365,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -3419,9 +4379,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.2" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5417783452c2be558477e104686f7de5dae53dba813c28435e0e70f82d9b04ee" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ "serde_core", ] @@ -3440,17 +4400,17 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.15.0" +version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6093cd8c01b25262b84927e0f7151692158fab02d961e04c979d3903eba7ecc5" +checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" dependencies = [ "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.11.4", + "indexmap 2.14.0", "schemars 0.9.0", - "schemars 1.0.4", + "schemars 1.2.1", "serde_core", "serde_json", "serde_with_macros", @@ -3459,21 +4419,21 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.15.0" +version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7e6c180db0816026a61afa1cff5344fb7ebded7e4d3062772179f2501481c27" +checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "serialize-to-javascript" -version = "0.1.2" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +checksum = "c9823f2d3b6a81d98228151fdeaf848206a7855a7a042bbf9bf870449a66cafb" dependencies = [ "serde", "serde_json", @@ -3482,13 +4442,13 @@ dependencies = [ [[package]] name = "serialize-to-javascript-impl" -version = "0.1.2" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +checksum = "74064874e9f6a15f04c1f3cb627902d0e6b410abbf36668afa873c61889f1763" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 1.0.109", ] [[package]] @@ -3501,6 +4461,15 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + [[package]] name = "sha2" version = "0.10.9" @@ -3520,18 +4489,28 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.6" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] [[package]] name = "simd-adler32" -version = "0.3.7" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "simd_helpers" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] [[package]] name = "siphasher" @@ -3541,15 +4520,15 @@ checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" [[package]] name = "siphasher" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" @@ -3559,34 +4538,34 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.6.0" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "softbuffer" -version = "0.4.6" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18051cdd562e792cad055119e0cdb2cfc137e44e3987532e0f9659a77931bb08" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" dependencies = [ "bytemuck", - "cfg_aliases", - "core-graphics", - "foreign-types 0.5.0", "js-sys", - "log", - "objc2 0.5.2", - "objc2-foundation 0.2.2", - "objc2-quartz-core 0.2.2", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", "raw-window-handle", "redox_syscall", + "tracing", "wasm-bindgen", "web-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3617,15 +4596,9 @@ dependencies = [ [[package]] name = "stable_deref_trait" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" - -[[package]] -name = "static_assertions" -version = "1.1.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "string_cache" @@ -3640,6 +4613,18 @@ dependencies = [ "serde", ] +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.13.1", + "precomputed-hash", +] + [[package]] name = "string_cache_codegen" version = "0.5.4" @@ -3652,6 +4637,18 @@ dependencies = [ "quote", ] +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", + "proc-macro2", + "quote", +] + [[package]] name = "strsim" version = "0.11.1" @@ -3688,9 +4685,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.106" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -3714,16 +4711,16 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "system-configuration" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.1", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -3753,35 +4750,33 @@ dependencies = [ [[package]] name = "tao" -version = "0.34.3" +version = "0.34.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "959469667dbcea91e5485fc48ba7dd6023face91bb0f1a14681a70f99847c3f7" +checksum = "9103edf55f2da3c82aea4c7fab7c4241032bfeea0e71fa557d98e00e7ce7cc20" dependencies = [ - "bitflags 2.9.4", - "block2 0.6.2", + "bitflags 2.11.1", + "block2", "core-foundation 0.10.1", "core-graphics", "crossbeam-channel", - "dispatch", + "dispatch2", "dlopen2", "dpi", "gdkwayland-sys", "gdkx11-sys", "gtk", "jni", - "lazy_static", "libc", "log", "ndk", "ndk-context", "ndk-sys", - "objc2 0.6.3", + "objc2", "objc2-app-kit", - "objc2-foundation 0.3.2", + "objc2-foundation", "once_cell", "parking_lot", "raw-window-handle", - "scopeguard", "tao-macros", "unicode-segmentation", "url", @@ -3799,7 +4794,7 @@ checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -3810,31 +4805,30 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tauri" -version = "2.8.5" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d1d3b3dc4c101ac989fd7db77e045cc6d91a25349cd410455cb5c57d510c1c" +checksum = "2f7a0f4019c80391d143ee26cd7cd1ed271ac241d3087d333f99f3269ba90812" dependencies = [ "anyhow", "bytes", - "cookie", "dirs", "dunce", "embed_plist", - "getrandom 0.3.3", + "getrandom 0.2.17", "glob", "gtk", "heck 0.5.0", "http", + "http-range", "jni", "libc", "log", "mime", "muda", - "objc2 0.6.3", + "objc2", "objc2-app-kit", - "objc2-foundation 0.3.2", + "objc2-foundation", "objc2-ui-kit", - "objc2-web-kit", "percent-encoding", "plist", "raw-window-handle", @@ -3849,7 +4843,7 @@ dependencies = [ "tauri-runtime", "tauri-runtime-wry", "tauri-utils", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tray-icon", "url", @@ -3862,9 +4856,9 @@ dependencies = [ [[package]] name = "tauri-build" -version = "2.4.1" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c432ccc9ff661803dab74c6cd78de11026a578a9307610bbc39d3c55be7943f" +checksum = "be9aa8c59a894f76c29a002501c589de5eb4987a5913d62a6e0a47f320901988" dependencies = [ "anyhow", "cargo_toml", @@ -3878,31 +4872,30 @@ dependencies = [ "serde_json", "tauri-utils", "tauri-winres", - "toml 0.9.7", "walkdir", ] [[package]] name = "tauri-codegen" -version = "2.4.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ab3a62cf2e6253936a8b267c2e95839674e7439f104fa96ad0025e149d54d8a" +checksum = "d3e4e8230d565106aa19dfbaa01a7ed01abf78047fe0577a83377224bd1bf20e" dependencies = [ "base64 0.22.1", "brotli", "ico", "json-patch", "plist", - "png", + "png 0.17.16", "proc-macro2", "quote", "semver", "serde", "serde_json", "sha2", - "syn 2.0.106", + "syn 2.0.117", "tauri-utils", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", "url", "uuid", @@ -3911,23 +4904,23 @@ dependencies = [ [[package]] name = "tauri-macros" -version = "2.4.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4368ea8094e7045217edb690f493b55b30caf9f3e61f79b4c24b6db91f07995e" +checksum = "bc8de2cddbbc33dbdf4c84f170121886595efdbcc9cb4b3d76342b79d082cedc" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", "tauri-codegen", "tauri-utils", ] [[package]] name = "tauri-plugin" -version = "2.4.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9946a3cede302eac0c6eb6c6070ac47b1768e326092d32efbb91f21ed58d978f" +checksum = "f8d5f58bfd0cdcfdbc0a68dc08b354eea2afc551b421de91b07b69e0dd769d57" dependencies = [ "anyhow", "glob", @@ -3936,15 +4929,14 @@ dependencies = [ "serde", "serde_json", "tauri-utils", - "toml 0.9.7", "walkdir", ] [[package]] name = "tauri-plugin-dialog" -version = "2.4.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0beee42a4002bc695550599b011728d9dfabf82f767f134754ed6655e434824e" +checksum = "1aefb14219b492afb30b12647b5b1247cadd2c0603467310c36e0f7ae1698c28" dependencies = [ "log", "raw-window-handle", @@ -3954,15 +4946,15 @@ dependencies = [ "tauri", "tauri-plugin", "tauri-plugin-fs", - "thiserror 2.0.17", + "thiserror 2.0.18", "url", ] [[package]] name = "tauri-plugin-fs" -version = "2.4.2" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "315784ec4be45e90a987687bae7235e6be3d6e9e350d2b75c16b8a4bf22c1db7" +checksum = "c341290d31991dbca38b31d412c73dfbdb070bb11536784f19dd2211d13b778f" dependencies = [ "anyhow", "dunce", @@ -3975,30 +4967,52 @@ dependencies = [ "tauri", "tauri-plugin", "tauri-utils", - "thiserror 2.0.17", - "toml 0.9.7", + "thiserror 2.0.18", + "toml 0.8.2", + "url", +] + +[[package]] +name = "tauri-plugin-opener" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecee219f11cdac713ab32959db5d0cceec4810ba4f4458da992292ecf9660321" +dependencies = [ + "dunce", + "glob", + "objc2-app-kit", + "objc2-foundation", + "open", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", "url", + "windows", + "zbus", ] [[package]] name = "tauri-runtime" -version = "2.8.0" +version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4cfc9ad45b487d3fded5a4731a567872a4812e9552e3964161b08edabf93846" +checksum = "87f766fe9f3d1efc4b59b17e7a891ad5ed195fa8d23582abb02e6c9a01137892" dependencies = [ "cookie", "dpi", "gtk", "http", "jni", - "objc2 0.6.3", + "objc2", "objc2-ui-kit", "objc2-web-kit", "raw-window-handle", "serde", "serde_json", "tauri-utils", - "thiserror 2.0.17", + "thiserror 2.0.18", "url", "webkit2gtk", "webview2-com", @@ -4007,17 +5021,17 @@ dependencies = [ [[package]] name = "tauri-runtime-wry" -version = "2.8.1" +version = "2.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1fe9d48bd122ff002064e88cfcd7027090d789c4302714e68fcccba0f4b7807" +checksum = "187a3f26f681bdf028f796ccf57cf478c1ee422c50128e5a0a6ebeb3f5910065" dependencies = [ "gtk", "http", "jni", "log", - "objc2 0.6.3", + "objc2", "objc2-app-kit", - "objc2-foundation 0.3.2", + "objc2-foundation", "once_cell", "percent-encoding", "raw-window-handle", @@ -4034,17 +5048,18 @@ dependencies = [ [[package]] name = "tauri-utils" -version = "2.7.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41a3852fdf9a4f8fbeaa63dc3e9a85284dd6ef7200751f0bd66ceee30c93f212" +checksum = "55f61d2bf7188fbcf2b0ed095b67a6bc498f713c939314bb19eb700118a573b7" dependencies = [ "anyhow", "brotli", "cargo_metadata", "ctor", + "dom_query", "dunce", "glob", - "html5ever", + "html5ever 0.29.1", "http", "infer", "json-patch", @@ -4052,6 +5067,7 @@ dependencies = [ "log", "memchr", "phf 0.11.3", + "plist", "proc-macro2", "quote", "regex", @@ -4062,8 +5078,8 @@ dependencies = [ "serde_json", "serde_with", "swift-rs", - "thiserror 2.0.17", - "toml 0.9.7", + "thiserror 2.0.18", + "toml 1.1.2+spec-1.1.0", "url", "urlpattern", "uuid", @@ -4072,22 +5088,23 @@ dependencies = [ [[package]] name = "tauri-winres" -version = "0.3.3" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd21509dd1fa9bd355dc29894a6ff10635880732396aa38c0066c1e6c1ab8074" +checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" dependencies = [ + "dunce", "embed-resource", - "toml 0.9.7", + "toml 1.1.2+spec-1.1.0", ] [[package]] name = "tempfile" -version = "3.23.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.3", + "getrandom 0.4.2", "once_cell", "rustix", "windows-sys 0.61.2", @@ -4104,6 +5121,16 @@ dependencies = [ "utf-8", ] +[[package]] +name = "tendril" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24" +dependencies = [ + "new_debug_unreachable", + "utf-8", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -4115,11 +5142,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.17", + "thiserror-impl 2.0.18", ] [[package]] @@ -4130,46 +5157,60 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", +] + +[[package]] +name = "tiff" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg", ] [[package]] name = "time" -version = "0.3.44" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", "num-conv", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.24" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", @@ -4177,9 +5218,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "zerovec", @@ -4187,34 +5228,31 @@ dependencies = [ [[package]] name = "tokio" -version = "1.47.1" +version = "1.52.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" dependencies = [ - "backtrace", "bytes", - "io-uring", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", - "slab", "socket2", "tokio-macros", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -4239,9 +5277,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.16" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", @@ -4264,17 +5302,32 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.7" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00e5e5d9bf2475ac9d4f0d9edab68cc573dc2fd644b0dba36b0c30a92dd9eaa0" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" dependencies = [ - "indexmap 2.11.4", + "indexmap 2.14.0", "serde_core", - "serde_spanned 1.0.2", - "toml_datetime 0.7.2", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", "toml_writer", - "winnow 0.7.13", + "winnow 1.0.2", ] [[package]] @@ -4288,9 +5341,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.2" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" dependencies = [ "serde_core", ] @@ -4301,7 +5363,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.11.4", + "indexmap 2.14.0", "toml_datetime 0.6.3", "winnow 0.5.40", ] @@ -4312,7 +5374,7 @@ version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" dependencies = [ - "indexmap 2.11.4", + "indexmap 2.14.0", "serde", "serde_spanned 0.6.9", "toml_datetime 0.6.3", @@ -4321,36 +5383,36 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.23.6" +version = "0.25.11+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3effe7c0e86fdff4f69cdd2ccc1b96f933e24811c5441d44904e8683e27184b" +checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" dependencies = [ - "indexmap 2.11.4", - "toml_datetime 0.7.2", + "indexmap 2.14.0", + "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", - "winnow 0.7.13", + "winnow 1.0.2", ] [[package]] name = "toml_parser" -version = "1.0.3" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow 0.7.13", + "winnow 1.0.2", ] [[package]] name = "toml_writer" -version = "1.0.3" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d163a63c116ce562a22cda521fcc4d79152e7aba014456fb5eb442f6d6a10109" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" [[package]] name = "tower" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", @@ -4363,11 +5425,11 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.6" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.1", "bytes", "futures-util", "http", @@ -4393,9 +5455,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -4404,43 +5466,43 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", ] [[package]] name = "tray-icon" -version = "0.21.1" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0d92153331e7d02ec09137538996a7786fe679c629c279e82a6be762b7e6fe2" +checksum = "9f7eee98ec5c90daf179d55c20a49d8c0d043054ce7c26336c09a24d31f14fa0" dependencies = [ "crossbeam-channel", "dirs", "libappindicator", "muda", - "objc2 0.6.3", + "objc2", "objc2-app-kit", "objc2-core-foundation", "objc2-core-graphics", - "objc2-foundation 0.3.2", + "objc2-foundation", "once_cell", - "png", + "png 0.17.16", "serde", - "thiserror 2.0.17", + "thiserror 2.0.18", "windows-sys 0.59.0", ] @@ -4450,6 +5512,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "ttf-parser" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49d64318d8311fc2668e48b63969f4343e0a85c4a109aa8460d6672e364b8bd1" + [[package]] name = "typeid" version = "1.0.3" @@ -4458,19 +5526,19 @@ checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] name = "uds_windows" -version = "1.1.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "winapi", + "windows-sys 0.61.2", ] [[package]] @@ -4516,15 +5584,21 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.19" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" -version = "1.12.0" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" + +[[package]] +name = "unicode-xid" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "untrusted" @@ -4534,14 +5608,15 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.7" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", "percent-encoding", "serde", + "serde_derive", ] [[package]] @@ -4570,13 +5645,24 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.18.1" +version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.4.2", "js-sys", - "serde", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "v_frame" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" +dependencies = [ + "aligned-vec", + "num-traits", "wasm-bindgen", ] @@ -4588,9 +5674,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version-compare" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" [[package]] name = "version_check" @@ -4650,28 +5736,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasi" -version = "0.14.7+wasi-0.2.4" +name = "wasip2" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "wasip2", + "wit-bindgen 0.57.1", ] [[package]] -name = "wasip2" -version = "1.0.1+wasi-0.2.4" +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.51.0", ] [[package]] name = "wasm-bindgen" -version = "0.2.104" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" +checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" dependencies = [ "cfg-if", "once_cell", @@ -4680,38 +5766,21 @@ dependencies = [ "wasm-bindgen-shared", ] -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn 2.0.106", - "wasm-bindgen-shared", -] - [[package]] name = "wasm-bindgen-futures" -version = "0.4.54" +version = "0.4.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" +checksum = "af934872acec734c2d80e6617bbb5ff4f12b052dd8e6332b0817bce889516084" dependencies = [ - "cfg-if", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.104" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" +checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4719,26 +5788,48 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.104" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" +checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" dependencies = [ + "bumpalo", "proc-macro2", "quote", - "syn 2.0.106", - "wasm-bindgen-backend", + "syn 2.0.117", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.104" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" +checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.14.0", + "wasm-encoder", + "wasmparser", +] + [[package]] name = "wasm-streams" version = "0.4.2" @@ -4752,11 +5843,23 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.1", + "hashbrown 0.15.5", + "indexmap 2.14.0", + "semver", +] + [[package]] name = "wayland-backend" -version = "0.3.11" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673a33c33048a5ade91a6b139580fa174e19fb0d23f396dca9fa15f2e1e49b35" +checksum = "2857dd20b54e916ec7253b3d6b4d5c4d7d4ca2c33c2e11c6c76a99bd8744755d" dependencies = [ "cc", "downcast-rs", @@ -4768,11 +5871,11 @@ dependencies = [ [[package]] name = "wayland-client" -version = "0.31.11" +version = "0.31.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d" +checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.1", "rustix", "wayland-backend", "wayland-scanner", @@ -4780,11 +5883,11 @@ dependencies = [ [[package]] name = "wayland-protocols" -version = "0.32.9" +version = "0.32.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901" +checksum = "563a85523cade2429938e790815fd7319062103b9f4a2dc806e9b53b95982d8f" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.1", "wayland-backend", "wayland-client", "wayland-scanner", @@ -4792,20 +5895,20 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.31.7" +version = "0.31.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3" +checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a" dependencies = [ "proc-macro2", - "quick-xml 0.37.5", + "quick-xml", "quote", ] [[package]] name = "wayland-sys" -version = "0.31.7" +version = "0.31.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34949b42822155826b41db8e5d0c1be3a2bd296c747577a43a3e6daefc296142" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" dependencies = [ "dlib", "log", @@ -4814,14 +5917,26 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.81" +version = "0.3.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" +checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602" dependencies = [ "js-sys", "wasm-bindgen", ] +[[package]] +name = "web_atoms" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7cff6eef815df1834fd250e3a2ff436044d82a9f1bc1980ca1dbdf07effc538" +dependencies = [ + "phf 0.13.1", + "phf_codegen 0.13.1", + "string_cache 0.9.0", + "string_cache_codegen 0.6.1", +] + [[package]] name = "webkit2gtk" version = "2.0.1" @@ -4868,9 +5983,9 @@ dependencies = [ [[package]] name = "webview2-com" -version = "0.38.0" +version = "0.38.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4ba622a989277ef3886dd5afb3e280e3dd6d974b766118950a08f8f678ad6a4" +checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" dependencies = [ "webview2-com-macros", "webview2-com-sys", @@ -4882,26 +5997,32 @@ dependencies = [ [[package]] name = "webview2-com-macros" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d228f15bba3b9d56dde8bddbee66fa24545bd17b48d5128ccf4a8742b18e431" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "webview2-com-sys" -version = "0.38.0" +version = "0.38.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36695906a1b53a3bf5c4289621efedac12b73eeb0b89e7e1a89b517302d5d75c" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" dependencies = [ - "thiserror 2.0.17", + "thiserror 2.0.18", "windows", "windows-core 0.61.2", ] +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + [[package]] name = "winapi" version = "0.3.9" @@ -4939,10 +6060,10 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" dependencies = [ - "objc2 0.6.3", + "objc2", "objc2-app-kit", "objc2-core-foundation", - "objc2-foundation 0.3.2", + "objc2-foundation", "raw-window-handle", "windows-sys 0.59.0", "windows-version", @@ -5015,7 +6136,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -5026,7 +6147,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -5053,13 +6174,13 @@ dependencies = [ [[package]] name = "windows-registry" -version = "0.5.3" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" dependencies = [ - "windows-link 0.1.3", - "windows-result 0.3.4", - "windows-strings 0.4.2", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", ] [[package]] @@ -5125,15 +6246,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - [[package]] name = "windows-sys" version = "0.61.2" @@ -5167,30 +6279,13 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", + "windows_i686_gnullvm", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link 0.2.1", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - [[package]] name = "windows-threading" version = "0.1.0" @@ -5221,12 +6316,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -5239,12 +6328,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -5257,24 +6340,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -5287,12 +6358,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -5305,12 +6370,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -5323,12 +6382,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -5341,12 +6394,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - [[package]] name = "winnow" version = "0.5.40" @@ -5358,9 +6405,15 @@ dependencies = [ [[package]] name = "winnow" -version = "0.7.13" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" dependencies = [ "memchr", ] @@ -5377,24 +6430,112 @@ dependencies = [ [[package]] name = "wit-bindgen" -version = "0.46.0" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap 2.14.0", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.1", + "indexmap 2.14.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.14.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] [[package]] name = "writeable" -version = "0.6.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "wry" -version = "0.53.4" +version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d78ec082b80fa088569a970d043bb3050abaabf4454101d44514ee8d9a8c9f6" +checksum = "728b7d4c8ec8d81cab295e0b5b8a4c263c0d41a785fb8f8c4df284e5411140a2" dependencies = [ "base64 0.22.1", - "block2 0.6.2", + "block2", "cookie", "crossbeam-channel", "dirs", @@ -5402,17 +6543,17 @@ dependencies = [ "dunce", "gdkx11", "gtk", - "html5ever", + "html5ever 0.29.1", "http", "javascriptcore-rs", "jni", "kuchikiki", "libc", "ndk", - "objc2 0.6.3", + "objc2", "objc2-app-kit", "objc2-core-foundation", - "objc2-foundation 0.3.2", + "objc2-foundation", "objc2-ui-kit", "objc2-web-kit", "once_cell", @@ -5421,7 +6562,7 @@ dependencies = [ "sha2", "soup3", "tao-macros", - "thiserror 2.0.17", + "thiserror 2.0.18", "url", "webkit2gtk", "webkit2gtk-sys", @@ -5454,12 +6595,17 @@ dependencies = [ ] [[package]] -name = "yoke" +name = "y4m" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -5467,39 +6613,47 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", "synstructure", ] [[package]] name = "zbus" -version = "5.11.0" +version = "5.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d07e46d035fb8e375b2ce63ba4e4ff90a7f73cf2ffb0138b29e1158d2eaadf7" +checksum = "c3bcbf15c8708d7fc1be0c993622e0a5cbd5e8b52bfa40afa4c3e0cd8d724ac1" dependencies = [ "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", "async-recursion", + "async-task", "async-trait", + "blocking", "enumflags2", "event-listener", "futures-core", "futures-lite", "hex", - "nix", + "libc", "ordered-stream", + "rustix", "serde", "serde_repr", "tokio", "tracing", "uds_windows", - "windows-sys 0.60.2", - "winnow 0.7.13", + "uuid", + "windows-sys 0.61.2", + "winnow 1.0.2", "zbus_macros", "zbus_names", "zvariant", @@ -5507,14 +6661,14 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "5.11.0" +version = "5.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57e797a9c847ed3ccc5b6254e8bcce056494b375b511b3d6edcec0aeb4defaca" +checksum = "51fa5406ad9175a8c825a931f8cf347116b531b3634fcb0b627c290f1f2516ff" dependencies = [ - "proc-macro-crate 3.4.0", + "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", "zbus_names", "zvariant", "zvariant_utils", @@ -5522,54 +6676,53 @@ dependencies = [ [[package]] name = "zbus_names" -version = "4.2.0" +version = "4.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" +checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d" dependencies = [ "serde", - "static_assertions", - "winnow 0.7.13", + "winnow 1.0.2", "zvariant", ] [[package]] name = "zerocopy" -version = "0.8.27" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.27" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "zerofrom" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", "synstructure", ] @@ -5581,9 +6734,9 @@ checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zerotrie" -version = "0.2.2" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", @@ -5592,9 +6745,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.4" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "yoke", "zerofrom", @@ -5603,52 +6756,82 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.1" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core", ] [[package]] name = "zvariant" -version = "5.7.0" +version = "5.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "999dd3be73c52b1fccd109a4a81e4fcd20fab1d3599c8121b38d04e1419498db" +checksum = "c4db0ecb8987cf5e92653c57c098f7f0e39a03112edb796f4fe089fb7eaa14ff" dependencies = [ "endi", "enumflags2", "serde", "url", - "winnow 0.7.13", + "winnow 1.0.2", "zvariant_derive", "zvariant_utils", ] [[package]] name = "zvariant_derive" -version = "5.7.0" +version = "5.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6643fd0b26a46d226bd90d3f07c1b5321fe9bb7f04673cb37ac6d6883885b68e" +checksum = "5b949b639ab1b4bed763aa7481ba0e368af68d8b55532f8ed4bec86a59f2ca98" dependencies = [ - "proc-macro-crate 3.4.0", + "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", "zvariant_utils", ] [[package]] name = "zvariant_utils" -version = "3.2.1" +version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6949d142f89f6916deca2232cf26a8afacf2b9fdc35ce766105e104478be599" +checksum = "6d464f5733ffa07a3164d656f18533caace9d0638596721355d73256a410d691" dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.106", - "winnow 0.7.13", + "syn 2.0.117", + "winnow 1.0.2", ] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 6d97cf29..73384a64 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,22 +1,32 @@ [package] name = "onedocs" -version = "1.2.0" +version = "1.4.0" description = "A Single Text, All is Known." -authors = ["you"] -edition = "2021" +authors = ["LYOfficial"] +edition = "2024" + +[lib] +name = "onedocs_lib" +crate-type = ["staticlib", "cdylib", "lib"] [build-dependencies] -tauri-build = { version = "2.0.0-beta.21", features = [] } +tauri-build = { version = "=2.6.0", features = [] } [dependencies] -tauri = { version = "2.0.0-beta.21", features = [] } -tauri-plugin-dialog = "2.0.0-beta" -tauri-plugin-fs = "2.0.0-beta" +tauri = { version = "=2.6.0", features = ["protocol-asset"] } +tauri-runtime = "=2.9.2" +tauri-plugin-dialog = "=2.3.0" +tauri-plugin-fs = "=2.4.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" reqwest = { version = "0.12", features = ["json"] } tokio = { version = "1", features = ["full"] } anyhow = "1.0" +lopdf = "0.34" +tauri-plugin-opener = "2" +image = "0.25" +base64 = "0.22" +printpdf = "0.7" [features] custom-protocol = ["tauri/custom-protocol"] diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index b9ec4c1f..4c0cc7f8 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -4,6 +4,28 @@ "description": "Capability for the main window", "windows": ["main"], "permissions": [ - "core:default" + "core:default", + "core:window:allow-minimize", + "core:window:allow-maximize", + "core:window:allow-close", + "core:window:allow-start-dragging", + "core:window:allow-toggle-maximize", + "dialog:allow-open", + "dialog:allow-save", + "fs:allow-write-text-file", + "fs:scope-home-recursive", + "fs:scope-desktop-recursive", + "fs:scope-document-recursive", + "fs:scope-download-recursive", + "fs:allow-mkdir", + "fs:allow-appdata-write-recursive", + "fs:allow-appdata-read-recursive", + "fs:allow-temp-write-recursive", + "fs:allow-resource-write-recursive", + "fs:allow-resource-read-recursive", + "fs:allow-read", + "fs:allow-read-file", + "fs:allow-exists", + "opener:default" ] } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 12435e23..2187e233 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,11 +1,35 @@ use anyhow::Result; +use base64::Engine; use reqwest; use serde::{Deserialize, Serialize}; +use std::fs; +use std::path::Path; -#[derive(Debug, Serialize, Deserialize)] +/// Content part for multimodal messages (text or image) +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(untagged)] +enum ContentPart { + Text { r#type: String, text: String }, + ImageUrl { r#type: String, image_url: ImageUrlContent }, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +struct ImageUrlContent { + url: String, +} + +/// Message content can be a simple string or an array of content parts +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(untagged)] +enum MessageContent { + Text(String), + Parts(Vec), +} + +#[derive(Debug, Serialize, Deserialize, Clone)] struct ChatMessage { role: String, - content: String, + content: MessageContent, } #[derive(Debug, Serialize, Deserialize)] @@ -14,11 +38,18 @@ struct ChatRequest { messages: Vec, max_tokens: Option, temperature: Option, + stream: Option, } #[derive(Debug, Serialize, Deserialize)] struct ChatChoice { - message: ChatMessage, + message: ChatMessageResponse, +} + +#[derive(Debug, Serialize, Deserialize)] +struct ChatMessageResponse { + role: String, + content: Option, } #[derive(Debug, Serialize, Deserialize)] @@ -26,6 +57,19 @@ struct ChatResponse { choices: Vec, } +#[derive(Debug, Serialize, Deserialize)] +struct ExtractedImage { + page_number: u32, + file_name: String, + local_path: String, +} + +#[derive(Debug, Serialize, Deserialize)] +struct ExtractImagesResult { + images: Vec, +} + +/// Analyze content using LLM API, with optional image support #[tauri::command] async fn analyze_content_rust( api_key: String, @@ -33,27 +77,81 @@ async fn analyze_content_rust( system_prompt: String, text_content: String, model: String, + images: Option>, ) -> Result { - // 构造请求消息 - let messages = vec![ - ChatMessage { - role: "system".to_string(), - content: system_prompt, - }, + let system_message = ChatMessage { + role: "system".to_string(), + content: MessageContent::Text(system_prompt), + }; + + // Build user message - if images are provided, use multimodal format + let user_message = if let Some(ref image_list) = images { + if image_list.is_empty() { + ChatMessage { + role: "user".to_string(), + content: MessageContent::Text(text_content), + } + } else { + let mut parts: Vec = vec![ContentPart::Text { + r#type: "text".to_string(), + text: text_content, + }]; + + for image_url in image_list { + // Support both base64 data URLs and regular URLs + let url = if image_url.starts_with("data:") || image_url.starts_with("http") { + image_url.clone() + } else { + // Treat as file path - read and convert to base64 data URL + match fs::read(image_url) { + Ok(bytes) => { + let base64_str = base64_encode(&bytes); + let ext = Path::new(image_url) + .extension() + .and_then(|e| e.to_str()) + .unwrap_or("png"); + let mime = match ext { + "jpg" | "jpeg" => "image/jpeg", + "png" => "image/png", + "gif" => "image/gif", + "webp" => "image/webp", + "bmp" => "image/bmp", + _ => "image/png", + }; + format!("data:{};base64,{}", mime, base64_str) + } + Err(_) => continue, // Skip unreadable images + } + }; + + parts.push(ContentPart::ImageUrl { + r#type: "image_url".to_string(), + image_url: ImageUrlContent { url }, + }); + } + + ChatMessage { + role: "user".to_string(), + content: MessageContent::Parts(parts), + } + } + } else { ChatMessage { role: "user".to_string(), - content: text_content, - }, - ]; + content: MessageContent::Text(text_content), + } + }; + + let messages = vec![system_message, user_message]; let chat_request = ChatRequest { model, messages, max_tokens: Some(4000), temperature: Some(0.7), + stream: Some(false), }; - // 发送 HTTP 请求 let client = reqwest::Client::new(); let url = format!("{}/chat/completions", api_base_url.trim_end_matches('/')); @@ -61,6 +159,10 @@ async fn analyze_content_rust( .post(&url) .header("Authorization", format!("Bearer {}", api_key)) .header("Content-Type", "application/json") + .header( + "User-Agent", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36", + ) .json(&chat_request) .send() .await @@ -72,22 +174,391 @@ async fn analyze_content_rust( return Err(format!("API 请求失败 {}: {}", status, error_text)); } - let chat_response: ChatResponse = response - .json() + let response_text = response + .text() .await - .map_err(|e| format!("解析响应失败: {}", e))?; + .map_err(|e| format!("读取响应失败: {}", e))?; + + let chat_response: ChatResponse = serde_json::from_str(&response_text) + .map_err(|e| format!("解析响应失败: {}。响应内容: {}", e, response_text))?; if chat_response.choices.is_empty() { return Err("API 返回空响应".to_string()); } - Ok(chat_response.choices[0].message.content.clone()) + let content = chat_response.choices[0].message.content.clone(); + match content { + Some(text) if !text.is_empty() => Ok(text), + _ => Err("模型返回空内容(可能仅包含思考过程,未生成可见输出)".to_string()), + } } -// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ +/// Extract images from a PDF file using lopdf. #[tauri::command] -fn greet(name: &str) -> String { - format!("Hello, {}! You've been greeted from Rust!", name) +async fn extract_pdf_images( + pdf_path: String, + output_dir: String, + base_name: String, +) -> Result { + let pdf_file_path = Path::new(&pdf_path); + if !pdf_file_path.exists() { + return Err(format!("PDF 文件不存在: {}", pdf_path)); + } + + // Ensure output directory exists + fs::create_dir_all(&output_dir) + .map_err(|e| format!("创建输出目录失败: {}", e))?; + + let mut extracted_images: Vec = Vec::new(); + + // Use lopdf for local extraction + eprintln!("使用 lopdf 本地提取图片..."); + let pdf_bytes = fs::read(pdf_file_path) + .map_err(|e| format!("读取 PDF 文件失败: {}", e))?; + + let lopdf_doc = lopdf::Document::load_mem(&pdf_bytes) + .map_err(|e| format!("解析 PDF 失败: {}", e))?; + + let mut image_index: u32 = 0; + + let pages = lopdf_doc.get_pages(); + for (page_num, page_id) in pages.iter() { + match lopdf_doc.get_page_images(*page_id) { + Ok(pdf_images) => { + for pdf_image in pdf_images { + let width = pdf_image.width as u32; + let height = pdf_image.height as u32; + + // Skip very small images (likely artifacts like decorative lines) + if width < 10 || height < 10 { + continue; + } + + // Skip very wide/short images (likely header/footer decorations) + if width > 0 && height > 0 && (width as f32 / height as f32) > 15.0 { + continue; + } + + image_index += 1; + + let filter_str = pdf_image + .filters + .as_ref() + .map(|f| f.join(",")) + .unwrap_or_default(); + + let save_result = if filter_str.contains("DCTDecode") { + save_jpeg(&pdf_image.content, &output_dir, &base_name, *page_num, image_index) + } else if filter_str.contains("JPXDecode") { + save_jpeg(&pdf_image.content, &output_dir, &base_name, *page_num, image_index) + } else if filter_str.contains("FlateDecode") { + save_raw_as_png(&pdf_image.content, width, height, pdf_image.color_space.as_deref(), &output_dir, &base_name, *page_num, image_index) + } else { + save_raw_as_png(&pdf_image.content, width, height, pdf_image.color_space.as_deref(), &output_dir, &base_name, *page_num, image_index) + }; + + if let Some(img) = save_result { + extracted_images.push(img); + } + } + } + Err(_) => continue, + } + } + + if !extracted_images.is_empty() { + eprintln!("lopdf 提取到 {} 张图片", extracted_images.len()); + } else { + eprintln!("lopdf 未提取到图片"); + } + + let result = ExtractImagesResult { + images: extracted_images, + }; + + serde_json::to_string(&result) + .map_err(|e| format!("序列化结果失败: {}", e)) +} + +/// Save JPEG/JP2 image data directly to file +fn save_jpeg( + content: &[u8], + output_dir: &str, + base_name: &str, + page_num: u32, + img_index: u32, +) -> Option { + let file_name = format!("{}_page{}_img{:03}.jpg", base_name, page_num, img_index); + let local_path = format!("{}/{}", output_dir.replace('\\', "/"), file_name); + + match fs::write(&local_path, content) { + Ok(()) => Some(ExtractedImage { + page_number: page_num, + file_name, + local_path, + }), + Err(e) => { + eprintln!("保存 JPEG 图片失败: {}", e); + None + } + } +} + +/// Save raw pixel data as PNG using the image crate +fn save_raw_as_png( + content: &[u8], + width: u32, + height: u32, + color_space: Option<&str>, + output_dir: &str, + base_name: &str, + page_num: u32, + img_index: u32, +) -> Option { + let file_name = format!("{}_page{}_img{:03}.png", base_name, page_num, img_index); + let local_path = format!("{}/{}", output_dir.replace('\\', "/"), file_name); + + let cs = color_space.unwrap_or("DeviceGray"); + + // Build an image from raw pixel data + let img = if cs == "DeviceRGB" || cs == "CalRGB" { + image::RgbImage::from_raw(width, height, content.to_vec()) + .map(|buf| image::DynamicImage::ImageRgb8(buf)) + } else if cs == "DeviceGray" || cs == "CalGray" { + // Convert grayscale to RGB for broader compatibility + let rgb_data: Vec = content + .iter() + .take((width * height) as usize) + .flat_map(|&g| [g, g, g]) + .collect(); + image::RgbImage::from_raw(width, height, rgb_data) + .map(|buf| image::DynamicImage::ImageRgb8(buf)) + } else if cs == "DeviceCMYK" { + // Convert CMYK to RGB + let rgb_data = cmyk_to_rgb(content); + image::RgbImage::from_raw(width, height, rgb_data) + .map(|buf| image::DynamicImage::ImageRgb8(buf)) + } else { + // Default: treat as grayscale + let rgb_data: Vec = content + .iter() + .take((width * height) as usize) + .flat_map(|&g| [g, g, g]) + .collect(); + image::RgbImage::from_raw(width, height, rgb_data) + .map(|buf| image::DynamicImage::ImageRgb8(buf)) + }; + + match img { + Some(dynamic_img) => { + match dynamic_img.save_with_format(&local_path, image::ImageFormat::Png) { + Ok(()) => Some(ExtractedImage { + page_number: page_num, + file_name, + local_path, + }), + Err(_) => None, + } + } + None => None, + } +} + +/// Simple base64 encoding (no external crate needed) +/// Base64 encode using the base64 crate +fn base64_encode(data: &[u8]) -> String { + base64::engine::general_purpose::STANDARD.encode(data) +} + +/// Convert CMYK color data to RGB +fn cmyk_to_rgb(cmyk_data: &[u8]) -> Vec { + let mut rgb = Vec::with_capacity(cmyk_data.len() / 4 * 3); + for chunk in cmyk_data.chunks(4) { + if chunk.len() < 4 { + break; + } + let c = chunk[0] as f32 / 255.0; + let m = chunk[1] as f32 / 255.0; + let y = chunk[2] as f32 / 255.0; + let k = chunk[3] as f32 / 255.0; + + let r = ((1.0 - c) * (1.0 - k) * 255.0) as u8; + let g = ((1.0 - m) * (1.0 - k) * 255.0) as u8; + let b = ((1.0 - y) * (1.0 - k) * 255.0) as u8; + + rgb.push(r); + rgb.push(g); + rgb.push(b); + } + rgb +} + +#[tauri::command] +async fn test_model_connection_rust( + api_key: String, + api_base_url: String, + model: String, +) -> Result { + let client = reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(12)) + .build() + .map_err(|e| format!("创建HTTP客户端失败: {}", e))?; + + let request_body = serde_json::json!({ + "model": model, + "messages": [{"role": "user", "content": "hi"}], + "stream": false, + "max_tokens": 1, + "temperature": 0.0, + }); + + let response = client + .post(format!("{}/chat/completions", api_base_url.trim_end_matches('/'))) + .header("Authorization", format!("Bearer {}", api_key)) + .header("Content-Type", "application/json") + .json(&request_body) + .send() + .await + .map_err(|e| format!("请求测试失败: {}", e))?; + + if !response.status().is_success() { + let status_code = response.status(); + let error_text = response.text().await.unwrap_or_else(|_| "Unknown error".to_string()); + return Err(format!("API request failed with status {}: {}", status_code, error_text)); + } + + Ok(true) +} + +/// Save a Markdown text as a simple PDF file. +/// Uses printpdf to generate a basic PDF with the text content. +/// For rich rendering (formulas, images), users should use the frontend export button. +#[tauri::command] +async fn save_markdown_as_pdf( + markdown_content: String, + output_path: String, + title: String, +) -> Result { + use printpdf::*; + + let page_width = Mm(210.0); // A4 + let page_height = Mm(297.0); + let margin_left = Mm(20.0); + let margin_top = Mm(20.0); + let margin_right = Mm(20.0); + let margin_bottom = Mm(20.0); + let font_size = 10.0; + let line_height = 14.0; + let usable_width = page_width - margin_left - margin_right; + let usable_height = page_height - margin_top - margin_bottom; + + let (doc, page1, layer1) = PdfDocument::new( + &title, + page_width, + page_height, + "Layer 1", + ); + let current_layer = doc.get_page(page1).get_layer(layer1); + + // Use built-in Helvetica font (ASCII only — CJK will show as blanks) + let font = doc.add_builtin_font(BuiltinFont::Helvetica) + .map_err(|e| format!("添加字体失败: {}", e))?; + + // Split content into lines that fit within the page width + let lines: Vec = markdown_content + .lines() + .flat_map(|line| { + // Rough estimate: ~2.5mm per character at 10pt Helvetica + let max_chars = (usable_width.0 / 2.5) as usize; + if line.is_empty() { + vec![String::new()] + } else if line.len() <= max_chars { + vec![line.to_string()] + } else { + // Word-wrap long lines + let mut wrapped = Vec::new(); + let mut remaining: &str = line; + while remaining.len() > max_chars { + // Find a good break point + let mut break_at = max_chars; + for (i, c) in remaining.char_indices().take(max_chars) { + if c == ' ' || c == ',' || c == ',' || c == '、' { + break_at = i + 1; + } + } + wrapped.push(remaining[..break_at].to_string()); + remaining = &remaining[break_at..]; + } + if !remaining.is_empty() { + wrapped.push(remaining.to_string()); + } + wrapped + } + }) + .collect(); + + // Calculate how many lines fit per page + let lines_per_page = (usable_height.0 / line_height) as usize; + + // Render lines onto pages + let mut line_idx = 0; + let mut page_count = 1; + + while line_idx < lines.len() { + let layer = if page_count == 1 { + ¤t_layer + } else { + // Add new page + let (new_page, new_layer) = doc.add_page(page_width, page_height, "Layer 1"); + &doc.get_page(new_page).get_layer(new_layer) + }; + + let page_lines = lines.len().min(line_idx + lines_per_page); + for (i, line) in lines[line_idx..page_lines].iter().enumerate() { + let y_pos = page_height - margin_top - Mm(line_height * (i as f32 + 1.0)); + + // Determine font style based on markdown markers + let (display_line, font_size_adj) = if line.starts_with("# ") { + (line.trim_start_matches("# "), 16.0) + } else if line.starts_with("## ") { + (line.trim_start_matches("## "), 14.0) + } else if line.starts_with("### ") { + (line.trim_start_matches("### "), 12.0) + } else { + (line.as_str(), font_size) + }; + + // Strip markdown formatting for plain text output + let clean_line = display_line + .replace("**", "") + .replace("__", "") + .replace("*", "") + .replace("_", "") + .replace("`", "") + .replace("$$", " ") + .replace("$", ""); + + layer.use_text( + clean_line, + font_size_adj, + margin_left, + y_pos, + &font, + ); + } + + line_idx = page_lines; + page_count += 1; + } + + // Save the PDF + let bytes = doc.save_to_bytes() + .map_err(|e| format!("保存 PDF 失败: {}", e))?; + fs::write(&output_path, &bytes) + .map_err(|e| format!("写入 PDF 文件失败: {}", e))?; + + eprintln!("PDF 已保存: {}", output_path); + Ok(output_path) } #[cfg_attr(mobile, tauri::mobile_entry_point)] @@ -95,7 +566,13 @@ pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_fs::init()) - .invoke_handler(tauri::generate_handler![greet, analyze_content_rust]) + .plugin(tauri_plugin_opener::init()) + .invoke_handler(tauri::generate_handler![ + analyze_content_rust, + extract_pdf_images, + test_model_connection_rust, + save_markdown_as_pdf, + ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 889c8ede..09f6fb6b 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1,83 +1,5 @@ -// Prevents additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize)] -struct RequestBody { - model: String, - messages: Vec, -} - -#[derive(Serialize, Deserialize)] -struct Message { - role: String, - content: String, -} - -#[derive(Serialize, Deserialize)] -struct ApiResponse { - choices: Vec, -} - -#[derive(Serialize, Deserialize)] -struct Choice { - message: Message, -} - -#[tauri::command] -async fn analyze_content_rust(api_key: String, api_base_url: String, system_prompt: String, text_content: String, model: String) -> Result { - let client = reqwest::Client::new(); - let request_body = RequestBody { - model: model.clone(), - messages: vec![ - Message { role: "system".to_string(), content: system_prompt }, - Message { role: "user".to_string(), content: format!("这是我上传的文档内容,请开始分析:\n\n{}", text_content) }, - ], - }; - - // 根据不同的API提供商设置不同的认证头 - let mut request_builder = client - .post(format!("{}/chat/completions", api_base_url)) - .json(&request_body); - - // 智谱GLM需要特殊的认证头格式 - if api_base_url.contains("bigmodel.cn") { - request_builder = request_builder.header("Authorization", format!("Bearer {}", api_key)); - } else { - // OpenAI 和 DeepSeek 使用标准的 Bearer token - request_builder = request_builder.header("Authorization", format!("Bearer {}", api_key)); - } - - let response = request_builder.send().await; - - match response { - Ok(res) => { - let status_code = res.status(); - if status_code.is_success() { - match res.json::().await { - Ok(api_response) => { - if let Some(choice) = api_response.choices.get(0) { - Ok(choice.message.content.clone()) - } else { - Err("API did not return any choices.".to_string()) - } - } - Err(e) => Err(format!("Failed to parse API response: {}", e)), - } - } else { - let error_text = res.text().await.unwrap_or_else(|_| "Unknown error".to_string()); - Err(format!("API request failed with status {}: {}", status_code, error_text)) - } - } - Err(e) => Err(format!("Request error: {}", e)), - } -} - - fn main() { - tauri::Builder::default() - .invoke_handler(tauri::generate_handler![analyze_content_rust]) - .run(tauri::generate_context!()) - .expect("error while running tauri application"); + onedocs_lib::run(); } \ No newline at end of file diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index f250af2b..3ae60550 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "onedocs", - "version": "1.2.0", + "version": "1.4.0", "identifier": "com.ijune.onedocs", "build": { "beforeDevCommand": "npm run dev", @@ -16,18 +16,26 @@ "title": "一文亦闻", "width": 800, "height": 700, - "resizable": false, + "resizable": true, + "decorations": false, "label": "main" } ], "security": { - "csp": null, - "capabilities": ["default"] + "csp": "default-src 'self' ipc: http://ipc.localhost; img-src 'self' asset: http://asset.localhost data: blob:; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; font-src 'self' https://cdn.jsdelivr.net; connect-src ipc: http://ipc.localhost https://*", + "capabilities": ["default"], + "assetProtocol": { + "enable": true, + "scope": { + "allow": ["**"] + } + } } }, "bundle": { "active": true, - "targets": "app", + "targets": "all", + "resources": [], "icon": [ "icons/32x32.png", "icons/128x128.png", diff --git a/src/App.tsx b/src/App.tsx index 0df03789..46a59d48 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,22 +1,39 @@ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { Landing } from "@/pages/Landing"; -import { Tool } from "@/pages/Tool"; +import { Analysis } from "@/pages/Analysis"; +import { AnalysisResult } from "@/pages/AnalysisResult"; +import { Settings } from "@/pages/Settings"; import { Toast } from "@/components/Toast"; +import { TitleBar } from "@/components/TitleBar"; +import { useAppStore } from "@/store/useAppStore"; -type Page = "landing" | "tool"; +type Page = "landing" | "analysis" | "analysisResult" | "settings"; const App: React.FC = () => { const [currentPage, setCurrentPage] = useState("landing"); + const { analysisResult, multiFileAnalysisResults } = useAppStore(); + const hasAnalysisResults = + analysisResult !== null || Object.keys(multiFileAnalysisResults).length > 0; + + useEffect(() => { + if (!hasAnalysisResults && currentPage === "analysisResult") { + setCurrentPage("analysis"); + } + }, [hasAnalysisResults, currentPage, setCurrentPage]); return ( - <> - {currentPage === "landing" ? ( - setCurrentPage("tool")} /> - ) : ( - setCurrentPage("landing")} /> - )} +
+ +
+ {currentPage === "landing" && ( + setCurrentPage("analysis")} /> + )} + {currentPage === "analysis" && } + {currentPage === "analysisResult" && } + {currentPage === "settings" && } +
- +
); }; diff --git a/src/components/AboutPanel.tsx b/src/components/AboutPanel.tsx new file mode 100644 index 00000000..d483f478 --- /dev/null +++ b/src/components/AboutPanel.tsx @@ -0,0 +1,75 @@ +import React, { useRef } from "react"; +import packageJson from "../../package.json"; +import { useAppStore } from "@/store/useAppStore"; +import { useToast } from "./Toast"; + +const TAP_WINDOW_MS = 2000; +const TAP_COUNT = 5; + +export const AboutPanel: React.FC = () => { + const { devMode, setDevMode } = useAppStore(); + const toast = useToast(); + const tapTimesRef = useRef([]); + const version = packageJson.version || "unknown"; + + const handleVersionTap = () => { + const now = Date.now(); + tapTimesRef.current = [...tapTimesRef.current, now].filter( + (time) => now - time <= TAP_WINDOW_MS, + ); + + if (tapTimesRef.current.length >= TAP_COUNT && !devMode) { + setDevMode(true); + toast.show("开发者模式已启用"); + tapTimesRef.current = []; + } + }; + + return ( +
+ ); +}; diff --git a/src/components/AnalysisSettingsPanel.tsx b/src/components/AnalysisSettingsPanel.tsx new file mode 100644 index 00000000..0332e957 --- /dev/null +++ b/src/components/AnalysisSettingsPanel.tsx @@ -0,0 +1,177 @@ +import React, { useEffect, useState } from "react"; +import { useAppStore } from "@/store/useAppStore"; +import { + getUserEmbeddingToken, + setEmbeddingToken, + hasUserEmbeddingToken, + getFreeEmbeddingUses, + isEmbeddingAvailable, +} from "@/services/rag/embeddingService"; +import { openUrl } from "@tauri-apps/plugin-opener"; + +export const AnalysisSettingsPanel: React.FC = () => { + const { enableFormatReview, setEnableFormatReview } = useAppStore(); + + const [sfToken, setSfToken] = useState(""); + const [freeUses, setFreeUses] = useState(0); + const [embeddingAvailable, setEmbeddingAvailable] = useState(false); + const [hasOwnToken, setHasOwnToken] = useState(false); + const [showToken, setShowToken] = useState(false); + + useEffect(() => { + setSfToken(getUserEmbeddingToken()); + setFreeUses(getFreeEmbeddingUses()); + setEmbeddingAvailable(isEmbeddingAvailable()); + setHasOwnToken(hasUserEmbeddingToken()); + }, []); + + const handleTokenChange = (value: string) => { + setSfToken(value); + setEmbeddingToken(value); + setEmbeddingAvailable(isEmbeddingAvailable()); + setHasOwnToken(hasUserEmbeddingToken()); + setFreeUses(getFreeEmbeddingUses()); + }; + + const handleRegister = () => { + openUrl("https://cloud.siliconflow.cn/i/AOugEVKt").catch(() => { + window.open("https://cloud.siliconflow.cn/i/AOugEVKt", "_blank"); + }); + }; + + return ( +
+ {/* SiliconFlow Embedding Token */} +
+
+
+
+ 🧠RAG 嵌入模型配置 +
+
+ 使用 SiliconFlow 的 BAAI/bge-m3 模型进行文本嵌入,提升分析质量 +
+
+ + {/* Status indicator */} +
+ + {embeddingAvailable ? ( + hasOwnToken ? ( + ✅ 已配置个人 Token,嵌入服务可用 + ) : ( + ✅ 使用开发者提供的免费额度,剩余次数:{freeUses} + ) + ) : ( + ⚠️ 嵌入服务不可用,请配置 SiliconFlow Token + )} +
+ + {/* Token input */} +
+
+ handleTokenChange(e.target.value)} + placeholder="sk-... 填入你自己的 SiliconFlow API Token" + style={{ + width: "100%", + padding: "8px 12px", + paddingRight: 40, + borderRadius: 8, + border: "1px solid var(--border-color, #e5e7eb)", + background: "var(--input-bg, #fff)", + color: "var(--text-primary, #111)", + fontSize: 13, + outline: "none", + boxSizing: "border-box", + }} + /> + +
+
+ + {/* Register button */} +
+ + + 注册即送免费额度,BAAI/bge-m3 为免费模型 + +
+
+
+ + {/* Format Review Toggle */} +
+
+
+
开启格式复查
+
分析结束后再走一次格式优化,确保标题和公式符合规范
+
+ +
+
+
+ ); +}; diff --git a/src/components/DataManagementPanel.tsx b/src/components/DataManagementPanel.tsx new file mode 100644 index 00000000..eb208a1e --- /dev/null +++ b/src/components/DataManagementPanel.tsx @@ -0,0 +1,125 @@ +import React, { useEffect, useState } from "react"; +import { open } from "@tauri-apps/plugin-dialog"; +import { executableDir, resourceDir } from "@tauri-apps/api/path"; +import { useAppStore } from "@/store/useAppStore"; +import { useToast } from "./Toast"; + +const FALLBACK_INSTALL_DIR = "C:\\Program Files\\onedocs"; + +export const DataManagementPanel: React.FC = () => { + const { + dataDirectory, + setDataDirectory, + autoSaveAnalysisResult, + setAutoSaveAnalysisResult, + } = useAppStore(); + const toast = useToast(); + const [defaultDir, setDefaultDir] = useState(""); + const [isLoadingDefault, setIsLoadingDefault] = useState(true); + + useEffect(() => { + let isMounted = true; + + const sanitizePath = (value?: string | null) => + (value || "").replace(/[\\/]+$/, ""); + + const detectInstallDir = async () => { + setIsLoadingDefault(true); + let resolvedDir = ""; + + try { + resolvedDir = sanitizePath(await executableDir()); + } catch (exeError) { + console.warn("获取安装目录失败,将尝试资源目录", exeError); + try { + resolvedDir = sanitizePath(await resourceDir()); + } catch (resError) { + console.error("资源目录获取失败", resError); + } + } + + const normalized = resolvedDir || FALLBACK_INSTALL_DIR; + + if (isMounted) { + setDefaultDir(normalized); + if (!dataDirectory) { + setDataDirectory(normalized); + } + setIsLoadingDefault(false); + } + }; + + detectInstallDir(); + return () => { + isMounted = false; + }; + }, [dataDirectory, setDataDirectory]); + + const handleChooseDirectory = async () => { + try { + const selected = await open({ + directory: true, + multiple: false, + title: "选择应用数据目录", + defaultPath: dataDirectory || defaultDir || undefined, + }); + + if (typeof selected === "string" && selected) { + setDataDirectory(selected); + toast.show(`数据目录已更新:${selected}`); + } + } catch (error: any) { + console.error("目录选择失败", error); + toast.show(error?.message || "目录选择失败,请确认已授予文件权限"); + } + }; + + const handleResetDirectory = () => { + if (!defaultDir) return; + setDataDirectory(defaultDir); + toast.show("已恢复默认目录"); + }; + + const currentDir = dataDirectory || defaultDir; + + return ( +
+
+
+
+

应用数据目录

+

{isLoadingDefault ? "正在获取默认目录..." : "修改后会立即生效"}

+
+ +
+
+ {currentDir || "尚未获取目录"} +
+
+ +
+
+ +
+
+
+
自动保存析文结果
+
开启后,分析完成即自动将 Markdown 结果保存到当前数据目录
+
+ +
+
+
+ ); +}; diff --git a/src/components/DeveloperModePanel.tsx b/src/components/DeveloperModePanel.tsx new file mode 100644 index 00000000..d634f5d1 --- /dev/null +++ b/src/components/DeveloperModePanel.tsx @@ -0,0 +1,91 @@ +import React, { useMemo, useState } from "react"; +import { useAppStore } from "@/store/useAppStore"; +import { useToast } from "./Toast"; + +const formatTime = (timestamp: number) => { + const date = new Date(timestamp); + return date.toLocaleString(); +}; + +export const DeveloperModePanel: React.FC = () => { + const { developerLogs, clearLogs } = useAppStore(); + const toast = useToast(); + const [levelFilter, setLevelFilter] = useState("all"); + + const filteredLogs = useMemo(() => { + if (levelFilter === "all") return developerLogs; + return developerLogs.filter((log) => log.level === levelFilter); + }, [developerLogs, levelFilter]); + + const logText = useMemo(() => { + return filteredLogs + .map((log) => { + const header = `${formatTime(log.timestamp)} · ${log.scope} · ${log.level.toUpperCase()}`; + const payloadText = + log.payload === undefined + ? "" + : `\n${typeof log.payload === "string" ? log.payload : JSON.stringify(log.payload, null, 2)}`; + return `${header}\n${log.message}${payloadText}`; + }) + .join("\n\n"); + }, [filteredLogs]); + + const handleCopyAll = async () => { + if (!logText.trim()) { + toast.show("暂无日志可复制"); + return; + } + + try { + await navigator.clipboard.writeText(logText); + toast.show("日志已复制到剪贴板"); + } catch (error: any) { + console.error("复制日志失败", error); + toast.show("复制失败,请手动选择复制"); + } + }; + + return ( +
+
+
+
+

开发者模式

+

查看系统内部执行日志、模型请求与响应

+
+
+ + + +
+
+
+

系统日志

+
+ {filteredLogs.length === 0 ? ( +
+
暂无日志
+
+ ) : ( +
+
{logText}
+
+ )} +
+
+ ); +}; diff --git a/src/components/FileUpload.tsx b/src/components/FileUpload.tsx index e8205c5e..0a022921 100644 --- a/src/components/FileUpload.tsx +++ b/src/components/FileUpload.tsx @@ -3,19 +3,39 @@ import { useAppStore } from "@/store/useAppStore"; import { DocumentProcessor } from "@/utils/documentProcessor"; import { FILE_SIZE_LIMIT } from "@/config/providers"; import { useToast } from "./Toast"; +import { useTranslation } from "react-i18next"; import type { FileInfo, SupportedFileType } from "@/types"; -export const FileUpload: React.FC = () => { - const { files, addFile, removeFile, setFiles, setCurrentFileId, currentFileId } = useAppStore(); +interface FileUploadProps { + onAnalyze?: () => void; + canAnalyze?: boolean; + isAnalyzing?: boolean; + hasAnalysisResults?: boolean; +} + +export const FileUpload: React.FC = ({ + onAnalyze, + canAnalyze = false, + isAnalyzing = false, + hasAnalysisResults = false, +}) => { + const { + files, + addFile, + removeFile, + setFiles, + setCurrentFileId, + currentFileId, + } = useAppStore(); const fileInputRef = useRef(null); const toast = useToast(); + const { t } = useTranslation(); const handleFileSelect = (event: React.ChangeEvent) => { const selectedFiles = Array.from(event.target.files || []); if (selectedFiles.length > 0) { processFiles(selectedFiles); } - // 重置input,允许重复选择同一文件 if (fileInputRef.current) { fileInputRef.current.value = ""; } @@ -23,23 +43,20 @@ export const FileUpload: React.FC = () => { const processFiles = (fileList: File[]) => { const validFiles: FileInfo[] = []; - + fileList.forEach((file) => { - // 检查文件类型 if (!DocumentProcessor.isValidFileType(file.type)) { toast.show( - `文件 ${file.name} 格式不支持 (${file.type}),已跳过`, + t("upload.toast.unsupported", { name: file.name, type: file.type }), ); return; } - // 检查文件大小 if (file.size > FILE_SIZE_LIMIT) { - toast.show(`文件 ${file.name} 过大(超过50MB),已跳过`); + toast.show(t("upload.toast.tooLarge", { name: file.name })); return; } - // 保存文件信息 const fileInfo: FileInfo = { file, name: file.name, @@ -52,7 +69,7 @@ export const FileUpload: React.FC = () => { if (validFiles.length > 0) { validFiles.forEach((fileInfo) => addFile(fileInfo)); - toast.show(`成功添加 ${validFiles.length} 个文件`); + toast.show(t("upload.toast.added", { count: validFiles.length })); } }; @@ -69,54 +86,81 @@ export const FileUpload: React.FC = () => { setCurrentFileId(fileId); }; - // 移动文件位置 const handleMoveUp = (e: React.MouseEvent, index: number) => { e.stopPropagation(); - if (index === 0) return; // 已经在最上面 - + if (index === 0) return; + const newFiles = [...files]; - [newFiles[index - 1], newFiles[index]] = [newFiles[index], newFiles[index - 1]]; + [newFiles[index - 1], newFiles[index]] = [ + newFiles[index], + newFiles[index - 1], + ]; setFiles(newFiles); }; const handleMoveDown = (e: React.MouseEvent, index: number) => { e.stopPropagation(); - if (index === files.length - 1) return; // 已经在最下面 - + if (index === files.length - 1) return; + const newFiles = [...files]; - [newFiles[index], newFiles[index + 1]] = [newFiles[index + 1], newFiles[index]]; + [newFiles[index], newFiles[index + 1]] = [ + newFiles[index + 1], + newFiles[index], + ]; setFiles(newFiles); }; return ( <>
-
-
-
📁
-

点击选择文档(支持多选)

-

支持 PDF、Word、PowerPoint、TXT 格式,单个文件不超过50MB

- +
+
+
+
📁
+

{t("upload.select")}

+

{t("upload.hint")}

+ +
+ + {onAnalyze && ( + + )}
{files.length > 0 && (
- 已上传文件 ({files.length}) - 点击箭头调整顺序 + {t("upload.files.header", { count: files.length })} + {t("upload.files.reorderHint")}
{files.map((fileInfo, index) => { @@ -124,7 +168,7 @@ export const FileUpload: React.FC = () => { const isActive = currentFileId === fileId; const isFirst = index === 0; const isLast = index === files.length - 1; - + return (
{ className="file-item-arrow file-item-arrow-up" onClick={(e) => handleMoveUp(e, index)} disabled={isFirst} - title="上移" + title={t("upload.moveUp")} > ↑ @@ -144,7 +188,7 @@ export const FileUpload: React.FC = () => { className="file-item-arrow file-item-arrow-down" onClick={(e) => handleMoveDown(e, index)} disabled={isLast} - title="下移" + title={t("upload.moveDown")} > ↓ @@ -158,7 +202,7 @@ export const FileUpload: React.FC = () => { diff --git a/src/components/FunctionSelector.tsx b/src/components/FunctionSelector.tsx index c86891bd..bc5c87dd 100644 --- a/src/components/FunctionSelector.tsx +++ b/src/components/FunctionSelector.tsx @@ -1,41 +1,53 @@ -import React from 'react'; -import { useAppStore } from '@/store/useAppStore'; -import { FUNCTION_INFO } from '@/config/providers'; -import { useToast } from './Toast'; -import type { PromptType } from '@/types'; +import React from "react"; +import { useAppStore } from "@/store/useAppStore"; +import { FUNCTION_INFO } from "@/config/providers"; +import { useToast } from "./Toast"; +import { useTranslation } from "react-i18next"; +import type { PromptType } from "@/types"; export const FunctionSelector: React.FC = () => { - const { selectedFunction, setSelectedFunction, isSidebarCollapsed, toggleSidebar } = useAppStore(); + const { + selectedFunction, + setSelectedFunction, + isSidebarCollapsed, + toggleSidebar, + } = useAppStore(); const toast = useToast(); + const { t } = useTranslation(); const handleFunctionSelect = (func: PromptType) => { setSelectedFunction(func); const info = FUNCTION_INFO[func]; - toast.show(`已选择功能: ${info.name}`); + const name = t(info.nameKey); + toast.show(t("functionSelector.toastSelected", { name })); }; return ( -
-
- 功能选择 - +
+
+ {t("functionSelector.title")} + + +
{(Object.keys(FUNCTION_INFO) as PromptType[]).map((key) => { const info = FUNCTION_INFO[key]; + const name = t(info.nameKey); + const description = t(info.descriptionKey); return ( ); diff --git a/src/components/ModelSelectionPanel.tsx b/src/components/ModelSelectionPanel.tsx new file mode 100644 index 00000000..26c9c207 --- /dev/null +++ b/src/components/ModelSelectionPanel.tsx @@ -0,0 +1,859 @@ +import React, { useEffect, useState } from "react"; +import { useAppStore } from "@/store/useAppStore"; +import { MODEL_PROVIDERS } from "@/config/providers"; +import { MODEL_SOURCE_LOGOS } from "@/config/logoAssets"; +import { APIService } from "@/services/api"; +import { useToast } from "./Toast"; +import type { AIProvider, AllProviders, ModelOption } from "@/types"; + +const ModelLogo = () => ( + +); + +const ModelIcon: React.FC<{ src: string | null; staticIcon?: boolean }> = ({ + src, + staticIcon = false, +}) => { + const [failed, setFailed] = useState(false); + + if (!src || failed) { + return ; + } + + return ( + setFailed(true)} + /> + ); +}; + +const SOURCE_LOGO_PROVIDERS = new Set([ + "onedocs", + "openrouter", + "comp_share", + "302_ai", + "pony", + "siliconflow", + "xinghe", + "ppio", + "modelscope", + "newapi", + "oneapi", +]); + +function getModelLogoSrc( + provider: AIProvider | null, + model: ModelOption +) { + if (!provider) return null; + + if (SOURCE_LOGO_PROVIDERS.has(provider)) { + const source = model.value.split("/")[0].toLowerCase(); + const logo = MODEL_SOURCE_LOGOS[source as keyof typeof MODEL_SOURCE_LOGOS]; + return logo || null; + } + + const providerConfig = MODEL_PROVIDERS[provider]; + return providerConfig?.icon || null; +} + +const PROVIDER_PRIORITY: AIProvider[] = [ + "onedocs", + "openai", + "gemini", + "openrouter", + "glm", + "siliconflow", + "xinghe", + "newapi", +]; +const ORDERED_PROVIDER_KEYS: AIProvider[] = [ + ...PROVIDER_PRIORITY.filter((key) => + Object.prototype.hasOwnProperty.call(MODEL_PROVIDERS, key) + ), + ...(Object.keys(MODEL_PROVIDERS) as AIProvider[]).filter( + (key) => !PROVIDER_PRIORITY.includes(key) + ), +]; + +export const ModelSelectionPanel: React.FC = () => { + + const { + currentProvider, + setCurrentProvider, + providerSettings, + updateProviderSettings, + providerCustomModels, + customProviders, + addCustomProvider, + updateCustomProvider, + deleteCustomProvider, + addProviderCustomModel, + removeProviderCustomModel, + clearAllCache, + } = useAppStore(); + + const toast = useToast(); + const [view, setView] = useState<"grid" | "form">("grid"); + const [isTesting, setIsTesting] = useState(false); + const [isCreatingCustom, setIsCreatingCustom] = useState(false); + + const [localProvider, setLocalProvider] = useState(currentProvider); + const [localApiKey, setLocalApiKey] = useState(""); + const [localBaseUrl, setLocalBaseUrl] = useState(""); + const [localModel, setLocalModel] = useState(""); + + const [customName, setCustomName] = useState(""); + const [customBaseUrl, setCustomBaseUrl] = useState(""); + const [customModel, setCustomModel] = useState(""); + const [customApiKey, setCustomApiKey] = useState(""); + + const [isAddingModel, setIsAddingModel] = useState(false); + const [newModelId, setNewModelId] = useState(""); + const [newModelName, setNewModelName] = useState(""); + + const [showClearConfirm, setShowClearConfirm] = useState(false); + const [errors, setErrors] = useState({ apiKey: false, baseUrl: false, model: false }); + const [infoDismissed, setInfoDismissed] = useState(false); + const [freeInfoDismissed, setFreeInfoDismissed] = useState(false); + const sanitizeValue = (value?: string) => (value ?? "").trim(); + + const isCustomProvider = typeof localProvider === "string" && localProvider.startsWith("custom_"); + const config = isCustomProvider ? null : MODEL_PROVIDERS[localProvider as AIProvider]; + const currentCustom = isCustomProvider ? customProviders[localProvider] : null; + + const providerModels = !isCustomProvider && config + ? [...config.models, ...(providerCustomModels[localProvider as AIProvider] || [])] + : []; + + const isCustomModelSelected = !isCustomProvider + ? providerCustomModels[localProvider as AIProvider]?.some((m) => m.value === localModel) + : false; + + const requiresApiKey = isCustomProvider ? true : config?.requiresApiKey !== false; + const requiresBaseUrl = isCustomProvider ? true : config?.requiresBaseUrl !== false; + const showApiKeyField = isCustomProvider ? true : config?.showApiKeyField !== false; + const showBaseUrlField = isCustomProvider ? true : config?.showBaseUrlField !== false; + const allowModelCustomization = isCustomProvider ? true : config?.allowModelCustomization !== false; + const hasManagedCredentials = !isCustomProvider && !!config?.credentialsReadOnly; + const missingManagedCredentials = hasManagedCredentials && (!localApiKey || !localBaseUrl); + const isFreeProvider = !isCustomProvider && localProvider === "onedocs"; + + useEffect(() => { + setLocalProvider(currentProvider); + setView("grid"); + setIsCreatingCustom(false); + setIsAddingModel(false); + setErrors({ apiKey: false, baseUrl: false, model: false }); + setInfoDismissed(false); + setFreeInfoDismissed(false); + }, [currentProvider]); + + useEffect(() => { + setErrors({ apiKey: false, baseUrl: false, model: false }); + setInfoDismissed(false); + setFreeInfoDismissed(false); + if (typeof localProvider === "string" && localProvider.startsWith("custom_")) { + const settings = customProviders[localProvider]; + if (settings) { + setLocalApiKey(settings.apiKey); + setLocalBaseUrl(settings.baseUrl); + setLocalModel(settings.model); + } else { + setLocalApiKey(""); + setLocalBaseUrl(""); + setLocalModel(""); + } + } else { + const settings = providerSettings[localProvider as AIProvider]; + setLocalApiKey(settings?.apiKey || config?.defaultApiKey || ""); + setLocalBaseUrl(settings?.baseUrl || config?.baseUrl || ""); + setLocalModel(settings?.model || config?.defaultModel || ""); + } + }, [localProvider, providerSettings, customProviders, config]); + + useEffect(() => { + if (!isCustomProvider && config?.credentialsReadOnly) { + if (config.baseUrl && config.baseUrl !== localBaseUrl) { + setLocalBaseUrl(config.baseUrl); + } + if (config.defaultApiKey && config.defaultApiKey !== localApiKey) { + setLocalApiKey(config.defaultApiKey); + } + } + }, [isCustomProvider, config, localBaseUrl, localApiKey]); + + const handleProviderSelect = (provider: AllProviders | "create_custom") => { + if (provider === "create_custom") { + setIsCreatingCustom(true); + setView("form"); + return; + } + + setLocalProvider(provider as AllProviders); + setView("form"); + }; + + const saveSettings = () => { + if (typeof localProvider === "string" && localProvider.startsWith("custom_")) { + updateCustomProvider(localProvider, { + apiKey: localApiKey, + baseUrl: localBaseUrl, + model: localModel, + name: customProviders[localProvider]?.name || "自定义模型", + }); + } else { + updateProviderSettings(localProvider as AIProvider, { + apiKey: localApiKey, + baseUrl: localBaseUrl, + model: localModel, + }); + } + }; + + const validateForm = () => { + const newErrors = { + apiKey: requiresApiKey && !localApiKey, + baseUrl: requiresBaseUrl && !localBaseUrl, + model: !localModel, + }; + setErrors(newErrors); + return !Object.values(newErrors).some(Boolean); + }; + + const handleSave = () => { + if (!validateForm()) { + toast.show("请填写所有必填项"); + return; + } + saveSettings(); + toast.show("设置已保存"); + }; + + const handleUse = () => { + if (!validateForm()) { + toast.show("请填写所有必填项"); + return; + } + saveSettings(); + setCurrentProvider(localProvider); + toast.show("已切换至该模型"); + setView("grid"); + setIsCreatingCustom(false); + }; + + const handleCreateCustomProvider = () => { + if (!customName || !customBaseUrl || !customModel) { + toast.show("请填写完整的自定义模型信息"); + return; + } + + const newId = addCustomProvider(customName, customBaseUrl, customModel, customApiKey); + setLocalProvider(newId as AllProviders); + setIsCreatingCustom(false); + setView("form"); + + setCustomName(""); + setCustomBaseUrl(""); + setCustomModel(""); + setCustomApiKey(""); + + toast.show("自定义模型已创建"); + }; + + const handleAddModel = () => { + if (!newModelId || !newModelName) { + toast.show("请填写完整的模型信息"); + return; + } + addProviderCustomModel(localProvider as AIProvider, { + value: newModelId, + name: newModelName, + }); + setLocalModel(newModelId); + setNewModelId(""); + setNewModelName(""); + setIsAddingModel(false); + toast.show("模型添加成功"); + }; + + const handleDeleteModel = (modelValue: string) => { + removeProviderCustomModel(localProvider as AIProvider, modelValue); + if (localModel === modelValue) { + setLocalModel(""); + } + toast.show("模型已删除"); + }; + + const handleTestConnection = async () => { + const providerId = localProvider.toString(); + const isLocalProvider = providerId.includes("ollama") || providerId.includes("lmstudio"); + if (!localApiKey && requiresApiKey && !isLocalProvider) { + toast.show("请先输入 API Key"); + return; + } + + setIsTesting(true); + try { + if (typeof localProvider === "string" && localProvider.startsWith("custom_")) { + await APIService.testCustomConnection(localApiKey, localBaseUrl, localModel); + } else { + await APIService.testConnection( + localProvider as AIProvider, + localApiKey, + localBaseUrl, + localModel + ); + } + toast.show("连接测试成功!"); + } catch (error: any) { + if (error.message === "BALANCE_WARNING" && error.isWarning) { + toast.show(`✅ ${error.originalMessage}`, 5000); + } else { + toast.show(`连接测试失败:${error.message}`, 5000); + } + } finally { + setIsTesting(false); + } + }; + + const handleClearCache = () => setShowClearConfirm(true); + + const confirmClear = () => { + clearAllCache(); + setShowClearConfirm(false); + toast.show("所有模型缓存已清除"); + }; + + const closeClearConfirm = () => setShowClearConfirm(false); + + const getProviderIcon = (key: string) => { + const provider = MODEL_PROVIDERS[key as AIProvider]; + if (provider?.icon) { + return ( + {provider.name} + ); + } + return key.charAt(0).toUpperCase(); + }; + + const renderModelTags = (model: ModelOption) => { + if (model.tags?.length) { + return model.tags.map((tag, index) => ( + + {tag.label} + + )); + } + + if (model.tag) { + return {model.tag}; + } + + return null; + }; + + return ( +
+ {view === "grid" ? ( +
+ {ORDERED_PROVIDER_KEYS.map((key) => { + const providerConfig = MODEL_PROVIDERS[key]; + const settings = providerSettings[key]; + const requiresApiKey = providerConfig.requiresApiKey !== false; + const requiresBaseUrl = providerConfig.requiresBaseUrl !== false; + const hasApiKey = Boolean( + sanitizeValue(settings?.apiKey) || sanitizeValue(providerConfig.defaultApiKey) + ); + const hasBaseUrl = Boolean( + sanitizeValue(settings?.baseUrl) || sanitizeValue(providerConfig.baseUrl) + ); + const isConfigured = (!requiresApiKey || hasApiKey) && (!requiresBaseUrl || hasBaseUrl); + const showPrimaryBadge = key === "onedocs" && providerConfig.badgeText; + return ( +
handleProviderSelect(key)} + > + {showPrimaryBadge && ( + + {providerConfig.badgeText} + + )} +
+ {currentProvider === key ? ( + 当前使用 + ) : isConfigured ? ( + 可用 + ) : null} +
+
{getProviderIcon(key)}
+
{MODEL_PROVIDERS[key].name}
+
+ ); + })} + + {Object.entries(customProviders).map(([id, provider]) => { + const isConfigured = !!( + sanitizeValue(provider.apiKey) && + sanitizeValue(provider.baseUrl) && + sanitizeValue(provider.model) + ); + return ( +
handleProviderSelect(id as AllProviders)} + > +
+ {currentProvider === id ? ( + 当前使用 + ) : isConfigured ? ( + 可用 + ) : null} +
+
{getProviderIcon(provider.name)}
+
{provider.name}
+
自定义
+
+ ); + })} + +
handleProviderSelect("create_custom")}> +
+
+
新建自定义模型
+
+ +
+
+ 🗑️ +
+
+ 清除所有模型缓存 +
+
+
+ ) : ( +
+
+ +
+

+ {isCreatingCustom + ? "新建自定义模型" + : isCustomProvider + ? currentCustom?.name + : config?.name} +

+

+ {isCreatingCustom + ? "将第三方兼容 OpenAI 接口的服务统一纳管" + : "为当前服务配置凭证与模型"} +

+
+
+ + {isCreatingCustom ? ( + <> +
+ + setCustomName(e.target.value)} + placeholder="为您的自定义模型起个名字" + /> + 自定义模型的显示名称 +
+ +
+ + setCustomBaseUrl(e.target.value)} + placeholder="https://api.example.com/v1" + /> + OpenAI 格式 API 的基础 URL +
+ +
+ + setCustomApiKey(e.target.value)} + placeholder="输入您的 API 密钥" + /> + 您的 API 访问密钥 +
+ +
+ + setCustomModel(e.target.value)} + placeholder="gpt-3.5-turbo" + /> + 模型的标识符,如 gpt-3.5-turbo +
+ + ) : ( + <> + {!isCustomProvider && config?.description && !infoDismissed && ( +
+ +

{config.description}

+ {hasManagedCredentials && 无需配置 URL 与 API Key} +
+ )} + + {isFreeProvider && !freeInfoDismissed && ( +
+ +

+ 免费模型输入输出以及分析能力有限,有概率出现格式错乱的问题,若遇到问题请输出后请自行导出修改源码,感谢理解! +

+
+ )} + + {missingManagedCredentials && ( +
+ ⚠️ 未检测到 {config?.name} 内置凭证,请在部署流程中注入 VITE_ONEDOCS_API_URL 与 VITE_ONEDOCS_API_KEY。 +
+ )} + + {isCustomProvider && currentCustom && ( +
+ +
+
+ 名称: {currentCustom.name} +
+
+ Base URL: {currentCustom.baseUrl} +
+
+ Model ID: {currentCustom.model} +
+
+ +
+ )} + + {showBaseUrlField && ( +
+ + { + setLocalBaseUrl(e.target.value); + if (e.target.value) setErrors((prev) => ({ ...prev, baseUrl: false })); + }} + className={errors.baseUrl ? "input-error" : ""} + placeholder={isCustomProvider ? currentCustom?.baseUrl : config?.baseUrl} + disabled={hasManagedCredentials} + /> + {isCustomProvider ? "自定义API服务器地址" : config?.baseUrlHint} +
+ )} + + {showApiKeyField && ( +
+ + { + setLocalApiKey(e.target.value); + if (e.target.value) setErrors((prev) => ({ ...prev, apiKey: false })); + }} + className={errors.apiKey ? "input-error" : ""} + placeholder="输入 API Key" + disabled={hasManagedCredentials} + /> + {isCustomProvider ? "API 访问密钥" : config?.keyHint} +
+ )} + +
+ + {isCustomProvider ? ( + { + setLocalModel(e.target.value); + if (e.target.value) setErrors((prev) => ({ ...prev, model: false })); + }} + className={errors.model ? "input-error" : ""} + placeholder="输入模型ID" + /> + ) : ( + <> + + +
+
+ {providerModels.map((model) => ( + + ))} +
+ + {!isAddingModel && allowModelCustomization && ( +
+ {isCustomModelSelected && ( + + )} + +
+ )} +
+ + {allowModelCustomization && isAddingModel && ( +
+
添加新模型
+ setNewModelId(e.target.value)} + /> + setNewModelName(e.target.value)} + /> +
+ + +
+
+ )} + + )} + 选择要使用的具体模型 +
+ + )} +
+ )} + + {view === "form" && ( +
+
+ {!isCreatingCustom && ( + + )} +
+
+ + {!isCreatingCustom ? ( + <> + + + + ) : ( + + )} +
+
+ )} + + {showClearConfirm && ( +
+
+

+ + ⚠️ + + 确认清除缓存 +

+

确认要删除所有模型缓存吗?若删除将会:

+
    +
  • 恢复所有 API Base URL 至默认值
  • +
  • 清空已填写的 API Key
  • +
  • 移除所有自定义模型
  • +
  • 移除所有自定义服务商
  • +
+
+ + +
+
+
+ )} +
+ ); +}; diff --git a/src/components/ProgressBar.tsx b/src/components/ProgressBar.tsx index 4b92fbc8..efb3c965 100644 --- a/src/components/ProgressBar.tsx +++ b/src/components/ProgressBar.tsx @@ -1,15 +1,17 @@ -import React from 'react'; -import { useAppStore } from '@/store/useAppStore'; +import React from "react"; +import { useAppStore } from "@/store/useAppStore"; +import { useTranslation } from "react-i18next"; export const ProgressBar: React.FC = () => { const { analysisProgress } = useAppStore(); + const { t } = useTranslation(); if (!analysisProgress) return null; return (
-

正在分析文档...

+

{t("analysis.progress.title")}

{analysisProgress.message}

@@ -19,7 +21,9 @@ export const ProgressBar: React.FC = () => { style={{ width: `${analysisProgress.percentage}%` }} />
- {analysisProgress.percentage}% + + {analysisProgress.percentage}% +
); diff --git a/src/components/ResultDisplay.tsx b/src/components/ResultDisplay.tsx index e73733b8..d85ec12a 100644 --- a/src/components/ResultDisplay.tsx +++ b/src/components/ResultDisplay.tsx @@ -1,9 +1,11 @@ -import React, { useState } from "react"; +import React, { useState, useCallback, useMemo } from "react"; import { useAppStore } from "@/store/useAppStore"; import { MarkdownRenderer } from "@/utils/markdownRenderer"; import { useToast } from "./Toast"; import { save } from "@tauri-apps/plugin-dialog"; import { writeTextFile } from "@tauri-apps/plugin-fs"; +import { useTranslation } from "react-i18next"; +import { openUrl } from "@tauri-apps/plugin-opener"; import "katex/dist/katex.min.css"; export const ResultDisplay: React.FC = () => { @@ -19,34 +21,43 @@ export const ResultDisplay: React.FC = () => { setCurrentFileId, } = useAppStore(); const toast = useToast(); + const { t } = useTranslation(); const [isCopying, setIsCopying] = useState(false); const [isMerging, setIsMerging] = useState(false); - // 确定要显示的结果 const displayResult = mergedResult || analysisResult; - if (!displayResult && Object.keys(multiFileAnalysisResults).length === 0) return null; + if (!displayResult && Object.keys(multiFileAnalysisResults).length === 0) + return null; const handleCopyResult = async () => { if (!displayResult) return; try { setIsCopying(true); await navigator.clipboard.writeText(displayResult.content); - toast.show("已复制Markdown源码到剪贴板"); + toast.show(t("result.toast.copied")); setTimeout(() => { setIsCopying(false); }, 1500); } catch (error) { setIsCopying(false); - toast.show("复制失败,请手动选择复制"); + toast.show(t("result.toast.copyFail")); } }; const handleExport = async () => { if (!displayResult) return; + const now = new Date(); + const dateStr = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, "0")}${String(now.getDate()).padStart(2, "0")}`; + const currentFile = files.find((f) => f.id === currentFileId) || files[0]; + const fileNameBase = mergedResult + ? t("result.title.merged") + : currentFile + ? currentFile.name.replace(/\.[^./\\]+$/, "") + : "OneDocs"; + const defaultFileName = `${fileNameBase}_OneDocs_${t("result.exportFileSuffix")}_${dateStr}.md`; try { - // 尝试使用 Tauri 对话框 const filePath = await save({ - defaultPath: `OneDocs_分析结果_${new Date().getTime()}.md`, + defaultPath: defaultFileName, filters: [ { name: "Markdown", @@ -56,14 +67,12 @@ export const ResultDisplay: React.FC = () => { }); if (filePath) { - // 写入文件 await writeTextFile(filePath, displayResult.content); - toast.show(`文件已保存到: ${filePath}`); + toast.show(t("result.toast.saved", { path: filePath })); } } catch (error: any) { console.error("Tauri 导出失败,尝试使用浏览器下载:", error); - // 备用方案:使用浏览器的下载方式 try { const blob = new Blob([displayResult.content], { type: "text/markdown;charset=utf-8", @@ -71,22 +80,22 @@ export const ResultDisplay: React.FC = () => { const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; - a.download = `OneDocs_分析结果_${new Date().getTime()}.md`; + a.download = defaultFileName; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); - toast.show("文件已导出到下载文件夹"); + toast.show(t("result.toast.exported")); } catch (fallbackError) { console.error("导出失败:", fallbackError); - toast.show("导出失败,请尝试复制内容后手动保存"); + toast.show(t("result.toast.exportFail")); } } }; const handleMerge = () => { if (files.length < 2) { - toast.show("至少需要2个文件才能合并"); + toast.show(t("result.toast.mergeNeedTwo")); return; } @@ -96,23 +105,30 @@ export const ResultDisplay: React.FC = () => { const result = multiFileAnalysisResults[fileId]; return result ? { file, result } : null; }) - .filter((item): item is { file: typeof files[0]; result: typeof multiFileAnalysisResults[string] } => item !== null); + .filter( + ( + item, + ): item is { + file: (typeof files)[0]; + result: (typeof multiFileAnalysisResults)[string]; + } => item !== null, + ); if (results.length < 2) { - toast.show("至少需要2个已分析的文件才能合并"); + toast.show(t("result.toast.mergeNeedTwoAnalyzed")); return; } setIsMerging(true); try { - // 合并所有结果,每个文件的标题降级 const mergedContent = results .map(({ file, result }) => { - const downgradedContent = MarkdownRenderer.downgradeHeadings(result.content); - // 添加文件名的注释(可选) + const downgradedContent = MarkdownRenderer.downgradeHeadings( + result.content, + ); return `\n\n${downgradedContent}`; }) - .join("\n\n---\n\n"); // 用分隔线分隔不同文件 + .join("\n\n---\n\n"); const merged: typeof mergedResult = { content: mergedContent, @@ -120,20 +136,42 @@ export const ResultDisplay: React.FC = () => { }; setMergedResult(merged); - toast.show(`成功合并 ${results.length} 个文件!`); + toast.show(t("result.toast.mergeSuccess", { count: results.length })); } catch (error: any) { console.error("合并失败:", error); - toast.show("合并失败: " + (error.message || "未知错误")); + const message = error.message || t("result.toast.unknownError"); + toast.show(t("result.toast.mergeFail", { message })); } finally { setIsMerging(false); } }; - const renderedContent = displayResult - ? viewMode === "render" - ? MarkdownRenderer.render(displayResult.content) - : displayResult.content - : ""; + // Memoize rendered HTML to avoid re-computing on every render (prevents UI freeze) + const renderedContent = useMemo(() => { + if (!displayResult) return ""; + if (viewMode === "render") { + return MarkdownRenderer.render(displayResult.content); + } + return displayResult.content; + }, [displayResult, viewMode]); + + // Intercept link clicks to open external URLs in the default browser + const handleContentClick = useCallback((e: React.MouseEvent) => { + const target = e.target as HTMLElement; + const anchor = target.closest("a") as HTMLAnchorElement | null; + if (!anchor) return; + const href = anchor.getAttribute("href") || ""; + // Skip internal/anchor links + if (!href || href.startsWith("#") || href.startsWith("javascript:")) return; + e.preventDefault(); + e.stopPropagation(); + // Open external links in default browser + openUrl(href).catch((err: any) => { + console.error("打开链接失败:", err); + // Fallback: try window.open + window.open(href, "_blank"); + }); + }, []); const hasMultipleFiles = files.length > 1; const hasMultipleResults = Object.keys(multiFileAnalysisResults).length > 1; @@ -145,7 +183,9 @@ export const ResultDisplay: React.FC = () => {

- {mergedResult ? "合并结果" : "析文成果"} + {mergedResult + ? t("result.title.merged") + : t("result.title.single")}

{canMerge && !mergedResult && ( @@ -154,7 +194,9 @@ export const ResultDisplay: React.FC = () => { onClick={handleMerge} disabled={isMerging} > - {isMerging ? "合并中..." : "一键合并"} + {isMerging + ? t("result.merge.loading") + : t("result.merge.button")} )} {mergedResult && ( @@ -162,7 +204,7 @@ export const ResultDisplay: React.FC = () => { className="back-button-small" onClick={() => setMergedResult(null)} > - 返回预览 + {t("result.back")} )}
@@ -170,28 +212,28 @@ export const ResultDisplay: React.FC = () => { className={`toggle-btn ${viewMode === "render" ? "active" : ""}`} onClick={() => setViewMode("render")} > - 渲染 + {t("result.view.render")}
- -
@@ -210,9 +252,15 @@ export const ResultDisplay: React.FC = () => { setCurrentFileId(fileId); } }} - title={hasResult ? file.name : `${file.name} (未分析)`} + title={ + hasResult + ? file.name + : t("result.fileNotAnalyzed", { name: file.name }) + } > - {file.name.length > 15 ? `${file.name.substring(0, 15)}...` : file.name} + {file.name.length > 15 + ? `${file.name.substring(0, 15)}...` + : file.name} ); })} @@ -225,6 +273,7 @@ export const ResultDisplay: React.FC = () => { viewMode === "render" ? (
) : ( @@ -232,7 +281,7 @@ export const ResultDisplay: React.FC = () => { ) ) : (
-

请先分析文档以查看结果

+

{t("result.empty")}

)}
diff --git a/src/components/SettingsModal.tsx b/src/components/SettingsModal.tsx deleted file mode 100644 index 7999eab0..00000000 --- a/src/components/SettingsModal.tsx +++ /dev/null @@ -1,386 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { useAppStore } from '@/store/useAppStore'; -import { MODEL_PROVIDERS } from '@/config/providers'; -import { APIService } from '@/services/api'; -import { useToast } from './Toast'; -import type { AIProvider, AllProviders } from '@/types'; - -export const SettingsModal: React.FC = () => { - const { - isSettingsOpen, - setSettingsOpen, - currentProvider, - setCurrentProvider, - providerSettings, - updateProviderSettings, - customProviders, - addCustomProvider, - updateCustomProvider, - deleteCustomProvider, - } = useAppStore(); - - const toast = useToast(); - const [isTesting, setIsTesting] = useState(false); - const [isCreatingCustom, setIsCreatingCustom] = useState(false); - - // 本地状态用于表单 - const [localProvider, setLocalProvider] = useState(currentProvider); - const [localApiKey, setLocalApiKey] = useState(''); - const [localBaseUrl, setLocalBaseUrl] = useState(''); - const [localModel, setLocalModel] = useState(''); - - // 新建自定义模型的状态 - const [customName, setCustomName] = useState(''); - const [customBaseUrl, setCustomBaseUrl] = useState(''); - const [customModel, setCustomModel] = useState(''); - const [customApiKey, setCustomApiKey] = useState(''); - - // 当打开设置或切换提供商时,加载对应的设置 - useEffect(() => { - if (isSettingsOpen) { - if (typeof localProvider === 'string' && localProvider.startsWith('custom_')) { - // 自定义提供商 - const settings = customProviders[localProvider]; - if (settings) { - setLocalApiKey(settings.apiKey); - setLocalBaseUrl(settings.baseUrl); - setLocalModel(settings.model); - } - } else { - // 内置提供商 - const settings = providerSettings[localProvider as AIProvider]; - if (settings) { - setLocalApiKey(settings.apiKey); - setLocalBaseUrl(settings.baseUrl); - setLocalModel(settings.model); - } - } - } - }, [isSettingsOpen, localProvider, providerSettings, customProviders]); - - const handleProviderChange = (provider: string) => { - if (provider === 'create_custom') { - setIsCreatingCustom(true); - return; - } - - setLocalProvider(provider as AllProviders); - - if (provider.startsWith('custom_')) { - // 自定义提供商 - const settings = customProviders[provider]; - if (settings) { - setLocalApiKey(settings.apiKey); - setLocalBaseUrl(settings.baseUrl); - setLocalModel(settings.model); - } - } else { - // 内置提供商 - const settings = providerSettings[provider as AIProvider]; - setLocalApiKey(settings.apiKey); - setLocalBaseUrl(settings.baseUrl); - setLocalModel(settings.model); - } - }; - - const handleSave = () => { - if (typeof localProvider === 'string' && localProvider.startsWith('custom_')) { - // 保存自定义提供商设置 - updateCustomProvider(localProvider, { - apiKey: localApiKey, - baseUrl: localBaseUrl, - model: localModel, - name: customProviders[localProvider]?.name || '自定义模型', - }); - } else { - // 保存内置提供商设置 - updateProviderSettings(localProvider as AIProvider, { - apiKey: localApiKey, - baseUrl: localBaseUrl, - model: localModel, - }); - } - - // 如果提供商发生变化,也要更新 - if (localProvider !== currentProvider) { - setCurrentProvider(localProvider); - } - - toast.show('设置已保存'); - setSettingsOpen(false); - }; - - const handleCreateCustomProvider = () => { - if (!customName || !customBaseUrl || !customModel) { - toast.show('请填写完整的自定义模型信息'); - return; - } - - const newId = addCustomProvider(customName, customBaseUrl, customModel, customApiKey); - setLocalProvider(newId as AllProviders); - setIsCreatingCustom(false); - - // 清空自定义模型表单 - setCustomName(''); - setCustomBaseUrl(''); - setCustomModel(''); - setCustomApiKey(''); - - toast.show('自定义模型已创建'); - }; - - const handleTestConnection = async () => { - if (!localApiKey) { - toast.show('请先输入 API Key'); - return; - } - - setIsTesting(true); - try { - if (typeof localProvider === 'string' && localProvider.startsWith('custom_')) { - // 测试自定义提供商连接 - await APIService.testCustomConnection( - localApiKey, - localBaseUrl, - localModel - ); - } else { - // 测试内置提供商连接 - await APIService.testConnection( - localProvider as AIProvider, - localApiKey, - localBaseUrl, - localModel - ); - } - toast.show('连接测试成功!'); - } catch (error: any) { - // 检查是否是余额不足的警告(连接正常但有余额问题) - if (error.message === 'BALANCE_WARNING' && error.isWarning) { - toast.show(`✅ ${error.originalMessage}`, 5000); - } else { - toast.show(`连接测试失败:${error.message}`, 5000); - } - } finally { - setIsTesting(false); - } - }; - - if (!isSettingsOpen) return null; - - const isCustomProvider = typeof localProvider === 'string' && localProvider.startsWith('custom_'); - const config = isCustomProvider ? null : MODEL_PROVIDERS[localProvider as AIProvider]; - const currentCustom = isCustomProvider ? customProviders[localProvider] : null; - - return ( -
-
-
-

{isCreatingCustom ? '新建自定义模型' : '设置配置'}

- -
-
- {isCreatingCustom ? ( - // 新建自定义模型表单 - <> -
- - setCustomName(e.target.value)} - placeholder="为您的自定义模型起个名字" - /> - 自定义模型的显示名称 -
- -
- - setCustomBaseUrl(e.target.value)} - placeholder="https://api.example.com/v1" - /> - OpenAI 格式 API 的基础 URL -
- -
- - setCustomApiKey(e.target.value)} - placeholder="输入您的 API 密钥" - /> - 您的 API 访问密钥 -
- -
- - setCustomModel(e.target.value)} - placeholder="gpt-3.5-turbo" - /> - 模型的标识符,如 gpt-3.5-turbo -
- - ) : ( - // 正常设置表单 - <> -
- - - 选择您要使用的AI模型提供商 -
- - {isCustomProvider && currentCustom && ( -
- -
-
名称: {currentCustom.name}
-
Base URL: {currentCustom.baseUrl}
-
Model ID: {currentCustom.model}
-
- -
- )} - -
- - setLocalBaseUrl(e.target.value)} - placeholder={isCustomProvider ? currentCustom?.baseUrl : config?.baseUrl} - /> - {isCustomProvider ? '自定义API服务器地址' : config?.baseUrlHint} -
- -
- - setLocalApiKey(e.target.value)} - placeholder="输入你的API密钥" - /> - {isCustomProvider ? '您的API访问密钥' : config?.keyHint} -
- - {!isCustomProvider && config && ( -
- - - 根据您的需求选择合适的AI模型 -
- )} - - {isCustomProvider && ( -
- - setLocalModel(e.target.value)} - placeholder={currentCustom?.model} - /> - 模型的标识符 -
- )} - - )} -
-
- {isCreatingCustom ? ( - <> - - - - ) : ( - <> - -
- - -
- - )} -
-
-
- ); -}; diff --git a/src/components/TitleBar.tsx b/src/components/TitleBar.tsx new file mode 100644 index 00000000..21938878 --- /dev/null +++ b/src/components/TitleBar.tsx @@ -0,0 +1,189 @@ +import React, { useCallback, useEffect, useRef } from "react"; +import { isTauri } from "@tauri-apps/api/core"; +import { getCurrentWindow } from "@tauri-apps/api/window"; +import { useAppStore } from "@/store/useAppStore"; +import { useTranslation } from "react-i18next"; + +type TabKey = "landing" | "analysis" | "analysisResult" | "settings"; + +interface TitleBarProps { + activeTab: TabKey; + onTabChange: (tab: TabKey) => void; +} + +export const TitleBar: React.FC = ({ + activeTab, + onTabChange, +}) => { + const { theme, setTheme } = useAppStore(); + const isTauriRuntime = isTauri(); + const appWindowRef = useRef(isTauriRuntime ? getCurrentWindow() : null); + const { t, i18n } = useTranslation(); + const isChinese = i18n.language.toLowerCase().startsWith("zh"); + const switchToLabel = isChinese + ? t("app.language.english") + : t("app.language.chinese"); + const switchToShort = isChinese + ? t("app.language.short.en") + : t("app.language.short.zh"); + + useEffect(() => { + const root = window.document.documentElement; + + const applyTheme = () => { + const effectiveTheme = + theme === "system" + ? window.matchMedia("(prefers-color-scheme: dark)").matches + ? "dark" + : "light" + : theme; + + if (effectiveTheme === "dark") { + root.classList.add("dark"); + root.setAttribute("data-theme", "dark"); + } else { + root.classList.remove("dark"); + root.setAttribute("data-theme", "light"); + } + }; + + applyTheme(); + + const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); + const handleChange = () => { + if (theme === "system") applyTheme(); + }; + + mediaQuery.addEventListener("change", handleChange); + return () => mediaQuery.removeEventListener("change", handleChange); + }, [theme]); + + const handlePointerDown = useCallback( + (event: React.PointerEvent) => { + if (event.button !== 0) return; + + const target = event.target as HTMLElement; + if (target.closest('[data-tauri-drag-region="false"]')) return; + + if (!appWindowRef.current) return; + + appWindowRef.current.startDragging().catch((error) => { + if (import.meta.env.DEV) { + console.warn("Window drag failed", error); + } + }); + }, + [], + ); + + return ( +
+
+
+ + + +
+
+ +
+ +
+
+
+ + + +
+ + + + +
+ + {isTauriRuntime && ( +
+ + + +
+ )} +
+
+ ); +}; diff --git a/src/config/logoAssets.ts b/src/config/logoAssets.ts new file mode 100644 index 00000000..a912f5f6 --- /dev/null +++ b/src/config/logoAssets.ts @@ -0,0 +1,54 @@ +import openaiLogo from '@lobehub/icons-static-png/light/openai.png'; +import anthropicLogo from '@lobehub/icons-static-png/light/anthropic.png'; +import geminiLogo from '@lobehub/icons-static-png/light/gemini.png'; +import openrouterLogo from '@lobehub/icons-static-png/light/openrouter.png'; +import moonshotLogo from '@lobehub/icons-static-png/light/moonshot.png'; +import zhipuLogo from '@lobehub/icons-static-png/light/zhipu.png'; +import deepseekLogo from '@lobehub/icons-static-png/light/deepseek.png'; +import ollamaLogo from '@lobehub/icons-static-png/light/ollama.png'; +import lmStudioLogo from '@lobehub/icons-static-png/light/lmstudio.png'; +import ai302Logo from '@lobehub/icons-static-png/light/ai302.png'; +import siliconCloudLogo from '@lobehub/icons-static-png/light/siliconcloud.png'; +import wenxinLogo from '@lobehub/icons-static-png/light/wenxin.png'; +import ppioLogo from '@lobehub/icons-static-png/light/ppio.png'; +import modelScopeLogo from '@lobehub/icons-static-png/light/modelscope.png'; +import qwenLogo from '@lobehub/icons-static-png/light/qwen.png'; +import metaLogo from '@lobehub/icons-static-png/light/meta.png'; +import minimaxLogo from '@lobehub/icons-static-png/light/minimax.png'; +import alibabaLogo from '@lobehub/icons-static-png/light/alibaba.png'; +import newapiLogo from '@lobehub/icons-static-png/light/newapi.png'; + +export const PROVIDER_LOGOS = { + openai: openaiLogo, + anthropic: anthropicLogo, + gemini: geminiLogo, + openrouter: openrouterLogo, + moonshot: moonshotLogo, + glm: zhipuLogo, + deepseek: deepseekLogo, + ollama: ollamaLogo, + lmstudio: lmStudioLogo, + comp_share: openaiLogo, + '302_ai': ai302Logo, + pony: openaiLogo, + siliconflow: siliconCloudLogo, + xinghe: wenxinLogo, + ppio: ppioLogo, + modelscope: modelScopeLogo, + newapi: newapiLogo, + oneapi: openaiLogo, +} as const; + +export const MODEL_SOURCE_LOGOS = { + openai: openaiLogo, + qwen: qwenLogo, + 'z-ai': zhipuLogo, + google: geminiLogo, + minimax: minimaxLogo, + 'meta-llama': metaLogo, + openrouter: openrouterLogo, + moonshotai: moonshotLogo, + anthropic: anthropicLogo, + deepseek: deepseekLogo, + alibaba: alibabaLogo, +} as const; diff --git a/src/config/prompts/index.ts b/src/config/prompts/index.ts index 046eb943..cea90f4c 100644 --- a/src/config/prompts/index.ts +++ b/src/config/prompts/index.ts @@ -4,390 +4,134 @@ export const PROMPT_CONFIGS: PromptConfigs = { science: { name: '理工速知', description: '理工科课件整理专家', - prompt: `# Role - -你是一个专业的中文学术助手,专门为中国理工科大学生服务。你必须用中文创建详尽、准确、易于复习的课程备考资料,风格要像一个学霸阅读学习文档后写的详细笔记。 - -# 语言和格式要求 - -## 语言规范 -- **全文必须使用中文**,绝对不能出现英文段落、英文解释或英文建议 -- 专业术语可标注英文,格式:**特征选择**(Feature Selection) -- 所有解释、说明、学习建议都必须用中文 - -## 数学公式格式 -- **行内公式**:用于简单变量,如 $x$、$S_w$ -- **块级公式**:复杂公式必须用 \`$$公式$$\` 且在同一行 -- **严格禁止**:换行的 \`$\\n公式\\n$\` 格式 - -**正确示例:** -\`\`\` -$$d(a,b) = \\sqrt{\\sum_{i=1}^{n}(a_i-b_i)^2}$$ -\`\`\` - -**错误示例:** -\`\`\` -$ -d(a,b) = \\sqrt{\\sum_{i=1}^{n}(a_i-b_i)^2} -$ -\`\`\` - -# Instructions + coreSystemPrompt: '你是中文学术笔记助手,只输出Markdown,无开场白。行内公式$x$,块级$$公式$$同一行,禁止换行$。概念加粗,结论用**【最终结论】**高亮。原文中的图片用标签[[IMG_P页码_序号]]表示,必须在相关文字后插入对应标签,每张图片都要引用,不要遗漏。', + chunkTasks: [ + '梳理本段核心概念与定义,加粗术语,公式附中文说明', + '整理本段例题与解题步骤,保留完整推导,结论高亮', + ], + sectionHeaders: ['基础知识', '典型例题'], + prompt: `你是专业的中文学术助手,为中国理工科大学生创建详尽、易于复习的课程备考资料,风格像学霸的详细笔记。 + +## 格式要求 +- 全文中文,专业术语可标注英文如 **特征选择**(Feature Selection) +- 行内公式用 $x$,块级公式用同一行 $$公式$$,禁止换行的 $公式$ 格式 +- 重要概念加粗,例题结论用 **【最终结论】** 高亮 ## 输出结构 -必须严格按以下两部分结构,顺序输出:"基础知识"和"典型例题" - -## 基础知识部分要求 - -### 标题层级结构 -输出时,请务必先分清楚哪些是标题内容,分别是第几级标题,根据标题不同,用不同的Markdown标题格式: -- **一级标题**:"基础知识"和"典型例题"必须用一级标题,如:"# 基础知识"和"# 典型例题" -- **二级标题**:用于章节序号及标题,忽略文档内原来的章节序号,从1开始编号,如:第1节为"## 1. 数据结构概述",第2节为"## 2. 算法分析" -- **三级标题**:用于章节内的重要子部分,如:第1节的第一部分为"### 1.1 线性结构" -- **四级标题**:用于更深入的细分内容,依此类推 +必须按两部分输出:"基础知识"和"典型例题" -### 内容组织 -分析文档需要体现以下要点,若文档中未包含,则可以忽略不写: -1. **概念定义**: - - 每个专业术语都必须有课件原文的定义或解释 - - 用**加粗**突出核心概念 - - 提供简洁明了的解释和上下文 +## 基础知识 +- 一级标题:# 基础知识 / # 典型例题 +- 二级标题从1编号:## 1. xxx +- 三级标题:### 1.1 xxx +- 概念定义加粗,公式附中文说明 +- 保留图片标签 [[IMG_P页码_序号]],在相关文字后插入 +- 图表转流程图或markdown表格 -2. **公式定理**: - - 完整记录标准形式 - - 复杂公式用 \`$$公式$$\` 格式 - - 附带中文说明,详细解释含义、适用条件和物理意义 - - 重要公式可添加注释和使用注意事项 - -3. **推导过程**: - - 逐步详细推导,不省略中间步骤 - - 每个步骤都有明确的中文说明 - - 重要推导可添加思路解析 - -4. **知识要点**: - - **逐条整理**,确保不遗漏任何知识点 - - 体现概念之间的内在联系和逻辑关系 - - 用**加粗**或特殊标记突出关键信息 - - 添加记忆技巧和理解要点 - -### 图表处理 -1. **图表转换**: - - 图:转换为箭头形式的流程图,例如:输入数据→预处理→建模→结果输出→…… - - 表格:转换为标准markdown表格形式,确保格式清晰 -2. **图表说明**: - - 为每个图表提供简要说明 - - 解释图表的关键信息和结论 - -### 写作风格 -- 使用连贯、流畅的中文叙述 -- 内容组织逻辑清晰,层层递进 -- 适当使用符号标记(✓、⚠️、💡等)增强可读性 -- 保持学术严谨性的同时,加入自己的理解和见解 -- 重点难点处添加笔记和提示 - -## 典型例题部分要求 - -### 例题来源 -- 所有例题必须来自用户文档 -- 完整复现题目和解答过程 -- 如缺少答案则独立计算并提供详细步骤 - -### 解答格式 -- **详细的计算步骤**:每一步都要有明确的说明和推导 -- **逻辑清晰**:解题思路明确,步骤间过渡自然 -- **公式正确**:复杂数学公式必须用 \`$$公式$$\` 格式 -- **结论高亮**:最后结论必须用特殊标记或格式高亮显示,如:**【最终结论】** 结论内容 -- **易错点提醒**:可添加常见错误和注意事项 +## 典型例题 +- 例题来自原文,完整复现题目和解答 +- 详细计算步骤,公式用 $$格式$$ +- 结论高亮 **【最终结论】** ## 质量标准 - -### 学术严谨性 -- 确保所有数学符号使用标准LaTeX语法 -- 单位符号用 \`\\text{单位}\` 格式 -- 每个公式都能正确渲染 -- 所有定义和解释都基于原文,准确无误 - -### 内容完整性 -- **全面覆盖**:处理文档全部内容,不遗漏任何知识点 -- **系统整理**:按原文顺序但更有逻辑性地组织内容 -- **深度理解**:不仅罗列知识点,还解释其意义和应用 - -### 输出要求 -- 直接以 \`# 基础知识\` 开头,不要任何开场白 -- 全程使用中文,保持语言统一 -- 禁止出现英文段落或英文解释 -- 整体风格要像一个认真学习、深入理解的学霸所做的详细笔记 - -# 学霸笔记特色要求 - -1. **知识连接**:展示不同概念之间的联系,构建完整的知识网络 -2. **理解深度**:对复杂概念提供自己的解释和理解角度 -3. **学习建议**:对难点、重点提供学习建议和记忆方法 -4. **应用提示**:说明概念和公式在实际问题中的应用方式 -5. **反思总结**:每部分内容后可添加简短总结和关键要点回顾 - -# 最终检查清单 - -输出前必须确认: -1. ✅ 全文使用中文,无英文段落 -2. ✅ 复杂公式使用 \`$$公式$$\` 同行格式 -3. ✅ 无换行的 \`$公式$\` 格式 -4. ✅ 重要概念已加粗标记 -5. ✅ 内容组织清晰,逻辑连贯 -6. ✅ 标题层级正确使用(一级、二级、三级等) -7. ✅ 图表已转换为流程图或表格形式 -8. ✅ 例题结论已高亮显示 -9. ✅ 整体风格符合学霸笔记的详尽、深入、系统的特点`, +- 全面覆盖不遗漏,系统整理有逻辑 +- 直接以 # 基础知识 开头,无开场白`, }, liberal: { name: '文采丰呈', description: '人文学科课件整理专家', - prompt: `# 人文学科课件整理专家 - -你是一位博学的人文学者,精通文史哲各个领域,擅长整理和阐释人文学科内容。请按照以下结构对文档进行深入分析: - -## 📖 文献概览 -- **学科归属**:确定文档所属的人文学科领域 -- **时代背景**:分析内容的历史文化背景 -- **文本性质**:判断文本的类型和特点 - -## 🎨 主题思想 -### 核心观点 -- 提取文献的主要思想和观点 -- 分析作者的立场和态度 -- 识别隐含的思想倾向 - -### 思想脉络 -- 梳理观点的逻辑发展脉络 -- 分析论证的方法和结构 -- 探讨思想的深层含义 - -## 📚 知识要点 -### 重要概念 -- 解释关键的人文概念和术语 -- 分析概念的内涵和外延 -- 探讨概念的演变和发展 - -### 理论框架 -- 整理重要的理论观点和体系 -- 分析理论的适用范围和局限 -- 比较不同理论的异同 - -## 🌍 文化解读 -### 历史语境 -- 分析内容的历史文化语境 -- 探讨时代特征对内容的影响 -- 比较不同历史时期的变化 - -### 文化意蕴 -- 挖掘深层的文化内涵 -- 分析文化符号和象征意义 -- 探讨跨文化的对话和交流 - -## 💭 批判思考 -### 多元视角 -- 从不同角度分析同一问题 -- 比较不同学派的观点分歧 -- 探讨观点的合理性和局限性 - -### 现代意义 -- 分析内容的当代价值和意义 -- 探讨传统文化的现代转化 -- 思考人文精神的时代表达 - -## 📝 文本分析 -### 修辞手法 -- 分析文本的语言特色和风格 -- 识别重要的修辞技巧和表达方式 -- 评价语言的美学价值 + coreSystemPrompt: '你是人文学术笔记助手,只输出Markdown,无开场白。核心观点加粗,语言典雅通顺。原文中的图片用标签[[IMG_P页码_序号]]表示,必须在相关文字后插入对应标签,每张图片都要引用。', + chunkTasks: [ + '梳理本段核心思想与概念辨析,加粗关键论点', + '拓展本段比较视野与当代价值,体现批判性思考', + ], + sectionHeaders: ['核心解读', '思辨拓展'], + prompt: `你是深厚人文素养的学术助手,为文史哲方向大学生做精读笔记,风格博学多才、富有洞察力。 + +## 格式要求 +- 全文中文,语言典雅通顺 +- 核心观点和金句加粗,段落层次清晰 -### 结构特征 -- 分析文本的组织结构和逻辑 -- 探讨叙述方式和表达技巧 -- 评价文本的完整性和条理性 - -## 🔍 学术价值 -### 研究意义 -- 评估内容的学术价值和贡献 -- 分析在学科发展中的地位 -- 探讨对后续研究的启发 - -### 影响评价 -- 分析对思想文化的影响 -- 评价在教育传播中的作用 -- 探讨对社会发展的意义 - -## 🎯 学习指导 -### 重点难点 -- 标识学习的重点和难点 -- 提供理解的方法和途径 -- 设计有效的学习策略 - -### 延伸思考 -- 提出深入思考的问题 -- 推荐相关的阅读材料 -- 设计创新的学习活动 - -## 📖 知识网络 -### 关联拓展 -- 建立与其他知识的联系 -- 构建系统的知识网络 -- 促进跨学科的理解 +## 输出结构 +两部分:"核心解读"和"思辨拓展" -### 实践应用 -- 探讨知识的实际应用 -- 设计创新的实践活动 -- 培养人文素养和批判思维 +## 核心解读 +- # 核心解读 +- 背景还原:作者生平、创作背景、时代特征 +- 义理阐释:核心思想、概念辨析 +- 文本分析:修辞意象或论证逻辑 ---- +## 思辨拓展 +- # 思辨拓展 +- 比较视野:横向对比、纵向演变 +- 当代价值:现实启示、批判性思考 -请用典雅而通俗的中文输出,体现人文学科的深度和温度。注重培养学生的人文素养、批判思维和创新能力。`, +## 质量标准 +- 深度穿透,理性分析体现人文关怀 +- 论述有理有据`, }, data: { name: '罗森析数', description: '数据内容分析专家', - prompt: `# 数据内容分析专家 - -你是一位专业的数据分析师,擅长从文档中提取、分析和解读各种数据信息。请按照以下结构对数据内容进行深入分析: - -## 📊 数据概览 -- **数据类型**:识别文档中包含的数据类型(数值型、分类型、时间序列等) -- **数据规模**:评估数据的规模和复杂程度 -- **数据来源**:分析数据的来源和可信度 - -## 🔢 数值分析 -### 描述统计 -- 提取关键的统计数值(均值、中位数、极值等) -- 计算重要的比率和百分比 -- 识别数据的分布特征 - -### 趋势分析 -- 分析数据的时间趋势和变化模式 -- 识别周期性和季节性特征 -- 预测可能的发展趋势 - -## 📈 关键指标 -### 核心KPI -- 提取最重要的关键绩效指标 -- 分析指标的含义和重要性 -- 评估指标的合理性和有效性 - -### 对比分析 -- 进行同期对比和环比分析 -- 识别显著的差异和变化 -- 分析变化的可能原因 - -## 💡 数据洞察 -### 模式识别 -- 发现数据中的规律和模式 -- 识别异常值和特殊情况 -- 分析数据背后的业务逻辑 - -### 相关性分析 -- 探索不同数据之间的关联关系 -- 识别影响因素和驱动要素 -- 分析因果关系的可能性 - -## 📋 风险评估 -### 数据质量 -- 评估数据的完整性和准确性 -- 识别可能的数据偏差或限制 -- 提出数据质量改进建议 - -### 业务风险 -- 基于数据分析识别潜在风险 -- 评估风险的影响程度和可能性 -- 提出风险预警和应对策略 + coreSystemPrompt: '你是数据商业分析助手,只输出Markdown,无开场白。关键数据加粗,图表描述趋势。原文中的图片用标签[[IMG_P页码_序号]]表示,必须在相关文字后插入对应标签,每张图片都要引用。', + chunkTasks: [ + '提炼本段核心发现与关键指标变化', + '详述本段趋势分析与结构分析数据', + ], + sectionHeaders: ['分析结论', '数据详情'], + prompt: `你是资深数据商业分析师,用理性客观的中文提炼数据洞察,风格像专业咨询报告。 + +## 格式要求 +- 全文中文,简练准确 +- 关键数据加粗,图表描述趋势 -## 🎯 决策建议 -### 数据结论 -- 总结最重要的数据发现 -- 提供基于数据的判断和结论 -- 量化分析的置信度 - -### 行动建议 -- 基于数据分析提出具体建议 -- 设定可量化的目标和指标 -- 制定数据驱动的决策方案 - -## 📊 可视化建议 -### 图表推荐 -- 推荐适合的数据可视化方式 -- 设计有效的图表展示方案 -- 突出关键信息的展示方法 +## 输出结构 +两部分:"分析结论"和"数据详情" -### 报告结构 -- 建议数据报告的组织结构 -- 设计清晰的信息层级 -- 提供数据storytelling的建议 +## 分析结论 +- # 分析结论 +- 核心发现:3-5句话概括最重要结论 +- 关键指标:核心KPIs及变化 +- 行动建议:具体可落地 ---- +## 数据详情 +- # 数据详情 +- 趋势分析、结构分析、异常监测 -请用专业而易懂的中文输出,确保数据分析的准确性和实用性。对于复杂的数据关系,请提供清晰的解释和可视化建议。`, +## 质量标准 +- 数字精确,结论由数据推导 +- MECE原则,多用对比`, }, news: { name: '要闻概览', description: '新闻要点梳理专家', - prompt: `# 新闻要点梳理专家 - -你是一位专业的新闻分析师,擅长快速提取新闻要点和深度分析。请按照以下结构对新闻内容进行梳理: - -## 📰 新闻概要 -- **标题要旨**:提炼新闻核心主题 -- **时效性**:事件发生时间和报道时间 -- **重要程度**:评估新闻的影响力等级 - -## 🎯 关键信息 -### 核心事实 -- 何时(When):事件发生的时间 -- 何地(Where):事件发生的地点 -- 何人(Who):涉及的主要人物或机构 -- 何事(What):具体发生了什么 -- 为何(Why):事件的原因和背景 -- 如何(How):事件的过程和方式 - -### 数据要点 -- 提取关键数字和统计数据 -- 标注数据的重要性和可信度 + coreSystemPrompt: '你是新闻分析助手,只输出Markdown,无开场白。涵盖5W1H,复杂事件用时间轴。原文中的图片用标签[[IMG_P页码_序号]]表示,必须在相关文字后插入对应标签,每张图片都要引用。', + chunkTasks: [ + '概括本段核心事件要素与影响评级', + '深入分析本段背景回溯与各方立场', + ], + sectionHeaders: ['要闻速览', '深度透视'], + prompt: `你是严谨的新闻分析师,用客观凝练的中文梳理新闻事件,风格像高层内参简报。 + +## 格式要求 +- 全文中文,文风冷静客观 +- 涵盖5W1H,涉及复杂事件用时间轴 -## 📊 影响分析 -### 直接影响 -- 对相关人群的immediate影响 -- 对相关行业或领域的影响 - -### 深远意义 -- 长期影响和趋势分析 -- 对政策、经济、社会的potential影响 - -## 🔍 背景解读 -### 历史背景 -- 相关的历史事件或政策背景 -- 类似事件的先例分析 - -### 现状分析 -- 当前的相关情况和环境 -- 各方立场和观点 - -## 💭 观点汇总 -### 官方立场 -- 政府或官方机构的态度 -- 相关政策或法规 - -### 多方声音 -- 专家学者的分析观点 -- 公众和媒体的反应 - -## 🎯 要点提炼 -### 核心观点 -- 最重要的3-5个要点 -- 需要重点关注的信息 +## 输出结构 +两部分:"要闻速览"和"深度透视" -### 后续关注 -- 值得持续跟踪的发展 -- 可能的后续事件预测 +## 要闻速览 +- # 要闻速览 +- 一句话摘要(100字内) +- 核心要素表 +- 影响评级 ---- +## 深度透视 +- # 深度透视 +- 背景回溯、各方立场、未来展望 -请用简洁明了的中文输出,确保信息准确客观。对于复杂事件,请提供多角度的分析视角。`, +## 质量标准 +- 中立不偏袒,信息多元交叉验证 +- 去伪存真,剔除营销话术`, }, }; diff --git a/src/config/providers.ts b/src/config/providers.ts index a6182d36..7651b111 100644 --- a/src/config/providers.ts +++ b/src/config/providers.ts @@ -1,10 +1,30 @@ -import { ModelProviders, CustomProviderConfig } from '@/types'; +import { ModelProviders, CustomProviderConfig } from "@/types"; +import { PROVIDER_LOGOS } from "./logoAssets"; +import OneDocsIcon from "../../app-icon.png"; + +const sanitizeEnvValue = (value: unknown) => + typeof value === "string" ? value.trim() : ""; + +let ONEDOCS_BASE_URL = sanitizeEnvValue(import.meta.env.VITE_ONEDOCS_API_URL); +if (ONEDOCS_BASE_URL && !ONEDOCS_BASE_URL.endsWith("/v1")) { + ONEDOCS_BASE_URL = `${ONEDOCS_BASE_URL.replace(/\/$/, "")}/v1`; +} + +const ONEDOCS_API_KEY = sanitizeEnvValue(import.meta.env.VITE_ONEDOCS_API_KEY); +const HAS_MANAGED_ONEDOCS_CREDENTIALS = Boolean( + ONEDOCS_BASE_URL && ONEDOCS_API_KEY, +); + +const MODEL_TAGS = { + flagship: { label: "旗舰", variant: "flagship" as const }, + affordable: { label: "平价", variant: "affordable" as const }, + free: { label: "免费", variant: "free" as const }, +}; -// 创建自定义模型配置的辅助函数 export const createCustomProvider = ( name: string, baseUrl: string, - model: string + model: string, ): CustomProviderConfig => ({ id: `custom_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, name, @@ -14,83 +34,514 @@ export const createCustomProvider = ( }); export const MODEL_PROVIDERS: ModelProviders = { + onedocs: { + name: "OneDocs", + baseUrl: ONEDOCS_BASE_URL, + endpoint: "/chat/completions", + models: [ + { + value: "openai/gpt-oss-120b:free", + name: "gpt-oss-120b", + }, + { + value: "qwen/qwen3-next-80b-a3b-instruct:free", + name: "qwen3-next-80b-a3b-instruct", + }, + { + value: "z-ai/glm-4.5-air:free", + name: "glm-4.5-air", + }, + { + value: "google/gemma-4-26b-a4b-it:free", + name: "gemma-4-26b-a4b-it", + }, + { + value: "minimax/minimax-m2.5:free", + name: "minimax-m2.5", + }, + { + value: "meta-llama/llama-3.3-70b-instruct:free", + name: "llama-3.3-70b-instruct", + }, + ], + defaultModel: "openai/gpt-oss-120b:free", + keyLabel: "OneDocs API Key", + keyHint: HAS_MANAGED_ONEDOCS_CREDENTIALS + ? "OneDocs 内置凭证,无需额外填写" + : "请输入 OneDocs API Key", + baseUrlHint: HAS_MANAGED_ONEDOCS_CREDENTIALS + ? "OneDocs 内置服务地址" + : "请输入 OneDocs 服务地址", + icon: OneDocsIcon, + badgeText: "免费模型", + badgeVariant: "success", + requiresApiKey: !HAS_MANAGED_ONEDOCS_CREDENTIALS, + requiresBaseUrl: !HAS_MANAGED_ONEDOCS_CREDENTIALS, + showApiKeyField: !HAS_MANAGED_ONEDOCS_CREDENTIALS, + showBaseUrlField: !HAS_MANAGED_ONEDOCS_CREDENTIALS, + credentialsReadOnly: HAS_MANAGED_ONEDOCS_CREDENTIALS, + allowModelCustomization: false, + defaultApiKey: HAS_MANAGED_ONEDOCS_CREDENTIALS + ? ONEDOCS_API_KEY + : undefined, + description: + "OneDocs为用户提供免费模型,开箱即用,额度原因有概率无法使用,若无法使用请替换其他模型,感谢配合!", + }, openai: { - name: 'OpenAI', - baseUrl: 'https://api.openai.com/v1', - endpoint: '/chat/completions', + name: "OpenAI", + baseUrl: "https://api.openai.com/v1", + endpoint: "/chat/completions", models: [ - { value: 'gpt-4o', name: 'GPT-4o' }, - { value: 'gpt-4o-mini', name: 'GPT-4o-mini' }, - { value: 'gpt-4', name: 'GPT-4' }, - { value: 'gpt-3.5-turbo', name: 'GPT-3.5 Turbo' }, + { value: "openai/gpt-5.5", name: "GPT-5.5", tags: [MODEL_TAGS.flagship] }, + { value: "openai/gpt-5.4", name: "GPT-5.4", tags: [MODEL_TAGS.flagship] }, + { value: "openai/gpt-5.4-mini", name: "GPT-5.4 mini", tags: [MODEL_TAGS.flagship] }, + { value: "openai/gpt-5-mini", name: "GPT-5 mini", tags: [MODEL_TAGS.flagship] }, + { value: "openai/gpt-5.4-nano", name: "GPT-5.4 nano", tags: [MODEL_TAGS.affordable] }, + { value: "openai/gpt-4o-mini", name: "GPT-4o mini", tags: [MODEL_TAGS.affordable] }, ], - defaultModel: 'gpt-4o', - keyLabel: 'OpenAI API Key', - keyHint: '需要填入有效的OpenAI API密钥方可使用', - baseUrlHint: 'API服务器地址,默认为OpenAI官方地址', + defaultModel: "openai/gpt-5.5", + keyLabel: "OpenAI API Key", + keyHint: "需要填入有效的OpenAI API密钥方可使用", + baseUrlHint: "API服务器地址,默认为OpenAI官方地址", + icon: PROVIDER_LOGOS.openai, }, - deepseek: { - name: 'DeepSeek', - baseUrl: 'https://api.deepseek.com/v1', - endpoint: '/chat/completions', + anthropic: { + name: "Anthropic", + baseUrl: "https://api.anthropic.com/v1", + endpoint: "/messages", + models: [ + { value: "claude-opus-4.7", name: "Claude Opus 4.7", tags: [MODEL_TAGS.flagship] }, + { value: "claude-opus-4.6", name: "Claude Opus 4.6", tags: [MODEL_TAGS.flagship] }, + { value: "claude-sonnet-4.6", name: "Claude Sonnet 4.6", tags: [MODEL_TAGS.affordable] }, + { value: "claude-sonnet-4.5", name: "Claude Sonnet 4.5", tags: [MODEL_TAGS.affordable] }, + { value: "claude-haiku-4.5", name: "Claude Haiku 4.5", tags: [MODEL_TAGS.affordable] }, + ], + defaultModel: "claude-opus-4.7", + keyLabel: "Anthropic API Key", + keyHint: "请输入 Anthropic API Key", + baseUrlHint: "API服务器地址", + icon: PROVIDER_LOGOS.anthropic, + }, + gemini: { + name: "Google Gemini", + baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai", + endpoint: "/chat/completions", + models: [ + { value: "gemini-3.1-pro-preview", name: "Gemini 3.1 Pro Preview", tags: [MODEL_TAGS.flagship] }, + { value: "gemini-2.5-pro", name: "Gemini 2.5 Pro", tags: [MODEL_TAGS.flagship] }, + { value: "gemini-3.1-flash-lite-preview", name: "Gemini 3.1 Flash Lite Preview", tags: [MODEL_TAGS.flagship] }, + { value: "gemini-2.5-flash", name: "Gemini 2.5 Flash", tags: [MODEL_TAGS.affordable] }, + { value: "gemini-2.5-flash-lite", name: "Gemini 2.5 Flash Lite", tags: [MODEL_TAGS.affordable] }, + { value: "gemini-2.0-flash", name: "Gemini 2.0 Flash", tags: [MODEL_TAGS.affordable] }, + ], + defaultModel: "gemini-3.1-pro-preview", + keyLabel: "Google API Key", + keyHint: "请输入 Google AI Studio API Key", + baseUrlHint: "API服务器地址", + icon: PROVIDER_LOGOS.gemini, + }, + openrouter: { + name: "OpenRouter", + baseUrl: "https://openrouter.ai/api/v1", + endpoint: "/chat/completions", models: [ - { value: 'deepseek-chat', name: 'DeepSeek-Chat (推荐)' }, - { value: 'deepseek-reasoner', name: 'DeepSeek-Reasoner (推理模型)' }, + { value: "anthropic/claude-opus-4.7", name: "Claude Opus 4.7", tags: [MODEL_TAGS.flagship] }, + { value: "anthropic/claude-sonnet-4.6", name: "Claude Sonnet 4.6", tags: [MODEL_TAGS.flagship] }, + { value: "google/gemini-3.1-pro-preview", name: "Gemini 3.1 Pro Preview", tags: [MODEL_TAGS.flagship] }, + { value: "google/gemini-3-flash-preview", name: "Gemini 3 Flash Preview", tags: [MODEL_TAGS.flagship] }, + { value: "openai/gpt-5.5", name: "GPT-5.5", tags: [MODEL_TAGS.flagship] }, + { value: "openai/gpt-5-mini", name: "GPT-5 mini", tags: [MODEL_TAGS.flagship] }, + { value: "qwen/qwen3.5-397b-a17b", name: "Qwen3.5 397B A17B", tags: [MODEL_TAGS.flagship] }, + { value: "z-ai/glm-5.1", name: "GLM-5.1", tags: [MODEL_TAGS.flagship] }, + { value: "moonshotai/kimi-k2.6", name: "Kimi K2.6", tags: [MODEL_TAGS.flagship] }, + { value: "minimax/minimax-m2.7", name: "MiniMax M2.7", tags: [MODEL_TAGS.flagship] }, + { value: "google/gemini-3.1-flash-lite-preview", name: "Gemini 3.1 Flash Lite Preview", tags: [MODEL_TAGS.affordable] }, + { value: "openai/gpt-4o-mini", name: "GPT-4o mini", tags: [MODEL_TAGS.affordable] }, + { value: "qwen/qwen3.6-plus", name: "Qwen3.6 Plus", tags: [MODEL_TAGS.affordable] }, + { value: "qwen/qwen3.6-flash", name: "Qwen3.6 Flash", tags: [MODEL_TAGS.affordable] }, + { value: "z-ai/glm-4.7", name: "GLM-4.7", tags: [MODEL_TAGS.affordable] }, + { value: "openai/gpt-oss-120b:free", name: "GPT-OSS 120B", tags: [MODEL_TAGS.free] }, ], - defaultModel: 'deepseek-chat', - keyLabel: 'DeepSeek API Key', - keyHint: '需要填入有效的DeepSeek API密钥方可使用,请确保账户余额充足', - baseUrlHint: 'DeepSeek API服务器地址', + defaultModel: "anthropic/claude-opus-4.7", + keyLabel: "OpenRouter API Key", + keyHint: "请输入 OpenRouter API Key", + baseUrlHint: "OpenRouter API服务器地址", + icon: PROVIDER_LOGOS.openrouter, + description: "OpenRouter 聚合多家模型提供方,使用 OpenAI 兼容接口。", + }, + moonshot: { + name: "Moonshot AI", + baseUrl: "https://api.moonshot.cn/v1", + endpoint: "/chat/completions", + models: [ + { value: "kimi-k2.6", name: "Kimi K2.6", tags: [MODEL_TAGS.flagship] }, + { value: "kimi-k2-0905", name: "Kimi K2 0905", tags: [MODEL_TAGS.flagship] }, + { value: "kimi-k2", name: "Kimi K2", tags: [MODEL_TAGS.affordable] }, + { value: "moonshot-v1-128k", name: "Moonshot V1 128K", tags: [MODEL_TAGS.affordable] }, + ], + defaultModel: "kimi-k2.6", + keyLabel: "Moonshot API Key", + keyHint: "请输入 Kimi 开放平台 API Key", + baseUrlHint: "API服务器地址", + icon: PROVIDER_LOGOS.moonshot, }, glm: { - name: '智谱GLM', - baseUrl: 'https://open.bigmodel.cn/api/paas/v4', - endpoint: '/chat/completions', - models: [ - { value: 'glm-4-flashx', name: 'GLM-4-FlashX (推荐)' }, - { value: 'glm-4-plus', name: 'GLM-4-Plus' }, - { value: 'glm-4-0520', name: 'GLM-4-0520' }, - { value: 'glm-4-long', name: 'GLM-4-Long' }, - { value: 'glm-4-flash', name: 'GLM-4-Flash' }, - { value: 'glm-4v-plus', name: 'GLM-4V-Plus (多模态)' }, - ], - defaultModel: 'glm-4-flashx', - keyLabel: '智谱 API Key', - keyHint: '需要填入有效的智谱API密钥方可使用', - baseUrlHint: '智谱GLM API服务器地址', + name: "智谱GLM", + baseUrl: "https://open.bigmodel.cn/api/paas/v4", + endpoint: "/chat/completions", + models: [ + { value: "glm-5.1", name: "GLM-5.1", tags: [MODEL_TAGS.flagship] }, + { value: "glm-5", name: "GLM-5", tags: [MODEL_TAGS.flagship] }, + { value: "glm-5v-turbo", name: "GLM-5V Turbo", tags: [MODEL_TAGS.flagship] }, + { value: "glm-5-turbo", name: "GLM-5 Turbo", tags: [MODEL_TAGS.affordable] }, + { value: "glm-4.7", name: "GLM-4.7", tags: [MODEL_TAGS.affordable] }, + { value: "glm-4.7-flash", name: "GLM-4.7 Flash", tags: [MODEL_TAGS.affordable] }, + ], + defaultModel: "glm-5.1", + keyLabel: "智谱 API Key", + keyHint: "需要填入有效的智谱API密钥方可使用", + baseUrlHint: "智谱GLM API服务器地址", + icon: PROVIDER_LOGOS.glm, + }, + deepseek: { + name: "DeepSeek", + baseUrl: "https://api.deepseek.com", + endpoint: "/chat/completions", + models: [ + { value: "deepseek-v4-pro", name: "DeepSeek V4 Pro", tags: [MODEL_TAGS.flagship] }, + { value: "deepseek-r1-0528", name: "DeepSeek R1 0528", tags: [MODEL_TAGS.flagship] }, + { value: "deepseek-v3.2", name: "DeepSeek V3.2", tags: [MODEL_TAGS.flagship] }, + { value: "deepseek-v4-flash", name: "DeepSeek V4 Flash", tags: [MODEL_TAGS.affordable] }, + { value: "deepseek-chat-v3.1", name: "DeepSeek Chat V3.1", tags: [MODEL_TAGS.affordable] }, + { value: "deepseek-reasoner", name: "DeepSeek Reasoner", tags: [MODEL_TAGS.affordable] }, + ], + defaultModel: "deepseek-v4-pro", + keyLabel: "DeepSeek API Key", + keyHint: "需要填入有效的DeepSeek API密钥方可使用,请确保账户余额充足", + baseUrlHint: "DeepSeek API服务器地址", + icon: PROVIDER_LOGOS.deepseek, + }, + ollama: { + name: "Ollama", + baseUrl: "http://localhost:11434/v1", + endpoint: "/chat/completions", + models: [ + { value: "qwen3-coder:30b-a3b", name: "Qwen3 Coder 30B A3B", tags: [MODEL_TAGS.flagship] }, + { value: "llama3.3:70b-instruct", name: "Llama 3.3 70B Instruct", tags: [MODEL_TAGS.flagship] }, + { value: "deepseek-r1:70b", name: "DeepSeek-R1 70B", tags: [MODEL_TAGS.flagship] }, + { value: "qwen3:14b", name: "Qwen3 14B", tags: [MODEL_TAGS.affordable] }, + { value: "gemma3:27b", name: "Gemma 3 27B", tags: [MODEL_TAGS.affordable] }, + { value: "mistral-small:24b", name: "Mistral Small 24B", tags: [MODEL_TAGS.affordable] }, + { value: "qwen2.5:7b-instruct", name: "Qwen2.5 7B", tags: [MODEL_TAGS.free] }, + { value: "llama3.2:3b-instruct", name: "Llama 3.2 3B", tags: [MODEL_TAGS.free] }, + { value: "phi3.5", name: "Phi-3.5", tags: [MODEL_TAGS.free] }, + ], + defaultModel: "qwen3-coder:30b-a3b", + keyLabel: "API Key (可选)", + keyHint: "本地部署通常不需要 API Key", + baseUrlHint: "Ollama 服务地址", + icon: PROVIDER_LOGOS.ollama, + }, + lmstudio: { + name: "LM Studio", + baseUrl: "http://localhost:1234/v1", + endpoint: "/chat/completions", + models: [ + { value: "qwen3-coder-30b-a3b-instruct", name: "Qwen3 Coder 30B A3B", tags: [MODEL_TAGS.flagship] }, + { value: "llama-3.3-70b-instruct", name: "Llama 3.3 70B Instruct", tags: [MODEL_TAGS.flagship] }, + { value: "deepseek-r1-70b", name: "DeepSeek-R1 70B", tags: [MODEL_TAGS.flagship] }, + { value: "qwen3-14b", name: "Qwen3 14B", tags: [MODEL_TAGS.affordable] }, + { value: "gemma-3-27b", name: "Gemma 3 27B", tags: [MODEL_TAGS.affordable] }, + { value: "mistral-small-24b", name: "Mistral Small 24B", tags: [MODEL_TAGS.affordable] }, + { value: "local-model", name: "Local Model", tags: [MODEL_TAGS.free] }, + ], + defaultModel: "qwen3-coder-30b-a3b-instruct", + keyLabel: "API Key (可选)", + keyHint: "本地部署通常不需要 API Key", + baseUrlHint: "LM Studio 服务地址", + icon: PROVIDER_LOGOS.lmstudio, + }, + comp_share: { + name: "优云智算", + baseUrl: "https://api.compshare.cn/v1", + endpoint: "/chat/completions", + models: [ + { value: "anthropic/claude-opus-4.7", name: "Claude Opus 4.7", tags: [MODEL_TAGS.flagship] }, + { value: "anthropic/claude-sonnet-4.6", name: "Claude Sonnet 4.6", tags: [MODEL_TAGS.flagship] }, + { value: "google/gemini-3.1-pro-preview", name: "Gemini 3.1 Pro Preview", tags: [MODEL_TAGS.flagship] }, + { value: "google/gemini-3-flash-preview", name: "Gemini 3 Flash Preview", tags: [MODEL_TAGS.flagship] }, + { value: "openai/gpt-5.5", name: "GPT-5.5", tags: [MODEL_TAGS.flagship] }, + { value: "openai/gpt-5-mini", name: "GPT-5 mini", tags: [MODEL_TAGS.flagship] }, + { value: "qwen/qwen3.5-397b-a17b", name: "Qwen3.5 397B A17B", tags: [MODEL_TAGS.flagship] }, + { value: "z-ai/glm-5.1", name: "GLM-5.1", tags: [MODEL_TAGS.flagship] }, + { value: "moonshotai/kimi-k2.6", name: "Kimi K2.6", tags: [MODEL_TAGS.flagship] }, + { value: "minimax/minimax-m2.7", name: "MiniMax M2.7", tags: [MODEL_TAGS.flagship] }, + { value: "google/gemini-3.1-flash-lite-preview", name: "Gemini 3.1 Flash Lite Preview", tags: [MODEL_TAGS.affordable] }, + { value: "openai/gpt-4o-mini", name: "GPT-4o mini", tags: [MODEL_TAGS.affordable] }, + { value: "qwen/qwen3.6-plus", name: "Qwen3.6 Plus", tags: [MODEL_TAGS.affordable] }, + { value: "qwen/qwen3.6-flash", name: "Qwen3.6 Flash", tags: [MODEL_TAGS.affordable] }, + { value: "z-ai/glm-4.7", name: "GLM-4.7", tags: [MODEL_TAGS.affordable] }, + { value: "openai/gpt-oss-120b:free", name: "GPT-OSS 120B", tags: [MODEL_TAGS.free] }, + ], + defaultModel: "anthropic/claude-opus-4.7", + keyLabel: "API Key", + keyHint: "请输入优云智算 API Key", + baseUrlHint: "API服务器地址", + icon: PROVIDER_LOGOS.comp_share, + }, + "302_ai": { + name: "302.AI", + baseUrl: "https://api.302.ai/v1", + endpoint: "/chat/completions", + models: [ + { value: "anthropic/claude-opus-4.7", name: "Claude Opus 4.7", tags: [MODEL_TAGS.flagship] }, + { value: "anthropic/claude-sonnet-4.6", name: "Claude Sonnet 4.6", tags: [MODEL_TAGS.flagship] }, + { value: "google/gemini-3.1-pro-preview", name: "Gemini 3.1 Pro Preview", tags: [MODEL_TAGS.flagship] }, + { value: "google/gemini-3-flash-preview", name: "Gemini 3 Flash Preview", tags: [MODEL_TAGS.flagship] }, + { value: "openai/gpt-5.5", name: "GPT-5.5", tags: [MODEL_TAGS.flagship] }, + { value: "openai/gpt-5-mini", name: "GPT-5 mini", tags: [MODEL_TAGS.flagship] }, + { value: "qwen/qwen3.5-397b-a17b", name: "Qwen3.5 397B A17B", tags: [MODEL_TAGS.flagship] }, + { value: "z-ai/glm-5.1", name: "GLM-5.1", tags: [MODEL_TAGS.flagship] }, + { value: "moonshotai/kimi-k2.6", name: "Kimi K2.6", tags: [MODEL_TAGS.flagship] }, + { value: "minimax/minimax-m2.7", name: "MiniMax M2.7", tags: [MODEL_TAGS.flagship] }, + { value: "google/gemini-3.1-flash-lite-preview", name: "Gemini 3.1 Flash Lite Preview", tags: [MODEL_TAGS.affordable] }, + { value: "openai/gpt-4o-mini", name: "GPT-4o mini", tags: [MODEL_TAGS.affordable] }, + { value: "qwen/qwen3.6-plus", name: "Qwen3.6 Plus", tags: [MODEL_TAGS.affordable] }, + { value: "qwen/qwen3.6-flash", name: "Qwen3.6 Flash", tags: [MODEL_TAGS.affordable] }, + { value: "z-ai/glm-4.7", name: "GLM-4.7", tags: [MODEL_TAGS.affordable] }, + { value: "openai/gpt-oss-120b:free", name: "GPT-OSS 120B", tags: [MODEL_TAGS.free] }, + ], + defaultModel: "anthropic/claude-opus-4.7", + keyLabel: "API Key", + keyHint: "请输入 302.AI API Key", + baseUrlHint: "API服务器地址", + icon: PROVIDER_LOGOS['302_ai'], + }, + pony: { + name: "小马算力", + baseUrl: "https://api.tokenpony.cn/v1", + endpoint: "/chat/completions", + models: [ + { value: "anthropic/claude-opus-4.7", name: "Claude Opus 4.7", tags: [MODEL_TAGS.flagship] }, + { value: "anthropic/claude-sonnet-4.6", name: "Claude Sonnet 4.6", tags: [MODEL_TAGS.flagship] }, + { value: "google/gemini-3.1-pro-preview", name: "Gemini 3.1 Pro Preview", tags: [MODEL_TAGS.flagship] }, + { value: "google/gemini-3-flash-preview", name: "Gemini 3 Flash Preview", tags: [MODEL_TAGS.flagship] }, + { value: "openai/gpt-5.5", name: "GPT-5.5", tags: [MODEL_TAGS.flagship] }, + { value: "openai/gpt-5-mini", name: "GPT-5 mini", tags: [MODEL_TAGS.flagship] }, + { value: "qwen/qwen3.5-397b-a17b", name: "Qwen3.5 397B A17B", tags: [MODEL_TAGS.flagship] }, + { value: "z-ai/glm-5.1", name: "GLM-5.1", tags: [MODEL_TAGS.flagship] }, + { value: "moonshotai/kimi-k2.6", name: "Kimi K2.6", tags: [MODEL_TAGS.flagship] }, + { value: "minimax/minimax-m2.7", name: "MiniMax M2.7", tags: [MODEL_TAGS.flagship] }, + { value: "google/gemini-3.1-flash-lite-preview", name: "Gemini 3.1 Flash Lite Preview", tags: [MODEL_TAGS.affordable] }, + { value: "openai/gpt-4o-mini", name: "GPT-4o mini", tags: [MODEL_TAGS.affordable] }, + { value: "qwen/qwen3.6-plus", name: "Qwen3.6 Plus", tags: [MODEL_TAGS.affordable] }, + { value: "qwen/qwen3.6-flash", name: "Qwen3.6 Flash", tags: [MODEL_TAGS.affordable] }, + { value: "z-ai/glm-4.7", name: "GLM-4.7", tags: [MODEL_TAGS.affordable] }, + { value: "openai/gpt-oss-120b:free", name: "GPT-OSS 120B", tags: [MODEL_TAGS.free] }, + ], + defaultModel: "anthropic/claude-opus-4.7", + keyLabel: "API Key", + keyHint: "请输入小马算力 API Key", + baseUrlHint: "API服务器地址", + icon: PROVIDER_LOGOS.pony, + }, + siliconflow: { + name: "硅基流动", + baseUrl: "https://api.siliconflow.cn/v1", + endpoint: "/chat/completions", + models: [ + { value: "anthropic/claude-opus-4.7", name: "Claude Opus 4.7", tags: [MODEL_TAGS.flagship] }, + { value: "anthropic/claude-sonnet-4.6", name: "Claude Sonnet 4.6", tags: [MODEL_TAGS.flagship] }, + { value: "google/gemini-3.1-pro-preview", name: "Gemini 3.1 Pro Preview", tags: [MODEL_TAGS.flagship] }, + { value: "google/gemini-3-flash-preview", name: "Gemini 3 Flash Preview", tags: [MODEL_TAGS.flagship] }, + { value: "openai/gpt-5.5", name: "GPT-5.5", tags: [MODEL_TAGS.flagship] }, + { value: "openai/gpt-5-mini", name: "GPT-5 mini", tags: [MODEL_TAGS.flagship] }, + { value: "qwen/qwen3.5-397b-a17b", name: "Qwen3.5 397B A17B", tags: [MODEL_TAGS.flagship] }, + { value: "z-ai/glm-5.1", name: "GLM-5.1", tags: [MODEL_TAGS.flagship] }, + { value: "moonshotai/kimi-k2.6", name: "Kimi K2.6", tags: [MODEL_TAGS.flagship] }, + { value: "minimax/minimax-m2.7", name: "MiniMax M2.7", tags: [MODEL_TAGS.flagship] }, + { value: "google/gemini-3.1-flash-lite-preview", name: "Gemini 3.1 Flash Lite Preview", tags: [MODEL_TAGS.affordable] }, + { value: "openai/gpt-4o-mini", name: "GPT-4o mini", tags: [MODEL_TAGS.affordable] }, + { value: "qwen/qwen3.6-plus", name: "Qwen3.6 Plus", tags: [MODEL_TAGS.affordable] }, + { value: "qwen/qwen3.6-flash", name: "Qwen3.6 Flash", tags: [MODEL_TAGS.affordable] }, + { value: "z-ai/glm-4.7", name: "GLM-4.7", tags: [MODEL_TAGS.affordable] }, + { value: "openai/gpt-oss-120b:free", name: "GPT-OSS 120B", tags: [MODEL_TAGS.free] }, + ], + defaultModel: "anthropic/claude-opus-4.7", + keyLabel: "SiliconFlow API Key", + keyHint: "需要填入有效的硅基流动API密钥方可使用", + baseUrlHint: "硅基流动 API服务器地址", + icon: PROVIDER_LOGOS.siliconflow, + }, + xinghe: { + name: "星河大模型", + baseUrl: "https://aistudio.baidu.com/llm/lmapi/v3", + endpoint: "/chat/completions", + models: [ + { value: "anthropic/claude-opus-4.7", name: "Claude Opus 4.7", tags: [MODEL_TAGS.flagship] }, + { value: "anthropic/claude-sonnet-4.6", name: "Claude Sonnet 4.6", tags: [MODEL_TAGS.flagship] }, + { value: "google/gemini-3.1-pro-preview", name: "Gemini 3.1 Pro Preview", tags: [MODEL_TAGS.flagship] }, + { value: "google/gemini-3-flash-preview", name: "Gemini 3 Flash Preview", tags: [MODEL_TAGS.flagship] }, + { value: "openai/gpt-5.5", name: "GPT-5.5", tags: [MODEL_TAGS.flagship] }, + { value: "openai/gpt-5-mini", name: "GPT-5 mini", tags: [MODEL_TAGS.flagship] }, + { value: "qwen/qwen3.5-397b-a17b", name: "Qwen3.5 397B A17B", tags: [MODEL_TAGS.flagship] }, + { value: "z-ai/glm-5.1", name: "GLM-5.1", tags: [MODEL_TAGS.flagship] }, + { value: "moonshotai/kimi-k2.6", name: "Kimi K2.6", tags: [MODEL_TAGS.flagship] }, + { value: "minimax/minimax-m2.7", name: "MiniMax M2.7", tags: [MODEL_TAGS.flagship] }, + { value: "google/gemini-3.1-flash-lite-preview", name: "Gemini 3.1 Flash Lite Preview", tags: [MODEL_TAGS.affordable] }, + { value: "openai/gpt-4o-mini", name: "GPT-4o mini", tags: [MODEL_TAGS.affordable] }, + { value: "qwen/qwen3.6-plus", name: "Qwen3.6 Plus", tags: [MODEL_TAGS.affordable] }, + { value: "qwen/qwen3.6-flash", name: "Qwen3.6 Flash", tags: [MODEL_TAGS.affordable] }, + { value: "z-ai/glm-4.7", name: "GLM-4.7", tags: [MODEL_TAGS.affordable] }, + { value: "openai/gpt-oss-120b:free", name: "GPT-OSS 120B", tags: [MODEL_TAGS.free] }, + ], + defaultModel: "anthropic/claude-opus-4.7", + keyLabel: "AI Studio 访问令牌", + keyHint: "请填写 AI Studio 访问令牌", + baseUrlHint: "AI Studio 大模型 API 服务域名", + icon: PROVIDER_LOGOS.xinghe, + description: "AI Studio 星河大模型服务,需配置访问令牌与 Base URL。", + }, + ppio: { + name: "PPIO 派欧云", + baseUrl: "https://api.ppinfra.com/v3/openai", + endpoint: "/chat/completions", + models: [ + { value: "anthropic/claude-opus-4.7", name: "Claude Opus 4.7", tags: [MODEL_TAGS.flagship] }, + { value: "anthropic/claude-sonnet-4.6", name: "Claude Sonnet 4.6", tags: [MODEL_TAGS.flagship] }, + { value: "google/gemini-3.1-pro-preview", name: "Gemini 3.1 Pro Preview", tags: [MODEL_TAGS.flagship] }, + { value: "google/gemini-3-flash-preview", name: "Gemini 3 Flash Preview", tags: [MODEL_TAGS.flagship] }, + { value: "openai/gpt-5.5", name: "GPT-5.5", tags: [MODEL_TAGS.flagship] }, + { value: "openai/gpt-5-mini", name: "GPT-5 mini", tags: [MODEL_TAGS.flagship] }, + { value: "qwen/qwen3.5-397b-a17b", name: "Qwen3.5 397B A17B", tags: [MODEL_TAGS.flagship] }, + { value: "z-ai/glm-5.1", name: "GLM-5.1", tags: [MODEL_TAGS.flagship] }, + { value: "moonshotai/kimi-k2.6", name: "Kimi K2.6", tags: [MODEL_TAGS.flagship] }, + { value: "minimax/minimax-m2.7", name: "MiniMax M2.7", tags: [MODEL_TAGS.flagship] }, + { value: "google/gemini-3.1-flash-lite-preview", name: "Gemini 3.1 Flash Lite Preview", tags: [MODEL_TAGS.affordable] }, + { value: "openai/gpt-4o-mini", name: "GPT-4o mini", tags: [MODEL_TAGS.affordable] }, + { value: "qwen/qwen3.6-plus", name: "Qwen3.6 Plus", tags: [MODEL_TAGS.affordable] }, + { value: "qwen/qwen3.6-flash", name: "Qwen3.6 Flash", tags: [MODEL_TAGS.affordable] }, + { value: "z-ai/glm-4.7", name: "GLM-4.7", tags: [MODEL_TAGS.affordable] }, + { value: "openai/gpt-oss-120b:free", name: "GPT-OSS 120B", tags: [MODEL_TAGS.free] }, + ], + defaultModel: "anthropic/claude-opus-4.7", + keyLabel: "API Key", + keyHint: "请输入 PPIO API Key", + baseUrlHint: "API服务器地址", + icon: PROVIDER_LOGOS.ppio, + }, + modelscope: { + name: "ModelScope", + baseUrl: "https://api-inference.modelscope.cn/v1", + endpoint: "/chat/completions", + models: [ + { value: "anthropic/claude-opus-4.7", name: "Claude Opus 4.7", tags: [MODEL_TAGS.flagship] }, + { value: "anthropic/claude-sonnet-4.6", name: "Claude Sonnet 4.6", tags: [MODEL_TAGS.flagship] }, + { value: "google/gemini-3.1-pro-preview", name: "Gemini 3.1 Pro Preview", tags: [MODEL_TAGS.flagship] }, + { value: "google/gemini-3-flash-preview", name: "Gemini 3 Flash Preview", tags: [MODEL_TAGS.flagship] }, + { value: "openai/gpt-5.5", name: "GPT-5.5", tags: [MODEL_TAGS.flagship] }, + { value: "openai/gpt-5-mini", name: "GPT-5 mini", tags: [MODEL_TAGS.flagship] }, + { value: "qwen/qwen3.5-397b-a17b", name: "Qwen3.5 397B A17B", tags: [MODEL_TAGS.flagship] }, + { value: "z-ai/glm-5.1", name: "GLM-5.1", tags: [MODEL_TAGS.flagship] }, + { value: "moonshotai/kimi-k2.6", name: "Kimi K2.6", tags: [MODEL_TAGS.flagship] }, + { value: "minimax/minimax-m2.7", name: "MiniMax M2.7", tags: [MODEL_TAGS.flagship] }, + { value: "google/gemini-3.1-flash-lite-preview", name: "Gemini 3.1 Flash Lite Preview", tags: [MODEL_TAGS.affordable] }, + { value: "openai/gpt-4o-mini", name: "GPT-4o mini", tags: [MODEL_TAGS.affordable] }, + { value: "qwen/qwen3.6-plus", name: "Qwen3.6 Plus", tags: [MODEL_TAGS.affordable] }, + { value: "qwen/qwen3.6-flash", name: "Qwen3.6 Flash", tags: [MODEL_TAGS.affordable] }, + { value: "z-ai/glm-4.7", name: "GLM-4.7", tags: [MODEL_TAGS.affordable] }, + { value: "openai/gpt-oss-120b:free", name: "GPT-OSS 120B", tags: [MODEL_TAGS.free] }, + ], + defaultModel: "anthropic/claude-opus-4.7", + keyLabel: "API Key", + keyHint: "请输入 ModelScope API Key", + baseUrlHint: "API服务器地址", + icon: PROVIDER_LOGOS.modelscope, + }, + newapi: { + name: "NewAPI", + baseUrl: "https://api.newapi.ai/v1", + endpoint: "/chat/completions", + models: [ + { value: "anthropic/claude-opus-4.7", name: "Claude Opus 4.7", tags: [MODEL_TAGS.flagship] }, + { value: "anthropic/claude-sonnet-4.6", name: "Claude Sonnet 4.6", tags: [MODEL_TAGS.flagship] }, + { value: "google/gemini-3.1-pro-preview", name: "Gemini 3.1 Pro Preview", tags: [MODEL_TAGS.flagship] }, + { value: "google/gemini-3-flash-preview", name: "Gemini 3 Flash Preview", tags: [MODEL_TAGS.flagship] }, + { value: "openai/gpt-5.5", name: "GPT-5.5", tags: [MODEL_TAGS.flagship] }, + { value: "openai/gpt-5-mini", name: "GPT-5 mini", tags: [MODEL_TAGS.flagship] }, + { value: "qwen/qwen3.5-397b-a17b", name: "Qwen3.5 397B A17B", tags: [MODEL_TAGS.flagship] }, + { value: "z-ai/glm-5.1", name: "GLM-5.1", tags: [MODEL_TAGS.flagship] }, + { value: "moonshotai/kimi-k2.6", name: "Kimi K2.6", tags: [MODEL_TAGS.flagship] }, + { value: "minimax/minimax-m2.7", name: "MiniMax M2.7", tags: [MODEL_TAGS.flagship] }, + { value: "google/gemini-3.1-flash-lite-preview", name: "Gemini 3.1 Flash Lite Preview", tags: [MODEL_TAGS.affordable] }, + { value: "openai/gpt-4o-mini", name: "GPT-4o mini", tags: [MODEL_TAGS.affordable] }, + { value: "qwen/qwen3.6-plus", name: "Qwen3.6 Plus", tags: [MODEL_TAGS.affordable] }, + { value: "qwen/qwen3.6-flash", name: "Qwen3.6 Flash", tags: [MODEL_TAGS.affordable] }, + { value: "z-ai/glm-4.7", name: "GLM-4.7", tags: [MODEL_TAGS.affordable] }, + { value: "openai/gpt-oss-120b:free", name: "GPT-OSS 120B", tags: [MODEL_TAGS.free] }, + ], + defaultModel: "anthropic/claude-opus-4.7", + keyLabel: "API Key", + keyHint: "请输入 NewAPI Key", + baseUrlHint: "API服务器地址", + icon: PROVIDER_LOGOS.newapi, + description: "NewAPI 提供 OpenAI 兼容接口。", + }, + oneapi: { + name: "OneAPI", + baseUrl: "https://api.oneapi.xyz/v1", + endpoint: "/chat/completions", + models: [ + { value: "anthropic/claude-opus-4.7", name: "Claude Opus 4.7", tags: [MODEL_TAGS.flagship] }, + { value: "anthropic/claude-sonnet-4.6", name: "Claude Sonnet 4.6", tags: [MODEL_TAGS.flagship] }, + { value: "google/gemini-3.1-pro-preview", name: "Gemini 3.1 Pro Preview", tags: [MODEL_TAGS.flagship] }, + { value: "google/gemini-3-flash-preview", name: "Gemini 3 Flash Preview", tags: [MODEL_TAGS.flagship] }, + { value: "openai/gpt-5.5", name: "GPT-5.5", tags: [MODEL_TAGS.flagship] }, + { value: "openai/gpt-5-mini", name: "GPT-5 mini", tags: [MODEL_TAGS.flagship] }, + { value: "qwen/qwen3.5-397b-a17b", name: "Qwen3.5 397B A17B", tags: [MODEL_TAGS.flagship] }, + { value: "z-ai/glm-5.1", name: "GLM-5.1", tags: [MODEL_TAGS.flagship] }, + { value: "moonshotai/kimi-k2.6", name: "Kimi K2.6", tags: [MODEL_TAGS.flagship] }, + { value: "minimax/minimax-m2.7", name: "MiniMax M2.7", tags: [MODEL_TAGS.flagship] }, + { value: "google/gemini-3.1-flash-lite-preview", name: "Gemini 3.1 Flash Lite Preview", tags: [MODEL_TAGS.affordable] }, + { value: "openai/gpt-4o-mini", name: "GPT-4o mini", tags: [MODEL_TAGS.affordable] }, + { value: "qwen/qwen3.6-plus", name: "Qwen3.6 Plus", tags: [MODEL_TAGS.affordable] }, + { value: "qwen/qwen3.6-flash", name: "Qwen3.6 Flash", tags: [MODEL_TAGS.affordable] }, + { value: "z-ai/glm-4.7", name: "GLM-4.7", tags: [MODEL_TAGS.affordable] }, + { value: "openai/gpt-oss-120b:free", name: "GPT-OSS 120B", tags: [MODEL_TAGS.free] }, + ], + defaultModel: "anthropic/claude-opus-4.7", + keyLabel: "API Key", + keyHint: "请输入 OneAPI Key", + baseUrlHint: "API服务器地址", + icon: PROVIDER_LOGOS.oneapi, }, }; export const SUPPORTED_FILE_TYPES = [ - 'application/pdf', - 'application/msword', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - 'application/vnd.ms-powerpoint', - 'application/vnd.openxmlformats-officedocument.presentationml.presentation', - 'text/plain', + "application/pdf", ] as const; -export const FILE_SIZE_LIMIT = 50 * 1024 * 1024; // 50MB +export const FILE_SIZE_LIMIT = 30 * 1024 * 1024; export const FUNCTION_INFO = { news: { - icon: '📰', - name: '要闻概览', - description: '新闻要点梳理', + icon: "📰", + nameKey: "function.news.name", + descriptionKey: "function.news.description", }, data: { - icon: '📊', - name: '罗森析数', - description: '数据内容分析', + icon: "📊", + nameKey: "function.data.name", + descriptionKey: "function.data.description", }, science: { - icon: '🔬', - name: '理工速知', - description: '理工课件整理', + icon: "🔬", + nameKey: "function.science.name", + descriptionKey: "function.science.description", }, liberal: { - icon: '📚', - name: '文采丰呈', - description: '文科课件整理', + icon: "📚", + nameKey: "function.liberal.name", + descriptionKey: "function.liberal.description", }, } as const; diff --git a/src/env.d.ts b/src/env.d.ts new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/src/env.d.ts @@ -0,0 +1 @@ +/// diff --git a/src/hooks/useAnalysis.ts b/src/hooks/useAnalysis.ts index a3b2daad..f1ee4f07 100644 --- a/src/hooks/useAnalysis.ts +++ b/src/hooks/useAnalysis.ts @@ -2,7 +2,33 @@ import { useAppStore } from "@/store/useAppStore"; import { APIService } from "@/services/api"; import { DocumentProcessor } from "@/utils/documentProcessor"; import { PROMPT_CONFIGS } from "@/config/prompts"; +import { MODEL_PROVIDERS } from "@/config/providers"; import { useToast } from "@/components/Toast"; +import { + applyImagePlacements, + insertImagesByPageContext, + stripLeadingMarkers, + stripMarkdownFence, +} from "@/utils/analysisWorkflow"; +import { chunkText } from "@/services/rag/textChunking"; +import { + EMBEDDING_MODEL, + generateEmbeddingsBatch, + getFreeEmbeddingUses, + hasUserEmbeddingToken, + isEmbeddingAvailable, +} from "@/services/rag/embeddingService"; +import type { AIProvider, DocumentAnalysisBundle } from "@/types"; +import { writeTextFile, mkdir } from "@tauri-apps/plugin-fs"; + +const SCIENCE_FORMAT_REVIEW_PROMPT = `你是中文学术排版审校助手,只负责在不改变含义的前提下修正格式。必须遵循: +- 仅处理理工速知的输出内容,保持全中文。 +- 一级标题只有 "# 基础知识" 与 "# 典型例题"。 +- 二级标题按数字递增(如 ## 1. xxx、## 2. xxx),三级标题使用 1.1、1.2 依次递进。 +- 复杂公式使用单行 $$公式$$ 包裹,禁止跨行的 $\n公式\n$;行内简单变量用 $x$ 形式。 +- 重要概念需加粗,例题结论用 **【最终结论】结论内容** 高亮。 +- 保留所有本地图片路径(如 ![描述](C:/...) 或 ![描述](/...)),不要删除或改写图像引用,不要将图片路径替换为占位符。 +- 保留原有内容顺序,仅修正格式;若内容已符合规范请原样返回。`; export const useAnalysis = () => { const { @@ -11,6 +37,10 @@ export const useAnalysis = () => { selectedFunction, currentProvider, getCurrentSettings, + enableFormatReview, + autoSaveAnalysisResult, + dataDirectory, + addLog, setIsAnalyzing, setAnalysisProgress, setAnalysisResult, @@ -19,8 +49,338 @@ export const useAnalysis = () => { const toast = useToast(); + const pushDevLog = ( + level: "info" | "warn" | "error", + scope: string, + message: string, + payload?: unknown, + ) => { + if (!addLog) return; + addLog({ + id: `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, + timestamp: Date.now(), + level, + scope, + message, + payload, + }); + }; + + const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + + const stripLeadingSectionHeading = (text: string, header: string) => { + const lines = text.split("\n"); + const headerRegex = new RegExp(`^#\\s*${escapeRegExp(header)}\\s*$`, "i"); + + while (lines.length > 0) { + const line = lines[0].trim(); + if (!line) { + lines.shift(); + continue; + } + if (headerRegex.test(line)) { + lines.shift(); + continue; + } + break; + } + + return lines.join("\n").trimStart(); + }; + + /** + * Full-coverage section pipeline (inspired by open-notebook): + * Instead of RAG top-K (which only sees ~25% of the document), + * we iterate through ALL text chunks in page order for each section task. + * Chunks are batched to fit within LLM context limits, and outputs + * are accumulated per section to ensure complete coverage. + */ + const runSectionPipeline = async ( + fileInfo: { file: File; name: string }, + promptConfig: typeof PROMPT_CONFIGS.science, + settings: { apiKey: string; baseUrl: string; model: string }, + analysisBundle: DocumentAnalysisBundle, + progressStart: number = 20, + progressEnd: number = 85, + ): Promise => { + const { coreSystemPrompt, chunkTasks, sectionHeaders } = promptConfig; + + // Chunk the entire document in page order (no RAG retrieval) + const allChunks = chunkText(analysisBundle.text, analysisBundle.pageTexts, { + targetChunkSize: 400, + overlapChars: 60, + minChunkSize: 60, + }); + + // Trigger embedding generation once per document to power RAG and logging + if (allChunks.length > 0) { + if (isEmbeddingAvailable()) { + setAnalysisProgress({ + percentage: progressStart, + message: `正在生成嵌入向量 (${allChunks.length} 个文本片段)...`, + }); + const startAt = Date.now(); + pushDevLog("info", "embedding", "开始生成嵌入向量", { + model: EMBEDDING_MODEL, + chunkCount: allChunks.length, + hasUserToken: hasUserEmbeddingToken(), + freeUsesBefore: hasUserEmbeddingToken() ? null : getFreeEmbeddingUses(), + }); + try { + await generateEmbeddingsBatch( + allChunks.map((chunk) => chunk.content), + { decrementOnce: true } + ); + pushDevLog("info", "embedding", "嵌入向量生成完成", { + model: EMBEDDING_MODEL, + durationMs: Date.now() - startAt, + freeUsesAfter: hasUserEmbeddingToken() ? null : getFreeEmbeddingUses(), + }); + } catch (error: any) { + pushDevLog("warn", "embedding", "嵌入向量生成失败,已降级", { + model: EMBEDDING_MODEL, + error: error?.message || String(error), + }); + } + } else { + pushDevLog("warn", "embedding", "未配置可用的嵌入 Token,已跳过嵌入生成", { + model: EMBEDDING_MODEL, + freeUsesRemaining: getFreeEmbeddingUses(), + }); + } + } + + pushDevLog("info", "analysis", `全量分段管道启动: ${fileInfo.name}`, { + sections: sectionHeaders.length, + tasks: chunkTasks.length, + totalChunks: allChunks.length, + imageCount: analysisBundle.images.length, + }); + + const sectionOutputs: Record = {}; + const totalSteps = chunkTasks.length; + const sectionProgressStart = progressStart + Math.round((progressEnd - progressStart) * 0.05); + const sectionProgressEnd = progressEnd; + + for (let taskIdx = 0; taskIdx < chunkTasks.length; taskIdx++) { + const task = chunkTasks[taskIdx]; + const sectionHeader = sectionHeaders[taskIdx] || sectionHeaders[sectionHeaders.length - 1]; + + setAnalysisProgress({ + percentage: Math.round(sectionProgressStart + ((taskIdx) / totalSteps) * (sectionProgressEnd - sectionProgressStart)), + message: `正在调用模型生成「${sectionHeader}」(${taskIdx + 1}/${totalSteps})...`, + }); + + // Process ALL chunks in batches for this section task + const MAX_CHARS_PER_BATCH = 8000; + const chunkOutputs: string[] = []; + let batchChunks: string[] = []; + let batchLen = 0; + + const flushBatch = async (batch: string[], batchIdx: number, totalBatches: number) => { + if (batch.length === 0) return; + + const batchText = batch.join("\n\n"); + + // Build image tag context for this batch — ONLY include image tags for pages + // that appear in this batch, to reduce token waste (was injecting ALL 19 tags per batch) + const imageTagLines: string[] = []; + if (analysisBundle.pageImageMap.length > 0) { + const pagesInBatch = new Set(); + // Extract page numbers from chunk sourcePage markers like [片段1(第2页)] + const chunkPageRegex = /片段\d+(第(\d+)页)/g; + let chunkPageMatch; + while ((chunkPageMatch = chunkPageRegex.exec(batchText)) !== null) { + pagesInBatch.add(parseInt(chunkPageMatch[1], 10)); + } + + // Only include image tags for pages that appear in this batch + for (const pageEntry of analysisBundle.pageImageMap) { + if (pageEntry.imageTags.length > 0 && pagesInBatch.has(pageEntry.pageNumber)) { + imageTagLines.push( + `第${pageEntry.pageNumber}页: ${pageEntry.imageTags.join(", ")} (上下文: "${pageEntry.textSnippet.substring(0, 80)}")`, + ); + } + } + } + + const imageTagSection = imageTagLines.length > 0 + ? [ + ``, + `# 可用图片标签`, + `原文中包含以下图片,请在相关内容后插入对应标签。每张图片都必须被引用,不要遗漏。`, + imageTagLines.join("\n"), + ``, + ].join("\n") + : ""; + + const userPrompt = [ + `# 任务`, + `输出部分:${sectionHeader}`, + `要求:${task}`, + ``, + `# 文档内容(批次 ${batchIdx + 1}/${totalBatches},共 ${batch.length} 片段)`, + batchText.trim(), + imageTagSection, + `请基于以上内容完成该部分输出。如果内容不足以完成该任务,请输出你能够确定的部分。`, + ].filter(Boolean).join("\n"); + + // Update progress for each batch within a section + const batchProgressBase = Math.round(sectionProgressStart + (taskIdx / totalSteps) * (sectionProgressEnd - sectionProgressStart)); + const batchProgressNext = Math.round(sectionProgressStart + ((taskIdx + 1) / totalSteps) * (sectionProgressEnd - sectionProgressStart)); + setAnalysisProgress({ + percentage: Math.round(batchProgressBase + (batchIdx / Math.max(totalBatches, 1)) * (batchProgressNext - batchProgressBase)), + message: `正在生成「${sectionHeader}」批次 ${batchIdx + 1}/${totalBatches}...`, + }); + + try { + // Don't pass local image paths to LLM — most models can't use them. + // Images will be inserted by post-processing (applyImagePlacements + insertImagesByPageContext) + const response = await APIService.callAIWithProvider( + currentProvider, + coreSystemPrompt, + userPrompt, + { + apiKey: settings.apiKey, + baseUrl: settings.baseUrl, + model: settings.model, + }, + undefined, // Don't send images to LLM + ); + + const cleanedRaw = stripLeadingMarkers(stripMarkdownFence(response)).trim(); + const cleaned = stripLeadingSectionHeading(cleanedRaw, sectionHeader).trim(); + if (cleaned) { + chunkOutputs.push(cleaned); + } + } catch (err: any) { + pushDevLog("warn", "analysis", `批次处理失败: ${sectionHeader} 批次${batchIdx + 1}`, { + error: err?.message || String(err), + }); + } + }; + + // All chunks are sent to every task (no keyword filtering) + const effectiveChunks = allChunks; + + pushDevLog("info", "analysis", `Task "${sectionHeader}" 使用全部 ${allChunks.length} 个片段`, { + taskIdx, + sectionHeader, + totalChunks: allChunks.length, + }); + + // Pre-count total batches for progress reporting + const tempBatches: string[][] = []; + let tempBatch: string[] = []; + let tempLen = 0; + for (let i = 0; i < effectiveChunks.length; i++) { + const chunkContent = `[片段${i + 1}(第${effectiveChunks[i].sourcePage}页)]\n${allChunks[i].content}`; + if (tempLen + chunkContent.length > MAX_CHARS_PER_BATCH && tempBatch.length > 0) { + tempBatches.push(tempBatch); + tempBatch = []; + tempLen = 0; + } + tempBatch.push(chunkContent); + tempLen += chunkContent.length; + } + if (tempBatch.length > 0) tempBatches.push(tempBatch); + const totalBatches = tempBatches.length; + + // Iterate through filtered chunks in page order, batching them + let batchIndex = 0; + for (let i = 0; i < effectiveChunks.length; i++) { + const chunkContent = `[片段${i + 1}(第${effectiveChunks[i].sourcePage}页)]\n${allChunks[i].content}`; + + if (batchLen + chunkContent.length > MAX_CHARS_PER_BATCH && batchChunks.length > 0) { + // Flush current batch + await flushBatch(batchChunks, batchIndex, totalBatches); + batchIndex++; + batchChunks = []; + batchLen = 0; + } + + batchChunks.push(chunkContent); + batchLen += chunkContent.length; + } + + // Flush remaining batch + await flushBatch(batchChunks, batchIndex, totalBatches); + + // Merge all batch outputs for this section + if (chunkOutputs.length > 0) { + sectionOutputs[sectionHeader] = chunkOutputs.join("\n\n"); + } + + pushDevLog("info", "analysis", `分段完成: ${sectionHeader}`, { + batches: batchIndex + 1, + outputChunks: chunkOutputs.length, + }); + } + + // Assemble sections in order + const assembledParts: string[] = []; + for (const header of sectionHeaders) { + const output = sectionOutputs[header]; + if (output) { + assembledParts.push(`# ${header}\n\n${output}`); + } + } + + let finalContent = assembledParts.join("\n\n"); + + // Image insertion is now handled in analyzeDocument after the pipeline completes, + // using both placeholder-based replacement and page-context insertion. + + pushDevLog("info", "analysis", `全量分段管道完成: ${fileInfo.name}`, { + sections: sectionHeaders.length, + imageCount: analysisBundle.images.length, + }); + + return finalContent; + }; + + const autoSaveIfNeeded = async ( + content: string, + originalFileName: string, + hashDir?: string, + ) => { + if (!autoSaveAnalysisResult) return; + + const dir = (dataDirectory || "").trim(); + if (!dir) { + toast.show("自动保存失败:请先在数据管理中设置数据目录"); + return; + } + + const normalizedDir = dir.replace(/[\\/]+$/, ""); + const baseName = originalFileName.replace(/\.[^./\\]+$/, "") || "OneDocs"; + const now = new Date(); + const dateStr = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, "0")}${String(now.getDate()).padStart(2, "0")}`; + + // Save to hash-based directory if available, otherwise flat + const saveDir = hashDir + ? `${normalizedDir}/${hashDir}` + : normalizedDir; + const mdFileName = `${baseName}_OneDocs_分析结果_${dateStr}.md`; + const mdFullPath = `${saveDir}/${mdFileName}`; + + try { + // Ensure the directory exists + await mkdir(saveDir, { recursive: true }); + + // Save Markdown file + await writeTextFile(mdFullPath, content); + pushDevLog("info", "analysis", `分析结果已自动保存 (MD): ${mdFullPath}`, { + path: mdFullPath, + hashDir, + }); + } catch (error: any) { + console.error("自动保存析文结果失败", error); + toast.show(error?.message || "自动保存失败,请检查数据目录权限", 5000); + } + }; + const analyzeDocument = async () => { - // 支持多文件分析 const filesToAnalyze = files.length > 0 ? files : (currentFile ? [currentFile] : []); if (filesToAnalyze.length === 0) { @@ -29,12 +389,25 @@ export const useAnalysis = () => { } const settings = getCurrentSettings(); - if (!settings.apiKey) { + const providerKey = + typeof currentProvider === "string" && currentProvider.startsWith("custom_") + ? null + : (currentProvider as AIProvider); + const requiresApiKey = providerKey + ? MODEL_PROVIDERS[providerKey]?.requiresApiKey !== false + : true; + + if (requiresApiKey && !settings.apiKey) { toast.show("请先在设置中配置 API Key"); return; } setIsAnalyzing(true); + pushDevLog("info", "analysis", "开始析文", { + totalFiles: filesToAnalyze.length, + selectedFunction, + provider: currentProvider, + }); try { const totalFiles = filesToAnalyze.length; @@ -43,78 +416,196 @@ export const useAnalysis = () => { throw new Error(`未找到 ${selectedFunction} 功能的配置`); } - // 批量分析所有文件 + const processedResults: { + fileId: string; + result: { content: string; timestamp: number; fileId: string }; + fileName: string; + hashDir: string; + }[] = []; + for (let i = 0; i < totalFiles; i++) { const fileInfo = filesToAnalyze[i]; const fileId = fileInfo.id || `file_${i}`; - + const fileBase = Math.round((i / totalFiles) * 100); + const fileSpan = Math.round(100 / totalFiles); + setAnalysisProgress({ - percentage: Math.round((i / totalFiles) * 100), - message: `正在分析文件 ${i + 1}/${totalFiles}: ${fileInfo.name}...`, + percentage: fileBase, + message: `正在读取文件 ${i + 1}/${totalFiles}: ${fileInfo.name}`, + }); + pushDevLog("info", "analysis", `开始处理文件: ${fileInfo.name}`, { + index: i + 1, + totalFiles, }); try { - // 解析文档 - const fileContent = await DocumentProcessor.extractContent( + setAnalysisProgress({ + percentage: fileBase + Math.round(fileSpan * 0.05), + message: `正在解析 PDF 文本: ${fileInfo.name}`, + }); + + const analysisBundle = await DocumentProcessor.extractAnalysisBundle( fileInfo.file, + dataDirectory.trim() || undefined, ); - if (!fileContent || fileContent.trim().length === 0) { + if (!analysisBundle.text || analysisBundle.text.trim().length === 0) { throw new Error(`文档 ${fileInfo.name} 内容为空或无法读取`); } setAnalysisProgress({ - percentage: Math.round((i / totalFiles) * 100 + 30 / totalFiles), - message: `正在调用AI分析文件 ${i + 1}/${totalFiles}: ${fileInfo.name}...`, + percentage: fileBase + Math.round(fileSpan * 0.15), + message: `已提取 ${analysisBundle.images.length} 张图片,${analysisBundle.pageCount} 页文本`, }); - // 调用 AI API - const result = await APIService.callAIWithProvider( - currentProvider, - promptConfig.prompt, - fileContent, + let finalContent = ""; + + try { + finalContent = await runSectionPipeline( + fileInfo, + promptConfig, + settings, + analysisBundle, + fileBase + Math.round(fileSpan * 0.2), + fileBase + Math.round(fileSpan * 0.85), + ); + } catch (pipelineError: any) { + console.warn(`文件 ${fileInfo.name} 分段管道失败:`, pipelineError); + pushDevLog("warn", "analysis", `分段管道失败: ${fileInfo.name}`, { + error: pipelineError?.message || String(pipelineError), + }); + throw pipelineError; + } + + if (!finalContent.trim()) { + throw new Error("分段管道未生成有效内容"); + } + + setAnalysisProgress({ + percentage: fileBase + Math.round(fileSpan * 0.87), + message: `正在插入 ${analysisBundle.images.length} 张提取图片...`, + }); + + pushDevLog("info", "analysis", `开始图片插入: ${analysisBundle.images.length} 张图片`, { + imageCount: analysisBundle.images.length, + images: analysisBundle.images.map(img => ({ page: img.pageNumber, name: img.fileName, path: img.localPath })), + }); + + // First try placeholder-based replacement (for any placeholders LLM may have generated) + finalContent = applyImagePlacements(finalContent, analysisBundle.images); + + // Then insert remaining images by page context (for images not matched by placeholders) + // Uses LLM-based matching when available, falls back to rule-based matching + finalContent = await insertImagesByPageContext( + finalContent, + analysisBundle.images, + analysisBundle.pageImageMap, { + provider: currentProvider, apiKey: settings.apiKey, baseUrl: settings.baseUrl, model: settings.model, - } + }, ); - // 保存单个文件的分析结果 + pushDevLog("info", "analysis", `图片插入完成,最终内容长度: ${finalContent.length}`, { + contentLength: finalContent.length, + imageRefCount: (finalContent.match(/!\[[^\]]*\]\([^)]+\)/g) || []).length, + }); + + if (selectedFunction === "science" && enableFormatReview) { + setAnalysisProgress({ + percentage: fileBase + Math.round(fileSpan * 0.9), + message: `正在进行格式复查: ${fileInfo.name}`, + }); + + const formatReviewContent = `请根据理工速知的格式要求,审查并仅修正以下内容的标题层级、加粗标记、公式排版与图片引用格式:\n\n【格式要求】\n1. 一级标题仅为"# 基础知识""# 典型例题"\n2. 二级标题从1开始递增,格式"## 1. 标题"\n3. 三级标题为"### 1.1 小节"依次递进\n4. 复杂公式使用同一行的 $$公式$$,禁止换行的 $\n公式\n$;行内公式用 $x$ 形式\n5. 重要概念加粗,例题结论用 **【最终结论】结论内容** 标识\n6. 保留所有本地图片路径(如 ![描述](C:/...) 或 ![描述](/...)),不要删除或改写图像引用,不要将图片路径替换为占位符\n7. 保留内容顺序,不新增英文解释\n\n【待复查内容】\n${finalContent}`; + + try { + const reviewResponse = await APIService.callAIWithProvider( + currentProvider, + SCIENCE_FORMAT_REVIEW_PROMPT, + formatReviewContent, + { + apiKey: settings.apiKey, + baseUrl: settings.baseUrl, + model: settings.model, + }, + ); + + const reviewedContent = stripLeadingMarkers(stripMarkdownFence(reviewResponse)).trim(); + if (reviewedContent) { + finalContent = applyImagePlacements(reviewedContent, analysisBundle.images); + finalContent = await insertImagesByPageContext( + finalContent, + analysisBundle.images, + analysisBundle.pageImageMap, + { + provider: currentProvider, + apiKey: settings.apiKey, + baseUrl: settings.baseUrl, + model: settings.model, + }, + ); + } + } catch (reviewError: any) { + console.error(`文件 ${fileInfo.name} 格式复查失败:`, reviewError); + toast.show(`格式复查失败,已返回初步结果:${reviewError.message || "请稍后重试"}`, 5000); + } + } + const analysisResult = { - content: result, + content: finalContent, timestamp: Date.now(), fileId: fileId, }; - - setMultiFileAnalysisResult(fileId, analysisResult); - // 如果是最后一个文件或者是单个文件,更新当前结果(向后兼容) - if (i === totalFiles - 1 || totalFiles === 1) { - setAnalysisResult(analysisResult); - } + setMultiFileAnalysisResult(fileId, analysisResult); + processedResults.push({ fileId, result: analysisResult, fileName: fileInfo.name, hashDir: analysisBundle.hashDir }); + pushDevLog("info", "analysis", `文件分析完成: ${fileInfo.name}`, { + fileId, + hashDir: analysisBundle.hashDir, + }); } catch (error: any) { console.error(`文件 ${fileInfo.name} 分析失败:`, error); + pushDevLog("error", "analysis", `文件分析失败: ${fileInfo.name}`, { + error: error?.message || String(error), + }); let errorMessage = error.message || "分析失败"; - // 提供针对性的错误提示 - if (errorMessage.includes("图片扫描版PDF")) { + if (errorMessage.includes("401")) { errorMessage += - "\n\n建议:请使用带有可选择文本的PDF,或将内容复制到TXT文件中"; + "\n\n建议:请使用带有可选择文本的PDF"; } else if (errorMessage.includes("PDF")) { - errorMessage += "\n\n建议:请尝试重新生成PDF或转换为其他格式"; - } else if (errorMessage.includes("Word")) { - errorMessage += "\n\n建议:请检查Word文档格式是否正确,或另存为新文档"; + errorMessage += "\n\n建议:请尝试重新生成PDF"; } toast.show(`文件 ${fileInfo.name} 分析失败: ${errorMessage}`, 5000); } } + if (processedResults.length === 0) { + throw new Error("未生成任何有效分析结果,请检查文档内容、模型配置或网络连接"); + } + setAnalysisProgress({ percentage: 100, message: "所有文件分析完成!" }); - toast.show(`成功分析 ${totalFiles} 个文件!`); + + const last = processedResults[processedResults.length - 1].result; + setAnalysisResult(last); + + for (const item of processedResults) { + await autoSaveIfNeeded(item.result.content, item.fileName, item.hashDir); + } + + toast.show(`成功分析 ${processedResults.length} 个文件!`); + pushDevLog("info", "analysis", "批量析文完成", { + count: processedResults.length, + }); } catch (error: any) { console.error("批量分析失败:", error); + pushDevLog("error", "analysis", "批量析文失败", { + error: error?.message || String(error), + }); toast.show(error.message || "批量分析失败", 5000); } finally { setIsAnalyzing(false); diff --git a/src/i18n/index.ts b/src/i18n/index.ts new file mode 100644 index 00000000..1ee297e3 --- /dev/null +++ b/src/i18n/index.ts @@ -0,0 +1,33 @@ +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import en from './locales/en.json'; +import zhCN from './locales/zh-CN.json'; + +const STORAGE_KEY = 'onedocs.language'; + +const resolveInitialLanguage = () => { + const stored = localStorage.getItem(STORAGE_KEY); + if (stored) return stored; + + const browserLanguage = navigator.language || 'en'; + return browserLanguage.toLowerCase().startsWith('zh') ? 'zh-CN' : 'en'; +}; + +i18n.use(initReactI18next).init({ + resources: { + en: { translation: en }, + 'zh-CN': { translation: zhCN }, + }, + lng: resolveInitialLanguage(), + fallbackLng: 'en', + supportedLngs: ['en', 'zh-CN'], + interpolation: { + escapeValue: false, + }, +}); + +i18n.on('languageChanged', (lng) => { + localStorage.setItem(STORAGE_KEY, lng); +}); + +export default i18n; diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json new file mode 100644 index 00000000..616d73ba --- /dev/null +++ b/src/i18n/locales/en.json @@ -0,0 +1,152 @@ +{ + "app": { + "titleBar": { + "tabs": { + "landing": "Home", + "analysis": "Analyze", + "analysisResult": "Results", + "settings": "Settings" + }, + "theme": { + "light": "Light mode", + "dark": "Dark mode", + "system": "System" + } + }, + "language": { + "english": "English", + "chinese": "中文", + "short": { + "en": "EN", + "zh": "中文" + }, + "switchTo": "Switch to {{language}}" + } + }, + "landing": { + "title": "Thousands of articles, known at a glance", + "subtitle": "A tool of wisdom to help you analyze and understand", + "description": "OneDocs brings together intelligent prompts to help you grasp document essentials quickly. Whether it is news, data analysis, or academic highlights, everything becomes clear with one click.", + "start": "Start with One Text", + "footer": "© 2025 OneDocs - 一文亦闻" + }, + "function": { + "news": { + "name": "News Overview", + "description": "Summary of news highlights" + }, + "data": { + "name": "Data Analysis", + "description": "Analysis of data content" + }, + "science": { + "name": "STEM Quick Know", + "description": "Organization of STEM courseware" + }, + "liberal": { + "name": "Liberal Arts Richness", + "description": "Organization of Liberal Arts courseware" + } + }, + "functionSelector": { + "title": "Function Selection", + "toastSelected": "Selected function: {{name}}" + }, + "settings": { + "title": "Settings", + "subtitle": "Centralize model and data strategy", + "sections": { + "analysis": { + "label": "Analysis", + "desc": "Control analysis preferences" + }, + "model": { + "label": "Models", + "desc": "Manage model providers" + }, + "data": { + "label": "Data", + "desc": "Configure data directory" + }, + "about": { + "label": "About", + "desc": "Version and links" + }, + "developer": { + "label": "Developer", + "desc": "Internal logs" + } + } + }, + "analysis": { + "formatNotice": { + "title": "📋 Format Notice:", + "body": "Only .pdf format is supported" + }, + "resultHint": { + "title": "Analysis results are ready", + "body": "Use the top menu to switch to “Results” to view, copy, or export." + }, + "progress": { + "title": "Analyzing document..." + } + }, + "upload": { + "select": "Click to select documents (multi-select supported)", + "hint": "Only PDF format. Max 30MB per file.", + "analyze": { + "start": "Start analysis", + "new": "New analysis" + }, + "files": { + "header": "Uploaded files ({{count}})", + "reorderHint": "Use arrows to reorder" + }, + "moveUp": "Move up", + "moveDown": "Move down", + "remove": "Delete", + "toast": { + "unsupported": "File {{name}} is not supported ({{type}}). Skipped.", + "tooLarge": "File {{name}} is too large (over 30MB). Skipped.", + "added": "Added {{count}} file(s) successfully." + } + }, + "analysisResult": { + "empty": { + "title": "No analysis results yet", + "body": "Upload documents on the “Analyze” page and run analysis to see results here." + } + }, + "result": { + "title": { + "single": "Analysis Results", + "merged": "Merged Results" + }, + "merge": { + "button": "Merge All", + "loading": "Merging..." + }, + "back": "Back to preview", + "view": { + "render": "Render", + "markdown": "Source" + }, + "copy": "Copy", + "export": "Export", + "exportFileSuffix": "Analysis_Result", + "empty": "Analyze a document to see results", + "fileNotAnalyzed": "{{name}} (not analyzed)", + "toast": { + "copied": "Markdown copied to clipboard", + "copyFail": "Copy failed. Please select and copy manually.", + "saved": "File saved to: {{path}}", + "exported": "File exported to downloads folder", + "exportFail": "Export failed. Please copy the content and save manually.", + "mergeNeedTwo": "At least 2 files are required to merge.", + "mergeNeedTwoAnalyzed": "At least 2 analyzed files are required to merge.", + "mergeSuccess": "Merged {{count}} files successfully!", + "unknownError": "Unknown error", + "mergeFail": "Merge failed: {{message}}" + } + } +} diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json new file mode 100644 index 00000000..6707cc3b --- /dev/null +++ b/src/i18n/locales/zh-CN.json @@ -0,0 +1,152 @@ +{ + "app": { + "titleBar": { + "tabs": { + "landing": "首页", + "analysis": "分析", + "analysisResult": "分析结果", + "settings": "设置" + }, + "theme": { + "light": "浅色模式", + "dark": "深色模式", + "system": "跟随系统" + } + }, + "language": { + "english": "English", + "chinese": "中文", + "short": { + "en": "EN", + "zh": "中文" + }, + "switchTo": "切换到{{language}}" + } + }, + "landing": { + "title": "文章千卷,一览而知", + "subtitle": "智慧之器,助君析文明理", + "description": "OneDocs者,一文亦闻也,乃集诸多智能提示之力,助君速览文档精髓,无论新闻要览、数据解析,抑或学科要点,皆可一键明了。", + "start": "始于一文", + "footer": "© 2025 OneDocs - 一文亦闻" + }, + "function": { + "news": { + "name": "要闻概览", + "description": "新闻要点梳理" + }, + "data": { + "name": "罗森析数", + "description": "数据内容分析" + }, + "science": { + "name": "理工速知", + "description": "理工课件整理" + }, + "liberal": { + "name": "文采丰呈", + "description": "文科课件整理" + } + }, + "functionSelector": { + "title": "功能选择", + "toastSelected": "已选择功能: {{name}}" + }, + "settings": { + "title": "设置", + "subtitle": "集中管理模型与数据策略", + "sections": { + "analysis": { + "label": "析文设置", + "desc": "控制析文偏好" + }, + "model": { + "label": "模型选择", + "desc": "管理模型供应商" + }, + "data": { + "label": "数据管理", + "desc": "配置数据目录" + }, + "about": { + "label": "关于", + "desc": "查看版本与入口" + }, + "developer": { + "label": "开发者模式", + "desc": "查看内部日志" + } + } + }, + "analysis": { + "formatNotice": { + "title": "📋 格式说明:", + "body": "仅支持 .pdf 格式文件" + }, + "resultHint": { + "title": "分析结果已生成", + "body": "请通过顶部菜单切换到「分析结果」页面查看、复制或导出内容。" + }, + "progress": { + "title": "正在分析文档..." + } + }, + "upload": { + "select": "点击选择文档(支持多选)", + "hint": "仅支持 PDF 格式,单个文件不超过30MB", + "analyze": { + "start": "开始析文", + "new": "新建析文" + }, + "files": { + "header": "已上传文件 ({{count}})", + "reorderHint": "点击箭头调整顺序" + }, + "moveUp": "上移", + "moveDown": "下移", + "remove": "删除", + "toast": { + "unsupported": "文件 {{name}} 格式不支持 ({{type}}),已跳过", + "tooLarge": "文件 {{name}} 过大(超过30MB),已跳过", + "added": "成功添加 {{count}} 个文件" + } + }, + "analysisResult": { + "empty": { + "title": "暂无分析结果", + "body": "请先在“分析”页面上传文档并完成析文,结果会在此处展示。" + } + }, + "result": { + "title": { + "single": "析文成果", + "merged": "合并结果" + }, + "merge": { + "button": "一键合并", + "loading": "合并中..." + }, + "back": "返回预览", + "view": { + "render": "渲染", + "markdown": "源码" + }, + "copy": "复制", + "export": "导出", + "exportFileSuffix": "分析结果", + "empty": "请先分析文档以查看结果", + "fileNotAnalyzed": "{{name}} (未分析)", + "toast": { + "copied": "已复制Markdown源码到剪贴板", + "copyFail": "复制失败,请手动选择复制", + "saved": "文件已保存到: {{path}}", + "exported": "文件已导出到下载文件夹", + "exportFail": "导出失败,请尝试复制内容后手动保存", + "mergeNeedTwo": "至少需要2个文件才能合并", + "mergeNeedTwoAnalyzed": "至少需要2个已分析的文件才能合并", + "mergeSuccess": "成功合并 {{count}} 个文件!", + "unknownError": "未知错误", + "mergeFail": "合并失败: {{message}}" + } + } +} diff --git a/src/main.tsx b/src/main.tsx index 42c1f750..a98ebc2c 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,10 +1,11 @@ -import React from 'react'; -import ReactDOM from 'react-dom/client'; -import App from './App'; -import './styles/index.css'; +import React from "react"; +import ReactDOM from "react-dom/client"; +import App from "./App"; +import "./styles/index.css"; +import "./i18n"; -ReactDOM.createRoot(document.getElementById('root')!).render( +ReactDOM.createRoot(document.getElementById("root")!).render( - + , ); diff --git a/src/pages/Analysis.tsx b/src/pages/Analysis.tsx new file mode 100644 index 00000000..a65026b5 --- /dev/null +++ b/src/pages/Analysis.tsx @@ -0,0 +1,82 @@ +import React from "react"; +import { useAppStore } from "@/store/useAppStore"; +import { useAnalysis } from "@/hooks/useAnalysis"; +import { FunctionSelector } from "@/components/FunctionSelector"; +import { FileUpload } from "@/components/FileUpload"; +import { ProgressBar } from "@/components/ProgressBar"; +import { useTranslation } from "react-i18next"; + +export const Analysis: React.FC = () => { + const { t } = useTranslation(); + const { + files, + currentFile, + isAnalyzing, + analysisResult, + multiFileAnalysisResults, + getCurrentSettings, + showFormatNotice, + setShowFormatNotice, + resetAll, + } = useAppStore(); + + const { analyzeDocument } = useAnalysis(); + const settings = getCurrentSettings(); + + const hasFiles = files.length > 0 || currentFile !== null; + const canAnalyze = hasFiles && settings.apiKey && !isAnalyzing; + + const hasAnalysisResults = + analysisResult !== null || Object.keys(multiFileAnalysisResults).length > 0; + + const handleMainButtonClick = () => { + if (hasAnalysisResults) { + resetAll(); + } else { + analyzeDocument(); + } + }; + + return ( +
+
+ + +
+
+ {showFormatNotice && ( +
+

+ {t("analysis.formatNotice.title")}{" "} + {t("analysis.formatNotice.body")} +

+ +
+ )} + + + + + + {hasAnalysisResults && ( +
+

{t("analysis.resultHint.title")}

+

{t("analysis.resultHint.body")}

+
+ )} +
+
+
+
+ ); +}; diff --git a/src/pages/AnalysisResult.tsx b/src/pages/AnalysisResult.tsx new file mode 100644 index 00000000..d582619e --- /dev/null +++ b/src/pages/AnalysisResult.tsx @@ -0,0 +1,32 @@ +import React from "react"; +import { FunctionSelector } from "@/components/FunctionSelector"; +import { ResultDisplay } from "@/components/ResultDisplay"; +import { useAppStore } from "@/store/useAppStore"; +import { useTranslation } from "react-i18next"; + +export const AnalysisResult: React.FC = () => { + const { analysisResult, multiFileAnalysisResults } = useAppStore(); + const { t } = useTranslation(); + const hasAnalysisResults = + analysisResult !== null || Object.keys(multiFileAnalysisResults).length > 0; + + return ( +
+
+ +
+
+ {hasAnalysisResults ? ( + + ) : ( +
+

{t("analysisResult.empty.title")}

+

{t("analysisResult.empty.body")}

+
+ )} +
+
+
+
+ ); +}; diff --git a/src/pages/Landing.tsx b/src/pages/Landing.tsx index 40848530..46e0cd9e 100644 --- a/src/pages/Landing.tsx +++ b/src/pages/Landing.tsx @@ -1,5 +1,6 @@ import React from "react"; import { FUNCTION_INFO } from "@/config/providers"; +import { useTranslation } from "react-i18next"; import type { PromptType } from "@/types"; interface LandingProps { @@ -7,6 +8,8 @@ interface LandingProps { } export const Landing: React.FC = ({ onStart }) => { + const { t } = useTranslation(); + return (
@@ -19,31 +22,31 @@ export const Landing: React.FC = ({ onStart }) => {
-

文章千卷,一览而知

-

智慧之器,助君析文明理

+

{t("landing.title")}

+

{t("landing.subtitle")}

-

- OneDocs者,一文亦闻也,乃集诸多智能提示之力,助君速览文档精髓,无论新闻要览、数据解析,抑或学科要点,皆可一键明了。 -

+

{t("landing.description")}

{(Object.keys(FUNCTION_INFO) as PromptType[]).map((key) => { const info = FUNCTION_INFO[key]; + const name = t(info.nameKey); + const description = t(info.descriptionKey); return (
{info.icon}
-
{info.name}
-
{info.description}
+
{name}
+
{description}
); })}
-

© 2025 OneDocs - 一文亦闻

+

{t("landing.footer")}

); diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx new file mode 100644 index 00000000..ec0c00ab --- /dev/null +++ b/src/pages/Settings.tsx @@ -0,0 +1,95 @@ +import React, { useMemo, useState } from "react"; +import { AnalysisSettingsPanel } from "@/components/AnalysisSettingsPanel"; +import { ModelSelectionPanel } from "@/components/ModelSelectionPanel"; +import { DataManagementPanel } from "@/components/DataManagementPanel"; +import { AboutPanel } from "@/components/AboutPanel"; +import { DeveloperModePanel } from "@/components/DeveloperModePanel"; +import { useTranslation } from "react-i18next"; +import { useAppStore } from "@/store/useAppStore"; + +type SectionId = "analysis" | "model" | "data" | "about" | "developer"; + +export const Settings: React.FC = () => { + const [activeSection, setActiveSection] = useState("model"); + const { t } = useTranslation(); + const { devMode } = useAppStore(); + + const settingSections = useMemo(() => { + const baseSections = [ + { + id: "analysis", + labelKey: "settings.sections.analysis.label", + descKey: "settings.sections.analysis.desc", + icon: "fas fa-sliders-h", + }, + { + id: "model", + labelKey: "settings.sections.model.label", + descKey: "settings.sections.model.desc", + icon: "fas fa-brain", + }, + { + id: "data", + labelKey: "settings.sections.data.label", + descKey: "settings.sections.data.desc", + icon: "fas fa-database", + }, + { + id: "about", + labelKey: "settings.sections.about.label", + descKey: "settings.sections.about.desc", + icon: "fas fa-info-circle", + }, + ] as const; + + if (devMode) { + return [ + ...baseSections, + { + id: "developer", + labelKey: "settings.sections.developer.label", + descKey: "settings.sections.developer.desc", + icon: "fas fa-code", + }, + ] as const; + } + + return baseSections; + }, [devMode]); + + return ( +
+ + +
+ {activeSection === "analysis" && } + {activeSection === "model" && } + {activeSection === "data" && } + {activeSection === "about" && } + {activeSection === "developer" && devMode && } +
+
+ ); +}; diff --git a/src/pages/Tool.tsx b/src/pages/Tool.tsx deleted file mode 100644 index 8188ed46..00000000 --- a/src/pages/Tool.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import React from "react"; -import { useAppStore } from "@/store/useAppStore"; -import { useAnalysis } from "@/hooks/useAnalysis"; -import { FunctionSelector } from "@/components/FunctionSelector"; -import { FileUpload } from "@/components/FileUpload"; -import { ProgressBar } from "@/components/ProgressBar"; -import { ResultDisplay } from "@/components/ResultDisplay"; -import { SettingsModal } from "@/components/SettingsModal"; - -interface ToolProps { - onBack: () => void; -} - -export const Tool: React.FC = ({ onBack }) => { - const { - files, - currentFile, - isAnalyzing, - analysisResult, - multiFileAnalysisResults, - setSettingsOpen, - getCurrentSettings, - showFormatNotice, - setShowFormatNotice, - resetAll, - } = useAppStore(); - - const { analyzeDocument } = useAnalysis(); - const settings = getCurrentSettings(); - - // 支持多文件分析:如果有多个文件,检查是否有文件;如果只有一个文件,检查currentFile(向后兼容) - const hasFiles = files.length > 0 || currentFile !== null; - const canAnalyze = hasFiles && settings.apiKey && !isAnalyzing; - - // 判断是否有分析结果 - const hasAnalysisResults = analysisResult !== null || Object.keys(multiFileAnalysisResults).length > 0; - - // 处理按钮点击 - const handleMainButtonClick = () => { - if (hasAnalysisResults) { - // 如果有分析结果,点击"新建析文"重置所有内容 - resetAll(); - } else { - // 如果没有分析结果,点击"开始析文"进行分析 - analyzeDocument(); - } - }; - - return ( -
-
-
- -

OneDocs

-
-
- - -
-
- -
- - -
-
- {!hasAnalysisResults && ( - <> - {showFormatNotice && ( -
-

- 📋 格式说明:支持 .pdf、 - .docx.doc.pptx、 - .ppt.txt 格式文件 -

- -
- )} - - - - )} - - - -
-
-
- - -
- ); -}; diff --git a/src/services/api.ts b/src/services/api.ts index fc69d3e3..50f74d1c 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -1,6 +1,52 @@ import { invoke } from "@tauri-apps/api/core"; import type { AIProvider } from "@/types"; import { MODEL_PROVIDERS } from "@/config/providers"; +import { useAppStore } from "@/store/useAppStore"; + +const REQUEST_TIMEOUT_MS = 120000; + +const createLogEntry = ( + level: "info" | "warn" | "error", + scope: string, + message: string, + payload?: unknown, +) => ({ + id: `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, + timestamp: Date.now(), + level, + scope, + message, + payload, +}); + +const pushDevLog = ( + level: "info" | "warn" | "error", + scope: string, + message: string, + payload?: unknown, +) => { + const { addLog } = useAppStore.getState(); + if (addLog) { + addLog(createLogEntry(level, scope, message, payload)); + } +}; + +const invokeWithTimeout = async (promise: Promise, timeoutMs: number) => { + let timeoutId: ReturnType | null = null; + const timeoutPromise = new Promise((_, reject) => { + timeoutId = setTimeout(() => { + reject(new Error("请求超时")); + }, timeoutMs); + }); + + try { + return await Promise.race([promise, timeoutPromise]); + } finally { + if (timeoutId) { + clearTimeout(timeoutId); + } + } +}; interface CallAIParams { systemPrompt: string; @@ -9,6 +55,8 @@ interface CallAIParams { apiKey: string; baseUrl?: string; model?: string; + /** Optional image URLs (file paths, data URLs, or HTTP URLs) for multimodal */ + images?: string[]; } interface CallCustomAIParams { @@ -17,17 +65,17 @@ interface CallCustomAIParams { apiKey: string; baseUrl: string; model: string; + /** Optional image URLs for multimodal */ + images?: string[]; } export class APIService { - /** - * 通用调用方法,支持内置和自定义提供商 - */ static async callAIWithProvider( provider: string, systemPrompt: string, content: string, - settings: { apiKey: string; baseUrl: string; model: string } + settings: { apiKey: string; baseUrl: string; model: string }, + images?: string[], ): Promise { if (provider.startsWith('custom_')) { return this.callCustomAI({ @@ -36,6 +84,7 @@ export class APIService { apiKey: settings.apiKey, baseUrl: settings.baseUrl, model: settings.model, + images, }); } else { return this.callAI({ @@ -45,13 +94,11 @@ export class APIService { apiKey: settings.apiKey, baseUrl: settings.baseUrl, model: settings.model, + images, }); } } - /** - * 调用 AI API 进行文档分析 - */ static async callAI({ systemPrompt, content, @@ -59,6 +106,7 @@ export class APIService { apiKey, baseUrl, model, + images, }: CallAIParams): Promise { const config = MODEL_PROVIDERS[provider]; if (!config) { @@ -67,65 +115,117 @@ export class APIService { const finalModel = model || config.defaultModel; const finalBaseUrl = baseUrl || config.baseUrl; + const finalApiKey = apiKey || config.defaultApiKey || ""; + + if (config.requiresApiKey !== false && !finalApiKey) { + throw new Error("请先配置 API Key"); + } + + if (!finalBaseUrl) { + throw new Error("未检测到有效的 Base URL 配置"); + } console.log(`调用 ${config.name} API`, { provider, model: finalModel, baseUrl: finalBaseUrl, + hasManagedKey: Boolean(config.defaultApiKey), + hasImages: Boolean(images && images.length > 0), + }); + + pushDevLog("info", "api", `准备调用 ${config.name} 模型`, { + provider, + model: finalModel, + baseUrl: finalBaseUrl, + systemPrompt, + content, + imageCount: images?.length || 0, }); try { - // 通过 Tauri 后端调用 API - const result = await invoke("analyze_content_rust", { - apiKey, + const result = await invokeWithTimeout( + invoke("analyze_content_rust", { + apiKey: finalApiKey, apiBaseUrl: finalBaseUrl, systemPrompt, - textContent: `请分析以下文档内容:\n\n${content}`, + textContent: content, model: finalModel, + images: images && images.length > 0 ? images : undefined, + }), + REQUEST_TIMEOUT_MS, + ); + + pushDevLog("info", "api", `模型响应成功 (${config.name})`, { + provider, + model: finalModel, + response: result, }); return result; } catch (error: any) { console.error("API 调用失败:", error); + pushDevLog("error", "api", `模型响应失败 (${config.name})`, { + provider, + model: finalModel, + error: error?.message || String(error), + }); throw this.handleAPIError(error, provider); } } - /** - * 调用自定义 AI API 进行文档分析 - */ static async callCustomAI({ systemPrompt, content, apiKey, baseUrl, model, + images, }: CallCustomAIParams): Promise { console.log(`调用自定义 API`, { model, baseUrl, + hasImages: Boolean(images && images.length > 0), + }); + + pushDevLog("info", "api", "准备调用自定义模型", { + model, + baseUrl, + systemPrompt, + content, + imageCount: images?.length || 0, }); try { - // 通过 Tauri 后端调用 API - const result = await invoke("analyze_content_rust", { + const result = await invokeWithTimeout( + invoke("analyze_content_rust", { apiKey, apiBaseUrl: baseUrl, systemPrompt, - textContent: `请分析以下文档内容:\n\n${content}`, + textContent: content, model, + images: images && images.length > 0 ? images : undefined, + }), + REQUEST_TIMEOUT_MS, + ); + + pushDevLog("info", "api", "自定义模型响应成功", { + model, + baseUrl, + response: result, }); return result; } catch (error: any) { console.error("自定义 API 调用失败:", error); + pushDevLog("error", "api", "自定义模型响应失败", { + model, + baseUrl, + error: error?.message || String(error), + }); throw this.handleCustomAPIError(error); } } - /** - * 测试 API 连接 - */ static async testConnection( provider: AIProvider, apiKey: string, @@ -133,21 +233,34 @@ export class APIService { model?: string, ): Promise { try { - await this.callAI({ - systemPrompt: "You are a helpful assistant.", - content: "Hello, this is a connection test.", - provider, - apiKey, - baseUrl, - model, + const finalModel = model || MODEL_PROVIDERS[provider]?.defaultModel || ''; + const finalBaseUrl = baseUrl || MODEL_PROVIDERS[provider]?.baseUrl || ''; + const finalApiKey = apiKey || MODEL_PROVIDERS[provider]?.defaultApiKey || ''; + + if (!finalBaseUrl) { + throw new Error('未检测到有效的 Base URL 配置'); + } + + const result = await invoke('test_model_connection_rust', { + apiKey: finalApiKey, + apiBaseUrl: finalBaseUrl, + model: finalModel, }); - return true; + + if (result) { + return true; + } + + throw new Error(`模型可用性检测失败:${finalModel}`); } catch (error: any) { - // 检查是否是余额不足错误 - 这种情况下连接是正常的,只是余额问题 - if ((error as any).isBalanceError) { + const handledError = this.handleAPIError( + error instanceof Error ? error : new Error(String(error)), + provider + ); + + if ((handledError as any).isBalanceError) { let rechargeMessage = ""; - // 根据不同提供商给出相应的充值提示 switch (provider) { case 'deepseek': rechargeMessage = "请前往 DeepSeek 官网充值后使用"; @@ -162,55 +275,51 @@ export class APIService { rechargeMessage = "请前往相应官网充值后使用"; } - // 余额不足时抛出一个特殊的"成功但有警告"的错误 const warningError = new Error("BALANCE_WARNING"); (warningError as any).isWarning = true; (warningError as any).originalMessage = `连接正常,但账户余额不足\n\n${rechargeMessage}`; throw warningError; } - // 其他错误照常抛出 - throw error; + throw handledError; } } - /** - * 测试自定义 API 连接 - */ static async testCustomConnection( apiKey: string, baseUrl: string, model: string, ): Promise { try { - await this.callCustomAI({ - systemPrompt: "You are a helpful assistant.", - content: "Hello, this is a connection test.", + const result = await invoke('test_model_connection_rust', { apiKey, - baseUrl, + apiBaseUrl: baseUrl, model, }); - return true; + + if (result) { + return true; + } + + throw new Error('模型不可用'); } catch (error: any) { - // 检查是否是余额不足错误 - 这种情况下连接是正常的,只是余额问题 - if ((error as any).isBalanceError) { + const handledError = this.handleCustomAPIError( + error instanceof Error ? error : new Error(String(error)) + ); + + if ((handledError as any).isBalanceError) { const rechargeMessage = "请检查账户余额是否充足"; - - // 余额不足时抛出一个特殊的"成功但有警告"的错误 + const warningError = new Error("BALANCE_WARNING"); (warningError as any).isWarning = true; (warningError as any).originalMessage = `连接正常,但账户余额不足\n\n${rechargeMessage}`; throw warningError; } - - // 其他错误照常抛出 - throw error; + + throw handledError; } } - /** - * 处理自定义 API 错误 - */ private static handleCustomAPIError(error: any): Error { let errorMessage = `自定义模型 API 调用失败`; @@ -220,10 +329,9 @@ export class APIService { errorMessage += `: ${error.message}`; } - // 提供针对性的错误提示 if ( errorMessage.includes("InsufficientBalance") || - errorMessage.includes("InsuffcientBalance") || // API返回的拼写错误 + errorMessage.includes("InsuffcientBalance") || errorMessage.includes("insufficient_balance") || errorMessage.includes("PaymentRequired") || errorMessage.includes("402") || @@ -231,7 +339,6 @@ export class APIService { ) { const providerSpecificMessage = `账户余额不足\n\n解决方案:\n1. 检查账户余额是否充足\n2. 确认账户状态是否正常\n3. 联系服务商确认账户问题`; - // 创建特殊的余额不足错误,带有标记 const balanceError = new Error(providerSpecificMessage); (balanceError as any).isBalanceError = true; return balanceError; @@ -255,7 +362,8 @@ export class APIService { errorMessage = `请求频率过高\n\n建议:\n1. 稍后再试\n2. 检查账户限额\n3. 考虑升级服务计划`; } else if ( errorMessage.includes("timeout") || - errorMessage.includes("ETIMEDOUT") + errorMessage.includes("ETIMEDOUT") || + errorMessage.includes("请求超时") ) { errorMessage = `请求超时\n\n可能原因:\n1. 网络连接不稳定\n2. 服务器响应缓慢\n3. 文档内容过长`; } else if ( @@ -268,9 +376,6 @@ export class APIService { return new Error(errorMessage); } - /** - * 处理 API 错误 - */ private static handleAPIError(error: any, provider: AIProvider): Error { const providerName = MODEL_PROVIDERS[provider]?.name || provider; @@ -282,16 +387,14 @@ export class APIService { errorMessage += `: ${error.message}`; } - // 提供针对性的错误提示 if ( errorMessage.includes("InsufficientBalance") || - errorMessage.includes("InsuffcientBalance") || // API返回的拼写错误 + errorMessage.includes("InsuffcientBalance") || errorMessage.includes("insufficient_balance") || errorMessage.includes("PaymentRequired") || errorMessage.includes("402") || errorMessage.includes("余额不足") ) { - // 根据提供商生成相应的充值提示 let providerSpecificMessage = ""; let websiteName = ""; @@ -311,7 +414,6 @@ export class APIService { providerSpecificMessage = `账户余额不足\n\n解决方案:\n1. 前往 ${websiteName} 官网充值账户\n2. 检查当前账户余额是否充足\n3. 确认账户状态是否正常\n4. 联系 ${websiteName} 客服确认账户问题`; - // 创建特殊的余额不足错误,带有标记 const balanceError = new Error(providerSpecificMessage); (balanceError as any).isBalanceError = true; return balanceError; @@ -335,7 +437,8 @@ export class APIService { errorMessage = `请求频率过高\n\n建议:\n1. 稍后再试\n2. 检查账户限额\n3. 考虑升级服务计划`; } else if ( errorMessage.includes("timeout") || - errorMessage.includes("ETIMEDOUT") + errorMessage.includes("ETIMEDOUT") || + errorMessage.includes("请求超时") ) { errorMessage = `请求超时\n\n可能原因:\n1. 网络连接不稳定\n2. 服务器响应缓慢\n3. 文档内容过长`; } else if ( diff --git a/src/services/pdfImageExtractor.ts b/src/services/pdfImageExtractor.ts new file mode 100644 index 00000000..f7b2d4ff --- /dev/null +++ b/src/services/pdfImageExtractor.ts @@ -0,0 +1,142 @@ +/** + * PDF Image Extraction Service + * + * Extracts images from PDF files using: + * 1. Rust-side lopdf extraction (primary, works offline) + * 2. easyyun API fallback (requires publicly accessible PDF URL) + * + * Extracted images are saved to the data directory. + */ + +import { invoke } from "@tauri-apps/api/core"; +import { appDataDir } from "@tauri-apps/api/path"; +import { mkdir } from "@tauri-apps/plugin-fs"; +import { useAppStore } from "@/store/useAppStore"; +import type { DocumentImageAsset } from "@/types"; + +const pushDevLog = ( + level: "info" | "warn" | "error", + scope: string, + message: string, + payload?: unknown, +) => { + const { addLog } = useAppStore.getState(); + if (addLog) { + addLog({ + id: `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, + timestamp: Date.now(), + level, + scope, + message, + payload, + }); + } +}; + +interface ExtractedImage { + page_number: number; + file_name: string; + local_path: string; +} + +interface ExtractImagesResult { + images: ExtractedImage[]; +} + +/** + * Extract images from a PDF file. + * + * The Rust backend will: + * 1. Try to extract embedded images using lopdf + * 2. If no embedded images found, try the easyyun API as fallback + * + * @param pdfPath - Local path to the PDF file + * @param outputDir - Directory to save extracted images + * @param baseName - Base name for output image files + * @returns Array of DocumentImageAsset + */ +export async function extractPdfImages( + pdfPath: string, + outputDir: string, + baseName: string, +): Promise { + pushDevLog("info", "image-extract", "开始调用 Rust 提取 PDF 图片", { + pdfPath, + outputDir, + baseName, + }); + + try { + const resultJson = await invoke("extract_pdf_images", { + pdfPath, + outputDir, + baseName, + }); + + const result: ExtractImagesResult = JSON.parse(resultJson || "{}"); + + if (!result.images || !Array.isArray(result.images)) { + pushDevLog("warn", "image-extract", "Rust 返回的图片数据为空或格式异常", { + resultJson: resultJson?.substring(0, 500), + }); + return []; + } + + const assets = result.images.map((img) => ({ + pageNumber: img.page_number, + fileName: img.file_name, + localPath: img.local_path, + })); + + pushDevLog("info", "image-extract", `PDF 图片提取成功: ${assets.length} 张`, { + images: assets.map((a) => ({ page: a.pageNumber, name: a.fileName, path: a.localPath })), + }); + + return assets; + } catch (error: any) { + console.error("PDF 图片提取失败:", error); + pushDevLog("error", "image-extract", "PDF 图片提取失败", { + error: error?.message || String(error), + pdfPath, + }); + // Return empty array rather than throwing - images are optional + return []; + } +} + +/** + * Save a PDF file to the app data directory and return its path. + * This is needed before calling extract_pdf_images. + */ +export async function savePdfToDataDir( + file: File, + dataDir?: string, +): Promise<{ pdfPath: string; baseName: string; imageDir: string }> { + const baseName = file.name.replace(/\.[^./\\]+$/, "").replace(/[\\/:*?"<>|]+/g, "_").trim() || "OneDocs"; + const baseDir = dataDir + ? dataDir.replace(/[\\/]+$/, "").replace(/\\/g, "/") + : (await appDataDir()).replace(/[\\/]+$/, "").replace(/\\/g, "/"); + + const imageDir = `${baseDir}/${baseName}_pdf_assets`; + const pdfPath = `${baseDir}/${baseName}_input.pdf`; + + pushDevLog("info", "image-extract", "保存 PDF 到数据目录", { + fileName: file.name, + fileSize: file.size, + pdfPath, + imageDir, + }); + + await mkdir(imageDir, { recursive: true }); + + const arrayBuffer = await file.arrayBuffer(); + const { writeFile } = await import("@tauri-apps/plugin-fs"); + await writeFile(pdfPath, new Uint8Array(arrayBuffer)); + + pushDevLog("info", "image-extract", "PDF 文件保存成功", { + pdfPath, + imageDir, + }); + + return { pdfPath, baseName, imageDir }; +} diff --git a/src/services/rag/embeddingService.ts b/src/services/rag/embeddingService.ts new file mode 100644 index 00000000..315ff500 --- /dev/null +++ b/src/services/rag/embeddingService.ts @@ -0,0 +1,292 @@ +/** + * SiliconFlow Embedding Service + * + * Uses BAAI/bge-m3 (8192 token limit, 1024-dim) via SiliconFlow API. + * Falls back to hash-based pseudo-embeddings when no token is available. + * + * API docs: https://docs.siliconflow.cn/cn/api-reference/embeddings/create-embeddings + * Endpoint: POST https://api.siliconflow.cn/v1/embeddings + */ + +import type { TextChunk } from '@/types'; + +const SILICONFLOW_EMBED_URL = 'https://api.siliconflow.cn/v1/embeddings'; +export const EMBEDDING_MODEL = 'BAAI/bge-m3'; +const EMBEDDING_DIM = 1024; // bge-m3 outputs 1024-dim vectors + +/** Get the active SiliconFlow token: user-provided > env default */ +function getToken(): string | null { + // Priority: user-set token in localStorage > env default + try { + const stored = localStorage.getItem('onedocs-sf-token'); + if (stored) return stored; + } catch { /* ignore */ } + const envToken = import.meta.env.VITE_SILICONFLOW_TOKEN; + if (envToken) return envToken; + return null; +} + +/** Check if a real embedding token is available */ +export function hasEmbeddingToken(): boolean { + return Boolean(getToken()); +} + +/** Get the user's own SiliconFlow token (never exposes the env default) */ +export function getUserEmbeddingToken(): string { + try { + return localStorage.getItem('onedocs-sf-token') || ''; + } catch { return ''; } +} + +/** Set the user's SiliconFlow token */ +export function setEmbeddingToken(token: string): void { + try { + if (token) { + localStorage.setItem('onedocs-sf-token', token); + } else { + localStorage.removeItem('onedocs-sf-token'); + } + } catch { /* ignore */ } +} + +/** Check if user has set their own token (vs using the env default) */ +export function hasUserEmbeddingToken(): boolean { + try { + return Boolean(localStorage.getItem('onedocs-sf-token')); + } catch { return false; } +} + +/** Get the number of free uses remaining (only relevant for env default token) */ +export function getFreeEmbeddingUses(): number { + try { + const stored = localStorage.getItem('onedocs-sf-uses'); + if (stored) return Math.max(0, parseInt(stored, 10) || 0); + } catch { /* ignore */ } + return 3; // default 3 free uses with the env token +} + +/** Decrement free use count */ +export function decrementFreeEmbeddingUse(): void { + try { + const current = getFreeEmbeddingUses(); + localStorage.setItem('onedocs-sf-uses', String(current - 1)); + } catch { /* ignore */ } +} + +/** Set free use count */ +export function setFreeEmbeddingUses(count: number): void { + try { + localStorage.setItem('onedocs-sf-uses', String(count)); + } catch { /* ignore */ } +} + +/** Check if embedding is available (has token or free uses remaining) */ +export function isEmbeddingAvailable(): boolean { + const token = getToken(); + if (!token) return false; + // If user has their own token, always available + if (hasUserEmbeddingToken()) return true; + // Using env default token, check free uses + return getFreeEmbeddingUses() > 0; +} + +interface EmbeddingResponse { + object: string; + model: string; + data: Array<{ + object: string; + embedding: number[]; + index: number; + }>; + usage: { + prompt_tokens: number; + total_tokens: number; + }; +} + +/** + * Call SiliconFlow embedding API + */ +async function callEmbeddingAPI(texts: string[], token: string): Promise { + const response = await fetch(SILICONFLOW_EMBED_URL, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + model: EMBEDDING_MODEL, + input: texts, + encoding_format: 'float', + }), + }); + + if (!response.ok) { + const errorText = await response.text().catch(() => ''); + throw new Error(`SiliconFlow API error ${response.status}: ${errorText.slice(0, 200)}`); + } + + const result: EmbeddingResponse = await response.json(); + + if (!result.data || result.data.length === 0) { + throw new Error('SiliconFlow API returned empty embeddings'); + } + + // Sort by index to maintain order + result.data.sort((a, b) => a.index - b.index); + return result.data.map((d) => d.embedding); +} + +/** + * Generate embedding for a single text using SiliconFlow API + * Falls back to hash-based pseudo-embedding if no token or API fails + */ +export async function generateEmbedding(text: string): Promise { + const token = getToken(); + + if (!token) { + return generateFallbackEmbedding(text); + } + + // Check free uses for env token + if (!hasUserEmbeddingToken() && getFreeEmbeddingUses() <= 0) { + console.warn('免费嵌入次数已用完,请配置自己的 SiliconFlow Token'); + return generateFallbackEmbedding(text); + } + + try { + const embeddings = await callEmbeddingAPI([text], token); + // Decrement free use if using env token + if (!hasUserEmbeddingToken()) decrementFreeEmbeddingUse(); + return embeddings[0]; + } catch (err) { + console.warn('SiliconFlow embedding API failed, using fallback:', err); + return generateFallbackEmbedding(text); + } +} + +/** + * Generate embeddings for multiple texts in batch + * Batches of 32 to stay within RPM limits + */ +export async function generateEmbeddingsBatch( + texts: string[], + options: { decrementOnce?: boolean } = {} +): Promise { + const token = getToken(); + + if (!token) { + return texts.map((t) => generateFallbackEmbedding(t)); + } + + // Check free uses for env token + if (!hasUserEmbeddingToken() && getFreeEmbeddingUses() <= 0) { + console.warn('免费嵌入次数已用完,请配置自己的 SiliconFlow Token'); + return texts.map((t) => generateFallbackEmbedding(t)); + } + + const BATCH_SIZE = 32; + const { decrementOnce = true } = options; + const allEmbeddings: number[][] = []; + + for (let i = 0; i < texts.length; i += BATCH_SIZE) { + const batch = texts.slice(i, i + BATCH_SIZE); + try { + const batchEmbeddings = await callEmbeddingAPI(batch, token); + allEmbeddings.push(...batchEmbeddings); + if (!hasUserEmbeddingToken() && !decrementOnce) { + decrementFreeEmbeddingUse(); + } + } catch (err) { + console.warn(`Batch embedding failed (batch ${i}), using fallback:`, err); + allEmbeddings.push(...batch.map((t) => generateFallbackEmbedding(t))); + } + } + + if (!hasUserEmbeddingToken() && decrementOnce && texts.length > 0) { + decrementFreeEmbeddingUse(); + } + + return allEmbeddings; +} + +/** + * Simple cosine similarity between two vectors + */ +export function cosineSimilarity(a: number[], b: number[]): number { + if (a.length !== b.length) return 0; + + let dotProduct = 0; + let normA = 0; + let normB = 0; + + for (let i = 0; i < a.length; i++) { + dotProduct += a[i] * b[i]; + normA += a[i] * a[i]; + normB += b[i] * b[i]; + } + + const denominator = Math.sqrt(normA) * Math.sqrt(normB); + return denominator === 0 ? 0 : dotProduct / denominator; +} + +/** + * Find most similar chunks to a query + */ +export function findTopKSimilar( + queryEmbedding: number[], + chunksWithEmbeddings: Array<{ chunk: TextChunk; embedding: number[] }>, + topK: number = 5 +): Array<{ chunk: TextChunk; score: number }> { + const scored = chunksWithEmbeddings.map(({ chunk, embedding }) => ({ + chunk, + score: cosineSimilarity(queryEmbedding, embedding), + })); + + scored.sort((a, b) => b.score - a.score); + return scored.slice(0, topK); +} + +/** + * Fallback embedding using simple hash (for offline/no-token scenarios) + * NOT a real embedding - produces deterministic pseudo-vectors + */ +export function generateFallbackEmbedding(text: string): number[] { + const dim = EMBEDDING_DIM; + const embedding = new Array(dim).fill(0); + + for (let i = 0; i < text.length; i++) { + const charCode = text.charCodeAt(i); + const idx = (charCode * (i + 1) * 31) % dim; + embedding[idx] += charCode / 255; + } + + const norm = Math.sqrt(embedding.reduce((sum, v) => sum + v * v, 0)); + if (norm > 0) { + for (let i = 0; i < dim; i++) { + embedding[i] /= norm; + } + } + + return embedding; +} + +/** + * Retrieve relevant chunks using RAG + */ +export async function retrieveRelevantChunks( + chunks: TextChunk[], + query: string, + topK: number = 5 +): Promise> { + const queryEmbedding = await generateEmbedding(query); + + const chunksWithEmbeddings = chunks + .filter((c) => c.embedding && c.embedding.length > 0) + .map((chunk) => ({ + chunk, + embedding: chunk.embedding!, + })); + + return findTopKSimilar(queryEmbedding, chunksWithEmbeddings, topK); +} \ No newline at end of file diff --git a/src/services/rag/textChunking.ts b/src/services/rag/textChunking.ts new file mode 100644 index 00000000..7c62520f --- /dev/null +++ b/src/services/rag/textChunking.ts @@ -0,0 +1,115 @@ +/** + * Text chunking utilities for RAG + * + * Strategy: + * - Chunk by paragraphs first (natural semantic boundaries) + * - If paragraph is too large, split by sentences + * - If sentence is too large, split by token count approximation + */ + +export interface ChunkResult { + id: string; + content: string; + startIndex: number; + endIndex: number; + sourcePage: number; + chunkIndex: number; +} + +/** Split text into chunks with a target size and overlap */ +export function chunkText( + text: string, + pageTexts: string[], + options: { + targetChunkSize?: number; // target characters per chunk (default 500) + overlapChars?: number; // overlap between chunks (default 50) + minChunkSize?: number; // minimum chunk size (default 100) + } = {} +): ChunkResult[] { + const { + targetChunkSize = 500, + overlapChars = 50, + minChunkSize = 100, + } = options; + + const chunks: ChunkResult[] = []; + let chunkIndex = 0; + + // Split by paragraphs first + const paragraphs = text.split(/\n\n+/); + + let currentChunk = ''; + let currentStart = 0; + + for (let pIdx = 0; pIdx < paragraphs.length; pIdx++) { + const para = paragraphs[pIdx].trim(); + if (!para) continue; + + // Find which page this paragraph belongs to + const pageStart = findPageForPosition(pageTexts, text.indexOf(para, currentStart)); + + // If adding this paragraph exceeds target size + if (currentChunk.length + para.length + 2 > targetChunkSize && currentChunk.length >= minChunkSize) { + // Save current chunk + chunks.push({ + id: `chunk_${chunkIndex}_${Date.now()}`, + content: currentChunk.trim(), + startIndex: currentStart, + endIndex: currentStart + currentChunk.length, + sourcePage: pageStart, + chunkIndex, + }); + chunkIndex++; + + // Start new chunk with overlap + const overlapText = currentChunk.slice(-overlapChars); + currentChunk = overlapText; + currentStart = currentStart + currentChunk.length - overlapChars; + } + + if (currentChunk.length === 0) { + currentChunk = para; + currentStart = text.indexOf(para, currentStart === 0 ? 0 : currentStart - 1); + if (currentStart === -1) currentStart = 0; + } else { + currentChunk += '\n\n' + para; + } + } + + // Don't forget the last chunk + if (currentChunk.trim().length >= minChunkSize) { + const lastPage = findPageForPosition(pageTexts, currentStart + currentChunk.length - 1); + chunks.push({ + id: `chunk_${chunkIndex}_${Date.now()}`, + content: currentChunk.trim(), + startIndex: currentStart, + endIndex: currentStart + currentChunk.length, + sourcePage: lastPage, + chunkIndex, + }); + } + + return chunks; +} + +/** Find which page a character position belongs to */ +function findPageForPosition(pageTexts: string[], position: number): number { + if (!pageTexts || pageTexts.length === 0) return 0; + + let charCount = 0; + for (let i = 0; i < pageTexts.length; i++) { + charCount += pageTexts[i].length; + if (position < charCount) { + return i + 1; // 1-indexed + } + } + return pageTexts.length; +} + +/** Simple approximate token count (Chinese chars count as 1, English words as ~1.5) */ +export function approximateTokenCount(text: string): number { + const chineseChars = (text.match(/[\u4e00-\u9fa5]/g) || []).length; + const englishWords = (text.match(/[a-zA-Z]+/g) || []).length; + // Rough approximation: 1 Chinese char ≈ 1 token, 1 English word ≈ 1.5 tokens + return chineseChars + englishWords * 1.5; +} \ No newline at end of file diff --git a/src/store/useAppStore.ts b/src/store/useAppStore.ts index 56116c90..482a5a50 100644 --- a/src/store/useAppStore.ts +++ b/src/store/useAppStore.ts @@ -8,82 +8,108 @@ import type { AnalysisProgress, AnalysisResult, MultiFileAnalysisResult, + DeveloperLogEntry, ViewMode, ProviderSettings, CustomProviderSettings, + ModelOption, } from '@/types'; import { MODEL_PROVIDERS, createCustomProvider } from '@/config/providers'; +const MAX_DEV_LOGS = 500; + interface AppState { - // File state - 支持多文件 files: FileInfo[]; - currentFileId: string | null; // 当前选中的文件ID + currentFileId: string | null; setFiles: (files: FileInfo[]) => void; addFile: (file: FileInfo) => void; removeFile: (fileId: string) => void; reorderFiles: (fileIds: string[]) => void; setCurrentFileId: (fileId: string | null) => void; - // 向后兼容 + currentFile: FileInfo | null; setCurrentFile: (file: FileInfo | null) => void; - // Function selection selectedFunction: PromptType; setSelectedFunction: (func: PromptType) => void; - // Analysis state - 支持多文件分析结果 isAnalyzing: boolean; analysisProgress: AnalysisProgress | null; - analysisResult: AnalysisResult | null; // 当前选中文件的结果(向后兼容) - multiFileAnalysisResults: MultiFileAnalysisResult; // 所有文件的分析结果 - mergedResult: AnalysisResult | null; // 合并后的结果 + analysisResult: AnalysisResult | null; + multiFileAnalysisResults: MultiFileAnalysisResult; + mergedResult: AnalysisResult | null; setIsAnalyzing: (isAnalyzing: boolean) => void; setAnalysisProgress: (progress: AnalysisProgress | null) => void; setAnalysisResult: (result: AnalysisResult | null) => void; setMultiFileAnalysisResult: (fileId: string, result: AnalysisResult) => void; setMergedResult: (result: AnalysisResult | null) => void; - // View mode viewMode: ViewMode; setViewMode: (mode: ViewMode) => void; - // Settings + enableFormatReview: boolean; + setEnableFormatReview: (enabled: boolean) => void; + autoSaveAnalysisResult: boolean; + setAutoSaveAnalysisResult: (enabled: boolean) => void; + currentProvider: AllProviders; providerSettings: Record; + providerCustomModels: Record; customProviders: Record; setCurrentProvider: (provider: AllProviders) => void; updateProviderSettings: (provider: AIProvider, settings: Partial) => void; + addProviderCustomModel: (provider: AIProvider, model: ModelOption) => void; + removeProviderCustomModel: (provider: AIProvider, modelValue: string) => void; addCustomProvider: (name: string, baseUrl: string, model: string, apiKey: string) => string; updateCustomProvider: (id: string, settings: Partial) => void; deleteCustomProvider: (id: string) => void; getCurrentSettings: () => ProviderSettings | CustomProviderSettings; - // UI state + theme: 'light' | 'dark' | 'system'; + setTheme: (theme: 'light' | 'dark' | 'system') => void; isSidebarCollapsed: boolean; - isSettingsOpen: boolean; showFormatNotice: boolean; toggleSidebar: () => void; - setSettingsOpen: (open: boolean) => void; setShowFormatNotice: (show: boolean) => void; - // Reset + dataDirectory: string; + setDataDirectory: (dir: string) => void; + + devMode: boolean; + setDevMode: (enabled: boolean) => void; + developerLogs: DeveloperLogEntry[]; + addLog: (entry: DeveloperLogEntry) => void; + clearLogs: () => void; + resetAnalysis: () => void; resetAll: () => void; + clearAllCache: () => void; } const getDefaultSettings = (provider: AIProvider): ProviderSettings => { const config = MODEL_PROVIDERS[provider]; return { - apiKey: '', + apiKey: config.defaultApiKey ?? '', baseUrl: config.baseUrl, model: config.defaultModel, }; }; +const withProviderDefaults = ( + provider: AIProvider, + settings?: Partial +): ProviderSettings => { + const defaults = getDefaultSettings(provider); + return { + apiKey: settings?.apiKey || defaults.apiKey, + baseUrl: settings?.baseUrl || defaults.baseUrl, + model: settings?.model || defaults.model, + }; +}; + export const useAppStore = create()( persist( (set, get) => ({ - // File state - 多文件支持 files: [], currentFileId: null, setFiles: (files) => set({ files }), @@ -93,7 +119,7 @@ export const useAppStore = create()( set((state) => ({ files: [...state.files, fileWithId], currentFileId: fileId, - currentFile: fileWithId, // 向后兼容 + currentFile: fileWithId, })); }, removeFile: (fileId) => { @@ -104,12 +130,11 @@ export const useAppStore = create()( ? (newFiles.length > 0 ? newFiles[0].id || null : null) : state.currentFileId; const newCurrentFile = newFiles.find((f) => f.id === newCurrentFileId) || null; - // 清理对应的分析结果 const { [fileId]: removed, ...restResults } = state.multiFileAnalysisResults; return { files: newFiles, currentFileId: newCurrentFileId, - currentFile: newCurrentFile, // 向后兼容 + currentFile: newCurrentFile, analysisResult: newCurrentFile ? restResults[newCurrentFileId || ''] || null : null, multiFileAnalysisResults: restResults, }; @@ -128,12 +153,11 @@ export const useAppStore = create()( const result = fileId ? state.multiFileAnalysisResults[fileId] || null : null; return { currentFileId: fileId, - currentFile: file, // 向后兼容 - analysisResult: result, // 向后兼容 + currentFile: file, + analysisResult: result, }; }); }, - // 向后兼容 currentFile: null, setCurrentFile: (file) => { if (file) { @@ -152,11 +176,9 @@ export const useAppStore = create()( } }, - // Function selection selectedFunction: 'science', setSelectedFunction: (func) => set({ selectedFunction: func }), - // Analysis state isAnalyzing: false, analysisProgress: null, analysisResult: null, @@ -171,7 +193,6 @@ export const useAppStore = create()( ...state.multiFileAnalysisResults, [fileId]: result, }; - // 如果当前文件ID匹配,也更新analysisResult(向后兼容) const newAnalysisResult = state.currentFileId === fileId ? result : state.analysisResult; return { multiFileAnalysisResults: newResults, @@ -181,16 +202,56 @@ export const useAppStore = create()( }, setMergedResult: (result) => set({ mergedResult: result }), - // View mode viewMode: 'render', setViewMode: (mode) => set({ viewMode: mode }), - // Settings + enableFormatReview: false, + setEnableFormatReview: (enabled) => set({ enableFormatReview: enabled }), + autoSaveAnalysisResult: false, + setAutoSaveAnalysisResult: (enabled) => set({ autoSaveAnalysisResult: enabled }), + currentProvider: 'openai', providerSettings: { + onedocs: getDefaultSettings('onedocs'), openai: getDefaultSettings('openai'), - deepseek: getDefaultSettings('deepseek'), + anthropic: getDefaultSettings('anthropic'), + gemini: getDefaultSettings('gemini'), + openrouter: getDefaultSettings('openrouter'), + moonshot: getDefaultSettings('moonshot'), glm: getDefaultSettings('glm'), + deepseek: getDefaultSettings('deepseek'), + ollama: getDefaultSettings('ollama'), + lmstudio: getDefaultSettings('lmstudio'), + comp_share: getDefaultSettings('comp_share'), + '302_ai': getDefaultSettings('302_ai'), + pony: getDefaultSettings('pony'), + siliconflow: getDefaultSettings('siliconflow'), + xinghe: getDefaultSettings('xinghe'), + ppio: getDefaultSettings('ppio'), + modelscope: getDefaultSettings('modelscope'), + newapi: getDefaultSettings('newapi'), + oneapi: getDefaultSettings('oneapi'), + }, + providerCustomModels: { + onedocs: [], + openai: [], + anthropic: [], + gemini: [], + openrouter: [], + moonshot: [], + glm: [], + deepseek: [], + ollama: [], + lmstudio: [], + comp_share: [], + '302_ai': [], + pony: [], + siliconflow: [], + xinghe: [], + ppio: [], + modelscope: [], + newapi: [], + oneapi: [], }, customProviders: {}, setCurrentProvider: (provider) => set({ currentProvider: provider }), @@ -198,10 +259,26 @@ export const useAppStore = create()( set((state) => ({ providerSettings: { ...state.providerSettings, - [provider]: { + [provider]: withProviderDefaults(provider, { ...state.providerSettings[provider], ...settings, - }, + }), + }, + })), + addProviderCustomModel: (provider, model) => + set((state) => ({ + providerCustomModels: { + ...state.providerCustomModels, + [provider]: [...(state.providerCustomModels[provider] || []), model], + }, + })), + removeProviderCustomModel: (provider, modelValue) => + set((state) => ({ + providerCustomModels: { + ...state.providerCustomModels, + [provider]: (state.providerCustomModels[provider] || []).filter( + (m) => m.value !== modelValue + ), }, })), addCustomProvider: (name, baseUrl, model, apiKey) => { @@ -241,23 +318,46 @@ export const useAppStore = create()( }), getCurrentSettings: () => { const state = get(); - // 如果是自定义提供商 if (typeof state.currentProvider === 'string' && state.currentProvider.startsWith('custom_')) { - return state.customProviders[state.currentProvider]; + return ( + state.customProviders[state.currentProvider] || { + apiKey: '', + baseUrl: '', + model: '', + name: 'custom-provider', + } + ); } - // 如果是内置提供商 - return state.providerSettings[state.currentProvider as AIProvider]; + + const provider = state.currentProvider as AIProvider; + return withProviderDefaults(provider, state.providerSettings[provider]); }, - // UI state + theme: 'system', + setTheme: (theme) => set({ theme }), isSidebarCollapsed: false, - isSettingsOpen: false, showFormatNotice: true, toggleSidebar: () => set((state) => ({ isSidebarCollapsed: !state.isSidebarCollapsed })), - setSettingsOpen: (open) => set({ isSettingsOpen: open }), setShowFormatNotice: (show) => set({ showFormatNotice: show }), - // Reset + dataDirectory: '', + setDataDirectory: (dir) => set({ dataDirectory: dir }), + + devMode: false, + setDevMode: (enabled) => set({ devMode: enabled }), + developerLogs: [], + addLog: (entry) => + set((state) => { + const nextLogs = [...state.developerLogs, entry]; + return { + developerLogs: + nextLogs.length > MAX_DEV_LOGS + ? nextLogs.slice(nextLogs.length - MAX_DEV_LOGS) + : nextLogs, + }; + }), + clearLogs: () => set({ developerLogs: [] }), + resetAnalysis: () => set({ analysisProgress: null, @@ -277,6 +377,52 @@ export const useAppStore = create()( mergedResult: null, isAnalyzing: false, }), + clearAllCache: () => + set({ + providerSettings: { + onedocs: getDefaultSettings('onedocs'), + openai: getDefaultSettings('openai'), + anthropic: getDefaultSettings('anthropic'), + gemini: getDefaultSettings('gemini'), + openrouter: getDefaultSettings('openrouter'), + moonshot: getDefaultSettings('moonshot'), + glm: getDefaultSettings('glm'), + deepseek: getDefaultSettings('deepseek'), + ollama: getDefaultSettings('ollama'), + lmstudio: getDefaultSettings('lmstudio'), + comp_share: getDefaultSettings('comp_share'), + '302_ai': getDefaultSettings('302_ai'), + pony: getDefaultSettings('pony'), + siliconflow: getDefaultSettings('siliconflow'), + xinghe: getDefaultSettings('xinghe'), + ppio: getDefaultSettings('ppio'), + modelscope: getDefaultSettings('modelscope'), + newapi: getDefaultSettings('newapi'), + oneapi: getDefaultSettings('oneapi'), + }, + providerCustomModels: { + onedocs: [], + openai: [], + anthropic: [], + gemini: [], + openrouter: [], + moonshot: [], + glm: [], + deepseek: [], + ollama: [], + lmstudio: [], + comp_share: [], + '302_ai': [], + pony: [], + siliconflow: [], + xinghe: [], + ppio: [], + modelscope: [], + newapi: [], + oneapi: [], + }, + customProviders: {}, + }), }), { name: 'onedocs-storage', @@ -287,6 +433,10 @@ export const useAppStore = create()( customProviders: state.customProviders, isSidebarCollapsed: state.isSidebarCollapsed, showFormatNotice: state.showFormatNotice, + dataDirectory: state.dataDirectory, + enableFormatReview: state.enableFormatReview, + autoSaveAnalysisResult: state.autoSaveAnalysisResult, + devMode: state.devMode, }), } ) diff --git a/src/styles/index.css b/src/styles/index.css index 8b71f504..3f08cdda 100644 --- a/src/styles/index.css +++ b/src/styles/index.css @@ -1,15 +1,20 @@ -/* 基础样式重置 */ + * { margin: 0; padding: 0; box-sizing: border-box; - /* 禁用全局文本选择 */ + -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } +html, body, #root { + width: 100%; + height: 100%; +} + :root { --primary-color: #1a1a1a; --secondary-color: #6b7280; @@ -22,6 +27,11 @@ --surface-color: #f9fafb; --border-color: #e5e7eb; --hover-color: #f3f4f6; + --panel-gradient-start: #f9fafb; + --panel-gradient-end: #f3f4f6; + --control-gradient-start: #ffffff; + --control-gradient-end: #f8f9fa; + --hover-accent-gradient: #f0f9ff; --shadow-light: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); --shadow-medium: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); --shadow-large: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); @@ -38,14 +48,15 @@ body { overflow: hidden; margin: 0; padding: 0; - width: 800px; - height: 700px; + width: 100%; + height: 100%; + min-height: 100vh; } -/* 固定800x700窗口尺寸 */ + .main-container { - width: 800px; - height: 700px; + width: 100%; + height: 100%; margin: 0; padding: 0; overflow: hidden; @@ -53,12 +64,49 @@ body { flex-direction: column; } -/* 首页样式 */ +.btn-text { + background: none; + border: none; + color: var(--accent-color); + cursor: pointer; + font-weight: 500; + display: inline-flex; + align-items: center; + gap: 0.25rem; + padding: 0; +} + +.btn-text:hover { + text-decoration: underline; +} + +.btn-text span:first-child { + font-size: 1.1rem; + line-height: 1; +} + +.btn-text-primary { + color: var(--accent-color); +} + +.btn-text-primary:hover { + color: var(--accent-hover); +} + +.btn-text-danger { + color: var(--error-color); +} + +.btn-text-danger:hover { + color: #b91c1c; +} + .main-container { - min-height: 100vh; + height: 100%; display: flex; flex-direction: column; position: relative; + overflow-y: auto; } .header { @@ -104,10 +152,16 @@ body { .hero-content { max-width: 700px; text-align: center; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 1.5rem; + height: 100%; } .title-section { - margin-bottom: 2rem; + margin-bottom: 0; } .main-title { @@ -125,7 +179,7 @@ body { } .description { - margin-bottom: 2.5rem; + margin-bottom: 0; font-size: 1rem; color: var(--secondary-color); line-height: 1.6; @@ -139,7 +193,7 @@ body { display: flex; justify-content: center; gap: 0.8rem; - margin-bottom: 2.5rem; + margin-bottom: 0; flex-wrap: nowrap; } @@ -222,13 +276,14 @@ body { flex-shrink: 0; } -/* 工具页面样式 */ + .tool-container { - width: 800px; - height: 700px; + width: 100%; + height: 100%; display: flex; flex-direction: column; overflow: hidden; + min-height: 0; } .tool-header { @@ -257,7 +312,7 @@ body { } .back-button { - background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%); + background: linear-gradient(135deg, var(--control-gradient-start) 0%, var(--control-gradient-end) 100%); border: 1px solid var(--border-color); padding: 0.6rem 1rem; border-radius: var(--border-radius); @@ -286,13 +341,59 @@ body { box-shadow: 0 2px 4px rgba(74, 144, 226, 0.2); } +.tool-title-button { + background: transparent; + border: none; + font-size: 1.5rem; + font-weight: 400; + color: var(--primary-color); + cursor: pointer; + padding: 0.5rem 1rem; + border-radius: var(--border-radius); + transition: var(--transition); + font-family: inherit; + display: flex; + align-items: center; + justify-content: center; + position: relative; +} + +.tool-title-button { + background: transparent; + border: none; + font-size: 1.5rem; + font-weight: 400; + color: var(--primary-color); + cursor: pointer; + padding: 0.5rem 1rem; + border-radius: var(--border-radius); + transition: var(--transition); + font-family: inherit; + display: flex; + align-items: center; + justify-content: center; + position: relative; +} + +.tool-title-button:hover { + color: var(--accent-color); + background: rgba(59, 130, 246, 0.08); + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(59, 130, 246, 0.25); +} + +.tool-title-button:active { + transform: translateY(-1px); + box-shadow: 0 2px 6px rgba(59, 130, 246, 0.15); +} + .tool-title { font-size: 1.5rem; font-weight: 400; } .settings-button { - background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%); + background: linear-gradient(135deg, var(--control-gradient-start) 0%, var(--control-gradient-end) 100%); border: 1px solid var(--border-color); padding: 0.6rem; border-radius: var(--border-radius); @@ -321,20 +422,36 @@ body { box-shadow: 0 2px 4px rgba(108, 117, 125, 0.2); } -/* 右上角小按钮样式 */ + .analyze-button-mini { background: linear-gradient(135deg, var(--accent-color) 0%, var(--accent-hover) 100%); color: white; border: none; - padding: 0.5rem 1rem; + padding: 1rem 0.75rem; border-radius: var(--border-radius); - font-size: 0.875rem; font-weight: 500; cursor: pointer; transition: var(--transition); position: relative; overflow: hidden; box-shadow: var(--shadow-light); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 0.5rem; + width: 80px; + min-height: 120px; +} + +.analyze-button-mini .button-text { + font-size: 1.2rem; + letter-spacing: 0.2rem; + font-weight: 600; +} + +.analyze-button-mini .button-icon { + font-size: 1.4rem; } .analyze-button-mini:hover:not(:disabled) { @@ -372,1515 +489,2624 @@ body { flex: 1; display: flex; overflow: hidden; - background: linear-gradient(135deg, #f9fafb 0%, #f3f4f6 100%); + background: linear-gradient(135deg, var(--panel-gradient-start) 0%, var(--panel-gradient-end) 100%); } -/* 侧边栏样式 */ -.sidebar { +.tools-container { + display: flex; + height: 100%; + width: 100%; + background: linear-gradient(135deg, var(--panel-gradient-start) 0%, var(--panel-gradient-end) 100%); +} + +.tools-sidebar { width: 240px; - background: linear-gradient(180deg, var(--background-color) 0%, var(--surface-color) 100%); + background: var(--background-color); border-right: 1px solid var(--border-color); display: flex; flex-direction: column; - transition: var(--transition); - overflow: hidden; - box-shadow: var(--shadow-light); + padding: 1.5rem 1rem; + gap: 1rem; } -.sidebar.collapsed { - width: 50px; +.tools-sidebar-header h2 { + margin: 0; + font-size: 1.2rem; + color: var(--primary-color); } -.sidebar.collapsed .sidebar-title { - opacity: 0; - transform: translateX(-20px); - transition: opacity 0.2s ease-out, transform 0.2s ease-out; +.tools-sidebar-header p { + margin: 0.2rem 0 0; + color: var(--secondary-color); + font-size: 0.85rem; } -.sidebar .sidebar-title { - opacity: 1; - transform: translateX(0); - transition: opacity 0.3s ease-out 0.1s, transform 0.3s ease-out 0.1s; +.tools-nav { + display: flex; + flex-direction: column; + gap: 0.5rem; } -.sidebar-header { - padding: 1rem; - border-bottom: 1px solid var(--border-color); +.tools-nav-item { display: flex; align-items: center; - justify-content: space-between; - flex-shrink: 0; + gap: 0.75rem; + padding: 0.85rem 0.75rem; + border-radius: var(--border-radius); + border: 1px solid transparent; + background: transparent; + cursor: pointer; + transition: var(--transition); + text-align: left; } -.sidebar-title { - font-weight: 500; - color: var(--primary-color); - white-space: nowrap; +.tools-nav-item.active { + border-color: var(--accent-color); + background: linear-gradient(135deg, rgba(59, 130, 246, 0.1) 0%, rgba(37, 99, 235, 0.1) 100%); } -.sidebar.collapsed .sidebar-title { - display: none; +.tools-nav-item:hover { + border-color: var(--hover-color); + background: var(--hover-color); } -.collapse-btn { - background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%); - border: 1px solid var(--border-color); - padding: 0.5rem; - border-radius: var(--border-radius); - cursor: pointer; - transition: var(--transition); - font-size: 0.85rem; - color: var(--secondary-color); +.tools-nav-icon { + width: 32px; + height: 32px; + border-radius: 8px; + background: var(--surface-color); display: flex; align-items: center; justify-content: center; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); - min-width: 32px; - height: 32px; + color: var(--accent-color); } -.collapse-btn:hover { - background: linear-gradient(135deg, var(--accent-color) 0%, #357abd 100%); - color: white; - border-color: var(--accent-color); - transform: translateY(-1px); - box-shadow: 0 3px 6px rgba(74, 144, 226, 0.3); +.tools-nav-meta span { + display: block; + font-size: 0.95rem; + color: var(--primary-color); } -.collapse-btn:active { - transform: translateY(0); - box-shadow: 0 2px 4px rgba(74, 144, 226, 0.2); +.tools-nav-meta small { + color: var(--secondary-color); + font-size: 0.75rem; } -.sidebar-content { +.tools-content { flex: 1; - padding: 1rem; + padding: 1.5rem; overflow-y: auto; + display: flex; + flex-direction: column; + gap: 1.5rem; } -.sidebar.collapsed .sidebar-content { - padding: 0.5rem; -} - -/* 主内容区域 */ -.main-content { - flex: 1; +.tool-panel { + background: var(--background-color); + border: 1px solid var(--border-color); + border-radius: var(--border-radius-lg); + box-shadow: var(--shadow-medium); + padding: 1.5rem; display: flex; flex-direction: column; - overflow: hidden; - transition: var(--transition); + gap: 1rem; + flex: 1; + min-height: 0; } -.chat-container { - flex: 1; +.provider-form { display: flex; flex-direction: column; - padding: 1rem; gap: 1rem; - overflow: hidden; + flex: 1; min-height: 0; + overflow-y: auto; + padding-right: 0.5rem; } -.upload-section { - animation: slideInUp 0.6s ease-out; - flex-shrink: 0; - flex: 0 0 100px; +.form-top-bar { + display: flex; + align-items: center; + gap: 1rem; + border-bottom: 1px solid var(--border-color); + padding-bottom: 1rem; } -.format-notice { - background: linear-gradient(135deg, rgba(74, 144, 226, 0.1) 0%, rgba(53, 122, 189, 0.1) 100%); - border: 1px solid rgba(74, 144, 226, 0.2); - border-radius: var(--border-radius); - padding: 0.8rem; - margin-bottom: 1rem; - font-size: 0.85rem; +.form-top-bar h3 { + margin: 0; color: var(--primary-color); - position: relative; - transition: var(--transition); } -.format-notice code { - background: rgba(74, 144, 226, 0.1); - padding: 0.2rem 0.4rem; - border-radius: 4px; - font-family: 'Courier New', monospace; - font-weight: 500; +.form-subtitle { + margin: 0.2rem 0 0; + color: var(--secondary-color); + font-size: 0.85rem; } -.notice-close { - position: absolute; - top: 0.5rem; - right: 0.5rem; - background: none; - border: none; - font-size: 1.2rem; - color: var(--secondary-color); - cursor: pointer; - padding: 0.2rem; - border-radius: 50%; - transition: var(--transition); - width: 24px; - height: 24px; - display: flex; - align-items: center; - justify-content: center; +.custom-summary { + background: var(--surface-color); + padding: 0.75rem; + border-radius: var(--border-radius); + border: 1px solid var(--border-color); + font-size: 0.9rem; + line-height: 1.6; } -.notice-close:hover { - background: rgba(74, 144, 226, 0.2); - color: var(--primary-color); +.model-select-row select { + flex: 1; } -.render-notice { - text-align: center; - margin: 0.5rem 0; - font-size: 0.75rem; - color: #888; - opacity: 0.7; - line-height: 1.2; +.custom-model-form { + background: var(--surface-color); + border: 1px solid var(--border-color); + padding: 0.75rem; + border-radius: var(--border-radius); + display: flex; + flex-direction: column; + gap: 0.5rem; } -.render-notice p { - margin: 0; +.custom-model-title { + font-weight: 600; + color: var(--primary-color); } -.upload-area { - border: 2px dashed var(--border-color); - border-radius: var(--border-radius); - padding: 1rem; - text-align: center; - transition: var(--transition); - cursor: pointer; - background: var(--background-color); - flex-shrink: 0; - box-shadow: var(--shadow-light); +.custom-model-actions { + display: flex; + gap: 0.5rem; + justify-content: flex-end; } -.upload-area:hover { - border-color: var(--accent-color); - background: linear-gradient(135deg, var(--hover-color) 0%, #f0f9ff 100%); - transform: translateY(-1px); - box-shadow: var(--shadow-light); +.tool-panel-footer { + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 1rem; + border-top: 1px solid var(--border-color); + padding-top: 1rem; } -.upload-area:active { - transform: translateY(0); - box-shadow: none; +.tool-panel-dialog { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.35); + display: flex; + align-items: center; + justify-content: center; + z-index: 999; } -/* 拖拽功能已禁用,相关样式已移除 */ - -.upload-icon { - font-size: 2rem; - margin-bottom: 0.5rem; - opacity: 0.6; +.tool-panel-dialog-content { + background: var(--background-color); + padding: 1.5rem; + border-radius: var(--border-radius-lg); + width: 420px; + max-width: 90%; + box-shadow: var(--shadow-large); } -.upload-text { - font-size: 0.95rem; - color: var(--primary-color); - margin-bottom: 0.25rem; +.tool-panel-dialog-content h3 { + margin-top: 0; + color: #ef4444; + display: flex; + align-items: center; + gap: 0.5rem; } -.upload-hint { +.tool-panel-dialog-content ul { + padding-left: 1.2rem; color: var(--secondary-color); - font-size: 0.8rem; -} - -.file-preview { - background: var(--background-color); - border: 1px solid var(--border-color); - border-radius: var(--border-radius); - padding: 1rem; + margin-bottom: 1rem; + line-height: 1.5; } -.file-info { +.tool-panel-dialog-actions { display: flex; - justify-content: space-between; - align-items: center; + justify-content: flex-end; + gap: 0.75rem; } -.file-name { - font-weight: 500; +.tool-panel-dialog .danger { + background: #ef4444; + border-color: #ef4444; } -/* 多文件列表样式 */ -.files-list { - background: var(--surface-color); +.data-card { border: 1px solid var(--border-color); border-radius: var(--border-radius); - padding: 0.75rem; - flex: 1 1 auto; - min-height: 0; - overflow-y: auto; - flex-shrink: 1; - box-shadow: var(--shadow-light); + padding: 1.25rem; display: flex; flex-direction: column; + gap: 1rem; + min-height: 0; } -.files-list-header { +.data-card-header { display: flex; justify-content: space-between; align-items: center; - margin-bottom: 0.75rem; - font-size: 0.9rem; - font-weight: 500; +} + +.data-card-header h3 { + margin: 0; color: var(--primary-color); } -.files-hint { - font-size: 0.75rem; +.data-card-header p { + margin: 0.15rem 0 0; color: var(--secondary-color); - font-weight: normal; + font-size: 0.85rem; } -.files-items { - display: flex; - flex-direction: column; - gap: 0.5rem; +.data-card-body { + color: var(--secondary-color); + font-size: 0.95rem; } -.file-item { - display: flex; - align-items: center; - gap: 0.75rem; +.data-path-display { + background: var(--surface-color); + border: 1px dashed var(--border-color); padding: 0.75rem; - background: var(--background-color); - border: 1px solid var(--border-color); border-radius: var(--border-radius); - cursor: pointer; - transition: var(--transition); - user-select: none; + color: var(--secondary-color); + font-size: 0.9rem; + word-break: break-all; } -.file-item:hover { - border-color: var(--accent-color); - background: var(--hover-color); - transform: translateX(2px); +.data-card-actions { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; + align-items: center; + justify-content: flex-end; } -.file-item.active { - border-color: var(--accent-color); - background: linear-gradient(135deg, rgba(59, 130, 246, 0.1) 0%, rgba(37, 99, 235, 0.1) 100%); - box-shadow: 0 2px 4px rgba(59, 130, 246, 0.2); +.data-card-actions.developer-actions .btn, +.data-card-actions.developer-actions select { + font-size: 0.8rem; + padding: 0.4rem 0.7rem; + height: 34px; } -.file-item-controls { - display: flex; - flex-direction: column; - gap: 0.25rem; - margin-right: 0.25rem; + +.developer-log-box { + margin-top: 1rem; + background: #0b0b0b; + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 10px; + padding: 16px; + resize: none; + overflow: auto; + min-height: 320px; + max-height: 60vh; + width: 100%; + max-width: 100%; + box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.02); } -.file-item-arrow { - background: var(--surface-color); - border: 1px solid var(--border-color); - border-radius: 4px; - width: 24px; - height: 24px; - display: flex; +.developer-log-content { + color: #e5e7eb; + font-family: "JetBrains Mono", "Fira Code", "SFMono-Regular", Menlo, Consolas, "Liberation Mono", monospace; + font-size: 12px; + line-height: 1.6; + white-space: pre-wrap; + word-break: break-word; + user-select: text; + -webkit-user-select: text; + cursor: text; +} + +.toggle-switch { + display: inline-flex; align-items: center; - justify-content: center; + gap: 0.5rem; cursor: pointer; - transition: var(--transition); - font-size: 0.9rem; - color: var(--secondary-color); - padding: 0; - line-height: 1; + user-select: none; } -.file-item-arrow:hover:not(:disabled) { - background: var(--accent-color); - border-color: var(--accent-color); - color: white; - transform: scale(1.1); +.toggle-switch input { + display: none; } -.file-item-arrow:active:not(:disabled) { - transform: scale(0.95); +.toggle-slider { + position: relative; + width: 44px; + height: 24px; + background: var(--border-color); + border-radius: 999px; + transition: var(--transition); + box-shadow: var(--shadow-light); } -.file-item-arrow:disabled { - opacity: 0.3; - cursor: not-allowed; +.toggle-slider::after { + content: ""; + position: absolute; + top: 3px; + left: 3px; + width: 18px; + height: 18px; + background: #fff; + border-radius: 50%; + transition: var(--transition); + box-shadow: var(--shadow-light); } -.file-item-info { - flex: 1; - display: flex; - flex-direction: column; - gap: 0.25rem; - min-width: 0; +.toggle-switch input:checked + .toggle-slider { + background: var(--accent-color); } -.file-item-name { - font-weight: 500; - color: var(--primary-color); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; +.toggle-switch input:checked + .toggle-slider::after { + transform: translateX(20px); } -.file-item-size { - font-size: 0.75rem; - color: var(--secondary-color); +.toggle-label { + font-size: 0.95rem; + color: var(--primary-color); + font-weight: 500; } -.file-item-remove { - background: none; - border: none; - font-size: 1.5rem; - color: var(--secondary-color); - cursor: pointer; - padding: 0.25rem 0.5rem; - border-radius: var(--border-radius); - transition: var(--transition); - line-height: 1; +.setting-row { display: flex; align-items: center; - justify-content: center; + justify-content: space-between; + gap: 1rem; } -.file-item-remove:hover { - background: var(--error-color); - color: white; - transform: scale(1.1); +.setting-text { + display: flex; + flex-direction: column; + gap: 0.2rem; + color: var(--primary-color); } -.remove-file { - background: none; - border: none; - color: var(--secondary-color); - cursor: pointer; - font-size: 1.5rem; - padding: 0.25rem; - border-radius: 50%; - transition: var(--transition); +.setting-title { + font-weight: 600; + font-size: 1rem; } -.remove-file:hover { - background: var(--hover-color); - color: var(--primary-color); +.setting-desc { + color: var(--secondary-color); + font-size: 0.92rem; } -.function-selection { - animation: slideInUp 0.6s ease-out 0.1s both; +.setting-hints { + margin: 0.5rem 0 0; + padding-left: 1.25rem; + color: var(--secondary-color); + line-height: 1.6; } -.section-title { - font-size: 1.2rem; - color: var(--primary-color); - margin-bottom: 1.5rem; - text-align: center; +.setting-hints li + li { + margin-top: 0.35rem; } -.function-buttons { - display: flex; - flex-direction: column; - gap: 0.5rem; +.sidebar { + width: 240px; + background: linear-gradient(180deg, var(--background-color) 0%, var(--surface-color) 100%); + border-right: 1px solid var(--border-color); + display: flex; + flex-direction: column; + transition: width 0.2s ease-in-out; + overflow: hidden; + box-shadow: var(--shadow-light); } -.function-btn { - background: var(--background-color); - border: 1px solid var(--border-color); - border-radius: var(--border-radius); - padding: 0.8rem; - cursor: pointer; - transition: var(--transition); - text-align: left; - font-family: inherit; - width: 100%; - display: flex; - align-items: center; - gap: 0.75rem; +.sidebar.collapsed { + width: 50px; } -.sidebar.collapsed .function-btn { - padding: 0.8rem 0.4rem; - text-align: center; - border-radius: 50%; - width: 40px; - height: 40px; - margin: 0.2rem auto; +.sidebar.collapsed .sidebar-title { + opacity: 0; + transform: translateX(-20px); + transition: opacity 0.2s ease-out, transform 0.2s ease-out; +} + +.sidebar .sidebar-title { + opacity: 1; + transform: translateX(0); + transition: opacity 0.3s ease-out 0.1s, transform 0.3s ease-out 0.1s; +} + +.sidebar-header { + padding: 1rem; + border-bottom: 1px solid var(--border-color); display: flex; align-items: center; - justify-content: center; - gap: 0; + justify-content: space-between; + flex-shrink: 0; + cursor: pointer; + transition: var(--transition); + user-select: none; } -.sidebar.collapsed .function-content { +.sidebar-header:hover { + background: var(--hover-color); +} + +.sidebar-title { + font-weight: 500; + color: var(--primary-color); + white-space: nowrap; +} + +.sidebar.collapsed .sidebar-title { display: none; } -.sidebar.collapsed .function-icon { +.collapse-icon { font-size: 1.2rem; + color: var(--secondary-color); + transition: var(--transition); + display: flex; + align-items: center; + justify-content: center; } -.function-btn:hover { - transform: translateY(-2px) scale(1.02); - box-shadow: var(--shadow-medium); - border-color: var(--accent-color); +.collapse-btn { + background: linear-gradient(135deg, var(--control-gradient-start) 0%, var(--control-gradient-end) 100%); + border: 1px solid var(--border-color); + padding: 0.5rem; + border-radius: var(--border-radius); + cursor: pointer; + transition: var(--transition); + font-size: 0.85rem; + color: var(--secondary-color); + display: none; + align-items: center; + justify-content: center; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); + min-width: 32px; + height: 32px; } -.function-btn.active { +.collapse-btn:hover { + background: linear-gradient(135deg, var(--accent-color) 0%, #357abd 100%); + color: white; border-color: var(--accent-color); - background: linear-gradient(135deg, rgba(59, 130, 246, 0.1) 0%, rgba(37, 99, 235, 0.1) 100%); - box-shadow: var(--shadow-light); -} - -.function-btn:active { - transform: translateY(-1px) scale(1.01); - transition: all 0.1s cubic-bezier(0.4, 0, 0.2, 1); + transform: translateY(-1px); + box-shadow: 0 3px 6px rgba(74, 144, 226, 0.3); } -.function-icon { - font-size: 1.5rem; - transition: transform 0.3s ease-out; - flex-shrink: 0; +.collapse-btn:active { + transform: translateY(0); + box-shadow: 0 2px 4px rgba(74, 144, 226, 0.2); } -.function-btn:hover .function-icon { - transform: scale(1.1) rotate(5deg); +.sidebar-content { + flex: 1; + padding: 1rem; + overflow-y: auto; + overflow-x: hidden; + width: 100%; } -.function-content { +.sidebar.collapsed .sidebar-content { + padding: 0.5rem; display: flex; flex-direction: column; - gap: 0.2rem; - flex: 1; + gap: 0.5rem; } -.function-name { - font-size: 0.95rem; - font-weight: 600; - color: var(--primary-color); - margin-bottom: 0; - transition: color 0.3s ease-out; - line-height: 1.3; + +.main-content { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; + transition: var(--transition); } -.function-desc { - font-size: 0.8rem; - color: var(--secondary-color); - transition: color 0.3s ease-out; - line-height: 1.2; +.chat-container { + flex: 1; + display: flex; + flex-direction: column; + padding: 1rem; + gap: 1rem; + overflow: hidden; + min-height: 0; } -.function-btn:hover .function-name { - color: var(--accent-color); +.upload-section { + animation: slideInUp 0.6s ease-out; + flex-shrink: 0; + flex: 0 0 100px; } -.function-btn:hover .function-desc { +.format-notice { + background: linear-gradient(135deg, rgba(74, 144, 226, 0.1) 0%, rgba(53, 122, 189, 0.1) 100%); + border: 1px solid rgba(74, 144, 226, 0.2); + border-radius: var(--border-radius); + padding: 0.8rem; + margin-bottom: 1rem; + font-size: 0.85rem; color: var(--primary-color); + position: relative; + transition: var(--transition); } -.action-section { - text-align: center; - animation: slideInUp 0.6s ease-out 0.2s both; +.format-notice code { + background: rgba(74, 144, 226, 0.1); + padding: 0.2rem 0.4rem; + border-radius: 4px; + font-family: 'Courier New', monospace; + font-weight: 500; } -.analyze-button { - background: linear-gradient(135deg, var(--accent-color) 0%, #357abd 100%); - color: white; +.notice-close { + position: absolute; + top: 0.5rem; + right: 0.5rem; + background: none; border: none; - padding: 1rem 2rem; - font-size: 1.1rem; - border-radius: var(--border-radius); + font-size: 1.2rem; + color: var(--secondary-color); cursor: pointer; + padding: 0.2rem; + border-radius: 50%; transition: var(--transition); - display: inline-flex; + width: 24px; + height: 24px; + display: flex; align-items: center; - gap: 0.5rem; - font-family: inherit; - min-width: 150px; justify-content: center; } -.analyze-button:hover:not(:disabled) { - transform: translateY(-2px); - box-shadow: var(--shadow-medium); -} - -.analyze-button:disabled { - opacity: 0.6; - cursor: not-allowed; +.notice-close:hover { + background: rgba(74, 144, 226, 0.2); + color: var(--primary-color); } -.button-hint { - margin-top: 0.5rem; - font-size: 0.9rem; - color: var(--secondary-color); +.render-notice { text-align: center; + margin: 0.5rem 0; + font-size: 0.75rem; + color: #888; + opacity: 0.7; + line-height: 1.2; } -.button-loader { - width: 20px; - height: 20px; - border: 2px solid transparent; - border-top: 2px solid white; - border-radius: 50%; - animation: spin 1s linear infinite; +.render-notice p { + margin: 0; } -/* 进度条样式 */ -.progress-section { - background: var(--background-color); +.upload-area { + border: 2px dashed var(--border-color); border-radius: var(--border-radius); - box-shadow: var(--shadow-light); padding: 1rem; - margin-bottom: 1rem; - animation: slideInUp 0.6s ease-out; + text-align: center; + transition: var(--transition); + cursor: pointer; + background: var(--background-color); flex-shrink: 0; - flex: 0 0 auto; + box-shadow: var(--shadow-light); } -.progress-header { - margin-bottom: 1rem; +.upload-area:hover { + border-color: var(--accent-color); + background: linear-gradient(135deg, var(--hover-color) 0%, var(--hover-accent-gradient) 100%); + transform: translateY(-1px); + box-shadow: var(--shadow-light); } -.progress-header h3 { - color: var(--primary-color); - margin: 0 0 0.5rem 0; - font-size: 1.1rem; +.upload-area:active { + transform: translateY(0); + box-shadow: none; } -.progress-text { - color: var(--secondary-color); - margin: 0; - font-size: 0.9rem; -} -.progress-container { - display: flex; - align-items: center; - gap: 1rem; -} -.progress-bar { - flex: 1; - height: 8px; - background: var(--border-color); - border-radius: 4px; - overflow: hidden; - position: relative; +.upload-icon { + font-size: 2rem; + margin-bottom: 0.5rem; + opacity: 0.6; } -.progress-bar::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent); - animation: shimmer 1.5s infinite; -} - -.progress-fill { - height: 100%; - background: linear-gradient(90deg, var(--accent-color), var(--accent-hover)); - border-radius: 4px; - width: 0%; - transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1); - position: relative; - overflow: hidden; +.upload-text { + font-size: 0.95rem; + color: var(--primary-color); + margin-bottom: 0.25rem; } -@keyframes shimmer { - 0% { transform: translateX(-100%); } - 100% { transform: translateX(100%); } +.upload-hint { + color: var(--secondary-color); + font-size: 0.8rem; } -.progress-percentage { - color: var(--primary-color); - font-weight: 600; - font-size: 0.9rem; - min-width: 35px; +.file-preview { + background: var(--background-color); + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + padding: 1rem; } -.result-section { - background: var(--background-color); - border-radius: var(--border-radius-lg); - box-shadow: var(--shadow-medium); - animation: slideInUp 0.8s cubic-bezier(0.4, 0, 0.2, 1); - flex: 1; +.file-info { display: flex; - border: 1px solid var(--border-color); - flex-direction: column; - min-height: 0; - overflow: hidden; + justify-content: space-between; + align-items: center; } -.result-body { - flex: 1; - overflow-y: auto; - padding: 1.5rem; - max-height: 85vh; /* 使用视口高度的85%,提供更大的滚动空间 */ - border-radius: 0 0 var(--border-radius) var(--border-radius); - margin: 0; +.file-name { + font-weight: 500; } -.result-header { + +.files-list { + background: var(--surface-color); + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + padding: 0.75rem; + flex: 1 1 auto; + min-height: 0; + overflow-y: auto; + flex-shrink: 1; + box-shadow: var(--shadow-light); display: flex; flex-direction: column; - gap: 0.75rem; - margin-bottom: 1rem; - padding: 1rem 1.5rem 0.8rem 1.5rem; - border-bottom: 1px solid var(--border-color); - background: var(--background-color); - border-radius: var(--border-radius) var(--border-radius) 0 0; } -.result-title-section { +.files-list-header { display: flex; - flex-direction: column; - gap: 0.75rem; - width: 100%; + justify-content: space-between; + align-items: center; + margin-bottom: 0.75rem; + font-size: 0.9rem; + font-weight: 500; + color: var(--primary-color); } -.file-tabs { +.files-hint { + font-size: 0.75rem; + color: var(--secondary-color); + font-weight: normal; +} + +.files-items { display: flex; + flex-direction: column; gap: 0.5rem; - flex-wrap: wrap; - overflow-x: auto; - padding-bottom: 0.25rem; } -.file-tab { - padding: 0.4rem 0.8rem; +.file-item { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.75rem; + background: var(--background-color); border: 1px solid var(--border-color); border-radius: var(--border-radius); - background: var(--background-color); - color: var(--secondary-color); - font-size: 0.8rem; cursor: pointer; transition: var(--transition); - white-space: nowrap; - flex-shrink: 0; + user-select: none; } -.file-tab:hover:not(.no-result) { +.file-item:hover { border-color: var(--accent-color); background: var(--hover-color); - color: var(--primary-color); + transform: translateX(2px); } -.file-tab.active { +.file-item.active { border-color: var(--accent-color); background: linear-gradient(135deg, rgba(59, 130, 246, 0.1) 0%, rgba(37, 99, 235, 0.1) 100%); - color: var(--accent-color); - font-weight: 500; + box-shadow: 0 2px 4px rgba(59, 130, 246, 0.2); } -.file-tab.no-result { - opacity: 0.5; - cursor: not-allowed; +.file-item-controls { + display: flex; + flex-direction: column; + gap: 0.25rem; + margin-right: 0.25rem; } -.result-header-row { +.file-item-arrow { + background: var(--surface-color); + border: 1px solid var(--border-color); + border-radius: 4px; + width: 24px; + height: 24px; display: flex; - justify-content: space-between; align-items: center; - width: 100%; - flex-wrap: wrap; - gap: 1rem; + justify-content: center; + cursor: pointer; + transition: var(--transition); + font-size: 0.9rem; + color: var(--secondary-color); + padding: 0; + line-height: 1; } -.result-controls { - display: flex; - align-items: center; - gap: 1rem; +.file-item-arrow:hover:not(:disabled) { + background: var(--accent-color); + border-color: var(--accent-color); + color: white; + transform: scale(1.1); } -.view-toggle { +.file-item-arrow:active:not(:disabled) { + transform: scale(0.95); +} + +.file-item-arrow:disabled { + opacity: 0.3; + cursor: not-allowed; +} + +.file-item-info { + flex: 1; display: flex; - border: 1px solid var(--border-color); - border-radius: var(--border-radius); + flex-direction: column; + gap: 0.25rem; + min-width: 0; +} + +.file-item-name { + font-weight: 500; + color: var(--primary-color); overflow: hidden; - box-shadow: 0 2px 4px rgba(0,0,0,0.1); + text-overflow: ellipsis; + white-space: nowrap; } -.toggle-btn { - padding: 0.5rem 1rem; +.file-item-size { + font-size: 0.75rem; + color: var(--secondary-color); +} + +.file-item-remove { + background: none; border: none; - background: var(--background-color); + font-size: 1.5rem; color: var(--secondary-color); - font-size: 0.9rem; - font-weight: 500; cursor: pointer; + padding: 0.25rem 0.5rem; + border-radius: var(--border-radius); transition: var(--transition); - font-size: 0.85rem; - border-right: 1px solid var(--border-color); + line-height: 1; + display: flex; + align-items: center; + justify-content: center; } -.toggle-btn:last-child { - border-right: none; +.file-item-remove:hover { + background: var(--error-color); + color: white; + transform: scale(1.1); } -.toggle-btn.active { - background: var(--accent-color); - color: white; +.remove-file { + background: none; + border: none; + color: var(--secondary-color); + cursor: pointer; + font-size: 1.5rem; + padding: 0.25rem; + border-radius: 50%; + transition: var(--transition); } -.toggle-btn:not(.active):hover { +.remove-file:hover { background: var(--hover-color); color: var(--primary-color); } -.result-title { +.function-selection { + animation: slideInUp 0.6s ease-out 0.1s both; +} + +.section-title { font-size: 1.2rem; color: var(--primary-color); - font-weight: 600; - margin: 0; + margin-bottom: 1.5rem; + text-align: center; } -.copy-button { - background: linear-gradient(135deg, var(--background-color) 0%, var(--surface-color) 100%); +.function-buttons { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + + +.sidebar:not(.collapsed) .function-btn { + background: var(--background-color); border: 1px solid var(--border-color); - padding: 0.5rem 1rem; border-radius: var(--border-radius); + padding: 0.8rem; cursor: pointer; - transition: var(--transition); - color: var(--secondary-color); - font-size: 0.875rem; - font-weight: 500; - box-shadow: var(--shadow-light); - position: relative; - overflow: hidden; -} - -.copy-button:hover { - background: linear-gradient(135deg, var(--accent-color) 0%, var(--accent-hover) 100%); - border-color: var(--accent-color); - color: white; - transform: translateY(-1px); - box-shadow: var(--shadow-medium); + transition: none; + text-align: left; + font-family: inherit; + width: 200px; + min-width: 200px; + flex-shrink: 0; + display: flex; + align-items: center; + gap: 0.75rem; + margin: 0.3rem 0; + height: auto; } -.copy-button:active { - transform: translateY(0); - box-shadow: var(--shadow-light); +.sidebar:not(.collapsed) .function-content { + display: flex; + flex-direction: column; + gap: 0.2rem; + flex: 1; } -.copy-button.copying { - opacity: 0.5; - color: var(--text-color-secondary); - background: var(--surface-color); - border-color: var(--border-color); - cursor: not-allowed; - transform: none; - pointer-events: none; +.sidebar:not(.collapsed) .function-icon { + font-size: 1.5rem; + flex-shrink: 0; } -.copy-button:disabled { - pointer-events: none; - opacity: 0.5; +.sidebar:not(.collapsed) .function-name { + font-size: 0.95rem; + font-weight: 600; + color: var(--primary-color); + line-height: 1.3; } -.merge-button { - background: linear-gradient(135deg, var(--success-color) 0%, #059669 100%); - border: 1px solid var(--success-color); - padding: 0.5rem 1rem; - border-radius: var(--border-radius); - cursor: pointer; - transition: var(--transition); - color: white; - font-size: 0.875rem; - font-weight: 500; - box-shadow: var(--shadow-light); +.sidebar:not(.collapsed) .function-desc { + font-size: 0.8rem; + color: var(--secondary-color); + line-height: 1.2; } -.merge-button:hover:not(:disabled) { - background: linear-gradient(135deg, #059669 0%, #047857 100%); - transform: translateY(-1px); +.sidebar:not(.collapsed) .function-btn:hover { + transform: translateY(-2px) scale(1.02); box-shadow: var(--shadow-medium); + border-color: var(--accent-color); } -.merge-button:disabled, -.merge-button.merging { - opacity: 0.6; - cursor: not-allowed; - transform: none; +.sidebar:not(.collapsed) .function-btn:hover .function-name { + color: var(--accent-color); } -.back-button-small { - background: linear-gradient(135deg, var(--background-color) 0%, var(--surface-color) 100%); - border: 1px solid var(--border-color); - padding: 0.5rem 1rem; - border-radius: var(--border-radius); - cursor: pointer; - transition: var(--transition); - color: var(--secondary-color); - font-size: 0.875rem; - font-weight: 500; - box-shadow: var(--shadow-light); +.sidebar:not(.collapsed) .function-btn:hover .function-desc { + color: var(--primary-color); } -.back-button-small:hover { - background: var(--hover-color); +.sidebar:not(.collapsed) .function-btn:hover .function-icon { + transform: scale(1.1) rotate(5deg); +} + +.sidebar:not(.collapsed) .function-btn.active { border-color: var(--accent-color); - color: var(--accent-color); - transform: translateY(-1px); + background: linear-gradient(135deg, rgba(59, 130, 246, 0.1) 0%, rgba(37, 99, 235, 0.1) 100%); + box-shadow: var(--shadow-light); } -.result-empty { +.sidebar:not(.collapsed) .function-btn:active { + transform: translateY(-1px) scale(1.01); +} + + +.sidebar.collapsed .function-btn { + width: 40px; + height: 40px; + min-width: 40px; + max-width: 40px; + padding: 0; + margin: 0.5rem auto; + border-radius: 50%; + border: 1px solid var(--border-color); + background: var(--background-color); + cursor: pointer; + transition: none; display: flex; align-items: center; justify-content: center; - height: 200px; - color: var(--secondary-color); - font-size: 0.9rem; + gap: 0; + font-family: inherit; } -.result-content { - line-height: 1.7; - color: var(--primary-color); - /* 允许结果内容被选择和复制 */ - -webkit-user-select: text; - -moz-user-select: text; - -ms-user-select: text; - user-select: text; +.sidebar.collapsed .function-content { + display: none !important; } -.result-content h1, -.result-content h2, -.result-content h3, -.result-content h4, -.result-content h5, -.result-content h6 { - margin: 1.5rem 0 1rem 0; - color: var(--primary-color); - font-weight: 600; +.sidebar.collapsed .function-icon { + font-size: 1.2rem; + margin: 0; + flex-shrink: 0; } -.result-content h1 { font-size: 1.8rem; border-bottom: 2px solid var(--border-color); padding-bottom: 0.5rem; } -.result-content h2 { font-size: 1.5rem; } -.result-content h3 { font-size: 1.3rem; } -.result-content h4 { font-size: 1.1rem; } - -.result-content p { - margin: 1rem 0; - line-height: 1.8; - text-align: justify; - word-wrap: break-word; - white-space: pre-line; +.sidebar.collapsed .function-btn:hover { + transform: scale(1.1); + box-shadow: var(--shadow-medium); + border-color: var(--accent-color); } -.result-content ul, -.result-content ol { - margin: 1rem 0; - padding-left: 2rem; +.sidebar.collapsed .function-btn.active { + border-color: var(--accent-color); + background: linear-gradient(135deg, rgba(59, 130, 246, 0.15) 0%, rgba(37, 99, 235, 0.15) 100%); + box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.3); } -.result-content li { - margin: 0.5rem 0; - line-height: 1.7; - word-wrap: break-word; +.sidebar.collapsed .function-btn:active { + transform: scale(0.95); } -.result-content br { - line-height: 2; +.action-section { + text-align: center; + animation: slideInUp 0.6s ease-out 0.2s both; } -.result-content div { - margin: 0.5rem 0; +.analyze-button { + background: linear-gradient(135deg, var(--accent-color) 0%, #357abd 100%); + color: white; + border: none; + padding: 1rem 2rem; + font-size: 1.1rem; + border-radius: var(--border-radius); + cursor: pointer; + transition: var(--transition); + display: inline-flex; + align-items: center; + gap: 0.5rem; + font-family: inherit; + min-width: 150px; + justify-content: center; } -.result-content b, -.result-content strong { - font-weight: 700; - color: var(--primary-color); - background: rgba(139, 69, 19, 0.1); - padding: 0.1em 0.2em; - border-radius: 3px; +.analyze-button:hover:not(:disabled) { + transform: translateY(-2px); + box-shadow: var(--shadow-medium); } -/* 数学公式样式 */ -.result-content .katex { - font-size: 1.1em; +.analyze-button:disabled { + opacity: 0.6; + cursor: not-allowed; } -.result-content .katex-display { - margin: 1.5em 0; +.button-hint { + margin-top: 0.5rem; + font-size: 0.9rem; + color: var(--secondary-color); text-align: center; - background: rgba(74, 144, 226, 0.05); - padding: 1rem; - border-radius: var(--border-radius); - border-left: 3px solid var(--primary-color); } -.result-content .katex-html { - white-space: nowrap; +.button-loader { + width: 20px; + height: 20px; + border: 2px solid transparent; + border-top: 2px solid white; + border-radius: 50%; + animation: spin 1s linear infinite; } -.result-content em { - font-style: italic; - color: var(--secondary-color); -} -.result-content code { - background: rgba(74, 144, 226, 0.1); - padding: 0.2rem 0.4rem; - border-radius: 4px; - font-family: 'Courier New', monospace; - font-size: 0.9em; - color: var(--accent-color); +.progress-section { + background: var(--background-color); + border-radius: var(--border-radius); + box-shadow: var(--shadow-light); + padding: 1rem; + margin-bottom: 1rem; + animation: slideInUp 0.6s ease-out; + flex-shrink: 0; + flex: 0 0 auto; } -.result-content pre { - background: var(--hover-color); - padding: 1rem; +.result-hint-card, +.result-empty-card { + background: var(--background-color); + border: 1px dashed var(--border-color); border-radius: var(--border-radius); - overflow-x: auto; - margin: 1rem 0; - border-left: 4px solid var(--accent-color); + padding: 1.25rem; + box-shadow: var(--shadow-light); + animation: fadeInUp 0.4s ease-out; } -.result-content pre code { - background: none; - padding: 0; +.result-hint-card h3, +.result-empty-card h3 { + margin-bottom: 0.5rem; + font-size: 1.1rem; color: var(--primary-color); } -.result-content blockquote { - border-left: 4px solid var(--accent-color); - margin: 1rem 0; - padding: 0.5rem 1rem; - background: rgba(74, 144, 226, 0.05); - font-style: italic; +.result-hint-card p, +.result-empty-card p { + margin: 0; + color: var(--secondary-color); + line-height: 1.5; } -.result-content table { - border-collapse: collapse; - width: 100%; - margin: 1rem 0; +.progress-header { + margin-bottom: 1rem; } -.result-content th, -.result-content td { - border: 1px solid var(--border-color); - padding: 0.5rem; - text-align: left; +.progress-header h3 { + color: var(--primary-color); + margin: 0 0 0.5rem 0; + font-size: 1.1rem; } -.result-content th { - background: var(--hover-color); - font-weight: 600; -} - -.result-markdown { - line-height: 1.6; - color: var(--primary-color); - white-space: pre-wrap; - font-family: 'Courier New', monospace; +.progress-text { + color: var(--secondary-color); + margin: 0; font-size: 0.9rem; - background: var(--hover-color); - padding: 1rem; - border-radius: var(--border-radius); - border: 1px solid var(--border-color); - /* 允许Markdown源码被选择和复制 */ - -webkit-user-select: text; - -moz-user-select: text; - -ms-user-select: text; - user-select: text; -} - -/* 模态框样式 */ -.modal { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0, 0, 0, 0.5); - display: flex; - align-items: center; - justify-content: center; - z-index: 1000; - animation: fadeIn 0.3s ease-out; -} - -.modal-content { - background: var(--background-color); - border-radius: var(--border-radius); - width: 90%; - max-width: 500px; - max-height: 90vh; - overflow-y: auto; - animation: slideInScale 0.3s ease-out; } -.modal-header { +.progress-container { display: flex; - justify-content: space-between; align-items: center; - padding: 1.5rem; - border-bottom: 1px solid var(--border-color); + gap: 1rem; } -.modal-close { - background: none; - border: none; - font-size: 1.5rem; - cursor: pointer; - color: var(--secondary-color); - padding: 0.25rem; - border-radius: 50%; - transition: var(--transition); +.progress-bar { + flex: 1; + height: 8px; + background: var(--border-color); + border-radius: 4px; + overflow: hidden; + position: relative; } -.modal-close:hover { - background: var(--hover-color); - color: var(--primary-color); +.progress-bar::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent); + animation: shimmer 1.5s infinite; } -.modal-body { - padding: 1.5rem; +.progress-fill { + height: 100%; + background: linear-gradient(90deg, var(--accent-color), var(--accent-hover)); + border-radius: 4px; + width: 0%; + transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + overflow: hidden; } -.setting-item { - margin-bottom: 1.5rem; +@keyframes shimmer { + 0% { transform: translateX(-100%); } + 100% { transform: translateX(100%); } } -.setting-item label { - display: block; - margin-bottom: 0.5rem; +.progress-percentage { color: var(--primary-color); - font-weight: 500; + font-weight: 600; + font-size: 0.9rem; + min-width: 35px; } -.setting-item input, -.setting-item select { - width: 100%; - padding: 0.75rem; +.result-section { + background: var(--background-color); + border-radius: var(--border-radius-lg); + box-shadow: var(--shadow-medium); + animation: slideInUp 0.8s cubic-bezier(0.4, 0, 0.2, 1); + flex: 1; + display: flex; border: 1px solid var(--border-color); - border-radius: var(--border-radius); - font-size: 1rem; - transition: var(--transition); - font-family: inherit; - /* 允许设置页面输入框进行文本选择 */ - -webkit-user-select: text; - -moz-user-select: text; - -ms-user-select: text; - user-select: text; + flex-direction: column; + min-height: 0; + overflow: hidden; } -/* 为select下拉框添加滚动条样式 */ -.setting-item select { - max-height: 200px; +.result-body { + flex: 1; overflow-y: auto; - -webkit-appearance: menulist; - -moz-appearance: menulist; - appearance: menulist; -} - -/* 自定义select选项在打开时的最大高度 */ -.setting-item select option { - padding: 0.5rem; - font-size: 0.95rem; + padding: 1.5rem; + max-height: 85vh; + border-radius: 0 0 var(--border-radius) var(--border-radius); + margin: 0; } -/* 为模型选择框特别设置 */ -#modelSelect { - max-height: 150px; +.result-header { + display: flex; + flex-direction: column; + gap: 0.75rem; + margin-bottom: 1rem; + padding: 1rem 1.5rem 0.8rem 1.5rem; + border-bottom: 1px solid var(--border-color); + background: var(--background-color); + border-radius: var(--border-radius) var(--border-radius) 0 0; } -/* 滚动条样式优化 */ -.setting-item select::-webkit-scrollbar { - width: 8px; +.result-title-section { + display: flex; + flex-direction: column; + gap: 0.75rem; + width: 100%; } -.setting-item select::-webkit-scrollbar-track { - background: #f1f1f1; - border-radius: 4px; +.file-tabs { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; + overflow-x: auto; + padding-bottom: 0.25rem; } -.setting-item select::-webkit-scrollbar-thumb { - background: var(--accent-color); - border-radius: 4px; +.file-tab { + padding: 0.4rem 0.8rem; + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + background: var(--background-color); + color: var(--secondary-color); + font-size: 0.8rem; + cursor: pointer; + transition: var(--transition); + white-space: nowrap; + flex-shrink: 0; } -.setting-item select::-webkit-scrollbar-thumb:hover { - background: #357abd; +.file-tab:hover:not(.no-result) { + border-color: var(--accent-color); + background: var(--hover-color); + color: var(--primary-color); } -.setting-item input:focus, -.setting-item select:focus { - outline: none; +.file-tab.active { border-color: var(--accent-color); - box-shadow: 0 0 0 3px rgba(74, 144, 226, 0.1); + background: linear-gradient(135deg, rgba(59, 130, 246, 0.1) 0%, rgba(37, 99, 235, 0.1) 100%); + color: var(--accent-color); + font-weight: 500; } -.setting-item small { - display: block; - margin-top: 0.25rem; - color: var(--secondary-color); - font-size: 0.85rem; +.file-tab.no-result { + opacity: 0.5; + cursor: not-allowed; } -.modal-footer { +.result-header-row { display: flex; justify-content: space-between; align-items: center; + width: 100%; + flex-wrap: wrap; gap: 1rem; - padding: 1.5rem; - border-top: 1px solid var(--border-color); } -.modal-footer .footer-right { +.result-controls { display: flex; + align-items: center; gap: 1rem; } -.btn { - padding: 0.75rem 1.5rem; +.view-toggle { + display: flex; + border: 1px solid var(--border-color); border-radius: var(--border-radius); - cursor: pointer; - transition: var(--transition); - font-size: 0.9rem; - font-family: inherit; + overflow: hidden; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); } -.btn-secondary { - background: none; - border: 1px solid var(--border-color); +.toggle-btn { + padding: 0.5rem 1rem; + border: none; + background: var(--background-color); color: var(--secondary-color); + font-size: 0.9rem; + font-weight: 500; + cursor: pointer; + transition: var(--transition); + font-size: 0.85rem; + border-right: 1px solid var(--border-color); } -.btn-secondary:hover { - background: var(--hover-color); - color: var(--primary-color); +.toggle-btn:last-child { + border-right: none; } -.btn-primary { +.toggle-btn.active { background: var(--accent-color); - border: 1px solid var(--accent-color); color: white; } -.btn-primary:hover { - background: #357abd; - border-color: #357abd; -} - -.btn-test { - background: linear-gradient(135deg, #28a745, #20c997); - border: 1px solid #28a745; - color: white; - position: relative; - overflow: hidden; +.toggle-btn:not(.active):hover { + background: var(--hover-color); + color: var(--primary-color); } -.btn-test:hover:not(:disabled) { - background: linear-gradient(135deg, #218838, #1ea688); - border-color: #218838; +.result-title { + font-size: 1.2rem; + color: var(--primary-color); + font-weight: 600; + margin: 0; +} + +.copy-button { + background: linear-gradient(135deg, var(--background-color) 0%, var(--surface-color) 100%); + border: 1px solid var(--border-color); + padding: 0.5rem 1rem; + border-radius: var(--border-radius); + cursor: pointer; + transition: var(--transition); + color: var(--secondary-color); + font-size: 0.875rem; + font-weight: 500; + box-shadow: var(--shadow-light); + position: relative; + overflow: hidden; +} + +.copy-button:hover { + background: linear-gradient(135deg, var(--accent-color) 0%, var(--accent-hover) 100%); + border-color: var(--accent-color); + color: white; transform: translateY(-1px); - box-shadow: 0 4px 8px rgba(40, 167, 69, 0.3); + box-shadow: var(--shadow-medium); } -.btn-test:disabled { - background: #6c757d; - border-color: #6c757d; +.copy-button:active { + transform: translateY(0); + box-shadow: var(--shadow-light); +} + +.copy-button.copying { + opacity: 0.5; + color: var(--text-color-secondary); + background: var(--surface-color); + border-color: var(--border-color); cursor: not-allowed; - opacity: 0.7; + transform: none; + pointer-events: none; } -/* 自定义模型样式 */ -.custom-provider-info { - background: #f8f9fa; - border: 1px solid #e9ecef; +.copy-button:disabled { + pointer-events: none; + opacity: 0.5; +} + +.merge-button { + background: linear-gradient(135deg, var(--success-color) 0%, #059669 100%); + border: 1px solid var(--success-color); + padding: 0.5rem 1rem; border-radius: var(--border-radius); - padding: 1rem; - margin: 0.5rem 0; - font-size: 0.9rem; + cursor: pointer; + transition: var(--transition); + color: white; + font-size: 0.875rem; + font-weight: 500; + box-shadow: var(--shadow-light); } -.custom-provider-info div { - margin-bottom: 0.5rem; +.merge-button:hover:not(:disabled) { + background: linear-gradient(135deg, #059669 0%, #047857 100%); + transform: translateY(-1px); + box-shadow: var(--shadow-medium); } -.custom-provider-info div:last-child { - margin-bottom: 0; +.merge-button:disabled, +.merge-button.merging { + opacity: 0.6; + cursor: not-allowed; + transform: none; } -.custom-provider-info strong { +.back-button-small { + background: linear-gradient(135deg, var(--background-color) 0%, var(--surface-color) 100%); + border: 1px solid var(--border-color); + padding: 0.5rem 1rem; + border-radius: var(--border-radius); + cursor: pointer; + transition: var(--transition); + color: var(--secondary-color); + font-size: 0.875rem; + font-weight: 500; + box-shadow: var(--shadow-light); +} + +.back-button-small:hover { + background: var(--hover-color); + border-color: var(--accent-color); + color: var(--accent-color); + transform: translateY(-1px); +} + +.result-empty { + display: flex; + align-items: center; + justify-content: center; + height: 200px; + color: var(--secondary-color); + font-size: 0.9rem; +} + +.result-content { + line-height: 1.7; + color: var(--primary-color); + + -webkit-user-select: text; + -moz-user-select: text; + -ms-user-select: text; + user-select: text; +} + +.result-content h1, +.result-content h2, +.result-content h3, +.result-content h4, +.result-content h5, +.result-content h6 { + margin: 1.5rem 0 1rem 0; color: var(--primary-color); font-weight: 600; } -.btn-delete { - background: var(--error-color); - border: 1px solid var(--error-color); - color: white; +.result-content h1 { font-size: 1.8rem; border-bottom: 2px solid var(--border-color); padding-bottom: 0.5rem; } +.result-content h2 { font-size: 1.5rem; } +.result-content h3 { font-size: 1.3rem; } +.result-content h4 { font-size: 1.1rem; } + +.result-content p { + margin: 1rem 0; + line-height: 1.8; + text-align: justify; + word-wrap: break-word; + white-space: pre-line; +} + +.result-content ul, +.result-content ol { + margin: 1rem 0; + padding-left: 2rem; +} + +.result-content li { + margin: 0.5rem 0; + line-height: 1.7; + word-wrap: break-word; +} + +.result-content br { + line-height: 2; +} + +.result-content div { + margin: 0.5rem 0; +} + +.result-content b, +.result-content strong { + font-weight: 700; + color: var(--primary-color); + background: rgba(139, 69, 19, 0.1); + padding: 0.1em 0.2em; + border-radius: 3px; +} + + +.result-content .katex { + font-size: 1.1em; +} + +.result-content .katex-display { + margin: 1.5em 0; + text-align: center; + background: rgba(74, 144, 226, 0.05); + padding: 1rem; + border-radius: var(--border-radius); + border-left: 3px solid var(--primary-color); +} + +.result-content .katex-html { + white-space: nowrap; +} + +.result-content em { + font-style: italic; + color: var(--secondary-color); +} + +.result-content code { + background: rgba(74, 144, 226, 0.1); + padding: 0.2rem 0.4rem; + border-radius: 4px; + font-family: 'Courier New', monospace; + font-size: 0.9em; + color: var(--accent-color); +} + +.result-content pre { + background: var(--hover-color); + padding: 1rem; + border-radius: var(--border-radius); + overflow-x: auto; + margin: 1rem 0; + border-left: 4px solid var(--accent-color); +} + +.result-content pre code { + background: none; + padding: 0; + color: var(--primary-color); +} + +.result-content img { + display: block; + max-width: 100%; + height: auto; + margin: 0.75rem auto; + border-radius: var(--border-radius); + box-shadow: var(--shadow-light); +} + +.result-content blockquote { + border-left: 4px solid var(--accent-color); + margin: 1rem 0; padding: 0.5rem 1rem; - font-size: 0.85rem; + background: rgba(74, 144, 226, 0.05); + font-style: italic; } -.btn-delete:hover { - background: #dc2626; - border-color: #dc2626; - transform: translateY(-1px); - box-shadow: 0 2px 4px rgba(239, 68, 68, 0.3); +.result-content table { + border-collapse: collapse; + width: 100%; + margin: 1rem 0; } -.custom-model-form .setting-item { - margin-bottom: 1.2rem; +.result-content th, +.result-content td { + border: 1px solid var(--border-color); + padding: 0.5rem; + text-align: left; } -.custom-model-form .setting-item:last-child { - margin-bottom: 0; +.result-content th { + background: var(--hover-color); + font-weight: 600; } -/* 新建自定义模型按钮样式 */ -select option[value="create_custom"] { - background: linear-gradient(135deg, #e3f2fd, #bbdefb); - color: #1976d2; - font-weight: 500; +.result-markdown { + line-height: 1.6; + color: var(--primary-color); + white-space: pre-wrap; + font-family: 'Courier New', monospace; + font-size: 0.9rem; + background: var(--hover-color); + padding: 1rem; + border-radius: var(--border-radius); + border: 1px solid var(--border-color); + + -webkit-user-select: text; + -moz-user-select: text; + -ms-user-select: text; + user-select: text; +} + + +.modal { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + animation: fadeIn 0.3s ease-out; +} + +.modal-content { + background: var(--background-color); + border-radius: var(--border-radius); + width: 90%; + max-width: 500px; + max-height: 90vh; + overflow-y: auto; + animation: slideInScale 0.3s ease-out; + -ms-overflow-style: none; + scrollbar-width: none; +} + +.modal-content::-webkit-scrollbar { + display: none; +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem; + border-bottom: 1px solid var(--border-color); +} + +.modal-close { + background: none; + border: none; + font-size: 1.5rem; + cursor: pointer; + color: var(--secondary-color); + padding: 0.25rem; + border-radius: 50%; + transition: var(--transition); +} + +.modal-close:hover { + background: var(--hover-color); + color: var(--primary-color); +} + +.modal-body { + padding: 1rem; +} + +.setting-item { + margin-bottom: 0.8rem; +} + +.setting-item label { + display: block; + margin-bottom: 0.2rem; + color: var(--primary-color); + font-weight: 500; + font-size: 0.9rem; +} + +.setting-item input, +.setting-item select, +.custom-model-form input { + width: 100%; + padding: 0.4rem; + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + font-size: 0.9rem; + transition: var(--transition); + font-family: inherit; + background-color: var(--surface-color); + color: var(--primary-color); + -webkit-user-select: text; + -moz-user-select: text; + -ms-user-select: text; + user-select: text; +} + +.setting-item select { + max-height: 200px; + overflow-y: auto; + -webkit-appearance: menulist; + -moz-appearance: menulist; + appearance: menulist; +} + +.setting-item select option { + background-color: var(--surface-color); + color: var(--primary-color); + padding: 0.5rem; + font-size: 0.95rem; +} + + +#modelSelect { + max-height: 150px; +} + + +.setting-item select::-webkit-scrollbar { + width: 8px; +} + +.setting-item select::-webkit-scrollbar-track { + background: var(--hover-color); + border-radius: 4px; +} + +.setting-item select::-webkit-scrollbar-thumb { + background: var(--accent-color); + border-radius: 4px; +} + +.setting-item select::-webkit-scrollbar-thumb:hover { + background: #357abd; +} + +.setting-item input:focus, +.setting-item select:focus { + outline: none; + border-color: var(--accent-color); + box-shadow: 0 0 0 3px rgba(74, 144, 226, 0.1); +} + +.setting-item small { + display: block; + margin-top: 0.25rem; + color: var(--secondary-color); + font-size: 0.85rem; +} + +.modal-footer { + display: flex; + justify-content: space-between; + align-items: center; + gap: 1rem; + padding: 1rem; + border-top: 1px solid var(--border-color); +} + +.modal-footer .footer-right { + display: flex; + gap: 1rem; +} + +.btn { + padding: 0.5rem 1rem; + border-radius: var(--border-radius); + cursor: pointer; + transition: var(--transition); + font-size: 0.9rem; + font-family: inherit; +} + +.btn-secondary { + background: none; + border: 1px solid var(--border-color); + color: var(--secondary-color); +} + +.btn-secondary:hover { + background: var(--hover-color); + color: var(--primary-color); +} + +.btn-primary { + background: var(--accent-color); + border: 1px solid var(--accent-color); + color: white; +} + +.btn-primary:hover { + background: #357abd; + border-color: #357abd; +} + +.btn-test { + background: linear-gradient(135deg, #28a745, #20c997); + border: 1px solid #28a745; + color: white; + position: relative; + overflow: hidden; +} + +.btn-test:hover:not(:disabled) { + background: linear-gradient(135deg, #218838, #1ea688); + border-color: #218838; + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(40, 167, 69, 0.3); +} + +.btn-test:disabled { + background: #6c757d; + border-color: #6c757d; + cursor: not-allowed; + opacity: 0.7; +} + + +.custom-provider-info { + background: var(--surface-color); + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + padding: 1rem; + margin: 0.5rem 0; + font-size: 0.9rem; +} + +.custom-provider-info div { + margin-bottom: 0.5rem; +} + +.custom-provider-info div:last-child { + margin-bottom: 0; +} + +.custom-provider-info strong { + color: var(--primary-color); + font-weight: 600; +} + +.btn-delete { + background: var(--error-color); + border: 1px solid var(--error-color); + color: white; + padding: 0.5rem 1rem; + font-size: 0.85rem; +} + +.btn-delete:hover { + background: #dc2626; + border-color: #dc2626; + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(239, 68, 68, 0.3); +} + +.custom-model-form .setting-item { + margin-bottom: 1.2rem; +} + +.custom-model-form .setting-item:last-child { + margin-bottom: 0; +} + + +select option[value="create_custom"] { + background: linear-gradient(135deg, #e3f2fd, #bbdefb); + color: #1976d2; + font-weight: 500; +} + + +.modal-header h3 { + margin: 0; + color: var(--primary-color); + font-size: 1.25rem; + font-weight: 600; +} + + +option[value^="custom_"] { + background: #fff3cd; + color: #856404; +} + + +.toast { + position: fixed; + top: 80px; + left: 50%; + transform: translateX(-50%); + background: var(--primary-color); + color: white; + padding: 1rem 1.5rem; + border-radius: var(--border-radius); + box-shadow: var(--shadow-medium); + z-index: 1001; + animation: slideInDown 0.3s ease-out; + max-width: 400px; + line-height: 1.4; + word-break: break-word; + text-align: center; +} + + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes fadeInDown { + from { + opacity: 0; + transform: translateY(-20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes slideInDown { + from { + opacity: 0; + transform: translateX(-50%) translateY(-30px); + } + to { + opacity: 1; + transform: translateX(-50%) translateY(0); + } +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes slideInUp { + from { + opacity: 0; + transform: translateY(30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes slideInRight { + from { + opacity: 0; + transform: translateX(100px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +@keyframes slideInScale { + 0% { + opacity: 0; + transform: scale(0.8) translateY(20px); + } + 100% { + opacity: 1; + transform: scale(1) translateY(0); + } +} + +@keyframes slideInUp { + 0% { + opacity: 0; + transform: translateY(30px); + } + 100% { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes pulse { + 0%, 100% { + transform: scale(1); + } + 50% { + transform: scale(1.05); + } +} + +@keyframes fadeInScale { + from { + opacity: 0; + transform: scale(0.95); + } + to { + opacity: 1; + transform: scale(1); + } +} + +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + + +@media (max-width: 768px) { + .logo-text { + font-size: 2.2rem; + } + + .main-title { + font-size: 1.8rem; + } + + .features-preview { + flex-wrap: wrap; + gap: 0.6rem; + } + + .feature-card { + width: 120px; + flex: 0 0 120px; + } + + .function-buttons { + grid-template-columns: repeat(2, 1fr); + } + + .tool-main { + padding: 1rem; + } + + .chat-container { + gap: 1.5rem; + } +} + +@media (max-width: 600px) { + .features-preview { + flex-direction: column; + align-items: center; + } + + .feature-card { + width: 160px; + flex: 0 0 160px; + } +} + +@media (max-width: 480px) { + .function-buttons { + grid-template-columns: 1fr; + } + + .modal-content { + width: 95%; + } + + .result-controls { + flex-direction: column; + align-items: flex-start; + gap: 0.5rem; + } +} + + +.sidebar .function-btn { + width: 100%; + margin: 0.3rem 0; + padding: 0.8rem; + font-size: 0.9rem; + text-align: left; + background: var(--background-color); + border: 1px solid var(--border-color); + color: var(--text-color); + transition: var(--transition); +} + +.sidebar .function-btn:hover { + background: var(--hover-color); + border-color: var(--primary-color); +} + +.sidebar .function-btn.active { + background: var(--background-color); + color: var(--text-color); + border: 2px solid var(--primary-color); + box-shadow: 0 0 0 1px var(--primary-color); +} + +.modal-large { + max-width: 800px !important; + width: 95% !important; +} + +.provider-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 0.75rem; + padding: 0.25rem; + flex: 1; + min-height: 0; + overflow-y: auto; + -ms-overflow-style: none; + scrollbar-width: none; +} + +.provider-card { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 0.85rem; + background: var(--surface-color); + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + cursor: pointer; + transition: all 0.2s ease; + position: relative; + min-height: 110px; + width: 100%; +} + +.provider-badges { + position: absolute; + top: 8px; + right: 8px; + display: flex; + flex-direction: column; + gap: 4px; + align-items: flex-end; +} + +.provider-tag { + font-size: 0.7rem; + padding: 2px 8px; + border-radius: 999px; + font-weight: 600; + background: rgba(59, 130, 246, 0.1); + color: var(--accent-color); +} + +.provider-tag-left { + position: absolute; + top: 8px; + left: 8px; +} + +.sr-only { + position: absolute !important; + width: 1px !important; + height: 1px !important; + padding: 0 !important; + margin: -1px !important; + overflow: hidden !important; + clip: rect(0, 0, 0, 0) !important; + white-space: nowrap !important; + border: 0 !important; +} + +.provider-tag-info { + background: rgba(59, 130, 246, 0.1); + color: var(--accent-color); +} + +:root.dark .provider-tag-info { + background: rgba(59, 130, 246, 0.18); + color: #bfdbfe; +} + +.provider-tag-success { + background: #dcfce7; + color: #15803d; +} + +:root.dark .provider-tag-success { + background: rgba(34, 197, 94, 0.16); + color: #bbf7d0; +} + +.provider-tag-warning { + background: #fef9c3; + color: #b45309; +} + +:root.dark .provider-tag-warning { + background: rgba(245, 158, 11, 0.16); + color: #fde68a; +} + +.provider-card:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-medium); + border-color: var(--accent-color); +} + +.provider-card.configured { + border-color: var(--accent-color); +} + +.provider-card.active { + border-color: var(--accent-color); + background: rgba(59, 130, 246, 0.1); + box-shadow: 0 0 0 2px var(--accent-color); +} + +:root.dark .provider-card.active { + background: rgba(59, 130, 246, 0.18); + box-shadow: 0 0 0 2px rgba(96, 165, 250, 0.7); +} + +.provider-icon { + width: 36px; + height: 36px; + border-radius: 10px; + background: #f3f4f6; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.1rem; + font-weight: bold; + color: var(--accent-color); + margin-bottom: 0.5rem; + box-shadow: var(--shadow-light); +} + +.provider-card.clear-cache { + background: #fef2f2; + border-color: #fecaca; +} + +:root.dark .provider-card.clear-cache { + background: #1f2937; + border-color: #7f1d1d; +} + +.provider-card.clear-cache .provider-icon { + font-size: 20px; +} + +.provider-card.clear-cache .provider-name { + color: #ef4444; +} + +:root.dark .provider-card.clear-cache .provider-name { + color: #fca5a5; +} + +:root.dark .provider-icon { + background: #111827; +} + +.provider-icon.custom { + color: var(--warning-color); +} + +.provider-icon.add { + color: var(--success-color); + font-size: 2rem; +} + +.provider-name { + font-size: 0.8rem; + font-weight: 500; + text-align: center; + color: var(--primary-color); +} + +.current-badge { + background: var(--accent-color); + color: white; + font-size: 0.7rem; + padding: 2px 6px; + border-radius: 4px; +} + +.available-badge { + background: rgba(59, 130, 246, 0.1); + color: var(--accent-color); + border: 1px solid var(--accent-color); + font-size: 0.7rem; + padding: 1px 5px; + border-radius: 4px; +} + +:root.dark .available-badge { + background: rgba(59, 130, 246, 0.16); + color: #bfdbfe; + border-color: rgba(96, 165, 250, 0.8); +} + +.input-error { + border-color: var(--error-color) !important; + background-color: #fef2f2; +} + +:root.dark .input-error { + background-color: rgba(127, 29, 29, 0.28); + border-color: #ef4444 !important; +} + +.model-options.input-error { + border-color: var(--error-color) !important; +} + +.custom-badge { + margin-top: 4px; + font-size: 0.7rem; + color: var(--warning-color); + background: rgba(245, 158, 11, 0.1); + padding: 2px 6px; + border-radius: 4px; +} + +:root.dark .custom-badge { + background: rgba(245, 158, 11, 0.16); + color: #fde68a; +} + +.header-title-area { + display: flex; + align-items: center; + gap: 1rem; +} + +.back-button { + background: none; + border: none; + cursor: pointer; + color: var(--secondary-color); + font-size: 1.2rem; + padding: 4px 8px; + border-radius: 4px; + transition: var(--transition); +} + +.back-button:hover { + background: var(--hover-color); + color: var(--primary-color); +} + + +.provider-grid::-webkit-scrollbar { + display: none; +} + + +.provider-icon-img { + width: 32px; + height: 32px; + object-fit: contain; +} + +.provider-icon-img-themed, +.model-option-icon-img-themed { + filter: var(--provider-icon-filter, none); + transition: filter 0.2s ease; +} + +.provider-icon-img-static, +.model-option-icon-img-static { + filter: none; +} + +.provider-info-banner { + background: rgba(59, 130, 246, 0.08); + border: 1px solid rgba(59, 130, 246, 0.2); + border-radius: var(--border-radius); + padding: 0.75rem; + margin-bottom: 1rem; + color: var(--primary-color); + position: relative; +} + +:root.dark .provider-info-banner { + background: rgba(30, 41, 59, 0.92); + border-color: rgba(96, 165, 250, 0.25); + color: var(--primary-color); +} + +.provider-info-banner small { + display: block; + margin-top: 0.25rem; + color: var(--secondary-color); +} + +.provider-info-close { + position: absolute; + top: 6px; + right: 8px; + border: none; + background: transparent; + color: var(--secondary-color); + font-size: 1rem; + cursor: pointer; + padding: 2px; +} + +.provider-info-close:hover { + color: var(--primary-color); +} + +.provider-alert { + padding: 0.65rem 0.8rem; + border-radius: var(--border-radius); + font-size: 0.85rem; + margin-bottom: 1rem; +} + +.provider-alert.warning { + background: #fef2f2; + border: 1px solid #fecaca; + color: #b91c1c; +} + +:root.dark .provider-alert.warning { + background: rgba(127, 29, 29, 0.28); + border-color: rgba(248, 113, 113, 0.45); + color: #fecaca; +} + +.model-options { + display: flex; + flex-direction: column; + gap: 0.6rem; + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + padding: 0.85rem; + background: var(--surface-color); +} + +.model-options-list { + display: flex; + flex-direction: column; + gap: 0.4rem; + max-height: 220px; + overflow-y: auto; + padding-right: 0.2rem; +} + +.model-options-actions { + display: flex; + flex-wrap: wrap; + gap: 0.9rem; + padding-top: 0.4rem; + border-top: 1px dashed var(--border-color); + margin-top: 0.2rem; +} + +.model-option { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + padding: 0.45rem 0.65rem; + background: transparent; + color: var(--primary-color); + cursor: pointer; + transition: var(--transition); +} + +.model-option:hover { + border-color: var(--accent-color); +} + +.model-option.active { + border-color: var(--accent-color); + background: rgba(59, 130, 246, 0.08); + box-shadow: inset 0 0 0 1px rgba(59, 130, 246, 0.2); +} + +:root.dark .model-option.active { + background: rgba(59, 130, 246, 0.18); + box-shadow: inset 0 0 0 1px rgba(96, 165, 250, 0.35); +} + +.model-option-name { + flex: 1; + text-align: left; +} + +.model-tag { + margin-left: 0.5rem; + padding: 2px 8px; + border-radius: 999px; + background: rgba(59, 130, 246, 0.15); + color: var(--accent-color); + font-size: 0.7rem; + font-weight: 600; +} + +.model-tag-vision { + background: rgba(168, 85, 247, 0.15); + color: #a855f7; +} + +:root.dark .model-tag-vision { + background: rgba(168, 85, 247, 0.18); + color: #d8b4fe; +} + +.model-tag-fast { + background: rgba(16, 185, 129, 0.15); + color: #10b981; +} + +:root.dark .model-tag-fast { + background: rgba(16, 185, 129, 0.18); + color: #a7f3d0; +} + +.model-tag-better { + background: rgba(239, 68, 68, 0.15); + color: #ef4444; +} + +:root.dark .model-tag-better { + background: rgba(239, 68, 68, 0.18); + color: #fecaca; +} + +.model-tag-flagship { + background: rgba(239, 68, 68, 0.14); + color: #dc2626; + border: 1px solid rgba(239, 68, 68, 0.35); +} + +:root.dark .model-tag-flagship { + background: rgba(239, 68, 68, 0.16); + color: #fecaca; + border-color: rgba(248, 113, 113, 0.35); +} + +.model-tag-affordable { + background: rgba(59, 130, 246, 0.14); + color: #2563eb; + border: 1px solid rgba(59, 130, 246, 0.35); } -/* 模态框标题动态样式 */ -.modal-header h3 { - margin: 0; - color: var(--primary-color); - font-size: 1.25rem; - font-weight: 600; +:root.dark .model-tag-affordable { + background: rgba(59, 130, 246, 0.16); + color: #bfdbfe; + border-color: rgba(96, 165, 250, 0.35); } -/* 自定义模型标识样式 */ -option[value^="custom_"] { - background: #fff3cd; - color: #856404; +.model-tag-free { + background: rgba(16, 185, 129, 0.14); + color: #059669; + border: 1px solid rgba(16, 185, 129, 0.35); } -/* Toast 通知样式 */ -.toast { - position: fixed; - top: 80px; - left: 50%; - transform: translateX(-50%); - background: var(--primary-color); - color: white; - padding: 1rem 1.5rem; - border-radius: var(--border-radius); - box-shadow: var(--shadow-medium); - z-index: 1001; - animation: slideInDown 0.3s ease-out; - max-width: 400px; - line-height: 1.4; - word-break: break-word; - text-align: center; +:root.dark .model-tag-free { + background: rgba(16, 185, 129, 0.16); + color: #a7f3d0; + border-color: rgba(52, 211, 153, 0.35); } -/* 动画定义 */ -@keyframes fadeIn { - from { opacity: 0; } - to { opacity: 1; } +:root.dark option[value^="custom_"] { + background: #1f2937; + color: #f3f4f6; } -@keyframes fadeInDown { - from { - opacity: 0; - transform: translateY(-20px); - } - to { - opacity: 1; - transform: translateY(0); - } + + +:root.dark { + --primary-color: #e5e7eb; + --secondary-color: #9ca3af; + --background-color: #111827; + --surface-color: #1f2937; + --border-color: #374151; + --hover-color: #374151; + --panel-gradient-start: #0f172a; + --panel-gradient-end: #111827; + --control-gradient-start: #1f2937; + --control-gradient-end: #0f172a; + --hover-accent-gradient: #1f2937; + --shadow-light: 0 1px 3px 0 rgba(0, 0, 0, 0.5); + --shadow-medium: 0 4px 6px -1px rgba(0, 0, 0, 0.5); + --shadow-large: 0 10px 15px -3px rgba(0, 0, 0, 0.5); + --provider-icon-filter: invert(1) brightness(1.8); } -@keyframes slideInDown { - from { - opacity: 0; - transform: translateX(-50%) translateY(-30px); - } - to { - opacity: 1; - transform: translateX(-50%) translateY(0); - } + +.app-container { + display: flex; + flex-direction: column; + height: 100vh; + width: 100vw; + overflow: hidden; + background-color: var(--background-color); + color: var(--primary-color); } -@keyframes fadeInUp { - from { - opacity: 0; - transform: translateY(20px); - } - to { - opacity: 1; - transform: translateY(0); - } +.page-content { + flex: 1; + overflow: hidden; + position: relative; } -@keyframes slideInUp { - from { - opacity: 0; - transform: translateY(30px); - } - to { - opacity: 1; - transform: translateY(0); - } + +.titlebar { + height: 40px; + background: var(--surface-color); + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 0 0 16px; + border-bottom: 1px solid var(--border-color); + user-select: none; + flex-shrink: 0; + -webkit-app-region: drag; } -@keyframes slideInRight { - from { - opacity: 0; - transform: translateX(100px); - } - to { - opacity: 1; - transform: translateX(0); - } +.titlebar [data-tauri-drag-region="false"] { + -webkit-app-region: no-drag; } -@keyframes slideInScale { - 0% { - opacity: 0; - transform: scale(0.8) translateY(20px); - } - 100% { - opacity: 1; - transform: scale(1) translateY(0); - } +.titlebar-left { + display: flex; + align-items: center; } -@keyframes slideInUp { - 0% { - opacity: 0; - transform: translateY(30px); - } - 100% { - opacity: 1; - transform: translateY(0); - } +.tabs { + display: flex; + gap: 8px; } -@keyframes pulse { - 0%, 100% { - transform: scale(1); - } - 50% { - transform: scale(1.05); - } +.tab-button { + background: transparent; + border: none; + padding: 6px 12px; + font-size: 14px; + color: var(--secondary-color); + cursor: pointer; + border-radius: 6px; + transition: all 0.2s; + font-family: inherit; } -@keyframes fadeInScale { - from { - opacity: 0; - transform: scale(0.95); - } - to { - opacity: 1; - transform: scale(1); - } +.tab-button:hover { + background: var(--hover-color); + color: var(--primary-color); } -@keyframes spin { - from { transform: rotate(0deg); } - to { transform: rotate(360deg); } +.tab-button.active { + background: var(--hover-color); + color: var(--accent-color); + font-weight: 500; } -/* 响应式设计 */ -@media (max-width: 768px) { - .logo-text { - font-size: 2.2rem; - } - - .main-title { - font-size: 1.8rem; - } - - .features-preview { - flex-wrap: wrap; - gap: 0.6rem; - } - - .feature-card { - width: 120px; - flex: 0 0 120px; - } - - .function-buttons { - grid-template-columns: repeat(2, 1fr); - } - - .tool-main { - padding: 1rem; - } - - .chat-container { - gap: 1.5rem; - } +.titlebar-right { + display: flex; + align-items: center; + height: 100%; } -@media (max-width: 600px) { - .features-preview { - flex-direction: column; - align-items: center; - } - - .feature-card { - width: 160px; - flex: 0 0 160px; - } +.control-group { + display: flex; + align-items: center; + padding-right: 12px; + border-right: 1px solid var(--border-color); + height: 24px; + gap: 8px; } -@media (max-width: 480px) { - .function-buttons { - grid-template-columns: 1fr; - } - - .modal-content { - width: 95%; - } - - .result-controls { - flex-direction: column; - align-items: flex-start; - gap: 0.5rem; - } +.theme-toggle { + display: flex; + background: var(--hover-color); + border-radius: 4px; + padding: 2px; } -/* 侧边栏功能按钮样式 */ -.sidebar .function-btn { - width: 100%; - margin: 0.3rem 0; - padding: 0.8rem; - font-size: 0.9rem; - text-align: left; - background: var(--background-color); - border: 1px solid var(--border-color); - color: var(--text-color); - transition: var(--transition); +.icon-btn { + background: transparent; + border: none; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + color: var(--secondary-color); + cursor: pointer; + border-radius: 4px; + font-size: 12px; + transition: all 0.2s; } -.sidebar .function-btn:hover { - background: var(--hover-color); - border-color: var(--primary-color); +.icon-btn:hover { + color: var(--primary-color); } -.sidebar .function-btn.active { +.icon-btn.active { background: var(--background-color); - color: var(--text-color); - border: 2px solid var(--primary-color); - box-shadow: 0 0 0 1px var(--primary-color); + color: var(--accent-color); + box-shadow: 0 1px 2px rgba(0,0,0,0.1); } -/* 侧边栏折叠时的样式 */ -.sidebar.collapsed { - width: 50px; +.settings-btn { + width: 28px; + height: 28px; + font-size: 14px; } -.sidebar.collapsed .sidebar-title, -.sidebar.collapsed .function-buttons { - display: none; +.window-controls { + display: flex; + height: 100%; } -.sidebar.collapsed .sidebar-header { +.window-btn { + background: transparent; + border: none; + width: 46px; + height: 100%; + display: flex; + align-items: center; justify-content: center; - padding: 0.5rem; + color: var(--primary-color); + cursor: pointer; + transition: background 0.2s; + font-size: 14px; } -.sidebar.collapsed .collapse-btn { - margin: 0; -} \ No newline at end of file +.window-btn:hover { + background: var(--hover-color); +} + +.window-btn.close:hover { + background: #ef4444; + color: white; +} + +:root.dark .toast { + background: var(--surface-color); + border: 1px solid var(--primary-color); + color: var(--primary-color); +} diff --git a/src/types/index.ts b/src/types/index.ts index be14fa34..b54b1c02 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,11 +1,46 @@ -// AI Provider Types -export type AIProvider = 'openai' | 'deepseek' | 'glm'; +export type AIProvider = + | 'onedocs' + | 'openai' + | 'anthropic' + | 'gemini' + | 'moonshot' + | 'glm' + | 'deepseek' + | 'ollama' + | 'lmstudio' + | 'comp_share' + | '302_ai' + | 'pony' + | 'siliconflow' + | 'xinghe' + | 'ppio' + | 'modelscope' + | 'newapi' + | 'openrouter' + | 'oneapi'; + export type CustomProviderKey = `custom_${string}`; export type AllProviders = AIProvider | CustomProviderKey; +export type ModelTagVariant = + | 'thinking' + | 'vision' + | 'fast' + | 'better' + | 'flagship' + | 'affordable' + | 'free'; + +export interface ModelTag { + label: string; + variant?: ModelTagVariant; +} + export interface ModelOption { value: string; name: string; + tag?: string; + tags?: ModelTag[]; } export interface ProviderConfig { @@ -17,6 +52,18 @@ export interface ProviderConfig { keyLabel: string; keyHint: string; baseUrlHint: string; + icon?: string; + iconColor?: string; + badgeText?: string; + badgeVariant?: 'info' | 'success' | 'warning'; + requiresApiKey?: boolean; + requiresBaseUrl?: boolean; + showApiKeyField?: boolean; + showBaseUrlField?: boolean; + credentialsReadOnly?: boolean; + allowModelCustomization?: boolean; + defaultApiKey?: string; + description?: string; } export interface CustomProviderConfig { @@ -29,35 +76,80 @@ export interface CustomProviderConfig { export type ModelProviders = Record; -// Prompt Configuration Types export type PromptType = 'science' | 'liberal' | 'data' | 'news'; export interface PromptConfig { name: string; description: string; + coreSystemPrompt: string; + chunkTasks: string[]; + sectionHeaders: string[]; prompt: string; } export type PromptConfigs = Record; -// File Types -export type SupportedFileType = - | 'application/pdf' - | 'application/msword' - | 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' - | 'application/vnd.ms-powerpoint' - | 'application/vnd.openxmlformats-officedocument.presentationml.presentation' - | 'text/plain'; +export type SupportedFileType = 'application/pdf'; export interface FileInfo { file: File; name: string; type: SupportedFileType; size: number; - id?: string; // 用于多文件管理 + id?: string; +} + +export interface DocumentImageAsset { + pageNumber: number; + fileName: string; + localPath: string; + dataUrl?: string; +} + +/** Mapping of page number to the images found on that page */ +export interface PageImageMap { + pageNumber: number; + /** Snippet of text content on this page (for context matching) */ + textSnippet: string; + /** Image file names found on this page */ + imageFileNames: string[]; + /** Unique insertion tags for each image on this page (e.g. [[IMG_P3_001]]) */ + imageTags: string[]; +} + +export interface DocumentAnalysisBundle { + text: string; + pageTexts: string[]; + images: DocumentImageAsset[]; + /** Mapping of page numbers to their images and text snippets */ + pageImageMap: PageImageMap[]; + pageCount: number; + /** Short hash-based directory name for this document's assets */ + hashDir: string; +} + +export interface ChunkPlan { + id: string; + title: string; + pageStart: number; + pageEnd: number; + summaryFocus?: string; + imagePages?: number[]; +} + +export interface AnalysisWorkflowPlan { + fileName: string; + fileType: SupportedFileType; + overview: string; + chunks: ChunkPlan[]; + imagePlacementHints: Array<{ + pageNumber: number; + reason: string; + preferredSection?: string; + }>; + finishingNotes: string[]; } -// Analysis Types export interface AnalysisProgress { percentage: number; message: string; @@ -66,15 +158,13 @@ export interface AnalysisProgress { export interface AnalysisResult { content: string; timestamp: number; - fileId?: string; // 关联到文件ID + fileId?: string; } -// 多文件分析结果 export interface MultiFileAnalysisResult { [fileId: string]: AnalysisResult; } -// Settings Types export interface ProviderSettings { apiKey: string; baseUrl: string; @@ -97,16 +187,24 @@ export interface AppSettings { customProviders: CustomSettings; } -// View Types export type ViewMode = 'render' | 'markdown'; -// Toast Types export interface ToastMessage { message: string; duration?: number; } -// API Request/Response Types +export type DeveloperLogLevel = 'info' | 'warn' | 'error'; + +export interface DeveloperLogEntry { + id: string; + timestamp: number; + level: DeveloperLogLevel; + scope: string; + message: string; + payload?: unknown; +} + export interface ChatMessage { role: 'system' | 'user' | 'assistant'; content: string; @@ -135,3 +233,18 @@ export interface APIError { code?: string; }; } + +// ========== RAG Types ========== + +export interface TextChunk { + id: string; + content: string; + /** Start character index in source text */ + startIndex: number; + /** End character index in source text */ + endIndex: number; + /** Which file page this chunk came from */ + sourcePage: number; + /** Embedding vector (stored as raw number array) */ + embedding?: number[]; +} diff --git a/src/types/pdfjs.d.ts b/src/types/pdfjs.d.ts new file mode 100644 index 00000000..106d415c --- /dev/null +++ b/src/types/pdfjs.d.ts @@ -0,0 +1,2 @@ +declare module 'pdfjs-dist/legacy/build/pdf'; +declare module 'pdfjs-dist/legacy/build/pdf.worker.min.mjs?url'; diff --git a/src/utils/analysisWorkflow.ts b/src/utils/analysisWorkflow.ts new file mode 100644 index 00000000..6edf3c4d --- /dev/null +++ b/src/utils/analysisWorkflow.ts @@ -0,0 +1,848 @@ +import { useAppStore } from "@/store/useAppStore"; +import { APIService } from "@/services/api"; +import type { + AnalysisWorkflowPlan, + ChunkPlan, + DocumentAnalysisBundle, + DocumentImageAsset, + PageImageMap, + SupportedFileType, +} from "@/types"; + +const pushDevLog = ( + level: "info" | "warn" | "error", + scope: string, + message: string, + payload?: unknown, +) => { + const { addLog } = useAppStore.getState(); + if (addLog) { + addLog({ + id: `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, + timestamp: Date.now(), + level, + scope, + message, + payload, + }); + } +}; + +const PAGE_IMAGE_TOKEN_PREFIX = "[[PAGE_IMAGE_"; +const DEFAULT_PAGE_CHUNK_SIZE = 4; +const DEFAULT_CHAR_CHUNK_SIZE = 7000; + +export const WORKFLOW_PLANNER_MODEL = "openai/gpt-oss-120b:free"; +export const WORKFLOW_COMPOSER_MODEL = "minimax/minimax-m2.5:free"; + +export const stripMarkdownFence = (text: string): string => { + let current = text.trim(); + const fenceRegex = /^```[a-zA-Z0-9_-]*\s+([\s\S]*?)\s*```\s*$/; + + while (true) { + const match = current.match(fenceRegex); + if (!match) break; + current = match[1].trim(); + } + + return current; +}; + +export const stripLeadingMarkers = (text: string): string => { + const trimmed = text.trimStart(); + return trimmed.replace(/^【?格式修正内容】?/i, "").trimStart(); +}; + +export const parseWorkflowPlan = ( + content: string, + fallbackPlan: AnalysisWorkflowPlan, +): AnalysisWorkflowPlan => { + const cleaned = stripMarkdownFence(content).trim(); + + if (!cleaned) { + return fallbackPlan; + } + + try { + const parsed = JSON.parse(cleaned) as Partial; + return { + ...fallbackPlan, + ...parsed, + chunks: + Array.isArray(parsed.chunks) && parsed.chunks.length > 0 + ? parsed.chunks.map((chunk, index) => ({ + id: chunk.id || `chunk_${index + 1}`, + title: chunk.title || `第 ${index + 1} 段`, + pageStart: Number(chunk.pageStart) || fallbackPlan.chunks[index]?.pageStart || 1, + pageEnd: Number(chunk.pageEnd) || fallbackPlan.chunks[index]?.pageEnd || 1, + summaryFocus: chunk.summaryFocus || fallbackPlan.chunks[index]?.summaryFocus, + imagePages: Array.isArray(chunk.imagePages) + ? chunk.imagePages.map((page) => Number(page)).filter(Boolean) + : fallbackPlan.chunks[index]?.imagePages, + })) + : fallbackPlan.chunks, + imagePlacementHints: Array.isArray(parsed.imagePlacementHints) + ? parsed.imagePlacementHints + .map((hint) => ({ + pageNumber: Number(hint.pageNumber) || 0, + reason: hint.reason || "", + preferredSection: hint.preferredSection, + })) + .filter((hint) => hint.pageNumber > 0) + : fallbackPlan.imagePlacementHints, + finishingNotes: + Array.isArray(parsed.finishingNotes) && parsed.finishingNotes.length > 0 + ? parsed.finishingNotes.map((item) => String(item)) + : fallbackPlan.finishingNotes, + }; + } catch (error) { + console.warn("解析工作流计划失败,回退到本地计划", error); + return fallbackPlan; + } +}; + +export const buildLocalWorkflowPlan = ( + bundle: DocumentAnalysisBundle, + fileName: string, + fileType: SupportedFileType, +): AnalysisWorkflowPlan => { + const chunks = buildChunkPlan(bundle); + const imagePlacementHints = bundle.images.map((image) => ({ + pageNumber: image.pageNumber, + reason: `第 ${image.pageNumber} 页存在页面图像素材,适合在对应文字说明附近插入。`, + preferredSection: inferPreferredSection(bundle.pageTexts[image.pageNumber - 1] || ""), + })); + + return { + fileName, + fileType, + overview: bundle.pageCount > 0 + ? `文档共 ${bundle.pageCount} 页,已按页切分为 ${chunks.length} 个分析段。` + : "文档已按字符长度切分为分析段。", + chunks, + imagePlacementHints, + finishingNotes: [ + "最终结果必须保留原文中的关键定义、公式、例题与结论。", + "如果存在图片素材,应优先插入到对应页面内容附近。", + "不要遗漏章节标题、图表说明和例题推导过程。", + ], + }; +}; + +export const buildChunkPlan = ( + bundle: DocumentAnalysisBundle, +): ChunkPlan[] => { + if (bundle.pageTexts.length === 0) { + return buildTextFallbackChunkPlan(bundle.text); + } + + const chunks: ChunkPlan[] = []; + let startPage = 1; + let currentLength = 0; + + for (let pageIndex = 0; pageIndex < bundle.pageTexts.length; pageIndex++) { + const pageText = bundle.pageTexts[pageIndex] || ""; + currentLength += pageText.length; + + const isChunkBoundary = + (pageIndex + 1 - startPage + 1) >= DEFAULT_PAGE_CHUNK_SIZE || + currentLength >= DEFAULT_CHAR_CHUNK_SIZE || + pageIndex === bundle.pageTexts.length - 1; + + if (!isChunkBoundary) { + continue; + } + + const pageStart = startPage; + const pageEnd = pageIndex + 1; + const chunkText = bundle.pageTexts.slice(pageStart - 1, pageEnd).join("\n\n"); + const chunkImages = bundle.images + .filter((image) => image.pageNumber >= pageStart && image.pageNumber <= pageEnd) + .map((image) => image.pageNumber); + + chunks.push({ + id: `chunk_${chunks.length + 1}`, + title: `第 ${chunks.length + 1} 段`, + pageStart, + pageEnd, + summaryFocus: inferChunkFocus(chunkText), + imagePages: chunkImages.length > 0 ? chunkImages : undefined, + }); + + startPage = pageEnd + 1; + currentLength = 0; + } + + return chunks.length > 0 ? chunks : buildTextFallbackChunkPlan(bundle.text); +}; + +export const buildChunkPrompt = ({ + sourcePrompt, + workflowPlan, + chunk, + chunkText, + fileName, + pageImageTokens, + includeImages = true, +}: { + sourcePrompt: string; + workflowPlan: AnalysisWorkflowPlan; + chunk: ChunkPlan; + chunkText: string; + fileName: string; + pageImageTokens: string[]; + includeImages?: boolean; +}): string => { + const imageTokenHint = includeImages + ? pageImageTokens.length > 0 + ? `\n\n本段对应图片占位符候选:${pageImageTokens.join("、")}。请在合适位置保留这些占位符,不要自行编造图片。` + : "\n\n本段没有可用图片素材。" + : ""; + + return [ + sourcePrompt.trim(), + "", + "# 分段写作任务", + `- 文件名:${fileName}`, + `- 当前段:${chunk.title}`, + `- 页码范围:第 ${chunk.pageStart} 页 - 第 ${chunk.pageEnd} 页`, + chunk.summaryFocus ? `- 当前段关注点:${chunk.summaryFocus}` : "", + `- 总体计划:${workflowPlan.overview}`, + "", + "## 任务要求", + "- 你只负责当前页码范围内的内容,不要输出完整总标题。", + "- 保留原文的专业术语、公式、定理、图表说明和例题步骤。", + "- 如果发现适合插图的位置,请保留对应图片占位符。", + "- 不要丢失原文的重要细节,不要把多个概念合并成一段空泛总结。", + imageTokenHint, + "", + "## 当前页内容", + chunkText.trim(), + ] + .filter(Boolean) + .join("\n"); +}; + +export const buildComposerPrompt = ({ + sourcePrompt, + workflowPlan, + fragments, + fileName, + imageManifest, + includeImages = true, +}: { + sourcePrompt: string; + workflowPlan: AnalysisWorkflowPlan; + fragments: string[]; + fileName: string; + imageManifest: DocumentImageAsset[]; + includeImages?: boolean; +}): string => { + const imageLines = includeImages + ? imageManifest.length > 0 + ? imageManifest + .map((image) => { + const token = getImageToken(image.pageNumber); + const pathHint = image.localPath || image.dataUrl || ""; + return `${token} => ${pathHint}`; + }) + .join("\n") + : "无可用图片素材。" + : ""; + + return [ + sourcePrompt.trim(), + "", + "# 最终整合任务", + `- 文件名:${fileName}`, + `- 工作流概览:${workflowPlan.overview}`, + "", + "## 任务说明", + "- 你需要把下方所有分段结果拼接成完整、连续、无重复的最终文档。", + "- 必须保留原有知识顺序,并补齐基础知识与典型例题的整体结构。", + "- 对于图片占位符,必须保留原占位符文本,不要替换成其它文字。", + "- 输出最终 Markdown,不要输出分析过程。", + "", + includeImages ? "## 图片映射" : "", + includeImages ? imageLines : "", + "", + "## 分段结果", + fragments.join("\n\n---\n\n"), + ] + .filter(Boolean) + .join("\n"); +}; + +export const applyImagePlacements = ( + markdown: string, + images: DocumentImageAsset[], +): string => { + if (!markdown || images.length === 0) { + pushDevLog("info", "image-insert", "applyImagePlacements: 无图片需要插入或 Markdown 为空", { + hasMarkdown: Boolean(markdown), + imageCount: images.length, + }); + return markdown; + } + + pushDevLog("info", "image-insert", "applyImagePlacements: 开始替换图片占位符", { + imageCount: images.length, + images: images.map(img => ({ page: img.pageNumber, name: img.fileName, path: img.localPath })), + }); + + let nextMarkdown = markdown; + const usedImageKeys = new Set(); + let replacedCount = 0; + + for (const image of images) { + const token = getImageToken(image.pageNumber); + const imageTarget = image.localPath || image.dataUrl || ""; + if (!imageTarget) { + continue; + } + + const imageMarkdown = `\n\n![第 ${image.pageNumber} 页图片](${normalizeMarkdownPath(imageTarget)})\n\n`; + if (nextMarkdown.includes(token)) { + nextMarkdown = nextMarkdown.split(token).join(imageMarkdown); + usedImageKeys.add(imageTarget); + replacedCount++; + } + } + + // Single unified pass: replace all placeholder patterns with extracted images + // Patterns handled (in priority order): + // 1. [[PAGE_IMAGE_NNN]] tokens + // 2. ![alt](placeholder) — markdown image with placeholder URL + // 3. [图片:xxx] — text-only placeholders + // 4. ![alt] — bare image without URL + const allRemainingImages = images.filter((image) => { + const target = image.localPath || image.dataUrl || ""; + return target && !usedImageKeys.has(target); + }); + + if (allRemainingImages.length > 0) { + let imageIndex = 0; + + const getNextImage = (): string | null => { + while (imageIndex < allRemainingImages.length) { + const image = allRemainingImages[imageIndex]; + imageIndex++; + const imageTarget = image.localPath || image.dataUrl || ""; + if (!imageTarget) continue; + usedImageKeys.add(imageTarget); + return normalizeMarkdownPath(imageTarget); + } + return null; + }; + + // Pass 1: Replace ![alt](placeholder) patterns (must come before text-only) + // This prevents double-replacement where [图片:xxx] → ![图片:xxx](path) then ![图片:xxx](path) → !!![...] + const mdImageRegex = /!\[([^\]]*?)\]\(([^)]*)\)/g; + nextMarkdown = nextMarkdown.replace(mdImageRegex, (match, alt, url) => { + const trimmedUrl = url.trim(); + const looksPlaceholder = + !trimmedUrl || + /^placeholder$/i.test(trimmedUrl) || + /^image_placeholder$/i.test(trimmedUrl) || + /图片占位符/.test(trimmedUrl) || + /^占位$/.test(trimmedUrl) || + /^[a-z_]*placeholder[a-z_]*$/i.test(trimmedUrl) || + /^image[_\-]?\d+\.[a-z]+$/i.test(trimmedUrl); + if (!looksPlaceholder) { + return match; + } + const imgPath = getNextImage(); + if (imgPath) replacedCount++; + return imgPath ? `\n\n![${alt.trim()}](${imgPath})\n\n` : match; + }); + + // Pass 2: Replace text-only placeholders like [图片:xxx] + const textPlaceholderRegex = /\[(图片|图|示意图|照片|插图)[::\s][^\]]+\]/g; + nextMarkdown = nextMarkdown.replace(textPlaceholderRegex, (match) => { + const imgPath = getNextImage(); + if (imgPath) replacedCount++; + return imgPath ? `\n\n![${match.slice(1, -1)}](${imgPath})\n\n` : match; + }); + + // Pass 3: Replace bare ![alt] without URL + const bareImageRegex = /!\[([^\]]+)\](?!\()/g; + nextMarkdown = nextMarkdown.replace(bareImageRegex, (match, alt) => { + const imgPath = getNextImage(); + if (imgPath) replacedCount++; + return imgPath ? `\n\n![${alt.trim()}](${imgPath})\n\n` : match; + }); + } + + pushDevLog("info", "image-insert", `applyImagePlacements 完成: 替换了 ${replacedCount} 处图片引用`, { + replacedCount, + usedImageCount: usedImageKeys.size, + totalImages: images.length, + }); + + return nextMarkdown; +}; + +export const buildImageTokenList = (images: DocumentImageAsset[]): string[] => { + return images.map((image) => getImageToken(image.pageNumber)); +}; + +export const getImageToken = (pageNumber: number): string => { + return `${PAGE_IMAGE_TOKEN_PREFIX}${String(pageNumber).padStart(3, "0")}]]`; +}; + +export const splitTextIntoChunks = (text: string, maxChunkLength = DEFAULT_CHAR_CHUNK_SIZE): string[] => { + const normalized = text.trim(); + if (!normalized) { + return []; + } + + if (normalized.length <= maxChunkLength) { + return [normalized]; + } + + const chunks: string[] = []; + let start = 0; + + while (start < normalized.length) { + let end = Math.min(start + maxChunkLength, normalized.length); + if (end < normalized.length) { + const nextBreak = normalized.lastIndexOf("\n\n", end); + if (nextBreak > start + Math.floor(maxChunkLength * 0.5)) { + end = nextBreak; + } + } + + const slice = normalized.slice(start, end).trim(); + if (slice) { + chunks.push(slice); + } + + start = end; + } + + return chunks; +}; + +const buildTextFallbackChunkPlan = (text: string): ChunkPlan[] => { + const chunks = splitTextIntoChunks(text, DEFAULT_CHAR_CHUNK_SIZE); + return chunks.map((chunkText, index) => ({ + id: `chunk_${index + 1}`, + title: `第 ${index + 1} 段`, + pageStart: index + 1, + pageEnd: index + 1, + summaryFocus: inferChunkFocus(chunkText), + })); +}; + + +const inferPreferredSection = (text: string): string => { + if (/例题|习题|答案/.test(text)) { + return "典型例题"; + } + + return "基础知识"; +}; + +const normalizeMarkdownPath = (pathValue: string): string => { + // Only normalize backslashes to forward slashes for Markdown compatibility. + // Do NOT convert to Tauri asset protocol URLs here — that is the + // responsibility of MarkdownRenderer.rewriteLocalImageSources at render time. + // Keeping real local paths in the Markdown source ensures: + // 1. Source view shows readable local file paths + // 2. Render view can properly convert them via convertFileSrc + return pathValue.replace(/\\/g, "/"); +}; + +/** + * Insert images into markdown using tag-based replacement + LLM fallback. + * + * Strategy: + * 1. Replace any [[IMG_P{page}_{idx}]] tags left by the LLM with actual image Markdown + * 2. For images not yet referenced (no tag in the output), use LLM matching or rule-based fallback + * 3. Ensure every image is on its own line (preceded by a newline) + */ +export const insertImagesByPageContext = async ( + markdown: string, + images: DocumentImageAsset[], + pageImageMap?: PageImageMap[], + llmContext?: { + provider: string; + apiKey: string; + baseUrl: string; + model: string; + }, +): Promise => { + if (!markdown || images.length === 0) { + pushDevLog("info", "image-insert", "insertImagesByPageContext: 无图片需要插入或 Markdown 为空", { + hasMarkdown: Boolean(markdown), + imageCount: images.length, + }); + return markdown; + } + + // Step 1: Build tag → image mapping from pageImageMap + const tagToImage = new Map(); + if (pageImageMap) { + for (const pageEntry of pageImageMap) { + for (let i = 0; i < pageEntry.imageTags.length; i++) { + const tag = pageEntry.imageTags[i]; + const fileName = pageEntry.imageFileNames[i]; + const image = images.find(img => img.fileName === fileName && img.pageNumber === pageEntry.pageNumber); + if (image) { + tagToImage.set(tag, image); + } + } + } + } + + // Step 2: Replace tags with actual image Markdown + let result = markdown; + let tagReplacedCount = 0; + + for (const [tag, image] of tagToImage) { + if (result.includes(tag)) { + const imageTarget = image.localPath || image.dataUrl || ""; + if (imageTarget) { + const imageMarkdown = `\n\n![第 ${image.pageNumber} 页图片](${normalizeMarkdownPath(imageTarget)})\n\n`; + // Replace all occurrences of the tag + result = result.split(tag).join(imageMarkdown); + tagReplacedCount++; + } + } + } + + pushDevLog("info", "image-insert", `标签替换完成: ${tagReplacedCount}/${tagToImage.size} 个标签已替换`, { + tagReplacedCount, + totalTags: tagToImage.size, + }); + + // Step 3: Find images not yet referenced in the markdown + const referencedPaths = new Set(); + const imgRefRegex = /!\[[^\]]*\]\(([^)]+)\)/g; + let refMatch; + while ((refMatch = imgRefRegex.exec(result)) !== null) { + referencedPaths.add(refMatch[1]); + } + + const unreferencedImages = images.filter((img) => { + const target = img.localPath || img.dataUrl || ""; + if (!target) return false; + const normalized = normalizeMarkdownPath(target); + return !referencedPaths.has(normalized) && !referencedPaths.has(target); + }); + + if (unreferencedImages.length === 0) { + pushDevLog("info", "image-insert", "所有图片已通过标签引用,无需额外插入"); + return result; + } + + pushDevLog("info", "image-insert", `还有 ${unreferencedImages.length} 张图片未被引用,尝试 LLM 匹配`, { + unreferencedCount: unreferencedImages.length, + unreferencedImages: unreferencedImages.map(img => ({ page: img.pageNumber, name: img.fileName })), + }); + + // Step 4: Try LLM-based matching for unreferenced images + if (llmContext && pageImageMap && pageImageMap.length > 0) { + try { + const llmResult = await matchImagesWithLLM(result, unreferencedImages, pageImageMap, llmContext); + if (llmResult) { + pushDevLog("info", "image-insert", `LLM 图文匹配成功`); + return llmResult; + } + } catch (error: any) { + pushDevLog("warn", "image-insert", `LLM 图文匹配失败,回退到规则匹配: ${error?.message || String(error)}`); + } + } + + // Step 5: Fallback to rule-based insertion + return ensureImageLineBreaks(insertImagesByPageContextFallback(result, unreferencedImages)); +}; + +/** + * Ensure all image references in markdown are on their own lines. + * Fixes cases where images are embedded inline within text. + */ +function ensureImageLineBreaks(markdown: string): string { + // Ensure ![alt](url) is preceded by \n\n and followed by \n\n + // But don't add extra newlines if already properly separated + return markdown.replace( + /([^\n])\s*(!\[[^\]]*\]\([^)]+\))\s*([^\n])/g, + "$1\n\n$2\n\n$3", + ).replace( + /([^\n])\s*(!\[[^\]]*\]\([^)]+\))\s*$/gm, + "$1\n\n$2", + ).replace( + /^(!\[[^\]]*\]\([^)]+\))\s*([^\n])/gm, + "$1\n\n$2", + ); +} + +/** + * Use LLM to match images with their corresponding text sections. + * + * The LLM receives: + * - The current markdown content (with section headings) + * - A list of images with their page numbers and file names + * - The page→image mapping with text snippets + * + * The LLM returns a JSON mapping of image file names to section headings + * where each image should be inserted. + */ +async function matchImagesWithLLM( + markdown: string, + unreferencedImages: DocumentImageAsset[], + pageImageMap: PageImageMap[], + llmContext: { + provider: string; + apiKey: string; + baseUrl: string; + model: string; + }, +): Promise { + // Build image info list for the prompt + const imageInfoList = unreferencedImages.map(img => { + const pageEntry = pageImageMap.find(p => p.pageNumber === img.pageNumber); + const snippet = pageEntry?.textSnippet || ""; + return `- 文件: ${img.fileName} (第${img.pageNumber}页) 上下文: "${snippet.substring(0, 100)}"`; + }).join("\n"); + + // Build section headings list for the prompt + const headingRegex = /^(#{1,3})\s+(.+)/gm; + const sections: string[] = []; + let headingMatch; + while ((headingMatch = headingRegex.exec(markdown)) !== null) { + sections.push(` - ${headingMatch[1]} ${headingMatch[2]}`); + } + const sectionList = sections.length > 0 ? sections.join("\n") : " (无标题)"; + + // Build page-image mapping for context + const pageMapStr = pageImageMap + .filter(p => p.imageFileNames.length > 0) + .map(p => `第${p.pageNumber}页: ${p.imageFileNames.join(", ")} | 文本摘要: "${p.textSnippet.substring(0, 80)}"`) + .join("\n"); + + const prompt = `你是一个文档图文匹配助手。你需要根据图片所在页码和该页的文本上下文,将图片插入到文档中最合适的章节位置。 + +## 文档章节结构 +${sectionList} + +## 待插入图片信息 +${imageInfoList} + +## 页码-图片-文本映射 +${pageMapStr} + +## 任务要求 +1. 根据图片所在页码的文本上下文,判断该图片最可能对应文档中的哪个章节 +2. 同一页的多张图片应插入到同一个章节 +3. 图片应插入到相关文字说明之后,而不是章节开头 +4. 返回JSON格式:{ "placements": [{ "fileName": "xxx.jpg", "afterText": "该图片应插入在此文本之后(取原文中一段独特的文字)" }] } +5. afterText 必须是原文中实际存在的一段文字(10-50字),用于定位插入点 +6. 只返回JSON,不要返回其他内容`; + + const response = await APIService.callAIWithProvider( + llmContext.provider, + "你是一个文档图文匹配助手,只返回JSON格式的匹配结果。", + prompt, + { + apiKey: llmContext.apiKey, + baseUrl: llmContext.baseUrl, + model: llmContext.model, + }, + ); + + // Parse the LLM response + const cleaned = stripMarkdownFence(response).trim(); + let placements: Array<{ fileName: string; afterText: string }>; + + try { + const parsed = JSON.parse(cleaned); + placements = parsed.placements || []; + } catch { + // Try to extract JSON from the response + const jsonMatch = cleaned.match(/\{[\s\S]*"placements"[\s\S]*\}/); + if (!jsonMatch) { + pushDevLog("warn", "image-insert", "LLM 返回的JSON无法解析"); + return null; + } + try { + const parsed = JSON.parse(jsonMatch[0]); + placements = parsed.placements || []; + } catch { + pushDevLog("warn", "image-insert", "LLM 返回的JSON格式错误"); + return null; + } + } + + if (placements.length === 0) { + pushDevLog("warn", "image-insert", "LLM 未返回任何图片匹配结果"); + return null; + } + + // Apply placements + let result = markdown; + let insertedCount = 0; + + for (const placement of placements) { + const image = unreferencedImages.find(img => img.fileName === placement.fileName); + if (!image) continue; + + const imageTarget = image.localPath || image.dataUrl || ""; + if (!imageTarget) continue; + + const imageMarkdown = `\n\n![第 ${image.pageNumber} 页图片](${normalizeMarkdownPath(imageTarget)})\n\n`; + const afterText = placement.afterText.trim(); + + if (afterText && result.includes(afterText)) { + // Insert after the specified text + const idx = result.indexOf(afterText); + const insertPos = idx + afterText.length; + result = result.substring(0, insertPos) + imageMarkdown + result.substring(insertPos); + insertedCount++; + } else { + // Fallback: try to find a partial match + const partialText = afterText.substring(0, Math.min(20, afterText.length)); + if (partialText && result.includes(partialText)) { + const idx = result.indexOf(partialText); + const insertPos = idx + partialText.length; + result = result.substring(0, insertPos) + imageMarkdown + result.substring(insertPos); + insertedCount++; + } + } + } + + pushDevLog("info", "image-insert", `LLM 图文匹配: 请求 ${placements.length} 处,成功插入 ${insertedCount} 处`, { + requestedPlacements: placements.length, + insertedCount, + }); + + return insertedCount > 0 ? ensureImageLineBreaks(result) : null; +} + +/** + * Fallback: Insert images by page context (rule-based, no LLM). + * Used when LLM matching is unavailable or fails. + */ +function insertImagesByPageContextFallback( + markdown: string, + unreferencedImages: DocumentImageAsset[], +): string { + const lines = markdown.split("\n"); + + // Build section boundaries + interface Section { + headingLine: number; + endLine: number; + level: number; + title: string; + pageHints: number[]; + } + + const sections: Section[] = []; + const headingRegex = /^(#{1,6})\s+(.+)/; + + for (let i = 0; i < lines.length; i++) { + const match = lines[i].match(headingRegex); + if (match) { + sections.push({ + headingLine: i, + endLine: lines.length, + level: match[1].length, + title: match[2], + pageHints: [], + }); + } + } + + for (let i = 0; i < sections.length; i++) { + sections[i].endLine = i + 1 < sections.length ? sections[i + 1].headingLine : lines.length; + } + + // Scan each section for page number references + for (const section of sections) { + const sectionText = lines.slice(section.headingLine, section.endLine).join(" "); + const pageRefRegex = /第\s*(\d+)\s*页/g; + let pageRefMatch; + while ((pageRefMatch = pageRefRegex.exec(sectionText)) !== null) { + section.pageHints.push(parseInt(pageRefMatch[1], 10)); + } + } + + const insertions: { lineIndex: number; imageMarkdown: string; pageNum: number }[] = []; + + for (const image of unreferencedImages) { + const imageTarget = image.localPath || image.dataUrl || ""; + if (!imageTarget) continue; + + const imageMarkdown = `\n\n![第 ${image.pageNumber} 页图片](${normalizeMarkdownPath(imageTarget)})\n\n`; + const pageNum = image.pageNumber; + + let insertAt = -1; + + // Strategy 1: Find a section that explicitly mentions this page number + for (const section of sections) { + if (section.pageHints.includes(pageNum)) { + insertAt = section.endLine; + break; + } + } + + // Strategy 2: Find the section whose page range covers this image's page + if (insertAt === -1 && sections.length > 0) { + const allPageHints = sections.flatMap(s => s.pageHints.map(p => ({ page: p, sectionIdx: sections.indexOf(s) }))); + allPageHints.sort((a, b) => a.page - b.page); + + for (const hint of allPageHints) { + if (hint.page <= pageNum) { + insertAt = sections[hint.sectionIdx].endLine; + } else { + break; + } + } + } + + // Strategy 3: Distribute proportionally + if (insertAt === -1 && sections.length > 0) { + const maxPage = Math.max(...unreferencedImages.map(img => img.pageNumber), 1); + const targetSectionIdx = Math.min( + Math.floor((pageNum / maxPage) * sections.length), + sections.length - 1, + ); + insertAt = sections[targetSectionIdx].endLine; + } + + if (insertAt === -1) { + insertAt = lines.length; + } + + insertions.push({ lineIndex: insertAt, imageMarkdown, pageNum }); + } + + insertions.sort((a, b) => b.lineIndex - a.lineIndex); + + for (const { lineIndex, imageMarkdown } of insertions) { + lines.splice(lineIndex, 0, imageMarkdown); + } + + pushDevLog("info", "image-insert", `规则匹配插入完成: ${insertions.length} 张图片`, { + insertedCount: insertions.length, + }); + + return lines.join("\n"); +} + +const inferChunkFocus = (text: string): string => { + if (/例题|例 题|习题/.test(text)) { + return "例题与解题步骤"; + } + + if (/定义|概念|原理|定理|公式/.test(text)) { + return "核心概念与公式"; + } + + if (/图|表|曲线|流程/.test(text)) { + return "图表与结构说明"; + } + + return "知识要点梳理"; +}; diff --git a/src/utils/documentProcessor.ts b/src/utils/documentProcessor.ts index b3d852fa..acfb5f7d 100644 --- a/src/utils/documentProcessor.ts +++ b/src/utils/documentProcessor.ts @@ -1,81 +1,110 @@ -import * as pdfjsLib from "pdfjs-dist"; -import mammoth from "mammoth"; -import JSZip from "jszip"; -import type { SupportedFileType } from "@/types"; +import * as pdfjsLib from "pdfjs-dist/legacy/build/pdf"; +import pdfWorker from "pdfjs-dist/legacy/build/pdf.worker.min.mjs?url"; +import { appDataDir } from "@tauri-apps/api/path"; +import { mkdir, writeFile } from "@tauri-apps/plugin-fs"; +import { extractPdfImages } from "@/services/pdfImageExtractor"; +import { useAppStore } from "@/store/useAppStore"; +import type { + DocumentAnalysisBundle, + DocumentImageAsset, + PageImageMap, + SupportedFileType, +} from "@/types"; + +const pushDevLog = ( + level: "info" | "warn" | "error", + scope: string, + message: string, + payload?: unknown, +) => { + const { addLog } = useAppStore.getState(); + if (addLog) { + addLog({ + id: `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, + timestamp: Date.now(), + level, + scope, + message, + payload, + }); + } +}; -// 设置 PDF.js worker - 使用 CDN 以确保稳定性 if (typeof window !== "undefined") { - // 使用与安装版本 4.10.38 匹配的 worker - pdfjsLib.GlobalWorkerOptions.workerSrc = `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.10.38/pdf.worker.min.mjs`; + pdfjsLib.GlobalWorkerOptions.workerSrc = pdfWorker; } export class DocumentProcessor { - /** - * 提取文件内容 - */ - static async extractContent(file: File): Promise { - const fileType = file.type as SupportedFileType; - - switch (fileType) { - case "text/plain": - return await this.extractTextFile(file); - - case "application/pdf": - return await this.extractPDFText(file); - - case "application/msword": - case "application/vnd.openxmlformats-officedocument.wordprocessingml.document": - return await this.extractWordText(file); - - case "application/vnd.ms-powerpoint": - case "application/vnd.openxmlformats-officedocument.presentationml.presentation": - return await this.extractPowerPointText(file); - - default: - throw new Error(`不支持的文件格式: ${fileType}`); + private static async loadPdfDocument(arrayBuffer: ArrayBuffer) { + const data = new Uint8Array(arrayBuffer); + try { + return await pdfjsLib.getDocument({ + data, + useSystemFonts: true, + disableAutoFetch: true, + }).promise; + } catch (error) { + console.warn("PDF 解析失败,改用主线程模式", error); + try { + return await pdfjsLib.getDocument({ + data, + disableWorker: true, + useSystemFonts: true, + disableAutoFetch: true, + }).promise; + } catch (fallbackError) { + console.warn("主线程解析失败,改用字节数组模式", fallbackError); + return await pdfjsLib.getDocument({ + data, + disableWorker: true, + isEvalSupported: false, + useSystemFonts: true, + }).promise; + } } } /** - * 提取纯文本文件 + * Extract a complete analysis bundle from a PDF file. + * + * Pipeline: + * 1. Extract text using pdfjs + * 2. Save PDF to data directory + * 3. Call Rust extract_pdf_images to extract embedded images (lopdf + easyyun API fallback) + * 4. Return combined bundle */ - private static async extractTextFile(file: File): Promise { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - - reader.onload = (e) => { - const content = e.target?.result as string; - if (!content || content.trim().length === 0) { - reject(new Error("文本文件内容为空")); - } else { - resolve(content); - } - }; + static async extractAnalysisBundle( + file: File, + outputRoot?: string, + ): Promise { + const fileType = file.type as SupportedFileType; - reader.onerror = () => reject(new Error("文件读取失败")); - reader.readAsText(file, "UTF-8"); - }); - } + if (fileType !== "application/pdf") { + throw new Error(`不支持的文件格式: ${fileType},仅支持 PDF 文件`); + } - /** - * 提取 PDF 文本 - */ - private static async extractPDFText(file: File): Promise { try { + // Step 1: Read the file into an ArrayBuffer. + // IMPORTANT: pdfjsLib.getDocument() transfers ownership of the underlying + // ArrayBuffer to the Web Worker, which detaches it. We must create a copy + // for the file-saving step BEFORE passing it to pdfjs. const arrayBuffer = await file.arrayBuffer(); - const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise; + const arrayBufferForSave = arrayBuffer.slice(0); // copy before pdfjs consumes it + + // Step 2: Extract text using pdfjs + const pdf = await this.loadPdfDocument(arrayBuffer); + const pageTexts: string[] = []; let fullText = ""; - // 遍历所有页面 for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) { const page = await pdf.getPage(pageNum); const textContent = await page.getTextContent(); - - // 提取文本内容 const pageText = textContent.items .map((item: any) => item.str) - .join(" "); + .join(" ") + .trim(); + pageTexts.push(pageText); fullText += `\n=== 第 ${pageNum} 页 ===\n${pageText}\n`; } @@ -83,125 +112,169 @@ export class DocumentProcessor { throw new Error("PDF文件中未检测到文本内容,可能是图片扫描版PDF"); } - return fullText.trim(); + // Step 3: Save PDF to data directory and extract images via Rust + let imageAssets: DocumentImageAsset[] = []; + const baseDir = outputRoot + ? this.normalizePath(outputRoot) + : this.normalizePath(await appDataDir()); + + // Use a short hash of the file content to avoid folder name collisions + const hashDir = await this.computeShortHash(arrayBufferForSave); + const imageDir = `${baseDir}/${hashDir}_pdf_assets`; + + try { + await mkdir(imageDir, { recursive: true }); + + // Save PDF file to data directory for Rust processing + // Use the pre-copied buffer since the original may have been detached by pdfjs + const pdfPath = `${baseDir}/${hashDir}_input.pdf`; + await writeFile(pdfPath, new Uint8Array(arrayBufferForSave)); + + pushDevLog("info", "document-processor", "PDF 已保存到数据目录,准备提取图片", { + pdfPath, + imageDir, + hashDir, + fileSize: arrayBufferForSave.byteLength, + }); + + // Step 3: Call Rust to extract images (lopdf) + imageAssets = await extractPdfImages(pdfPath, imageDir, hashDir); + console.log(`[DocumentProcessor] 图片提取完成: ${imageAssets.length} 张图片`, + imageAssets.map(img => ({ page: img.pageNumber, name: img.fileName, path: img.localPath }))); + + pushDevLog("info", "document-processor", `图片提取完成: ${imageAssets.length} 张`, { + imageCount: imageAssets.length, + hashDir, + images: imageAssets.map(img => ({ page: img.pageNumber, name: img.fileName, path: img.localPath })), + }); + } catch (imageError: any) { + console.warn("PDF 图片提取失败,继续无图片分析:", imageError); + pushDevLog("warn", "document-processor", "PDF 图片提取失败,继续无图片分析", { + error: imageError?.message || String(imageError), + }); + // Images are optional - continue without them + } + + // Note: No page-render fallback — we only want actual embedded images, + // not full-page screenshots. The Rust backend (lopdf) handles extraction. + // Build pageImageMap: for each page, record which images and a text snippet + // Also generate unique insertion tags (e.g. [[IMG_P3_001]]) for each image + const pageImageMap: PageImageMap[] = []; + for (let i = 0; i < pageTexts.length; i++) { + const pageNum = i + 1; + const pageImages = imageAssets.filter(img => img.pageNumber === pageNum); + const textSnippet = (pageTexts[i] || "").substring(0, 300).trim(); + const imageTags = pageImages.map((_, imgIdx) => + `[[IMG_P${pageNum}_${String(imgIdx + 1).padStart(3, "0")}]]` + ); + if (pageImages.length > 0 || textSnippet) { + pageImageMap.push({ + pageNumber: pageNum, + textSnippet, + imageFileNames: pageImages.map(img => img.fileName), + imageTags, + }); + } + } + + return { + text: fullText.trim(), + pageTexts, + images: imageAssets, + pageImageMap, + pageCount: pdf.numPages, + hashDir, + }; } catch (error: any) { console.error("PDF解析错误:", error); - if (error.message?.includes("图片扫描版")) { + const message = error?.message || String(error) || "未知错误"; + if (message.includes("图片扫描版")) { throw error; } - throw new Error("PDF文件解析失败,请确认文件格式正确或尝试转换为TXT格式"); + throw new Error( + `PDF文件解析失败:${message},请确认文件格式正确`, + ); } } - /** - * 提取 Word 文档文本 - */ - private static async extractWordText(file: File): Promise { - try { - const arrayBuffer = await file.arrayBuffer(); - const result = await mammoth.extractRawText({ arrayBuffer }); - - if (!result.value || result.value.trim().length === 0) { - throw new Error("Word文档中未检测到文本内容"); - } - - // 记录警告信息 - if (result.messages && result.messages.length > 0) { - console.warn("Word文档解析警告:", result.messages); - } + static async extractContent(file: File): Promise { + const fileType = file.type as SupportedFileType; - return result.value.trim(); - } catch (error: any) { - console.error("Word文档解析错误:", error); - throw new Error(`Word文档解析失败:${error.message}`); + if (fileType !== "application/pdf") { + throw new Error(`不支持的文件格式: ${fileType},仅支持 PDF 文件`); } + + return await this.extractPDFText(file); } - /** - * 提取 PowerPoint 文本 - */ - private static async extractPowerPointText(file: File): Promise { + private static async extractPDFText(file: File): Promise { try { const arrayBuffer = await file.arrayBuffer(); - const zip = await JSZip.loadAsync(arrayBuffer); + const pdf = await this.loadPdfDocument(arrayBuffer); let fullText = ""; - // PPTX 文件中的幻灯片存储在 ppt/slides/ 目录下 - const slideFiles = Object.keys(zip.files).filter( - (filename) => - filename.startsWith("ppt/slides/slide") && filename.endsWith(".xml"), - ); - - // 按幻灯片编号排序 - slideFiles.sort((a, b) => { - const numA = parseInt(a.match(/slide(\d+)\.xml/)?.[1] || "0"); - const numB = parseInt(b.match(/slide(\d+)\.xml/)?.[1] || "0"); - return numA - numB; - }); - - // 提取每个幻灯片的文本 - for (const filename of slideFiles) { - const content = await zip.files[filename].async("string"); - - // 从 XML 中提取文本内容(a:t 标签内的文本) - const textMatches = content.match(/]*>([^<]+)<\/a:t>/g); + for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) { + const page = await pdf.getPage(pageNum); + const textContent = await page.getTextContent(); - if (textMatches) { - const slideNumber = filename.match(/slide(\d+)\.xml/)?.[1]; - fullText += `\n\n--- 幻灯片 ${slideNumber} ---\n`; + const pageText = textContent.items + .map((item: any) => item.str) + .join(" "); - textMatches.forEach((match) => { - const text = match.replace(/]*>([^<]+)<\/a:t>/, "$1"); - fullText += text + "\n"; - }); - } + fullText += `\n=== 第 ${pageNum} 页 ===\n${pageText}\n`; } - if (!fullText || fullText.trim().length === 0) { - throw new Error("PowerPoint文档中未检测到文本内容"); + if (fullText.trim().length === 0) { + throw new Error("PDF文件中未检测到文本内容,可能是图片扫描版PDF"); } - console.log(`成功解析 ${slideFiles.length} 张幻灯片`); return fullText.trim(); } catch (error: any) { - console.error("PowerPoint文档解析错误:", error); - if (error.message?.includes("处理库未加载")) { + console.error("PDF解析错误:", error); + if (error.message?.includes("图片扫描版")) { throw error; } throw new Error( - "PowerPoint文档解析失败,请确认文件格式正确或尝试转换为TXT格式", + `PDF文件解析失败:${error?.message || "未知错误"},请确认文件格式正确`, ); } } /** - * 验证文件类型 + * Compute a short hash from an ArrayBuffer + timestamp using SubtleCrypto. + * Each analysis gets a unique hash (even for the same file) by including + * a timestamp, ensuring a fresh directory every time. */ + private static async computeShortHash(buffer: ArrayBuffer): Promise { + // Include timestamp so same file analyzed multiple times gets different hashes + const timestamp = Date.now().toString(36); + const timestampBytes = new TextEncoder().encode(timestamp); + const combined = new Uint8Array(buffer.byteLength + timestampBytes.length); + combined.set(new Uint8Array(buffer), 0); + combined.set(timestampBytes, buffer.byteLength); + const hashBuffer = await crypto.subtle.digest("SHA-256", combined); + const hashArray = new Uint8Array(hashBuffer); + // Take first 6 bytes (12 hex chars) — sufficient for uniqueness + const hex = Array.from(hashArray.slice(0, 6)) + .map((b) => b.toString(16).padStart(2, "0")) + .join(""); + return hex; + } + + private static normalizePath(pathValue: string): string { + return pathValue.replace(/[\\/]+$/, "").replace(/\\/g, "/"); + } + static isValidFileType(type: string): type is SupportedFileType { const supportedTypes: SupportedFileType[] = [ "application/pdf", - "application/msword", - "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "application/vnd.ms-powerpoint", - "application/vnd.openxmlformats-officedocument.presentationml.presentation", - "text/plain", ]; return supportedTypes.includes(type as SupportedFileType); } - /** - * 获取文件类型友好提示 - */ static getFileTypeHint(type: SupportedFileType): string { const hints: Record = { "application/pdf": "已选择PDF文件,正在准备解析...", - "application/msword": "已选择Word文档,正在准备解析...", - "application/vnd.openxmlformats-officedocument.wordprocessingml.document": - "已选择Word文档,正在准备解析...", - "application/vnd.ms-powerpoint": "已选择PowerPoint文档,正在准备解析...", - "application/vnd.openxmlformats-officedocument.presentationml.presentation": - "已选择PowerPoint文档,正在准备解析...", - "text/plain": "已选择TXT文件,解析速度最快", }; return hints[type] || "已选择文件"; } diff --git a/src/utils/markdownRenderer.ts b/src/utils/markdownRenderer.ts index 02fdca91..517398cc 100644 --- a/src/utils/markdownRenderer.ts +++ b/src/utils/markdownRenderer.ts @@ -1,52 +1,110 @@ import { marked } from "marked"; import katex from "katex"; +import { convertFileSrc } from "@tauri-apps/api/core"; +import { useAppStore } from "@/store/useAppStore"; + +const pushDevLog = ( + level: "info" | "warn" | "error", + scope: string, + message: string, + payload?: unknown, +) => { + const { addLog } = useAppStore.getState(); + if (addLog) { + addLog({ + id: `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, + timestamp: Date.now(), + level, + scope, + message, + payload, + }); + } +}; -// 配置 marked marked.setOptions({ breaks: true, gfm: true, }); export class MarkdownRenderer { - /** - * 渲染 Markdown 内容(包含 LaTeX 数学公式支持) - */ static render(content: string): string { if (!content) return ""; try { - // 存储数学公式 const mathStore: Map = new Map(); let counter = 0; - // 第一步:提取块级公式 $$...$$ - content = content.replace(/\$\$([^$]+?)\$\$/gs, (_match, latex) => { - const id = `KATEXBLOCK${counter}KATEX`; - mathStore.set(id, { latex: latex.trim(), isBlock: true }); + const storeMath = (latex: string, isBlock: boolean) => { + const id = `${isBlock ? "KATEXBLOCK" : "KATEXINLINE"}${counter}KATEX`; + mathStore.set(id, { latex: latex.trim(), isBlock }); counter++; - return `\n\n${id}\n\n`; + return isBlock ? `\n\n${id}\n\n` : id; + }; + + content = content.replace(/\$\$([^$]+?)\$\$/gs, (_match, latex) => { + return storeMath(latex, true); }); - // 第二步:提取行内公式 $...$ content = content.replace( - /(? { - const id = `KATEXINLINE${counter}KATEX`; - mathStore.set(id, { latex: latex.trim(), isBlock: false }); - counter++; - return id; + const normalized = latex.trim(); + const isBlock = + /\\begin|\\end|\\\\|\n|\\cases|\\array|\\align/i.test( + normalized, + ); + return storeMath(normalized, isBlock); + }, + ); + + // Extract local image paths before marked.parse to prevent URL encoding issues. + // marked may mangle Windows paths (C:/...) or other local paths containing colons. + const imageStore: Map = new Map(); + let imageCounter = 0; + content = content.replace( + /!\[([^\]]*)\]\(([^)]+)\)/g, + (_match, alt, url) => { + const decodedUrl = url.trim(); + if (this.isLocalImageSource(decodedUrl)) { + const placeholder = `LOCALIMG${imageCounter}LOCALIMG`; + imageStore.set(placeholder, decodedUrl); + imageCounter++; + return `![${alt}](${placeholder})`; + } + return _match; }, ); console.log("提取公式数量:", mathStore.size); + if (imageStore.size > 0) { + console.log(`提取本地图片路径: ${imageStore.size} 张`); + } - // 第三步:渲染 Markdown let html = marked.parse(content) as string; + // Restore local image paths and convert to Tauri asset URLs + imageStore.forEach((originalPath, placeholder) => { + const normalized = originalPath.replace(/\\/g, "/"); + const converted = convertFileSrc(normalized); + // marked may have HTML-encoded the placeholder, so try both raw and encoded forms + html = html.replace(new RegExp(placeholder, "g"), converted); + // Also handle cases where marked encodes the placeholder in the src attribute + const encodedPlaceholder = placeholder + .replace(/LOCALIMG/g, "LOCALIMG"); + html = html.replace(new RegExp(encodedPlaceholder, "g"), converted); + }); + + if (imageStore.size > 0) { + pushDevLog("info", "markdown-render", `预提取并转换了 ${imageStore.size} 张本地图片路径`, { + count: imageStore.size, + paths: Array.from(imageStore.values()), + }); + } + console.log("Markdown 渲染完成,开始替换公式"); - // 第四步:替换所有占位符 mathStore.forEach(({ latex, isBlock }, id) => { try { const rendered = katex.renderToString(latex, { @@ -55,12 +113,8 @@ export class MarkdownRenderer { output: "html", }); - // 使用多种模式查找并替换 - // 1. 直接替换 html = html.replace(new RegExp(id, "g"), rendered); - // 2. 在

标签中 html = html.replace(new RegExp(`

${id}

`, "g"), rendered); - // 3. 在

标签中带空格 html = html.replace( new RegExp(`

\\s*${id}\\s*

`, "g"), rendered, @@ -74,7 +128,20 @@ export class MarkdownRenderer { } }); - // 第五步:清理可能残留的占位符 + html = this.rewriteLocalImageSources(html); + + // Add lazy loading to all images to prevent UI freeze when many images load at once + html = html.replace( + /]*?)>/g, + (match, attrs) => { + if (/loading\s*=/.test(attrs)) return match; // already has loading attr + return ``; + }, + ); + + // Add target="_blank" to external links so they open in default browser + html = this.addTargetBlankToLinks(html); + html = html.replace(/KATEX(BLOCK|INLINE)\d+KATEX/g, (match) => { console.warn("发现未替换的占位符:", match); return "[公式]"; @@ -87,9 +154,6 @@ export class MarkdownRenderer { } } - /** - * 提取纯文本(移除 HTML 和公式) - */ static extractPlainText(markdown: string): string { let text = markdown .replace(/\$\$.*?\$\$/g, "[公式]") @@ -106,15 +170,83 @@ export class MarkdownRenderer { return text.trim(); } - /** - * 将Markdown中的标题降级(一级降为二级,二级降为三级,以此类推) - * @param markdown 原始Markdown内容 - * @returns 降级后的Markdown内容 - */ static downgradeHeadings(markdown: string): string { return markdown.replace(/^(#{1,6})\s+(.+)$/gm, (_, hashes, text) => { - // 在每个#前再加一个#,实现降级 return `${hashes}# ${text}`; }); } + + private static decodeHtmlEntities(str: string): string { + return str + .replace(/:/g, ":") + .replace(///g, "/") + .replace(/\/g, "/") + .replace(/:/gi, ":") + .replace(///gi, "/") + .replace(/\/gi, "/") + .replace(/&/g, "&") + .replace(/</g, "<") + .replace(/>/g, ">"); + } + + private static rewriteLocalImageSources(html: string): string { + let convertedCount = 0; + const convertedImages: Array<{ original: string; converted: string }> = []; + + const result = html.replace( + /]*?)src=("|')(.*?)(\2)([^>]*?)>/g, + (_match, before, quote, src, _closingQuote, after) => { + const decodedSrc = this.decodeHtmlEntities(src); + if (!this.isLocalImageSource(decodedSrc)) { + return ``; + } + + const normalized = decodedSrc.replace(/\\/g, "/"); + const converted = convertFileSrc(normalized); + convertedCount++; + convertedImages.push({ original: decodedSrc, converted }); + return ``; + }, + ); + + if (convertedCount > 0) { + pushDevLog("info", "markdown-render", `渲染时转换了 ${convertedCount} 张本地图片路径`, { + convertedCount, + images: convertedImages, + }); + } + + return result; + } + + private static isLocalImageSource(src: string): boolean { + // Match Windows absolute paths (C:/, D:\, etc.) + // Match Unix absolute paths (/home/, /tmp/, /Users/, etc.) — but not protocol-relative URLs (//cdn.example.com) + // Match file:// protocol URLs + // Also match paths that may have been partially HTML-encoded + return /^(?:[a-zA-Z]:[\\/])/.test(src) || + /^\/(?!\/)[^\s]/.test(src) || + /^file:\/\//.test(src) || + /^(?:[a-zA-Z]:/)/.test(src) || + /^/(?!/)/.test(src) || + /^file:///.test(src); + } + + /** Add target="_blank" rel="noopener noreferrer" to all tags */ + private static addTargetBlankToLinks(html: string): string { + return html.replace( + /]*?)href=("|')(.*?)\2([^>]*)>/g, + (_match, before, _quote, href, after) => { + // Skip anchor links + if (href.startsWith("#") || href.startsWith("javascript:")) { + return _match; + } + // Avoid duplicate target attributes + if (/target\s*=/i.test(before + after)) { + return _match; + } + return ``; + }, + ); + } } diff --git a/vite.config.ts b/vite.config.ts index 28b7f057..5e72f5c4 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,6 +2,19 @@ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; import { resolve } from "path"; +const buildTargetEnv = process.env.VITE_BUILD_TARGET?.trim(); +const buildTarget = (() => { + if (!buildTargetEnv) { + return ["es2021", "chrome100", "safari13"]; + } + + if (!buildTargetEnv.includes(",")) { + return buildTargetEnv; + } + + return buildTargetEnv.split(",").map((item) => item.trim()).filter(Boolean); +})(); + export default defineConfig({ plugins: [react()], resolve: { @@ -14,7 +27,6 @@ export default defineConfig({ port: 1420, strictPort: true, fs: { - // 允许访问 node_modules 中的 worker 文件 allow: [".."], }, }, @@ -23,7 +35,7 @@ export default defineConfig({ exclude: ["pdfjs-dist"], }, build: { - target: ["es2021", "chrome100", "safari13"], + target: buildTarget, minify: !process.env.TAURI_DEBUG ? "esbuild" : false, sourcemap: !!process.env.TAURI_DEBUG, outDir: "dist",