mirror of https://github.com/langgenius/dify.git
feat(web): add snippets api
This commit is contained in:
parent
4b9a26a5e6
commit
9f28575903
|
|
@ -18,7 +18,7 @@ import { CheckModal } from '@/hooks/use-pay'
|
|||
import dynamic from '@/next/dynamic'
|
||||
import Link from '@/next/link'
|
||||
import { useInfiniteAppList } from '@/service/use-apps'
|
||||
import { getSnippetListMock } from '@/service/use-snippets'
|
||||
import { getSnippetListMock } from '@/service/use-snippets.mock'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import AppCard from './app-card'
|
||||
import { AppCardSkeleton } from './app-card-skeleton'
|
||||
|
|
|
|||
|
|
@ -0,0 +1,342 @@
|
|||
import type {
|
||||
CreateSnippetPayload,
|
||||
IncrementSnippetUseCountResponse,
|
||||
PublishSnippetWorkflowResponse,
|
||||
Snippet,
|
||||
SnippetDraftConfig,
|
||||
SnippetDraftNodeRunPayload,
|
||||
SnippetDraftRunPayload,
|
||||
SnippetDraftSyncPayload,
|
||||
SnippetDraftSyncResponse,
|
||||
SnippetImportPayload,
|
||||
SnippetIterationNodeRunPayload,
|
||||
SnippetListResponse,
|
||||
SnippetLoopNodeRunPayload,
|
||||
SnippetWorkflow,
|
||||
UpdateSnippetPayload,
|
||||
WorkflowNodeExecution,
|
||||
WorkflowNodeExecutionListResponse,
|
||||
WorkflowRunDetail,
|
||||
WorkflowRunPagination,
|
||||
} from '@/types/snippet'
|
||||
import { type } from '@orpc/contract'
|
||||
import { base } from '../base'
|
||||
|
||||
export const listCustomizedSnippetsContract = base
|
||||
.route({
|
||||
path: '/workspaces/current/customized-snippets',
|
||||
method: 'GET',
|
||||
})
|
||||
.input(type<{
|
||||
query: {
|
||||
page: number
|
||||
limit: number
|
||||
keyword?: string
|
||||
is_published?: boolean
|
||||
}
|
||||
}>())
|
||||
.output(type<SnippetListResponse>())
|
||||
|
||||
export const createCustomizedSnippetContract = base
|
||||
.route({
|
||||
path: '/workspaces/current/customized-snippets',
|
||||
method: 'POST',
|
||||
})
|
||||
.input(type<{
|
||||
body: CreateSnippetPayload
|
||||
}>())
|
||||
.output(type<Snippet>())
|
||||
|
||||
export const getCustomizedSnippetContract = base
|
||||
.route({
|
||||
path: '/workspaces/current/customized-snippets/{snippetId}',
|
||||
method: 'GET',
|
||||
})
|
||||
.input(type<{
|
||||
params: {
|
||||
snippetId: string
|
||||
}
|
||||
}>())
|
||||
.output(type<Snippet>())
|
||||
|
||||
export const updateCustomizedSnippetContract = base
|
||||
.route({
|
||||
path: '/workspaces/current/customized-snippets/{snippetId}',
|
||||
method: 'PATCH',
|
||||
})
|
||||
.input(type<{
|
||||
params: {
|
||||
snippetId: string
|
||||
}
|
||||
body: UpdateSnippetPayload
|
||||
}>())
|
||||
.output(type<Snippet>())
|
||||
|
||||
export const deleteCustomizedSnippetContract = base
|
||||
.route({
|
||||
path: '/workspaces/current/customized-snippets/{snippetId}',
|
||||
method: 'DELETE',
|
||||
})
|
||||
.input(type<{
|
||||
params: {
|
||||
snippetId: string
|
||||
}
|
||||
}>())
|
||||
.output(type<unknown>())
|
||||
|
||||
export const exportCustomizedSnippetContract = base
|
||||
.route({
|
||||
path: '/workspaces/current/customized-snippets/{snippetId}/export',
|
||||
method: 'GET',
|
||||
})
|
||||
.input(type<{
|
||||
params: {
|
||||
snippetId: string
|
||||
}
|
||||
query: {
|
||||
include_secret?: 'true' | 'false'
|
||||
}
|
||||
}>())
|
||||
.output(type<string>())
|
||||
|
||||
export const importCustomizedSnippetContract = base
|
||||
.route({
|
||||
path: '/workspaces/current/customized-snippets/imports',
|
||||
method: 'POST',
|
||||
})
|
||||
.input(type<{
|
||||
body: SnippetImportPayload
|
||||
}>())
|
||||
.output(type<unknown>())
|
||||
|
||||
export const confirmSnippetImportContract = base
|
||||
.route({
|
||||
path: '/workspaces/current/customized-snippets/imports/{importId}/confirm',
|
||||
method: 'POST',
|
||||
})
|
||||
.input(type<{
|
||||
params: {
|
||||
importId: string
|
||||
}
|
||||
}>())
|
||||
.output(type<unknown>())
|
||||
|
||||
export const checkSnippetDependenciesContract = base
|
||||
.route({
|
||||
path: '/workspaces/current/customized-snippets/{snippetId}/check-dependencies',
|
||||
method: 'GET',
|
||||
})
|
||||
.input(type<{
|
||||
params: {
|
||||
snippetId: string
|
||||
}
|
||||
}>())
|
||||
.output(type<unknown>())
|
||||
|
||||
export const incrementSnippetUseCountContract = base
|
||||
.route({
|
||||
path: '/workspaces/current/customized-snippets/{snippetId}/use-count/increment',
|
||||
method: 'POST',
|
||||
})
|
||||
.input(type<{
|
||||
params: {
|
||||
snippetId: string
|
||||
}
|
||||
}>())
|
||||
.output(type<IncrementSnippetUseCountResponse>())
|
||||
|
||||
export const getSnippetDraftWorkflowContract = base
|
||||
.route({
|
||||
path: '/snippets/{snippetId}/workflows/draft',
|
||||
method: 'GET',
|
||||
})
|
||||
.input(type<{
|
||||
params: {
|
||||
snippetId: string
|
||||
}
|
||||
}>())
|
||||
.output(type<SnippetWorkflow>())
|
||||
|
||||
export const syncSnippetDraftWorkflowContract = base
|
||||
.route({
|
||||
path: '/snippets/{snippetId}/workflows/draft',
|
||||
method: 'POST',
|
||||
})
|
||||
.input(type<{
|
||||
params: {
|
||||
snippetId: string
|
||||
}
|
||||
body: SnippetDraftSyncPayload
|
||||
}>())
|
||||
.output(type<SnippetDraftSyncResponse>())
|
||||
|
||||
export const getSnippetDraftConfigContract = base
|
||||
.route({
|
||||
path: '/snippets/{snippetId}/workflows/draft/config',
|
||||
method: 'GET',
|
||||
})
|
||||
.input(type<{
|
||||
params: {
|
||||
snippetId: string
|
||||
}
|
||||
}>())
|
||||
.output(type<SnippetDraftConfig>())
|
||||
|
||||
export const getSnippetPublishedWorkflowContract = base
|
||||
.route({
|
||||
path: '/snippets/{snippetId}/workflows/publish',
|
||||
method: 'GET',
|
||||
})
|
||||
.input(type<{
|
||||
params: {
|
||||
snippetId: string
|
||||
}
|
||||
}>())
|
||||
.output(type<SnippetWorkflow>())
|
||||
|
||||
export const publishSnippetWorkflowContract = base
|
||||
.route({
|
||||
path: '/snippets/{snippetId}/workflows/publish',
|
||||
method: 'POST',
|
||||
})
|
||||
.input(type<{
|
||||
params: {
|
||||
snippetId: string
|
||||
}
|
||||
}>())
|
||||
.output(type<PublishSnippetWorkflowResponse>())
|
||||
|
||||
export const getSnippetDefaultBlockConfigsContract = base
|
||||
.route({
|
||||
path: '/snippets/{snippetId}/workflows/default-workflow-block-configs',
|
||||
method: 'GET',
|
||||
})
|
||||
.input(type<{
|
||||
params: {
|
||||
snippetId: string
|
||||
}
|
||||
}>())
|
||||
.output(type<unknown>())
|
||||
|
||||
export const listSnippetWorkflowRunsContract = base
|
||||
.route({
|
||||
path: '/snippets/{snippetId}/workflow-runs',
|
||||
method: 'GET',
|
||||
})
|
||||
.input(type<{
|
||||
params: {
|
||||
snippetId: string
|
||||
}
|
||||
query: {
|
||||
last_id?: string
|
||||
limit?: number
|
||||
}
|
||||
}>())
|
||||
.output(type<WorkflowRunPagination>())
|
||||
|
||||
export const getSnippetWorkflowRunDetailContract = base
|
||||
.route({
|
||||
path: '/snippets/{snippetId}/workflow-runs/{runId}',
|
||||
method: 'GET',
|
||||
})
|
||||
.input(type<{
|
||||
params: {
|
||||
snippetId: string
|
||||
runId: string
|
||||
}
|
||||
}>())
|
||||
.output(type<WorkflowRunDetail>())
|
||||
|
||||
export const listSnippetWorkflowRunNodeExecutionsContract = base
|
||||
.route({
|
||||
path: '/snippets/{snippetId}/workflow-runs/{runId}/node-executions',
|
||||
method: 'GET',
|
||||
})
|
||||
.input(type<{
|
||||
params: {
|
||||
snippetId: string
|
||||
runId: string
|
||||
}
|
||||
}>())
|
||||
.output(type<WorkflowNodeExecutionListResponse>())
|
||||
|
||||
export const runSnippetDraftNodeContract = base
|
||||
.route({
|
||||
path: '/snippets/{snippetId}/workflows/draft/nodes/{nodeId}/run',
|
||||
method: 'POST',
|
||||
})
|
||||
.input(type<{
|
||||
params: {
|
||||
snippetId: string
|
||||
nodeId: string
|
||||
}
|
||||
body: SnippetDraftNodeRunPayload
|
||||
}>())
|
||||
.output(type<WorkflowNodeExecution>())
|
||||
|
||||
export const getSnippetDraftNodeLastRunContract = base
|
||||
.route({
|
||||
path: '/snippets/{snippetId}/workflows/draft/nodes/{nodeId}/last-run',
|
||||
method: 'GET',
|
||||
})
|
||||
.input(type<{
|
||||
params: {
|
||||
snippetId: string
|
||||
nodeId: string
|
||||
}
|
||||
}>())
|
||||
.output(type<WorkflowNodeExecution>())
|
||||
|
||||
export const runSnippetDraftIterationNodeContract = base
|
||||
.route({
|
||||
path: '/snippets/{snippetId}/workflows/draft/iteration/nodes/{nodeId}/run',
|
||||
method: 'POST',
|
||||
})
|
||||
.input(type<{
|
||||
params: {
|
||||
snippetId: string
|
||||
nodeId: string
|
||||
}
|
||||
body: SnippetIterationNodeRunPayload
|
||||
}>())
|
||||
.output(type<unknown>())
|
||||
|
||||
export const runSnippetDraftLoopNodeContract = base
|
||||
.route({
|
||||
path: '/snippets/{snippetId}/workflows/draft/loop/nodes/{nodeId}/run',
|
||||
method: 'POST',
|
||||
})
|
||||
.input(type<{
|
||||
params: {
|
||||
snippetId: string
|
||||
nodeId: string
|
||||
}
|
||||
body: SnippetLoopNodeRunPayload
|
||||
}>())
|
||||
.output(type<unknown>())
|
||||
|
||||
export const runSnippetDraftWorkflowContract = base
|
||||
.route({
|
||||
path: '/snippets/{snippetId}/workflows/draft/run',
|
||||
method: 'POST',
|
||||
})
|
||||
.input(type<{
|
||||
params: {
|
||||
snippetId: string
|
||||
}
|
||||
body: SnippetDraftRunPayload
|
||||
}>())
|
||||
.output(type<unknown>())
|
||||
|
||||
export const stopSnippetWorkflowTaskContract = base
|
||||
.route({
|
||||
path: '/snippets/{snippetId}/workflow-runs/tasks/{taskId}/stop',
|
||||
method: 'POST',
|
||||
})
|
||||
.input(type<{
|
||||
params: {
|
||||
snippetId: string
|
||||
taskId: string
|
||||
}
|
||||
}>())
|
||||
.output(type<unknown>())
|
||||
|
|
@ -15,6 +15,33 @@ import {
|
|||
import { changePreferredProviderTypeContract, modelProvidersModelsContract } from './console/model-providers'
|
||||
import { notificationContract, notificationDismissContract } from './console/notification'
|
||||
import { pluginCheckInstalledContract, pluginLatestVersionsContract } from './console/plugins'
|
||||
import {
|
||||
checkSnippetDependenciesContract,
|
||||
confirmSnippetImportContract,
|
||||
createCustomizedSnippetContract,
|
||||
deleteCustomizedSnippetContract,
|
||||
exportCustomizedSnippetContract,
|
||||
getCustomizedSnippetContract,
|
||||
getSnippetDefaultBlockConfigsContract,
|
||||
getSnippetDraftConfigContract,
|
||||
getSnippetDraftNodeLastRunContract,
|
||||
getSnippetDraftWorkflowContract,
|
||||
getSnippetPublishedWorkflowContract,
|
||||
getSnippetWorkflowRunDetailContract,
|
||||
importCustomizedSnippetContract,
|
||||
incrementSnippetUseCountContract,
|
||||
listCustomizedSnippetsContract,
|
||||
listSnippetWorkflowRunNodeExecutionsContract,
|
||||
listSnippetWorkflowRunsContract,
|
||||
publishSnippetWorkflowContract,
|
||||
runSnippetDraftIterationNodeContract,
|
||||
runSnippetDraftLoopNodeContract,
|
||||
runSnippetDraftNodeContract,
|
||||
runSnippetDraftWorkflowContract,
|
||||
stopSnippetWorkflowTaskContract,
|
||||
syncSnippetDraftWorkflowContract,
|
||||
updateCustomizedSnippetContract,
|
||||
} from './console/snippets'
|
||||
import { systemFeaturesContract } from './console/system'
|
||||
import {
|
||||
triggerOAuthConfigContract,
|
||||
|
|
@ -74,6 +101,33 @@ export const consoleRouterContract = {
|
|||
checkInstalled: pluginCheckInstalledContract,
|
||||
latestVersions: pluginLatestVersionsContract,
|
||||
},
|
||||
snippets: {
|
||||
list: listCustomizedSnippetsContract,
|
||||
create: createCustomizedSnippetContract,
|
||||
detail: getCustomizedSnippetContract,
|
||||
update: updateCustomizedSnippetContract,
|
||||
delete: deleteCustomizedSnippetContract,
|
||||
export: exportCustomizedSnippetContract,
|
||||
import: importCustomizedSnippetContract,
|
||||
confirmImport: confirmSnippetImportContract,
|
||||
checkDependencies: checkSnippetDependenciesContract,
|
||||
incrementUseCount: incrementSnippetUseCountContract,
|
||||
draftWorkflow: getSnippetDraftWorkflowContract,
|
||||
syncDraftWorkflow: syncSnippetDraftWorkflowContract,
|
||||
draftConfig: getSnippetDraftConfigContract,
|
||||
publishedWorkflow: getSnippetPublishedWorkflowContract,
|
||||
publishWorkflow: publishSnippetWorkflowContract,
|
||||
defaultBlockConfigs: getSnippetDefaultBlockConfigsContract,
|
||||
workflowRuns: listSnippetWorkflowRunsContract,
|
||||
workflowRunDetail: getSnippetWorkflowRunDetailContract,
|
||||
workflowRunNodeExecutions: listSnippetWorkflowRunNodeExecutionsContract,
|
||||
runDraftNode: runSnippetDraftNodeContract,
|
||||
lastDraftNodeRun: getSnippetDraftNodeLastRunContract,
|
||||
runDraftIterationNode: runSnippetDraftIterationNodeContract,
|
||||
runDraftLoopNode: runSnippetDraftLoopNodeContract,
|
||||
runDraftWorkflow: runSnippetDraftWorkflowContract,
|
||||
stopWorkflowTask: stopSnippetWorkflowTaskContract,
|
||||
},
|
||||
billing: {
|
||||
invoices: invoicesContract,
|
||||
bindPartnerStack: bindPartnerStackContract,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,215 @@
|
|||
import type { Node } from '@/app/components/workflow/types'
|
||||
import type { SnippetDetailPayload, SnippetInputField, SnippetListItem } from '@/models/snippet'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
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 NAME_SPACE = 'snippets'
|
||||
|
||||
export const getSnippetListMock = (): SnippetListItem[] => ([
|
||||
{
|
||||
id: 'snippet-1',
|
||||
name: 'Tone Rewriter',
|
||||
description: 'Rewrites rough drafts into a concise, professional tone for internal stakeholder updates.',
|
||||
author: 'Evan',
|
||||
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',
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const getSnippetDetailMock = (snippetId: string): SnippetDetailPayload | null => {
|
||||
const snippet = getSnippetListMock().find(item => item.id === snippetId)
|
||||
if (!snippet)
|
||||
return null
|
||||
|
||||
const inputFields = getSnippetInputFieldsMock()
|
||||
|
||||
return {
|
||||
snippet,
|
||||
graph: getSnippetGraphMock(),
|
||||
inputFields,
|
||||
uiMeta: {
|
||||
inputFieldCount: inputFields.length,
|
||||
checklistCount: 2,
|
||||
autoSavedAt: 'Auto-saved · a few seconds ago',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export const useSnippetDetail = (snippetId: string) => {
|
||||
return useQuery({
|
||||
queryKey: [NAME_SPACE, 'detail', snippetId],
|
||||
queryFn: async () => getSnippetDetailMock(snippetId),
|
||||
enabled: !!snippetId,
|
||||
})
|
||||
}
|
||||
|
||||
export const publishSnippet = async (_snippetId: string) => {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
export const runSnippet = async (_snippetId: string) => {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
export const updateSnippetInputFields = async (_snippetId: string, _fields: SnippetInputField[]) => {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
|
@ -1,215 +1,273 @@
|
|||
import type { Node } from '@/app/components/workflow/types'
|
||||
import type { SnippetDetailPayload, SnippetInputField, SnippetListItem } from '@/models/snippet'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
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'
|
||||
import type {
|
||||
SnippetCanvasData,
|
||||
SnippetDetailPayload,
|
||||
SnippetDetail as SnippetDetailUIModel,
|
||||
SnippetInputField as SnippetInputFieldUIModel,
|
||||
SnippetListItem as SnippetListItemUIModel,
|
||||
} from '@/models/snippet'
|
||||
import type {
|
||||
CreateSnippetPayload,
|
||||
Snippet as SnippetContract,
|
||||
SnippetListResponse,
|
||||
SnippetWorkflow,
|
||||
UpdateSnippetPayload,
|
||||
} from '@/types/snippet'
|
||||
import {
|
||||
useMutation,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
} from '@tanstack/react-query'
|
||||
import dayjs from 'dayjs'
|
||||
import { consoleClient, consoleQuery } from '@/service/client'
|
||||
|
||||
const NAME_SPACE = 'snippets'
|
||||
type SnippetListParams = {
|
||||
page?: number
|
||||
limit?: number
|
||||
keyword?: string
|
||||
is_published?: boolean
|
||||
}
|
||||
|
||||
export const getSnippetListMock = (): SnippetListItem[] => ([
|
||||
{
|
||||
id: 'snippet-1',
|
||||
name: 'Tone Rewriter',
|
||||
description: 'Rewrites rough drafts into a concise, professional tone for internal stakeholder updates.',
|
||||
author: 'Evan',
|
||||
updatedAt: 'Updated 2h ago',
|
||||
usage: 'Used 19 times',
|
||||
icon: '🪄',
|
||||
iconBackground: '#E0EAFF',
|
||||
status: 'Draft',
|
||||
},
|
||||
])
|
||||
type SnippetSummary = Pick<SnippetContract, 'id' | 'name' | 'description' | 'use_count' | 'icon_info' | 'updated_at'>
|
||||
|
||||
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 DEFAULT_SNIPPET_LIST_PARAMS = {
|
||||
page: 1,
|
||||
limit: 30,
|
||||
} satisfies Required<Pick<SnippetListParams, 'page' | 'limit'>>
|
||||
|
||||
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',
|
||||
},
|
||||
],
|
||||
})
|
||||
const DEFAULT_GRAPH: SnippetCanvasData = {
|
||||
nodes: [],
|
||||
edges: [],
|
||||
viewport: { x: 0, y: 0, zoom: 1 },
|
||||
}
|
||||
|
||||
const getSnippetDetailMock = (snippetId: string): SnippetDetailPayload | null => {
|
||||
const snippet = getSnippetListMock().find(item => item.id === snippetId)
|
||||
if (!snippet)
|
||||
return null
|
||||
const toMilliseconds = (timestamp?: number) => {
|
||||
if (!timestamp)
|
||||
return undefined
|
||||
|
||||
const inputFields = getSnippetInputFieldsMock()
|
||||
return timestamp > 1_000_000_000_000 ? timestamp : timestamp * 1000
|
||||
}
|
||||
|
||||
const formatTimestamp = (timestamp?: number) => {
|
||||
const milliseconds = toMilliseconds(timestamp)
|
||||
if (!milliseconds)
|
||||
return ''
|
||||
|
||||
return dayjs(milliseconds).format('YYYY-MM-DD HH:mm')
|
||||
}
|
||||
|
||||
const getSnippetIcon = (iconInfo: SnippetContract['icon_info']) => {
|
||||
return typeof iconInfo?.icon === 'string' ? iconInfo.icon : ''
|
||||
}
|
||||
|
||||
const getSnippetIconBackground = (iconInfo: SnippetContract['icon_info']) => {
|
||||
return typeof iconInfo?.icon_background === 'string' ? iconInfo.icon_background : ''
|
||||
}
|
||||
|
||||
const toSnippetListItem = (snippet: SnippetSummary): SnippetListItemUIModel => {
|
||||
return {
|
||||
id: snippet.id,
|
||||
name: snippet.name,
|
||||
description: snippet.description,
|
||||
author: '',
|
||||
updatedAt: formatTimestamp(snippet.updated_at),
|
||||
usage: String(snippet.use_count ?? 0),
|
||||
icon: getSnippetIcon(snippet.icon_info),
|
||||
iconBackground: getSnippetIconBackground(snippet.icon_info),
|
||||
status: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
const toSnippetDetail = (snippet: SnippetContract): SnippetDetailUIModel => {
|
||||
return {
|
||||
...toSnippetListItem(snippet),
|
||||
}
|
||||
}
|
||||
|
||||
const toSnippetCanvasData = (workflow?: SnippetWorkflow): SnippetCanvasData => {
|
||||
const graph = workflow?.graph
|
||||
|
||||
if (!graph || typeof graph !== 'object')
|
||||
return DEFAULT_GRAPH
|
||||
|
||||
const graphRecord = graph as Record<string, unknown>
|
||||
|
||||
return {
|
||||
snippet,
|
||||
graph: getSnippetGraphMock(),
|
||||
nodes: Array.isArray(graphRecord.nodes) ? graphRecord.nodes as SnippetCanvasData['nodes'] : DEFAULT_GRAPH.nodes,
|
||||
edges: Array.isArray(graphRecord.edges) ? graphRecord.edges as SnippetCanvasData['edges'] : DEFAULT_GRAPH.edges,
|
||||
viewport: graphRecord.viewport && typeof graphRecord.viewport === 'object'
|
||||
? graphRecord.viewport as SnippetCanvasData['viewport']
|
||||
: DEFAULT_GRAPH.viewport,
|
||||
}
|
||||
}
|
||||
|
||||
const toSnippetDetailPayload = (snippet: SnippetContract, workflow?: SnippetWorkflow): SnippetDetailPayload => {
|
||||
const inputFields = Array.isArray(snippet.input_fields)
|
||||
? snippet.input_fields as SnippetInputFieldUIModel[]
|
||||
: []
|
||||
|
||||
return {
|
||||
snippet: toSnippetDetail(snippet),
|
||||
graph: toSnippetCanvasData(workflow),
|
||||
inputFields,
|
||||
uiMeta: {
|
||||
inputFieldCount: inputFields.length,
|
||||
checklistCount: 2,
|
||||
autoSavedAt: 'Auto-saved · a few seconds ago',
|
||||
checklistCount: 0,
|
||||
autoSavedAt: formatTimestamp(workflow?.updated_at ?? snippet.updated_at),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export const useSnippetDetail = (snippetId: string) => {
|
||||
return useQuery({
|
||||
queryKey: [NAME_SPACE, 'detail', snippetId],
|
||||
queryFn: async () => getSnippetDetailMock(snippetId),
|
||||
enabled: !!snippetId,
|
||||
const normalizeSnippetListParams = (params: SnippetListParams) => {
|
||||
return {
|
||||
page: params.page ?? DEFAULT_SNIPPET_LIST_PARAMS.page,
|
||||
limit: params.limit ?? DEFAULT_SNIPPET_LIST_PARAMS.limit,
|
||||
...(params.keyword ? { keyword: params.keyword } : {}),
|
||||
...(typeof params.is_published === 'boolean' ? { is_published: params.is_published } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
const isNotFoundError = (error: unknown) => {
|
||||
return !!error && typeof error === 'object' && 'status' in error && error.status === 404
|
||||
}
|
||||
|
||||
export const useSnippetList = (params: SnippetListParams = {}, options?: { enabled?: boolean }) => {
|
||||
const query = normalizeSnippetListParams(params)
|
||||
|
||||
return useQuery(consoleQuery.snippets.list.queryOptions({
|
||||
input: { query },
|
||||
enabled: options?.enabled ?? true,
|
||||
}))
|
||||
}
|
||||
|
||||
export const useSnippetListItems = (params: SnippetListParams = {}, options?: { enabled?: boolean }) => {
|
||||
return useQuery<SnippetListItemUIModel[]>({
|
||||
queryKey: [...consoleQuery.snippets.list.queryKey({ input: { query: normalizeSnippetListParams(params) } }), 'items'],
|
||||
queryFn: async () => {
|
||||
const response = await consoleClient.snippets.list({
|
||||
query: normalizeSnippetListParams(params),
|
||||
})
|
||||
|
||||
return response.data.map(toSnippetListItem)
|
||||
},
|
||||
enabled: options?.enabled ?? true,
|
||||
})
|
||||
}
|
||||
|
||||
export const publishSnippet = async (_snippetId: string) => {
|
||||
return Promise.resolve()
|
||||
export const useSnippetApiDetail = (snippetId: string) => {
|
||||
return useQuery(consoleQuery.snippets.detail.queryOptions({
|
||||
input: {
|
||||
params: { snippetId },
|
||||
},
|
||||
enabled: !!snippetId,
|
||||
}))
|
||||
}
|
||||
|
||||
export const runSnippet = async (_snippetId: string) => {
|
||||
return Promise.resolve()
|
||||
export const useSnippetDetail = (snippetId: string) => {
|
||||
return useQuery<SnippetDetailPayload | null>({
|
||||
queryKey: [...consoleQuery.snippets.detail.queryKey({
|
||||
input: {
|
||||
params: { snippetId },
|
||||
},
|
||||
}), 'payload'],
|
||||
enabled: !!snippetId,
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const [snippet, workflow] = await Promise.all([
|
||||
consoleClient.snippets.detail({
|
||||
params: { snippetId },
|
||||
}),
|
||||
consoleClient.snippets.draftWorkflow({
|
||||
params: { snippetId },
|
||||
}).catch((error) => {
|
||||
if (isNotFoundError(error))
|
||||
return undefined
|
||||
|
||||
throw error
|
||||
}),
|
||||
])
|
||||
|
||||
return toSnippetDetailPayload(snippet, workflow)
|
||||
}
|
||||
catch (error) {
|
||||
if (isNotFoundError(error))
|
||||
return null
|
||||
|
||||
throw error
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const updateSnippetInputFields = async (_snippetId: string, _fields: SnippetInputField[]) => {
|
||||
return Promise.resolve()
|
||||
export const useCreateSnippetMutation = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation(consoleQuery.snippets.create.mutationOptions({
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.snippets.key(),
|
||||
})
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
export const useUpdateSnippetMutation = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
...consoleQuery.snippets.update.mutationOptions({
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.snippets.key(),
|
||||
})
|
||||
},
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
export const useDeleteSnippetMutation = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
...consoleQuery.snippets.delete.mutationOptions({
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.snippets.key(),
|
||||
})
|
||||
},
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
export const usePublishSnippetMutation = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
...consoleQuery.snippets.publishWorkflow.mutationOptions({
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.snippets.key(),
|
||||
})
|
||||
},
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
export const useIncrementSnippetUseCountMutation = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
...consoleQuery.snippets.incrementUseCount.mutationOptions({
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.snippets.key(),
|
||||
})
|
||||
},
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
export type {
|
||||
CreateSnippetPayload,
|
||||
SnippetListResponse,
|
||||
UpdateSnippetPayload,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,146 @@
|
|||
export type SnippetType = 'node' | 'group'
|
||||
|
||||
export type SnippetIconInfo = Record<string, unknown>
|
||||
|
||||
export type SnippetInputField = Record<string, unknown>
|
||||
|
||||
export type Snippet = {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
type: SnippetType
|
||||
is_published: boolean
|
||||
version: string
|
||||
use_count: number
|
||||
icon_info: SnippetIconInfo
|
||||
input_fields: SnippetInputField[]
|
||||
created_at: number
|
||||
updated_at: number
|
||||
}
|
||||
|
||||
export type SnippetListItem = Omit<Snippet, 'version' | 'input_fields'>
|
||||
|
||||
export type SnippetListResponse = {
|
||||
data: SnippetListItem[]
|
||||
page: number
|
||||
limit: number
|
||||
total: number
|
||||
has_more: boolean
|
||||
}
|
||||
|
||||
export type CreateSnippetPayload = {
|
||||
name: string
|
||||
description?: string
|
||||
type?: SnippetType
|
||||
icon_info?: SnippetIconInfo
|
||||
input_fields?: SnippetInputField[]
|
||||
}
|
||||
|
||||
export type UpdateSnippetPayload = {
|
||||
name?: string
|
||||
description?: string
|
||||
icon_info?: SnippetIconInfo
|
||||
}
|
||||
|
||||
export type SnippetImportPayload = {
|
||||
mode?: string
|
||||
yaml_content?: string
|
||||
yaml_url?: string
|
||||
snippet_id?: string
|
||||
name?: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export type IncrementSnippetUseCountResponse = {
|
||||
result: string
|
||||
use_count: number
|
||||
}
|
||||
|
||||
export type SnippetWorkflow = {
|
||||
id: string
|
||||
graph: Record<string, unknown>
|
||||
features: Record<string, unknown>
|
||||
hash: string
|
||||
created_at: number
|
||||
updated_at: number
|
||||
}
|
||||
|
||||
export type SnippetDraftSyncPayload = {
|
||||
graph?: Record<string, unknown>
|
||||
hash?: string
|
||||
environment_variables?: Record<string, unknown>[]
|
||||
conversation_variables?: Record<string, unknown>[]
|
||||
input_variables?: Record<string, unknown>[]
|
||||
}
|
||||
|
||||
export type SnippetDraftSyncResponse = {
|
||||
result: string
|
||||
hash: string
|
||||
updated_at: number
|
||||
}
|
||||
|
||||
export type SnippetDraftConfig = {
|
||||
parallel_depth_limit: number
|
||||
}
|
||||
|
||||
export type PublishSnippetWorkflowResponse = {
|
||||
result: string
|
||||
created_at: number
|
||||
}
|
||||
|
||||
export type WorkflowRunDetail = {
|
||||
id: string
|
||||
version: string
|
||||
status: 'running' | 'succeeded' | 'failed' | 'stopped' | 'partial-succeeded'
|
||||
elapsed_time: number
|
||||
total_tokens: number
|
||||
total_steps: number
|
||||
created_at: number
|
||||
finished_at: number
|
||||
exceptions_count: number
|
||||
}
|
||||
|
||||
export type WorkflowRunPagination = {
|
||||
limit: number
|
||||
has_more: boolean
|
||||
data: WorkflowRunDetail[]
|
||||
}
|
||||
|
||||
export type WorkflowNodeExecution = {
|
||||
id: string
|
||||
index: number
|
||||
node_id: string
|
||||
node_type: string
|
||||
title: string
|
||||
inputs: Record<string, unknown>
|
||||
process_data: Record<string, unknown>
|
||||
outputs: Record<string, unknown>
|
||||
status: string
|
||||
error: string
|
||||
elapsed_time: number
|
||||
created_at: number
|
||||
finished_at: number
|
||||
}
|
||||
|
||||
export type WorkflowNodeExecutionListResponse = {
|
||||
data: WorkflowNodeExecution[]
|
||||
}
|
||||
|
||||
export type SnippetDraftNodeRunPayload = {
|
||||
inputs?: Record<string, unknown>
|
||||
query?: string
|
||||
files?: Record<string, unknown>[]
|
||||
}
|
||||
|
||||
export type SnippetDraftRunPayload = {
|
||||
inputs?: Record<string, unknown>
|
||||
files?: Record<string, unknown>[]
|
||||
}
|
||||
|
||||
export type SnippetIterationNodeRunPayload = {
|
||||
inputs?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export type SnippetLoopNodeRunPayload = {
|
||||
inputs?: Record<string, unknown>
|
||||
}
|
||||
Loading…
Reference in New Issue