mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-18 21:57:27 +08:00
refactor(ui): adjust dataset page styles (#13452)
### What problem does this PR solve? - Adjust UI styles in **Dataset** pages. - Adjust several shared components styles - Modify files and directory structure in `src/layouts` ### Type of change - [x] Refactoring
This commit is contained in:
@@ -1,8 +1,7 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { BrushCleaning } from 'lucide-react';
|
||||
import { ReactNode, useCallback } from 'react';
|
||||
import { ReactNode, useId } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
ConfirmDeleteDialog,
|
||||
@@ -30,58 +29,69 @@ export function BulkOperateBar({
|
||||
className,
|
||||
unit,
|
||||
}: BulkOperateBarProps) {
|
||||
const isDeleteItem = useCallback((id: string) => {
|
||||
return id === 'delete';
|
||||
}, []);
|
||||
const { t } = useTranslation();
|
||||
const ariaDescriptionId = useId();
|
||||
|
||||
return (
|
||||
<Card className={cn('mb-4', className)}>
|
||||
<CardContent className="p-1 pl-5 flex items-center gap-6">
|
||||
<section className="text-text-sub-title-invert flex items-center gap-2">
|
||||
<span>
|
||||
{t('common.selected')}: {count}{' '}
|
||||
{unit ?? t('knowledgeDetails.files')}
|
||||
</span>
|
||||
<BrushCleaning className="size-3" />
|
||||
</section>
|
||||
<Separator orientation={'vertical'} className="h-3"></Separator>
|
||||
<Card
|
||||
className={className}
|
||||
role="menu"
|
||||
aria-label={t('common.bulkOperate')}
|
||||
aria-describedby={ariaDescriptionId}
|
||||
>
|
||||
<CardContent className="ps-5 pe-1 py-1 flex items-center gap-6">
|
||||
<p
|
||||
id={ariaDescriptionId}
|
||||
className="text-sm text-text-secondary flex items-center gap-2"
|
||||
>
|
||||
{t('common.selected')}: {count} {unit ?? t('knowledgeDetails.files')}
|
||||
<BrushCleaning className="size-[1em]" />
|
||||
</p>
|
||||
|
||||
<Separator orientation={'vertical'} className="h-[1em]"></Separator>
|
||||
|
||||
<ul className="flex gap-2">
|
||||
{list.map((x) => (
|
||||
<li
|
||||
key={x.id}
|
||||
className={cn({ ['text-state-error']: isDeleteItem(x.id) })}
|
||||
>
|
||||
<ConfirmDeleteDialog
|
||||
hidden={!isDeleteItem(x.id)}
|
||||
onOk={x.onClick}
|
||||
title={
|
||||
unit
|
||||
? t('common.delete') + ' ' + unit
|
||||
: t('deleteModal.delFiles')
|
||||
}
|
||||
content={{
|
||||
title: t('common.deleteThem'),
|
||||
node: (
|
||||
<ConfirmDeleteDialogNode
|
||||
name={`${unit ? t('common.selected') + ' ' + count + ' ' + unit : t('deleteModal.delFilesContent', { count })}`}
|
||||
></ConfirmDeleteDialogNode>
|
||||
),
|
||||
}}
|
||||
{list.map((x) => {
|
||||
const isDeleteItem = x.id === 'delete';
|
||||
|
||||
const buttonEl = (
|
||||
<Button
|
||||
variant={isDeleteItem ? 'danger' : 'outline'}
|
||||
onClick={isDeleteItem ? () => {} : x.onClick}
|
||||
role="menuitem"
|
||||
>
|
||||
<Button
|
||||
variant={!isDeleteItem(x.id) ? 'ghost' : 'delete'}
|
||||
onClick={isDeleteItem(x.id) ? () => {} : x.onClick}
|
||||
className={cn({
|
||||
['text-state-error border border-state-error bg-state-error/5']:
|
||||
isDeleteItem(x.id),
|
||||
})}
|
||||
>
|
||||
{x.icon} {x.label}
|
||||
</Button>
|
||||
</ConfirmDeleteDialog>
|
||||
</li>
|
||||
))}
|
||||
{x.icon} {x.label}
|
||||
</Button>
|
||||
);
|
||||
|
||||
return (
|
||||
<li key={x.id}>
|
||||
{isDeleteItem ? (
|
||||
<ConfirmDeleteDialog
|
||||
key="deleteModal"
|
||||
onOk={x.onClick}
|
||||
title={
|
||||
unit
|
||||
? t('common.delete') + ' ' + unit
|
||||
: t('deleteModal.delFiles')
|
||||
}
|
||||
content={{
|
||||
title: t('common.deleteThem'),
|
||||
node: (
|
||||
<ConfirmDeleteDialogNode
|
||||
name={`${unit ? t('common.selected') + ' ' + count + ' ' + unit : t('deleteModal.delFilesContent', { count })}`}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{buttonEl}
|
||||
</ConfirmDeleteDialog>
|
||||
) : (
|
||||
buttonEl
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Plus, X } from 'lucide-react';
|
||||
import { LucidePlus, LucideTrash2 } from 'lucide-react';
|
||||
import { useFieldArray, useFormContext } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Separator } from '../ui/separator';
|
||||
@@ -51,7 +51,9 @@ export function DynamicPageRange() {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Separator className="w-3 "></Separator>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`parser_config.pages.${index}.to`}
|
||||
@@ -70,19 +72,27 @@ export function DynamicPageRange() {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button variant={'ghost'} onClick={() => remove(index)}>
|
||||
<X />
|
||||
|
||||
<Button
|
||||
className="ml-4"
|
||||
size="icon"
|
||||
variant="outline"
|
||||
onClick={() => remove(index)}
|
||||
>
|
||||
<LucideTrash2 />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<Button
|
||||
onClick={() => append({ from: 1, to: 100 })}
|
||||
className="mt-4 border-dashed w-full"
|
||||
variant={'outline'}
|
||||
block
|
||||
className="mt-4"
|
||||
variant="dashed"
|
||||
type="button"
|
||||
>
|
||||
<Plus />
|
||||
<LucidePlus />
|
||||
{t('knowledgeDetails.addPage')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -42,7 +42,6 @@ import { DataFlowSelect } from '../data-pipeline-select';
|
||||
import { DelimiterFormField } from '../delimiter-form-field';
|
||||
import { EntityTypesFormField } from '../entity-types-form-field';
|
||||
import { ExcelToHtmlFormField } from '../excel-to-html-form-field';
|
||||
import { FormContainer } from '../form-container';
|
||||
import { LayoutRecognizeFormField } from '../layout-recognize-form-field';
|
||||
import { MaxTokenNumberFormField } from '../max-token-number-from-field';
|
||||
import { MinerUOptionsFormField } from '../mineru-options-form-field';
|
||||
@@ -293,22 +292,16 @@ export function ChunkMethodDialog({
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('knowledgeDetails.chunkMethod')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-6 max-h-[70vh] overflow-auto"
|
||||
className="space-y-6 max-h-[70vh] overflow-auto -mx-6 px-10 py-5"
|
||||
id={FormId}
|
||||
>
|
||||
<FormContainer>
|
||||
<div className="space-y-6">
|
||||
<ParseTypeItem />
|
||||
{parseType === 1 && <ChunkMethodItem></ChunkMethodItem>}
|
||||
{parseType === 2 && (
|
||||
<DataFlowSelect
|
||||
isMult={false}
|
||||
// toDataPipeline={navigateToAgents}
|
||||
formFieldName="pipeline_id"
|
||||
/>
|
||||
)}
|
||||
{parseType === 1 && <ChunkMethodItem />}
|
||||
|
||||
{/* <FormField
|
||||
control={form.control}
|
||||
@@ -326,9 +319,9 @@ export function ChunkMethodDialog({
|
||||
</FormItem>
|
||||
)}
|
||||
/> */}
|
||||
{showPages && parseType === 1 && (
|
||||
<DynamicPageRange></DynamicPageRange>
|
||||
)}
|
||||
|
||||
{showPages && parseType === 1 && <DynamicPageRange />}
|
||||
|
||||
{showPages && parseType === 1 && layoutRecognize && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
@@ -341,31 +334,25 @@ export function ChunkMethodDialog({
|
||||
{t('knowledgeDetails.taskPageSize')}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
type={'number'}
|
||||
min={1}
|
||||
max={128}
|
||||
></Input>
|
||||
<Input {...field} type={'number'} min={1} max={128} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</FormContainer>
|
||||
</div>
|
||||
|
||||
{parseType === 1 && (
|
||||
<>
|
||||
<FormContainer
|
||||
show={showOne || showMaxTokenNumber}
|
||||
className="space-y-3"
|
||||
>
|
||||
<div className="space-y-6 border-t-0.5 border-border-button pt-6 empty:hidden">
|
||||
{showOne && (
|
||||
<>
|
||||
<LayoutRecognizeFormField showMineruOptions={false} />
|
||||
{isMineruSelected && <MinerUOptionsFormField />}
|
||||
</>
|
||||
)}
|
||||
|
||||
{showMaxTokenNumber && (
|
||||
<>
|
||||
<MaxTokenNumberFormField
|
||||
@@ -374,26 +361,21 @@ export function ChunkMethodDialog({
|
||||
? 8192 * 2
|
||||
: 2048
|
||||
}
|
||||
></MaxTokenNumberFormField>
|
||||
<DelimiterFormField></DelimiterFormField>
|
||||
/>
|
||||
<DelimiterFormField />
|
||||
<ChildrenDelimiterForm />
|
||||
</>
|
||||
)}
|
||||
</FormContainer>
|
||||
<FormContainer
|
||||
show={
|
||||
isMineruSelected ||
|
||||
showAutoKeywords(selectedTag) ||
|
||||
showExcelToHtml
|
||||
}
|
||||
className="space-y-3"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6 border-t-0.5 border-border-button pt-6 empty:hidden">
|
||||
{selectedTag === DocumentParserType.Naive && (
|
||||
<>
|
||||
<EnableTocToggle />
|
||||
<ImageContextWindow />
|
||||
</>
|
||||
)}
|
||||
|
||||
{showAutoKeywords(selectedTag) && (
|
||||
<>
|
||||
<AutoMetadata
|
||||
@@ -404,28 +386,39 @@ export function ChunkMethodDialog({
|
||||
<AutoQuestionsFormField></AutoQuestionsFormField>
|
||||
</>
|
||||
)}
|
||||
|
||||
{showExcelToHtml && (
|
||||
<ExcelToHtmlFormField></ExcelToHtmlFormField>
|
||||
)}
|
||||
</FormContainer>
|
||||
</div>
|
||||
{/* {showRaptorParseConfiguration(
|
||||
selectedTag as DocumentParserType,
|
||||
) && (
|
||||
<FormContainer>
|
||||
<RaptorFormFields></RaptorFormFields>
|
||||
</FormContainer>
|
||||
)} */}
|
||||
{/* {showGraphRagItems(selectedTag as DocumentParserType) &&
|
||||
useGraphRag && (
|
||||
selectedTag as DocumentParserType,
|
||||
) && (
|
||||
<FormContainer>
|
||||
<UseGraphRagFormField></UseGraphRagFormField>
|
||||
<RaptorFormFields></RaptorFormFields>
|
||||
</FormContainer>
|
||||
)} */}
|
||||
{showEntityTypes && (
|
||||
<EntityTypesFormField></EntityTypesFormField>
|
||||
)}
|
||||
{/* {showGraphRagItems(selectedTag as DocumentParserType) &&
|
||||
useGraphRag && (
|
||||
<FormContainer>
|
||||
<UseGraphRagFormField></UseGraphRagFormField>
|
||||
</FormContainer>
|
||||
)} */}
|
||||
<div className="space-y-6 border-t-0.5 border-border-button pt-6 empty:hidden">
|
||||
{showEntityTypes && <EntityTypesFormField />}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="space-y-6 empty:hidden">
|
||||
{parseType === 2 && (
|
||||
<DataFlowSelect
|
||||
isMult={false}
|
||||
// toDataPipeline={navigateToAgents}
|
||||
formFieldName="pipeline_id"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
<DialogFooter>
|
||||
|
||||
@@ -53,22 +53,24 @@ const Empty = (props: EmptyProps) => {
|
||||
export default Empty;
|
||||
|
||||
export const EmptyCard = (props: EmptyCardProps) => {
|
||||
const { icon, className, children, title, description, style } = props;
|
||||
const { icon, className, children, title, description, style, ...restProps } =
|
||||
props;
|
||||
return (
|
||||
<div
|
||||
<article
|
||||
className={cn(
|
||||
'flex flex-col gap-3 items-start justify-start border border-dashed border-border-button rounded-md p-5 w-fit',
|
||||
className,
|
||||
)}
|
||||
style={style}
|
||||
{...restProps}
|
||||
>
|
||||
{icon}
|
||||
{title && <div className="text-text-primary text-base">{title}</div>}
|
||||
{title && <div className="text-text-primary text-sm">{title}</div>}
|
||||
{description && (
|
||||
<div className="text-text-secondary text-sm">{description}</div>
|
||||
<p className="text-text-secondary text-sm">{description}</p>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -104,11 +106,14 @@ export const EmptyAppCard = (props: {
|
||||
break;
|
||||
}
|
||||
return (
|
||||
<div onClick={isSearch ? undefined : props.onClick} data-testid={testId}>
|
||||
<div>
|
||||
<EmptyCard
|
||||
onClick={isSearch ? undefined : props.onClick}
|
||||
data-testid={testId}
|
||||
tabIndex={isSearch ? undefined : 0}
|
||||
icon={showIcon ? cardData.icon : undefined}
|
||||
title={isSearch ? notFound : title}
|
||||
className={cn('cursor-pointer ', className)}
|
||||
className={cn('cursor-pointer', className)}
|
||||
style={style}
|
||||
// description={EmptyCardData[type].description}
|
||||
>
|
||||
|
||||
@@ -15,4 +15,4 @@ export type EmptyCardProps = {
|
||||
title?: string;
|
||||
description?: string;
|
||||
style?: React.CSSProperties;
|
||||
};
|
||||
} & Omit<React.HTMLAttributes<HTMLDivElement>, 'title'>;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { RAGFlowAvatar } from '@/components/ragflow-avatar';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { formatDate } from '@/utils/date';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
@@ -26,48 +26,61 @@ export function HomeCard({
|
||||
}: IProps) {
|
||||
return (
|
||||
<Card
|
||||
as="article"
|
||||
data-testid={testId}
|
||||
data-agent-name={data.name}
|
||||
onClick={() => {
|
||||
// navigateToSearch(data?.id);
|
||||
onClick?.();
|
||||
}}
|
||||
tabIndex={0}
|
||||
className="px-2.5 py-4 flex gap-2 items-start group h-full w-full hover:shadow-md"
|
||||
>
|
||||
<CardContent className="p-4 flex gap-2 items-start group h-full w-full hover:shadow-md">
|
||||
<div className="flex justify-between mb-4">
|
||||
<RAGFlowAvatar
|
||||
className="w-[32px] h-[32px]"
|
||||
avatar={data.avatar}
|
||||
name={data.name}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col justify-between gap-1 flex-1 h-full w-[calc(100%-50px)]">
|
||||
<section className="flex justify-between">
|
||||
<section className="flex flex-1 min-w-0 gap-1 items-center">
|
||||
<div
|
||||
className="text-base font-bold leading-snug truncate"
|
||||
data-testid="agent-name"
|
||||
>
|
||||
{data.name}
|
||||
</div>
|
||||
{icon}
|
||||
</section>
|
||||
{moreDropdown}
|
||||
</section>
|
||||
<div>
|
||||
<RAGFlowAvatar
|
||||
className="w-[32px] h-[32px]"
|
||||
avatar={data.avatar}
|
||||
name={data.name}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<section className="flex flex-col gap-1 mt-1">
|
||||
<div className="whitespace-nowrap overflow-hidden text-ellipsis">
|
||||
{data.description}
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<p className="text-sm opacity-80 whitespace-nowrap">
|
||||
{formatDate(data.update_time)}
|
||||
</p>
|
||||
{sharedBadge}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</CardContent>
|
||||
<div className="flex-1 w-0">
|
||||
<CardHeader
|
||||
as="header"
|
||||
className="p-0 flex-1 flex flex-row items-center gap-2 space-y-0"
|
||||
>
|
||||
<CardTitle className="flex-1 inline-flex w-0 me-auto">
|
||||
<h3
|
||||
className="flex-1 truncate text-base font-bold leading-snug"
|
||||
data-testid="agent-name"
|
||||
>
|
||||
{data.name}
|
||||
</h3>
|
||||
|
||||
{icon}
|
||||
</CardTitle>
|
||||
|
||||
<div>{moreDropdown}</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="p-0">
|
||||
<div className="flex flex-col justify-between gap-1 flex-1 h-full w-[calc(100%-50px)]">
|
||||
<section className="flex justify-between"></section>
|
||||
|
||||
<section className="flex flex-col gap-1 mt-1">
|
||||
<div className="whitespace-nowrap overflow-hidden text-ellipsis">
|
||||
{data.description}
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<p className="text-sm opacity-80 whitespace-nowrap">
|
||||
{formatDate(data.update_time)}
|
||||
</p>
|
||||
{sharedBadge}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</CardContent>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ export default function ListFilterBar({
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<div className={cn('flex justify-between mb-5 items-center', className)}>
|
||||
<div className={cn('flex justify-between items-center', className)}>
|
||||
<div className="text-2xl font-semibold flex items-center gap-2.5">
|
||||
{typeof icon === 'string' ? (
|
||||
// <IconFont name={icon} className="size-6"></IconFont>
|
||||
@@ -101,7 +101,8 @@ export default function ListFilterBar({
|
||||
)}
|
||||
{leftPanel || title}
|
||||
</div>
|
||||
<div className="flex gap-5 items-center">
|
||||
|
||||
<div className="flex gap-4 items-center" role="toolbar">
|
||||
{preChildren}
|
||||
{showFilter && (
|
||||
<FilterPopover
|
||||
@@ -119,6 +120,7 @@ export default function ListFilterBar({
|
||||
value={searchString}
|
||||
onChange={onSearchChange}
|
||||
className="w-32"
|
||||
role="searchbox"
|
||||
></SearchInput>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -9,10 +9,10 @@ export const MoreButton = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
<Button
|
||||
ref={ref}
|
||||
variant="ghost"
|
||||
size={size || 'icon'}
|
||||
size={size || 'icon-xs'}
|
||||
className={cn(
|
||||
'invisible size-3.5 bg-transparent group-hover:bg-transparent',
|
||||
'group-focus-within:visible group-hover:visible aria-expanded:visible',
|
||||
'opacity-0 size-3.5 transition-all bg-transparent group-hover:bg-transparent',
|
||||
'group-focus-within:opacity-100 group-hover:opacity-100 aria-expanded:opacity-100',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
import * as AvatarPrimitive from '@radix-ui/react-avatar';
|
||||
import { forwardRef, memo, useEffect, useRef, useState } from 'react';
|
||||
import { forwardRef, memo, useMemo } from 'react';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar';
|
||||
|
||||
const PREDEFINED_COLORS = [
|
||||
@@ -26,6 +26,15 @@ const getStringHash = (str: string): number => {
|
||||
return Math.abs(hash);
|
||||
};
|
||||
|
||||
const getInitials = (name?: string) => {
|
||||
if (typeof name !== 'string' || !name) return '';
|
||||
const parts = name?.trim().split(/\s+/);
|
||||
if (parts.length === 1) {
|
||||
return parts[0][0].toUpperCase();
|
||||
}
|
||||
return parts[0][0].toUpperCase();
|
||||
};
|
||||
|
||||
const getColorForName = (name: string): { from: string; to: string } => {
|
||||
const hash = getStringHash(name);
|
||||
const index = hash % PREDEFINED_COLORS.length;
|
||||
@@ -42,50 +51,15 @@ export const RAGFlowAvatar = memo(
|
||||
}
|
||||
>(({ name, avatar, isPerson = false, className, ...props }, ref) => {
|
||||
// Generate initial letter logic
|
||||
const getInitials = (name?: string) => {
|
||||
if (typeof name !== 'string' || !name) return '';
|
||||
const parts = name?.trim().split(/\s+/);
|
||||
if (parts.length === 1) {
|
||||
return parts[0][0].toUpperCase();
|
||||
}
|
||||
return parts[0][0].toUpperCase();
|
||||
};
|
||||
|
||||
const initials = getInitials(name);
|
||||
const { from, to } = name
|
||||
? getColorForName(name)
|
||||
: { from: 'hsl(0, 0%, 30%)', to: 'hsl(0, 0%, 80%)' };
|
||||
|
||||
const fallbackRef = useRef<HTMLElement>(null);
|
||||
const [fontSize, setFontSize] = useState('0.875rem');
|
||||
|
||||
// Calculate font size
|
||||
const calculateFontSize = () => {
|
||||
if (fallbackRef.current) {
|
||||
const containerWidth = fallbackRef.current.offsetWidth;
|
||||
const newSize = containerWidth * 0.6;
|
||||
setFontSize(`${newSize}px`);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
calculateFontSize();
|
||||
|
||||
if (fallbackRef.current) {
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
calculateFontSize();
|
||||
});
|
||||
|
||||
resizeObserver.observe(fallbackRef.current);
|
||||
|
||||
return () => {
|
||||
if (fallbackRef.current) {
|
||||
resizeObserver.unobserve(fallbackRef.current);
|
||||
}
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
const { initials, from, to } = useMemo(
|
||||
() => ({
|
||||
initials: getInitials(name),
|
||||
from: 'hsl(0, 0%, 30%)',
|
||||
to: 'hsl(0, 0%, 80%)',
|
||||
...(name ? getColorForName(name) : {}),
|
||||
}),
|
||||
[name],
|
||||
);
|
||||
|
||||
return (
|
||||
<Avatar
|
||||
@@ -95,22 +69,27 @@ export const RAGFlowAvatar = memo(
|
||||
>
|
||||
<AvatarImage src={avatar} />
|
||||
<AvatarFallback
|
||||
ref={(node) => {
|
||||
fallbackRef.current = node;
|
||||
calculateFontSize();
|
||||
}}
|
||||
className={cn(
|
||||
'bg-gradient-to-b',
|
||||
'flex items-center justify-center',
|
||||
'text-white ',
|
||||
{ 'rounded-md': !isPerson },
|
||||
)}
|
||||
className="flex items-center justify-center bg-gradient-to-b text-white"
|
||||
style={{
|
||||
backgroundImage: `linear-gradient(to bottom, ${from}, ${to})`,
|
||||
fontSize: fontSize,
|
||||
}}
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{initials}
|
||||
<svg
|
||||
className="size-full block text-current select-none"
|
||||
viewBox={`${-(50 + 22.5 * (initials.length - 1))} -50 ${100 + 45 * (initials.length - 1)} 100`}
|
||||
preserveAspectRatio="xMinYMid meet"
|
||||
>
|
||||
<text
|
||||
fontSize={55}
|
||||
fill="currentColor"
|
||||
textAnchor="middle"
|
||||
dominantBaseline="central"
|
||||
>
|
||||
{initials}
|
||||
</text>
|
||||
</svg>
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
);
|
||||
|
||||
@@ -143,8 +143,8 @@ export const HomeIcon = ({
|
||||
imgClass,
|
||||
}: {
|
||||
name: string;
|
||||
height?: string;
|
||||
width?: string;
|
||||
height?: string | number;
|
||||
width?: string | number;
|
||||
imgClass?: string;
|
||||
}) => {
|
||||
const isDark = useIsDarkTheme();
|
||||
|
||||
56
web/src/components/theme-switch.tsx
Normal file
56
web/src/components/theme-switch.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import { useIsDarkTheme, useTheme } from '@/components/theme-provider';
|
||||
import { ThemeEnum } from '@/constants/common';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Root, Thumb } from '@radix-ui/react-switch';
|
||||
import { LucideMoon, LucideSun } from 'lucide-react';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
const ThemeSwitch = forwardRef<
|
||||
React.ElementRef<typeof Root>,
|
||||
React.ComponentPropsWithoutRef<typeof Root>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { setTheme } = useTheme();
|
||||
const isDark = useIsDarkTheme();
|
||||
|
||||
return (
|
||||
<Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'group/theme-switch relative rounded-full outline-none self-center focus-visible:ring-1 focus-visible:ring-accent-primary',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
checked={isDark}
|
||||
onCheckedChange={(value) =>
|
||||
setTheme(value ? ThemeEnum.Dark : ThemeEnum.Light)
|
||||
}
|
||||
>
|
||||
<div className="self-center p-3 py-2 rounded-full bg-bg-card transition-[background-color]">
|
||||
<div className="h-full flex items-center justify-between gap-4 relative z-[1] text-text-disabled transition-[text-color]">
|
||||
<LucideSun
|
||||
className={cn('size-[1em]', !isDark && 'text-text-primary')}
|
||||
/>
|
||||
<LucideMoon
|
||||
className={cn('size-[1em]', isDark && 'text-text-primary')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Thumb
|
||||
className={cn(
|
||||
'absolute top-0 left-0 w-[calc(50%+.25rem)] p-0.5 h-full rounded-full overflow-hidden',
|
||||
'transition-all ease-out',
|
||||
'group-hover/theme-switch:w-[calc(50%+.66rem)] group-focus-visible/theme-switch:w-[calc(50%+.66rem)]',
|
||||
{
|
||||
'left-[calc(50%-.25rem)] group-hover/theme-switch:left-[calc(50%-.66rem)] group-focus-visible/theme-switch:left-[calc(50%-.66rem)]':
|
||||
isDark,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<div className="size-full rounded-full bg-bg-base shadow-md transition-colors ease-out" />
|
||||
</Thumb>
|
||||
</Root>
|
||||
);
|
||||
});
|
||||
|
||||
export default ThemeSwitch;
|
||||
@@ -1,48 +0,0 @@
|
||||
import { ThemeEnum } from '@/constants/common';
|
||||
import { Moon, Sun } from 'lucide-react';
|
||||
import { FC, useCallback } from 'react';
|
||||
import { useIsDarkTheme, useTheme } from './theme-provider';
|
||||
import { Button } from './ui/button';
|
||||
|
||||
const ThemeToggle: FC = () => {
|
||||
const { setTheme } = useTheme();
|
||||
const isDarkTheme = useIsDarkTheme();
|
||||
const handleThemeChange = useCallback(
|
||||
(checked: boolean) => {
|
||||
setTheme(checked ? ThemeEnum.Dark : ThemeEnum.Light);
|
||||
},
|
||||
[setTheme],
|
||||
);
|
||||
return (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => handleThemeChange(!isDarkTheme)}
|
||||
className="relative inline-flex h-6 w-14 items-center rounded-full transition-colors p-0.5 border-none focus:border-none bg-bg-card hover:bg-bg-card"
|
||||
// aria-label={isDarkTheme ? 'Switch to light mode' : 'Switch to dark mode'}
|
||||
>
|
||||
<div className="inline-flex h-full w-full items-center">
|
||||
<div
|
||||
className={`inline-flex transform items-center justify-center rounded-full transition-transform ${
|
||||
isDarkTheme
|
||||
? ' text-text-disabled h-4 w-5'
|
||||
: ' text-text-primary bg-bg-base h-full w-8 flex-1'
|
||||
}`}
|
||||
>
|
||||
<Sun />
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`inline-flex transform items-center justify-center rounded-full transition-transform ${
|
||||
isDarkTheme
|
||||
? ' text-text-primary bg-bg-base h-full w-8 flex-1'
|
||||
: 'text-text-disabled h-4 w-5'
|
||||
}`}
|
||||
>
|
||||
<Moon />
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThemeToggle;
|
||||
@@ -39,7 +39,7 @@ const AvatarFallback = React.forwardRef<
|
||||
<AvatarPrimitive.Fallback
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex h-full w-full items-center justify-center rounded-full bg-bg-member',
|
||||
'flex h-full w-full items-center justify-center bg-bg-member',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Link, LinkProps } from 'react-router';
|
||||
|
||||
const buttonVariants = cva(
|
||||
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 transition-colors outline-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',
|
||||
),
|
||||
@@ -83,9 +83,13 @@ const buttonVariants = cva(
|
||||
`,
|
||||
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
|
||||
// Static
|
||||
// Button has no interaction transitions
|
||||
static: '',
|
||||
},
|
||||
size: {
|
||||
auto: 'h-full px-1',
|
||||
auto: '',
|
||||
|
||||
xl: 'h-12 rounded-xl px-5',
|
||||
lg: 'h-10 rounded-lg px-4',
|
||||
@@ -107,6 +111,8 @@ const buttonVariants = cva(
|
||||
},
|
||||
);
|
||||
|
||||
export type ButtonVariants = VariantProps<typeof buttonVariants>;
|
||||
|
||||
export type ButtonProps<IsAnchor extends boolean = false> = {
|
||||
asChild?: boolean;
|
||||
asLink?: boolean;
|
||||
@@ -114,7 +120,7 @@ export type ButtonProps<IsAnchor extends boolean = false> = {
|
||||
block?: boolean;
|
||||
disabled?: boolean;
|
||||
dot?: boolean;
|
||||
} & VariantProps<typeof buttonVariants> &
|
||||
} & ButtonVariants &
|
||||
(IsAnchor extends true
|
||||
? LinkProps
|
||||
: React.ButtonHTMLAttributes<HTMLButtonElement>);
|
||||
@@ -144,7 +150,7 @@ const Button = React.forwardRef(
|
||||
<Comp
|
||||
className={cn(
|
||||
buttonVariants({ variant, size, className }),
|
||||
{ 'block w-full': block },
|
||||
{ 'w-full': block },
|
||||
{ relative: dot },
|
||||
)}
|
||||
// @ts-ignore
|
||||
|
||||
@@ -3,10 +3,10 @@ import * as React from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
HTMLElement,
|
||||
React.HTMLAttributes<HTMLElement> & { as?: React.ElementType }
|
||||
>(({ as: As = 'div', className, ...props }, ref) => (
|
||||
<As
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'rounded-lg border-border-button border-0.5 shadow-sm bg-bg-input transition-shadow',
|
||||
@@ -19,9 +19,9 @@ Card.displayName = 'Card';
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
React.HTMLAttributes<HTMLDivElement> & { as?: React.ElementType }
|
||||
>(({ as: As = 'div', className, ...props }, ref) => (
|
||||
<As
|
||||
ref={ref}
|
||||
className={cn('flex flex-col space-y-1.5 p-6', className)}
|
||||
{...props}
|
||||
@@ -32,7 +32,7 @@ CardHeader.displayName = 'CardHeader';
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLElement,
|
||||
React.HTMLAttributes<HTMLElement> & { as?: React.ElementType }
|
||||
>(({ className, as: As = 'div', ...props }, ref) => (
|
||||
>(({ as: As = 'div', className, ...props }, ref) => (
|
||||
<As
|
||||
ref={ref}
|
||||
className={cn('text-2xl leading-normal font-medium', className)}
|
||||
@@ -43,9 +43,9 @@ CardTitle.displayName = 'CardTitle';
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
React.HTMLAttributes<HTMLDivElement> & { as?: React.ElementType }
|
||||
>(({ as: As = 'div', className, ...props }, ref) => (
|
||||
<As
|
||||
ref={ref}
|
||||
className={cn('text-sm text-text-secondary', className)}
|
||||
{...props}
|
||||
@@ -55,9 +55,9 @@ CardDescription.displayName = 'CardDescription';
|
||||
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
React.HTMLAttributes<HTMLDivElement> & { as?: React.ElementType }
|
||||
>(({ as: As = 'div', className, ...props }, ref) => (
|
||||
<As
|
||||
ref={ref}
|
||||
className={cn('p-6 pt-0 transition-shadow', className)}
|
||||
{...props}
|
||||
@@ -67,9 +67,9 @@ CardContent.displayName = 'CardContent';
|
||||
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
React.HTMLAttributes<HTMLDivElement> & { as?: React.ElementType }
|
||||
>(({ as: As = 'div', className, ...props }, ref) => (
|
||||
<As
|
||||
ref={ref}
|
||||
className={cn('flex items-center p-6 pt-0', className)}
|
||||
{...props}
|
||||
|
||||
@@ -38,7 +38,10 @@ const DialogContent = React.forwardRef<
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'outline-0 fixed left-[50%] top-[50%] rounded-lg z-50 grid w-full max-w-xl translate-x-[-50%] translate-y-[-50%] gap-4',
|
||||
'outline-none outline-0 fixed left-[50%] top-[50%] rounded-lg z-50 grid w-full max-w-xl translate-x-[-50%] translate-y-[-50%]',
|
||||
// TODO: to keep scrollbar perfectly aligned to header bottom and/or footer top,
|
||||
// 'gap-4' should be removed, then bring your own body container with padding-y instead.
|
||||
'gap-4',
|
||||
'border-0.5 border-border-button bg-bg-base p-6 shadow-lg duration-200 sm:rounded-lg',
|
||||
'data-[state=open]:animate-in data-[state=closed]:animate-out',
|
||||
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
||||
@@ -88,7 +91,7 @@ const DialogFooter = ({
|
||||
<div
|
||||
className={cn(
|
||||
// '-mx-6 -mb-6 px-12 pt-4 pb-8',
|
||||
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-4',
|
||||
'-mx-6 -mb-6 p-6 flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-4',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -18,7 +18,13 @@ const HoverCardContent = React.forwardRef<
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'z-50 w-fit max-w-96 overflow-auto break-words whitespace-pre-wrap rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-hover-card-content-transform-origin]',
|
||||
'z-50 w-fit max-w-96 overflow-auto break-words whitespace-pre-wrap',
|
||||
'rounded-md border bg-bg-base p-4 text-text-primary shadow-md outline-none',
|
||||
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0',
|
||||
'data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
|
||||
'data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2',
|
||||
'data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
'origin-[--radix-hover-card-content-transform-origin]',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { cn } from '@/lib/utils';
|
||||
import React, { useContext, useState } from 'react';
|
||||
|
||||
const RadioGroupContext = React.createContext<{
|
||||
name?: string;
|
||||
value: string | number;
|
||||
onChange: (value: string | number) => void;
|
||||
disabled?: boolean;
|
||||
@@ -50,30 +51,46 @@ function Radio({
|
||||
return (
|
||||
<label
|
||||
className={cn(
|
||||
'flex items-center cursor-pointer gap-2 text-sm',
|
||||
'group/radio relative flex items-center cursor-pointer gap-2 text-sm',
|
||||
mergedDisabled && 'cursor-not-allowed opacity-50',
|
||||
)}
|
||||
>
|
||||
<span
|
||||
<input
|
||||
type="radio"
|
||||
name={groupContext?.name}
|
||||
value={value}
|
||||
checked={isChecked}
|
||||
onClick={handleClick}
|
||||
disabled={mergedDisabled}
|
||||
data-testid={testId}
|
||||
className="peer absolute size-[1px] opacity-0"
|
||||
/>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-4 w-4 items-center justify-center rounded-full border border-border transition-colors',
|
||||
'peer outline-none focus-visible:border-border-button',
|
||||
'flex h-4 w-4 items-center justify-center rounded-full border border-border-button transition-colors',
|
||||
'group-hover/radio:border-border-default hover:border-border-default',
|
||||
'peer-focus:border-primary',
|
||||
isChecked && 'border-primary bg-primary/10',
|
||||
mergedDisabled && 'border-muted',
|
||||
)}
|
||||
onClick={handleClick}
|
||||
data-testid={testId}
|
||||
>
|
||||
{isChecked && (
|
||||
<div className="h-3 w-3 fill-primary text-primary bg-text-primary rounded-full" />
|
||||
)}
|
||||
</span>
|
||||
<div
|
||||
className={cn(
|
||||
'h-2 w-2 fill-primary text-primary bg-text-primary rounded-full opacity-0 scale-0 transition-all',
|
||||
isChecked && 'opacity-100 scale-100',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{children && <span className="text-foreground">{children}</span>}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
type RadioGroupProps = {
|
||||
name?: string;
|
||||
value?: string | number;
|
||||
defaultValue?: string | number;
|
||||
onChange?: (value: string | number) => void;
|
||||
@@ -86,6 +103,7 @@ type RadioGroupProps = {
|
||||
const Group = React.forwardRef<HTMLDivElement, RadioGroupProps>(
|
||||
(
|
||||
{
|
||||
name,
|
||||
value,
|
||||
defaultValue,
|
||||
onChange,
|
||||
@@ -116,6 +134,7 @@ const Group = React.forwardRef<HTMLDivElement, RadioGroupProps>(
|
||||
return (
|
||||
<RadioGroupContext.Provider
|
||||
value={{
|
||||
name,
|
||||
value: mergedValue,
|
||||
onChange: handleChange,
|
||||
disabled,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
import * as React from 'react';
|
||||
import { Button, ButtonVariants } from './button';
|
||||
export declare type SegmentedValue = string | number;
|
||||
export declare type SegmentedRawOption = SegmentedValue;
|
||||
export interface SegmentedLabeledOption {
|
||||
@@ -56,7 +57,7 @@ export interface SegmentedProps extends Omit<
|
||||
itemClassName?: string;
|
||||
rounded?: keyof typeof segmentedVariants.round;
|
||||
sizeType?: keyof typeof segmentedVariants.size;
|
||||
buttonSize?: keyof typeof segmentedVariants.buttonSize;
|
||||
buttonSize?: ButtonVariants['size'];
|
||||
}
|
||||
|
||||
export const Segmented = React.forwardRef<HTMLDivElement, SegmentedProps>(
|
||||
@@ -101,12 +102,12 @@ export const Segmented = React.forwardRef<HTMLDivElement, SegmentedProps>(
|
||||
const actualValue = isObject ? option.value : option;
|
||||
|
||||
return (
|
||||
<div
|
||||
<Button
|
||||
key={actualValue}
|
||||
type="button"
|
||||
size={buttonSize}
|
||||
variant="static"
|
||||
className={cn(
|
||||
'inline-flex items-center text-base font-normal cursor-pointer',
|
||||
segmentedVariants.round[rounded],
|
||||
segmentedVariants.buttonSize[buttonSize],
|
||||
{
|
||||
'text-text-primary bg-bg-base': selectedValue === actualValue,
|
||||
},
|
||||
@@ -118,7 +119,7 @@ export const Segmented = React.forwardRef<HTMLDivElement, SegmentedProps>(
|
||||
onClick={() => handleOnChange(actualValue)}
|
||||
>
|
||||
{isObject ? option.label : option}
|
||||
</div>
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user