Skip to content

Commit 4244e5e

Browse files
committed
Lexical @唤起BOT选择
1 parent d607d55 commit 4244e5e

File tree

26 files changed

+963
-66
lines changed

26 files changed

+963
-66
lines changed

src/app.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
llmProviderService,
2020
systemService,
2121
userService,
22+
workspaceService,
2223
} from '@/services';
2324
import type { Response } from '@/services/typings';
2425
import {
@@ -33,6 +34,7 @@ import { ACCESS_TOKEN, TOKEN_TYPE } from './constants';
3334
import { LLM } from './services/llm/typings';
3435
import { SYSTEM } from './services/system/typings';
3536
import { USER } from './services/user/typings';
37+
import { WORKSPACE } from './services/workspace/typings';
3638

3739
const loginPath = '/login';
3840
const ignoreAuthPath = ['/login', '/setup'];
@@ -49,6 +51,8 @@ export function render(oldRender: Function) {
4951
export async function getInitialState(): Promise<{
5052
// 当前登录账号
5153
userMe?: USER.UserEntity;
54+
// 我的工作空间
55+
mineWorkspace?: WORKSPACE.WorkspaceEntity;
5256
// 初始化状态
5357
setupStatus?: boolean;
5458
// 应用
@@ -67,6 +71,15 @@ export async function getInitialState(): Promise<{
6771
}
6872
};
6973

74+
const fetchMineWorkspace = async () => {
75+
try {
76+
const result = await workspaceService.mine();
77+
return result.content;
78+
} catch (ex) {
79+
return undefined;
80+
}
81+
};
82+
7083
const fetchSetupStatus = async () => {
7184
try {
7285
const result = await systemService.isSetup();
@@ -94,17 +107,19 @@ export async function getInitialState(): Promise<{
94107
}
95108
};
96109

97-
const [userMe, setupStatus, appInfo, providers] = await Promise.all<
110+
const [userMe, workspace, setupStatus, appInfo, providers] = await Promise.all<
98111
[
99112
Promise<USER.UserEntity | undefined>,
113+
Promise<WORKSPACE.WorkspaceEntity | undefined>,
100114
Promise<boolean | undefined>,
101115
Promise<SYSTEM.AppInfo | undefined>,
102116
Promise<LLM.ProviderSchema[] | undefined>,
103117
]
104-
>([fetchUserInfo(), fetchSetupStatus(), fetchAppInfo(), fetchProviders()]);
118+
>([fetchUserInfo(), fetchMineWorkspace(), fetchSetupStatus(), fetchAppInfo(), fetchProviders()]);
105119

106120
return {
107121
userMe: await userMe,
122+
mineWorkspace: await workspace,
108123
setupStatus: await setupStatus,
109124
appInfo: await appInfo,
110125
providers: await providers,

src/components/chat/chat-input/index.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,15 @@ const ChatInput: React.FC<{
113113
readOnly={loading}
114114
defaultValue=''
115115
showToolbar={false}
116-
onChange={(markdown) => setQuery(markdown)} />
116+
onChange={(markdown) => setQuery(markdown)}
117+
botMentionOptions={{
118+
enable: true,
119+
trigger: '@',
120+
onSelect: (botFavorite) => {
121+
console.log('===1===', botFavorite);
122+
},
123+
}}
124+
/>
117125
</div>
118126

119127
{/* 底部按钮 */}

src/components/chat/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ const ChatContent: React.FC<{
4141
}> = ({ workspaceUid, conversationUid: _conversationUid, className, withChatBackgroundImage=true, emptyNode }) => {
4242
const [messageApi, contextHolder] = message.useMessage();
4343

44-
const chatContentPopoverRef = useRef<ScrollToBottomBtnRefProperty>();
44+
const chatContentPopoverRef = useRef<ScrollToBottomBtnRefProperty>(null);
4545
const { initialState } = useModel('@@initialState');
4646

4747
// 当前会话

src/components/common/ScrollToBottomBtn.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@
1616

1717
import { Button } from 'antd';
1818
import { ArrowDownToLine } from 'lucide-react';
19-
import React, { forwardRef, useImperativeHandle } from 'react';
19+
import { forwardRef, useImperativeHandle } from 'react';
2020
import { useScrollToBottom, useSticky } from 'react-scroll-to-bottom';
2121

2222
export interface ScrollToBottomBtnRefProperty {
2323
trigScrollToBottom: () => void;
2424
}
2525

26-
const ScrollToBottomBtn: React.FC<{ className?: string }> = forwardRef(({ className }, ref) => {
26+
const ScrollToBottomBtn = forwardRef<ScrollToBottomBtnRefProperty, { className?: string }>(({ className }, ref) => {
2727
const scrollToBottom = useScrollToBottom();
2828
const [sticky] = useSticky();
2929

src/components/common/TabHeader.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@
1717
import { getPathAndModule } from '@/services/common';
1818
import { history, useLocation } from '@umijs/max';
1919
import { Tabs, TabsProps } from 'antd';
20+
import { TabsType } from 'antd/es/tabs';
2021
import { SizeType } from 'antd/lib/config-provider/SizeContext';
2122
import { useState } from 'react';
2223
import StickyBox from 'react-sticky-box';
2324

2425
const TabHeader: React.FC<{
2526
items: TabsProps['items'],
27+
type?: TabsType,
2628
centered?: boolean,
2729
size?: SizeType;
2830
sticky?: boolean,
@@ -32,7 +34,7 @@ const TabHeader: React.FC<{
3234
tabBarRender?: boolean,
3335
defaultActiveKey?: string;
3436
tabBarExtraContent?: Partial<Record<'left' | 'right', React.ReactNode>>
35-
}> = ({ items, centered, size, sticky, className, contentNoPpadding, locationUpdate, tabBarRender, defaultActiveKey, tabBarExtraContent }) => {
37+
}> = ({ items, type, centered, size, sticky, className, contentNoPpadding, locationUpdate, tabBarRender, defaultActiveKey, tabBarExtraContent }) => {
3638
const location = useLocation();
3739

3840
// 选择的菜单KEY
@@ -62,6 +64,7 @@ const TabHeader: React.FC<{
6264

6365
return (
6466
<Tabs
67+
type={type}
6568
centered={centered}
6669
size={size || "large"}
6770
defaultActiveKey={defaultActiveKey}

src/components/markdown/lexical/lexical-editor.tsx

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import { ContentEditable } from '@lexical/react/LexicalContentEditable';
2121
import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin';
2222
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary'
2323
import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin'
24-
import ToolbarPlugin from './plugins/ToolbarPlugin';
2524

2625
import { $convertToMarkdownString } from '@lexical/markdown';
2726
import { Typography } from 'antd';
@@ -49,27 +48,35 @@ import TabFocusPlugin from './plugins/TabFocusPlugin';
4948
import { TabIndentationPlugin } from '@lexical/react/LexicalTabIndentationPlugin';
5049
import { ClearEditorPlugin } from '@lexical/react/LexicalClearEditorPlugin';
5150
import { useSharedHistoryContext } from './context/SharedHistoryContext';
51+
import { USER_FAVORITE } from '@/services/user/favorite/typings';
5252

5353
export interface LexicalInnerEditorRefProperty {
5454
getEditor: () => LexicalEditor;
5555
getMarkdownContent: () => string;
5656
}
5757

58+
export type BotMentionOptions = {
59+
enable?: boolean,
60+
trigger?: string,
61+
onSelect?: (botFavorite: USER_FAVORITE.BotFavoriteEntity) => void;
62+
}
63+
5864
/**
5965
* 需要保证该组件无状态
6066
*/
61-
const LexicalInnerEditor: React.FC<{
67+
const LexicalInnerEditor = forwardRef<LexicalInnerEditorRefProperty, {
6268
placeholder?: string,
6369
showToolbar?: boolean,
6470
onChange?: (markdown: string) => void,
65-
}> = forwardRef(({ placeholder, showToolbar = true, onChange }, ref) => {
71+
botMentionOptions?: BotMentionOptions
72+
}>(({ placeholder, showToolbar = true, onChange, botMentionOptions }, ref) => {
6673
const [floatingAnchorElem, setFloatingAnchorElem] = useState<HTMLDivElement | null>(null);
6774
const [isSmallWidthViewport, setIsSmallWidthViewport] = useState<boolean>(false);
6875
const [editor] = useLexicalComposerContext();
6976
const [isLinkEditMode, setIsLinkEditMode] = useState<boolean>(false);
7077
const { historyState } = useSharedHistoryContext();
7178

72-
const [markdown, setMarkdown] = useState<string>();
79+
const [markdown, setMarkdown] = useState<string>('');
7380

7481
useImperativeHandle(ref, () => ({
7582
getEditor() {
@@ -105,14 +112,15 @@ const LexicalInnerEditor: React.FC<{
105112

106113
return <>
107114
{/* 工具栏 */}
108-
{showToolbar &&
109-
<ToolbarPlugin
115+
{showToolbar && (() => {
116+
const ToolbarPlugin = require('./plugins/ToolbarPlugin').default;
117+
return <ToolbarPlugin
110118
editor={editor}
111119
activeEditor={editor}
112120
setActiveEditor={() => { }}
113121
setIsLinkEditMode={setIsLinkEditMode}
114-
/>
115-
}
122+
/>;
123+
})()}
116124

117125
{/* 键盘事件监听 */}
118126
{/* <KeyboardEventPlugin editor={editor} onKeyboardEvent={(event: KeyboardEvent) => {
@@ -177,7 +185,16 @@ const LexicalInnerEditor: React.FC<{
177185
<SelectionAlwaysOnDisplay />
178186

179187
{/* 斜杠青年 */}
180-
<ComponentPickerPlugin trigger="/" />
188+
<ComponentPickerPlugin />
189+
190+
{/* @BOT */}
191+
{botMentionOptions?.enable && (() => {
192+
const BotMentionsPlugin = require('./plugins/BotMentionsPlugin').default;
193+
return <BotMentionsPlugin
194+
trigger={botMentionOptions.trigger}
195+
onSelect={botMentionOptions.onSelect}
196+
/>;
197+
})()}
181198

182199
{floatingAnchorElem && !isSmallWidthViewport && (<>
183200
{/* 代码块工具 */}

src/components/markdown/lexical/lexical-textarea.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { TRANSFORMERS } from './plugins/ExtTransformers';
2424
import { ToolbarContext } from './context/ToolbarContext';
2525
import { SharedHistoryContext } from './context/SharedHistoryContext';
2626
import { TableContext } from './plugins/TablePlugin';
27-
import LexicalInnerEditor, { LexicalInnerEditorRefProperty } from './lexical-editor';
27+
import LexicalInnerEditor, { BotMentionOptions, LexicalInnerEditorRefProperty } from './lexical-editor';
2828
import nodes from './nodes';
2929
import {
3030
CLEAR_EDITOR_COMMAND,
@@ -46,8 +46,9 @@ const LexicalTextarea: React.FC<{
4646
defaultValue?: string,
4747
showToolbar?: boolean,
4848
onChange?: (markdown: string) => void,
49-
}> = forwardRef(({ placeholder, readOnly, defaultValue, showToolbar, onChange }, ref) => {
50-
const lexicalInnerEditorPopoverRef = useRef<LexicalInnerEditorRefProperty>();
49+
botMentionOptions?: BotMentionOptions,
50+
}> = forwardRef(({ placeholder, readOnly, defaultValue, showToolbar, onChange, botMentionOptions }, ref) => {
51+
const lexicalInnerEditorPopoverRef = useRef<LexicalInnerEditorRefProperty>(null);
5152

5253
useImperativeHandle(ref, () => ({
5354
clearEditor() {
@@ -108,7 +109,8 @@ const LexicalTextarea: React.FC<{
108109
ref={lexicalInnerEditorPopoverRef}
109110
placeholder={placeholder}
110111
showToolbar={showToolbar}
111-
onChange={onChange} />
112+
onChange={onChange}
113+
botMentionOptions={botMentionOptions} />
112114
</ToolbarContext>
113115
</TableContext>
114116
</SharedHistoryContext>
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
*/
8+
9+
import {
10+
$applyNodeReplacement,
11+
type DOMConversionMap,
12+
type DOMConversionOutput,
13+
type DOMExportOutput,
14+
type EditorConfig,
15+
type LexicalNode,
16+
type NodeKey,
17+
type SerializedTextNode,
18+
type Spread,
19+
TextNode,
20+
} from 'lexical';
21+
22+
export type SerializedMentionNode = Spread<
23+
{
24+
mentionName: string;
25+
},
26+
SerializedTextNode
27+
>;
28+
29+
function $convertMentionElement(
30+
domNode: HTMLElement,
31+
): DOMConversionOutput | null {
32+
const textContent = domNode.textContent;
33+
34+
if (textContent !== null) {
35+
const node = $createMentionNode(textContent);
36+
return {
37+
node,
38+
};
39+
}
40+
41+
return null;
42+
}
43+
44+
const mentionStyle = 'background-color: rgba(24, 119, 232, 0.2)';
45+
export class MentionNode extends TextNode {
46+
__mention: string;
47+
48+
static getType(): string {
49+
return 'mention';
50+
}
51+
52+
static clone(node: MentionNode): MentionNode {
53+
return new MentionNode(node.__mention, node.__text, node.__key);
54+
}
55+
static importJSON(serializedNode: SerializedMentionNode): MentionNode {
56+
const node = $createMentionNode(serializedNode.mentionName);
57+
node.setTextContent(serializedNode.text);
58+
node.setFormat(serializedNode.format);
59+
node.setDetail(serializedNode.detail);
60+
node.setMode(serializedNode.mode);
61+
node.setStyle(serializedNode.style);
62+
return node;
63+
}
64+
65+
constructor(mentionName: string, text?: string, key?: NodeKey) {
66+
super(text ?? mentionName, key);
67+
this.__mention = mentionName;
68+
}
69+
70+
exportJSON(): SerializedMentionNode {
71+
return {
72+
...super.exportJSON(),
73+
mentionName: this.__mention,
74+
type: 'mention',
75+
version: 1,
76+
};
77+
}
78+
79+
createDOM(config: EditorConfig): HTMLElement {
80+
const dom = super.createDOM(config);
81+
dom.style.cssText = mentionStyle;
82+
dom.className = 'mention';
83+
dom.spellcheck = false;
84+
return dom;
85+
}
86+
87+
exportDOM(): DOMExportOutput {
88+
const element = document.createElement('span');
89+
element.setAttribute('data-lexical-mention', 'true');
90+
element.textContent = this.__text;
91+
return {element};
92+
}
93+
94+
static importDOM(): DOMConversionMap | null {
95+
return {
96+
span: (domNode: HTMLElement) => {
97+
if (!domNode.hasAttribute('data-lexical-mention')) {
98+
return null;
99+
}
100+
return {
101+
conversion: $convertMentionElement,
102+
priority: 1,
103+
};
104+
},
105+
};
106+
}
107+
108+
isTextEntity(): true {
109+
return true;
110+
}
111+
112+
canInsertTextBefore(): boolean {
113+
return false;
114+
}
115+
116+
canInsertTextAfter(): boolean {
117+
return false;
118+
}
119+
}
120+
121+
export function $createMentionNode(mentionName: string): MentionNode {
122+
const mentionNode = new MentionNode(mentionName);
123+
mentionNode.setMode('segmented').toggleDirectionless();
124+
return $applyNodeReplacement(mentionNode);
125+
}
126+
127+
export function $isMentionNode(
128+
node: LexicalNode | null | undefined,
129+
): node is MentionNode {
130+
return node instanceof MentionNode;
131+
}

0 commit comments

Comments
 (0)