fix(web): detail info in snippet evaluation

This commit is contained in:
JzoNg 2026-04-29 13:11:09 +08:00
parent 31e74371ef
commit c56f1a8216
5 changed files with 53 additions and 220 deletions

View File

@ -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 = ({
<SelectContent placement="bottom-start" popupClassName="w-[320px]">
{metricOptionGroups.map(group => (
<SelectGroup key={group.label}>
<SelectGroupLabel className="px-3 pt-2 pb-1 system-xs-medium-uppercase text-text-tertiary">{group.label}</SelectGroupLabel>
<SelectLabel className="px-3 pt-2 pb-1 system-xs-medium-uppercase text-text-tertiary">{group.label}</SelectLabel>
{group.options.map(option => (
<SelectItem
key={option.id}

View File

@ -12,8 +12,8 @@ import {
Select,
SelectContent,
SelectGroup,
SelectGroupLabel,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from '@langgenius/dify-ui/select'
@ -110,7 +110,7 @@ const ConditionMetricSelect = ({
<SelectContent popupClassName="w-[360px]">
{groupedMetricOptions.map(group => (
<SelectGroup key={group.label}>
<SelectGroupLabel className="px-3 pt-2 pb-1 system-xs-medium-uppercase text-text-tertiary">{group.label}</SelectGroupLabel>
<SelectLabel className="px-3 pt-2 pb-1 system-xs-medium-uppercase text-text-tertiary">{group.label}</SelectLabel>
{group.options.map(option => (
<SelectItem key={option.id} value={serializeVariableSelector(option.variableSelector)}>
<div className="flex min-w-0 flex-1 items-center gap-2">

View File

@ -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<typeof import('@/service/use-snippets')>()
@ -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(<SnippetEvaluationPage snippetId="snippet-1" />)
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')
})

View File

@ -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 (
<div className="flex h-full items-center justify-center bg-background-body">
<Loading />
</div>
)
}
if (!snippet)
return null

View File

@ -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',
},
}
}