update utils

This commit is contained in:
Zhang Minghan 2023-10-29 08:48:03 +08:00
parent 83213a60fb
commit a4a2398045
53 changed files with 681 additions and 553 deletions

View File

@ -54,8 +54,8 @@
align-items: center;
justify-content: center;
z-index: 1;
top: -30px;
right: 2px;
top: -34px;
right: 0px;
user-select: none;
p {
@ -63,7 +63,6 @@
line-height: 1;
margin: 0 0 0 6px;
padding: 0;
transform: translateY(-2px);
}
svg {

View File

@ -6,15 +6,15 @@ import {
DialogTitle,
DialogTrigger,
} from "./ui/dialog.tsx";
import {Maximize, Image, MenuSquare, PanelRight, XSquare} from "lucide-react";
import { Maximize, Image, MenuSquare, PanelRight, XSquare } from "lucide-react";
import { useTranslation } from "react-i18next";
import "../assets/editor.less";
import "@/assets/editor.less";
import { Textarea } from "./ui/textarea.tsx";
import Markdown from "./Markdown.tsx";
import { useEffect, useRef, useState } from "react";
import { Toggle } from "./ui/toggle.tsx";
import { mobile } from "../utils.ts";
import {Button} from "./ui/button.tsx";
import { mobile } from "@/utils/device.ts";
import { Button } from "./ui/button.tsx";
type RichEditorProps = {
value: string;

View File

@ -1,6 +1,6 @@
import React, { useEffect, useRef, useState } from "react";
import { AlertCircle, File, FileCheck, Plus, X } from "lucide-react";
import "../assets/file.less";
import "@/assets/file.less";
import {
Dialog,
DialogContent,
@ -12,7 +12,7 @@ import {
import { useTranslation } from "react-i18next";
import { Alert, AlertTitle } from "./ui/alert.tsx";
import { useToast } from "./ui/use-toast.ts";
import { useDraggableInput } from "../utils.ts";
import { useDraggableInput } from "@/utils/dom.ts";
export type FileObject = {
name: string;

View File

@ -6,7 +6,7 @@ import {
DropdownMenuContent,
DropdownMenuTrigger,
} from "./ui/dropdown-menu.tsx";
import { setLanguage } from "../i18n.ts";
import { setLanguage } from "@/i18n.ts";
import { useTranslation } from "react-i18next";
function I18nProvider() {

View File

@ -1,4 +1,4 @@
import "../assets/loader.less";
import "@/assets/loader.less";
type LoaderProps = {
className?: string;

View File

@ -5,16 +5,16 @@ import remarkGfm from "remark-gfm";
import remarkMath from "remark-math";
import rehypeKatex from "rehype-katex";
import { parseFile } from "./plugins/file.tsx";
import "../assets/markdown/all.less";
import "@/assets/markdown/all.less";
import { useEffect } from "react";
import { useDispatch } from "react-redux";
import { openDialog as openQuotaDialog } from "../store/quota.ts";
import { openDialog as openSubscriptionDialog } from "../store/subscription.ts";
import { AppDispatch } from "../store";
import {Copy} from "lucide-react";
import {copyClipboard} from "../utils.ts";
import {useToast} from "./ui/use-toast.ts";
import {useTranslation} from "react-i18next";
import { openDialog as openQuotaDialog } from "@/store/quota.ts";
import { openDialog as openSubscriptionDialog } from "@/store/subscription.ts";
import { AppDispatch } from "@/store";
import { Copy } from "lucide-react";
import { copyClipboard } from "@/utils/dom.ts";
import { useToast } from "./ui/use-toast.ts";
import { useTranslation } from "react-i18next";
type MarkdownProps = {
children: string;
@ -37,7 +37,6 @@ function Markdown({ children, className }: MarkdownProps) {
const { t } = useTranslation();
const { toast } = useToast();
useEffect(() => {
document.querySelectorAll(".file-instance").forEach((el) => {
const parent = el.parentElement as HTMLElement;
@ -76,12 +75,15 @@ function Markdown({ children, className }: MarkdownProps) {
return !inline && match ? (
<div className={`markdown-syntax`}>
<div className={`markdown-syntax-header`}>
<Copy className={`h-3 w-3`} onClick={async () => {
await copyClipboard(children.toString());
toast({
title: t("share.copied"),
});
}} />
<Copy
className={`h-3 w-3`}
onClick={async () => {
await copyClipboard(children.toString());
toast({
title: t("share.copied"),
});
}}
/>
<p>{match[1]}</p>
</div>
<SyntaxHighlighter

View File

@ -1,5 +1,5 @@
import { Message } from "../conversation/types.ts";
import Markdown from "./Markdown.tsx";
import { Message } from "@/conversation/types.ts";
import Markdown from "@/components/Markdown.tsx";
import {
Cloud,
CloudFog,
@ -8,28 +8,30 @@ import {
Loader2,
MousePointerSquare,
Power,
RotateCcw, ScanText,
RotateCcw,
ScanText,
} from "lucide-react";
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuTrigger,
} from "./ui/context-menu.tsx";
} from "@/components/ui/context-menu.tsx";
import { filterMessage } from "@/utils/processor.ts";
import {
copyClipboard,
filterMessage, getSelectionTextInArea,
saveAsFile,
getSelectionTextInArea,
useInputValue,
} from "../utils.ts";
} from "@/utils/dom.ts";
import { useTranslation } from "react-i18next";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "./ui/tooltip.tsx";
import {Ref, useRef, useState} from "react";
} from "@/components/ui/tooltip.tsx";
import { Ref, useRef, useState } from "react";
type MessageProps = {
message: Message;
@ -39,7 +41,7 @@ type MessageProps = {
};
function MessageSegment(props: MessageProps) {
const [ copied, setCopied ] = useState<string>("");
const [copied, setCopied] = useState<string>("");
const { t } = useTranslation();
const ref = useRef(null);
const { message } = props;
@ -74,15 +76,11 @@ function MessageSegment(props: MessageProps) {
</div>
</ContextMenuTrigger>
<ContextMenuContent>
{
copied.length > 0 && (
<ContextMenuItem
onClick={() => copyClipboard(copied)}
>
<ScanText className={`h-4 w-4 mr-2`} /> {t("message.copy-area")}
</ContextMenuItem>
)
}
{copied.length > 0 && (
<ContextMenuItem onClick={() => copyClipboard(copied)}>
<ScanText className={`h-4 w-4 mr-2`} /> {t("message.copy-area")}
</ContextMenuItem>
)}
<ContextMenuItem
onClick={() => copyClipboard(filterMessage(message.content))}
>

View File

@ -1,8 +1,8 @@
import { Button } from "./ui/button.tsx";
import { selectMessages } from "../store/chat.ts";
import { selectMessages } from "@/store/chat.ts";
import { useDispatch, useSelector } from "react-redux";
import { MessageSquarePlus } from "lucide-react";
import { toggleConversation } from "../conversation/history.ts";
import { toggleConversation } from "@/conversation/history.ts";
function ProjectLink() {
const dispatch = useDispatch();

View File

@ -1,5 +1,5 @@
import { useRegisterSW } from "virtual:pwa-register/react";
import { version } from "../conf.ts";
import { version } from "@/conf.ts";
import { useTranslation } from "react-i18next";
import { useToast } from "./ui/use-toast.ts";
import { useEffect } from "react";

View File

@ -5,7 +5,7 @@ import {
SelectTrigger,
SelectValue,
} from "./ui/select";
import { mobile } from "../utils.ts";
import { mobile } from "@/utils/device.ts";
import { useEffect, useState } from "react";
import { Badge } from "./ui/badge.tsx";

View File

@ -1,6 +1,6 @@
import NavBar from "./NavBar.tsx";
import { ThemeProvider } from "../ThemeProvider.tsx";
import DialogManager from "../../dialogs";
import { ThemeProvider } from "@/components/ThemeProvider.tsx";
import DialogManager from "@/dialogs";
function AppProvider() {
return (

View File

@ -1,10 +1,7 @@
import { useTranslation } from "react-i18next";
import { useDispatch, useSelector } from "react-redux";
import { logout, selectUsername } from "../../store/auth.ts";
import {
openDialog as openQuotaDialog,
quotaSelector,
} from "../../store/quota.ts";
import { logout, selectUsername } from "@/store/auth.ts";
import { openDialog as openQuotaDialog, quotaSelector } from "@/store/quota.ts";
import {
DropdownMenu,
DropdownMenuContent,
@ -12,8 +9,8 @@ import {
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "../ui/dropdown-menu.tsx";
import { Button } from "../ui/button.tsx";
} from "@/components/ui/dropdown-menu.tsx";
import { Button } from "@/components/ui/button.tsx";
import {
BadgeCent,
Boxes,
@ -23,11 +20,11 @@ import {
ListStart,
Plug,
} from "lucide-react";
import { openDialog as openSub } from "../../store/subscription.ts";
import { openDialog as openPackageDialog } from "../../store/package.ts";
import { openDialog as openInvitationDialog } from "../../store/invitation.ts";
import { openDialog as openSharingDialog } from "../../store/sharing.ts";
import { openDialog as openApiDialog } from "../../store/api.ts";
import { openDialog as openSub } from "@/store/subscription.ts";
import { openDialog as openPackageDialog } from "@/store/package.ts";
import { openDialog as openInvitationDialog } from "@/store/invitation.ts";
import { openDialog as openSharingDialog } from "@/store/sharing.ts";
import { openDialog as openApiDialog } from "@/store/api.ts";
type MenuBarProps = {
children: React.ReactNode;

View File

@ -5,16 +5,16 @@ import {
selectAuthenticated,
selectUsername,
validateToken,
} from "../../store/auth.ts";
import { Button } from "../ui/button.tsx";
} from "@/store/auth.ts";
import { Button } from "@/components/ui/button.tsx";
import { Menu } from "lucide-react";
import { useEffect } from "react";
import { login, tokenField } from "../../conf.ts";
import { toggleMenu } from "../../store/menu.ts";
import ProjectLink from "../ProjectLink.tsx";
import ModeToggle from "../ThemeProvider.tsx";
import I18nProvider from "../I18nProvider.tsx";
import router from "../../router.tsx";
import { login, tokenField } from "@/conf.ts";
import { toggleMenu } from "@/store/menu.ts";
import ProjectLink from "@/components/ProjectLink.tsx";
import ModeToggle from "@/components/ThemeProvider.tsx";
import I18nProvider from "@/components/I18nProvider.tsx";
import router from "@/router.tsx";
import MenuBar from "./MenuBar.tsx";
function NavMenu() {

View File

@ -1,11 +1,11 @@
import { useEffect, useRef, useState } from "react";
import { Message } from "../../conversation/types.ts";
import { Message } from "@/conversation/types.ts";
import { useSelector } from "react-redux";
import { selectCurrent, selectMessages } from "../../store/chat.ts";
import { Button } from "../ui/button.tsx";
import { selectCurrent, selectMessages } from "@/store/chat.ts";
import { Button } from "@/components/ui/button.tsx";
import { ChevronDown } from "lucide-react";
import MessageSegment from "../Message.tsx";
import { connectionEvent } from "../../events/connection.ts";
import MessageSegment from "@/components/Message.tsx";
import { connectionEvent } from "@/events/connection.ts";
function ChatInterface() {
const ref = useRef(null);

View File

@ -1,19 +1,20 @@
import { useTranslation } from "react-i18next";
import React, { useEffect, useRef, useState } from "react";
import FileProvider, { FileObject } from "../FileProvider.tsx";
import FileProvider, { FileObject } from "@/components/FileProvider.tsx";
import { useDispatch, useSelector } from "react-redux";
import { selectAuthenticated, selectInit } from "../../store/auth.ts";
import { selectAuthenticated, selectInit } from "@/store/auth.ts";
import {
selectMessages,
selectModel,
selectWeb,
setWeb,
} from "../../store/chat.ts";
import { manager } from "../../conversation/manager.ts";
import { formatMessage, triggerInstallApp } from "../../utils.ts";
import ChatInterface from "./ChatInterface.tsx";
import { Button } from "../ui/button.tsx";
import router from "../../router.tsx";
} from "@/store/chat.ts";
import { manager } from "@/conversation/manager.ts";
import { formatMessage } from "@/utils/processor.ts";
import { triggerInstallApp } from "@/utils/app.ts";
import ChatInterface from "@/components/home/ChatInterface.tsx";
import { Button } from "@/components/ui/button.tsx";
import router from "@/router.tsx";
import {
BookMarked,
ChevronRight,
@ -26,10 +27,10 @@ import {
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "../ui/tooltip.tsx";
import { Toggle } from "../ui/toggle.tsx";
import { Input } from "../ui/input.tsx";
import EditorProvider from "../EditorProvider.tsx";
} from "@/components/ui/tooltip.tsx";
import { Toggle } from "@/components/ui/toggle.tsx";
import { Input } from "@/components/ui/input.tsx";
import EditorProvider from "@/components/EditorProvider.tsx";
import ModelSelector from "./ModelSelector.tsx";
import {
Dialog,
@ -37,8 +38,9 @@ import {
DialogDescription,
DialogHeader,
DialogTitle,
} from "../ui/dialog.tsx";
import { version } from "../../conf.ts";
} from "@/components/ui/dialog.tsx";
import { version } from "@/conf.ts";
import { getQueryParam } from "@/utils/path.ts";
function ChatSpace() {
const [open, setOpen] = useState(false);
@ -145,8 +147,7 @@ function ChatWrapper() {
useEffect(() => {
if (!init) return;
const search = new URLSearchParams(window.location.search);
const query = (search.get("q") || "").trim();
const query = getQueryParam("q").trim();
if (query.length > 0) processSend(query, auth, model, web).then();
window.history.replaceState({}, "", "/");
}, [init]);

View File

@ -1,16 +1,17 @@
import { toggleConversation } from "../../conversation/history.ts";
import { filterMessage, mobile } from "../../utils.ts";
import { setMenu } from "../../store/menu.ts";
import { toggleConversation } from "@/conversation/history.ts";
import { mobile } from "@/utils/device.ts";
import { filterMessage } from "@/utils/processor.ts";
import { setMenu } from "@/store/menu.ts";
import { MessageSquare, MoreHorizontal, Share2, Trash2 } from "lucide-react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "../ui/dropdown-menu.tsx";
} from "@/components/ui/dropdown-menu.tsx";
import { useDispatch } from "react-redux";
import { useTranslation } from "react-i18next";
import { ConversationInstance } from "../../conversation/types.ts";
import { ConversationInstance } from "@/conversation/types.ts";
import { useState } from "react";
type ConversationSegmentProps = {

View File

@ -1,15 +1,16 @@
import SelectGroup, { SelectItemProps } from "../SelectGroup.tsx";
import { supportModels } from "../../conf.ts";
import { selectModel, setModel } from "../../store/chat.ts";
import SelectGroup, { SelectItemProps } from "@/components/SelectGroup.tsx";
import { login, supportModels } from "@/conf.ts";
import { selectModel, setModel } from "@/store/chat.ts";
import { useTranslation } from "react-i18next";
import { useDispatch, useSelector } from "react-redux";
import { selectAuthenticated } from "../../store/auth.ts";
import { useToast } from "../ui/use-toast.ts";
import { selectAuthenticated } from "@/store/auth.ts";
import { useToast } from "@/components/ui/use-toast.ts";
import { useEffect } from "react";
import { Model } from "../../conversation/types.ts";
import { modelEvent } from "../../events/model.ts";
import { isSubscribedSelector } from "../../store/subscription.ts";
import {teenagerSelector} from "../../store/package.ts";
import { Model } from "@/conversation/types.ts";
import { modelEvent } from "@/events/model.ts";
import { isSubscribedSelector } from "@/store/subscription.ts";
import { teenagerSelector } from "@/store/package.ts";
import { ToastAction } from "@/components/ui/toast.tsx";
function GetModel(name: string): Model {
return supportModels.find((model) => model.id === name) as Model;
@ -42,30 +43,28 @@ function ModelSelector(props: ModelSelectorProps) {
}
});
const list = supportModels.map(
(model: Model): SelectItemProps => {
const array = ["gpt-4", "claude-2"];
if (subscription && array.includes(model.id)) {
return {
name: model.id,
value: model.name,
badge: { variant: "gold", name: "plus" },
} as SelectItemProps;
} else if (student && model.id === "claude-2") {
return {
name: model.id,
value: model.name,
badge: { variant: "gold", name: "student" },
} as SelectItemProps;
}
const list = supportModels.map((model: Model): SelectItemProps => {
const array = ["gpt-4", "claude-2"];
if (subscription && array.includes(model.id)) {
return {
name: model.id,
value: model.name,
badge: { variant: "gold", name: "plus" },
} as SelectItemProps;
} else if (student && model.id === "claude-2") {
return {
name: model.id,
value: model.name,
badge: { variant: "gold", name: "student" },
} as SelectItemProps;
}
return {
name: model.id,
value: model.name,
badge: model.free && { variant: "default", name: "free" }
} as SelectItemProps;
},
);
badge: model.free && { variant: "default", name: "free" },
} as SelectItemProps;
});
return (
<SelectGroup
@ -81,6 +80,11 @@ function ModelSelector(props: ModelSelectorProps) {
if (!auth && model.auth) {
toast({
title: t("login-require"),
action: (
<ToastAction altText={t("login")} onClick={login}>
{t("login")}
</ToastAction>
),
});
return;
}

View File

@ -1,27 +1,23 @@
import { useTranslation } from "react-i18next";
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "../../store";
import { selectAuthenticated, selectUsername } from "../../store/auth.ts";
import { selectCurrent, selectHistory } from "../../store/chat.ts";
import { RootState } from "@/store";
import { selectAuthenticated, selectUsername } from "@/store/auth.ts";
import { selectCurrent, selectHistory } from "@/store/chat.ts";
import { useRef, useState } from "react";
import { ConversationInstance } from "../../conversation/types.ts";
import { useToast } from "../ui/use-toast.ts";
import {
copyClipboard,
extractMessage,
filterMessage,
mobile,
useAnimation,
useEffectAsync,
} from "../../utils.ts";
import { ConversationInstance } from "@/conversation/types.ts";
import { useToast } from "@/components/ui/use-toast.ts";
import { extractMessage, filterMessage } from "@/utils/processor.ts";
import { copyClipboard } from "@/utils/dom.ts";
import { useEffectAsync, useAnimation } from "@/utils/hook.ts";
import { mobile } from "@/utils/device.ts";
import {
deleteAllConversations,
deleteConversation,
toggleConversation,
updateConversationList,
} from "../../conversation/history.ts";
import { Button } from "../ui/button.tsx";
import { setMenu } from "../../store/menu.ts";
} from "@/conversation/history.ts";
import { Button } from "@/components/ui/button.tsx";
import { setMenu } from "@/store/menu.ts";
import {
Copy,
Eraser,
@ -41,15 +37,12 @@ import {
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "../ui/alert-dialog.tsx";
import {
getSharedLink,
shareConversation,
} from "../../conversation/sharing.ts";
import { Input } from "../ui/input.tsx";
import { login } from "../../conf.ts";
import MenuBar from "../app/MenuBar.tsx";
import { Separator } from "../ui/separator.tsx";
} from "@/components/ui/alert-dialog.tsx";
import { getSharedLink, shareConversation } from "@/conversation/sharing.ts";
import { Input } from "@/components/ui/input.tsx";
import { login } from "@/conf.ts";
import MenuBar from "@/components/app/MenuBar.tsx";
import { Separator } from "@/components/ui/separator.tsx";
type Operation = {
target: ConversationInstance | null;

View File

@ -1,5 +1,5 @@
import { File } from "lucide-react";
import { saveAsFile } from "../../utils.ts";
import { saveAsFile } from "@/utils/dom.ts";
/**
* file format:

View File

@ -1,4 +1,4 @@
import { tokenField, ws_api } from "../conf.ts";
import { tokenField, ws_api } from "@/conf.ts";
export const endpoint = `${ws_api}/chat`;

View File

@ -1,10 +1,10 @@
import { ChatProps, Connection, StreamMessage } from "./connection.ts";
import { Message } from "./types.ts";
import { sharingEvent } from "../events/sharing.ts";
import { connectionEvent } from "../events/connection.ts";
import { AppDispatch } from "../store";
import { setMessages } from "../store/chat.ts";
import { modelEvent } from "../events/model.ts";
import { sharingEvent } from "@/events/sharing.ts";
import { connectionEvent } from "@/events/connection.ts";
import { AppDispatch } from "@/store";
import { setMessages } from "@/store/chat.ts";
import { modelEvent } from "@/events/model.ts";
type ConversationCallback = (idx: number, message: Message[]) => boolean;

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

View File

@ -1,8 +1,8 @@
import axios from "axios";
import type { ConversationInstance } from "./types.ts";
import { setHistory } from "../store/chat.ts";
import { setHistory } from "@/store/chat.ts";
import { manager } from "./manager.ts";
import { AppDispatch } from "../store";
import { AppDispatch } from "@/store";
export async function updateConversationList(
dispatch: AppDispatch,

View File

@ -1,16 +1,16 @@
import { Conversation } from "./conversation";
import { ConversationMapper, Message } from "./types.ts";
import { loadConversation } from "./history.ts";
import { Conversation } from "@/conversation/conversation.ts";
import { ConversationMapper, Message } from "@/conversation/types.ts";
import { loadConversation } from "@/conversation/history.ts";
import {
addHistory,
removeHistory,
setCurrent,
setMessages,
} from "../store/chat.ts";
import { useShared } from "../utils.ts";
import { ChatProps } from "./connection.ts";
import { AppDispatch } from "../store";
import { sharingEvent } from "../events/sharing.ts";
} from "@/store/chat.ts";
import { useShared } from "@/utils/hook.ts";
import { ChatProps } from "@/conversation/connection.ts";
import { AppDispatch } from "@/store";
import { sharingEvent } from "@/events/sharing.ts";
export class Manager {
conversations: Record<number, Conversation>;

View File

@ -5,9 +5,9 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
} from "../components/ui/dialog.tsx";
import { Button } from "../components/ui/button.tsx";
import "../assets/api.less";
} from "@/components/ui/dialog.tsx";
import { Button } from "@/components/ui/button.tsx";
import "@/assets/api.less";
import { useTranslation } from "react-i18next";
import { useDispatch, useSelector } from "react-redux";
import {
@ -16,12 +16,13 @@ import {
setDialog,
keySelector,
getApiKey,
} from "../store/api.ts";
import { Input } from "../components/ui/input.tsx";
} from "@/store/api.ts";
import { Input } from "@/components/ui/input.tsx";
import { Copy, ExternalLink } from "lucide-react";
import { useToast } from "../components/ui/use-toast.ts";
import { copyClipboard, useEffectAsync } from "../utils.ts";
import { selectInit } from "../store/auth.ts";
import { useToast } from "@/components/ui/use-toast.ts";
import { copyClipboard } from "@/utils/dom.ts";
import { useEffectAsync } from "@/utils/hook.ts";
import { selectInit } from "@/store/auth.ts";
function ApiKey() {
const { t } = useTranslation();

View File

@ -5,15 +5,15 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
} from "../components/ui/dialog.tsx";
import { Button } from "../components/ui/button.tsx";
} from "@/components/ui/dialog.tsx";
import { Button } from "@/components/ui/button.tsx";
import { useTranslation } from "react-i18next";
import { useDispatch, useSelector } from "react-redux";
import { closeDialog, dialogSelector, setDialog } from "../store/invitation.ts";
import { Input } from "../components/ui/input.tsx";
import { useToast } from "../components/ui/use-toast.ts";
import { closeDialog, dialogSelector, setDialog } from "@/store/invitation.ts";
import { Input } from "@/components/ui/input.tsx";
import { useToast } from "@/components/ui/use-toast.ts";
import { useState } from "react";
import { getInvitation } from "../conversation/invitation.ts";
import { getInvitation } from "@/conversation/invitation.ts";
function Invitation() {
const { t } = useTranslation();

View File

@ -5,9 +5,9 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
} from "../components/ui/dialog.tsx";
import { Button } from "../components/ui/button.tsx";
import "../assets/package.less";
} from "@/components/ui/dialog.tsx";
import { Button } from "@/components/ui/button.tsx";
import "@/assets/package.less";
import { useTranslation } from "react-i18next";
import { useDispatch, useSelector } from "react-redux";
import {
@ -17,11 +17,11 @@ import {
refreshPackageTask,
setDialog,
teenagerSelector,
} from "../store/package.ts";
} from "@/store/package.ts";
import { useEffect } from "react";
import { Gift } from "lucide-react";
import { Separator } from "../components/ui/separator.tsx";
import { Badge } from "../components/ui/badge.tsx";
import { Separator } from "@/components/ui/separator.tsx";
import { Badge } from "@/components/ui/badge.tsx";
function Package() {
const { t } = useTranslation();

View File

@ -4,7 +4,7 @@ import {
dialogSelector,
refreshQuotaTask,
setDialog,
} from "../store/quota.ts";
} from "@/store/quota.ts";
import { useTranslation } from "react-i18next";
import { useEffect, useState } from "react";
import {
@ -13,8 +13,8 @@ import {
DialogDescription,
DialogHeader,
DialogTitle,
} from "../components/ui/dialog.tsx";
import "../assets/quota.less";
} from "@/components/ui/dialog.tsx";
import "@/assets/quota.less";
import {
BadgePercent,
Cloud,
@ -24,10 +24,10 @@ import {
Info,
Plus,
} from "lucide-react";
import { Input } from "../components/ui/input.tsx";
import { testNumberInputEvent } from "../utils.ts";
import { Button } from "../components/ui/button.tsx";
import { Separator } from "../components/ui/separator.tsx";
import { Input } from "@/components/ui/input.tsx";
import { testNumberInputEvent } from "@/utils/dom.ts";
import { Button } from "@/components/ui/button.tsx";
import { Separator } from "@/components/ui/separator.tsx";
import {
AlertDialog,
AlertDialogAction,
@ -37,10 +37,10 @@ import {
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTrigger,
} from "../components/ui/alert-dialog.tsx";
} from "@/components/ui/alert-dialog.tsx";
import { AlertDialogTitle } from "@radix-ui/react-alert-dialog";
import { buyQuota } from "../conversation/addition.ts";
import { useToast } from "../components/ui/use-toast.ts";
import { buyQuota } from "@/conversation/addition.ts";
import { useToast } from "@/components/ui/use-toast.ts";
type AmountComponentProps = {
amount: number;

View File

@ -1,4 +1,4 @@
import "../assets/share-manager.less";
import "@/assets/share-manager.less";
import { useTranslation } from "react-i18next";
import { useDispatch, useSelector } from "react-redux";
import {
@ -6,10 +6,10 @@ import {
dataSelector,
syncData,
deleteData,
} from "../store/sharing.ts";
import { useToast } from "../components/ui/use-toast.ts";
import { selectAuthenticated, selectInit } from "../store/auth.ts";
import { useEffectAsync } from "../utils.ts";
} from "@/store/sharing.ts";
import { useToast } from "@/components/ui/use-toast.ts";
import { selectAuthenticated, selectInit } from "@/store/auth.ts";
import { useEffectAsync } from "@/utils/hook.ts";
import {
Dialog,
DialogContent,
@ -17,7 +17,7 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
} from "../components/ui/dialog.tsx";
} from "@/components/ui/dialog.tsx";
import {
Table,
TableBody,
@ -25,9 +25,9 @@ import {
TableHead,
TableHeader,
TableRow,
} from "../components/ui/table.tsx";
import { closeDialog, setDialog } from "../store/sharing.ts";
import { Button } from "../components/ui/button.tsx";
} from "@/components/ui/table.tsx";
import { closeDialog, setDialog } from "@/store/sharing.ts";
import { Button } from "@/components/ui/button.tsx";
import { useMemo } from "react";
import { Eye, MoreHorizontal, Trash2 } from "lucide-react";
import {
@ -35,8 +35,8 @@ import {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "../components/ui/dropdown-menu.tsx";
import { getSharedLink, SharingPreviewForm } from "../conversation/sharing.ts";
} from "@/components/ui/dropdown-menu.tsx";
import { getSharedLink, SharingPreviewForm } from "@/conversation/sharing.ts";
type ShareTableProps = {
data: SharingPreviewForm[];

View File

@ -6,7 +6,7 @@ import {
refreshSubscriptionTask,
setDialog,
usageSelector,
} from "../store/subscription.ts";
} from "@/store/subscription.ts";
import {
Dialog,
DialogContent,
@ -15,12 +15,12 @@ import {
DialogHeader,
DialogTitle,
DialogTrigger,
} from "../components/ui/dialog.tsx";
} from "@/components/ui/dialog.tsx";
import { useDispatch, useSelector } from "react-redux";
import { useTranslation } from "react-i18next";
import { useToast } from "../components/ui/use-toast.ts";
import { useToast } from "@/components/ui/use-toast.ts";
import React, { useEffect } from "react";
import "../assets/subscription.less";
import "@/assets/subscription.less";
import {
BookText,
Calendar,
@ -35,16 +35,16 @@ import {
ServerCrash,
Webhook,
} from "lucide-react";
import { Button } from "../components/ui/button.tsx";
import { Button } from "@/components/ui/button.tsx";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "../components/ui/select.tsx";
import { Badge } from "../components/ui/badge.tsx";
import { buySubscription } from "../conversation/addition.ts";
} from "@/components/ui/select.tsx";
import { Badge } from "@/components/ui/badge.tsx";
import { buySubscription } from "@/conversation/addition.ts";
function calc_prize(month: number): number {
const base = 32 * month;

View File

@ -1,4 +1,4 @@
import { Toaster } from "../components/ui/toaster.tsx";
import { Toaster } from "@/components/ui/toaster.tsx";
import Quota from "./Quota.tsx";
import ApiKey from "./ApiKey.tsx";
import Package from "./Package.tsx";

View File

@ -1,5 +1,5 @@
import { EventCommitter } from "./struct.ts";
import { Message } from "../conversation/types.ts";
import { Message } from "@/conversation/types.ts";
export type SharingEvent = {
refer: string;

View File

@ -1,14 +1,14 @@
import { useToast } from "../components/ui/use-toast.ts";
import { useToast } from "@/components/ui/use-toast.ts";
import { useLocation } from "react-router-dom";
import { ToastAction } from "../components/ui/toast.tsx";
import { login } from "../conf.ts";
import { ToastAction } from "@/components/ui/toast.tsx";
import { login } from "@/conf.ts";
import { useEffect } from "react";
import Loader from "../components/Loader.tsx";
import "../assets/auth.less";
import Loader from "@/components/Loader.tsx";
import "@/assets/auth.less";
import axios from "axios";
import { validateToken } from "../store/auth.ts";
import { validateToken } from "@/store/auth.ts";
import { useDispatch } from "react-redux";
import router from "../router.tsx";
import router from "@/router.tsx";
import { useTranslation } from "react-i18next";
function Auth() {

View File

@ -1,17 +1,17 @@
import "../assets/generation.less";
import "@/assets/generation.less";
import { useSelector } from "react-redux";
import { useTranslation } from "react-i18next";
import { Button } from "../components/ui/button.tsx";
import { Button } from "@/components/ui/button.tsx";
import { ChevronLeft, Cloud, FileDown, Send } from "lucide-react";
import { rest_api } from "../conf.ts";
import router from "../router.tsx";
import { Input } from "../components/ui/input.tsx";
import { rest_api } from "@/conf.ts";
import router from "@/router.tsx";
import { Input } from "@/components/ui/input.tsx";
import { useEffect, useRef, useState } from "react";
import { manager } from "../conversation/generation.ts";
import { useToast } from "../components/ui/use-toast.ts";
import { handleGenerationData } from "../utils.ts";
import { selectModel } from "../store/chat.ts";
import ModelSelector from "../components/home/ModelSelector.tsx";
import { manager } from "@/conversation/generation.ts";
import { useToast } from "@/components/ui/use-toast.ts";
import { handleGenerationData } from "@/utils/processor.ts";
import { selectModel } from "@/store/chat.ts";
import ModelSelector from "@/components/home/ModelSelector.tsx";
type WrapperProps = {
onSend?: (value: string, model: string) => boolean;

View File

@ -1,7 +1,7 @@
import "../assets/home.less";
import "../assets/chat.less";
import ChatWrapper from "../components/home/ChatWrapper.tsx";
import SideBar from "../components/home/SideBar.tsx";
import "@/assets/home.less";
import "@/assets/chat.less";
import ChatWrapper from "@/components/home/ChatWrapper.tsx";
import SideBar from "@/components/home/SideBar.tsx";
function Home() {
return (

View File

@ -1,7 +1,7 @@
import "../assets/404.less";
import { Button } from "../components/ui/button.tsx";
import "@/assets/404.less";
import { Button } from "@/components/ui/button.tsx";
import { HelpCircle } from "lucide-react";
import router from "../router.tsx";
import router from "@/router.tsx";
import { useTranslation } from "react-i18next";
function NotFound() {

View File

@ -1,20 +1,21 @@
import "../assets/sharing.less";
import "@/assets/sharing.less";
import { useParams } from "react-router-dom";
import {
viewConversation,
ViewData,
ViewForm,
} from "../conversation/sharing.ts";
import { copyClipboard, saveAsFile, useEffectAsync } from "../utils.ts";
} from "@/conversation/sharing.ts";
import { copyClipboard, saveAsFile } from "@/utils/dom.ts";
import { useEffectAsync } from "@/utils/hook.ts";
import { useState } from "react";
import { Copy, File, HelpCircle, Loader2, MessagesSquare } from "lucide-react";
import { useTranslation } from "react-i18next";
import MessageSegment from "../components/Message.tsx";
import { Button } from "../components/ui/button.tsx";
import router from "../router.tsx";
import { useToast } from "../components/ui/use-toast.ts";
import { sharingEvent } from "../events/sharing.ts";
import { Message } from "../conversation/types.ts";
import MessageSegment from "@/components/Message.tsx";
import { Button } from "@/components/ui/button.tsx";
import router from "@/router.tsx";
import { useToast } from "@/components/ui/use-toast.ts";
import { sharingEvent } from "@/events/sharing.ts";
import { Message } from "@/conversation/types.ts";
type SharingFormProps = {
refer?: string;

View File

@ -1,5 +1,5 @@
import { createSlice } from "@reduxjs/toolkit";
import { getKey } from "../conversation/addition.ts";
import { getKey } from "@/conversation/addition.ts";
import { AppDispatch, RootState } from "./index.ts";
export const apiSlice = createSlice({

View File

@ -1,6 +1,6 @@
import { createSlice } from "@reduxjs/toolkit";
import axios from "axios";
import { tokenField } from "../conf.ts";
import { tokenField } from "@/conf.ts";
import { AppDispatch } from "./index.ts";
export const authSlice = createSlice({

View File

@ -1,9 +1,9 @@
import { createSlice } from "@reduxjs/toolkit";
import { ConversationInstance, Model } from "../conversation/types.ts";
import { Message } from "../conversation/types.ts";
import { insertStart } from "../utils.ts";
import { ConversationInstance, Model } from "@/conversation/types.ts";
import { Message } from "@/conversation/types.ts";
import { insertStart } from "@/utils/base.ts";
import { RootState } from "./index.ts";
import { supportModels } from "../conf.ts";
import { supportModels } from "@/conf.ts";
type initialStateType = {
history: ConversationInstance[];

View File

@ -1,5 +1,5 @@
import { createSlice } from "@reduxjs/toolkit";
import { mobile } from "../utils.ts";
import { mobile } from "@/utils/device.ts";
export const menuSlice = createSlice({
name: "menu",

View File

@ -1,5 +1,5 @@
import { createSlice } from "@reduxjs/toolkit";
import { getPackage } from "../conversation/addition.ts";
import { getPackage } from "@/conversation/addition.ts";
import { AppDispatch } from "./index.ts";
export const packageSlice = createSlice({

View File

@ -4,7 +4,7 @@ import {
deleteSharing,
listSharing,
SharingPreviewForm,
} from "../conversation/sharing.ts";
} from "@/conversation/sharing.ts";
export const sharingSlice = createSlice({
name: "sharing",

View File

@ -1,5 +1,5 @@
import { createSlice } from "@reduxjs/toolkit";
import { getSubscription } from "../conversation/addition.ts";
import { getSubscription } from "@/conversation/addition.ts";
import { AppDispatch } from "./index.ts";
export const subscriptionSlice = createSlice({

View File

@ -1,284 +0,0 @@
import React, { useEffect } from "react";
import { FileObject } from "./components/FileProvider.tsx";
export let event: BeforeInstallPromptEvent | undefined;
export let mobile = isMobile();
window.addEventListener("resize", () => {
mobile = isMobile();
});
window.addEventListener("beforeinstallprompt", (e: Event) => {
// e.preventDefault();
event = e as BeforeInstallPromptEvent;
});
export function triggerInstallApp() {
if (!event) return;
try {
event.prompt();
event.userChoice.then((choice: any) => {
console.debug(`[service] installed app (status: ${choice.outcome})`);
});
} catch (err) {
console.debug("[service] install app error", err);
}
event = undefined;
}
export function isMobile(): boolean {
return (
(document.documentElement.clientWidth || window.innerWidth) <= 668 ||
(document.documentElement.clientHeight || window.innerHeight) <= 468 ||
navigator.userAgent.includes("Mobile")
);
}
export function useEffectAsync<T>(effect: () => Promise<T>, deps?: any[]) {
return useEffect(() => {
effect().catch((err) =>
console.debug("[runtime] error during use effect", err),
);
}, deps);
}
export function useAnimation(
ref: React.MutableRefObject<any>,
cls: string,
min?: number,
): (() => number) | undefined {
if (!ref.current) return;
const target = ref.current as HTMLButtonElement;
const stamp = Date.now();
target.classList.add(cls);
return function () {
const duration = Date.now() - stamp;
const timeout = min ? Math.max(min - duration, 0) : 0;
setTimeout(() => target.classList.remove(cls), timeout);
return timeout;
};
}
export function useShared<T>(): {
hook: (v: T) => void;
useHook: () => Promise<T>;
} {
let value: T | undefined = undefined;
return {
hook: (v: T) => {
value = v;
},
useHook: () => {
return new Promise<T>((resolve) => {
if (value) return resolve(value);
const interval = setInterval(() => {
if (value) {
clearInterval(interval);
resolve(value);
}
}, 50);
});
},
};
}
export function insert<T>(arr: T[], idx: number, value: T): T[] {
return [...arr.slice(0, idx), value, ...arr.slice(idx)];
}
export function insertStart<T>(arr: T[], value: T): T[] {
return [value, ...arr];
}
export function remove<T>(arr: T[], idx: number): T[] {
return [...arr.slice(0, idx), ...arr.slice(idx + 1)];
}
export function replace<T>(arr: T[], idx: number, value: T): T[] {
return [...arr.slice(0, idx), value, ...arr.slice(idx + 1)];
}
export function move<T>(arr: T[], from: number, to: number): T[] {
const value = arr[from];
return insert(remove(arr, from), to, value);
}
export async function copyClipboard(text: string) {
if (!navigator.clipboard) {
const input = document.createElement("input");
input.value = text;
input.style.position = "absolute";
input.style.left = "-9999px";
document.body.appendChild(input);
input.select();
document.execCommand("copy");
document.body.removeChild(input);
return;
}
await navigator.clipboard.writeText(text);
}
export function getQueryParams() {
const params = new URLSearchParams(window.location.search);
const obj: Record<string, string> = {};
for (const [key, value] of params.entries()) {
obj[key] = value;
}
return obj;
}
export function getQueryParam(key: string): string {
const params = new URLSearchParams(window.location.search);
return params.get(key) || "";
}
export function saveAsFile(filename: string, content: string) {
const a = document.createElement("a");
a.href = URL.createObjectURL(new Blob([content]));
a.download = filename;
a.click();
}
export function replaceInputValue(
input: HTMLInputElement | undefined,
value: string,
) {
return input && (input.value = value);
}
export function useInputValue(id: string, value: string) {
const input = document.getElementById(id) as HTMLInputElement | undefined;
return input && replaceInputValue(input, value) && input.focus();
}
export function testNumberInputEvent(e: any): boolean {
if (
/^[0-9]+$/.test(e.key) ||
["Backspace", "Delete", "ArrowLeft", "ArrowRight", "Tab"].includes(e.key)
) {
return true;
}
e.preventDefault();
return false;
}
export function formatMessage(file: FileObject, message: string): string {
message = message.trim();
if (file.name.length > 0 || file.content.length > 0) {
return `
\`\`\`file
[[${file.name}]]
${file.content}
\`\`\`
${message}`;
} else {
return message;
}
}
export function filterMessage(message: string): string {
return message.replace(/```file\n\[\[.*]]\n[\s\S]*?\n```\n\n/g, "");
}
export function extractMessage(
message: string,
length: number = 50,
flow: string = "...",
) {
return message.length > length ? message.slice(0, length) + flow : message;
}
export function useDraggableInput(
t: any,
toast: any,
target: HTMLLabelElement,
handleChange: (filename?: string, content?: string) => void,
) {
target.addEventListener("dragover", (e) => {
e.preventDefault();
e.stopPropagation();
});
target.addEventListener("drop", (e) => {
e.preventDefault();
e.stopPropagation();
const file = e.dataTransfer?.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
const data = e.target?.result as string;
if (!/^[\x00-\x7F]*$/.test(data)) {
toast({
title: t("file.parse-error"),
description: t("file.parse-error-prompt"),
});
handleChange();
} else {
handleChange(file.name, e.target?.result as string);
}
};
reader.readAsText(file);
} else {
handleChange();
}
});
}
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, "'");
}
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");
} else {
return data;
}
}
export function handleGenerationData(data: string): string {
data = data
.replace(/{\s*"result":\s*{/g, "")
.trim()
.replace(/}\s*$/g, "");
return handleLine(escapeRegExp(data), 6);
}
export function getSelectionText(): string {
if (window.getSelection) {
return window.getSelection()?.toString() || "";
} else if (document.getSelection && document.getSelection()?.toString()) {
return document.getSelection()?.toString() || "";
}
return "";
}
// browser compatibility issue
export function getSelectionTextInArea(el: HTMLElement): string {
const selection = window.getSelection();
if (!selection) return "";
const range = selection.getRangeAt(0);
const preSelectionRange = range.cloneRange();
preSelectionRange.selectNodeContents(el);
preSelectionRange.setEnd(range.startContainer, range.startOffset);
const start = preSelectionRange.toString().length;
return el.innerText.slice(start, start + range.toString().length);
}

29
app/src/utils/app.ts Normal file
View File

@ -0,0 +1,29 @@
export let event: BeforeInstallPromptEvent | undefined;
window.addEventListener("beforeinstallprompt", (e: Event) => {
// e.preventDefault();
console.debug(`[service] catch event from app install prompt`);
event = e as BeforeInstallPromptEvent;
});
export function triggerInstallApp() {
/**
* Trigger install app prompt
* Warning: this is a browser experimental feature, it may not work on some browsers
* @see https://developer.mozilla.org/en-US/docs/Web/API/BeforeInstallPromptEvent
*
* @example
* triggerInstallApp();
*/
if (!event) return;
try {
event.prompt();
event.userChoice.then((choice: any) => {
console.debug(`[service] installed app (status: ${choice.outcome})`);
});
} catch (err) {
console.debug("[service] install app error", err);
}
event = undefined;
}

20
app/src/utils/base.ts Normal file
View File

@ -0,0 +1,20 @@
export function insert<T>(arr: T[], idx: number, value: T): T[] {
return [...arr.slice(0, idx), value, ...arr.slice(idx)];
}
export function insertStart<T>(arr: T[], value: T): T[] {
return [value, ...arr];
}
export function remove<T>(arr: T[], idx: number): T[] {
return [...arr.slice(0, idx), ...arr.slice(idx + 1)];
}
export function replace<T>(arr: T[], idx: number, value: T): T[] {
return [...arr.slice(0, idx), value, ...arr.slice(idx + 1)];
}
export function move<T>(arr: T[], from: number, to: number): T[] {
const value = arr[from];
return insert(remove(arr, from), to, value);
}

13
app/src/utils/device.ts Normal file
View File

@ -0,0 +1,13 @@
export let mobile = isMobile();
window.addEventListener("resize", () => {
mobile = isMobile();
});
export function isMobile(): boolean {
return (
(document.documentElement.clientWidth || window.innerWidth) <= 668 ||
(document.documentElement.clientHeight || window.innerHeight) <= 468 ||
navigator.userAgent.includes("Mobile")
);
}

177
app/src/utils/dom.ts Normal file
View File

@ -0,0 +1,177 @@
export async function copyClipboard(text: string) {
/**
* Copy text to clipboard
* @param text Text to copy
* @example
* await copyClipboard("Hello world!");
* @see https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/writeText
*/
if (!navigator.clipboard) {
const input = document.createElement("input");
input.value = text;
input.style.position = "absolute";
input.style.left = "-9999px";
document.body.appendChild(input);
input.select();
document.execCommand("copy");
document.body.removeChild(input);
return;
}
await navigator.clipboard.writeText(text);
}
export function saveAsFile(filename: string, content: string) {
/**
* Save text as file
* @param filename Filename
* @param content File content
* @example
* saveAsFile("hello.txt", "Hello world!");
* @see https://developer.mozilla.org/en-US/docs/Web/API/Blob
*/
const a = document.createElement("a");
a.href = URL.createObjectURL(new Blob([content]));
a.download = filename;
a.click();
}
export function getSelectionText(): string {
/**
* Get selected text
* @example
* const text = getSelectionText();
* console.log(text);
* @see https://developer.mozilla.org/en-US/docs/Web/API/Window/getSelection
*/
if (window.getSelection) {
return window.getSelection()?.toString() || "";
} else if (document.getSelection && document.getSelection()?.toString()) {
return document.getSelection()?.toString() || "";
}
return "";
}
export function getSelectionTextInArea(el: HTMLElement): string {
/**
* Get selected text in element
* @param el Element
* @example
* const text = getSelectionTextInArea(document.getElementById("textarea"));
* console.log(text);
* @see https://developer.mozilla.org/en-US/docs/Web/API/Window/getSelection
*/
const selection = window.getSelection();
if (!selection) return "";
const range = selection.getRangeAt(0);
const preSelectionRange = range.cloneRange();
preSelectionRange.selectNodeContents(el);
preSelectionRange.setEnd(range.startContainer, range.startOffset);
const start = preSelectionRange.toString().length;
return el.innerText.slice(start, start + range.toString().length);
}
export function useDraggableInput(
t: any,
toast: any,
target: HTMLLabelElement,
handleChange: (filename?: string, content?: string) => void,
) {
/**
* Make input element draggable
* @param t i18n function
* @param toast Toast function
* @param target Input element
* @param handleChange Handle change function
* @example
* const input = document.getElementById("input") as HTMLLabelElement;
* useDraggableInput(t, toast, input, handleChange);
*/
target.addEventListener("dragover", (e) => {
e.preventDefault();
e.stopPropagation();
});
target.addEventListener("drop", (e) => {
e.preventDefault();
e.stopPropagation();
const file = e.dataTransfer?.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
const data = e.target?.result as string;
if (!/^[\x00-\x7F]*$/.test(data)) {
toast({
title: t("file.parse-error"),
description: t("file.parse-error-prompt"),
});
handleChange();
} else {
handleChange(file.name, e.target?.result as string);
}
};
reader.readAsText(file);
} else {
handleChange();
}
});
}
export function testNumberInputEvent(e: any): boolean {
/**
* Test if input event is valid for number input
* @param e Input event
* @example
* const handler = (e: any) => {
* if (testNumberInputEvent(e)) {
* // do something
* }
* return;
* }
* @see https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key
*/
if (
/^[0-9]+$/.test(e.key) ||
["Backspace", "Delete", "ArrowLeft", "ArrowRight", "Tab"].includes(e.key)
) {
return true;
}
e.preventDefault();
return false;
}
export function replaceInputValue(
input: HTMLInputElement | undefined,
value: string,
) {
/**
* Replace input value and focus
* @param input Input element
* @param value New value
* @example
* const input = document.getElementById("input") as HTMLInputElement;
* replaceInputValue(input, "Hello world!");
*/
return input && (input.value = value);
}
export function useInputValue(id: string, value: string) {
/**
* Replace input value and focus
* @param id Input element id
* @param value New value
* @example
* const input = document.getElementById("input") as HTMLInputElement;
* useInputValue("input", "Hello world!");
*/
const input = document.getElementById(id) as HTMLInputElement | undefined;
return input && replaceInputValue(input, value) && input.focus();
}

79
app/src/utils/hook.ts Normal file
View File

@ -0,0 +1,79 @@
import React, { useEffect } from "react";
export function useEffectAsync<T>(effect: () => Promise<T>, deps?: any[]) {
/**
* useEffect with async/await support
*
* @example
* useEffectAsync(async () => {
* const result = await fetch("https://api.example.com");
* console.log(result);
* }, []);
*/
return useEffect(() => {
effect().catch((err) =>
console.debug("[runtime] error during use effect", err),
);
}, deps);
}
export function useAnimation(
ref: React.MutableRefObject<any>,
cls: string,
min?: number,
): (() => number) | undefined {
/**
* Add animation class to react ref element and remove it after min ms when returned function is called
*
* @example
* const animation = useAnimation(ref, "animate", 1000);
* axios.get("https://api.example.com")
* .finally(() => animation());
*/
if (!ref.current) return;
const target = ref.current as HTMLButtonElement;
const stamp = Date.now();
target.classList.add(cls);
return function () {
const duration = Date.now() - stamp;
const timeout = min ? Math.max(min - duration, 0) : 0;
setTimeout(() => target.classList.remove(cls), timeout);
return timeout;
};
}
export function useShared<T>(): {
hook: (v: T) => void;
useHook: () => Promise<T>;
} {
/**
* Share value between components, useful for sharing data between components / redux dispatches
*
* @example
*
* const dispatch = useDispatch();
* const { hook, useHook } = useShared<string>();
*
* dispatch(updateMigration({ hook }));
* const response = await useHook();
*/
let value: T | undefined = undefined;
return {
hook: (v: T) => {
value = v;
},
useHook: () => {
return new Promise<T>((resolve) => {
if (value) return resolve(value);
const interval = setInterval(() => {
if (value) {
clearInterval(interval);
resolve(value);
}
}, 50);
});
},
};
}

31
app/src/utils/path.ts Normal file
View File

@ -0,0 +1,31 @@
export function getQueryParams() {
/**
* Get query params from url
*
* @example
* // https://example.com?foo=bar&baz=qux
* getQueryParams();
* // { foo: "bar", baz: "qux" }
*/
const params = new URLSearchParams(window.location.search);
const obj: Record<string, string> = {};
for (const [key, value] of params.entries()) {
obj[key] = value;
}
return obj;
}
export function getQueryParam(key: string): string {
/**
* Get query param from url
*
* @example
* // https://example.com?foo=bar&baz=qux
* getQueryParam("foo");
* // "bar"
*/
const params = new URLSearchParams(window.location.search);
return params.get(key) || "";
}

View File

@ -0,0 +1,63 @@
import { FileObject } from "@/components/FileProvider.tsx";
export function formatMessage(file: FileObject, message: string): string {
message = message.trim();
if (file.name.length > 0 || file.content.length > 0) {
return `
\`\`\`file
[[${file.name}]]
${file.content}
\`\`\`
${message}`;
} else {
return message;
}
}
export function filterMessage(message: string): string {
return message.replace(/```file\n\[\[.*]]\n[\s\S]*?\n```\n\n/g, "");
}
export function extractMessage(
message: string,
length: number = 50,
flow: string = "...",
) {
return message.length > length ? message.slice(0, length) + flow : message;
}
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, "'");
}
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");
} else {
return data;
}
}
export function handleGenerationData(data: string): string {
data = data
.replace(/{\s*"result":\s*{/g, "")
.trim()
.replace(/}\s*$/g, "");
return handleLine(escapeRegExp(data), 6);
}

View File

@ -6,7 +6,7 @@
"module": "ESNext",
"skipLibCheck": true,
"types": [
"vite-plugin-pwa/react",
"vite-plugin-pwa/react"
],
/* Bundler mode */
@ -21,12 +21,15 @@
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
"noFallthroughCasesInSwitch": true,
"paths": {
"@/*": ["./src/*"],
}
},
"include": ["src"],
"include": [
"src",
"*.ts",
],
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
"references": [{ "path": "./tsconfig.node.json" }],
}