Compare commits
	
		
			34 Commits
		
	
	
		
			bestsanmao
			...
			v3
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					00b1a9781d | ||
| 
						 | 
					240d330001 | ||
| 
						 | 
					4e4431339f | ||
| 
						 | 
					fa2f8c66d1 | ||
| 
						 | 
					32f62d70af | ||
| 
						 | 
					68f0fa917f | ||
| 
						 | 
					8a14cb19a9 | ||
| 
						 | 
					3d99965a8f | ||
| 
						 | 
					4d5a9476b6 | ||
| 
						 | 
					15d6ed252f | ||
| 
						 | 
					ecf6cc27d6 | ||
| 
						 | 
					cadd2558fd | ||
| 
						 | 
					c3d91bf0cd | ||
| 
						 | 
					996537d262 | ||
| 
						 | 
					5ea6206319 | ||
| 
						 | 
					8c28c408d8 | ||
| 
						 | 
					c34b8ab919 | ||
| 
						 | 
					9f4813326c | ||
| 
						 | 
					9569888b0e | ||
| 
						 | 
					1a636b0f50 | ||
| 
						 | 
					48e8c0a194 | ||
| 
						 | 
					59583e53bd | ||
| 
						 | 
					bb7422c526 | ||
| 
						 | 
					c99086447e | ||
| 
						 | 
					f7074bba8c | ||
| 
						 | 
					4400392c0c | ||
| 
						 | 
					4a5465f884 | ||
| 
						 | 
					37cc87531c | ||
| 
						 | 
					1074fffe79 | ||
| 
						 | 
					3d0a98d5d2 | ||
| 
						 | 
					b3559f99a2 | ||
| 
						 | 
					51a1d9f92a | ||
| 
						 | 
					3fc9b91bf1 | ||
| 
						 | 
					0a8e5d6734 | 
@@ -1,4 +1,12 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  "extends": "next/core-web-vitals",
 | 
					  "extends": "next/core-web-vitals",
 | 
				
			||||||
  "plugins": ["prettier"]
 | 
					  "plugins": [
 | 
				
			||||||
 | 
					    "prettier"
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					  "parserOptions": {
 | 
				
			||||||
 | 
					    "ecmaFeatures": {
 | 
				
			||||||
 | 
					      "legacyDecorators": true
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "ignorePatterns": ["globals.css"]
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										123
									
								
								app/components/ActionsBar/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,123 @@
 | 
				
			|||||||
 | 
					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;
 | 
				
			||||||
 | 
					  onClick?: () => void;
 | 
				
			||||||
 | 
					  activeClassName?: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Groups = {
 | 
				
			||||||
 | 
					  normal: string[][];
 | 
				
			||||||
 | 
					  mobile: string[][];
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface ActionsBarProps {
 | 
				
			||||||
 | 
					  actionsShema: Action[];
 | 
				
			||||||
 | 
					  onSelect?: (id: string) => void;
 | 
				
			||||||
 | 
					  selected?: string;
 | 
				
			||||||
 | 
					  groups: string[][] | Groups;
 | 
				
			||||||
 | 
					  className?: string;
 | 
				
			||||||
 | 
					  inMobile?: boolean;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function ActionsBar(props: ActionsBarProps) {
 | 
				
			||||||
 | 
					  const { actionsShema, onSelect, selected, groups, className, inMobile } =
 | 
				
			||||||
 | 
					    props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handlerClick =
 | 
				
			||||||
 | 
					    (action: Action) => (e: { preventDefault: () => void }) => {
 | 
				
			||||||
 | 
					      e.preventDefault();
 | 
				
			||||||
 | 
					      if (action.onClick) {
 | 
				
			||||||
 | 
					        action.onClick();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (selected !== action.id) {
 | 
				
			||||||
 | 
					        onSelect?.(action.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={` cursor-pointer shrink-1 grow-0 basis-[${
 | 
				
			||||||
 | 
					                (100 - 1) / arr.length
 | 
				
			||||||
 | 
					              }%] flex flex-col items-center justify-around gap-0.5 py-1.5
 | 
				
			||||||
 | 
					                        ${
 | 
				
			||||||
 | 
					                          selected === action.id
 | 
				
			||||||
 | 
					                            ? "text-text-sidebar-tab-mobile-active"
 | 
				
			||||||
 | 
					                            : "text-text-sidebar-tab-mobile-inactive"
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    `}
 | 
				
			||||||
 | 
					              onClick={handlerClick(action)}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              {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={`cursor-pointer p-3 ${
 | 
				
			||||||
 | 
					              selected === action.id
 | 
				
			||||||
 | 
					                ? `!bg-actions-bar-btn-default ${action.activeClassName}`
 | 
				
			||||||
 | 
					                : "bg-transparent"
 | 
				
			||||||
 | 
					            } rounded-md items-center ${
 | 
				
			||||||
 | 
					              action.className
 | 
				
			||||||
 | 
					            } transition duration-300 ease-in-out`}
 | 
				
			||||||
 | 
					            onClick={handlerClick(action)}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            {selected === action.id ? activeIcon : inactiveIcon}
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    if (ind < arr.length - 1) {
 | 
				
			||||||
 | 
					      res.push(<div key={String(ind)} className=" flex-1"></div>);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return res;
 | 
				
			||||||
 | 
					  }, [] as JSX.Element[]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return <div className={`flex items-center ${className} `}>{content}</div>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										78
									
								
								app/components/Btn/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,78 @@
 | 
				
			|||||||
 | 
					import * as React from "react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type ButtonType = "primary" | "danger" | null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface BtnProps {
 | 
				
			||||||
 | 
					  onClick?: () => void;
 | 
				
			||||||
 | 
					  icon?: JSX.Element;
 | 
				
			||||||
 | 
					  prefixIcon?: JSX.Element;
 | 
				
			||||||
 | 
					  type?: ButtonType;
 | 
				
			||||||
 | 
					  text?: React.ReactNode;
 | 
				
			||||||
 | 
					  bordered?: boolean;
 | 
				
			||||||
 | 
					  shadow?: boolean;
 | 
				
			||||||
 | 
					  className?: string;
 | 
				
			||||||
 | 
					  title?: string;
 | 
				
			||||||
 | 
					  disabled?: boolean;
 | 
				
			||||||
 | 
					  tabIndex?: number;
 | 
				
			||||||
 | 
					  autoFocus?: boolean;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function Btn(props: BtnProps) {
 | 
				
			||||||
 | 
					  const {
 | 
				
			||||||
 | 
					    onClick,
 | 
				
			||||||
 | 
					    icon,
 | 
				
			||||||
 | 
					    type,
 | 
				
			||||||
 | 
					    text,
 | 
				
			||||||
 | 
					    className,
 | 
				
			||||||
 | 
					    title,
 | 
				
			||||||
 | 
					    disabled,
 | 
				
			||||||
 | 
					    tabIndex,
 | 
				
			||||||
 | 
					    autoFocus,
 | 
				
			||||||
 | 
					    prefixIcon,
 | 
				
			||||||
 | 
					  } = props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let btnClassName;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  switch (type) {
 | 
				
			||||||
 | 
					    case "primary":
 | 
				
			||||||
 | 
					      btnClassName = `${
 | 
				
			||||||
 | 
					        disabled
 | 
				
			||||||
 | 
					          ? "bg-primary-btn-disabled dark:opacity-30 dark:text-primary-btn-disabled-dark"
 | 
				
			||||||
 | 
					          : "bg-primary-btn shadow-btn"
 | 
				
			||||||
 | 
					      } text-text-btn-primary `;
 | 
				
			||||||
 | 
					      break;
 | 
				
			||||||
 | 
					    case "danger":
 | 
				
			||||||
 | 
					      btnClassName = `bg-danger-btn text-text-btn-danger hover:bg-hovered-danger-btn`;
 | 
				
			||||||
 | 
					      break;
 | 
				
			||||||
 | 
					    default:
 | 
				
			||||||
 | 
					      btnClassName = `bg-default-btn text-text-btn-default hover:bg-hovered-btn`;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <button
 | 
				
			||||||
 | 
					      className={`
 | 
				
			||||||
 | 
					        ${className ?? ""} 
 | 
				
			||||||
 | 
					        py-2 px-3 flex items-center justify-center gap-1 rounded-action-btn transition-all duration-300 select-none
 | 
				
			||||||
 | 
					        ${disabled ? "cursor-not-allowed" : "cursor-pointer"}
 | 
				
			||||||
 | 
					        ${btnClassName} 
 | 
				
			||||||
 | 
					        follow-parent-svg
 | 
				
			||||||
 | 
					      `}
 | 
				
			||||||
 | 
					      onClick={onClick}
 | 
				
			||||||
 | 
					      title={title}
 | 
				
			||||||
 | 
					      disabled={disabled}
 | 
				
			||||||
 | 
					      role="button"
 | 
				
			||||||
 | 
					      tabIndex={tabIndex}
 | 
				
			||||||
 | 
					      autoFocus={autoFocus}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      {prefixIcon && (
 | 
				
			||||||
 | 
					        <div className={`flex items-center justify-center`}>{prefixIcon}</div>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					      {text && (
 | 
				
			||||||
 | 
					        <div className={`font-common text-sm-title leading-4 line-clamp-1`}>
 | 
				
			||||||
 | 
					          {text}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					      {icon && <div className={`flex items-center justify-center`}>{icon}</div>}
 | 
				
			||||||
 | 
					    </button>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										32
									
								
								app/components/Card/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,32 @@
 | 
				
			|||||||
 | 
					import { ReactNode } from "react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface CardProps {
 | 
				
			||||||
 | 
					  className?: string;
 | 
				
			||||||
 | 
					  children?: ReactNode;
 | 
				
			||||||
 | 
					  title?: ReactNode;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function Card(props: CardProps) {
 | 
				
			||||||
 | 
					  const { className, children, title } = props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <>
 | 
				
			||||||
 | 
					      {title && (
 | 
				
			||||||
 | 
					        <div
 | 
				
			||||||
 | 
					          className={`
 | 
				
			||||||
 | 
					            capitalize !font-semibold text-sm-mobile font-weight-setting-card-title text-text-card-title
 | 
				
			||||||
 | 
					            mb-3
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            ml-3
 | 
				
			||||||
 | 
					            md:ml-4  
 | 
				
			||||||
 | 
					          `}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          {title}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					      <div className={`px-4 py-1 rounded-lg bg-card ${className}`}>
 | 
				
			||||||
 | 
					        {children}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										18
									
								
								app/components/GlobalLoading/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					import BotIcon from "@/app/icons/bot.svg";
 | 
				
			||||||
 | 
					import LoadingIcon from "@/app/icons/three-dots.svg";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function GloablLoading({
 | 
				
			||||||
 | 
					  noLogo,
 | 
				
			||||||
 | 
					}: {
 | 
				
			||||||
 | 
					  noLogo?: boolean;
 | 
				
			||||||
 | 
					  useSkeleton?: boolean;
 | 
				
			||||||
 | 
					}) {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div
 | 
				
			||||||
 | 
					      className={`flex flex-col justify-center items-center w-[100%] h-[100%]`}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      {!noLogo && <BotIcon />}
 | 
				
			||||||
 | 
					      <LoadingIcon />
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										39
									
								
								app/components/HoverPopover/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,39 @@
 | 
				
			|||||||
 | 
					import * as HoverCard from "@radix-ui/react-hover-card";
 | 
				
			||||||
 | 
					import { ComponentProps } from "react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface PopoverProps {
 | 
				
			||||||
 | 
					  content?: JSX.Element | string;
 | 
				
			||||||
 | 
					  children?: JSX.Element;
 | 
				
			||||||
 | 
					  arrowClassName?: string;
 | 
				
			||||||
 | 
					  popoverClassName?: string;
 | 
				
			||||||
 | 
					  noArrow?: boolean;
 | 
				
			||||||
 | 
					  align?: ComponentProps<typeof HoverCard.Content>["align"];
 | 
				
			||||||
 | 
					  openDelay?: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function HoverPopover(props: PopoverProps) {
 | 
				
			||||||
 | 
					  const {
 | 
				
			||||||
 | 
					    content,
 | 
				
			||||||
 | 
					    children,
 | 
				
			||||||
 | 
					    arrowClassName,
 | 
				
			||||||
 | 
					    popoverClassName,
 | 
				
			||||||
 | 
					    noArrow = false,
 | 
				
			||||||
 | 
					    align,
 | 
				
			||||||
 | 
					    openDelay = 300,
 | 
				
			||||||
 | 
					  } = props;
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <HoverCard.Root openDelay={openDelay}>
 | 
				
			||||||
 | 
					      <HoverCard.Trigger asChild>{children}</HoverCard.Trigger>
 | 
				
			||||||
 | 
					      <HoverCard.Portal>
 | 
				
			||||||
 | 
					        <HoverCard.Content
 | 
				
			||||||
 | 
					          className={`${popoverClassName}`}
 | 
				
			||||||
 | 
					          sideOffset={5}
 | 
				
			||||||
 | 
					          align={align}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          {content}
 | 
				
			||||||
 | 
					          {!noArrow && <HoverCard.Arrow className={`${arrowClassName}`} />}
 | 
				
			||||||
 | 
					        </HoverCard.Content>
 | 
				
			||||||
 | 
					      </HoverCard.Portal>
 | 
				
			||||||
 | 
					    </HoverCard.Root>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										42
									
								
								app/components/Imgs/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,42 @@
 | 
				
			|||||||
 | 
					import { CSSProperties } from "react";
 | 
				
			||||||
 | 
					import { getMessageImages } from "@/app/utils";
 | 
				
			||||||
 | 
					import { RequestMessage } from "@/app/client/api";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface ImgsProps {
 | 
				
			||||||
 | 
					  message: RequestMessage;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function Imgs(props: ImgsProps) {
 | 
				
			||||||
 | 
					  const { message } = props;
 | 
				
			||||||
 | 
					  const imgSrcs = getMessageImages(message);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (imgSrcs.length < 1) {
 | 
				
			||||||
 | 
					    return <></>;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const imgVars = {
 | 
				
			||||||
 | 
					    "--imgs-width": `calc(var(--max-message-width) - ${
 | 
				
			||||||
 | 
					      imgSrcs.length - 1
 | 
				
			||||||
 | 
					    }*0.25rem)`,
 | 
				
			||||||
 | 
					    "--img-width": `calc(var(--imgs-width)/ ${imgSrcs.length})`,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div
 | 
				
			||||||
 | 
					      className={`w-[100%] mt-[0.625rem] flex gap-1`}
 | 
				
			||||||
 | 
					      style={imgVars as CSSProperties}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      {imgSrcs.map((image, index) => {
 | 
				
			||||||
 | 
					        return (
 | 
				
			||||||
 | 
					          <div
 | 
				
			||||||
 | 
					            key={index}
 | 
				
			||||||
 | 
					            className="flex-1 min-w-[var(--img-width)] pb-[var(--img-width)] object-cover bg-cover bg-no-repeat bg-center box-border rounded-chat-img"
 | 
				
			||||||
 | 
					            style={{
 | 
				
			||||||
 | 
					              backgroundImage: `url(${image})`,
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      })}
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										88
									
								
								app/components/Input/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,88 @@
 | 
				
			|||||||
 | 
					import PasswordVisible from "@/app/icons/passwordVisible.svg";
 | 
				
			||||||
 | 
					import PasswordInvisible from "@/app/icons/passwordInvisible.svg";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  DetailedHTMLProps,
 | 
				
			||||||
 | 
					  InputHTMLAttributes,
 | 
				
			||||||
 | 
					  useContext,
 | 
				
			||||||
 | 
					  useLayoutEffect,
 | 
				
			||||||
 | 
					  useState,
 | 
				
			||||||
 | 
					} from "react";
 | 
				
			||||||
 | 
					import List, { ListContext } from "@/app/components/List";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface CommonInputProps
 | 
				
			||||||
 | 
					  extends Omit<
 | 
				
			||||||
 | 
					    DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>,
 | 
				
			||||||
 | 
					    "onChange" | "type" | "value"
 | 
				
			||||||
 | 
					  > {
 | 
				
			||||||
 | 
					  className?: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface NumberInputProps {
 | 
				
			||||||
 | 
					  onChange?: (v: number) => void;
 | 
				
			||||||
 | 
					  type?: "number";
 | 
				
			||||||
 | 
					  value?: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface TextInputProps {
 | 
				
			||||||
 | 
					  onChange?: (v: string) => void;
 | 
				
			||||||
 | 
					  type?: "text" | "password";
 | 
				
			||||||
 | 
					  value?: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface InputProps {
 | 
				
			||||||
 | 
					  onChange?: ((v: string) => void) | ((v: number) => void);
 | 
				
			||||||
 | 
					  type?: "text" | "password" | "number";
 | 
				
			||||||
 | 
					  value?: string | number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function Input(
 | 
				
			||||||
 | 
					  props: CommonInputProps & NumberInputProps,
 | 
				
			||||||
 | 
					): JSX.Element;
 | 
				
			||||||
 | 
					export default function Input(
 | 
				
			||||||
 | 
					  props: CommonInputProps & TextInputProps,
 | 
				
			||||||
 | 
					): JSX.Element;
 | 
				
			||||||
 | 
					export default function Input(props: CommonInputProps & InputProps) {
 | 
				
			||||||
 | 
					  const { value, type = "text", onChange, className, ...rest } = props;
 | 
				
			||||||
 | 
					  const [show, setShow] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const { inputClassName } = useContext(ListContext);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const internalType = (show && "text") || type;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const { update, handleValidate } = useContext(List.ListContext);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useLayoutEffect(() => {
 | 
				
			||||||
 | 
					    update?.({ type: "input" });
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useLayoutEffect(() => {
 | 
				
			||||||
 | 
					    handleValidate?.(value);
 | 
				
			||||||
 | 
					  }, [value]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div
 | 
				
			||||||
 | 
					      className={` group/input w-[100%] rounded-chat-input bg-input transition-colors duration-300 ease-in-out flex gap-3 items-center px-3 py-2 ${className} hover:bg-select-hover ${inputClassName}`}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <input
 | 
				
			||||||
 | 
					        {...rest}
 | 
				
			||||||
 | 
					        className=" overflow-hidden text-text-input text-sm-title leading-input outline-none flex-1 group-hover/input:bg-input-input-ele-hover"
 | 
				
			||||||
 | 
					        type={internalType}
 | 
				
			||||||
 | 
					        value={value}
 | 
				
			||||||
 | 
					        onChange={(e) => {
 | 
				
			||||||
 | 
					          if (type === "number") {
 | 
				
			||||||
 | 
					            const v = e.currentTarget.valueAsNumber;
 | 
				
			||||||
 | 
					            (onChange as NumberInputProps["onChange"])?.(v);
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            const v = e.currentTarget.value;
 | 
				
			||||||
 | 
					            (onChange as TextInputProps["onChange"])?.(v);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					      {type == "password" && (
 | 
				
			||||||
 | 
					        <div className=" cursor-pointer" onClick={() => setShow((pre) => !pre)}>
 | 
				
			||||||
 | 
					          {show ? <PasswordVisible /> : <PasswordInvisible />}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										157
									
								
								app/components/List/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,157 @@
 | 
				
			|||||||
 | 
					import {
 | 
				
			||||||
 | 
					  ReactNode,
 | 
				
			||||||
 | 
					  createContext,
 | 
				
			||||||
 | 
					  useCallback,
 | 
				
			||||||
 | 
					  useContext,
 | 
				
			||||||
 | 
					  useState,
 | 
				
			||||||
 | 
					} from "react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface WidgetStyle {
 | 
				
			||||||
 | 
					  selectClassName?: string;
 | 
				
			||||||
 | 
					  inputClassName?: string;
 | 
				
			||||||
 | 
					  rangeClassName?: string;
 | 
				
			||||||
 | 
					  switchClassName?: string;
 | 
				
			||||||
 | 
					  inputNextLine?: boolean;
 | 
				
			||||||
 | 
					  rangeNextLine?: boolean;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface ChildrenMeta {
 | 
				
			||||||
 | 
					  type?: "unknown" | "input" | "range";
 | 
				
			||||||
 | 
					  error?: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface ListProps {
 | 
				
			||||||
 | 
					  className?: string;
 | 
				
			||||||
 | 
					  children?: ReactNode;
 | 
				
			||||||
 | 
					  id?: string;
 | 
				
			||||||
 | 
					  isMobileScreen?: boolean;
 | 
				
			||||||
 | 
					  widgetStyle?: WidgetStyle;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Error =
 | 
				
			||||||
 | 
					  | {
 | 
				
			||||||
 | 
					      error: true;
 | 
				
			||||||
 | 
					      message: string;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  | {
 | 
				
			||||||
 | 
					      error: false;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface ListItemProps {
 | 
				
			||||||
 | 
					  title: string;
 | 
				
			||||||
 | 
					  subTitle?: string;
 | 
				
			||||||
 | 
					  children?: JSX.Element | JSX.Element[];
 | 
				
			||||||
 | 
					  className?: string;
 | 
				
			||||||
 | 
					  onClick?: () => void;
 | 
				
			||||||
 | 
					  nextline?: boolean;
 | 
				
			||||||
 | 
					  validator?: (v: any) => Error | Promise<Error>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const ListContext = createContext<
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    isMobileScreen?: boolean;
 | 
				
			||||||
 | 
					    update?: (m: ChildrenMeta) => void;
 | 
				
			||||||
 | 
					    handleValidate?: (v: any) => void;
 | 
				
			||||||
 | 
					  } & WidgetStyle
 | 
				
			||||||
 | 
					>({ isMobileScreen: false });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function ListItem(props: ListItemProps) {
 | 
				
			||||||
 | 
					  const {
 | 
				
			||||||
 | 
					    className = "",
 | 
				
			||||||
 | 
					    onClick,
 | 
				
			||||||
 | 
					    title,
 | 
				
			||||||
 | 
					    subTitle,
 | 
				
			||||||
 | 
					    children,
 | 
				
			||||||
 | 
					    nextline,
 | 
				
			||||||
 | 
					    validator,
 | 
				
			||||||
 | 
					  } = props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const context = useContext(ListContext);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [childrenMeta, setMeta] = useState<ChildrenMeta>({});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const { inputNextLine, rangeNextLine } = context;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const { type, error } = childrenMeta;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let internalNextLine;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  switch (type) {
 | 
				
			||||||
 | 
					    case "input":
 | 
				
			||||||
 | 
					      internalNextLine = !!(nextline || inputNextLine);
 | 
				
			||||||
 | 
					      break;
 | 
				
			||||||
 | 
					    case "range":
 | 
				
			||||||
 | 
					      internalNextLine = !!(nextline || rangeNextLine);
 | 
				
			||||||
 | 
					      break;
 | 
				
			||||||
 | 
					    default:
 | 
				
			||||||
 | 
					      internalNextLine = false;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const update = useCallback((m: ChildrenMeta) => {
 | 
				
			||||||
 | 
					    setMeta((pre) => ({ ...pre, ...m }));
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleValidate = useCallback((v: any) => {
 | 
				
			||||||
 | 
					    const insideValidator = validator || (() => {});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Promise.resolve(insideValidator(v)).then((result) => {
 | 
				
			||||||
 | 
					      if (result && result.error) {
 | 
				
			||||||
 | 
					        return update({
 | 
				
			||||||
 | 
					          error: result.message,
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      update({
 | 
				
			||||||
 | 
					        error: undefined,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div
 | 
				
			||||||
 | 
					      className={`relative after:h-[0.5px] after:bottom-0 after:w-[100%] after:left-0 after:absolute last:after:hidden after:bg-list-item-divider ${
 | 
				
			||||||
 | 
					        internalNextLine ? "" : "flex gap-3"
 | 
				
			||||||
 | 
					      } justify-between items-center px-0 py-2 md:py-3 ${className}`}
 | 
				
			||||||
 | 
					      onClick={onClick}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <div className={`flex-1 flex flex-col justify-start gap-1`}>
 | 
				
			||||||
 | 
					        <div className=" font-common text-sm-mobile font-weight-[500] line-clamp-1 text-text-list-title">
 | 
				
			||||||
 | 
					          {title}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        {subTitle && (
 | 
				
			||||||
 | 
					          <div className={` text-sm text-text-list-subtitle`}>{subTitle}</div>
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <ListContext.Provider value={{ ...context, update, handleValidate }}>
 | 
				
			||||||
 | 
					        <div
 | 
				
			||||||
 | 
					          className={`${
 | 
				
			||||||
 | 
					            internalNextLine ? "mt-[0.625rem]" : "max-w-[70%]"
 | 
				
			||||||
 | 
					          } flex flex-col items-center justify-center`}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <div>{children}</div>
 | 
				
			||||||
 | 
					          {!!error && (
 | 
				
			||||||
 | 
					            <div className="text-text-btn-danger text-sm-mobile-tab mt-[0.3125rem] flex items-start w-[100%]">
 | 
				
			||||||
 | 
					              <div className="">{error}</div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          )}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </ListContext.Provider>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function List(props: ListProps) {
 | 
				
			||||||
 | 
					  const { className, children, id, widgetStyle } = props;
 | 
				
			||||||
 | 
					  const { isMobileScreen } = useContext(ListContext);
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <ListContext.Provider value={{ isMobileScreen, ...widgetStyle }}>
 | 
				
			||||||
 | 
					      <div className={`flex flex-col w-[100%] ${className}`} id={id}>
 | 
				
			||||||
 | 
					        {children}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </ListContext.Provider>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					List.ListItem = ListItem;
 | 
				
			||||||
 | 
					List.ListContext = ListContext;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default List;
 | 
				
			||||||
							
								
								
									
										35
									
								
								app/components/Loading/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,35 @@
 | 
				
			|||||||
 | 
					import BotIcon from "@/app/icons/bot.svg";
 | 
				
			||||||
 | 
					import LoadingIcon from "@/app/icons/three-dots.svg";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { getCSSVar } from "@/app/utils";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function Loading({
 | 
				
			||||||
 | 
					  noLogo,
 | 
				
			||||||
 | 
					  useSkeleton = true,
 | 
				
			||||||
 | 
					}: {
 | 
				
			||||||
 | 
					  noLogo?: boolean;
 | 
				
			||||||
 | 
					  useSkeleton?: boolean;
 | 
				
			||||||
 | 
					}) {
 | 
				
			||||||
 | 
					  let theme;
 | 
				
			||||||
 | 
					  if (typeof window !== "undefined") {
 | 
				
			||||||
 | 
					    theme = getCSSVar("--default-container-bg");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div
 | 
				
			||||||
 | 
					      className={`
 | 
				
			||||||
 | 
					        flex flex-col justify-center items-center w-[100%] 
 | 
				
			||||||
 | 
					        h-[100%]
 | 
				
			||||||
 | 
					        md:my-2.5
 | 
				
			||||||
 | 
					        md:ml-1
 | 
				
			||||||
 | 
					        md:mr-2.5
 | 
				
			||||||
 | 
					        md:rounded-md
 | 
				
			||||||
 | 
					        md:h-[calc(100%-1.25rem)]
 | 
				
			||||||
 | 
					        `}
 | 
				
			||||||
 | 
					      style={{ background: useSkeleton ? theme : "" }}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      {!noLogo && <BotIcon />}
 | 
				
			||||||
 | 
					      <LoadingIcon />
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										115
									
								
								app/components/MenuLayout/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,115 @@
 | 
				
			|||||||
 | 
					import {
 | 
				
			||||||
 | 
					  DEFAULT_SIDEBAR_WIDTH,
 | 
				
			||||||
 | 
					  MAX_SIDEBAR_WIDTH,
 | 
				
			||||||
 | 
					  MIN_SIDEBAR_WIDTH,
 | 
				
			||||||
 | 
					  Path,
 | 
				
			||||||
 | 
					} from "@/app/constant";
 | 
				
			||||||
 | 
					import useDrag from "@/app/hooks/useDrag";
 | 
				
			||||||
 | 
					import useMobileScreen from "@/app/hooks/useMobileScreen";
 | 
				
			||||||
 | 
					import { updateGlobalCSSVars } from "@/app/utils/client";
 | 
				
			||||||
 | 
					import { ComponentType, useRef, useState } from "react";
 | 
				
			||||||
 | 
					import { useAppConfig } from "@/app/store/config";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface MenuWrapperInspectProps {
 | 
				
			||||||
 | 
					  setExternalProps?: (v: Record<string, any>) => void;
 | 
				
			||||||
 | 
					  setShowPanel?: (v: boolean) => void;
 | 
				
			||||||
 | 
					  showPanel?: boolean;
 | 
				
			||||||
 | 
					  [k: string]: any;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function MenuLayout<
 | 
				
			||||||
 | 
					  ListComponentProps extends MenuWrapperInspectProps,
 | 
				
			||||||
 | 
					  PanelComponentProps extends MenuWrapperInspectProps,
 | 
				
			||||||
 | 
					>(
 | 
				
			||||||
 | 
					  ListComponent: ComponentType<ListComponentProps>,
 | 
				
			||||||
 | 
					  PanelComponent: ComponentType<PanelComponentProps>,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					  return function MenuHood(props: ListComponentProps & PanelComponentProps) {
 | 
				
			||||||
 | 
					    const [showPanel, setShowPanel] = useState(false);
 | 
				
			||||||
 | 
					    const [externalProps, setExternalProps] = useState({});
 | 
				
			||||||
 | 
					    const config = useAppConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const isMobileScreen = useMobileScreen();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const startDragWidth = useRef(config.sidebarWidth ?? DEFAULT_SIDEBAR_WIDTH);
 | 
				
			||||||
 | 
					    // drag side bar
 | 
				
			||||||
 | 
					    const { onDragStart } = useDrag({
 | 
				
			||||||
 | 
					      customToggle: () => {
 | 
				
			||||||
 | 
					        config.update((config) => {
 | 
				
			||||||
 | 
					          config.sidebarWidth = DEFAULT_SIDEBAR_WIDTH;
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      customDragMove: (nextWidth: number) => {
 | 
				
			||||||
 | 
					        const { menuWidth } = updateGlobalCSSVars(nextWidth);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        document.documentElement.style.setProperty(
 | 
				
			||||||
 | 
					          "--menu-width",
 | 
				
			||||||
 | 
					          `${menuWidth}px`,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        config.update((config) => {
 | 
				
			||||||
 | 
					          config.sidebarWidth = nextWidth;
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      customLimit: (x: number) =>
 | 
				
			||||||
 | 
					        Math.max(
 | 
				
			||||||
 | 
					          MIN_SIDEBAR_WIDTH,
 | 
				
			||||||
 | 
					          Math.min(MAX_SIDEBAR_WIDTH, startDragWidth.current + x),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <div
 | 
				
			||||||
 | 
					        className={`
 | 
				
			||||||
 | 
					          w-[100%] relative bg-center
 | 
				
			||||||
 | 
					          max-md:h-[100%]
 | 
				
			||||||
 | 
					          md:flex md:my-2.5
 | 
				
			||||||
 | 
					        `}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <div
 | 
				
			||||||
 | 
					          className={`
 | 
				
			||||||
 | 
					            flex flex-col px-6 
 | 
				
			||||||
 | 
					            h-[100%] 
 | 
				
			||||||
 | 
					            max-md:w-[100%] max-md:px-4 max-md:pb-4 max-md:flex-1
 | 
				
			||||||
 | 
					            md:relative md:basis-sidebar  md:pb-6  md:rounded-md md:bg-menu
 | 
				
			||||||
 | 
					          `}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <ListComponent
 | 
				
			||||||
 | 
					            {...props}
 | 
				
			||||||
 | 
					            setShowPanel={setShowPanel}
 | 
				
			||||||
 | 
					            setExternalProps={setExternalProps}
 | 
				
			||||||
 | 
					            showPanel={showPanel}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        {!isMobileScreen && (
 | 
				
			||||||
 | 
					          <div
 | 
				
			||||||
 | 
					            className={`group/menu-dragger cursor-col-resize w-[0.25rem]  flex items-center justify-center`}
 | 
				
			||||||
 | 
					            onPointerDown={(e) => {
 | 
				
			||||||
 | 
					              startDragWidth.current = config.sidebarWidth;
 | 
				
			||||||
 | 
					              onDragStart(e as any);
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <div className="w-[2px] opacity-0 group-hover/menu-dragger:opacity-100 bg-menu-dragger h-[100%] rounded-[2px]">
 | 
				
			||||||
 | 
					               
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					        <div
 | 
				
			||||||
 | 
					          className={`
 | 
				
			||||||
 | 
					          md:flex-1 md:h-[100%] md:w-page
 | 
				
			||||||
 | 
					          max-md:transition-all max-md:duration-300 max-md:absolute max-md:top-0 max-md:max-h-[100vh] max-md:w-[100%] ${
 | 
				
			||||||
 | 
					            showPanel ? "max-md:left-0" : "max-md:left-[101%]"
 | 
				
			||||||
 | 
					          } max-md:z-10
 | 
				
			||||||
 | 
					        `}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <PanelComponent
 | 
				
			||||||
 | 
					            {...props}
 | 
				
			||||||
 | 
					            {...externalProps}
 | 
				
			||||||
 | 
					            setShowPanel={setShowPanel}
 | 
				
			||||||
 | 
					            setExternalProps={setExternalProps}
 | 
				
			||||||
 | 
					            showPanel={showPanel}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										352
									
								
								app/components/Modal/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,352 @@
 | 
				
			|||||||
 | 
					import React, { useLayoutEffect, useState } from "react";
 | 
				
			||||||
 | 
					import { createRoot } from "react-dom/client";
 | 
				
			||||||
 | 
					import * as AlertDialog from "@radix-ui/react-alert-dialog";
 | 
				
			||||||
 | 
					import Btn, { BtnProps } from "@/app/components/Btn";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import Warning from "@/app/icons/warning.svg";
 | 
				
			||||||
 | 
					import Close from "@/app/icons/closeIcon.svg";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface ModalProps {
 | 
				
			||||||
 | 
					  onOk?: () => void;
 | 
				
			||||||
 | 
					  onCancel?: () => void;
 | 
				
			||||||
 | 
					  okText?: string;
 | 
				
			||||||
 | 
					  cancelText?: string;
 | 
				
			||||||
 | 
					  okBtnProps?: BtnProps;
 | 
				
			||||||
 | 
					  cancelBtnProps?: BtnProps;
 | 
				
			||||||
 | 
					  content?:
 | 
				
			||||||
 | 
					    | React.ReactNode
 | 
				
			||||||
 | 
					    | ((handlers: { close: () => void }) => JSX.Element);
 | 
				
			||||||
 | 
					  title?: React.ReactNode;
 | 
				
			||||||
 | 
					  visible?: boolean;
 | 
				
			||||||
 | 
					  noFooter?: boolean;
 | 
				
			||||||
 | 
					  noHeader?: boolean;
 | 
				
			||||||
 | 
					  isMobile?: boolean;
 | 
				
			||||||
 | 
					  closeble?: boolean;
 | 
				
			||||||
 | 
					  type?: "modal" | "bottom-drawer";
 | 
				
			||||||
 | 
					  headerBordered?: boolean;
 | 
				
			||||||
 | 
					  modelClassName?: string;
 | 
				
			||||||
 | 
					  onOpen?: (v: boolean) => void;
 | 
				
			||||||
 | 
					  maskCloseble?: boolean;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface WarnProps
 | 
				
			||||||
 | 
					  extends Omit<
 | 
				
			||||||
 | 
					    ModalProps,
 | 
				
			||||||
 | 
					    | "closeble"
 | 
				
			||||||
 | 
					    | "isMobile"
 | 
				
			||||||
 | 
					    | "noHeader"
 | 
				
			||||||
 | 
					    | "noFooter"
 | 
				
			||||||
 | 
					    | "onOk"
 | 
				
			||||||
 | 
					    | "okBtnProps"
 | 
				
			||||||
 | 
					    | "cancelBtnProps"
 | 
				
			||||||
 | 
					    | "content"
 | 
				
			||||||
 | 
					  > {
 | 
				
			||||||
 | 
					  onOk?: () => Promise<void> | void;
 | 
				
			||||||
 | 
					  content?: React.ReactNode;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface TriggerProps
 | 
				
			||||||
 | 
					  extends Omit<ModalProps, "visible" | "onOk" | "onCancel"> {
 | 
				
			||||||
 | 
					  children: JSX.Element;
 | 
				
			||||||
 | 
					  className?: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const baseZIndex = 150;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Modal = (props: ModalProps) => {
 | 
				
			||||||
 | 
					  const {
 | 
				
			||||||
 | 
					    onOk,
 | 
				
			||||||
 | 
					    onCancel,
 | 
				
			||||||
 | 
					    okText,
 | 
				
			||||||
 | 
					    cancelText,
 | 
				
			||||||
 | 
					    content,
 | 
				
			||||||
 | 
					    title,
 | 
				
			||||||
 | 
					    visible,
 | 
				
			||||||
 | 
					    noFooter,
 | 
				
			||||||
 | 
					    noHeader,
 | 
				
			||||||
 | 
					    closeble = true,
 | 
				
			||||||
 | 
					    okBtnProps,
 | 
				
			||||||
 | 
					    cancelBtnProps,
 | 
				
			||||||
 | 
					    type = "modal",
 | 
				
			||||||
 | 
					    headerBordered,
 | 
				
			||||||
 | 
					    modelClassName,
 | 
				
			||||||
 | 
					    onOpen,
 | 
				
			||||||
 | 
					    maskCloseble = true,
 | 
				
			||||||
 | 
					  } = props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [open, setOpen] = useState(!!visible);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const mergeOpen = visible ?? open;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleClose = () => {
 | 
				
			||||||
 | 
					    setOpen(false);
 | 
				
			||||||
 | 
					    onCancel?.();
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleOk = () => {
 | 
				
			||||||
 | 
					    setOpen(false);
 | 
				
			||||||
 | 
					    onOk?.();
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useLayoutEffect(() => {
 | 
				
			||||||
 | 
					    onOpen?.(mergeOpen);
 | 
				
			||||||
 | 
					  }, [mergeOpen]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let layoutClassName = "";
 | 
				
			||||||
 | 
					  let panelClassName = "";
 | 
				
			||||||
 | 
					  let titleClassName = "";
 | 
				
			||||||
 | 
					  let footerClassName = "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  switch (type) {
 | 
				
			||||||
 | 
					    case "bottom-drawer":
 | 
				
			||||||
 | 
					      layoutClassName = "fixed inset-0 flex flex-col w-[100%] bottom-0";
 | 
				
			||||||
 | 
					      panelClassName =
 | 
				
			||||||
 | 
					        "rounded-t-chat-model-select overflow-y-auto overflow-x-hidden";
 | 
				
			||||||
 | 
					      titleClassName = "px-4 py-3";
 | 
				
			||||||
 | 
					      footerClassName = "absolute w-[100%]";
 | 
				
			||||||
 | 
					      break;
 | 
				
			||||||
 | 
					    case "modal":
 | 
				
			||||||
 | 
					    default:
 | 
				
			||||||
 | 
					      layoutClassName =
 | 
				
			||||||
 | 
					        "fixed inset-0 flex flex-col item-start top-0 left-[50vw] translate-x-[-50%] max-sm:w-modal-modal-type-mobile";
 | 
				
			||||||
 | 
					      panelClassName = "rounded-lg px-6 sm:w-modal-modal-type";
 | 
				
			||||||
 | 
					      titleClassName = "py-6 max-sm:pb-3";
 | 
				
			||||||
 | 
					      footerClassName = "py-6";
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  const btnCommonClass = "px-4 py-2.5 rounded-md max-sm:flex-1";
 | 
				
			||||||
 | 
					  const { className: okBtnClass } = okBtnProps || {};
 | 
				
			||||||
 | 
					  const { className: cancelBtnClass } = cancelBtnProps || {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <AlertDialog.Root open={mergeOpen} onOpenChange={setOpen}>
 | 
				
			||||||
 | 
					      <AlertDialog.Portal>
 | 
				
			||||||
 | 
					        <AlertDialog.Overlay
 | 
				
			||||||
 | 
					          className="bg-modal-mask fixed inset-0 animate-mask "
 | 
				
			||||||
 | 
					          style={{ zIndex: baseZIndex - 1 }}
 | 
				
			||||||
 | 
					          onClick={() => {
 | 
				
			||||||
 | 
					            if (maskCloseble) {
 | 
				
			||||||
 | 
					              handleClose();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					        <AlertDialog.Content
 | 
				
			||||||
 | 
					          className={`
 | 
				
			||||||
 | 
					            ${layoutClassName}
 | 
				
			||||||
 | 
					          `}
 | 
				
			||||||
 | 
					          style={{ zIndex: baseZIndex - 1 }}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <div
 | 
				
			||||||
 | 
					            className="flex-1"
 | 
				
			||||||
 | 
					            onClick={() => {
 | 
				
			||||||
 | 
					              if (maskCloseble) {
 | 
				
			||||||
 | 
					                handleClose();
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					             
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          <div
 | 
				
			||||||
 | 
					            className={`flex flex-col flex-0      
 | 
				
			||||||
 | 
					              bg-moda-panel text-modal-panel    
 | 
				
			||||||
 | 
					              ${modelClassName}
 | 
				
			||||||
 | 
					              ${panelClassName}
 | 
				
			||||||
 | 
					            `}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            {!noHeader && (
 | 
				
			||||||
 | 
					              <AlertDialog.Title
 | 
				
			||||||
 | 
					                className={`
 | 
				
			||||||
 | 
					                      flex items-center justify-between gap-3 font-common
 | 
				
			||||||
 | 
					                      md:text-chat-header-title md:font-bold md:leading-5 
 | 
				
			||||||
 | 
					                      ${
 | 
				
			||||||
 | 
					                        headerBordered
 | 
				
			||||||
 | 
					                          ? " border-b border-modal-header-bottom"
 | 
				
			||||||
 | 
					                          : ""
 | 
				
			||||||
 | 
					                      }
 | 
				
			||||||
 | 
					                      ${titleClassName}
 | 
				
			||||||
 | 
					                  `}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <div className="flex gap-3 justify-start flex-1 items-center text-text-modal-title text-chat-header-title">
 | 
				
			||||||
 | 
					                  {title}
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                {closeble && (
 | 
				
			||||||
 | 
					                  <div
 | 
				
			||||||
 | 
					                    className="items-center"
 | 
				
			||||||
 | 
					                    onClick={() => {
 | 
				
			||||||
 | 
					                      handleClose();
 | 
				
			||||||
 | 
					                    }}
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
 | 
					                    <Close />
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                )}
 | 
				
			||||||
 | 
					              </AlertDialog.Title>
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					            <div className="flex-1 overflow-hidden text-text-modal-content text-sm-title">
 | 
				
			||||||
 | 
					              {typeof content === "function"
 | 
				
			||||||
 | 
					                ? content({
 | 
				
			||||||
 | 
					                    close: () => {
 | 
				
			||||||
 | 
					                      handleClose();
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                  })
 | 
				
			||||||
 | 
					                : content}
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            {!noFooter && (
 | 
				
			||||||
 | 
					              <div
 | 
				
			||||||
 | 
					                className={`
 | 
				
			||||||
 | 
					                  flex gap-3 sm:justify-end max-sm:justify-between
 | 
				
			||||||
 | 
					                  ${footerClassName}
 | 
				
			||||||
 | 
					                  `}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <AlertDialog.Cancel asChild>
 | 
				
			||||||
 | 
					                  <Btn
 | 
				
			||||||
 | 
					                    {...cancelBtnProps}
 | 
				
			||||||
 | 
					                    onClick={() => handleClose()}
 | 
				
			||||||
 | 
					                    text={cancelText}
 | 
				
			||||||
 | 
					                    className={`${btnCommonClass} ${cancelBtnClass}`}
 | 
				
			||||||
 | 
					                  />
 | 
				
			||||||
 | 
					                </AlertDialog.Cancel>
 | 
				
			||||||
 | 
					                <AlertDialog.Action asChild>
 | 
				
			||||||
 | 
					                  <Btn
 | 
				
			||||||
 | 
					                    {...okBtnProps}
 | 
				
			||||||
 | 
					                    onClick={handleOk}
 | 
				
			||||||
 | 
					                    text={okText}
 | 
				
			||||||
 | 
					                    className={`${btnCommonClass} ${okBtnClass}`}
 | 
				
			||||||
 | 
					                  />
 | 
				
			||||||
 | 
					                </AlertDialog.Action>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          {type === "modal" && (
 | 
				
			||||||
 | 
					            <div
 | 
				
			||||||
 | 
					              className="flex-1"
 | 
				
			||||||
 | 
					              onClick={() => {
 | 
				
			||||||
 | 
					                if (maskCloseble) {
 | 
				
			||||||
 | 
					                  handleClose();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					              }}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					               
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          )}
 | 
				
			||||||
 | 
					        </AlertDialog.Content>
 | 
				
			||||||
 | 
					      </AlertDialog.Portal>
 | 
				
			||||||
 | 
					    </AlertDialog.Root>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const Warn = ({
 | 
				
			||||||
 | 
					  title,
 | 
				
			||||||
 | 
					  onOk,
 | 
				
			||||||
 | 
					  visible,
 | 
				
			||||||
 | 
					  content,
 | 
				
			||||||
 | 
					  ...props
 | 
				
			||||||
 | 
					}: WarnProps) => {
 | 
				
			||||||
 | 
					  const [internalVisible, setVisible] = useState(visible);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Modal
 | 
				
			||||||
 | 
					      {...props}
 | 
				
			||||||
 | 
					      title={
 | 
				
			||||||
 | 
					        <>
 | 
				
			||||||
 | 
					          <Warning />
 | 
				
			||||||
 | 
					          {title}
 | 
				
			||||||
 | 
					        </>
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      content={
 | 
				
			||||||
 | 
					        <AlertDialog.Description
 | 
				
			||||||
 | 
					          className={`
 | 
				
			||||||
 | 
					                    font-common font-normal
 | 
				
			||||||
 | 
					                    md:text-sm-title md:leading-[158%]
 | 
				
			||||||
 | 
					                `}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          {content}
 | 
				
			||||||
 | 
					        </AlertDialog.Description>
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      closeble={false}
 | 
				
			||||||
 | 
					      onOk={() => {
 | 
				
			||||||
 | 
					        const toDo = onOk?.();
 | 
				
			||||||
 | 
					        if (toDo instanceof Promise) {
 | 
				
			||||||
 | 
					          toDo.then(() => {
 | 
				
			||||||
 | 
					            setVisible(false);
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          setVisible(false);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }}
 | 
				
			||||||
 | 
					      visible={internalVisible}
 | 
				
			||||||
 | 
					      okBtnProps={{
 | 
				
			||||||
 | 
					        className: `bg-delete-chat-ok-btn text-text-delete-chat-ok-btn `,
 | 
				
			||||||
 | 
					      }}
 | 
				
			||||||
 | 
					      cancelBtnProps={{
 | 
				
			||||||
 | 
					        className: `bg-delete-chat-cancel-btn  border border-delete-chat-cancel-btn text-text-delete-chat-cancel-btn`,
 | 
				
			||||||
 | 
					      }}
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const div = document.createElement("div");
 | 
				
			||||||
 | 
					div.id = "confirm-root";
 | 
				
			||||||
 | 
					div.style.height = "0px";
 | 
				
			||||||
 | 
					document.body.appendChild(div);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Modal.warn = (props: Omit<WarnProps, "visible" | "onCancel" | "onOk">) => {
 | 
				
			||||||
 | 
					  const root = createRoot(div);
 | 
				
			||||||
 | 
					  const closeModal = () => {
 | 
				
			||||||
 | 
					    root.unmount();
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return new Promise<boolean>((resolve) => {
 | 
				
			||||||
 | 
					    root.render(
 | 
				
			||||||
 | 
					      <Warn
 | 
				
			||||||
 | 
					        {...props}
 | 
				
			||||||
 | 
					        visible={true}
 | 
				
			||||||
 | 
					        onCancel={() => {
 | 
				
			||||||
 | 
					          closeModal();
 | 
				
			||||||
 | 
					          resolve(false);
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					        onOk={() => {
 | 
				
			||||||
 | 
					          closeModal();
 | 
				
			||||||
 | 
					          resolve(true);
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					      />,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const Trigger = (props: TriggerProps) => {
 | 
				
			||||||
 | 
					  const { children, className, content, ...rest } = props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [internalVisible, setVisible] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <>
 | 
				
			||||||
 | 
					      <div
 | 
				
			||||||
 | 
					        className={className}
 | 
				
			||||||
 | 
					        onClick={() => {
 | 
				
			||||||
 | 
					          setVisible(true);
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        {children}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <Modal
 | 
				
			||||||
 | 
					        {...rest}
 | 
				
			||||||
 | 
					        visible={internalVisible}
 | 
				
			||||||
 | 
					        onCancel={() => {
 | 
				
			||||||
 | 
					          setVisible(false);
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					        content={
 | 
				
			||||||
 | 
					          typeof content === "function"
 | 
				
			||||||
 | 
					            ? content({
 | 
				
			||||||
 | 
					                close: () => {
 | 
				
			||||||
 | 
					                  setVisible(false);
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					              })
 | 
				
			||||||
 | 
					            : content
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Modal.Trigger = Trigger;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default Modal;
 | 
				
			||||||
							
								
								
									
										352
									
								
								app/components/Popover/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,352 @@
 | 
				
			|||||||
 | 
					import useRelativePosition from "@/app/hooks/useRelativePosition";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  RefObject,
 | 
				
			||||||
 | 
					  useEffect,
 | 
				
			||||||
 | 
					  useLayoutEffect,
 | 
				
			||||||
 | 
					  useMemo,
 | 
				
			||||||
 | 
					  useRef,
 | 
				
			||||||
 | 
					  useState,
 | 
				
			||||||
 | 
					} from "react";
 | 
				
			||||||
 | 
					import { createPortal } from "react-dom";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ArrowIcon = ({ sibling }: { sibling: RefObject<HTMLDivElement> }) => {
 | 
				
			||||||
 | 
					  const [color, setColor] = useState<string>("");
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    if (sibling.current) {
 | 
				
			||||||
 | 
					      const { backgroundColor } = window.getComputedStyle(sibling.current);
 | 
				
			||||||
 | 
					      setColor(backgroundColor);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <svg
 | 
				
			||||||
 | 
					      xmlns="http://www.w3.org/2000/svg"
 | 
				
			||||||
 | 
					      width="16"
 | 
				
			||||||
 | 
					      height="6"
 | 
				
			||||||
 | 
					      viewBox="0 0 16 6"
 | 
				
			||||||
 | 
					      fill="none"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <path
 | 
				
			||||||
 | 
					        d="M16 0H0C1.28058 0 2.50871 0.508709 3.41421 1.41421L6.91 4.91C7.51199 5.51199 8.48801 5.51199 9.09 4.91L12.5858 1.41421C13.4913 0.508708 14.7194 0 16 0Z"
 | 
				
			||||||
 | 
					        fill={color}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    </svg>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const baseZIndex = 100;
 | 
				
			||||||
 | 
					const popoverRootName = "popoverRoot";
 | 
				
			||||||
 | 
					let popoverRoot = document.querySelector(
 | 
				
			||||||
 | 
					  `#${popoverRootName}`,
 | 
				
			||||||
 | 
					) as HTMLDivElement;
 | 
				
			||||||
 | 
					if (!popoverRoot) {
 | 
				
			||||||
 | 
					  popoverRoot = document.createElement("div");
 | 
				
			||||||
 | 
					  document.body.appendChild(popoverRoot);
 | 
				
			||||||
 | 
					  popoverRoot.style.height = "0px";
 | 
				
			||||||
 | 
					  popoverRoot.style.width = "100%";
 | 
				
			||||||
 | 
					  popoverRoot.style.position = "fixed";
 | 
				
			||||||
 | 
					  popoverRoot.style.bottom = "0";
 | 
				
			||||||
 | 
					  popoverRoot.style.zIndex = "10000";
 | 
				
			||||||
 | 
					  popoverRoot.id = "popover-root";
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface PopoverProps {
 | 
				
			||||||
 | 
					  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" | "l" | "r";
 | 
				
			||||||
 | 
					  noArrow?: boolean;
 | 
				
			||||||
 | 
					  delayClose?: number;
 | 
				
			||||||
 | 
					  useGlobalRoot?: boolean;
 | 
				
			||||||
 | 
					  getPopoverPanelRef?: (ref: RefObject<HTMLDivElement>) => void;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function Popover(props: PopoverProps) {
 | 
				
			||||||
 | 
					  const {
 | 
				
			||||||
 | 
					    content,
 | 
				
			||||||
 | 
					    children,
 | 
				
			||||||
 | 
					    show,
 | 
				
			||||||
 | 
					    onShow,
 | 
				
			||||||
 | 
					    className,
 | 
				
			||||||
 | 
					    popoverClassName,
 | 
				
			||||||
 | 
					    trigger = "hover",
 | 
				
			||||||
 | 
					    placement = "t",
 | 
				
			||||||
 | 
					    noArrow = false,
 | 
				
			||||||
 | 
					    delayClose = 0,
 | 
				
			||||||
 | 
					    useGlobalRoot,
 | 
				
			||||||
 | 
					    getPopoverPanelRef,
 | 
				
			||||||
 | 
					  } = props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [internalShow, setShow] = useState(false);
 | 
				
			||||||
 | 
					  const { position, getRelativePosition } = useRelativePosition({
 | 
				
			||||||
 | 
					    delay: 0,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const popoverCommonClass = `absolute p-2 box-border`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const mergedShow = show ?? internalShow;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const { arrowClassName, placementStyle, placementClassName } = useMemo(() => {
 | 
				
			||||||
 | 
					    const arrowCommonClassName = `${
 | 
				
			||||||
 | 
					      noArrow ? "hidden" : ""
 | 
				
			||||||
 | 
					    } absolute z-10 left-[50%] translate-x-[calc(-50%)]`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let defaultTopPlacement = true; // when users dont config 't' or 'b'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const {
 | 
				
			||||||
 | 
					      distanceToBottomBoundary = 0,
 | 
				
			||||||
 | 
					      distanceToLeftBoundary = 0,
 | 
				
			||||||
 | 
					      distanceToRightBoundary = -10000,
 | 
				
			||||||
 | 
					      distanceToTopBoundary = 0,
 | 
				
			||||||
 | 
					      targetH = 0,
 | 
				
			||||||
 | 
					      targetW = 0,
 | 
				
			||||||
 | 
					    } = position?.poi || {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (distanceToBottomBoundary > distanceToTopBoundary) {
 | 
				
			||||||
 | 
					      defaultTopPlacement = false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const placements = {
 | 
				
			||||||
 | 
					      lt: {
 | 
				
			||||||
 | 
					        placementStyle: {
 | 
				
			||||||
 | 
					          bottom: `calc(${distanceToBottomBoundary + targetH}px + 0.5rem)`,
 | 
				
			||||||
 | 
					          left: `calc(${distanceToLeftBoundary}px - ${targetW * 0.02}px)`,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        arrowClassName: `${arrowCommonClassName} bottom-[calc(100%+0.5rem)] translate-y-[calc(100%)] pb-[0.5rem]`,
 | 
				
			||||||
 | 
					        placementClassName: "bottom-[calc(100%+0.5rem)] left-[calc(-2%)]",
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      lb: {
 | 
				
			||||||
 | 
					        placementStyle: {
 | 
				
			||||||
 | 
					          top: `calc(-${distanceToBottomBoundary}px + 0.5rem)`,
 | 
				
			||||||
 | 
					          left: `calc(${distanceToLeftBoundary}px - ${targetW * 0.02}px)`,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        arrowClassName: `${arrowCommonClassName} top-[calc(100%+0.5rem)] translate-y-[calc(-100%)]  pt-[0.5rem]`,
 | 
				
			||||||
 | 
					        placementClassName: "top-[calc(100%+0.5rem)] left-[calc(-2%)]",
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      rt: {
 | 
				
			||||||
 | 
					        placementStyle: {
 | 
				
			||||||
 | 
					          bottom: `calc(${distanceToBottomBoundary + targetH}px + 0.5rem)`,
 | 
				
			||||||
 | 
					          right: `calc(${distanceToRightBoundary}px - ${targetW * 0.02}px)`,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        arrowClassName: `${arrowCommonClassName} bottom-[calc(100%+0.5rem)] translate-y-[calc(100%)] pb-[0.5rem]`,
 | 
				
			||||||
 | 
					        placementClassName: "bottom-[calc(100%+0.5rem)] right-[calc(-2%)]",
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      rb: {
 | 
				
			||||||
 | 
					        placementStyle: {
 | 
				
			||||||
 | 
					          top: `calc(-${distanceToBottomBoundary}px + 0.5rem)`,
 | 
				
			||||||
 | 
					          right: `calc(${distanceToRightBoundary}px - ${targetW * 0.02}px)`,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        arrowClassName: `${arrowCommonClassName} top-[calc(100%+0.5rem)] translate-y-[calc(-100%)] pt-[0.5rem]`,
 | 
				
			||||||
 | 
					        placementClassName: "top-[calc(100%+0.5rem)] right-[calc(-2%)]",
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      t: {
 | 
				
			||||||
 | 
					        placementStyle: {
 | 
				
			||||||
 | 
					          bottom: `calc(${distanceToBottomBoundary + targetH}px + 0.5rem)`,
 | 
				
			||||||
 | 
					          left: `calc(${distanceToLeftBoundary + targetW / 2}px`,
 | 
				
			||||||
 | 
					          transform: "translateX(-50%)",
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        arrowClassName: `${arrowCommonClassName} bottom-[calc(100%+0.5rem)] translate-y-[calc(100%)] pb-[0.5rem]`,
 | 
				
			||||||
 | 
					        placementClassName:
 | 
				
			||||||
 | 
					          "bottom-[calc(100%+0.5rem)] left-[50%] translate-x-[calc(-50%)]",
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      b: {
 | 
				
			||||||
 | 
					        placementStyle: {
 | 
				
			||||||
 | 
					          top: `calc(-${distanceToBottomBoundary}px + 0.5rem)`,
 | 
				
			||||||
 | 
					          left: `calc(${distanceToLeftBoundary + targetW / 2}px`,
 | 
				
			||||||
 | 
					          transform: "translateX(-50%)",
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        arrowClassName: `${arrowCommonClassName} top-[calc(100%+0.5rem)] translate-y-[calc(-100%)] pt-[0.5rem]`,
 | 
				
			||||||
 | 
					        placementClassName:
 | 
				
			||||||
 | 
					          "top-[calc(100%+0.5rem)] left-[50%]  translate-x-[calc(-50%)]",
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const getStyle = () => {
 | 
				
			||||||
 | 
					      if (["l", "r"].includes(placement)) {
 | 
				
			||||||
 | 
					        return placements[
 | 
				
			||||||
 | 
					          `${placement}${defaultTopPlacement ? "t" : "b"}` as
 | 
				
			||||||
 | 
					            | "lt"
 | 
				
			||||||
 | 
					            | "lb"
 | 
				
			||||||
 | 
					            | "rb"
 | 
				
			||||||
 | 
					            | "rt"
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return placements[placement as Exclude<typeof placement, "l" | "r">];
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return getStyle();
 | 
				
			||||||
 | 
					  }, [Object.values(position?.poi || {})]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const popoverRef = useRef<HTMLDivElement>(null);
 | 
				
			||||||
 | 
					  const closeTimer = useRef<number>(0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useLayoutEffect(() => {
 | 
				
			||||||
 | 
					    getPopoverPanelRef?.(popoverRef);
 | 
				
			||||||
 | 
					    onShow?.(internalShow);
 | 
				
			||||||
 | 
					  }, [internalShow]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (trigger === "click") {
 | 
				
			||||||
 | 
					    const handleOpen = (e: { currentTarget: any }) => {
 | 
				
			||||||
 | 
					      clearTimeout(closeTimer.current);
 | 
				
			||||||
 | 
					      setShow(true);
 | 
				
			||||||
 | 
					      getRelativePosition(e.currentTarget, "");
 | 
				
			||||||
 | 
					      window.document.documentElement.style.overflow = "hidden";
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    const handleClose = () => {
 | 
				
			||||||
 | 
					      if (delayClose) {
 | 
				
			||||||
 | 
					        closeTimer.current = window.setTimeout(() => {
 | 
				
			||||||
 | 
					          setShow(false);
 | 
				
			||||||
 | 
					        }, delayClose);
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        setShow(false);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      window.document.documentElement.style.overflow = "auto";
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <div
 | 
				
			||||||
 | 
					        className={`relative ${className}`}
 | 
				
			||||||
 | 
					        onClick={(e) => {
 | 
				
			||||||
 | 
					          e.preventDefault();
 | 
				
			||||||
 | 
					          e.stopPropagation();
 | 
				
			||||||
 | 
					          if (!mergedShow) {
 | 
				
			||||||
 | 
					            handleOpen(e);
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            handleClose();
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        {children}
 | 
				
			||||||
 | 
					        {mergedShow && (
 | 
				
			||||||
 | 
					          <>
 | 
				
			||||||
 | 
					            {!noArrow && (
 | 
				
			||||||
 | 
					              <div className={`${arrowClassName}`}>
 | 
				
			||||||
 | 
					                <ArrowIcon sibling={popoverRef} />
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					            {createPortal(
 | 
				
			||||||
 | 
					              <div
 | 
				
			||||||
 | 
					                className={`${popoverCommonClass} ${popoverClassName} cursor-pointer overflow-auto`}
 | 
				
			||||||
 | 
					                style={{ zIndex: baseZIndex + 1, ...placementStyle }}
 | 
				
			||||||
 | 
					                ref={popoverRef}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                {content}
 | 
				
			||||||
 | 
					              </div>,
 | 
				
			||||||
 | 
					              popoverRoot,
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					            {createPortal(
 | 
				
			||||||
 | 
					              <div
 | 
				
			||||||
 | 
					                className=" fixed w-[100vw] h-[100vh] right-0 bottom-0"
 | 
				
			||||||
 | 
					                style={{ zIndex: baseZIndex }}
 | 
				
			||||||
 | 
					                onClick={(e) => {
 | 
				
			||||||
 | 
					                  e.preventDefault();
 | 
				
			||||||
 | 
					                  handleClose();
 | 
				
			||||||
 | 
					                }}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                 
 | 
				
			||||||
 | 
					              </div>,
 | 
				
			||||||
 | 
					              popoverRoot,
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					          </>
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (useGlobalRoot) {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <div
 | 
				
			||||||
 | 
					        className={`relative ${className}`}
 | 
				
			||||||
 | 
					        onPointerEnter={(e) => {
 | 
				
			||||||
 | 
					          e.preventDefault();
 | 
				
			||||||
 | 
					          clearTimeout(closeTimer.current);
 | 
				
			||||||
 | 
					          onShow?.(true);
 | 
				
			||||||
 | 
					          setShow(true);
 | 
				
			||||||
 | 
					          getRelativePosition(e.currentTarget, "");
 | 
				
			||||||
 | 
					          window.document.documentElement.style.overflow = "hidden";
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					        onPointerLeave={(e) => {
 | 
				
			||||||
 | 
					          e.preventDefault();
 | 
				
			||||||
 | 
					          if (delayClose) {
 | 
				
			||||||
 | 
					            closeTimer.current = window.setTimeout(() => {
 | 
				
			||||||
 | 
					              onShow?.(false);
 | 
				
			||||||
 | 
					              setShow(false);
 | 
				
			||||||
 | 
					            }, delayClose);
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            onShow?.(false);
 | 
				
			||||||
 | 
					            setShow(false);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          window.document.documentElement.style.overflow = "auto";
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        {children}
 | 
				
			||||||
 | 
					        {mergedShow && (
 | 
				
			||||||
 | 
					          <>
 | 
				
			||||||
 | 
					            <div
 | 
				
			||||||
 | 
					              className={`${
 | 
				
			||||||
 | 
					                noArrow ? "opacity-0" : ""
 | 
				
			||||||
 | 
					              } bg-inherit ${arrowClassName}`}
 | 
				
			||||||
 | 
					              style={{ zIndex: baseZIndex + 1 }}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              <ArrowIcon sibling={popoverRef} />
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            {createPortal(
 | 
				
			||||||
 | 
					              <div
 | 
				
			||||||
 | 
					                className={` whitespace-nowrap ${popoverCommonClass} ${popoverClassName} cursor-pointer`}
 | 
				
			||||||
 | 
					                style={{ zIndex: baseZIndex + 1, ...placementStyle }}
 | 
				
			||||||
 | 
					                ref={popoverRef}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                {content}
 | 
				
			||||||
 | 
					              </div>,
 | 
				
			||||||
 | 
					              popoverRoot,
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					          </>
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div
 | 
				
			||||||
 | 
					      className={`group/popover relative ${className}`}
 | 
				
			||||||
 | 
					      onPointerEnter={(e) => {
 | 
				
			||||||
 | 
					        getRelativePosition(e.currentTarget, "");
 | 
				
			||||||
 | 
					        e.preventDefault();
 | 
				
			||||||
 | 
					        e.stopPropagation();
 | 
				
			||||||
 | 
					      }}
 | 
				
			||||||
 | 
					      onClick={(e) => {
 | 
				
			||||||
 | 
					        e.preventDefault();
 | 
				
			||||||
 | 
					        e.stopPropagation();
 | 
				
			||||||
 | 
					      }}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      {children}
 | 
				
			||||||
 | 
					      <div
 | 
				
			||||||
 | 
					        className={`
 | 
				
			||||||
 | 
					          hidden group-hover/popover:block 
 | 
				
			||||||
 | 
					          ${noArrow ? "opacity-0" : ""} 
 | 
				
			||||||
 | 
					          bg-inherit 
 | 
				
			||||||
 | 
					          ${arrowClassName}
 | 
				
			||||||
 | 
					        `}
 | 
				
			||||||
 | 
					        style={{ zIndex: baseZIndex + 1 }}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <ArrowIcon sibling={popoverRef} />
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <div
 | 
				
			||||||
 | 
					        className={`
 | 
				
			||||||
 | 
					          hidden group-hover/popover:block whitespace-nowrap 
 | 
				
			||||||
 | 
					          ${popoverCommonClass} 
 | 
				
			||||||
 | 
					          ${placementClassName} 
 | 
				
			||||||
 | 
					          ${popoverClassName}
 | 
				
			||||||
 | 
					        `}
 | 
				
			||||||
 | 
					        ref={popoverRef}
 | 
				
			||||||
 | 
					        style={{ zIndex: baseZIndex + 1 }}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        {content}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										71
									
								
								app/components/Screen/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,71 @@
 | 
				
			|||||||
 | 
					import { useLocation } from "react-router-dom";
 | 
				
			||||||
 | 
					import { useMemo, ReactNode } from "react";
 | 
				
			||||||
 | 
					import { Path, SIDEBAR_ID, SlotID } from "@/app/constant";
 | 
				
			||||||
 | 
					import { getLang } from "@/app/locales";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import useMobileScreen from "@/app/hooks/useMobileScreen";
 | 
				
			||||||
 | 
					import { isIOS } from "@/app/utils";
 | 
				
			||||||
 | 
					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 isMobileScreen = useMobileScreen();
 | 
				
			||||||
 | 
					  const isIOSMobile = useMemo(
 | 
				
			||||||
 | 
					    () => isIOS() && isMobileScreen,
 | 
				
			||||||
 | 
					    [isMobileScreen],
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useListenWinResize();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div
 | 
				
			||||||
 | 
					      className={`
 | 
				
			||||||
 | 
					         flex h-[100%] w-[100%] bg-center
 | 
				
			||||||
 | 
					        max-md:relative  max-md:flex-col-reverse  max-md:bg-global-mobile
 | 
				
			||||||
 | 
					        md:overflow-hidden md:bg-global
 | 
				
			||||||
 | 
					      `}
 | 
				
			||||||
 | 
					      style={{
 | 
				
			||||||
 | 
					        direction: getLang() === "ar" ? "rtl" : "ltr",
 | 
				
			||||||
 | 
					      }}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      {isAuth ? (
 | 
				
			||||||
 | 
					        props.noAuth
 | 
				
			||||||
 | 
					      ) : (
 | 
				
			||||||
 | 
					        <>
 | 
				
			||||||
 | 
					          <div
 | 
				
			||||||
 | 
					            className={`
 | 
				
			||||||
 | 
					              max-md:absolute max-md:w-[100%] max-md:bottom-0 max-md:z-10
 | 
				
			||||||
 | 
					              md:flex-0 md:overflow-hidden
 | 
				
			||||||
 | 
					            `}
 | 
				
			||||||
 | 
					            id={SIDEBAR_ID}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            {props.sidebar}
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <div
 | 
				
			||||||
 | 
					            className={`
 | 
				
			||||||
 | 
					              h-[100%]
 | 
				
			||||||
 | 
					              max-md:w-[100%] 
 | 
				
			||||||
 | 
					              md:flex-1 md:min-w-0 md:overflow-hidden md:flex
 | 
				
			||||||
 | 
					            `}
 | 
				
			||||||
 | 
					            id={SlotID.AppBody}
 | 
				
			||||||
 | 
					            style={{
 | 
				
			||||||
 | 
					              // #3016 disable transition on ios mobile screen
 | 
				
			||||||
 | 
					              transition: isIOSMobile ? "none" : undefined,
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            {props.children}
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										24
									
								
								app/components/Search/index.module.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,24 @@
 | 
				
			|||||||
 | 
					.search {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    max-width: 460px;
 | 
				
			||||||
 | 
					    height: 50px;
 | 
				
			||||||
 | 
					    padding: 16px;
 | 
				
			||||||
 | 
					    align-items: center;
 | 
				
			||||||
 | 
					    gap: 8px;
 | 
				
			||||||
 | 
					    flex-shrink: 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    border-radius: 16px;
 | 
				
			||||||
 | 
					    border: 1px solid var(--Light-Text-Black, #18182A);
 | 
				
			||||||
 | 
					    background: var(--light-opacity-white-70, rgba(255, 255, 255, 0.70));
 | 
				
			||||||
 | 
					    box-shadow: 0px 8px 40px 0px rgba(60, 68, 255, 0.12);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .icon {
 | 
				
			||||||
 | 
					        height: 20px;
 | 
				
			||||||
 | 
					        width: 20px;
 | 
				
			||||||
 | 
					        flex: 0 0;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    .input {
 | 
				
			||||||
 | 
					        height: 18px;
 | 
				
			||||||
 | 
					        flex: 1 1;
 | 
				
			||||||
 | 
					    } 
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										30
									
								
								app/components/Search/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,30 @@
 | 
				
			|||||||
 | 
					import styles from "./index.module.scss";
 | 
				
			||||||
 | 
					import SearchIcon from "@/app/icons/search.svg";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface SearchProps {
 | 
				
			||||||
 | 
					  value?: string;
 | 
				
			||||||
 | 
					  onSearch?: (v: string) => void;
 | 
				
			||||||
 | 
					  placeholder?: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Search = (props: SearchProps) => {
 | 
				
			||||||
 | 
					  const { placeholder = "", value, onSearch } = props;
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className={styles["search"]}>
 | 
				
			||||||
 | 
					      <div className={styles["icon"]}>
 | 
				
			||||||
 | 
					        <SearchIcon />
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <input
 | 
				
			||||||
 | 
					        className={styles["input"]}
 | 
				
			||||||
 | 
					        placeholder={placeholder}
 | 
				
			||||||
 | 
					        value={value}
 | 
				
			||||||
 | 
					        onChange={(e) => {
 | 
				
			||||||
 | 
					          e.preventDefault();
 | 
				
			||||||
 | 
					          onSearch?.(e.target.value);
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default Search;
 | 
				
			||||||
							
								
								
									
										118
									
								
								app/components/Select/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,118 @@
 | 
				
			|||||||
 | 
					import SelectIcon from "@/app/icons/downArrowIcon.svg";
 | 
				
			||||||
 | 
					import Popover from "@/app/components/Popover";
 | 
				
			||||||
 | 
					import React, { useContext, useMemo, useRef } from "react";
 | 
				
			||||||
 | 
					import useRelativePosition, {
 | 
				
			||||||
 | 
					  Orientation,
 | 
				
			||||||
 | 
					} from "@/app/hooks/useRelativePosition";
 | 
				
			||||||
 | 
					import List from "@/app/components/List";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import Selected from "@/app/icons/selectedIcon.svg";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type Option<Value> = {
 | 
				
			||||||
 | 
					  value: Value;
 | 
				
			||||||
 | 
					  label: string;
 | 
				
			||||||
 | 
					  icon?: React.ReactNode;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface SearchProps<Value> {
 | 
				
			||||||
 | 
					  value?: string;
 | 
				
			||||||
 | 
					  onSelect?: (v: Value) => void;
 | 
				
			||||||
 | 
					  options?: Option<Value>[];
 | 
				
			||||||
 | 
					  inMobile?: boolean;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Select = <Value extends number | string>(props: SearchProps<Value>) => {
 | 
				
			||||||
 | 
					  const { value, onSelect, options = [], inMobile } = props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const { isMobileScreen, selectClassName } = useContext(List.ListContext);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const optionsRef = useRef<Option<Value>[]>([]);
 | 
				
			||||||
 | 
					  optionsRef.current = options;
 | 
				
			||||||
 | 
					  const selectedOption = useMemo(
 | 
				
			||||||
 | 
					    () => optionsRef.current.find((o) => o.value === value),
 | 
				
			||||||
 | 
					    [value],
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const contentRef = useRef<HTMLDivElement>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const { position, getRelativePosition } = useRelativePosition({
 | 
				
			||||||
 | 
					    delay: 0,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let headerH = 100;
 | 
				
			||||||
 | 
					  let baseH = position?.poi.distanceToBottomBoundary || 0;
 | 
				
			||||||
 | 
					  if (isMobileScreen) {
 | 
				
			||||||
 | 
					    headerH = 60;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (position?.poi.relativePosition[1] === Orientation.bottom) {
 | 
				
			||||||
 | 
					    baseH = position?.poi.distanceToTopBoundary;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const maxHeight = `${baseH - headerH}px`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const content = (
 | 
				
			||||||
 | 
					    <div
 | 
				
			||||||
 | 
					      className={` flex flex-col gap-1 overflow-y-auto overflow-x-hidden`}
 | 
				
			||||||
 | 
					      style={{ maxHeight }}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      {options?.map((o) => (
 | 
				
			||||||
 | 
					        <div
 | 
				
			||||||
 | 
					          key={o.value}
 | 
				
			||||||
 | 
					          className={`
 | 
				
			||||||
 | 
					            flex items-center px-3 py-2 gap-3 rounded-action-btn hover:bg-select-option-hovered cursor-pointer
 | 
				
			||||||
 | 
					          `}
 | 
				
			||||||
 | 
					          onClick={() => {
 | 
				
			||||||
 | 
					            onSelect?.(o.value);
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <div className="flex gap-2 flex-1 follow-parent-svg text-text-select-option">
 | 
				
			||||||
 | 
					            {!!o.icon && <div className="flex items-center">{o.icon}</div>}
 | 
				
			||||||
 | 
					            <div className={`flex-1 text-text-select-option`}>{o.label}</div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          <div
 | 
				
			||||||
 | 
					            className={
 | 
				
			||||||
 | 
					              selectedOption?.value === o.value ? "opacity-100" : "opacity-0"
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <Selected />
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      ))}
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Popover
 | 
				
			||||||
 | 
					      content={content}
 | 
				
			||||||
 | 
					      trigger="click"
 | 
				
			||||||
 | 
					      noArrow
 | 
				
			||||||
 | 
					      placement={
 | 
				
			||||||
 | 
					        position?.poi.relativePosition[1] !== Orientation.bottom ? "rb" : "rt"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      popoverClassName="border border-select-popover rounded-lg shadow-select-popover-shadow w-actions-popover  bg-select-popover-panel"
 | 
				
			||||||
 | 
					      onShow={(e) => {
 | 
				
			||||||
 | 
					        getRelativePosition(contentRef.current!, "");
 | 
				
			||||||
 | 
					      }}
 | 
				
			||||||
 | 
					      className={selectClassName}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <div
 | 
				
			||||||
 | 
					        className={`flex items-center gap-3 py-2 px-3 bg-select rounded-action-btn font-common text-sm-title  cursor-pointer hover:bg-select-hover transition duration-300 ease-in-out`}
 | 
				
			||||||
 | 
					        ref={contentRef}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <div
 | 
				
			||||||
 | 
					          className={`flex items-center gap-2 flex-1 follow-parent-svg text-text-select`}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          {!!selectedOption?.icon && (
 | 
				
			||||||
 | 
					            <div className={``}>{selectedOption?.icon}</div>
 | 
				
			||||||
 | 
					          )}
 | 
				
			||||||
 | 
					          <div className={`flex-1`}>{selectedOption?.label}</div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <div className={``}>
 | 
				
			||||||
 | 
					          <SelectIcon />
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </Popover>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default Select;
 | 
				
			||||||
							
								
								
									
										99
									
								
								app/components/SlideRange/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,99 @@
 | 
				
			|||||||
 | 
					import { useContext, useEffect, useRef } from "react";
 | 
				
			||||||
 | 
					import { ListContext } from "@/app/components/List";
 | 
				
			||||||
 | 
					import { useResizeObserver } from "usehooks-ts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface SlideRangeProps {
 | 
				
			||||||
 | 
					  className?: string;
 | 
				
			||||||
 | 
					  description?: string;
 | 
				
			||||||
 | 
					  range?: {
 | 
				
			||||||
 | 
					    start?: number;
 | 
				
			||||||
 | 
					    stroke?: number;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  onSlide?: (v: number) => void;
 | 
				
			||||||
 | 
					  value?: number;
 | 
				
			||||||
 | 
					  step?: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const margin = 15;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function SlideRange(props: SlideRangeProps) {
 | 
				
			||||||
 | 
					  const {
 | 
				
			||||||
 | 
					    className = "",
 | 
				
			||||||
 | 
					    description = "",
 | 
				
			||||||
 | 
					    range = {},
 | 
				
			||||||
 | 
					    value,
 | 
				
			||||||
 | 
					    onSlide,
 | 
				
			||||||
 | 
					    step,
 | 
				
			||||||
 | 
					  } = props;
 | 
				
			||||||
 | 
					  const { start = 0, stroke = 1 } = range;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const { rangeClassName, update } = useContext(ListContext);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const slideRef = useRef<HTMLDivElement>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useResizeObserver({
 | 
				
			||||||
 | 
					    ref: slideRef,
 | 
				
			||||||
 | 
					    onResize: () => {
 | 
				
			||||||
 | 
					      setProperty(value);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const transformToWidth = (x: number = start) => {
 | 
				
			||||||
 | 
					    const abs = x - start;
 | 
				
			||||||
 | 
					    const maxWidth = (slideRef.current?.clientWidth || 1) - margin * 2;
 | 
				
			||||||
 | 
					    const result = (abs / stroke) * maxWidth;
 | 
				
			||||||
 | 
					    return result;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const setProperty = (value?: number) => {
 | 
				
			||||||
 | 
					    const initWidth = transformToWidth(value);
 | 
				
			||||||
 | 
					    slideRef.current?.style.setProperty(
 | 
				
			||||||
 | 
					      "--slide-value-size",
 | 
				
			||||||
 | 
					      `${initWidth + margin}px`,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    update?.({ type: "range" });
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div
 | 
				
			||||||
 | 
					      className={`flex flex-col justify-center items-end gap-1 w-[100%] ${className} ${rangeClassName}`}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      {!!description && (
 | 
				
			||||||
 | 
					        <div className=" text-common text-sm ">{description}</div>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					      <div
 | 
				
			||||||
 | 
					        className="flex my-1.5 relative w-[100%] h-1.5 bg-slider rounded-slide cursor-pointer"
 | 
				
			||||||
 | 
					        ref={slideRef}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <div className="cursor-pointer absolute  marker:top-0 h-[100%] w-[var(--slide-value-size)]  bg-slider-slided-travel rounded-slide">
 | 
				
			||||||
 | 
					           
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <div
 | 
				
			||||||
 | 
					          className="cursor-pointer absolute z-1 w-[30px] top-[50%] translate-y-[-50%] left-[var(--slide-value-size)] translate-x-[-50%]  h-slide-btn leading-slide-btn text-sm-mobile text-center rounded-slide border border-slider-block bg-slider-block hover:bg-slider-block-hover text-text-slider-block"
 | 
				
			||||||
 | 
					          // onPointerDown={onPointerDown}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          {value}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <input
 | 
				
			||||||
 | 
					          type="range"
 | 
				
			||||||
 | 
					          className="w-[100%] h-[100%] opacity-0 cursor-pointer"
 | 
				
			||||||
 | 
					          value={value}
 | 
				
			||||||
 | 
					          min={start}
 | 
				
			||||||
 | 
					          max={start + stroke}
 | 
				
			||||||
 | 
					          step={step}
 | 
				
			||||||
 | 
					          onChange={(e) => {
 | 
				
			||||||
 | 
					            setProperty(e.target.valueAsNumber);
 | 
				
			||||||
 | 
					            onSlide?.(e.target.valueAsNumber);
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					          style={{
 | 
				
			||||||
 | 
					            marginLeft: margin,
 | 
				
			||||||
 | 
					            marginRight: margin,
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										33
									
								
								app/components/Switch/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,33 @@
 | 
				
			|||||||
 | 
					import * as RadixSwitch from "@radix-ui/react-switch";
 | 
				
			||||||
 | 
					import { useContext } from "react";
 | 
				
			||||||
 | 
					import List from "../List";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface SwitchProps {
 | 
				
			||||||
 | 
					  value: boolean;
 | 
				
			||||||
 | 
					  onChange: (v: boolean) => void;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function Switch(props: SwitchProps) {
 | 
				
			||||||
 | 
					  const { value, onChange } = props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const { switchClassName = "" } = useContext(List.ListContext);
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <RadixSwitch.Root
 | 
				
			||||||
 | 
					      checked={value}
 | 
				
			||||||
 | 
					      onCheckedChange={onChange}
 | 
				
			||||||
 | 
					      className={` 
 | 
				
			||||||
 | 
					        cursor-pointer flex w-switch h-switch p-0.5 box-content rounded-md transition-colors duration-300 ease-in-out
 | 
				
			||||||
 | 
					        ${switchClassName} 
 | 
				
			||||||
 | 
					        ${
 | 
				
			||||||
 | 
					          value
 | 
				
			||||||
 | 
					            ? "bg-switch-checked justify-end"
 | 
				
			||||||
 | 
					            : "bg-switch-unchecked justify-start"
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      `}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <RadixSwitch.Thumb
 | 
				
			||||||
 | 
					        className={` bg-switch-btn block w-4 h-4 drop-shadow-sm rounded-md`}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    </RadixSwitch.Root>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										27
									
								
								app/components/ThumbnailImg/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,27 @@
 | 
				
			|||||||
 | 
					import ImgDeleteIcon from "@/app/icons/imgDeleteIcon.svg";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface ThumbnailProps {
 | 
				
			||||||
 | 
					  image: string;
 | 
				
			||||||
 | 
					  deleteImage: () => void;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function Thumbnail(props: ThumbnailProps) {
 | 
				
			||||||
 | 
					  const { image, deleteImage } = props;
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div
 | 
				
			||||||
 | 
					      className={` h-thumbnail w-thumbnail cursor-default border border-thumbnail rounded-action-btn flex-0 bg-cover bg-center`}
 | 
				
			||||||
 | 
					      style={{ backgroundImage: `url("${image}")` }}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <div
 | 
				
			||||||
 | 
					        className={` w-[100%] h-[100%] opacity-0 transition-all duration-200 rounded-action-btn hover:opacity-100 hover:bg-thumbnail-mask`}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <div
 | 
				
			||||||
 | 
					          className={`cursor-pointer flex items-center justify-center float-right`}
 | 
				
			||||||
 | 
					          onClick={deleteImage}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <ImgDeleteIcon />
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -6,6 +6,8 @@
 | 
				
			|||||||
  width: 100%;
 | 
					  width: 100%;
 | 
				
			||||||
  flex-direction: column;
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  background-color: var(--white);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .auth-logo {
 | 
					  .auth-logo {
 | 
				
			||||||
    transform: scale(1.4);
 | 
					    transform: scale(1.4);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -33,4 +35,18 @@
 | 
				
			|||||||
      margin-bottom: 10px;
 | 
					      margin-bottom: 10px;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					  input[type="number"],
 | 
				
			||||||
 | 
					  input[type="text"],
 | 
				
			||||||
 | 
					  input[type="password"] {
 | 
				
			||||||
 | 
					    appearance: none;
 | 
				
			||||||
 | 
					    border-radius: 10px;
 | 
				
			||||||
 | 
					    border: var(--border-in-light);
 | 
				
			||||||
 | 
					    min-height: 36px;
 | 
				
			||||||
 | 
					    box-sizing: border-box;
 | 
				
			||||||
 | 
					    background: var(--white);
 | 
				
			||||||
 | 
					    color: var(--black);
 | 
				
			||||||
 | 
					    padding: 0 10px;
 | 
				
			||||||
 | 
					    max-width: 50%;
 | 
				
			||||||
 | 
					    font-family: inherit;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,6 +2,9 @@
 | 
				
			|||||||
  &-body {
 | 
					  &-body {
 | 
				
			||||||
    margin-top: 20px;
 | 
					    margin-top: 20px;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					  div:not(.no-dark) > svg {
 | 
				
			||||||
 | 
					    filter: invert(0.5);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.export-content {
 | 
					.export-content {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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`,
 | 
				
			||||||
      }}
 | 
					      }}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,6 +4,10 @@
 | 
				
			|||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  flex-direction: column;
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  div:not(.no-dark) > svg {
 | 
				
			||||||
 | 
					    filter: invert(0.5);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .mask-page-body {
 | 
					  .mask-page-body {
 | 
				
			||||||
    padding: 20px;
 | 
					    padding: 20px;
 | 
				
			||||||
    overflow-y: auto;
 | 
					    overflow-y: auto;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,4 @@
 | 
				
			|||||||
import { IconButton } from "./button";
 | 
					import { IconButton } from "./button";
 | 
				
			||||||
import { ErrorBoundary } from "./error";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import styles from "./mask.module.scss";
 | 
					import styles from "./mask.module.scss";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -56,6 +55,7 @@ import {
 | 
				
			|||||||
  OnDragEndResponder,
 | 
					  OnDragEndResponder,
 | 
				
			||||||
} from "@hello-pangea/dnd";
 | 
					} from "@hello-pangea/dnd";
 | 
				
			||||||
import { getMessageTextContent } from "../utils";
 | 
					import { getMessageTextContent } from "../utils";
 | 
				
			||||||
 | 
					import useMobileScreen from "@/app/hooks/useMobileScreen";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// drag and drop helper function
 | 
					// drag and drop helper function
 | 
				
			||||||
function reorder<T>(list: T[], startIndex: number, endIndex: number): T[] {
 | 
					function reorder<T>(list: T[], startIndex: number, endIndex: number): T[] {
 | 
				
			||||||
@@ -398,7 +398,7 @@ export function ContextPrompts(props: {
 | 
				
			|||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function MaskPage() {
 | 
					export function MaskPage(props: { className?: string }) {
 | 
				
			||||||
  const navigate = useNavigate();
 | 
					  const navigate = useNavigate();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const maskStore = useMaskStore();
 | 
					  const maskStore = useMaskStore();
 | 
				
			||||||
@@ -466,8 +466,13 @@ export function MaskPage() {
 | 
				
			|||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <ErrorBoundary>
 | 
					    <>
 | 
				
			||||||
      <div className={styles["mask-page"]}>
 | 
					      <div
 | 
				
			||||||
 | 
					        className={`
 | 
				
			||||||
 | 
					          ${styles["mask-page"]} 
 | 
				
			||||||
 | 
					          ${props.className}
 | 
				
			||||||
 | 
					          `}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
        <div className="window-header">
 | 
					        <div className="window-header">
 | 
				
			||||||
          <div className="window-header-title">
 | 
					          <div className="window-header-title">
 | 
				
			||||||
            <div className="window-header-main-title">
 | 
					            <div className="window-header-main-title">
 | 
				
			||||||
@@ -645,6 +650,6 @@ export function MaskPage() {
 | 
				
			|||||||
          </Modal>
 | 
					          </Modal>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      )}
 | 
					      )}
 | 
				
			||||||
    </ErrorBoundary>
 | 
					    </>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,6 +8,10 @@
 | 
				
			|||||||
  justify-content: center;
 | 
					  justify-content: center;
 | 
				
			||||||
  flex-direction: column;
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  div:not(.no-dark) > svg {
 | 
				
			||||||
 | 
					    filter: invert(0.5);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .mask-header {
 | 
					  .mask-header {
 | 
				
			||||||
    display: flex;
 | 
					    display: flex;
 | 
				
			||||||
    justify-content: space-between;
 | 
					    justify-content: space-between;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,6 +16,7 @@ import { MaskAvatar } from "./mask";
 | 
				
			|||||||
import { useCommand } from "../command";
 | 
					import { useCommand } from "../command";
 | 
				
			||||||
import { showConfirm } from "./ui-lib";
 | 
					import { showConfirm } from "./ui-lib";
 | 
				
			||||||
import { BUILTIN_MASK_STORE } from "../masks";
 | 
					import { BUILTIN_MASK_STORE } from "../masks";
 | 
				
			||||||
 | 
					import useMobileScreen from "@/app/hooks/useMobileScreen";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function MaskItem(props: { mask: Mask; onClick?: () => void }) {
 | 
					function MaskItem(props: { mask: Mask; onClick?: () => void }) {
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
@@ -71,7 +72,7 @@ function useMaskGroup(masks: Mask[]) {
 | 
				
			|||||||
  return groups;
 | 
					  return groups;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function NewChat() {
 | 
					export function NewChat(props: { className?: string }) {
 | 
				
			||||||
  const chatStore = useChatStore();
 | 
					  const chatStore = useChatStore();
 | 
				
			||||||
  const maskStore = useMaskStore();
 | 
					  const maskStore = useMaskStore();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -110,8 +111,15 @@ export function NewChat() {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }, [groups]);
 | 
					  }, [groups]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const isMobileScreen = useMobileScreen();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div className={styles["new-chat"]}>
 | 
					    <div
 | 
				
			||||||
 | 
					      className={`
 | 
				
			||||||
 | 
					      ${styles["new-chat"]}
 | 
				
			||||||
 | 
					      ${props.className}
 | 
				
			||||||
 | 
					      `}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
      <div className={styles["mask-header"]}>
 | 
					      <div className={styles["mask-header"]}>
 | 
				
			||||||
        <IconButton
 | 
					        <IconButton
 | 
				
			||||||
          icon={<LeftIcon />}
 | 
					          icon={<LeftIcon />}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -101,6 +101,7 @@ interface ModalProps {
 | 
				
			|||||||
  defaultMax?: boolean;
 | 
					  defaultMax?: boolean;
 | 
				
			||||||
  footer?: React.ReactNode;
 | 
					  footer?: React.ReactNode;
 | 
				
			||||||
  onClose?: () => void;
 | 
					  onClose?: () => void;
 | 
				
			||||||
 | 
					  className?: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
export function Modal(props: ModalProps) {
 | 
					export function Modal(props: ModalProps) {
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
@@ -122,14 +123,14 @@ export function Modal(props: ModalProps) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div
 | 
					    <div
 | 
				
			||||||
      className={
 | 
					      className={`${styles["modal-container"]} ${
 | 
				
			||||||
        styles["modal-container"] + ` ${isMax && styles["modal-container-max"]}`
 | 
					        isMax && styles["modal-container-max"]
 | 
				
			||||||
      }
 | 
					      } ${props.className ?? ""}`}
 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
      <div className={styles["modal-header"]}>
 | 
					      <div className={`${styles["modal-header"]} new-header follow-parent-svg`}>
 | 
				
			||||||
        <div className={styles["modal-title"]}>{props.title}</div>
 | 
					        <div className={`${styles["modal-title"]}`}>{props.title}</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <div className={styles["modal-header-actions"]}>
 | 
					        <div className={`${styles["modal-header-actions"]}`}>
 | 
				
			||||||
          <div
 | 
					          <div
 | 
				
			||||||
            className={styles["modal-header-action"]}
 | 
					            className={styles["modal-header-action"]}
 | 
				
			||||||
            onClick={() => setMax(!isMax)}
 | 
					            onClick={() => setMax(!isMax)}
 | 
				
			||||||
@@ -147,11 +148,11 @@ export function Modal(props: ModalProps) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      <div className={styles["modal-content"]}>{props.children}</div>
 | 
					      <div className={styles["modal-content"]}>{props.children}</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <div className={styles["modal-footer"]}>
 | 
					      <div className={`${styles["modal-footer"]} new-footer`}>
 | 
				
			||||||
        {props.footer}
 | 
					        {props.footer}
 | 
				
			||||||
        <div className={styles["modal-actions"]}>
 | 
					        <div className={styles["modal-actions"]}>
 | 
				
			||||||
          {props.actions?.map((action, i) => (
 | 
					          {props.actions?.map((action, i) => (
 | 
				
			||||||
            <div key={i} className={styles["modal-action"]}>
 | 
					            <div key={i} className={`${styles["modal-action"]} new-btn`}>
 | 
				
			||||||
              {action}
 | 
					              {action}
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
          ))}
 | 
					          ))}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -49,11 +49,18 @@ export enum StoreKey {
 | 
				
			|||||||
  Sync = "sync",
 | 
					  Sync = "sync",
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const DEFAULT_SIDEBAR_WIDTH = 300;
 | 
					 | 
				
			||||||
export const MAX_SIDEBAR_WIDTH = 500;
 | 
					 | 
				
			||||||
export const MIN_SIDEBAR_WIDTH = 230;
 | 
					 | 
				
			||||||
export const NARROW_SIDEBAR_WIDTH = 100;
 | 
					export const NARROW_SIDEBAR_WIDTH = 100;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const DEFAULT_SIDEBAR_WIDTH = 340;
 | 
				
			||||||
 | 
					export const MAX_SIDEBAR_WIDTH = 440;
 | 
				
			||||||
 | 
					export const MIN_SIDEBAR_WIDTH = 230;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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-";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const LAST_INPUT_KEY = "last-input";
 | 
					export const LAST_INPUT_KEY = "last-input";
 | 
				
			||||||
@@ -218,3 +225,5 @@ export const internalWhiteWebDavEndpoints = [
 | 
				
			|||||||
  "https://webdav.yandex.com",
 | 
					  "https://webdav.yandex.com",
 | 
				
			||||||
  "https://app.koofr.net/dav/Koofr",
 | 
					  "https://app.koofr.net/dav/Koofr",
 | 
				
			||||||
];
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const SIDEBAR_ID = "sidebar";
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										301
									
								
								app/containers/Chat/ChatPanel.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,301 @@
 | 
				
			|||||||
 | 
					import React, { useState, useRef, useEffect, useMemo } from "react";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  useChatStore,
 | 
				
			||||||
 | 
					  BOT_HELLO,
 | 
				
			||||||
 | 
					  createMessage,
 | 
				
			||||||
 | 
					  useAccessStore,
 | 
				
			||||||
 | 
					  useAppConfig,
 | 
				
			||||||
 | 
					  ModelType,
 | 
				
			||||||
 | 
					} from "@/app/store";
 | 
				
			||||||
 | 
					import Locale from "@/app/locales";
 | 
				
			||||||
 | 
					import { Selector, showConfirm, showToast } from "@/app/components/ui-lib";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  CHAT_PAGE_SIZE,
 | 
				
			||||||
 | 
					  REQUEST_TIMEOUT_MS,
 | 
				
			||||||
 | 
					  UNFINISHED_INPUT,
 | 
				
			||||||
 | 
					} from "@/app/constant";
 | 
				
			||||||
 | 
					import { useCommand } from "@/app/command";
 | 
				
			||||||
 | 
					import { prettyObject } from "@/app/utils/format";
 | 
				
			||||||
 | 
					import { ExportMessageModal } from "@/app/components/exporter";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import PromptToast from "./components/PromptToast";
 | 
				
			||||||
 | 
					import { EditMessageModal } from "./components/EditMessageModal";
 | 
				
			||||||
 | 
					import ChatHeader from "./components/ChatHeader";
 | 
				
			||||||
 | 
					import ChatInputPanel, {
 | 
				
			||||||
 | 
					  ChatInputPanelInstance,
 | 
				
			||||||
 | 
					} from "./components/ChatInputPanel";
 | 
				
			||||||
 | 
					import ChatMessagePanel, { RenderMessage } from "./components/ChatMessagePanel";
 | 
				
			||||||
 | 
					import { useAllModels } from "@/app/utils/hooks";
 | 
				
			||||||
 | 
					import useRows from "@/app/hooks/useRows";
 | 
				
			||||||
 | 
					import SessionConfigModel from "./components/SessionConfigModal";
 | 
				
			||||||
 | 
					import useScrollToBottom from "@/app/hooks/useScrollToBottom";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function _Chat() {
 | 
				
			||||||
 | 
					  const chatStore = useChatStore();
 | 
				
			||||||
 | 
					  const session = chatStore.currentSession();
 | 
				
			||||||
 | 
					  const config = useAppConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const { isMobileScreen } = config;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [showExport, setShowExport] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const inputRef = useRef<HTMLTextAreaElement>(null);
 | 
				
			||||||
 | 
					  const [userInput, setUserInput] = useState("");
 | 
				
			||||||
 | 
					  const [isLoading, setIsLoading] = useState(false);
 | 
				
			||||||
 | 
					  const scrollRef = useRef<HTMLDivElement>(null);
 | 
				
			||||||
 | 
					  const chatInputPanelRef = useRef<ChatInputPanelInstance | null>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [hitBottom, setHitBottom] = useState(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [attachImages, setAttachImages] = useState<string[]>([]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // auto grow input
 | 
				
			||||||
 | 
					  const { measure, inputRows } = useRows({
 | 
				
			||||||
 | 
					    inputRef,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const { setAutoScroll, scrollDomToBottom } = useScrollToBottom(scrollRef);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
				
			||||||
 | 
					  useEffect(measure, [userInput]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    chatStore.updateCurrentSession((session) => {
 | 
				
			||||||
 | 
					      const stopTiming = Date.now() - REQUEST_TIMEOUT_MS;
 | 
				
			||||||
 | 
					      session.messages.forEach((m) => {
 | 
				
			||||||
 | 
					        // check if should stop all stale messages
 | 
				
			||||||
 | 
					        if (m.isError || new Date(m.date).getTime() < stopTiming) {
 | 
				
			||||||
 | 
					          if (m.streaming) {
 | 
				
			||||||
 | 
					            m.streaming = false;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (m.content.length === 0) {
 | 
				
			||||||
 | 
					            m.isError = true;
 | 
				
			||||||
 | 
					            m.content = prettyObject({
 | 
				
			||||||
 | 
					              error: true,
 | 
				
			||||||
 | 
					              message: "empty response",
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // auto sync mask config from global config
 | 
				
			||||||
 | 
					      if (session.mask.syncGlobalConfig) {
 | 
				
			||||||
 | 
					        console.log("[Mask] syncing from global, name = ", session.mask.name);
 | 
				
			||||||
 | 
					        session.mask.modelConfig = { ...config.modelConfig };
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const context: RenderMessage[] = useMemo(() => {
 | 
				
			||||||
 | 
					    return session.mask.hideContext ? [] : session.mask.context.slice();
 | 
				
			||||||
 | 
					  }, [session.mask.context, session.mask.hideContext]);
 | 
				
			||||||
 | 
					  const accessStore = useAccessStore();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (
 | 
				
			||||||
 | 
					    context.length === 0 &&
 | 
				
			||||||
 | 
					    session.messages.at(0)?.content !== BOT_HELLO.content
 | 
				
			||||||
 | 
					  ) {
 | 
				
			||||||
 | 
					    const copiedHello = Object.assign({}, BOT_HELLO);
 | 
				
			||||||
 | 
					    if (!accessStore.isAuthorized()) {
 | 
				
			||||||
 | 
					      copiedHello.content = Locale.Error.Unauthorized;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    context.push(copiedHello);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // preview messages
 | 
				
			||||||
 | 
					  const renderMessages = useMemo(() => {
 | 
				
			||||||
 | 
					    return context
 | 
				
			||||||
 | 
					      .concat(session.messages as RenderMessage[])
 | 
				
			||||||
 | 
					      .concat(
 | 
				
			||||||
 | 
					        isLoading
 | 
				
			||||||
 | 
					          ? [
 | 
				
			||||||
 | 
					              {
 | 
				
			||||||
 | 
					                ...createMessage({
 | 
				
			||||||
 | 
					                  role: "assistant",
 | 
				
			||||||
 | 
					                  content: "……",
 | 
				
			||||||
 | 
					                }),
 | 
				
			||||||
 | 
					                preview: true,
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					            ]
 | 
				
			||||||
 | 
					          : [],
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					      .concat(
 | 
				
			||||||
 | 
					        userInput.length > 0 && config.sendPreviewBubble
 | 
				
			||||||
 | 
					          ? [
 | 
				
			||||||
 | 
					              {
 | 
				
			||||||
 | 
					                ...createMessage(
 | 
				
			||||||
 | 
					                  {
 | 
				
			||||||
 | 
					                    role: "user",
 | 
				
			||||||
 | 
					                    content: userInput,
 | 
				
			||||||
 | 
					                  },
 | 
				
			||||||
 | 
					                  {
 | 
				
			||||||
 | 
					                    customId: "typing",
 | 
				
			||||||
 | 
					                  },
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                preview: true,
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					            ]
 | 
				
			||||||
 | 
					          : [],
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					  }, [
 | 
				
			||||||
 | 
					    config.sendPreviewBubble,
 | 
				
			||||||
 | 
					    context,
 | 
				
			||||||
 | 
					    isLoading,
 | 
				
			||||||
 | 
					    session.messages,
 | 
				
			||||||
 | 
					    userInput,
 | 
				
			||||||
 | 
					  ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [msgRenderIndex, _setMsgRenderIndex] = useState(
 | 
				
			||||||
 | 
					    Math.max(0, renderMessages.length - CHAT_PAGE_SIZE),
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [showPromptModal, setShowPromptModal] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useCommand({
 | 
				
			||||||
 | 
					    fill: setUserInput,
 | 
				
			||||||
 | 
					    submit: (text) => {
 | 
				
			||||||
 | 
					      chatInputPanelRef.current?.doSubmit(text);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    code: (text) => {
 | 
				
			||||||
 | 
					      if (accessStore.disableFastLink) return;
 | 
				
			||||||
 | 
					      console.log("[Command] got code from url: ", text);
 | 
				
			||||||
 | 
					      showConfirm(Locale.URLCommand.Code + `code = ${text}`).then((res) => {
 | 
				
			||||||
 | 
					        if (res) {
 | 
				
			||||||
 | 
					          accessStore.update((access) => (access.accessCode = text));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    settings: (text) => {
 | 
				
			||||||
 | 
					      if (accessStore.disableFastLink) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const payload = JSON.parse(text) as {
 | 
				
			||||||
 | 
					          key?: string;
 | 
				
			||||||
 | 
					          url?: string;
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        console.log("[Command] got settings from url: ", payload);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (payload.key || payload.url) {
 | 
				
			||||||
 | 
					          showConfirm(
 | 
				
			||||||
 | 
					            Locale.URLCommand.Settings +
 | 
				
			||||||
 | 
					              `\n${JSON.stringify(payload, null, 4)}`,
 | 
				
			||||||
 | 
					          ).then((res) => {
 | 
				
			||||||
 | 
					            if (!res) return;
 | 
				
			||||||
 | 
					            if (payload.key) {
 | 
				
			||||||
 | 
					              accessStore.update(
 | 
				
			||||||
 | 
					                (access) => (access.openaiApiKey = payload.key!),
 | 
				
			||||||
 | 
					              );
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if (payload.url) {
 | 
				
			||||||
 | 
					              accessStore.update((access) => (access.openaiUrl = payload.url!));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch {
 | 
				
			||||||
 | 
					        console.error("[Command] failed to get settings from url: ", text);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // edit / insert message modal
 | 
				
			||||||
 | 
					  const [isEditingMessage, setIsEditingMessage] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // remember unfinished input
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    // try to load from local storage
 | 
				
			||||||
 | 
					    const key = UNFINISHED_INPUT(session.id);
 | 
				
			||||||
 | 
					    const mayBeUnfinishedInput = localStorage.getItem(key);
 | 
				
			||||||
 | 
					    if (mayBeUnfinishedInput && userInput.length === 0) {
 | 
				
			||||||
 | 
					      setUserInput(mayBeUnfinishedInput);
 | 
				
			||||||
 | 
					      localStorage.removeItem(key);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const dom = inputRef.current;
 | 
				
			||||||
 | 
					    return () => {
 | 
				
			||||||
 | 
					      localStorage.setItem(key, dom?.value ?? "");
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const chatinputPanelProps = {
 | 
				
			||||||
 | 
					    inputRef,
 | 
				
			||||||
 | 
					    isMobileScreen,
 | 
				
			||||||
 | 
					    renderMessages,
 | 
				
			||||||
 | 
					    attachImages,
 | 
				
			||||||
 | 
					    userInput,
 | 
				
			||||||
 | 
					    hitBottom,
 | 
				
			||||||
 | 
					    inputRows,
 | 
				
			||||||
 | 
					    setAttachImages,
 | 
				
			||||||
 | 
					    setUserInput,
 | 
				
			||||||
 | 
					    setIsLoading,
 | 
				
			||||||
 | 
					    showChatSetting: setShowPromptModal,
 | 
				
			||||||
 | 
					    _setMsgRenderIndex,
 | 
				
			||||||
 | 
					    scrollDomToBottom,
 | 
				
			||||||
 | 
					    setAutoScroll,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const chatMessagePanelProps = {
 | 
				
			||||||
 | 
					    scrollRef,
 | 
				
			||||||
 | 
					    inputRef,
 | 
				
			||||||
 | 
					    isMobileScreen,
 | 
				
			||||||
 | 
					    msgRenderIndex,
 | 
				
			||||||
 | 
					    userInput,
 | 
				
			||||||
 | 
					    context,
 | 
				
			||||||
 | 
					    renderMessages,
 | 
				
			||||||
 | 
					    setAutoScroll,
 | 
				
			||||||
 | 
					    setMsgRenderIndex: chatInputPanelRef.current?.setMsgRenderIndex,
 | 
				
			||||||
 | 
					    setHitBottom,
 | 
				
			||||||
 | 
					    setUserInput,
 | 
				
			||||||
 | 
					    setIsLoading,
 | 
				
			||||||
 | 
					    setShowPromptModal,
 | 
				
			||||||
 | 
					    scrollDomToBottom,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div
 | 
				
			||||||
 | 
					      className={`
 | 
				
			||||||
 | 
					        relative flex flex-col overflow-hidden bg-chat-panel
 | 
				
			||||||
 | 
					        max-md:absolute max-md:h-[100vh] max-md:w-[100%]
 | 
				
			||||||
 | 
					        md:h-[100%] md:mr-2.5 md:rounded-md
 | 
				
			||||||
 | 
					        `}
 | 
				
			||||||
 | 
					      key={session.id}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <ChatHeader
 | 
				
			||||||
 | 
					        setIsEditingMessage={setIsEditingMessage}
 | 
				
			||||||
 | 
					        setShowExport={setShowExport}
 | 
				
			||||||
 | 
					        isMobileScreen={isMobileScreen}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <ChatMessagePanel {...chatMessagePanelProps} />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <ChatInputPanel ref={chatInputPanelRef} {...chatinputPanelProps} />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      {showExport && (
 | 
				
			||||||
 | 
					        <ExportMessageModal onClose={() => setShowExport(false)} />
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      {isEditingMessage && (
 | 
				
			||||||
 | 
					        <EditMessageModal
 | 
				
			||||||
 | 
					          onClose={() => {
 | 
				
			||||||
 | 
					            setIsEditingMessage(false);
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <PromptToast showToast={!hitBottom} setShowModal={setShowPromptModal} />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      {showPromptModal && (
 | 
				
			||||||
 | 
					        <SessionConfigModel onClose={() => setShowPromptModal(false)} />
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function Chat() {
 | 
				
			||||||
 | 
					  const chatStore = useChatStore();
 | 
				
			||||||
 | 
					  const sessionIndex = chatStore.currentSessionIndex;
 | 
				
			||||||
 | 
					  return <_Chat key={sessionIndex}></_Chat>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										277
									
								
								app/containers/Chat/components/ChatActions.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,277 @@
 | 
				
			|||||||
 | 
					import { useNavigate } from "react-router-dom";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { ModelType, Theme, useAppConfig } from "@/app/store/config";
 | 
				
			||||||
 | 
					import { useChatStore } from "@/app/store/chat";
 | 
				
			||||||
 | 
					import { ChatControllerPool } from "@/app/client/controller";
 | 
				
			||||||
 | 
					import { useAllModels } from "@/app/utils/hooks";
 | 
				
			||||||
 | 
					import { useEffect, useMemo, useState } from "react";
 | 
				
			||||||
 | 
					import { isVisionModel } from "@/app/utils";
 | 
				
			||||||
 | 
					import { showToast } from "@/app/components/ui-lib";
 | 
				
			||||||
 | 
					import Locale from "@/app/locales";
 | 
				
			||||||
 | 
					import { Path } from "@/app/constant";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import BottomIcon from "@/app/icons/bottom.svg";
 | 
				
			||||||
 | 
					import StopIcon from "@/app/icons/pause.svg";
 | 
				
			||||||
 | 
					import LoadingButtonIcon from "@/app/icons/loading.svg";
 | 
				
			||||||
 | 
					import PromptIcon from "@/app/icons/comandIcon.svg";
 | 
				
			||||||
 | 
					import MaskIcon from "@/app/icons/maskIcon.svg";
 | 
				
			||||||
 | 
					import BreakIcon from "@/app/icons/eraserIcon.svg";
 | 
				
			||||||
 | 
					import SettingsIcon from "@/app/icons/configIcon.svg";
 | 
				
			||||||
 | 
					import ImageIcon from "@/app/icons/uploadImgIcon.svg";
 | 
				
			||||||
 | 
					import AddCircleIcon from "@/app/icons/addCircle.svg";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import Popover from "@/app/components/Popover";
 | 
				
			||||||
 | 
					import ModelSelect from "./ModelSelect";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface Action {
 | 
				
			||||||
 | 
					  onClick?: () => void;
 | 
				
			||||||
 | 
					  text: string;
 | 
				
			||||||
 | 
					  isShow: boolean;
 | 
				
			||||||
 | 
					  render?: (key: string) => JSX.Element;
 | 
				
			||||||
 | 
					  icon?: JSX.Element;
 | 
				
			||||||
 | 
					  placement: "left" | "right";
 | 
				
			||||||
 | 
					  className?: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function ChatActions(props: {
 | 
				
			||||||
 | 
					  uploadImage: () => void;
 | 
				
			||||||
 | 
					  setAttachImages: (images: string[]) => void;
 | 
				
			||||||
 | 
					  setUploading: (uploading: boolean) => void;
 | 
				
			||||||
 | 
					  showChatSetting: () => void;
 | 
				
			||||||
 | 
					  scrollToBottom: () => void;
 | 
				
			||||||
 | 
					  showPromptHints: () => void;
 | 
				
			||||||
 | 
					  hitBottom: boolean;
 | 
				
			||||||
 | 
					  uploading: boolean;
 | 
				
			||||||
 | 
					  isMobileScreen: boolean;
 | 
				
			||||||
 | 
					  className?: string;
 | 
				
			||||||
 | 
					}) {
 | 
				
			||||||
 | 
					  const config = useAppConfig();
 | 
				
			||||||
 | 
					  const navigate = useNavigate();
 | 
				
			||||||
 | 
					  const chatStore = useChatStore();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // switch themes
 | 
				
			||||||
 | 
					  const theme = config.theme;
 | 
				
			||||||
 | 
					  function nextTheme() {
 | 
				
			||||||
 | 
					    const themes = [Theme.Auto, Theme.Light, Theme.Dark];
 | 
				
			||||||
 | 
					    const themeIndex = themes.indexOf(theme);
 | 
				
			||||||
 | 
					    const nextIndex = (themeIndex + 1) % themes.length;
 | 
				
			||||||
 | 
					    const nextTheme = themes[nextIndex];
 | 
				
			||||||
 | 
					    config.update((config) => (config.theme = nextTheme));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // stop all responses
 | 
				
			||||||
 | 
					  const couldStop = ChatControllerPool.hasPending();
 | 
				
			||||||
 | 
					  const stopAll = () => ChatControllerPool.stopAll();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // switch model
 | 
				
			||||||
 | 
					  const currentModel = chatStore.currentSession().mask.modelConfig.model;
 | 
				
			||||||
 | 
					  const allModels = useAllModels();
 | 
				
			||||||
 | 
					  const models = useMemo(
 | 
				
			||||||
 | 
					    () => allModels.filter((m) => m.available),
 | 
				
			||||||
 | 
					    [allModels],
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					  const [showUploadImage, setShowUploadImage] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    const show = isVisionModel(currentModel);
 | 
				
			||||||
 | 
					    setShowUploadImage(show);
 | 
				
			||||||
 | 
					    if (!show) {
 | 
				
			||||||
 | 
					      props.setAttachImages([]);
 | 
				
			||||||
 | 
					      props.setUploading(false);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // if current model is not available
 | 
				
			||||||
 | 
					    // switch to first available model
 | 
				
			||||||
 | 
					    const isUnavaliableModel = !models.some((m) => m.name === currentModel);
 | 
				
			||||||
 | 
					    if (isUnavaliableModel && models.length > 0) {
 | 
				
			||||||
 | 
					      const nextModel = models[0].name as ModelType;
 | 
				
			||||||
 | 
					      chatStore.updateCurrentSession(
 | 
				
			||||||
 | 
					        (session) => (session.mask.modelConfig.model = nextModel),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      showToast(nextModel);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [chatStore, currentModel, models]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const actions: Action[] = [
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      onClick: stopAll,
 | 
				
			||||||
 | 
					      text: Locale.Chat.InputActions.Stop,
 | 
				
			||||||
 | 
					      isShow: couldStop,
 | 
				
			||||||
 | 
					      icon: <StopIcon />,
 | 
				
			||||||
 | 
					      placement: "left",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      text: currentModel,
 | 
				
			||||||
 | 
					      isShow: !props.isMobileScreen,
 | 
				
			||||||
 | 
					      render: (key: string) => <ModelSelect key={key} />,
 | 
				
			||||||
 | 
					      placement: "left",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      onClick: props.scrollToBottom,
 | 
				
			||||||
 | 
					      text: Locale.Chat.InputActions.ToBottom,
 | 
				
			||||||
 | 
					      isShow: !props.hitBottom,
 | 
				
			||||||
 | 
					      icon: <BottomIcon />,
 | 
				
			||||||
 | 
					      placement: "left",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      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.showChatSetting,
 | 
				
			||||||
 | 
					      text: Locale.Chat.InputActions.Settings,
 | 
				
			||||||
 | 
					      isShow: true,
 | 
				
			||||||
 | 
					      icon: <SettingsIcon />,
 | 
				
			||||||
 | 
					      placement: "right",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (props.isMobileScreen) {
 | 
				
			||||||
 | 
					    const content = (
 | 
				
			||||||
 | 
					      <div className="w-[100%]">
 | 
				
			||||||
 | 
					        {actions
 | 
				
			||||||
 | 
					          .filter((v) => v.isShow && v.icon)
 | 
				
			||||||
 | 
					          .map((act) => {
 | 
				
			||||||
 | 
					            return (
 | 
				
			||||||
 | 
					              <div
 | 
				
			||||||
 | 
					                key={act.text}
 | 
				
			||||||
 | 
					                className={`flex items-center gap-3 p-3 rounded-action-btn leading-6 cursor-pointer follow-parent-svg default-icon-color`}
 | 
				
			||||||
 | 
					                onClick={act.onClick}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                {act.icon}
 | 
				
			||||||
 | 
					                <div className="flex-1 font-common text-actions-popover-menu-item">
 | 
				
			||||||
 | 
					                  {act.text}
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          })}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <Popover
 | 
				
			||||||
 | 
					        content={content}
 | 
				
			||||||
 | 
					        trigger="click"
 | 
				
			||||||
 | 
					        placement="rt"
 | 
				
			||||||
 | 
					        noArrow
 | 
				
			||||||
 | 
					        popoverClassName="border border-chat-actions-popover-mobile rounded-md shadow-chat-actions-popover-mobile w-actions-popover bg-chat-actions-popover-panel-mobile "
 | 
				
			||||||
 | 
					        className=" cursor-pointer follow-parent-svg default-icon-color"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <AddCircleIcon />
 | 
				
			||||||
 | 
					      </Popover>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const popoverClassName = `bg-chat-actions-btn-popover px-3 py-2.5 text-text-chat-actions-btn-popover text-sm-title rounded-md`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className={`flex gap-2 item-center ${props.className}`}>
 | 
				
			||||||
 | 
					      {actions
 | 
				
			||||||
 | 
					        .filter((v) => v.placement === "left" && v.isShow)
 | 
				
			||||||
 | 
					        .map((act, ind) => {
 | 
				
			||||||
 | 
					          if (act.render) {
 | 
				
			||||||
 | 
					            return (
 | 
				
			||||||
 | 
					              <div className={`${act.className ?? ""}`} key={act.text}>
 | 
				
			||||||
 | 
					                {act.render(act.text)}
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          return (
 | 
				
			||||||
 | 
					            <Popover
 | 
				
			||||||
 | 
					              key={act.text}
 | 
				
			||||||
 | 
					              content={act.text}
 | 
				
			||||||
 | 
					              popoverClassName={`${popoverClassName}`}
 | 
				
			||||||
 | 
					              placement={ind ? "t" : "lt"}
 | 
				
			||||||
 | 
					              className={`${act.className ?? ""}`}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              <div
 | 
				
			||||||
 | 
					                className={` 
 | 
				
			||||||
 | 
					                  cursor-pointer h-[32px] w-[32px] flex items-center justify-center transition duration-300 ease-in-out 
 | 
				
			||||||
 | 
					                  hover:bg-chat-actions-btn-hovered hover:rounded-action-btn
 | 
				
			||||||
 | 
					                  follow-parent-svg default-icon-color
 | 
				
			||||||
 | 
					                `}
 | 
				
			||||||
 | 
					                onClick={act.onClick}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                {act.icon}
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </Popover>
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        })}
 | 
				
			||||||
 | 
					      <div className="flex-1"></div>
 | 
				
			||||||
 | 
					      {actions
 | 
				
			||||||
 | 
					        .filter((v) => v.placement === "right" && v.isShow)
 | 
				
			||||||
 | 
					        .map((act, ind, arr) => {
 | 
				
			||||||
 | 
					          return (
 | 
				
			||||||
 | 
					            <Popover
 | 
				
			||||||
 | 
					              key={act.text}
 | 
				
			||||||
 | 
					              content={act.text}
 | 
				
			||||||
 | 
					              popoverClassName={`${popoverClassName}`}
 | 
				
			||||||
 | 
					              placement={ind === arr.length - 1 ? "rt" : "t"}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              <div
 | 
				
			||||||
 | 
					                className={`
 | 
				
			||||||
 | 
					                  cursor-pointer h-[32px] w-[32px] flex items-center transition duration-300 ease-in-out justify-center 
 | 
				
			||||||
 | 
					                  hover:bg-chat-actions-btn-hovered hover:rounded-action-btn
 | 
				
			||||||
 | 
					                  follow-parent-svg default-icon-color
 | 
				
			||||||
 | 
					                `}
 | 
				
			||||||
 | 
					                onClick={act.onClick}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                {act.icon}
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </Popover>
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        })}
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										91
									
								
								app/containers/Chat/components/ChatHeader.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,91 @@
 | 
				
			|||||||
 | 
					import { useNavigate } from "react-router-dom";
 | 
				
			||||||
 | 
					import Locale from "@/app/locales";
 | 
				
			||||||
 | 
					import { Path } from "@/app/constant";
 | 
				
			||||||
 | 
					import { DEFAULT_TOPIC, useChatStore } from "@/app/store/chat";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import LogIcon from "@/app/icons/logIcon.svg";
 | 
				
			||||||
 | 
					import GobackIcon from "@/app/icons/goback.svg";
 | 
				
			||||||
 | 
					import ShareIcon from "@/app/icons/shareIcon.svg";
 | 
				
			||||||
 | 
					import ModelSelect from "./ModelSelect";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface ChatHeaderProps {
 | 
				
			||||||
 | 
					  isMobileScreen: boolean;
 | 
				
			||||||
 | 
					  setIsEditingMessage: (v: boolean) => void;
 | 
				
			||||||
 | 
					  setShowExport: (v: boolean) => void;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function ChatHeader(props: ChatHeaderProps) {
 | 
				
			||||||
 | 
					  const { isMobileScreen, setIsEditingMessage, setShowExport } = props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const navigate = useNavigate();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const chatStore = useChatStore();
 | 
				
			||||||
 | 
					  const session = chatStore.currentSession();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div
 | 
				
			||||||
 | 
					      className={`
 | 
				
			||||||
 | 
					        absolute w-[100%] backdrop-blur-[30px] z-20 flex flex-0 justify-between items-center px-6 py-4 gap-chat-header-gap 
 | 
				
			||||||
 | 
					        sm:border-b sm:border-chat-header-bottom 
 | 
				
			||||||
 | 
					        max-md:h-menu-title-mobile
 | 
				
			||||||
 | 
					      `}
 | 
				
			||||||
 | 
					      data-tauri-drag-region
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <div
 | 
				
			||||||
 | 
					        className={`absolute z-[-1] top-0 left-0 w-[100%] h-[100%] opacity-85 backdrop-blur-[20px]  sm:bg-chat-panel-header-mask bg-chat-panel-header-mobile flex flex-0 justify-between items-center  gap-chat-header-gap`}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        {" "}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      {isMobileScreen ? (
 | 
				
			||||||
 | 
					        <div
 | 
				
			||||||
 | 
					          className=" cursor-pointer follow-parent-svg default-icon-color"
 | 
				
			||||||
 | 
					          onClick={() => navigate(Path.Home)}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <GobackIcon />
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      ) : (
 | 
				
			||||||
 | 
					        <LogIcon />
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div
 | 
				
			||||||
 | 
					        className={`
 | 
				
			||||||
 | 
					        flex-1 
 | 
				
			||||||
 | 
					        max-md:flex max-md:flex-col max-md:items-center max-md:justify-center max-md:gap-0.5 max-md:text
 | 
				
			||||||
 | 
					        md:mr-4
 | 
				
			||||||
 | 
					      `}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <div
 | 
				
			||||||
 | 
					          className={`
 | 
				
			||||||
 | 
					            line-clamp-1 cursor-pointer text-text-chat-header-title text-chat-header-title font-common 
 | 
				
			||||||
 | 
					            max-md:text-sm-title max-md:h-chat-header-title-mobile max-md:leading-5
 | 
				
			||||||
 | 
					          `}
 | 
				
			||||||
 | 
					          onClickCapture={() => setIsEditingMessage(true)}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          {!session.topic ? DEFAULT_TOPIC : session.topic}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <div
 | 
				
			||||||
 | 
					          className={`
 | 
				
			||||||
 | 
					            text-text-chat-header-subtitle text-sm 
 | 
				
			||||||
 | 
					            max-md:text-sm-mobile-tab max-md:leading-4
 | 
				
			||||||
 | 
					          `}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          {isMobileScreen ? (
 | 
				
			||||||
 | 
					            <ModelSelect />
 | 
				
			||||||
 | 
					          ) : (
 | 
				
			||||||
 | 
					            Locale.Chat.SubTitle(session.messages.length)
 | 
				
			||||||
 | 
					          )}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div
 | 
				
			||||||
 | 
					        className=" cursor-pointer hover:bg-hovered-btn p-1.5 rounded-action-btn follow-parent-svg default-icon-color"
 | 
				
			||||||
 | 
					        onClick={() => {
 | 
				
			||||||
 | 
					          setShowExport(true);
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <ShareIcon />
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										322
									
								
								app/containers/Chat/components/ChatInputPanel.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,322 @@
 | 
				
			|||||||
 | 
					import { forwardRef, useImperativeHandle, useState } from "react";
 | 
				
			||||||
 | 
					import { useNavigate } from "react-router-dom";
 | 
				
			||||||
 | 
					import { useDebouncedCallback } from "use-debounce";
 | 
				
			||||||
 | 
					import useUploadImage from "@/app/hooks/useUploadImage";
 | 
				
			||||||
 | 
					import Locale from "@/app/locales";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import useSubmitHandler from "@/app/hooks/useSubmitHandler";
 | 
				
			||||||
 | 
					import { CHAT_PAGE_SIZE, LAST_INPUT_KEY, Path } from "@/app/constant";
 | 
				
			||||||
 | 
					import { ChatCommandPrefix, useChatCommand } from "@/app/command";
 | 
				
			||||||
 | 
					import { useChatStore } from "@/app/store/chat";
 | 
				
			||||||
 | 
					import { usePromptStore } from "@/app/store/prompt";
 | 
				
			||||||
 | 
					import { useAppConfig } from "@/app/store/config";
 | 
				
			||||||
 | 
					import usePaste from "@/app/hooks/usePaste";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { ChatActions } from "./ChatActions";
 | 
				
			||||||
 | 
					import PromptHints, { RenderPompt } from "./PromptHint";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// import CEIcon from "@/app/icons/command&enterIcon.svg";
 | 
				
			||||||
 | 
					// import EnterIcon from "@/app/icons/enterIcon.svg";
 | 
				
			||||||
 | 
					import SendIcon from "@/app/icons/sendIcon.svg";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import Btn from "@/app/components/Btn";
 | 
				
			||||||
 | 
					import Thumbnail from "@/app/components/ThumbnailImg";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface ChatInputPanelProps {
 | 
				
			||||||
 | 
					  inputRef: React.RefObject<HTMLTextAreaElement>;
 | 
				
			||||||
 | 
					  isMobileScreen: boolean;
 | 
				
			||||||
 | 
					  renderMessages: any[];
 | 
				
			||||||
 | 
					  attachImages: string[];
 | 
				
			||||||
 | 
					  userInput: string;
 | 
				
			||||||
 | 
					  hitBottom: boolean;
 | 
				
			||||||
 | 
					  inputRows: number;
 | 
				
			||||||
 | 
					  setAttachImages: (imgs: string[]) => void;
 | 
				
			||||||
 | 
					  setUserInput: (v: string) => void;
 | 
				
			||||||
 | 
					  setIsLoading: (value: boolean) => void;
 | 
				
			||||||
 | 
					  showChatSetting: (value: boolean) => void;
 | 
				
			||||||
 | 
					  _setMsgRenderIndex: (value: number) => void;
 | 
				
			||||||
 | 
					  setAutoScroll: (value: boolean) => void;
 | 
				
			||||||
 | 
					  scrollDomToBottom: () => void;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface ChatInputPanelInstance {
 | 
				
			||||||
 | 
					  setUploading: (v: boolean) => void;
 | 
				
			||||||
 | 
					  doSubmit: (userInput: string) => void;
 | 
				
			||||||
 | 
					  setMsgRenderIndex: (v: number) => void;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// only search prompts when user input is short
 | 
				
			||||||
 | 
					const SEARCH_TEXT_LIMIT = 30;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default forwardRef<ChatInputPanelInstance, ChatInputPanelProps>(
 | 
				
			||||||
 | 
					  function ChatInputPanel(props, ref) {
 | 
				
			||||||
 | 
					    const {
 | 
				
			||||||
 | 
					      attachImages,
 | 
				
			||||||
 | 
					      inputRef,
 | 
				
			||||||
 | 
					      setAttachImages,
 | 
				
			||||||
 | 
					      userInput,
 | 
				
			||||||
 | 
					      isMobileScreen,
 | 
				
			||||||
 | 
					      setUserInput,
 | 
				
			||||||
 | 
					      setIsLoading,
 | 
				
			||||||
 | 
					      showChatSetting,
 | 
				
			||||||
 | 
					      renderMessages,
 | 
				
			||||||
 | 
					      _setMsgRenderIndex,
 | 
				
			||||||
 | 
					      hitBottom,
 | 
				
			||||||
 | 
					      inputRows,
 | 
				
			||||||
 | 
					      setAutoScroll,
 | 
				
			||||||
 | 
					      scrollDomToBottom,
 | 
				
			||||||
 | 
					    } = props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const [uploading, setUploading] = useState(false);
 | 
				
			||||||
 | 
					    const [promptHints, setPromptHints] = useState<RenderPompt[]>([]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const chatStore = useChatStore();
 | 
				
			||||||
 | 
					    const navigate = useNavigate();
 | 
				
			||||||
 | 
					    const config = useAppConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const { uploadImage } = useUploadImage(attachImages, {
 | 
				
			||||||
 | 
					      emitImages: setAttachImages,
 | 
				
			||||||
 | 
					      setUploading,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    const { submitKey, shouldSubmit } = useSubmitHandler();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const autoFocus = !isMobileScreen; // wont auto focus on mobile screen
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // chat commands shortcuts
 | 
				
			||||||
 | 
					    const chatCommands = useChatCommand({
 | 
				
			||||||
 | 
					      new: () => chatStore.newSession(),
 | 
				
			||||||
 | 
					      newm: () => navigate(Path.NewChat),
 | 
				
			||||||
 | 
					      prev: () => chatStore.nextSession(-1),
 | 
				
			||||||
 | 
					      next: () => chatStore.nextSession(1),
 | 
				
			||||||
 | 
					      clear: () =>
 | 
				
			||||||
 | 
					        chatStore.updateCurrentSession(
 | 
				
			||||||
 | 
					          (session) => (session.clearContextIndex = session.messages.length),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      del: () => chatStore.deleteSession(chatStore.currentSessionIndex),
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // prompt hints
 | 
				
			||||||
 | 
					    const promptStore = usePromptStore();
 | 
				
			||||||
 | 
					    const onSearch = useDebouncedCallback(
 | 
				
			||||||
 | 
					      (text: string) => {
 | 
				
			||||||
 | 
					        const matchedPrompts = promptStore.search(text);
 | 
				
			||||||
 | 
					        setPromptHints(matchedPrompts);
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      100,
 | 
				
			||||||
 | 
					      { leading: true, trailing: true },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // check if should send message
 | 
				
			||||||
 | 
					    const onInputKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
 | 
				
			||||||
 | 
					      // if ArrowUp and no userInput, fill with last input
 | 
				
			||||||
 | 
					      if (
 | 
				
			||||||
 | 
					        e.key === "ArrowUp" &&
 | 
				
			||||||
 | 
					        userInput.length <= 0 &&
 | 
				
			||||||
 | 
					        !(e.metaKey || e.altKey || e.ctrlKey)
 | 
				
			||||||
 | 
					      ) {
 | 
				
			||||||
 | 
					        setUserInput(localStorage.getItem(LAST_INPUT_KEY) ?? "");
 | 
				
			||||||
 | 
					        e.preventDefault();
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (shouldSubmit(e) && promptHints.length === 0) {
 | 
				
			||||||
 | 
					        doSubmit(userInput);
 | 
				
			||||||
 | 
					        e.preventDefault();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const onPromptSelect = (prompt: RenderPompt) => {
 | 
				
			||||||
 | 
					      setTimeout(() => {
 | 
				
			||||||
 | 
					        setPromptHints([]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const matchedChatCommand = chatCommands.match(prompt.content);
 | 
				
			||||||
 | 
					        if (matchedChatCommand.matched) {
 | 
				
			||||||
 | 
					          // if user is selecting a chat command, just trigger it
 | 
				
			||||||
 | 
					          matchedChatCommand.invoke();
 | 
				
			||||||
 | 
					          setUserInput("");
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          // or fill the prompt
 | 
				
			||||||
 | 
					          setUserInput(prompt.content);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        inputRef.current?.focus();
 | 
				
			||||||
 | 
					      }, 30);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const doSubmit = (userInput: string) => {
 | 
				
			||||||
 | 
					      if (userInput.trim() === "") return;
 | 
				
			||||||
 | 
					      const matchCommand = chatCommands.match(userInput);
 | 
				
			||||||
 | 
					      if (matchCommand.matched) {
 | 
				
			||||||
 | 
					        setUserInput("");
 | 
				
			||||||
 | 
					        setPromptHints([]);
 | 
				
			||||||
 | 
					        matchCommand.invoke();
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      setIsLoading(true);
 | 
				
			||||||
 | 
					      chatStore
 | 
				
			||||||
 | 
					        .onUserInput(userInput, attachImages)
 | 
				
			||||||
 | 
					        .then(() => setIsLoading(false));
 | 
				
			||||||
 | 
					      setAttachImages([]);
 | 
				
			||||||
 | 
					      localStorage.setItem(LAST_INPUT_KEY, userInput);
 | 
				
			||||||
 | 
					      setUserInput("");
 | 
				
			||||||
 | 
					      setPromptHints([]);
 | 
				
			||||||
 | 
					      if (!isMobileScreen) inputRef.current?.focus();
 | 
				
			||||||
 | 
					      setAutoScroll(true);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    useImperativeHandle(ref, () => ({
 | 
				
			||||||
 | 
					      setUploading,
 | 
				
			||||||
 | 
					      doSubmit,
 | 
				
			||||||
 | 
					      setMsgRenderIndex,
 | 
				
			||||||
 | 
					    }));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    function scrollToBottom() {
 | 
				
			||||||
 | 
					      setMsgRenderIndex(renderMessages.length - CHAT_PAGE_SIZE);
 | 
				
			||||||
 | 
					      scrollDomToBottom();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const onInput = (text: string) => {
 | 
				
			||||||
 | 
					      setUserInput(text);
 | 
				
			||||||
 | 
					      const n = text.trim().length;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // clear search results
 | 
				
			||||||
 | 
					      if (n === 0) {
 | 
				
			||||||
 | 
					        setPromptHints([]);
 | 
				
			||||||
 | 
					      } else if (text.startsWith(ChatCommandPrefix)) {
 | 
				
			||||||
 | 
					        setPromptHints(chatCommands.search(text));
 | 
				
			||||||
 | 
					      } else if (!config.disablePromptHint && n < SEARCH_TEXT_LIMIT) {
 | 
				
			||||||
 | 
					        // check if need to trigger auto completion
 | 
				
			||||||
 | 
					        if (text.startsWith("/")) {
 | 
				
			||||||
 | 
					          let searchText = text.slice(1);
 | 
				
			||||||
 | 
					          onSearch(searchText);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    function setMsgRenderIndex(newIndex: number) {
 | 
				
			||||||
 | 
					      newIndex = Math.min(renderMessages.length - CHAT_PAGE_SIZE, newIndex);
 | 
				
			||||||
 | 
					      newIndex = Math.max(0, newIndex);
 | 
				
			||||||
 | 
					      _setMsgRenderIndex(newIndex);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const { handlePaste } = usePaste(attachImages, {
 | 
				
			||||||
 | 
					      emitImages: setAttachImages,
 | 
				
			||||||
 | 
					      setUploading,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <div
 | 
				
			||||||
 | 
					        className={`
 | 
				
			||||||
 | 
					        relative w-[100%] box-border 
 | 
				
			||||||
 | 
					        max-md:rounded-tl-md max-md:rounded-tr-md
 | 
				
			||||||
 | 
					        md:border-t md:border-chat-input-top
 | 
				
			||||||
 | 
					      `}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <PromptHints
 | 
				
			||||||
 | 
					          prompts={promptHints}
 | 
				
			||||||
 | 
					          onPromptSelect={onPromptSelect}
 | 
				
			||||||
 | 
					          className=" border-chat-input-top"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div
 | 
				
			||||||
 | 
					          className={`
 | 
				
			||||||
 | 
					            flex
 | 
				
			||||||
 | 
					            max-md:flex-row-reverse max-md:items-center max-md:gap-2 max-md:p-3
 | 
				
			||||||
 | 
					            md:flex-col md:px-5 md:pb-5
 | 
				
			||||||
 | 
					          `}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <ChatActions
 | 
				
			||||||
 | 
					            uploadImage={uploadImage}
 | 
				
			||||||
 | 
					            setAttachImages={setAttachImages}
 | 
				
			||||||
 | 
					            setUploading={setUploading}
 | 
				
			||||||
 | 
					            showChatSetting={() => showChatSetting(true)}
 | 
				
			||||||
 | 
					            scrollToBottom={scrollToBottom}
 | 
				
			||||||
 | 
					            hitBottom={hitBottom}
 | 
				
			||||||
 | 
					            uploading={uploading}
 | 
				
			||||||
 | 
					            showPromptHints={() => {
 | 
				
			||||||
 | 
					              // Click again to close
 | 
				
			||||||
 | 
					              if (promptHints.length > 0) {
 | 
				
			||||||
 | 
					                setPromptHints([]);
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              inputRef.current?.focus();
 | 
				
			||||||
 | 
					              setUserInput("/");
 | 
				
			||||||
 | 
					              onSearch("");
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					            className={`
 | 
				
			||||||
 | 
					              md:py-2.5
 | 
				
			||||||
 | 
					            `}
 | 
				
			||||||
 | 
					            isMobileScreen={isMobileScreen}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					          <label
 | 
				
			||||||
 | 
					            className={`
 | 
				
			||||||
 | 
					              cursor-text flex flex-col bg-chat-panel-input-hood border border-chat-input-hood 
 | 
				
			||||||
 | 
					              focus-within:border-chat-input-hood-focus sm:focus-within:shadow-chat-input-hood-focus-shadow 
 | 
				
			||||||
 | 
					              rounded-chat-input p-3 gap-3 max-md:flex-1
 | 
				
			||||||
 | 
					              md:rounded-md md:p-4 md:gap-4
 | 
				
			||||||
 | 
					            `}
 | 
				
			||||||
 | 
					            htmlFor="chat-input"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            {attachImages.length != 0 && (
 | 
				
			||||||
 | 
					              <div className={`flex gap-2`}>
 | 
				
			||||||
 | 
					                {attachImages.map((image, index) => {
 | 
				
			||||||
 | 
					                  return (
 | 
				
			||||||
 | 
					                    <Thumbnail
 | 
				
			||||||
 | 
					                      key={index}
 | 
				
			||||||
 | 
					                      deleteImage={() => {
 | 
				
			||||||
 | 
					                        setAttachImages(
 | 
				
			||||||
 | 
					                          attachImages.filter((_, i) => i !== index),
 | 
				
			||||||
 | 
					                        );
 | 
				
			||||||
 | 
					                      }}
 | 
				
			||||||
 | 
					                      image={image}
 | 
				
			||||||
 | 
					                    />
 | 
				
			||||||
 | 
					                  );
 | 
				
			||||||
 | 
					                })}
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					            <textarea
 | 
				
			||||||
 | 
					              id="chat-input"
 | 
				
			||||||
 | 
					              ref={inputRef}
 | 
				
			||||||
 | 
					              className={`
 | 
				
			||||||
 | 
					                leading-[19px] flex-1 focus:outline-none focus:shadow-none focus:border-none resize-none bg-inherit text-text-input
 | 
				
			||||||
 | 
					                max-md:h-chat-input-mobile
 | 
				
			||||||
 | 
					                md:min-h-chat-input
 | 
				
			||||||
 | 
					              `}
 | 
				
			||||||
 | 
					              placeholder={
 | 
				
			||||||
 | 
					                isMobileScreen
 | 
				
			||||||
 | 
					                  ? Locale.Chat.Input(submitKey, isMobileScreen)
 | 
				
			||||||
 | 
					                  : undefined
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					              onInput={(e) => onInput(e.currentTarget.value)}
 | 
				
			||||||
 | 
					              value={userInput}
 | 
				
			||||||
 | 
					              onKeyDown={onInputKeyDown}
 | 
				
			||||||
 | 
					              onFocus={scrollToBottom}
 | 
				
			||||||
 | 
					              onClick={scrollToBottom}
 | 
				
			||||||
 | 
					              onPaste={handlePaste}
 | 
				
			||||||
 | 
					              rows={inputRows}
 | 
				
			||||||
 | 
					              autoFocus={autoFocus}
 | 
				
			||||||
 | 
					              style={{
 | 
				
			||||||
 | 
					                fontSize: config.fontSize,
 | 
				
			||||||
 | 
					              }}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					            {!isMobileScreen && (
 | 
				
			||||||
 | 
					              <div className="flex items-center justify-center text-sm gap-3">
 | 
				
			||||||
 | 
					                <div className="flex-1"> </div>
 | 
				
			||||||
 | 
					                <div className="text-text-chat-input-placeholder font-common line-clamp-1">
 | 
				
			||||||
 | 
					                  {Locale.Chat.Input(submitKey)}
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                <Btn
 | 
				
			||||||
 | 
					                  className="min-w-[77px]"
 | 
				
			||||||
 | 
					                  icon={<SendIcon />}
 | 
				
			||||||
 | 
					                  text={Locale.Chat.Send}
 | 
				
			||||||
 | 
					                  disabled={!userInput.length}
 | 
				
			||||||
 | 
					                  type="primary"
 | 
				
			||||||
 | 
					                  onClick={() => doSubmit(userInput)}
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					          </label>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
							
								
								
									
										246
									
								
								app/containers/Chat/components/ChatMessagePanel.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,246 @@
 | 
				
			|||||||
 | 
					import { Fragment, useMemo } from "react";
 | 
				
			||||||
 | 
					import { ChatMessage, useChatStore } from "@/app/store/chat";
 | 
				
			||||||
 | 
					import { CHAT_PAGE_SIZE } from "@/app/constant";
 | 
				
			||||||
 | 
					import Locale from "@/app/locales";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { getMessageTextContent, selectOrCopy } from "@/app/utils";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import LoadingIcon from "@/app/icons/three-dots.svg";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { Avatar } from "@/app/components/emoji";
 | 
				
			||||||
 | 
					import { MaskAvatar } from "@/app/components/mask";
 | 
				
			||||||
 | 
					import { useAppConfig } from "@/app/store/config";
 | 
				
			||||||
 | 
					import ClearContextDivider from "./ClearContextDivider";
 | 
				
			||||||
 | 
					import dynamic from "next/dynamic";
 | 
				
			||||||
 | 
					import useRelativePosition, {
 | 
				
			||||||
 | 
					  Orientation,
 | 
				
			||||||
 | 
					} from "@/app/hooks/useRelativePosition";
 | 
				
			||||||
 | 
					import MessageActions, { RenderMessage } from "./MessageActions";
 | 
				
			||||||
 | 
					import Imgs from "@/app/components/Imgs";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type { RenderMessage };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface ChatMessagePanelProps {
 | 
				
			||||||
 | 
					  scrollRef: React.RefObject<HTMLDivElement>;
 | 
				
			||||||
 | 
					  inputRef: React.RefObject<HTMLTextAreaElement>;
 | 
				
			||||||
 | 
					  isMobileScreen: boolean;
 | 
				
			||||||
 | 
					  msgRenderIndex: number;
 | 
				
			||||||
 | 
					  userInput: string;
 | 
				
			||||||
 | 
					  context: any[];
 | 
				
			||||||
 | 
					  renderMessages: RenderMessage[];
 | 
				
			||||||
 | 
					  scrollDomToBottom: () => void;
 | 
				
			||||||
 | 
					  setAutoScroll?: (value: boolean) => void;
 | 
				
			||||||
 | 
					  setMsgRenderIndex?: (newIndex: number) => void;
 | 
				
			||||||
 | 
					  setHitBottom?: (value: boolean) => void;
 | 
				
			||||||
 | 
					  setUserInput?: (v: string) => void;
 | 
				
			||||||
 | 
					  setIsLoading?: (value: boolean) => void;
 | 
				
			||||||
 | 
					  setShowPromptModal?: (value: boolean) => void;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let MarkdownLoadedCallback: () => void;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Markdown = dynamic(
 | 
				
			||||||
 | 
					  async () => {
 | 
				
			||||||
 | 
					    const bundle = await import("@/app/components/markdown");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (MarkdownLoadedCallback) {
 | 
				
			||||||
 | 
					      MarkdownLoadedCallback();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return bundle.Markdown;
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    loading: () => <LoadingIcon />,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function ChatMessagePanel(props: ChatMessagePanelProps) {
 | 
				
			||||||
 | 
					  const {
 | 
				
			||||||
 | 
					    scrollRef,
 | 
				
			||||||
 | 
					    inputRef,
 | 
				
			||||||
 | 
					    setAutoScroll,
 | 
				
			||||||
 | 
					    setMsgRenderIndex,
 | 
				
			||||||
 | 
					    isMobileScreen,
 | 
				
			||||||
 | 
					    msgRenderIndex,
 | 
				
			||||||
 | 
					    setHitBottom,
 | 
				
			||||||
 | 
					    setUserInput,
 | 
				
			||||||
 | 
					    userInput,
 | 
				
			||||||
 | 
					    context,
 | 
				
			||||||
 | 
					    renderMessages,
 | 
				
			||||||
 | 
					    setIsLoading,
 | 
				
			||||||
 | 
					    setShowPromptModal,
 | 
				
			||||||
 | 
					    scrollDomToBottom,
 | 
				
			||||||
 | 
					  } = props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const chatStore = useChatStore();
 | 
				
			||||||
 | 
					  const session = chatStore.currentSession();
 | 
				
			||||||
 | 
					  const config = useAppConfig();
 | 
				
			||||||
 | 
					  const fontSize = config.fontSize;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const { position, getRelativePosition } = useRelativePosition({
 | 
				
			||||||
 | 
					    containerRef: scrollRef,
 | 
				
			||||||
 | 
					    delay: 0,
 | 
				
			||||||
 | 
					    offsetDistance: 20,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // clear context index = context length + index in messages
 | 
				
			||||||
 | 
					  const clearContextIndex =
 | 
				
			||||||
 | 
					    (session.clearContextIndex ?? -1) >= 0
 | 
				
			||||||
 | 
					      ? session.clearContextIndex! + context.length - msgRenderIndex
 | 
				
			||||||
 | 
					      : -1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!MarkdownLoadedCallback) {
 | 
				
			||||||
 | 
					    MarkdownLoadedCallback = () => {
 | 
				
			||||||
 | 
					      window.setTimeout(scrollDomToBottom, 100);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const messages = useMemo(() => {
 | 
				
			||||||
 | 
					    const endRenderIndex = Math.min(
 | 
				
			||||||
 | 
					      msgRenderIndex + 3 * CHAT_PAGE_SIZE,
 | 
				
			||||||
 | 
					      renderMessages.length,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    return renderMessages.slice(msgRenderIndex, endRenderIndex);
 | 
				
			||||||
 | 
					  }, [msgRenderIndex, renderMessages]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const onChatBodyScroll = (e: HTMLElement) => {
 | 
				
			||||||
 | 
					    const bottomHeight = e.scrollTop + e.clientHeight;
 | 
				
			||||||
 | 
					    const edgeThreshold = e.clientHeight;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const isTouchTopEdge = e.scrollTop <= edgeThreshold;
 | 
				
			||||||
 | 
					    const isTouchBottomEdge = bottomHeight >= e.scrollHeight - edgeThreshold;
 | 
				
			||||||
 | 
					    const isHitBottom =
 | 
				
			||||||
 | 
					      bottomHeight >= e.scrollHeight - (isMobileScreen ? 4 : 10);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const prevPageMsgIndex = msgRenderIndex - CHAT_PAGE_SIZE;
 | 
				
			||||||
 | 
					    const nextPageMsgIndex = msgRenderIndex + CHAT_PAGE_SIZE;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (isTouchTopEdge && !isTouchBottomEdge) {
 | 
				
			||||||
 | 
					      setMsgRenderIndex?.(prevPageMsgIndex);
 | 
				
			||||||
 | 
					    } else if (isTouchBottomEdge) {
 | 
				
			||||||
 | 
					      setMsgRenderIndex?.(nextPageMsgIndex);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    setHitBottom?.(isHitBottom);
 | 
				
			||||||
 | 
					    setAutoScroll?.(isHitBottom);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const onRightClick = (e: any, message: ChatMessage) => {
 | 
				
			||||||
 | 
					    // copy to clipboard
 | 
				
			||||||
 | 
					    if (selectOrCopy(e.currentTarget, getMessageTextContent(message))) {
 | 
				
			||||||
 | 
					      if (userInput.length === 0) {
 | 
				
			||||||
 | 
					        setUserInput?.(getMessageTextContent(message));
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      e.preventDefault();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div
 | 
				
			||||||
 | 
					      className={`pt-[80px] relative flex-1 overscroll-y-none overflow-y-auto overflow-x-hidden px-3 pb-6 md:bg-chat-panel-message bg-chat-panel-message-mobile`}
 | 
				
			||||||
 | 
					      ref={scrollRef}
 | 
				
			||||||
 | 
					      onScroll={(e) => onChatBodyScroll(e.currentTarget)}
 | 
				
			||||||
 | 
					      onMouseDown={() => inputRef.current?.blur()}
 | 
				
			||||||
 | 
					      onTouchStart={() => {
 | 
				
			||||||
 | 
					        inputRef.current?.blur();
 | 
				
			||||||
 | 
					        setAutoScroll?.(false);
 | 
				
			||||||
 | 
					      }}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      {messages.map((message, i) => {
 | 
				
			||||||
 | 
					        const isUser = message.role === "user";
 | 
				
			||||||
 | 
					        const isContext = i < context.length;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const shouldShowClearContextDivider = i === clearContextIndex - 1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const actionsBarPosition =
 | 
				
			||||||
 | 
					          position?.id === message.id &&
 | 
				
			||||||
 | 
					          position?.poi.overlapPositions[Orientation.bottom]
 | 
				
			||||||
 | 
					            ? "bottom-[calc(100%-0.25rem)]"
 | 
				
			||||||
 | 
					            : "top-[calc(100%-0.25rem)]";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return (
 | 
				
			||||||
 | 
					          <Fragment key={message.id}>
 | 
				
			||||||
 | 
					            <div
 | 
				
			||||||
 | 
					              className={`flex mt-6 gap-2 ${isUser ? "flex-row-reverse" : ""}`}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              <div className={`relative flex-0`}>
 | 
				
			||||||
 | 
					                {isUser ? (
 | 
				
			||||||
 | 
					                  <Avatar avatar={config.avatar} />
 | 
				
			||||||
 | 
					                ) : (
 | 
				
			||||||
 | 
					                  <>
 | 
				
			||||||
 | 
					                    {["system"].includes(message.role) ? (
 | 
				
			||||||
 | 
					                      <Avatar avatar="2699-fe0f" />
 | 
				
			||||||
 | 
					                    ) : (
 | 
				
			||||||
 | 
					                      <MaskAvatar
 | 
				
			||||||
 | 
					                        avatar={session.mask.avatar}
 | 
				
			||||||
 | 
					                        model={message.model || session.mask.modelConfig.model}
 | 
				
			||||||
 | 
					                      />
 | 
				
			||||||
 | 
					                    )}
 | 
				
			||||||
 | 
					                  </>
 | 
				
			||||||
 | 
					                )}
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					              <div
 | 
				
			||||||
 | 
					                className={`group relative flex ${
 | 
				
			||||||
 | 
					                  isUser ? "flex-row-reverse" : ""
 | 
				
			||||||
 | 
					                }`}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <div
 | 
				
			||||||
 | 
					                  className={` pointer-events-none  text-text-chat-message-date text-right font-common 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 ${
 | 
				
			||||||
 | 
					                    isUser
 | 
				
			||||||
 | 
					                      ? "rounded-user-message bg-chat-panel-message-user"
 | 
				
			||||||
 | 
					                      : "rounded-bot-message bg-chat-panel-message-bot"
 | 
				
			||||||
 | 
					                  } box-border peer py-2 px-3`}
 | 
				
			||||||
 | 
					                  onPointerMoveCapture={(e) =>
 | 
				
			||||||
 | 
					                    getRelativePosition(e.currentTarget, message.id)
 | 
				
			||||||
 | 
					                  }
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  <Markdown
 | 
				
			||||||
 | 
					                    content={getMessageTextContent(message)}
 | 
				
			||||||
 | 
					                    loading={
 | 
				
			||||||
 | 
					                      (message.preview || message.streaming) &&
 | 
				
			||||||
 | 
					                      message.content.length === 0 &&
 | 
				
			||||||
 | 
					                      !isUser
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    onContextMenu={(e) => onRightClick(e, message)}
 | 
				
			||||||
 | 
					                    onDoubleClickCapture={() => {
 | 
				
			||||||
 | 
					                      if (!isMobileScreen) return;
 | 
				
			||||||
 | 
					                      setUserInput?.(getMessageTextContent(message));
 | 
				
			||||||
 | 
					                    }}
 | 
				
			||||||
 | 
					                    fontSize={fontSize}
 | 
				
			||||||
 | 
					                    parentRef={scrollRef}
 | 
				
			||||||
 | 
					                    defaultShow={i >= messages.length - 6}
 | 
				
			||||||
 | 
					                    className={`leading-6 max-w-message-width ${
 | 
				
			||||||
 | 
					                      isUser
 | 
				
			||||||
 | 
					                        ? " text-text-chat-message-markdown-user"
 | 
				
			||||||
 | 
					                        : "text-text-chat-message-markdown-bot"
 | 
				
			||||||
 | 
					                    }`}
 | 
				
			||||||
 | 
					                  />
 | 
				
			||||||
 | 
					                  <Imgs message={message} />
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                <MessageActions
 | 
				
			||||||
 | 
					                  className={actionsBarPosition}
 | 
				
			||||||
 | 
					                  message={message}
 | 
				
			||||||
 | 
					                  inputRef={inputRef}
 | 
				
			||||||
 | 
					                  isUser={isUser}
 | 
				
			||||||
 | 
					                  isContext={isContext}
 | 
				
			||||||
 | 
					                  setIsLoading={setIsLoading}
 | 
				
			||||||
 | 
					                  setShowPromptModal={setShowPromptModal}
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            {shouldShowClearContextDivider && <ClearContextDivider />}
 | 
				
			||||||
 | 
					          </Fragment>
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      })}
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										46
									
								
								app/containers/Chat/components/ClearContextDivider.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,46 @@
 | 
				
			|||||||
 | 
					import { useChatStore } from "@/app/store/chat";
 | 
				
			||||||
 | 
					import Locale from "@/app/locales";
 | 
				
			||||||
 | 
					import { useAppConfig } from "@/app/store";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function ClearContextDivider() {
 | 
				
			||||||
 | 
					  const chatStore = useChatStore();
 | 
				
			||||||
 | 
					  const { isMobileScreen } = useAppConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div
 | 
				
			||||||
 | 
					      className={`mt-6 mb-8 flex items-center justify-center gap-2.5 max-md:cursor-pointer`}
 | 
				
			||||||
 | 
					      onClick={() => {
 | 
				
			||||||
 | 
					        if (!isMobileScreen) {
 | 
				
			||||||
 | 
					          return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        chatStore.updateCurrentSession(
 | 
				
			||||||
 | 
					          (session) => (session.clearContextIndex = undefined),
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <div className="bg-chat-panel-message-clear-divider h-[1px] w-10"> </div>
 | 
				
			||||||
 | 
					      <div className="flex items-center justify-between gap-1 text-sm">
 | 
				
			||||||
 | 
					        <div className={`text-text-chat-panel-message-clear`}>
 | 
				
			||||||
 | 
					          {Locale.Context.Clear}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <div
 | 
				
			||||||
 | 
					          className={`
 | 
				
			||||||
 | 
					          text-text-chat-panel-message-clear-revert  underline font-common 
 | 
				
			||||||
 | 
					          md:cursor-pointer
 | 
				
			||||||
 | 
					          `}
 | 
				
			||||||
 | 
					          onClick={() => {
 | 
				
			||||||
 | 
					            if (isMobileScreen) {
 | 
				
			||||||
 | 
					              return;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            chatStore.updateCurrentSession(
 | 
				
			||||||
 | 
					              (session) => (session.clearContextIndex = undefined),
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          {Locale.Context.Revert}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <div className="bg-chat-panel-message-clear-divider h-[1px] w-10"> </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										75
									
								
								app/containers/Chat/components/EditMessageModal.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,75 @@
 | 
				
			|||||||
 | 
					import { useState } from "react";
 | 
				
			||||||
 | 
					import { useChatStore } from "@/app/store/chat";
 | 
				
			||||||
 | 
					import { List, ListItem, Modal } from "@/app/components/ui-lib";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import Locale from "@/app/locales";
 | 
				
			||||||
 | 
					import { IconButton } from "@/app/components/button";
 | 
				
			||||||
 | 
					import { ContextPrompts } from "@/app/components/mask";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import CancelIcon from "@/app/icons/cancel.svg";
 | 
				
			||||||
 | 
					import ConfirmIcon from "@/app/icons/confirm.svg";
 | 
				
			||||||
 | 
					import Input from "@/app/components/Input";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function EditMessageModal(props: { onClose: () => void }) {
 | 
				
			||||||
 | 
					  const chatStore = useChatStore();
 | 
				
			||||||
 | 
					  const session = chatStore.currentSession();
 | 
				
			||||||
 | 
					  const [messages, setMessages] = useState(session.messages.slice());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className="modal-mask">
 | 
				
			||||||
 | 
					      <Modal
 | 
				
			||||||
 | 
					        title={Locale.Chat.EditMessage.Title}
 | 
				
			||||||
 | 
					        onClose={props.onClose}
 | 
				
			||||||
 | 
					        actions={[
 | 
				
			||||||
 | 
					          <IconButton
 | 
				
			||||||
 | 
					            text={Locale.UI.Cancel}
 | 
				
			||||||
 | 
					            icon={<CancelIcon />}
 | 
				
			||||||
 | 
					            key="cancel"
 | 
				
			||||||
 | 
					            onClick={() => {
 | 
				
			||||||
 | 
					              props.onClose();
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					          />,
 | 
				
			||||||
 | 
					          <IconButton
 | 
				
			||||||
 | 
					            type="primary"
 | 
				
			||||||
 | 
					            text={Locale.UI.Confirm}
 | 
				
			||||||
 | 
					            icon={<ConfirmIcon />}
 | 
				
			||||||
 | 
					            key="ok"
 | 
				
			||||||
 | 
					            onClick={() => {
 | 
				
			||||||
 | 
					              chatStore.updateCurrentSession(
 | 
				
			||||||
 | 
					                (session) => (session.messages = messages),
 | 
				
			||||||
 | 
					              );
 | 
				
			||||||
 | 
					              props.onClose();
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					          />,
 | 
				
			||||||
 | 
					        ]}
 | 
				
			||||||
 | 
					        // className="!bg-modal-mask"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <List>
 | 
				
			||||||
 | 
					          <ListItem
 | 
				
			||||||
 | 
					            title={Locale.Chat.EditMessage.Topic.Title}
 | 
				
			||||||
 | 
					            subTitle={Locale.Chat.EditMessage.Topic.SubTitle}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <Input
 | 
				
			||||||
 | 
					              type="text"
 | 
				
			||||||
 | 
					              value={session.topic}
 | 
				
			||||||
 | 
					              onChange={(e) =>
 | 
				
			||||||
 | 
					                chatStore.updateCurrentSession(
 | 
				
			||||||
 | 
					                  (session) => (session.topic = e || ""),
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					              className=" text-center"
 | 
				
			||||||
 | 
					            ></Input>
 | 
				
			||||||
 | 
					          </ListItem>
 | 
				
			||||||
 | 
					        </List>
 | 
				
			||||||
 | 
					        <ContextPrompts
 | 
				
			||||||
 | 
					          context={messages}
 | 
				
			||||||
 | 
					          updateContext={(updater) => {
 | 
				
			||||||
 | 
					            const newMessages = messages.slice();
 | 
				
			||||||
 | 
					            updater(newMessages);
 | 
				
			||||||
 | 
					            setMessages(newMessages);
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </Modal>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										295
									
								
								app/containers/Chat/components/MessageActions.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,295 @@
 | 
				
			|||||||
 | 
					import Locale from "@/app/locales";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import StopIcon from "@/app/icons/pause.svg";
 | 
				
			||||||
 | 
					import DeleteRequestIcon from "@/app/icons/deleteRequestIcon.svg";
 | 
				
			||||||
 | 
					import RetryRequestIcon from "@/app/icons/retryRequestIcon.svg";
 | 
				
			||||||
 | 
					import CopyRequestIcon from "@/app/icons/copyRequestIcon.svg";
 | 
				
			||||||
 | 
					import EditRequestIcon from "@/app/icons/editRequestIcon.svg";
 | 
				
			||||||
 | 
					import PinRequestIcon from "@/app/icons/pinRequestIcon.svg";
 | 
				
			||||||
 | 
					import { showPrompt, showToast } from "@/app/components/ui-lib";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  copyToClipboard,
 | 
				
			||||||
 | 
					  getMessageImages,
 | 
				
			||||||
 | 
					  getMessageTextContent,
 | 
				
			||||||
 | 
					} from "@/app/utils";
 | 
				
			||||||
 | 
					import { MultimodalContent } from "@/app/client/api";
 | 
				
			||||||
 | 
					import { ChatMessage, useChatStore } from "@/app/store/chat";
 | 
				
			||||||
 | 
					import ActionsBar from "@/app/components/ActionsBar";
 | 
				
			||||||
 | 
					import { ChatControllerPool } from "@/app/client/controller";
 | 
				
			||||||
 | 
					import { RefObject } from "react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type RenderMessage = ChatMessage & { preview?: boolean };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface MessageActionsProps {
 | 
				
			||||||
 | 
					  message: RenderMessage;
 | 
				
			||||||
 | 
					  isUser: boolean;
 | 
				
			||||||
 | 
					  isContext: boolean;
 | 
				
			||||||
 | 
					  showActions?: boolean;
 | 
				
			||||||
 | 
					  inputRef: RefObject<HTMLTextAreaElement>;
 | 
				
			||||||
 | 
					  className?: string;
 | 
				
			||||||
 | 
					  setIsLoading?: (value: boolean) => void;
 | 
				
			||||||
 | 
					  setShowPromptModal?: (value: boolean) => void;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const genActionsShema = (
 | 
				
			||||||
 | 
					  message: RenderMessage,
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    onEdit,
 | 
				
			||||||
 | 
					    onCopy,
 | 
				
			||||||
 | 
					    onPinMessage,
 | 
				
			||||||
 | 
					    onDelete,
 | 
				
			||||||
 | 
					    onResend,
 | 
				
			||||||
 | 
					    onUserStop,
 | 
				
			||||||
 | 
					  }: Record<
 | 
				
			||||||
 | 
					    | "onEdit"
 | 
				
			||||||
 | 
					    | "onCopy"
 | 
				
			||||||
 | 
					    | "onPinMessage"
 | 
				
			||||||
 | 
					    | "onDelete"
 | 
				
			||||||
 | 
					    | "onResend"
 | 
				
			||||||
 | 
					    | "onUserStop",
 | 
				
			||||||
 | 
					    (message: RenderMessage) => void
 | 
				
			||||||
 | 
					  >,
 | 
				
			||||||
 | 
					) => {
 | 
				
			||||||
 | 
					  const className =
 | 
				
			||||||
 | 
					    " !p-1 hover:bg-chat-message-actions-btn-hovered !rounded-actions-bar-btn ";
 | 
				
			||||||
 | 
					  return [
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      id: "Edit",
 | 
				
			||||||
 | 
					      icons: <EditRequestIcon />,
 | 
				
			||||||
 | 
					      title: "Edit",
 | 
				
			||||||
 | 
					      className,
 | 
				
			||||||
 | 
					      onClick: () => onEdit(message),
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      id: Locale.Chat.Actions.Copy,
 | 
				
			||||||
 | 
					      icons: <CopyRequestIcon />,
 | 
				
			||||||
 | 
					      title: Locale.Chat.Actions.Copy,
 | 
				
			||||||
 | 
					      className,
 | 
				
			||||||
 | 
					      onClick: () => onCopy(message),
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      id: Locale.Chat.Actions.Pin,
 | 
				
			||||||
 | 
					      icons: <PinRequestIcon />,
 | 
				
			||||||
 | 
					      title: Locale.Chat.Actions.Pin,
 | 
				
			||||||
 | 
					      className,
 | 
				
			||||||
 | 
					      onClick: () => onPinMessage(message),
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      id: Locale.Chat.Actions.Delete,
 | 
				
			||||||
 | 
					      icons: <DeleteRequestIcon />,
 | 
				
			||||||
 | 
					      title: Locale.Chat.Actions.Delete,
 | 
				
			||||||
 | 
					      className,
 | 
				
			||||||
 | 
					      onClick: () => onDelete(message),
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      id: Locale.Chat.Actions.Retry,
 | 
				
			||||||
 | 
					      icons: <RetryRequestIcon />,
 | 
				
			||||||
 | 
					      title: Locale.Chat.Actions.Retry,
 | 
				
			||||||
 | 
					      className,
 | 
				
			||||||
 | 
					      onClick: () => onResend(message),
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      id: Locale.Chat.Actions.Stop,
 | 
				
			||||||
 | 
					      icons: <StopIcon />,
 | 
				
			||||||
 | 
					      title: Locale.Chat.Actions.Stop,
 | 
				
			||||||
 | 
					      className,
 | 
				
			||||||
 | 
					      onClick: () => onUserStop(message),
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  ];
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					enum GroupType {
 | 
				
			||||||
 | 
					  "streaming" = "streaming",
 | 
				
			||||||
 | 
					  "isContext" = "isContext",
 | 
				
			||||||
 | 
					  "normal" = "normal",
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const groupsTypes = {
 | 
				
			||||||
 | 
					  [GroupType.streaming]: [[Locale.Chat.Actions.Stop]],
 | 
				
			||||||
 | 
					  [GroupType.isContext]: [["Edit"]],
 | 
				
			||||||
 | 
					  [GroupType.normal]: [
 | 
				
			||||||
 | 
					    [
 | 
				
			||||||
 | 
					      Locale.Chat.Actions.Retry,
 | 
				
			||||||
 | 
					      "Edit",
 | 
				
			||||||
 | 
					      Locale.Chat.Actions.Copy,
 | 
				
			||||||
 | 
					      Locale.Chat.Actions.Pin,
 | 
				
			||||||
 | 
					      Locale.Chat.Actions.Delete,
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function MessageActions(props: MessageActionsProps) {
 | 
				
			||||||
 | 
					  const {
 | 
				
			||||||
 | 
					    className,
 | 
				
			||||||
 | 
					    message,
 | 
				
			||||||
 | 
					    isUser,
 | 
				
			||||||
 | 
					    isContext,
 | 
				
			||||||
 | 
					    showActions = true,
 | 
				
			||||||
 | 
					    setIsLoading,
 | 
				
			||||||
 | 
					    inputRef,
 | 
				
			||||||
 | 
					    setShowPromptModal,
 | 
				
			||||||
 | 
					  } = props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const chatStore = useChatStore();
 | 
				
			||||||
 | 
					  const session = chatStore.currentSession();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const deleteMessage = (msgId?: string) => {
 | 
				
			||||||
 | 
					    chatStore.updateCurrentSession(
 | 
				
			||||||
 | 
					      (session) =>
 | 
				
			||||||
 | 
					        (session.messages = session.messages.filter((m) => m.id !== msgId)),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const onDelete = (message: ChatMessage) => {
 | 
				
			||||||
 | 
					    deleteMessage(message.id);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const onResend = (message: ChatMessage) => {
 | 
				
			||||||
 | 
					    // when it is resending a message
 | 
				
			||||||
 | 
					    // 1. for a user's message, find the next bot response
 | 
				
			||||||
 | 
					    // 2. for a bot's message, find the last user's input
 | 
				
			||||||
 | 
					    // 3. delete original user input and bot's message
 | 
				
			||||||
 | 
					    // 4. resend the user's input
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const resendingIndex = session.messages.findIndex(
 | 
				
			||||||
 | 
					      (m) => m.id === message.id,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (resendingIndex < 0 || resendingIndex >= session.messages.length) {
 | 
				
			||||||
 | 
					      console.error("[Chat] failed to find resending message", message);
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let userMessage: ChatMessage | undefined;
 | 
				
			||||||
 | 
					    let botMessage: ChatMessage | undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (message.role === "assistant") {
 | 
				
			||||||
 | 
					      // if it is resending a bot's message, find the user input for it
 | 
				
			||||||
 | 
					      botMessage = message;
 | 
				
			||||||
 | 
					      for (let i = resendingIndex; i >= 0; i -= 1) {
 | 
				
			||||||
 | 
					        if (session.messages[i].role === "user") {
 | 
				
			||||||
 | 
					          userMessage = session.messages[i];
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } else if (message.role === "user") {
 | 
				
			||||||
 | 
					      // if it is resending a user's input, find the bot's response
 | 
				
			||||||
 | 
					      userMessage = message;
 | 
				
			||||||
 | 
					      for (let i = resendingIndex; i < session.messages.length; i += 1) {
 | 
				
			||||||
 | 
					        if (session.messages[i].role === "assistant") {
 | 
				
			||||||
 | 
					          botMessage = session.messages[i];
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (userMessage === undefined) {
 | 
				
			||||||
 | 
					      console.error("[Chat] failed to resend", message);
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // delete the original messages
 | 
				
			||||||
 | 
					    deleteMessage(userMessage.id);
 | 
				
			||||||
 | 
					    deleteMessage(botMessage?.id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // resend the message
 | 
				
			||||||
 | 
					    setIsLoading?.(true);
 | 
				
			||||||
 | 
					    const textContent = getMessageTextContent(userMessage);
 | 
				
			||||||
 | 
					    const images = getMessageImages(userMessage);
 | 
				
			||||||
 | 
					    chatStore
 | 
				
			||||||
 | 
					      .onUserInput(textContent, images)
 | 
				
			||||||
 | 
					      .then(() => setIsLoading?.(false));
 | 
				
			||||||
 | 
					    inputRef.current?.focus();
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const onPinMessage = (message: ChatMessage) => {
 | 
				
			||||||
 | 
					    chatStore.updateCurrentSession((session) =>
 | 
				
			||||||
 | 
					      session.mask.context.push(message),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    showToast(Locale.Chat.Actions.PinToastContent, {
 | 
				
			||||||
 | 
					      text: Locale.Chat.Actions.PinToastAction,
 | 
				
			||||||
 | 
					      onClick: () => {
 | 
				
			||||||
 | 
					        setShowPromptModal?.(true);
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // stop response
 | 
				
			||||||
 | 
					  const onUserStop = (message: ChatMessage) => {
 | 
				
			||||||
 | 
					    ChatControllerPool.stop(session.id, message.id);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const onEdit = 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;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const onCopy = () => copyToClipboard(getMessageTextContent(message));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const groupsType = [
 | 
				
			||||||
 | 
					    message.streaming && GroupType.streaming,
 | 
				
			||||||
 | 
					    isContext && GroupType.isContext,
 | 
				
			||||||
 | 
					    GroupType.normal,
 | 
				
			||||||
 | 
					  ].find((i) => i) as GroupType;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    showActions && (
 | 
				
			||||||
 | 
					      <div
 | 
				
			||||||
 | 
					        className={`
 | 
				
			||||||
 | 
					          absolute z-10 w-[100%]
 | 
				
			||||||
 | 
					          ${isUser ? "right-0" : "left-0"} 
 | 
				
			||||||
 | 
					          transition-all duration-300 
 | 
				
			||||||
 | 
					          opacity-0
 | 
				
			||||||
 | 
					          pointer-events-none
 | 
				
			||||||
 | 
					          group-hover:opacity-100 
 | 
				
			||||||
 | 
					          group-hover:pointer-events-auto
 | 
				
			||||||
 | 
					          ${className}
 | 
				
			||||||
 | 
					        `}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <ActionsBar
 | 
				
			||||||
 | 
					          actionsShema={genActionsShema(message, {
 | 
				
			||||||
 | 
					            onCopy,
 | 
				
			||||||
 | 
					            onDelete,
 | 
				
			||||||
 | 
					            onPinMessage,
 | 
				
			||||||
 | 
					            onEdit,
 | 
				
			||||||
 | 
					            onResend,
 | 
				
			||||||
 | 
					            onUserStop,
 | 
				
			||||||
 | 
					          })}
 | 
				
			||||||
 | 
					          groups={groupsTypes[groupsType]}
 | 
				
			||||||
 | 
					          className={`
 | 
				
			||||||
 | 
					            float-right flex flex-row gap-1  p-1
 | 
				
			||||||
 | 
					            bg-chat-message-actions 
 | 
				
			||||||
 | 
					            rounded-md 
 | 
				
			||||||
 | 
					            shadow-message-actions-bar 
 | 
				
			||||||
 | 
					            dark:bg-none
 | 
				
			||||||
 | 
					          `}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										159
									
								
								app/containers/Chat/components/ModelSelect.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,159 @@
 | 
				
			|||||||
 | 
					import Popover from "@/app/components/Popover";
 | 
				
			||||||
 | 
					import React, { useMemo, useRef } from "react";
 | 
				
			||||||
 | 
					import useRelativePosition, {
 | 
				
			||||||
 | 
					  Orientation,
 | 
				
			||||||
 | 
					} from "@/app/hooks/useRelativePosition";
 | 
				
			||||||
 | 
					import Locale from "@/app/locales";
 | 
				
			||||||
 | 
					import { useChatStore } from "@/app/store/chat";
 | 
				
			||||||
 | 
					import { useAllModels } from "@/app/utils/hooks";
 | 
				
			||||||
 | 
					import { ModelType, useAppConfig } from "@/app/store/config";
 | 
				
			||||||
 | 
					import { showToast } from "@/app/components/ui-lib";
 | 
				
			||||||
 | 
					import BottomArrow from "@/app/icons/downArrowLgIcon.svg";
 | 
				
			||||||
 | 
					import BottomArrowMobile from "@/app/icons/bottomArrow.svg";
 | 
				
			||||||
 | 
					import Modal, { TriggerProps } from "@/app/components/Modal";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import Selected from "@/app/icons/selectedIcon.svg";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ModelSelect = () => {
 | 
				
			||||||
 | 
					  const config = useAppConfig();
 | 
				
			||||||
 | 
					  const { isMobileScreen } = config;
 | 
				
			||||||
 | 
					  const chatStore = useChatStore();
 | 
				
			||||||
 | 
					  const currentModel = chatStore.currentSession().mask.modelConfig.model;
 | 
				
			||||||
 | 
					  const allModels = useAllModels();
 | 
				
			||||||
 | 
					  const models = useMemo(() => {
 | 
				
			||||||
 | 
					    const filteredModels = allModels.filter((m) => m.available);
 | 
				
			||||||
 | 
					    const defaultModel = filteredModels.find((m) => m.isDefault);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (defaultModel) {
 | 
				
			||||||
 | 
					      const arr = [
 | 
				
			||||||
 | 
					        defaultModel,
 | 
				
			||||||
 | 
					        ...filteredModels.filter((m) => m !== defaultModel),
 | 
				
			||||||
 | 
					      ];
 | 
				
			||||||
 | 
					      return arr;
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      return filteredModels;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [allModels]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const rootRef = useRef<HTMLDivElement>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const { position, getRelativePosition } = useRelativePosition({
 | 
				
			||||||
 | 
					    delay: 0,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const contentRef = useMemo<{ current: HTMLDivElement | null }>(() => {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      current: null,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					  const selectedItemRef = useRef<HTMLDivElement>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const autoScrollToSelectedModal = () => {
 | 
				
			||||||
 | 
					    window.setTimeout(() => {
 | 
				
			||||||
 | 
					      const distanceToParent = selectedItemRef.current?.offsetTop || 0;
 | 
				
			||||||
 | 
					      const childHeight = selectedItemRef.current?.offsetHeight || 0;
 | 
				
			||||||
 | 
					      const parentHeight = contentRef.current?.offsetHeight || 0;
 | 
				
			||||||
 | 
					      const distanceToParentCenter =
 | 
				
			||||||
 | 
					        distanceToParent + childHeight / 2 - parentHeight / 2;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (distanceToParentCenter > 0 && contentRef.current) {
 | 
				
			||||||
 | 
					        contentRef.current.scrollTop = distanceToParentCenter;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const content: TriggerProps["content"] = ({ close }) => (
 | 
				
			||||||
 | 
					    <div
 | 
				
			||||||
 | 
					      className={`flex flex-col gap-1 overflow-x-hidden  relative text-sm-title`}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      {models?.map((o) => (
 | 
				
			||||||
 | 
					        <div
 | 
				
			||||||
 | 
					          key={o.displayName}
 | 
				
			||||||
 | 
					          className={`flex  items-center px-3 py-2 gap-3 rounded-action-btn hover:bg-select-option-hovered  cursor-pointer`}
 | 
				
			||||||
 | 
					          onClick={() => {
 | 
				
			||||||
 | 
					            close();
 | 
				
			||||||
 | 
					            chatStore.updateCurrentSession((session) => {
 | 
				
			||||||
 | 
					              session.mask.modelConfig.model = o.name as ModelType;
 | 
				
			||||||
 | 
					              session.mask.syncGlobalConfig = false;
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            showToast(o.name);
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					          ref={currentModel === o.name ? selectedItemRef : undefined}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <div className={`flex-1 text-text-select`}>{o.name}</div>
 | 
				
			||||||
 | 
					          <div
 | 
				
			||||||
 | 
					            className={currentModel === o.name ? "opacity-100" : "opacity-0"}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <Selected />
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      ))}
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (isMobileScreen) {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <Modal.Trigger
 | 
				
			||||||
 | 
					        content={(e) => (
 | 
				
			||||||
 | 
					          <div className="h-[100%]  overflow-y-auto" ref={contentRef}>
 | 
				
			||||||
 | 
					            {content(e)}
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					        type="bottom-drawer"
 | 
				
			||||||
 | 
					        onOpen={(e) => {
 | 
				
			||||||
 | 
					          if (e) {
 | 
				
			||||||
 | 
					            autoScrollToSelectedModal();
 | 
				
			||||||
 | 
					            getRelativePosition(rootRef.current!, "");
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					        title={Locale.Chat.SelectModel}
 | 
				
			||||||
 | 
					        headerBordered
 | 
				
			||||||
 | 
					        noFooter
 | 
				
			||||||
 | 
					        modelClassName="h-model-bottom-drawer"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <div
 | 
				
			||||||
 | 
					          className="flex items-center gap-1 cursor-pointer text-text-modal-select"
 | 
				
			||||||
 | 
					          ref={rootRef}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          {currentModel}
 | 
				
			||||||
 | 
					          <BottomArrowMobile />
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </Modal.Trigger>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Popover
 | 
				
			||||||
 | 
					      content={
 | 
				
			||||||
 | 
					        <div className="max-h-chat-actions-select-model-popover overflow-y-auto">
 | 
				
			||||||
 | 
					          {content({ close: () => {} })}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      trigger="click"
 | 
				
			||||||
 | 
					      noArrow
 | 
				
			||||||
 | 
					      placement={
 | 
				
			||||||
 | 
					        position?.poi.relativePosition[1] !== Orientation.bottom ? "lb" : "lt"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      popoverClassName="border border-select-popover rounded-lg shadow-select-popover-shadow w-actions-popover  bg-model-select-popover-panel w-[280px]"
 | 
				
			||||||
 | 
					      onShow={(e) => {
 | 
				
			||||||
 | 
					        if (e) {
 | 
				
			||||||
 | 
					          autoScrollToSelectedModal();
 | 
				
			||||||
 | 
					          getRelativePosition(rootRef.current!, "");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }}
 | 
				
			||||||
 | 
					      getPopoverPanelRef={(ref) => (contentRef.current = ref.current)}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <div
 | 
				
			||||||
 | 
					        className="flex items-center justify-center gap-1 cursor-pointer rounded-chat-model-select pl-3 pr-2.5 py-2 font-common leading-4 bg-chat-actions-select-model hover:bg-chat-actions-select-model-hover"
 | 
				
			||||||
 | 
					        ref={rootRef}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <div className="line-clamp-1 max-w-chat-actions-select-model text-sm-title text-text-modal-select">
 | 
				
			||||||
 | 
					          {currentModel}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <BottomArrow />
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </Popover>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default ModelSelect;
 | 
				
			||||||
							
								
								
									
										96
									
								
								app/containers/Chat/components/PromptHint.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,96 @@
 | 
				
			|||||||
 | 
					import { useEffect, useRef, useState } from "react";
 | 
				
			||||||
 | 
					import { Prompt } from "@/app/store/prompt";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import styles from "../index.module.scss";
 | 
				
			||||||
 | 
					import useShowPromptHint from "@/app/hooks/useShowPromptHint";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type RenderPompt = Pick<Prompt, "title" | "content">;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function PromptHints(props: {
 | 
				
			||||||
 | 
					  prompts: RenderPompt[];
 | 
				
			||||||
 | 
					  onPromptSelect: (prompt: RenderPompt) => void;
 | 
				
			||||||
 | 
					  className?: string;
 | 
				
			||||||
 | 
					}) {
 | 
				
			||||||
 | 
					  const noPrompts = props.prompts.length === 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [selectIndex, setSelectIndex] = useState(0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const selectedRef = useRef<HTMLDivElement>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const { internalPrompts, notShowPrompt } = useShowPromptHint({ ...props });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    setSelectIndex(0);
 | 
				
			||||||
 | 
					  }, [props.prompts.length]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    const onKeyDown = (e: KeyboardEvent) => {
 | 
				
			||||||
 | 
					      if (noPrompts || e.metaKey || e.altKey || e.ctrlKey) {
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      // arrow up / down to select prompt
 | 
				
			||||||
 | 
					      const changeIndex = (delta: number) => {
 | 
				
			||||||
 | 
					        e.stopPropagation();
 | 
				
			||||||
 | 
					        e.preventDefault();
 | 
				
			||||||
 | 
					        const nextIndex = Math.max(
 | 
				
			||||||
 | 
					          0,
 | 
				
			||||||
 | 
					          Math.min(props.prompts.length - 1, selectIndex + delta),
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        setSelectIndex(nextIndex);
 | 
				
			||||||
 | 
					        selectedRef.current?.scrollIntoView({
 | 
				
			||||||
 | 
					          block: "center",
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (e.key === "ArrowUp") {
 | 
				
			||||||
 | 
					        changeIndex(1);
 | 
				
			||||||
 | 
					      } else if (e.key === "ArrowDown") {
 | 
				
			||||||
 | 
					        changeIndex(-1);
 | 
				
			||||||
 | 
					      } else if (e.key === "Enter") {
 | 
				
			||||||
 | 
					        const selectedPrompt = props.prompts.at(selectIndex);
 | 
				
			||||||
 | 
					        if (selectedPrompt) {
 | 
				
			||||||
 | 
					          props.onPromptSelect(selectedPrompt);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    window.addEventListener("keydown", onKeyDown);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return () => window.removeEventListener("keydown", onKeyDown);
 | 
				
			||||||
 | 
					    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
				
			||||||
 | 
					  }, [props.prompts.length, selectIndex]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!internalPrompts.length) {
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div
 | 
				
			||||||
 | 
					      className={`
 | 
				
			||||||
 | 
					        transition-all duration-300 shadow-prompt-hint-container rounded-none  flex flex-col-reverse overflow-x-hidden
 | 
				
			||||||
 | 
					        ${
 | 
				
			||||||
 | 
					          notShowPrompt
 | 
				
			||||||
 | 
					            ? "max-h-[0vh] border-none"
 | 
				
			||||||
 | 
					            : "border-b pt-2.5 max-h-[50vh]"
 | 
				
			||||||
 | 
					        } 
 | 
				
			||||||
 | 
					        ${props.className}
 | 
				
			||||||
 | 
					      `}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      {internalPrompts.map((prompt, i) => (
 | 
				
			||||||
 | 
					        <div
 | 
				
			||||||
 | 
					          ref={i === selectIndex ? selectedRef : null}
 | 
				
			||||||
 | 
					          className={
 | 
				
			||||||
 | 
					            styles["prompt-hint"] +
 | 
				
			||||||
 | 
					            ` ${i === selectIndex ? styles["prompt-hint-selected"] : ""}`
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          key={prompt.title + i.toString()}
 | 
				
			||||||
 | 
					          onClick={() => props.onPromptSelect(prompt)}
 | 
				
			||||||
 | 
					          onMouseEnter={() => setSelectIndex(i)}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <div className={styles["hint-title"]}>{prompt.title}</div>
 | 
				
			||||||
 | 
					          <div className={styles["hint-content"]}>{prompt.content}</div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      ))}
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										32
									
								
								app/containers/Chat/components/PromptToast.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,32 @@
 | 
				
			|||||||
 | 
					import { useChatStore } from "@/app/store/chat";
 | 
				
			||||||
 | 
					import Locale from "@/app/locales";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import BrainIcon from "@/app/icons/brain.svg";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import styles from "../index.module.scss";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function PromptToast(props: {
 | 
				
			||||||
 | 
					  showToast?: boolean;
 | 
				
			||||||
 | 
					  setShowModal: (_: boolean) => void;
 | 
				
			||||||
 | 
					}) {
 | 
				
			||||||
 | 
					  const chatStore = useChatStore();
 | 
				
			||||||
 | 
					  const session = chatStore.currentSession();
 | 
				
			||||||
 | 
					  const context = session.mask.context;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className={styles["prompt-toast"]} key="prompt-toast">
 | 
				
			||||||
 | 
					      {props.showToast && (
 | 
				
			||||||
 | 
					        <div
 | 
				
			||||||
 | 
					          className={styles["prompt-toast-inner"] + " clickable"}
 | 
				
			||||||
 | 
					          role="button"
 | 
				
			||||||
 | 
					          onClick={() => props.setShowModal(true)}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <BrainIcon />
 | 
				
			||||||
 | 
					          <span className={styles["prompt-toast-content"]}>
 | 
				
			||||||
 | 
					            {Locale.Context.Toast(context.length)}
 | 
				
			||||||
 | 
					          </span>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										77
									
								
								app/containers/Chat/components/SessionConfigModal.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,77 @@
 | 
				
			|||||||
 | 
					import { Modal, showConfirm } from "@/app/components/ui-lib";
 | 
				
			||||||
 | 
					import { useChatStore } from "@/app/store/chat";
 | 
				
			||||||
 | 
					import { useMaskStore } from "@/app/store/mask";
 | 
				
			||||||
 | 
					import { useNavigate } from "react-router-dom";
 | 
				
			||||||
 | 
					import Locale from "@/app/locales";
 | 
				
			||||||
 | 
					import { IconButton } from "@/app/components/button";
 | 
				
			||||||
 | 
					import { Path } from "@/app/constant";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import ResetIcon from "@/app/icons/reload.svg";
 | 
				
			||||||
 | 
					import CopyIcon from "@/app/icons/copy.svg";
 | 
				
			||||||
 | 
					import MaskConfig from "@/app/containers/Settings/components/MaskConfig";
 | 
				
			||||||
 | 
					import { ListItem } from "@/app/components/List";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function SessionConfigModel(props: { onClose: () => void }) {
 | 
				
			||||||
 | 
					  const chatStore = useChatStore();
 | 
				
			||||||
 | 
					  const session = chatStore.currentSession();
 | 
				
			||||||
 | 
					  const maskStore = useMaskStore();
 | 
				
			||||||
 | 
					  const navigate = useNavigate();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className="modal-mask">
 | 
				
			||||||
 | 
					      <Modal
 | 
				
			||||||
 | 
					        title={Locale.Context.Edit}
 | 
				
			||||||
 | 
					        onClose={() => props.onClose()}
 | 
				
			||||||
 | 
					        actions={[
 | 
				
			||||||
 | 
					          <IconButton
 | 
				
			||||||
 | 
					            key="reset"
 | 
				
			||||||
 | 
					            icon={<ResetIcon />}
 | 
				
			||||||
 | 
					            bordered
 | 
				
			||||||
 | 
					            text={Locale.Chat.Config.Reset}
 | 
				
			||||||
 | 
					            onClick={async () => {
 | 
				
			||||||
 | 
					              if (await showConfirm(Locale.Memory.ResetConfirm)) {
 | 
				
			||||||
 | 
					                chatStore.updateCurrentSession(
 | 
				
			||||||
 | 
					                  (session) => (session.memoryPrompt = ""),
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					          />,
 | 
				
			||||||
 | 
					          <IconButton
 | 
				
			||||||
 | 
					            key="copy"
 | 
				
			||||||
 | 
					            icon={<CopyIcon />}
 | 
				
			||||||
 | 
					            bordered
 | 
				
			||||||
 | 
					            text={Locale.Chat.Config.SaveAs}
 | 
				
			||||||
 | 
					            onClick={() => {
 | 
				
			||||||
 | 
					              navigate(Path.Masks);
 | 
				
			||||||
 | 
					              setTimeout(() => {
 | 
				
			||||||
 | 
					                maskStore.create(session.mask);
 | 
				
			||||||
 | 
					              }, 500);
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					          />,
 | 
				
			||||||
 | 
					        ]}
 | 
				
			||||||
 | 
					        // className="!bg-modal-mask"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <MaskConfig
 | 
				
			||||||
 | 
					          mask={session.mask}
 | 
				
			||||||
 | 
					          updateMask={(updater) => {
 | 
				
			||||||
 | 
					            const mask = { ...session.mask };
 | 
				
			||||||
 | 
					            updater(mask);
 | 
				
			||||||
 | 
					            chatStore.updateCurrentSession((session) => (session.mask = mask));
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					          shouldSyncFromGlobal
 | 
				
			||||||
 | 
					          extraListItems={
 | 
				
			||||||
 | 
					            session.mask.modelConfig.sendMemory ? (
 | 
				
			||||||
 | 
					              <ListItem
 | 
				
			||||||
 | 
					                className="copyable"
 | 
				
			||||||
 | 
					                title={`${Locale.Memory.Title} (${session.lastSummarizeIndex} of ${session.messages.length})`}
 | 
				
			||||||
 | 
					                subTitle={session.memoryPrompt || Locale.Memory.EmptyContent}
 | 
				
			||||||
 | 
					              ></ListItem>
 | 
				
			||||||
 | 
					            ) : (
 | 
				
			||||||
 | 
					              <></>
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        ></MaskConfig>
 | 
				
			||||||
 | 
					      </Modal>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										184
									
								
								app/containers/Chat/components/SessionItem.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,184 @@
 | 
				
			|||||||
 | 
					import { Draggable } from "@hello-pangea/dnd";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import Locale from "@/app/locales";
 | 
				
			||||||
 | 
					import { useLocation } from "react-router-dom";
 | 
				
			||||||
 | 
					import { Path } from "@/app/constant";
 | 
				
			||||||
 | 
					import { Mask } from "@/app/store/mask";
 | 
				
			||||||
 | 
					import { useRef, useEffect } from "react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import DeleteChatIcon from "@/app/icons/deleteChatIcon.svg";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { getTime } from "@/app/utils";
 | 
				
			||||||
 | 
					import DeleteIcon from "@/app/icons/deleteIcon.svg";
 | 
				
			||||||
 | 
					import LogIcon from "@/app/icons/logIcon.svg";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import HoverPopover from "@/app/components/HoverPopover";
 | 
				
			||||||
 | 
					import Popover from "@/app/components/Popover";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function SessionItem(props: {
 | 
				
			||||||
 | 
					  onClick?: () => void;
 | 
				
			||||||
 | 
					  onDelete?: () => void;
 | 
				
			||||||
 | 
					  title: string;
 | 
				
			||||||
 | 
					  count: number;
 | 
				
			||||||
 | 
					  time: string;
 | 
				
			||||||
 | 
					  selected: boolean;
 | 
				
			||||||
 | 
					  id: string;
 | 
				
			||||||
 | 
					  index: number;
 | 
				
			||||||
 | 
					  narrow?: boolean;
 | 
				
			||||||
 | 
					  mask: Mask;
 | 
				
			||||||
 | 
					  isMobileScreen: boolean;
 | 
				
			||||||
 | 
					}) {
 | 
				
			||||||
 | 
					  const draggableRef = useRef<HTMLDivElement | null>(null);
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    if (props.selected && draggableRef.current) {
 | 
				
			||||||
 | 
					      draggableRef.current?.scrollIntoView({
 | 
				
			||||||
 | 
					        block: "center",
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [props.selected]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const { pathname: currentPath } = useLocation();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Draggable draggableId={`${props.id}`} index={props.index}>
 | 
				
			||||||
 | 
					      {(provided) => (
 | 
				
			||||||
 | 
					        <div
 | 
				
			||||||
 | 
					          className={`
 | 
				
			||||||
 | 
					              group/chat-menu-list relative flex p-3 items-center gap-2 self-stretch rounded-md mb-2 
 | 
				
			||||||
 | 
					              border 
 | 
				
			||||||
 | 
					              transition-colors duration-300 ease-in-out
 | 
				
			||||||
 | 
					              bg-chat-menu-session-unselected-mobile border-chat-menu-session-unselected-mobile
 | 
				
			||||||
 | 
					              md:bg-chat-menu-session-unselected md:border-chat-menu-session-unselected
 | 
				
			||||||
 | 
					              ${
 | 
				
			||||||
 | 
					                props.selected &&
 | 
				
			||||||
 | 
					                (currentPath === Path.Chat || currentPath === Path.Home)
 | 
				
			||||||
 | 
					                  ? `
 | 
				
			||||||
 | 
					                    md:!bg-chat-menu-session-selected md:!border-chat-menu-session-selected
 | 
				
			||||||
 | 
					                    !bg-chat-menu-session-selected-mobile !border-chat-menu-session-selected-mobile
 | 
				
			||||||
 | 
					                    `
 | 
				
			||||||
 | 
					                  : `md:hover:bg-chat-menu-session-hovered md:hover:chat-menu-session-hovered`
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            `}
 | 
				
			||||||
 | 
					          onClick={props.onClick}
 | 
				
			||||||
 | 
					          ref={(ele) => {
 | 
				
			||||||
 | 
					            draggableRef.current = ele;
 | 
				
			||||||
 | 
					            provided.innerRef(ele);
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					          {...provided.draggableProps}
 | 
				
			||||||
 | 
					          {...provided.dragHandleProps}
 | 
				
			||||||
 | 
					          title={`${props.title}\n${Locale.ChatItem.ChatItemCount(
 | 
				
			||||||
 | 
					            props.count,
 | 
				
			||||||
 | 
					          )}`}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <div className=" flex-shrink-0">
 | 
				
			||||||
 | 
					            <LogIcon />
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          <div className="flex flex-col flex-1">
 | 
				
			||||||
 | 
					            <div className={`flex justify-between items-center`}>
 | 
				
			||||||
 | 
					              <div
 | 
				
			||||||
 | 
					                className={` text-text-chat-menu-item-title text-sm-title line-clamp-1 flex-1`}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                {props.title}
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					              <div
 | 
				
			||||||
 | 
					                className={`text-text-chat-menu-item-time text-sm group-hover/chat-menu-list:opacity-0 pl-3 hidden md:block`}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                {getTime(props.time)}
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div className={`text-text-chat-menu-item-description text-sm`}>
 | 
				
			||||||
 | 
					              {Locale.ChatItem.ChatItemCount(props.count)}
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          <div
 | 
				
			||||||
 | 
					            className={`text-text-chat-menu-item-time text-sm pl-3 block md:hidden`}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            {getTime(props.time)}
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          {props.isMobileScreen ? (
 | 
				
			||||||
 | 
					            <Popover
 | 
				
			||||||
 | 
					              content={
 | 
				
			||||||
 | 
					                <div
 | 
				
			||||||
 | 
					                  className={`
 | 
				
			||||||
 | 
					                    flex items-center gap-3 p-3 rounded-action-btn leading-6 cursor-pointer
 | 
				
			||||||
 | 
					                    follow-parent-svg
 | 
				
			||||||
 | 
					                    fill-none
 | 
				
			||||||
 | 
					                    text-text-chat-menu-item-delete
 | 
				
			||||||
 | 
					                `}
 | 
				
			||||||
 | 
					                  onClickCapture={(e) => {
 | 
				
			||||||
 | 
					                    props.onDelete?.();
 | 
				
			||||||
 | 
					                  }}
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  <DeleteChatIcon />
 | 
				
			||||||
 | 
					                  <div className="flex-1 font-common text-actions-popover-menu-item ">
 | 
				
			||||||
 | 
					                    {Locale.Chat.Actions.Delete}
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					              popoverClassName={`
 | 
				
			||||||
 | 
					                    px-2 py-1 border-delete-chat-popover bg-delete-chat-popover-panel rounded-md shadow-delete-chat-popover-shadow 
 | 
				
			||||||
 | 
					                `}
 | 
				
			||||||
 | 
					              noArrow
 | 
				
			||||||
 | 
					              placement="r"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              <div
 | 
				
			||||||
 | 
					                className={`
 | 
				
			||||||
 | 
					                        cursor-pointer rounded-chat-img
 | 
				
			||||||
 | 
					                        md:!absolute md:top-[50%] md:translate-y-[-50%] md:right-3 md:pointer-events-none md:opacity-0 
 | 
				
			||||||
 | 
					                        md:group-hover/chat-menu-list:pointer-events-auto 
 | 
				
			||||||
 | 
					                        md:group-hover/chat-menu-list:opacity-100
 | 
				
			||||||
 | 
					                        md:hover:bg-select-hover 
 | 
				
			||||||
 | 
					                        follow-parent-svg
 | 
				
			||||||
 | 
					                        fill-none
 | 
				
			||||||
 | 
					                        text-text-chat-menu-item-time
 | 
				
			||||||
 | 
					                    `}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <DeleteIcon />
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </Popover>
 | 
				
			||||||
 | 
					          ) : (
 | 
				
			||||||
 | 
					            <HoverPopover
 | 
				
			||||||
 | 
					              content={
 | 
				
			||||||
 | 
					                <div
 | 
				
			||||||
 | 
					                  className={`
 | 
				
			||||||
 | 
					                    flex items-center gap-3 p-3 rounded-action-btn leading-6 cursor-pointer
 | 
				
			||||||
 | 
					                    follow-parent-svg
 | 
				
			||||||
 | 
					                    fill-none
 | 
				
			||||||
 | 
					                    text-text-chat-menu-item-delete
 | 
				
			||||||
 | 
					                `}
 | 
				
			||||||
 | 
					                  onClickCapture={(e) => {
 | 
				
			||||||
 | 
					                    props.onDelete?.();
 | 
				
			||||||
 | 
					                    e.preventDefault();
 | 
				
			||||||
 | 
					                    e.stopPropagation();
 | 
				
			||||||
 | 
					                  }}
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  <DeleteChatIcon />
 | 
				
			||||||
 | 
					                  <div className="flex-1 font-common text-actions-popover-menu-item text-text-chat-menu-item-delete">
 | 
				
			||||||
 | 
					                    {Locale.Chat.Actions.Delete}
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					              popoverClassName={`
 | 
				
			||||||
 | 
					                    px-2 py-1 border-delete-chat-popover bg-delete-chat-popover-panel rounded-md shadow-delete-chat-popover-shadow 
 | 
				
			||||||
 | 
					                `}
 | 
				
			||||||
 | 
					              noArrow
 | 
				
			||||||
 | 
					              align="start"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              <div
 | 
				
			||||||
 | 
					                className={`
 | 
				
			||||||
 | 
					                        cursor-pointer rounded-chat-img
 | 
				
			||||||
 | 
					                        md:!absolute md:top-[50%] md:translate-y-[-50%] md:right-3 md:pointer-events-none md:opacity-0 
 | 
				
			||||||
 | 
					                        md:group-hover/chat-menu-list:pointer-events-auto 
 | 
				
			||||||
 | 
					                        md:group-hover/chat-menu-list:opacity-100
 | 
				
			||||||
 | 
					                        md:hover:bg-select-hover 
 | 
				
			||||||
 | 
					                    `}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <DeleteIcon />
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </HoverPopover>
 | 
				
			||||||
 | 
					          )}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					    </Draggable>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										609
									
								
								app/containers/Chat/index.module.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,609 @@
 | 
				
			|||||||
 | 
					@import "~@/app/styles/animation.scss";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.attach-images {
 | 
				
			||||||
 | 
					  position: absolute;
 | 
				
			||||||
 | 
					  left: 30px;
 | 
				
			||||||
 | 
					  bottom: 32px;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.attach-image {
 | 
				
			||||||
 | 
					  cursor: default;
 | 
				
			||||||
 | 
					  width: 64px;
 | 
				
			||||||
 | 
					  height: 64px;
 | 
				
			||||||
 | 
					  border: rgba($color: #888, $alpha: 0.2) 1px solid;
 | 
				
			||||||
 | 
					  border-radius: 5px;
 | 
				
			||||||
 | 
					  margin-right: 10px;
 | 
				
			||||||
 | 
					  background-size: cover;
 | 
				
			||||||
 | 
					  background-position: center;
 | 
				
			||||||
 | 
					  background-color: var(--white);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .attach-image-mask {
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					    height: 100%;
 | 
				
			||||||
 | 
					    opacity: 0;
 | 
				
			||||||
 | 
					    transition: all ease 0.2s;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .attach-image-mask:hover {
 | 
				
			||||||
 | 
					    opacity: 1;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .delete-image {
 | 
				
			||||||
 | 
					    width: 24px;
 | 
				
			||||||
 | 
					    height: 24px;
 | 
				
			||||||
 | 
					    cursor: pointer;
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    align-items: center;
 | 
				
			||||||
 | 
					    justify-content: center;
 | 
				
			||||||
 | 
					    border-radius: 5px;
 | 
				
			||||||
 | 
					    float: right;
 | 
				
			||||||
 | 
					    background-color: var(--white);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.chat-input-actions {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-wrap: wrap;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .chat-input-action {
 | 
				
			||||||
 | 
					    display: inline-flex;
 | 
				
			||||||
 | 
					    border-radius: 20px;
 | 
				
			||||||
 | 
					    font-size: 12px;
 | 
				
			||||||
 | 
					    background-color: var(--white);
 | 
				
			||||||
 | 
					    color: var(--black);
 | 
				
			||||||
 | 
					    border: var(--border-in-light);
 | 
				
			||||||
 | 
					    padding: 4px 10px;
 | 
				
			||||||
 | 
					    animation: slide-in ease 0.3s;
 | 
				
			||||||
 | 
					    box-shadow: var(--card-shadow);
 | 
				
			||||||
 | 
					    transition: width ease 0.3s;
 | 
				
			||||||
 | 
					    align-items: center;
 | 
				
			||||||
 | 
					    height: 16px;
 | 
				
			||||||
 | 
					    width: var(--icon-width);
 | 
				
			||||||
 | 
					    overflow: hidden;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &:not(:last-child) {
 | 
				
			||||||
 | 
					      margin-right: 5px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .text {
 | 
				
			||||||
 | 
					      white-space: nowrap;
 | 
				
			||||||
 | 
					      padding-left: 5px;
 | 
				
			||||||
 | 
					      opacity: 0;
 | 
				
			||||||
 | 
					      transform: translateX(-5px);
 | 
				
			||||||
 | 
					      transition: all ease 0.3s;
 | 
				
			||||||
 | 
					      pointer-events: none;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &:hover {
 | 
				
			||||||
 | 
					      --delay: 0.5s;
 | 
				
			||||||
 | 
					      width: var(--full-width);
 | 
				
			||||||
 | 
					      transition-delay: var(--delay);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .text {
 | 
				
			||||||
 | 
					        transition-delay: var(--delay);
 | 
				
			||||||
 | 
					        opacity: 1;
 | 
				
			||||||
 | 
					        transform: translate(0);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .text,
 | 
				
			||||||
 | 
					    .icon {
 | 
				
			||||||
 | 
					      display: flex;
 | 
				
			||||||
 | 
					      align-items: center;
 | 
				
			||||||
 | 
					      justify-content: center;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.prompt-toast {
 | 
				
			||||||
 | 
					  position: absolute;
 | 
				
			||||||
 | 
					  bottom: -50px;
 | 
				
			||||||
 | 
					  z-index: 999;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  justify-content: center;
 | 
				
			||||||
 | 
					  width: calc(100% - 40px);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .prompt-toast-inner {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    justify-content: center;
 | 
				
			||||||
 | 
					    align-items: center;
 | 
				
			||||||
 | 
					    font-size: 12px;
 | 
				
			||||||
 | 
					    background-color: var(--white);
 | 
				
			||||||
 | 
					    color: var(--black);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    border: var(--border-in-light);
 | 
				
			||||||
 | 
					    box-shadow: var(--card-shadow);
 | 
				
			||||||
 | 
					    padding: 10px 20px;
 | 
				
			||||||
 | 
					    border-radius: 100px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    animation: slide-in-from-top ease 0.3s;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .prompt-toast-content {
 | 
				
			||||||
 | 
					      margin-left: 10px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.section-title {
 | 
				
			||||||
 | 
					  font-size: 12px;
 | 
				
			||||||
 | 
					  font-weight: bold;
 | 
				
			||||||
 | 
					  margin-bottom: 10px;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  justify-content: space-between;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .section-title-action {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    align-items: center;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.context-prompt {
 | 
				
			||||||
 | 
					  .context-prompt-insert {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    justify-content: center;
 | 
				
			||||||
 | 
					    padding: 4px;
 | 
				
			||||||
 | 
					    opacity: 0.2;
 | 
				
			||||||
 | 
					    transition: all ease 0.3s;
 | 
				
			||||||
 | 
					    background-color: rgba(0, 0, 0, 0);
 | 
				
			||||||
 | 
					    cursor: pointer;
 | 
				
			||||||
 | 
					    border-radius: 4px;
 | 
				
			||||||
 | 
					    margin-top: 4px;
 | 
				
			||||||
 | 
					    margin-bottom: 4px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &:hover {
 | 
				
			||||||
 | 
					      opacity: 1;
 | 
				
			||||||
 | 
					      background-color: rgba(0, 0, 0, 0.05);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .context-prompt-row {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    justify-content: center;
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &:hover {
 | 
				
			||||||
 | 
					      .context-drag {
 | 
				
			||||||
 | 
					        opacity: 1;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .context-drag {
 | 
				
			||||||
 | 
					      display: flex;
 | 
				
			||||||
 | 
					      align-items: center;
 | 
				
			||||||
 | 
					      opacity: 0.5;
 | 
				
			||||||
 | 
					      transition: all ease 0.3s;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .context-role {
 | 
				
			||||||
 | 
					      margin-right: 10px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .context-content {
 | 
				
			||||||
 | 
					      flex: 1;
 | 
				
			||||||
 | 
					      max-width: 100%;
 | 
				
			||||||
 | 
					      text-align: left;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .context-delete-button {
 | 
				
			||||||
 | 
					      margin-left: 10px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .context-prompt-button {
 | 
				
			||||||
 | 
					    flex: 1;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.memory-prompt {
 | 
				
			||||||
 | 
					  margin: 20px 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .memory-prompt-content {
 | 
				
			||||||
 | 
					    background-color: var(--white);
 | 
				
			||||||
 | 
					    color: var(--black);
 | 
				
			||||||
 | 
					    border: var(--border-in-light);
 | 
				
			||||||
 | 
					    border-radius: 10px;
 | 
				
			||||||
 | 
					    padding: 10px;
 | 
				
			||||||
 | 
					    font-size: 12px;
 | 
				
			||||||
 | 
					    user-select: text;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.clear-context {
 | 
				
			||||||
 | 
					  margin: 20px 0 0 0;
 | 
				
			||||||
 | 
					  padding: 4px 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  border-top: var(--border-in-light);
 | 
				
			||||||
 | 
					  border-bottom: var(--border-in-light);
 | 
				
			||||||
 | 
					  box-shadow: var(--card-shadow) inset;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  justify-content: center;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  color: var(--black);
 | 
				
			||||||
 | 
					  transition: all ease 0.3s;
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					  overflow: hidden;
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					  font-size: 12px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  animation: slide-in ease 0.3s;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  $linear: linear-gradient(to right,
 | 
				
			||||||
 | 
					      rgba(0, 0, 0, 0),
 | 
				
			||||||
 | 
					      rgba(0, 0, 0, 1),
 | 
				
			||||||
 | 
					      rgba(0, 0, 0, 0));
 | 
				
			||||||
 | 
					  mask-image: $linear;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @mixin show {
 | 
				
			||||||
 | 
					    transform: translateY(0);
 | 
				
			||||||
 | 
					    position: relative;
 | 
				
			||||||
 | 
					    transition: all ease 0.3s;
 | 
				
			||||||
 | 
					    opacity: 1;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @mixin hide {
 | 
				
			||||||
 | 
					    transform: translateY(-50%);
 | 
				
			||||||
 | 
					    position: absolute;
 | 
				
			||||||
 | 
					    transition: all ease 0.1s;
 | 
				
			||||||
 | 
					    opacity: 0;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &-tips {
 | 
				
			||||||
 | 
					    @include show;
 | 
				
			||||||
 | 
					    opacity: 0.5;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &-revert-btn {
 | 
				
			||||||
 | 
					    color: var(--primary);
 | 
				
			||||||
 | 
					    @include hide;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &:hover {
 | 
				
			||||||
 | 
					    opacity: 1;
 | 
				
			||||||
 | 
					    border-color: var(--primary);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .clear-context-tips {
 | 
				
			||||||
 | 
					      @include hide;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .clear-context-revert-btn {
 | 
				
			||||||
 | 
					      @include show;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.chat {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					  // height: 100%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.chat-body {
 | 
				
			||||||
 | 
					  flex: 1;
 | 
				
			||||||
 | 
					  overflow: auto;
 | 
				
			||||||
 | 
					  overflow-x: hidden;
 | 
				
			||||||
 | 
					  padding: 20px;
 | 
				
			||||||
 | 
					  padding-bottom: 40px;
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					  overscroll-behavior: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.chat-body-main-title {
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &:hover {
 | 
				
			||||||
 | 
					    text-decoration: underline;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@media only screen and (max-width: 600px) {
 | 
				
			||||||
 | 
					  .chat-body-title {
 | 
				
			||||||
 | 
					    text-align: center;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.chat-message {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-direction: row;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &:last-child {
 | 
				
			||||||
 | 
					    animation: slide-in ease 0.3s;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.chat-message-user {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-direction: row-reverse;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .chat-message-header {
 | 
				
			||||||
 | 
					    flex-direction: row-reverse;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.chat-message-header {
 | 
				
			||||||
 | 
					  margin-top: 20px;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .chat-message-actions {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    box-sizing: border-box;
 | 
				
			||||||
 | 
					    font-size: 12px;
 | 
				
			||||||
 | 
					    align-items: flex-end;
 | 
				
			||||||
 | 
					    justify-content: space-between;
 | 
				
			||||||
 | 
					    transition: all ease 0.3s;
 | 
				
			||||||
 | 
					    transform: scale(0.9) translateY(5px);
 | 
				
			||||||
 | 
					    margin: 0 10px;
 | 
				
			||||||
 | 
					    opacity: 0;
 | 
				
			||||||
 | 
					    pointer-events: none;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .chat-input-actions {
 | 
				
			||||||
 | 
					      display: flex;
 | 
				
			||||||
 | 
					      flex-wrap: nowrap;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.chat-message-container {
 | 
				
			||||||
 | 
					  max-width: var(--message-max-width);
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					  align-items: flex-start;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &:hover {
 | 
				
			||||||
 | 
					    .chat-message-edit {
 | 
				
			||||||
 | 
					      opacity: 0.9;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .chat-message-actions {
 | 
				
			||||||
 | 
					      opacity: 1;
 | 
				
			||||||
 | 
					      pointer-events: all;
 | 
				
			||||||
 | 
					      transform: scale(1) translateY(0);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.chat-message-user>.chat-message-container {
 | 
				
			||||||
 | 
					  align-items: flex-end;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.chat-message-avatar {
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .chat-message-edit {
 | 
				
			||||||
 | 
					    position: absolute;
 | 
				
			||||||
 | 
					    height: 100%;
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					    overflow: hidden;
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    align-items: center;
 | 
				
			||||||
 | 
					    justify-content: center;
 | 
				
			||||||
 | 
					    opacity: 0;
 | 
				
			||||||
 | 
					    transition: all ease 0.3s;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    button {
 | 
				
			||||||
 | 
					      padding: 7px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /* Specific styles for iOS devices */
 | 
				
			||||||
 | 
					  @media screen and (max-device-width: 812px) and (-webkit-min-device-pixel-ratio: 2) {
 | 
				
			||||||
 | 
					    @supports (-webkit-touch-callout: none) {
 | 
				
			||||||
 | 
					      .chat-message-edit {
 | 
				
			||||||
 | 
					        top: -8%;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.chat-message-status {
 | 
				
			||||||
 | 
					  font-size: 12px;
 | 
				
			||||||
 | 
					  color: #aaa;
 | 
				
			||||||
 | 
					  line-height: 1.5;
 | 
				
			||||||
 | 
					  margin-top: 5px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.chat-message-item {
 | 
				
			||||||
 | 
					  // box-sizing: border-box;
 | 
				
			||||||
 | 
					  // max-width: 100%;
 | 
				
			||||||
 | 
					  // margin-top: 10px;
 | 
				
			||||||
 | 
					  // border-radius: 10px;
 | 
				
			||||||
 | 
					  // background-color: rgba(0, 0, 0, 0.05);
 | 
				
			||||||
 | 
					  // padding: 10px;
 | 
				
			||||||
 | 
					  // font-size: 14px;
 | 
				
			||||||
 | 
					  // user-select: text;
 | 
				
			||||||
 | 
					  // word-break: break-word;
 | 
				
			||||||
 | 
					  // border: var(--border-in-light);
 | 
				
			||||||
 | 
					  // position: relative;
 | 
				
			||||||
 | 
					  transition: all ease 0.3s;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.chat-message-item-image {
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  margin-top: 10px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.chat-message-item-images {
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  display: grid;
 | 
				
			||||||
 | 
					  justify-content: left;
 | 
				
			||||||
 | 
					  grid-gap: 10px;
 | 
				
			||||||
 | 
					  grid-template-columns: repeat(var(--image-count), auto);
 | 
				
			||||||
 | 
					  margin-top: 10px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.chat-message-item-image-multi {
 | 
				
			||||||
 | 
					  object-fit: cover;
 | 
				
			||||||
 | 
					  background-size: cover;
 | 
				
			||||||
 | 
					  background-position: center;
 | 
				
			||||||
 | 
					  background-repeat: no-repeat;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.chat-message-item-image,
 | 
				
			||||||
 | 
					.chat-message-item-image-multi {
 | 
				
			||||||
 | 
					  box-sizing: border-box;
 | 
				
			||||||
 | 
					  border-radius: 10px;
 | 
				
			||||||
 | 
					  border: rgba($color: #888, $alpha: 0.2) 1px solid;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@media only screen and (max-width: 600px) {
 | 
				
			||||||
 | 
					  $calc-image-width: calc(100vw/3*2/var(--image-count));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .chat-message-item-image-multi {
 | 
				
			||||||
 | 
					    width: $calc-image-width;
 | 
				
			||||||
 | 
					    height: $calc-image-width;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  .chat-message-item-image {
 | 
				
			||||||
 | 
					    max-width: calc(100vw/3*2);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@media screen and (min-width: 600px) {
 | 
				
			||||||
 | 
					  $max-image-width: calc(calc(1200px - var(--sidebar-width))/3*2/var(--image-count));
 | 
				
			||||||
 | 
					  $image-width: calc(calc(var(--window-width) - var(--sidebar-width))/3*2/var(--image-count));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .chat-message-item-image-multi {
 | 
				
			||||||
 | 
					    width: $image-width;
 | 
				
			||||||
 | 
					    height: $image-width;
 | 
				
			||||||
 | 
					    max-width: $max-image-width;
 | 
				
			||||||
 | 
					    max-height: $max-image-width;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .chat-message-item-image {
 | 
				
			||||||
 | 
					    max-width: calc(calc(1200px - var(--sidebar-width))/3*2);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// .chat-message-action-date {
 | 
				
			||||||
 | 
					//   // font-size: 12px;
 | 
				
			||||||
 | 
					//   // opacity: 0.2;
 | 
				
			||||||
 | 
					//   // white-space: nowrap;
 | 
				
			||||||
 | 
					//   // transition: all ease 0.6s;
 | 
				
			||||||
 | 
					//   // color: var(--black);
 | 
				
			||||||
 | 
					//   // text-align: right;
 | 
				
			||||||
 | 
					//   // width: 100%;
 | 
				
			||||||
 | 
					//   // box-sizing: border-box;
 | 
				
			||||||
 | 
					//   // padding-right: 10px;
 | 
				
			||||||
 | 
					//   // pointer-events: none;
 | 
				
			||||||
 | 
					//   // z-index: 1;
 | 
				
			||||||
 | 
					// }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.chat-message-user>.chat-message-container>.chat-message-item {
 | 
				
			||||||
 | 
					  background-color: var(--second);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &:hover {
 | 
				
			||||||
 | 
					    min-width: 0;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.chat-input-panel {
 | 
				
			||||||
 | 
					  // position: relative;
 | 
				
			||||||
 | 
					  // width: 100%;
 | 
				
			||||||
 | 
					  // padding: 20px;
 | 
				
			||||||
 | 
					  // padding-top: 10px;
 | 
				
			||||||
 | 
					  // box-sizing: border-box;
 | 
				
			||||||
 | 
					  // flex-direction: column;
 | 
				
			||||||
 | 
					  // border-top: var(--border-in-light);
 | 
				
			||||||
 | 
					  // box-shadow: var(--card-shadow);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .chat-input-actions {
 | 
				
			||||||
 | 
					    .chat-input-action {
 | 
				
			||||||
 | 
					      margin-bottom: 10px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@mixin single-line {
 | 
				
			||||||
 | 
					  white-space: nowrap;
 | 
				
			||||||
 | 
					  overflow: hidden;
 | 
				
			||||||
 | 
					  text-overflow: ellipsis;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.prompt-hint {
 | 
				
			||||||
 | 
					  color:var(--btn-default-text);
 | 
				
			||||||
 | 
					  padding: 6px 10px;
 | 
				
			||||||
 | 
					  border: transparent 1px solid;
 | 
				
			||||||
 | 
					  margin: 4px;
 | 
				
			||||||
 | 
					  border-radius: 8px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &:not(:last-child) {
 | 
				
			||||||
 | 
					    margin-top: 0;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .hint-title {
 | 
				
			||||||
 | 
					    font-size: 12px;
 | 
				
			||||||
 | 
					    font-weight: bolder;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @include single-line();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .hint-content {
 | 
				
			||||||
 | 
					    font-size: 12px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @include single-line();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &-selected,
 | 
				
			||||||
 | 
					  &:hover {
 | 
				
			||||||
 | 
					    border-color: var(--primary);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// .chat-input-panel-inner {
 | 
				
			||||||
 | 
					//   cursor: text;
 | 
				
			||||||
 | 
					//   display: flex;
 | 
				
			||||||
 | 
					//   flex: 1;
 | 
				
			||||||
 | 
					//   border-radius: 10px;
 | 
				
			||||||
 | 
					//   border: var(--border-in-light);
 | 
				
			||||||
 | 
					// }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.chat-input-panel-inner-attach {
 | 
				
			||||||
 | 
					  padding-bottom: 80px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.chat-input-panel-inner:has(.chat-input:focus) {
 | 
				
			||||||
 | 
					  border: 1px solid var(--primary);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.chat-input {
 | 
				
			||||||
 | 
					  height: 100%;
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  border-radius: 10px;
 | 
				
			||||||
 | 
					  border: none;
 | 
				
			||||||
 | 
					  box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.03);
 | 
				
			||||||
 | 
					  background-color: var(--white);
 | 
				
			||||||
 | 
					  color: var(--black);
 | 
				
			||||||
 | 
					  font-family: inherit;
 | 
				
			||||||
 | 
					  padding: 10px 90px 10px 14px;
 | 
				
			||||||
 | 
					  resize: none;
 | 
				
			||||||
 | 
					  outline: none;
 | 
				
			||||||
 | 
					  box-sizing: border-box;
 | 
				
			||||||
 | 
					  min-height: 68px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.chat-input:focus {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.chat-input-send {
 | 
				
			||||||
 | 
					  background-color: var(--primary);
 | 
				
			||||||
 | 
					  color: white;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  position: absolute;
 | 
				
			||||||
 | 
					  right: 30px;
 | 
				
			||||||
 | 
					  bottom: 32px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@media only screen and (max-width: 600px) {
 | 
				
			||||||
 | 
					  .chat-input {
 | 
				
			||||||
 | 
					    font-size: 16px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .chat-input-send {
 | 
				
			||||||
 | 
					    bottom: 30px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										146
									
								
								app/containers/Chat/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,146 @@
 | 
				
			|||||||
 | 
					import {
 | 
				
			||||||
 | 
					  DragDropContext,
 | 
				
			||||||
 | 
					  Droppable,
 | 
				
			||||||
 | 
					  OnDragEndResponder,
 | 
				
			||||||
 | 
					} from "@hello-pangea/dnd";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { useAppConfig, useChatStore } from "@/app/store";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import Locale from "@/app/locales";
 | 
				
			||||||
 | 
					import { useLocation, useNavigate } from "react-router-dom";
 | 
				
			||||||
 | 
					import { Path } from "@/app/constant";
 | 
				
			||||||
 | 
					import { useEffect } from "react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import AddIcon from "@/app/icons/addIcon.svg";
 | 
				
			||||||
 | 
					import NextChatTitle from "@/app/icons/nextchatTitle.svg";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import MenuLayout from "@/app/components/MenuLayout";
 | 
				
			||||||
 | 
					import Panel from "./ChatPanel";
 | 
				
			||||||
 | 
					import Modal from "@/app/components/Modal";
 | 
				
			||||||
 | 
					import SessionItem from "./components/SessionItem";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default MenuLayout(function SessionList(props) {
 | 
				
			||||||
 | 
					  const { setShowPanel } = props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [sessions, selectedIndex, selectSession, moveSession] = useChatStore(
 | 
				
			||||||
 | 
					    (state) => [
 | 
				
			||||||
 | 
					      state.sessions,
 | 
				
			||||||
 | 
					      state.currentSessionIndex,
 | 
				
			||||||
 | 
					      state.selectSession,
 | 
				
			||||||
 | 
					      state.moveSession,
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					  const navigate = useNavigate();
 | 
				
			||||||
 | 
					  const config = useAppConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const { isMobileScreen } = config;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const chatStore = useChatStore();
 | 
				
			||||||
 | 
					  const { pathname: currentPath } = useLocation();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    setShowPanel?.(currentPath === Path.Chat);
 | 
				
			||||||
 | 
					  }, [currentPath]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const onDragEnd: OnDragEndResponder = (result) => {
 | 
				
			||||||
 | 
					    const { destination, source } = result;
 | 
				
			||||||
 | 
					    if (!destination) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (
 | 
				
			||||||
 | 
					      destination.droppableId === source.droppableId &&
 | 
				
			||||||
 | 
					      destination.index === source.index
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    moveSession(source.index, destination.index);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div
 | 
				
			||||||
 | 
					      className={`
 | 
				
			||||||
 | 
					      h-[100%] flex flex-col
 | 
				
			||||||
 | 
					      md:px-0
 | 
				
			||||||
 | 
					    `}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <div data-tauri-drag-region>
 | 
				
			||||||
 | 
					        <div
 | 
				
			||||||
 | 
					          className={`
 | 
				
			||||||
 | 
					            flex items-center justify-between
 | 
				
			||||||
 | 
					            py-6 max-md:box-content max-md:h-0
 | 
				
			||||||
 | 
					            md:py-7
 | 
				
			||||||
 | 
					          `}
 | 
				
			||||||
 | 
					          data-tauri-drag-region
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <div className="">
 | 
				
			||||||
 | 
					            <NextChatTitle />
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          <div
 | 
				
			||||||
 | 
					            className=" cursor-pointer"
 | 
				
			||||||
 | 
					            onClick={() => {
 | 
				
			||||||
 | 
					              if (config.dontShowMaskSplashScreen) {
 | 
				
			||||||
 | 
					                chatStore.newSession();
 | 
				
			||||||
 | 
					                navigate(Path.Chat);
 | 
				
			||||||
 | 
					              } else {
 | 
				
			||||||
 | 
					                navigate(Path.NewChat);
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <AddIcon />
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <div
 | 
				
			||||||
 | 
					          className={`pb-3 text-sm sm:text-sm-mobile text-text-chat-header-subtitle`}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          Build your own AI assistant.
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div className={`flex-1 overflow-y-auto max-md:pb-chat-panel-mobile `}>
 | 
				
			||||||
 | 
					        <DragDropContext onDragEnd={onDragEnd}>
 | 
				
			||||||
 | 
					          <Droppable droppableId="chat-list">
 | 
				
			||||||
 | 
					            {(provided) => (
 | 
				
			||||||
 | 
					              <div
 | 
				
			||||||
 | 
					                ref={provided.innerRef}
 | 
				
			||||||
 | 
					                {...provided.droppableProps}
 | 
				
			||||||
 | 
					                className={`w-[100%]`}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                {sessions.map((item, i) => (
 | 
				
			||||||
 | 
					                  <SessionItem
 | 
				
			||||||
 | 
					                    title={item.topic}
 | 
				
			||||||
 | 
					                    time={new Date(item.lastUpdate).toLocaleString()}
 | 
				
			||||||
 | 
					                    count={item.messages.length}
 | 
				
			||||||
 | 
					                    key={item.id}
 | 
				
			||||||
 | 
					                    id={item.id}
 | 
				
			||||||
 | 
					                    index={i}
 | 
				
			||||||
 | 
					                    selected={i === selectedIndex}
 | 
				
			||||||
 | 
					                    onClick={() => {
 | 
				
			||||||
 | 
					                      navigate(Path.Chat);
 | 
				
			||||||
 | 
					                      selectSession(i);
 | 
				
			||||||
 | 
					                    }}
 | 
				
			||||||
 | 
					                    onDelete={async () => {
 | 
				
			||||||
 | 
					                      if (
 | 
				
			||||||
 | 
					                        await Modal.warn({
 | 
				
			||||||
 | 
					                          okText: Locale.ChatItem.DeleteOkBtn,
 | 
				
			||||||
 | 
					                          cancelText: Locale.ChatItem.DeleteCancelBtn,
 | 
				
			||||||
 | 
					                          title: Locale.ChatItem.DeleteTitle,
 | 
				
			||||||
 | 
					                          content: Locale.ChatItem.DeleteContent,
 | 
				
			||||||
 | 
					                        })
 | 
				
			||||||
 | 
					                      ) {
 | 
				
			||||||
 | 
					                        chatStore.deleteSession(i);
 | 
				
			||||||
 | 
					                      }
 | 
				
			||||||
 | 
					                    }}
 | 
				
			||||||
 | 
					                    mask={item.mask}
 | 
				
			||||||
 | 
					                    isMobileScreen={isMobileScreen}
 | 
				
			||||||
 | 
					                  />
 | 
				
			||||||
 | 
					                ))}
 | 
				
			||||||
 | 
					                {provided.placeholder}
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					          </Droppable>
 | 
				
			||||||
 | 
					        </DragDropContext>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}, Panel);
 | 
				
			||||||
							
								
								
									
										137
									
								
								app/containers/Settings/SettingPanel.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,137 @@
 | 
				
			|||||||
 | 
					import { useEffect, useMemo } from "react";
 | 
				
			||||||
 | 
					import { useAccessStore, useAppConfig } from "@/app/store";
 | 
				
			||||||
 | 
					import Locale from "@/app/locales";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { Path } from "@/app/constant";
 | 
				
			||||||
 | 
					import List from "@/app/components/List";
 | 
				
			||||||
 | 
					import { useNavigate } from "react-router-dom";
 | 
				
			||||||
 | 
					import { getClientConfig } from "@/app/config/client";
 | 
				
			||||||
 | 
					import Card from "@/app/components/Card";
 | 
				
			||||||
 | 
					import SettingHeader from "./components/SettingHeader";
 | 
				
			||||||
 | 
					import { MenuWrapperInspectProps } from "@/app/components/MenuLayout";
 | 
				
			||||||
 | 
					import SyncItems from "./components/SyncItems";
 | 
				
			||||||
 | 
					import DangerItems from "./components/DangerItems";
 | 
				
			||||||
 | 
					import AppSetting from "./components/AppSetting";
 | 
				
			||||||
 | 
					import MaskSetting from "./components/MaskSetting";
 | 
				
			||||||
 | 
					import PromptSetting from "./components/PromptSetting";
 | 
				
			||||||
 | 
					import ProviderSetting from "./components/ProviderSetting";
 | 
				
			||||||
 | 
					import ModelConfigList from "./components/ModelSetting";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function Settings(props: MenuWrapperInspectProps) {
 | 
				
			||||||
 | 
					  const { setShowPanel, id } = props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const navigate = useNavigate();
 | 
				
			||||||
 | 
					  const accessStore = useAccessStore();
 | 
				
			||||||
 | 
					  const config = useAppConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const { isMobileScreen } = config;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    const keydownEvent = (e: KeyboardEvent) => {
 | 
				
			||||||
 | 
					      if (e.key === "Escape") {
 | 
				
			||||||
 | 
					        navigate(Path.Home);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    if (clientConfig?.isApp) {
 | 
				
			||||||
 | 
					      // Force to set custom endpoint to true if it's app
 | 
				
			||||||
 | 
					      accessStore.update((state) => {
 | 
				
			||||||
 | 
					        state.useCustomConfig = true;
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    document.addEventListener("keydown", keydownEvent);
 | 
				
			||||||
 | 
					    return () => {
 | 
				
			||||||
 | 
					      document.removeEventListener("keydown", keydownEvent);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const clientConfig = useMemo(() => getClientConfig(), []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const cardClassName = "mb-6 md:mb-8 last:mb-0";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const itemMap = {
 | 
				
			||||||
 | 
					    [Locale.Settings.GeneralSettings]: (
 | 
				
			||||||
 | 
					      <>
 | 
				
			||||||
 | 
					        <Card className={cardClassName} title={Locale.Settings.Basic.Title}>
 | 
				
			||||||
 | 
					          <AppSetting />
 | 
				
			||||||
 | 
					        </Card>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <Card className={cardClassName} title={Locale.Settings.Mask.Title}>
 | 
				
			||||||
 | 
					          <MaskSetting />
 | 
				
			||||||
 | 
					        </Card>
 | 
				
			||||||
 | 
					        <Card className={cardClassName} title={Locale.Settings.Prompt.Title}>
 | 
				
			||||||
 | 
					          <PromptSetting />
 | 
				
			||||||
 | 
					        </Card>
 | 
				
			||||||
 | 
					        <Card className={cardClassName} title={Locale.Settings.Provider.Title}>
 | 
				
			||||||
 | 
					          <ProviderSetting />
 | 
				
			||||||
 | 
					        </Card>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <Card className={cardClassName} title={Locale.Settings.Danger.Title}>
 | 
				
			||||||
 | 
					          <DangerItems />
 | 
				
			||||||
 | 
					        </Card>
 | 
				
			||||||
 | 
					      </>
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					    [Locale.Settings.ModelSettings]: (
 | 
				
			||||||
 | 
					      <Card className={cardClassName} title={Locale.Settings.Models.Title}>
 | 
				
			||||||
 | 
					        <List
 | 
				
			||||||
 | 
					          widgetStyle={{
 | 
				
			||||||
 | 
					            // selectClassName: "min-w-select-mobile-lg",
 | 
				
			||||||
 | 
					            selectClassName: "min-w-select-mobile md:min-w-select",
 | 
				
			||||||
 | 
					            inputClassName: "md:min-w-select",
 | 
				
			||||||
 | 
					            rangeClassName: "md:min-w-select",
 | 
				
			||||||
 | 
					            rangeNextLine: isMobileScreen,
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <ModelConfigList
 | 
				
			||||||
 | 
					            modelConfig={config.modelConfig}
 | 
				
			||||||
 | 
					            updateConfig={(updater) => {
 | 
				
			||||||
 | 
					              const modelConfig = { ...config.modelConfig };
 | 
				
			||||||
 | 
					              updater(modelConfig);
 | 
				
			||||||
 | 
					              config.update((config) => (config.modelConfig = modelConfig));
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </List>
 | 
				
			||||||
 | 
					      </Card>
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					    [Locale.Settings.DataSettings]: (
 | 
				
			||||||
 | 
					      <Card className={cardClassName} title={Locale.Settings.Sync.Title}>
 | 
				
			||||||
 | 
					        <SyncItems />
 | 
				
			||||||
 | 
					      </Card>
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div
 | 
				
			||||||
 | 
					      className={`
 | 
				
			||||||
 | 
					        flex flex-col overflow-hidden bg-settings-panel 
 | 
				
			||||||
 | 
					        h-setting-panel-mobile
 | 
				
			||||||
 | 
					        md:h-[100%] md:mr-2.5 md:rounded-md
 | 
				
			||||||
 | 
					      `}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <SettingHeader
 | 
				
			||||||
 | 
					        isMobileScreen={isMobileScreen}
 | 
				
			||||||
 | 
					        goback={() => setShowPanel?.(false)}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					      <div
 | 
				
			||||||
 | 
					        className={`
 | 
				
			||||||
 | 
					          max-md:w-[100%]
 | 
				
			||||||
 | 
					          px-4 py-5
 | 
				
			||||||
 | 
					          md:px-6 md:py-8
 | 
				
			||||||
 | 
					          flex items-start justify-center
 | 
				
			||||||
 | 
					          overflow-y-auto
 | 
				
			||||||
 | 
					        `}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <div
 | 
				
			||||||
 | 
					          className={`
 | 
				
			||||||
 | 
					            w-full
 | 
				
			||||||
 | 
					            max-w-screen-md
 | 
				
			||||||
 | 
					            !overflow-x-hidden 
 | 
				
			||||||
 | 
					            overflow-y-auto
 | 
				
			||||||
 | 
					          `}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          {itemMap[id] || null}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										200
									
								
								app/containers/Settings/components/AppSetting.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,200 @@
 | 
				
			|||||||
 | 
					import LoadingIcon from "@/app/icons/three-dots.svg";
 | 
				
			||||||
 | 
					import ResetIcon from "@/app/icons/reload.svg";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import styles from "../index.module.scss";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { useEffect, useState } from "react";
 | 
				
			||||||
 | 
					import { Avatar, AvatarPicker } from "@/app/components/emoji";
 | 
				
			||||||
 | 
					import { Popover } from "@/app/components/ui-lib";
 | 
				
			||||||
 | 
					import Locale, {
 | 
				
			||||||
 | 
					  ALL_LANG_OPTIONS,
 | 
				
			||||||
 | 
					  AllLangs,
 | 
				
			||||||
 | 
					  changeLang,
 | 
				
			||||||
 | 
					  getLang,
 | 
				
			||||||
 | 
					} from "@/app/locales";
 | 
				
			||||||
 | 
					import Link from "next/link";
 | 
				
			||||||
 | 
					import { IconButton } from "@/app/components/button";
 | 
				
			||||||
 | 
					import { useUpdateStore } from "@/app/store/update";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  SubmitKey,
 | 
				
			||||||
 | 
					  Theme,
 | 
				
			||||||
 | 
					  ThemeConfig,
 | 
				
			||||||
 | 
					  useAppConfig,
 | 
				
			||||||
 | 
					} from "@/app/store/config";
 | 
				
			||||||
 | 
					import { getClientConfig } from "@/app/config/client";
 | 
				
			||||||
 | 
					import { RELEASE_URL, UPDATE_URL } from "@/app/constant";
 | 
				
			||||||
 | 
					import List, { ListItem } from "@/app/components/List";
 | 
				
			||||||
 | 
					import Select from "@/app/components/Select";
 | 
				
			||||||
 | 
					import SlideRange from "@/app/components/SlideRange";
 | 
				
			||||||
 | 
					import Switch from "@/app/components/Switch";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface AppSettingProps {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function AppSetting(props: AppSettingProps) {
 | 
				
			||||||
 | 
					  const [checkingUpdate, setCheckingUpdate] = useState(false);
 | 
				
			||||||
 | 
					  const [showEmojiPicker, setShowEmojiPicker] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const updateStore = useUpdateStore();
 | 
				
			||||||
 | 
					  const config = useAppConfig();
 | 
				
			||||||
 | 
					  const { update: updateConfig, isMobileScreen } = config;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const currentVersion = updateStore.formatVersion(updateStore.version);
 | 
				
			||||||
 | 
					  const remoteId = updateStore.formatVersion(updateStore.remoteVersion);
 | 
				
			||||||
 | 
					  const hasNewVersion = currentVersion !== remoteId;
 | 
				
			||||||
 | 
					  const updateUrl = getClientConfig()?.isApp ? RELEASE_URL : UPDATE_URL;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function checkUpdate(force = false) {
 | 
				
			||||||
 | 
					    setCheckingUpdate(true);
 | 
				
			||||||
 | 
					    updateStore.getLatestVersion(force).then(() => {
 | 
				
			||||||
 | 
					      setCheckingUpdate(false);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log("[Update] local version ", updateStore.version);
 | 
				
			||||||
 | 
					    console.log("[Update] remote version ", updateStore.remoteVersion);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    // checks per minutes
 | 
				
			||||||
 | 
					    checkUpdate();
 | 
				
			||||||
 | 
					    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <List
 | 
				
			||||||
 | 
					      widgetStyle={{
 | 
				
			||||||
 | 
					        selectClassName: "min-w-select-mobile md:min-w-select",
 | 
				
			||||||
 | 
					        inputClassName: "md:min-w-select",
 | 
				
			||||||
 | 
					        rangeClassName: "md:min-w-select",
 | 
				
			||||||
 | 
					        rangeNextLine: isMobileScreen,
 | 
				
			||||||
 | 
					      }}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <ListItem title={Locale.Settings.Avatar}>
 | 
				
			||||||
 | 
					        <Popover
 | 
				
			||||||
 | 
					          onClose={() => setShowEmojiPicker(false)}
 | 
				
			||||||
 | 
					          content={
 | 
				
			||||||
 | 
					            <AvatarPicker
 | 
				
			||||||
 | 
					              onEmojiClick={(avatar: string) => {
 | 
				
			||||||
 | 
					                updateConfig((config) => (config.avatar = avatar));
 | 
				
			||||||
 | 
					                setShowEmojiPicker(false);
 | 
				
			||||||
 | 
					              }}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          open={showEmojiPicker}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <div
 | 
				
			||||||
 | 
					            className={styles.avatar}
 | 
				
			||||||
 | 
					            onClick={() => {
 | 
				
			||||||
 | 
					              setShowEmojiPicker(!showEmojiPicker);
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <Avatar avatar={config.avatar} />
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </Popover>
 | 
				
			||||||
 | 
					      </ListItem>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <ListItem
 | 
				
			||||||
 | 
					        title={Locale.Settings.Update.Version(currentVersion ?? "unknown")}
 | 
				
			||||||
 | 
					        subTitle={
 | 
				
			||||||
 | 
					          checkingUpdate
 | 
				
			||||||
 | 
					            ? Locale.Settings.Update.IsChecking
 | 
				
			||||||
 | 
					            : hasNewVersion
 | 
				
			||||||
 | 
					            ? Locale.Settings.Update.FoundUpdate(remoteId ?? "ERROR")
 | 
				
			||||||
 | 
					            : Locale.Settings.Update.IsLatest
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        {checkingUpdate ? (
 | 
				
			||||||
 | 
					          <LoadingIcon />
 | 
				
			||||||
 | 
					        ) : hasNewVersion ? (
 | 
				
			||||||
 | 
					          <Link href={updateUrl} target="_blank" className="link">
 | 
				
			||||||
 | 
					            {Locale.Settings.Update.GoToUpdate}
 | 
				
			||||||
 | 
					          </Link>
 | 
				
			||||||
 | 
					        ) : (
 | 
				
			||||||
 | 
					          <IconButton
 | 
				
			||||||
 | 
					            icon={<ResetIcon />}
 | 
				
			||||||
 | 
					            text={Locale.Settings.Update.CheckUpdate}
 | 
				
			||||||
 | 
					            onClick={() => checkUpdate(true)}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					      </ListItem>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <ListItem title={Locale.Settings.SendKey}>
 | 
				
			||||||
 | 
					        <Select
 | 
				
			||||||
 | 
					          value={config.submitKey}
 | 
				
			||||||
 | 
					          options={Object.values(SubmitKey).map((v) => ({
 | 
				
			||||||
 | 
					            value: v,
 | 
				
			||||||
 | 
					            label: v,
 | 
				
			||||||
 | 
					          }))}
 | 
				
			||||||
 | 
					          onSelect={(v) => {
 | 
				
			||||||
 | 
					            updateConfig((config) => (config.submitKey = v));
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </ListItem>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <ListItem title={Locale.Settings.Theme}>
 | 
				
			||||||
 | 
					        <Select
 | 
				
			||||||
 | 
					          value={config.theme}
 | 
				
			||||||
 | 
					          options={Object.entries(ThemeConfig).map(([k, t]) => ({
 | 
				
			||||||
 | 
					            value: k as Theme,
 | 
				
			||||||
 | 
					            label: t.title,
 | 
				
			||||||
 | 
					            icon: <t.icon />,
 | 
				
			||||||
 | 
					          }))}
 | 
				
			||||||
 | 
					          onSelect={(e) => {
 | 
				
			||||||
 | 
					            updateConfig((config) => (config.theme = e));
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </ListItem>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <ListItem title={Locale.Settings.Lang.Name}>
 | 
				
			||||||
 | 
					        <Select
 | 
				
			||||||
 | 
					          value={getLang()}
 | 
				
			||||||
 | 
					          options={AllLangs.map((lang) => ({
 | 
				
			||||||
 | 
					            value: lang,
 | 
				
			||||||
 | 
					            label: ALL_LANG_OPTIONS[lang],
 | 
				
			||||||
 | 
					          }))}
 | 
				
			||||||
 | 
					          onSelect={(e) => {
 | 
				
			||||||
 | 
					            changeLang(e);
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </ListItem>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <ListItem
 | 
				
			||||||
 | 
					        title={Locale.Settings.FontSize.Title}
 | 
				
			||||||
 | 
					        subTitle={Locale.Settings.FontSize.SubTitle}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <SlideRange
 | 
				
			||||||
 | 
					          value={config.fontSize}
 | 
				
			||||||
 | 
					          range={{
 | 
				
			||||||
 | 
					            start: 12,
 | 
				
			||||||
 | 
					            stroke: 28,
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					          step={1}
 | 
				
			||||||
 | 
					          onSlide={(e) => updateConfig((config) => (config.fontSize = e))}
 | 
				
			||||||
 | 
					        ></SlideRange>
 | 
				
			||||||
 | 
					      </ListItem>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <ListItem
 | 
				
			||||||
 | 
					        title={Locale.Settings.AutoGenerateTitle.Title}
 | 
				
			||||||
 | 
					        subTitle={Locale.Settings.AutoGenerateTitle.SubTitle}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <Switch
 | 
				
			||||||
 | 
					          value={config.enableAutoGenerateTitle}
 | 
				
			||||||
 | 
					          onChange={(e) =>
 | 
				
			||||||
 | 
					            updateConfig((config) => (config.enableAutoGenerateTitle = e))
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </ListItem>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <ListItem
 | 
				
			||||||
 | 
					        title={Locale.Settings.SendPreviewBubble.Title}
 | 
				
			||||||
 | 
					        subTitle={Locale.Settings.SendPreviewBubble.SubTitle}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <Switch
 | 
				
			||||||
 | 
					          value={config.sendPreviewBubble}
 | 
				
			||||||
 | 
					          onChange={(e) =>
 | 
				
			||||||
 | 
					            updateConfig((config) => (config.sendPreviewBubble = e))
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </ListItem>
 | 
				
			||||||
 | 
					    </List>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										153
									
								
								app/containers/Settings/components/DangerItems.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,153 @@
 | 
				
			|||||||
 | 
					import { IconButton } from "@/app/components/button";
 | 
				
			||||||
 | 
					import { showConfirm } from "@/app/components/ui-lib";
 | 
				
			||||||
 | 
					import { useChatStore } from "@/app/store/chat";
 | 
				
			||||||
 | 
					import { useAppConfig } from "@/app/store/config";
 | 
				
			||||||
 | 
					import Locale from "@/app/locales";
 | 
				
			||||||
 | 
					import { useAccessStore } from "@/app/store/access";
 | 
				
			||||||
 | 
					import { useEffect, useMemo, useState } from "react";
 | 
				
			||||||
 | 
					import { getClientConfig } from "@/app/config/client";
 | 
				
			||||||
 | 
					import { OPENAI_BASE_URL, ServiceProvider } from "@/app/constant";
 | 
				
			||||||
 | 
					import { useUpdateStore } from "@/app/store/update";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import ResetIcon from "@/app/icons/reload.svg";
 | 
				
			||||||
 | 
					import List, { ListItem } from "@/app/components/List";
 | 
				
			||||||
 | 
					import Input from "@/app/components/Input";
 | 
				
			||||||
 | 
					import Btn from "@/app/components/Btn";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function DangerItems() {
 | 
				
			||||||
 | 
					  const chatStore = useChatStore();
 | 
				
			||||||
 | 
					  const appConfig = useAppConfig();
 | 
				
			||||||
 | 
					  const accessStore = useAccessStore();
 | 
				
			||||||
 | 
					  const updateStore = useUpdateStore();
 | 
				
			||||||
 | 
					  const { isMobileScreen } = appConfig;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const enabledAccessControl = useMemo(
 | 
				
			||||||
 | 
					    () => accessStore.enabledAccessControl(),
 | 
				
			||||||
 | 
					    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
				
			||||||
 | 
					    [],
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const clientConfig = useMemo(() => getClientConfig(), []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const showAccessCode = enabledAccessControl && !clientConfig?.isApp;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const shouldHideBalanceQuery = useMemo(() => {
 | 
				
			||||||
 | 
					    const isOpenAiUrl = accessStore.openaiUrl.includes(OPENAI_BASE_URL);
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      accessStore.hideBalanceQuery ||
 | 
				
			||||||
 | 
					      isOpenAiUrl ||
 | 
				
			||||||
 | 
					      accessStore.provider === ServiceProvider.Azure
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }, [
 | 
				
			||||||
 | 
					    accessStore.hideBalanceQuery,
 | 
				
			||||||
 | 
					    accessStore.openaiUrl,
 | 
				
			||||||
 | 
					    accessStore.provider,
 | 
				
			||||||
 | 
					  ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [loadingUsage, setLoadingUsage] = useState(false);
 | 
				
			||||||
 | 
					  const usage = {
 | 
				
			||||||
 | 
					    used: updateStore.used,
 | 
				
			||||||
 | 
					    subscription: updateStore.subscription,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function checkUsage(force = false) {
 | 
				
			||||||
 | 
					    if (shouldHideBalanceQuery) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    setLoadingUsage(true);
 | 
				
			||||||
 | 
					    updateStore.updateUsage(force).finally(() => {
 | 
				
			||||||
 | 
					      setLoadingUsage(false);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const showUsage = accessStore.isAuthorized();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    showUsage && checkUsage();
 | 
				
			||||||
 | 
					    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <List
 | 
				
			||||||
 | 
					      widgetStyle={{
 | 
				
			||||||
 | 
					        selectClassName: "min-w-select-mobile md:min-w-select",
 | 
				
			||||||
 | 
					        inputClassName: "md:min-w-select",
 | 
				
			||||||
 | 
					        rangeClassName: "md:min-w-select",
 | 
				
			||||||
 | 
					        rangeNextLine: isMobileScreen,
 | 
				
			||||||
 | 
					        inputNextLine: isMobileScreen,
 | 
				
			||||||
 | 
					      }}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      {showAccessCode && (
 | 
				
			||||||
 | 
					        <ListItem
 | 
				
			||||||
 | 
					          title={Locale.Settings.Access.AccessCode.Title}
 | 
				
			||||||
 | 
					          subTitle={Locale.Settings.Access.AccessCode.SubTitle}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <Input
 | 
				
			||||||
 | 
					            value={accessStore.accessCode}
 | 
				
			||||||
 | 
					            type="password"
 | 
				
			||||||
 | 
					            placeholder={Locale.Settings.Access.AccessCode.Placeholder}
 | 
				
			||||||
 | 
					            onChange={(e) => {
 | 
				
			||||||
 | 
					              accessStore.update((access) => (access.accessCode = e));
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </ListItem>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      {!shouldHideBalanceQuery && !clientConfig?.isApp ? (
 | 
				
			||||||
 | 
					        <ListItem
 | 
				
			||||||
 | 
					          title={Locale.Settings.Usage.Title}
 | 
				
			||||||
 | 
					          subTitle={
 | 
				
			||||||
 | 
					            showUsage
 | 
				
			||||||
 | 
					              ? loadingUsage
 | 
				
			||||||
 | 
					                ? Locale.Settings.Usage.IsChecking
 | 
				
			||||||
 | 
					                : Locale.Settings.Usage.SubTitle(
 | 
				
			||||||
 | 
					                    usage?.used ?? "[?]",
 | 
				
			||||||
 | 
					                    usage?.subscription ?? "[?]",
 | 
				
			||||||
 | 
					                  )
 | 
				
			||||||
 | 
					              : Locale.Settings.Usage.NoAccess
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          {!showUsage || loadingUsage ? (
 | 
				
			||||||
 | 
					            <div />
 | 
				
			||||||
 | 
					          ) : (
 | 
				
			||||||
 | 
					            <IconButton
 | 
				
			||||||
 | 
					              icon={<ResetIcon />}
 | 
				
			||||||
 | 
					              text={Locale.Settings.Usage.Check}
 | 
				
			||||||
 | 
					              onClick={() => checkUsage(true)}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					          )}
 | 
				
			||||||
 | 
					        </ListItem>
 | 
				
			||||||
 | 
					      ) : null}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <ListItem
 | 
				
			||||||
 | 
					        title={Locale.Settings.Danger.Reset.Title}
 | 
				
			||||||
 | 
					        subTitle={Locale.Settings.Danger.Reset.SubTitle}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <Btn
 | 
				
			||||||
 | 
					          text={Locale.Settings.Danger.Reset.Action}
 | 
				
			||||||
 | 
					          onClick={async () => {
 | 
				
			||||||
 | 
					            if (await showConfirm(Locale.Settings.Danger.Reset.Confirm)) {
 | 
				
			||||||
 | 
					              appConfig.reset();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					          type="danger"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </ListItem>
 | 
				
			||||||
 | 
					      <ListItem
 | 
				
			||||||
 | 
					        title={Locale.Settings.Danger.Clear.Title}
 | 
				
			||||||
 | 
					        subTitle={Locale.Settings.Danger.Clear.SubTitle}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <Btn
 | 
				
			||||||
 | 
					          text={Locale.Settings.Danger.Clear.Action}
 | 
				
			||||||
 | 
					          onClick={async () => {
 | 
				
			||||||
 | 
					            if (await showConfirm(Locale.Settings.Danger.Clear.Confirm)) {
 | 
				
			||||||
 | 
					              chatStore.clearAllData();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					          type="danger"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </ListItem>
 | 
				
			||||||
 | 
					    </List>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										162
									
								
								app/containers/Settings/components/MaskConfig.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,162 @@
 | 
				
			|||||||
 | 
					import { useState } from "react";
 | 
				
			||||||
 | 
					import List, { ListItem } from "@/app/components/List";
 | 
				
			||||||
 | 
					import { ContextPrompts, MaskAvatar } from "@/app/components/mask";
 | 
				
			||||||
 | 
					import { Path } from "@/app/constant";
 | 
				
			||||||
 | 
					import { ModelConfig, useAppConfig } from "@/app/store/config";
 | 
				
			||||||
 | 
					import { Mask } from "@/app/store/mask";
 | 
				
			||||||
 | 
					import { Updater } from "@/app/typing";
 | 
				
			||||||
 | 
					import { copyToClipboard } from "@/app/utils";
 | 
				
			||||||
 | 
					import Locale from "@/app/locales";
 | 
				
			||||||
 | 
					import { Popover, showConfirm } from "@/app/components/ui-lib";
 | 
				
			||||||
 | 
					import { AvatarPicker } from "@/app/components/emoji";
 | 
				
			||||||
 | 
					import ModelSetting from "@/app/containers/Settings/components/ModelSetting";
 | 
				
			||||||
 | 
					import { IconButton } from "@/app/components/button";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import CopyIcon from "@/app/icons/copy.svg";
 | 
				
			||||||
 | 
					import Switch from "@/app/components/Switch";
 | 
				
			||||||
 | 
					import Input from "@/app/components/Input";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function MaskConfig(props: {
 | 
				
			||||||
 | 
					  mask: Mask;
 | 
				
			||||||
 | 
					  updateMask: Updater<Mask>;
 | 
				
			||||||
 | 
					  extraListItems?: JSX.Element;
 | 
				
			||||||
 | 
					  readonly?: boolean;
 | 
				
			||||||
 | 
					  shouldSyncFromGlobal?: boolean;
 | 
				
			||||||
 | 
					}) {
 | 
				
			||||||
 | 
					  const [showPicker, setShowPicker] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const updateConfig = (updater: (config: ModelConfig) => void) => {
 | 
				
			||||||
 | 
					    if (props.readonly) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const config = { ...props.mask.modelConfig };
 | 
				
			||||||
 | 
					    updater(config);
 | 
				
			||||||
 | 
					    props.updateMask((mask) => {
 | 
				
			||||||
 | 
					      mask.modelConfig = config;
 | 
				
			||||||
 | 
					      // if user changed current session mask, it will disable auto sync
 | 
				
			||||||
 | 
					      mask.syncGlobalConfig = false;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const copyMaskLink = () => {
 | 
				
			||||||
 | 
					    const maskLink = `${location.protocol}//${location.host}/#${Path.NewChat}?mask=${props.mask.id}`;
 | 
				
			||||||
 | 
					    copyToClipboard(maskLink);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const globalConfig = useAppConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const { isMobileScreen } = globalConfig;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <>
 | 
				
			||||||
 | 
					      <ContextPrompts
 | 
				
			||||||
 | 
					        context={props.mask.context}
 | 
				
			||||||
 | 
					        updateContext={(updater) => {
 | 
				
			||||||
 | 
					          const context = props.mask.context.slice();
 | 
				
			||||||
 | 
					          updater(context);
 | 
				
			||||||
 | 
					          props.updateMask((mask) => (mask.context = context));
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <List
 | 
				
			||||||
 | 
					        widgetStyle={{
 | 
				
			||||||
 | 
					          rangeNextLine: isMobileScreen,
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <ListItem title={Locale.Mask.Config.Avatar}>
 | 
				
			||||||
 | 
					          <Popover
 | 
				
			||||||
 | 
					            content={
 | 
				
			||||||
 | 
					              <AvatarPicker
 | 
				
			||||||
 | 
					                onEmojiClick={(emoji) => {
 | 
				
			||||||
 | 
					                  props.updateMask((mask) => (mask.avatar = emoji));
 | 
				
			||||||
 | 
					                  setShowPicker(false);
 | 
				
			||||||
 | 
					                }}
 | 
				
			||||||
 | 
					              ></AvatarPicker>
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            open={showPicker}
 | 
				
			||||||
 | 
					            onClose={() => setShowPicker(false)}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <div
 | 
				
			||||||
 | 
					              onClick={() => setShowPicker(true)}
 | 
				
			||||||
 | 
					              style={{ cursor: "pointer" }}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              <MaskAvatar
 | 
				
			||||||
 | 
					                avatar={props.mask.avatar}
 | 
				
			||||||
 | 
					                model={props.mask.modelConfig.model}
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </Popover>
 | 
				
			||||||
 | 
					        </ListItem>
 | 
				
			||||||
 | 
					        <ListItem title={Locale.Mask.Config.Name}>
 | 
				
			||||||
 | 
					          <Input
 | 
				
			||||||
 | 
					            type="text"
 | 
				
			||||||
 | 
					            value={props.mask.name}
 | 
				
			||||||
 | 
					            onChange={(e) =>
 | 
				
			||||||
 | 
					              props.updateMask((mask) => {
 | 
				
			||||||
 | 
					                mask.name = e;
 | 
				
			||||||
 | 
					              })
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          ></Input>
 | 
				
			||||||
 | 
					        </ListItem>
 | 
				
			||||||
 | 
					        <ListItem
 | 
				
			||||||
 | 
					          title={Locale.Mask.Config.HideContext.Title}
 | 
				
			||||||
 | 
					          subTitle={Locale.Mask.Config.HideContext.SubTitle}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <Switch
 | 
				
			||||||
 | 
					            value={!!props.mask.hideContext}
 | 
				
			||||||
 | 
					            onChange={(e) => {
 | 
				
			||||||
 | 
					              props.updateMask((mask) => {
 | 
				
			||||||
 | 
					                mask.hideContext = e;
 | 
				
			||||||
 | 
					              });
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					          ></Switch>
 | 
				
			||||||
 | 
					        </ListItem>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        {!props.shouldSyncFromGlobal ? (
 | 
				
			||||||
 | 
					          <ListItem
 | 
				
			||||||
 | 
					            title={Locale.Mask.Config.Share.Title}
 | 
				
			||||||
 | 
					            subTitle={Locale.Mask.Config.Share.SubTitle}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <IconButton
 | 
				
			||||||
 | 
					              icon={<CopyIcon />}
 | 
				
			||||||
 | 
					              text={Locale.Mask.Config.Share.Action}
 | 
				
			||||||
 | 
					              onClick={copyMaskLink}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					          </ListItem>
 | 
				
			||||||
 | 
					        ) : null}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        {props.shouldSyncFromGlobal ? (
 | 
				
			||||||
 | 
					          <ListItem
 | 
				
			||||||
 | 
					            title={Locale.Mask.Config.Sync.Title}
 | 
				
			||||||
 | 
					            subTitle={Locale.Mask.Config.Sync.SubTitle}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <Switch
 | 
				
			||||||
 | 
					              value={!!props.mask.syncGlobalConfig}
 | 
				
			||||||
 | 
					              onChange={async (e) => {
 | 
				
			||||||
 | 
					                const checked = e;
 | 
				
			||||||
 | 
					                if (
 | 
				
			||||||
 | 
					                  checked &&
 | 
				
			||||||
 | 
					                  (await showConfirm(Locale.Mask.Config.Sync.Confirm))
 | 
				
			||||||
 | 
					                ) {
 | 
				
			||||||
 | 
					                  props.updateMask((mask) => {
 | 
				
			||||||
 | 
					                    mask.syncGlobalConfig = checked;
 | 
				
			||||||
 | 
					                    mask.modelConfig = { ...globalConfig.modelConfig };
 | 
				
			||||||
 | 
					                  });
 | 
				
			||||||
 | 
					                } else if (!checked) {
 | 
				
			||||||
 | 
					                  props.updateMask((mask) => {
 | 
				
			||||||
 | 
					                    mask.syncGlobalConfig = checked;
 | 
				
			||||||
 | 
					                  });
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					              }}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					          </ListItem>
 | 
				
			||||||
 | 
					        ) : null}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <ModelSetting
 | 
				
			||||||
 | 
					          modelConfig={{ ...props.mask.modelConfig }}
 | 
				
			||||||
 | 
					          updateConfig={updateConfig}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					        {props.extraListItems}
 | 
				
			||||||
 | 
					      </List>
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										39
									
								
								app/containers/Settings/components/MaskSetting.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,39 @@
 | 
				
			|||||||
 | 
					import List, { ListItem } from "@/app/components/List";
 | 
				
			||||||
 | 
					import Switch from "@/app/components/Switch";
 | 
				
			||||||
 | 
					import Locale from "@/app/locales";
 | 
				
			||||||
 | 
					import { useAppConfig } from "@/app/store/config";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface MaskSettingProps {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function MaskSetting(props: MaskSettingProps) {
 | 
				
			||||||
 | 
					  const config = useAppConfig();
 | 
				
			||||||
 | 
					  const updateConfig = config.update;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <List>
 | 
				
			||||||
 | 
					      <ListItem
 | 
				
			||||||
 | 
					        title={Locale.Settings.Mask.Splash.Title}
 | 
				
			||||||
 | 
					        subTitle={Locale.Settings.Mask.Splash.SubTitle}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <Switch
 | 
				
			||||||
 | 
					          value={!config.dontShowMaskSplashScreen}
 | 
				
			||||||
 | 
					          onChange={(e) =>
 | 
				
			||||||
 | 
					            updateConfig((config) => (config.dontShowMaskSplashScreen = !e))
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </ListItem>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <ListItem
 | 
				
			||||||
 | 
					        title={Locale.Settings.Mask.Builtin.Title}
 | 
				
			||||||
 | 
					        subTitle={Locale.Settings.Mask.Builtin.SubTitle}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <Switch
 | 
				
			||||||
 | 
					          value={config.hideBuiltinMasks}
 | 
				
			||||||
 | 
					          onChange={(e) =>
 | 
				
			||||||
 | 
					            updateConfig((config) => (config.hideBuiltinMasks = e))
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </ListItem>
 | 
				
			||||||
 | 
					    </List>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										220
									
								
								app/containers/Settings/components/ModelSetting.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,220 @@
 | 
				
			|||||||
 | 
					import { ListItem } from "@/app/components/List";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  ModalConfigValidator,
 | 
				
			||||||
 | 
					  ModelConfig,
 | 
				
			||||||
 | 
					  useAppConfig,
 | 
				
			||||||
 | 
					} from "@/app/store/config";
 | 
				
			||||||
 | 
					import { useAllModels } from "@/app/utils/hooks";
 | 
				
			||||||
 | 
					import Locale from "@/app/locales";
 | 
				
			||||||
 | 
					import Select from "@/app/components/Select";
 | 
				
			||||||
 | 
					import SlideRange from "@/app/components/SlideRange";
 | 
				
			||||||
 | 
					import Switch from "@/app/components/Switch";
 | 
				
			||||||
 | 
					import Input from "@/app/components/Input";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function ModelSetting(props: {
 | 
				
			||||||
 | 
					  modelConfig: ModelConfig;
 | 
				
			||||||
 | 
					  updateConfig: (updater: (config: ModelConfig) => void) => void;
 | 
				
			||||||
 | 
					}) {
 | 
				
			||||||
 | 
					  const allModels = useAllModels();
 | 
				
			||||||
 | 
					  const { isMobileScreen } = useAppConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <>
 | 
				
			||||||
 | 
					      <ListItem title={Locale.Settings.Model}>
 | 
				
			||||||
 | 
					        <Select
 | 
				
			||||||
 | 
					          value={props.modelConfig.model}
 | 
				
			||||||
 | 
					          options={allModels
 | 
				
			||||||
 | 
					            .filter((v) => v.available)
 | 
				
			||||||
 | 
					            .map((v) => ({
 | 
				
			||||||
 | 
					              value: v.name,
 | 
				
			||||||
 | 
					              label: `${v.displayName}(${v.provider?.providerName})`,
 | 
				
			||||||
 | 
					            }))}
 | 
				
			||||||
 | 
					          onSelect={(e) => {
 | 
				
			||||||
 | 
					            props.updateConfig(
 | 
				
			||||||
 | 
					              (config) => (config.model = ModalConfigValidator.model(e)),
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </ListItem>
 | 
				
			||||||
 | 
					      <ListItem
 | 
				
			||||||
 | 
					        title={Locale.Settings.Temperature.Title}
 | 
				
			||||||
 | 
					        subTitle={Locale.Settings.Temperature.SubTitle}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <SlideRange
 | 
				
			||||||
 | 
					          value={props.modelConfig.temperature}
 | 
				
			||||||
 | 
					          range={{
 | 
				
			||||||
 | 
					            start: 0,
 | 
				
			||||||
 | 
					            stroke: 1,
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					          step={0.1}
 | 
				
			||||||
 | 
					          onSlide={(e) => {
 | 
				
			||||||
 | 
					            props.updateConfig(
 | 
				
			||||||
 | 
					              (config) =>
 | 
				
			||||||
 | 
					                (config.temperature = ModalConfigValidator.temperature(e)),
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					        ></SlideRange>
 | 
				
			||||||
 | 
					      </ListItem>
 | 
				
			||||||
 | 
					      <ListItem
 | 
				
			||||||
 | 
					        title={Locale.Settings.TopP.Title}
 | 
				
			||||||
 | 
					        subTitle={Locale.Settings.TopP.SubTitle}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <SlideRange
 | 
				
			||||||
 | 
					          value={props.modelConfig.top_p ?? 1}
 | 
				
			||||||
 | 
					          range={{
 | 
				
			||||||
 | 
					            start: 0,
 | 
				
			||||||
 | 
					            stroke: 1,
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					          step={0.1}
 | 
				
			||||||
 | 
					          onSlide={(e) => {
 | 
				
			||||||
 | 
					            props.updateConfig(
 | 
				
			||||||
 | 
					              (config) => (config.top_p = ModalConfigValidator.top_p(e)),
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					        ></SlideRange>
 | 
				
			||||||
 | 
					      </ListItem>
 | 
				
			||||||
 | 
					      <ListItem
 | 
				
			||||||
 | 
					        title={Locale.Settings.MaxTokens.Title}
 | 
				
			||||||
 | 
					        subTitle={Locale.Settings.MaxTokens.SubTitle}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <Input
 | 
				
			||||||
 | 
					          type="number"
 | 
				
			||||||
 | 
					          min={1024}
 | 
				
			||||||
 | 
					          max={512000}
 | 
				
			||||||
 | 
					          value={props.modelConfig.max_tokens}
 | 
				
			||||||
 | 
					          onChange={(e) =>
 | 
				
			||||||
 | 
					            props.updateConfig(
 | 
				
			||||||
 | 
					              (config) =>
 | 
				
			||||||
 | 
					                (config.max_tokens = ModalConfigValidator.max_tokens(e)),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        ></Input>
 | 
				
			||||||
 | 
					      </ListItem>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      {props.modelConfig.model.startsWith("gemini") ? null : (
 | 
				
			||||||
 | 
					        <>
 | 
				
			||||||
 | 
					          <ListItem
 | 
				
			||||||
 | 
					            title={Locale.Settings.PresencePenalty.Title}
 | 
				
			||||||
 | 
					            subTitle={Locale.Settings.PresencePenalty.SubTitle}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <SlideRange
 | 
				
			||||||
 | 
					              value={props.modelConfig.presence_penalty}
 | 
				
			||||||
 | 
					              range={{
 | 
				
			||||||
 | 
					                start: -2,
 | 
				
			||||||
 | 
					                stroke: 4,
 | 
				
			||||||
 | 
					              }}
 | 
				
			||||||
 | 
					              step={0.1}
 | 
				
			||||||
 | 
					              onSlide={(e) => {
 | 
				
			||||||
 | 
					                props.updateConfig(
 | 
				
			||||||
 | 
					                  (config) =>
 | 
				
			||||||
 | 
					                    (config.presence_penalty =
 | 
				
			||||||
 | 
					                      ModalConfigValidator.presence_penalty(e)),
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					              }}
 | 
				
			||||||
 | 
					            ></SlideRange>
 | 
				
			||||||
 | 
					          </ListItem>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <ListItem
 | 
				
			||||||
 | 
					            title={Locale.Settings.FrequencyPenalty.Title}
 | 
				
			||||||
 | 
					            subTitle={Locale.Settings.FrequencyPenalty.SubTitle}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <SlideRange
 | 
				
			||||||
 | 
					              value={props.modelConfig.frequency_penalty}
 | 
				
			||||||
 | 
					              range={{
 | 
				
			||||||
 | 
					                start: -2,
 | 
				
			||||||
 | 
					                stroke: 4,
 | 
				
			||||||
 | 
					              }}
 | 
				
			||||||
 | 
					              step={0.1}
 | 
				
			||||||
 | 
					              onSlide={(e) => {
 | 
				
			||||||
 | 
					                props.updateConfig(
 | 
				
			||||||
 | 
					                  (config) =>
 | 
				
			||||||
 | 
					                    (config.frequency_penalty =
 | 
				
			||||||
 | 
					                      ModalConfigValidator.frequency_penalty(e)),
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					              }}
 | 
				
			||||||
 | 
					            ></SlideRange>
 | 
				
			||||||
 | 
					          </ListItem>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <ListItem
 | 
				
			||||||
 | 
					            title={Locale.Settings.InjectSystemPrompts.Title}
 | 
				
			||||||
 | 
					            subTitle={Locale.Settings.InjectSystemPrompts.SubTitle}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <Switch
 | 
				
			||||||
 | 
					              value={props.modelConfig.enableInjectSystemPrompts}
 | 
				
			||||||
 | 
					              onChange={(e) =>
 | 
				
			||||||
 | 
					                props.updateConfig(
 | 
				
			||||||
 | 
					                  (config) => (config.enableInjectSystemPrompts = e),
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					          </ListItem>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <ListItem
 | 
				
			||||||
 | 
					            title={Locale.Settings.InputTemplate.Title}
 | 
				
			||||||
 | 
					            subTitle={Locale.Settings.InputTemplate.SubTitle}
 | 
				
			||||||
 | 
					            nextline={isMobileScreen}
 | 
				
			||||||
 | 
					            validator={(v: string) => {
 | 
				
			||||||
 | 
					              if (!v.includes("{{input}}")) {
 | 
				
			||||||
 | 
					                return {
 | 
				
			||||||
 | 
					                  error: true,
 | 
				
			||||||
 | 
					                  message: Locale.Settings.InputTemplate.Error,
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              return { error: false };
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <Input
 | 
				
			||||||
 | 
					              type="text"
 | 
				
			||||||
 | 
					              value={props.modelConfig.template}
 | 
				
			||||||
 | 
					              onChange={(e = "") =>
 | 
				
			||||||
 | 
					                props.updateConfig((config) => (config.template = e))
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            ></Input>
 | 
				
			||||||
 | 
					          </ListItem>
 | 
				
			||||||
 | 
					        </>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					      <ListItem
 | 
				
			||||||
 | 
					        title={Locale.Settings.HistoryCount.Title}
 | 
				
			||||||
 | 
					        subTitle={Locale.Settings.HistoryCount.SubTitle}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <SlideRange
 | 
				
			||||||
 | 
					          value={props.modelConfig.historyMessageCount}
 | 
				
			||||||
 | 
					          range={{
 | 
				
			||||||
 | 
					            start: 0,
 | 
				
			||||||
 | 
					            stroke: 64,
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					          step={1}
 | 
				
			||||||
 | 
					          onSlide={(e) => {
 | 
				
			||||||
 | 
					            props.updateConfig((config) => (config.historyMessageCount = e));
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					        ></SlideRange>
 | 
				
			||||||
 | 
					      </ListItem>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <ListItem
 | 
				
			||||||
 | 
					        title={Locale.Settings.CompressThreshold.Title}
 | 
				
			||||||
 | 
					        subTitle={Locale.Settings.CompressThreshold.SubTitle}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <Input
 | 
				
			||||||
 | 
					          type="number"
 | 
				
			||||||
 | 
					          min={500}
 | 
				
			||||||
 | 
					          max={4000}
 | 
				
			||||||
 | 
					          value={props.modelConfig.compressMessageLengthThreshold}
 | 
				
			||||||
 | 
					          onChange={(e) =>
 | 
				
			||||||
 | 
					            props.updateConfig(
 | 
				
			||||||
 | 
					              (config) => (config.compressMessageLengthThreshold = e),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        ></Input>
 | 
				
			||||||
 | 
					      </ListItem>
 | 
				
			||||||
 | 
					      <ListItem title={Locale.Memory.Title} subTitle={Locale.Memory.Send}>
 | 
				
			||||||
 | 
					        <Switch
 | 
				
			||||||
 | 
					          value={props.modelConfig.sendMemory}
 | 
				
			||||||
 | 
					          onChange={(e) =>
 | 
				
			||||||
 | 
					            props.updateConfig((config) => (config.sendMemory = e))
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </ListItem>
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										63
									
								
								app/containers/Settings/components/PromptSetting.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,63 @@
 | 
				
			|||||||
 | 
					import { useState } from "react";
 | 
				
			||||||
 | 
					import UserPromptModal from "./UserPromptModal";
 | 
				
			||||||
 | 
					import List, { ListItem } from "@/app/components/List";
 | 
				
			||||||
 | 
					import Locale from "@/app/locales";
 | 
				
			||||||
 | 
					import { useAppConfig } from "@/app/store/config";
 | 
				
			||||||
 | 
					import { SearchService, usePromptStore } from "@/app/store/prompt";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import Switch from "@/app/components/Switch";
 | 
				
			||||||
 | 
					import Btn from "@/app/components/Btn";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import EditIcon from "@/app/icons/editIcon.svg";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface PromptSettingProps {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function PromptSetting(props: PromptSettingProps) {
 | 
				
			||||||
 | 
					  const [shouldShowPromptModal, setShowPromptModal] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const config = useAppConfig();
 | 
				
			||||||
 | 
					  const updateConfig = config.update;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const builtinCount = SearchService.count.builtin;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const promptStore = usePromptStore();
 | 
				
			||||||
 | 
					  const customCount = promptStore.getUserPrompts().length ?? 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const textStyle = " !text-sm";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <>
 | 
				
			||||||
 | 
					      <List>
 | 
				
			||||||
 | 
					        <ListItem
 | 
				
			||||||
 | 
					          title={Locale.Settings.Prompt.Disable.Title}
 | 
				
			||||||
 | 
					          subTitle={Locale.Settings.Prompt.Disable.SubTitle}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <Switch
 | 
				
			||||||
 | 
					            value={config.disablePromptHint}
 | 
				
			||||||
 | 
					            onChange={(e) =>
 | 
				
			||||||
 | 
					              updateConfig((config) => (config.disablePromptHint = e))
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </ListItem>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <ListItem
 | 
				
			||||||
 | 
					          title={Locale.Settings.Prompt.List}
 | 
				
			||||||
 | 
					          subTitle={Locale.Settings.Prompt.ListCount(builtinCount, customCount)}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <div className="flex gap-3">
 | 
				
			||||||
 | 
					            <Btn
 | 
				
			||||||
 | 
					              onClick={() => setShowPromptModal(true)}
 | 
				
			||||||
 | 
					              text={
 | 
				
			||||||
 | 
					                <span className={textStyle}>{Locale.Settings.Prompt.Edit}</span>
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					              prefixIcon={config.isMobileScreen ? undefined : <EditIcon />}
 | 
				
			||||||
 | 
					            ></Btn>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </ListItem>
 | 
				
			||||||
 | 
					      </List>
 | 
				
			||||||
 | 
					      {shouldShowPromptModal && (
 | 
				
			||||||
 | 
					        <UserPromptModal onClose={() => setShowPromptModal(false)} />
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										283
									
								
								app/containers/Settings/components/ProviderSetting.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,283 @@
 | 
				
			|||||||
 | 
					import { useMemo } from "react";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  Anthropic,
 | 
				
			||||||
 | 
					  Azure,
 | 
				
			||||||
 | 
					  Google,
 | 
				
			||||||
 | 
					  OPENAI_BASE_URL,
 | 
				
			||||||
 | 
					  ServiceProvider,
 | 
				
			||||||
 | 
					  SlotID,
 | 
				
			||||||
 | 
					} from "@/app/constant";
 | 
				
			||||||
 | 
					import Locale from "@/app/locales";
 | 
				
			||||||
 | 
					import { useAccessStore } from "@/app/store/access";
 | 
				
			||||||
 | 
					import { getClientConfig } from "@/app/config/client";
 | 
				
			||||||
 | 
					import { useAppConfig } from "@/app/store/config";
 | 
				
			||||||
 | 
					import List, { ListItem } from "@/app/components/List";
 | 
				
			||||||
 | 
					import Select from "@/app/components/Select";
 | 
				
			||||||
 | 
					import Switch from "@/app/components/Switch";
 | 
				
			||||||
 | 
					import Input from "@/app/components/Input";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function ProviderSetting() {
 | 
				
			||||||
 | 
					  const accessStore = useAccessStore();
 | 
				
			||||||
 | 
					  const config = useAppConfig();
 | 
				
			||||||
 | 
					  const { isMobileScreen } = config;
 | 
				
			||||||
 | 
					  const clientConfig = useMemo(() => getClientConfig(), []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <List
 | 
				
			||||||
 | 
					      id={SlotID.CustomModel}
 | 
				
			||||||
 | 
					      widgetStyle={{
 | 
				
			||||||
 | 
					        selectClassName: "min-w-select-mobile md:min-w-select",
 | 
				
			||||||
 | 
					        inputClassName: "md:min-w-select",
 | 
				
			||||||
 | 
					        rangeClassName: "md:min-w-select",
 | 
				
			||||||
 | 
					        inputNextLine: isMobileScreen,
 | 
				
			||||||
 | 
					      }}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      {!accessStore.hideUserApiKey && (
 | 
				
			||||||
 | 
					        <>
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            // Conditionally render the following ListItem based on clientConfig.isApp
 | 
				
			||||||
 | 
					            !clientConfig?.isApp && ( // only show if isApp is false
 | 
				
			||||||
 | 
					              <ListItem
 | 
				
			||||||
 | 
					                title={Locale.Settings.Access.CustomEndpoint.Title}
 | 
				
			||||||
 | 
					                subTitle={Locale.Settings.Access.CustomEndpoint.SubTitle}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <Switch
 | 
				
			||||||
 | 
					                  value={accessStore.useCustomConfig}
 | 
				
			||||||
 | 
					                  onChange={(e) =>
 | 
				
			||||||
 | 
					                    accessStore.update((access) => (access.useCustomConfig = e))
 | 
				
			||||||
 | 
					                  }
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					              </ListItem>
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          {accessStore.useCustomConfig && (
 | 
				
			||||||
 | 
					            <>
 | 
				
			||||||
 | 
					              <ListItem
 | 
				
			||||||
 | 
					                title={Locale.Settings.Access.Provider.Title}
 | 
				
			||||||
 | 
					                subTitle={Locale.Settings.Access.Provider.SubTitle}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <Select
 | 
				
			||||||
 | 
					                  value={accessStore.provider}
 | 
				
			||||||
 | 
					                  onSelect={(e) => {
 | 
				
			||||||
 | 
					                    accessStore.update((access) => (access.provider = e));
 | 
				
			||||||
 | 
					                  }}
 | 
				
			||||||
 | 
					                  options={Object.entries(ServiceProvider).map(([k, v]) => ({
 | 
				
			||||||
 | 
					                    value: v,
 | 
				
			||||||
 | 
					                    label: k,
 | 
				
			||||||
 | 
					                  }))}
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					              </ListItem>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              {accessStore.provider === ServiceProvider.OpenAI && (
 | 
				
			||||||
 | 
					                <>
 | 
				
			||||||
 | 
					                  <ListItem
 | 
				
			||||||
 | 
					                    title={Locale.Settings.Access.OpenAI.Endpoint.Title}
 | 
				
			||||||
 | 
					                    subTitle={Locale.Settings.Access.OpenAI.Endpoint.SubTitle}
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
 | 
					                    <Input
 | 
				
			||||||
 | 
					                      type="text"
 | 
				
			||||||
 | 
					                      value={accessStore.openaiUrl}
 | 
				
			||||||
 | 
					                      placeholder={OPENAI_BASE_URL}
 | 
				
			||||||
 | 
					                      onChange={(e = "") =>
 | 
				
			||||||
 | 
					                        accessStore.update((access) => (access.openaiUrl = e))
 | 
				
			||||||
 | 
					                      }
 | 
				
			||||||
 | 
					                    ></Input>
 | 
				
			||||||
 | 
					                  </ListItem>
 | 
				
			||||||
 | 
					                  <ListItem
 | 
				
			||||||
 | 
					                    title={Locale.Settings.Access.OpenAI.ApiKey.Title}
 | 
				
			||||||
 | 
					                    subTitle={Locale.Settings.Access.OpenAI.ApiKey.SubTitle}
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
 | 
					                    <Input
 | 
				
			||||||
 | 
					                      value={accessStore.openaiApiKey}
 | 
				
			||||||
 | 
					                      type="password"
 | 
				
			||||||
 | 
					                      placeholder={
 | 
				
			||||||
 | 
					                        Locale.Settings.Access.OpenAI.ApiKey.Placeholder
 | 
				
			||||||
 | 
					                      }
 | 
				
			||||||
 | 
					                      onChange={(e) => {
 | 
				
			||||||
 | 
					                        accessStore.update(
 | 
				
			||||||
 | 
					                          (access) => (access.openaiApiKey = e),
 | 
				
			||||||
 | 
					                        );
 | 
				
			||||||
 | 
					                      }}
 | 
				
			||||||
 | 
					                    />
 | 
				
			||||||
 | 
					                  </ListItem>
 | 
				
			||||||
 | 
					                </>
 | 
				
			||||||
 | 
					              )}
 | 
				
			||||||
 | 
					              {accessStore.provider === ServiceProvider.Azure && (
 | 
				
			||||||
 | 
					                <>
 | 
				
			||||||
 | 
					                  <ListItem
 | 
				
			||||||
 | 
					                    title={Locale.Settings.Access.Azure.Endpoint.Title}
 | 
				
			||||||
 | 
					                    subTitle={
 | 
				
			||||||
 | 
					                      Locale.Settings.Access.Azure.Endpoint.SubTitle +
 | 
				
			||||||
 | 
					                      Azure.ExampleEndpoint
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
 | 
					                    <Input
 | 
				
			||||||
 | 
					                      type="text"
 | 
				
			||||||
 | 
					                      value={accessStore.azureUrl}
 | 
				
			||||||
 | 
					                      placeholder={Azure.ExampleEndpoint}
 | 
				
			||||||
 | 
					                      onChange={(e) =>
 | 
				
			||||||
 | 
					                        accessStore.update((access) => (access.azureUrl = e))
 | 
				
			||||||
 | 
					                      }
 | 
				
			||||||
 | 
					                    ></Input>
 | 
				
			||||||
 | 
					                  </ListItem>
 | 
				
			||||||
 | 
					                  <ListItem
 | 
				
			||||||
 | 
					                    title={Locale.Settings.Access.Azure.ApiKey.Title}
 | 
				
			||||||
 | 
					                    subTitle={Locale.Settings.Access.Azure.ApiKey.SubTitle}
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
 | 
					                    <Input
 | 
				
			||||||
 | 
					                      value={accessStore.azureApiKey}
 | 
				
			||||||
 | 
					                      type="password"
 | 
				
			||||||
 | 
					                      placeholder={
 | 
				
			||||||
 | 
					                        Locale.Settings.Access.Azure.ApiKey.Placeholder
 | 
				
			||||||
 | 
					                      }
 | 
				
			||||||
 | 
					                      onChange={(e) => {
 | 
				
			||||||
 | 
					                        accessStore.update(
 | 
				
			||||||
 | 
					                          (access) => (access.azureApiKey = e),
 | 
				
			||||||
 | 
					                        );
 | 
				
			||||||
 | 
					                      }}
 | 
				
			||||||
 | 
					                    />
 | 
				
			||||||
 | 
					                  </ListItem>
 | 
				
			||||||
 | 
					                  <ListItem
 | 
				
			||||||
 | 
					                    title={Locale.Settings.Access.Azure.ApiVerion.Title}
 | 
				
			||||||
 | 
					                    subTitle={Locale.Settings.Access.Azure.ApiVerion.SubTitle}
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
 | 
					                    <Input
 | 
				
			||||||
 | 
					                      type="text"
 | 
				
			||||||
 | 
					                      value={accessStore.azureApiVersion}
 | 
				
			||||||
 | 
					                      placeholder="2023-08-01-preview"
 | 
				
			||||||
 | 
					                      onChange={(e) =>
 | 
				
			||||||
 | 
					                        accessStore.update(
 | 
				
			||||||
 | 
					                          (access) => (access.azureApiVersion = e),
 | 
				
			||||||
 | 
					                        )
 | 
				
			||||||
 | 
					                      }
 | 
				
			||||||
 | 
					                    ></Input>
 | 
				
			||||||
 | 
					                  </ListItem>
 | 
				
			||||||
 | 
					                </>
 | 
				
			||||||
 | 
					              )}
 | 
				
			||||||
 | 
					              {accessStore.provider === ServiceProvider.Google && (
 | 
				
			||||||
 | 
					                <>
 | 
				
			||||||
 | 
					                  <ListItem
 | 
				
			||||||
 | 
					                    title={Locale.Settings.Access.Google.Endpoint.Title}
 | 
				
			||||||
 | 
					                    subTitle={
 | 
				
			||||||
 | 
					                      Locale.Settings.Access.Google.Endpoint.SubTitle +
 | 
				
			||||||
 | 
					                      Google.ExampleEndpoint
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
 | 
					                    <Input
 | 
				
			||||||
 | 
					                      type="text"
 | 
				
			||||||
 | 
					                      value={accessStore.googleUrl}
 | 
				
			||||||
 | 
					                      placeholder={Google.ExampleEndpoint}
 | 
				
			||||||
 | 
					                      onChange={(e) =>
 | 
				
			||||||
 | 
					                        accessStore.update((access) => (access.googleUrl = e))
 | 
				
			||||||
 | 
					                      }
 | 
				
			||||||
 | 
					                    ></Input>
 | 
				
			||||||
 | 
					                  </ListItem>
 | 
				
			||||||
 | 
					                  <ListItem
 | 
				
			||||||
 | 
					                    title={Locale.Settings.Access.Google.ApiKey.Title}
 | 
				
			||||||
 | 
					                    subTitle={Locale.Settings.Access.Google.ApiKey.SubTitle}
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
 | 
					                    <Input
 | 
				
			||||||
 | 
					                      value={accessStore.googleApiKey}
 | 
				
			||||||
 | 
					                      type="password"
 | 
				
			||||||
 | 
					                      placeholder={
 | 
				
			||||||
 | 
					                        Locale.Settings.Access.Google.ApiKey.Placeholder
 | 
				
			||||||
 | 
					                      }
 | 
				
			||||||
 | 
					                      onChange={(e) => {
 | 
				
			||||||
 | 
					                        accessStore.update(
 | 
				
			||||||
 | 
					                          (access) => (access.googleApiKey = e),
 | 
				
			||||||
 | 
					                        );
 | 
				
			||||||
 | 
					                      }}
 | 
				
			||||||
 | 
					                    />
 | 
				
			||||||
 | 
					                  </ListItem>
 | 
				
			||||||
 | 
					                  <ListItem
 | 
				
			||||||
 | 
					                    title={Locale.Settings.Access.Google.ApiVersion.Title}
 | 
				
			||||||
 | 
					                    subTitle={Locale.Settings.Access.Google.ApiVersion.SubTitle}
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
 | 
					                    <Input
 | 
				
			||||||
 | 
					                      type="text"
 | 
				
			||||||
 | 
					                      value={accessStore.googleApiVersion}
 | 
				
			||||||
 | 
					                      placeholder="2023-08-01-preview"
 | 
				
			||||||
 | 
					                      onChange={(e) =>
 | 
				
			||||||
 | 
					                        accessStore.update(
 | 
				
			||||||
 | 
					                          (access) => (access.googleApiVersion = e),
 | 
				
			||||||
 | 
					                        )
 | 
				
			||||||
 | 
					                      }
 | 
				
			||||||
 | 
					                    ></Input>
 | 
				
			||||||
 | 
					                  </ListItem>
 | 
				
			||||||
 | 
					                </>
 | 
				
			||||||
 | 
					              )}
 | 
				
			||||||
 | 
					              {accessStore.provider === ServiceProvider.Anthropic && (
 | 
				
			||||||
 | 
					                <>
 | 
				
			||||||
 | 
					                  <ListItem
 | 
				
			||||||
 | 
					                    title={Locale.Settings.Access.Anthropic.Endpoint.Title}
 | 
				
			||||||
 | 
					                    subTitle={
 | 
				
			||||||
 | 
					                      Locale.Settings.Access.Anthropic.Endpoint.SubTitle +
 | 
				
			||||||
 | 
					                      Anthropic.ExampleEndpoint
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
 | 
					                    <Input
 | 
				
			||||||
 | 
					                      type="text"
 | 
				
			||||||
 | 
					                      value={accessStore.anthropicUrl}
 | 
				
			||||||
 | 
					                      placeholder={Anthropic.ExampleEndpoint}
 | 
				
			||||||
 | 
					                      onChange={(e) =>
 | 
				
			||||||
 | 
					                        accessStore.update(
 | 
				
			||||||
 | 
					                          (access) => (access.anthropicUrl = e),
 | 
				
			||||||
 | 
					                        )
 | 
				
			||||||
 | 
					                      }
 | 
				
			||||||
 | 
					                    ></Input>
 | 
				
			||||||
 | 
					                  </ListItem>
 | 
				
			||||||
 | 
					                  <ListItem
 | 
				
			||||||
 | 
					                    title={Locale.Settings.Access.Anthropic.ApiKey.Title}
 | 
				
			||||||
 | 
					                    subTitle={Locale.Settings.Access.Anthropic.ApiKey.SubTitle}
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
 | 
					                    <Input
 | 
				
			||||||
 | 
					                      value={accessStore.anthropicApiKey}
 | 
				
			||||||
 | 
					                      type="password"
 | 
				
			||||||
 | 
					                      placeholder={
 | 
				
			||||||
 | 
					                        Locale.Settings.Access.Anthropic.ApiKey.Placeholder
 | 
				
			||||||
 | 
					                      }
 | 
				
			||||||
 | 
					                      onChange={(e) => {
 | 
				
			||||||
 | 
					                        accessStore.update(
 | 
				
			||||||
 | 
					                          (access) => (access.anthropicApiKey = e),
 | 
				
			||||||
 | 
					                        );
 | 
				
			||||||
 | 
					                      }}
 | 
				
			||||||
 | 
					                    />
 | 
				
			||||||
 | 
					                  </ListItem>
 | 
				
			||||||
 | 
					                  <ListItem
 | 
				
			||||||
 | 
					                    title={Locale.Settings.Access.Anthropic.ApiVerion.Title}
 | 
				
			||||||
 | 
					                    subTitle={
 | 
				
			||||||
 | 
					                      Locale.Settings.Access.Anthropic.ApiVerion.SubTitle
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
 | 
					                    <Input
 | 
				
			||||||
 | 
					                      type="text"
 | 
				
			||||||
 | 
					                      value={accessStore.anthropicApiVersion}
 | 
				
			||||||
 | 
					                      placeholder={Anthropic.Vision}
 | 
				
			||||||
 | 
					                      onChange={(e) =>
 | 
				
			||||||
 | 
					                        accessStore.update(
 | 
				
			||||||
 | 
					                          (access) => (access.anthropicApiVersion = e),
 | 
				
			||||||
 | 
					                        )
 | 
				
			||||||
 | 
					                      }
 | 
				
			||||||
 | 
					                    ></Input>
 | 
				
			||||||
 | 
					                  </ListItem>
 | 
				
			||||||
 | 
					                </>
 | 
				
			||||||
 | 
					              )}
 | 
				
			||||||
 | 
					            </>
 | 
				
			||||||
 | 
					          )}
 | 
				
			||||||
 | 
					        </>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <ListItem
 | 
				
			||||||
 | 
					        title={Locale.Settings.Access.CustomModel.Title}
 | 
				
			||||||
 | 
					        subTitle={Locale.Settings.Access.CustomModel.SubTitle}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <Input
 | 
				
			||||||
 | 
					          type="text"
 | 
				
			||||||
 | 
					          value={config.customModels}
 | 
				
			||||||
 | 
					          placeholder="model1,model2,model3"
 | 
				
			||||||
 | 
					          onChange={(e) => config.update((config) => (config.customModels = e))}
 | 
				
			||||||
 | 
					        ></Input>
 | 
				
			||||||
 | 
					      </ListItem>
 | 
				
			||||||
 | 
					    </List>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										47
									
								
								app/containers/Settings/components/SettingHeader.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,47 @@
 | 
				
			|||||||
 | 
					import Locale from "@/app/locales";
 | 
				
			||||||
 | 
					import GobackIcon from "@/app/icons/goback.svg";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface ChatHeaderProps {
 | 
				
			||||||
 | 
					  isMobileScreen: boolean;
 | 
				
			||||||
 | 
					  goback: () => void;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function SettingHeader(props: ChatHeaderProps) {
 | 
				
			||||||
 | 
					  const { isMobileScreen, goback } = props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div
 | 
				
			||||||
 | 
					      className={`
 | 
				
			||||||
 | 
					        relative flex flex-0 justify-between items-center px-6 py-4 gap-chat-header-gap border-b border-settings-header 
 | 
				
			||||||
 | 
					        max-md:h-menu-title-mobile max-md:bg-settings-header-mobile
 | 
				
			||||||
 | 
					      `}
 | 
				
			||||||
 | 
					      data-tauri-drag-region
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      {isMobileScreen ? (
 | 
				
			||||||
 | 
					        <div
 | 
				
			||||||
 | 
					          className="absolute left-4 top-[50%] translate-y-[-50%] cursor-pointer"
 | 
				
			||||||
 | 
					          onClick={() => goback()}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <GobackIcon />
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      ) : null}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div
 | 
				
			||||||
 | 
					        className={`
 | 
				
			||||||
 | 
					        flex-1 
 | 
				
			||||||
 | 
					        max-md:flex max-md:flex-col max-md:items-center max-md:justify-center max-md:gap-0.5 max-md:text
 | 
				
			||||||
 | 
					        md:mr-4
 | 
				
			||||||
 | 
					      `}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <div
 | 
				
			||||||
 | 
					          className={`
 | 
				
			||||||
 | 
					          line-clamp-1 cursor-pointer text-text-settings-panel-header-title text-chat-header-title font-common 
 | 
				
			||||||
 | 
					          max-md:text-sm-title max-md:h-chat-header-title-mobile max-md:leading-5 !font-medium
 | 
				
			||||||
 | 
					          `}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          {Locale.Settings.Title}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										199
									
								
								app/containers/Settings/components/SyncConfigModal.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,199 @@
 | 
				
			|||||||
 | 
					import { Modal } from "@/app/components/ui-lib";
 | 
				
			||||||
 | 
					import { useSyncStore } from "@/app/store/sync";
 | 
				
			||||||
 | 
					import Locale from "@/app/locales";
 | 
				
			||||||
 | 
					import { IconButton } from "@/app/components/button";
 | 
				
			||||||
 | 
					import { ProviderType } from "@/app/utils/cloud";
 | 
				
			||||||
 | 
					import { STORAGE_KEY } from "@/app/constant";
 | 
				
			||||||
 | 
					import { useMemo, useState } from "react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import ConnectionIcon from "@/app/icons/connection.svg";
 | 
				
			||||||
 | 
					import CloudSuccessIcon from "@/app/icons/cloud-success.svg";
 | 
				
			||||||
 | 
					import CloudFailIcon from "@/app/icons/cloud-fail.svg";
 | 
				
			||||||
 | 
					import ConfirmIcon from "@/app/icons/confirm.svg";
 | 
				
			||||||
 | 
					import LoadingIcon from "@/app/icons/three-dots.svg";
 | 
				
			||||||
 | 
					import List, { ListItem } from "@/app/components/List";
 | 
				
			||||||
 | 
					import Switch from "@/app/components/Switch";
 | 
				
			||||||
 | 
					import Select from "@/app/components/Select";
 | 
				
			||||||
 | 
					import Input from "@/app/components/Input";
 | 
				
			||||||
 | 
					import { useAppConfig } from "@/app/store";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function CheckButton() {
 | 
				
			||||||
 | 
					  const syncStore = useSyncStore();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const couldCheck = useMemo(() => {
 | 
				
			||||||
 | 
					    return syncStore.cloudSync();
 | 
				
			||||||
 | 
					  }, [syncStore]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [checkState, setCheckState] = useState<
 | 
				
			||||||
 | 
					    "none" | "checking" | "success" | "failed"
 | 
				
			||||||
 | 
					  >("none");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async function check() {
 | 
				
			||||||
 | 
					    setCheckState("checking");
 | 
				
			||||||
 | 
					    const valid = await syncStore.check();
 | 
				
			||||||
 | 
					    setCheckState(valid ? "success" : "failed");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!couldCheck) return null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <IconButton
 | 
				
			||||||
 | 
					      text={Locale.Settings.Sync.Config.Modal.Check}
 | 
				
			||||||
 | 
					      bordered
 | 
				
			||||||
 | 
					      onClick={check}
 | 
				
			||||||
 | 
					      icon={
 | 
				
			||||||
 | 
					        checkState === "none" ? (
 | 
				
			||||||
 | 
					          <ConnectionIcon />
 | 
				
			||||||
 | 
					        ) : checkState === "checking" ? (
 | 
				
			||||||
 | 
					          <LoadingIcon />
 | 
				
			||||||
 | 
					        ) : checkState === "success" ? (
 | 
				
			||||||
 | 
					          <CloudSuccessIcon />
 | 
				
			||||||
 | 
					        ) : checkState === "failed" ? (
 | 
				
			||||||
 | 
					          <CloudFailIcon />
 | 
				
			||||||
 | 
					        ) : (
 | 
				
			||||||
 | 
					          <ConnectionIcon />
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    ></IconButton>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function SyncConfigModal(props: { onClose?: () => void }) {
 | 
				
			||||||
 | 
					  const syncStore = useSyncStore();
 | 
				
			||||||
 | 
					  const config = useAppConfig();
 | 
				
			||||||
 | 
					  const { isMobileScreen } = config;
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className="modal-mask">
 | 
				
			||||||
 | 
					      <Modal
 | 
				
			||||||
 | 
					        title={Locale.Settings.Sync.Config.Modal.Title}
 | 
				
			||||||
 | 
					        onClose={() => props.onClose?.()}
 | 
				
			||||||
 | 
					        actions={[
 | 
				
			||||||
 | 
					          <CheckButton key="check" />,
 | 
				
			||||||
 | 
					          <IconButton
 | 
				
			||||||
 | 
					            key="confirm"
 | 
				
			||||||
 | 
					            onClick={props.onClose}
 | 
				
			||||||
 | 
					            icon={<ConfirmIcon />}
 | 
				
			||||||
 | 
					            bordered
 | 
				
			||||||
 | 
					            text={Locale.UI.Confirm}
 | 
				
			||||||
 | 
					          />,
 | 
				
			||||||
 | 
					        ]}
 | 
				
			||||||
 | 
					        className="!bg-modal-mask active-new"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <List
 | 
				
			||||||
 | 
					          widgetStyle={{
 | 
				
			||||||
 | 
					            rangeNextLine: isMobileScreen,
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <ListItem
 | 
				
			||||||
 | 
					            title={Locale.Settings.Sync.Config.SyncType.Title}
 | 
				
			||||||
 | 
					            subTitle={Locale.Settings.Sync.Config.SyncType.SubTitle}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <Select
 | 
				
			||||||
 | 
					              value={syncStore.provider}
 | 
				
			||||||
 | 
					              options={Object.entries(ProviderType).map(([k, v]) => ({
 | 
				
			||||||
 | 
					                value: v,
 | 
				
			||||||
 | 
					                label: k,
 | 
				
			||||||
 | 
					              }))}
 | 
				
			||||||
 | 
					              onSelect={(v) => {
 | 
				
			||||||
 | 
					                syncStore.update((config) => (config.provider = v));
 | 
				
			||||||
 | 
					              }}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					          </ListItem>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <ListItem
 | 
				
			||||||
 | 
					            title={Locale.Settings.Sync.Config.Proxy.Title}
 | 
				
			||||||
 | 
					            subTitle={Locale.Settings.Sync.Config.Proxy.SubTitle}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <Switch
 | 
				
			||||||
 | 
					              value={syncStore.useProxy}
 | 
				
			||||||
 | 
					              onChange={(e) => {
 | 
				
			||||||
 | 
					                syncStore.update((config) => (config.useProxy = e));
 | 
				
			||||||
 | 
					              }}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					          </ListItem>
 | 
				
			||||||
 | 
					          {syncStore.useProxy ? (
 | 
				
			||||||
 | 
					            <ListItem
 | 
				
			||||||
 | 
					              title={Locale.Settings.Sync.Config.ProxyUrl.Title}
 | 
				
			||||||
 | 
					              subTitle={Locale.Settings.Sync.Config.ProxyUrl.SubTitle}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              <Input
 | 
				
			||||||
 | 
					                type="text"
 | 
				
			||||||
 | 
					                value={syncStore.proxyUrl}
 | 
				
			||||||
 | 
					                onChange={(e) => {
 | 
				
			||||||
 | 
					                  syncStore.update((config) => (config.proxyUrl = e));
 | 
				
			||||||
 | 
					                }}
 | 
				
			||||||
 | 
					              ></Input>
 | 
				
			||||||
 | 
					            </ListItem>
 | 
				
			||||||
 | 
					          ) : null}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          {syncStore.provider === ProviderType.WebDAV && (
 | 
				
			||||||
 | 
					            <>
 | 
				
			||||||
 | 
					              <ListItem title={Locale.Settings.Sync.Config.WebDav.Endpoint}>
 | 
				
			||||||
 | 
					                <Input
 | 
				
			||||||
 | 
					                  type="text"
 | 
				
			||||||
 | 
					                  value={syncStore.webdav.endpoint}
 | 
				
			||||||
 | 
					                  onChange={(e) => {
 | 
				
			||||||
 | 
					                    syncStore.update((config) => (config.webdav.endpoint = e));
 | 
				
			||||||
 | 
					                  }}
 | 
				
			||||||
 | 
					                ></Input>
 | 
				
			||||||
 | 
					              </ListItem>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              <ListItem title={Locale.Settings.Sync.Config.WebDav.UserName}>
 | 
				
			||||||
 | 
					                <Input
 | 
				
			||||||
 | 
					                  type="text"
 | 
				
			||||||
 | 
					                  value={syncStore.webdav.username}
 | 
				
			||||||
 | 
					                  onChange={(e) => {
 | 
				
			||||||
 | 
					                    syncStore.update((config) => (config.webdav.username = e));
 | 
				
			||||||
 | 
					                  }}
 | 
				
			||||||
 | 
					                ></Input>
 | 
				
			||||||
 | 
					              </ListItem>
 | 
				
			||||||
 | 
					              <ListItem title={Locale.Settings.Sync.Config.WebDav.Password}>
 | 
				
			||||||
 | 
					                <Input
 | 
				
			||||||
 | 
					                  value={syncStore.webdav.password}
 | 
				
			||||||
 | 
					                  type="password"
 | 
				
			||||||
 | 
					                  onChange={(e) => {
 | 
				
			||||||
 | 
					                    syncStore.update((config) => (config.webdav.password = e));
 | 
				
			||||||
 | 
					                  }}
 | 
				
			||||||
 | 
					                ></Input>
 | 
				
			||||||
 | 
					              </ListItem>
 | 
				
			||||||
 | 
					            </>
 | 
				
			||||||
 | 
					          )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          {syncStore.provider === ProviderType.UpStash && (
 | 
				
			||||||
 | 
					            <>
 | 
				
			||||||
 | 
					              <ListItem title={Locale.Settings.Sync.Config.UpStash.Endpoint}>
 | 
				
			||||||
 | 
					                <Input
 | 
				
			||||||
 | 
					                  type="text"
 | 
				
			||||||
 | 
					                  value={syncStore.upstash.endpoint}
 | 
				
			||||||
 | 
					                  onChange={(e) => {
 | 
				
			||||||
 | 
					                    syncStore.update((config) => (config.upstash.endpoint = e));
 | 
				
			||||||
 | 
					                  }}
 | 
				
			||||||
 | 
					                ></Input>
 | 
				
			||||||
 | 
					              </ListItem>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              <ListItem title={Locale.Settings.Sync.Config.UpStash.UserName}>
 | 
				
			||||||
 | 
					                <Input
 | 
				
			||||||
 | 
					                  type="text"
 | 
				
			||||||
 | 
					                  value={syncStore.upstash.username}
 | 
				
			||||||
 | 
					                  placeholder={STORAGE_KEY}
 | 
				
			||||||
 | 
					                  onChange={(e) => {
 | 
				
			||||||
 | 
					                    syncStore.update((config) => (config.upstash.username = e));
 | 
				
			||||||
 | 
					                  }}
 | 
				
			||||||
 | 
					                ></Input>
 | 
				
			||||||
 | 
					              </ListItem>
 | 
				
			||||||
 | 
					              <ListItem title={Locale.Settings.Sync.Config.UpStash.Password}>
 | 
				
			||||||
 | 
					                <Input
 | 
				
			||||||
 | 
					                  value={syncStore.upstash.apiKey}
 | 
				
			||||||
 | 
					                  type="password"
 | 
				
			||||||
 | 
					                  onChange={(e) => {
 | 
				
			||||||
 | 
					                    syncStore.update((config) => (config.upstash.apiKey = e));
 | 
				
			||||||
 | 
					                  }}
 | 
				
			||||||
 | 
					                ></Input>
 | 
				
			||||||
 | 
					              </ListItem>
 | 
				
			||||||
 | 
					            </>
 | 
				
			||||||
 | 
					          )}
 | 
				
			||||||
 | 
					        </List>
 | 
				
			||||||
 | 
					      </Modal>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										112
									
								
								app/containers/Settings/components/SyncItems.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,112 @@
 | 
				
			|||||||
 | 
					import ConfigIcon from "@/app/icons/configIcon2.svg";
 | 
				
			||||||
 | 
					import ExportIcon from "@/app/icons/exportIcon.svg";
 | 
				
			||||||
 | 
					import ImportIcon from "@/app/icons/importIcon.svg";
 | 
				
			||||||
 | 
					import SyncIcon from "@/app/icons/syncIcon.svg";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { showToast } from "@/app/components/ui-lib";
 | 
				
			||||||
 | 
					import { useChatStore } from "@/app/store/chat";
 | 
				
			||||||
 | 
					import { useMaskStore } from "@/app/store/mask";
 | 
				
			||||||
 | 
					import { usePromptStore } from "@/app/store/prompt";
 | 
				
			||||||
 | 
					import { useSyncStore } from "@/app/store/sync";
 | 
				
			||||||
 | 
					import { useMemo, useState } from "react";
 | 
				
			||||||
 | 
					import Locale from "@/app/locales";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import SyncConfigModal from "./SyncConfigModal";
 | 
				
			||||||
 | 
					import List, { ListItem } from "@/app/components/List";
 | 
				
			||||||
 | 
					import Btn from "@/app/components/Btn";
 | 
				
			||||||
 | 
					import { useAppConfig } from "@/app/store";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function SyncItems() {
 | 
				
			||||||
 | 
					  const syncStore = useSyncStore();
 | 
				
			||||||
 | 
					  const chatStore = useChatStore();
 | 
				
			||||||
 | 
					  const promptStore = usePromptStore();
 | 
				
			||||||
 | 
					  const maskStore = useMaskStore();
 | 
				
			||||||
 | 
					  const couldSync = useMemo(() => {
 | 
				
			||||||
 | 
					    return syncStore.cloudSync();
 | 
				
			||||||
 | 
					  }, [syncStore]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const { isMobileScreen } = useAppConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [showSyncConfigModal, setShowSyncConfigModal] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const stateOverview = useMemo(() => {
 | 
				
			||||||
 | 
					    const sessions = chatStore.sessions;
 | 
				
			||||||
 | 
					    const messageCount = sessions.reduce((p, c) => p + c.messages.length, 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      chat: sessions.length,
 | 
				
			||||||
 | 
					      message: messageCount,
 | 
				
			||||||
 | 
					      prompt: Object.keys(promptStore.prompts).length,
 | 
				
			||||||
 | 
					      mask: Object.keys(maskStore.masks).length,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }, [chatStore.sessions, maskStore.masks, promptStore.prompts]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const textStyle = "!text-sm";
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <>
 | 
				
			||||||
 | 
					      <List>
 | 
				
			||||||
 | 
					        <ListItem
 | 
				
			||||||
 | 
					          title={Locale.Settings.Sync.CloudState}
 | 
				
			||||||
 | 
					          subTitle={
 | 
				
			||||||
 | 
					            syncStore.lastProvider
 | 
				
			||||||
 | 
					              ? `${new Date(syncStore.lastSyncTime).toLocaleString()} [${
 | 
				
			||||||
 | 
					                  syncStore.lastProvider
 | 
				
			||||||
 | 
					                }]`
 | 
				
			||||||
 | 
					              : Locale.Settings.Sync.NotSyncYet
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <div className="flex gap-3">
 | 
				
			||||||
 | 
					            <Btn
 | 
				
			||||||
 | 
					              onClick={() => {
 | 
				
			||||||
 | 
					                setShowSyncConfigModal(true);
 | 
				
			||||||
 | 
					              }}
 | 
				
			||||||
 | 
					              text={<span className={textStyle}>{Locale.UI.Config}</span>}
 | 
				
			||||||
 | 
					              prefixIcon={isMobileScreen ? undefined : <ConfigIcon />}
 | 
				
			||||||
 | 
					            ></Btn>
 | 
				
			||||||
 | 
					            {couldSync && (
 | 
				
			||||||
 | 
					              <Btn
 | 
				
			||||||
 | 
					                onClick={async () => {
 | 
				
			||||||
 | 
					                  try {
 | 
				
			||||||
 | 
					                    await syncStore.sync();
 | 
				
			||||||
 | 
					                    showToast(Locale.Settings.Sync.Success);
 | 
				
			||||||
 | 
					                  } catch (e) {
 | 
				
			||||||
 | 
					                    showToast(Locale.Settings.Sync.Fail);
 | 
				
			||||||
 | 
					                    console.error("[Sync]", e);
 | 
				
			||||||
 | 
					                  }
 | 
				
			||||||
 | 
					                }}
 | 
				
			||||||
 | 
					                text={<span className={textStyle}>{Locale.UI.Sync}</span>}
 | 
				
			||||||
 | 
					                prefixIcon={<SyncIcon />}
 | 
				
			||||||
 | 
					              ></Btn>
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </ListItem>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <ListItem
 | 
				
			||||||
 | 
					          title={Locale.Settings.Sync.LocalState}
 | 
				
			||||||
 | 
					          subTitle={Locale.Settings.Sync.Overview(stateOverview)}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <div className="flex gap-3">
 | 
				
			||||||
 | 
					            <Btn
 | 
				
			||||||
 | 
					              onClick={() => {
 | 
				
			||||||
 | 
					                syncStore.export();
 | 
				
			||||||
 | 
					              }}
 | 
				
			||||||
 | 
					              text={<span className={textStyle}>{Locale.UI.Export}</span>}
 | 
				
			||||||
 | 
					              prefixIcon={<ExportIcon />}
 | 
				
			||||||
 | 
					            ></Btn>
 | 
				
			||||||
 | 
					            <Btn
 | 
				
			||||||
 | 
					              onClick={async () => {
 | 
				
			||||||
 | 
					                syncStore.import();
 | 
				
			||||||
 | 
					              }}
 | 
				
			||||||
 | 
					              text={<span className={textStyle}>{Locale.UI.Import}</span>}
 | 
				
			||||||
 | 
					              prefixIcon={<ImportIcon />}
 | 
				
			||||||
 | 
					            ></Btn>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </ListItem>
 | 
				
			||||||
 | 
					      </List>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      {showSyncConfigModal && (
 | 
				
			||||||
 | 
					        <SyncConfigModal onClose={() => setShowSyncConfigModal(false)} />
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										169
									
								
								app/containers/Settings/components/UserPromptModal.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,169 @@
 | 
				
			|||||||
 | 
					import { useEffect, useState } from "react";
 | 
				
			||||||
 | 
					import { nanoid } from "nanoid";
 | 
				
			||||||
 | 
					import { Prompt, SearchService, usePromptStore } from "@/app/store/prompt";
 | 
				
			||||||
 | 
					import { Input as Textarea, Modal } from "@/app/components/ui-lib";
 | 
				
			||||||
 | 
					import Locale from "@/app/locales";
 | 
				
			||||||
 | 
					import { IconButton } from "@/app/components/button";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import AddIcon from "@/app/icons/add.svg";
 | 
				
			||||||
 | 
					import CopyIcon from "@/app/icons/copy.svg";
 | 
				
			||||||
 | 
					import ClearIcon from "@/app/icons/clear.svg";
 | 
				
			||||||
 | 
					import EditIcon from "@/app/icons/edit.svg";
 | 
				
			||||||
 | 
					import EyeIcon from "@/app/icons/eye.svg";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import styles from "../index.module.scss";
 | 
				
			||||||
 | 
					import { copyToClipboard } from "@/app/utils";
 | 
				
			||||||
 | 
					import Input from "@/app/components/Input";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function EditPromptModal(props: { id: string; onClose: () => void }) {
 | 
				
			||||||
 | 
					  const promptStore = usePromptStore();
 | 
				
			||||||
 | 
					  const prompt = promptStore.get(props.id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return prompt ? (
 | 
				
			||||||
 | 
					    <div className="modal-mask">
 | 
				
			||||||
 | 
					      <Modal
 | 
				
			||||||
 | 
					        title={Locale.Settings.Prompt.EditModal.Title}
 | 
				
			||||||
 | 
					        onClose={props.onClose}
 | 
				
			||||||
 | 
					        actions={[
 | 
				
			||||||
 | 
					          <IconButton
 | 
				
			||||||
 | 
					            key=""
 | 
				
			||||||
 | 
					            onClick={props.onClose}
 | 
				
			||||||
 | 
					            text={Locale.UI.Confirm}
 | 
				
			||||||
 | 
					            bordered
 | 
				
			||||||
 | 
					          />,
 | 
				
			||||||
 | 
					        ]}
 | 
				
			||||||
 | 
					        // className="!bg-modal-mask"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <div className={styles["edit-prompt-modal"]}>
 | 
				
			||||||
 | 
					          <Input
 | 
				
			||||||
 | 
					            type="text"
 | 
				
			||||||
 | 
					            value={prompt.title}
 | 
				
			||||||
 | 
					            readOnly={!prompt.isUser}
 | 
				
			||||||
 | 
					            className={styles["edit-prompt-title"]}
 | 
				
			||||||
 | 
					            onChange={(e) =>
 | 
				
			||||||
 | 
					              promptStore.updatePrompt(props.id, (prompt) => (prompt.title = e))
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          ></Input>
 | 
				
			||||||
 | 
					          <Textarea
 | 
				
			||||||
 | 
					            value={prompt.content}
 | 
				
			||||||
 | 
					            readOnly={!prompt.isUser}
 | 
				
			||||||
 | 
					            className={styles["edit-prompt-content"]}
 | 
				
			||||||
 | 
					            rows={10}
 | 
				
			||||||
 | 
					            onInput={(e) =>
 | 
				
			||||||
 | 
					              promptStore.updatePrompt(
 | 
				
			||||||
 | 
					                props.id,
 | 
				
			||||||
 | 
					                (prompt) => (prompt.content = e.currentTarget.value),
 | 
				
			||||||
 | 
					              )
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          ></Textarea>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </Modal>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  ) : null;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function UserPromptModal(props: { onClose?: () => void }) {
 | 
				
			||||||
 | 
					  const promptStore = usePromptStore();
 | 
				
			||||||
 | 
					  const userPrompts = promptStore.getUserPrompts();
 | 
				
			||||||
 | 
					  const builtinPrompts = SearchService.builtinPrompts;
 | 
				
			||||||
 | 
					  const allPrompts = userPrompts.concat(builtinPrompts);
 | 
				
			||||||
 | 
					  const [searchInput, setSearchInput] = useState("");
 | 
				
			||||||
 | 
					  const [searchPrompts, setSearchPrompts] = useState<Prompt[]>([]);
 | 
				
			||||||
 | 
					  const prompts = searchInput.length > 0 ? searchPrompts : allPrompts;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [editingPromptId, setEditingPromptId] = useState<string>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    if (searchInput.length > 0) {
 | 
				
			||||||
 | 
					      const searchResult = SearchService.search(searchInput);
 | 
				
			||||||
 | 
					      setSearchPrompts(searchResult);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      setSearchPrompts([]);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [searchInput]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className="modal-mask">
 | 
				
			||||||
 | 
					      <Modal
 | 
				
			||||||
 | 
					        title={Locale.Settings.Prompt.Modal.Title}
 | 
				
			||||||
 | 
					        onClose={() => props.onClose?.()}
 | 
				
			||||||
 | 
					        actions={[
 | 
				
			||||||
 | 
					          <IconButton
 | 
				
			||||||
 | 
					            key="add"
 | 
				
			||||||
 | 
					            onClick={() => {
 | 
				
			||||||
 | 
					              const promptId = promptStore.add({
 | 
				
			||||||
 | 
					                id: nanoid(),
 | 
				
			||||||
 | 
					                createdAt: Date.now(),
 | 
				
			||||||
 | 
					                title: "Empty Prompt",
 | 
				
			||||||
 | 
					                content: "Empty Prompt Content",
 | 
				
			||||||
 | 
					              });
 | 
				
			||||||
 | 
					              setEditingPromptId(promptId);
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					            icon={<AddIcon />}
 | 
				
			||||||
 | 
					            bordered
 | 
				
			||||||
 | 
					            text={Locale.Settings.Prompt.Modal.Add}
 | 
				
			||||||
 | 
					          />,
 | 
				
			||||||
 | 
					        ]}
 | 
				
			||||||
 | 
					        // className="!bg-modal-mask"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <div className={styles["user-prompt-modal"]}>
 | 
				
			||||||
 | 
					          <Input
 | 
				
			||||||
 | 
					            type="text"
 | 
				
			||||||
 | 
					            className={styles["user-prompt-search"]}
 | 
				
			||||||
 | 
					            placeholder={Locale.Settings.Prompt.Modal.Search}
 | 
				
			||||||
 | 
					            value={searchInput}
 | 
				
			||||||
 | 
					            onChange={(e) => setSearchInput(e)}
 | 
				
			||||||
 | 
					          ></Input>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <div className={styles["user-prompt-list"]}>
 | 
				
			||||||
 | 
					            {prompts.map((v, _) => (
 | 
				
			||||||
 | 
					              <div className={styles["user-prompt-item"]} key={v.id ?? v.title}>
 | 
				
			||||||
 | 
					                <div className={styles["user-prompt-header"]}>
 | 
				
			||||||
 | 
					                  <div className={styles["user-prompt-title"]}>{v.title}</div>
 | 
				
			||||||
 | 
					                  <div className={styles["user-prompt-content"] + " one-line"}>
 | 
				
			||||||
 | 
					                    {v.content}
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <div className={styles["user-prompt-buttons"]}>
 | 
				
			||||||
 | 
					                  {v.isUser && (
 | 
				
			||||||
 | 
					                    <IconButton
 | 
				
			||||||
 | 
					                      icon={<ClearIcon />}
 | 
				
			||||||
 | 
					                      className={styles["user-prompt-button"]}
 | 
				
			||||||
 | 
					                      onClick={() => promptStore.remove(v.id!)}
 | 
				
			||||||
 | 
					                    />
 | 
				
			||||||
 | 
					                  )}
 | 
				
			||||||
 | 
					                  {v.isUser ? (
 | 
				
			||||||
 | 
					                    <IconButton
 | 
				
			||||||
 | 
					                      icon={<EditIcon />}
 | 
				
			||||||
 | 
					                      className={styles["user-prompt-button"]}
 | 
				
			||||||
 | 
					                      onClick={() => setEditingPromptId(v.id)}
 | 
				
			||||||
 | 
					                    />
 | 
				
			||||||
 | 
					                  ) : (
 | 
				
			||||||
 | 
					                    <IconButton
 | 
				
			||||||
 | 
					                      icon={<EyeIcon />}
 | 
				
			||||||
 | 
					                      className={styles["user-prompt-button"]}
 | 
				
			||||||
 | 
					                      onClick={() => setEditingPromptId(v.id)}
 | 
				
			||||||
 | 
					                    />
 | 
				
			||||||
 | 
					                  )}
 | 
				
			||||||
 | 
					                  <IconButton
 | 
				
			||||||
 | 
					                    icon={<CopyIcon />}
 | 
				
			||||||
 | 
					                    className={styles["user-prompt-button"]}
 | 
				
			||||||
 | 
					                    onClick={() => copyToClipboard(v.content)}
 | 
				
			||||||
 | 
					                  />
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            ))}
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </Modal>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      {editingPromptId !== undefined && (
 | 
				
			||||||
 | 
					        <EditPromptModal
 | 
				
			||||||
 | 
					          id={editingPromptId!}
 | 
				
			||||||
 | 
					          onClose={() => setEditingPromptId(undefined)}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										69
									
								
								app/containers/Settings/index.module.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,69 @@
 | 
				
			|||||||
 | 
					.avatar {
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					  z-index: 1;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.edit-prompt-modal {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .edit-prompt-title {
 | 
				
			||||||
 | 
					    max-width: unset;
 | 
				
			||||||
 | 
					    margin-bottom: 20px;
 | 
				
			||||||
 | 
					    text-align: left;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  .edit-prompt-content {
 | 
				
			||||||
 | 
					    max-width: unset;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.user-prompt-modal {
 | 
				
			||||||
 | 
					  min-height: 40vh;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .user-prompt-search {
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					    max-width: 100%;
 | 
				
			||||||
 | 
					    margin-bottom: 10px;
 | 
				
			||||||
 | 
					    background-color: var(--gray);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .user-prompt-list {
 | 
				
			||||||
 | 
					    border: var(--border-in-light);
 | 
				
			||||||
 | 
					    border-radius: 10px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .user-prompt-item {
 | 
				
			||||||
 | 
					      display: flex;
 | 
				
			||||||
 | 
					      justify-content: space-between;
 | 
				
			||||||
 | 
					      padding: 10px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      &:not(:last-child) {
 | 
				
			||||||
 | 
					        border-bottom: var(--border-in-light);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .user-prompt-header {
 | 
				
			||||||
 | 
					        max-width: calc(100% - 100px);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .user-prompt-title {
 | 
				
			||||||
 | 
					          font-size: 14px;
 | 
				
			||||||
 | 
					          line-height: 2;
 | 
				
			||||||
 | 
					          font-weight: bold;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        .user-prompt-content {
 | 
				
			||||||
 | 
					          font-size: 12px;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .user-prompt-buttons {
 | 
				
			||||||
 | 
					        display: flex;
 | 
				
			||||||
 | 
					        align-items: center;
 | 
				
			||||||
 | 
					        column-gap: 2px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .user-prompt-button {
 | 
				
			||||||
 | 
					          //height: 100%;
 | 
				
			||||||
 | 
					          padding: 7px;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										98
									
								
								app/containers/Settings/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,98 @@
 | 
				
			|||||||
 | 
					"use client";
 | 
				
			||||||
 | 
					import Locale from "@/app/locales";
 | 
				
			||||||
 | 
					import MenuLayout from "@/app/components/MenuLayout";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import Panel from "./SettingPanel";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import GotoIcon from "@/app/icons/goto.svg";
 | 
				
			||||||
 | 
					import { useAppConfig } from "@/app/store";
 | 
				
			||||||
 | 
					import { useEffect, useState } from "react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const list = [
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    id: Locale.Settings.GeneralSettings,
 | 
				
			||||||
 | 
					    title: Locale.Settings.GeneralSettings,
 | 
				
			||||||
 | 
					    icon: null,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    id: Locale.Settings.ModelSettings,
 | 
				
			||||||
 | 
					    title: Locale.Settings.ModelSettings,
 | 
				
			||||||
 | 
					    icon: null,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    id: Locale.Settings.DataSettings,
 | 
				
			||||||
 | 
					    title: Locale.Settings.DataSettings,
 | 
				
			||||||
 | 
					    icon: null,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default MenuLayout(function SettingList(props) {
 | 
				
			||||||
 | 
					  const { setShowPanel, setExternalProps } = props;
 | 
				
			||||||
 | 
					  const config = useAppConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const { isMobileScreen } = config;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [selected, setSelected] = useState(list[0].id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    setExternalProps?.(list[0]);
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div
 | 
				
			||||||
 | 
					      className={`
 | 
				
			||||||
 | 
					      max-md:h-[100%] max-md:mx-[-1rem] max-md:py-6 max-md:px-4 max-md:bg-settings-menu-mobile
 | 
				
			||||||
 | 
					      md:pt-7
 | 
				
			||||||
 | 
					    `}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <div data-tauri-drag-region>
 | 
				
			||||||
 | 
					        <div
 | 
				
			||||||
 | 
					          className={`
 | 
				
			||||||
 | 
					            flex items-center justify-between 
 | 
				
			||||||
 | 
					            max-md:h-menu-title-mobile
 | 
				
			||||||
 | 
					            md:pb-5 md:px-4
 | 
				
			||||||
 | 
					          `}
 | 
				
			||||||
 | 
					          data-tauri-drag-region
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <div className="text-setting-title text-text-settings-menu-title font-common !font-bold">
 | 
				
			||||||
 | 
					            {Locale.Settings.Title}
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div
 | 
				
			||||||
 | 
					        className={`flex flex-col gap-2 overflow-y-auto overflow-x-hidden w-[100%]`}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        {list.map((i) => (
 | 
				
			||||||
 | 
					          <div
 | 
				
			||||||
 | 
					            key={i.id}
 | 
				
			||||||
 | 
					            className={`
 | 
				
			||||||
 | 
					              p-4 font-common text-setting-items font-normal text-text-settings-menu-item-title
 | 
				
			||||||
 | 
					              cursor-pointer
 | 
				
			||||||
 | 
					              border 
 | 
				
			||||||
 | 
					              rounded-md
 | 
				
			||||||
 | 
					              border-transparent
 | 
				
			||||||
 | 
					              ${
 | 
				
			||||||
 | 
					                selected === i.id && !isMobileScreen
 | 
				
			||||||
 | 
					                  ? `!bg-chat-menu-session-selected !border-chat-menu-session-selected !font-medium`
 | 
				
			||||||
 | 
					                  : `hover:bg-chat-menu-session-unselected hover:border-chat-menu-session-unselected`
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              flex justify-between items-center
 | 
				
			||||||
 | 
					              max-md:bg-settings-menu-item-mobile
 | 
				
			||||||
 | 
					            `}
 | 
				
			||||||
 | 
					            onClick={() => {
 | 
				
			||||||
 | 
					              setShowPanel?.(true);
 | 
				
			||||||
 | 
					              setExternalProps?.(i);
 | 
				
			||||||
 | 
					              setSelected(i.id);
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            {i.title}
 | 
				
			||||||
 | 
					            {i.icon}
 | 
				
			||||||
 | 
					            {isMobileScreen && <GotoIcon />}
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        ))}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}, Panel);
 | 
				
			||||||
							
								
								
									
										124
									
								
								app/containers/Sidebar/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,124 @@
 | 
				
			|||||||
 | 
					import GitHubIcon from "@/app/icons/githubIcon.svg";
 | 
				
			||||||
 | 
					import DiscoverIcon from "@/app/icons/discoverActive.svg";
 | 
				
			||||||
 | 
					import DiscoverInactiveIcon from "@/app/icons/discoverInactive.svg";
 | 
				
			||||||
 | 
					import DiscoverMobileActive from "@/app/icons/discoverMobileActive.svg";
 | 
				
			||||||
 | 
					import DiscoverMobileInactive from "@/app/icons/discoverMobileInactive.svg";
 | 
				
			||||||
 | 
					import SettingIcon from "@/app/icons/settingActive.svg";
 | 
				
			||||||
 | 
					import SettingInactiveIcon from "@/app/icons/settingInactive.svg";
 | 
				
			||||||
 | 
					import SettingMobileActive from "@/app/icons/settingMobileActive.svg";
 | 
				
			||||||
 | 
					import SettingMobileInactive from "@/app/icons/settingMobileInactive.svg";
 | 
				
			||||||
 | 
					import AssistantActiveIcon from "@/app/icons/assistantActive.svg";
 | 
				
			||||||
 | 
					import AssistantInactiveIcon from "@/app/icons/assistantInactive.svg";
 | 
				
			||||||
 | 
					import AssistantMobileActive from "@/app/icons/assistantMobileActive.svg";
 | 
				
			||||||
 | 
					import AssistantMobileInactive from "@/app/icons/assistantMobileInactive.svg";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { useAppConfig } from "@/app/store";
 | 
				
			||||||
 | 
					import { Path, REPO_URL } from "@/app/constant";
 | 
				
			||||||
 | 
					import { useNavigate, useLocation } from "react-router-dom";
 | 
				
			||||||
 | 
					import useHotKey from "@/app/hooks/useHotKey";
 | 
				
			||||||
 | 
					import ActionsBar from "@/app/components/ActionsBar";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function SideBar(props: { className?: string }) {
 | 
				
			||||||
 | 
					  const navigate = useNavigate();
 | 
				
			||||||
 | 
					  const loc = useLocation();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const config = useAppConfig();
 | 
				
			||||||
 | 
					  const { isMobileScreen } = config;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useHotKey();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let selectedTab: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  switch (loc.pathname) {
 | 
				
			||||||
 | 
					    case Path.Masks:
 | 
				
			||||||
 | 
					    case Path.NewChat:
 | 
				
			||||||
 | 
					      selectedTab = Path.Masks;
 | 
				
			||||||
 | 
					      break;
 | 
				
			||||||
 | 
					    case Path.Settings:
 | 
				
			||||||
 | 
					      selectedTab = Path.Settings;
 | 
				
			||||||
 | 
					      break;
 | 
				
			||||||
 | 
					    default:
 | 
				
			||||||
 | 
					      selectedTab = Path.Home;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div
 | 
				
			||||||
 | 
					      className={`
 | 
				
			||||||
 | 
					      flex h-[100%]
 | 
				
			||||||
 | 
					      max-md:flex-col-reverse max-md:w-[100%]
 | 
				
			||||||
 | 
					      md:relative 
 | 
				
			||||||
 | 
					    `}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <ActionsBar
 | 
				
			||||||
 | 
					        inMobile={isMobileScreen}
 | 
				
			||||||
 | 
					        actionsShema={[
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            id: Path.Masks,
 | 
				
			||||||
 | 
					            icons: {
 | 
				
			||||||
 | 
					              active: <DiscoverIcon />,
 | 
				
			||||||
 | 
					              inactive: <DiscoverInactiveIcon />,
 | 
				
			||||||
 | 
					              mobileActive: <DiscoverMobileActive />,
 | 
				
			||||||
 | 
					              mobileInactive: <DiscoverMobileInactive />,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            title: "Discover",
 | 
				
			||||||
 | 
					            activeClassName: "shadow-sidebar-btn-shadow",
 | 
				
			||||||
 | 
					            className: "mb-4 hover:bg-sidebar-btn-hovered",
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            id: Path.Home,
 | 
				
			||||||
 | 
					            icons: {
 | 
				
			||||||
 | 
					              active: <AssistantActiveIcon />,
 | 
				
			||||||
 | 
					              inactive: <AssistantInactiveIcon />,
 | 
				
			||||||
 | 
					              mobileActive: <AssistantMobileActive />,
 | 
				
			||||||
 | 
					              mobileInactive: <AssistantMobileInactive />,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            title: "Assistant",
 | 
				
			||||||
 | 
					            activeClassName: "shadow-sidebar-btn-shadow",
 | 
				
			||||||
 | 
					            className: "mb-4 hover:bg-sidebar-btn-hovered",
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            id: "github",
 | 
				
			||||||
 | 
					            icons: <GitHubIcon />,
 | 
				
			||||||
 | 
					            className: "!p-2 mb-3 hover:bg-sidebar-btn-hovered",
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            id: Path.Settings,
 | 
				
			||||||
 | 
					            icons: {
 | 
				
			||||||
 | 
					              active: <SettingIcon />,
 | 
				
			||||||
 | 
					              inactive: <SettingInactiveIcon />,
 | 
				
			||||||
 | 
					              mobileActive: <SettingMobileActive />,
 | 
				
			||||||
 | 
					              mobileInactive: <SettingMobileInactive />,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            className: "!p-2 hover:bg-sidebar-btn-hovered",
 | 
				
			||||||
 | 
					            title: "Settrings",
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        ]}
 | 
				
			||||||
 | 
					        onSelect={(id) => {
 | 
				
			||||||
 | 
					          if (id === "github") {
 | 
				
			||||||
 | 
					            return window.open(REPO_URL, "noopener noreferrer");
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          if (id !== Path.Masks) {
 | 
				
			||||||
 | 
					            return navigate(id);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          if (config.dontShowMaskSplashScreen !== true) {
 | 
				
			||||||
 | 
					            navigate(Path.NewChat, { state: { fromHome: true } });
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            navigate(Path.Masks, { state: { fromHome: true } });
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					        groups={{
 | 
				
			||||||
 | 
					          normal: [
 | 
				
			||||||
 | 
					            [Path.Home, Path.Masks],
 | 
				
			||||||
 | 
					            ["github", Path.Settings],
 | 
				
			||||||
 | 
					          ],
 | 
				
			||||||
 | 
					          mobile: [[Path.Home, Path.Masks, Path.Settings]],
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					        selected={selectedTab}
 | 
				
			||||||
 | 
					        className={`
 | 
				
			||||||
 | 
					        max-md:bg-sidebar-mobile  max-md:h-mobile max-md:justify-around
 | 
				
			||||||
 | 
					        2xl:px-5 xl:px-4 md:px-2 md:py-6 md:flex-col
 | 
				
			||||||
 | 
					        `}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										146
									
								
								app/containers/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,146 @@
 | 
				
			|||||||
 | 
					"use client";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					require("../polyfill");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { HashRouter as Router, Routes, Route } from "react-router-dom";
 | 
				
			||||||
 | 
					import { useState, useEffect, useLayoutEffect } 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, useAppConfig } 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";
 | 
				
			||||||
 | 
					import GlobalLoading from "@/app/components/GlobalLoading";
 | 
				
			||||||
 | 
					import { MOBILE_MAX_WIDTH } from "../hooks/useListenWinResize";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Settings = dynamic(
 | 
				
			||||||
 | 
					  async () => await import("@/app/containers/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();
 | 
				
			||||||
 | 
					  const config = useAppConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    console.log("[Config] got config from build time", getClientConfig());
 | 
				
			||||||
 | 
					    useAccessStore.getState().fetch();
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useLayoutEffect(() => {
 | 
				
			||||||
 | 
					    loadAsyncGoogleFont();
 | 
				
			||||||
 | 
					    config.update(
 | 
				
			||||||
 | 
					      (config) =>
 | 
				
			||||||
 | 
					        (config.isMobileScreen = window.innerWidth <= MOBILE_MAX_WIDTH),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!useHasHydrated()) {
 | 
				
			||||||
 | 
					    return <GlobalLoading />;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <ErrorBoundary>
 | 
				
			||||||
 | 
					      <Router>
 | 
				
			||||||
 | 
					        <Screen noAuth={<AuthPage />} sidebar={<SideBar />}>
 | 
				
			||||||
 | 
					          <ErrorBoundary>
 | 
				
			||||||
 | 
					            <Routes>
 | 
				
			||||||
 | 
					              <Route path={Path.Home} element={<Chat />} />
 | 
				
			||||||
 | 
					              <Route
 | 
				
			||||||
 | 
					                path={Path.NewChat}
 | 
				
			||||||
 | 
					                element={
 | 
				
			||||||
 | 
					                  <NewChat
 | 
				
			||||||
 | 
					                    className={`
 | 
				
			||||||
 | 
					              md:w-[100%] px-1
 | 
				
			||||||
 | 
					              ${config.theme === "dark" ? "bg-[var(--white)]" : "bg-gray-50"}
 | 
				
			||||||
 | 
					              ${config.isMobileScreen ? "pb-chat-panel-mobile" : ""}
 | 
				
			||||||
 | 
					              `}
 | 
				
			||||||
 | 
					                  />
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					              <Route
 | 
				
			||||||
 | 
					                path={Path.Masks}
 | 
				
			||||||
 | 
					                element={
 | 
				
			||||||
 | 
					                  <MaskPage
 | 
				
			||||||
 | 
					                    className={`
 | 
				
			||||||
 | 
					                md:w-[100%]
 | 
				
			||||||
 | 
					                ${config.theme === "dark" ? "bg-[var(--white)]" : "bg-gray-50"}
 | 
				
			||||||
 | 
					                ${config.isMobileScreen ? "pb-chat-panel-mobile" : ""}
 | 
				
			||||||
 | 
					              `}
 | 
				
			||||||
 | 
					                  />
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					              <Route path={Path.Chat} element={<Chat />} />
 | 
				
			||||||
 | 
					              <Route path={Path.Settings} element={<Settings />} />
 | 
				
			||||||
 | 
					            </Routes>
 | 
				
			||||||
 | 
					          </ErrorBoundary>
 | 
				
			||||||
 | 
					        </Screen>
 | 
				
			||||||
 | 
					      </Router>
 | 
				
			||||||
 | 
					    </ErrorBoundary>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										
											BIN
										
									
								
								app/fonts/Satoshi-Variable.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								app/fonts/Satoshi-Variable.woff
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								app/fonts/Satoshi-Variable.woff2
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										59
									
								
								app/hooks/useDrag.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,59 @@
 | 
				
			|||||||
 | 
					import { RefObject, useRef } from "react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function useDrag(options: {
 | 
				
			||||||
 | 
					  customDragMove: (nextWidth: number, start?: number) => void;
 | 
				
			||||||
 | 
					  customToggle: () => void;
 | 
				
			||||||
 | 
					  customLimit?: (x: number, start?: number) => number;
 | 
				
			||||||
 | 
					  customDragEnd?: (nextWidth: number, start?: number) => void;
 | 
				
			||||||
 | 
					}) {
 | 
				
			||||||
 | 
					  const { customDragMove, customToggle, customLimit, customDragEnd } =
 | 
				
			||||||
 | 
					    options || {};
 | 
				
			||||||
 | 
					  const limit = customLimit;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const startX = useRef(0);
 | 
				
			||||||
 | 
					  const lastUpdateTime = useRef(Date.now());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const toggleSideBar = customToggle;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const onDragMove = customDragMove;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const onDragStart = (e: MouseEvent) => {
 | 
				
			||||||
 | 
					    // Remembers the initial width each time the mouse is pressed
 | 
				
			||||||
 | 
					    startX.current = e.clientX;
 | 
				
			||||||
 | 
					    const dragStartTime = Date.now();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const handleDragMove = (e: MouseEvent) => {
 | 
				
			||||||
 | 
					      if (Date.now() < lastUpdateTime.current + 20) {
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      lastUpdateTime.current = Date.now();
 | 
				
			||||||
 | 
					      const d = e.clientX - startX.current;
 | 
				
			||||||
 | 
					      const nextWidth = limit?.(d, startX.current) ?? d;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      onDragMove(nextWidth, startX.current);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const handleDragEnd = (e: MouseEvent) => {
 | 
				
			||||||
 | 
					      // In useRef the data is non-responsive, so `config.sidebarWidth` can't get the dynamic sidebarWidth
 | 
				
			||||||
 | 
					      window.removeEventListener("pointermove", handleDragMove);
 | 
				
			||||||
 | 
					      window.removeEventListener("pointerup", handleDragEnd);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // if user click the drag icon, should toggle the sidebar
 | 
				
			||||||
 | 
					      const shouldFireClick = Date.now() - dragStartTime < 300;
 | 
				
			||||||
 | 
					      if (shouldFireClick) {
 | 
				
			||||||
 | 
					        toggleSideBar();
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        const d = e.clientX - startX.current;
 | 
				
			||||||
 | 
					        const nextWidth = limit?.(d, startX.current) ?? d;
 | 
				
			||||||
 | 
					        customDragEnd?.(nextWidth, startX.current);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    window.addEventListener("pointermove", handleDragMove);
 | 
				
			||||||
 | 
					    window.addEventListener("pointerup", handleDragEnd);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    onDragStart,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										21
									
								
								app/hooks/useHotKey.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,21 @@
 | 
				
			|||||||
 | 
					import { useEffect } from "react";
 | 
				
			||||||
 | 
					import { useChatStore } from "../store/chat";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function useHotKey() {
 | 
				
			||||||
 | 
					  const chatStore = useChatStore();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    const onKeyDown = (e: KeyboardEvent) => {
 | 
				
			||||||
 | 
					      if (e.altKey || e.ctrlKey) {
 | 
				
			||||||
 | 
					        if (e.key === "ArrowUp") {
 | 
				
			||||||
 | 
					          chatStore.nextSession(-1);
 | 
				
			||||||
 | 
					        } else if (e.key === "ArrowDown") {
 | 
				
			||||||
 | 
					          chatStore.nextSession(1);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    window.addEventListener("keydown", onKeyDown);
 | 
				
			||||||
 | 
					    return () => window.removeEventListener("keydown", onKeyDown);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										55
									
								
								app/hooks/useListenWinResize.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,55 @@
 | 
				
			|||||||
 | 
					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 "@/app/store/config";
 | 
				
			||||||
 | 
					import { updateGlobalCSSVars } from "@/app/utils/client";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  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;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const { menuWidth } = updateGlobalCSSVars(nextSidebar);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    config.update((config) => {
 | 
				
			||||||
 | 
					      config.sidebarWidth = menuWidth;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    config.update((config) => {
 | 
				
			||||||
 | 
					      config.isMobileScreen = size.width <= MOBILE_MAX_WIDTH;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										25
									
								
								app/hooks/useLoadData.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -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
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										8
									
								
								app/hooks/useMobileScreen.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,8 @@
 | 
				
			|||||||
 | 
					import { useWindowSize } from "@/app/hooks/useWindowSize";
 | 
				
			||||||
 | 
					import { MOBILE_MAX_WIDTH } from "@/app/hooks/useListenWinResize";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function useMobileScreen() {
 | 
				
			||||||
 | 
					  const { width } = useWindowSize();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return width <= MOBILE_MAX_WIDTH;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										72
									
								
								app/hooks/usePaste.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,72 @@
 | 
				
			|||||||
 | 
					import { compressImage, isVisionModel } from "@/app/utils";
 | 
				
			||||||
 | 
					import { useCallback, useRef } from "react";
 | 
				
			||||||
 | 
					import { useChatStore } from "../store/chat";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface UseUploadImageOptions {
 | 
				
			||||||
 | 
					  setUploading?: (v: boolean) => void;
 | 
				
			||||||
 | 
					  emitImages?: (imgs: string[]) => void;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function usePaste(
 | 
				
			||||||
 | 
					  attachImages: string[],
 | 
				
			||||||
 | 
					  options: UseUploadImageOptions,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					  const chatStore = useChatStore();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const attachImagesRef = useRef<string[]>([]);
 | 
				
			||||||
 | 
					  const optionsRef = useRef<UseUploadImageOptions>({});
 | 
				
			||||||
 | 
					  const chatStoreRef = useRef<typeof chatStore | undefined>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  attachImagesRef.current = attachImages;
 | 
				
			||||||
 | 
					  optionsRef.current = options;
 | 
				
			||||||
 | 
					  chatStoreRef.current = chatStore;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handlePaste = useCallback(
 | 
				
			||||||
 | 
					    async (event: React.ClipboardEvent<HTMLTextAreaElement>) => {
 | 
				
			||||||
 | 
					      const { setUploading, emitImages } = optionsRef.current;
 | 
				
			||||||
 | 
					      const currentModel =
 | 
				
			||||||
 | 
					        chatStoreRef.current?.currentSession().mask.modelConfig.model;
 | 
				
			||||||
 | 
					      if (currentModel && !isVisionModel(currentModel)) {
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      const items = (event.clipboardData || window.clipboardData).items;
 | 
				
			||||||
 | 
					      for (const item of items) {
 | 
				
			||||||
 | 
					        if (item.kind === "file" && item.type.startsWith("image/")) {
 | 
				
			||||||
 | 
					          event.preventDefault();
 | 
				
			||||||
 | 
					          const file = item.getAsFile();
 | 
				
			||||||
 | 
					          if (file) {
 | 
				
			||||||
 | 
					            const images: string[] = [];
 | 
				
			||||||
 | 
					            images.push(...attachImages);
 | 
				
			||||||
 | 
					            images.push(
 | 
				
			||||||
 | 
					              ...(await new Promise<string[]>((res, rej) => {
 | 
				
			||||||
 | 
					                setUploading?.(true);
 | 
				
			||||||
 | 
					                const imagesData: string[] = [];
 | 
				
			||||||
 | 
					                compressImage(file, 256 * 1024)
 | 
				
			||||||
 | 
					                  .then((dataUrl) => {
 | 
				
			||||||
 | 
					                    imagesData.push(dataUrl);
 | 
				
			||||||
 | 
					                    setUploading?.(false);
 | 
				
			||||||
 | 
					                    res(imagesData);
 | 
				
			||||||
 | 
					                  })
 | 
				
			||||||
 | 
					                  .catch((e) => {
 | 
				
			||||||
 | 
					                    setUploading?.(false);
 | 
				
			||||||
 | 
					                    rej(e);
 | 
				
			||||||
 | 
					                  });
 | 
				
			||||||
 | 
					              })),
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					            const imagesLength = images.length;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (imagesLength > 3) {
 | 
				
			||||||
 | 
					              images.splice(3, imagesLength - 3);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            emitImages?.(images);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    [],
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    handlePaste,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										104
									
								
								app/hooks/useRelativePosition.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,104 @@
 | 
				
			|||||||
 | 
					import { RefObject, useState } from "react";
 | 
				
			||||||
 | 
					import { useDebouncedCallback } from "use-debounce";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface Options {
 | 
				
			||||||
 | 
					  containerRef?: RefObject<HTMLElement | null>;
 | 
				
			||||||
 | 
					  delay?: number;
 | 
				
			||||||
 | 
					  offsetDistance?: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export enum Orientation {
 | 
				
			||||||
 | 
					  left,
 | 
				
			||||||
 | 
					  right,
 | 
				
			||||||
 | 
					  bottom,
 | 
				
			||||||
 | 
					  top,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type X = Orientation.left | Orientation.right;
 | 
				
			||||||
 | 
					export type Y = Orientation.top | Orientation.bottom;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface Position {
 | 
				
			||||||
 | 
					  id: string;
 | 
				
			||||||
 | 
					  poi: {
 | 
				
			||||||
 | 
					    targetH: number;
 | 
				
			||||||
 | 
					    targetW: number;
 | 
				
			||||||
 | 
					    distanceToRightBoundary: number;
 | 
				
			||||||
 | 
					    distanceToLeftBoundary: number;
 | 
				
			||||||
 | 
					    distanceToTopBoundary: number;
 | 
				
			||||||
 | 
					    distanceToBottomBoundary: number;
 | 
				
			||||||
 | 
					    overlapPositions: Record<Orientation, boolean>;
 | 
				
			||||||
 | 
					    relativePosition: [X, Y];
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function useRelativePosition({
 | 
				
			||||||
 | 
					  containerRef = { current: window.document.body },
 | 
				
			||||||
 | 
					  delay = 100,
 | 
				
			||||||
 | 
					  offsetDistance = 0,
 | 
				
			||||||
 | 
					}: Options) {
 | 
				
			||||||
 | 
					  const [position, setPosition] = useState<Position | undefined>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const getRelativePosition = useDebouncedCallback(
 | 
				
			||||||
 | 
					    (target: HTMLDivElement, id: string) => {
 | 
				
			||||||
 | 
					      if (!containerRef.current) {
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      const {
 | 
				
			||||||
 | 
					        x: targetX,
 | 
				
			||||||
 | 
					        y: targetY,
 | 
				
			||||||
 | 
					        width: targetW,
 | 
				
			||||||
 | 
					        height: targetH,
 | 
				
			||||||
 | 
					      } = target.getBoundingClientRect();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const {
 | 
				
			||||||
 | 
					        x: containerX,
 | 
				
			||||||
 | 
					        y: containerY,
 | 
				
			||||||
 | 
					        width: containerWidth,
 | 
				
			||||||
 | 
					        height: containerHeight,
 | 
				
			||||||
 | 
					      } = containerRef.current.getBoundingClientRect();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const distanceToRightBoundary =
 | 
				
			||||||
 | 
					        containerX + containerWidth - (targetX + targetW) - offsetDistance;
 | 
				
			||||||
 | 
					      const distanceToLeftBoundary = targetX - containerX - offsetDistance;
 | 
				
			||||||
 | 
					      const distanceToTopBoundary = targetY - containerY - offsetDistance;
 | 
				
			||||||
 | 
					      const distanceToBottomBoundary =
 | 
				
			||||||
 | 
					        containerY + containerHeight - (targetY + targetH) - offsetDistance;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      setPosition({
 | 
				
			||||||
 | 
					        id,
 | 
				
			||||||
 | 
					        poi: {
 | 
				
			||||||
 | 
					          targetW: targetW + 2 * offsetDistance,
 | 
				
			||||||
 | 
					          targetH: targetH + 2 * offsetDistance,
 | 
				
			||||||
 | 
					          distanceToRightBoundary,
 | 
				
			||||||
 | 
					          distanceToLeftBoundary,
 | 
				
			||||||
 | 
					          distanceToTopBoundary,
 | 
				
			||||||
 | 
					          distanceToBottomBoundary,
 | 
				
			||||||
 | 
					          overlapPositions: {
 | 
				
			||||||
 | 
					            [Orientation.left]: distanceToLeftBoundary <= 0,
 | 
				
			||||||
 | 
					            [Orientation.top]: distanceToTopBoundary <= 0,
 | 
				
			||||||
 | 
					            [Orientation.right]: distanceToRightBoundary <= 0,
 | 
				
			||||||
 | 
					            [Orientation.bottom]: distanceToBottomBoundary <= 0,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          relativePosition: [
 | 
				
			||||||
 | 
					            distanceToLeftBoundary <= distanceToRightBoundary
 | 
				
			||||||
 | 
					              ? Orientation.left
 | 
				
			||||||
 | 
					              : Orientation.right,
 | 
				
			||||||
 | 
					            distanceToTopBoundary <= distanceToBottomBoundary
 | 
				
			||||||
 | 
					              ? Orientation.top
 | 
				
			||||||
 | 
					              : Orientation.bottom,
 | 
				
			||||||
 | 
					          ],
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    delay,
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      leading: true,
 | 
				
			||||||
 | 
					      trailing: true,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    getRelativePosition,
 | 
				
			||||||
 | 
					    position,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										39
									
								
								app/hooks/useRows.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,39 @@
 | 
				
			|||||||
 | 
					import { useEffect, useState } from "react";
 | 
				
			||||||
 | 
					import { useDebouncedCallback } from "use-debounce";
 | 
				
			||||||
 | 
					import { autoGrowTextArea } from "../utils";
 | 
				
			||||||
 | 
					import { useAppConfig } from "../store";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function useRows({
 | 
				
			||||||
 | 
					  inputRef,
 | 
				
			||||||
 | 
					}: {
 | 
				
			||||||
 | 
					  inputRef: React.RefObject<HTMLTextAreaElement>;
 | 
				
			||||||
 | 
					}) {
 | 
				
			||||||
 | 
					  const [inputRows, setInputRows] = useState(2);
 | 
				
			||||||
 | 
					  const config = useAppConfig();
 | 
				
			||||||
 | 
					  const { isMobileScreen } = config;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const measure = useDebouncedCallback(
 | 
				
			||||||
 | 
					    () => {
 | 
				
			||||||
 | 
					      const rows = inputRef.current ? autoGrowTextArea(inputRef.current) : 1;
 | 
				
			||||||
 | 
					      const inputRows = Math.min(
 | 
				
			||||||
 | 
					        20,
 | 
				
			||||||
 | 
					        Math.max(2 + (isMobileScreen ? -1 : 1), rows),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      setInputRows(inputRows);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    100,
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      leading: true,
 | 
				
			||||||
 | 
					      trailing: true,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    measure();
 | 
				
			||||||
 | 
					  }, [isMobileScreen]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    inputRows,
 | 
				
			||||||
 | 
					    measure,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										61
									
								
								app/hooks/useScrollToBottom.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,61 @@
 | 
				
			|||||||
 | 
					import { RefObject, useEffect, useRef, useState } from "react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function useScrollToBottom(
 | 
				
			||||||
 | 
					  scrollRef: RefObject<HTMLDivElement>,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					  const detach = scrollRef?.current
 | 
				
			||||||
 | 
					    ? Math.abs(
 | 
				
			||||||
 | 
					        scrollRef.current.scrollHeight -
 | 
				
			||||||
 | 
					          (scrollRef.current.scrollTop + scrollRef.current.clientHeight),
 | 
				
			||||||
 | 
					      ) <= 1
 | 
				
			||||||
 | 
					    : false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // for auto-scroll
 | 
				
			||||||
 | 
					  const [autoScroll, setAutoScroll] = useState(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const autoScrollRef = useRef<typeof autoScroll>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  autoScrollRef.current = autoScroll;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function scrollDomToBottom() {
 | 
				
			||||||
 | 
					    const dom = scrollRef.current;
 | 
				
			||||||
 | 
					    if (dom) {
 | 
				
			||||||
 | 
					      requestAnimationFrame(() => {
 | 
				
			||||||
 | 
					        setAutoScroll(true);
 | 
				
			||||||
 | 
					        dom.scrollTo(0, dom.scrollHeight);
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // useEffect(() => {
 | 
				
			||||||
 | 
					  //   const dom = scrollRef.current;
 | 
				
			||||||
 | 
					  //   if (dom) {
 | 
				
			||||||
 | 
					  //     dom.ontouchstart = (e) => {
 | 
				
			||||||
 | 
					  //       const autoScroll = autoScrollRef.current;
 | 
				
			||||||
 | 
					  //       if (autoScroll) {
 | 
				
			||||||
 | 
					  //         setAutoScroll(false);
 | 
				
			||||||
 | 
					  //       }
 | 
				
			||||||
 | 
					  //     }
 | 
				
			||||||
 | 
					  //     dom.onscroll = (e) => {
 | 
				
			||||||
 | 
					  //       const autoScroll = autoScrollRef.current;
 | 
				
			||||||
 | 
					  //       if (autoScroll) {
 | 
				
			||||||
 | 
					  //         setAutoScroll(false);
 | 
				
			||||||
 | 
					  //       }
 | 
				
			||||||
 | 
					  //     }
 | 
				
			||||||
 | 
					  //   }
 | 
				
			||||||
 | 
					  // }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // auto scroll
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    if (autoScroll && !detach) {
 | 
				
			||||||
 | 
					      scrollDomToBottom();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    scrollRef,
 | 
				
			||||||
 | 
					    autoScroll,
 | 
				
			||||||
 | 
					    setAutoScroll,
 | 
				
			||||||
 | 
					    scrollDomToBottom,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										29
									
								
								app/hooks/useShowPromptHint.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,29 @@
 | 
				
			|||||||
 | 
					import { useEffect, useState } from "react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function useShowPromptHint<RenderPompt>(props: {
 | 
				
			||||||
 | 
					  prompts: RenderPompt[];
 | 
				
			||||||
 | 
					}) {
 | 
				
			||||||
 | 
					  const [internalPrompts, setInternalPrompts] = useState<RenderPompt[]>([]);
 | 
				
			||||||
 | 
					  const [notShowPrompt, setNotShowPrompt] = useState(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    if (props.prompts.length !== 0) {
 | 
				
			||||||
 | 
					      setInternalPrompts(props.prompts);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      window.setTimeout(() => {
 | 
				
			||||||
 | 
					        setNotShowPrompt(false);
 | 
				
			||||||
 | 
					      }, 50);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    setNotShowPrompt(true);
 | 
				
			||||||
 | 
					    window.setTimeout(() => {
 | 
				
			||||||
 | 
					      setInternalPrompts(props.prompts);
 | 
				
			||||||
 | 
					    }, 300);
 | 
				
			||||||
 | 
					  }, [props.prompts]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    notShowPrompt,
 | 
				
			||||||
 | 
					    internalPrompts,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										49
									
								
								app/hooks/useSubmitHandler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,49 @@
 | 
				
			|||||||
 | 
					import { useEffect, useRef } from "react";
 | 
				
			||||||
 | 
					import { SubmitKey, useAppConfig } from "../store/config";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function useSubmitHandler() {
 | 
				
			||||||
 | 
					  const config = useAppConfig();
 | 
				
			||||||
 | 
					  const submitKey = config.submitKey;
 | 
				
			||||||
 | 
					  const isComposing = useRef(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    const onCompositionStart = () => {
 | 
				
			||||||
 | 
					      isComposing.current = true;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    const onCompositionEnd = () => {
 | 
				
			||||||
 | 
					      isComposing.current = false;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    window.addEventListener("compositionstart", onCompositionStart);
 | 
				
			||||||
 | 
					    window.addEventListener("compositionend", onCompositionEnd);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return () => {
 | 
				
			||||||
 | 
					      window.removeEventListener("compositionstart", onCompositionStart);
 | 
				
			||||||
 | 
					      window.removeEventListener("compositionend", onCompositionEnd);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const shouldSubmit = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
 | 
				
			||||||
 | 
					    // Fix Chinese input method "Enter" on Safari
 | 
				
			||||||
 | 
					    if (e.keyCode == 229) return false;
 | 
				
			||||||
 | 
					    if (e.key !== "Enter") return false;
 | 
				
			||||||
 | 
					    if (e.key === "Enter" && (e.nativeEvent.isComposing || isComposing.current))
 | 
				
			||||||
 | 
					      return false;
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      (config.submitKey === SubmitKey.AltEnter && e.altKey) ||
 | 
				
			||||||
 | 
					      (config.submitKey === SubmitKey.CtrlEnter && e.ctrlKey) ||
 | 
				
			||||||
 | 
					      (config.submitKey === SubmitKey.ShiftEnter && e.shiftKey) ||
 | 
				
			||||||
 | 
					      (config.submitKey === SubmitKey.MetaEnter && e.metaKey) ||
 | 
				
			||||||
 | 
					      (config.submitKey === SubmitKey.Enter &&
 | 
				
			||||||
 | 
					        !e.altKey &&
 | 
				
			||||||
 | 
					        !e.ctrlKey &&
 | 
				
			||||||
 | 
					        !e.shiftKey &&
 | 
				
			||||||
 | 
					        !e.metaKey)
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    submitKey,
 | 
				
			||||||
 | 
					    shouldSubmit,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										48
									
								
								app/hooks/useSwitchTheme.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,48 @@
 | 
				
			|||||||
 | 
					import { useLayoutEffect } from "react";
 | 
				
			||||||
 | 
					import { Theme, useAppConfig } from "@/app/store/config";
 | 
				
			||||||
 | 
					import { getCSSVar } from "../utils";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const DARK_CLASS = "dark-new";
 | 
				
			||||||
 | 
					const LIGHT_CLASS = "light-new";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function useSwitchTheme() {
 | 
				
			||||||
 | 
					  const config = useAppConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useLayoutEffect(() => {
 | 
				
			||||||
 | 
					    document.body.classList.remove(DARK_CLASS);
 | 
				
			||||||
 | 
					    document.body.classList.remove(LIGHT_CLASS);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (config.theme === Theme.Dark) {
 | 
				
			||||||
 | 
					      document.body.classList.add(DARK_CLASS);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      document.body.classList.add(LIGHT_CLASS);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [config.theme]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useLayoutEffect(() => {
 | 
				
			||||||
 | 
					    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]);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										69
									
								
								app/hooks/useUploadImage.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,69 @@
 | 
				
			|||||||
 | 
					import { compressImage } from "@/app/utils";
 | 
				
			||||||
 | 
					import { useCallback, useRef } from "react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface UseUploadImageOptions {
 | 
				
			||||||
 | 
					  setUploading?: (v: boolean) => void;
 | 
				
			||||||
 | 
					  emitImages?: (imgs: string[]) => void;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function useUploadImage(
 | 
				
			||||||
 | 
					  attachImages: string[],
 | 
				
			||||||
 | 
					  options: UseUploadImageOptions,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					  const attachImagesRef = useRef<string[]>([]);
 | 
				
			||||||
 | 
					  const optionsRef = useRef<UseUploadImageOptions>({});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  attachImagesRef.current = attachImages;
 | 
				
			||||||
 | 
					  optionsRef.current = options;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const uploadImage = useCallback(async function uploadImage() {
 | 
				
			||||||
 | 
					    const images: string[] = [];
 | 
				
			||||||
 | 
					    images.push(...attachImagesRef.current);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const { setUploading, emitImages } = optionsRef.current;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    images.push(
 | 
				
			||||||
 | 
					      ...(await new Promise<string[]>((res, rej) => {
 | 
				
			||||||
 | 
					        const fileInput = document.createElement("input");
 | 
				
			||||||
 | 
					        fileInput.type = "file";
 | 
				
			||||||
 | 
					        fileInput.accept =
 | 
				
			||||||
 | 
					          "image/png, image/jpeg, image/webp, image/heic, image/heif";
 | 
				
			||||||
 | 
					        fileInput.multiple = true;
 | 
				
			||||||
 | 
					        fileInput.onchange = (event: any) => {
 | 
				
			||||||
 | 
					          setUploading?.(true);
 | 
				
			||||||
 | 
					          const files = event.target.files;
 | 
				
			||||||
 | 
					          const imagesData: string[] = [];
 | 
				
			||||||
 | 
					          for (let i = 0; i < files.length; i++) {
 | 
				
			||||||
 | 
					            const file = event.target.files[i];
 | 
				
			||||||
 | 
					            compressImage(file, 256 * 1024)
 | 
				
			||||||
 | 
					              .then((dataUrl) => {
 | 
				
			||||||
 | 
					                imagesData.push(dataUrl);
 | 
				
			||||||
 | 
					                if (
 | 
				
			||||||
 | 
					                  imagesData.length === 3 ||
 | 
				
			||||||
 | 
					                  imagesData.length === files.length
 | 
				
			||||||
 | 
					                ) {
 | 
				
			||||||
 | 
					                  setUploading?.(false);
 | 
				
			||||||
 | 
					                  res(imagesData);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					              })
 | 
				
			||||||
 | 
					              .catch((e) => {
 | 
				
			||||||
 | 
					                setUploading?.(false);
 | 
				
			||||||
 | 
					                rej(e);
 | 
				
			||||||
 | 
					              });
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        fileInput.click();
 | 
				
			||||||
 | 
					      })),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const imagesLength = images.length;
 | 
				
			||||||
 | 
					    if (imagesLength > 3) {
 | 
				
			||||||
 | 
					      images.splice(3, imagesLength - 3);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    emitImages?.(images);
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    uploadImage,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										43
									
								
								app/hooks/useWindowSize.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,43 @@
 | 
				
			|||||||
 | 
					import { useLayoutEffect, useRef, useState } from "react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Size = {
 | 
				
			||||||
 | 
					  width: number;
 | 
				
			||||||
 | 
					  height: number;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function useWindowSize(callback?: (size: Size) => void) {
 | 
				
			||||||
 | 
					  const callbackRef = useRef<typeof callback>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  callbackRef.current = callback;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [size, setSize] = useState({
 | 
				
			||||||
 | 
					    width: window.innerWidth,
 | 
				
			||||||
 | 
					    height: window.innerHeight,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useLayoutEffect(() => {
 | 
				
			||||||
 | 
					    const onResize = () => {
 | 
				
			||||||
 | 
					      callbackRef.current?.({
 | 
				
			||||||
 | 
					        width: window.innerWidth,
 | 
				
			||||||
 | 
					        height: window.innerHeight,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      setSize({
 | 
				
			||||||
 | 
					        width: window.innerWidth,
 | 
				
			||||||
 | 
					        height: window.innerHeight,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    window.addEventListener("resize", onResize);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    callback?.({
 | 
				
			||||||
 | 
					      width: window.innerWidth,
 | 
				
			||||||
 | 
					      height: window.innerHeight,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return () => {
 | 
				
			||||||
 | 
					      window.removeEventListener("resize", onResize);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return size;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										5
									
								
								app/icons/addCircle.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -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  | 
							
								
								
									
										3
									
								
								app/icons/addIcon.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none">
 | 
				
			||||||
 | 
					<path fill-rule="evenodd" clip-rule="evenodd" d="M8.91663 0.666504C4.36028 0.666504 0.666626 4.36015 0.666626 8.9165V15.5832C0.666626 16.4576 1.37551 17.1665 2.24996 17.1665H8.91663C13.473 17.1665 17.1666 13.4729 17.1666 8.9165C17.1666 4.36015 13.473 0.666504 8.91663 0.666504ZM8.50004 5.25C8.91425 5.25 9.25004 5.58579 9.25004 6V8.16667H11.4167C11.8309 8.16667 12.1667 8.50245 12.1667 8.91667C12.1667 9.33088 11.8309 9.66667 11.4167 9.66667H9.25004V11.8333C9.25004 12.2475 8.91425 12.5833 8.50004 12.5833C8.08583 12.5833 7.75004 12.2475 7.75004 11.8333V9.66667H5.58333C5.16912 9.66667 4.83333 9.33088 4.83333 8.91667C4.83333 8.50245 5.16912 8.16667 5.58333 8.16667H7.75004V6C7.75004 5.58579 8.08583 5.25 8.50004 5.25Z" fill="#2E42F3"/>
 | 
				
			||||||
 | 
					</svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 839 B  | 
							
								
								
									
										9
									
								
								app/icons/assistantActive.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
 | 
				
			||||||
 | 
					<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_434_27904)"/>
 | 
				
			||||||
 | 
					<defs>
 | 
				
			||||||
 | 
					<linearGradient id="paint0_linear_434_27904" 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: 1021 B  | 
							
								
								
									
										3
									
								
								app/icons/assistantInactive.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -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: 792 B  | 
							
								
								
									
										9
									
								
								app/icons/assistantMobileActive.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -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  | 
							
								
								
									
										7
									
								
								app/icons/assistantMobileInactive.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
 | 
				
			||||||
 | 
					<mask id="path-1-inside-1_769_8890" fill="white">
 | 
				
			||||||
 | 
					<path fill-rule="evenodd" clip-rule="evenodd" d="M4.5 2C3.39543 2 2.5 2.89543 2.5 4V11.8413H2.50123C2.50041 11.8941 2.5 11.947 2.5 12C2.5 17.5228 6.97715 22 12.5 22C18.0228 22 22.5 17.5228 22.5 12C22.5 6.5319 18.1112 2.08887 12.6639 2.00132V2H12.5H4.5Z"/>
 | 
				
			||||||
 | 
					</mask>
 | 
				
			||||||
 | 
					<path d="M2.5 11.8413H0.7V13.6413H2.5V11.8413ZM2.50123 11.8413L4.30102 11.8693L4.32946 10.0413H2.50123V11.8413ZM12.6639 2.00132H10.8639V3.77262L12.635 3.80108L12.6639 2.00132ZM12.6639 2H14.4639V0.2H12.6639V2ZM4.3 4C4.3 3.88954 4.38954 3.8 4.5 3.8V0.2C2.40132 0.2 0.7 1.90131 0.7 4H4.3ZM4.3 11.8413V4H0.7V11.8413H4.3ZM2.50123 10.0413H2.5V13.6413H2.50123V10.0413ZM4.3 12C4.3 11.9563 4.30034 11.9128 4.30102 11.8693L0.701452 11.8133C0.700485 11.8754 0.7 11.9377 0.7 12H4.3ZM12.5 20.2C7.97126 20.2 4.3 16.5287 4.3 12H0.7C0.7 18.517 5.98304 23.8 12.5 23.8V20.2ZM20.7 12C20.7 16.5287 17.0287 20.2 12.5 20.2V23.8C19.017 23.8 24.3 18.517 24.3 12H20.7ZM12.635 3.80108C17.1011 3.87287 20.7 7.51632 20.7 12H24.3C24.3 5.54748 19.1212 0.30487 12.6929 0.201549L12.635 3.80108ZM10.8639 2V2.00132H14.4639V2H10.8639ZM12.5 3.8H12.6639V0.2H12.5V3.8ZM4.5 3.8H12.5V0.2H4.5V3.8Z" fill="#A5A5B3" mask="url(#path-1-inside-1_769_8890)"/>
 | 
				
			||||||
 | 
					<path opacity="0.89" fill-rule="evenodd" clip-rule="evenodd" d="M9.30324 8.55762C8.6242 8.55762 8.07373 9.10809 8.07373 9.78713V11.2625C8.07373 11.9416 8.6242 12.492 9.30324 12.492C9.98228 12.492 10.5327 11.9416 10.5327 11.2625V9.78713C10.5327 9.10809 9.98228 8.55762 9.30324 8.55762ZM15.6968 8.55762C15.0178 8.55762 14.4673 9.10809 14.4673 9.78713V11.2625C14.4673 11.9416 15.0178 12.492 15.6968 12.492C16.3758 12.492 16.9263 11.9416 16.9263 11.2625V9.78713C16.9263 9.10809 16.3758 8.55762 15.6968 8.55762Z" fill="#A5A5B3"/>
 | 
				
			||||||
 | 
					</svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 1.8 KiB  | 
							
								
								
									
										3
									
								
								app/icons/bottomArrow.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -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  | 
							
								
								
									
										3
									
								
								app/icons/closeIcon.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					<svg width="16" height="28" viewBox="0 0 16 28" fill="none" xmlns="http://www.w3.org/2000/svg">
 | 
				
			||||||
 | 
					<path fill-rule="evenodd" clip-rule="evenodd" d="M13.1486 20.0156C13.388 20.255 13.7762 20.255 14.0156 20.0156C14.255 19.7762 14.255 19.388 14.0156 19.1486L8.86702 14L14.0157 8.85133C14.2551 8.6119 14.2551 8.22371 14.0157 7.98428C13.7762 7.74486 13.3881 7.74486 13.1486 7.98428L7.99998 13.1329L2.8513 7.98426C2.61187 7.74483 2.22368 7.74483 1.98426 7.98426C1.74483 8.22368 1.74483 8.61187 1.98426 8.8513L7.13294 14L1.98432 19.1486C1.74489 19.388 1.74489 19.7762 1.98432 20.0156C2.22374 20.2551 2.61193 20.2551 2.85136 20.0156L7.99998 14.867L13.1486 20.0156Z" fill="#606078"/>
 | 
				
			||||||
 | 
					</svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 679 B  | 
							
								
								
									
										3
									
								
								app/icons/comandIcon.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
 | 
				
			||||||
 | 
					<path fill-rule="evenodd" clip-rule="evenodd" d="M5 1.75C3.21079 1.75 1.75 3.21079 1.75 5C1.75 6.78921 3.21079 8.25 5 8.25H6.75V11.75H5C3.21079 11.75 1.75 13.2108 1.75 15C1.75 16.7892 3.21079 18.25 5 18.25C6.78921 18.25 8.25 16.7892 8.25 15V13.25H11.75V15C11.75 16.7892 13.2108 18.25 15 18.25C16.7892 18.25 18.25 16.7892 18.25 15C18.25 13.2108 16.7892 11.75 15 11.75H13.25V8.25H15C16.7892 8.25 18.25 6.78921 18.25 5C18.25 3.21079 16.7892 1.75 15 1.75C13.2108 1.75 11.75 3.21079 11.75 5V6.75H8.25V5C8.25 3.21079 6.78921 1.75 5 1.75ZM3.25 5C3.25 4.03921 4.03921 3.25 5 3.25C5.96079 3.25 6.75 4.03921 6.75 5V6.75H5C4.03921 6.75 3.25 5.96079 3.25 5ZM8.25 8.25V11.75H11.75V8.25H8.25ZM5 13.25C4.03921 13.25 3.25 14.0392 3.25 15C3.25 15.9608 4.03921 16.75 5 16.75C5.96079 16.75 6.75 15.9608 6.75 15V13.25H5ZM15 3.25C14.0392 3.25 13.25 4.03921 13.25 5V6.75H15C15.9608 6.75 16.75 5.96079 16.75 5C16.75 4.03921 15.9608 3.25 15 3.25ZM13.25 15V13.25H15C15.9608 13.25 16.75 14.0392 16.75 15C16.75 15.9608 15.9608 16.75 15 16.75C14.0392 16.75 13.25 15.9608 13.25 15Z" fill="#606078"/>
 | 
				
			||||||
 | 
					</svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 1.1 KiB  | 
							
								
								
									
										6
									
								
								app/icons/command&enterIcon.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,6 @@
 | 
				
			|||||||
 | 
					<svg width="36" height="20" viewBox="0 0 36 20" fill="none" xmlns="http://www.w3.org/2000/svg">
 | 
				
			||||||
 | 
					<rect width="36" height="20" rx="4" fill="#F0F0F3"/>
 | 
				
			||||||
 | 
					<path fill-rule="evenodd" clip-rule="evenodd" d="M5.5 7.04688C5.5 6.19256 6.19256 5.5 7.04688 5.5C7.90119 5.5 8.59375 6.19256 8.59375 7.04688V7.65625H11.4062V7.04688C11.4062 6.19255 12.0988 5.5 12.9531 5.5C13.8074 5.5 14.5 6.19256 14.5 7.04688C14.5 7.90118 13.8074 8.59375 12.9531 8.59375H12.3438V11.3594H12.9531C13.8074 11.3594 14.5 12.0519 14.5 12.9062C14.5 13.7606 13.8074 14.4531 12.9531 14.4531C12.0988 14.4531 11.4062 13.7606 11.4062 12.9062V12.2969H8.59375V12.9062C8.59375 13.7606 7.90119 14.4531 7.04688 14.4531C6.19255 14.4531 5.5 13.7606 5.5 12.9062C5.5 12.0519 6.19256 11.3594 7.04688 11.3594H7.65625V8.59375H7.04688C6.19256 8.59375 5.5 7.90119 5.5 7.04688ZM7.04688 6.4375C6.71033 6.4375 6.4375 6.71033 6.4375 7.04688C6.4375 7.38342 6.71033 7.65625 7.04688 7.65625H7.65625V7.04688C7.65625 6.71033 7.38342 6.4375 7.04688 6.4375ZM7.04688 12.2969C6.71032 12.2969 6.4375 12.5697 6.4375 12.9062C6.4375 13.2428 6.71033 13.5156 7.04688 13.5156C7.38342 13.5156 7.65625 13.2428 7.65625 12.9062V12.2969H7.04688ZM8.59375 11.3594V8.59375H11.4062V11.3594H8.59375ZM12.9531 6.4375C12.6166 6.4375 12.3438 6.71033 12.3438 7.04688V7.65625H12.9531C13.2897 7.65625 13.5625 7.38342 13.5625 7.04688C13.5625 6.71032 13.2897 6.4375 12.9531 6.4375ZM12.3438 12.9062V12.2969H12.9531C13.2897 12.2969 13.5625 12.5697 13.5625 12.9062C13.5625 13.2428 13.2897 13.5156 12.9531 13.5156C12.6166 13.5156 12.3438 13.2428 12.3438 12.9062Z" fill="#88889A"/>
 | 
				
			||||||
 | 
					<path fill-rule="evenodd" clip-rule="evenodd" d="M22.0156 12.8125C22.0156 13.0714 22.2255 13.2812 22.4844 13.2812H27.5938C29.4577 13.2812 30.9688 11.7702 30.9688 9.90625C30.9688 8.04229 29.4577 6.53125 27.5938 6.53125H22.4844C22.2255 6.53125 22.0156 6.74112 22.0156 7C22.0156 7.25888 22.2255 7.46875 22.4844 7.46875H27.5938C28.9399 7.46875 30.0313 8.56006 30.0313 9.90625C30.0313 11.2524 28.9399 12.3438 27.5938 12.3438H22.4844C22.2255 12.3438 22.0156 12.5536 22.0156 12.8125Z" fill="#88889A"/>
 | 
				
			||||||
 | 
					<path fill-rule="evenodd" clip-rule="evenodd" d="M23.6215 14.8224C23.7996 14.6344 23.7916 14.3378 23.6036 14.1597L22.1816 12.8125L21.5368 13.4931L22.9589 14.8403C23.1468 15.0183 23.4435 15.0103 23.6215 14.8224ZM21.5368 13.4931C21.1465 13.1233 21.1465 12.5017 21.5368 12.1319L22.9589 10.7847C23.1468 10.6067 23.4435 10.6147 23.6215 10.8026C23.7996 10.9906 23.7916 11.2872 23.6036 11.4653L22.1816 12.8125L21.5368 13.4931Z" fill="#88889A"/>
 | 
				
			||||||
 | 
					</svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 2.5 KiB  | 
							
								
								
									
										8
									
								
								app/icons/configIcon.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,8 @@
 | 
				
			|||||||
 | 
					<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="M12.5834 5.4165C12.5834 5.00229 12.9192 4.6665 13.3334 4.6665H18.3334C18.7476 4.6665 19.0834 5.00229 19.0834 5.4165C19.0834 5.83072 18.7476 6.1665 18.3334 6.1665H13.3334C12.9192 6.1665 12.5834 5.83072 12.5834 5.4165Z" fill="#606078"/>
 | 
				
			||||||
 | 
					<path fill-rule="evenodd" clip-rule="evenodd" d="M0.916687 5.4165C0.916687 5.00229 1.25247 4.6665 1.66669 4.6665H5.00002C5.41423 4.6665 5.75002 5.00229 5.75002 5.4165C5.75002 5.83072 5.41423 6.1665 5.00002 6.1665H1.66669C1.25247 6.1665 0.916687 5.83072 0.916687 5.4165Z" fill="#606078"/>
 | 
				
			||||||
 | 
					<path fill-rule="evenodd" clip-rule="evenodd" d="M8.33335 3.25C7.13674 3.25 6.16669 4.22005 6.16669 5.41667C6.16669 6.61328 7.13674 7.58333 8.33335 7.58333C9.52997 7.58333 10.5 6.61328 10.5 5.41667C10.5 4.22005 9.52997 3.25 8.33335 3.25ZM4.66669 5.41667C4.66669 3.39162 6.30831 1.75 8.33335 1.75C10.3584 1.75 12 3.39162 12 5.41667C12 7.44171 10.3584 9.08333 8.33335 9.08333C6.30831 9.08333 4.66669 7.44171 4.66669 5.41667Z" fill="#606078"/>
 | 
				
			||||||
 | 
					<path fill-rule="evenodd" clip-rule="evenodd" d="M14.25 14.5835C14.25 14.1693 14.5858 13.8335 15 13.8335H18.3333C18.7475 13.8335 19.0833 14.1693 19.0833 14.5835C19.0833 14.9977 18.7475 15.3335 18.3333 15.3335H15C14.5858 15.3335 14.25 14.9977 14.25 14.5835Z" fill="#606078"/>
 | 
				
			||||||
 | 
					<path fill-rule="evenodd" clip-rule="evenodd" d="M0.916687 14.5835C0.916687 14.1693 1.25247 13.8335 1.66669 13.8335H6.66669C7.0809 13.8335 7.41669 14.1693 7.41669 14.5835C7.41669 14.9977 7.0809 15.3335 6.66669 15.3335H1.66669C1.25247 15.3335 0.916687 14.9977 0.916687 14.5835Z" fill="#606078"/>
 | 
				
			||||||
 | 
					<path fill-rule="evenodd" clip-rule="evenodd" d="M11.6667 12.4165C10.47 12.4165 9.5 13.3866 9.5 14.5832C9.5 15.7798 10.47 16.7498 11.6667 16.7498C12.8633 16.7498 13.8333 15.7798 13.8333 14.5832C13.8333 13.3866 12.8633 12.4165 11.6667 12.4165ZM8 14.5832C8 12.5581 9.64162 10.9165 11.6667 10.9165C13.6917 10.9165 15.3333 12.5581 15.3333 14.5832C15.3333 16.6082 13.6917 18.2498 11.6667 18.2498C9.64162 18.2498 8 16.6082 8 14.5832Z" fill="#606078"/>
 | 
				
			||||||
 | 
					</svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 2.1 KiB  | 
							
								
								
									
										8
									
								
								app/icons/configIcon2.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,8 @@
 | 
				
			|||||||
 | 
					<svg xmlns="http://www.w3.org/2000/svg" width="16" height="14" viewBox="0 0 16 14" fill="none">
 | 
				
			||||||
 | 
					<path fill-rule="evenodd" clip-rule="evenodd" d="M10.0667 3.33315C10.0667 3.00178 10.3353 2.73315 10.6667 2.73315H14.6667C14.998 2.73315 15.2667 3.00178 15.2667 3.33315C15.2667 3.66453 14.998 3.93315 14.6667 3.93315H10.6667C10.3353 3.93315 10.0667 3.66453 10.0667 3.33315Z" fill="#606078"/>
 | 
				
			||||||
 | 
					<path fill-rule="evenodd" clip-rule="evenodd" d="M0.733276 3.33315C0.733276 3.00178 1.00191 2.73315 1.33328 2.73315H3.99994C4.33131 2.73315 4.59994 3.00178 4.59994 3.33315C4.59994 3.66453 4.33131 3.93315 3.99994 3.93315H1.33328C1.00191 3.93315 0.733276 3.66453 0.733276 3.33315Z" fill="#606078"/>
 | 
				
			||||||
 | 
					<path fill-rule="evenodd" clip-rule="evenodd" d="M6.66661 1.5999C5.70932 1.5999 4.93328 2.37594 4.93328 3.33324C4.93328 4.29053 5.70932 5.06657 6.66661 5.06657C7.6239 5.06657 8.39994 4.29053 8.39994 3.33324C8.39994 2.37594 7.6239 1.5999 6.66661 1.5999ZM3.73328 3.33324C3.73328 1.7132 5.04657 0.399902 6.66661 0.399902C8.28665 0.399902 9.59994 1.7132 9.59994 3.33324C9.59994 4.95327 8.28665 6.26657 6.66661 6.26657C5.04657 6.26657 3.73328 4.95327 3.73328 3.33324Z" fill="#606078"/>
 | 
				
			||||||
 | 
					<path fill-rule="evenodd" clip-rule="evenodd" d="M11.3999 10.6667C11.3999 10.3353 11.6685 10.0667 11.9999 10.0667H14.6666C14.9979 10.0667 15.2666 10.3353 15.2666 10.6667C15.2666 10.998 14.9979 11.2667 14.6666 11.2667H11.9999C11.6685 11.2667 11.3999 10.998 11.3999 10.6667Z" fill="#606078"/>
 | 
				
			||||||
 | 
					<path fill-rule="evenodd" clip-rule="evenodd" d="M0.733276 10.6667C0.733276 10.3353 1.00191 10.0667 1.33328 10.0667H5.33328C5.66465 10.0667 5.93328 10.3353 5.93328 10.6667C5.93328 10.998 5.66465 11.2667 5.33328 11.2667H1.33328C1.00191 11.2667 0.733276 10.998 0.733276 10.6667Z" fill="#606078"/>
 | 
				
			||||||
 | 
					<path fill-rule="evenodd" clip-rule="evenodd" d="M9.33324 8.93315C8.37594 8.93315 7.5999 9.70919 7.5999 10.6665C7.5999 11.6238 8.37594 12.3998 9.33324 12.3998C10.2905 12.3998 11.0666 11.6238 11.0666 10.6665C11.0666 9.70919 10.2905 8.93315 9.33324 8.93315ZM6.3999 10.6665C6.3999 9.04645 7.7132 7.73315 9.33324 7.73315C10.9533 7.73315 12.2666 9.04645 12.2666 10.6665C12.2666 12.2865 10.9533 13.5998 9.33324 13.5998C7.7132 13.5998 6.3999 12.2865 6.3999 10.6665Z" fill="#606078"/>
 | 
				
			||||||
 | 
					</svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 2.2 KiB  | 
							
								
								
									
										4
									
								
								app/icons/copyRequestIcon.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,4 @@
 | 
				
			|||||||
 | 
					<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
 | 
				
			||||||
 | 
					<path fill-rule="evenodd" clip-rule="evenodd" d="M11 2.625C11.7248 2.625 12.2055 2.62633 12.5626 2.67435C12.9018 2.71995 13.0345 2.79709 13.1187 2.88128C13.2029 2.96547 13.2801 3.09821 13.3257 3.43737C13.3737 3.79452 13.375 4.27523 13.375 5V8.94186C13.375 9.44328 13.3743 9.7756 13.3503 10.0289C13.3272 10.273 13.2868 10.3787 13.2469 10.4442C13.1744 10.563 13.0746 10.6628 12.9558 10.7353C12.8904 10.7752 12.7846 10.8156 12.5406 10.8387C12.4195 10.8502 12.2804 10.8563 12.1134 10.8596L12.1134 7.46904C12.1134 6.7983 12.1134 6.23273 12.0529 5.78244C11.9886 5.30467 11.8461 4.86418 11.491 4.50903C11.1358 4.15388 10.6953 4.01136 10.2176 3.94712C9.76726 3.88658 9.20172 3.8866 8.53096 3.88663L5.1387 3.88663C5.14095 3.71923 5.1456 3.58282 5.15527 3.46541C5.17281 3.25236 5.20362 3.15794 5.23345 3.09992C5.31695 2.93752 5.44915 2.80532 5.61154 2.72183C5.66957 2.69199 5.76399 2.66118 5.97704 2.64364C6.19747 2.6255 6.48488 2.625 6.91861 2.625H11ZM3.88843 3.89878C3.89083 3.69998 3.89643 3.52141 3.90949 3.36285C3.93344 3.07185 3.98563 2.79318 4.12178 2.52836C4.32454 2.13398 4.64561 1.81292 5.03999 1.61015C5.30481 1.474 5.58348 1.42181 5.87447 1.39786C6.15232 1.37498 6.49155 1.37499 6.89348 1.375L11.0426 1.375C11.7133 1.37497 12.2789 1.37495 12.7292 1.43549C13.207 1.49973 13.6475 1.64225 14.0026 1.9974C14.3578 2.35255 14.5003 2.79304 14.5645 3.27081C14.625 3.7211 14.625 4.28663 14.625 4.95738L14.625 8.97099C14.625 9.43552 14.625 9.82753 14.5948 10.1469C14.563 10.4821 14.4935 10.801 14.3139 11.0954C14.1378 11.3839 13.8955 11.6262 13.607 11.8023C13.3126 11.9819 12.9937 12.0514 12.6585 12.0831C12.4924 12.0989 12.3067 12.1064 12.1013 12.11C12.0927 12.3358 12.078 12.5424 12.0529 12.7292C11.9886 13.207 11.8461 13.6475 11.491 14.0026C11.1358 14.3578 10.6953 14.5003 10.2176 14.5645C9.76728 14.625 9.20176 14.625 8.53103 14.625H4.95735C4.28662 14.625 3.7211 14.625 3.27081 14.5645C2.79304 14.5003 2.35255 14.3578 1.9974 14.0026C1.64225 13.6475 1.49973 13.207 1.43549 12.7292C1.37495 12.2789 1.37497 11.7133 1.375 11.0426V7.46905C1.37497 6.79828 1.37495 6.23274 1.43549 5.78244C1.49973 5.30467 1.64225 4.86418 1.9974 4.50903C2.35255 4.15388 2.79304 4.01136 3.27081 3.94712C3.45718 3.92206 3.66329 3.90738 3.88843 3.89878ZM2.88128 5.39291C2.96547 5.30872 3.09821 5.23157 3.43737 5.18597C3.79452 5.13796 4.27523 5.13663 5 5.13663H8.48837C9.21315 5.13663 9.69386 5.13796 10.051 5.18597C10.3902 5.23157 10.5229 5.30872 10.6071 5.39291C10.6913 5.4771 10.7684 5.60984 10.814 5.949C10.862 6.30615 10.8634 6.78685 10.8634 7.51163V11C10.8634 11.7248 10.862 12.2055 10.814 12.5626C10.7684 12.9018 10.6913 13.0345 10.6071 13.1187C10.5229 13.2029 10.3902 13.2801 10.051 13.3257C9.69386 13.3737 9.21315 13.375 8.48837 13.375H5C4.27523 13.375 3.79452 13.3737 3.43737 13.3257C3.09821 13.2801 2.96547 13.2029 2.88128 13.1187C2.79709 13.0345 2.71995 12.9018 2.67435 12.5626C2.62633 12.2055 2.625 11.7248 2.625 11V7.51163C2.625 6.78685 2.62633 6.30615 2.67435 5.949C2.71995 5.60984 2.79709 5.4771 2.88128 5.39291Z" fill="#717187"/>
 | 
				
			||||||
 | 
					</svg>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 3.0 KiB  | 
							
								
								
									
										3
									
								
								app/icons/darkIcon.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
 | 
				
			||||||
 | 
					<path fill-rule="evenodd" clip-rule="evenodd" d="M6.91496 1.69334C7.36752 2.11888 7.25215 2.75585 7.05615 3.20591L7.05576 3.20682C6.76923 3.86173 6.61177 4.57947 6.61761 5.33877L6.61763 5.34085C6.62929 8.15802 8.97294 10.5654 11.8442 10.6828L11.8446 10.6828C12.2688 10.7005 12.6741 10.6711 13.0675 10.6007C13.3386 10.5513 13.6087 10.5418 13.8548 10.5992C14.1063 10.6578 14.3642 10.7963 14.525 11.0595C14.6852 11.3216 14.6914 11.6128 14.6324 11.861C14.5744 12.1051 14.4476 12.3415 14.2854 12.5613C12.887 14.4744 10.5595 15.6853 7.96279 15.5727L7.96243 15.5727C4.27649 15.4106 1.1912 12.463 0.935785 8.81417C0.702559 5.58027 2.62215 2.76506 5.4097 1.58951C5.85844 1.39947 6.48064 1.28494 6.91496 1.69334ZM6.03358 2.60725C5.98613 2.61983 5.92991 2.63882 5.86504 2.66633L5.864 2.66677C3.50753 3.66053 1.90679 6.02819 2.10194 8.73064L2.10203 8.73198C2.3146 11.7729 4.90597 14.268 8.0138 14.4047C10.2099 14.4998 12.1686 13.4768 13.3423 11.8703L13.3441 11.8679C13.3819 11.8167 13.4114 11.7709 13.4342 11.7308C13.3894 11.7337 13.3369 11.74 13.2762 11.751L13.2744 11.7514C12.7978 11.8367 12.3071 11.8722 11.7964 11.851C8.31568 11.7086 5.46338 8.80347 5.4485 5.34673C5.4415 4.41787 5.63468 3.53833 5.98441 2.73875C6.00608 2.68894 6.02205 2.64512 6.03358 2.60725Z" fill="#18182A"/>
 | 
				
			||||||
 | 
					</svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 1.3 KiB  | 
							
								
								
									
										4
									
								
								app/icons/deleteChatIcon.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,4 @@
 | 
				
			|||||||
 | 
					<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
 | 
				
			||||||
 | 
					<rect width="16" height="16" rx="8" fill="white"/>
 | 
				
			||||||
 | 
					<path fill-rule="evenodd" clip-rule="evenodd" d="M5.77152 1.59431C6.13008 1.05646 6.73373 0.733398 7.38015 0.733398H8.61966C9.26607 0.733398 9.86972 1.05646 10.2283 1.59431L10.9877 2.7334H13.9999C14.3313 2.7334 14.5999 3.00203 14.5999 3.3334C14.5999 3.66477 14.3313 3.9334 13.9999 3.9334H1.9999C1.66853 3.9334 1.3999 3.66477 1.3999 3.3334C1.3999 3.00203 1.66853 2.7334 1.9999 2.7334H5.01213L5.77152 1.59431ZM6.45435 2.7334H9.54546L9.22983 2.25995C9.09382 2.05594 8.86485 1.9334 8.61966 1.9334H7.38015C7.13496 1.9334 6.90599 2.05594 6.76998 2.25995L6.45435 2.7334ZM3.33324 4.7334C3.66461 4.7334 3.93324 5.00203 3.93324 5.3334V12.0001C3.93324 13.1415 4.85851 14.0667 5.9999 14.0667H9.9999C11.1413 14.0667 12.0666 13.1415 12.0666 12.0001V5.3334C12.0666 5.00203 12.3352 4.7334 12.6666 4.7334C12.9979 4.7334 13.2666 5.00203 13.2666 5.3334V12.0001C13.2666 13.8042 11.804 15.2667 9.9999 15.2667H5.9999C4.19577 15.2667 2.73324 13.8042 2.73324 12.0001V5.3334C2.73324 5.00203 3.00186 4.7334 3.33324 4.7334ZM6.66657 6.7334C6.99794 6.7334 7.26657 7.00203 7.26657 7.3334V11.3334C7.26657 11.6648 6.99794 11.9334 6.66657 11.9334C6.3352 11.9334 6.06657 11.6648 6.06657 11.3334V7.3334C6.06657 7.00203 6.3352 6.7334 6.66657 6.7334ZM9.33324 6.7334C9.66461 6.7334 9.93324 7.00203 9.93324 7.3334V11.3334C9.93324 11.6648 9.66461 11.9334 9.33324 11.9334C9.00186 11.9334 8.73324 11.6648 8.73324 11.3334V7.3334C8.73324 7.00203 9.00186 6.7334 9.33324 6.7334Z" fill="#FF5454"/>
 | 
				
			||||||
 | 
					</svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 1.6 KiB  | 
							
								
								
									
										4
									
								
								app/icons/deleteIcon.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,4 @@
 | 
				
			|||||||
 | 
					<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
 | 
				
			||||||
 | 
					<rect width="28" height="28" rx="8" fill="none" fill-opacity="0.05"/>
 | 
				
			||||||
 | 
					<path fill-rule="evenodd" clip-rule="evenodd" d="M15.334 8.65915C15.334 7.92647 14.7371 7.33252 14.0007 7.33252C13.2644 7.33252 12.6675 7.92647 12.6675 8.65915L12.6675 8.67239C12.6675 9.40507 13.2644 9.99902 14.0007 9.99902C14.7371 9.99902 15.334 9.40507 15.334 8.67239L15.334 8.65915ZM14.0007 18.0011C14.7371 18.0011 15.334 18.5951 15.334 19.3278L15.334 19.341C15.334 20.0737 14.7371 20.6676 14.0007 20.6676C13.2644 20.6676 12.6675 20.0737 12.6675 19.341L12.6675 19.3278C12.6675 18.5951 13.2644 18.0011 14.0007 18.0011ZM14.0007 12.6668C14.7371 12.6668 15.334 13.2608 15.334 13.9935L15.334 14.0067C15.334 14.7394 14.7371 15.3333 14.0007 15.3333C13.2644 15.3333 12.6675 14.7394 12.6675 14.0067L12.6675 13.9935C12.6675 13.2608 13.2644 12.6668 14.0007 12.6668Z" fill="#606078"/>
 | 
				
			||||||
 | 
					</svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 949 B  | 
							
								
								
									
										4
									
								
								app/icons/deleteRequestIcon.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,4 @@
 | 
				
			|||||||
 | 
					<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
 | 
				
			||||||
 | 
					<path fill-rule="evenodd" clip-rule="evenodd" d="M6.74419 1.375C6.39901 1.375 6.11919 1.65482 6.11919 2C6.11919 2.34518 6.39901 2.625 6.74419 2.625H9.25581C9.60099 2.625 9.88081 2.34518 9.88081 2C9.88081 1.65482 9.60099 1.375 9.25581 1.375H6.74419ZM2 3.39825C1.65482 3.39825 1.375 3.67808 1.375 4.02325C1.375 4.36843 1.65482 4.64825 2 4.64825H3.25873V12.5C3.25873 13.6736 4.21012 14.625 5.38373 14.625H10.6163C11.7899 14.625 12.7413 13.6736 12.7413 12.5V4.64825H14C14.3452 4.64825 14.625 4.36843 14.625 4.02325C14.625 3.67808 14.3452 3.39825 14 3.39825H12.1163H3.88373H2ZM4.50873 12.5V4.64825H11.4913V12.5C11.4913 12.9832 11.0995 13.375 10.6163 13.375H5.38373C4.90048 13.375 4.50873 12.9832 4.50873 12.5ZM8.625 6.88372C8.625 6.53854 8.34518 6.25872 8 6.25872C7.65482 6.25872 7.375 6.53854 7.375 6.88372V10.7907C7.375 11.1359 7.65482 11.4157 8 11.4157C8.34518 11.4157 8.625 11.1359 8.625 10.7907V6.88372Z" fill="#717187"/>
 | 
				
			||||||
 | 
					</svg>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 1.0 KiB  | 
							
								
								
									
										9
									
								
								app/icons/discoverActive.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -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  | 
							
								
								
									
										3
									
								
								app/icons/discoverInactive.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
 | 
				
			||||||
 | 
					<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="#A5A5B3"/>
 | 
				
			||||||
 | 
					</svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 798 B  | 
							
								
								
									
										9
									
								
								app/icons/discoverMobileActive.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
 | 
				
			||||||
 | 
					<path d="M12.5 22C6.97727 22 2.5 17.5227 2.5 12C2.5 6.47727 6.97727 2 12.5 2C18.0227 2 22.5 6.47727 22.5 12C22.5 17.5227 18.0227 22 12.5 22ZM11.0036 10.0409C10.8013 10.1327 10.6397 10.2953 10.5491 10.4982L8.20364 15.7555C8.16577 15.8399 8.15455 15.9338 8.17148 16.0247C8.18841 16.1157 8.23269 16.1993 8.29839 16.2644C8.3641 16.3295 8.44811 16.373 8.5392 16.3891C8.63029 16.4052 8.72413 16.3932 8.80818 16.3545L14.0291 13.9664C14.2322 13.8735 14.394 13.7091 14.4836 13.5045L16.7655 8.29455C16.8024 8.21026 16.813 8.11673 16.7956 8.02634C16.7783 7.93594 16.734 7.85293 16.6684 7.78829C16.6029 7.72365 16.5193 7.68043 16.4287 7.66434C16.3381 7.64825 16.2447 7.66005 16.1609 7.69818L11.0036 10.0409Z" fill="url(#paint0_linear_769_8893)"/>
 | 
				
			||||||
 | 
					<defs>
 | 
				
			||||||
 | 
					<linearGradient id="paint0_linear_769_8893" x1="2.5" y1="12" x2="22.522" 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  | 
							
								
								
									
										8
									
								
								app/icons/discoverMobileInactive.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,8 @@
 | 
				
			|||||||
 | 
					<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
 | 
				
			||||||
 | 
					<mask id="path-1-inside-1_769_8003" fill="white">
 | 
				
			||||||
 | 
					<path fill-rule="evenodd" clip-rule="evenodd" d="M12.5001 21.1001C7.47444 21.1001 3.4001 17.0258 3.4001 12.0001C3.4001 6.97444 7.47444 2.9001 12.5001 2.9001C17.5258 2.9001 21.6001 6.97444 21.6001 12.0001C21.6001 17.0258 17.5258 21.1001 12.5001 21.1001ZM12.5001 22.9001C6.48032 22.9001 1.6001 18.0199 1.6001 12.0001C1.6001 5.98032 6.48032 1.1001 12.5001 1.1001C18.5199 1.1001 23.4001 5.98032 23.4001 12.0001C23.4001 18.0199 18.5199 22.9001 12.5001 22.9001ZM11.1296 10.1822C10.9423 10.2662 10.7926 10.4151 10.7087 10.6008L8.53701 15.4136C8.50194 15.4909 8.49155 15.5769 8.50723 15.6602C8.52291 15.7434 8.56391 15.82 8.62475 15.8796C8.68559 15.9392 8.76337 15.979 8.84772 15.9938C8.93206 16.0085 9.01895 15.9975 9.09677 15.9621L13.931 13.7758C14.119 13.6908 14.2688 13.5403 14.3518 13.353L16.4646 8.58346C16.4989 8.5063 16.5086 8.42068 16.4926 8.33793C16.4766 8.25517 16.4355 8.17918 16.3748 8.12C16.3141 8.06083 16.2367 8.02126 16.1528 8.00653C16.0689 7.9918 15.9824 8.0026 15.9049 8.03751L11.1296 10.1822Z"/>
 | 
				
			||||||
 | 
					</mask>
 | 
				
			||||||
 | 
					<path fill-rule="evenodd" clip-rule="evenodd" d="M12.5001 21.1001C7.47444 21.1001 3.4001 17.0258 3.4001 12.0001C3.4001 6.97444 7.47444 2.9001 12.5001 2.9001C17.5258 2.9001 21.6001 6.97444 21.6001 12.0001C21.6001 17.0258 17.5258 21.1001 12.5001 21.1001ZM12.5001 22.9001C6.48032 22.9001 1.6001 18.0199 1.6001 12.0001C1.6001 5.98032 6.48032 1.1001 12.5001 1.1001C18.5199 1.1001 23.4001 5.98032 23.4001 12.0001C23.4001 18.0199 18.5199 22.9001 12.5001 22.9001ZM11.1296 10.1822C10.9423 10.2662 10.7926 10.4151 10.7087 10.6008L8.53701 15.4136C8.50194 15.4909 8.49155 15.5769 8.50723 15.6602C8.52291 15.7434 8.56391 15.82 8.62475 15.8796C8.68559 15.9392 8.76337 15.979 8.84772 15.9938C8.93206 16.0085 9.01895 15.9975 9.09677 15.9621L13.931 13.7758C14.119 13.6908 14.2688 13.5403 14.3518 13.353L16.4646 8.58346C16.4989 8.5063 16.5086 8.42068 16.4926 8.33793C16.4766 8.25517 16.4355 8.17918 16.3748 8.12C16.3141 8.06083 16.2367 8.02126 16.1528 8.00653C16.0689 7.9918 15.9824 8.0026 15.9049 8.03751L11.1296 10.1822Z" fill="#A5A5B3"/>
 | 
				
			||||||
 | 
					<path d="M10.7087 10.6008L9.06823 9.86003L9.06803 9.86047L10.7087 10.6008ZM11.1296 10.1822L11.8662 11.8246L11.8671 11.8242L11.1296 10.1822ZM8.53701 15.4136L10.1761 16.1575L10.1777 16.154L8.53701 15.4136ZM8.50723 15.6602L10.2761 15.3271L10.2761 15.3271L8.50723 15.6602ZM8.62475 15.8796L7.36502 17.1653L7.36503 17.1653L8.62475 15.8796ZM8.84772 15.9938L9.1577 14.2207L9.15767 14.2207L8.84772 15.9938ZM9.09677 15.9621L8.35504 14.322L8.35205 14.3234L9.09677 15.9621ZM13.931 13.7758L13.1894 12.1357L13.1892 12.1357L13.931 13.7758ZM14.3518 13.353L15.9974 14.0825L15.9976 14.0821L14.3518 13.353ZM16.4646 8.58346L14.8194 7.85319L14.8189 7.85443L16.4646 8.58346ZM15.9049 8.03751L16.6423 9.67951L16.6436 9.67893L15.9049 8.03751ZM1.6001 12.0001C1.6001 18.0199 6.48032 22.9001 12.5001 22.9001V19.3001C8.46855 19.3001 5.2001 16.0316 5.2001 12.0001H1.6001ZM12.5001 1.1001C6.48032 1.1001 1.6001 5.98032 1.6001 12.0001H5.2001C5.2001 7.96855 8.46855 4.7001 12.5001 4.7001V1.1001ZM23.4001 12.0001C23.4001 5.98032 18.5199 1.1001 12.5001 1.1001V4.7001C16.5316 4.7001 19.8001 7.96855 19.8001 12.0001H23.4001ZM12.5001 22.9001C18.5199 22.9001 23.4001 18.0199 23.4001 12.0001H19.8001C19.8001 16.0316 16.5316 19.3001 12.5001 19.3001V22.9001ZM-0.199902 12.0001C-0.199902 19.014 5.48621 24.7001 12.5001 24.7001V21.1001C7.47444 21.1001 3.4001 17.0258 3.4001 12.0001H-0.199902ZM12.5001 -0.699902C5.48621 -0.699902 -0.199902 4.98621 -0.199902 12.0001H3.4001C3.4001 6.97444 7.47444 2.9001 12.5001 2.9001V-0.699902ZM25.2001 12.0001C25.2001 4.98621 19.514 -0.699902 12.5001 -0.699902V2.9001C17.5258 2.9001 21.6001 6.97444 21.6001 12.0001H25.2001ZM12.5001 24.7001C19.514 24.7001 25.2001 19.014 25.2001 12.0001H21.6001C21.6001 17.0258 17.5258 21.1001 12.5001 21.1001V24.7001ZM12.3492 11.3416C12.2506 11.5601 12.0769 11.7301 11.8662 11.8246L10.393 8.53983C9.80769 8.80234 9.33461 9.27013 9.06823 9.86003L12.3492 11.3416ZM10.1777 16.154L12.3494 11.3412L9.06803 9.86047L6.89631 14.6733L10.1777 16.154ZM10.2761 15.3271C10.3291 15.6082 10.2938 15.8983 10.1761 16.1575L6.89791 14.6698C6.71011 15.0836 6.65402 15.5456 6.73832 15.9933L10.2761 15.3271ZM9.88447 14.5938C10.0854 14.7907 10.2232 15.046 10.2761 15.3271L6.73832 15.9933C6.8226 16.4408 7.04246 16.8493 7.36502 17.1653L9.88447 14.5938ZM9.15767 14.2207C9.42992 14.2682 9.68384 14.3973 9.88446 14.5938L7.36503 17.1653C7.68733 17.4811 8.09683 17.6898 8.53777 17.7669L9.15767 14.2207ZM8.35205 14.3234C8.60481 14.2085 8.88558 14.1731 9.1577 14.2207L8.53773 17.7669C8.97854 17.8439 9.43308 17.7864 9.8415 17.6008L8.35205 14.3234ZM13.1892 12.1357L8.35504 14.322L9.83851 17.6022L14.6727 15.4159L13.1892 12.1357ZM12.7063 12.6236C12.804 12.4031 12.9778 12.2313 13.1894 12.1357L14.6726 15.4159C15.2602 15.1502 15.7337 14.6774 15.9974 14.0825L12.7063 12.6236ZM14.8189 7.85443L12.7061 12.624L15.9976 14.0821L18.1104 9.31249L14.8189 7.85443ZM14.7255 8.68043C14.6713 8.401 14.7045 8.11222 14.8194 7.85319L18.1098 9.31374C18.2933 8.90039 18.346 8.44037 18.2597 7.99543L14.7255 8.68043ZM15.118 9.4086C14.9177 9.21323 14.7796 8.95973 14.7255 8.68043L18.2597 7.99543C18.1735 7.55061 17.9533 7.14512 17.6316 6.83141L15.118 9.4086ZM15.8416 9.77942C15.5708 9.73188 15.3181 9.60373 15.118 9.4086L17.6316 6.83141C17.3102 6.51793 16.9027 6.31063 16.464 6.23364L15.8416 9.77942ZM16.6436 9.67893C16.3917 9.79231 16.1123 9.82694 15.8416 9.77942L16.464 6.23364C16.0255 6.15666 15.5732 6.2129 15.1661 6.39609L16.6436 9.67893ZM11.8671 11.8242L16.6423 9.67951L15.1674 6.39552L10.3922 8.5402L11.8671 11.8242Z" fill="#A5A5B3" mask="url(#path-1-inside-1_769_8003)"/>
 | 
				
			||||||
 | 
					</svg>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 5.6 KiB  | 
							
								
								
									
										3
									
								
								app/icons/downArrowIcon.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12" fill="none">
 | 
				
			||||||
 | 
					<path fill-rule="evenodd" clip-rule="evenodd" d="M1.16681 3.65466C1.34891 3.47064 1.6457 3.46909 1.82972 3.65119L5.67028 7.45175C5.85294 7.6325 6.14706 7.6325 6.32972 7.45175L10.1703 3.65119C10.3543 3.46909 10.6511 3.47064 10.8332 3.65466C11.0153 3.83867 11.0137 4.13546 10.8297 4.31756L6.98915 8.11812C6.44119 8.66037 5.55881 8.66038 5.01085 8.11812L1.17028 4.31756C0.986269 4.13546 0.984716 3.83867 1.16681 3.65466Z" fill="#A5A5B3"/>
 | 
				
			||||||
 | 
					</svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 539 B  | 
							
								
								
									
										5
									
								
								app/icons/downArrowLgIcon.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
 | 
				
			||||||
 | 
					<path fill-rule="evenodd" clip-rule="evenodd" d="M13.7574 4.90913C13.5231 4.67482 13.1432 4.67482 12.9088 4.90913L8.04691 9.77106C8.02088 9.7971 7.97867 9.7971 7.95263 9.77106L3.0907 4.90913C2.85639 4.67482 2.47649 4.67482 2.24217 4.90913C2.00786 5.14345 2.00786 5.52335 2.24217 5.75766L7.1041 10.6196C7.59877 11.1143 8.40078 11.1143 8.89544 10.6196L13.7574 5.75766C13.9917 5.52335 13.9917 5.14345 13.7574 4.90913Z" fill="#606078"/>
 | 
				
			||||||
 | 
					</svg>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 538 B  |