diff --git a/.gitignore b/.gitignore index 3fb19310e1..debe42665a 100644 --- a/.gitignore +++ b/.gitignore @@ -46,30 +46,8 @@ config/lpmm_config.toml.bak template/compare/bot_config_template.toml template/compare/model_config_template.toml CLAUDE.md +MaiBot-Dashboard/ cloudflare-workers/ -log_viewer/ -dev/ -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* -node_modules/ -dist/ -dist-ssr/ -*.local - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? result.json # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/dashboard/.gitignore b/dashboard/.gitignore deleted file mode 100644 index 8b13789179..0000000000 --- a/dashboard/.gitignore +++ /dev/null @@ -1 +0,0 @@ - diff --git a/dashboard/.prettierrc b/dashboard/.prettierrc deleted file mode 100644 index e999e95bc0..0000000000 --- a/dashboard/.prettierrc +++ /dev/null @@ -1,8 +0,0 @@ -{ - "semi": false, - "singleQuote": true, - "tabWidth": 2, - "trailingComma": "es5", - "printWidth": 100, - "plugins": ["prettier-plugin-tailwindcss"] -} diff --git a/dashboard/LICENSE b/dashboard/LICENSE deleted file mode 100644 index 0ad25db4bd..0000000000 --- a/dashboard/LICENSE +++ /dev/null @@ -1,661 +0,0 @@ - GNU AFFERO GENERAL PUBLIC LICENSE - Version 3, 19 November 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU Affero General Public License is a free, copyleft license for -software and other kinds of works, specifically designed to ensure -cooperation with the community in the case of network server software. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -our General Public Licenses are intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - Developers that use our General Public Licenses protect your rights -with two steps: (1) assert copyright on the software, and (2) offer -you this License which gives you legal permission to copy, distribute -and/or modify the software. - - A secondary benefit of defending all users' freedom is that -improvements made in alternate versions of the program, if they -receive widespread use, become available for other developers to -incorporate. Many developers of free software are heartened and -encouraged by the resulting cooperation. However, in the case of -software used on network servers, this result may fail to come about. -The GNU General Public License permits making a modified version and -letting the public access it on a server without ever releasing its -source code to the public. - - The GNU Affero General Public License is designed specifically to -ensure that, in such cases, the modified source code becomes available -to the community. It requires the operator of a network server to -provide the source code of the modified version running there to the -users of that server. Therefore, public use of a modified version, on -a publicly accessible server, gives the public access to the source -code of the modified version. - - An older license, called the Affero General Public License and -published by Affero, was designed to accomplish similar goals. This is -a different license, not a version of the Affero GPL, but Affero has -released a new version of the Affero GPL which permits relicensing under -this license. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU Affero General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Remote Network Interaction; Use with the GNU General Public License. - - Notwithstanding any other provision of this License, if you modify the -Program, your modified version must prominently offer all users -interacting with it remotely through a computer network (if your version -supports such interaction) an opportunity to receive the Corresponding -Source of your version by providing access to the Corresponding Source -from a network server at no charge, through some standard or customary -means of facilitating copying of software. This Corresponding Source -shall include the Corresponding Source for any work covered by version 3 -of the GNU General Public License that is incorporated pursuant to the -following paragraph. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the work with which it is combined will remain governed by version -3 of the GNU General Public License. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU Affero General Public License from time to time. Such new versions -will be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU Affero General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU Affero General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU Affero General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If your software can interact with users remotely through a computer -network, you should also make sure that it provides a way for users to -get its source. For example, if your program is a web application, its -interface could display a "Source" link that leads users to an archive -of the code. There are many ways you could offer source, and different -solutions will be better for different programs; see section 13 for the -specific requirements. - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU AGPL, see -. diff --git a/dashboard/README.md b/dashboard/README.md deleted file mode 100644 index b2a260f8f2..0000000000 --- a/dashboard/README.md +++ /dev/null @@ -1,377 +0,0 @@ -# MaiBot Dashboard - -> MaiBot 的现代化 Web 管理面板 - 基于 React 19 + TypeScript + Vite 构建 - -
- -[![React](https://img.shields.io/badge/React-19.2-61DAFB?logo=react&logoColor=white)](https://react.dev/) -[![TypeScript](https://img.shields.io/badge/TypeScript-5.9-3178C6?logo=typescript&logoColor=white)](https://www.typescriptlang.org/) -[![Vite](https://img.shields.io/badge/Vite-7.2-646CFF?logo=vite&logoColor=white)](https://vitejs.dev/) -[![TailwindCSS](https://img.shields.io/badge/TailwindCSS-3.4-38B2AC?logo=tailwind-css&logoColor=white)](https://tailwindcss.com/) - -
- -## 📖 项目简介 - -MaiBot Dashboard 是 MaiBot 聊天机器人的 Web 管理界面,提供了直观的配置管理、实时监控、插件管理、资源管理等功能。通过自动解析后端配置类,动态生成表单,实现了配置的可视化编辑。 - -
- MaiBot Dashboard 界面预览 -
- -### ✨ 核心特性 - -- 🎨 **现代化 UI** - 基于 shadcn/ui 组件库,支持亮色/暗色主题切换 -- ⚡ **高性能** - 使用 Vite 7.2 构建,React 19 最新特性 -- 🔐 **安全认证** - Token 认证机制,支持自定义和自动生成 Token -- 📝 **智能配置** - 自动解析 Python dataclass,生成配置表单 -- 🎯 **类型安全** - 完整的 TypeScript 类型定义 -- 🔄 **实时更新** - WebSocket 实时日志流、配置自动保存 -- 📱 **响应式设计** - 完美适配桌面和移动设备 -- 💬 **本地对话** - 直接在 WebUI 与麦麦对话,无需外部平台 - -## 🎯 功能模块 - -### 📊 仪表盘(首页) -- **实时统计** - 总请求数、Token 消耗、费用统计、在线时长 -- **模型统计** - 各模型的使用次数、费用、平均响应时间 -- **趋势图表** - 每小时请求量、Token 消耗、费用趋势折线图 -- **模型分布** - 饼图展示模型使用占比 -- **最近活动** - 实时刷新的请求活动列表 - -### 💬 本地聊天室 -- **WebSocket 实时通信** - 与麦麦直接对话 -- **消息历史** - 自动加载 SQLite 存储的历史消息 -- **连接状态** - 实时显示 WebSocket 连接状态 -- **自定义昵称** - 可自定义用户身份 -- **移动端适配** - 完整的响应式聊天界面 - -### ⚙️ 配置管理 - -#### 麦麦主程序配置 -- **分组展示** - 配置项按功能分组(基础设置、功能开关等) -- **智能表单** - 根据配置类型自动生成对应控件 -- **自动保存** - 2秒防抖自动保存,无需手动操作 -- **一键重启** - 保存并重启麦麦,使配置生效 - -#### AI 模型厂商配置 -- **提供商管理** - 添加、编辑、删除 API 提供商 -- **模板选择** - 预设常用厂商模板(OpenAI、DeepSeek、硅基流动等) -- **连接测试** - ⚡ 测试提供商连接状态和 API Key 有效性 -- **批量操作** - 批量删除、批量测试所有提供商 -- **搜索过滤** - 按名称、URL、类型快速筛选 - -#### 模型管理与分配 -- **模型列表** - 管理可用的模型配置 -- **使用状态** - 显示模型是否被任务使用 -- **任务分配** - 为不同功能分配模型(回复、工具调用、VLM 等) -- **参数调整** - 温度、最大 Token 等参数配置 -- **新手引导** - 交互式引导教程 - -#### 适配器配置 -- **NapCat 配置** - 管理 QQ 机器人适配器 -- **Docker 支持** - 支持容器模式配置 -- **配置导入导出** - 跨环境迁移配置 - -### 📋 实时日志 -- **WebSocket 流式传输** - 实时接收后端日志 -- **虚拟滚动** - 高性能处理大量日志 -- **多级过滤** - 按日志级别(DEBUG/INFO/WARNING/ERROR)过滤 -- **模块过滤** - 按日志来源模块筛选 -- **时间范围** - 日期选择器筛选日志 -- **搜索高亮** - 关键字搜索并高亮显示 -- **字号调整** - 自定义日志显示字号和行间距 -- **日志导出** - 导出过滤后的日志 - -### 🔌 插件管理 -- **插件市场** - 浏览和搜索可用插件 -- **分类筛选** - 按类别、状态筛选插件 -- **一键安装** - 自动处理依赖并安装插件 -- **版本兼容** - 检查插件与 MaiBot 版本兼容性 -- **进度显示** - WebSocket 实时显示安装进度 -- **插件统计** - 下载量、更新时间等信息 -- **卸载更新** - 管理已安装插件 - -### 👤 人物关系管理 -- **人物列表** - 查看所有已知用户信息 -- **详情编辑** - 编辑用户昵称、备注等信息 -- **关系统计** - 查看消息数、互动频率等统计 -- **批量操作** - 批量删除用户记录 - -### 📦 资源管理 - -#### 表情包管理 -- **预览管理** - 图片/GIF 预览 -- **分类过滤** - 按注册状态、描述筛选 -- **编辑标签** - 修改表情包描述和属性 -- **批量禁用** - 启用/禁用表情包 - -#### 表达方式管理 -- **表达列表** - 查看麦麦学习的表达方式 -- **来源追踪** - 记录表达来源群组和用户 -- **编辑创建** - 手动添加或编辑表达 - -#### 知识图谱 -- **可视化展示** - ReactFlow 交互式图谱 -- **节点搜索** - 搜索实体和关系 -- **布局算法** - 自动布局优化 -- **详情查看** - 点击节点查看详细信息 - -### ⚙️ 系统设置 -- **主题切换** - 亮色/暗色/跟随系统 -- **动画控制** - 开启/关闭界面动画 -- **Token 管理** - 查看、复制、重新生成认证 Token -- **版本信息** - 查看前端和后端版本 - -## 🏗️ 技术架构 - -### 前端技术栈 - -``` -React 19.2.0 # UI 框架 -├── TypeScript 5.9 # 类型系统 -├── Vite 7.2 # 构建工具 -├── TanStack Router # 路由管理 -├── TanStack Virtual # 虚拟滚动 -├── Jotai # 状态管理 -├── Tailwind CSS 3.4 # 样式框架 -├── ReactFlow # 知识图谱可视化 -├── Recharts # 数据图表 -└── shadcn/ui # 组件库 - ├── Radix UI # 无障碍组件 - └── lucide-react # 图标库 -``` - -### 后端集成 - -``` -FastAPI # Python 后端框架 -├── WebSocket # 实时日志、聊天 -├── config_schema.py # 配置架构生成器 -├── config_routes.py # 配置管理 API -├── model_routes.py # 模型管理 API -├── chat_routes.py # 本地聊天 API -├── plugin_routes.py # 插件管理 API -├── person_routes.py # 人物管理 API -├── emoji_routes.py # 表情包管理 API -├── expression_routes.py # 表达管理 API -├── knowledge_routes.py # 知识图谱 API -├── logs_routes.py # 日志 API -└── tomlkit # TOML 文件处理 -``` - -## 📁 项目结构 - -``` -MaiBot-Dashboard/ -├── src/ -│ ├── components/ # 组件目录 -│ │ ├── ui/ # shadcn/ui 组件 -│ │ ├── layout.tsx # 布局组件(侧边栏+导航) -│ │ ├── tour/ # 新手引导组件 -│ │ ├── plugin-stats.tsx # 插件统计组件 -│ │ ├── RestartingOverlay.tsx # 重启遮罩 -│ │ └── use-theme.tsx # 主题管理 -│ ├── routes/ # 路由页面 -│ │ ├── index.tsx # 仪表盘首页 -│ │ ├── auth.tsx # 登录页 -│ │ ├── chat.tsx # 本地聊天室 -│ │ ├── logs.tsx # 日志查看 -│ │ ├── plugins.tsx # 插件管理 -│ │ ├── person.tsx # 人物管理 -│ │ ├── settings.tsx # 系统设置 -│ │ ├── config/ # 配置管理页面 -│ │ │ ├── bot.tsx # 麦麦主程序配置 -│ │ │ ├── modelProvider.tsx # 模型提供商 -│ │ │ ├── model.tsx # 模型管理 -│ │ │ └── adapter.tsx # 适配器配置 -│ │ └── resource/ # 资源管理页面 -│ │ ├── emoji.tsx # 表情包管理 -│ │ ├── expression.tsx # 表达方式管理 -│ │ └── knowledge-graph.tsx # 知识图谱 -│ ├── lib/ # 工具库 -│ │ ├── config-api.ts # 配置 API 客户端 -│ │ ├── plugin-api.ts # 插件 API 客户端 -│ │ ├── person-api.ts # 人物 API 客户端 -│ │ ├── expression-api.ts # 表达 API 客户端 -│ │ ├── log-websocket.ts # 日志 WebSocket -│ │ ├── fetch-with-auth.ts # 认证请求封装 -│ │ └── utils.ts # 通用工具函数 -│ ├── types/ # 类型定义 -│ │ ├── config-schema.ts # 配置架构类型 -│ │ ├── plugin.ts # 插件类型 -│ │ ├── person.ts # 人物类型 -│ │ └── expression.ts # 表达类型 -│ ├── hooks/ # React Hooks -│ │ ├── use-auth.ts # 认证逻辑 -│ │ ├── use-animation.ts # 动画控制 -│ │ └── use-toast.ts # 消息提示 -│ ├── store/ # 全局状态 -│ │ └── auth.ts # 认证状态 -│ ├── router.tsx # 路由配置 -│ ├── main.tsx # 应用入口 -│ └── index.css # 全局样式 -├── public/ # 静态资源 -├── vite.config.ts # Vite 配置 -├── tailwind.config.js # Tailwind 配置 -├── tsconfig.json # TypeScript 配置 -└── package.json # 依赖管理 -``` - -## 🚀 快速开始 - -### 环境要求 - -- Node.js >= 18.0.0 -- Bun >= 1.0.0 (推荐) 或 npm/yarn/pnpm - -### 安装依赖 - -```bash -# 使用 Bun(推荐) -bun install - -# 或使用 npm -npm install -``` - -### 开发模式 - -```bash -# 启动开发服务器 (默认端口: 7999) -bun run dev - -# 或 -npm run dev -``` - -访问 http://localhost:7999 查看应用。 - -### 生产构建 - -```bash -# 构建生产版本 -bun run build - -# 预览生产构建 -bun run preview -``` - -构建产物会输出到 `dist/` 目录,由 MaiBot 后端静态服务。 - -### 代码格式化 - -```bash -# 格式化代码 -bun run format -``` - -## 🔧 开发配置 - -### Vite 代理配置 - -开发模式下,Vite 会将 API 请求代理到后端: - -```typescript -// vite.config.ts -proxy: { - '/api': { - target: 'http://127.0.0.1:8001', - changeOrigin: true, - ws: true, // WebSocket 支持 - }, -}, -``` - -### 环境变量 - -开发环境默认使用 `http://localhost:7999`,生产环境使用相对路径。 - -## 📸 界面预览 - -### 仪表盘 -实时统计、模型使用分布、趋势图表 - -### 本地聊天 -直接与麦麦对话,消息实时同步 - -### 配置管理 -分组配置项,自动生成表单,自动保存 - -### 模型提供商 -一键测试连接状态,模板快速添加 - -### 日志查看 -实时日志流,多级过滤,虚拟滚动 - -## 📦 依赖说明 - -### 核心依赖 - -| 包名 | 版本 | 用途 | -|------|------|------| -| react | ^19.2.0 | UI 框架 | -| react-dom | ^19.2.0 | React DOM 渲染 | -| typescript | ~5.9.3 | 类型系统 | -| vite | ^7.2.2 | 构建工具 | -| @tanstack/react-router | ^1.136.1 | 路由管理 | -| @tanstack/react-virtual | ^3.x | 虚拟滚动 | -| jotai | ^2.15.1 | 状态管理 | -| axios | ^1.13.2 | HTTP 客户端 | -| recharts | ^2.x | 数据图表 | -| reactflow | ^11.x | 知识图谱可视化 | -| dagre | ^0.8.x | 图布局算法 | - -### UI 组件库 - -| 包名 | 版本 | 用途 | -|------|------|------| -| @radix-ui/react-* | ^1.x | 无障碍组件基础 | -| lucide-react | ^0.553.0 | 图标库 | -| tailwindcss | ^3.4 | CSS 框架 | -| class-variance-authority | ^0.7.1 | 类名管理 | -| tailwind-merge | ^3.4.0 | Tailwind 类合并 | -| date-fns | ^3.x | 日期处理 | - - -## 🤝 贡献指南 - -1. Fork 本仓库 -2. 创建特性分支 (`git checkout -b feature/AmazingFeature`) -3. 提交更改 (`git commit -m 'Add some AmazingFeature'`) -4. 推送到分支 (`git push origin feature/AmazingFeature`) -5. 开启 Pull Request - -### 代码规范 - -- 使用 TypeScript 严格模式 -- 遵循 ESLint 规则 -- 使用 Prettier 格式化代码 -- 组件使用函数式编写 -- 优先使用 Hooks -- 响应式设计优先(移动端适配) - -## 📄 开源协议 - -本项目基于 GPLv3 协议开源,详见 [LICENSE](./LICENSE) 文件。 - -## 👥 作者 - -**MotricSeven** - [GitHub](https://github.com/DrSmoothl) - -## 🙏 致谢 - -- [React](https://react.dev/) - UI 框架 -- [shadcn/ui](https://ui.shadcn.com/) - 组件库 -- [Radix UI](https://www.radix-ui.com/) - 无障碍组件 -- [TanStack Router](https://tanstack.com/router) - 路由解决方案 -- [TanStack Virtual](https://tanstack.com/virtual) - 虚拟滚动 -- [Tailwind CSS](https://tailwindcss.com/) - CSS 框架 -- [ReactFlow](https://reactflow.dev/) - 流程图/知识图谱 -- [Recharts](https://recharts.org/) - React 图表库 - ---- - -
-Made with ❤️ by MotricSeven and Mai-with-u -
diff --git a/dashboard/components.json b/dashboard/components.json deleted file mode 100644 index 106236b138..0000000000 --- a/dashboard/components.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema.json", - "style": "new-york", - "rsc": false, - "tsx": true, - "tailwind": { - "config": "tailwind.config.js", - "css": "src/index.css", - "baseColor": "slate", - "cssVariables": true, - "prefix": "" - }, - "aliases": { - "components": "src/components", - "utils": "src/lib/utils", - "ui": "src/components/ui", - "lib": "src/lib", - "hooks": "src/hooks" - } -} diff --git a/dashboard/docs/main.png b/dashboard/docs/main.png deleted file mode 100644 index 5a0feed3f9..0000000000 Binary files a/dashboard/docs/main.png and /dev/null differ diff --git a/dashboard/eslint.config.js b/dashboard/eslint.config.js deleted file mode 100644 index 61baa88595..0000000000 --- a/dashboard/eslint.config.js +++ /dev/null @@ -1,35 +0,0 @@ -import js from '@eslint/js' -import globals from 'globals' -import reactHooks from 'eslint-plugin-react-hooks' -import reactRefresh from 'eslint-plugin-react-refresh' -import tseslint from 'typescript-eslint' - -export default tseslint.config( - { ignores: ['dist'] }, - { - extends: [js.configs.recommended, ...tseslint.configs.recommended], - files: ['**/*.{ts,tsx}'], - languageOptions: { - ecmaVersion: 2020, - globals: globals.browser, - }, - plugins: { - 'react-hooks': reactHooks, - 'react-refresh': reactRefresh, - }, - rules: { - // 将所有 React Hooks 推荐规则降级为警告 - ...Object.keys(reactHooks.configs.recommended.rules).reduce((acc, key) => { - acc[key] = 'warn' - return acc - }, {}), - 'react-refresh/only-export-components': [ - 'warn', - { allowConstantExport: true }, - ], - // 关闭或降级其他规则 - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/no-unused-vars': 'warn', - }, - }, -) diff --git a/dashboard/index.html b/dashboard/index.html deleted file mode 100644 index 9848982440..0000000000 --- a/dashboard/index.html +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - MaiBot Dashboard - - -
- - - diff --git a/dashboard/package.json b/dashboard/package.json deleted file mode 100644 index 4266f5d499..0000000000 --- a/dashboard/package.json +++ /dev/null @@ -1,95 +0,0 @@ -{ - "name": "maibot-dashboard", - "private": true, - "version": "0.11.6", - "type": "module", - "scripts": { - "dev": "vite", - "build": "tsc -b && vite build", - "lint": "eslint .", - "preview": "vite preview", - "format": "prettier --write \"src/**/*.{ts,tsx,css}\"" - }, - "dependencies": { - "@codemirror/lang-javascript": "^6.2.4", - "@codemirror/lang-json": "^6.0.2", - "@codemirror/lang-python": "^6.2.1", - "@codemirror/legacy-modes": "^6.5.2", - "@codemirror/lint": "^6.9.2", - "@codemirror/theme-one-dark": "^6.1.3", - "@dnd-kit/core": "^6.3.1", - "@dnd-kit/sortable": "^10.0.0", - "@dnd-kit/utilities": "^3.2.2", - "@radix-ui/react-accordion": "^1.2.12", - "@radix-ui/react-alert-dialog": "^1.1.15", - "@radix-ui/react-avatar": "^1.1.11", - "@radix-ui/react-checkbox": "^1.3.3", - "@radix-ui/react-collapsible": "^1.1.12", - "@radix-ui/react-context-menu": "^2.2.16", - "@radix-ui/react-dialog": "^1.1.15", - "@radix-ui/react-dropdown-menu": "^2.1.16", - "@radix-ui/react-label": "^2.1.8", - "@radix-ui/react-popover": "^1.1.15", - "@radix-ui/react-progress": "^1.1.8", - "@radix-ui/react-radio-group": "^1.3.8", - "@radix-ui/react-scroll-area": "^1.2.10", - "@radix-ui/react-select": "^2.2.6", - "@radix-ui/react-separator": "^1.1.8", - "@radix-ui/react-slider": "^1.3.6", - "@radix-ui/react-slot": "^1.2.4", - "@radix-ui/react-switch": "^1.2.6", - "@radix-ui/react-tabs": "^1.1.13", - "@radix-ui/react-toast": "^1.2.15", - "@radix-ui/react-tooltip": "^1.2.8", - "@tanstack/react-router": "^1.140.0", - "@tanstack/react-virtual": "^3.13.13", - "@tanstack/router-devtools": "^1.140.0", - "@types/dagre": "^0.7.53", - "@uiw/react-codemirror": "^4.25.3", - "@uppy/core": "^5.2.0", - "@uppy/dashboard": "^5.1.0", - "@uppy/react": "^5.1.1", - "@uppy/xhr-upload": "^5.1.1", - "axios": "^1.13.2", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "cmdk": "^1.1.1", - "dagre": "^0.8.5", - "date-fns": "^4.1.0", - "html-to-image": "^1.11.13", - "jotai": "^2.16.0", - "katex": "^0.16.27", - "lucide-react": "^0.556.0", - "react": "^19.2.1", - "react-day-picker": "^9.12.0", - "react-dom": "^19.2.1", - "react-joyride": "^2.9.3", - "react-markdown": "^10.1.0", - "reactflow": "^11.11.4", - "recharts": "3.5.1", - "rehype-katex": "^7.0.1", - "remark-gfm": "^4.0.1", - "remark-math": "^6.0.0", - "smol-toml": "^1.5.2", - "tailwind-merge": "^3.4.0" - }, - "devDependencies": { - "@eslint/js": "^9.39.1", - "@types/node": "^24.10.2", - "@types/react": "^19.2.7", - "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^5.1.2", - "autoprefixer": "^10.4.22", - "eslint": "^9.39.1", - "eslint-plugin-react-hooks": "^7.0.1", - "eslint-plugin-react-refresh": "^0.4.24", - "globals": "^16.5.0", - "postcss": "^8.5.6", - "prettier": "^3.7.4", - "prettier-plugin-tailwindcss": "^0.7.2", - "tailwindcss": "^3", - "typescript": "~5.9.3", - "typescript-eslint": "^8.49.0", - "vite": "^7.2.7" - } -} diff --git a/dashboard/postcss.config.js b/dashboard/postcss.config.js deleted file mode 100644 index 2e7af2b7f1..0000000000 --- a/dashboard/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -export default { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -} diff --git a/dashboard/public/fonts/JetBrainsMono-Medium.ttf b/dashboard/public/fonts/JetBrainsMono-Medium.ttf deleted file mode 100644 index 97671156df..0000000000 Binary files a/dashboard/public/fonts/JetBrainsMono-Medium.ttf and /dev/null differ diff --git a/dashboard/public/maimai.ico b/dashboard/public/maimai.ico deleted file mode 100644 index 3c1b131eda..0000000000 Binary files a/dashboard/public/maimai.ico and /dev/null differ diff --git a/dashboard/src/assets/maimai.ico b/dashboard/src/assets/maimai.ico deleted file mode 100644 index 578b11cdd0..0000000000 Binary files a/dashboard/src/assets/maimai.ico and /dev/null differ diff --git a/dashboard/src/assets/react.svg b/dashboard/src/assets/react.svg deleted file mode 100644 index 6c87de9bb3..0000000000 --- a/dashboard/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/dashboard/src/components/CodeEditor.tsx b/dashboard/src/components/CodeEditor.tsx deleted file mode 100644 index 52b22d536c..0000000000 --- a/dashboard/src/components/CodeEditor.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import { useEffect, useState } from 'react' -import CodeMirror from '@uiw/react-codemirror' -import { python } from '@codemirror/lang-python' -import { json, jsonParseLinter } from '@codemirror/lang-json' -import { oneDark } from '@codemirror/theme-one-dark' -import { EditorView } from '@codemirror/view' -import { StreamLanguage } from '@codemirror/language' -import { toml as tomlMode } from '@codemirror/legacy-modes/mode/toml' - -export type Language = 'python' | 'json' | 'toml' | 'text' - -interface CodeEditorProps { - value: string - onChange?: (value: string) => void - language?: Language - readOnly?: boolean - height?: string - minHeight?: string - maxHeight?: string - placeholder?: string - theme?: 'light' | 'dark' - className?: string -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const languageExtensions: Record = { - python: [python()], - json: [json(), jsonParseLinter()], - toml: [StreamLanguage.define(tomlMode)], - text: [], -} - -export function CodeEditor({ - value, - onChange, - language = 'text', - readOnly = false, - height = '400px', - minHeight, - maxHeight, - placeholder, - theme = 'dark', - className = '', -}: CodeEditorProps) { - const [mounted, setMounted] = useState(false) - - useEffect(() => { - setMounted(true) - }, []) - - if (!mounted) { - return ( -
- ) - } - - const extensions = [ - ...(languageExtensions[language] || []), - EditorView.lineWrapping, - // 应用 JetBrains Mono 字体 - EditorView.theme({ - '&': { - fontFamily: '"JetBrains Mono", "Fira Code", "Consolas", "Monaco", monospace', - }, - '.cm-content': { - fontFamily: '"JetBrains Mono", "Fira Code", "Consolas", "Monaco", monospace', - }, - '.cm-gutters': { - fontFamily: '"JetBrains Mono", "Fira Code", "Consolas", "Monaco", monospace', - }, - '.cm-scroller': { - fontFamily: '"JetBrains Mono", "Fira Code", "Consolas", "Monaco", monospace', - }, - }), - ] - - if (readOnly) { - extensions.push(EditorView.editable.of(false)) - } - - return ( -
- -
- ) -} - -export default CodeEditor diff --git a/dashboard/src/components/ListFieldEditor.tsx b/dashboard/src/components/ListFieldEditor.tsx deleted file mode 100644 index ac3373f923..0000000000 --- a/dashboard/src/components/ListFieldEditor.tsx +++ /dev/null @@ -1,525 +0,0 @@ -/** - * ListFieldEditor - 动态数组字段编辑器 - * - * 支持功能: - * - 字符串数组 (string[]) - * - 数字数组 (number[]) - * - 对象数组 (object[]) - 根据 item_fields 定义渲染 - * - 拖拽排序 - * - 动态增删项 - */ - -import { useState, useCallback, useMemo } from 'react' -import { - DndContext, - closestCenter, - KeyboardSensor, - PointerSensor, - useSensor, - useSensors, - type DragEndEvent, -} from '@dnd-kit/core' -import { - arrayMove, - SortableContext, - sortableKeyboardCoordinates, - useSortable, - verticalListSortingStrategy, -} from '@dnd-kit/sortable' -import { CSS } from '@dnd-kit/utilities' -import { Button } from '@/components/ui/button' -import { Input } from '@/components/ui/input' -import { Label } from '@/components/ui/label' -import { Card } from '@/components/ui/card' -import { Switch } from '@/components/ui/switch' -import { Slider } from '@/components/ui/slider' -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@/components/ui/select' -import { GripVertical, Plus, Trash2, AlertCircle } from 'lucide-react' -import { cn } from '@/lib/utils' - -// ============ 类型定义 ============ - -export interface ItemFieldDefinition { - /** 字段类型: "string" | "number" | "boolean" | "select" */ - type: string - label?: string - placeholder?: string - default?: unknown - /** select 类型的选项 */ - choices?: unknown[] - /** slider 类型的最小值 */ - min?: number - /** slider 类型的最大值 */ - max?: number - /** slider 类型的步进 */ - step?: number -} - -export interface ListFieldEditorProps { - /** 当前值 */ - value: unknown[] | unknown - /** 值变化回调 */ - onChange: (value: unknown[]) => void - /** 数组元素类型: "string" | "number" | "object" */ - itemType?: string - /** 当 itemType="object" 时的字段定义 */ - itemFields?: Record - /** 最小元素数量 */ - minItems?: number - /** 最大元素数量 */ - maxItems?: number - /** 是否禁用 */ - disabled?: boolean - /** 新项的占位符文字 */ - placeholder?: string -} - -// ============ 可排序项组件 ============ - -interface SortableItemProps { - id: string - index: number - itemType: string - itemFields?: Record - value: unknown - onChange: (value: unknown) => void - onRemove: () => void - disabled?: boolean - canRemove: boolean - placeholder?: string -} - -function SortableItem({ - id, - index, - itemType, - itemFields, - value, - onChange, - onRemove, - disabled, - canRemove, - placeholder, -}: SortableItemProps) { - const { - attributes, - listeners, - setNodeRef, - transform, - transition, - isDragging, - } = useSortable({ id, disabled }) - - const style = { - transform: CSS.Transform.toString(transform), - transition, - } - - return ( -
- {/* 拖拽手柄 */} - - - {/* 内容区域 */} -
- {itemType === 'object' && itemFields ? ( - } - onChange={onChange} - fields={itemFields} - disabled={disabled} - /> - ) : itemType === 'number' ? ( - onChange(parseFloat(e.target.value) || 0)} - placeholder={placeholder ?? `第 ${index + 1} 项`} - disabled={disabled} - className="font-mono" - /> - ) : ( - onChange(e.target.value)} - placeholder={placeholder ?? `第 ${index + 1} 项`} - disabled={disabled} - /> - )} -
- - {/* 删除按钮 */} - -
- ) -} - -// ============ 对象项编辑器 ============ - -interface ObjectItemEditorProps { - value: Record - onChange: (value: Record) => void - fields: Record - disabled?: boolean -} - -function ObjectItemEditor({ - value, - onChange, - fields, - disabled, -}: ObjectItemEditorProps) { - const handleFieldChange = useCallback( - (fieldName: string, fieldValue: unknown) => { - onChange({ - ...value, - [fieldName]: fieldValue, - }) - }, - [value, onChange] - ) - - const renderField = (fieldName: string, fieldDef: ItemFieldDefinition) => { - const fieldValue = value?.[fieldName] - - // boolean / switch - if (fieldDef.type === 'boolean' || fieldDef.type === 'switch') { - return ( -
- - handleFieldChange(fieldName, checked)} - disabled={disabled} - /> -
- ) - } - - // slider (number with min/max) - if (fieldDef.type === 'slider' || (fieldDef.type === 'number' && fieldDef.min != null && fieldDef.max != null)) { - const numValue = (fieldValue as number) ?? (fieldDef.default as number) ?? fieldDef.min ?? 0 - return ( -
-
- - {numValue} -
- handleFieldChange(fieldName, v[0])} - min={fieldDef.min ?? 0} - max={fieldDef.max ?? 100} - step={fieldDef.step ?? 1} - disabled={disabled} - className="py-1" - /> -
- ) - } - - // select - if (fieldDef.type === 'select' && fieldDef.choices) { - return ( -
- - -
- ) - } - - // number - if (fieldDef.type === 'number') { - return ( -
- - - handleFieldChange(fieldName, parseFloat(e.target.value) || 0) - } - placeholder={fieldDef.placeholder} - disabled={disabled} - className="h-8 text-sm" - /> -
- ) - } - - // string (default) - return ( -
- - handleFieldChange(fieldName, e.target.value)} - placeholder={fieldDef.placeholder} - disabled={disabled} - className="h-8 text-sm" - /> -
- ) - } - - return ( - - {Object.entries(fields).map(([fieldName, fieldDef]) => ( -
- {renderField(fieldName, fieldDef)} -
- ))} -
- ) -} - -// ============ 主组件 ============ - -export function ListFieldEditor({ - value, - onChange, - itemType = 'string', - itemFields, - minItems, - maxItems, - disabled, - placeholder, -}: ListFieldEditorProps) { - // 确保 value 是数组 - const items: unknown[] = useMemo(() => { - if (Array.isArray(value)) return value - if (typeof value === 'string' && value.trim()) { - // 尝试解析逗号分隔的字符串 - return value.split(',').map((s: string) => s.trim()) - } - return [] - }, [value]) - - // 为每个项生成稳定的 ID - const [itemIds] = useState(() => new Map()) - const getItemId = useCallback( - (index: number) => { - if (!itemIds.has(index)) { - itemIds.set(index, `item-${Date.now()}-${index}-${Math.random().toString(36).slice(2)}`) - } - return itemIds.get(index)! - }, - [itemIds] - ) - - // 同步 itemIds - const sortableIds = useMemo(() => { - // 清理多余的 ID - const newIds: string[] = [] - for (let i = 0; i < items.length; i++) { - newIds.push(getItemId(i)) - } - return newIds - }, [items.length, getItemId]) - - // DnD 传感器配置 - const sensors = useSensors( - useSensor(PointerSensor, { - activationConstraint: { - distance: 8, - }, - }), - useSensor(KeyboardSensor, { - coordinateGetter: sortableKeyboardCoordinates, - }) - ) - - // 拖拽结束处理 - const handleDragEnd = useCallback( - (event: DragEndEvent) => { - const { active, over } = event - if (over && active.id !== over.id) { - const oldIndex = sortableIds.indexOf(active.id as string) - const newIndex = sortableIds.indexOf(over.id as string) - const newItems = arrayMove(items, oldIndex, newIndex) - onChange(newItems) - } - }, - [items, sortableIds, onChange] - ) - - // 添加新项 - const handleAddItem = useCallback(() => { - if (maxItems != null && items.length >= maxItems) return - - let newItem: unknown - if (itemType === 'object' && itemFields) { - // 创建包含默认值的对象 - newItem = Object.fromEntries( - Object.entries(itemFields).map(([k, v]) => [k, v.default ?? '']) - ) - } else if (itemType === 'number') { - newItem = 0 - } else { - newItem = '' - } - - onChange([...items, newItem]) - }, [items, maxItems, itemType, itemFields, onChange]) - - // 修改项 - const handleItemChange = useCallback( - (index: number, newValue: unknown) => { - const newItems = [...items] - newItems[index] = newValue - onChange(newItems) - }, - [items, onChange] - ) - - // 删除项 - const handleRemoveItem = useCallback( - (index: number) => { - if (minItems != null && items.length <= minItems) return - const newItems = items.filter((_: unknown, i: number) => i !== index) - // 清理 itemIds 映射 - itemIds.delete(index) - onChange(newItems) - }, - [items, minItems, itemIds, onChange] - ) - - const canAdd = maxItems == null || items.length < maxItems - const canRemove = minItems == null || items.length > minItems - - return ( -
- {/* 列表项 */} - {items.length === 0 ? ( -
- - 暂无数据,点击下方按钮添加 -
- ) : ( - - -
- {items.map((item: unknown, index: number) => ( - handleItemChange(index, newValue)} - onRemove={() => handleRemoveItem(index)} - disabled={disabled} - canRemove={canRemove} - placeholder={placeholder} - /> - ))} -
-
-
- )} - - {/* 添加按钮 */} - - - {/* 限制提示 */} - {(minItems != null || maxItems != null) && (minItems !== null || maxItems !== null) && ( -

- {minItems != null && maxItems != null - ? `允许 ${minItems} - ${maxItems} 项` - : minItems != null - ? `至少 ${minItems} 项` - : `最多 ${maxItems} 项`} -

- )} -
- ) -} - -export default ListFieldEditor diff --git a/dashboard/src/components/RestartingOverlay.legacy.tsx b/dashboard/src/components/RestartingOverlay.legacy.tsx deleted file mode 100644 index aa368f1bb0..0000000000 --- a/dashboard/src/components/RestartingOverlay.legacy.tsx +++ /dev/null @@ -1,189 +0,0 @@ -import { useEffect, useState } from 'react' -import { Loader2, CheckCircle2, AlertCircle } from 'lucide-react' -import { Progress } from '@/components/ui/progress' - -/** - * @deprecated 请使用新的 RestartOverlay 组件 - * import { RestartOverlay } from '@/components/restart-overlay' - */ -interface RestartingOverlayProps { - onRestartComplete?: () => void - onRestartFailed?: () => void -} - -/** - * @deprecated 请使用新的 RestartOverlay 组件 - * import { RestartOverlay } from '@/components/restart-overlay' - */ -export function RestartingOverlay({ onRestartComplete, onRestartFailed }: RestartingOverlayProps) { - const [progress, setProgress] = useState(0) - const [status, setStatus] = useState<'restarting' | 'checking' | 'success' | 'failed'>('restarting') - const [elapsedTime, setElapsedTime] = useState(0) - const [checkAttempts, setCheckAttempts] = useState(0) - - useEffect(() => { - // 进度条动画 - const progressInterval = setInterval(() => { - setProgress((prev) => { - if (prev >= 90) return prev - return prev + 1 - }) - }, 200) - - // 计时器 - const timerInterval = setInterval(() => { - setElapsedTime((prev) => prev + 1) - }, 1000) - - // 等待3秒后开始检查状态(给后端重启时间) - const initialDelay = setTimeout(() => { - setStatus('checking') - startHealthCheck() - }, 3000) - - return () => { - clearInterval(progressInterval) - clearInterval(timerInterval) - clearTimeout(initialDelay) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) - - const startHealthCheck = () => { - const maxAttempts = 60 // 最多尝试60次(约2分钟) - - const checkHealth = async () => { - try { - setCheckAttempts((prev) => prev + 1) - - const response = await fetch('/api/webui/system/status', { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - signal: AbortSignal.timeout(3000), // 3秒超时 - }) - - if (response.ok) { - // 重启成功 - setProgress(100) - setStatus('success') - setTimeout(() => { - onRestartComplete?.() - }, 1500) - } else { - throw new Error('Status check failed') - } - } catch { - // 继续尝试 - if (checkAttempts < maxAttempts) { - setTimeout(checkHealth, 2000) // 2秒后重试 - } else { - // 超过最大尝试次数 - setStatus('failed') - onRestartFailed?.() - } - } - } - - checkHealth() - } - - const formatTime = (seconds: number) => { - const mins = Math.floor(seconds / 60) - const secs = seconds % 60 - return `${mins}:${secs.toString().padStart(2, '0')}` - } - - return ( -
-
- {/* 图标和状态 */} -
- {status === 'restarting' && ( - <> - -

正在重启麦麦

-

- 请稍候,麦麦正在重启中... -

- - )} - - {status === 'checking' && ( - <> - -

检查服务状态

-

- 等待服务恢复... (尝试 {checkAttempts}/60) -

- - )} - - {status === 'success' && ( - <> - -

重启成功

-

- 正在跳转到登录页面... -

- - )} - - {status === 'failed' && ( - <> - -

重启超时

-

- 服务未能在预期时间内恢复,请手动检查或刷新页面 -

- - )} -
- - {/* 进度条 */} - {status !== 'failed' && ( -
- -
- {progress}% - 已用时: {formatTime(elapsedTime)} -
-
- )} - - {/* 提示信息 */} -
-

- {status === 'restarting' && '🔄 配置已保存,正在重启主程序...'} - {status === 'checking' && '⏳ 正在等待服务恢复,请勿关闭页面...'} - {status === 'success' && '✅ 配置已生效,服务运行正常'} - {status === 'failed' && '⚠️ 如果长时间无响应,请尝试手动重启'} -

-
- - {/* 失败时的操作按钮 */} - {status === 'failed' && ( -
- - -
- )} -
-
- ) -} diff --git a/dashboard/src/components/animation-provider.tsx b/dashboard/src/components/animation-provider.tsx deleted file mode 100644 index 2a72ba1df8..0000000000 --- a/dashboard/src/components/animation-provider.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { useEffect, useState } from 'react' -import type { ReactNode } from 'react' -import { AnimationContext } from '@/lib/animation-context' - -type AnimationProviderProps = { - children: ReactNode - defaultEnabled?: boolean - defaultWavesEnabled?: boolean - storageKey?: string - wavesStorageKey?: string -} - -export function AnimationProvider({ - children, - defaultEnabled = true, - defaultWavesEnabled = true, - storageKey = 'enable-animations', - wavesStorageKey = 'enable-waves-background', -}: AnimationProviderProps) { - const [enableAnimations, setEnableAnimations] = useState(() => { - const stored = localStorage.getItem(storageKey) - return stored !== null ? stored === 'true' : defaultEnabled - }) - - const [enableWavesBackground, setEnableWavesBackground] = useState(() => { - const stored = localStorage.getItem(wavesStorageKey) - return stored !== null ? stored === 'true' : defaultWavesEnabled - }) - - useEffect(() => { - const root = document.documentElement - - if (enableAnimations) { - root.classList.remove('no-animations') - } else { - root.classList.add('no-animations') - } - - localStorage.setItem(storageKey, String(enableAnimations)) - }, [enableAnimations, storageKey]) - - useEffect(() => { - localStorage.setItem(wavesStorageKey, String(enableWavesBackground)) - }, [enableWavesBackground, wavesStorageKey]) - - const value = { - enableAnimations, - setEnableAnimations, - enableWavesBackground, - setEnableWavesBackground, - } - - return {children} -} diff --git a/dashboard/src/components/back-to-top.tsx b/dashboard/src/components/back-to-top.tsx deleted file mode 100644 index 3473566ef6..0000000000 --- a/dashboard/src/components/back-to-top.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { useEffect, useState, useRef } from 'react' -import { ArrowUp } from 'lucide-react' -import { cn } from '@/lib/utils' -import { Button } from '@/components/ui/button' - -export function BackToTop() { - const [progress, setProgress] = useState(0) - const [visible, setVisible] = useState(false) - const scrollerRef = useRef(null) - - useEffect(() => { - const handleScroll = (e: Event) => { - const target = e.target as HTMLElement - - // 简单的启发式:如果是主要滚动容器(通常高度较大) - // 我们假设页面中主要的滚动区域是高度最大的那个,或者就是当前触发滚动的这个 - // 只要它有足够的滚动空间 - if (target.scrollHeight > target.clientHeight + 100) { - scrollerRef.current = target - - const scrollTop = target.scrollTop - const height = target.scrollHeight - target.clientHeight - const scrolled = height > 0 ? (scrollTop / height) * 100 : 0 - - setProgress(scrolled) - setVisible(scrollTop > 300) - } - } - - // 使用捕获阶段监听所有滚动事件,因为 scroll 事件不冒泡 - window.addEventListener('scroll', handleScroll, { capture: true, passive: true }) - return () => window.removeEventListener('scroll', handleScroll, { capture: true }) - }, []) - - const scrollToTop = () => { - scrollerRef.current?.scrollTo({ top: 0, behavior: 'smooth' }) - } - - // SVG 环形进度条参数 - const radius = 18 - const circumference = 2 * Math.PI * radius - const strokeDashoffset = circumference - (progress / 100) * circumference - - return ( -
- -
- ) -} diff --git a/dashboard/src/components/emoji-thumbnail.tsx b/dashboard/src/components/emoji-thumbnail.tsx deleted file mode 100644 index c92cd91a4e..0000000000 --- a/dashboard/src/components/emoji-thumbnail.tsx +++ /dev/null @@ -1,123 +0,0 @@ -/** - * 表情包缩略图组件 - * - * 特性: - * - 自动处理 202 响应(缩略图生成中) - * - 显示 Skeleton 占位符 - * - 自动重试加载 - * - 加载失败显示占位图标 - */ - -import { useState, useEffect, useCallback } from 'react' -import { Skeleton } from '@/components/ui/skeleton' -import { ImageIcon } from 'lucide-react' -import { cn } from '@/lib/utils' - -interface EmojiThumbnailProps { - src: string - alt?: string - className?: string - /** 最大重试次数 */ - maxRetries?: number - /** 重试间隔(毫秒) */ - retryInterval?: number -} - -type LoadingState = 'loading' | 'loaded' | 'generating' | 'error' - -export function EmojiThumbnail({ - src, - alt = '表情包', - className, - maxRetries = 5, - retryInterval = 1500, -}: EmojiThumbnailProps) { - const [state, setState] = useState('loading') - const [retryCount, setRetryCount] = useState(0) - const [imageSrc, setImageSrc] = useState(null) - const [currentSrc, setCurrentSrc] = useState(src) - - // 当 src 变化时重置状态 - if (src !== currentSrc) { - setState('loading') - setRetryCount(0) - setImageSrc(null) - setCurrentSrc(src) - } - - const loadImage = useCallback(async () => { - try { - const response = await fetch(src, { - credentials: 'include', // 携带 Cookie - }) - - if (response.status === 202) { - // 缩略图正在生成中 - setState('generating') - - if (retryCount < maxRetries) { - // 延迟后重试 - setTimeout(() => { - setRetryCount(prev => prev + 1) - }, retryInterval) - } else { - // 超过最大重试次数,显示错误 - setState('error') - } - return - } - - if (!response.ok) { - setState('error') - return - } - - // 成功获取图片 - const blob = await response.blob() - const objectUrl = URL.createObjectURL(blob) - setImageSrc(objectUrl) - setState('loaded') - } catch (error) { - console.error('加载缩略图失败:', error) - setState('error') - } - }, [src, retryCount, maxRetries, retryInterval]) - - useEffect(() => { - loadImage() - }, [loadImage]) - - // 清理 Object URL - useEffect(() => { - return () => { - if (imageSrc) { - URL.revokeObjectURL(imageSrc) - } - } - }, [imageSrc]) - - // 加载中或生成中显示 Skeleton - if (state === 'loading' || state === 'generating') { - return ( - - ) - } - - // 加载失败显示占位图标 - if (state === 'error' || !imageSrc) { - return ( -
- -
- ) - } - - // 加载成功显示图片 - return ( - {alt} - ) -} diff --git a/dashboard/src/components/error-boundary.tsx b/dashboard/src/components/error-boundary.tsx deleted file mode 100644 index 7e3e0700a6..0000000000 --- a/dashboard/src/components/error-boundary.tsx +++ /dev/null @@ -1,307 +0,0 @@ -import { Component } from 'react' -import type { ErrorInfo, ReactNode } from 'react' -import { AlertTriangle, RefreshCw, Home, ChevronDown, ChevronUp, Copy, Check, Bug } from 'lucide-react' -import { Button } from '@/components/ui/button' -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' -import { Alert, AlertDescription } from '@/components/ui/alert' -import { ScrollArea } from '@/components/ui/scroll-area' -import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible' -import { useState } from 'react' - -interface Props { - children: ReactNode - fallback?: ReactNode -} - -interface State { - hasError: boolean - error: Error | null - errorInfo: ErrorInfo | null -} - -// 解析堆栈信息为结构化数据 -interface StackFrame { - functionName: string - fileName: string - lineNumber: string - columnNumber: string - raw: string -} - -function parseStackTrace(stack: string): StackFrame[] { - const lines = stack.split('\n').slice(1) // 跳过第一行(错误消息) - const frames: StackFrame[] = [] - - for (const line of lines) { - const trimmed = line.trim() - if (!trimmed.startsWith('at ')) continue - - // 匹配格式: at functionName (fileName:line:column) 或 at fileName:line:column - const match = trimmed.match(/at\s+(?:(.+?)\s+\()?(.+?):(\d+):(\d+)\)?$/) - if (match) { - frames.push({ - functionName: match[1] || '', - fileName: match[2], - lineNumber: match[3], - columnNumber: match[4], - raw: trimmed, - }) - } else { - frames.push({ - functionName: '', - fileName: '', - lineNumber: '', - columnNumber: '', - raw: trimmed, - }) - } - } - - return frames -} - -// 错误详情展示组件(函数组件,用于使用 hooks) -function ErrorDetails({ error, errorInfo }: { error: Error; errorInfo: ErrorInfo | null }) { - const [isStackOpen, setIsStackOpen] = useState(true) - const [isComponentStackOpen, setIsComponentStackOpen] = useState(false) - const [copied, setCopied] = useState(false) - - const stackFrames = error.stack ? parseStackTrace(error.stack) : [] - - const copyErrorInfo = async () => { - const errorText = ` -Error: ${error.name} -Message: ${error.message} - -Stack Trace: -${error.stack || 'No stack trace available'} - -Component Stack: -${errorInfo?.componentStack || 'No component stack available'} - -URL: ${window.location.href} -User Agent: ${navigator.userAgent} -Time: ${new Date().toISOString()} - `.trim() - - try { - await navigator.clipboard.writeText(errorText) - setCopied(true) - setTimeout(() => setCopied(false), 2000) - } catch (err) { - console.error('Failed to copy:', err) - } - } - - return ( -
- {/* 错误消息 */} - - - - {error.name}: {error.message} - - - - {/* 堆栈跟踪 */} - {stackFrames.length > 0 && ( - - - - - - -
- {stackFrames.map((frame, index) => ( -
-
- - {index + 1}. - -
- - {frame.functionName} - - {frame.fileName && ( -
- {frame.fileName} - {frame.lineNumber && ( - - :{frame.lineNumber}:{frame.columnNumber} - - )} -
- )} -
-
-
- ))} -
-
-
-
- )} - - {/* 组件堆栈 */} - {errorInfo?.componentStack && ( - - - - - - -
-                {errorInfo.componentStack}
-              
-
-
-
- )} - - {/* 复制按钮 */} - -
- ) -} - -// 错误回退 UI -function ErrorFallback({ - error, - errorInfo, -}: { - error: Error - errorInfo: ErrorInfo | null -}) { - const handleGoHome = () => { - window.location.href = '/' - } - - const handleRefresh = () => { - window.location.reload() - } - - return ( -
- - -
- -
- 页面出现了问题 - - 应用程序遇到了意外错误。您可以尝试刷新页面或返回首页。 - -
- - - - - {/* 操作按钮 */} -
- - -
- - {/* 提示信息 */} -

