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;
|
padding: 15vh 0;
|
||||||
gap: 2rem;
|
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 {
|
.product {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
padding: 6px 8px;
|
padding: 6px 8px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
.select-group-item {
|
.select-group-item {
|
||||||
padding: 0.35rem 0.5rem;
|
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": {
|
"generate": {
|
||||||
"title": "AI Project Generator",
|
"title": "AI Project Generator",
|
||||||
"input-placeholder": "generate a python game",
|
"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": {
|
"generate": {
|
||||||
"title": "AI 项目生成器",
|
"title": "AI 项目生成器",
|
||||||
"input-placeholder": "生成一个python小游戏",
|
"input-placeholder": "生成一个python小游戏",
|
||||||
|
"failed": "生成失败",
|
||||||
|
"reason": "原因:",
|
||||||
|
"success": "生成成功",
|
||||||
|
"success-prompt": "成功生成项目!请选择下载格式。",
|
||||||
|
"empty": "生成中...",
|
||||||
|
"download": "下载 {{name}} 格式",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -427,6 +439,12 @@ const resources = {
|
|||||||
generate: {
|
generate: {
|
||||||
title: "Генератор AI проектов",
|
title: "Генератор AI проектов",
|
||||||
"input-placeholder": "сгенерировать python игру",
|
"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 { selectAuthenticated } from "../store/auth.ts";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Button } from "../components/ui/button.tsx";
|
import { Button } from "../components/ui/button.tsx";
|
||||||
import { ChevronLeft, Info, LogIn, Send } from "lucide-react";
|
import {ChevronLeft, Cloud, FileDown, Info, LogIn, Send} from "lucide-react";
|
||||||
import { login } from "../conf.ts";
|
import {login, rest_api} from "../conf.ts";
|
||||||
import router from "../router.ts";
|
import router from "../router.ts";
|
||||||
import { Input } from "../components/ui/input.tsx";
|
import { Input } from "../components/ui/input.tsx";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import SelectGroup from "../components/SelectGroup.tsx";
|
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 = {
|
type WrapperProps = {
|
||||||
onSend?: (value: string) => boolean;
|
onSend?: (value: string, model: string) => boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
function Wrapper(props: WrapperProps) {
|
function Wrapper({ onSend }: WrapperProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const ref = useRef(null);
|
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 [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() {
|
function handleSend() {
|
||||||
const target = ref.current as HTMLInputElement | null;
|
const target = ref.current as HTMLInputElement | null;
|
||||||
@ -26,7 +59,9 @@ function Wrapper(props: WrapperProps) {
|
|||||||
const value = target.value.trim();
|
const value = target.value.trim();
|
||||||
if (!value.length) return;
|
if (!value.length) return;
|
||||||
|
|
||||||
if (props.onSend?.(value)) {
|
if (onSend?.(value, model.toLowerCase())) {
|
||||||
|
setStayed(true);
|
||||||
|
clear();
|
||||||
target.value = "";
|
target.value = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -42,10 +77,33 @@ function Wrapper(props: WrapperProps) {
|
|||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<div className={`generation-wrapper`}>
|
<div className={`generation-wrapper`}>
|
||||||
<div className={`product`}>
|
{
|
||||||
<img src={`/favicon.ico`} alt={""} />
|
stayed ?
|
||||||
AI Code Generator
|
<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> :
|
||||||
|
<div className={`product`}><img src={`/favicon.ico`} alt={""} />AI Code Generator</div>
|
||||||
|
}
|
||||||
<div className={`generate-box`}>
|
<div className={`generate-box`}>
|
||||||
<Input className={`input`} ref={ref} placeholder={t('generate.input-placeholder')} />
|
<Input className={`input`} ref={ref} placeholder={t('generate.input-placeholder')} />
|
||||||
<Button
|
<Button
|
||||||
@ -68,9 +126,12 @@ function Wrapper(props: WrapperProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
function Generation() {
|
function Generation() {
|
||||||
|
const [ state, setState ] = useState(false);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const auth = useSelector(selectAuthenticated);
|
const auth = useSelector(selectAuthenticated);
|
||||||
|
|
||||||
|
manager.setProcessingChangeHandler(setState);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`generation-page`}>
|
<div className={`generation-page`}>
|
||||||
{auth ? (
|
{auth ? (
|
||||||
@ -80,13 +141,13 @@ function Generation() {
|
|||||||
variant={`ghost`}
|
variant={`ghost`}
|
||||||
size={`icon`}
|
size={`icon`}
|
||||||
onClick={() => router.navigate("/")}
|
onClick={() => router.navigate("/")}
|
||||||
|
disabled={state}
|
||||||
>
|
>
|
||||||
<ChevronLeft className={`h-5 w-5 back`} />
|
<ChevronLeft className={`h-5 w-5 back`} />
|
||||||
</Button>
|
</Button>
|
||||||
<Wrapper
|
<Wrapper
|
||||||
onSend={(value: string) => {
|
onSend={(prompt: string, model: string) => {
|
||||||
console.log(value);
|
return manager.generateWithBlock(prompt, model)
|
||||||
return true;
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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 (
|
import (
|
||||||
"chat/utils"
|
"chat/utils"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetFolder(hash string) string {
|
func GetFolder(hash string) string {
|
||||||
@ -10,7 +11,7 @@ func GetFolder(hash string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetFolderByHash(model string, prompt string) (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)
|
return hash, GetFolder(hash)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
2
main.go
2
main.go
@ -7,6 +7,7 @@ import (
|
|||||||
"chat/conversation"
|
"chat/conversation"
|
||||||
"chat/generation"
|
"chat/generation"
|
||||||
"chat/middleware"
|
"chat/middleware"
|
||||||
|
"fmt"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
@ -17,6 +18,7 @@ func main() {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
app := gin.Default()
|
app := gin.Default()
|
||||||
{
|
{
|
||||||
app.Use(middleware.CORSMiddleware())
|
app.Use(middleware.CORSMiddleware())
|
||||||
|
Loading…
Reference in New Issue
Block a user