Compare commits

...

3 Commits

Author SHA1 Message Date
rdmclin2 399440c84d fix: improve Home starter E2E test reliability
- Add editor text verification after typing
- Type Space+Backspace to force-flush editor onChange debounce
  (workaround for @lobehub/editor useMemo-based debounce timer loss)
- Add editor focus check and auto-focus before sending
- Add TRPC response monitoring with retry via send button
- Add diagnostic logging for CI debugging
- Capture unhandled promise rejections in world.ts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 20:23:36 +08:00
rdmclin2 e68ae8e79a fix: e2e test 2026-02-12 18:05:30 +08:00
rdmclin2 f8032adf3f feat: support model detail dropdown 2026-02-12 18:05:27 +08:00
61 changed files with 430 additions and 379 deletions
+6 -2
View File
@@ -275,8 +275,12 @@ async function buildApp(): Promise<void> {
async function isServerRunning(port: number): Promise<boolean> {
try {
const response = await fetch(`http://localhost:${port}/chat`, { method: 'HEAD' });
return response.ok;
const response = await fetch(`http://localhost:${port}/`, {
method: 'HEAD',
redirect: 'manual',
});
// Any HTTP response (including redirects) means the server is running
return response.status > 0;
} catch {
return false;
}
+148 -50
View File
@@ -23,7 +23,7 @@ let createdDocumentId: string | null = null;
// Given Steps
// ============================================
Given('用户在 Home 页面', async function (this: CustomWorld) {
Given('用户在 Home 页面', { timeout: 30_000 }, async function (this: CustomWorld) {
console.log(' 📍 Step: 设置 LLM mock...');
// Setup LLM mock before navigation (for agent/group/page builder message)
llmMockManager.setResponse('E2E Test Agent', presetResponses.greeting);
@@ -51,7 +51,7 @@ Given('用户在 Home 页面', async function (this: CustomWorld) {
// When Steps
// ============================================
When('用户点击创建 Agent 按钮', async function (this: CustomWorld) {
When('用户点击创建 Agent 按钮', { timeout: 30_000 }, async function (this: CustomWorld) {
console.log(' 📍 Step: 点击创建 Agent 按钮...');
// Find the "Create Agent" button by text (supports both English and Chinese)
@@ -66,7 +66,7 @@ When('用户点击创建 Agent 按钮', async function (this: CustomWorld) {
console.log(' ✅ 已点击创建 Agent 按钮');
});
When('用户点击创建 Group 按钮', async function (this: CustomWorld) {
When('用户点击创建 Group 按钮', { timeout: 30_000 }, async function (this: CustomWorld) {
console.log(' 📍 Step: 点击创建 Group 按钮...');
// Find the "Create Group" button by text (supports both English and Chinese)
@@ -81,7 +81,7 @@ When('用户点击创建 Group 按钮', async function (this: CustomWorld) {
console.log(' ✅ 已点击创建 Group 按钮');
});
When('用户点击写作按钮', async function (this: CustomWorld) {
When('用户点击写作按钮', { timeout: 30_000 }, async function (this: CustomWorld) {
console.log(' 📍 Step: 点击写作按钮...');
// Find the "Write" button by text (supports both English and Chinese)
@@ -94,49 +94,139 @@ When('用户点击写作按钮', async function (this: CustomWorld) {
console.log(' ✅ 已点击写作按钮');
});
When('用户在输入框中输入 {string}', async function (this: CustomWorld, message: string) {
console.log(` 📍 Step: 在输入框中输入 "${message}"...`);
When(
'用户在输入框中输入 {string}',
{ timeout: 30_000 },
async function (this: CustomWorld, message: string) {
console.log(` 📍 Step: 在输入框中输入 "${message}"...`);
// The chat input is a contenteditable editor, need to click first then type
const chatInputContainer = this.page.locator('[data-testid="chat-input"]').first();
// The chat input is a contenteditable editor, need to click first then type
const chatInputContainer = this.page.locator('[data-testid="chat-input"]').first();
// If data-testid not found, try alternative selectors
let inputFound = false;
if ((await chatInputContainer.count()) > 0) {
await chatInputContainer.click();
inputFound = true;
} else {
// Try to find the editor by its contenteditable attribute
const editor = this.page.locator('[contenteditable="true"]').first();
if ((await editor.count()) > 0) {
await editor.click();
// If data-testid not found, try alternative selectors
let inputFound = false;
if ((await chatInputContainer.count()) > 0) {
await chatInputContainer.click();
inputFound = true;
} else {
// Try to find the editor by its contenteditable attribute
const editor = this.page.locator('[contenteditable="true"]').first();
if ((await editor.count()) > 0) {
await editor.click();
inputFound = true;
}
}
}
if (!inputFound) {
throw new Error('Could not find chat input');
}
if (!inputFound) {
throw new Error('Could not find chat input');
}
await this.page.waitForTimeout(300);
await this.page.keyboard.type(message, { delay: 30 });
await this.page.waitForTimeout(300);
await this.page.keyboard.type(message, { delay: 30 });
console.log(` ✅ 已输入 "${message}"`);
});
// Verify text appeared in the editor
const editorEl = this.page
.locator('[data-testid="chat-input"] [contenteditable="true"]')
.first();
const fallbackEditor = this.page.locator('[contenteditable="true"]').first();
const targetEditor = (await editorEl.count()) > 0 ? editorEl : fallbackEditor;
await expect(targetEditor).toContainText(message.slice(0, 10), { timeout: 5000 });
When('用户按 Enter 发送', { timeout: 30_000 }, async function (this: CustomWorld) {
// Wait for the editor's debounced onChange (100ms) to fire and sync inputMessage to store.
// The @lobehub/editor debounces onChange using useMemo which may lose timers on re-renders,
// so we wait generously and then type+delete a character to ensure a fresh debounce fires.
await this.page.waitForTimeout(300);
console.log(` ✅ 已输入 "${message}"`);
},
);
When('用户按 Enter 发送', { timeout: 60_000 }, async function (this: CustomWorld) {
console.log(' 📍 Step: 按 Enter 发送...');
// Wait for editor's debounced onChange (100ms default) to sync inputMessage to store
// Without this, inputMessage is empty and send() silently returns
await this.page.waitForTimeout(200);
// Diagnostic: check editor state before sending
const editorState = await this.page.evaluate(() => {
const el = document.querySelector('[contenteditable="true"]');
return {
activeTag: document.activeElement?.tagName || 'none',
editorExists: !!el,
focused: document.activeElement === el || (el?.contains(document.activeElement) ?? false),
text: el?.textContent?.trim() || '',
};
});
console.log(
` 📍 Editor: text="${editorState.text.slice(0, 30)}", focused=${editorState.focused}, active=${editorState.activeTag}`,
);
// Ensure focus is on the editor before pressing Enter
if (!editorState.focused) {
console.log(' ⚠️ Editor not focused, clicking to focus...');
const editor = this.page.locator('[contenteditable="true"]').first();
await editor.click();
await this.page.waitForTimeout(200);
}
// Type a space and delete it to trigger a fresh onChange debounce cycle.
// The @lobehub/editor uses useMemo to create debounced onChange, which can lose
// pending timers when the component re-renders (useEditorState triggers re-renders
// on every keystroke). By typing after re-renders have settled, we ensure the
// debounce timer fires and syncs inputMessage to the store.
await this.page.keyboard.press('Space');
await this.page.waitForTimeout(50);
await this.page.keyboard.press('Backspace');
// Wait for the debounce (100ms) to fire and sync inputMessage
await this.page.waitForTimeout(300);
// Listen for navigation to capture the agent/group ID
const navigationPromise = this.page.waitForURL(/\/(agent|group)\/.*\/profile/, {
timeout: 30_000,
timeout: 45_000,
});
// Monitor TRPC response to confirm the request reaches the server
const trpcResponsePromise = this.page
.waitForResponse(
(response) => response.url().includes('/trpc/lambda') && response.status() === 200,
{ timeout: 10_000 },
)
.catch(() => null);
await this.page.keyboard.press('Enter');
console.log(' 📍 Enter pressed, waiting for response...');
// Wait for TRPC response (short timeout) to confirm the request was sent
const trpcResponse = await trpcResponsePromise;
if (trpcResponse) {
console.log(` 📍 TRPC response: ${trpcResponse.url()}`);
} else {
// TRPC request was not sent. Diagnose and retry.
const postEnterState = await this.page.evaluate(() => {
const el = document.querySelector('[contenteditable="true"]');
return {
text: el?.textContent?.trim() || '',
url: window.location.href,
};
});
console.log(
` ⚠️ No TRPC response. Editor text="${postEnterState.text.slice(0, 30)}", URL=${postEnterState.url}`,
);
// Retry: click the send button if visible
console.log(' ⚠️ Retrying via send button...');
const sendBtn = this.page.locator('[data-testid="chat-input"] button:has(svg)').last();
const sendBtnVisible = await sendBtn.isVisible().catch(() => false);
if (sendBtnVisible) {
await sendBtn.click();
console.log(' 📍 Clicked send button');
} else {
// Last resort: re-focus editor and press Enter again
console.log(' ⚠️ No send button, re-pressing Enter...');
const editor = this.page.locator('[contenteditable="true"]').first();
await editor.click();
await this.page.waitForTimeout(300);
await this.page.keyboard.press('Enter');
}
}
// Wait for navigation to profile page
await navigationPromise;
@@ -188,7 +278,7 @@ When('用户按 Enter 发送创建文档', { timeout: 30_000 }, async function (
console.log(' ✅ 已发送并创建文档');
});
When('用户返回 Home 页面', async function (this: CustomWorld) {
When('用户返回 Home 页面', { timeout: 30_000 }, async function (this: CustomWorld) {
console.log(' 📍 Step: 返回 Home 页面...');
await this.page.goto('/');
@@ -202,27 +292,35 @@ When('用户返回 Home 页面', async function (this: CustomWorld) {
// Then Steps
// ============================================
Then('页面应该跳转到 Agent 的 profile 页面', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证页面跳转到 Agent profile 页面...');
Then(
'页面应该跳转到 Agent profile 页面',
{ timeout: 30_000 },
async function (this: CustomWorld) {
console.log(' 📍 Step: 验证页面跳转到 Agent profile 页面...');
// Check current URL matches /agent/{id}/profile pattern
const currentUrl = this.page.url();
expect(currentUrl).toMatch(/\/agent\/[^/]+\/profile/);
// Check current URL matches /agent/{id}/profile pattern
const currentUrl = this.page.url();
expect(currentUrl).toMatch(/\/agent\/[^/]+\/profile/);
console.log(' ✅ 已跳转到 Agent profile 页面');
});
console.log(' ✅ 已跳转到 Agent profile 页面');
},
);
Then('页面应该跳转到 Group 的 profile 页面', async function (this: CustomWorld) {
console.log(' 📍 Step: 验证页面跳转到 Group profile 页面...');
Then(
'页面应该跳转到 Group profile 页面',
{ timeout: 30_000 },
async function (this: CustomWorld) {
console.log(' 📍 Step: 验证页面跳转到 Group profile 页面...');
// Check current URL matches /group/{id}/profile pattern
const currentUrl = this.page.url();
expect(currentUrl).toMatch(/\/group\/[^/]+\/profile/);
// Check current URL matches /group/{id}/profile pattern
const currentUrl = this.page.url();
expect(currentUrl).toMatch(/\/group\/[^/]+\/profile/);
console.log(' ✅ 已跳转到 Group profile 页面');
});
console.log(' ✅ 已跳转到 Group profile 页面');
},
);
Then('新创建的 Agent 应该在侧边栏中显示', async function (this: CustomWorld) {
Then('新创建的 Agent 应该在侧边栏中显示', { timeout: 30_000 }, async function (this: CustomWorld) {
console.log(' 📍 Step: 验证 Agent 在侧边栏中显示...');
// Wait for sidebar to be visible and data to load
@@ -245,7 +343,7 @@ Then('新创建的 Agent 应该在侧边栏中显示', async function (this: Cus
console.log(' ✅ Agent 已在侧边栏中显示');
});
Then('新创建的 Group 应该在侧边栏中显示', async function (this: CustomWorld) {
Then('新创建的 Group 应该在侧边栏中显示', { timeout: 30_000 }, async function (this: CustomWorld) {
console.log(' 📍 Step: 验证 Group 在侧边栏中显示...');
// Wait for sidebar to be visible and data to load
@@ -268,7 +366,7 @@ Then('新创建的 Group 应该在侧边栏中显示', async function (this: Cus
console.log(' ✅ Group 已在侧边栏中显示');
});
Then('页面应该跳转到文档编辑页面', async function (this: CustomWorld) {
Then('页面应该跳转到文档编辑页面', { timeout: 30_000 }, async function (this: CustomWorld) {
console.log(' 📍 Step: 验证页面跳转到文档编辑页面...');
// Check current URL matches /page/{id} pattern
@@ -282,7 +380,7 @@ Then('页面应该跳转到文档编辑页面', async function (this: CustomWorl
console.log(` ✅ 已跳转到文档编辑页面: /page/${createdDocumentId}`);
});
Then('Page Agent 应该收到用户的提示词', async function (this: CustomWorld) {
Then('Page Agent 应该收到用户的提示词', { timeout: 30_000 }, async function (this: CustomWorld) {
console.log(' 📍 Step: 验证 Page Agent 收到用户的提示词...');
// Wait for the page to fully load and Page Agent panel to appear
+6 -6
View File
@@ -1,13 +1,13 @@
import { After, AfterAll, Before, BeforeAll, Status, setDefaultTimeout } from '@cucumber/cucumber';
import { type Cookie, chromium } from 'playwright';
import { After, AfterAll, Before, BeforeAll, setDefaultTimeout,Status } from '@cucumber/cucumber';
import { chromium,type Cookie } from 'playwright';
import { TEST_USER, seedTestUser } from '../support/seedTestUser';
import { seedTestUser,TEST_USER } from '../support/seedTestUser';
import { startWebServer, stopWebServer } from '../support/webServer';
import { CustomWorld } from '../support/world';
import { type CustomWorld } from '../support/world';
process.env['E2E'] = '1';
// Set default timeout for all steps to 10 seconds
setDefaultTimeout(10_000);
// Set default timeout for all steps to 30 seconds (CI runners are slower than local)
setDefaultTimeout(30_000);
// Store base URL and cached session cookies
let baseUrl: string;
+16 -2
View File
@@ -1,8 +1,15 @@
import { IWorldOptions, World, setWorldConstructor } from '@cucumber/cucumber';
import { Browser, BrowserContext, Page, Response, chromium } from '@playwright/test';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { type IWorldOptions, setWorldConstructor, World } from '@cucumber/cucumber';
import {
type Browser,
type BrowserContext,
chromium,
type Page,
type Response,
} from '@playwright/test';
/**
* Default timeout for waiting operations (e.g., waitForURL, toBeVisible)
*/
@@ -73,6 +80,13 @@ export class CustomWorld extends World {
});
this.page.setDefaultTimeout(30_000);
// Capture unhandled promise rejections (not caught by pageerror)
await this.page.addInitScript(() => {
window.addEventListener('unhandledrejection', (event) => {
console.error('[E2E] Unhandled rejection:', String(event.reason));
});
});
}
async cleanup() {
-70
View File
@@ -4,39 +4,11 @@
"count": 1
}
},
"src/app/[variants]/(main)/agent/profile/features/Header/AgentPublishButton/PublishButton.tsx": {
"import-x/consistent-type-specifier-style": {
"count": 1
}
},
"src/app/[variants]/(main)/community/(detail)/agent/features/Details/Capabilities/PluginItem.tsx": {
"simple-import-sort/imports": {
"count": 1
}
},
"src/app/[variants]/(main)/community/(detail)/agent/features/Header.tsx": {
"simple-import-sort/imports": {
"count": 1
}
},
"src/app/[variants]/(main)/community/components/VirtuosoGridList/index.tsx": {
"@eslint-react/no-nested-component-definitions": {
"count": 2
}
},
"src/app/[variants]/(main)/home/_layout/Body/Agent/Modals/ConfigGroupModal/GroupItem.tsx": {
"import-x/consistent-type-specifier-style": {
"count": 1
}
},
"src/app/[variants]/(main)/home/_layout/Body/Agent/Modals/ConfigGroupModal/index.tsx": {
"@typescript-eslint/consistent-type-imports": {
"count": 1
},
"simple-import-sort/imports": {
"count": 1
}
},
"src/app/[variants]/(main)/home/features/RecentPage/Item.tsx": {
"regexp/no-super-linear-backtracking": {
"count": 1
@@ -114,9 +86,6 @@
}
},
"src/envs/auth.ts": {
"perfectionist/sort-interfaces": {
"count": 19
},
"sort-keys-fix/sort-keys-fix": {
"count": 1
},
@@ -158,16 +127,6 @@
"count": 1
}
},
"src/features/Conversation/Messages/CompressedGroup/index.tsx": {
"import-x/consistent-type-specifier-style": {
"count": 1
}
},
"src/features/Conversation/Messages/Supervisor/components/ContentBlock.tsx": {
"simple-import-sort/imports": {
"count": 1
}
},
"src/features/Conversation/types/hooks.ts": {
"typescript-sort-keys/interface": {
"count": 1
@@ -312,22 +271,6 @@
"count": 2
}
},
"src/server/routers/lambda/userMemory.ts": {
"@typescript-eslint/consistent-type-imports": {
"count": 1
},
"import-x/no-duplicates": {
"count": 2
},
"simple-import-sort/imports": {
"count": 1
}
},
"src/server/routers/tools/_helpers/scheduleToolCallReport.test.ts": {
"simple-import-sort/imports": {
"count": 1
}
},
"src/server/routers/tools/mcp.ts": {
"sort-keys-fix/sort-keys-fix": {
"count": 1
@@ -485,14 +428,6 @@
"count": 1
}
},
"src/store/chat/slices/aiChat/actions/__tests__/StreamingHandler.test.ts": {
"import-x/consistent-type-specifier-style": {
"count": 1
},
"unused-imports/no-unused-imports": {
"count": 1
}
},
"src/store/chat/slices/aiChat/actions/conversationControl.ts": {
"no-unused-private-class-members": {
"count": 1
@@ -746,11 +681,6 @@
"count": 1
}
},
"src/store/tool/slices/builtin/executors/index.ts": {
"import-x/consistent-type-specifier-style": {
"count": 1
}
},
"src/store/tool/slices/oldStore/initialState.ts": {
"typescript-sort-keys/string-enum": {
"count": 1
@@ -46,26 +46,26 @@ const Nav = memo(() => {
<Flexbox gap={1} paddingInline={4}>
<NavItem
icon={MessageSquarePlusIcon}
onClick={handleNewTopic}
title={tTopic('actions.addNewTopic')}
onClick={handleNewTopic}
/>
{!hideProfile && (
<NavItem
active={isProfileActive}
icon={BotPromptIcon}
title={t('tab.profile')}
onClick={() => {
switchTopic(null, { skipRefreshMessage: true });
router.push(urlJoin('/agent', agentId!, 'profile'));
}}
title={t('tab.profile')}
/>
)}
<NavItem
icon={SearchIcon}
title={t('tab.search')}
onClick={() => {
toggleCommandMenu(true);
}}
title={t('tab.search')}
/>
</Flexbox>
);
@@ -33,14 +33,14 @@ const MainChatInput = memo(() => {
return (
<ChatInput
skipScrollMarginWithList
leftActions={leftActions}
rightActions={rightActions}
sendMenu={{ items: sendMenuItems }}
onEditorReady={(instance) => {
// Sync to global ChatStore for compatibility with other features
useChatStore.setState({ mainInputEditor: instance });
}}
rightActions={rightActions}
sendMenu={{ items: sendMenuItems }}
skipScrollMarginWithList
/>
);
});
@@ -13,7 +13,7 @@ import { agentSelectors } from '@/store/agent/selectors';
import { useVersionReviewStatus } from '../AgentVersionReviewTag';
import ForkConfirmModal from './ForkConfirmModal';
import type { MarketPublishAction } from './types';
import { type MarketPublishAction } from './types';
import { type OriginalAgentInfo, useMarketPublish } from './useMarketPublish';
interface MarketPublishButtonProps {
@@ -80,7 +80,8 @@ const PublishButton = memo<MarketPublishButtonProps>(({ action, onPublishSuccess
if (isUnderReview) {
message.warning({
content: t('marketPublish.validation.underReview', {
defaultValue: 'Your new version is currently under review. Please wait for approval before publishing a new version.',
defaultValue:
'Your new version is currently under review. Please wait for approval before publishing a new version.',
}),
});
return;
@@ -144,6 +145,9 @@ const PublishButton = memo<MarketPublishButtonProps>(({ action, onPublishSuccess
<Popconfirm
arrow={false}
okButtonProps={{ type: 'primary' }}
open={confirmOpened}
placement="bottomRight"
title={t('marketPublish.validation.confirmPublish')}
onCancel={() => setConfirmOpened(false)}
onConfirm={handleConfirmPublish}
onOpenChange={(open) => {
@@ -151,25 +155,22 @@ const PublishButton = memo<MarketPublishButtonProps>(({ action, onPublishSuccess
setConfirmOpened(false);
}
}}
open={confirmOpened}
placement="bottomRight"
title={t('marketPublish.validation.confirmPublish')}
>
<Button
icon={ShapesUploadIcon}
loading={loading}
onClick={handleButtonClick}
title={buttonTitle}
onClick={handleButtonClick}
>
{t('publishToCommunity')}
</Button>
</Popconfirm>
<ForkConfirmModal
loading={isPublishing}
onCancel={handleForkCancel}
onConfirm={handleForkConfirm}
open={showForkModal}
originalAgent={originalAgentInfo}
onCancel={handleForkCancel}
onConfirm={handleForkConfirm}
/>
</>
);
@@ -1,6 +1,6 @@
import {
KLAVIS_SERVER_TYPES,
getLobehubSkillProviderById,
KLAVIS_SERVER_TYPES,
type KlavisServerType,
type LobehubSkillProviderType,
} from '@lobechat/const';
@@ -20,16 +20,16 @@ import { builtinTools } from '@/tools';
* For string type icon, use Image component to render
* For IconType type icon, use Icon component to render with theme fill color
*/
const BuiltinToolIcon = memo<
Pick<KlavisServerType | LobehubSkillProviderType, 'icon' | 'label'>
>(({ icon, label }) => {
if (typeof icon === 'string') {
return <Image alt={label} height={40} src={icon} style={{ flex: 'none' }} width={40} />;
}
const BuiltinToolIcon = memo<Pick<KlavisServerType | LobehubSkillProviderType, 'icon' | 'label'>>(
({ icon, label }) => {
if (typeof icon === 'string') {
return <Image alt={label} height={40} src={icon} style={{ flex: 'none' }} width={40} />;
}
// Use theme color fill, automatically adapts in dark mode
return <Icon fill={cssVar.colorText} icon={icon} size={40} />;
});
// Use theme color fill, automatically adapts in dark mode
return <Icon fill={cssVar.colorText} icon={icon} size={40} />;
},
);
BuiltinToolIcon.displayName = 'BuiltinToolIcon';
@@ -195,7 +195,7 @@ const PluginItem = memo<PluginItemProps>(({ identifier }) => {
if (isLoading)
return (
<Block gap={12} horizontal key={identifier} padding={12} variant={'outlined'}>
<Block horizontal gap={12} key={identifier} padding={12} variant={'outlined'}>
<Skeleton paragraph={{ rows: 1 }} title={false} />
</Block>
);
@@ -216,9 +216,9 @@ const PluginItem = memo<PluginItemProps>(({ identifier }) => {
const content = (
<Block
horizontal
className={cx(sourceConfig.clickable ? styles.clickable : styles.noLink)}
gap={12}
horizontal
key={identifier}
padding={12}
variant={'outlined'}
@@ -232,7 +232,7 @@ const PluginItem = memo<PluginItemProps>(({ identifier }) => {
}}
>
<div className={styles.titleRow}>
<Text as={'h2'} className={cx(styles.title, 'plugin-title')} ellipsis>
<Text ellipsis as={'h2'} className={cx(styles.title, 'plugin-title')}>
{data.title}
</Text>
{sourceConfig.tagText && (
@@ -15,9 +15,9 @@ import {
import { App } from 'antd';
import { createStaticStyles, cssVar, useResponsive } from 'antd-style';
import {
BookTextIcon,
BookmarkCheckIcon,
BookmarkIcon,
BookTextIcon,
CoinsIcon,
DotIcon,
GitBranchIcon,
@@ -33,8 +33,8 @@ import { useMarketAuth } from '@/layout/AuthProvider/MarketAuth';
import { socialService } from '@/services/social';
import { formatIntergerNumber } from '@/utils/format';
import { useCategory } from '../../../(list)/agent/features/Category/useCategory';
import PublishedTime from '../../../../../../../components/PublishedTime';
import { useCategory } from '../../../(list)/agent/features/Category/useCategory';
import AgentForkTag from './AgentForkTag';
import { useDetailContext } from './DetailProvider';
@@ -123,7 +123,7 @@ const Header = memo<{ mobile?: boolean }>(({ mobile: isMobile }) => {
return (
<Flexbox gap={12}>
<Flexbox align={'flex-start'} gap={16} horizontal width={'100%'}>
<Flexbox horizontal align={'flex-start'} gap={16} width={'100%'}>
<Avatar avatar={avatar} shape={'square'} size={mobile ? 48 : 64} />
<Flexbox
flex={1}
@@ -133,9 +133,9 @@ const Header = memo<{ mobile?: boolean }>(({ mobile: isMobile }) => {
}}
>
<Flexbox
horizontal
align={'center'}
gap={8}
horizontal
justify={'space-between'}
style={{
overflow: 'hidden',
@@ -143,18 +143,18 @@ const Header = memo<{ mobile?: boolean }>(({ mobile: isMobile }) => {
}}
>
<Flexbox
horizontal
align={'center'}
flex={1}
gap={12}
horizontal
style={{
overflow: 'hidden',
position: 'relative',
}}
>
<Text
as={'h1'}
ellipsis
as={'h1'}
style={{ fontSize: mobile ? 18 : 24, margin: 0 }}
title={identifier}
>
@@ -165,12 +165,12 @@ const Header = memo<{ mobile?: boolean }>(({ mobile: isMobile }) => {
<ActionIcon
icon={isFavorited ? BookmarkCheckIcon : BookmarkIcon}
loading={favoriteLoading}
onClick={handleFavoriteClick}
variant={isFavorited ? 'outlined' : undefined}
onClick={handleFavoriteClick}
/>
</Tooltip>
</Flexbox>
<Flexbox align={'center'} gap={8} horizontal wrap={'wrap'}>
<Flexbox horizontal align={'center'} gap={8} wrap={'wrap'}>
{author && userName ? (
<Link style={{ color: 'inherit' }} to={urlJoin('/community/user', userName)}>
{author}
@@ -195,9 +195,9 @@ const Header = memo<{ mobile?: boolean }>(({ mobile: isMobile }) => {
</Flexbox>
<TooltipGroup>
<Flexbox
horizontal
align={'center'}
gap={mobile ? 12 : 24}
horizontal
style={{
color: cssVar.colorTextSecondary,
}}
@@ -208,7 +208,7 @@ const Header = memo<{ mobile?: boolean }>(({ mobile: isMobile }) => {
styles={{ root: { pointerEvents: 'none' } }}
title={t('assistants.tokenUsage')}
>
<Flexbox align={'center'} gap={6} horizontal>
<Flexbox horizontal align={'center'} gap={6}>
<Icon icon={CoinsIcon} />
{formatIntergerNumber(tokenUsage)}
</Flexbox>
@@ -219,7 +219,7 @@ const Header = memo<{ mobile?: boolean }>(({ mobile: isMobile }) => {
styles={{ root: { pointerEvents: 'none' } }}
title={t('assistants.withPlugin')}
>
<Flexbox align={'center'} gap={6} horizontal>
<Flexbox horizontal align={'center'} gap={6}>
<Icon fill={cssVar.colorTextSecondary} icon={MCP} />
{pluginCount}
</Flexbox>
@@ -230,7 +230,7 @@ const Header = memo<{ mobile?: boolean }>(({ mobile: isMobile }) => {
styles={{ root: { pointerEvents: 'none' } }}
title={t('assistants.withKnowledge')}
>
<Flexbox align={'center'} gap={6} horizontal>
<Flexbox horizontal align={'center'} gap={6}>
<Icon icon={BookTextIcon} />
{knowledgeCount}
</Flexbox>
@@ -13,13 +13,7 @@ import {
} from '@lobehub/ui';
import { App } from 'antd';
import { createStaticStyles, cssVar, useResponsive } from 'antd-style';
import {
BookmarkCheckIcon,
BookmarkIcon,
DotIcon,
GitBranchIcon,
UsersIcon,
} from 'lucide-react';
import { BookmarkCheckIcon, BookmarkIcon, DotIcon, GitBranchIcon, UsersIcon } from 'lucide-react';
import qs from 'query-string';
import { memo, useState } from 'react';
import { useTranslation } from 'react-i18next';
@@ -119,7 +113,7 @@ const Header = memo<{ mobile?: boolean }>(({ mobile: isMobile }) => {
return (
<Flexbox gap={12}>
<Flexbox align={'flex-start'} gap={16} horizontal width={'100%'}>
<Flexbox horizontal align={'flex-start'} gap={16} width={'100%'}>
<Avatar avatar={displayAvatar} shape={'square'} size={mobile ? 48 : 64} />
<Flexbox
flex={1}
@@ -129,9 +123,9 @@ const Header = memo<{ mobile?: boolean }>(({ mobile: isMobile }) => {
}}
>
<Flexbox
horizontal
align={'center'}
gap={8}
horizontal
justify={'space-between'}
style={{
overflow: 'hidden',
@@ -139,18 +133,18 @@ const Header = memo<{ mobile?: boolean }>(({ mobile: isMobile }) => {
}}
>
<Flexbox
horizontal
align={'center'}
flex={1}
gap={12}
horizontal
style={{
overflow: 'hidden',
position: 'relative',
}}
>
<Text
as={'h1'}
ellipsis
as={'h1'}
style={{ fontSize: mobile ? 18 : 24, margin: 0 }}
title={identifier}
>
@@ -161,12 +155,12 @@ const Header = memo<{ mobile?: boolean }>(({ mobile: isMobile }) => {
<ActionIcon
icon={isFavorited ? BookmarkCheckIcon : BookmarkIcon}
loading={favoriteLoading}
onClick={handleFavoriteClick}
variant={isFavorited ? 'outlined' : undefined}
onClick={handleFavoriteClick}
/>
</Tooltip>
</Flexbox>
<Flexbox align={'center'} gap={8} horizontal wrap={'wrap'}>
<Flexbox horizontal align={'center'} gap={8} wrap={'wrap'}>
{(() => {
// API returns author as object {avatar, name, userName}, but type definition says string
const authorObj =
@@ -198,9 +192,9 @@ const Header = memo<{ mobile?: boolean }>(({ mobile: isMobile }) => {
</Flexbox>
<TooltipGroup>
<Flexbox
horizontal
align={'center'}
gap={mobile ? 12 : 24}
horizontal
style={{
color: cssVar.colorTextSecondary,
}}
@@ -211,7 +205,7 @@ const Header = memo<{ mobile?: boolean }>(({ mobile: isMobile }) => {
styles={{ root: { pointerEvents: 'none' } }}
title={t('groupAgents.memberCount', { defaultValue: 'Members' })}
>
<Flexbox align={'center'} gap={6} horizontal>
<Flexbox horizontal align={'center'} gap={6}>
<Icon icon={UsersIcon} />
{memberCount}
</Flexbox>
@@ -4,7 +4,13 @@ import { Select } from 'antd';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
export type StatusFilterValue = 'published' | 'unpublished' | 'deprecated' | 'archived' | 'forked' | 'favorite';
export type StatusFilterValue =
| 'published'
| 'unpublished'
| 'deprecated'
| 'archived'
| 'forked'
| 'favorite';
interface StatusFilterProps {
onChange: (value: StatusFilterValue) => void;
@@ -23,14 +29,7 @@ const StatusFilter = memo<StatusFilterProps>(({ value, onChange }) => {
{ label: t('user.statusFilter.favorite'), value: 'favorite' as const },
];
return (
<Select
onChange={onChange}
options={options}
style={{ minWidth: 120 }}
value={value}
/>
);
return <Select options={options} style={{ minWidth: 120 }} value={value} onChange={onChange} />;
});
export default StatusFilter;
@@ -6,12 +6,12 @@ import {
DropdownMenu,
Flexbox,
Icon,
stopPropagation,
Tag as AntTag,
Tag,
Text,
Tooltip,
TooltipGroup,
stopPropagation,
} from '@lobehub/ui';
import { App } from 'antd';
import { createStaticStyles, cx } from 'antd-style';
@@ -17,7 +17,13 @@ interface UserAgentListProps {
const UserAgentList = memo<UserAgentListProps>(({ rows = 4, pageSize = 8 }) => {
const { t } = useTranslation('discover');
const { agents, agentCount, forkedAgents = [], favoriteAgents = [], isOwner } = useUserDetailContext();
const {
agents,
agentCount,
forkedAgents = [],
favoriteAgents = [],
isOwner,
} = useUserDetailContext();
const [currentPage, setCurrentPage] = useState(1);
const [statusFilter, setStatusFilter] = useState<StatusFilterValue>('published');
const [searchQuery, setSearchQuery] = useState('');
@@ -69,26 +75,23 @@ const UserAgentList = memo<UserAgentListProps>(({ rows = 4, pageSize = 8 }) => {
return (
<Flexbox gap={16}>
<Flexbox align={'center'} gap={8} horizontal justify={'space-between'}>
<Flexbox align={'center'} gap={8} horizontal>
<Flexbox horizontal align={'center'} gap={8} justify={'space-between'}>
<Flexbox horizontal align={'center'} gap={8}>
<Text fontSize={16} weight={500}>
{t('user.publishedAgents')}
</Text>
{agentCount > 0 && <Tag>{filteredAgents.length}</Tag>}
</Flexbox>
{isOwner && (
<Flexbox align={'center'} gap={8} horizontal>
<Flexbox horizontal align={'center'} gap={8}>
<Input.Search
allowClear
onChange={(e) => setSearchQuery(e.target.value)}
placeholder={t('user.searchPlaceholder')}
style={{ width: 200 }}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
<StatusFilter
onChange={(value) => setStatusFilter(value)}
value={statusFilter}
/>
<StatusFilter value={statusFilter} onChange={(value) => setStatusFilter(value)} />
</Flexbox>
)}
</Flexbox>
@@ -101,10 +104,10 @@ const UserAgentList = memo<UserAgentListProps>(({ rows = 4, pageSize = 8 }) => {
<Flexbox align={'center'} justify={'center'}>
<Pagination
current={currentPage}
onChange={(page) => setCurrentPage(page)}
pageSize={pageSize}
showSizeChanger={false}
total={filteredAgents.length}
onChange={(page) => setCurrentPage(page)}
/>
</Flexbox>
)}
@@ -6,10 +6,10 @@ import {
Flexbox,
Grid,
Icon,
stopPropagation,
Tag,
Text,
Tooltip,
stopPropagation,
} from '@lobehub/ui';
import { App } from 'antd';
import { createStaticStyles, cx } from 'antd-style';
@@ -6,10 +6,10 @@ import {
Flexbox,
Grid,
Icon,
stopPropagation,
Tag,
Text,
Tooltip,
stopPropagation,
} from '@lobehub/ui';
import { App } from 'antd';
import { createStaticStyles, cx } from 'antd-style';
@@ -6,12 +6,12 @@ import {
DropdownMenu,
Flexbox,
Icon,
stopPropagation,
Tag as AntTag,
Tag,
Text,
Tooltip,
TooltipGroup,
stopPropagation,
} from '@lobehub/ui';
import { createStaticStyles, cx } from 'antd-style';
import {
@@ -16,7 +16,13 @@ interface UserGroupListProps {
const UserGroupList = memo<UserGroupListProps>(({ rows = 4, pageSize = 8 }) => {
const { t } = useTranslation('discover');
const { agentGroups = [], groupCount, forkedAgentGroups = [], favoriteAgentGroups = [], isOwner } = useUserDetailContext();
const {
agentGroups = [],
groupCount,
forkedAgentGroups = [],
favoriteAgentGroups = [],
isOwner,
} = useUserDetailContext();
const [currentPage, setCurrentPage] = useState(1);
const [statusFilter, setStatusFilter] = useState<StatusFilterValue>('published');
const [searchQuery, setSearchQuery] = useState('');
@@ -67,26 +73,23 @@ const UserGroupList = memo<UserGroupListProps>(({ rows = 4, pageSize = 8 }) => {
return (
<Flexbox gap={16}>
<Flexbox align={'center'} gap={8} horizontal justify={'space-between'}>
<Flexbox align={'center'} gap={8} horizontal>
<Flexbox horizontal align={'center'} gap={8} justify={'space-between'}>
<Flexbox horizontal align={'center'} gap={8}>
<Text fontSize={16} weight={500}>
{t('user.publishedGroups', { defaultValue: '创作的群组' })}
</Text>
{groupCount > 0 && <Tag>{filteredGroups.length}</Tag>}
</Flexbox>
{isOwner && (
<Flexbox align={'center'} gap={8} horizontal>
<Flexbox horizontal align={'center'} gap={8}>
<Input.Search
allowClear
onChange={(e) => setSearchQuery(e.target.value)}
placeholder={t('user.searchPlaceholder')}
style={{ width: 200 }}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
<StatusFilter
onChange={(value) => setStatusFilter(value)}
value={statusFilter}
/>
<StatusFilter value={statusFilter} onChange={(value) => setStatusFilter(value)} />
</Flexbox>
)}
</Flexbox>
@@ -99,10 +102,10 @@ const UserGroupList = memo<UserGroupListProps>(({ rows = 4, pageSize = 8 }) => {
<Flexbox align={'center'} justify={'center'}>
<Pagination
current={currentPage}
onChange={(page) => setCurrentPage(page)}
pageSize={pageSize}
showSizeChanger={false}
total={filteredGroups.length}
onChange={(page) => setCurrentPage(page)}
/>
</Flexbox>
)}
@@ -7,10 +7,10 @@ import {
Block,
Flexbox,
Icon,
stopPropagation,
Tag,
Text,
Tooltip,
stopPropagation,
} from '@lobehub/ui';
import { Spotlight } from '@lobehub/ui/awesome';
import { createStaticStyles, cssVar } from 'antd-style';
@@ -1,5 +1,5 @@
import { Github, ModelTag, ProviderCombine } from '@lobehub/icons';
import { ActionIcon, Block, Flexbox, MaskShadow, Text, stopPropagation } from '@lobehub/ui';
import { ActionIcon, Block, Flexbox, MaskShadow, stopPropagation, Text } from '@lobehub/ui';
import { createStaticStyles, cssVar } from 'antd-style';
import { GlobeIcon } from 'lucide-react';
import { memo } from 'react';
@@ -33,14 +33,14 @@ const MainChatInput = memo(() => {
return (
<ChatInput
skipScrollMarginWithList
leftActions={leftActions}
rightActions={rightActions}
sendMenu={{ items: sendMenuItems }}
onEditorReady={(instance) => {
// Sync to global ChatStore for compatibility with other features
useChatStore.setState({ mainInputEditor: instance });
}}
rightActions={rightActions}
sendMenu={{ items: sendMenuItems }}
skipScrollMarginWithList
/>
);
});
@@ -145,6 +145,9 @@ const PublishButton = memo<GroupPublishButtonProps>(({ action, onPublishSuccess
<Popconfirm
arrow={false}
okButtonProps={{ type: 'primary' }}
open={confirmOpened}
placement="bottomRight"
title={t('marketPublish.validation.confirmPublish')}
onCancel={() => setConfirmOpened(false)}
onConfirm={handleConfirmPublish}
onOpenChange={(open) => {
@@ -152,25 +155,22 @@ const PublishButton = memo<GroupPublishButtonProps>(({ action, onPublishSuccess
setConfirmOpened(false);
}
}}
open={confirmOpened}
placement="bottomRight"
title={t('marketPublish.validation.confirmPublish')}
>
<Button
icon={ShapesUploadIcon}
loading={loading}
onClick={handleButtonClick}
title={buttonTitle}
onClick={handleButtonClick}
>
{t('publishToCommunity')}
</Button>
</Popconfirm>
<GroupForkConfirmModal
loading={isPublishing}
onCancel={handleForkCancel}
onConfirm={handleForkConfirm}
open={showForkModal}
originalGroup={originalGroupInfo}
onCancel={handleForkCancel}
onConfirm={handleForkConfirm}
/>
</>
);
@@ -1,4 +1,4 @@
import type { MenuProps } from '@lobehub/ui';
import { type MenuProps } from '@lobehub/ui';
import { ActionIcon, DropdownMenu } from '@lobehub/ui';
import { MoreHorizontalIcon } from 'lucide-react';
import { memo } from 'react';
@@ -6,7 +6,7 @@ import { memo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useHomeStore } from '@/store/home';
import type { SessionGroupItemBase } from '@/types/session';
import { type SessionGroupItemBase } from '@/types/session';
const styles = createStaticStyles(({ css }) => ({
content: css`
@@ -35,9 +35,10 @@ const GroupItem = memo<SessionGroupItemBase>(({ id, name }) => {
{!editing ? (
<>
<span className={styles.title}>{name}</span>
<ActionIcon icon={PencilLine} onClick={() => setEditing(true)} size={'small'} />
<ActionIcon icon={PencilLine} size={'small'} onClick={() => setEditing(true)} />
<ActionIcon
icon={Trash}
size={'small'}
onClick={() => {
modal.confirm({
centered: true,
@@ -51,12 +52,15 @@ const GroupItem = memo<SessionGroupItemBase>(({ id, name }) => {
title: t('sessionGroup.confirmRemoveGroupAlert'),
});
}}
size={'small'}
/>
</>
) : (
<EditableText
editing={editing}
showEditIcon={false}
style={{ height: 28 }}
value={name}
onEditingChange={(e) => setEditing(e)}
onChangeEnd={async (input) => {
if (name !== input) {
if (!input) return;
@@ -68,10 +72,6 @@ const GroupItem = memo<SessionGroupItemBase>(({ id, name }) => {
}
setEditing(false);
}}
onEditingChange={(e) => setEditing(e)}
showEditIcon={false}
style={{ height: 28 }}
value={name}
/>
)}
</>
@@ -8,9 +8,9 @@ import { useTranslation } from 'react-i18next';
import { useHomeStore } from '@/store/home';
import { homeAgentListSelectors } from '@/store/home/selectors';
import { type SessionGroupItemBase } from '@/types/session';
import GroupItem from './GroupItem';
import { SessionGroupItemBase } from '@/types/session';
const styles = createStaticStyles(({ css, cssVar }) => ({
container: css`
@@ -44,29 +44,29 @@ const ConfigGroupModal = memo<ModalProps>(({ open, onCancel }) => {
<Modal
allowFullscreen
footer={null}
onCancel={onCancel}
open={open}
title={t('sessionGroup.config')}
width={400}
onCancel={onCancel}
>
<Flexbox>
<SortableList
items={sessionGroupItems}
onChange={(items: SessionGroupItemBase[]) => {
updateGroupSort(items);
}}
renderItem={(item: SessionGroupItemBase) => (
<SortableList.Item
horizontal
align={'center'}
className={styles.container}
gap={4}
horizontal
id={item.id}
justify={'space-between'}
>
<GroupItem {...item} />
</SortableList.Item>
)}
onChange={(items: SessionGroupItemBase[]) => {
updateGroupSort(items);
}}
/>
<Button
block
@@ -7,8 +7,8 @@ import {
Center,
DropdownMenu,
Skeleton,
Text,
stopPropagation,
Text,
} from '@lobehub/ui';
import { createStaticStyles, cx } from 'antd-style';
import { ChevronsUpDownIcon } from 'lucide-react';
@@ -6,8 +6,8 @@ import {
ActionIcon,
ContextMenuTrigger,
Flexbox,
Text,
stopPropagation,
Text,
} from '@lobehub/ui';
import isEqual from 'fast-deep-equal';
import { ArrowDownUpIcon } from 'lucide-react';
@@ -11,8 +11,8 @@ import {
Form,
Icon,
Skeleton,
Tooltip,
stopPropagation,
Tooltip,
} from '@lobehub/ui';
import { useDebounceFn } from 'ahooks';
import { Form as AntdForm, Switch } from 'antd';
@@ -10,9 +10,9 @@ import {
List,
Modal,
SearchBar,
stopPropagation,
Text,
Tooltip,
stopPropagation,
} from '@lobehub/ui';
import { Switch } from 'antd';
import { createStaticStyles, cssVar, cx } from 'antd-style';
+49 -49
View File
@@ -6,92 +6,83 @@ declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace NodeJS {
interface ProcessEnv {
// ===== Better Auth ===== //
AUTH_SECRET?: string;
AUTH_EMAIL_VERIFICATION?: string;
AUTH_ENABLE_MAGIC_LINK?: string;
AUTH_SSO_PROVIDERS?: string;
AUTH_TRUSTED_ORIGINS?: string;
AUTH_ALLOWED_EMAILS?: string;
AUTH_DISABLE_EMAIL_PASSWORD?: string;
// ===== Auth Provider Credentials ===== //
AUTH_GOOGLE_ID?: string;
AUTH_GOOGLE_SECRET?: string;
AUTH_APPLE_APP_BUNDLE_IDENTIFIER?: string;
AUTH_APPLE_CLIENT_ID?: string;
AUTH_APPLE_CLIENT_SECRET?: string;
AUTH_APPLE_APP_BUNDLE_IDENTIFIER?: string;
AUTH_GITHUB_ID?: string;
AUTH_GITHUB_SECRET?: string;
AUTH_COGNITO_ID?: string;
AUTH_COGNITO_SECRET?: string;
AUTH_COGNITO_ISSUER?: string;
AUTH_COGNITO_DOMAIN?: string;
AUTH_COGNITO_REGION?: string;
AUTH_COGNITO_USERPOOL_ID?: string;
AUTH_MICROSOFT_AUTHORITY_URL?: string;
AUTH_MICROSOFT_ID?: string;
AUTH_MICROSOFT_SECRET?: string;
AUTH_MICROSOFT_TENANT_ID?: string;
AUTH_AUTH0_ID?: string;
AUTH_AUTH0_SECRET?: string;
AUTH_AUTH0_ISSUER?: string;
AUTH_AUTH0_SECRET?: string;
AUTH_AUTHELIA_ID?: string;
AUTH_AUTHELIA_SECRET?: string;
AUTH_AUTHELIA_ISSUER?: string;
AUTH_AUTHELIA_SECRET?: string;
AUTH_AUTHENTIK_ID?: string;
AUTH_AUTHENTIK_SECRET?: string;
AUTH_AUTHENTIK_ISSUER?: string;
AUTH_AUTHENTIK_SECRET?: string;
AUTH_CASDOOR_ID?: string;
AUTH_CASDOOR_SECRET?: string;
AUTH_CASDOOR_ISSUER?: string;
AUTH_CASDOOR_SECRET?: string;
AUTH_CLOUDFLARE_ZERO_TRUST_ID?: string;
AUTH_CLOUDFLARE_ZERO_TRUST_SECRET?: string;
AUTH_CLOUDFLARE_ZERO_TRUST_ISSUER?: string;
AUTH_CLOUDFLARE_ZERO_TRUST_SECRET?: string;
AUTH_COGNITO_DOMAIN?: string;
AUTH_COGNITO_ID?: string;
AUTH_COGNITO_ISSUER?: string;
AUTH_COGNITO_REGION?: string;
AUTH_COGNITO_SECRET?: string;
AUTH_COGNITO_USERPOOL_ID?: string;
AUTH_DISABLE_EMAIL_PASSWORD?: string;
AUTH_EMAIL_VERIFICATION?: string;
AUTH_ENABLE_MAGIC_LINK?: string;
AUTH_FEISHU_APP_ID?: string;
AUTH_FEISHU_APP_SECRET?: string;
AUTH_GENERIC_OIDC_ID?: string;
AUTH_GENERIC_OIDC_SECRET?: string;
AUTH_GENERIC_OIDC_ISSUER?: string;
AUTH_GENERIC_OIDC_SECRET?: string;
AUTH_GITHUB_ID?: string;
AUTH_GITHUB_SECRET?: string;
// ===== Auth Provider Credentials ===== //
AUTH_GOOGLE_ID?: string;
AUTH_GOOGLE_SECRET?: string;
AUTH_KEYCLOAK_ID?: string;
AUTH_KEYCLOAK_SECRET?: string;
AUTH_KEYCLOAK_ISSUER?: string;
AUTH_KEYCLOAK_SECRET?: string;
AUTH_LOGTO_ID?: string;
AUTH_LOGTO_SECRET?: string;
AUTH_LOGTO_ISSUER?: string;
AUTH_LOGTO_SECRET?: string;
AUTH_MICROSOFT_AUTHORITY_URL?: string;
AUTH_MICROSOFT_ID?: string;
AUTH_MICROSOFT_SECRET?: string;
AUTH_MICROSOFT_TENANT_ID?: string;
AUTH_OKTA_ID?: string;
AUTH_OKTA_SECRET?: string;
AUTH_OKTA_ISSUER?: string;
AUTH_OKTA_SECRET?: string;
// ===== Better Auth ===== //
AUTH_SECRET?: string;
AUTH_SSO_PROVIDERS?: string;
AUTH_TRUSTED_ORIGINS?: string;
AUTH_WECHAT_ID?: string;
AUTH_WECHAT_SECRET?: string;
AUTH_ZITADEL_ID?: string;
AUTH_ZITADEL_SECRET?: string;
AUTH_ZITADEL_ISSUER?: string;
// ===== JWKS Key ===== //
/**
* Generic JWKS key for signing/verifying JWTs.
* Used for internal service authentication and other cryptographic operations.
* Must be a JWKS JSON string containing an RS256 RSA key pair.
* Can be generated using `node scripts/generate-oidc-jwk.mjs`.
*/
JWKS_KEY?: string;
AUTH_ZITADEL_SECRET?: string;
/**
* Internal JWT expiration time for lambda → async calls.
@@ -101,6 +92,15 @@ declare global {
* @default '30s'
*/
INTERNAL_JWT_EXPIRATION?: string;
// ===== JWKS Key ===== //
/**
* Generic JWKS key for signing/verifying JWTs.
* Used for internal service authentication and other cryptographic operations.
* Must be a JWKS JSON string containing an RS256 RSA key pair.
* Can be generated using `node scripts/generate-oidc-jwk.mjs`.
*/
JWKS_KEY?: string;
}
}
}
@@ -103,7 +103,12 @@ const ModelSwitch = memo(() => {
provider={provider}
onModelChange={handleModelChange}
>
<Center className={styles.model} height={36} style={borderRadius ? { borderRadius } : undefined} width={36} >
<Center
className={styles.model}
height={36}
style={borderRadius ? { borderRadius } : undefined}
width={36}
>
<div className={styles.icon}>
<ModelIcon model={model} size={22} />
</div>
@@ -6,7 +6,7 @@ import PluginTag from '@/components/Plugins/PluginTag';
import { useToolStore } from '@/store/tool';
import { customPluginSelectors } from '@/store/tool/selectors';
import type { CheckboxItemProps } from '../components/CheckboxWithLoading';
import { type CheckboxItemProps } from '../components/CheckboxWithLoading';
import CheckboxItem from '../components/CheckboxWithLoading';
const ToolItem = memo<CheckboxItemProps>(({ id, onUpdate, label, checked }) => {
@@ -1,14 +1,13 @@
import type { ItemType } from '@lobehub/ui';
import { type ItemType } from '@lobehub/ui';
import { Flexbox, Icon, Text } from '@lobehub/ui';
import { Divider } from 'antd';
import { createStaticStyles, cssVar } from 'antd-style';
import type { ReactNode } from 'react';
import { type ReactNode } from 'react';
import { Fragment, isValidElement, memo } from 'react';
export const toolsListStyles = createStaticStyles(({ css }) => ({
groupLabel: css`
padding-block-start: 12px;
padding-block-end: 4px;
padding-block: 12px 4px;
padding-inline: 12px;
`,
item: css`
@@ -1,21 +1,21 @@
import { type UIChatMessage } from '@lobechat/types';
import { ActionIconGroup, Flexbox, createRawModal } from '@lobehub/ui';
import type { ActionIconGroupEvent, ActionIconGroupItemType } from '@lobehub/ui';
import { type ActionIconGroupEvent, type ActionIconGroupItemType } from '@lobehub/ui';
import { ActionIconGroup, createRawModal, Flexbox } from '@lobehub/ui';
import { memo, useCallback, useMemo } from 'react';
import { ReactionPicker } from '../../../components/Reaction';
import ShareMessageModal, { type ShareModalProps } from '../../../components/ShareMessageModal';
import {
Provider,
createStore,
messageStateSelectors,
Provider,
useConversationStore,
useConversationStoreApi,
} from '../../../store';
import type {
MessageActionItem,
MessageActionItemOrDivider,
MessageActionsConfig,
import {
type MessageActionItem,
type MessageActionItemOrDivider,
type MessageActionsConfig,
} from '../../../types';
import { ErrorActionsBar } from './Error';
import { useAssistantActions } from './useAssistantActions';
@@ -211,7 +211,7 @@ export const AssistantActionsBar = memo<AssistantActionsBarProps>(
if (error) return <ErrorActionsBar actions={defaultActions} onActionClick={handleAction} />;
return (
<Flexbox align={'center'} gap={8} horizontal>
<Flexbox horizontal align={'center'} gap={8}>
<ReactionPicker messageId={id} />
<ActionIconGroup items={items} menu={menu} onActionClick={handleAction} />
</Flexbox>
@@ -85,8 +85,8 @@ const MessageContent = memo<UIChatMessage>(
<ReactionDisplay
isActive={isActive}
messageId={id}
onReactionClick={handleReactionClick}
reactions={reactions}
onReactionClick={handleReactionClick}
/>
)}
</Flexbox>
@@ -1,21 +1,21 @@
import { type AssistantContentBlock, type UIChatMessage } from '@lobechat/types';
import { ActionIconGroup, Flexbox, createRawModal } from '@lobehub/ui';
import type { ActionIconGroupEvent, ActionIconGroupItemType } from '@lobehub/ui';
import { type ActionIconGroupEvent, type ActionIconGroupItemType } from '@lobehub/ui';
import { ActionIconGroup, createRawModal, Flexbox } from '@lobehub/ui';
import { memo, useCallback, useMemo } from 'react';
import { ReactionPicker } from '../../../components/Reaction';
import ShareMessageModal, { type ShareModalProps } from '../../../components/ShareMessageModal';
import {
Provider,
createStore,
messageStateSelectors,
Provider,
useConversationStore,
useConversationStoreApi,
} from '../../../store';
import type {
MessageActionItem,
MessageActionItemOrDivider,
MessageActionsConfig,
import {
type MessageActionItem,
type MessageActionItemOrDivider,
type MessageActionsConfig,
} from '../../../types';
import { useGroupActions } from './useGroupActions';
@@ -164,7 +164,7 @@ const WithContentId = memo<GroupActionsProps>(({ actionsConfig, id, data, conten
);
return (
<Flexbox align={'center'} gap={8} horizontal>
<Flexbox horizontal align={'center'} gap={8}>
<ReactionPicker messageId={id} />
<ActionIconGroup items={items} menu={menu} onActionClick={handleAction} />
</Flexbox>
@@ -1,9 +1,9 @@
'use client';
import type { AssistantContentBlock, EmojiReaction } from '@lobechat/types';
import { type AssistantContentBlock, type EmojiReaction } from '@lobechat/types';
import isEqual from 'fast-deep-equal';
import type {MouseEventHandler} from 'react';
import { memo, Suspense, useCallback, useMemo } from 'react';
import { type MouseEventHandler } from 'react';
import { memo, Suspense, useCallback, useMemo } from 'react';
import { MESSAGE_ACTION_BAR_PORTAL_ATTRIBUTES } from '@/const/messageActionPortal';
import { ChatItem } from '@/features/Conversation/ChatItem';
@@ -177,8 +177,8 @@ const GroupMessage = memo<GroupMessageProps>(({ id, index, disableEditing, isLat
<ReactionDisplay
isActive={isReactionActive}
messageId={id}
onReactionClick={handleReactionClick}
reactions={reactions}
onReactionClick={handleReactionClick}
/>
)}
<Suspense fallback={null}>
@@ -1,6 +1,6 @@
'use client';
import type { CompressionGroupMetadata, UIChatMessage } from '@lobechat/types';
import { type CompressionGroupMetadata, type UIChatMessage } from '@lobechat/types';
import {
ActionIcon,
Flexbox,
@@ -148,26 +148,26 @@ const CompressedGroupMessage = memo<CompressedGroupMessageProps>(({ id }) => {
<StreamingMarkdown>{content}</StreamingMarkdown>
</>
) : (
<Flexbox align={'center'} distribution={'space-between'} horizontal width={'100%'}>
<Flexbox horizontal align={'center'} distribution={'space-between'} width={'100%'}>
<Tabs
compact
activeKey={isGeneratingSummary ? 'summary' : activeTab}
className={styles.header}
compact
items={tabItems}
onChange={handleTabChange}
variant={'rounded'}
onChange={handleTabChange}
/>
<Flexbox gap={4} horizontal>
<Flexbox horizontal gap={4}>
<ActionIcon
icon={Undo2}
onClick={handleCancelCompression}
size={'small'}
title={t('compression.cancel')}
onClick={handleCancelCompression}
/>
<ActionIcon
icon={expanded ? ChevronUp : ChevronDown}
onClick={() => toggleCompressedGroupExpanded(id)}
size={'small'}
onClick={() => toggleCompressedGroupExpanded(id)}
/>
</Flexbox>
</Flexbox>
@@ -1,21 +1,21 @@
import type {AssistantContentBlock, UIChatMessage} from '@lobechat/types';
import type { ActionIconGroupEvent, ActionIconGroupItemType } from '@lobehub/ui';
import { ActionIconGroup, createRawModal , Flexbox} from '@lobehub/ui';
import { type AssistantContentBlock, type UIChatMessage } from '@lobechat/types';
import { type ActionIconGroupEvent, type ActionIconGroupItemType } from '@lobehub/ui';
import { ActionIconGroup, createRawModal, Flexbox } from '@lobehub/ui';
import { memo, useCallback, useMemo } from 'react';
import { ReactionPicker } from '../../../components/Reaction';
import ShareMessageModal, { type ShareModalProps } from '../../../components/ShareMessageModal';
import {
Provider,
createStore,
messageStateSelectors,
Provider,
useConversationStore,
useConversationStoreApi,
} from '../../../store';
import type {
MessageActionItem,
MessageActionItemOrDivider,
MessageActionsConfig,
import {
type MessageActionItem,
type MessageActionItemOrDivider,
type MessageActionsConfig,
} from '../../../types';
import { useGroupActions } from './useGroupActions';
@@ -156,7 +156,7 @@ const WithContentId = memo<GroupActionsProps>(({ actionsConfig, id, data, conten
);
return (
<Flexbox align={'center'} gap={8} horizontal>
<Flexbox horizontal align={'center'} gap={8}>
<ReactionPicker messageId={id} />
<ActionIconGroup items={items} menu={menu} onActionClick={handleAction} />
</Flexbox>
@@ -7,8 +7,8 @@ import { type AssistantContentBlock } from '@/types/index';
import ErrorContent from '../../../ChatItem/components/ErrorContent';
import { messageStateSelectors, useConversationStore } from '../../../store';
import { Tools } from '../../AssistantGroup/Tools';
import MessageContent from '../../AssistantGroup/components/MessageContent';
import { Tools } from '../../AssistantGroup/Tools';
import Reasoning from '../../components/Reasoning';
interface ContentBlockProps extends AssistantContentBlock {
@@ -1,6 +1,6 @@
'use client';
import type { EmojiReaction } from '@lobechat/types';
import { type EmojiReaction } from '@lobechat/types';
import { Tag } from '@lobehub/ui';
import isEqual from 'fast-deep-equal';
import { type MouseEventHandler } from 'react';
@@ -156,8 +156,8 @@ const GroupMessage = memo<GroupMessageProps>(({ id, index, disableEditing, isLat
<ReactionDisplay
isActive={isReactionActive}
messageId={id}
onReactionClick={handleReactionClick}
reactions={reactions}
onReactionClick={handleReactionClick}
/>
)}
</ChatItem>
@@ -1,6 +1,6 @@
'use client';
import type { EmojiReaction } from '@lobechat/types';
import { type EmojiReaction } from '@lobechat/types';
import { Flexbox } from '@lobehub/ui';
import { createStyles } from 'antd-style';
import { memo } from 'react';
@@ -71,7 +71,7 @@ const ReactionDisplay = memo<ReactionDisplayProps>(
if (reactions.length === 0) return null;
return (
<Flexbox align={'center'} className={styles.container} horizontal>
<Flexbox horizontal align={'center'} className={styles.container}>
{reactions.map((reaction) => (
<div
className={cx(styles.reactionTag, isActive?.(reaction.emoji) && styles.active)}
@@ -6,7 +6,7 @@ import { ActionIcon, Flexbox, Tooltip } from '@lobehub/ui';
import { Popover } from 'antd';
import { createStyles, useTheme } from 'antd-style';
import { PlusIcon, SmilePlus } from 'lucide-react';
import { type FC, type ReactNode, memo, useState } from 'react';
import { type FC, memo, type ReactNode, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useGlobalStore } from '@/store/global';
@@ -91,13 +91,13 @@ const ReactionPicker: FC<ReactionPickerProps> = memo(({ messageId, trigger }) =>
<Picker
data={data}
locale={locale?.split('-')[0] || 'en'}
onEmojiSelect={(emoji: any) => handleSelect(emoji.native)}
previewPosition="none"
skinTonePosition="none"
theme={theme.appearance === 'dark' ? 'dark' : 'light'}
onEmojiSelect={(emoji: any) => handleSelect(emoji.native)}
/>
) : (
<Flexbox className={styles.pickerContainer} gap={4} horizontal wrap="wrap">
<Flexbox horizontal className={styles.pickerContainer} gap={4} wrap="wrap">
{QUICK_REACTIONS.map((emoji) => (
<div className={styles.emojiButton} key={emoji} onClick={() => handleSelect(emoji)}>
{emoji}
@@ -113,11 +113,11 @@ const ReactionPicker: FC<ReactionPickerProps> = memo(({ messageId, trigger }) =>
<Popover
arrow={false}
content={content}
onOpenChange={handleOpenChange}
open={open}
overlayInnerStyle={{ padding: 0 }}
placement="top"
trigger="click"
onOpenChange={handleOpenChange}
>
{trigger || (
<span {...(open ? { 'data-popup-open': '' } : {})}>
@@ -1,11 +1,11 @@
import type { StateCreator } from 'zustand';
import { type StateCreator } from 'zustand';
import type { Store as ConversationStore } from '../../../action';
import { type Store as ConversationStore } from '../../../action';
import { type MessageCRUDAction, messageCRUDSlice } from './crud';
import { type MessageReactionAction, messageReactionSlice } from './reaction';
import { sendMessage } from './sendMessage';
import type {MessageStateAction} from './state';
import { messageStateSlice } from './state';
import { type MessageStateAction } from './state';
import { messageStateSlice } from './state';
/**
* Message Actions
@@ -16,7 +16,8 @@ import { messageStateSlice } from './state';
* - State management (loading, collapsed, editing)
* - Sending messages
*/
export interface MessageAction extends MessageCRUDAction, MessageReactionAction, MessageStateAction {
export interface MessageAction
extends MessageCRUDAction, MessageReactionAction, MessageStateAction {
/**
* Add an AI message (convenience method)
*/
@@ -1,10 +1,10 @@
import type { EmojiReaction } from '@lobechat/types';
import type { StateCreator } from 'zustand';
import { type EmojiReaction } from '@lobechat/types';
import { type StateCreator } from 'zustand';
import { useUserStore } from '@/store/user';
import { userProfileSelectors } from '@/store/user/selectors';
import type { Store as ConversationStore } from '../../../action';
import { type Store as ConversationStore } from '../../../action';
import { dataSelectors } from '../../data/selectors';
export interface MessageReactionAction {
+2 -2
View File
@@ -1,6 +1,6 @@
'use client';
import type { IEditor } from '@lobehub/editor';
import { type IEditor } from '@lobehub/editor';
import {
ReactCodemirrorPlugin,
ReactCodePlugin,
@@ -17,7 +17,7 @@ import { Editor, useEditorState } from '@lobehub/editor/react';
import { memo, useEffect, useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import type { EditorCanvasProps } from './EditorCanvas';
import { type EditorCanvasProps } from './EditorCanvas';
import InlineToolbar from './InlineToolbar';
/**
+1 -1
View File
@@ -1,6 +1,6 @@
'use client';
import { Center, Flexbox, Icon, Tooltip, stopPropagation } from '@lobehub/ui';
import { Center, Flexbox, Icon, stopPropagation, Tooltip } from '@lobehub/ui';
import { createStaticStyles, cssVar, cx } from 'antd-style';
import { CircleDashedIcon, HammerIcon, LayersIcon, MessageSquareQuoteIcon } from 'lucide-react';
import qs from 'query-string';
+1 -1
View File
@@ -7,10 +7,10 @@ import {
Button,
Flexbox,
Icon,
stopPropagation,
Tag,
Text,
Tooltip,
stopPropagation,
} from '@lobehub/ui';
import { App } from 'antd';
import { createStaticStyles, cssVar, useResponsive } from 'antd-style';
+1 -1
View File
@@ -4,7 +4,7 @@ import { BUILTIN_AGENT_SLUGS } from '@lobechat/builtin-agents';
import { EditorProvider } from '@lobehub/editor/react';
import { Flexbox } from '@lobehub/ui';
import { cssVar } from 'antd-style';
import type { FC } from 'react';
import { type FC } from 'react';
import { memo, useEffect } from 'react';
import Loading from '@/components/Loading/BrandTextLoading';
@@ -491,8 +491,8 @@ const FileListItem = memo<FileListItemProps>(
align={'center'}
gap={8}
paddingInline={8}
onPointerDown={stopPropagation}
onClick={stopPropagation}
onPointerDown={stopPropagation}
>
{!isFolder &&
!isPage &&
@@ -1,4 +1,4 @@
import { Button, Flexbox, Tooltip, stopPropagation } from '@lobehub/ui';
import { Button, Flexbox, stopPropagation, Tooltip } from '@lobehub/ui';
import { createStaticStyles, cx } from 'antd-style';
import { isNull } from 'es-toolkit/compat';
import { FileBoxIcon, Folder } from 'lucide-react';
@@ -1,4 +1,4 @@
import { Button, Flexbox, Tooltip, stopPropagation } from '@lobehub/ui';
import { Button, Flexbox, stopPropagation, Tooltip } from '@lobehub/ui';
import { Image } from 'antd';
import { createStaticStyles, cx } from 'antd-style';
import { isNull } from 'es-toolkit/compat';
@@ -1,4 +1,4 @@
import { Button, Tooltip, stopPropagation } from '@lobehub/ui';
import { Button, stopPropagation, Tooltip } from '@lobehub/ui';
import { createStaticStyles, cx } from 'antd-style';
import { isNull } from 'es-toolkit/compat';
import { FileBoxIcon } from 'lucide-react';
@@ -1,4 +1,4 @@
import { Button, Tooltip, stopPropagation } from '@lobehub/ui';
import { Button, stopPropagation, Tooltip } from '@lobehub/ui';
import { createStaticStyles, cx } from 'antd-style';
import { isNull } from 'es-toolkit/compat';
import { FileBoxIcon } from 'lucide-react';
@@ -3,7 +3,7 @@
import { ActionIcon, Block, DropdownMenu, Flexbox, Icon, stopPropagation } from '@lobehub/ui';
import { App } from 'antd';
import { cssVar } from 'antd-style';
import type { Klavis } from 'klavis';
import { type Klavis } from 'klavis';
import { Loader2, MoreVerticalIcon, Plus, Unplug } from 'lucide-react';
import React, { memo } from 'react';
import { useTranslation } from 'react-i18next';
+4 -4
View File
@@ -6,29 +6,29 @@ import {
CreateUserMemoryIdentitySchema,
MemorySourceType,
UpdateUserMemoryIdentitySchema,
UserMemoryExtractionMetadata,
type UserMemoryExtractionMetadata,
} from '@lobechat/types';
import { TRPCError } from '@trpc/server';
import { z } from 'zod';
import { AsyncTaskModel, initUserMemoryExtractionMetadata } from '@/database/models/asyncTask';
import { TopicModel } from '@/database/models/topic';
import { UserMemoryModel } from '@/database/models/userMemory';
import {
UserMemoryActivityModel,
UserMemoryContextModel,
UserMemoryExperienceModel,
UserMemoryIdentityModel,
UserMemoryModel,
UserMemoryPreferenceModel,
} from '@/database/models/userMemory/index';
} from '@/database/models/userMemory';
import { UserPersonaModel } from '@/database/models/userMemory/persona';
import { appEnv } from '@/envs/app';
import { authedProcedure, router } from '@/libs/trpc/lambda';
import { serverDatabase } from '@/libs/trpc/lambda/middleware';
import { parseMemoryExtractionConfig } from '@/server/globalConfig/parseMemoryExtractionConfig';
import {
MemoryExtractionWorkflowService,
buildWorkflowPayloadInput,
MemoryExtractionWorkflowService,
normalizeMemoryExtractionPayload,
} from '@/server/services/memory/userMemory/extract';
@@ -6,8 +6,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { DiscoverService } from '@/server/services/discover';
import {
type ScheduleToolCallReportParams,
scheduleToolCallReport,
type ScheduleToolCallReportParams,
} from './scheduleToolCallReport';
// Mock Next.js after() function
@@ -1,7 +1,7 @@
import { describe, expect, it, vi } from 'vitest';
import { StreamingHandler } from '../StreamingHandler';
import type { StreamChunk, StreamingCallbacks, StreamingContext } from '../types/streaming';
import { type StreamingCallbacks, type StreamingContext } from '../types/streaming';
// Helper to create a mock streaming context
const createContext = (overrides: Partial<StreamingContext> = {}): StreamingContext => ({
+5 -5
View File
@@ -1,15 +1,15 @@
'use client';
import type { IEditor } from '@lobehub/editor/es/types';
import type { EditorState as LobehubEditorState } from '@lobehub/editor/react';
import { type IEditor } from '@lobehub/editor/es/types';
import { type EditorState as LobehubEditorState } from '@lobehub/editor/react';
import isEqual from 'fast-deep-equal';
import { documentService } from '@/services/document';
import type { StoreSetter } from '@/store/types';
import { type StoreSetter } from '@/store/types';
import { setNamespace } from '@/utils/storeDebug';
import type { DocumentStore } from '../../store';
import type { DocumentDispatch } from './reducer';
import { type DocumentStore } from '../../store';
import { type DocumentDispatch } from './reducer';
import { documentReducer } from './reducer';
const n = setNamespace('document/editor');
@@ -13,7 +13,7 @@ import { knowledgeBaseExecutor } from '@lobechat/builtin-tool-knowledge-base/exe
import { localSystemExecutor } from '@lobechat/builtin-tool-local-system/executor';
import { memoryExecutor } from '@lobechat/builtin-tool-memory/executor';
import type { IBuiltinToolExecutor } from '../types';
import { type IBuiltinToolExecutor } from '../types';
import { notebookExecutor } from './lobe-notebook';
import { pageAgentExecutor } from './lobe-page-agent';
import { webBrowsing } from './lobe-web-browsing';
+1 -1
View File
@@ -1,5 +1,5 @@
import { CLASSNAMES } from '@lobehub/ui';
import type { Theme } from 'antd-style';
import { type Theme } from 'antd-style';
import { css } from 'antd-style';
// fix ios input keyboard