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" }], }