feat: chat panel redesigned ui
|
@ -0,0 +1,8 @@
|
||||||
|
.loading-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
import BotIcon from "@/app/icons/bot.svg";
|
||||||
|
import LoadingIcon from "@/app/icons/three-dots.svg";
|
||||||
|
|
||||||
|
import styles from "./index.module.scss";
|
||||||
|
|
||||||
|
export default function Loading(props: { noLogo?: boolean }) {
|
||||||
|
return (
|
||||||
|
<div className={styles["loading-content"] + " no-dark"}>
|
||||||
|
{!props.noLogo && <BotIcon />}
|
||||||
|
<LoadingIcon />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,119 @@
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export default function Popover(props: {
|
||||||
|
content?: JSX.Element | string;
|
||||||
|
children?: JSX.Element;
|
||||||
|
show?: boolean;
|
||||||
|
onShow?: (v: boolean) => void;
|
||||||
|
className?: string;
|
||||||
|
popoverClassName?: string;
|
||||||
|
trigger?: "hover" | "click";
|
||||||
|
placement?: "t" | "lt" | "rt" | "lb" | "rb" | "b";
|
||||||
|
noArrow?: boolean;
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
content,
|
||||||
|
children,
|
||||||
|
show,
|
||||||
|
onShow,
|
||||||
|
className,
|
||||||
|
popoverClassName,
|
||||||
|
trigger = "hover",
|
||||||
|
placement = "t",
|
||||||
|
noArrow = false,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const [internalShow, setShow] = useState(false);
|
||||||
|
|
||||||
|
const mergedShow = show ?? internalShow;
|
||||||
|
|
||||||
|
let placementClassName;
|
||||||
|
let arrowClassName =
|
||||||
|
"rotate-45 w-[8.5px] h-[8.5px] left-[50%] translate-x-[calc(-50%)] bg-black rounded-[1px] ";
|
||||||
|
|
||||||
|
switch (placement) {
|
||||||
|
case "b":
|
||||||
|
placementClassName =
|
||||||
|
"bottom-[calc(-100%-0.5rem)] left-[50%] translate-x-[calc(-50%)]";
|
||||||
|
arrowClassName += "bottom-[-5px] ";
|
||||||
|
break;
|
||||||
|
// case 'l':
|
||||||
|
// placementClassName = '';
|
||||||
|
// break;
|
||||||
|
// case 'r':
|
||||||
|
// placementClassName = '';
|
||||||
|
// break;
|
||||||
|
case "rb":
|
||||||
|
placementClassName = "bottom-[calc(-100%-0.5rem)]";
|
||||||
|
arrowClassName += "bottom-[-5px] ";
|
||||||
|
break;
|
||||||
|
case "lt":
|
||||||
|
placementClassName =
|
||||||
|
"top-[calc(-100%-0.5rem)] left-[100%] translate-x-[calc(-100%)]";
|
||||||
|
arrowClassName += "top-[-5px] ";
|
||||||
|
break;
|
||||||
|
case "lb":
|
||||||
|
placementClassName =
|
||||||
|
"bottom-[calc(-100%-0.5rem)] left-[100%] translate-x-[calc(-100%)]";
|
||||||
|
arrowClassName += "bottom-[-5px] ";
|
||||||
|
break;
|
||||||
|
case "rt":
|
||||||
|
placementClassName = "top-[calc(-100%-0.5rem)]";
|
||||||
|
arrowClassName += "top-[-5px] ";
|
||||||
|
break;
|
||||||
|
case "t":
|
||||||
|
default:
|
||||||
|
placementClassName =
|
||||||
|
"top-[calc(-100%-0.5rem)] left-[50%] translate-x-[calc(-50%)]";
|
||||||
|
arrowClassName += "top-[-5px] ";
|
||||||
|
}
|
||||||
|
|
||||||
|
const popoverCommonClass = "absolute p-2 box-border";
|
||||||
|
|
||||||
|
if (noArrow) {
|
||||||
|
arrowClassName = "hidden";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trigger === "click") {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`relative ${className}`}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onShow?.(!mergedShow);
|
||||||
|
setShow(!mergedShow);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{mergedShow && (
|
||||||
|
<>
|
||||||
|
{!noArrow && (
|
||||||
|
<div className={`absolute ${arrowClassName}`}> </div>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className={`${popoverCommonClass} ${placementClassName} ${popoverClassName}`}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`group relative ${className}`}>
|
||||||
|
{children}
|
||||||
|
{!noArrow && (
|
||||||
|
<div className={`hidden group-hover:block absolute ${arrowClassName}`}>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className={`hidden group-hover:block ${popoverCommonClass} ${placementClassName} ${popoverClassName}`}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
import { useLocation } from "react-router-dom";
|
||||||
|
import { useMemo, ReactNode, useLayoutEffect } from "react";
|
||||||
|
import { DEFAULT_SIDEBAR_WIDTH, Path, SlotID } from "@/app/constant";
|
||||||
|
import { getLang } from "@/app/locales";
|
||||||
|
|
||||||
|
import useMobileScreen from "@/app/hooks/useMobileScreen";
|
||||||
|
import { isIOS } from "@/app/utils";
|
||||||
|
|
||||||
|
import backgroundUrl from "!url-loader!@/app/icons/background.svg";
|
||||||
|
import useListenWinResize from "@/app/hooks/useListenWinResize";
|
||||||
|
|
||||||
|
interface ScreenProps {
|
||||||
|
children: ReactNode;
|
||||||
|
noAuth: ReactNode;
|
||||||
|
sidebar: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Screen(props: ScreenProps) {
|
||||||
|
const location = useLocation();
|
||||||
|
const isAuth = location.pathname === Path.Auth;
|
||||||
|
const isHome = location.pathname === Path.Home;
|
||||||
|
|
||||||
|
const isMobileScreen = useMobileScreen();
|
||||||
|
const isIOSMobile = useMemo(
|
||||||
|
() => isIOS() && isMobileScreen,
|
||||||
|
[isMobileScreen],
|
||||||
|
);
|
||||||
|
|
||||||
|
useListenWinResize();
|
||||||
|
|
||||||
|
let containerClassName = "flex h-[100%] w-[100%]";
|
||||||
|
let pageClassName = "flex-1 h-[100%]";
|
||||||
|
let sidebarClassName = "basis-sidebar h-[100%]";
|
||||||
|
|
||||||
|
if (isMobileScreen) {
|
||||||
|
containerClassName = "h-[100%] w-[100%] relative bg-center";
|
||||||
|
pageClassName = `absolute top-0 h-[100%] w-[100%] ${
|
||||||
|
!isHome ? "left-0" : "left-[101%]"
|
||||||
|
} z-10`;
|
||||||
|
sidebarClassName = `h-[100%] w-[100%]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={containerClassName}
|
||||||
|
style={{
|
||||||
|
backgroundImage: `url(${backgroundUrl})`,
|
||||||
|
direction: getLang() === "ar" ? "rtl" : "ltr",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isAuth ? (
|
||||||
|
props.noAuth
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className={sidebarClassName}>{props.sidebar}</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={pageClassName}
|
||||||
|
id={SlotID.AppBody}
|
||||||
|
style={{
|
||||||
|
// #3016 disable transition on ios mobile screen
|
||||||
|
transition: isMobileScreen && isIOSMobile ? "none" : undefined,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,70 +0,0 @@
|
||||||
import { isValidElement } from "react";
|
|
||||||
|
|
||||||
type IconMap = {
|
|
||||||
active: JSX.Element;
|
|
||||||
inactive: JSX.Element;
|
|
||||||
};
|
|
||||||
interface Action {
|
|
||||||
id: string;
|
|
||||||
icons: JSX.Element | IconMap;
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TabActionsProps {
|
|
||||||
actionsShema: Action[];
|
|
||||||
onSelect: (id: string) => void;
|
|
||||||
selected: string;
|
|
||||||
groups: string[][];
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function TabActions(props: TabActionsProps) {
|
|
||||||
const { actionsShema, onSelect, selected, groups, className } = props;
|
|
||||||
|
|
||||||
const content = groups.reduce((res, group, ind, arr) => {
|
|
||||||
res.push(
|
|
||||||
...group.map((i) => {
|
|
||||||
const action = actionsShema.find((a) => a.id === i);
|
|
||||||
if (!action) {
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { icons } = action;
|
|
||||||
let activeIcon, inactiveIcon;
|
|
||||||
|
|
||||||
if (isValidElement(icons)) {
|
|
||||||
activeIcon = icons;
|
|
||||||
inactiveIcon = icons;
|
|
||||||
} else {
|
|
||||||
activeIcon = (icons as IconMap).active;
|
|
||||||
inactiveIcon = (icons as IconMap).inactive;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={action.id}
|
|
||||||
className={` ${
|
|
||||||
selected === action.id ? "bg-blue-900" : "bg-transparent"
|
|
||||||
} p-3 rounded-md items-center ${action.className}`}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (selected !== action.id) {
|
|
||||||
onSelect?.(action.id);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{selected === action.id ? activeIcon : inactiveIcon}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
if (ind < arr.length - 1) {
|
|
||||||
res.push(<div className=" flex-1"></div>);
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
}, [] as JSX.Element[]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`flex flex-col items-center ${className}`}>{content}</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -30,7 +30,6 @@ import { getClientConfig } from "../config/client";
|
||||||
import { ClientApi } from "../client/api";
|
import { ClientApi } from "../client/api";
|
||||||
import { useAccessStore } from "../store";
|
import { useAccessStore } from "../store";
|
||||||
import { identifyDefaultClaudeModel } from "../utils/checkers";
|
import { identifyDefaultClaudeModel } from "../utils/checkers";
|
||||||
import useMobileScreen from "@/app/hooks/useMobileScreen";
|
|
||||||
import backgroundUrl from "!url-loader!@/app/icons/background.svg";
|
import backgroundUrl from "!url-loader!@/app/icons/background.svg";
|
||||||
|
|
||||||
export function Loading(props: { noLogo?: boolean }) {
|
export function Loading(props: { noLogo?: boolean }) {
|
||||||
|
@ -126,11 +125,9 @@ const loadAsyncGoogleFont = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
function Screen() {
|
function Screen() {
|
||||||
const config = useAppConfig();
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const isHome = location.pathname === Path.Home;
|
const isHome = location.pathname === Path.Home;
|
||||||
const isAuth = location.pathname === Path.Auth;
|
const isAuth = location.pathname === Path.Auth;
|
||||||
const isMobileScreen = useMobileScreen();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadAsyncGoogleFont();
|
loadAsyncGoogleFont();
|
||||||
|
@ -155,7 +152,7 @@ function Screen() {
|
||||||
<SideBar className={isHome ? styles["sidebar-show"] : ""} />
|
<SideBar className={isHome ? styles["sidebar-show"] : ""} />
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={`flex flex-col h-[100%] w-[--window-content-width`}
|
className={`flex flex-col h-[100%] w-[--window-content-width]`}
|
||||||
id={SlotID.AppBody}
|
id={SlotID.AppBody}
|
||||||
>
|
>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
|
|
|
@ -177,13 +177,14 @@ export function Markdown(
|
||||||
fontSize?: number;
|
fontSize?: number;
|
||||||
parentRef?: RefObject<HTMLDivElement>;
|
parentRef?: RefObject<HTMLDivElement>;
|
||||||
defaultShow?: boolean;
|
defaultShow?: boolean;
|
||||||
|
className?: string;
|
||||||
} & React.DOMAttributes<HTMLDivElement>,
|
} & React.DOMAttributes<HTMLDivElement>,
|
||||||
) {
|
) {
|
||||||
const mdRef = useRef<HTMLDivElement>(null);
|
const mdRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="markdown-body"
|
className={`markdown-body ${props.className}`}
|
||||||
style={{
|
style={{
|
||||||
fontSize: `${props.fontSize ?? 14}px`,
|
fontSize: `${props.fontSize ?? 14}px`,
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -49,10 +49,15 @@ export enum StoreKey {
|
||||||
Sync = "sync",
|
Sync = "sync",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_SIDEBAR_WIDTH = 300;
|
export const DEFAULT_SIDEBAR_WIDTH = 404;
|
||||||
export const MAX_SIDEBAR_WIDTH = 500;
|
export const MAX_SIDEBAR_WIDTH = 504;
|
||||||
export const MIN_SIDEBAR_WIDTH = 230;
|
export const MIN_SIDEBAR_WIDTH = 294;
|
||||||
export const NARROW_SIDEBAR_WIDTH = 100;
|
|
||||||
|
export const WINDOW_WIDTH_SM = 480;
|
||||||
|
export const WINDOW_WIDTH_MD = 768;
|
||||||
|
export const WINDOW_WIDTH_LG = 1120;
|
||||||
|
export const WINDOW_WIDTH_XL = 1440;
|
||||||
|
export const WINDOW_WIDTH_2XL = 1980;
|
||||||
|
|
||||||
export const ACCESS_CODE_PREFIX = "nk-";
|
export const ACCESS_CODE_PREFIX = "nk-";
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { ChatControllerPool } from "@/app/client/controller";
|
||||||
import { useAllModels } from "@/app/utils/hooks";
|
import { useAllModels } from "@/app/utils/hooks";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { isVisionModel } from "@/app/utils";
|
import { isVisionModel } from "@/app/utils";
|
||||||
import { Selector, showToast } from "@/app/components/ui-lib";
|
import { showToast } from "@/app/components/ui-lib";
|
||||||
import Locale from "@/app/locales";
|
import Locale from "@/app/locales";
|
||||||
import { Path } from "@/app/constant";
|
import { Path } from "@/app/constant";
|
||||||
|
|
||||||
|
@ -22,10 +22,12 @@ import MaskIcon from "@/app/icons/mask.svg";
|
||||||
import BreakIcon from "@/app/icons/break.svg";
|
import BreakIcon from "@/app/icons/break.svg";
|
||||||
import SettingsIcon from "@/app/icons/chat-settings.svg";
|
import SettingsIcon from "@/app/icons/chat-settings.svg";
|
||||||
import ImageIcon from "@/app/icons/image.svg";
|
import ImageIcon from "@/app/icons/image.svg";
|
||||||
|
import AddCircleIcon from "@/app/icons/addCircle.svg";
|
||||||
|
|
||||||
import ChatAction from "./ChatAction";
|
import ChatAction from "./ChatAction";
|
||||||
|
|
||||||
import styles from "./index.module.scss";
|
import styles from "./index.module.scss";
|
||||||
|
import Popover from "@/app/components/Popover";
|
||||||
|
|
||||||
export function ChatActions(props: {
|
export function ChatActions(props: {
|
||||||
uploadImage: () => void;
|
uploadImage: () => void;
|
||||||
|
@ -34,8 +36,11 @@ export function ChatActions(props: {
|
||||||
showPromptModal: () => void;
|
showPromptModal: () => void;
|
||||||
scrollToBottom: () => void;
|
scrollToBottom: () => void;
|
||||||
showPromptHints: () => void;
|
showPromptHints: () => void;
|
||||||
|
showModelSelector: (show: boolean) => void;
|
||||||
hitBottom: boolean;
|
hitBottom: boolean;
|
||||||
uploading: boolean;
|
uploading: boolean;
|
||||||
|
isMobileScreen: boolean;
|
||||||
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
const config = useAppConfig();
|
const config = useAppConfig();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
@ -62,7 +67,6 @@ export function ChatActions(props: {
|
||||||
() => allModels.filter((m) => m.available),
|
() => allModels.filter((m) => m.available),
|
||||||
[allModels],
|
[allModels],
|
||||||
);
|
);
|
||||||
const [showModelSelector, setShowModelSelector] = useState(false);
|
|
||||||
const [showUploadImage, setShowUploadImage] = useState(false);
|
const [showUploadImage, setShowUploadImage] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -85,106 +89,159 @@ export function ChatActions(props: {
|
||||||
}
|
}
|
||||||
}, [chatStore, currentModel, models]);
|
}, [chatStore, currentModel, models]);
|
||||||
|
|
||||||
|
const actions = [
|
||||||
|
{
|
||||||
|
onClick: stopAll,
|
||||||
|
text: Locale.Chat.InputActions.Stop,
|
||||||
|
isShow: couldStop,
|
||||||
|
icon: <StopIcon />,
|
||||||
|
placement: "left",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onClick: props.scrollToBottom,
|
||||||
|
text: Locale.Chat.InputActions.ToBottom,
|
||||||
|
isShow: !props.hitBottom,
|
||||||
|
icon: <BottomIcon />,
|
||||||
|
placement: "left",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onClick: props.showPromptModal,
|
||||||
|
text: Locale.Chat.InputActions.Settings,
|
||||||
|
isShow: props.hitBottom,
|
||||||
|
icon: <SettingsIcon />,
|
||||||
|
placement: "right",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onClick: props.uploadImage,
|
||||||
|
text: Locale.Chat.InputActions.UploadImage,
|
||||||
|
isShow: showUploadImage,
|
||||||
|
icon: props.uploading ? <LoadingButtonIcon /> : <ImageIcon />,
|
||||||
|
placement: "left",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onClick: nextTheme,
|
||||||
|
text: Locale.Chat.InputActions.Theme[theme],
|
||||||
|
isShow: true,
|
||||||
|
icon: (
|
||||||
|
<>
|
||||||
|
{theme === Theme.Auto ? (
|
||||||
|
<AutoIcon />
|
||||||
|
) : theme === Theme.Light ? (
|
||||||
|
<LightIcon />
|
||||||
|
) : theme === Theme.Dark ? (
|
||||||
|
<DarkIcon />
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
placement: "left",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onClick: props.showPromptHints,
|
||||||
|
text: Locale.Chat.InputActions.Prompt,
|
||||||
|
isShow: true,
|
||||||
|
icon: <PromptIcon />,
|
||||||
|
placement: "left",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onClick: () => {
|
||||||
|
navigate(Path.Masks);
|
||||||
|
},
|
||||||
|
text: Locale.Chat.InputActions.Masks,
|
||||||
|
isShow: true,
|
||||||
|
icon: <MaskIcon />,
|
||||||
|
placement: "left",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onClick: () => {
|
||||||
|
chatStore.updateCurrentSession((session) => {
|
||||||
|
if (session.clearContextIndex === session.messages.length) {
|
||||||
|
session.clearContextIndex = undefined;
|
||||||
|
} else {
|
||||||
|
session.clearContextIndex = session.messages.length;
|
||||||
|
session.memoryPrompt = ""; // will clear memory
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
text: Locale.Chat.InputActions.Clear,
|
||||||
|
isShow: true,
|
||||||
|
icon: <BreakIcon />,
|
||||||
|
placement: "right",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onClick: () => props.showModelSelector(true),
|
||||||
|
text: currentModel,
|
||||||
|
isShow: true,
|
||||||
|
icon: <RobotIcon />,
|
||||||
|
placement: "left",
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
if (props.isMobileScreen) {
|
||||||
|
const content = (
|
||||||
|
<div>
|
||||||
|
{actions.map((act) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={act.text}
|
||||||
|
className={`flex p-3 bg-white hover:bg-select-btn rounded-action-btn`}
|
||||||
|
>
|
||||||
|
{act.icon}
|
||||||
|
{act.text}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<Popover content={content}>
|
||||||
|
<AddCircleIcon />
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const popoverClassName = `bg-gray-800 whitespace-nowrap px-3 py-2.5 text-white text-sm-title rounded-md`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles["chat-input-actions"]}>
|
<div className={`flex gap-2 item-center ${props.className}`}>
|
||||||
{couldStop && (
|
{actions
|
||||||
<ChatAction
|
.filter((v) => v.placement === "left" && v.isShow)
|
||||||
onClick={stopAll}
|
.map((act, ind) => {
|
||||||
text={Locale.Chat.InputActions.Stop}
|
return (
|
||||||
icon={<StopIcon />}
|
<Popover
|
||||||
/>
|
key={act.text}
|
||||||
)}
|
content={act.text}
|
||||||
{!props.hitBottom && (
|
popoverClassName={`${popoverClassName}`}
|
||||||
<ChatAction
|
placement={ind ? "t" : "rt"}
|
||||||
onClick={props.scrollToBottom}
|
>
|
||||||
text={Locale.Chat.InputActions.ToBottom}
|
<div
|
||||||
icon={<BottomIcon />}
|
className="h-[32px] w-[32px] flex items-center justify-center"
|
||||||
/>
|
onClick={act.onClick}
|
||||||
)}
|
>
|
||||||
{props.hitBottom && (
|
{act.icon}
|
||||||
<ChatAction
|
</div>
|
||||||
onClick={props.showPromptModal}
|
</Popover>
|
||||||
text={Locale.Chat.InputActions.Settings}
|
);
|
||||||
icon={<SettingsIcon />}
|
})}
|
||||||
/>
|
<div className="flex-1"></div>
|
||||||
)}
|
{actions
|
||||||
|
.filter((v) => v.placement === "right" && v.isShow)
|
||||||
{showUploadImage && (
|
.map((act, ind, arr) => {
|
||||||
<ChatAction
|
return (
|
||||||
onClick={props.uploadImage}
|
<Popover
|
||||||
text={Locale.Chat.InputActions.UploadImage}
|
key={act.text}
|
||||||
icon={props.uploading ? <LoadingButtonIcon /> : <ImageIcon />}
|
content={act.text}
|
||||||
/>
|
popoverClassName={`${popoverClassName}`}
|
||||||
)}
|
placement={ind === arr.length - 1 ? "lt" : "t"}
|
||||||
<ChatAction
|
>
|
||||||
onClick={nextTheme}
|
<div
|
||||||
text={Locale.Chat.InputActions.Theme[theme]}
|
className="h-[32px] w-[32px] flex items-center justify-center"
|
||||||
icon={
|
onClick={act.onClick}
|
||||||
<>
|
>
|
||||||
{theme === Theme.Auto ? (
|
{act.icon}
|
||||||
<AutoIcon />
|
</div>
|
||||||
) : theme === Theme.Light ? (
|
</Popover>
|
||||||
<LightIcon />
|
);
|
||||||
) : theme === Theme.Dark ? (
|
})}
|
||||||
<DarkIcon />
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ChatAction
|
|
||||||
onClick={props.showPromptHints}
|
|
||||||
text={Locale.Chat.InputActions.Prompt}
|
|
||||||
icon={<PromptIcon />}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ChatAction
|
|
||||||
onClick={() => {
|
|
||||||
navigate(Path.Masks);
|
|
||||||
}}
|
|
||||||
text={Locale.Chat.InputActions.Masks}
|
|
||||||
icon={<MaskIcon />}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ChatAction
|
|
||||||
text={Locale.Chat.InputActions.Clear}
|
|
||||||
icon={<BreakIcon />}
|
|
||||||
onClick={() => {
|
|
||||||
chatStore.updateCurrentSession((session) => {
|
|
||||||
if (session.clearContextIndex === session.messages.length) {
|
|
||||||
session.clearContextIndex = undefined;
|
|
||||||
} else {
|
|
||||||
session.clearContextIndex = session.messages.length;
|
|
||||||
session.memoryPrompt = ""; // will clear memory
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ChatAction
|
|
||||||
onClick={() => setShowModelSelector(true)}
|
|
||||||
text={currentModel}
|
|
||||||
icon={<RobotIcon />}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{showModelSelector && (
|
|
||||||
<Selector
|
|
||||||
defaultSelectedValue={currentModel}
|
|
||||||
items={models.map((m) => ({
|
|
||||||
title: m.displayName,
|
|
||||||
value: m.name,
|
|
||||||
}))}
|
|
||||||
onClose={() => setShowModelSelector(false)}
|
|
||||||
onSelection={(s) => {
|
|
||||||
if (s.length === 0) return;
|
|
||||||
chatStore.updateCurrentSession((session) => {
|
|
||||||
session.mask.modelConfig.model = s[0] as ModelType;
|
|
||||||
session.mask.syncGlobalConfig = false;
|
|
||||||
});
|
|
||||||
showToast(s[0]);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,75 +1,88 @@
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { IconButton } from "@/app/components/button";
|
|
||||||
import Locale from "@/app/locales";
|
import Locale from "@/app/locales";
|
||||||
import { Path } from "@/app/constant";
|
import { Path } from "@/app/constant";
|
||||||
import { DEFAULT_TOPIC, useChatStore } from "@/app/store/chat";
|
import { DEFAULT_TOPIC, useChatStore } from "@/app/store/chat";
|
||||||
|
|
||||||
import RenameIcon from "@/app/icons/rename.svg";
|
import LogIcon from "@/app/icons/logIcon.svg";
|
||||||
import ExportIcon from "@/app/icons/share.svg";
|
import GobackIcon from "@/app/icons/goback.svg";
|
||||||
import ReturnIcon from "@/app/icons/return.svg";
|
import ShareIcon from "@/app/icons/shareIcon.svg";
|
||||||
|
import BottomArrow from "@/app/icons/bottomArrow.svg";
|
||||||
import styles from "./index.module.scss";
|
|
||||||
|
|
||||||
export interface ChatHeaderProps {
|
export interface ChatHeaderProps {
|
||||||
isMobileScreen: boolean;
|
isMobileScreen: boolean;
|
||||||
setIsEditingMessage: (v: boolean) => void;
|
setIsEditingMessage: (v: boolean) => void;
|
||||||
setShowExport: (v: boolean) => void;
|
setShowExport: (v: boolean) => void;
|
||||||
|
showModelSelector: (v: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ChatHeader(props: ChatHeaderProps) {
|
export default function ChatHeader(props: ChatHeaderProps) {
|
||||||
const { isMobileScreen, setIsEditingMessage, setShowExport } = props;
|
const {
|
||||||
|
isMobileScreen,
|
||||||
|
setIsEditingMessage,
|
||||||
|
setShowExport,
|
||||||
|
showModelSelector,
|
||||||
|
} = props;
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const chatStore = useChatStore();
|
const chatStore = useChatStore();
|
||||||
const session = chatStore.currentSession();
|
const session = chatStore.currentSession();
|
||||||
|
|
||||||
|
const currentModel = chatStore.currentSession().mask.modelConfig.model;
|
||||||
|
|
||||||
|
let containerClassName = "";
|
||||||
|
let titleClassName = "mr-4";
|
||||||
|
let mainTitleClassName = "";
|
||||||
|
let subTitleClassName = "";
|
||||||
|
|
||||||
|
if (isMobileScreen) {
|
||||||
|
containerClassName = "h-menu-title-mobile";
|
||||||
|
titleClassName = "flex flex-col items-center justify-center gap-0.5 text";
|
||||||
|
mainTitleClassName = "text-sm-title h-[19px] leading-5";
|
||||||
|
subTitleClassName = "text-sm-mobile-tab leading-4";
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="window-header" data-tauri-drag-region>
|
<div
|
||||||
{isMobileScreen && (
|
className={`flex flex-0 justify-between items-center px-6 py-4 gap-chat-header-gap border-b-[1px] border-gray-200 ${containerClassName}`}
|
||||||
<div className="window-actions">
|
data-tauri-drag-region
|
||||||
<div className={"window-action-button"}>
|
>
|
||||||
<IconButton
|
{isMobileScreen ? (
|
||||||
icon={<ReturnIcon />}
|
<div onClick={() => navigate(Path.Home)}>
|
||||||
bordered
|
<GobackIcon />
|
||||||
title={Locale.Chat.Actions.ChatList}
|
|
||||||
onClick={() => navigate(Path.Home)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<LogIcon />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={`window-header-title ${styles["chat-body-title"]}`}>
|
<div className={`flex-1 ${titleClassName}`}>
|
||||||
<div
|
<div
|
||||||
className={`window-header-main-title ${styles["chat-body-main-title"]}`}
|
className={`line-clamp-1 cursor-pointer text-black text-chat-header-title font-common ${mainTitleClassName}`}
|
||||||
onClickCapture={() => setIsEditingMessage(true)}
|
onClickCapture={() => setIsEditingMessage(true)}
|
||||||
>
|
>
|
||||||
{!session.topic ? DEFAULT_TOPIC : session.topic}
|
{!session.topic ? DEFAULT_TOPIC : session.topic}
|
||||||
</div>
|
</div>
|
||||||
<div className="window-header-sub-title">
|
<div className={`text-gray-500 text-sm ${subTitleClassName}`}>
|
||||||
{Locale.Chat.SubTitle(session.messages.length)}
|
{isMobileScreen ? (
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-1"
|
||||||
|
onClick={() => showModelSelector(true)}
|
||||||
|
>
|
||||||
|
{currentModel}
|
||||||
|
<BottomArrow />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
Locale.Chat.SubTitle(session.messages.length)
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="window-actions">
|
|
||||||
{!isMobileScreen && (
|
<div
|
||||||
<div className="window-action-button">
|
onClick={() => {
|
||||||
<IconButton
|
setShowExport(true);
|
||||||
icon={<RenameIcon />}
|
}}
|
||||||
bordered
|
>
|
||||||
onClick={() => setIsEditingMessage(true)}
|
<ShareIcon />
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="window-action-button">
|
|
||||||
<IconButton
|
|
||||||
icon={<ExportIcon />}
|
|
||||||
bordered
|
|
||||||
title={Locale.Chat.Actions.Export}
|
|
||||||
onClick={() => {
|
|
||||||
setShowExport(true);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -36,6 +36,7 @@ export interface ChatInputPanelProps {
|
||||||
setIsLoading: (value: boolean) => void;
|
setIsLoading: (value: boolean) => void;
|
||||||
setShowPromptModal: (value: boolean) => void;
|
setShowPromptModal: (value: boolean) => void;
|
||||||
_setMsgRenderIndex: (value: number) => void;
|
_setMsgRenderIndex: (value: number) => void;
|
||||||
|
showModelSelector: (value: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ChatInputPanelInstance {
|
export interface ChatInputPanelInstance {
|
||||||
|
@ -72,6 +73,7 @@ export default forwardRef<ChatInputPanelInstance, ChatInputPanelProps>(
|
||||||
_setMsgRenderIndex,
|
_setMsgRenderIndex,
|
||||||
hitBottom,
|
hitBottom,
|
||||||
inputRows,
|
inputRows,
|
||||||
|
showModelSelector,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const [uploading, setUploading] = useState(false);
|
const [uploading, setUploading] = useState(false);
|
||||||
|
@ -222,86 +224,107 @@ export default forwardRef<ChatInputPanelInstance, ChatInputPanelProps>(
|
||||||
setUploading,
|
setUploading,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let inputClassName = " flex flex-col px-5 pb-5";
|
||||||
|
let actionsClassName = "py-2.5";
|
||||||
|
let inputTextAreaClassName = "";
|
||||||
|
|
||||||
|
if (isMobileScreen) {
|
||||||
|
inputClassName = "flex flex-row-reverse items-center gap-2 p-3";
|
||||||
|
actionsClassName = "";
|
||||||
|
inputTextAreaClassName = "";
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles["chat-input-panel"]}>
|
<div
|
||||||
<PromptHints prompts={promptHints} onPromptSelect={onPromptSelect} />
|
className={`relative w-[100%] box-border border-gray-200 border-t-[1px]`}
|
||||||
|
>
|
||||||
<ChatActions
|
<PromptHints
|
||||||
uploadImage={uploadImage}
|
prompts={promptHints}
|
||||||
setAttachImages={setAttachImages}
|
onPromptSelect={onPromptSelect}
|
||||||
setUploading={setUploading}
|
className=""
|
||||||
showPromptModal={() => setShowPromptModal(true)}
|
|
||||||
scrollToBottom={scrollToBottom}
|
|
||||||
hitBottom={hitBottom}
|
|
||||||
uploading={uploading}
|
|
||||||
showPromptHints={() => {
|
|
||||||
// Click again to close
|
|
||||||
if (promptHints.length > 0) {
|
|
||||||
setPromptHints([]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
inputRef.current?.focus();
|
|
||||||
setUserInput("/");
|
|
||||||
onSearch("");
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<label
|
|
||||||
className={`${styles["chat-input-panel-inner"]} ${
|
<div className={`${inputClassName}`}>
|
||||||
attachImages.length != 0
|
<ChatActions
|
||||||
? styles["chat-input-panel-inner-attach"]
|
showModelSelector={showModelSelector}
|
||||||
: ""
|
uploadImage={uploadImage}
|
||||||
}`}
|
setAttachImages={setAttachImages}
|
||||||
htmlFor="chat-input"
|
setUploading={setUploading}
|
||||||
>
|
showPromptModal={() => setShowPromptModal(true)}
|
||||||
<textarea
|
scrollToBottom={scrollToBottom}
|
||||||
id="chat-input"
|
hitBottom={hitBottom}
|
||||||
ref={inputRef}
|
uploading={uploading}
|
||||||
className={styles["chat-input"]}
|
showPromptHints={() => {
|
||||||
placeholder={Locale.Chat.Input(submitKey)}
|
// Click again to close
|
||||||
onInput={(e) => onInput(e.currentTarget.value)}
|
if (promptHints.length > 0) {
|
||||||
value={userInput}
|
setPromptHints([]);
|
||||||
onKeyDown={onInputKeyDown}
|
return;
|
||||||
onFocus={scrollToBottom}
|
}
|
||||||
onClick={scrollToBottom}
|
|
||||||
onPaste={handlePaste}
|
inputRef.current?.focus();
|
||||||
rows={inputRows}
|
setUserInput("/");
|
||||||
autoFocus={autoFocus}
|
onSearch("");
|
||||||
style={{
|
|
||||||
fontSize: config.fontSize,
|
|
||||||
}}
|
}}
|
||||||
|
className={actionsClassName}
|
||||||
|
isMobileScreen={isMobileScreen}
|
||||||
/>
|
/>
|
||||||
{attachImages.length != 0 && (
|
<label
|
||||||
<div className={styles["attach-images"]}>
|
className={`${styles["chat-input-panel-inner"]} ${
|
||||||
{attachImages.map((image, index) => {
|
attachImages.length != 0
|
||||||
return (
|
? styles["chat-input-panel-inner-attach"]
|
||||||
<div
|
: ""
|
||||||
key={index}
|
} ${inputTextAreaClassName}`}
|
||||||
className={styles["attach-image"]}
|
htmlFor="chat-input"
|
||||||
style={{ backgroundImage: `url("${image}")` }}
|
>
|
||||||
>
|
<textarea
|
||||||
<div className={styles["attach-image-mask"]}>
|
id="chat-input"
|
||||||
<DeleteImageButton
|
ref={inputRef}
|
||||||
deleteImage={() => {
|
className={styles["chat-input"]}
|
||||||
setAttachImages(
|
placeholder={Locale.Chat.Input(submitKey)}
|
||||||
attachImages.filter((_, i) => i !== index),
|
onInput={(e) => onInput(e.currentTarget.value)}
|
||||||
);
|
value={userInput}
|
||||||
}}
|
onKeyDown={onInputKeyDown}
|
||||||
/>
|
onFocus={scrollToBottom}
|
||||||
|
onClick={scrollToBottom}
|
||||||
|
onPaste={handlePaste}
|
||||||
|
rows={inputRows}
|
||||||
|
autoFocus={autoFocus}
|
||||||
|
style={{
|
||||||
|
fontSize: config.fontSize,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{attachImages.length != 0 && (
|
||||||
|
<div className={styles["attach-images"]}>
|
||||||
|
{attachImages.map((image, index) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={styles["attach-image"]}
|
||||||
|
style={{ backgroundImage: `url("${image}")` }}
|
||||||
|
>
|
||||||
|
<div className={styles["attach-image-mask"]}>
|
||||||
|
<DeleteImageButton
|
||||||
|
deleteImage={() => {
|
||||||
|
setAttachImages(
|
||||||
|
attachImages.filter((_, i) => i !== index),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
})}
|
||||||
})}
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
<IconButton
|
||||||
<IconButton
|
icon={<SendWhiteIcon />}
|
||||||
icon={<SendWhiteIcon />}
|
text={Locale.Chat.Send}
|
||||||
text={Locale.Chat.Send}
|
className={styles["chat-input-send"]}
|
||||||
className={styles["chat-input-send"]}
|
type="primary"
|
||||||
type="primary"
|
onClick={() => doSubmit(userInput)}
|
||||||
onClick={() => doSubmit(userInput)}
|
/>
|
||||||
/>
|
</label>
|
||||||
</label>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -10,7 +10,6 @@ import {
|
||||||
getMessageTextContent,
|
getMessageTextContent,
|
||||||
selectOrCopy,
|
selectOrCopy,
|
||||||
} from "@/app/utils";
|
} from "@/app/utils";
|
||||||
import { IconButton } from "@/app/components/button";
|
|
||||||
import { showPrompt, showToast } from "@/app/components/ui-lib";
|
import { showPrompt, showToast } from "@/app/components/ui-lib";
|
||||||
|
|
||||||
import CopyIcon from "@/app/icons/copy.svg";
|
import CopyIcon from "@/app/icons/copy.svg";
|
||||||
|
@ -213,7 +212,7 @@ export default function ChatMessagePanel(props: ChatMessagePanelProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={styles["chat-body"]}
|
className={`relative flex-1 overscroll-y-none overflow-x-hidden px-3 pb-5`}
|
||||||
ref={scrollRef}
|
ref={scrollRef}
|
||||||
onScroll={(e) => onChatBodyScroll(e.currentTarget)}
|
onScroll={(e) => onChatBodyScroll(e.currentTarget)}
|
||||||
onMouseDown={() => inputRef.current?.blur()}
|
onMouseDown={() => inputRef.current?.blur()}
|
||||||
|
@ -229,119 +228,51 @@ export default function ChatMessagePanel(props: ChatMessagePanelProps) {
|
||||||
i > 0 &&
|
i > 0 &&
|
||||||
!(message.preview || message.content.length === 0) &&
|
!(message.preview || message.content.length === 0) &&
|
||||||
!isContext;
|
!isContext;
|
||||||
const showTyping = message.preview || message.streaming;
|
// const showTyping = message.preview || message.streaming;
|
||||||
|
|
||||||
const shouldShowClearContextDivider = i === clearContextIndex - 1;
|
const shouldShowClearContextDivider = i === clearContextIndex - 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment key={message.id}>
|
<Fragment key={message.id}>
|
||||||
<div
|
<div
|
||||||
className={
|
className={`flex mt-6 gap-2 ${isUser ? "flex-row-reverse" : ""}`}
|
||||||
isUser ? styles["chat-message-user"] : styles["chat-message"]
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<div className={styles["chat-message-container"]}>
|
<div className={`relative flex-0`}>
|
||||||
<div className={styles["chat-message-header"]}>
|
{isUser ? (
|
||||||
<div className={styles["chat-message-avatar"]}>
|
<Avatar avatar={config.avatar} />
|
||||||
<div className={styles["chat-message-edit"]}>
|
) : (
|
||||||
<IconButton
|
<>
|
||||||
icon={<EditIcon />}
|
{["system"].includes(message.role) ? (
|
||||||
onClick={async () => {
|
<Avatar avatar="2699-fe0f" />
|
||||||
const newMessage = await showPrompt(
|
|
||||||
Locale.Chat.Actions.Edit,
|
|
||||||
getMessageTextContent(message),
|
|
||||||
10,
|
|
||||||
);
|
|
||||||
let newContent: string | MultimodalContent[] =
|
|
||||||
newMessage;
|
|
||||||
const images = getMessageImages(message);
|
|
||||||
if (images.length > 0) {
|
|
||||||
newContent = [{ type: "text", text: newMessage }];
|
|
||||||
for (let i = 0; i < images.length; i++) {
|
|
||||||
newContent.push({
|
|
||||||
type: "image_url",
|
|
||||||
image_url: {
|
|
||||||
url: images[i],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
chatStore.updateCurrentSession((session) => {
|
|
||||||
const m = session.mask.context
|
|
||||||
.concat(session.messages)
|
|
||||||
.find((m) => m.id === message.id);
|
|
||||||
if (m) {
|
|
||||||
m.content = newContent;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
></IconButton>
|
|
||||||
</div>
|
|
||||||
{isUser ? (
|
|
||||||
<Avatar avatar={config.avatar} />
|
|
||||||
) : (
|
) : (
|
||||||
<>
|
<MaskAvatar
|
||||||
{["system"].includes(message.role) ? (
|
avatar={session.mask.avatar}
|
||||||
<Avatar avatar="2699-fe0f" />
|
model={message.model || session.mask.modelConfig.model}
|
||||||
) : (
|
/>
|
||||||
<MaskAvatar
|
|
||||||
avatar={session.mask.avatar}
|
|
||||||
model={
|
|
||||||
message.model || session.mask.modelConfig.model
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</>
|
||||||
|
|
||||||
{showActions && (
|
|
||||||
<div className={styles["chat-message-actions"]}>
|
|
||||||
<div className={styles["chat-input-actions"]}>
|
|
||||||
{message.streaming ? (
|
|
||||||
<ChatAction
|
|
||||||
text={Locale.Chat.Actions.Stop}
|
|
||||||
icon={<StopIcon />}
|
|
||||||
onClick={() => onUserStop(message.id ?? i)}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<ChatAction
|
|
||||||
text={Locale.Chat.Actions.Retry}
|
|
||||||
icon={<ResetIcon />}
|
|
||||||
onClick={() => onResend(message)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ChatAction
|
|
||||||
text={Locale.Chat.Actions.Delete}
|
|
||||||
icon={<DeleteIcon />}
|
|
||||||
onClick={() => onDelete(message.id ?? i)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ChatAction
|
|
||||||
text={Locale.Chat.Actions.Pin}
|
|
||||||
icon={<PinIcon />}
|
|
||||||
onClick={() => onPinMessage(message)}
|
|
||||||
/>
|
|
||||||
<ChatAction
|
|
||||||
text={Locale.Chat.Actions.Copy}
|
|
||||||
icon={<CopyIcon />}
|
|
||||||
onClick={() =>
|
|
||||||
copyToClipboard(getMessageTextContent(message))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{showTyping && (
|
|
||||||
<div className={styles["chat-message-status"]}>
|
|
||||||
{Locale.Chat.Typing}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
<div className={styles["chat-message-item"]}>
|
</div>
|
||||||
|
{/* {showTyping && (
|
||||||
|
<div className={styles["chat-message-status"]}>
|
||||||
|
{Locale.Chat.Typing}
|
||||||
|
</div>
|
||||||
|
)} */}
|
||||||
|
<div className={`group relative max-w-message-width`}>
|
||||||
|
<div
|
||||||
|
className={` pointer-events-none text-gray-500 text-right text-time whitespace-nowrap transition-all duration-500 text-sm absolute z-1 ${
|
||||||
|
isUser ? "right-0" : "left-0"
|
||||||
|
} bottom-[100%] hidden group-hover:block`}
|
||||||
|
>
|
||||||
|
{isContext
|
||||||
|
? Locale.Chat.IsContext
|
||||||
|
: message.date.toLocaleString()}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`transition-all duration-300 select-text break-words font-common text-sm-title rounded-message box-border peer py-2 px-3 ${
|
||||||
|
isUser ? "text-right bg-message-bg" : " bg-white"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
<Markdown
|
<Markdown
|
||||||
content={getMessageTextContent(message)}
|
content={getMessageTextContent(message)}
|
||||||
loading={
|
loading={
|
||||||
|
@ -357,17 +288,18 @@ export default function ChatMessagePanel(props: ChatMessagePanelProps) {
|
||||||
fontSize={fontSize}
|
fontSize={fontSize}
|
||||||
parentRef={scrollRef}
|
parentRef={scrollRef}
|
||||||
defaultShow={i >= messages.length - 6}
|
defaultShow={i >= messages.length - 6}
|
||||||
|
className={isUser ? " text-white" : "text-black"}
|
||||||
/>
|
/>
|
||||||
{getMessageImages(message).length == 1 && (
|
{getMessageImages(message).length == 1 && (
|
||||||
<img
|
<img
|
||||||
className={styles["chat-message-item-image"]}
|
className={` w-[100%] mt-2.5`}
|
||||||
src={getMessageImages(message)[0]}
|
src={getMessageImages(message)[0]}
|
||||||
alt=""
|
alt=""
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{getMessageImages(message).length > 1 && (
|
{getMessageImages(message).length > 1 && (
|
||||||
<div
|
<div
|
||||||
className={styles["chat-message-item-images"]}
|
className={`styles["chat-message-item-images"] w-[100%]`}
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
"--image-count": getMessageImages(message).length,
|
"--image-count": getMessageImages(message).length,
|
||||||
|
@ -388,11 +320,85 @@ export default function ChatMessagePanel(props: ChatMessagePanelProps) {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles["chat-message-action-date"]}>
|
{showActions && (
|
||||||
{isContext
|
<div
|
||||||
? Locale.Chat.IsContext
|
className={` absolute ${
|
||||||
: message.date.toLocaleString()}
|
isUser ? "right-0" : "left-0"
|
||||||
</div>
|
} top-[100%] hidden group-hover:block`}
|
||||||
|
>
|
||||||
|
<div className={styles["chat-input-actions"]}>
|
||||||
|
{message.streaming ? (
|
||||||
|
<ChatAction
|
||||||
|
text={Locale.Chat.Actions.Stop}
|
||||||
|
icon={<StopIcon />}
|
||||||
|
onClick={() => onUserStop(message.id ?? i)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<ChatAction
|
||||||
|
text={Locale.Chat.Actions.Retry}
|
||||||
|
icon={<ResetIcon />}
|
||||||
|
onClick={() => onResend(message)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ChatAction
|
||||||
|
text={Locale.Chat.Actions.Delete}
|
||||||
|
icon={<DeleteIcon />}
|
||||||
|
onClick={() => onDelete(message.id ?? i)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ChatAction
|
||||||
|
text={Locale.Chat.Actions.Pin}
|
||||||
|
icon={<PinIcon />}
|
||||||
|
onClick={() => onPinMessage(message)}
|
||||||
|
/>
|
||||||
|
<ChatAction
|
||||||
|
text={Locale.Chat.Actions.Copy}
|
||||||
|
icon={<CopyIcon />}
|
||||||
|
onClick={() =>
|
||||||
|
copyToClipboard(getMessageTextContent(message))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<ChatAction
|
||||||
|
text={Locale.Chat.Actions.Copy}
|
||||||
|
icon={<EditIcon />}
|
||||||
|
onClick={async () => {
|
||||||
|
const newMessage = await showPrompt(
|
||||||
|
Locale.Chat.Actions.Edit,
|
||||||
|
getMessageTextContent(message),
|
||||||
|
10,
|
||||||
|
);
|
||||||
|
let newContent: string | MultimodalContent[] =
|
||||||
|
newMessage;
|
||||||
|
const images = getMessageImages(message);
|
||||||
|
if (images.length > 0) {
|
||||||
|
newContent = [
|
||||||
|
{ type: "text", text: newMessage },
|
||||||
|
];
|
||||||
|
for (let i = 0; i < images.length; i++) {
|
||||||
|
newContent.push({
|
||||||
|
type: "image_url",
|
||||||
|
image_url: {
|
||||||
|
url: images[i],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
chatStore.updateCurrentSession((session) => {
|
||||||
|
const m = session.mask.context
|
||||||
|
.concat(session.messages)
|
||||||
|
.find((m) => m.id === message.id);
|
||||||
|
if (m) {
|
||||||
|
m.content = newContent;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{shouldShowClearContextDivider && <ClearContextDivider />}
|
{shouldShowClearContextDivider && <ClearContextDivider />}
|
||||||
|
|
|
@ -8,6 +8,7 @@ export type RenderPompt = Pick<Prompt, "title" | "content">;
|
||||||
export default function PromptHints(props: {
|
export default function PromptHints(props: {
|
||||||
prompts: RenderPompt[];
|
prompts: RenderPompt[];
|
||||||
onPromptSelect: (prompt: RenderPompt) => void;
|
onPromptSelect: (prompt: RenderPompt) => void;
|
||||||
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
const noPrompts = props.prompts.length === 0;
|
const noPrompts = props.prompts.length === 0;
|
||||||
const [selectIndex, setSelectIndex] = useState(0);
|
const [selectIndex, setSelectIndex] = useState(0);
|
||||||
|
@ -56,7 +57,7 @@ export default function PromptHints(props: {
|
||||||
|
|
||||||
if (noPrompts) return null;
|
if (noPrompts) return null;
|
||||||
return (
|
return (
|
||||||
<div className={styles["prompt-hints"]}>
|
<div className={`${styles["prompt-hints"]} ${props.className}`}>
|
||||||
{props.prompts.map((prompt, i) => (
|
{props.prompts.map((prompt, i) => (
|
||||||
<div
|
<div
|
||||||
ref={i === selectIndex ? selectedRef : null}
|
ref={i === selectIndex ? selectedRef : null}
|
||||||
|
|
|
@ -408,17 +408,17 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-message-item {
|
.chat-message-item {
|
||||||
box-sizing: border-box;
|
// box-sizing: border-box;
|
||||||
max-width: 100%;
|
// max-width: 100%;
|
||||||
margin-top: 10px;
|
// margin-top: 10px;
|
||||||
border-radius: 10px;
|
// border-radius: 10px;
|
||||||
background-color: rgba(0, 0, 0, 0.05);
|
// background-color: rgba(0, 0, 0, 0.05);
|
||||||
padding: 10px;
|
// padding: 10px;
|
||||||
font-size: 14px;
|
// font-size: 14px;
|
||||||
user-select: text;
|
// user-select: text;
|
||||||
word-break: break-word;
|
// word-break: break-word;
|
||||||
border: var(--border-in-light);
|
// border: var(--border-in-light);
|
||||||
position: relative;
|
// position: relative;
|
||||||
transition: all ease 0.3s;
|
transition: all ease 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -480,19 +480,19 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-message-action-date {
|
// .chat-message-action-date {
|
||||||
font-size: 12px;
|
// // font-size: 12px;
|
||||||
opacity: 0.2;
|
// // opacity: 0.2;
|
||||||
white-space: nowrap;
|
// // white-space: nowrap;
|
||||||
transition: all ease 0.6s;
|
// // transition: all ease 0.6s;
|
||||||
color: var(--black);
|
// // color: var(--black);
|
||||||
text-align: right;
|
// // text-align: right;
|
||||||
width: 100%;
|
// // width: 100%;
|
||||||
box-sizing: border-box;
|
// // box-sizing: border-box;
|
||||||
padding-right: 10px;
|
// // padding-right: 10px;
|
||||||
pointer-events: none;
|
// // pointer-events: none;
|
||||||
z-index: 1;
|
// // z-index: 1;
|
||||||
}
|
// }
|
||||||
|
|
||||||
.chat-message-user>.chat-message-container>.chat-message-item {
|
.chat-message-user>.chat-message-container>.chat-message-item {
|
||||||
background-color: var(--second);
|
background-color: var(--second);
|
||||||
|
@ -503,14 +503,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-input-panel {
|
.chat-input-panel {
|
||||||
position: relative;
|
// position: relative;
|
||||||
width: 100%;
|
// width: 100%;
|
||||||
padding: 20px;
|
// padding: 20px;
|
||||||
padding-top: 10px;
|
// padding-top: 10px;
|
||||||
box-sizing: border-box;
|
// box-sizing: border-box;
|
||||||
flex-direction: column;
|
// flex-direction: column;
|
||||||
border-top: var(--border-in-light);
|
// border-top: var(--border-in-light);
|
||||||
box-shadow: var(--card-shadow);
|
// box-shadow: var(--card-shadow);
|
||||||
|
|
||||||
.chat-input-actions {
|
.chat-input-actions {
|
||||||
.chat-input-action {
|
.chat-input-action {
|
||||||
|
|
|
@ -6,10 +6,11 @@ import {
|
||||||
createMessage,
|
createMessage,
|
||||||
useAccessStore,
|
useAccessStore,
|
||||||
useAppConfig,
|
useAppConfig,
|
||||||
|
ModelType,
|
||||||
} from "@/app/store";
|
} from "@/app/store";
|
||||||
import { autoGrowTextArea, useMobileScreen } from "@/app/utils";
|
import { autoGrowTextArea, useMobileScreen } from "@/app/utils";
|
||||||
import Locale from "@/app/locales";
|
import Locale from "@/app/locales";
|
||||||
import { showConfirm } from "@/app/components/ui-lib";
|
import { Selector, showConfirm, showToast } from "@/app/components/ui-lib";
|
||||||
import {
|
import {
|
||||||
CHAT_PAGE_SIZE,
|
CHAT_PAGE_SIZE,
|
||||||
REQUEST_TIMEOUT_MS,
|
REQUEST_TIMEOUT_MS,
|
||||||
|
@ -24,8 +25,7 @@ import { EditMessageModal } from "./EditMessageModal";
|
||||||
import ChatHeader from "./ChatHeader";
|
import ChatHeader from "./ChatHeader";
|
||||||
import ChatInputPanel, { ChatInputPanelInstance } from "./ChatInputPanel";
|
import ChatInputPanel, { ChatInputPanelInstance } from "./ChatInputPanel";
|
||||||
import ChatMessagePanel, { RenderMessage } from "./ChatMessagePanel";
|
import ChatMessagePanel, { RenderMessage } from "./ChatMessagePanel";
|
||||||
|
import { useAllModels } from "@/app/utils/hooks";
|
||||||
import styles from "./index.module.scss";
|
|
||||||
|
|
||||||
function _Chat() {
|
function _Chat() {
|
||||||
const chatStore = useChatStore();
|
const chatStore = useChatStore();
|
||||||
|
@ -33,6 +33,7 @@ function _Chat() {
|
||||||
const config = useAppConfig();
|
const config = useAppConfig();
|
||||||
|
|
||||||
const [showExport, setShowExport] = useState(false);
|
const [showExport, setShowExport] = useState(false);
|
||||||
|
const [showModelSelector, setShowModelSelector] = useState(false);
|
||||||
|
|
||||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const [userInput, setUserInput] = useState("");
|
const [userInput, setUserInput] = useState("");
|
||||||
|
@ -236,6 +237,7 @@ function _Chat() {
|
||||||
setIsLoading,
|
setIsLoading,
|
||||||
setShowPromptModal,
|
setShowPromptModal,
|
||||||
_setMsgRenderIndex,
|
_setMsgRenderIndex,
|
||||||
|
showModelSelector: setShowModelSelector,
|
||||||
};
|
};
|
||||||
|
|
||||||
const chatMessagePanelProps = {
|
const chatMessagePanelProps = {
|
||||||
|
@ -254,15 +256,25 @@ function _Chat() {
|
||||||
setShowPromptModal,
|
setShowPromptModal,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const currentModel = chatStore.currentSession().mask.modelConfig.model;
|
||||||
|
const allModels = useAllModels();
|
||||||
|
const models = useMemo(
|
||||||
|
() => allModels.filter((m) => m.available),
|
||||||
|
[allModels],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`${styles.chat} my-2.5 ml-1 mr-2.5 rounded-md bg-gray-50`}
|
className={`flex flex-col h-[100%] overflow-hidden ${
|
||||||
|
isMobileScreen ? "" : `my-2.5 ml-1 mr-2.5 rounded-md`
|
||||||
|
} bg-gray-50`}
|
||||||
key={session.id}
|
key={session.id}
|
||||||
>
|
>
|
||||||
<ChatHeader
|
<ChatHeader
|
||||||
setIsEditingMessage={setIsEditingMessage}
|
setIsEditingMessage={setIsEditingMessage}
|
||||||
setShowExport={setShowExport}
|
setShowExport={setShowExport}
|
||||||
isMobileScreen={isMobileScreen}
|
isMobileScreen={isMobileScreen}
|
||||||
|
showModelSelector={setShowModelSelector}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ChatMessagePanel {...chatMessagePanelProps} />
|
<ChatMessagePanel {...chatMessagePanelProps} />
|
||||||
|
@ -286,6 +298,25 @@ function _Chat() {
|
||||||
showModal={showPromptModal}
|
showModal={showPromptModal}
|
||||||
setShowModal={setShowPromptModal}
|
setShowModal={setShowPromptModal}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{showModelSelector && (
|
||||||
|
<Selector
|
||||||
|
defaultSelectedValue={currentModel}
|
||||||
|
items={models.map((m) => ({
|
||||||
|
title: m.displayName,
|
||||||
|
value: m.name,
|
||||||
|
}))}
|
||||||
|
onClose={() => setShowModelSelector(false)}
|
||||||
|
onSelection={(s) => {
|
||||||
|
if (s.length === 0) return;
|
||||||
|
chatStore.updateCurrentSession((session) => {
|
||||||
|
session.mask.modelConfig.model = s[0] as ModelType;
|
||||||
|
session.mask.syncGlobalConfig = false;
|
||||||
|
});
|
||||||
|
showToast(s[0]);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { Path } from "@/app/constant";
|
||||||
|
import { ComponentType } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
export interface MenuWrapperProps {
|
||||||
|
show: boolean;
|
||||||
|
wrapperClassName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MenuWrapper<ComponentProps>(
|
||||||
|
Component: ComponentType<ComponentProps>,
|
||||||
|
) {
|
||||||
|
return function MenuHood(props: MenuWrapperProps & ComponentProps) {
|
||||||
|
const { show, wrapperClassName } = props;
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
if (!show) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`flex flex-col px-6 pb-6 ${wrapperClassName}`}
|
||||||
|
onClick={(e) => {
|
||||||
|
if (e.target === e.currentTarget) {
|
||||||
|
navigate(Path.Home);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Component {...props} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
|
@ -134,11 +134,17 @@ export default function SessionList(props: ListHoodProps) {
|
||||||
moveSession(source.index, destination.index);
|
moveSession(source.index, destination.index);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let layoutClassName = "py-7 px-0";
|
||||||
|
|
||||||
|
if (isMobileScreen) {
|
||||||
|
layoutClassName = "h-menu-title-mobile py-6";
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div data-tauri-drag-region>
|
<div data-tauri-drag-region>
|
||||||
<div
|
<div
|
||||||
className="flex items-center justify-between py-7 px-0"
|
className={`flex items-center justify-between ${layoutClassName}`}
|
||||||
data-tauri-drag-region
|
data-tauri-drag-region
|
||||||
>
|
>
|
||||||
<div className="">
|
<div className="">
|
||||||
|
|
|
@ -0,0 +1,121 @@
|
||||||
|
import { isValidElement } from "react";
|
||||||
|
|
||||||
|
type IconMap = {
|
||||||
|
active?: JSX.Element;
|
||||||
|
inactive?: JSX.Element;
|
||||||
|
mobileActive?: JSX.Element;
|
||||||
|
mobileInactive?: JSX.Element;
|
||||||
|
};
|
||||||
|
interface Action {
|
||||||
|
id: string;
|
||||||
|
title?: string;
|
||||||
|
icons: JSX.Element | IconMap;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Groups = {
|
||||||
|
normal: string[][];
|
||||||
|
mobile: string[][];
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface TabActionsProps {
|
||||||
|
actionsShema: Action[];
|
||||||
|
onSelect: (id: string) => void;
|
||||||
|
selected: string;
|
||||||
|
groups: string[][] | Groups;
|
||||||
|
className?: string;
|
||||||
|
inMobile: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TabActions(props: TabActionsProps) {
|
||||||
|
const { actionsShema, onSelect, selected, groups, className, inMobile } =
|
||||||
|
props;
|
||||||
|
|
||||||
|
const handlerClick = (id: string) => (e: { preventDefault: () => void }) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (selected !== id) {
|
||||||
|
onSelect?.(id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const internalGroup = Array.isArray(groups)
|
||||||
|
? groups
|
||||||
|
: inMobile
|
||||||
|
? groups.mobile
|
||||||
|
: groups.normal;
|
||||||
|
|
||||||
|
const content = internalGroup.reduce((res, group, ind, arr) => {
|
||||||
|
res.push(
|
||||||
|
...group.map((i) => {
|
||||||
|
const action = actionsShema.find((a) => a.id === i);
|
||||||
|
if (!action) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { icons } = action;
|
||||||
|
let activeIcon, inactiveIcon, mobileActiveIcon, mobileInactiveIcon;
|
||||||
|
|
||||||
|
if (isValidElement(icons)) {
|
||||||
|
activeIcon = icons;
|
||||||
|
inactiveIcon = icons;
|
||||||
|
mobileActiveIcon = icons;
|
||||||
|
mobileInactiveIcon = icons;
|
||||||
|
} else {
|
||||||
|
activeIcon = (icons as IconMap).active;
|
||||||
|
inactiveIcon = (icons as IconMap).inactive;
|
||||||
|
mobileActiveIcon = (icons as IconMap).mobileActive;
|
||||||
|
mobileInactiveIcon = (icons as IconMap).mobileInactive;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inMobile) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={action.id}
|
||||||
|
className={` shrink-1 grow-0 basis-[${
|
||||||
|
(100 - 1) / arr.length
|
||||||
|
}%] flex flex-col items-center justify-center gap-0.5
|
||||||
|
${
|
||||||
|
selected === action.id
|
||||||
|
? "text-blue-700"
|
||||||
|
: "text-gray-400"
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
onClick={handlerClick(action.id)}
|
||||||
|
>
|
||||||
|
{selected === action.id ? mobileActiveIcon : mobileInactiveIcon}
|
||||||
|
<div className=" leading-3 text-sm-mobile-tab h-3 font-common w-[100%]">
|
||||||
|
{action.title || " "}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={action.id}
|
||||||
|
className={` ${
|
||||||
|
selected === action.id ? "bg-blue-900" : "bg-transparent"
|
||||||
|
} p-3 rounded-md items-center ${action.className}`}
|
||||||
|
onClick={handlerClick(action.id)}
|
||||||
|
>
|
||||||
|
{selected === action.id ? activeIcon : inactiveIcon}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
if (ind < arr.length - 1) {
|
||||||
|
res.push(<div className=" flex-1"></div>);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}, [] as JSX.Element[]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`flex ${
|
||||||
|
inMobile ? "justify-around" : "flex-col"
|
||||||
|
} items-center ${className}`}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,4 +1,3 @@
|
||||||
import { useMemo } from "react";
|
|
||||||
import DragIcon from "@/app/icons/drag.svg";
|
import DragIcon from "@/app/icons/drag.svg";
|
||||||
import DiscoverIcon from "@/app/icons/discoverActive.svg";
|
import DiscoverIcon from "@/app/icons/discoverActive.svg";
|
||||||
import AssistantActiveIcon from "@/app/icons/assistantActive.svg";
|
import AssistantActiveIcon from "@/app/icons/assistantActive.svg";
|
||||||
|
@ -7,30 +6,34 @@ import SettingIcon from "@/app/icons/settingActive.svg";
|
||||||
import DiscoverInactiveIcon from "@/app/icons/discoverInactive.svg";
|
import DiscoverInactiveIcon from "@/app/icons/discoverInactive.svg";
|
||||||
import AssistantInactiveIcon from "@/app/icons/assistantInactive.svg";
|
import AssistantInactiveIcon from "@/app/icons/assistantInactive.svg";
|
||||||
import SettingInactiveIcon from "@/app/icons/settingInactive.svg";
|
import SettingInactiveIcon from "@/app/icons/settingInactive.svg";
|
||||||
|
import SettingMobileActive from "@/app/icons/settingMobileActive.svg";
|
||||||
|
import DiscoverMobileActive from "@/app/icons/discoverMobileActive.svg";
|
||||||
|
import AssistantMobileActive from "@/app/icons/assistantMobileActive.svg";
|
||||||
|
import AssistantMobileInactive from "@/app/icons/assistantMobileInactive.svg";
|
||||||
|
|
||||||
import { useAppConfig, useChatStore } from "@/app/store";
|
import { useAppConfig } from "@/app/store";
|
||||||
|
|
||||||
import { Path, REPO_URL } from "@/app/constant";
|
import { Path, REPO_URL } from "@/app/constant";
|
||||||
|
|
||||||
import { useNavigate, useLocation } from "react-router-dom";
|
import { useNavigate, useLocation } from "react-router-dom";
|
||||||
import { isIOS } from "@/app/utils";
|
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import useHotKey from "@/app/hooks/useHotKey";
|
import useHotKey from "@/app/hooks/useHotKey";
|
||||||
import useDragSideBar from "@/app/hooks/useDragSideBar";
|
import useDragSideBar from "@/app/hooks/useDragSideBar";
|
||||||
import useMobileScreen from "@/app/hooks/useMobileScreen";
|
import useMobileScreen from "@/app/hooks/useMobileScreen";
|
||||||
import TabActions from "@/app/components/TabActions";
|
import TabActions from "./TabActions";
|
||||||
|
import MenuWrapper from "./MenuWrapper";
|
||||||
|
|
||||||
const SessionList = dynamic(async () => await import("./SessionList"), {
|
const SessionList = MenuWrapper(
|
||||||
loading: () => null,
|
dynamic(async () => await import("./SessionList"), {
|
||||||
});
|
loading: () => null,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
const SettingList = dynamic(async () => await import("./SettingList"), {
|
const SettingList = MenuWrapper(
|
||||||
loading: () => null,
|
dynamic(async () => await import("./SettingList"), {
|
||||||
});
|
loading: () => null,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
export function SideBar(props: { className?: string }) {
|
export function SideBar(props: { className?: string }) {
|
||||||
const chatStore = useChatStore();
|
|
||||||
|
|
||||||
// drag side bar
|
// drag side bar
|
||||||
const { onDragStart } = useDragSideBar();
|
const { onDragStart } = useDragSideBar();
|
||||||
|
|
||||||
|
@ -39,10 +42,6 @@ export function SideBar(props: { className?: string }) {
|
||||||
|
|
||||||
const config = useAppConfig();
|
const config = useAppConfig();
|
||||||
const isMobileScreen = useMobileScreen();
|
const isMobileScreen = useMobileScreen();
|
||||||
const isIOSMobile = useMemo(
|
|
||||||
() => isIOS() && isMobileScreen,
|
|
||||||
[isMobileScreen],
|
|
||||||
);
|
|
||||||
|
|
||||||
useHotKey();
|
useHotKey();
|
||||||
|
|
||||||
|
@ -60,29 +59,41 @@ export function SideBar(props: { className?: string }) {
|
||||||
selectedTab = Path.Chat;
|
selectedTab = Path.Chat;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let containerClassName = "relative flex h-[100%] w-[100%]";
|
||||||
|
let tabActionsClassName = "2xl:px-5 xl:px-4 px-2 py-6";
|
||||||
|
let menuClassName =
|
||||||
|
"max-md:px-4 max-md:pb-4 rounded-md my-2.5 bg-gray-50 flex-1";
|
||||||
|
|
||||||
|
if (isMobileScreen) {
|
||||||
|
containerClassName = "flex flex-col-reverse w-[100%] h-[100%]";
|
||||||
|
tabActionsClassName = "bg-gray-100 rounded-tl-md rounded-tr-md h-mobile";
|
||||||
|
menuClassName = `flex-1 px-4`;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={`${containerClassName}`}>
|
||||||
className={` inline-flex h-[100%] ${props.className} relative`}
|
|
||||||
style={{
|
|
||||||
// #3016 disable transition on ios mobile screen
|
|
||||||
transition: isMobileScreen && isIOSMobile ? "none" : undefined,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TabActions
|
<TabActions
|
||||||
|
inMobile={isMobileScreen}
|
||||||
actionsShema={[
|
actionsShema={[
|
||||||
{
|
{
|
||||||
id: Path.Masks,
|
id: Path.Masks,
|
||||||
icons: {
|
icons: {
|
||||||
active: <DiscoverIcon />,
|
active: <DiscoverIcon />,
|
||||||
inactive: <DiscoverInactiveIcon />,
|
inactive: <DiscoverInactiveIcon />,
|
||||||
|
mobileActive: <DiscoverMobileActive />,
|
||||||
|
mobileInactive: <DiscoverInactiveIcon />,
|
||||||
},
|
},
|
||||||
|
title: "Discover",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: Path.Chat,
|
id: Path.Chat,
|
||||||
icons: {
|
icons: {
|
||||||
active: <AssistantActiveIcon />,
|
active: <AssistantActiveIcon />,
|
||||||
inactive: <AssistantInactiveIcon />,
|
inactive: <AssistantInactiveIcon />,
|
||||||
|
mobileActive: <AssistantMobileActive />,
|
||||||
|
mobileInactive: <AssistantMobileInactive />,
|
||||||
},
|
},
|
||||||
|
title: "Assistant",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "github",
|
id: "github",
|
||||||
|
@ -94,8 +105,11 @@ export function SideBar(props: { className?: string }) {
|
||||||
icons: {
|
icons: {
|
||||||
active: <SettingIcon />,
|
active: <SettingIcon />,
|
||||||
inactive: <SettingInactiveIcon />,
|
inactive: <SettingInactiveIcon />,
|
||||||
|
mobileActive: <SettingMobileActive />,
|
||||||
|
mobileInactive: <SettingInactiveIcon />,
|
||||||
},
|
},
|
||||||
className: "p-2",
|
className: "p-2",
|
||||||
|
title: "Settrings",
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
onSelect={(id) => {
|
onSelect={(id) => {
|
||||||
|
@ -111,27 +125,25 @@ export function SideBar(props: { className?: string }) {
|
||||||
navigate(Path.Masks, { state: { fromHome: true } });
|
navigate(Path.Masks, { state: { fromHome: true } });
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
groups={[
|
groups={{
|
||||||
[Path.Chat, Path.Masks],
|
normal: [
|
||||||
["github", Path.Settings],
|
[Path.Chat, Path.Masks],
|
||||||
]}
|
["github", Path.Settings],
|
||||||
|
],
|
||||||
|
mobile: [[Path.Chat, Path.Masks, Path.Settings]],
|
||||||
|
}}
|
||||||
selected={selectedTab}
|
selected={selectedTab}
|
||||||
className="px-5 py-6"
|
className={tabActionsClassName}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<SessionList
|
||||||
className={`flex flex-col w-md lg:w-lg 2xl:w-2xl px-6 pb-6 max-md:px-4 max-md:pb-4 bg-gray-50 rounded-md my-2.5 ${
|
show={selectedTab === Path.Chat}
|
||||||
isMobileScreen && `bg-gray-300`
|
wrapperClassName={menuClassName}
|
||||||
}`}
|
/>
|
||||||
onClick={(e) => {
|
<SettingList
|
||||||
if (e.target === e.currentTarget) {
|
show={selectedTab === Path.Settings}
|
||||||
navigate(Path.Home);
|
wrapperClassName={menuClassName}
|
||||||
}
|
/>
|
||||||
}}
|
|
||||||
>
|
|
||||||
{selectedTab === Path.Chat && <SessionList />}
|
|
||||||
{loc.pathname === Path.Settings && <SettingList />}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!isMobileScreen && (
|
{!isMobileScreen && (
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -0,0 +1,117 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
require("../polyfill");
|
||||||
|
|
||||||
|
import { HashRouter as Router, Routes, Route } from "react-router-dom";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
import { Path } from "@/app/constant";
|
||||||
|
import { ErrorBoundary } from "@/app/components/error";
|
||||||
|
import { getISOLang } from "@/app/locales";
|
||||||
|
import { useSwitchTheme } from "@/app/hooks/useSwitchTheme";
|
||||||
|
import { AuthPage } from "@/app/components/auth";
|
||||||
|
import { getClientConfig } from "@/app/config/client";
|
||||||
|
import { useAccessStore } from "@/app/store";
|
||||||
|
import { useLoadData } from "@/app/hooks/useLoadData";
|
||||||
|
import Loading from "@/app/components/Loading";
|
||||||
|
import Screen from "@/app/components/Screen";
|
||||||
|
import { SideBar } from "./Sidebar";
|
||||||
|
|
||||||
|
const Settings = dynamic(
|
||||||
|
async () => (await import("@/app/components/settings")).Settings,
|
||||||
|
{
|
||||||
|
loading: () => <Loading noLogo />,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const Chat = dynamic(async () => await import("@/app/containers/Chat"), {
|
||||||
|
loading: () => <Loading noLogo />,
|
||||||
|
});
|
||||||
|
|
||||||
|
const NewChat = dynamic(
|
||||||
|
async () => (await import("@/app/components/new-chat")).NewChat,
|
||||||
|
{
|
||||||
|
loading: () => <Loading noLogo />,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const MaskPage = dynamic(
|
||||||
|
async () => (await import("@/app/components/mask")).MaskPage,
|
||||||
|
{
|
||||||
|
loading: () => <Loading noLogo />,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
function useHtmlLang() {
|
||||||
|
useEffect(() => {
|
||||||
|
const lang = getISOLang();
|
||||||
|
const htmlLang = document.documentElement.lang;
|
||||||
|
|
||||||
|
if (lang !== htmlLang) {
|
||||||
|
document.documentElement.lang = lang;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
const useHasHydrated = () => {
|
||||||
|
const [hasHydrated, setHasHydrated] = useState<boolean>(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setHasHydrated(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return hasHydrated;
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadAsyncGoogleFont = () => {
|
||||||
|
const linkEl = document.createElement("link");
|
||||||
|
const proxyFontUrl = "/google-fonts";
|
||||||
|
const remoteFontUrl = "https://fonts.googleapis.com";
|
||||||
|
const googleFontUrl =
|
||||||
|
getClientConfig()?.buildMode === "export" ? remoteFontUrl : proxyFontUrl;
|
||||||
|
linkEl.rel = "stylesheet";
|
||||||
|
linkEl.href =
|
||||||
|
googleFontUrl +
|
||||||
|
"/css2?family=" +
|
||||||
|
encodeURIComponent("Noto Sans:wght@300;400;700;900") +
|
||||||
|
"&display=swap";
|
||||||
|
document.head.appendChild(linkEl);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
useSwitchTheme();
|
||||||
|
useLoadData();
|
||||||
|
useHtmlLang();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log("[Config] got config from build time", getClientConfig());
|
||||||
|
useAccessStore.getState().fetch();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadAsyncGoogleFont();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!useHasHydrated()) {
|
||||||
|
return <Loading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ErrorBoundary>
|
||||||
|
<Router>
|
||||||
|
<Screen noAuth={<AuthPage />} sidebar={<SideBar />}>
|
||||||
|
<ErrorBoundary>
|
||||||
|
<Routes>
|
||||||
|
<Route path={Path.Home} element={<Chat />} />
|
||||||
|
<Route path={Path.NewChat} element={<NewChat />} />
|
||||||
|
<Route path={Path.Masks} element={<MaskPage />} />
|
||||||
|
<Route path={Path.Chat} element={<Chat />} />
|
||||||
|
<Route path={Path.Settings} element={<Settings />} />
|
||||||
|
</Routes>
|
||||||
|
</ErrorBoundary>
|
||||||
|
</Screen>
|
||||||
|
</Router>
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
}
|
|
@ -2,14 +2,13 @@ import {
|
||||||
DEFAULT_SIDEBAR_WIDTH,
|
DEFAULT_SIDEBAR_WIDTH,
|
||||||
MAX_SIDEBAR_WIDTH,
|
MAX_SIDEBAR_WIDTH,
|
||||||
MIN_SIDEBAR_WIDTH,
|
MIN_SIDEBAR_WIDTH,
|
||||||
NARROW_SIDEBAR_WIDTH,
|
|
||||||
} from "@/app/constant";
|
} from "@/app/constant";
|
||||||
import { useAppConfig } from "../store/config";
|
import { useAppConfig } from "../store/config";
|
||||||
import { useEffect, useRef } from "react";
|
import { useRef } from "react";
|
||||||
import useMobileScreen from "@/app/hooks/useMobileScreen";
|
|
||||||
|
|
||||||
export default function useDragSideBar() {
|
export default function useDragSideBar() {
|
||||||
const limit = (x: number) => Math.min(MAX_SIDEBAR_WIDTH, x);
|
const limit = (x: number) =>
|
||||||
|
Math.max(MIN_SIDEBAR_WIDTH, Math.min(MAX_SIDEBAR_WIDTH, x));
|
||||||
|
|
||||||
const config = useAppConfig();
|
const config = useAppConfig();
|
||||||
const startX = useRef(0);
|
const startX = useRef(0);
|
||||||
|
@ -18,11 +17,7 @@ export default function useDragSideBar() {
|
||||||
|
|
||||||
const toggleSideBar = () => {
|
const toggleSideBar = () => {
|
||||||
config.update((config) => {
|
config.update((config) => {
|
||||||
if (config.sidebarWidth < MIN_SIDEBAR_WIDTH) {
|
config.sidebarWidth = DEFAULT_SIDEBAR_WIDTH;
|
||||||
config.sidebarWidth = DEFAULT_SIDEBAR_WIDTH;
|
|
||||||
} else {
|
|
||||||
config.sidebarWidth = NARROW_SIDEBAR_WIDTH;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -39,12 +34,13 @@ export default function useDragSideBar() {
|
||||||
lastUpdateTime.current = Date.now();
|
lastUpdateTime.current = Date.now();
|
||||||
const d = e.clientX - startX.current;
|
const d = e.clientX - startX.current;
|
||||||
const nextWidth = limit(startDragWidth.current + d);
|
const nextWidth = limit(startDragWidth.current + d);
|
||||||
|
|
||||||
|
document.documentElement.style.setProperty(
|
||||||
|
"--sidebar-width",
|
||||||
|
`${nextWidth}px`,
|
||||||
|
);
|
||||||
config.update((config) => {
|
config.update((config) => {
|
||||||
if (nextWidth < MIN_SIDEBAR_WIDTH) {
|
config.sidebarWidth = nextWidth;
|
||||||
config.sidebarWidth = NARROW_SIDEBAR_WIDTH;
|
|
||||||
} else {
|
|
||||||
config.sidebarWidth = nextWidth;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -64,20 +60,12 @@ export default function useDragSideBar() {
|
||||||
window.addEventListener("pointerup", handleDragEnd);
|
window.addEventListener("pointerup", handleDragEnd);
|
||||||
};
|
};
|
||||||
|
|
||||||
const isMobileScreen = useMobileScreen();
|
// useLayoutEffect(() => {
|
||||||
const shouldNarrow =
|
// const barWidth = limit(config.sidebarWidth ?? DEFAULT_SIDEBAR_WIDTH);
|
||||||
!isMobileScreen && config.sidebarWidth < MIN_SIDEBAR_WIDTH;
|
// document.documentElement.style.setProperty("--sidebar-width", `${barWidth}px`);
|
||||||
|
// }, [config.sidebarWidth]);
|
||||||
useEffect(() => {
|
|
||||||
const barWidth = shouldNarrow
|
|
||||||
? NARROW_SIDEBAR_WIDTH
|
|
||||||
: limit(config.sidebarWidth ?? DEFAULT_SIDEBAR_WIDTH);
|
|
||||||
const sideBarWidth = isMobileScreen ? "100vw" : `${barWidth}px`;
|
|
||||||
document.documentElement.style.setProperty("--sidebar-width", sideBarWidth);
|
|
||||||
}, [config.sidebarWidth, isMobileScreen, shouldNarrow]);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
onDragStart,
|
onDragStart,
|
||||||
shouldNarrow,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
import { useWindowSize } from "@/app/hooks/useWindowSize";
|
||||||
|
import {
|
||||||
|
WINDOW_WIDTH_2XL,
|
||||||
|
WINDOW_WIDTH_LG,
|
||||||
|
WINDOW_WIDTH_MD,
|
||||||
|
WINDOW_WIDTH_SM,
|
||||||
|
WINDOW_WIDTH_XL,
|
||||||
|
DEFAULT_SIDEBAR_WIDTH,
|
||||||
|
MAX_SIDEBAR_WIDTH,
|
||||||
|
MIN_SIDEBAR_WIDTH,
|
||||||
|
} from "@/app/constant";
|
||||||
|
import { useAppConfig } from "../store/config";
|
||||||
|
import { useReducer, useState } from "react";
|
||||||
|
|
||||||
|
export const MOBILE_MAX_WIDTH = 768;
|
||||||
|
|
||||||
|
const widths = [
|
||||||
|
WINDOW_WIDTH_2XL,
|
||||||
|
WINDOW_WIDTH_XL,
|
||||||
|
WINDOW_WIDTH_LG,
|
||||||
|
WINDOW_WIDTH_MD,
|
||||||
|
WINDOW_WIDTH_SM,
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function useListenWinResize() {
|
||||||
|
const config = useAppConfig();
|
||||||
|
|
||||||
|
const [_, refresh] = useReducer((x) => x + 1, 0);
|
||||||
|
|
||||||
|
useWindowSize((size) => {
|
||||||
|
let nextSidebar = config.sidebarWidth;
|
||||||
|
if (!nextSidebar) {
|
||||||
|
switch (widths.find((w) => w < size.width)) {
|
||||||
|
case WINDOW_WIDTH_2XL:
|
||||||
|
nextSidebar = MAX_SIDEBAR_WIDTH;
|
||||||
|
break;
|
||||||
|
case WINDOW_WIDTH_XL:
|
||||||
|
case WINDOW_WIDTH_LG:
|
||||||
|
nextSidebar = DEFAULT_SIDEBAR_WIDTH;
|
||||||
|
break;
|
||||||
|
case WINDOW_WIDTH_MD:
|
||||||
|
case WINDOW_WIDTH_SM:
|
||||||
|
default:
|
||||||
|
nextSidebar = MIN_SIDEBAR_WIDTH;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nextSidebar = Math.max(
|
||||||
|
MIN_SIDEBAR_WIDTH,
|
||||||
|
Math.min(MAX_SIDEBAR_WIDTH, nextSidebar),
|
||||||
|
);
|
||||||
|
document.documentElement.style.setProperty(
|
||||||
|
"--sidebar-width",
|
||||||
|
`${nextSidebar}px`,
|
||||||
|
);
|
||||||
|
config.update((config) => {
|
||||||
|
config.sidebarWidth = nextSidebar;
|
||||||
|
});
|
||||||
|
refresh();
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useAppConfig } from "@/app/store/config";
|
||||||
|
import { ClientApi } from "@/app/client/api";
|
||||||
|
import { ModelProvider } from "@/app/constant";
|
||||||
|
import { identifyDefaultClaudeModel } from "@/app/utils/checkers";
|
||||||
|
|
||||||
|
export function useLoadData() {
|
||||||
|
const config = useAppConfig();
|
||||||
|
|
||||||
|
var api: ClientApi;
|
||||||
|
if (config.modelConfig.model.startsWith("gemini")) {
|
||||||
|
api = new ClientApi(ModelProvider.GeminiPro);
|
||||||
|
} else if (identifyDefaultClaudeModel(config.modelConfig.model)) {
|
||||||
|
api = new ClientApi(ModelProvider.Claude);
|
||||||
|
} else {
|
||||||
|
api = new ClientApi(ModelProvider.GPT);
|
||||||
|
}
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
const models = await api.llm.models();
|
||||||
|
config.mergeModels(models);
|
||||||
|
})();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
}
|
|
@ -1,12 +1,16 @@
|
||||||
import { useLayoutEffect } from "react";
|
import { useWindowSize } from "@/app/hooks/useWindowSize";
|
||||||
import { useWindowSize } from "../utils";
|
import { useRef } from "react";
|
||||||
|
|
||||||
export const MOBILE_MAX_WIDTH = 600;
|
export const MOBILE_MAX_WIDTH = 768;
|
||||||
|
|
||||||
export default function useMobileScreen() {
|
export default function useMobileScreen() {
|
||||||
const { width } = useWindowSize();
|
const widthRef = useRef<number>(0);
|
||||||
|
|
||||||
const isMobile = width <= MOBILE_MAX_WIDTH;
|
useWindowSize((size) => {
|
||||||
|
widthRef.current = size.width;
|
||||||
|
});
|
||||||
|
|
||||||
|
const isMobile = widthRef.current <= MOBILE_MAX_WIDTH;
|
||||||
|
|
||||||
return isMobile;
|
return isMobile;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useAppConfig } from "@/app/store/config";
|
||||||
|
import { getCSSVar } from "@/app/utils";
|
||||||
|
|
||||||
|
export function useSwitchTheme() {
|
||||||
|
const config = useAppConfig();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.body.classList.remove("light");
|
||||||
|
document.body.classList.remove("dark");
|
||||||
|
|
||||||
|
if (config.theme === "dark") {
|
||||||
|
document.body.classList.add("dark");
|
||||||
|
} else if (config.theme === "light") {
|
||||||
|
document.body.classList.add("light");
|
||||||
|
}
|
||||||
|
|
||||||
|
const metaDescriptionDark = document.querySelector(
|
||||||
|
'meta[name="theme-color"][media*="dark"]',
|
||||||
|
);
|
||||||
|
const metaDescriptionLight = document.querySelector(
|
||||||
|
'meta[name="theme-color"][media*="light"]',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (config.theme === "auto") {
|
||||||
|
metaDescriptionDark?.setAttribute("content", "#151515");
|
||||||
|
metaDescriptionLight?.setAttribute("content", "#fafafa");
|
||||||
|
} else {
|
||||||
|
const themeColor = getCSSVar("--theme-color");
|
||||||
|
metaDescriptionDark?.setAttribute("content", themeColor);
|
||||||
|
metaDescriptionLight?.setAttribute("content", themeColor);
|
||||||
|
}
|
||||||
|
}, [config.theme]);
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { useLayoutEffect, useMemo, useRef } from "react";
|
||||||
|
|
||||||
|
type Size = {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useWindowSize(callback: (size: Size) => void) {
|
||||||
|
const callbackRef = useRef<typeof callback>();
|
||||||
|
const hascalled = useRef(false);
|
||||||
|
|
||||||
|
if (typeof window !== "undefined" && !hascalled.current) {
|
||||||
|
callback({
|
||||||
|
width: window.innerWidth,
|
||||||
|
height: window.innerHeight,
|
||||||
|
});
|
||||||
|
hascalled.current = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
callbackRef.current = callback;
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const onResize = () => {
|
||||||
|
callbackRef.current?.({
|
||||||
|
width: window.innerWidth,
|
||||||
|
height: window.innerHeight,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("resize", onResize);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("resize", onResize);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.1001 12.0001C1.1001 6.00304 6.00304 1.1001 12.0001 1.1001C17.9972 1.1001 22.9001 6.00304 22.9001 12.0001C22.9001 17.9972 17.9972 22.9001 12.0001 22.9001C6.00304 22.9001 1.1001 17.9972 1.1001 12.0001ZM12.0001 2.9001C6.99715 2.9001 2.9001 6.99715 2.9001 12.0001C2.9001 17.003 6.99715 21.1001 12.0001 21.1001C17.003 21.1001 21.1001 17.003 21.1001 12.0001C21.1001 6.99715 17.003 2.9001 12.0001 2.9001Z" fill="#606078"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.1001 12.0001C7.1001 11.503 7.50304 11.1001 8.0001 11.1001H16.0001C16.4972 11.1001 16.9001 11.503 16.9001 12.0001C16.9001 12.4972 16.4972 12.9001 16.0001 12.9001H8.0001C7.50304 12.9001 7.1001 12.4972 7.1001 12.0001Z" fill="#606078"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.0001 7.1001C12.4972 7.1001 12.9001 7.50304 12.9001 8.0001V16.0001C12.9001 16.4972 12.4972 16.9001 12.0001 16.9001C11.503 16.9001 11.1001 16.4972 11.1001 16.0001V8.0001C11.1001 7.50304 11.503 7.1001 12.0001 7.1001Z" fill="#606078"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
|
@ -0,0 +1,9 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M2 4C2 2.89543 2.89543 2 4 2H12H12.1639V2.00132C17.6112 2.08887 22 6.5319 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 11.947 2.00041 11.8941 2.00123 11.8413H2V4ZM7.57373 9.78713C7.57373 9.10809 8.1242 8.55762 8.80324 8.55762C9.48228 8.55762 10.0327 9.10809 10.0327 9.78713V11.2625C10.0327 11.9416 9.48228 12.492 8.80324 12.492C8.1242 12.492 7.57373 11.9416 7.57373 11.2625V9.78713ZM13.9673 9.78713C13.9673 9.10809 14.5178 8.55762 15.1968 8.55762C15.8758 8.55762 16.4263 9.10809 16.4263 9.78713V11.2625C16.4263 11.9416 15.8758 12.492 15.1968 12.492C14.5178 12.492 13.9673 11.9416 13.9673 11.2625V9.78713Z" fill="url(#paint0_linear_460_34351)"/>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="paint0_linear_460_34351" x1="2" y1="12" x2="22.022" y2="12" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#2A33FF"/>
|
||||||
|
<stop offset="0.997219" stop-color="#7963FF"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.0 KiB |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M2 4C2 2.89543 2.89543 2 4 2H12H12.1639V2.00132C17.6112 2.08887 22 6.5319 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 11.947 2.00041 11.8941 2.00123 11.8413H2V4ZM7.57373 9.78713C7.57373 9.10809 8.1242 8.55762 8.80324 8.55762C9.48228 8.55762 10.0327 9.10809 10.0327 9.78713V11.2625C10.0327 11.9416 9.48228 12.492 8.80324 12.492C8.1242 12.492 7.57373 11.9416 7.57373 11.2625V9.78713ZM13.9673 9.78713C13.9673 9.10809 14.5178 8.55762 15.1968 8.55762C15.8758 8.55762 16.4263 9.10809 16.4263 9.78713V11.2625C16.4263 11.9416 15.8758 12.492 15.1968 12.492C14.5178 12.492 13.9673 11.9416 13.9673 11.2625V9.78713Z" fill="#A5A5B3"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 791 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="9" height="9" viewBox="0 0 9 9" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.37893 2.95457C7.26177 2.83741 7.07182 2.83741 6.95466 2.95457L4.5237 5.38553C4.51068 5.39855 4.48958 5.39855 4.47656 5.38553L2.0456 2.95457C1.92844 2.83741 1.73849 2.83741 1.62133 2.95457C1.50417 3.07172 1.50417 3.26167 1.62133 3.37883L4.0523 5.8098C4.29963 6.05713 4.70063 6.05713 4.94796 5.8098L7.37893 3.37883C7.49609 3.26167 7.49609 3.07172 7.37893 2.95457Z" fill="#88889A"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 531 B |
|
@ -0,0 +1,9 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M12 22C6.47727 22 2 17.5227 2 12C2 6.47727 6.47727 2 12 2C17.5227 2 22 6.47727 22 12C22 17.5227 17.5227 22 12 22ZM10.5036 10.0409C10.3013 10.1327 10.1397 10.2953 10.0491 10.4982L7.70364 15.7555C7.66577 15.8399 7.65455 15.9338 7.67148 16.0247C7.68841 16.1157 7.73269 16.1993 7.79839 16.2644C7.8641 16.3295 7.94811 16.373 8.0392 16.3891C8.13029 16.4052 8.22413 16.3932 8.30818 16.3545L13.5291 13.9664C13.7322 13.8735 13.894 13.7091 13.9836 13.5045L16.2655 8.29455C16.3024 8.21026 16.313 8.11673 16.2956 8.02634C16.2783 7.93594 16.234 7.85293 16.1684 7.78829C16.1029 7.72365 16.0193 7.68043 15.9287 7.66434C15.8381 7.64825 15.7447 7.66005 15.6609 7.69818L10.5036 10.0409Z" fill="url(#paint0_linear_496_51074)"/>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="paint0_linear_496_51074" x1="12" y1="2" x2="12" y2="22" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#E5E6FF"/>
|
||||||
|
<stop offset="1" stop-color="white"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.0 KiB |
|
@ -0,0 +1,7 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<mask id="path-1-inside-1_460_34354" fill="white">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.0001 21.1001C6.97444 21.1001 2.9001 17.0258 2.9001 12.0001C2.9001 6.97444 6.97444 2.9001 12.0001 2.9001C17.0258 2.9001 21.1001 6.97444 21.1001 12.0001C21.1001 17.0258 17.0258 21.1001 12.0001 21.1001ZM12.0001 22.9001C5.98032 22.9001 1.1001 18.0199 1.1001 12.0001C1.1001 5.98032 5.98032 1.1001 12.0001 1.1001C18.0199 1.1001 22.9001 5.98032 22.9001 12.0001C22.9001 18.0199 18.0199 22.9001 12.0001 22.9001ZM10.6296 10.1822C10.4423 10.2662 10.2926 10.4151 10.2087 10.6008L8.03701 15.4136C8.00194 15.4909 7.99155 15.5769 8.00723 15.6602C8.02291 15.7434 8.06391 15.82 8.12475 15.8796C8.18559 15.9392 8.26337 15.979 8.34772 15.9938C8.43206 16.0085 8.51895 15.9975 8.59677 15.9621L13.431 13.7758C13.619 13.6908 13.7688 13.5403 13.8518 13.353L15.9646 8.58346C15.9989 8.5063 16.0086 8.42068 15.9926 8.33793C15.9766 8.25517 15.9355 8.17918 15.8748 8.12C15.8141 8.06083 15.7367 8.02126 15.6528 8.00653C15.5689 7.9918 15.4824 8.0026 15.4049 8.03751L10.6296 10.1822Z"/>
|
||||||
|
</mask>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.0001 21.1001C6.97444 21.1001 2.9001 17.0258 2.9001 12.0001C2.9001 6.97444 6.97444 2.9001 12.0001 2.9001C17.0258 2.9001 21.1001 6.97444 21.1001 12.0001C21.1001 17.0258 17.0258 21.1001 12.0001 21.1001ZM12.0001 22.9001C5.98032 22.9001 1.1001 18.0199 1.1001 12.0001C1.1001 5.98032 5.98032 1.1001 12.0001 1.1001C18.0199 1.1001 22.9001 5.98032 22.9001 12.0001C22.9001 18.0199 18.0199 22.9001 12.0001 22.9001ZM10.6296 10.1822C10.4423 10.2662 10.2926 10.4151 10.2087 10.6008L8.03701 15.4136C8.00194 15.4909 7.99155 15.5769 8.00723 15.6602C8.02291 15.7434 8.06391 15.82 8.12475 15.8796C8.18559 15.9392 8.26337 15.979 8.34772 15.9938C8.43206 16.0085 8.51895 15.9975 8.59677 15.9621L13.431 13.7758C13.619 13.6908 13.7688 13.5403 13.8518 13.353L15.9646 8.58346C15.9989 8.5063 16.0086 8.42068 15.9926 8.33793C15.9766 8.25517 15.9355 8.17918 15.8748 8.12C15.8141 8.06083 15.7367 8.02126 15.6528 8.00653C15.5689 7.9918 15.4824 8.0026 15.4049 8.03751L10.6296 10.1822Z" fill="#A5A5B3"/>
|
||||||
|
<path d="M10.2087 10.6008L8.56823 9.86003L8.56803 9.86047L10.2087 10.6008ZM10.6296 10.1822L11.3662 11.8246L11.3671 11.8242L10.6296 10.1822ZM8.03701 15.4136L9.67611 16.1575L9.67771 16.154L8.03701 15.4136ZM8.00723 15.6602L9.77614 15.3271L9.77614 15.3271L8.00723 15.6602ZM8.12475 15.8796L6.86502 17.1653L6.86503 17.1653L8.12475 15.8796ZM8.34772 15.9938L8.6577 14.2207L8.65767 14.2207L8.34772 15.9938ZM8.59677 15.9621L7.85504 14.322L7.85205 14.3234L8.59677 15.9621ZM13.431 13.7758L12.6894 12.1357L12.6892 12.1357L13.431 13.7758ZM13.8518 13.353L15.4974 14.0825L15.4976 14.0821L13.8518 13.353ZM15.9646 8.58346L14.3194 7.85319L14.3189 7.85443L15.9646 8.58346ZM15.4049 8.03751L16.1423 9.67951L16.1436 9.67893L15.4049 8.03751ZM1.1001 12.0001C1.1001 18.0199 5.98032 22.9001 12.0001 22.9001V19.3001C7.96855 19.3001 4.7001 16.0316 4.7001 12.0001H1.1001ZM12.0001 1.1001C5.98032 1.1001 1.1001 5.98032 1.1001 12.0001H4.7001C4.7001 7.96855 7.96855 4.7001 12.0001 4.7001V1.1001ZM22.9001 12.0001C22.9001 5.98032 18.0199 1.1001 12.0001 1.1001V4.7001C16.0316 4.7001 19.3001 7.96855 19.3001 12.0001H22.9001ZM12.0001 22.9001C18.0199 22.9001 22.9001 18.0199 22.9001 12.0001H19.3001C19.3001 16.0316 16.0316 19.3001 12.0001 19.3001V22.9001ZM-0.699902 12.0001C-0.699902 19.014 4.98621 24.7001 12.0001 24.7001V21.1001C6.97444 21.1001 2.9001 17.0258 2.9001 12.0001H-0.699902ZM12.0001 -0.699902C4.98621 -0.699902 -0.699902 4.98621 -0.699902 12.0001H2.9001C2.9001 6.97444 6.97444 2.9001 12.0001 2.9001V-0.699902ZM24.7001 12.0001C24.7001 4.98621 19.014 -0.699902 12.0001 -0.699902V2.9001C17.0258 2.9001 21.1001 6.97444 21.1001 12.0001H24.7001ZM12.0001 24.7001C19.014 24.7001 24.7001 19.014 24.7001 12.0001H21.1001C21.1001 17.0258 17.0258 21.1001 12.0001 21.1001V24.7001ZM11.8492 11.3416C11.7506 11.5601 11.5769 11.7301 11.3662 11.8246L9.89297 8.53983C9.30769 8.80234 8.83461 9.27013 8.56823 9.86003L11.8492 11.3416ZM9.67771 16.154L11.8494 11.3412L8.56803 9.86047L6.39631 14.6733L9.67771 16.154ZM9.77614 15.3271C9.82909 15.6082 9.79377 15.8983 9.6761 16.1575L6.39791 14.6698C6.21011 15.0836 6.15402 15.5456 6.23832 15.9933L9.77614 15.3271ZM9.38447 14.5938C9.58536 14.7907 9.72322 15.046 9.77614 15.3271L6.23832 15.9933C6.3226 16.4408 6.54246 16.8493 6.86502 17.1653L9.38447 14.5938ZM8.65767 14.2207C8.92992 14.2682 9.18384 14.3973 9.38446 14.5938L6.86503 17.1653C7.18733 17.4811 7.59683 17.6898 8.03777 17.7669L8.65767 14.2207ZM7.85205 14.3234C8.10481 14.2085 8.38558 14.1731 8.6577 14.2207L8.03773 17.7669C8.47854 17.8439 8.93308 17.7864 9.3415 17.6008L7.85205 14.3234ZM12.6892 12.1357L7.85504 14.322L9.33851 17.6022L14.1727 15.4159L12.6892 12.1357ZM12.2063 12.6236C12.304 12.4031 12.4778 12.2313 12.6894 12.1357L14.1726 15.4159C14.7602 15.1502 15.2337 14.6774 15.4974 14.0825L12.2063 12.6236ZM14.3189 7.85443L12.2061 12.624L15.4976 14.0821L17.6104 9.31249L14.3189 7.85443ZM14.2255 8.68043C14.1713 8.401 14.2045 8.11222 14.3194 7.85319L17.6098 9.31374C17.7933 8.90039 17.846 8.44037 17.7597 7.99543L14.2255 8.68043ZM14.618 9.4086C14.4177 9.21323 14.2796 8.95973 14.2255 8.68043L17.7597 7.99543C17.6735 7.55061 17.4533 7.14512 17.1316 6.83141L14.618 9.4086ZM15.3416 9.77942C15.0708 9.73188 14.8181 9.60373 14.618 9.4086L17.1316 6.83141C16.8102 6.51793 16.4027 6.31063 15.964 6.23364L15.3416 9.77942ZM16.1436 9.67893C15.8917 9.79231 15.6123 9.82694 15.3416 9.77942L15.964 6.23364C15.5255 6.15666 15.0732 6.2129 14.6661 6.39609L16.1436 9.67893ZM11.3671 11.8242L16.1423 9.67951L14.6674 6.39552L9.89215 8.5402L11.3671 11.8242Z" fill="#A5A5B3" mask="url(#path-1-inside-1_460_34354)"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 5.6 KiB |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.0303 2.80317C13.3232 3.09606 13.3232 3.57093 13.0303 3.86383L6.95292 9.94124C6.92038 9.97378 6.92038 10.0265 6.95292 10.0591L13.0303 16.1365C13.3232 16.4294 13.3232 16.9043 13.0303 17.1972C12.7374 17.4901 12.2626 17.4901 11.9697 17.1972L5.89226 11.1197C5.27393 10.5014 5.27393 9.49891 5.89226 8.88058L11.9697 2.80317C12.2626 2.51027 12.7374 2.51027 13.0303 2.80317Z" fill="#606078"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 540 B |
|
@ -0,0 +1,9 @@
|
||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.76848 17.7057C7.23852 17.9053 7.74631 17.6097 8.0672 17.2124C8.52265 16.6486 9.21926 16.288 10 16.288C10.7807 16.288 11.4774 16.6486 11.9328 17.2124C12.2537 17.6097 12.7615 17.9053 13.2315 17.7057C14.0061 17.3768 14.7202 16.9332 15.3528 16.3962C15.7186 16.0857 15.7116 15.5429 15.5422 15.094C15.4392 14.8208 15.3828 14.5247 15.3828 14.2154C15.3828 13.0558 16.2086 12.0782 17.2746 11.8048C17.7398 11.6855 18.192 11.383 18.2398 10.9051C18.2672 10.6306 18.2812 10.3521 18.2812 10.0702C18.2812 9.44766 18.2127 8.84109 18.0827 8.2577C17.9926 7.85301 17.6101 7.60594 17.2115 7.49187C16.007 7.1472 15.2251 5.88212 15.4254 4.65919C15.4923 4.25081 15.4335 3.80009 15.1081 3.54445C14.4573 3.03323 13.7282 2.61746 12.9417 2.31819C12.5252 2.1597 12.0809 2.38084 11.768 2.69814C11.3175 3.15489 10.6918 3.43795 10 3.43795C9.30822 3.43795 8.68246 3.15489 8.23202 2.69814C7.91911 2.38084 7.47476 2.1597 7.05826 2.31819C6.27178 2.61746 5.54265 3.03323 4.89191 3.54445C4.5665 3.80009 4.508 4.25085 4.57388 4.65939C4.77111 5.8825 3.97642 7.14178 2.78809 7.48944C2.39017 7.60586 2.00743 7.853 1.91727 8.25769C1.78731 8.84108 1.71875 9.44765 1.71875 10.0702C1.71875 10.3521 1.73279 10.6306 1.76022 10.9051C1.80795 11.383 2.26081 11.6825 2.72602 11.8018C4.12149 12.1598 4.93356 13.7537 4.4533 15.0913C4.29116 15.5428 4.28143 16.0857 4.64721 16.3962C5.27978 16.9332 5.99395 17.3768 6.76848 17.7057ZM13.2031 10.0001C13.2031 11.7691 11.769 13.2032 10 13.2032C8.23096 13.2032 6.79688 11.7691 6.79688 10.0001C6.79688 8.23104 8.23096 6.79695 10 6.79695C11.769 6.79695 13.2031 8.23104 13.2031 10.0001Z" fill="url(#paint0_linear_460_42476)"/>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="paint0_linear_460_42476" x1="10" y1="2.26562" x2="10" y2="17.7697" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#E5E6FF"/>
|
||||||
|
<stop offset="1" stop-color="white"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.9 KiB |
|
@ -0,0 +1,4 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.0001 9.9001C10.8403 9.9001 9.9001 10.8403 9.9001 12.0001C9.9001 13.1599 10.8403 14.1001 12.0001 14.1001C13.1599 14.1001 14.1001 13.1599 14.1001 12.0001C14.1001 10.8403 13.1599 9.9001 12.0001 9.9001ZM8.1001 12.0001C8.1001 9.84619 9.84619 8.1001 12.0001 8.1001C14.154 8.1001 15.9001 9.84619 15.9001 12.0001C15.9001 14.154 14.154 15.9001 12.0001 15.9001C9.84619 15.9001 8.1001 14.154 8.1001 12.0001Z" fill="#A5A5B3"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.5358 3.84629L14.429 4.03081C13.8781 4.98117 13.0302 5.65737 12.0013 5.65737C10.9718 5.65737 10.1263 4.98074 9.58028 4.02919L9.47439 3.84628C9.24726 3.46903 8.77471 3.35841 8.43026 3.56334L8.41711 3.57101L6.68711 4.56101C6.21226 4.83235 6.04765 5.44933 6.31938 5.91962L5.5401 6.36987L6.31875 5.91855C6.86983 6.86931 7.03394 7.94095 6.51988 8.83299C6.00591 9.72489 4.99685 10.1199 3.9001 10.1199C3.35023 10.1199 2.9001 10.5738 2.9001 11.1199V12.8799C2.9001 13.4259 3.35023 13.8799 3.9001 13.8799C4.99685 13.8799 6.00591 14.2748 6.51988 15.1667C7.03379 16.0585 6.86993 17.1298 6.31923 18.0804C6.04771 18.5506 6.21186 19.1672 6.68662 19.4384L8.41711 20.4287L8.43026 20.4364C8.7747 20.6413 9.24723 20.5307 9.47436 20.1535L9.58121 19.9689C10.1321 19.0186 10.98 18.3424 12.0088 18.3424C13.0384 18.3424 13.8838 19.019 14.4299 19.9704C14.4302 19.9711 14.4306 19.9717 14.4309 19.9723L14.5358 20.1535C14.7629 20.5307 15.2355 20.6413 15.5799 20.4364L15.5931 20.4287L17.3231 19.4387C17.7969 19.168 17.9647 18.5595 17.6887 18.0764L18.4701 17.6299L17.6914 18.0812C17.1404 17.1304 16.9763 16.0588 17.4903 15.1667C18.0043 14.2748 19.0134 13.8799 20.1101 13.8799C20.66 13.8799 21.1101 13.4259 21.1101 12.8799V11.1199C21.1101 10.57 20.6561 10.1199 20.1101 10.1199C19.0134 10.1199 18.0043 9.72489 17.4903 8.83299C16.9764 7.94123 17.1403 6.86998 17.6909 5.91945C17.9625 5.44918 17.7984 4.8326 17.3236 4.56129L15.5931 3.57101L15.5799 3.56334C15.2355 3.35841 14.7629 3.46903 14.5358 3.84629ZM16.493 2.01211C15.2597 1.28376 13.699 1.73233 12.9866 2.92971L12.8714 3.12855C12.8714 3.12855 12.8714 3.12855 12.8714 3.12855C12.5123 3.74817 12.165 3.85737 12.0013 3.85737C11.839 3.85737 11.4949 3.74982 11.1409 3.13228L11.029 2.93894L11.0236 2.92971C10.3112 1.73233 8.75046 1.28376 7.51717 2.01211L5.79357 2.99845C4.4488 3.76725 3.99263 5.49057 4.76082 6.82012L4.76144 6.82119C5.12036 7.44043 5.04125 7.7938 4.96031 7.93425C4.87929 8.07485 4.61335 8.31987 3.9001 8.31987C2.34997 8.31987 1.1001 9.5859 1.1001 11.1199V12.8799C1.1001 14.4138 2.34996 15.6799 3.9001 15.6799C4.61335 15.6799 4.87929 15.9249 4.96031 16.0655C5.04125 16.2059 5.12036 16.5593 4.76144 17.1785L4.76082 17.1796C3.99254 18.5093 4.44842 20.2326 5.79357 21.0013L7.5172 21.9876C8.75048 22.716 10.3112 22.2674 11.0236 21.07L11.1388 20.8712C11.1387 20.8713 11.1388 20.8711 11.1388 20.8712C11.4978 20.2519 11.8452 20.1424 12.0088 20.1424C12.1711 20.1424 12.5153 20.2499 12.8693 20.8675L12.8712 20.8708L12.9812 21.0608L12.9866 21.07C13.699 22.2674 15.2597 22.716 16.493 21.9876L18.2166 21.0013C19.5628 20.232 20.0155 18.5202 19.2515 17.1833L19.2488 17.1785C18.8898 16.5593 18.9689 16.2059 19.0499 16.0655C19.1309 15.9249 19.3968 15.6799 20.1101 15.6799C21.6602 15.6799 22.9101 14.4138 22.9101 12.8799V11.1199C22.9101 9.56974 21.6441 8.31987 20.1101 8.31987C19.3968 8.31987 19.1309 8.07485 19.0499 7.93425C18.9689 7.79379 18.8898 7.44043 19.2488 6.82119L19.2494 6.82012C20.0176 5.49057 19.5619 3.76753 18.2171 2.99873L16.493 2.01211Z" fill="#A5A5B3"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 3.5 KiB |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.79967 8.92015L12.3273 6.23188C12.8372 7.27337 13.9075 7.99055 15.1453 7.99055C16.8772 7.99055 18.2813 6.58655 18.2813 4.85465C18.2813 3.12274 16.8772 1.71875 15.1453 1.71875C13.5001 1.71875 12.1508 2.98574 12.0199 4.59724L6.9098 7.63135C6.35941 7.1534 5.64081 6.8641 4.85465 6.8641C3.12275 6.8641 1.71875 8.2681 1.71875 10C1.71875 11.7319 3.12275 13.1359 4.85465 13.1359C5.64081 13.1359 6.35941 12.8466 6.9098 12.3687L12.0199 15.4028C12.1508 17.0143 13.5001 18.2813 15.1453 18.2813C16.8772 18.2813 18.2813 16.8772 18.2813 15.1453C18.2813 13.4135 16.8772 12.0094 15.1453 12.0094C13.9075 12.0094 12.8372 12.7266 12.3273 13.7681L7.79967 11.0798C7.92315 10.7432 7.99055 10.3795 7.99055 10C7.99055 9.62054 7.92315 9.25681 7.79967 8.92015ZM15.1453 3.28125C14.2764 3.28125 13.5719 3.98569 13.5719 4.85465C13.5719 5.72361 14.2764 6.42805 15.1453 6.42805C16.0143 6.42805 16.7188 5.72361 16.7188 4.85465C16.7188 3.98569 16.0143 3.28125 15.1453 3.28125ZM4.85465 8.4266C3.9857 8.4266 3.28125 9.13105 3.28125 10C3.28125 10.869 3.9857 11.5734 4.85465 11.5734C5.72361 11.5734 6.42805 10.869 6.42805 10C6.42805 9.13105 5.72361 8.4266 4.85465 8.4266ZM13.5719 15.1453C13.5719 14.2764 14.2764 13.5719 15.1453 13.5719C16.0143 13.5719 16.7188 14.2764 16.7188 15.1453C16.7188 16.0143 16.0143 16.7188 15.1453 16.7188C14.2764 16.7188 13.5719 16.0143 13.5719 15.1453Z" fill="#606078"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
|
@ -1,5 +1,5 @@
|
||||||
/* eslint-disable @next/next/no-page-custom-font */
|
/* eslint-disable @next/next/no-page-custom-font */
|
||||||
import "./styles/globals.scss";
|
// import "./styles/globals.scss";
|
||||||
import "./styles/markdown.scss";
|
import "./styles/markdown.scss";
|
||||||
import "./styles/highlight.scss";
|
import "./styles/highlight.scss";
|
||||||
import "./styles/globals.css";
|
import "./styles/globals.css";
|
||||||
|
@ -38,7 +38,10 @@ export default function RootLayout({
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta name="config" content={JSON.stringify(getClientConfig())} />
|
<meta name="config" content={JSON.stringify(getClientConfig())} />
|
||||||
{/* <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> */}
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||||
|
/>
|
||||||
<link rel="manifest" href="/site.webmanifest"></link>
|
<link rel="manifest" href="/site.webmanifest"></link>
|
||||||
<script src="/serviceWorkerRegister.js" defer></script>
|
<script src="/serviceWorkerRegister.js" defer></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Analytics } from "@vercel/analytics/react";
|
import { Analytics } from "@vercel/analytics/react";
|
||||||
|
|
||||||
import { Home } from "./components/home";
|
import Home from "@/app/containers";
|
||||||
|
|
||||||
import { getServerSideConfig } from "./config/server";
|
import { getServerSideConfig } from "./config/server";
|
||||||
|
|
||||||
|
|
|
@ -3,3 +3,23 @@
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
@font-face {
|
||||||
|
font-family: "Satoshi Variable";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
font-feature-settings:
|
||||||
|
"clig" off,
|
||||||
|
"liga" off;
|
||||||
|
src: url(/fonts/Roboto.woff2) format("woff2");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
|
@ -61,7 +61,7 @@
|
||||||
|
|
||||||
--window-width: 90vw;
|
--window-width: 90vw;
|
||||||
--window-height: 90vh;
|
--window-height: 90vh;
|
||||||
--sidebar-width: 300px;
|
// --sidebar-width: 300px;
|
||||||
--window-content-width: calc(100% - var(--sidebar-width));
|
--window-content-width: calc(100% - var(--sidebar-width));
|
||||||
--message-max-width: 80%;
|
--message-max-width: 80%;
|
||||||
--full-height: 100%;
|
--full-height: 100%;
|
||||||
|
@ -71,7 +71,7 @@
|
||||||
:root {
|
:root {
|
||||||
--window-width: 100vw;
|
--window-width: 100vw;
|
||||||
--window-height: var(--full-height);
|
--window-height: var(--full-height);
|
||||||
--sidebar-width: 100vw;
|
// --sidebar-width: 100vw;
|
||||||
--window-content-width: var(--window-width);
|
--window-content-width: var(--window-width);
|
||||||
--message-max-width: 100%;
|
--message-max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,8 @@ module.exports = {
|
||||||
sm: '0.75rem',
|
sm: '0.75rem',
|
||||||
'sm-mobile': '0.875rem',
|
'sm-mobile': '0.875rem',
|
||||||
'sm-title': '0.875rem',
|
'sm-title': '0.875rem',
|
||||||
|
'sm-mobile-tab': '0.625rem',
|
||||||
|
'chat-header-title': '1rem',
|
||||||
},
|
},
|
||||||
screens: {
|
screens: {
|
||||||
sm: '480px',
|
sm: '480px',
|
||||||
|
@ -25,6 +27,30 @@ module.exports = {
|
||||||
'md': '15rem',
|
'md': '15rem',
|
||||||
'lg': '21.25rem',
|
'lg': '21.25rem',
|
||||||
'2xl': '27.5rem',
|
'2xl': '27.5rem',
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
mobile: '3.125rem',
|
||||||
|
'menu-title-mobile': '3rem',
|
||||||
|
},
|
||||||
|
flexBasis: {
|
||||||
|
'sidebar': 'var(--sidebar-width)',
|
||||||
|
'page': 'calc(100%-var(--sidebar-width))',
|
||||||
|
},
|
||||||
|
spacing: {
|
||||||
|
'chat-header-gap': '0.625rem',
|
||||||
|
},
|
||||||
|
backgroundImage: {
|
||||||
|
'message-bg': 'linear-gradient(259deg, #9786FF 8.42%, #4A5CFF 90.13%)',
|
||||||
|
},
|
||||||
|
transitionProperty: {
|
||||||
|
'time': 'all ease 0.6s',
|
||||||
|
'message': 'all ease 0.3s',
|
||||||
|
},
|
||||||
|
maxWidth: {
|
||||||
|
'message-width': 'var(--max-message-width, 70%)'
|
||||||
|
},
|
||||||
|
backgroundColor: {
|
||||||
|
'select-btn': 'rgba(0, 0, 0, 0.05)',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
borderRadius: {
|
borderRadius: {
|
||||||
|
@ -33,6 +59,12 @@ module.exports = {
|
||||||
DEFAULT: '0.25rem',
|
DEFAULT: '0.25rem',
|
||||||
'md': '0.75rem',
|
'md': '0.75rem',
|
||||||
'lg': '1rem',
|
'lg': '1rem',
|
||||||
|
'message': '16px 4px 16px 16px',
|
||||||
|
'action-btn': '0.5rem',
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
'common': ['Satoshi Variable', 'Variable'],
|
||||||
|
'time': ['Hind', 'Variable']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [],
|
||||||
|
|