mirror of
https://github.com/coaidev/coai.git
synced 2025-05-19 13:00:14 +09:00
update generation frontend
This commit is contained in:
parent
2c15612bd7
commit
f68b64a732
@ -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;
|
||||
|
@ -6,6 +6,7 @@
|
||||
padding: 6px 8px;
|
||||
border-radius: 4px;
|
||||
user-select: none;
|
||||
justify-content: center;
|
||||
|
||||
.select-group-item {
|
||||
padding: 0.35rem 0.5rem;
|
||||
|
118
app/src/conversation/generation.ts
Normal file
118
app/src/conversation/generation.ts
Normal 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();
|
@ -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}} формат",
|
||||
}
|
||||
},
|
||||
},
|
||||
|
@ -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
|
||||
{
|
||||
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>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user