Merge branch 'main' into add-endpoint-of-get-feedback

pull/18697/head
Ganondorf 1 year ago committed by GitHub
commit 657ed52d9c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -79,7 +79,7 @@ class AnnotationListApi(Resource):
class AnnotationUpdateDeleteApi(Resource):
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON))
@marshal_with(annotation_fields)
def post(self, app_model: App, end_user: EndUser, annotation_id):
def put(self, app_model: App, end_user: EndUser, annotation_id):
if not current_user.is_editor:
raise Forbidden()

@ -1,6 +1,7 @@
from collections.abc import Mapping
from datetime import datetime
from enum import StrEnum
from typing import Generic, Optional, TypeVar
from typing import Any, Generic, Optional, TypeVar
from pydantic import BaseModel, ConfigDict, Field
@ -158,3 +159,11 @@ class PluginInstallTaskStartResponse(BaseModel):
class PluginUploadResponse(BaseModel):
unique_identifier: str = Field(description="The unique identifier of the plugin.")
manifest: PluginDeclaration
class PluginOAuthAuthorizationUrlResponse(BaseModel):
authorization_url: str = Field(description="The URL of the authorization.")
class PluginOAuthCredentialsResponse(BaseModel):
credentials: Mapping[str, Any] = Field(description="The credentials of the OAuth.")

@ -1,6 +1,98 @@
from collections.abc import Mapping
from typing import Any
from werkzeug import Request
from core.plugin.entities.plugin_daemon import PluginOAuthAuthorizationUrlResponse, PluginOAuthCredentialsResponse
from core.plugin.impl.base import BasePluginClient
class OAuthHandler(BasePluginClient):
def get_authorization_url(self, tenant_id: str, user_id: str, provider_name: str) -> str:
return "1234567890"
def get_authorization_url(
self,
tenant_id: str,
user_id: str,
plugin_id: str,
provider: str,
system_credentials: Mapping[str, Any],
) -> PluginOAuthAuthorizationUrlResponse:
return self._request_with_plugin_daemon_response(
"POST",
f"plugin/{tenant_id}/dispatch/oauth/get_authorization_url",
PluginOAuthAuthorizationUrlResponse,
data={
"user_id": user_id,
"data": {
"provider": provider,
"system_credentials": system_credentials,
},
},
headers={
"X-Plugin-ID": plugin_id,
"Content-Type": "application/json",
},
)
def get_credentials(
self,
tenant_id: str,
user_id: str,
plugin_id: str,
provider: str,
system_credentials: Mapping[str, Any],
request: Request,
) -> PluginOAuthCredentialsResponse:
"""
Get credentials from the given request.
"""
# encode request to raw http request
raw_request_bytes = self._convert_request_to_raw_data(request)
return self._request_with_plugin_daemon_response(
"POST",
f"plugin/{tenant_id}/dispatch/oauth/get_credentials",
PluginOAuthCredentialsResponse,
data={
"user_id": user_id,
"data": {
"provider": provider,
"system_credentials": system_credentials,
"raw_request_bytes": raw_request_bytes,
},
},
headers={
"X-Plugin-ID": plugin_id,
"Content-Type": "application/json",
},
)
def _convert_request_to_raw_data(self, request: Request) -> bytes:
"""
Convert a Request object to raw HTTP data.
Args:
request: The Request object to convert.
Returns:
The raw HTTP data as bytes.
"""
# Start with the request line
method = request.method
path = request.path
protocol = request.headers.get("HTTP_VERSION", "HTTP/1.1")
raw_data = f"{method} {path} {protocol}\r\n".encode()
# Add headers
for header_name, header_value in request.headers.items():
raw_data += f"{header_name}: {header_value}\r\n".encode()
# Add empty line to separate headers from body
raw_data += b"\r\n"
# Add body if exists
body = request.get_data(as_text=False)
if body:
raw_data += body
return raw_data

