mirror of
https://github.com/langgenius/dify.git
synced 2026-05-10 05:56:31 +08:00
fix(web): detail info in snippet evaluation
This commit is contained in:
parent
31e74371ef
commit
c56f1a8216
@ -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}
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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')
|
||||
})
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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',
|
||||
},
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user