From f68b64a732441d35e0af068c1e32da21c67292a7 Mon Sep 17 00:00:00 2001 From: Zhang Minghan Date: Sat, 23 Sep 2023 18:43:09 +0800 Subject: [PATCH] update generation frontend --- app/src/assets/generation.less | 82 ++++++++++++++++++++ app/src/assets/ui.less | 1 + app/src/conversation/generation.ts | 118 +++++++++++++++++++++++++++++ app/src/i18n.ts | 18 +++++ app/src/routes/Generation.tsx | 85 ++++++++++++++++++--- app/src/utils.ts | 10 +++ generation/build.go | 3 +- main.go | 2 + 8 files changed, 306 insertions(+), 13 deletions(-) create mode 100644 app/src/conversation/generation.ts diff --git a/app/src/assets/generation.less b/app/src/assets/generation.less index 86a51c1..ef8cbcb 100644 --- a/app/src/assets/generation.less +++ b/app/src/assets/generation.less @@ -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; diff --git a/app/src/assets/ui.less b/app/src/assets/ui.less index 6cd02f1..c6688c7 100644 --- a/app/src/assets/ui.less +++ b/app/src/assets/ui.less @@ -6,6 +6,7 @@ padding: 6px 8px; border-radius: 4px; user-select: none; + justify-content: center; .select-group-item { padding: 0.35rem 0.5rem; diff --git a/app/src/conversation/generation.ts b/app/src/conversation/generation.ts new file mode 100644 index 0000000..9fb3e74 --- /dev/null +++ b/app/src/conversation/generation.ts @@ -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(); diff --git a/app/src/i18n.ts b/app/src/i18n.ts index 3b36e22..688a449 100644 --- a/app/src/i18n.ts +++ b/app/src/i18n.ts @@ -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}} формат", } }, }, diff --git a/app/src/routes/Generation.tsx b/app/src/routes/Generation.tsx index ffb58b6..70b1c95 100644 --- a/app/src/routes/Generation.tsx +++ b/app/src/routes/Generation.tsx @@ -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(false); + const [ hash, setHash ] = useState(""); + const [ data, setData ] = useState(""); + const [ quota, setQuota ] = useState(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 (
-
- {""} - AI Code Generator -
+ { + stayed ? +
+ { quota > 0 &&
+ + {quota} +
} +
+              { handleLine(data, 10) || t('generate.empty') }
+            
+ { + hash.length > 0 && + + } +
: +
{""}AI Code Generator
+ }
{ - console.log(value); - return true; + onSend={(prompt: string, model: string) => { + return manager.generateWithBlock(prompt, model) }} />
diff --git a/app/src/utils.ts b/app/src/utils.ts index 1029e72..31613ad 100644 --- a/app/src/utils.ts +++ b/app/src/utils.ts @@ -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; + } +} diff --git a/generation/build.go b/generation/build.go index b860f76..12bbfd2 100644 --- a/generation/build.go +++ b/generation/build.go @@ -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) } diff --git a/main.go b/main.go index 3c86424..04746c6 100644 --- a/main.go +++ b/main.go @@ -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())