Fix: Home and team page style adjustment, and some bug fixes #10703 (#10805)

### What problem does this PR solve?

Fix: Home and team page style adjustment, and some bug fixes #10703

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
This commit is contained in:
chanx
2025-10-27 15:15:12 +08:00
committed by GitHub
parent 33a189f620
commit 5312b75362
26 changed files with 628 additions and 276 deletions

View File

@ -0,0 +1,46 @@
.single :nth-child(n + 2):not(:last-child) {
display: none;
}
.single :nth-child(-n + 1):not(:last-child) {
display: flex;
}
@media (min-width: 640px) {
.single :nth-child(n + 2):not(:last-child) {
display: none;
}
.single :nth-child(-n + 1):not(:last-child) {
display: flex;
}
}
@media (min-width: 768px) {
.single :nth-child(n + 3):not(:last-child) {
display: none;
}
.single :nth-child(-n + 2):not(:last-child) {
display: flex;
}
}
@media (min-width: 1024px) {
.single :nth-child(n + 4):not(:last-child) {
display: none;
}
.single :nth-child(-n + 3):not(:last-child) {
display: flex;
}
}
@media (min-width: 1280px) {
.single :nth-child(n + 5):not(:last-child) {
display: none;
}
.single :nth-child(-n + 4):not(:last-child) {
display: flex;
}
}
@media (min-width: 1536px) {
.single :nth-child(n + 6):not(:last-child) {
display: none;
}
.single :nth-child(-n + 5):not(:last-child) {
display: flex;
}
}

View File

@ -0,0 +1,41 @@
import { cn } from '@/lib/utils';
import { isValidElement, PropsWithChildren, ReactNode } from 'react';
import './index.less';
type CardContainerProps = { className?: string } & PropsWithChildren;
export function CardSineLineContainer({
children,
className,
}: CardContainerProps) {
const flattenChildren = (children: ReactNode): ReactNode[] => {
const result: ReactNode[] = [];
const traverse = (child: ReactNode) => {
if (Array.isArray(child)) {
child.forEach(traverse);
} else if (isValidElement(child) && child.props.children) {
result.push(child);
} else {
result.push(child);
}
};
traverse(children);
return result;
};
const childArray = flattenChildren(children);
const childCount = childArray.length;
console.log(childArray, childCount);
return (
<section
className={cn(
'grid gap-6 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 single',
className,
)}
>
{children}
</section>
);
}

View File

@ -29,7 +29,7 @@ export function HomeCard({
onClick?.();
}}
>
<CardContent className="p-4 flex gap-2 items-start group h-full">
<CardContent className="p-4 flex gap-2 items-start group h-full w-full">
<div className="flex justify-between mb-4">
<RAGFlowAvatar
className="w-[32px] h-[32px]"

View File

