mirror of
https://github.com/coaidev/coai.git
synced 2025-05-23 06:50:14 +09:00
update login page and operation interface
This commit is contained in:
parent
c4b84f038b
commit
36ea2ba04e
@ -11,7 +11,14 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.5",
|
||||
"@radix-ui/react-label": "^2.0.2",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@radix-ui/react-switch": "^1.0.3",
|
||||
"@radix-ui/react-toast": "^1.1.4",
|
||||
"@radix-ui/react-toggle": "^1.0.3",
|
||||
"@radix-ui/react-tooltip": "^1.0.6",
|
||||
"@reduxjs/toolkit": "^1.9.5",
|
||||
"axios": "^1.5.0",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"localforage": "^1.10.0",
|
||||
@ -19,7 +26,10 @@
|
||||
"match-sorter": "^6.3.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-markdown": "^8.0.7",
|
||||
"react-redux": "^8.1.2",
|
||||
"react-router-dom": "^6.15.0",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"sort-by": "^1.2.0",
|
||||
"tailwind-merge": "^1.14.0",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
|
955
renio/pnpm-lock.yaml
generated
955
renio/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -3,19 +3,75 @@ import "./assets/navbar.less";
|
||||
import ModeToggle, {ThemeProvider} from "./components/ThemeProvider.tsx";
|
||||
import {Button} from "./components/ui/button.tsx";
|
||||
import router from "./router.ts";
|
||||
import I18nProvider from "./components/I18nProvider.tsx";
|
||||
import ProjectLink from "./components/ProjectLink.tsx";
|
||||
import {Menu} from "lucide-react";
|
||||
import {Provider, useDispatch, useSelector} from "react-redux";
|
||||
import {toggleMenu} from "./store/menu.ts";
|
||||
import store from "./store/index.ts";
|
||||
import {logout, selectAuthenticated, selectUsername, validateToken} from "./store/auth.ts";
|
||||
import {useEffect} from "react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "./components/ui/dropdown-menu.tsx";
|
||||
import {Toaster} from "./components/ui/toaster.tsx";
|
||||
import {login} from "./conf.ts";
|
||||
|
||||
function login() {
|
||||
location.href = "https://deeptrain.lightxi.com/login?app=chatnio"
|
||||
function Settings() {
|
||||
const dispatch = useDispatch();
|
||||
const username = useSelector(selectUsername);
|
||||
|
||||
return (
|
||||
<div className={`avatar`}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant={`ghost`} size={`icon`}>
|
||||
<img src={`https://api.deeptrain.net/avatar/${username}`} alt="" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align={`end`}>
|
||||
<DropdownMenuLabel>My Account</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>Quota</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Button size={`sm`} className={`action-button`} onClick={() => dispatch(logout())}>
|
||||
Logout
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function NavBar() {
|
||||
const dispatch = useDispatch();
|
||||
useEffect(() => {
|
||||
validateToken(dispatch, localStorage.getItem("token") ?? "");
|
||||
}, []);
|
||||
const auth = useSelector(selectAuthenticated);
|
||||
|
||||
return (
|
||||
<nav className={`navbar`}>
|
||||
<div className={`items`}>
|
||||
<Button size={`icon`} variant={`ghost`} onClick={() => dispatch(toggleMenu())}>
|
||||
<Menu />
|
||||
</Button>
|
||||
<img className={`logo`} src="/favicon.ico" alt="" onClick={() => router.navigate('/')} />
|
||||
<div className={`grow`} />
|
||||
<ProjectLink />
|
||||
<ModeToggle />
|
||||
<Button size={`sm`} onClick={login}>Login</Button>
|
||||
<I18nProvider />
|
||||
{
|
||||
auth ?
|
||||
<Settings />
|
||||
: <Button size={`sm`} onClick={login}>Login</Button>
|
||||
}
|
||||
</div>
|
||||
</nav>
|
||||
)
|
||||
@ -23,11 +79,12 @@ function NavBar() {
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<>
|
||||
<Provider store={store}>
|
||||
<NavBar />
|
||||
<ThemeProvider />
|
||||
<RouterProvider router={router} />
|
||||
</>
|
||||
<Toaster />
|
||||
</Provider>
|
||||
)
|
||||
}
|
||||
|
||||
|
6
renio/src/assets/auth.less
Normal file
6
renio/src/assets/auth.less
Normal file
@ -0,0 +1,6 @@
|
||||
.auth {
|
||||
width: 100%;
|
||||
height: calc(100vh - 56px);
|
||||
overflow: hidden;
|
||||
background: hsl(var(--background-container));
|
||||
}
|
@ -23,8 +23,9 @@
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
--accent: 240 4.8% 95.9%;
|
||||
--accent-secondary: 240 5.9% 10%;
|
||||
--accent-foreground: 240 5.9% 10%;
|
||||
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
@ -32,6 +33,8 @@
|
||||
--border: 42 24.2% 92.2%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--ring: 222.2 84% 4.9%;
|
||||
--text: 0 0% 0%;
|
||||
--text-secondary: 0 0% 20%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
|
||||
@ -58,15 +61,18 @@
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
|
||||
--accent: 217.2 32.6% 17.5%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
--accent: 240 3.7% 15.9%;
|
||||
--accent-secondary: 240 3.7% 15.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--border: 0 0% 20%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
--ring: 212.7 26.8% 83.9%;
|
||||
--border: 240 3.7% 15.9%;
|
||||
--input: 240 3.7% 15.9%;
|
||||
--ring: 240 4.9% 83.9%;
|
||||
--text: 0 0% 100%;
|
||||
--text-secondary: 0 0% 80%;
|
||||
|
||||
--background-sidebar: rgba(0, 0, 0, 0.25);
|
||||
}
|
@ -11,15 +11,121 @@
|
||||
.sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 260px;
|
||||
width: 0;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background: var(--background-sidebar);
|
||||
transition: 0.2s ease-in-out;
|
||||
transition-property: width, background, box-shadow;
|
||||
border-right: 0;
|
||||
|
||||
&.open {
|
||||
width: 260px;
|
||||
border-right: 1px solid hsl(var(--border));
|
||||
}
|
||||
|
||||
.login-action {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin: auto;
|
||||
width: max-content;
|
||||
|
||||
svg {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
&.open {
|
||||
width: max(30vw, 180px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 468px) {
|
||||
// sidebar collapsed
|
||||
&.open {
|
||||
width: 100% !important;
|
||||
}
|
||||
&.open ~ .chat-container {
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
background: hsl(var(--background-container));
|
||||
transition: width 0.2s ease-in-out;
|
||||
|
||||
.chat-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
user-select: none;
|
||||
|
||||
strong {
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-content {
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
height: max-content;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
padding: 6px 24px;
|
||||
|
||||
.input-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
width: 100%;
|
||||
gap: 4px;
|
||||
height: min-content;
|
||||
|
||||
.input-box {
|
||||
text-align: center;
|
||||
color: hsl(var(--text));
|
||||
}
|
||||
|
||||
.input-box::placeholder {
|
||||
color: hsl(var(--text-secondary));
|
||||
}
|
||||
|
||||
.send-button {
|
||||
padding: 0 6px;
|
||||
}
|
||||
|
||||
.send-button svg {
|
||||
fill: hsl(var(--text));
|
||||
}
|
||||
}
|
||||
|
||||
.input-options {
|
||||
width: max-content;
|
||||
margin: 16px auto 2px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
gap: 4px;
|
||||
height: min-content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
68
renio/src/assets/loader.less
Normal file
68
renio/src/assets/loader.less
Normal file
@ -0,0 +1,68 @@
|
||||
.loader-wrapper {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
vertical-align: center;
|
||||
text-align: center;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
gap: 20px;
|
||||
transform: translate(-50%, -50%);
|
||||
margin-top: -28px;
|
||||
|
||||
p {
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
|
||||
&:after {
|
||||
content: '.';
|
||||
color: hsl(var(--text-secondary));
|
||||
animation: dots 1s steps(5, end) infinite;
|
||||
|
||||
@keyframes dots {
|
||||
0%, 20% {
|
||||
color: rgba(0, 0, 0, 0);
|
||||
text-shadow:
|
||||
.25em 0 0 rgba(0, 0, 0, 0),
|
||||
.5em 0 0 rgba(0, 0, 0, 0);
|
||||
}
|
||||
40% {
|
||||
color: hsl(var(--text-secondary));
|
||||
text-shadow:
|
||||
.25em 0 0 rgba(0, 0, 0, 0),
|
||||
.5em 0 0 rgba(0, 0, 0, 0);
|
||||
}
|
||||
60% {
|
||||
text-shadow:
|
||||
.25em 0 0 hsl(var(--text-secondary)),
|
||||
.5em 0 0 rgba(0, 0, 0, 0);
|
||||
}
|
||||
80%, 100% {
|
||||
text-shadow:
|
||||
.25em 0 0 hsl(var(--text-secondary)),
|
||||
.5em 0 0 hsl(var(--text-secondary));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.loader {
|
||||
border: 4px solid hsl(var(--text));
|
||||
border-left-color: transparent;
|
||||
border-radius: 50%;
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
animation: SpinAnimation 1s linear infinite;
|
||||
|
||||
@keyframes SpinAnimation {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -15,6 +15,10 @@ html, body {
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
touch-action: pan-y;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
:root {
|
||||
@ -31,6 +35,29 @@ html, body {
|
||||
-webkit-text-size-adjust: @-webkit-text-size-adjust;
|
||||
}
|
||||
|
||||
* {
|
||||
outline: 0;
|
||||
box-sizing: border-box;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.grow {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hover\:bg-accent[aria-pressed="false"] {
|
||||
color: hsl(var(--text-secondary));
|
||||
|
||||
&:hover {
|
||||
background: hsl(var(--accent-secondary));
|
||||
}
|
||||
}
|
||||
|
||||
.hover\:bg-accent[aria-pressed="true"] {
|
||||
background: hsl(var(--accent));
|
||||
color: hsl(var(--text));
|
||||
}
|
||||
|
@ -17,7 +17,7 @@
|
||||
flex-direction: row;
|
||||
align-content: center;
|
||||
vertical-align: center;
|
||||
gap: 8px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
@ -27,3 +27,32 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar {
|
||||
outline: 0;
|
||||
user-select: none;
|
||||
|
||||
img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
padding: 2px;
|
||||
border-radius: var(--radius);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
div[data-radix-popper-content-wrapper=""] {
|
||||
user-select: none;
|
||||
|
||||
div.relative {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
margin-top: 6px;
|
||||
margin-bottom: 2px;
|
||||
height: max-content !important;
|
||||
}
|
||||
}
|
||||
|
26
renio/src/components/I18nProvider.tsx
Normal file
26
renio/src/components/I18nProvider.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import {Button} from "./ui/button.tsx";
|
||||
import {Languages} from "lucide-react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "./ui/dropdown-menu.tsx";
|
||||
|
||||
function I18nProvider() {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="icon">
|
||||
<Languages className="absolute h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem>简体中文</DropdownMenuItem>
|
||||
<DropdownMenuItem>English</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
|
||||
export default I18nProvider
|
17
renio/src/components/Loader.tsx
Normal file
17
renio/src/components/Loader.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import '../assets/loader.less'
|
||||
|
||||
type LoaderProps = {
|
||||
className?: string,
|
||||
prompt?: string,
|
||||
}
|
||||
|
||||
function Loader({ className, prompt }: LoaderProps) {
|
||||
return (
|
||||
<div className={`loader-wrapper ${className}`}>
|
||||
<div className={`loader`} />
|
||||
<p>{prompt}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Loader
|
13
renio/src/components/ProjectLink.tsx
Normal file
13
renio/src/components/ProjectLink.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import {Button} from "./ui/button.tsx";
|
||||
|
||||
function ProjectLink() {
|
||||
return (
|
||||
<Button variant="outline" size="icon" onClick={() => window.open("https://github.com/zmh-program/chatnio")}>
|
||||
<svg viewBox="0 0 438.549 438.549" className="absolute h-[1.2rem] w-[1.2rem] rotate-0 scale-100">
|
||||
<path fill="currentColor" d="M409.132 114.573c-19.608-33.596-46.205-60.194-79.798-79.8-33.598-19.607-70.277-29.408-110.063-29.408-39.781 0-76.472 9.804-110.063 29.408-33.596 19.605-60.192 46.204-79.8 79.8C9.803 148.168 0 184.854 0 224.63c0 47.78 13.94 90.745 41.827 128.906 27.884 38.164 63.906 64.572 108.063 79.227 5.14.954 8.945.283 11.419-1.996 2.475-2.282 3.711-5.14 3.711-8.562 0-.571-.049-5.708-.144-15.417a2549.81 2549.81 0 01-.144-25.406l-6.567 1.136c-4.187.767-9.469 1.092-15.846 1-6.374-.089-12.991-.757-19.842-1.999-6.854-1.231-13.229-4.086-19.13-8.559-5.898-4.473-10.085-10.328-12.56-17.556l-2.855-6.57c-1.903-4.374-4.899-9.233-8.992-14.559-4.093-5.331-8.232-8.945-12.419-10.848l-1.999-1.431c-1.332-.951-2.568-2.098-3.711-3.429-1.142-1.331-1.997-2.663-2.568-3.997-.572-1.335-.098-2.43 1.427-3.289 1.525-.859 4.281-1.276 8.28-1.276l5.708.853c3.807.763 8.516 3.042 14.133 6.851 5.614 3.806 10.229 8.754 13.846 14.842 4.38 7.806 9.657 13.754 15.846 17.847 6.184 4.093 12.419 6.136 18.699 6.136 6.28 0 11.704-.476 16.274-1.423 4.565-.952 8.848-2.383 12.847-4.285 1.713-12.758 6.377-22.559 13.988-29.41-10.848-1.14-20.601-2.857-29.264-5.14-8.658-2.286-17.605-5.996-26.835-11.14-9.235-5.137-16.896-11.516-22.985-19.126-6.09-7.614-11.088-17.61-14.987-29.979-3.901-12.374-5.852-26.648-5.852-42.826 0-23.035 7.52-42.637 22.557-58.817-7.044-17.318-6.379-36.732 1.997-58.24 5.52-1.715 13.706-.428 24.554 3.853 10.85 4.283 18.794 7.952 23.84 10.994 5.046 3.041 9.089 5.618 12.135 7.708 17.705-4.947 35.976-7.421 54.818-7.421s37.117 2.474 54.823 7.421l10.849-6.849c7.419-4.57 16.18-8.758 26.262-12.565 10.088-3.805 17.802-4.853 23.134-3.138 8.562 21.509 9.325 40.922 2.279 58.24 15.036 16.18 22.559 35.787 22.559 58.817 0 16.178-1.958 30.497-5.853 42.966-3.9 12.471-8.941 22.457-15.125 29.979-6.191 7.521-13.901 13.85-23.131 18.986-9.232 5.14-18.182 8.85-26.84 11.136-8.662 2.286-18.415 4.004-29.263 5.146 9.894 8.562 14.842 22.077 14.842 40.539v60.237c0 3.422 1.19 6.279 3.572 8.562 2.379 2.279 6.136 2.95 11.276 1.995 44.163-14.653 80.185-41.062 108.068-79.226 27.88-38.161 41.825-81.126 41.825-128.906-.01-39.771-9.818-76.454-29.414-110.049z"></path>
|
||||
</svg>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export default ProjectLink
|
25
renio/src/components/ui/input.tsx
Normal file
25
renio/src/components/ui/input.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "./lib/utils"
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Input.displayName = "Input"
|
||||
|
||||
export { Input }
|
24
renio/src/components/ui/label.tsx
Normal file
24
renio/src/components/ui/label.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "./lib/utils"
|
||||
|
||||
const labelVariants = cva(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
)
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||
VariantProps<typeof labelVariants>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(labelVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Label.displayName = LabelPrimitive.Root.displayName
|
||||
|
||||
export { Label }
|
27
renio/src/components/ui/switch.tsx
Normal file
27
renio/src/components/ui/switch.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import * as React from "react"
|
||||
import * as SwitchPrimitives from "@radix-ui/react-switch"
|
||||
|
||||
import { cn } from "./lib/utils"
|
||||
|
||||
const Switch = React.forwardRef<
|
||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SwitchPrimitives.Root
|
||||
className={cn(
|
||||
"peer inline-flex h-[24px] w-[44px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cn(
|
||||
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
))
|
||||
Switch.displayName = SwitchPrimitives.Root.displayName
|
||||
|
||||
export { Switch }
|
24
renio/src/components/ui/textarea.tsx
Normal file
24
renio/src/components/ui/textarea.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "./lib/utils"
|
||||
|
||||
export interface TextareaProps
|
||||
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
||||
|
||||
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
({ className, ...props }, ref) => {
|
||||
return (
|
||||
<textarea
|
||||
className={cn(
|
||||
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Textarea.displayName = "Textarea"
|
||||
|
||||
export { Textarea }
|
127
renio/src/components/ui/toast.tsx
Normal file
127
renio/src/components/ui/toast.tsx
Normal file
@ -0,0 +1,127 @@
|
||||
import * as React from "react"
|
||||
import * as ToastPrimitives from "@radix-ui/react-toast"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { X } from "lucide-react"
|
||||
|
||||
import { cn } from "./lib/utils"
|
||||
|
||||
const ToastProvider = ToastPrimitives.Provider
|
||||
|
||||
const ToastViewport = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Viewport>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Viewport
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
|
||||
|
||||
const toastVariants = cva(
|
||||
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "border bg-background text-foreground",
|
||||
destructive:
|
||||
"destructive group border-destructive bg-destructive text-destructive-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const Toast = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
|
||||
VariantProps<typeof toastVariants>
|
||||
>(({ className, variant, ...props }, ref) => {
|
||||
return (
|
||||
<ToastPrimitives.Root
|
||||
ref={ref}
|
||||
className={cn(toastVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Toast.displayName = ToastPrimitives.Root.displayName
|
||||
|
||||
const ToastAction = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Action>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Action
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastAction.displayName = ToastPrimitives.Action.displayName
|
||||
|
||||
const ToastClose = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Close>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Close
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
|
||||
className
|
||||
)}
|
||||
toast-close=""
|
||||
{...props}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</ToastPrimitives.Close>
|
||||
))
|
||||
ToastClose.displayName = ToastPrimitives.Close.displayName
|
||||
|
||||
const ToastTitle = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Title
|
||||
ref={ref}
|
||||
className={cn("text-sm font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastTitle.displayName = ToastPrimitives.Title.displayName
|
||||
|
||||
const ToastDescription = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm opacity-90", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastDescription.displayName = ToastPrimitives.Description.displayName
|
||||
|
||||
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
|
||||
|
||||
type ToastActionElement = React.ReactElement<typeof ToastAction>
|
||||
|
||||
export {
|
||||
type ToastProps,
|
||||
type ToastActionElement,
|
||||
ToastProvider,
|
||||
ToastViewport,
|
||||
Toast,
|
||||
ToastTitle,
|
||||
ToastDescription,
|
||||
ToastClose,
|
||||
ToastAction,
|
||||
}
|
33
renio/src/components/ui/toaster.tsx
Normal file
33
renio/src/components/ui/toaster.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import {
|
||||
Toast,
|
||||
ToastClose,
|
||||
ToastDescription,
|
||||
ToastProvider,
|
||||
ToastTitle,
|
||||
ToastViewport,
|
||||
} from "./toast"
|
||||
import { useToast } from "./use-toast"
|
||||
|
||||
export function Toaster() {
|
||||
const { toasts } = useToast()
|
||||
|
||||
return (
|
||||
<ToastProvider>
|
||||
{toasts.map(function ({ id, title, description, action, ...props }) {
|
||||
return (
|
||||
<Toast key={id} {...props}>
|
||||
<div className="grid gap-1">
|
||||
{title && <ToastTitle>{title}</ToastTitle>}
|
||||
{description && (
|
||||
<ToastDescription>{description}</ToastDescription>
|
||||
)}
|
||||
</div>
|
||||
{action}
|
||||
<ToastClose />
|
||||
</Toast>
|
||||
)
|
||||
})}
|
||||
<ToastViewport />
|
||||
</ToastProvider>
|
||||
)
|
||||
}
|
43
renio/src/components/ui/toggle.tsx
Normal file
43
renio/src/components/ui/toggle.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import * as React from "react"
|
||||
import * as TogglePrimitive from "@radix-ui/react-toggle"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "./lib/utils"
|
||||
|
||||
const toggleVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-transparent",
|
||||
outline:
|
||||
"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-3",
|
||||
sm: "h-9 px-2.5",
|
||||
lg: "h-11 px-5",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const Toggle = React.forwardRef<
|
||||
React.ElementRef<typeof TogglePrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> &
|
||||
VariantProps<typeof toggleVariants>
|
||||
>(({ className, variant, size, ...props }, ref) => (
|
||||
<TogglePrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(toggleVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
Toggle.displayName = TogglePrimitive.Root.displayName
|
||||
|
||||
export { Toggle, toggleVariants }
|
28
renio/src/components/ui/tooltip.tsx
Normal file
28
renio/src/components/ui/tooltip.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import * as React from "react"
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
||||
|
||||
import { cn } from "./lib/utils"
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider
|
||||
|
||||
const Tooltip = TooltipPrimitive.Root
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
192
renio/src/components/ui/use-toast.ts
Normal file
192
renio/src/components/ui/use-toast.ts
Normal file
@ -0,0 +1,192 @@
|
||||
// Inspired by react-hot-toast library
|
||||
import * as React from "react"
|
||||
|
||||
import type {
|
||||
ToastActionElement,
|
||||
ToastProps,
|
||||
} from "./toast"
|
||||
|
||||
const TOAST_LIMIT = 1
|
||||
const TOAST_REMOVE_DELAY = 1000000
|
||||
|
||||
type ToasterToast = ToastProps & {
|
||||
id: string
|
||||
title?: React.ReactNode
|
||||
description?: React.ReactNode
|
||||
action?: ToastActionElement
|
||||
}
|
||||
|
||||
const actionTypes = {
|
||||
ADD_TOAST: "ADD_TOAST",
|
||||
UPDATE_TOAST: "UPDATE_TOAST",
|
||||
DISMISS_TOAST: "DISMISS_TOAST",
|
||||
REMOVE_TOAST: "REMOVE_TOAST",
|
||||
} as const
|
||||
|
||||
let count = 0
|
||||
|
||||
function genId() {
|
||||
count = (count + 1) % Number.MAX_VALUE
|
||||
return count.toString()
|
||||
}
|
||||
|
||||
type ActionType = typeof actionTypes
|
||||
|
||||
type Action =
|
||||
| {
|
||||
type: ActionType["ADD_TOAST"]
|
||||
toast: ToasterToast
|
||||
}
|
||||
| {
|
||||
type: ActionType["UPDATE_TOAST"]
|
||||
toast: Partial<ToasterToast>
|
||||
}
|
||||
| {
|
||||
type: ActionType["DISMISS_TOAST"]
|
||||
toastId?: ToasterToast["id"]
|
||||
}
|
||||
| {
|
||||
type: ActionType["REMOVE_TOAST"]
|
||||
toastId?: ToasterToast["id"]
|
||||
}
|
||||
|
||||
interface State {
|
||||
toasts: ToasterToast[]
|
||||
}
|
||||
|
||||
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
|
||||
|
||||
const addToRemoveQueue = (toastId: string) => {
|
||||
if (toastTimeouts.has(toastId)) {
|
||||
return
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
toastTimeouts.delete(toastId)
|
||||
dispatch({
|
||||
type: "REMOVE_TOAST",
|
||||
toastId: toastId,
|
||||
})
|
||||
}, TOAST_REMOVE_DELAY)
|
||||
|
||||
toastTimeouts.set(toastId, timeout)
|
||||
}
|
||||
|
||||
export const reducer = (state: State, action: Action): State => {
|
||||
switch (action.type) {
|
||||
case "ADD_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
||||
}
|
||||
|
||||
case "UPDATE_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === action.toast.id ? { ...t, ...action.toast } : t
|
||||
),
|
||||
}
|
||||
|
||||
case "DISMISS_TOAST": {
|
||||
const { toastId } = action
|
||||
|
||||
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
||||
// but I'll keep it here for simplicity
|
||||
if (toastId) {
|
||||
addToRemoveQueue(toastId)
|
||||
} else {
|
||||
state.toasts.forEach((toast) => {
|
||||
addToRemoveQueue(toast.id)
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === toastId || toastId === undefined
|
||||
? {
|
||||
...t,
|
||||
open: false,
|
||||
}
|
||||
: t
|
||||
),
|
||||
}
|
||||
}
|
||||
case "REMOVE_TOAST":
|
||||
if (action.toastId === undefined) {
|
||||
return {
|
||||
...state,
|
||||
toasts: [],
|
||||
}
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const listeners: Array<(state: State) => void> = []
|
||||
|
||||
let memoryState: State = { toasts: [] }
|
||||
|
||||
function dispatch(action: Action) {
|
||||
memoryState = reducer(memoryState, action)
|
||||
listeners.forEach((listener) => {
|
||||
listener(memoryState)
|
||||
})
|
||||
}
|
||||
|
||||
type Toast = Omit<ToasterToast, "id">
|
||||
|
||||
function toast({ ...props }: Toast) {
|
||||
const id = genId()
|
||||
|
||||
const update = (props: ToasterToast) =>
|
||||
dispatch({
|
||||
type: "UPDATE_TOAST",
|
||||
toast: { ...props, id },
|
||||
})
|
||||
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
|
||||
|
||||
dispatch({
|
||||
type: "ADD_TOAST",
|
||||
toast: {
|
||||
...props,
|
||||
id,
|
||||
open: true,
|
||||
onOpenChange: (open) => {
|
||||
if (!open) dismiss()
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
id: id,
|
||||
dismiss,
|
||||
update,
|
||||
}
|
||||
}
|
||||
|
||||
function useToast() {
|
||||
const [state, setState] = React.useState<State>(memoryState)
|
||||
|
||||
React.useEffect(() => {
|
||||
listeners.push(setState)
|
||||
return () => {
|
||||
const index = listeners.indexOf(setState)
|
||||
if (index > -1) {
|
||||
listeners.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}, [state])
|
||||
|
||||
return {
|
||||
...state,
|
||||
toast,
|
||||
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
|
||||
}
|
||||
}
|
||||
|
||||
export { useToast, toast }
|
17
renio/src/conf.ts
Normal file
17
renio/src/conf.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import axios from "axios";
|
||||
|
||||
export const deploy: boolean = true;
|
||||
export let rest_api: string = "http://localhost:8094";
|
||||
export let ws_api: string = "ws://localhost:8094";
|
||||
|
||||
if (deploy) {
|
||||
rest_api = "https://nioapi.fystart.cn";
|
||||
ws_api = "wss://nioapi.fystart.cn";
|
||||
}
|
||||
|
||||
export function login() {
|
||||
location.href = "https://deeptrain.lightxi.com/login?app=chatnio"
|
||||
}
|
||||
|
||||
axios.defaults.baseURL = rest_api;
|
||||
axios.defaults.headers.post["Content-Type"] = "application/json";
|
@ -1,8 +1,9 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App.tsx'
|
||||
import './conf.ts'
|
||||
import './assets/main.less'
|
||||
import '../app/globals.css'
|
||||
import './assets/globals.less'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
|
@ -1,6 +1,7 @@
|
||||
import {createBrowserRouter} from "react-router-dom";
|
||||
import Home from "./routes/Home.tsx";
|
||||
import NotFound from "./routes/NotFound.tsx";
|
||||
import Auth from "./routes/Auth.tsx";
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
@ -8,7 +9,13 @@ const router = createBrowserRouter([
|
||||
path: '/',
|
||||
Component: Home,
|
||||
ErrorBoundary: NotFound,
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'login',
|
||||
path: '/login',
|
||||
Component: Auth,
|
||||
ErrorBoundary: NotFound,
|
||||
},
|
||||
]);
|
||||
|
||||
export default router;
|
||||
|
72
renio/src/routes/Auth.tsx
Normal file
72
renio/src/routes/Auth.tsx
Normal file
@ -0,0 +1,72 @@
|
||||
import {useToast} from "../components/ui/use-toast.ts";
|
||||
import {useLocation} from "react-router-dom";
|
||||
import {ToastAction} from "../components/ui/toast.tsx";
|
||||
import {login} from "../conf.ts";
|
||||
import {useEffect} from "react";
|
||||
import Loader from "../components/Loader.tsx";
|
||||
import '../assets/auth.less';
|
||||
import axios from "axios";
|
||||
import {validateToken} from "../store/auth.ts";
|
||||
import {useDispatch} from "react-redux";
|
||||
import router from "../router.ts";
|
||||
|
||||
function Auth() {
|
||||
const { toast } = useToast();
|
||||
const dispatch = useDispatch();
|
||||
const search = new URLSearchParams(useLocation().search);
|
||||
const token = (search.get("token") || "").trim();
|
||||
|
||||
if (!token.length) {
|
||||
toast({
|
||||
title: "Invalid token",
|
||||
description: "Please try again.",
|
||||
action: (
|
||||
<ToastAction altText={"Try again"} onClick={login}>Try again</ToastAction>
|
||||
)
|
||||
});
|
||||
|
||||
setTimeout(login, 2500);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
axios.post('/login', { token })
|
||||
.then((res) => {
|
||||
const data = res.data;
|
||||
if (!data.status) {
|
||||
toast({
|
||||
title: "Invalid token",
|
||||
description: "Login failed! Please check your token expiration and try again.",
|
||||
action: (
|
||||
<ToastAction altText={"Try again"} onClick={login}>Try again</ToastAction>
|
||||
)
|
||||
});
|
||||
}
|
||||
else validateToken(dispatch, data.token, () => {
|
||||
toast({
|
||||
title: "Login successful",
|
||||
description: "You have been logged in successfully.",
|
||||
});
|
||||
|
||||
router.navigate('/');
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.debug(err);
|
||||
toast({
|
||||
title: "Server error",
|
||||
description: "There was an error logging you in. Please try again.",
|
||||
action: (
|
||||
<ToastAction altText={"Try again"} onClick={login}>Try again</ToastAction>
|
||||
)
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={`auth`}>
|
||||
<Loader prompt={`Login`} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Auth;
|
@ -1,15 +1,90 @@
|
||||
import "../assets/home.less";
|
||||
import {Input} from "../components/ui/input.tsx";
|
||||
import {Toggle} from "../components/ui/toggle.tsx";
|
||||
import {Globe, LogIn} from "lucide-react";
|
||||
import {Button} from "../components/ui/button.tsx";
|
||||
import {Switch} from "../components/ui/switch.tsx";
|
||||
import {Label} from "../components/ui/label.tsx";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "../components/ui/tooltip.tsx";
|
||||
import {useSelector} from "react-redux";
|
||||
import type {RootState} from "../store";
|
||||
import {selectAuthenticated} from "../store/auth.ts";
|
||||
import {login} from "../conf.ts";
|
||||
|
||||
function SideBar() {
|
||||
const open = useSelector((state: RootState) => state.menu.open);
|
||||
const auth = useSelector(selectAuthenticated);
|
||||
|
||||
return (
|
||||
<div className={`sidebar`}>
|
||||
<div className={`sidebar ${open ? "open" : ""}`}>
|
||||
{
|
||||
auth ?
|
||||
<div className={`sidebar-content`}></div>
|
||||
: <Button className={`login-action`} variant={`default`} onClick={login}>
|
||||
<LogIn className={`h-3 w-3 mr-2`} /> login
|
||||
</Button>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ChatWrapper() {
|
||||
type ChatWrapperProps = {
|
||||
onSend?: (message: string) => void
|
||||
}
|
||||
|
||||
function ChatWrapper({ onSend }: ChatWrapperProps) {
|
||||
function handleSend() {
|
||||
const target = document.getElementById("input") as HTMLInputElement;
|
||||
const message = target.value.trim();
|
||||
if (message.length > 0) {
|
||||
onSend?.(message);
|
||||
target.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter") {
|
||||
handleSend();
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={`chat-container`}>
|
||||
<div className={`chat-wrapper`}>
|
||||
<div className={`chat-content`}>
|
||||
</div>
|
||||
<div className={`chat-input`}>
|
||||
<div className={`input-wrapper`}>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Toggle aria-label="Toggle chatgpt web feature" variant={`outline`}>
|
||||
<Globe className="h-4 w-4" />
|
||||
</Toggle>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p className={`tooltip`}>Enable ChatGPT <strong>web</strong> feature</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<Input className={`input-box`} id={`input`} placeholder={`Write something...`} />
|
||||
<Button size={`icon`} variant="outline" className={`send-button`} onClick={handleSend}>
|
||||
<svg className="h-4 w-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" data-v-f9a7276b=""><path d="m21.426 11.095-17-8A1 1 0 0 0 3.03 4.242l1.212 4.849L12 12l-7.758 2.909-1.212 4.849a.998.998 0 0 0 1.396 1.147l17-8a1 1 0 0 0 0-1.81z"></path></svg>
|
||||
</Button>
|
||||
</div>
|
||||
<div className={`input-options`}>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch id="enable-gpt4" />
|
||||
<Label htmlFor="enable-gpt4">GPT-4</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -18,7 +93,7 @@ function Home() {
|
||||
return (
|
||||
<div className={`main`}>
|
||||
<SideBar />
|
||||
<ChatWrapper />
|
||||
<ChatWrapper onSend={console.log} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
60
renio/src/store/auth.ts
Normal file
60
renio/src/store/auth.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import {createSlice} from "@reduxjs/toolkit";
|
||||
import axios from "axios";
|
||||
|
||||
export const authSlice = createSlice({
|
||||
name: 'auth',
|
||||
initialState: {
|
||||
token: '',
|
||||
authenticated: false,
|
||||
username: '',
|
||||
},
|
||||
reducers: {
|
||||
setToken: (state, action) => {
|
||||
state.token = action.payload as string;
|
||||
axios.defaults.headers.common['Authorization'] = state.token;
|
||||
localStorage.setItem('token', state.token);
|
||||
},
|
||||
setAuthenticated: (state, action) => {
|
||||
state.authenticated = action.payload as boolean;
|
||||
},
|
||||
setUsername: (state, action) => {
|
||||
state.username = action.payload as string;
|
||||
},
|
||||
logout: (state) => {
|
||||
state.token = '';
|
||||
state.authenticated = false;
|
||||
state.username = '';
|
||||
axios.defaults.headers.common['Authorization'] = '';
|
||||
localStorage.removeItem('token');
|
||||
|
||||
location.reload();
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
export function validateToken(dispatch: any, token: string, hook?: () => any) {
|
||||
token = token.trim();
|
||||
dispatch(setToken(token));
|
||||
|
||||
if (token.length === 0) {
|
||||
dispatch(setAuthenticated(false));
|
||||
dispatch(setUsername(''));
|
||||
return;
|
||||
} else axios.post('/state')
|
||||
.then(res => {
|
||||
dispatch(setAuthenticated(res.data.status));
|
||||
dispatch(setUsername(res.data.user));
|
||||
hook && hook();
|
||||
})
|
||||
.catch(err => {
|
||||
// keep state
|
||||
console.debug(err);
|
||||
});
|
||||
}
|
||||
|
||||
export const selectAuthenticated = (state: any) => state.auth.authenticated;
|
||||
export const selectUsername = (state: any) => state.auth.username;
|
||||
|
||||
export const {setToken, setAuthenticated, setUsername, logout} = authSlice.actions;
|
||||
export default authSlice.reducer;
|
42
renio/src/store/chat.ts
Normal file
42
renio/src/store/chat.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import {createSlice} from "@reduxjs/toolkit";
|
||||
|
||||
type Message = {
|
||||
id: string;
|
||||
text: string;
|
||||
isBot: boolean;
|
||||
}
|
||||
|
||||
type initialStateType = {
|
||||
messages: Message[];
|
||||
gpt4: boolean;
|
||||
web: boolean;
|
||||
}
|
||||
|
||||
const chatSlice = createSlice({
|
||||
name: 'chat',
|
||||
initialState: {
|
||||
messages: [],
|
||||
gpt4: false,
|
||||
web: false,
|
||||
} as initialStateType,
|
||||
reducers: {
|
||||
setMessages: (state, action) => {
|
||||
state.messages = action.payload;
|
||||
},
|
||||
setGPT4: (state, action) => {
|
||||
state.gpt4 = action.payload;
|
||||
},
|
||||
setWeb: (state, action) => {
|
||||
state.web = action.payload;
|
||||
},
|
||||
addMessage: (state, action) => {
|
||||
state.messages.push(action.payload as Message);
|
||||
},
|
||||
setMessage: (state, action) => {
|
||||
state.messages[state.messages.length - 1] = action.payload as Message;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const {setMessages, addMessage, setMessage, setGPT4, setWeb} = chatSlice.actions;
|
||||
export default chatSlice.reducer;
|
16
renio/src/store/index.ts
Normal file
16
renio/src/store/index.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { configureStore } from '@reduxjs/toolkit'
|
||||
import menuReducer from './menu'
|
||||
import authReducer from './auth'
|
||||
|
||||
const store = configureStore({
|
||||
reducer: {
|
||||
menu: menuReducer,
|
||||
auth: authReducer,
|
||||
},
|
||||
});
|
||||
|
||||
type RootState = ReturnType<typeof store.getState>;
|
||||
type AppDispatch = typeof store.dispatch;
|
||||
|
||||
export type {RootState, AppDispatch};
|
||||
export default store;
|
25
renio/src/store/menu.ts
Normal file
25
renio/src/store/menu.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import {createSlice} from "@reduxjs/toolkit";
|
||||
|
||||
export const menuSlice = createSlice({
|
||||
name: 'menu',
|
||||
initialState: {
|
||||
open: false,
|
||||
},
|
||||
reducers: {
|
||||
toggleMenu: (state) => {
|
||||
state.open = !state.open
|
||||
},
|
||||
closeMenu: (state) => {
|
||||
state.open = false;
|
||||
},
|
||||
openMenu: (state) => {
|
||||
state.open = true;
|
||||
},
|
||||
setMenu: (state, action) => {
|
||||
state.open = action.payload as boolean;
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export const {toggleMenu, closeMenu, openMenu, setMenu} = menuSlice.actions;
|
||||
export default menuSlice.reducer;
|
13
renio/src/store/utils.ts
Normal file
13
renio/src/store/utils.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
|
||||
export function dispatchWrapper(action: (state: any, payload?: any) => any) {
|
||||
return (payload?: any) => {
|
||||
const dispatch = useDispatch();
|
||||
dispatch(action(payload));
|
||||
};
|
||||
}
|
||||
|
||||
export function getSelector(reducer: string, key: string) {
|
||||
return useSelector((state: any) => state[reducer][key]);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user