Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
acfd34e876 | ||
|
|
036a7cf839 | ||
|
|
86beacc64f | ||
|
|
2c6bd90d6f | ||
|
|
f5aaa8f97e | ||
|
|
a22cc5bc5e | ||
|
|
1fbdf6b465 |
@@ -90,7 +90,7 @@ jobs:
|
||||
uses: actions/setup-node@v6
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
cache-dependency-path: ./web/pnpm-lock.yaml
|
||||
|
||||
|
||||
@@ -16,10 +16,6 @@ jobs:
|
||||
name: unit test for Node.js SDK
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [16, 18, 20, 22]
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: sdks/nodejs-client
|
||||
@@ -29,10 +25,10 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
node-version: 24
|
||||
cache: ''
|
||||
cache-dependency-path: 'pnpm-lock.yaml'
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 'lts/*'
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
cache-dependency-path: ./web/pnpm-lock.yaml
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
cache-dependency-path: ./web/pnpm-lock.yaml
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import TypeAlias
|
||||
from uuid import uuid4
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
|
||||
@@ -20,8 +21,8 @@ class SimpleFeedback(ResponseModel):
|
||||
|
||||
|
||||
class RetrieverResource(ResponseModel):
|
||||
id: str
|
||||
message_id: str
|
||||
id: str = Field(default_factory=lambda: str(uuid4()))
|
||||
message_id: str = Field(default_factory=lambda: str(uuid4()))
|
||||
position: int
|
||||
dataset_id: str | None = None
|
||||
dataset_name: str | None = None
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "dify-api"
|
||||
version = "1.11.2"
|
||||
version = "1.11.4"
|
||||
requires-python = ">=3.11,<3.13"
|
||||
|
||||
dependencies = [
|
||||
|
||||
Generated
+1
-1
@@ -1368,7 +1368,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "dify-api"
|
||||
version = "1.11.2"
|
||||
version = "1.11.4"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "aliyun-log-python-sdk" },
|
||||
|
||||
@@ -21,7 +21,7 @@ services:
|
||||
|
||||
# API service
|
||||
api:
|
||||
image: langgenius/dify-api:1.11.2
|
||||
image: langgenius/dify-api:1.11.4
|
||||
restart: always
|
||||
environment:
|
||||
# Use the shared environment variables.
|
||||
@@ -63,7 +63,7 @@ services:
|
||||
# worker service
|
||||
# The Celery worker for processing all queues (dataset, workflow, mail, etc.)
|
||||
worker:
|
||||
image: langgenius/dify-api:1.11.2
|
||||
image: langgenius/dify-api:1.11.4
|
||||
restart: always
|
||||
environment:
|
||||
# Use the shared environment variables.
|
||||
@@ -102,7 +102,7 @@ services:
|
||||
# worker_beat service
|
||||
# Celery beat for scheduling periodic tasks.
|
||||
worker_beat:
|
||||
image: langgenius/dify-api:1.11.2
|
||||
image: langgenius/dify-api:1.11.4
|
||||
restart: always
|
||||
environment:
|
||||
# Use the shared environment variables.
|
||||
@@ -132,7 +132,7 @@ services:
|
||||
|
||||
# Frontend web application.
|
||||
web:
|
||||
image: langgenius/dify-web:1.11.2
|
||||
image: langgenius/dify-web:1.11.4
|
||||
restart: always
|
||||
environment:
|
||||
CONSOLE_API_URL: ${CONSOLE_API_URL:-}
|
||||
|
||||
@@ -704,7 +704,7 @@ services:
|
||||
|
||||
# API service
|
||||
api:
|
||||
image: langgenius/dify-api:1.11.2
|
||||
image: langgenius/dify-api:1.11.4
|
||||
restart: always
|
||||
environment:
|
||||
# Use the shared environment variables.
|
||||
@@ -746,7 +746,7 @@ services:
|
||||
# worker service
|
||||
# The Celery worker for processing all queues (dataset, workflow, mail, etc.)
|
||||
worker:
|
||||
image: langgenius/dify-api:1.11.2
|
||||
image: langgenius/dify-api:1.11.4
|
||||
restart: always
|
||||
environment:
|
||||
# Use the shared environment variables.
|
||||
@@ -785,7 +785,7 @@ services:
|
||||
# worker_beat service
|
||||
# Celery beat for scheduling periodic tasks.
|
||||
worker_beat:
|
||||
image: langgenius/dify-api:1.11.2
|
||||
image: langgenius/dify-api:1.11.4
|
||||
restart: always
|
||||
environment:
|
||||
# Use the shared environment variables.
|
||||
@@ -815,7 +815,7 @@ services:
|
||||
|
||||
# Frontend web application.
|
||||
web:
|
||||
image: langgenius/dify-web:1.11.2
|
||||
image: langgenius/dify-web:1.11.4
|
||||
restart: always
|
||||
environment:
|
||||
CONSOLE_API_URL: ${CONSOLE_API_URL:-}
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
22.21.1
|
||||
24
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
# base image
|
||||
FROM node:22.21.1-alpine3.23 AS base
|
||||
FROM node:24-alpine AS base
|
||||
LABEL maintainer="takatost@gmail.com"
|
||||
|
||||
# if you located in China, you can use aliyun mirror to speed up
|
||||
|
||||
+2
-2
@@ -8,8 +8,8 @@ This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next
|
||||
|
||||
Before starting the web frontend service, please make sure the following environment is ready.
|
||||
|
||||
- [Node.js](https://nodejs.org) >= v22.11.x
|
||||
- [pnpm](https://pnpm.io) v10.x
|
||||
- [Node.js](https://nodejs.org)
|
||||
- [pnpm](https://pnpm.io)
|
||||
|
||||
> [!TIP]
|
||||
> It is recommended to install and enable Corepack to manage package manager versions automatically:
|
||||
|
||||
@@ -53,6 +53,7 @@ vi.mock('@/context/global-public-context', () => {
|
||||
)
|
||||
return {
|
||||
useGlobalPublicStore,
|
||||
useIsSystemFeaturesPending: () => false,
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -66,7 +66,9 @@ export default function CheckCode() {
|
||||
setIsLoading(true)
|
||||
const ret = await webAppEmailLoginWithCode({ email, code: encryptVerificationCode(code), token })
|
||||
if (ret.result === 'success') {
|
||||
setWebAppAccessToken(ret.data.access_token)
|
||||
if (ret?.data?.access_token) {
|
||||
setWebAppAccessToken(ret.data.access_token)
|
||||
}
|
||||
const { access_token } = await fetchAccessToken({
|
||||
appCode: appCode!,
|
||||
userId: embeddedUserId || undefined,
|
||||
|
||||
@@ -82,7 +82,9 @@ export default function MailAndPasswordAuth({ isEmailSetup }: MailAndPasswordAut
|
||||
body: loginData,
|
||||
})
|
||||
if (res.result === 'success') {
|
||||
setWebAppAccessToken(res.data.access_token)
|
||||
if (res?.data?.access_token) {
|
||||
setWebAppAccessToken(res.data.access_token)
|
||||
}
|
||||
|
||||
const { access_token } = await fetchAccessToken({
|
||||
appCode: appCode!,
|
||||
|
||||
@@ -9,8 +9,8 @@ import {
|
||||
EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION,
|
||||
EDUCATION_VERIFYING_LOCALSTORAGE_ITEM,
|
||||
} from '@/app/education-apply/constants'
|
||||
import { fetchSetupStatus } from '@/service/common'
|
||||
import { sendGAEvent } from '@/utils/gtag'
|
||||
import { fetchSetupStatusWithCache } from '@/utils/setup-status'
|
||||
import { resolvePostLoginRedirect } from '../signin/utils/post-login-redirect'
|
||||
import { trackEvent } from './base/amplitude'
|
||||
|
||||
@@ -33,15 +33,8 @@ export const AppInitializer = ({
|
||||
|
||||
const isSetupFinished = useCallback(async () => {
|
||||
try {
|
||||
if (localStorage.getItem('setup_status') === 'finished')
|
||||
return true
|
||||
const setUpStatus = await fetchSetupStatus()
|
||||
if (setUpStatus.step !== 'finished') {
|
||||
localStorage.removeItem('setup_status')
|
||||
return false
|
||||
}
|
||||
localStorage.setItem('setup_status', 'finished')
|
||||
return true
|
||||
const setUpStatus = await fetchSetupStatusWithCache()
|
||||
return setUpStatus.step === 'finished'
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
|
||||
@@ -125,7 +125,6 @@ const resetAccessControlStore = () => {
|
||||
const resetGlobalStore = () => {
|
||||
useGlobalPublicStore.setState({
|
||||
systemFeatures: defaultSystemFeatures,
|
||||
isGlobalPending: false,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ import BasicContent from './basic-content'
|
||||
import More from './more'
|
||||
import Operation from './operation'
|
||||
import SuggestedQuestions from './suggested-questions'
|
||||
import ToolCalls from './tool-calls'
|
||||
import WorkflowProcessItem from './workflow-process'
|
||||
|
||||
type AnswerProps = {
|
||||
@@ -62,7 +61,6 @@ const Answer: FC<AnswerProps> = ({
|
||||
workflowProcess,
|
||||
allFiles,
|
||||
message_files,
|
||||
toolCalls,
|
||||
} = item
|
||||
const hasAgentThoughts = !!agent_thoughts?.length
|
||||
|
||||
@@ -156,11 +154,6 @@ const Answer: FC<AnswerProps> = ({
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
!!toolCalls?.length && (
|
||||
<ToolCalls toolCalls={toolCalls} />
|
||||
)
|
||||
}
|
||||
{
|
||||
responding && contentIsEmpty && !hasAgentThoughts && (
|
||||
<div className="flex h-5 w-6 items-center justify-center">
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import type { ToolCallItem } from '@/types/workflow'
|
||||
import ToolCallItemComponent from '@/app/components/workflow/run/llm-log/tool-call-item'
|
||||
|
||||
type ToolCallsProps = {
|
||||
toolCalls: ToolCallItem[]
|
||||
}
|
||||
const ToolCalls = ({
|
||||
toolCalls,
|
||||
}: ToolCallsProps) => {
|
||||
return (
|
||||
<div className="my-1 space-y-1">
|
||||
{toolCalls.map((toolCall: ToolCallItem, index: number) => (
|
||||
<ToolCallItemComponent
|
||||
key={index}
|
||||
payload={toolCall}
|
||||
className="bg-background-gradient-bg-fill-chat-bubble-bg-2 shadow-none"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ToolCalls
|
||||
@@ -45,7 +45,7 @@ const WorkflowProcessItem = ({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-xl px-2.5',
|
||||
'-mx-1 rounded-xl px-2.5',
|
||||
collapse ? 'border-l-[0.25px] border-components-panel-border py-[7px]' : 'border-[0.5px] border-components-panel-border-subtle px-1 pb-1 pt-[7px]',
|
||||
running && !collapse && 'bg-background-section-burn',
|
||||
succeeded && !collapse && 'bg-state-success-hover',
|
||||
|
||||
@@ -319,9 +319,6 @@ export const useChat = (
|
||||
return player
|
||||
}
|
||||
|
||||
let toolCallId = ''
|
||||
let thoughtId = ''
|
||||
|
||||
ssePost(
|
||||
url,
|
||||
{
|
||||
@@ -329,19 +326,7 @@ export const useChat = (
|
||||
},
|
||||
{
|
||||
isPublicAPI,
|
||||
onData: (message: string, isFirstMessage: boolean, {
|
||||
conversationId: newConversationId,
|
||||
messageId,
|
||||
taskId,
|
||||
chunk_type,
|
||||
tool_icon,
|
||||
tool_icon_dark,
|
||||
tool_name,
|
||||
tool_arguments,
|
||||
tool_files,
|
||||
tool_error,
|
||||
tool_elapsed_time,
|
||||
}: any) => {
|
||||
onData: (message: string, isFirstMessage: boolean, { conversationId: newConversationId, messageId, taskId }: any) => {
|
||||
if (!isAgentMode) {
|
||||
responseItem.content = responseItem.content + message
|
||||
}
|
||||
@@ -351,57 +336,6 @@ export const useChat = (
|
||||
lastThought.thought = lastThought.thought + message // need immer setAutoFreeze
|
||||
}
|
||||
|
||||
if (chunk_type === 'tool_call') {
|
||||
if (!responseItem.toolCalls)
|
||||
responseItem.toolCalls = []
|
||||
toolCallId = uuidV4()
|
||||
responseItem.toolCalls?.push({
|
||||
id: toolCallId,
|
||||
type: 'tool',
|
||||
toolName: tool_name,
|
||||
toolArguments: tool_arguments,
|
||||
toolIcon: tool_icon,
|
||||
toolIconDark: tool_icon_dark,
|
||||
})
|
||||
}
|
||||
|
||||
if (chunk_type === 'tool_result') {
|
||||
const currentToolCallIndex = responseItem.toolCalls?.findIndex(item => item.id === toolCallId) ?? -1
|
||||
|
||||
if (currentToolCallIndex > -1) {
|
||||
responseItem.toolCalls![currentToolCallIndex].toolError = tool_error
|
||||
responseItem.toolCalls![currentToolCallIndex].toolDuration = tool_elapsed_time
|
||||
responseItem.toolCalls![currentToolCallIndex].toolFiles = tool_files
|
||||
responseItem.toolCalls![currentToolCallIndex].toolOutput = message
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk_type === 'thought_start') {
|
||||
if (!responseItem.toolCalls)
|
||||
responseItem.toolCalls = []
|
||||
thoughtId = uuidV4()
|
||||
responseItem.toolCalls.push({
|
||||
id: thoughtId,
|
||||
type: 'thought',
|
||||
thoughtOutput: '',
|
||||
})
|
||||
}
|
||||
|
||||
if (chunk_type === 'thought') {
|
||||
const currentThoughtIndex = responseItem.toolCalls?.findIndex(item => item.id === thoughtId) ?? -1
|
||||
if (currentThoughtIndex > -1) {
|
||||
responseItem.toolCalls![currentThoughtIndex].thoughtOutput += message
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk_type === 'thought_end') {
|
||||
const currentThoughtIndex = responseItem.toolCalls?.findIndex(item => item.id === thoughtId) ?? -1
|
||||
if (currentThoughtIndex > -1) {
|
||||
responseItem.toolCalls![currentThoughtIndex].thoughtOutput += message
|
||||
responseItem.toolCalls![currentThoughtIndex].thoughtCompleted = true
|
||||
}
|
||||
}
|
||||
|
||||
if (messageId && !hasSetResponseId) {
|
||||
questionItem.id = `question-${messageId}`
|
||||
responseItem.id = messageId
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { FileEntity } from '@/app/components/base/file-uploader/types'
|
||||
import type { TypeWithI18N } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import type { InputVarType } from '@/app/components/workflow/types'
|
||||
import type { Annotation, MessageRating } from '@/models/log'
|
||||
import type { FileResponse, ToolCallItem } from '@/types/workflow'
|
||||
import type { FileResponse } from '@/types/workflow'
|
||||
|
||||
export type MessageMore = {
|
||||
time: string
|
||||
@@ -104,7 +104,6 @@ export type IChatItem = {
|
||||
siblingIndex?: number
|
||||
prevSibling?: string
|
||||
nextSibling?: string
|
||||
toolCalls?: ToolCallItem[]
|
||||
}
|
||||
|
||||
export type Metadata = {
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
<svg width="12" height="14" viewBox="0 0 12 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2 9.49479C0.782372 8.51826 0 7.01768 0 5.33333C0 2.38782 2.38782 0 5.33333 0C8.20841 0 10.5503 2.27504 10.6608 5.12305L11.888 6.96354C12.0843 7.25794 12.0161 7.65424 11.7331 7.86654L10.6667 8.66602V10C10.6667 10.7364 10.0697 11.3333 9.33333 11.3333H8V13.3333H6.66667V10.6667C6.66667 10.2985 6.96514 10 7.33333 10H9.33333V8.33333C9.33333 8.12349 9.43239 7.92603 9.60026 7.80013L10.4284 7.17838L9.44531 5.70312C9.3723 5.59361 9.33333 5.46495 9.33333 5.33333C9.33333 3.1242 7.54248 1.33333 5.33333 1.33333C3.1242 1.33333 1.33333 3.1242 1.33333 5.33333C1.33333 6.69202 2.0103 7.89261 3.04818 8.61654C3.2269 8.74119 3.33329 8.94552 3.33333 9.16341V13.3333H2V9.49479Z" fill="#354052"/>
|
||||
<path d="M6.04367 4.24012L5.6504 3.21778C5.59993 3.08657 5.47393 3 5.33333 3C5.19273 3 5.06673 3.08657 5.01627 3.21778L4.62303 4.24012C4.55531 4.41618 4.41618 4.55531 4.24012 4.62303L3.21778 5.01624C3.08657 5.0667 3 5.19276 3 5.33333C3 5.47393 3.08657 5.59993 3.21778 5.6504L4.24012 6.04367C4.41618 6.11133 4.55531 6.25047 4.62303 6.42653L5.01627 7.44887C5.06673 7.58007 5.19273 7.66667 5.33333 7.66667C5.47393 7.66667 5.59993 7.58007 5.6504 7.44887L6.04367 6.42653C6.11133 6.25047 6.25047 6.11133 6.42653 6.04367L7.44887 5.6504C7.58007 5.59993 7.66667 5.47393 7.66667 5.33333C7.66667 5.19276 7.58007 5.0667 7.44887 5.01624L6.42653 4.62303C6.25047 4.55531 6.11133 4.41618 6.04367 4.24012Z" fill="#354052"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "12",
|
||||
"height": "14",
|
||||
"viewBox": "0 0 12 14",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M2 9.49479C0.782372 8.51826 0 7.01768 0 5.33333C0 2.38782 2.38782 0 5.33333 0C8.20841 0 10.5503 2.27504 10.6608 5.12305L11.888 6.96354C12.0843 7.25794 12.0161 7.65424 11.7331 7.86654L10.6667 8.66602V10C10.6667 10.7364 10.0697 11.3333 9.33333 11.3333H8V13.3333H6.66667V10.6667C6.66667 10.2985 6.96514 10 7.33333 10H9.33333V8.33333C9.33333 8.12349 9.43239 7.92603 9.60026 7.80013L10.4284 7.17838L9.44531 5.70312C9.3723 5.59361 9.33333 5.46495 9.33333 5.33333C9.33333 3.1242 7.54248 1.33333 5.33333 1.33333C3.1242 1.33333 1.33333 3.1242 1.33333 5.33333C1.33333 6.69202 2.0103 7.89261 3.04818 8.61654C3.2269 8.74119 3.33329 8.94552 3.33333 9.16341V13.3333H2V9.49479Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M6.04367 4.24012L5.6504 3.21778C5.59993 3.08657 5.47393 3 5.33333 3C5.19273 3 5.06673 3.08657 5.01627 3.21778L4.62303 4.24012C4.55531 4.41618 4.41618 4.55531 4.24012 4.62303L3.21778 5.01624C3.08657 5.0667 3 5.19276 3 5.33333C3 5.47393 3.08657 5.59993 3.21778 5.6504L4.24012 6.04367C4.41618 6.11133 4.55531 6.25047 4.62303 6.42653L5.01627 7.44887C5.06673 7.58007 5.19273 7.66667 5.33333 7.66667C5.47393 7.66667 5.59993 7.58007 5.6504 7.44887L6.04367 6.42653C6.11133 6.25047 6.25047 6.11133 6.42653 6.04367L7.44887 5.6504C7.58007 5.59993 7.66667 5.47393 7.66667 5.33333C7.66667 5.19276 7.58007 5.0667 7.44887 5.01624L6.42653 4.62303C6.25047 4.55531 6.11133 4.41618 6.04367 4.24012Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "Thinking"
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import type { IconData } from '@/app/components/base/icons/IconBase'
|
||||
import * as React from 'react'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import data from './Thinking.json'
|
||||
|
||||
const Icon = (
|
||||
{
|
||||
ref,
|
||||
...props
|
||||
}: React.SVGProps<SVGSVGElement> & {
|
||||
ref?: React.RefObject<React.RefObject<HTMLOrSVGElement>>
|
||||
},
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />
|
||||
|
||||
Icon.displayName = 'Thinking'
|
||||
|
||||
export default Icon
|
||||
@@ -24,7 +24,6 @@ export { default as ParameterExtractor } from './ParameterExtractor'
|
||||
export { default as QuestionClassifier } from './QuestionClassifier'
|
||||
export { default as Schedule } from './Schedule'
|
||||
export { default as TemplatingTransform } from './TemplatingTransform'
|
||||
export { default as Thinking } from './Thinking'
|
||||
export { default as TriggerAll } from './TriggerAll'
|
||||
export { default as VariableX } from './VariableX'
|
||||
export { default as WebhookLine } from './WebhookLine'
|
||||
|
||||
@@ -362,6 +362,18 @@ describe('PreviewDocumentPicker', () => {
|
||||
expect(screen.getByText('--')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render when value prop is omitted (optional)', () => {
|
||||
const files = createMockDocumentList(2)
|
||||
const onChange = vi.fn()
|
||||
// Do not pass `value` at all to verify optional behavior
|
||||
render(<PreviewDocumentPicker files={files} onChange={onChange} />)
|
||||
|
||||
// Renders placeholder for missing name
|
||||
expect(screen.getByText('--')).toBeInTheDocument()
|
||||
// Portal wrapper renders
|
||||
expect(screen.getByTestId('portal-elem')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle empty files array', () => {
|
||||
renderComponent({ files: [] })
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import DocumentList from './document-list'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
value: DocumentItem
|
||||
value?: DocumentItem
|
||||
files: DocumentItem[]
|
||||
onChange: (value: DocumentItem) => void
|
||||
}
|
||||
@@ -30,7 +30,8 @@ const PreviewDocumentPicker: FC<Props> = ({
|
||||
onChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { name, extension } = value
|
||||
const name = value?.name || ''
|
||||
const extension = value?.extension
|
||||
|
||||
const [open, {
|
||||
set: setOpen,
|
||||
|
||||
+11
-28
@@ -2,7 +2,6 @@ import type { FC } from 'react'
|
||||
import {
|
||||
RiFileTextLine,
|
||||
RiFilmAiLine,
|
||||
RiHammerLine,
|
||||
RiImageCircleAiLine,
|
||||
RiVoiceAiFill,
|
||||
} from '@remixicon/react'
|
||||
@@ -39,33 +38,17 @@ const FeatureIcon: FC<FeatureIconProps> = ({
|
||||
// )
|
||||
// }
|
||||
|
||||
if (feature === ModelFeatureEnum.toolCall) {
|
||||
if (showFeaturesLabel) {
|
||||
return (
|
||||
<ModelBadge className={cn('gap-x-0.5', className)}>
|
||||
<RiHammerLine className="size-3" />
|
||||
<span>{ModelFeatureTextEnum.toolCall}</span>
|
||||
</ModelBadge>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
popupContent={t('modelProvider.featureSupported', { ns: 'common', feature: ModelFeatureTextEnum.toolCall })}
|
||||
>
|
||||
<div className="inline-block cursor-help">
|
||||
<ModelBadge
|
||||
className={cn(
|
||||
'w-[18px] justify-center !px-0',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<RiHammerLine className="size-3" />
|
||||
</ModelBadge>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
// if (feature === ModelFeatureEnum.toolCall) {
|
||||
// return (
|
||||
// <Tooltip
|
||||
// popupContent={t('common.modelProvider.featureSupported', { feature: ModelFeatureTextEnum.toolCall })}
|
||||
// >
|
||||
// <ModelBadge className={`mr-0.5 !px-0 w-[18px] justify-center text-gray-500 ${className}`}>
|
||||
// <MagicWand className='w-3 h-3' />
|
||||
// </ModelBadge>
|
||||
// </Tooltip>
|
||||
// )
|
||||
// }
|
||||
|
||||
// if (feature === ModelFeatureEnum.multiToolCall) {
|
||||
// return (
|
||||
|
||||
+1
-9
@@ -96,14 +96,6 @@ const PopupItem: FC<PopupItemProps> = ({
|
||||
<div className='text-text-tertiary system-xs-regular'>{currentProvider?.description?.[language] || currentProvider?.description?.en_US}</div>
|
||||
)} */}
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{
|
||||
modelItem.features?.includes(ModelFeatureEnum.toolCall) && (
|
||||
<FeatureIcon
|
||||
feature={ModelFeatureEnum.toolCall}
|
||||
showFeaturesLabel
|
||||
/>
|
||||
)
|
||||
}
|
||||
{modelItem.model_type && (
|
||||
<ModelBadge>
|
||||
{modelTypeFormat(modelItem.model_type)}
|
||||
@@ -126,7 +118,7 @@ const PopupItem: FC<PopupItemProps> = ({
|
||||
<div className="pt-2">
|
||||
<div className="system-2xs-medium-uppercase mb-1 text-text-tertiary">{t('model.capabilities', { ns: 'common' })}</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{modelItem.features?.filter(feature => feature !== ModelFeatureEnum.toolCall).map(feature => (
|
||||
{modelItem.features?.map(feature => (
|
||||
<FeatureIcon
|
||||
key={feature}
|
||||
feature={feature}
|
||||
|
||||
@@ -150,10 +150,6 @@ export const LLM_OUTPUT_STRUCT: Var[] = [
|
||||
variable: 'usage',
|
||||
type: VarType.object,
|
||||
},
|
||||
{
|
||||
variable: 'generation',
|
||||
type: VarType.object,
|
||||
},
|
||||
]
|
||||
|
||||
export const KNOWLEDGE_RETRIEVAL_OUTPUT_STRUCT: Var[] = [
|
||||
|
||||
@@ -6,20 +6,17 @@ export type BoxProps = {
|
||||
className?: string
|
||||
children?: ReactNode
|
||||
withBorderBottom?: boolean
|
||||
withBorderTop?: boolean
|
||||
}
|
||||
export const Box = memo(({
|
||||
className,
|
||||
children,
|
||||
withBorderBottom,
|
||||
withBorderTop,
|
||||
}: BoxProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'py-2',
|
||||
withBorderBottom && 'border-b border-divider-subtle',
|
||||
withBorderTop && 'border-t border-divider-subtle',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -9,7 +9,6 @@ import { cn } from '@/utils/classnames'
|
||||
|
||||
export type FieldTitleProps = {
|
||||
title?: string
|
||||
className?: string
|
||||
operation?: ReactNode
|
||||
subTitle?: string | ReactNode
|
||||
tooltip?: string
|
||||
@@ -20,7 +19,6 @@ export type FieldTitleProps = {
|
||||
}
|
||||
export const FieldTitle = memo(({
|
||||
title,
|
||||
className,
|
||||
operation,
|
||||
subTitle,
|
||||
tooltip,
|
||||
@@ -33,7 +31,7 @@ export const FieldTitle = memo(({
|
||||
const collapsedMerged = collapsed !== undefined ? collapsed : collapsedLocal
|
||||
|
||||
return (
|
||||
<div className={cn('mb-0.5', !!subTitle && 'mb-1', className)}>
|
||||
<div className={cn('mb-0.5', !!subTitle && 'mb-1')}>
|
||||
<div
|
||||
className="group/collapse flex items-center justify-between py-1"
|
||||
onClick={() => {
|
||||
|
||||
@@ -6,20 +6,17 @@ export type GroupProps = {
|
||||
className?: string
|
||||
children?: ReactNode
|
||||
withBorderBottom?: boolean
|
||||
withBorderTop?: boolean
|
||||
}
|
||||
export const Group = memo(({
|
||||
className,
|
||||
children,
|
||||
withBorderBottom,
|
||||
withBorderTop,
|
||||
}: GroupProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'px-4 py-2',
|
||||
withBorderBottom && 'border-b border-divider-subtle',
|
||||
withBorderTop && 'border-t border-divider-subtle',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import type { CommonNodeType } from '@/app/components/workflow/types'
|
||||
import { useCallback } from 'react'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import { useNodeDataUpdate } from '@/app/components/workflow/hooks'
|
||||
|
||||
const useNodeCrud = <T>(id: string, data: CommonNodeType<T>) => {
|
||||
@@ -20,27 +18,3 @@ const useNodeCrud = <T>(id: string, data: CommonNodeType<T>) => {
|
||||
}
|
||||
|
||||
export default useNodeCrud
|
||||
|
||||
export const useNodeCurdKit = <T>(id: string) => {
|
||||
const store = useStoreApi()
|
||||
const { handleNodeDataUpdateWithSyncDraft } = useNodeDataUpdate()
|
||||
|
||||
const getNodeData = useCallback(() => {
|
||||
const { getNodes } = store.getState()
|
||||
const nodes = getNodes()
|
||||
|
||||
return nodes.find(node => node.id === id)
|
||||
}, [store, id])
|
||||
|
||||
const handleNodeDataUpdate = useCallback((data: Partial<CommonNodeType<T>>) => {
|
||||
handleNodeDataUpdateWithSyncDraft({
|
||||
id,
|
||||
data,
|
||||
})
|
||||
}, [id, handleNodeDataUpdateWithSyncDraft])
|
||||
|
||||
return {
|
||||
getNodeData,
|
||||
handleNodeDataUpdate,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import type { ToolValue } from '@/app/components/workflow/block-selector/types'
|
||||
import { memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import MultipleToolSelector from '@/app/components/plugins/plugin-detail-panel/multiple-tool-selector'
|
||||
import { BoxGroup } from '@/app/components/workflow/nodes/_base/components/layout'
|
||||
import MaxIterations from './max-iterations'
|
||||
import { useNodeTools } from './use-node-tools'
|
||||
|
||||
type ToolsProps = {
|
||||
nodeId: string
|
||||
tools?: ToolValue[]
|
||||
maxIterations?: number
|
||||
}
|
||||
const Tools = ({
|
||||
nodeId,
|
||||
tools = [],
|
||||
maxIterations = 10,
|
||||
}: ToolsProps) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
handleToolsChange,
|
||||
handleMaxIterationsChange,
|
||||
} = useNodeTools(nodeId)
|
||||
|
||||
return (
|
||||
<BoxGroup
|
||||
boxProps={{
|
||||
withBorderBottom: true,
|
||||
withBorderTop: true,
|
||||
}}
|
||||
groupProps={{
|
||||
className: 'px-0',
|
||||
}}
|
||||
>
|
||||
<MultipleToolSelector
|
||||
nodeId={nodeId}
|
||||
nodeOutputVars={[]}
|
||||
availableNodes={[]}
|
||||
value={tools}
|
||||
label={t(`nodes.llm.tools.title`, { ns: 'workflow' })}
|
||||
tooltip={t(`nodes.llm.tools.title`, { ns: 'workflow' })}
|
||||
onChange={handleToolsChange}
|
||||
supportCollapse
|
||||
/>
|
||||
<MaxIterations
|
||||
value={maxIterations}
|
||||
onChange={handleMaxIterationsChange}
|
||||
/>
|
||||
</BoxGroup>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(Tools)
|
||||
@@ -1,40 +0,0 @@
|
||||
import { memo } from 'react'
|
||||
import { InputNumber } from '@/app/components/base/input-number'
|
||||
import Slider from '@/app/components/base/slider'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
|
||||
type MaxIterationsProps = {
|
||||
value?: number
|
||||
onChange?: (value: number) => void
|
||||
}
|
||||
const MaxIterations = ({ value = 10, onChange }: MaxIterationsProps) => {
|
||||
return (
|
||||
<div className="mt-3 flex h-10 items-center">
|
||||
<div className="system-sm-semibold mr-0.5 truncate uppercase text-text-secondary">Max Iterations</div>
|
||||
<Tooltip
|
||||
popupContent="Max Iterations is the maximum number of iterations to run the tool."
|
||||
triggerClassName="shrink-0 w-4 h-4"
|
||||
/>
|
||||
<div className="mr-3 flex grow items-center justify-end">
|
||||
<Slider
|
||||
className="w-[124px]"
|
||||
value={value}
|
||||
onChange={onChange ?? (() => {})}
|
||||
min={1}
|
||||
max={99}
|
||||
step={1}
|
||||
/>
|
||||
</div>
|
||||
<InputNumber
|
||||
className="w-10 shrink-0"
|
||||
value={value}
|
||||
onChange={onChange ?? (() => {})}
|
||||
min={1}
|
||||
max={99}
|
||||
step={1}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(MaxIterations)
|
||||
@@ -1,23 +0,0 @@
|
||||
import type { LLMNodeType } from '../../types'
|
||||
import type { ToolValue } from '@/app/components/workflow/block-selector/types'
|
||||
import { useNodeCurdKit } from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
|
||||
|
||||
export const useNodeTools = (nodeId: string) => {
|
||||
const { handleNodeDataUpdate } = useNodeCurdKit<LLMNodeType>(nodeId)
|
||||
|
||||
const handleToolsChange = (tools: ToolValue[]) => {
|
||||
handleNodeDataUpdate({
|
||||
tools,
|
||||
})
|
||||
}
|
||||
const handleMaxIterationsChange = (maxIterations: number) => {
|
||||
handleNodeDataUpdate({
|
||||
max_iterations: maxIterations,
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
handleToolsChange,
|
||||
handleMaxIterationsChange,
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,6 @@ import VarReferencePicker from '../_base/components/variable/var-reference-picke
|
||||
import ConfigPrompt from './components/config-prompt'
|
||||
import ReasoningFormatConfig from './components/reasoning-format-config'
|
||||
import StructureOutput from './components/structure-output'
|
||||
import Tools from './components/tools'
|
||||
import useConfig from './use-config'
|
||||
|
||||
const i18nPrefix = 'nodes.llm'
|
||||
@@ -234,12 +233,6 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
|
||||
</>
|
||||
)}
|
||||
|
||||
<Tools
|
||||
nodeId={id}
|
||||
tools={inputs.tools}
|
||||
maxIterations={inputs.max_iterations}
|
||||
/>
|
||||
|
||||
{/* Vision: GPT4-vision and so on */}
|
||||
<ConfigVision
|
||||
nodeId={id}
|
||||
@@ -315,11 +308,6 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
|
||||
type="object"
|
||||
description={t(`${i18nPrefix}.outputVars.usage`, { ns: 'workflow' })}
|
||||
/>
|
||||
<VarItem
|
||||
name="generation"
|
||||
type="object"
|
||||
description={t(`${i18nPrefix}.outputVars.generation`, { ns: 'workflow' })}
|
||||
/>
|
||||
{inputs.structured_output_enabled && (
|
||||
<>
|
||||
<Split className="mt-3" />
|
||||
|
||||
@@ -1,18 +1,5 @@
|
||||
import type { ToolValue } from '@/app/components/workflow/block-selector/types'
|
||||
import type { CommonNodeType, Memory, ModelConfig, PromptItem, ValueSelector, Variable, VisionSetting } from '@/app/components/workflow/types'
|
||||
|
||||
export type Tool = {
|
||||
enabled: boolean
|
||||
type: string
|
||||
provider_name: 'plugin' | 'builtin' | 'api' | 'workflow' | 'app' | 'dataset-retrieval'
|
||||
tool_name: string
|
||||
plugin_unique_identifier?: string
|
||||
credential_id?: string
|
||||
parameters?: Record<string, any>
|
||||
settings?: Record<string, any>
|
||||
extra?: Record<string, any>
|
||||
}
|
||||
|
||||
export type LLMNodeType = CommonNodeType & {
|
||||
model: ModelConfig
|
||||
prompt_template: PromptItem[] | PromptItem
|
||||
@@ -31,8 +18,6 @@ export type LLMNodeType = CommonNodeType & {
|
||||
structured_output_enabled?: boolean
|
||||
structured_output?: StructuredOutput
|
||||
reasoning_format?: 'tagged' | 'separated'
|
||||
tools?: ToolValue[]
|
||||
max_iterations?: number
|
||||
}
|
||||
|
||||
export enum Type {
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { v4 as uuidV4 } from 'uuid'
|
||||
import {
|
||||
getProcessedInputs,
|
||||
processOpeningStatement,
|
||||
@@ -267,78 +266,13 @@ export const useChat = (
|
||||
}
|
||||
|
||||
let hasSetResponseId = false
|
||||
let toolCallId = ''
|
||||
let thoughtId = ''
|
||||
|
||||
handleRun(
|
||||
bodyParams,
|
||||
{
|
||||
onData: (message: string, isFirstMessage: boolean, {
|
||||
conversationId: newConversationId,
|
||||
messageId,
|
||||
taskId,
|
||||
chunk_type,
|
||||
tool_icon,
|
||||
tool_icon_dark,
|
||||
tool_name,
|
||||
tool_arguments,
|
||||
tool_files,
|
||||
tool_error,
|
||||
tool_elapsed_time,
|
||||
}: any) => {
|
||||
onData: (message: string, isFirstMessage: boolean, { conversationId: newConversationId, messageId, taskId }: any) => {
|
||||
responseItem.content = responseItem.content + message
|
||||
|
||||
if (chunk_type === 'tool_call') {
|
||||
if (!responseItem.toolCalls)
|
||||
responseItem.toolCalls = []
|
||||
toolCallId = uuidV4()
|
||||
responseItem.toolCalls?.push({
|
||||
id: toolCallId,
|
||||
type: 'tool',
|
||||
toolName: tool_name,
|
||||
toolArguments: tool_arguments,
|
||||
toolIcon: tool_icon,
|
||||
toolIconDark: tool_icon_dark,
|
||||
})
|
||||
}
|
||||
|
||||
if (chunk_type === 'tool_result') {
|
||||
const currentToolCallIndex = responseItem.toolCalls?.findIndex(item => item.id === toolCallId) ?? -1
|
||||
|
||||
if (currentToolCallIndex > -1) {
|
||||
responseItem.toolCalls![currentToolCallIndex].toolError = tool_error
|
||||
responseItem.toolCalls![currentToolCallIndex].toolDuration = tool_elapsed_time
|
||||
responseItem.toolCalls![currentToolCallIndex].toolFiles = tool_files
|
||||
responseItem.toolCalls![currentToolCallIndex].toolOutput = message
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk_type === 'thought_start') {
|
||||
if (!responseItem.toolCalls)
|
||||
responseItem.toolCalls = []
|
||||
thoughtId = uuidV4()
|
||||
responseItem.toolCalls.push({
|
||||
id: thoughtId,
|
||||
type: 'thought',
|
||||
thoughtOutput: '',
|
||||
})
|
||||
}
|
||||
|
||||
if (chunk_type === 'thought') {
|
||||
const currentThoughtIndex = responseItem.toolCalls?.findIndex(item => item.id === thoughtId) ?? -1
|
||||
if (currentThoughtIndex > -1) {
|
||||
responseItem.toolCalls![currentThoughtIndex].thoughtOutput += message
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk_type === 'thought_end') {
|
||||
const currentThoughtIndex = responseItem.toolCalls?.findIndex(item => item.id === thoughtId) ?? -1
|
||||
if (currentThoughtIndex > -1) {
|
||||
responseItem.toolCalls![currentThoughtIndex].thoughtOutput += message
|
||||
responseItem.toolCalls![currentThoughtIndex].thoughtCompleted = true
|
||||
}
|
||||
}
|
||||
|
||||
if (messageId && !hasSetResponseId) {
|
||||
questionItem.id = `question-${messageId}`
|
||||
responseItem.id = messageId
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type {
|
||||
AgentLogItemWithChildren,
|
||||
IterationDurationMap,
|
||||
LLMTraceItem,
|
||||
LoopDurationMap,
|
||||
LoopVariableMap,
|
||||
NodeTracing,
|
||||
@@ -80,18 +79,8 @@ export const useLogs = () => {
|
||||
}
|
||||
}, [setAgentOrToolLogItemStack, setAgentOrToolLogListMap])
|
||||
|
||||
const [showLLMDetail, {
|
||||
setTrue: setShowLLMDetailTrue,
|
||||
setFalse: setShowLLMDetailFalse,
|
||||
}] = useBoolean(false)
|
||||
const [llmResultList, setLLMResultList] = useState<LLMTraceItem[]>([])
|
||||
const handleShowLLMDetail = useCallback((detail: LLMTraceItem[]) => {
|
||||
setShowLLMDetailTrue()
|
||||
setLLMResultList(detail)
|
||||
}, [setShowLLMDetailTrue, setLLMResultList])
|
||||
|
||||
return {
|
||||
showSpecialResultPanel: showRetryDetail || showIteratingDetail || showLoopingDetail || !!agentOrToolLogItemStack.length || showLLMDetail,
|
||||
showSpecialResultPanel: showRetryDetail || showIteratingDetail || showLoopingDetail || !!agentOrToolLogItemStack.length,
|
||||
showRetryDetail,
|
||||
setShowRetryDetailTrue,
|
||||
setShowRetryDetailFalse,
|
||||
@@ -122,12 +111,5 @@ export const useLogs = () => {
|
||||
agentOrToolLogItemStack,
|
||||
agentOrToolLogListMap,
|
||||
handleShowAgentOrToolLog,
|
||||
|
||||
showLLMDetail,
|
||||
setShowLLMDetailTrue,
|
||||
setShowLLMDetailFalse,
|
||||
llmResultList,
|
||||
setLLMResultList,
|
||||
handleShowLLMDetail,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ const RunPanel: FC<RunProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
{/* panel detail */}
|
||||
<div ref={ref} className={cn('relative h-0 grow overflow-y-auto rounded-b-xl bg-background-section')}>
|
||||
<div ref={ref} className={cn('relative h-0 grow overflow-y-auto rounded-b-xl bg-components-panel-bg')}>
|
||||
{loading && (
|
||||
<div className="flex h-full items-center justify-center bg-components-panel-bg">
|
||||
<Loading />
|
||||
@@ -192,7 +192,7 @@ const RunPanel: FC<RunProps> = ({
|
||||
)}
|
||||
{!loading && currentTab === 'TRACING' && (
|
||||
<TracingPanel
|
||||
className="bg-background-section"
|
||||
className="bg-background-section-burn"
|
||||
list={list}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
export { default as LLMLogTrigger } from './llm-log-trigger'
|
||||
export { default as LLMResultPanel } from './llm-result-panel'
|
||||
@@ -1,41 +0,0 @@
|
||||
import type { LLMTraceItem, NodeTracing } from '@/types/workflow'
|
||||
import {
|
||||
RiArrowRightSLine,
|
||||
} from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { Thinking } from '@/app/components/base/icons/src/vender/workflow'
|
||||
|
||||
type LLMLogTriggerProps = {
|
||||
nodeInfo: NodeTracing
|
||||
onShowLLMDetail: (detail: LLMTraceItem[]) => void
|
||||
}
|
||||
const LLMLogTrigger = ({
|
||||
nodeInfo,
|
||||
onShowLLMDetail,
|
||||
}: LLMLogTriggerProps) => {
|
||||
const { t } = useTranslation()
|
||||
const llmTrace = nodeInfo?.execution_metadata?.llm_trace || []
|
||||
|
||||
const handleShowLLMDetail = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation()
|
||||
e.nativeEvent.stopImmediatePropagation()
|
||||
onShowLLMDetail(llmTrace || [])
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
className="mb-1 flex w-full items-center justify-between"
|
||||
variant="tertiary"
|
||||
onClick={handleShowLLMDetail}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<Thinking className="mr-[5px] h-4 w-4 shrink-0 text-components-button-tertiary-text" />
|
||||
{t('detail', { ns: 'runLog' })}
|
||||
</div>
|
||||
<RiArrowRightSLine className="h-4 w-4 shrink-0 text-components-button-tertiary-text" />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export default LLMLogTrigger
|
||||
@@ -1,73 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import type { FC } from 'react'
|
||||
import type {
|
||||
LLMTraceItem,
|
||||
ToolCallItem,
|
||||
} from '@/types/workflow'
|
||||
import {
|
||||
RiArrowLeftLine,
|
||||
} from '@remixicon/react'
|
||||
import { memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ToolCallItemComponent from '@/app/components/workflow/run/llm-log/tool-call-item'
|
||||
|
||||
type Props = {
|
||||
list: LLMTraceItem[]
|
||||
onBack: () => void
|
||||
}
|
||||
|
||||
const LLMResultPanel: FC<Props> = ({
|
||||
list,
|
||||
onBack,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const formattedList = list.map((item) => {
|
||||
if (item.type === 'tool') {
|
||||
return {
|
||||
type: 'tool',
|
||||
toolName: item.name,
|
||||
toolProvider: item.provider,
|
||||
toolIcon: item.icon,
|
||||
toolIconDark: item.icon_dark,
|
||||
toolArguments: item.output.arguments,
|
||||
toolOutput: item.output.output,
|
||||
toolDuration: item.duration,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'model',
|
||||
modelName: item.name,
|
||||
modelProvider: item.provider,
|
||||
modelIcon: item.icon,
|
||||
modelIconDark: item.icon_dark,
|
||||
modelOutput: item.output,
|
||||
modelDuration: item.duration,
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className="system-sm-medium flex h-8 cursor-pointer items-center bg-components-panel-bg px-4 text-text-accent-secondary"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
e.nativeEvent.stopImmediatePropagation()
|
||||
onBack()
|
||||
}}
|
||||
>
|
||||
<RiArrowLeftLine className="mr-1 h-4 w-4" />
|
||||
{t('singleRun.back', { ns: 'workflow' })}
|
||||
</div>
|
||||
<div className="space-y-1 p-2">
|
||||
{
|
||||
formattedList.map((item, index) => (
|
||||
<ToolCallItemComponent key={index} payload={item as ToolCallItem} />
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default memo(LLMResultPanel)
|
||||
@@ -1,152 +0,0 @@
|
||||
import type { ToolCallItem } from '@/types/workflow'
|
||||
import {
|
||||
RiArrowDownSLine,
|
||||
} from '@remixicon/react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import { Thinking } from '@/app/components/base/icons/src/vender/workflow'
|
||||
import BlockIcon from '@/app/components/workflow/block-icon'
|
||||
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
|
||||
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
type ToolCallItemComponentProps = {
|
||||
className?: string
|
||||
payload: ToolCallItem
|
||||
}
|
||||
const ToolCallItemComponent = ({
|
||||
className,
|
||||
payload,
|
||||
}: ToolCallItemComponentProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [expand, setExpand] = useState(false)
|
||||
return (
|
||||
<div
|
||||
className={cn('rounded-[10px] border-[0.5px] border-components-panel-border bg-background-default-subtle px-2 pb-1 pt-2 shadow-xs', className)}
|
||||
>
|
||||
<div
|
||||
className="mb-1 flex cursor-pointer items-center hover:bg-background-gradient-bg-fill-chat-bubble-bg-2"
|
||||
onClick={() => {
|
||||
setExpand(!expand)
|
||||
}}
|
||||
>
|
||||
{
|
||||
payload.type === 'thought' && (
|
||||
<Thinking className="mr-1 h-4 w-4 shrink-0" />
|
||||
)
|
||||
}
|
||||
{
|
||||
payload.type === 'tool' && (
|
||||
<BlockIcon
|
||||
type={BlockEnum.Tool}
|
||||
toolIcon={payload.toolIcon}
|
||||
className="mr-1 h-4 w-4 shrink-0"
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
payload.type === 'model' && (
|
||||
<AppIcon
|
||||
iconType={typeof payload.modelIcon === 'string' ? 'image' : undefined}
|
||||
imageUrl={typeof payload.modelIcon === 'string' ? payload.modelIcon : undefined}
|
||||
background={typeof payload.modelIcon === 'object' ? payload.modelIcon.background : undefined}
|
||||
className="mr-1 h-4 w-4 shrink-0"
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
payload.type === 'thought' && (
|
||||
<div className="system-xs-medium mr-1 grow truncate text-text-secondary" title={payload.thoughtOutput}>
|
||||
{
|
||||
payload.thoughtCompleted && !expand && (payload.thoughtOutput || '') as string
|
||||
}
|
||||
{
|
||||
payload.thoughtCompleted && expand && 'THOUGHT'
|
||||
}
|
||||
{
|
||||
!payload.thoughtCompleted && 'THINKING...'
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
payload.type === 'tool' && (
|
||||
<div className="system-xs-medium mr-1 grow truncate text-text-secondary" title={payload.toolName}>{payload.toolName}</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
payload.type === 'model' && (
|
||||
<div className="system-xs-medium mr-1 grow truncate text-text-secondary" title={payload.modelName}>{payload.modelName}</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
!!payload.toolDuration && (
|
||||
<div className="system-xs-regular mr-1 shrink-0 text-text-tertiary">
|
||||
{payload.toolDuration?.toFixed(1)}
|
||||
s
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
!!payload.modelDuration && (
|
||||
<div className="system-xs-regular mr-1 shrink-0 text-text-tertiary">
|
||||
{payload.modelDuration?.toFixed(1)}
|
||||
s
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<RiArrowDownSLine className="h-4 w-4 shrink-0" />
|
||||
</div>
|
||||
{
|
||||
expand && (
|
||||
<div className="relative px-2 pl-9">
|
||||
<div className="absolute bottom-1 left-2 top-1 w-[1px] bg-divider-regular"></div>
|
||||
{
|
||||
payload.type === 'thought' && typeof payload.thoughtOutput === 'string' && (
|
||||
<div className="body-sm-medium text-text-tertiary">{payload.thoughtOutput}</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
payload.type === 'model' && (
|
||||
<CodeEditor
|
||||
readOnly
|
||||
title={<div>{t('common.data', { ns: 'workflow' })}</div>}
|
||||
language={CodeLanguage.json}
|
||||
value={payload.modelOutput}
|
||||
isJSONStringifyBeauty
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
payload.type === 'tool' && (
|
||||
<CodeEditor
|
||||
readOnly
|
||||
title={<div>{t('common.input', { ns: 'workflow' })}</div>}
|
||||
language={CodeLanguage.json}
|
||||
value={payload.toolArguments}
|
||||
isJSONStringifyBeauty
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
payload.type === 'tool' && (
|
||||
<CodeEditor
|
||||
readOnly
|
||||
className="mt-1"
|
||||
title={<div>{t('common.output', { ns: 'workflow' })}</div>}
|
||||
language={CodeLanguage.json}
|
||||
value={payload.toolOutput}
|
||||
isJSONStringifyBeauty
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ToolCallItemComponent
|
||||
@@ -3,7 +3,6 @@ import type { FC } from 'react'
|
||||
import type {
|
||||
AgentLogItemWithChildren,
|
||||
IterationDurationMap,
|
||||
LLMTraceItem,
|
||||
LoopDurationMap,
|
||||
LoopVariableMap,
|
||||
NodeTracing,
|
||||
@@ -30,7 +29,6 @@ import { BlockEnum } from '../types'
|
||||
import LargeDataAlert from '../variable-inspect/large-data-alert'
|
||||
import { AgentLogTrigger } from './agent-log'
|
||||
import { IterationLogTrigger } from './iteration-log'
|
||||
import { LLMLogTrigger } from './llm-log'
|
||||
import { LoopLogTrigger } from './loop-log'
|
||||
import { RetryLogTrigger } from './retry-log'
|
||||
|
||||
@@ -45,7 +43,6 @@ type Props = {
|
||||
onShowLoopDetail?: (detail: NodeTracing[][], loopDurationMap: LoopDurationMap, loopVariableMap: LoopVariableMap) => void
|
||||
onShowRetryDetail?: (detail: NodeTracing[]) => void
|
||||
onShowAgentOrToolLog?: (detail?: AgentLogItemWithChildren) => void
|
||||
onShowLLMDetail?: (detail: LLMTraceItem[]) => void
|
||||
notShowIterationNav?: boolean
|
||||
notShowLoopNav?: boolean
|
||||
}
|
||||
@@ -61,7 +58,6 @@ const NodePanel: FC<Props> = ({
|
||||
onShowLoopDetail,
|
||||
onShowRetryDetail,
|
||||
onShowAgentOrToolLog,
|
||||
onShowLLMDetail,
|
||||
notShowIterationNav,
|
||||
notShowLoopNav,
|
||||
}) => {
|
||||
@@ -100,7 +96,6 @@ const NodePanel: FC<Props> = ({
|
||||
const isRetryNode = hasRetryNode(nodeInfo.node_type) && !!nodeInfo.retryDetail?.length
|
||||
const isAgentNode = nodeInfo.node_type === BlockEnum.Agent && !!nodeInfo.agentLog?.length
|
||||
const isToolNode = nodeInfo.node_type === BlockEnum.Tool && !!nodeInfo.agentLog?.length
|
||||
const isLLMNode = nodeInfo.node_type === BlockEnum.LLM && !!nodeInfo.execution_metadata?.llm_trace?.length
|
||||
|
||||
const inputsTitle = useMemo(() => {
|
||||
let text = t('common.input', { ns: 'workflow' })
|
||||
@@ -198,12 +193,6 @@ const NodePanel: FC<Props> = ({
|
||||
onShowRetryResultList={onShowRetryDetail}
|
||||
/>
|
||||
)}
|
||||
{isLLMNode && onShowLLMDetail && (
|
||||
<LLMLogTrigger
|
||||
nodeInfo={nodeInfo}
|
||||
onShowLLMDetail={onShowLLMDetail}
|
||||
/>
|
||||
)}
|
||||
{
|
||||
(isAgentNode || isToolNode) && onShowAgentOrToolLog && (
|
||||
<AgentLogTrigger
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import type { FC } from 'react'
|
||||
import type {
|
||||
AgentLogItemWithChildren,
|
||||
LLMTraceItem,
|
||||
NodeTracing,
|
||||
} from '@/types/workflow'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -16,7 +15,6 @@ import { RetryLogTrigger } from '@/app/components/workflow/run/retry-log'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { hasRetryNode } from '@/app/components/workflow/utils'
|
||||
import LargeDataAlert from '../variable-inspect/large-data-alert'
|
||||
import { LLMLogTrigger } from './llm-log'
|
||||
import MetaData from './meta'
|
||||
import StatusPanel from './status'
|
||||
|
||||
@@ -47,7 +45,6 @@ export type ResultPanelProps = {
|
||||
handleShowLoopResultList?: (detail: NodeTracing[][], loopDurationMap: any) => void
|
||||
onShowRetryDetail?: (detail: NodeTracing[]) => void
|
||||
handleShowAgentOrToolLog?: (detail?: AgentLogItemWithChildren) => void
|
||||
onShowLLMDetail?: (detail: LLMTraceItem[]) => void
|
||||
}
|
||||
|
||||
const ResultPanel: FC<ResultPanelProps> = ({
|
||||
@@ -74,7 +71,6 @@ const ResultPanel: FC<ResultPanelProps> = ({
|
||||
handleShowLoopResultList,
|
||||
onShowRetryDetail,
|
||||
handleShowAgentOrToolLog,
|
||||
onShowLLMDetail,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const isIterationNode = nodeInfo?.node_type === BlockEnum.Iteration && !!nodeInfo?.details?.length
|
||||
@@ -82,7 +78,6 @@ const ResultPanel: FC<ResultPanelProps> = ({
|
||||
const isRetryNode = hasRetryNode(nodeInfo?.node_type) && !!nodeInfo?.retryDetail?.length
|
||||
const isAgentNode = nodeInfo?.node_type === BlockEnum.Agent && !!nodeInfo?.agentLog?.length
|
||||
const isToolNode = nodeInfo?.node_type === BlockEnum.Tool && !!nodeInfo?.agentLog?.length
|
||||
const isLLMNode = nodeInfo?.node_type === BlockEnum.LLM && !!nodeInfo?.execution_metadata?.llm_trace?.length
|
||||
|
||||
return (
|
||||
<div className="bg-components-panel-bg py-2">
|
||||
@@ -121,14 +116,6 @@ const ResultPanel: FC<ResultPanelProps> = ({
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
isLLMNode && onShowLLMDetail && (
|
||||
<LLMLogTrigger
|
||||
nodeInfo={nodeInfo}
|
||||
onShowLLMDetail={onShowLLMDetail}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
(isAgentNode || isToolNode) && handleShowAgentOrToolLog && (
|
||||
<AgentLogTrigger
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import type {
|
||||
AgentLogItemWithChildren,
|
||||
IterationDurationMap,
|
||||
LLMTraceItem,
|
||||
LoopDurationMap,
|
||||
LoopVariableMap,
|
||||
NodeTracing,
|
||||
} from '@/types/workflow'
|
||||
import { AgentResultPanel } from './agent-log'
|
||||
import { IterationResultPanel } from './iteration-log'
|
||||
import { LLMResultPanel } from './llm-log'
|
||||
import { LoopResultPanel } from './loop-log'
|
||||
import { RetryResultPanel } from './retry-log'
|
||||
|
||||
@@ -31,10 +29,6 @@ export type SpecialResultPanelProps = {
|
||||
agentOrToolLogItemStack?: AgentLogItemWithChildren[]
|
||||
agentOrToolLogListMap?: Record<string, AgentLogItemWithChildren[]>
|
||||
handleShowAgentOrToolLog?: (detail?: AgentLogItemWithChildren) => void
|
||||
|
||||
showLLMDetail?: boolean
|
||||
setShowLLMDetailFalse?: () => void
|
||||
llmResultList?: LLMTraceItem[]
|
||||
}
|
||||
const SpecialResultPanel = ({
|
||||
showRetryDetail,
|
||||
@@ -55,10 +49,6 @@ const SpecialResultPanel = ({
|
||||
agentOrToolLogItemStack,
|
||||
agentOrToolLogListMap,
|
||||
handleShowAgentOrToolLog,
|
||||
|
||||
showLLMDetail,
|
||||
setShowLLMDetailFalse,
|
||||
llmResultList,
|
||||
}: SpecialResultPanelProps) => {
|
||||
return (
|
||||
<div onClick={(e) => {
|
||||
@@ -74,14 +64,6 @@ const SpecialResultPanel = ({
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
!!showLLMDetail && !!llmResultList?.length && setShowLLMDetailFalse && (
|
||||
<LLMResultPanel
|
||||
list={llmResultList}
|
||||
onBack={setShowLLMDetailFalse}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
showIteratingDetail && !!iterationResultList?.length && setShowIteratingDetailFalse && (
|
||||
<IterationResultPanel
|
||||
|
||||
@@ -91,11 +91,6 @@ const TracingPanel: FC<TracingPanelProps> = ({
|
||||
agentOrToolLogItemStack,
|
||||
agentOrToolLogListMap,
|
||||
handleShowAgentOrToolLog,
|
||||
|
||||
showLLMDetail,
|
||||
setShowLLMDetailFalse,
|
||||
llmResultList,
|
||||
handleShowLLMDetail,
|
||||
} = useLogs()
|
||||
|
||||
const renderNode = (node: NodeTracing) => {
|
||||
@@ -158,7 +153,6 @@ const TracingPanel: FC<TracingPanelProps> = ({
|
||||
onShowLoopDetail={handleShowLoopResultList}
|
||||
onShowRetryDetail={handleShowRetryResultList}
|
||||
onShowAgentOrToolLog={handleShowAgentOrToolLog}
|
||||
onShowLLMDetail={handleShowLLMDetail}
|
||||
hideInfo={hideNodeInfo}
|
||||
hideProcessDetail={hideNodeProcessDetail}
|
||||
/>
|
||||
@@ -188,10 +182,6 @@ const TracingPanel: FC<TracingPanelProps> = ({
|
||||
agentOrToolLogItemStack={agentOrToolLogItemStack}
|
||||
agentOrToolLogListMap={agentOrToolLogListMap}
|
||||
handleShowAgentOrToolLog={handleShowAgentOrToolLog}
|
||||
|
||||
showLLMDetail={showLLMDetail}
|
||||
setShowLLMDetailFalse={setShowLLMDetailFalse}
|
||||
llmResultList={llmResultList}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -19,6 +19,14 @@ vi.mock('@/service/common', () => ({
|
||||
getSystemFeatures: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/global-public-context', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/context/global-public-context')>()
|
||||
return {
|
||||
...actual,
|
||||
useIsSystemFeaturesPending: () => false,
|
||||
}
|
||||
})
|
||||
|
||||
const mockFetchSetupStatus = vi.mocked(fetchSetupStatus)
|
||||
const mockFetchInitValidateStatus = vi.mocked(fetchInitValidateStatus)
|
||||
const mockSetup = vi.mocked(setup)
|
||||
|
||||
@@ -65,8 +65,10 @@ export default function MailAndPasswordAuth({ isInvite, isEmailSetup, allowRegis
|
||||
body: loginData,
|
||||
})
|
||||
if (res.result === 'success') {
|
||||
// Track login success event
|
||||
setWebAppAccessToken(res.data.access_token)
|
||||
if (res?.data?.access_token) {
|
||||
// Track login success event
|
||||
setWebAppAccessToken(res.data.access_token)
|
||||
}
|
||||
trackEvent('user_login_success', {
|
||||
method: 'email_password',
|
||||
is_invite: isInvite,
|
||||
|
||||
@@ -2,42 +2,61 @@
|
||||
import type { FC, PropsWithChildren } from 'react'
|
||||
import type { SystemFeatures } from '@/types/feature'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useEffect } from 'react'
|
||||
import { create } from 'zustand'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { getSystemFeatures } from '@/service/common'
|
||||
import { defaultSystemFeatures } from '@/types/feature'
|
||||
import { fetchSetupStatusWithCache } from '@/utils/setup-status'
|
||||
|
||||
type GlobalPublicStore = {
|
||||
isGlobalPending: boolean
|
||||
setIsGlobalPending: (isPending: boolean) => void
|
||||
systemFeatures: SystemFeatures
|
||||
setSystemFeatures: (systemFeatures: SystemFeatures) => void
|
||||
}
|
||||
|
||||
export const useGlobalPublicStore = create<GlobalPublicStore>(set => ({
|
||||
isGlobalPending: true,
|
||||
setIsGlobalPending: (isPending: boolean) => set(() => ({ isGlobalPending: isPending })),
|
||||
systemFeatures: defaultSystemFeatures,
|
||||
setSystemFeatures: (systemFeatures: SystemFeatures) => set(() => ({ systemFeatures })),
|
||||
}))
|
||||
|
||||
const systemFeaturesQueryKey = ['systemFeatures'] as const
|
||||
const setupStatusQueryKey = ['setupStatus'] as const
|
||||
|
||||
async function fetchSystemFeatures() {
|
||||
const data = await getSystemFeatures()
|
||||
const { setSystemFeatures } = useGlobalPublicStore.getState()
|
||||
setSystemFeatures({ ...defaultSystemFeatures, ...data })
|
||||
return data
|
||||
}
|
||||
|
||||
export function useSystemFeaturesQuery() {
|
||||
return useQuery({
|
||||
queryKey: systemFeaturesQueryKey,
|
||||
queryFn: fetchSystemFeatures,
|
||||
})
|
||||
}
|
||||
|
||||
export function useIsSystemFeaturesPending() {
|
||||
const { isPending } = useSystemFeaturesQuery()
|
||||
return isPending
|
||||
}
|
||||
|
||||
export function useSetupStatusQuery() {
|
||||
return useQuery({
|
||||
queryKey: setupStatusQueryKey,
|
||||
queryFn: fetchSetupStatusWithCache,
|
||||
staleTime: Infinity,
|
||||
})
|
||||
}
|
||||
|
||||
const GlobalPublicStoreProvider: FC<PropsWithChildren> = ({
|
||||
children,
|
||||
}) => {
|
||||
const { isPending, data } = useQuery({
|
||||
queryKey: ['systemFeatures'],
|
||||
queryFn: getSystemFeatures,
|
||||
})
|
||||
const { setSystemFeatures, setIsGlobalPending: setIsPending } = useGlobalPublicStore()
|
||||
useEffect(() => {
|
||||
if (data)
|
||||
setSystemFeatures({ ...defaultSystemFeatures, ...data })
|
||||
}, [data, setSystemFeatures])
|
||||
// Fetch systemFeatures and setupStatus in parallel to reduce waterfall.
|
||||
// setupStatus is prefetched here and cached in localStorage for AppInitializer.
|
||||
const { isPending } = useSystemFeaturesQuery()
|
||||
|
||||
useEffect(() => {
|
||||
setIsPending(isPending)
|
||||
}, [isPending, setIsPending])
|
||||
// Prefetch setupStatus for AppInitializer (result not needed here)
|
||||
useSetupStatusQuery()
|
||||
|
||||
if (isPending)
|
||||
return <div className="flex h-screen w-screen items-center justify-center"><Loading /></div>
|
||||
|
||||
@@ -10,7 +10,7 @@ import { getProcessedSystemVariablesFromUrlParams } from '@/app/components/base/
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { AccessMode } from '@/models/access-control'
|
||||
import { useGetWebAppAccessModeByCode } from '@/service/use-share'
|
||||
import { useGlobalPublicStore } from './global-public-context'
|
||||
import { useIsSystemFeaturesPending } from './global-public-context'
|
||||
|
||||
type WebAppStore = {
|
||||
shareCode: string | null
|
||||
@@ -65,7 +65,7 @@ const getShareCodeFromPathname = (pathname: string): string | null => {
|
||||
}
|
||||
|
||||
const WebAppStoreProvider: FC<PropsWithChildren> = ({ children }) => {
|
||||
const isGlobalPending = useGlobalPublicStore(s => s.isGlobalPending)
|
||||
const isGlobalPending = useIsSystemFeaturesPending()
|
||||
const updateWebAppAccessMode = useWebAppStore(state => state.updateWebAppAccessMode)
|
||||
const updateShareCode = useWebAppStore(state => state.updateShareCode)
|
||||
const updateEmbeddedUserId = useWebAppStore(state => state.updateEmbeddedUserId)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { useGlobalPublicStore, useIsSystemFeaturesPending } from '@/context/global-public-context'
|
||||
/**
|
||||
* Test suite for useDocumentTitle hook
|
||||
*
|
||||
@@ -15,6 +15,14 @@ import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { defaultSystemFeatures } from '@/types/feature'
|
||||
import useDocumentTitle from './use-document-title'
|
||||
|
||||
vi.mock('@/context/global-public-context', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/context/global-public-context')>()
|
||||
return {
|
||||
...actual,
|
||||
useIsSystemFeaturesPending: vi.fn(() => false),
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/service/common', () => ({
|
||||
getSystemFeatures: vi.fn(() => ({ ...defaultSystemFeatures })),
|
||||
}))
|
||||
@@ -24,10 +32,12 @@ vi.mock('@/service/common', () => ({
|
||||
* Title should remain empty to prevent flicker
|
||||
*/
|
||||
describe('title should be empty if systemFeatures is pending', () => {
|
||||
act(() => {
|
||||
useGlobalPublicStore.setState({
|
||||
systemFeatures: { ...defaultSystemFeatures, branding: { ...defaultSystemFeatures.branding, enabled: false } },
|
||||
isGlobalPending: true,
|
||||
beforeEach(() => {
|
||||
vi.mocked(useIsSystemFeaturesPending).mockReturnValue(true)
|
||||
act(() => {
|
||||
useGlobalPublicStore.setState({
|
||||
systemFeatures: { ...defaultSystemFeatures, branding: { ...defaultSystemFeatures.branding, enabled: false } },
|
||||
})
|
||||
})
|
||||
})
|
||||
/**
|
||||
@@ -52,9 +62,9 @@ describe('title should be empty if systemFeatures is pending', () => {
|
||||
*/
|
||||
describe('use default branding', () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(useIsSystemFeaturesPending).mockReturnValue(false)
|
||||
act(() => {
|
||||
useGlobalPublicStore.setState({
|
||||
isGlobalPending: false,
|
||||
systemFeatures: { ...defaultSystemFeatures, branding: { ...defaultSystemFeatures.branding, enabled: false } },
|
||||
})
|
||||
})
|
||||
@@ -84,9 +94,9 @@ describe('use default branding', () => {
|
||||
*/
|
||||
describe('use specific branding', () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(useIsSystemFeaturesPending).mockReturnValue(false)
|
||||
act(() => {
|
||||
useGlobalPublicStore.setState({
|
||||
isGlobalPending: false,
|
||||
systemFeatures: { ...defaultSystemFeatures, branding: { ...defaultSystemFeatures.branding, enabled: true, application_title: 'Test' } },
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
'use client'
|
||||
import { useFavicon, useTitle } from 'ahooks'
|
||||
import { useEffect } from 'react'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { useGlobalPublicStore, useIsSystemFeaturesPending } from '@/context/global-public-context'
|
||||
import { basePath } from '@/utils/var'
|
||||
|
||||
export default function useDocumentTitle(title: string) {
|
||||
const isPending = useGlobalPublicStore(s => s.isGlobalPending)
|
||||
const isPending = useIsSystemFeaturesPending()
|
||||
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
|
||||
const prefix = title ? `${title} - ` : ''
|
||||
let titleStr = ''
|
||||
|
||||
@@ -126,7 +126,6 @@
|
||||
"common.currentDraftUnpublished": "Current Draft Unpublished",
|
||||
"common.currentView": "Current View",
|
||||
"common.currentWorkflow": "Current Workflow",
|
||||
"common.data": "Data",
|
||||
"common.debugAndPreview": "Preview",
|
||||
"common.disconnect": "Disconnect",
|
||||
"common.duplicate": "Duplicate",
|
||||
@@ -651,7 +650,6 @@
|
||||
"nodes.llm.jsonSchema.warningTips.saveSchema": "Please finish editing the current field before saving the schema",
|
||||
"nodes.llm.model": "model",
|
||||
"nodes.llm.notSetContextInPromptTip": "To enable the context feature, please fill in the context variable in PROMPT.",
|
||||
"nodes.llm.outputVars.generation": "Generation Information",
|
||||
"nodes.llm.outputVars.output": "Generate content",
|
||||
"nodes.llm.outputVars.reasoning_content": "Reasoning Content",
|
||||
"nodes.llm.outputVars.usage": "Model Usage Information",
|
||||
@@ -668,7 +666,6 @@
|
||||
"nodes.llm.roleDescription.user": "Provide instructions, queries, or any text-based input to the model",
|
||||
"nodes.llm.singleRun.variable": "Variable",
|
||||
"nodes.llm.sysQueryInUser": "sys.query in user message is required",
|
||||
"nodes.llm.tools.title": "Tools",
|
||||
"nodes.llm.variables": "variables",
|
||||
"nodes.llm.vision": "vision",
|
||||
"nodes.loop.ErrorMethod.continueOnError": "Continue on Error",
|
||||
|
||||
@@ -126,7 +126,6 @@
|
||||
"common.currentDraftUnpublished": "当前草稿未发布",
|
||||
"common.currentView": "当前视图",
|
||||
"common.currentWorkflow": "整个工作流",
|
||||
"common.data": "数据",
|
||||
"common.debugAndPreview": "预览",
|
||||
"common.disconnect": "断开连接",
|
||||
"common.duplicate": "复制",
|
||||
@@ -651,7 +650,6 @@
|
||||
"nodes.llm.jsonSchema.warningTips.saveSchema": "请先完成当前字段的编辑",
|
||||
"nodes.llm.model": "模型",
|
||||
"nodes.llm.notSetContextInPromptTip": "要启用上下文功能,请在提示中填写上下文变量。",
|
||||
"nodes.llm.outputVars.generation": "生成信息",
|
||||
"nodes.llm.outputVars.output": "生成内容",
|
||||
"nodes.llm.outputVars.reasoning_content": "推理内容",
|
||||
"nodes.llm.outputVars.usage": "模型用量信息",
|
||||
|
||||
+1
-4
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "dify-web",
|
||||
"type": "module",
|
||||
"version": "1.11.2",
|
||||
"version": "1.11.4",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@10.27.0+sha512.72d699da16b1179c14ba9e64dc71c9a40988cbdc65c264cb0e489db7de917f20dcf4d64d8723625f2969ba52d4b7e2a1170682d9ac2a5dcaeaab732b7e16f04a",
|
||||
"imports": {
|
||||
@@ -10,9 +10,6 @@
|
||||
"default": "./i18n-config/lib.client.ts"
|
||||
}
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.12.0"
|
||||
},
|
||||
"browserslist": [
|
||||
"last 1 Chrome version",
|
||||
"last 1 Firefox version",
|
||||
|
||||
@@ -34,27 +34,12 @@ import { getWebAppPassport } from './webapp-auth'
|
||||
|
||||
const TIME_OUT = 100000
|
||||
|
||||
export type IconObject = {
|
||||
background: string
|
||||
content: string
|
||||
}
|
||||
|
||||
export type IOnDataMoreInfo = {
|
||||
conversationId?: string
|
||||
taskId?: string
|
||||
messageId: string
|
||||
errorMessage?: string
|
||||
errorCode?: string
|
||||
chunk_type?: 'text' | 'tool_call' | 'tool_result' | 'thought' | 'thought_start' | 'thought_end'
|
||||
tool_call_id?: string
|
||||
tool_name?: string
|
||||
tool_arguments?: string
|
||||
tool_icon?: string | IconObject
|
||||
tool_icon_dark?: string | IconObject
|
||||
|
||||
tool_files?: string[]
|
||||
tool_error?: string
|
||||
tool_elapsed_time?: number
|
||||
}
|
||||
|
||||
export type IOnData = (message: string, isFirstMessage: boolean, moreInfo: IOnDataMoreInfo) => void
|
||||
@@ -260,15 +245,6 @@ export const handleStream = (
|
||||
conversationId: bufferObj.conversation_id,
|
||||
taskId: bufferObj.task_id,
|
||||
messageId: bufferObj.id,
|
||||
chunk_type: bufferObj.chunk_type,
|
||||
tool_call_id: bufferObj.tool_call_id,
|
||||
tool_name: bufferObj.tool_name,
|
||||
tool_arguments: bufferObj.tool_arguments,
|
||||
tool_icon: bufferObj.tool_icon,
|
||||
tool_icon_dark: bufferObj.tool_icon_dark,
|
||||
tool_files: bufferObj.tool_files,
|
||||
tool_error: bufferObj.tool_error,
|
||||
tool_elapsed_time: bufferObj.tool_elapsed_time,
|
||||
})
|
||||
isFirstMessage = false
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ import { del, get, patch, post, put } from './base'
|
||||
|
||||
type LoginSuccess = {
|
||||
result: 'success'
|
||||
data: { access_token: string }
|
||||
data?: { access_token?: string }
|
||||
}
|
||||
type LoginFail = {
|
||||
result: 'fail'
|
||||
|
||||
@@ -28,68 +28,6 @@ export type AgentLogItemWithChildren = AgentLogItem & {
|
||||
children: AgentLogItemWithChildren[]
|
||||
}
|
||||
|
||||
export type IconObject = {
|
||||
background: string
|
||||
content: string
|
||||
}
|
||||
|
||||
export type ToolCallItem = {
|
||||
id: string
|
||||
type: 'model' | 'tool' | 'thought'
|
||||
thoughtCompleted?: boolean
|
||||
thoughtOutput?: string
|
||||
|
||||
toolName?: string
|
||||
toolProvider?: string
|
||||
toolIcon?: string | IconObject
|
||||
toolIconDark?: string | IconObject
|
||||
toolArguments?: string
|
||||
toolOutput?: Record<string, any> | string
|
||||
toolFiles?: string[]
|
||||
toolError?: string
|
||||
toolDuration?: number
|
||||
|
||||
modelName?: string
|
||||
modelProvider?: string
|
||||
modelOutput?: Record<string, any> | string
|
||||
modelDuration?: number
|
||||
modelIcon?: string | IconObject
|
||||
modelIconDark?: string | IconObject
|
||||
}
|
||||
|
||||
export type ToolCallDetail = {
|
||||
id: string
|
||||
name: string
|
||||
arguments: string
|
||||
output: string
|
||||
files: string[]
|
||||
error: string
|
||||
elapsed_time?: number
|
||||
status: string
|
||||
}
|
||||
export type SequenceSegment
|
||||
= | { type: 'context', start: number, end: number }
|
||||
| { type: 'reasoning', index: number }
|
||||
| { type: 'tool_call', index: number }
|
||||
|
||||
export type LLMLogItem = {
|
||||
reasoning_content: string[]
|
||||
tool_calls: ToolCallDetail[]
|
||||
sequence: SequenceSegment[]
|
||||
}
|
||||
|
||||
export type LLMTraceItem = {
|
||||
type: 'model' | 'tool'
|
||||
duration: number
|
||||
output: Record<string, any>
|
||||
provider?: string
|
||||
name: string
|
||||
icon?: string | IconObject
|
||||
icon_dark?: string | IconObject
|
||||
error?: string
|
||||
status?: 'success' | 'error'
|
||||
}
|
||||
|
||||
export type NodeTracing = {
|
||||
id: string
|
||||
index: number
|
||||
@@ -134,7 +72,6 @@ export type NodeTracing = {
|
||||
icon?: string
|
||||
}
|
||||
loop_variable_map?: Record<string, any>
|
||||
llm_trace?: LLMTraceItem[]
|
||||
}
|
||||
metadata: {
|
||||
iterator_length: number
|
||||
@@ -167,7 +104,6 @@ export type NodeTracing = {
|
||||
parent_parallel_id?: string
|
||||
parent_parallel_start_node_id?: string
|
||||
agentLog?: AgentLogItemWithChildren[] // agent log
|
||||
generation_detail?: LLMLogItem
|
||||
}
|
||||
|
||||
export type FetchWorkflowDraftResponse = {
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import type { SetupStatusResponse } from '@/models/common'
|
||||
|
||||
import { fetchSetupStatus } from '@/service/common'
|
||||
|
||||
import { fetchSetupStatusWithCache } from './setup-status'
|
||||
|
||||
vi.mock('@/service/common', () => ({
|
||||
fetchSetupStatus: vi.fn(),
|
||||
}))
|
||||
|
||||
const mockFetchSetupStatus = vi.mocked(fetchSetupStatus)
|
||||
|
||||
describe('setup-status utilities', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
localStorage.clear()
|
||||
})
|
||||
|
||||
describe('fetchSetupStatusWithCache', () => {
|
||||
describe('when cache exists', () => {
|
||||
it('should return cached finished status without API call', async () => {
|
||||
localStorage.setItem('setup_status', 'finished')
|
||||
|
||||
const result = await fetchSetupStatusWithCache()
|
||||
|
||||
expect(result).toEqual({ step: 'finished' })
|
||||
expect(mockFetchSetupStatus).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not modify localStorage when returning cached value', async () => {
|
||||
localStorage.setItem('setup_status', 'finished')
|
||||
|
||||
await fetchSetupStatusWithCache()
|
||||
|
||||
expect(localStorage.getItem('setup_status')).toBe('finished')
|
||||
})
|
||||
})
|
||||
|
||||
describe('when cache does not exist', () => {
|
||||
it('should call API and cache finished status', async () => {
|
||||
const apiResponse: SetupStatusResponse = { step: 'finished' }
|
||||
mockFetchSetupStatus.mockResolvedValue(apiResponse)
|
||||
|
||||
const result = await fetchSetupStatusWithCache()
|
||||
|
||||
expect(mockFetchSetupStatus).toHaveBeenCalledTimes(1)
|
||||
expect(result).toEqual(apiResponse)
|
||||
expect(localStorage.getItem('setup_status')).toBe('finished')
|
||||
})
|
||||
|
||||
it('should call API and remove cache when not finished', async () => {
|
||||
const apiResponse: SetupStatusResponse = { step: 'not_started' }
|
||||
mockFetchSetupStatus.mockResolvedValue(apiResponse)
|
||||
|
||||
const result = await fetchSetupStatusWithCache()
|
||||
|
||||
expect(mockFetchSetupStatus).toHaveBeenCalledTimes(1)
|
||||
expect(result).toEqual(apiResponse)
|
||||
expect(localStorage.getItem('setup_status')).toBeNull()
|
||||
})
|
||||
|
||||
it('should clear stale cache when API returns not_started', async () => {
|
||||
localStorage.setItem('setup_status', 'some_invalid_value')
|
||||
const apiResponse: SetupStatusResponse = { step: 'not_started' }
|
||||
mockFetchSetupStatus.mockResolvedValue(apiResponse)
|
||||
|
||||
const result = await fetchSetupStatusWithCache()
|
||||
|
||||
expect(result).toEqual(apiResponse)
|
||||
expect(localStorage.getItem('setup_status')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('cache edge cases', () => {
|
||||
it('should call API when cache value is empty string', async () => {
|
||||
localStorage.setItem('setup_status', '')
|
||||
const apiResponse: SetupStatusResponse = { step: 'finished' }
|
||||
mockFetchSetupStatus.mockResolvedValue(apiResponse)
|
||||
|
||||
const result = await fetchSetupStatusWithCache()
|
||||
|
||||
expect(mockFetchSetupStatus).toHaveBeenCalledTimes(1)
|
||||
expect(result).toEqual(apiResponse)
|
||||
})
|
||||
|
||||
it('should call API when cache value is not "finished"', async () => {
|
||||
localStorage.setItem('setup_status', 'not_started')
|
||||
const apiResponse: SetupStatusResponse = { step: 'finished' }
|
||||
mockFetchSetupStatus.mockResolvedValue(apiResponse)
|
||||
|
||||
const result = await fetchSetupStatusWithCache()
|
||||
|
||||
expect(mockFetchSetupStatus).toHaveBeenCalledTimes(1)
|
||||
expect(result).toEqual(apiResponse)
|
||||
})
|
||||
|
||||
it('should call API when localStorage key does not exist', async () => {
|
||||
const apiResponse: SetupStatusResponse = { step: 'finished' }
|
||||
mockFetchSetupStatus.mockResolvedValue(apiResponse)
|
||||
|
||||
const result = await fetchSetupStatusWithCache()
|
||||
|
||||
expect(mockFetchSetupStatus).toHaveBeenCalledTimes(1)
|
||||
expect(result).toEqual(apiResponse)
|
||||
})
|
||||
})
|
||||
|
||||
describe('API response handling', () => {
|
||||
it('should preserve setup_at from API response', async () => {
|
||||
const setupDate = new Date('2024-01-01')
|
||||
const apiResponse: SetupStatusResponse = {
|
||||
step: 'finished',
|
||||
setup_at: setupDate,
|
||||
}
|
||||
mockFetchSetupStatus.mockResolvedValue(apiResponse)
|
||||
|
||||
const result = await fetchSetupStatusWithCache()
|
||||
|
||||
expect(result).toEqual(apiResponse)
|
||||
expect(result.setup_at).toEqual(setupDate)
|
||||
})
|
||||
|
||||
it('should propagate API errors', async () => {
|
||||
const apiError = new Error('Network error')
|
||||
mockFetchSetupStatus.mockRejectedValue(apiError)
|
||||
|
||||
await expect(fetchSetupStatusWithCache()).rejects.toThrow('Network error')
|
||||
})
|
||||
|
||||
it('should not update cache when API call fails', async () => {
|
||||
mockFetchSetupStatus.mockRejectedValue(new Error('API error'))
|
||||
|
||||
await expect(fetchSetupStatusWithCache()).rejects.toThrow()
|
||||
|
||||
expect(localStorage.getItem('setup_status')).toBeNull()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { SetupStatusResponse } from '@/models/common'
|
||||
import { fetchSetupStatus } from '@/service/common'
|
||||
|
||||
const SETUP_STATUS_KEY = 'setup_status'
|
||||
|
||||
const isSetupStatusCached = (): boolean =>
|
||||
localStorage.getItem(SETUP_STATUS_KEY) === 'finished'
|
||||
|
||||
export const fetchSetupStatusWithCache = async (): Promise<SetupStatusResponse> => {
|
||||
if (isSetupStatusCached())
|
||||
return { step: 'finished' }
|
||||
|
||||
const status = await fetchSetupStatus()
|
||||
|
||||
if (status.step === 'finished')
|
||||
localStorage.setItem(SETUP_STATUS_KEY, 'finished')
|
||||
else
|
||||
localStorage.removeItem(SETUP_STATUS_KEY)
|
||||
|
||||
return status
|
||||
}
|
||||
Reference in New Issue
Block a user