@lobehub/lobehub 2.0.0-next.280 → 2.0.0-next.282
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 +50 -0
- package/changelog/v1.json +18 -0
- package/e2e/src/steps/agent/conversation-mgmt.steps.ts +47 -3
- package/package.json +1 -1
- package/packages/builtin-tool-group-agent-builder/src/client/Render/BatchCreateAgents.tsx +19 -3
- package/packages/builtin-tool-group-agent-builder/src/client/Streaming/BatchCreateAgents/index.tsx +19 -4
- package/packages/builtin-tool-group-agent-builder/src/client/Streaming/UpdateAgentPrompt/index.tsx +3 -13
- package/packages/builtin-tool-group-agent-builder/src/client/Streaming/UpdateGroupPrompt/index.tsx +1 -1
- package/packages/builtin-tool-group-agent-builder/src/systemRole.ts +83 -121
- package/packages/builtin-tool-local-system/src/client/Inspector/ReadLocalFile/index.tsx +20 -2
- package/packages/observability-otel/src/node.ts +40 -3
- package/src/app/[variants]/(main)/agent/_layout/Sidebar/Topic/List/Item/index.tsx +9 -1
- package/src/app/[variants]/(main)/agent/features/Conversation/ConversationArea.tsx +1 -28
- package/src/app/[variants]/(main)/community/(detail)/features/MakedownRender.tsx +3 -3
- package/src/app/[variants]/(main)/group/_layout/Sidebar/GroupConfig/GroupMember.tsx +8 -1
- package/src/app/[variants]/(main)/group/_layout/Sidebar/GroupConfig/Header/Avatar.tsx +2 -13
- package/src/app/[variants]/(main)/group/_layout/Sidebar/Header/Agent/index.tsx +3 -4
- package/src/app/[variants]/(main)/group/_layout/Sidebar/Topic/List/Item/index.tsx +17 -8
- package/src/app/[variants]/(main)/group/features/Conversation/ConversationArea.tsx +1 -29
- package/src/app/[variants]/(main)/group/features/Conversation/Header/ShareButton/index.tsx +0 -2
- package/src/app/[variants]/(main)/group/features/GroupAvatar.tsx +17 -9
- package/src/app/[variants]/(main)/group/profile/features/AgentBuilder/TopicSelector.tsx +8 -5
- package/src/app/[variants]/(main)/group/profile/features/Header/ChromeTabs/index.tsx +20 -2
- package/src/app/[variants]/(main)/group/profile/features/MemberProfile/AgentTool.tsx +4 -2
- package/src/app/[variants]/(main)/group/profile/features/ProfileHydration.tsx +5 -25
- package/src/features/AgentGroupAvatar/index.tsx +38 -0
- package/src/features/Conversation/Messages/Supervisor/index.tsx +8 -2
- package/src/features/Conversation/Messages/User/useMarkdown.tsx +1 -2
- package/src/features/EditorModal/EditorCanvas.tsx +62 -0
- package/src/features/EditorModal/TextArea.tsx +30 -0
- package/src/features/EditorModal/Typobar.tsx +139 -0
- package/src/features/EditorModal/index.tsx +18 -8
- package/src/features/NavPanel/components/EmptyNavItem.tsx +2 -2
- package/src/features/NavPanel/components/NavItem.tsx +27 -3
- package/src/features/ToolTag/index.tsx +167 -0
- package/src/server/routers/lambda/topic.ts +8 -1
- package/src/services/chat/mecha/contextEngineering.test.ts +1 -1
- package/src/services/chat/mecha/contextEngineering.ts +3 -4
- package/src/services/chat/mecha/memoryManager.ts +9 -38
- package/src/store/agentGroup/initialState.ts +1 -1
- package/src/store/agentGroup/slices/lifecycle.ts +15 -2
- package/src/app/[variants]/(main)/group/_layout/Sidebar/Topic/hooks/useTopicNavigation.ts +0 -49
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import {
|
|
2
|
+
IEditor,
|
|
3
|
+
ReactCodePlugin,
|
|
4
|
+
ReactCodemirrorPlugin,
|
|
5
|
+
ReactHRPlugin,
|
|
6
|
+
ReactLinkPlugin,
|
|
7
|
+
ReactListPlugin,
|
|
8
|
+
ReactMathPlugin,
|
|
9
|
+
ReactTablePlugin,
|
|
10
|
+
} from '@lobehub/editor';
|
|
11
|
+
import { Editor } from '@lobehub/editor/react';
|
|
12
|
+
import { Flexbox } from '@lobehub/ui';
|
|
13
|
+
import { FC } from 'react';
|
|
14
|
+
|
|
15
|
+
import TypoBar from './Typobar';
|
|
16
|
+
|
|
17
|
+
interface EditorCanvasProps {
|
|
18
|
+
defaultValue?: string;
|
|
19
|
+
editor?: IEditor;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const EditorCanvas: FC<EditorCanvasProps> = ({ defaultValue, editor }) => {
|
|
23
|
+
return (
|
|
24
|
+
<>
|
|
25
|
+
<TypoBar editor={editor} />
|
|
26
|
+
<Flexbox
|
|
27
|
+
padding={16}
|
|
28
|
+
style={{ cursor: 'text', maxHeight: '80vh', minHeight: '50vh', overflowY: 'auto' }}
|
|
29
|
+
>
|
|
30
|
+
<Editor
|
|
31
|
+
autoFocus
|
|
32
|
+
content={''}
|
|
33
|
+
editor={editor}
|
|
34
|
+
onInit={(editor) => {
|
|
35
|
+
if (!editor || !defaultValue) return;
|
|
36
|
+
try {
|
|
37
|
+
editor?.setDocument('markdown', defaultValue);
|
|
38
|
+
} catch (e) {
|
|
39
|
+
console.error('setDocument error:', e);
|
|
40
|
+
}
|
|
41
|
+
}}
|
|
42
|
+
plugins={[
|
|
43
|
+
ReactListPlugin,
|
|
44
|
+
ReactCodePlugin,
|
|
45
|
+
ReactCodemirrorPlugin,
|
|
46
|
+
ReactHRPlugin,
|
|
47
|
+
ReactLinkPlugin,
|
|
48
|
+
ReactTablePlugin,
|
|
49
|
+
ReactMathPlugin,
|
|
50
|
+
]}
|
|
51
|
+
style={{
|
|
52
|
+
paddingBottom: 120,
|
|
53
|
+
}}
|
|
54
|
+
type={'text'}
|
|
55
|
+
variant={'chat'}
|
|
56
|
+
/>
|
|
57
|
+
</Flexbox>
|
|
58
|
+
</>
|
|
59
|
+
);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export default EditorCanvas;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { TextArea } from '@lobehub/ui';
|
|
2
|
+
import { FC } from 'react';
|
|
3
|
+
|
|
4
|
+
interface EditorCanvasProps {
|
|
5
|
+
defaultValue?: string;
|
|
6
|
+
onChange?: (value: string) => void;
|
|
7
|
+
value?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const EditorCanvas: FC<EditorCanvasProps> = ({ defaultValue, value, onChange }) => {
|
|
11
|
+
return (
|
|
12
|
+
<TextArea
|
|
13
|
+
defaultValue={defaultValue}
|
|
14
|
+
onChange={(e) => {
|
|
15
|
+
onChange?.(e.target.value);
|
|
16
|
+
}}
|
|
17
|
+
style={{
|
|
18
|
+
cursor: 'text',
|
|
19
|
+
maxHeight: '80vh',
|
|
20
|
+
minHeight: '50vh',
|
|
21
|
+
overflowY: 'auto',
|
|
22
|
+
padding: 16,
|
|
23
|
+
}}
|
|
24
|
+
value={value}
|
|
25
|
+
variant={'borderless'}
|
|
26
|
+
/>
|
|
27
|
+
);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export default EditorCanvas;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { HotkeyEnum, type IEditor, getHotkeyById } from '@lobehub/editor';
|
|
2
|
+
import { useEditorState } from '@lobehub/editor/react';
|
|
3
|
+
import {
|
|
4
|
+
ChatInputActionBar,
|
|
5
|
+
ChatInputActions,
|
|
6
|
+
type ChatInputActionsProps,
|
|
7
|
+
} from '@lobehub/editor/react';
|
|
8
|
+
import { cssVar } from 'antd-style';
|
|
9
|
+
import {
|
|
10
|
+
BoldIcon,
|
|
11
|
+
CodeXmlIcon,
|
|
12
|
+
ItalicIcon,
|
|
13
|
+
ListIcon,
|
|
14
|
+
ListOrderedIcon,
|
|
15
|
+
ListTodoIcon,
|
|
16
|
+
MessageSquareQuote,
|
|
17
|
+
SigmaIcon,
|
|
18
|
+
SquareDashedBottomCodeIcon,
|
|
19
|
+
StrikethroughIcon,
|
|
20
|
+
UnderlineIcon,
|
|
21
|
+
} from 'lucide-react';
|
|
22
|
+
import { memo, useMemo } from 'react';
|
|
23
|
+
import { useTranslation } from 'react-i18next';
|
|
24
|
+
|
|
25
|
+
const TypoBar = memo<{ editor?: IEditor }>(({ editor }) => {
|
|
26
|
+
const { t } = useTranslation('editor');
|
|
27
|
+
const editorState = useEditorState(editor);
|
|
28
|
+
|
|
29
|
+
const items: ChatInputActionsProps['items'] = useMemo(
|
|
30
|
+
() =>
|
|
31
|
+
[
|
|
32
|
+
{
|
|
33
|
+
active: editorState.isBold,
|
|
34
|
+
icon: BoldIcon,
|
|
35
|
+
key: 'bold',
|
|
36
|
+
label: t('typobar.bold'),
|
|
37
|
+
onClick: editorState.bold,
|
|
38
|
+
tooltipProps: { hotkey: getHotkeyById(HotkeyEnum.Bold).keys },
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
active: editorState.isItalic,
|
|
42
|
+
icon: ItalicIcon,
|
|
43
|
+
key: 'italic',
|
|
44
|
+
label: t('typobar.italic'),
|
|
45
|
+
onClick: editorState.italic,
|
|
46
|
+
tooltipProps: { hotkey: getHotkeyById(HotkeyEnum.Italic).keys },
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
active: editorState.isUnderline,
|
|
50
|
+
icon: UnderlineIcon,
|
|
51
|
+
key: 'underline',
|
|
52
|
+
label: t('typobar.underline'),
|
|
53
|
+
onClick: editorState.underline,
|
|
54
|
+
tooltipProps: { hotkey: getHotkeyById(HotkeyEnum.Underline).keys },
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
active: editorState.isStrikethrough,
|
|
58
|
+
icon: StrikethroughIcon,
|
|
59
|
+
key: 'strikethrough',
|
|
60
|
+
label: t('typobar.strikethrough'),
|
|
61
|
+
onClick: editorState.strikethrough,
|
|
62
|
+
tooltipProps: { hotkey: getHotkeyById(HotkeyEnum.Strikethrough).keys },
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
type: 'divider',
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
{
|
|
69
|
+
icon: ListIcon,
|
|
70
|
+
key: 'bulletList',
|
|
71
|
+
label: t('typobar.bulletList'),
|
|
72
|
+
onClick: editorState.bulletList,
|
|
73
|
+
tooltipProps: { hotkey: getHotkeyById(HotkeyEnum.BulletList).keys },
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
icon: ListOrderedIcon,
|
|
77
|
+
key: 'numberlist',
|
|
78
|
+
label: t('typobar.numberList'),
|
|
79
|
+
onClick: editorState.numberList,
|
|
80
|
+
tooltipProps: { hotkey: getHotkeyById(HotkeyEnum.NumberList).keys },
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
icon: ListTodoIcon,
|
|
84
|
+
key: 'tasklist',
|
|
85
|
+
label: t('typobar.taskList'),
|
|
86
|
+
onClick: editorState.checkList,
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
type: 'divider',
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
active: editorState.isBlockquote,
|
|
93
|
+
icon: MessageSquareQuote,
|
|
94
|
+
key: 'blockquote',
|
|
95
|
+
label: t('typobar.blockquote'),
|
|
96
|
+
onClick: editorState.blockquote,
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
type: 'divider',
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
icon: SigmaIcon,
|
|
103
|
+
key: 'math',
|
|
104
|
+
label: t('typobar.tex'),
|
|
105
|
+
onClick: editorState.insertMath,
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
active: editorState.isCode,
|
|
109
|
+
icon: CodeXmlIcon,
|
|
110
|
+
key: 'code',
|
|
111
|
+
label: t('typobar.code'),
|
|
112
|
+
onClick: editorState.code,
|
|
113
|
+
tooltipProps: { hotkey: getHotkeyById(HotkeyEnum.CodeInline).keys },
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
icon: SquareDashedBottomCodeIcon,
|
|
117
|
+
key: 'codeblock',
|
|
118
|
+
label: t('typobar.codeblock'),
|
|
119
|
+
onClick: editorState.codeblock,
|
|
120
|
+
},
|
|
121
|
+
].filter(Boolean) as ChatInputActionsProps['items'],
|
|
122
|
+
[editorState],
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<ChatInputActionBar
|
|
127
|
+
left={<ChatInputActions items={items} />}
|
|
128
|
+
style={{
|
|
129
|
+
background: cssVar.colorFillQuaternary,
|
|
130
|
+
borderTopLeftRadius: 8,
|
|
131
|
+
borderTopRightRadius: 8,
|
|
132
|
+
}}
|
|
133
|
+
/>
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
TypoBar.displayName = 'TypoBar';
|
|
138
|
+
|
|
139
|
+
export default TypoBar;
|
|
@@ -3,7 +3,11 @@ import { Modal, ModalProps, createRawModal } from '@lobehub/ui';
|
|
|
3
3
|
import { memo, useState } from 'react';
|
|
4
4
|
import { useTranslation } from 'react-i18next';
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { useUserStore } from '@/store/user';
|
|
7
|
+
import { labPreferSelectors } from '@/store/user/selectors';
|
|
8
|
+
|
|
9
|
+
import EditorCanvas from './EditorCanvas';
|
|
10
|
+
import TextareCanvas from './TextArea';
|
|
7
11
|
|
|
8
12
|
interface EditorModalProps extends ModalProps {
|
|
9
13
|
onConfirm?: (value: string) => Promise<void>;
|
|
@@ -13,7 +17,8 @@ interface EditorModalProps extends ModalProps {
|
|
|
13
17
|
export const EditorModal = memo<EditorModalProps>(({ value, onConfirm, ...rest }) => {
|
|
14
18
|
const [confirmLoading, setConfirmLoading] = useState(false);
|
|
15
19
|
const { t } = useTranslation('common');
|
|
16
|
-
|
|
20
|
+
const [v, setV] = useState(value);
|
|
21
|
+
const enableRichRender = useUserStore(labPreferSelectors.enableInputMarkdown);
|
|
17
22
|
const editor = useEditor();
|
|
18
23
|
|
|
19
24
|
return (
|
|
@@ -25,12 +30,13 @@ export const EditorModal = memo<EditorModalProps>(({ value, onConfirm, ...rest }
|
|
|
25
30
|
okText={t('ok')}
|
|
26
31
|
onOk={async () => {
|
|
27
32
|
setConfirmLoading(true);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
+
let finalValue;
|
|
34
|
+
if (enableRichRender) {
|
|
35
|
+
finalValue = editor?.getDocument('markdown') as unknown as string;
|
|
36
|
+
} else {
|
|
37
|
+
finalValue = v;
|
|
33
38
|
}
|
|
39
|
+
await onConfirm?.(finalValue || '');
|
|
34
40
|
setConfirmLoading(false);
|
|
35
41
|
}}
|
|
36
42
|
styles={{
|
|
@@ -43,7 +49,11 @@ export const EditorModal = memo<EditorModalProps>(({ value, onConfirm, ...rest }
|
|
|
43
49
|
width={'min(90vw, 920px)'}
|
|
44
50
|
{...rest}
|
|
45
51
|
>
|
|
46
|
-
|
|
52
|
+
{enableRichRender ? (
|
|
53
|
+
<EditorCanvas defaultValue={value} editor={editor} />
|
|
54
|
+
) : (
|
|
55
|
+
<TextareCanvas defaultValue={value} onChange={(v) => setV(v)} value={v} />
|
|
56
|
+
)}
|
|
47
57
|
</Modal>
|
|
48
58
|
);
|
|
49
59
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Block, Center, Icon, Text } from '@lobehub/ui';
|
|
2
2
|
import { PlusIcon } from 'lucide-react';
|
|
3
3
|
import { memo } from 'react';
|
|
4
4
|
|
|
@@ -22,7 +22,7 @@ const EmptyNavItem = memo<EmptyStatusProps>(({ title, onClick, className }) => {
|
|
|
22
22
|
variant={'borderless'}
|
|
23
23
|
>
|
|
24
24
|
<Center flex={'none'} height={28} width={28}>
|
|
25
|
-
<
|
|
25
|
+
<Icon icon={PlusIcon} size={'small'} />
|
|
26
26
|
</Center>
|
|
27
27
|
<Text align={'center'} type={'secondary'}>
|
|
28
28
|
{title}
|
|
@@ -12,9 +12,10 @@ import {
|
|
|
12
12
|
Text,
|
|
13
13
|
} from '@lobehub/ui';
|
|
14
14
|
import { createStaticStyles, cssVar, cx } from 'antd-style';
|
|
15
|
-
import { Loader2Icon } from 'lucide-react';
|
|
16
15
|
import { type ReactNode, memo } from 'react';
|
|
17
16
|
|
|
17
|
+
import NeuralNetworkLoading from '@/components/NeuralNetworkLoading';
|
|
18
|
+
|
|
18
19
|
const ACTION_CLASS_NAME = 'nav-item-actions';
|
|
19
20
|
|
|
20
21
|
const styles = createStaticStyles(({ css }) => ({
|
|
@@ -50,6 +51,10 @@ export interface NavItemProps extends Omit<BlockProps, 'children' | 'title'> {
|
|
|
50
51
|
contextMenuItems?: GenericItemType[] | (() => GenericItemType[]);
|
|
51
52
|
disabled?: boolean;
|
|
52
53
|
extra?: ReactNode;
|
|
54
|
+
/**
|
|
55
|
+
* Optional href for cmd+click to open in new tab
|
|
56
|
+
*/
|
|
57
|
+
href?: string;
|
|
53
58
|
icon?: IconProps['icon'];
|
|
54
59
|
loading?: boolean;
|
|
55
60
|
title: ReactNode;
|
|
@@ -61,6 +66,7 @@ const NavItem = memo<NavItemProps>(
|
|
|
61
66
|
actions,
|
|
62
67
|
contextMenuItems,
|
|
63
68
|
active,
|
|
69
|
+
href,
|
|
64
70
|
icon,
|
|
65
71
|
title,
|
|
66
72
|
onClick,
|
|
@@ -72,7 +78,15 @@ const NavItem = memo<NavItemProps>(
|
|
|
72
78
|
const iconColor = active ? cssVar.colorText : cssVar.colorTextDescription;
|
|
73
79
|
const textColor = active ? cssVar.colorText : cssVar.colorTextSecondary;
|
|
74
80
|
const variant = active ? 'filled' : 'borderless';
|
|
75
|
-
|
|
81
|
+
|
|
82
|
+
// Link props for cmd+click support
|
|
83
|
+
const linkProps = href
|
|
84
|
+
? {
|
|
85
|
+
as: 'a' as const,
|
|
86
|
+
href,
|
|
87
|
+
style: { color: 'inherit', textDecoration: 'none' },
|
|
88
|
+
}
|
|
89
|
+
: {};
|
|
76
90
|
|
|
77
91
|
const Content = (
|
|
78
92
|
<Block
|
|
@@ -84,15 +98,25 @@ const NavItem = memo<NavItemProps>(
|
|
|
84
98
|
horizontal
|
|
85
99
|
onClick={(e) => {
|
|
86
100
|
if (disabled || loading) return;
|
|
101
|
+
// Prevent default link behavior for normal clicks (let onClick handle it)
|
|
102
|
+
// But allow cmd+click to open in new tab
|
|
103
|
+
if (href && !e.metaKey && !e.ctrlKey) {
|
|
104
|
+
e.preventDefault();
|
|
105
|
+
}
|
|
87
106
|
onClick?.(e);
|
|
88
107
|
}}
|
|
89
108
|
paddingInline={4}
|
|
90
109
|
variant={variant}
|
|
110
|
+
{...linkProps}
|
|
91
111
|
{...rest}
|
|
92
112
|
>
|
|
93
113
|
{icon && (
|
|
94
114
|
<Center flex={'none'} height={28} width={28}>
|
|
95
|
-
|
|
115
|
+
{loading ? (
|
|
116
|
+
<NeuralNetworkLoading size={18} />
|
|
117
|
+
) : (
|
|
118
|
+
<Icon color={iconColor} icon={icon} size={18} />
|
|
119
|
+
)}
|
|
96
120
|
</Center>
|
|
97
121
|
)}
|
|
98
122
|
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { KLAVIS_SERVER_TYPES, type KlavisServerType } from '@lobechat/const';
|
|
4
|
+
import { Avatar, Icon, Tag } from '@lobehub/ui';
|
|
5
|
+
import { createStaticStyles, cssVar } from 'antd-style';
|
|
6
|
+
import isEqual from 'fast-deep-equal';
|
|
7
|
+
import { memo, useMemo } from 'react';
|
|
8
|
+
|
|
9
|
+
import PluginAvatar from '@/components/Plugins/PluginAvatar';
|
|
10
|
+
import { useIsDark } from '@/hooks/useIsDark';
|
|
11
|
+
import { useDiscoverStore } from '@/store/discover';
|
|
12
|
+
import { serverConfigSelectors, useServerConfigStore } from '@/store/serverConfig';
|
|
13
|
+
import { useToolStore } from '@/store/tool';
|
|
14
|
+
import {
|
|
15
|
+
builtinToolSelectors,
|
|
16
|
+
klavisStoreSelectors,
|
|
17
|
+
pluginSelectors,
|
|
18
|
+
} from '@/store/tool/selectors';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Klavis server icon component
|
|
22
|
+
*/
|
|
23
|
+
const KlavisIcon = memo<Pick<KlavisServerType, 'icon' | 'label'>>(({ icon, label }) => {
|
|
24
|
+
if (typeof icon === 'string') {
|
|
25
|
+
return <img alt={label} height={16} src={icon} style={{ flexShrink: 0 }} width={16} />;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return <Icon fill={cssVar.colorText} icon={icon} size={16} />;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const styles = createStaticStyles(({ css, cssVar }) => ({
|
|
32
|
+
compact: css`
|
|
33
|
+
height: auto !important;
|
|
34
|
+
padding: 0 !important;
|
|
35
|
+
border: none !important;
|
|
36
|
+
background: transparent !important;
|
|
37
|
+
`,
|
|
38
|
+
tag: css`
|
|
39
|
+
height: 24px !important;
|
|
40
|
+
border-radius: ${cssVar.borderRadiusSM} !important;
|
|
41
|
+
`,
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
export interface ToolTagProps {
|
|
45
|
+
/**
|
|
46
|
+
* The tool identifier to display
|
|
47
|
+
*/
|
|
48
|
+
identifier: string;
|
|
49
|
+
/**
|
|
50
|
+
* Variant style of the tag
|
|
51
|
+
* - 'default': normal tag with background and border
|
|
52
|
+
* - 'compact': no padding, no background, no border (text only with icon)
|
|
53
|
+
* @default 'default'
|
|
54
|
+
*/
|
|
55
|
+
variant?: 'compact' | 'default';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* A readonly tag component that displays tool information based on identifier.
|
|
60
|
+
* Unlike PluginTag, this component is not closable and is designed for display-only purposes.
|
|
61
|
+
*/
|
|
62
|
+
const ToolTag = memo<ToolTagProps>(({ identifier, variant = 'default' }) => {
|
|
63
|
+
const isDarkMode = useIsDark();
|
|
64
|
+
const isCompact = variant === 'compact';
|
|
65
|
+
|
|
66
|
+
// Get local plugin lists
|
|
67
|
+
const builtinList = useToolStore(builtinToolSelectors.metaList, isEqual);
|
|
68
|
+
const installedPluginList = useToolStore(pluginSelectors.installedPluginMetaList, isEqual);
|
|
69
|
+
|
|
70
|
+
// Klavis related state
|
|
71
|
+
const allKlavisServers = useToolStore(klavisStoreSelectors.getServers, isEqual);
|
|
72
|
+
const isKlavisEnabledInEnv = useServerConfigStore(serverConfigSelectors.enableKlavis);
|
|
73
|
+
|
|
74
|
+
// Check if plugin is installed
|
|
75
|
+
const isInstalled = useToolStore(pluginSelectors.isPluginInstalled(identifier));
|
|
76
|
+
|
|
77
|
+
// Try to find in local lists first (including Klavis)
|
|
78
|
+
const localMeta = useMemo(() => {
|
|
79
|
+
// Check if it's a Klavis server type
|
|
80
|
+
if (isKlavisEnabledInEnv) {
|
|
81
|
+
const klavisType = KLAVIS_SERVER_TYPES.find((type) => type.identifier === identifier);
|
|
82
|
+
if (klavisType) {
|
|
83
|
+
const connectedServer = allKlavisServers.find((s) => s.identifier === identifier);
|
|
84
|
+
return {
|
|
85
|
+
icon: klavisType.icon,
|
|
86
|
+
isInstalled: !!connectedServer,
|
|
87
|
+
label: klavisType.label,
|
|
88
|
+
title: klavisType.label,
|
|
89
|
+
type: 'klavis' as const,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const builtinMeta = builtinList.find((p) => p.identifier === identifier);
|
|
95
|
+
if (builtinMeta) {
|
|
96
|
+
return {
|
|
97
|
+
avatar: builtinMeta.meta.avatar,
|
|
98
|
+
isInstalled: true,
|
|
99
|
+
title: builtinMeta.meta.title,
|
|
100
|
+
type: 'builtin' as const,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const installedMeta = installedPluginList.find((p) => p.identifier === identifier);
|
|
105
|
+
if (installedMeta) {
|
|
106
|
+
return {
|
|
107
|
+
avatar: installedMeta.avatar,
|
|
108
|
+
isInstalled: true,
|
|
109
|
+
title: installedMeta.title,
|
|
110
|
+
type: 'plugin' as const,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return null;
|
|
115
|
+
}, [identifier, builtinList, installedPluginList, isKlavisEnabledInEnv, allKlavisServers]);
|
|
116
|
+
|
|
117
|
+
// Fetch from remote if not found locally
|
|
118
|
+
const usePluginDetail = useDiscoverStore((s) => s.usePluginDetail);
|
|
119
|
+
const { data: remoteData, isLoading } = usePluginDetail({
|
|
120
|
+
identifier: !localMeta && !isInstalled ? identifier : undefined,
|
|
121
|
+
withManifest: false,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Determine final metadata
|
|
125
|
+
const meta = localMeta || {
|
|
126
|
+
avatar: remoteData?.avatar,
|
|
127
|
+
isInstalled: false,
|
|
128
|
+
title: remoteData?.title || identifier,
|
|
129
|
+
type: 'plugin' as const,
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const displayTitle = isLoading ? 'Loading...' : meta.title;
|
|
133
|
+
|
|
134
|
+
// Render icon based on type
|
|
135
|
+
const renderIcon = () => {
|
|
136
|
+
// Klavis type has icon property
|
|
137
|
+
if (meta.type === 'klavis' && 'icon' in meta && 'label' in meta) {
|
|
138
|
+
return <KlavisIcon icon={meta.icon} label={meta.label} />;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Builtin type has avatar
|
|
142
|
+
if (meta.type === 'builtin' && 'avatar' in meta && meta.avatar) {
|
|
143
|
+
return <Avatar avatar={meta.avatar} shape={'square'} size={16} style={{ flexShrink: 0 }} />;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Plugin type
|
|
147
|
+
if ('avatar' in meta) {
|
|
148
|
+
return <PluginAvatar avatar={meta.avatar} size={16} />;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return null;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
<Tag
|
|
156
|
+
className={isCompact ? styles.compact : styles.tag}
|
|
157
|
+
icon={renderIcon()}
|
|
158
|
+
variant={isCompact ? 'borderless' : isDarkMode ? 'filled' : 'outlined'}
|
|
159
|
+
>
|
|
160
|
+
{displayTitle}
|
|
161
|
+
</Tag>
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
ToolTag.displayName = 'ToolTag';
|
|
166
|
+
|
|
167
|
+
export default ToolTag;
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
type RecentTopicGroup,
|
|
4
4
|
type RecentTopicGroupMember,
|
|
5
5
|
} from '@lobechat/types';
|
|
6
|
+
import { cleanObject } from '@lobechat/utils';
|
|
6
7
|
import { eq, inArray } from 'drizzle-orm';
|
|
7
8
|
import { after } from 'next/server';
|
|
8
9
|
import { z } from 'zod';
|
|
@@ -410,8 +411,14 @@ export const topicRouter = router({
|
|
|
410
411
|
const agentId = topicAgentIdMap.get(topic.id);
|
|
411
412
|
const agentInfo = agentId ? agentInfoMap.get(agentId) : null;
|
|
412
413
|
|
|
414
|
+
// Clean agent info - if avatar/title are all null, return null
|
|
415
|
+
const cleanedAgent = agentInfo ? cleanObject(agentInfo) : null;
|
|
416
|
+
// Only return agent if it has meaningful display info (avatar or title)
|
|
417
|
+
const validAgent =
|
|
418
|
+
cleanedAgent && (cleanedAgent.avatar || cleanedAgent.title) ? cleanedAgent : null;
|
|
419
|
+
|
|
413
420
|
return {
|
|
414
|
-
agent:
|
|
421
|
+
agent: validAgent,
|
|
415
422
|
group: null,
|
|
416
423
|
id: topic.id,
|
|
417
424
|
title: topic.title,
|
|
@@ -448,7 +448,7 @@ describe('contextEngineering', () => {
|
|
|
448
448
|
];
|
|
449
449
|
|
|
450
450
|
// Mock topic memories and global identities separately
|
|
451
|
-
vi.spyOn(memoryManager, 'resolveTopicMemories').
|
|
451
|
+
vi.spyOn(memoryManager, 'resolveTopicMemories').mockReturnValue({
|
|
452
452
|
contexts: [
|
|
453
453
|
{
|
|
454
454
|
accessedAt: new Date('2024-01-01T00:00:00.000Z'),
|
|
@@ -252,12 +252,11 @@ export const contextEngineering = async ({
|
|
|
252
252
|
.map((kb) => ({ description: kb.description, id: kb.id, name: kb.name }));
|
|
253
253
|
|
|
254
254
|
// Resolve user memories: topic memories and global identities are independent layers
|
|
255
|
+
// Both functions now read from cache only (no network requests) to avoid blocking sendMessage
|
|
255
256
|
let userMemoryData;
|
|
256
257
|
if (enableUserMemories) {
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
Promise.resolve(resolveGlobalIdentities()),
|
|
260
|
-
]);
|
|
258
|
+
const topicMemories = resolveTopicMemories();
|
|
259
|
+
const globalIdentities = resolveGlobalIdentities();
|
|
261
260
|
userMemoryData = combineUserMemoryData(topicMemories, globalIdentities);
|
|
262
261
|
}
|
|
263
262
|
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import type { UserMemoryData, UserMemoryIdentityItem } from '@lobechat/context-engine';
|
|
2
2
|
import type { RetrieveMemoryResult } from '@lobechat/types';
|
|
3
3
|
|
|
4
|
-
import { mutate } from '@/libs/swr';
|
|
5
|
-
import { userMemoryService } from '@/services/userMemory';
|
|
6
4
|
import { getChatStoreState } from '@/store/chat';
|
|
7
|
-
import { getUserMemoryStoreState
|
|
5
|
+
import { getUserMemoryStoreState } from '@/store/userMemory';
|
|
8
6
|
import { agentMemorySelectors, identitySelectors } from '@/store/userMemory/selectors';
|
|
9
7
|
|
|
10
8
|
const EMPTY_MEMORIES: RetrieveMemoryResult = {
|
|
@@ -39,17 +37,13 @@ export interface TopicMemoryResolverContext {
|
|
|
39
37
|
}
|
|
40
38
|
|
|
41
39
|
/**
|
|
42
|
-
* Resolves topic-based memories (contexts, experiences, preferences)
|
|
40
|
+
* Resolves topic-based memories (contexts, experiences, preferences) from cache only.
|
|
43
41
|
*
|
|
44
|
-
* This function
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* 3. Fetching memories from the service if not cached
|
|
48
|
-
* 4. Caching the fetched memories by topic ID
|
|
42
|
+
* This function only reads from cache and does NOT trigger network requests.
|
|
43
|
+
* Memory data is pre-loaded by SWR in ChatList via useFetchTopicMemories hook.
|
|
44
|
+
* This ensures sendMessage is not blocked by memory retrieval network calls.
|
|
49
45
|
*/
|
|
50
|
-
export const resolveTopicMemories =
|
|
51
|
-
ctx?: TopicMemoryResolverContext,
|
|
52
|
-
): Promise<RetrieveMemoryResult> => {
|
|
46
|
+
export const resolveTopicMemories = (ctx?: TopicMemoryResolverContext): RetrieveMemoryResult => {
|
|
53
47
|
// Get topic ID from context or active topic
|
|
54
48
|
const topicId = ctx?.topicId ?? getChatStoreState().activeTopicId;
|
|
55
49
|
|
|
@@ -60,34 +54,11 @@ export const resolveTopicMemories = async (
|
|
|
60
54
|
|
|
61
55
|
const userMemoryStoreState = getUserMemoryStoreState();
|
|
62
56
|
|
|
63
|
-
//
|
|
57
|
+
// Only read from cache, do not trigger network request
|
|
58
|
+
// Memory data is pre-loaded by SWR in ChatList
|
|
64
59
|
const cachedMemories = agentMemorySelectors.topicMemories(topicId)(userMemoryStoreState);
|
|
65
60
|
|
|
66
|
-
|
|
67
|
-
return cachedMemories;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Fetch memories for this topic
|
|
71
|
-
try {
|
|
72
|
-
const result = await userMemoryService.retrieveMemoryForTopic(topicId);
|
|
73
|
-
const memories = result ?? EMPTY_MEMORIES;
|
|
74
|
-
|
|
75
|
-
// Cache the fetched memories by topic ID
|
|
76
|
-
useUserMemoryStore.setState((state) => ({
|
|
77
|
-
topicMemoriesMap: {
|
|
78
|
-
...state.topicMemoriesMap,
|
|
79
|
-
[topicId]: memories,
|
|
80
|
-
},
|
|
81
|
-
}));
|
|
82
|
-
|
|
83
|
-
// Also trigger SWR mutate to keep in sync
|
|
84
|
-
await mutate(['useFetchMemoriesForTopic', topicId]);
|
|
85
|
-
|
|
86
|
-
return memories;
|
|
87
|
-
} catch (error) {
|
|
88
|
-
console.error('Failed to retrieve memories for topic:', error);
|
|
89
|
-
return EMPTY_MEMORIES;
|
|
90
|
-
}
|
|
61
|
+
return cachedMemories ?? EMPTY_MEMORIES;
|
|
91
62
|
};
|
|
92
63
|
|
|
93
64
|
/**
|
|
@@ -4,7 +4,7 @@ import type { ParsedQuery } from 'query-string';
|
|
|
4
4
|
import type { ChatGroupItem } from '@/database/schemas/chatGroup';
|
|
5
5
|
|
|
6
6
|
export interface QueryRouter {
|
|
7
|
-
push: (url: string, options?: { query?: ParsedQuery }) => void;
|
|
7
|
+
push: (url: string, options?: { query?: ParsedQuery; replace?: boolean }) => void;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export interface ChatGroupState {
|