@ -36,6 +36,31 @@ from configs import dify_config
from dify_app import DifyApp
class ExceptionLoggingHandler(logging.Handler):
"""Custom logging handler that creates spans for logging.exception() calls"""
def emit(self, record):
try:
if record.exc_info:
tracer = get_tracer_provider().get_tracer("dify.exception.logging")
with tracer.start_as_current_span(
"log.exception",
attributes={
"log.level": record.levelname,
"log.message": record.getMessage(),
"log.logger": record.name,
"log.file.path": record.pathname,
"log.file.line": record.lineno,
},
) as span:
span.set_status(StatusCode.ERROR)
span.record_exception(record.exc_info[1])
span.set_attribute("exception.type", record.exc_info[0].__name__)
span.set_attribute("exception.message", str(record.exc_info[1]))
except Exception:
pass
@user_logged_in.connect
@user_loaded_from_request.connect
def on_user_loaded(_sender, user):
@ -103,6 +128,7 @@ def init_app(app: DifyApp):
if not is_celery_worker():
init_flask_instrumentor(app)
CeleryInstrumentor(tracer_provider=get_tracer_provider(), meter_provider=get_meter_provider()).instrument()
instrument_exception_logging()
init_sqlalchemy_instrumentor(app)
atexit.register(shutdown_tracer)
@ -111,6 +137,11 @@ def is_celery_worker():
return "celery" in sys.argv[0].lower()
def instrument_exception_logging():
exception_handler = ExceptionLoggingHandler()
logging.getLogger().addHandler(exception_handler)
def init_flask_instrumentor(app: DifyApp):
meter = get_meter("http_metrics", version=dify_config.CURRENT_VERSION)
_http_response_counter = meter.create_counter(

@ -0,0 +1,20 @@
from werkzeug import Request
from werkzeug.datastructures import Headers
from werkzeug.test import EnvironBuilder
from core.plugin.impl.oauth import OAuthHandler
def test_oauth_convert_request_to_raw_data():
oauth_handler = OAuthHandler()
builder = EnvironBuilder(
method="GET",
path="/test",
headers=Headers({"Content-Type": "application/json"}),
)
request = Request(builder.get_environ())
raw_request_bytes = oauth_handler._convert_request_to_raw_data(request)
assert b"GET /test HTTP/1.1" in raw_request_bytes
assert b"Content-Type: application/json" in raw_request_bytes
assert b"\r\n\r\n" in raw_request_bytes

@ -4,7 +4,6 @@ import { useMount } from 'ahooks'
import { useTranslation } from 'react-i18next'
import { isEqual } from 'lodash-es'
import { RiCloseLine } from '@remixicon/react'
import { BookOpenIcon } from '@heroicons/react/24/outline'
import { ApiConnectionMod } from '@/app/components/base/icons/src/vender/solid/development'
import cn from '@/utils/classnames'
import IndexMethodRadio from '@/app/components/datasets/settings/index-method-radio'
@ -223,10 +222,6 @@ const SettingsModal: FC<SettingsModalProps> = ({
className='resize-none'
placeholder={t('datasetSettings.form.descPlaceholder') || ''}
/>
<a className='mt-2 flex h-[18px] items-center px-3 text-xs text-text-tertiary' href="https://docs.dify.ai/features/datasets#how-to-write-a-good-dataset-description" target='_blank' rel='noopener noreferrer'>
<BookOpenIcon className='mr-1 h-[18px] w-3' />
{t('datasetSettings.form.descWrite')}
</a>
</div>
</div>
<div className={rowClass}>

@ -356,32 +356,31 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
</Row>
---
<Heading
url='/app/feedbacks'
method='GET'
title='获取APP的消息点赞和反馈'
title='Get feedbacks of application'
name='#app-feedbacks'
/>
<Row>
<Col>
获取应用的终端用户反馈、点赞。
Get application's feedbacks.
### Query
<Properties>
<Property name='page' type='string' key='page'>
选填)分页,默认值1
optionalpaginationdefault1
</Property>
</Properties>
<Properties>
<Property name='limit' type='string' key='limit'>
选填)每页数量,默认值20
optional records per page default20
</Property>
</Properties>
### Response
- `data` (List) 返回该APP的点赞、反馈列表。
- `data` (List) return apps feedback list.
</Col>
<Col sticky>
@ -397,23 +396,23 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
"data": [
{
"id": "8c0fbed8-e2f9-49ff-9f0e-15a35bdd0e25",
"app_id": "f252d396-fe48-450e-94ec-e184218e7346",
"conversation_id": "2397604b-9deb-430e-b285-4726e51fd62d",
"message_id": "709c0b0f-0a96-4a4e-91a4-ec0889937b11",
"rating": "like",
"content": "message feedback information-3",
"from_source": "user",
"from_end_user_id": "74286412-9a1a-42c1-929c-01edb1d381d5",
"from_account_id": null,
"created_at": "2025-04-24T09:24:38",
"updated_at": "2025-04-24T09:24:38"
}
]
}
{
"data": [
{
"id": "8c0fbed8-e2f9-49ff-9f0e-15a35bdd0e25",
"app_id": "f252d396-fe48-450e-94ec-e184218e7346",
"conversation_id": "2397604b-9deb-430e-b285-4726e51fd62d",
"message_id": "709c0b0f-0a96-4a4e-91a4-ec0889937b11",
"rating": "like",
"content": "message feedback information-3",
"from_source": "user",
"from_end_user_id": "74286412-9a1a-42c1-929c-01edb1d381d5",
"from_account_id": null,
"created_at": "2025-04-24T09:24:38",
"updated_at": "2025-04-24T09:24:38"
}
]
}
```
</CodeGroup>
</Col>
@ -706,13 +705,11 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
{
"id": "69d48372-ad81-4c75-9c46-2ce197b4d402",
"question": "What is your name?",
"answer": "I am Dify.",
"hit_count": 0,
"created_at": 1735625869
}
"id": "69d48372-ad81-4c75-9c46-2ce197b4d402",
"question": "What is your name?",
"answer": "I am Dify.",
"hit_count": 0,
"created_at": 1735625869
}
```
</CodeGroup>
@ -746,10 +743,10 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
title="Request"
tag="PUT"
label="/apps/annotations/{annotation_id}"
targetCode={`curl --location --request POST '${props.apiBaseUrl}/apps/annotations/{annotation_id}' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{"question": "What is your name?","answer": "I am Dify."}'`}
targetCode={`curl --location --request PUT '${props.apiBaseUrl}/apps/annotations/{annotation_id}' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{"question": "What is your name?","answer": "I am Dify."}'`}
>
```bash {{ title: 'cURL' }}
curl --location --request POST '${props.apiBaseUrl}/apps/annotations/{annotation_id}' \
curl --location --request PUT '${props.apiBaseUrl}/apps/annotations/{annotation_id}' \
--header 'Authorization: Bearer {api_key}' \
--header 'Content-Type: application/json' \
--data-raw '{
@ -762,13 +759,11 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
{
"id": "69d48372-ad81-4c75-9c46-2ce197b4d402",
"question": "What is your name?",
"answer": "I am Dify.",
"hit_count": 0,
"created_at": 1735625869
}
"id": "69d48372-ad81-4c75-9c46-2ce197b4d402",
"question": "What is your name?",
"answer": "I am Dify.",
"hit_count": 0,
"created_at": 1735625869
}
```
</CodeGroup>
@ -826,10 +821,10 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
<Property name='action' type='string' key='action'>
动作,只能是 'enable' 或 'disable'
</Property>
<Property name='embedding_model_provider' type='string' key='embedding_model_provider'>
<Property name='embedding_provider_name' type='string' key='embedding_provider_name'>
指定的嵌入模型提供商, 必须先在系统内设定好接入的模型对应的是provider字段
</Property>
<Property name='embedding_model' type='string' key='embedding_model'>
<Property name='embedding_model_name' type='string' key='embedding_model_name'>
指定的嵌入模型对应的是model字段
</Property>
<Property name='score_threshold' type='number' key='score_threshold'>

