Compare commits
70
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dacd480c6f | ||
|
|
d960d3ee3e | ||
|
|
898e65d645 | ||
|
|
818966e53f | ||
|
|
e285917e37 | ||
|
|
d18b2c0e82 | ||
|
|
d205b4c10e | ||
|
|
4bd114a90a | ||
|
|
84a9ddc2d8 | ||
|
|
e658a6a80d | ||
|
|
6f93e2f780 | ||
|
|
37fc386af2 | ||
|
|
9aa9c87e0d | ||
|
|
56e9129efb | ||
|
|
1e2311128c | ||
|
|
08edf20672 | ||
|
|
28626590d1 | ||
|
|
8dc5d98174 | ||
|
|
9c10731494 | ||
|
|
eaed72a40c | ||
|
|
168a1524b2 | ||
|
|
8e01eb4561 | ||
|
|
96865ebc8c | ||
|
|
7ca5e8812e | ||
|
|
201fa723a5 | ||
|
|
32b5ae3b71 | ||
|
|
f3bfccbd67 | ||
|
|
a4e3a39dd2 | ||
|
|
9e753b031a | ||
|
|
15f3d68bc5 | ||
|
|
b9202d1e0a | ||
|
|
a6e9da1d89 | ||
|
|
125f37b562 | ||
|
|
eea3f14777 | ||
|
|
0e43f5ad4d | ||
|
|
e3f85c1658 | ||
|
|
10571fcf7c | ||
|
+4 |
244bd08db2 | ||
|
|
73ef99c25a | ||
|
|
d5a30c667b | ||
|
|
9648020ab4 | ||
|
|
76be5c054b | ||
|
|
c76f370f97 | ||
|
|
853f5ff09e | ||
|
|
2ee55d8bc8 | ||
|
|
57a7e3c20f | ||
|
|
3160242fca | ||
|
|
001e4f8c69 | ||
|
|
48c95f4ccf | ||
|
|
1418512f24 | ||
|
|
f834f8a160 | ||
|
|
feaad67531 | ||
|
|
9592b72aa3 | ||
|
|
6325ed9cd3 | ||
|
|
ab39f39db0 | ||
|
|
becabd84a9 | ||
|
|
62de0e97e5 | ||
|
|
aa0fdbf759 | ||
|
|
c3b48cca11 | ||
|
|
874ee69f2b | ||
|
|
8f499d9894 | ||
|
|
f16a42ee52 | ||
|
|
9b43b9f1e9 | ||
|
|
f8e01d7df8 | ||
|
|
ead5a78faa | ||
|
|
fd2ff963a9 | ||
|
|
7492b39ff0 | ||
|
|
8b5533e1a4 | ||
|
|
fa7a0fd3d1 | ||
|
|
2a20a96123 |
+2
-2
@@ -37,7 +37,7 @@ dependencies = [
|
||||
"numpy~=1.26.4",
|
||||
"openpyxl~=3.1.5",
|
||||
"opik~=1.8.72",
|
||||
"litellm==1.77.1", # Pinned to avoid madoka dependency issue
|
||||
"litellm==1.77.1", # Pinned to avoid madoka dependency issue
|
||||
"opentelemetry-api==1.27.0",
|
||||
"opentelemetry-distro==0.48b0",
|
||||
"opentelemetry-exporter-otlp==1.27.0",
|
||||
@@ -79,7 +79,6 @@ dependencies = [
|
||||
"tiktoken~=0.9.0",
|
||||
"transformers~=4.56.1",
|
||||
"unstructured[docx,epub,md,ppt,pptx]~=0.16.1",
|
||||
"weave~=0.51.0",
|
||||
"yarl~=1.18.3",
|
||||
"webvtt-py~=0.5.1",
|
||||
"sseclient-py~=1.8.0",
|
||||
@@ -90,6 +89,7 @@ dependencies = [
|
||||
"croniter>=6.0.0",
|
||||
"weaviate-client==4.17.0",
|
||||
"apscheduler>=3.11.0",
|
||||
"weave>=0.52.16",
|
||||
]
|
||||
# Before adding new dependency, consider place it in
|
||||
# alphabet order (a-z) and suitable group.
|
||||
|
||||
Generated
+5
-15
@@ -1588,7 +1588,7 @@ requires-dist = [
|
||||
{ name = "tiktoken", specifier = "~=0.9.0" },
|
||||
{ name = "transformers", specifier = "~=4.56.1" },
|
||||
{ name = "unstructured", extras = ["docx", "epub", "md", "ppt", "pptx"], specifier = "~=0.16.1" },
|
||||
{ name = "weave", specifier = "~=0.51.0" },
|
||||
{ name = "weave", specifier = ">=0.52.16" },
|
||||
{ name = "weaviate-client", specifier = "==4.17.0" },
|
||||
{ name = "webvtt-py", specifier = "~=0.5.1" },
|
||||
{ name = "yarl", specifier = "~=1.18.3" },
|
||||
@@ -3538,15 +3538,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/95/e1/45373c06781340c7b74fe9b88b85278ac05321889a307eaa5be079a997d4/mysql_connector_python-9.5.0-py2.py3-none-any.whl", hash = "sha256:ace137b88eb6fdafa1e5b2e03ac76ce1b8b1844b3a4af1192a02ae7c1a45bdee", size = 479047, upload-time = "2025-10-22T09:02:27.809Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nest-asyncio"
|
||||
version = "1.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "networkx"
|
||||
version = "3.5"
|
||||
@@ -6906,7 +6897,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "weave"
|
||||
version = "0.51.59"
|
||||
version = "0.52.16"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
@@ -6914,18 +6905,17 @@ dependencies = [
|
||||
{ name = "eval-type-backport" },
|
||||
{ name = "gql", extra = ["aiohttp", "requests"] },
|
||||
{ name = "jsonschema" },
|
||||
{ name = "nest-asyncio" },
|
||||
{ name = "packaging" },
|
||||
{ name = "polyfile-weave" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "rich" },
|
||||
{ name = "sentry-sdk" },
|
||||
{ name = "tenacity" },
|
||||
{ name = "tzdata", marker = "sys_platform == 'win32'" },
|
||||
{ name = "wandb" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0e/53/1b0350a64837df3e29eda6149a542f3a51e706122086f82547153820e982/weave-0.51.59.tar.gz", hash = "sha256:fad34c0478f3470401274cba8fa2bfd45d14a187db0a5724bd507e356761b349", size = 480572, upload-time = "2025-07-25T22:05:07.458Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/be/30/b795b5a857e8a908e68f3ed969587bb2bc63527ef2260f72ac1a6fd983e8/weave-0.52.16.tar.gz", hash = "sha256:7bb8fdce0393007e9c40fb1769d0606bfe55401c4cd13146457ccac4b49c695d", size = 607024, upload-time = "2025-11-07T19:45:30.898Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/bc/fa5ffb887a1ee28109b29c62416c9e0f41da8e75e6871671208b3d42b392/weave-0.51.59-py3-none-any.whl", hash = "sha256:2238578574ecdf6285efdf028c78987769720242ac75b7b84b1dbc59060468ce", size = 612468, upload-time = "2025-07-25T22:05:05.088Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/87/a54513796605dfaef2c3c23c2733bcb4b24866a623635c057b2ffdb74052/weave-0.52.16-py3-none-any.whl", hash = "sha256:85985b8cf233032c6d915dfac95b3bcccb1304444d99a6b4a61f3666b58146ce", size = 764366, upload-time = "2025-11-07T19:45:28.878Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -37,18 +37,22 @@ const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
|
||||
const appDetail = useAppStore(state => state.appDetail)
|
||||
const setAppDetail = useAppStore(state => state.setAppDetail)
|
||||
|
||||
const isWorkflowApp = appDetail?.mode === AppModeEnum.WORKFLOW
|
||||
const showMCPCard = isInPanel
|
||||
const showTriggerCard = isInPanel && appDetail?.mode === AppModeEnum.WORKFLOW
|
||||
const { data: currentWorkflow } = useAppWorkflow(appDetail?.mode === AppModeEnum.WORKFLOW ? appDetail.id : '')
|
||||
const hasTriggerNode = useMemo(() => {
|
||||
if (appDetail?.mode !== AppModeEnum.WORKFLOW)
|
||||
const showTriggerCard = isInPanel && isWorkflowApp
|
||||
const { data: currentWorkflow } = useAppWorkflow(isWorkflowApp ? appDetail.id : '')
|
||||
const hasTriggerNode = useMemo<boolean | null>(() => {
|
||||
if (!isWorkflowApp)
|
||||
return false
|
||||
const nodes = currentWorkflow?.graph?.nodes || []
|
||||
if (!currentWorkflow)
|
||||
return null
|
||||
const nodes = currentWorkflow.graph?.nodes || []
|
||||
return nodes.some((node) => {
|
||||
const nodeType = node.data?.type as BlockEnum | undefined
|
||||
return !!nodeType && isTriggerNode(nodeType)
|
||||
})
|
||||
}, [appDetail?.mode, currentWorkflow])
|
||||
}, [isWorkflowApp, currentWorkflow])
|
||||
const shouldRenderAppCards = !isWorkflowApp || hasTriggerNode === false
|
||||
|
||||
const updateAppDetail = async () => {
|
||||
try {
|
||||
@@ -123,7 +127,7 @@ const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
|
||||
return (
|
||||
<div className={className || 'mb-6 grid w-full grid-cols-1 gap-6 xl:grid-cols-2'}>
|
||||
{
|
||||
!hasTriggerNode && (
|
||||
shouldRenderAppCards && (
|
||||
<>
|
||||
<AppCard
|
||||
appInfo={appDetail}
|
||||
|
||||
@@ -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'
|
||||
@@ -17,6 +18,7 @@ const Layout = ({ children }: { children: ReactNode }) => {
|
||||
return (
|
||||
<>
|
||||
<GA gaType={GaType.admin} />
|
||||
<AmplitudeProvider />
|
||||
<SwrInitializer>
|
||||
<AppContextProvider>
|
||||
<EventEmitterContextProvider>
|
||||
|
||||
@@ -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,16 @@ 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,
|
||||
time: new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }),
|
||||
has_description: 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,14 @@ 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,
|
||||
time: new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }),
|
||||
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)
|
||||
|
||||
@@ -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 '../../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' },
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
'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'
|
||||
|
||||
export type IAmplitudeProps = {
|
||||
apiKey?: string
|
||||
sessionReplaySampleRate?: number
|
||||
}
|
||||
|
||||
const AmplitudeProvider: FC<IAmplitudeProps> = ({
|
||||
apiKey = '702e89332ab88a7f14e665f417244e9d',
|
||||
sessionReplaySampleRate = 1,
|
||||
}) => {
|
||||
useEffect(() => {
|
||||
// // Only enable in non-CE edition
|
||||
// if (IS_CE_EDITION) {
|
||||
// console.warn('[Amplitude] Amplitude is disabled in CE 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)
|
||||
}, [apiKey, sessionReplaySampleRate])
|
||||
|
||||
// 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()
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
import type { Meta, StoryObj } from '@storybook/nextjs'
|
||||
import React from 'react'
|
||||
|
||||
declare const require: any
|
||||
|
||||
type IconComponent = React.ComponentType<Record<string, unknown>>
|
||||
|
||||
type IconEntry = {
|
||||
name: string
|
||||
category: string
|
||||
path: string
|
||||
Component: IconComponent
|
||||
}
|
||||
|
||||
const iconContext = require.context('./src', true, /\.tsx$/)
|
||||
|
||||
const iconEntries: IconEntry[] = iconContext
|
||||
.keys()
|
||||
.filter((key: string) => !key.endsWith('.stories.tsx') && !key.endsWith('.spec.tsx'))
|
||||
.map((key: string) => {
|
||||
const mod = iconContext(key)
|
||||
const Component = mod.default as IconComponent | undefined
|
||||
if (!Component)
|
||||
return null
|
||||
|
||||
const relativePath = key.replace(/^\.\//, '')
|
||||
const path = `app/components/base/icons/src/${relativePath}`
|
||||
const parts = relativePath.split('/')
|
||||
const fileName = parts.pop() || ''
|
||||
const category = parts.length ? parts.join('/') : '(root)'
|
||||
const name = Component.displayName || fileName.replace(/\.tsx$/, '')
|
||||
|
||||
return {
|
||||
name,
|
||||
category,
|
||||
path,
|
||||
Component,
|
||||
}
|
||||
})
|
||||
.filter(Boolean) as IconEntry[]
|
||||
|
||||
const sortedEntries = [...iconEntries].sort((a, b) => {
|
||||
if (a.category === b.category)
|
||||
return a.name.localeCompare(b.name)
|
||||
return a.category.localeCompare(b.category)
|
||||
})
|
||||
|
||||
const filterEntries = (entries: IconEntry[], query: string) => {
|
||||
const normalized = query.trim().toLowerCase()
|
||||
if (!normalized)
|
||||
return entries
|
||||
|
||||
return entries.filter(entry =>
|
||||
entry.name.toLowerCase().includes(normalized)
|
||||
|| entry.path.toLowerCase().includes(normalized)
|
||||
|| entry.category.toLowerCase().includes(normalized),
|
||||
)
|
||||
}
|
||||
|
||||
const groupByCategory = (entries: IconEntry[]) => entries.reduce((acc, entry) => {
|
||||
if (!acc[entry.category])
|
||||
acc[entry.category] = []
|
||||
|
||||
acc[entry.category].push(entry)
|
||||
return acc
|
||||
}, {} as Record<string, IconEntry[]>)
|
||||
|
||||
const containerStyle: React.CSSProperties = {
|
||||
padding: 24,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 24,
|
||||
}
|
||||
|
||||
const headerStyle: React.CSSProperties = {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 8,
|
||||
}
|
||||
|
||||
const controlsStyle: React.CSSProperties = {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
flexWrap: 'wrap',
|
||||
}
|
||||
|
||||
const searchInputStyle: React.CSSProperties = {
|
||||
padding: '8px 12px',
|
||||
minWidth: 280,
|
||||
borderRadius: 6,
|
||||
border: '1px solid #d0d0d5',
|
||||
}
|
||||
|
||||
const toggleButtonStyle: React.CSSProperties = {
|
||||
padding: '8px 12px',
|
||||
borderRadius: 6,
|
||||
border: '1px solid #d0d0d5',
|
||||
background: '#fff',
|
||||
cursor: 'pointer',
|
||||
}
|
||||
|
||||
const emptyTextStyle: React.CSSProperties = { color: '#5f5f66' }
|
||||
|
||||
const sectionStyle: React.CSSProperties = {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 12,
|
||||
}
|
||||
|
||||
const gridStyle: React.CSSProperties = {
|
||||
display: 'grid',
|
||||
gap: 12,
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))',
|
||||
}
|
||||
|
||||
const cardStyle: React.CSSProperties = {
|
||||
border: '1px solid #e1e1e8',
|
||||
borderRadius: 8,
|
||||
padding: 12,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 8,
|
||||
minHeight: 140,
|
||||
}
|
||||
|
||||
const previewBaseStyle: React.CSSProperties = {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
minHeight: 48,
|
||||
borderRadius: 6,
|
||||
}
|
||||
|
||||
const nameButtonBaseStyle: React.CSSProperties = {
|
||||
display: 'inline-flex',
|
||||
padding: 0,
|
||||
border: 'none',
|
||||
background: 'transparent',
|
||||
font: 'inherit',
|
||||
cursor: 'pointer',
|
||||
textAlign: 'left',
|
||||
fontWeight: 600,
|
||||
}
|
||||
|
||||
const PREVIEW_SIZE = 40
|
||||
|
||||
const IconGalleryStory = () => {
|
||||
const [query, setQuery] = React.useState('')
|
||||
const [copiedPath, setCopiedPath] = React.useState<string | null>(null)
|
||||
const [previewTheme, setPreviewTheme] = React.useState<'light' | 'dark'>('light')
|
||||
|
||||
const filtered = React.useMemo(() => filterEntries(sortedEntries, query), [query])
|
||||
|
||||
const grouped = React.useMemo(() => groupByCategory(filtered), [filtered])
|
||||
|
||||
const categoryOrder = React.useMemo(
|
||||
() => Object.keys(grouped).sort((a, b) => a.localeCompare(b)),
|
||||
[grouped],
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!copiedPath)
|
||||
return undefined
|
||||
|
||||
const timerId = window.setTimeout(() => {
|
||||
setCopiedPath(null)
|
||||
}, 1200)
|
||||
|
||||
return () => window.clearTimeout(timerId)
|
||||
}, [copiedPath])
|
||||
|
||||
const handleCopy = React.useCallback((text: string) => {
|
||||
navigator.clipboard?.writeText(text)
|
||||
.then(() => {
|
||||
setCopiedPath(text)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Failed to copy icon path:', err)
|
||||
})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div style={containerStyle}>
|
||||
<header style={headerStyle}>
|
||||
<h1 style={{ margin: 0 }}>Icon Gallery</h1>
|
||||
<p style={{ margin: 0, color: '#5f5f66' }}>
|
||||
Browse all icon components sourced from <code>app/components/base/icons/src</code>. Use the search bar
|
||||
to filter by name or path.
|
||||
</p>
|
||||
<div style={controlsStyle}>
|
||||
<input
|
||||
style={searchInputStyle}
|
||||
placeholder="Search icons"
|
||||
value={query}
|
||||
onChange={event => setQuery(event.target.value)}
|
||||
/>
|
||||
<span style={{ color: '#5f5f66' }}>{filtered.length} icons</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setPreviewTheme(prev => (prev === 'light' ? 'dark' : 'light'))}
|
||||
style={toggleButtonStyle}
|
||||
>
|
||||
Toggle {previewTheme === 'light' ? 'dark' : 'light'} preview
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
{categoryOrder.length === 0 && (
|
||||
<p style={emptyTextStyle}>No icons match the current filter.</p>
|
||||
)}
|
||||
{categoryOrder.map(category => (
|
||||
<section key={category} style={sectionStyle}>
|
||||
<h2 style={{ margin: 0, fontSize: 18 }}>{category}</h2>
|
||||
<div style={gridStyle}>
|
||||
{grouped[category].map(entry => (
|
||||
<div key={entry.path} style={cardStyle}>
|
||||
<div
|
||||
style={{
|
||||
...previewBaseStyle,
|
||||
background: previewTheme === 'dark' ? '#1f2024' : '#fff',
|
||||
}}
|
||||
>
|
||||
<entry.Component style={{ width: PREVIEW_SIZE, height: PREVIEW_SIZE }} />
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleCopy(entry.path)}
|
||||
style={{
|
||||
...nameButtonBaseStyle,
|
||||
color: copiedPath === entry.path ? '#00754a' : '#24262c',
|
||||
}}
|
||||
>
|
||||
{copiedPath === entry.path ? 'Copied!' : entry.name}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const meta: Meta<typeof IconGalleryStory> = {
|
||||
title: 'Base/Icons/Icon Gallery',
|
||||
component: IconGalleryStory,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof IconGalleryStory>
|
||||
|
||||
export const All: Story = {
|
||||
render: () => <IconGalleryStory />,
|
||||
}
|
||||
@@ -249,6 +249,8 @@ export const useChecklistBeforePublish = () => {
|
||||
const { data: buildInTools } = useAllBuiltInTools()
|
||||
const { data: customTools } = useAllCustomTools()
|
||||
const { data: workflowTools } = useAllWorkflowTools()
|
||||
const appMode = useAppStore.getState().appDetail?.mode
|
||||
const shouldCheckStartNode = appMode === AppModeEnum.WORKFLOW || appMode === AppModeEnum.ADVANCED_CHAT
|
||||
|
||||
const getCheckData = useCallback((data: CommonNodeType<{}>, datasets: DataSet[]) => {
|
||||
let checkData = data
|
||||
@@ -366,17 +368,22 @@ export const useChecklistBeforePublish = () => {
|
||||
}
|
||||
}
|
||||
|
||||
if (!validNodes.find(n => n.id === node.id)) {
|
||||
const isStartNodeMeta = nodesExtraData?.[node.data.type as BlockEnum]?.metaData.isStart ?? false
|
||||
const canSkipConnectionCheck = shouldCheckStartNode ? isStartNodeMeta : true
|
||||
const isUnconnected = !validNodes.find(n => n.id === node.id)
|
||||
|
||||
if (isUnconnected && !canSkipConnectionCheck) {
|
||||
notify({ type: 'error', message: `[${node.data.title}] ${t('workflow.common.needConnectTip')}` })
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const startNodesFiltered = nodes.filter(node => START_NODE_TYPES.includes(node.data.type as BlockEnum))
|
||||
|
||||
if (startNodesFiltered.length === 0) {
|
||||
notify({ type: 'error', message: t('workflow.common.needStartNode') })
|
||||
return false
|
||||
if (shouldCheckStartNode) {
|
||||
const startNodesFiltered = nodes.filter(node => START_NODE_TYPES.includes(node.data.type as BlockEnum))
|
||||
if (startNodesFiltered.length === 0) {
|
||||
notify({ type: 'error', message: t('workflow.common.needStartNode') })
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const isRequiredNodesType = Object.keys(nodesExtraData!).filter((key: any) => (nodesExtraData as any)[key].metaData.isRequired)
|
||||
@@ -391,7 +398,7 @@ export const useChecklistBeforePublish = () => {
|
||||
}
|
||||
|
||||
return true
|
||||
}, [store, notify, t, language, nodesExtraData, strategyProviders, updateDatasetsDetail, getCheckData, workflowStore, buildInTools, customTools, workflowTools])
|
||||
}, [store, notify, t, language, nodesExtraData, strategyProviders, updateDatasetsDetail, getCheckData, workflowStore, buildInTools, customTools, workflowTools, shouldCheckStartNode])
|
||||
|
||||
return {
|
||||
handleCheckBeforePublish,
|
||||
|
||||
@@ -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()
|
||||
@@ -43,6 +44,13 @@ 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',
|
||||
time: new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }),
|
||||
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,13 @@ export default function MailAndPasswordAuth({ isInvite, isEmailSetup, allowRegis
|
||||
body: loginData,
|
||||
})
|
||||
if (res.result === 'success') {
|
||||
// Track login success event
|
||||
trackEvent('user_login_success', {
|
||||
method: 'email_password',
|
||||
time: new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }),
|
||||
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,12 @@ const ChangePasswordForm = () => {
|
||||
})
|
||||
const { result } = res as MailRegisterResponse
|
||||
if (result === 'success') {
|
||||
// Track registration success event
|
||||
trackEvent('user_registration_success', {
|
||||
method: 'email',
|
||||
time: new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }),
|
||||
})
|
||||
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('common.api.actionSuccess'),
|
||||
|
||||
@@ -10,6 +10,7 @@ import MaintenanceNotice from '@/app/components/header/maintenance-notice'
|
||||
import { noop } from 'lodash-es'
|
||||
import { setZendeskConversationFields } from '@/app/components/base/zendesk/utils'
|
||||
import { ZENDESK_FIELD_IDS } from '@/config'
|
||||
import { setUserId, setUserProperties } from '@/app/components/base/amplitude'
|
||||
import { useGlobalPublicStore } from './global-public-context'
|
||||
|
||||
export type AppContextValue = {
|
||||
@@ -159,6 +160,33 @@ export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) =>
|
||||
}, [currentWorkspace?.id])
|
||||
// #endregion Zendesk conversation fields
|
||||
|
||||
// #region Amplitude user tracking
|
||||
useEffect(() => {
|
||||
// Report user info to Amplitude when loaded
|
||||
if (userProfile?.id) {
|
||||
setUserId(userProfile.email)
|
||||
setUserProperties({
|
||||
email: userProfile.email,
|
||||
name: userProfile.name,
|
||||
has_password: userProfile.is_password_set,
|
||||
})
|
||||
}
|
||||
}, [userProfile?.id, userProfile?.email, userProfile?.name, userProfile?.is_password_set])
|
||||
|
||||
useEffect(() => {
|
||||
// Report workspace info to Amplitude when loaded
|
||||
if (currentWorkspace?.id && userProfile?.id) {
|
||||
setUserProperties({
|
||||
workspace_id: currentWorkspace.id,
|
||||
workspace_name: currentWorkspace.name,
|
||||
workspace_plan: currentWorkspace.plan,
|
||||
workspace_status: currentWorkspace.status,
|
||||
workspace_role: currentWorkspace.role,
|
||||
})
|
||||
}
|
||||
}, [currentWorkspace?.id, currentWorkspace?.name, currentWorkspace?.plan, currentWorkspace?.status, currentWorkspace?.role, userProfile?.id])
|
||||
// #endregion Amplitude user tracking
|
||||
|
||||
return (
|
||||
<AppContext.Provider value={{
|
||||
userProfile,
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -44,6 +44,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",
|
||||
|
||||
Generated
+1837
-1577
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user