- 如果问题持续存在,请将错误信息复制并反馈给开发者 -

-
-
-
- ) -} - -// 错误边界类组件 -export class ErrorBoundary extends Component { - constructor(props: Props) { - super(props) - this.state = { - hasError: false, - error: null, - errorInfo: null, - } - } - - static getDerivedStateFromError(error: Error): Partial { - return { hasError: true, error } - } - - componentDidCatch(error: Error, errorInfo: ErrorInfo) { - console.error('ErrorBoundary caught an error:', error, errorInfo) - this.setState({ errorInfo }) - } - - handleReset = () => { - this.setState({ - hasError: false, - error: null, - errorInfo: null, - }) - } - - render() { - if (this.state.hasError && this.state.error) { - if (this.props.fallback) { - return this.props.fallback - } - - return ( - - ) - } - - return this.props.children - } -} - -// 路由级别的错误边界组件(用于 TanStack Router) -export function RouteErrorBoundary({ error }: { error: Error }) { - return ( - - ) -} diff --git a/dashboard/src/components/expression-reviewer.tsx b/dashboard/src/components/expression-reviewer.tsx deleted file mode 100644 index 512146ef7f..0000000000 --- a/dashboard/src/components/expression-reviewer.tsx +++ /dev/null @@ -1,1597 +0,0 @@ -/** - * 表达方式审核器弹窗组件 - * - * 功能: - * 1. 分页显示待审核/已通过/已拒绝的表达方式 - * 2. 支持单条通过/拒绝 - * 3. 支持批量操作 - * 4. 冲突检测(防止与AI自动检查冲突) - */ - -import { useState, useEffect, useCallback, useRef } from 'react' -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog' -import { Button } from '@/components/ui/button' -import { Input } from '@/components/ui/input' -import { Badge } from '@/components/ui/badge' -import { ScrollArea } from '@/components/ui/scroll-area' -import { Checkbox } from '@/components/ui/checkbox' -import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs' -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@/components/ui/select' -import { - Pagination, - PaginationContent, - PaginationEllipsis, - PaginationItem, - PaginationLink, -} from '@/components/ui/pagination' -import { useToast } from '@/hooks/use-toast' -import { - CheckCircle2, - XCircle, - Clock, - Search, - RefreshCw, - ChevronLeft, - ChevronRight, - Bot, - User, - AlertCircle, - List, - Zap, - X, - Ban, -} from 'lucide-react' -import { cn } from '@/lib/utils' -import { - getReviewStats, - getReviewList, - batchReviewExpressions, - getChatList, -} from '@/lib/expression-api' -import type { Expression, ReviewStats, ChatInfo, BatchReviewItem } from '@/types/expression' - -interface ExpressionReviewerProps { - open: boolean - onOpenChange: (open: boolean) => void -} - -export function ExpressionReviewer({ open, onOpenChange }: ExpressionReviewerProps) { - // 审核模式:list(列表模式)或 quick(快速审核模式) - const [reviewMode, setReviewMode] = useState<'list' | 'quick'>('list') - const [stats, setStats] = useState(null) - const [expressions, setExpressions] = useState([]) - - // 快速审核模式状态 - const [quickFilterType, setQuickFilterType] = useState<'unchecked' | 'passed' | 'rejected' | 'all'>('unchecked') - const [quickExpressions, setQuickExpressions] = useState([]) - const [quickCurrentIndex, setQuickCurrentIndex] = useState(0) - const [quickLoading, setQuickLoading] = useState(false) - const [quickTotal, setQuickTotal] = useState(0) - const [quickPage, setQuickPage] = useState(1) - const [swipeDirection, setSwipeDirection] = useState<'left' | 'right' | null>(null) - const [swipeOffset, setSwipeOffset] = useState(0) - const [isAnimating, setIsAnimating] = useState(false) - const [conflictId, setConflictId] = useState(null) - const cardRef = useRef(null) - const dragStartRef = useRef<{ x: number; y: number } | null>(null) - const isDraggingRef = useRef(false) - const [loading, setLoading] = useState(false) - const [statsLoading, setStatsLoading] = useState(false) - const [total, setTotal] = useState(0) - const [page, setPage] = useState(1) - const [pageSize, setPageSize] = useState(20) - const [jumpPage, setJumpPage] = useState('') - const [filterType, setFilterType] = useState<'unchecked' | 'passed' | 'rejected' | 'all'>('unchecked') - const [search, setSearch] = useState('') - const [searchInput, setSearchInput] = useState('') - const [selectedIds, setSelectedIds] = useState>(new Set()) - const [processingIds, setProcessingIds] = useState>(new Set()) - const [chatNameMap, setChatNameMap] = useState>(new Map()) - const { toast } = useToast() - - // 加载统计数据 - const loadStats = useCallback(async () => { - try { - setStatsLoading(true) - const data = await getReviewStats() - setStats(data) - } catch (error) { - console.error('加载统计失败:', error) - } finally { - setStatsLoading(false) - } - }, []) - - // 加载列表 - const loadList = useCallback(async () => { - try { - setLoading(true) - const response = await getReviewList({ - page, - page_size: pageSize, - filter_type: filterType, - search: search || undefined, - }) - setExpressions(response.data) - setTotal(response.total) - } catch (error) { - toast({ - title: '加载失败', - description: error instanceof Error ? error.message : '无法加载列表', - variant: 'destructive', - }) - } finally { - setLoading(false) - } - }, [page, pageSize, filterType, search, toast]) - - // 加载聊天名称映射 - const loadChatNames = useCallback(async () => { - try { - const response = await getChatList() - if (response?.data) { - const nameMap = new Map() - response.data.forEach((chat: ChatInfo) => { - nameMap.set(chat.chat_id, chat.chat_name) - }) - setChatNameMap(nameMap) - } - } catch (error) { - console.error('加载聊天名称失败:', error) - } - }, []) - - // 快速审核模式 - 加载数据 - const loadQuickList = useCallback(async (resetIndex = true, append = false) => { - try { - setQuickLoading(true) - const pageToLoad = append ? quickPage + 1 : quickPage - const response = await getReviewList({ - page: pageToLoad, - page_size: 20, - filter_type: quickFilterType, - }) - - if (append) { - // 追加模式:拼接数据 - setQuickExpressions(prev => [...prev, ...response.data]) - setQuickPage(pageToLoad) - } else { - // 替换模式 - setQuickExpressions(response.data) - } - - setQuickTotal(response.total) - if (resetIndex) { - setQuickCurrentIndex(0) - } - } catch (error) { - toast({ - title: '加载失败', - description: error instanceof Error ? error.message : '无法加载列表', - variant: 'destructive', - }) - } finally { - setQuickLoading(false) - } - }, [quickPage, quickFilterType, toast]) - - // 快速审核模式 - 切换筛选时重置 - useEffect(() => { - if (reviewMode === 'quick') { - setQuickPage(1) - setQuickCurrentIndex(0) - } - }, [quickFilterType, reviewMode]) - - // 快速审核模式 - 加载数据 - useEffect(() => { - if (open && reviewMode === 'quick') { - loadQuickList() - loadStats() - } - }, [open, reviewMode, quickPage, quickFilterType, loadQuickList, loadStats]) - - // 获取当前卡片允许的滑动方向 - const getAllowedDirections = useCallback((expr: Expression | undefined) => { - if (!expr) return { left: false, right: false } - - if (quickFilterType === 'unchecked') { - // 待审核:左拒绝,右通过 - return { left: true, right: true } - } else if (quickFilterType === 'passed') { - // 已通过:只能左滑改为拒绝 - return { left: true, right: false } - } else if (quickFilterType === 'rejected') { - // 已拒绝:只能右滑改为通过 - return { left: false, right: true } - } else { - // 全部:智能判断 - if (!expr.checked) { - // 未审核:双向 - return { left: true, right: true } - } else if (expr.rejected) { - // 已拒绝:只能右滑 - return { left: false, right: true } - } else { - // 已通过:只能左滑 - return { left: true, right: false } - } - } - }, [quickFilterType]) - - // 快速审核 - 执行审核操作 - const handleQuickReview = useCallback(async (rejected: boolean) => { - const currentExpr = quickExpressions[quickCurrentIndex] - if (!currentExpr || isAnimating) return - - const directions = getAllowedDirections(currentExpr) - if ((rejected && !directions.left) || (!rejected && !directions.right)) { - return - } - - setIsAnimating(true) - setSwipeDirection(rejected ? 'left' : 'right') - setSwipeOffset(rejected ? -400 : 400) - - try { - const response = await batchReviewExpressions([{ - id: currentExpr.id, - rejected, - require_unchecked: quickFilterType === 'unchecked', - }]) - - if (response.results[0]?.success) { - toast({ - title: rejected ? '已拒绝' : '已通过', - description: `表达方式 #${currentExpr.id} ${rejected ? '已拒绝' : '已通过'}`, - }) - - // 从列表中移除当前项 - setTimeout(() => { - setQuickExpressions(prev => prev.filter((_, i) => i !== quickCurrentIndex)) - setQuickTotal(prev => prev - 1) - - // 如果当前索引超出范围,调整索引 - if (quickCurrentIndex >= quickExpressions.length - 1) { - setQuickCurrentIndex(Math.max(0, quickCurrentIndex - 1)) - } - - // 重置状态 - setSwipeDirection(null) - setSwipeOffset(0) - setIsAnimating(false) - - // 刷新统计 - loadStats() - - // 如果列表为空且还有更多数据,加载下一页 - if (quickExpressions.length <= 1 && quickTotal > 1) { - loadQuickList(false) - } - }, 300) - } else { - // 冲突处理 - setConflictId(currentExpr.id) - toast({ - title: '数据冲突', - description: '该条目已被后台任务处理,正在刷新数据...', - variant: 'destructive', - }) - - // 播放冲突动画后刷新 - setTimeout(() => { - setConflictId(null) - setSwipeDirection(null) - setSwipeOffset(0) - setIsAnimating(false) - loadQuickList(false) // 重新加载当前页 - loadStats() - }, 1500) - } - } catch (error) { - toast({ - title: '操作失败', - description: error instanceof Error ? error.message : '未知错误', - variant: 'destructive', - }) - setSwipeDirection(null) - setSwipeOffset(0) - setIsAnimating(false) - } - }, [quickExpressions, quickCurrentIndex, isAnimating, getAllowedDirections, quickFilterType, toast, loadStats, quickTotal, loadQuickList]) - - // 拖拽开始 - const handleDragStart = useCallback((clientX: number, clientY: number) => { - if (isAnimating) return - dragStartRef.current = { x: clientX, y: clientY } - isDraggingRef.current = false - }, [isAnimating]) - - // 触发无效操作动画 - const triggerInvalidAnimation = useCallback((direction: 'left' | 'right') => { - if (isAnimating) return - setIsAnimating(true) - // 模拟向该方向移动一点 - setSwipeOffset(direction === 'left' ? -30 : 30) - - setTimeout(() => { - setSwipeOffset(0) - setTimeout(() => setIsAnimating(false), 300) - }, 150) - }, [isAnimating]) - - // 拖拽移动 - const handleDragMove = useCallback((clientX: number) => { - if (!dragStartRef.current || isAnimating) return - - const deltaX = clientX - dragStartRef.current.x - const currentExpr = quickExpressions[quickCurrentIndex] - const directions = getAllowedDirections(currentExpr) - - // 检查方向限制 - if (deltaX < 0 && !directions.left) { - setSwipeOffset(deltaX * 0.2) // 提供阻力反馈 - setSwipeDirection(null) - return - } - if (deltaX > 0 && !directions.right) { - setSwipeOffset(deltaX * 0.2) - setSwipeDirection(null) - return - } - - isDraggingRef.current = true - setSwipeOffset(deltaX) - - if (Math.abs(deltaX) > 50) { - setSwipeDirection(deltaX > 0 ? 'right' : 'left') - } else { - setSwipeDirection(null) - } - }, [quickExpressions, quickCurrentIndex, getAllowedDirections, isAnimating]) - - // 拖拽结束 - const handleDragEnd = useCallback(() => { - if (!dragStartRef.current) return - - const threshold = 100 - if (Math.abs(swipeOffset) > threshold && swipeDirection) { - handleQuickReview(swipeDirection === 'left') - } else { - // 回弹 - setSwipeOffset(0) - setSwipeDirection(null) - } - - dragStartRef.current = null - isDraggingRef.current = false - }, [swipeOffset, swipeDirection, handleQuickReview]) - - // 鼠标事件处理 - const handleMouseDown = useCallback((e: React.MouseEvent) => { - handleDragStart(e.clientX, e.clientY) - }, [handleDragStart]) - - const handleMouseMove = useCallback((e: React.MouseEvent) => { - if (dragStartRef.current) { - e.preventDefault() - handleDragMove(e.clientX) - } - }, [handleDragMove]) - - const handleMouseUp = useCallback(() => { - handleDragEnd() - }, [handleDragEnd]) - - const handleMouseLeave = useCallback(() => { - if (dragStartRef.current) { - handleDragEnd() - } - }, [handleDragEnd]) - - // 触摸事件处理 - const handleTouchStart = useCallback((e: React.TouchEvent) => { - const touch = e.touches[0] - handleDragStart(touch.clientX, touch.clientY) - }, [handleDragStart]) - - const handleTouchMove = useCallback((e: React.TouchEvent) => { - const touch = e.touches[0] - handleDragMove(touch.clientX) - }, [handleDragMove]) - - const handleTouchEnd = useCallback(() => { - handleDragEnd() - }, [handleDragEnd]) - - // 键盘事件处理 - useEffect(() => { - if (!open || reviewMode !== 'quick') return - - const handleKeyDown = (e: KeyboardEvent) => { - // 只处理方向键 - if (!['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key)) return - - // 阻止事件继续传播,避免被 Tabs 组件捕获 - e.preventDefault() - e.stopPropagation() - e.stopImmediatePropagation() - - if (isAnimating || quickLoading) return - - const currentExpr = quickExpressions[quickCurrentIndex] - const directions = getAllowedDirections(currentExpr) - - if (e.key === 'ArrowLeft') { - if (directions.left) { - handleQuickReview(true) // 拒绝 - } else { - triggerInvalidAnimation('left') - } - } else if (e.key === 'ArrowRight') { - if (directions.right) { - handleQuickReview(false) // 通过 - } else { - triggerInvalidAnimation('right') - } - } else if (e.key === 'ArrowDown') { - // 跳过当前项 - if (quickCurrentIndex < quickExpressions.length - 1) { - setQuickCurrentIndex(prev => prev + 1) - } - } else if (e.key === 'ArrowUp') { - // 返回上一项 - if (quickCurrentIndex > 0) { - setQuickCurrentIndex(prev => prev - 1) - } - } - } - - // 使用 capture 模式,在事件到达 Tabs 之前拦截 - window.addEventListener('keydown', handleKeyDown, true) - return () => window.removeEventListener('keydown', handleKeyDown, true) - }, [open, reviewMode, quickExpressions, quickCurrentIndex, isAnimating, quickLoading, getAllowedDirections, handleQuickReview, triggerInvalidAnimation]) - - // 动态加载更多数据 - 当接近列表末尾时自动加载 - useEffect(() => { - if (!open || reviewMode !== 'quick' || quickLoading) return - - // 距离末尾还有5个或更少时,且还有更多数据时,自动加载 - const remaining = quickExpressions.length - quickCurrentIndex - 1 - const hasMoreData = quickExpressions.length < quickTotal - - if (remaining <= 5 && hasMoreData) { - loadQuickList(false, true) // 追加模式 - } - }, [open, reviewMode, quickCurrentIndex, quickExpressions.length, quickTotal, quickLoading, loadQuickList]) - - // 初始加载 - useEffect(() => { - if (open) { - loadStats() - loadList() - loadChatNames() - } - }, [open, loadStats, loadList, loadChatNames]) - - // 切换筛选时重置页码 - useEffect(() => { - setPage(1) - setSelectedIds(new Set()) - }, [filterType, search]) - - // 列表加载时清空选择 - useEffect(() => { - setSelectedIds(new Set()) - }, [expressions]) - - // 搜索处理 - const handleSearch = () => { - setSearch(searchInput) - setPage(1) - } - - // 获取聊天名称 - const getChatName = (chatId: string): string => { - return chatNameMap.get(chatId) || chatId - } - - // 单条审核 - const handleReview = async (id: number, rejected: boolean) => { - try { - setProcessingIds((prev) => new Set(prev).add(id)) - - const response = await batchReviewExpressions([ - { id, rejected, require_unchecked: filterType === 'unchecked' } - ]) - - if (response.results[0]?.success) { - toast({ - title: rejected ? '已拒绝' : '已通过', - description: `表达方式 #${id} ${rejected ? '已拒绝' : '已通过'}`, - }) - // 刷新列表和统计 - loadList() - loadStats() - } else { - toast({ - title: '操作失败', - description: response.results[0]?.message || '未知错误', - variant: 'destructive', - }) - } - } catch (error) { - toast({ - title: '操作失败', - description: error instanceof Error ? error.message : '未知错误', - variant: 'destructive', - }) - } finally { - setProcessingIds((prev) => { - const next = new Set(prev) - next.delete(id) - return next - }) - } - } - - // 批量审核 - const handleBatchReview = async (rejected: boolean) => { - if (selectedIds.size === 0) { - toast({ - title: '请选择', - description: '请先选择要审核的表达方式', - variant: 'destructive', - }) - return - } - - try { - setLoading(true) - - const items: BatchReviewItem[] = Array.from(selectedIds).map((id) => ({ - id, - rejected, - require_unchecked: filterType === 'unchecked', - })) - - const response = await batchReviewExpressions(items) - - toast({ - title: '批量审核完成', - description: `成功 ${response.succeeded} 条,失败 ${response.failed} 条`, - variant: response.failed > 0 ? 'destructive' : 'default', - }) - - // 清空选择并刷新 - setSelectedIds(new Set()) - loadList() - loadStats() - } catch (error) { - toast({ - title: '批量审核失败', - description: error instanceof Error ? error.message : '未知错误', - variant: 'destructive', - }) - } finally { - setLoading(false) - } - } - - // 全选/取消全选 - const handleSelectAll = () => { - if (selectedIds.size === expressions.length) { - setSelectedIds(new Set()) - } else { - setSelectedIds(new Set(expressions.map((e) => e.id))) - } - } - - // 切换选择 - const toggleSelect = (id: number) => { - setSelectedIds((prev) => { - const next = new Set(prev) - if (next.has(id)) { - next.delete(id) - } else { - next.add(id) - } - return next - }) - } - - // 格式化时间 - const formatTime = (timestamp: number | null) => { - if (!timestamp) return '-' - return new Date(timestamp * 1000).toLocaleString('zh-CN', { - month: '2-digit', - day: '2-digit', - hour: '2-digit', - minute: '2-digit', - }) - } - - // 获取状态标签 - const getStatusBadge = (expr: Expression) => { - if (!expr.checked) { - return ( - - - 待审核 - - ) - } - if (expr.rejected) { - return ( - - - 已拒绝 - - ) - } - return ( - - - 已通过 - - ) - } - - // 获取修改者标签 - const getModifierBadge = (modifier: string | null) => { - if (!modifier) return null - if (modifier === 'ai') { - return ( - - - AI - - ) - } - return ( - - - 人工 - - ) - } - - const totalPages = Math.ceil(total / pageSize) - - // 生成页码数组 - const getPageNumbers = () => { - const pages: (number | 'ellipsis')[] = [] - if (totalPages <= 7) { - // 总页数不多,全部显示 - for (let i = 1; i <= totalPages; i++) { - pages.push(i) - } - } else { - // 总是显示第一页 - pages.push(1) - - if (page > 3) { - pages.push('ellipsis') - } - - // 当前页附近的页码 - const start = Math.max(2, page - 1) - const end = Math.min(totalPages - 1, page + 1) - - for (let i = start; i <= end; i++) { - pages.push(i) - } - - if (page < totalPages - 2) { - pages.push('ellipsis') - } - - // 总是显示最后一页 - if (totalPages > 1) { - pages.push(totalPages) - } - } - return pages - } - - // 处理页码跳转 - const handleJumpPage = () => { - const targetPage = parseInt(jumpPage, 10) - if (!isNaN(targetPage) && targetPage >= 1 && targetPage <= totalPages) { - setPage(targetPage) - setJumpPage('') - } - } - - return ( - - - {/* 浏览器标签页风格的模式切换器 */} -
- {/* 列表模式标签 */} - - - {/* 快速审核标签 */} - - - {/* 右侧空白区域和关闭按钮 */} -
- -
- - {/* 列表模式内容 */} - {reviewMode === 'list' && ( - <> - - 表达方式审核 - - 审核麦麦学习到的表达方式。通过审核的项目才会被使用(可在配置中调整),被拒绝的项目永远不会被使用。 - - - {/* 统计卡片 */} -
-
-
- {statsLoading ? '-' : stats?.unchecked ?? 0} -
-
待审核
-
-
-
- {statsLoading ? '-' : stats?.passed ?? 0} -
-
已通过
-
-
-
- {statsLoading ? '-' : stats?.rejected ?? 0} -
-
已拒绝
-
-
-
- {statsLoading ? '-' : stats?.total ?? 0} -
-
总计
-
-
-
- - {/* 筛选和操作栏 */} -
- setFilterType(v as typeof filterType)} - className="w-full" - > - - - - 待审核 - 待审 - ({stats?.unchecked ?? 0}) - - - - 已通过 - 通过 - ({stats?.passed ?? 0}) - - - - 已拒绝 - 拒绝 - ({stats?.rejected ?? 0}) - - - 全部 - ({stats?.total ?? 0}) - - - - -
-
- - setSearchInput(e.target.value)} - onKeyDown={(e) => e.key === 'Enter' && handleSearch()} - className="pl-9" - /> -
-
- - -
- - {/* 批量操作按钮 */} - {selectedIds.size > 0 && ( -
- {filterType === 'unchecked' ? ( - // 待审核:显示批量通过和批量拒绝 - <> - - - - ) : filterType === 'passed' ? ( - // 已通过:只显示批量改为拒绝 - - ) : filterType === 'rejected' ? ( - // 已拒绝:只显示批量改为通过 - - ) : ( - // 全部:显示两个按钮 - <> - - - - )} -
- )} -
-
- - {/* 列表区域 */} - - {loading && expressions.length === 0 ? ( -
- -
- ) : expressions.length === 0 ? ( -
- -

没有找到表达方式

-
- ) : ( -
- {/* 全选 */} - {expressions.length > 0 && ( -
-
- 0} - onCheckedChange={handleSelectAll} - /> - - {selectedIds.size === expressions.length && expressions.length > 0 - ? `已全选当前页 (${expressions.length} 条)` - : `全选当前页 (${expressions.length} 条)`} - -
- {selectedIds.size > 0 && ( - - )} -
- )} - - {/* 表达方式列表 */} - {expressions.map((expr) => ( -
-
- {/* 选择框 */} - toggleSelect(expr.id)} - disabled={processingIds.has(expr.id)} - className="mt-1" - /> - - {/* 内容 */} -
- {/* 情景 */} -
- 情景: -

{expr.situation}

-
- - {/* 风格 */} -
- 风格: -

{expr.style}

-
- - {/* 元信息 */} -
- #{expr.id} - · - - {getChatName(expr.chat_id)} - - · - {formatTime(expr.create_date)} -
- {getStatusBadge(expr)} - {getModifierBadge(expr.modified_by)} -
-
-
- - {/* 操作按钮 */} -
- {filterType === 'unchecked' ? ( - <> - - - - ) : filterType === 'passed' ? ( - - ) : filterType === 'rejected' ? ( - - ) : ( - // all 模式下显示两个按钮 - <> - {expr.rejected ? ( - - ) : expr.checked ? ( - - ) : ( - <> - - - - )} - - )} -
-
-
- ))} -
- )} -
- - {/* 分页 */} -
- {/* 左侧:每页显示数量 */} -
- 每页 - - - 共 {total} 条 -
- - {/* 中间:页码导航 */} - - - - - - - {getPageNumbers().map((pageNum, idx) => ( - - {pageNum === 'ellipsis' ? ( - - ) : ( - { - e.preventDefault() - setPage(pageNum) - }} - className="h-8 w-8 cursor-pointer" - > - {pageNum} - - )} - - ))} - - - - - - - - {/* 右侧:跳转 */} -
- 跳至 - setJumpPage(e.target.value)} - onKeyDown={(e) => e.key === 'Enter' && handleJumpPage()} - className="w-16 h-8 text-center" - placeholder={page.toString()} - /> - - -
-
- - )} - - {/* 快速审核模式内容 */} - {reviewMode === 'quick' && ( -
- {/* 顶部筛选和统计 */} -
- {/* 统计信息 */} -
-
- - 待审核: {stats?.unchecked ?? 0} - - - 已通过: {stats?.passed ?? 0} - - - 已拒绝: {stats?.rejected ?? 0} - -
- -
- - {/* 筛选标签 */} - setQuickFilterType(v as typeof quickFilterType)} - className="w-full" - > - - - - 待审核 - 待审 - - - - 已通过 - 通过 - - - - 已拒绝 - 拒绝 - - - 全部 - - - -
- - {/* 卡片区域 */} -
- {quickLoading && quickExpressions.length === 0 ? ( -
- -

加载中...

-
- ) : quickExpressions.length === 0 ? ( -
-
- -
-

全部审核完成!

-

当前筛选条件下没有待处理的项目

-
- ) : ( - <> - {/* 进度提示 */} -
- {quickCurrentIndex + 1} / {quickExpressions.length} - {quickTotal > quickExpressions.length && ( - (共 {quickTotal} 条) - )} -
- - {/* 方向提示 (仅针对当前卡片) */} -
- {(() => { - const currentExpr = quickExpressions[quickCurrentIndex] - const directions = getAllowedDirections(currentExpr) - return ( - <> -
- - 拒绝 -
-
- 通过 - -
- - ) - })()} -
- - {/* 堆叠卡片 */} -
- {quickExpressions - .slice(quickCurrentIndex, quickCurrentIndex + 5) - .reverse() - .map((expr, reverseIndex, array) => { - const index = array.length - 1 - reverseIndex // 0 is current, 1 is next... - const isCurrent = index === 0 - - // 计算样式 - let style: React.CSSProperties = { - zIndex: 5 - index, - position: 'absolute', - width: '100%', - transition: isCurrent && !isDraggingRef.current ? 'all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1)' : 'none', - } - - if (isCurrent) { - // 当前卡片样式 - style = { - ...style, - transform: `translateX(${swipeOffset}px) rotate(${swipeOffset * 0.05}deg)`, - opacity: Math.max(0, 1 - Math.abs(swipeOffset) / 500), - cursor: 'grab', - } - } else { - // 后方卡片样式 - const progress = Math.min(Math.abs(swipeOffset) / 200, 1) // 0 to 1 - - // 计算指定索引的样式属性 - const getStyleForIndex = (i: number) => { - // 增加一些伪随机的错位感,让堆叠看起来不那么死板 - const randomRotate = (i * 7) % 5 - const randomX = (i * 13) % 7 - - return { - scale: 1 - i * 0.05, - translateY: i * 12, - // 错位效果:奇偶交替旋转 + 伪随机偏移 - rotate: (i % 2 === 0 ? 1 : -1) * (i * 2) + randomRotate, - translateX: (i % 2 === 0 ? -1 : 1) * (i * 4) + randomX, - } - } - - const base = getStyleForIndex(index) - const target = getStyleForIndex(index - 1) - - // 插值计算:所有后方卡片都会跟随第一张卡片的滑动而向前移动 - const currentScale = base.scale + (target.scale - base.scale) * progress - const currentTranslateY = base.translateY + (target.translateY - base.translateY) * progress - const currentRotate = base.rotate + (target.rotate - base.rotate) * progress - const currentTranslateX = base.translateX + (target.translateX - base.translateX) * progress - - style = { - ...style, - transform: `translate3d(${currentTranslateX}px, ${currentTranslateY}px, 0) scale(${currentScale}) rotate(${currentRotate}deg)`, - opacity: 1 - index * 0.15, - filter: `blur(${Math.max(0, index * 1 - progress)}px)`, // 模糊度也随之减小 - pointerEvents: 'none', - } - } - - return ( -
- {/* 冲突提示遮罩 */} - {isCurrent && conflictId === expr.id && ( -
-
-
- -
-

数据已更新

-

后台任务已处理此条目

-
- )} - - {/* 无效操作提示 */} - {isCurrent && ( -
10 && !getAllowedDirections(expr).right)) - ? "opacity-100" - : "opacity-0" - )}> -
- -
-
- )} - -
- {/* 状态和ID */} -
- #{expr.id} -
- {getStatusBadge(expr)} - {getModifierBadge(expr.modified_by)} -
-
- - {/* 情景 */} -
- -
-

{expr.situation}

-
-
- - {/* 风格 */} -
- -
- {expr.style.split(/[,,]/).map((s, i) => ( - - {s.trim()} - - ))} -
-
-
- - {/* 底部信息 */} -
-
-
- -
- - {getChatName(expr.chat_id)} - -
- {formatTime(expr.create_date)} -
-
- ) - })} -
- - {/* 操作按钮(移动端) */} -
- {(() => { - const currentExpr = quickExpressions[quickCurrentIndex] - const directions = getAllowedDirections(currentExpr) - return ( - <> - - - - ) - })()} -
- - )} -
- - {/* 底部快捷键提示(桌面端) */} -
-
- - 拒绝 -
-
- - 通过 -
-
- - 上一条 -
-
- - 下一条 -
- | - 拖拽卡片滑动审核 -
-
- )} - -
- ) -} diff --git a/dashboard/src/components/http-warning-banner.tsx b/dashboard/src/components/http-warning-banner.tsx deleted file mode 100644 index 5573592810..0000000000 --- a/dashboard/src/components/http-warning-banner.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { useState } from 'react' -import { AlertTriangle, X } from 'lucide-react' -import { Button } from '@/components/ui/button' - -/** - * HTTP 警告横幅组件 - * 当用户通过 HTTP 访问时显示安全警告 - */ -export function HttpWarningBanner() { - // 直接计算初始状态,避免 effect 中调用 setState - const isHttp = window.location.protocol === 'http:' - const hostname = window.location.hostname.toLowerCase() - const isLocalhost = hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1' - const dismissed = sessionStorage.getItem('http-warning-dismissed') === 'true' - - // 本地访问(localhost/127.0.0.1)不显示警告 - const [isVisible, setIsVisible] = useState(isHttp && !isLocalhost && !dismissed) - const [isDismissed, setIsDismissed] = useState(false) - - const handleDismiss = () => { - setIsDismissed(true) - setIsVisible(false) - sessionStorage.setItem('http-warning-dismissed', 'true') - } - - if (!isVisible || isDismissed) { - return null - } - - return ( -
-
-
-
- -
-

- 安全警告: - 您正在使用 HTTP 访问 MaiBot WebUI -

-

- 如果这是公网服务器,您的数据(包括 Token、聊天记录等)可能在传输过程中被窃取。强烈建议使用 HTTPS 访问或仅在本地网络使用。 -

-
-
- -
-
-
- ) -} diff --git a/dashboard/src/components/index.ts b/dashboard/src/components/index.ts deleted file mode 100644 index cc623fba29..0000000000 --- a/dashboard/src/components/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -export { CodeEditor } from './CodeEditor' -export type { Language } from './CodeEditor' - -// 重启遮罩层 -export { RestartOverlay } from './restart-overlay' -// 兼容旧版本 -export { RestartingOverlay } from './RestartingOverlay.legacy' - -// 列表编辑器 -export { ListFieldEditor } from './ListFieldEditor' - -// Markdown 渲染器 -export { MarkdownRenderer } from './markdown-renderer' diff --git a/dashboard/src/components/layout.tsx b/dashboard/src/components/layout.tsx deleted file mode 100644 index 4a675b9f1e..0000000000 --- a/dashboard/src/components/layout.tsx +++ /dev/null @@ -1,409 +0,0 @@ -import { Menu, Moon, Sun, ChevronLeft, Home, Settings, LogOut, FileText, Server, Boxes, Smile, MessageSquare, UserCircle, FileSearch, Package, BookOpen, Search, Sliders, Network, Hash, LayoutGrid, Database, Activity, PieChart } from 'lucide-react' -import { useState, useEffect } from 'react' -import { Link, useMatchRoute } from '@tanstack/react-router' -import { useTheme, toggleThemeWithTransition } from './use-theme' -import { useAuthGuard } from '@/hooks/use-auth' -import { logout } from '@/lib/fetch-with-auth' -import { Button } from '@/components/ui/button' -import { Kbd } from '@/components/ui/kbd' -import { SearchDialog } from '@/components/search-dialog' -import { ScrollArea } from '@/components/ui/scroll-area' -import { HttpWarningBanner } from '@/components/http-warning-banner' -import { BackToTop } from '@/components/back-to-top' -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from '@/components/ui/tooltip' -import { cn } from '@/lib/utils' -import { formatVersion } from '@/lib/version' -import type { ReactNode, ComponentType } from 'react' -import type { LucideProps } from 'lucide-react' - -interface LayoutProps { - children: ReactNode -} - -interface MenuItem { - icon: ComponentType - label: string - path: string - tourId?: string -} - -interface MenuSection { - title: string - items: MenuItem[] -} - -export function Layout({ children }: LayoutProps) { - const { checking } = useAuthGuard() // 检查认证状态 - - const [sidebarOpen, setSidebarOpen] = useState(true) - const [mobileMenuOpen, setMobileMenuOpen] = useState(false) - const [searchOpen, setSearchOpen] = useState(false) - const [tooltipsEnabled, setTooltipsEnabled] = useState(false) // 控制 tooltip 启用状态 - const { theme, setTheme } = useTheme() - const matchRoute = useMatchRoute() - - // 侧边栏状态变化时,延迟启用/禁用 tooltip - useEffect(() => { - if (sidebarOpen) { - // 侧边栏展开时,立即禁用 tooltip - setTooltipsEnabled(false) - } else { - // 侧边栏收起时,等待动画完成后再启用 tooltip - const timer = setTimeout(() => { - setTooltipsEnabled(true) - }, 350) // 稍大于 CSS transition duration (300ms) - return () => clearTimeout(timer) - } - }, [sidebarOpen]) - - // 搜索快捷键监听(Cmd/Ctrl + K) - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - if ((e.metaKey || e.ctrlKey) && e.key === 'k') { - e.preventDefault() - setSearchOpen(true) - } - } - - window.addEventListener('keydown', handleKeyDown) - return () => window.removeEventListener('keydown', handleKeyDown) - }, []) - - // 认证检查中,显示加载状态 - if (checking) { - return ( -
-
正在验证登录状态...
-
- ) - } - - // 菜单项配置 - 分块结构 - const menuSections: MenuSection[] = [ - { - title: '概览', - items: [ - { icon: Home, label: '首页', path: '/' }, - ], - }, - { - title: '麦麦配置编辑', - items: [ - { icon: FileText, label: '麦麦主程序配置', path: '/config/bot' }, - { icon: Server, label: 'AI模型厂商配置', path: '/config/modelProvider', tourId: 'sidebar-model-provider' }, - { icon: Boxes, label: '模型管理与分配', path: '/config/model', tourId: 'sidebar-model-management' }, - { icon: Sliders, label: '麦麦适配器配置', path: '/config/adapter' }, - ], - }, - { - title: '麦麦资源管理', - items: [ - { icon: Smile, label: '表情包管理', path: '/resource/emoji' }, - { icon: MessageSquare, label: '表达方式管理', path: '/resource/expression' }, - { icon: Hash, label: '黑话管理', path: '/resource/jargon' }, - { icon: UserCircle, label: '人物信息管理', path: '/resource/person' }, - { icon: Network, label: '知识库图谱可视化', path: '/resource/knowledge-graph' }, - { icon: Database, label: '麦麦知识库管理', path: '/resource/knowledge-base' }, - ], - }, - { - title: '扩展与监控', - items: [ - { icon: Package, label: '插件市场', path: '/plugins' }, - { icon: LayoutGrid, label: '配置模板市场', path: '/config/pack-market' }, - { icon: Sliders, label: '插件配置', path: '/plugin-config' }, - { icon: FileSearch, label: '日志查看器', path: '/logs' }, - { icon: Activity, label: '计划器&回复器监控', path: '/planner-monitor' }, - { icon: MessageSquare, label: '本地聊天室', path: '/chat' }, - ], - }, - { - title: '系统', - items: [ - { icon: Settings, label: '系统设置', path: '/settings' }, - ], - }, - ] - - // 获取实际应用的主题(处理 system 情况) - const getActualTheme = () => { - if (theme === 'system') { - return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' - } - return theme - } - - const actualTheme = getActualTheme() - - // 登出处理 - const handleLogout = async () => { - await logout() - } - - return ( - -
- {/* Sidebar */} - - - {/* Mobile overlay */} - {mobileMenuOpen && ( -
setMobileMenuOpen(false)} - /> - )} - - {/* Main content */} -
- {/* HTTP 安全警告横幅 */} - - - {/* Topbar */} -
-
- {/* 移动端菜单按钮 */} - - - {/* 桌面端侧边栏收起/展开按钮 */} - -
- -
- {/* 年度总结入口 */} - - - - - {/* 搜索框 */} - - - {/* 搜索对话框 */} - - - {/* 麦麦文档链接 */} - - - {/* 主题切换按钮 */} - - - {/* 分隔线 */} -
- - {/* 登出按钮 */} - -
-
- - {/* Page content */} -
{children}
- - {/* Back to Top Button */} - -
-
- - ) -} diff --git a/dashboard/src/components/markdown-renderer.tsx b/dashboard/src/components/markdown-renderer.tsx deleted file mode 100644 index 429ea41f74..0000000000 --- a/dashboard/src/components/markdown-renderer.tsx +++ /dev/null @@ -1,134 +0,0 @@ -import ReactMarkdown from 'react-markdown' -import remarkGfm from 'remark-gfm' -import remarkMath from 'remark-math' -import rehypeKatex from 'rehype-katex' -import 'katex/dist/katex.min.css' -import type { ComponentPropsWithoutRef } from 'react' - -interface MarkdownRendererProps { - content: string - className?: string -} - -export function MarkdownRenderer({ content, className = '' }: MarkdownRendererProps) { - return ( -
- & { inline?: boolean }) { - return inline ? ( - - {children} - - ) : ( - - {children} - - ) - }, - // 自定义表格样式 - table({ children, ...props }) { - return ( -
- - {children} -
-
- ) - }, - th({ children, ...props }) { - return ( - - {children} - - ) - }, - td({ children, ...props }) { - return ( - - {children} - - ) - }, - // 自定义链接样式 - a({ children, ...props }) { - return ( - - {children} - - ) - }, - // 自定义引用块样式 - blockquote({ children, ...props }) { - return ( -
- {children} -
- ) - }, - // 自定义标题样式 - h1({ children, ...props }) { - return ( -

