feat: manually mirror opencoze's code from bytedance

Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
fanlv
2025-07-20 17:36:12 +08:00
commit 890153324f
14811 changed files with 1923430 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
.hover-show-scrollbar {
&::-webkit-scrollbar-thumb {
background-color: transparent;
}
&:hover {
&::-webkit-scrollbar-thumb {
background-color: rgba(6, 7, 9, 20%);
}
}
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useEffect, useState } from 'react';
import { useEditor } from '@coze-editor/editor/react';
import { type EditorAPI } from '@coze-editor/editor/preset-prompt';
import { type ViewUpdate } from '@codemirror/view';
export const useReadonly = () => {
const editor = useEditor<EditorAPI>();
const [isReadOnly, setIsReadOnly] = useState(false);
useEffect(() => {
if (!editor) {
return;
}
setIsReadOnly(editor.$view.state.readOnly);
const handleViewUpdate = (update: ViewUpdate) => {
if (update.startState.readOnly !== update.state.readOnly) {
setIsReadOnly(update.state.readOnly);
}
};
editor.$on('viewUpdate', handleViewUpdate);
return () => {
editor.$off('viewUpdate', handleViewUpdate);
};
}, [editor]);
return isReadOnly;
};

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useState } from 'react';
import { localStorageService } from '@coze-foundation/local-storage';
const SESSION_HIDDEN_KEY = 'coze-promptkit-recommend-pannel-hidden-key';
export const useSetSessionVisiblePersist = (key: string) => {
const [isSessionVisible, setIsSessionVisible] = useState(isKeyExist(key));
return {
isSessionVisible,
toggleSessionVisible: (visible: boolean) => {
const oldValue = localStorageService.getValue(SESSION_HIDDEN_KEY) || '';
if (isKeyExist(key) && visible) {
return;
}
if (visible) {
localStorageService.setValue(
SESSION_HIDDEN_KEY,
oldValue ? `${oldValue},${key}` : key,
);
setIsSessionVisible(true);
return;
}
localStorageService.setValue(
SESSION_HIDDEN_KEY,
oldValue.replace(key, ''),
);
setIsSessionVisible(false);
},
};
};
const isKeyExist = (key: string) => {
const oldValue = localStorageService.getValue(SESSION_HIDDEN_KEY);
return oldValue?.includes(key);
};

View File

@@ -0,0 +1,25 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export {
createFreeGrabModalHierarchyStore,
type FreeGrabModalHierarchyStore,
} from './service/free-grab-modal-hierarchy-service/store';
export { FreeGrabModalHierarchyService } from './service/free-grab-modal-hierarchy-service';
export { getSelectionBoundary } from './utils/rect';
export { useReadonly } from './hooks/use-editor-readonly';
export { insertToNewline } from './utils/insert-to-newline';
export { type PromptContextInfo } from './types';

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type ModalHierarchyServiceConstructor } from './type';
import { type FreeGrabModalHierarchyAction } from './store';
export class FreeGrabModalHierarchyService {
/** Tip: semi modal zIndex 为 1000 */
private baseZIndex = 1000;
public registerModal: FreeGrabModalHierarchyAction['registerModal'];
public removeModal: FreeGrabModalHierarchyAction['removeModal'];
public onFocus: FreeGrabModalHierarchyAction['setModalToTopLayer'];
private getModalIndex: FreeGrabModalHierarchyAction['getModalIndex'];
constructor({
registerModal,
removeModal,
getModalIndex,
setModalToTopLayer,
}: ModalHierarchyServiceConstructor) {
this.registerModal = registerModal;
this.removeModal = removeModal;
this.getModalIndex = getModalIndex;
this.onFocus = setModalToTopLayer;
}
public getModalZIndex = (keyOrIndex: string | number) => {
if (typeof keyOrIndex === 'string') {
return this.getModalIndex(keyOrIndex) + this.baseZIndex;
}
return keyOrIndex + this.baseZIndex;
};
}

View File

