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: | run: |
uvx --python 3.13 mdformat . --exclude ".claude/skills/**/SKILL.md" 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 - uses: autofix-ci/action@635ffb0c9798bd160680f18fd73371e355b85f27

11
.gitignore vendored
View File

@ -139,7 +139,6 @@ pyrightconfig.json
.idea/' .idea/'
.DS_Store .DS_Store
web/.vscode/settings.json
# Intellij IDEA Files # Intellij IDEA Files
.idea/* .idea/*
@ -205,7 +204,6 @@ sdks/python-client/dify_client.egg-info
!.vscode/launch.json.template !.vscode/launch.json.template
!.vscode/README.md !.vscode/README.md
api/.vscode api/.vscode
web/.vscode
# vscode Code History Extension # vscode Code History Extension
.history .history
@ -220,15 +218,6 @@ plugins.jsonl
# mise # mise
mise.toml 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 # AI Assistant
.roo/ .roo/

10
web/.gitignore vendored
View File

@ -54,3 +54,13 @@ package-lock.json
# mise # mise
mise.toml 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 type { Preview } from '@storybook/react'
import { withThemeByDataAttribute } from '@storybook/addon-themes' import { withThemeByDataAttribute } from '@storybook/addon-themes'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import I18N from '../app/components/i18n'
import { ToastProvider } from '../app/components/base/toast' import { ToastProvider } from '../app/components/base/toast'
import I18N from '../app/components/i18n'
import '../app/styles/globals.css' import '../app/styles/globals.css'
import '../app/styles/markdown.scss' import '../app/styles/markdown.scss'

View File

@ -1,6 +1,6 @@
import { useState } from 'react'
import type { ReactNode } from 'react' import type { ReactNode } from 'react'
import { useStore } from '@tanstack/react-form' import { useStore } from '@tanstack/react-form'
import { useState } from 'react'
import { useAppForm } from '@/app/components/base/form' import { useAppForm } from '@/app/components/base/form'
type UseAppFormOptions = Parameters<typeof useAppForm>[0] 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"> <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"> <div className="flex items-center justify-between text-[11px] uppercase tracking-wide text-text-tertiary">
<span>Form State</span> <span>Form State</span>
<span>{submitCount} submit{submitCount === 1 ? '' : 's'}</span> <span>
{submitCount}
{' '}
submit
{submitCount === 1 ? '' : 's'}
</span>
</div> </div>
<dl className="mt-2 space-y-1"> <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"> <div className="flex items-center justify-between rounded-md bg-components-button-tertiary-bg px-2 py-1">

View File

@ -1,26 +1,50 @@
{ {
// Disable the default formatter, use eslint instead
"prettier.enable": false, "prettier.enable": false,
"editor.formatOnSave": true, "editor.formatOnSave": false,
// Auto fix
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit" "source.fixAll.eslint": "explicit",
"source.organizeImports": "never"
}, },
"eslint.format.enable": true,
"[python]": { // Silent the stylistic rules in your IDE, but still auto fix them
"editor.formatOnType": true "eslint.rules.customizations": [
}, { "rule": "style/*", "severity": "off", "fixable": true },
"[html]": { { "rule": "format/*", "severity": "off", "fixable": true },
"editor.defaultFormatter": "vscode.html-language-features" { "rule": "*-indent", "severity": "off", "fixable": true },
}, { "rule": "*-spacing", "severity": "off", "fixable": true },
"[typescriptreact]": { { "rule": "*-spaces", "severity": "off", "fixable": true },
"editor.defaultFormatter": "vscode.typescript-language-features" { "rule": "*-order", "severity": "off", "fixable": true },
}, { "rule": "*-dangle", "severity": "off", "fixable": true },
"[javascriptreact]": { { "rule": "*-newline", "severity": "off", "fixable": true },
"editor.defaultFormatter": "vscode.typescript-language-features" { "rule": "*quotes", "severity": "off", "fixable": true },
}, { "rule": "*semi", "severity": "off", "fixable": true }
"[jsonc]": { ],
"editor.defaultFormatter": "vscode.json-language-features"
}, // Enable eslint for all supported languages
"typescript.tsdk": "node_modules/typescript/lib", "eslint.validate": [
"typescript.enablePromptUseWorkspaceTsdk": true, "javascript",
"npm.packageManager": "pnpm" "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 { merge, noop } from 'lodash-es'
import { defaultPlan } from '@/app/components/billing/config' 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 // Avoid being mocked in tests
export const baseProviderContextValue: ProviderContextState = { export const baseProviderContextValue: ProviderContextState = {

View File

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

View File

@ -15,19 +15,19 @@ describe('Description Validation Logic', () => {
} }
describe('Backend Validation Function', () => { describe('Backend Validation Function', () => {
test('allows description within 400 characters', () => { it('allows description within 400 characters', () => {
const validDescription = 'x'.repeat(400) const validDescription = 'x'.repeat(400)
expect(() => validateDescriptionLength(validDescription)).not.toThrow() expect(() => validateDescriptionLength(validDescription)).not.toThrow()
expect(validateDescriptionLength(validDescription)).toBe(validDescription) expect(validateDescriptionLength(validDescription)).toBe(validDescription)
}) })
test('allows empty description', () => { it('allows empty description', () => {
expect(() => validateDescriptionLength('')).not.toThrow() expect(() => validateDescriptionLength('')).not.toThrow()
expect(() => validateDescriptionLength(null)).not.toThrow() expect(() => validateDescriptionLength(null)).not.toThrow()
expect(() => validateDescriptionLength(undefined)).not.toThrow() expect(() => validateDescriptionLength(undefined)).not.toThrow()
}) })
test('rejects description exceeding 400 characters', () => { it('rejects description exceeding 400 characters', () => {
const invalidDescription = 'x'.repeat(401) const invalidDescription = 'x'.repeat(401)
expect(() => validateDescriptionLength(invalidDescription)).toThrow( expect(() => validateDescriptionLength(invalidDescription)).toThrow(
'Description cannot exceed 400 characters.', 'Description cannot exceed 400 characters.',
@ -36,7 +36,7 @@ describe('Description Validation Logic', () => {
}) })
describe('Backend Validation Consistency', () => { describe('Backend Validation Consistency', () => {
test('App and Dataset have consistent validation limits', () => { it('App and Dataset have consistent validation limits', () => {
const maxLength = 400 const maxLength = 400
const validDescription = 'x'.repeat(maxLength) const validDescription = 'x'.repeat(maxLength)
const invalidDescription = 'x'.repeat(maxLength + 1) const invalidDescription = 'x'.repeat(maxLength + 1)
@ -50,7 +50,7 @@ describe('Description Validation Logic', () => {
expect(() => validateDescriptionLength(invalidDescription)).toThrow() expect(() => validateDescriptionLength(invalidDescription)).toThrow()
}) })
test('validation error messages are consistent', () => { it('validation error messages are consistent', () => {
const expectedErrorMessage = 'Description cannot exceed 400 characters.' const expectedErrorMessage = 'Description cannot exceed 400 characters.'
// This would be the error message from both App and Dataset backend validation // 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 }) => { testCases.forEach(({ length, shouldPass, description }) => {
test(`handles ${description} correctly`, () => { it(`handles ${description} correctly`, () => {
const testDescription = length > 0 ? 'x'.repeat(length) : '' const testDescription = length > 0 ? 'x'.repeat(length) : ''
expect(testDescription.length).toBe(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 // 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() const router = useRouter()
// This is the FIXED implementation from detail/index.tsx // This is the FIXED implementation from detail/index.tsx
@ -59,7 +59,12 @@ const DocumentDetailWithFix = ({ datasetId, documentId }: { datasetId: string; d
Back to Documents Back to Documents
</button> </button>
<div data-testid="document-info"> <div data-testid="document-info">
Dataset: {datasetId}, Document: {documentId} Dataset:
{' '}
{datasetId}
, Document:
{' '}
{documentId}
</div> </div>
</div> </div>
) )
@ -88,7 +93,7 @@ describe('Document Detail Navigation Fix Verification', () => {
}) })
describe('Query Parameter Preservation', () => { 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 // Simulate user coming from page 3 with 25 items per page
Object.defineProperty(window, 'location', { Object.defineProperty(window, 'location', {
value: { value: {
@ -108,7 +113,7 @@ describe('Document Detail Navigation Fix Verification', () => {
console.log('✅ Pagination state preserved: page=3&limit=25') 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 // Simulate user with search and filters applied
Object.defineProperty(window, 'location', { Object.defineProperty(window, 'location', {
value: { value: {
@ -127,7 +132,7 @@ describe('Document Detail Navigation Fix Verification', () => {
console.log('✅ Search and filters preserved') 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 // Test with complex query string including encoded characters
Object.defineProperty(window, 'location', { Object.defineProperty(window, 'location', {
value: { value: {
@ -152,7 +157,7 @@ describe('Document Detail Navigation Fix Verification', () => {
console.log('✅ Complex query parameters handled:', expectedCall) console.log('✅ Complex query parameters handled:', expectedCall)
}) })
test('handles empty query parameters gracefully', () => { it('handles empty query parameters gracefully', () => {
// No query parameters in URL // No query parameters in URL
Object.defineProperty(window, 'location', { Object.defineProperty(window, 'location', {
value: { value: {
@ -173,7 +178,7 @@ describe('Document Detail Navigation Fix Verification', () => {
}) })
describe('Different Dataset IDs', () => { describe('Different Dataset IDs', () => {
test('works with different dataset identifiers', () => { it('works with different dataset identifiers', () => {
Object.defineProperty(window, 'location', { Object.defineProperty(window, 'location', {
value: { value: {
search: '?page=5&limit=10', search: '?page=5&limit=10',
@ -193,7 +198,7 @@ describe('Document Detail Navigation Fix Verification', () => {
}) })
describe('Real User Scenarios', () => { 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 // User searched for "API" and navigated to page 3
Object.defineProperty(window, 'location', { Object.defineProperty(window, 'location', {
value: { value: {
@ -213,7 +218,7 @@ describe('Document Detail Navigation Fix Verification', () => {
console.log('✅ Real user scenario: search + pagination preserved') 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 // User has applied multiple filters and is on page 2
Object.defineProperty(window, 'location', { Object.defineProperty(window, 'location', {
value: { value: {
@ -234,7 +239,7 @@ describe('Document Detail Navigation Fix Verification', () => {
}) })
describe('Error Handling and Edge Cases', () => { describe('Error Handling and Edge Cases', () => {
test('handles malformed query parameters gracefully', () => { it('handles malformed query parameters gracefully', () => {
// Test with potentially problematic query string // Test with potentially problematic query string
Object.defineProperty(window, 'location', { Object.defineProperty(window, 'location', {
value: { value: {
@ -258,7 +263,7 @@ describe('Document Detail Navigation Fix Verification', () => {
console.log('✅ Malformed parameters handled gracefully:', navigationPath) 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 // Test with a very long query string
const longKeyword = 'a'.repeat(1000) const longKeyword = 'a'.repeat(1000)
Object.defineProperty(window, 'location', { Object.defineProperty(window, 'location', {
@ -281,7 +286,7 @@ describe('Document Detail Navigation Fix Verification', () => {
}) })
describe('Performance Verification', () => { describe('Performance Verification', () => {
test('navigation function executes quickly', () => { it('navigation function executes quickly', () => {
Object.defineProperty(window, 'location', { Object.defineProperty(window, 'location', {
value: { value: {
search: '?page=1&limit=10&keyword=test', 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') const sorted = sortDocuments(mockDocuments, 'name', 'desc')
expect(sorted.map(doc => doc.name)).toEqual(['Gamma.docx', 'Beta.pdf', 'Alpha.txt']) 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') const sorted = sortDocuments(mockDocuments, 'name', 'asc')
expect(sorted.map(doc => doc.name)).toEqual(['Alpha.txt', 'Beta.pdf', 'Gamma.docx']) 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') const sorted = sortDocuments(mockDocuments, 'word_count', 'desc')
expect(sorted.map(doc => doc.word_count)).toEqual([800, 500, 200]) 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') const sorted = sortDocuments(mockDocuments, 'hit_count', 'desc')
expect(sorted.map(doc => doc.hit_count)).toEqual([25, 10, 5]) 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') const sorted = sortDocuments(mockDocuments, 'created_at', 'desc')
expect(sorted.map(doc => doc.created_at)).toEqual([1699123500, 1699123456, 1699123400]) expect(sorted.map(doc => doc.created_at)).toEqual([1699123500, 1699123456, 1699123400])
}) })
test('handles empty values correctly', () => { it('handles empty values correctly', () => {
const docsWithEmpty = [ const docsWithEmpty = [
{ id: '1', name: 'Test', word_count: 100, hit_count: 5, created_at: 1699123456 }, { 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 }, { 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 { 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 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 replaceMock = vi.fn()
const backMock = 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 { 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 WebAppStoreProvider, { useWebAppStore } from '@/context/web-app-context'
import { AccessMode } from '@/models/access-control'
vi.mock('next/navigation', () => ({ vi.mock('next/navigation', () => ({
usePathname: vi.fn(() => '/chatbot/sample-app'), usePathname: vi.fn(() => '/chatbot/sample-app'),
useSearchParams: vi.fn(() => { 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 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', () => ({ vi.mock('cmdk', () => ({
Command: { Command: {

View File

@ -1,6 +1,10 @@
import type { Mock } from 'vitest' import type { Mock } from 'vitest'
import type { ActionItem } from '../../app/components/goto-anything/actions/types' 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 // Mock the entire actions module to avoid import issues
vi.mock('../../app/components/goto-anything/actions', () => ({ vi.mock('../../app/components/goto-anything/actions', () => ({
matchAction: vi.fn(), matchAction: vi.fn(),
@ -8,10 +12,6 @@ vi.mock('../../app/components/goto-anything/actions', () => ({
vi.mock('../../app/components/goto-anything/actions/commands/registry') 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 // Implement the actual matchAction logic for testing
const actualMatchAction = (query: string, actions: Record<string, ActionItem>) => { const actualMatchAction = (query: string, actions: Record<string, ActionItem>) => {
const result = Object.values(actions).find((action) => { 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 { render, screen } from '@testing-library/react'
import React from 'react'
// Type alias for search mode // Type alias for search mode
type SearchMode = 'scopes' | 'commands' | null type SearchMode = 'scopes' | 'commands' | null
// Mock component to test tag display logic // Mock component to test tag display logic
const TagDisplay: React.FC<{ searchMode: SearchMode }> = ({ searchMode }) => { const TagDisplay: React.FC<{ searchMode: SearchMode }> = ({ searchMode }) => {
if (!searchMode) return null if (!searchMode)
return null
return ( return (
<div className="flex items-center gap-1 text-xs text-text-tertiary"> <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', () => { describe('Search Mode Detection', () => {
const getSearchMode = (query: string): SearchMode => { const getSearchMode = (query: string): SearchMode => {
if (query.startsWith('@')) return 'scopes' if (query.startsWith('@'))
if (query.startsWith('/')) return 'commands' return 'scopes'
if (query.startsWith('/'))
return 'commands'
return null return null
} }
@ -90,8 +93,10 @@ describe('Scope and Command Tags', () => {
const SearchComponent: React.FC<{ query: string }> = ({ query }) => { const SearchComponent: React.FC<{ query: string }> = ({ query }) => {
let searchMode: SearchMode = null let searchMode: SearchMode = null
if (query.startsWith('@')) searchMode = 'scopes' if (query.startsWith('@'))
else if (query.startsWith('/')) searchMode = 'commands' searchMode = 'scopes'
else if (query.startsWith('/'))
searchMode = 'commands'
return ( return (
<div> <div>

View File

@ -10,8 +10,8 @@ import type { MockedFunction } from 'vitest'
*/ */
import { Actions, searchAnything } from '@/app/components/goto-anything/actions' import { Actions, searchAnything } from '@/app/components/goto-anything/actions'
import { postMarketplace } from '@/service/base'
import { fetchAppList } from '@/service/apps' import { fetchAppList } from '@/service/apps'
import { postMarketplace } from '@/service/base'
import { fetchDatasets } from '@/service/datasets' import { fetchDatasets } from '@/service/datasets'
// Mock API functions // 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 type { SlashCommandHandler } from '../../app/components/goto-anything/actions/commands/types'
import { slashCommandRegistry } from '../../app/components/goto-anything/actions/commands/registry'
// Mock the registry // Mock the registry
vi.mock('../../app/components/goto-anything/actions/commands/registry') vi.mock('../../app/components/goto-anything/actions/commands/registry')
@ -50,8 +50,10 @@ describe('Slash Command Dual-Mode System', () => {
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks() vi.clearAllMocks()
;(slashCommandRegistry as any).findCommand = vi.fn((name: string) => { ;(slashCommandRegistry as any).findCommand = vi.fn((name: string) => {
if (name === 'docs') return mockDirectCommand if (name === 'docs')
if (name === 'theme') return mockSubmenuCommand return mockDirectCommand
if (name === 'theme')
return mockSubmenuCommand
return null return null
}) })
;(slashCommandRegistry as any).getAllCommands = vi.fn(() => [ ;(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 // Helper function to check if upload features exist
const hasUploadFeatures = (content: string): { [key: string]: boolean } => { const hasUploadFeatures = (content: string): { [key: string]: boolean } => {
return { return {
fileUpload: /fileUpload\s*:\s*{/.test(content), fileUpload: /fileUpload\s*:\s*\{/.test(content),
imageUpload: /imageUpload\s*:\s*{/.test(content), imageUpload: /imageUpload\s*:\s*\{/.test(content),
documentUpload: /documentUpload\s*:\s*{/.test(content), documentUpload: /documentUpload\s*:\s*\{/.test(content),
audioUpload: /audioUpload\s*:\s*{/.test(content), audioUpload: /audioUpload\s*:\s*\{/.test(content),
featureBar: /bar\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`) 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) => { supportedLocales.forEach((locale) => {
const filePath = path.join(I18N_DIR, locale, 'app-debug.ts') const filePath = path.join(I18N_DIR, locale, 'app-debug.ts')
expect(fs.existsSync(filePath)).toBe(true) 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 } } = {} const results: { [locale: string]: { [feature: string]: boolean } } = {}
supportedLocales.forEach((locale) => { supportedLocales.forEach((locale) => {
@ -69,7 +69,7 @@ describe('Upload Features i18n Translations - Issue #23062', () => {
console.log('✅ All locales have complete upload features') 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 // These locales were specifically missing audioUpload
const previouslyMissingLocales = ['fa-IR', 'hi-IN', 'ro-RO', 'sl-SI', 'th-TH', 'uk-UA', 'vi-VN'] 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) const content = loadTranslationContent(locale)
// Verify audioUpload exists // 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 // Verify it has title and description
expect(/audioUpload[^}]*title\s*:/.test(content)).toBe(true) 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) => { supportedLocales.forEach((locale) => {
const content = loadTranslationContent(locale) const content = loadTranslationContent(locale)
// Check fileUpload has required properties // 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[^}]*title\s*:/.test(content)).toBe(true)
expect(/fileUpload[^}]*description\s*:/.test(content)).toBe(true) expect(/fileUpload[^}]*description\s*:/.test(content)).toBe(true)
} }
// Check imageUpload has required properties // 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[^}]*title\s*:/.test(content)).toBe(true)
expect(/imageUpload[^}]*description\s*:/.test(content)).toBe(true) expect(/imageUpload[^}]*description\s*:/.test(content)).toBe(true)
} }
// Check documentUpload has required properties // 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[^}]*title\s*:/.test(content)).toBe(true)
expect(/documentUpload[^}]*description\s*:/.test(content)).toBe(true) expect(/documentUpload[^}]*description\s*:/.test(content)).toBe(true)
} }
// Check audioUpload has required properties // 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[^}]*title\s*:/.test(content)).toBe(true)
expect(/audioUpload[^}]*description\s*:/.test(content)).toBe(true) expect(/audioUpload[^}]*description\s*:/.test(content)).toBe(true)
} }

View File

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

View File

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

View File

@ -70,7 +70,7 @@ describe('Unified Tags Editing - Pure Logic Tests', () => {
}) })
describe('Fallback Logic (from layout-main.tsx)', () => { describe('Fallback Logic (from layout-main.tsx)', () => {
type Tag = { id: string; name: string } type Tag = { id: string, name: string }
type AppDetail = { tags: Tag[] } type AppDetail = { tags: Tag[] }
type FallbackResult = { tags?: Tag[] } | null type FallbackResult = { tags?: Tag[] } | null
// no-op // no-op
@ -316,7 +316,7 @@ describe('Unified Tags Editing - Pure Logic Tests', () => {
] ]
// Filter out invalid entries // 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 tag != null
&& typeof tag === 'object' && typeof tag === 'object'
&& 'id' in tag && 'id' in tag

View File

@ -1,6 +1,6 @@
import type { Mock } from 'vitest' import type { Mock } from 'vitest'
import { BlockEnum } from '@/app/components/workflow/types'
import { useWorkflowStore } from '@/app/components/workflow/store' import { useWorkflowStore } from '@/app/components/workflow/store'
import { BlockEnum } from '@/app/components/workflow/types'
// Type for mocked store // Type for mocked store
type MockWorkflowStore = { type MockWorkflowStore = {

View File

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

View File

@ -5,8 +5,8 @@
* components have been properly fixed by replacing dangerouslySetInnerHTML with safe React rendering. * components have been properly fixed by replacing dangerouslySetInnerHTML with safe React rendering.
*/ */
import React from 'react'
import { cleanup, render } from '@testing-library/react' import { cleanup, render } from '@testing-library/react'
import React from 'react'
import BlockInput from '../app/components/base/block-input' import BlockInput from '../app/components/base/block-input'
import SupportVarInput from '../app/components/workflow/nodes/_base/components/support-var-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 type { Locale } from '@/i18n-config'
import React from 'react'
import DevelopMain from '@/app/components/develop' import DevelopMain from '@/app/components/develop'
export type IDevelopProps = { export type IDevelopProps = {
params: Promise<{ locale: Locale; appId: string }> params: Promise<{ locale: Locale, appId: string }>
} }
const Develop = async (props: IDevelopProps) => { const Develop = async (props: IDevelopProps) => {

View File

@ -1,8 +1,7 @@
'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import { useUnmount } from 'ahooks' import type { NavIcon } from '@/app/components/app-sidebar/navLink'
import React, { useCallback, useEffect, useState } from 'react' import type { App } from '@/types/app'
import { usePathname, useRouter } from 'next/navigation'
import { import {
RiDashboard2Fill, RiDashboard2Fill,
RiDashboard2Line, RiDashboard2Line,
@ -13,21 +12,23 @@ import {
RiTerminalWindowFill, RiTerminalWindowFill,
RiTerminalWindowLine, RiTerminalWindowLine,
} from '@remixicon/react' } 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 { useTranslation } from 'react-i18next'
import { useShallow } from 'zustand/react/shallow' 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 AppSideBar from '@/app/components/app-sidebar'
import type { NavIcon } from '@/app/components/app-sidebar/navLink' import { useStore } from '@/app/components/app/store'
import { fetchAppDetailDirect } from '@/service/apps'
import { useAppContext } from '@/context/app-context'
import Loading from '@/app/components/base/loading' 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 { 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'), { const TagManagementModal = dynamic(() => import('@/app/components/base/tag-management'), {
ssr: false, ssr: false,
@ -156,7 +157,7 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
if (!appDetail) { if (!appDetail) {
return ( 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 /> <Loading />
</div> </div>
) )
@ -173,7 +174,7 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
{children} {children}
</div> </div>
{showTagManagementModal && ( {showTagManagementModal && (
<TagManagementModal type='app' show={showTagManagementModal} /> <TagManagementModal type="app" show={showTagManagementModal} />
)} )}
</div> </div>
) )

View File

@ -1,30 +1,30 @@
'use client' 'use client'
import type { FC } from 'react' 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 React, { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector' import { useContext } from 'use-context-selector'
import AppCard from '@/app/components/app/overview/app-card' 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 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 { 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 { import {
fetchAppDetail, fetchAppDetail,
updateAppSiteAccessToken, updateAppSiteAccessToken,
updateAppSiteConfig, updateAppSiteConfig,
updateAppSiteStatus, updateAppSiteStatus,
} from '@/service/apps' } 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 { useAppWorkflow } from '@/service/use-workflow'
import type { BlockEnum } from '@/app/components/workflow/types' import { AppModeEnum } from '@/types/app'
import { isTriggerNode } from '@/app/components/workflow/types' import { asyncRunSafe } from '@/utils'
import { useDocLink } from '@/context/i18n'
export type ICardViewProps = { export type ICardViewProps = {
appId: string appId: string
@ -59,12 +59,12 @@ const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
const triggerDocUrl = docLink('/guides/workflow/node/start') const triggerDocUrl = docLink('/guides/workflow/node/start')
const buildTriggerModeMessage = useCallback((featureName: string) => ( const buildTriggerModeMessage = useCallback((featureName: string) => (
<div className='flex flex-col gap-1'> <div className="flex flex-col gap-1">
<div className='text-xs text-text-secondary'> <div className="text-xs text-text-secondary">
{t('appOverview.overview.disableTooltip.triggerMode', { feature: featureName })} {t('appOverview.overview.disableTooltip.triggerMode', { feature: featureName })}
</div> </div>
<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) => { onClick={(event) => {
event.stopPropagation() event.stopPropagation()
window.open(triggerDocUrl, '_blank') window.open(triggerDocUrl, '_blank')
@ -185,12 +185,14 @@ const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
</> </>
) )
const triggerCardNode = showTriggerCard ? ( const triggerCardNode = showTriggerCard
? (
<TriggerCard <TriggerCard
appInfo={appDetail} appInfo={appDetail}
onToggleResult={handleCallbackResult} onToggleResult={handleCallbackResult}
/> />
) : null )
: null
return ( return (
<div className={className || 'mb-6 grid w-full grid-cols-1 gap-6 xl:grid-cols-2'}> <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' 'use client'
import React, { useState } from 'react' import type { PeriodParams } from '@/app/components/app/overview/app-chart'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import quarterOfYear from 'dayjs/plugin/quarterOfYear' import quarterOfYear from 'dayjs/plugin/quarterOfYear'
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next' 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 { 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 { 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 { IS_CLOUD_EDITION } from '@/config'
import LongTimeRangePicker from './long-time-range-picker' import LongTimeRangePicker from './long-time-range-picker'
import TimeRangePicker from './time-range-picker'
dayjs.extend(quarterOfYear) dayjs.extend(quarterOfYear)
@ -43,16 +43,18 @@ export default function ChartView({ appId, headerRight }: IChartViewProps) {
return ( return (
<div> <div>
<div className='mb-4'> <div className="mb-4">
<div className='system-xl-semibold mb-2 text-text-primary'>{t('common.appMenus.overview')}</div> <div className="system-xl-semibold mb-2 text-text-primary">{t('common.appMenus.overview')}</div>
<div className='flex items-center justify-between'> <div className="flex items-center justify-between">
{IS_CLOUD_EDITION ? ( {IS_CLOUD_EDITION
? (
<TimeRangePicker <TimeRangePicker
ranges={TIME_PERIOD_MAPPING} ranges={TIME_PERIOD_MAPPING}
onSelect={setPeriod} onSelect={setPeriod}
queryDateFormat={queryDateFormat} queryDateFormat={queryDateFormat}
/> />
) : ( )
: (
<LongTimeRangePicker <LongTimeRangePicker
periodMapping={LONG_TIME_PERIOD_MAPPING} periodMapping={LONG_TIME_PERIOD_MAPPING}
onSelect={setPeriod} onSelect={setPeriod}
@ -64,13 +66,13 @@ export default function ChartView({ appId, headerRight }: IChartViewProps) {
</div> </div>
</div> </div>
{!isWorkflow && ( {!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} /> <ConversationsChart period={period} id={appId} />
<EndUsersChart period={period} id={appId} /> <EndUsersChart period={period} id={appId} />
</div> </div>
)} )}
{!isWorkflow && ( {!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 {isChatApp
? ( ? (
<AvgSessionInteractions period={period} id={appId} /> <AvgSessionInteractions period={period} id={appId} />
@ -82,24 +84,24 @@ export default function ChartView({ appId, headerRight }: IChartViewProps) {
</div> </div>
)} )}
{!isWorkflow && ( {!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} /> <UserSatisfactionRate period={period} id={appId} />
<CostChart period={period} id={appId} /> <CostChart period={period} id={appId} />
</div> </div>
)} )}
{!isWorkflow && isChatApp && ( {!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} /> <MessagesChart period={period} id={appId} />
</div> </div>
)} )}
{isWorkflow && ( {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} /> <WorkflowMessagesChart period={period} id={appId} />
<WorkflowDailyTerminalsChart period={period} id={appId} /> <WorkflowDailyTerminalsChart period={period} id={appId} />
</div> </div>
)} )}
{isWorkflow && ( {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} /> <WorkflowCostChart period={period} id={appId} />
<AvgUserInteractions period={period} id={appId} /> <AvgUserInteractions period={period} id={appId} />
</div> </div>

View File

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

View File

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

View File

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

View File

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

View File

@ -1,19 +1,19 @@
'use client' 'use client'
import type { PeriodParamsWithTimeRange, TimeRange } from '@/app/components/app/overview/app-chart'
import type { FC } from 'react' import type { FC } from 'react'
import React, { useCallback } from 'react' import type { PeriodParamsWithTimeRange, TimeRange } from '@/app/components/app/overview/app-chart'
import { SimpleSelect } from '@/app/components/base/select'
import type { Item } from '@/app/components/base/select' import type { Item } from '@/app/components/base/select'
import dayjs from 'dayjs'
import { RiArrowDownSLine, RiCheckLine } from '@remixicon/react' 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 { useTranslation } from 'react-i18next'
import { SimpleSelect } from '@/app/components/base/select'
import { cn } from '@/utils/classnames'
const today = dayjs() const today = dayjs()
type Props = { type Props = {
isCustomRange: boolean isCustomRange: boolean
ranges: { value: number; name: string }[] ranges: { value: number, name: string }[]
onSelect: (payload: PeriodParamsWithTimeRange) => void onSelect: (payload: PeriodParamsWithTimeRange) => void
} }
@ -41,13 +41,13 @@ const RangeSelector: FC<Props> = ({
const renderTrigger = useCallback((item: Item | null, isOpen: boolean) => { const renderTrigger = useCallback((item: Item | null, isOpen: boolean) => {
return ( 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={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')} /> <RiArrowDownSLine className={cn('size-4 text-text-quaternary', isOpen && 'text-text-secondary')} />
</div> </div>
) )
}, [isCustomRange]) }, [isCustomRange])
const renderOption = useCallback(({ item, selected }: { item: Item; selected: boolean }) => { const renderOption = useCallback(({ item, selected }: { item: Item, selected: boolean }) => {
return ( return (
<> <>
{selected && ( {selected && (
@ -66,14 +66,14 @@ const RangeSelector: FC<Props> = ({
return ( return (
<SimpleSelect <SimpleSelect
items={ranges.map(v => ({ ...v, name: t(`appLog.filter.period.${v.name}`) }))} items={ranges.map(v => ({ ...v, name: t(`appLog.filter.period.${v.name}`) }))}
className='mt-0 !w-40' className="mt-0 !w-40"
notClearable={true} notClearable={true}
onSelect={handleSelectRange} onSelect={handleSelectRange}
defaultValue={0} defaultValue={0}
wrapperClassName='h-8' wrapperClassName="h-8"
optionWrapClassName='w-[200px] translate-x-[-24px]' optionWrapClassName="w-[200px] translate-x-[-24px]"
renderTrigger={renderTrigger} 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} renderOption={renderOption}
/> />
) )

View File

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

View File

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

View File

@ -1,18 +1,18 @@
'use client' 'use client'
import type { FC, JSX } from 'react' 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 React, { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useBoolean } from 'ahooks' import Divider from '@/app/components/base/divider'
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 Switch from '@/app/components/base/switch' import Switch from '@/app/components/base/switch'
import Tooltip from '@/app/components/base/tooltip' 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 { 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' const I18N_PREFIX = 'app.tracing'
@ -92,7 +92,7 @@ const ConfigPopup: FC<PopupProps> = ({
const switchContent = ( const switchContent = (
<Switch <Switch
className='ml-3' className="ml-3"
defaultValue={enabled} defaultValue={enabled}
onChange={onStatusChange} onChange={onStatusChange}
disabled={providerAllNotConfigured} disabled={providerAllNotConfigured}
@ -322,13 +322,13 @@ const ConfigPopup: FC<PopupProps> = ({
} }
return ( return (
<div className='w-[420px] rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-4 shadow-xl'> <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 justify-between">
<div className='flex items-center'> <div className="flex items-center">
<TracingIcon size='md' className='mr-2' /> <TracingIcon size="md" className="mr-2" />
<div className='title-2xl-semi-bold text-text-primary'>{t(`${I18N_PREFIX}.tracing`)}</div> <div className="title-2xl-semi-bold text-text-primary">{t(`${I18N_PREFIX}.tracing`)}</div>
</div> </div>
<div className='flex items-center'> <div className="flex items-center">
<Indicator color={enabled ? 'green' : 'gray'} /> <Indicator color={enabled ? 'green' : 'gray'} />
<div className={cn('system-xs-semibold-uppercase ml-1 text-text-tertiary', enabled && 'text-util-colors-green-green-600')}> <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'}`)} {t(`${I18N_PREFIX}.${enabled ? 'enabled' : 'disabled'}`)}
@ -349,16 +349,16 @@ const ConfigPopup: FC<PopupProps> = ({
</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}.tracingDescription`)} {t(`${I18N_PREFIX}.tracingDescription`)}
</div> </div>
<Divider className='my-3' /> <Divider className="my-3" />
<div className='relative'> <div className="relative">
{(providerAllConfigured || providerAllNotConfigured) {(providerAllConfigured || providerAllNotConfigured)
? ( ? (
<> <>
<div className='system-xs-medium-uppercase text-text-tertiary'>{t(`${I18N_PREFIX}.configProviderTitle.${providerAllConfigured ? 'configured' : 'notConfigured'}`)}</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'> <div className="mt-2 max-h-96 space-y-2 overflow-y-auto">
{langfusePanel} {langfusePanel}
{langSmithPanel} {langSmithPanel}
{opikPanel} {opikPanel}
@ -374,12 +374,12 @@ const ConfigPopup: FC<PopupProps> = ({
) )
: ( : (
<> <>
<div className='system-xs-medium-uppercase text-text-tertiary'>{t(`${I18N_PREFIX}.configProviderTitle.configured`)}</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'> <div className="mt-2 max-h-40 space-y-2 overflow-y-auto">
{configuredProviderPanel()} {configuredProviderPanel()}
</div> </div>
<div className='system-xs-medium-uppercase mt-3 text-text-tertiary'>{t(`${I18N_PREFIX}.configProviderTitle.moreProvider`)}</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'> <div className="mt-2 max-h-40 space-y-2 overflow-y-auto">
{moreProviderPanel()} {moreProviderPanel()}
</div> </div>
</> </>

View File

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

View File

@ -1,26 +1,26 @@
'use client' 'use client'
import type { FC } from 'react' 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 { import {
RiArrowDownDoubleLine, RiArrowDownDoubleLine,
RiEqualizer2Line, RiEqualizer2Line,
} from '@remixicon/react' } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { usePathname } from 'next/navigation'
import { useBoolean } from 'ahooks' import { useBoolean } from 'ahooks'
import type { AliyunConfig, ArizeConfig, DatabricksConfig, LangFuseConfig, LangSmithConfig, MLflowConfig, OpikConfig, PhoenixConfig, TencentConfig, WeaveConfig } from './type' import { usePathname } from 'next/navigation'
import { TracingProvider } from './type' import React, { useEffect, useState } from 'react'
import TracingIcon from './tracing-icon' import { useTranslation } from 'react-i18next'
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 Divider from '@/app/components/base/divider' 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' const I18N_PREFIX = 'app.tracing'
@ -215,8 +215,8 @@ const Panel: FC = () => {
if (!isLoaded) { if (!isLoaded) {
return ( return (
<div className='mb-3 flex items-center justify-between'> <div className="mb-3 flex items-center justify-between">
<div className='w-[200px]'> <div className="w-[200px]">
<Loading /> <Loading />
</div> </div>
</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', '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' /> <TracingIcon size="md" />
<div className='system-sm-semibold mx-2 text-text-secondary'>{t(`${I18N_PREFIX}.title`)}</div> <div className="system-sm-semibold mx-2 text-text-secondary">{t(`${I18N_PREFIX}.title`)}</div>
<div className='rounded-md p-1'> <div className="rounded-md p-1">
<RiEqualizer2Line className='h-4 w-4 text-text-tertiary' /> <RiEqualizer2Line className="h-4 w-4 text-text-tertiary" />
</div> </div>
<Divider type='vertical' className='h-3.5' /> <Divider type="vertical" className="h-3.5" />
<div className='rounded-md p-1'> <div className="rounded-md p-1">
<RiArrowDownDoubleLine className='h-4 w-4 text-text-tertiary' /> <RiArrowDownDoubleLine className="h-4 w-4 text-text-tertiary" />
</div> </div>
</div> </div>
</ConfigButton> </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', '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'} /> <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'}`)} {t(`${I18N_PREFIX}.${enabled ? 'enabled' : 'disabled'}`)}
</div> </div>
</div> </div>
{InUseProviderIcon && <InUseProviderIcon className='ml-1 h-4' />} {InUseProviderIcon && <InUseProviderIcon className="ml-1 h-4" />}
<div className='ml-2 rounded-md p-1'> <div className="ml-2 rounded-md p-1">
<RiEqualizer2Line className='h-4 w-4 text-text-tertiary' /> <RiEqualizer2Line className="h-4 w-4 text-text-tertiary" />
</div> </div>
<Divider type='vertical' className='h-3.5' /> <Divider type="vertical" className="h-3.5" />
</div> </div>
</ConfigButton> </ConfigButton>
)} )}

View File

@ -1,23 +1,23 @@
'use client' 'use client'
import type { FC } from 'react' 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 React, { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useBoolean } from 'ahooks' import Button from '@/app/components/base/button'
import Field from './field' import Confirm from '@/app/components/base/confirm'
import type { AliyunConfig, ArizeConfig, DatabricksConfig, LangFuseConfig, LangSmithConfig, MLflowConfig, OpikConfig, PhoenixConfig, TencentConfig, WeaveConfig } from './type' import Divider from '@/app/components/base/divider'
import { TracingProvider } from './type' import { LinkExternal02 } from '@/app/components/base/icons/src/vender/line/general'
import { docURL } from './config' import { Lock01 } from '@/app/components/base/icons/src/vender/solid/security'
import { import {
PortalToFollowElem, PortalToFollowElem,
PortalToFollowElemContent, PortalToFollowElemContent,
} from '@/app/components/base/portal-to-follow-elem' } 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 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 = { type Props = {
appId: string appId: string
@ -296,28 +296,31 @@ const ProviderConfigModal: FC<Props> = ({
{!isShowRemoveConfirm {!isShowRemoveConfirm
? ( ? (
<PortalToFollowElem open> <PortalToFollowElem open>
<PortalToFollowElemContent className='z-[60] h-full w-full'> <PortalToFollowElemContent className="z-[60] h-full w-full">
<div className='fixed inset-0 flex items-center justify-center bg-background-overlay'> <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="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="px-8 pt-8">
<div className='mb-4 flex items-center justify-between'> <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 className="title-2xl-semi-bold text-text-primary">
{t(`${I18N_PREFIX}.title`)}
{t(`app.tracing.${type}.title`)}
</div>
</div> </div>
<div className='space-y-4'> <div className="space-y-4">
{type === TracingProvider.arize && ( {type === TracingProvider.arize && (
<> <>
<Field <Field
label='API Key' label="API Key"
labelClassName='!text-sm' labelClassName="!text-sm"
isRequired isRequired
value={(config as ArizeConfig).api_key} value={(config as ArizeConfig).api_key}
onChange={handleConfigChange('api_key')} onChange={handleConfigChange('api_key')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'API Key' })!} placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'API Key' })!}
/> />
<Field <Field
label='Space ID' label="Space ID"
labelClassName='!text-sm' labelClassName="!text-sm"
isRequired isRequired
value={(config as ArizeConfig).space_id} value={(config as ArizeConfig).space_id}
onChange={handleConfigChange('space_id')} onChange={handleConfigChange('space_id')}
@ -325,26 +328,26 @@ const ProviderConfigModal: FC<Props> = ({
/> />
<Field <Field
label={t(`${I18N_PREFIX}.project`)!} label={t(`${I18N_PREFIX}.project`)!}
labelClassName='!text-sm' labelClassName="!text-sm"
isRequired isRequired
value={(config as ArizeConfig).project} value={(config as ArizeConfig).project}
onChange={handleConfigChange('project')} onChange={handleConfigChange('project')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.project`) })!} placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.project`) })!}
/> />
<Field <Field
label='Endpoint' label="Endpoint"
labelClassName='!text-sm' labelClassName="!text-sm"
value={(config as ArizeConfig).endpoint} value={(config as ArizeConfig).endpoint}
onChange={handleConfigChange('endpoint')} onChange={handleConfigChange('endpoint')}
placeholder={'https://otlp.arize.com'} placeholder="https://otlp.arize.com"
/> />
</> </>
)} )}
{type === TracingProvider.phoenix && ( {type === TracingProvider.phoenix && (
<> <>
<Field <Field
label='API Key' label="API Key"
labelClassName='!text-sm' labelClassName="!text-sm"
isRequired isRequired
value={(config as PhoenixConfig).api_key} value={(config as PhoenixConfig).api_key}
onChange={handleConfigChange('api_key')} onChange={handleConfigChange('api_key')}
@ -352,41 +355,41 @@ const ProviderConfigModal: FC<Props> = ({
/> />
<Field <Field
label={t(`${I18N_PREFIX}.project`)!} label={t(`${I18N_PREFIX}.project`)!}
labelClassName='!text-sm' labelClassName="!text-sm"
isRequired isRequired
value={(config as PhoenixConfig).project} value={(config as PhoenixConfig).project}
onChange={handleConfigChange('project')} onChange={handleConfigChange('project')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.project`) })!} placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.project`) })!}
/> />
<Field <Field
label='Endpoint' label="Endpoint"
labelClassName='!text-sm' labelClassName="!text-sm"
value={(config as PhoenixConfig).endpoint} value={(config as PhoenixConfig).endpoint}
onChange={handleConfigChange('endpoint')} onChange={handleConfigChange('endpoint')}
placeholder={'https://app.phoenix.arize.com'} placeholder="https://app.phoenix.arize.com"
/> />
</> </>
)} )}
{type === TracingProvider.aliyun && ( {type === TracingProvider.aliyun && (
<> <>
<Field <Field
label='License Key' label="License Key"
labelClassName='!text-sm' labelClassName="!text-sm"
isRequired isRequired
value={(config as AliyunConfig).license_key} value={(config as AliyunConfig).license_key}
onChange={handleConfigChange('license_key')} onChange={handleConfigChange('license_key')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'License Key' })!} placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'License Key' })!}
/> />
<Field <Field
label='Endpoint' label="Endpoint"
labelClassName='!text-sm' labelClassName="!text-sm"
value={(config as AliyunConfig).endpoint} value={(config as AliyunConfig).endpoint}
onChange={handleConfigChange('endpoint')} onChange={handleConfigChange('endpoint')}
placeholder={'https://tracing.arms.aliyuncs.com'} placeholder="https://tracing.arms.aliyuncs.com"
/> />
<Field <Field
label='App Name' label="App Name"
labelClassName='!text-sm' labelClassName="!text-sm"
value={(config as AliyunConfig).app_name} value={(config as AliyunConfig).app_name}
onChange={handleConfigChange('app_name')} onChange={handleConfigChange('app_name')}
/> />
@ -395,36 +398,36 @@ const ProviderConfigModal: FC<Props> = ({
{type === TracingProvider.tencent && ( {type === TracingProvider.tencent && (
<> <>
<Field <Field
label='Token' label="Token"
labelClassName='!text-sm' labelClassName="!text-sm"
isRequired isRequired
value={(config as TencentConfig).token} value={(config as TencentConfig).token}
onChange={handleConfigChange('token')} onChange={handleConfigChange('token')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'Token' })!} placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'Token' })!}
/> />
<Field <Field
label='Endpoint' label="Endpoint"
labelClassName='!text-sm' labelClassName="!text-sm"
isRequired isRequired
value={(config as TencentConfig).endpoint} value={(config as TencentConfig).endpoint}
onChange={handleConfigChange('endpoint')} onChange={handleConfigChange('endpoint')}
placeholder='https://your-region.cls.tencentcs.com' placeholder="https://your-region.cls.tencentcs.com"
/> />
<Field <Field
label='Service Name' label="Service Name"
labelClassName='!text-sm' labelClassName="!text-sm"
isRequired isRequired
value={(config as TencentConfig).service_name} value={(config as TencentConfig).service_name}
onChange={handleConfigChange('service_name')} onChange={handleConfigChange('service_name')}
placeholder='dify_app' placeholder="dify_app"
/> />
</> </>
)} )}
{type === TracingProvider.weave && ( {type === TracingProvider.weave && (
<> <>
<Field <Field
label='API Key' label="API Key"
labelClassName='!text-sm' labelClassName="!text-sm"
isRequired isRequired
value={(config as WeaveConfig).api_key} value={(config as WeaveConfig).api_key}
onChange={handleConfigChange('api_key')} onChange={handleConfigChange('api_key')}
@ -432,40 +435,40 @@ const ProviderConfigModal: FC<Props> = ({
/> />
<Field <Field
label={t(`${I18N_PREFIX}.project`)!} label={t(`${I18N_PREFIX}.project`)!}
labelClassName='!text-sm' labelClassName="!text-sm"
isRequired isRequired
value={(config as WeaveConfig).project} value={(config as WeaveConfig).project}
onChange={handleConfigChange('project')} onChange={handleConfigChange('project')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.project`) })!} placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.project`) })!}
/> />
<Field <Field
label='Entity' label="Entity"
labelClassName='!text-sm' labelClassName="!text-sm"
value={(config as WeaveConfig).entity} value={(config as WeaveConfig).entity}
onChange={handleConfigChange('entity')} onChange={handleConfigChange('entity')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'Entity' })!} placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'Entity' })!}
/> />
<Field <Field
label='Endpoint' label="Endpoint"
labelClassName='!text-sm' labelClassName="!text-sm"
value={(config as WeaveConfig).endpoint} value={(config as WeaveConfig).endpoint}
onChange={handleConfigChange('endpoint')} onChange={handleConfigChange('endpoint')}
placeholder={'https://trace.wandb.ai/'} placeholder="https://trace.wandb.ai/"
/> />
<Field <Field
label='Host' label="Host"
labelClassName='!text-sm' labelClassName="!text-sm"
value={(config as WeaveConfig).host} value={(config as WeaveConfig).host}
onChange={handleConfigChange('host')} onChange={handleConfigChange('host')}
placeholder={'https://api.wandb.ai'} placeholder="https://api.wandb.ai"
/> />
</> </>
)} )}
{type === TracingProvider.langSmith && ( {type === TracingProvider.langSmith && (
<> <>
<Field <Field
label='API Key' label="API Key"
labelClassName='!text-sm' labelClassName="!text-sm"
isRequired isRequired
value={(config as LangSmithConfig).api_key} value={(config as LangSmithConfig).api_key}
onChange={handleConfigChange('api_key')} onChange={handleConfigChange('api_key')}
@ -473,18 +476,18 @@ const ProviderConfigModal: FC<Props> = ({
/> />
<Field <Field
label={t(`${I18N_PREFIX}.project`)!} label={t(`${I18N_PREFIX}.project`)!}
labelClassName='!text-sm' labelClassName="!text-sm"
isRequired isRequired
value={(config as LangSmithConfig).project} value={(config as LangSmithConfig).project}
onChange={handleConfigChange('project')} onChange={handleConfigChange('project')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.project`) })!} placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.project`) })!}
/> />
<Field <Field
label='Endpoint' label="Endpoint"
labelClassName='!text-sm' labelClassName="!text-sm"
value={(config as LangSmithConfig).endpoint} value={(config as LangSmithConfig).endpoint}
onChange={handleConfigChange('endpoint')} onChange={handleConfigChange('endpoint')}
placeholder={'https://api.smith.langchain.com'} placeholder="https://api.smith.langchain.com"
/> />
</> </>
)} )}
@ -492,7 +495,7 @@ const ProviderConfigModal: FC<Props> = ({
<> <>
<Field <Field
label={t(`${I18N_PREFIX}.secretKey`)!} label={t(`${I18N_PREFIX}.secretKey`)!}
labelClassName='!text-sm' labelClassName="!text-sm"
value={(config as LangFuseConfig).secret_key} value={(config as LangFuseConfig).secret_key}
isRequired isRequired
onChange={handleConfigChange('secret_key')} onChange={handleConfigChange('secret_key')}
@ -500,51 +503,51 @@ const ProviderConfigModal: FC<Props> = ({
/> />
<Field <Field
label={t(`${I18N_PREFIX}.publicKey`)!} label={t(`${I18N_PREFIX}.publicKey`)!}
labelClassName='!text-sm' labelClassName="!text-sm"
isRequired isRequired
value={(config as LangFuseConfig).public_key} value={(config as LangFuseConfig).public_key}
onChange={handleConfigChange('public_key')} onChange={handleConfigChange('public_key')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.publicKey`) })!} placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.publicKey`) })!}
/> />
<Field <Field
label='Host' label="Host"
labelClassName='!text-sm' labelClassName="!text-sm"
isRequired isRequired
value={(config as LangFuseConfig).host} value={(config as LangFuseConfig).host}
onChange={handleConfigChange('host')} onChange={handleConfigChange('host')}
placeholder='https://cloud.langfuse.com' placeholder="https://cloud.langfuse.com"
/> />
</> </>
)} )}
{type === TracingProvider.opik && ( {type === TracingProvider.opik && (
<> <>
<Field <Field
label='API Key' label="API Key"
labelClassName='!text-sm' labelClassName="!text-sm"
value={(config as OpikConfig).api_key} value={(config as OpikConfig).api_key}
onChange={handleConfigChange('api_key')} onChange={handleConfigChange('api_key')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'API Key' })!} placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'API Key' })!}
/> />
<Field <Field
label={t(`${I18N_PREFIX}.project`)!} label={t(`${I18N_PREFIX}.project`)!}
labelClassName='!text-sm' labelClassName="!text-sm"
value={(config as OpikConfig).project} value={(config as OpikConfig).project}
onChange={handleConfigChange('project')} onChange={handleConfigChange('project')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.project`) })!} placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.project`) })!}
/> />
<Field <Field
label='Workspace' label="Workspace"
labelClassName='!text-sm' labelClassName="!text-sm"
value={(config as OpikConfig).workspace} value={(config as OpikConfig).workspace}
onChange={handleConfigChange('workspace')} onChange={handleConfigChange('workspace')}
placeholder={'default'} placeholder="default"
/> />
<Field <Field
label='Url' label="Url"
labelClassName='!text-sm' labelClassName="!text-sm"
value={(config as OpikConfig).url} value={(config as OpikConfig).url}
onChange={handleConfigChange('url')} onChange={handleConfigChange('url')}
placeholder={'https://www.comet.com/opik/api/'} placeholder="https://www.comet.com/opik/api/"
/> />
</> </>
)} )}
@ -552,15 +555,15 @@ const ProviderConfigModal: FC<Props> = ({
<> <>
<Field <Field
label={t(`${I18N_PREFIX}.trackingUri`)!} label={t(`${I18N_PREFIX}.trackingUri`)!}
labelClassName='!text-sm' labelClassName="!text-sm"
value={(config as MLflowConfig).tracking_uri} value={(config as MLflowConfig).tracking_uri}
isRequired isRequired
onChange={handleConfigChange('tracking_uri')} onChange={handleConfigChange('tracking_uri')}
placeholder={'http://localhost:5000'} placeholder="http://localhost:5000"
/> />
<Field <Field
label={t(`${I18N_PREFIX}.experimentId`)!} label={t(`${I18N_PREFIX}.experimentId`)!}
labelClassName='!text-sm' labelClassName="!text-sm"
isRequired isRequired
value={(config as MLflowConfig).experiment_id} value={(config as MLflowConfig).experiment_id}
onChange={handleConfigChange('experiment_id')} onChange={handleConfigChange('experiment_id')}
@ -568,14 +571,14 @@ const ProviderConfigModal: FC<Props> = ({
/> />
<Field <Field
label={t(`${I18N_PREFIX}.username`)!} label={t(`${I18N_PREFIX}.username`)!}
labelClassName='!text-sm' labelClassName="!text-sm"
value={(config as MLflowConfig).username} value={(config as MLflowConfig).username}
onChange={handleConfigChange('username')} onChange={handleConfigChange('username')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.username`) })!} placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.username`) })!}
/> />
<Field <Field
label={t(`${I18N_PREFIX}.password`)!} label={t(`${I18N_PREFIX}.password`)!}
labelClassName='!text-sm' labelClassName="!text-sm"
value={(config as MLflowConfig).password} value={(config as MLflowConfig).password}
onChange={handleConfigChange('password')} onChange={handleConfigChange('password')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.password`) })!} placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.password`) })!}
@ -586,7 +589,7 @@ const ProviderConfigModal: FC<Props> = ({
<> <>
<Field <Field
label={t(`${I18N_PREFIX}.experimentId`)!} label={t(`${I18N_PREFIX}.experimentId`)!}
labelClassName='!text-sm' labelClassName="!text-sm"
value={(config as DatabricksConfig).experiment_id} value={(config as DatabricksConfig).experiment_id}
onChange={handleConfigChange('experiment_id')} onChange={handleConfigChange('experiment_id')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.experimentId`) })!} placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.experimentId`) })!}
@ -594,7 +597,7 @@ const ProviderConfigModal: FC<Props> = ({
/> />
<Field <Field
label={t(`${I18N_PREFIX}.databricksHost`)!} label={t(`${I18N_PREFIX}.databricksHost`)!}
labelClassName='!text-sm' labelClassName="!text-sm"
value={(config as DatabricksConfig).host} value={(config as DatabricksConfig).host}
onChange={handleConfigChange('host')} onChange={handleConfigChange('host')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.databricksHost`) })!} placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.databricksHost`) })!}
@ -602,21 +605,21 @@ const ProviderConfigModal: FC<Props> = ({
/> />
<Field <Field
label={t(`${I18N_PREFIX}.clientId`)!} label={t(`${I18N_PREFIX}.clientId`)!}
labelClassName='!text-sm' labelClassName="!text-sm"
value={(config as DatabricksConfig).client_id} value={(config as DatabricksConfig).client_id}
onChange={handleConfigChange('client_id')} onChange={handleConfigChange('client_id')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.clientId`) })!} placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.clientId`) })!}
/> />
<Field <Field
label={t(`${I18N_PREFIX}.clientSecret`)!} label={t(`${I18N_PREFIX}.clientSecret`)!}
labelClassName='!text-sm' labelClassName="!text-sm"
value={(config as DatabricksConfig).client_secret} value={(config as DatabricksConfig).client_secret}
onChange={handleConfigChange('client_secret')} onChange={handleConfigChange('client_secret')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.clientSecret`) })!} placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.clientSecret`) })!}
/> />
<Field <Field
label={t(`${I18N_PREFIX}.personalAccessToken`)!} label={t(`${I18N_PREFIX}.personalAccessToken`)!}
labelClassName='!text-sm' labelClassName="!text-sm"
value={(config as DatabricksConfig).personal_access_token} value={(config as DatabricksConfig).personal_access_token}
onChange={handleConfigChange('personal_access_token')} onChange={handleConfigChange('personal_access_token')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.personalAccessToken`) })!} placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.personalAccessToken`) })!}
@ -624,36 +627,36 @@ const ProviderConfigModal: FC<Props> = ({
</> </>
)} )}
</div> </div>
<div className='my-8 flex h-8 items-center justify-between'> <div className="my-8 flex h-8 items-center justify-between">
<a <a
className='flex items-center space-x-1 text-xs font-normal leading-[18px] text-[#155EEF]' className="flex items-center space-x-1 text-xs font-normal leading-[18px] text-[#155EEF]"
target='_blank' target="_blank"
href={docURL[type]} href={docURL[type]}
> >
<span>{t(`${I18N_PREFIX}.viewDocsLink`, { key: t(`app.tracing.${type}.title`) })}</span> <span>{t(`${I18N_PREFIX}.viewDocsLink`, { key: t(`app.tracing.${type}.title`) })}</span>
<LinkExternal02 className='h-3 w-3' /> <LinkExternal02 className="h-3 w-3" />
</a> </a>
<div className='flex items-center'> <div className="flex items-center">
{isEdit && ( {isEdit && (
<> <>
<Button <Button
className='h-9 text-sm font-medium text-text-secondary' className="h-9 text-sm font-medium text-text-secondary"
onClick={showRemoveConfirm} onClick={showRemoveConfirm}
> >
<span className='text-[#D92D20]'>{t('common.operation.remove')}</span> <span className="text-[#D92D20]">{t('common.operation.remove')}</span>
</Button> </Button>
<Divider type='vertical' className='mx-3 h-[18px]' /> <Divider type="vertical" className="mx-3 h-[18px]" />
</> </>
)} )}
<Button <Button
className='mr-2 h-9 text-sm font-medium text-text-secondary' className="mr-2 h-9 text-sm font-medium text-text-secondary"
onClick={onCancel} onClick={onCancel}
> >
{t('common.operation.cancel')} {t('common.operation.cancel')}
</Button> </Button>
<Button <Button
className='h-9 text-sm font-medium' className="h-9 text-sm font-medium"
variant='primary' variant="primary"
onClick={handleSave} onClick={handleSave}
loading={isSaving} loading={isSaving}
> >
@ -663,14 +666,15 @@ const ProviderConfigModal: FC<Props> = ({
</div> </div>
</div> </div>
<div className='border-t-[0.5px] border-divider-regular'> <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'> <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' /> <Lock01 className="mr-1 h-3 w-3 text-text-tertiary" />
{t('common.modelProvider.encrypted.front')} {t('common.modelProvider.encrypted.front')}
<a <a
className='mx-1 text-primary-600' className="mx-1 text-primary-600"
target='_blank' rel='noopener noreferrer' target="_blank"
href='https://pycryptodome.readthedocs.io/en/latest/src/cipher/oaep.html' rel="noopener noreferrer"
href="https://pycryptodome.readthedocs.io/en/latest/src/cipher/oaep.html"
> >
PKCS1_OAEP PKCS1_OAEP
</a> </a>
@ -685,7 +689,7 @@ const ProviderConfigModal: FC<Props> = ({
: ( : (
<Confirm <Confirm
isShow isShow
type='warning' type="warning"
title={t(`${I18N_PREFIX}.removeConfirmTitle`, { key: t(`app.tracing.${type}.title`) })!} title={t(`${I18N_PREFIX}.removeConfirmTitle`, { key: t(`app.tracing.${type}.title`) })!}
content={t(`${I18N_PREFIX}.removeConfirmContent`)} content={t(`${I18N_PREFIX}.removeConfirmContent`)}
onConfirm={handleRemove} onConfirm={handleRemove}

View File

@ -1,14 +1,14 @@
'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import React, { useCallback } from 'react'
import { import {
RiEqualizer2Line, RiEqualizer2Line,
} from '@remixicon/react' } from '@remixicon/react'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next' 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 { 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 { 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' const I18N_PREFIX = 'app.tracing'
@ -78,30 +78,30 @@ const ProviderPanel: FC<Props> = ({
)} )}
onClick={handleChosen} onClick={handleChosen}
> >
<div className={'flex items-center justify-between space-x-1'}> <div className="flex items-center justify-between space-x-1">
<div className='flex items-center'> <div className="flex items-center">
<Icon className='h-6' /> <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>} {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> </div>
{!readOnly && ( {!readOnly && (
<div className={'flex items-center justify-between space-x-1'}> <div className="flex items-center justify-between space-x-1">
{hasConfigured && ( {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} > <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' /> <View className="h-3 w-3" />
<div className='text-xs font-medium'>{t(`${I18N_PREFIX}.view`)}</div> <div className="text-xs font-medium">{t(`${I18N_PREFIX}.view`)}</div>
</div> </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} onClick={handleConfigBtnClick}
> >
<RiEqualizer2Line className='h-3 w-3' /> <RiEqualizer2Line className="h-3 w-3" />
<div className='text-xs font-medium'>{t(`${I18N_PREFIX}.config`)}</div> <div className="text-xs font-medium">{t(`${I18N_PREFIX}.config`)}</div>
</div> </div>
</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`)} {t(`${I18N_PREFIX}.${type}.description`)}
</div> </div>
</div> </div>

View File

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

View File

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

View File

@ -1,7 +1,7 @@
'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import React, { useEffect } from 'react'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import React, { useEffect } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useAppContext } from '@/context/app-context' import { useAppContext } from '@/context/app-context'
import useDocumentTitle from '@/hooks/use-document-title' 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' import MainDetail from '@/app/components/datasets/documents/detail'
export type IDocumentDetailProps = { export type IDocumentDetailProps = {
params: Promise<{ datasetId: string; documentId: string }> params: Promise<{ datasetId: string, documentId: string }>
} }
const DocumentDetail = async (props: IDocumentDetailProps) => { const DocumentDetail = async (props: IDocumentDetailProps) => {

View File

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

View File

@ -1,9 +1,6 @@
'use client' '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 { RemixiconComponentType } from '@remixicon/react'
import type { FC } from 'react'
import { import {
RiEqualizer2Fill, RiEqualizer2Fill,
RiEqualizer2Line, RiEqualizer2Line,
@ -12,17 +9,20 @@ import {
RiFocus2Fill, RiFocus2Fill,
RiFocus2Line, RiFocus2Line,
} from '@remixicon/react' } 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 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 { useStore } from '@/app/components/app/store'
import { useAppContext } from '@/context/app-context'
import { PipelineFill, PipelineLine } from '@/app/components/base/icons/src/vender/pipeline' import { PipelineFill, PipelineLine } from '@/app/components/base/icons/src/vender/pipeline'
import { useDatasetDetail, useDatasetRelatedApps } from '@/service/knowledge/use-dataset' import Loading from '@/app/components/base/loading'
import useDocumentTitle from '@/hooks/use-document-title'
import ExtraInfo from '@/app/components/datasets/extra-info' 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 { 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' import { cn } from '@/utils/classnames'
export type IAppDetailLayoutProps = { export type IAppDetailLayoutProps = {
@ -115,7 +115,7 @@ const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
}, [isMobile, setAppSidebarExpand]) }, [isMobile, setAppSidebarExpand])
if (!datasetRes && !error) if (!datasetRes && !error)
return <Loading type='app' /> return <Loading type="app" />
return ( return (
<div <div
@ -128,7 +128,8 @@ const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
indexingTechnique: datasetRes?.indexing_technique, indexingTechnique: datasetRes?.indexing_technique,
dataset: datasetRes, dataset: datasetRes,
mutateDatasetRes, mutateDatasetRes,
}}> }}
>
{!hideSideBar && ( {!hideSideBar && (
<AppSideBar <AppSideBar
navigation={navigation} navigation={navigation}
@ -137,10 +138,10 @@ const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
? mode => <ExtraInfo relatedApps={relatedApps} expand={mode === 'expand'} documentCount={datasetRes?.document_count} /> ? mode => <ExtraInfo relatedApps={relatedApps} expand={mode === 'expand'} documentCount={datasetRes?.document_count} />
: undefined : 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> </DatasetDetailContext.Provider>
</div> </div>
) )

View File

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

View File

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

View File

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

View File

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

View File

@ -1,18 +1,18 @@
import React from 'react'
import type { ReactNode } 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 SwrInitializer from '@/app/components/swr-initializer'
import { AppContextProvider } from '@/context/app-context' 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 { EventEmitterContextProvider } from '@/context/event-emitter'
import { ProviderContextProvider } from '@/context/provider-context'
import { ModalContextProvider } from '@/context/modal-context' import { ModalContextProvider } from '@/context/modal-context'
import GotoAnything from '@/app/components/goto-anything' import { ProviderContextProvider } from '@/context/provider-context'
import Zendesk from '@/app/components/base/zendesk'
import PartnerStack from '../components/billing/partner-stack' import PartnerStack from '../components/billing/partner-stack'
import ReadmePanel from '@/app/components/plugins/readme-panel'
import Splash from '../components/splash' import Splash from '../components/splash'
const Layout = ({ children }: { children: ReactNode }) => { 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 PluginPage from '@/app/components/plugins/plugin-page'
import PluginsPanel from '@/app/components/plugins/plugin-page/plugins-panel' import PluginsPanel from '@/app/components/plugins/plugin-page/plugins-panel'
import Marketplace from '@/app/components/plugins/marketplace'
import { getLocaleOnServer } from '@/i18n-config/server' import { getLocaleOnServer } from '@/i18n-config/server'
const PluginList = async () => { const PluginList = async () => {
@ -8,7 +8,7 @@ const PluginList = async () => {
return ( return (
<PluginPage <PluginPage
plugins={<PluginsPanel />} 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 ToolProviderList from '@/app/components/tools/provider-list'
import { useAppContext } from '@/context/app-context' import { useAppContext } from '@/context/app-context'
import useDocumentTitle from '@/hooks/use-document-title' import useDocumentTitle from '@/hooks/use-document-title'
const ToolsList: FC = () => { const ToolsList: FC = () => {
const router = useRouter() const router = useRouter()
const { isCurrentWorkspaceDatasetOperator } = useAppContext() const { isCurrentWorkspaceDatasetOperator } = useAppContext()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,12 +1,12 @@
'use client' 'use client'
import { useRouter, useSearchParams } from 'next/navigation' import { useRouter, useSearchParams } from 'next/navigation'
import React, { useCallback, useEffect } from 'react' 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 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 ExternalMemberSSOAuth = () => {
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
@ -68,9 +68,11 @@ const ExternalMemberSSOAuth = () => {
}, [handleSSOLogin]) }, [handleSSOLogin])
if (!systemFeatures.webapp_auth.sso_config.protocol) { if (!systemFeatures.webapp_auth.sso_config.protocol) {
return <div className="flex h-full items-center justify-center"> return (
<AppUnavailable code={403} unknownReason='sso protocol is invalid.' /> <div className="flex h-full items-center justify-center">
<AppUnavailable code={403} unknownReason="sso protocol is invalid." />
</div> </div>
)
} }
return ( return (

View File

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

View File

@ -1,17 +1,17 @@
'use client' 'use client'
import { noop } from 'lodash-es'
import Link from 'next/link' import Link from 'next/link'
import { useRouter, useSearchParams } from 'next/navigation'
import { useCallback, useState } from 'react' import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useRouter, useSearchParams } from 'next/navigation'
import { useContext } from 'use-context-selector' import { useContext } from 'use-context-selector'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import Toast from '@/app/components/base/toast' import Toast from '@/app/components/base/toast'
import { emailRegex } from '@/config' import { emailRegex } from '@/config'
import { webAppLogin } from '@/service/common'
import Input from '@/app/components/base/input'
import I18NContext from '@/context/i18n' import I18NContext from '@/context/i18n'
import { useWebAppStore } from '@/context/web-app-context' import { useWebAppStore } from '@/context/web-app-context'
import { noop } from 'lodash-es' import { webAppLogin } from '@/service/common'
import { fetchAccessToken } from '@/service/share' import { fetchAccessToken } from '@/service/share'
import { setWebAppAccessToken, setWebAppPassport } from '@/service/webapp-auth' import { setWebAppAccessToken, setWebAppPassport } from '@/service/webapp-auth'
@ -107,8 +107,9 @@ export default function MailAndPasswordAuth({ isEmailSetup }: MailAndPasswordAut
} }
} }
return <form onSubmit={noop}> return (
<div className='mb-3'> <form onSubmit={noop}>
<div className="mb-3">
<label htmlFor="email" className="system-md-semibold my-2 text-text-secondary"> <label htmlFor="email" className="system-md-semibold my-2 text-text-secondary">
{t('login.email')} {t('login.email')}
</label> </label>
@ -125,9 +126,9 @@ export default function MailAndPasswordAuth({ isEmailSetup }: MailAndPasswordAut
</div> </div>
</div> </div>
<div className='mb-3'> <div className="mb-3">
<label htmlFor="password" className="my-2 flex items-center justify-between"> <label htmlFor="password" className="my-2 flex items-center justify-between">
<span className='system-md-semibold text-text-secondary'>{t('login.password')}</span> <span className="system-md-semibold text-text-secondary">{t('login.password')}</span>
<Link <Link
href={`/webapp-reset-password?${searchParams.toString()}`} 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'}`} className={`system-xs-regular ${isEmailSetup ? 'text-components-button-secondary-accent-text' : 'pointer-events-none text-components-button-secondary-accent-text-disabled'}`}
@ -154,7 +155,7 @@ export default function MailAndPasswordAuth({ isEmailSetup }: MailAndPasswordAut
<div className="absolute inset-y-0 right-0 flex items-center"> <div className="absolute inset-y-0 right-0 flex items-center">
<Button <Button
type="button" type="button"
variant='ghost' variant="ghost"
onClick={() => setShowPassword(!showPassword)} onClick={() => setShowPassword(!showPassword)}
> >
{showPassword ? '👀' : '😝'} {showPassword ? '👀' : '😝'}
@ -163,14 +164,17 @@ export default function MailAndPasswordAuth({ isEmailSetup }: MailAndPasswordAut
</div> </div>
</div> </div>
<div className='mb-2'> <div className="mb-2">
<Button <Button
tabIndex={2} tabIndex={2}
variant='primary' variant="primary"
onClick={handleEmailPasswordLogin} onClick={handleEmailPasswordLogin}
disabled={isLoading || !email || !password} disabled={isLoading || !email || !password}
className="w-full" className="w-full"
>{t('login.signBtn')}</Button> >
{t('login.signBtn')}
</Button>
</div> </div>
</form> </form>
)
} }

View File

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

View File

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

View File

@ -1,16 +1,16 @@
'use client' 'use client'
import { RiContractLine, RiDoorLockLine, RiErrorWarningFill } from '@remixicon/react'
import Link from 'next/link'
import React, { useCallback, useEffect, useState } from 'react' import React, { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' 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 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 MailAndCodeAuth from './components/mail-and-code-auth'
import MailAndPasswordAuth from './components/mail-and-password-auth' import MailAndPasswordAuth from './components/mail-and-password-auth'
import SSOAuth from './components/sso-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 NormalForm = () => {
const { t } = useTranslation() const { t } = useTranslation()
@ -37,57 +37,66 @@ const NormalForm = () => {
init() init()
}, [init]) }, [init])
if (isLoading) { if (isLoading) {
return <div className={ return (
<div className={
cn( cn(
'flex w-full grow flex-col items-center justify-center', 'flex w-full grow flex-col items-center justify-center',
'px-6', 'px-6',
'md:px-[108px]', 'md:px-[108px]',
) )
}> }
<Loading type='area' /> >
<Loading type="area" />
</div> </div>
)
} }
if (systemFeatures.license?.status === LicenseStatus.LOST) { if (systemFeatures.license?.status === LicenseStatus.LOST) {
return <div className='mx-auto mt-8 w-full'> return (
<div className='relative'> <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="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'> <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' /> <RiContractLine className="h-5 w-5" />
<RiErrorWarningFill className='absolute -right-1 -top-1 h-4 w-4 text-text-warning-secondary' /> <RiErrorWarningFill className="absolute -right-1 -top-1 h-4 w-4 text-text-warning-secondary" />
</div> </div>
<p className='system-sm-medium text-text-primary'>{t('login.licenseLost')}</p> <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> <p className="system-xs-regular mt-1 text-text-tertiary">{t('login.licenseLostTip')}</p>
</div> </div>
</div> </div>
</div> </div>
)
} }
if (systemFeatures.license?.status === LicenseStatus.EXPIRED) { if (systemFeatures.license?.status === LicenseStatus.EXPIRED) {
return <div className='mx-auto mt-8 w-full'> return (
<div className='relative'> <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="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'> <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' /> <RiContractLine className="h-5 w-5" />
<RiErrorWarningFill className='absolute -right-1 -top-1 h-4 w-4 text-text-warning-secondary' /> <RiErrorWarningFill className="absolute -right-1 -top-1 h-4 w-4 text-text-warning-secondary" />
</div> </div>
<p className='system-sm-medium text-text-primary'>{t('login.licenseExpired')}</p> <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> <p className="system-xs-regular mt-1 text-text-tertiary">{t('login.licenseExpiredTip')}</p>
</div> </div>
</div> </div>
</div> </div>
)
} }
if (systemFeatures.license?.status === LicenseStatus.INACTIVE) { if (systemFeatures.license?.status === LicenseStatus.INACTIVE) {
return <div className='mx-auto mt-8 w-full'> return (
<div className='relative'> <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="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'> <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' /> <RiContractLine className="h-5 w-5" />
<RiErrorWarningFill className='absolute -right-1 -top-1 h-4 w-4 text-text-warning-secondary' /> <RiErrorWarningFill className="absolute -right-1 -top-1 h-4 w-4 text-text-warning-secondary" />
</div> </div>
<p className='system-sm-medium text-text-primary'>{t('login.licenseInactive')}</p> <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> <p className="system-xs-regular mt-1 text-text-tertiary">{t('login.licenseInactiveTip')}</p>
</div> </div>
</div> </div>
</div> </div>
)
} }
return ( return (
@ -95,78 +104,106 @@ const NormalForm = () => {
<div className="mx-auto mt-8 w-full"> <div className="mx-auto mt-8 w-full">
<div className="mx-auto 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> <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>
<div className="relative"> <div className="relative">
<div className="mt-6 flex flex-col gap-3"> <div className="mt-6 flex flex-col gap-3">
{systemFeatures.sso_enforced_for_signin && <div className='w-full'> {systemFeatures.sso_enforced_for_signin && (
<div className="w-full">
<SSOAuth protocol={systemFeatures.sso_enforced_for_signin_protocol} /> <SSOAuth protocol={systemFeatures.sso_enforced_for_signin_protocol} />
</div>} </div>
)}
</div> </div>
{showORLine && <div className="relative mt-6"> {showORLine && (
<div className="relative mt-6">
<div className="absolute inset-0 flex items-center" aria-hidden="true"> <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>
<div className="relative flex justify-center"> <div className="relative flex justify-center">
<span className="system-xs-medium-uppercase px-2 text-text-tertiary">{t('login.or')}</span> <span className="system-xs-medium-uppercase px-2 text-text-tertiary">{t('login.or')}</span>
</div> </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> </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> {
(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>
<div className="relative my-2 py-2"> <div className="relative my-2 py-2">
<div className="absolute inset-0 flex items-center" aria-hidden="true"> <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>
</div> </div>
</>} </>
{!systemFeatures.branding.enabled && <> )}
{!systemFeatures.branding.enabled && (
<>
<div className="system-xs-regular mt-2 block w-full text-text-tertiary"> <div className="system-xs-regular mt-2 block w-full text-text-tertiary">
{t('login.tosDesc')} {t('login.tosDesc')}
&nbsp; &nbsp;
<Link <Link
className='system-xs-medium text-text-secondary hover:underline' className="system-xs-medium text-text-secondary hover:underline"
target='_blank' rel='noopener noreferrer' target="_blank"
href='https://dify.ai/terms' rel="noopener noreferrer"
>{t('login.tos')}</Link> href="https://dify.ai/terms"
>
{t('login.tos')}
</Link>
&nbsp;&&nbsp; &nbsp;&&nbsp;
<Link <Link
className='system-xs-medium text-text-secondary hover:underline' className="system-xs-medium text-text-secondary hover:underline"
target='_blank' rel='noopener noreferrer' target="_blank"
href='https://dify.ai/privacy' rel="noopener noreferrer"
>{t('login.pp')}</Link> href="https://dify.ai/privacy"
>
{t('login.pp')}
</Link>
</div> </div>
{IS_CE_EDITION && <div className="w-hull system-xs-regular mt-2 block text-text-tertiary"> {IS_CE_EDITION && (
<div className="w-hull system-xs-regular mt-2 block text-text-tertiary">
{t('login.goToInit')} {t('login.goToInit')}
&nbsp; &nbsp;
<Link <Link
className='system-xs-medium text-text-secondary hover:underline' className="system-xs-medium text-text-secondary hover:underline"
href='/install' href="/install"
>{t('login.setAdminAccount')}</Link> >
</div>} {t('login.setAdminAccount')}
</>} </Link>
</div>
)}
</>
)}
</div> </div>
</div> </div>

View File

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

View File

@ -1,23 +1,25 @@
'use client' 'use client'
import type { Area } from 'react-easy-crop' 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 React, { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector' import { useContext } from 'use-context-selector'
import { RiDeleteBin5Line, RiPencilLine } from '@remixicon/react' import ImageInput from '@/app/components/base/app-icon-picker/ImageInput'
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 getCroppedImg from '@/app/components/base/app-icon-picker/utils' 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 { 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 } type AvatarWithEditProps = AvatarProps & { onSave?: () => void }
const AvatarWithEdit = ({ onSave, ...props }: AvatarWithEditProps) => { const AvatarWithEdit = ({ onSave, ...props }: AvatarWithEditProps) => {
@ -116,11 +118,13 @@ const AvatarWithEdit = ({ onSave, ...props }: AvatarWithEditProps) => {
setHoverArea(isRight ? 'right' : 'left') setHoverArea(isRight ? 'right' : 'left')
}} }}
> >
{hoverArea === 'right' && !onAvatarError ? ( {hoverArea === 'right' && !onAvatarError
? (
<span className="text-xs text-white"> <span className="text-xs text-white">
<RiDeleteBin5Line /> <RiDeleteBin5Line />
</span> </span>
) : ( )
: (
<span className="text-xs text-white"> <span className="text-xs text-white">
<RiPencilLine /> <RiPencilLine />
</span> </span>
@ -135,15 +139,15 @@ const AvatarWithEdit = ({ onSave, ...props }: AvatarWithEditProps) => {
isShow={isShowAvatarPicker} isShow={isShowAvatarPicker}
onClose={() => setIsShowAvatarPicker(false)} onClose={() => setIsShowAvatarPicker(false)}
> >
<ImageInput onImageInput={handleImageInput} cropShape='round' /> <ImageInput onImageInput={handleImageInput} cropShape="round" />
<Divider className='m-0' /> <Divider className="m-0" />
<div className='flex w-full items-center justify-center gap-2 p-3'> <div className="flex w-full items-center justify-center gap-2 p-3">
<Button className='w-full' onClick={() => setIsShowAvatarPicker(false)}> <Button className="w-full" onClick={() => setIsShowAvatarPicker(false)}>
{t('app.iconPicker.cancel')} {t('app.iconPicker.cancel')}
</Button> </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')} {t('app.iconPicker.ok')}
</Button> </Button>
</div> </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 React, { useState } from 'react'
import { Trans, useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
import { useRouter } from 'next/navigation'
import { useContext } from 'use-context-selector' 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 Button from '@/app/components/base/button'
import Input from '@/app/components/base/input' import Input from '@/app/components/base/input'
import Modal from '@/app/components/base/modal'
import { ToastContext } from '@/app/components/base/toast'
import { import {
checkEmailExisted, checkEmailExisted,
resetEmail, resetEmail,
sendVerifyCode, sendVerifyCode,
verifyEmail, verifyEmail,
} from '@/service/common' } 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 { useLogout } from '@/service/use-common'
import { asyncRunSafe } from '@/utils'
type Props = { type Props = {
show: boolean show: boolean
@ -116,7 +116,7 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => {
} }
const isValidEmail = (email: string): boolean => { 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 return rfc5322emailRegex.test(email) && email.length <= 254
} }
@ -201,35 +201,35 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => {
<Modal <Modal
isShow={show} isShow={show}
onClose={noop} 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}> <div className="absolute right-5 top-5 cursor-pointer p-1.5" onClick={onClose}>
<RiCloseLine className='h-5 w-5 text-text-tertiary' /> <RiCloseLine className="h-5 w-5 text-text-tertiary" />
</div> </div>
{step === STEP.start && ( {step === STEP.start && (
<> <>
<div className='title-2xl-semi-bold pb-3 text-text-primary'>{t('common.account.changeEmail.title')}</div> <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="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-medium text-text-warning">{t('common.account.changeEmail.authTip')}</div>
<div className='body-md-regular text-text-secondary'> <div className="body-md-regular text-text-secondary">
<Trans <Trans
i18nKey="common.account.changeEmail.content1" 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 }} values={{ email }}
/> />
</div> </div>
</div> </div>
<div className='pt-3'></div> <div className="pt-3"></div>
<div className='space-y-2'> <div className="space-y-2">
<Button <Button
className='!w-full' className="!w-full"
variant='primary' variant="primary"
onClick={sendCodeToOriginEmail} onClick={sendCodeToOriginEmail}
> >
{t('common.account.changeEmail.sendVerifyCode')} {t('common.account.changeEmail.sendVerifyCode')}
</Button> </Button>
<Button <Button
className='!w-full' className="!w-full"
onClick={onClose} onClick={onClose}
> >
{t('common.operation.cancel')} {t('common.operation.cancel')}
@ -239,86 +239,86 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => {
)} )}
{step === STEP.verifyOrigin && ( {step === STEP.verifyOrigin && (
<> <>
<div className='title-2xl-semi-bold pb-3 text-text-primary'>{t('common.account.changeEmail.verifyEmail')}</div> <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="space-y-0.5 pb-2 pt-1">
<div className='body-md-regular text-text-secondary'> <div className="body-md-regular text-text-secondary">
<Trans <Trans
i18nKey="common.account.changeEmail.content2" 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 }} values={{ email }}
/> />
</div> </div>
</div> </div>
<div className='pt-3'> <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="system-sm-medium mb-1 flex h-6 items-center text-text-secondary">{t('common.account.changeEmail.codeLabel')}</div>
<Input <Input
className='!w-full' className="!w-full"
placeholder={t('common.account.changeEmail.codePlaceholder')} placeholder={t('common.account.changeEmail.codePlaceholder')}
value={code} value={code}
onChange={e => setCode(e.target.value)} onChange={e => setCode(e.target.value)}
maxLength={6} maxLength={6}
/> />
</div> </div>
<div className='mt-3 space-y-2'> <div className="mt-3 space-y-2">
<Button <Button
disabled={code.length !== 6} disabled={code.length !== 6}
className='!w-full' className="!w-full"
variant='primary' variant="primary"
onClick={handleVerifyOriginEmail} onClick={handleVerifyOriginEmail}
> >
{t('common.account.changeEmail.continue')} {t('common.account.changeEmail.continue')}
</Button> </Button>
<Button <Button
className='!w-full' className="!w-full"
onClick={onClose} onClick={onClose}
> >
{t('common.operation.cancel')} {t('common.operation.cancel')}
</Button> </Button>
</div> </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> <span>{t('common.account.changeEmail.resendTip')}</span>
{time > 0 && ( {time > 0 && (
<span>{t('common.account.changeEmail.resendCount', { count: time })}</span> <span>{t('common.account.changeEmail.resendCount', { count: time })}</span>
)} )}
{!time && ( {!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> </div>
</> </>
)} )}
{step === STEP.newEmail && ( {step === STEP.newEmail && (
<> <>
<div className='title-2xl-semi-bold pb-3 text-text-primary'>{t('common.account.changeEmail.newEmail')}</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="space-y-0.5 pb-2 pt-1">
<div className='body-md-regular text-text-secondary'>{t('common.account.changeEmail.content3')}</div> <div className="body-md-regular text-text-secondary">{t('common.account.changeEmail.content3')}</div>
</div> </div>
<div className='pt-3'> <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="system-sm-medium mb-1 flex h-6 items-center text-text-secondary">{t('common.account.changeEmail.emailLabel')}</div>
<Input <Input
className='!w-full' className="!w-full"
placeholder={t('common.account.changeEmail.emailPlaceholder')} placeholder={t('common.account.changeEmail.emailPlaceholder')}
value={mail} value={mail}
onChange={e => handleNewEmailValueChange(e.target.value)} onChange={e => handleNewEmailValueChange(e.target.value)}
destructive={newEmailExited || unAvailableEmail} destructive={newEmailExited || unAvailableEmail}
/> />
{newEmailExited && ( {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 && ( {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>
<div className='mt-3 space-y-2'> <div className="mt-3 space-y-2">
<Button <Button
disabled={!mail || newEmailExited || unAvailableEmail || isCheckingEmail || !isValidEmail(mail)} disabled={!mail || newEmailExited || unAvailableEmail || isCheckingEmail || !isValidEmail(mail)}
className='!w-full' className="!w-full"
variant='primary' variant="primary"
onClick={sendCodeToNewEmail} onClick={sendCodeToNewEmail}
> >
{t('common.account.changeEmail.sendVerifyCode')} {t('common.account.changeEmail.sendVerifyCode')}
</Button> </Button>
<Button <Button
className='!w-full' className="!w-full"
onClick={onClose} onClick={onClose}
> >
{t('common.operation.cancel')} {t('common.operation.cancel')}
@ -328,49 +328,49 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => {
)} )}
{step === STEP.verifyNew && ( {step === STEP.verifyNew && (
<> <>
<div className='title-2xl-semi-bold pb-3 text-text-primary'>{t('common.account.changeEmail.verifyNew')}</div> <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="space-y-0.5 pb-2 pt-1">
<div className='body-md-regular text-text-secondary'> <div className="body-md-regular text-text-secondary">
<Trans <Trans
i18nKey="common.account.changeEmail.content4" 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 }} values={{ email: mail }}
/> />
</div> </div>
</div> </div>
<div className='pt-3'> <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="system-sm-medium mb-1 flex h-6 items-center text-text-secondary">{t('common.account.changeEmail.codeLabel')}</div>
<Input <Input
className='!w-full' className="!w-full"
placeholder={t('common.account.changeEmail.codePlaceholder')} placeholder={t('common.account.changeEmail.codePlaceholder')}
value={code} value={code}
onChange={e => setCode(e.target.value)} onChange={e => setCode(e.target.value)}
maxLength={6} maxLength={6}
/> />
</div> </div>
<div className='mt-3 space-y-2'> <div className="mt-3 space-y-2">
<Button <Button
disabled={code.length !== 6} disabled={code.length !== 6}
className='!w-full' className="!w-full"
variant='primary' variant="primary"
onClick={submitNewEmail} onClick={submitNewEmail}
> >
{t('common.account.changeEmail.changeTo', { email: mail })} {t('common.account.changeEmail.changeTo', { email: mail })}
</Button> </Button>
<Button <Button
className='!w-full' className="!w-full"
onClick={onClose} onClick={onClose}
> >
{t('common.operation.cancel')} {t('common.operation.cancel')}
</Button> </Button>
</div> </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> <span>{t('common.account.changeEmail.resendTip')}</span>
{time > 0 && ( {time > 0 && (
<span>{t('common.account.changeEmail.resendCount', { count: time })}</span> <span>{t('common.account.changeEmail.resendCount', { count: time })}</span>
)} )}
{!time && ( {!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> </div>
</> </>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,11 +1,11 @@
'use client' 'use client'
import { useTranslation } from 'react-i18next'
import { useCallback, useState } from 'react' import { useCallback, useState } from 'react'
import CheckEmail from './components/check-email' import { useTranslation } from 'react-i18next'
import VerifyEmail from './components/verify-email'
import FeedBack from './components/feed-back'
import CustomDialog from '@/app/components/base/dialog' import CustomDialog from '@/app/components/base/dialog'
import { COUNT_DOWN_KEY, COUNT_DOWN_TIME_MS } from '@/app/components/signin/countdown' 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 = { type DeleteAccountProps = {
onCancel: () => void onCancel: () => void
@ -29,7 +29,8 @@ export default function DeleteAccount(props: DeleteAccountProps) {
if (showFeedbackDialog) if (showFeedbackDialog)
return <FeedBack onCancel={props.onCancel} onConfirm={props.onConfirm} /> return <FeedBack onCancel={props.onCancel} onConfirm={props.onConfirm} />
return <CustomDialog return (
<CustomDialog
show={true} show={true}
onClose={props.onCancel} onClose={props.onCancel}
title={t('common.account.delete')} title={t('common.account.delete')}
@ -37,8 +38,14 @@ export default function DeleteAccount(props: DeleteAccountProps) {
footer={false} footer={false}
> >
{!showVerifyEmail && <CheckEmail onCancel={props.onCancel} onConfirm={handleEmailCheckSuccess} />} {!showVerifyEmail && <CheckEmail onCancel={props.onCancel} onConfirm={handleEmailCheckSuccess} />}
{showVerifyEmail && <VerifyEmail onCancel={props.onCancel} onConfirm={() => { {showVerifyEmail && (
<VerifyEmail
onCancel={props.onCancel}
onConfirm={() => {
setShowFeedbackDialog(true) setShowFeedbackDialog(true)
}} />} }}
/>
)}
</CustomDialog> </CustomDialog>
)
} }

View File

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

View File

@ -1,14 +1,14 @@
import React from 'react'
import type { ReactNode } 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 SwrInitor from '@/app/components/swr-initializer'
import { AppContextProvider } from '@/context/app-context' 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 { EventEmitterContextProvider } from '@/context/event-emitter'
import { ProviderContextProvider } from '@/context/provider-context'
import { ModalContextProvider } from '@/context/modal-context' import { ModalContextProvider } from '@/context/modal-context'
import { ProviderContextProvider } from '@/context/provider-context'
import Header from './header'
const Layout = ({ children }: { children: ReactNode }) => { const Layout = ({ children }: { children: ReactNode }) => {
return ( return (
@ -23,7 +23,7 @@ const Layout = ({ children }: { children: ReactNode }) => {
<HeaderWrapper> <HeaderWrapper>
<Header /> <Header />
</HeaderWrapper> </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} {children}
</div> </div>
</ModalContextProvider> </ModalContextProvider>

View File

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

View File

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

View File

@ -1,15 +1,5 @@
'use client' '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 { import {
RiAccountCircleLine, RiAccountCircleLine,
RiGlobalLine, RiGlobalLine,
@ -18,7 +8,17 @@ import {
RiTranslate2, RiTranslate2,
} from '@remixicon/react' } from '@remixicon/react'
import dayjs from 'dayjs' 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 { useIsLogin } from '@/service/use-common'
import { useAuthorizeOAuthApp, useOAuthAppInfo } from '@/service/use-oauth'
import { import {
OAUTH_AUTHORIZE_PENDING_KEY, OAUTH_AUTHORIZE_PENDING_KEY,
OAUTH_AUTHORIZE_PENDING_TTL, OAUTH_AUTHORIZE_PENDING_TTL,
@ -124,49 +124,49 @@ export default function OAuthAuthorize() {
if (isLoading) { if (isLoading) {
return ( return (
<div className='bg-background-default-subtle'> <div className="bg-background-default-subtle">
<Loading type='app' /> <Loading type="app" />
</div> </div>
) )
} }
return ( return (
<div className='bg-background-default-subtle'> <div className="bg-background-default-subtle">
{authAppInfo?.app_icon && ( {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'> <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' /> <img src={authAppInfo.app_icon} alt="app icon" className="h-10 w-10 rounded" />
</div> </div>
)} )}
<div className={`mb-4 mt-5 flex flex-col gap-2 ${isLoggedIn ? 'pb-2' : ''}`}> <div className={`mb-4 mt-5 flex flex-col gap-2 ${isLoggedIn ? 'pb-2' : ''}`}>
<div className='title-4xl-semi-bold'> <div className="title-4xl-semi-bold">
{isLoggedIn && <div className='text-text-primary'>{t('oauth.connect')}</div>} {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> <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>} {!isLoggedIn && <div className="text-text-primary">{t('oauth.tips.notLoggedIn')}</div>}
</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> </div>
{isLoggedIn && userProfile && ( {isLoggedIn && userProfile && (
<div className='flex items-center justify-between rounded-xl bg-background-section-burn-inverted p-3'> <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 gap-2.5">
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} size={36} /> <Avatar avatar={userProfile.avatar_url} name={userProfile.name} size={36} />
<div> <div>
<div className='system-md-semi-bold text-text-secondary'>{userProfile.name}</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-xs-regular text-text-tertiary">{userProfile.email}</div>
</div> </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> </div>
)} )}
{isLoggedIn && Boolean(authAppInfo?.scope) && ( {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) => { {authAppInfo!.scope.split(/\s+/).filter(Boolean).map((scope: string) => {
const Icon = SCOPE_INFO_MAP[scope] const Icon = SCOPE_INFO_MAP[scope]
return ( return (
<div key={scope} className='body-sm-medium flex items-center gap-2 text-text-secondary'> <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 ? <Icon.icon className="h-4 w-4" /> : <RiAccountCircleLine className="h-4 w-4" />}
{Icon.label} {Icon.label}
</div> </div>
) )
@ -174,17 +174,19 @@ export default function OAuthAuthorize() {
</div> </div>
)} )}
<div className='flex flex-col items-center gap-2 pt-4'> <div className="flex flex-col items-center gap-2 pt-4">
{!isLoggedIn ? ( {!isLoggedIn
<Button variant='primary' size='large' className='w-full' onClick={onLoginSwitchClick}>{t('oauth.login')}</Button> ? (
) : ( <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 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> <Button size="large" className="w-full" onClick={() => router.push('/apps')}>{t('common.operation.cancel')}</Button>
</> </>
)} )}
</div> </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"> <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)" /> <path d="M0 0.5H400" stroke="url(#paint0_linear_2_5904)" />
<defs> <defs>
@ -196,7 +198,7 @@ export default function OAuthAuthorize() {
</defs> </defs>
</svg> </svg>
</div> </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> </div>
) )
} }

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import { useTranslation } from 'react-i18next' import type { Operation } from './app-operations'
import { useRouter } from 'next/navigation' import type { DuplicateAppModalProps } from '@/app/components/app/duplicate-modal'
import { useContext } from 'use-context-selector' import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal'
import React, { useCallback, useState } from 'react' import type { EnvironmentVariable } from '@/app/components/workflow/types'
import { import {
RiDeleteBinLine, RiDeleteBinLine,
RiEditLine, RiEditLine,
@ -11,26 +11,26 @@ import {
RiFileDownloadLine, RiFileDownloadLine,
RiFileUploadLine, RiFileUploadLine,
} from '@remixicon/react' } 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 { 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 { ToastContext } from '@/app/components/base/toast'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import { useAppContext } from '@/context/app-context' import { useAppContext } from '@/context/app-context'
import { useProviderContext } from '@/context/provider-context' import { useProviderContext } from '@/context/provider-context'
import { copyApp, deleteApp, exportAppConfig, updateAppInfo } from '@/service/apps' 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 { 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 { 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'), { const SwitchAppModal = dynamic(() => import('@/app/components/app/switch-app-modal'), {
ssr: false, ssr: false,
@ -239,7 +239,8 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
const secondaryOperations: Operation[] = [ const secondaryOperations: Operation[] = [
// Import DSL (conditional) // Import DSL (conditional)
...(appDetail.mode === AppModeEnum.ADVANCED_CHAT || appDetail.mode === AppModeEnum.WORKFLOW) ? [{ ...(appDetail.mode === AppModeEnum.ADVANCED_CHAT || appDetail.mode === AppModeEnum.WORKFLOW)
? [{
id: 'import', id: 'import',
title: t('workflow.common.importDSL'), title: t('workflow.common.importDSL'),
icon: <RiFileUploadLine />, icon: <RiFileUploadLine />,
@ -248,7 +249,8 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
onDetailExpand?.(false) onDetailExpand?.(false)
setShowImportDSLModal(true) setShowImportDSLModal(true)
}, },
}] : [], }]
: [],
// Divider // Divider
{ {
id: 'divider-1', id: 'divider-1',
@ -271,7 +273,8 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
] ]
// Keep the switch operation separate as it's not part of the main operations // Keep the switch operation separate as it's not part of the main operations
const switchOperation = (appDetail.mode === AppModeEnum.COMPLETION || appDetail.mode === AppModeEnum.CHAT) ? { const switchOperation = (appDetail.mode === AppModeEnum.COMPLETION || appDetail.mode === AppModeEnum.CHAT)
? {
id: 'switch', id: 'switch',
title: t('app.switch'), title: t('app.switch'),
icon: <RiExchange2Line />, icon: <RiExchange2Line />,
@ -280,20 +283,22 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
onDetailExpand?.(false) onDetailExpand?.(false)
setShowSwitchModal(true) setShowSwitchModal(true)
}, },
} : null }
: null
return ( return (
<div> <div>
{!onlyShowDetail && ( {!onlyShowDetail && (
<button type="button" <button
type="button"
onClick={() => { onClick={() => {
if (isCurrentWorkspaceEditor) if (isCurrentWorkspaceEditor)
setOpen(v => !v) 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 flex-col gap-2 rounded-lg p-1 hover:bg-state-base-hover">
<div className='flex items-center gap-1'> <div className="flex items-center gap-1">
<div className={cn(!expand && 'ml-1')}> <div className={cn(!expand && 'ml-1')}>
<AppIcon <AppIcon
size={expand ? 'large' : 'small'} size={expand ? 'large' : 'small'}
@ -304,31 +309,36 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
/> />
</div> </div>
{expand && ( {expand && (
<div className='ml-auto flex items-center justify-center rounded-md p-0.5'> <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'> <div className="flex h-5 w-5 items-center justify-center">
<RiEqualizer2Line className='h-4 w-4 text-text-tertiary' /> <RiEqualizer2Line className="h-4 w-4 text-text-tertiary" />
</div> </div>
</div> </div>
)} )}
</div> </div>
{!expand && ( {!expand && (
<div className='flex items-center justify-center'> <div className="flex items-center justify-center">
<div className='flex h-5 w-5 items-center justify-center rounded-md p-0.5'> <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' /> <RiEqualizer2Line className="h-4 w-4 text-text-tertiary" />
</div> </div>
</div> </div>
)} )}
{expand && ( {expand && (
<div className='flex flex-col items-start gap-1'> <div className="flex flex-col items-start gap-1">
<div className='flex w-full'> <div className="flex w-full">
<div className='system-md-semibold truncate whitespace-nowrap text-text-secondary'>{appDetail.name}</div> <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>
<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>
)} )}
</div> </div>
@ -340,25 +350,25 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
setOpen(false) setOpen(false)
onDetailExpand?.(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 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 items-center gap-3 self-stretch">
<AppIcon <AppIcon
size='large' size="large"
iconType={appDetail.icon_type} iconType={appDetail.icon_type}
icon={appDetail.icon} icon={appDetail.icon}
background={appDetail.icon_background} background={appDetail.icon_background}
imageUrl={appDetail.icon_url} imageUrl={appDetail.icon_url}
/> />
<div className='flex flex-1 flex-col items-start justify-center overflow-hidden'> <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-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="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>
{/* description */} {/* description */}
{appDetail.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 */} {/* operations */}
<AppOperations <AppOperations
@ -370,19 +380,19 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
<CardView <CardView
appId={appDetail.id} appId={appDetail.id}
isInPanel={true} 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) */} {/* Switch operation (if available) */}
{switchOperation && ( {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 <Button
size={'medium'} size="medium"
variant={'ghost'} variant="ghost"
className='gap-0.5' className="gap-0.5"
onClick={switchOperation.onClick} onClick={switchOperation.onClick}
> >
{switchOperation.icon} {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> </Button>
</div> </div>
)} )}

View File

@ -1,9 +1,9 @@
import type { JSX } from 'react' import type { JSX } from 'react'
import { RiMoreLine } from '@remixicon/react'
import { cloneElement, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { cloneElement, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '../base/portal-to-follow-elem' import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '../base/portal-to-follow-elem'
import { RiMoreLine } from '@remixicon/react'
export type Operation = { export type Operation = {
id: string id: string
@ -91,7 +91,8 @@ const AppOperations = ({
for (let i = 0; i < childrens.length; i++) { for (let i = 0; i < childrens.length; i++) {
const child = childrens[i] as HTMLElement const child = childrens[i] as HTMLElement
const id = child.dataset.targetid const id = child.dataset.targetid
if (!id) break if (!id)
break
const childWidth = child.clientWidth const childWidth = child.clientWidth
if (width + gap + childWidth + moreWidth <= containerWidth) { if (width + gap + childWidth + moreWidth <= containerWidth) {
@ -127,8 +128,8 @@ const AppOperations = ({
<Button <Button
key={operation.id} key={operation.id}
data-targetid={operation.id} data-targetid={operation.id}
size={'small'} size="small"
variant={'secondary'} variant="secondary"
className="gap-[1px]" className="gap-[1px]"
tabIndex={-1} tabIndex={-1}
> >
@ -140,8 +141,8 @@ const AppOperations = ({
))} ))}
<Button <Button
id="more-measure" id="more-measure"
size={'small'} size="small"
variant={'secondary'} variant="secondary"
className="gap-[1px]" className="gap-[1px]"
tabIndex={-1} tabIndex={-1}
> >
@ -156,8 +157,8 @@ const AppOperations = ({
<Button <Button
key={operation.id} key={operation.id}
data-targetid={operation.id} data-targetid={operation.id}
size={'small'} size="small"
variant={'secondary'} variant="secondary"
className="gap-[1px]" className="gap-[1px]"
onClick={operation.onClick} onClick={operation.onClick}
> >
@ -176,8 +177,8 @@ const AppOperations = ({
> >
<PortalToFollowElemTrigger onClick={handleTriggerMore}> <PortalToFollowElemTrigger onClick={handleTriggerMore}>
<Button <Button
size={'small'} size="small"
variant={'secondary'} variant="secondary"
className="gap-[1px]" className="gap-[1px]"
> >
<RiMoreLine className="h-3.5 w-3.5 text-components-button-secondary-text" /> <RiMoreLine className="h-3.5 w-3.5 text-components-button-secondary-text" />

View File

@ -1,23 +1,23 @@
import React, { useCallback, useRef, useState } from 'react' import type { NavIcon } from './navLink'
import { useTranslation } from 'react-i18next'
import { useAppContext } from '@/context/app-context'
import { import {
RiEqualizer2Line, RiEqualizer2Line,
RiMenuLine, RiMenuLine,
} from '@remixicon/react' } 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 { import {
PortalToFollowElem, PortalToFollowElem,
PortalToFollowElemContent, PortalToFollowElemContent,
PortalToFollowElemTrigger, PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem' } 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 AppIcon from '../base/app-icon'
import Divider from '../base/divider' import Divider from '../base/divider'
import AppInfo from './app-info' import AppInfo from './app-info'
import NavLink from './navLink' 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 = { type Props = {
navigation: Array<{ navigation: Array<{
@ -49,11 +49,11 @@ const AppSidebarDropdown = ({ navigation }: Props) => {
return ( return (
<> <>
<div className='fixed left-2 top-2 z-20'> <div className="fixed left-2 top-2 z-20">
<PortalToFollowElem <PortalToFollowElem
open={open} open={open}
onOpenChange={setOpen} onOpenChange={setOpen}
placement='bottom-start' placement="bottom-start"
offset={{ offset={{
mainAxis: -41, mainAxis: -41,
}} }}
@ -61,18 +61,18 @@ const AppSidebarDropdown = ({ navigation }: Props) => {
<PortalToFollowElemTrigger onClick={handleTrigger}> <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')}> <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 <AppIcon
size='small' size="small"
iconType={appDetail.icon_type} iconType={appDetail.icon_type}
icon={appDetail.icon} icon={appDetail.icon}
background={appDetail.icon_background} background={appDetail.icon_background}
imageUrl={appDetail.icon_url} imageUrl={appDetail.icon_url}
/> />
<RiMenuLine className='h-4 w-4 text-text-tertiary' /> <RiMenuLine className="h-4 w-4 text-text-tertiary" />
</div> </div>
</PortalToFollowElemTrigger> </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={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 <div
className={cn('flex flex-col gap-2 rounded-lg p-2 pb-2.5', isCurrentWorkspaceEditor && 'cursor-pointer hover:bg-state-base-hover')} className={cn('flex flex-col gap-2 rounded-lg p-2 pb-2.5', isCurrentWorkspaceEditor && 'cursor-pointer hover:bg-state-base-hover')}
onClick={() => { onClick={() => {
@ -80,35 +80,35 @@ const AppSidebarDropdown = ({ navigation }: Props) => {
setOpen(false) setOpen(false)
}} }}
> >
<div className='flex items-center justify-between self-stretch'> <div className="flex items-center justify-between self-stretch">
<AppIcon <AppIcon
size='large' size="large"
iconType={appDetail.icon_type} iconType={appDetail.icon_type}
icon={appDetail.icon} icon={appDetail.icon}
background={appDetail.icon_background} background={appDetail.icon_background}
imageUrl={appDetail.icon_url} imageUrl={appDetail.icon_url}
/> />
<div className='flex items-center justify-center rounded-md p-0.5'> <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 h-5 w-5 items-center justify-center">
<RiEqualizer2Line className='h-4 w-4 text-text-tertiary' /> <RiEqualizer2Line className="h-4 w-4 text-text-tertiary" />
</div> </div>
</div> </div>
</div> </div>
<div className='flex flex-col items-start gap-1'> <div className="flex flex-col items-start gap-1">
<div className='flex w-full'> <div className="flex w-full">
<div className='system-md-semibold truncate text-text-secondary'>{appDetail.name}</div> <div className="system-md-semibold truncate text-text-secondary">{appDetail.name}</div>
</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>
</div> </div>
<div className='px-4'> <div className="px-4">
<Divider bgStyle='gradient' /> <Divider bgStyle="gradient" />
</div> </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) => { {navigation.map((item, index) => {
return ( 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> </nav>
@ -116,7 +116,7 @@ const AppSidebarDropdown = ({ navigation }: Props) => {
</PortalToFollowElemContent> </PortalToFollowElemContent>
</PortalToFollowElem> </PortalToFollowElem>
</div> </div>
<div className='z-20'> <div className="z-20">
<AppInfo expand onlyShowDetail openState={detailExpand} onDetailExpand={setDetailExpand} /> <AppInfo expand onlyShowDetail openState={detailExpand} onDetailExpand={setDetailExpand} />
</div> </div>
</> </>

View File

@ -1,11 +1,11 @@
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import AppIcon from '../base/app-icon'
import Tooltip from '@/app/components/base/tooltip'
import { import {
ApiAggregate, ApiAggregate,
WindowCursor, WindowCursor,
} from '@/app/components/base/icons/src/vender/workflow' } from '@/app/components/base/icons/src/vender/workflow'
import Tooltip from '@/app/components/base/tooltip'
import AppIcon from '../base/app-icon'
export type IAppBasicProps = { export type IAppBasicProps = {
iconType?: 'app' | 'api' | 'dataset' | 'webapp' | 'notion' iconType?: 'app' | 'api' | 'dataset' | 'webapp' | 'notion'
@ -15,17 +15,20 @@ export type IAppBasicProps = {
name: string name: string
type: string | React.ReactNode type: string | React.ReactNode
hoverTip?: string hoverTip?: string
textStyle?: { main?: string; extra?: string } textStyle?: { main?: string, extra?: string }
isExtraInLine?: boolean isExtraInLine?: boolean
mode?: string mode?: string
hideType?: boolean hideType?: boolean
} }
const DatasetSvg = <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/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" /> <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> </svg>
)
const NotionSvg = <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/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)"> <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 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 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" />
@ -36,18 +39,23 @@ const NotionSvg = <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xm
<rect width="24" height="24" fill="white" /> <rect width="24" height="24" fill="white" />
</clipPath> </clipPath>
</defs> </defs>
</svg> </svg>
)
const ICON_MAP = { const ICON_MAP = {
app: <AppIcon className='border !border-[rgba(0,0,0,0.05)]' />, 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'> api: (
<ApiAggregate className='h-4 w-4 text-text-primary-on-surface' /> <div className="rounded-lg border-[0.5px] border-divider-subtle bg-util-colors-blue-brand-blue-brand-500 p-1 shadow-md">
</div>, <ApiAggregate className="h-4 w-4 text-text-primary-on-surface" />
dataset: <AppIcon innerIcon={DatasetSvg} className='!border-[0.5px] !border-indigo-100 !bg-indigo-25' />, </div>
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' /> dataset: <AppIcon innerIcon={DatasetSvg} className="!border-[0.5px] !border-indigo-100 !bg-indigo-25" />,
</div>, webapp: (
notion: <AppIcon innerIcon={NotionSvg} className='!border-[0.5px] !border-indigo-100 !bg-white' />, <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) { 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 ( return (
<div className="flex grow items-center"> <div className="flex grow items-center">
{icon && icon_background && iconType === 'app' && ( {icon && icon_background && iconType === 'app' && (
<div className='mr-2 shrink-0'> <div className="mr-2 shrink-0">
<AppIcon icon={icon} background={icon_background} /> <AppIcon icon={icon} background={icon_background} />
</div> </div>
)} )}
{iconType !== 'app' {iconType !== 'app'
&& <div className='mr-2 shrink-0'> && (
<div className="mr-2 shrink-0">
{ICON_MAP[iconType]} {ICON_MAP[iconType]}
</div> </div>
)}
} {mode === 'expand' && (
{mode === 'expand' && <div className="group w-full"> <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={`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"> <div className="min-w-0 overflow-hidden text-ellipsis break-normal">
{name} {name}
</div> </div>
{hoverTip {hoverTip
&& <Tooltip && (
popupContent={ <Tooltip
<div className='w-[240px]'> popupContent={(
<div className="w-[240px]">
{hoverTip} {hoverTip}
</div> </div>
} )}
popupClassName='ml-1' popupClassName="ml-1"
triggerClassName='w-4 h-4 ml-1' triggerClassName="w-4 h-4 ml-1"
position='top' position="top"
/> />
} )}
</div> </div>
{!hideType && isExtraInLine && ( {!hideType && isExtraInLine && (
<div className="system-2xs-medium-uppercase flex text-text-tertiary">{type}</div> <div className="system-2xs-medium-uppercase flex text-text-tertiary">{type}</div>
)} )}
{!hideType && !isExtraInLine && ( {!hideType && !isExtraInLine && (
<div className='system-2xs-medium-uppercase text-text-tertiary'>{isExternal ? t('dataset.externalTag') : type}</div> <div className="system-2xs-medium-uppercase text-text-tertiary">{isExternal ? t('dataset.externalTag') : type}</div>
)}
</div>
)} )}
</div>}
</div> </div>
) )
} }

View File

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

View File

@ -1,14 +1,14 @@
'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import type { DataSet } from '@/models/datasets'
import React, { useMemo } from 'react' import React, { useMemo } from 'react'
import { useTranslation } from 'react-i18next' 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 AppIcon from '../../base/app-icon'
import Effect from '../../base/effect' 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' import Dropdown from './dropdown'
type DatasetInfoProps = { type DatasetInfoProps = {
@ -35,11 +35,11 @@ const DatasetInfo: FC<DatasetInfoProps> = ({
return ( return (
<div className={cn('relative flex flex-col', expand ? '' : 'p-1')}> <div className={cn('relative flex flex-col', expand ? '' : 'p-1')}>
{expand && ( {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 flex-col gap-2 p-2">
<div className='flex items-center gap-1'> <div className="flex items-center gap-1">
<div className={cn(!expand && '-ml-1')}> <div className={cn(!expand && '-ml-1')}>
<AppIcon <AppIcon
size={expand ? 'large' : 'small'} size={expand ? 'large' : 'small'}
@ -50,35 +50,35 @@ const DatasetInfo: FC<DatasetInfoProps> = ({
/> />
</div> </div>
{expand && ( {expand && (
<div className='ml-auto'> <div className="ml-auto">
<Dropdown expand /> <Dropdown expand />
</div> </div>
)} )}
</div> </div>
{!expand && ( {!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} /> <Dropdown expand={false} />
</div> </div>
)} )}
{expand && ( {expand && (
<div className='flex flex-col gap-y-1 pb-0.5'> <div className="flex flex-col gap-y-1 pb-0.5">
<div <div
className='system-md-semibold truncate text-text-secondary' className="system-md-semibold truncate text-text-secondary"
title={dataset.name} title={dataset.name}
> >
{dataset.name} {dataset.name}
</div> </div>
<div className='system-2xs-medium-uppercase text-text-tertiary'> <div className="system-2xs-medium-uppercase text-text-tertiary">
{isExternalProvider && t('dataset.externalTag')} {isExternalProvider && t('dataset.externalTag')}
{!isExternalProvider && isPipelinePublished && dataset.doc_form && dataset.indexing_technique && ( {!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>{t(`dataset.chunkingMode.${DOC_FORM_TEXT[dataset.doc_form]}`)}</span>
<span>{formatIndexingTechniqueAndMethod(dataset.indexing_technique, dataset.retrieval_model_dict?.search_method)}</span> <span>{formatIndexingTechniqueAndMethod(dataset.indexing_technique, dataset.retrieval_model_dict?.search_method)}</span>
</div> </div>
)} )}
</div> </div>
{!!dataset.description && ( {!!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} {dataset.description}
</p> </p>
)} )}

View File

@ -1,5 +1,5 @@
import React from 'react'
import type { RemixiconComponentType } from '@remixicon/react' import type { RemixiconComponentType } from '@remixicon/react'
import React from 'react'
type MenuItemProps = { type MenuItemProps = {
name: string name: string
@ -14,15 +14,15 @@ const MenuItem = ({
}: MenuItemProps) => { }: MenuItemProps) => {
return ( return (
<div <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) => { onClick={(e) => {
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
handleClick?.() handleClick?.()
}} }}
> >
<Icon className='size-4 text-text-tertiary' /> <Icon className="size-4 text-text-tertiary" />
<span className='system-md-regular px-1 text-text-secondary'>{name}</span> <span className="system-md-regular px-1 text-text-secondary">{name}</span>
</div> </div>
) )
} }

View File

@ -1,9 +1,9 @@
import { RiDeleteBinLine, RiEditLine, RiFileDownloadLine } from '@remixicon/react'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' 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 { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
import Divider from '../../base/divider'
import MenuItem from './menu-item'
type MenuProps = { type MenuProps = {
showDelete: boolean showDelete: boolean
@ -22,8 +22,8 @@ const Menu = ({
const runtimeMode = useDatasetDetailContextWithSelector(state => state.dataset?.runtime_mode) const runtimeMode = useDatasetDetailContextWithSelector(state => state.dataset?.runtime_mode)
return ( 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 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 flex-col p-1">
<MenuItem <MenuItem
Icon={RiEditLine} Icon={RiEditLine}
name={t('common.operation.edit')} name={t('common.operation.edit')}
@ -39,8 +39,8 @@ const Menu = ({
</div> </div>
{showDelete && ( {showDelete && (
<> <>
<Divider type='horizontal' className='my-0 bg-divider-subtle' /> <Divider type="horizontal" className="my-0 bg-divider-subtle" />
<div className='flex flex-col p-1'> <div className="flex flex-col p-1">
<MenuItem <MenuItem
Icon={RiDeleteBinLine} Icon={RiDeleteBinLine}
name={t('common.operation.delete')} 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 { import {
RiMenuLine, RiMenuLine,
} from '@remixicon/react' } from '@remixicon/react'
import React, { useCallback, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { import {
PortalToFollowElem, PortalToFollowElem,
PortalToFollowElemContent, PortalToFollowElemContent,
PortalToFollowElemTrigger, PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem' } 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 AppIcon from '../base/app-icon'
import Divider from '../base/divider' 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 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 ExtraInfo from '../datasets/extra-info'
import Dropdown from './dataset-info/dropdown'
import NavLink from './navLink'
type DatasetSidebarDropdownProps = { type DatasetSidebarDropdownProps = {
navigation: Array<{ navigation: Array<{
@ -64,11 +64,11 @@ const DatasetSidebarDropdown = ({
return ( return (
<> <>
<div className='fixed left-2 top-2 z-20'> <div className="fixed left-2 top-2 z-20">
<PortalToFollowElem <PortalToFollowElem
open={open} open={open}
onOpenChange={setOpen} onOpenChange={setOpen}
placement='bottom-start' placement="bottom-start"
offset={{ offset={{
mainAxis: -41, mainAxis: -41,
}} }}
@ -81,22 +81,22 @@ const DatasetSidebarDropdown = ({
)} )}
> >
<AppIcon <AppIcon
size='small' size="small"
iconType={iconInfo.icon_type} iconType={iconInfo.icon_type}
icon={iconInfo.icon} icon={iconInfo.icon}
background={iconInfo.icon_background} background={iconInfo.icon_background}
imageUrl={iconInfo.icon_url} imageUrl={iconInfo.icon_url}
/> />
<RiMenuLine className='size-4 text-text-tertiary' /> <RiMenuLine className="size-4 text-text-tertiary" />
</div> </div>
</PortalToFollowElemTrigger> </PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-50'> <PortalToFollowElemContent className="z-50">
<div className='relative w-[216px] rounded-xl border-[0.5px] border-components-panel-border bg-background-default-subtle shadow-lg'> <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' /> <Effect className="-left-5 top-[-22px] opacity-15" />
<div className='flex flex-col gap-y-2 p-4'> <div className="flex flex-col gap-y-2 p-4">
<div className='flex items-center justify-between'> <div className="flex items-center justify-between">
<AppIcon <AppIcon
size='medium' size="medium"
iconType={iconInfo.icon_type} iconType={iconInfo.icon_type}
icon={iconInfo.icon} icon={iconInfo.icon}
background={iconInfo.icon_background} background={iconInfo.icon_background}
@ -104,17 +104,17 @@ const DatasetSidebarDropdown = ({
/> />
<Dropdown expand /> <Dropdown expand />
</div> </div>
<div className='flex flex-col gap-y-1 pb-0.5'> <div className="flex flex-col gap-y-1 pb-0.5">
<div <div
className='system-md-semibold truncate text-text-secondary' className="system-md-semibold truncate text-text-secondary"
title={dataset.name} title={dataset.name}
> >
{dataset.name} {dataset.name}
</div> </div>
<div className='system-2xs-medium-uppercase text-text-tertiary'> <div className="system-2xs-medium-uppercase text-text-tertiary">
{isExternalProvider && t('dataset.externalTag')} {isExternalProvider && t('dataset.externalTag')}
{!isExternalProvider && dataset.doc_form && dataset.indexing_technique && ( {!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>{t(`dataset.chunkingMode.${DOC_FORM_TEXT[dataset.doc_form]}`)}</span>
<span>{formatIndexingTechniqueAndMethod(dataset.indexing_technique, dataset.retrieval_model_dict?.search_method)}</span> <span>{formatIndexingTechniqueAndMethod(dataset.indexing_technique, dataset.retrieval_model_dict?.search_method)}</span>
</div> </div>
@ -122,24 +122,24 @@ const DatasetSidebarDropdown = ({
</div> </div>
</div> </div>
{!!dataset.description && ( {!!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} {dataset.description}
</p> </p>
)} )}
</div> </div>
<div className='px-4 py-2'> <div className="px-4 py-2">
<Divider <Divider
type='horizontal' type="horizontal"
bgStyle='gradient' bgStyle="gradient"
className='my-0 h-px bg-gradient-to-r from-divider-subtle to-background-gradient-mask-transparent' className="my-0 h-px bg-gradient-to-r from-divider-subtle to-background-gradient-mask-transparent"
/> />
</div> </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) => { {navigation.map((item, index) => {
return ( return (
<NavLink <NavLink
key={index} key={index}
mode='expand' mode="expand"
iconMap={{ selected: item.selectedIcon, normal: item.icon }} iconMap={{ selected: item.selectedIcon, normal: item.icon }}
name={item.name} name={item.name}
href={item.href} 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 type { NavIcon } from './navLink'
import AppInfo from './app-info' import { useHover, useKeyPress } from 'ahooks'
import DatasetInfo from './dataset-info' import { usePathname } from 'next/navigation'
import AppSidebarDropdown from './app-sidebar-dropdown' import React, { useCallback, useEffect, useState } from 'react'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import { useShallow } from 'zustand/react/shallow'
import { useStore as useAppStore } from '@/app/components/app/store' import { useStore as useAppStore } from '@/app/components/app/store'
import { useEventEmitterContextContext } from '@/context/event-emitter' import { useEventEmitterContextContext } from '@/context/event-emitter'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import { cn } from '@/utils/classnames' import { cn } from '@/utils/classnames'
import Divider from '../base/divider' import Divider from '../base/divider'
import { useHover, useKeyPress } from 'ahooks'
import ToggleButton from './toggle-button'
import { getKeyboardKeyCodeBySystem } from '../workflow/utils' 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 DatasetSidebarDropdown from './dataset-sidebar-dropdown'
import NavLink from './navLink'
import ToggleButton from './toggle-button'
export type IAppDetailNavProps = { export type IAppDetailNavProps = {
iconType?: 'app' | 'dataset' iconType?: 'app' | 'dataset'
@ -75,7 +75,7 @@ const AppDetailNav = ({
if (inWorkflowCanvas && hideHeader) { if (inWorkflowCanvas && hideHeader) {
return ( return (
<div className='flex w-0 shrink-0'> <div className="flex w-0 shrink-0">
<AppSidebarDropdown navigation={navigation} /> <AppSidebarDropdown navigation={navigation} />
</div> </div>
) )
@ -83,7 +83,7 @@ const AppDetailNav = ({
if (isPipelineCanvas && hideHeader) { if (isPipelineCanvas && hideHeader) {
return ( return (
<div className='flex w-0 shrink-0'> <div className="flex w-0 shrink-0">
<DatasetSidebarDropdown navigation={navigation} /> <DatasetSidebarDropdown navigation={navigation} />
</div> </div>
) )
@ -110,9 +110,9 @@ const AppDetailNav = ({
<DatasetInfo expand={expand} /> <DatasetInfo expand={expand} />
)} )}
</div> </div>
<div className='relative px-4 py-2'> <div className="relative px-4 py-2">
<Divider <Divider
type='horizontal' type="horizontal"
bgStyle={expand ? 'gradient' : 'solid'} bgStyle={expand ? 'gradient' : 'solid'}
className={cn( className={cn(
'my-0 h-px', 'my-0 h-px',
@ -123,7 +123,7 @@ const AppDetailNav = ({
/> />
{!isMobile && isHoveringSidebar && ( {!isMobile && isHoveringSidebar && (
<ToggleButton <ToggleButton
className='absolute -right-3 top-[-3.5px] z-20' className="absolute -right-3 top-[-3.5px] z-20"
expand={expand} expand={expand}
handleToggle={handleToggle} 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 type { NavLinkProps } from './navLink'
import { render, screen } from '@testing-library/react'
import React from 'react'
import NavLink from './navLink'
// Mock Next.js navigation // Mock Next.js navigation
vi.mock('next/navigation', () => ({ vi.mock('next/navigation', () => ({

View File

@ -1,15 +1,16 @@
'use client' '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 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< export type NavIcon = React.ComponentType<
React.PropsWithoutRef<React.ComponentProps<'svg'>> & { React.PropsWithoutRef<React.ComponentProps<'svg'>> & {
title?: string | undefined title?: string | undefined
titleId?: string | undefined titleId?: string | undefined
}> | RemixiconComponentType }
> | RemixiconComponentType
export type NavLinkProps = { export type NavLinkProps = {
name: string name: string
@ -51,17 +52,15 @@ const NavLink = ({
return ( return (
<button <button
key={name} key={name}
type='button' type="button"
disabled 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', 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')}
'pl-3 pr-1')}
title={mode === 'collapse' ? name : ''} title={mode === 'collapse' ? name : ''}
aria-disabled aria-disabled
> >
{renderIcon()} {renderIcon()}
<span <span
className={cn('overflow-hidden whitespace-nowrap transition-all duration-200 ease-in-out', className={cn('overflow-hidden whitespace-nowrap transition-all duration-200 ease-in-out', mode === 'expand'
mode === 'expand'
? 'ml-2 max-w-none opacity-100' ? 'ml-2 max-w-none opacity-100'
: 'ml-0 max-w-0 opacity-0')} : 'ml-0 max-w-0 opacity-0')}
> >
@ -77,14 +76,12 @@ const NavLink = ({
href={href} href={href}
className={cn(isActive 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-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', : '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')}
'flex h-8 items-center rounded-lg pl-3 pr-1')}
title={mode === 'collapse' ? name : ''} title={mode === 'collapse' ? name : ''}
> >
{renderIcon()} {renderIcon()}
<span <span
className={cn('overflow-hidden whitespace-nowrap transition-all duration-200 ease-in-out', className={cn('overflow-hidden whitespace-nowrap transition-all duration-200 ease-in-out', mode === 'expand'
mode === 'expand'
? 'ml-2 max-w-none opacity-100' ? 'ml-2 max-w-none opacity-100'
: 'ml-0 max-w-0 opacity-0')} : 'ml-0 max-w-0 opacity-0')}
> >

View File

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

View File

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

View File

@ -1,9 +1,9 @@
import React from 'react'
import Button from '../base/button'
import { RiArrowLeftSLine, RiArrowRightSLine } from '@remixicon/react' import { RiArrowLeftSLine, RiArrowRightSLine } from '@remixicon/react'
import { cn } from '@/utils/classnames' import React from 'react'
import Tooltip from '../base/tooltip'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { cn } from '@/utils/classnames'
import Button from '../base/button'
import Tooltip from '../base/tooltip'
import { getKeyboardKeyNameBySystem } from '../workflow/utils' import { getKeyboardKeyNameBySystem } from '../workflow/utils'
type TooltipContentProps = { type TooltipContentProps = {
@ -18,14 +18,14 @@ const TooltipContent = ({
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<div className='flex items-center gap-x-1'> <div className="flex items-center gap-x-1">
<span className='system-xs-medium px-0.5 text-text-secondary'>{expand ? t('layout.sidebar.collapseSidebar') : t('layout.sidebar.expandSidebar')}</span> <span className="system-xs-medium px-0.5 text-text-secondary">{expand ? t('layout.sidebar.collapseSidebar') : t('layout.sidebar.expandSidebar')}</span>
<div className='flex items-center gap-x-0.5'> <div className="flex items-center gap-x-0.5">
{ {
TOGGLE_SHORTCUT.map(key => ( TOGGLE_SHORTCUT.map(key => (
<span <span
key={key} key={key}
className='system-kbd inline-flex items-center justify-center rounded-[4px] bg-components-kbd-bg-gray px-1 text-text-tertiary' className="system-kbd inline-flex items-center justify-center rounded-[4px] bg-components-kbd-bg-gray px-1 text-text-tertiary"
> >
{getKeyboardKeyNameBySystem(key)} {getKeyboardKeyNameBySystem(key)}
</span> </span>
@ -50,18 +50,18 @@ const ToggleButton = ({
return ( return (
<Tooltip <Tooltip
popupContent={<TooltipContent expand={expand} />} popupContent={<TooltipContent expand={expand} />}
popupClassName='p-1.5 rounded-lg' popupClassName="p-1.5 rounded-lg"
position='right' position="right"
> >
<Button <Button
size='small' size="small"
onClick={handleToggle} onClick={handleToggle}
className={cn('rounded-full px-1', className)} className={cn('rounded-full px-1', className)}
> >
{ {
expand expand
? <RiArrowLeftSLine className='size-4' /> ? <RiArrowLeftSLine className="size-4" />
: <RiArrowRightSLine className='size-4' /> : <RiArrowRightSLine className="size-4" />
} }
</Button> </Button>
</Tooltip> </Tooltip>

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