mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-27 12:51:26 +08:00
Merge remote-tracking branch 'origin/main' into feat/cherry-store
This commit is contained in:
commit
ce8808b023
@ -28,7 +28,7 @@
|
||||
"dev": "dotenv electron-vite dev",
|
||||
"debug": "electron-vite -- --inspect --sourcemap --remote-debugging-port=9222",
|
||||
"build": "npm run typecheck && electron-vite build",
|
||||
"build:check": "yarn typecheck && yarn check:i18n && yarn test",
|
||||
"build:check": "yarn lint && yarn test",
|
||||
"build:unpack": "dotenv npm run build && electron-builder --dir",
|
||||
"build:win": "dotenv npm run build && electron-builder --win --x64 --arm64",
|
||||
"build:win:x64": "dotenv npm run build && electron-builder --win --x64",
|
||||
@ -66,7 +66,7 @@
|
||||
"test:lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts",
|
||||
"test:scripts": "vitest scripts",
|
||||
"format": "prettier --write .",
|
||||
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
|
||||
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix && yarn typecheck && yarn check:i18n",
|
||||
"prepare": "git config blame.ignoreRevsFile .git-blame-ignore-revs && husky"
|
||||
},
|
||||
"dependencies": {
|
||||
@ -74,6 +74,7 @@
|
||||
"@libsql/client": "0.14.0",
|
||||
"@libsql/win32-x64-msvc": "^0.4.7",
|
||||
"@strongtz/win32-arm64-msvc": "^0.4.7",
|
||||
"express": "^5.1.0",
|
||||
"graceful-fs": "^4.2.11",
|
||||
"jsdom": "26.1.0",
|
||||
"node-stream-zip": "^1.15.0",
|
||||
@ -212,7 +213,6 @@
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||
"eslint-plugin-unused-imports": "^4.1.4",
|
||||
"express": "^5.1.0",
|
||||
"fast-diff": "^1.3.0",
|
||||
"fast-xml-parser": "^5.2.0",
|
||||
"fetch-socks": "1.3.2",
|
||||
|
||||
@ -22,12 +22,14 @@ class ConfigManager {
|
||||
})
|
||||
|
||||
this._config = {
|
||||
enabled: settings?.apiServer?.enabled ?? false,
|
||||
port: settings?.apiServer?.port ?? 23333,
|
||||
host: 'localhost',
|
||||
apiKey: generatedKey
|
||||
}
|
||||
} else {
|
||||
this._config = {
|
||||
enabled: settings?.apiServer?.enabled ?? false,
|
||||
port: settings?.apiServer?.port ?? 23333,
|
||||
host: 'localhost',
|
||||
apiKey: settings.apiServer.apiKey
|
||||
@ -38,6 +40,7 @@ class ConfigManager {
|
||||
} catch (error: any) {
|
||||
logger.warn('Failed to load config from Redux, using defaults:', error)
|
||||
this._config = {
|
||||
enabled: false,
|
||||
port: 23333,
|
||||
host: 'localhost',
|
||||
apiKey: `cs-sk-${uuidv4()}`
|
||||
|
||||
@ -143,9 +143,13 @@ if (!app.requestSingleInstanceLock()) {
|
||||
|
||||
// Start API server if enabled
|
||||
try {
|
||||
await apiServerService.start()
|
||||
const config = await apiServerService.getCurrentConfig()
|
||||
logger.info('API server config:', config)
|
||||
if (config.enabled) {
|
||||
await apiServerService.start()
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.error('Failed to start API server:', error)
|
||||
logger.error('Failed to check/start API server:', error)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@ -11,6 +11,7 @@ interface CustomCollapseProps {
|
||||
defaultActiveKey?: string[]
|
||||
activeKey?: string[]
|
||||
collapsible?: 'header' | 'icon' | 'disabled'
|
||||
onChange?: (activeKeys: string | string[]) => void
|
||||
style?: React.CSSProperties
|
||||
styles?: {
|
||||
header?: React.CSSProperties
|
||||
@ -26,6 +27,7 @@ const CustomCollapse: FC<CustomCollapseProps> = ({
|
||||
defaultActiveKey = ['1'],
|
||||
activeKey,
|
||||
collapsible = undefined,
|
||||
onChange,
|
||||
style,
|
||||
styles
|
||||
}) => {
|
||||
@ -78,7 +80,10 @@ const CustomCollapse: FC<CustomCollapseProps> = ({
|
||||
activeKey={activeKey}
|
||||
destroyInactivePanel={destroyInactivePanel}
|
||||
collapsible={collapsible}
|
||||
onChange={setActiveKeys}
|
||||
onChange={(keys) => {
|
||||
setActiveKeys(keys)
|
||||
onChange?.(keys)
|
||||
}}
|
||||
expandIcon={({ isActive }) => (
|
||||
<ChevronRight
|
||||
size={16}
|
||||
|
||||
57
src/renderer/src/components/ModelList/EditModelPopup.tsx
Normal file
57
src/renderer/src/components/ModelList/EditModelPopup.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import ModelEditContent from '@renderer/components/ModelList/ModelEditContent'
|
||||
import { TopView } from '@renderer/components/TopView'
|
||||
import { Model, Provider } from '@renderer/types'
|
||||
import React from 'react'
|
||||
|
||||
interface ShowParams {
|
||||
provider: Provider
|
||||
model: Model
|
||||
}
|
||||
|
||||
interface Props extends ShowParams {
|
||||
resolve: (data?: Model) => void
|
||||
}
|
||||
|
||||
const PopupContainer: React.FC<Props> = ({ provider, model, resolve }) => {
|
||||
const handleUpdateModel = (updatedModel: Model) => {
|
||||
resolve(updatedModel)
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
resolve(undefined) // Resolve with no data on close
|
||||
}
|
||||
|
||||
return (
|
||||
<ModelEditContent
|
||||
provider={provider}
|
||||
model={model}
|
||||
onUpdateModel={handleUpdateModel}
|
||||
open={true} // Always open when rendered by TopView
|
||||
onClose={handleClose}
|
||||
key={model.id} // Ensure re-mount when model changes
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const TopViewKey = 'EditModelPopup'
|
||||
|
||||
export default class EditModelPopup {
|
||||
static hide() {
|
||||
TopView.hide(TopViewKey)
|
||||
}
|
||||
|
||||
static show(props: ShowParams) {
|
||||
return new Promise<Model | undefined>((resolve) => {
|
||||
TopView.show(
|
||||
<PopupContainer
|
||||
{...props}
|
||||
resolve={(v) => {
|
||||
resolve(v)
|
||||
this.hide()
|
||||
}}
|
||||
/>,
|
||||
TopViewKey
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
281
src/renderer/src/components/ModelList/ManageModelsList.tsx
Normal file
281
src/renderer/src/components/ModelList/ManageModelsList.tsx
Normal file
@ -0,0 +1,281 @@
|
||||
import { MinusOutlined, PlusOutlined } from '@ant-design/icons'
|
||||
import CustomTag from '@renderer/components/CustomTag'
|
||||
import ExpandableText from '@renderer/components/ExpandableText'
|
||||
import ModelIdWithTags from '@renderer/components/ModelIdWithTags'
|
||||
import NewApiBatchAddModelPopup from '@renderer/components/ModelList/NewApiBatchAddModelPopup'
|
||||
import { getModelLogo } from '@renderer/config/models'
|
||||
import FileItem from '@renderer/pages/files/FileItem'
|
||||
import { Model, Provider } from '@renderer/types'
|
||||
import { defaultRangeExtractor, useVirtualizer } from '@tanstack/react-virtual'
|
||||
import { Button, Flex, Tooltip } from 'antd'
|
||||
import { Avatar } from 'antd'
|
||||
import { ChevronRight } from 'lucide-react'
|
||||
import React, { memo, useCallback, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { isModelInProvider, isValidNewApiModel } from './utils'
|
||||
|
||||
// 列表项类型定义
|
||||
interface GroupRowData {
|
||||
type: 'group'
|
||||
groupName: string
|
||||
models: Model[]
|
||||
}
|
||||
|
||||
interface ModelRowData {
|
||||
type: 'model'
|
||||
model: Model
|
||||
}
|
||||
|
||||
type RowData = GroupRowData | ModelRowData
|
||||
|
||||
interface ManageModelsListProps {
|
||||
modelGroups: Record<string, Model[]>
|
||||
provider: Provider
|
||||
onAddModel: (model: Model) => void
|
||||
onRemoveModel: (model: Model) => void
|
||||
}
|
||||
|
||||
const ManageModelsList: React.FC<ManageModelsListProps> = ({ modelGroups, provider, onAddModel, onRemoveModel }) => {
|
||||
const { t } = useTranslation()
|
||||
const scrollerRef = useRef<HTMLDivElement>(null)
|
||||
const activeStickyIndexRef = useRef(0)
|
||||
const [collapsedGroups, setCollapsedGroups] = useState(new Set<string>())
|
||||
|
||||
const handleGroupToggle = useCallback((groupName: string) => {
|
||||
setCollapsedGroups((prev) => {
|
||||
const newSet = new Set(prev)
|
||||
if (newSet.has(groupName)) {
|
||||
newSet.delete(groupName) // 如果已折叠,则展开
|
||||
} else {
|
||||
newSet.add(groupName) // 如果已展开,则折叠
|
||||
}
|
||||
return newSet
|
||||
})
|
||||
}, [])
|
||||
|
||||
// 将分组数据扁平化为单一列表,过滤掉空组
|
||||
const flatRows = useMemo(() => {
|
||||
const rows: RowData[] = []
|
||||
|
||||
Object.entries(modelGroups).forEach(([groupName, models]) => {
|
||||
if (models.length > 0) {
|
||||
// 只添加非空组
|
||||
rows.push({ type: 'group', groupName, models })
|
||||
if (!collapsedGroups.has(groupName)) {
|
||||
models.forEach((model) => {
|
||||
rows.push({ type: 'model', model })
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return rows
|
||||
}, [modelGroups, collapsedGroups])
|
||||
|
||||
// 找到所有组 header 的索引
|
||||
const stickyIndexes = useMemo(() => {
|
||||
return flatRows.map((row, index) => (row.type === 'group' ? index : -1)).filter((index) => index !== -1)
|
||||
}, [flatRows])
|
||||
|
||||
const isSticky = useCallback((index: number) => stickyIndexes.includes(index), [stickyIndexes])
|
||||
|
||||
const isActiveSticky = useCallback((index: number) => activeStickyIndexRef.current === index, [])
|
||||
|
||||
// 自定义 range extractor 用于 sticky header
|
||||
const rangeExtractor = useCallback(
|
||||
(range: any) => {
|
||||
activeStickyIndexRef.current = [...stickyIndexes].reverse().find((index) => range.startIndex >= index) ?? 0
|
||||
const next = new Set([activeStickyIndexRef.current, ...defaultRangeExtractor(range)])
|
||||
return [...next].sort((a, b) => a - b)
|
||||
},
|
||||
[stickyIndexes]
|
||||
)
|
||||
|
||||
const virtualizer = useVirtualizer({
|
||||
count: flatRows.length,
|
||||
getScrollElement: () => scrollerRef.current,
|
||||
estimateSize: () => 42,
|
||||
rangeExtractor,
|
||||
overscan: 5
|
||||
})
|
||||
|
||||
const renderGroupTools = useCallback(
|
||||
(models: Model[]) => {
|
||||
const isAllInProvider = models.every((model) => isModelInProvider(provider, model.id))
|
||||
|
||||
const handleGroupAction = () => {
|
||||
if (isAllInProvider) {
|
||||
// 移除整组
|
||||
models.filter((model) => isModelInProvider(provider, model.id)).forEach(onRemoveModel)
|
||||
} else {
|
||||
// 添加整组
|
||||
const wouldAddModels = models.filter((model) => !isModelInProvider(provider, model.id))
|
||||
|
||||
if (provider.id === 'new-api') {
|
||||
if (wouldAddModels.every(isValidNewApiModel)) {
|
||||
wouldAddModels.forEach(onAddModel)
|
||||
} else {
|
||||
NewApiBatchAddModelPopup.show({
|
||||
title: t('settings.models.add.batch_add_models'),
|
||||
batchModels: wouldAddModels,
|
||||
provider
|
||||
})
|
||||
}
|
||||
} else {
|
||||
wouldAddModels.forEach(onAddModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
destroyTooltipOnHide
|
||||
title={
|
||||
isAllInProvider
|
||||
? t('settings.models.manage.remove_whole_group')
|
||||
: t('settings.models.manage.add_whole_group')
|
||||
}
|
||||
mouseLeaveDelay={0}
|
||||
placement="top">
|
||||
<Button
|
||||
type="text"
|
||||
icon={isAllInProvider ? <MinusOutlined /> : <PlusOutlined />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleGroupAction()
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
)
|
||||
},
|
||||
[provider, onRemoveModel, onAddModel, t]
|
||||
)
|
||||
|
||||
const virtualItems = virtualizer.getVirtualItems()
|
||||
|
||||
return (
|
||||
<ListContainer ref={scrollerRef}>
|
||||
<div
|
||||
style={{
|
||||
height: `${virtualizer.getTotalSize()}px`,
|
||||
width: '100%',
|
||||
position: 'relative'
|
||||
}}>
|
||||
{virtualItems.map((virtualItem) => {
|
||||
const row = flatRows[virtualItem.index]
|
||||
const isRowSticky = isSticky(virtualItem.index)
|
||||
const isRowActiveSticky = isActiveSticky(virtualItem.index)
|
||||
const isCollapsed = row.type === 'group' && collapsedGroups.has(row.groupName)
|
||||
|
||||
if (!row) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
key={virtualItem.index}
|
||||
data-index={virtualItem.index}
|
||||
ref={virtualizer.measureElement}
|
||||
style={{
|
||||
...(isRowSticky
|
||||
? {
|
||||
background: 'var(--color-background)',
|
||||
zIndex: 1
|
||||
}
|
||||
: {}),
|
||||
...(isRowActiveSticky
|
||||
? {
|
||||
position: 'sticky'
|
||||
}
|
||||
: {
|
||||
position: 'absolute',
|
||||
transform: `translateY(${virtualItem.start}px)`
|
||||
}),
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%'
|
||||
}}>
|
||||
{row.type === 'group' ? (
|
||||
<GroupHeader onClick={() => handleGroupToggle(row.groupName)}>
|
||||
<Flex align="center" gap={10} style={{ flex: 1 }}>
|
||||
<ChevronRight
|
||||
size={16}
|
||||
color="var(--color-text-3)"
|
||||
strokeWidth={1.5}
|
||||
style={{ transform: isCollapsed ? 'rotate(0deg)' : 'rotate(90deg)' }}
|
||||
/>
|
||||
<span style={{ fontWeight: 'bold', fontSize: '14px' }}>{row.groupName}</span>
|
||||
<CustomTag color="#02B96B" size={10}>
|
||||
{row.models.length}
|
||||
</CustomTag>
|
||||
</Flex>
|
||||
{renderGroupTools(row.models)}
|
||||
</GroupHeader>
|
||||
) : (
|
||||
<div style={{ padding: '4px 0' }}>
|
||||
<ModelListItem
|
||||
model={row.model}
|
||||
provider={provider}
|
||||
onAddModel={onAddModel}
|
||||
onRemoveModel={onRemoveModel}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</ListContainer>
|
||||
)
|
||||
}
|
||||
|
||||
// 模型列表项组件
|
||||
interface ModelListItemProps {
|
||||
model: Model
|
||||
provider: Provider
|
||||
onAddModel: (model: Model) => void
|
||||
onRemoveModel: (model: Model) => void
|
||||
}
|
||||
|
||||
const ModelListItem: React.FC<ModelListItemProps> = memo(({ model, provider, onAddModel, onRemoveModel }) => {
|
||||
const isAdded = useMemo(() => isModelInProvider(provider, model.id), [provider, model.id])
|
||||
|
||||
return (
|
||||
<FileItem
|
||||
style={{
|
||||
backgroundColor: isAdded ? 'rgba(0, 126, 0, 0.06)' : '',
|
||||
border: 'none',
|
||||
boxShadow: 'none'
|
||||
}}
|
||||
fileInfo={{
|
||||
icon: <Avatar src={getModelLogo(model.id)}>{model?.name?.[0]?.toUpperCase()}</Avatar>,
|
||||
name: <ModelIdWithTags model={model} />,
|
||||
extra: model.description && <ExpandableText text={model.description} />,
|
||||
ext: '.model',
|
||||
actions: isAdded ? (
|
||||
<Button type="text" onClick={() => onRemoveModel(model)} icon={<MinusOutlined />} />
|
||||
) : (
|
||||
<Button type="text" onClick={() => onAddModel(model)} icon={<PlusOutlined />} />
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
const ListContainer = styled.div`
|
||||
height: calc(100vh - 300px);
|
||||
overflow: auto;
|
||||
padding-right: 10px;
|
||||
`
|
||||
|
||||
const GroupHeader = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 8px;
|
||||
min-height: 48px;
|
||||
color: var(--color-text);
|
||||
cursor: pointer;
|
||||
`
|
||||
|
||||
export default memo(ManageModelsList)
|
||||
@ -1,15 +1,10 @@
|
||||
import { MinusOutlined, PlusOutlined } from '@ant-design/icons'
|
||||
import { loggerService } from '@logger'
|
||||
import CustomCollapse from '@renderer/components/CustomCollapse'
|
||||
import CustomTag from '@renderer/components/CustomTag'
|
||||
import ExpandableText from '@renderer/components/ExpandableText'
|
||||
import ModelIdWithTags from '@renderer/components/ModelIdWithTags'
|
||||
import SvgSpinners180Ring from '@renderer/components/Icons/SvgSpinners180Ring'
|
||||
import NewApiAddModelPopup from '@renderer/components/ModelList/NewApiAddModelPopup'
|
||||
import NewApiBatchAddModelPopup from '@renderer/components/ModelList/NewApiBatchAddModelPopup'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { TopView } from '@renderer/components/TopView'
|
||||
import {
|
||||
getModelLogo,
|
||||
groupQwenModels,
|
||||
isEmbeddingModel,
|
||||
isFunctionCallingModel,
|
||||
@ -21,7 +16,6 @@ import {
|
||||
SYSTEM_MODELS
|
||||
} from '@renderer/config/models'
|
||||
import { useProvider } from '@renderer/hooks/useProvider'
|
||||
import FileItem from '@renderer/pages/files/FileItem'
|
||||
import { fetchModels } from '@renderer/services/ApiService'
|
||||
import { Model, Provider } from '@renderer/types'
|
||||
import {
|
||||
@ -31,16 +25,19 @@ import {
|
||||
isFreeModel,
|
||||
runAsyncFunction
|
||||
} from '@renderer/utils'
|
||||
import { Avatar, Button, Empty, Flex, Modal, Spin, Tabs, Tooltip } from 'antd'
|
||||
import { Button, Empty, Flex, Modal, Spin, Tabs, Tooltip } from 'antd'
|
||||
import Input from 'antd/es/input/Input'
|
||||
import { groupBy, isEmpty, uniqBy } from 'lodash'
|
||||
import { debounce } from 'lodash'
|
||||
import { Search } from 'lucide-react'
|
||||
import { memo, useCallback, useEffect, useMemo, useOptimistic, useRef, useState, useTransition } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useOptimistic, useRef, useState, useTransition } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const logger = loggerService.withContext('EditModelsPopup')
|
||||
import ManageModelsList from './ManageModelsList'
|
||||
import { isModelInProvider, isValidNewApiModel } from './utils'
|
||||
|
||||
const logger = loggerService.withContext('ManageModelsPopup')
|
||||
|
||||
interface ShowParams {
|
||||
provider: Provider
|
||||
@ -50,15 +47,6 @@ interface Props extends ShowParams {
|
||||
resolve: (data: any) => void
|
||||
}
|
||||
|
||||
// Check if the model exists in the provider's model list
|
||||
const isModelInProvider = (provider: Provider, modelId: string): boolean => {
|
||||
return provider.models.some((m) => m.id === modelId)
|
||||
}
|
||||
|
||||
const isValidNewApiModel = (model: Model): boolean => {
|
||||
return !!(model.supported_endpoint_types && model.supported_endpoint_types.length > 0)
|
||||
}
|
||||
|
||||
const PopupContainer: React.FC<Props> = ({ provider: _provider, resolve }) => {
|
||||
const [open, setOpen] = useState(true)
|
||||
const { provider, models, addModel, removeModel } = useProvider(_provider.id)
|
||||
@ -286,50 +274,6 @@ const PopupContainer: React.FC<Props> = ({ provider: _provider, resolve }) => {
|
||||
)
|
||||
}, [list, t, loading, provider, onRemoveModel, models, onAddModel])
|
||||
|
||||
const renderGroupTools = useCallback(
|
||||
(group: string) => {
|
||||
const isAllInProvider = modelGroups[group].every((model) => isModelInProvider(provider, model.id))
|
||||
return (
|
||||
<Tooltip
|
||||
destroyTooltipOnHide
|
||||
title={
|
||||
isAllInProvider
|
||||
? t('settings.models.manage.remove_whole_group')
|
||||
: t('settings.models.manage.add_whole_group')
|
||||
}
|
||||
mouseLeaveDelay={0}
|
||||
placement="top">
|
||||
<Button
|
||||
type="text"
|
||||
icon={isAllInProvider ? <MinusOutlined /> : <PlusOutlined />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
if (isAllInProvider) {
|
||||
modelGroups[group].filter((model) => isModelInProvider(provider, model.id)).forEach(onRemoveModel)
|
||||
} else {
|
||||
const wouldAddModel = modelGroups[group].filter((model) => !isModelInProvider(provider, model.id))
|
||||
if (provider.id === 'new-api') {
|
||||
if (wouldAddModel.every(isValidNewApiModel)) {
|
||||
wouldAddModel.forEach(onAddModel)
|
||||
} else {
|
||||
NewApiBatchAddModelPopup.show({
|
||||
title: t('settings.models.add.batch_add_models'),
|
||||
batchModels: wouldAddModel,
|
||||
provider
|
||||
})
|
||||
}
|
||||
} else {
|
||||
wouldAddModel.forEach(onAddModel)
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
)
|
||||
},
|
||||
[modelGroups, provider, onRemoveModel, onAddModel, t]
|
||||
)
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={<ModalHeader />}
|
||||
@ -388,38 +332,15 @@ const PopupContainer: React.FC<Props> = ({ provider: _provider, resolve }) => {
|
||||
<ListContainer>
|
||||
{loading || isFilterTypePending || isSearchPending ? (
|
||||
<Flex justify="center" align="center" style={{ height: '70%' }}>
|
||||
<Spin size="large" />
|
||||
<Spin indicator={<SvgSpinners180Ring color="var(--color-text-2)" />} />
|
||||
</Flex>
|
||||
) : (
|
||||
Object.keys(modelGroups).map((group, i) => {
|
||||
return (
|
||||
<CustomCollapse
|
||||
key={i}
|
||||
defaultActiveKey={['1']}
|
||||
styles={{ body: { padding: '0 10px' } }}
|
||||
label={
|
||||
<Flex align="center" gap={10}>
|
||||
<span style={{ fontWeight: 600 }}>{group}</span>
|
||||
<CustomTag color="#02B96B" size={10}>
|
||||
{modelGroups[group].length}
|
||||
</CustomTag>
|
||||
</Flex>
|
||||
}
|
||||
extra={renderGroupTools(group)}>
|
||||
<FlexColumn style={{ margin: '10px 0' }}>
|
||||
{modelGroups[group].map((model) => (
|
||||
<ModelListItem
|
||||
key={model.id}
|
||||
model={model}
|
||||
provider={provider}
|
||||
onAddModel={onAddModel}
|
||||
onRemoveModel={onRemoveModel}
|
||||
/>
|
||||
))}
|
||||
</FlexColumn>
|
||||
</CustomCollapse>
|
||||
)
|
||||
})
|
||||
<ManageModelsList
|
||||
modelGroups={modelGroups}
|
||||
provider={provider}
|
||||
onAddModel={onAddModel}
|
||||
onRemoveModel={onRemoveModel}
|
||||
/>
|
||||
)}
|
||||
{!(loading || isFilterTypePending || isSearchPending) && isEmpty(list) && (
|
||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={t('settings.models.empty')} />
|
||||
@ -429,38 +350,6 @@ const PopupContainer: React.FC<Props> = ({ provider: _provider, resolve }) => {
|
||||
)
|
||||
}
|
||||
|
||||
interface ModelListItemProps {
|
||||
model: Model
|
||||
provider: Provider
|
||||
onAddModel: (model: Model) => void
|
||||
onRemoveModel: (model: Model) => void
|
||||
}
|
||||
|
||||
const ModelListItem: React.FC<ModelListItemProps> = memo(({ model, provider, onAddModel, onRemoveModel }) => {
|
||||
const isAdded = useMemo(() => isModelInProvider(provider, model.id), [provider, model.id])
|
||||
|
||||
return (
|
||||
<FileItem
|
||||
style={{
|
||||
backgroundColor: isAdded ? 'rgba(0, 126, 0, 0.06)' : '',
|
||||
border: 'none',
|
||||
boxShadow: 'none'
|
||||
}}
|
||||
fileInfo={{
|
||||
icon: <Avatar src={getModelLogo(model.id)}>{model?.name?.[0]?.toUpperCase()}</Avatar>,
|
||||
name: <ModelIdWithTags model={model} />,
|
||||
extra: model.description && <ExpandableText text={model.description} />,
|
||||
ext: '.model',
|
||||
actions: isAdded ? (
|
||||
<Button type="text" onClick={() => onRemoveModel(model)} icon={<MinusOutlined />} />
|
||||
) : (
|
||||
<Button type="text" onClick={() => onAddModel(model)} icon={<PlusOutlined />} />
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
const SearchContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -480,19 +369,8 @@ const TopToolsWrapper = styled.div`
|
||||
margin-bottom: 0;
|
||||
`
|
||||
|
||||
const ListContainer = styled(Scrollbar)`
|
||||
const ListContainer = styled.div`
|
||||
height: calc(100vh - 300px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding-bottom: 30px;
|
||||
`
|
||||
|
||||
const FlexColumn = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-top: 16px;
|
||||
`
|
||||
|
||||
const ModelHeaderTitle = styled.div`
|
||||
@ -502,10 +380,12 @@ const ModelHeaderTitle = styled.div`
|
||||
margin-right: 10px;
|
||||
`
|
||||
|
||||
export default class EditModelsPopup {
|
||||
const TopViewKey = 'ManageModelsPopup'
|
||||
|
||||
export default class ManageModelsPopup {
|
||||
static topviewId = 0
|
||||
static hide() {
|
||||
TopView.hide('EditModelsPopup')
|
||||
TopView.hide(TopViewKey)
|
||||
}
|
||||
static show(props: ShowParams) {
|
||||
return new Promise<any>((resolve) => {
|
||||
@ -517,7 +397,7 @@ export default class EditModelsPopup {
|
||||
this.hide()
|
||||
}}
|
||||
/>,
|
||||
'EditModelsPopup'
|
||||
TopViewKey
|
||||
)
|
||||
})
|
||||
}
|
||||
@ -1,9 +1,11 @@
|
||||
import CollapsibleSearchBar from '@renderer/components/CollapsibleSearchBar'
|
||||
import CustomTag from '@renderer/components/CustomTag'
|
||||
import { StreamlineGoodHealthAndWellBeing } from '@renderer/components/Icons/SVGIcon'
|
||||
import SvgSpinners180Ring from '@renderer/components/Icons/SvgSpinners180Ring'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import AddModelPopup from '@renderer/components/ModelList/AddModelPopup'
|
||||
import EditModelsPopup from '@renderer/components/ModelList/EditModelsPopup'
|
||||
import ModelEditContent from '@renderer/components/ModelList/ModelEditContent'
|
||||
import EditModelPopup from '@renderer/components/ModelList/EditModelPopup'
|
||||
import ManageModelsPopup from '@renderer/components/ModelList/ManageModelsPopup'
|
||||
import NewApiAddModelPopup from '@renderer/components/ModelList/NewApiAddModelPopup'
|
||||
import { PROVIDER_CONFIG } from '@renderer/config/providers'
|
||||
import { useAssistants, useDefaultModel } from '@renderer/hooks/useAssistant'
|
||||
@ -14,10 +16,10 @@ import { useAppDispatch } from '@renderer/store'
|
||||
import { setModel } from '@renderer/store/assistants'
|
||||
import { Model } from '@renderer/types'
|
||||
import { filterModelsByKeywords } from '@renderer/utils'
|
||||
import { Button, Flex, Tooltip } from 'antd'
|
||||
import { Button, Flex, Spin, Tooltip } from 'antd'
|
||||
import { groupBy, sortBy, toPairs } from 'lodash'
|
||||
import { ListCheck, Plus } from 'lucide-react'
|
||||
import React, { memo, startTransition, useCallback, useMemo, useState } from 'react'
|
||||
import React, { memo, startTransition, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import ModelListGroup from './ModelListGroup'
|
||||
@ -27,6 +29,21 @@ interface ModelListProps {
|
||||
providerId: string
|
||||
}
|
||||
|
||||
type ModelGroups = Record<string, Model[]>
|
||||
const MODEL_COUNT_THRESHOLD = 10
|
||||
|
||||
/**
|
||||
* 根据搜索文本筛选模型、分组并排序
|
||||
*/
|
||||
const calculateModelGroups = (models: Model[], searchText: string): ModelGroups => {
|
||||
const filteredModels = searchText ? filterModelsByKeywords(searchText, models) : models
|
||||
const grouped = groupBy(filteredModels, 'group')
|
||||
return sortBy(toPairs(grouped), [0]).reduce((acc, [key, value]) => {
|
||||
acc[key] = value
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
/**
|
||||
* 模型列表组件,用于 CRUD 操作和健康检查
|
||||
*/
|
||||
@ -41,8 +58,13 @@ const ModelList: React.FC<ModelListProps> = ({ providerId }) => {
|
||||
const docsWebsite = providerConfig?.websites?.docs
|
||||
const modelsWebsite = providerConfig?.websites?.models
|
||||
|
||||
const [editingModel, setEditingModel] = useState<Model | null>(null)
|
||||
const [searchText, _setSearchText] = useState('')
|
||||
const [displayedModelGroups, setDisplayedModelGroups] = useState<ModelGroups | null>(() => {
|
||||
if (models.length > MODEL_COUNT_THRESHOLD) {
|
||||
return null
|
||||
}
|
||||
return calculateModelGroups(models, '')
|
||||
})
|
||||
|
||||
const { isChecking: isHealthChecking, modelStatuses, runHealthCheck } = useHealthCheck(provider, models)
|
||||
|
||||
@ -52,20 +74,22 @@ const ModelList: React.FC<ModelListProps> = ({ providerId }) => {
|
||||
})
|
||||
}, [])
|
||||
|
||||
const modelGroups = useMemo(() => {
|
||||
const filteredModels = searchText ? filterModelsByKeywords(searchText, models) : models
|
||||
return groupBy(filteredModels, 'group')
|
||||
}, [searchText, models])
|
||||
useEffect(() => {
|
||||
if (models.length > MODEL_COUNT_THRESHOLD) {
|
||||
startTransition(() => {
|
||||
setDisplayedModelGroups(calculateModelGroups(models, searchText))
|
||||
})
|
||||
} else {
|
||||
setDisplayedModelGroups(calculateModelGroups(models, searchText))
|
||||
}
|
||||
}, [models, searchText])
|
||||
|
||||
const sortedModelGroups = useMemo(() => {
|
||||
return sortBy(toPairs(modelGroups), [0]).reduce((acc, [key, value]) => {
|
||||
acc[key] = value
|
||||
return acc
|
||||
}, {})
|
||||
}, [modelGroups])
|
||||
const modelCount = useMemo(() => {
|
||||
return Object.values(displayedModelGroups ?? {}).reduce((acc, group) => acc + group.length, 0)
|
||||
}, [displayedModelGroups])
|
||||
|
||||
const onManageModel = useCallback(() => {
|
||||
EditModelsPopup.show({ provider })
|
||||
ManageModelsPopup.show({ provider })
|
||||
}, [provider])
|
||||
|
||||
const onAddModel = useCallback(() => {
|
||||
@ -76,10 +100,6 @@ const ModelList: React.FC<ModelListProps> = ({ providerId }) => {
|
||||
}
|
||||
}, [provider, t])
|
||||
|
||||
const onEditModel = useCallback((model: Model) => {
|
||||
setEditingModel(model)
|
||||
}, [])
|
||||
|
||||
const onUpdateModel = useCallback(
|
||||
(updatedModel: Model) => {
|
||||
const updatedModels = models.map((m) => (m.id === updatedModel.id ? updatedModel : m))
|
||||
@ -104,12 +124,27 @@ const ModelList: React.FC<ModelListProps> = ({ providerId }) => {
|
||||
[models, updateProvider, provider.id, assistants, defaultModel, dispatch, setDefaultModel]
|
||||
)
|
||||
|
||||
const onEditModel = useCallback(
|
||||
async (model: Model) => {
|
||||
const updatedModel = await EditModelPopup.show({ provider, model })
|
||||
if (updatedModel) {
|
||||
onUpdateModel(updatedModel)
|
||||
}
|
||||
},
|
||||
[provider, onUpdateModel]
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingSubtitle style={{ marginBottom: 5 }}>
|
||||
<HStack alignItems="center" justifyContent="space-between" style={{ width: '100%' }}>
|
||||
<HStack alignItems="center" gap={8}>
|
||||
<SettingSubtitle style={{ marginTop: 0 }}>{t('common.models')}</SettingSubtitle>
|
||||
{modelCount > 0 && (
|
||||
<CustomTag color="#8c8c8c" size={10}>
|
||||
{modelCount}
|
||||
</CustomTag>
|
||||
)}
|
||||
<CollapsibleSearchBar onSearch={setSearchText} />
|
||||
</HStack>
|
||||
<HStack>
|
||||
@ -123,41 +158,47 @@ const ModelList: React.FC<ModelListProps> = ({ providerId }) => {
|
||||
</HStack>
|
||||
</HStack>
|
||||
</SettingSubtitle>
|
||||
<Flex gap={12} vertical>
|
||||
{Object.keys(sortedModelGroups).map((group, i) => (
|
||||
<ModelListGroup
|
||||
key={group}
|
||||
groupName={group}
|
||||
models={sortedModelGroups[group]}
|
||||
modelStatuses={modelStatuses}
|
||||
defaultOpen={i <= 5}
|
||||
disabled={isHealthChecking}
|
||||
onEditModel={onEditModel}
|
||||
onRemoveModel={removeModel}
|
||||
onRemoveGroup={() => modelGroups[group].forEach((model) => removeModel(model))}
|
||||
/>
|
||||
))}
|
||||
{docsWebsite || modelsWebsite ? (
|
||||
<SettingHelpTextRow>
|
||||
<SettingHelpText>{t('settings.provider.docs_check')} </SettingHelpText>
|
||||
{docsWebsite && (
|
||||
<SettingHelpLink target="_blank" href={docsWebsite}>
|
||||
{getProviderLabel(provider.id) + ' '}
|
||||
{t('common.docs')}
|
||||
</SettingHelpLink>
|
||||
)}
|
||||
{docsWebsite && modelsWebsite && <SettingHelpText>{t('common.and')}</SettingHelpText>}
|
||||
{modelsWebsite && (
|
||||
<SettingHelpLink target="_blank" href={modelsWebsite}>
|
||||
{t('common.models')}
|
||||
</SettingHelpLink>
|
||||
)}
|
||||
<SettingHelpText>{t('settings.provider.docs_more_details')}</SettingHelpText>
|
||||
</SettingHelpTextRow>
|
||||
) : (
|
||||
<div style={{ height: 5 }} />
|
||||
)}
|
||||
</Flex>
|
||||
{displayedModelGroups === null ? (
|
||||
<Flex align="center" justify="center" style={{ minHeight: '8rem' }}>
|
||||
<Spin indicator={<SvgSpinners180Ring color="var(--color-text-2)" />} />
|
||||
</Flex>
|
||||
) : (
|
||||
<Flex gap={12} vertical>
|
||||
{Object.keys(displayedModelGroups).map((group, i) => (
|
||||
<ModelListGroup
|
||||
key={group}
|
||||
groupName={group}
|
||||
models={displayedModelGroups[group]}
|
||||
modelStatuses={modelStatuses}
|
||||
defaultOpen={i <= 5}
|
||||
disabled={isHealthChecking}
|
||||
onEditModel={onEditModel}
|
||||
onRemoveModel={removeModel}
|
||||
onRemoveGroup={() => displayedModelGroups[group].forEach((model) => removeModel(model))}
|
||||
/>
|
||||
))}
|
||||
{docsWebsite || modelsWebsite ? (
|
||||
<SettingHelpTextRow>
|
||||
<SettingHelpText>{t('settings.provider.docs_check')} </SettingHelpText>
|
||||
{docsWebsite && (
|
||||
<SettingHelpLink target="_blank" href={docsWebsite}>
|
||||
{getProviderLabel(provider.id) + ' '}
|
||||
{t('common.docs')}
|
||||
</SettingHelpLink>
|
||||
)}
|
||||
{docsWebsite && modelsWebsite && <SettingHelpText>{t('common.and')}</SettingHelpText>}
|
||||
{modelsWebsite && (
|
||||
<SettingHelpLink target="_blank" href={modelsWebsite}>
|
||||
{t('common.models')}
|
||||
</SettingHelpLink>
|
||||
)}
|
||||
<SettingHelpText>{t('settings.provider.docs_more_details')}</SettingHelpText>
|
||||
</SettingHelpTextRow>
|
||||
) : (
|
||||
<div style={{ height: 5 }} />
|
||||
)}
|
||||
</Flex>
|
||||
)}
|
||||
<Flex gap={10} style={{ marginTop: 10 }}>
|
||||
<Button type="primary" onClick={onManageModel} icon={<ListCheck size={16} />} disabled={isHealthChecking}>
|
||||
{t('button.manage')}
|
||||
@ -166,16 +207,6 @@ const ModelList: React.FC<ModelListProps> = ({ providerId }) => {
|
||||
{t('button.add')}
|
||||
</Button>
|
||||
</Flex>
|
||||
{models.map((model) => (
|
||||
<ModelEditContent
|
||||
provider={provider}
|
||||
model={model}
|
||||
onUpdateModel={onUpdateModel}
|
||||
open={editingModel?.id === model.id}
|
||||
onClose={() => setEditingModel(null)}
|
||||
key={model.id}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -2,8 +2,9 @@ import { MinusOutlined } from '@ant-design/icons'
|
||||
import CustomCollapse from '@renderer/components/CustomCollapse'
|
||||
import { Model } from '@renderer/types'
|
||||
import { ModelWithStatus } from '@renderer/types/healthCheck'
|
||||
import { useVirtualizer } from '@tanstack/react-virtual'
|
||||
import { Button, Flex, Tooltip } from 'antd'
|
||||
import React, { memo } from 'react'
|
||||
import React, { memo, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
@ -31,11 +32,35 @@ const ModelListGroup: React.FC<ModelListGroupProps> = ({
|
||||
onRemoveGroup
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const scrollerRef = useRef<HTMLDivElement>(null)
|
||||
const [isExpanded, setIsExpanded] = useState(defaultOpen)
|
||||
|
||||
const virtualizer = useVirtualizer({
|
||||
count: models.length,
|
||||
getScrollElement: () => scrollerRef.current,
|
||||
estimateSize: () => 52,
|
||||
overscan: 5
|
||||
})
|
||||
|
||||
const virtualItems = virtualizer.getVirtualItems()
|
||||
|
||||
// 监听折叠面板状态变化,确保虚拟列表在展开时正确渲染
|
||||
useEffect(() => {
|
||||
if (isExpanded && scrollerRef.current) {
|
||||
requestAnimationFrame(() => virtualizer.measure())
|
||||
}
|
||||
}, [isExpanded, virtualizer])
|
||||
|
||||
const handleCollapseChange = (activeKeys: string[] | string) => {
|
||||
const isNowExpanded = Array.isArray(activeKeys) ? activeKeys.length > 0 : !!activeKeys
|
||||
setIsExpanded(isNowExpanded)
|
||||
}
|
||||
|
||||
return (
|
||||
<CustomCollapseWrapper>
|
||||
<CustomCollapse
|
||||
defaultActiveKey={defaultOpen ? ['1'] : []}
|
||||
onChange={handleCollapseChange}
|
||||
label={
|
||||
<Flex align="center" gap={10}>
|
||||
<span style={{ fontWeight: 'bold' }}>{groupName}</span>
|
||||
@ -47,23 +72,53 @@ const ModelListGroup: React.FC<ModelListGroupProps> = ({
|
||||
type="text"
|
||||
className="toolbar-item"
|
||||
icon={<MinusOutlined />}
|
||||
onClick={onRemoveGroup}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onRemoveGroup()
|
||||
}}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Tooltip>
|
||||
}>
|
||||
<Flex gap={10} vertical style={{ marginTop: 10 }}>
|
||||
{models.map((model) => (
|
||||
<ModelListItem
|
||||
key={model.id}
|
||||
model={model}
|
||||
modelStatus={modelStatuses.find((status) => status.model.id === model.id)}
|
||||
onEdit={onEditModel}
|
||||
onRemove={onRemoveModel}
|
||||
disabled={disabled}
|
||||
/>
|
||||
))}
|
||||
</Flex>
|
||||
<ScrollContainer ref={scrollerRef}>
|
||||
<div
|
||||
style={{
|
||||
height: `${virtualizer.getTotalSize()}px`,
|
||||
width: '100%',
|
||||
position: 'relative'
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
transform: `translateY(${virtualItems[0]?.start ?? 0}px)`
|
||||
}}>
|
||||
{virtualItems.map((virtualItem) => {
|
||||
const model = models[virtualItem.index]
|
||||
return (
|
||||
<div
|
||||
key={virtualItem.key}
|
||||
data-index={virtualItem.index}
|
||||
ref={virtualizer.measureElement}
|
||||
style={{
|
||||
/* 在这里调整 item 间距 */
|
||||
padding: '4px 0'
|
||||
}}>
|
||||
<ModelListItem
|
||||
model={model}
|
||||
modelStatus={modelStatuses.find((status) => status.model.id === model.id)}
|
||||
onEdit={onEditModel}
|
||||
onRemove={onRemoveModel}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</ScrollContainer>
|
||||
</CustomCollapse>
|
||||
</CustomCollapseWrapper>
|
||||
)
|
||||
@ -79,6 +134,17 @@ const CustomCollapseWrapper = styled.div`
|
||||
&:hover .toolbar-item {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 移除 collapse 的 padding,转而在 scroller 内部调整 */
|
||||
.ant-collapse-content-box {
|
||||
padding: 0 !important;
|
||||
}
|
||||
`
|
||||
|
||||
const ScrollContainer = styled.div`
|
||||
overflow-y: auto;
|
||||
max-height: 390px;
|
||||
padding: 4px 16px;
|
||||
`
|
||||
|
||||
export default memo(ModelListGroup)
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
export { default as AddModelPopup } from './AddModelPopup'
|
||||
export { default as EditModelsPopup } from './EditModelsPopup'
|
||||
export { default as EditModelPopup } from './EditModelPopup'
|
||||
export { default as HealthCheckPopup } from './HealthCheckPopup'
|
||||
export { default as ManageModelsPopup } from './ManageModelsPopup'
|
||||
export { default as ModelEditContent } from './ModelEditContent'
|
||||
export { default as ModelList } from './ModelList'
|
||||
export { default as NewApiAddModelPopup } from './NewApiAddModelPopup'
|
||||
|
||||
10
src/renderer/src/components/ModelList/utils.ts
Normal file
10
src/renderer/src/components/ModelList/utils.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { Model, Provider } from '@renderer/types'
|
||||
|
||||
// Check if the model exists in the provider's model list
|
||||
export const isModelInProvider = (provider: Provider, modelId: string): boolean => {
|
||||
return provider.models.some((m) => m.id === modelId)
|
||||
}
|
||||
|
||||
export const isValidNewApiModel = (model: Model): boolean => {
|
||||
return !!(model.supported_endpoint_types && model.supported_endpoint_types.length > 0)
|
||||
}
|
||||
@ -199,7 +199,8 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
}}
|
||||
/>
|
||||
}
|
||||
arrow>
|
||||
arrow
|
||||
trigger="click">
|
||||
<Button icon={emoji && <span style={{ fontSize: 20 }}>{emoji}</span>}>{t('common.select')}</Button>
|
||||
</Popover>
|
||||
</Form.Item>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { loggerService } from '@renderer/services/LoggerService'
|
||||
import { RootState, useAppDispatch } from '@renderer/store'
|
||||
import { setApiServerApiKey, setApiServerPort } from '@renderer/store/settings'
|
||||
import { setApiServerApiKey, setApiServerEnabled, setApiServerPort } from '@renderer/store/settings'
|
||||
import { IpcChannel } from '@shared/IpcChannel'
|
||||
import { Button, Input, InputNumber, Tooltip, Typography } from 'antd'
|
||||
import { Copy, ExternalLink, Play, RotateCcw, Square } from 'lucide-react'
|
||||
@ -64,6 +64,7 @@ const ApiServerSettings: FC = () => {
|
||||
} catch (error) {
|
||||
window.message.error(t('apiServer.messages.operationFailed') + (error as Error).message)
|
||||
} finally {
|
||||
dispatch(setApiServerEnabled(enabled))
|
||||
setApiServerLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
@ -109,7 +109,7 @@ const AssistantSettings: FC = () => {
|
||||
theme={theme}>
|
||||
<SettingSubtitle style={{ marginTop: 0 }}>{t('common.name')}</SettingSubtitle>
|
||||
<HStack gap={8} alignItems="center">
|
||||
<Popover content={<EmojiPicker onEmojiClick={handleEmojiSelect} />} arrow>
|
||||
<Popover content={<EmojiPicker onEmojiClick={handleEmojiSelect} />} arrow trigger="click">
|
||||
<EmojiButtonWrapper>
|
||||
<Button style={{ fontSize: 20, padding: '4px', minWidth: '30px', height: '30px' }}>{emoji}</Button>
|
||||
{emoji && (
|
||||
|
||||
@ -19,7 +19,7 @@ import {
|
||||
} from '@renderer/utils'
|
||||
import { Avatar, Button, Card, Dropdown, Input, MenuProps, Tag } from 'antd'
|
||||
import { Eye, EyeOff, Search, UserPen } from 'lucide-react'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { FC, startTransition, useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
import styled from 'styled-components'
|
||||
@ -34,12 +34,19 @@ const ProvidersList: FC = () => {
|
||||
const [searchParams] = useSearchParams()
|
||||
const providers = useAllProviders()
|
||||
const { updateProviders, addProvider, removeProvider, updateProvider } = useProviders()
|
||||
const [selectedProvider, setSelectedProvider] = useState<Provider>(providers[0])
|
||||
const [selectedProvider, _setSelectedProvider] = useState<Provider>(providers[0])
|
||||
const { t } = useTranslation()
|
||||
const [searchText, setSearchText] = useState<string>('')
|
||||
const [dragging, setDragging] = useState(false)
|
||||
const [providerLogos, setProviderLogos] = useState<Record<string, string>>({})
|
||||
|
||||
const setSelectedProvider = useCallback(
|
||||
(provider: Provider) => {
|
||||
startTransition(() => _setSelectedProvider(provider))
|
||||
},
|
||||
[_setSelectedProvider]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const loadAllLogos = async () => {
|
||||
const logos: Record<string, string> = {}
|
||||
@ -71,7 +78,7 @@ const ProvidersList: FC = () => {
|
||||
setSelectedProvider(providers[0])
|
||||
}
|
||||
}
|
||||
}, [providers, searchParams])
|
||||
}, [providers, searchParams, setSelectedProvider])
|
||||
|
||||
// Handle provider add key from URL schema
|
||||
useEffect(() => {
|
||||
|
||||
@ -312,16 +312,18 @@ async function fetchExternalTool(
|
||||
let memorySearchReferences: MemoryItem[] | undefined
|
||||
|
||||
const parentSpanId = currentSpan(lastUserMessage.topicId, assistant.model?.name)?.spanContext().spanId
|
||||
// 并行执行搜索
|
||||
if (shouldWebSearch || shouldKnowledgeSearch || shouldSearchMemory) {
|
||||
;[webSearchResponseFromSearch, knowledgeReferencesFromSearch, memorySearchReferences] = await Promise.all([
|
||||
searchTheWeb(extractResults, parentSpanId),
|
||||
searchKnowledgeBase(extractResults, parentSpanId, assistant.model?.name),
|
||||
searchMemory()
|
||||
])
|
||||
if (shouldWebSearch) {
|
||||
webSearchResponseFromSearch = await searchTheWeb(extractResults, parentSpanId)
|
||||
}
|
||||
|
||||
if (shouldKnowledgeSearch) {
|
||||
knowledgeReferencesFromSearch = await searchKnowledgeBase(extractResults, parentSpanId, assistant.model?.name)
|
||||
}
|
||||
|
||||
if (shouldSearchMemory) {
|
||||
memorySearchReferences = await searchMemory()
|
||||
}
|
||||
|
||||
// 存储搜索结果
|
||||
if (lastUserMessage) {
|
||||
if (webSearchResponseFromSearch) {
|
||||
window.keyv.set(`web-search-${lastUserMessage.id}`, webSearchResponseFromSearch)
|
||||
@ -492,7 +494,7 @@ export async function fetchChatCompletion({
|
||||
// Post-conversation memory processing
|
||||
const globalMemoryEnabled = selectGlobalMemoryEnabled(store.getState())
|
||||
if (globalMemoryEnabled && assistant.enableMemory) {
|
||||
await processConversationMemory(messages, assistant)
|
||||
processConversationMemory(messages, assistant)
|
||||
}
|
||||
|
||||
return await AI.completionsForTrace(completionsParams, requestOptions)
|
||||
|
||||
@ -1925,6 +1925,7 @@ const migrateConfig = {
|
||||
// Initialize API server configuration if not present
|
||||
if (!state.settings.apiServer) {
|
||||
state.settings.apiServer = {
|
||||
enabled: false,
|
||||
host: 'localhost',
|
||||
port: 23333,
|
||||
apiKey: `cs-sk-${uuid()}`
|
||||
|
||||
@ -391,6 +391,7 @@ export const initialState: SettingsState = {
|
||||
navbarPosition: 'left',
|
||||
// API Server
|
||||
apiServer: {
|
||||
enabled: false,
|
||||
host: 'localhost',
|
||||
port: 23333,
|
||||
apiKey: `cs-sk-${uuid()}`
|
||||
@ -800,6 +801,12 @@ const settingsSlice = createSlice({
|
||||
state.navbarPosition = action.payload
|
||||
},
|
||||
// API Server actions
|
||||
setApiServerEnabled: (state, action: PayloadAction<boolean>) => {
|
||||
state.apiServer = {
|
||||
...state.apiServer,
|
||||
enabled: action.payload
|
||||
}
|
||||
},
|
||||
setApiServerPort: (state, action: PayloadAction<number>) => {
|
||||
state.apiServer = {
|
||||
...state.apiServer,
|
||||
@ -935,6 +942,7 @@ export const {
|
||||
setEnableDeveloperMode,
|
||||
setNavbarPosition,
|
||||
// API Server actions
|
||||
setApiServerEnabled,
|
||||
setApiServerPort,
|
||||
setApiServerApiKey
|
||||
} = settingsSlice.actions
|
||||
|
||||
@ -842,6 +842,7 @@ export type S3Config = {
|
||||
export type { Message } from './newMessage'
|
||||
|
||||
export interface ApiServerConfig {
|
||||
enabled: boolean
|
||||
host: string
|
||||
port: number
|
||||
apiKey: string
|
||||
|
||||
Loading…
Reference in New Issue
Block a user