diff --git a/app/src/assets/markdown/all.less b/app/src/assets/markdown/all.less
index 94d9fe2..537feff 100644
--- a/app/src/assets/markdown/all.less
+++ b/app/src/assets/markdown/all.less
@@ -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 {
diff --git a/app/src/components/EditorProvider.tsx b/app/src/components/EditorProvider.tsx
index ef1fb36..c5f135f 100644
--- a/app/src/components/EditorProvider.tsx
+++ b/app/src/components/EditorProvider.tsx
@@ -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;
diff --git a/app/src/components/FileProvider.tsx b/app/src/components/FileProvider.tsx
index 1d770d7..c84da93 100644
--- a/app/src/components/FileProvider.tsx
+++ b/app/src/components/FileProvider.tsx
@@ -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;
diff --git a/app/src/components/I18nProvider.tsx b/app/src/components/I18nProvider.tsx
index 867bc85..13fcb47 100644
--- a/app/src/components/I18nProvider.tsx
+++ b/app/src/components/I18nProvider.tsx
@@ -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() {
diff --git a/app/src/components/Loader.tsx b/app/src/components/Loader.tsx
index e6dff63..2c64a7a 100644
--- a/app/src/components/Loader.tsx
+++ b/app/src/components/Loader.tsx
@@ -1,4 +1,4 @@
-import "../assets/loader.less";
+import "@/assets/loader.less";
type LoaderProps = {
className?: string;
diff --git a/app/src/components/Markdown.tsx b/app/src/components/Markdown.tsx
index f3f705f..7ed2de2 100644
--- a/app/src/components/Markdown.tsx
+++ b/app/src/components/Markdown.tsx
@@ -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 ? (
-
{
- await copyClipboard(children.toString());
- toast({
- title: t("share.copied"),
- });
- }} />
+ {
+ await copyClipboard(children.toString());
+ toast({
+ title: t("share.copied"),
+ });
+ }}
+ />
{match[1]}
("");
+ const [copied, setCopied] = useState("");
const { t } = useTranslation();
const ref = useRef(null);
const { message } = props;
@@ -74,15 +76,11 @@ function MessageSegment(props: MessageProps) {
- {
- copied.length > 0 && (
- copyClipboard(copied)}
- >
- {t("message.copy-area")}
-
- )
- }
+ {copied.length > 0 && (
+ copyClipboard(copied)}>
+ {t("message.copy-area")}
+
+ )}
copyClipboard(filterMessage(message.content))}
>
diff --git a/app/src/components/ProjectLink.tsx b/app/src/components/ProjectLink.tsx
index 52c1df8..76ff8fb 100644
--- a/app/src/components/ProjectLink.tsx
+++ b/app/src/components/ProjectLink.tsx
@@ -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();
diff --git a/app/src/components/ReloadService.tsx b/app/src/components/ReloadService.tsx
index e50b536..843d19b 100644
--- a/app/src/components/ReloadService.tsx
+++ b/app/src/components/ReloadService.tsx
@@ -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";
diff --git a/app/src/components/SelectGroup.tsx b/app/src/components/SelectGroup.tsx
index 2799e94..5ebda84 100644
--- a/app/src/components/SelectGroup.tsx
+++ b/app/src/components/SelectGroup.tsx
@@ -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";
diff --git a/app/src/components/app/AppProvider.tsx b/app/src/components/app/AppProvider.tsx
index 4b1ac1e..7621f48 100644
--- a/app/src/components/app/AppProvider.tsx
+++ b/app/src/components/app/AppProvider.tsx
@@ -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 (
diff --git a/app/src/components/app/MenuBar.tsx b/app/src/components/app/MenuBar.tsx
index fbc655a..be03db9 100644
--- a/app/src/components/app/MenuBar.tsx
+++ b/app/src/components/app/MenuBar.tsx
@@ -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;
diff --git a/app/src/components/app/NavBar.tsx b/app/src/components/app/NavBar.tsx
index a817bfa..d611f95 100644
--- a/app/src/components/app/NavBar.tsx
+++ b/app/src/components/app/NavBar.tsx
@@ -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() {
diff --git a/app/src/components/home/ChatInterface.tsx b/app/src/components/home/ChatInterface.tsx
index 3d7851d..1a3fa18 100644
--- a/app/src/components/home/ChatInterface.tsx
+++ b/app/src/components/home/ChatInterface.tsx
@@ -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);
diff --git a/app/src/components/home/ChatWrapper.tsx b/app/src/components/home/ChatWrapper.tsx
index 4df4cf7..9f4a046 100644
--- a/app/src/components/home/ChatWrapper.tsx
+++ b/app/src/components/home/ChatWrapper.tsx
@@ -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]);
diff --git a/app/src/components/home/ConversationSegment.tsx b/app/src/components/home/ConversationSegment.tsx
index e93d65c..a583682 100644
--- a/app/src/components/home/ConversationSegment.tsx
+++ b/app/src/components/home/ConversationSegment.tsx
@@ -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 = {
diff --git a/app/src/components/home/ModelSelector.tsx b/app/src/components/home/ModelSelector.tsx
index 9c76492..0cd9d30 100644
--- a/app/src/components/home/ModelSelector.tsx
+++ b/app/src/components/home/ModelSelector.tsx
@@ -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 (
+ {t("login")}
+
+ ),
});
return;
}
diff --git a/app/src/components/home/SideBar.tsx b/app/src/components/home/SideBar.tsx
index 84f84f5..f1d8397 100644
--- a/app/src/components/home/SideBar.tsx
+++ b/app/src/components/home/SideBar.tsx
@@ -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;
diff --git a/app/src/components/plugins/file.tsx b/app/src/components/plugins/file.tsx
index 034a097..e346624 100644
--- a/app/src/components/plugins/file.tsx
+++ b/app/src/components/plugins/file.tsx
@@ -1,5 +1,5 @@
import { File } from "lucide-react";
-import { saveAsFile } from "../../utils.ts";
+import { saveAsFile } from "@/utils/dom.ts";
/**
* file format:
diff --git a/app/src/conversation/connection.ts b/app/src/conversation/connection.ts
index fdbda9b..5132bf6 100644
--- a/app/src/conversation/connection.ts
+++ b/app/src/conversation/connection.ts
@@ -1,4 +1,4 @@
-import { tokenField, ws_api } from "../conf.ts";
+import { tokenField, ws_api } from "@/conf.ts";
export const endpoint = `${ws_api}/chat`;
diff --git a/app/src/conversation/conversation.ts b/app/src/conversation/conversation.ts
index b316d3a..5307a01 100644
--- a/app/src/conversation/conversation.ts
+++ b/app/src/conversation/conversation.ts
@@ -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;
diff --git a/app/src/conversation/generation.ts b/app/src/conversation/generation.ts
index bd87cdd..5a93ab1 100644
--- a/app/src/conversation/generation.ts
+++ b/app/src/conversation/generation.ts
@@ -1,4 +1,4 @@
-import { ws_api } from "../conf.ts";
+import { ws_api } from "@/conf.ts";
export const endpoint = `${ws_api}/generation/create`;
diff --git a/app/src/conversation/history.ts b/app/src/conversation/history.ts
index 5969e78..9060ae9 100644
--- a/app/src/conversation/history.ts
+++ b/app/src/conversation/history.ts
@@ -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,
diff --git a/app/src/conversation/manager.ts b/app/src/conversation/manager.ts
index e8a4535..30aea80 100644
--- a/app/src/conversation/manager.ts
+++ b/app/src/conversation/manager.ts
@@ -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;
diff --git a/app/src/dialogs/ApiKey.tsx b/app/src/dialogs/ApiKey.tsx
index 3a56514..8279efa 100644
--- a/app/src/dialogs/ApiKey.tsx
+++ b/app/src/dialogs/ApiKey.tsx
@@ -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();
diff --git a/app/src/dialogs/Invitation.tsx b/app/src/dialogs/Invitation.tsx
index eb8beb1..814e914 100644
--- a/app/src/dialogs/Invitation.tsx
+++ b/app/src/dialogs/Invitation.tsx
@@ -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();
diff --git a/app/src/dialogs/Package.tsx b/app/src/dialogs/Package.tsx
index cf9f889..f905973 100644
--- a/app/src/dialogs/Package.tsx
+++ b/app/src/dialogs/Package.tsx
@@ -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();
diff --git a/app/src/dialogs/Quota.tsx b/app/src/dialogs/Quota.tsx
index a5f3976..97e3ead 100644
--- a/app/src/dialogs/Quota.tsx
+++ b/app/src/dialogs/Quota.tsx
@@ -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;
diff --git a/app/src/dialogs/ShareManagement.tsx b/app/src/dialogs/ShareManagement.tsx
index 43ac8ec..f0c1930 100644
--- a/app/src/dialogs/ShareManagement.tsx
+++ b/app/src/dialogs/ShareManagement.tsx
@@ -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[];
diff --git a/app/src/dialogs/Subscription.tsx b/app/src/dialogs/Subscription.tsx
index 7537197..b613e48 100644
--- a/app/src/dialogs/Subscription.tsx
+++ b/app/src/dialogs/Subscription.tsx
@@ -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;
diff --git a/app/src/dialogs/index.tsx b/app/src/dialogs/index.tsx
index cb89d56..caa75ee 100644
--- a/app/src/dialogs/index.tsx
+++ b/app/src/dialogs/index.tsx
@@ -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";
diff --git a/app/src/events/sharing.ts b/app/src/events/sharing.ts
index ab546d0..5cbc28c 100644
--- a/app/src/events/sharing.ts
+++ b/app/src/events/sharing.ts
@@ -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;
diff --git a/app/src/routes/Auth.tsx b/app/src/routes/Auth.tsx
index a13c553..d988268 100644
--- a/app/src/routes/Auth.tsx
+++ b/app/src/routes/Auth.tsx
@@ -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() {
diff --git a/app/src/routes/Generation.tsx b/app/src/routes/Generation.tsx
index 4ed75a1..08ed3e3 100644
--- a/app/src/routes/Generation.tsx
+++ b/app/src/routes/Generation.tsx
@@ -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;
diff --git a/app/src/routes/Home.tsx b/app/src/routes/Home.tsx
index 3ec682f..f4f1908 100644
--- a/app/src/routes/Home.tsx
+++ b/app/src/routes/Home.tsx
@@ -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 (
diff --git a/app/src/routes/NotFound.tsx b/app/src/routes/NotFound.tsx
index 32f523f..a0c360e 100644
--- a/app/src/routes/NotFound.tsx
+++ b/app/src/routes/NotFound.tsx
@@ -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() {
diff --git a/app/src/routes/Sharing.tsx b/app/src/routes/Sharing.tsx
index af15125..eba8d08 100644
--- a/app/src/routes/Sharing.tsx
+++ b/app/src/routes/Sharing.tsx
@@ -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;
diff --git a/app/src/store/api.ts b/app/src/store/api.ts
index fc0984e..9fc5d83 100644
--- a/app/src/store/api.ts
+++ b/app/src/store/api.ts
@@ -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({
diff --git a/app/src/store/auth.ts b/app/src/store/auth.ts
index 94941fa..c86b160 100644
--- a/app/src/store/auth.ts
+++ b/app/src/store/auth.ts
@@ -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({
diff --git a/app/src/store/chat.ts b/app/src/store/chat.ts
index 8e34e53..1fb3a0a 100644
--- a/app/src/store/chat.ts
+++ b/app/src/store/chat.ts
@@ -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[];
diff --git a/app/src/store/menu.ts b/app/src/store/menu.ts
index ca61393..9f3aac4 100644
--- a/app/src/store/menu.ts
+++ b/app/src/store/menu.ts
@@ -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",
diff --git a/app/src/store/package.ts b/app/src/store/package.ts
index 3c24331..3a0b71f 100644
--- a/app/src/store/package.ts
+++ b/app/src/store/package.ts
@@ -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({
diff --git a/app/src/store/sharing.ts b/app/src/store/sharing.ts
index 4810e90..85d6a41 100644
--- a/app/src/store/sharing.ts
+++ b/app/src/store/sharing.ts
@@ -4,7 +4,7 @@ import {
deleteSharing,
listSharing,
SharingPreviewForm,
-} from "../conversation/sharing.ts";
+} from "@/conversation/sharing.ts";
export const sharingSlice = createSlice({
name: "sharing",
diff --git a/app/src/store/subscription.ts b/app/src/store/subscription.ts
index b5b8b85..6630c90 100644
--- a/app/src/store/subscription.ts
+++ b/app/src/store/subscription.ts
@@ -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({
diff --git a/app/src/utils.ts b/app/src/utils.ts
deleted file mode 100644
index 653e6d4..0000000
--- a/app/src/utils.ts
+++ /dev/null
@@ -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(effect: () => Promise, deps?: any[]) {
- return useEffect(() => {
- effect().catch((err) =>
- console.debug("[runtime] error during use effect", err),
- );
- }, deps);
-}
-
-export function useAnimation(
- ref: React.MutableRefObject,
- 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(): {
- hook: (v: T) => void;
- useHook: () => Promise;
-} {
- let value: T | undefined = undefined;
- return {
- hook: (v: T) => {
- value = v;
- },
- useHook: () => {
- return new Promise((resolve) => {
- if (value) return resolve(value);
- const interval = setInterval(() => {
- if (value) {
- clearInterval(interval);
- resolve(value);
- }
- }, 50);
- });
- },
- };
-}
-
-export function insert(arr: T[], idx: number, value: T): T[] {
- return [...arr.slice(0, idx), value, ...arr.slice(idx)];
-}
-
-export function insertStart(arr: T[], value: T): T[] {
- return [value, ...arr];
-}
-
-export function remove(arr: T[], idx: number): T[] {
- return [...arr.slice(0, idx), ...arr.slice(idx + 1)];
-}
-
-export function replace(arr: T[], idx: number, value: T): T[] {
- return [...arr.slice(0, idx), value, ...arr.slice(idx + 1)];
-}
-
-export function move(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 = {};
- 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);
-}
diff --git a/app/src/utils/app.ts b/app/src/utils/app.ts
new file mode 100644
index 0000000..91e882d
--- /dev/null
+++ b/app/src/utils/app.ts
@@ -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;
+}
diff --git a/app/src/utils/base.ts b/app/src/utils/base.ts
new file mode 100644
index 0000000..27ea95e
--- /dev/null
+++ b/app/src/utils/base.ts
@@ -0,0 +1,20 @@
+export function insert(arr: T[], idx: number, value: T): T[] {
+ return [...arr.slice(0, idx), value, ...arr.slice(idx)];
+}
+
+export function insertStart(arr: T[], value: T): T[] {
+ return [value, ...arr];
+}
+
+export function remove(arr: T[], idx: number): T[] {
+ return [...arr.slice(0, idx), ...arr.slice(idx + 1)];
+}
+
+export function replace(arr: T[], idx: number, value: T): T[] {
+ return [...arr.slice(0, idx), value, ...arr.slice(idx + 1)];
+}
+
+export function move(arr: T[], from: number, to: number): T[] {
+ const value = arr[from];
+ return insert(remove(arr, from), to, value);
+}
diff --git a/app/src/utils/device.ts b/app/src/utils/device.ts
new file mode 100644
index 0000000..da21e16
--- /dev/null
+++ b/app/src/utils/device.ts
@@ -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")
+ );
+}
diff --git a/app/src/utils/dom.ts b/app/src/utils/dom.ts
new file mode 100644
index 0000000..df6478d
--- /dev/null
+++ b/app/src/utils/dom.ts
@@ -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();
+}
diff --git a/app/src/utils/hook.ts b/app/src/utils/hook.ts
new file mode 100644
index 0000000..1587a56
--- /dev/null
+++ b/app/src/utils/hook.ts
@@ -0,0 +1,79 @@
+import React, { useEffect } from "react";
+
+export function useEffectAsync(effect: () => Promise, 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,
+ 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(): {
+ hook: (v: T) => void;
+ useHook: () => Promise;
+} {
+ /**
+ * Share value between components, useful for sharing data between components / redux dispatches
+ *
+ * @example
+ *
+ * const dispatch = useDispatch();
+ * const { hook, useHook } = useShared();
+ *
+ * dispatch(updateMigration({ hook }));
+ * const response = await useHook();
+ */
+ let value: T | undefined = undefined;
+ return {
+ hook: (v: T) => {
+ value = v;
+ },
+ useHook: () => {
+ return new Promise((resolve) => {
+ if (value) return resolve(value);
+ const interval = setInterval(() => {
+ if (value) {
+ clearInterval(interval);
+ resolve(value);
+ }
+ }, 50);
+ });
+ },
+ };
+}
diff --git a/app/src/utils/path.ts b/app/src/utils/path.ts
new file mode 100644
index 0000000..faf1762
--- /dev/null
+++ b/app/src/utils/path.ts
@@ -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 = {};
+ 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) || "";
+}
diff --git a/app/src/utils/processor.ts b/app/src/utils/processor.ts
new file mode 100644
index 0000000..5979744
--- /dev/null
+++ b/app/src/utils/processor.ts
@@ -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);
+}
diff --git a/app/tsconfig.json b/app/tsconfig.json
index 3b2d923..96454fe 100644
--- a/app/tsconfig.json
+++ b/app/tsconfig.json
@@ -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" }],
}