From 0ee79fa4a0176cbadcc915d20639c5797f774683 Mon Sep 17 00:00:00 2001 From: Zhang Minghan Date: Sun, 24 Sep 2023 12:45:19 +0800 Subject: [PATCH] fix adapter of select group in mobile --- app/src/assets/home.less | 4 +- app/src/assets/ui.less | 8 ++ app/src/components/SelectGroup.tsx | 35 ++++++- app/src/conf.ts | 12 ++- app/src/conversation/generation.ts | 150 +++++++++++++++-------------- app/src/i18n.ts | 50 +++++----- app/src/routes/Generation.tsx | 131 ++++++++++++++----------- app/src/routes/Home.tsx | 61 ++++++------ app/src/utils.ts | 23 ++++- 9 files changed, 283 insertions(+), 191 deletions(-) diff --git a/app/src/assets/home.less b/app/src/assets/home.less index 24aca80..c4d264f 100644 --- a/app/src/assets/home.less +++ b/app/src/assets/home.less @@ -319,12 +319,12 @@ } .input-options { - width: max-content; margin: 16px auto 2px; display: flex; flex-direction: row; align-items: center; - flex-wrap: nowrap; + justify-content: center; + flex-wrap: wrap; gap: 4px; height: min-content; } diff --git a/app/src/assets/ui.less b/app/src/assets/ui.less index c6688c7..189c26a 100644 --- a/app/src/assets/ui.less +++ b/app/src/assets/ui.less @@ -8,6 +8,14 @@ user-select: none; justify-content: center; + &.mobile { + text-align: center; + + & span { + margin: 0 auto; + } + } + .select-group-item { padding: 0.35rem 0.5rem; border-radius: 4px; diff --git a/app/src/components/SelectGroup.tsx b/app/src/components/SelectGroup.tsx index 9d97fd0..c0c7fef 100644 --- a/app/src/components/SelectGroup.tsx +++ b/app/src/components/SelectGroup.tsx @@ -1,3 +1,13 @@ +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "./ui/select"; +import { mobile } from "../utils.ts"; +import { useEffect, useState } from "react"; + type SelectGroupProps = { current: string; list: string[]; @@ -5,7 +15,30 @@ type SelectGroupProps = { }; function SelectGroup(props: SelectGroupProps) { - return ( + const [state, setState] = useState(mobile); + useEffect(() => { + window.addEventListener("resize", () => { + setState(mobile); + }); + }, []); + + return state ? ( + + ) : (
{props.list.map((select: string, idx: number) => (
void; constructor() { - this.processing = false; + 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 setProcessingChangeHandler(handler: (processing: boolean) => void): void { - this.onProcessingChange = handler; + 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; } - public setMessageHandler(handler: (message: MessageEvent) => void): void { - this.onMessage = handler; - } + this.message += message.message; + this.onMessage?.({ + message: this.message, + quota: message.quota, + }); - public setErrorHandler(handler: (error: string) => void): void { - this.onError = handler; + if (message.end) { + this.onFinished?.(message.hash); + this.setProcessing(false); } + } - 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); + 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); - 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; + 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 82cf0bb..b61a2a2 100644 --- a/app/src/i18n.ts +++ b/app/src/i18n.ts @@ -143,16 +143,17 @@ const resources = { "max-length-prompt": "The content has been truncated due to the context length limit", }, - "generate": { - "title": "AI Project Generator", + 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", - } + failed: "Generate failed", + reason: "Reason: ", + success: "Generate success", + "success-prompt": + "Project generated successfully! Please select the download format.", + empty: "generating...", + download: "Download {{name}} format", + }, }, }, cn: { @@ -283,16 +284,16 @@ const resources = { "max-length": "内容过长", "max-length-prompt": "由于上下文长度限制,内容已被截取", }, - "generate": { - "title": "AI 项目生成器", + generate: { + title: "AI 项目生成器", "input-placeholder": "生成一个python小游戏", - "failed": "生成失败", - "reason": "原因:", - "success": "生成成功", + failed: "生成失败", + reason: "原因:", + success: "生成成功", "success-prompt": "成功生成项目!请选择下载格式。", - "empty": "生成中...", - "download": "下载 {{name}} 格式", - } + empty: "生成中...", + download: "下载 {{name}} 格式", + }, }, }, ru: { @@ -436,13 +437,14 @@ const resources = { generate: { title: "Генератор AI проектов", "input-placeholder": "сгенерировать python игру", - "failed": "Генерация не удалась", - "reason": "Причина: ", - "success": "Генерация успешна", - "success-prompt": "Проект успешно сгенерирован! Пожалуйста, выберите формат загрузки.", - "empty": "генерация...", - "download": "Загрузить {{name}} формат", - } + failed: "Генерация не удалась", + reason: "Причина: ", + success: "Генерация успешна", + "success-prompt": + "Проект успешно сгенерирован! Пожалуйста, выберите формат загрузки.", + empty: "генерация...", + download: "Загрузить {{name}} формат", + }, }, }, }; diff --git a/app/src/routes/Generation.tsx b/app/src/routes/Generation.tsx index 9fbc0b9..8026670 100644 --- a/app/src/routes/Generation.tsx +++ b/app/src/routes/Generation.tsx @@ -1,18 +1,18 @@ import "../assets/generation.less"; -import {useDispatch, useSelector} from "react-redux"; +import { useDispatch, useSelector } from "react-redux"; import { selectAuthenticated } from "../store/auth.ts"; import { useTranslation } from "react-i18next"; import { Button } from "../components/ui/button.tsx"; -import {ChevronLeft, Cloud, FileDown, Info, LogIn, Send} from "lucide-react"; -import {login, rest_api} from "../conf.ts"; +import { ChevronLeft, Cloud, FileDown, Info, LogIn, Send } from "lucide-react"; +import { login, rest_api, supportModels } 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 {handleGenerationData} from "../utils.ts"; -import {selectModel, setModel} from "../store/chat.ts"; +import { manager } from "../conversation/generation.ts"; +import { useToast } from "../components/ui/use-toast.ts"; +import { handleGenerationData } from "../utils.ts"; +import { selectModel, setModel } from "../store/chat.ts"; type WrapperProps = { onSend?: (value: string, model: string) => boolean; @@ -22,10 +22,10 @@ function Wrapper({ onSend }: WrapperProps) { const { t } = useTranslation(); const dispatch = useDispatch(); const ref = useRef(null); - const [ stayed, setStayed ] = useState(false); - const [ hash, setHash ] = useState(""); - const [ data, setData ] = useState(""); - const [ quota, setQuota ] = useState(0); + const [stayed, setStayed] = useState(false); + const [hash, setHash] = useState(""); + const [data, setData] = useState(""); + const [quota, setQuota] = useState(0); const model = useSelector(selectModel); const modelRef = useRef(model); const auth = useSelector(selectAuthenticated); @@ -45,21 +45,21 @@ function Wrapper({ onSend }: WrapperProps) { manager.setMessageHandler(({ message, quota }) => { setData(message); setQuota(quota); - }) + }); manager.setErrorHandler((err: string) => { toast({ - title: t('generate.failed'), - description: `${t('generate.reason')} ${err}`, - }) - }) + title: t("generate.failed"), + description: `${t("generate.reason")} ${err}`, + }); + }); manager.setFinishedHandler((hash: string) => { toast({ - title: t('generate.success'), - description: t('generate.success-prompt'), - }) + title: t("generate.success"), + description: t("generate.success-prompt"), + }); setHash(hash); - }) + }); function handleSend(model: string = "gpt-3.5-16k") { const target = ref.current as HTMLInputElement | null; @@ -81,15 +81,19 @@ function Wrapper({ onSend }: WrapperProps) { target.focus(); target.removeEventListener("keydown", () => {}); target.addEventListener("keydown", (e) => { - if (e.key === "Enter") { - // cannot use model here, because model is not updated - handleSend(modelRef.current); - } - }); + if (e.key === "Enter") { + // cannot use model here, because model is not updated + handleSend(modelRef.current); + } + }); return () => { - ref.current && (ref.current as HTMLInputElement).removeEventListener("keydown", () => {}); - } + ref.current && + (ref.current as HTMLInputElement).removeEventListener( + "keydown", + () => {}, + ); + }; }, [ref]); useEffect(() => { @@ -98,35 +102,48 @@ function Wrapper({ onSend }: WrapperProps) { return (
- { - stayed ? -
- { quota > 0 &&
+ {stayed ? ( +
+ {quota > 0 && ( +
{quota} -
} -
-              { handleGenerationData(data) || t('generate.empty') }
-            
- { - hash.length > 0 && - - } -
: -
{""}AI Code Generator
- } +
+ )} +
+            {handleGenerationData(data) || t("generate.empty")}
+          
+ {hash.length > 0 && ( + + )} +
+ ) : ( +
+ {""} + AI Code Generator +
+ )}
- + { - console.debug(`[generation] create generation request (prompt: ${prompt}, model: ${model.toLowerCase()})`); + console.debug( + `[generation] create generation request (prompt: ${prompt}, model: ${model.toLowerCase()})`, + ); return manager.generateWithBlock(prompt, model.toLowerCase()); }} /> diff --git a/app/src/routes/Home.tsx b/app/src/routes/Home.tsx index 51dbafe..50c92a4 100644 --- a/app/src/routes/Home.tsx +++ b/app/src/routes/Home.tsx @@ -3,7 +3,9 @@ import "../assets/chat.less"; import { Input } from "../components/ui/input.tsx"; import { Toggle } from "../components/ui/toggle.tsx"; import { - ChevronDown, ChevronRight, FolderKanban, + ChevronDown, + ChevronRight, + FolderKanban, Globe, LogIn, MessageSquare, @@ -21,7 +23,7 @@ import { import { useDispatch, useSelector } from "react-redux"; import type { RootState } from "../store"; import { selectAuthenticated } from "../store/auth.ts"; -import { login } from "../conf.ts"; +import { login, supportModels } from "../conf.ts"; import { deleteConversation, toggleConversation, @@ -35,7 +37,7 @@ import { useAnimation, useEffectAsync, } from "../utils.ts"; -import {toast, useToast} from "../components/ui/use-toast.ts"; +import { toast, useToast } from "../components/ui/use-toast.ts"; import { ConversationInstance, Message } from "../conversation/types.ts"; import { selectCurrent, @@ -315,17 +317,20 @@ function ChatWrapper() { return (
- { - messages.length > 0 ? - : -
- -
- } + {messages.length > 0 ? ( + + ) : ( +
+ +
+ )}
@@ -385,21 +390,19 @@ function ChatWrapper() {
-
- { - if (!auth && model !== "GPT-3.5") { - toast({ - title: t("login-require"), - }) - return; - } - dispatch(setModel(model)); - }} - /> -
+ { + if (!auth && model !== "GPT-3.5") { + toast({ + title: t("login-require"), + }); + return; + } + dispatch(setModel(model)); + }} + />
diff --git a/app/src/utils.ts b/app/src/utils.ts index 95947f8..78161ba 100644 --- a/app/src/utils.ts +++ b/app/src/utils.ts @@ -199,20 +199,35 @@ export function useDraggableInput( export function escapeRegExp(str: string): string { // convert \n to [enter], \t to [tab], \r to [return], \s to [space], \" to [quote], \' to [single-quote] - return str.replace(/\\n/g, "\n").replace(/\\t/g, "\t").replace(/\\r/g, "\r").replace(/\\s/g, " ").replace(/\\"/g, "\"").replace(/\\'/g, "'"); + return str + .replace(/\\n/g, "\n") + .replace(/\\t/g, "\t") + .replace(/\\r/g, "\r") + .replace(/\\s/g, " ") + .replace(/\\"/g, '"') + .replace(/\\'/g, "'"); } -export function handleLine(data: string, max_line: number, end?: boolean): string { +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 ?? true) ? segment.slice(line - max_line).join("\n") : segment.slice(0, max_line).join("\n"); + return end ?? true + ? segment.slice(line - max_line).join("\n") + : segment.slice(0, max_line).join("\n"); } else { return data; } } export function handleGenerationData(data: string): string { - data = data.replace(/{\s*"result":\s*{/g, "").trim().replace(/}\s*$/g, ""); + data = data + .replace(/{\s*"result":\s*{/g, "") + .trim() + .replace(/}\s*$/g, ""); return handleLine(escapeRegExp(data), 6); }