mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-25 19:30:17 +08:00
refactor: update Scrollbar component and integrate horizontal scrolling in TabContainer and KnowledgeBaseInput (#9988)
* refactor: update Scrollbar component and integrate horizontal scrolling in TabContainer and KnowledgeBaseInput - Renamed Props interface to ScrollbarProps for clarity. - Implemented useHorizontalScroll hook in TabContainer to manage horizontal scrolling. - Removed deprecated scroll handling logic and replaced it with the new hook. - Enhanced KnowledgeBaseInput to utilize horizontal scrolling for better UI management. - Cleaned up unused imports and components for improved code maintainability. * refactor: update dependencies type in useHorizontalScroll hook to readonly unknown[] for better type safety * feat: add scrollDistance parameter to useHorizontalScroll hook for customizable scrolling behavior * refactor: replace useHorizontalScroll with HorizontalScrollContainer in TabContainer, KnowledgeBaseInput, and MentionModelsInput components - Updated TabContainer to utilize HorizontalScrollContainer for improved scrolling functionality. - Refactored KnowledgeBaseInput and MentionModelsInput to replace the custom horizontal scroll implementation with HorizontalScrollContainer, simplifying the code and enhancing maintainability. * refactor(HorizontalScrollContainer): remove paddingRight prop and update scroll handling - Removed the unused paddingRight prop from HorizontalScrollContainerProps and its implementation. - Updated handleScrollRight to accept the event parameter and stop propagation. - Simplified the Container styled component by eliminating the padding-right style. * fix: sync issue * fix: isLeftNavbar inputbar display issue * feat(HorizontalScrollContainer): add scroll end detection and disable button hover effect --------- Co-authored-by: 自由的世界人 <3196812536@qq.com>
This commit is contained in:
parent
7fec4c0dac
commit
66115ca306
179
src/renderer/src/components/HorizontalScrollContainer/index.tsx
Normal file
179
src/renderer/src/components/HorizontalScrollContainer/index.tsx
Normal file
@ -0,0 +1,179 @@
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { ChevronRight } from 'lucide-react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
/**
|
||||
* 水平滚动容器
|
||||
* @param children 子元素
|
||||
* @param dependencies 依赖项
|
||||
* @param scrollDistance 滚动距离
|
||||
* @param className 类名
|
||||
* @param gap 间距
|
||||
* @param expandable 是否可展开
|
||||
*/
|
||||
export interface HorizontalScrollContainerProps {
|
||||
children: React.ReactNode
|
||||
dependencies?: readonly unknown[]
|
||||
scrollDistance?: number
|
||||
className?: string
|
||||
gap?: string
|
||||
expandable?: boolean
|
||||
}
|
||||
|
||||
const HorizontalScrollContainer: React.FC<HorizontalScrollContainerProps> = ({
|
||||
children,
|
||||
dependencies = [],
|
||||
scrollDistance = 200,
|
||||
className,
|
||||
gap = '8px',
|
||||
expandable = false
|
||||
}) => {
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
const [canScroll, setCanScroll] = useState(false)
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
const [isScrolledToEnd, setIsScrolledToEnd] = useState(false)
|
||||
|
||||
const handleScrollRight = (event: React.MouseEvent) => {
|
||||
scrollRef.current?.scrollBy({ left: scrollDistance, behavior: 'smooth' })
|
||||
event.stopPropagation()
|
||||
}
|
||||
|
||||
const handleContainerClick = (e: React.MouseEvent) => {
|
||||
if (expandable) {
|
||||
// 确保不是点击了其他交互元素(如 tag 的关闭按钮)
|
||||
const target = e.target as HTMLElement
|
||||
if (!target.closest('[data-no-expand]')) {
|
||||
setIsExpanded(!isExpanded)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const checkScrollability = () => {
|
||||
const scrollElement = scrollRef.current
|
||||
if (scrollElement) {
|
||||
const parentElement = scrollElement.parentElement
|
||||
const availableWidth = parentElement ? parentElement.clientWidth : scrollElement.clientWidth
|
||||
|
||||
// 确保容器不会超出可用宽度
|
||||
const canScrollValue = scrollElement.scrollWidth > Math.min(availableWidth, scrollElement.clientWidth)
|
||||
setCanScroll(canScrollValue)
|
||||
|
||||
// 检查是否滚动到最右侧
|
||||
if (canScrollValue) {
|
||||
const isAtEnd = Math.abs(scrollElement.scrollLeft + scrollElement.clientWidth - scrollElement.scrollWidth) <= 1
|
||||
setIsScrolledToEnd(isAtEnd)
|
||||
} else {
|
||||
setIsScrolledToEnd(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const scrollElement = scrollRef.current
|
||||
if (!scrollElement) return
|
||||
|
||||
checkScrollability()
|
||||
|
||||
const handleScroll = () => {
|
||||
checkScrollability()
|
||||
}
|
||||
|
||||
const resizeObserver = new ResizeObserver(checkScrollability)
|
||||
resizeObserver.observe(scrollElement)
|
||||
|
||||
scrollElement.addEventListener('scroll', handleScroll)
|
||||
window.addEventListener('resize', checkScrollability)
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect()
|
||||
scrollElement.removeEventListener('scroll', handleScroll)
|
||||
window.removeEventListener('resize', checkScrollability)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, dependencies)
|
||||
|
||||
return (
|
||||
<Container
|
||||
className={className}
|
||||
$expandable={expandable}
|
||||
$disableHoverButton={isScrolledToEnd}
|
||||
onClick={expandable ? handleContainerClick : undefined}>
|
||||
<ScrollContent ref={scrollRef} $gap={gap} $isExpanded={isExpanded} $expandable={expandable}>
|
||||
{children}
|
||||
</ScrollContent>
|
||||
{canScroll && !isExpanded && !isScrolledToEnd && (
|
||||
<ScrollButton onClick={handleScrollRight} className="scroll-right-button">
|
||||
<ChevronRight size={14} />
|
||||
</ScrollButton>
|
||||
)}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div<{ $expandable?: boolean; $disableHoverButton?: boolean }>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
position: relative;
|
||||
cursor: ${(props) => (props.$expandable ? 'pointer' : 'default')};
|
||||
|
||||
${(props) =>
|
||||
!props.$disableHoverButton &&
|
||||
`
|
||||
&:hover {
|
||||
.scroll-right-button {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
`}
|
||||
`
|
||||
|
||||
const ScrollContent = styled(Scrollbar)<{
|
||||
$gap: string
|
||||
$isExpanded?: boolean
|
||||
$expandable?: boolean
|
||||
}>`
|
||||
display: flex;
|
||||
overflow-x: ${(props) => (props.$expandable && props.$isExpanded ? 'hidden' : 'auto')};
|
||||
overflow-y: hidden;
|
||||
white-space: ${(props) => (props.$expandable && props.$isExpanded ? 'normal' : 'nowrap')};
|
||||
gap: ${(props) => props.$gap};
|
||||
flex-wrap: ${(props) => (props.$expandable && props.$isExpanded ? 'wrap' : 'nowrap')};
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
|
||||
const ScrollButton = styled.div`
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
z-index: 1;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
cursor: pointer;
|
||||
background: var(--color-background);
|
||||
border-radius: 50%;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow:
|
||||
0 6px 16px 0 rgba(0, 0, 0, 0.08),
|
||||
0 3px 6px -4px rgba(0, 0, 0, 0.12),
|
||||
0 9px 28px 8px rgba(0, 0, 0, 0.05);
|
||||
color: var(--color-text-2);
|
||||
|
||||
&:hover {
|
||||
color: var(--color-text);
|
||||
background: var(--color-list-item);
|
||||
}
|
||||
`
|
||||
|
||||
export default HorizontalScrollContainer
|
||||
@ -32,6 +32,11 @@ export const QuickPanelProvider: React.FC<React.PropsWithChildren> = ({ children
|
||||
setList((prevList) => prevList.map((item) => (item === targetItem ? { ...item, isSelected } : item)))
|
||||
}, [])
|
||||
|
||||
// 添加更新整个列表的方法
|
||||
const updateList = useCallback((newList: QuickPanelListItem[]) => {
|
||||
setList(newList)
|
||||
}, [])
|
||||
|
||||
const open = useCallback((options: QuickPanelOpenOptions) => {
|
||||
if (clearTimer.current) {
|
||||
clearTimeout(clearTimer.current)
|
||||
@ -85,6 +90,7 @@ export const QuickPanelProvider: React.FC<React.PropsWithChildren> = ({ children
|
||||
open,
|
||||
close,
|
||||
updateItemSelection,
|
||||
updateList,
|
||||
|
||||
isVisible,
|
||||
symbol,
|
||||
@ -103,6 +109,7 @@ export const QuickPanelProvider: React.FC<React.PropsWithChildren> = ({ children
|
||||
open,
|
||||
close,
|
||||
updateItemSelection,
|
||||
updateList,
|
||||
isVisible,
|
||||
symbol,
|
||||
list,
|
||||
|
||||
@ -68,6 +68,7 @@ export interface QuickPanelContextType {
|
||||
readonly open: (options: QuickPanelOpenOptions) => void
|
||||
readonly close: (action?: QuickPanelCloseAction, searchText?: string) => void
|
||||
readonly updateItemSelection: (targetItem: QuickPanelListItem, isSelected: boolean) => void
|
||||
readonly updateList: (newList: QuickPanelListItem[]) => void
|
||||
readonly isVisible: boolean
|
||||
readonly symbol: string
|
||||
readonly list: QuickPanelListItem[]
|
||||
|
||||
@ -2,12 +2,12 @@ import { throttle } from 'lodash'
|
||||
import { FC, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface Props extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onScroll'> {
|
||||
export interface ScrollbarProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onScroll'> {
|
||||
ref?: React.Ref<HTMLDivElement | null>
|
||||
onScroll?: () => void // Custom onScroll prop for useScrollPosition's handleScroll
|
||||
}
|
||||
|
||||
const Scrollbar: FC<Props> = ({ ref: passedRef, children, onScroll: externalOnScroll, ...htmlProps }) => {
|
||||
const Scrollbar: FC<ScrollbarProps> = ({ ref: passedRef, children, onScroll: externalOnScroll, ...htmlProps }) => {
|
||||
const [isScrolling, setIsScrolling] = useState(false)
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null)
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { PlusOutlined } from '@ant-design/icons'
|
||||
import { Sortable, useDndReorder } from '@renderer/components/dnd'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import HorizontalScrollContainer from '@renderer/components/HorizontalScrollContainer'
|
||||
import { isMac } from '@renderer/config/constant'
|
||||
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
@ -14,9 +14,8 @@ import type { Tab } from '@renderer/store/tabs'
|
||||
import { addTab, removeTab, setActiveTab, setTabs } from '@renderer/store/tabs'
|
||||
import { ThemeMode } from '@renderer/types'
|
||||
import { classNames } from '@renderer/utils'
|
||||
import { Button, Tooltip } from 'antd'
|
||||
import { Tooltip } from 'antd'
|
||||
import {
|
||||
ChevronRight,
|
||||
FileSearch,
|
||||
Folder,
|
||||
Hammer,
|
||||
@ -33,7 +32,7 @@ import {
|
||||
Terminal,
|
||||
X
|
||||
} from 'lucide-react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import styled from 'styled-components'
|
||||
@ -98,8 +97,6 @@ const TabsContainer: React.FC<TabsContainerProps> = ({ children }) => {
|
||||
const { hideMinappPopup } = useMinappPopup()
|
||||
const { minapps } = useMinapps()
|
||||
const { t } = useTranslation()
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
const [canScroll, setCanScroll] = useState(false)
|
||||
|
||||
const getTabId = (path: string): string => {
|
||||
if (path === '/') return 'home'
|
||||
@ -175,31 +172,6 @@ const TabsContainer: React.FC<TabsContainerProps> = ({ children }) => {
|
||||
navigate(tab.path)
|
||||
}
|
||||
|
||||
const handleScrollRight = () => {
|
||||
scrollRef.current?.scrollBy({ left: 200, behavior: 'smooth' })
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const scrollElement = scrollRef.current
|
||||
if (!scrollElement) return
|
||||
|
||||
const checkScrollability = () => {
|
||||
setCanScroll(scrollElement.scrollWidth > scrollElement.clientWidth)
|
||||
}
|
||||
|
||||
checkScrollability()
|
||||
|
||||
const resizeObserver = new ResizeObserver(checkScrollability)
|
||||
resizeObserver.observe(scrollElement)
|
||||
|
||||
window.addEventListener('resize', checkScrollability)
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect()
|
||||
window.removeEventListener('resize', checkScrollability)
|
||||
}
|
||||
}, [tabs])
|
||||
|
||||
const visibleTabs = useMemo(() => tabs.filter((tab) => !specialTabs.includes(tab.id)), [tabs])
|
||||
|
||||
const { onSortEnd } = useDndReorder<Tab>({
|
||||
@ -212,46 +184,39 @@ const TabsContainer: React.FC<TabsContainerProps> = ({ children }) => {
|
||||
return (
|
||||
<Container>
|
||||
<TabsBar $isFullscreen={isFullscreen}>
|
||||
<TabsArea>
|
||||
<TabsScroll ref={scrollRef}>
|
||||
<Sortable
|
||||
items={visibleTabs}
|
||||
itemKey="id"
|
||||
layout="list"
|
||||
horizontal
|
||||
gap={'6px'}
|
||||
onSortEnd={onSortEnd}
|
||||
className="tabs-sortable"
|
||||
renderItem={(tab) => (
|
||||
<Tab key={tab.id} active={tab.id === activeTabId} onClick={() => handleTabClick(tab)}>
|
||||
<TabHeader>
|
||||
{tab.id && <TabIcon>{getTabIcon(tab.id, minapps)}</TabIcon>}
|
||||
<TabTitle>{getTabTitle(tab.id)}</TabTitle>
|
||||
</TabHeader>
|
||||
{tab.id !== 'home' && (
|
||||
<CloseButton
|
||||
className="close-button"
|
||||
data-no-dnd
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
closeTab(tab.id)
|
||||
}}>
|
||||
<X size={12} />
|
||||
</CloseButton>
|
||||
)}
|
||||
</Tab>
|
||||
)}
|
||||
/>
|
||||
</TabsScroll>
|
||||
{canScroll && (
|
||||
<ScrollButton onClick={handleScrollRight} className="scroll-right-button" shape="circle" size="small">
|
||||
<ChevronRight size={16} />
|
||||
</ScrollButton>
|
||||
)}
|
||||
<HorizontalScrollContainer dependencies={[tabs]} gap="6px" className="tab-scroll-container">
|
||||
<Sortable
|
||||
items={visibleTabs}
|
||||
itemKey="id"
|
||||
layout="list"
|
||||
horizontal
|
||||
gap={'6px'}
|
||||
onSortEnd={onSortEnd}
|
||||
className="tabs-sortable"
|
||||
renderItem={(tab) => (
|
||||
<Tab key={tab.id} active={tab.id === activeTabId} onClick={() => handleTabClick(tab)}>
|
||||
<TabHeader>
|
||||
{tab.id && <TabIcon>{getTabIcon(tab.id, minapps)}</TabIcon>}
|
||||
<TabTitle>{getTabTitle(tab.id)}</TabTitle>
|
||||
</TabHeader>
|
||||
{tab.id !== 'home' && (
|
||||
<CloseButton
|
||||
className="close-button"
|
||||
data-no-dnd
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
closeTab(tab.id)
|
||||
}}>
|
||||
<X size={12} />
|
||||
</CloseButton>
|
||||
)}
|
||||
</Tab>
|
||||
)}
|
||||
/>
|
||||
<AddTabButton onClick={handleAddTab} className={classNames({ active: activeTabId === 'launchpad' })}>
|
||||
<PlusOutlined />
|
||||
</AddTabButton>
|
||||
</TabsArea>
|
||||
</HorizontalScrollContainer>
|
||||
<RightButtonsContainer>
|
||||
<Tooltip
|
||||
title={t('settings.theme.title') + ': ' + getThemeModeLabel(settedTheme)}
|
||||
@ -307,36 +272,16 @@ const TabsBar = styled.div<{ $isFullscreen: boolean }>`
|
||||
z-index: 1;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
`
|
||||
|
||||
const TabsArea = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
gap: 6px;
|
||||
padding-right: 2rem;
|
||||
position: relative;
|
||||
.tab-scroll-container {
|
||||
-webkit-app-region: drag;
|
||||
|
||||
-webkit-app-region: drag;
|
||||
|
||||
> * {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.scroll-right-button {
|
||||
opacity: 1;
|
||||
> * {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const TabsScroll = styled(Scrollbar)`
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
|
||||
const Tab = styled.div<{ active?: boolean }>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -414,22 +359,6 @@ const AddTabButton = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
const ScrollButton = styled(Button)`
|
||||
position: absolute;
|
||||
right: 4rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
z-index: 1;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
|
||||
border: none;
|
||||
box-shadow:
|
||||
0 6px 16px 0 rgba(0, 0, 0, 0.08),
|
||||
0 3px 6px -4px rgba(0, 0, 0, 0.12),
|
||||
0 9px 28px 8px rgba(0, 0, 0, 0.05);
|
||||
`
|
||||
|
||||
const RightButtonsContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@ -43,6 +43,7 @@ const Chat: FC<Props> = (props) => {
|
||||
const { showTopics } = useShowTopics()
|
||||
const { isMultiSelectMode } = useChatContext(props.activeTopic)
|
||||
const { isTopNavbar } = useNavbarPosition()
|
||||
const chatMaxWidth = useChatMaxWidth()
|
||||
|
||||
const mainRef = React.useRef<HTMLDivElement>(null)
|
||||
const contentSearchRef = React.useRef<ContentSearchRef>(null)
|
||||
@ -153,7 +154,7 @@ const Chat: FC<Props> = (props) => {
|
||||
vertical
|
||||
flex={1}
|
||||
justify="space-between"
|
||||
style={{ maxWidth: '100%', height: mainHeight }}>
|
||||
style={{ maxWidth: chatMaxWidth, height: mainHeight }}>
|
||||
<Messages
|
||||
key={props.activeTopic.id}
|
||||
assistant={assistant}
|
||||
@ -215,7 +216,7 @@ const Container = styled.div`
|
||||
height: calc(100vh - var(--navbar-height));
|
||||
flex: 1;
|
||||
[navbar-position='top'] & {
|
||||
height: calc(100vh - var(--navbar-height) -6px);
|
||||
height: calc(100vh - var(--navbar-height) - 6px);
|
||||
background-color: var(--color-background);
|
||||
border-top-left-radius: 10px;
|
||||
border-bottom-left-radius: 10px;
|
||||
|
||||
@ -61,6 +61,8 @@ import styled from 'styled-components'
|
||||
import NarrowLayout from '../Messages/NarrowLayout'
|
||||
import AttachmentPreview from './AttachmentPreview'
|
||||
import InputbarTools, { InputbarToolsRef } from './InputbarTools'
|
||||
import KnowledgeBaseInput from './KnowledgeBaseInput'
|
||||
import MentionModelsInput from './MentionModelsInput'
|
||||
import SendMessageButton from './SendMessageButton'
|
||||
import TokenCount from './TokenCount'
|
||||
|
||||
@ -765,6 +767,19 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
setSelectedKnowledgeBases(bases ?? [])
|
||||
}
|
||||
|
||||
const handleRemoveModel = (model: Model) => {
|
||||
setMentionedModels(mentionedModels.filter((m) => m.id !== model.id))
|
||||
}
|
||||
|
||||
const handleRemoveKnowledgeBase = (knowledgeBase: KnowledgeBase) => {
|
||||
const newKnowledgeBases = assistant.knowledge_bases?.filter((kb) => kb.id !== knowledgeBase.id)
|
||||
updateAssistant({
|
||||
...assistant,
|
||||
knowledge_bases: newKnowledgeBases
|
||||
})
|
||||
setSelectedKnowledgeBases(newKnowledgeBases ?? [])
|
||||
}
|
||||
|
||||
const onEnableGenerateImage = () => {
|
||||
updateAssistant({ ...assistant, enableGenerateImage: !assistant.enableGenerateImage })
|
||||
}
|
||||
@ -851,6 +866,15 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
className={classNames('inputbar-container', inputFocus && 'focus', isFileDragging && 'file-dragging')}
|
||||
ref={containerRef}>
|
||||
{files.length > 0 && <AttachmentPreview files={files} setFiles={setFiles} />}
|
||||
{selectedKnowledgeBases.length > 0 && (
|
||||
<KnowledgeBaseInput
|
||||
selectedKnowledgeBases={selectedKnowledgeBases}
|
||||
onRemoveKnowledgeBase={handleRemoveKnowledgeBase}
|
||||
/>
|
||||
)}
|
||||
{mentionedModels.length > 0 && (
|
||||
<MentionModelsInput selectedModels={mentionedModels} onRemoveModel={handleRemoveModel} />
|
||||
)}
|
||||
<Textarea
|
||||
value={text}
|
||||
onChange={onChange}
|
||||
|
||||
@ -93,6 +93,14 @@ const KnowledgeBaseButton: FC<Props> = ({ ref, selectedBases, onSelect, disabled
|
||||
}
|
||||
}, [openQuickPanel, quickPanel])
|
||||
|
||||
// 监听 selectedBases 变化,动态更新已打开的 QuickPanel 列表状态
|
||||
useEffect(() => {
|
||||
if (quickPanel.isVisible && quickPanel.symbol === '#') {
|
||||
// 直接使用重新计算的 baseItems,因为它已经包含了最新的 isSelected 状态
|
||||
quickPanel.updateList(baseItems)
|
||||
}
|
||||
}, [selectedBases, quickPanel, baseItems])
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
openQuickPanel
|
||||
}))
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { FileSearchOutlined } from '@ant-design/icons'
|
||||
import HorizontalScrollContainer from '@renderer/components/HorizontalScrollContainer'
|
||||
import CustomTag from '@renderer/components/Tags/CustomTag'
|
||||
import { KnowledgeBase } from '@renderer/types'
|
||||
import { FC } from 'react'
|
||||
@ -10,16 +11,18 @@ const KnowledgeBaseInput: FC<{
|
||||
}> = ({ selectedKnowledgeBases, onRemoveKnowledgeBase }) => {
|
||||
return (
|
||||
<Container>
|
||||
{selectedKnowledgeBases.map((knowledgeBase) => (
|
||||
<CustomTag
|
||||
icon={<FileSearchOutlined />}
|
||||
color="#3d9d0f"
|
||||
key={knowledgeBase.id}
|
||||
closable
|
||||
onClose={() => onRemoveKnowledgeBase(knowledgeBase)}>
|
||||
{knowledgeBase.name}
|
||||
</CustomTag>
|
||||
))}
|
||||
<HorizontalScrollContainer dependencies={[selectedKnowledgeBases]} expandable>
|
||||
{selectedKnowledgeBases.map((knowledgeBase) => (
|
||||
<CustomTag
|
||||
icon={<FileSearchOutlined />}
|
||||
color="#3d9d0f"
|
||||
key={knowledgeBase.id}
|
||||
closable
|
||||
onClose={() => onRemoveKnowledgeBase(knowledgeBase)}>
|
||||
{knowledgeBase.name}
|
||||
</CustomTag>
|
||||
))}
|
||||
</HorizontalScrollContainer>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@ -27,9 +30,6 @@ const KnowledgeBaseInput: FC<{
|
||||
const Container = styled.div`
|
||||
width: 100%;
|
||||
padding: 5px 15px 5px 15px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px 4px;
|
||||
`
|
||||
|
||||
export default KnowledgeBaseInput
|
||||
|
||||
@ -294,6 +294,14 @@ const MentionModelsButton: FC<Props> = ({
|
||||
}
|
||||
}, [files, quickPanel])
|
||||
|
||||
// 监听 mentionedModels 变化,动态更新已打开的 QuickPanel 列表状态
|
||||
useEffect(() => {
|
||||
if (quickPanel.isVisible && quickPanel.symbol === '@') {
|
||||
// 直接使用重新计算的 modelItems,因为它已经包含了最新的 isSelected 状态
|
||||
quickPanel.updateList(modelItems)
|
||||
}
|
||||
}, [mentionedModels, quickPanel, modelItems])
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
openQuickPanel
|
||||
}))
|
||||
|
||||
44
src/renderer/src/pages/home/Inputbar/MentionModelsInput.tsx
Normal file
44
src/renderer/src/pages/home/Inputbar/MentionModelsInput.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import HorizontalScrollContainer from '@renderer/components/HorizontalScrollContainer'
|
||||
import CustomTag from '@renderer/components/Tags/CustomTag'
|
||||
import { useProviders } from '@renderer/hooks/useProvider'
|
||||
import { getModelUniqId } from '@renderer/services/ModelService'
|
||||
import { Model } from '@renderer/types'
|
||||
import { getFancyProviderName } from '@renderer/utils'
|
||||
import { FC } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const MentionModelsInput: FC<{
|
||||
selectedModels: Model[]
|
||||
onRemoveModel: (model: Model) => void
|
||||
}> = ({ selectedModels, onRemoveModel }) => {
|
||||
const { providers } = useProviders()
|
||||
|
||||
const getProviderName = (model: Model) => {
|
||||
const provider = providers.find((p) => p.id === model?.provider)
|
||||
return provider ? getFancyProviderName(provider) : ''
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<HorizontalScrollContainer dependencies={[selectedModels]} expandable>
|
||||
{selectedModels.map((model) => (
|
||||
<CustomTag
|
||||
icon={<i className="iconfont icon-at" />}
|
||||
color="#1677ff"
|
||||
key={getModelUniqId(model)}
|
||||
closable
|
||||
onClose={() => onRemoveModel(model)}>
|
||||
{model.name} ({getProviderName(model)})
|
||||
</CustomTag>
|
||||
))}
|
||||
</HorizontalScrollContainer>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
width: 100%;
|
||||
padding: 5px 15px 5px 15px;
|
||||
`
|
||||
|
||||
export default MentionModelsInput
|
||||
Loading…
Reference in New Issue
Block a user