feat(ModelSwitchPanel): add provider preference storage in By Model view (#11246)

* fix: Translation

* feat: Search settings command in any page

* feat: Add more cloud-dedicated actions

* feat: New CMDK style

* feat: New CMDK style

* fix: Commands order

* fix: Type error
This commit is contained in:
René Wang
2026-01-06 19:32:43 +08:00
committed by GitHub
parent ae053da00f
commit d778093d87
7 changed files with 264 additions and 324 deletions

View File

@@ -119,8 +119,8 @@
"cmdk.navigate": "Navigate",
"cmdk.newAgent": "Create New Agent",
"cmdk.newAgentTeam": "Create New Group",
"cmdk.newLibrary": "New Library",
"cmdk.newPage": "New Page",
"cmdk.newLibrary": "Create New Library",
"cmdk.newPage": "Create New Page",
"cmdk.newTopic": "New topic in current Agent",
"cmdk.noResults": "No results found",
"cmdk.openSettings": "Open Settings",
@@ -158,6 +158,7 @@
"cmdk.themeLight": "Light",
"cmdk.toOpen": "Open",
"cmdk.toSelect": "Select",
"cmdk.upgradePlan": "Upgrade Plan",
"confirm": "Confirm",
"contact": "Contact Us",
"copy": "Copy",

View File

@@ -29,7 +29,7 @@ export interface KnowledgeItem {
}
/**
* Knowledge Repository - combines files and documents into a unified interface
* Resources Repository - combines files and documents into a unified interface
*/
export class KnowledgeRepo {
private userId: string;

View File

@@ -6,11 +6,12 @@ import { useTranslation } from 'react-i18next';
import { useCommandMenuContext } from './CommandMenuContext';
import { CommandItem } from './components';
import { useCommandMenu } from './useCommandMenu';
import { getContextCommands } from './utils/contextCommands';
import { CONTEXT_COMMANDS, getContextCommands } from './utils/contextCommands';
const ContextCommands = memo(() => {
const { t } = useTranslation('setting');
const { t: tAuth } = useTranslation('auth');
const { t: tSubscription } = useTranslation('subscription');
const { t: tCommon } = useTranslation('common');
const { handleNavigate } = useCommandMenu();
const { menuContext, pathname } = useCommandMenuContext();
@@ -23,48 +24,107 @@ const ContextCommands = memo(() => {
const commands = getContextCommands(menuContext, subPath);
if (commands.length === 0) return null;
// Get settings commands to show globally (when not in settings context)
const globalSettingsCommands = useMemo(() => {
if (menuContext === 'settings') return [];
return CONTEXT_COMMANDS.settings;
}, [menuContext]);
const hasCommands = commands.length > 0 || globalSettingsCommands.length > 0;
if (!hasCommands) return null;
// Get localized context name
const contextName = tCommon(`cmdk.context.${menuContext}`, { defaultValue: menuContext });
const settingsContextName = tCommon('cmdk.context.settings', { defaultValue: 'settings' });
return (
<Command.Group>
{commands.map((cmd) => {
const Icon = cmd.icon;
// Get localized label using the correct namespace
let label = cmd.label;
if (cmd.labelKey) {
if (cmd.labelNamespace === 'auth') {
label = tAuth(cmd.labelKey, { defaultValue: cmd.label });
} else {
label = t(cmd.labelKey, { defaultValue: cmd.label });
}
}
const searchValue = `${contextName} ${label} ${cmd.keywords.join(' ')}`;
<>
{/* Current context commands */}
{commands.length > 0 && (
<Command.Group>
{commands.map((cmd) => {
const Icon = cmd.icon;
// Get localized label using the correct namespace
let label = cmd.label;
if (cmd.labelKey) {
if (cmd.labelNamespace === 'auth') {
label = tAuth(cmd.labelKey, { defaultValue: cmd.label });
} else if (cmd.labelNamespace === 'subscription') {
label = tSubscription(cmd.labelKey, { defaultValue: cmd.label });
} else {
label = t(cmd.labelKey, { defaultValue: cmd.label });
}
}
const searchValue = `${contextName} ${label} ${cmd.keywords.join(' ')}`;
return (
<CommandItem
icon={<Icon />}
key={cmd.path}
onSelect={() => handleNavigate(cmd.path)}
value={searchValue}
>
<span style={{ opacity: 0.5 }}>{contextName}</span>
<ChevronRight
size={14}
style={{
display: 'inline',
marginInline: '6px',
opacity: 0.5,
verticalAlign: 'middle',
}}
/>
{label}
</CommandItem>
);
})}
</Command.Group>
return (
<CommandItem
icon={<Icon />}
key={cmd.path}
onSelect={() => handleNavigate(cmd.path)}
value={searchValue}
>
<span style={{ opacity: 0.5 }}>{contextName}</span>
<ChevronRight
size={14}
style={{
display: 'inline',
marginInline: '6px',
opacity: 0.5,
verticalAlign: 'middle',
}}
/>
{label}
</CommandItem>
);
})}
</Command.Group>
)}
{/* Global settings commands (searchable from any page) */}
{globalSettingsCommands.length > 0 && (
<Command.Group>
{globalSettingsCommands.map((cmd) => {
const Icon = cmd.icon;
// Get localized label using the correct namespace
let label = cmd.label;
if (cmd.labelKey) {
if (cmd.labelNamespace === 'auth') {
label = tAuth(cmd.labelKey, { defaultValue: cmd.label });
} else if (cmd.labelNamespace === 'subscription') {
label = tSubscription(cmd.labelKey, { defaultValue: cmd.label });
} else {
label = t(cmd.labelKey, { defaultValue: cmd.label });
}
}
const searchValue = `${settingsContextName} ${label} ${cmd.keywords.join(' ')}`;
return (
<CommandItem
icon={<Icon />}
key={cmd.path}
onSelect={() => handleNavigate(cmd.path)}
unpinned={true}
value={searchValue}
>
<span style={{ opacity: 0.5 }}>{settingsContextName}</span>
<ChevronRight
size={14}
style={{
display: 'inline',
marginInline: '6px',
opacity: 0.5,
verticalAlign: 'middle',
}}
/>
{label}
</CommandItem>
);
})}
</Command.Group>
)}
</>
);
});

View File

@@ -1,6 +1,15 @@
import { Command } from 'cmdk';
import dayjs from 'dayjs';
import { Bot, FileText, MessageCircle, MessageSquare, Plug, Puzzle, Sparkles } from 'lucide-react';
import {
Bot,
ChevronRight,
FileText,
MessageCircle,
MessageSquare,
Plug,
Puzzle,
Sparkles,
} from 'lucide-react';
import { markdownToTxt } from 'markdown-to-txt';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -8,7 +17,6 @@ import { useNavigate } from 'react-router-dom';
import type { SearchResult } from '@/database/repositories/search';
import { useCommandMenuContext } from './CommandMenuContext';
import { CommandItem } from './components';
import { styles } from './styles';
import type { ValidSearchType } from './utils/queryParser';
@@ -29,7 +37,6 @@ const SearchResults = memo<SearchResultsProps>(
({ isLoading, onClose, onSetTypeFilter, results, searchQuery, typeFilter }) => {
const { t } = useTranslation('common');
const navigate = useNavigate();
const { menuContext } = useCommandMenuContext();
const handleNavigate = (result: SearchResult) => {
switch (result.type) {
@@ -146,15 +153,6 @@ const SearchResults = memo<SearchResultsProps>(
}
};
// Get trailing label for search results (shows "Market" for marketplace items)
const getTrailingLabel = (type: SearchResult['type']) => {
// Marketplace items: MCP, plugins, assistants
if (type === 'mcp' || type === 'plugin' || type === 'communityAgent') {
return t('cmdk.search.market');
}
return getTypeLabel(type);
};
// eslint-disable-next-line unicorn/consistent-function-scoping
const getItemValue = (result: SearchResult) => {
const meta = [result.title, result.description].filter(Boolean).join(' ');
@@ -193,26 +191,6 @@ const SearchResults = memo<SearchResultsProps>(
onSetTypeFilter(type);
};
// Helper to render "Search More" button
const renderSearchMore = (type: ValidSearchType, count: number) => {
// Don't show if already filtering by this type
if (typeFilter) return null;
// Show if there are results (might have more)
if (count === 0) return null;
return (
<CommandItem
forceMount
icon={getIcon(type)}
onSelect={() => handleSearchMore(type)}
title={t('cmdk.search.searchMore', { type: getTypeLabel(type) })}
value={`action-show-more-results-for-type-${type}`}
variant="detailed"
/>
);
};
const hasResults = results.length > 0;
// Group results by type
@@ -225,289 +203,135 @@ const SearchResults = memo<SearchResultsProps>(
const pluginResults = results.filter((r) => r.type === 'plugin');
const assistantResults = results.filter((r) => r.type === 'communityAgent');
// Detect context types
const isResourceContext = menuContext === 'resource';
const isPageContext = menuContext === 'page';
// Don't render anything if no results and not loading
if (!hasResults && !isLoading) {
return null;
}
// Render a single result item with type prefix (like "Message > content")
const renderResultItem = (result: SearchResult) => {
const typeLabel = getTypeLabel(result.type);
const subtitle = getSubtitle(result);
// Hide type prefix when filtering by specific type
const showTypePrefix = !typeFilter;
// Create title with or without type prefix
const titleWithPrefix = showTypePrefix ? (
<>
<span style={{ opacity: 0.5 }}>{typeLabel}</span>
<ChevronRight
size={14}
style={{
display: 'inline',
marginInline: '6px',
opacity: 0.5,
verticalAlign: 'middle',
}}
/>
{result.title}
</>
) : (
result.title
);
return (
<CommandItem
description={subtitle}
icon={getIcon(result.type)}
key={result.id}
onSelect={() => handleNavigate(result)}
title={titleWithPrefix}
value={getItemValue(result)}
variant="detailed"
/>
);
};
// Helper to render "Search More" button
const renderSearchMore = (type: ValidSearchType, count: number) => {
// Don't show if already filtering by this type
if (typeFilter) return null;
// Show if there are results (might have more)
if (count === 0) return null;
const typeLabel = getTypeLabel(type);
const titleText = `${t('cmdk.search.searchMore', { type: typeLabel })} with "${searchQuery}"`;
return (
<Command.Item
forceMount
key={`search-more-${type}`}
keywords={[`zzz-action-${type}`]}
onSelect={() => handleSearchMore(type)}
value={`zzz-action-${type}-search-more`}
>
<div className={styles.itemContent}>
<div className={styles.itemIcon}>{getIcon(type)}</div>
<div className={styles.itemDetails}>
<div className={styles.itemTitle}>{titleText}</div>
</div>
</div>
</Command.Item>
);
};
return (
<>
{/* Show pages first in page context */}
{hasResults && isPageContext && pageResults.length > 0 && (
<Command.Group heading={t('cmdk.search.pages')} key="pages-page-context">
{pageResults.map((result) => (
<CommandItem
description={result.description}
icon={getIcon(result.type)}
key={`page-page-context-${result.id}`}
onSelect={() => handleNavigate(result)}
title={result.title}
trailingLabel={getTrailingLabel(result.type)}
value={getItemValue(result)}
variant="detailed"
/>
))}
{renderSearchMore('page', pageResults.length)}
</Command.Group>
)}
{/* Show other results in page context */}
{hasResults && isPageContext && fileResults.length > 0 && (
<Command.Group heading={t('cmdk.search.files')}>
{fileResults.map((result) => (
<CommandItem
description={result.type === 'file' ? result.fileType : undefined}
icon={getIcon(result.type)}
key={`file-page-context-${result.id}`}
onSelect={() => handleNavigate(result)}
title={result.title}
trailingLabel={getTrailingLabel(result.type)}
value={getItemValue(result)}
variant="detailed"
/>
))}
{renderSearchMore('file', fileResults.length)}
</Command.Group>
)}
{hasResults && isPageContext && agentResults.length > 0 && (
<Command.Group heading={t('cmdk.search.agents')}>
{agentResults.map((result) => (
<CommandItem
description={getDescription(result)}
icon={getIcon(result.type)}
key={`agent-page-context-${result.id}`}
onSelect={() => handleNavigate(result)}
title={result.title}
trailingLabel={getTrailingLabel(result.type)}
value={getItemValue(result)}
variant="detailed"
/>
))}
{renderSearchMore('agent', agentResults.length)}
</Command.Group>
)}
{hasResults && isPageContext && topicResults.length > 0 && (
<Command.Group heading={t('cmdk.search.topics')}>
{topicResults.map((result) => (
<CommandItem
description={getSubtitle(result)}
icon={getIcon(result.type)}
key={`topic-page-context-${result.id}`}
onSelect={() => handleNavigate(result)}
title={result.title}
trailingLabel={getTrailingLabel(result.type)}
value={getItemValue(result)}
variant="detailed"
/>
))}
{renderSearchMore('topic', topicResults.length)}
</Command.Group>
)}
{hasResults && isPageContext && messageResults.length > 0 && (
<Command.Group heading={t('cmdk.search.messages')}>
{messageResults.map((result) => (
<CommandItem
description={getSubtitle(result)}
icon={getIcon(result.type)}
key={`message-page-context-${result.id}`}
onSelect={() => handleNavigate(result)}
title={result.title}
trailingLabel={getTrailingLabel(result.type)}
value={getItemValue(result)}
variant="detailed"
/>
))}
{/* Render search results grouped by type without headers */}
{messageResults.length > 0 && (
<Command.Group>
{messageResults.map((result) => renderResultItem(result))}
{renderSearchMore('message', messageResults.length)}
</Command.Group>
)}
{/* Show pages first in resource context */}
{hasResults && isResourceContext && pageResults.length > 0 && (
<Command.Group heading={t('cmdk.search.pages')} key="pages-resource">
{pageResults.map((result) => (
<CommandItem
description={result.description}
icon={getIcon(result.type)}
key={`page-resource-${result.id}`}
onSelect={() => handleNavigate(result)}
title={result.title}
trailingLabel={getTrailingLabel(result.type)}
value={getItemValue(result)}
variant="detailed"
/>
))}
{renderSearchMore('page', pageResults.length)}
</Command.Group>
)}
{/* Show files in resource context */}
{hasResults && isResourceContext && fileResults.length > 0 && (
<Command.Group heading={t('cmdk.search.files')}>
{fileResults.map((result) => (
<CommandItem
description={result.type === 'file' ? result.fileType : undefined}
icon={getIcon(result.type)}
key={`file-${result.id}`}
onSelect={() => handleNavigate(result)}
title={result.title}
trailingLabel={getTrailingLabel(result.type)}
value={getItemValue(result)}
variant="detailed"
/>
))}
{renderSearchMore('file', fileResults.length)}
</Command.Group>
)}
{hasResults && !isPageContext && !isResourceContext && messageResults.length > 0 && (
<Command.Group heading={t('cmdk.search.messages')}>
{messageResults.map((result) => (
<CommandItem
description={getSubtitle(result)}
icon={getIcon(result.type)}
key={`message-${result.id}`}
onSelect={() => handleNavigate(result)}
title={result.title}
trailingLabel={getTrailingLabel(result.type)}
value={getItemValue(result)}
variant="detailed"
/>
))}
{renderSearchMore('message', messageResults.length)}
</Command.Group>
)}
{hasResults && !isPageContext && agentResults.length > 0 && (
<Command.Group heading={t('cmdk.search.agents')}>
{agentResults.map((result) => (
<CommandItem
description={getDescription(result)}
icon={getIcon(result.type)}
key={`agent-${result.id}`}
onSelect={() => handleNavigate(result)}
title={result.title}
trailingLabel={getTrailingLabel(result.type)}
value={getItemValue(result)}
variant="detailed"
/>
))}
{agentResults.length > 0 && (
<Command.Group>
{agentResults.map((result) => renderResultItem(result))}
{renderSearchMore('agent', agentResults.length)}
</Command.Group>
)}
{hasResults && !isPageContext && topicResults.length > 0 && (
<Command.Group heading={t('cmdk.search.topics')}>
{topicResults.map((result) => (
<CommandItem
description={getSubtitle(result)}
icon={getIcon(result.type)}
key={`topic-${result.id}`}
onSelect={() => handleNavigate(result)}
title={result.title}
trailingLabel={getTrailingLabel(result.type)}
value={getItemValue(result)}
variant="detailed"
/>
))}
{topicResults.length > 0 && (
<Command.Group>
{topicResults.map((result) => renderResultItem(result))}
{renderSearchMore('topic', topicResults.length)}
</Command.Group>
)}
{/* Show document pages in normal context (not in resource or page context) */}
{hasResults && !isResourceContext && !isPageContext && pageResults.length > 0 && (
<Command.Group heading={t('cmdk.search.pages')} key="pages-normal">
{pageResults.map((result) => (
<CommandItem
description={result.description}
icon={getIcon(result.type)}
key={`page-normal-${result.id}`}
onSelect={() => handleNavigate(result)}
title={result.title}
trailingLabel={getTrailingLabel(result.type)}
value={getItemValue(result)}
variant="detailed"
/>
))}
{pageResults.length > 0 && (
<Command.Group>
{pageResults.map((result) => renderResultItem(result))}
{renderSearchMore('page', pageResults.length)}
</Command.Group>
)}
{/* Show files in original position when NOT in resource or page context */}
{hasResults && !isResourceContext && !isPageContext && fileResults.length > 0 && (
<Command.Group heading={t('cmdk.search.files')}>
{fileResults.map((result) => (
<CommandItem
description={result.type === 'file' ? result.fileType : undefined}
icon={getIcon(result.type)}
key={`file-${result.id}`}
onSelect={() => handleNavigate(result)}
title={result.title}
trailingLabel={getTrailingLabel(result.type)}
value={getItemValue(result)}
variant="detailed"
/>
))}
{fileResults.length > 0 && (
<Command.Group>
{fileResults.map((result) => renderResultItem(result))}
{renderSearchMore('file', fileResults.length)}
</Command.Group>
)}
{hasResults && mcpResults.length > 0 && (
<Command.Group heading={t('cmdk.search.mcps')}>
{mcpResults.map((result) => (
<CommandItem
description={getDescription(result)}
icon={getIcon(result.type)}
key={`mcp-${result.id}`}
onSelect={() => handleNavigate(result)}
title={result.title}
trailingLabel={getTrailingLabel(result.type)}
value={getItemValue(result)}
variant="detailed"
/>
))}
{mcpResults.length > 0 && (
<Command.Group>
{mcpResults.map((result) => renderResultItem(result))}
{renderSearchMore('mcp', mcpResults.length)}
</Command.Group>
)}
{hasResults && pluginResults.length > 0 && (
<Command.Group heading={t('cmdk.search.plugins')}>
{pluginResults.map((result) => (
<CommandItem
description={getDescription(result)}
icon={getIcon(result.type)}
key={`plugin-${result.id}`}
onSelect={() => handleNavigate(result)}
title={result.title}
trailingLabel={getTrailingLabel(result.type)}
value={getItemValue(result)}
variant="detailed"
/>
))}
{pluginResults.length > 0 && (
<Command.Group>
{pluginResults.map((result) => renderResultItem(result))}
{renderSearchMore('plugin', pluginResults.length)}
</Command.Group>
)}
{hasResults && assistantResults.length > 0 && (
<Command.Group heading={t('cmdk.search.assistants')}>
{assistantResults.map((result) => (
<CommandItem
description={getDescription(result)}
icon={getIcon(result.type)}
key={`assistant-${result.id}`}
onSelect={() => handleNavigate(result)}
title={result.title}
trailingLabel={getTrailingLabel(result.type)}
value={getItemValue(result)}
variant="detailed"
/>
))}
{assistantResults.length > 0 && (
<Command.Group>
{assistantResults.map((result) => renderResultItem(result))}
{renderSearchMore('communityAgent', assistantResults.length)}
</Command.Group>
)}

View File

@@ -5,7 +5,7 @@ import { cloneElement, isValidElement, memo } from 'react';
import { useCommandMenuContext } from '../CommandMenuContext';
import { styles } from '../styles';
type BaseCommandItemProps = Omit<ComponentProps<typeof Command.Item>, 'children'> & {
type BaseCommandItemProps = Omit<ComponentProps<typeof Command.Item>, 'children' | 'title'> & {
/**
* Hide the item from default view but keep it searchable
* When true, the item won't show in the default list but will appear in search results

View File

@@ -1,13 +1,19 @@
import { ENABLE_BUSINESS_FEATURES } from '@lobechat/business-const';
import type { LucideIcon } from 'lucide-react';
import {
Brain,
ChartColumnBigIcon,
Coins,
CreditCard,
EthernetPort,
Gift,
Image as ImageIcon,
Info,
KeyIcon,
KeyboardIcon,
Map,
Palette as PaletteIcon,
PieChart,
UserCircle,
} from 'lucide-react';
@@ -18,7 +24,7 @@ export interface ContextCommand {
keywords: string[];
label: string;
labelKey?: string; // i18n key for the label
labelNamespace?: 'setting' | 'auth'; // i18n namespace for the label
labelNamespace?: 'setting' | 'auth' | 'subscription'; // i18n namespace for the label
path: string;
subPath: string;
}
@@ -114,6 +120,55 @@ export const CONTEXT_COMMANDS: Record<ContextType, ContextCommand[]> = {
path: '/settings/about',
subPath: 'about',
},
...(ENABLE_BUSINESS_FEATURES
? [
{
icon: Map,
keywords: ['subscription', 'plan', 'upgrade', 'pricing'],
label: 'Subscription Plans',
labelKey: 'tab.plans',
labelNamespace: 'subscription' as const,
path: '/settings/plans',
subPath: 'plans',
},
{
icon: Coins,
keywords: ['funds', 'balance', 'credit', 'money'],
label: 'Funds',
labelKey: 'tab.funds',
labelNamespace: 'subscription' as const,
path: '/settings/funds',
subPath: 'funds',
},
{
icon: PieChart,
keywords: ['usage', 'statistics', 'consumption', 'quota'],
label: 'Usage',
labelKey: 'tab.usage',
labelNamespace: 'subscription' as const,
path: '/settings/usage',
subPath: 'usage',
},
{
icon: CreditCard,
keywords: ['billing', 'payment', 'invoice', 'transaction'],
label: 'Billing',
labelKey: 'tab.billing',
labelNamespace: 'subscription' as const,
path: '/settings/billing',
subPath: 'billing',
},
{
icon: Gift,
keywords: ['referral', 'rewards', 'invite', 'bonus'],
label: 'Referral Rewards',
labelKey: 'tab.referral',
labelNamespace: 'subscription' as const,
path: '/settings/referral',
subPath: 'referral',
},
]
: []),
],
};

View File

@@ -133,8 +133,8 @@ export default {
'cmdk.navigate': 'Navigate',
'cmdk.newAgent': 'Create New Agent',
'cmdk.newAgentTeam': 'Create New Group',
'cmdk.newLibrary': 'New Library',
'cmdk.newPage': 'New Page',
'cmdk.newLibrary': 'Create New Library',
'cmdk.newPage': 'Create New Page',
'cmdk.newTopic': 'New topic in current Agent',
'cmdk.noResults': 'No results found',
'cmdk.openSettings': 'Open Settings',