mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-27 21:01:32 +08:00
feat(CustomCollapse, ThinkingBlock, KnowledgeSearchPopup): add expand icons and adjust styles for improved UI
- Introduced ChevronRight expand icons in CustomCollapse and ThinkingBlock components for better visual feedback. - Adjusted height and styling in KnowledgeSearchPopup for enhanced usability and consistency. - Updated various styled components to improve layout and user interaction.
This commit is contained in:
parent
eb832cc25a
commit
36fa3af9e9
@ -1,5 +1,6 @@
|
||||
import { Collapse } from 'antd'
|
||||
import { merge } from 'lodash'
|
||||
import { ChevronRight } from 'lucide-react'
|
||||
import { FC, memo, useMemo, useState } from 'react'
|
||||
|
||||
interface CustomCollapseProps {
|
||||
@ -78,6 +79,14 @@ const CustomCollapse: FC<CustomCollapseProps> = ({
|
||||
destroyInactivePanel={destroyInactivePanel}
|
||||
collapsible={collapsible}
|
||||
onChange={setActiveKeys}
|
||||
expandIcon={({ isActive }) => (
|
||||
<ChevronRight
|
||||
size={16}
|
||||
color="var(--color-text-3)"
|
||||
strokeWidth={1.5}
|
||||
style={{ transform: isActive ? 'rotate(90deg)' : 'rotate(0deg)' }}
|
||||
/>
|
||||
)}
|
||||
items={[
|
||||
{
|
||||
styles: collapseItemStyles,
|
||||
|
||||
@ -42,7 +42,7 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
paddingBottom: 16
|
||||
},
|
||||
body: {
|
||||
height: '85vh',
|
||||
height: '80vh',
|
||||
maxHeight: 'inherit',
|
||||
padding: 0
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { MessageBlockStatus, type ThinkingMessageBlock } from '@renderer/types/newMessage'
|
||||
import { lightbulbVariants } from '@renderer/utils/motionVariants'
|
||||
import { Collapse, message as antdMessage, Tooltip } from 'antd'
|
||||
import { Lightbulb } from 'lucide-react'
|
||||
import { ChevronRight, Lightbulb } from 'lucide-react'
|
||||
import { motion } from 'motion/react'
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -57,6 +57,14 @@ const ThinkingBlock: React.FC<Props> = ({ block }) => {
|
||||
size="small"
|
||||
onChange={() => setActiveKey((key) => (key ? '' : 'thought'))}
|
||||
className="message-thought-container"
|
||||
expandIcon={({ isActive }) => (
|
||||
<ChevronRight
|
||||
color="var(--color-text-3)"
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
style={{ transform: isActive ? 'rotate(90deg)' : 'rotate(0deg)' }}
|
||||
/>
|
||||
)}
|
||||
expandIconPosition="end"
|
||||
items={[
|
||||
{
|
||||
|
||||
@ -1,14 +1,15 @@
|
||||
import { CopyOutlined } from '@ant-design/icons'
|
||||
import type { ExtractChunkData } from '@cherrystudio/embedjs-interfaces'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import { TopView } from '@renderer/components/TopView'
|
||||
import { searchKnowledgeBase } from '@renderer/services/KnowledgeService'
|
||||
import { FileType, KnowledgeBase } from '@renderer/types'
|
||||
import { Input, List, message, Modal, Spin, Tooltip, Typography } from 'antd'
|
||||
import { useRef, useState } from 'react'
|
||||
import { Divider, Input, List, message, Modal, Spin, Tooltip, Typography } from 'antd'
|
||||
import { Search } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const { Search } = Input
|
||||
const { Text, Paragraph } = Typography
|
||||
|
||||
interface ShowParams {
|
||||
@ -25,7 +26,6 @@ const PopupContainer: React.FC<Props> = ({ base, resolve }) => {
|
||||
const [results, setResults] = useState<Array<ExtractChunkData & { file: FileType | null }>>([])
|
||||
const [searchKeyword, setSearchKeyword] = useState('')
|
||||
const { t } = useTranslation()
|
||||
const searchInputRef = useRef<any>(null)
|
||||
|
||||
const handleSearch = async (value: string) => {
|
||||
if (!value.trim()) {
|
||||
@ -84,77 +84,98 @@ const PopupContainer: React.FC<Props> = ({ base, resolve }) => {
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t('knowledge.search')}
|
||||
title={null}
|
||||
open={open}
|
||||
onOk={onOk}
|
||||
onCancel={onCancel}
|
||||
afterClose={onClose}
|
||||
afterOpenChange={(visible) => visible && searchInputRef.current?.focus()}
|
||||
width={800}
|
||||
width={700}
|
||||
footer={null}
|
||||
centered
|
||||
transitionName="animation-move-down">
|
||||
<SearchContainer>
|
||||
<Search
|
||||
placeholder={t('knowledge.search_placeholder')}
|
||||
closable={false}
|
||||
transitionName="animation-move-down"
|
||||
styles={{
|
||||
content: {
|
||||
borderRadius: 20,
|
||||
padding: 0,
|
||||
overflow: 'hidden',
|
||||
paddingBottom: 12
|
||||
},
|
||||
body: {
|
||||
maxHeight: '80vh',
|
||||
overflow: 'hidden',
|
||||
padding: 0
|
||||
}
|
||||
}}>
|
||||
<HStack style={{ padding: '0 12px', marginTop: 8 }}>
|
||||
<Input
|
||||
prefix={
|
||||
<SearchIcon>
|
||||
<Search size={15} />
|
||||
</SearchIcon>
|
||||
}
|
||||
value={searchKeyword}
|
||||
placeholder={t('knowledge.search')}
|
||||
allowClear
|
||||
enterButton
|
||||
size="large"
|
||||
onSearch={handleSearch}
|
||||
ref={searchInputRef}
|
||||
autoFocus
|
||||
spellCheck={false}
|
||||
style={{ paddingLeft: 0 }}
|
||||
variant="borderless"
|
||||
size="middle"
|
||||
onChange={(e) => setSearchKeyword(e.target.value)}
|
||||
onPressEnter={() => handleSearch(searchKeyword)}
|
||||
/>
|
||||
<ResultsContainer>
|
||||
{loading ? (
|
||||
<LoadingContainer>
|
||||
<Spin size="large" />
|
||||
</LoadingContainer>
|
||||
) : (
|
||||
<List
|
||||
dataSource={results}
|
||||
renderItem={(item) => (
|
||||
<List.Item>
|
||||
<ResultItem>
|
||||
<TagContainer>
|
||||
<ScoreTag>Score: {(item.score * 100).toFixed(1)}%</ScoreTag>
|
||||
<Tooltip title={t('common.copy')}>
|
||||
<CopyButton onClick={() => handleCopy(item.pageContent)}>
|
||||
<CopyOutlined />
|
||||
</CopyButton>
|
||||
</Tooltip>
|
||||
</TagContainer>
|
||||
<Paragraph style={{ userSelect: 'text' }}>{highlightText(item.pageContent)}</Paragraph>
|
||||
<MetadataContainer>
|
||||
<Text type="secondary">
|
||||
{t('knowledge.source')}:{' '}
|
||||
{item.file ? (
|
||||
<a href={`http://file/${item.file.name}`} target="_blank" rel="noreferrer">
|
||||
{item.file.origin_name}
|
||||
</a>
|
||||
) : (
|
||||
item.metadata.source
|
||||
)}
|
||||
</Text>
|
||||
</MetadataContainer>
|
||||
</ResultItem>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</ResultsContainer>
|
||||
</SearchContainer>
|
||||
</HStack>
|
||||
<Divider style={{ margin: 0, marginTop: 4, borderBlockStartWidth: 0.5 }} />
|
||||
|
||||
<ResultsContainer>
|
||||
{loading ? (
|
||||
<LoadingContainer>
|
||||
<Spin size="large" />
|
||||
</LoadingContainer>
|
||||
) : (
|
||||
<List
|
||||
dataSource={results}
|
||||
renderItem={(item) => (
|
||||
<List.Item>
|
||||
<ResultItem>
|
||||
<MetadataContainer>
|
||||
<Text type="secondary" ellipsis>
|
||||
{t('knowledge.source')}:{' '}
|
||||
{item.file ? (
|
||||
<a href={`http://file/${item.file.name}`} target="_blank" rel="noreferrer">
|
||||
{item.file.origin_name}
|
||||
</a>
|
||||
) : (
|
||||
item.metadata.source
|
||||
)}
|
||||
</Text>
|
||||
<ScoreTag>Score: {(item.score * 100).toFixed(1)}%</ScoreTag>
|
||||
</MetadataContainer>
|
||||
<TagContainer>
|
||||
<Tooltip title={t('common.copy')}>
|
||||
<CopyButton onClick={() => handleCopy(item.pageContent)}>
|
||||
<CopyOutlined />
|
||||
</CopyButton>
|
||||
</Tooltip>
|
||||
</TagContainer>
|
||||
<Paragraph style={{ userSelect: 'text', marginBottom: 0 }}>
|
||||
{highlightText(item.pageContent)}
|
||||
</Paragraph>
|
||||
</ResultItem>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</ResultsContainer>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
const SearchContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
`
|
||||
|
||||
const ResultsContainer = styled.div`
|
||||
max-height: 60vh;
|
||||
padding: 0 16px;
|
||||
overflow-y: auto;
|
||||
max-height: 70vh;
|
||||
`
|
||||
|
||||
const LoadingContainer = styled.div`
|
||||
@ -164,21 +185,29 @@ const LoadingContainer = styled.div`
|
||||
height: 200px;
|
||||
`
|
||||
|
||||
const TagContainer = styled.div`
|
||||
position: absolute;
|
||||
top: 58px;
|
||||
right: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
`
|
||||
|
||||
const ResultItem = styled.div`
|
||||
width: 100%;
|
||||
position: relative;
|
||||
padding: 16px;
|
||||
background: var(--color-background-soft);
|
||||
border-radius: 8px;
|
||||
`
|
||||
|
||||
const TagContainer = styled.div`
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
&:hover {
|
||||
${TagContainer} {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const ScoreTag = styled.div`
|
||||
@ -187,6 +216,7 @@ const ScoreTag = styled.div`
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
flex-shrink: 0;
|
||||
`
|
||||
|
||||
const CopyButton = styled.div`
|
||||
@ -195,7 +225,7 @@ const CopyButton = styled.div`
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: var(--color-background);
|
||||
background: var(--color-background-mute);
|
||||
color: var(--color-text);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
@ -208,12 +238,35 @@ const CopyButton = styled.div`
|
||||
`
|
||||
|
||||
const MetadataContainer = styled.div`
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 8px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
user-select: text;
|
||||
`
|
||||
|
||||
const SearchIcon = styled.div`
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: var(--color-background-soft);
|
||||
margin-right: 2px;
|
||||
&.back-icon {
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
&:hover {
|
||||
background-color: var(--color-background-mute);
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const TopViewKey = 'KnowledgeSearchPopup'
|
||||
|
||||
export default class KnowledgeSearchPopup {
|
||||
|
||||
@ -15,7 +15,6 @@ import { sortBy } from 'lodash'
|
||||
import { ChevronDown } from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface ShowParams {
|
||||
base: KnowledgeBase
|
||||
@ -271,15 +270,6 @@ const PopupContainer: React.FC<Props> = ({ base: _base, resolve }) => {
|
||||
|
||||
const TopViewKey = 'KnowledgeSettingsPopup'
|
||||
|
||||
const AdvancedSettingsButton = styled.div`
|
||||
cursor: pointer;
|
||||
margin-bottom: 16px;
|
||||
margin-top: -10px;
|
||||
color: var(--color-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
export default class KnowledgeSettingsPopup {
|
||||
static hide() {
|
||||
TopView.hide(TopViewKey)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user