@ -1400,13 +1400,11 @@ Chat applications support session persistence, allowing previous chat history to
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
{
"id": "69d48372-ad81-4c75-9c46-2ce197b4d402",
"question": "What is your name?",
"answer": "I am Dify.",
"hit_count": 0,
"created_at": 1735625869
}
"id": "69d48372-ad81-4c75-9c46-2ce197b4d402",
"question": "What is your name?",
"answer": "I am Dify.",
"hit_count": 0,
"created_at": 1735625869
}
```
</CodeGroup>
@ -1440,10 +1438,10 @@ Chat applications support session persistence, allowing previous chat history to
title="Request"
tag="PUT"
label="/apps/annotations/{annotation_id}"
targetCode={`curl --location --request POST '${props.apiBaseUrl}/apps/annotations/{annotation_id}' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{"question": "What is your name?","answer": "I am Dify."}'`}
targetCode={`curl --location --request PUT '${props.apiBaseUrl}/apps/annotations/{annotation_id}' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{"question": "What is your name?","answer": "I am Dify."}'`}
>
```bash {{ title: 'cURL' }}
curl --location --request POST '${props.apiBaseUrl}/apps/annotations/{annotation_id}' \
curl --location --request PUT '${props.apiBaseUrl}/apps/annotations/{annotation_id}' \
--header 'Authorization: Bearer {api_key}' \
--header 'Content-Type: application/json' \
--data-raw '{
@ -1456,13 +1454,11 @@ Chat applications support session persistence, allowing previous chat history to
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
{
"id": "69d48372-ad81-4c75-9c46-2ce197b4d402",
"question": "What is your name?",
"answer": "I am Dify.",
"hit_count": 0,
"created_at": 1735625869
}
"id": "69d48372-ad81-4c75-9c46-2ce197b4d402",
"question": "What is your name?",
"answer": "I am Dify.",
"hit_count": 0,
"created_at": 1735625869
}
```
</CodeGroup>
@ -1520,10 +1516,10 @@ Chat applications support session persistence, allowing previous chat history to
<Property name='action' type='string' key='action'>
Action, can only be 'enable' or 'disable'
</Property>
<Property name='embedding_model_provider' type='string' key='embedding_model_provider'>
<Property name='embedding_provider_name' type='string' key='embedding_provider_name'>
Specified embedding model provider, must be set up in the system first, corresponding to the provider field(Optional)
</Property>
<Property name='embedding_model' type='string' key='embedding_model'>
<Property name='embedding_model_name' type='string' key='embedding_model_name'>
Specified embedding model, corresponding to the model field(Optional)
</Property>
<Property name='score_threshold' type='number' key='score_threshold'>

