fix adapter of select group in mobile

This commit is contained in:
Zhang Minghan 2023-09-24 12:45:41 +08:00
parent 9e20d56ba2
commit edae895fc3
9 changed files with 283 additions and 191 deletions

View File

@ -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;
}

View File

@ -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;

View File

@ -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 ? (
<Select
value={props.current}
onValueChange={(value: string) => props.onChange?.(value)}
>
<SelectTrigger className="select-group mobile">
<SelectValue placeholder={props.current} />
</SelectTrigger>
<SelectContent>
{props.list.map((select: string, idx: number) => (
<SelectItem key={idx} value={select}>
{select}
</SelectItem>
))}
</SelectContent>
</Select>
) : (
<div className={`select-group`}>
{props.list.map((select: string, idx: number) => (
<div

View File

@ -1,7 +1,7 @@
import axios from "axios";
export const version: string = "2.7.0";
export const deploy: boolean = true;
export const version: string = "2.8.0";
export const deploy: boolean = false;
export let rest_api: string = "http://localhost:8094";
export let ws_api: string = "ws://localhost:8094";
@ -11,6 +11,14 @@ if (deploy) {
}
export const tokenField = deploy ? "token" : "token-dev";
export const supportModels: string[] = [
"GPT-3.5",
"GPT-3.5-16k",
"GPT-4",
"GPT-4-32k",
"Claude-2",
"Claude-2-100k",
];
export function login() {
location.href = "https://deeptrain.lightxi.com/login?app=chatnio";

View File

@ -1,4 +1,4 @@
import {ws_api} from "../conf.ts";
import { ws_api } from "../conf.ts";
export const endpoint = `${ws_api}/generation/create`;
@ -6,7 +6,7 @@ export type GenerationForm = {
token: string;
prompt: string;
model: string;
}
};
export type GenerationSegmentResponse = {
message: string;
@ -14,12 +14,12 @@ export type GenerationSegmentResponse = {
end: boolean;
error: string;
hash: string;
}
};
export type MessageEvent = {
message: string;
quota: number;
}
};
export class GenerationManager {
protected processing: boolean;
@ -36,7 +36,9 @@ export class GenerationManager {
this.message = "";
}
public setProcessingChangeHandler(handler: (processing: boolean) => void): void {
public setProcessingChangeHandler(
handler: (processing: boolean) => void,
): void {
this.onProcessingChange = handler;
}
@ -95,7 +97,9 @@ export class GenerationManager {
if (token) {
this.connection = new WebSocket(endpoint);
this.connection.onopen = () => {
this.connection?.send(JSON.stringify({ token, prompt, model } as GenerationForm));
this.connection?.send(
JSON.stringify({ token, prompt, model } as GenerationForm),
);
};
this.connection.onmessage = (event) => {
this.handleMessage(JSON.parse(event.data) as GenerationSegmentResponse);

View File

@ -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}} формат",
},
},
},
};

View File

@ -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<boolean>(false);
const [ hash, setHash ] = useState<string>("");
const [ data, setData ] = useState<string>("");
const [ quota, setQuota ] = useState<number>(0);
const [stayed, setStayed] = useState<boolean>(false);
const [hash, setHash] = useState<string>("");
const [data, setData] = useState<string>("");
const [quota, setQuota] = useState<number>(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;
@ -88,8 +88,12 @@ function Wrapper({ onSend }: WrapperProps) {
});
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 (
<div className={`generation-wrapper`}>
{
stayed ?
{stayed ? (
<div className={`box`}>
{ quota > 0 && <div className={`quota-box`}>
{quota > 0 && (
<div className={`quota-box`}>
<Cloud className={`h-4 w-4 mr-2`} />
{quota}
</div> }
<pre className={`message-box`}>
{ handleGenerationData(data) || t('generate.empty') }
</pre>
{
hash.length > 0 &&
<div className={`hash-box`}>
<a className={`download-box`} 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`} 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>
}
)}
<pre className={`message-box`}>
{handleGenerationData(data) || t("generate.empty")}
</pre>
{hash.length > 0 && (
<div className={`hash-box`}>
<a
className={`download-box`}
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`}
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')} />
<Input
className={`input`}
ref={ref}
placeholder={t("generate.input-placeholder")}
/>
<Button
size={`icon`}
className={`action`}
@ -139,7 +156,7 @@ function Wrapper({ onSend }: WrapperProps) {
<div className={`model-box`}>
<SelectGroup
current={model}
list={["GPT-3.5", "GPT-3.5-16k", "GPT-4", "GPT-4-32k"]}
list={supportModels}
onChange={(value: string) => {
dispatch(setModel(value));
}}
@ -149,7 +166,7 @@ function Wrapper({ onSend }: WrapperProps) {
);
}
function Generation() {
const [ state, setState ] = useState(false);
const [state, setState] = useState(false);
const { t } = useTranslation();
const auth = useSelector(selectAuthenticated);
@ -170,7 +187,9 @@ function Generation() {
</Button>
<Wrapper
onSend={(prompt: string, model: string) => {
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());
}}
/>

View File

@ -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 (
<div className={`chat-container`}>
<div className={`chat-wrapper`}>
{
messages.length > 0 ?
<ChatInterface /> :
{messages.length > 0 ? (
<ChatInterface />
) : (
<div className={`chat-product`}>
<Button variant={`outline`} onClick={() => router.navigate('/generate')}>
<Button
variant={`outline`}
onClick={() => router.navigate("/generate")}
>
<FolderKanban className={`h-4 w-4 mr-1.5`} />
{ t('generate.title') }
{t("generate.title")}
<ChevronRight className={`h-4 w-4 ml-2`} />
</Button>
</div>
}
)}
<div className={`chat-input`}>
<div className={`input-wrapper`}>
<TooltipProvider>
@ -385,15 +390,14 @@ function ChatWrapper() {
</Button>
</div>
<div className={`input-options`}>
<div className="flex items-center space-x-2">
<SelectGroup
current={model}
list={["GPT-3.5", "GPT-3.5-16k", "GPT-4", "GPT-4-32k"]}
list={supportModels}
onChange={(model: string) => {
if (!auth && model !== "GPT-3.5") {
toast({
title: t("login-require"),
})
});
return;
}
dispatch(setModel(model));
@ -403,7 +407,6 @@ function ChatWrapper() {
</div>
</div>
</div>
</div>
);
}

View File

@ -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);
}