feat: continue work

This commit is contained in:
Joel 2026-05-09 17:25:59 +08:00
parent 0c387f8bc4
commit e2411ab4ee
6 changed files with 163 additions and 0 deletions

View File

@ -47,6 +47,13 @@ vi.mock('@/hooks/use-import-dsl', () => ({
isFetching: false,
}),
}))
vi.mock('@/hooks/use-format-time-from-now', () => ({
useFormatTimeFromNow: () => ({
formatTimeFromNow: () => '3 minutes ago',
}),
}))
vi.mock('@/utils/create-app-tracking', () => ({
trackCreateApp: (...args: unknown[]) => mockTrackCreateApp(...args),
}))
@ -193,6 +200,22 @@ describe('AppList', () => {
expect(screen.getByText('Alpha')).toBeInTheDocument()
expect(screen.getByText('Beta')).toBeInTheDocument()
})
it('should render continue work placeholders', () => {
mockExploreData = {
categories: ['Writing'],
allList: [createApp()],
}
renderAppList()
expect(screen.getByRole('heading', { name: 'explore.continueWork.title' })).toBeInTheDocument()
expect(screen.getByText('Automated Email Reply')).toBeInTheDocument()
expect(screen.getByText('Customer Feedback Summary')).toBeInTheDocument()
expect(screen.getAllByText('Evan')).toHaveLength(5)
expect(screen.getAllByText('explore.continueWork.editedAt:{"time":"3 minutes ago"}')).toHaveLength(5)
expect(screen.getByRole('link', { name: 'explore.continueWork.exploreStudio' })).toHaveAttribute('href', '/apps')
})
})
describe('Props', () => {

View File

@ -17,6 +17,7 @@ import Loading from '@/app/components/base/loading'
import AppCard from '@/app/components/explore/app-card'
import Banner from '@/app/components/explore/banner/banner'
import Category from '@/app/components/explore/category'
import ContinueWork from '@/app/components/explore/continue-work'
import CreateAppModal from '@/app/components/explore/create-app-modal'
import { useAppContext } from '@/context/app-context'
import { useImportDSL } from '@/hooks/use-import-dsl'
@ -191,6 +192,7 @@ const Apps = ({
<Banner />
</div>
)}
<ContinueWork className="mt-10" />
<div className="sticky top-0 z-10 bg-background-body">
<div className={cn(

View File

@ -0,0 +1,61 @@
import { AppModeEnum } from '@/types/app'
export type ContinueWorkItem = {
id: string
title: string
author: string
updatedAt: number
emoji: string
avatarClassName: string
mode: AppModeEnum
}
const currentTime = Date.now()
export const continueWorkItems: ContinueWorkItem[] = [
{
id: 'automated-email-reply',
title: 'Automated Email Reply',
author: 'Evan',
updatedAt: currentTime - 30 * 1000,
emoji: '🕹️',
avatarClassName: 'bg-components-icon-bg-pink-soft',
mode: AppModeEnum.CHAT,
},
{
id: 'feature-request-copilot',
title: 'Dify Feature Request Copilot',
author: 'Evan',
updatedAt: currentTime - 3 * 60 * 1000,
emoji: '🪼',
avatarClassName: 'bg-components-icon-bg-blue-soft',
mode: AppModeEnum.CHAT,
},
{
id: 'book-translation',
title: 'Book Translation',
author: 'Evan',
updatedAt: currentTime - 2 * 60 * 60 * 1000,
emoji: '📙',
avatarClassName: 'bg-components-icon-bg-orange-dark-soft',
mode: AppModeEnum.WORKFLOW,
},
{
id: 'svg-logo-design',
title: 'SVG Logo Design',
author: 'Evan',
updatedAt: currentTime - 24 * 60 * 60 * 1000,
emoji: '🖌️',
avatarClassName: 'bg-components-icon-bg-indigo-soft',
mode: AppModeEnum.AGENT_CHAT,
},
{
id: 'customer-feedback-summary',
title: 'Customer Feedback Summary',
author: 'Evan',
updatedAt: currentTime - 5 * 24 * 60 * 60 * 1000,
emoji: '📊',
avatarClassName: 'bg-components-icon-bg-teal-soft',
mode: AppModeEnum.COMPLETION,
},
]

View File

@ -0,0 +1,71 @@
'use client'
import { cn } from '@langgenius/dify-ui/cn'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import { AppTypeIcon } from '@/app/components/app/type-selector'
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
import Link from '@/next/link'
import { continueWorkItems } from './data'
type ContinueWorkProps = {
className?: string
}
const ContinueWork = ({
className,
}: ContinueWorkProps) => {
const { t } = useTranslation()
const { formatTimeFromNow } = useFormatTimeFromNow()
return (
<section className={cn('px-12', className)} aria-labelledby="continue-work-title">
<div className="flex items-center justify-between">
<h2 id="continue-work-title" className="min-w-0 truncate system-xl-semibold text-text-primary">
{t('continueWork.title', { ns: 'explore' })}
</h2>
<Link
href="/apps"
className="ml-4 shrink-0 system-sm-medium text-text-accent"
>
{t('continueWork.exploreStudio', { ns: 'explore' })}
</Link>
</div>
<div className="grid grid-cols-1 gap-3 pt-2 sm:grid-cols-2 xl:grid-cols-4">
{continueWorkItems.map(item => (
<article
key={item.id}
className="flex min-w-0 items-center gap-3 overflow-hidden rounded-xl border-[0.5px] border-components-panel-border-subtle bg-components-panel-on-panel-item-bg px-4 pt-4 pb-3 shadow-md"
>
<div className="relative shrink-0">
<div className={cn(
'flex size-10 items-center justify-center rounded-lg border-[0.5px] border-divider-regular text-2xl leading-none',
item.avatarClassName,
)}
>
<span aria-hidden>{item.emoji}</span>
</div>
<AppTypeIcon
type={item.mode}
wrapperClassName="absolute -right-0.5 -bottom-0.5 size-4 rounded-xs border-components-panel-on-panel-item-bg shadow-sm"
className="size-3"
/>
</div>
<div className="min-w-0 py-px">
<h3 className="truncate system-md-semibold text-text-secondary" title={item.title}>
{item.title}
</h3>
<div className="flex min-w-0 items-center gap-1 system-xs-regular text-text-tertiary">
<span className="shrink-0">{item.author}</span>
<span className="shrink-0">·</span>
<span className="min-w-0 truncate">{t('continueWork.editedAt', { ns: 'explore', time: formatTimeFromNow(item.updatedAt) })}</span>
</div>
</div>
</article>
))}
</div>
</section>
)
}
export default React.memo(ContinueWork)

View File

@ -20,6 +20,9 @@
"category.Translate": "Translate",
"category.Workflow": "Workflow",
"category.Writing": "Writing",
"continueWork.editedAt": "Edited {{time}}",
"continueWork.exploreStudio": "Explore Studio →",
"continueWork.title": "Continue work with",
"sidebar.action.delete": "Delete",
"sidebar.action.pin": "Pin",
"sidebar.action.rename": "Rename",

View File

@ -20,6 +20,9 @@
"category.Translate": "翻译",
"category.Workflow": "工作流",
"category.Writing": "写作",
"continueWork.editedAt": "{{time}}编辑",
"continueWork.exploreStudio": "探索工作室 →",
"continueWork.title": "继续处理",
"sidebar.action.delete": "删除",
"sidebar.action.pin": "置顶",
"sidebar.action.rename": "重命名",