@ -1426,13 +1426,11 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
{
"id": "69d48372-ad81-4c75-9c46-2ce197b4d402",
"question": "What is your name?",
"answer": "I am Dify.",
"hit_count": 0,
"created_at": 1735625869
}
"id": "69d48372-ad81-4c75-9c46-2ce197b4d402",
"question": "What is your name?",
"answer": "I am Dify.",
"hit_count": 0,
"created_at": 1735625869
}
```
</CodeGroup>
@ -1466,10 +1464,10 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
title="Request"
tag="PUT"
label="/apps/annotations/{annotation_id}"
targetCode={`curl --location --request POST '${props.appDetail.api_base_url}/apps/annotations/{annotation_id}' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{"question": "What is your name?","answer": "I am Dify."}'`}
targetCode={`curl --location --request PUT '${props.appDetail.api_base_url}/apps/annotations/{annotation_id}' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{"question": "What is your name?","answer": "I am Dify."}'`}
>
```bash {{ title: 'cURL' }}
curl --location --request POST '${props.appDetail.api_base_url}/apps/annotations/{annotation_id}' \
curl --location --request PUT '${props.appDetail.api_base_url}/apps/annotations/{annotation_id}' \
--header 'Authorization: Bearer {api_key}' \
--header 'Content-Type: application/json' \
--data-raw '{
@ -1482,13 +1480,11 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
{
"id": "69d48372-ad81-4c75-9c46-2ce197b4d402",
"question": "What is your name?",
"answer": "I am Dify.",
"hit_count": 0,
"created_at": 1735625869
}
"id": "69d48372-ad81-4c75-9c46-2ce197b4d402",
"question": "What is your name?",
"answer": "I am Dify.",
"hit_count": 0,
"created_at": 1735625869
}
```
</CodeGroup>
@ -1546,10 +1542,10 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
<Property name='action' type='string' key='action'>
动作,只能是 'enable' 或 'disable'
</Property>
<Property name='embedding_model_provider' type='string' key='embedding_model_provider'>
<Property name='embedding_provider_name' type='string' key='embedding_provider_name'>
指定的嵌入模型提供商, 必须先在系统内设定好接入的模型对应的是provider字段
</Property>
<Property name='embedding_model' type='string' key='embedding_model'>
<Property name='embedding_model_name' type='string' key='embedding_model_name'>
指定的嵌入模型对应的是model字段
</Property>
<Property name='score_threshold' type='number' key='score_threshold'>

