first_commit
This commit is contained in:
29
.gitignore
vendored
Normal file
29
.gitignore
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
# Logs
|
||||
logs
|
||||
*.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/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
|
||||
pnpm-lock.yaml
|
||||
yarn.lock
|
||||
package-lock.json
|
||||
bun.lockb
|
||||
5
.vscode/settings.json
vendored
Normal file
5
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"tailwindCSS.experimental.classRegex": [
|
||||
["([\"'`][^\"'`]*.*?[\"'`])", "[\"'`]([^\"'`]*).*?[\"'`]"]
|
||||
],
|
||||
}
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Next UI
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
50
README.md
Normal file
50
README.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Vite & HeroUI Template
|
||||
|
||||
This is a template for creating applications using Vite and HeroUI (v2).
|
||||
|
||||
[Try it on CodeSandbox](https://githubbox.com/frontio-ai/vite-template)
|
||||
|
||||
## Technologies Used
|
||||
|
||||
- [Vite](https://vitejs.dev/guide/)
|
||||
- [HeroUI](https://heroui.com)
|
||||
- [Tailwind CSS](https://tailwindcss.com)
|
||||
- [Tailwind Variants](https://tailwind-variants.org)
|
||||
- [TypeScript](https://www.typescriptlang.org)
|
||||
- [Framer Motion](https://www.framer.com/motion)
|
||||
|
||||
## How to Use
|
||||
|
||||
To clone the project, run the following command:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/frontio-ai/vite-template.git
|
||||
```
|
||||
|
||||
### Install dependencies
|
||||
|
||||
You can use one of them `npm`, `yarn`, `pnpm`, `bun`, Example using `npm`:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### Run the development server
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Setup pnpm (optional)
|
||||
|
||||
If you are using `pnpm`, you need to add the following code to your `.npmrc` file:
|
||||
|
||||
```bash
|
||||
public-hoist-pattern[]=*@heroui/*
|
||||
```
|
||||
|
||||
After modifying the `.npmrc` file, you need to run `pnpm install` again to ensure that the dependencies are installed correctly.
|
||||
|
||||
## License
|
||||
|
||||
Licensed under the [MIT license](https://github.com/frontio-ai/vite-template/blob/main/LICENSE).
|
||||
175
eslint.config.mjs
Normal file
175
eslint.config.mjs
Normal file
@@ -0,0 +1,175 @@
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { defineConfig, globalIgnores } from "eslint/config";
|
||||
import { fixupConfigRules, fixupPluginRules } from "@eslint/compat";
|
||||
import react from "eslint-plugin-react";
|
||||
import unusedImports from "eslint-plugin-unused-imports";
|
||||
import _import from "eslint-plugin-import";
|
||||
import typescriptEslint from "@typescript-eslint/eslint-plugin";
|
||||
import jsxA11Y from "eslint-plugin-jsx-a11y";
|
||||
import prettier from "eslint-plugin-prettier";
|
||||
import globals from "globals";
|
||||
import tsParser from "@typescript-eslint/parser";
|
||||
import js from "@eslint/js";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
allConfig: js.configs.all,
|
||||
});
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores([
|
||||
".now/*",
|
||||
"**/*.css",
|
||||
"**/.changeset",
|
||||
"**/dist",
|
||||
"esm/*",
|
||||
"public/*",
|
||||
"tests/*",
|
||||
"scripts/*",
|
||||
"**/*.config.js",
|
||||
"**/.DS_Store",
|
||||
"**/node_modules",
|
||||
"**/coverage",
|
||||
"**/.next",
|
||||
"**/build",
|
||||
"!**/.commitlintrc.cjs",
|
||||
"!**/.lintstagedrc.cjs",
|
||||
"!**/jest.config.js",
|
||||
"!**/plopfile.js",
|
||||
"!**/react-shim.js",
|
||||
"!**/tsup.config.ts",
|
||||
]),
|
||||
{
|
||||
extends: fixupConfigRules(
|
||||
compat.extends(
|
||||
"plugin:react/recommended",
|
||||
"plugin:prettier/recommended",
|
||||
"plugin:react-hooks/recommended",
|
||||
"plugin:jsx-a11y/recommended",
|
||||
),
|
||||
),
|
||||
|
||||
plugins: {
|
||||
react: fixupPluginRules(react),
|
||||
"unused-imports": unusedImports,
|
||||
import: fixupPluginRules(_import),
|
||||
"@typescript-eslint": typescriptEslint,
|
||||
"jsx-a11y": fixupPluginRules(jsxA11Y),
|
||||
prettier: fixupPluginRules(prettier),
|
||||
},
|
||||
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...Object.fromEntries(
|
||||
Object.entries(globals.browser).map(([key]) => [key, "off"]),
|
||||
),
|
||||
...globals.node,
|
||||
},
|
||||
|
||||
parser: tsParser,
|
||||
ecmaVersion: 12,
|
||||
sourceType: "module",
|
||||
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
settings: {
|
||||
react: {
|
||||
version: "detect",
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
files: ["**/*.ts", "**/*.tsx"],
|
||||
|
||||
rules: {
|
||||
"no-console": "warn",
|
||||
"react/prop-types": "off",
|
||||
"react/jsx-uses-react": "off",
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"react-hooks/exhaustive-deps": "off",
|
||||
"jsx-a11y/click-events-have-key-events": "warn",
|
||||
"jsx-a11y/interactive-supports-focus": "warn",
|
||||
"prettier/prettier": "warn",
|
||||
"no-unused-vars": "off",
|
||||
"unused-imports/no-unused-vars": "off",
|
||||
"unused-imports/no-unused-imports": "warn",
|
||||
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
args: "after-used",
|
||||
ignoreRestSiblings: false,
|
||||
argsIgnorePattern: "^_.*?$",
|
||||
},
|
||||
],
|
||||
|
||||
"import/order": [
|
||||
"warn",
|
||||
{
|
||||
groups: [
|
||||
"type",
|
||||
"builtin",
|
||||
"object",
|
||||
"external",
|
||||
"internal",
|
||||
"parent",
|
||||
"sibling",
|
||||
"index",
|
||||
],
|
||||
|
||||
pathGroups: [
|
||||
{
|
||||
pattern: "~/**",
|
||||
group: "external",
|
||||
position: "after",
|
||||
},
|
||||
],
|
||||
|
||||
"newlines-between": "always",
|
||||
},
|
||||
],
|
||||
|
||||
"react/self-closing-comp": "warn",
|
||||
|
||||
"react/jsx-sort-props": [
|
||||
"warn",
|
||||
{
|
||||
callbacksLast: true,
|
||||
shorthandFirst: true,
|
||||
noSortAlphabetically: false,
|
||||
reservedFirst: true,
|
||||
},
|
||||
],
|
||||
|
||||
"padding-line-between-statements": [
|
||||
"warn",
|
||||
{
|
||||
blankLine: "always",
|
||||
prev: "*",
|
||||
next: "return",
|
||||
},
|
||||
{
|
||||
blankLine: "always",
|
||||
prev: ["const", "let", "var"],
|
||||
next: "*",
|
||||
},
|
||||
{
|
||||
blankLine: "any",
|
||||
prev: ["const", "let", "var"],
|
||||
next: ["const", "let", "var"],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
]);
|
||||
BIN
favicon.ico
Normal file
BIN
favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
28
index.html
Normal file
28
index.html
Normal file
@@ -0,0 +1,28 @@
|
||||
<!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>Vite + HeroUI</title>
|
||||
<meta key="title" content="Vite + HeroUI" property="og:title" />
|
||||
<meta
|
||||
content="Make beautiful websites regardless of your design experience."
|
||||
property="og:description"
|
||||
/>
|
||||
<meta
|
||||
content="Make beautiful websites regardless of your design experience."
|
||||
name="description"
|
||||
/>
|
||||
<meta
|
||||
key="viewport"
|
||||
content="viewport-fit=cover, width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"
|
||||
name="viewport"
|
||||
/>
|
||||
<link href="/favicon.ico" rel="icon" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
65
package.json
Normal file
65
package.json
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"name": "vite-template",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"lint": "eslint --fix",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@heroicons/react": "^2.2.0",
|
||||
"@heroui/button": "^2.2.24",
|
||||
"@heroui/code": "^2.2.18",
|
||||
"@heroui/dropdown": "^2.3.24",
|
||||
"@heroui/input": "^2.4.25",
|
||||
"@heroui/kbd": "^2.2.19",
|
||||
"@heroui/link": "^2.2.21",
|
||||
"@heroui/navbar": "^2.2.22",
|
||||
"@heroui/react": "^2.8.2",
|
||||
"@heroui/snippet": "^2.2.25",
|
||||
"@heroui/switch": "^2.2.22",
|
||||
"@heroui/system": "^2.4.20",
|
||||
"@heroui/theme": "^2.4.20",
|
||||
"@heroui/use-theme": "2.1.10",
|
||||
"@react-aria/visually-hidden": "3.8.26",
|
||||
"@react-types/shared": "3.31.0",
|
||||
"@tailwindcss/postcss": "4.1.11",
|
||||
"@tailwindcss/vite": "4.1.11",
|
||||
"clsx": "2.1.1",
|
||||
"framer-motion": "11.18.2",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-router-dom": "6.23.0",
|
||||
"tailwind-variants": "2.0.1",
|
||||
"tailwindcss": "4.1.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "1.2.8",
|
||||
"@eslint/eslintrc": "3.3.1",
|
||||
"@eslint/js": "9.25.1",
|
||||
"@types/node": "20.5.7",
|
||||
"@types/react": "18.3.3",
|
||||
"@types/react-dom": "18.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.31.1",
|
||||
"@typescript-eslint/parser": "8.31.1",
|
||||
"@vitejs/plugin-react": "^4.7.0",
|
||||
"eslint": "9.25.1",
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
"eslint-plugin-jsx-a11y": "6.10.2",
|
||||
"eslint-plugin-node": "11.1.0",
|
||||
"eslint-plugin-prettier": "5.2.1",
|
||||
"eslint-plugin-react": "7.37.5",
|
||||
"eslint-plugin-react-hooks": "5.2.0",
|
||||
"eslint-plugin-unused-imports": "4.1.4",
|
||||
"globals": "16.0.0",
|
||||
"postcss": "8.5.6",
|
||||
"prettier": "3.5.3",
|
||||
"typescript": "5.6.3",
|
||||
"vite": "6.0.11",
|
||||
"vite-tsconfig-paths": "5.1.4"
|
||||
}
|
||||
}
|
||||
5
postcss.config.js
Normal file
5
postcss.config.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export default {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
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 |
15
src/App.tsx
Normal file
15
src/App.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import Navbar_create from "./Navbar";
|
||||
import ChatbotInterface from "./new chatbot-interaction";
|
||||
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<>
|
||||
<Navbar_create />
|
||||
|
||||
<ChatbotInterface/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
86
src/ChatbotInput.tsx
Normal file
86
src/ChatbotInput.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import { Input } from "@heroui/react";
|
||||
import { useState } from "react";
|
||||
import {Button} from "@heroui/react";
|
||||
|
||||
interface ChatbotInputProps {
|
||||
onSubmit?: (question: string) => void;
|
||||
placeholder?: string;
|
||||
maxHeight?: number;
|
||||
minHeight?: number;
|
||||
}
|
||||
|
||||
const ChatbotInput = ({
|
||||
onSubmit,
|
||||
placeholder = "请输入你的问题...",
|
||||
minHeight = 18,
|
||||
maxHeight = 120,
|
||||
}: ChatbotInputProps) => {
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
const [inputHeight, setInputHeight] = useState(minHeight);
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const value = e.target.value;
|
||||
setInputValue(value);
|
||||
|
||||
const textarea = e.target;
|
||||
textarea.style.height = `${minHeight}px`;
|
||||
const newHeight = Math.min(textarea.scrollHeight, maxHeight);
|
||||
setInputHeight(newHeight);
|
||||
textarea.style.height = `${newHeight}px`;
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
const question = inputValue.trim();
|
||||
if (question && onSubmit) {
|
||||
onSubmit(question);
|
||||
setInputValue("");
|
||||
setInputHeight(18);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex flex-col w-full max-w-2xl px-4 py-2
|
||||
border border-gray-200 rounded-lg
|
||||
hover:border-gray-300 focus-within:border-blue-400
|
||||
transition-all transition-all duration-200 bg-white shadow-sm"
|
||||
>
|
||||
|
||||
|
||||
<textarea
|
||||
value={inputValue}
|
||||
onChange={handleInputChange}
|
||||
placeholder={placeholder}
|
||||
style={{
|
||||
minHeight: `${minHeight}px`,
|
||||
maxHeight: `${maxHeight}px`,
|
||||
height: `${inputHeight}px`,
|
||||
overflowY: inputHeight >= maxHeight ? "auto" : "hidden",
|
||||
}}
|
||||
className="flex flex-1 w-full border-0 shadow-none focus:ring-0
|
||||
px-3 py-2 bg-transparent placeholder:text-gray-400
|
||||
resize-none outline-none"
|
||||
spellCheck={false}
|
||||
/>
|
||||
|
||||
<div className="flex justify-end mt-1">
|
||||
<Button
|
||||
onPress={handleSubmit}
|
||||
disabled={!inputValue.trim()}
|
||||
className={`flex items-center justify-center w-8 h-8 rounded-full
|
||||
transition-colors duration-200 cursor-pointer
|
||||
${
|
||||
inputValue.trim()
|
||||
? "bg-blue-500 text-white hover:bg-blue-600"
|
||||
: "bg-gray-100 text-gray-400 cursor-not-allowed"
|
||||
}`}
|
||||
aria-label="发送问题"
|
||||
>
|
||||
<span className="text-lg">{"-" + ">"}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatbotInput;
|
||||
52
src/Left-drawer.tsx
Normal file
52
src/Left-drawer.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Drawer,
|
||||
DrawerContent,
|
||||
DrawerHeader,
|
||||
DrawerBody,
|
||||
DrawerFooter,
|
||||
Button,
|
||||
useDisclosure,
|
||||
} from "@heroui/react";
|
||||
|
||||
export default function Left() {
|
||||
const {isOpen, onOpen, onOpenChange} = useDisclosure();
|
||||
const [placement, setPlacement] = React.useState("left");
|
||||
|
||||
const handleOpen = (placement: React.SetStateAction<string>) => {
|
||||
setPlacement(placement);
|
||||
onOpen();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{["left"].map((placement) => (
|
||||
<Button key={placement} className="capitalize" onPress={() => handleOpen(placement)}>
|
||||
历史对话
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
<Drawer isOpen={isOpen} placement={placement} onOpenChange={onOpenChange}>
|
||||
<DrawerContent>
|
||||
{(onClose) => (
|
||||
<>
|
||||
<DrawerHeader className="flex flex-col gap-1"></DrawerHeader>
|
||||
<DrawerBody>
|
||||
</DrawerBody>
|
||||
<DrawerFooter>
|
||||
<Button color="danger" variant="light" onPress={onClose}>
|
||||
|
||||
</Button>
|
||||
<Button color="primary" onPress={onClose}>
|
||||
Action
|
||||
</Button>
|
||||
</DrawerFooter>
|
||||
</>
|
||||
)}
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
54
src/Navbar.tsx
Normal file
54
src/Navbar.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import {Navbar, NavbarBrand, NavbarContent, NavbarItem,Button,Link} from "@heroui/react";
|
||||
import Left from "./Left-drawer";
|
||||
|
||||
|
||||
export const AcmeLogo = () => {
|
||||
return (
|
||||
<svg fill="none" height="36" viewBox="0 0 32 32" width="36">
|
||||
<path
|
||||
clipRule="evenodd"
|
||||
d="M17.6482 10.1305L15.8785 7.02583L7.02979 22.5499H10.5278L17.6482 10.1305ZM19.8798 14.0457L18.11 17.1983L19.394 19.4511H16.8453L15.1056 22.5499H24.7272L19.8798 14.0457Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default function Navbar_create() {
|
||||
return (
|
||||
|
||||
<Navbar>
|
||||
<NavbarBrand>
|
||||
<AcmeLogo />
|
||||
<p className="font-bold text-inherit">ChatBot</p>
|
||||
</NavbarBrand>
|
||||
<NavbarContent className="hidden sm:flex gap-4" justify="center">
|
||||
<NavbarItem>
|
||||
<Link color="foreground" href="#">
|
||||
<Left 历史对话/>
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem isActive>
|
||||
<Link aria-current="page" href="#">
|
||||
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<Link color="foreground" href="#">
|
||||
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
<NavbarContent justify="end">
|
||||
<NavbarItem className="hidden lg:flex">
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<Button as={Link} color="primary" href="#" variant="flat">
|
||||
用户中心
|
||||
</Button>
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
</Navbar>
|
||||
);
|
||||
}
|
||||
0
src/Scroll Shadow.tsx
Normal file
0
src/Scroll Shadow.tsx
Normal file
186
src/components/icons.tsx
Normal file
186
src/components/icons.tsx
Normal file
@@ -0,0 +1,186 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { IconSvgProps } from "@/types";
|
||||
|
||||
export const Logo: React.FC<IconSvgProps> = ({
|
||||
size = 36,
|
||||
height,
|
||||
...props
|
||||
}) => (
|
||||
<svg
|
||||
fill="none"
|
||||
height={size || height}
|
||||
viewBox="0 0 32 32"
|
||||
width={size || height}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
clipRule="evenodd"
|
||||
d="M17.6482 10.1305L15.8785 7.02583L7.02979 22.5499H10.5278L17.6482 10.1305ZM19.8798 14.0457L18.11 17.1983L19.394 19.4511H16.8453L15.1056 22.5499H24.7272L19.8798 14.0457Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const DiscordIcon: React.FC<IconSvgProps> = ({
|
||||
size = 24,
|
||||
width,
|
||||
height,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
height={size || height}
|
||||
viewBox="0 0 24 24"
|
||||
width={size || width}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M14.82 4.26a10.14 10.14 0 0 0-.53 1.1 14.66 14.66 0 0 0-4.58 0 10.14 10.14 0 0 0-.53-1.1 16 16 0 0 0-4.13 1.3 17.33 17.33 0 0 0-3 11.59 16.6 16.6 0 0 0 5.07 2.59A12.89 12.89 0 0 0 8.23 18a9.65 9.65 0 0 1-1.71-.83 3.39 3.39 0 0 0 .42-.33 11.66 11.66 0 0 0 10.12 0q.21.18.42.33a10.84 10.84 0 0 1-1.71.84 12.41 12.41 0 0 0 1.08 1.78 16.44 16.44 0 0 0 5.06-2.59 17.22 17.22 0 0 0-3-11.59 16.09 16.09 0 0 0-4.09-1.35zM8.68 14.81a1.94 1.94 0 0 1-1.8-2 1.93 1.93 0 0 1 1.8-2 1.93 1.93 0 0 1 1.8 2 1.93 1.93 0 0 1-1.8 2zm6.64 0a1.94 1.94 0 0 1-1.8-2 1.93 1.93 0 0 1 1.8-2 1.92 1.92 0 0 1 1.8 2 1.92 1.92 0 0 1-1.8 2z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const TwitterIcon: React.FC<IconSvgProps> = ({
|
||||
size = 24,
|
||||
width,
|
||||
height,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
height={size || height}
|
||||
viewBox="0 0 24 24"
|
||||
width={size || width}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M19.633 7.997c.013.175.013.349.013.523 0 5.325-4.053 11.461-11.46 11.461-2.282 0-4.402-.661-6.186-1.809.324.037.636.05.973.05a8.07 8.07 0 0 0 5.001-1.721 4.036 4.036 0 0 1-3.767-2.793c.249.037.499.062.761.062.361 0 .724-.05 1.061-.137a4.027 4.027 0 0 1-3.23-3.953v-.05c.537.299 1.16.486 1.82.511a4.022 4.022 0 0 1-1.796-3.354c0-.748.199-1.434.548-2.032a11.457 11.457 0 0 0 8.306 4.215c-.062-.3-.1-.611-.1-.923a4.026 4.026 0 0 1 4.028-4.028c1.16 0 2.207.486 2.943 1.272a7.957 7.957 0 0 0 2.556-.973 4.02 4.02 0 0 1-1.771 2.22 8.073 8.073 0 0 0 2.319-.624 8.645 8.645 0 0 1-2.019 2.083z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const GithubIcon: React.FC<IconSvgProps> = ({
|
||||
size = 24,
|
||||
width,
|
||||
height,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
height={size || height}
|
||||
viewBox="0 0 24 24"
|
||||
width={size || width}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
clipRule="evenodd"
|
||||
d="M12.026 2c-5.509 0-9.974 4.465-9.974 9.974 0 4.406 2.857 8.145 6.821 9.465.499.09.679-.217.679-.481 0-.237-.008-.865-.011-1.696-2.775.602-3.361-1.338-3.361-1.338-.452-1.152-1.107-1.459-1.107-1.459-.905-.619.069-.605.069-.605 1.002.07 1.527 1.028 1.527 1.028.89 1.524 2.336 1.084 2.902.829.091-.645.351-1.085.635-1.334-2.214-.251-4.542-1.107-4.542-4.93 0-1.087.389-1.979 1.024-2.675-.101-.253-.446-1.268.099-2.64 0 0 .837-.269 2.742 1.021a9.582 9.582 0 0 1 2.496-.336 9.554 9.554 0 0 1 2.496.336c1.906-1.291 2.742-1.021 2.742-1.021.545 1.372.203 2.387.099 2.64.64.696 1.024 1.587 1.024 2.675 0 3.833-2.33 4.675-4.552 4.922.355.308.675.916.675 1.846 0 1.334-.012 2.41-.012 2.737 0 .267.178.577.687.479C19.146 20.115 22 16.379 22 11.974 22 6.465 17.535 2 12.026 2z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const MoonFilledIcon = ({
|
||||
size = 24,
|
||||
width,
|
||||
height,
|
||||
...props
|
||||
}: IconSvgProps) => (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
height={size || height}
|
||||
role="presentation"
|
||||
viewBox="0 0 24 24"
|
||||
width={size || width}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M21.53 15.93c-.16-.27-.61-.69-1.73-.49a8.46 8.46 0 01-1.88.13 8.409 8.409 0 01-5.91-2.82 8.068 8.068 0 01-1.44-8.66c.44-1.01.13-1.54-.09-1.76s-.77-.55-1.83-.11a10.318 10.318 0 00-6.32 10.21 10.475 10.475 0 007.04 8.99 10 10 0 002.89.55c.16.01.32.02.48.02a10.5 10.5 0 008.47-4.27c.67-.93.49-1.519.32-1.79z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const SunFilledIcon = ({
|
||||
size = 24,
|
||||
width,
|
||||
height,
|
||||
...props
|
||||
}: IconSvgProps) => (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
height={size || height}
|
||||
role="presentation"
|
||||
viewBox="0 0 24 24"
|
||||
width={size || width}
|
||||
{...props}
|
||||
>
|
||||
<g fill="currentColor">
|
||||
<path d="M19 12a7 7 0 11-7-7 7 7 0 017 7z" />
|
||||
<path d="M12 22.96a.969.969 0 01-1-.96v-.08a1 1 0 012 0 1.038 1.038 0 01-1 1.04zm7.14-2.82a1.024 1.024 0 01-.71-.29l-.13-.13a1 1 0 011.41-1.41l.13.13a1 1 0 010 1.41.984.984 0 01-.7.29zm-14.28 0a1.024 1.024 0 01-.71-.29 1 1 0 010-1.41l.13-.13a1 1 0 011.41 1.41l-.13.13a1 1 0 01-.7.29zM22 13h-.08a1 1 0 010-2 1.038 1.038 0 011.04 1 .969.969 0 01-.96 1zM2.08 13H2a1 1 0 010-2 1.038 1.038 0 011.04 1 .969.969 0 01-.96 1zm16.93-7.01a1.024 1.024 0 01-.71-.29 1 1 0 010-1.41l.13-.13a1 1 0 011.41 1.41l-.13.13a.984.984 0 01-.7.29zm-14.02 0a1.024 1.024 0 01-.71-.29l-.13-.14a1 1 0 011.41-1.41l.13.13a1 1 0 010 1.41.97.97 0 01-.7.3zM12 3.04a.969.969 0 01-1-.96V2a1 1 0 012 0 1.038 1.038 0 01-1 1.04z" />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const HeartFilledIcon = ({
|
||||
size = 24,
|
||||
width,
|
||||
height,
|
||||
...props
|
||||
}: IconSvgProps) => (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
height={size || height}
|
||||
role="presentation"
|
||||
viewBox="0 0 24 24"
|
||||
width={size || width}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M12.62 20.81c-.34.12-.9.12-1.24 0C8.48 19.82 2 15.69 2 8.69 2 5.6 4.49 3.1 7.56 3.1c1.82 0 3.43.88 4.44 2.24a5.53 5.53 0 0 1 4.44-2.24C19.51 3.1 22 5.6 22 8.69c0 7-6.48 11.13-9.38 12.12Z"
|
||||
fill="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const SearchIcon = (props: IconSvgProps) => (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
role="presentation"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M11.5 21C16.7467 21 21 16.7467 21 11.5C21 6.25329 16.7467 2 11.5 2C6.25329 2 2 6.25329 2 11.5C2 16.7467 6.25329 21 11.5 21Z"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<path
|
||||
d="M22 22L20 20"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
144
src/components/navbar.tsx
Normal file
144
src/components/navbar.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
import { Button } from "@heroui/button";
|
||||
import { Kbd } from "@heroui/kbd";
|
||||
import { Link } from "@heroui/link";
|
||||
import { Input } from "@heroui/input";
|
||||
import {
|
||||
Navbar as HeroUINavbar,
|
||||
NavbarBrand,
|
||||
NavbarContent,
|
||||
NavbarItem,
|
||||
NavbarMenuToggle,
|
||||
NavbarMenu,
|
||||
NavbarMenuItem,
|
||||
} from "@heroui/navbar";
|
||||
import { link as linkStyles } from "@heroui/theme";
|
||||
import clsx from "clsx";
|
||||
|
||||
import { siteConfig } from "@/config/site";
|
||||
import { ThemeSwitch } from "@/components/theme-switch";
|
||||
import {
|
||||
TwitterIcon,
|
||||
GithubIcon,
|
||||
DiscordIcon,
|
||||
HeartFilledIcon,
|
||||
SearchIcon,
|
||||
} from "@/components/icons";
|
||||
import { Logo } from "@/components/icons";
|
||||
|
||||
export const Navbar = () => {
|
||||
const searchInput = (
|
||||
<Input
|
||||
aria-label="Search"
|
||||
classNames={{
|
||||
inputWrapper: "bg-default-100",
|
||||
input: "text-sm",
|
||||
}}
|
||||
endContent={
|
||||
<Kbd className="hidden lg:inline-block" keys={["command"]}>
|
||||
K
|
||||
</Kbd>
|
||||
}
|
||||
labelPlacement="outside"
|
||||
placeholder="Search..."
|
||||
startContent={
|
||||
<SearchIcon className="text-base text-default-400 pointer-events-none flex-shrink-0" />
|
||||
}
|
||||
type="search"
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<HeroUINavbar maxWidth="xl" position="sticky">
|
||||
<NavbarContent className="basis-1/5 sm:basis-full" justify="start">
|
||||
<NavbarBrand className="gap-3 max-w-fit">
|
||||
<Link
|
||||
className="flex justify-start items-center gap-1"
|
||||
color="foreground"
|
||||
href="/"
|
||||
>
|
||||
<Logo />
|
||||
<p className="font-bold text-inherit">ACME</p>
|
||||
</Link>
|
||||
</NavbarBrand>
|
||||
<div className="hidden lg:flex gap-4 justify-start ml-2">
|
||||
{siteConfig.navItems.map((item) => (
|
||||
<NavbarItem key={item.href}>
|
||||
<Link
|
||||
className={clsx(
|
||||
linkStyles({ color: "foreground" }),
|
||||
"data-[active=true]:text-primary data-[active=true]:font-medium",
|
||||
)}
|
||||
color="foreground"
|
||||
href={item.href}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
))}
|
||||
</div>
|
||||
</NavbarContent>
|
||||
|
||||
<NavbarContent
|
||||
className="hidden sm:flex basis-1/5 sm:basis-full"
|
||||
justify="end"
|
||||
>
|
||||
<NavbarItem className="hidden sm:flex gap-2">
|
||||
<Link isExternal href={siteConfig.links.twitter} title="Twitter">
|
||||
<TwitterIcon className="text-default-500" />
|
||||
</Link>
|
||||
<Link isExternal href={siteConfig.links.discord} title="Discord">
|
||||
<DiscordIcon className="text-default-500" />
|
||||
</Link>
|
||||
<Link isExternal href={siteConfig.links.github} title="GitHub">
|
||||
<GithubIcon className="text-default-500" />
|
||||
</Link>
|
||||
<ThemeSwitch />
|
||||
</NavbarItem>
|
||||
<NavbarItem className="hidden lg:flex">{searchInput}</NavbarItem>
|
||||
<NavbarItem className="hidden md:flex">
|
||||
<Button
|
||||
isExternal
|
||||
as={Link}
|
||||
className="text-sm font-normal text-default-600 bg-default-100"
|
||||
href={siteConfig.links.sponsor}
|
||||
startContent={<HeartFilledIcon className="text-danger" />}
|
||||
variant="flat"
|
||||
>
|
||||
Sponsor
|
||||
</Button>
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
|
||||
<NavbarContent className="sm:hidden basis-1 pl-4" justify="end">
|
||||
<Link isExternal href={siteConfig.links.github}>
|
||||
<GithubIcon className="text-default-500" />
|
||||
</Link>
|
||||
<ThemeSwitch />
|
||||
<NavbarMenuToggle />
|
||||
</NavbarContent>
|
||||
|
||||
<NavbarMenu>
|
||||
{searchInput}
|
||||
<div className="mx-4 mt-2 flex flex-col gap-2">
|
||||
{siteConfig.navMenuItems.map((item, index) => (
|
||||
<NavbarMenuItem key={`${item}-${index}`}>
|
||||
<Link
|
||||
color={
|
||||
index === 2
|
||||
? "primary"
|
||||
: index === siteConfig.navMenuItems.length - 1
|
||||
? "danger"
|
||||
: "foreground"
|
||||
}
|
||||
href="#"
|
||||
size="lg"
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
</NavbarMenuItem>
|
||||
))}
|
||||
</div>
|
||||
</NavbarMenu>
|
||||
</HeroUINavbar>
|
||||
);
|
||||
};
|
||||
53
src/components/primitives.ts
Normal file
53
src/components/primitives.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { tv } from "tailwind-variants";
|
||||
|
||||
export const title = tv({
|
||||
base: "tracking-tight inline font-semibold",
|
||||
variants: {
|
||||
color: {
|
||||
violet: "from-[#FF1CF7] to-[#b249f8]",
|
||||
yellow: "from-[#FF705B] to-[#FFB457]",
|
||||
blue: "from-[#5EA2EF] to-[#0072F5]",
|
||||
cyan: "from-[#00b7fa] to-[#01cfea]",
|
||||
green: "from-[#6FEE8D] to-[#17c964]",
|
||||
pink: "from-[#FF72E1] to-[#F54C7A]",
|
||||
foreground: "dark:from-[#FFFFFF] dark:to-[#4B4B4B]",
|
||||
},
|
||||
size: {
|
||||
sm: "text-3xl lg:text-4xl",
|
||||
md: "text-[2.3rem] lg:text-5xl",
|
||||
lg: "text-4xl lg:text-6xl",
|
||||
},
|
||||
fullWidth: {
|
||||
true: "w-full block",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "md",
|
||||
},
|
||||
compoundVariants: [
|
||||
{
|
||||
color: [
|
||||
"violet",
|
||||
"yellow",
|
||||
"blue",
|
||||
"cyan",
|
||||
"green",
|
||||
"pink",
|
||||
"foreground",
|
||||
],
|
||||
class: "bg-clip-text text-transparent bg-gradient-to-b",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export const subtitle = tv({
|
||||
base: "w-full md:w-1/2 my-2 text-lg lg:text-xl text-default-600 block max-w-full",
|
||||
variants: {
|
||||
fullWidth: {
|
||||
true: "!w-full",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
fullWidth: true,
|
||||
},
|
||||
});
|
||||
82
src/components/theme-switch.tsx
Normal file
82
src/components/theme-switch.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import { FC, useState, useEffect } from "react";
|
||||
import { VisuallyHidden } from "@react-aria/visually-hidden";
|
||||
import { SwitchProps, useSwitch } from "@heroui/switch";
|
||||
import clsx from "clsx";
|
||||
import { useTheme } from "@heroui/use-theme";
|
||||
|
||||
import { SunFilledIcon, MoonFilledIcon } from "@/components/icons";
|
||||
|
||||
export interface ThemeSwitchProps {
|
||||
className?: string;
|
||||
classNames?: SwitchProps["classNames"];
|
||||
}
|
||||
|
||||
export const ThemeSwitch: FC<ThemeSwitchProps> = ({
|
||||
className,
|
||||
classNames,
|
||||
}) => {
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
const {
|
||||
Component,
|
||||
slots,
|
||||
isSelected,
|
||||
getBaseProps,
|
||||
getInputProps,
|
||||
getWrapperProps,
|
||||
} = useSwitch({
|
||||
isSelected: theme === "light",
|
||||
onChange: () => setTheme(theme === "light" ? "dark" : "light"),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setIsMounted(true);
|
||||
}, [isMounted]);
|
||||
|
||||
// Prevent Hydration Mismatch
|
||||
if (!isMounted) return <div className="w-6 h-6" />;
|
||||
|
||||
return (
|
||||
<Component
|
||||
aria-label={isSelected ? "Switch to dark mode" : "Switch to light mode"}
|
||||
{...getBaseProps({
|
||||
className: clsx(
|
||||
"px-px transition-opacity hover:opacity-80 cursor-pointer",
|
||||
className,
|
||||
classNames?.base,
|
||||
),
|
||||
})}
|
||||
>
|
||||
<VisuallyHidden>
|
||||
<input {...getInputProps()} />
|
||||
</VisuallyHidden>
|
||||
<div
|
||||
{...getWrapperProps()}
|
||||
className={slots.wrapper({
|
||||
class: clsx(
|
||||
[
|
||||
"w-auto h-auto",
|
||||
"bg-transparent",
|
||||
"rounded-lg",
|
||||
"flex items-center justify-center",
|
||||
"group-data-[selected=true]:bg-transparent",
|
||||
"!text-default-500",
|
||||
"pt-px",
|
||||
"px-0",
|
||||
"mx-0",
|
||||
],
|
||||
classNames?.wrapper,
|
||||
),
|
||||
})}
|
||||
>
|
||||
{isSelected ? (
|
||||
<MoonFilledIcon size={22} />
|
||||
) : (
|
||||
<SunFilledIcon size={22} />
|
||||
)}
|
||||
</div>
|
||||
</Component>
|
||||
);
|
||||
};
|
||||
69
src/config/site.ts
Normal file
69
src/config/site.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
export type SiteConfig = typeof siteConfig;
|
||||
|
||||
export const siteConfig = {
|
||||
name: "Vite + HeroUI",
|
||||
description: "Make beautiful websites regardless of your design experience.",
|
||||
navItems: [
|
||||
{
|
||||
label: "Home",
|
||||
href: "/",
|
||||
},
|
||||
{
|
||||
label: "Docs",
|
||||
href: "/docs",
|
||||
},
|
||||
{
|
||||
label: "Pricing",
|
||||
href: "/pricing",
|
||||
},
|
||||
{
|
||||
label: "Blog",
|
||||
href: "/blog",
|
||||
},
|
||||
{
|
||||
label: "About",
|
||||
href: "/about",
|
||||
},
|
||||
],
|
||||
navMenuItems: [
|
||||
{
|
||||
label: "Profile",
|
||||
href: "/profile",
|
||||
},
|
||||
{
|
||||
label: "Dashboard",
|
||||
href: "/dashboard",
|
||||
},
|
||||
{
|
||||
label: "Projects",
|
||||
href: "/projects",
|
||||
},
|
||||
{
|
||||
label: "Team",
|
||||
href: "/team",
|
||||
},
|
||||
{
|
||||
label: "Calendar",
|
||||
href: "/calendar",
|
||||
},
|
||||
{
|
||||
label: "Settings",
|
||||
href: "/settings",
|
||||
},
|
||||
{
|
||||
label: "Help & Feedback",
|
||||
href: "/help-feedback",
|
||||
},
|
||||
{
|
||||
label: "Logout",
|
||||
href: "/logout",
|
||||
},
|
||||
],
|
||||
links: {
|
||||
github: "https://github.com/frontio-ai/heroui",
|
||||
twitter: "https://twitter.com/hero_ui",
|
||||
docs: "https://heroui.com",
|
||||
discord: "https://discord.gg/9b6yyZKmH4",
|
||||
sponsor: "https://patreon.com/jrgarciadev",
|
||||
},
|
||||
};
|
||||
29
src/layouts/default.tsx
Normal file
29
src/layouts/default.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Link } from "@heroui/link";
|
||||
|
||||
import { Navbar } from "@/components/navbar";
|
||||
|
||||
export default function DefaultLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="relative flex flex-col h-screen">
|
||||
<Navbar />
|
||||
<main className="container mx-auto max-w-7xl px-6 flex-grow pt-16">
|
||||
{children}
|
||||
</main>
|
||||
<footer className="w-full flex items-center justify-center py-3">
|
||||
<Link
|
||||
isExternal
|
||||
className="flex items-center gap-1 text-current"
|
||||
href="https://heroui.com"
|
||||
title="heroui.com homepage"
|
||||
>
|
||||
<span className="text-default-600">Powered by</span>
|
||||
<p className="text-primary">HeroUI</p>
|
||||
</Link>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
17
src/main.tsx
Normal file
17
src/main.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
|
||||
import App from "./App.tsx";
|
||||
import { Provider } from "./provider.tsx";
|
||||
import "@/styles/globals.css";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<BrowserRouter>
|
||||
<Provider>
|
||||
<App />
|
||||
</Provider>
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
282
src/new chatbot-interaction.tsx
Normal file
282
src/new chatbot-interaction.tsx
Normal file
@@ -0,0 +1,282 @@
|
||||
import React, { useState, useRef, useEffect } from "react";
|
||||
import ChatbotInput from "./ChatbotInput";
|
||||
import {Button} from "@heroui/react";
|
||||
|
||||
// import { li, use } from "framer-motion/client";
|
||||
// import { user } from "@heroui/theme";
|
||||
// import { text } from "stream/consumers";
|
||||
// import addToast from "@heroui/react";
|
||||
interface Message {
|
||||
id: string;
|
||||
text: string;
|
||||
sender: "user" | "bot";
|
||||
status: "sending" | "sent" | "received" | "error";
|
||||
}
|
||||
const ChatbotInterface: React.FC = () => {
|
||||
|
||||
const [messages, setMessages] = useState<Message[]>([
|
||||
{
|
||||
id: "iniial",
|
||||
text: "你好!我是智能聊天机器人",
|
||||
sender: "bot",
|
||||
status: "received",
|
||||
},
|
||||
]);
|
||||
//复制chatbot消息函数
|
||||
const copyText = async (text: string, messageId: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
if (messageId) {
|
||||
setCopySuccessId(messageId);
|
||||
}
|
||||
alert("复制成功");
|
||||
} catch (err) {
|
||||
console.error("复制失败:", err);
|
||||
alert("复制失败,请手动复制");
|
||||
}
|
||||
};
|
||||
//chatbot消息重新发送函数
|
||||
const reasendMessage = async (messageId: string) => {
|
||||
const userMessage = messages.find((msg) => msg.id === messageId);
|
||||
|
||||
if (!userMessage) {
|
||||
return;
|
||||
}
|
||||
//使用fillter删除掉bot旧的消息
|
||||
|
||||
setMessages((prev) =>
|
||||
prev.filter((msg) => !(msg.sender === "bot" && msg.id === messageId))
|
||||
);
|
||||
try {
|
||||
const newReply = await new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
id: `${Date.now()}`,
|
||||
text:
|
||||
"你好,我是智能聊天机器人,很高兴为您服务。您刚才说的是:" +
|
||||
userMessage.text,
|
||||
sender: "bot",
|
||||
status: "received",
|
||||
});
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
setMessages((prev) => [...prev, newReply as Message]);
|
||||
} catch (err) {
|
||||
console.error("重新发送失败:", err);
|
||||
setMessages((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: `${Date.now()}`,
|
||||
text: "重新发送失败,请稍后再试",
|
||||
sender: "bot",
|
||||
status: "error",
|
||||
},
|
||||
]);
|
||||
alert("重新发送失败,请稍后再试");
|
||||
}
|
||||
};
|
||||
//新建对话功能
|
||||
const handleNewChat = () => {
|
||||
setMessages([
|
||||
{
|
||||
id: "iniial",
|
||||
text: "你好!我是智能聊天机器人",
|
||||
sender: "bot",
|
||||
status: "received",
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const [copySuccessId, setCopySuccessId] = useState<string | null>(null);
|
||||
//文本行字数限制以及自动换行
|
||||
const addLineBreaksByCharLimit = (
|
||||
text: string,
|
||||
limit: number = 30,
|
||||
): string => {
|
||||
if (text || limit < 0) return text;
|
||||
let result = "";
|
||||
let currentLinetext = "";
|
||||
|
||||
for (const char of text){
|
||||
if (char === "\n") {
|
||||
result += currentLinetext + char;
|
||||
currentLinetext = "";
|
||||
continue;
|
||||
}
|
||||
if (currentLinetext.length >= limit) {
|
||||
result += currentLinetext + "\n";
|
||||
currentLinetext = char;
|
||||
}else{
|
||||
currentLinetext += char;
|
||||
}
|
||||
}
|
||||
|
||||
return result + currentLinetext;
|
||||
};
|
||||
const [isBotTyping, setIsBotTyping] = useState<boolean>(false);
|
||||
//定位到消息列表最底部
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const scrollFlow = () => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
};
|
||||
//副作用钩子处理自动滚动
|
||||
|
||||
useEffect(() => {
|
||||
scrollFlow();
|
||||
}, [messages, isBotTyping]);
|
||||
//处理用户消息
|
||||
const handleUserMessage = (text: string) => {
|
||||
//限制自动换行
|
||||
const formattedQuestion = addLineBreaksByCharLimit(text, 30);
|
||||
const userMessage: Message = {
|
||||
id: `${Date.now()}`,
|
||||
text: formattedQuestion,
|
||||
sender: "user",
|
||||
status: "sending",
|
||||
};
|
||||
|
||||
setMessages((prevMessages) => [...prevMessages, userMessage]);
|
||||
setTimeout(() => {
|
||||
setMessages((prev) =>
|
||||
prev.map((msg) =>
|
||||
// 找到当前用户消息,更新status;其他消息不变
|
||||
msg.id === userMessage.id ? { ...msg, status: "sent" } : msg
|
||||
)
|
||||
);
|
||||
handBotMessage(text, "");
|
||||
}, 1000);
|
||||
};
|
||||
//机器人回复
|
||||
const handBotMessage = (userText: string, botMessageId: string) => {
|
||||
setIsBotTyping(true);
|
||||
setTimeout(() => {
|
||||
const formattedQuestion = addLineBreaksByCharLimit(userText, 30);
|
||||
const botMessageContent =
|
||||
"你好,我是智能聊天机器人,很高兴为您服务。您刚才说的是:" +
|
||||
formattedQuestion;
|
||||
const newBotMessage: Message = {
|
||||
id: `${Date.now()}` || botMessageId,
|
||||
text: botMessageContent,
|
||||
sender: "bot",
|
||||
status: "received",
|
||||
};
|
||||
|
||||
if (botMessageId) {
|
||||
setMessages((prev) =>
|
||||
prev.map((msg) => (msg.id === botMessageId ? newBotMessage : msg))
|
||||
);
|
||||
} else {
|
||||
setMessages((prevMessages) => [...prevMessages, newBotMessage]);
|
||||
}
|
||||
setIsBotTyping(false);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto w-full flex flex-col h-screen bg-gray-50 shadow-xl">
|
||||
<main className="flex-1 overflow-y-auto p-4 space-y-6">
|
||||
{messages.map((message) => (
|
||||
<div
|
||||
key={message.id}
|
||||
className={`flex ${message.sender === "user" ? "justify-end" : "justify-start"} items-start`}
|
||||
>
|
||||
{message.sender === "bot" && (
|
||||
<div className="w-8 h-8 rounded-full bg-blue-500 flex items-center justify-center text-white flex-shrink-0 mr-2">
|
||||
<span className="text-xs font-bold">Bot</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={"max-w-[80%] flex flex-col"}>
|
||||
<div
|
||||
className={`
|
||||
px-4 py-3 rounded-2xl shadow-sm relative
|
||||
${
|
||||
message.sender === "user"
|
||||
? "bg-blue-500 text-white rounded-tr-none"
|
||||
: "bg-white text-gray-800 rounded-tl-none border border-gray-200"
|
||||
}
|
||||
`}
|
||||
>
|
||||
<p className="whitespace-pre-wrap">{message.text}</p>
|
||||
</div>
|
||||
{/* 复制按钮 */}
|
||||
{message.sender === "bot" && (
|
||||
<div className="flex gap-2 mt-2">
|
||||
<Button
|
||||
onPress={() => copyText(message.text, message.id)}
|
||||
className= " px-1.5px py-1 rounded-sm font-medium hover:bg-blue-700 transition-colors text-xs w-4 h-4"
|
||||
aria-label="复制消息"
|
||||
>
|
||||
复制
|
||||
</Button>
|
||||
<Button
|
||||
onPress={() => reasendMessage(message.id)}
|
||||
className=" px-1.5px py-1 rounded-sm font-medium hover:bg-blue-700 transition-colors text-xs w-4 h-4"
|
||||
aria-label="重新发送"
|
||||
>
|
||||
重新发送
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* {message.sender === "user" && (
|
||||
<Button
|
||||
onPress={() => reasendMessage(message.id)}
|
||||
className=" px-4 py-2 rounded-sm font-medium hover:bg-blue-700 transition-colors text-sm w-fit"
|
||||
aria-label="重新发送"
|
||||
>
|
||||
重新发送
|
||||
</Button>
|
||||
)}
|
||||
</div> */}
|
||||
|
||||
{message.sender === "user" && (
|
||||
<div className="w-8 h-8 rounded-full bg-blue-500 flex items-center justify-center text-white flex-shrink-0 mr-2overflow-hidden">
|
||||
<span className="text-xs font-bold">User</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{isBotTyping && (
|
||||
<div className="flex justify-start items-start">
|
||||
<div className="w-8 h-8 rounded-full bg-blue-500 flex items-center justify-center text-white flex-shrink-0 mr-2">
|
||||
<span className="text-xs font-bold">Bot</span>
|
||||
</div>
|
||||
<div className="bg-white border border-gray-200 rounded-2xl rounded-tl-none px-4 py-3 shadow-sm">
|
||||
<div className="flex space-x-1">
|
||||
<div
|
||||
className="w-2 h-2 bg-gray-400 rounded-full animate-bounce"
|
||||
style={{ animationDelay: "0ms" }}
|
||||
></div>
|
||||
<div
|
||||
className="w-2 h-2 bg-gray-400 rounded-full animate-bounce"
|
||||
style={{ animationDelay: "150ms" }}
|
||||
></div>
|
||||
<div
|
||||
className="w-2 h-2 bg-gray-400 rounded-full animate-bounce"
|
||||
style={{ animationDelay: "300ms" }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div ref={messagesEndRef} />
|
||||
</main>
|
||||
<div className="p-4 border-t border-gray-200 bg-gray-50">
|
||||
<Button onPress={handleNewChat}
|
||||
className="bg-blue-600 text-white px-4 py-2 rounded-sm font-medium hover:bg-blue-700 transition-colors text-sm w-fit"
|
||||
> 新建对话 </Button>
|
||||
<ChatbotInput
|
||||
onSubmit={handleUserMessage}
|
||||
placeholder="请输入要发送的消息"
|
||||
minHeight={10}
|
||||
maxHeight={80}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatbotInterface;
|
||||
14
src/pages/about.tsx
Normal file
14
src/pages/about.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { title } from "@/components/primitives";
|
||||
import DefaultLayout from "@/layouts/default";
|
||||
|
||||
export default function DocsPage() {
|
||||
return (
|
||||
<DefaultLayout>
|
||||
<section className="flex flex-col items-center justify-center gap-4 py-8 md:py-10">
|
||||
<div className="inline-block max-w-lg text-center justify-center">
|
||||
<h1 className={title()}>About</h1>
|
||||
</div>
|
||||
</section>
|
||||
</DefaultLayout>
|
||||
);
|
||||
}
|
||||
14
src/pages/blog.tsx
Normal file
14
src/pages/blog.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { title } from "@/components/primitives";
|
||||
import DefaultLayout from "@/layouts/default";
|
||||
|
||||
export default function DocsPage() {
|
||||
return (
|
||||
<DefaultLayout>
|
||||
<section className="flex flex-col items-center justify-center gap-4 py-8 md:py-10">
|
||||
<div className="inline-block max-w-lg text-center justify-center">
|
||||
<h1 className={title()}>Blog</h1>
|
||||
</div>
|
||||
</section>
|
||||
</DefaultLayout>
|
||||
);
|
||||
}
|
||||
14
src/pages/docs.tsx
Normal file
14
src/pages/docs.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { title } from "@/components/primitives";
|
||||
import DefaultLayout from "@/layouts/default";
|
||||
|
||||
export default function DocsPage() {
|
||||
return (
|
||||
<DefaultLayout>
|
||||
<section className="flex flex-col items-center justify-center gap-4 py-8 md:py-10">
|
||||
<div className="inline-block max-w-lg text-center justify-center">
|
||||
<h1 className={title()}>Docs</h1>
|
||||
</div>
|
||||
</section>
|
||||
</DefaultLayout>
|
||||
);
|
||||
}
|
||||
60
src/pages/index.tsx
Normal file
60
src/pages/index.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import { Link } from "@heroui/link";
|
||||
import { Snippet } from "@heroui/snippet";
|
||||
import { Code } from "@heroui/code";
|
||||
import { button as buttonStyles } from "@heroui/theme";
|
||||
|
||||
import { siteConfig } from "@/config/site";
|
||||
import { title, subtitle } from "@/components/primitives";
|
||||
import { GithubIcon } from "@/components/icons";
|
||||
import DefaultLayout from "@/layouts/default";
|
||||
|
||||
export default function IndexPage() {
|
||||
return (
|
||||
<DefaultLayout>
|
||||
<section className="flex flex-col items-center justify-center gap-4 py-8 md:py-10">
|
||||
<div className="inline-block max-w-lg text-center justify-center">
|
||||
<span className={title()}>Make </span>
|
||||
<span className={title({ color: "violet" })}>beautiful </span>
|
||||
<br />
|
||||
<span className={title()}>
|
||||
websites regardless of your design experience.
|
||||
</span>
|
||||
<div className={subtitle({ class: "mt-4" })}>
|
||||
Beautiful, fast and modern React UI library.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<Link
|
||||
isExternal
|
||||
className={buttonStyles({
|
||||
color: "primary",
|
||||
radius: "full",
|
||||
variant: "shadow",
|
||||
})}
|
||||
href={siteConfig.links.docs}
|
||||
>
|
||||
Documentation
|
||||
</Link>
|
||||
<Link
|
||||
isExternal
|
||||
className={buttonStyles({ variant: "bordered", radius: "full" })}
|
||||
href={siteConfig.links.github}
|
||||
>
|
||||
<GithubIcon size={20} />
|
||||
GitHub
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="mt-8">
|
||||
<Snippet hideCopyButton hideSymbol variant="bordered">
|
||||
<span>
|
||||
Get started by editing{" "}
|
||||
<Code color="primary">pages/index.tsx</Code>
|
||||
</span>
|
||||
</Snippet>
|
||||
</div>
|
||||
</section>
|
||||
</DefaultLayout>
|
||||
);
|
||||
}
|
||||
14
src/pages/pricing.tsx
Normal file
14
src/pages/pricing.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { title } from "@/components/primitives";
|
||||
import DefaultLayout from "@/layouts/default";
|
||||
|
||||
export default function DocsPage() {
|
||||
return (
|
||||
<DefaultLayout>
|
||||
<section className="flex flex-col items-center justify-center gap-4 py-8 md:py-10">
|
||||
<div className="inline-block max-w-lg text-center justify-center">
|
||||
<h1 className={title()}>Pricing</h1>
|
||||
</div>
|
||||
</section>
|
||||
</DefaultLayout>
|
||||
);
|
||||
}
|
||||
20
src/provider.tsx
Normal file
20
src/provider.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { NavigateOptions } from "react-router-dom";
|
||||
|
||||
import { HeroUIProvider } from "@heroui/system";
|
||||
import { useHref, useNavigate } from "react-router-dom";
|
||||
|
||||
declare module "@react-types/shared" {
|
||||
interface RouterConfig {
|
||||
routerOptions: NavigateOptions;
|
||||
}
|
||||
}
|
||||
|
||||
export function Provider({ children }: { children: React.ReactNode }) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<HeroUIProvider navigate={navigate} useHref={useHref}>
|
||||
{children}
|
||||
</HeroUIProvider>
|
||||
);
|
||||
}
|
||||
3
src/styles/globals.css
Normal file
3
src/styles/globals.css
Normal file
@@ -0,0 +1,3 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@config "../../tailwind.config.js"
|
||||
5
src/types/index.ts
Normal file
5
src/types/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { SVGProps } from "react";
|
||||
|
||||
export type IconSvgProps = SVGProps<SVGSVGElement> & {
|
||||
size?: number;
|
||||
};
|
||||
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" />
|
||||
17
tailwind.config.js
Normal file
17
tailwind.config.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import {heroui} from "@heroui/theme"
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./index.html",
|
||||
'./src/layouts/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
"./node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
darkMode: "class",
|
||||
plugins: [heroui()],
|
||||
}
|
||||
28
tsconfig.json
Normal file
28
tsconfig.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
11
tsconfig.node.json
Normal file
11
tsconfig.node.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
5
vercel.json
Normal file
5
vercel.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"rewrites": [
|
||||
{ "source": "/(.*)", "destination": "/" }
|
||||
]
|
||||
}
|
||||
9
vite.config.ts
Normal file
9
vite.config.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import tsconfigPaths from "vite-tsconfig-paths";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react(), tsconfigPaths(), tailwindcss()],
|
||||
});
|
||||
BIN
李永政纯前端chatbot.zip
Normal file
BIN
李永政纯前端chatbot.zip
Normal file
Binary file not shown.
Reference in New Issue
Block a user