init
This commit is contained in:
94
README.md
94
README.md
@@ -1,93 +1 @@
|
||||
# official-chat
|
||||
|
||||
|
||||
|
||||
## Getting started
|
||||
|
||||
To make it easy for you to get started with GitLab, here's a list of recommended next steps.
|
||||
|
||||
Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)!
|
||||
|
||||
## Add your files
|
||||
|
||||
- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files
|
||||
- [ ] [Add files using the command line](https://docs.gitlab.com/topics/git/add_files/#add-files-to-a-git-repository) or push an existing Git repository with the following command:
|
||||
|
||||
```
|
||||
cd existing_repo
|
||||
git remote add origin https://gitlab.lbyc.dev/mujian/frontend/ugc-boilerplate/official-chat.git
|
||||
git branch -M main
|
||||
git push -uf origin main
|
||||
```
|
||||
|
||||
## Integrate with your tools
|
||||
|
||||
- [ ] [Set up project integrations](https://gitlab.lbyc.dev/mujian/frontend/ugc-boilerplate/official-chat/-/settings/integrations)
|
||||
|
||||
## Collaborate with your team
|
||||
|
||||
- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/)
|
||||
- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html)
|
||||
- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically)
|
||||
- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/)
|
||||
- [ ] [Set auto-merge](https://docs.gitlab.com/user/project/merge_requests/auto_merge/)
|
||||
|
||||
## Test and Deploy
|
||||
|
||||
Use the built-in continuous integration in GitLab.
|
||||
|
||||
- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/)
|
||||
- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/ee/user/application_security/sast/)
|
||||
- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html)
|
||||
- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/)
|
||||
- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html)
|
||||
|
||||
***
|
||||
|
||||
# Editing this README
|
||||
|
||||
When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thanks to [makeareadme.com](https://www.makeareadme.com/) for this template.
|
||||
|
||||
## Suggestions for a good README
|
||||
|
||||
Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
|
||||
|
||||
## Name
|
||||
Choose a self-explaining name for your project.
|
||||
|
||||
## Description
|
||||
Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.
|
||||
|
||||
## Badges
|
||||
On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.
|
||||
|
||||
## Visuals
|
||||
Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
|
||||
|
||||
## Installation
|
||||
Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.
|
||||
|
||||
## Usage
|
||||
Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.
|
||||
|
||||
## Support
|
||||
Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
|
||||
|
||||
## Roadmap
|
||||
If you have ideas for releases in the future, it is a good idea to list them in the README.
|
||||
|
||||
## Contributing
|
||||
State if you are open to contributions and what your requirements are for accepting them.
|
||||
|
||||
For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self.
|
||||
|
||||
You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser.
|
||||
|
||||
## Authors and acknowledgment
|
||||
Show your appreciation to those who have contributed to the project.
|
||||
|
||||
## License
|
||||
For open source projects, say how it is licensed.
|
||||
|
||||
## Project status
|
||||
If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.
|
||||
# 幕间官方Chat
|
||||
|
||||
322
biome.json
Normal file
322
biome.json
Normal file
@@ -0,0 +1,322 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.1.3/schema.json",
|
||||
"vcs": {
|
||||
"enabled": false,
|
||||
"clientKind": "git",
|
||||
"useIgnoreFile": false
|
||||
},
|
||||
"files": {
|
||||
"ignoreUnknown": false
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"indentStyle": "space",
|
||||
"indentWidth": 2
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": false
|
||||
},
|
||||
"includes": ["**", "!dist"]
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"quoteStyle": "single"
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"includes": ["**/*.{ts,tsx,js,jsx}"],
|
||||
"linter": {
|
||||
"rules": {
|
||||
"complexity": {
|
||||
"noAdjacentSpacesInRegex": "error",
|
||||
"noExtraBooleanCast": "error",
|
||||
"noUselessCatch": "error",
|
||||
"noUselessEscapeInRegex": "error"
|
||||
},
|
||||
"correctness": {
|
||||
"noConstAssign": "error",
|
||||
"noConstantCondition": "error",
|
||||
"noEmptyCharacterClassInRegex": "error",
|
||||
"noEmptyPattern": "error",
|
||||
"noGlobalObjectCalls": "error",
|
||||
"noInvalidBuiltinInstantiation": "error",
|
||||
"noInvalidConstructorSuper": "error",
|
||||
"noNonoctalDecimalEscape": "error",
|
||||
"noPrecisionLoss": "error",
|
||||
"noSelfAssign": "error",
|
||||
"noSetterReturn": "error",
|
||||
"noSwitchDeclarations": "error",
|
||||
"noUndeclaredVariables": "error",
|
||||
"noUnreachable": "error",
|
||||
"noUnreachableSuper": "error",
|
||||
"noUnsafeFinally": "error",
|
||||
"noUnsafeOptionalChaining": "error",
|
||||
"noUnusedLabels": "error",
|
||||
"noUnusedPrivateClassMembers": "error",
|
||||
"noUnusedVariables": "warn",
|
||||
"useIsNan": "error",
|
||||
"useValidForDirection": "error",
|
||||
"useValidTypeof": "error",
|
||||
"useYield": "error"
|
||||
},
|
||||
"suspicious": {
|
||||
"noAsyncPromiseExecutor": "error",
|
||||
"noCatchAssign": "error",
|
||||
"noClassAssign": "error",
|
||||
"noCompareNegZero": "error",
|
||||
"noControlCharactersInRegex": "error",
|
||||
"noDebugger": "error",
|
||||
"noDuplicateCase": "error",
|
||||
"noDuplicateClassMembers": "error",
|
||||
"noDuplicateElseIf": "error",
|
||||
"noDuplicateObjectKeys": "error",
|
||||
"noDuplicateParameters": "error",
|
||||
"noEmptyBlockStatements": "error",
|
||||
"noFallthroughSwitchClause": "error",
|
||||
"noFunctionAssign": "error",
|
||||
"noGlobalAssign": "error",
|
||||
"noImportAssign": "error",
|
||||
"noIrregularWhitespace": "error",
|
||||
"noMisleadingCharacterClass": "error",
|
||||
"noPrototypeBuiltins": "error",
|
||||
"noRedeclare": "error",
|
||||
"noShadowRestrictedNames": "error",
|
||||
"noSparseArray": "error",
|
||||
"noUnsafeNegation": "error",
|
||||
"noWith": "error",
|
||||
"useGetterReturn": "error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"includes": ["**/*.{ts,tsx,js,jsx}"],
|
||||
"javascript": {
|
||||
"globals": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"includes": ["**/*.{ts,tsx,js,jsx}"],
|
||||
"linter": {
|
||||
"rules": {
|
||||
"complexity": {
|
||||
"noArguments": "error"
|
||||
},
|
||||
"correctness": {
|
||||
"noConstAssign": "off",
|
||||
"noGlobalObjectCalls": "off",
|
||||
"noInvalidBuiltinInstantiation": "off",
|
||||
"noInvalidConstructorSuper": "off",
|
||||
"noSetterReturn": "off",
|
||||
"noUndeclaredVariables": "off",
|
||||
"noUnreachable": "off",
|
||||
"noUnreachableSuper": "off"
|
||||
},
|
||||
"style": {
|
||||
"useConst": "error"
|
||||
},
|
||||
"suspicious": {
|
||||
"noClassAssign": "off",
|
||||
"noDuplicateClassMembers": "off",
|
||||
"noDuplicateObjectKeys": "off",
|
||||
"noDuplicateParameters": "off",
|
||||
"noFunctionAssign": "off",
|
||||
"noImportAssign": "off",
|
||||
"noRedeclare": "off",
|
||||
"noUnsafeNegation": "off",
|
||||
"noVar": "error",
|
||||
"noWith": "off",
|
||||
"useGetterReturn": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"includes": ["**/*.{ts,tsx,js,jsx}"],
|
||||
"linter": {
|
||||
"rules": {
|
||||
"complexity": {
|
||||
"noUselessTypeConstraint": "error"
|
||||
},
|
||||
"correctness": {
|
||||
"noUnusedVariables": "warn"
|
||||
},
|
||||
"style": {
|
||||
"noCommonJs": "error",
|
||||
"noNamespace": "error",
|
||||
"useArrayLiterals": "error",
|
||||
"useAsConstAssertion": "error"
|
||||
},
|
||||
"suspicious": {
|
||||
"noExplicitAny": "error",
|
||||
"noExtraNonNullAssertion": "error",
|
||||
"noMisleadingInstantiator": "error",
|
||||
"noUnsafeDeclarationMerging": "error",
|
||||
"useNamespaceKeyword": "error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"includes": ["**/*.{ts,tsx,js,jsx}"],
|
||||
"linter": {
|
||||
"rules": {
|
||||
"correctness": {
|
||||
"useExhaustiveDependencies": "warn",
|
||||
"useHookAtTopLevel": "error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"includes": ["**/*.{ts,tsx,js,jsx}"],
|
||||
"linter": {
|
||||
"rules": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"includes": ["**/*.{ts,tsx,js,jsx}"],
|
||||
"javascript": {
|
||||
"globals": [
|
||||
"onanimationend",
|
||||
"ongamepadconnected",
|
||||
"onlostpointercapture",
|
||||
"onanimationiteration",
|
||||
"onkeyup",
|
||||
"onmousedown",
|
||||
"onanimationstart",
|
||||
"onslotchange",
|
||||
"onprogress",
|
||||
"ontransitionstart",
|
||||
"onpause",
|
||||
"onended",
|
||||
"onpointerover",
|
||||
"onscrollend",
|
||||
"onformdata",
|
||||
"ontransitionrun",
|
||||
"onanimationcancel",
|
||||
"ondrag",
|
||||
"onchange",
|
||||
"onbeforeinstallprompt",
|
||||
"onbeforexrselect",
|
||||
"onmessage",
|
||||
"ontransitioncancel",
|
||||
"onpointerdown",
|
||||
"onabort",
|
||||
"onpointerout",
|
||||
"oncuechange",
|
||||
"ongotpointercapture",
|
||||
"onscrollsnapchanging",
|
||||
"onsearch",
|
||||
"onsubmit",
|
||||
"onstalled",
|
||||
"onsuspend",
|
||||
"onreset",
|
||||
"onerror",
|
||||
"onresize",
|
||||
"onmouseenter",
|
||||
"ongamepaddisconnected",
|
||||
"ondragover",
|
||||
"onbeforetoggle",
|
||||
"onmouseover",
|
||||
"onpagehide",
|
||||
"onmousemove",
|
||||
"onratechange",
|
||||
"oncommand",
|
||||
"onmessageerror",
|
||||
"onwheel",
|
||||
"ondevicemotion",
|
||||
"onauxclick",
|
||||
"ontransitionend",
|
||||
"onpaste",
|
||||
"onpageswap",
|
||||
"ononline",
|
||||
"ondeviceorientationabsolute",
|
||||
"onkeydown",
|
||||
"onclose",
|
||||
"onselect",
|
||||
"onpageshow",
|
||||
"onpointercancel",
|
||||
"onbeforematch",
|
||||
"onpointerrawupdate",
|
||||
"ondragleave",
|
||||
"onscrollsnapchange",
|
||||
"onseeked",
|
||||
"onwaiting",
|
||||
"onbeforeunload",
|
||||
"onplaying",
|
||||
"onvolumechange",
|
||||
"ondragend",
|
||||
"onstorage",
|
||||
"onloadeddata",
|
||||
"onfocus",
|
||||
"onoffline",
|
||||
"onplay",
|
||||
"onafterprint",
|
||||
"onclick",
|
||||
"oncut",
|
||||
"onmouseout",
|
||||
"ondblclick",
|
||||
"oncanplay",
|
||||
"onloadstart",
|
||||
"onappinstalled",
|
||||
"onpointermove",
|
||||
"ontoggle",
|
||||
"oncontextmenu",
|
||||
"onblur",
|
||||
"oncancel",
|
||||
"onbeforeprint",
|
||||
"oncontextrestored",
|
||||
"onloadedmetadata",
|
||||
"onpointerup",
|
||||
"onlanguagechange",
|
||||
"oncopy",
|
||||
"onselectstart",
|
||||
"onscroll",
|
||||
"onload",
|
||||
"ondragstart",
|
||||
"onbeforeinput",
|
||||
"oncanplaythrough",
|
||||
"oninput",
|
||||
"oninvalid",
|
||||
"ontimeupdate",
|
||||
"ondurationchange",
|
||||
"onselectionchange",
|
||||
"onmouseup",
|
||||
"location",
|
||||
"onkeypress",
|
||||
"onpointerleave",
|
||||
"oncontextlost",
|
||||
"ondrop",
|
||||
"onsecuritypolicyviolation",
|
||||
"oncontentvisibilityautostatechange",
|
||||
"ondeviceorientation",
|
||||
"onseeking",
|
||||
"onrejectionhandled",
|
||||
"onunload",
|
||||
"onmouseleave",
|
||||
"onhashchange",
|
||||
"onpointerenter",
|
||||
"onmousewheel",
|
||||
"onunhandledrejection",
|
||||
"ondragenter",
|
||||
"onpopstate",
|
||||
"onpagereveal",
|
||||
"onemptied"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"assist": {
|
||||
"enabled": true,
|
||||
"actions": {
|
||||
"source": {
|
||||
"organizeImports": "on"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
13
index.html
Normal file
13
index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>幕间官方Chat</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
50
package.json
Normal file
50
package.json
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"name": "@mujian/official-chat",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=22.0.0",
|
||||
"pnpm": ">=10.0.0"
|
||||
},
|
||||
"packageManager": "pnpm@10.0.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"build-without-tsc": "vite build",
|
||||
"lint": "biome check --write ./src",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@heroui/react": "2.8.3",
|
||||
"@heroui/theme": "2.4.21",
|
||||
"@mujian/js-sdk": "0.0.6-beta.33",
|
||||
"@tailwindcss/vite": "4.1.12",
|
||||
"ahooks": "3.9.5",
|
||||
"axios": "1.11.0",
|
||||
"biome": "^0.3.3",
|
||||
"dayjs": "1.11.18",
|
||||
"lodash-es": "4.17.21",
|
||||
"lucide-react": "^0.546.0",
|
||||
"motion": "12.23.12",
|
||||
"react": "19.1.1",
|
||||
"react-dom": "19.1.1",
|
||||
"react-icons": "5.5.0",
|
||||
"react-markdown": "10.1.0",
|
||||
"react-router": "7.8.2",
|
||||
"tailwindcss": "4.1.12",
|
||||
"tailwindcss-animate": "1.0.7",
|
||||
"zustand": "5.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mujian/cli": "0.0.0",
|
||||
"@types/lodash-es": "4.17.12",
|
||||
"@types/node": "22.13.9",
|
||||
"@types/react": "19.1.10",
|
||||
"@types/react-dom": "19.1.7",
|
||||
"@vitejs/plugin-react": "5.0.0",
|
||||
"globals": "16.3.0",
|
||||
"typescript": "~5.8.3",
|
||||
"vite": "7.1.2"
|
||||
}
|
||||
}
|
||||
7039
pnpm-lock.yaml
generated
Normal file
7039
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
1
public/vite.svg
Normal file
1
public/vite.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
1
src/assets/react.svg
Normal file
1
src/assets/react.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
12
src/main.tsx
Normal file
12
src/main.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import '@/styles/fonts.css';
|
||||
import '@/styles/global.css';
|
||||
import { MujianProvider } from '@mujian/js-sdk/react';
|
||||
import { ReactRouterProvider } from './providers/RouterProvider.tsx';
|
||||
import '@mujian/js-sdk/react.css';
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<MujianProvider>
|
||||
<ReactRouterProvider />
|
||||
</MujianProvider>,
|
||||
);
|
||||
5
src/pages/.cursor/rules/pages_rules.mdc
Normal file
5
src/pages/.cursor/rules/pages_rules.mdc
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
48
src/pages/chat/MessageItem/components/EditActions.tsx
Normal file
48
src/pages/chat/MessageItem/components/EditActions.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { Button, ButtonGroup, cn } from '@heroui/react';
|
||||
|
||||
export type EditActionsProps = {
|
||||
isUser: boolean;
|
||||
isEditing: boolean;
|
||||
onSaveEdit: () => void;
|
||||
onCancelEdit: () => void;
|
||||
};
|
||||
|
||||
export const EditActions = ({
|
||||
isUser,
|
||||
isEditing,
|
||||
onSaveEdit,
|
||||
onCancelEdit,
|
||||
}: EditActionsProps) => {
|
||||
if (!isEditing) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
`flex w-full row-start-2 col-start-1 col-end-4`,
|
||||
isUser ? 'justify-end' : 'justify-start',
|
||||
)}
|
||||
>
|
||||
<ButtonGroup
|
||||
className="bg-white/15 text-default rounded-full"
|
||||
radius="full"
|
||||
size="sm"
|
||||
variant="light"
|
||||
>
|
||||
<Button
|
||||
className="text-default"
|
||||
size="sm"
|
||||
onPress={onSaveEdit}
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
<Button
|
||||
className="text-default"
|
||||
size="sm"
|
||||
onPress={onCancelEdit}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
124
src/pages/chat/MessageItem/components/MessageActions.tsx
Normal file
124
src/pages/chat/MessageItem/components/MessageActions.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
cn,
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
DropdownMenu,
|
||||
DropdownTrigger,
|
||||
} from '@heroui/react';
|
||||
import { ChevronLeftIcon, ChevronRightIcon, Ellipsis } from 'lucide-react';
|
||||
import React from 'react';
|
||||
import { useMessageActions } from './useMessageActions';
|
||||
|
||||
export type LastMessageActionsProps = {
|
||||
isUser: boolean;
|
||||
isLastMsg: boolean;
|
||||
isFirstMsg: boolean;
|
||||
isStreaming: boolean;
|
||||
isEditing: boolean;
|
||||
swipes: string[];
|
||||
activeSwipeId: number;
|
||||
onRegenerate: (() => Promise<void>) | undefined;
|
||||
onContinue: (() => Promise<void>) | undefined;
|
||||
onEditButtonClick: () => void;
|
||||
onSwipe: (direction: 'prev' | 'next') => void;
|
||||
onMessageTextClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
|
||||
onDelete: () => void;
|
||||
};
|
||||
|
||||
export const MessageActions = ({
|
||||
isUser,
|
||||
isLastMsg,
|
||||
isFirstMsg,
|
||||
isStreaming,
|
||||
isEditing,
|
||||
swipes,
|
||||
activeSwipeId,
|
||||
onRegenerate,
|
||||
onContinue,
|
||||
onEditButtonClick,
|
||||
onSwipe,
|
||||
onMessageTextClick,
|
||||
onDelete,
|
||||
}: LastMessageActionsProps) => {
|
||||
const dropdownItems = useMessageActions({
|
||||
isLastMsg,
|
||||
onEditButtonClick,
|
||||
onRegenerate,
|
||||
onContinue,
|
||||
onDelete,
|
||||
});
|
||||
|
||||
if (isStreaming || isUser || isEditing) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'message-actions flex justify-between row-start-2 col-start-1 col-end-4',
|
||||
)}
|
||||
role="presentation"
|
||||
onClick={onMessageTextClick} /* Stop propagation */
|
||||
>
|
||||
<div className="flex items-start">
|
||||
{!isFirstMsg && (
|
||||
<Dropdown placement="top-start" className="dark" size="sm">
|
||||
<DropdownTrigger>
|
||||
<Button
|
||||
variant="light"
|
||||
size="sm"
|
||||
className="bg-white/15 text-default min-w-10"
|
||||
>
|
||||
<Ellipsis size={14}/>
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu items={dropdownItems}>
|
||||
{(item) => (
|
||||
<DropdownItem
|
||||
key={item.key}
|
||||
color={item.color}
|
||||
className={item.className}
|
||||
onPress={item.onPress}
|
||||
>
|
||||
{item.label}
|
||||
</DropdownItem>
|
||||
)}
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-end">
|
||||
{isLastMsg && swipes.length > 1 && (
|
||||
<ButtonGroup
|
||||
className="bg-white/15 rounded-full backdrop-blur-sm text-default"
|
||||
radius="full"
|
||||
size="sm"
|
||||
variant="light"
|
||||
>
|
||||
<Button
|
||||
className="min-w-8 px-2 text-default"
|
||||
isDisabled={swipes.length <= 1}
|
||||
size="sm"
|
||||
onPress={() => onSwipe('prev')}
|
||||
>
|
||||
<ChevronLeftIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
<div className="flex items-center px-1 text-xs">
|
||||
{swipes.length > 0
|
||||
? `${activeSwipeId + 1}/${swipes.length}`
|
||||
: '1/1'}
|
||||
</div>
|
||||
<Button
|
||||
className="min-w-8 px-2 text-default"
|
||||
isDisabled={swipes.length <= 1}
|
||||
size="sm"
|
||||
onPress={() => onSwipe('next')}
|
||||
>
|
||||
<ChevronRightIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
99
src/pages/chat/MessageItem/components/MessageBubble.tsx
Normal file
99
src/pages/chat/MessageItem/components/MessageBubble.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
import {
|
||||
cn,
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
DropdownMenu,
|
||||
DropdownTrigger,
|
||||
Spinner,
|
||||
} from '@heroui/react';
|
||||
import { type Message as BaseMessage, MdRenderer } from '@mujian/js-sdk/react';
|
||||
import { MessageEditArea } from './MessageEditArea';
|
||||
import { useMessageActions } from './useMessageActions';
|
||||
|
||||
export type MessageBubbleProps = {
|
||||
message: BaseMessage;
|
||||
isUser: boolean;
|
||||
isEditing: boolean;
|
||||
editedMessage: string;
|
||||
onEditChange: (content: string) => void;
|
||||
isStreaming: boolean;
|
||||
currentMessage: string;
|
||||
onEditButtonClick: () => void;
|
||||
onDelete: () => void;
|
||||
onEditAreaShow: () => void;
|
||||
};
|
||||
|
||||
export const MessageBubble = ({
|
||||
isUser,
|
||||
isEditing,
|
||||
editedMessage,
|
||||
onEditChange,
|
||||
isStreaming,
|
||||
currentMessage,
|
||||
onEditButtonClick,
|
||||
onDelete,
|
||||
onEditAreaShow,
|
||||
}: MessageBubbleProps) => {
|
||||
const dropdownItems = useMessageActions({
|
||||
isLastMsg: false,
|
||||
onEditButtonClick,
|
||||
onDelete,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'row-start-1 col-start-1 col-end-4 flex flex-col size-full min-w-0 message-wrapper',
|
||||
isUser ? 'items-end' : 'items-stretch',
|
||||
)}
|
||||
>
|
||||
{!isEditing ? (
|
||||
isUser ? (
|
||||
<Dropdown placement="top-end" className="dark">
|
||||
<DropdownTrigger>
|
||||
<div
|
||||
className={cn(
|
||||
'h-full rounded-xl dark:prose-invert text-blue-100 p-4 bg-black/50',
|
||||
{
|
||||
'rounded-br-sm': isUser,
|
||||
},
|
||||
)}
|
||||
role="presentation"
|
||||
>
|
||||
<MdRenderer content={currentMessage} />
|
||||
</div>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu items={dropdownItems}>
|
||||
{(item) => (
|
||||
<DropdownItem
|
||||
key={item.key}
|
||||
color={item.color}
|
||||
className={item.className}
|
||||
onPress={item.onPress}
|
||||
>
|
||||
{item.label}
|
||||
</DropdownItem>
|
||||
)}
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
) : (
|
||||
<div className="py-4">
|
||||
<MdRenderer content={currentMessage} />
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<MessageEditArea
|
||||
isUser={isUser}
|
||||
editedMessage={editedMessage}
|
||||
onEditChange={onEditChange}
|
||||
onEditAreaShow={onEditAreaShow}
|
||||
/>
|
||||
)}
|
||||
{isStreaming && !currentMessage?.length && (
|
||||
<div className="flex w-full items-center">
|
||||
<Spinner variant="dots" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
64
src/pages/chat/MessageItem/components/MessageEditArea.tsx
Normal file
64
src/pages/chat/MessageItem/components/MessageEditArea.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import { cn } from '@heroui/react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
export type MessageEditAreaProps = {
|
||||
isUser: boolean;
|
||||
editedMessage: string;
|
||||
onEditChange: (content: string) => void;
|
||||
onEditAreaShow: () => void;
|
||||
};
|
||||
|
||||
export const MessageEditArea = ({
|
||||
isUser,
|
||||
editedMessage,
|
||||
onEditChange,
|
||||
onEditAreaShow,
|
||||
}: MessageEditAreaProps) => {
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const [wrapHeight, setWrapHeight] = useState(0);
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: useMount
|
||||
useEffect(() => {
|
||||
const t = textareaRef.current;
|
||||
if (t) {
|
||||
const parent = t.parentElement;
|
||||
console.log(parent);
|
||||
|
||||
t.style.height = 'auto';
|
||||
t.style.height = t.scrollHeight + 'px';
|
||||
if (parent) {
|
||||
parent.style.height = t?.scrollHeight + 'px';
|
||||
}
|
||||
|
||||
t.focus();
|
||||
// 光标移到最后
|
||||
const length = t.value.length;
|
||||
t.setSelectionRange(length, length);
|
||||
onEditAreaShow();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full"
|
||||
style={{
|
||||
height: wrapHeight,
|
||||
}}
|
||||
>
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
className={cn(
|
||||
'w-full outline-none resize-none h-fit overflow-hidden p-4 bg-background rounded-xl text-[16px]',
|
||||
isUser ? 'rounded-br-sm' : 'rounded-bl-sm',
|
||||
)}
|
||||
value={editedMessage}
|
||||
onChange={(e) => {
|
||||
onEditChange(e.target.value);
|
||||
e.target.style.height = '5px';
|
||||
e.target.style.height = `${e.target.scrollHeight}px`;
|
||||
setWrapHeight(e.target.scrollHeight);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
61
src/pages/chat/MessageItem/components/useMessageActions.ts
Normal file
61
src/pages/chat/MessageItem/components/useMessageActions.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
export type DropdownItem = {
|
||||
key: string;
|
||||
label: string;
|
||||
onPress: () => void | Promise<void>;
|
||||
color?:
|
||||
| 'default'
|
||||
| 'primary'
|
||||
| 'secondary'
|
||||
| 'success'
|
||||
| 'warning'
|
||||
| 'danger';
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export type UseDropdownItemsProps = {
|
||||
isLastMsg: boolean;
|
||||
onEditButtonClick: () => void;
|
||||
onRegenerate?: (() => Promise<void>) | undefined;
|
||||
onContinue?: (() => Promise<void>) | undefined;
|
||||
onDelete: () => void;
|
||||
};
|
||||
|
||||
export const useMessageActions = ({
|
||||
isLastMsg,
|
||||
onEditButtonClick,
|
||||
onRegenerate,
|
||||
onContinue,
|
||||
onDelete,
|
||||
}: UseDropdownItemsProps): DropdownItem[] => {
|
||||
const dropdownItems: DropdownItem[] = [
|
||||
{
|
||||
key: 'edit',
|
||||
label: '编辑',
|
||||
onPress: onEditButtonClick,
|
||||
},
|
||||
...(isLastMsg
|
||||
? [
|
||||
{
|
||||
key: 'regenerate',
|
||||
label: '重说',
|
||||
onPress: async () => await onRegenerate?.(),
|
||||
},
|
||||
{
|
||||
key: 'continue',
|
||||
label: '继续',
|
||||
onPress: async () => await onContinue?.(),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
key: 'delete',
|
||||
label: '删除',
|
||||
onPress: onDelete,
|
||||
color: 'danger',
|
||||
className: 'text-danger',
|
||||
},
|
||||
];
|
||||
|
||||
return dropdownItems;
|
||||
};
|
||||
|
||||
217
src/pages/chat/MessageItem/index.tsx
Normal file
217
src/pages/chat/MessageItem/index.tsx
Normal file
@@ -0,0 +1,217 @@
|
||||
import { cn } from '@heroui/react';
|
||||
import { type Message as BaseMessage } from '@mujian/js-sdk/react';
|
||||
import React, { type MouseEventHandler } from 'react';
|
||||
import { EditActions } from './components/EditActions';
|
||||
import { MessageActions } from './components/MessageActions';
|
||||
import { MessageBubble } from './components/MessageBubble';
|
||||
|
||||
export type MessageItemProps = {
|
||||
message: BaseMessage;
|
||||
originalMessage: BaseMessage;
|
||||
|
||||
index: number;
|
||||
avatar: string;
|
||||
name: string;
|
||||
isLastMsg?: boolean;
|
||||
isFirstMsg?: boolean;
|
||||
|
||||
onRegenerate?: () => Promise<void>;
|
||||
onContinue?: () => Promise<void>;
|
||||
onDelete: (messageId: string) => Promise<void>;
|
||||
onEdit: (messageId: string, content: string) => Promise<void>;
|
||||
onSwipe: (messageId: string, swipeId: number) => Promise<void>;
|
||||
sendMessage?: (message: string) => Promise<void>;
|
||||
};
|
||||
|
||||
// Add comparison function for React.memo
|
||||
const arePropsEqual = (
|
||||
prevProps: MessageItemProps,
|
||||
nextProps: MessageItemProps,
|
||||
) => {
|
||||
// 如果 swipes 长度不同,直接返回 false
|
||||
if (prevProps.message.swipes?.length !== nextProps.message.swipes?.length) {
|
||||
return false;
|
||||
}
|
||||
// 如果 swipes 内容不同,直接返回 false
|
||||
if (
|
||||
prevProps.message.swipes?.some(
|
||||
(swipe, index) => swipe !== nextProps.message.swipes[index],
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 其他属性比较
|
||||
return (
|
||||
prevProps.avatar === nextProps.avatar &&
|
||||
prevProps.message.content === nextProps.message.content &&
|
||||
prevProps.message.role === nextProps.message.role &&
|
||||
prevProps.message.isStreaming === nextProps.message.isStreaming &&
|
||||
prevProps.name === nextProps.name &&
|
||||
prevProps.isLastMsg === nextProps.isLastMsg &&
|
||||
prevProps.message.sendAt === nextProps.message.sendAt &&
|
||||
prevProps.message.activeSwipeId === nextProps.message.activeSwipeId &&
|
||||
prevProps.message.id === nextProps.message.id &&
|
||||
prevProps.index === nextProps.index
|
||||
);
|
||||
};
|
||||
|
||||
export const MessageItem = React.memo((props: MessageItemProps) => {
|
||||
const {
|
||||
message,
|
||||
originalMessage,
|
||||
|
||||
// index,
|
||||
// avatar,
|
||||
// name,
|
||||
isLastMsg,
|
||||
isFirstMsg,
|
||||
onRegenerate,
|
||||
onContinue,
|
||||
onDelete,
|
||||
onEdit,
|
||||
onSwipe,
|
||||
} = props;
|
||||
const {
|
||||
id,
|
||||
|
||||
role,
|
||||
isStreaming,
|
||||
|
||||
content,
|
||||
swipes = [],
|
||||
activeSwipeId,
|
||||
|
||||
// sendAt,
|
||||
} = message;
|
||||
const isUser = role === 'user';
|
||||
|
||||
const messageRef = React.useRef<HTMLDivElement>(null); // 使用 useRef 获取当前组件的引用
|
||||
const [isEditing, setIsEditing] = React.useState(false); // State to toggle edit mode
|
||||
const [editedMessage, setEditedMessage] = React.useState(content);
|
||||
|
||||
const renderedMessage = isUser ? content : swipes[activeSwipeId];
|
||||
|
||||
// 滚动到消息可见位置的通用函数
|
||||
const scrollMessageIntoView = (
|
||||
alignToBlock: ScrollLogicalPosition = 'end',
|
||||
behavior: ScrollBehavior = 'instant',
|
||||
) => {
|
||||
if (messageRef.current) {
|
||||
messageRef.current.scrollIntoView({
|
||||
behavior: behavior,
|
||||
block: alignToBlock,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveEdit = async () => {
|
||||
if (id && activeSwipeId !== undefined) {
|
||||
await onEdit(id, editedMessage);
|
||||
setIsEditing(false);
|
||||
|
||||
setTimeout(() => scrollMessageIntoView(), 50);
|
||||
}
|
||||
};
|
||||
|
||||
// Separate function to handle message deletion
|
||||
const handleDeleteMessage = async () => {
|
||||
if (id) {
|
||||
onDelete(id);
|
||||
}
|
||||
};
|
||||
// console.log("content", index, content);
|
||||
|
||||
// 确保 currentSwipeId 在有效范围内
|
||||
const validSwipeId = Math.min(Math.max(0, activeSwipeId), swipes.length - 1);
|
||||
|
||||
const handleSwipe = async (direction: 'prev' | 'next') => {
|
||||
if (swipes.length <= 1) return;
|
||||
|
||||
// 简化计算逻辑,直接使用模运算确保能够循环浏览所有页面
|
||||
const newSwipeId =
|
||||
direction === 'prev'
|
||||
? (activeSwipeId - 1 + swipes.length) % swipes.length
|
||||
: (activeSwipeId + 1) % swipes.length;
|
||||
|
||||
await onSwipe(id, newSwipeId);
|
||||
|
||||
scrollMessageIntoView();
|
||||
};
|
||||
|
||||
// Stop propagation for message text clicks
|
||||
const handleMessageTextClick: MouseEventHandler<HTMLDivElement> = (e) => {
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
const handleEditButtonClick = async () => {
|
||||
setIsEditing(true);
|
||||
if (isUser) {
|
||||
setEditedMessage(originalMessage.content);
|
||||
} else {
|
||||
setEditedMessage(originalMessage.swipes[validSwipeId]);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`message-${activeSwipeId}`}
|
||||
ref={messageRef} // 将 ref 绑定到根容器
|
||||
className={cn(
|
||||
'grid items-end gap-2 mb-2 w-full grid-cols-[32px_1fr_32px]',
|
||||
)}
|
||||
onContextMenu={(event) => {
|
||||
// 按ctrl点击时打印ID
|
||||
if (event.ctrlKey) {
|
||||
event.preventDefault();
|
||||
console.log('Message ID:', id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MessageBubble
|
||||
message={message}
|
||||
isUser={isUser}
|
||||
isEditing={isEditing}
|
||||
editedMessage={editedMessage}
|
||||
onEditChange={setEditedMessage}
|
||||
isStreaming={isStreaming}
|
||||
currentMessage={renderedMessage}
|
||||
onEditButtonClick={handleEditButtonClick}
|
||||
onDelete={handleDeleteMessage}
|
||||
onEditAreaShow={() => scrollMessageIntoView()}
|
||||
/>
|
||||
<EditActions
|
||||
isUser={isUser}
|
||||
isEditing={isEditing}
|
||||
onSaveEdit={handleSaveEdit}
|
||||
onCancelEdit={() => {
|
||||
setIsEditing(false);
|
||||
setEditedMessage(
|
||||
isUser
|
||||
? originalMessage.content
|
||||
: originalMessage.swipes[activeSwipeId],
|
||||
);
|
||||
// 滚动到消息位置
|
||||
setTimeout(() => scrollMessageIntoView('nearest', 'smooth'), 10);
|
||||
}}
|
||||
/>
|
||||
<MessageActions
|
||||
isUser={isUser}
|
||||
isLastMsg={Boolean(isLastMsg)}
|
||||
isFirstMsg={Boolean(isFirstMsg)}
|
||||
isStreaming={isStreaming}
|
||||
isEditing={isEditing}
|
||||
swipes={swipes}
|
||||
activeSwipeId={activeSwipeId}
|
||||
onRegenerate={onRegenerate}
|
||||
onContinue={onContinue}
|
||||
onEditButtonClick={handleEditButtonClick}
|
||||
onSwipe={handleSwipe}
|
||||
onMessageTextClick={handleMessageTextClick}
|
||||
onDelete={handleDeleteMessage}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}, arePropsEqual);
|
||||
|
||||
MessageItem.displayName = 'Message';
|
||||
128
src/pages/chat/MessageList.tsx
Normal file
128
src/pages/chat/MessageList.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
import type { Message as MessageType } from '@mujian/js-sdk/react';
|
||||
import { Thread, type VListHandle } from '@mujian/js-sdk/react';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import { MessageItem } from './MessageItem/';
|
||||
|
||||
export type MsgProps = {
|
||||
data: MessageType[];
|
||||
onRegenerate?: () => Promise<void>;
|
||||
sendMessage?: (message: string) => Promise<void>;
|
||||
onContinue?: () => Promise<void>;
|
||||
onDelete: (messageId: string) => Promise<void>;
|
||||
onEdit: (messageId: string, content: string) => Promise<void>;
|
||||
onSwipe: (messageId: string, swipeId: number) => Promise<void>;
|
||||
};
|
||||
|
||||
const OVERSCAN = 10;
|
||||
|
||||
export const MessageList = React.memo(
|
||||
({
|
||||
data,
|
||||
onRegenerate,
|
||||
sendMessage,
|
||||
onContinue,
|
||||
onDelete,
|
||||
onEdit,
|
||||
onSwipe,
|
||||
}: MsgProps) => {
|
||||
const prevLengthRef = useRef(0);
|
||||
const { activePersona, projectInfo } = useGlobalStore();
|
||||
const vListRef = useRef<VListHandle>(null);
|
||||
const [visibleRange, setVisibleRange] = useState<[number, number]>([0, 0]);
|
||||
|
||||
useEffect(() => {
|
||||
const vl = vListRef.current;
|
||||
if (!vl) {
|
||||
return;
|
||||
}
|
||||
const isScrolledBottom = () =>
|
||||
vl.viewportSize + vl.scrollOffset + 16 >= vl.scrollSize;
|
||||
const isFirstLoading = prevLengthRef.current === 0;
|
||||
|
||||
const shouldScroll = [
|
||||
isFirstLoading, // 首次加载
|
||||
data.length > prevLengthRef.current, // 新消息
|
||||
data[data.length - 1]?.isStreaming && isScrolledBottom(), // 正在生成
|
||||
].some(Boolean);
|
||||
|
||||
if (shouldScroll) {
|
||||
vListRef.current?.scrollTo(isFirstLoading ? 99999999 : vl.scrollSize);
|
||||
}
|
||||
|
||||
prevLengthRef.current = data.length;
|
||||
}, [data]);
|
||||
|
||||
const onScroll = () => {
|
||||
const vl = vListRef.current;
|
||||
if (!vl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const startIndex = vl.findStartIndex();
|
||||
const endIndex = vl.findEndIndex();
|
||||
setVisibleRange([startIndex, endIndex]);
|
||||
};
|
||||
|
||||
const keepMountedList = useMemo(() => {
|
||||
if (visibleRange[0] === 0 && visibleRange[1] === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const overscan = 10;
|
||||
const from = Math.max(visibleRange[0] - overscan, 0);
|
||||
const to = Math.min(visibleRange[1] + overscan, data.length - 1);
|
||||
const array = [];
|
||||
for (let i = from; i <= to; i++) {
|
||||
array.push(i);
|
||||
}
|
||||
return array;
|
||||
}, [visibleRange[0], visibleRange[1], data.length]);
|
||||
|
||||
return (
|
||||
<Thread.MessageList
|
||||
ref={vListRef}
|
||||
messages={data}
|
||||
vListProps={{
|
||||
reverse: false,
|
||||
overscan: OVERSCAN,
|
||||
keepMounted: keepMountedList,
|
||||
onScroll,
|
||||
}}
|
||||
>
|
||||
{(props) => (
|
||||
<Thread.MessageItem key={props.message.id} {...props}>
|
||||
{(msg, originalMessage) => (
|
||||
<MessageItem
|
||||
key={msg.id}
|
||||
message={msg}
|
||||
originalMessage={originalMessage}
|
||||
avatar={
|
||||
msg.role === 'user'
|
||||
? activePersona?.avatarUrl || ''
|
||||
: projectInfo?.coverImageUrl || ''
|
||||
}
|
||||
name={
|
||||
msg.role === 'user'
|
||||
? activePersona?.name || ''
|
||||
: projectInfo?.title || ''
|
||||
}
|
||||
index={props.index}
|
||||
isFirstMsg={props.index === 0}
|
||||
isLastMsg={props.depth === 0}
|
||||
sendMessage={sendMessage}
|
||||
onContinue={onContinue}
|
||||
onDelete={onDelete}
|
||||
onEdit={onEdit}
|
||||
onSwipe={onSwipe}
|
||||
onRegenerate={onRegenerate}
|
||||
/>
|
||||
)}
|
||||
</Thread.MessageItem>
|
||||
)}
|
||||
</Thread.MessageList>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
MessageList.displayName = 'MessageList';
|
||||
107
src/pages/chat/MsgSend.tsx
Normal file
107
src/pages/chat/MsgSend.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import { Button, Textarea } from '@heroui/react';
|
||||
import { CircleStopIcon, SendIcon } from 'lucide-react';
|
||||
|
||||
interface Props {
|
||||
onSend: (query: string) => void;
|
||||
onStop: () => void;
|
||||
running?: boolean;
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
|
||||
export const isMobile = ((): boolean => {
|
||||
const userAgent =
|
||||
typeof window !== 'undefined'
|
||||
? window?.navigator?.userAgent?.toLowerCase()
|
||||
: '';
|
||||
const mobileRegex =
|
||||
/android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini|mobile|tablet/i;
|
||||
|
||||
return mobileRegex.test(userAgent);
|
||||
})();
|
||||
|
||||
export const MsgSend = ({
|
||||
onSend,
|
||||
onStop,
|
||||
running = false,
|
||||
value,
|
||||
onChange,
|
||||
}: Props) => {
|
||||
const isEmptyInput = value.trim().length === 0;
|
||||
|
||||
const handleSend = () => {
|
||||
if (value) {
|
||||
onSend(value);
|
||||
onChange(''); // 发送后清空输入框
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Textarea
|
||||
className="dark"
|
||||
classNames={{
|
||||
label: 'text-black/50 dark:text-white/90',
|
||||
input: [
|
||||
'bg-transparent',
|
||||
'text-black/90 dark:text-white/90',
|
||||
'placeholder:text-default-700/50 dark:placeholder:text-white/60',
|
||||
],
|
||||
innerWrapper: 'bg-transparent items-center',
|
||||
inputWrapper: ['shadow-xl', 'bg-dark/40', 'dark:bg-black/40'],
|
||||
mainWrapper: 'px-2',
|
||||
}}
|
||||
disabled={running}
|
||||
endContent={
|
||||
running ? (
|
||||
<Button
|
||||
isIconOnly
|
||||
color="primary"
|
||||
radius="full"
|
||||
size="sm"
|
||||
onPress={() => {
|
||||
onStop();
|
||||
}}
|
||||
>
|
||||
<CircleStopIcon className="w-5 h-5" />
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
isIconOnly
|
||||
color="primary"
|
||||
radius="full"
|
||||
size="sm"
|
||||
onPress={handleSend}
|
||||
isDisabled={isEmptyInput}
|
||||
>
|
||||
<SendIcon className="w-5 h-5" />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
maxLength={1000}
|
||||
maxRows={4}
|
||||
minRows={1}
|
||||
placeholder={running ? '正在生成...' : '按 Enter 发送'}
|
||||
radius="lg"
|
||||
size="lg"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (
|
||||
e.key === 'Enter' &&
|
||||
!isMobile &&
|
||||
!e.ctrlKey &&
|
||||
!e.shiftKey &&
|
||||
!e.altKey &&
|
||||
!e.metaKey &&
|
||||
!e.nativeEvent.isComposing &&
|
||||
!isEmptyInput
|
||||
) {
|
||||
e.preventDefault();
|
||||
handleSend();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
169
src/pages/chat/index.tsx
Normal file
169
src/pages/chat/index.tsx
Normal file
@@ -0,0 +1,169 @@
|
||||
import { Alert, ScrollShadow, Spinner } from '@heroui/react';
|
||||
import { useChat, useMujian } from '@mujian/js-sdk/react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import { MessageList } from './MessageList';
|
||||
import { MsgSend } from './MsgSend';
|
||||
|
||||
// 扩展Window接口以包含chat对象
|
||||
declare global {
|
||||
interface Window {
|
||||
$mj_engine: {
|
||||
chat: {
|
||||
complete?: (message: string) => Promise<void>;
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
window.$mj_engine = {
|
||||
chat: {},
|
||||
};
|
||||
|
||||
export const Chat = () => {
|
||||
const { init, projectInfo } = useGlobalStore();
|
||||
const mujian = useMujian();
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const {
|
||||
messages,
|
||||
status,
|
||||
error,
|
||||
append,
|
||||
continueGenerate,
|
||||
regenerate,
|
||||
stop,
|
||||
deleteMessage,
|
||||
editMessage,
|
||||
setSwipe,
|
||||
} = useChat({
|
||||
onError: (e) => {
|
||||
console.error(e);
|
||||
},
|
||||
});
|
||||
|
||||
// 自定义删除消息函数
|
||||
const handleDeleteMessage = async (messageId: string) => {
|
||||
// 找到要删除的消息
|
||||
const messageIndex = messages.findIndex((msg) => msg.id === messageId);
|
||||
if (messageIndex === -1) return;
|
||||
|
||||
let deletedUserMessageContent = '';
|
||||
|
||||
// 检查前一条消息是否是用户消息且被删除的消息是AI消息
|
||||
if (messageIndex > 0 && messages[messageIndex].role === 'assistant') {
|
||||
const prevMessage = messages[messageIndex - 1];
|
||||
if (prevMessage.role === 'user') {
|
||||
// 保存用户消息内容
|
||||
deletedUserMessageContent = prevMessage.content;
|
||||
// 同时删除前一条用户消息
|
||||
deleteMessage(prevMessage.id);
|
||||
}
|
||||
}
|
||||
|
||||
// 删除当前消息
|
||||
await deleteMessage(messageId);
|
||||
|
||||
// 如果有删除的用户消息,将其内容填入发送框
|
||||
if (deletedUserMessageContent) {
|
||||
setInputValue(deletedUserMessageContent);
|
||||
}
|
||||
};
|
||||
|
||||
const paddingTop = useMemo(() => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
return parseInt(params.get('insetTop') || '0', 10);
|
||||
}, []);
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: onMount ONLY
|
||||
useEffect(() => {
|
||||
init(mujian);
|
||||
}, []);
|
||||
|
||||
const onSend = append;
|
||||
|
||||
// 将chat对象挂载到window上
|
||||
useEffect(() => {
|
||||
window.$mj_engine.chat.complete = async (message: string) => {
|
||||
await onSend(message);
|
||||
};
|
||||
|
||||
return () => {
|
||||
delete window.$mj_engine.chat.complete;
|
||||
};
|
||||
}, [onSend]);
|
||||
|
||||
const handleInputChange = (value: string) => {
|
||||
setInputValue(value);
|
||||
};
|
||||
|
||||
if (status === 'uninitialized') {
|
||||
return (
|
||||
<div className="w-full h-full flex justify-center items-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const errorMessage =
|
||||
Boolean(error) &&
|
||||
(typeof error === 'object' &&
|
||||
error &&
|
||||
'message' in error &&
|
||||
typeof error.message === 'string'
|
||||
? error.message
|
||||
: '哎呀,发生了未知错误,刷新页面再试试吧~');
|
||||
|
||||
return (
|
||||
<div className="size-full relative">
|
||||
<div
|
||||
className="h-full opacity-30 blur-xs object-cover absolute top-0 left-1/2 w-[min(640px,100%)] -translate-x-1/2 bg-cover bg-top bg-no-repeat [mask-image:linear-gradient(to_right,transparent,black_100px,black_calc(100%-100px),transparent)]"
|
||||
style={{
|
||||
backgroundImage: projectInfo
|
||||
? `url(${projectInfo.coverImageUrl})`
|
||||
: undefined,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="size-full z-10 p-3 flex flex-col"
|
||||
style={{
|
||||
paddingTop,
|
||||
}}
|
||||
>
|
||||
<ScrollShadow hideScrollBar className="h-full" size={20}>
|
||||
<MessageList
|
||||
data={messages}
|
||||
sendMessage={async (q) => {
|
||||
await onSend(q);
|
||||
}}
|
||||
onContinue={continueGenerate}
|
||||
onDelete={handleDeleteMessage}
|
||||
onEdit={editMessage}
|
||||
onSwipe={setSwipe}
|
||||
onRegenerate={regenerate}
|
||||
/>
|
||||
{errorMessage && (
|
||||
<Alert
|
||||
hideIcon
|
||||
className="mt-2 mb-6"
|
||||
classNames={{
|
||||
mainWrapper: 'min-h-4 ml-0',
|
||||
}}
|
||||
color="danger"
|
||||
title={errorMessage}
|
||||
variant="bordered"
|
||||
/>
|
||||
)}
|
||||
</ScrollShadow>
|
||||
<div className="flex flex-col gap-2 mt-2">
|
||||
<MsgSend
|
||||
running={status === 'streaming'}
|
||||
onSend={onSend}
|
||||
onStop={stop}
|
||||
value={inputValue}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
14
src/providers/RouterProvider.tsx
Normal file
14
src/providers/RouterProvider.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { createBrowserRouter } from 'react-router';
|
||||
import { RouterProvider } from 'react-router/dom';
|
||||
import { Chat } from '@/pages/chat';
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: '/',
|
||||
element: <Chat />,
|
||||
},
|
||||
]);
|
||||
|
||||
export const ReactRouterProvider = () => {
|
||||
return <RouterProvider router={router} />;
|
||||
};
|
||||
5
src/store/.cursor/rules/store_rules.mdc
Normal file
5
src/store/.cursor/rules/store_rules.mdc
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
23
src/store/global.tsx
Normal file
23
src/store/global.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { MujianSdk } from '@mujian/js-sdk';
|
||||
import type { PersonaInfo, ProjectInfo } from '@mujian/js-sdk/types';
|
||||
import { create } from 'zustand';
|
||||
|
||||
type GlobalState = {
|
||||
projectInfo: ProjectInfo | null;
|
||||
activePersona: PersonaInfo | null;
|
||||
init: (mujian: MujianSdk) => Promise<void>;
|
||||
};
|
||||
|
||||
export const useGlobalStore = create<GlobalState>((set) => ({
|
||||
count: 0,
|
||||
projectInfo: null,
|
||||
activePersona: null,
|
||||
|
||||
init: async (mujian: MujianSdk) => {
|
||||
const [projectInfo, persona] = await Promise.all([
|
||||
mujian.ai.chat.project.getInfo(),
|
||||
mujian.ai.chat.settings.persona.getActive(),
|
||||
]);
|
||||
set({ projectInfo, activePersona: persona });
|
||||
},
|
||||
}));
|
||||
0
src/styles/fonts.css
Normal file
0
src/styles/fonts.css
Normal file
33
src/styles/global.css
Normal file
33
src/styles/global.css
Normal file
@@ -0,0 +1,33 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@config "../../tailwind.config.js";
|
||||
|
||||
@theme {
|
||||
--breakpoint-xs: 30rem;
|
||||
--breakpoint-2xs: 20rem;
|
||||
}
|
||||
|
||||
:root {
|
||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
||||
background: #18181b;
|
||||
}
|
||||
|
||||
:root,
|
||||
body,
|
||||
#root {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.mes_text> :last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
46
tailwind.config.js
Normal file
46
tailwind.config.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import { heroui } from '@heroui/theme';
|
||||
import animate from 'tailwindcss-animate';
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
'./node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}',
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ['var(--font-sans)'],
|
||||
mono: ['var(--font-mono)'],
|
||||
},
|
||||
backgroundImage: {
|
||||
modalMark:
|
||||
'linear-gradient(to top, rgba(0, 0, 0) 0%, rgba(255, 255, 255, 0.9) 50%, rgba(255, 255, 255, 0.8) 68%, rgba(255, 255, 255, 0.65) 82%, rgba(255, 255, 255, 0) 100%)',
|
||||
},
|
||||
},
|
||||
},
|
||||
darkMode: ['class'],
|
||||
plugins: [
|
||||
heroui({
|
||||
addCommonColors: true,
|
||||
// defaultTheme: "light", // default theme from the themes object
|
||||
// defaultExtendTheme: "light", // default theme to extend on custom themes
|
||||
themes: {
|
||||
light: {
|
||||
colors: {
|
||||
primary: { DEFAULT: '#EC4342' },
|
||||
background: { DEFAULT: '#18181B' },
|
||||
},
|
||||
},
|
||||
dark: {
|
||||
colors: {
|
||||
primary: { DEFAULT: '#EC4342' },
|
||||
background: { DEFAULT: '#18181B' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
animate,
|
||||
],
|
||||
};
|
||||
31
tsconfig.app.json
Normal file
31
tsconfig.app.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
7
tsconfig.json
Normal file
7
tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
25
tsconfig.node.json
Normal file
25
tsconfig.node.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2023",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
14
vite.config.ts
Normal file
14
vite.config.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import path from 'path';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user