update generation frontend

This commit is contained in:
Zhang Minghan 2023-09-23 18:43:09 +08:00
parent 2c15612bd7
commit f68b64a732
8 changed files with 306 additions and 13 deletions

View File

@ -46,6 +46,88 @@
padding: 15vh 0;
gap: 2rem;
.box {
display: flex;
flex-direction: column;
align-items: center;
width: 80%;
max-width: 680px;
height: max-content;
margin: 6px 0;
gap: 12px;
.message-box {
width: 100%;
height: max-content;
border-radius: var(--radius);
border: 1px solid hsl(var(--border));
color: hsl(var(--text-secondary));
padding: 0.6rem 1rem;
font-size: 10px;
overflow: hidden;
white-space: pre-wrap;
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-all;
}
.quota-box {
display: flex;
flex-direction: row;
align-items: center;
width: max-content;
height: max-content;
border: 1px solid hsl(var(--border));
border-radius: var(--radius);
user-select: none;
padding: 4px 12px;
transition: .2s;
cursor: pointer;
&:hover {
border: 1px solid hsl(var(--border-hover));
}
}
.hash-box {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 1rem;
flex-wrap: wrap;
width: max-content;
height: max-content;
padding: 1rem 1.5rem;
.download-box {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.5rem;
min-width: 10rem;
width: max-content;
height: max-content;
padding: 1rem 1.6rem;
text-decoration: none;
cursor: pointer;
user-select: none;
border-radius: var(--radius);
border: 1px solid hsl(var(--border));
transition: .2s;
color: hsl(var(--text));
font-size: 1rem;
font-weight: 500;
background: hsl(var(--background-container));
&:hover {
border: 1px solid hsl(var(--border-hover));
}
}
}
}
.product {
display: flex;
flex-direction: row;

View File

@ -6,6 +6,7 @@
padding: 6px 8px;
border-radius: 4px;
user-select: none;
justify-content: center;
.select-group-item {
padding: 0.35rem 0.5rem;

View File

@ -0,0 +1,118 @@
import {ws_api} from "../conf.ts";
export const endpoint = `${ws_api}/generation/create`;
export type GenerationForm = {
token: string;
prompt: string;
model: string;
}
export type GenerationSegmentResponse = {
message: string;
quota: number;
end: boolean;
error: string;
hash: string;
}
export type MessageEvent = {
message: string;
quota: number;
}
export class GenerationManager {
protected processing: boolean;
protected connection: WebSocket | null;
protected message: string;
protected onProcessingChange?: (processing: boolean) => void;
protected onMessage?: (message: MessageEvent) => void;
protected onError?: (error: string) => void;
protected onFinished?: (hash: string) => void;
constructor() {
this.processing = false;
this.connection = null;
this.message = "";
}
public setProcessingChangeHandler(handler: (processing: boolean) => void): void {
this.onProcessingChange = handler;
}
public setMessageHandler(handler: (message: MessageEvent) => void): void {
this.onMessage = handler;
}
public setErrorHandler(handler: (error: string) => void): void {
this.onError = handler;
}
public setFinishedHandler(handler: (hash: string) => void): void {
this.onFinished = handler;
}
public isProcessing(): boolean {
return this.processing;
}
protected setProcessing(processing: boolean): boolean {
this.processing = processing;
if (!processing) {
this.connection = null;
this.message = "";
}
this.onProcessingChange?.(processing);
return processing;
}
public getConnection(): WebSocket | null {
return this.connection;
}
protected handleMessage(message: GenerationSegmentResponse): void {
if (message.error && message.end) {
this.onError?.(message.error);
this.setProcessing(false);
return;
}
this.message += message.message;
this.onMessage?.({
message: this.message,
quota: message.quota,
});
if (message.end) {
this.onFinished?.(message.hash);
this.setProcessing(false);
}
}
public generate(prompt: string, model: string) {
this.setProcessing(true);
const token = localStorage.getItem("token") || "";
if (token) {
this.connection = new WebSocket(endpoint);
this.connection.onopen = () => {
this.connection?.send(JSON.stringify({ token, prompt, model } as GenerationForm));
};
this.connection.onmessage = (event) => {
this.handleMessage(JSON.parse(event.data) as GenerationSegmentResponse);
};
this.connection.onclose = () => {
this.setProcessing(false);
};
}
}
public generateWithBlock(prompt: string, model: string): boolean {
if (this.isProcessing()) {
return false;
}
this.generate(prompt, model);
return true;
}
}
export const manager = new GenerationManager();

View File

@ -147,6 +147,12 @@ const resources = {
"generate": {
"title": "AI Project Generator",
"input-placeholder": "generate a python game",
"failed": "Generate failed",
"reason": "Reason: ",
"success": "Generate success",
"success-prompt": "Project generated successfully! Please select the download format.",
"empty": "generating...",
"download": "Download {{name}} format",
}
},
},
@ -282,6 +288,12 @@ const resources = {
"generate": {
"title": "AI 项目生成器",
"input-placeholder": "生成一个python小游戏",
"failed": "生成失败",
"reason": "原因:",
"success": "生成成功",
"success-prompt": "成功生成项目!请选择下载格式。",
"empty": "生成中...",
"download": "下载 {{name}} 格式",
}
},
},
@ -427,6 +439,12 @@ const resources = {
generate: {
title: "Генератор AI проектов",
"input-placeholder": "сгенерировать python игру",
"failed": "Генерация не удалась",
"reason": "Причина: ",
"success": "Генерация успешна",
"success-prompt": "Проект успешно сгенерирован! Пожалуйста, выберите формат загрузки.",
"empty": "генерация...",
"download": "Загрузить {{name}} формат",
}
},
},

