mirror of
https://github.com/langgenius/dify.git
synced 2026-02-09 01:33:59 +00:00
Compare commits
9 Commits
mysql-adap
...
feat/ampli
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f01c06451 | ||
|
|
6b95723f1c | ||
|
|
4a89403566 | ||
|
|
e0c05b2123 | ||
|
|
85b99580ea | ||
|
|
15fbedfcad | ||
|
|
1e6d0de48b | ||
|
|
cad751c00c | ||
|
|
a47276ac24 |
@@ -3,7 +3,7 @@ import io
|
||||
import json
|
||||
from collections.abc import Generator
|
||||
|
||||
from google.cloud import storage as google_cloud_storage
|
||||
from google.cloud import storage as google_cloud_storage # type: ignore
|
||||
|
||||
from configs import dify_config
|
||||
from extensions.storage.base_storage import BaseStorage
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "dify-api"
|
||||
version = "1.9.2"
|
||||
version = "1.10.0"
|
||||
requires-python = ">=3.11,<3.13"
|
||||
|
||||
dependencies = [
|
||||
@@ -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.
|
||||
|
||||
1598
api/uv.lock
generated
1598
api/uv.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@ x-shared-env: &shared-api-worker-env
|
||||
services:
|
||||
# API service
|
||||
api:
|
||||
image: langgenius/dify-api:1.10.0-rc1
|
||||
image: langgenius/dify-api:1.10.0
|
||||
restart: always
|
||||
environment:
|
||||
# Use the shared environment variables.
|
||||
@@ -31,7 +31,7 @@ services:
|
||||
# worker service
|
||||
# The Celery worker for processing all queues (dataset, workflow, mail, etc.)
|
||||
worker:
|
||||
image: langgenius/dify-api:1.10.0-rc1
|
||||
image: langgenius/dify-api:1.10.0
|
||||
restart: always
|
||||
environment:
|
||||
# Use the shared environment variables.
|
||||
@@ -58,7 +58,7 @@ services:
|
||||
# worker_beat service
|
||||
# Celery beat for scheduling periodic tasks.
|
||||
worker_beat:
|
||||
image: langgenius/dify-api:1.10.0-rc1
|
||||
image: langgenius/dify-api:1.10.0
|
||||
restart: always
|
||||
environment:
|
||||
# Use the shared environment variables.
|
||||
@@ -76,7 +76,7 @@ services:
|
||||
|
||||
# Frontend web application.
|
||||
web:
|
||||
image: langgenius/dify-web:1.10.0-rc1
|
||||
image: langgenius/dify-web:1.10.0
|
||||
restart: always
|
||||
environment:
|
||||
CONSOLE_API_URL: ${CONSOLE_API_URL:-}
|
||||
@@ -182,7 +182,7 @@ services:
|
||||
|
||||
# plugin daemon
|
||||
plugin_daemon:
|
||||
image: langgenius/dify-plugin-daemon:0.4.0-local
|
||||
image: langgenius/dify-plugin-daemon:0.4.1-local
|
||||
restart: always
|
||||
environment:
|
||||
# Use the shared environment variables.
|
||||
|
||||
@@ -625,7 +625,7 @@ x-shared-env: &shared-api-worker-env
|
||||
services:
|
||||
# API service
|
||||
api:
|
||||
image: langgenius/dify-api:1.10.0-rc1
|
||||
image: langgenius/dify-api:1.10.0
|
||||
restart: always
|
||||
environment:
|
||||
# Use the shared environment variables.
|
||||
@@ -654,7 +654,7 @@ services:
|
||||
# worker service
|
||||
# The Celery worker for processing all queues (dataset, workflow, mail, etc.)
|
||||
worker:
|
||||
image: langgenius/dify-api:1.10.0-rc1
|
||||
image: langgenius/dify-api:1.10.0
|
||||
restart: always
|
||||
environment:
|
||||
# Use the shared environment variables.
|
||||
@@ -681,7 +681,7 @@ services:
|
||||
# worker_beat service
|
||||
# Celery beat for scheduling periodic tasks.
|
||||
worker_beat:
|
||||
image: langgenius/dify-api:1.10.0-rc1
|
||||
image: langgenius/dify-api:1.10.0
|
||||
restart: always
|
||||
environment:
|
||||
# Use the shared environment variables.
|
||||
@@ -699,7 +699,7 @@ services:
|
||||
|
||||
# Frontend web application.
|
||||
web:
|
||||
image: langgenius/dify-web:1.10.0-rc1
|
||||
image: langgenius/dify-web:1.10.0
|
||||
restart: always
|
||||
environment:
|
||||
CONSOLE_API_URL: ${CONSOLE_API_URL:-}
|
||||
@@ -805,7 +805,7 @@ services:
|
||||
|
||||
# plugin daemon
|
||||
plugin_daemon:
|
||||
image: langgenius/dify-plugin-daemon:0.4.0-local
|
||||
image: langgenius/dify-plugin-daemon:0.4.1-local
|
||||
restart: always
|
||||
environment:
|
||||
# Use the shared environment variables.
|
||||
|
||||
@@ -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/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/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>
|
||||
|
||||
43
web/app/components/amplitude/AmplitudeProvider.tsx
Normal file
43
web/app/components/amplitude/AmplitudeProvider.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
'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
|
||||
enableSessionReplay?: boolean
|
||||
}
|
||||
|
||||
const AmplitudeProvider: FC<IAmplitudeProps> = ({
|
||||
apiKey = '702e89332ab88a7f14e665f417244e9d',
|
||||
}) => {
|
||||
useEffect(() => {
|
||||
// // Only enable in non-CE edition
|
||||
// if (IS_CE_EDITION) {
|
||||
// console.warn('[Amplitude] Amplitude is disabled in CE edition')
|
||||
// return
|
||||
// }
|
||||
|
||||
const sessionReplay = sessionReplayPlugin({
|
||||
sampleRate: 0.1,
|
||||
})
|
||||
|
||||
amplitude.add(sessionReplay)
|
||||
|
||||
amplitude.init(apiKey, {
|
||||
defaultTracking: {
|
||||
sessions: true,
|
||||
pageViews: true,
|
||||
formInteractions: true,
|
||||
fileDownloads: true,
|
||||
},
|
||||
logLevel: amplitude.Types.LogLevel.Warn,
|
||||
})
|
||||
}, [apiKey])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export default React.memo(AmplitudeProvider)
|
||||
2
web/app/components/amplitude/index.ts
Normal file
2
web/app/components/amplitude/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default } from './AmplitudeProvider'
|
||||
export { resetUser, setUserId, setUserProperties, trackEvent } from './utils'
|
||||
37
web/app/components/amplitude/utils.ts
Normal file
37
web/app/components/amplitude/utils.ts
Normal file
@@ -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()
|
||||
}
|
||||
@@ -49,6 +49,7 @@ import { fetchInstalledAppList } from '@/service/explore'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import type { PublishWorkflowParams } from '@/types/workflow'
|
||||
import { basePath } from '@/utils/var'
|
||||
import { trackEvent } from '../../amplitude'
|
||||
|
||||
const ACCESS_MODE_MAP: Record<AccessMode, { label: string, icon: React.ElementType }> = {
|
||||
[AccessMode.ORGANIZATION]: {
|
||||
@@ -182,6 +183,12 @@ const AppPublisher = ({
|
||||
try {
|
||||
await onPublish?.(params)
|
||||
setPublished(true)
|
||||
trackEvent('app·_published_time', {
|
||||
app_mode: appDetail?.mode,
|
||||
app_id: appDetail?.id,
|
||||
app_name: appDetail?.name,
|
||||
time: new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }),
|
||||
})
|
||||
}
|
||||
catch {
|
||||
setPublished(false)
|
||||
|
||||
@@ -42,6 +42,7 @@ import { getProcessedFilesFromResponse } from '@/app/components/base/file-upload
|
||||
import cn from '@/utils/classnames'
|
||||
import { noop } from 'lodash-es'
|
||||
import PromptLogModal from '../../base/prompt-log-modal'
|
||||
import { WorkflowContextProvider } from '@/app/components/workflow/context'
|
||||
|
||||
type AppStoreState = ReturnType<typeof useAppStore.getState>
|
||||
type ConversationListItem = ChatConversationGeneralDetail | CompletionConversationGeneralDetail
|
||||
@@ -779,15 +780,17 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
||||
}
|
||||
</div>
|
||||
{showMessageLogModal && (
|
||||
<MessageLogModal
|
||||
width={width}
|
||||
currentLogItem={currentLogItem}
|
||||
onCancel={() => {
|
||||
setCurrentLogItem()
|
||||
setShowMessageLogModal(false)
|
||||
}}
|
||||
defaultTab={currentLogModalActiveTab}
|
||||
/>
|
||||
<WorkflowContextProvider>
|
||||
<MessageLogModal
|
||||
width={width}
|
||||
currentLogItem={currentLogItem}
|
||||
onCancel={() => {
|
||||
setCurrentLogItem()
|
||||
setShowMessageLogModal(false)
|
||||
}}
|
||||
defaultTab={currentLogModalActiveTab}
|
||||
/>
|
||||
</WorkflowContextProvider>
|
||||
)}
|
||||
{!isChatMode && showPromptLogModal && (
|
||||
<PromptLogModal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
import { RiDiscordFill, RiGithubFill } from '@remixicon/react'
|
||||
import { RiDiscordFill, RiDiscussLine, RiGithubFill } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type CustomLinkProps = {
|
||||
@@ -38,6 +38,9 @@ const Footer = () => {
|
||||
<CustomLink href='https://discord.gg/FngNHpbcY7'>
|
||||
<RiDiscordFill className='h-5 w-5 text-text-tertiary' />
|
||||
</CustomLink>
|
||||
<CustomLink href='https://forum.dify.ai'>
|
||||
<RiDiscussLine className='h-5 w-5 text-text-tertiary' />
|
||||
</CustomLink>
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
|
||||
258
web/app/components/base/icons/icon-gallery.stories.tsx
Normal file
258
web/app/components/base/icons/icon-gallery.stories.tsx
Normal file
@@ -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 />,
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { useStore } from '@/app/components/app/store'
|
||||
import type { WorkflowRunDetailResponse } from '@/models/log'
|
||||
import type { NodeTracing, NodeTracingListResponse } from '@/types/workflow'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { WorkflowContextProvider } from '@/app/components/workflow/context'
|
||||
|
||||
const SAMPLE_APP_DETAIL = {
|
||||
id: 'app-demo-1',
|
||||
@@ -143,10 +144,12 @@ const MessageLogPreview = (props: MessageLogModalProps) => {
|
||||
|
||||
return (
|
||||
<div className="relative min-h-[640px] w-full bg-background-default-subtle p-6">
|
||||
<MessageLogModal
|
||||
{...props}
|
||||
currentLogItem={mockCurrentLogItem}
|
||||
/>
|
||||
<WorkflowContextProvider>
|
||||
<MessageLogModal
|
||||
{...props}
|
||||
currentLogItem={mockCurrentLogItem}
|
||||
/>
|
||||
</WorkflowContextProvider>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useCreatePipelineDataset } from '@/service/knowledge/use-create-dataset
|
||||
import { useInvalidDatasetList } from '@/service/knowledge/use-dataset'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { trackEvent } from '@/app/components/amplitude'
|
||||
|
||||
const CreateCard = () => {
|
||||
const { t } = useTranslation()
|
||||
@@ -22,6 +23,9 @@ const CreateCard = () => {
|
||||
type: 'success',
|
||||
message: t('datasetPipeline.creation.successTip'),
|
||||
})
|
||||
trackEvent('create_datasets_from_scratch', {
|
||||
name: data.name,
|
||||
})
|
||||
invalidDatasetList()
|
||||
push(`/datasets/${id}/pipeline`)
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import Content from './content'
|
||||
import Actions from './actions'
|
||||
import { useCreatePipelineDatasetFromCustomized } from '@/service/knowledge/use-create-dataset'
|
||||
import { useInvalidDatasetList } from '@/service/knowledge/use-dataset'
|
||||
import { trackEvent } from '@/app/components/amplitude'
|
||||
|
||||
type TemplateCardProps = {
|
||||
pipeline: PipelineTemplate
|
||||
@@ -63,6 +64,13 @@ const TemplateCard = ({
|
||||
type: 'success',
|
||||
message: t('datasetPipeline.creation.successTip'),
|
||||
})
|
||||
|
||||
trackEvent('create_datasets_with_pipeline', {
|
||||
template_type: type,
|
||||
template_name: pipeline.name,
|
||||
chunk_structure: pipeline.chunk_structure,
|
||||
})
|
||||
|
||||
invalidDatasetList()
|
||||
if (newDataset.pipeline_id)
|
||||
await handleCheckPluginDependencies(newDataset.pipeline_id, true)
|
||||
@@ -75,7 +83,7 @@ const TemplateCard = ({
|
||||
})
|
||||
},
|
||||
})
|
||||
}, [getPipelineTemplateInfo, createDataset, t, handleCheckPluginDependencies, push, invalidDatasetList])
|
||||
}, [getPipelineTemplateInfo, createDataset, t, handleCheckPluginDependencies, push, invalidDatasetList, pipeline, type])
|
||||
|
||||
const handleShowTemplateDetails = useCallback(() => {
|
||||
setShowDetailModal(true)
|
||||
|
||||
@@ -12,6 +12,7 @@ import Button from '@/app/components/base/button'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import { createEmptyDataset } from '@/service/datasets'
|
||||
import { useInvalidDatasetList } from '@/service/knowledge/use-dataset'
|
||||
import { trackEvent } from '@/app/components/amplitude'
|
||||
|
||||
type IProps = {
|
||||
show: boolean
|
||||
@@ -41,6 +42,9 @@ const EmptyDatasetCreationModal = ({
|
||||
const dataset = await createEmptyDataset({ name: inputValue })
|
||||
invalidDatasetList()
|
||||
onHide()
|
||||
trackEvent('create_empty_datasets', {
|
||||
name: inputValue,
|
||||
})
|
||||
router.push(`/datasets/${dataset.id}/documents`)
|
||||
}
|
||||
catch {
|
||||
|
||||
@@ -63,6 +63,7 @@ import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/aler
|
||||
import { noop } from 'lodash-es'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
import { useInvalidDatasetList } from '@/service/knowledge/use-dataset'
|
||||
import { trackEvent } from '@/app/components/amplitude'
|
||||
|
||||
const TextLabel: FC<PropsWithChildren> = (props) => {
|
||||
return <label className='system-sm-semibold text-text-secondary'>{props.children}</label>
|
||||
@@ -571,6 +572,12 @@ const StepTwo = ({
|
||||
updateIndexingTypeCache?.(indexType as string)
|
||||
updateResultCache?.(data)
|
||||
updateRetrievalMethodCache?.(retrievalConfig.search_method as string)
|
||||
|
||||
trackEvent('create_datasets', {
|
||||
data_source_type: dataSourceType,
|
||||
indexing_technique: indexType,
|
||||
doc_form: currentDocForm,
|
||||
})
|
||||
},
|
||||
},
|
||||
)
|
||||
@@ -581,6 +588,13 @@ const StepTwo = ({
|
||||
updateIndexingTypeCache?.(indexType as string)
|
||||
updateResultCache?.(data)
|
||||
updateRetrievalMethodCache?.(retrievalConfig.search_method as string)
|
||||
|
||||
// Track document addition to existing dataset
|
||||
trackEvent('dataset_document_added', {
|
||||
data_source_type: dataSourceType,
|
||||
indexing_technique: indexType,
|
||||
doc_form: currentDocForm,
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useToastContext } from '@/app/components/base/toast'
|
||||
import ExternalKnowledgeBaseCreate from '@/app/components/datasets/external-knowledge-base/create'
|
||||
import type { CreateKnowledgeBaseReq } from '@/app/components/datasets/external-knowledge-base/create/declarations'
|
||||
import { createExternalKnowledgeBase } from '@/service/datasets'
|
||||
import { trackEvent } from '@/app/components/amplitude'
|
||||
|
||||
const ExternalKnowledgeBaseConnector = () => {
|
||||
const { notify } = useToastContext()
|
||||
@@ -17,6 +18,9 @@ const ExternalKnowledgeBaseConnector = () => {
|
||||
setLoading(true)
|
||||
const result = await createExternalKnowledgeBase({ body: formValue })
|
||||
if (result && result.id) {
|
||||
trackEvent('create_external_knowledge_base', {
|
||||
value: formValue,
|
||||
})
|
||||
notify({ type: 'success', message: 'External Knowledge Base Connected Successfully' })
|
||||
router.back()
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ import { AUTO_UPDATE_MODE } from '../reference-setting-modal/auto-update-setting
|
||||
import { convertUTCDaySecondsToLocalSeconds, timeOfDayToDayjs } from '../reference-setting-modal/auto-update-setting/utils'
|
||||
import type { PluginDetail } from '../types'
|
||||
import { PluginCategoryEnum, PluginSource } from '../types'
|
||||
import { trackEvent } from '../../amplitude'
|
||||
|
||||
const i18nPrefix = 'plugin.action'
|
||||
|
||||
@@ -199,6 +200,14 @@ const DetailHeader = ({
|
||||
const handleDelete = useCallback(async () => {
|
||||
showDeleting()
|
||||
const res = await uninstallPlugin(id)
|
||||
|
||||
trackEvent('plugin_deleted', {
|
||||
plugin_id: id,
|
||||
plugin_name: label[locale],
|
||||
plugin_category: category,
|
||||
plugin_source: source,
|
||||
plugin_version: version,
|
||||
})
|
||||
hideDeleting()
|
||||
if (res.success) {
|
||||
hideDeleteConfirm()
|
||||
|
||||
@@ -30,6 +30,7 @@ import { useCategories } from '../hooks'
|
||||
import { usePluginPageContext } from '../plugin-page/context'
|
||||
import { PluginCategoryEnum, type PluginDetail, PluginSource } from '../types'
|
||||
import Action from './action'
|
||||
import { trackEvent } from '../../amplitude'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
@@ -77,6 +78,10 @@ const PluginItem: FC<Props> = ({
|
||||
}, [status, deprecated_reason])
|
||||
|
||||
const handleDelete = useCallback(() => {
|
||||
trackEvent('plugin_deleted', {
|
||||
plugin_id,
|
||||
plugin_name: name,
|
||||
})
|
||||
refreshPluginList({ category } as any)
|
||||
}, [category, refreshPluginList])
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { getKeyboardKeyNameBySystem } from '@/app/components/workflow/utils'
|
||||
import cn from '@/utils/classnames'
|
||||
import { RiCloseLine, RiDatabase2Line, RiLoader2Line, RiPlayLargeLine } from '@remixicon/react'
|
||||
import { StopCircle } from '@/app/components/base/icons/src/vender/line/mediaAndDevices'
|
||||
import { trackEvent } from '@/app/components/amplitude'
|
||||
|
||||
type RunModeProps = {
|
||||
text?: string
|
||||
@@ -53,6 +54,9 @@ const RunMode = ({
|
||||
isDisabled ? 'rounded-l-md' : 'rounded-md',
|
||||
)}
|
||||
onClick={() => {
|
||||
trackEvent('pipeline_start_action_time', {
|
||||
time: new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }),
|
||||
})
|
||||
handleWorkflowStartRunInWorkflow()
|
||||
}}
|
||||
disabled={isDisabled}
|
||||
|
||||
@@ -11,6 +11,7 @@ import BlockIcon from '../../block-icon'
|
||||
import cn from '@/utils/classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { basePath } from '@/utils/var'
|
||||
import { trackEvent } from '@/app/components/amplitude'
|
||||
|
||||
const normalizeProviderIcon = (icon: ToolWithProvider['icon']) => {
|
||||
if (typeof icon === 'string' && basePath && icon.startsWith('/') && !icon.startsWith(`${basePath}/`))
|
||||
@@ -67,6 +68,13 @@ const ToolItem: FC<Props> = ({
|
||||
params[item.name] = ''
|
||||
})
|
||||
}
|
||||
trackEvent('tool_selected', {
|
||||
tool_name: payload.name,
|
||||
tool_parameters: payload.parameters,
|
||||
tool_params: params,
|
||||
plugin_id: provider.plugin_id,
|
||||
plugin_unique_identifier: provider.plugin_unique_identifier,
|
||||
})
|
||||
onSelect(BlockEnum.Tool, {
|
||||
provider_id: provider.id,
|
||||
provider_type: provider.type,
|
||||
|
||||
@@ -12,6 +12,7 @@ import { StopCircle } from '@/app/components/base/icons/src/vender/line/mediaAnd
|
||||
import { useDynamicTestRunOptions } from '../hooks/use-dynamic-test-run-options'
|
||||
import TestRunMenu, { type TestRunMenuRef, type TriggerOption, TriggerType } from './test-run-menu'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import { trackEvent } from '../../amplitude'
|
||||
|
||||
type RunModeProps = {
|
||||
text?: string
|
||||
@@ -67,6 +68,11 @@ const RunMode = ({
|
||||
return
|
||||
}
|
||||
|
||||
trackEvent('app_start_action_time', {
|
||||
time: new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }),
|
||||
action_type: option.type,
|
||||
})
|
||||
|
||||
if (option.type === TriggerType.UserInput) {
|
||||
handleWorkflowStartRunInWorkflow()
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useCallback } from 'react'
|
||||
import { produce } from 'immer'
|
||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import { WorkflowRunningStatus } from '@/app/components/workflow/types'
|
||||
import { trackEvent } from '@/app/components/amplitude'
|
||||
|
||||
export const useWorkflowFailed = () => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
@@ -18,6 +19,12 @@ export const useWorkflowFailed = () => {
|
||||
status: WorkflowRunningStatus.Failed,
|
||||
}
|
||||
}))
|
||||
|
||||
trackEvent('workflow_run_failed', {
|
||||
workflow_id: workflowRunningData?.task_id,
|
||||
error: workflowRunningData?.result.error,
|
||||
data: workflowRunningData?.result,
|
||||
})
|
||||
}, [workflowStore])
|
||||
|
||||
return {
|
||||
|
||||
@@ -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/amplitude'
|
||||
|
||||
export type AppContextValue = {
|
||||
userProfile: UserProfileResponse
|
||||
@@ -159,6 +160,29 @@ export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) =>
|
||||
}, [currentWorkspace?.id])
|
||||
// #endregion Zendesk conversation fields
|
||||
|
||||
useEffect(() => {
|
||||
if (userProfile?.id) {
|
||||
setUserId(userProfile.email)
|
||||
setUserProperties({
|
||||
email: userProfile.email,
|
||||
name: userProfile.name,
|
||||
has_password: userProfile.is_password_set,
|
||||
})
|
||||
}
|
||||
}, [userProfile])
|
||||
|
||||
useEffect(() => {
|
||||
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])
|
||||
|
||||
return (
|
||||
<AppContext.Provider value={{
|
||||
userProfile,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dify-web",
|
||||
"version": "1.10.0-rc1",
|
||||
"version": "1.10.0",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@10.22.0+sha512.bf049efe995b28f527fd2b41ae0474ce29186f7edcb3bf545087bd61fbbebb2bf75362d1307fda09c2d288e1e499787ac12d4fcb617a974718a6051f2eee741c",
|
||||
"engines": {
|
||||
@@ -44,6 +44,8 @@
|
||||
"knip": "knip"
|
||||
},
|
||||
"dependencies": {
|
||||
"@amplitude/analytics-browser": "^2.30.1",
|
||||
"@amplitude/plugin-session-replay-browser": "^1.23.2",
|
||||
"@emoji-mart/data": "^1.2.1",
|
||||
"@floating-ui/react": "^0.26.28",
|
||||
"@formatjs/intl-localematcher": "^0.5.10",
|
||||
|
||||
3047
web/pnpm-lock.yaml
generated
3047
web/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user