@@ -0,0 +1,96 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { devtools } from 'zustand/middleware';
import { create } from 'zustand';
import { produce } from 'immer';
export interface FreeGrabModalHierarchyState {
// modal 的 key list
modalHierarchyList: string[];
}
export interface FreeGrabModalHierarchyAction {
registerModal: (key: string) => void;
removeModal: (key: string) => void;
getModalIndex: (key: string) => number;
setModalToTopLayer: (key: string) => void;
}
/**
* 可自由拖拽的弹窗之间的层级关系
*/
export const createFreeGrabModalHierarchyStore = () =>
create<FreeGrabModalHierarchyState & FreeGrabModalHierarchyAction>()(
devtools(
(set, get) => ({
modalHierarchyList: [],
getModalIndex: key =>
get().modalHierarchyList.findIndex(modalKey => modalKey === key),
registerModal: key => {
set(
{
modalHierarchyList: produce(get().modalHierarchyList, draft => {
draft.unshift(key);
}),
},
false,
'registerModal',
);
},
removeModal: key => {
set(
{
modalHierarchyList: produce(get().modalHierarchyList, draft => {
const index = get().getModalIndex(key);
if (index < 0) {
return;
}
draft.splice(index, 1);
}),
},
false,
'removeModal',
);
},
setModalToTopLayer: key => {
set(
{
modalHierarchyList: produce(get().modalHierarchyList, draft => {
const index = get().getModalIndex(key);
if (index < 0) {
return;
}
get().removeModal(key);
get().registerModal(key);
}),
},
false,
'setModalToTopLayer',
);
},
}),
{
enabled: IS_DEV_MODE,
name: 'botStudio.botEditor.ModalHierarchy',
},
),
);
export type FreeGrabModalHierarchyStore = ReturnType<
typeof createFreeGrabModalHierarchyStore
>;

View File

@@ -0,0 +1,24 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type FreeGrabModalHierarchyAction } from './store';
export interface ModalHierarchyServiceConstructor {
registerModal: FreeGrabModalHierarchyAction['registerModal'];
removeModal: FreeGrabModalHierarchyAction['removeModal'];
setModalToTopLayer: FreeGrabModalHierarchyAction['setModalToTopLayer'];
getModalIndex: FreeGrabModalHierarchyAction['getModalIndex'];
}

View File

@@ -0,0 +1,17 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { type PromptContextInfo } from './prompt';

View File

@@ -0,0 +1,22 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export interface PromptContextInfo {
botId?: string;
name?: string;
description?: string;
contextHistory?: string;
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type EditorAPI } from '@coze-editor/editor/preset-prompt';
export const insertToNewline = async ({
editor,
prompt,
}: {
editor?: EditorAPI;
prompt: string;
}): Promise<string> => {
if (!editor) {
return '';
}
const { state } = editor.$view;
const isDocEmpty = state.doc.length === 0;
const insertPrompt = isDocEmpty ? prompt : `\n${prompt}`;
const selection = isDocEmpty
? undefined
: {
anchor: state.doc.length,
head: state.doc.length + insertPrompt.length,
};
editor.$view.dispatch({
changes: {
from: state.doc.length,
to: state.doc.length,
insert: insertPrompt,
},
selection,
scrollIntoView: true,
});
// 等待下一个微任务周期,确保状态已更新
await Promise.resolve();
// 使用更新后的state获取最新文档内容
const newDoc = editor.$view.state.doc.toString();
// 插入到新一行
// 注意:该操作提前会触发 chrome bug 导致崩溃问题
editor.focus();
return newDoc;
};

View File

@@ -0,0 +1,63 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type EditorAPI } from '@coze-editor/editor/preset-prompt';
export const getSelectionBoundary = (editor: EditorAPI) => {
const rects = editor.getMainSelectionRects();
if (rects.length === 0) {
return { left: 0, top: 0, width: 0, height: 0 };
}
// 初始化最大矩形
let maxLeft = Infinity;
let maxTop = Infinity;
let maxRight = -Infinity;
let maxBottom = -Infinity;
// 遍历所有矩形,计算包围盒的边界
rects.forEach(rect => {
maxLeft = Math.min(maxLeft, rect.left);
maxTop = Math.min(maxTop, rect.top);
maxRight = Math.max(maxRight, rect.left + (rect.width ?? 0));
maxBottom = Math.max(maxBottom, rect.top + (rect.height ?? 0));
});
// 计算最终的宽度和高度
const width = maxRight - maxLeft;
const height = maxBottom - maxTop;
// 获取编辑器的滚动位置
const { scrollLeft } = editor.$view.scrollDOM;
const { scrollTop } = editor.$view.scrollDOM;
// 获取编辑器容器的位置
const editorRect = editor.$view.dom.getBoundingClientRect();
// 计算相对于视口的绝对位置
const absoluteLeft = editorRect.left + maxLeft - scrollLeft;
const absoluteTop = editorRect.top + maxTop - scrollTop;
const absoluteBottom = editorRect.top + maxBottom - scrollTop;
return {
left: absoluteLeft,
top: absoluteTop,
bottom: absoluteBottom,
width,
height,
};
};