View File

@ -3,21 +3,54 @@ 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 {ChevronLeft, Cloud, FileDown, Info, LogIn, Send} from "lucide-react";
import {login, rest_api} 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";
import {manager} from "../conversation/generation.ts";
import {useToast} from "../components/ui/use-toast.ts";
import {handleLine} from "../utils.ts";
type WrapperProps = {
onSend?: (value: string) => boolean;
onSend?: (value: string, model: string) => boolean;
};
function Wrapper(props: WrapperProps) {
function Wrapper({ onSend }: WrapperProps) {
const { t } = useTranslation();
const ref = useRef(null);
const [ stayed, setStayed ] = useState<boolean>(false);
const [ hash, setHash ] = useState<string>("");
const [ data, setData ] = useState<string>("");
const [ quota, setQuota ] = useState<number>(0);
const [model, setModel] = useState("GPT-3.5");
const { toast } = useToast();
function clear() {
setData("");
setQuota(0);
setHash("");
}
manager.setMessageHandler(({ message, quota }) => {
setData(message);
setQuota(quota);
})
manager.setErrorHandler((err: string) => {
toast({
title: t('generate.failed'),
description: `${t('generate.reason')} ${err}`,
})
})
manager.setFinishedHandler((hash: string) => {
toast({
title: t('generate.success'),
description: t('generate.success-prompt'),
})
setHash(hash);
})
function handleSend() {
const target = ref.current as HTMLInputElement | null;
@ -26,7 +59,9 @@ function Wrapper(props: WrapperProps) {
const value = target.value.trim();
if (!value.length) return;
if (props.onSend?.(value)) {
if (onSend?.(value, model.toLowerCase())) {
setStayed(true);
clear();
target.value = "";
}
}
@ -42,10 +77,33 @@ function Wrapper(props: WrapperProps) {
});
return (
<div className={`generation-wrapper`}>
<div className={`product`}>
<img src={`/favicon.ico`} alt={""} />
AI Code Generator
</div>
{
stayed ?
<div className={`box`}>
{ quota > 0 && <div className={`quota-box`}>
<Cloud className={`h-4 w-4 mr-2`} />
{quota}
</div> }
<pre className={`message-box`}>
{ handleLine(data, 10) || t('generate.empty') }
</pre>
{
hash.length > 0 &&
<div className={`hash-box`}>
<a className={`download-box`} target={`_blank`} href={`${rest_api}/generation/download/tar?hash=${hash}`}>
<FileDown className={`h-6 w-6`} />
<p>{ t('generate.download', { name: "tar.gz"}) }</p>
</a>
<a className={`download-box`} target={`_blank`} href={`${rest_api}/generation/download/zip?hash=${hash}`}>
<FileDown className={`h-6 w-6`} />
<p>{ t('generate.download', { name: "zip"}) }</p>
</a>
</div>
}
</div> :
<div className={`product`}><img src={`/favicon.ico`} alt={""} />AI Code Generator</div>
}
<div className={`generate-box`}>
<Input className={`input`} ref={ref} placeholder={t('generate.input-placeholder')} />
<Button
@ -68,9 +126,12 @@ function Wrapper(props: WrapperProps) {
);
}
function Generation() {
const [ state, setState ] = useState(false);
const { t } = useTranslation();
const auth = useSelector(selectAuthenticated);
manager.setProcessingChangeHandler(setState);
return (
<div className={`generation-page`}>
{auth ? (
@ -80,13 +141,13 @@ function Generation() {
variant={`ghost`}
size={`icon`}
onClick={() => router.navigate("/")}
disabled={state}
>
<ChevronLeft className={`h-5 w-5 back`} />
</Button>
<Wrapper
onSend={(value: string) => {
console.log(value);
return true;
onSend={(prompt: string, model: string) => {
return manager.generateWithBlock(prompt, model)
}}
/>
</div>

View File

@ -196,3 +196,13 @@ export function useDraggableInput(
}
});
}
export function handleLine(data: string, max_line: number, end?: boolean): string {
const segment = data.split("\n");
const line = segment.length;
if (line > max_line) {
return end ? segment.slice(line - max_line).join("\n") : segment.slice(0, max_line).join("\n");
} else {
return data;
}
}

View File

@ -3,6 +3,7 @@ package generation
import (
"chat/utils"
"fmt"
"time"
)
func GetFolder(hash string) string {
@ -10,7 +11,7 @@ func GetFolder(hash string) string {
}
func GetFolderByHash(model string, prompt string) (string, string) {
hash := utils.Sha2Encrypt(model + prompt)
hash := utils.Sha2Encrypt(model + prompt + time.Now().Format("2006-01-02 15:04:05"))
return hash, GetFolder(hash)
}

View File

@ -7,6 +7,7 @@ import (
"chat/conversation"
"chat/generation"
"chat/middleware"
"fmt"
"github.com/gin-gonic/gin"
"github.com/spf13/viper"
)
@ -17,6 +18,7 @@ func main() {
panic(err)
}
fmt.Println()
app := gin.Default()
{
app.Use(middleware.CORSMiddleware())