🐛 fix: recent delete (#13878)

* chore: update skills dir

* chore: remove unused recent fetch actions and components

* fix: recent delete functions

* chore: update comments
This commit is contained in:
Rdmclin2 2026-04-16 16:42:50 +08:00 committed by GitHub
parent a7339bea13
commit 85227cf467
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 24 additions and 954 deletions

View file

@ -393,16 +393,16 @@ The pattern is the same for every platform:
Pick the file for your target platform — each contains activation, navigation, send-message, and verification snippets specific to that app:
| Platform | Reference | Quick switcher |
| ------------- | ------------------------------------------------ | -------------- |
| Discord | [reference/discord.md](./reference/discord.md) | `Cmd+K` |
| Slack | [reference/slack.md](./reference/slack.md) | `Cmd+K` |
| Telegram | [reference/telegram.md](./reference/telegram.md) | `Cmd+F` |
| WeChat / 微信 | [reference/wechat.md](./reference/wechat.md) | `Cmd+F` |
| Lark / 飞书 | [reference/lark.md](./reference/lark.md) | `Cmd+K` |
| QQ | [reference/qq.md](./reference/qq.md) | `Cmd+F` |
| Platform | Reference | Quick switcher |
| ------------- | -------------------------------------------------- | -------------- |
| Discord | [references/discord.md](./references/discord.md) | `Cmd+K` |
| Slack | [references/slack.md](./references/slack.md) | `Cmd+K` |
| Telegram | [references/telegram.md](./references/telegram.md) | `Cmd+F` |
| WeChat / 微信 | [references/wechat.md](./references/wechat.md) | `Cmd+F` |
| Lark / 飞书 | [references/lark.md](./references/lark.md) | `Cmd+K` |
| QQ | [references/qq.md](./references/qq.md) | `Cmd+F` |
For **shared osascript patterns** (activate, type, paste, screenshot, read accessibility, common workflow template, gotchas), see [reference/osascript-common.md](./reference/osascript-common.md). Read this first if you're new to osascript automation.
For **shared osascript patterns** (activate, type, paste, screenshot, read accessibility, common workflow template, gotchas), see [references/osascript-common.md](./references/osascript-common.md). Read this first if you're new to osascript automation.
---
@ -513,4 +513,4 @@ Outputs to `.records/` directory (gitignored): `<name>.mp4` (video) + `<name>/`
### osascript
See [reference/osascript-common.md](./reference/osascript-common.md#gotchas) for the full osascript gotchas list (accessibility permissions, `keystroke` non-ASCII issues, locale-specific app names, rate limiting, etc.).
See [references/osascript-common.md](./references/osascript-common.md#gotchas) for the full osascript gotchas list (accessibility permissions, `keystroke` non-ASCII issues, locale-specific app names, rate limiting, etc.).

View file

@ -1,114 +0,0 @@
---
name: recent-data
description: Guide for using Recent Data (topics, resources, pages). Use when working with recently accessed items, implementing recent lists, or accessing session store recent data. Triggers on recent data usage or implementation tasks.
user-invocable: false
---
# Recent Data Usage Guide
Recent data (recentTopics, recentResources, recentPages) is stored in session store.
## Initialization
In app top-level (e.g., `RecentHydration.tsx`):
```tsx
import { useInitRecentTopic } from '@/hooks/useInitRecentTopic';
import { useInitRecentResource } from '@/hooks/useInitRecentResource';
import { useInitRecentPage } from '@/hooks/useInitRecentPage';
const App = () => {
useInitRecentTopic();
useInitRecentResource();
useInitRecentPage();
return <YourComponents />;
};
```
## Usage
### Method 1: Read from Store (Recommended)
```tsx
import { useSessionStore } from '@/store/session';
import { recentSelectors } from '@/store/session/selectors';
const Component = () => {
const recentTopics = useSessionStore(recentSelectors.recentTopics);
const isInit = useSessionStore(recentSelectors.isRecentTopicsInit);
if (!isInit) return <div>Loading...</div>;
return (
<div>
{recentTopics.map((topic) => (
<div key={topic.id}>{topic.title}</div>
))}
</div>
);
};
```
### Method 2: Use Hook Return (Single component)
```tsx
const { data: recentTopics, isLoading } = useInitRecentTopic();
```
## Available Selectors
### Recent Topics
```tsx
const recentTopics = useSessionStore(recentSelectors.recentTopics);
// Type: RecentTopic[]
const isInit = useSessionStore(recentSelectors.isRecentTopicsInit);
// Type: boolean
```
**RecentTopic type:**
```typescript
interface RecentTopic {
agent: {
avatar: string | null;
backgroundColor: string | null;
id: string;
title: string | null;
} | null;
id: string;
title: string | null;
updatedAt: Date;
}
```
### Recent Resources
```tsx
const recentResources = useSessionStore(recentSelectors.recentResources);
// Type: FileListItem[]
const isInit = useSessionStore(recentSelectors.isRecentResourcesInit);
```
### Recent Pages
```tsx
const recentPages = useSessionStore(recentSelectors.recentPages);
const isInit = useSessionStore(recentSelectors.isRecentPagesInit);
```
## Features
1. **Auto login detection**: Only loads when user is logged in
2. **Data caching**: Stored in store, no repeated loading
3. **Auto refresh**: SWR refreshes on focus (5-minute interval)
4. **Type safe**: Full TypeScript types
## Best Practices
1. Initialize all recent data at app top-level
2. Use selectors to read from store
3. For multi-component use, prefer Method 1
4. Use selectors for render optimization

View file

@ -1,30 +0,0 @@
import { useHomeStore } from '@/store/home';
import { useUserStore } from '@/store/user';
import { authSelectors } from '@/store/user/selectors';
/**
* Hook to initialize and fetch recent pages (documents)
* Only fetches when user is logged in
*
* Usage:
* const { isLoading } = useInitRecentPage();
*
* Then access data directly from store:
* const recentPages = useHomeStore(homeRecentSelectors.recentPages);
* const isInit = useHomeStore(homeRecentSelectors.isRecentPagesInit);
*/
export const useInitRecentPage = () => {
const useFetchRecentPages = useHomeStore((s) => s.useFetchRecentPages);
const isLogin = useUserStore(authSelectors.isLogin);
const { isValidating, data, ...rest } = useFetchRecentPages(isLogin);
return {
...rest,
data,
isLoading: rest.isLoading && isLogin,
// isRevalidating: has cached data, updating in background
isRevalidating: isValidating && !!data,
};
};

View file

@ -1,30 +0,0 @@
import { useHomeStore } from '@/store/home';
import { useUserStore } from '@/store/user';
import { authSelectors } from '@/store/user/selectors';
/**
* Hook to initialize and fetch recent resources (files)
* Only fetches when user is logged in
*
* Usage:
* const { isLoading } = useInitRecentResource();
*
* Then access data directly from store:
* const recentResources = useHomeStore(homeRecentSelectors.recentResources);
* const isInit = useHomeStore(homeRecentSelectors.isRecentResourcesInit);
*/
export const useInitRecentResource = () => {
const useFetchRecentResources = useHomeStore((s) => s.useFetchRecentResources);
const isLogin = useUserStore(authSelectors.isLogin);
const { isValidating, data, ...rest } = useFetchRecentResources(isLogin);
return {
...rest,
data,
isLoading: rest.isLoading && isLogin,
// isRevalidating: has cached data, updating in background
isRevalidating: isValidating && !!data,
};
};

View file

@ -1,30 +0,0 @@
import { useHomeStore } from '@/store/home';
import { useUserStore } from '@/store/user';
import { authSelectors } from '@/store/user/selectors';
/**
* Hook to initialize and fetch recent topics
* Only fetches when user is logged in
*
* Usage:
* const { isLoading } = useInitRecentTopic();
*
* Then access data directly from store:
* const recentTopics = useHomeStore(homeRecentSelectors.recentTopics);
* const isInit = useHomeStore(homeRecentSelectors.isRecentTopicsInit);
*/
export const useInitRecentTopic = () => {
const useFetchRecentTopics = useHomeStore((s) => s.useFetchRecentTopics);
const isLogin = useUserStore(authSelectors.isLogin);
const { isValidating, data, ...rest } = useFetchRecentTopics(isLogin);
return {
...rest,
data,
isLoading: rest.isLoading && isLogin,
// isRevalidating: has cached data, updating in background
isRevalidating: isValidating && !!data,
};
};

View file

@ -1,14 +1,8 @@
import { memo } from 'react';
import { useInitRecentPage } from '@/hooks/useInitRecentPage';
import { useInitRecentResource } from '@/hooks/useInitRecentResource';
import { useInitRecents } from '@/hooks/useInitRecents';
import { useInitRecentTopic } from '@/hooks/useInitRecentTopic';
const RecentHydration = memo(() => {
useInitRecentTopic();
useInitRecentResource();
useInitRecentPage();
useInitRecents();
return null;

View file

@ -1,103 +0,0 @@
'use client';
import { Avatar, Block, Center, Flexbox, Icon, Text } from '@lobehub/ui';
import { cssVar } from 'antd-style';
import { FileTextIcon } from 'lucide-react';
import { memo } from 'react';
import Time from '@/routes/(main)/home/features/components/Time';
import { RECENT_BLOCK_SIZE } from '@/routes/(main)/home/features/const';
import { type FileListItem } from '@/types/files';
import markdownToTxt from '@/utils/markdownToTxt';
// Helper to extract title from markdown content
const extractTitle = (content: string): string | null => {
if (!content) return null;
// Find first markdown header (# title)
// eslint-disable-next-line regexp/no-super-linear-backtracking
const match = content.match(/^#\s+(.+)$/m);
return match ? match[1].trim() : null;
};
// Helper to extract preview text from note content
const getPreviewText = (item: FileListItem): string => {
if (!item.content) return '';
// Convert markdown to plain text
let plainText = markdownToTxt(item.content.slice(0, 120));
// Remove the title line if it exists
const title = extractTitle(item.content);
if (title) {
plainText = plainText.replace(title, '').trim();
}
// Limit to first 200 characters for preview
return plainText.slice(0, 200);
};
interface RecentPageItemProps {
document: FileListItem;
}
const RecentPageItem = memo<RecentPageItemProps>(({ document }) => {
const title = document.name || '';
const previewText = getPreviewText(document);
const emoji = document.metadata?.emoji;
return (
<Block
clickable
flex={'none'}
height={RECENT_BLOCK_SIZE.PAGE.HEIGHT}
variant={'outlined'}
width={RECENT_BLOCK_SIZE.PAGE.WIDTH}
style={{
borderRadius: cssVar.borderRadiusLG,
overflow: 'hidden',
}}
>
<Center
flex={'none'}
height={44}
style={{
background: cssVar.colorFillTertiary,
overflow: 'hidden',
}}
/>
<Flexbox flex={1} gap={6} justify={'space-between'} padding={12}>
<Flexbox
gap={6}
style={{
marginTop: -28,
}}
>
{emoji ? (
<Avatar avatar={emoji} shape={'square'} size={30} />
) : (
<Center flex={'none'} height={30} style={{ marginLeft: -4 }} width={30}>
<Icon color={cssVar.colorTextDescription} icon={FileTextIcon} size={24} />
</Center>
)}
<Text ellipsis={{ rows: 2 }} style={{ fontSize: 14, lineHeight: 1.4 }} weight={500}>
{title}
</Text>
{previewText && (
<Text
ellipsis={{ rows: 2 }}
fontSize={13}
style={{ lineHeight: 1.5 }}
type={'secondary'}
>
{previewText}
</Text>
)}
</Flexbox>
<Time date={document.updatedAt} />
</Flexbox>
</Block>
);
});
export default RecentPageItem;

View file

@ -1,43 +0,0 @@
'use client';
import { memo } from 'react';
import { Link } from 'react-router-dom';
import GroupSkeleton from '@/routes/(main)/home/features/components/GroupSkeleton';
import { RECENT_BLOCK_SIZE } from '@/routes/(main)/home/features/const';
import { useHomeStore } from '@/store/home';
import { homeRecentSelectors } from '@/store/home/selectors';
import { standardizeIdentifier } from '@/utils/identifier';
import RecentPageItem from './Item';
const RecentPageList = memo(() => {
const documents = useHomeStore(homeRecentSelectors.recentPages);
const isInit = useHomeStore(homeRecentSelectors.isRecentPagesInit);
// Loading state
if (!isInit) {
return (
<GroupSkeleton height={RECENT_BLOCK_SIZE.PAGE.HEIGHT} width={RECENT_BLOCK_SIZE.PAGE.WIDTH} />
);
}
return documents.map((document) => {
const pageUrl = `/page/${standardizeIdentifier(document.id)}`;
return (
<Link
key={document.id}
to={pageUrl}
style={{
color: 'inherit',
textDecoration: 'none',
}}
>
<RecentPageItem document={document} />
</Link>
);
});
});
export default RecentPageList;

View file

@ -1,75 +0,0 @@
'use client';
import { ActionIcon, DropdownMenu } from '@lobehub/ui';
import { FileTextIcon, MoreHorizontal } from 'lucide-react';
import { memo, Suspense } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import NeuralNetworkLoading from '@/components/NeuralNetworkLoading';
import { useInitRecentPage } from '@/hooks/useInitRecentPage';
import { useResourceManagerStore } from '@/routes/(main)/resource/features/store';
import { useHomeStore } from '@/store/home';
import { homeRecentSelectors } from '@/store/home/selectors';
import { FilesTabs } from '@/types/files';
import GroupBlock from '../components/GroupBlock';
import GroupSkeleton from '../components/GroupSkeleton';
import ScrollShadowWithButton from '../components/ScrollShadowWithButton';
import { RECENT_BLOCK_SIZE } from '../const';
import RecentPageList from './List';
const RecentPage = memo(() => {
const { t } = useTranslation('file');
const navigate = useNavigate();
const setCategory = useResourceManagerStore((s) => s.setCategory);
const recentPages = useHomeStore(homeRecentSelectors.recentPages);
const isInit = useHomeStore(homeRecentSelectors.isRecentPagesInit);
const { isRevalidating } = useInitRecentPage();
// After loaded, if no data, don't render
if (isInit && (!recentPages || recentPages.length === 0)) {
return null;
}
return (
<GroupBlock
icon={FileTextIcon}
title={t('home.recentPages')}
action={
<>
{isRevalidating && <NeuralNetworkLoading size={14} />}
<DropdownMenu
items={[
{
key: 'all-documents',
label: t('menu.allPages'),
onClick: () => {
setCategory(FilesTabs.Pages);
navigate('/resource');
},
},
]}
>
<ActionIcon icon={MoreHorizontal} size="small" />
</DropdownMenu>
</>
}
>
<ScrollShadowWithButton>
<Suspense
fallback={
<GroupSkeleton
height={RECENT_BLOCK_SIZE.PAGE.HEIGHT}
width={RECENT_BLOCK_SIZE.PAGE.WIDTH}
/>
}
>
<RecentPageList />
</Suspense>
</ScrollShadowWithButton>
</GroupBlock>
);
});
export default RecentPage;

View file

@ -1,80 +0,0 @@
'use client';
import { Block, Center, Flexbox, Image, Text } from '@lobehub/ui';
import { cssVar } from 'antd-style';
import { memo } from 'react';
import FileIcon from '@/components/FileIcon';
import Time from '@/routes/(main)/home/features/components/Time';
import { RECENT_BLOCK_SIZE } from '@/routes/(main)/home/features/const';
import { type FileListItem } from '@/types/files';
import { formatSize } from '@/utils/format';
const IMAGE_FILE_TYPES = new Set([
'image/png',
'image/jpeg',
'image/jpg',
'image/gif',
'image/webp',
'image/svg+xml',
]);
interface RecentResourceItemProps {
file: FileListItem;
}
const RecentResourceItem = memo<RecentResourceItemProps>(({ file }) => {
const isImage = IMAGE_FILE_TYPES.has(file.fileType);
return (
<Block
clickable
flex={'none'}
height={RECENT_BLOCK_SIZE.RESOURCE.HEIGHT}
variant={'outlined'}
width={RECENT_BLOCK_SIZE.RESOURCE.WIDTH}
style={{
borderRadius: cssVar.borderRadiusLG,
overflow: 'hidden',
}}
>
<Center
flex={'none'}
height={148}
style={{ background: cssVar.colorFillTertiary, overflow: 'hidden' }}
>
{isImage && file.url ? (
<Image
alt={file.name}
height={'100%'}
objectFit={'cover'}
preview={false}
src={file.url}
width={'100%'}
style={{
borderRadius: 0,
width: '100%',
}}
/>
) : (
<FileIcon fileName={file.name} fileType={file.fileType} size={48} />
)}
</Center>
{/* File Info */}
<Flexbox flex={1} gap={6} justify={'space-between'} padding={12}>
<Text ellipsis fontSize={13} style={{ lineHeight: 1.4 }} weight={500}>
{file.name}
</Text>
<Flexbox horizontal align={'center'} gap={8}>
<Time date={file.updatedAt} />
<Text ellipsis fontSize={12} type={'secondary'}>
{formatSize(file.size)}
</Text>
</Flexbox>
</Flexbox>
</Block>
);
});
export default RecentResourceItem;

View file

@ -1,46 +0,0 @@
'use client';
import { memo } from 'react';
import { Link } from 'react-router-dom';
import GroupSkeleton from '@/routes/(main)/home/features/components/GroupSkeleton';
import { RECENT_BLOCK_SIZE } from '@/routes/(main)/home/features/const';
import { useHomeStore } from '@/store/home';
import { homeRecentSelectors } from '@/store/home/selectors';
import RecentResourceItem from './Item';
const RecentResourceList = memo(() => {
const files = useHomeStore(homeRecentSelectors.recentResources);
const isInit = useHomeStore(homeRecentSelectors.isRecentResourcesInit);
// Loading state
if (!isInit) {
return (
<GroupSkeleton
height={RECENT_BLOCK_SIZE.RESOURCE.HEIGHT}
width={RECENT_BLOCK_SIZE.RESOURCE.WIDTH}
/>
);
}
return files.map((file) => {
const isPage = file.fileType === 'text/plain';
const fileUrl = isPage ? `/resource/${file.id}` : `/resource?file=${file.id}`;
return (
<Link
key={file.id}
to={fileUrl}
style={{
color: 'inherit',
textDecoration: 'none',
}}
>
<RecentResourceItem file={file} />
</Link>
);
});
});
export default RecentResourceList;

View file

@ -1,75 +0,0 @@
'use client';
import { ActionIcon, DropdownMenu } from '@lobehub/ui';
import { Clock, MoreHorizontal } from 'lucide-react';
import { memo, Suspense } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import NeuralNetworkLoading from '@/components/NeuralNetworkLoading';
import { useInitRecentResource } from '@/hooks/useInitRecentResource';
import { useResourceManagerStore } from '@/routes/(main)/resource/features/store';
import { useHomeStore } from '@/store/home';
import { homeRecentSelectors } from '@/store/home/selectors';
import { FilesTabs } from '@/types/files';
import GroupBlock from '../components/GroupBlock';
import GroupSkeleton from '../components/GroupSkeleton';
import ScrollShadowWithButton from '../components/ScrollShadowWithButton';
import { RECENT_BLOCK_SIZE } from '../const';
import RecentResourceList from './List';
const RecentResource = memo(() => {
const { t } = useTranslation('file');
const navigate = useNavigate();
const setCategory = useResourceManagerStore((s) => s.setCategory);
const recentResources = useHomeStore(homeRecentSelectors.recentResources);
const isInit = useHomeStore(homeRecentSelectors.isRecentResourcesInit);
const { isRevalidating } = useInitRecentResource();
// After loaded, if no data, don't render
if (isInit && (!recentResources || recentResources.length === 0)) {
return null;
}
return (
<GroupBlock
icon={Clock}
title={t('home.recentFiles')}
action={
<>
{isRevalidating && <NeuralNetworkLoading size={14} />}
<DropdownMenu
items={[
{
key: 'all-files',
label: t('menu.allFiles'),
onClick: () => {
setCategory(FilesTabs.All);
navigate('/resource');
},
},
]}
>
<ActionIcon icon={MoreHorizontal} size="small" />
</DropdownMenu>
</>
}
>
<ScrollShadowWithButton>
<Suspense
fallback={
<GroupSkeleton
height={RECENT_BLOCK_SIZE.RESOURCE.HEIGHT}
width={RECENT_BLOCK_SIZE.RESOURCE.WIDTH}
/>
}
>
<RecentResourceList />
</Suspense>
</ScrollShadowWithButton>
</GroupBlock>
);
});
export default RecentResource;

View file

@ -1,105 +0,0 @@
import { Avatar, Block, Center, Flexbox, Text } from '@lobehub/ui';
import { cssVar } from 'antd-style';
import { memo, useMemo } from 'react';
import { DEFAULT_AVATAR } from '@/const/meta';
import GroupAvatar from '@/features/GroupAvatar';
import Time from '@/routes/(main)/home/features/components/Time';
import { RECENT_BLOCK_SIZE } from '@/routes/(main)/home/features/const';
import { type RecentTopic } from '@/types/topic';
const ReactTopicItem = memo<RecentTopic>(({ title, updatedAt, agent, group, type }) => {
const isGroup = type === 'group';
// For group topics, get the first member's background for blur effect
const blurBackground = isGroup ? group?.members?.[0]?.backgroundColor : agent?.backgroundColor;
// Build group avatars for GroupAvatar component
const groupAvatars = useMemo(() => {
if (!isGroup || !group?.members) return [];
return group.members.map((member) => ({
avatar: member.avatar || DEFAULT_AVATAR,
background: member.backgroundColor || undefined,
style: { borderRadius: 3 },
}));
}, [isGroup, group?.members]);
// Display title - group title or agent title
const displayTitle = isGroup ? group?.title : agent?.title;
return (
<Block
clickable
flex={'none'}
height={RECENT_BLOCK_SIZE.TOPIC.HEIGHT}
variant={'outlined'}
width={RECENT_BLOCK_SIZE.TOPIC.WIDTH}
style={{
borderRadius: cssVar.borderRadiusLG,
overflow: 'hidden',
}}
>
<Center
flex={'none'}
height={44}
style={{
background: cssVar.colorFillTertiary,
overflow: 'hidden',
}}
>
<Avatar
emojiScaleWithBackground
background={blurBackground || undefined}
shape={'square'}
size={200}
avatar={
isGroup
? group?.members?.[0]?.avatar || DEFAULT_AVATAR
: agent?.avatar || DEFAULT_AVATAR
}
style={{
filter: 'blur(100px)',
}}
/>
</Center>
<Flexbox flex={1} gap={6} justify={'space-between'} padding={12}>
<Flexbox
gap={6}
style={{
marginTop: -28,
}}
>
{isGroup ? (
<GroupAvatar
avatars={groupAvatars}
size={30}
style={{
background: cssVar.colorBgLayout,
}}
/>
) : (
<Avatar
emojiScaleWithBackground
avatar={agent?.avatar || DEFAULT_AVATAR}
background={agent?.backgroundColor || undefined}
shape={'square'}
size={30}
title={agent?.title || undefined}
/>
)}
<Text ellipsis={{ rows: 2 }} style={{ lineHeight: 1.4 }} weight={500}>
{title}
</Text>
</Flexbox>
<Flexbox horizontal align={'center'} gap={8}>
<Time date={updatedAt} />
<Text ellipsis fontSize={12} type={'secondary'}>
{displayTitle}
</Text>
</Flexbox>
</Flexbox>
</Block>
);
});
export default ReactTopicItem;

View file

@ -1,47 +0,0 @@
import { memo } from 'react';
import { Link } from 'react-router-dom';
import GroupSkeleton from '@/routes/(main)/home/features/components/GroupSkeleton';
import { RECENT_BLOCK_SIZE } from '@/routes/(main)/home/features/const';
import { useHomeStore } from '@/store/home';
import { homeRecentSelectors } from '@/store/home/selectors';
import ReactTopicItem from './Item';
const RecentTopicList = memo(() => {
const recentTopics = useHomeStore(homeRecentSelectors.recentTopics);
const isInit = useHomeStore(homeRecentSelectors.isRecentTopicsInit);
// Loading state
if (!isInit) {
return (
<GroupSkeleton
height={RECENT_BLOCK_SIZE.TOPIC.HEIGHT}
width={RECENT_BLOCK_SIZE.TOPIC.WIDTH}
/>
);
}
return recentTopics.map((topic) => {
// Build URL based on topic type
const topicUrl =
topic.type === 'group' && topic.group
? `/group/${topic.group.id}?topic=${topic.id}`
: `/agent/${topic?.agent?.id}?topic=${topic.id}`;
return (
<Link
key={topic.id}
to={topicUrl}
style={{
color: 'inherit',
textDecoration: 'none',
}}
>
<ReactTopicItem {...topic} />
</Link>
);
});
});
export default RecentTopicList;

View file

@ -1,49 +0,0 @@
import { BotMessageSquareIcon } from 'lucide-react';
import { memo,Suspense } from 'react';
import { useTranslation } from 'react-i18next';
import NeuralNetworkLoading from '@/components/NeuralNetworkLoading';
import { useInitRecentTopic } from '@/hooks/useInitRecentTopic';
import { useHomeStore } from '@/store/home';
import { homeRecentSelectors } from '@/store/home/selectors';
import GroupBlock from '../components/GroupBlock';
import GroupSkeleton from '../components/GroupSkeleton';
import ScrollShadowWithButton from '../components/ScrollShadowWithButton';
import { RECENT_BLOCK_SIZE } from '../const';
import RecentTopicList from './List';
const RecentTopic = memo(() => {
const { t } = useTranslation('chat');
const recentTopics = useHomeStore(homeRecentSelectors.recentTopics);
const isInit = useHomeStore(homeRecentSelectors.isRecentTopicsInit);
const { isRevalidating } = useInitRecentTopic();
// After loaded, if no data, don't render
if (isInit && (!recentTopics || recentTopics.length === 0)) {
return null;
}
return (
<GroupBlock
action={isRevalidating && <NeuralNetworkLoading size={14} />}
icon={BotMessageSquareIcon}
title={t('topic.recent')}
>
<ScrollShadowWithButton>
<Suspense
fallback={
<GroupSkeleton
height={RECENT_BLOCK_SIZE.TOPIC.HEIGHT}
width={RECENT_BLOCK_SIZE.TOPIC.WIDTH}
/>
}
>
<RecentTopicList />
</Suspense>
</ScrollShadowWithButton>
</GroupBlock>
);
});
export default RecentTopic;

View file

@ -10,6 +10,7 @@ import SkeletonList from '@/features/NavPanel/components/SkeletonList';
import SideBarDrawer from '@/features/NavPanel/SideBarDrawer';
import { useClientDataSWR } from '@/libs/swr';
import { recentService } from '@/services/recent';
import { ALL_RECENTS_DRAWER_SWR_PREFIX } from '@/store/home/slices/recent/action';
import RecentListItem from './Item';
@ -22,8 +23,9 @@ const AllRecentsDrawer = memo<AllRecentsDrawerProps>(({ open, onClose }) => {
const { t } = useTranslation('common');
const [searchKeyword, setSearchKeyword] = useState('');
const { data: recents, isLoading } = useClientDataSWR(open ? ['allRecents', open] : null, () =>
recentService.getAll(50),
const { data: recents, isLoading } = useClientDataSWR(
open ? [ALL_RECENTS_DRAWER_SWR_PREFIX, open] : null,
() => recentService.getAll(50),
);
const filteredRecents = useMemo(() => {

View file

@ -9,7 +9,6 @@ import { type RecentItem } from '@/server/routers/lambda/recent';
import { documentService } from '@/services/document';
import { taskService } from '@/services/task';
import { topicService } from '@/services/topic';
import { useChatStore } from '@/store/chat';
import { useHomeStore } from '@/store/home';
export const useRecentItemDropdownMenu = (
@ -18,10 +17,8 @@ export const useRecentItemDropdownMenu = (
) => {
const { t } = useTranslation(['common', 'topic', 'components']);
const { modal } = App.useApp();
const removeTopic = useChatStore((s) => s.removeTopic);
const [updateRecentTitle, removeRecent, refreshRecents] = useHomeStore((s) => [
const [updateRecentTitle, refreshRecents] = useHomeStore((s) => [
s.updateRecentTitle,
s.removeRecent,
s.refreshRecents,
]);
@ -59,13 +56,10 @@ export const useRecentItemDropdownMenu = (
centered: true,
okButtonProps: { danger: true },
onOk: async () => {
// Optimistic remove
removeRecent(item.id);
// Persist to server
switch (item.type) {
case 'topic': {
await removeTopic(item.id);
// Home has no active agent/group, so chatStore.removeTopic early-returns; call the service directly
await topicService.removeTopic(item.id);
break;
}
case 'document': {
@ -77,12 +71,11 @@ export const useRecentItemDropdownMenu = (
break;
}
}
// Refresh to get accurate data from server
await refreshRecents();
},
title: confirmMessages[item.type] || t('delete', { ns: 'common' }),
});
}, [item, modal, t, removeTopic, removeRecent, refreshRecents]);
}, [item, modal, t, refreshRecents]);
const dropdownMenu = useCallback((): MenuProps['items'] => {
const canRename = true;

View file

@ -3,21 +3,16 @@ import { type SWRResponse } from 'swr';
import { mutate, useClientDataSWRWithSync } from '@/libs/swr';
import { type RecentItem } from '@/server/routers/lambda/recent';
import { fileService } from '@/services/file';
import { recentService } from '@/services/recent';
import { topicService } from '@/services/topic';
import { type HomeStore } from '@/store/home/store';
import { type StoreSetter } from '@/store/types';
import { type FileListItem } from '@/types/files';
import { type RecentTopic } from '@/types/topic';
import { setNamespace } from '@/utils/storeDebug';
const n = setNamespace('recent');
const FETCH_RECENT_TOPICS_KEY = 'fetchRecentTopics';
const FETCH_RECENT_RESOURCES_KEY = 'fetchRecentResources';
const FETCH_RECENT_PAGES_KEY = 'fetchRecentPages';
const FETCH_RECENTS_KEY = 'fetchRecents';
/** SWR key prefix for `AllRecentsDrawer` (`['allRecents', open]`) */
export const ALL_RECENTS_DRAWER_SWR_PREFIX = 'allRecents';
type Setter = StoreSetter<HomeStore>;
export const createRecentSlice = (set: Setter, get: () => HomeStore, _api?: unknown) =>
@ -46,13 +41,11 @@ export class RecentActionImpl {
this.#set({ recents }, false, n('updateRecentTitle'));
};
removeRecent = (id: string): void => {
const recents = this.#get().recents.filter((item) => item.id !== id);
this.#set({ recents }, false, n('removeRecent'));
};
refreshRecents = async (): Promise<void> => {
await mutate((key: unknown) => Array.isArray(key) && key[0] === FETCH_RECENTS_KEY);
await Promise.all([
mutate((key: unknown) => Array.isArray(key) && key[0] === FETCH_RECENTS_KEY),
mutate((key: unknown) => Array.isArray(key) && key[0] === ALL_RECENTS_DRAWER_SWR_PREFIX),
]);
};
useFetchRecents = (
@ -71,64 +64,6 @@ export class RecentActionImpl {
},
);
};
useFetchRecentPages = (isLogin: boolean | undefined): SWRResponse<any[]> => {
return useClientDataSWRWithSync<any[]>(
// Only fetch when login status is explicitly true (not null/undefined)
isLogin === true ? [FETCH_RECENT_PAGES_KEY, isLogin] : null,
async () => fileService.getRecentPages(12),
{
onData: (data) => {
if (this.#get().isRecentPagesInit && isEqual(this.#get().recentPages, data)) return;
this.#set(
{ isRecentPagesInit: true, recentPages: data },
false,
n('useFetchRecentPages/onData'),
);
},
},
);
};
useFetchRecentResources = (isLogin: boolean | undefined): SWRResponse<FileListItem[]> => {
return useClientDataSWRWithSync<FileListItem[]>(
// Only fetch when login status is explicitly true (not null/undefined)
isLogin === true ? [FETCH_RECENT_RESOURCES_KEY, isLogin] : null,
async () => fileService.getRecentFiles(12),
{
onData: (data) => {
if (this.#get().isRecentResourcesInit && isEqual(this.#get().recentResources, data))
return;
this.#set(
{ isRecentResourcesInit: true, recentResources: data },
false,
n('useFetchRecentResources/onData'),
);
},
},
);
};
useFetchRecentTopics = (isLogin: boolean | undefined): SWRResponse<RecentTopic[]> => {
return useClientDataSWRWithSync<RecentTopic[]>(
// Only fetch when login status is explicitly true (not null/undefined)
isLogin === true ? [FETCH_RECENT_TOPICS_KEY, isLogin] : null,
async () => topicService.getRecentTopics(12),
{
onData: (data) => {
if (this.#get().isRecentTopicsInit && isEqual(this.#get().recentTopics, data)) return;
this.#set(
{ isRecentTopicsInit: true, recentTopics: data },
false,
n('useFetchRecentTopics/onData'),
);
},
},
);
};
}
export type RecentAction = Pick<RecentActionImpl, keyof RecentActionImpl>;

View file

@ -1,27 +1,13 @@
import { type RecentItem } from '@/server/routers/lambda/recent';
import { type FileListItem } from '@/types/files';
import { type RecentTopic } from '@/types/topic';
export interface RecentState {
allRecentsDrawerOpen: boolean;
isRecentPagesInit: boolean;
isRecentResourcesInit: boolean;
isRecentsInit: boolean;
isRecentTopicsInit: boolean;
recentPages: any[];
recentResources: FileListItem[];
recents: RecentItem[];
recentTopics: RecentTopic[];
}
export const initialRecentState: RecentState = {
allRecentsDrawerOpen: false,
isRecentPagesInit: false,
isRecentResourcesInit: false,
isRecentTopicsInit: false,
isRecentsInit: false,
recentPages: [],
recentResources: [],
recentTopics: [],
recents: [],
};

View file

@ -1,22 +1,9 @@
import { type HomeStore } from '@/store/home/store';
const recentTopics = (s: HomeStore) => s.recentTopics;
const recentResources = (s: HomeStore) => s.recentResources;
const recentPages = (s: HomeStore) => s.recentPages;
const recents = (s: HomeStore) => s.recents;
const isRecentTopicsInit = (s: HomeStore) => s.isRecentTopicsInit;
const isRecentResourcesInit = (s: HomeStore) => s.isRecentResourcesInit;
const isRecentPagesInit = (s: HomeStore) => s.isRecentPagesInit;
const isRecentsInit = (s: HomeStore) => s.isRecentsInit;
export const homeRecentSelectors = {
isRecentPagesInit,
isRecentResourcesInit,
isRecentTopicsInit,
isRecentsInit,
recentPages,
recentResources,
recentTopics,
recents,
};