chore(web): new lint setup (#30020)

Co-authored-by: yyh <yuanyouhuilyz@gmail.com>
This commit is contained in:
Stephen Zhou 2025-12-23 16:58:55 +08:00 committed by GitHub
parent 9701a2994b
commit f2842da397
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3356 changed files with 85046 additions and 81278 deletions

View File

@ -68,25 +68,4 @@ jobs:
run: |
uvx --python 3.13 mdformat . --exclude ".claude/skills/**/SKILL.md"
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
package_json_file: web/package.json
run_install: false
- name: Setup NodeJS
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
cache-dependency-path: ./web/pnpm-lock.yaml
- name: Web dependencies
working-directory: ./web
run: pnpm install --frozen-lockfile
- name: oxlint
working-directory: ./web
run: pnpm exec oxlint --config .oxlintrc.json --fix .
- uses: autofix-ci/action@635ffb0c9798bd160680f18fd73371e355b85f27

11
.gitignore vendored
View File

@ -139,7 +139,6 @@ pyrightconfig.json
.idea/'
.DS_Store
web/.vscode/settings.json
# Intellij IDEA Files
.idea/*
@ -205,7 +204,6 @@ sdks/python-client/dify_client.egg-info
!.vscode/launch.json.template
!.vscode/README.md
api/.vscode
web/.vscode
# vscode Code History Extension
.history
@ -220,15 +218,6 @@ plugins.jsonl
# mise
mise.toml
# Next.js build output
.next/
# PWA generated files
web/public/sw.js
web/public/sw.js.map
web/public/workbox-*.js
web/public/workbox-*.js.map
web/public/fallback-*.js
# AI Assistant
.roo/

10
web/.gitignore vendored
View File

@ -54,3 +54,13 @@ package-lock.json
# mise
mise.toml
# PWA generated files
public/sw.js
public/sw.js.map
public/workbox-*.js
public/workbox-*.js.map
public/fallback-*.js
.vscode/settings.json
.vscode/mcp.json

View File

@ -1,144 +0,0 @@
{
"plugins": [
"unicorn",
"typescript",
"oxc"
],
"categories": {},
"rules": {
"for-direction": "error",
"no-async-promise-executor": "error",
"no-caller": "error",
"no-class-assign": "error",
"no-compare-neg-zero": "error",
"no-cond-assign": "warn",
"no-const-assign": "warn",
"no-constant-binary-expression": "error",
"no-constant-condition": "warn",
"no-control-regex": "warn",
"no-debugger": "warn",
"no-delete-var": "warn",
"no-dupe-class-members": "warn",
"no-dupe-else-if": "warn",
"no-dupe-keys": "warn",
"no-duplicate-case": "warn",
"no-empty-character-class": "warn",
"no-empty-pattern": "warn",
"no-empty-static-block": "warn",
"no-eval": "warn",
"no-ex-assign": "warn",
"no-extra-boolean-cast": "warn",
"no-func-assign": "warn",
"no-global-assign": "warn",
"no-import-assign": "warn",
"no-invalid-regexp": "warn",
"no-irregular-whitespace": "warn",
"no-loss-of-precision": "warn",
"no-new-native-nonconstructor": "warn",
"no-nonoctal-decimal-escape": "warn",
"no-obj-calls": "warn",
"no-self-assign": "warn",
"no-setter-return": "warn",
"no-shadow-restricted-names": "warn",
"no-sparse-arrays": "warn",
"no-this-before-super": "warn",
"no-unassigned-vars": "warn",
"no-unsafe-finally": "warn",
"no-unsafe-negation": "warn",
"no-unsafe-optional-chaining": "error",
"no-unused-labels": "warn",
"no-unused-private-class-members": "warn",
"no-unused-vars": "warn",
"no-useless-backreference": "warn",
"no-useless-catch": "error",
"no-useless-escape": "warn",
"no-useless-rename": "warn",
"no-with": "warn",
"require-yield": "warn",
"use-isnan": "warn",
"valid-typeof": "warn",
"oxc/bad-array-method-on-arguments": "warn",
"oxc/bad-char-at-comparison": "warn",
"oxc/bad-comparison-sequence": "warn",
"oxc/bad-min-max-func": "warn",
"oxc/bad-object-literal-comparison": "warn",
"oxc/bad-replace-all-arg": "warn",
"oxc/const-comparisons": "warn",
"oxc/double-comparisons": "warn",
"oxc/erasing-op": "warn",
"oxc/missing-throw": "warn",
"oxc/number-arg-out-of-range": "warn",
"oxc/only-used-in-recursion": "warn",
"oxc/uninvoked-array-callback": "warn",
"typescript/await-thenable": "warn",
"typescript/no-array-delete": "warn",
"typescript/no-base-to-string": "warn",
"typescript/no-confusing-void-expression": "warn",
"typescript/no-duplicate-enum-values": "warn",
"typescript/no-duplicate-type-constituents": "warn",
"typescript/no-extra-non-null-assertion": "warn",
"typescript/no-floating-promises": "warn",
"typescript/no-for-in-array": "warn",
"typescript/no-implied-eval": "warn",
"typescript/no-meaningless-void-operator": "warn",
"typescript/no-misused-new": "warn",
"typescript/no-misused-spread": "warn",
"typescript/no-non-null-asserted-optional-chain": "warn",
"typescript/no-redundant-type-constituents": "warn",
"typescript/no-this-alias": "warn",
"typescript/no-unnecessary-parameter-property-assignment": "warn",
"typescript/no-unsafe-declaration-merging": "warn",
"typescript/no-unsafe-unary-minus": "warn",
"typescript/no-useless-empty-export": "warn",
"typescript/no-wrapper-object-types": "warn",
"typescript/prefer-as-const": "warn",
"typescript/require-array-sort-compare": "warn",
"typescript/restrict-template-expressions": "warn",
"typescript/triple-slash-reference": "warn",
"typescript/unbound-method": "warn",
"unicorn/no-await-in-promise-methods": "warn",
"unicorn/no-empty-file": "warn",
"unicorn/no-invalid-fetch-options": "warn",
"unicorn/no-invalid-remove-event-listener": "warn",
"unicorn/no-new-array": "warn",
"unicorn/no-single-promise-in-promise-methods": "warn",
"unicorn/no-thenable": "warn",
"unicorn/no-unnecessary-await": "warn",
"unicorn/no-useless-fallback-in-spread": "warn",
"unicorn/no-useless-length-check": "warn",
"unicorn/no-useless-spread": "warn",
"unicorn/prefer-set-size": "warn",
"unicorn/prefer-string-starts-ends-with": "warn"
},
"settings": {
"jsx-a11y": {
"polymorphicPropName": null,
"components": {},
"attributes": {}
},
"next": {
"rootDir": []
},
"react": {
"formComponents": [],
"linkComponents": []
},
"jsdoc": {
"ignorePrivate": false,
"ignoreInternal": false,
"ignoreReplacesDocs": true,
"overrideReplacesDocs": true,
"augmentsExtendsReplacesDocs": false,
"implementsReplacesDocs": false,
"exemptDestructuredRootsFromChecks": false,
"tagNamePreference": {}
}
},
"env": {
"builtin": true
},
"globals": {},
"ignorePatterns": [
"**/*.js"
]
}

View File

@ -1,8 +1,8 @@
import type { Preview } from '@storybook/react'
import { withThemeByDataAttribute } from '@storybook/addon-themes'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import I18N from '../app/components/i18n'
import { ToastProvider } from '../app/components/base/toast'
import I18N from '../app/components/i18n'
import '../app/styles/globals.css'
import '../app/styles/markdown.scss'

View File

@ -1,6 +1,6 @@
import { useState } from 'react'
import type { ReactNode } from 'react'
import { useStore } from '@tanstack/react-form'
import { useState } from 'react'
import { useAppForm } from '@/app/components/base/form'
type UseAppFormOptions = Parameters<typeof useAppForm>[0]
@ -49,7 +49,12 @@ export const FormStoryWrapper = ({
<aside className="w-full max-w-sm rounded-xl border border-divider-subtle bg-components-panel-bg p-4 text-xs text-text-secondary shadow-sm">
<div className="flex items-center justify-between text-[11px] uppercase tracking-wide text-text-tertiary">
<span>Form State</span>
<span>{submitCount} submit{submitCount === 1 ? '' : 's'}</span>
<span>
{submitCount}
{' '}
submit
{submitCount === 1 ? '' : 's'}
</span>
</div>
<dl className="mt-2 space-y-1">
<div className="flex items-center justify-between rounded-md bg-components-button-tertiary-bg px-2 py-1">

View File

@ -1,15 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Launch Chrome against localhost",
"url": "http://localhost:3000",
"webRoot": "${workspaceFolder}"
}
]
}
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Launch Chrome against localhost",
"url": "http://localhost:3000",
"webRoot": "${workspaceFolder}"
}
]
}

View File

@ -1,26 +1,50 @@
{
// Disable the default formatter, use eslint instead
"prettier.enable": false,
"editor.formatOnSave": true,
"editor.formatOnSave": false,
// Auto fix
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
"source.fixAll.eslint": "explicit",
"source.organizeImports": "never"
},
"eslint.format.enable": true,
"[python]": {
"editor.formatOnType": true
},
"[html]": {
"editor.defaultFormatter": "vscode.html-language-features"
},
"[typescriptreact]": {
"editor.defaultFormatter": "vscode.typescript-language-features"
},
"[javascriptreact]": {
"editor.defaultFormatter": "vscode.typescript-language-features"
},
"[jsonc]": {
"editor.defaultFormatter": "vscode.json-language-features"
},
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true,
"npm.packageManager": "pnpm"
// Silent the stylistic rules in your IDE, but still auto fix them
"eslint.rules.customizations": [
{ "rule": "style/*", "severity": "off", "fixable": true },
{ "rule": "format/*", "severity": "off", "fixable": true },
{ "rule": "*-indent", "severity": "off", "fixable": true },
{ "rule": "*-spacing", "severity": "off", "fixable": true },
{ "rule": "*-spaces", "severity": "off", "fixable": true },
{ "rule": "*-order", "severity": "off", "fixable": true },
{ "rule": "*-dangle", "severity": "off", "fixable": true },
{ "rule": "*-newline", "severity": "off", "fixable": true },
{ "rule": "*quotes", "severity": "off", "fixable": true },
{ "rule": "*semi", "severity": "off", "fixable": true }
],
// Enable eslint for all supported languages
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"vue",
"html",
"markdown",
"json",
"jsonc",
"yaml",
"toml",
"xml",
"gql",
"graphql",
"astro",
"svelte",
"css",
"less",
"scss",
"pcss",
"postcss"
]
}

View File