- {children} -

- ) - }, - h2({ children, ...props }) { - return ( -

- {children} -

- ) - }, - h3({ children, ...props }) { - return ( -

- {children} -

- ) - }, - h4({ children, ...props }) { - return ( -

- {children} -

- ) - }, - // 自定义列表样式 - ul({ children, ...props }) { - return ( -
    - {children} -
- ) - }, - ol({ children, ...props }) { - return ( -
    - {children} -
- ) - }, - // 自定义段落样式 - p({ children, ...props }) { - return ( -

- {children} -

- ) - }, - // 自定义分隔线样式 - hr({ ...props }) { - return
- }, - }} - > - {content} -
-
- ) -} diff --git a/dashboard/src/components/plugin-stats.tsx b/dashboard/src/components/plugin-stats.tsx deleted file mode 100644 index 63bee1b8e7..0000000000 --- a/dashboard/src/components/plugin-stats.tsx +++ /dev/null @@ -1,302 +0,0 @@ -/** - * 插件统计组件 - * 显示点赞、点踩、评分和下载量 - */ - -import { useState, useEffect } from 'react' -import { ThumbsUp, ThumbsDown, Star, Download } from 'lucide-react' -import { Button } from '@/components/ui/button' -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from '@/components/ui/dialog' -import { Textarea } from '@/components/ui/textarea' -import { useToast } from '@/hooks/use-toast' -import { - getPluginStats, - likePlugin, - dislikePlugin, - ratePlugin, - type PluginStatsData, -} from '@/lib/plugin-stats' - -interface PluginStatsProps { - pluginId: string - compact?: boolean // 紧凑模式(只显示数字) -} - -export function PluginStats({ pluginId, compact = false }: PluginStatsProps) { - const [stats, setStats] = useState(null) - const [loading, setLoading] = useState(true) - const [userRating, setUserRating] = useState(0) - const [userComment, setUserComment] = useState('') - const [isRatingDialogOpen, setIsRatingDialogOpen] = useState(false) - const { toast } = useToast() - - // 加载统计数据 - const loadStats = async () => { - setLoading(true) - const data = await getPluginStats(pluginId) - if (data) { - setStats(data) - } - setLoading(false) - } - - useEffect(() => { - loadStats() - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [pluginId]) - - // 处理点赞 - const handleLike = async () => { - const result = await likePlugin(pluginId) - - if (result.success) { - toast({ title: '已点赞', description: '感谢你的支持!' }) - loadStats() // 重新加载统计数据 - } else { - toast({ - title: '点赞失败', - description: result.error || '未知错误', - variant: 'destructive', - }) - } - } - - // 处理点踩 - const handleDislike = async () => { - const result = await dislikePlugin(pluginId) - - if (result.success) { - toast({ title: '已反馈', description: '感谢你的反馈!' }) - loadStats() - } else { - toast({ - title: '操作失败', - description: result.error || '未知错误', - variant: 'destructive', - }) - } - } - - // 提交评分 - const handleSubmitRating = async () => { - if (userRating === 0) { - toast({ - title: '请选择评分', - description: '至少选择 1 颗星', - variant: 'destructive', - }) - return - } - - const result = await ratePlugin(pluginId, userRating, userComment || undefined) - - if (result.success) { - toast({ title: '评分成功', description: '感谢你的评价!' }) - setIsRatingDialogOpen(false) - setUserRating(0) - setUserComment('') - loadStats() - } else { - toast({ - title: '评分失败', - description: result.error || '未知错误', - variant: 'destructive', - }) - } - } - - if (loading) { - return ( -
-
- - - -
-
- - - -
-
- ) - } - - if (!stats) { - return null - } - - // 紧凑模式 - if (compact) { - return ( -
-
- - {stats.downloads.toLocaleString()} -
-
- - {stats.rating.toFixed(1)} -
-
- - {stats.likes} -
-
- ) - } - - // 完整模式 - return ( -
- {/* 统计数字 */} -
-
- - {stats.downloads.toLocaleString()} - 下载量 -
- -
- - {stats.rating.toFixed(1)} - {stats.rating_count} 条评价 -
- -
- - {stats.likes} - 点赞 -
- -
- - {stats.dislikes} - 点踩 -
-
- - {/* 操作按钮 */} -
- - - - - - - - - - - 为插件评分 - 分享你的使用体验,帮助其他用户 - - -
- {/* 星级评分 */} -
-
- {[1, 2, 3, 4, 5].map((star) => ( - - ))} -
- - {userRating === 0 && '点击星星进行评分'} - {userRating === 1 && '很差'} - {userRating === 2 && '一般'} - {userRating === 3 && '还行'} - {userRating === 4 && '不错'} - {userRating === 5 && '非常好'} - -
- - {/* 评论 */} -
- -