feat(web): add snippets api

This commit is contained in:
JzoNg 2026-03-20 15:11:33 +08:00
parent 4b9a26a5e6
commit 9f28575903
6 changed files with 1008 additions and 193 deletions

View File

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

View File

@ -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>())

View File

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

View File

@ -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()
}

View File

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

146
web/types/snippet.ts Normal file
View File

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