diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view.tsx index 7e592729a5..57f3ef6881 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view.tsx @@ -37,18 +37,22 @@ const CardView: FC = ({ appId, isInPanel, className }) => { const appDetail = useAppStore(state => state.appDetail) const setAppDetail = useAppStore(state => state.setAppDetail) + const isWorkflowApp = appDetail?.mode === AppModeEnum.WORKFLOW const showMCPCard = isInPanel - const showTriggerCard = isInPanel && appDetail?.mode === AppModeEnum.WORKFLOW - const { data: currentWorkflow } = useAppWorkflow(appDetail?.mode === AppModeEnum.WORKFLOW ? appDetail.id : '') - const hasTriggerNode = useMemo(() => { - if (appDetail?.mode !== AppModeEnum.WORKFLOW) + const showTriggerCard = isInPanel && isWorkflowApp + const { data: currentWorkflow } = useAppWorkflow(isWorkflowApp ? appDetail.id : '') + const hasTriggerNode = useMemo(() => { + if (!isWorkflowApp) return false - const nodes = currentWorkflow?.graph?.nodes || [] + if (!currentWorkflow) + return null + const nodes = currentWorkflow.graph?.nodes || [] return nodes.some((node) => { const nodeType = node.data?.type as BlockEnum | undefined return !!nodeType && isTriggerNode(nodeType) }) - }, [appDetail?.mode, currentWorkflow]) + }, [isWorkflowApp, currentWorkflow]) + const shouldRenderAppCards = !isWorkflowApp || hasTriggerNode === false const updateAppDetail = async () => { try { @@ -123,7 +127,7 @@ const CardView: FC = ({ appId, isInPanel, className }) => { return (
{ - !hasTriggerNode && ( + shouldRenderAppCards && ( <> type ConversationListItem = ChatConversationGeneralDetail | CompletionConversationGeneralDetail @@ -779,15 +780,17 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) { }
{showMessageLogModal && ( - { - setCurrentLogItem() - setShowMessageLogModal(false) - }} - defaultTab={currentLogModalActiveTab} - /> + + { + setCurrentLogItem() + setShowMessageLogModal(false) + }} + defaultTab={currentLogModalActiveTab} + /> + )} {!isChatMode && showPromptLogModal && ( { + + + ) diff --git a/web/app/components/base/icons/icon-gallery.stories.tsx b/web/app/components/base/icons/icon-gallery.stories.tsx new file mode 100644 index 0000000000..7da71b3b0b --- /dev/null +++ b/web/app/components/base/icons/icon-gallery.stories.tsx @@ -0,0 +1,258 @@ +import type { Meta, StoryObj } from '@storybook/nextjs' +import React from 'react' + +declare const require: any + +type IconComponent = React.ComponentType> + +type IconEntry = { + name: string + category: string + path: string + Component: IconComponent +} + +const iconContext = require.context('./src', true, /\.tsx$/) + +const iconEntries: IconEntry[] = iconContext + .keys() + .filter((key: string) => !key.endsWith('.stories.tsx') && !key.endsWith('.spec.tsx')) + .map((key: string) => { + const mod = iconContext(key) + const Component = mod.default as IconComponent | undefined + if (!Component) + return null + + const relativePath = key.replace(/^\.\//, '') + const path = `app/components/base/icons/src/${relativePath}` + const parts = relativePath.split('/') + const fileName = parts.pop() || '' + const category = parts.length ? parts.join('/') : '(root)' + const name = Component.displayName || fileName.replace(/\.tsx$/, '') + + return { + name, + category, + path, + Component, + } + }) + .filter(Boolean) as IconEntry[] + +const sortedEntries = [...iconEntries].sort((a, b) => { + if (a.category === b.category) + return a.name.localeCompare(b.name) + return a.category.localeCompare(b.category) +}) + +const filterEntries = (entries: IconEntry[], query: string) => { + const normalized = query.trim().toLowerCase() + if (!normalized) + return entries + + return entries.filter(entry => + entry.name.toLowerCase().includes(normalized) + || entry.path.toLowerCase().includes(normalized) + || entry.category.toLowerCase().includes(normalized), + ) +} + +const groupByCategory = (entries: IconEntry[]) => entries.reduce((acc, entry) => { + if (!acc[entry.category]) + acc[entry.category] = [] + + acc[entry.category].push(entry) + return acc +}, {} as Record) + +const containerStyle: React.CSSProperties = { + padding: 24, + display: 'flex', + flexDirection: 'column', + gap: 24, +} + +const headerStyle: React.CSSProperties = { + display: 'flex', + flexDirection: 'column', + gap: 8, +} + +const controlsStyle: React.CSSProperties = { + display: 'flex', + alignItems: 'center', + gap: 12, + flexWrap: 'wrap', +} + +const searchInputStyle: React.CSSProperties = { + padding: '8px 12px', + minWidth: 280, + borderRadius: 6, + border: '1px solid #d0d0d5', +} + +const toggleButtonStyle: React.CSSProperties = { + padding: '8px 12px', + borderRadius: 6, + border: '1px solid #d0d0d5', + background: '#fff', + cursor: 'pointer', +} + +const emptyTextStyle: React.CSSProperties = { color: '#5f5f66' } + +const sectionStyle: React.CSSProperties = { + display: 'flex', + flexDirection: 'column', + gap: 12, +} + +const gridStyle: React.CSSProperties = { + display: 'grid', + gap: 12, + gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))', +} + +const cardStyle: React.CSSProperties = { + border: '1px solid #e1e1e8', + borderRadius: 8, + padding: 12, + display: 'flex', + flexDirection: 'column', + gap: 8, + minHeight: 140, +} + +const previewBaseStyle: React.CSSProperties = { + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + minHeight: 48, + borderRadius: 6, +} + +const nameButtonBaseStyle: React.CSSProperties = { + display: 'inline-flex', + padding: 0, + border: 'none', + background: 'transparent', + font: 'inherit', + cursor: 'pointer', + textAlign: 'left', + fontWeight: 600, +} + +const PREVIEW_SIZE = 40 + +const IconGalleryStory = () => { + const [query, setQuery] = React.useState('') + const [copiedPath, setCopiedPath] = React.useState(null) + const [previewTheme, setPreviewTheme] = React.useState<'light' | 'dark'>('light') + + const filtered = React.useMemo(() => filterEntries(sortedEntries, query), [query]) + + const grouped = React.useMemo(() => groupByCategory(filtered), [filtered]) + + const categoryOrder = React.useMemo( + () => Object.keys(grouped).sort((a, b) => a.localeCompare(b)), + [grouped], + ) + + React.useEffect(() => { + if (!copiedPath) + return undefined + + const timerId = window.setTimeout(() => { + setCopiedPath(null) + }, 1200) + + return () => window.clearTimeout(timerId) + }, [copiedPath]) + + const handleCopy = React.useCallback((text: string) => { + navigator.clipboard?.writeText(text) + .then(() => { + setCopiedPath(text) + }) + .catch((err) => { + console.error('Failed to copy icon path:', err) + }) + }, []) + + return ( +
+
+

Icon Gallery

+

+ Browse all icon components sourced from app/components/base/icons/src. Use the search bar + to filter by name or path. +

+
+ setQuery(event.target.value)} + /> + {filtered.length} icons + +
+
+ {categoryOrder.length === 0 && ( +

No icons match the current filter.

+ )} + {categoryOrder.map(category => ( +
+

{category}

+
+ {grouped[category].map(entry => ( +
+
+ +
+ +
+ ))} +
+
+ ))} +
+ ) +} + +const meta: Meta = { + title: 'Base/Icons/Icon Gallery', + component: IconGalleryStory, + parameters: { + layout: 'fullscreen', + }, +} + +export default meta + +type Story = StoryObj + +export const All: Story = { + render: () => , +} diff --git a/web/app/components/base/message-log-modal/index.stories.tsx b/web/app/components/base/message-log-modal/index.stories.tsx index 3dd4b06a55..4173a85ebc 100644 --- a/web/app/components/base/message-log-modal/index.stories.tsx +++ b/web/app/components/base/message-log-modal/index.stories.tsx @@ -6,6 +6,7 @@ import { useStore } from '@/app/components/app/store' import type { WorkflowRunDetailResponse } from '@/models/log' import type { NodeTracing, NodeTracingListResponse } from '@/types/workflow' import { BlockEnum } from '@/app/components/workflow/types' +import { WorkflowContextProvider } from '@/app/components/workflow/context' const SAMPLE_APP_DETAIL = { id: 'app-demo-1', @@ -143,10 +144,12 @@ const MessageLogPreview = (props: MessageLogModalProps) => { return (
- + + +
) }