refactor(ui): adjust global navigation bar style (#13419)

### What problem does this PR solve?

Renovate global navigation bar, align styles to the design.
(May causes minor layout issues in sub-pages, will check and fix soon)

### Type of change

- [x] Refactoring
This commit is contained in:
Jimmy Ben Klieve
2026-03-05 20:47:29 +08:00
committed by GitHub
parent 9e0e128ce5
commit ef4cbe72a3
45 changed files with 1334 additions and 1250 deletions

View File

@ -14,6 +14,9 @@ module.exports = {
jsx: true, jsx: true,
}, },
}, },
includes: [
'./src',
],
settings: { settings: {
react: { react: {
version: 'detect', version: 'detect',

View File

@ -3,7 +3,7 @@ import { Toaster } from '@/components/ui/toaster';
import i18n, { changeLanguageAsync } from '@/locales/config'; import i18n, { changeLanguageAsync } from '@/locales/config';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { configResponsive } from 'ahooks'; import { configResponsive } from 'ahooks';
import { App, ConfigProvider, ConfigProviderProps, theme } from 'antd'; import { ConfigProvider, ConfigProviderProps, theme } from 'antd';
import pt_BR from 'antd/lib/locale/pt_BR'; import pt_BR from 'antd/lib/locale/pt_BR';
import arEG from 'antd/locale/ar_EG'; import arEG from 'antd/locale/ar_EG';
import deDE from 'antd/locale/de_DE'; import deDE from 'antd/locale/de_DE';
@ -22,7 +22,6 @@ import weekday from 'dayjs/plugin/weekday';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { RouterProvider } from 'react-router'; import { RouterProvider } from 'react-router';
import { ThemeProvider, useTheme } from './components/theme-provider'; import { ThemeProvider, useTheme } from './components/theme-provider';
import { SidebarProvider } from './components/ui/sidebar';
import { TooltipProvider } from './components/ui/tooltip'; import { TooltipProvider } from './components/ui/tooltip';
import { ThemeEnum } from './constants/common'; import { ThemeEnum } from './constants/common';
import { routers } from './routes'; import { routers } from './routes';
@ -125,10 +124,10 @@ function Root({ children }: React.PropsWithChildren) {
}} }}
locale={locale} locale={locale}
> >
<SidebarProvider className="h-full"> {children}
<App className="w-full h-dvh relative">{children}</App>
</SidebarProvider> <Sonner position="top-right" expand richColors closeButton />
<Sonner position={'top-right'} expand richColors closeButton></Sonner>
<Toaster /> <Toaster />
</ConfigProvider> </ConfigProvider>
</> </>

View File

@ -1,5 +1,4 @@
import CopyToClipboard from '@/components/copy-to-clipboard'; import CopyToClipboard from '@/components/copy-to-clipboard';
import HighLightMarkdown from '@/components/highlight-markdown';
import { SelectWithSearch } from '@/components/originui/select-with-search'; import { SelectWithSearch } from '@/components/originui/select-with-search';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { import {
@ -32,7 +31,13 @@ import { zodResolver } from '@hookform/resolvers/zod';
import { ExternalLink } from 'lucide-react'; import { ExternalLink } from 'lucide-react';
import { memo, useCallback, useMemo } from 'react'; import { memo, useCallback, useMemo } from 'react';
import { useForm, useWatch } from 'react-hook-form'; import { useForm, useWatch } from 'react-hook-form';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import {
oneDark,
oneLight,
} from 'react-syntax-highlighter/dist/esm/styles/prism';
import { z } from 'zod'; import { z } from 'zod';
import { useIsDarkTheme } from '../theme-provider';
const FormSchema = z.object({ const FormSchema = z.object({
visibleAvatar: z.boolean(), visibleAvatar: z.boolean(),
@ -56,8 +61,10 @@ function EmbedDialog({
from, from,
beta = '', beta = '',
isAgent, isAgent,
visible,
}: IProps) { }: IProps) {
const { t } = useTranslate('chat'); const { t } = useTranslate('chat');
const isDarkTheme = useIsDarkTheme();
const form = useForm<z.infer<typeof FormSchema>>({ const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema), resolver: zodResolver(FormSchema),
@ -95,23 +102,30 @@ function EmbedDialog({
: from === SharedFrom.Agent : from === SharedFrom.Agent
? Routes.AgentShare ? Routes.AgentShare
: Routes.ChatShare; : Routes.ChatShare;
let src = `${location.origin}${baseRoute}?shared_id=${token}&from=${from}&auth=${beta}`;
const src = new URL(`${location.origin}${baseRoute}`);
src.searchParams.append('shared_id', token);
src.searchParams.append('from', from);
src.searchParams.append('auth', beta);
if (publishAvatar) { if (publishAvatar) {
src += '&release=true'; src.searchParams.append('release', 'true');
} }
if (visibleAvatar) { if (visibleAvatar) {
src += '&visible_avatar=1'; src.searchParams.append('visible_avatar', '1');
} }
if (locale) { if (locale) {
src += `&locale=${locale}`; src.searchParams.append('locale', locale);
} }
if (enableStreaming) { if (embedType === 'widget') {
src += '&streaming=true'; src.searchParams.append('mode', 'master');
src.searchParams.append('streaming', String(enableStreaming));
} }
if (theme && embedType === 'fullscreen') { if (theme && embedType === 'fullscreen') {
src += `&theme=${theme}`; src.searchParams.append('theme', theme);
} }
return src;
return src.toString();
}, [beta, from, token, values]); }, [beta, from, token, values]);
const text = useMemo(() => { const text = useMemo(() => {
@ -119,44 +133,36 @@ function EmbedDialog({
const { embedType } = values; const { embedType } = values;
if (embedType === 'widget') { if (embedType === 'widget') {
const { enableStreaming } = values; return `<iframe
const streamingParam = enableStreaming src="${iframeSrc}"
? '&streaming=true' style="position:fixed;bottom:0;right:0;width:100px;height:100px;border:none;background:transparent;z-index:9999"
: '&streaming=false'; frameborder="0"
return ` allow="microphone;camera"
~~~ html ></iframe>
<iframe src="${iframeSrc}&mode=master${streamingParam}" <script>
style="position:fixed;bottom:0;right:0;width:100px;height:100px;border:none;background:transparent;z-index:9999" window.addEventListener('message',e=>{
frameborder="0" allow="microphone;camera"></iframe> if(e.origin!=='${location.origin.replace(/:\d+/, ':9222')}')return;
<script> if(e.data.type==='CREATE_CHAT_WINDOW'){
window.addEventListener('message',e=>{ if(document.getElementById('chat-win'))return;
if(e.origin!=='${location.origin.replace(/:\d+/, ':9222')}')return; const i=document.createElement('iframe');
if(e.data.type==='CREATE_CHAT_WINDOW'){ i.id='chat-win';i.src=e.data.src;
if(document.getElementById('chat-win'))return; i.style.cssText='position:fixed;bottom:104px;right:24px;width:380px;height:500px;border:none;background:transparent;z-index:9998;display:none';
const i=document.createElement('iframe'); i.frameBorder='0';i.allow='microphone;camera';
i.id='chat-win';i.src=e.data.src; document.body.appendChild(i);
i.style.cssText='position:fixed;bottom:104px;right:24px;width:380px;height:500px;border:none;background:transparent;z-index:9998;display:none'; }else if(e.data.type==='TOGGLE_CHAT'){
i.frameBorder='0';i.allow='microphone;camera'; const w=document.getElementById('chat-win');
document.body.appendChild(i); if(w)w.style.display=e.data.isOpen?'block':'none';
}else if(e.data.type==='TOGGLE_CHAT'){ }else if(e.data.type==='SCROLL_PASSTHROUGH')window.scrollBy(0,e.data.deltaY);
const w=document.getElementById('chat-win'); });
if(w)w.style.display=e.data.isOpen?'block':'none'; </script>
}else if(e.data.type==='SCROLL_PASSTHROUGH')window.scrollBy(0,e.data.deltaY); `;
});
</script>
~~~
`;
} else { } else {
return ` return `<iframe
~~~ html
<iframe
src="${iframeSrc}" src="${iframeSrc}"
style="width: 100%; height: 100%; min-height: 600px" style="width: 100%; height: 100%; min-height: 600px"
frameborder="0" frameborder="0"
> ></iframe>
</iframe> `;
~~~
`;
} }
}, [generateIframeSrc, values]); }, [generateIframeSrc, values]);
@ -166,13 +172,14 @@ function EmbedDialog({
}, [generateIframeSrc]); }, [generateIframeSrc]);
return ( return (
<Dialog open onOpenChange={hideModal}> <Dialog open={visible} onOpenChange={hideModal}>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle> <DialogTitle>
{t('embedIntoSite', { keyPrefix: 'common' })} {t('embedIntoSite', { keyPrefix: 'common' })}
</DialogTitle> </DialogTitle>
</DialogHeader> </DialogHeader>
<section className="w-full overflow-auto space-y-5 text-sm text-text-secondary"> <section className="w-full overflow-auto space-y-5 text-sm text-text-secondary">
<Form {...form}> <Form {...form}>
<form className="space-y-5"> <form className="space-y-5">
@ -309,10 +316,16 @@ function EmbedDialog({
/> />
</form> </form>
</Form> </Form>
<div className="max-h-[350px] overflow-auto"> <div>
<span>{t('embedCode', { keyPrefix: 'search' })}</span> <span>{t('embedCode', { keyPrefix: 'search' })}</span>
<div className="max-h-full overflow-y-auto"> <div>
<HighLightMarkdown>{text}</HighLightMarkdown> <SyntaxHighlighter
className="max-h-[350px] overflow-auto scrollbar-auto"
language="html"
style={isDarkTheme ? oneDark : oneLight}
>
{text}
</SyntaxHighlighter>
</div> </div>
</div> </div>
<Button <Button
@ -327,7 +340,7 @@ function EmbedDialog({
{t(isAgent ? 'flow' : 'chat', { keyPrefix: 'header' })} {t(isAgent ? 'flow' : 'chat', { keyPrefix: 'header' })}
<span className="ml-1 inline-block">ID</span> <span className="ml-1 inline-block">ID</span>
</div> </div>
<div className="bg-bg-card rounded-lg flex justify-between p-2"> <div className="bg-bg-card rounded-lg flex items-center justify-between p-2">
<span>{token} </span> <span>{token} </span>
<CopyToClipboard text={token}></CopyToClipboard> <CopyToClipboard text={token}></CopyToClipboard>
</div> </div>

View File

@ -12,7 +12,12 @@ export function FormContainer({
className, className,
}: FormContainerProps) { }: FormContainerProps) {
return show ? ( return show ? (
<section className={cn('border rounded-lg p-5 space-y-5', className)}> <section
className={cn(
'border-0.5 border-border-button rounded-lg p-5 space-y-5',
className,
)}
>
{children} {children}
</section> </section>
) : ( ) : (

View File

@ -19,8 +19,10 @@ import { useIsDarkTheme } from '../theme-provider';
import styles from './index.module.less'; import styles from './index.module.less';
const HighLightMarkdown = ({ const HighLightMarkdown = ({
className,
children, children,
}: { }: {
className?: string;
children: string | null | undefined; children: string | null | undefined;
}) => { }) => {
const isDarkTheme = useIsDarkTheme(); const isDarkTheme = useIsDarkTheme();

View File

@ -25,7 +25,12 @@ export const FilterButton = React.forwardRef<
ButtonProps & { count?: number } ButtonProps & { count?: number }
>(({ count = 0, ...props }, ref) => { >(({ count = 0, ...props }, ref) => {
return ( return (
<Button variant="secondary" {...props} ref={ref}> <Button
variant="outline"
size={count > 0 ? 'default' : 'icon'}
{...props}
ref={ref}
>
{/* <span {/* <span
className={cn({ className={cn({
'text-text-primary': count > 0, 'text-text-primary': count > 0,
@ -34,12 +39,13 @@ export const FilterButton = React.forwardRef<
> >
Filter Filter
</span> */} </span> */}
<Funnel />
{count > 0 && ( {count > 0 && (
<span className="rounded-full bg-text-badge px-1 text-xs "> <span className="rounded bg-text-badge px-1 py-0.5 text-xs leading-none text-text-primary">
{count} {count}
</span> </span>
)} )}
<Funnel />
</Button> </Button>
); );
}); });

View File

@ -4,40 +4,57 @@ import * as React from 'react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { LucideLoader2, Plus } from 'lucide-react'; import { LucideLoader2, Plus } from 'lucide-react';
import { Link, LinkProps } from 'react-router';
const buttonVariants = cva( const buttonVariants = cva(
cn( cn(
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors outline-0', 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors outline-0',
'disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*="size-"])]:size-4 shrink-0 [&_svg]:shrink-0', 'disabled:pointer-events-none disabled:opacity-50 rounded border-0.5 border-transparent',
'[&_svg]:pointer-events-none [&_svg:not([class*="size-"])]:size-4 shrink-0 [&_svg]:shrink-0',
), ),
{ {
variants: { variants: {
variant: { variant: {
// Solid variant series:
// Button has its own background color, may have borders
default: default:
'bg-text-primary text-bg-base shadow-xs hover:bg-text-primary/90 focus-visible:bg-text-primary/90', 'bg-text-primary text-bg-base shadow-xs hover:bg-text-primary/90 focus-visible:bg-text-primary/90',
secondary: `
bg-bg-card
hover:text-text-primary hover:bg-border-button
focus-visible:text-text-primary focus-visible:bg-border-button
`,
highlighted: `
bg-text-primary text-bg-base border-b-4 border-b-accent-primary
hover:bg-text-primary/90 focus-visible:bg-text-primary/90
`,
accent: `
bg-accent-primary text-white
hover:bg-accent-primary/90 focus-visible:bg-accent-primary/90
`,
destructive: ` destructive: `
bg-state-error text-white shadow-xs bg-state-error text-white shadow-xs
hover:bg-state-error/90 focus-visible:ring-state-error/20 dark:focus-visible:ring-state-error/40 hover:bg-state-error/90 focus-visible:ring-state-error/20 dark:focus-visible:ring-state-error/40
`, `,
// Outline variant series
// Button has transparent or greyish background, may have borders
outline: ` outline: `
text-text-secondary bg-bg-input border-0.5 border-border-button text-text-secondary bg-bg-input border-0.5 border-border-button
hover:text-text-primary hover:bg-border-button hover:border-border-default hover:text-text-primary hover:bg-border-button hover:border-border-default
focus-visible:text-text-primary focus-visible:bg-border-button focus-visible:border-border-button focus-visible:text-text-primary focus-visible:bg-border-button focus-visible:border-border-button
`, `, // light: bg=transparent, dark: bg-input
secondary:
'bg-bg-input text-text-primary shadow-xs hover:bg-bg-input/80 border border-border-button',
ghost: ` dashed: `
text-text-secondary text-text-secondary border-border-button border-dashed
hover:bg-border-button hover:text-text-primary hover:text-text-primary hover:bg-border-button hover:border-border-default
focus-visible:text-text-primary focus-visible:bg-border-button focus-visible:text-text-primary focus-visible:bg-border-button focus-visible:border-border-button
`, `,
link: 'text-primary underline-offset-4 hover:underline',
icon: 'bg-colors-background-inverse-standard text-foreground hover:bg-colors-background-inverse-standard/80',
dashed: 'border border-dashed border-border-button hover:bg-accent',
transparent: ` transparent: `
text-text-secondary bg-transparent border-0.5 border-border-button text-text-secondary bg-transparent border-0.5 border-border-button
hover:text-text-primary hover:bg-border-button hover:text-text-primary hover:bg-border-button
@ -49,14 +66,12 @@ const buttonVariants = cva(
hover:bg-state-error/10 focus-visible:bg-state-error/10 hover:bg-state-error/10 focus-visible:bg-state-error/10
`, `,
accent: ` // Ghost variant series
bg-accent-primary text-white // Button has transparent background, without borders
hover:bg-accent-primary/90 focus-visible:bg-accent-primary/90 ghost: `
`, text-text-secondary
hover:bg-border-button focus-visible:bg-border-button
highlighted: ` hover:text-text-primary focus-visible:text-text-primary
bg-text-primary text-bg-base border-b-4 border-b-accent-primary
hover:bg-text-primary/90 focus-visible:bg-text-primary/90
`, `,
delete: ` delete: `
@ -64,15 +79,23 @@ const buttonVariants = cva(
hover:bg-state-error-5 hover:text-state-error hover:bg-state-error-5 hover:text-state-error
focus-visible:text-state-error focus-visible:bg-state-error-5 focus-visible:text-state-error focus-visible:bg-state-error-5
`, `,
link: 'text-primary underline-offset-4 hover:underline',
}, },
size: { size: {
default: 'h-8 px-2.5 py-1.5 ',
sm: 'h-6 rounded-sm px-2',
lg: 'h-11 rounded-md px-8',
icon: 'h-10 w-10',
auto: 'h-full px-1', auto: 'h-full px-1',
'icon-sm': 'size-8',
'icon-xs': 'size-7', xl: 'h-12 rounded-xl px-5',
lg: 'h-10 rounded-lg px-4',
default: 'h-8 rounded px-3',
sm: 'h-7 rounded-sm px-2',
xs: 'h-6 rounded-xs px-1',
'icon-xl': 'size-12 rounded-xl',
'icon-lg': 'size-10 rounded-lg',
icon: 'size-8 rounded',
'icon-sm': 'size-7 rounded-sm',
'icon-xs': 'size-6 rounded-xs',
}, },
}, },
defaultVariants: { defaultVariants: {
@ -82,45 +105,58 @@ const buttonVariants = cva(
}, },
); );
export interface ButtonProps export type ButtonProps<IsAnchor extends boolean = false> = {
extends
React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean; asChild?: boolean;
asLink?: boolean;
loading?: boolean; loading?: boolean;
block?: boolean; block?: boolean;
} disabled?: boolean;
dot?: boolean;
} & VariantProps<typeof buttonVariants> &
(IsAnchor extends true
? LinkProps
: React.ButtonHTMLAttributes<HTMLButtonElement>);
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( const Button = React.forwardRef(
( <IsAnchor extends boolean = false>(
{ {
children, children,
className, className,
variant, variant,
size, size,
dot = false,
asChild = false, asChild = false,
asLink = false,
loading = false, loading = false,
disabled = false, disabled = false,
block = false, block = false,
...props ...props
}, }: ButtonProps<IsAnchor>,
ref, ref: React.ForwardedRef<
IsAnchor extends true ? HTMLAnchorElement : HTMLButtonElement
>,
) => { ) => {
const Comp = asChild ? Slot : 'button'; const Comp = asChild ? Slot : asLink ? Link : 'button';
return ( return (
<Comp <Comp
className={cn( className={cn(
'bg-bg-card',
{ 'block w-full': block },
buttonVariants({ variant, size, className }), buttonVariants({ variant, size, className }),
{ 'block w-full': block },
{ relative: dot },
)} )}
ref={ref} // @ts-ignore
ref={ref as React.RefObject<HTMLButtonElement | HTMLAnchorElement>}
disabled={loading || disabled} disabled={loading || disabled}
{...props} {...props}
> >
{loading && <LucideLoader2 className="animate-spin" />} <>
{children} {dot && (
<span className="absolute size-[6px] rounded-full -right-[3px] -top-[3px] bg-state-error animate" />
)}
{loading && <LucideLoader2 className="animate-spin" />}
{children}
</>
</Comp> </Comp>
); );
}, },

View File

@ -30,15 +30,12 @@ const CardHeader = React.forwardRef<
CardHeader.displayName = 'CardHeader'; CardHeader.displayName = 'CardHeader';
const CardTitle = React.forwardRef< const CardTitle = React.forwardRef<
HTMLDivElement, HTMLElement,
React.HTMLAttributes<HTMLDivElement> React.HTMLAttributes<HTMLElement> & { as?: React.ElementType }
>(({ className, ...props }, ref) => ( >(({ className, as: As = 'div', ...props }, ref) => (
<div <As
ref={ref} ref={ref}
className={cn( className={cn('text-2xl leading-normal font-medium', className)}
'text-2xl font-semibold leading-none tracking-tight',
className,
)}
{...props} {...props}
/> />
)); ));
@ -50,7 +47,7 @@ const CardDescription = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<div <div
ref={ref} ref={ref}
className={cn('text-sm text-muted-foreground', className)} className={cn('text-sm text-text-secondary', className)}
{...props} {...props}
/> />
)); ));

View File

@ -13,7 +13,7 @@ const Checkbox = React.forwardRef<
<CheckboxPrimitive.Root <CheckboxPrimitive.Root
ref={ref} ref={ref}
className={cn( className={cn(
'peer size-4 shrink-0 rounded-sm border border-text-secondary outline-0 transition-colors bg-bg-component', 'peer size-4 shrink-0 rounded-2xs border border-text-disabled outline-0 transition-colors bg-transparent',
'hover:border-border-default hover:bg-border-button', 'hover:border-border-default hover:bg-border-button',
'focus-visible:border-border-default focus-visible:bg-border-default', 'focus-visible:border-border-default focus-visible:bg-border-default',
'disabled:cursor-not-allowed disabled:opacity-50', 'disabled:cursor-not-allowed disabled:opacity-50',

View File

@ -84,8 +84,12 @@ const DropdownMenuItem = React.forwardRef<
<DropdownMenuPrimitive.Item <DropdownMenuPrimitive.Item
ref={ref} ref={ref}
className={cn( className={cn(
'relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0', 'relative flex cursor-default select-none items-center gap-2',
inset && 'ps-8', 'rounded-sm px-2 py-1.5 text-sm text-text-secondary outline-none transition-colors',
'focus:bg-bg-card focus:text-text-primary',
'data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
'[&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
inset && 'pl-8',
justifyBetween && 'flex justify-between', justifyBetween && 'flex justify-between',
className, className,
)} )}

View File

@ -6,7 +6,7 @@ function Skeleton({
}: React.HTMLAttributes<HTMLDivElement>) { }: React.HTMLAttributes<HTMLDivElement>) {
return ( return (
<div <div
className={cn('animate-pulse rounded-md bg-muted', className)} className={cn('animate-pulse rounded-md bg-bg-card', className)}
{...props} {...props}
/> />
); );
@ -26,12 +26,10 @@ function ParagraphSkeleton() {
function CardSkeleton() { function CardSkeleton() {
return ( return (
<div className="flex flex-col space-y-3"> <div className="w-64">
<Skeleton className="h-[125px] w-[250px] rounded-xl" /> <Skeleton className="mb-3 h-28 rounded-xl" />
<div className="space-y-2"> <Skeleton className="mb-2 h-4" />
<Skeleton className="h-4 w-[250px]" /> <Skeleton className="h-4 w-4/5" />
<Skeleton className="h-4 w-[200px]" />
</div>
</div> </div>
); );
} }

View File

@ -0,0 +1,15 @@
import { LucideCircleQuestionMark } from 'lucide-react';
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip';
export default function WhatIsThis({ children }: React.PropsWithChildren<{}>) {
return (
<Tooltip>
<TooltipTrigger>
<LucideCircleQuestionMark className="size-[1em]" />
</TooltipTrigger>
<TooltipContent>{children}</TooltipContent>
</Tooltip>
);
}

View File

@ -1,28 +1,26 @@
@import url(./inter.less); @import url(./inter.less);
html { :root {
height: 100%; --font-sans: 'Inter';
overflow: hidden; // The content of the DatasetSettings page is too high, which will cause scroll bars to appear on the html tags. Setting the maximum height in DatasetSettings does not work either. I don't understand.
} }
body { @supports (font-variation-settings: normal) {
font-family: :root {
'Inter', --font-sans: 'InterVariable';
system-ui, }
-apple-system,
'Segoe UI',
sans-serif;
margin: 0;
height: 100%;
} }
html,
body,
#root { #root {
height: 100%; width: 100vw;
} width: 100dvw;
height: 100vh;
height: 100dvh;
overflow: hidden;
.ant-app { margin: 0;
height: 100%; padding: 0;
width: 100%;
} }
.vue-office-excel { .vue-office-excel {

View File

@ -1,28 +1,18 @@
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { useNavigateWithFromState } from '@/hooks/route-hook'; import { Routes } from '@/routes';
import { useListTenant } from '@/hooks/use-user-setting-request';
import { TenantRole } from '@/pages/user-setting/constants';
import { BellRing } from 'lucide-react'; import { BellRing } from 'lucide-react';
import { useCallback, useMemo } from 'react';
export function BellButton() { export function BellButton() {
const { data } = useListTenant(); return (
const navigate = useNavigateWithFromState(); <Button
asLink
const showBell = useMemo(() => { to={`${Routes.UserSetting}${Routes.Team}`}
return data.some((x) => x.role === TenantRole.Invite); variant="ghost"
}, [data]); size="icon"
className="group"
const handleBellClick = useCallback(() => { dot
navigate('/user-setting/team'); >
}, [navigate]); <BellRing className="size-4 animate-bell-shake group-hover:animate-none" />
return showBell ? (
<Button variant={'ghost'} onClick={handleBellClick}>
<div className="relative">
<BellRing className="size-4 " />
<span className="absolute size-1 rounded -right-1 -top-1 bg-red-600"></span>
</div>
</Button> </Button>
) : null; );
} }

View File

@ -0,0 +1,158 @@
import { useId, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Link, useLocation } from 'react-router';
import { LucideHouse } from 'lucide-react';
import { cn } from '@/lib/utils';
import { Routes } from '@/routes';
import { supportsCssAnchor } from '@/utils/css-support';
const PathMap = {
[Routes.Datasets]: [Routes.Datasets, Routes.DatasetBase],
[Routes.Chats]: [Routes.Chats, Routes.Chat],
[Routes.Searches]: [Routes.Searches, Routes.Search],
[Routes.Agents]: [Routes.Agents, Routes.AgentTemplates],
[Routes.Memories]: [Routes.Memories, Routes.Memory, Routes.MemoryMessage],
[Routes.Files]: [Routes.Files],
} as const;
const menuItems = [
{ path: Routes.Root, name: 'header.Root', icon: LucideHouse },
{ path: Routes.Datasets, name: 'header.dataset' /* icon: Library, */ },
{
path: Routes.Chats,
name: 'header.chat',
/* icon: MessageSquareText, */ 'data-testid': 'nav-chat',
},
{
path: Routes.Searches,
name: 'header.search',
/* icon: Search, */ 'data-testid': 'nav-search',
},
{
path: Routes.Agents,
name: 'header.flow',
/* icon: Cpu, */ 'data-testid': 'nav-agent',
},
{ path: Routes.Memories, name: 'header.memories' /* icon: Cpu, */ },
{ path: Routes.Files, name: 'header.fileManager' /* icon: File, */ },
];
const GlobalNavbar = supportsCssAnchor
? () => {
const { t } = useTranslation();
const { pathname } = useLocation();
const navbarAnchorNamePrefix = useId().replace(/:/g, '');
const activePath = useMemo(() => {
return (
Object.keys(PathMap).find((x: string) =>
PathMap[x as keyof typeof PathMap].some((y: string) =>
pathname.includes(y),
),
) || pathname
);
}, [pathname]);
const activePathAnchorName = `--${navbarAnchorNamePrefix}${activePath === Routes.Root ? '-root' : activePath.replace('/', '-')}`;
const hasAnyActive = useMemo(
() => menuItems.some(({ path }) => path === activePath),
[activePath],
);
return (
<nav>
<ul className="relative flex items-center p-1 bg-bg-card rounded-full border border-border-button">
{menuItems.map(({ path, name, icon: Icon, ...props }) => {
const isActive = path === activePath;
const anchorName = `--${navbarAnchorNamePrefix}${path === Routes.Root ? '-root' : path.replace('/', '-')}`;
return (
<li key={path} className="relative" style={{ anchorName }}>
<Link
{...props}
to={path}
className={cn(
'h-10 px-6 text-base inline-flex items-center justify-center',
'hover:text-current focus-visible:text-current rounded-full transition-all',
isActive && '!text-bg-base',
)}
aria-current={isActive ? 'page' : undefined}
>
{Icon && <Icon className="size-6 stroke-[1.5]" />}
<span className={cn(Icon && 'sr-only')}>{t(name)}</span>
</Link>
</li>
);
})}
<li
className={cn(
'absolute -z-[1] bg-text-primary border-b-2 border-b-accent-primary rounded-full opacity-0',
'transition-all',
hasAnyActive && 'opacity-100',
)}
role="presentation"
style={{
top: 'anchor(top)',
left: 'anchor(left)',
width: 'anchor-size(width)',
height: 'anchor-size(height)',
positionAnchor: activePathAnchorName,
}}
/>
</ul>
</nav>
);
}
: () => {
const { t } = useTranslation();
const { pathname } = useLocation();
const activePath = useMemo(() => {
return (
Object.keys(PathMap).find((x: string) =>
PathMap[x as keyof typeof PathMap].some((y: string) =>
pathname.includes(y),
),
) || pathname
);
}, [pathname]);
return (
<nav>
<ul className="flex items-center p-1 bg-bg-card rounded-full border border-border-button">
{menuItems.map(({ path, name, icon: Icon, ...props }) => {
const isActive = path === activePath;
return (
<li key={path}>
<Link
{...props}
to={path}
className={cn(
'h-10 px-6 text-base inline-flex items-center justify-center',
'hover:text-current focus-visible:text-current rounded-full transition-all',
isActive &&
'!text-bg-base bg-text-primary border-b-2 border-b-accent-primary',
)}
aria-label={t(name)}
aria-current={isActive ? 'page' : undefined}
>
{Icon ? (
<Icon className="size-6 stroke-[1.5]" />
) : (
<span>{t(name)}</span>
)}
</Link>
</li>
);
})}
</ul>
</nav>
);
};
export default GlobalNavbar;

View File

@ -1,27 +0,0 @@
.navs {
ul {
padding: 0;
list-style: none;
display: flex;
}
li {
margin-right: 1em;
}
}
.layout {
height: 100vh;
}
body {
margin: 0;
}
.divider {
margin: 0;
}
.clickAvailable {
cursor: pointer;
}

View File

@ -1,6 +1,5 @@
import { IconFontFill } from '@/components/icon-font'; import { IconFontFill } from '@/components/icon-font';
import { RAGFlowAvatar } from '@/components/ragflow-avatar'; import { RAGFlowAvatar } from '@/components/ragflow-avatar';
import { useTheme } from '@/components/theme-provider';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { import {
DropdownMenu, DropdownMenu,
@ -8,213 +7,143 @@ import {
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'; } from '@/components/ui/dropdown-menu';
import { Segmented, SegmentedValue } from '@/components/ui/segmented'; import { LanguageList, LanguageMap } from '@/constants/common';
import { LanguageList, LanguageMap, ThemeEnum } from '@/constants/common';
import { useChangeLanguage } from '@/hooks/logic-hooks'; import { useChangeLanguage } from '@/hooks/logic-hooks';
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; import {
import { useNavigateWithFromState } from '@/hooks/route-hook'; useFetchUserInfo,
import { useFetchUserInfo } from '@/hooks/use-user-setting-request'; useListTenant,
} from '@/hooks/use-user-setting-request';
import { cn } from '@/lib/utils';
import { TenantRole } from '@/pages/user-setting/constants';
import { Routes } from '@/routes'; import { Routes } from '@/routes';
import { camelCase } from 'lodash'; import { camelCase } from 'lodash';
import { import { LucideChevronDown, LucideCircleHelp } from 'lucide-react';
ChevronDown, import React, { useMemo } from 'react';
CircleHelp,
Cpu,
File,
House,
Library,
MessageSquareText,
Moon,
Search,
Sun,
} from 'lucide-react';
import React, { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useLocation } from 'react-router'; import { Link, useLocation } from 'react-router';
import { BellButton } from './bell-button'; import { BellButton } from './bell-button';
import GlobalNavbar from './global-navbar';
import ThemeButton from './theme-button';
const handleDocHelpCLick = () => { export function Header({
window.open('https://ragflow.io/docs/dev/category/guides', 'target'); className,
}; ...props
}: React.HTMLAttributes<HTMLElement>) {
const PathMap = {
[Routes.Datasets]: [Routes.Datasets],
[Routes.Chats]: [Routes.Chats],
[Routes.Searches]: [Routes.Searches],
[Routes.Agents]: [Routes.Agents],
[Routes.Memories]: [Routes.Memories, Routes.Memory, Routes.MemoryMessage],
[Routes.Files]: [Routes.Files],
} as const;
export function Header() {
const { t } = useTranslation(); const { t } = useTranslation();
const { pathname } = useLocation(); const { pathname } = useLocation();
const navigate = useNavigateWithFromState();
const { navigateToOldProfile } = useNavigatePage();
const changeLanguage = useChangeLanguage(); const changeLanguage = useChangeLanguage();
const { setTheme, theme } = useTheme();
const { const {
data: { language = 'English', avatar, nickname }, data: { language = 'English', avatar, nickname },
} = useFetchUserInfo(); } = useFetchUserInfo();
const handleItemClick = (key: string) => () => { const { data: tenantData } = useListTenant();
changeLanguage(key); const hasNotification = useMemo(
}; () => tenantData?.some((x) => x.role === TenantRole.Invite),
[tenantData],
);
const items = LanguageList.map((x) => ({ const langItems = LanguageList.map((x) => ({
key: x, key: x,
label: <span>{LanguageMap[x as keyof typeof LanguageMap]}</span>, label: <span>{LanguageMap[x as keyof typeof LanguageMap]}</span>,
})); }));
const onThemeClick = React.useCallback(() => {
setTheme(theme === ThemeEnum.Dark ? ThemeEnum.Light : ThemeEnum.Dark);
}, [setTheme, theme]);
const tagsData = useMemo(
() => [
{ path: Routes.Root, name: t('header.Root'), icon: House },
{ path: Routes.Datasets, name: t('header.dataset'), icon: Library },
{ path: Routes.Chats, name: t('header.chat'), icon: MessageSquareText },
{ path: Routes.Searches, name: t('header.search'), icon: Search },
{ path: Routes.Agents, name: t('header.flow'), icon: Cpu },
{ path: Routes.Memories, name: t('header.memories'), icon: Cpu },
{ path: Routes.Files, name: t('header.fileManager'), icon: File },
],
[t],
);
const options = useMemo(() => {
return tagsData.map((tag) => {
const HeaderIcon = tag.icon;
return {
label:
tag.path === Routes.Root ? (
<HeaderIcon className="size-6"></HeaderIcon>
) : (
<span
data-testid={
tag.path === Routes.Chats
? 'nav-chat'
: tag.path === Routes.Searches
? 'nav-search'
: tag.path === Routes.Agents
? 'nav-agent'
: undefined
}
>
{tag.name}
</span>
),
value: tag.path,
};
});
}, [tagsData]);
// const currentPath = useMemo(() => {
// return (
// tagsData.find((x) => pathname.startsWith(x.path))?.path || Routes.Root
// );
// }, [pathname, tagsData]);
const handleChange = (path: SegmentedValue) => {
navigate(path as Routes);
};
const handleLogoClick = useCallback(() => {
navigate(Routes.Root);
}, [navigate]);
const activePathName = useMemo(() => {
const name = Object.keys(PathMap).find((x: string) => {
const pathList = PathMap[x as keyof typeof PathMap];
return pathList.some((y: string) => pathname.indexOf(y) > -1);
});
if (name) {
return name;
} else {
return pathname;
}
}, [pathname]);
return ( return (
<section <header
className="py-5 px-10 flex justify-between items-center " key="app-navbar"
data-testid="top-nav" className={cn(
'w-full grid grid-cols-[1fr_auto_1fr] grid-rows-1 items-center gap-8',
className,
)}
{...props}
> >
<div className="flex items-center gap-4"> <div className="inline-flex items-center">
<img <Link
src={'/logo.svg'} to={Routes.Root}
alt="logo" aria-current={pathname === Routes.Root ? 'page' : undefined}
className="size-10 mr-[12] cursor-pointer" >
onClick={handleLogoClick} <img src={'/logo.svg'} alt="RAGFlow logo" className="size-10" />
/> </Link>
</div> </div>
<Segmented
rounded="xxxl" <GlobalNavbar />
sizeType="xl"
buttonSize="xl"
options={options}
value={activePathName}
onChange={handleChange}
activeClassName="text-bg-base bg-metallic-gradient border-b-[#00BEB4] border-b-2"
></Segmented>
<div <div
className="flex items-center gap-5 text-text-badge" className="flex items-center justify-end gap-4 text-text-badge"
data-testid="auth-status" data-testid="auth-status"
> >
<a <a
className="p-2 text-text-secondary hover:text-text-primary focus-visible:text-text-primary"
target="_blank" target="_blank"
href="https://discord.com/invite/NjYzJD3GM3" href="https://discord.com/invite/NjYzJD3GM3"
rel="noreferrer" rel="noreferrer noopener"
> >
<IconFontFill name="a-DiscordIconSVGVectorIcon"></IconFontFill> <IconFontFill name="a-DiscordIconSVGVectorIcon" />
</a> </a>
<a <a
className="p-2 text-text-secondary hover:text-text-primary focus-visible:text-text-primary"
target="_blank" target="_blank"
href="https://github.com/infiniflow/ragflow" href="https://github.com/infiniflow/ragflow"
rel="noreferrer" rel="noreferrer noopener"
> >
<IconFontFill name="GitHub"></IconFontFill> <IconFontFill name="GitHub" />
</a> </a>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger> <DropdownMenuTrigger asChild>
<div className="flex items-center gap-1"> <Button className="flex items-center gap-1" variant="ghost">
{t(`common.${camelCase(language)}`)} {t(`common.${camelCase(language)}`)}
<ChevronDown className="size-4" />
</div> <LucideChevronDown className="size-4" />
</Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent> <DropdownMenuContent>
{items.map((x) => ( {langItems.map((x) => (
<DropdownMenuItem key={x.key} onClick={handleItemClick(x.key)}> <DropdownMenuItem
key={x.key}
onClick={() => changeLanguage(x.key)}
>
{x.label} {x.label}
</DropdownMenuItem> </DropdownMenuItem>
))} ))}
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
<Button variant={'ghost'} onClick={handleDocHelpCLick}>
<CircleHelp /> <Button
asLink
variant="ghost"
size="icon"
to="https://ragflow.io/docs/dev/category/user-guides"
target="_blank"
rel="noreferrer noopener"
>
<LucideCircleHelp className="size-[1em]" />
</Button> </Button>
<Button variant={'ghost'} onClick={onThemeClick}>
{theme === 'light' ? <Sun /> : <Moon />} <ThemeButton />
</Button>
<BellButton></BellButton> {hasNotification && <BellButton />}
<div className="relative" data-testid="settings-entrypoint">
<Link
to={Routes.UserSetting}
className="relative ms-3"
data-testid="settings-entrypoint"
>
<RAGFlowAvatar <RAGFlowAvatar
name={nickname} name={nickname}
avatar={avatar} avatar={avatar}
isPerson isPerson
className="size-8 cursor-pointer" className="size-8"
onClick={navigateToOldProfile} />
></RAGFlowAvatar>
{/* Temporarily hidden */} {/* Temporarily hidden */}
{/* <Badge className="h-5 w-8 absolute font-normal p-0 justify-center -right-8 -top-2 text-bg-base bg-gradient-to-l from-[#42D7E7] to-[#478AF5]"> {/* <Badge className="h-5 w-8 absolute font-normal p-0 justify-center -right-8 -top-2 text-bg-base bg-gradient-to-l from-[#42D7E7] to-[#478AF5]">
Pro Pro
</Badge> */} </Badge> */}
</div> </Link>
</div> </div>
</section> </header>
); );
} }

View File

@ -1,11 +1,20 @@
import { Outlet } from 'react-router'; import { Outlet } from 'react-router';
import { Header } from './next-header'; import { Header } from './next-header';
export default function NextLayout() { export function NextLayoutContainer({ children }: React.PropsWithChildren) {
return ( return (
<main className="h-full flex flex-col"> <div className="size-full grid grid-rows-[auto_1fr] grid-cols-1 grid-flow-col">
<Header /> <Header className="px-5 py-4" />
<Outlet />
</main> <main className="size-full overflow-hidden">{children}</main>
</div>
);
}
export default function NextLayout() {
return (
<NextLayoutContainer>
<Outlet />
</NextLayoutContainer>
); );
} }

View File

@ -0,0 +1,19 @@
import { cn } from '@/lib/utils';
/**
* Basic page container:
* - Full size
* - Padding x=2.5rem top=0.75rem
* - Auto scrollbar
*/
export function PageContainer({
className,
...props
}: React.PropsWithChildren<React.HTMLAttributes<HTMLDivElement>>) {
return (
<div
className={cn('size-full px-10 py-3 overflow-auto', className)}
{...props}
/>
);
}

View File

@ -0,0 +1,22 @@
import { LucideMoon, LucideSun } from 'lucide-react';
import { useTheme } from '@/components/theme-provider';
import { Button } from '@/components/ui/button';
import { ThemeEnum } from '@/constants/common';
export default function ThemeButton() {
const { setTheme, theme } = useTheme();
return (
<Button
variant="ghost"
size="icon"
className="relative"
onClick={() =>
setTheme(theme === ThemeEnum.Dark ? ThemeEnum.Light : ThemeEnum.Dark)
}
>
{theme === ThemeEnum.Light ? <LucideSun /> : <LucideMoon />}
</Button>
);
}

View File

@ -1,12 +1,3 @@
import { PageHeader } from '@/components/page-header';
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from '@/components/ui/breadcrumb';
import { useSetModalState } from '@/hooks/common-hooks'; import { useSetModalState } from '@/hooks/common-hooks';
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
import { useFetchAgentTemplates, useSetAgent } from '@/hooks/use-agent-request'; import { useFetchAgentTemplates, useSetAgent } from '@/hooks/use-agent-request';
@ -15,14 +6,11 @@ import { CardContainer } from '@/components/card-container';
import { AgentCategory } from '@/constants/agent'; import { AgentCategory } from '@/constants/agent';
import { IFlowTemplate } from '@/interfaces/database/agent'; import { IFlowTemplate } from '@/interfaces/database/agent';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { CreateAgentDialog } from './create-agent-dialog'; import { CreateAgentDialog } from './create-agent-dialog';
import { TemplateCard } from './template-card'; import { TemplateCard } from './template-card';
import { MenuItemKey, SideBar } from './template-sidebar'; import { MenuItemKey, SideBar } from './template-sidebar';
export default function AgentTemplates() { export default function AgentTemplates() {
const { navigateToAgents } = useNavigatePage();
const { t } = useTranslation();
const list = useFetchAgentTemplates(); const list = useFetchAgentTemplates();
const { loading, setAgent } = useSetAgent(); const { loading, setAgent } = useSetAgent();
const [templateList, setTemplateList] = useState<IFlowTemplate[]>([]); const [templateList, setTemplateList] = useState<IFlowTemplate[]>([]);
@ -99,21 +87,6 @@ export default function AgentTemplates() {
return ( return (
<section> <section>
<PageHeader>
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink onClick={navigateToAgents}>
{t('flow.agent')}
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>{t('flow.createGraph')}</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</PageHeader>
<div className="flex flex-1 h-dvh"> <div className="flex flex-1 h-dvh">
<SideBar <SideBar
change={handleSiderBarChange} change={handleSiderBarChange}

View File

@ -23,14 +23,7 @@ import DocumentPreview from '@/components/document-preview';
import DocumentHeader from '@/components/document-preview/document-header'; import DocumentHeader from '@/components/document-preview/document-header';
import { useGetDocumentUrl } from '@/components/document-preview/hooks'; import { useGetDocumentUrl } from '@/components/document-preview/hooks';
import { PageHeader } from '@/components/page-header'; import { PageHeader } from '@/components/page-header';
import { import { Button } from '@/components/ui/button';
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from '@/components/ui/breadcrumb';
import message from '@/components/ui/message'; import message from '@/components/ui/message';
import { import {
RAGFlowPagination, RAGFlowPagination,
@ -42,6 +35,7 @@ import {
useNavigatePage, useNavigatePage,
} from '@/hooks/logic-hooks/navigate-hooks'; } from '@/hooks/logic-hooks/navigate-hooks';
import { useFetchKnowledgeBaseConfiguration } from '@/hooks/use-knowledge-request'; import { useFetchKnowledgeBaseConfiguration } from '@/hooks/use-knowledge-request';
import { LucideArrowBigLeft } from 'lucide-react';
import styles from './index.module.less'; import styles from './index.module.less';
const Chunk = () => { const Chunk = () => {
@ -182,29 +176,15 @@ const Chunk = () => {
return ( return (
<> <>
<PageHeader> <PageHeader>
<Breadcrumb> <Button
<BreadcrumbList> variant="outline"
<BreadcrumbItem> onClick={navigateToDataFile(
<BreadcrumbLink onClick={navigateToDatasetList}> getQueryString(QueryStringMap.id) as string,
{t('knowledgeDetails.dataset')} )}
</BreadcrumbLink> >
</BreadcrumbItem> <LucideArrowBigLeft />
<BreadcrumbSeparator /> {t('common.back')}
<BreadcrumbItem> </Button>
<BreadcrumbLink
onClick={navigateToDataFile(
getQueryString(QueryStringMap.id) as string,
)}
>
{dataset.name}
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>{documentInfo?.name}</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</PageHeader> </PageHeader>
<div className={styles.chunkPage}> <div className={styles.chunkPage}>
<div className="flex flex-1 gap-8"> <div className="flex flex-1 gap-8">

View File

@ -2,10 +2,17 @@ import FileStatusBadge from '@/components/file-status-badge';
import { FilterCollection } from '@/components/list-filter-bar/interface'; import { FilterCollection } from '@/components/list-filter-bar/interface';
import SvgIcon from '@/components/svg-icon'; import SvgIcon from '@/components/svg-icon';
import { useIsDarkTheme } from '@/components/theme-provider'; import { useIsDarkTheme } from '@/components/theme-provider';
import { AntToolTip } from '@/components/ui/tooltip';
import {
Card,
CardDescription,
CardFooter,
CardHeader,
} from '@/components/ui/card';
import WhatIsThis from '@/components/what-is-this';
import { RunningStatusMap } from '@/constants/knowledge'; import { RunningStatusMap } from '@/constants/knowledge';
import { useFetchDocumentList } from '@/hooks/use-document-request'; import { useFetchDocumentList } from '@/hooks/use-document-request';
import { CircleQuestionMark } from 'lucide-react';
import { FC, useEffect, useMemo, useState } from 'react'; import { FC, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { RunningStatus } from '../dataset/constant'; import { RunningStatus } from '../dataset/constant';
@ -37,23 +44,35 @@ const StatCard: FC<StatCardProps> = ({
tooltip, tooltip,
}) => { }) => {
return ( return (
<div className="bg-bg-card p-4 rounded-lg border border-border flex flex-col gap-2"> <Card
<div className="flex items-center justify-between"> className="px-5 py-2.5 rounded-lg border-border-default grid grid-cols-[1fr_auto] grid-rows-[1fr_auto]"
<h3 className="flex items-center gap-1 text-sm font-medium text-text-secondary"> style={{
{title} gridTemplateAreas: '"data icon" "footer footer"',
{tooltip && ( }}
<AntToolTip title={tooltip} trigger="hover"> >
<CircleQuestionMark size={12} /> <span style={{ gridArea: 'icon' }}>{icon}</span>
</AntToolTip>
)} <div style={{ gridArea: 'data' }}>
</h3> <CardHeader className="p-0">
{icon} <h3 className="flex items-center gap-1 text-sm font-medium text-text-secondary">
{title}
{tooltip && <WhatIsThis>{tooltip}</WhatIsThis>}
</h3>
</CardHeader>
<CardDescription className="text-text-primary text-2xl font-medium leading-9">
<data value={value}>{value}</data>
</CardDescription>
</div> </div>
<div className="text-2xl font-bold text-text-primary">{value}</div>
<div className="h-12 w-full flex items-center"> <CardFooter
className="p-0 mt-1.5 h-8 w-full flex items-end"
style={{ gridArea: 'footer' }}
>
<div className="flex-1">{children}</div> <div className="flex-1">{children}</div>
</div> </CardFooter>
</div> </Card>
); );
}; };
@ -64,38 +83,34 @@ const CardFooterProcess: FC<CardFooterProcessProps> = ({
failedTip, failedTip,
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<div className="flex items-center flex-col gap-2"> <div className="flex items-center flex-col gap-2">
<div className="w-full flex justify-between gap-4 rounded-lg text-sm font-bold text-text-primary"> <dl className="w-full flex justify-between gap-4 rounded-lg text-sm font-bold text-text-primary">
<div className="flex items-center justify-between rounded-md w-1/2 p-2 bg-state-success-5"> <div className="flex items-center justify-between rounded-sm w-1/2 p-2 bg-state-success-5">
<div className="flex items-center rounded-lg gap-1"> <dt className="flex items-center rounded-lg gap-1">
<div className="w-2 h-2 rounded-full bg-state-success "></div> <div className="w-1 h-1 rounded-full bg-state-success"></div>
<div className="font-normal text-text-secondary text-xs flex items-center gap-1"> <div className="font-normal text-text-secondary text-xs flex items-center gap-1">
{t('knowledgeDetails.success')} {t('knowledgeDetails.success')}
{successTip && ( {successTip && <WhatIsThis>{successTip}</WhatIsThis>}
<AntToolTip title={successTip} trigger="hover">
<CircleQuestionMark size={12} />
</AntToolTip>
)}
</div> </div>
</div> </dt>
<div>{success || 0}</div>
<dd className="font-normal">{success || 0}</dd>
</div> </div>
<div className="flex items-center justify-between rounded-md w-1/2 bg-state-error-5 p-2">
<div className="flex items-center rounded-lg gap-1"> <div className="flex items-center justify-between rounded-sm w-1/2 bg-state-error-5 p-2">
<div className="w-2 h-2 rounded-full bg-state-error"></div> <dt className="flex items-center rounded-lg gap-1">
<div className="w-1 h-1 rounded-full bg-state-error"></div>
<div className="font-normal text-text-secondary text-xs flex items-center gap-1"> <div className="font-normal text-text-secondary text-xs flex items-center gap-1">
{t('knowledgeDetails.failed')} {t('knowledgeDetails.failed')}
{failedTip && ( {failedTip && <WhatIsThis>{failedTip}</WhatIsThis>}
<AntToolTip title={failedTip} trigger="hover">
<CircleQuestionMark size={12} />
</AntToolTip>
)}
</div> </div>
</div> </dt>
<div>{failed || 0}</div>
<dd className="font-normal">{failed || 0}</dd>
</div> </div>
</div> </dl>
</div> </div>
); );
}; };
@ -247,9 +262,13 @@ const FileLogsPage: FC = () => {
const isDark = useIsDarkTheme(); const isDark = useIsDarkTheme();
return ( return (
<div className="p-5 min-w-[880px] border-border border rounded-lg mr-5"> <Card
className="
p-5 min-w-[880px] mr-5 mb-5 bg-transparent shadow-none
flex flex-col overflow-y-auto scrollbar-auto"
>
{/* Stats Cards */} {/* Stats Cards */}
<div className="grid grid-cols-3 md:grid-cols-3 gap-4 mb-6"> <div className="grid grid-cols-3 md:grid-cols-3 gap-7 mb-6">
<StatCard <StatCard
title={t('datasetOverview.totalFiles')} title={t('datasetOverview.totalFiles')}
value={topAllData.totalFiles.value} value={topAllData.totalFiles.value}
@ -261,12 +280,12 @@ const FileLogsPage: FC = () => {
) )
} }
> >
<div> <div className="text-xs">
<span className="text-accent-primary"> <span className="text-accent-primary">
{topAllData.totalFiles.precent > 0 ? '+' : ''} {topAllData.totalFiles.precent > 0 ? '+' : ''}
{topAllData.totalFiles.precent}%{' '} {topAllData.totalFiles.precent}%{' '}
</span> </span>
<span className="font-normal text-text-secondary text-xs"> <span className="font-normal text-text-secondary">
{t('knowledgeConfiguration.lastWeek')} {t('knowledgeConfiguration.lastWeek')}
</span> </span>
</div> </div>
@ -330,7 +349,7 @@ const FileLogsPage: FC = () => {
pageCount={10} pageCount={10}
active={active} active={active}
/> />
</div> </Card>
); );
}; };

View File

@ -414,8 +414,8 @@ const FileLogsTable: FC<FileLogsTableProps> = ({
}); });
return ( return (
<div className="w-full h-[calc(100vh-360px)]"> <div className="size-full flex flex-col">
<Table rootClassName="max-h-[calc(100vh-380px)]"> <Table rootClassName="max-h-full mb-4">
<TableHeader> <TableHeader>
{table.getHeaderGroups().map((headerGroup) => ( {table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}> <TableRow key={headerGroup.id}>
@ -460,15 +460,15 @@ const FileLogsTable: FC<FileLogsTableProps> = ({
)} )}
</TableBody> </TableBody>
</Table> </Table>
<div className="flex items-center justify-end absolute bottom-3 right-12">
<div className="space-x-2"> <div className="mt-auto flex items-center justify-end">
<RAGFlowPagination <RAGFlowPagination
{...{ current: pagination.current, pageSize: pagination.pageSize }} {...{ current: pagination.current, pageSize: pagination.pageSize }}
total={pagination.total} total={pagination.total}
onChange={(page, pageSize) => setPagination({ page, pageSize })} onChange={(page, pageSize) => setPagination({ page, pageSize })}
/> />
</div>
</div> </div>
{isModalVisible && ( {isModalVisible && (
<ProcessLogModal <ProcessLogModal
title={active === LogTabs.FILE_LOGS ? t('fileLogs') : t('datasetLog')} title={active === LogTabs.FILE_LOGS ? t('fileLogs') : t('datasetLog')}

View File

@ -2,6 +2,13 @@ import { DataFlowSelect } from '@/components/data-pipeline-select';
import GraphRagItems from '@/components/parse-configuration/graph-rag-form-fields'; import GraphRagItems from '@/components/parse-configuration/graph-rag-form-fields';
import RaptorFormFields from '@/components/parse-configuration/raptor-form-fields'; import RaptorFormFields from '@/components/parse-configuration/raptor-form-fields';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import Divider from '@/components/ui/divider'; import Divider from '@/components/ui/divider';
import { Form } from '@/components/ui/form'; import { Form } from '@/components/ui/form';
import { FormLayout } from '@/constants/form'; import { FormLayout } from '@/constants/form';
@ -15,7 +22,6 @@ import { createContext, useEffect, useState } from 'react';
import { useForm, useWatch } from 'react-hook-form'; import { useForm, useWatch } from 'react-hook-form';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { z } from 'zod'; import { z } from 'zod';
import { TopTitle } from '../dataset-title';
import { import {
GenerateType, GenerateType,
IGenerateLogButtonProps, IGenerateLogButtonProps,
@ -258,98 +264,113 @@ export default function DatasetSettings() {
}; };
return ( return (
<section className="p-5 h-full flex flex-col"> <div className="pr-5 pb-5">
<TopTitle <Card className="p-0 h-full flex flex-col bg-transparent shadow-none">
title={t('knowledgeDetails.configuration')} <CardHeader className="p-5 border-b-0.5 border-border-button">
description={t('knowledgeConfiguration.titleDescription')} <header>
></TopTitle> <CardTitle as="h1">{t('knowledgeDetails.configuration')}</CardTitle>
<div className="flex gap-14 flex-1 min-h-0">
<DataSetContext.Provider
value={{
loading: datasetSettingLoading,
knowledgeDetails: knowledgeDetails,
}}
>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6 ">
<div className="w-[768px] h-[calc(100vh-240px)] pr-1 overflow-y-auto scrollbar-auto">
<MainContainer className="text-text-secondary">
<div className="text-base font-medium text-text-primary">
{t('knowledgeConfiguration.baseInfo')}
</div>
<GeneralForm></GeneralForm>
<Divider /> <CardDescription>
<div className="text-base font-medium text-text-primary"> {t('knowledgeConfiguration.titleDescription')}
{t('knowledgeConfiguration.dataPipeline')} </CardDescription>
</div>
<ParseTypeItem line={1} />
{parseType === 1 && (
<ChunkMethodItem line={1}></ChunkMethodItem>
)}
{parseType === 2 && (
<DataFlowSelect
isMult={false}
showToDataPipeline={true}
formFieldName="pipeline_id"
layout={FormLayout.Horizontal}
/>
)}
{/* <Divider /> */} {/* <Button>Save as Preset</Button> */}
{parseType === 1 && <ChunkMethodForm />} </header>
</CardHeader>
{/* <LinkDataPipeline <CardContent className="p-0 flex-1 h-0 flex divide-x-0.5">
data={pipelineData} <DataSetContext.Provider
handleLinkOrEditSubmit={handleLinkOrEditSubmit} value={{
/> */} loading: datasetSettingLoading,
<Divider /> knowledgeDetails: knowledgeDetails,
<LinkDataSource }}
data={sourceData} >
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="flex flex-col"
>
<div className="flex-1 h-0 w-[768px] px-5 pt-5 overflow-y-auto scrollbar-auto">
<MainContainer className="text-text-secondary">
<div className="text-base font-medium text-text-primary">
{t('knowledgeConfiguration.baseInfo')}
</div>
<GeneralForm></GeneralForm>
<Divider />
<div className="text-base font-medium text-text-primary">
{t('knowledgeConfiguration.dataPipeline')}
</div>
<ParseTypeItem line={1} />
{parseType === 1 && (
<ChunkMethodItem line={1}></ChunkMethodItem>
)}
{parseType === 2 && (
<DataFlowSelect
isMult={false}
showToDataPipeline={true}
formFieldName="pipeline_id"
layout={FormLayout.Horizontal}
/>
)}
{/* <Divider /> */}
{parseType === 1 && <ChunkMethodForm />}
{/* <LinkDataPipeline
data={pipelineData}
handleLinkOrEditSubmit={handleLinkOrEditSubmit} handleLinkOrEditSubmit={handleLinkOrEditSubmit}
unbindFunc={unbindFunc} /> */}
handleAutoParse={handleAutoParse} <Divider />
/> <LinkDataSource
<Divider /> data={sourceData}
<div className="text-base font-medium text-text-primary"> handleLinkOrEditSubmit={handleLinkOrEditSubmit}
{t('knowledgeConfiguration.globalIndex')} unbindFunc={unbindFunc}
</div> handleAutoParse={handleAutoParse}
<GraphRagItems />
className="border-none p-0" <Divider />
data={graphRagGenerateData as IGenerateLogButtonProps} <div className="text-base font-medium text-text-primary">
onDelete={() => {t('knowledgeConfiguration.globalIndex')}
handleDeletePipelineTask(GenerateType.KnowledgeGraph) </div>
} <GraphRagItems
></GraphRagItems> className="border-none p-0"
<Divider /> data={graphRagGenerateData as IGenerateLogButtonProps}
<RaptorFormFields onDelete={() =>
data={raptorGenerateData as IGenerateLogButtonProps} handleDeletePipelineTask(GenerateType.KnowledgeGraph)
onDelete={() => }
handleDeletePipelineTask(GenerateType.Raptor) ></GraphRagItems>
} <Divider />
></RaptorFormFields> <RaptorFormFields
</MainContainer> data={raptorGenerateData as IGenerateLogButtonProps}
</div> onDelete={() =>
<div className="text-right items-center flex justify-end gap-3 w-[768px]"> handleDeletePipelineTask(GenerateType.Raptor)
<Button }
type="reset" ></RaptorFormFields>
className="bg-transparent text-color-white hover:bg-transparent border-gray-500 border-[1px]" </MainContainer>
data-testid="ds-settings-page-cancel-btn" </div>
onClick={() => {
form.reset(); <div className="p-5 text-right items-center flex justify-end gap-3 w-[768px]">
}} <Button
> type="reset"
{t('knowledgeConfiguration.cancel')} variant="transparent"
</Button> onClick={() => {
<SavingButton></SavingButton> form.reset();
</div> }}
</form> >
</Form> {t('knowledgeConfiguration.cancel')}
<div className="flex-1"> </Button>
<SavingButton />
</div>
</form>
</Form>
</DataSetContext.Provider>
<div className="flex-1 p-5 overflow-auto">
{parseType === 1 && <ChunkMethodLearnMore parserId={selectedTag} />} {parseType === 1 && <ChunkMethodLearnMore parserId={selectedTag} />}
</div> </div>
</DataSetContext.Provider> </CardContent>
</div> </Card>
</section> </div>
); );
} }

View File

@ -7,6 +7,11 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'; } from '@/components/ui/dropdown-menu';
import { Modal } from '@/components/ui/modal/modal'; import { Modal } from '@/components/ui/modal/modal';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { toFixed } from '@/utils/common-util'; import { toFixed } from '@/utils/common-util';
import { formatDate } from '@/utils/date'; import { formatDate } from '@/utils/date';
@ -25,6 +30,7 @@ import {
useTraceGenerate, useTraceGenerate,
useUnBindTask, useUnBindTask,
} from './hook'; } from './hook';
export enum GenerateType { export enum GenerateType {
KnowledgeGraph = 'KnowledgeGraph', KnowledgeGraph = 'KnowledgeGraph',
Raptor = 'Raptor', Raptor = 'Raptor',
@ -191,45 +197,46 @@ const Generate: React.FC<GenerateProps> = (props) => {
}; };
return ( return (
<div className="generate"> <DropdownMenu open={open} onOpenChange={handleOpenChange}>
<DropdownMenu open={open} onOpenChange={handleOpenChange}> <DropdownMenuTrigger asChild disabled={disabled}>
<DropdownMenuTrigger asChild disabled={disabled}> <Tooltip>
<div className={cn({ 'cursor-not-allowed': disabled })}> <TooltipTrigger asChild>
<Button <Button
disabled={disabled} disabled={disabled}
variant={'transparent'} className={cn(disabled && '!cursor-not-allowed')}
variant="transparent"
size="icon"
onClick={() => { onClick={() => {
if (!disabled) { if (!disabled) {
handleOpenChange(!open); handleOpenChange(!open);
} }
}} }}
> >
<WandSparkles className="mr-2 size-4" /> <WandSparkles />
{t('knowledgeDetails.generate')}
</Button> </Button>
</div> </TooltipTrigger>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-[380px] p-5 flex flex-col gap-2 "> <TooltipContent>{t('knowledgeDetails.generate')}</TooltipContent>
{Object.values(GenerateType).map((name) => { </Tooltip>
const data = ( </DropdownMenuTrigger>
name === GenerateType.KnowledgeGraph <DropdownMenuContent className="w-[380px] p-5 flex flex-col gap-2 ">
? graphRunData {Object.values(GenerateType).map((name) => {
: raptorRunData const data = (
) as ITraceInfo; name === GenerateType.KnowledgeGraph ? graphRunData : raptorRunData
return ( ) as ITraceInfo;
<div key={name}> return (
<MenuItem <div key={name}>
name={name} <MenuItem
runGenerate={runGenerate} name={name}
data={data} runGenerate={runGenerate}
pauseGenerate={pauseGenerate} data={data}
/> pauseGenerate={pauseGenerate}
</div> />
); </div>
})} );
</DropdownMenuContent> })}
</DropdownMenu> </DropdownMenuContent>
</div> </DropdownMenu>
); );
}; };

View File

@ -137,9 +137,6 @@ export default function Dataset() {
return ( return (
<> <>
<div className="absolute top-4 right-5">
<Generate disabled={!(dataSetData.chunk_num > 0)} />
</div>
<section className="p-5 min-w-[880px]"> <section className="p-5 min-w-[880px]">
<ListFilterBar <ListFilterBar
title={t('header.dataset')} title={t('header.dataset')}
@ -158,6 +155,7 @@ export default function Dataset() {
</div> </div>
</div> </div>
} }
preChildren={<Generate disabled={!(dataSetData.chunk_num > 0)} />}
// preChildren={ // preChildren={
// <Button // <Button
// variant={'ghost'} // variant={'ghost'}

View File

@ -1,51 +1,19 @@
import { PageHeader } from '@/components/page-header';
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from '@/components/ui/breadcrumb';
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
import { useFetchKnowledgeBaseConfiguration } from '@/hooks/use-knowledge-request'; import { useFetchKnowledgeBaseConfiguration } from '@/hooks/use-knowledge-request';
import { KnowledgeBaseProvider } from '@/pages/dataset/contexts/knowledge-base-context'; import { KnowledgeBaseProvider } from '@/pages/dataset/contexts/knowledge-base-context';
import { useTranslation } from 'react-i18next';
import { Outlet } from 'react-router'; import { Outlet } from 'react-router';
import { SideBar } from './sidebar'; import { SideBar } from './sidebar';
export default function DatasetWrapper() { export default function DatasetWrapper() {
const { navigateToDatasetList } = useNavigatePage();
const { t } = useTranslation();
const { data, loading } = useFetchKnowledgeBaseConfiguration(); const { data, loading } = useFetchKnowledgeBaseConfiguration();
return ( return (
<KnowledgeBaseProvider knowledgeBase={data} loading={loading}> <KnowledgeBaseProvider knowledgeBase={data} loading={loading}>
<section className="flex h-full flex-col w-full"> <article className="pt-3 size-full grid grid-cols-[auto_1fr] grid-rows-1">
<PageHeader> <SideBar />
<Breadcrumb>
<BreadcrumbList> <Outlet />
<BreadcrumbItem> </article>
<BreadcrumbLink onClick={navigateToDatasetList}>
{t('knowledgeDetails.dataset')}
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage className="w-28 whitespace-nowrap text-ellipsis overflow-hidden">
{data.name}
</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</PageHeader>
<div className="flex flex-1 min-h-0">
<SideBar></SideBar>
<div className="flex-1 overflow-auto">
<Outlet />
</div>
</div>
</section>
</KnowledgeBaseProvider> </KnowledgeBaseProvider>
); );
} }

View File

@ -1,3 +1,15 @@
import { isEmpty } from 'lodash';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import {
LucideFolderOpen,
LucideLogs,
LucideSettings,
LucideTextSearch,
} from 'lucide-react';
import { IconFontFill } from '@/components/icon-font'; import { IconFontFill } from '@/components/icon-font';
import { RAGFlowAvatar } from '@/components/ragflow-avatar'; import { RAGFlowAvatar } from '@/components/ragflow-avatar';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
@ -9,11 +21,8 @@ import {
import { cn, formatBytes } from '@/lib/utils'; import { cn, formatBytes } from '@/lib/utils';
import { Routes } from '@/routes'; import { Routes } from '@/routes';
import { formatPureDate } from '@/utils/date'; import { formatPureDate } from '@/utils/date';
import { isEmpty } from 'lodash';
import { Banknote, FileSearch2, FolderOpen, Logs } from 'lucide-react'; import { useParams } from 'react-router';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useHandleMenuClick } from './hooks';
type PropType = { type PropType = {
refreshCount?: number; refreshCount?: number;
@ -21,7 +30,7 @@ type PropType = {
export function SideBar({ refreshCount }: PropType) { export function SideBar({ refreshCount }: PropType) {
const pathName = useSecondPathName(); const pathName = useSecondPathName();
const { handleMenuClick } = useHandleMenuClick(); const { id } = useParams();
// refreshCount: be for avatar img sync update on top left // refreshCount: be for avatar img sync update on top left
const { data } = useFetchKnowledgeBaseConfiguration({ refreshCount }); const { data } = useFetchKnowledgeBaseConfiguration({ refreshCount });
const { data: routerData } = useFetchKnowledgeGraph(); const { data: routerData } = useFetchKnowledgeGraph();
@ -30,75 +39,91 @@ export function SideBar({ refreshCount }: PropType) {
const items = useMemo(() => { const items = useMemo(() => {
const list = [ const list = [
{ {
icon: <FolderOpen className="size-4" />, icon: <LucideFolderOpen className="size-[1em]" />,
label: t(`knowledgeDetails.subbarFiles`), label: t(`knowledgeDetails.subbarFiles`),
key: Routes.DatasetBase, key: Routes.DatasetBase,
}, },
{ {
icon: <FileSearch2 className="size-4" />, icon: <LucideTextSearch className="size-[1em]" />,
label: t(`knowledgeDetails.testing`), label: t(`knowledgeDetails.testing`),
key: Routes.DatasetTesting, key: Routes.DatasetTesting,
}, },
{ {
icon: <Logs className="size-4" />, icon: <LucideLogs className="size-[1em]" />,
label: t(`knowledgeDetails.overview`), label: t(`knowledgeDetails.overview`),
key: Routes.DataSetOverview, key: Routes.DataSetOverview,
}, },
{ {
icon: <Banknote className="size-4" />, icon: <LucideSettings className="size-[1em]" />,
label: t(`knowledgeDetails.configuration`), label: t(`knowledgeDetails.configuration`),
key: Routes.DataSetSetting, key: Routes.DataSetSetting,
}, },
]; ];
if (!isEmpty(routerData?.graph)) { if (!isEmpty(routerData?.graph)) {
list.push({ list.push({
icon: <IconFontFill name="knowledgegraph" className="size-4" />, icon: <IconFontFill name="knowledgegraph" className="size-[1em]" />,
label: t(`knowledgeDetails.knowledgeGraph`), label: t(`knowledgeDetails.knowledgeGraph`),
key: Routes.KnowledgeGraph, key: Routes.KnowledgeGraph,
}); });
} }
return list; return list;
}, [t, routerData]); }, [t, routerData]);
return ( return (
<aside className="relative p-5 space-y-8"> <aside className="w-64 relative px-5 space-y-8">
<div className="flex gap-2.5 max-w-[200px] items-center"> <header
className="grid grid-cols-[auto_1fr] grid-rows-[auto_auto] gap-x-3"
style={{
gridTemplateAreas: '"avatar title" "avatar stats"',
}}
>
<RAGFlowAvatar <RAGFlowAvatar
avatar={data.avatar} avatar={data.avatar}
name={data.name} name={data.name}
className="size-16" className="size-16"
></RAGFlowAvatar> style={{ gridArea: 'avatar' }}
<div className=" text-text-secondary text-xs space-y-1 overflow-hidden"> />
<h3 className="text-lg font-semibold line-clamp-1 text-text-primary text-ellipsis overflow-hidden">
{data.name} <h3
</h3> className="text-lg font-semibold line-clamp-1 text-text-primary text-ellipsis overflow-hidden"
style={{ gridArea: 'title' }}
>
{data.name}
</h3>
<div
className="self-end text-text-secondary text-xs overflow-hidden"
style={{ gridArea: 'stats' }}
>
<div className="flex justify-between"> <div className="flex justify-between">
<span> <span>
{data.doc_num} {t('knowledgeDetails.files')} {data.doc_num} {t('knowledgeDetails.files')}
</span> </span>
<span>{formatBytes(data.size)}</span> <span>{formatBytes(data.size)}</span>
</div> </div>
<div>
<div className="mt-0.5">
{t('knowledgeDetails.created')} {formatPureDate(data.create_time)} {t('knowledgeDetails.created')} {formatPureDate(data.create_time)}
</div> </div>
</div> </div>
</div> </header>
<div className="w-[200px] flex flex-col gap-5"> <div className="flex flex-col gap-5">
{items.map((item, itemIdx) => { {items.map((item, itemIdx) => {
const active = '/' + pathName === item.key; const active = '/' + pathName === item.key;
return ( return (
<Button <Button
key={itemIdx} key={itemIdx}
variant={active ? 'secondary' : 'ghost'} asLink
variant="ghost"
className={cn( className={cn(
'w-full justify-start gap-2.5 px-3 relative h-10 text-text-secondary', 'w-full justify-start gap-2.5 px-3 relative h-10 text-base',
{ active && 'bg-bg-card text-text-primary',
'bg-bg-card': active,
'text-text-primary': active,
},
)} )}
onClick={handleMenuClick(item.key)} to={`${Routes.DatasetBase}${item.key}/${id}`}
> >
{item.icon} {item.icon}
<span>{item.label}</span> <span>{item.label}</span>

View File

@ -1,7 +1,13 @@
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { useTestRetrieval } from '@/hooks/use-knowledge-request'; import { useTestRetrieval } from '@/hooks/use-knowledge-request';
import { t } from 'i18next'; import { t } from 'i18next';
import { useState } from 'react'; import { useState } from 'react';
import { TopTitle } from '../dataset-title';
import TestingForm from './testing-form'; import TestingForm from './testing-form';
import { TestingResult } from './testing-result'; import { TestingResult } from './testing-result';
@ -21,79 +27,92 @@ export default function RetrievalTesting() {
const [count] = useState(1); const [count] = useState(1);
return ( return (
<div className="p-5"> <div className="pr-5 pb-5">
<section className="flex justify-between items-center"> <Card className="size-full bg-transparent shadow-none flex flex-col">
<TopTitle <CardHeader className="p-5 border-b-0.5 border-border-button">
title={t('knowledgeDetails.retrievalTesting')} <header>
description={t('knowledgeDetails.testingDescription')} <CardTitle as="h1">
></TopTitle> {t('knowledgeDetails.retrievalTesting')}
{/* <Button>Save as Preset</Button> */} </CardTitle>
</section>
{count === 1 ? ( <CardDescription>
<section className="flex divide-x h-full"> {t('knowledgeDetails.testingDescription')}
<div className="p-4 flex-1"> </CardDescription>
<div className="flex justify-between pb-2.5">
<span className="text-text-primary font-semibold text-2xl"> {/* <Button>Save as Preset</Button> */}
{t('knowledgeDetails.testSetting')} </header>
</span> </CardHeader>
{/* <Button variant={'outline'} onClick={addCount}>
<Plus /> Add New Test {count === 1 ? (
</Button> */} <CardContent className="flex-1 overflow-hidden p-0 grid grid-rows-1 grid-cols-2 divide-x-0.5">
<article className="size-full flex-1 flex flex-col">
<header className="px-5 py-3">
<h2 className="font-semibold text-base leading-8">
{t('knowledgeDetails.testSetting')}
</h2>
{/* <Button variant={'outline'} onClick={addCount}>
<Plus /> Add New Test
</Button> */}
</header>
<div className="flex-1 h-0">
<TestingForm
loading={loading}
setValues={setValues}
refetch={refetch}
/>
</div>
</article>
<div className="flex-1">
<TestingResult
data={data}
page={page}
loading={loading}
pageSize={pageSize}
filterValue={filterValue}
handleFilterSubmit={handleFilterSubmit}
onPaginationChange={onPaginationChange}
/>
</div> </div>
<div className="h-[calc(100vh-241px)] overflow-auto scrollbar-thin px-1"> </CardContent>
) : (
<CardContent className="p-0 flex gap-2">
<div className="flex-1">
<TestingForm <TestingForm
loading={loading} loading={loading}
setValues={setValues} setValues={setValues}
refetch={refetch} refetch={refetch}
></TestingForm> ></TestingForm>
<TestingResult
data={data}
page={page}
loading={loading}
pageSize={pageSize}
filterValue={filterValue}
handleFilterSubmit={handleFilterSubmit}
onPaginationChange={onPaginationChange}
></TestingResult>
</div> </div>
</div> <div className="flex-1">
<TestingResult <TestingForm
data={data} loading={loading}
page={page} setValues={setValues}
loading={loading} refetch={refetch}
pageSize={pageSize} ></TestingForm>
filterValue={filterValue} <TestingResult
handleFilterSubmit={handleFilterSubmit} data={data}
onPaginationChange={onPaginationChange} page={page}
></TestingResult> loading={loading}
</section> pageSize={pageSize}
) : ( filterValue={filterValue}
<section className="flex gap-2"> handleFilterSubmit={handleFilterSubmit}
<div className="flex-1"> onPaginationChange={onPaginationChange}
<TestingForm ></TestingResult>
loading={loading} </div>
setValues={setValues} </CardContent>
refetch={refetch} )}
></TestingForm> </Card>
<TestingResult
data={data}
page={page}
loading={loading}
pageSize={pageSize}
filterValue={filterValue}
handleFilterSubmit={handleFilterSubmit}
onPaginationChange={onPaginationChange}
></TestingResult>
</div>
<div className="flex-1">
<TestingForm
loading={loading}
setValues={setValues}
refetch={refetch}
></TestingForm>
<TestingResult
data={data}
page={page}
loading={loading}
pageSize={pageSize}
filterValue={filterValue}
handleFilterSubmit={handleFilterSubmit}
onPaginationChange={onPaginationChange}
></TestingResult>
</div>
</section>
)}
</div> </div>
); );
} }

View File

@ -90,43 +90,52 @@ export default function TestingForm({
return ( return (
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8"> <form
<FormContainer className="p-10"> className="size-full flex flex-col"
<SimilaritySliderFormField onSubmit={form.handleSubmit(onSubmit)}
isTooltipShown={true} >
></SimilaritySliderFormField> <div className="px-5 h-0 flex-1">
<RerankFormFields></RerankFormFields> <FormContainer className="p-5 h-full overflow-auto">
<UseKnowledgeGraphFormField name="use_kg"></UseKnowledgeGraphFormField> <SimilaritySliderFormField
<CrossLanguageFormField isTooltipShown={true}
name={'cross_languages'} ></SimilaritySliderFormField>
></CrossLanguageFormField> <RerankFormFields></RerankFormFields>
<MetadataFilter prefix=""></MetadataFilter> <UseKnowledgeGraphFormField name="use_kg"></UseKnowledgeGraphFormField>
</FormContainer> <CrossLanguageFormField
<FormField name={'cross_languages'}
control={form.control} ></CrossLanguageFormField>
name="question" <MetadataFilter prefix=""></MetadataFilter>
render={({ field }) => ( </FormContainer>
<FormItem>
{/* <FormLabel>{t('knowledgeDetails.testText')}</FormLabel> */}
<FormControl>
<Textarea {...field}></Textarea>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end">
<ButtonLoading
type="submit"
disabled={!!!trim(question)}
loading={loading}
>
{/* {!loading && <CirclePlay />} */}
{t('knowledgeDetails.testingLabel')}
<Send />
</ButtonLoading>
</div> </div>
<footer className="flex-0 p-5">
<FormField
control={form.control}
name="question"
render={({ field }) => (
<FormItem>
{/* <FormLabel>{t('knowledgeDetails.testText')}</FormLabel> */}
<FormControl>
<Textarea {...field}></Textarea>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="mt-2.5 text-end">
<ButtonLoading
type="submit"
disabled={!!!trim(question)}
loading={loading}
>
{/* {!loading && <CirclePlay />} */}
{t('knowledgeDetails.testingLabel')}
<Send />
</ButtonLoading>
</div>
</footer>
</form> </form>
</Form> </Form>
); );

View File

@ -1,9 +1,9 @@
import { EmptyType } from '@/components/empty/constant'; import { EmptyType } from '@/components/empty/constant';
import Empty from '@/components/empty/empty'; import Empty from '@/components/empty/empty';
import { FormContainer } from '@/components/form-container';
import { FilterButton } from '@/components/list-filter-bar'; import { FilterButton } from '@/components/list-filter-bar';
import { FilterPopover } from '@/components/list-filter-bar/filter-popover'; import { FilterPopover } from '@/components/list-filter-bar/filter-popover';
import { FilterCollection } from '@/components/list-filter-bar/interface'; import { FilterCollection } from '@/components/list-filter-bar/interface';
import { Card } from '@/components/ui/card';
import { RAGFlowPagination } from '@/components/ui/ragflow-pagination'; import { RAGFlowPagination } from '@/components/ui/ragflow-pagination';
import { useTranslate } from '@/hooks/common-hooks'; import { useTranslate } from '@/hooks/common-hooks';
import { useTestRetrieval } from '@/hooks/use-knowledge-request'; import { useTestRetrieval } from '@/hooks/use-knowledge-request';
@ -21,12 +21,12 @@ const similarityList: Array<{ field: keyof ITestingChunk; label: string }> = [
const ChunkTitle = ({ item }: { item: ITestingChunk }) => { const ChunkTitle = ({ item }: { item: ITestingChunk }) => {
const { t } = useTranslate('knowledgeDetails'); const { t } = useTranslate('knowledgeDetails');
return ( return (
<div className="flex gap-3 text-xs text-text-sub-title-invert italic"> <div className="text-xs text-text-sub-title-invert italic space-x-4 rtl:space-x-reverse">
{similarityList.map((x) => ( {similarityList.map((x) => (
<div key={x.field} className="space-x-1"> <p key={x.field} className="inline">
<span>{((item[x.field] as number) * 100).toFixed(2)}</span> {((item[x.field] as number) * 100).toFixed(2)}{' '}
<span>{t(camelCase(x.field))}</span> <dfn>{t(camelCase(x.field))}</dfn>
</div> </p>
))} ))}
</div> </div>
); );
@ -68,11 +68,12 @@ export function TestingResult({
}, [data.doc_aggs]); }, [data.doc_aggs]);
return ( return (
<div className="p-4 flex-1"> <article className="size-full flex flex-col">
<div className="flex justify-between pb-2.5"> <header className="flex-0 px-5 py-3 flex justify-between">
<span className="text-text-primary font-semibold text-2xl"> <h2 className="font-semibold text-base leading-8">
{t('knowledgeDetails.testResults')} {t('knowledgeDetails.testResults')}
</span> </h2>
<FilterPopover <FilterPopover
filters={filters} filters={filters}
onChange={handleFilterSubmit} onChange={handleFilterSubmit}
@ -80,43 +81,48 @@ export function TestingResult({
> >
<FilterButton></FilterButton> <FilterButton></FilterButton>
</FilterPopover> </FilterPopover>
</div> </header>
{data.chunks?.length > 0 && !loading && (
<> <div className="flex-1 h-0">
<section className="flex flex-col gap-5 overflow-auto h-[calc(100vh-241px)] scrollbar-thin mb-5"> {data.chunks?.length > 0 && !loading && (
{data.chunks?.map((x) => ( <>
<FormContainer key={x.chunk_id} className="px-5 py-2.5"> <section className="px-5 pb-5 flex flex-col gap-5 overflow-auto h-full scrollbar-thin">
<ChunkTitle item={x}></ChunkTitle> {data.chunks?.map((x) => (
<p className="!mt-2.5"> {x.content_with_weight}</p> <article>
</FormContainer> <Card
))} key={x.chunk_id}
</section> className="px-5 py-2.5 bg-transparent shadow-none"
<RAGFlowPagination >
total={data.total} <ChunkTitle item={x}></ChunkTitle>
onChange={onPaginationChange} <p className="!mt-2.5"> {x.content_with_weight}</p>
current={page} </Card>
pageSize={pageSize} </article>
></RAGFlowPagination> ))}
</> </section>
)} <RAGFlowPagination
{!data.chunks?.length && !loading && ( total={data.total}
<div className="flex justify-center items-center w-full h-[calc(100vh-280px)]"> onChange={onPaginationChange}
<div> current={page}
<Empty type={EmptyType.SearchData} iconWidth={80}> pageSize={pageSize}
{data.isRuned && ( ></RAGFlowPagination>
<div className="text-text-secondary"> </>
{t('knowledgeDetails.noTestResultsForRuned')} )}
{!data.chunks?.length && !loading && (
<div className="size-full p-5 flex justify-center items-center">
<div>
<Empty type={EmptyType.SearchData} iconWidth={80}>
<div className="text-text-secondary text-sm">
{t(
data.isRuned
? 'knowledgeDetails.noTestResultsForRuned'
: 'knowledgeDetails.noTestResultsForNotRuned',
)}
</div> </div>
)} </Empty>
{!data.isRuned && ( </div>
<div className="text-text-secondary">
{t('knowledgeDetails.noTestResultsForNotRuned')}
</div>
)}
</Empty>
</div> </div>
</div> )}
)} </div>
</div> </article>
); );
} }

View File

@ -66,10 +66,10 @@ export default function Datasets() {
searchUrl.delete('isCreate'); searchUrl.delete('isCreate');
setSearchUrl(searchUrl); setSearchUrl(searchUrl);
} }
}, [isCreate, showModal, searchUrl, setSearchUrl]); }, [isCreate, showModal, searchUrl, setSearchUrl, queryClient]);
return ( return (
<> <>
<section className="py-4 flex-1 flex flex-col"> <section className="py-4 pt-8 flex-1 flex flex-col">
{(!kbs?.length || kbs?.length <= 0) && !searchString && ( {(!kbs?.length || kbs?.length <= 0) && !searchString && (
<div className="flex w-full items-center justify-center h-[calc(100vh-164px)]"> <div className="flex w-full items-center justify-center h-[calc(100vh-164px)]">
<EmptyAppCard <EmptyAppCard

View File

@ -42,11 +42,11 @@ export function Banner() {
export function NextBanner() { export function NextBanner() {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<section className="text-5xl pt-10 pb-14 font-bold px-10"> <h1 className="text-5xl font-bold leading-normal">
<span className="text-text-primary">{t('header.welcome')}</span> <span className="text-text-primary">{t('header.welcome')} </span>
<span className="pl-3 text-transparent bg-clip-text bg-gradient-to-l from-[#40EBE3] to-[#4A51FF]"> <span className="text-transparent bg-clip-text bg-gradient-to-l from-[#40EBE3] to-[#4A51FF]">
RAGFlow RAGFlow
</span> </span>
</section> </h1>
); );
} }

View File

@ -1,16 +1,18 @@
import { PageContainer } from '@/layouts/page-container';
import { Applications } from './applications'; import { Applications } from './applications';
import { NextBanner } from './banner'; import { NextBanner } from './banner';
import { Datasets } from './datasets'; import { Datasets } from './datasets';
const Home = () => { const Home = () => {
return ( return (
<section> <PageContainer>
<NextBanner></NextBanner> <header className="mb-8">
<section className="h-[calc(100dvh-260px)] overflow-auto px-10"> <NextBanner />
<Datasets></Datasets> </header>
<Applications></Applications>
</section> <Datasets />
</section> <Applications />
</PageContainer>
); );
}; };

View File

@ -71,43 +71,24 @@ function LoginFormContent({
</div> </div>
<div className=" w-full max-w-[540px] bg-bg-component backdrop-blur-sm rounded-2xl shadow-xl pt-14 pl-10 pr-10 pb-2 border border-border-button "> <div className=" w-full max-w-[540px] bg-bg-component backdrop-blur-sm rounded-2xl shadow-xl pt-14 pl-10 pr-10 pb-2 border border-border-button ">
{!disablePasswordLogin && ( {!disablePasswordLogin && (
<Form {...form}> <Form {...form}>
<form <form
className="flex flex-col gap-8 text-text-primary " className="flex flex-col gap-8 text-text-primary "
data-testid="auth-form" data-testid="auth-form"
data-active={isActiveFace ? 'true' : undefined} data-active={isActiveFace ? 'true' : undefined}
onSubmit={form.handleSubmit(onCheck)} onSubmit={form.handleSubmit(onCheck)}
> >
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel required>{t('emailLabel')}</FormLabel>
<FormControl>
<Input
data-testid="auth-email"
placeholder={t('emailPlaceholder')}
autoComplete="email"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{title === 'register' && (
<FormField <FormField
control={form.control} control={form.control}
name="nickname" name="email"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel required>{t('nicknameLabel')}</FormLabel> <FormLabel required>{t('emailLabel')}</FormLabel>
<FormControl> <FormControl>
<Input <Input
data-testid="auth-nickname" data-testid="auth-email"
placeholder={t('nicknamePlaceholder')} placeholder={t('emailPlaceholder')}
autoComplete="username" autoComplete="email"
{...field} {...field}
/> />
</FormControl> </FormControl>
@ -115,73 +96,92 @@ function LoginFormContent({
</FormItem> </FormItem>
)} )}
/> />
)} {title === 'register' && (
<FormField
<FormField control={form.control}
control={form.control} name="nickname"
name="password" render={({ field }) => (
render={({ field }) => ( <FormItem>
<FormItem> <FormLabel required>{t('nicknameLabel')}</FormLabel>
<FormLabel required>{t('passwordLabel')}</FormLabel> <FormControl>
<FormControl> <Input
<div className="relative"> data-testid="auth-nickname"
<Input placeholder={t('nicknamePlaceholder')}
data-testid="auth-password" autoComplete="username"
type={'password'} {...field}
placeholder={t('passwordPlaceholder')} />
autoComplete={ </FormControl>
title === 'login' <FormMessage />
? 'current-password' </FormItem>
: 'new-password' )}
} />
{...field}
/>
</div>
</FormControl>
<FormMessage />
</FormItem>
)} )}
/>
{title === 'login' && (
<FormField <FormField
control={form.control} control={form.control}
name="remember" name="password"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel required>{t('passwordLabel')}</FormLabel>
<FormControl> <FormControl>
<div className="flex gap-2"> <div className="relative">
<Checkbox <Input
checked={field.value} data-testid="auth-password"
onCheckedChange={(checked) => { type={'password'}
field.onChange(checked); placeholder={t('passwordPlaceholder')}
}} autoComplete={
title === 'login'
? 'current-password'
: 'new-password'
}
{...field}
/> />
<FormLabel
className={cn(' hover:text-text-primary', {
'text-text-disabled': !field.value,
'text-text-primary': field.value,
})}
>
{t('rememberMe')}
</FormLabel>
</div> </div>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
)}
<ButtonLoading {title === 'login' && (
data-testid="auth-submit" <FormField
type="submit" control={form.control}
loading={loading} name="remember"
className="bg-metallic-gradient border-b-[#00BEB4] border-b-2 hover:bg-metallic-gradient hover:border-b-[#02bcdd] w-full my-8" render={({ field }) => (
> <FormItem>
{title === 'login' ? t('login') : t('continue')} <FormControl>
</ButtonLoading> <div className="flex gap-2">
</form> <Checkbox
</Form> checked={field.value}
onCheckedChange={(checked) => {
field.onChange(checked);
}}
/>
<FormLabel
className={cn(' hover:text-text-primary', {
'text-text-disabled': !field.value,
'text-text-primary': field.value,
})}
>
{t('rememberMe')}
</FormLabel>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
<ButtonLoading
data-testid="auth-submit"
type="submit"
loading={loading}
className="bg-metallic-gradient border-b-[#00BEB4] border-b-2 hover:bg-metallic-gradient hover:border-b-[#02bcdd] w-full my-8"
>
{title === 'login' ? t('login') : t('continue')}
</ButtonLoading>
</form>
</Form>
)} )}
{title === 'login' && channels && channels.length > 0 && ( {title === 'login' && channels && channels.length > 0 && (
@ -361,7 +361,7 @@ const Login = () => {
<div className=" h-[inherit] relative overflow-auto"> <div className=" h-[inherit] relative overflow-auto">
<BgSvg isPaused /> <BgSvg isPaused />
<div className="absolute top-3 flex flex-col items-center mb-12 w-full text-text-primary"> <div className="z-20 absolute top-3 flex flex-col items-center mb-12 w-full text-text-primary">
<div className="flex items-center mb-4 w-full pl-10 pt-10 "> <div className="flex items-center mb-4 w-full pl-10 pt-10 ">
<div className="w-12 h-12 p-2 rounded-lg flex items-center justify-center mr-3"> <div className="w-12 h-12 p-2 rounded-lg flex items-center justify-center mr-3">
<img <img
@ -399,4 +399,4 @@ const Login = () => {
); );
}; };
export default Login; export default Login;

View File

@ -181,7 +181,7 @@ const ChatCard = forwardRef(function ChatCard(
</div> </div>
<div className="space-x-2"> <div className="space-x-2">
<Tooltip> <Tooltip>
<TooltipTrigger> <TooltipTrigger asChild>
<Button <Button
variant="ghost" variant="ghost"
size="icon-sm" size="icon-sm"

View File

@ -1,36 +1,18 @@
import EmbedDialog from '@/components/embed-dialog';
import { useShowEmbedModal } from '@/components/embed-dialog/use-show-embed-dialog';
import { PageHeader } from '@/components/page-header';
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from '@/components/ui/breadcrumb';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { SharedFrom } from '@/constants/chat';
import { import {
useFetchConversationList, useFetchConversationList,
useFetchConversationManually, useFetchConversationManually,
useFetchDialog,
useGetChatSearchParams, useGetChatSearchParams,
} from '@/hooks/use-chat-request'; } from '@/hooks/use-chat-request';
import { IClientConversation } from '@/interfaces/database/chat'; import { IClientConversation } from '@/interfaces/database/chat';
import { NextLayoutContainer } from '@/layouts/next';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { Routes } from '@/routes';
import { useMount } from 'ahooks'; import { useMount } from 'ahooks';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { import { LucideArrowBigLeft, LucideArrowUpRight } from 'lucide-react';
LucideArrowBigLeft,
LucideArrowUpRight,
LucideSend,
} from 'lucide-react';
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router';
import { useHandleClickConversationCard } from '../hooks/use-click-card'; import { useHandleClickConversationCard } from '../hooks/use-click-card';
import { ChatSettings } from './app-settings/chat-settings'; import { ChatSettings } from './app-settings/chat-settings';
import { MultipleChatBox } from './chat-box/next-multiple-chat-box'; import { MultipleChatBox } from './chat-box/next-multiple-chat-box';
@ -40,8 +22,6 @@ import { useAddChatBox } from './use-add-box';
import { useSwitchDebugMode } from './use-switch-debug-mode'; import { useSwitchDebugMode } from './use-switch-debug-mode';
export default function Chat() { export default function Chat() {
const { id } = useParams();
const { data } = useFetchDialog();
const { t } = useTranslation(); const { t } = useTranslation();
const [currentConversation, setCurrentConversation] = const [currentConversation, setCurrentConversation] =
useState<IClientConversation>({} as IClientConversation); useState<IClientConversation>({} as IClientConversation);
@ -55,9 +35,6 @@ export default function Chat() {
const { removeChatBox, addChatBox, chatBoxIds, hasSingleChatBox } = const { removeChatBox, addChatBox, chatBoxIds, hasSingleChatBox } =
useAddChatBox(isDebugMode); useAddChatBox(isDebugMode);
const { showEmbedModal, hideEmbedModal, embedVisible, beta } =
useShowEmbedModal();
const { conversationId, isNew } = useGetChatSearchParams(); const { conversationId, isNew } = useGetChatSearchParams();
const { data: dialogList } = useFetchConversationList(); const { data: dialogList } = useFetchConversationList();
@ -93,7 +70,7 @@ export default function Chat() {
if (isDebugMode) { if (isDebugMode) {
return ( return (
<section <section
className="pt-5 pb-16 h-[100vh] flex flex-col" className="pt-5 pb-14 h-[100vh] flex flex-col"
data-testid="chat-detail-multimodel-root" data-testid="chat-detail-multimodel-root"
> >
<header className="px-10 pb-5"> <header className="px-10 pb-5">
@ -126,77 +103,46 @@ export default function Chat() {
} }
return ( return (
<section className="h-full flex flex-col" data-testid="chat-detail"> <NextLayoutContainer>
<PageHeader> <section className="h-full flex flex-col" data-testid="chat-detail">
<Breadcrumb> <article className="flex flex-1 min-h-0 pb-9">
<BreadcrumbList> <Sessions handleConversationCardClick={handleSessionClick}></Sessions>
<BreadcrumbItem>
<BreadcrumbLink
// Not friendly for keyboard navigation
// onClick={navigateToChatList}
href={Routes.Chats}
>
{t('chat.chat')}
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>{data.name}</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
<Button onClick={showEmbedModal} data-testid="chat-detail-embed-open">
<LucideSend />
{t('common.embedIntoSite')}
</Button>
</PageHeader>
<article className="flex flex-1 min-h-0 pb-9"> <Card className="flex-1 min-w-0 bg-transparent border-none shadow-none h-full">
<Sessions handleConversationCardClick={handleSessionClick}></Sessions> <CardContent className="flex p-0 h-full">
<Card className="flex flex-col flex-1 bg-transparent min-w-0">
<CardHeader
className={cn('p-5', {
'border-b-0.5 border-border-button': hasSingleChatBox,
})}
>
<CardTitle className="flex justify-between items-center text-base gap-2">
<div className="truncate">{currentConversationName}</div>
<Card className="flex-1 min-w-0 bg-transparent border-none shadow-none h-full"> <Button
<CardContent className="flex p-0 h-full"> variant="ghost"
<Card className="flex flex-col flex-1 bg-transparent min-w-0"> onClick={switchDebugMode}
<CardHeader data-testid="chat-detail-multimodel-toggle"
className={cn('p-5', { >
'border-b-0.5 border-border-button': hasSingleChatBox, <LucideArrowUpRight />
})} {t('chat.multipleModels')}
> </Button>
<CardTitle className="flex justify-between items-center text-base gap-2"> </CardTitle>
<div className="truncate">{currentConversationName}</div> </CardHeader>
<Button <CardContent className="flex-1 p-0 min-h-0">
variant={'ghost'} <SingleChatBox
onClick={switchDebugMode} controller={controller}
data-testid="chat-detail-multimodel-toggle" stopOutputMessage={stopOutputMessage}
> conversation={currentConversation}
<LucideArrowUpRight /> {t('chat.multipleModels')} />
</Button> </CardContent>
</CardTitle> </Card>
</CardHeader>
<CardContent className="flex-1 p-0 min-h-0">
<SingleChatBox
controller={controller}
stopOutputMessage={stopOutputMessage}
conversation={currentConversation}
/>
</CardContent>
</Card>
<ChatSettings hasSingleChatBox={hasSingleChatBox}></ChatSettings> <ChatSettings hasSingleChatBox={hasSingleChatBox}></ChatSettings>
</CardContent> </CardContent>
</Card> </Card>
</article> </article>
</section>
{embedVisible && ( </NextLayoutContainer>
<EmbedDialog
visible={embedVisible}
hideModal={hideEmbedModal}
token={id!}
from={SharedFrom.Chat}
beta={beta}
isAgent={false}
></EmbedDialog>
)}
</section>
); );
} }

View File

@ -1,9 +1,17 @@
import { ConfirmDeleteDialog } from '@/components/confirm-delete-dialog'; import { ConfirmDeleteDialog } from '@/components/confirm-delete-dialog';
import EmbedDialog from '@/components/embed-dialog';
import { useShowEmbedModal } from '@/components/embed-dialog/use-show-embed-dialog';
import { MoreButton } from '@/components/more-button'; import { MoreButton } from '@/components/more-button';
import { RAGFlowAvatar } from '@/components/ragflow-avatar'; import { RAGFlowAvatar } from '@/components/ragflow-avatar';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox'; import { Checkbox } from '@/components/ui/checkbox';
import { SearchInput } from '@/components/ui/input'; import { SearchInput } from '@/components/ui/input';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip';
import { SharedFrom } from '@/constants/chat';
import { useSetModalState } from '@/hooks/common-hooks'; import { useSetModalState } from '@/hooks/common-hooks';
import { import {
useFetchDialog, useFetchDialog,
@ -15,11 +23,13 @@ import {
LucideListChecks, LucideListChecks,
LucidePanelLeftClose, LucidePanelLeftClose,
LucidePlus, LucidePlus,
LucideSend,
LucideTrash2, LucideTrash2,
LucideUndo2, LucideUndo2,
} from 'lucide-react'; } from 'lucide-react';
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router';
import { useChatUrlParams } from '../hooks/use-chat-url'; import { useChatUrlParams } from '../hooks/use-chat-url';
import { useHandleClickConversationCard } from '../hooks/use-click-card'; import { useHandleClickConversationCard } from '../hooks/use-click-card';
import { useSelectDerivedConversationList } from '../hooks/use-select-conversation-list'; import { useSelectDerivedConversationList } from '../hooks/use-select-conversation-list';
@ -132,6 +142,10 @@ export function Sessions({ handleConversationCardClick }: SessionProps) {
const selectedCount = useMemo(() => selectedIds.size, [selectedIds]); const selectedCount = useMemo(() => selectedIds.size, [selectedIds]);
const { id } = useParams();
const { showEmbedModal, hideEmbedModal, embedVisible, beta } =
useShowEmbedModal();
if (!visible) { if (!visible) {
return ( return (
<div className="p-5"> <div className="p-5">
@ -158,7 +172,7 @@ export function Sessions({ handleConversationCardClick }: SessionProps) {
role="complementary" role="complementary"
data-testid="chat-detail-sessions" data-testid="chat-detail-sessions"
> >
<header className="flex items-center text-base justify-between gap-2"> <header className="flex items-center text-base justify-between gap-4">
<div className="flex gap-3 items-center min-w-0"> <div className="flex gap-3 items-center min-w-0">
<RAGFlowAvatar <RAGFlowAvatar
avatar={data.icon} avatar={data.icon}
@ -169,10 +183,32 @@ export function Sessions({ handleConversationCardClick }: SessionProps) {
<span className="flex-1 truncate">{data.name}</span> <span className="flex-1 truncate">{data.name}</span>
</div> </div>
<Tooltip>
<TooltipTrigger asChild>
<Button
onClick={showEmbedModal}
size="icon-xs"
data-testid="chat-detail-embed-open"
>
<LucideSend />
</Button>
</TooltipTrigger>
<TooltipContent>{t('common.embedIntoSite')}</TooltipContent>
</Tooltip>
<EmbedDialog
visible={embedVisible}
hideModal={hideEmbedModal}
token={id!}
from={SharedFrom.Chat}
beta={beta}
isAgent={false}
/>
<Button <Button
variant="transparent" variant="transparent"
size="icon-sm" size="icon-sm"
className="border-0" className="border-0 ml-auto"
onClick={switchVisible} onClick={switchVisible}
data-testid="chat-detail-sessions-close" data-testid="chat-detail-sessions-close"
> >

View File

@ -1,16 +1,7 @@
import { useFetchTokenListBeforeOtherStep } from '@/components/embed-dialog/use-show-embed-dialog'; import { useFetchTokenListBeforeOtherStep } from '@/components/embed-dialog/use-show-embed-dialog';
import { PageHeader } from '@/components/page-header';
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from '@/components/ui/breadcrumb';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { SharedFrom } from '@/constants/chat'; import { SharedFrom } from '@/constants/chat';
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
import { import {
useFetchTenantInfo, useFetchTenantInfo,
useFetchUserInfo, useFetchUserInfo,
@ -31,7 +22,6 @@ import { SearchSetting } from './search-setting';
import SearchingPage from './searching'; import SearchingPage from './searching';
export default function SearchPage() { export default function SearchPage() {
const { navigateToSearchList } = useNavigatePage();
const [isSearching, setIsSearching] = useState(false); const [isSearching, setIsSearching] = useState(false);
const { data: SearchData } = useFetchSearchDetail(); const { data: SearchData } = useFetchSearchDetail();
const { beta, handleOperate } = useFetchTokenListBeforeOtherStep(); const { beta, handleOperate } = useFetchTokenListBeforeOtherStep();
@ -57,22 +47,7 @@ export default function SearchPage() {
}, [isSearching]); }, [isSearching]);
return ( return (
<section data-testid="search-detail"> <section className="size-full relative" data-testid="search-detail">
<PageHeader>
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink onClick={navigateToSearchList}>
{t('header.search')}
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>{SearchData?.name}</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</PageHeader>
<div className="flex gap-3 w-full bg-bg-base"> <div className="flex gap-3 w-full bg-bg-base">
<div className="flex-1"> <div className="flex-1">
{!isSearching && ( {!isSearching && (
@ -128,9 +103,8 @@ export default function SearchPage() {
// ></EmbedDialog> // ></EmbedDialog>
} }
</div> </div>
<div className="absolute end-5 top-4 "> <div className="absolute end-5 top-4">
<Button <Button
className="bg-text-primary text-bg-base border-b-accent-primary border-b-2"
onClick={() => { onClick={() => {
handleOperate().then((res) => { handleOperate().then((res) => {
console.log(res, 'res'); console.log(res, 'res');

View File

@ -1,52 +1,15 @@
import { Outlet } from 'react-router'; import { Outlet } from 'react-router';
import { SideBar } from './sidebar'; import { SideBar } from './sidebar';
import { PageHeader } from '@/components/page-header';
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from '@/components/ui/breadcrumb';
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { House } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import styles from './index.module.less';
const UserSetting = () => { const UserSetting = () => {
const { t } = useTranslation();
const { navigateToHome } = useNavigatePage();
return ( return (
<section className="flex flex-col h-full"> <section className="pt-8 size-full grid grid-cols-[auto_1fr] grid-rows-1">
<PageHeader> <SideBar></SideBar>
<Breadcrumb>
<BreadcrumbList> <div className={cn('pr-6 pb-6 flex flex-1 rounded-lg overflow-hidden')}>
<BreadcrumbItem> <Outlet></Outlet>
<BreadcrumbLink onClick={navigateToHome}>
<House className="size-4" />
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>{t('setting.profile')}</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</PageHeader>
<div
className={cn(
styles.settingWrapper,
'overflow-auto flex flex-1 pt-4 pr-4 pb-4',
)}
>
<SideBar></SideBar>
<div className={cn(styles.outletWrapper, 'flex flex-1 rounded-lg')}>
<Outlet></Outlet>
</div>
</div> </div>
</section> </section>
); );

View File

@ -165,62 +165,71 @@ const routeConfigOptions = [
], ],
}, },
{ {
path: Routes.Datasets, path: Routes.Chat + '/:id',
layout: false, Component: () => import('@/pages/next-chats/chat'),
},
{
path: Routes.Root,
Component: () => import('@/layouts/next'), Component: () => import('@/layouts/next'),
children: [ children: [
{ {
path: Routes.Datasets, path: Routes.Datasets,
Component: () => import('@/pages/datasets'), Component: () => import('@/pages/datasets'),
}, },
], {
}, path: Routes.DatasetBase,
{ Component: () => import('@/pages/dataset'),
path: Routes.Chats, children: [
layout: false, {
Component: () => import('@/layouts/next'), path: `${Routes.Dataset}/:id`,
children: [ Component: () => import('@/pages/dataset/dataset'),
},
{
path: `${Routes.DatasetBase}${Routes.DatasetTesting}/:id`,
Component: () => import('@/pages/dataset/testing'),
},
{
path: `${Routes.DatasetBase}${Routes.KnowledgeGraph}/:id`,
Component: () => import('@/pages/dataset/knowledge-graph'),
},
{
path: `${Routes.DatasetBase}${Routes.DataSetOverview}/:id`,
Component: () => import('@/pages/dataset/dataset-overview'),
},
{
path: `${Routes.DatasetBase}${Routes.DataSetSetting}/:id`,
Component: () => import('@/pages/dataset/dataset-setting'),
},
],
},
{ {
path: Routes.Chats, path: Routes.Chats,
Component: () => import('@/pages/next-chats'), Component: () => import('@/pages/next-chats'),
}, },
],
},
{
path: Routes.Chat + '/:id',
layout: false,
Component: () => import('@/pages/next-chats/chat'),
},
{
path: Routes.Searches,
layout: false,
Component: () => import('@/layouts/next'),
children: [
{ {
path: Routes.Searches, path: Routes.Searches,
Component: () => import('@/pages/next-searches'), Component: () => import('@/pages/next-searches'),
}, },
], {
}, path: `${Routes.Search}/:id`,
{ layout: false,
path: Routes.Memories, Component: () => import('@/pages/next-search'),
layout: false, },
Component: () => import('@/layouts/next'), {
children: [ path: Routes.Agents,
Component: () => import('@/pages/agents'),
},
{
path: Routes.AgentTemplates,
layout: false,
Component: () => import('@/pages/agents/agent-templates'),
},
{ {
path: Routes.Memories, path: Routes.Memories,
Component: () => import('@/pages/memories'), Component: () => import('@/pages/memories'),
}, },
],
},
{
path: `${Routes.Memory}`,
layout: false,
Component: () => import('@/layouts/next'),
children: [
{ {
path: `${Routes.Memory}`, path: `${Routes.Memory}`,
layout: false,
Component: () => import('@/pages/memory'), Component: () => import('@/pages/memory'),
children: [ children: [
{ {
@ -233,181 +242,105 @@ const routeConfigOptions = [
}, },
], ],
}, },
],
},
{
path: `${Routes.Search}/:id`,
layout: false,
Component: () => import('@/pages/next-search'),
},
{
path: `${Routes.SearchShare}`,
layout: false,
Component: () => import('@/pages/next-search/share'),
},
{
path: Routes.Agents,
layout: false,
Component: () => import('@/layouts/next'),
children: [
{
path: Routes.Agents,
Component: () => import('@/pages/agents'),
},
],
},
{
path: `${Routes.AgentLogPage}/:id`,
layout: false,
Component: () => import('@/pages/agents/agent-log-page'),
},
{
path: `${Routes.Agent}/:id`,
layout: false,
Component: () => import('@/pages/agent'),
},
{
path: Routes.AgentExplore,
layout: false,
Component: () => import('@/pages/agent/explore'),
errorElement: <FallbackComponent />,
},
{
path: Routes.AgentTemplates,
layout: false,
Component: () => import('@/pages/agents/agent-templates'),
},
{
path: Routes.Files,
layout: false,
Component: () => import('@/layouts/next'),
children: [
{ {
path: Routes.Files, path: Routes.Files,
Component: () => import('@/pages/files'), Component: () => import('@/pages/files'),
}, },
],
},
{
path: Routes.DatasetBase,
layout: false,
Component: () => import('@/layouts/next'),
children: [
{ {
path: Routes.DatasetBase, path: Routes.UserSetting,
element: <Navigate to={Routes.Dataset} replace />, Component: () => import('@/pages/user-setting'),
}, layout: false,
],
},
{
path: Routes.DatasetBase,
layout: false,
Component: () => import('@/pages/dataset'),
children: [
{
path: `${Routes.Dataset}/:id`,
Component: () => import('@/pages/dataset/dataset'),
},
{
path: `${Routes.DatasetBase}${Routes.DatasetTesting}/:id`,
Component: () => import('@/pages/dataset/testing'),
},
{
path: `${Routes.DatasetBase}${Routes.KnowledgeGraph}/:id`,
Component: () => import('@/pages/dataset/knowledge-graph'),
},
{
path: `${Routes.DatasetBase}${Routes.DataSetOverview}/:id`,
Component: () => import('@/pages/dataset/dataset-overview'),
},
{
path: `${Routes.DatasetBase}${Routes.DataSetSetting}/:id`,
Component: () => import('@/pages/dataset/dataset-setting'),
},
],
},
{
path: `${Routes.DataflowResult}`,
layout: false,
Component: () => import('@/pages/dataflow-result'),
},
{
path: `${Routes.ParsedResult}/chunks`,
layout: false,
Component: () =>
import('@/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk'),
},
{
path: Routes.Chunk,
layout: false,
children: [
{
path: Routes.Chunk,
Component: () => import('@/pages/chunk'),
children: [ children: [
{ {
path: `${Routes.ChunkResult}/:id`, path: Routes.UserSetting,
Component: () => import('@/pages/chunk/chunk-result'), element: (
<Navigate to={`/user-setting${Routes.DataSource}`} replace />
),
}, },
{ {
path: `${Routes.ResultView}/:id`, path: `${Routes.UserSetting}/profile`,
Component: () => import('@/pages/chunk/result-view'), Component: () => import('@/pages/user-setting/profile'),
},
{
path: `${Routes.UserSetting}/locale`,
Component: () => import('@/pages/user-setting/setting-locale'),
},
{
path: `${Routes.UserSetting}/model`,
Component: () => import('@/pages/user-setting/setting-model'),
},
{
path: `${Routes.UserSetting}/team`,
Component: () => import('@/pages/user-setting/setting-team'),
},
{
path: `${Routes.UserSetting}${Routes.Api}`,
Component: () => import('@/pages/user-setting/setting-api'),
},
{
path: `${Routes.UserSetting}${Routes.Mcp}`,
Component: () => import('@/pages/user-setting/mcp'),
},
{
path: `${Routes.UserSetting}${Routes.DataSource}${Routes.DataSourceDetailPage}`,
Component: () =>
import('@/pages/user-setting/data-source/data-source-detail-page'),
},
{
path: `${Routes.UserSetting}${Routes.DataSource}`,
Component: () => import('@/pages/user-setting/data-source'),
}, },
], ],
}, },
], ],
}, },
{ {
path: Routes.Chunk, path: `${Routes.SearchShare}`,
layout: false, Component: () => import('@/pages/next-search/share'),
Component: () => import('@/pages/chunk'),
}, },
{ {
path: '/user-setting', path: Routes.Agent,
Component: () => import('@/pages/user-setting'),
layout: false,
children: [ children: [
{ {
path: '/user-setting', path: `${Routes.Agent}/:id`,
element: <Navigate to={`/user-setting${Routes.DataSource}`} replace />, Component: () => import('@/pages/agent'),
}, },
{ {
path: '/user-setting/profile', path: Routes.AgentExplore,
Component: () => import('@/pages/user-setting/profile'), Component: () => import('@/pages/agent/explore'),
}, errorElement: <FallbackComponent />,
{
path: '/user-setting/locale',
Component: () => import('@/pages/user-setting/setting-locale'),
},
{
path: '/user-setting/model',
Component: () => import('@/pages/user-setting/setting-model'),
},
{
path: '/user-setting/team',
Component: () => import('@/pages/user-setting/setting-team'),
},
{
path: `/user-setting${Routes.Api}`,
Component: () => import('@/pages/user-setting/setting-api'),
},
{
path: `/user-setting${Routes.Mcp}`,
Component: () => import('@/pages/user-setting/mcp'),
},
{
path: `/user-setting${Routes.DataSource}`,
Component: () => import('@/pages/user-setting/data-source'),
}, },
], ],
}, },
{ {
path: `/user-setting${Routes.DataSource}${Routes.DataSourceDetailPage}`, path: `${Routes.AgentLogPage}/:id`,
Component: () => Component: () => import('@/pages/agents/agent-log-page'),
import('@/pages/user-setting/data-source/data-source-detail-page'), },
{
layout: false, path: `${Routes.DataflowResult}`,
Component: () => import('@/pages/dataflow-result'),
},
{
path: Routes.Chunk,
children: [
{
path: `${Routes.Chunk}`,
Component: () => import('@/pages/chunk'),
},
{
path: `${Routes.ParsedResult}/chunks`,
Component: () =>
import('@/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk'),
},
{
path: `${Routes.ChunkResult}/:id`,
Component: () => import('@/pages/chunk/chunk-result'),
},
{
path: `${Routes.ResultView}/:id`,
Component: () => import('@/pages/chunk/result-view'),
},
],
}, },
{ {
path: Routes.Admin, path: Routes.Admin,
@ -420,7 +353,6 @@ const routeConfigOptions = [
{ {
path: Routes.Admin, path: Routes.Admin,
Component: () => import('@/pages/admin/layouts/authorized-layout'), Component: () => import('@/pages/admin/layouts/authorized-layout'),
children: [ children: [
{ {
path: `${Routes.AdminUserManagement}/:id`, path: `${Routes.AdminUserManagement}/:id`,
@ -428,7 +360,6 @@ const routeConfigOptions = [
}, },
{ {
Component: () => import('@/pages/admin/layouts/navigation-layout'), Component: () => import('@/pages/admin/layouts/navigation-layout'),
children: [ children: [
{ {
path: Routes.AdminServices, path: Routes.AdminServices,

View File

@ -0,0 +1,5 @@
export const supportsCssAnchor =
CSS.supports('position-anchor', '--anchor-name') &&
CSS.supports('anchor-name', '--anchor-name') &&
CSS.supports('top', 'anchor(--anchor-name bottom)') &&
CSS.supports('width', 'anchor-size(--anchor-name width)');

View File

@ -191,9 +191,18 @@ module.exports = {
'linear-gradient(104deg, rgb(var(--text-primary)) 30%, var(--metallic) 50%, rgb(var(--text-primary)) 70%)', 'linear-gradient(104deg, rgb(var(--text-primary)) 30%, var(--metallic) 50%, rgb(var(--text-primary)) 70%)',
}, },
borderRadius: { borderRadius: {
lg: `var(--radius)`, px: '1px',
md: `calc(var(--radius) - 2px)`,
sm: 'calc(var(--radius) - 4px)', '4xl': '1rem' /* 16px */,
'3xl': '0.75rem' /* 12px */,
'2xl': '0.625rem' /* 10px */,
xl: '0.5rem' /* 8px */,
lg: '0.4375rem' /* 7px */,
DEFAULT: '0.375rem' /* 6px */,
sm: '0.3125rem' /* 5px */,
xs: '0.25rem' /* 4px */,
'2xs': '0.1875rem' /* 3px */,
'3xs': '0.125' /* 2px */,
}, },
fontFamily: { fontFamily: {
sans: ['var(--font-sans)', ...fontFamily.sans], sans: ['var(--font-sans)', ...fontFamily.sans],
@ -215,12 +224,21 @@ module.exports = {
from: { transform: 'rotate(0deg)' }, from: { transform: 'rotate(0deg)' },
to: { transform: 'rotate(-360deg)' }, to: { transform: 'rotate(-360deg)' },
}, },
'bell-shake': {
'0%,25%': { transform: 'rotate(0)', transformOrigin: 'center 25% ' },
'3.125%': { transform: 'rotate(-12.5deg)' },
'9.375%': { transform: 'rotate(11deg)' },
'15.625%': { transform: 'rotate(-9.5deg)' },
'21.875%': { transform: 'rotate(7.5deg)' },
},
}, },
animation: { animation: {
'accordion-down': 'accordion-down 0.2s ease-out', 'accordion-down': 'accordion-down 0.2s ease-out',
'accordion-up': 'accordion-up 0.2s ease-out', 'accordion-up': 'accordion-up 0.2s ease-out',
'caret-blink': 'caret-blink 1.25s ease-out infinite', 'caret-blink': 'caret-blink 1.25s ease-out infinite',
'spin-reverse': 'spin-reverse 1s linear infinite', 'spin-reverse': 'spin-reverse 1s linear infinite',
'bell-shake':
'bell-shake 2s 1s cubic-bezier(0.33, 1, 0.68, 1) infinite',
}, },
}, },
}, },

View File

@ -286,6 +286,14 @@
} }
} }
@layer base {
* {
/* Make this default */
scrollbar-width: thin;
scrollbar-color: var(--border-default) var(--bg-card);
}
}
@layer utilities { @layer utilities {
.scrollbar-auto { .scrollbar-auto {
/* hide scrollbar */ /* hide scrollbar */