add code generation feature

This commit is contained in:
Zhang Minghan 2023-09-18 21:26:27 +08:00
parent f10b26ec5f
commit a6ebbb2802
7 changed files with 247 additions and 0 deletions

View File

@ -0,0 +1,88 @@
.generation-page {
position: relative;
display: flex;
width: 100%;
height: calc(100vh - 56px);
.login-action {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin: auto;
transform: translateY(-28px);
.tip {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: max(6vh, 24px);
user-select: none;
}
.text {
font-size: 1rem;
}
}
}
.generation-container {
display: flex;
flex-direction: column;
padding: 12px 16px;
gap: 6px;
width: 100%;
height: 100%;
.action {
flex-shrink: 0;
}
.generation-wrapper {
display: flex;
flex-direction: column;
align-items: center;
flex-grow: 1;
padding: 15vh 0;
gap: 2rem;
.product {
display: flex;
flex-direction: row;
align-items: center;
text-align: center;
font-size: 2rem;
gap: 12px;
user-select: none;
img {
width: 3rem;
height: 3rem;
}
}
}
}
.input-box {
display: flex;
flex-direction: row;
width: 80%;
gap: 8px;
margin: 0 auto;
max-width: 680px;
.input {
flex-grow: 1;
text-align: center;
font-size: 1.25rem;
height: 46px;
border-radius: var(--radius);
border: 1px solid hsl(var(--border-hover));
letter-spacing: 1px;
}
.action {
width: 46px;
height: 46px;
}
}

View File

@ -1,3 +1,4 @@
@import "ui";
@font-family: Andika,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";
@line-height: 1.5;
@font-weight: 400;

27
app/src/assets/ui.less Normal file
View File

@ -0,0 +1,27 @@
.select-group {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 8px;
padding: 6px 8px;
border-radius: 4px;
user-select: none;
.select-group-item {
padding: 0.35rem 0.5rem;
border-radius: 4px;
transition: .2s;
cursor: pointer;
font-size: 16px;
background: hsl(var(--accent-secondary));
&:hover {
background: hsl(var(--accent));
}
&.active {
background: hsl(var(--text));
color: hsl(var(--background));
}
}
}

View File

@ -0,0 +1,25 @@
type SelectGroupProps = {
current: string,
list: string[],
onChange?: (select: string) => void,
}
function SelectGroup(props: SelectGroupProps) {
return (
<div className={`select-group`}>
{
props.list.map((select: string, idx: number) => (
<div
key={idx}
onClick={() => props.onChange?.(select)}
className={`select-group-item ${select == props.current ? 'active' : ''}`}
>
{ select }
</div>
))
}
</div>
)
}
export default SelectGroup;

View File

@ -11,6 +11,7 @@ const resources = {
"not-found": "Page not found",
home: "Home",
login: "Login",
"login-require": "You need to login to use this feature",
logout: "Logout",
quota: "Quota",
"try-again": "Try again",
@ -151,6 +152,7 @@ const resources = {
"not-found": "页面未找到",
home: "首页",
login: "登录",
"login-require": "您需要登录才能使用此功能",
logout: "登出",
quota: "配额",
"try-again": "重试",
@ -281,6 +283,7 @@ const resources = {
"not-found": "Страница не найдена",
home: "Главная",
login: "Войти",
"login-require": "Вам нужно войти, чтобы использовать эту функцию",
logout: "Выйти",
quota: "Квота",
"try-again": "Попробуйте еще раз",

View File

@ -2,6 +2,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";
import Generation from "./routes/Generation.tsx";
const router = createBrowserRouter([
{
@ -16,6 +17,11 @@ const router = createBrowserRouter([
Component: Auth,
ErrorBoundary: NotFound,
},
{
id: "generation",
path: "/generate",
Component: Generation,
}
]);
export default router;

View File

@ -0,0 +1,97 @@
import "../assets/generation.less";
import {useSelector} from "react-redux";
import {selectAuthenticated} from "../store/auth.ts";
import {useTranslation} from "react-i18next";
import {Button} from "../components/ui/button.tsx";
import {ChevronLeft, Info, LogIn, Send} from "lucide-react";
import {login} from "../conf.ts";
import router from "../router.ts";
import {Input} from "../components/ui/input.tsx";
import {useEffect, useRef, useState} from "react";
import SelectGroup from "../components/SelectGroup.tsx";
type WrapperProps = {
onSend?: (value: string) => boolean,
}
function Wrapper(props: WrapperProps) {
const ref = useRef(null);
const [ model, setModel ] = useState('GPT-3.5');
function handleSend() {
const target = ref.current as HTMLInputElement | null;
if (!target) return;
const value = target.value.trim();
if (!value.length) return;
if (props.onSend?.(value)) {
target.value = '';
}
}
useEffect(() => {
ref.current && (ref.current as HTMLInputElement).focus();
ref.current && (ref.current as HTMLInputElement).addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
handleSend();
}
});
})
return (
<div className={`generation-wrapper`}>
<div className={`product`}>
<img src={`/favicon.ico`} alt={""} />
AI Code Generator
</div>
<div className={`input-box`}>
<Input className={`input`} ref={ref} />
<Button size={`icon`} className={`action`} variant={`default`} onClick={handleSend}>
<Send className={`h-5 w-5`} />
</Button>
</div>
<div className={`model-box`}>
<SelectGroup current={model} list={[
'GPT-3.5', 'GPT-3.5-16k', 'GPT-4', 'GPT-4-32k'
]} onChange={setModel} />
</div>
</div>
)
}
function Generation() {
const { t } = useTranslation();
const auth = useSelector(selectAuthenticated);
return (
<div className={`generation-page`}>
{
auth ?
<div className={`generation-container`}>
<Button className={`action`} variant={`ghost`} size={`icon`} onClick={
() => router.navigate("/")
}>
<ChevronLeft className={`h-5 w-5 back`} />
</Button>
<Wrapper onSend={(value: string) => {
console.log(value);
return true;
}} />
</div> :
<div className={`login-action`}>
<div className={`tip`}>
<Info className={`h-4 w-4 mr-2`} />
{ t('login-require') }
</div>
<Button size={`lg`} onClick={login}>
<LogIn className={`h-4 w-4 mr-2`} />
<p className={`text`}>
{ t('login') }
</p>
</Button>
</div>
}
</div>
)
}
export default Generation;