@ -1,7 +1,7 @@
import type { Plan, UsagePlanInfo } from '@/app/components/billing/type'
import type { ProviderContextState } from '@/context/provider-context'
import { merge, noop } from 'lodash-es'
import { defaultPlan } from '@/app/components/billing/config'
import type { ProviderContextState } from '@/context/provider-context'
import type { Plan, UsagePlanInfo } from '@/app/components/billing/type'
// Avoid being mocked in tests
export const baseProviderContextValue: ProviderContextState = {

View File

@ -33,8 +33,7 @@ describe('check-i18n script functionality', () => {
const filePath = path.join(folderPath, file)
const fileName = file.replace(/\.[^/.]+$/, '')
const camelCaseFileName = fileName.replace(/[-_](.)/g, (_, c) =>
c.toUpperCase(),
)
c.toUpperCase())
try {
const content = fs.readFileSync(filePath, 'utf8')
@ -617,9 +616,10 @@ export default translation
// Check if this line ends the value (ends with quote and comma/no comma)
if ((trimmed.endsWith('\',') || trimmed.endsWith('",') || trimmed.endsWith('`,')
|| trimmed.endsWith('\'') || trimmed.endsWith('"') || trimmed.endsWith('`'))
&& !trimmed.startsWith('//'))
|| trimmed.endsWith('\'') || trimmed.endsWith('"') || trimmed.endsWith('`'))
&& !trimmed.startsWith('//')) {
break
}
}
else {
break

View File

@ -15,19 +15,19 @@ describe('Description Validation Logic', () => {
}
describe('Backend Validation Function', () => {
test('allows description within 400 characters', () => {
it('allows description within 400 characters', () => {
const validDescription = 'x'.repeat(400)
expect(() => validateDescriptionLength(validDescription)).not.toThrow()
expect(validateDescriptionLength(validDescription)).toBe(validDescription)
})
test('allows empty description', () => {
it('allows empty description', () => {
expect(() => validateDescriptionLength('')).not.toThrow()
expect(() => validateDescriptionLength(null)).not.toThrow()
expect(() => validateDescriptionLength(undefined)).not.toThrow()
})
test('rejects description exceeding 400 characters', () => {
it('rejects description exceeding 400 characters', () => {
const invalidDescription = 'x'.repeat(401)
expect(() => validateDescriptionLength(invalidDescription)).toThrow(
'Description cannot exceed 400 characters.',
@ -36,7 +36,7 @@ describe('Description Validation Logic', () => {
})
describe('Backend Validation Consistency', () => {
test('App and Dataset have consistent validation limits', () => {
it('App and Dataset have consistent validation limits', () => {
const maxLength = 400
const validDescription = 'x'.repeat(maxLength)
const invalidDescription = 'x'.repeat(maxLength + 1)
@ -50,7 +50,7 @@ describe('Description Validation Logic', () => {
expect(() => validateDescriptionLength(invalidDescription)).toThrow()
})
test('validation error messages are consistent', () => {
it('validation error messages are consistent', () => {
const expectedErrorMessage = 'Description cannot exceed 400 characters.'
// This would be the error message from both App and Dataset backend validation
@ -78,7 +78,7 @@ describe('Description Validation Logic', () => {
]
testCases.forEach(({ length, shouldPass, description }) => {
test(`handles ${description} correctly`, () => {
it(`handles ${description} correctly`, () => {
const testDescription = length > 0 ? 'x'.repeat(length) : ''
expect(testDescription.length).toBe(length)

View File

@ -40,7 +40,7 @@ vi.mock('@/service/knowledge/use-segment', () => ({
}))
// Create a minimal version of the DocumentDetail component that includes our fix
const DocumentDetailWithFix = ({ datasetId, documentId }: { datasetId: string; documentId: string }) => {
const DocumentDetailWithFix = ({ datasetId, documentId }: { datasetId: string, documentId: string }) => {
const router = useRouter()
// This is the FIXED implementation from detail/index.tsx
@ -59,7 +59,12 @@ const DocumentDetailWithFix = ({ datasetId, documentId }: { datasetId: string; d
Back to Documents
</button>
<div data-testid="document-info">
Dataset: {datasetId}, Document: {documentId}
Dataset:
{' '}
{datasetId}
, Document:
{' '}
{documentId}
</div>
</div>
)
@ -88,7 +93,7 @@ describe('Document Detail Navigation Fix Verification', () => {
})
describe('Query Parameter Preservation', () => {
test('preserves pagination state (page 3, limit 25)', () => {
it('preserves pagination state (page 3, limit 25)', () => {
// Simulate user coming from page 3 with 25 items per page
Object.defineProperty(window, 'location', {
value: {
@ -108,7 +113,7 @@ describe('Document Detail Navigation Fix Verification', () => {
console.log('✅ Pagination state preserved: page=3&limit=25')
})
test('preserves search keyword and filters', () => {
it('preserves search keyword and filters', () => {
// Simulate user with search and filters applied
Object.defineProperty(window, 'location', {
value: {
@ -127,7 +132,7 @@ describe('Document Detail Navigation Fix Verification', () => {
console.log('✅ Search and filters preserved')
})
test('handles complex query parameters with special characters', () => {
it('handles complex query parameters with special characters', () => {
// Test with complex query string including encoded characters
Object.defineProperty(window, 'location', {
value: {
@ -152,7 +157,7 @@ describe('Document Detail Navigation Fix Verification', () => {
console.log('✅ Complex query parameters handled:', expectedCall)
})
test('handles empty query parameters gracefully', () => {
it('handles empty query parameters gracefully', () => {
// No query parameters in URL
Object.defineProperty(window, 'location', {
value: {
@ -173,7 +178,7 @@ describe('Document Detail Navigation Fix Verification', () => {
})
describe('Different Dataset IDs', () => {
test('works with different dataset identifiers', () => {
it('works with different dataset identifiers', () => {
Object.defineProperty(window, 'location', {
value: {
search: '?page=5&limit=10',
@ -193,7 +198,7 @@ describe('Document Detail Navigation Fix Verification', () => {
})
describe('Real User Scenarios', () => {
test('scenario: user searches, goes to page 3, views document, clicks back', () => {
it('scenario: user searches, goes to page 3, views document, clicks back', () => {
// User searched for "API" and navigated to page 3
Object.defineProperty(window, 'location', {
value: {
@ -213,7 +218,7 @@ describe('Document Detail Navigation Fix Verification', () => {
console.log('✅ Real user scenario: search + pagination preserved')
})
test('scenario: user applies multiple filters, goes to document, returns', () => {
it('scenario: user applies multiple filters, goes to document, returns', () => {
// User has applied multiple filters and is on page 2
Object.defineProperty(window, 'location', {
value: {
@ -234,7 +239,7 @@ describe('Document Detail Navigation Fix Verification', () => {
})
describe('Error Handling and Edge Cases', () => {
test('handles malformed query parameters gracefully', () => {
it('handles malformed query parameters gracefully', () => {
// Test with potentially problematic query string
Object.defineProperty(window, 'location', {
value: {
@ -258,7 +263,7 @@ describe('Document Detail Navigation Fix Verification', () => {
console.log('✅ Malformed parameters handled gracefully:', navigationPath)
})
test('handles very long query strings', () => {
it('handles very long query strings', () => {
// Test with a very long query string
const longKeyword = 'a'.repeat(1000)
Object.defineProperty(window, 'location', {
@ -281,7 +286,7 @@ describe('Document Detail Navigation Fix Verification', () => {
})
describe('Performance Verification', () => {
test('navigation function executes quickly', () => {
it('navigation function executes quickly', () => {
Object.defineProperty(window, 'location', {
value: {
search: '?page=1&limit=10&keyword=test',

View File

@ -46,32 +46,32 @@ describe('Document List Sorting', () => {
})
}
test('sorts by name descending (default for UI consistency)', () => {
it('sorts by name descending (default for UI consistency)', () => {
const sorted = sortDocuments(mockDocuments, 'name', 'desc')
expect(sorted.map(doc => doc.name)).toEqual(['Gamma.docx', 'Beta.pdf', 'Alpha.txt'])
})
test('sorts by name ascending (after toggle)', () => {
it('sorts by name ascending (after toggle)', () => {
const sorted = sortDocuments(mockDocuments, 'name', 'asc')
expect(sorted.map(doc => doc.name)).toEqual(['Alpha.txt', 'Beta.pdf', 'Gamma.docx'])
})
test('sorts by word_count descending', () => {
it('sorts by word_count descending', () => {
const sorted = sortDocuments(mockDocuments, 'word_count', 'desc')
expect(sorted.map(doc => doc.word_count)).toEqual([800, 500, 200])
})
test('sorts by hit_count descending', () => {
it('sorts by hit_count descending', () => {
const sorted = sortDocuments(mockDocuments, 'hit_count', 'desc')
expect(sorted.map(doc => doc.hit_count)).toEqual([25, 10, 5])
})
test('sorts by created_at descending (newest first)', () => {
it('sorts by created_at descending (newest first)', () => {
const sorted = sortDocuments(mockDocuments, 'created_at', 'desc')
expect(sorted.map(doc => doc.created_at)).toEqual([1699123500, 1699123456, 1699123400])
})
test('handles empty values correctly', () => {
it('handles empty values correctly', () => {
const docsWithEmpty = [
{ id: '1', name: 'Test', word_count: 100, hit_count: 5, created_at: 1699123456 },
{ id: '2', name: 'Empty', word_count: 0, hit_count: 0, created_at: 1699123400 },

View File

@ -1,8 +1,8 @@
import React from 'react'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import React from 'react'
import MailAndPasswordAuth from '@/app/(shareLayout)/webapp-signin/components/mail-and-password-auth'
import CheckCode from '@/app/(shareLayout)/webapp-signin/check-code/page'
import MailAndPasswordAuth from '@/app/(shareLayout)/webapp-signin/components/mail-and-password-auth'
const replaceMock = vi.fn()
const backMock = vi.fn()

View File

@ -1,9 +1,9 @@
import React from 'react'
import { render, screen, waitFor } from '@testing-library/react'
import { AccessMode } from '@/models/access-control'
import React from 'react'
import WebAppStoreProvider, { useWebAppStore } from '@/context/web-app-context'
import { AccessMode } from '@/models/access-control'
vi.mock('next/navigation', () => ({
usePathname: vi.fn(() => '/chatbot/sample-app'),
useSearchParams: vi.fn(() => {

View File

@ -1,7 +1,7 @@
import React from 'react'
import { fireEvent, render, screen } from '@testing-library/react'
import CommandSelector from '../../app/components/goto-anything/command-selector'
import type { ActionItem } from '../../app/components/goto-anything/actions/types'
import { fireEvent, render, screen } from '@testing-library/react'
import React from 'react'
import CommandSelector from '../../app/components/goto-anything/command-selector'
vi.mock('cmdk', () => ({
Command: {

View File

@ -1,6 +1,10 @@
import type { Mock } from 'vitest'
import type { ActionItem } from '../../app/components/goto-anything/actions/types'
// Import after mocking to get mocked version
import { matchAction } from '../../app/components/goto-anything/actions'
import { slashCommandRegistry } from '../../app/components/goto-anything/actions/commands/registry'
// Mock the entire actions module to avoid import issues
vi.mock('../../app/components/goto-anything/actions', () => ({
matchAction: vi.fn(),
@ -8,10 +12,6 @@ vi.mock('../../app/components/goto-anything/actions', () => ({
vi.mock('../../app/components/goto-anything/actions/commands/registry')
// Import after mocking to get mocked version
import { matchAction } from '../../app/components/goto-anything/actions'
import { slashCommandRegistry } from '../../app/components/goto-anything/actions/commands/registry'
// Implement the actual matchAction logic for testing
const actualMatchAction = (query: string, actions: Record<string, ActionItem>) => {
const result = Object.values(actions).find((action) => {

View File

@ -1,12 +1,13 @@
import React from 'react'
import { render, screen } from '@testing-library/react'
import React from 'react'
// Type alias for search mode
type SearchMode = 'scopes' | 'commands' | null
// Mock component to test tag display logic
const TagDisplay: React.FC<{ searchMode: SearchMode }> = ({ searchMode }) => {
if (!searchMode) return null
if (!searchMode)
return null
return (
<div className="flex items-center gap-1 text-xs text-text-tertiary">
@ -37,8 +38,10 @@ describe('Scope and Command Tags', () => {
describe('Search Mode Detection', () => {
const getSearchMode = (query: string): SearchMode => {
if (query.startsWith('@')) return 'scopes'
if (query.startsWith('/')) return 'commands'
if (query.startsWith('@'))
return 'scopes'
if (query.startsWith('/'))
return 'commands'
return null
}
@ -90,8 +93,10 @@ describe('Scope and Command Tags', () => {
const SearchComponent: React.FC<{ query: string }> = ({ query }) => {
let searchMode: SearchMode = null
if (query.startsWith('@')) searchMode = 'scopes'
else if (query.startsWith('/')) searchMode = 'commands'
if (query.startsWith('@'))
searchMode = 'scopes'
else if (query.startsWith('/'))
searchMode = 'commands'
return (
<div>

View File

@ -10,8 +10,8 @@ import type { MockedFunction } from 'vitest'
*/
import { Actions, searchAnything } from '@/app/components/goto-anything/actions'
import { postMarketplace } from '@/service/base'
import { fetchAppList } from '@/service/apps'
import { postMarketplace } from '@/service/base'
import { fetchDatasets } from '@/service/datasets'
// Mock API functions

View File

@ -1,5 +1,5 @@
import { slashCommandRegistry } from '../../app/components/goto-anything/actions/commands/registry'
import type { SlashCommandHandler } from '../../app/components/goto-anything/actions/commands/types'
import { slashCommandRegistry } from '../../app/components/goto-anything/actions/commands/registry'
// Mock the registry
vi.mock('../../app/components/goto-anything/actions/commands/registry')
@ -50,8 +50,10 @@ describe('Slash Command Dual-Mode System', () => {
beforeEach(() => {
vi.clearAllMocks()
;(slashCommandRegistry as any).findCommand = vi.fn((name: string) => {
if (name === 'docs') return mockDirectCommand
if (name === 'theme') return mockSubmenuCommand
if (name === 'docs')
return mockDirectCommand
if (name === 'theme')
return mockSubmenuCommand
return null
})
;(slashCommandRegistry as any).getAllCommands = vi.fn(() => [

View File

@ -27,11 +27,11 @@ const loadTranslationContent = (locale: string): string => {
// Helper function to check if upload features exist
const hasUploadFeatures = (content: string): { [key: string]: boolean } => {
return {
fileUpload: /fileUpload\s*:\s*{/.test(content),
imageUpload: /imageUpload\s*:\s*{/.test(content),
documentUpload: /documentUpload\s*:\s*{/.test(content),
audioUpload: /audioUpload\s*:\s*{/.test(content),
featureBar: /bar\s*:\s*{/.test(content),
fileUpload: /fileUpload\s*:\s*\{/.test(content),
imageUpload: /imageUpload\s*:\s*\{/.test(content),
documentUpload: /documentUpload\s*:\s*\{/.test(content),
audioUpload: /audioUpload\s*:\s*\{/.test(content),
featureBar: /bar\s*:\s*\{/.test(content),
}
}
@ -43,14 +43,14 @@ describe('Upload Features i18n Translations - Issue #23062', () => {
console.log(`Testing ${supportedLocales.length} locales for upload features`)
})
test('all locales should have translation files', () => {
it('all locales should have translation files', () => {
supportedLocales.forEach((locale) => {
const filePath = path.join(I18N_DIR, locale, 'app-debug.ts')
expect(fs.existsSync(filePath)).toBe(true)
})
})
test('all locales should have required upload features', () => {
it('all locales should have required upload features', () => {
const results: { [locale: string]: { [feature: string]: boolean } } = {}
supportedLocales.forEach((locale) => {
@ -69,7 +69,7 @@ describe('Upload Features i18n Translations - Issue #23062', () => {
console.log('✅ All locales have complete upload features')
})
test('previously missing locales should now have audioUpload - Issue #23062', () => {
it('previously missing locales should now have audioUpload - Issue #23062', () => {
// These locales were specifically missing audioUpload
const previouslyMissingLocales = ['fa-IR', 'hi-IN', 'ro-RO', 'sl-SI', 'th-TH', 'uk-UA', 'vi-VN']
@ -77,7 +77,7 @@ describe('Upload Features i18n Translations - Issue #23062', () => {
const content = loadTranslationContent(locale)
// Verify audioUpload exists
expect(/audioUpload\s*:\s*{/.test(content)).toBe(true)
expect(/audioUpload\s*:\s*\{/.test(content)).toBe(true)
// Verify it has title and description
expect(/audioUpload[^}]*title\s*:/.test(content)).toBe(true)
@ -87,30 +87,30 @@ describe('Upload Features i18n Translations - Issue #23062', () => {
})
})
test('upload features should have required properties', () => {
it('upload features should have required properties', () => {
supportedLocales.forEach((locale) => {
const content = loadTranslationContent(locale)
// Check fileUpload has required properties
if (/fileUpload\s*:\s*{/.test(content)) {
if (/fileUpload\s*:\s*\{/.test(content)) {
expect(/fileUpload[^}]*title\s*:/.test(content)).toBe(true)
expect(/fileUpload[^}]*description\s*:/.test(content)).toBe(true)
}
// Check imageUpload has required properties
if (/imageUpload\s*:\s*{/.test(content)) {
if (/imageUpload\s*:\s*\{/.test(content)) {
expect(/imageUpload[^}]*title\s*:/.test(content)).toBe(true)
expect(/imageUpload[^}]*description\s*:/.test(content)).toBe(true)
}
// Check documentUpload has required properties
if (/documentUpload\s*:\s*{/.test(content)) {
if (/documentUpload\s*:\s*\{/.test(content)) {
expect(/documentUpload[^}]*title\s*:/.test(content)).toBe(true)
expect(/documentUpload[^}]*description\s*:/.test(content)).toBe(true)
}
// Check audioUpload has required properties
if (/audioUpload\s*:\s*{/.test(content)) {
if (/audioUpload\s*:\s*\{/.test(content)) {
expect(/audioUpload[^}]*title\s*:/.test(content)).toBe(true)
expect(/audioUpload[^}]*description\s*:/.test(content)).toBe(true)
}

View File

@ -24,7 +24,7 @@ describe('Navigation Utilities', () => {
})
describe('createNavigationPath', () => {
test('preserves query parameters by default', () => {
it('preserves query parameters by default', () => {
Object.defineProperty(window, 'location', {
value: { search: '?page=3&limit=10&keyword=test' },
writable: true,
@ -34,7 +34,7 @@ describe('Navigation Utilities', () => {
expect(path).toBe('/datasets/123/documents?page=3&limit=10&keyword=test')
})
test('returns clean path when preserveParams is false', () => {
it('returns clean path when preserveParams is false', () => {
Object.defineProperty(window, 'location', {
value: { search: '?page=3&limit=10' },
writable: true,
@ -44,7 +44,7 @@ describe('Navigation Utilities', () => {
expect(path).toBe('/datasets/123/documents')
})
test('handles empty query parameters', () => {
it('handles empty query parameters', () => {
Object.defineProperty(window, 'location', {
value: { search: '' },
writable: true,
@ -54,7 +54,7 @@ describe('Navigation Utilities', () => {
expect(path).toBe('/datasets/123/documents')
})
test('handles errors gracefully', () => {
it('handles errors gracefully', () => {
// Mock window.location to throw an error
Object.defineProperty(window, 'location', {
get: () => {
@ -74,7 +74,7 @@ describe('Navigation Utilities', () => {
})
describe('createBackNavigation', () => {
test('creates function that navigates with preserved params', () => {
it('creates function that navigates with preserved params', () => {
Object.defineProperty(window, 'location', {
value: { search: '?page=2&limit=25' },
writable: true,
@ -86,7 +86,7 @@ describe('Navigation Utilities', () => {
expect(mockPush).toHaveBeenCalledWith('/datasets/123/documents?page=2&limit=25')
})
test('creates function that navigates without params when specified', () => {
it('creates function that navigates without params when specified', () => {
Object.defineProperty(window, 'location', {
value: { search: '?page=2&limit=25' },
writable: true,
@ -100,7 +100,7 @@ describe('Navigation Utilities', () => {
})
describe('extractQueryParams', () => {
test('extracts specified parameters', () => {
it('extracts specified parameters', () => {
Object.defineProperty(window, 'location', {
value: { search: '?page=3&limit=10&keyword=test&other=value' },
writable: true,
@ -114,7 +114,7 @@ describe('Navigation Utilities', () => {
})
})
test('handles missing parameters', () => {
it('handles missing parameters', () => {
Object.defineProperty(window, 'location', {
value: { search: '?page=3' },
writable: true,
@ -126,7 +126,7 @@ describe('Navigation Utilities', () => {
})
})
test('handles errors gracefully', () => {
it('handles errors gracefully', () => {
Object.defineProperty(window, 'location', {
get: () => {
throw new Error('Location access denied')
@ -145,7 +145,7 @@ describe('Navigation Utilities', () => {
})
describe('createNavigationPathWithParams', () => {
test('creates path with specified parameters', () => {
it('creates path with specified parameters', () => {
const path = createNavigationPathWithParams('/datasets/123/documents', {
page: 1,
limit: 25,
@ -155,7 +155,7 @@ describe('Navigation Utilities', () => {
expect(path).toBe('/datasets/123/documents?page=1&limit=25&keyword=search+term')
})
test('filters out empty values', () => {
it('filters out empty values', () => {
const path = createNavigationPathWithParams('/datasets/123/documents', {
page: 1,
limit: '',
@ -166,7 +166,7 @@ describe('Navigation Utilities', () => {
expect(path).toBe('/datasets/123/documents?page=1&keyword=test')
})
test('handles errors gracefully', () => {
it('handles errors gracefully', () => {
// Mock URLSearchParams to throw an error
const originalURLSearchParams = globalThis.URLSearchParams
globalThis.URLSearchParams = vi.fn(() => {
@ -185,7 +185,7 @@ describe('Navigation Utilities', () => {
})
describe('mergeQueryParams', () => {
test('merges new params with existing ones', () => {
it('merges new params with existing ones', () => {
Object.defineProperty(window, 'location', {
value: { search: '?page=3&limit=10' },
writable: true,
@ -199,7 +199,7 @@ describe('Navigation Utilities', () => {
expect(result).toContain('keyword=test') // added
})
test('removes parameters when value is null', () => {
it('removes parameters when value is null', () => {
Object.defineProperty(window, 'location', {
value: { search: '?page=3&limit=10&keyword=test' },
writable: true,
@ -214,7 +214,7 @@ describe('Navigation Utilities', () => {
expect(result).toContain('filter=active')
})
test('creates fresh params when preserveExisting is false', () => {
it('creates fresh params when preserveExisting is false', () => {
Object.defineProperty(window, 'location', {
value: { search: '?page=3&limit=10' },
writable: true,
@ -228,7 +228,7 @@ describe('Navigation Utilities', () => {
})
describe('datasetNavigation', () => {
test('backToDocuments creates correct navigation function', () => {
it('backToDocuments creates correct navigation function', () => {
Object.defineProperty(window, 'location', {
value: { search: '?page=2&limit=25' },
writable: true,
@ -240,14 +240,14 @@ describe('Navigation Utilities', () => {
expect(mockPush).toHaveBeenCalledWith('/datasets/dataset-123/documents?page=2&limit=25')
})
test('toDocumentDetail creates correct navigation function', () => {
it('toDocumentDetail creates correct navigation function', () => {
const detailFn = datasetNavigation.toDocumentDetail(mockRouter, 'dataset-123', 'doc-456')
detailFn()
expect(mockPush).toHaveBeenCalledWith('/datasets/dataset-123/documents/doc-456')
})
test('toDocumentSettings creates correct navigation function', () => {
it('toDocumentSettings creates correct navigation function', () => {
const settingsFn = datasetNavigation.toDocumentSettings(mockRouter, 'dataset-123', 'doc-456')
settingsFn()
@ -256,7 +256,7 @@ describe('Navigation Utilities', () => {
})
describe('Real-world Integration Scenarios', () => {
test('complete user workflow: list -> detail -> back', () => {
it('complete user workflow: list -> detail -> back', () => {
// User starts on page 3 with search
Object.defineProperty(window, 'location', {
value: { search: '?page=3&keyword=API&limit=25' },
@ -273,7 +273,7 @@ describe('Navigation Utilities', () => {
expect(mockPush).toHaveBeenCalledWith('/datasets/main-dataset/documents?page=3&keyword=API&limit=25')
})
test('user applies filters then views document', () => {
it('user applies filters then views document', () => {
// Complex filter state
Object.defineProperty(window, 'location', {
value: { search: '?page=1&limit=50&status=active&type=pdf&sort=created_at&order=desc' },
@ -288,7 +288,7 @@ describe('Navigation Utilities', () => {
})
describe('Edge Cases and Error Handling', () => {
test('handles special characters in query parameters', () => {
it('handles special characters in query parameters', () => {
Object.defineProperty(window, 'location', {
value: { search: '?keyword=hello%20world&filter=type%3Apdf&tag=%E4%B8%AD%E6%96%87' },
writable: true,
@ -300,7 +300,7 @@ describe('Navigation Utilities', () => {
expect(path).toContain('%E4%B8%AD%E6%96%87')
})
test('handles duplicate query parameters', () => {
it('handles duplicate query parameters', () => {
Object.defineProperty(window, 'location', {
value: { search: '?tag=tag1&tag=tag2&tag=tag3' },
writable: true,
@ -311,7 +311,7 @@ describe('Navigation Utilities', () => {
expect(params.tag).toBe('tag1')
})
test('handles very long query strings', () => {
it('handles very long query strings', () => {
const longValue = 'a'.repeat(1000)
Object.defineProperty(window, 'location', {
value: { search: `?data=${longValue}` },
@ -323,7 +323,7 @@ describe('Navigation Utilities', () => {
expect(path.length).toBeGreaterThan(1000)
})
test('handles empty string values in query parameters', () => {
it('handles empty string values in query parameters', () => {
const path = createNavigationPathWithParams('/datasets/123/documents', {
page: 1,
keyword: '',
@ -336,7 +336,7 @@ describe('Navigation Utilities', () => {
expect(path).not.toContain('filter=')
})
test('handles null and undefined values in mergeQueryParams', () => {
it('handles null and undefined values in mergeQueryParams', () => {
Object.defineProperty(window, 'location', {
value: { search: '?page=1&limit=10&keyword=test' },
writable: true,
@ -355,7 +355,7 @@ describe('Navigation Utilities', () => {
expect(result).toContain('sort=name')
})
test('handles navigation with hash fragments', () => {
it('handles navigation with hash fragments', () => {
Object.defineProperty(window, 'location', {
value: { search: '?page=1', hash: '#section-2' },
writable: true,
@ -366,7 +366,7 @@ describe('Navigation Utilities', () => {
expect(path).toBe('/datasets/123/documents?page=1')
})
test('handles malformed query strings gracefully', () => {
it('handles malformed query strings gracefully', () => {
Object.defineProperty(window, 'location', {
value: { search: '?page=1&invalid&limit=10&=value&key=' },
writable: true,
@ -382,7 +382,7 @@ describe('Navigation Utilities', () => {
})
describe('Performance Tests', () => {
test('handles large number of query parameters efficiently', () => {
it('handles large number of query parameters efficiently', () => {
const manyParams = Array.from({ length: 50 }, (_, i) => `param${i}=value${i}`).join('&')
Object.defineProperty(window, 'location', {
value: { search: `?${manyParams}` },

View File

@ -10,8 +10,8 @@
import { render, screen, waitFor } from '@testing-library/react'
import { ThemeProvider } from 'next-themes'
import useTheme from '@/hooks/use-theme'
import { useEffect, useState } from 'react'
import useTheme from '@/hooks/use-theme'
const DARK_MODE_MEDIA_QUERY = /prefers-color-scheme:\s*dark/i
@ -81,9 +81,9 @@ const setupMockEnvironment = (storedTheme: string | null, systemPrefersDark = fa
// Helper function to create timing page component
const createTimingPageComponent = (
timingData: Array<{ phase: string; timestamp: number; styles: { backgroundColor: string; color: string } }>,
timingData: Array<{ phase: string, timestamp: number, styles: { backgroundColor: string, color: string } }>,
) => {
const recordTiming = (phase: string, styles: { backgroundColor: string; color: string }) => {
const recordTiming = (phase: string, styles: { backgroundColor: string, color: string }) => {
timingData.push({
phase,
timestamp: performance.now(),
@ -113,7 +113,17 @@ const createTimingPageComponent = (
style={currentStyles}
>
<div data-testid="timing-status">
Phase: {mounted ? 'CSR' : 'Initial'} | Theme: {theme} | Visual: {isDark ? 'dark' : 'light'}
Phase:
{' '}
{mounted ? 'CSR' : 'Initial'}
{' '}
| Theme:
{' '}
{theme}
{' '}
| Visual:
{' '}
{isDark ? 'dark' : 'light'}
</div>
</div>
)
@ -124,7 +134,7 @@ const createTimingPageComponent = (
// Helper function to create CSS test component
const createCSSTestComponent = (
cssStates: Array<{ className: string; timestamp: number }>,
cssStates: Array<{ className: string, timestamp: number }>,
) => {
const recordCSSState = (className: string) => {
cssStates.push({
@ -151,7 +161,10 @@ const createCSSTestComponent = (
data-testid="css-component"
className={className}
>
<div data-testid="css-classes">Classes: {className}</div>
<div data-testid="css-classes">
Classes:
{className}
</div>
</div>
)
}
@ -161,7 +174,7 @@ const createCSSTestComponent = (
// Helper function to create performance test component
const createPerformanceTestComponent = (
performanceMarks: Array<{ event: string; timestamp: number }>,
performanceMarks: Array<{ event: string, timestamp: number }>,
) => {
const recordPerformanceMark = (event: string) => {
performanceMarks.push({ event, timestamp: performance.now() })
@ -186,7 +199,13 @@ const createPerformanceTestComponent = (
return (
<div data-testid="performance-test">
Mounted: {mounted.toString()} | Theme: {theme || 'loading'}
Mounted:
{' '}
{mounted.toString()}
{' '}
| Theme:
{' '}
{theme || 'loading'}
</div>
)
}
@ -216,10 +235,14 @@ const PageComponent = () => {
Dify Application
</h1>
<div data-testid="theme-indicator">
Current Theme: {mounted ? theme : 'unknown'}
Current Theme:
{' '}
{mounted ? theme : 'unknown'}
</div>
<div data-testid="visual-appearance">
Appearance: {isDark ? 'dark' : 'light'}
Appearance:
{' '}
{isDark ? 'dark' : 'light'}
</div>
</div>
</div>
@ -254,7 +277,7 @@ describe('Real Browser Environment Dark Mode Flicker Test', () => {
})
describe('Page Refresh Scenario Simulation', () => {
test('simulates complete page loading process with dark theme', async () => {
it('simulates complete page loading process with dark theme', async () => {
// Setup: User previously selected dark mode
setupMockEnvironment('dark')
@ -286,7 +309,7 @@ describe('Real Browser Environment Dark Mode Flicker Test', () => {
console.log('State change detection: Initial -> Final')
})
test('handles light theme correctly', async () => {
it('handles light theme correctly', async () => {
setupMockEnvironment('light')
render(
@ -302,7 +325,7 @@ describe('Real Browser Environment Dark Mode Flicker Test', () => {
expect(screen.getByTestId('visual-appearance')).toHaveTextContent('Appearance: light')
})
test('handles system theme with dark preference', async () => {
it('handles system theme with dark preference', async () => {
setupMockEnvironment('system', true) // system theme, dark preference
render(
@ -318,7 +341,7 @@ describe('Real Browser Environment Dark Mode Flicker Test', () => {
expect(screen.getByTestId('visual-appearance')).toHaveTextContent('Appearance: dark')
})
test('handles system theme with light preference', async () => {
it('handles system theme with light preference', async () => {
setupMockEnvironment('system', false) // system theme, light preference
render(
@ -334,7 +357,7 @@ describe('Real Browser Environment Dark Mode Flicker Test', () => {
expect(screen.getByTestId('visual-appearance')).toHaveTextContent('Appearance: light')
})
test('handles no stored theme (defaults to system)', async () => {
it('handles no stored theme (defaults to system)', async () => {
setupMockEnvironment(null, false) // no stored theme, system prefers light
render(
@ -348,10 +371,10 @@ describe('Real Browser Environment Dark Mode Flicker Test', () => {
})
})
test('measures timing window of style changes', async () => {
it('measures timing window of style changes', async () => {
setupMockEnvironment('dark')
const timingData: Array<{ phase: string; timestamp: number; styles: any }> = []
const timingData: Array<{ phase: string, timestamp: number, styles: any }> = []
const TimingPageComponent = createTimingPageComponent(timingData)
render(
@ -384,10 +407,10 @@ describe('Real Browser Environment Dark Mode Flicker Test', () => {
})
describe('CSS Application Timing Tests', () => {
test('checks CSS class changes causing flicker', async () => {
it('checks CSS class changes causing flicker', async () => {
setupMockEnvironment('dark')
const cssStates: Array<{ className: string; timestamp: number }> = []
const cssStates: Array<{ className: string, timestamp: number }> = []
const CSSTestComponent = createCSSTestComponent(cssStates)
render(
@ -420,7 +443,7 @@ describe('Real Browser Environment Dark Mode Flicker Test', () => {
})
describe('Edge Cases and Error Handling', () => {
test('handles localStorage access errors gracefully', async () => {
it('handles localStorage access errors gracefully', async () => {
setupMockEnvironment(null)
const mockStorage = {
@ -457,7 +480,7 @@ describe('Real Browser Environment Dark Mode Flicker Test', () => {
}
})
test('handles invalid theme values in localStorage', async () => {
it('handles invalid theme values in localStorage', async () => {
setupMockEnvironment('invalid-theme-value')
render(
@ -477,8 +500,8 @@ describe('Real Browser Environment Dark Mode Flicker Test', () => {
})
describe('Performance and Regression Tests', () => {
test('verifies ThemeProvider position fix reduces initialization delay', async () => {
const performanceMarks: Array<{ event: string; timestamp: number }> = []
it('verifies ThemeProvider position fix reduces initialization delay', async () => {
const performanceMarks: Array<{ event: string, timestamp: number }> = []
setupMockEnvironment('dark')
@ -507,7 +530,7 @@ describe('Real Browser Environment Dark Mode Flicker Test', () => {
})
describe('Solution Requirements Definition', () => {
test('defines technical requirements to eliminate flicker', () => {
it('defines technical requirements to eliminate flicker', () => {
const technicalRequirements = {
ssrConsistency: 'SSR and CSR must render identical initial styles',
synchronousDetection: 'Theme detection must complete synchronously before first render',

View File

@ -14,8 +14,8 @@ describe('Unified Tags Editing - Pure Logic Tests', () => {
// This is the valueNotChanged logic from TagSelector component
const valueNotChanged
= currentValue.length === newSelectedTagIDs.length
&& currentValue.every(v => newSelectedTagIDs.includes(v))
&& newSelectedTagIDs.every(v => currentValue.includes(v))
&& currentValue.every(v => newSelectedTagIDs.includes(v))
&& newSelectedTagIDs.every(v => currentValue.includes(v))
expect(valueNotChanged).toBe(false)
})
@ -26,8 +26,8 @@ describe('Unified Tags Editing - Pure Logic Tests', () => {
const valueNotChanged
= currentValue.length === newSelectedTagIDs.length
&& currentValue.every(v => newSelectedTagIDs.includes(v))
&& newSelectedTagIDs.every(v => currentValue.includes(v))
&& currentValue.every(v => newSelectedTagIDs.includes(v))
&& newSelectedTagIDs.every(v => currentValue.includes(v))
expect(valueNotChanged).toBe(true)
})
@ -70,7 +70,7 @@ describe('Unified Tags Editing - Pure Logic Tests', () => {
})
describe('Fallback Logic (from layout-main.tsx)', () => {
type Tag = { id: string; name: string }
type Tag = { id: string, name: string }
type AppDetail = { tags: Tag[] }
type FallbackResult = { tags?: Tag[] } | null
// no-op
@ -316,7 +316,7 @@ describe('Unified Tags Editing - Pure Logic Tests', () => {
]
// Filter out invalid entries
const validTags = mixedData.filter((tag): tag is { id: string; name: string; type: string; binding_count: number } =>
const validTags = mixedData.filter((tag): tag is { id: string, name: string, type: string, binding_count: number } =>
tag != null
&& typeof tag === 'object'
&& 'id' in tag

View File

@ -1,6 +1,6 @@
import type { Mock } from 'vitest'
import { BlockEnum } from '@/app/components/workflow/types'
import { useWorkflowStore } from '@/app/components/workflow/store'
import { BlockEnum } from '@/app/components/workflow/types'
// Type for mocked store
type MockWorkflowStore = {
@ -103,9 +103,9 @@ describe('Workflow Onboarding Integration Logic', () => {
// Simulate the validation logic from use-nodes-sync-draft.ts
const isValidStartNode = mockNode.data.type === BlockEnum.Start
|| mockNode.data.type === BlockEnum.TriggerSchedule
|| mockNode.data.type === BlockEnum.TriggerWebhook
|| mockNode.data.type === BlockEnum.TriggerPlugin
|| mockNode.data.type === BlockEnum.TriggerSchedule
|| mockNode.data.type === BlockEnum.TriggerWebhook
|| mockNode.data.type === BlockEnum.TriggerPlugin
expect(isValidStartNode).toBe(true)
})
@ -117,9 +117,9 @@ describe('Workflow Onboarding Integration Logic', () => {
}
const isValidStartNode = mockNode.data.type === BlockEnum.Start
|| mockNode.data.type === BlockEnum.TriggerSchedule
|| mockNode.data.type === BlockEnum.TriggerWebhook
|| mockNode.data.type === BlockEnum.TriggerPlugin
|| mockNode.data.type === BlockEnum.TriggerSchedule
|| mockNode.data.type === BlockEnum.TriggerWebhook
|| mockNode.data.type === BlockEnum.TriggerPlugin
expect(isValidStartNode).toBe(true)
})
@ -131,9 +131,9 @@ describe('Workflow Onboarding Integration Logic', () => {
}
const isValidStartNode = mockNode.data.type === BlockEnum.Start
|| mockNode.data.type === BlockEnum.TriggerSchedule
|| mockNode.data.type === BlockEnum.TriggerWebhook
|| mockNode.data.type === BlockEnum.TriggerPlugin
|| mockNode.data.type === BlockEnum.TriggerSchedule
|| mockNode.data.type === BlockEnum.TriggerWebhook
|| mockNode.data.type === BlockEnum.TriggerPlugin
expect(isValidStartNode).toBe(true)
})
@ -145,9 +145,9 @@ describe('Workflow Onboarding Integration Logic', () => {
}
const isValidStartNode = mockNode.data.type === BlockEnum.Start
|| mockNode.data.type === BlockEnum.TriggerSchedule
|| mockNode.data.type === BlockEnum.TriggerWebhook
|| mockNode.data.type === BlockEnum.TriggerPlugin
|| mockNode.data.type === BlockEnum.TriggerSchedule
|| mockNode.data.type === BlockEnum.TriggerWebhook
|| mockNode.data.type === BlockEnum.TriggerPlugin
expect(isValidStartNode).toBe(true)
})
@ -159,9 +159,9 @@ describe('Workflow Onboarding Integration Logic', () => {
}
const isValidStartNode = mockNode.data.type === BlockEnum.Start
|| mockNode.data.type === BlockEnum.TriggerSchedule
|| mockNode.data.type === BlockEnum.TriggerWebhook
|| mockNode.data.type === BlockEnum.TriggerPlugin
|| mockNode.data.type === BlockEnum.TriggerSchedule
|| mockNode.data.type === BlockEnum.TriggerWebhook
|| mockNode.data.type === BlockEnum.TriggerPlugin
expect(isValidStartNode).toBe(false)
})

View File

@ -35,11 +35,16 @@ function restoreEnvironment() {
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => {
if (key.includes('MaxParallelismTitle')) return 'Max Parallelism'
if (key.includes('MaxParallelismDesc')) return 'Maximum number of parallel executions'
if (key.includes('parallelMode')) return 'Parallel Mode'
if (key.includes('parallelPanelDesc')) return 'Enable parallel execution'
if (key.includes('errorResponseMethod')) return 'Error Response Method'
if (key.includes('MaxParallelismTitle'))
return 'Max Parallelism'
if (key.includes('MaxParallelismDesc'))
return 'Maximum number of parallel executions'
if (key.includes('parallelMode'))
return 'Parallel Mode'
if (key.includes('parallelPanelDesc'))
return 'Enable parallel execution'
if (key.includes('errorResponseMethod'))
return 'Error Response Method'
return key
},
}),

View File

@ -5,8 +5,8 @@
* components have been properly fixed by replacing dangerouslySetInnerHTML with safe React rendering.
*/
import React from 'react'
import { cleanup, render } from '@testing-library/react'
import React from 'react'
import BlockInput from '../app/components/base/block-input'
import SupportVarInput from '../app/components/workflow/nodes/_base/components/support-var-input'

View File

@ -1,9 +1,9 @@
import React from 'react'
import type { Locale } from '@/i18n-config'
import React from 'react'
import DevelopMain from '@/app/components/develop'
export type IDevelopProps = {
params: Promise<{ locale: Locale; appId: string }>
params: Promise<{ locale: Locale, appId: string }>
}
const Develop = async (props: IDevelopProps) => {

View File

@ -1,8 +1,7 @@
'use client'
import type { FC } from 'react'
import { useUnmount } from 'ahooks'
import React, { useCallback, useEffect, useState } from 'react'
import { usePathname, useRouter } from 'next/navigation'
import type { NavIcon } from '@/app/components/app-sidebar/navLink'
import type { App } from '@/types/app'
import {
RiDashboard2Fill,
RiDashboard2Line,
@ -13,21 +12,23 @@ import {
RiTerminalWindowFill,
RiTerminalWindowLine,
} from '@remixicon/react'
import { useUnmount } from 'ahooks'
import dynamic from 'next/dynamic'
import { usePathname, useRouter } from 'next/navigation'
import React, { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useShallow } from 'zustand/react/shallow'
import s from './style.module.css'
import { cn } from '@/utils/classnames'
import { useStore } from '@/app/components/app/store'
import AppSideBar from '@/app/components/app-sidebar'
import type { NavIcon } from '@/app/components/app-sidebar/navLink'
import { fetchAppDetailDirect } from '@/service/apps'
import { useAppContext } from '@/context/app-context'
import { useStore } from '@/app/components/app/store'
import Loading from '@/app/components/base/loading'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import { type App, AppModeEnum } from '@/types/app'
import useDocumentTitle from '@/hooks/use-document-title'
import { useStore as useTagStore } from '@/app/components/base/tag-management/store'
import dynamic from 'next/dynamic'
import { useAppContext } from '@/context/app-context'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import useDocumentTitle from '@/hooks/use-document-title'
import { fetchAppDetailDirect } from '@/service/apps'
import { AppModeEnum } from '@/types/app'
import { cn } from '@/utils/classnames'
import s from './style.module.css'
const TagManagementModal = dynamic(() => import('@/app/components/base/tag-management'), {
ssr: false,
@ -68,11 +69,11 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
const navConfig = [
...(isCurrentWorkspaceEditor
? [{
name: t('common.appMenus.promptEng'),
href: `/app/${appId}/${(mode === AppModeEnum.WORKFLOW || mode === AppModeEnum.ADVANCED_CHAT) ? 'workflow' : 'configuration'}`,
icon: RiTerminalWindowLine,
selectedIcon: RiTerminalWindowFill,
}]
name: t('common.appMenus.promptEng'),
href: `/app/${appId}/${(mode === AppModeEnum.WORKFLOW || mode === AppModeEnum.ADVANCED_CHAT) ? 'workflow' : 'configuration'}`,
icon: RiTerminalWindowLine,
selectedIcon: RiTerminalWindowFill,
}]
: []
),
{
@ -83,13 +84,13 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
},
...(isCurrentWorkspaceEditor
? [{
name: mode !== AppModeEnum.WORKFLOW
? t('common.appMenus.logAndAnn')
: t('common.appMenus.logs'),
href: `/app/${appId}/logs`,
icon: RiFileList3Line,
selectedIcon: RiFileList3Fill,
}]
name: mode !== AppModeEnum.WORKFLOW
? t('common.appMenus.logAndAnn')
: t('common.appMenus.logs'),
href: `/app/${appId}/logs`,
icon: RiFileList3Line,
selectedIcon: RiFileList3Fill,
}]
: []
),
{
@ -156,7 +157,7 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
if (!appDetail) {
return (
<div className='flex h-full items-center justify-center bg-background-body'>
<div className="flex h-full items-center justify-center bg-background-body">
<Loading />
</div>
)
@ -173,7 +174,7 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
{children}
</div>
{showTagManagementModal && (
<TagManagementModal type='app' show={showTagManagementModal} />
<TagManagementModal type="app" show={showTagManagementModal} />
)}
</div>
)

View File

@ -1,30 +1,30 @@
'use client'
import type { FC } from 'react'
import type { IAppCardProps } from '@/app/components/app/overview/app-card'
import type { BlockEnum } from '@/app/components/workflow/types'
import type { UpdateAppSiteCodeResponse } from '@/models/app'
import type { App } from '@/types/app'
import React, { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import AppCard from '@/app/components/app/overview/app-card'
import Loading from '@/app/components/base/loading'
import MCPServiceCard from '@/app/components/tools/mcp/mcp-service-card'
import TriggerCard from '@/app/components/app/overview/trigger-card'
import { useStore as useAppStore } from '@/app/components/app/store'
import Loading from '@/app/components/base/loading'
import { ToastContext } from '@/app/components/base/toast'
import MCPServiceCard from '@/app/components/tools/mcp/mcp-service-card'
import { isTriggerNode } from '@/app/components/workflow/types'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import { useDocLink } from '@/context/i18n'
import {
fetchAppDetail,
updateAppSiteAccessToken,
updateAppSiteConfig,
updateAppSiteStatus,
} from '@/service/apps'
import type { App } from '@/types/app'
import { AppModeEnum } from '@/types/app'
import type { UpdateAppSiteCodeResponse } from '@/models/app'
import { asyncRunSafe } from '@/utils'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import type { IAppCardProps } from '@/app/components/app/overview/app-card'
import { useStore as useAppStore } from '@/app/components/app/store'
import { useAppWorkflow } from '@/service/use-workflow'
import type { BlockEnum } from '@/app/components/workflow/types'
import { isTriggerNode } from '@/app/components/workflow/types'
import { useDocLink } from '@/context/i18n'
import { AppModeEnum } from '@/types/app'
import { asyncRunSafe } from '@/utils'
export type ICardViewProps = {
appId: string
@ -59,12 +59,12 @@ const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
const triggerDocUrl = docLink('/guides/workflow/node/start')
const buildTriggerModeMessage = useCallback((featureName: string) => (
<div className='flex flex-col gap-1'>
<div className='text-xs text-text-secondary'>
<div className="flex flex-col gap-1">
<div className="text-xs text-text-secondary">
{t('appOverview.overview.disableTooltip.triggerMode', { feature: featureName })}
</div>
<div
className='cursor-pointer text-xs font-medium text-text-accent hover:underline'
className="cursor-pointer text-xs font-medium text-text-accent hover:underline"
onClick={(event) => {
event.stopPropagation()
window.open(triggerDocUrl, '_blank')
@ -185,12 +185,14 @@ const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
</>
)
const triggerCardNode = showTriggerCard ? (
<TriggerCard
appInfo={appDetail}
onToggleResult={handleCallbackResult}
/>
) : null
const triggerCardNode = showTriggerCard
? (
<TriggerCard
appInfo={appDetail}
onToggleResult={handleCallbackResult}
/>
)
: null
return (
<div className={className || 'mb-6 grid w-full grid-cols-1 gap-6 xl:grid-cols-2'}>

View File

@ -1,15 +1,15 @@
'use client'
import React, { useState } from 'react'
import type { PeriodParams } from '@/app/components/app/overview/app-chart'
import dayjs from 'dayjs'
import quarterOfYear from 'dayjs/plugin/quarterOfYear'
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import type { PeriodParams } from '@/app/components/app/overview/app-chart'
import { TIME_PERIOD_MAPPING as LONG_TIME_PERIOD_MAPPING } from '@/app/components/app/log/filter'
import { AvgResponseTime, AvgSessionInteractions, AvgUserInteractions, ConversationsChart, CostChart, EndUsersChart, MessagesChart, TokenPerSecond, UserSatisfactionRate, WorkflowCostChart, WorkflowDailyTerminalsChart, WorkflowMessagesChart } from '@/app/components/app/overview/app-chart'
import { useStore as useAppStore } from '@/app/components/app/store'
import TimeRangePicker from './time-range-picker'
import { TIME_PERIOD_MAPPING as LONG_TIME_PERIOD_MAPPING } from '@/app/components/app/log/filter'
import { IS_CLOUD_EDITION } from '@/config'
import LongTimeRangePicker from './long-time-range-picker'
import TimeRangePicker from './time-range-picker'
dayjs.extend(quarterOfYear)
@ -43,63 +43,65 @@ export default function ChartView({ appId, headerRight }: IChartViewProps) {
return (
<div>
<div className='mb-4'>
<div className='system-xl-semibold mb-2 text-text-primary'>{t('common.appMenus.overview')}</div>
<div className='flex items-center justify-between'>
{IS_CLOUD_EDITION ? (
<TimeRangePicker
ranges={TIME_PERIOD_MAPPING}
onSelect={setPeriod}
queryDateFormat={queryDateFormat}
/>
) : (
<LongTimeRangePicker
periodMapping={LONG_TIME_PERIOD_MAPPING}
onSelect={setPeriod}
queryDateFormat={queryDateFormat}
/>
)}
<div className="mb-4">
<div className="system-xl-semibold mb-2 text-text-primary">{t('common.appMenus.overview')}</div>
<div className="flex items-center justify-between">
{IS_CLOUD_EDITION
? (
<TimeRangePicker
ranges={TIME_PERIOD_MAPPING}
onSelect={setPeriod}
queryDateFormat={queryDateFormat}
/>
)
: (
<LongTimeRangePicker
periodMapping={LONG_TIME_PERIOD_MAPPING}
onSelect={setPeriod}
queryDateFormat={queryDateFormat}
/>
)}
{headerRight}
</div>
</div>
{!isWorkflow && (
<div className='mb-6 grid w-full grid-cols-1 gap-6 xl:grid-cols-2'>
<div className="mb-6 grid w-full grid-cols-1 gap-6 xl:grid-cols-2">
<ConversationsChart period={period} id={appId} />
<EndUsersChart period={period} id={appId} />
</div>
)}
{!isWorkflow && (
<div className='mb-6 grid w-full grid-cols-1 gap-6 xl:grid-cols-2'>
<div className="mb-6 grid w-full grid-cols-1 gap-6 xl:grid-cols-2">
{isChatApp
? (
<AvgSessionInteractions period={period} id={appId} />
)
<AvgSessionInteractions period={period} id={appId} />
)
: (
<AvgResponseTime period={period} id={appId} />
)}
<AvgResponseTime period={period} id={appId} />
)}
<TokenPerSecond period={period} id={appId} />
</div>
)}
{!isWorkflow && (
<div className='mb-6 grid w-full grid-cols-1 gap-6 xl:grid-cols-2'>
<div className="mb-6 grid w-full grid-cols-1 gap-6 xl:grid-cols-2">
<UserSatisfactionRate period={period} id={appId} />
<CostChart period={period} id={appId} />
</div>
)}
{!isWorkflow && isChatApp && (
<div className='mb-6 grid w-full grid-cols-1 gap-6 xl:grid-cols-2'>
<div className="mb-6 grid w-full grid-cols-1 gap-6 xl:grid-cols-2">
<MessagesChart period={period} id={appId} />
</div>
)}
{isWorkflow && (
<div className='mb-6 grid w-full grid-cols-1 gap-6 xl:grid-cols-2'>
<div className="mb-6 grid w-full grid-cols-1 gap-6 xl:grid-cols-2">
<WorkflowMessagesChart period={period} id={appId} />
<WorkflowDailyTerminalsChart period={period} id={appId} />
</div>
)}
{isWorkflow && (
<div className='mb-6 grid w-full grid-cols-1 gap-6 xl:grid-cols-2'>
<div className="mb-6 grid w-full grid-cols-1 gap-6 xl:grid-cols-2">
<WorkflowCostChart period={period} id={appId} />
<AvgUserInteractions period={period} id={appId} />
</div>

View File

@ -1,13 +1,14 @@
'use client'
import type { PeriodParams } from '@/app/components/app/overview/app-chart'
import type { FC } from 'react'
import React from 'react'
import type { PeriodParams } from '@/app/components/app/overview/app-chart'
import type { Item } from '@/app/components/base/select'
import { SimpleSelect } from '@/app/components/base/select'
import { useTranslation } from 'react-i18next'
import dayjs from 'dayjs'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { SimpleSelect } from '@/app/components/base/select'
type Props = {
periodMapping: { [key: string]: { value: number; name: string } }
periodMapping: { [key: string]: { value: number, name: string } }
onSelect: (payload: PeriodParams) => void
queryDateFormat: string
}
@ -53,10 +54,10 @@ const LongTimeRangePicker: FC<Props> = ({
return (
<SimpleSelect
items={Object.entries(periodMapping).map(([k, v]) => ({ value: k, name: t(`appLog.filter.period.${v.name}`) }))}
className='mt-0 !w-40'
className="mt-0 !w-40"
notClearable={true}
onSelect={handleSelect}
defaultValue={'2'}
defaultValue="2"
/>
)
}

View File

@ -1,7 +1,7 @@
import React from 'react'
import ApikeyInfoPanel from '@/app/components/app/overview/apikey-info-panel'
import ChartView from './chart-view'
import TracingPanel from './tracing/panel'
import ApikeyInfoPanel from '@/app/components/app/overview/apikey-info-panel'
export type IDevelopProps = {
params: Promise<{ appId: string }>

View File

@ -1,15 +1,15 @@
'use client'
import { RiCalendarLine } from '@remixicon/react'
import type { Dayjs } from 'dayjs'
import type { FC } from 'react'
import type { TriggerProps } from '@/app/components/base/date-and-time-picker/types'
import { RiCalendarLine } from '@remixicon/react'
import dayjs from 'dayjs'
import { noop } from 'lodash-es'
import React, { useCallback } from 'react'
import Picker from '@/app/components/base/date-and-time-picker/date-picker'
import { useI18N } from '@/context/i18n'
import { cn } from '@/utils/classnames'
import { formatToLocalTime } from '@/utils/format'
import { useI18N } from '@/context/i18n'
import Picker from '@/app/components/base/date-and-time-picker/date-picker'
import type { TriggerProps } from '@/app/components/base/date-and-time-picker/types'
import { noop } from 'lodash-es'
import dayjs from 'dayjs'
type Props = {
start: Dayjs
@ -50,9 +50,9 @@ const DatePicker: FC<Props> = ({
}, [availableEndDate, start])
return (
<div className='flex h-8 items-center space-x-0.5 rounded-lg bg-components-input-bg-normal px-2'>
<div className='p-px'>
<RiCalendarLine className='size-3.5 text-text-tertiary' />
<div className="flex h-8 items-center space-x-0.5 rounded-lg bg-components-input-bg-normal px-2">
<div className="p-px">
<RiCalendarLine className="size-3.5 text-text-tertiary" />
</div>
<Picker
value={start}
@ -63,7 +63,7 @@ const DatePicker: FC<Props> = ({
noConfirm
getIsDateDisabled={startDateDisabled}
/>
<span className='system-sm-regular text-text-tertiary'>-</span>
<span className="system-sm-regular text-text-tertiary">-</span>
<Picker
value={end}
onChange={onEndChange}

View File

@ -1,19 +1,19 @@
'use client'
import type { PeriodParams, PeriodParamsWithTimeRange } from '@/app/components/app/overview/app-chart'
import type { FC } from 'react'
import React, { useCallback, useState } from 'react'
import type { Dayjs } from 'dayjs'
import { HourglassShape } from '@/app/components/base/icons/src/vender/other'
import RangeSelector from './range-selector'
import DatePicker from './date-picker'
import type { FC } from 'react'
import type { PeriodParams, PeriodParamsWithTimeRange } from '@/app/components/app/overview/app-chart'
import dayjs from 'dayjs'
import React, { useCallback, useState } from 'react'
import { HourglassShape } from '@/app/components/base/icons/src/vender/other'
import { useI18N } from '@/context/i18n'
import { formatToLocalTime } from '@/utils/format'
import DatePicker from './date-picker'
import RangeSelector from './range-selector'
const today = dayjs()
type Props = {
ranges: { value: number; name: string }[]
ranges: { value: number, name: string }[]
onSelect: (payload: PeriodParams) => void
queryDateFormat: string
}
@ -44,9 +44,12 @@ const TimeRangePicker: FC<Props> = ({
const handleDateChange = useCallback((type: 'start' | 'end') => {
return (date?: Dayjs) => {
if (!date) return
if (type === 'start' && date.isSame(start)) return
if (type === 'end' && date.isSame(end)) return
if (!date)
return
if (type === 'start' && date.isSame(start))
return
if (type === 'end' && date.isSame(end))
return
if (type === 'start')
setStart(date)
else
@ -67,13 +70,13 @@ const TimeRangePicker: FC<Props> = ({
}, [start, end, onSelect, locale, queryDateFormat])
return (
<div className='flex items-center'>
<div className="flex items-center">
<RangeSelector
isCustomRange={isCustomRange}
ranges={ranges}
onSelect={handleRangeChange}
/>
<HourglassShape className='h-3.5 w-2 text-components-input-bg-normal' />
<HourglassShape className="h-3.5 w-2 text-components-input-bg-normal" />
<DatePicker
start={start}
end={end}

View File

@ -1,19 +1,19 @@
'use client'
import type { PeriodParamsWithTimeRange, TimeRange } from '@/app/components/app/overview/app-chart'
import type { FC } from 'react'
import React, { useCallback } from 'react'
import { SimpleSelect } from '@/app/components/base/select'
import type { PeriodParamsWithTimeRange, TimeRange } from '@/app/components/app/overview/app-chart'
import type { Item } from '@/app/components/base/select'
import dayjs from 'dayjs'
import { RiArrowDownSLine, RiCheckLine } from '@remixicon/react'
import { cn } from '@/utils/classnames'
import dayjs from 'dayjs'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { SimpleSelect } from '@/app/components/base/select'
import { cn } from '@/utils/classnames'
const today = dayjs()
type Props = {
isCustomRange: boolean
ranges: { value: number; name: string }[]
ranges: { value: number, name: string }[]
onSelect: (payload: PeriodParamsWithTimeRange) => void
}
@ -41,13 +41,13 @@ const RangeSelector: FC<Props> = ({
const renderTrigger = useCallback((item: Item | null, isOpen: boolean) => {
return (
<div className={cn('flex h-8 cursor-pointer items-center space-x-1.5 rounded-lg bg-components-input-bg-normal pl-3 pr-2', isOpen && 'bg-state-base-hover-alt')}>
<div className='system-sm-regular text-components-input-text-filled'>{isCustomRange ? t('appLog.filter.period.custom') : item?.name}</div>
<div className="system-sm-regular text-components-input-text-filled">{isCustomRange ? t('appLog.filter.period.custom') : item?.name}</div>
<RiArrowDownSLine className={cn('size-4 text-text-quaternary', isOpen && 'text-text-secondary')} />
</div>
)
}, [isCustomRange])
const renderOption = useCallback(({ item, selected }: { item: Item; selected: boolean }) => {
const renderOption = useCallback(({ item, selected }: { item: Item, selected: boolean }) => {
return (
<>
{selected && (
@ -66,14 +66,14 @@ const RangeSelector: FC<Props> = ({
return (
<SimpleSelect
items={ranges.map(v => ({ ...v, name: t(`appLog.filter.period.${v.name}`) }))}
className='mt-0 !w-40'
className="mt-0 !w-40"
notClearable={true}
onSelect={handleSelectRange}
defaultValue={0}
wrapperClassName='h-8'
optionWrapClassName='w-[200px] translate-x-[-24px]'
wrapperClassName="h-8"
optionWrapClassName="w-[200px] translate-x-[-24px]"
renderTrigger={renderTrigger}
optionClassName='flex items-center py-0 pl-7 pr-2 h-8'
optionClassName="flex items-center py-0 pl-7 pr-2 h-8"
renderOption={renderOption}
/>
)

View File

@ -1,8 +1,8 @@
import React from 'react'
import { render } from '@testing-library/react'
import React from 'react'
import { OpikIconBig } from '@/app/components/base/icons/src/public/tracing'
import { normalizeAttrs } from '@/app/components/base/icons/utils'
import iconData from '@/app/components/base/icons/src/public/tracing/OpikIconBig.json'
import { normalizeAttrs } from '@/app/components/base/icons/utils'
describe('SVG Attribute Error Reproduction', () => {
// Capture console errors

View File

@ -1,15 +1,15 @@
'use client'
import type { FC } from 'react'
import React, { useCallback, useRef, useState } from 'react'
import type { PopupProps } from './config-popup'
import ConfigPopup from './config-popup'
import { cn } from '@/utils/classnames'
import React, { useCallback, useRef, useState } from 'react'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import { cn } from '@/utils/classnames'
import ConfigPopup from './config-popup'
type Props = {
readOnly: boolean
@ -42,7 +42,7 @@ const ConfigBtn: FC<Props> = ({
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement='bottom-end'
placement="bottom-end"
offset={12}
>
<PortalToFollowElemTrigger onClick={handleTrigger}>
@ -50,7 +50,7 @@ const ConfigBtn: FC<Props> = ({
{children}
</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[11]'>
<PortalToFollowElemContent className="z-[11]">
<ConfigPopup {...popupProps} />
</PortalToFollowElemContent>
</PortalToFollowElem>

View File

@ -1,18 +1,18 @@
'use client'
import type { FC, JSX } from 'react'
import type { AliyunConfig, ArizeConfig, DatabricksConfig, LangFuseConfig, LangSmithConfig, MLflowConfig, OpikConfig, PhoenixConfig, TencentConfig, WeaveConfig } from './type'
import { useBoolean } from 'ahooks'
import React, { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useBoolean } from 'ahooks'
import TracingIcon from './tracing-icon'
import ProviderPanel from './provider-panel'
import type { AliyunConfig, ArizeConfig, DatabricksConfig, LangFuseConfig, LangSmithConfig, MLflowConfig, OpikConfig, PhoenixConfig, TencentConfig, WeaveConfig } from './type'
import { TracingProvider } from './type'
import ProviderConfigModal from './provider-config-modal'
import Indicator from '@/app/components/header/indicator'
import Divider from '@/app/components/base/divider'
import Switch from '@/app/components/base/switch'
import Tooltip from '@/app/components/base/tooltip'
import Divider from '@/app/components/base/divider'
import Indicator from '@/app/components/header/indicator'
import { cn } from '@/utils/classnames'
import ProviderConfigModal from './provider-config-modal'
import ProviderPanel from './provider-panel'
import TracingIcon from './tracing-icon'
import { TracingProvider } from './type'
const I18N_PREFIX = 'app.tracing'
@ -92,7 +92,7 @@ const ConfigPopup: FC<PopupProps> = ({
const switchContent = (
<Switch
className='ml-3'
className="ml-3"
defaultValue={enabled}
onChange={onStatusChange}
disabled={providerAllNotConfigured}
@ -322,13 +322,13 @@ const ConfigPopup: FC<PopupProps> = ({
}
return (
<div className='w-[420px] rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-4 shadow-xl'>
<div className='flex items-center justify-between'>
<div className='flex items-center'>
<TracingIcon size='md' className='mr-2' />
<div className='title-2xl-semi-bold text-text-primary'>{t(`${I18N_PREFIX}.tracing`)}</div>
<div className="w-[420px] rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-4 shadow-xl">
<div className="flex items-center justify-between">
<div className="flex items-center">
<TracingIcon size="md" className="mr-2" />
<div className="title-2xl-semi-bold text-text-primary">{t(`${I18N_PREFIX}.tracing`)}</div>
</div>
<div className='flex items-center'>
<div className="flex items-center">
<Indicator color={enabled ? 'green' : 'gray'} />
<div className={cn('system-xs-semibold-uppercase ml-1 text-text-tertiary', enabled && 'text-util-colors-green-green-600')}>
{t(`${I18N_PREFIX}.${enabled ? 'enabled' : 'disabled'}`)}
@ -337,53 +337,53 @@ const ConfigPopup: FC<PopupProps> = ({
<>
{providerAllNotConfigured
? (
<Tooltip
popupContent={t(`${I18N_PREFIX}.disabledTip`)}
>
{switchContent}
</Tooltip>
)
<Tooltip
popupContent={t(`${I18N_PREFIX}.disabledTip`)}
>
{switchContent}
</Tooltip>
)
: switchContent}
</>
)}
</div>
</div>
<div className='system-xs-regular mt-2 text-text-tertiary'>
<div className="system-xs-regular mt-2 text-text-tertiary">
{t(`${I18N_PREFIX}.tracingDescription`)}
</div>
<Divider className='my-3' />
<div className='relative'>
<Divider className="my-3" />
<div className="relative">
{(providerAllConfigured || providerAllNotConfigured)
? (
<>
<div className='system-xs-medium-uppercase text-text-tertiary'>{t(`${I18N_PREFIX}.configProviderTitle.${providerAllConfigured ? 'configured' : 'notConfigured'}`)}</div>
<div className='mt-2 max-h-96 space-y-2 overflow-y-auto'>
{langfusePanel}
{langSmithPanel}
{opikPanel}
{mlflowPanel}
{databricksPanel}
{weavePanel}
{arizePanel}
{phoenixPanel}
{aliyunPanel}
{tencentPanel}
</div>
</>
)
<>
<div className="system-xs-medium-uppercase text-text-tertiary">{t(`${I18N_PREFIX}.configProviderTitle.${providerAllConfigured ? 'configured' : 'notConfigured'}`)}</div>
<div className="mt-2 max-h-96 space-y-2 overflow-y-auto">
{langfusePanel}
{langSmithPanel}
{opikPanel}
{mlflowPanel}
{databricksPanel}
{weavePanel}
{arizePanel}
{phoenixPanel}
{aliyunPanel}
{tencentPanel}
</div>
</>
)
: (
<>
<div className='system-xs-medium-uppercase text-text-tertiary'>{t(`${I18N_PREFIX}.configProviderTitle.configured`)}</div>
<div className='mt-2 max-h-40 space-y-2 overflow-y-auto'>
{configuredProviderPanel()}
</div>
<div className='system-xs-medium-uppercase mt-3 text-text-tertiary'>{t(`${I18N_PREFIX}.configProviderTitle.moreProvider`)}</div>
<div className='mt-2 max-h-40 space-y-2 overflow-y-auto'>
{moreProviderPanel()}
</div>
</>
)}
<>
<div className="system-xs-medium-uppercase text-text-tertiary">{t(`${I18N_PREFIX}.configProviderTitle.configured`)}</div>
<div className="mt-2 max-h-40 space-y-2 overflow-y-auto">
{configuredProviderPanel()}
</div>
<div className="system-xs-medium-uppercase mt-3 text-text-tertiary">{t(`${I18N_PREFIX}.configProviderTitle.moreProvider`)}</div>
<div className="mt-2 max-h-40 space-y-2 overflow-y-auto">
{moreProviderPanel()}
</div>
</>
)}
</div>
{isShowConfigModal && (

View File

@ -1,8 +1,8 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { cn } from '@/utils/classnames'
import Input from '@/app/components/base/input'
import { cn } from '@/utils/classnames'
type Props = {
className?: string
@ -25,14 +25,17 @@ const Field: FC<Props> = ({
}) => {
return (
<div className={cn(className)}>
<div className='flex py-[7px]'>
<div className={cn(labelClassName, 'flex h-[18px] items-center text-[13px] font-medium text-text-primary')}>{label} </div>
{isRequired && <span className='ml-0.5 text-xs font-semibold text-[#D92D20]'>*</span>}
<div className="flex py-[7px]">
<div className={cn(labelClassName, 'flex h-[18px] items-center text-[13px] font-medium text-text-primary')}>
{label}
{' '}
</div>
{isRequired && <span className="ml-0.5 text-xs font-semibold text-[#D92D20]">*</span>}
</div>
<Input
value={value}
onChange={e => onChange(e.target.value)}
className='h-9'
className="h-9"
placeholder={placeholder}
/>
</div>

View File

@ -1,26 +1,26 @@
'use client'
import type { FC } from 'react'
import React, { useEffect, useState } from 'react'
import type { AliyunConfig, ArizeConfig, DatabricksConfig, LangFuseConfig, LangSmithConfig, MLflowConfig, OpikConfig, PhoenixConfig, TencentConfig, WeaveConfig } from './type'
import type { TracingStatus } from '@/models/app'
import {
RiArrowDownDoubleLine,
RiEqualizer2Line,
} from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { usePathname } from 'next/navigation'
import { useBoolean } from 'ahooks'
import type { AliyunConfig, ArizeConfig, DatabricksConfig, LangFuseConfig, LangSmithConfig, MLflowConfig, OpikConfig, PhoenixConfig, TencentConfig, WeaveConfig } from './type'
import { TracingProvider } from './type'
import TracingIcon from './tracing-icon'
import ConfigButton from './config-button'
import { cn } from '@/utils/classnames'
import { AliyunIcon, ArizeIcon, DatabricksIcon, LangfuseIcon, LangsmithIcon, MlflowIcon, OpikIcon, PhoenixIcon, TencentIcon, WeaveIcon } from '@/app/components/base/icons/src/public/tracing'
import Indicator from '@/app/components/header/indicator'
import { fetchTracingConfig as doFetchTracingConfig, fetchTracingStatus, updateTracingStatus } from '@/service/apps'
import type { TracingStatus } from '@/models/app'
import Toast from '@/app/components/base/toast'
import { useAppContext } from '@/context/app-context'
import Loading from '@/app/components/base/loading'
import { usePathname } from 'next/navigation'
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Divider from '@/app/components/base/divider'
import { AliyunIcon, ArizeIcon, DatabricksIcon, LangfuseIcon, LangsmithIcon, MlflowIcon, OpikIcon, PhoenixIcon, TencentIcon, WeaveIcon } from '@/app/components/base/icons/src/public/tracing'
import Loading from '@/app/components/base/loading'
import Toast from '@/app/components/base/toast'
import Indicator from '@/app/components/header/indicator'
import { useAppContext } from '@/context/app-context'
import { fetchTracingConfig as doFetchTracingConfig, fetchTracingStatus, updateTracingStatus } from '@/service/apps'
import { cn } from '@/utils/classnames'
import ConfigButton from './config-button'
import TracingIcon from './tracing-icon'
import { TracingProvider } from './type'
const I18N_PREFIX = 'app.tracing'
@ -215,8 +215,8 @@ const Panel: FC = () => {
if (!isLoaded) {
return (
<div className='mb-3 flex items-center justify-between'>
<div className='w-[200px]'>
<div className="mb-3 flex items-center justify-between">
<div className="w-[200px]">
<Loading />
</div>
</div>
@ -252,14 +252,14 @@ const Panel: FC = () => {
'flex cursor-pointer select-none items-center rounded-xl border-l-[0.5px] border-t border-effects-highlight bg-background-default-dodge p-2 shadow-xs hover:border-effects-highlight-lightmode-off hover:bg-background-default-lighter',
)}
>
<TracingIcon size='md' />
<div className='system-sm-semibold mx-2 text-text-secondary'>{t(`${I18N_PREFIX}.title`)}</div>
<div className='rounded-md p-1'>
<RiEqualizer2Line className='h-4 w-4 text-text-tertiary' />
<TracingIcon size="md" />
<div className="system-sm-semibold mx-2 text-text-secondary">{t(`${I18N_PREFIX}.title`)}</div>
<div className="rounded-md p-1">
<RiEqualizer2Line className="h-4 w-4 text-text-tertiary" />
</div>
<Divider type='vertical' className='h-3.5' />
<div className='rounded-md p-1'>
<RiArrowDownDoubleLine className='h-4 w-4 text-text-tertiary' />
<Divider type="vertical" className="h-3.5" />
<div className="rounded-md p-1">
<RiArrowDownDoubleLine className="h-4 w-4 text-text-tertiary" />
</div>
</div>
</ConfigButton>
@ -291,17 +291,17 @@ const Panel: FC = () => {
'flex cursor-pointer select-none items-center rounded-xl border-l-[0.5px] border-t border-effects-highlight bg-background-default-dodge p-2 shadow-xs hover:border-effects-highlight-lightmode-off hover:bg-background-default-lighter',
)}
>
<div className='ml-4 mr-1 flex items-center'>
<div className="ml-4 mr-1 flex items-center">
<Indicator color={enabled ? 'green' : 'gray'} />
<div className='system-xs-semibold-uppercase ml-1.5 text-text-tertiary'>
<div className="system-xs-semibold-uppercase ml-1.5 text-text-tertiary">
{t(`${I18N_PREFIX}.${enabled ? 'enabled' : 'disabled'}`)}
</div>
</div>
{InUseProviderIcon && <InUseProviderIcon className='ml-1 h-4' />}
<div className='ml-2 rounded-md p-1'>
<RiEqualizer2Line className='h-4 w-4 text-text-tertiary' />
{InUseProviderIcon && <InUseProviderIcon className="ml-1 h-4" />}
<div className="ml-2 rounded-md p-1">
<RiEqualizer2Line className="h-4 w-4 text-text-tertiary" />
</div>
<Divider type='vertical' className='h-3.5' />
<Divider type="vertical" className="h-3.5" />
</div>
</ConfigButton>
)}

View File

@ -1,23 +1,23 @@
'use client'
import type { FC } from 'react'
import type { AliyunConfig, ArizeConfig, DatabricksConfig, LangFuseConfig, LangSmithConfig, MLflowConfig, OpikConfig, PhoenixConfig, TencentConfig, WeaveConfig } from './type'
import { useBoolean } from 'ahooks'
import React, { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useBoolean } from 'ahooks'
import Field from './field'
import type { AliyunConfig, ArizeConfig, DatabricksConfig, LangFuseConfig, LangSmithConfig, MLflowConfig, OpikConfig, PhoenixConfig, TencentConfig, WeaveConfig } from './type'
import { TracingProvider } from './type'
import { docURL } from './config'
import Button from '@/app/components/base/button'
import Confirm from '@/app/components/base/confirm'
import Divider from '@/app/components/base/divider'
import { LinkExternal02 } from '@/app/components/base/icons/src/vender/line/general'
import { Lock01 } from '@/app/components/base/icons/src/vender/solid/security'
import {
PortalToFollowElem,
PortalToFollowElemContent,
} from '@/app/components/base/portal-to-follow-elem'
import { Lock01 } from '@/app/components/base/icons/src/vender/solid/security'
import Button from '@/app/components/base/button'
import { LinkExternal02 } from '@/app/components/base/icons/src/vender/line/general'
import Confirm from '@/app/components/base/confirm'
import { addTracingConfig, removeTracingConfig, updateTracingConfig } from '@/service/apps'
import Toast from '@/app/components/base/toast'
import Divider from '@/app/components/base/divider'
import { addTracingConfig, removeTracingConfig, updateTracingConfig } from '@/service/apps'
import { docURL } from './config'
import Field from './field'
import { TracingProvider } from './type'
type Props = {
appId: string
@ -295,403 +295,407 @@ const ProviderConfigModal: FC<Props> = ({
<>
{!isShowRemoveConfirm
? (
<PortalToFollowElem open>
<PortalToFollowElemContent className='z-[60] h-full w-full'>
<div className='fixed inset-0 flex items-center justify-center bg-background-overlay'>
<div className='mx-2 max-h-[calc(100vh-120px)] w-[640px] overflow-y-auto rounded-2xl bg-components-panel-bg shadow-xl'>
<div className='px-8 pt-8'>
<div className='mb-4 flex items-center justify-between'>
<div className='title-2xl-semi-bold text-text-primary'>{t(`${I18N_PREFIX}.title`)}{t(`app.tracing.${type}.title`)}</div>
</div>
<div className='space-y-4'>
{type === TracingProvider.arize && (
<>
<Field
label='API Key'
labelClassName='!text-sm'
isRequired
value={(config as ArizeConfig).api_key}
onChange={handleConfigChange('api_key')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'API Key' })!}
/>
<Field
label='Space ID'
labelClassName='!text-sm'
isRequired
value={(config as ArizeConfig).space_id}
onChange={handleConfigChange('space_id')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'Space ID' })!}
/>
<Field
label={t(`${I18N_PREFIX}.project`)!}
labelClassName='!text-sm'
isRequired
value={(config as ArizeConfig).project}
onChange={handleConfigChange('project')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.project`) })!}
/>
<Field
label='Endpoint'
labelClassName='!text-sm'
value={(config as ArizeConfig).endpoint}
onChange={handleConfigChange('endpoint')}
placeholder={'https://otlp.arize.com'}
/>
</>
)}
{type === TracingProvider.phoenix && (
<>
<Field
label='API Key'
labelClassName='!text-sm'
isRequired
value={(config as PhoenixConfig).api_key}
onChange={handleConfigChange('api_key')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'API Key' })!}
/>
<Field
label={t(`${I18N_PREFIX}.project`)!}
labelClassName='!text-sm'
isRequired
value={(config as PhoenixConfig).project}
onChange={handleConfigChange('project')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.project`) })!}
/>
<Field
label='Endpoint'
labelClassName='!text-sm'
value={(config as PhoenixConfig).endpoint}
onChange={handleConfigChange('endpoint')}
placeholder={'https://app.phoenix.arize.com'}
/>
</>
)}
{type === TracingProvider.aliyun && (
<>
<Field
label='License Key'
labelClassName='!text-sm'
isRequired
value={(config as AliyunConfig).license_key}
onChange={handleConfigChange('license_key')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'License Key' })!}
/>
<Field
label='Endpoint'
labelClassName='!text-sm'
value={(config as AliyunConfig).endpoint}
onChange={handleConfigChange('endpoint')}
placeholder={'https://tracing.arms.aliyuncs.com'}
/>
<Field
label='App Name'
labelClassName='!text-sm'
value={(config as AliyunConfig).app_name}
onChange={handleConfigChange('app_name')}
/>
</>
)}
{type === TracingProvider.tencent && (
<>
<Field
label='Token'
labelClassName='!text-sm'
isRequired
value={(config as TencentConfig).token}
onChange={handleConfigChange('token')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'Token' })!}
/>
<Field
label='Endpoint'
labelClassName='!text-sm'
isRequired
value={(config as TencentConfig).endpoint}
onChange={handleConfigChange('endpoint')}
placeholder='https://your-region.cls.tencentcs.com'
/>
<Field
label='Service Name'
labelClassName='!text-sm'
isRequired
value={(config as TencentConfig).service_name}
onChange={handleConfigChange('service_name')}
placeholder='dify_app'
/>
</>
)}
{type === TracingProvider.weave && (
<>
<Field
label='API Key'
labelClassName='!text-sm'
isRequired
value={(config as WeaveConfig).api_key}
onChange={handleConfigChange('api_key')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'API Key' })!}
/>
<Field
label={t(`${I18N_PREFIX}.project`)!}
labelClassName='!text-sm'
isRequired
value={(config as WeaveConfig).project}
onChange={handleConfigChange('project')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.project`) })!}
/>
<Field
label='Entity'
labelClassName='!text-sm'
value={(config as WeaveConfig).entity}
onChange={handleConfigChange('entity')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'Entity' })!}
/>
<Field
label='Endpoint'
labelClassName='!text-sm'
value={(config as WeaveConfig).endpoint}
onChange={handleConfigChange('endpoint')}
placeholder={'https://trace.wandb.ai/'}
/>
<Field
label='Host'
labelClassName='!text-sm'
value={(config as WeaveConfig).host}
onChange={handleConfigChange('host')}
placeholder={'https://api.wandb.ai'}
/>
</>
)}
{type === TracingProvider.langSmith && (
<>
<Field
label='API Key'
labelClassName='!text-sm'
isRequired
value={(config as LangSmithConfig).api_key}
onChange={handleConfigChange('api_key')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'API Key' })!}
/>
<Field
label={t(`${I18N_PREFIX}.project`)!}
labelClassName='!text-sm'
isRequired
value={(config as LangSmithConfig).project}
onChange={handleConfigChange('project')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.project`) })!}
/>
<Field
label='Endpoint'
labelClassName='!text-sm'
value={(config as LangSmithConfig).endpoint}
onChange={handleConfigChange('endpoint')}
placeholder={'https://api.smith.langchain.com'}
/>
</>
)}
{type === TracingProvider.langfuse && (
<>
<Field
label={t(`${I18N_PREFIX}.secretKey`)!}
labelClassName='!text-sm'
value={(config as LangFuseConfig).secret_key}
isRequired
onChange={handleConfigChange('secret_key')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.secretKey`) })!}
/>
<Field
label={t(`${I18N_PREFIX}.publicKey`)!}
labelClassName='!text-sm'
isRequired
value={(config as LangFuseConfig).public_key}
onChange={handleConfigChange('public_key')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.publicKey`) })!}
/>
<Field
label='Host'
labelClassName='!text-sm'
isRequired
value={(config as LangFuseConfig).host}
onChange={handleConfigChange('host')}
placeholder='https://cloud.langfuse.com'
/>
</>
)}
{type === TracingProvider.opik && (
<>
<Field
label='API Key'
labelClassName='!text-sm'
value={(config as OpikConfig).api_key}
onChange={handleConfigChange('api_key')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'API Key' })!}
/>
<Field
label={t(`${I18N_PREFIX}.project`)!}
labelClassName='!text-sm'
value={(config as OpikConfig).project}
onChange={handleConfigChange('project')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.project`) })!}
/>
<Field
label='Workspace'
labelClassName='!text-sm'
value={(config as OpikConfig).workspace}
onChange={handleConfigChange('workspace')}
placeholder={'default'}
/>
<Field
label='Url'
labelClassName='!text-sm'
value={(config as OpikConfig).url}
onChange={handleConfigChange('url')}
placeholder={'https://www.comet.com/opik/api/'}
/>
</>
)}
{type === TracingProvider.mlflow && (
<>
<Field
label={t(`${I18N_PREFIX}.trackingUri`)!}
labelClassName='!text-sm'
value={(config as MLflowConfig).tracking_uri}
isRequired
onChange={handleConfigChange('tracking_uri')}
placeholder={'http://localhost:5000'}
/>
<Field
label={t(`${I18N_PREFIX}.experimentId`)!}
labelClassName='!text-sm'
isRequired
value={(config as MLflowConfig).experiment_id}
onChange={handleConfigChange('experiment_id')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.experimentId`) })!}
/>
<Field
label={t(`${I18N_PREFIX}.username`)!}
labelClassName='!text-sm'
value={(config as MLflowConfig).username}
onChange={handleConfigChange('username')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.username`) })!}
/>
<Field
label={t(`${I18N_PREFIX}.password`)!}
labelClassName='!text-sm'
value={(config as MLflowConfig).password}
onChange={handleConfigChange('password')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.password`) })!}
/>
</>
)}
{type === TracingProvider.databricks && (
<>
<Field
label={t(`${I18N_PREFIX}.experimentId`)!}
labelClassName='!text-sm'
value={(config as DatabricksConfig).experiment_id}
onChange={handleConfigChange('experiment_id')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.experimentId`) })!}
isRequired
/>
<Field
label={t(`${I18N_PREFIX}.databricksHost`)!}
labelClassName='!text-sm'
value={(config as DatabricksConfig).host}
onChange={handleConfigChange('host')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.databricksHost`) })!}
isRequired
/>
<Field
label={t(`${I18N_PREFIX}.clientId`)!}
labelClassName='!text-sm'
value={(config as DatabricksConfig).client_id}
onChange={handleConfigChange('client_id')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.clientId`) })!}
/>
<Field
label={t(`${I18N_PREFIX}.clientSecret`)!}
labelClassName='!text-sm'
value={(config as DatabricksConfig).client_secret}
onChange={handleConfigChange('client_secret')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.clientSecret`) })!}
/>
<Field
label={t(`${I18N_PREFIX}.personalAccessToken`)!}
labelClassName='!text-sm'
value={(config as DatabricksConfig).personal_access_token}
onChange={handleConfigChange('personal_access_token')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.personalAccessToken`) })!}
/>
</>
)}
</div>
<div className='my-8 flex h-8 items-center justify-between'>
<a
className='flex items-center space-x-1 text-xs font-normal leading-[18px] text-[#155EEF]'
target='_blank'
href={docURL[type]}
>
<span>{t(`${I18N_PREFIX}.viewDocsLink`, { key: t(`app.tracing.${type}.title`) })}</span>
<LinkExternal02 className='h-3 w-3' />
</a>
<div className='flex items-center'>
{isEdit && (
<>
<Button
className='h-9 text-sm font-medium text-text-secondary'
onClick={showRemoveConfirm}
>
<span className='text-[#D92D20]'>{t('common.operation.remove')}</span>
</Button>
<Divider type='vertical' className='mx-3 h-[18px]' />
</>
)}
<Button
className='mr-2 h-9 text-sm font-medium text-text-secondary'
onClick={onCancel}
>
{t('common.operation.cancel')}
</Button>
<Button
className='h-9 text-sm font-medium'
variant='primary'
onClick={handleSave}
loading={isSaving}
>
{t(`common.operation.${isAdd ? 'saveAndEnable' : 'save'}`)}
</Button>
<PortalToFollowElem open>
<PortalToFollowElemContent className="z-[60] h-full w-full">
<div className="fixed inset-0 flex items-center justify-center bg-background-overlay">
<div className="mx-2 max-h-[calc(100vh-120px)] w-[640px] overflow-y-auto rounded-2xl bg-components-panel-bg shadow-xl">
<div className="px-8 pt-8">
<div className="mb-4 flex items-center justify-between">
<div className="title-2xl-semi-bold text-text-primary">
{t(`${I18N_PREFIX}.title`)}
{t(`app.tracing.${type}.title`)}
</div>
</div>
<div className="space-y-4">
{type === TracingProvider.arize && (
<>
<Field
label="API Key"
labelClassName="!text-sm"
isRequired
value={(config as ArizeConfig).api_key}
onChange={handleConfigChange('api_key')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'API Key' })!}
/>
<Field
label="Space ID"
labelClassName="!text-sm"
isRequired
value={(config as ArizeConfig).space_id}
onChange={handleConfigChange('space_id')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'Space ID' })!}
/>
<Field
label={t(`${I18N_PREFIX}.project`)!}
labelClassName="!text-sm"
isRequired
value={(config as ArizeConfig).project}
onChange={handleConfigChange('project')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.project`) })!}
/>
<Field
label="Endpoint"
labelClassName="!text-sm"
value={(config as ArizeConfig).endpoint}
onChange={handleConfigChange('endpoint')}
placeholder="https://otlp.arize.com"
/>
</>
)}
{type === TracingProvider.phoenix && (
<>
<Field
label="API Key"
labelClassName="!text-sm"
isRequired
value={(config as PhoenixConfig).api_key}
onChange={handleConfigChange('api_key')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'API Key' })!}
/>
<Field
label={t(`${I18N_PREFIX}.project`)!}
labelClassName="!text-sm"
isRequired
value={(config as PhoenixConfig).project}
onChange={handleConfigChange('project')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.project`) })!}
/>
<Field
label="Endpoint"
labelClassName="!text-sm"
value={(config as PhoenixConfig).endpoint}
onChange={handleConfigChange('endpoint')}
placeholder="https://app.phoenix.arize.com"
/>
</>
)}
{type === TracingProvider.aliyun && (
<>
<Field
label="License Key"
labelClassName="!text-sm"
isRequired
value={(config as AliyunConfig).license_key}
onChange={handleConfigChange('license_key')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'License Key' })!}
/>
<Field
label="Endpoint"
labelClassName="!text-sm"
value={(config as AliyunConfig).endpoint}
onChange={handleConfigChange('endpoint')}
placeholder="https://tracing.arms.aliyuncs.com"
/>
<Field
label="App Name"
labelClassName="!text-sm"
value={(config as AliyunConfig).app_name}
onChange={handleConfigChange('app_name')}
/>
</>
)}
{type === TracingProvider.tencent && (
<>
<Field
label="Token"
labelClassName="!text-sm"
isRequired
value={(config as TencentConfig).token}
onChange={handleConfigChange('token')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'Token' })!}
/>
<Field
label="Endpoint"
labelClassName="!text-sm"
isRequired
value={(config as TencentConfig).endpoint}
onChange={handleConfigChange('endpoint')}
placeholder="https://your-region.cls.tencentcs.com"
/>
<Field
label="Service Name"
labelClassName="!text-sm"
isRequired
value={(config as TencentConfig).service_name}
onChange={handleConfigChange('service_name')}
placeholder="dify_app"
/>
</>
)}
{type === TracingProvider.weave && (
<>
<Field
label="API Key"
labelClassName="!text-sm"
isRequired
value={(config as WeaveConfig).api_key}
onChange={handleConfigChange('api_key')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'API Key' })!}
/>
<Field
label={t(`${I18N_PREFIX}.project`)!}
labelClassName="!text-sm"
isRequired
value={(config as WeaveConfig).project}
onChange={handleConfigChange('project')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.project`) })!}
/>
<Field
label="Entity"
labelClassName="!text-sm"
value={(config as WeaveConfig).entity}
onChange={handleConfigChange('entity')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'Entity' })!}
/>
<Field
label="Endpoint"
labelClassName="!text-sm"
value={(config as WeaveConfig).endpoint}
onChange={handleConfigChange('endpoint')}
placeholder="https://trace.wandb.ai/"
/>
<Field
label="Host"
labelClassName="!text-sm"
value={(config as WeaveConfig).host}
onChange={handleConfigChange('host')}
placeholder="https://api.wandb.ai"
/>
</>
)}
{type === TracingProvider.langSmith && (
<>
<Field
label="API Key"
labelClassName="!text-sm"
isRequired
value={(config as LangSmithConfig).api_key}
onChange={handleConfigChange('api_key')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'API Key' })!}
/>
<Field
label={t(`${I18N_PREFIX}.project`)!}
labelClassName="!text-sm"
isRequired
value={(config as LangSmithConfig).project}
onChange={handleConfigChange('project')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.project`) })!}
/>
<Field
label="Endpoint"
labelClassName="!text-sm"
value={(config as LangSmithConfig).endpoint}
onChange={handleConfigChange('endpoint')}
placeholder="https://api.smith.langchain.com"
/>
</>
)}
{type === TracingProvider.langfuse && (
<>
<Field
label={t(`${I18N_PREFIX}.secretKey`)!}
labelClassName="!text-sm"
value={(config as LangFuseConfig).secret_key}
isRequired
onChange={handleConfigChange('secret_key')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.secretKey`) })!}
/>
<Field
label={t(`${I18N_PREFIX}.publicKey`)!}
labelClassName="!text-sm"
isRequired
value={(config as LangFuseConfig).public_key}
onChange={handleConfigChange('public_key')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.publicKey`) })!}
/>
<Field
label="Host"
labelClassName="!text-sm"
isRequired
value={(config as LangFuseConfig).host}
onChange={handleConfigChange('host')}
placeholder="https://cloud.langfuse.com"
/>
</>
)}
{type === TracingProvider.opik && (
<>
<Field
label="API Key"
labelClassName="!text-sm"
value={(config as OpikConfig).api_key}
onChange={handleConfigChange('api_key')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'API Key' })!}
/>
<Field
label={t(`${I18N_PREFIX}.project`)!}
labelClassName="!text-sm"
value={(config as OpikConfig).project}
onChange={handleConfigChange('project')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.project`) })!}
/>
<Field
label="Workspace"
labelClassName="!text-sm"
value={(config as OpikConfig).workspace}
onChange={handleConfigChange('workspace')}
placeholder="default"
/>
<Field
label="Url"
labelClassName="!text-sm"
value={(config as OpikConfig).url}
onChange={handleConfigChange('url')}
placeholder="https://www.comet.com/opik/api/"
/>
</>
)}
{type === TracingProvider.mlflow && (
<>
<Field
label={t(`${I18N_PREFIX}.trackingUri`)!}
labelClassName="!text-sm"
value={(config as MLflowConfig).tracking_uri}
isRequired
onChange={handleConfigChange('tracking_uri')}
placeholder="http://localhost:5000"
/>
<Field
label={t(`${I18N_PREFIX}.experimentId`)!}
labelClassName="!text-sm"
isRequired
value={(config as MLflowConfig).experiment_id}
onChange={handleConfigChange('experiment_id')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.experimentId`) })!}
/>
<Field
label={t(`${I18N_PREFIX}.username`)!}
labelClassName="!text-sm"
value={(config as MLflowConfig).username}
onChange={handleConfigChange('username')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.username`) })!}
/>
<Field
label={t(`${I18N_PREFIX}.password`)!}
labelClassName="!text-sm"
value={(config as MLflowConfig).password}
onChange={handleConfigChange('password')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.password`) })!}
/>
</>
)}
{type === TracingProvider.databricks && (
<>
<Field
label={t(`${I18N_PREFIX}.experimentId`)!}
labelClassName="!text-sm"
value={(config as DatabricksConfig).experiment_id}
onChange={handleConfigChange('experiment_id')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.experimentId`) })!}
isRequired
/>
<Field
label={t(`${I18N_PREFIX}.databricksHost`)!}
labelClassName="!text-sm"
value={(config as DatabricksConfig).host}
onChange={handleConfigChange('host')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.databricksHost`) })!}
isRequired
/>
<Field
label={t(`${I18N_PREFIX}.clientId`)!}
labelClassName="!text-sm"
value={(config as DatabricksConfig).client_id}
onChange={handleConfigChange('client_id')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.clientId`) })!}
/>
<Field
label={t(`${I18N_PREFIX}.clientSecret`)!}
labelClassName="!text-sm"
value={(config as DatabricksConfig).client_secret}
onChange={handleConfigChange('client_secret')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.clientSecret`) })!}
/>
<Field
label={t(`${I18N_PREFIX}.personalAccessToken`)!}
labelClassName="!text-sm"
value={(config as DatabricksConfig).personal_access_token}
onChange={handleConfigChange('personal_access_token')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.personalAccessToken`) })!}
/>
</>
)}
</div>
<div className="my-8 flex h-8 items-center justify-between">
<a
className="flex items-center space-x-1 text-xs font-normal leading-[18px] text-[#155EEF]"
target="_blank"
href={docURL[type]}
>
<span>{t(`${I18N_PREFIX}.viewDocsLink`, { key: t(`app.tracing.${type}.title`) })}</span>
<LinkExternal02 className="h-3 w-3" />
</a>
<div className="flex items-center">
{isEdit && (
<>
<Button
className="h-9 text-sm font-medium text-text-secondary"
onClick={showRemoveConfirm}
>
<span className="text-[#D92D20]">{t('common.operation.remove')}</span>
</Button>
<Divider type="vertical" className="mx-3 h-[18px]" />
</>
)}
<Button
className="mr-2 h-9 text-sm font-medium text-text-secondary"
onClick={onCancel}
>
{t('common.operation.cancel')}
</Button>
<Button
className="h-9 text-sm font-medium"
variant="primary"
onClick={handleSave}
loading={isSaving}
>
{t(`common.operation.${isAdd ? 'saveAndEnable' : 'save'}`)}
</Button>
</div>
</div>
</div>
</div>
<div className='border-t-[0.5px] border-divider-regular'>
<div className='flex items-center justify-center bg-background-section-burn py-3 text-xs text-text-tertiary'>
<Lock01 className='mr-1 h-3 w-3 text-text-tertiary' />
{t('common.modelProvider.encrypted.front')}
<a
className='mx-1 text-primary-600'
target='_blank' rel='noopener noreferrer'
href='https://pycryptodome.readthedocs.io/en/latest/src/cipher/oaep.html'
>
PKCS1_OAEP
</a>
{t('common.modelProvider.encrypted.back')}
<div className="border-t-[0.5px] border-divider-regular">
<div className="flex items-center justify-center bg-background-section-burn py-3 text-xs text-text-tertiary">
<Lock01 className="mr-1 h-3 w-3 text-text-tertiary" />
{t('common.modelProvider.encrypted.front')}
<a
className="mx-1 text-primary-600"
target="_blank"
rel="noopener noreferrer"
href="https://pycryptodome.readthedocs.io/en/latest/src/cipher/oaep.html"
>
PKCS1_OAEP
</a>
{t('common.modelProvider.encrypted.back')}
</div>
</div>
</div>
</div>
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
</PortalToFollowElemContent>
</PortalToFollowElem>
)
: (
<Confirm
isShow
type='warning'
title={t(`${I18N_PREFIX}.removeConfirmTitle`, { key: t(`app.tracing.${type}.title`) })!}
content={t(`${I18N_PREFIX}.removeConfirmContent`)}
onConfirm={handleRemove}
onCancel={hideRemoveConfirm}
/>
)}
<Confirm
isShow
type="warning"
title={t(`${I18N_PREFIX}.removeConfirmTitle`, { key: t(`app.tracing.${type}.title`) })!}
content={t(`${I18N_PREFIX}.removeConfirmContent`)}
onConfirm={handleRemove}
onCancel={hideRemoveConfirm}
/>
)}
</>
)
}

View File

@ -1,14 +1,14 @@
'use client'
import type { FC } from 'react'
import React, { useCallback } from 'react'
import {
RiEqualizer2Line,
} from '@remixicon/react'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { TracingProvider } from './type'
import { cn } from '@/utils/classnames'
import { AliyunIconBig, ArizeIconBig, DatabricksIconBig, LangfuseIconBig, LangsmithIconBig, MlflowIconBig, OpikIconBig, PhoenixIconBig, TencentIconBig, WeaveIconBig } from '@/app/components/base/icons/src/public/tracing'
import { Eye as View } from '@/app/components/base/icons/src/vender/solid/general'
import { cn } from '@/utils/classnames'
import { TracingProvider } from './type'
const I18N_PREFIX = 'app.tracing'
@ -78,30 +78,30 @@ const ProviderPanel: FC<Props> = ({
)}
onClick={handleChosen}
>
<div className={'flex items-center justify-between space-x-1'}>
<div className='flex items-center'>
<Icon className='h-6' />
{isChosen && <div className='system-2xs-medium-uppercase ml-1 flex h-4 items-center rounded-[4px] border border-text-accent-secondary px-1 text-text-accent-secondary'>{t(`${I18N_PREFIX}.inUse`)}</div>}
<div className="flex items-center justify-between space-x-1">
<div className="flex items-center">
<Icon className="h-6" />
{isChosen && <div className="system-2xs-medium-uppercase ml-1 flex h-4 items-center rounded-[4px] border border-text-accent-secondary px-1 text-text-accent-secondary">{t(`${I18N_PREFIX}.inUse`)}</div>}
</div>
{!readOnly && (
<div className={'flex items-center justify-between space-x-1'}>
<div className="flex items-center justify-between space-x-1">
{hasConfigured && (
<div className='flex h-6 cursor-pointer items-center space-x-1 rounded-md border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg px-2 text-text-secondary shadow-xs' onClick={viewBtnClick} >
<View className='h-3 w-3' />
<div className='text-xs font-medium'>{t(`${I18N_PREFIX}.view`)}</div>
<div className="flex h-6 cursor-pointer items-center space-x-1 rounded-md border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg px-2 text-text-secondary shadow-xs" onClick={viewBtnClick}>
<View className="h-3 w-3" />
<div className="text-xs font-medium">{t(`${I18N_PREFIX}.view`)}</div>
</div>
)}
<div
className='flex h-6 cursor-pointer items-center space-x-1 rounded-md border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg px-2 text-text-secondary shadow-xs'
className="flex h-6 cursor-pointer items-center space-x-1 rounded-md border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg px-2 text-text-secondary shadow-xs"
onClick={handleConfigBtnClick}
>
<RiEqualizer2Line className='h-3 w-3' />
<div className='text-xs font-medium'>{t(`${I18N_PREFIX}.config`)}</div>
<RiEqualizer2Line className="h-3 w-3" />
<div className="text-xs font-medium">{t(`${I18N_PREFIX}.config`)}</div>
</div>
</div>
)}
</div>
<div className='system-xs-regular mt-2 text-text-tertiary'>
<div className="system-xs-regular mt-2 text-text-tertiary">
{t(`${I18N_PREFIX}.${type}.description`)}
</div>
</div>

View File

@ -1,8 +1,8 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { cn } from '@/utils/classnames'
import { TracingIcon as Icon } from '@/app/components/base/icons/src/public/tracing'
import { cn } from '@/utils/classnames'
type Props = {
className?: string
@ -21,7 +21,7 @@ const TracingIcon: FC<Props> = ({
const sizeClass = sizeClassMap[size]
return (
<div className={cn(className, sizeClass, 'bg-primary-500 shadow-md')}>
<Icon className='h-full w-full' />
<Icon className="h-full w-full" />
</div>
)
}

View File

@ -2,7 +2,7 @@ import WorkflowApp from '@/app/components/workflow-app'
const Page = () => {
return (
<div className='h-full w-full overflow-x-auto'>
<div className="h-full w-full overflow-x-auto">
<WorkflowApp />
</div>
)

View File

@ -1,7 +1,7 @@
'use client'
import type { FC } from 'react'
import React, { useEffect } from 'react'
import { useRouter } from 'next/navigation'
import React, { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { useAppContext } from '@/context/app-context'
import useDocumentTitle from '@/hooks/use-document-title'

View File

@ -2,7 +2,7 @@ import React from 'react'
import MainDetail from '@/app/components/datasets/documents/detail'
export type IDocumentDetailProps = {
params: Promise<{ datasetId: string; documentId: string }>
params: Promise<{ datasetId: string, documentId: string }>
}
const DocumentDetail = async (props: IDocumentDetailProps) => {

View File

@ -2,7 +2,7 @@ import React from 'react'
import Settings from '@/app/components/datasets/documents/detail/settings'
export type IProps = {
params: Promise<{ datasetId: string; documentId: string }>
params: Promise<{ datasetId: string, documentId: string }>
}
const DocumentSettings = async (props: IProps) => {

View File

@ -1,9 +1,6 @@
'use client'
import type { FC } from 'react'
import React, { useEffect, useMemo, useState } from 'react'
import { usePathname } from 'next/navigation'
import { useTranslation } from 'react-i18next'
import type { RemixiconComponentType } from '@remixicon/react'
import type { FC } from 'react'
import {
RiEqualizer2Fill,
RiEqualizer2Line,
@ -12,17 +9,20 @@ import {
RiFocus2Fill,
RiFocus2Line,
} from '@remixicon/react'
import { usePathname } from 'next/navigation'
import React, { useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import AppSideBar from '@/app/components/app-sidebar'
import Loading from '@/app/components/base/loading'
import DatasetDetailContext from '@/context/dataset-detail'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import { useStore } from '@/app/components/app/store'
import { useAppContext } from '@/context/app-context'
import { PipelineFill, PipelineLine } from '@/app/components/base/icons/src/vender/pipeline'
import { useDatasetDetail, useDatasetRelatedApps } from '@/service/knowledge/use-dataset'
import useDocumentTitle from '@/hooks/use-document-title'
import Loading from '@/app/components/base/loading'
import ExtraInfo from '@/app/components/datasets/extra-info'
import { useAppContext } from '@/context/app-context'
import DatasetDetailContext from '@/context/dataset-detail'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import useDocumentTitle from '@/hooks/use-document-title'
import { useDatasetDetail, useDatasetRelatedApps } from '@/service/knowledge/use-dataset'
import { cn } from '@/utils/classnames'
export type IAppDetailLayoutProps = {
@ -115,7 +115,7 @@ const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
}, [isMobile, setAppSidebarExpand])
if (!datasetRes && !error)
return <Loading type='app' />
return <Loading type="app" />
return (
<div
@ -128,7 +128,8 @@ const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
indexingTechnique: datasetRes?.indexing_technique,
dataset: datasetRes,
mutateDatasetRes,
}}>
}}
>
{!hideSideBar && (
<AppSideBar
navigation={navigation}
@ -137,10 +138,10 @@ const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
? mode => <ExtraInfo relatedApps={relatedApps} expand={mode === 'expand'} documentCount={datasetRes?.document_count} />
: undefined
}
iconType='dataset'
iconType="dataset"
/>
)}
<div className='grow overflow-hidden bg-background-default-subtle'>{children}</div>
<div className="grow overflow-hidden bg-background-default-subtle">{children}</div>
</DatasetDetailContext.Provider>
</div>
)

View File

@ -3,7 +3,7 @@ import RagPipeline from '@/app/components/rag-pipeline'
const PipelinePage = () => {
return (
<div className='h-full w-full overflow-x-auto'>
<div className="h-full w-full overflow-x-auto">
<RagPipeline />
</div>
)

View File

@ -1,16 +1,16 @@
import React from 'react'
import { getLocaleOnServer, useTranslation as translate } from '@/i18n-config/server'
import Form from '@/app/components/datasets/settings/form'
import { getLocaleOnServer, useTranslation as translate } from '@/i18n-config/server'
const Settings = async () => {
const locale = await getLocaleOnServer()
const { t } = await translate(locale, 'dataset-settings')
return (
<div className='h-full overflow-y-auto'>
<div className='flex flex-col gap-y-0.5 px-6 pb-2 pt-3'>
<div className='system-xl-semibold text-text-primary'>{t('title')}</div>
<div className='system-sm-regular text-text-tertiary'>{t('desc')}</div>
<div className="h-full overflow-y-auto">
<div className="flex flex-col gap-y-0.5 px-6 pb-2 pt-3">
<div className="system-xl-semibold text-text-primary">{t('title')}</div>
<div className="system-sm-regular text-text-tertiary">{t('desc')}</div>
</div>
<Form />
</div>

View File

@ -1,11 +1,11 @@
'use client'
import { useRouter } from 'next/navigation'
import { useEffect } from 'react'
import Loading from '@/app/components/base/loading'
import { useAppContext } from '@/context/app-context'
import { ExternalApiPanelProvider } from '@/context/external-api-panel-context'
import { ExternalKnowledgeApiProvider } from '@/context/external-knowledge-api-context'
import { useRouter } from 'next/navigation'
import { useEffect } from 'react'
export default function DatasetsLayout({ children }: { children: React.ReactNode }) {
const { isCurrentWorkspaceEditor, isCurrentWorkspaceDatasetOperator, currentWorkspace, isLoadingCurrentWorkspace } = useAppContext()
@ -19,7 +19,7 @@ export default function DatasetsLayout({ children }: { children: React.ReactNode
}, [isCurrentWorkspaceEditor, isCurrentWorkspaceDatasetOperator, isLoadingCurrentWorkspace, currentWorkspace, router])
if (isLoadingCurrentWorkspace || !(isCurrentWorkspaceEditor || isCurrentWorkspaceDatasetOperator))
return <Loading type='app' />
return <Loading type="app" />
return (
<ExternalKnowledgeApiProvider>
<ExternalApiPanelProvider>

View File

@ -1,13 +1,13 @@
'use client'
import {
useEffect,
useMemo,
} from 'react'
import {
useRouter,
useSearchParams,
} from 'next/navigation'
import {
useEffect,
useMemo,
} from 'react'
import EducationApplyPage from '@/app/education-apply/education-apply-page'
import { useProviderContext } from '@/context/provider-context'

View File

@ -1,18 +1,18 @@
import React from 'react'
import type { ReactNode } from 'react'
import React from 'react'
import AmplitudeProvider from '@/app/components/base/amplitude'
import GA, { GaType } from '@/app/components/base/ga'
import Zendesk from '@/app/components/base/zendesk'
import GotoAnything from '@/app/components/goto-anything'
import Header from '@/app/components/header'
import HeaderWrapper from '@/app/components/header/header-wrapper'
import ReadmePanel from '@/app/components/plugins/readme-panel'
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'
import { ProviderContextProvider } from '@/context/provider-context'
import { ModalContextProvider } from '@/context/modal-context'
import GotoAnything from '@/app/components/goto-anything'
import Zendesk from '@/app/components/base/zendesk'
import { ProviderContextProvider } from '@/context/provider-context'
import PartnerStack from '../components/billing/partner-stack'
import ReadmePanel from '@/app/components/plugins/readme-panel'
import Splash from '../components/splash'
const Layout = ({ children }: { children: ReactNode }) => {

View File

@ -1,6 +1,6 @@
import Marketplace from '@/app/components/plugins/marketplace'
import PluginPage from '@/app/components/plugins/plugin-page'
import PluginsPanel from '@/app/components/plugins/plugin-page/plugins-panel'
import Marketplace from '@/app/components/plugins/marketplace'
import { getLocaleOnServer } from '@/i18n-config/server'
const PluginList = async () => {
@ -8,7 +8,7 @@ const PluginList = async () => {
return (
<PluginPage
plugins={<PluginsPanel />}
marketplace={<Marketplace locale={locale} pluginTypeSwitchClassName='top-[60px]' showSearchParams={false} />}
marketplace={<Marketplace locale={locale} pluginTypeSwitchClassName="top-[60px]" showSearchParams={false} />}
/>
)
}

View File

@ -6,6 +6,7 @@ import { useTranslation } from 'react-i18next'
import ToolProviderList from '@/app/components/tools/provider-list'
import { useAppContext } from '@/context/app-context'
import useDocumentTitle from '@/hooks/use-document-title'
const ToolsList: FC = () => {
const router = useRouter()
const { isCurrentWorkspaceDatasetOperator } = useAppContext()

View File

@ -1,14 +1,14 @@
'use client'
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
import React, { useCallback, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import AppUnavailable from '@/app/components/base/app-unavailable'
import Loading from '@/app/components/base/loading'
import { useWebAppStore } from '@/context/web-app-context'
import { useGetUserCanAccessApp } from '@/service/access-control'
import { useGetWebAppInfo, useGetWebAppMeta, useGetWebAppParams } from '@/service/use-share'
import { webAppLogout } from '@/service/webapp-auth'
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
import React, { useCallback, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
const AuthenticatedLayout = ({ children }: { children: React.ReactNode }) => {
const { t } = useTranslation()
@ -49,35 +49,47 @@ const AuthenticatedLayout = ({ children }: { children: React.ReactNode }) => {
}, [getSigninUrl, router, webAppLogout, shareCode])
if (appInfoError) {
return <div className='flex h-full items-center justify-center'>
<AppUnavailable unknownReason={appInfoError.message} />
</div>
return (
<div className="flex h-full items-center justify-center">
<AppUnavailable unknownReason={appInfoError.message} />
</div>
)
}
if (appParamsError) {
return <div className='flex h-full items-center justify-center'>
<AppUnavailable unknownReason={appParamsError.message} />
</div>
return (
<div className="flex h-full items-center justify-center">
<AppUnavailable unknownReason={appParamsError.message} />
</div>
)
}
if (appMetaError) {
return <div className='flex h-full items-center justify-center'>
<AppUnavailable unknownReason={appMetaError.message} />
</div>
return (
<div className="flex h-full items-center justify-center">
<AppUnavailable unknownReason={appMetaError.message} />
</div>
)
}
if (useCanAccessAppError) {
return <div className='flex h-full items-center justify-center'>
<AppUnavailable unknownReason={useCanAccessAppError.message} />
</div>
return (
<div className="flex h-full items-center justify-center">
<AppUnavailable unknownReason={useCanAccessAppError.message} />
</div>
)
}
if (userCanAccessApp && !userCanAccessApp.result) {
return <div className='flex h-full flex-col items-center justify-center gap-y-2'>
<AppUnavailable className='h-auto w-auto' code={403} unknownReason='no permission.' />
<span className='system-sm-regular cursor-pointer text-text-tertiary' onClick={backToHome}>{t('common.userProfile.logout')}</span>
</div>
return (
<div className="flex h-full flex-col items-center justify-center gap-y-2">
<AppUnavailable className="h-auto w-auto" code={403} unknownReason="no permission." />
<span className="system-sm-regular cursor-pointer text-text-tertiary" onClick={backToHome}>{t('common.userProfile.logout')}</span>
</div>
)
}
if (isFetchingAppInfo || isFetchingAppParams || isFetchingAppMeta) {
return <div className='flex h-full items-center justify-center'>
<Loading />
</div>
return (
<div className="flex h-full items-center justify-center">
<Loading />
</div>
)
}
return <>{children}</>
}

View File

@ -1,15 +1,13 @@
'use client'
import type { FC, PropsWithChildren } from 'react'
import { useEffect, useState } from 'react'
import { useCallback } from 'react'
import { useWebAppStore } from '@/context/web-app-context'
import { useRouter, useSearchParams } from 'next/navigation'
import AppUnavailable from '@/app/components/base/app-unavailable'
import { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { webAppLoginStatus, webAppLogout } from '@/service/webapp-auth'
import { fetchAccessToken } from '@/service/share'
import AppUnavailable from '@/app/components/base/app-unavailable'
import Loading from '@/app/components/base/loading'
import { setWebAppAccessToken, setWebAppPassport } from '@/service/webapp-auth'
import { useWebAppStore } from '@/context/web-app-context'
import { fetchAccessToken } from '@/service/share'
import { setWebAppAccessToken, setWebAppPassport, webAppLoginStatus, webAppLogout } from '@/service/webapp-auth'
const Splash: FC<PropsWithChildren> = ({ children }) => {
const { t } = useTranslation()
@ -42,7 +40,7 @@ const Splash: FC<PropsWithChildren> = ({ children }) => {
return
}
if(tokenFromUrl)
if (tokenFromUrl)
setWebAppAccessToken(tokenFromUrl)
const redirectOrFinish = () => {
@ -90,19 +88,24 @@ const Splash: FC<PropsWithChildren> = ({ children }) => {
message,
webAppAccessMode,
tokenFromUrl,
embeddedUserId])
embeddedUserId,
])
if (message) {
return <div className='flex h-full flex-col items-center justify-center gap-y-4'>
<AppUnavailable className='h-auto w-auto' code={code || t('share.common.appUnavailable')} unknownReason={message} />
<span className='system-sm-regular cursor-pointer text-text-tertiary' onClick={backToHome}>{code === '403' ? t('common.userProfile.logout') : t('share.login.backToHome')}</span>
</div>
return (
<div className="flex h-full flex-col items-center justify-center gap-y-4">
<AppUnavailable className="h-auto w-auto" code={code || t('share.common.appUnavailable')} unknownReason={message} />
<span className="system-sm-regular cursor-pointer text-text-tertiary" onClick={backToHome}>{code === '403' ? t('common.userProfile.logout') : t('share.login.backToHome')}</span>
</div>
)
}
if (isLoading) {
return <div className='flex h-full items-center justify-center'>
<Loading />
</div>
return (
<div className="flex h-full items-center justify-center">
<Loading />
</div>
)
}
return <>{children}</>
}

View File

@ -1,15 +1,15 @@
'use client'
import { RiArrowLeftLine, RiMailSendFill } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { useState } from 'react'
import { useRouter, useSearchParams } from 'next/navigation'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import Countdown from '@/app/components/signin/countdown'
import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import Toast from '@/app/components/base/toast'
import { sendWebAppResetPasswordCode, verifyWebAppResetPasswordCode } from '@/service/common'
import Countdown from '@/app/components/signin/countdown'
import I18NContext from '@/context/i18n'
import { sendWebAppResetPasswordCode, verifyWebAppResetPasswordCode } from '@/service/common'
export default function CheckCode() {
const { t } = useTranslation()
@ -63,37 +63,39 @@ export default function CheckCode() {
catch (error) { console.error(error) }
}
return <div className='flex flex-col gap-3'>
<div className='inline-flex h-14 w-14 items-center justify-center rounded-2xl border border-components-panel-border-subtle bg-background-default-dodge text-text-accent-light-mode-only shadow-lg'>
<RiMailSendFill className='h-6 w-6 text-2xl' />
</div>
<div className='pb-4 pt-2'>
<h2 className='title-4xl-semi-bold text-text-primary'>{t('login.checkCode.checkYourEmail')}</h2>
<p className='body-md-regular mt-2 text-text-secondary'>
<span>
{t('login.checkCode.tipsPrefix')}
<strong>{email}</strong>
</span>
<br />
{t('login.checkCode.validTime')}
</p>
</div>
<form action="">
<input type='text' className='hidden' />
<label htmlFor="code" className='system-md-semibold mb-1 text-text-secondary'>{t('login.checkCode.verificationCode')}</label>
<Input value={code} onChange={e => setVerifyCode(e.target.value)} maxLength={6} className='mt-1' placeholder={t('login.checkCode.verificationCodePlaceholder') || ''} />
<Button loading={loading} disabled={loading} className='my-3 w-full' variant='primary' onClick={verify}>{t('login.checkCode.verify')}</Button>
<Countdown onResend={resendCode} />
</form>
<div className='py-2'>
<div className='h-px bg-gradient-to-r from-background-gradient-mask-transparent via-divider-regular to-background-gradient-mask-transparent'></div>
</div>
<div onClick={() => router.back()} className='flex h-9 cursor-pointer items-center justify-center text-text-tertiary'>
<div className='bg-background-default-dimm inline-block rounded-full p-1'>
<RiArrowLeftLine size={12} />
return (
<div className="flex flex-col gap-3">
<div className="inline-flex h-14 w-14 items-center justify-center rounded-2xl border border-components-panel-border-subtle bg-background-default-dodge text-text-accent-light-mode-only shadow-lg">
<RiMailSendFill className="h-6 w-6 text-2xl" />
</div>
<div className="pb-4 pt-2">
<h2 className="title-4xl-semi-bold text-text-primary">{t('login.checkCode.checkYourEmail')}</h2>
<p className="body-md-regular mt-2 text-text-secondary">
<span>
{t('login.checkCode.tipsPrefix')}
<strong>{email}</strong>
</span>
<br />
{t('login.checkCode.validTime')}
</p>
</div>
<form action="">
<input type="text" className="hidden" />
<label htmlFor="code" className="system-md-semibold mb-1 text-text-secondary">{t('login.checkCode.verificationCode')}</label>
<Input value={code} onChange={e => setVerifyCode(e.target.value)} maxLength={6} className="mt-1" placeholder={t('login.checkCode.verificationCodePlaceholder') || ''} />
<Button loading={loading} disabled={loading} className="my-3 w-full" variant="primary" onClick={verify}>{t('login.checkCode.verify')}</Button>
<Countdown onResend={resendCode} />
</form>
<div className="py-2">
<div className="h-px bg-gradient-to-r from-background-gradient-mask-transparent via-divider-regular to-background-gradient-mask-transparent"></div>
</div>
<div onClick={() => router.back()} className="flex h-9 cursor-pointer items-center justify-center text-text-tertiary">
<div className="bg-background-default-dimm inline-block rounded-full p-1">
<RiArrowLeftLine size={12} />
</div>
<span className="system-xs-regular ml-2">{t('login.back')}</span>
</div>
<span className='system-xs-regular ml-2'>{t('login.back')}</span>
</div>
</div>
)
}

View File

@ -1,30 +1,39 @@
'use client'
import Header from '@/app/signin/_header'
import { cn } from '@/utils/classnames'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { cn } from '@/utils/classnames'
export default function SignInLayout({ children }: any) {
const { systemFeatures } = useGlobalPublicStore()
return <>
<div className={cn('flex min-h-screen w-full justify-center bg-background-default-burn p-6')}>
<div className={cn('flex w-full shrink-0 flex-col rounded-2xl border border-effects-highlight bg-background-default-subtle')}>
<Header />
<div className={
cn(
'flex w-full grow flex-col items-center justify-center',
'px-6',
'md:px-[108px]',
)
}>
<div className='flex w-[400px] flex-col'>
{children}
return (
<>
<div className={cn('flex min-h-screen w-full justify-center bg-background-default-burn p-6')}>
<div className={cn('flex w-full shrink-0 flex-col rounded-2xl border border-effects-highlight bg-background-default-subtle')}>
<Header />
<div className={
cn(
'flex w-full grow flex-col items-center justify-center',
'px-6',
'md:px-[108px]',
)
}
>
<div className="flex w-[400px] flex-col">
{children}
</div>
</div>
{!systemFeatures.branding.enabled && (
<div className="system-xs-regular px-8 py-6 text-text-tertiary">
©
{' '}
{new Date().getFullYear()}
{' '}
LangGenius, Inc. All rights reserved.
</div>
)}
</div>
{!systemFeatures.branding.enabled && <div className='system-xs-regular px-8 py-6 text-text-tertiary'>
© {new Date().getFullYear()} LangGenius, Inc. All rights reserved.
</div>}
</div>
</div>
</>
</>
)
}

View File

@ -1,19 +1,19 @@
'use client'
import Link from 'next/link'
import { RiArrowLeftLine, RiLockPasswordLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { useState } from 'react'
import { noop } from 'lodash-es'
import Link from 'next/link'
import { useRouter, useSearchParams } from 'next/navigation'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import { COUNT_DOWN_KEY, COUNT_DOWN_TIME_MS } from '@/app/components/signin/countdown'
import { emailRegex } from '@/config'
import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import Toast from '@/app/components/base/toast'
import { sendResetPasswordCode } from '@/service/common'
import { COUNT_DOWN_KEY, COUNT_DOWN_TIME_MS } from '@/app/components/signin/countdown'
import { emailRegex } from '@/config'
import I18NContext from '@/context/i18n'
import { noop } from 'lodash-es'
import useDocumentTitle from '@/hooks/use-document-title'
import { sendResetPasswordCode } from '@/service/common'
export default function CheckCode() {
const { t } = useTranslation()
@ -68,37 +68,39 @@ export default function CheckCode() {
}
}
return <div className='flex flex-col gap-3'>
<div className='inline-flex h-14 w-14 items-center justify-center rounded-2xl border border-components-panel-border-subtle bg-background-default-dodge shadow-lg'>
<RiLockPasswordLine className='h-6 w-6 text-2xl text-text-accent-light-mode-only' />
</div>
<div className='pb-4 pt-2'>
<h2 className='title-4xl-semi-bold text-text-primary'>{t('login.resetPassword')}</h2>
<p className='body-md-regular mt-2 text-text-secondary'>
{t('login.resetPasswordDesc')}
</p>
</div>
return (
<div className="flex flex-col gap-3">
<div className="inline-flex h-14 w-14 items-center justify-center rounded-2xl border border-components-panel-border-subtle bg-background-default-dodge shadow-lg">
<RiLockPasswordLine className="h-6 w-6 text-2xl text-text-accent-light-mode-only" />
</div>
<div className="pb-4 pt-2">
<h2 className="title-4xl-semi-bold text-text-primary">{t('login.resetPassword')}</h2>
<p className="body-md-regular mt-2 text-text-secondary">
{t('login.resetPasswordDesc')}
</p>
</div>
<form onSubmit={noop}>
<input type='text' className='hidden' />
<div className='mb-2'>
<label htmlFor="email" className='system-md-semibold my-2 text-text-secondary'>{t('login.email')}</label>
<div className='mt-1'>
<Input id='email' type="email" disabled={loading} value={email} placeholder={t('login.emailPlaceholder') as string} onChange={e => setEmail(e.target.value)} />
</div>
<div className='mt-3'>
<Button loading={loading} disabled={loading} variant='primary' className='w-full' onClick={handleGetEMailVerificationCode}>{t('login.sendVerificationCode')}</Button>
<form onSubmit={noop}>
<input type="text" className="hidden" />
<div className="mb-2">
<label htmlFor="email" className="system-md-semibold my-2 text-text-secondary">{t('login.email')}</label>
<div className="mt-1">
<Input id="email" type="email" disabled={loading} value={email} placeholder={t('login.emailPlaceholder') as string} onChange={e => setEmail(e.target.value)} />
</div>
<div className="mt-3">
<Button loading={loading} disabled={loading} variant="primary" className="w-full" onClick={handleGetEMailVerificationCode}>{t('login.sendVerificationCode')}</Button>
</div>
</div>
</form>
<div className="py-2">
<div className="h-px bg-gradient-to-r from-background-gradient-mask-transparent via-divider-regular to-background-gradient-mask-transparent"></div>
</div>
</form>
<div className='py-2'>
<div className='h-px bg-gradient-to-r from-background-gradient-mask-transparent via-divider-regular to-background-gradient-mask-transparent'></div>
<Link href={`/webapp-signin?${searchParams.toString()}`} className="flex h-9 items-center justify-center text-text-tertiary hover:text-text-primary">
<div className="inline-block rounded-full bg-background-default-dimmed p-1">
<RiArrowLeftLine size={12} />
</div>
<span className="system-xs-regular ml-2">{t('login.backToLogin')}</span>
</Link>
</div>
<Link href={`/webapp-signin?${searchParams.toString()}`} className='flex h-9 items-center justify-center text-text-tertiary hover:text-text-primary'>
<div className='inline-block rounded-full bg-background-default-dimmed p-1'>
<RiArrowLeftLine size={12} />
</div>
<span className='system-xs-regular ml-2'>{t('login.backToLogin')}</span>
</Link>
</div>
)
}

View File

@ -1,15 +1,15 @@
'use client'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useRouter, useSearchParams } from 'next/navigation'
import { cn } from '@/utils/classnames'
import { RiCheckboxCircleFill } from '@remixicon/react'
import { useCountDown } from 'ahooks'
import { useRouter, useSearchParams } from 'next/navigation'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import { changeWebAppPasswordWithToken } from '@/service/common'
import Toast from '@/app/components/base/toast'
import Input from '@/app/components/base/input'
import Toast from '@/app/components/base/toast'
import { validPassword } from '@/config'
import { changeWebAppPasswordWithToken } from '@/service/common'
import { cn } from '@/utils/classnames'
const ChangePasswordForm = () => {
const { t } = useTranslation()
@ -86,14 +86,15 @@ const ChangePasswordForm = () => {
'px-6',
'md:px-[108px]',
)
}>
}
>
{!showSuccess && (
<div className='flex flex-col md:w-[400px]'>
<div className="flex flex-col md:w-[400px]">
<div className="mx-auto w-full">
<h2 className="title-4xl-semi-bold text-text-primary">
{t('login.changePassword')}
</h2>
<p className='body-md-regular mt-2 text-text-secondary'>
<p className="body-md-regular mt-2 text-text-secondary">
{t('login.changePasswordTip')}
</p>
</div>
@ -101,13 +102,14 @@ const ChangePasswordForm = () => {
<div className="mx-auto mt-6 w-full">
<div className="bg-white">
{/* Password */}
<div className='mb-5'>
<div className="mb-5">
<label htmlFor="password" className="system-md-semibold my-2 text-text-secondary">
{t('common.account.newPassword')}
</label>
<div className='relative mt-1'>
<div className="relative mt-1">
<Input
id="password" type={showPassword ? 'text' : 'password'}
id="password"
type={showPassword ? 'text' : 'password'}
value={password}
onChange={e => setPassword(e.target.value)}
placeholder={t('login.passwordPlaceholder') || ''}
@ -116,21 +118,21 @@ const ChangePasswordForm = () => {
<div className="absolute inset-y-0 right-0 flex items-center">
<Button
type="button"
variant='ghost'
variant="ghost"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? '👀' : '😝'}
</Button>
</div>
</div>
<div className='body-xs-regular mt-1 text-text-secondary'>{t('login.error.passwordInvalid')}</div>
<div className="body-xs-regular mt-1 text-text-secondary">{t('login.error.passwordInvalid')}</div>
</div>
{/* Confirm Password */}
<div className='mb-5'>
<div className="mb-5">
<label htmlFor="confirmPassword" className="system-md-semibold my-2 text-text-secondary">
{t('common.account.confirmPassword')}
</label>
<div className='relative mt-1'>
<div className="relative mt-1">
<Input
id="confirmPassword"
type={showConfirmPassword ? 'text' : 'password'}
@ -141,7 +143,7 @@ const ChangePasswordForm = () => {
<div className="absolute inset-y-0 right-0 flex items-center">
<Button
type="button"
variant='ghost'
variant="ghost"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
>
{showConfirmPassword ? '👀' : '😝'}
@ -151,8 +153,8 @@ const ChangePasswordForm = () => {
</div>
<div>
<Button
variant='primary'
className='w-full'
variant="primary"
className="w-full"
onClick={handleChangePassword}
>
{t('login.changePasswordBtn')}
@ -166,17 +168,28 @@ const ChangePasswordForm = () => {
<div className="flex flex-col md:w-[400px]">
<div className="mx-auto w-full">
<div className="mb-3 flex h-14 w-14 items-center justify-center rounded-2xl border border-components-panel-border-subtle font-bold shadow-lg">
<RiCheckboxCircleFill className='h-6 w-6 text-text-success' />
<RiCheckboxCircleFill className="h-6 w-6 text-text-success" />
</div>
<h2 className="title-4xl-semi-bold text-text-primary">
{t('login.passwordChangedTip')}
</h2>
</div>
<div className="mx-auto mt-6 w-full">
<Button variant='primary' className='w-full' onClick={() => {
setLeftTime(undefined)
router.replace(getSignInUrl())
}}>{t('login.passwordChanged')} ({Math.round(countdown / 1000)}) </Button>
<Button
variant="primary"
className="w-full"
onClick={() => {
setLeftTime(undefined)
router.replace(getSignInUrl())
}}
>
{t('login.passwordChanged')}
{' '}
(
{Math.round(countdown / 1000)}
)
{' '}
</Button>
</div>
</div>
)}

View File

@ -1,18 +1,19 @@
'use client'
import type { FormEvent } from 'react'
import { RiArrowLeftLine, RiMailSendFill } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { type FormEvent, useCallback, useEffect, useRef, useState } from 'react'
import { useRouter, useSearchParams } from 'next/navigation'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import Countdown from '@/app/components/signin/countdown'
import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import Toast from '@/app/components/base/toast'
import { sendWebAppEMailLoginCode, webAppEmailLoginWithCode } from '@/service/common'
import Countdown from '@/app/components/signin/countdown'
import I18NContext from '@/context/i18n'
import { setWebAppAccessToken, setWebAppPassport } from '@/service/webapp-auth'
import { fetchAccessToken } from '@/service/share'
import { useWebAppStore } from '@/context/web-app-context'
import { sendWebAppEMailLoginCode, webAppEmailLoginWithCode } from '@/service/common'
import { fetchAccessToken } from '@/service/share'
import { setWebAppAccessToken, setWebAppPassport } from '@/service/webapp-auth'
export default function CheckCode() {
const { t } = useTranslation()
@ -101,44 +102,46 @@ export default function CheckCode() {
catch (error) { console.error(error) }
}
return <div className='flex w-[400px] flex-col gap-3'>
<div className='inline-flex h-14 w-14 items-center justify-center rounded-2xl border border-components-panel-border-subtle bg-background-default-dodge shadow-lg'>
<RiMailSendFill className='h-6 w-6 text-2xl text-text-accent-light-mode-only' />
</div>
<div className='pb-4 pt-2'>
<h2 className='title-4xl-semi-bold text-text-primary'>{t('login.checkCode.checkYourEmail')}</h2>
<p className='body-md-regular mt-2 text-text-secondary'>
<span>
{t('login.checkCode.tipsPrefix')}
<strong>{email}</strong>
</span>
<br />
{t('login.checkCode.validTime')}
</p>
</div>
<form onSubmit={handleSubmit}>
<label htmlFor="code" className='system-md-semibold mb-1 text-text-secondary'>{t('login.checkCode.verificationCode')}</label>
<Input
ref={codeInputRef}
id='code'
value={code}
onChange={e => setVerifyCode(e.target.value)}
maxLength={6}
className='mt-1'
placeholder={t('login.checkCode.verificationCodePlaceholder') || ''}
/>
<Button type='submit' loading={loading} disabled={loading} className='my-3 w-full' variant='primary'>{t('login.checkCode.verify')}</Button>
<Countdown onResend={resendCode} />
</form>
<div className='py-2'>
<div className='h-px bg-gradient-to-r from-background-gradient-mask-transparent via-divider-regular to-background-gradient-mask-transparent'></div>
</div>
<div onClick={() => router.back()} className='flex h-9 cursor-pointer items-center justify-center text-text-tertiary'>
<div className='bg-background-default-dimm inline-block rounded-full p-1'>
<RiArrowLeftLine size={12} />
return (
<div className="flex w-[400px] flex-col gap-3">
<div className="inline-flex h-14 w-14 items-center justify-center rounded-2xl border border-components-panel-border-subtle bg-background-default-dodge shadow-lg">
<RiMailSendFill className="h-6 w-6 text-2xl text-text-accent-light-mode-only" />
</div>
<div className="pb-4 pt-2">
<h2 className="title-4xl-semi-bold text-text-primary">{t('login.checkCode.checkYourEmail')}</h2>
<p className="body-md-regular mt-2 text-text-secondary">
<span>
{t('login.checkCode.tipsPrefix')}
<strong>{email}</strong>
</span>
<br />
{t('login.checkCode.validTime')}
</p>
</div>
<form onSubmit={handleSubmit}>
<label htmlFor="code" className="system-md-semibold mb-1 text-text-secondary">{t('login.checkCode.verificationCode')}</label>
<Input
ref={codeInputRef}
id="code"
value={code}
onChange={e => setVerifyCode(e.target.value)}
maxLength={6}
className="mt-1"
placeholder={t('login.checkCode.verificationCodePlaceholder') || ''}
/>
<Button type="submit" loading={loading} disabled={loading} className="my-3 w-full" variant="primary">{t('login.checkCode.verify')}</Button>
<Countdown onResend={resendCode} />
</form>
<div className="py-2">
<div className="h-px bg-gradient-to-r from-background-gradient-mask-transparent via-divider-regular to-background-gradient-mask-transparent"></div>
</div>
<div onClick={() => router.back()} className="flex h-9 cursor-pointer items-center justify-center text-text-tertiary">
<div className="bg-background-default-dimm inline-block rounded-full p-1">
<RiArrowLeftLine size={12} />
</div>
<span className="system-xs-regular ml-2">{t('login.back')}</span>
</div>
<span className='system-xs-regular ml-2'>{t('login.back')}</span>
</div>
</div>
)
}

View File

@ -1,12 +1,12 @@
'use client'
import { useRouter, useSearchParams } from 'next/navigation'
import React, { useCallback, useEffect } from 'react'
import Toast from '@/app/components/base/toast'
import { fetchWebOAuth2SSOUrl, fetchWebOIDCSSOUrl, fetchWebSAMLSSOUrl } from '@/service/share'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { SSOProtocol } from '@/types/feature'
import Loading from '@/app/components/base/loading'
import AppUnavailable from '@/app/components/base/app-unavailable'
import Loading from '@/app/components/base/loading'
import Toast from '@/app/components/base/toast'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { fetchWebOAuth2SSOUrl, fetchWebOIDCSSOUrl, fetchWebSAMLSSOUrl } from '@/service/share'
import { SSOProtocol } from '@/types/feature'
const ExternalMemberSSOAuth = () => {
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
@ -68,9 +68,11 @@ const ExternalMemberSSOAuth = () => {
}, [handleSSOLogin])
if (!systemFeatures.webapp_auth.sso_config.protocol) {
return <div className="flex h-full items-center justify-center">
<AppUnavailable code={403} unknownReason='sso protocol is invalid.' />
</div>
return (
<div className="flex h-full items-center justify-center">
<AppUnavailable code={403} unknownReason="sso protocol is invalid." />
</div>
)
}
return (

View File

@ -1,15 +1,15 @@
import { noop } from 'lodash-es'
import { useRouter, useSearchParams } from 'next/navigation'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useRouter, useSearchParams } from 'next/navigation'
import { useContext } from 'use-context-selector'
import Input from '@/app/components/base/input'
import Button from '@/app/components/base/button'
import { emailRegex } from '@/config'
import Input from '@/app/components/base/input'
import Toast from '@/app/components/base/toast'
import { sendWebAppEMailLoginCode } from '@/service/common'
import { COUNT_DOWN_KEY, COUNT_DOWN_TIME_MS } from '@/app/components/signin/countdown'
import { emailRegex } from '@/config'
import I18NContext from '@/context/i18n'
import { noop } from 'lodash-es'
import { sendWebAppEMailLoginCode } from '@/service/common'
export default function MailAndCodeAuth() {
const { t } = useTranslation()
@ -52,17 +52,18 @@ export default function MailAndCodeAuth() {
}
}
return (<form onSubmit={noop}>
<input type='text' className='hidden' />
<div className='mb-2'>
<label htmlFor="email" className='system-md-semibold my-2 text-text-secondary'>{t('login.email')}</label>
<div className='mt-1'>
<Input id='email' type="email" value={email} placeholder={t('login.emailPlaceholder') as string} onChange={e => setEmail(e.target.value)} />
return (
<form onSubmit={noop}>
<input type="text" className="hidden" />
<div className="mb-2">
<label htmlFor="email" className="system-md-semibold my-2 text-text-secondary">{t('login.email')}</label>
<div className="mt-1">
<Input id="email" type="email" value={email} placeholder={t('login.emailPlaceholder') as string} onChange={e => setEmail(e.target.value)} />
</div>
<div className="mt-3">
<Button loading={loading} disabled={loading || !email} variant="primary" className="w-full" onClick={handleGetEMailVerificationCode}>{t('login.signup.verifyMail')}</Button>
</div>
</div>
<div className='mt-3'>
<Button loading={loading} disabled={loading || !email} variant='primary' className='w-full' onClick={handleGetEMailVerificationCode}>{t('login.signup.verifyMail')}</Button>
</div>
</div>
</form>
</form>
)
}

View File

@ -1,17 +1,17 @@
'use client'
import { noop } from 'lodash-es'
import Link from 'next/link'
import { useRouter, useSearchParams } from 'next/navigation'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useRouter, useSearchParams } from 'next/navigation'
import { useContext } from 'use-context-selector'
import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import Toast from '@/app/components/base/toast'
import { emailRegex } from '@/config'
import { webAppLogin } from '@/service/common'
import Input from '@/app/components/base/input'
import I18NContext from '@/context/i18n'
import { useWebAppStore } from '@/context/web-app-context'
import { noop } from 'lodash-es'
import { webAppLogin } from '@/service/common'
import { fetchAccessToken } from '@/service/share'
import { setWebAppAccessToken, setWebAppPassport } from '@/service/webapp-auth'
@ -107,70 +107,74 @@ export default function MailAndPasswordAuth({ isEmailSetup }: MailAndPasswordAut
}
}
return <form onSubmit={noop}>
<div className='mb-3'>
<label htmlFor="email" className="system-md-semibold my-2 text-text-secondary">
{t('login.email')}
</label>
<div className="mt-1">
<Input
value={email}
onChange={e => setEmail(e.target.value)}
id="email"
type="email"
autoComplete="email"
placeholder={t('login.emailPlaceholder') || ''}
tabIndex={1}
/>
</div>
</div>
<div className='mb-3'>
<label htmlFor="password" className="my-2 flex items-center justify-between">
<span className='system-md-semibold text-text-secondary'>{t('login.password')}</span>
<Link
href={`/webapp-reset-password?${searchParams.toString()}`}
className={`system-xs-regular ${isEmailSetup ? 'text-components-button-secondary-accent-text' : 'pointer-events-none text-components-button-secondary-accent-text-disabled'}`}
tabIndex={isEmailSetup ? 0 : -1}
aria-disabled={!isEmailSetup}
>
{t('login.forget')}
</Link>
</label>
<div className="relative mt-1">
<Input
value={password}
onChange={e => setPassword(e.target.value)}
id="password"
onKeyDown={(e) => {
if (e.key === 'Enter')
handleEmailPasswordLogin()
}}
type={showPassword ? 'text' : 'password'}
autoComplete="current-password"
placeholder={t('login.passwordPlaceholder') || ''}
tabIndex={2}
/>
<div className="absolute inset-y-0 right-0 flex items-center">
<Button
type="button"
variant='ghost'
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? '👀' : '😝'}
</Button>
return (
<form onSubmit={noop}>
<div className="mb-3">
<label htmlFor="email" className="system-md-semibold my-2 text-text-secondary">
{t('login.email')}
</label>
<div className="mt-1">
<Input
value={email}
onChange={e => setEmail(e.target.value)}
id="email"
type="email"
autoComplete="email"
placeholder={t('login.emailPlaceholder') || ''}
tabIndex={1}
/>
</div>
</div>
</div>
<div className='mb-2'>
<Button
tabIndex={2}
variant='primary'
onClick={handleEmailPasswordLogin}
disabled={isLoading || !email || !password}
className="w-full"
>{t('login.signBtn')}</Button>
</div>
</form>
<div className="mb-3">
<label htmlFor="password" className="my-2 flex items-center justify-between">
<span className="system-md-semibold text-text-secondary">{t('login.password')}</span>
<Link
href={`/webapp-reset-password?${searchParams.toString()}`}
className={`system-xs-regular ${isEmailSetup ? 'text-components-button-secondary-accent-text' : 'pointer-events-none text-components-button-secondary-accent-text-disabled'}`}
tabIndex={isEmailSetup ? 0 : -1}
aria-disabled={!isEmailSetup}
>
{t('login.forget')}
</Link>
</label>
<div className="relative mt-1">
<Input
value={password}
onChange={e => setPassword(e.target.value)}
id="password"
onKeyDown={(e) => {
if (e.key === 'Enter')
handleEmailPasswordLogin()
}}
type={showPassword ? 'text' : 'password'}
autoComplete="current-password"
placeholder={t('login.passwordPlaceholder') || ''}
tabIndex={2}
/>
<div className="absolute inset-y-0 right-0 flex items-center">
<Button
type="button"
variant="ghost"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? '👀' : '😝'}
</Button>
</div>
</div>
</div>
<div className="mb-2">
<Button
tabIndex={2}
variant="primary"
onClick={handleEmailPasswordLogin}
disabled={isLoading || !email || !password}
className="w-full"
>
{t('login.signBtn')}
</Button>
</div>
</form>
)
}

View File

@ -1,14 +1,13 @@
'use client'
import { useRouter, useSearchParams } from 'next/navigation'
import type { FC } from 'react'
import { useCallback } from 'react'
import { useState } from 'react'
import { useRouter, useSearchParams } from 'next/navigation'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import { Lock01 } from '@/app/components/base/icons/src/vender/solid/security'
import Toast from '@/app/components/base/toast'
import Button from '@/app/components/base/button'
import { SSOProtocol } from '@/types/feature'
import { fetchMembersOAuth2SSOUrl, fetchMembersOIDCSSOUrl, fetchMembersSAMLSSOUrl } from '@/service/share'
import { SSOProtocol } from '@/types/feature'
type SSOAuthProps = {
protocol: SSOProtocol | ''
@ -82,7 +81,7 @@ const SSOAuth: FC<SSOAuthProps> = ({
disabled={isLoading}
className="w-full"
>
<Lock01 className='mr-2 h-5 w-5 text-text-accent-light-mode-only' />
<Lock01 className="mr-2 h-5 w-5 text-text-accent-light-mode-only" />
<span className="truncate">{t('login.withSSO')}</span>
</Button>
)

View File

@ -1,28 +1,36 @@
'use client'
import { cn } from '@/utils/classnames'
import { useGlobalPublicStore } from '@/context/global-public-context'
import useDocumentTitle from '@/hooks/use-document-title'
import type { PropsWithChildren } from 'react'
import { useTranslation } from 'react-i18next'
import { useGlobalPublicStore } from '@/context/global-public-context'
import useDocumentTitle from '@/hooks/use-document-title'
import { cn } from '@/utils/classnames'
export default function SignInLayout({ children }: PropsWithChildren) {
const { t } = useTranslation()
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
useDocumentTitle(t('login.webapp.login'))
return <>
<div className={cn('flex min-h-screen w-full justify-center bg-background-default-burn p-6')}>
<div className={cn('flex w-full shrink-0 flex-col rounded-2xl border border-effects-highlight bg-background-default-subtle')}>
{/* <Header /> */}
<div className={cn('flex w-full grow flex-col items-center justify-center px-6 md:px-[108px]')}>
<div className='flex justify-center md:w-[440px] lg:w-[600px]'>
{children}
return (
<>
<div className={cn('flex min-h-screen w-full justify-center bg-background-default-burn p-6')}>
<div className={cn('flex w-full shrink-0 flex-col rounded-2xl border border-effects-highlight bg-background-default-subtle')}>
{/* <Header /> */}
<div className={cn('flex w-full grow flex-col items-center justify-center px-6 md:px-[108px]')}>
<div className="flex justify-center md:w-[440px] lg:w-[600px]">
{children}
</div>
</div>
{systemFeatures.branding.enabled === false && (
<div className="system-xs-regular px-8 py-6 text-text-tertiary">
©
{' '}
{new Date().getFullYear()}
{' '}
LangGenius, Inc. All rights reserved.
</div>
)}
</div>
{systemFeatures.branding.enabled === false && <div className='system-xs-regular px-8 py-6 text-text-tertiary'>
© {new Date().getFullYear()} LangGenius, Inc. All rights reserved.
</div>}
</div>
</div>
</>
</>
)
}

View File

@ -1,16 +1,16 @@
'use client'
import { RiContractLine, RiDoorLockLine, RiErrorWarningFill } from '@remixicon/react'
import Link from 'next/link'
import React, { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Link from 'next/link'
import { RiContractLine, RiDoorLockLine, RiErrorWarningFill } from '@remixicon/react'
import Loading from '@/app/components/base/loading'
import { IS_CE_EDITION } from '@/config'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { LicenseStatus } from '@/types/feature'
import { cn } from '@/utils/classnames'
import MailAndCodeAuth from './components/mail-and-code-auth'
import MailAndPasswordAuth from './components/mail-and-password-auth'
import SSOAuth from './components/sso-auth'
import { cn } from '@/utils/classnames'
import { LicenseStatus } from '@/types/feature'
import { IS_CE_EDITION } from '@/config'
import { useGlobalPublicStore } from '@/context/global-public-context'
const NormalForm = () => {
const { t } = useTranslation()
@ -37,57 +37,66 @@ const NormalForm = () => {
init()
}, [init])
if (isLoading) {
return <div className={
cn(
'flex w-full grow flex-col items-center justify-center',
'px-6',
'md:px-[108px]',
)
}>
<Loading type='area' />
</div>
return (
<div className={
cn(
'flex w-full grow flex-col items-center justify-center',
'px-6',
'md:px-[108px]',
)
}
>
<Loading type="area" />
</div>
)
}
if (systemFeatures.license?.status === LicenseStatus.LOST) {
return <div className='mx-auto mt-8 w-full'>
<div className='relative'>
<div className="rounded-lg bg-gradient-to-r from-workflow-workflow-progress-bg-1 to-workflow-workflow-progress-bg-2 p-4">
<div className='shadows-shadow-lg relative mb-2 flex h-10 w-10 items-center justify-center rounded-xl bg-components-card-bg shadow'>
<RiContractLine className='h-5 w-5' />
<RiErrorWarningFill className='absolute -right-1 -top-1 h-4 w-4 text-text-warning-secondary' />
return (
<div className="mx-auto mt-8 w-full">
<div className="relative">
<div className="rounded-lg bg-gradient-to-r from-workflow-workflow-progress-bg-1 to-workflow-workflow-progress-bg-2 p-4">
<div className="shadows-shadow-lg relative mb-2 flex h-10 w-10 items-center justify-center rounded-xl bg-components-card-bg shadow">
<RiContractLine className="h-5 w-5" />
<RiErrorWarningFill className="absolute -right-1 -top-1 h-4 w-4 text-text-warning-secondary" />
</div>
<p className="system-sm-medium text-text-primary">{t('login.licenseLost')}</p>
<p className="system-xs-regular mt-1 text-text-tertiary">{t('login.licenseLostTip')}</p>
</div>
<p className='system-sm-medium text-text-primary'>{t('login.licenseLost')}</p>
<p className='system-xs-regular mt-1 text-text-tertiary'>{t('login.licenseLostTip')}</p>
</div>
</div>
</div>
)
}
if (systemFeatures.license?.status === LicenseStatus.EXPIRED) {
return <div className='mx-auto mt-8 w-full'>
<div className='relative'>
<div className="rounded-lg bg-gradient-to-r from-workflow-workflow-progress-bg-1 to-workflow-workflow-progress-bg-2 p-4">
<div className='shadows-shadow-lg relative mb-2 flex h-10 w-10 items-center justify-center rounded-xl bg-components-card-bg shadow'>
<RiContractLine className='h-5 w-5' />
<RiErrorWarningFill className='absolute -right-1 -top-1 h-4 w-4 text-text-warning-secondary' />
return (
<div className="mx-auto mt-8 w-full">
<div className="relative">
<div className="rounded-lg bg-gradient-to-r from-workflow-workflow-progress-bg-1 to-workflow-workflow-progress-bg-2 p-4">
<div className="shadows-shadow-lg relative mb-2 flex h-10 w-10 items-center justify-center rounded-xl bg-components-card-bg shadow">
<RiContractLine className="h-5 w-5" />
<RiErrorWarningFill className="absolute -right-1 -top-1 h-4 w-4 text-text-warning-secondary" />
</div>
<p className="system-sm-medium text-text-primary">{t('login.licenseExpired')}</p>
<p className="system-xs-regular mt-1 text-text-tertiary">{t('login.licenseExpiredTip')}</p>
</div>
<p className='system-sm-medium text-text-primary'>{t('login.licenseExpired')}</p>
<p className='system-xs-regular mt-1 text-text-tertiary'>{t('login.licenseExpiredTip')}</p>
</div>
</div>
</div>
)
}
if (systemFeatures.license?.status === LicenseStatus.INACTIVE) {
return <div className='mx-auto mt-8 w-full'>
<div className='relative'>
<div className="rounded-lg bg-gradient-to-r from-workflow-workflow-progress-bg-1 to-workflow-workflow-progress-bg-2 p-4">
<div className='shadows-shadow-lg relative mb-2 flex h-10 w-10 items-center justify-center rounded-xl bg-components-card-bg shadow'>
<RiContractLine className='h-5 w-5' />
<RiErrorWarningFill className='absolute -right-1 -top-1 h-4 w-4 text-text-warning-secondary' />
return (
<div className="mx-auto mt-8 w-full">
<div className="relative">
<div className="rounded-lg bg-gradient-to-r from-workflow-workflow-progress-bg-1 to-workflow-workflow-progress-bg-2 p-4">
<div className="shadows-shadow-lg relative mb-2 flex h-10 w-10 items-center justify-center rounded-xl bg-components-card-bg shadow">
<RiContractLine className="h-5 w-5" />
<RiErrorWarningFill className="absolute -right-1 -top-1 h-4 w-4 text-text-warning-secondary" />
</div>
<p className="system-sm-medium text-text-primary">{t('login.licenseInactive')}</p>
<p className="system-xs-regular mt-1 text-text-tertiary">{t('login.licenseInactiveTip')}</p>
</div>
<p className='system-sm-medium text-text-primary'>{t('login.licenseInactive')}</p>
<p className='system-xs-regular mt-1 text-text-tertiary'>{t('login.licenseInactiveTip')}</p>
</div>
</div>
</div>
)
}
return (
@ -95,78 +104,106 @@ const NormalForm = () => {
<div className="mx-auto mt-8 w-full">
<div className="mx-auto w-full">
<h2 className="title-4xl-semi-bold text-text-primary">{systemFeatures.branding.enabled ? t('login.pageTitleForE') : t('login.pageTitle')}</h2>
<p className='body-md-regular mt-2 text-text-tertiary'>{t('login.welcome')}</p>
<p className="body-md-regular mt-2 text-text-tertiary">{t('login.welcome')}</p>
</div>
<div className="relative">
<div className="mt-6 flex flex-col gap-3">
{systemFeatures.sso_enforced_for_signin && <div className='w-full'>
<SSOAuth protocol={systemFeatures.sso_enforced_for_signin_protocol} />
</div>}
{systemFeatures.sso_enforced_for_signin && (
<div className="w-full">
<SSOAuth protocol={systemFeatures.sso_enforced_for_signin_protocol} />
</div>
)}
</div>
{showORLine && <div className="relative mt-6">
<div className="absolute inset-0 flex items-center" aria-hidden="true">
<div className='h-px w-full bg-gradient-to-r from-background-gradient-mask-transparent via-divider-regular to-background-gradient-mask-transparent'></div>
</div>
<div className="relative flex justify-center">
<span className="system-xs-medium-uppercase px-2 text-text-tertiary">{t('login.or')}</span>
</div>
</div>}
{
(systemFeatures.enable_email_code_login || systemFeatures.enable_email_password_login) && <>
{systemFeatures.enable_email_code_login && authType === 'code' && <>
<MailAndCodeAuth />
{systemFeatures.enable_email_password_login && <div className='cursor-pointer py-1 text-center' onClick={() => { updateAuthType('password') }}>
<span className='system-xs-medium text-components-button-secondary-accent-text'>{t('login.usePassword')}</span>
</div>}
</>}
{systemFeatures.enable_email_password_login && authType === 'password' && <>
<MailAndPasswordAuth isEmailSetup={systemFeatures.is_email_setup} />
{systemFeatures.enable_email_code_login && <div className='cursor-pointer py-1 text-center' onClick={() => { updateAuthType('code') }}>
<span className='system-xs-medium text-components-button-secondary-accent-text'>{t('login.useVerificationCode')}</span>
</div>}
</>}
</>
}
{allMethodsAreDisabled && <>
<div className="rounded-lg bg-gradient-to-r from-workflow-workflow-progress-bg-1 to-workflow-workflow-progress-bg-2 p-4">
<div className='shadows-shadow-lg mb-2 flex h-10 w-10 items-center justify-center rounded-xl bg-components-card-bg shadow'>
<RiDoorLockLine className='h-5 w-5' />
</div>
<p className='system-sm-medium text-text-primary'>{t('login.noLoginMethod')}</p>
<p className='system-xs-regular mt-1 text-text-tertiary'>{t('login.noLoginMethodTip')}</p>
</div>
<div className="relative my-2 py-2">
{showORLine && (
<div className="relative mt-6">
<div className="absolute inset-0 flex items-center" aria-hidden="true">
<div className='h-px w-full bg-gradient-to-r from-background-gradient-mask-transparent via-divider-regular to-background-gradient-mask-transparent'></div>
<div className="h-px w-full bg-gradient-to-r from-background-gradient-mask-transparent via-divider-regular to-background-gradient-mask-transparent"></div>
</div>
<div className="relative flex justify-center">
<span className="system-xs-medium-uppercase px-2 text-text-tertiary">{t('login.or')}</span>
</div>
</div>
</>}
{!systemFeatures.branding.enabled && <>
<div className="system-xs-regular mt-2 block w-full text-text-tertiary">
{t('login.tosDesc')}
)}
{
(systemFeatures.enable_email_code_login || systemFeatures.enable_email_password_login) && (
<>
{systemFeatures.enable_email_code_login && authType === 'code' && (
<>
<MailAndCodeAuth />
{systemFeatures.enable_email_password_login && (
<div className="cursor-pointer py-1 text-center" onClick={() => { updateAuthType('password') }}>
<span className="system-xs-medium text-components-button-secondary-accent-text">{t('login.usePassword')}</span>
</div>
)}
</>
)}
{systemFeatures.enable_email_password_login && authType === 'password' && (
<>
<MailAndPasswordAuth isEmailSetup={systemFeatures.is_email_setup} />
{systemFeatures.enable_email_code_login && (
<div className="cursor-pointer py-1 text-center" onClick={() => { updateAuthType('code') }}>
<span className="system-xs-medium text-components-button-secondary-accent-text">{t('login.useVerificationCode')}</span>
</div>
)}
</>
)}
</>
)
}
{allMethodsAreDisabled && (
<>
<div className="rounded-lg bg-gradient-to-r from-workflow-workflow-progress-bg-1 to-workflow-workflow-progress-bg-2 p-4">
<div className="shadows-shadow-lg mb-2 flex h-10 w-10 items-center justify-center rounded-xl bg-components-card-bg shadow">
<RiDoorLockLine className="h-5 w-5" />
</div>
<p className="system-sm-medium text-text-primary">{t('login.noLoginMethod')}</p>
<p className="system-xs-regular mt-1 text-text-tertiary">{t('login.noLoginMethodTip')}</p>
</div>
<div className="relative my-2 py-2">
<div className="absolute inset-0 flex items-center" aria-hidden="true">
<div className="h-px w-full bg-gradient-to-r from-background-gradient-mask-transparent via-divider-regular to-background-gradient-mask-transparent"></div>
</div>
</div>
</>
)}
{!systemFeatures.branding.enabled && (
<>
<div className="system-xs-regular mt-2 block w-full text-text-tertiary">
{t('login.tosDesc')}
&nbsp;
<Link
className='system-xs-medium text-text-secondary hover:underline'
target='_blank' rel='noopener noreferrer'
href='https://dify.ai/terms'
>{t('login.tos')}</Link>
<Link
className="system-xs-medium text-text-secondary hover:underline"
target="_blank"
rel="noopener noreferrer"
href="https://dify.ai/terms"
>
{t('login.tos')}
</Link>
&nbsp;&&nbsp;
<Link
className='system-xs-medium text-text-secondary hover:underline'
target='_blank' rel='noopener noreferrer'
href='https://dify.ai/privacy'
>{t('login.pp')}</Link>
</div>
{IS_CE_EDITION && <div className="w-hull system-xs-regular mt-2 block text-text-tertiary">
{t('login.goToInit')}
<Link
className="system-xs-medium text-text-secondary hover:underline"
target="_blank"
rel="noopener noreferrer"
href="https://dify.ai/privacy"
>
{t('login.pp')}
</Link>
</div>
{IS_CE_EDITION && (
<div className="w-hull system-xs-regular mt-2 block text-text-tertiary">
{t('login.goToInit')}
&nbsp;
<Link
className='system-xs-medium text-text-secondary hover:underline'
href='/install'
>{t('login.setAdminAccount')}</Link>
</div>}
</>}
<Link
className="system-xs-medium text-text-secondary hover:underline"
href="/install"
>
{t('login.setAdminAccount')}
</Link>
</div>
)}
</>
)}
</div>
</div>

View File

@ -1,15 +1,15 @@
'use client'
import { useRouter, useSearchParams } from 'next/navigation'
import type { FC } from 'react'
import { useRouter, useSearchParams } from 'next/navigation'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { useGlobalPublicStore } from '@/context/global-public-context'
import AppUnavailable from '@/app/components/base/app-unavailable'
import NormalForm from './normalForm'
import { AccessMode } from '@/models/access-control'
import ExternalMemberSsoAuth from './components/external-member-sso-auth'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useWebAppStore } from '@/context/web-app-context'
import { AccessMode } from '@/models/access-control'
import { webAppLogout } from '@/service/webapp-auth'
import ExternalMemberSsoAuth from './components/external-member-sso-auth'
import NormalForm from './normalForm'
const WebSSOForm: FC = () => {
const { t } = useTranslation()
@ -34,29 +34,37 @@ const WebSSOForm: FC = () => {
}, [getSigninUrl, router, webAppLogout, shareCode])
if (!redirectUrl) {
return <div className='flex h-full items-center justify-center'>
<AppUnavailable code={t('share.common.appUnavailable')} unknownReason='redirect url is invalid.' />
</div>
return (
<div className="flex h-full items-center justify-center">
<AppUnavailable code={t('share.common.appUnavailable')} unknownReason="redirect url is invalid." />
</div>
)
}
if (!systemFeatures.webapp_auth.enabled) {
return <div className="flex h-full items-center justify-center">
<p className='system-xs-regular text-text-tertiary'>{t('login.webapp.disabled')}</p>
</div>
return (
<div className="flex h-full items-center justify-center">
<p className="system-xs-regular text-text-tertiary">{t('login.webapp.disabled')}</p>
</div>
)
}
if (webAppAccessMode && (webAppAccessMode === AccessMode.ORGANIZATION || webAppAccessMode === AccessMode.SPECIFIC_GROUPS_MEMBERS)) {
return <div className='w-full max-w-[400px]'>
<NormalForm />
</div>
return (
<div className="w-full max-w-[400px]">
<NormalForm />
</div>
)
}
if (webAppAccessMode && webAppAccessMode === AccessMode.EXTERNAL_MEMBERS)
return <ExternalMemberSsoAuth />
return <div className='flex h-full flex-col items-center justify-center gap-y-4'>
<AppUnavailable className='h-auto w-auto' isUnknownReason={true} />
<span className='system-sm-regular cursor-pointer text-text-tertiary' onClick={backToHome}>{t('share.login.backToHome')}</span>
</div>
return (
<div className="flex h-full flex-col items-center justify-center gap-y-4">
<AppUnavailable className="h-auto w-auto" isUnknownReason={true} />
<span className="system-sm-regular cursor-pointer text-text-tertiary" onClick={backToHome}>{t('share.login.backToHome')}</span>
</div>
)
}
export default React.memo(WebSSOForm)

View File

@ -1,23 +1,25 @@
'use client'
import type { Area } from 'react-easy-crop'
import type { OnImageInput } from '@/app/components/base/app-icon-picker/ImageInput'
import type { AvatarProps } from '@/app/components/base/avatar'
import type { ImageFile } from '@/types/app'
import { RiDeleteBin5Line, RiPencilLine } from '@remixicon/react'
import React, { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import { RiDeleteBin5Line, RiPencilLine } from '@remixicon/react'
import { updateUserProfile } from '@/service/common'
import { ToastContext } from '@/app/components/base/toast'
import ImageInput, { type OnImageInput } from '@/app/components/base/app-icon-picker/ImageInput'
import Modal from '@/app/components/base/modal'
import Divider from '@/app/components/base/divider'
import Button from '@/app/components/base/button'
import Avatar, { type AvatarProps } from '@/app/components/base/avatar'
import { useLocalFileUploader } from '@/app/components/base/image-uploader/hooks'
import type { ImageFile } from '@/types/app'
import ImageInput from '@/app/components/base/app-icon-picker/ImageInput'
import getCroppedImg from '@/app/components/base/app-icon-picker/utils'
import Avatar from '@/app/components/base/avatar'
import Button from '@/app/components/base/button'
import Divider from '@/app/components/base/divider'
import { useLocalFileUploader } from '@/app/components/base/image-uploader/hooks'
import Modal from '@/app/components/base/modal'
import { ToastContext } from '@/app/components/base/toast'
import { DISABLE_UPLOAD_IMAGE_AS_ICON } from '@/config'
import { updateUserProfile } from '@/service/common'
type InputImageInfo = { file: File } | { tempUrl: string; croppedAreaPixels: Area; fileName: string }
type InputImageInfo = { file: File } | { tempUrl: string, croppedAreaPixels: Area, fileName: string }
type AvatarWithEditProps = AvatarProps & { onSave?: () => void }
const AvatarWithEdit = ({ onSave, ...props }: AvatarWithEditProps) => {
@ -116,15 +118,17 @@ const AvatarWithEdit = ({ onSave, ...props }: AvatarWithEditProps) => {
setHoverArea(isRight ? 'right' : 'left')
}}
>
{hoverArea === 'right' && !onAvatarError ? (
<span className="text-xs text-white">
<RiDeleteBin5Line />
</span>
) : (
<span className="text-xs text-white">
<RiPencilLine />
</span>
)}
{hoverArea === 'right' && !onAvatarError
? (
<span className="text-xs text-white">
<RiDeleteBin5Line />
</span>
)
: (
<span className="text-xs text-white">
<RiPencilLine />
</span>
)}
</div>
</div>
</div>
@ -135,15 +139,15 @@ const AvatarWithEdit = ({ onSave, ...props }: AvatarWithEditProps) => {
isShow={isShowAvatarPicker}
onClose={() => setIsShowAvatarPicker(false)}
>
<ImageInput onImageInput={handleImageInput} cropShape='round' />
<Divider className='m-0' />
<ImageInput onImageInput={handleImageInput} cropShape="round" />
<Divider className="m-0" />
<div className='flex w-full items-center justify-center gap-2 p-3'>
<Button className='w-full' onClick={() => setIsShowAvatarPicker(false)}>
<div className="flex w-full items-center justify-center gap-2 p-3">
<Button className="w-full" onClick={() => setIsShowAvatarPicker(false)}>
{t('app.iconPicker.cancel')}
</Button>
<Button variant="primary" className='w-full' disabled={uploading || !inputImageInfo} loading={uploading} onClick={handleSelect}>
<Button variant="primary" className="w-full" disabled={uploading || !inputImageInfo} loading={uploading} onClick={handleSelect}>
{t('app.iconPicker.ok')}
</Button>
</div>

View File

@ -1,22 +1,22 @@
import type { ResponseError } from '@/service/fetch'
import { RiCloseLine } from '@remixicon/react'
import { noop } from 'lodash-es'
import { useRouter } from 'next/navigation'
import React, { useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { useRouter } from 'next/navigation'
import { useContext } from 'use-context-selector'
import { ToastContext } from '@/app/components/base/toast'
import { RiCloseLine } from '@remixicon/react'
import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import Modal from '@/app/components/base/modal'
import { ToastContext } from '@/app/components/base/toast'
import {
checkEmailExisted,
resetEmail,
sendVerifyCode,
verifyEmail,
} from '@/service/common'
import { noop } from 'lodash-es'
import { asyncRunSafe } from '@/utils'
import type { ResponseError } from '@/service/fetch'
import { useLogout } from '@/service/use-common'
import { asyncRunSafe } from '@/utils'
type Props = {
show: boolean
@ -116,7 +116,7 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => {
}
const isValidEmail = (email: string): boolean => {
const rfc5322emailRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
const rfc5322emailRegex = /^[\w.!#$%&'*+/=?^`{|}~-]+@[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/i
return rfc5322emailRegex.test(email) && email.length <= 254
}
@ -201,35 +201,35 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => {
<Modal
isShow={show}
onClose={noop}
className='!w-[420px] !p-6'
className="!w-[420px] !p-6"
>
<div className='absolute right-5 top-5 cursor-pointer p-1.5' onClick={onClose}>
<RiCloseLine className='h-5 w-5 text-text-tertiary' />
<div className="absolute right-5 top-5 cursor-pointer p-1.5" onClick={onClose}>
<RiCloseLine className="h-5 w-5 text-text-tertiary" />
</div>
{step === STEP.start && (
<>
<div className='title-2xl-semi-bold pb-3 text-text-primary'>{t('common.account.changeEmail.title')}</div>
<div className='space-y-0.5 pb-2 pt-1'>
<div className='body-md-medium text-text-warning'>{t('common.account.changeEmail.authTip')}</div>
<div className='body-md-regular text-text-secondary'>
<div className="title-2xl-semi-bold pb-3 text-text-primary">{t('common.account.changeEmail.title')}</div>
<div className="space-y-0.5 pb-2 pt-1">
<div className="body-md-medium text-text-warning">{t('common.account.changeEmail.authTip')}</div>
<div className="body-md-regular text-text-secondary">
<Trans
i18nKey="common.account.changeEmail.content1"
components={{ email: <span className='body-md-medium text-text-primary'></span> }}
components={{ email: <span className="body-md-medium text-text-primary"></span> }}
values={{ email }}
/>
</div>
</div>
<div className='pt-3'></div>
<div className='space-y-2'>
<div className="pt-3"></div>
<div className="space-y-2">
<Button
className='!w-full'
variant='primary'
className="!w-full"
variant="primary"
onClick={sendCodeToOriginEmail}
>
{t('common.account.changeEmail.sendVerifyCode')}
</Button>
<Button
className='!w-full'
className="!w-full"
onClick={onClose}
>
{t('common.operation.cancel')}
@ -239,86 +239,86 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => {
)}
{step === STEP.verifyOrigin && (
<>
<div className='title-2xl-semi-bold pb-3 text-text-primary'>{t('common.account.changeEmail.verifyEmail')}</div>
<div className='space-y-0.5 pb-2 pt-1'>
<div className='body-md-regular text-text-secondary'>
<div className="title-2xl-semi-bold pb-3 text-text-primary">{t('common.account.changeEmail.verifyEmail')}</div>
<div className="space-y-0.5 pb-2 pt-1">
<div className="body-md-regular text-text-secondary">
<Trans
i18nKey="common.account.changeEmail.content2"
components={{ email: <span className='body-md-medium text-text-primary'></span> }}
components={{ email: <span className="body-md-medium text-text-primary"></span> }}
values={{ email }}
/>
</div>
</div>
<div className='pt-3'>
<div className='system-sm-medium mb-1 flex h-6 items-center text-text-secondary'>{t('common.account.changeEmail.codeLabel')}</div>
<div className="pt-3">
<div className="system-sm-medium mb-1 flex h-6 items-center text-text-secondary">{t('common.account.changeEmail.codeLabel')}</div>
<Input
className='!w-full'
className="!w-full"
placeholder={t('common.account.changeEmail.codePlaceholder')}
value={code}
onChange={e => setCode(e.target.value)}
maxLength={6}
/>
</div>
<div className='mt-3 space-y-2'>
<div className="mt-3 space-y-2">
<Button
disabled={code.length !== 6}
className='!w-full'
variant='primary'
className="!w-full"
variant="primary"
onClick={handleVerifyOriginEmail}
>
{t('common.account.changeEmail.continue')}
</Button>
<Button
className='!w-full'
className="!w-full"
onClick={onClose}
>
{t('common.operation.cancel')}
</Button>
</div>
<div className='system-xs-regular mt-3 flex items-center gap-1 text-text-tertiary'>
<div className="system-xs-regular mt-3 flex items-center gap-1 text-text-tertiary">
<span>{t('common.account.changeEmail.resendTip')}</span>
{time > 0 && (
<span>{t('common.account.changeEmail.resendCount', { count: time })}</span>
)}
{!time && (
<span onClick={sendCodeToOriginEmail} className='system-xs-medium cursor-pointer text-text-accent-secondary'>{t('common.account.changeEmail.resend')}</span>
<span onClick={sendCodeToOriginEmail} className="system-xs-medium cursor-pointer text-text-accent-secondary">{t('common.account.changeEmail.resend')}</span>
)}
</div>
</>
)}
{step === STEP.newEmail && (
<>
<div className='title-2xl-semi-bold pb-3 text-text-primary'>{t('common.account.changeEmail.newEmail')}</div>
<div className='space-y-0.5 pb-2 pt-1'>
<div className='body-md-regular text-text-secondary'>{t('common.account.changeEmail.content3')}</div>
<div className="title-2xl-semi-bold pb-3 text-text-primary">{t('common.account.changeEmail.newEmail')}</div>
<div className="space-y-0.5 pb-2 pt-1">
<div className="body-md-regular text-text-secondary">{t('common.account.changeEmail.content3')}</div>
</div>
<div className='pt-3'>
<div className='system-sm-medium mb-1 flex h-6 items-center text-text-secondary'>{t('common.account.changeEmail.emailLabel')}</div>
<div className="pt-3">
<div className="system-sm-medium mb-1 flex h-6 items-center text-text-secondary">{t('common.account.changeEmail.emailLabel')}</div>
<Input
className='!w-full'
className="!w-full"
placeholder={t('common.account.changeEmail.emailPlaceholder')}
value={mail}
onChange={e => handleNewEmailValueChange(e.target.value)}
destructive={newEmailExited || unAvailableEmail}
/>
{newEmailExited && (
<div className='body-xs-regular mt-1 py-0.5 text-text-destructive'>{t('common.account.changeEmail.existingEmail')}</div>
<div className="body-xs-regular mt-1 py-0.5 text-text-destructive">{t('common.account.changeEmail.existingEmail')}</div>
)}
{unAvailableEmail && (
<div className='body-xs-regular mt-1 py-0.5 text-text-destructive'>{t('common.account.changeEmail.unAvailableEmail')}</div>
<div className="body-xs-regular mt-1 py-0.5 text-text-destructive">{t('common.account.changeEmail.unAvailableEmail')}</div>
)}
</div>
<div className='mt-3 space-y-2'>
<div className="mt-3 space-y-2">
<Button
disabled={!mail || newEmailExited || unAvailableEmail || isCheckingEmail || !isValidEmail(mail)}
className='!w-full'
variant='primary'
className="!w-full"
variant="primary"
onClick={sendCodeToNewEmail}
>
{t('common.account.changeEmail.sendVerifyCode')}
</Button>
<Button
className='!w-full'
className="!w-full"
onClick={onClose}
>
{t('common.operation.cancel')}
@ -328,49 +328,49 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => {
)}
{step === STEP.verifyNew && (
<>
<div className='title-2xl-semi-bold pb-3 text-text-primary'>{t('common.account.changeEmail.verifyNew')}</div>
<div className='space-y-0.5 pb-2 pt-1'>
<div className='body-md-regular text-text-secondary'>
<div className="title-2xl-semi-bold pb-3 text-text-primary">{t('common.account.changeEmail.verifyNew')}</div>
<div className="space-y-0.5 pb-2 pt-1">
<div className="body-md-regular text-text-secondary">
<Trans
i18nKey="common.account.changeEmail.content4"
components={{ email: <span className='body-md-medium text-text-primary'></span> }}
components={{ email: <span className="body-md-medium text-text-primary"></span> }}
values={{ email: mail }}
/>
</div>
</div>
<div className='pt-3'>
<div className='system-sm-medium mb-1 flex h-6 items-center text-text-secondary'>{t('common.account.changeEmail.codeLabel')}</div>
<div className="pt-3">
<div className="system-sm-medium mb-1 flex h-6 items-center text-text-secondary">{t('common.account.changeEmail.codeLabel')}</div>
<Input
className='!w-full'
className="!w-full"
placeholder={t('common.account.changeEmail.codePlaceholder')}
value={code}
onChange={e => setCode(e.target.value)}
maxLength={6}
/>
</div>
<div className='mt-3 space-y-2'>
<div className="mt-3 space-y-2">
<Button
disabled={code.length !== 6}
className='!w-full'
variant='primary'
className="!w-full"
variant="primary"
onClick={submitNewEmail}
>
{t('common.account.changeEmail.changeTo', { email: mail })}
</Button>
<Button
className='!w-full'
className="!w-full"
onClick={onClose}
>
{t('common.operation.cancel')}
</Button>
</div>
<div className='system-xs-regular mt-3 flex items-center gap-1 text-text-tertiary'>
<div className="system-xs-regular mt-3 flex items-center gap-1 text-text-tertiary">
<span>{t('common.account.changeEmail.resendTip')}</span>
{time > 0 && (
<span>{t('common.account.changeEmail.resendCount', { count: time })}</span>
)}
{!time && (
<span onClick={sendCodeToNewEmail} className='system-xs-medium cursor-pointer text-text-accent-secondary'>{t('common.account.changeEmail.resend')}</span>
<span onClick={sendCodeToNewEmail} className="system-xs-medium cursor-pointer text-text-accent-secondary">{t('common.account.changeEmail.resend')}</span>
)}
</div>
</>

View File

@ -1,30 +1,29 @@
'use client'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import type { IItem } from '@/app/components/header/account-setting/collapse'
import type { App } from '@/types/app'
import {
RiGraduationCapFill,
} from '@remixicon/react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import DeleteAccount from '../delete-account'
import AvatarWithEdit from './AvatarWithEdit'
import Collapse from '@/app/components/header/account-setting/collapse'
import type { IItem } from '@/app/components/header/account-setting/collapse'
import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button'
import { updateUserProfile } from '@/service/common'
import { useAppContext } from '@/context/app-context'
import { useProviderContext } from '@/context/provider-context'
import { ToastContext } from '@/app/components/base/toast'
import AppIcon from '@/app/components/base/app-icon'
import { IS_CE_EDITION } from '@/config'
import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import Modal from '@/app/components/base/modal'
import PremiumBadge from '@/app/components/base/premium-badge'
import { ToastContext } from '@/app/components/base/toast'
import Collapse from '@/app/components/header/account-setting/collapse'
import { IS_CE_EDITION, validPassword } from '@/config'
import { useAppContext } from '@/context/app-context'
import { useGlobalPublicStore } from '@/context/global-public-context'
import EmailChangeModal from './email-change-modal'
import { validPassword } from '@/config'
import type { App } from '@/types/app'
import { useProviderContext } from '@/context/provider-context'
import { updateUserProfile } from '@/service/common'
import { useAppList } from '@/service/use-apps'
import DeleteAccount from '../delete-account'
import AvatarWithEdit from './AvatarWithEdit'
import EmailChangeModal from './email-change-modal'
const titleClassName = `
system-sm-semibold text-text-secondary
@ -129,60 +128,60 @@ export default function AccountPage() {
const renderAppItem = (item: IItem) => {
const { icon, icon_background, icon_type, icon_url } = item as any
return (
<div className='flex px-3 py-1'>
<div className='mr-3'>
<div className="flex px-3 py-1">
<div className="mr-3">
<AppIcon
size='tiny'
size="tiny"
iconType={icon_type}
icon={icon}
background={icon_background}
imageUrl={icon_url}
/>
</div>
<div className='system-sm-medium mt-[3px] text-text-secondary'>{item.name}</div>
<div className="system-sm-medium mt-[3px] text-text-secondary">{item.name}</div>
</div>
)
}
return (
<>
<div className='pb-3 pt-2'>
<h4 className='title-2xl-semi-bold text-text-primary'>{t('common.account.myAccount')}</h4>
<div className="pb-3 pt-2">
<h4 className="title-2xl-semi-bold text-text-primary">{t('common.account.myAccount')}</h4>
</div>
<div className='mb-8 flex items-center rounded-xl bg-gradient-to-r from-background-gradient-bg-fill-chat-bg-2 to-background-gradient-bg-fill-chat-bg-1 p-6'>
<div className="mb-8 flex items-center rounded-xl bg-gradient-to-r from-background-gradient-bg-fill-chat-bg-2 to-background-gradient-bg-fill-chat-bg-1 p-6">
<AvatarWithEdit avatar={userProfile.avatar_url} name={userProfile.name} onSave={mutateUserProfile} size={64} />
<div className='ml-4'>
<p className='system-xl-semibold text-text-primary'>
<div className="ml-4">
<p className="system-xl-semibold text-text-primary">
{userProfile.name}
{isEducationAccount && (
<PremiumBadge size='s' color='blue' className='ml-1 !px-2'>
<RiGraduationCapFill className='mr-1 h-3 w-3' />
<span className='system-2xs-medium'>EDU</span>
<PremiumBadge size="s" color="blue" className="ml-1 !px-2">
<RiGraduationCapFill className="mr-1 h-3 w-3" />
<span className="system-2xs-medium">EDU</span>
</PremiumBadge>
)}
</p>
<p className='system-xs-regular text-text-tertiary'>{userProfile.email}</p>
<p className="system-xs-regular text-text-tertiary">{userProfile.email}</p>
</div>
</div>
<div className='mb-8'>
<div className="mb-8">
<div className={titleClassName}>{t('common.account.name')}</div>
<div className='mt-2 flex w-full items-center justify-between gap-2'>
<div className='system-sm-regular flex-1 rounded-lg bg-components-input-bg-normal p-2 text-components-input-text-filled '>
<span className='pl-1'>{userProfile.name}</span>
<div className="mt-2 flex w-full items-center justify-between gap-2">
<div className="system-sm-regular flex-1 rounded-lg bg-components-input-bg-normal p-2 text-components-input-text-filled ">
<span className="pl-1">{userProfile.name}</span>
</div>
<div className='system-sm-medium cursor-pointer rounded-lg bg-components-button-tertiary-bg px-3 py-2 text-components-button-tertiary-text' onClick={handleEditName}>
<div className="system-sm-medium cursor-pointer rounded-lg bg-components-button-tertiary-bg px-3 py-2 text-components-button-tertiary-text" onClick={handleEditName}>
{t('common.operation.edit')}
</div>
</div>
</div>
<div className='mb-8'>
<div className="mb-8">
<div className={titleClassName}>{t('common.account.email')}</div>
<div className='mt-2 flex w-full items-center justify-between gap-2'>
<div className='system-sm-regular flex-1 rounded-lg bg-components-input-bg-normal p-2 text-components-input-text-filled '>
<span className='pl-1'>{userProfile.email}</span>
<div className="mt-2 flex w-full items-center justify-between gap-2">
<div className="system-sm-regular flex-1 rounded-lg bg-components-input-bg-normal p-2 text-components-input-text-filled ">
<span className="pl-1">{userProfile.email}</span>
</div>
{systemFeatures.enable_change_email && (
<div className='system-sm-medium cursor-pointer rounded-lg bg-components-button-tertiary-bg px-3 py-2 text-components-button-tertiary-text' onClick={() => setShowUpdateEmail(true)}>
<div className="system-sm-medium cursor-pointer rounded-lg bg-components-button-tertiary-bg px-3 py-2 text-components-button-tertiary-text" onClick={() => setShowUpdateEmail(true)}>
{t('common.operation.change')}
</div>
)}
@ -190,17 +189,17 @@ export default function AccountPage() {
</div>
{
systemFeatures.enable_email_password_login && (
<div className='mb-8 flex justify-between gap-2'>
<div className="mb-8 flex justify-between gap-2">
<div>
<div className='system-sm-semibold mb-1 text-text-secondary'>{t('common.account.password')}</div>
<div className='body-xs-regular mb-2 text-text-tertiary'>{t('common.account.passwordTip')}</div>
<div className="system-sm-semibold mb-1 text-text-secondary">{t('common.account.password')}</div>
<div className="body-xs-regular mb-2 text-text-tertiary">{t('common.account.passwordTip')}</div>
</div>
<Button onClick={() => setEditPasswordModalVisible(true)}>{userProfile.is_password_set ? t('common.account.resetPassword') : t('common.account.setPassword')}</Button>
</div>
)
}
<div className='mb-6 border-[1px] border-divider-subtle' />
<div className='mb-8'>
<div className="mb-6 border-[1px] border-divider-subtle" />
<div className="mb-8">
<div className={titleClassName}>{t('common.account.langGeniusAccount')}</div>
<div className={descriptionClassName}>{t('common.account.langGeniusAccountTip')}</div>
{!!apps.length && (
@ -208,29 +207,30 @@ export default function AccountPage() {
title={`${t('common.account.showAppLength', { length: apps.length })}`}
items={apps.map((app: App) => ({ ...app, key: app.id, name: app.name }))}
renderItem={renderAppItem}
wrapperClassName='mt-2'
wrapperClassName="mt-2"
/>
)}
{!IS_CE_EDITION && <Button className='mt-2 text-components-button-destructive-secondary-text' onClick={() => setShowDeleteAccountModal(true)}>{t('common.account.delete')}</Button>}
{!IS_CE_EDITION && <Button className="mt-2 text-components-button-destructive-secondary-text" onClick={() => setShowDeleteAccountModal(true)}>{t('common.account.delete')}</Button>}
</div>
{
editNameModalVisible && (
<Modal
isShow
onClose={() => setEditNameModalVisible(false)}
className='!w-[420px] !p-6'
className="!w-[420px] !p-6"
>
<div className='title-2xl-semi-bold mb-6 text-text-primary'>{t('common.account.editName')}</div>
<div className="title-2xl-semi-bold mb-6 text-text-primary">{t('common.account.editName')}</div>
<div className={titleClassName}>{t('common.account.name')}</div>
<Input className='mt-2'
<Input
className="mt-2"
value={editName}
onChange={e => setEditName(e.target.value)}
/>
<div className='mt-10 flex justify-end'>
<Button className='mr-2' onClick={() => setEditNameModalVisible(false)}>{t('common.operation.cancel')}</Button>
<div className="mt-10 flex justify-end">
<Button className="mr-2" onClick={() => setEditNameModalVisible(false)}>{t('common.operation.cancel')}</Button>
<Button
disabled={editing || !editName}
variant='primary'
variant="primary"
onClick={handleSaveName}
>
{t('common.operation.save')}
@ -247,13 +247,13 @@ export default function AccountPage() {
setEditPasswordModalVisible(false)
resetPasswordForm()
}}
className='!w-[420px] !p-6'
className="!w-[420px] !p-6"
>
<div className='title-2xl-semi-bold mb-6 text-text-primary'>{userProfile.is_password_set ? t('common.account.resetPassword') : t('common.account.setPassword')}</div>
<div className="title-2xl-semi-bold mb-6 text-text-primary">{userProfile.is_password_set ? t('common.account.resetPassword') : t('common.account.setPassword')}</div>
{userProfile.is_password_set && (
<>
<div className={titleClassName}>{t('common.account.currentPassword')}</div>
<div className='relative mt-2'>
<div className="relative mt-2">
<Input
type={showCurrentPassword ? 'text' : 'password'}
value={currentPassword}
@ -263,7 +263,7 @@ export default function AccountPage() {
<div className="absolute inset-y-0 right-0 flex items-center">
<Button
type="button"
variant='ghost'
variant="ghost"
onClick={() => setShowCurrentPassword(!showCurrentPassword)}
>
{showCurrentPassword ? '👀' : '😝'}
@ -272,10 +272,10 @@ export default function AccountPage() {
</div>
</>
)}
<div className='system-sm-semibold mt-8 text-text-secondary'>
<div className="system-sm-semibold mt-8 text-text-secondary">
{userProfile.is_password_set ? t('common.account.newPassword') : t('common.account.password')}
</div>
<div className='relative mt-2'>
<div className="relative mt-2">
<Input
type={showPassword ? 'text' : 'password'}
value={password}
@ -284,15 +284,15 @@ export default function AccountPage() {
<div className="absolute inset-y-0 right-0 flex items-center">
<Button
type="button"
variant='ghost'
variant="ghost"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? '👀' : '😝'}
</Button>
</div>
</div>
<div className='system-sm-semibold mt-8 text-text-secondary'>{t('common.account.confirmPassword')}</div>
<div className='relative mt-2'>
<div className="system-sm-semibold mt-8 text-text-secondary">{t('common.account.confirmPassword')}</div>
<div className="relative mt-2">
<Input
type={showConfirmPassword ? 'text' : 'password'}
value={confirmPassword}
@ -301,21 +301,26 @@ export default function AccountPage() {
<div className="absolute inset-y-0 right-0 flex items-center">
<Button
type="button"
variant='ghost'
variant="ghost"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
>
{showConfirmPassword ? '👀' : '😝'}
</Button>
</div>
</div>
<div className='mt-10 flex justify-end'>
<Button className='mr-2' onClick={() => {
setEditPasswordModalVisible(false)
resetPasswordForm()
}}>{t('common.operation.cancel')}</Button>
<div className="mt-10 flex justify-end">
<Button
className="mr-2"
onClick={() => {
setEditPasswordModalVisible(false)
resetPasswordForm()
}}
>
{t('common.operation.cancel')}
</Button>
<Button
disabled={editing}
variant='primary'
variant="primary"
onClick={handleSavePassword}
>
{userProfile.is_password_set ? t('common.operation.reset') : t('common.operation.save')}

View File

@ -1,18 +1,18 @@
'use client'
import { useTranslation } from 'react-i18next'
import { Fragment } from 'react'
import { useRouter } from 'next/navigation'
import { Menu, MenuButton, MenuItem, MenuItems, Transition } from '@headlessui/react'
import {
RiGraduationCapFill,
} from '@remixicon/react'
import { Menu, MenuButton, MenuItem, MenuItems, Transition } from '@headlessui/react'
import { useRouter } from 'next/navigation'
import { Fragment } from 'react'
import { useTranslation } from 'react-i18next'
import { resetUser } from '@/app/components/base/amplitude/utils'
import Avatar from '@/app/components/base/avatar'
import { useAppContext } from '@/context/app-context'
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 { useAppContext } from '@/context/app-context'
import { useProviderContext } from '@/context/provider-context'
import { useLogout } from '@/service/use-common'
import { resetUser } from '@/app/components/base/amplitude/utils'
export type IAppSelector = {
isMobile: boolean
@ -70,31 +70,31 @@ export default function AppSelector() {
"
>
<MenuItem>
<div className='p-1'>
<div className='flex flex-nowrap items-center px-3 py-2'>
<div className='grow'>
<div className='system-md-medium break-all text-text-primary'>
<div className="p-1">
<div className="flex flex-nowrap items-center px-3 py-2">
<div className="grow">
<div className="system-md-medium break-all text-text-primary">
{userProfile.name}
{isEducationAccount && (
<PremiumBadge size='s' color='blue' className='ml-1 !px-2'>
<RiGraduationCapFill className='mr-1 h-3 w-3' />
<span className='system-2xs-medium'>EDU</span>
<PremiumBadge size="s" color="blue" className="ml-1 !px-2">
<RiGraduationCapFill className="mr-1 h-3 w-3" />
<span className="system-2xs-medium">EDU</span>
</PremiumBadge>
)}
</div>
<div className='system-xs-regular break-all text-text-tertiary'>{userProfile.email}</div>
<div className="system-xs-regular break-all text-text-tertiary">{userProfile.email}</div>
</div>
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} size={32} />
</div>
</div>
</MenuItem>
<MenuItem>
<div className='p-1' onClick={() => handleLogout()}>
<div className="p-1" onClick={() => handleLogout()}>
<div
className='group flex h-9 cursor-pointer items-center justify-start rounded-lg px-3 hover:bg-state-base-hover'
className="group flex h-9 cursor-pointer items-center justify-start rounded-lg px-3 hover:bg-state-base-hover"
>
<LogOut01 className='mr-1 flex h-4 w-4 text-text-tertiary' />
<div className='text-[14px] font-normal text-text-secondary'>{t('common.userProfile.logout')}</div>
<LogOut01 className="mr-1 flex h-4 w-4 text-text-tertiary" />
<div className="text-[14px] font-normal text-text-secondary">{t('common.userProfile.logout')}</div>
</div>
</div>
</MenuItem>

View File

@ -1,11 +1,11 @@
'use client'
import { useTranslation } from 'react-i18next'
import { useCallback, useState } from 'react'
import Link from 'next/link'
import { useSendDeleteAccountEmail } from '../state'
import { useAppContext } from '@/context/app-context'
import Input from '@/app/components/base/input'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import { useAppContext } from '@/context/app-context'
import { useSendDeleteAccountEmail } from '../state'
type DeleteAccountProps = {
onCancel: () => void
@ -28,21 +28,26 @@ export default function CheckEmail(props: DeleteAccountProps) {
catch (error) { console.error(error) }
}, [getDeleteEmailVerifyCode, props])
return <>
<div className='body-md-medium py-1 text-text-destructive'>
{t('common.account.deleteTip')}
</div>
<div className='body-md-regular pb-2 pt-1 text-text-secondary'>
{t('common.account.deletePrivacyLinkTip')}
<Link href='https://dify.ai/privacy' className='text-text-accent'>{t('common.account.deletePrivacyLink')}</Link>
</div>
<label className='system-sm-semibold mb-1 mt-3 flex h-6 items-center text-text-secondary'>{t('common.account.deleteLabel')}</label>
<Input placeholder={t('common.account.deletePlaceholder') as string} onChange={(e) => {
setUserInputEmail(e.target.value)
}} />
<div className='mt-3 flex w-full flex-col gap-2'>
<Button className='w-full' disabled={userInputEmail !== userProfile.email || isSendingEmail} loading={isSendingEmail} variant='primary' onClick={handleConfirm}>{t('common.account.sendVerificationButton')}</Button>
<Button className='w-full' onClick={props.onCancel}>{t('common.operation.cancel')}</Button>
</div>
</>
return (
<>
<div className="body-md-medium py-1 text-text-destructive">
{t('common.account.deleteTip')}
</div>
<div className="body-md-regular pb-2 pt-1 text-text-secondary">
{t('common.account.deletePrivacyLinkTip')}
<Link href="https://dify.ai/privacy" className="text-text-accent">{t('common.account.deletePrivacyLink')}</Link>
</div>
<label className="system-sm-semibold mb-1 mt-3 flex h-6 items-center text-text-secondary">{t('common.account.deleteLabel')}</label>
<Input
placeholder={t('common.account.deletePlaceholder') as string}
onChange={(e) => {
setUserInputEmail(e.target.value)
}}
/>
<div className="mt-3 flex w-full flex-col gap-2">
<Button className="w-full" disabled={userInputEmail !== userProfile.email || isSendingEmail} loading={isSendingEmail} variant="primary" onClick={handleConfirm}>{t('common.account.sendVerificationButton')}</Button>
<Button className="w-full" onClick={props.onCancel}>{t('common.operation.cancel')}</Button>
</div>
</>
)
}

View File

@ -1,14 +1,14 @@
'use client'
import { useTranslation } from 'react-i18next'
import { useCallback, useState } from 'react'
import { useRouter } from 'next/navigation'
import { useDeleteAccountFeedback } from '../state'
import { useAppContext } from '@/context/app-context'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import CustomDialog from '@/app/components/base/dialog'
import Textarea from '@/app/components/base/textarea'
import Toast from '@/app/components/base/toast'
import { useAppContext } from '@/context/app-context'
import { useLogout } from '@/service/use-common'
import { useDeleteAccountFeedback } from '../state'
type DeleteAccountProps = {
onCancel: () => void
@ -46,20 +46,27 @@ export default function FeedBack(props: DeleteAccountProps) {
props.onCancel()
handleSuccess()
}, [handleSuccess, props])
return <CustomDialog
show={true}
onClose={props.onCancel}
title={t('common.account.feedbackTitle')}
className="max-w-[480px]"
footer={false}
>
<label className='system-sm-semibold mb-1 mt-3 flex items-center text-text-secondary'>{t('common.account.feedbackLabel')}</label>
<Textarea rows={6} value={userFeedback} placeholder={t('common.account.feedbackPlaceholder') as string} onChange={(e) => {
setUserFeedback(e.target.value)
}} />
<div className='mt-3 flex w-full flex-col gap-2'>
<Button className='w-full' loading={isPending} variant='primary' onClick={handleSubmit}>{t('common.operation.submit')}</Button>
<Button className='w-full' onClick={handleSkip}>{t('common.operation.skip')}</Button>
</div>
</CustomDialog>
return (
<CustomDialog
show={true}
onClose={props.onCancel}
title={t('common.account.feedbackTitle')}
className="max-w-[480px]"
footer={false}
>
<label className="system-sm-semibold mb-1 mt-3 flex items-center text-text-secondary">{t('common.account.feedbackLabel')}</label>
<Textarea
rows={6}
value={userFeedback}
placeholder={t('common.account.feedbackPlaceholder') as string}
onChange={(e) => {
setUserFeedback(e.target.value)
}}
/>
<div className="mt-3 flex w-full flex-col gap-2">
<Button className="w-full" loading={isPending} variant="primary" onClick={handleSubmit}>{t('common.operation.submit')}</Button>
<Button className="w-full" onClick={handleSkip}>{t('common.operation.skip')}</Button>
</div>
</CustomDialog>
)
}

View File

@ -1,13 +1,13 @@
'use client'
import { useTranslation } from 'react-i18next'
import { useCallback, useEffect, useState } from 'react'
import Link from 'next/link'
import { useAccountDeleteStore, useConfirmDeleteAccount, useSendDeleteAccountEmail } from '../state'
import Input from '@/app/components/base/input'
import { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import Countdown from '@/app/components/signin/countdown'
import { useAccountDeleteStore, useConfirmDeleteAccount, useSendDeleteAccountEmail } from '../state'
const CODE_EXP = /[A-Za-z\d]{6}/gi
const CODE_EXP = /[A-Z\d]{6}/gi
type DeleteAccountProps = {
onCancel: () => void
@ -34,22 +34,29 @@ export default function VerifyEmail(props: DeleteAccountProps) {
}
catch (error) { console.error(error) }
}, [emailToken, verificationCode, confirmDeleteAccount, props])
return <>
<div className='body-md-medium pt-1 text-text-destructive'>
{t('common.account.deleteTip')}
</div>
<div className='body-md-regular pb-2 pt-1 text-text-secondary'>
{t('common.account.deletePrivacyLinkTip')}
<Link href='https://dify.ai/privacy' className='text-text-accent'>{t('common.account.deletePrivacyLink')}</Link>
</div>
<label className='system-sm-semibold mb-1 mt-3 flex h-6 items-center text-text-secondary'>{t('common.account.verificationLabel')}</label>
<Input minLength={6} maxLength={6} placeholder={t('common.account.verificationPlaceholder') as string} onChange={(e) => {
setVerificationCode(e.target.value)
}} />
<div className='mt-3 flex w-full flex-col gap-2'>
<Button className='w-full' disabled={shouldButtonDisabled} loading={isDeleting} variant='warning' onClick={handleConfirm}>{t('common.account.permanentlyDeleteButton')}</Button>
<Button className='w-full' onClick={props.onCancel}>{t('common.operation.cancel')}</Button>
<Countdown onResend={sendEmail} />
</div>
</>
return (
<>
<div className="body-md-medium pt-1 text-text-destructive">
{t('common.account.deleteTip')}
</div>
<div className="body-md-regular pb-2 pt-1 text-text-secondary">
{t('common.account.deletePrivacyLinkTip')}
<Link href="https://dify.ai/privacy" className="text-text-accent">{t('common.account.deletePrivacyLink')}</Link>
</div>
<label className="system-sm-semibold mb-1 mt-3 flex h-6 items-center text-text-secondary">{t('common.account.verificationLabel')}</label>
<Input
minLength={6}
maxLength={6}
placeholder={t('common.account.verificationPlaceholder') as string}
onChange={(e) => {
setVerificationCode(e.target.value)
}}
/>
<div className="mt-3 flex w-full flex-col gap-2">
<Button className="w-full" disabled={shouldButtonDisabled} loading={isDeleting} variant="warning" onClick={handleConfirm}>{t('common.account.permanentlyDeleteButton')}</Button>
<Button className="w-full" onClick={props.onCancel}>{t('common.operation.cancel')}</Button>
<Countdown onResend={sendEmail} />
</div>
</>
)
}

View File

@ -1,11 +1,11 @@
'use client'
import { useTranslation } from 'react-i18next'
import { useCallback, useState } from 'react'
import CheckEmail from './components/check-email'
import VerifyEmail from './components/verify-email'
import FeedBack from './components/feed-back'
import { useTranslation } from 'react-i18next'
import CustomDialog from '@/app/components/base/dialog'
import { COUNT_DOWN_KEY, COUNT_DOWN_TIME_MS } from '@/app/components/signin/countdown'
import CheckEmail from './components/check-email'
import FeedBack from './components/feed-back'
import VerifyEmail from './components/verify-email'
type DeleteAccountProps = {
onCancel: () => void
@ -29,16 +29,23 @@ export default function DeleteAccount(props: DeleteAccountProps) {
if (showFeedbackDialog)
return <FeedBack onCancel={props.onCancel} onConfirm={props.onConfirm} />
return <CustomDialog
show={true}
onClose={props.onCancel}
title={t('common.account.delete')}
className="max-w-[480px]"
footer={false}
>
{!showVerifyEmail && <CheckEmail onCancel={props.onCancel} onConfirm={handleEmailCheckSuccess} />}
{showVerifyEmail && <VerifyEmail onCancel={props.onCancel} onConfirm={() => {
setShowFeedbackDialog(true)
}} />}
</CustomDialog>
return (
<CustomDialog
show={true}
onClose={props.onCancel}
title={t('common.account.delete')}
className="max-w-[480px]"
footer={false}
>
{!showVerifyEmail && <CheckEmail onCancel={props.onCancel} onConfirm={handleEmailCheckSuccess} />}
{showVerifyEmail && (
<VerifyEmail
onCancel={props.onCancel}
onConfirm={() => {
setShowFeedbackDialog(true)
}}
/>
)}
</CustomDialog>
)
}

View File

@ -1,10 +1,10 @@
'use client'
import { useTranslation } from 'react-i18next'
import { RiArrowRightUpLine, RiRobot2Line } from '@remixicon/react'
import { useRouter } from 'next/navigation'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import DifyLogo from '@/app/components/base/logo/dify-logo'
import { useCallback } from 'react'
import { useGlobalPublicStore } from '@/context/global-public-context'
import Avatar from './avatar'
@ -18,27 +18,29 @@ const Header = () => {
}, [router])
return (
<div className='flex flex-1 items-center justify-between px-4'>
<div className='flex items-center gap-3'>
<div className='flex cursor-pointer items-center' onClick={goToStudio}>
<div className="flex flex-1 items-center justify-between px-4">
<div className="flex items-center gap-3">
<div className="flex cursor-pointer items-center" onClick={goToStudio}>
{systemFeatures.branding.enabled && systemFeatures.branding.login_page_logo
? <img
src={systemFeatures.branding.login_page_logo}
className='block h-[22px] w-auto object-contain'
alt='Dify logo'
/>
? (
<img
src={systemFeatures.branding.login_page_logo}
className="block h-[22px] w-auto object-contain"
alt="Dify logo"
/>
)
: <DifyLogo />}
</div>
<div className='h-4 w-[1px] origin-center rotate-[11.31deg] bg-divider-regular' />
<p className='title-3xl-semi-bold relative mt-[-2px] text-text-primary'>{t('common.account.account')}</p>
<div className="h-4 w-[1px] origin-center rotate-[11.31deg] bg-divider-regular" />
<p className="title-3xl-semi-bold relative mt-[-2px] text-text-primary">{t('common.account.account')}</p>
</div>
<div className='flex shrink-0 items-center gap-3'>
<Button className='system-sm-medium gap-2 px-3 py-2' onClick={goToStudio}>
<RiRobot2Line className='h-4 w-4' />
<div className="flex shrink-0 items-center gap-3">
<Button className="system-sm-medium gap-2 px-3 py-2" onClick={goToStudio}>
<RiRobot2Line className="h-4 w-4" />
<p>{t('common.account.studio')}</p>
<RiArrowRightUpLine className='h-4 w-4' />
<RiArrowRightUpLine className="h-4 w-4" />
</Button>
<div className='h-4 w-[1px] bg-divider-regular' />
<div className="h-4 w-[1px] bg-divider-regular" />
<Avatar />
</div>
</div>

View File

@ -1,14 +1,14 @@
import React from 'react'
import type { ReactNode } from 'react'
import Header from './header'
import React from 'react'
import AmplitudeProvider from '@/app/components/base/amplitude'
import GA, { GaType } from '@/app/components/base/ga'
import HeaderWrapper from '@/app/components/header/header-wrapper'
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'
import { ModalContextProvider } from '@/context/modal-context'
import { ProviderContextProvider } from '@/context/provider-context'
import Header from './header'
const Layout = ({ children }: { children: ReactNode }) => {
return (
@ -23,7 +23,7 @@ const Layout = ({ children }: { children: ReactNode }) => {
<HeaderWrapper>
<Header />
</HeaderWrapper>
<div className='relative flex h-0 shrink-0 grow flex-col overflow-y-auto bg-components-panel-bg'>
<div className="relative flex h-0 shrink-0 grow flex-col overflow-y-auto bg-components-panel-bg">
{children}
</div>
</ModalContextProvider>

View File

@ -1,12 +1,14 @@
'use client'
import { useTranslation } from 'react-i18next'
import AccountPage from './account-page'
import useDocumentTitle from '@/hooks/use-document-title'
import AccountPage from './account-page'
export default function Account() {
const { t } = useTranslation()
useDocumentTitle(t('common.menus.account'))
return <div className='mx-auto w-full max-w-[640px] px-6 pt-12'>
<AccountPage />
</div>
return (
<div className="mx-auto w-full max-w-[640px] px-6 pt-12">
<AccountPage />
</div>
)
}

View File

@ -1,12 +1,12 @@
'use client'
import Header from '@/app/signin/_header'
import Loading from '@/app/components/base/loading'
import { cn } from '@/utils/classnames'
import Header from '@/app/signin/_header'
import { AppContextProvider } from '@/context/app-context'
import { useGlobalPublicStore } from '@/context/global-public-context'
import useDocumentTitle from '@/hooks/use-document-title'
import { AppContextProvider } from '@/context/app-context'
import { useIsLogin } from '@/service/use-common'
import Loading from '@/app/components/base/loading'
import { cn } from '@/utils/classnames'
export default function SignInLayout({ children }: any) {
const { systemFeatures } = useGlobalPublicStore()
@ -14,29 +14,40 @@ export default function SignInLayout({ children }: any) {
const { isLoading, data: loginData } = useIsLogin()
const isLoggedIn = loginData?.logged_in
if(isLoading) {
if (isLoading) {
return (
<div className='flex min-h-screen w-full justify-center bg-background-default-burn'>
<div className="flex min-h-screen w-full justify-center bg-background-default-burn">
<Loading />
</div>
)
}
return <>
<div className={cn('flex min-h-screen w-full justify-center bg-background-default-burn p-6')}>
<div className={cn('flex w-full shrink-0 flex-col items-center rounded-2xl border border-effects-highlight bg-background-default-subtle')}>
<Header />
<div className={cn('flex w-full grow flex-col items-center justify-center px-6 md:px-[108px]')}>
<div className='flex flex-col md:w-[400px]'>
{isLoggedIn ? <AppContextProvider>
{children}
</AppContextProvider>
: children}
return (
<>
<div className={cn('flex min-h-screen w-full justify-center bg-background-default-burn p-6')}>
<div className={cn('flex w-full shrink-0 flex-col items-center rounded-2xl border border-effects-highlight bg-background-default-subtle')}>
<Header />
<div className={cn('flex w-full grow flex-col items-center justify-center px-6 md:px-[108px]')}>
<div className="flex flex-col md:w-[400px]">
{isLoggedIn
? (
<AppContextProvider>
{children}
</AppContextProvider>
)
: children}
</div>
</div>
{systemFeatures.branding.enabled === false && (
<div className="system-xs-regular px-8 py-6 text-text-tertiary">
©
{' '}
{new Date().getFullYear()}
{' '}
LangGenius, Inc. All rights reserved.
</div>
)}
</div>
{systemFeatures.branding.enabled === false && <div className='system-xs-regular px-8 py-6 text-text-tertiary'>
© {new Date().getFullYear()} LangGenius, Inc. All rights reserved.
</div>}
</div>
</div>
</>
</>
)
}

View File

@ -1,15 +1,5 @@
'use client'
import React, { useEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { useRouter, useSearchParams } from 'next/navigation'
import Button from '@/app/components/base/button'
import Avatar from '@/app/components/base/avatar'
import Loading from '@/app/components/base/loading'
import Toast from '@/app/components/base/toast'
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { useAppContext } from '@/context/app-context'
import { useAuthorizeOAuthApp, useOAuthAppInfo } from '@/service/use-oauth'
import {
RiAccountCircleLine,
RiGlobalLine,
@ -18,7 +8,17 @@ import {
RiTranslate2,
} from '@remixicon/react'
import dayjs from 'dayjs'
import { useRouter, useSearchParams } from 'next/navigation'
import React, { useEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import Avatar from '@/app/components/base/avatar'
import Button from '@/app/components/base/button'
import Loading from '@/app/components/base/loading'
import Toast from '@/app/components/base/toast'
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { useAppContext } from '@/context/app-context'
import { useIsLogin } from '@/service/use-common'
import { useAuthorizeOAuthApp, useOAuthAppInfo } from '@/service/use-oauth'
import {
OAUTH_AUTHORIZE_PENDING_KEY,
OAUTH_AUTHORIZE_PENDING_TTL,
@ -124,49 +124,49 @@ export default function OAuthAuthorize() {
if (isLoading) {
return (
<div className='bg-background-default-subtle'>
<Loading type='app' />
<div className="bg-background-default-subtle">
<Loading type="app" />
</div>
)
}
return (
<div className='bg-background-default-subtle'>
<div className="bg-background-default-subtle">
{authAppInfo?.app_icon && (
<div className='w-max rounded-2xl border-[0.5px] border-components-panel-border bg-text-primary-on-surface p-3 shadow-lg'>
<img src={authAppInfo.app_icon} alt='app icon' className='h-10 w-10 rounded' />
<div className="w-max rounded-2xl border-[0.5px] border-components-panel-border bg-text-primary-on-surface p-3 shadow-lg">
<img src={authAppInfo.app_icon} alt="app icon" className="h-10 w-10 rounded" />
</div>
)}
<div className={`mb-4 mt-5 flex flex-col gap-2 ${isLoggedIn ? 'pb-2' : ''}`}>
<div className='title-4xl-semi-bold'>
{isLoggedIn && <div className='text-text-primary'>{t('oauth.connect')}</div>}
<div className='text-[var(--color-saas-dify-blue-inverted)]'>{authAppInfo?.app_label[language] || authAppInfo?.app_label?.en_US || t('oauth.unknownApp')}</div>
{!isLoggedIn && <div className='text-text-primary'>{t('oauth.tips.notLoggedIn')}</div>}
<div className="title-4xl-semi-bold">
{isLoggedIn && <div className="text-text-primary">{t('oauth.connect')}</div>}
<div className="text-[var(--color-saas-dify-blue-inverted)]">{authAppInfo?.app_label[language] || authAppInfo?.app_label?.en_US || t('oauth.unknownApp')}</div>
{!isLoggedIn && <div className="text-text-primary">{t('oauth.tips.notLoggedIn')}</div>}
</div>
<div className='body-md-regular text-text-secondary'>{isLoggedIn ? `${authAppInfo?.app_label[language] || authAppInfo?.app_label?.en_US || t('oauth.unknownApp')} ${t('oauth.tips.loggedIn')}` : t('oauth.tips.needLogin')}</div>
<div className="body-md-regular text-text-secondary">{isLoggedIn ? `${authAppInfo?.app_label[language] || authAppInfo?.app_label?.en_US || t('oauth.unknownApp')} ${t('oauth.tips.loggedIn')}` : t('oauth.tips.needLogin')}</div>
</div>
{isLoggedIn && userProfile && (
<div className='flex items-center justify-between rounded-xl bg-background-section-burn-inverted p-3'>
<div className='flex items-center gap-2.5'>
<div className="flex items-center justify-between rounded-xl bg-background-section-burn-inverted p-3">
<div className="flex items-center gap-2.5">
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} size={36} />
<div>
<div className='system-md-semi-bold text-text-secondary'>{userProfile.name}</div>
<div className='system-xs-regular text-text-tertiary'>{userProfile.email}</div>
<div className="system-md-semi-bold text-text-secondary">{userProfile.name}</div>
<div className="system-xs-regular text-text-tertiary">{userProfile.email}</div>
</div>
</div>
<Button variant='tertiary' size='small' onClick={onLoginSwitchClick}>{t('oauth.switchAccount')}</Button>
<Button variant="tertiary" size="small" onClick={onLoginSwitchClick}>{t('oauth.switchAccount')}</Button>
</div>
)}
{isLoggedIn && Boolean(authAppInfo?.scope) && (
<div className='mt-2 flex flex-col gap-2.5 rounded-xl bg-background-section-burn-inverted px-[22px] py-5 text-text-secondary'>
<div className="mt-2 flex flex-col gap-2.5 rounded-xl bg-background-section-burn-inverted px-[22px] py-5 text-text-secondary">
{authAppInfo!.scope.split(/\s+/).filter(Boolean).map((scope: string) => {
const Icon = SCOPE_INFO_MAP[scope]
return (
<div key={scope} className='body-sm-medium flex items-center gap-2 text-text-secondary'>
{Icon ? <Icon.icon className='h-4 w-4' /> : <RiAccountCircleLine className='h-4 w-4' />}
<div key={scope} className="body-sm-medium flex items-center gap-2 text-text-secondary">
{Icon ? <Icon.icon className="h-4 w-4" /> : <RiAccountCircleLine className="h-4 w-4" />}
{Icon.label}
</div>
)
@ -174,17 +174,19 @@ export default function OAuthAuthorize() {
</div>
)}
<div className='flex flex-col items-center gap-2 pt-4'>
{!isLoggedIn ? (
<Button variant='primary' size='large' className='w-full' onClick={onLoginSwitchClick}>{t('oauth.login')}</Button>
) : (
<>
<Button variant='primary' size='large' className='w-full' onClick={onAuthorize} disabled={!client_id || !redirect_uri || isError || authorizing} loading={authorizing}>{t('oauth.continue')}</Button>
<Button size='large' className='w-full' onClick={() => router.push('/apps')}>{t('common.operation.cancel')}</Button>
</>
)}
<div className="flex flex-col items-center gap-2 pt-4">
{!isLoggedIn
? (
<Button variant="primary" size="large" className="w-full" onClick={onLoginSwitchClick}>{t('oauth.login')}</Button>
)
: (
<>
<Button variant="primary" size="large" className="w-full" onClick={onAuthorize} disabled={!client_id || !redirect_uri || isError || authorizing} loading={authorizing}>{t('oauth.continue')}</Button>
<Button size="large" className="w-full" onClick={() => router.push('/apps')}>{t('common.operation.cancel')}</Button>
</>
)}
</div>
<div className='mt-4 py-2'>
<div className="mt-4 py-2">
<svg xmlns="http://www.w3.org/2000/svg" width="400" height="1" viewBox="0 0 400 1" fill="none">
<path d="M0 0.5H400" stroke="url(#paint0_linear_2_5904)" />
<defs>
@ -196,7 +198,7 @@ export default function OAuthAuthorize() {
</defs>
</svg>
</div>
<div className='system-xs-regular mt-3 text-text-tertiary'>{t('oauth.tips.common')}</div>
<div className="system-xs-regular mt-3 text-text-tertiary">{t('oauth.tips.common')}</div>
</div>
)
}

View File

@ -1,13 +1,13 @@
'use client'
import { useRouter, useSearchParams } from 'next/navigation'
import { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { useRouter, useSearchParams } from 'next/navigation'
import { cn } from '@/utils/classnames'
import Button from '@/app/components/base/button'
import Loading from '@/app/components/base/loading'
import useDocumentTitle from '@/hooks/use-document-title'
import { useInvitationCheck } from '@/service/use-common'
import { cn } from '@/utils/classnames'
const ActivateForm = () => {
useDocumentTitle('')
@ -49,7 +49,8 @@ const ActivateForm = () => {
'px-6',
'md:px-[108px]',
)
}>
}
>
{!checkRes && <Loading />}
{checkRes && !checkRes.is_valid && (
<div className="flex flex-col md:w-[400px]">
@ -58,7 +59,7 @@ const ActivateForm = () => {
<h2 className="text-[32px] font-bold text-text-primary">{t('login.invalid')}</h2>
</div>
<div className="mx-auto mt-6 w-full">
<Button variant='primary' className='w-full !text-sm'>
<Button variant="primary" className="w-full !text-sm">
<a href="https://dify.ai">{t('login.explore')}</a>
</Button>
</div>

View File

@ -1,9 +1,9 @@
'use client'
import React from 'react'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { cn } from '@/utils/classnames'
import Header from '../signin/_header'
import ActivateForm from './activateForm'
import { cn } from '@/utils/classnames'
import { useGlobalPublicStore } from '@/context/global-public-context'
const Activate = () => {
const { systemFeatures } = useGlobalPublicStore()
@ -12,9 +12,15 @@ const Activate = () => {
<div className={cn('flex w-full shrink-0 flex-col rounded-2xl border border-effects-highlight bg-background-default-subtle')}>
<Header />
<ActivateForm />
{!systemFeatures.branding.enabled && <div className='px-8 py-6 text-sm font-normal text-text-tertiary'>
© {new Date().getFullYear()} LangGenius, Inc. All rights reserved.
</div>}
{!systemFeatures.branding.enabled && (
<div className="px-8 py-6 text-sm font-normal text-text-tertiary">
©
{' '}
{new Date().getFullYear()}
{' '}
LangGenius, Inc. All rights reserved.
</div>
)}
</div>
</div>
)

View File

@ -1,7 +1,7 @@
import { useTranslation } from 'react-i18next'
import { useRouter } from 'next/navigation'
import { useContext } from 'use-context-selector'
import React, { useCallback, useState } from 'react'
import type { Operation } from './app-operations'
import type { DuplicateAppModalProps } from '@/app/components/app/duplicate-modal'
import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal'
import type { EnvironmentVariable } from '@/app/components/workflow/types'
import {
RiDeleteBinLine,
RiEditLine,
@ -11,26 +11,26 @@ import {
RiFileDownloadLine,
RiFileUploadLine,
} from '@remixicon/react'
import AppIcon from '../base/app-icon'
import dynamic from 'next/dynamic'
import { useRouter } from 'next/navigation'
import React, { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import CardView from '@/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view'
import { useStore as useAppStore } from '@/app/components/app/store'
import Button from '@/app/components/base/button'
import ContentDialog from '@/app/components/base/content-dialog'
import { ToastContext } from '@/app/components/base/toast'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import { useAppContext } from '@/context/app-context'
import { useProviderContext } from '@/context/provider-context'
import { copyApp, deleteApp, exportAppConfig, updateAppInfo } from '@/service/apps'
import type { DuplicateAppModalProps } from '@/app/components/app/duplicate-modal'
import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import { getRedirection } from '@/utils/app-redirection'
import type { EnvironmentVariable } from '@/app/components/workflow/types'
import { fetchWorkflowDraft } from '@/service/workflow'
import ContentDialog from '@/app/components/base/content-dialog'
import Button from '@/app/components/base/button'
import CardView from '@/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view'
import type { Operation } from './app-operations'
import AppOperations from './app-operations'
import dynamic from 'next/dynamic'
import { cn } from '@/utils/classnames'
import { AppModeEnum } from '@/types/app'
import { getRedirection } from '@/utils/app-redirection'
import { cn } from '@/utils/classnames'
import AppIcon from '../base/app-icon'
import AppOperations from './app-operations'
const SwitchAppModal = dynamic(() => import('@/app/components/app/switch-app-modal'), {
ssr: false,
@ -239,16 +239,18 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
const secondaryOperations: Operation[] = [
// Import DSL (conditional)
...(appDetail.mode === AppModeEnum.ADVANCED_CHAT || appDetail.mode === AppModeEnum.WORKFLOW) ? [{
id: 'import',
title: t('workflow.common.importDSL'),
icon: <RiFileUploadLine />,
onClick: () => {
setOpen(false)
onDetailExpand?.(false)
setShowImportDSLModal(true)
},
}] : [],
...(appDetail.mode === AppModeEnum.ADVANCED_CHAT || appDetail.mode === AppModeEnum.WORKFLOW)
? [{
id: 'import',
title: t('workflow.common.importDSL'),
icon: <RiFileUploadLine />,
onClick: () => {
setOpen(false)
onDetailExpand?.(false)
setShowImportDSLModal(true)
},
}]
: [],
// Divider
{
id: 'divider-1',
@ -271,29 +273,32 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
]
// Keep the switch operation separate as it's not part of the main operations
const switchOperation = (appDetail.mode === AppModeEnum.COMPLETION || appDetail.mode === AppModeEnum.CHAT) ? {
id: 'switch',
title: t('app.switch'),
icon: <RiExchange2Line />,
onClick: () => {
setOpen(false)
onDetailExpand?.(false)
setShowSwitchModal(true)
},
} : null
const switchOperation = (appDetail.mode === AppModeEnum.COMPLETION || appDetail.mode === AppModeEnum.CHAT)
? {
id: 'switch',
title: t('app.switch'),
icon: <RiExchange2Line />,
onClick: () => {
setOpen(false)
onDetailExpand?.(false)
setShowSwitchModal(true)
},
}
: null
return (
<div>
{!onlyShowDetail && (
<button type="button"
<button
type="button"
onClick={() => {
if (isCurrentWorkspaceEditor)
setOpen(v => !v)
}}
className='block w-full'
className="block w-full"
>
<div className='flex flex-col gap-2 rounded-lg p-1 hover:bg-state-base-hover'>
<div className='flex items-center gap-1'>
<div className="flex flex-col gap-2 rounded-lg p-1 hover:bg-state-base-hover">
<div className="flex items-center gap-1">
<div className={cn(!expand && 'ml-1')}>
<AppIcon
size={expand ? 'large' : 'small'}
@ -304,31 +309,36 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
/>
</div>
{expand && (
<div className='ml-auto flex items-center justify-center rounded-md p-0.5'>
<div className='flex h-5 w-5 items-center justify-center'>
<RiEqualizer2Line className='h-4 w-4 text-text-tertiary' />
<div className="ml-auto flex items-center justify-center rounded-md p-0.5">
<div className="flex h-5 w-5 items-center justify-center">
<RiEqualizer2Line className="h-4 w-4 text-text-tertiary" />
</div>
</div>
)}
</div>
{!expand && (
<div className='flex items-center justify-center'>
<div className='flex h-5 w-5 items-center justify-center rounded-md p-0.5'>
<RiEqualizer2Line className='h-4 w-4 text-text-tertiary' />
<div className="flex items-center justify-center">
<div className="flex h-5 w-5 items-center justify-center rounded-md p-0.5">
<RiEqualizer2Line className="h-4 w-4 text-text-tertiary" />
</div>
</div>
)}
{expand && (
<div className='flex flex-col items-start gap-1'>
<div className='flex w-full'>
<div className='system-md-semibold truncate whitespace-nowrap text-text-secondary'>{appDetail.name}</div>
<div className="flex flex-col items-start gap-1">
<div className="flex w-full">
<div className="system-md-semibold truncate whitespace-nowrap text-text-secondary">{appDetail.name}</div>
</div>
<div className="system-2xs-medium-uppercase whitespace-nowrap text-text-tertiary">
{appDetail.mode === AppModeEnum.ADVANCED_CHAT
? t('app.types.advanced')
: appDetail.mode === AppModeEnum.AGENT_CHAT
? t('app.types.agent')
: appDetail.mode === AppModeEnum.CHAT
? t('app.types.chatbot')
: appDetail.mode === AppModeEnum.COMPLETION
? t('app.types.completion')
: t('app.types.workflow')}
</div>
<div className='system-2xs-medium-uppercase whitespace-nowrap text-text-tertiary'>
{appDetail.mode === AppModeEnum.ADVANCED_CHAT ? t('app.types.advanced')
: appDetail.mode === AppModeEnum.AGENT_CHAT ? t('app.types.agent')
: appDetail.mode === AppModeEnum.CHAT ? t('app.types.chatbot')
: appDetail.mode === AppModeEnum.COMPLETION ? t('app.types.completion')
: t('app.types.workflow')}</div>
</div>
)}
</div>
@ -340,25 +350,25 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
setOpen(false)
onDetailExpand?.(false)
}}
className='absolute bottom-2 left-2 top-2 flex w-[420px] flex-col rounded-2xl !p-0'
className="absolute bottom-2 left-2 top-2 flex w-[420px] flex-col rounded-2xl !p-0"
>
<div className='flex shrink-0 flex-col items-start justify-center gap-3 self-stretch p-4'>
<div className='flex items-center gap-3 self-stretch'>
<div className="flex shrink-0 flex-col items-start justify-center gap-3 self-stretch p-4">
<div className="flex items-center gap-3 self-stretch">
<AppIcon
size='large'
size="large"
iconType={appDetail.icon_type}
icon={appDetail.icon}
background={appDetail.icon_background}
imageUrl={appDetail.icon_url}
/>
<div className='flex flex-1 flex-col items-start justify-center overflow-hidden'>
<div className='system-md-semibold w-full truncate text-text-secondary'>{appDetail.name}</div>
<div className='system-2xs-medium-uppercase text-text-tertiary'>{appDetail.mode === AppModeEnum.ADVANCED_CHAT ? t('app.types.advanced') : appDetail.mode === AppModeEnum.AGENT_CHAT ? t('app.types.agent') : appDetail.mode === AppModeEnum.CHAT ? t('app.types.chatbot') : appDetail.mode === AppModeEnum.COMPLETION ? t('app.types.completion') : t('app.types.workflow')}</div>
<div className="flex flex-1 flex-col items-start justify-center overflow-hidden">
<div className="system-md-semibold w-full truncate text-text-secondary">{appDetail.name}</div>
<div className="system-2xs-medium-uppercase text-text-tertiary">{appDetail.mode === AppModeEnum.ADVANCED_CHAT ? t('app.types.advanced') : appDetail.mode === AppModeEnum.AGENT_CHAT ? t('app.types.agent') : appDetail.mode === AppModeEnum.CHAT ? t('app.types.chatbot') : appDetail.mode === AppModeEnum.COMPLETION ? t('app.types.completion') : t('app.types.workflow')}</div>
</div>
</div>
{/* description */}
{appDetail.description && (
<div className='system-xs-regular overflow-wrap-anywhere max-h-[105px] w-full max-w-full overflow-y-auto whitespace-normal break-words text-text-tertiary'>{appDetail.description}</div>
<div className="system-xs-regular overflow-wrap-anywhere max-h-[105px] w-full max-w-full overflow-y-auto whitespace-normal break-words text-text-tertiary">{appDetail.description}</div>
)}
{/* operations */}
<AppOperations
@ -370,19 +380,19 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
<CardView
appId={appDetail.id}
isInPanel={true}
className='flex flex-1 flex-col gap-2 overflow-auto px-2 py-1'
className="flex flex-1 flex-col gap-2 overflow-auto px-2 py-1"
/>
{/* Switch operation (if available) */}
{switchOperation && (
<div className='flex min-h-fit shrink-0 flex-col items-start justify-center gap-3 self-stretch pb-2'>
<div className="flex min-h-fit shrink-0 flex-col items-start justify-center gap-3 self-stretch pb-2">
<Button
size={'medium'}
variant={'ghost'}
className='gap-0.5'
size="medium"
variant="ghost"
className="gap-0.5"
onClick={switchOperation.onClick}
>
{switchOperation.icon}
<span className='system-sm-medium text-text-tertiary'>{switchOperation.title}</span>
<span className="system-sm-medium text-text-tertiary">{switchOperation.title}</span>
</Button>
</div>
)}

View File

@ -1,9 +1,9 @@
import type { JSX } from 'react'
import { RiMoreLine } from '@remixicon/react'
import { cloneElement, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '../base/portal-to-follow-elem'
import { RiMoreLine } from '@remixicon/react'
export type Operation = {
id: string
@ -91,7 +91,8 @@ const AppOperations = ({
for (let i = 0; i < childrens.length; i++) {
const child = childrens[i] as HTMLElement
const id = child.dataset.targetid
if (!id) break
if (!id)
break
const childWidth = child.clientWidth
if (width + gap + childWidth + moreWidth <= containerWidth) {
@ -127,8 +128,8 @@ const AppOperations = ({
<Button
key={operation.id}
data-targetid={operation.id}
size={'small'}
variant={'secondary'}
size="small"
variant="secondary"
className="gap-[1px]"
tabIndex={-1}
>
@ -140,8 +141,8 @@ const AppOperations = ({
))}
<Button
id="more-measure"
size={'small'}
variant={'secondary'}
size="small"
variant="secondary"
className="gap-[1px]"
tabIndex={-1}
>
@ -156,8 +157,8 @@ const AppOperations = ({
<Button
key={operation.id}
data-targetid={operation.id}
size={'small'}
variant={'secondary'}
size="small"
variant="secondary"
className="gap-[1px]"
onClick={operation.onClick}
>
@ -176,8 +177,8 @@ const AppOperations = ({
>
<PortalToFollowElemTrigger onClick={handleTriggerMore}>
<Button
size={'small'}
variant={'secondary'}
size="small"
variant="secondary"
className="gap-[1px]"
>
<RiMoreLine className="h-3.5 w-3.5 text-components-button-secondary-text" />
@ -190,18 +191,18 @@ const AppOperations = ({
<div className="flex min-w-[264px] flex-col rounded-[12px] border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg backdrop-blur-[5px]">
{moreOperations.map(item => item.type === 'divider'
? (
<div key={item.id} className="my-1 h-px bg-divider-subtle" />
)
<div key={item.id} className="my-1 h-px bg-divider-subtle" />
)
: (
<div
key={item.id}
className="flex h-8 cursor-pointer items-center gap-x-1 rounded-lg p-1.5 hover:bg-state-base-hover"
onClick={item.onClick}
>
{cloneElement(item.icon, { className: 'h-4 w-4 text-text-tertiary' })}
<span className="system-md-regular text-text-secondary">{item.title}</span>
</div>
))}
<div
key={item.id}
className="flex h-8 cursor-pointer items-center gap-x-1 rounded-lg p-1.5 hover:bg-state-base-hover"
onClick={item.onClick}
>
{cloneElement(item.icon, { className: 'h-4 w-4 text-text-tertiary' })}
<span className="system-md-regular text-text-secondary">{item.title}</span>
</div>
))}
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>

View File

@ -1,23 +1,23 @@
import React, { useCallback, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useAppContext } from '@/context/app-context'
import type { NavIcon } from './navLink'
import {
RiEqualizer2Line,
RiMenuLine,
} from '@remixicon/react'
import React, { useCallback, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useStore as useAppStore } from '@/app/components/app/store'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import { useAppContext } from '@/context/app-context'
import { AppModeEnum } from '@/types/app'
import { cn } from '@/utils/classnames'
import AppIcon from '../base/app-icon'
import Divider from '../base/divider'
import AppInfo from './app-info'
import NavLink from './navLink'
import { useStore as useAppStore } from '@/app/components/app/store'
import type { NavIcon } from './navLink'
import { cn } from '@/utils/classnames'
import { AppModeEnum } from '@/types/app'
type Props = {
navigation: Array<{
@ -49,11 +49,11 @@ const AppSidebarDropdown = ({ navigation }: Props) => {
return (
<>
<div className='fixed left-2 top-2 z-20'>
<div className="fixed left-2 top-2 z-20">
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement='bottom-start'
placement="bottom-start"
offset={{
mainAxis: -41,
}}
@ -61,18 +61,18 @@ const AppSidebarDropdown = ({ navigation }: Props) => {
<PortalToFollowElemTrigger onClick={handleTrigger}>
<div className={cn('flex cursor-pointer items-center rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-1 shadow-lg backdrop-blur-sm hover:bg-background-default-hover', open && 'bg-background-default-hover')}>
<AppIcon
size='small'
size="small"
iconType={appDetail.icon_type}
icon={appDetail.icon}
background={appDetail.icon_background}
imageUrl={appDetail.icon_url}
/>
<RiMenuLine className='h-4 w-4 text-text-tertiary' />
<RiMenuLine className="h-4 w-4 text-text-tertiary" />
</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[1000]'>
<PortalToFollowElemContent className="z-[1000]">
<div className={cn('w-[305px] rounded-xl border-[0.5px] border-components-panel-border bg-background-default-subtle shadow-lg')}>
<div className='p-2'>
<div className="p-2">
<div
className={cn('flex flex-col gap-2 rounded-lg p-2 pb-2.5', isCurrentWorkspaceEditor && 'cursor-pointer hover:bg-state-base-hover')}
onClick={() => {
@ -80,35 +80,35 @@ const AppSidebarDropdown = ({ navigation }: Props) => {
setOpen(false)
}}
>
<div className='flex items-center justify-between self-stretch'>
<div className="flex items-center justify-between self-stretch">
<AppIcon
size='large'
size="large"
iconType={appDetail.icon_type}
icon={appDetail.icon}
background={appDetail.icon_background}
imageUrl={appDetail.icon_url}
/>
<div className='flex items-center justify-center rounded-md p-0.5'>
<div className='flex h-5 w-5 items-center justify-center'>
<RiEqualizer2Line className='h-4 w-4 text-text-tertiary' />
<div className="flex items-center justify-center rounded-md p-0.5">
<div className="flex h-5 w-5 items-center justify-center">
<RiEqualizer2Line className="h-4 w-4 text-text-tertiary" />
</div>
</div>
</div>
<div className='flex flex-col items-start gap-1'>
<div className='flex w-full'>
<div className='system-md-semibold truncate text-text-secondary'>{appDetail.name}</div>
<div className="flex flex-col items-start gap-1">
<div className="flex w-full">
<div className="system-md-semibold truncate text-text-secondary">{appDetail.name}</div>
</div>
<div className='system-2xs-medium-uppercase text-text-tertiary'>{appDetail.mode === AppModeEnum.ADVANCED_CHAT ? t('app.types.advanced') : appDetail.mode === AppModeEnum.AGENT_CHAT ? t('app.types.agent') : appDetail.mode === AppModeEnum.CHAT ? t('app.types.chatbot') : appDetail.mode === AppModeEnum.COMPLETION ? t('app.types.completion') : t('app.types.workflow')}</div>
<div className="system-2xs-medium-uppercase text-text-tertiary">{appDetail.mode === AppModeEnum.ADVANCED_CHAT ? t('app.types.advanced') : appDetail.mode === AppModeEnum.AGENT_CHAT ? t('app.types.agent') : appDetail.mode === AppModeEnum.CHAT ? t('app.types.chatbot') : appDetail.mode === AppModeEnum.COMPLETION ? t('app.types.completion') : t('app.types.workflow')}</div>
</div>
</div>
</div>
<div className='px-4'>
<Divider bgStyle='gradient' />
<div className="px-4">
<Divider bgStyle="gradient" />
</div>
<nav className='space-y-0.5 px-3 pb-6 pt-4'>
<nav className="space-y-0.5 px-3 pb-6 pt-4">
{navigation.map((item, index) => {
return (
<NavLink key={index} mode='expand' iconMap={{ selected: item.selectedIcon, normal: item.icon }} name={item.name} href={item.href} />
<NavLink key={index} mode="expand" iconMap={{ selected: item.selectedIcon, normal: item.icon }} name={item.name} href={item.href} />
)
})}
</nav>
@ -116,7 +116,7 @@ const AppSidebarDropdown = ({ navigation }: Props) => {
</PortalToFollowElemContent>
</PortalToFollowElem>
</div>
<div className='z-20'>
<div className="z-20">
<AppInfo expand onlyShowDetail openState={detailExpand} onDetailExpand={setDetailExpand} />
</div>
</>

View File

@ -1,11 +1,11 @@
import React from 'react'
import { useTranslation } from 'react-i18next'
import AppIcon from '../base/app-icon'
import Tooltip from '@/app/components/base/tooltip'
import {
ApiAggregate,
WindowCursor,
} from '@/app/components/base/icons/src/vender/workflow'
import Tooltip from '@/app/components/base/tooltip'
import AppIcon from '../base/app-icon'
export type IAppBasicProps = {
iconType?: 'app' | 'api' | 'dataset' | 'webapp' | 'notion'
@ -15,39 +15,47 @@ export type IAppBasicProps = {
name: string
type: string | React.ReactNode
hoverTip?: string
textStyle?: { main?: string; extra?: string }
textStyle?: { main?: string, extra?: string }
isExtraInLine?: boolean
mode?: string
hideType?: boolean
}
const DatasetSvg = <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" clipRule="evenodd" d="M0.833497 5.13481C0.833483 4.69553 0.83347 4.31654 0.858973 4.0044C0.88589 3.67495 0.94532 3.34727 1.10598 3.03195C1.34567 2.56155 1.72812 2.17909 2.19852 1.93941C2.51384 1.77875 2.84152 1.71932 3.17097 1.6924C3.48312 1.6669 3.86209 1.66691 4.30137 1.66693L7.62238 1.66684C8.11701 1.66618 8.55199 1.66561 8.95195 1.80356C9.30227 1.92439 9.62134 2.12159 9.88607 2.38088C10.1883 2.67692 10.3823 3.06624 10.603 3.50894L11.3484 5.00008H14.3679C15.0387 5.00007 15.5924 5.00006 16.0434 5.03691C16.5118 5.07518 16.9424 5.15732 17.3468 5.36339C17.974 5.68297 18.4839 6.19291 18.8035 6.82011C19.0096 7.22456 19.0917 7.65515 19.13 8.12356C19.1668 8.57455 19.1668 9.12818 19.1668 9.79898V13.5345C19.1668 14.2053 19.1668 14.7589 19.13 15.2099C19.0917 15.6784 19.0096 16.1089 18.8035 16.5134C18.4839 17.1406 17.974 17.6505 17.3468 17.9701C16.9424 18.1762 16.5118 18.2583 16.0434 18.2966C15.5924 18.3334 15.0387 18.3334 14.3679 18.3334H5.63243C4.96163 18.3334 4.40797 18.3334 3.95698 18.2966C3.48856 18.2583 3.05798 18.1762 2.65353 17.9701C2.02632 17.6505 1.51639 17.1406 1.19681 16.5134C0.990734 16.1089 0.908597 15.6784 0.870326 15.2099C0.833478 14.7589 0.833487 14.2053 0.833497 13.5345V5.13481ZM7.51874 3.33359C8.17742 3.33359 8.30798 3.34447 8.4085 3.37914C8.52527 3.41942 8.63163 3.48515 8.71987 3.57158C8.79584 3.64598 8.86396 3.7579 9.15852 4.34704L9.48505 5.00008L2.50023 5.00008C2.50059 4.61259 2.50314 4.34771 2.5201 4.14012C2.5386 3.91374 2.57 3.82981 2.59099 3.7886C2.67089 3.6318 2.79837 3.50432 2.95517 3.42442C2.99638 3.40343 3.08031 3.37203 3.30669 3.35353C3.54281 3.33424 3.85304 3.33359 4.3335 3.33359H7.51874Z" fill="#444CE7" />
</svg>
const DatasetSvg = (
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" clipRule="evenodd" d="M0.833497 5.13481C0.833483 4.69553 0.83347 4.31654 0.858973 4.0044C0.88589 3.67495 0.94532 3.34727 1.10598 3.03195C1.34567 2.56155 1.72812 2.17909 2.19852 1.93941C2.51384 1.77875 2.84152 1.71932 3.17097 1.6924C3.48312 1.6669 3.86209 1.66691 4.30137 1.66693L7.62238 1.66684C8.11701 1.66618 8.55199 1.66561 8.95195 1.80356C9.30227 1.92439 9.62134 2.12159 9.88607 2.38088C10.1883 2.67692 10.3823 3.06624 10.603 3.50894L11.3484 5.00008H14.3679C15.0387 5.00007 15.5924 5.00006 16.0434 5.03691C16.5118 5.07518 16.9424 5.15732 17.3468 5.36339C17.974 5.68297 18.4839 6.19291 18.8035 6.82011C19.0096 7.22456 19.0917 7.65515 19.13 8.12356C19.1668 8.57455 19.1668 9.12818 19.1668 9.79898V13.5345C19.1668 14.2053 19.1668 14.7589 19.13 15.2099C19.0917 15.6784 19.0096 16.1089 18.8035 16.5134C18.4839 17.1406 17.974 17.6505 17.3468 17.9701C16.9424 18.1762 16.5118 18.2583 16.0434 18.2966C15.5924 18.3334 15.0387 18.3334 14.3679 18.3334H5.63243C4.96163 18.3334 4.40797 18.3334 3.95698 18.2966C3.48856 18.2583 3.05798 18.1762 2.65353 17.9701C2.02632 17.6505 1.51639 17.1406 1.19681 16.5134C0.990734 16.1089 0.908597 15.6784 0.870326 15.2099C0.833478 14.7589 0.833487 14.2053 0.833497 13.5345V5.13481ZM7.51874 3.33359C8.17742 3.33359 8.30798 3.34447 8.4085 3.37914C8.52527 3.41942 8.63163 3.48515 8.71987 3.57158C8.79584 3.64598 8.86396 3.7579 9.15852 4.34704L9.48505 5.00008L2.50023 5.00008C2.50059 4.61259 2.50314 4.34771 2.5201 4.14012C2.5386 3.91374 2.57 3.82981 2.59099 3.7886C2.67089 3.6318 2.79837 3.50432 2.95517 3.42442C2.99638 3.40343 3.08031 3.37203 3.30669 3.35353C3.54281 3.33424 3.85304 3.33359 4.3335 3.33359H7.51874Z" fill="#444CE7" />
</svg>
)
const NotionSvg = <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clipPath="url(#clip0_6294_13848)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.287 21.9133L1.70748 18.6999C1.08685 17.9267 0.75 16.976 0.75 15.9974V4.36124C0.75 2.89548 1.92269 1.67923 3.43553 1.57594L15.3991 0.759137C16.2682 0.699797 17.1321 0.930818 17.8461 1.41353L22.0494 4.25543C22.8018 4.76414 23.25 5.59574 23.25 6.48319V19.7124C23.25 21.1468 22.0969 22.3345 20.6157 22.4256L7.3375 23.243C6.1555 23.3158 5.01299 22.8178 4.287 21.9133Z" fill="white" />
<path d="M8.43607 10.1842V10.0318C8.43607 9.64564 8.74535 9.32537 9.14397 9.29876L12.0475 9.10491L16.0628 15.0178V9.82823L15.0293 9.69046V9.6181C15.0293 9.22739 15.3456 8.90501 15.7493 8.88433L18.3912 8.74899V9.12918C18.3912 9.30765 18.2585 9.46031 18.0766 9.49108L17.4408 9.59861V18.0029L16.6429 18.2773C15.9764 18.5065 15.2343 18.2611 14.8527 17.6853L10.9545 11.803V17.4173L12.1544 17.647L12.1377 17.7583C12.0853 18.1069 11.7843 18.3705 11.4202 18.3867L8.43607 18.5195C8.39662 18.1447 8.67758 17.8093 9.06518 17.7686L9.45771 17.7273V10.2416L8.43607 10.1842Z" fill="black" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.5062 2.22521L3.5426 3.04201C2.82599 3.09094 2.27051 3.66706 2.27051 4.36136V15.9975C2.27051 16.6499 2.49507 17.2837 2.90883 17.7992L5.48835 21.0126C5.90541 21.5322 6.56174 21.8183 7.24076 21.7765L20.519 20.9591C21.1995 20.9172 21.7293 20.3716 21.7293 19.7125V6.48332C21.7293 6.07557 21.5234 5.69348 21.1777 5.45975L16.9743 2.61784C16.546 2.32822 16.0277 2.1896 15.5062 2.22521ZM4.13585 4.54287C3.96946 4.41968 4.04865 4.16303 4.25768 4.14804L15.5866 3.33545C15.9476 3.30956 16.3063 3.40896 16.5982 3.61578L18.8713 5.22622C18.9576 5.28736 18.9171 5.41935 18.8102 5.42516L6.8129 6.07764C6.44983 6.09739 6.09144 5.99073 5.80276 5.77699L4.13585 4.54287ZM6.25018 8.12315C6.25018 7.7334 6.56506 7.41145 6.9677 7.38952L19.6523 6.69871C20.0447 6.67734 20.375 6.97912 20.375 7.35898V18.8141C20.375 19.2031 20.0613 19.5247 19.6594 19.5476L7.05516 20.2648C6.61845 20.2896 6.25018 19.954 6.25018 19.5312V8.12315Z" fill="black" />
</g>
<defs>
<clipPath id="clip0_6294_13848">
<rect width="24" height="24" fill="white" />
</clipPath>
</defs>
</svg>
const NotionSvg = (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clipPath="url(#clip0_6294_13848)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.287 21.9133L1.70748 18.6999C1.08685 17.9267 0.75 16.976 0.75 15.9974V4.36124C0.75 2.89548 1.92269 1.67923 3.43553 1.57594L15.3991 0.759137C16.2682 0.699797 17.1321 0.930818 17.8461 1.41353L22.0494 4.25543C22.8018 4.76414 23.25 5.59574 23.25 6.48319V19.7124C23.25 21.1468 22.0969 22.3345 20.6157 22.4256L7.3375 23.243C6.1555 23.3158 5.01299 22.8178 4.287 21.9133Z" fill="white" />
<path d="M8.43607 10.1842V10.0318C8.43607 9.64564 8.74535 9.32537 9.14397 9.29876L12.0475 9.10491L16.0628 15.0178V9.82823L15.0293 9.69046V9.6181C15.0293 9.22739 15.3456 8.90501 15.7493 8.88433L18.3912 8.74899V9.12918C18.3912 9.30765 18.2585 9.46031 18.0766 9.49108L17.4408 9.59861V18.0029L16.6429 18.2773C15.9764 18.5065 15.2343 18.2611 14.8527 17.6853L10.9545 11.803V17.4173L12.1544 17.647L12.1377 17.7583C12.0853 18.1069 11.7843 18.3705 11.4202 18.3867L8.43607 18.5195C8.39662 18.1447 8.67758 17.8093 9.06518 17.7686L9.45771 17.7273V10.2416L8.43607 10.1842Z" fill="black" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.5062 2.22521L3.5426 3.04201C2.82599 3.09094 2.27051 3.66706 2.27051 4.36136V15.9975C2.27051 16.6499 2.49507 17.2837 2.90883 17.7992L5.48835 21.0126C5.90541 21.5322 6.56174 21.8183 7.24076 21.7765L20.519 20.9591C21.1995 20.9172 21.7293 20.3716 21.7293 19.7125V6.48332C21.7293 6.07557 21.5234 5.69348 21.1777 5.45975L16.9743 2.61784C16.546 2.32822 16.0277 2.1896 15.5062 2.22521ZM4.13585 4.54287C3.96946 4.41968 4.04865 4.16303 4.25768 4.14804L15.5866 3.33545C15.9476 3.30956 16.3063 3.40896 16.5982 3.61578L18.8713 5.22622C18.9576 5.28736 18.9171 5.41935 18.8102 5.42516L6.8129 6.07764C6.44983 6.09739 6.09144 5.99073 5.80276 5.77699L4.13585 4.54287ZM6.25018 8.12315C6.25018 7.7334 6.56506 7.41145 6.9677 7.38952L19.6523 6.69871C20.0447 6.67734 20.375 6.97912 20.375 7.35898V18.8141C20.375 19.2031 20.0613 19.5247 19.6594 19.5476L7.05516 20.2648C6.61845 20.2896 6.25018 19.954 6.25018 19.5312V8.12315Z" fill="black" />
</g>
<defs>
<clipPath id="clip0_6294_13848">
<rect width="24" height="24" fill="white" />
</clipPath>
</defs>
</svg>
)
const ICON_MAP = {
app: <AppIcon className='border !border-[rgba(0,0,0,0.05)]' />,
api: <div className='rounded-lg border-[0.5px] border-divider-subtle bg-util-colors-blue-brand-blue-brand-500 p-1 shadow-md'>
<ApiAggregate className='h-4 w-4 text-text-primary-on-surface' />
</div>,
dataset: <AppIcon innerIcon={DatasetSvg} className='!border-[0.5px] !border-indigo-100 !bg-indigo-25' />,
webapp: <div className='rounded-lg border-[0.5px] border-divider-subtle bg-util-colors-blue-brand-blue-brand-500 p-1 shadow-md'>
<WindowCursor className='h-4 w-4 text-text-primary-on-surface' />
</div>,
notion: <AppIcon innerIcon={NotionSvg} className='!border-[0.5px] !border-indigo-100 !bg-white' />,
app: <AppIcon className="border !border-[rgba(0,0,0,0.05)]" />,
api: (
<div className="rounded-lg border-[0.5px] border-divider-subtle bg-util-colors-blue-brand-blue-brand-500 p-1 shadow-md">
<ApiAggregate className="h-4 w-4 text-text-primary-on-surface" />
</div>
),
dataset: <AppIcon innerIcon={DatasetSvg} className="!border-[0.5px] !border-indigo-100 !bg-indigo-25" />,
webapp: (
<div className="rounded-lg border-[0.5px] border-divider-subtle bg-util-colors-blue-brand-blue-brand-500 p-1 shadow-md">
<WindowCursor className="h-4 w-4 text-text-primary-on-surface" />
</div>
),
notion: <AppIcon innerIcon={NotionSvg} className="!border-[0.5px] !border-indigo-100 !bg-white" />,
}
export default function AppBasic({ icon, icon_background, name, isExternal, type, hoverTip, textStyle, isExtraInLine, mode = 'expand', iconType = 'app', hideType }: IAppBasicProps) {
@ -56,41 +64,44 @@ export default function AppBasic({ icon, icon_background, name, isExternal, type
return (
<div className="flex grow items-center">
{icon && icon_background && iconType === 'app' && (
<div className='mr-2 shrink-0'>
<div className="mr-2 shrink-0">
<AppIcon icon={icon} background={icon_background} />
</div>
)}
{iconType !== 'app'
&& <div className='mr-2 shrink-0'>
{ICON_MAP[iconType]}
</div>
}
{mode === 'expand' && <div className="group w-full">
<div className={`system-md-semibold flex flex-row items-center text-text-secondary group-hover:text-text-primary ${textStyle?.main ?? ''}`}>
<div className="min-w-0 overflow-hidden text-ellipsis break-normal">
{name}
&& (
<div className="mr-2 shrink-0">
{ICON_MAP[iconType]}
</div>
{hoverTip
&& <Tooltip
popupContent={
<div className='w-[240px]'>
{hoverTip}
</div>
}
popupClassName='ml-1'
triggerClassName='w-4 h-4 ml-1'
position='top'
/>
}
)}
{mode === 'expand' && (
<div className="group w-full">
<div className={`system-md-semibold flex flex-row items-center text-text-secondary group-hover:text-text-primary ${textStyle?.main ?? ''}`}>
<div className="min-w-0 overflow-hidden text-ellipsis break-normal">
{name}
</div>
{hoverTip
&& (
<Tooltip
popupContent={(
<div className="w-[240px]">
{hoverTip}
</div>
)}
popupClassName="ml-1"
triggerClassName="w-4 h-4 ml-1"
position="top"
/>
)}
</div>
{!hideType && isExtraInLine && (
<div className="system-2xs-medium-uppercase flex text-text-tertiary">{type}</div>
)}
{!hideType && !isExtraInLine && (
<div className="system-2xs-medium-uppercase text-text-tertiary">{isExternal ? t('dataset.externalTag') : type}</div>
)}
</div>
{!hideType && isExtraInLine && (
<div className="system-2xs-medium-uppercase flex text-text-tertiary">{type}</div>
)}
{!hideType && !isExtraInLine && (
<div className='system-2xs-medium-uppercase text-text-tertiary'>{isExternal ? t('dataset.externalTag') : type}</div>
)}
</div>}
)}
</div>
)
}

View File

@ -1,21 +1,21 @@
import React, { useCallback, useState } from 'react'
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '../../base/portal-to-follow-elem'
import ActionButton from '../../base/action-button'
import type { DataSet } from '@/models/datasets'
import { RiMoreFill } from '@remixicon/react'
import { cn } from '@/utils/classnames'
import Menu from './menu'
import { useRouter } from 'next/navigation'
import React, { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
import type { DataSet } from '@/models/datasets'
import { checkIsUsedInApp, deleteDataset } from '@/service/datasets'
import { datasetDetailQueryKeyPrefix, useInvalidDatasetList } from '@/service/knowledge/use-dataset'
import { useInvalid } from '@/service/use-base'
import { useExportPipelineDSL } from '@/service/use-pipeline'
import Toast from '../../base/toast'
import { useTranslation } from 'react-i18next'
import RenameDatasetModal from '../../datasets/rename-modal'
import { checkIsUsedInApp, deleteDataset } from '@/service/datasets'
import { cn } from '@/utils/classnames'
import ActionButton from '../../base/action-button'
import Confirm from '../../base/confirm'
import { useRouter } from 'next/navigation'
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '../../base/portal-to-follow-elem'
import Toast from '../../base/toast'
import RenameDatasetModal from '../../datasets/rename-modal'
import Menu from './menu'
type DropDownProps = {
expand: boolean
@ -108,19 +108,21 @@ const DropDown = ({
open={open}
onOpenChange={setOpen}
placement={expand ? 'bottom-end' : 'right'}
offset={expand ? {
mainAxis: 4,
crossAxis: 10,
} : {
mainAxis: 4,
}}
offset={expand
? {
mainAxis: 4,
crossAxis: 10,
}
: {
mainAxis: 4,
}}
>
<PortalToFollowElemTrigger onClick={handleTrigger}>
<ActionButton className={cn(expand ? 'size-8 rounded-lg' : 'size-6 rounded-md')}>
<RiMoreFill className='size-4' />
<RiMoreFill className="size-4" />
</ActionButton>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[60]'>
<PortalToFollowElemContent className="z-[60]">
<Menu
showDelete={!isCurrentWorkspaceDatasetOperator}
openRenameModal={openRenameModal}

View File

@ -1,18 +1,18 @@
import React from 'react'
import type { DataSet } from '@/models/datasets'
import { RiEditLine } from '@remixicon/react'
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import DatasetInfo from './index'
import Dropdown from './dropdown'
import Menu from './menu'
import MenuItem from './menu-item'
import type { DataSet } from '@/models/datasets'
import React from 'react'
import {
ChunkingMode,
DataSourceType,
DatasetPermission,
DataSourceType,
} from '@/models/datasets'
import { RETRIEVE_METHOD } from '@/types/app'
import { RiEditLine } from '@remixicon/react'
import Dropdown from './dropdown'
import DatasetInfo from './index'
import Menu from './menu'
import MenuItem from './menu-item'
let mockDataset: DataSet
let mockIsDatasetOperator = false

View File

@ -1,14 +1,14 @@
'use client'
import type { FC } from 'react'
import type { DataSet } from '@/models/datasets'
import React, { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
import { useKnowledge } from '@/hooks/use-knowledge'
import { DOC_FORM_TEXT } from '@/models/datasets'
import { cn } from '@/utils/classnames'
import AppIcon from '../../base/app-icon'
import Effect from '../../base/effect'
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
import type { DataSet } from '@/models/datasets'
import { DOC_FORM_TEXT } from '@/models/datasets'
import { useKnowledge } from '@/hooks/use-knowledge'
import { cn } from '@/utils/classnames'
import Dropdown from './dropdown'
type DatasetInfoProps = {
@ -35,11 +35,11 @@ const DatasetInfo: FC<DatasetInfoProps> = ({
return (
<div className={cn('relative flex flex-col', expand ? '' : 'p-1')}>
{expand && (
<Effect className='-left-5 top-[-22px] opacity-15' />
<Effect className="-left-5 top-[-22px] opacity-15" />
)}
<div className='flex flex-col gap-2 p-2'>
<div className='flex items-center gap-1'>
<div className="flex flex-col gap-2 p-2">
<div className="flex items-center gap-1">
<div className={cn(!expand && '-ml-1')}>
<AppIcon
size={expand ? 'large' : 'small'}
@ -50,35 +50,35 @@ const DatasetInfo: FC<DatasetInfoProps> = ({
/>
</div>
{expand && (
<div className='ml-auto'>
<div className="ml-auto">
<Dropdown expand />
</div>
)}
</div>
{!expand && (
<div className='-mb-2 -mt-1 flex items-center justify-center'>
<div className="-mb-2 -mt-1 flex items-center justify-center">
<Dropdown expand={false} />
</div>
)}
{expand && (
<div className='flex flex-col gap-y-1 pb-0.5'>
<div className="flex flex-col gap-y-1 pb-0.5">
<div
className='system-md-semibold truncate text-text-secondary'
className="system-md-semibold truncate text-text-secondary"
title={dataset.name}
>
{dataset.name}
</div>
<div className='system-2xs-medium-uppercase text-text-tertiary'>
<div className="system-2xs-medium-uppercase text-text-tertiary">
{isExternalProvider && t('dataset.externalTag')}
{!isExternalProvider && isPipelinePublished && dataset.doc_form && dataset.indexing_technique && (
<div className='flex items-center gap-x-2'>
<div className="flex items-center gap-x-2">
<span>{t(`dataset.chunkingMode.${DOC_FORM_TEXT[dataset.doc_form]}`)}</span>
<span>{formatIndexingTechniqueAndMethod(dataset.indexing_technique, dataset.retrieval_model_dict?.search_method)}</span>
</div>
)}
</div>
{!!dataset.description && (
<p className='system-xs-regular line-clamp-3 text-text-tertiary first-letter:capitalize'>
<p className="system-xs-regular line-clamp-3 text-text-tertiary first-letter:capitalize">
{dataset.description}
</p>
)}

View File

@ -1,5 +1,5 @@
import React from 'react'
import type { RemixiconComponentType } from '@remixicon/react'
import React from 'react'
type MenuItemProps = {
name: string
@ -14,15 +14,15 @@ const MenuItem = ({
}: MenuItemProps) => {
return (
<div
className='flex items-center gap-x-1 rounded-lg px-2 py-1.5 hover:bg-state-base-hover'
className="flex items-center gap-x-1 rounded-lg px-2 py-1.5 hover:bg-state-base-hover"
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
handleClick?.()
}}
>
<Icon className='size-4 text-text-tertiary' />
<span className='system-md-regular px-1 text-text-secondary'>{name}</span>
<Icon className="size-4 text-text-tertiary" />
<span className="system-md-regular px-1 text-text-secondary">{name}</span>
</div>
)
}

View File

@ -1,9 +1,9 @@
import { RiDeleteBinLine, RiEditLine, RiFileDownloadLine } from '@remixicon/react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import MenuItem from './menu-item'
import { RiDeleteBinLine, RiEditLine, RiFileDownloadLine } from '@remixicon/react'
import Divider from '../../base/divider'
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
import Divider from '../../base/divider'
import MenuItem from './menu-item'
type MenuProps = {
showDelete: boolean
@ -22,8 +22,8 @@ const Menu = ({
const runtimeMode = useDatasetDetailContextWithSelector(state => state.dataset?.runtime_mode)
return (
<div className='flex w-[200px] flex-col rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg shadow-shadow-shadow-5 backdrop-blur-[5px]'>
<div className='flex flex-col p-1'>
<div className="flex w-[200px] flex-col rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg shadow-shadow-shadow-5 backdrop-blur-[5px]">
<div className="flex flex-col p-1">
<MenuItem
Icon={RiEditLine}
name={t('common.operation.edit')}
@ -39,8 +39,8 @@ const Menu = ({
</div>
{showDelete && (
<>
<Divider type='horizontal' className='my-0 bg-divider-subtle' />
<div className='flex flex-col p-1'>
<Divider type="horizontal" className="my-0 bg-divider-subtle" />
<div className="flex flex-col p-1">
<MenuItem
Icon={RiDeleteBinLine}
name={t('common.operation.delete')}

View File

@ -1,26 +1,26 @@
import React, { useCallback, useRef, useState } from 'react'
import type { NavIcon } from './navLink'
import type { DataSet } from '@/models/datasets'
import {
RiMenuLine,
} from '@remixicon/react'
import React, { useCallback, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
import { useKnowledge } from '@/hooks/use-knowledge'
import { DOC_FORM_TEXT } from '@/models/datasets'
import { useDatasetRelatedApps } from '@/service/knowledge/use-dataset'
import { cn } from '@/utils/classnames'
import AppIcon from '../base/app-icon'
import Divider from '../base/divider'
import NavLink from './navLink'
import type { NavIcon } from './navLink'
import { cn } from '@/utils/classnames'
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
import Effect from '../base/effect'
import Dropdown from './dataset-info/dropdown'
import type { DataSet } from '@/models/datasets'
import { DOC_FORM_TEXT } from '@/models/datasets'
import { useKnowledge } from '@/hooks/use-knowledge'
import { useTranslation } from 'react-i18next'
import { useDatasetRelatedApps } from '@/service/knowledge/use-dataset'
import ExtraInfo from '../datasets/extra-info'
import Dropdown from './dataset-info/dropdown'
import NavLink from './navLink'
type DatasetSidebarDropdownProps = {
navigation: Array<{
@ -64,11 +64,11 @@ const DatasetSidebarDropdown = ({
return (
<>
<div className='fixed left-2 top-2 z-20'>
<div className="fixed left-2 top-2 z-20">
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement='bottom-start'
placement="bottom-start"
offset={{
mainAxis: -41,
}}
@ -81,22 +81,22 @@ const DatasetSidebarDropdown = ({
)}
>
<AppIcon
size='small'
size="small"
iconType={iconInfo.icon_type}
icon={iconInfo.icon}
background={iconInfo.icon_background}
imageUrl={iconInfo.icon_url}
/>
<RiMenuLine className='size-4 text-text-tertiary' />
<RiMenuLine className="size-4 text-text-tertiary" />
</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-50'>
<div className='relative w-[216px] rounded-xl border-[0.5px] border-components-panel-border bg-background-default-subtle shadow-lg'>
<Effect className='-left-5 top-[-22px] opacity-15' />
<div className='flex flex-col gap-y-2 p-4'>
<div className='flex items-center justify-between'>
<PortalToFollowElemContent className="z-50">
<div className="relative w-[216px] rounded-xl border-[0.5px] border-components-panel-border bg-background-default-subtle shadow-lg">
<Effect className="-left-5 top-[-22px] opacity-15" />
<div className="flex flex-col gap-y-2 p-4">
<div className="flex items-center justify-between">
<AppIcon
size='medium'
size="medium"
iconType={iconInfo.icon_type}
icon={iconInfo.icon}
background={iconInfo.icon_background}
@ -104,17 +104,17 @@ const DatasetSidebarDropdown = ({
/>
<Dropdown expand />
</div>
<div className='flex flex-col gap-y-1 pb-0.5'>
<div className="flex flex-col gap-y-1 pb-0.5">
<div
className='system-md-semibold truncate text-text-secondary'
className="system-md-semibold truncate text-text-secondary"
title={dataset.name}
>
{dataset.name}
</div>
<div className='system-2xs-medium-uppercase text-text-tertiary'>
<div className="system-2xs-medium-uppercase text-text-tertiary">
{isExternalProvider && t('dataset.externalTag')}
{!isExternalProvider && dataset.doc_form && dataset.indexing_technique && (
<div className='flex items-center gap-x-2'>
<div className="flex items-center gap-x-2">
<span>{t(`dataset.chunkingMode.${DOC_FORM_TEXT[dataset.doc_form]}`)}</span>
<span>{formatIndexingTechniqueAndMethod(dataset.indexing_technique, dataset.retrieval_model_dict?.search_method)}</span>
</div>
@ -122,24 +122,24 @@ const DatasetSidebarDropdown = ({
</div>
</div>
{!!dataset.description && (
<p className='system-xs-regular line-clamp-3 text-text-tertiary first-letter:capitalize'>
<p className="system-xs-regular line-clamp-3 text-text-tertiary first-letter:capitalize">
{dataset.description}
</p>
)}
</div>
<div className='px-4 py-2'>
<div className="px-4 py-2">
<Divider
type='horizontal'
bgStyle='gradient'
className='my-0 h-px bg-gradient-to-r from-divider-subtle to-background-gradient-mask-transparent'
type="horizontal"
bgStyle="gradient"
className="my-0 h-px bg-gradient-to-r from-divider-subtle to-background-gradient-mask-transparent"
/>
</div>
<nav className='flex min-h-[200px] grow flex-col gap-y-0.5 px-3 py-2'>
<nav className="flex min-h-[200px] grow flex-col gap-y-0.5 px-3 py-2">
{navigation.map((item, index) => {
return (
<NavLink
key={index}
mode='expand'
mode="expand"
iconMap={{ selected: item.selectedIcon, normal: item.icon }}
name={item.name}
href={item.href}

View File

@ -1,20 +1,20 @@
import React, { useCallback, useEffect, useState } from 'react'
import { usePathname } from 'next/navigation'
import { useShallow } from 'zustand/react/shallow'
import NavLink from './navLink'
import type { NavIcon } from './navLink'
import AppInfo from './app-info'
import DatasetInfo from './dataset-info'
import AppSidebarDropdown from './app-sidebar-dropdown'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import { useHover, useKeyPress } from 'ahooks'
import { usePathname } from 'next/navigation'
import React, { useCallback, useEffect, useState } from 'react'
import { useShallow } from 'zustand/react/shallow'
import { useStore as useAppStore } from '@/app/components/app/store'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import { cn } from '@/utils/classnames'
import Divider from '../base/divider'
import { useHover, useKeyPress } from 'ahooks'
import ToggleButton from './toggle-button'
import { getKeyboardKeyCodeBySystem } from '../workflow/utils'
import AppInfo from './app-info'
import AppSidebarDropdown from './app-sidebar-dropdown'
import DatasetInfo from './dataset-info'
import DatasetSidebarDropdown from './dataset-sidebar-dropdown'
import NavLink from './navLink'
import ToggleButton from './toggle-button'
export type IAppDetailNavProps = {
iconType?: 'app' | 'dataset'
@ -75,7 +75,7 @@ const AppDetailNav = ({
if (inWorkflowCanvas && hideHeader) {
return (
<div className='flex w-0 shrink-0'>
<div className="flex w-0 shrink-0">
<AppSidebarDropdown navigation={navigation} />
</div>
)
@ -83,7 +83,7 @@ const AppDetailNav = ({
if (isPipelineCanvas && hideHeader) {
return (
<div className='flex w-0 shrink-0'>
<div className="flex w-0 shrink-0">
<DatasetSidebarDropdown navigation={navigation} />
</div>
)
@ -110,9 +110,9 @@ const AppDetailNav = ({
<DatasetInfo expand={expand} />
)}
</div>
<div className='relative px-4 py-2'>
<div className="relative px-4 py-2">
<Divider
type='horizontal'
type="horizontal"
bgStyle={expand ? 'gradient' : 'solid'}
className={cn(
'my-0 h-px',
@ -123,7 +123,7 @@ const AppDetailNav = ({
/>
{!isMobile && isHoveringSidebar && (
<ToggleButton
className='absolute -right-3 top-[-3.5px] z-20'
className="absolute -right-3 top-[-3.5px] z-20"
expand={expand}
handleToggle={handleToggle}
/>

View File

@ -1,7 +1,7 @@
import React from 'react'
import { render, screen } from '@testing-library/react'
import NavLink from './navLink'
import type { NavLinkProps } from './navLink'
import { render, screen } from '@testing-library/react'
import React from 'react'
import NavLink from './navLink'
// Mock Next.js navigation
vi.mock('next/navigation', () => ({

View File

@ -1,15 +1,16 @@
'use client'
import React from 'react'
import { useSelectedLayoutSegment } from 'next/navigation'
import Link from 'next/link'
import { cn } from '@/utils/classnames'
import type { RemixiconComponentType } from '@remixicon/react'
import Link from 'next/link'
import { useSelectedLayoutSegment } from 'next/navigation'
import React from 'react'
import { cn } from '@/utils/classnames'
export type NavIcon = React.ComponentType<
React.PropsWithoutRef<React.ComponentProps<'svg'>> & {
title?: string | undefined
titleId?: string | undefined
}> | RemixiconComponentType
}
> | RemixiconComponentType
export type NavLinkProps = {
name: string
@ -51,19 +52,17 @@ const NavLink = ({
return (
<button
key={name}
type='button'
type="button"
disabled
className={cn('system-sm-medium flex h-8 cursor-not-allowed items-center rounded-lg text-components-menu-item-text opacity-30 hover:bg-components-menu-item-bg-hover',
'pl-3 pr-1')}
className={cn('system-sm-medium flex h-8 cursor-not-allowed items-center rounded-lg text-components-menu-item-text opacity-30 hover:bg-components-menu-item-bg-hover', 'pl-3 pr-1')}
title={mode === 'collapse' ? name : ''}
aria-disabled
>
{renderIcon()}
<span
className={cn('overflow-hidden whitespace-nowrap transition-all duration-200 ease-in-out',
mode === 'expand'
? 'ml-2 max-w-none opacity-100'
: 'ml-0 max-w-0 opacity-0')}
className={cn('overflow-hidden whitespace-nowrap transition-all duration-200 ease-in-out', mode === 'expand'
? 'ml-2 max-w-none opacity-100'
: 'ml-0 max-w-0 opacity-0')}
>
{name}
</span>
@ -77,16 +76,14 @@ const NavLink = ({
href={href}
className={cn(isActive
? 'system-sm-semibold border-b-[0.25px] border-l-[0.75px] border-r-[0.25px] border-t-[0.75px] border-effects-highlight-lightmode-off bg-components-menu-item-bg-active text-text-accent-light-mode-only'
: 'system-sm-medium text-components-menu-item-text hover:bg-components-menu-item-bg-hover hover:text-components-menu-item-text-hover',
'flex h-8 items-center rounded-lg pl-3 pr-1')}
: 'system-sm-medium text-components-menu-item-text hover:bg-components-menu-item-bg-hover hover:text-components-menu-item-text-hover', 'flex h-8 items-center rounded-lg pl-3 pr-1')}
title={mode === 'collapse' ? name : ''}
>
{renderIcon()}
<span
className={cn('overflow-hidden whitespace-nowrap transition-all duration-200 ease-in-out',
mode === 'expand'
? 'ml-2 max-w-none opacity-100'
: 'ml-0 max-w-0 opacity-0')}
className={cn('overflow-hidden whitespace-nowrap transition-all duration-200 ease-in-out', mode === 'expand'
? 'ml-2 max-w-none opacity-100'
: 'ml-0 max-w-0 opacity-0')}
>
{name}
</span>

View File

@ -1,8 +1,8 @@
import React from 'react'
import { fireEvent, render, screen } from '@testing-library/react'
import React from 'react'
// Simple Mock Components that reproduce the exact UI issues
const MockNavLink = ({ name, mode }: { name: string; mode: string }) => {
const MockNavLink = ({ name, mode }: { name: string, mode: string }) => {
return (
<a
className={`
@ -23,7 +23,7 @@ const MockNavLink = ({ name, mode }: { name: string; mode: string }) => {
)
}
const MockSidebarToggleButton = ({ expand, onToggle }: { expand: boolean; onToggle: () => void }) => {
const MockSidebarToggleButton = ({ expand, onToggle }: { expand: boolean, onToggle: () => void }) => {
return (
<div
className={`
@ -50,8 +50,9 @@ const MockSidebarToggleButton = ({ expand, onToggle }: { expand: boolean; onTogg
className="shrink-0 px-4 py-3"
data-testid="toggle-section"
>
<button type="button"
className='flex h-6 w-6 cursor-pointer items-center justify-center'
<button
type="button"
className="flex h-6 w-6 cursor-pointer items-center justify-center"
onClick={onToggle}
data-testid="toggle-button"
>
@ -65,7 +66,7 @@ const MockSidebarToggleButton = ({ expand, onToggle }: { expand: boolean; onTogg
const MockAppInfo = ({ expand }: { expand: boolean }) => {
return (
<div data-testid="app-info" data-expand={expand}>
<button type="button" className='block w-full'>
<button type="button" className="block w-full">
{/* Container with layout mode switching - reproduces issue #3 */}
<div className={`flex rounded-lg ${expand ? 'flex-col gap-2 p-2 pb-2.5' : 'items-start justify-center gap-1 p-1'}`}>
{/* Icon container with justify-between to flex-col switch - reproduces issue #3 */}
@ -83,19 +84,19 @@ const MockAppInfo = ({ expand }: { expand: boolean }) => {
>
Icon
</div>
<div className='flex items-center justify-center rounded-md p-0.5'>
<div className='flex h-5 w-5 items-center justify-center'>
<div className="flex items-center justify-center rounded-md p-0.5">
<div className="flex h-5 w-5 items-center justify-center">
</div>
</div>
</div>
{/* Text that appears/disappears conditionally */}
{expand && (
<div className='flex flex-col items-start gap-1'>
<div className='flex w-full'>
<div className='system-md-semibold truncate text-text-secondary'>Test App</div>
<div className="flex flex-col items-start gap-1">
<div className="flex w-full">
<div className="system-md-semibold truncate text-text-secondary">Test App</div>
</div>
<div className='system-2xs-medium-uppercase text-text-tertiary'>chatflow</div>
<div className="system-2xs-medium-uppercase text-text-tertiary">chatflow</div>
</div>
)}
</div>

View File

@ -3,8 +3,8 @@
* This test verifies that the CSS-based text rendering fixes work correctly
*/
import React from 'react'
import { render } from '@testing-library/react'
import React from 'react'
// Mock Next.js navigation
vi.mock('next/navigation', () => ({
@ -25,7 +25,8 @@ const TestNavLink = ({ mode }: { mode: 'expand' | 'collapse' }) => {
<div className="nav-link-container">
<div className={`flex h-9 items-center rounded-md py-2 text-sm font-normal ${
mode === 'expand' ? 'px-3' : 'px-2.5'
}`}>
}`}
>
<div className={`h-4 w-4 shrink-0 ${mode === 'expand' ? 'mr-2' : 'mr-0'}`}>
Icon
</div>
@ -66,16 +67,16 @@ const TestAppInfo = ({ expand }: { expand: boolean }) => {
}`}
data-testid="app-text-container"
>
<div className='flex w-full'>
<div className="flex w-full">
<div
className='system-md-semibold truncate whitespace-nowrap text-text-secondary'
className="system-md-semibold truncate whitespace-nowrap text-text-secondary"
data-testid="app-name"
>
{appDetail.name}
</div>
</div>
<div
className='system-2xs-medium-uppercase whitespace-nowrap text-text-tertiary'
className="system-2xs-medium-uppercase whitespace-nowrap text-text-tertiary"
data-testid="app-type"
>
ChatBot

Some files were not shown because too many files have changed in this diff Show More