@ -17,6 +17,7 @@ type Props = {
children?: React.JSX.Element | string | null
operations?: React.JSX.Element
inline?: boolean
required?: boolean
}
const Field: FC<Props> = ({
@ -28,6 +29,7 @@ const Field: FC<Props> = ({
operations,
inline,
supportFold,
required,
}) => {
const [fold, {
toggle: toggleFold,
@ -38,7 +40,9 @@ const Field: FC<Props> = ({
onClick={() => supportFold && toggleFold()}
className={cn('flex items-center justify-between', supportFold && 'cursor-pointer')}>
<div className='flex h-6 items-center'>
<div className={cn(isSubTitle ? 'system-xs-medium-uppercase text-text-tertiary' : 'system-sm-semibold-uppercase text-text-secondary')}>{title}</div>
<div className={cn(isSubTitle ? 'system-xs-medium-uppercase text-text-tertiary' : 'system-sm-semibold-uppercase text-text-secondary')}>
{title} {required && <span className='text-text-destructive'>*</span>}
</div>
{tooltip && (
<Tooltip
popupContent={tooltip}

@ -81,7 +81,11 @@ const AgentPanel: FC<NodePanelProps<AgentNodeType>> = (props) => {
const resetEditor = useStore(s => s.setControlPromptEditorRerenderKey)
return <div className='my-2'>
<Field title={t('workflow.nodes.agent.strategy.label')} className='px-4 py-2' tooltip={t('workflow.nodes.agent.strategy.tooltip')} >
<Field
required
title={t('workflow.nodes.agent.strategy.label')}
className='px-4 py-2'
tooltip={t('workflow.nodes.agent.strategy.tooltip')} >
<AgentStrategy
strategy={inputs.agent_strategy_name ? {
agent_strategy_provider_name: inputs.agent_strategy_provider_name!,

@ -117,8 +117,8 @@ const Panel: FC<NodePanelProps<CodeNodeType>> = ({
operations={
<AddButton onClick={handleAddOutputVariable} />
}
required
>
<OutputVarList
readonly={readOnly}
outputs={inputs.outputs}

@ -64,6 +64,7 @@ const Panel: FC<NodePanelProps<DocExtractorNodeType>> = ({
<div className='space-y-4 px-4 pb-4'>
<Field
title={t(`${i18nPrefix}.inputVar`)}
required
>
<>
<VarReferencePicker

@ -69,6 +69,7 @@ const Panel: FC<NodePanelProps<HttpNodeType>> = ({
<div className='space-y-4 px-4 pb-4'>
<Field
title={t(`${i18nPrefix}.api`)}
required
operations={
<div className='flex'>
<div
@ -126,6 +127,7 @@ const Panel: FC<NodePanelProps<HttpNodeType>> = ({
</Field>
<Field
title={t(`${i18nPrefix}.body`)}
required
>
<EditBody
nodeId={id}

@ -73,6 +73,7 @@ const Panel: FC<NodePanelProps<IterationNodeType>> = ({
<div className='space-y-4 px-4 pb-4'>
<Field
title={t(`${i18nPrefix}.input`)}
required
operations={(
<div className='system-2xs-medium-uppercase flex h-[18px] items-center rounded-[5px] border border-divider-deep px-1 capitalize text-text-tertiary'>Array</div>
)}
@ -91,6 +92,7 @@ const Panel: FC<NodePanelProps<IterationNodeType>> = ({
<div className='mt-2 space-y-4 px-4 pb-4'>
<Field
title={t(`${i18nPrefix}.output`)}
required
operations={(
<div className='system-2xs-medium-uppercase flex h-[18px] items-center rounded-[5px] border border-divider-deep px-1 capitalize text-text-tertiary'>Array</div>
)}

@ -81,6 +81,7 @@ const Panel: FC<NodePanelProps<KnowledgeRetrievalNodeType>> = ({
{/* {JSON.stringify(inputs, null, 2)} */}
<Field
title={t(`${i18nPrefix}.queryVariable`)}
required
>
<VarReferencePicker
nodeId={id}
@ -94,6 +95,7 @@ const Panel: FC<NodePanelProps<KnowledgeRetrievalNodeType>> = ({
<Field
title={t(`${i18nPrefix}.knowledge`)}
required
operations={
<div className='flex items-center space-x-1'>
<RetrievalConfig

@ -46,6 +46,7 @@ const Panel: FC<NodePanelProps<ListFilterNodeType>> = ({
<div className='space-y-4 px-4'>
<Field
title={t(`${i18nPrefix}.inputVar`)}
required
>
<VarReferencePicker
readonly={readOnly}

@ -147,6 +147,7 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
<div className='space-y-4 px-4 pb-4'>
<Field
title={t(`${i18nPrefix}.model`)}
required
>
<ModelParameterModal
popupClassName='!w-[387px]'

@ -115,6 +115,7 @@ const Panel: FC<NodePanelProps<ParameterExtractorNodeType>> = ({
<div className='space-y-4 px-4'>
<Field
title={t(`${i18nCommonPrefix}.model`)}
required
>
<ModelParameterModal
popupClassName='!w-[387px]'
@ -133,6 +134,7 @@ const Panel: FC<NodePanelProps<ParameterExtractorNodeType>> = ({
</Field>
<Field
title={t(`${i18nPrefix}.inputVar`)}
required
>
<>
<VarReferencePicker
@ -157,6 +159,7 @@ const Panel: FC<NodePanelProps<ParameterExtractorNodeType>> = ({
/>
<Field
title={t(`${i18nPrefix}.extractParameters`)}
required
operations={
!readOnly
? (

@ -103,6 +103,7 @@ const Panel: FC<NodePanelProps<QuestionClassifierNodeType>> = ({
<div className='space-y-4 px-4'>
<Field
title={t(`${i18nPrefix}.model`)}
required
>
<ModelParameterModal
popupClassName='!w-[387px]'
@ -121,6 +122,7 @@ const Panel: FC<NodePanelProps<QuestionClassifierNodeType>> = ({
</Field>
<Field
title={t(`${i18nPrefix}.inputVars`)}
required
>
<VarReferencePicker
readonly={readOnly}
@ -143,6 +145,7 @@ const Panel: FC<NodePanelProps<QuestionClassifierNodeType>> = ({
/>
<Field
title={t(`${i18nPrefix}.class`)}
required
>
<ClassList
nodeId={id}

Loading…
Cancel
Save