@ -7,10 +7,10 @@ type SkeletonCardProps = {
export function SkeletonCard(props: SkeletonCardProps) {
const { className } = props;
return (
<div className={cn('space-y-2', className)}>
<Skeleton className="h-4 w-full bg-bg-card" />
<Skeleton className="h-4 w-full bg-bg-card" />
<Skeleton className="h-4 w-2/3 bg-bg-card" />
<div className={cn('space-y-4', className)}>
<Skeleton className="h-8 w-full bg-bg-card rounded-lg" />
<Skeleton className="h-8 w-4/5 bg-bg-card rounded-lg" />
<Skeleton className="h-8 w-3/5 bg-bg-card rounded-lg" />
</div>
);
}

View File

@ -13,6 +13,33 @@ export interface SegmentedLabeledOption {
title?: string;
}
declare type SegmentedOptions = (SegmentedRawOption | SegmentedLabeledOption)[];
const segmentedVariants = {
round: {
default: 'rounded-md',
none: 'rounded-none',
sm: 'rounded-sm',
md: 'rounded-md',
lg: 'rounded-lg',
xl: 'rounded-xl',
xxl: 'rounded-2xl',
xxxl: 'rounded-3xl',
full: 'rounded-full',
},
size: {
default: 'px-1 py-1',
sm: 'px-1 py-1',
md: 'px-2 py-1.5',
lg: 'px-4 px-2',
xl: 'px-5 py-2.5',
xxl: 'px-6 py-3',
},
buttonSize: {
default: 'px-2 py-1',
md: 'px-2 py-1',
lg: 'px-4 px-1.5',
xl: 'px-6 py-2',
},
};
export interface SegmentedProps
extends Omit<React.HTMLProps<HTMLDivElement>, 'onChange'> {
options: SegmentedOptions;
@ -24,6 +51,9 @@ export interface SegmentedProps
direction?: 'ltr' | 'rtl';
motionName?: string;
activeClassName?: string;
rounded?: keyof typeof segmentedVariants.round;
sizeType?: keyof typeof segmentedVariants.size;
buttonSize?: keyof typeof segmentedVariants.buttonSize;
}
export function Segmented({
@ -32,6 +62,9 @@ export function Segmented({
onChange,
className,
activeClassName,
rounded = 'default',
sizeType = 'default',
buttonSize = 'default',
}: SegmentedProps) {
const [selectedValue, setSelectedValue] = React.useState<
SegmentedValue | undefined
@ -45,7 +78,9 @@ export function Segmented({
return (
<div
className={cn(
'flex items-center rounded-3xl p-1 gap-2 bg-bg-card px-5 py-2.5',
'flex items-center p-1 gap-2 bg-bg-card',
segmentedVariants.round[rounded],
segmentedVariants.size[sizeType],
className,
)}
>
@ -57,10 +92,11 @@ export function Segmented({
<div
key={actualValue}
className={cn(
'inline-flex items-center px-6 py-2 text-base font-normal rounded-3xl cursor-pointer',
'inline-flex items-center text-base font-normal cursor-pointer',
segmentedVariants.round[rounded],
segmentedVariants.buttonSize[buttonSize],
{
'text-bg-base bg-metallic-gradient border-b-[#00BEB4] border-b-2':
selectedValue === actualValue,
'text-text-primary bg-bg-base': selectedValue === actualValue,
},
activeClassName && selectedValue === actualValue
? activeClassName

View File

@ -116,9 +116,13 @@ export function Header() {
/>
</div>
<Segmented
rounded="xxxl"
sizeType="xl"
buttonSize="xl"
options={options}
value={pathname}
onChange={handleChange}
activeClassName="text-bg-base bg-metallic-gradient border-b-[#00BEB4] border-b-2"
></Segmented>
<div className="flex items-center gap-5 text-text-badge">
<a

View File

@ -352,7 +352,7 @@ export default {
manual: `<p>Only <b>PDF</b> is supported.</p><p>
We assume that the manual has a hierarchical section structure, using the lowest section titles as basic unit for chunking documents. Therefore, figures and tables in the same section will not be separated, which may result in larger chunk sizes.
</p>`,
naive: `<p>Supported file formats are <b>MD, MDX, DOCX, XLSX, XLS (Excel 97-2003), PPT, PDF, TXT, JPEG, JPG, PNG, TIF, GIF, CSV, JSON, EML, HTML</b>.</p>
naive: `<p>Supported file formats are <b>MD, MDX, DOCX, XLSX, XLS (Excel 97-2003), PPTX, PDF, TXT, JPEG, JPG, PNG, TIF, GIF, CSV, JSON, EML, HTML</b>.</p>
<p>This method chunks files using a 'naive' method: </p>
<p>
<li>Use vision detection model to split the texts into smaller segments.</li>
@ -710,7 +710,7 @@ This auto-tagging feature enhances retrieval by adding another layer of domain-s
timezone: 'Time zone',
timezoneMessage: 'Please input your timezone!',
timezonePlaceholder: 'select your timezone',
email: 'Email address',
email: 'Email',
emailDescription: 'Once registered, E-mail cannot be changed.',
currentPassword: 'Current password',
currentPasswordMessage: 'Please input your password!',
@ -865,9 +865,9 @@ This auto-tagging feature enhances retrieval by adding another layer of domain-s
apiVersion: 'API-Version',
apiVersionMessage: 'Please input API version',
add: 'Add',
updateDate: 'Update Date',
role: 'Role',
invite: 'Invite',
updateDate: 'Date',
role: 'State',
invite: 'Invite Member',
agree: 'Accept',
refuse: 'Decline',
teamMembers: 'Team Members',
@ -1631,7 +1631,7 @@ This delimiter is used to split the input text into several text pieces echo of
email: 'Email',
'text&markdown': 'Text & Markup',
word: 'Word',
slides: 'PPT',
slides: 'PPTX',
audio: 'Audio',
video: 'Video',
},
@ -1803,13 +1803,13 @@ Important structured information may include: names, dates, locations, events, k
confirmRerun: 'Confirm Rerun Process',
confirmRerunModalContent: `
<p class="text-sm text-text-disabled font-medium mb-2">
You are about to rerun the process starting from the <strong class="text-text-primary">{{step}}</strong> step.
You are about to rerun the process starting from the <span class="text-text-secondary">{{step}}</span> step.
</p>
<p class="text-sm mb-3 text-text-secondary">This will:</p>
<p class="text-sm mb-3 text-text-disabled">This will:</p><br />
<ul class="list-disc list-inside space-y-1 text-sm text-text-secondary">
<li>Overwrite existing results from the current step onwards</li>
<li>Create a new log entry for tracking</li>
<li>Previous steps will remain unchanged</li>
<li>Overwrite existing results from the current step onwards</li>
<li>Create a new log entry for tracking</li>
<li>Previous steps will remain unchanged</li>
</ul>`,
changeStepModalTitle: 'Step Switch Warning',
changeStepModalContent: `

View File

@ -336,7 +336,7 @@ export default {
我们假设手册具有分层部分结构。 我们使用最低的部分标题作为对文档进行切片的枢轴。
因此,同一部分中的图和表不会被分割,并且块大小可能会很大。
</p>`,
naive: `<p>支持的文件格式为<b>MD、MDX、DOCX、XLSX、XLS (Excel 97-2003)、PPT、PDF、TXT、JPEG、JPG、PNG、TIF、GIF、CSV、JSON、EML、HTML</b>。</p>
naive: `<p>支持的文件格式为<b>MD、MDX、DOCX、XLSX、XLS (Excel 97-2003)、PPTX、PDF、TXT、JPEG、JPG、PNG、TIF、GIF、CSV、JSON、EML、HTML</b>。</p>
<p>此方法将简单的方法应用于块文件:</p>
<p>
<li>系统将使用视觉检测模型将连续文本分割成多个片段。</li>
@ -701,7 +701,7 @@ General实体和关系提取提示来自 GitHub - microsoft/graphrag基于
timezone: '时区',
timezoneMessage: '请选择时区',
timezonePlaceholder: '请选择时区',
email: '邮箱地址',
email: '邮箱',
emailDescription: '一旦注册,电子邮件将无法更改。',
currentPassword: '当前密码',
currentPasswordMessage: '请输入当前密码',
@ -821,9 +821,9 @@ General实体和关系提取提示来自 GitHub - microsoft/graphrag基于
apiVersion: 'API版本',
apiVersionMessage: '请输入API版本!',
add: '添加',
updateDate: '更新日期',
role: '角色',
invite: '邀请',
updateDate: '日期',
role: '状态',
invite: '邀请成员',
agree: '同意',
refuse: '拒绝',
teamMembers: '团队成员',
@ -1690,13 +1690,13 @@ Tokenizer 会根据所选方式将内容存储为对应的数据结构。`,
confirmRerun: '确认重新运行流程',
confirmRerunModalContent: `
<p class="text-sm text-text-disabled font-medium mb-2">
您即将从 <strong class="text-text-primary">{{step}}</strong> 步骤开始重新运行该过程
您即将从 <span class="text-text-secondary">{{step}}</span> 步骤开始重新运行该过程
</p>
<p class="text-sm mb-3 text-text-secondary">这将:</p>
<p class="text-sm mb-3 text-text-disabled">这将:</p>
<ul class="list-disc list-inside space-y-1 text-sm text-text-secondary">
<li>从当前步骤开始覆盖现有结果</li>
<li>创建新的日志条目进行跟踪</li>
<li>之前的步骤将保持不变</li>
<li>从当前步骤开始覆盖现有结果</li>
<li>创建新的日志条目进行跟踪</li>
<li>之前的步骤将保持不变</li>
</ul>`,
changeStepModalTitle: '切换步骤警告',
changeStepModalContent: `

View File

@ -43,7 +43,7 @@ export function SeeAllCard() {
const { navigateToDatasetList } = useNavigatePage();
return (
<Card className="w-40 flex-none h-full" onClick={navigateToDatasetList}>
<Card className="w-full flex-none h-full" onClick={navigateToDatasetList}>
<CardContent className="p-2.5 pt-1 w-full h-full flex items-center justify-center gap-1.5 text-text-secondary">
See All <ChevronRight className="size-4" />
</CardContent>

View File

@ -1,10 +1,10 @@
import { HomeCard } from '@/components/home-card';
import { MoreButton } from '@/components/more-button';
import { RenameDialog } from '@/components/rename-dialog';
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
import { useFetchAgentListByPage } from '@/hooks/use-agent-request';
import { AgentDropdown } from '../agents/agent-dropdown';
import { useRenameAgent } from '../agents/use-rename-agent';
import { ApplicationCard } from './application-card';
export function Agents() {
const { data } = useFetchAgentListByPage();
@ -21,9 +21,9 @@ export function Agents() {
return (
<>
{data.slice(0, 10).map((x) => (
<ApplicationCard
<HomeCard
key={x.id}
app={x}
data={{ name: x.title, ...x } as any}
onClick={navigateToAgent(x.id)}
moreDropdown={
<AgentDropdown
@ -33,7 +33,7 @@ export function Agents() {
<MoreButton></MoreButton>
</AgentDropdown>
}
></ApplicationCard>
></HomeCard>
))}
{agentRenameVisible && (
<RenameDialog

View File

@ -48,7 +48,7 @@ export type SeeAllAppCardProps = {
export function SeeAllAppCard({ click }: SeeAllAppCardProps) {
return (
<Card className="w-64 min-h-[76px]" onClick={click}>
<Card className="w-full min-h-[76px]" onClick={click}>
<CardContent className="p-2.5 pt-1 w-full h-full flex items-center justify-center gap-1.5 text-text-secondary">
See All <ChevronRight className="size-4" />
</CardContent>

View File

@ -1,3 +1,4 @@
import { CardSineLineContainer } from '@/components/card-singleline-container';
import { IconFont } from '@/components/icon-font';
import { Segmented, SegmentedValue } from '@/components/ui/segmented';
import { Routes } from '@/routes';
@ -34,7 +35,7 @@ export function Applications() {
);
const handleChange = (path: SegmentedValue) => {
setVal(path as string);
setVal(path as Routes);
};
return (
@ -51,16 +52,19 @@ export function Applications() {
options={options}
value={val}
onChange={handleChange}
className="bg-bg-card border border-border-button rounded-full"
activeClassName="bg-text-primary border-none"
buttonSize="xl"
// className="bg-bg-card border border-border-button rounded-lg"
// activeClassName="bg-text-primary border-none rounded-lg"
></Segmented>
</div>
<div className="flex flex-wrap gap-4">
{/* <div className="flex flex-wrap gap-4"> */}
<CardSineLineContainer>
{val === Routes.Agents && <Agents></Agents>}
{val === Routes.Chats && <ChatList></ChatList>}
{val === Routes.Searches && <SearchList></SearchList>}
{<SeeAllAppCard click={handleNavigate}></SeeAllAppCard>}
</div>
</CardSineLineContainer>
{/* </div> */}
</section>
);
}

View File

@ -1,3 +1,4 @@
import { HomeCard } from '@/components/home-card';
import { MoreButton } from '@/components/more-button';
import { RenameDialog } from '@/components/rename-dialog';
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
@ -5,7 +6,6 @@ import { useFetchDialogList } from '@/hooks/use-chat-request';
import { useTranslation } from 'react-i18next';
import { ChatDropdown } from '../next-chats/chat-dropdown';
import { useRenameChat } from '../next-chats/hooks/use-rename-chat';
import { ApplicationCard } from './application-card';
export function ChatList() {
const { t } = useTranslation();
@ -24,12 +24,11 @@ export function ChatList() {
return (
<>
{data.dialogs.slice(0, 10).map((x) => (
<ApplicationCard
<HomeCard
key={x.id}
app={{
data={{
avatar: x.icon,
title: x.name,
update_time: x.update_time,
...x,
}}
onClick={navigateToChat(x.id)}
moreDropdown={
@ -37,7 +36,7 @@ export function ChatList() {
<MoreButton></MoreButton>
</ChatDropdown>
}
></ApplicationCard>
></HomeCard>
))}
{chatRenameVisible && (
<RenameDialog

View File

@ -1,3 +1,4 @@
import { CardSineLineContainer } from '@/components/card-singleline-container';
import { IconFont } from '@/components/icon-font';
import { RenameDialog } from '@/components/rename-dialog';
import { CardSkeleton } from '@/components/ui/skeleton';
@ -30,7 +31,8 @@ export function Datasets() {
<CardSkeleton />
</div>
) : (
<div className="grid gap-6 grid-cols-1 md:grid-cols-2 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 3xl:grid-cols-7 max-h-[78vh] overflow-auto">
// <div className="grid gap-6 grid-cols-1 md:grid-cols-2 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 3xl:grid-cols-7 max-h-[78vh] overflow-auto">
<CardSineLineContainer>
{kbs
?.slice(0, 6)
.map((dataset) => (
@ -43,7 +45,8 @@ export function Datasets() {
<div className="min-h-24">
<SeeAllCard></SeeAllCard>
</div>
</div>
</CardSineLineContainer>
// </div>
)}
</div>
{datasetRenameVisible && (

View File

@ -1,10 +1,10 @@
import { HomeCard } from '@/components/home-card';
import { IconFont } from '@/components/icon-font';
import { MoreButton } from '@/components/more-button';
import { RenameDialog } from '@/components/rename-dialog';
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
import { useFetchSearchList, useRenameSearch } from '../next-searches/hooks';
import { SearchDropdown } from '../next-searches/search-dropdown';
import { ApplicationCard } from './application-card';
export function SearchList() {
const { data, refetch: refetchList } = useFetchSearchList();
@ -25,13 +25,9 @@ export function SearchList() {
return (
<>
{data?.data.search_apps.slice(0, 10).map((x) => (
<ApplicationCard
<HomeCard
key={x.id}
app={{
avatar: x.avatar,
title: x.name,
update_time: x.update_time,
}}
data={x}
onClick={navigateToSearch(x.id)}
moreDropdown={
<SearchDropdown
@ -41,7 +37,7 @@ export function SearchList() {
<MoreButton></MoreButton>
</SearchDropdown>
}
></ApplicationCard>
></HomeCard>
))}
{openCreateModal && (
<RenameDialog

View File

@ -26,7 +26,7 @@ export default function SearchPage({
// const { data: userInfo } = useFetchUserInfo();
const { t } = useTranslation();
return (
<section className="relative w-full flex transition-all justify-center items-center mt-32">
<section className="relative w-full flex transition-all justify-center items-center mt-[15vh]">
<div className="relative z-10 px-8 pt-8 flex text-transparent flex-col justify-center items-center w-[780px]">
<h1
className={cn(
@ -36,7 +36,7 @@ export default function SearchPage({
RAGFlow
</h1>
<div className="rounded-lg text-primary text-xl sticky flex justify-center w-full transform scale-100 mt-8 p-6 h-[230px] border">
<div className="rounded-lg text-primary text-xl sticky flex justify-center w-full transform scale-100 mt-8 p-6 h-[240px] border">
{!isSearching && <Spotlight className="z-0" />}
<div className="flex flex-col justify-center items-center w-2/3">
{!isSearching && (
@ -55,7 +55,7 @@ export default function SearchPage({
<div className="relative w-full ">
<Input
placeholder={t('search.searchGreeting')}
className="w-full rounded-full py-6 px-4 pr-10 text-text-primary text-lg bg-bg-base delay-700"
className="w-full rounded-full py-7 px-4 pr-10 text-text-primary text-lg bg-bg-base delay-700"
value={searchText}
onKeyUp={(e) => {
if (e.key === 'Enter') {

View File

@ -91,7 +91,6 @@ export default function SearchingView({
>
RAGFlow
</h1>
<div
className={cn(
' rounded-lg text-primary text-xl sticky flex flex-col justify-center w-2/3 max-w-[780px] transform scale-100 ml-16 ',
@ -117,14 +116,14 @@ export default function SearchingView({
/>
<div className="absolute right-2 top-1/2 -translate-y-1/2 transform flex items-center gap-1">
<X
className="text-text-secondary cursor-pointer"
className="text-text-secondary cursor-pointer opacity-80"
size={14}
onClick={() => {
setSearchtext('');
handleClickRelatedQuestion('');
}}
/>
<span className="text-text-secondary ml-4">|</span>
<span className="text-text-secondary opacity-20 ml-4">|</span>
<button
type="button"
className="rounded-full bg-text-primary p-1 text-bg-base shadow w-12 h-8 ml-4"
@ -170,11 +169,13 @@ export default function SearchingView({
</div>
)
)}
<div className="w-full border-b border-border-default/80 my-6"></div>
{answer.answer && !sendingLoading && (
<div className="w-full border-b border-border-default/80 my-6"></div>
)}
</>
)}
{/* retrieval documents */}
{!isSearchStrEmpty && (
{!isSearchStrEmpty && !sendingLoading && (
<>
<div className=" mt-3 w-44 ">
<RetrievalDocuments
@ -183,7 +184,7 @@ export default function SearchingView({
onTesting={handleTestChunk}
></RetrievalDocuments>
</div>
<div className="w-full border-b border-border-default/80 my-6"></div>
{/* <div className="w-full border-b border-border-default/80 my-6"></div> */}
</>
)}
<div className="mt-3 ">
@ -218,7 +219,7 @@ export default function SearchingView({
</Popover>
</div>
<div
className="flex gap-2 items-center text-xs text-text-secondary border p-1 rounded-lg w-fit"
className="flex gap-2 items-center text-xs text-text-secondary border p-1 rounded-lg w-fit mt-3"
onClick={() =>
clickDocumentButton(chunk.doc_id, chunk as any)
}
@ -237,26 +238,30 @@ export default function SearchingView({
)}
{relatedQuestions?.length > 0 &&
searchData.search_config.related_search && (
<div className="mt-14 w-full overflow-hidden opacity-100 max-h-96">
<p className="text-text-primary mb-2 text-xl">
{t('search.relatedSearch')}
</p>
<div className="mt-2 flex flex-wrap justify-start gap-2">
{relatedQuestions?.map((x, idx) => (
<Button
key={idx}
variant="transparent"
className="bg-bg-card text-text-secondary"
onClick={handleClickRelatedQuestion(
x,
searchData.search_config.summary,
)}
>
{x}
</Button>
))}
<>
<div className="w-full border-b border-border-default/80 mt-6"></div>
<div className="mt-6 w-full overflow-hidden opacity-100 max-h-96">
<p className="text-text-primary mb-2 text-xl">
{t('search.relatedSearch')}
</p>
<div className="mt-2 flex flex-wrap justify-start gap-2">
{relatedQuestions?.map((x, idx) => (
<Button
key={idx}
variant="transparent"
className="bg-bg-card text-text-secondary"
onClick={handleClickRelatedQuestion(
x,
searchData.search_config.summary,
)}
>
{x}
</Button>
))}
</div>
</div>
</div>
</>
)}
</div>
</div>
@ -272,7 +277,6 @@ export default function SearchingView({
</div>
)}
</div>
{mindMapVisible && (
<div className="flex-1 h-[88dvh] z-30 ml-32 mt-5">
<MindMapDrawer

View File

@ -14,7 +14,7 @@ export function ProfileSettingWrapperCard({
children,
}: ProfileSettingWrapperCardProps) {
return (
<Card className="w-full mb-5 border-border-button bg-transparent">
<Card className="w-full border-border-button bg-transparent relative">
<CardHeader className="border-b border-border-button p-5">
{header}
</CardHeader>

View File

@ -151,7 +151,7 @@ export default function McpServer() {
onOk={onImportOk}
></ImportMcpDialog>
)}
<Spotlight className="z-0" opcity={0.7} coverage={70} />
<Spotlight />
</ProfileSettingWrapperCard>
);
}

View File

@ -38,10 +38,18 @@ const UserSetting = () => {
</Breadcrumb>
</PageHeader>
<div
className={cn(styles.settingWrapper, 'overflow-auto flex flex-1 pt-4')}
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')}>
<div
className={cn(
styles.outletWrapper,
'flex flex-1 border border-border-button rounded-lg',
)}
>
<Outlet></Outlet>
</div>
</div>

View File

@ -1,6 +1,7 @@
// src/components/ProfilePage.tsx
import { AvatarUpload } from '@/components/avatar-upload';
import PasswordInput from '@/components/originui/password-input';
import Spotlight from '@/components/spotlight';
import { Button } from '@/components/ui/button';
import {
Form,
@ -121,7 +122,8 @@ const ProfilePage: FC = () => {
// };
return (
<div className="h-full w-full bg-bg-base text-text-secondary p-5">
<div className="h-full w-full text-text-secondary p-5 relative">
<Spotlight />
{/* Header */}
<header className="flex flex-col gap-1 justify-between items-start mb-6">
<h1 className="text-2xl font-bold text-text-primary">{t('profile')}</h1>

View File

@ -1,6 +1,18 @@
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Modal } from '@/components/ui/modal/modal';
import { IModalProps } from '@/interfaces/common';
import { Form, Input, Modal } from 'antd';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import * as z from 'zod';
const AddingUserModal = ({
visible,
@ -8,42 +20,54 @@ const AddingUserModal = ({
loading,
onOk,
}: IModalProps<string>) => {
const [form] = Form.useForm();
const { t } = useTranslation();
type FieldType = {
email?: string;
};
const formSchema = z.object({
email: z
.string()
.email()
.min(1, { message: t('common.required') }),
});
const handleOk = async () => {
const ret = await form.validateFields();
type FormData = z.infer<typeof formSchema>;
return onOk?.(ret.email);
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: {
email: '',
},
});
const handleOk = async (data: FormData) => {
return onOk?.(data.email);
};
return (
<Modal
title={t('setting.add')}
open={visible}
onOk={handleOk}
onCancel={hideModal}
okButtonProps={{ loading }}
open={visible || false}
onOpenChange={(open) => !open && hideModal?.()}
onOk={form.handleSubmit(handleOk)}
confirmLoading={loading}
okText={t('common.ok')}
cancelText={t('common.cancel')}
>
<Form
name="basic"
labelCol={{ span: 6 }}
wrapperCol={{ span: 18 }}
autoComplete="off"
form={form}
>
<Form.Item<FieldType>
label={t('setting.email')}
name="email"
rules={[{ required: true }]}
>
<Input />
</Form.Item>
<Form {...form}>
<form onSubmit={form.handleSubmit(handleOk)} className="space-y-4">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel required>{t('setting.email')}</FormLabel>
<FormControl>
<Input placeholder={t('setting.email')} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</Modal>
);

View File

@ -1,9 +0,0 @@
.teamWrapper {
width: 100%;
display: flex;
flex-direction: column;
gap: 20px;
.teamCard {
// width: 100%;
}
}

View File

@ -1,22 +1,26 @@
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import {
useFetchUserInfo,
useListTenantUser,
} from '@/hooks/user-setting-hooks';
import { Button, Card, Flex, Space } from 'antd';
import { useTranslation } from 'react-i18next';
import { TeamOutlined, UserAddOutlined, UserOutlined } from '@ant-design/icons';
import Spotlight from '@/components/spotlight';
import { SearchInput } from '@/components/ui/input';
import { Separator } from '@/components/ui/separator';
import { UserPlus } from 'lucide-react';
import { useState } from 'react';
import AddingUserModal from './add-user-modal';
import { useAddUser } from './hooks';
import styles from './index.less';
import TenantTable from './tenant-table';
import UserTable from './user-table';
const iconStyle = { fontSize: 20, color: '#1677ff' };
const UserSettingTeam = () => {
const { data: userInfo } = useFetchUserInfo();
const { t } = useTranslation();
const [searchTerm, setSearchTerm] = useState('');
const [searchUser, setSearchUser] = useState('');
useListTenantUser();
const {
addingTenantModalVisible,
@ -26,38 +30,58 @@ const UserSettingTeam = () => {
} = useAddUser();
return (
<div className={styles.teamWrapper}>
<Card className={styles.teamCard}>
<Flex align="center" justify={'space-between'}>
<span>
{userInfo.nickname} {t('setting.workspace')}
</span>
<Button type="primary" onClick={showAddingTenantModal}>
<UserAddOutlined />
{t('setting.invite')}
</Button>
</Flex>
<div className="w-full flex flex-col gap-4 p-4 relative">
<Spotlight />
<Card className="bg-transparent border-none px-0">
<CardHeader className="flex flex-row items-center justify-between space-y-0 px-0 pt-1">
<CardTitle className="text-2xl font-medium">
{userInfo?.nickname} {t('setting.workspace')}
</CardTitle>
</CardHeader>
</Card>
<Card
title={
<Space>
<UserOutlined style={iconStyle} /> {t('setting.teamMembers')}
</Space>
}
bordered={false}
>
<UserTable></UserTable>
<Separator className="border-border-button bg-border-button w-[calc(100%+2rem)] -translate-x-4 -translate-y-4" />
<Card className="bg-transparent border-none">
<CardHeader className="flex flex-row items-center justify-between space-y-0 p-0 pb-4">
{/* <User className="mr-2 h-5 w-5 text-[#1677ff]" /> */}
<CardTitle className="text-base">
{t('setting.teamMembers')}
</CardTitle>
<section className="flex gap-4 items-center">
<SearchInput
className="bg-bg-input border-border-default w-32"
placeholder={t('common.search')}
value={searchUser}
onChange={(e) => setSearchUser(e.target.value)}
/>
<Button onClick={showAddingTenantModal}>
<UserPlus className=" h-4 w-4" />
{t('setting.invite')}
</Button>
</section>
</CardHeader>
<CardContent className="p-0">
<UserTable searchUser={searchUser}></UserTable>
</CardContent>
</Card>
<Card
title={
<Space>
<TeamOutlined style={iconStyle} /> {t('setting.joinedTeams')}
</Space>
}
bordered={false}
>
<TenantTable></TenantTable>
<Card className="bg-transparent border-none mt-8">
<CardHeader className="flex flex-row items-center justify-between space-y-0 p-0 pb-4">
{/* <Users className="mr-2 h-5 w-5 text-[#1677ff]" /> */}
<CardTitle className="text-base">
{t('setting.joinedTeams')}
</CardTitle>
<SearchInput
className="bg-bg-input border-border-default w-32"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder={t('common.search')}
/>
</CardHeader>
<CardContent className="p-0">
<TenantTable searchTerm={searchTerm}></TenantTable>
</CardContent>
</Card>
{addingTenantModalVisible && (
<AddingUserModal
visible

View File

@ -1,75 +1,160 @@
import { RAGFlowAvatar } from '@/components/ragflow-avatar';
import { Button } from '@/components/ui/button';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { useFetchUserInfo, useListTenant } from '@/hooks/user-setting-hooks';
import { ITenant } from '@/interfaces/database/user-setting';
import { formatDate } from '@/utils/date';
import type { TableProps } from 'antd';
import { Button, Space, Table } from 'antd';
import { ArrowDown, ArrowUp, ArrowUpDown, LogOut } from 'lucide-react';
import { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { TenantRole } from '../constants';
import { useHandleAgreeTenant, useHandleQuitUser } from './hooks';
const TenantTable = () => {
const TenantTable = ({ searchTerm }: { searchTerm: string }) => {
const { t } = useTranslation();
const { data, loading } = useListTenant();
const { handleAgree } = useHandleAgreeTenant();
const { data: user } = useFetchUserInfo();
const { handleQuitTenantUser } = useHandleQuitUser();
const [sortOrder, setSortOrder] = useState<'asc' | 'desc' | null>(null);
const sortedData = useMemo(() => {
if (!data || data.length === 0) return data;
let filtered = data;
if (searchTerm) {
filtered = data.filter(
(tenant) =>
tenant.nickname.toLowerCase().includes(searchTerm.toLowerCase()) ||
tenant.email.toLowerCase().includes(searchTerm.toLowerCase()),
);
}
if (sortOrder) {
filtered = [...filtered].sort((a, b) => {
const dateA = new Date(a.update_date).getTime();
const dateB = new Date(b.update_date).getTime();
const columns: TableProps<ITenant>['columns'] = [
{
title: t('common.name'),
dataIndex: 'nickname',
key: 'nickname',
},
{
title: t('setting.email'),
dataIndex: 'email',
key: 'email',
},
{
title: t('setting.updateDate'),
dataIndex: 'update_date',
key: 'update_date',
render(value) {
return formatDate(value);
},
},
{
title: t('common.action'),
key: 'action',
render: (_, { role, tenant_id }) => {
if (role === TenantRole.Invite) {
return (
<Space>
<Button type="link" onClick={handleAgree(tenant_id, true)}>
{t(`setting.agree`)}
</Button>
<Button type="link" onClick={handleAgree(tenant_id, false)}>
{t(`setting.refuse`)}
</Button>
</Space>
);
} else if (role === TenantRole.Normal && user.id !== tenant_id) {
return (
<Button
type="link"
onClick={handleQuitTenantUser(user.id, tenant_id)}
>
{t('setting.quit')}
</Button>
);
if (sortOrder === 'asc') {
return dateA - dateB;
} else {
return dateB - dateA;
}
},
},
];
});
}
return filtered;
}, [data, sortOrder, searchTerm]);
const toggleSortOrder = () => {
if (sortOrder === 'asc') {
setSortOrder('desc');
} else if (sortOrder === 'desc') {
setSortOrder(null);
} else {
setSortOrder('asc');
}
};
const renderSortIcon = () => {
if (sortOrder === 'asc') {
return <ArrowUp className="ml-1 h-4 w-4" />;
} else if (sortOrder === 'desc') {
return <ArrowDown className="ml-1 h-4 w-4" />;
} else {
return <ArrowUpDown className="ml-1 h-4 w-4" />;
}
};
return (
<Table<ITenant>
columns={columns}
dataSource={data}
rowKey={'tenant_id'}
loading={loading}
pagination={false}
/>
<div className="rounded-lg bg-bg-input scrollbar-auto overflow-hidden border border-border-default">
<Table rootClassName="rounded-lg">
<TableHeader>
<TableRow>
<TableHead className="h-12 px-4">{t('common.name')}</TableHead>
<TableHead
className="h-12 px-4 cursor-pointer"
onClick={toggleSortOrder}
>
<div className="flex items-center">
{t('setting.updateDate')}
{renderSortIcon()}
</div>
</TableHead>
<TableHead className="h-12 px-4">{t('setting.email')}</TableHead>
<TableHead className="h-12 px-4">{t('common.action')}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow>
<TableCell colSpan={4} className="h-24 text-center">
<div className="flex items-center justify-center">
<div className="h-4 w-4 animate-spin rounded-full border-2 border-solid border-current border-r-transparent align-[-0.125em] motion-reduce:animate-[spin_1.5s_linear_infinite]"></div>
</div>
</TableCell>
</TableRow>
) : sortedData && sortedData.length > 0 ? (
sortedData.map((tenant) => (
<TableRow key={tenant.tenant_id} className="hover:bg-bg-card">
<TableCell className="p-4 flex gap-1 items-center">
<RAGFlowAvatar
isPerson
className="size-4"
avatar={tenant.avatar}
name={tenant.nickname}
/>
{tenant.nickname}
</TableCell>
<TableCell className="p-4">
{formatDate(tenant.update_date)}
</TableCell>
<TableCell className="p-4">{tenant.email}</TableCell>
<TableCell className="p-4">
{tenant.role === TenantRole.Invite ? (
<div className="flex gap-2">
<Button
variant="link"
className="p-0 h-auto"
onClick={handleAgree(tenant.tenant_id, true)}
>
{t(`setting.agree`)}
</Button>
<Button
variant="link"
className="p-0 h-auto"
onClick={handleAgree(tenant.tenant_id, false)}
>
{t(`setting.refuse`)}
</Button>
</div>
) : tenant.role === TenantRole.Normal &&
user.id !== tenant.tenant_id ? (
<Button
variant="ghost"
size="icon"
className="h-8 w-8 p-0"
onClick={handleQuitTenantUser(user.id, tenant.tenant_id)}
>
{/* {t('setting.quit')} */}
<LogOut />
</Button>
) : null}
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={4} className="h-24 text-center">
{t('common.noData')}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
);
};

View File

@ -1,75 +1,160 @@
import { RAGFlowAvatar } from '@/components/ragflow-avatar';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { useListTenantUser } from '@/hooks/user-setting-hooks';
import { ITenantUser } from '@/interfaces/database/user-setting';
import { formatDate } from '@/utils/date';
import { DeleteOutlined } from '@ant-design/icons';
import type { TableProps } from 'antd';
import { Button, Table, Tag } from 'antd';
import { upperFirst } from 'lodash';
import { ArrowDown, ArrowUp, ArrowUpDown, Trash2 } from 'lucide-react';
import { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { TenantRole } from '../constants';
import { useHandleDeleteUser } from './hooks';
const ColorMap = {
[TenantRole.Normal]: 'green',
[TenantRole.Invite]: 'orange',
[TenantRole.Owner]: 'red',
const ColorMap: Record<string, string> = {
[TenantRole.Normal]: 'bg-transparent text-text-primary',
[TenantRole.Invite]: 'bg-accent-primary-5 bg-accent-primary rounded-sm',
[TenantRole.Owner]: 'bg-red-100 text-red-800',
};
const UserTable = () => {
const UserTable = ({ searchUser }: { searchUser: string }) => {
const { data, loading } = useListTenantUser();
const { handleDeleteTenantUser } = useHandleDeleteUser();
const [sortOrder, setSortOrder] = useState<'asc' | 'desc' | null>(null);
const { t } = useTranslation();
const sortedData = useMemo(() => {
console.log('sortedData', data, searchUser);
if (!data || data.length === 0) return data;
let filtered = data;
if (searchUser) {
filtered = filtered.filter(
(tenant) =>
tenant.nickname.toLowerCase().includes(searchUser.toLowerCase()) ||
tenant.email.toLowerCase().includes(searchUser.toLowerCase()),
);
}
if (sortOrder) {
filtered = [...filtered].sort((a, b) => {
const dateA = new Date(a.update_date).getTime();
const dateB = new Date(b.update_date).getTime();
const columns: TableProps<ITenantUser>['columns'] = [
{
title: t('common.name'),
dataIndex: 'nickname',
key: 'nickname',
},
{
title: t('setting.email'),
dataIndex: 'email',
key: 'email',
},
{
title: t('setting.role'),
dataIndex: 'role',
key: 'role',
render(value, { role }) {
return (
<Tag color={ColorMap[role as keyof typeof ColorMap]}>
{upperFirst(role)}
</Tag>
);
},
},
{
title: t('setting.updateDate'),
dataIndex: 'update_date',
key: 'update_date',
render(value) {
return formatDate(value);
},
},
{
title: t('common.action'),
key: 'action',
render: (_, record) => (
<Button type="text" onClick={handleDeleteTenantUser(record.user_id)}>
<DeleteOutlined size={20} />
</Button>
),
},
];
if (sortOrder === 'asc') {
return dateA - dateB;
} else {
return dateB - dateA;
}
});
}
return filtered;
}, [data, sortOrder, searchUser]);
const toggleSortOrder = () => {
if (sortOrder === 'asc') {
setSortOrder('desc');
} else if (sortOrder === 'desc') {
setSortOrder(null);
} else {
setSortOrder('asc');
}
};
const renderSortIcon = () => {
if (sortOrder === 'asc') {
return <ArrowUp className="ml-1 h-4 w-4 " />;
} else if (sortOrder === 'desc') {
return <ArrowDown className="ml-1 h-4 w-4" />;
} else {
return <ArrowUpDown className="ml-1 h-4 w-4" />;
}
};
return (
<Table<ITenantUser>
rowKey={'user_id'}
columns={columns}
dataSource={data}
loading={loading}
pagination={false}
/>
<div className="rounded-lg bg-bg-input scrollbar-auto overflow-hidden border border-border-default">
<Table rootClassName="rounded-lg">
<TableHeader>
<TableRow>
<TableHead className="h-12 px-4">{t('common.name')}</TableHead>
<TableHead
className="h-12 px-4 cursor-pointer"
onClick={toggleSortOrder}
>
<div className="flex items-center">
{t('setting.updateDate')}
{renderSortIcon()}
</div>
</TableHead>
<TableHead className="h-12 px-4">{t('setting.email')}</TableHead>
<TableHead className="h-12 px-4">{t('setting.role')}</TableHead>
<TableHead className="h-12 px-4">{t('common.action')}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow>
<TableCell colSpan={5} className="h-24 text-center">
<div className="flex items-center justify-center">
<div className="h-4 w-4 animate-spin rounded-full border-2 border-solid border-current border-r-transparent align-[-0.125em] motion-reduce:animate-[spin_1.5s_linear_infinite]"></div>
</div>
</TableCell>
</TableRow>
) : sortedData && sortedData.length > 0 ? (
sortedData.map((record) => (
<TableRow key={record.user_id} className="hover:bg-bg-card">
<TableCell className="p-4 ">
<div className="flex gap-1 items-center">
<RAGFlowAvatar
isPerson
className="size-4"
avatar={record.avatar}
name={record.nickname}
/>
{record.nickname}
</div>
</TableCell>
<TableCell className="p-4">
{formatDate(record.update_date)}
</TableCell>
<TableCell className="p-4">{record.email}</TableCell>
<TableCell className="p-4">
{record.role === TenantRole.Normal && (
<Badge className={ColorMap[record.role]}>
{upperFirst('Member')}
</Badge>
)}
{record.role !== TenantRole.Normal && (
<Badge className={ColorMap[record.role]}>
{upperFirst(record.role)}
</Badge>
)}
</TableCell>
<TableCell className="p-4">
<Button
variant="ghost"
size="icon"
className="h-8 w-8 p-0"
onClick={handleDeleteTenantUser(record.user_id)}
>
<Trash2 className="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={5} className="h-24 text-center">
{t('common.noData')}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
);
};