mirror of https://github.com/langgenius/dify.git
Merge remote-tracking branch 'origin/main' into feat/end-user-oauth
This commit is contained in:
commit
f3258bab9e
|
|
@ -1,6 +1,8 @@
|
|||
import logging
|
||||
import time
|
||||
|
||||
from opentelemetry.trace import get_current_span
|
||||
|
||||
from configs import dify_config
|
||||
from contexts.wrapper import RecyclableContextVar
|
||||
from dify_app import DifyApp
|
||||
|
|
@ -26,8 +28,25 @@ def create_flask_app_with_configs() -> DifyApp:
|
|||
# add an unique identifier to each request
|
||||
RecyclableContextVar.increment_thread_recycles()
|
||||
|
||||
# add after request hook for injecting X-Trace-Id header from OpenTelemetry span context
|
||||
@dify_app.after_request
|
||||
def add_trace_id_header(response):
|
||||
try:
|
||||
span = get_current_span()
|
||||
ctx = span.get_span_context() if span else None
|
||||
if ctx and ctx.is_valid:
|
||||
trace_id_hex = format(ctx.trace_id, "032x")
|
||||
# Avoid duplicates if some middleware added it
|
||||
if "X-Trace-Id" not in response.headers:
|
||||
response.headers["X-Trace-Id"] = trace_id_hex
|
||||
except Exception:
|
||||
# Never break the response due to tracing header injection
|
||||
logger.warning("Failed to add trace ID to response header", exc_info=True)
|
||||
return response
|
||||
|
||||
# Capture the decorator's return value to avoid pyright reportUnusedFunction
|
||||
_ = before_request
|
||||
_ = add_trace_id_header
|
||||
|
||||
return dify_app
|
||||
|
||||
|
|
|
|||
|
|
@ -553,7 +553,10 @@ class LoggingConfig(BaseSettings):
|
|||
|
||||
LOG_FORMAT: str = Field(
|
||||
description="Format string for log messages",
|
||||
default="%(asctime)s.%(msecs)03d %(levelname)s [%(threadName)s] [%(filename)s:%(lineno)d] - %(message)s",
|
||||
default=(
|
||||
"%(asctime)s.%(msecs)03d %(levelname)s [%(threadName)s] "
|
||||
"[%(filename)s:%(lineno)d] %(trace_id)s - %(message)s"
|
||||
),
|
||||
)
|
||||
|
||||
LOG_DATEFORMAT: str | None = Field(
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ BASE_CORS_HEADERS: tuple[str, ...] = ("Content-Type", HEADER_NAME_APP_CODE, HEAD
|
|||
SERVICE_API_HEADERS: tuple[str, ...] = (*BASE_CORS_HEADERS, "Authorization")
|
||||
AUTHENTICATED_HEADERS: tuple[str, ...] = (*SERVICE_API_HEADERS, HEADER_NAME_CSRF_TOKEN)
|
||||
FILES_HEADERS: tuple[str, ...] = (*BASE_CORS_HEADERS, HEADER_NAME_CSRF_TOKEN)
|
||||
EXPOSED_HEADERS: tuple[str, ...] = ("X-Version", "X-Env", "X-Trace-Id")
|
||||
|
||||
|
||||
def init_app(app: DifyApp):
|
||||
|
|
@ -25,6 +26,7 @@ def init_app(app: DifyApp):
|
|||
service_api_bp,
|
||||
allow_headers=list(SERVICE_API_HEADERS),
|
||||
methods=["GET", "PUT", "POST", "DELETE", "OPTIONS", "PATCH"],
|
||||
expose_headers=list(EXPOSED_HEADERS),
|
||||
)
|
||||
app.register_blueprint(service_api_bp)
|
||||
|
||||
|
|
@ -34,7 +36,7 @@ def init_app(app: DifyApp):
|
|||
supports_credentials=True,
|
||||
allow_headers=list(AUTHENTICATED_HEADERS),
|
||||
methods=["GET", "PUT", "POST", "DELETE", "OPTIONS", "PATCH"],
|
||||
expose_headers=["X-Version", "X-Env"],
|
||||
expose_headers=list(EXPOSED_HEADERS),
|
||||
)
|
||||
app.register_blueprint(web_bp)
|
||||
|
||||
|
|
@ -44,7 +46,7 @@ def init_app(app: DifyApp):
|
|||
supports_credentials=True,
|
||||
allow_headers=list(AUTHENTICATED_HEADERS),
|
||||
methods=["GET", "PUT", "POST", "DELETE", "OPTIONS", "PATCH"],
|
||||
expose_headers=["X-Version", "X-Env"],
|
||||
expose_headers=list(EXPOSED_HEADERS),
|
||||
)
|
||||
app.register_blueprint(console_app_bp)
|
||||
|
||||
|
|
@ -52,6 +54,7 @@ def init_app(app: DifyApp):
|
|||
files_bp,
|
||||
allow_headers=list(FILES_HEADERS),
|
||||
methods=["GET", "PUT", "POST", "DELETE", "OPTIONS", "PATCH"],
|
||||
expose_headers=list(EXPOSED_HEADERS),
|
||||
)
|
||||
app.register_blueprint(files_bp)
|
||||
|
||||
|
|
@ -63,5 +66,6 @@ def init_app(app: DifyApp):
|
|||
trigger_bp,
|
||||
allow_headers=["Content-Type", "Authorization", "X-App-Code"],
|
||||
methods=["GET", "PUT", "POST", "DELETE", "OPTIONS", "PATCH", "HEAD"],
|
||||
expose_headers=list(EXPOSED_HEADERS),
|
||||
)
|
||||
app.register_blueprint(trigger_bp)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ from logging.handlers import RotatingFileHandler
|
|||
import flask
|
||||
|
||||
from configs import dify_config
|
||||
from core.helper.trace_id_helper import get_trace_id_from_otel_context
|
||||
from dify_app import DifyApp
|
||||
|
||||
|
||||
|
|
@ -76,7 +77,9 @@ class RequestIdFilter(logging.Filter):
|
|||
# the logging format. Note that we're checking if we're in a request
|
||||
# context, as we may want to log things before Flask is fully loaded.
|
||||
def filter(self, record):
|
||||
trace_id = get_trace_id_from_otel_context() or ""
|
||||
record.req_id = get_request_id() if flask.has_request_context() else ""
|
||||
record.trace_id = trace_id
|
||||
return True
|
||||
|
||||
|
||||
|
|
@ -84,6 +87,8 @@ class RequestIdFormatter(logging.Formatter):
|
|||
def format(self, record):
|
||||
if not hasattr(record, "req_id"):
|
||||
record.req_id = ""
|
||||
if not hasattr(record, "trace_id"):
|
||||
record.trace_id = ""
|
||||
return super().format(record)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
import json
|
||||
import logging
|
||||
import time
|
||||
|
||||
import flask
|
||||
import werkzeug.http
|
||||
from flask import Flask
|
||||
from flask import Flask, g
|
||||
from flask.signals import request_finished, request_started
|
||||
|
||||
from configs import dify_config
|
||||
from core.helper.trace_id_helper import get_trace_id_from_otel_context
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -20,6 +22,9 @@ def _is_content_type_json(content_type: str) -> bool:
|
|||
|
||||
def _log_request_started(_sender, **_extra):
|
||||
"""Log the start of a request."""
|
||||
# Record start time for access logging
|
||||
g.__request_started_ts = time.perf_counter()
|
||||
|
||||
if not logger.isEnabledFor(logging.DEBUG):
|
||||
return
|
||||
|
||||
|
|
@ -42,8 +47,39 @@ def _log_request_started(_sender, **_extra):
|
|||
|
||||
|
||||
def _log_request_finished(_sender, response, **_extra):
|
||||
"""Log the end of a request."""
|
||||
if not logger.isEnabledFor(logging.DEBUG) or response is None:
|
||||
"""Log the end of a request.
|
||||
|
||||
Safe to call with or without an active Flask request context.
|
||||
"""
|
||||
if response is None:
|
||||
return
|
||||
|
||||
# Always emit a compact access line at INFO with trace_id so it can be grepped
|
||||
has_ctx = flask.has_request_context()
|
||||
start_ts = getattr(g, "__request_started_ts", None) if has_ctx else None
|
||||
duration_ms = None
|
||||
if start_ts is not None:
|
||||
duration_ms = round((time.perf_counter() - start_ts) * 1000, 3)
|
||||
|
||||
# Request attributes are available only when a request context exists
|
||||
if has_ctx:
|
||||
req_method = flask.request.method
|
||||
req_path = flask.request.path
|
||||
else:
|
||||
req_method = "-"
|
||||
req_path = "-"
|
||||
|
||||
trace_id = get_trace_id_from_otel_context() or response.headers.get("X-Trace-Id") or ""
|
||||
logger.info(
|
||||
"%s %s %s %s %s",
|
||||
req_method,
|
||||
req_path,
|
||||
getattr(response, "status_code", "-"),
|
||||
duration_ms if duration_ms is not None else "-",
|
||||
trace_id,
|
||||
)
|
||||
|
||||
if not logger.isEnabledFor(logging.DEBUG):
|
||||
return
|
||||
|
||||
if not _is_content_type_json(response.content_type):
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ class StringUUID(TypeDecorator[uuid.UUID | str | None]):
|
|||
def process_bind_param(self, value: uuid.UUID | str | None, dialect: Dialect) -> str | None:
|
||||
if value is None:
|
||||
return value
|
||||
elif dialect.name == "postgresql":
|
||||
elif dialect.name in ["postgresql", "mysql"]:
|
||||
return str(value)
|
||||
else:
|
||||
if isinstance(value, uuid.UUID):
|
||||
|
|
|
|||
|
|
@ -233,7 +233,7 @@ workflow:
|
|||
- value_selector:
|
||||
- iteration_node
|
||||
- output
|
||||
value_type: array[array[number]]
|
||||
value_type: array[number]
|
||||
variable: output
|
||||
selected: false
|
||||
title: End
|
||||
|
|
|
|||
|
|
@ -7,9 +7,31 @@ This module tests the iteration node's ability to:
|
|||
"""
|
||||
|
||||
from .test_database_utils import skip_if_database_unavailable
|
||||
from .test_mock_config import MockConfigBuilder, NodeMockConfig
|
||||
from .test_table_runner import TableTestRunner, WorkflowTestCase
|
||||
|
||||
|
||||
def _create_iteration_mock_config():
|
||||
"""Helper to create a mock config for iteration tests."""
|
||||
|
||||
def code_inner_handler(node):
|
||||
pool = node.graph_runtime_state.variable_pool
|
||||
item_seg = pool.get(["iteration_node", "item"])
|
||||
if item_seg is not None:
|
||||
item = item_seg.to_object()
|
||||
return {"result": [item, item * 2]}
|
||||
# This fallback is likely unreachable, but if it is,
|
||||
# it doesn't simulate iteration with different values as the comment suggests.
|
||||
return {"result": [1, 2]}
|
||||
|
||||
return (
|
||||
MockConfigBuilder()
|
||||
.with_node_output("code_node", {"result": [1, 2, 3]})
|
||||
.with_node_config(NodeMockConfig(node_id="code_inner_node", custom_handler=code_inner_handler))
|
||||
.build()
|
||||
)
|
||||
|
||||
|
||||
@skip_if_database_unavailable()
|
||||
def test_iteration_with_flatten_output_enabled():
|
||||
"""
|
||||
|
|
@ -27,7 +49,8 @@ def test_iteration_with_flatten_output_enabled():
|
|||
inputs={},
|
||||
expected_outputs={"output": [1, 2, 2, 4, 3, 6]},
|
||||
description="Iteration with flatten_output=True flattens nested arrays",
|
||||
use_auto_mock=False, # Run code nodes directly
|
||||
use_auto_mock=True, # Use auto-mock to avoid sandbox service
|
||||
mock_config=_create_iteration_mock_config(),
|
||||
)
|
||||
|
||||
result = runner.run_test_case(test_case)
|
||||
|
|
@ -56,7 +79,8 @@ def test_iteration_with_flatten_output_disabled():
|
|||
inputs={},
|
||||
expected_outputs={"output": [[1, 2], [2, 4], [3, 6]]},
|
||||
description="Iteration with flatten_output=False preserves nested structure",
|
||||
use_auto_mock=False, # Run code nodes directly
|
||||
use_auto_mock=True, # Use auto-mock to avoid sandbox service
|
||||
mock_config=_create_iteration_mock_config(),
|
||||
)
|
||||
|
||||
result = runner.run_test_case(test_case)
|
||||
|
|
@ -81,14 +105,16 @@ def test_iteration_flatten_output_comparison():
|
|||
inputs={},
|
||||
expected_outputs={"output": [1, 2, 2, 4, 3, 6]},
|
||||
description="flatten_output=True: Flattened output",
|
||||
use_auto_mock=False, # Run code nodes directly
|
||||
use_auto_mock=True, # Use auto-mock to avoid sandbox service
|
||||
mock_config=_create_iteration_mock_config(),
|
||||
),
|
||||
WorkflowTestCase(
|
||||
fixture_path="iteration_flatten_output_disabled_workflow",
|
||||
inputs={},
|
||||
expected_outputs={"output": [[1, 2], [2, 4], [3, 6]]},
|
||||
description="flatten_output=False: Nested output",
|
||||
use_auto_mock=False, # Run code nodes directly
|
||||
use_auto_mock=True, # Use auto-mock to avoid sandbox service
|
||||
mock_config=_create_iteration_mock_config(),
|
||||
),
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -263,3 +263,62 @@ class TestResponseUnmodified:
|
|||
)
|
||||
assert response.text == _RESPONSE_NEEDLE
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
class TestRequestFinishedInfoAccessLine:
|
||||
def test_info_access_log_includes_method_path_status_duration_trace_id(self, monkeypatch, caplog):
|
||||
"""Ensure INFO access line contains expected fields with computed duration and trace id."""
|
||||
app = _get_test_app()
|
||||
# Push a real request context so flask.request and g are available
|
||||
with app.test_request_context("/foo", method="GET"):
|
||||
# Seed start timestamp via the extension's own start hook and control perf_counter deterministically
|
||||
seq = iter([100.0, 100.123456])
|
||||
monkeypatch.setattr(ext_request_logging.time, "perf_counter", lambda: next(seq))
|
||||
# Provide a deterministic trace id
|
||||
monkeypatch.setattr(
|
||||
ext_request_logging,
|
||||
"get_trace_id_from_otel_context",
|
||||
lambda: "trace-xyz",
|
||||
)
|
||||
# Simulate request_started to record start timestamp on g
|
||||
ext_request_logging._log_request_started(app)
|
||||
|
||||
# Capture logs from the real logger at INFO level only (skip DEBUG branch)
|
||||
caplog.set_level(logging.INFO, logger=ext_request_logging.__name__)
|
||||
response = Response(json.dumps({"ok": True}), mimetype="application/json", status=200)
|
||||
_log_request_finished(app, response)
|
||||
|
||||
# Verify a single INFO record with the five fields in order
|
||||
info_records = [rec for rec in caplog.records if rec.levelno == logging.INFO]
|
||||
assert len(info_records) == 1
|
||||
msg = info_records[0].getMessage()
|
||||
# Expected format: METHOD PATH STATUS DURATION_MS TRACE_ID
|
||||
assert "GET" in msg
|
||||
assert "/foo" in msg
|
||||
assert "200" in msg
|
||||
assert "123.456" in msg # rounded to 3 decimals
|
||||
assert "trace-xyz" in msg
|
||||
|
||||
def test_info_access_log_uses_dash_without_start_timestamp(self, monkeypatch, caplog):
|
||||
app = _get_test_app()
|
||||
with app.test_request_context("/bar", method="POST"):
|
||||
# No g.__request_started_ts set -> duration should be '-'
|
||||
monkeypatch.setattr(
|
||||
ext_request_logging,
|
||||
"get_trace_id_from_otel_context",
|
||||
lambda: "tid-no-start",
|
||||
)
|
||||
caplog.set_level(logging.INFO, logger=ext_request_logging.__name__)
|
||||
response = Response("OK", mimetype="text/plain", status=204)
|
||||
_log_request_finished(app, response)
|
||||
|
||||
info_records = [rec for rec in caplog.records if rec.levelno == logging.INFO]
|
||||
assert len(info_records) == 1
|
||||
msg = info_records[0].getMessage()
|
||||
assert "POST" in msg
|
||||
assert "/bar" in msg
|
||||
assert "204" in msg
|
||||
# Duration placeholder
|
||||
# The fields are space separated; ensure a standalone '-' appears
|
||||
assert " - " in msg or msg.endswith(" -")
|
||||
assert "tid-no-start" in msg
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import type { ReactNode } from 'react'
|
|||
import SwrInitializer from '@/app/components/swr-initializer'
|
||||
import { AppContextProvider } from '@/context/app-context'
|
||||
import GA, { GaType } from '@/app/components/base/ga'
|
||||
import AmplitudeProvider from '@/app/components/base/amplitude'
|
||||
import HeaderWrapper from '@/app/components/header/header-wrapper'
|
||||
import Header from '@/app/components/header'
|
||||
import { EventEmitterContextProvider } from '@/context/event-emitter'
|
||||
|
|
@ -18,6 +19,7 @@ const Layout = ({ children }: { children: ReactNode }) => {
|
|||
return (
|
||||
<>
|
||||
<GA gaType={GaType.admin} />
|
||||
<AmplitudeProvider />
|
||||
<SwrInitializer>
|
||||
<AppContextProvider>
|
||||
<EventEmitterContextProvider>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
'use client'
|
||||
import { useState } from 'react'
|
||||
import useSWR from 'swr'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiGraduationCapFill,
|
||||
|
|
@ -23,8 +22,9 @@ import PremiumBadge from '@/app/components/base/premium-badge'
|
|||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import EmailChangeModal from './email-change-modal'
|
||||
import { validPassword } from '@/config'
|
||||
import { fetchAppList } from '@/service/apps'
|
||||
|
||||
import type { App } from '@/types/app'
|
||||
import { useAppList } from '@/service/use-apps'
|
||||
|
||||
const titleClassName = `
|
||||
system-sm-semibold text-text-secondary
|
||||
|
|
@ -36,7 +36,7 @@ const descriptionClassName = `
|
|||
export default function AccountPage() {
|
||||
const { t } = useTranslation()
|
||||
const { systemFeatures } = useGlobalPublicStore()
|
||||
const { data: appList } = useSWR({ url: '/apps', params: { page: 1, limit: 100, name: '' } }, fetchAppList)
|
||||
const { data: appList } = useAppList({ page: 1, limit: 100, name: '' })
|
||||
const apps = appList?.data || []
|
||||
const { mutateUserProfile, userProfile } = useAppContext()
|
||||
const { isEducationAccount } = useProviderContext()
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { useProviderContext } from '@/context/provider-context'
|
|||
import { LogOut01 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import PremiumBadge from '@/app/components/base/premium-badge'
|
||||
import { useLogout } from '@/service/use-common'
|
||||
import { resetUser } from '@/app/components/base/amplitude/utils'
|
||||
|
||||
export type IAppSelector = {
|
||||
isMobile: boolean
|
||||
|
|
@ -28,6 +29,7 @@ export default function AppSelector() {
|
|||
await logout()
|
||||
|
||||
localStorage.removeItem('setup_status')
|
||||
resetUser()
|
||||
// Tokens are now stored in cookies and cleared by backend
|
||||
|
||||
router.push('/signin')
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import Header from './header'
|
|||
import SwrInitor from '@/app/components/swr-initializer'
|
||||
import { AppContextProvider } from '@/context/app-context'
|
||||
import GA, { GaType } from '@/app/components/base/ga'
|
||||
import AmplitudeProvider from '@/app/components/base/amplitude'
|
||||
import HeaderWrapper from '@/app/components/header/header-wrapper'
|
||||
import { EventEmitterContextProvider } from '@/context/event-emitter'
|
||||
import { ProviderContextProvider } from '@/context/provider-context'
|
||||
|
|
@ -13,6 +14,7 @@ const Layout = ({ children }: { children: ReactNode }) => {
|
|||
return (
|
||||
<>
|
||||
<GA gaType={GaType.admin} />
|
||||
<AmplitudeProvider />
|
||||
<SwrInitor>
|
||||
<AppContextProvider>
|
||||
<EventEmitterContextProvider>
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import Input from '@/app/components/base/input'
|
|||
import { AppModeEnum } from '@/types/app'
|
||||
import { DSLImportMode } from '@/models/app'
|
||||
import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks'
|
||||
import { trackEvent } from '@/app/components/base/amplitude'
|
||||
|
||||
type AppsProps = {
|
||||
onSuccess?: () => void
|
||||
|
|
@ -141,6 +142,15 @@ const Apps = ({
|
|||
icon_background,
|
||||
description,
|
||||
})
|
||||
|
||||
// Track app creation from template
|
||||
trackEvent('create_app_with_template', {
|
||||
app_mode: mode,
|
||||
template_id: currApp?.app.id,
|
||||
template_name: currApp?.app.name,
|
||||
description,
|
||||
})
|
||||
|
||||
setIsShowCreateModal(false)
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import { getRedirection } from '@/utils/app-redirection'
|
|||
import FullScreenModal from '@/app/components/base/fullscreen-modal'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
import { trackEvent } from '@/app/components/base/amplitude'
|
||||
|
||||
type CreateAppProps = {
|
||||
onSuccess: () => void
|
||||
|
|
@ -82,6 +83,13 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate, defaultAppMode }:
|
|||
icon_background: appIcon.type === 'emoji' ? appIcon.background : undefined,
|
||||
mode: appMode,
|
||||
})
|
||||
|
||||
// Track app creation success
|
||||
trackEvent('create_app', {
|
||||
app_mode: appMode,
|
||||
description,
|
||||
})
|
||||
|
||||
notify({ type: 'success', message: t('app.newApp.appCreated') })
|
||||
onSuccess()
|
||||
onClose()
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import { getRedirection } from '@/utils/app-redirection'
|
|||
import cn from '@/utils/classnames'
|
||||
import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks'
|
||||
import { noop } from 'lodash-es'
|
||||
import { trackEvent } from '@/app/components/base/amplitude'
|
||||
|
||||
type CreateFromDSLModalProps = {
|
||||
show: boolean
|
||||
|
|
@ -112,6 +113,13 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
|
|||
return
|
||||
const { id, status, app_id, app_mode, imported_dsl_version, current_dsl_version } = response
|
||||
if (status === DSLImportStatus.COMPLETED || status === DSLImportStatus.COMPLETED_WITH_WARNINGS) {
|
||||
// Track app creation from DSL import
|
||||
trackEvent('create_app_with_dsl', {
|
||||
app_mode,
|
||||
creation_method: currentTab === CreateFromDSLModalTab.FROM_FILE ? 'dsl_file' : 'dsl_url',
|
||||
has_warnings: status === DSLImportStatus.COMPLETED_WITH_WARNINGS,
|
||||
})
|
||||
|
||||
if (onSuccess)
|
||||
onSuccess()
|
||||
if (onClose)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import type { FC } from 'react'
|
|||
import React from 'react'
|
||||
import ReactECharts from 'echarts-for-react'
|
||||
import type { EChartsOption } from 'echarts'
|
||||
import useSWR from 'swr'
|
||||
import type { Dayjs } from 'dayjs'
|
||||
import dayjs from 'dayjs'
|
||||
import { get } from 'lodash-es'
|
||||
|
|
@ -13,7 +12,20 @@ import { formatNumber } from '@/utils/format'
|
|||
import Basic from '@/app/components/app-sidebar/basic'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import type { AppDailyConversationsResponse, AppDailyEndUsersResponse, AppDailyMessagesResponse, AppTokenCostsResponse } from '@/models/app'
|
||||
import { getAppDailyConversations, getAppDailyEndUsers, getAppDailyMessages, getAppStatistics, getAppTokenCosts, getWorkflowDailyConversations } from '@/service/apps'
|
||||
import {
|
||||
useAppAverageResponseTime,
|
||||
useAppAverageSessionInteractions,
|
||||
useAppDailyConversations,
|
||||
useAppDailyEndUsers,
|
||||
useAppDailyMessages,
|
||||
useAppSatisfactionRate,
|
||||
useAppTokenCosts,
|
||||
useAppTokensPerSecond,
|
||||
useWorkflowAverageInteractions,
|
||||
useWorkflowDailyConversations,
|
||||
useWorkflowDailyTerminals,
|
||||
useWorkflowTokenCosts,
|
||||
} from '@/service/use-apps'
|
||||
const valueFormatter = (v: string | number) => v
|
||||
|
||||
const COLOR_TYPE_MAP = {
|
||||
|
|
@ -272,8 +284,8 @@ const getDefaultChartData = ({ start, end, key = 'count' }: { start: string; end
|
|||
|
||||
export const MessagesChart: FC<IBizChartProps> = ({ id, period }) => {
|
||||
const { t } = useTranslation()
|
||||
const { data: response } = useSWR({ url: `/apps/${id}/statistics/daily-messages`, params: period.query }, getAppDailyMessages)
|
||||
if (!response)
|
||||
const { data: response, isLoading } = useAppDailyMessages(id, period.query)
|
||||
if (isLoading || !response)
|
||||
return <Loading />
|
||||
const noDataFlag = !response.data || response.data.length === 0
|
||||
return <Chart
|
||||
|
|
@ -286,8 +298,8 @@ export const MessagesChart: FC<IBizChartProps> = ({ id, period }) => {
|
|||
|
||||
export const ConversationsChart: FC<IBizChartProps> = ({ id, period }) => {
|
||||
const { t } = useTranslation()
|
||||
const { data: response } = useSWR({ url: `/apps/${id}/statistics/daily-conversations`, params: period.query }, getAppDailyConversations)
|
||||
if (!response)
|
||||
const { data: response, isLoading } = useAppDailyConversations(id, period.query)
|
||||
if (isLoading || !response)
|
||||
return <Loading />
|
||||
const noDataFlag = !response.data || response.data.length === 0
|
||||
return <Chart
|
||||
|
|
@ -301,8 +313,8 @@ export const ConversationsChart: FC<IBizChartProps> = ({ id, period }) => {
|
|||
export const EndUsersChart: FC<IBizChartProps> = ({ id, period }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { data: response } = useSWR({ url: `/apps/${id}/statistics/daily-end-users`, id, params: period.query }, getAppDailyEndUsers)
|
||||
if (!response)
|
||||
const { data: response, isLoading } = useAppDailyEndUsers(id, period.query)
|
||||
if (isLoading || !response)
|
||||
return <Loading />
|
||||
const noDataFlag = !response.data || response.data.length === 0
|
||||
return <Chart
|
||||
|
|
@ -315,8 +327,8 @@ export const EndUsersChart: FC<IBizChartProps> = ({ id, period }) => {
|
|||
|
||||
export const AvgSessionInteractions: FC<IBizChartProps> = ({ id, period }) => {
|
||||
const { t } = useTranslation()
|
||||
const { data: response } = useSWR({ url: `/apps/${id}/statistics/average-session-interactions`, params: period.query }, getAppStatistics)
|
||||
if (!response)
|
||||
const { data: response, isLoading } = useAppAverageSessionInteractions(id, period.query)
|
||||
if (isLoading || !response)
|
||||
return <Loading />
|
||||
const noDataFlag = !response.data || response.data.length === 0
|
||||
return <Chart
|
||||
|
|
@ -331,8 +343,8 @@ export const AvgSessionInteractions: FC<IBizChartProps> = ({ id, period }) => {
|
|||
|
||||
export const AvgResponseTime: FC<IBizChartProps> = ({ id, period }) => {
|
||||
const { t } = useTranslation()
|
||||
const { data: response } = useSWR({ url: `/apps/${id}/statistics/average-response-time`, params: period.query }, getAppStatistics)
|
||||
if (!response)
|
||||
const { data: response, isLoading } = useAppAverageResponseTime(id, period.query)
|
||||
if (isLoading || !response)
|
||||
return <Loading />
|
||||
const noDataFlag = !response.data || response.data.length === 0
|
||||
return <Chart
|
||||
|
|
@ -348,8 +360,8 @@ export const AvgResponseTime: FC<IBizChartProps> = ({ id, period }) => {
|
|||
|
||||
export const TokenPerSecond: FC<IBizChartProps> = ({ id, period }) => {
|
||||
const { t } = useTranslation()
|
||||
const { data: response } = useSWR({ url: `/apps/${id}/statistics/tokens-per-second`, params: period.query }, getAppStatistics)
|
||||
if (!response)
|
||||
const { data: response, isLoading } = useAppTokensPerSecond(id, period.query)
|
||||
if (isLoading || !response)
|
||||
return <Loading />
|
||||
const noDataFlag = !response.data || response.data.length === 0
|
||||
return <Chart
|
||||
|
|
@ -366,8 +378,8 @@ export const TokenPerSecond: FC<IBizChartProps> = ({ id, period }) => {
|
|||
|
||||
export const UserSatisfactionRate: FC<IBizChartProps> = ({ id, period }) => {
|
||||
const { t } = useTranslation()
|
||||
const { data: response } = useSWR({ url: `/apps/${id}/statistics/user-satisfaction-rate`, params: period.query }, getAppStatistics)
|
||||
if (!response)
|
||||
const { data: response, isLoading } = useAppSatisfactionRate(id, period.query)
|
||||
if (isLoading || !response)
|
||||
return <Loading />
|
||||
const noDataFlag = !response.data || response.data.length === 0
|
||||
return <Chart
|
||||
|
|
@ -384,8 +396,8 @@ export const UserSatisfactionRate: FC<IBizChartProps> = ({ id, period }) => {
|
|||
export const CostChart: FC<IBizChartProps> = ({ id, period }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { data: response } = useSWR({ url: `/apps/${id}/statistics/token-costs`, params: period.query }, getAppTokenCosts)
|
||||
if (!response)
|
||||
const { data: response, isLoading } = useAppTokenCosts(id, period.query)
|
||||
if (isLoading || !response)
|
||||
return <Loading />
|
||||
const noDataFlag = !response.data || response.data.length === 0
|
||||
return <Chart
|
||||
|
|
@ -398,8 +410,8 @@ export const CostChart: FC<IBizChartProps> = ({ id, period }) => {
|
|||
|
||||
export const WorkflowMessagesChart: FC<IBizChartProps> = ({ id, period }) => {
|
||||
const { t } = useTranslation()
|
||||
const { data: response } = useSWR({ url: `/apps/${id}/workflow/statistics/daily-conversations`, params: period.query }, getWorkflowDailyConversations)
|
||||
if (!response)
|
||||
const { data: response, isLoading } = useWorkflowDailyConversations(id, period.query)
|
||||
if (isLoading || !response)
|
||||
return <Loading />
|
||||
const noDataFlag = !response.data || response.data.length === 0
|
||||
return <Chart
|
||||
|
|
@ -414,8 +426,8 @@ export const WorkflowMessagesChart: FC<IBizChartProps> = ({ id, period }) => {
|
|||
export const WorkflowDailyTerminalsChart: FC<IBizChartProps> = ({ id, period }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { data: response } = useSWR({ url: `/apps/${id}/workflow/statistics/daily-terminals`, id, params: period.query }, getAppDailyEndUsers)
|
||||
if (!response)
|
||||
const { data: response, isLoading } = useWorkflowDailyTerminals(id, period.query)
|
||||
if (isLoading || !response)
|
||||
return <Loading />
|
||||
const noDataFlag = !response.data || response.data.length === 0
|
||||
return <Chart
|
||||
|
|
@ -429,8 +441,8 @@ export const WorkflowDailyTerminalsChart: FC<IBizChartProps> = ({ id, period })
|
|||
export const WorkflowCostChart: FC<IBizChartProps> = ({ id, period }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { data: response } = useSWR({ url: `/apps/${id}/workflow/statistics/token-costs`, params: period.query }, getAppTokenCosts)
|
||||
if (!response)
|
||||
const { data: response, isLoading } = useWorkflowTokenCosts(id, period.query)
|
||||
if (isLoading || !response)
|
||||
return <Loading />
|
||||
const noDataFlag = !response.data || response.data.length === 0
|
||||
return <Chart
|
||||
|
|
@ -443,8 +455,8 @@ export const WorkflowCostChart: FC<IBizChartProps> = ({ id, period }) => {
|
|||
|
||||
export const AvgUserInteractions: FC<IBizChartProps> = ({ id, period }) => {
|
||||
const { t } = useTranslation()
|
||||
const { data: response } = useSWR({ url: `/apps/${id}/workflow/statistics/average-app-interactions`, params: period.query }, getAppStatistics)
|
||||
if (!response)
|
||||
const { data: response, isLoading } = useWorkflowAverageInteractions(id, period.query)
|
||||
if (isLoading || !response)
|
||||
return <Loading />
|
||||
const noDataFlag = !response.data || response.data.length === 0
|
||||
return <Chart
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import quarterOfYear from 'dayjs/plugin/quarterOfYear'
|
|||
import type { QueryParam } from './index'
|
||||
import Chip from '@/app/components/base/chip'
|
||||
import Input from '@/app/components/base/input'
|
||||
import { trackEvent } from '@/app/components/base/amplitude/utils'
|
||||
dayjs.extend(quarterOfYear)
|
||||
|
||||
const today = dayjs()
|
||||
|
|
@ -37,6 +38,9 @@ const Filter: FC<IFilterProps> = ({ queryParams, setQueryParams }: IFilterProps)
|
|||
value={queryParams.status || 'all'}
|
||||
onSelect={(item) => {
|
||||
setQueryParams({ ...queryParams, status: item.value as string })
|
||||
trackEvent('workflow_log_filter_status_selected', {
|
||||
workflow_log_filter_status: item.value as string,
|
||||
})
|
||||
}}
|
||||
onClear={() => setQueryParams({ ...queryParams, status: 'all' })}
|
||||
items={[{ value: 'all', name: 'All' },
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ const Empty = () => {
|
|||
return (
|
||||
<>
|
||||
<DefaultCards />
|
||||
<div className='absolute inset-0 z-20 flex items-center justify-center bg-gradient-to-t from-background-body to-transparent pointer-events-none'>
|
||||
<div className='pointer-events-none absolute inset-0 z-20 flex items-center justify-center bg-gradient-to-t from-background-body to-transparent'>
|
||||
<span className='system-md-medium text-text-tertiary'>
|
||||
{t('app.newApp.noAppsFound')}
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import { useCallback, useEffect, useRef, useState } from 'react'
|
|||
import {
|
||||
useRouter,
|
||||
} from 'next/navigation'
|
||||
import useSWRInfinite from 'swr/infinite'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useDebounceFn } from 'ahooks'
|
||||
import {
|
||||
|
|
@ -19,8 +18,6 @@ import AppCard from './app-card'
|
|||
import NewAppCard from './new-app-card'
|
||||
import useAppsQueryState from './hooks/use-apps-query-state'
|
||||
import { useDSLDragDrop } from './hooks/use-dsl-drag-drop'
|
||||
import type { AppListResponse } from '@/models/app'
|
||||
import { fetchAppList } from '@/service/apps'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||
import { CheckModal } from '@/hooks/use-pay'
|
||||
|
|
@ -35,6 +32,7 @@ import Empty from './empty'
|
|||
import Footer from './footer'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { useInfiniteAppList } from '@/service/use-apps'
|
||||
|
||||
const TagManagementModal = dynamic(() => import('@/app/components/base/tag-management'), {
|
||||
ssr: false,
|
||||
|
|
@ -43,30 +41,6 @@ const CreateFromDSLModal = dynamic(() => import('@/app/components/app/create-fro
|
|||
ssr: false,
|
||||
})
|
||||
|
||||
const getKey = (
|
||||
pageIndex: number,
|
||||
previousPageData: AppListResponse,
|
||||
activeTab: string,
|
||||
isCreatedByMe: boolean,
|
||||
tags: string[],
|
||||
keywords: string,
|
||||
) => {
|
||||
if (!pageIndex || previousPageData.has_more) {
|
||||
const params: any = { url: 'apps', params: { page: pageIndex + 1, limit: 30, name: keywords, is_created_by_me: isCreatedByMe } }
|
||||
|
||||
if (activeTab !== 'all')
|
||||
params.params.mode = activeTab
|
||||
else
|
||||
delete params.params.mode
|
||||
|
||||
if (tags.length)
|
||||
params.params.tag_ids = tags
|
||||
|
||||
return params
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const List = () => {
|
||||
const { t } = useTranslation()
|
||||
const { systemFeatures } = useGlobalPublicStore()
|
||||
|
|
@ -102,16 +76,24 @@ const List = () => {
|
|||
enabled: isCurrentWorkspaceEditor,
|
||||
})
|
||||
|
||||
const { data, isLoading, error, setSize, mutate } = useSWRInfinite(
|
||||
(pageIndex: number, previousPageData: AppListResponse) => getKey(pageIndex, previousPageData, activeTab, isCreatedByMe, tagIDs, searchKeywords),
|
||||
fetchAppList,
|
||||
{
|
||||
revalidateFirstPage: true,
|
||||
shouldRetryOnError: false,
|
||||
dedupingInterval: 500,
|
||||
errorRetryCount: 3,
|
||||
},
|
||||
)
|
||||
const appListQueryParams = {
|
||||
page: 1,
|
||||
limit: 30,
|
||||
name: searchKeywords,
|
||||
tag_ids: tagIDs,
|
||||
is_created_by_me: isCreatedByMe,
|
||||
...(activeTab !== 'all' ? { mode: activeTab as AppModeEnum } : {}),
|
||||
}
|
||||
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
isFetchingNextPage,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
error,
|
||||
refetch,
|
||||
} = useInfiniteAppList(appListQueryParams, { enabled: !isCurrentWorkspaceDatasetOperator })
|
||||
|
||||
const anchorRef = useRef<HTMLDivElement>(null)
|
||||
const options = [
|
||||
|
|
@ -126,9 +108,9 @@ const List = () => {
|
|||
useEffect(() => {
|
||||
if (localStorage.getItem(NEED_REFRESH_APP_LIST_KEY) === '1') {
|
||||
localStorage.removeItem(NEED_REFRESH_APP_LIST_KEY)
|
||||
mutate()
|
||||
refetch()
|
||||
}
|
||||
}, [mutate, t])
|
||||
}, [refetch])
|
||||
|
||||
useEffect(() => {
|
||||
if (isCurrentWorkspaceDatasetOperator)
|
||||
|
|
@ -136,7 +118,9 @@ const List = () => {
|
|||
}, [router, isCurrentWorkspaceDatasetOperator])
|
||||
|
||||
useEffect(() => {
|
||||
const hasMore = data?.at(-1)?.has_more ?? true
|
||||
if (isCurrentWorkspaceDatasetOperator)
|
||||
return
|
||||
const hasMore = hasNextPage ?? true
|
||||
let observer: IntersectionObserver | undefined
|
||||
|
||||
if (error) {
|
||||
|
|
@ -151,8 +135,8 @@ const List = () => {
|
|||
const dynamicMargin = Math.max(100, Math.min(containerHeight * 0.2, 200)) // Clamps to 100-200px range, using 20% of container height as the base value
|
||||
|
||||
observer = new IntersectionObserver((entries) => {
|
||||
if (entries[0].isIntersecting && !isLoading && !error && hasMore)
|
||||
setSize((size: number) => size + 1)
|
||||
if (entries[0].isIntersecting && !isLoading && !isFetchingNextPage && !error && hasMore)
|
||||
fetchNextPage()
|
||||
}, {
|
||||
root: containerRef.current,
|
||||
rootMargin: `${dynamicMargin}px`,
|
||||
|
|
@ -161,7 +145,7 @@ const List = () => {
|
|||
observer.observe(anchorRef.current)
|
||||
}
|
||||
return () => observer?.disconnect()
|
||||
}, [isLoading, setSize, data, error])
|
||||
}, [isLoading, isFetchingNextPage, fetchNextPage, error, hasNextPage, isCurrentWorkspaceDatasetOperator])
|
||||
|
||||
const { run: handleSearch } = useDebounceFn(() => {
|
||||
setSearchKeywords(keywords)
|
||||
|
|
@ -185,6 +169,9 @@ const List = () => {
|
|||
setQuery(prev => ({ ...prev, isCreatedByMe: newValue }))
|
||||
}, [isCreatedByMe, setQuery])
|
||||
|
||||
const pages = data?.pages ?? []
|
||||
const hasAnyApp = (pages[0]?.total ?? 0) > 0
|
||||
|
||||
return (
|
||||
<>
|
||||
<div ref={containerRef} className='relative flex h-0 shrink-0 grow flex-col overflow-y-auto bg-background-body'>
|
||||
|
|
@ -217,17 +204,17 @@ const List = () => {
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
{(data && data[0].total > 0)
|
||||
{hasAnyApp
|
||||
? <div className='relative grid grow grid-cols-1 content-start gap-4 px-12 pt-2 sm:grid-cols-1 md:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-5 2k:grid-cols-6'>
|
||||
{isCurrentWorkspaceEditor
|
||||
&& <NewAppCard ref={newAppCardRef} onSuccess={mutate} selectedAppType={activeTab} />}
|
||||
{data.map(({ data: apps }) => apps.map(app => (
|
||||
<AppCard key={app.id} app={app} onRefresh={mutate} />
|
||||
&& <NewAppCard ref={newAppCardRef} onSuccess={refetch} selectedAppType={activeTab} />}
|
||||
{pages.map(({ data: apps }) => apps.map(app => (
|
||||
<AppCard key={app.id} app={app} onRefresh={refetch} />
|
||||
)))}
|
||||
</div>
|
||||
: <div className='relative grid grow grid-cols-1 content-start gap-4 overflow-hidden px-12 pt-2 sm:grid-cols-1 md:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-5 2k:grid-cols-6'>
|
||||
{isCurrentWorkspaceEditor
|
||||
&& <NewAppCard ref={newAppCardRef} className='z-10' onSuccess={mutate} selectedAppType={activeTab} />}
|
||||
&& <NewAppCard ref={newAppCardRef} className='z-10' onSuccess={refetch} selectedAppType={activeTab} />}
|
||||
<Empty />
|
||||
</div>}
|
||||
|
||||
|
|
@ -261,7 +248,7 @@ const List = () => {
|
|||
onSuccess={() => {
|
||||
setShowCreateFromDSLModal(false)
|
||||
setDroppedDSLFile(undefined)
|
||||
mutate()
|
||||
refetch()
|
||||
}}
|
||||
droppedFile={droppedDSLFile}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
'use client'
|
||||
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect } from 'react'
|
||||
import * as amplitude from '@amplitude/analytics-browser'
|
||||
import { sessionReplayPlugin } from '@amplitude/plugin-session-replay-browser'
|
||||
import { IS_CLOUD_EDITION } from '@/config'
|
||||
|
||||
export type IAmplitudeProps = {
|
||||
apiKey?: string
|
||||
sessionReplaySampleRate?: number
|
||||
}
|
||||
|
||||
const AmplitudeProvider: FC<IAmplitudeProps> = ({
|
||||
apiKey = process.env.NEXT_PUBLIC_AMPLITUDE_API_KEY ?? '',
|
||||
sessionReplaySampleRate = 1,
|
||||
}) => {
|
||||
useEffect(() => {
|
||||
// Only enable in Saas edition
|
||||
if (!IS_CLOUD_EDITION)
|
||||
return
|
||||
|
||||
// Initialize Amplitude
|
||||
amplitude.init(apiKey, {
|
||||
defaultTracking: {
|
||||
sessions: true,
|
||||
pageViews: true,
|
||||
formInteractions: true,
|
||||
fileDownloads: true,
|
||||
},
|
||||
// Enable debug logs in development environment
|
||||
logLevel: amplitude.Types.LogLevel.Warn,
|
||||
})
|
||||
|
||||
// Add Session Replay plugin
|
||||
const sessionReplay = sessionReplayPlugin({
|
||||
sampleRate: sessionReplaySampleRate,
|
||||
})
|
||||
amplitude.add(sessionReplay)
|
||||
}, [])
|
||||
|
||||
// This is a client component that renders nothing
|
||||
return null
|
||||
}
|
||||
|
||||
export default React.memo(AmplitudeProvider)
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export { default } from './AmplitudeProvider'
|
||||
export { resetUser, setUserId, setUserProperties, trackEvent } from './utils'
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
import * as amplitude from '@amplitude/analytics-browser'
|
||||
|
||||
/**
|
||||
* Track custom event
|
||||
* @param eventName Event name
|
||||
* @param eventProperties Event properties (optional)
|
||||
*/
|
||||
export const trackEvent = (eventName: string, eventProperties?: Record<string, any>) => {
|
||||
amplitude.track(eventName, eventProperties)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set user ID
|
||||
* @param userId User ID
|
||||
*/
|
||||
export const setUserId = (userId: string) => {
|
||||
amplitude.setUserId(userId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set user properties
|
||||
* @param properties User properties
|
||||
*/
|
||||
export const setUserProperties = (properties: Record<string, any>) => {
|
||||
const identifyEvent = new amplitude.Identify()
|
||||
Object.entries(properties).forEach(([key, value]) => {
|
||||
identifyEvent.set(key, value)
|
||||
})
|
||||
amplitude.identify(identifyEvent)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset user (e.g., when user logs out)
|
||||
*/
|
||||
export const resetUser = () => {
|
||||
amplitude.reset()
|
||||
}
|
||||
|
|
@ -11,7 +11,10 @@ import {
|
|||
RiThumbDownLine,
|
||||
RiThumbUpLine,
|
||||
} from '@remixicon/react'
|
||||
import type { ChatItem } from '../../types'
|
||||
import type {
|
||||
ChatItem,
|
||||
Feedback,
|
||||
} from '../../types'
|
||||
import { useChatContext } from '../context'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
|
|
@ -22,6 +25,7 @@ import ActionButton, { ActionButtonState } from '@/app/components/base/action-bu
|
|||
import NewAudioButton from '@/app/components/base/new-audio-button'
|
||||
import Modal from '@/app/components/base/modal/modal'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type OperationProps = {
|
||||
|
|
@ -66,8 +70,9 @@ const Operation: FC<OperationProps> = ({
|
|||
adminFeedback,
|
||||
agent_thoughts,
|
||||
} = item
|
||||
const [localFeedback, setLocalFeedback] = useState(config?.supportAnnotation ? adminFeedback : feedback)
|
||||
const [userLocalFeedback, setUserLocalFeedback] = useState(feedback)
|
||||
const [adminLocalFeedback, setAdminLocalFeedback] = useState(adminFeedback)
|
||||
const [feedbackTarget, setFeedbackTarget] = useState<'user' | 'admin'>('user')
|
||||
|
||||
// Separate feedback types for display
|
||||
const userFeedback = feedback
|
||||
|
|
@ -79,24 +84,68 @@ const Operation: FC<OperationProps> = ({
|
|||
return messageContent
|
||||
}, [agent_thoughts, messageContent])
|
||||
|
||||
const handleFeedback = async (rating: 'like' | 'dislike' | null, content?: string) => {
|
||||
const displayUserFeedback = userLocalFeedback ?? userFeedback
|
||||
|
||||
const hasUserFeedback = !!displayUserFeedback?.rating
|
||||
const hasAdminFeedback = !!adminLocalFeedback?.rating
|
||||
|
||||
const shouldShowUserFeedbackBar = !isOpeningStatement && config?.supportFeedback && !!onFeedback && !config?.supportAnnotation
|
||||
const shouldShowAdminFeedbackBar = !isOpeningStatement && config?.supportFeedback && !!onFeedback && !!config?.supportAnnotation
|
||||
|
||||
const userFeedbackLabel = t('appLog.table.header.userRate') || 'User feedback'
|
||||
const adminFeedbackLabel = t('appLog.table.header.adminRate') || 'Admin feedback'
|
||||
const feedbackTooltipClassName = 'max-w-[260px]'
|
||||
|
||||
const buildFeedbackTooltip = (feedbackData?: Feedback | null, label = userFeedbackLabel) => {
|
||||
if (!feedbackData?.rating)
|
||||
return label
|
||||
|
||||
const ratingLabel = feedbackData.rating === 'like'
|
||||
? (t('appLog.detail.operation.like') || 'like')
|
||||
: (t('appLog.detail.operation.dislike') || 'dislike')
|
||||
const feedbackText = feedbackData.content?.trim()
|
||||
|
||||
if (feedbackText)
|
||||
return `${label}: ${ratingLabel} - ${feedbackText}`
|
||||
|
||||
return `${label}: ${ratingLabel}`
|
||||
}
|
||||
|
||||
const handleFeedback = async (rating: 'like' | 'dislike' | null, content?: string, target: 'user' | 'admin' = 'user') => {
|
||||
if (!config?.supportFeedback || !onFeedback)
|
||||
return
|
||||
|
||||
await onFeedback?.(id, { rating, content })
|
||||
setLocalFeedback({ rating })
|
||||
|
||||
// Update admin feedback state separately if annotation is supported
|
||||
if (config?.supportAnnotation)
|
||||
setAdminLocalFeedback(rating ? { rating } : undefined)
|
||||
const nextFeedback = rating === null ? { rating: null } : { rating, content }
|
||||
|
||||
if (target === 'admin')
|
||||
setAdminLocalFeedback(nextFeedback)
|
||||
else
|
||||
setUserLocalFeedback(nextFeedback)
|
||||
}
|
||||
|
||||
const handleThumbsDown = () => {
|
||||
const handleLikeClick = (target: 'user' | 'admin') => {
|
||||
const currentRating = target === 'admin' ? adminLocalFeedback?.rating : displayUserFeedback?.rating
|
||||
if (currentRating === 'like') {
|
||||
handleFeedback(null, undefined, target)
|
||||
return
|
||||
}
|
||||
handleFeedback('like', undefined, target)
|
||||
}
|
||||
|
||||
const handleDislikeClick = (target: 'user' | 'admin') => {
|
||||
const currentRating = target === 'admin' ? adminLocalFeedback?.rating : displayUserFeedback?.rating
|
||||
if (currentRating === 'dislike') {
|
||||
handleFeedback(null, undefined, target)
|
||||
return
|
||||
}
|
||||
setFeedbackTarget(target)
|
||||
setIsShowFeedbackModal(true)
|
||||
}
|
||||
|
||||
const handleFeedbackSubmit = async () => {
|
||||
await handleFeedback('dislike', feedbackContent)
|
||||
await handleFeedback('dislike', feedbackContent, feedbackTarget)
|
||||
setFeedbackContent('')
|
||||
setIsShowFeedbackModal(false)
|
||||
}
|
||||
|
|
@ -116,12 +165,13 @@ const Operation: FC<OperationProps> = ({
|
|||
width += 26
|
||||
if (!isOpeningStatement && config?.supportAnnotation && config?.annotation_reply?.enabled)
|
||||
width += 26
|
||||
if (config?.supportFeedback && !localFeedback?.rating && onFeedback && !isOpeningStatement)
|
||||
width += 60 + 8
|
||||
if (config?.supportFeedback && localFeedback?.rating && onFeedback && !isOpeningStatement)
|
||||
width += 28 + 8
|
||||
if (shouldShowUserFeedbackBar)
|
||||
width += hasUserFeedback ? 28 + 8 : 60 + 8
|
||||
if (shouldShowAdminFeedbackBar)
|
||||
width += (hasAdminFeedback ? 28 : 60) + 8 + (hasUserFeedback ? 28 : 0)
|
||||
|
||||
return width
|
||||
}, [isOpeningStatement, showPromptLog, config?.text_to_speech?.enabled, config?.supportAnnotation, config?.annotation_reply?.enabled, config?.supportFeedback, localFeedback?.rating, onFeedback])
|
||||
}, [config?.annotation_reply?.enabled, config?.supportAnnotation, config?.text_to_speech?.enabled, hasAdminFeedback, hasUserFeedback, isOpeningStatement, shouldShowAdminFeedbackBar, shouldShowUserFeedbackBar, showPromptLog])
|
||||
|
||||
const positionRight = useMemo(() => operationWidth < maxSize, [operationWidth, maxSize])
|
||||
|
||||
|
|
@ -136,6 +186,110 @@ const Operation: FC<OperationProps> = ({
|
|||
)}
|
||||
style={(!hasWorkflowProcess && positionRight) ? { left: contentWidth + 8 } : {}}
|
||||
>
|
||||
{shouldShowUserFeedbackBar && (
|
||||
<div className={cn(
|
||||
'ml-1 items-center gap-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 shadow-md backdrop-blur-sm',
|
||||
hasUserFeedback ? 'flex' : 'hidden group-hover:flex',
|
||||
)}>
|
||||
{hasUserFeedback ? (
|
||||
<Tooltip
|
||||
popupContent={buildFeedbackTooltip(displayUserFeedback, userFeedbackLabel)}
|
||||
popupClassName={feedbackTooltipClassName}
|
||||
>
|
||||
<ActionButton
|
||||
state={displayUserFeedback?.rating === 'like' ? ActionButtonState.Active : ActionButtonState.Destructive}
|
||||
onClick={() => handleFeedback(null, undefined, 'user')}
|
||||
>
|
||||
{displayUserFeedback?.rating === 'like'
|
||||
? <RiThumbUpLine className='h-4 w-4' />
|
||||
: <RiThumbDownLine className='h-4 w-4' />}
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<>
|
||||
<ActionButton
|
||||
state={displayUserFeedback?.rating === 'like' ? ActionButtonState.Active : ActionButtonState.Default}
|
||||
onClick={() => handleLikeClick('user')}
|
||||
>
|
||||
<RiThumbUpLine className='h-4 w-4' />
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
state={displayUserFeedback?.rating === 'dislike' ? ActionButtonState.Destructive : ActionButtonState.Default}
|
||||
onClick={() => handleDislikeClick('user')}
|
||||
>
|
||||
<RiThumbDownLine className='h-4 w-4' />
|
||||
</ActionButton>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{shouldShowAdminFeedbackBar && (
|
||||
<div className={cn(
|
||||
'ml-1 items-center gap-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 shadow-md backdrop-blur-sm',
|
||||
(hasAdminFeedback || hasUserFeedback) ? 'flex' : 'hidden group-hover:flex',
|
||||
)}>
|
||||
{/* User Feedback Display */}
|
||||
{displayUserFeedback?.rating && (
|
||||
<Tooltip
|
||||
popupContent={buildFeedbackTooltip(displayUserFeedback, userFeedbackLabel)}
|
||||
popupClassName={feedbackTooltipClassName}
|
||||
>
|
||||
{displayUserFeedback.rating === 'like' ? (
|
||||
<ActionButton state={ActionButtonState.Active}>
|
||||
<RiThumbUpLine className='h-4 w-4' />
|
||||
</ActionButton>
|
||||
) : (
|
||||
<ActionButton state={ActionButtonState.Destructive}>
|
||||
<RiThumbDownLine className='h-4 w-4' />
|
||||
</ActionButton>
|
||||
)}
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{/* Admin Feedback Controls */}
|
||||
{displayUserFeedback?.rating && <div className='mx-1 h-3 w-[0.5px] bg-components-actionbar-border' />}
|
||||
{hasAdminFeedback ? (
|
||||
<Tooltip
|
||||
popupContent={buildFeedbackTooltip(adminLocalFeedback, adminFeedbackLabel)}
|
||||
popupClassName={feedbackTooltipClassName}
|
||||
>
|
||||
<ActionButton
|
||||
state={adminLocalFeedback?.rating === 'like' ? ActionButtonState.Active : ActionButtonState.Destructive}
|
||||
onClick={() => handleFeedback(null, undefined, 'admin')}
|
||||
>
|
||||
{adminLocalFeedback?.rating === 'like'
|
||||
? <RiThumbUpLine className='h-4 w-4' />
|
||||
: <RiThumbDownLine className='h-4 w-4' />}
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<>
|
||||
<Tooltip
|
||||
popupContent={buildFeedbackTooltip(adminLocalFeedback, adminFeedbackLabel)}
|
||||
popupClassName={feedbackTooltipClassName}
|
||||
>
|
||||
<ActionButton
|
||||
state={adminLocalFeedback?.rating === 'like' ? ActionButtonState.Active : ActionButtonState.Default}
|
||||
onClick={() => handleLikeClick('admin')}
|
||||
>
|
||||
<RiThumbUpLine className='h-4 w-4' />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
popupContent={buildFeedbackTooltip(adminLocalFeedback, adminFeedbackLabel)}
|
||||
popupClassName={feedbackTooltipClassName}
|
||||
>
|
||||
<ActionButton
|
||||
state={adminLocalFeedback?.rating === 'dislike' ? ActionButtonState.Destructive : ActionButtonState.Default}
|
||||
onClick={() => handleDislikeClick('admin')}
|
||||
>
|
||||
<RiThumbDownLine className='h-4 w-4' />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{showPromptLog && !isOpeningStatement && (
|
||||
<div className='hidden group-hover:block'>
|
||||
<Log logItem={item} />
|
||||
|
|
@ -174,69 +328,6 @@ const Operation: FC<OperationProps> = ({
|
|||
)}
|
||||
</div>
|
||||
)}
|
||||
{!isOpeningStatement && config?.supportFeedback && !localFeedback?.rating && onFeedback && (
|
||||
<div className='ml-1 hidden items-center gap-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 shadow-md backdrop-blur-sm group-hover:flex'>
|
||||
{!localFeedback?.rating && (
|
||||
<>
|
||||
<ActionButton onClick={() => handleFeedback('like')}>
|
||||
<RiThumbUpLine className='h-4 w-4' />
|
||||
</ActionButton>
|
||||
<ActionButton onClick={handleThumbsDown}>
|
||||
<RiThumbDownLine className='h-4 w-4' />
|
||||
</ActionButton>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!isOpeningStatement && config?.supportFeedback && onFeedback && (
|
||||
<div className='ml-1 flex items-center gap-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 shadow-md backdrop-blur-sm'>
|
||||
{/* User Feedback Display */}
|
||||
{userFeedback?.rating && (
|
||||
<div className='flex items-center'>
|
||||
<span className='mr-1 text-xs text-text-tertiary'>User</span>
|
||||
{userFeedback.rating === 'like' ? (
|
||||
<ActionButton state={ActionButtonState.Active} title={userFeedback.content ? `User liked this response: ${userFeedback.content}` : 'User liked this response'}>
|
||||
<RiThumbUpLine className='h-3 w-3' />
|
||||
</ActionButton>
|
||||
) : (
|
||||
<ActionButton state={ActionButtonState.Destructive} title={userFeedback.content ? `User disliked this response: ${userFeedback.content}` : 'User disliked this response'}>
|
||||
<RiThumbDownLine className='h-3 w-3' />
|
||||
</ActionButton>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Admin Feedback Controls */}
|
||||
{config?.supportAnnotation && (
|
||||
<div className='flex items-center'>
|
||||
{userFeedback?.rating && <div className='mx-1 h-3 w-[0.5px] bg-components-actionbar-border' />}
|
||||
{!adminLocalFeedback?.rating ? (
|
||||
<>
|
||||
<ActionButton onClick={() => handleFeedback('like')}>
|
||||
<RiThumbUpLine className='h-4 w-4' />
|
||||
</ActionButton>
|
||||
<ActionButton onClick={handleThumbsDown}>
|
||||
<RiThumbDownLine className='h-4 w-4' />
|
||||
</ActionButton>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{adminLocalFeedback.rating === 'like' ? (
|
||||
<ActionButton state={ActionButtonState.Active} onClick={() => handleFeedback(null)}>
|
||||
<RiThumbUpLine className='h-4 w-4' />
|
||||
</ActionButton>
|
||||
) : (
|
||||
<ActionButton state={ActionButtonState.Destructive} onClick={() => handleFeedback(null)}>
|
||||
<RiThumbDownLine className='h-4 w-4' />
|
||||
</ActionButton>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<EditReplyModal
|
||||
isShow={isShowReplyModal}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
'use client'
|
||||
import useSWR from 'swr'
|
||||
import { produce } from 'immer'
|
||||
import React, { Fragment } from 'react'
|
||||
import { usePathname } from 'next/navigation'
|
||||
|
|
@ -9,7 +8,6 @@ import { Listbox, ListboxButton, ListboxOption, ListboxOptions, Transition } fro
|
|||
import { CheckIcon, ChevronDownIcon } from '@heroicons/react/20/solid'
|
||||
import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks'
|
||||
import type { Item } from '@/app/components/base/select'
|
||||
import { fetchAppVoices } from '@/service/apps'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
import AudioBtn from '@/app/components/base/audio-btn'
|
||||
|
|
@ -17,6 +15,7 @@ import { languages } from '@/i18n-config/language'
|
|||
import { TtsAutoPlay } from '@/types/app'
|
||||
import type { OnFeaturesChange } from '@/app/components/base/features/types'
|
||||
import classNames from '@/utils/classnames'
|
||||
import { useAppVoices } from '@/service/use-apps'
|
||||
|
||||
type VoiceParamConfigProps = {
|
||||
onClose: () => void
|
||||
|
|
@ -39,7 +38,7 @@ const VoiceParamConfig = ({
|
|||
const localLanguagePlaceholder = languageItem?.name || t('common.placeholder.select')
|
||||
|
||||
const language = languageItem?.value
|
||||
const voiceItems = useSWR({ appId, language }, fetchAppVoices).data
|
||||
const { data: voiceItems } = useAppVoices(appId, language)
|
||||
let voiceItem = voiceItems?.find(item => item.value === text2speech?.voice)
|
||||
if (voiceItems && !voiceItem)
|
||||
voiceItem = voiceItems[0]
|
||||
|
|
|
|||
|
|
@ -1,24 +0,0 @@
|
|||
import type { CrawlResultItem } from '@/models/datasets'
|
||||
|
||||
const result: CrawlResultItem[] = [
|
||||
{
|
||||
title: 'Start the frontend Docker container separately',
|
||||
content: 'Markdown 1',
|
||||
description: 'Description 1',
|
||||
source_url: 'https://example.com/1',
|
||||
},
|
||||
{
|
||||
title: 'Advanced Tool Integration',
|
||||
content: 'Markdown 2',
|
||||
description: 'Description 2',
|
||||
source_url: 'https://example.com/2',
|
||||
},
|
||||
{
|
||||
title: 'Local Source Code Start | English | Dify',
|
||||
content: 'Markdown 3',
|
||||
description: 'Description 3',
|
||||
source_url: 'https://example.com/3',
|
||||
},
|
||||
]
|
||||
|
||||
export default result
|
||||
|
|
@ -5,7 +5,7 @@ import {
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import { RiDeleteBinLine } from '@remixicon/react'
|
||||
import { PlusIcon, XMarkIcon } from '@heroicons/react/20/solid'
|
||||
import useSWR, { useSWRConfig } from 'swr'
|
||||
import useSWR from 'swr'
|
||||
import SecretKeyGenerateModal from './secret-key-generate'
|
||||
import s from './style.module.css'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
|
|
@ -15,7 +15,6 @@ import CopyFeedback from '@/app/components/base/copy-feedback'
|
|||
import {
|
||||
createApikey as createAppApikey,
|
||||
delApikey as delAppApikey,
|
||||
fetchApiKeysList as fetchAppApiKeysList,
|
||||
} from '@/service/apps'
|
||||
import {
|
||||
createApikey as createDatasetApikey,
|
||||
|
|
@ -27,6 +26,7 @@ import Loading from '@/app/components/base/loading'
|
|||
import Confirm from '@/app/components/base/confirm'
|
||||
import useTimestamp from '@/hooks/use-timestamp'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useAppApiKeys, useInvalidateAppApiKeys } from '@/service/use-apps'
|
||||
|
||||
type ISecretKeyModalProps = {
|
||||
isShow: boolean
|
||||
|
|
@ -45,12 +45,14 @@ const SecretKeyModal = ({
|
|||
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
|
||||
const [isVisible, setVisible] = useState(false)
|
||||
const [newKey, setNewKey] = useState<CreateApiKeyResponse | undefined>(undefined)
|
||||
const { mutate } = useSWRConfig()
|
||||
const commonParams = appId
|
||||
? { url: `/apps/${appId}/api-keys`, params: {} }
|
||||
: { url: '/datasets/api-keys', params: {} }
|
||||
const fetchApiKeysList = appId ? fetchAppApiKeysList : fetchDatasetApiKeysList
|
||||
const { data: apiKeysList } = useSWR(commonParams, fetchApiKeysList)
|
||||
const invalidateAppApiKeys = useInvalidateAppApiKeys()
|
||||
const { data: appApiKeys, isLoading: isAppApiKeysLoading } = useAppApiKeys(appId, { enabled: !!appId && isShow })
|
||||
const { data: datasetApiKeys, isLoading: isDatasetApiKeysLoading, mutate: mutateDatasetApiKeys } = useSWR(
|
||||
!appId && isShow ? { url: '/datasets/api-keys', params: {} } : null,
|
||||
fetchDatasetApiKeysList,
|
||||
)
|
||||
const apiKeysList = appId ? appApiKeys : datasetApiKeys
|
||||
const isApiKeysLoading = appId ? isAppApiKeysLoading : isDatasetApiKeysLoading
|
||||
|
||||
const [delKeyID, setDelKeyId] = useState('')
|
||||
|
||||
|
|
@ -64,7 +66,10 @@ const SecretKeyModal = ({
|
|||
? { url: `/apps/${appId}/api-keys/${delKeyID}`, params: {} }
|
||||
: { url: `/datasets/api-keys/${delKeyID}`, params: {} }
|
||||
await delApikey(params)
|
||||
mutate(commonParams)
|
||||
if (appId)
|
||||
invalidateAppApiKeys(appId)
|
||||
else
|
||||
mutateDatasetApiKeys()
|
||||
}
|
||||
|
||||
const onCreate = async () => {
|
||||
|
|
@ -75,7 +80,10 @@ const SecretKeyModal = ({
|
|||
const res = await createApikey(params)
|
||||
setVisible(true)
|
||||
setNewKey(res)
|
||||
mutate(commonParams)
|
||||
if (appId)
|
||||
invalidateAppApiKeys(appId)
|
||||
else
|
||||
mutateDatasetApiKeys()
|
||||
}
|
||||
|
||||
const generateToken = (token: string) => {
|
||||
|
|
@ -88,7 +96,7 @@ const SecretKeyModal = ({
|
|||
<XMarkIcon className="h-6 w-6 cursor-pointer text-text-tertiary" onClick={onClose} />
|
||||
</div>
|
||||
<p className='mt-1 shrink-0 text-[13px] font-normal leading-5 text-text-tertiary'>{t('appApi.apiKeyModal.apiSecretKeyTips')}</p>
|
||||
{!apiKeysList && <div className='mt-4'><Loading /></div>}
|
||||
{isApiKeysLoading && <div className='mt-4'><Loading /></div>}
|
||||
{
|
||||
!!apiKeysList?.data?.length && (
|
||||
<div className='mt-4 flex grow flex-col overflow-hidden'>
|
||||
|
|
|
|||
|
|
@ -214,8 +214,12 @@ export const searchAnything = async (
|
|||
actionItem?: ActionItem,
|
||||
dynamicActions?: Record<string, ActionItem>,
|
||||
): Promise<SearchResult[]> => {
|
||||
const trimmedQuery = query.trim()
|
||||
|
||||
if (actionItem) {
|
||||
const searchTerm = query.replace(actionItem.key, '').replace(actionItem.shortcut, '').trim()
|
||||
const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
const prefixPattern = new RegExp(`^(${escapeRegExp(actionItem.key)}|${escapeRegExp(actionItem.shortcut)})\\s*`)
|
||||
const searchTerm = trimmedQuery.replace(prefixPattern, '').trim()
|
||||
try {
|
||||
return await actionItem.search(query, searchTerm, locale)
|
||||
}
|
||||
|
|
@ -225,10 +229,12 @@ export const searchAnything = async (
|
|||
}
|
||||
}
|
||||
|
||||
if (query.startsWith('@') || query.startsWith('/'))
|
||||
if (trimmedQuery.startsWith('@') || trimmedQuery.startsWith('/'))
|
||||
return []
|
||||
|
||||
const globalSearchActions = Object.values(dynamicActions || Actions)
|
||||
// Exclude slash commands from general search results
|
||||
.filter(action => action.key !== '/')
|
||||
|
||||
// Use Promise.allSettled to handle partial failures gracefully
|
||||
const searchPromises = globalSearchActions.map(async (action) => {
|
||||
|
|
|
|||
|
|
@ -177,31 +177,42 @@ const GotoAnything: FC<Props> = ({
|
|||
}
|
||||
}, [router])
|
||||
|
||||
const dedupedResults = useMemo(() => {
|
||||
const seen = new Set<string>()
|
||||
return searchResults.filter((result) => {
|
||||
const key = `${result.type}-${result.id}`
|
||||
if (seen.has(key))
|
||||
return false
|
||||
seen.add(key)
|
||||
return true
|
||||
})
|
||||
}, [searchResults])
|
||||
|
||||
// Group results by type
|
||||
const groupedResults = useMemo(() => searchResults.reduce((acc, result) => {
|
||||
const groupedResults = useMemo(() => dedupedResults.reduce((acc, result) => {
|
||||
if (!acc[result.type])
|
||||
acc[result.type] = []
|
||||
|
||||
acc[result.type].push(result)
|
||||
return acc
|
||||
}, {} as { [key: string]: SearchResult[] }),
|
||||
[searchResults])
|
||||
[dedupedResults])
|
||||
|
||||
useEffect(() => {
|
||||
if (isCommandsMode)
|
||||
return
|
||||
|
||||
if (!searchResults.length)
|
||||
if (!dedupedResults.length)
|
||||
return
|
||||
|
||||
const currentValueExists = searchResults.some(result => `${result.type}-${result.id}` === cmdVal)
|
||||
const currentValueExists = dedupedResults.some(result => `${result.type}-${result.id}` === cmdVal)
|
||||
|
||||
if (!currentValueExists)
|
||||
setCmdVal(`${searchResults[0].type}-${searchResults[0].id}`)
|
||||
}, [isCommandsMode, searchResults, cmdVal])
|
||||
setCmdVal(`${dedupedResults[0].type}-${dedupedResults[0].id}`)
|
||||
}, [isCommandsMode, dedupedResults, cmdVal])
|
||||
|
||||
const emptyResult = useMemo(() => {
|
||||
if (searchResults.length || !searchQuery.trim() || isLoading || isCommandsMode)
|
||||
if (dedupedResults.length || !searchQuery.trim() || isLoading || isCommandsMode)
|
||||
return null
|
||||
|
||||
const isCommandSearch = searchMode !== 'general'
|
||||
|
|
@ -246,7 +257,7 @@ const GotoAnything: FC<Props> = ({
|
|||
</div>
|
||||
</div>
|
||||
)
|
||||
}, [searchResults, searchQuery, Actions, searchMode, isLoading, isError, isCommandsMode])
|
||||
}, [dedupedResults, searchQuery, Actions, searchMode, isLoading, isError, isCommandsMode])
|
||||
|
||||
const defaultUI = useMemo(() => {
|
||||
if (searchQuery.trim())
|
||||
|
|
@ -430,14 +441,14 @@ const GotoAnything: FC<Props> = ({
|
|||
{/* Always show footer to prevent height jumping */}
|
||||
<div className='border-t border-divider-subtle bg-components-panel-bg-blur px-4 py-2 text-xs text-text-tertiary'>
|
||||
<div className='flex min-h-[16px] items-center justify-between'>
|
||||
{(!!searchResults.length || isError) ? (
|
||||
{(!!dedupedResults.length || isError) ? (
|
||||
<>
|
||||
<span>
|
||||
{isError ? (
|
||||
<span className='text-red-500'>{t('app.gotoAnything.someServicesUnavailable')}</span>
|
||||
) : (
|
||||
<>
|
||||
{t('app.gotoAnything.resultCount', { count: searchResults.length })}
|
||||
{t('app.gotoAnything.resultCount', { count: dedupedResults.length })}
|
||||
{searchMode !== 'general' && (
|
||||
<span className='ml-2 opacity-60'>
|
||||
{t('app.gotoAnything.inScope', { scope: searchMode.replace('@', '') })}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import { useGlobalPublicStore } from '@/context/global-public-context'
|
|||
import { useDocLink } from '@/context/i18n'
|
||||
import { useLogout } from '@/service/use-common'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import { resetUser } from '@/app/components/base/amplitude/utils'
|
||||
|
||||
export default function AppSelector() {
|
||||
const itemClassName = `
|
||||
|
|
@ -53,7 +54,7 @@ export default function AppSelector() {
|
|||
const { mutateAsync: logout } = useLogout()
|
||||
const handleLogout = async () => {
|
||||
await logout()
|
||||
|
||||
resetUser()
|
||||
localStorage.removeItem('setup_status')
|
||||
// Tokens are now stored in cookies and cleared by backend
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams } from 'next/navigation'
|
||||
import useSWRInfinite from 'swr/infinite'
|
||||
import { flatten } from 'lodash-es'
|
||||
import { produce } from 'immer'
|
||||
import {
|
||||
|
|
@ -12,33 +11,13 @@ import {
|
|||
} from '@remixicon/react'
|
||||
import Nav from '../nav'
|
||||
import type { NavItem } from '../nav/nav-selector'
|
||||
import { fetchAppList } from '@/service/apps'
|
||||
import CreateAppTemplateDialog from '@/app/components/app/create-app-dialog'
|
||||
import CreateAppModal from '@/app/components/app/create-app-modal'
|
||||
import CreateFromDSLModal from '@/app/components/app/create-from-dsl-modal'
|
||||
import type { AppListResponse } from '@/models/app'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
|
||||
const getKey = (
|
||||
pageIndex: number,
|
||||
previousPageData: AppListResponse,
|
||||
activeTab: string,
|
||||
keywords: string,
|
||||
) => {
|
||||
if (!pageIndex || previousPageData.has_more) {
|
||||
const params: any = { url: 'apps', params: { page: pageIndex + 1, limit: 30, name: keywords } }
|
||||
|
||||
if (activeTab !== 'all')
|
||||
params.params.mode = activeTab
|
||||
else
|
||||
delete params.params.mode
|
||||
|
||||
return params
|
||||
}
|
||||
return null
|
||||
}
|
||||
import { useInfiniteAppList } from '@/service/use-apps'
|
||||
|
||||
const AppNav = () => {
|
||||
const { t } = useTranslation()
|
||||
|
|
@ -50,17 +29,21 @@ const AppNav = () => {
|
|||
const [showCreateFromDSLModal, setShowCreateFromDSLModal] = useState(false)
|
||||
const [navItems, setNavItems] = useState<NavItem[]>([])
|
||||
|
||||
const { data: appsData, setSize, mutate } = useSWRInfinite(
|
||||
appId
|
||||
? (pageIndex: number, previousPageData: AppListResponse) => getKey(pageIndex, previousPageData, 'all', '')
|
||||
: () => null,
|
||||
fetchAppList,
|
||||
{ revalidateFirstPage: false },
|
||||
)
|
||||
const {
|
||||
data: appsData,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
refetch,
|
||||
} = useInfiniteAppList({
|
||||
page: 1,
|
||||
limit: 30,
|
||||
name: '',
|
||||
}, { enabled: !!appId })
|
||||
|
||||
const handleLoadMore = useCallback(() => {
|
||||
setSize(size => size + 1)
|
||||
}, [setSize])
|
||||
if (hasNextPage)
|
||||
fetchNextPage()
|
||||
}, [fetchNextPage, hasNextPage])
|
||||
|
||||
const openModal = (state: string) => {
|
||||
if (state === 'blank')
|
||||
|
|
@ -73,7 +56,7 @@ const AppNav = () => {
|
|||
|
||||
useEffect(() => {
|
||||
if (appsData) {
|
||||
const appItems = flatten(appsData?.map(appData => appData.data))
|
||||
const appItems = flatten((appsData.pages ?? []).map(appData => appData.data))
|
||||
const navItems = appItems.map((app) => {
|
||||
const link = ((isCurrentWorkspaceEditor, app) => {
|
||||
if (!isCurrentWorkspaceEditor) {
|
||||
|
|
@ -132,17 +115,17 @@ const AppNav = () => {
|
|||
<CreateAppModal
|
||||
show={showNewAppDialog}
|
||||
onClose={() => setShowNewAppDialog(false)}
|
||||
onSuccess={() => mutate()}
|
||||
onSuccess={() => refetch()}
|
||||
/>
|
||||
<CreateAppTemplateDialog
|
||||
show={showNewAppTemplateDialog}
|
||||
onClose={() => setShowNewAppTemplateDialog(false)}
|
||||
onSuccess={() => mutate()}
|
||||
onSuccess={() => refetch()}
|
||||
/>
|
||||
<CreateFromDSLModal
|
||||
show={showCreateFromDSLModal}
|
||||
onClose={() => setShowCreateFromDSLModal(false)}
|
||||
onSuccess={() => mutate()}
|
||||
onSuccess={() => refetch()}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -15,32 +15,10 @@ import type {
|
|||
OffsetOptions,
|
||||
Placement,
|
||||
} from '@floating-ui/react'
|
||||
import useSWRInfinite from 'swr/infinite'
|
||||
import { fetchAppList } from '@/service/apps'
|
||||
import type { AppListResponse } from '@/models/app'
|
||||
import { useInfiniteAppList } from '@/service/use-apps'
|
||||
|
||||
const PAGE_SIZE = 20
|
||||
|
||||
const getKey = (
|
||||
pageIndex: number,
|
||||
previousPageData: AppListResponse,
|
||||
searchText: string,
|
||||
) => {
|
||||
if (pageIndex === 0 || (previousPageData && previousPageData.has_more)) {
|
||||
const params: any = {
|
||||
url: 'apps',
|
||||
params: {
|
||||
page: pageIndex + 1,
|
||||
limit: PAGE_SIZE,
|
||||
name: searchText,
|
||||
},
|
||||
}
|
||||
|
||||
return params
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
type Props = {
|
||||
value?: {
|
||||
app_id: string
|
||||
|
|
@ -72,30 +50,32 @@ const AppSelector: FC<Props> = ({
|
|||
const [searchText, setSearchText] = useState('')
|
||||
const [isLoadingMore, setIsLoadingMore] = useState(false)
|
||||
|
||||
const { data, isLoading, setSize } = useSWRInfinite(
|
||||
(pageIndex: number, previousPageData: AppListResponse) => getKey(pageIndex, previousPageData, searchText),
|
||||
fetchAppList,
|
||||
{
|
||||
revalidateFirstPage: true,
|
||||
shouldRetryOnError: false,
|
||||
dedupingInterval: 500,
|
||||
errorRetryCount: 3,
|
||||
},
|
||||
)
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
isFetchingNextPage,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
} = useInfiniteAppList({
|
||||
page: 1,
|
||||
limit: PAGE_SIZE,
|
||||
name: searchText,
|
||||
})
|
||||
|
||||
const pages = data?.pages ?? []
|
||||
const displayedApps = useMemo(() => {
|
||||
if (!data) return []
|
||||
return data.flatMap(({ data: apps }) => apps)
|
||||
}, [data])
|
||||
if (!pages.length) return []
|
||||
return pages.flatMap(({ data: apps }) => apps)
|
||||
}, [pages])
|
||||
|
||||
const hasMore = data?.at(-1)?.has_more ?? true
|
||||
const hasMore = hasNextPage ?? true
|
||||
|
||||
const handleLoadMore = useCallback(async () => {
|
||||
if (isLoadingMore || !hasMore) return
|
||||
if (isLoadingMore || isFetchingNextPage || !hasMore) return
|
||||
|
||||
setIsLoadingMore(true)
|
||||
try {
|
||||
await setSize((size: number) => size + 1)
|
||||
await fetchNextPage()
|
||||
}
|
||||
finally {
|
||||
// Add a small delay to ensure state updates are complete
|
||||
|
|
@ -103,7 +83,7 @@ const AppSelector: FC<Props> = ({
|
|||
setIsLoadingMore(false)
|
||||
}, 300)
|
||||
}
|
||||
}, [isLoadingMore, hasMore, setSize])
|
||||
}, [isLoadingMore, isFetchingNextPage, hasMore, fetchNextPage])
|
||||
|
||||
const handleTriggerClick = () => {
|
||||
if (disabled) return
|
||||
|
|
@ -185,7 +165,7 @@ const AppSelector: FC<Props> = ({
|
|||
onSelect={handleSelectApp}
|
||||
scope={scope || 'all'}
|
||||
apps={displayedApps}
|
||||
isLoading={isLoading || isLoadingMore}
|
||||
isLoading={isLoading || isLoadingMore || isFetchingNextPage}
|
||||
hasMore={hasMore}
|
||||
onLoadMore={handleLoadMore}
|
||||
searchText={searchText}
|
||||
|
|
|
|||
|
|
@ -1,154 +0,0 @@
|
|||
const tools = [
|
||||
{
|
||||
author: 'Novice',
|
||||
name: 'NOTION_ADD_PAGE_CONTENT',
|
||||
label: {
|
||||
en_US: 'NOTION_ADD_PAGE_CONTENT',
|
||||
zh_Hans: 'NOTION_ADD_PAGE_CONTENT',
|
||||
pt_BR: 'NOTION_ADD_PAGE_CONTENT',
|
||||
ja_JP: 'NOTION_ADD_PAGE_CONTENT',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Adds a single content block to a notion page. multiple calls needed for multiple blocks. note: only supports adding to notion pages. blocks that can contain children: - page (any block type) - toggle (any nested content) - to-do (nested to-dos/blocks) - bulleted list (nested lists/blocks) - numbered list (nested lists/blocks) - callout (child blocks) - quote (nested blocks)',
|
||||
zh_Hans: 'Adds a single content block to a notion page. multiple calls needed for multiple blocks. note: only supports adding to notion pages. blocks that can contain children: - page (any block type) - toggle (any nested content) - to-do (nested to-dos/blocks) - bulleted list (nested lists/blocks) - numbered list (nested lists/blocks) - callout (child blocks) - quote (nested blocks)',
|
||||
pt_BR: 'Adds a single content block to a notion page. multiple calls needed for multiple blocks. note: only supports adding to notion pages. blocks that can contain children: - page (any block type) - toggle (any nested content) - to-do (nested to-dos/blocks) - bulleted list (nested lists/blocks) - numbered list (nested lists/blocks) - callout (child blocks) - quote (nested blocks)',
|
||||
ja_JP: 'Adds a single content block to a notion page. multiple calls needed for multiple blocks. note: only supports adding to notion pages. blocks that can contain children: - page (any block type) - toggle (any nested content) - to-do (nested to-dos/blocks) - bulleted list (nested lists/blocks) - numbered list (nested lists/blocks) - callout (child blocks) - quote (nested blocks)',
|
||||
},
|
||||
parameters: [
|
||||
{
|
||||
name: 'after',
|
||||
label: {
|
||||
en_US: 'after',
|
||||
zh_Hans: 'after',
|
||||
pt_BR: 'after',
|
||||
ja_JP: 'after',
|
||||
},
|
||||
placeholder: null,
|
||||
scope: null,
|
||||
auto_generate: null,
|
||||
template: null,
|
||||
required: false,
|
||||
default: null,
|
||||
min: null,
|
||||
max: null,
|
||||
precision: null,
|
||||
options: [],
|
||||
type: 'string',
|
||||
human_description: {
|
||||
en_US: 'The ID of the existing block that the new block should be appended after. If not provided, content will be appended at the end of the page.',
|
||||
zh_Hans: 'The ID of the existing block that the new block should be appended after. If not provided, content will be appended at the end of the page.',
|
||||
pt_BR: 'The ID of the existing block that the new block should be appended after. If not provided, content will be appended at the end of the page.',
|
||||
ja_JP: 'The ID of the existing block that the new block should be appended after. If not provided, content will be appended at the end of the page.',
|
||||
},
|
||||
form: 'llm',
|
||||
llm_description: 'The ID of the existing block that the new block should be appended after. If not provided, content will be appended at the end of the page.',
|
||||
},
|
||||
{
|
||||
name: 'content_block',
|
||||
label: {
|
||||
en_US: 'content_block',
|
||||
zh_Hans: 'content_block',
|
||||
pt_BR: 'content_block',
|
||||
ja_JP: 'content_block',
|
||||
},
|
||||
placeholder: null,
|
||||
scope: null,
|
||||
auto_generate: null,
|
||||
template: null,
|
||||
required: false,
|
||||
default: null,
|
||||
min: null,
|
||||
max: null,
|
||||
precision: null,
|
||||
options: [],
|
||||
type: 'string',
|
||||
human_description: {
|
||||
en_US: 'Child content to append to a page.',
|
||||
zh_Hans: 'Child content to append to a page.',
|
||||
pt_BR: 'Child content to append to a page.',
|
||||
ja_JP: 'Child content to append to a page.',
|
||||
},
|
||||
form: 'llm',
|
||||
llm_description: 'Child content to append to a page.',
|
||||
},
|
||||
{
|
||||
name: 'parent_block_id',
|
||||
label: {
|
||||
en_US: 'parent_block_id',
|
||||
zh_Hans: 'parent_block_id',
|
||||
pt_BR: 'parent_block_id',
|
||||
ja_JP: 'parent_block_id',
|
||||
},
|
||||
placeholder: null,
|
||||
scope: null,
|
||||
auto_generate: null,
|
||||
template: null,
|
||||
required: false,
|
||||
default: null,
|
||||
min: null,
|
||||
max: null,
|
||||
precision: null,
|
||||
options: [],
|
||||
type: 'string',
|
||||
human_description: {
|
||||
en_US: 'The ID of the page which the children will be added.',
|
||||
zh_Hans: 'The ID of the page which the children will be added.',
|
||||
pt_BR: 'The ID of the page which the children will be added.',
|
||||
ja_JP: 'The ID of the page which the children will be added.',
|
||||
},
|
||||
form: 'llm',
|
||||
llm_description: 'The ID of the page which the children will be added.',
|
||||
},
|
||||
],
|
||||
labels: [],
|
||||
output_schema: null,
|
||||
},
|
||||
]
|
||||
|
||||
export const listData = [
|
||||
{
|
||||
id: 'fdjklajfkljadslf111',
|
||||
author: 'KVOJJJin',
|
||||
name: 'GOGOGO',
|
||||
icon: 'https://cloud.dify.dev/console/api/workspaces/694cc430-fa36-4458-86a0-4a98c09c4684/model-providers/langgenius/openai/openai/icon_small/en_US',
|
||||
server_url: 'https://mcp.composio.dev/notion/****/abc',
|
||||
type: 'mcp',
|
||||
is_team_authorization: true,
|
||||
tools,
|
||||
update_elapsed_time: 1744793369,
|
||||
label: {
|
||||
en_US: 'GOGOGO',
|
||||
zh_Hans: 'GOGOGO',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'fdjklajfkljadslf222',
|
||||
author: 'KVOJJJin',
|
||||
name: 'GOGOGO2',
|
||||
icon: 'https://cloud.dify.dev/console/api/workspaces/694cc430-fa36-4458-86a0-4a98c09c4684/model-providers/langgenius/openai/openai/icon_small/en_US',
|
||||
server_url: 'https://mcp.composio.dev/notion/****/abc',
|
||||
type: 'mcp',
|
||||
is_team_authorization: false,
|
||||
tools: [],
|
||||
update_elapsed_time: 1744793369,
|
||||
label: {
|
||||
en_US: 'GOGOGO2',
|
||||
zh_Hans: 'GOGOGO2',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'fdjklajfkljadslf333',
|
||||
author: 'KVOJJJin',
|
||||
name: 'GOGOGO3',
|
||||
icon: 'https://cloud.dify.dev/console/api/workspaces/694cc430-fa36-4458-86a0-4a98c09c4684/model-providers/langgenius/openai/openai/icon_small/en_US',
|
||||
server_url: 'https://mcp.composio.dev/notion/****/abc',
|
||||
type: 'mcp',
|
||||
is_team_authorization: true,
|
||||
tools,
|
||||
update_elapsed_time: 1744793369,
|
||||
label: {
|
||||
en_US: 'GOGOGO3',
|
||||
zh_Hans: 'GOGOGO3',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
|
@ -1,90 +0,0 @@
|
|||
import { VarType } from '../../../types'
|
||||
import type { VarInInspect } from '@/types/workflow'
|
||||
import { VarInInspectType } from '@/types/workflow'
|
||||
|
||||
export const vars: VarInInspect[] = [
|
||||
{
|
||||
id: 'xxx',
|
||||
type: VarInInspectType.node,
|
||||
name: 'text00',
|
||||
description: '',
|
||||
selector: ['1745476079387', 'text'],
|
||||
value_type: VarType.string,
|
||||
value: 'text value...',
|
||||
edited: false,
|
||||
visible: true,
|
||||
is_truncated: false,
|
||||
full_content: { size_bytes: 0, download_url: '' },
|
||||
},
|
||||
{
|
||||
id: 'fdklajljgldjglkagjlk',
|
||||
type: VarInInspectType.node,
|
||||
name: 'text',
|
||||
description: '',
|
||||
selector: ['1712386917734', 'text'],
|
||||
value_type: VarType.string,
|
||||
value: 'made zhizhang',
|
||||
edited: false,
|
||||
visible: true,
|
||||
is_truncated: false,
|
||||
full_content: { size_bytes: 0, download_url: '' },
|
||||
},
|
||||
]
|
||||
|
||||
export const conversationVars: VarInInspect[] = [
|
||||
{
|
||||
id: 'con1',
|
||||
type: VarInInspectType.conversation,
|
||||
name: 'conversationVar 1',
|
||||
description: '',
|
||||
selector: ['conversation', 'var1'],
|
||||
value_type: VarType.string,
|
||||
value: 'conversation var value...',
|
||||
edited: false,
|
||||
visible: true,
|
||||
is_truncated: false,
|
||||
full_content: { size_bytes: 0, download_url: '' },
|
||||
},
|
||||
{
|
||||
id: 'con2',
|
||||
type: VarInInspectType.conversation,
|
||||
name: 'conversationVar 2',
|
||||
description: '',
|
||||
selector: ['conversation', 'var2'],
|
||||
value_type: VarType.number,
|
||||
value: 456,
|
||||
edited: false,
|
||||
visible: true,
|
||||
is_truncated: false,
|
||||
full_content: { size_bytes: 0, download_url: '' },
|
||||
},
|
||||
]
|
||||
|
||||
export const systemVars: VarInInspect[] = [
|
||||
{
|
||||
id: 'sys1',
|
||||
type: VarInInspectType.system,
|
||||
name: 'query',
|
||||
description: '',
|
||||
selector: ['sys', 'query'],
|
||||
value_type: VarType.string,
|
||||
value: 'Hello robot!',
|
||||
edited: false,
|
||||
visible: true,
|
||||
is_truncated: false,
|
||||
full_content: { size_bytes: 0, download_url: '' },
|
||||
},
|
||||
{
|
||||
id: 'sys2',
|
||||
type: VarInInspectType.system,
|
||||
name: 'user_id',
|
||||
description: '',
|
||||
selector: ['sys', 'user_id'],
|
||||
value_type: VarType.string,
|
||||
value: 'djflakjerlkjdlksfjslakjsdfl',
|
||||
edited: false,
|
||||
visible: true,
|
||||
is_truncated: false,
|
||||
full_content: { size_bytes: 0, download_url: '' },
|
||||
},
|
||||
]
|
||||
|
|
@ -11,6 +11,7 @@ import Toast from '@/app/components/base/toast'
|
|||
import { emailLoginWithCode, sendEMailLoginCode } from '@/service/common'
|
||||
import I18NContext from '@/context/i18n'
|
||||
import { resolvePostLoginRedirect } from '../utils/post-login-redirect'
|
||||
import { trackEvent } from '@/app/components/base/amplitude'
|
||||
|
||||
export default function CheckCode() {
|
||||
const { t, i18n } = useTranslation()
|
||||
|
|
@ -44,6 +45,12 @@ export default function CheckCode() {
|
|||
setIsLoading(true)
|
||||
const ret = await emailLoginWithCode({ email, code, token, language })
|
||||
if (ret.result === 'success') {
|
||||
// Track login success event
|
||||
trackEvent('user_login_success', {
|
||||
method: 'email_code',
|
||||
is_invite: !!invite_token,
|
||||
})
|
||||
|
||||
if (invite_token) {
|
||||
router.replace(`/signin/invite-settings?${searchParams.toString()}`)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import I18NContext from '@/context/i18n'
|
|||
import { noop } from 'lodash-es'
|
||||
import { resolvePostLoginRedirect } from '../utils/post-login-redirect'
|
||||
import type { ResponseError } from '@/service/fetch'
|
||||
import { trackEvent } from '@/app/components/base/amplitude'
|
||||
|
||||
type MailAndPasswordAuthProps = {
|
||||
isInvite: boolean
|
||||
|
|
@ -63,6 +64,12 @@ export default function MailAndPasswordAuth({ isInvite, isEmailSetup, allowRegis
|
|||
body: loginData,
|
||||
})
|
||||
if (res.result === 'success') {
|
||||
// Track login success event
|
||||
trackEvent('user_login_success', {
|
||||
method: 'email_password',
|
||||
is_invite: isInvite,
|
||||
})
|
||||
|
||||
if (isInvite) {
|
||||
router.replace(`/signin/invite-settings?${searchParams.toString()}`)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,6 @@ export default function CheckCode() {
|
|||
}
|
||||
setIsLoading(true)
|
||||
const res = await verifyCode({ email, code, token })
|
||||
console.log(res)
|
||||
if ((res as MailValidityResponse).is_valid) {
|
||||
const params = new URLSearchParams(searchParams)
|
||||
params.set('token', encodeURIComponent((res as MailValidityResponse).token))
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import Input from '@/app/components/base/input'
|
|||
import { validPassword } from '@/config'
|
||||
import type { MailRegisterResponse } from '@/service/use-common'
|
||||
import { useMailRegister } from '@/service/use-common'
|
||||
import { trackEvent } from '@/app/components/base/amplitude'
|
||||
|
||||
const ChangePasswordForm = () => {
|
||||
const { t } = useTranslation()
|
||||
|
|
@ -54,6 +55,11 @@ const ChangePasswordForm = () => {
|
|||
})
|
||||
const { result } = res as MailRegisterResponse
|
||||
if (result === 'success') {
|
||||
// Track registration success event
|
||||
trackEvent('user_registration_success', {
|
||||
method: 'email',
|
||||
})
|
||||
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('common.api.actionSuccess'),
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { noop } from 'lodash-es'
|
|||
import { setZendeskConversationFields } from '@/app/components/base/zendesk/utils'
|
||||
import { ZENDESK_FIELD_IDS } from '@/config'
|
||||
import { useGlobalPublicStore } from './global-public-context'
|
||||
import { setUserId, setUserProperties } from '@/app/components/base/amplitude'
|
||||
|
||||
export type AppContextValue = {
|
||||
userProfile: UserProfileResponse
|
||||
|
|
@ -159,6 +160,28 @@ export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) =>
|
|||
}, [currentWorkspace?.id])
|
||||
// #endregion Zendesk conversation fields
|
||||
|
||||
useEffect(() => {
|
||||
// Report user and workspace info to Amplitude when loaded
|
||||
if (userProfile?.id) {
|
||||
setUserId(userProfile.email)
|
||||
const properties: Record<string, any> = {
|
||||
email: userProfile.email,
|
||||
name: userProfile.name,
|
||||
has_password: userProfile.is_password_set,
|
||||
}
|
||||
|
||||
if (currentWorkspace?.id) {
|
||||
properties.workspace_id = currentWorkspace.id
|
||||
properties.workspace_name = currentWorkspace.name
|
||||
properties.workspace_plan = currentWorkspace.plan
|
||||
properties.workspace_status = currentWorkspace.status
|
||||
properties.workspace_role = currentWorkspace.role
|
||||
}
|
||||
|
||||
setUserProperties(properties)
|
||||
}
|
||||
}, [userProfile, currentWorkspace])
|
||||
|
||||
return (
|
||||
<AppContext.Provider value={{
|
||||
userProfile,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { NextRequest } from 'next/server'
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
const NECESSARY_DOMAIN = '*.sentry.io http://localhost:* http://127.0.0.1:* https://analytics.google.com googletagmanager.com *.googletagmanager.com https://www.google-analytics.com https://api.github.com'
|
||||
const NECESSARY_DOMAIN = '*.sentry.io http://localhost:* http://127.0.0.1:* https://analytics.google.com googletagmanager.com *.googletagmanager.com https://www.google-analytics.com https://api.github.com https://api2.amplitude.com *.amplitude.com'
|
||||
|
||||
const wrapResponseWithXFrameOptions = (response: NextResponse, pathname: string) => {
|
||||
// prevent clickjacking: https://owasp.org/www-community/attacks/Clickjacking
|
||||
|
|
|
|||
|
|
@ -45,6 +45,8 @@
|
|||
"knip": "knip"
|
||||
},
|
||||
"dependencies": {
|
||||
"@amplitude/analytics-browser": "^2.31.3",
|
||||
"@amplitude/plugin-session-replay-browser": "^1.23.6",
|
||||
"@emoji-mart/data": "^1.2.1",
|
||||
"@floating-ui/react": "^0.26.28",
|
||||
"@formatjs/intl-localematcher": "^0.5.10",
|
||||
|
|
|
|||
3563
web/pnpm-lock.yaml
3563
web/pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
|
@ -1,15 +1,14 @@
|
|||
import type { Fetcher } from 'swr'
|
||||
import { del, get, patch, post, put } from './base'
|
||||
import type { ApiKeysListResponse, AppDailyConversationsResponse, AppDailyEndUsersResponse, AppDailyMessagesResponse, AppDetailResponse, AppListResponse, AppStatisticsResponse, AppTemplatesResponse, AppTokenCostsResponse, AppVoicesListResponse, CreateApiKeyResponse, DSLImportMode, DSLImportResponse, GenerationIntroductionResponse, TracingConfig, TracingStatus, UpdateAppModelConfigResponse, UpdateAppSiteCodeResponse, UpdateOpenAIKeyResponse, ValidateOpenAIKeyResponse, WebhookTriggerResponse, WorkflowDailyConversationsResponse } from '@/models/app'
|
||||
import type { CommonResponse } from '@/models/common'
|
||||
import type { AppIconType, AppModeEnum, ModelConfig } from '@/types/app'
|
||||
import type { TracingProvider } from '@/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/type'
|
||||
|
||||
export const fetchAppList: Fetcher<AppListResponse, { url: string; params?: Record<string, any> }> = ({ url, params }) => {
|
||||
export const fetchAppList = ({ url, params }: { url: string; params?: Record<string, any> }): Promise<AppListResponse> => {
|
||||
return get<AppListResponse>(url, { params })
|
||||
}
|
||||
|
||||
export const fetchAppDetail: Fetcher<AppDetailResponse, { url: string; id: string }> = ({ url, id }) => {
|
||||
export const fetchAppDetail = ({ url, id }: { url: string; id: string }): Promise<AppDetailResponse> => {
|
||||
return get<AppDetailResponse>(`${url}/${id}`)
|
||||
}
|
||||
|
||||
|
|
@ -18,24 +17,74 @@ export const fetchAppDetailDirect = async ({ url, id }: { url: string; id: strin
|
|||
return get<AppDetailResponse>(`${url}/${id}`)
|
||||
}
|
||||
|
||||
export const fetchAppTemplates: Fetcher<AppTemplatesResponse, { url: string }> = ({ url }) => {
|
||||
export const fetchAppTemplates = ({ url }: { url: string }): Promise<AppTemplatesResponse> => {
|
||||
return get<AppTemplatesResponse>(url)
|
||||
}
|
||||
|
||||
export const createApp: Fetcher<AppDetailResponse, { name: string; icon_type?: AppIconType; icon?: string; icon_background?: string; mode: AppModeEnum; description?: string; config?: ModelConfig }> = ({ name, icon_type, icon, icon_background, mode, description, config }) => {
|
||||
export const createApp = ({
|
||||
name,
|
||||
icon_type,
|
||||
icon,
|
||||
icon_background,
|
||||
mode,
|
||||
description,
|
||||
config,
|
||||
}: {
|
||||
name: string
|
||||
icon_type?: AppIconType
|
||||
icon?: string
|
||||
icon_background?: string
|
||||
mode: AppModeEnum
|
||||
description?: string
|
||||
config?: ModelConfig
|
||||
}): Promise<AppDetailResponse> => {
|
||||
return post<AppDetailResponse>('apps', { body: { name, icon_type, icon, icon_background, mode, description, model_config: config } })
|
||||
}
|
||||
|
||||
export const updateAppInfo: Fetcher<AppDetailResponse, { appID: string; name: string; icon_type: AppIconType; icon: string; icon_background?: string; description: string; use_icon_as_answer_icon?: boolean; max_active_requests?: number | null }> = ({ appID, name, icon_type, icon, icon_background, description, use_icon_as_answer_icon, max_active_requests }) => {
|
||||
export const updateAppInfo = ({
|
||||
appID,
|
||||
name,
|
||||
icon_type,
|
||||
icon,
|
||||
icon_background,
|
||||
description,
|
||||
use_icon_as_answer_icon,
|
||||
max_active_requests,
|
||||
}: {
|
||||
appID: string
|
||||
name: string
|
||||
icon_type: AppIconType
|
||||
icon: string
|
||||
icon_background?: string
|
||||
description: string
|
||||
use_icon_as_answer_icon?: boolean
|
||||
max_active_requests?: number | null
|
||||
}): Promise<AppDetailResponse> => {
|
||||
const body = { name, icon_type, icon, icon_background, description, use_icon_as_answer_icon, max_active_requests }
|
||||
return put<AppDetailResponse>(`apps/${appID}`, { body })
|
||||
}
|
||||
|
||||
export const copyApp: Fetcher<AppDetailResponse, { appID: string; name: string; icon_type: AppIconType; icon: string; icon_background?: string | null; mode: AppModeEnum; description?: string }> = ({ appID, name, icon_type, icon, icon_background, mode, description }) => {
|
||||
export const copyApp = ({
|
||||
appID,
|
||||
name,
|
||||
icon_type,
|
||||
icon,
|
||||
icon_background,
|
||||
mode,
|
||||
description,
|
||||
}: {
|
||||
appID: string
|
||||
name: string
|
||||
icon_type: AppIconType
|
||||
icon: string
|
||||
icon_background?: string | null
|
||||
mode: AppModeEnum
|
||||
description?: string
|
||||
}): Promise<AppDetailResponse> => {
|
||||
return post<AppDetailResponse>(`apps/${appID}/copy`, { body: { name, icon_type, icon, icon_background, mode, description } })
|
||||
}
|
||||
|
||||
export const exportAppConfig: Fetcher<{ data: string }, { appID: string; include?: boolean; workflowID?: string }> = ({ appID, include = false, workflowID }) => {
|
||||
export const exportAppConfig = ({ appID, include = false, workflowID }: { appID: string; include?: boolean; workflowID?: string }): Promise<{ data: string }> => {
|
||||
const params = new URLSearchParams({
|
||||
include_secret: include.toString(),
|
||||
})
|
||||
|
|
@ -44,126 +93,116 @@ export const exportAppConfig: Fetcher<{ data: string }, { appID: string; include
|
|||
return get<{ data: string }>(`apps/${appID}/export?${params.toString()}`)
|
||||
}
|
||||
|
||||
// TODO: delete
|
||||
export const importApp: Fetcher<AppDetailResponse, { data: string; name?: string; description?: string; icon_type?: AppIconType; icon?: string; icon_background?: string }> = ({ data, name, description, icon_type, icon, icon_background }) => {
|
||||
return post<AppDetailResponse>('apps/import', { body: { data, name, description, icon_type, icon, icon_background } })
|
||||
}
|
||||
|
||||
// TODO: delete
|
||||
export const importAppFromUrl: Fetcher<AppDetailResponse, { url: string; name?: string; description?: string; icon?: string; icon_background?: string }> = ({ url, name, description, icon, icon_background }) => {
|
||||
return post<AppDetailResponse>('apps/import/url', { body: { url, name, description, icon, icon_background } })
|
||||
}
|
||||
|
||||
export const importDSL: Fetcher<DSLImportResponse, { mode: DSLImportMode; yaml_content?: string; yaml_url?: string; app_id?: string; name?: string; description?: string; icon_type?: AppIconType; icon?: string; icon_background?: string }> = ({ mode, yaml_content, yaml_url, app_id, name, description, icon_type, icon, icon_background }) => {
|
||||
export const importDSL = ({ mode, yaml_content, yaml_url, app_id, name, description, icon_type, icon, icon_background }: { mode: DSLImportMode; yaml_content?: string; yaml_url?: string; app_id?: string; name?: string; description?: string; icon_type?: AppIconType; icon?: string; icon_background?: string }): Promise<DSLImportResponse> => {
|
||||
return post<DSLImportResponse>('apps/imports', { body: { mode, yaml_content, yaml_url, app_id, name, description, icon, icon_type, icon_background } })
|
||||
}
|
||||
|
||||
export const importDSLConfirm: Fetcher<DSLImportResponse, { import_id: string }> = ({ import_id }) => {
|
||||
export const importDSLConfirm = ({ import_id }: { import_id: string }): Promise<DSLImportResponse> => {
|
||||
return post<DSLImportResponse>(`apps/imports/${import_id}/confirm`, { body: {} })
|
||||
}
|
||||
|
||||
export const switchApp: Fetcher<{ new_app_id: string }, { appID: string; name: string; icon_type: AppIconType; icon: string; icon_background?: string | null }> = ({ appID, name, icon_type, icon, icon_background }) => {
|
||||
export const switchApp = ({ appID, name, icon_type, icon, icon_background }: { appID: string; name: string; icon_type: AppIconType; icon: string; icon_background?: string | null }): Promise<{ new_app_id: string }> => {
|
||||
return post<{ new_app_id: string }>(`apps/${appID}/convert-to-workflow`, { body: { name, icon_type, icon, icon_background } })
|
||||
}
|
||||
|
||||
export const deleteApp: Fetcher<CommonResponse, string> = (appID) => {
|
||||
export const deleteApp = (appID: string): Promise<CommonResponse> => {
|
||||
return del<CommonResponse>(`apps/${appID}`)
|
||||
}
|
||||
|
||||
export const updateAppSiteStatus: Fetcher<AppDetailResponse, { url: string; body: Record<string, any> }> = ({ url, body }) => {
|
||||
export const updateAppSiteStatus = ({ url, body }: { url: string; body: Record<string, any> }): Promise<AppDetailResponse> => {
|
||||
return post<AppDetailResponse>(url, { body })
|
||||
}
|
||||
|
||||
export const updateAppApiStatus: Fetcher<AppDetailResponse, { url: string; body: Record<string, any> }> = ({ url, body }) => {
|
||||
export const updateAppApiStatus = ({ url, body }: { url: string; body: Record<string, any> }): Promise<AppDetailResponse> => {
|
||||
return post<AppDetailResponse>(url, { body })
|
||||
}
|
||||
|
||||
// path: /apps/{appId}/rate-limit
|
||||
export const updateAppRateLimit: Fetcher<AppDetailResponse, { url: string; body: Record<string, any> }> = ({ url, body }) => {
|
||||
export const updateAppRateLimit = ({ url, body }: { url: string; body: Record<string, any> }): Promise<AppDetailResponse> => {
|
||||
return post<AppDetailResponse>(url, { body })
|
||||
}
|
||||
|
||||
export const updateAppSiteAccessToken: Fetcher<UpdateAppSiteCodeResponse, { url: string }> = ({ url }) => {
|
||||
export const updateAppSiteAccessToken = ({ url }: { url: string }): Promise<UpdateAppSiteCodeResponse> => {
|
||||
return post<UpdateAppSiteCodeResponse>(url)
|
||||
}
|
||||
|
||||
export const updateAppSiteConfig = ({ url, body }: { url: string; body: Record<string, any> }) => {
|
||||
export const updateAppSiteConfig = ({ url, body }: { url: string; body: Record<string, any> }): Promise<AppDetailResponse> => {
|
||||
return post<AppDetailResponse>(url, { body })
|
||||
}
|
||||
|
||||
export const getAppDailyMessages: Fetcher<AppDailyMessagesResponse, { url: string; params: Record<string, any> }> = ({ url, params }) => {
|
||||
export const getAppDailyMessages = ({ url, params }: { url: string; params: Record<string, any> }): Promise<AppDailyMessagesResponse> => {
|
||||
return get<AppDailyMessagesResponse>(url, { params })
|
||||
}
|
||||
|
||||
export const getAppDailyConversations: Fetcher<AppDailyConversationsResponse, { url: string; params: Record<string, any> }> = ({ url, params }) => {
|
||||
export const getAppDailyConversations = ({ url, params }: { url: string; params: Record<string, any> }): Promise<AppDailyConversationsResponse> => {
|
||||
return get<AppDailyConversationsResponse>(url, { params })
|
||||
}
|
||||
|
||||
export const getWorkflowDailyConversations: Fetcher<WorkflowDailyConversationsResponse, { url: string; params: Record<string, any> }> = ({ url, params }) => {
|
||||
export const getWorkflowDailyConversations = ({ url, params }: { url: string; params: Record<string, any> }): Promise<WorkflowDailyConversationsResponse> => {
|
||||
return get<WorkflowDailyConversationsResponse>(url, { params })
|
||||
}
|
||||
|
||||
export const getAppStatistics: Fetcher<AppStatisticsResponse, { url: string; params: Record<string, any> }> = ({ url, params }) => {
|
||||
export const getAppStatistics = ({ url, params }: { url: string; params: Record<string, any> }): Promise<AppStatisticsResponse> => {
|
||||
return get<AppStatisticsResponse>(url, { params })
|
||||
}
|
||||
|
||||
export const getAppDailyEndUsers: Fetcher<AppDailyEndUsersResponse, { url: string; params: Record<string, any> }> = ({ url, params }) => {
|
||||
export const getAppDailyEndUsers = ({ url, params }: { url: string; params: Record<string, any> }): Promise<AppDailyEndUsersResponse> => {
|
||||
return get<AppDailyEndUsersResponse>(url, { params })
|
||||
}
|
||||
|
||||
export const getAppTokenCosts: Fetcher<AppTokenCostsResponse, { url: string; params: Record<string, any> }> = ({ url, params }) => {
|
||||
export const getAppTokenCosts = ({ url, params }: { url: string; params: Record<string, any> }): Promise<AppTokenCostsResponse> => {
|
||||
return get<AppTokenCostsResponse>(url, { params })
|
||||
}
|
||||
|
||||
export const updateAppModelConfig: Fetcher<UpdateAppModelConfigResponse, { url: string; body: Record<string, any> }> = ({ url, body }) => {
|
||||
export const updateAppModelConfig = ({ url, body }: { url: string; body: Record<string, any> }): Promise<UpdateAppModelConfigResponse> => {
|
||||
return post<UpdateAppModelConfigResponse>(url, { body })
|
||||
}
|
||||
|
||||
// For temp testing
|
||||
export const fetchAppListNoMock: Fetcher<AppListResponse, { url: string; params: Record<string, any> }> = ({ url, params }) => {
|
||||
export const fetchAppListNoMock = ({ url, params }: { url: string; params: Record<string, any> }): Promise<AppListResponse> => {
|
||||
return get<AppListResponse>(url, params)
|
||||
}
|
||||
|
||||
export const fetchApiKeysList: Fetcher<ApiKeysListResponse, { url: string; params: Record<string, any> }> = ({ url, params }) => {
|
||||
export const fetchApiKeysList = ({ url, params }: { url: string; params: Record<string, any> }): Promise<ApiKeysListResponse> => {
|
||||
return get<ApiKeysListResponse>(url, params)
|
||||
}
|
||||
|
||||
export const delApikey: Fetcher<CommonResponse, { url: string; params: Record<string, any> }> = ({ url, params }) => {
|
||||
export const delApikey = ({ url, params }: { url: string; params: Record<string, any> }): Promise<CommonResponse> => {
|
||||
return del<CommonResponse>(url, params)
|
||||
}
|
||||
|
||||
export const createApikey: Fetcher<CreateApiKeyResponse, { url: string; body: Record<string, any> }> = ({ url, body }) => {
|
||||
export const createApikey = ({ url, body }: { url: string; body: Record<string, any> }): Promise<CreateApiKeyResponse> => {
|
||||
return post<CreateApiKeyResponse>(url, body)
|
||||
}
|
||||
|
||||
export const validateOpenAIKey: Fetcher<ValidateOpenAIKeyResponse, { url: string; body: { token: string } }> = ({ url, body }) => {
|
||||
export const validateOpenAIKey = ({ url, body }: { url: string; body: { token: string } }): Promise<ValidateOpenAIKeyResponse> => {
|
||||
return post<ValidateOpenAIKeyResponse>(url, { body })
|
||||
}
|
||||
|
||||
export const updateOpenAIKey: Fetcher<UpdateOpenAIKeyResponse, { url: string; body: { token: string } }> = ({ url, body }) => {
|
||||
export const updateOpenAIKey = ({ url, body }: { url: string; body: { token: string } }): Promise<UpdateOpenAIKeyResponse> => {
|
||||
return post<UpdateOpenAIKeyResponse>(url, { body })
|
||||
}
|
||||
|
||||
export const generationIntroduction: Fetcher<GenerationIntroductionResponse, { url: string; body: { prompt_template: string } }> = ({ url, body }) => {
|
||||
export const generationIntroduction = ({ url, body }: { url: string; body: { prompt_template: string } }): Promise<GenerationIntroductionResponse> => {
|
||||
return post<GenerationIntroductionResponse>(url, { body })
|
||||
}
|
||||
|
||||
export const fetchAppVoices: Fetcher<AppVoicesListResponse, { appId: string; language?: string }> = ({ appId, language }) => {
|
||||
export const fetchAppVoices = ({ appId, language }: { appId: string; language?: string }): Promise<AppVoicesListResponse> => {
|
||||
language = language || 'en-US'
|
||||
return get<AppVoicesListResponse>(`apps/${appId}/text-to-audio/voices?language=${language}`)
|
||||
}
|
||||
|
||||
// Tracing
|
||||
export const fetchTracingStatus: Fetcher<TracingStatus, { appId: string }> = ({ appId }) => {
|
||||
return get(`/apps/${appId}/trace`)
|
||||
export const fetchTracingStatus = ({ appId }: { appId: string }): Promise<TracingStatus> => {
|
||||
return get<TracingStatus>(`/apps/${appId}/trace`)
|
||||
}
|
||||
|
||||
export const updateTracingStatus: Fetcher<CommonResponse, { appId: string; body: Record<string, any> }> = ({ appId, body }) => {
|
||||
return post(`/apps/${appId}/trace`, { body })
|
||||
export const updateTracingStatus = ({ appId, body }: { appId: string; body: Record<string, any> }): Promise<CommonResponse> => {
|
||||
return post<CommonResponse>(`/apps/${appId}/trace`, { body })
|
||||
}
|
||||
|
||||
// Webhook Trigger
|
||||
export const fetchWebhookUrl: Fetcher<WebhookTriggerResponse, { appId: string; nodeId: string }> = ({ appId, nodeId }) => {
|
||||
export const fetchWebhookUrl = ({ appId, nodeId }: { appId: string; nodeId: string }): Promise<WebhookTriggerResponse> => {
|
||||
return get<WebhookTriggerResponse>(
|
||||
`apps/${appId}/workflows/triggers/webhook`,
|
||||
{ params: { node_id: nodeId } },
|
||||
|
|
@ -171,22 +210,22 @@ export const fetchWebhookUrl: Fetcher<WebhookTriggerResponse, { appId: string; n
|
|||
)
|
||||
}
|
||||
|
||||
export const fetchTracingConfig: Fetcher<TracingConfig & { has_not_configured: true }, { appId: string; provider: TracingProvider }> = ({ appId, provider }) => {
|
||||
return get(`/apps/${appId}/trace-config`, {
|
||||
export const fetchTracingConfig = ({ appId, provider }: { appId: string; provider: TracingProvider }): Promise<TracingConfig & { has_not_configured: true }> => {
|
||||
return get<TracingConfig & { has_not_configured: true }>(`/apps/${appId}/trace-config`, {
|
||||
params: {
|
||||
tracing_provider: provider,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const addTracingConfig: Fetcher<CommonResponse, { appId: string; body: TracingConfig }> = ({ appId, body }) => {
|
||||
return post(`/apps/${appId}/trace-config`, { body })
|
||||
export const addTracingConfig = ({ appId, body }: { appId: string; body: TracingConfig }): Promise<CommonResponse> => {
|
||||
return post<CommonResponse>(`/apps/${appId}/trace-config`, { body })
|
||||
}
|
||||
|
||||
export const updateTracingConfig: Fetcher<CommonResponse, { appId: string; body: TracingConfig }> = ({ appId, body }) => {
|
||||
return patch(`/apps/${appId}/trace-config`, { body })
|
||||
export const updateTracingConfig = ({ appId, body }: { appId: string; body: TracingConfig }): Promise<CommonResponse> => {
|
||||
return patch<CommonResponse>(`/apps/${appId}/trace-config`, { body })
|
||||
}
|
||||
|
||||
export const removeTracingConfig: Fetcher<CommonResponse, { appId: string; provider: TracingProvider }> = ({ appId, provider }) => {
|
||||
return del(`/apps/${appId}/trace-config?tracing_provider=${provider}`)
|
||||
export const removeTracingConfig = ({ appId, provider }: { appId: string; provider: TracingProvider }): Promise<CommonResponse> => {
|
||||
return del<CommonResponse>(`/apps/${appId}/trace-config?tracing_provider=${provider}`)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,38 +1,85 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import useSWR, { useSWRConfig } from 'swr'
|
||||
import { createApp, fetchAppDetail, fetchAppList, getAppDailyConversations, getAppDailyEndUsers, updateAppApiStatus, updateAppModelConfig, updateAppRateLimit, updateAppSiteAccessToken, updateAppSiteConfig, updateAppSiteStatus } from '../apps'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { createApp, updateAppApiStatus, updateAppModelConfig, updateAppRateLimit, updateAppSiteAccessToken, updateAppSiteConfig, updateAppSiteStatus } from '../apps'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import {
|
||||
useAppDailyConversations,
|
||||
useAppDailyEndUsers,
|
||||
useAppDetail,
|
||||
useAppList,
|
||||
} from '../use-apps'
|
||||
|
||||
const Service: FC = () => {
|
||||
const { data: appList, error: appListError } = useSWR({ url: '/apps', params: { page: 1 } }, fetchAppList)
|
||||
const { data: firstApp, error: appDetailError } = useSWR({ url: '/apps', id: '1' }, fetchAppDetail)
|
||||
const { data: updateAppSiteStatusRes, error: err1 } = useSWR({ url: '/apps', id: '1', body: { enable_site: false } }, updateAppSiteStatus)
|
||||
const { data: updateAppApiStatusRes, error: err2 } = useSWR({ url: '/apps', id: '1', body: { enable_api: true } }, updateAppApiStatus)
|
||||
const { data: updateAppRateLimitRes, error: err3 } = useSWR({ url: '/apps', id: '1', body: { api_rpm: 10, api_rph: 20 } }, updateAppRateLimit)
|
||||
const { data: updateAppSiteCodeRes, error: err4 } = useSWR({ url: '/apps', id: '1', body: {} }, updateAppSiteAccessToken)
|
||||
const { data: updateAppSiteConfigRes, error: err5 } = useSWR({ url: '/apps', id: '1', body: { title: 'title test', author: 'author test' } }, updateAppSiteConfig)
|
||||
const { data: getAppDailyConversationsRes, error: err6 } = useSWR({ url: '/apps', id: '1', body: { start: '1', end: '2' } }, getAppDailyConversations)
|
||||
const { data: getAppDailyEndUsersRes, error: err7 } = useSWR({ url: '/apps', id: '1', body: { start: '1', end: '2' } }, getAppDailyEndUsers)
|
||||
const { data: updateAppModelConfigRes, error: err8 } = useSWR({ url: '/apps', id: '1', body: { model_id: 'gpt-100' } }, updateAppModelConfig)
|
||||
const appId = '1'
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const { mutate } = useSWRConfig()
|
||||
const { data: appList, error: appListError, isLoading: isAppListLoading } = useAppList({ page: 1, limit: 30, name: '' })
|
||||
const { data: firstApp, error: appDetailError, isLoading: isAppDetailLoading } = useAppDetail(appId)
|
||||
|
||||
const handleCreateApp = async () => {
|
||||
await createApp({
|
||||
const { data: updateAppSiteStatusRes, error: err1, isLoading: isUpdatingSiteStatus } = useQuery({
|
||||
queryKey: ['demo', 'updateAppSiteStatus', appId],
|
||||
queryFn: () => updateAppSiteStatus({ url: '/apps', body: { enable_site: false } }),
|
||||
})
|
||||
const { data: updateAppApiStatusRes, error: err2, isLoading: isUpdatingApiStatus } = useQuery({
|
||||
queryKey: ['demo', 'updateAppApiStatus', appId],
|
||||
queryFn: () => updateAppApiStatus({ url: '/apps', body: { enable_api: true } }),
|
||||
})
|
||||
const { data: updateAppRateLimitRes, error: err3, isLoading: isUpdatingRateLimit } = useQuery({
|
||||
queryKey: ['demo', 'updateAppRateLimit', appId],
|
||||
queryFn: () => updateAppRateLimit({ url: '/apps', body: { api_rpm: 10, api_rph: 20 } }),
|
||||
})
|
||||
const { data: updateAppSiteCodeRes, error: err4, isLoading: isUpdatingSiteCode } = useQuery({
|
||||
queryKey: ['demo', 'updateAppSiteAccessToken', appId],
|
||||
queryFn: () => updateAppSiteAccessToken({ url: '/apps' }),
|
||||
})
|
||||
const { data: updateAppSiteConfigRes, error: err5, isLoading: isUpdatingSiteConfig } = useQuery({
|
||||
queryKey: ['demo', 'updateAppSiteConfig', appId],
|
||||
queryFn: () => updateAppSiteConfig({ url: '/apps', body: { title: 'title test', author: 'author test' } }),
|
||||
})
|
||||
|
||||
const { data: getAppDailyConversationsRes, error: err6, isLoading: isConversationsLoading } = useAppDailyConversations(appId, { start: '1', end: '2' })
|
||||
const { data: getAppDailyEndUsersRes, error: err7, isLoading: isEndUsersLoading } = useAppDailyEndUsers(appId, { start: '1', end: '2' })
|
||||
|
||||
const { data: updateAppModelConfigRes, error: err8, isLoading: isUpdatingModelConfig } = useQuery({
|
||||
queryKey: ['demo', 'updateAppModelConfig', appId],
|
||||
queryFn: () => updateAppModelConfig({ url: '/apps', body: { model_id: 'gpt-100' } }),
|
||||
})
|
||||
|
||||
const { mutateAsync: mutateCreateApp } = useMutation({
|
||||
mutationKey: ['demo', 'createApp'],
|
||||
mutationFn: () => createApp({
|
||||
name: `new app${Math.round(Math.random() * 100)}`,
|
||||
mode: AppModeEnum.CHAT,
|
||||
})
|
||||
// reload app list
|
||||
mutate({ url: '/apps', params: { page: 1 } })
|
||||
}),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['apps', 'list'],
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const handleCreateApp = async () => {
|
||||
await mutateCreateApp()
|
||||
}
|
||||
|
||||
if (appListError || appDetailError || err1 || err2 || err3 || err4 || err5 || err6 || err7 || err8)
|
||||
return <div>{JSON.stringify(appListError)}</div>
|
||||
return <div>{JSON.stringify(appListError ?? appDetailError ?? err1 ?? err2 ?? err3 ?? err4 ?? err5 ?? err6 ?? err7 ?? err8)}</div>
|
||||
|
||||
if (!appList || !firstApp || !updateAppSiteStatusRes || !updateAppApiStatusRes || !updateAppRateLimitRes || !updateAppSiteCodeRes || !updateAppSiteConfigRes || !getAppDailyConversationsRes || !getAppDailyEndUsersRes || !updateAppModelConfigRes)
|
||||
const isLoading = isAppListLoading
|
||||
|| isAppDetailLoading
|
||||
|| isUpdatingSiteStatus
|
||||
|| isUpdatingApiStatus
|
||||
|| isUpdatingRateLimit
|
||||
|| isUpdatingSiteCode
|
||||
|| isUpdatingSiteConfig
|
||||
|| isConversationsLoading
|
||||
|| isEndUsersLoading
|
||||
|| isUpdatingModelConfig
|
||||
|
||||
if (isLoading || !appList || !firstApp || !updateAppSiteStatusRes || !updateAppApiStatusRes || !updateAppRateLimitRes || !updateAppSiteCodeRes || !updateAppSiteConfigRes || !getAppDailyConversationsRes || !getAppDailyEndUsersRes || !updateAppModelConfigRes)
|
||||
return <Loading />
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,31 +1,63 @@
|
|||
import { get, post } from './base'
|
||||
import type { App } from '@/types/app'
|
||||
import type { AppListResponse } from '@/models/app'
|
||||
import type {
|
||||
ApiKeysListResponse,
|
||||
AppDailyConversationsResponse,
|
||||
AppDailyEndUsersResponse,
|
||||
AppDailyMessagesResponse,
|
||||
AppListResponse,
|
||||
AppStatisticsResponse,
|
||||
AppTokenCostsResponse,
|
||||
AppVoicesListResponse,
|
||||
WorkflowDailyConversationsResponse,
|
||||
} from '@/models/app'
|
||||
import type { App, AppModeEnum } from '@/types/app'
|
||||
import { useInvalid } from './use-base'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import {
|
||||
useInfiniteQuery,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
} from '@tanstack/react-query'
|
||||
import type { GeneratorType } from '@/app/components/app/configuration/config/automatic/types'
|
||||
|
||||
const NAME_SPACE = 'apps'
|
||||
|
||||
// TODO paging for list
|
||||
type AppListParams = {
|
||||
page?: number
|
||||
limit?: number
|
||||
name?: string
|
||||
mode?: AppModeEnum | 'all'
|
||||
tag_ids?: string[]
|
||||
is_created_by_me?: boolean
|
||||
}
|
||||
|
||||
type DateRangeParams = {
|
||||
start?: string
|
||||
end?: string
|
||||
}
|
||||
|
||||
const normalizeAppListParams = (params: AppListParams) => {
|
||||
const {
|
||||
page = 1,
|
||||
limit = 30,
|
||||
name = '',
|
||||
mode,
|
||||
tag_ids,
|
||||
is_created_by_me,
|
||||
} = params
|
||||
|
||||
return {
|
||||
page,
|
||||
limit,
|
||||
name,
|
||||
...(mode && mode !== 'all' ? { mode } : {}),
|
||||
...(tag_ids?.length ? { tag_ids } : {}),
|
||||
...(is_created_by_me ? { is_created_by_me } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
const appListKey = (params: AppListParams) => [NAME_SPACE, 'list', params]
|
||||
|
||||
const useAppFullListKey = [NAME_SPACE, 'full-list']
|
||||
export const useAppFullList = () => {
|
||||
return useQuery<AppListResponse>({
|
||||
queryKey: useAppFullListKey,
|
||||
queryFn: () => get<AppListResponse>('/apps', { params: { page: 1, limit: 100 } }),
|
||||
})
|
||||
}
|
||||
|
||||
export const useInvalidateAppFullList = () => {
|
||||
return useInvalid(useAppFullListKey)
|
||||
}
|
||||
|
||||
export const useAppDetail = (appID: string) => {
|
||||
return useQuery<App>({
|
||||
queryKey: [NAME_SPACE, 'detail', appID],
|
||||
queryFn: () => get<App>(`/apps/${appID}`),
|
||||
})
|
||||
}
|
||||
|
||||
export const useGenerateRuleTemplate = (type: GeneratorType, disabled?: boolean) => {
|
||||
return useQuery({
|
||||
|
|
@ -39,3 +71,142 @@ export const useGenerateRuleTemplate = (type: GeneratorType, disabled?: boolean)
|
|||
retry: 0,
|
||||
})
|
||||
}
|
||||
|
||||
export const useAppDetail = (appID: string) => {
|
||||
return useQuery<App>({
|
||||
queryKey: [NAME_SPACE, 'detail', appID],
|
||||
queryFn: () => get<App>(`/apps/${appID}`),
|
||||
enabled: !!appID,
|
||||
})
|
||||
}
|
||||
|
||||
export const useAppList = (params: AppListParams, options?: { enabled?: boolean }) => {
|
||||
const normalizedParams = normalizeAppListParams(params)
|
||||
return useQuery<AppListResponse>({
|
||||
queryKey: appListKey(normalizedParams),
|
||||
queryFn: () => get<AppListResponse>('/apps', { params: normalizedParams }),
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useAppFullList = () => {
|
||||
return useQuery<AppListResponse>({
|
||||
queryKey: useAppFullListKey,
|
||||
queryFn: () => get<AppListResponse>('/apps', { params: { page: 1, limit: 100, name: '' } }),
|
||||
})
|
||||
}
|
||||
|
||||
export const useInvalidateAppFullList = () => {
|
||||
return useInvalid(useAppFullListKey)
|
||||
}
|
||||
|
||||
export const useInfiniteAppList = (params: AppListParams, options?: { enabled?: boolean }) => {
|
||||
const normalizedParams = normalizeAppListParams(params)
|
||||
return useInfiniteQuery<AppListResponse>({
|
||||
queryKey: appListKey(normalizedParams),
|
||||
queryFn: ({ pageParam = normalizedParams.page }) => get<AppListResponse>('/apps', { params: { ...normalizedParams, page: pageParam } }),
|
||||
getNextPageParam: lastPage => lastPage.has_more ? lastPage.page + 1 : undefined,
|
||||
initialPageParam: normalizedParams.page,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useInvalidateAppList = () => {
|
||||
const queryClient = useQueryClient()
|
||||
return () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [NAME_SPACE, 'list'],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const useAppStatisticsQuery = <T>(metric: string, appId: string, params?: DateRangeParams) => {
|
||||
return useQuery<T>({
|
||||
queryKey: [NAME_SPACE, 'statistics', metric, appId, params],
|
||||
queryFn: () => get<T>(`/apps/${appId}/statistics/${metric}`, { params }),
|
||||
enabled: !!appId,
|
||||
})
|
||||
}
|
||||
|
||||
const useWorkflowStatisticsQuery = <T>(metric: string, appId: string, params?: DateRangeParams) => {
|
||||
return useQuery<T>({
|
||||
queryKey: [NAME_SPACE, 'workflow-statistics', metric, appId, params],
|
||||
queryFn: () => get<T>(`/apps/${appId}/workflow/statistics/${metric}`, { params }),
|
||||
enabled: !!appId,
|
||||
})
|
||||
}
|
||||
|
||||
export const useAppDailyMessages = (appId: string, params?: DateRangeParams) => {
|
||||
return useAppStatisticsQuery<AppDailyMessagesResponse>('daily-messages', appId, params)
|
||||
}
|
||||
|
||||
export const useAppDailyConversations = (appId: string, params?: DateRangeParams) => {
|
||||
return useAppStatisticsQuery<AppDailyConversationsResponse>('daily-conversations', appId, params)
|
||||
}
|
||||
|
||||
export const useAppDailyEndUsers = (appId: string, params?: DateRangeParams) => {
|
||||
return useAppStatisticsQuery<AppDailyEndUsersResponse>('daily-end-users', appId, params)
|
||||
}
|
||||
|
||||
export const useAppAverageSessionInteractions = (appId: string, params?: DateRangeParams) => {
|
||||
return useAppStatisticsQuery<AppStatisticsResponse>('average-session-interactions', appId, params)
|
||||
}
|
||||
|
||||
export const useAppAverageResponseTime = (appId: string, params?: DateRangeParams) => {
|
||||
return useAppStatisticsQuery<AppStatisticsResponse>('average-response-time', appId, params)
|
||||
}
|
||||
|
||||
export const useAppTokensPerSecond = (appId: string, params?: DateRangeParams) => {
|
||||
return useAppStatisticsQuery<AppStatisticsResponse>('tokens-per-second', appId, params)
|
||||
}
|
||||
|
||||
export const useAppSatisfactionRate = (appId: string, params?: DateRangeParams) => {
|
||||
return useAppStatisticsQuery<AppStatisticsResponse>('user-satisfaction-rate', appId, params)
|
||||
}
|
||||
|
||||
export const useAppTokenCosts = (appId: string, params?: DateRangeParams) => {
|
||||
return useAppStatisticsQuery<AppTokenCostsResponse>('token-costs', appId, params)
|
||||
}
|
||||
|
||||
export const useWorkflowDailyConversations = (appId: string, params?: DateRangeParams) => {
|
||||
return useWorkflowStatisticsQuery<WorkflowDailyConversationsResponse>('daily-conversations', appId, params)
|
||||
}
|
||||
|
||||
export const useWorkflowDailyTerminals = (appId: string, params?: DateRangeParams) => {
|
||||
return useWorkflowStatisticsQuery<AppDailyEndUsersResponse>('daily-terminals', appId, params)
|
||||
}
|
||||
|
||||
export const useWorkflowTokenCosts = (appId: string, params?: DateRangeParams) => {
|
||||
return useWorkflowStatisticsQuery<AppTokenCostsResponse>('token-costs', appId, params)
|
||||
}
|
||||
|
||||
export const useWorkflowAverageInteractions = (appId: string, params?: DateRangeParams) => {
|
||||
return useWorkflowStatisticsQuery<AppStatisticsResponse>('average-app-interactions', appId, params)
|
||||
}
|
||||
|
||||
export const useAppVoices = (appId?: string, language?: string) => {
|
||||
return useQuery<AppVoicesListResponse>({
|
||||
queryKey: [NAME_SPACE, 'voices', appId, language || 'en-US'],
|
||||
queryFn: () => get<AppVoicesListResponse>(`/apps/${appId}/text-to-audio/voices`, { params: { language: language || 'en-US' } }),
|
||||
enabled: !!appId,
|
||||
})
|
||||
}
|
||||
|
||||
export const useAppApiKeys = (appId?: string, options?: { enabled?: boolean }) => {
|
||||
return useQuery<ApiKeysListResponse>({
|
||||
queryKey: [NAME_SPACE, 'api-keys', appId],
|
||||
queryFn: () => get<ApiKeysListResponse>(`/apps/${appId}/api-keys`),
|
||||
enabled: !!appId && (options?.enabled ?? true),
|
||||
})
|
||||
}
|
||||
|
||||
export const useInvalidateAppApiKeys = () => {
|
||||
const queryClient = useQueryClient()
|
||||
return (appId?: string) => {
|
||||
if (!appId)
|
||||
return
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [NAME_SPACE, 'api-keys', appId],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue