mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-27 13:29:15 +07:00
🐛 fix: fix group ux and memory retriever (#11481)
* update topic * update topic * memory not block * remove incorrectly memory fetch * refactor group switch topic * improve streaming style for Agent Tool display * fix group tab active issue * 🐛 fix: E2E test for switching conversations and TypeScript type errors - Fix E2E test 'AGENT-CONV-002' that was failing when switching between conversations - Add detection for home page vs agent page state - Use correct selectors for Recent Topics cards on home page - Add proper wait time for messages to load after topic switch - Use '.message-wrapper' selector for message verification - Fix TypeScript type errors in MakedownRender.tsx - Add explicit type annotations for a and img component props - Import ReactNode type - Remove unused @ts-expect-error directive in useMarkdown.tsx 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -124,9 +124,41 @@ When('用户点击新建对话按钮', async function (this: CustomWorld) {
|
||||
When('用户点击另一个对话', async function (this: CustomWorld) {
|
||||
console.log(' 📍 Step: 点击另一个对话...');
|
||||
|
||||
// Find topic items in the sidebar
|
||||
// Check if we're on the home page (has Recent Topics section)
|
||||
const recentTopicsSection = this.page.locator('text=Recent Topics');
|
||||
const isOnHomePage = (await recentTopicsSection.count()) > 0;
|
||||
console.log(` 📍 Is on home page: ${isOnHomePage}`);
|
||||
|
||||
if (isOnHomePage) {
|
||||
// Click the second topic card in Recent Topics section
|
||||
// Cards are wrapped in Link components and contain "Hello! I am a mock AI" text from the mock
|
||||
const recentTopicCards = this.page.locator('a[href*="topic="]');
|
||||
const cardCount = await recentTopicCards.count();
|
||||
console.log(` 📍 Found ${cardCount} recent topic cards (by href)`);
|
||||
|
||||
if (cardCount >= 2) {
|
||||
// Click the second card (different from current topic)
|
||||
await recentTopicCards.nth(1).click();
|
||||
console.log(' ✅ 已点击首页 Recent Topics 中的另一个对话');
|
||||
await this.page.waitForTimeout(2000);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback: try to find by text content
|
||||
const topicTextCards = this.page.locator('text=Hello! I am a mock AI');
|
||||
const textCardCount = await topicTextCards.count();
|
||||
console.log(` 📍 Found ${textCardCount} topic cards by text`);
|
||||
|
||||
if (textCardCount >= 2) {
|
||||
await topicTextCards.nth(1).click();
|
||||
console.log(' ✅ 已点击首页 Recent Topics 中的另一个对话 (by text)');
|
||||
await this.page.waitForTimeout(2000);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: try to find topic items in the sidebar
|
||||
// Topics are displayed with star icons (lucide-star) in the left sidebar
|
||||
// Each topic item has a star icon as part of it
|
||||
const sidebarTopics = this.page.locator('svg.lucide-star').locator('..').locator('..');
|
||||
let topicCount = await sidebarTopics.count();
|
||||
console.log(` 📍 Found ${topicCount} topics with star icons`);
|
||||
@@ -505,8 +537,20 @@ Then('应该切换到该对话', async function (this: CustomWorld) {
|
||||
Then('显示该对话的历史消息', async function (this: CustomWorld) {
|
||||
console.log(' 📍 Step: 验证显示历史消息...');
|
||||
|
||||
// Wait for the loading to finish - the messages need time to load after switching topics
|
||||
console.log(' 📍 等待消息加载...');
|
||||
await this.page.waitForTimeout(2000);
|
||||
|
||||
// Wait for the message wrapper to appear (ChatItem component uses message-wrapper class)
|
||||
const messageSelector = '.message-wrapper';
|
||||
try {
|
||||
await this.page.waitForSelector(messageSelector, { timeout: 10_000 });
|
||||
} catch {
|
||||
console.log(' ⚠️ 等待消息选择器超时,尝试备用选择器...');
|
||||
}
|
||||
|
||||
// There should be messages in the chat area
|
||||
const messages = this.page.locator('[class*="message"], [data-role]');
|
||||
const messages = this.page.locator(messageSelector);
|
||||
const messageCount = await messages.count();
|
||||
|
||||
console.log(` 📍 找到 ${messageCount} 条消息`);
|
||||
|
||||
@@ -7,6 +7,8 @@ import { Users } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import ToolTag from '@/features/ToolTag';
|
||||
|
||||
import type { BatchCreateAgentsParams, BatchCreateAgentsState } from '../../types';
|
||||
|
||||
const styles = createStaticStyles(({ css, cssVar }) => ({
|
||||
@@ -58,19 +60,33 @@ interface AgentItemProps {
|
||||
avatar?: string;
|
||||
description?: string;
|
||||
title: string;
|
||||
tools?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
const AgentItem = memo<AgentItemProps>(({ agent, definition }) => {
|
||||
const avatar = definition?.avatar;
|
||||
const description = definition?.description;
|
||||
const tools = definition?.tools;
|
||||
|
||||
return (
|
||||
<Flexbox align="center" className={styles.item} gap={12} horizontal>
|
||||
<Avatar avatar={avatar} size={24} style={{ flexShrink: 0 }} title={agent.title} />
|
||||
<Flexbox flex={1} gap={2} style={{ minWidth: 0, overflow: 'hidden' }}>
|
||||
<Flexbox align="flex-start" className={styles.item} gap={12} horizontal>
|
||||
<Avatar
|
||||
avatar={avatar}
|
||||
size={24}
|
||||
style={{ flexShrink: 0, marginTop: 4 }}
|
||||
title={agent.title}
|
||||
/>
|
||||
<Flexbox flex={1} gap={4} style={{ minWidth: 0, overflow: 'hidden' }}>
|
||||
<span className={styles.title}>{agent.title}</span>
|
||||
{description && <span className={styles.description}>{description}</span>}
|
||||
{tools && tools.length > 0 && (
|
||||
<Flexbox gap={4} horizontal style={{ marginTop: 8 }} wrap={'wrap'}>
|
||||
{tools.map((tool) => (
|
||||
<ToolTag identifier={tool} key={tool} variant={'compact'} />
|
||||
))}
|
||||
</Flexbox>
|
||||
)}
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
);
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
|
||||
import type { BuiltinStreamingProps } from '@lobechat/types';
|
||||
import { Avatar, Block, Flexbox, Markdown } from '@lobehub/ui';
|
||||
import { Divider } from 'antd';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
import { memo } from 'react';
|
||||
|
||||
import ToolTag from '@/features/ToolTag';
|
||||
|
||||
import type { BatchCreateAgentsParams } from '../../../types';
|
||||
|
||||
const styles = createStaticStyles(({ css, cssVar }) => ({
|
||||
@@ -12,9 +15,8 @@ const styles = createStaticStyles(({ css, cssVar }) => ({
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-line-clamp: 1;
|
||||
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: ${cssVar.colorTextDescription};
|
||||
text-overflow: ellipsis;
|
||||
@@ -62,12 +64,25 @@ export const BatchCreateAgentsStreaming = memo<BuiltinStreamingProps<BatchCreate
|
||||
return (
|
||||
<Block variant={'outlined'} width="100%">
|
||||
{agents.map((agent, index) => (
|
||||
<Flexbox className={styles.item} gap={8} horizontal key={index}>
|
||||
<Flexbox align={'flex-start'} className={styles.item} gap={8} horizontal key={index}>
|
||||
<div className={styles.index}>{index + 1}.</div>
|
||||
<Avatar avatar={agent.avatar} size={24} style={{ flexShrink: 0 }} title={agent.title} />
|
||||
<Avatar
|
||||
avatar={agent.avatar}
|
||||
size={24}
|
||||
style={{ flexShrink: 0, marginTop: 4 }}
|
||||
title={agent.title}
|
||||
/>
|
||||
<Flexbox flex={1} gap={4} style={{ minWidth: 0, overflow: 'hidden' }}>
|
||||
<span className={styles.title}>{agent.title}</span>
|
||||
{agent.description && <span className={styles.description}>{agent.description}</span>}
|
||||
{agent.tools && agent.tools.length > 0 && (
|
||||
<Flexbox gap={4} horizontal style={{ marginTop: 8 }} wrap={'wrap'}>
|
||||
{agent.tools.map((tool) => (
|
||||
<ToolTag identifier={tool} key={tool} />
|
||||
))}
|
||||
</Flexbox>
|
||||
)}
|
||||
<Divider />
|
||||
{agent.systemRole && (
|
||||
<div className={styles.systemRole}>
|
||||
<Markdown animated variant={'chat'}>
|
||||
|
||||
@@ -2,28 +2,18 @@
|
||||
|
||||
import type { BuiltinStreamingProps } from '@lobechat/types';
|
||||
import { Block, Markdown } from '@lobehub/ui';
|
||||
import { memo, useEffect } from 'react';
|
||||
|
||||
import { useGroupProfileStore } from '@/store/groupProfile';
|
||||
import { memo } from 'react';
|
||||
|
||||
import type { UpdateAgentPromptParams } from '../../../types';
|
||||
|
||||
export const UpdateAgentPromptStreaming = memo<BuiltinStreamingProps<UpdateAgentPromptParams>>(
|
||||
({ args }) => {
|
||||
const { agentId, prompt } = args || {};
|
||||
const setActiveTabId = useGroupProfileStore((s) => s.setActiveTabId);
|
||||
|
||||
// Switch to agent tab when streaming agent prompt
|
||||
useEffect(() => {
|
||||
if (agentId) {
|
||||
setActiveTabId(agentId);
|
||||
}
|
||||
}, [agentId, setActiveTabId]);
|
||||
const { prompt } = args || {};
|
||||
|
||||
if (!prompt) return null;
|
||||
|
||||
return (
|
||||
<Block padding={4} variant={'outlined'} width="100%">
|
||||
<Block paddingBlock={8} paddingInline={12} variant={'outlined'} width="100%">
|
||||
<Markdown animated variant={'chat'}>
|
||||
{prompt}
|
||||
</Markdown>
|
||||
|
||||
@@ -21,7 +21,7 @@ export const UpdateGroupPromptStreaming = memo<BuiltinStreamingProps<UpdateGroup
|
||||
if (!prompt) return null;
|
||||
|
||||
return (
|
||||
<Block padding={4} variant={'outlined'} width="100%">
|
||||
<Block paddingBlock={8} paddingInline={12} variant={'outlined'} width="100%">
|
||||
<Markdown animated variant={'chat'}>
|
||||
{prompt}
|
||||
</Markdown>
|
||||
|
||||
@@ -141,29 +141,48 @@ When creating agents (via \`createAgent\` or \`batchCreateAgents\`), you MUST an
|
||||
</agent_tools_assignment>
|
||||
|
||||
<workflow>
|
||||
**CRITICAL: Follow this execution order strictly when setting up or modifying a group:**
|
||||
|
||||
1. **Understand the request**: Listen carefully to what the user wants to configure
|
||||
2. **Reference injected context**: Use the \`<current_group_context>\` to understand current state - no need to call read APIs
|
||||
3. **Distinguish prompt types**: Determine if the user wants to modify shared content (group prompt) or a specific agent's behavior (agent prompt)
|
||||
4. **Make targeted changes**: Use the appropriate API based on whether you're modifying the group or a specific agent
|
||||
5. **Update supervisor prompt after member changes**: **IMPORTANT** - After ANY member change (create, invite, or remove agent), you MUST automatically update the supervisor's prompt using \`updateAgentPrompt\` with the supervisor's agentId. Generate an appropriate orchestration prompt based on the current members.
|
||||
6. **Confirm changes**: Report what was changed and the new values
|
||||
|
||||
**Execution Order (MUST follow this sequence):**
|
||||
|
||||
3. **Step 1 - Update Group Identity FIRST**: Before anything else, update the group's title, description, and avatar using \`updateGroup\`. This establishes the group's identity and purpose.
|
||||
|
||||
4. **Step 2 - Set Group Context SECOND**: Use \`updateGroupPrompt\` to establish the shared knowledge base, background information, and project context. This must be done BEFORE creating agents so they can benefit from this context.
|
||||
|
||||
5. **Step 3 - Create/Invite Agents THIRD**: Only after steps 1 and 2 are complete, proceed to create or invite agents using \`createAgent\`, \`batchCreateAgents\`, or \`inviteAgent\`.
|
||||
|
||||
6. **Step 4 - Update Supervisor Prompt**: After ANY member change (create, invite, or remove agent), you MUST automatically update the supervisor's prompt using \`updateAgentPrompt\` with the supervisor's agentId. Generate an appropriate orchestration prompt based on the current members.
|
||||
|
||||
7. **Step 5 - Configure Additional Settings**: Set opening message, opening questions, and other configurations using \`updateGroup\`.
|
||||
|
||||
8. **Confirm changes**: Report what was changed and the new values
|
||||
|
||||
**Why this order matters:**
|
||||
- Group identity (title/avatar) helps users understand the group's purpose immediately
|
||||
- Group context provides the foundation that all agents will reference
|
||||
- Agents created after context is set can leverage that shared knowledge
|
||||
- Supervisor prompt should reflect the final team composition
|
||||
</workflow>
|
||||
|
||||
<guidelines>
|
||||
1. **Use injected context**: The current group's config and member list are already available. Reference them directly instead of calling read APIs.
|
||||
2. **Distinguish group vs agent prompts**:
|
||||
1. **CRITICAL - Follow execution order**: When building or significantly modifying a group, ALWAYS follow the sequence: (1) Update group title/avatar → (2) Set group context → (3) Create/invite agents → (4) Update supervisor prompt. Never create agents before setting the group identity and context.
|
||||
2. **Use injected context**: The current group's config and member list are already available. Reference them directly instead of calling read APIs.
|
||||
3. **Distinguish group vs agent prompts**:
|
||||
- Group prompt: Shared content for all members, NO member info needed (auto-injected)
|
||||
- Agent prompt: Individual agent's system role (supervisor or member), requires agentId
|
||||
3. **Distinguish group vs agent operations**:
|
||||
4. **Distinguish group vs agent operations**:
|
||||
- Group-level: updateGroupPrompt, updateGroup, inviteAgent, removeAgent, batchCreateAgents
|
||||
- Agent-level: updateAgentPrompt (requires agentId), updateConfig (agentId optional, defaults to supervisor), installPlugin
|
||||
4. **CRITICAL - Auto-update supervisor after member changes**: After ANY member change (create, invite, remove), you MUST automatically call \`updateAgentPrompt\` with supervisor's agentId to regenerate the orchestration prompt. This is NOT optional - the supervisor needs updated delegation rules to coordinate the team effectively.
|
||||
5. **CRITICAL - Assign tools when creating agents**: When using \`createAgent\` or \`batchCreateAgents\`, ALWAYS include appropriate \`tools\` based on the agent's role. Reference \`official_tools\` in the context for available tool identifiers. An agent without proper tools cannot perform specialized tasks.
|
||||
6. **Explain your changes**: When modifying configurations, explain what you're changing and why it might benefit the group collaboration.
|
||||
7. **Validate user intent**: For significant changes (like removing an agent), confirm with the user before proceeding.
|
||||
8. **Provide recommendations**: When users ask for advice, consider how changes affect multi-agent collaboration.
|
||||
9. **Use user's language**: Always respond in the same language the user is using.
|
||||
10. **Cannot remove supervisor**: The supervisor agent cannot be removed from the group - it's the orchestrator.
|
||||
5. **CRITICAL - Auto-update supervisor after member changes**: After ANY member change (create, invite, remove), you MUST automatically call \`updateAgentPrompt\` with supervisor's agentId to regenerate the orchestration prompt. This is NOT optional - the supervisor needs updated delegation rules to coordinate the team effectively.
|
||||
6. **CRITICAL - Assign tools when creating agents**: When using \`createAgent\` or \`batchCreateAgents\`, ALWAYS include appropriate \`tools\` based on the agent's role. Reference \`official_tools\` in the context for available tool identifiers. An agent without proper tools cannot perform specialized tasks.
|
||||
7. **Explain your changes**: When modifying configurations, explain what you're changing and why it might benefit the group collaboration.
|
||||
8. **Validate user intent**: For significant changes (like removing an agent), confirm with the user before proceeding.
|
||||
9. **Provide recommendations**: When users ask for advice, consider how changes affect multi-agent collaboration.
|
||||
10. **Use user's language**: Always respond in the same language the user is using.
|
||||
11. **Cannot remove supervisor**: The supervisor agent cannot be removed from the group - it's the orchestrator.
|
||||
</guidelines>
|
||||
|
||||
<configuration_knowledge>
|
||||
@@ -199,113 +218,56 @@ When creating agents (via \`createAgent\` or \`batchCreateAgents\`), you MUST an
|
||||
</configuration_knowledge>
|
||||
|
||||
<examples>
|
||||
<example>
|
||||
User: "Invite an agent to the group"
|
||||
Action:
|
||||
1. Use searchAgent to find available agents, show the results to user
|
||||
2. Use inviteAgent with the selected agent ID
|
||||
3. **Then automatically** use updateAgentPrompt with supervisor's agentId to update orchestration prompt with the newly invited agent's delegation rules
|
||||
</example>
|
||||
<example title="Complete Team Setup (Shows Required Order)">
|
||||
User: "Help me build a development team"
|
||||
Action (MUST follow this order):
|
||||
1. **First** - updateGroup: { meta: { title: "Development Team", avatar: "👨💻" } }
|
||||
2. **Second** - updateGroupPrompt: Add project background, tech stack, coding standards
|
||||
3. **Third** - batchCreateAgents: Create team members with appropriate tools (e.g., Developer with ["lobe-cloud-sandbox"], Researcher with ["web-crawler"])
|
||||
4. **Fourth** - updateAgentPrompt: Update supervisor with delegation rules
|
||||
5. **Finally** - updateGroup: Set openingMessage and openingQuestions
|
||||
</example>
|
||||
|
||||
<example>
|
||||
User: "Add a developer agent to help with coding"
|
||||
Action:
|
||||
1. Use searchAgent with query "developer" or "coding" to find relevant agents
|
||||
2. Use inviteAgent or createAgent if no suitable agent exists. If creating, include tools: ["lobe-cloud-sandbox"] for code execution
|
||||
3. **Then automatically** use updateAgentPrompt with supervisor's agentId to update orchestration prompt with the new developer agent's delegation rules
|
||||
</example>
|
||||
<example title="Add Agent to Group">
|
||||
User: "Add a developer agent" / "Invite an agent"
|
||||
Action:
|
||||
1. Use searchAgent to find existing agents, or createAgent if none suitable (include tools like ["lobe-cloud-sandbox"] for developers)
|
||||
2. Use inviteAgent with the agent ID
|
||||
3. **Auto** - updateAgentPrompt with supervisor's agentId to add delegation rules
|
||||
</example>
|
||||
|
||||
<example>
|
||||
User: "Create a marketing expert for this group"
|
||||
Action:
|
||||
1. Use createAgent with title "Marketing Expert", appropriate systemRole, description, and tools: ["web-crawler"] for research capabilities
|
||||
2. **Then automatically** use updateAgentPrompt with supervisor's agentId to update orchestration prompt, adding delegation rules for marketing-related tasks
|
||||
</example>
|
||||
<example title="Remove Agent">
|
||||
User: "Remove the coding assistant"
|
||||
Action:
|
||||
1. Find agent ID from \`<group_members>\` context
|
||||
2. Use removeAgent
|
||||
3. **Auto** - updateAgentPrompt with supervisor's agentId to remove delegation rules
|
||||
</example>
|
||||
|
||||
<example>
|
||||
User: "Create 3 expert agents for me"
|
||||
Action:
|
||||
1. Use batchCreateAgents to create multiple agents at once with their respective titles, systemRoles, descriptions, and **appropriate tools for each agent's role**
|
||||
2. **Then automatically** use updateAgentPrompt with supervisor's agentId to generate orchestration prompt that includes delegation rules for all 3 new experts
|
||||
</example>
|
||||
<example title="Update Group Prompt (Shared Context)">
|
||||
User: "Add project background" / "Update shared knowledge"
|
||||
Action: Use updateGroupPrompt - this is shared content accessible by ALL members. Do NOT include member info (auto-injected).
|
||||
</example>
|
||||
|
||||
<example>
|
||||
User: "Create a quant trading team"
|
||||
Action:
|
||||
1. Use batchCreateAgents with agents like:
|
||||
- Quant Researcher: tools: ["web-crawler", "lobe-cloud-sandbox"] for market research and data analysis
|
||||
- Execution Specialist: tools: ["lobe-cloud-sandbox"] for backtesting and trade simulation (note: if specific trading MCP is needed, check official_tools or recommend installing one)
|
||||
- Risk Manager: tools: ["lobe-cloud-sandbox"] for risk calculations
|
||||
2. **Then automatically** use updateAgentPrompt with supervisor's agentId to generate orchestration prompt with delegation rules for quant workflows
|
||||
</example>
|
||||
<example title="Update Agent Prompt">
|
||||
User: "Change how supervisor coordinates" / "Update the designer's prompt"
|
||||
Action:
|
||||
- For supervisor: updateAgentPrompt with supervisor's agentId
|
||||
- For member: Find agentId from \`<group_members>\`, then updateAgentPrompt with that agentId
|
||||
</example>
|
||||
|
||||
<example>
|
||||
User: "Remove the coding assistant from the group"
|
||||
Action:
|
||||
1. Check the group members in context, find the agent ID for "coding assistant"
|
||||
2. Use removeAgent to remove the agent
|
||||
3. **Then automatically** use updateAgentPrompt with supervisor's agentId to update orchestration prompt, removing the delegation rules for the removed agent
|
||||
</example>
|
||||
<example title="Update Configuration">
|
||||
User: "Change model to Claude" / "Set welcome message"
|
||||
Action:
|
||||
- Model: updateConfig with { config: { model: "claude-sonnet-4-5-20250929", provider: "anthropic" } }
|
||||
- Welcome/Questions: updateGroup with { config: { openingMessage: "...", openingQuestions: [...] } }
|
||||
- Tools: searchMarketTools then installPlugin
|
||||
</example>
|
||||
|
||||
<example>
|
||||
User: "What agents are in this group?"
|
||||
Action: Reference the \`<group_members>\` from the injected context and display the list
|
||||
</example>
|
||||
|
||||
<example>
|
||||
User: "Add some background information about our project to the group"
|
||||
Action: Use updateGroupPrompt to add the project context as shared content for all members
|
||||
</example>
|
||||
|
||||
<example>
|
||||
User: "Update the group's shared knowledge base"
|
||||
Action: Use updateGroupPrompt - this is shared content, do NOT include member information (auto-injected)
|
||||
</example>
|
||||
|
||||
<example>
|
||||
User: "Change how the supervisor coordinates the team"
|
||||
Action: Use updateAgentPrompt with the supervisor's agentId to update orchestration logic
|
||||
</example>
|
||||
|
||||
<example>
|
||||
User: "Make the supervisor more proactive in assigning tasks"
|
||||
Action: Use updateAgentPrompt with supervisor's agentId to update coordination strategy
|
||||
</example>
|
||||
|
||||
<example>
|
||||
User: "Update the coding assistant's prompt to focus more on Python"
|
||||
Action: Find the coding assistant's agentId from group_members context, then use updateAgentPrompt with that agentId
|
||||
</example>
|
||||
|
||||
<example>
|
||||
User: "Modify the designer agent's prompt"
|
||||
Action: Find the designer agent's agentId from group_members context, then use updateAgentPrompt with that agentId
|
||||
</example>
|
||||
|
||||
<example>
|
||||
User: "Change the supervisor's model to Claude"
|
||||
Action: Use updateConfig with { config: { model: "claude-sonnet-4-5-20250929", provider: "anthropic" } } for the supervisor agent
|
||||
</example>
|
||||
|
||||
<example>
|
||||
User: "What can the supervisor agent do?"
|
||||
Action: Reference the \`<supervisor_agent>\` config from the context, including model, tools, etc.
|
||||
</example>
|
||||
|
||||
<example>
|
||||
User: "Add some new tools to this group"
|
||||
Action: Use searchMarketTools to find tools, then use installPlugin for the supervisor agent
|
||||
</example>
|
||||
|
||||
<example>
|
||||
User: "Set a welcome message for this group"
|
||||
Action: Use updateGroup with { config: { openingMessage: "Welcome to the team! We're here to help you with your project." } }
|
||||
</example>
|
||||
|
||||
<example>
|
||||
User: "Set some opening questions"
|
||||
Action: Use updateGroup with { config: { openingQuestions: ["What project are you working on?", "How can we help you today?", "Do you have any specific questions?"] } }
|
||||
</example>
|
||||
<example title="Query Information">
|
||||
User: "What agents are in this group?" / "What can the supervisor do?"
|
||||
Action: Reference the injected \`<current_group_context>\` directly (group_members, supervisor_agent, etc.)
|
||||
</example>
|
||||
</examples>
|
||||
|
||||
<response_format>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { ActionIcon, Flexbox, Icon, Skeleton, Tag } from '@lobehub/ui';
|
||||
import { cssVar } from 'antd-style';
|
||||
import { MessageSquareDashed, Star } from 'lucide-react';
|
||||
import { Suspense, memo, useCallback } from 'react';
|
||||
import { Suspense, memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import urlJoin from 'url-join';
|
||||
|
||||
import { isDesktop } from '@/const/version';
|
||||
import NavItem from '@/features/NavPanel/components/NavItem';
|
||||
@@ -29,6 +30,12 @@ const TopicItem = memo<TopicItemProps>(({ id, title, fav, active, threadId }) =>
|
||||
const openTopicInNewWindow = useGlobalStore((s) => s.openTopicInNewWindow);
|
||||
const activeAgentId = useAgentStore((s) => s.activeAgentId);
|
||||
|
||||
// Construct href for cmd+click support
|
||||
const href = useMemo(() => {
|
||||
if (!activeAgentId || !id) return undefined;
|
||||
return urlJoin('/chat', `?agent=${activeAgentId}&topic=${id}`);
|
||||
}, [activeAgentId, id]);
|
||||
|
||||
const [editing, isLoading] = useChatStore((s) => [
|
||||
id ? s.topicRenamingId === id : false,
|
||||
id ? s.topicLoadingIds.includes(id) : false,
|
||||
@@ -97,6 +104,7 @@ const TopicItem = memo<TopicItemProps>(({ id, title, fav, active, threadId }) =>
|
||||
active={active && !threadId && !isInAgentSubRoute}
|
||||
contextMenuItems={dropdownMenu}
|
||||
disabled={editing}
|
||||
href={href}
|
||||
icon={
|
||||
<ActionIcon
|
||||
color={fav ? cssVar.colorWarning : undefined}
|
||||
|
||||
@@ -1,20 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import { Flexbox } from '@lobehub/ui';
|
||||
import { Suspense, memo, useEffect, useMemo } from 'react';
|
||||
import { Suspense, memo, useMemo } from 'react';
|
||||
|
||||
import ChatMiniMap from '@/features/ChatMiniMap';
|
||||
import { ChatList, ConversationProvider, TodoProgress } from '@/features/Conversation';
|
||||
import ZenModeToast from '@/features/ZenModeToast';
|
||||
import { useOperationState } from '@/hooks/useOperationState';
|
||||
import { useAgentStore } from '@/store/agent';
|
||||
import { agentSelectors } from '@/store/agent/selectors';
|
||||
import { useChatStore } from '@/store/chat';
|
||||
import { topicSelectors } from '@/store/chat/selectors';
|
||||
import { messageMapKey } from '@/store/chat/utils/messageMapKey';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { settingsSelectors } from '@/store/user/selectors';
|
||||
import { useUserMemoryStore } from '@/store/userMemory';
|
||||
|
||||
import WelcomeChatItem from './AgentWelcome';
|
||||
import ChatHydration from './ChatHydration';
|
||||
@@ -33,27 +27,6 @@ import { useAgentContext } from './useAgentContext';
|
||||
const Conversation = memo(() => {
|
||||
const context = useAgentContext();
|
||||
|
||||
const [useFetchUserMemory, setActiveMemoryContext] = useUserMemoryStore((s) => [
|
||||
s.useFetchUserMemory,
|
||||
s.setActiveMemoryContext,
|
||||
]);
|
||||
const [currentAgentMeta, activeTopic] = [
|
||||
useAgentStore(agentSelectors.currentAgentMeta),
|
||||
useChatStore(topicSelectors.currentActiveTopic),
|
||||
];
|
||||
const enableUserMemories = useUserStore(settingsSelectors.memoryEnabled);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enableUserMemories) {
|
||||
setActiveMemoryContext(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
setActiveMemoryContext({ agent: currentAgentMeta, topic: activeTopic });
|
||||
}, [activeTopic, currentAgentMeta, enableUserMemories, setActiveMemoryContext]);
|
||||
|
||||
useFetchUserMemory(Boolean(enableUserMemories && context.agentId));
|
||||
|
||||
// Get raw dbMessages from ChatStore for this context
|
||||
// ConversationStore will parse them internally to generate displayMessages
|
||||
const chatKey = useMemo(
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { Center, Empty, Markdown } from '@lobehub/ui';
|
||||
import { FileText } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { memo } from 'react';
|
||||
import { type ReactNode, memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { H1, H2, H3, H4, H5 } from './Toc/Heading';
|
||||
@@ -26,7 +26,7 @@ const MarkdownRender = memo<{ children?: string }>(({ children }) => {
|
||||
<Markdown
|
||||
allowHtml
|
||||
components={{
|
||||
a: ({ href, ...rest }) => {
|
||||
a: ({ href, ...rest }: { children?: ReactNode; href?: string }) => {
|
||||
if (href && href.startsWith('http'))
|
||||
return <Link {...rest} href={href} target={'_blank'} />;
|
||||
return rest?.children;
|
||||
@@ -36,7 +36,7 @@ const MarkdownRender = memo<{ children?: string }>(({ children }) => {
|
||||
h3: H3,
|
||||
h4: H4,
|
||||
h5: H5,
|
||||
img: ({ src, ...rest }) => {
|
||||
img: ({ src, ...rest }: { alt?: string; src?: string | Blob }) => {
|
||||
// FIXME ignore experimental blob image prop passing
|
||||
if (typeof src !== 'string') return null;
|
||||
if (src.includes('glama.ai')) return null;
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { DEFAULT_AVATAR } from '@/const/meta';
|
||||
import NavItem from '@/features/NavPanel/components/NavItem';
|
||||
import UserAvatar from '@/features/User/UserAvatar';
|
||||
import { useQueryRoute } from '@/hooks/useQueryRoute';
|
||||
import { useAgentGroupStore } from '@/store/agentGroup';
|
||||
import { agentGroupSelectors } from '@/store/agentGroup/selectors';
|
||||
import { useChatStore } from '@/store/chat';
|
||||
@@ -30,6 +31,7 @@ interface GroupMemberProps {
|
||||
*/
|
||||
const GroupMember = memo<GroupMemberProps>(({ addModalOpen, onAddModalOpenChange, groupId }) => {
|
||||
const { t } = useTranslation('chat');
|
||||
const router = useQueryRoute();
|
||||
const [nickname, username] = useUserStore((s) => [
|
||||
userProfileSelectors.nickName(s),
|
||||
userProfileSelectors.username(s),
|
||||
@@ -80,6 +82,11 @@ const GroupMember = memo<GroupMemberProps>(({ addModalOpen, onAddModalOpenChange
|
||||
pushPortalView({ agentId, type: PortalViewType.GroupThread });
|
||||
};
|
||||
|
||||
const handleMemberDoubleClick = (agentId: string) => {
|
||||
if (!groupId) return;
|
||||
router.push(`/group/${groupId}/profile`, { query: { tab: agentId }, replace: true });
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flexbox gap={2}>
|
||||
@@ -93,7 +100,7 @@ const GroupMember = memo<GroupMemberProps>(({ addModalOpen, onAddModalOpenChange
|
||||
key={item.id}
|
||||
onChat={() => handleMemberClick(item.id)}
|
||||
>
|
||||
<div>
|
||||
<div onDoubleClick={() => handleMemberDoubleClick(item.id)}>
|
||||
<GroupMemberItem
|
||||
actions={
|
||||
<ActionIcon
|
||||
|
||||
@@ -3,15 +3,10 @@
|
||||
import { Block } from '@lobehub/ui';
|
||||
import { memo } from 'react';
|
||||
|
||||
import GroupAvatar from '@/features/GroupAvatar';
|
||||
import SupervisorAvatar from '@/app/[variants]/(main)/group/features/GroupAvatar';
|
||||
import { useOpenChatSettings } from '@/hooks/useInterceptingRoutes';
|
||||
import { useAgentGroupStore } from '@/store/agentGroup';
|
||||
import { agentGroupSelectors } from '@/store/agentGroup/selectors';
|
||||
|
||||
const HeaderAvatar = memo<{ size?: number }>(() => {
|
||||
const currentGroup = useAgentGroupStore(agentGroupSelectors.currentGroup);
|
||||
const agents = currentGroup?.agents || [];
|
||||
|
||||
const openChatSettings = useOpenChatSettings();
|
||||
|
||||
return (
|
||||
@@ -30,13 +25,7 @@ const HeaderAvatar = memo<{ size?: number }>(() => {
|
||||
variant={'borderless'}
|
||||
width={32}
|
||||
>
|
||||
<GroupAvatar
|
||||
avatars={agents.map((agent) => ({
|
||||
avatar: agent.avatar,
|
||||
background: agent.backgroundColor || undefined,
|
||||
}))}
|
||||
size={28}
|
||||
/>
|
||||
<SupervisorAvatar size={28} />
|
||||
</Block>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ChevronsUpDownIcon } from 'lucide-react';
|
||||
import React, { type PropsWithChildren, memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import GroupAvatar from '@/features/GroupAvatar';
|
||||
import SupervisorAvatar from '@/app/[variants]/(main)/group/features/GroupAvatar';
|
||||
import { SkeletonItem } from '@/features/NavPanel/components/SkeletonList';
|
||||
import { useAgentGroupStore } from '@/store/agentGroup';
|
||||
import { agentGroupSelectors } from '@/store/agentGroup/selectors';
|
||||
@@ -15,10 +15,9 @@ import SwitchPanel from './SwitchPanel';
|
||||
const Agent = memo<PropsWithChildren>(() => {
|
||||
const { t } = useTranslation(['chat', 'common']);
|
||||
|
||||
const [isGroupsInit, groupMeta, memberAvatars] = useAgentGroupStore((s) => [
|
||||
const [isGroupsInit, groupMeta] = useAgentGroupStore((s) => [
|
||||
agentGroupSelectors.isGroupsInit(s),
|
||||
agentGroupSelectors.currentGroupMeta(s),
|
||||
agentGroupSelectors.currentGroupMemberAvatars(s),
|
||||
]);
|
||||
|
||||
const displayTitle = groupMeta?.title || t('untitledGroup', { ns: 'chat' });
|
||||
@@ -39,7 +38,7 @@ const Agent = memo<PropsWithChildren>(() => {
|
||||
}}
|
||||
variant={'borderless'}
|
||||
>
|
||||
<GroupAvatar avatars={memberAvatars} size={28} />
|
||||
<SupervisorAvatar size={28} />
|
||||
<Text ellipsis weight={500}>
|
||||
{displayTitle}
|
||||
</Text>
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import { ActionIcon, Flexbox, Icon, Skeleton, Tag } from '@lobehub/ui';
|
||||
import { cssVar } from 'antd-style';
|
||||
import { MessageSquareDashed, Star } from 'lucide-react';
|
||||
import { Suspense, memo, useCallback } from 'react';
|
||||
import { Suspense, memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import urlJoin from 'url-join';
|
||||
|
||||
import { isDesktop } from '@/const/version';
|
||||
import NavItem from '@/features/NavPanel/components/NavItem';
|
||||
import { useAgentStore } from '@/store/agent';
|
||||
import { useAgentGroupStore } from '@/store/agentGroup';
|
||||
import { useChatStore } from '@/store/chat';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
|
||||
import ThreadList from '../../TopicListContent/ThreadList';
|
||||
import { useTopicNavigation } from '../../hooks/useTopicNavigation';
|
||||
import Actions from './Actions';
|
||||
import Editing from './Editing';
|
||||
import { useTopicItemDropdownMenu } from './useDropdownMenu';
|
||||
@@ -27,7 +28,15 @@ interface TopicItemProps {
|
||||
const TopicItem = memo<TopicItemProps>(({ id, title, fav, active, threadId }) => {
|
||||
const { t } = useTranslation('topic');
|
||||
const openTopicInNewWindow = useGlobalStore((s) => s.openTopicInNewWindow);
|
||||
const toggleMobileTopic = useGlobalStore((s) => s.toggleMobileTopic);
|
||||
const activeAgentId = useAgentStore((s) => s.activeAgentId);
|
||||
const [activeGroupId, switchTopic] = useAgentGroupStore((s) => [s.activeGroupId, s.switchTopic]);
|
||||
|
||||
// Construct href for cmd+click support
|
||||
const href = useMemo(() => {
|
||||
if (!activeGroupId || !id) return undefined;
|
||||
return urlJoin('/group', activeGroupId, `?topic=${id}`);
|
||||
}, [activeGroupId, id]);
|
||||
|
||||
const [editing, isLoading] = useChatStore((s) => [
|
||||
id ? s.topicRenamingId === id : false,
|
||||
@@ -36,8 +45,6 @@ const TopicItem = memo<TopicItemProps>(({ id, title, fav, active, threadId }) =>
|
||||
|
||||
const [favoriteTopic] = useChatStore((s) => [s.favoriteTopic]);
|
||||
|
||||
const { navigateToTopic, isInAgentSubRoute } = useTopicNavigation();
|
||||
|
||||
const toggleEditing = useCallback(
|
||||
(visible?: boolean) => {
|
||||
useChatStore.setState({ topicRenamingId: visible && id ? id : '' });
|
||||
@@ -47,8 +54,9 @@ const TopicItem = memo<TopicItemProps>(({ id, title, fav, active, threadId }) =>
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
if (editing) return;
|
||||
navigateToTopic(id);
|
||||
}, [editing, id, navigateToTopic]);
|
||||
switchTopic(id);
|
||||
toggleMobileTopic(false);
|
||||
}, [editing, id, switchTopic, toggleMobileTopic]);
|
||||
|
||||
const handleDoubleClick = useCallback(() => {
|
||||
if (!id || !activeAgentId) return;
|
||||
@@ -66,7 +74,7 @@ const TopicItem = memo<TopicItemProps>(({ id, title, fav, active, threadId }) =>
|
||||
if (!id) {
|
||||
return (
|
||||
<NavItem
|
||||
active={active && !isInAgentSubRoute}
|
||||
active={active}
|
||||
icon={
|
||||
<Icon color={cssVar.colorTextDescription} icon={MessageSquareDashed} size={'small'} />
|
||||
}
|
||||
@@ -94,9 +102,10 @@ const TopicItem = memo<TopicItemProps>(({ id, title, fav, active, threadId }) =>
|
||||
<Flexbox style={{ position: 'relative' }}>
|
||||
<NavItem
|
||||
actions={<Actions dropdownMenu={dropdownMenu} />}
|
||||
active={active && !threadId && !isInAgentSubRoute}
|
||||
active={active && !threadId}
|
||||
contextMenuItems={dropdownMenu}
|
||||
disabled={editing}
|
||||
href={!editing ? href : undefined}
|
||||
icon={
|
||||
<ActionIcon
|
||||
color={fav ? cssVar.colorWarning : undefined}
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { useCallback } from 'react';
|
||||
import urlJoin from 'url-join';
|
||||
|
||||
import { useQueryRoute } from '@/hooks/useQueryRoute';
|
||||
import { useAgentGroupStore } from '@/store/agentGroup';
|
||||
import { useChatStore } from '@/store/chat';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
|
||||
/**
|
||||
* Hook to handle topic navigation with automatic route detection
|
||||
* If in agent sub-route (e.g., /agent/:aid/profile), navigate back to chat first
|
||||
*/
|
||||
export const useTopicNavigation = () => {
|
||||
const pathname = usePathname();
|
||||
const activeGroupId = useAgentGroupStore((s) => s.activeGroupId);
|
||||
const router = useQueryRoute();
|
||||
const toggleConfig = useGlobalStore((s) => s.toggleMobileTopic);
|
||||
const switchTopic = useChatStore((s) => s.switchTopic);
|
||||
|
||||
const isInAgentSubRoute = useCallback(() => {
|
||||
if (!activeGroupId) return false;
|
||||
const agentBasePath = `/group/${activeGroupId}`;
|
||||
// If pathname has more segments after /agent/:aid, it's a sub-route
|
||||
return (
|
||||
pathname.startsWith(agentBasePath) &&
|
||||
pathname !== agentBasePath &&
|
||||
pathname !== `${agentBasePath}/`
|
||||
);
|
||||
}, [pathname, activeGroupId]);
|
||||
|
||||
const navigateToTopic = useCallback(
|
||||
(topicId?: string) => {
|
||||
// If in agent sub-route, navigate back to agent chat first
|
||||
if (isInAgentSubRoute() && activeGroupId) {
|
||||
router.push(urlJoin('/group', activeGroupId as string));
|
||||
}
|
||||
|
||||
switchTopic(topicId);
|
||||
toggleConfig(false);
|
||||
},
|
||||
[activeGroupId, router, switchTopic, toggleConfig, isInAgentSubRoute],
|
||||
);
|
||||
|
||||
return {
|
||||
isInAgentSubRoute: isInAgentSubRoute(),
|
||||
navigateToTopic,
|
||||
};
|
||||
};
|
||||
@@ -1,20 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import { Flexbox } from '@lobehub/ui';
|
||||
import { Suspense, memo, useEffect, useMemo } from 'react';
|
||||
import { Suspense, memo, useMemo } from 'react';
|
||||
|
||||
import ChatMiniMap from '@/features/ChatMiniMap';
|
||||
import { ChatList, ConversationProvider } from '@/features/Conversation';
|
||||
import ZenModeToast from '@/features/ZenModeToast';
|
||||
import { useOperationState } from '@/hooks/useOperationState';
|
||||
import { useAgentStore } from '@/store/agent';
|
||||
import { agentSelectors } from '@/store/agent/selectors';
|
||||
import { useChatStore } from '@/store/chat';
|
||||
import { topicSelectors } from '@/store/chat/selectors';
|
||||
import { messageMapKey } from '@/store/chat/utils/messageMapKey';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { settingsSelectors } from '@/store/user/selectors';
|
||||
import { useUserMemoryStore } from '@/store/userMemory';
|
||||
|
||||
import WelcomeChatItem from './AgentWelcome';
|
||||
import ChatHydration from './ChatHydration';
|
||||
@@ -39,28 +33,6 @@ interface ConversationAreaProps {
|
||||
const Conversation = memo<ConversationAreaProps>(({ mobile = false }) => {
|
||||
const context = useGroupContext();
|
||||
|
||||
const [useFetchUserMemory, setActiveMemoryContext] = useUserMemoryStore((s) => [
|
||||
s.useFetchUserMemory,
|
||||
s.setActiveMemoryContext,
|
||||
]);
|
||||
const [currentAgentMeta, activeTopic] = [
|
||||
useAgentStore(agentSelectors.currentAgentMeta),
|
||||
useChatStore(topicSelectors.currentActiveTopic),
|
||||
];
|
||||
|
||||
const enableUserMemories = useUserStore(settingsSelectors.memoryEnabled);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enableUserMemories) {
|
||||
setActiveMemoryContext(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
setActiveMemoryContext({ agent: currentAgentMeta, topic: activeTopic });
|
||||
}, [activeTopic, currentAgentMeta, enableUserMemories, setActiveMemoryContext]);
|
||||
|
||||
useFetchUserMemory(Boolean(enableUserMemories && context.agentId));
|
||||
|
||||
// Get raw dbMessages from ChatStore for this context
|
||||
// ConversationStore will parse them internally to generate displayMessages
|
||||
const chatKey = useMemo(
|
||||
|
||||
@@ -11,8 +11,6 @@ import { DESKTOP_HEADER_ICON_SIZE, MOBILE_HEADER_ICON_SIZE } from '@/const/layou
|
||||
import { useWorkspaceModal } from '@/hooks/useWorkspaceModal';
|
||||
import { useChatStore } from '@/store/chat';
|
||||
|
||||
console.log('ENABLE_TOPIC_LINK_SHARE', ENABLE_TOPIC_LINK_SHARE);
|
||||
|
||||
const ShareModal = dynamic(() => import('@/features/ShareModal'));
|
||||
const SharePopover = dynamic(() => import('@/features/SharePopover'));
|
||||
|
||||
|
||||
@@ -1,19 +1,27 @@
|
||||
'use client';
|
||||
|
||||
import isEqual from 'fast-deep-equal';
|
||||
import React, { memo } from 'react';
|
||||
import { memo } from 'react';
|
||||
|
||||
import GroupAvatar from '@/features/GroupAvatar';
|
||||
import AgentGroupAvatar from '@/features/AgentGroupAvatar';
|
||||
import { useAgentGroupStore } from '@/store/agentGroup';
|
||||
import { agentGroupSelectors } from '@/store/agentGroup/selectors';
|
||||
|
||||
const SupervisorAvatar = memo<{ size?: number }>(({ size = 28 }) => {
|
||||
const memberAvatars = useAgentGroupStore(
|
||||
(s) => agentGroupSelectors.currentGroupMemberAvatars(s),
|
||||
isEqual,
|
||||
);
|
||||
/**
|
||||
* Connected AgentGroupAvatar that reads from agentGroup store
|
||||
*/
|
||||
const CurrentAgentGroupAvatar = memo<{ size?: number }>(({ size = 28 }) => {
|
||||
const groupMeta = useAgentGroupStore(agentGroupSelectors.currentGroupMeta, isEqual);
|
||||
const memberAvatars = useAgentGroupStore(agentGroupSelectors.currentGroupMemberAvatars, isEqual);
|
||||
|
||||
return <GroupAvatar avatars={memberAvatars} size={size} />;
|
||||
return (
|
||||
<AgentGroupAvatar
|
||||
avatar={groupMeta.avatar}
|
||||
backgroundColor={groupMeta.backgroundColor}
|
||||
memberAvatars={memberAvatars}
|
||||
size={size}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export default SupervisorAvatar;
|
||||
export default CurrentAgentGroupAvatar;
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { DESKTOP_HEADER_ICON_SIZE } from '@/const/layoutTokens';
|
||||
import NavHeader from '@/features/NavHeader';
|
||||
import { useQueryState } from '@/hooks/useQueryParam';
|
||||
import { useChatStore } from '@/store/chat';
|
||||
import { topicSelectors } from '@/store/chat/slices/topic/selectors';
|
||||
|
||||
@@ -18,16 +19,18 @@ const TopicSelector = memo<TopicSelectorProps>(({ agentId }) => {
|
||||
// Fetch topics for the group agent builder
|
||||
useChatStore((s) => s.useFetchTopics)(true, { agentId });
|
||||
|
||||
// Use activeTopicId from chatStore (synced with URL query 'bt' via ProfileHydration)
|
||||
const [activeTopicId, switchTopic] = useChatStore((s) => [s.activeTopicId, s.switchTopic]);
|
||||
// Use activeTopicId from chatStore (synced from URL query 'bt' via ProfileHydration)
|
||||
const activeTopicId = useChatStore((s) => s.activeTopicId);
|
||||
const topics = useChatStore((s) => topicSelectors.getTopicsByAgentId(agentId)(s));
|
||||
|
||||
// Switch topic - ProfileHydration handles URL sync automatically
|
||||
// Directly update URL query 'bt' to switch topic in profile page
|
||||
const [, setBuilderTopicId] = useQueryState('bt');
|
||||
|
||||
const handleSwitchTopic = useCallback(
|
||||
(topicId?: string) => {
|
||||
switchTopic(topicId);
|
||||
setBuilderTopicId(topicId ?? null);
|
||||
},
|
||||
[switchTopic],
|
||||
[setBuilderTopicId],
|
||||
);
|
||||
|
||||
// Find active topic from the agent's topics list directly
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { Avatar, Flexbox } from '@lobehub/ui';
|
||||
import { createStaticStyles, cx } from 'antd-style';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { ReactNode, memo } from 'react';
|
||||
import { ReactNode, memo, useEffect, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const styles = createStaticStyles(({ css, cssVar: cv }) => ({
|
||||
@@ -109,15 +109,33 @@ interface ChromeTabsProps {
|
||||
|
||||
const ChromeTabs = memo<ChromeTabsProps>(({ items, activeId, onChange, onAdd }) => {
|
||||
const { t } = useTranslation('chat');
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current || !activeId) return;
|
||||
|
||||
const activeTab = containerRef.current.querySelector(`[data-tab-id="${activeId}"]`);
|
||||
if (!activeTab) return;
|
||||
|
||||
const containerRect = containerRef.current.getBoundingClientRect();
|
||||
const tabRect = activeTab.getBoundingClientRect();
|
||||
|
||||
const isVisible = tabRect.left >= containerRect.left && tabRect.right <= containerRect.right;
|
||||
|
||||
if (!isVisible) {
|
||||
activeTab.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' });
|
||||
}
|
||||
}, [activeId]);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.container} ref={containerRef}>
|
||||
{items.map((item) => {
|
||||
const isActive = item.id === activeId;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(styles.tab, isActive && styles.tabActive)}
|
||||
data-tab-id={item.id}
|
||||
key={item.id}
|
||||
onClick={() => onChange(item.id)}
|
||||
>
|
||||
|
||||
@@ -5,12 +5,14 @@ import { useGroupProfileStore } from '@/store/groupProfile';
|
||||
|
||||
/**
|
||||
* AgentTool for group profile editor
|
||||
* - Uses default settings (no web browsing, no filterAvailableInWeb, uses metaList)
|
||||
* - showWebBrowsing: Group member profile supports web browsing toggle
|
||||
* - filterAvailableInWeb: Filter out desktop-only tools in web version
|
||||
* - useAllMetaList: Use allMetaList to include hidden tools
|
||||
* - Passes agentId from group profile store to display the correct member's plugins
|
||||
*/
|
||||
const AgentTool = () => {
|
||||
const agentId = useGroupProfileStore((s) => s.activeTabId);
|
||||
return <SharedAgentTool agentId={agentId} />;
|
||||
return <SharedAgentTool agentId={agentId} filterAvailableInWeb showWebBrowsing useAllMetaList />;
|
||||
};
|
||||
|
||||
export default AgentTool;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useEditor, useEditorState } from '@lobehub/editor/react';
|
||||
import { useUnmount } from 'ahooks';
|
||||
import { memo, useEffect, useRef } from 'react';
|
||||
import { memo, useEffect } from 'react';
|
||||
import { createStoreUpdater } from 'zustand-utils';
|
||||
|
||||
import { useRegisterFilesHotkeys, useSaveDocumentHotkey } from '@/hooks/useHotkeys';
|
||||
@@ -25,34 +25,15 @@ const ProfileHydration = memo(() => {
|
||||
const [activeTabId] = useQueryState('tab', parseAsString.withDefault('group'));
|
||||
storeUpdater('activeTabId', activeTabId);
|
||||
|
||||
// Bidirectional sync between URL query 'bt' and chatStore.activeTopicId
|
||||
const [builderTopicId, setBuilderTopicId] = useQueryState('bt');
|
||||
const activeTopicId = useChatStore((s) => s.activeTopicId);
|
||||
// Sync URL query 'bt' → chatStore.activeTopicId (one-way only)
|
||||
// Store → URL sync is handled directly by TopicSelector using setBuilderTopicId
|
||||
const [builderTopicId] = useQueryState('bt');
|
||||
|
||||
// Track if the change came from URL to prevent sync loops
|
||||
const isUrlChangeRef = useRef(false);
|
||||
|
||||
// Sync URL → Store (when URL changes)
|
||||
useEffect(() => {
|
||||
const urlTopicId = builderTopicId ?? undefined;
|
||||
if (urlTopicId !== activeTopicId) {
|
||||
isUrlChangeRef.current = true;
|
||||
useChatStore.setState({ activeTopicId: urlTopicId });
|
||||
}
|
||||
useChatStore.setState({ activeTopicId: urlTopicId });
|
||||
}, [builderTopicId]);
|
||||
|
||||
// Sync Store → URL (when store changes, but not from URL)
|
||||
useEffect(() => {
|
||||
if (isUrlChangeRef.current) {
|
||||
isUrlChangeRef.current = false;
|
||||
return;
|
||||
}
|
||||
const urlTopicId = builderTopicId ?? undefined;
|
||||
if (activeTopicId !== urlTopicId) {
|
||||
setBuilderTopicId(activeTopicId ?? null);
|
||||
}
|
||||
}, [activeTopicId]);
|
||||
|
||||
// Register hotkeys
|
||||
useRegisterFilesHotkeys();
|
||||
useSaveDocumentHotkey(flushSave);
|
||||
@@ -65,7 +46,6 @@ const ProfileHydration = memo(() => {
|
||||
editorState: undefined,
|
||||
saveStateMap: {},
|
||||
});
|
||||
useChatStore.setState({ activeTopicId: undefined });
|
||||
});
|
||||
|
||||
return null;
|
||||
|
||||
38
src/features/AgentGroupAvatar/index.tsx
Normal file
38
src/features/AgentGroupAvatar/index.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
'use client';
|
||||
|
||||
import { Avatar } from '@lobehub/ui';
|
||||
import { memo } from 'react';
|
||||
|
||||
import GroupAvatar from '@/features/GroupAvatar';
|
||||
|
||||
export interface AgentGroupAvatarProps {
|
||||
/**
|
||||
* Custom avatar for the group (emoji or url)
|
||||
*/
|
||||
avatar?: string;
|
||||
/**
|
||||
* Background color for custom avatar
|
||||
*/
|
||||
backgroundColor?: string;
|
||||
/**
|
||||
* Member avatars to display when no custom avatar
|
||||
*/
|
||||
memberAvatars?: { avatar?: string; background?: string }[];
|
||||
/**
|
||||
* Avatar size
|
||||
*/
|
||||
size?: number;
|
||||
}
|
||||
|
||||
const AgentGroupAvatar = memo<AgentGroupAvatarProps>(
|
||||
({ avatar, backgroundColor, memberAvatars = [], size = 28 }) => {
|
||||
// If group has custom avatar, show it; otherwise show member avatars composition
|
||||
if (avatar) {
|
||||
return <Avatar avatar={avatar} background={backgroundColor} shape="square" size={size} />;
|
||||
}
|
||||
|
||||
return <GroupAvatar avatars={memberAvatars} size={size} />;
|
||||
},
|
||||
);
|
||||
|
||||
export default AgentGroupAvatar;
|
||||
@@ -6,9 +6,9 @@ import { type MouseEventHandler, memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { MESSAGE_ACTION_BAR_PORTAL_ATTRIBUTES } from '@/const/messageActionPortal';
|
||||
import AgentGroupAvatar from '@/features/AgentGroupAvatar';
|
||||
import { ChatItem } from '@/features/Conversation/ChatItem';
|
||||
import { useNewScreen } from '@/features/Conversation/Messages/components/useNewScreen';
|
||||
import GroupAvatar from '@/features/GroupAvatar';
|
||||
import { useAgentGroupStore } from '@/store/agentGroup';
|
||||
import { agentGroupSelectors } from '@/store/agentGroup/selectors';
|
||||
|
||||
@@ -90,7 +90,13 @@ const GroupMessage = memo<GroupMessageProps>(({ id, index, disableEditing, isLat
|
||||
</>
|
||||
}
|
||||
avatar={{ ...avatar, title: groupMeta.title }}
|
||||
customAvatarRender={() => <GroupAvatar avatars={memberAvatars} />}
|
||||
customAvatarRender={() => (
|
||||
<AgentGroupAvatar
|
||||
avatar={groupMeta.avatar}
|
||||
backgroundColor={groupMeta.backgroundColor}
|
||||
memberAvatars={memberAvatars}
|
||||
/>
|
||||
)}
|
||||
newScreen={newScreen}
|
||||
onMouseEnter={onMouseEnter}
|
||||
placement={'left'}
|
||||
|
||||
@@ -30,12 +30,11 @@ export const useMarkdown = (id: string): Partial<MarkdownProps> => {
|
||||
() =>
|
||||
({
|
||||
components: Object.fromEntries(
|
||||
// @ts-expect-error
|
||||
markdownElements.map((element) => {
|
||||
const Component = element.Component;
|
||||
return [element.tag, (props: any) => <Component {...props} id={id} />];
|
||||
}),
|
||||
),
|
||||
) as any,
|
||||
customRender: (dom: ReactNode, { text }: { text: string }) => {
|
||||
if (text.length > 30_000) return <ContentPreview content={text} id={id} />;
|
||||
return dom;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ActionIcon, Block, Center, Text } from '@lobehub/ui';
|
||||
import { Block, Center, Icon, Text } from '@lobehub/ui';
|
||||
import { PlusIcon } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
|
||||
@@ -22,7 +22,7 @@ const EmptyNavItem = memo<EmptyStatusProps>(({ title, onClick, className }) => {
|
||||
variant={'borderless'}
|
||||
>
|
||||
<Center flex={'none'} height={28} width={28}>
|
||||
<ActionIcon icon={PlusIcon} size={'small'} />
|
||||
<Icon icon={PlusIcon} size={'small'} />
|
||||
</Center>
|
||||
<Text align={'center'} type={'secondary'}>
|
||||
{title}
|
||||
|
||||
@@ -12,9 +12,10 @@ import {
|
||||
Text,
|
||||
} from '@lobehub/ui';
|
||||
import { createStaticStyles, cssVar, cx } from 'antd-style';
|
||||
import { Loader2Icon } from 'lucide-react';
|
||||
import { type ReactNode, memo } from 'react';
|
||||
|
||||
import NeuralNetworkLoading from '@/components/NeuralNetworkLoading';
|
||||
|
||||
const ACTION_CLASS_NAME = 'nav-item-actions';
|
||||
|
||||
const styles = createStaticStyles(({ css }) => ({
|
||||
@@ -50,6 +51,10 @@ export interface NavItemProps extends Omit<BlockProps, 'children' | 'title'> {
|
||||
contextMenuItems?: GenericItemType[] | (() => GenericItemType[]);
|
||||
disabled?: boolean;
|
||||
extra?: ReactNode;
|
||||
/**
|
||||
* Optional href for cmd+click to open in new tab
|
||||
*/
|
||||
href?: string;
|
||||
icon?: IconProps['icon'];
|
||||
loading?: boolean;
|
||||
title: ReactNode;
|
||||
@@ -61,6 +66,7 @@ const NavItem = memo<NavItemProps>(
|
||||
actions,
|
||||
contextMenuItems,
|
||||
active,
|
||||
href,
|
||||
icon,
|
||||
title,
|
||||
onClick,
|
||||
@@ -72,7 +78,15 @@ const NavItem = memo<NavItemProps>(
|
||||
const iconColor = active ? cssVar.colorText : cssVar.colorTextDescription;
|
||||
const textColor = active ? cssVar.colorText : cssVar.colorTextSecondary;
|
||||
const variant = active ? 'filled' : 'borderless';
|
||||
const iconComponent = loading ? Loader2Icon : icon;
|
||||
|
||||
// Link props for cmd+click support
|
||||
const linkProps = href
|
||||
? {
|
||||
as: 'a' as const,
|
||||
href,
|
||||
style: { color: 'inherit', textDecoration: 'none' },
|
||||
}
|
||||
: {};
|
||||
|
||||
const Content = (
|
||||
<Block
|
||||
@@ -84,15 +98,25 @@ const NavItem = memo<NavItemProps>(
|
||||
horizontal
|
||||
onClick={(e) => {
|
||||
if (disabled || loading) return;
|
||||
// Prevent default link behavior for normal clicks (let onClick handle it)
|
||||
// But allow cmd+click to open in new tab
|
||||
if (href && !e.metaKey && !e.ctrlKey) {
|
||||
e.preventDefault();
|
||||
}
|
||||
onClick?.(e);
|
||||
}}
|
||||
paddingInline={4}
|
||||
variant={variant}
|
||||
{...linkProps}
|
||||
{...rest}
|
||||
>
|
||||
{icon && (
|
||||
<Center flex={'none'} height={28} width={28}>
|
||||
<Icon color={iconColor} icon={iconComponent} size={18} spin={loading} />
|
||||
{loading ? (
|
||||
<NeuralNetworkLoading size={18} />
|
||||
) : (
|
||||
<Icon color={iconColor} icon={icon} size={18} />
|
||||
)}
|
||||
</Center>
|
||||
)}
|
||||
|
||||
|
||||
167
src/features/ToolTag/index.tsx
Normal file
167
src/features/ToolTag/index.tsx
Normal file
@@ -0,0 +1,167 @@
|
||||
'use client';
|
||||
|
||||
import { KLAVIS_SERVER_TYPES, type KlavisServerType } from '@lobechat/const';
|
||||
import { Avatar, Icon, Tag } from '@lobehub/ui';
|
||||
import { createStaticStyles, cssVar } from 'antd-style';
|
||||
import isEqual from 'fast-deep-equal';
|
||||
import { memo, useMemo } from 'react';
|
||||
|
||||
import PluginAvatar from '@/components/Plugins/PluginAvatar';
|
||||
import { useIsDark } from '@/hooks/useIsDark';
|
||||
import { useDiscoverStore } from '@/store/discover';
|
||||
import { serverConfigSelectors, useServerConfigStore } from '@/store/serverConfig';
|
||||
import { useToolStore } from '@/store/tool';
|
||||
import {
|
||||
builtinToolSelectors,
|
||||
klavisStoreSelectors,
|
||||
pluginSelectors,
|
||||
} from '@/store/tool/selectors';
|
||||
|
||||
/**
|
||||
* Klavis server icon component
|
||||
*/
|
||||
const KlavisIcon = memo<Pick<KlavisServerType, 'icon' | 'label'>>(({ icon, label }) => {
|
||||
if (typeof icon === 'string') {
|
||||
return <img alt={label} height={16} src={icon} style={{ flexShrink: 0 }} width={16} />;
|
||||
}
|
||||
|
||||
return <Icon fill={cssVar.colorText} icon={icon} size={16} />;
|
||||
});
|
||||
|
||||
const styles = createStaticStyles(({ css, cssVar }) => ({
|
||||
compact: css`
|
||||
height: auto !important;
|
||||
padding: 0 !important;
|
||||
border: none !important;
|
||||
background: transparent !important;
|
||||
`,
|
||||
tag: css`
|
||||
height: 24px !important;
|
||||
border-radius: ${cssVar.borderRadiusSM} !important;
|
||||
`,
|
||||
}));
|
||||
|
||||
export interface ToolTagProps {
|
||||
/**
|
||||
* The tool identifier to display
|
||||
*/
|
||||
identifier: string;
|
||||
/**
|
||||
* Variant style of the tag
|
||||
* - 'default': normal tag with background and border
|
||||
* - 'compact': no padding, no background, no border (text only with icon)
|
||||
* @default 'default'
|
||||
*/
|
||||
variant?: 'compact' | 'default';
|
||||
}
|
||||
|
||||
/**
|
||||
* A readonly tag component that displays tool information based on identifier.
|
||||
* Unlike PluginTag, this component is not closable and is designed for display-only purposes.
|
||||
*/
|
||||
const ToolTag = memo<ToolTagProps>(({ identifier, variant = 'default' }) => {
|
||||
const isDarkMode = useIsDark();
|
||||
const isCompact = variant === 'compact';
|
||||
|
||||
// Get local plugin lists
|
||||
const builtinList = useToolStore(builtinToolSelectors.metaList, isEqual);
|
||||
const installedPluginList = useToolStore(pluginSelectors.installedPluginMetaList, isEqual);
|
||||
|
||||
// Klavis related state
|
||||
const allKlavisServers = useToolStore(klavisStoreSelectors.getServers, isEqual);
|
||||
const isKlavisEnabledInEnv = useServerConfigStore(serverConfigSelectors.enableKlavis);
|
||||
|
||||
// Check if plugin is installed
|
||||
const isInstalled = useToolStore(pluginSelectors.isPluginInstalled(identifier));
|
||||
|
||||
// Try to find in local lists first (including Klavis)
|
||||
const localMeta = useMemo(() => {
|
||||
// Check if it's a Klavis server type
|
||||
if (isKlavisEnabledInEnv) {
|
||||
const klavisType = KLAVIS_SERVER_TYPES.find((type) => type.identifier === identifier);
|
||||
if (klavisType) {
|
||||
const connectedServer = allKlavisServers.find((s) => s.identifier === identifier);
|
||||
return {
|
||||
icon: klavisType.icon,
|
||||
isInstalled: !!connectedServer,
|
||||
label: klavisType.label,
|
||||
title: klavisType.label,
|
||||
type: 'klavis' as const,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const builtinMeta = builtinList.find((p) => p.identifier === identifier);
|
||||
if (builtinMeta) {
|
||||
return {
|
||||
avatar: builtinMeta.meta.avatar,
|
||||
isInstalled: true,
|
||||
title: builtinMeta.meta.title,
|
||||
type: 'builtin' as const,
|
||||
};
|
||||
}
|
||||
|
||||
const installedMeta = installedPluginList.find((p) => p.identifier === identifier);
|
||||
if (installedMeta) {
|
||||
return {
|
||||
avatar: installedMeta.avatar,
|
||||
isInstalled: true,
|
||||
title: installedMeta.title,
|
||||
type: 'plugin' as const,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [identifier, builtinList, installedPluginList, isKlavisEnabledInEnv, allKlavisServers]);
|
||||
|
||||
// Fetch from remote if not found locally
|
||||
const usePluginDetail = useDiscoverStore((s) => s.usePluginDetail);
|
||||
const { data: remoteData, isLoading } = usePluginDetail({
|
||||
identifier: !localMeta && !isInstalled ? identifier : undefined,
|
||||
withManifest: false,
|
||||
});
|
||||
|
||||
// Determine final metadata
|
||||
const meta = localMeta || {
|
||||
avatar: remoteData?.avatar,
|
||||
isInstalled: false,
|
||||
title: remoteData?.title || identifier,
|
||||
type: 'plugin' as const,
|
||||
};
|
||||
|
||||
const displayTitle = isLoading ? 'Loading...' : meta.title;
|
||||
|
||||
// Render icon based on type
|
||||
const renderIcon = () => {
|
||||
// Klavis type has icon property
|
||||
if (meta.type === 'klavis' && 'icon' in meta && 'label' in meta) {
|
||||
return <KlavisIcon icon={meta.icon} label={meta.label} />;
|
||||
}
|
||||
|
||||
// Builtin type has avatar
|
||||
if (meta.type === 'builtin' && 'avatar' in meta && meta.avatar) {
|
||||
return <Avatar avatar={meta.avatar} shape={'square'} size={16} style={{ flexShrink: 0 }} />;
|
||||
}
|
||||
|
||||
// Plugin type
|
||||
if ('avatar' in meta) {
|
||||
return <PluginAvatar avatar={meta.avatar} size={16} />;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<Tag
|
||||
className={isCompact ? styles.compact : styles.tag}
|
||||
icon={renderIcon()}
|
||||
variant={isCompact ? 'borderless' : isDarkMode ? 'filled' : 'outlined'}
|
||||
>
|
||||
{displayTitle}
|
||||
</Tag>
|
||||
);
|
||||
});
|
||||
|
||||
ToolTag.displayName = 'ToolTag';
|
||||
|
||||
export default ToolTag;
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
type RecentTopicGroup,
|
||||
type RecentTopicGroupMember,
|
||||
} from '@lobechat/types';
|
||||
import { cleanObject } from '@lobechat/utils';
|
||||
import { eq, inArray } from 'drizzle-orm';
|
||||
import { after } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
@@ -410,8 +411,14 @@ export const topicRouter = router({
|
||||
const agentId = topicAgentIdMap.get(topic.id);
|
||||
const agentInfo = agentId ? agentInfoMap.get(agentId) : null;
|
||||
|
||||
// Clean agent info - if avatar/title are all null, return null
|
||||
const cleanedAgent = agentInfo ? cleanObject(agentInfo) : null;
|
||||
// Only return agent if it has meaningful display info (avatar or title)
|
||||
const validAgent =
|
||||
cleanedAgent && (cleanedAgent.avatar || cleanedAgent.title) ? cleanedAgent : null;
|
||||
|
||||
return {
|
||||
agent: agentInfo ?? null,
|
||||
agent: validAgent,
|
||||
group: null,
|
||||
id: topic.id,
|
||||
title: topic.title,
|
||||
|
||||
@@ -448,7 +448,7 @@ describe('contextEngineering', () => {
|
||||
];
|
||||
|
||||
// Mock topic memories and global identities separately
|
||||
vi.spyOn(memoryManager, 'resolveTopicMemories').mockResolvedValue({
|
||||
vi.spyOn(memoryManager, 'resolveTopicMemories').mockReturnValue({
|
||||
contexts: [
|
||||
{
|
||||
accessedAt: new Date('2024-01-01T00:00:00.000Z'),
|
||||
|
||||
@@ -252,12 +252,11 @@ export const contextEngineering = async ({
|
||||
.map((kb) => ({ description: kb.description, id: kb.id, name: kb.name }));
|
||||
|
||||
// Resolve user memories: topic memories and global identities are independent layers
|
||||
// Both functions now read from cache only (no network requests) to avoid blocking sendMessage
|
||||
let userMemoryData;
|
||||
if (enableUserMemories) {
|
||||
const [topicMemories, globalIdentities] = await Promise.all([
|
||||
resolveTopicMemories(),
|
||||
Promise.resolve(resolveGlobalIdentities()),
|
||||
]);
|
||||
const topicMemories = resolveTopicMemories();
|
||||
const globalIdentities = resolveGlobalIdentities();
|
||||
userMemoryData = combineUserMemoryData(topicMemories, globalIdentities);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import type { UserMemoryData, UserMemoryIdentityItem } from '@lobechat/context-engine';
|
||||
import type { RetrieveMemoryResult } from '@lobechat/types';
|
||||
|
||||
import { mutate } from '@/libs/swr';
|
||||
import { userMemoryService } from '@/services/userMemory';
|
||||
import { getChatStoreState } from '@/store/chat';
|
||||
import { getUserMemoryStoreState, useUserMemoryStore } from '@/store/userMemory';
|
||||
import { getUserMemoryStoreState } from '@/store/userMemory';
|
||||
import { agentMemorySelectors, identitySelectors } from '@/store/userMemory/selectors';
|
||||
|
||||
const EMPTY_MEMORIES: RetrieveMemoryResult = {
|
||||
@@ -39,17 +37,13 @@ export interface TopicMemoryResolverContext {
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves topic-based memories (contexts, experiences, preferences)
|
||||
* Resolves topic-based memories (contexts, experiences, preferences) from cache only.
|
||||
*
|
||||
* This function handles:
|
||||
* 1. Getting the topic ID from context or active topic
|
||||
* 2. Checking if memories are already cached for the topic
|
||||
* 3. Fetching memories from the service if not cached
|
||||
* 4. Caching the fetched memories by topic ID
|
||||
* This function only reads from cache and does NOT trigger network requests.
|
||||
* Memory data is pre-loaded by SWR in ChatList via useFetchTopicMemories hook.
|
||||
* This ensures sendMessage is not blocked by memory retrieval network calls.
|
||||
*/
|
||||
export const resolveTopicMemories = async (
|
||||
ctx?: TopicMemoryResolverContext,
|
||||
): Promise<RetrieveMemoryResult> => {
|
||||
export const resolveTopicMemories = (ctx?: TopicMemoryResolverContext): RetrieveMemoryResult => {
|
||||
// Get topic ID from context or active topic
|
||||
const topicId = ctx?.topicId ?? getChatStoreState().activeTopicId;
|
||||
|
||||
@@ -60,34 +54,11 @@ export const resolveTopicMemories = async (
|
||||
|
||||
const userMemoryStoreState = getUserMemoryStoreState();
|
||||
|
||||
// Check if already have cached memories for this topic
|
||||
// Only read from cache, do not trigger network request
|
||||
// Memory data is pre-loaded by SWR in ChatList
|
||||
const cachedMemories = agentMemorySelectors.topicMemories(topicId)(userMemoryStoreState);
|
||||
|
||||
if (cachedMemories) {
|
||||
return cachedMemories;
|
||||
}
|
||||
|
||||
// Fetch memories for this topic
|
||||
try {
|
||||
const result = await userMemoryService.retrieveMemoryForTopic(topicId);
|
||||
const memories = result ?? EMPTY_MEMORIES;
|
||||
|
||||
// Cache the fetched memories by topic ID
|
||||
useUserMemoryStore.setState((state) => ({
|
||||
topicMemoriesMap: {
|
||||
...state.topicMemoriesMap,
|
||||
[topicId]: memories,
|
||||
},
|
||||
}));
|
||||
|
||||
// Also trigger SWR mutate to keep in sync
|
||||
await mutate(['useFetchMemoriesForTopic', topicId]);
|
||||
|
||||
return memories;
|
||||
} catch (error) {
|
||||
console.error('Failed to retrieve memories for topic:', error);
|
||||
return EMPTY_MEMORIES;
|
||||
}
|
||||
return cachedMemories ?? EMPTY_MEMORIES;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { ParsedQuery } from 'query-string';
|
||||
import type { ChatGroupItem } from '@/database/schemas/chatGroup';
|
||||
|
||||
export interface QueryRouter {
|
||||
push: (url: string, options?: { query?: ParsedQuery }) => void;
|
||||
push: (url: string, options?: { query?: ParsedQuery; replace?: boolean }) => void;
|
||||
}
|
||||
|
||||
export interface ChatGroupState {
|
||||
|
||||
@@ -14,10 +14,16 @@ export interface ChatGroupLifecycleAction {
|
||||
silent?: boolean,
|
||||
) => Promise<string>;
|
||||
/**
|
||||
* @deprecated Use switchTopic(undefined) instead
|
||||
* Switch to a new topic in the group
|
||||
* Clears activeTopicId and navigates to group root
|
||||
*/
|
||||
switchToNewTopic: () => void;
|
||||
/**
|
||||
* Switch to a topic in the group with proper route handling
|
||||
* @param topicId - Topic ID to switch to, or undefined/null for new topic
|
||||
*/
|
||||
switchTopic: (topicId?: string | null) => void;
|
||||
}
|
||||
|
||||
export const chatGroupLifecycleSlice: StateCreator<
|
||||
@@ -57,13 +63,20 @@ export const chatGroupLifecycleSlice: StateCreator<
|
||||
},
|
||||
|
||||
switchToNewTopic: () => {
|
||||
get().switchTopic(undefined);
|
||||
},
|
||||
|
||||
switchTopic: (topicId) => {
|
||||
const { activeGroupId, router } = get();
|
||||
if (!activeGroupId || !router) return;
|
||||
|
||||
useChatStore.setState({ activeTopicId: undefined });
|
||||
// Update chat store's activeTopicId
|
||||
useChatStore.getState().switchTopic(topicId ?? undefined);
|
||||
|
||||
// Navigate with replace to avoid stale query params
|
||||
router.push(urlJoin('/group', activeGroupId), {
|
||||
query: { bt: null, tab: null, thread: null, topic: null },
|
||||
query: { topic: topicId ?? null },
|
||||
replace: true,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user