feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* 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 { messageSource } from '@coze-common/chat-area';
|
||||
|
||||
export const FILTER_MESSAGE_SOURCE: number[] = [messageSource.Notice];
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* 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 { nanoid } from 'nanoid';
|
||||
import mitt from 'mitt';
|
||||
import { type PluginRegistryEntry } from '@coze-common/chat-area';
|
||||
|
||||
import {
|
||||
type EventCenter,
|
||||
type GrabPluginBizContext,
|
||||
type PublicEventCenter,
|
||||
type EventCallbacks,
|
||||
} from './types/plugin-biz-context';
|
||||
import { createSelectionStore } from './stores/selection';
|
||||
import { createQuoteStore, subscribeQuoteUpdate } from './stores/quote';
|
||||
import { createPreferenceStore } from './stores/preference';
|
||||
import { ChatAreaGrabPlugin } from './plugin';
|
||||
|
||||
interface Preference {
|
||||
enableGrab: boolean;
|
||||
}
|
||||
|
||||
export type Scene = 'store' | 'other';
|
||||
|
||||
type CreateGrabPluginParams = {
|
||||
preference: Preference;
|
||||
scene?: Scene;
|
||||
} & EventCallbacks;
|
||||
|
||||
export const publicEventCenter = mitt<PublicEventCenter>();
|
||||
|
||||
export const createGrabPlugin = (params: CreateGrabPluginParams) => {
|
||||
const { preference, onQuote, onQuoteChange, scene } = params;
|
||||
|
||||
const grabPluginId = nanoid();
|
||||
|
||||
const grabPlugin: PluginRegistryEntry<GrabPluginBizContext> = {
|
||||
createPluginBizContext: () => {
|
||||
const eventCallbacks = {
|
||||
onQuote,
|
||||
onQuoteChange,
|
||||
};
|
||||
|
||||
const storeSet = {
|
||||
useSelectionStore: createSelectionStore('plugin'),
|
||||
useQuoteStore: createQuoteStore('plugin'),
|
||||
usePreferenceStore: createPreferenceStore('plugin'),
|
||||
};
|
||||
|
||||
const eventCenter = mitt<EventCenter>();
|
||||
|
||||
// 默认注入preference
|
||||
storeSet.usePreferenceStore
|
||||
.getState()
|
||||
.updateEnableGrab(preference.enableGrab);
|
||||
|
||||
const unsubscribeQuoteStore = subscribeQuoteUpdate(
|
||||
{
|
||||
useQuoteStore: storeSet.useQuoteStore,
|
||||
},
|
||||
eventCallbacks,
|
||||
);
|
||||
|
||||
const ctx = {
|
||||
grabPluginId,
|
||||
storeSet,
|
||||
eventCallbacks,
|
||||
unsubscribe: () => {
|
||||
unsubscribeQuoteStore();
|
||||
},
|
||||
eventCenter,
|
||||
publicEventCenter,
|
||||
scene,
|
||||
};
|
||||
|
||||
return ctx;
|
||||
},
|
||||
Plugin: ChatAreaGrabPlugin,
|
||||
};
|
||||
|
||||
return { grabPlugin, grabPluginId };
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
.close {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
font-size: 12px;
|
||||
|
||||
:global {
|
||||
.semi-button.semi-button-with-icon-only {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
|
||||
@apply coz-fg-secondary;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 { useEffect, type FC } from 'react';
|
||||
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { PluginName, useWriteablePlugin } from '@coze-common/chat-area';
|
||||
import { IconCozCross, IconCozQuotation } from '@coze-arch/coze-design/icons';
|
||||
import { IconButton } from '@coze-arch/coze-design';
|
||||
|
||||
import { QuoteNode } from '../quote-node';
|
||||
import { type GrabPluginBizContext } from '../../types/plugin-biz-context';
|
||||
|
||||
type IProps = Record<string, unknown>;
|
||||
|
||||
export const QuoteInputAddonTop: FC<IProps> = () => {
|
||||
const { pluginBizContext, chatAreaPluginContext } =
|
||||
useWriteablePlugin<GrabPluginBizContext>(PluginName.MessageGrab);
|
||||
|
||||
const { useChatInputLayout } = chatAreaPluginContext.readonlyHook.input;
|
||||
|
||||
const { useQuoteStore } = pluginBizContext.storeSet;
|
||||
|
||||
const { quoteVisible, quoteContent, updateQuoteVisible, updateQuoteContent } =
|
||||
useQuoteStore(
|
||||
useShallow(state => ({
|
||||
quoteVisible: state.quoteVisible,
|
||||
quoteContent: state.quoteContent,
|
||||
updateQuoteVisible: state.updateQuoteVisible,
|
||||
updateQuoteContent: state.updateQuoteContent,
|
||||
})),
|
||||
);
|
||||
|
||||
const handleClose = () => {
|
||||
updateQuoteContent(null);
|
||||
updateQuoteVisible(false);
|
||||
};
|
||||
|
||||
const { layoutContainerRef } = useChatInputLayout();
|
||||
|
||||
useEffect(() => {
|
||||
if (!layoutContainerRef?.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleStopPropagation = (e: PointerEvent) => e.stopPropagation();
|
||||
|
||||
layoutContainerRef.current.addEventListener(
|
||||
'pointerup',
|
||||
handleStopPropagation,
|
||||
);
|
||||
|
||||
return () => {
|
||||
layoutContainerRef.current?.removeEventListener(
|
||||
'pointerup',
|
||||
handleStopPropagation,
|
||||
);
|
||||
};
|
||||
}, [layoutContainerRef?.current]);
|
||||
|
||||
if (!quoteContent || !quoteVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full h-[32px] flex items-center px-[16px] coz-mg-primary">
|
||||
<IconCozQuotation className="coz-fg-secondary mr-[8px] w-[12px] h-[12px]" />
|
||||
<div className="flex flex-row items-center flex-1">
|
||||
<div className="coz-fg-secondary flex-1 min-w-0 w-0 truncate text-[12px] leading-[16px]">
|
||||
<QuoteNode nodeList={quoteContent} theme="black" />
|
||||
</div>
|
||||
<IconButton
|
||||
icon={<IconCozCross className="w-[14px] h-[14px]" />}
|
||||
onClick={handleClose}
|
||||
color="secondary"
|
||||
size="small"
|
||||
className="!rounded-[4px]"
|
||||
wrapperClass="flex item-center justify-center"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
.container-box-shadow {
|
||||
z-index: 1;
|
||||
box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 8%), 0 4px 12px 0 rgba(0, 0, 0, 4%);
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* 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 ReactNode,
|
||||
forwardRef,
|
||||
type CSSProperties,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
type MutableRefObject,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { useEventCallback } from '@coze-common/chat-hooks';
|
||||
import { PluginName, useWriteablePlugin } from '@coze-common/chat-area';
|
||||
|
||||
import {
|
||||
EventNames,
|
||||
type GrabPluginBizContext,
|
||||
} from '../../types/plugin-biz-context';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
interface MenuListProps {
|
||||
style?: CSSProperties;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
onScroll?: () => void;
|
||||
onMouseEnter?: () => void;
|
||||
onMouseLeave?: () => void;
|
||||
}
|
||||
|
||||
export interface MenuListRef {
|
||||
getRef: () => MutableRefObject<HTMLDivElement | null>;
|
||||
refreshOpacity: () => void;
|
||||
}
|
||||
|
||||
const GRADIENT_RANGE = 100;
|
||||
|
||||
export const MenuList = forwardRef<MenuListRef, MenuListProps>((props, ref) => {
|
||||
const plugin = useWriteablePlugin<GrabPluginBizContext>(
|
||||
PluginName.MessageGrab,
|
||||
);
|
||||
|
||||
const { useGetScrollView } =
|
||||
plugin.chatAreaPluginContext.readonlyHook.scrollView;
|
||||
|
||||
const { on, off } = plugin.pluginBizContext.eventCenter;
|
||||
const { style, children, onScroll, onMouseEnter, onMouseLeave, className } =
|
||||
props;
|
||||
|
||||
const getScrollView = useGetScrollView();
|
||||
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [opacity, setOpacity] = useState(1);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
getRef: () => menuRef,
|
||||
refreshOpacity,
|
||||
}));
|
||||
|
||||
const refreshOpacity = useEventCallback(() => {
|
||||
onScroll?.();
|
||||
|
||||
const localRect = menuRef.current?.getBoundingClientRect();
|
||||
|
||||
const { rect: scrollRect } = getScrollView().getOriginScrollInfo();
|
||||
|
||||
if (!scrollRect || !localRect) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (localRect.y - GRADIENT_RANGE <= scrollRect.top) {
|
||||
const _opacity = (localRect.y - scrollRect.top) / GRADIENT_RANGE;
|
||||
setOpacity(_opacity < 0 ? 0 : _opacity);
|
||||
} else if (localRect.y + GRADIENT_RANGE >= scrollRect.bottom) {
|
||||
const _opacity = (scrollRect.bottom - localRect.y) / GRADIENT_RANGE;
|
||||
setOpacity(_opacity < 0 ? 0 : _opacity);
|
||||
} else {
|
||||
setOpacity(1);
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
on(EventNames.OnMessageUpdate, refreshOpacity);
|
||||
return () => {
|
||||
off(EventNames.OnMessageUpdate, refreshOpacity);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'fixed p-[2px] coz-bg-max coz-stroke-primary border-[0.5px] rounded-[8px] overflow-hidden grid grid-flow-col gap-[2px] h-[28px]',
|
||||
s['container-box-shadow'],
|
||||
className,
|
||||
)}
|
||||
style={{
|
||||
...style,
|
||||
opacity,
|
||||
}}
|
||||
ref={menuRef}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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 { useShallow } from 'zustand/react/shallow';
|
||||
import {
|
||||
PluginName,
|
||||
useWriteablePlugin,
|
||||
type CustomTextMessageInnerTopSlot,
|
||||
} from '@coze-common/chat-area';
|
||||
|
||||
import { type GrabPluginBizContext } from '../../types/plugin-biz-context';
|
||||
import { RemoteQuoteInnerTopSlot } from './remote-slot';
|
||||
import { LocalQuoteInnerTopSlot } from './local-slot';
|
||||
|
||||
export const QuoteMessageInnerTopSlot: CustomTextMessageInnerTopSlot = ({
|
||||
message,
|
||||
}) => {
|
||||
const localMessageId = message.extra_info.local_message_id;
|
||||
|
||||
const plugin = useWriteablePlugin<GrabPluginBizContext>(
|
||||
PluginName.MessageGrab,
|
||||
);
|
||||
|
||||
const { useQuoteStore } = plugin.pluginBizContext.storeSet;
|
||||
|
||||
// 优先用本地映射的
|
||||
const hasLocal = useQuoteStore(
|
||||
useShallow(state => !!state.quoteContentMap[localMessageId]),
|
||||
);
|
||||
|
||||
if (hasLocal) {
|
||||
return <LocalQuoteInnerTopSlot message={message} />;
|
||||
}
|
||||
|
||||
return <RemoteQuoteInnerTopSlot message={message} />;
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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 { useShallow } from 'zustand/react/shallow';
|
||||
import {
|
||||
PluginName,
|
||||
useWriteablePlugin,
|
||||
type CustomTextMessageInnerTopSlot,
|
||||
} from '@coze-common/chat-area';
|
||||
|
||||
import { QuoteNode } from '../quote-node';
|
||||
import { type GrabPluginBizContext } from '../../types/plugin-biz-context';
|
||||
import { QuoteTopUI } from './quote-top-ui';
|
||||
|
||||
export const LocalQuoteInnerTopSlot: CustomTextMessageInnerTopSlot = ({
|
||||
message,
|
||||
}) => {
|
||||
const localMessageId = message.extra_info.local_message_id;
|
||||
|
||||
const plugin = useWriteablePlugin<GrabPluginBizContext>(
|
||||
PluginName.MessageGrab,
|
||||
);
|
||||
|
||||
const { useQuoteStore } = plugin.pluginBizContext.storeSet;
|
||||
|
||||
// 优先用本地映射的
|
||||
const localNodeList = useQuoteStore(
|
||||
useShallow(state => state.quoteContentMap[localMessageId]),
|
||||
);
|
||||
|
||||
if (localNodeList) {
|
||||
return (
|
||||
<QuoteTopUI>
|
||||
<QuoteNode nodeList={localNodeList} theme="white" />
|
||||
</QuoteTopUI>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* 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 FC, type PropsWithChildren } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { useShowBackGround } from '@coze-common/chat-area';
|
||||
import { IconCozQuotation } from '@coze-arch/coze-design/icons';
|
||||
|
||||
import { typeSafeQuoteNodeColorVariants } from '../variants';
|
||||
|
||||
export const QuoteTopUI: FC<PropsWithChildren> = ({ children }) => {
|
||||
const showBackground = useShowBackGround();
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
['h-auto', 'py-4px'],
|
||||
'flex flex-row items-center select-none w-m-0',
|
||||
)}
|
||||
>
|
||||
<IconCozQuotation
|
||||
className={classNames(
|
||||
typeSafeQuoteNodeColorVariants({ showBackground }),
|
||||
'mr-[8px] shrink-0 w-[12px] h-[12px]',
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
className={classNames('flex-1 min-w-0 truncate text-[12px]', [
|
||||
'leading-[16px]',
|
||||
typeSafeQuoteNodeColorVariants({ showBackground }),
|
||||
])}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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 { parseMarkdownToGrabNode } from '@coze-common/text-grab';
|
||||
import {
|
||||
ContentType,
|
||||
type CustomTextMessageInnerTopSlot,
|
||||
} from '@coze-common/chat-area';
|
||||
|
||||
import { QuoteNode } from '../quote-node';
|
||||
import { getReferFromMessage } from '../../utils/get-refer-from-message';
|
||||
import { QuoteTopUI } from './quote-top-ui';
|
||||
|
||||
export const RemoteQuoteInnerTopSlot: CustomTextMessageInnerTopSlot = ({
|
||||
message,
|
||||
}) => {
|
||||
// 本地没有用服务端下发的
|
||||
const refer = getReferFromMessage(message);
|
||||
|
||||
if (!refer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (refer.type === ContentType.Image) {
|
||||
return (
|
||||
<QuoteTopUI>
|
||||
<img className="w-[24px] h-[24px] rounded-[4px]" src={refer.url} />
|
||||
</QuoteTopUI>
|
||||
);
|
||||
}
|
||||
|
||||
// 尝试解析ast
|
||||
const nodeList = parseMarkdownToGrabNode(refer.text);
|
||||
|
||||
return (
|
||||
<QuoteTopUI>
|
||||
<QuoteNode nodeList={nodeList} theme="white" />
|
||||
</QuoteTopUI>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
* 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 } from 'react';
|
||||
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { defer } from 'lodash-es';
|
||||
import classNames from 'classnames';
|
||||
import { isTouchDevice } from '@coze-common/text-grab';
|
||||
import {
|
||||
CONTENT_ATTRIBUTE_NAME,
|
||||
GrabElementType,
|
||||
type GrabNode,
|
||||
} from '@coze-common/text-grab';
|
||||
import {
|
||||
PluginName,
|
||||
useWriteablePlugin,
|
||||
type MessageListFloatSlot,
|
||||
} from '@coze-common/chat-area';
|
||||
|
||||
import { QuoteButton } from '../quote-button';
|
||||
import { MenuList } from '../menu-list';
|
||||
import { getMessage } from '../../utils/get-message';
|
||||
import {
|
||||
EventNames,
|
||||
type GrabPluginBizContext,
|
||||
} from '../../types/plugin-biz-context';
|
||||
import { useHideQuote } from '../../hooks/use-hide-quote';
|
||||
import { useFloatMenuListener } from '../../hooks/use-float-menu-listener';
|
||||
import { useAutoGetMaxPosition } from '../../hooks/use-auto-get-max-position';
|
||||
|
||||
export const FloatMenu: MessageListFloatSlot = () => {
|
||||
const plugin = useWriteablePlugin<GrabPluginBizContext>(
|
||||
PluginName.MessageGrab,
|
||||
);
|
||||
const { chatAreaPluginContext, pluginBizContext } = plugin;
|
||||
const { eventCenter } = pluginBizContext;
|
||||
const { usePreferenceStore } = pluginBizContext.storeSet;
|
||||
const enableGrab = usePreferenceStore(state => state.enableGrab);
|
||||
|
||||
const { on, off } = eventCenter;
|
||||
|
||||
const { useQuoteStore } = pluginBizContext.storeSet;
|
||||
const { updateQuoteContent, updateQuoteVisible } = useQuoteStore(
|
||||
useShallow(state => ({
|
||||
updateQuoteVisible: state.updateQuoteVisible,
|
||||
updateQuoteContent: state.updateQuoteContent,
|
||||
})),
|
||||
);
|
||||
|
||||
const {
|
||||
handleMenuMouseEnter,
|
||||
handleMenuMouseLeave,
|
||||
targetElement,
|
||||
targetInfo,
|
||||
position,
|
||||
floatMenuRef,
|
||||
visible,
|
||||
setVisible,
|
||||
computePosition,
|
||||
isScrolling,
|
||||
} = useFloatMenuListener({
|
||||
plugin,
|
||||
});
|
||||
|
||||
const { targetRef: messageRef, forceHidden } = useHideQuote<HTMLElement>({
|
||||
containerRef: targetElement,
|
||||
});
|
||||
|
||||
const { maxPositionX } = useAutoGetMaxPosition({
|
||||
messageRef,
|
||||
position,
|
||||
floatMenuRef,
|
||||
});
|
||||
|
||||
const isGrabMenuVisible = plugin.pluginBizContext.storeSet.useSelectionStore(
|
||||
state => state.isFloatMenuVisible && !!state.floatMenuPosition,
|
||||
);
|
||||
|
||||
const handleQuoteClick = () => {
|
||||
const { source = '', type = 'link', text = '' } = targetInfo.current ?? {};
|
||||
const target = messageRef.current;
|
||||
const messageId = target?.attributes.getNamedItem(
|
||||
CONTENT_ATTRIBUTE_NAME,
|
||||
)?.value;
|
||||
|
||||
if (!messageId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const message = getMessage({
|
||||
messageId,
|
||||
chatAreaPluginContext,
|
||||
});
|
||||
|
||||
const node: GrabNode =
|
||||
type === 'image'
|
||||
? {
|
||||
type: GrabElementType.IMAGE,
|
||||
src: source,
|
||||
children: [
|
||||
{
|
||||
text: source,
|
||||
},
|
||||
],
|
||||
}
|
||||
: {
|
||||
type: GrabElementType.LINK,
|
||||
url: source,
|
||||
children: [
|
||||
{
|
||||
text,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
updateQuoteContent([node]);
|
||||
updateQuoteVisible(true);
|
||||
pluginBizContext.eventCallbacks.onQuote?.({
|
||||
botId: message?.sender_id ?? '',
|
||||
source: message?.source,
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
on(EventNames.OnMessageUpdate, computePosition);
|
||||
on(EventNames.OnViewScroll, computePosition);
|
||||
|
||||
return () => {
|
||||
off(EventNames.OnMessageUpdate, computePosition);
|
||||
off(EventNames.OnViewScroll, computePosition);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!enableGrab || isTouchDevice()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const top = position?.y;
|
||||
const left =
|
||||
(position?.x ?? 0) > (maxPositionX ?? 0) ? maxPositionX : position?.x;
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
<MenuList
|
||||
ref={floatMenuRef}
|
||||
className={classNames({
|
||||
hidden: !visible || forceHidden || isGrabMenuVisible || isScrolling,
|
||||
})}
|
||||
style={{
|
||||
top,
|
||||
left,
|
||||
}}
|
||||
onMouseEnter={handleMenuMouseEnter}
|
||||
onMouseLeave={handleMenuMouseLeave}
|
||||
>
|
||||
<QuoteButton
|
||||
onClick={handleQuoteClick}
|
||||
onClose={() => {
|
||||
defer(() => setVisible(false));
|
||||
}}
|
||||
/>
|
||||
</MenuList>
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
* 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, useRef } from 'react';
|
||||
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { defer } from 'lodash-es';
|
||||
import classNames from 'classnames';
|
||||
import { type GrabPosition, type SelectionData } from '@coze-common/text-grab';
|
||||
import { useGrab } from '@coze-common/text-grab';
|
||||
import { NO_MESSAGE_ID_MARK } from '@coze-common/chat-uikit';
|
||||
import {
|
||||
PluginName,
|
||||
useWriteablePlugin,
|
||||
type MessageListFloatSlot,
|
||||
} from '@coze-common/chat-area';
|
||||
|
||||
import { QuoteButton } from '../quote-button';
|
||||
import { MenuList, type MenuListRef } from '../menu-list';
|
||||
import {
|
||||
EventNames,
|
||||
type GrabPluginBizContext,
|
||||
} from '../../types/plugin-biz-context';
|
||||
import { FILTER_MESSAGE_SOURCE } from '../../constants/filter-message';
|
||||
|
||||
export const GrabMenu: MessageListFloatSlot = ({ contentRef }) => {
|
||||
// 获取插件实例
|
||||
const plugin = useWriteablePlugin<GrabPluginBizContext>(
|
||||
PluginName.MessageGrab,
|
||||
);
|
||||
|
||||
// 获取插件的业务上下文信息
|
||||
const { pluginBizContext } = plugin;
|
||||
const { eventCenter, storeSet } = pluginBizContext;
|
||||
const { usePreferenceStore, useSelectionStore } = storeSet;
|
||||
|
||||
// 事件中心监听函数
|
||||
const { on, off } = eventCenter;
|
||||
|
||||
/**
|
||||
* Float Menu 的 Ref
|
||||
*/
|
||||
const floatMenuRef = useRef<MenuListRef>(null);
|
||||
|
||||
const { isFloatMenuVisible, position, updateFloatMenuPosition } =
|
||||
useSelectionStore(
|
||||
useShallow(state => ({
|
||||
isFloatMenuVisible: state.isFloatMenuVisible,
|
||||
position: state.floatMenuPosition,
|
||||
updateFloatMenuPosition: state.updateFloatMenuPosition,
|
||||
})),
|
||||
);
|
||||
|
||||
/**
|
||||
* 是否启用 Grab 插件
|
||||
*/
|
||||
const enableGrab = usePreferenceStore(state => state.enableGrab);
|
||||
|
||||
/**
|
||||
* 处理位置信息的回调
|
||||
*/
|
||||
const handlePositionChange = (_position: GrabPosition | null) => {
|
||||
// 更新浮动菜单在 Store 中的数据
|
||||
updateFloatMenuPosition(_position);
|
||||
|
||||
// 清空栈后调用刷新的逻辑 (因为 floatMenu 在 hidden 状态下 通过 ref 拿到的 rect 信息是空,所以需要延迟一会儿获取)
|
||||
defer(() => floatMenuRef.current?.refreshOpacity());
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理选区的变化
|
||||
*/
|
||||
const handleSelectChange = (selectionData: SelectionData | null) => {
|
||||
const {
|
||||
updateHumanizedContentText,
|
||||
updateNormalizeSelectionNodeList,
|
||||
updateOriginContentText,
|
||||
updateSelectionData,
|
||||
updateIsFloatMenuVisible,
|
||||
} = plugin.pluginBizContext.storeSet.useSelectionStore.getState();
|
||||
|
||||
/**
|
||||
* 过滤特殊类型的消息
|
||||
* 目前只有通过消息来源进行判断 messageSource
|
||||
* 1. 通知类型 (禁止选择)
|
||||
*/
|
||||
if (
|
||||
selectionData &&
|
||||
FILTER_MESSAGE_SOURCE.includes(selectionData.messageSource)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新选区数据
|
||||
updateSelectionData(selectionData);
|
||||
|
||||
// 更新展示浮动菜单的状态
|
||||
updateIsFloatMenuVisible(!!selectionData);
|
||||
|
||||
// 拿取 MessageId
|
||||
const messageId = selectionData?.ancestorAttributeValue;
|
||||
|
||||
// 判断是否是特殊消息
|
||||
const isSpecialMessage = messageId === NO_MESSAGE_ID_MARK;
|
||||
|
||||
// 如果拿不到消息 ID,就证明选区不在消息哪
|
||||
if (!messageId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 特殊处理,需要判断是否是回复中的消息
|
||||
const { is_finish } =
|
||||
plugin.chatAreaPluginContext.readonlyAPI.message.findMessage(messageId) ??
|
||||
{};
|
||||
|
||||
// 如果消息回复没完成 并且 不是一个特殊的消息 那么就不展示,否则展示
|
||||
if (!is_finish && !isSpecialMessage) {
|
||||
updateIsFloatMenuVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
updateHumanizedContentText(selectionData?.humanizedContentText ?? '');
|
||||
updateNormalizeSelectionNodeList(
|
||||
selectionData?.normalizeSelectionNodeList ?? [],
|
||||
);
|
||||
updateOriginContentText(selectionData?.originContentText ?? '');
|
||||
};
|
||||
|
||||
const { clearSelection, isScrolling, computePosition } = useGrab({
|
||||
contentRef,
|
||||
floatMenuRef: floatMenuRef.current?.getRef(),
|
||||
onSelectChange: handleSelectChange,
|
||||
onPositionChange: handlePositionChange,
|
||||
});
|
||||
|
||||
const isShowFloatMenu = !!position && isFloatMenuVisible;
|
||||
|
||||
useEffect(() => {
|
||||
on(EventNames.OnMessageUpdate, computePosition);
|
||||
on(EventNames.OnViewScroll, computePosition);
|
||||
|
||||
return () => {
|
||||
off(EventNames.OnMessageUpdate, computePosition);
|
||||
off(EventNames.OnViewScroll, computePosition);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 如果没有开启 Grab 插件,那么就隐藏了当然
|
||||
if (!enableGrab) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<MenuList
|
||||
ref={floatMenuRef}
|
||||
style={{
|
||||
top: position?.y,
|
||||
left: position?.x,
|
||||
}}
|
||||
className={classNames({
|
||||
hidden: !isShowFloatMenu || isScrolling,
|
||||
})}
|
||||
>
|
||||
<QuoteButton onClose={clearSelection} />
|
||||
</MenuList>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* 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 MessageListFloatSlot } from '@coze-common/chat-area';
|
||||
|
||||
import { GrabMenu } from './grab-menu';
|
||||
import { FloatMenu } from './float-menu';
|
||||
|
||||
export const MessageListFloat: MessageListFloatSlot = props => (
|
||||
<>
|
||||
<GrabMenu {...props} />
|
||||
<FloatMenu {...props} />
|
||||
</>
|
||||
);
|
||||
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* 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 FC } from 'react';
|
||||
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { PluginName, useWriteablePlugin } from '@coze-common/chat-area';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozQuotation } from '@coze-arch/coze-design/icons';
|
||||
import { IconButton, Tooltip } from '@coze-arch/coze-design';
|
||||
|
||||
import { getMessage } from '../../utils/get-message';
|
||||
import { type GrabPluginBizContext } from '../../types/plugin-biz-context';
|
||||
|
||||
export interface QuoteButtonProps {
|
||||
onClose?: () => void;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export const QuoteButton: FC<QuoteButtonProps> = ({ onClose, onClick }) => {
|
||||
const { pluginBizContext, chatAreaPluginContext } =
|
||||
useWriteablePlugin<GrabPluginBizContext>(PluginName.MessageGrab);
|
||||
|
||||
const { onQuote } = pluginBizContext.eventCallbacks;
|
||||
|
||||
const { useQuoteStore, useSelectionStore, usePreferenceStore } =
|
||||
pluginBizContext.storeSet;
|
||||
|
||||
const { updateQuoteContent, updateQuoteVisible } = useQuoteStore(
|
||||
useShallow(state => ({
|
||||
updateQuoteVisible: state.updateQuoteVisible,
|
||||
updateQuoteContent: state.updateQuoteContent,
|
||||
})),
|
||||
);
|
||||
|
||||
const enableGrab = usePreferenceStore(state => state.enableGrab);
|
||||
|
||||
const { useDeleteFile } = chatAreaPluginContext.writeableHook.file;
|
||||
const { getFileStoreInstantValues } =
|
||||
chatAreaPluginContext.readonlyAPI.batchFile;
|
||||
|
||||
const deleteFile = useDeleteFile();
|
||||
|
||||
const deleteAllFile = () => {
|
||||
const { fileIdList } = getFileStoreInstantValues();
|
||||
|
||||
fileIdList.forEach(id => deleteFile(id));
|
||||
};
|
||||
|
||||
const getMessageInfo = () => {
|
||||
const { selectionData } = useSelectionStore.getState();
|
||||
|
||||
const messageId = selectionData?.ancestorAttributeValue;
|
||||
|
||||
if (!messageId) {
|
||||
return;
|
||||
}
|
||||
|
||||
return getMessage({ messageId, chatAreaPluginContext });
|
||||
};
|
||||
|
||||
const handleClick = () => {
|
||||
deleteAllFile();
|
||||
|
||||
if (onClick) {
|
||||
onClick();
|
||||
onClose?.();
|
||||
return;
|
||||
}
|
||||
|
||||
const { normalizeSelectionNodeList } = useSelectionStore.getState();
|
||||
|
||||
updateQuoteContent(normalizeSelectionNodeList);
|
||||
|
||||
updateQuoteVisible(true);
|
||||
|
||||
const message = getMessageInfo();
|
||||
|
||||
onQuote?.({ botId: message?.sender_id ?? '', source: message?.source });
|
||||
|
||||
onClose?.();
|
||||
};
|
||||
|
||||
if (!enableGrab) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip content={I18n.t('quote_ask_in_chat')} clickToHide={true}>
|
||||
<IconButton
|
||||
icon={<IconCozQuotation className="text-lg coz-fg-secondary" />}
|
||||
color="secondary"
|
||||
onClick={handleClick}
|
||||
size="small"
|
||||
wrapperClass="flex justify-center items-center"
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
.image {
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* 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, type FC } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
isGrabTextNode,
|
||||
isGrabLink,
|
||||
isGrabImage,
|
||||
} from '@coze-common/text-grab';
|
||||
import { type GrabNode } from '@coze-common/text-grab';
|
||||
import { useShowBackGround } from '@coze-common/chat-area';
|
||||
import { Image, ImagePreview } from '@coze-arch/coze-design';
|
||||
|
||||
import { typeSafeQuoteNodeColorVariants } from '../variants';
|
||||
import DefaultImage from '../../assets/image-default.png';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
const getQuoteNode = ({
|
||||
nodeList,
|
||||
theme,
|
||||
handleClickImage,
|
||||
showBackground,
|
||||
}: {
|
||||
nodeList: GrabNode[];
|
||||
theme: 'white' | 'black';
|
||||
handleClickImage: (url: string) => void;
|
||||
showBackground: boolean;
|
||||
}): React.ReactNode =>
|
||||
nodeList.map((node, index) => {
|
||||
if (isGrabTextNode(node)) {
|
||||
return (
|
||||
<span
|
||||
className={classNames('text-[14px] align-middle', 'leading-[16px]', {
|
||||
'coz-fg-secondary': theme === 'black',
|
||||
[typeSafeQuoteNodeColorVariants({ showBackground })]:
|
||||
theme === 'white',
|
||||
})}
|
||||
key={index}
|
||||
>
|
||||
{node.text}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (isGrabImage(node)) {
|
||||
return (
|
||||
<Image
|
||||
className={classNames(
|
||||
styles.image,
|
||||
'w-[24px] h-[24px] leading-[24px] align-middle rounded-[4px] cursor-zoom-in',
|
||||
{
|
||||
'mx-[4px]': index !== 0,
|
||||
'mr-[4px]': index === 0,
|
||||
},
|
||||
)}
|
||||
src={node.src}
|
||||
onClick={() => handleClickImage(node.src)}
|
||||
key={index}
|
||||
fallback={DefaultImage}
|
||||
preview={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isGrabLink(node)) {
|
||||
return (
|
||||
<span
|
||||
className={classNames('text-[14px] align-middle', 'leading-[16px]', {
|
||||
'coz-fg-secondary': theme === 'black',
|
||||
[typeSafeQuoteNodeColorVariants({ showBackground })]:
|
||||
theme === 'white',
|
||||
})}
|
||||
key={index}
|
||||
>
|
||||
[
|
||||
{getQuoteNode({
|
||||
nodeList: node.children,
|
||||
theme,
|
||||
handleClickImage,
|
||||
showBackground,
|
||||
})}
|
||||
]
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
export const QuoteNode: FC<{
|
||||
nodeList: GrabNode[];
|
||||
theme: 'white' | 'black';
|
||||
}> = ({ nodeList, theme }) => {
|
||||
const [previewUrl, setPreviewUrl] = useState('');
|
||||
const showBackground = useShowBackGround();
|
||||
const handleClickImage = (url: string) => {
|
||||
setPreviewUrl(url);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ImagePreview
|
||||
src={previewUrl}
|
||||
visible={Boolean(previewUrl)}
|
||||
onVisibleChange={() => setPreviewUrl('')}
|
||||
/>
|
||||
{getQuoteNode({
|
||||
nodeList,
|
||||
theme,
|
||||
handleClickImage,
|
||||
showBackground,
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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 { cva, type VariantProps } from 'class-variance-authority';
|
||||
|
||||
const quoteNodeColorVariants = cva([], {
|
||||
variants: {
|
||||
showBackground: {
|
||||
true: ['text-[#FFFFFF/60]'],
|
||||
false: ['coz-fg-secondary'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const typeSafeQuoteNodeColorVariants: (
|
||||
props: Required<VariantProps<typeof quoteNodeColorVariants>>,
|
||||
) => string = quoteNodeColorVariants;
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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 MutableRefObject,
|
||||
useEffect,
|
||||
useState,
|
||||
type RefObject,
|
||||
} from 'react';
|
||||
|
||||
import { defer } from 'lodash-es';
|
||||
import { type GrabPosition } from '@coze-common/text-grab';
|
||||
|
||||
import { type MenuListRef } from '../custom-components/menu-list';
|
||||
|
||||
export const useAutoGetMaxPosition = ({
|
||||
position,
|
||||
messageRef,
|
||||
floatMenuRef,
|
||||
}: {
|
||||
position: GrabPosition | null;
|
||||
messageRef: MutableRefObject<Element | null>;
|
||||
floatMenuRef: RefObject<MenuListRef>;
|
||||
}) => {
|
||||
const [maxPositionX, setMaxPositionX] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const maxX = messageRef.current?.getBoundingClientRect().right ?? 0;
|
||||
setMaxPositionX(maxX);
|
||||
defer(() => floatMenuRef.current?.refreshOpacity());
|
||||
}, [position, messageRef.current, floatMenuRef.current]);
|
||||
|
||||
return { maxPositionX };
|
||||
};
|
||||
@@ -0,0 +1,240 @@
|
||||
/*
|
||||
* 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, useRef, useState } from 'react';
|
||||
|
||||
import { defer, delay } from 'lodash-es';
|
||||
import {
|
||||
CONTENT_ATTRIBUTE_NAME,
|
||||
getAncestorAttributeNode,
|
||||
type GrabPosition,
|
||||
} from '@coze-common/text-grab';
|
||||
import { NO_MESSAGE_ID_MARK } from '@coze-common/chat-uikit';
|
||||
import { useEventCallback } from '@coze-common/chat-hooks';
|
||||
import {
|
||||
type OnLinkElementContext,
|
||||
type useWriteablePlugin,
|
||||
} from '@coze-common/chat-area';
|
||||
|
||||
import { getMouseNearbyRect } from '../utils/get-mouse-nearby-rect';
|
||||
import {
|
||||
EventNames,
|
||||
type GrabPluginBizContext,
|
||||
} from '../types/plugin-biz-context';
|
||||
import { type MenuListRef } from '../custom-components/menu-list';
|
||||
|
||||
const DELAY_DISAPPEAR_TIME = 100;
|
||||
const TIMEOUT = 100;
|
||||
// eslint-disable-next-line max-lines-per-function, @coze-arch/max-line-per-function
|
||||
export const useFloatMenuListener = ({
|
||||
plugin,
|
||||
}: {
|
||||
plugin: ReturnType<typeof useWriteablePlugin<GrabPluginBizContext>>;
|
||||
}) => {
|
||||
const floatMenuRef = useRef<MenuListRef>(null);
|
||||
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
const targetElement = useRef<HTMLElement | null>(null);
|
||||
const targetInfo = useRef<{
|
||||
source: string;
|
||||
type: 'image' | 'link';
|
||||
text: string;
|
||||
} | null>(null);
|
||||
|
||||
/**
|
||||
* 是否在 Scrolling 中
|
||||
*/
|
||||
const [isScrolling, setIsScrolling] = useState(false);
|
||||
|
||||
/**
|
||||
* Scrolling 计时器
|
||||
*/
|
||||
const scrollingTimer = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const { pluginBizContext } = plugin;
|
||||
const { eventCenter } = pluginBizContext;
|
||||
const { on, off } = eventCenter;
|
||||
|
||||
const isMouseInMenu = useRef(false);
|
||||
const [position, setPosition] = useState<GrabPosition | null>(null);
|
||||
const timer = useRef<number | null>(null);
|
||||
|
||||
const mouseInfo = useRef<GrabPosition>({ x: 0, y: 0 });
|
||||
|
||||
const handleMenuMouseEnter = useEventCallback(() => {
|
||||
isMouseInMenu.current = true;
|
||||
if (timer.current) {
|
||||
clearTimeout(timer.current);
|
||||
}
|
||||
});
|
||||
|
||||
const handleMenuMouseLeave = useEventCallback(() => {
|
||||
isMouseInMenu.current = false;
|
||||
handleCardLinkElementMouseLeave();
|
||||
});
|
||||
|
||||
const isMessageFinished = () => {
|
||||
const target = getAncestorAttributeNode(
|
||||
targetElement.current,
|
||||
CONTENT_ATTRIBUTE_NAME,
|
||||
);
|
||||
|
||||
const messageId = target?.attributes.getNamedItem(
|
||||
CONTENT_ATTRIBUTE_NAME,
|
||||
)?.value;
|
||||
|
||||
const isSpecialMessage = messageId === NO_MESSAGE_ID_MARK;
|
||||
|
||||
if (isSpecialMessage) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!messageId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { is_finish } =
|
||||
plugin.chatAreaPluginContext.readonlyAPI.message.findMessage(messageId) ??
|
||||
{};
|
||||
|
||||
return is_finish;
|
||||
};
|
||||
|
||||
const handleCardLinkElementMouseEnter = useEventCallback(
|
||||
(ctx: OnLinkElementContext & { type: 'link' | 'image' }) => {
|
||||
if (timer.current) {
|
||||
clearTimeout(timer.current);
|
||||
timer.current = null;
|
||||
}
|
||||
targetElement.current = ctx.element;
|
||||
targetInfo.current = {
|
||||
source: ctx.link,
|
||||
type: ctx.type,
|
||||
text: ctx.element.textContent ?? '',
|
||||
};
|
||||
|
||||
const isFinished = isMessageFinished();
|
||||
|
||||
if (!isFinished) {
|
||||
return;
|
||||
}
|
||||
|
||||
setVisible(true);
|
||||
handleViewScroll();
|
||||
|
||||
defer(() => {
|
||||
floatMenuRef.current?.refreshOpacity();
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const handleCardLinkElementMouseLeave = useEventCallback(() => {
|
||||
timer.current = delay(() => {
|
||||
if (isMouseInMenu.current) {
|
||||
return;
|
||||
}
|
||||
targetElement.current = null;
|
||||
setVisible(false);
|
||||
}, DELAY_DISAPPEAR_TIME);
|
||||
});
|
||||
|
||||
const handleViewScroll = useEventCallback(() => {
|
||||
const menuRef = floatMenuRef.current?.getRef();
|
||||
|
||||
if (
|
||||
!targetElement.current ||
|
||||
!menuRef ||
|
||||
!menuRef.current ||
|
||||
!targetInfo.current
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = targetElement.current;
|
||||
const targetRect = target.getBoundingClientRect();
|
||||
const menuRect = menuRef.current.getBoundingClientRect();
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
if (targetInfo.current.type === 'image') {
|
||||
x = targetRect.right;
|
||||
y = targetRect.bottom - targetRect.height / 2 - menuRect.height / 2;
|
||||
} else {
|
||||
const targetRects = target.getClientRects();
|
||||
|
||||
const nearbyRect =
|
||||
getMouseNearbyRect(Array.from(targetRects), mouseInfo.current) ??
|
||||
targetRect;
|
||||
|
||||
x = mouseInfo.current.x;
|
||||
y = nearbyRect.bottom;
|
||||
}
|
||||
|
||||
setPosition({ x, y });
|
||||
});
|
||||
|
||||
const handleMouseMove = useEventCallback((event: MouseEvent) => {
|
||||
const [x, y] = [event.clientX, event.clientY];
|
||||
mouseInfo.current = { x, y };
|
||||
});
|
||||
|
||||
const handleSmartScreenChange = useEventCallback(() => {
|
||||
if (scrollingTimer.current) {
|
||||
clearTimeout(scrollingTimer.current);
|
||||
}
|
||||
|
||||
setIsScrolling(true);
|
||||
scrollingTimer.current = setTimeout(() => {
|
||||
handleViewScroll();
|
||||
setIsScrolling(false);
|
||||
}, TIMEOUT);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
on(EventNames.OnLinkElementMouseEnter, handleCardLinkElementMouseEnter);
|
||||
on(EventNames.OnLinkElementMouseLeave, handleCardLinkElementMouseLeave);
|
||||
on(EventNames.OnViewScroll, handleSmartScreenChange);
|
||||
on(EventNames.OnMessageUpdate, handleSmartScreenChange);
|
||||
window.addEventListener('mousemove', handleMouseMove);
|
||||
window.addEventListener('resize', handleSmartScreenChange);
|
||||
window.addEventListener('wheel', handleSmartScreenChange);
|
||||
window.addEventListener('scroll', handleSmartScreenChange);
|
||||
|
||||
return () => {
|
||||
off(EventNames.OnLinkElementMouseEnter, handleCardLinkElementMouseEnter);
|
||||
off(EventNames.OnLinkElementMouseLeave, handleCardLinkElementMouseLeave);
|
||||
off(EventNames.OnViewScroll, handleSmartScreenChange);
|
||||
off(EventNames.OnMessageUpdate, handleSmartScreenChange);
|
||||
window.removeEventListener('mousemove', handleMouseMove);
|
||||
window.removeEventListener('resize', handleSmartScreenChange);
|
||||
window.removeEventListener('wheel', handleSmartScreenChange);
|
||||
window.removeEventListener('scroll', handleSmartScreenChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
handleMenuMouseEnter,
|
||||
handleMenuMouseLeave,
|
||||
targetElement,
|
||||
targetInfo,
|
||||
position,
|
||||
floatMenuRef,
|
||||
visible,
|
||||
setVisible,
|
||||
computePosition: handleSmartScreenChange,
|
||||
isScrolling,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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 { useMemo, useState } from 'react';
|
||||
|
||||
import { type MessageSource } from '@coze-common/chat-area';
|
||||
|
||||
import { type Scene, createGrabPlugin } from '../create';
|
||||
|
||||
interface Params {
|
||||
onQuote?: ({
|
||||
botId,
|
||||
source,
|
||||
}: {
|
||||
botId: string;
|
||||
source: MessageSource | undefined;
|
||||
}) => void;
|
||||
// 目前只需要区分出 store 的场景
|
||||
scene?: Scene;
|
||||
}
|
||||
export const useCreateGrabPlugin = (params?: Params) => {
|
||||
const { onQuote, scene = 'other' } = params ?? {};
|
||||
const [grabEnableUpload, setGrabEnableUpload] = useState(true);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- 符合预期的命名
|
||||
const { grabPlugin: GrabPlugin, grabPluginId } = useMemo(
|
||||
() =>
|
||||
createGrabPlugin({
|
||||
preference: {
|
||||
enableGrab: true,
|
||||
},
|
||||
onQuote,
|
||||
onQuoteChange: ({ isEmpty }) => {
|
||||
setGrabEnableUpload(isEmpty);
|
||||
},
|
||||
scene,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
return { grabEnableUpload, GrabPlugin, grabPluginId };
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* 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 MutableRefObject, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import {
|
||||
CONTENT_ATTRIBUTE_NAME,
|
||||
MESSAGE_SOURCE_ATTRIBUTE_NAME,
|
||||
getAncestorAttributeNode,
|
||||
} from '@coze-common/text-grab';
|
||||
|
||||
import { FILTER_MESSAGE_SOURCE } from '../constants/filter-message';
|
||||
|
||||
interface HideQuoteProps<T> {
|
||||
containerRef?: MutableRefObject<T | null>;
|
||||
}
|
||||
|
||||
export const useHideQuote = <T extends Element>(props?: HideQuoteProps<T>) => {
|
||||
const containerRef = useRef<T | null>(null);
|
||||
const targetRef = useRef<Element | null>(null);
|
||||
|
||||
const usedContainerRef = props?.containerRef?.current
|
||||
? props.containerRef
|
||||
: containerRef;
|
||||
|
||||
const [forceHidden, setForceHidden] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const target = getAncestorAttributeNode(
|
||||
usedContainerRef.current,
|
||||
CONTENT_ATTRIBUTE_NAME,
|
||||
);
|
||||
|
||||
const messageSource = target?.attributes.getNamedItem(
|
||||
MESSAGE_SOURCE_ATTRIBUTE_NAME,
|
||||
)?.value;
|
||||
|
||||
if (FILTER_MESSAGE_SOURCE.includes(Number(messageSource))) {
|
||||
setForceHidden(true);
|
||||
}
|
||||
|
||||
targetRef.current = target;
|
||||
|
||||
return () => {
|
||||
setForceHidden(false);
|
||||
targetRef.current = null;
|
||||
};
|
||||
}, [usedContainerRef.current]);
|
||||
|
||||
return { targetRef, containerRef: usedContainerRef, forceHidden };
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* 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 ComponentType } from 'react';
|
||||
|
||||
import {
|
||||
type GrabPluginBizContext,
|
||||
type PublicEventCenter,
|
||||
PublicEventNames,
|
||||
} from './types/plugin-biz-context';
|
||||
|
||||
export {
|
||||
GrabNode,
|
||||
GrabElement,
|
||||
GrabElementType,
|
||||
GrabImageElement,
|
||||
GrabLinkElement,
|
||||
GrabPosition,
|
||||
GrabText,
|
||||
isGrabImage,
|
||||
isGrabLink,
|
||||
isGrabTextNode,
|
||||
} from '@coze-common/text-grab';
|
||||
|
||||
export type CustomFloatMenu = ComponentType<{
|
||||
grabBizContext: GrabPluginBizContext;
|
||||
}>;
|
||||
|
||||
export { GrabPluginBizContext };
|
||||
|
||||
export { GrabPublicMethod } from './types/public-methods';
|
||||
|
||||
export { PublicEventNames, PublicEventCenter };
|
||||
|
||||
export { useCreateGrabPlugin } from './hooks/use-grab-plugin';
|
||||
|
||||
export { publicEventCenter } from './create';
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* 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 {
|
||||
PluginMode,
|
||||
PluginName,
|
||||
WriteableChatAreaPlugin,
|
||||
createCustomComponents,
|
||||
} from '@coze-common/chat-area';
|
||||
|
||||
import { type GrabPublicMethod } from './types/public-methods';
|
||||
import { type GrabPluginBizContext } from './types/plugin-biz-context';
|
||||
import { GrabMessageLifeCycleService } from './services/life-cycle/message';
|
||||
import { GrabCommandLifeCycleService } from './services/life-cycle/command';
|
||||
import { GrabAppLifeCycleService } from './services/life-cycle/app';
|
||||
import { MessageListFloat } from './custom-components/message-list-float-slot';
|
||||
import { QuoteMessageInnerTopSlot } from './custom-components/message-inner-top-slot';
|
||||
import { QuoteInputAddonTop } from './custom-components/input-addon-top';
|
||||
|
||||
export class ChatAreaGrabPlugin extends WriteableChatAreaPlugin<
|
||||
GrabPluginBizContext,
|
||||
GrabPublicMethod
|
||||
> {
|
||||
public pluginMode = PluginMode.Writeable;
|
||||
public pluginName = PluginName.MessageGrab;
|
||||
|
||||
public customComponents = createCustomComponents({
|
||||
MessageListFloatSlot: MessageListFloat,
|
||||
TextMessageInnerTopSlot: QuoteMessageInnerTopSlot,
|
||||
InputAddonTop: QuoteInputAddonTop,
|
||||
});
|
||||
|
||||
public lifeCycleServices = {
|
||||
appLifeCycleService: new GrabAppLifeCycleService(this),
|
||||
messageLifeCycleService: new GrabMessageLifeCycleService(this),
|
||||
commandLifeCycleService: new GrabCommandLifeCycleService(this),
|
||||
};
|
||||
|
||||
public publicMethods: GrabPublicMethod = {
|
||||
updateEnableGrab: (enable: boolean) => {
|
||||
if (!this.pluginBizContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { updateEnableGrab } =
|
||||
this.pluginBizContext.storeSet.usePreferenceStore.getState();
|
||||
|
||||
updateEnableGrab(enable);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* 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 GrabNode } from '@coze-common/text-grab';
|
||||
import { WriteableAppLifeCycleService } from '@coze-common/chat-area';
|
||||
|
||||
import {
|
||||
PublicEventNames,
|
||||
type GrabPluginBizContext,
|
||||
} from '../../types/plugin-biz-context';
|
||||
|
||||
export class GrabAppLifeCycleService extends WriteableAppLifeCycleService<GrabPluginBizContext> {
|
||||
onBeforeDestroy(): void {
|
||||
const { unsubscribe, eventCenter, publicEventCenter, scene } =
|
||||
this.pluginInstance.pluginBizContext;
|
||||
|
||||
// Store 历史逻辑有一些问题,导致调用了 多次 destroy 但未初始化的情况,就不走下面的强制清理流程,而是走组件生命周期销毁
|
||||
if (scene === 'store') {
|
||||
return;
|
||||
}
|
||||
|
||||
unsubscribe();
|
||||
eventCenter.all.clear();
|
||||
publicEventCenter.all.clear();
|
||||
}
|
||||
onBeforeInitial(): void {
|
||||
const {
|
||||
publicEventCenter,
|
||||
grabPluginId: currentGrabPluginId,
|
||||
storeSet,
|
||||
} = this.pluginInstance.pluginBizContext;
|
||||
|
||||
const { useQuoteStore } = storeSet;
|
||||
|
||||
const { updateQuoteContent, updateQuoteVisible } = useQuoteStore.getState();
|
||||
|
||||
publicEventCenter.on(
|
||||
PublicEventNames.UpdateQuote,
|
||||
({
|
||||
grabPluginId,
|
||||
quote,
|
||||
}: {
|
||||
grabPluginId: string;
|
||||
quote: GrabNode[] | null;
|
||||
}) => {
|
||||
if (currentGrabPluginId !== grabPluginId) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateQuoteContent(quote);
|
||||
updateQuoteVisible(true);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* 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 OnImageElementContext,
|
||||
WriteableCommandLifeCycleService,
|
||||
type OnLinkElementContext,
|
||||
} from '@coze-common/chat-area';
|
||||
|
||||
import {
|
||||
EventNames,
|
||||
type GrabPluginBizContext,
|
||||
} from '../../types/plugin-biz-context';
|
||||
|
||||
export class GrabCommandLifeCycleService extends WriteableCommandLifeCycleService<GrabPluginBizContext> {
|
||||
onViewScroll(): void {
|
||||
const { emit } = this.pluginInstance.pluginBizContext.eventCenter;
|
||||
emit(EventNames.OnViewScroll);
|
||||
}
|
||||
onCardLinkElementMouseEnter(ctx: OnLinkElementContext) {
|
||||
const { emit } = this.pluginInstance.pluginBizContext.eventCenter;
|
||||
emit(EventNames.OnLinkElementMouseEnter, {
|
||||
...ctx,
|
||||
type: 'image',
|
||||
});
|
||||
}
|
||||
onCardLinkElementMouseLeave(ctx: OnLinkElementContext) {
|
||||
const { emit } = this.pluginInstance.pluginBizContext.eventCenter;
|
||||
emit(EventNames.OnLinkElementMouseLeave, {
|
||||
...ctx,
|
||||
type: 'image',
|
||||
});
|
||||
}
|
||||
onMdBoxImageElementMouseEnter(ctx: OnImageElementContext): void {
|
||||
const { emit } = this.pluginInstance.pluginBizContext.eventCenter;
|
||||
emit(EventNames.OnLinkElementMouseEnter, {
|
||||
...ctx,
|
||||
type: 'image',
|
||||
});
|
||||
}
|
||||
onMdBoxImageElementMouseLeave(ctx: OnImageElementContext): void {
|
||||
const { emit } = this.pluginInstance.pluginBizContext.eventCenter;
|
||||
emit(EventNames.OnLinkElementMouseLeave, {
|
||||
...ctx,
|
||||
type: 'image',
|
||||
});
|
||||
}
|
||||
onMdBoxLinkElementMouseEnter(ctx: OnLinkElementContext): void {
|
||||
const { emit } = this.pluginInstance.pluginBizContext.eventCenter;
|
||||
emit(EventNames.OnLinkElementMouseEnter, {
|
||||
...ctx,
|
||||
type: 'link',
|
||||
});
|
||||
}
|
||||
onMdBoxLinkElementMouseLeave(ctx: OnLinkElementContext): void {
|
||||
const { emit } = this.pluginInstance.pluginBizContext.eventCenter;
|
||||
emit(EventNames.OnLinkElementMouseLeave, {
|
||||
...ctx,
|
||||
type: 'link',
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* 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 { getOriginContentText } from '@coze-common/text-grab';
|
||||
import {
|
||||
ContentType,
|
||||
type OnBeforeAppendSenderMessageIntoStore,
|
||||
WriteableMessageLifeCycleService,
|
||||
type OnAfterAppendSenderMessageIntoStore,
|
||||
} from '@coze-common/chat-area';
|
||||
|
||||
import {
|
||||
EventNames,
|
||||
type GrabPluginBizContext,
|
||||
} from '../../types/plugin-biz-context';
|
||||
|
||||
export class GrabMessageLifeCycleService extends WriteableMessageLifeCycleService<GrabPluginBizContext> {
|
||||
onBeforeAppendSenderMessageIntoStore(
|
||||
ctx: OnBeforeAppendSenderMessageIntoStore,
|
||||
) {
|
||||
const { quoteContent, updateQuoteContentMapByImmer } =
|
||||
this.pluginInstance.pluginBizContext.storeSet.useQuoteStore.getState();
|
||||
|
||||
if (!quoteContent || ctx.from === 'shortcut') {
|
||||
return ctx;
|
||||
}
|
||||
|
||||
const originMessage = ctx.message;
|
||||
|
||||
const newContent = {
|
||||
item_list: [
|
||||
{
|
||||
type: 'text',
|
||||
text: originMessage.content,
|
||||
},
|
||||
],
|
||||
refer_items: [] as unknown[],
|
||||
};
|
||||
|
||||
newContent.refer_items.push({
|
||||
type: ContentType.Text,
|
||||
text: getOriginContentText(quoteContent),
|
||||
});
|
||||
|
||||
const newMessage = {
|
||||
...originMessage,
|
||||
content_type: ContentType.Mix,
|
||||
content: JSON.stringify(newContent),
|
||||
content_obj: newContent,
|
||||
};
|
||||
|
||||
updateQuoteContentMapByImmer(quoteContentMap => {
|
||||
const localMessageId = newMessage.extra_info.local_message_id;
|
||||
if (!quoteContentMap[localMessageId]) {
|
||||
quoteContentMap[localMessageId] = quoteContent;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
...ctx,
|
||||
message: newMessage,
|
||||
};
|
||||
}
|
||||
|
||||
onAfterAppendSenderMessageIntoStore(
|
||||
ctx: OnAfterAppendSenderMessageIntoStore,
|
||||
) {
|
||||
if (ctx.from === 'shortcut') {
|
||||
return;
|
||||
}
|
||||
|
||||
const { storeSet } = this.pluginInstance.pluginBizContext;
|
||||
const { updateQuoteVisible, updateQuoteContent } =
|
||||
storeSet.useQuoteStore.getState();
|
||||
updateQuoteVisible(false);
|
||||
updateQuoteContent(null);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
onAfterProcessReceiveMessage() {
|
||||
const { emit } = this.pluginInstance.pluginBizContext.eventCenter;
|
||||
emit(EventNames.OnMessageUpdate);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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, subscribeWithSelector } from 'zustand/middleware';
|
||||
import { create } from 'zustand';
|
||||
|
||||
export interface PreferenceState {
|
||||
enableGrab: boolean;
|
||||
}
|
||||
|
||||
export interface PreferenceAction {
|
||||
updateEnableGrab: (enable: boolean) => void;
|
||||
}
|
||||
|
||||
export const createPreferenceStore = (mark: string) => {
|
||||
const usePreferenceStore = create<PreferenceState & PreferenceAction>()(
|
||||
devtools(
|
||||
subscribeWithSelector(set => ({
|
||||
enableGrab: false,
|
||||
updateEnableGrab: enable => {
|
||||
set({
|
||||
enableGrab: enable,
|
||||
});
|
||||
},
|
||||
})),
|
||||
{
|
||||
name: `botStudio.ChatAreaGrabPlugin.Preference.${mark}`,
|
||||
enabled: IS_DEV_MODE,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
return usePreferenceStore;
|
||||
};
|
||||
|
||||
export type PreferenceStore = ReturnType<typeof createPreferenceStore>;
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* 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, subscribeWithSelector } from 'zustand/middleware';
|
||||
import { create } from 'zustand';
|
||||
import { produce } from 'immer';
|
||||
import { type GrabNode } from '@coze-common/text-grab';
|
||||
|
||||
import { type EventCallbacks } from '../types/plugin-biz-context';
|
||||
|
||||
export interface QuoteState {
|
||||
quoteContent: GrabNode[] | null;
|
||||
quoteVisible: boolean;
|
||||
quoteContentMap: Record<string, GrabNode[]>;
|
||||
}
|
||||
|
||||
export interface QuoteAction {
|
||||
updateQuoteContent: (quote: GrabNode[] | null) => void;
|
||||
updateQuoteContentMapByImmer: (
|
||||
updater: (quoteContentMap: Record<string, GrabNode[]>) => void,
|
||||
) => void;
|
||||
updateQuoteVisible: (visible: boolean) => void;
|
||||
clearStore: () => void;
|
||||
}
|
||||
|
||||
export const createQuoteStore = (mark: string) => {
|
||||
const useQuoteStore = create<QuoteState & QuoteAction>()(
|
||||
devtools(
|
||||
subscribeWithSelector(set => ({
|
||||
quoteContent: null,
|
||||
quoteVisible: false,
|
||||
quoteContentMap: {},
|
||||
updateQuoteContent: quote => {
|
||||
set({
|
||||
quoteContent: quote,
|
||||
});
|
||||
},
|
||||
updateQuoteContentMapByImmer: updater => {
|
||||
set(produce<QuoteState>(state => updater(state.quoteContentMap)));
|
||||
},
|
||||
updateQuoteVisible: visible => {
|
||||
set({
|
||||
quoteVisible: visible,
|
||||
});
|
||||
},
|
||||
clearStore: () => {
|
||||
set({
|
||||
quoteContent: null,
|
||||
quoteVisible: false,
|
||||
});
|
||||
},
|
||||
})),
|
||||
{
|
||||
name: `botStudio.ChatAreaGrabPlugin.Quote.${mark}`,
|
||||
enabled: IS_DEV_MODE,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
return useQuoteStore;
|
||||
};
|
||||
|
||||
export type QuoteStore = ReturnType<typeof createQuoteStore>;
|
||||
|
||||
export const subscribeQuoteUpdate = (
|
||||
store: {
|
||||
useQuoteStore: QuoteStore;
|
||||
},
|
||||
eventCallbacks: EventCallbacks,
|
||||
) => {
|
||||
const { useQuoteStore } = store;
|
||||
|
||||
return useQuoteStore.subscribe(
|
||||
state => state.quoteContent,
|
||||
quoteContent => {
|
||||
const { onQuoteChange } = eventCallbacks;
|
||||
onQuoteChange?.({ isEmpty: !quoteContent });
|
||||
},
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* 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 {
|
||||
type SelectionData,
|
||||
type GrabNode,
|
||||
type GrabPosition,
|
||||
} from '@coze-common/text-grab';
|
||||
|
||||
export interface SelectionState {
|
||||
humanizedContentText: string;
|
||||
originContentText: string;
|
||||
normalizeSelectionNodeList: GrabNode[];
|
||||
selectionData: SelectionData | null;
|
||||
isFloatMenuVisible: boolean;
|
||||
floatMenuPosition: GrabPosition | null;
|
||||
}
|
||||
|
||||
export interface SelectionAction {
|
||||
updateHumanizedContentText: (text: string) => void;
|
||||
updateOriginContentText: (text: string) => void;
|
||||
updateNormalizeSelectionNodeList: (nodeList: GrabNode[]) => void;
|
||||
updateSelectionData: (selectionData: SelectionData | null) => void;
|
||||
updateIsFloatMenuVisible: (visible: boolean) => void;
|
||||
updateFloatMenuPosition: (position: GrabPosition | null) => void;
|
||||
clearStore: () => void;
|
||||
}
|
||||
|
||||
export const createSelectionStore = (mark: string) => {
|
||||
const useSelectionStore = create<SelectionState & SelectionAction>()(
|
||||
devtools(
|
||||
(set, get) => ({
|
||||
humanizedContentText: '',
|
||||
originContentText: '',
|
||||
normalizeSelectionNodeList: [],
|
||||
selectionData: null,
|
||||
isFloatMenuVisible: false,
|
||||
floatMenuPosition: null,
|
||||
updateHumanizedContentText: text => {
|
||||
set({
|
||||
humanizedContentText: text,
|
||||
});
|
||||
},
|
||||
updateOriginContentText: text => {
|
||||
set({
|
||||
originContentText: text,
|
||||
});
|
||||
},
|
||||
updateNormalizeSelectionNodeList: nodeList => {
|
||||
set({
|
||||
normalizeSelectionNodeList: nodeList,
|
||||
});
|
||||
},
|
||||
updateIsFloatMenuVisible: visible => {
|
||||
set({
|
||||
isFloatMenuVisible: visible,
|
||||
});
|
||||
},
|
||||
updateSelectionData: selectionData => {
|
||||
set({
|
||||
selectionData,
|
||||
});
|
||||
},
|
||||
updateFloatMenuPosition: position => {
|
||||
set({
|
||||
floatMenuPosition: position,
|
||||
});
|
||||
},
|
||||
clearStore: () => {
|
||||
set({
|
||||
humanizedContentText: '',
|
||||
originContentText: '',
|
||||
normalizeSelectionNodeList: [],
|
||||
selectionData: null,
|
||||
});
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: `botStudio.ChatAreaGrabPlugin.SelectionStore.${mark}`,
|
||||
enabled: IS_DEV_MODE,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
return useSelectionStore;
|
||||
};
|
||||
|
||||
export type SelectionStore = ReturnType<typeof createSelectionStore>;
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* 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 Emitter } from 'mitt';
|
||||
import { type GrabNode } from '@coze-common/text-grab';
|
||||
import {
|
||||
type OnLinkElementContext,
|
||||
type MessageSource,
|
||||
} from '@coze-common/chat-area';
|
||||
|
||||
import { type SelectionStore } from '../stores/selection';
|
||||
import { type QuoteStore } from '../stores/quote';
|
||||
import { type PreferenceStore } from '../stores/preference';
|
||||
import { type Scene } from '../create';
|
||||
|
||||
export const enum EventNames {
|
||||
OnViewScroll = 'onViewScroll',
|
||||
OnMessageUpdate = 'onMessageUpdate',
|
||||
OnLinkElementMouseEnter = 'onCardLinkElementMouseEnter',
|
||||
OnLinkElementMouseLeave = 'onCardLinkElementMouseLeave',
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -- 符合预期
|
||||
export type EventCenter = {
|
||||
[EventNames.OnMessageUpdate]: unknown;
|
||||
[EventNames.OnViewScroll]: unknown;
|
||||
[EventNames.OnLinkElementMouseEnter]: OnLinkElementContext & {
|
||||
type: 'link' | 'image';
|
||||
};
|
||||
[EventNames.OnLinkElementMouseLeave]: OnLinkElementContext & {
|
||||
type: 'link' | 'image';
|
||||
};
|
||||
};
|
||||
|
||||
export const enum PublicEventNames {
|
||||
UpdateQuote = 'updateQuote',
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
||||
export type PublicEventCenter = {
|
||||
[PublicEventNames.UpdateQuote]: {
|
||||
grabPluginId: string;
|
||||
quote: GrabNode[] | null;
|
||||
};
|
||||
};
|
||||
|
||||
export interface GrabPluginBizContext {
|
||||
grabPluginId: string;
|
||||
storeSet: {
|
||||
useSelectionStore: SelectionStore;
|
||||
useQuoteStore: QuoteStore;
|
||||
usePreferenceStore: PreferenceStore;
|
||||
};
|
||||
eventCallbacks: EventCallbacks;
|
||||
eventCenter: Emitter<EventCenter>;
|
||||
publicEventCenter: Emitter<PublicEventCenter>;
|
||||
unsubscribe: () => void;
|
||||
scene?: Scene;
|
||||
}
|
||||
|
||||
export interface EventCallbacks {
|
||||
onQuote?: ({
|
||||
botId,
|
||||
source,
|
||||
}: {
|
||||
botId: string;
|
||||
source: MessageSource | undefined;
|
||||
}) => void;
|
||||
onQuoteChange?: ({ isEmpty }: { isEmpty: boolean }) => void;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* 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 GrabPublicMethod {
|
||||
updateEnableGrab: (enable: boolean) => void;
|
||||
}
|
||||
17
frontend/packages/common/chat-area/plugin-message-grab/src/typings.d.ts
vendored
Normal file
17
frontend/packages/common/chat-area/plugin-message-grab/src/typings.d.ts
vendored
Normal 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.
|
||||
*/
|
||||
|
||||
/// <reference types='@coze-arch/bot-typings' />
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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 {
|
||||
Children,
|
||||
type ReactElement,
|
||||
isValidElement,
|
||||
type ReactNode,
|
||||
} from 'react';
|
||||
|
||||
import { isObject } from 'lodash-es';
|
||||
|
||||
const isReactElementWithChildren = (
|
||||
node: unknown,
|
||||
): node is ReactElement<{ children: ReactNode }> =>
|
||||
isValidElement(node) &&
|
||||
'props' in node &&
|
||||
isObject(node.props) &&
|
||||
'children' in node.props;
|
||||
|
||||
/**
|
||||
* 从 ReactNode 中提取纯文本(不包括各种特殊转换逻辑)
|
||||
*/
|
||||
export const extractTextFromReactNode = (children: ReactNode): string => {
|
||||
let text = '';
|
||||
|
||||
Children.forEach(children, child => {
|
||||
if (typeof child === 'string' || typeof child === 'number') {
|
||||
// 如果 child 是字符串或数字,直接加到 text 上
|
||||
text += child.toString();
|
||||
} else if (
|
||||
isValidElement(child) &&
|
||||
isReactElementWithChildren(child) &&
|
||||
child.props.children
|
||||
) {
|
||||
// 如果 child 是 React 元素且有 children 属性,递归提取
|
||||
text += extractTextFromReactNode(child.props.children);
|
||||
}
|
||||
// 如果 child 是 null 或 boolean,不需要做任何操作
|
||||
});
|
||||
|
||||
return text;
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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 ChatAreaPluginContext,
|
||||
type PluginMode,
|
||||
} from '@coze-common/chat-area';
|
||||
|
||||
export const getMessage = ({
|
||||
messageId,
|
||||
chatAreaPluginContext,
|
||||
}: {
|
||||
messageId: string;
|
||||
chatAreaPluginContext: ChatAreaPluginContext<PluginMode.Writeable>;
|
||||
}) => {
|
||||
if (!messageId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const message =
|
||||
chatAreaPluginContext.readonlyAPI.message.findMessage(messageId);
|
||||
|
||||
return message;
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* 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 GrabPosition } from '@coze-common/text-grab';
|
||||
|
||||
const BUFFER_SIZE = 5;
|
||||
|
||||
export const getMouseNearbyRect = (
|
||||
rects: DOMRect[],
|
||||
mouseInfo: GrabPosition,
|
||||
) => {
|
||||
let nearbyRect = rects.at(0);
|
||||
for (const rect of rects) {
|
||||
if (
|
||||
mouseInfo.x >= rect.left - BUFFER_SIZE &&
|
||||
mouseInfo.x <= rect.right + BUFFER_SIZE &&
|
||||
mouseInfo.y >= rect.top - BUFFER_SIZE &&
|
||||
mouseInfo.y <= rect.bottom + BUFFER_SIZE
|
||||
) {
|
||||
nearbyRect = rect;
|
||||
}
|
||||
}
|
||||
|
||||
return nearbyRect;
|
||||
};
|
||||
@@ -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 { ContentType, type Message } from '@coze-common/chat-area';
|
||||
|
||||
export const getReferFromMessage = (message: Message<ContentType>) => {
|
||||
if (message.content_type === ContentType.Mix) {
|
||||
const { refer_items } = JSON.parse(message.content) ?? {};
|
||||
|
||||
if (refer_items) {
|
||||
const firstItem = refer_items?.[0];
|
||||
|
||||
if (firstItem?.type === 'text') {
|
||||
return {
|
||||
type: ContentType.Text,
|
||||
text: firstItem?.text,
|
||||
};
|
||||
} else if (firstItem?.type === 'image') {
|
||||
return {
|
||||
type: ContentType.Image,
|
||||
uri: firstItem?.image?.key,
|
||||
url: firstItem?.image?.image_thumb?.url,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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 ChatAreaPluginContext,
|
||||
type PluginMode,
|
||||
} from '@coze-common/chat-area';
|
||||
|
||||
export const getSenderId = ({
|
||||
messageId,
|
||||
chatAreaPluginContext,
|
||||
}: {
|
||||
messageId: string;
|
||||
chatAreaPluginContext: ChatAreaPluginContext<PluginMode.Writeable>;
|
||||
}) => {
|
||||
if (!messageId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const senderId =
|
||||
chatAreaPluginContext.readonlyAPI.message.findMessage(messageId)?.sender_id;
|
||||
|
||||
return senderId;
|
||||
};
|
||||
Reference in New Issue
Block a user