fix: handle optional list length in DraggableVirtualList and update padding in QuickPanel

This commit is contained in:
kangfenmao 2025-07-17 13:39:11 +08:00
parent f01b7075fb
commit 30b080efbd
15 changed files with 115 additions and 86 deletions

View File

@ -226,10 +226,11 @@ const StyledModal = styled(Modal)<{ $isFullscreen?: boolean }>`
}
.ant-modal-header {
padding: 10px 12px !important;
padding: 10px !important;
border-bottom: 1px solid var(--color-border);
background: var(--color-background);
margin-bottom: 0 !important;
border-radius: 0 !important;
}
`

View File

@ -82,7 +82,7 @@ function DraggableVirtualList<T>({
const parentRef = useRef<HTMLDivElement>(null)
const virtualizer = useVirtualizer({
count: list.length,
count: list?.length ?? 0,
getScrollElement: useCallback(() => parentRef.current, []),
getItemKey: itemKey,
estimateSize: useCallback(() => 50, []),

View File

@ -611,7 +611,7 @@ const QuickPanelContainer = styled.div<{
left: 0;
right: 0;
width: 100%;
padding: 0 30px 0 30px;
padding: 0 35px 0 35px;
transform: translateY(-100%);
transform-origin: bottom;
transition: max-height 0.2s ease;

View File

@ -410,6 +410,7 @@ export const REFERENCE_PROMPT = `Please answer the question based on the referen
- Please cite the context at the end of sentences when appropriate.
- Please use the format of citation number [number] to reference the context in corresponding parts of your answer.
- If a sentence comes from multiple contexts, please list all relevant citation numbers, e.g., [1][2]. Remember not to group citations at the end but list them in the corresponding parts of your answer.
- If all reference content is not relevant to the user's question, please answer based on your knowledge.
## My question is:

View File

@ -460,7 +460,8 @@
"swap": "Swap",
"topics": "Topics",
"warning": "Warning",
"you": "You"
"you": "You",
"i_know": "I know"
},
"docs": {
"title": "Docs"
@ -2308,7 +2309,7 @@
},
"provider": "OCR Provider",
"provider_placeholder": "Choose an OCR provider",
"title": "OCR"
"title": "OCR Settings"
},
"preprocess": {
"provider": "Pre Process Provider",
@ -2557,7 +2558,8 @@
"please_select_embedding_model": "Please select an embedding model",
"select_embedding_model_placeholder": "Select Embedding Model",
"embedding_dimensions": "Embedding Dimensions",
"stored_memories": "Stored Memories"
"stored_memories": "Stored Memories",
"global_memory_description": "To use memory features, please enable global memory in assistant settings."
}
}
}

View File

@ -460,7 +460,8 @@
"swap": "交換",
"topics": "トピック",
"warning": "警告",
"you": "あなた"
"you": "あなた",
"i_know": "わかりました"
},
"docs": {
"title": "ドキュメント"
@ -2455,6 +2456,7 @@
"visualization": "可視化"
},
"memory": {
"title": "グローバルメモリ",
"add_memory": "メモリーを追加",
"edit_memory": "メモリーを編集",
"memory_content": "メモリー内容",
@ -2476,7 +2478,6 @@
"user": "ユーザー",
"content": "内容",
"score": "スコア",
"title": "メモリー",
"memories_description": "{{total}}件中{{count}}件のメモリーを表示",
"search_placeholder": "メモリーを検索...",
"start_date": "開始日",
@ -2557,7 +2558,8 @@
"please_select_embedding_model": "埋め込みモデルを選択してください",
"select_embedding_model_placeholder": "埋め込みモデルを選択",
"embedding_dimensions": "埋め込み次元",
"stored_memories": "保存された記憶"
"stored_memories": "保存された記憶",
"global_memory_description": "メモリ機能を使用するには、アシスタント設定でグローバルメモリを有効にしてください。"
}
}
}

View File

@ -460,7 +460,8 @@
"swap": "Поменять местами",
"topics": "Топики",
"warning": "Предупреждение",
"you": "Вы"
"you": "Вы",
"i_know": "Я понял"
},
"docs": {
"title": "Документация"
@ -2455,6 +2456,7 @@
"visualization": "Визуализация"
},
"memory": {
"title": "Глобальная память",
"add_memory": "Добавить память",
"edit_memory": "Редактировать память",
"memory_content": "Содержимое памяти",
@ -2531,7 +2533,6 @@
"total_memories": "всего воспоминаний",
"default": "По умолчанию",
"custom": "Пользовательский",
"title": "Воспоминания",
"description": "Память позволяет хранить и управлять информацией о ваших взаимодействиях с ассистентом. Вы можете добавлять, редактировать и удалять воспоминания, а также фильтровать и искать их.",
"global_memory_enabled": "Глобальная память включена",
"global_memory": "Глобальная память",
@ -2557,7 +2558,8 @@
"please_select_embedding_model": "Пожалуйста, выберите модель для внедрения",
"select_embedding_model_placeholder": "Выберите модель внедрения",
"embedding_dimensions": "Размерность вложения",
"stored_memories": "Запасённые воспоминания"
"stored_memories": "Запасённые воспоминания",
"global_memory_description": "Для использования функций памяти необходимо включить глобальную память в настройках ассистента."
}
}
}

View File

@ -460,7 +460,8 @@
"swap": "交换",
"topics": "话题",
"warning": "警告",
"you": "用户"
"you": "用户",
"i_know": "我知道了"
},
"docs": {
"title": "帮助文档"
@ -2308,7 +2309,7 @@
},
"provider": "OCR 服务商",
"provider_placeholder": "选择一个 OCR 服务商",
"title": "OCR"
"title": "OCR 文字识别"
},
"preprocess": {
"provider": "文档预处理服务商",
@ -2455,6 +2456,7 @@
"visualization": "可视化"
},
"memory": {
"title": "全局记忆",
"settings": "设置",
"statistics": "统计",
"search": "搜索",
@ -2464,8 +2466,8 @@
"memory_content": "记忆内容",
"please_enter_memory": "请输入记忆内容",
"memory_placeholder": "输入记忆内容...",
"user_id": "用户ID",
"user_id_placeholder": "输入用户ID可选",
"user_id": "用户 ID",
"user_id_placeholder": "输入用户 ID可选",
"load_failed": "加载记忆失败",
"add_success": "记忆添加成功",
"add_failed": "添加记忆失败",
@ -2480,7 +2482,6 @@
"user": "用户",
"content": "内容",
"score": "分数",
"title": "记忆",
"memories_description": "显示 {{count}} / {{total}} 条记忆",
"search_placeholder": "搜索记忆...",
"start_date": "开始日期",
@ -2557,7 +2558,8 @@
"please_select_embedding_model": "请选择嵌入模型",
"select_embedding_model_placeholder": "选择嵌入模型",
"embedding_dimensions": "嵌入维度",
"stored_memories": "已存储记忆"
"stored_memories": "已存储记忆",
"global_memory_description": "需要开启助手设置中的全局记忆才能使用"
}
}
}

View File

@ -460,7 +460,8 @@
"swap": "交換",
"topics": "話題",
"warning": "警告",
"you": "您"
"you": "您",
"i_know": "我知道了"
},
"docs": {
"title": "說明文件"
@ -2308,7 +2309,7 @@
},
"provider": "OCR 供應商",
"provider_placeholder": "選擇一個OCR服務提供商",
"title": "光學字符識別"
"title": "OCR 文字識別"
},
"preprocess": {
"provider": "前置處理供應商",
@ -2455,6 +2456,7 @@
"visualization": "視覺化"
},
"memory": {
"title": "全域記憶",
"add_memory": "新增記憶",
"edit_memory": "編輯記憶",
"memory_content": "記憶內容",
@ -2476,7 +2478,6 @@
"user": "使用者",
"content": "內容",
"score": "分數",
"title": "記憶",
"memories_description": "顯示 {{count}} / {{total}} 條記憶",
"search_placeholder": "搜尋記憶...",
"start_date": "開始日期",
@ -2557,7 +2558,8 @@
"please_select_embedding_model": "請選擇一個嵌入模型",
"select_embedding_model_placeholder": "選擇嵌入模型",
"embedding_dimensions": "嵌入維度",
"stored_memories": "儲存的記憶"
"stored_memories": "儲存的記憶",
"global_memory_description": "需要開啟助手設定中的全域記憶才能使用"
}
}
}

View File

@ -960,7 +960,7 @@ const InputBarContainer = styled.div`
border: 0.5px solid var(--color-border);
transition: all 0.2s ease;
position: relative;
border-radius: 20px;
border-radius: 17px;
padding-top: 8px; // 为拖动手柄留出空间
background-color: var(--color-background-opacity);

View File

@ -6,7 +6,7 @@ import { useKnowledge } from '@renderer/hooks/useKnowledge'
import FileItem from '@renderer/pages/files/FileItem'
import { getProviderName } from '@renderer/services/ProviderService'
import { KnowledgeBase, KnowledgeItem } from '@renderer/types'
import { Button, Dropdown, message, Tooltip } from 'antd'
import { Button, Dropdown, Tooltip } from 'antd'
import dayjs from 'dayjs'
import { Plus } from 'lucide-react'
import { FC } from 'react'
@ -72,7 +72,7 @@ const KnowledgeUrls: FC<KnowledgeContentProps> = ({ selectedBase }) => {
if (!urlItems.find((item) => item.content === url.trim())) {
addUrl(url.trim())
} else {
message.success(t('knowledge.url_added'))
window.message.success(t('knowledge.url_added'))
}
} catch (e) {
// Skip invalid URLs silently
@ -143,7 +143,7 @@ const KnowledgeUrls: FC<KnowledgeContentProps> = ({ selectedBase }) => {
label: t('common.copy'),
onClick: () => {
navigator.clipboard.writeText(item.content as string)
message.success(t('message.copied'))
window.message.success(t('message.copied'))
}
}
]

View File

@ -539,8 +539,6 @@ const MemoriesPage = () => {
title: t('memory.delete_user_confirm_title'),
content: t('memory.delete_user_confirm_content', { user: userId }),
icon: <ExclamationCircleOutlined />,
okText: t('common.yes'),
cancelText: t('common.no'),
okType: 'danger',
onOk: async () => {
try {

View File

@ -1,10 +1,11 @@
import { InfoCircleOutlined, SettingOutlined } from '@ant-design/icons'
import { InfoCircleOutlined } from '@ant-design/icons'
import { Box } from '@renderer/components/Layout'
import MemoryService from '@renderer/services/MemoryService'
import { selectGlobalMemoryEnabled, selectMemoryConfig } from '@renderer/store/memory'
import { Assistant, AssistantSettings } from '@renderer/types'
import { Alert, Button, Card, Space, Switch, Tooltip, Typography } from 'antd'
import { useForm } from 'antd/es/form/Form'
import { Settings2 } from 'lucide-react'
import { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
@ -78,9 +79,7 @@ const AssistantMemorySettings: React.FC<Props> = ({ assistant, updateAssistant,
</Tooltip>
</Box>
<Space>
<Button size="small" icon={<SettingOutlined />} onClick={handleNavigateToMemory}>
{t('common.settings')}
</Button>
<Button type="text" icon={<Settings2 size={15} />} onClick={handleNavigateToMemory} />
<Tooltip
title={
!globalMemoryEnabled

View File

@ -6,16 +6,18 @@ import {
MoreOutlined,
PlusOutlined,
ReloadOutlined,
SettingOutlined,
UserAddOutlined,
UserDeleteOutlined,
UserOutlined
} from '@ant-design/icons'
import { HStack } from '@renderer/components/Layout'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useModel } from '@renderer/hooks/useModel'
import MemoryService from '@renderer/services/MemoryService'
import {
selectCurrentUserId,
selectGlobalMemoryEnabled,
selectMemoryConfig,
setCurrentUserId,
setGlobalMemoryEnabled
} from '@renderer/store/memory'
@ -37,7 +39,7 @@ import {
} from 'antd'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import { Brain } from 'lucide-react'
import { Brain, Settings2 } from 'lucide-react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useDispatch, useSelector } from 'react-redux'
@ -123,10 +125,7 @@ const AddMemoryModal: React.FC<AddMemoryModalProps> = ({ visible, onCancel, onAd
}
}}>
<Form form={form} layout="vertical" onFinish={handleSubmit}>
<Form.Item
label={t('memory.memory_content')}
name="memory"
rules={[{ required: true, message: t('memory.please_enter_memory') }]}>
<Form.Item name="memory" rules={[{ required: true, message: t('memory.please_enter_memory') }]}>
<TextArea
rows={5}
placeholder={t('memory.memory_placeholder')}
@ -502,11 +501,16 @@ const MemorySettings = () => {
const handleSettingsSubmit = async () => {
setSettingsModalVisible(false)
await memoryService.updateConfig()
if (window.keyv.get('memory.wait.settings')) {
window.keyv.remove('memory.wait.settings')
dispatch(setGlobalMemoryEnabled(true))
}
}
const handleSettingsCancel = () => {
setSettingsModalVisible(false)
form.resetFields()
window.keyv.remove('memory.wait.settings')
}
const handleResetMemories = async (userId: string) => {
@ -544,8 +548,6 @@ const MemorySettings = () => {
title: t('memory.delete_user_confirm_title'),
content: t('memory.delete_user_confirm_content', { user: userId }),
icon: <ExclamationCircleOutlined />,
okText: t('common.yes'),
cancelText: t('common.no'),
okType: 'danger',
onOk: async () => {
try {
@ -569,9 +571,32 @@ const MemorySettings = () => {
})
}
const handleGlobalMemoryToggle = (enabled: boolean) => {
const memoryConfig = useSelector(selectMemoryConfig)
const embedderModel = useModel(memoryConfig.embedderApiClient?.model, memoryConfig.embedderApiClient?.provider)
const handleGlobalMemoryToggle = async (enabled: boolean) => {
if (enabled && !embedderModel) {
window.keyv.set('memory.wait.settings', true)
return setSettingsModalVisible(true)
}
dispatch(setGlobalMemoryEnabled(enabled))
window.message.success(enabled ? t('memory.global_memory_enabled') : t('memory.global_memory_disabled_title'))
if (enabled) {
return window.modal.confirm({
centered: true,
title: t('memory.global_memory_enabled'),
content: t('memory.global_memory_description'),
okText: t('common.i_know'),
cancelButtonProps: {
style: {
display: 'none'
}
}
})
}
window.message.success(t('memory.global_memory_disabled_title'))
}
const { theme } = useTheme()
@ -579,33 +604,27 @@ const MemorySettings = () => {
return (
<SettingContainer theme={theme}>
{/* Memory Settings */}
<SettingGroup theme={theme}>
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', gap: '2px' }}>
<SettingTitle>{t('memory.settings')}</SettingTitle>
<span
style={{
fontSize: '12px',
color: 'var(--color-primary)',
background: 'var(--color-primary-bg)',
padding: '2px 6px',
borderRadius: '4px',
fontWeight: '500'
}}>
Beta
</span>
</div>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('memory.global_memory')}</SettingRowTitle>
<Switch checked={globalMemoryEnabled} onChange={handleGlobalMemoryToggle} />
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('memory.settings')}</SettingRowTitle>
<Button icon={<SettingOutlined />} onClick={() => setSettingsModalVisible(true)}>
{t('common.settings')}
</Button>
</SettingRow>
<SettingGroup style={{ justifyContent: 'space-between', alignItems: 'center' }} theme={theme}>
<HStack style={{ justifyContent: 'space-between', alignItems: 'center' }}>
<HStack style={{ alignItems: 'center', gap: '2px' }}>
<SettingRowTitle>{t('memory.global_memory')}</SettingRowTitle>
<span
style={{
fontSize: '12px',
color: 'var(--color-primary)',
background: 'var(--color-primary-bg)',
padding: '2px 6px',
borderRadius: '4px',
fontWeight: '500'
}}>
Beta
</span>
</HStack>
<HStack style={{ alignItems: 'center', gap: 10 }}>
<Switch checked={globalMemoryEnabled} onChange={handleGlobalMemoryToggle} />
<Button icon={<Settings2 size={16} />} onClick={() => setSettingsModalVisible(true)} />
</HStack>
</HStack>
</SettingGroup>
{/* User Management */}
@ -636,32 +655,32 @@ const MemorySettings = () => {
alignItems: 'center',
justifyContent: 'flex-start'
}}>
<Space align="center">
<HStack alignItems="center" gap={10}>
<UserAddOutlined />
<span>{t('memory.add_new_user')}</span>
</Space>
</HStack>
</Button>
</div>
</>
)}>
<Option value={DEFAULT_USER_ID}>
<Space align="center">
<HStack alignItems="center" gap={10}>
<Avatar size={20} style={{ background: 'var(--color-primary)' }}>
{getUserAvatar(DEFAULT_USER_ID)}
</Avatar>
<span>{t('memory.default_user')}</span>
</Space>
</HStack>
</Option>
{uniqueUsers
.filter((user) => user !== DEFAULT_USER_ID)
.map((user) => (
<Option key={user} value={user}>
<Space align="center">
<HStack alignItems="center" gap={10}>
<Avatar size={20} style={{ background: 'var(--color-primary)' }}>
{getUserAvatar(user)}
</Avatar>
<span>{user}</span>
</Space>
</HStack>
</Option>
))}
</Select>
@ -736,7 +755,8 @@ const MemorySettings = () => {
</Dropdown>
</Space>
</div>
<SettingDivider />
<SettingDivider style={{ marginBottom: 15 }} />
{/* Memory Content Area */}
<div style={{ minHeight: 400 }}>
@ -867,7 +887,7 @@ const MemorySettings = () => {
const MemoryListContainer = styled.div`
display: flex;
flex-direction: column;
gap: 8px;
gap: 15px;
max-height: 500px;
overflow-y: auto;
`
@ -876,7 +896,7 @@ const MemoryItem = styled.div`
padding: 12px;
background: var(--color-background-soft);
border: 1px solid var(--color-border);
border-radius: var(--list-item-border-radius);
border-radius: 10px;
transition: all 0.2s ease;
&:hover {

View File

@ -590,7 +590,7 @@ const TranslatePage: FC = () => {
<ContentContainer id="content-container" ref={contentContainerRef} $historyDrawerVisible={historyDrawerVisible}>
<HistoryContainer $historyDrawerVisible={historyDrawerVisible}>
<OperationBar>
<span style={{ fontSize: 16 }}>{t('translate.history.title')}</span>
<span style={{ fontSize: 14 }}>{t('translate.history.title')}</span>
{!isEmpty(translateHistory) && (
<Popconfirm
title={t('translate.history.clear')}
@ -623,13 +623,8 @@ const TranslatePage: FC = () => {
<Flex justify="space-between" vertical gap={4} style={{ width: '100%' }}>
<Flex align="center" justify="space-between" style={{ flex: 1 }}>
<Flex align="center" gap={6}>
<span>
{item._sourceLanguage.emoji} {item._sourceLanguage.label()}
</span>
<span>
{item._targetLanguage.emoji} {item._targetLanguage.label()}
</span>
<HistoryListItemLanguage>{item._sourceLanguage.label()} </HistoryListItemLanguage>
<HistoryListItemLanguage>{item._targetLanguage.label()}</HistoryListItemLanguage>
</Flex>
<HistoryListItemDate>{dayjs(item.createdAt).format('MM/DD HH:mm')}</HistoryListItemDate>
</Flex>
@ -933,4 +928,9 @@ const HistoryListItemDate = styled.div`
color: var(--color-text-3);
`
const HistoryListItemLanguage = styled.div`
font-size: 12px;
color: var(--color-text-3);
`
export default TranslatePage