mirror of
https://github.com/lobehub/lobehub
synced 2026-04-21 17:47:27 +00:00
🐛 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:
parent
a7339bea13
commit
85227cf467
27 changed files with 24 additions and 954 deletions
|
|
@ -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.).
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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: [],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue