From c56f1a82164966b05334a0d79086082df3549852 Mon Sep 17 00:00:00 2001 From: JzoNg Date: Wed, 29 Apr 2026 13:11:09 +0800 Subject: [PATCH] fix(web): detail info in snippet evaluation --- .../add-condition-select.tsx | 4 +- .../conditions-section/condition-group.tsx | 4 +- .../snippet-evaluation-page.spec.tsx | 39 +++- .../snippets/snippet-evaluation-page.tsx | 23 +- web/service/use-snippets.mock.ts | 203 ------------------ 5 files changed, 53 insertions(+), 220 deletions(-) delete mode 100644 web/service/use-snippets.mock.ts diff --git a/web/app/components/evaluation/components/conditions-section/add-condition-select.tsx b/web/app/components/evaluation/components/conditions-section/add-condition-select.tsx index 93116d4dd9..2a17fadf83 100644 --- a/web/app/components/evaluation/components/conditions-section/add-condition-select.tsx +++ b/web/app/components/evaluation/components/conditions-section/add-condition-select.tsx @@ -6,8 +6,8 @@ import { Select, SelectContent, SelectGroup, - SelectGroupLabel, SelectItem, + SelectLabel, SelectTrigger, } from '@langgenius/dify-ui/select' import { useState } from 'react' @@ -46,7 +46,7 @@ const AddConditionSelect = ({ {metricOptionGroups.map(group => ( - {group.label} + {group.label} {group.options.map(option => ( {groupedMetricOptions.map(group => ( - {group.label} + {group.label} {group.options.map(option => (
diff --git a/web/app/components/snippets/__tests__/snippet-evaluation-page.spec.tsx b/web/app/components/snippets/__tests__/snippet-evaluation-page.spec.tsx index 1c30cab2c9..b3962c8792 100644 --- a/web/app/components/snippets/__tests__/snippet-evaluation-page.spec.tsx +++ b/web/app/components/snippets/__tests__/snippet-evaluation-page.spec.tsx @@ -1,10 +1,11 @@ import type { SnippetDetailPayload } from '@/models/snippet' +import type { Snippet } from '@/types/snippet' import { render, screen } from '@testing-library/react' import SnippetEvaluationPage from '../snippet-evaluation-page' const mockUseSnippetApiDetail = vi.fn() -const mockGetSnippetDetailMock = vi.fn() const mockSetAppSidebarExpand = vi.fn() +const mockUseDocumentTitle = vi.fn() vi.mock('@/service/use-snippets', async (importOriginal) => { const actual = await importOriginal() @@ -34,15 +35,15 @@ vi.mock('@/next/navigation', () => ({ }), })) -vi.mock('@/service/use-snippets.mock', () => ({ - getSnippetDetailMock: (snippetId: string) => mockGetSnippetDetailMock(snippetId), -})) - vi.mock('@/hooks/use-breakpoints', () => ({ default: () => 'desktop', MediaType: { mobile: 'mobile', desktop: 'desktop' }, })) +vi.mock('@/hooks/use-document-title', () => ({ + default: (title: string) => mockUseDocumentTitle(title), +})) + vi.mock('@/app/components/app/store', () => ({ useStore: (selector: (state: { setAppSidebarExpand: typeof mockSetAppSidebarExpand }) => unknown) => selector({ setAppSidebarExpand: mockSetAppSidebarExpand, @@ -101,21 +102,39 @@ const mockSnippetDetail: SnippetDetailPayload = { }, } +const mockSnippetApiDetail: Snippet = { + id: mockSnippetDetail.snippet.id, + name: mockSnippetDetail.snippet.name, + description: mockSnippetDetail.snippet.description, + type: 'node', + version: '1', + use_count: 19, + icon_info: { + icon: mockSnippetDetail.snippet.icon, + icon_background: mockSnippetDetail.snippet.iconBackground, + icon_type: 'emoji', + }, + input_fields: [], + created_at: 1711267200, + created_by: 'user-1', + updated_at: 1711267200, + updated_by: 'user-1', + is_published: true, +} + describe('SnippetEvaluationPage', () => { beforeEach(() => { vi.clearAllMocks() mockUseSnippetApiDetail.mockReturnValue({ - data: undefined, + data: mockSnippetApiDetail, isLoading: false, }) - mockGetSnippetDetailMock.mockReturnValue(mockSnippetDetail) }) - it('should render evaluation with mock snippet detail data', () => { + it('should render evaluation with snippet detail data from api', () => { render() - expect(mockGetSnippetDetailMock).toHaveBeenCalledWith('snippet-1') - expect(mockUseSnippetApiDetail).not.toHaveBeenCalled() + expect(mockUseSnippetApiDetail).toHaveBeenCalledWith('snippet-1') expect(screen.getByTestId('app-sidebar')).toBeInTheDocument() expect(screen.getByTestId('evaluation')).toHaveTextContent('snippet-1') }) diff --git a/web/app/components/snippets/snippet-evaluation-page.tsx b/web/app/components/snippets/snippet-evaluation-page.tsx index f6aeeef335..c0585cef25 100644 --- a/web/app/components/snippets/snippet-evaluation-page.tsx +++ b/web/app/components/snippets/snippet-evaluation-page.tsx @@ -1,9 +1,13 @@ 'use client' import { useMemo } from 'react' +import Loading from '@/app/components/base/loading' import SnippetAndEvaluationPlanGuard from '@/app/components/billing/snippet-and-evaluation-plan-guard' import Evaluation from '@/app/components/evaluation' -import { getSnippetDetailMock } from '@/service/use-snippets.mock' +import { + buildSnippetDetailPayload, + useSnippetApiDetail, +} from '@/service/use-snippets' import SnippetLayout from './components/snippet-layout' type SnippetEvaluationPageProps = { @@ -11,8 +15,21 @@ type SnippetEvaluationPageProps = { } const SnippetEvaluationPage = ({ snippetId }: SnippetEvaluationPageProps) => { - const mockSnippet = useMemo(() => getSnippetDetailMock(snippetId)?.snippet, [snippetId]) - const snippet = mockSnippet + const { data, isLoading } = useSnippetApiDetail(snippetId) + const snippet = useMemo(() => { + if (!data) + return undefined + + return buildSnippetDetailPayload(data).snippet + }, [data]) + + if (isLoading) { + return ( +
+ +
+ ) + } if (!snippet) return null diff --git a/web/service/use-snippets.mock.ts b/web/service/use-snippets.mock.ts deleted file mode 100644 index b4fffa4814..0000000000 --- a/web/service/use-snippets.mock.ts +++ /dev/null @@ -1,203 +0,0 @@ -import type { Node } from '@/app/components/workflow/types' -import type { SnippetDetailPayload, SnippetInputField, SnippetListItem } from '@/models/snippet' -import codeDefault from '@/app/components/workflow/nodes/code/default' -import { CodeLanguage } from '@/app/components/workflow/nodes/code/types' -import httpDefault from '@/app/components/workflow/nodes/http/default' -import { Method } from '@/app/components/workflow/nodes/http/types' -import llmDefault from '@/app/components/workflow/nodes/llm/default' -import questionClassifierDefault from '@/app/components/workflow/nodes/question-classifier/default' -import { BlockEnum, PromptRole } from '@/app/components/workflow/types' -import { PipelineInputVarType } from '@/models/pipeline' -import { AppModeEnum } from '@/types/app' - -const getSnippetListMock = (): SnippetListItem[] => ([ - { - id: 'snippet-1', - name: 'Tone Rewriter', - description: 'Rewrites rough drafts into a concise, professional tone for internal stakeholder updates.', - updatedAt: 'Updated 2h ago', - usage: 'Used 19 times', - icon: '🪄', - iconBackground: '#E0EAFF', - status: 'Draft', - }, -]) - -const createSnippetMock = (snippetId: string): SnippetListItem => ({ - id: snippetId, - name: 'Tone Rewriter', - description: 'Rewrites rough drafts into a concise, professional tone for internal stakeholder updates.', - updatedAt: 'Updated 2h ago', - usage: 'Used 19 times', - icon: '🪄', - iconBackground: '#E0EAFF', - status: 'Draft', -}) - -const getSnippetInputFieldsMock = (): SnippetInputField[] => ([ - { - type: PipelineInputVarType.textInput, - label: 'Blog URL', - variable: 'blog_url', - required: true, - placeholder: 'Paste a source article URL', - options: [], - max_length: 256, - }, - { - type: PipelineInputVarType.textInput, - label: 'Target Platforms', - variable: 'platforms', - required: true, - placeholder: 'X, LinkedIn, Instagram', - options: [], - max_length: 128, - }, - { - type: PipelineInputVarType.textInput, - label: 'Tone', - variable: 'tone', - required: false, - placeholder: 'Concise and executive-ready', - options: [], - max_length: 48, - }, - { - type: PipelineInputVarType.textInput, - label: 'Max Length', - variable: 'max_length', - required: false, - placeholder: 'Set an ideal output length', - options: [], - max_length: 48, - }, -]) - -const getSnippetGraphMock = (): SnippetDetailPayload['graph'] => ({ - viewport: { x: 120, y: 30, zoom: 0.9 }, - nodes: [ - { - id: 'question-classifier', - position: { x: 280, y: 208 }, - data: { - ...questionClassifierDefault.defaultValue, - title: 'Question Classifier', - desc: 'After-sales related questions', - type: BlockEnum.QuestionClassifier, - query_variable_selector: ['sys', 'query'], - model: { - provider: 'openai', - name: 'gpt-4o', - mode: AppModeEnum.CHAT, - completion_params: { - temperature: 0.2, - }, - }, - classes: [ - { - id: '1', - name: 'HTTP Request', - }, - { - id: '2', - name: 'LLM', - }, - { - id: '3', - name: 'Code', - }, - ], - } as unknown as Node['data'], - }, - { - id: 'http-request', - position: { x: 670, y: 72 }, - data: { - ...httpDefault.defaultValue, - title: 'HTTP Request', - desc: 'POST https://api.example.com/content/rewrite', - type: BlockEnum.HttpRequest, - method: Method.post, - url: 'https://api.example.com/content/rewrite', - headers: 'Content-Type: application/json', - } as unknown as Node['data'], - }, - { - id: 'llm', - position: { x: 670, y: 248 }, - data: { - ...llmDefault.defaultValue, - title: 'LLM', - desc: 'GPT-4o', - type: BlockEnum.LLM, - model: { - provider: 'openai', - name: 'gpt-4o', - mode: AppModeEnum.CHAT, - completion_params: { - temperature: 0.7, - }, - }, - prompt_template: [{ - role: PromptRole.system, - text: 'Rewrite the content with the requested tone.', - }], - } as unknown as Node['data'], - }, - { - id: 'code', - position: { x: 670, y: 424 }, - data: { - ...codeDefault.defaultValue, - title: 'Code', - desc: 'Python', - type: BlockEnum.Code, - code_language: CodeLanguage.python3, - code: 'def main(text: str) -> dict:\n return {"content": text.strip()}', - } as unknown as Node['data'], - }, - ], - edges: [ - { - id: 'edge-question-http', - source: 'question-classifier', - sourceHandle: '1', - target: 'http-request', - targetHandle: 'target', - }, - { - id: 'edge-question-llm', - source: 'question-classifier', - sourceHandle: '2', - target: 'llm', - targetHandle: 'target', - }, - { - id: 'edge-question-code', - source: 'question-classifier', - sourceHandle: '3', - target: 'code', - targetHandle: 'target', - }, - ], -}) - -export const getSnippetDetailMock = (snippetId: string): SnippetDetailPayload | null => { - if (!snippetId) - return null - - const snippet = getSnippetListMock().find(item => item.id === snippetId) ?? createSnippetMock(snippetId) - - const inputFields = getSnippetInputFieldsMock() - - return { - snippet, - graph: getSnippetGraphMock(), - inputFields, - uiMeta: { - inputFieldCount: inputFields.length, - checklistCount: 2, - autoSavedAt: 'Auto-saved · a few seconds ago', - }, - } -}