@lobehub/lobehub 2.0.0-next.188 → 2.0.0-next.189
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/changelog/v1.json +9 -0
- package/e2e/CLAUDE.md +109 -2
- package/e2e/docs/llm-mock.md +68 -0
- package/e2e/docs/local-setup.md +354 -0
- package/e2e/docs/testing-tips.md +94 -0
- package/e2e/src/features/journeys/agent/agent-conversation.feature +0 -32
- package/e2e/src/mocks/llm/index.ts +6 -6
- package/e2e/src/steps/agent/conversation.steps.ts +3 -471
- package/package.json +2 -2
- package/src/app/[variants]/(main)/chat/_layout/Sidebar/Topic/List/Item/Actions.tsx +4 -13
- package/src/app/[variants]/(main)/chat/_layout/Sidebar/Topic/List/Item/index.tsx +23 -29
- package/src/app/[variants]/(main)/chat/_layout/Sidebar/Topic/List/Item/useDropdownMenu.tsx +3 -3
- package/src/app/[variants]/(main)/chat/_layout/Sidebar/Topic/TopicListContent/ThreadList/ThreadItem/Actions.tsx +4 -13
- package/src/app/[variants]/(main)/chat/_layout/Sidebar/Topic/TopicListContent/ThreadList/ThreadItem/index.tsx +10 -18
- package/src/app/[variants]/(main)/chat/_layout/Sidebar/Topic/TopicListContent/ThreadList/ThreadItem/useDropdownMenu.tsx +3 -3
- package/src/app/[variants]/(main)/community/(detail)/assistant/features/Sidebar/ActionButton/AddAgent.tsx +47 -27
- package/src/app/[variants]/(main)/community/(detail)/user/features/UserAgentCard.tsx +4 -3
- package/src/app/[variants]/(main)/group/_layout/Sidebar/Topic/List/Item/Actions.tsx +4 -13
- package/src/app/[variants]/(main)/group/_layout/Sidebar/Topic/List/Item/index.tsx +23 -29
- package/src/app/[variants]/(main)/group/_layout/Sidebar/Topic/List/Item/useDropdownMenu.tsx +3 -3
- package/src/app/[variants]/(main)/group/_layout/Sidebar/Topic/TopicListContent/ThreadList/ThreadItem/Actions.tsx +4 -13
- package/src/app/[variants]/(main)/group/_layout/Sidebar/Topic/TopicListContent/ThreadList/ThreadItem/index.tsx +10 -18
- package/src/app/[variants]/(main)/group/_layout/Sidebar/Topic/TopicListContent/ThreadList/ThreadItem/useDropdownMenu.tsx +3 -3
- package/src/app/[variants]/(main)/group/profile/features/AgentBuilder/TopicSelector.tsx +18 -20
- package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentGroupItem/index.tsx +19 -25
- package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentItem/index.tsx +21 -26
- package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/Item/Actions.tsx +4 -13
- package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/Item/useDropdownMenu.tsx +3 -3
- package/src/app/[variants]/(main)/home/_layout/Body/Project/List/Actions.tsx +4 -13
- package/src/app/[variants]/(main)/home/_layout/Body/Project/List/Item.tsx +8 -15
- package/src/app/[variants]/(main)/home/_layout/Body/Project/List/useDropdownMenu.tsx +3 -3
- package/src/app/[variants]/(main)/home/_layout/Header/components/AddButton.tsx +3 -4
- package/src/app/[variants]/(main)/page/_layout/Body/List/Item/Actions.tsx +4 -13
- package/src/app/[variants]/(main)/page/_layout/Body/List/Item/index.tsx +13 -20
- package/src/app/[variants]/(main)/page/_layout/Body/List/Item/useDropdownMenu.tsx +3 -3
- package/src/app/[variants]/(main)/resource/(home)/_layout/Body/LibraryList/List/Item/Actions.tsx +4 -13
- package/src/app/[variants]/(main)/resource/(home)/_layout/Body/LibraryList/List/Item/index.tsx +16 -23
- package/src/app/[variants]/(main)/resource/(home)/_layout/Body/LibraryList/List/Item/useDropdownMenu.tsx +3 -3
- package/src/app/[variants]/(main)/resource/library/_layout/Header/LibraryHead.tsx +4 -6
- package/src/features/AgentBuilder/TopicSelector.tsx +18 -17
- package/src/features/Conversation/ChatItem/style.ts +7 -0
- package/src/features/Conversation/Messages/Assistant/Actions/Error.tsx +1 -3
- package/src/features/Conversation/Messages/Assistant/Actions/index.tsx +37 -16
- package/src/features/Conversation/Messages/AssistantGroup/Actions/index.tsx +36 -17
- package/src/features/Conversation/Messages/Supervisor/Actions/index.tsx +36 -17
- package/src/features/Conversation/Messages/Task/Actions/Error.tsx +1 -3
- package/src/features/Conversation/Messages/Task/Actions/index.tsx +31 -15
- package/src/features/Conversation/Messages/User/Actions/index.tsx +1 -1
- package/src/features/Conversation/Messages/index.tsx +8 -59
- package/src/features/Conversation/components/ShareMessageModal/index.tsx +1 -1
- package/src/features/Conversation/hooks/useChatItemContextMenu.tsx +313 -83
- package/src/features/NavPanel/components/NavItem.tsx +33 -3
- package/src/features/PageEditor/Copilot/TopicSelector/Actions.tsx +6 -14
- package/src/features/PageEditor/Copilot/TopicSelector/TopicItem.tsx +1 -0
- package/src/features/PageEditor/Copilot/TopicSelector/useDropdownMenu.tsx +6 -3
- package/src/features/ResourceManager/components/Explorer/ItemDropdown/DropdownMenu.tsx +12 -35
- package/src/features/ResourceManager/components/Explorer/ItemDropdown/useFileItemDropdown.tsx +4 -8
- package/src/features/ResourceManager/components/Explorer/ListView/ListItem/index.tsx +162 -160
- package/src/features/ResourceManager/components/Explorer/MasonryView/MasonryFileItem/index.tsx +16 -8
- package/src/features/ResourceManager/components/Explorer/ToolBar/ActionIconWithChevron.tsx +4 -3
- package/src/features/ResourceManager/components/Explorer/ToolBar/BatchActionsDropdown.tsx +6 -12
- package/src/features/ResourceManager/components/Explorer/ToolBar/SortDropdown.tsx +8 -8
- package/src/features/ResourceManager/components/Explorer/ToolBar/ViewSwitcher.tsx +8 -11
- package/src/features/ResourceManager/components/Tree/index.tsx +121 -122
- package/src/layout/GlobalProvider/index.tsx +5 -2
- package/src/styles/global.ts +6 -0
- package/src/features/Conversation/components/ContextMenu.tsx +0 -418
|
@@ -11,35 +11,3 @@ Feature: Agent 对话用户体验链路
|
|
|
11
11
|
When 用户发送消息 "hello"
|
|
12
12
|
Then 用户应该收到助手的回复
|
|
13
13
|
And 回复内容应该可见
|
|
14
|
-
|
|
15
|
-
@AGENT-CHAT-002 @P0
|
|
16
|
-
Scenario: 多轮对话保持上下文
|
|
17
|
-
Given 用户进入 Lobe AI 对话页面
|
|
18
|
-
When 用户发送消息 "我的名字是小明"
|
|
19
|
-
Then 用户应该收到助手的回复
|
|
20
|
-
When 用户发送消息 "我刚才说我的名字是什么?"
|
|
21
|
-
Then 用户应该收到助手的回复
|
|
22
|
-
And 回复内容应该包含 "小明"
|
|
23
|
-
|
|
24
|
-
@AGENT-CHAT-003 @P0
|
|
25
|
-
Scenario: 清空对话历史
|
|
26
|
-
Given 用户进入 Lobe AI 对话页面
|
|
27
|
-
And 用户已发送消息 "hello"
|
|
28
|
-
When 用户点击清空对话按钮
|
|
29
|
-
Then 对话历史应该被清空
|
|
30
|
-
And 页面应该显示欢迎界面
|
|
31
|
-
|
|
32
|
-
@AGENT-CHAT-004 @P0
|
|
33
|
-
Scenario: 重新生成回复
|
|
34
|
-
Given 用户进入 Lobe AI 对话页面
|
|
35
|
-
And 用户已发送消息 "hello"
|
|
36
|
-
When 用户点击重新生成按钮
|
|
37
|
-
Then 用户应该收到新的助手回复
|
|
38
|
-
|
|
39
|
-
@AGENT-CHAT-005 @P0
|
|
40
|
-
Scenario: 停止生成回复
|
|
41
|
-
Given 用户进入 Lobe AI 对话页面
|
|
42
|
-
When 用户发送消息 "写一篇很长的文章"
|
|
43
|
-
And 用户在生成过程中点击停止按钮
|
|
44
|
-
Then 回复应该停止生成
|
|
45
|
-
And 已生成的内容应该保留
|
|
@@ -231,14 +231,14 @@ export const presetResponses = {
|
|
|
231
231
|
codeHelp: 'I can help you with coding! Please share the code you would like me to review.',
|
|
232
232
|
error: 'I apologize, but I encountered an error processing your request.',
|
|
233
233
|
greeting: 'Hello! I am Lobe AI, your AI assistant. How can I help you today?',
|
|
234
|
-
|
|
234
|
+
|
|
235
235
|
// Long response for stop generation test
|
|
236
|
-
longArticle:
|
|
236
|
+
longArticle:
|
|
237
237
|
'这是一篇很长的文章。第一段:人工智能是计算机科学的一个分支,它企图了解智能的实质,并生产出一种新的能以人类智能相似的方式做出反应的智能机器。第二段:人工智能研究的主要目标包括推理、知识、规划、学习、自然语言处理、感知和移动与操控物体的能力。第三段:目前,人工智能已经在许多领域取得了重大突破,包括图像识别、语音识别、自然语言处理等。',
|
|
238
|
-
|
|
239
|
-
// Multi-turn conversation responses
|
|
240
|
-
nameIntro: '好的,我记住了,你的名字是小明。很高兴认识你,小明!有什么我可以帮助你的吗?',
|
|
241
|
-
|
|
238
|
+
|
|
239
|
+
// Multi-turn conversation responses
|
|
240
|
+
nameIntro: '好的,我记住了,你的名字是小明。很高兴认识你,小明!有什么我可以帮助你的吗?',
|
|
241
|
+
|
|
242
242
|
nameRecall: '你刚才说你的名字是小明。',
|
|
243
243
|
// Regenerate response
|
|
244
244
|
regenerated: '这是重新生成的回复内容。我是 Lobe AI,很高兴为你服务!',
|
|
@@ -22,31 +22,19 @@ Given('用户已登录系统', async function (this: CustomWorld) {
|
|
|
22
22
|
|
|
23
23
|
Given('用户进入 Lobe AI 对话页面', async function (this: CustomWorld) {
|
|
24
24
|
console.log(' 📍 Step: 设置 LLM mock...');
|
|
25
|
-
// Setup LLM mock before navigation
|
|
25
|
+
// Setup LLM mock before navigation
|
|
26
26
|
llmMockManager.setResponse('hello', presetResponses.greeting);
|
|
27
|
-
llmMockManager.setResponse('hello world', presetResponses.greeting);
|
|
28
|
-
llmMockManager.setResponse('我的名字是小明', presetResponses.nameIntro);
|
|
29
|
-
llmMockManager.setResponse('我刚才说我的名字是什么?', presetResponses.nameRecall);
|
|
30
|
-
llmMockManager.setResponse('我刚才说我的名字是什么', presetResponses.nameRecall);
|
|
31
|
-
llmMockManager.setResponse('写一篇很长的文章', presetResponses.longArticle);
|
|
32
|
-
llmMockManager.setResponse('测试对话内容', '这是测试对话的回复内容。');
|
|
33
|
-
llmMockManager.setResponse('第一个对话', '这是第一个对话的回复。');
|
|
34
|
-
llmMockManager.setResponse('第二个对话', '这是第二个对话的回复。');
|
|
35
27
|
await llmMockManager.setup(this.page);
|
|
36
28
|
|
|
37
29
|
console.log(' 📍 Step: 导航到首页...');
|
|
38
30
|
// Navigate to home page first
|
|
39
31
|
await this.page.goto('/');
|
|
40
|
-
await this.page.waitForLoadState('networkidle', { timeout:
|
|
41
|
-
|
|
42
|
-
console.log(' 📍 Step: 等待助手列表加载...');
|
|
43
|
-
// Wait for skeletons to disappear (assistant list to load)
|
|
44
|
-
await this.page.waitForTimeout(2000);
|
|
32
|
+
await this.page.waitForLoadState('networkidle', { timeout: 10_000 });
|
|
45
33
|
|
|
46
34
|
console.log(' 📍 Step: 查找 Lobe AI...');
|
|
47
35
|
// Find and click on "Lobe AI" agent in the sidebar/home
|
|
48
36
|
const lobeAIAgent = this.page.locator('text=Lobe AI').first();
|
|
49
|
-
await expect(lobeAIAgent).toBeVisible({ timeout:
|
|
37
|
+
await expect(lobeAIAgent).toBeVisible({ timeout: 10_000 });
|
|
50
38
|
|
|
51
39
|
console.log(' 📍 Step: 点击 Lobe AI...');
|
|
52
40
|
await lobeAIAgent.click();
|
|
@@ -163,459 +151,3 @@ Then('回复内容应该可见', async function (this: CustomWorld) {
|
|
|
163
151
|
|
|
164
152
|
console.log(` ✅ Assistant replied: "${text?.slice(0, 50)}..."`);
|
|
165
153
|
});
|
|
166
|
-
|
|
167
|
-
Then('回复内容应该包含 {string}', async function (this: CustomWorld, expectedText: string) {
|
|
168
|
-
console.log(` 📍 Step: 验证回复包含 "${expectedText}"...`);
|
|
169
|
-
|
|
170
|
-
// Get the last assistant message
|
|
171
|
-
const assistantMessages = this.page.locator(
|
|
172
|
-
'[data-role="assistant"], [class*="assistant"], [class*="message"]',
|
|
173
|
-
);
|
|
174
|
-
const lastMessage = assistantMessages.last();
|
|
175
|
-
|
|
176
|
-
await expect(lastMessage).toBeVisible({ timeout: 10_000 });
|
|
177
|
-
|
|
178
|
-
// Get text content
|
|
179
|
-
const text = await lastMessage.textContent();
|
|
180
|
-
console.log(` 📍 回复内容: "${text?.slice(0, 100)}..."`);
|
|
181
|
-
|
|
182
|
-
expect(text).toContain(expectedText);
|
|
183
|
-
console.log(` ✅ 回复包含 "${expectedText}"`);
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
// ============================================
|
|
187
|
-
// Given Steps for Advanced Scenarios
|
|
188
|
-
// ============================================
|
|
189
|
-
|
|
190
|
-
Given('用户已发送消息 {string}', async function (this: CustomWorld, message: string) {
|
|
191
|
-
console.log(` 📍 Step: 发送预备消息 "${message}"...`);
|
|
192
|
-
|
|
193
|
-
// Find and click the chat input
|
|
194
|
-
const chatInputs = this.page.locator('[data-testid="chat-input"]');
|
|
195
|
-
const count = await chatInputs.count();
|
|
196
|
-
|
|
197
|
-
let chatInputContainer = chatInputs.first();
|
|
198
|
-
for (let i = 0; i < count; i++) {
|
|
199
|
-
const elem = chatInputs.nth(i);
|
|
200
|
-
const box = await elem.boundingBox();
|
|
201
|
-
if (box && box.width > 0 && box.height > 0) {
|
|
202
|
-
chatInputContainer = elem;
|
|
203
|
-
break;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
await chatInputContainer.click();
|
|
208
|
-
await this.page.waitForTimeout(300);
|
|
209
|
-
await this.page.keyboard.type(message, { delay: 30 });
|
|
210
|
-
await this.page.keyboard.press('Enter');
|
|
211
|
-
|
|
212
|
-
// Wait for response
|
|
213
|
-
await this.page.waitForTimeout(2000);
|
|
214
|
-
|
|
215
|
-
// Verify we got a response
|
|
216
|
-
const assistantMessage = this.page
|
|
217
|
-
.locator('[data-role="assistant"], [class*="assistant"], [class*="message"]')
|
|
218
|
-
.last();
|
|
219
|
-
await expect(assistantMessage).toBeVisible({ timeout: 15_000 });
|
|
220
|
-
|
|
221
|
-
console.log(` ✅ 预备消息已发送并收到回复`);
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
// ============================================
|
|
225
|
-
// When Steps for Advanced Scenarios
|
|
226
|
-
// ============================================
|
|
227
|
-
|
|
228
|
-
When('用户点击清空对话按钮', async function (this: CustomWorld) {
|
|
229
|
-
console.log(' 📍 Step: 查找清空对话按钮...');
|
|
230
|
-
|
|
231
|
-
// The clear button uses an Eraser icon from lucide-react and is visible in the ActionBar
|
|
232
|
-
// The ActionBar is in the footer of ChatInput component
|
|
233
|
-
// We need to find all buttons on the page and look for the one with the Eraser icon
|
|
234
|
-
|
|
235
|
-
// Look for ALL buttons on the page that have SVG icons
|
|
236
|
-
// This is a broader search to capture all action bar buttons
|
|
237
|
-
const allPageButtons = this.page.locator('button:has(svg)');
|
|
238
|
-
const pageButtonCount = await allPageButtons.count();
|
|
239
|
-
console.log(` 📍 Found ${pageButtonCount} buttons with SVG on page`);
|
|
240
|
-
|
|
241
|
-
let clearButtonFound = false;
|
|
242
|
-
|
|
243
|
-
// First try to find by lucide class name for eraser
|
|
244
|
-
const eraserByClass = this.page.locator('svg.lucide-eraser').locator('..');
|
|
245
|
-
if ((await eraserByClass.count()) > 0) {
|
|
246
|
-
console.log(' 📍 Found eraser button by class name');
|
|
247
|
-
await eraserByClass.first().click();
|
|
248
|
-
clearButtonFound = true;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// If not found by class, iterate through buttons and check SVG path data
|
|
252
|
-
if (!clearButtonFound) {
|
|
253
|
-
for (let i = 0; i < pageButtonCount; i++) {
|
|
254
|
-
const btn = allPageButtons.nth(i);
|
|
255
|
-
const box = await btn.boundingBox();
|
|
256
|
-
if (!box || box.width === 0 || box.height === 0) continue;
|
|
257
|
-
|
|
258
|
-
// Check SVG class
|
|
259
|
-
const svgInButton = btn.locator('svg').first();
|
|
260
|
-
const svgClass = await svgInButton.getAttribute('class').catch(() => '');
|
|
261
|
-
|
|
262
|
-
if (svgClass?.includes('eraser') || svgClass?.toLowerCase().includes('eraser')) {
|
|
263
|
-
console.log(` 📍 Found eraser by class at button ${i}: ${svgClass}`);
|
|
264
|
-
await btn.click();
|
|
265
|
-
clearButtonFound = true;
|
|
266
|
-
break;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// Check path data - the Eraser icon has specific path
|
|
270
|
-
const pathElement = btn.locator('svg path').first();
|
|
271
|
-
const pathD = await pathElement.getAttribute('d').catch(() => '');
|
|
272
|
-
|
|
273
|
-
// Eraser icon path data pattern from lucide-react
|
|
274
|
-
// Check for multiple possible patterns
|
|
275
|
-
if (
|
|
276
|
-
pathD?.includes('m7 21') ||
|
|
277
|
-
pathD?.includes('M7 21') ||
|
|
278
|
-
pathD?.includes('7 21-4.3-4.3') ||
|
|
279
|
-
pathD?.includes('21l-4.3')
|
|
280
|
-
) {
|
|
281
|
-
console.log(` 📍 Found eraser button by path at index ${i}`);
|
|
282
|
-
await btn.click();
|
|
283
|
-
clearButtonFound = true;
|
|
284
|
-
break;
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// Fallback: hover over buttons in bottom area and find one with "清空" tooltip
|
|
290
|
-
if (!clearButtonFound) {
|
|
291
|
-
console.log(' 📍 Trying hover approach to find button with 清空 tooltip...');
|
|
292
|
-
|
|
293
|
-
// Focus on buttons in the bottom 200px of viewport
|
|
294
|
-
for (let i = 0; i < pageButtonCount; i++) {
|
|
295
|
-
const btn = allPageButtons.nth(i);
|
|
296
|
-
const box = await btn.boundingBox();
|
|
297
|
-
|
|
298
|
-
// Only check buttons in the bottom area (action bar)
|
|
299
|
-
if (!box || box.width === 0 || box.height === 0) continue;
|
|
300
|
-
if (box.y < 500) continue; // Skip buttons not in bottom area
|
|
301
|
-
|
|
302
|
-
// Hover to trigger tooltip
|
|
303
|
-
await btn.hover();
|
|
304
|
-
await this.page.waitForTimeout(300);
|
|
305
|
-
|
|
306
|
-
// Check if tooltip with "清空" appeared
|
|
307
|
-
const tooltip = this.page.locator('.ant-tooltip:has-text("清空")');
|
|
308
|
-
if ((await tooltip.count()) > 0) {
|
|
309
|
-
console.log(` 📍 Found clear button by tooltip at index ${i}`);
|
|
310
|
-
await btn.click();
|
|
311
|
-
clearButtonFound = true;
|
|
312
|
-
break;
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// Last resort: click buttons in bottom area and check for Popconfirm
|
|
318
|
-
if (!clearButtonFound) {
|
|
319
|
-
console.log(' 📍 Last resort: clicking bottom buttons to find Popconfirm...');
|
|
320
|
-
for (let i = 0; i < pageButtonCount; i++) {
|
|
321
|
-
const btn = allPageButtons.nth(i);
|
|
322
|
-
const box = await btn.boundingBox();
|
|
323
|
-
if (!box || box.width === 0 || box.height === 0) continue;
|
|
324
|
-
if (box.y < 500) continue; // Focus on bottom area
|
|
325
|
-
|
|
326
|
-
await btn.click();
|
|
327
|
-
await this.page.waitForTimeout(300);
|
|
328
|
-
|
|
329
|
-
// Check if Popconfirm appeared
|
|
330
|
-
const popconfirm = this.page.locator(
|
|
331
|
-
'.ant-popconfirm, .ant-popover:has(button.ant-btn-dangerous)',
|
|
332
|
-
);
|
|
333
|
-
if ((await popconfirm.count()) > 0 && (await popconfirm.first().isVisible())) {
|
|
334
|
-
console.log(` 📍 Found Popconfirm after clicking button ${i}`);
|
|
335
|
-
clearButtonFound = true;
|
|
336
|
-
break;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// Press Escape to dismiss any popover
|
|
340
|
-
await this.page.keyboard.press('Escape');
|
|
341
|
-
await this.page.waitForTimeout(100);
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
if (!clearButtonFound) {
|
|
346
|
-
throw new Error('Could not find the clear button');
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
// Wait for Popconfirm to appear and click the confirm button
|
|
350
|
-
console.log(' 📍 Step: 确认清空...');
|
|
351
|
-
await this.page.waitForTimeout(500);
|
|
352
|
-
|
|
353
|
-
// The Popconfirm has a danger primary button for confirmation
|
|
354
|
-
const confirmButton = this.page.locator(
|
|
355
|
-
'.ant-popconfirm button.ant-btn-primary, .ant-popover button.ant-btn-primary',
|
|
356
|
-
);
|
|
357
|
-
await expect(confirmButton).toBeVisible({ timeout: 5000 });
|
|
358
|
-
await confirmButton.click();
|
|
359
|
-
|
|
360
|
-
await this.page.waitForTimeout(500);
|
|
361
|
-
console.log(' ✅ 已点击清空对话按钮');
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
When('用户点击重新生成按钮', async function (this: CustomWorld) {
|
|
365
|
-
console.log(' 📍 Step: 查找重新生成按钮...');
|
|
366
|
-
|
|
367
|
-
// The regenerate action is in the ActionIconGroup menu for assistant messages
|
|
368
|
-
// ActionIconGroup renders ActionIcon buttons and a "more" button (MoreHorizontal icon)
|
|
369
|
-
// The "more" button opens a dropdown menu with "重新生成" option
|
|
370
|
-
// Action buttons only appear on hover over the message
|
|
371
|
-
|
|
372
|
-
// Wait for the message to be rendered
|
|
373
|
-
await this.page.waitForTimeout(500);
|
|
374
|
-
|
|
375
|
-
// Find assistant messages by their structure
|
|
376
|
-
// Assistant messages have class "message-wrapper" and are aligned to the left
|
|
377
|
-
const messageWrappers = this.page.locator('.message-wrapper');
|
|
378
|
-
const wrapperCount = await messageWrappers.count();
|
|
379
|
-
console.log(` 📍 Found ${wrapperCount} message wrappers`);
|
|
380
|
-
|
|
381
|
-
// Find the assistant message by looking for the one with "Lobe AI" text
|
|
382
|
-
let assistantMessage = null;
|
|
383
|
-
for (let i = wrapperCount - 1; i >= 0; i--) {
|
|
384
|
-
const wrapper = messageWrappers.nth(i);
|
|
385
|
-
const titleText = await wrapper
|
|
386
|
-
.locator('.message-header')
|
|
387
|
-
.textContent()
|
|
388
|
-
.catch(() => '');
|
|
389
|
-
console.log(` 📍 Message ${i} title: "${titleText?.slice(0, 30)}..."`);
|
|
390
|
-
|
|
391
|
-
// Check if this is an assistant message (has "Lobe AI" or similar in title)
|
|
392
|
-
if (titleText?.includes('Lobe AI') || titleText?.includes('AI')) {
|
|
393
|
-
assistantMessage = wrapper;
|
|
394
|
-
console.log(` 📍 Found assistant message at index ${i}`);
|
|
395
|
-
break;
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
if (!assistantMessage) {
|
|
400
|
-
throw new Error('No assistant messages found');
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
// Hover over the message to reveal action buttons
|
|
404
|
-
console.log(' 📍 Hovering over assistant message to reveal actions...');
|
|
405
|
-
await assistantMessage.hover();
|
|
406
|
-
await this.page.waitForTimeout(800);
|
|
407
|
-
|
|
408
|
-
// The action bar with role="menubar" contains the ActionIconGroup
|
|
409
|
-
// The "more" button uses MoreHorizontal icon from lucide-react (class: lucide-more-horizontal)
|
|
410
|
-
// Try to find the more button by its icon class
|
|
411
|
-
const moreButtonByClass = this.page.locator('svg.lucide-more-horizontal').locator('..');
|
|
412
|
-
let moreButtonCount = await moreButtonByClass.count();
|
|
413
|
-
console.log(` 📍 Found ${moreButtonCount} buttons with more-horizontal icon`);
|
|
414
|
-
|
|
415
|
-
let menuOpened = false;
|
|
416
|
-
|
|
417
|
-
if (moreButtonCount > 0) {
|
|
418
|
-
// Find the one in the main content area (not sidebar)
|
|
419
|
-
for (let i = 0; i < moreButtonCount; i++) {
|
|
420
|
-
const btn = moreButtonByClass.nth(i);
|
|
421
|
-
const btnBox = await btn.boundingBox();
|
|
422
|
-
if (!btnBox || btnBox.x < 320) continue; // Skip sidebar buttons
|
|
423
|
-
|
|
424
|
-
console.log(` 📍 More button ${i} at (${btnBox.x}, ${btnBox.y})`);
|
|
425
|
-
await btn.click();
|
|
426
|
-
await this.page.waitForTimeout(500);
|
|
427
|
-
|
|
428
|
-
// Check if dropdown menu appeared with regenerate option
|
|
429
|
-
const menu = this.page.locator('.ant-dropdown-menu:visible');
|
|
430
|
-
if ((await menu.count()) > 0) {
|
|
431
|
-
const hasRegenerate = this.page.locator('.ant-dropdown-menu-item:has-text("重新生成")');
|
|
432
|
-
if ((await hasRegenerate.count()) > 0) {
|
|
433
|
-
console.log(` 📍 Found menu with regenerate option`);
|
|
434
|
-
menuOpened = true;
|
|
435
|
-
break;
|
|
436
|
-
} else {
|
|
437
|
-
const menuItems = await this.page.locator('.ant-dropdown-menu-item').allTextContents();
|
|
438
|
-
console.log(` 📍 Menu items: ${menuItems.slice(0, 5).join(', ')}...`);
|
|
439
|
-
await this.page.keyboard.press('Escape');
|
|
440
|
-
await this.page.waitForTimeout(200);
|
|
441
|
-
// Re-hover to keep action bar visible
|
|
442
|
-
await assistantMessage.hover();
|
|
443
|
-
await this.page.waitForTimeout(300);
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
// Fallback: Look for all buttons in the action bar area after hovering
|
|
450
|
-
if (!menuOpened) {
|
|
451
|
-
console.log(' 📍 Fallback: Looking for buttons in action bar area...');
|
|
452
|
-
await assistantMessage.hover();
|
|
453
|
-
await this.page.waitForTimeout(500);
|
|
454
|
-
|
|
455
|
-
// Find the action bar within message
|
|
456
|
-
const actionBar = assistantMessage.locator('[role="menubar"]');
|
|
457
|
-
if ((await actionBar.count()) > 0) {
|
|
458
|
-
// Look for all buttons (ActionIcon components render as buttons)
|
|
459
|
-
const allButtons = actionBar.locator('button, [role="button"]');
|
|
460
|
-
const allButtonCount = await allButtons.count();
|
|
461
|
-
console.log(` 📍 Found ${allButtonCount} buttons in action bar`);
|
|
462
|
-
|
|
463
|
-
// Try clicking the last button (usually the "more" button)
|
|
464
|
-
for (let i = allButtonCount - 1; i >= 0; i--) {
|
|
465
|
-
const btn = allButtons.nth(i);
|
|
466
|
-
await btn.click();
|
|
467
|
-
await this.page.waitForTimeout(500);
|
|
468
|
-
|
|
469
|
-
const menu = this.page.locator('.ant-dropdown-menu:visible');
|
|
470
|
-
if ((await menu.count()) > 0) {
|
|
471
|
-
const hasRegenerate = this.page.locator('.ant-dropdown-menu-item:has-text("重新生成")');
|
|
472
|
-
if ((await hasRegenerate.count()) > 0) {
|
|
473
|
-
menuOpened = true;
|
|
474
|
-
break;
|
|
475
|
-
}
|
|
476
|
-
await this.page.keyboard.press('Escape');
|
|
477
|
-
await assistantMessage.hover();
|
|
478
|
-
await this.page.waitForTimeout(300);
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
// Click on the regenerate option in the dropdown menu
|
|
485
|
-
console.log(' 📍 Looking for regenerate option in menu...');
|
|
486
|
-
const regenerateOption = this.page.locator(
|
|
487
|
-
'.ant-dropdown-menu-item:has-text("重新生成"), .ant-dropdown-menu-item:has-text("Regenerate"), [data-menu-id*="regenerate"]',
|
|
488
|
-
);
|
|
489
|
-
|
|
490
|
-
if ((await regenerateOption.count()) > 0) {
|
|
491
|
-
await expect(regenerateOption.first()).toBeVisible({ timeout: 5000 });
|
|
492
|
-
console.log(' 📍 Clicking regenerate option...');
|
|
493
|
-
await regenerateOption.first().click();
|
|
494
|
-
} else {
|
|
495
|
-
throw new Error('Regenerate option not found in menu');
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
console.log(' ✅ 已点击重新生成按钮');
|
|
499
|
-
});
|
|
500
|
-
|
|
501
|
-
When('用户在生成过程中点击停止按钮', async function (this: CustomWorld) {
|
|
502
|
-
console.log(' 📍 Step: 等待生成开始...');
|
|
503
|
-
await this.page.waitForTimeout(500);
|
|
504
|
-
|
|
505
|
-
console.log(' 📍 Step: 查找停止按钮...');
|
|
506
|
-
const stopButton = this.page.locator(
|
|
507
|
-
'button[aria-label*="停止"], button[aria-label*="stop"], [data-testid="stop-generate"]',
|
|
508
|
-
);
|
|
509
|
-
|
|
510
|
-
// The stop button should appear during generation
|
|
511
|
-
const stopButtonVisible = await stopButton
|
|
512
|
-
.first()
|
|
513
|
-
.isVisible()
|
|
514
|
-
.catch(() => false);
|
|
515
|
-
if (stopButtonVisible) {
|
|
516
|
-
await stopButton.first().click();
|
|
517
|
-
console.log(' ✅ 已点击停止按钮');
|
|
518
|
-
} else {
|
|
519
|
-
console.log(' ⚠️ 停止按钮不可见,可能生成已完成');
|
|
520
|
-
}
|
|
521
|
-
});
|
|
522
|
-
|
|
523
|
-
// ============================================
|
|
524
|
-
// Then Steps for Advanced Scenarios
|
|
525
|
-
// ============================================
|
|
526
|
-
|
|
527
|
-
Then('对话历史应该被清空', async function (this: CustomWorld) {
|
|
528
|
-
console.log(' 📍 Step: 验证对话历史已清空...');
|
|
529
|
-
|
|
530
|
-
// Wait for the clear to take effect
|
|
531
|
-
await this.page.waitForTimeout(1000);
|
|
532
|
-
|
|
533
|
-
// Check that there are no user/assistant messages in the main chat area
|
|
534
|
-
// Only look for messages with explicit data-role attribute, which are actual chat messages
|
|
535
|
-
// Avoid matching sidebar items or other elements with "message" in class
|
|
536
|
-
const userMessages = this.page.locator('[data-role="user"]');
|
|
537
|
-
const assistantMessages = this.page.locator('[data-role="assistant"]');
|
|
538
|
-
|
|
539
|
-
const userCount = await userMessages.count();
|
|
540
|
-
const assistantCount = await assistantMessages.count();
|
|
541
|
-
|
|
542
|
-
console.log(` 📍 用户消息数量: ${userCount}, 助手消息数量: ${assistantCount}`);
|
|
543
|
-
|
|
544
|
-
// There should be no user or assistant messages after clearing
|
|
545
|
-
expect(userCount).toBe(0);
|
|
546
|
-
expect(assistantCount).toBe(0);
|
|
547
|
-
|
|
548
|
-
console.log(' ✅ 对话历史已清空');
|
|
549
|
-
});
|
|
550
|
-
|
|
551
|
-
Then('页面应该显示欢迎界面', async function (this: CustomWorld) {
|
|
552
|
-
console.log(' 📍 Step: 验证显示欢迎界面...');
|
|
553
|
-
|
|
554
|
-
// Look for welcome elements - Lobe AI title or welcome text in the main chat area
|
|
555
|
-
// The welcome page shows Lobe AI avatar and introductory text
|
|
556
|
-
// Try multiple selectors to find the welcome content
|
|
557
|
-
const welcomeText = this.page.locator('text=我是你的智能助理');
|
|
558
|
-
const lobeAITitle = this.page.locator('h1:has-text("Lobe AI"), h2:has-text("Lobe AI")');
|
|
559
|
-
const welcomeStart = this.page.locator('text=从任何想法开始');
|
|
560
|
-
|
|
561
|
-
const hasWelcomeText = (await welcomeText.count()) > 0;
|
|
562
|
-
const hasLobeTitle = (await lobeAITitle.count()) > 0;
|
|
563
|
-
const hasStartText = (await welcomeStart.count()) > 0;
|
|
564
|
-
|
|
565
|
-
console.log(
|
|
566
|
-
` 📍 欢迎文本: ${hasWelcomeText}, Lobe标题: ${hasLobeTitle}, 开始提示: ${hasStartText}`,
|
|
567
|
-
);
|
|
568
|
-
|
|
569
|
-
// At least one of the welcome elements should be visible
|
|
570
|
-
expect(hasWelcomeText || hasLobeTitle || hasStartText).toBeTruthy();
|
|
571
|
-
console.log(' ✅ 欢迎界面可见');
|
|
572
|
-
});
|
|
573
|
-
|
|
574
|
-
Then('用户应该收到新的助手回复', async function (this: CustomWorld) {
|
|
575
|
-
console.log(' 📍 Step: 等待新回复...');
|
|
576
|
-
|
|
577
|
-
// Wait for a new response to appear
|
|
578
|
-
await this.page.waitForTimeout(2000);
|
|
579
|
-
|
|
580
|
-
const assistantMessage = this.page
|
|
581
|
-
.locator('[data-role="assistant"], [class*="assistant"], [class*="message"]')
|
|
582
|
-
.last();
|
|
583
|
-
|
|
584
|
-
await expect(assistantMessage).toBeVisible({ timeout: 15_000 });
|
|
585
|
-
console.log(' ✅ 收到新的助手回复');
|
|
586
|
-
});
|
|
587
|
-
|
|
588
|
-
Then('回复应该停止生成', async function (this: CustomWorld) {
|
|
589
|
-
console.log(' 📍 Step: 验证生成已停止...');
|
|
590
|
-
|
|
591
|
-
// The stop button should no longer be visible
|
|
592
|
-
const stopButton = this.page.locator(
|
|
593
|
-
'button[aria-label*="停止"], button[aria-label*="stop"], [data-testid="stop-generate"]',
|
|
594
|
-
);
|
|
595
|
-
|
|
596
|
-
// Wait a bit and check if stop button is gone
|
|
597
|
-
await this.page.waitForTimeout(1000);
|
|
598
|
-
const isStopVisible = await stopButton
|
|
599
|
-
.first()
|
|
600
|
-
.isVisible()
|
|
601
|
-
.catch(() => false);
|
|
602
|
-
|
|
603
|
-
// Stop button should be hidden after stopping
|
|
604
|
-
expect(isStopVisible).toBeFalsy();
|
|
605
|
-
console.log(' ✅ 生成已停止');
|
|
606
|
-
});
|
|
607
|
-
|
|
608
|
-
Then('已生成的内容应该保留', async function (this: CustomWorld) {
|
|
609
|
-
console.log(' 📍 Step: 验证已生成内容...');
|
|
610
|
-
|
|
611
|
-
// There should be some content in the last assistant message
|
|
612
|
-
const assistantMessage = this.page
|
|
613
|
-
.locator('[data-role="assistant"], [class*="assistant"], [class*="message"]')
|
|
614
|
-
.last();
|
|
615
|
-
|
|
616
|
-
const text = await assistantMessage.textContent();
|
|
617
|
-
expect(text).toBeTruthy();
|
|
618
|
-
expect(text!.length).toBeGreaterThan(0);
|
|
619
|
-
|
|
620
|
-
console.log(` ✅ 已生成内容保留: "${text?.slice(0, 50)}..."`);
|
|
621
|
-
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobehub/lobehub",
|
|
3
|
-
"version": "2.0.0-next.
|
|
3
|
+
"version": "2.0.0-next.189",
|
|
4
4
|
"description": "LobeHub - an open-source,comprehensive AI Agent framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"framework",
|
|
@@ -207,7 +207,7 @@
|
|
|
207
207
|
"@lobehub/icons": "^4.0.2",
|
|
208
208
|
"@lobehub/market-sdk": "^0.25.1",
|
|
209
209
|
"@lobehub/tts": "^4.0.2",
|
|
210
|
-
"@lobehub/ui": "^4.
|
|
210
|
+
"@lobehub/ui": "^4.8.0",
|
|
211
211
|
"@modelcontextprotocol/sdk": "^1.25.1",
|
|
212
212
|
"@neondatabase/serverless": "^1.0.2",
|
|
213
213
|
"@next/third-parties": "^16.1.1",
|
|
@@ -1,25 +1,16 @@
|
|
|
1
|
-
import { ActionIcon,
|
|
1
|
+
import { ActionIcon, type DropdownItem, DropdownMenu } from '@lobehub/ui';
|
|
2
2
|
import { MoreHorizontalIcon } from 'lucide-react';
|
|
3
3
|
import { memo } from 'react';
|
|
4
4
|
|
|
5
5
|
interface ActionProps {
|
|
6
|
-
dropdownMenu:
|
|
6
|
+
dropdownMenu: DropdownItem[] | (() => DropdownItem[]);
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
const Actions = memo<ActionProps>(({ dropdownMenu }) => {
|
|
10
10
|
return (
|
|
11
|
-
<
|
|
12
|
-
arrow={false}
|
|
13
|
-
menu={{
|
|
14
|
-
items: dropdownMenu,
|
|
15
|
-
onClick: ({ domEvent }) => {
|
|
16
|
-
domEvent.stopPropagation();
|
|
17
|
-
},
|
|
18
|
-
}}
|
|
19
|
-
trigger={['click']}
|
|
20
|
-
>
|
|
11
|
+
<DropdownMenu items={dropdownMenu}>
|
|
21
12
|
<ActionIcon icon={MoreHorizontalIcon} size={'small'} />
|
|
22
|
-
</
|
|
13
|
+
</DropdownMenu>
|
|
23
14
|
);
|
|
24
15
|
});
|
|
25
16
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ActionIcon,
|
|
1
|
+
import { ActionIcon, Flexbox, Icon, Skeleton, Tag } from '@lobehub/ui';
|
|
2
2
|
import { cssVar } from 'antd-style';
|
|
3
3
|
import { MessageSquareDashed, Star } from 'lucide-react';
|
|
4
4
|
import { Suspense, memo, useCallback } from 'react';
|
|
@@ -92,34 +92,28 @@ const TopicItem = memo<TopicItemProps>(({ id, title, fav, active, threadId }) =>
|
|
|
92
92
|
|
|
93
93
|
return (
|
|
94
94
|
<Flexbox style={{ position: 'relative' }}>
|
|
95
|
-
<
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
loading={isLoading}
|
|
118
|
-
onClick={handleClick}
|
|
119
|
-
onDoubleClick={handleDoubleClick}
|
|
120
|
-
title={title}
|
|
121
|
-
/>
|
|
122
|
-
</Dropdown>
|
|
95
|
+
<NavItem
|
|
96
|
+
actions={<Actions dropdownMenu={dropdownMenu} />}
|
|
97
|
+
active={active && !threadId && !isInAgentSubRoute}
|
|
98
|
+
contextMenuItems={dropdownMenu}
|
|
99
|
+
disabled={editing}
|
|
100
|
+
icon={
|
|
101
|
+
<ActionIcon
|
|
102
|
+
color={fav ? cssVar.colorWarning : undefined}
|
|
103
|
+
fill={fav ? cssVar.colorWarning : 'transparent'}
|
|
104
|
+
icon={Star}
|
|
105
|
+
onClick={(e) => {
|
|
106
|
+
e.stopPropagation();
|
|
107
|
+
favoriteTopic(id, !fav);
|
|
108
|
+
}}
|
|
109
|
+
size={'small'}
|
|
110
|
+
/>
|
|
111
|
+
}
|
|
112
|
+
loading={isLoading}
|
|
113
|
+
onClick={handleClick}
|
|
114
|
+
onDoubleClick={handleDoubleClick}
|
|
115
|
+
title={title}
|
|
116
|
+
/>
|
|
123
117
|
<Editing id={id} title={title} toggleEditing={toggleEditing} />
|
|
124
118
|
{active && (
|
|
125
119
|
<Suspense
|