diff --git a/src/components/chat-search/chat-search.scss b/src/components/chat-search/chat-search.scss new file mode 100644 index 0000000000000000000000000000000000000000..14448dcf4045e78cd3f945dd2e8906216e51473a --- /dev/null +++ b/src/components/chat-search/chat-search.scss @@ -0,0 +1,41 @@ +@include b(chat-search) { + gap: 8px; + width: 100%; + display: flex; + line-height: 32px; + border-radius: 1px; + padding: 1px 11px; + align-items: center; + justify-content: center; + border: 1px solid #{getCssVar('ai-chat', 'border-color')}; + + &:hover { + border: 1px solid #{getCssVar('ai-chat', 'background-color-2')}; + } + + @include when(focus) { + border: 1px solid #{getCssVar('ai-chat', 'background-color-2')}; + } + + @include e(prefix) { + width: 14px; + height: 14px; + display: flex; + flex-shrink: 0; + font-size: 14px; + align-items: center; + } + + @include e(inner) { + padding: 0; + outline: 0; + flex-grow: 1; + border: none; + height: 28px; + min-width: 0; + color: inherit; + padding-top: 2px; + line-height: 28px; + background-color: initial; + } +} diff --git a/src/components/chat-search/chat-search.tsx b/src/components/chat-search/chat-search.tsx new file mode 100644 index 0000000000000000000000000000000000000000..62f5bb288028c36bfcec281b1b9dea9af9b8d5ce --- /dev/null +++ b/src/components/chat-search/chat-search.tsx @@ -0,0 +1,66 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { useState } from 'preact/hooks'; +import { Namespace } from '../../utils'; +import { SearchSvg } from '../../icons'; +import './chat-search.scss'; + +export interface ChatSearchProps { + /** + * 值 + * + * @type {string} + * @memberof ChatSearchProps + */ + value: string | undefined; + /** + * 类名 + * + * @type {string} + * @memberof ChatSearchProps + */ + className?: string; + /** + * 空白占位 + * + * @type {string} + * @memberof ChatSearchProps + */ + placeholder?: string; + /** + * 点击事件 + * + * @memberof ChatSearchProps + */ + onChange?: (value: string) => void; +} + +const ns = new Namespace('chat-search'); + +export const ChatSearch = (props: ChatSearchProps) => { + const { className, value, placeholder, onChange } = props; + + const [isFocused, setIsFocused] = useState(false); + + const handleChange = (event: any) => { + event.stopPropagation(); + onChange?.(event.target?.value); + }; + + return ( +
+
+ +
+ setIsFocused(true)} + onBlur={() => setIsFocused(false)} + onChange={event => handleChange(event)} + /> +
+ ); +}; diff --git a/src/components/chat-topics/chat-topics.scss b/src/components/chat-topics/chat-topics.scss index 37ee39abeaeaea93d734029b0c2a4c43e1e5bc74..2468f669a7bf3bcc550a8b0ee349858ccda0e3fc 100644 --- a/src/components/chat-topics/chat-topics.scss +++ b/src/components/chat-topics/chat-topics.scss @@ -1,5 +1,47 @@ @include b(chat-topics) { + gap: 8px; height: 100%; - padding: 8px; - overflow-y: auto; + padding: 8px 0; + display: flex; + flex-direction: column; + + @include e(header) { + padding: 0 8px; + flex-shrink: 0; + } + + @include e(main) { + flex-grow: 1; + padding: 0 8px; + overflow-y: auto; + } + + @include e(footer) { + display: flex; + flex-shrink: 0; + padding: 0 8px; + align-items: center; + justify-content: center; + } + + @include e(action) { + gap: 4px; + padding: 4px 12px; + font-size: 14px; + display: flex; + cursor: pointer; + border-radius: 2px; + align-items: center; + border: 1px solid #{getCssVar('ai-chat', 'border-color')}; + + &:hover { + color: #{getCssVar('ai-chat', 'hover-color')}; + background-color: #{getCssVar('ai-chat', 'hover-background-color')}; + } + + > svg { + width: 14px !important; + height: 14px !important; + } + } } diff --git a/src/components/chat-topics/chat-topics.tsx b/src/components/chat-topics/chat-topics.tsx index ed6dc01eccb589ee90dfda3cfbd3f98a55282a2d..e627da40162cda26ab1940a0bc0cf4fa030cd3c4 100644 --- a/src/components/chat-topics/chat-topics.tsx +++ b/src/components/chat-topics/chat-topics.tsx @@ -1,8 +1,12 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { useComputed } from '@preact/signals'; import { useEffect, useRef } from 'preact/hooks'; import { AiTopicController } from '../../controller'; import { ChatTopic } from '../../entity'; import { Namespace } from '../../utils'; import { ChatTopicItem } from '../chat-topic-item/chat-topic-item'; +import { RemoveSvg } from '../../icons'; +import { ChatSearch } from '../chat-search/chat-search'; import './chat-topics.scss'; export interface ChatTopicProps { @@ -19,8 +23,25 @@ export interface ChatTopicProps { const ns = new Namespace('chat-topics'); export const ChatTopics = (props: ChatTopicProps) => { - const topics = props.controller.topics; - const ref = useRef(null); + const containerRef = useRef(null); + + /** + * 快速搜索值 + */ + const query = useRef(undefined); + + /** + * 当前话题列表 + */ + const topics = useComputed(() => { + return props.controller.topics.value.filter( + topic => + topic.caption + ?.toLowerCase() + .includes(query.current?.trim().toLowerCase() || ''), + ); + }); + /** * 项点击 * @@ -49,9 +70,18 @@ export const ChatTopics = (props: ChatTopicProps) => { props.controller.handleTopicAction(action, topic, event); }; + /** + * 处理值改变 + * + * @param {*} event + */ + const handleChange = (value: string) => { + query.current = value; + }; + // 监听激活项改变,滚动到激活项 useEffect(() => { - const container = ref.current; + const container = containerRef.current; if (!container) return; const selectedElement = container.querySelector( '.ibiz-chat-topic-item.is-active', @@ -60,20 +90,39 @@ export const ChatTopics = (props: ChatTopicProps) => { }, [props.controller.activedTopic.value]); return ( -
- {topics.value.map(topic => { - return ( - itemClick(topic)} - onAction={(action: string, event: MouseEvent) => - handleTopicAction(action, topic, event) - } - /> - ); - })} +
+
+ +
+
+ {topics.value.map(topic => { + return ( + itemClick(topic)} + onAction={(action: string, event: MouseEvent) => + handleTopicAction(action, topic, event) + } + /> + ); + })} +
+
+
props.controller.clearTopic()} + > + + 清空会话 +
+
); }; diff --git a/src/controller/ai-topic/ai-topic.controller.ts b/src/controller/ai-topic/ai-topic.controller.ts index e8eea73c95f5605e8d582416a07d15437ad856da..cdf4914b16d8bf177f94598fd587b2790603555d 100644 --- a/src/controller/ai-topic/ai-topic.controller.ts +++ b/src/controller/ai-topic/ai-topic.controller.ts @@ -313,4 +313,60 @@ export class AiTopicController { // 切换激活 this.handleTopicChange(chatTopic); } + + /** + * 清空话题 + * - 当前激活项不清空 + * @return {*} {Promise} + * @memberof AiTopicController + */ + public async clearTopic(): Promise { + const actived = this.topics.value.find( + item => item.id === this.activedTopic.value?.id, + ); + if (!this.currentTopicOptions || !actived) return; + let checkResult: boolean = true; + if (this.currentTopicOptions.beforeDelete) { + checkResult = await this.currentTopicOptions.beforeDelete( + actived.aiChat!.context, + actived.aiChat!.params, + actived, + undefined, + true, + ); + } + if (!checkResult) return; + // 删除非激活项本地缓存 + await Promise.all( + this.topics.value.map(topic => { + if (topic.id !== actived?.id) + return IndexedDBUtil.deleteData( + AIChatConst.DATA_BASE_NAME, + AIChatConst.DATA_TABLE_NAME, + topic.id, + ); + return; + }), + ); + // 话题列表只保留激活项 + this.topics.value = actived ? [actived] : []; + // 存储数据 + const config = this.currentTopicOptions?.configService( + this.currentTopicOptions.appid, + 'aitopics', + this.currentTopicOptions.type, + ); + const result = this.topics.value.map(item => { + return { + appid: item.appid, + id: item.id, + type: item.type, + caption: item.caption, + sourceCaption: item.sourceCaption, + url: item.url, + aiChat: item.aiChat, + }; + }); + await config?.save(result); + } } diff --git a/src/icons/index.ts b/src/icons/index.ts index 11fbb46ea584f2ed2eed832bdbd39e4228fcd6ea..8701eabf2848eb6e9f02ebecc45f55a45d499f8a 100644 --- a/src/icons/index.ts +++ b/src/icons/index.ts @@ -27,3 +27,4 @@ export { ClearDialogueSvg } from './clear-dialogue-svg'; export { ResetDialogueSvg } from './reset-dialogue-svg'; export { RenameSvg } from './rename-svg'; export { ChevronForwardSvg } from './chevron-forward-svg'; +export { SearchSvg } from './search-svg'; diff --git a/src/icons/search-svg.tsx b/src/icons/search-svg.tsx new file mode 100644 index 0000000000000000000000000000000000000000..ab87983a63cdaa917523738815cc9a87c8f9099d --- /dev/null +++ b/src/icons/search-svg.tsx @@ -0,0 +1,10 @@ +export const SearchSvg = () => ( + + + +); diff --git a/src/interface/i-topic-options/i-topic-options.ts b/src/interface/i-topic-options/i-topic-options.ts index f46f519a57bcada4cba02060327a4319dead2c42..c0678c7e1109184264870b777398c703dbdf3988 100644 --- a/src/interface/i-topic-options/i-topic-options.ts +++ b/src/interface/i-topic-options/i-topic-options.ts @@ -86,19 +86,20 @@ export interface ITopicOptions extends ITopic { /** * 删除之前 * - * @author tony001 - * @date 2025-02-24 16:02:35 * @param {object} context * @param {object} params * @param {ITopic} data - * @param {MouseEvent} event + * @param {MouseEvent} [event] + * @param {boolean} [clear] * @return {*} {Promise} + * @memberof ITopicOptions */ beforeDelete?( context: object, params: object, data: ITopic, - event: MouseEvent, + event?: MouseEvent, + clear?: boolean, ): Promise; /**