mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-08 06:19:05 +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",
|
"dev": "dotenv electron-vite dev",
|
||||||
"debug": "electron-vite -- --inspect --sourcemap --remote-debugging-port=9222",
|
"debug": "electron-vite -- --inspect --sourcemap --remote-debugging-port=9222",
|
||||||
"build": "npm run typecheck && electron-vite build",
|
"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:unpack": "dotenv npm run build && electron-builder --dir",
|
||||||
"build:win": "dotenv npm run build && electron-builder --win --x64 --arm64",
|
"build:win": "dotenv npm run build && electron-builder --win --x64 --arm64",
|
||||||
"build:win:x64": "dotenv npm run build && electron-builder --win --x64",
|
"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:lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts",
|
||||||
"test:scripts": "vitest scripts",
|
"test:scripts": "vitest scripts",
|
||||||
"format": "prettier --write .",
|
"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"
|
"prepare": "git config blame.ignoreRevsFile .git-blame-ignore-revs && husky"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -74,6 +74,7 @@
|
|||||||
"@libsql/client": "0.14.0",
|
"@libsql/client": "0.14.0",
|
||||||
"@libsql/win32-x64-msvc": "^0.4.7",
|
"@libsql/win32-x64-msvc": "^0.4.7",
|
||||||
"@strongtz/win32-arm64-msvc": "^0.4.7",
|
"@strongtz/win32-arm64-msvc": "^0.4.7",
|
||||||
|
"express": "^5.1.0",
|
||||||
"graceful-fs": "^4.2.11",
|
"graceful-fs": "^4.2.11",
|
||||||
"jsdom": "26.1.0",
|
"jsdom": "26.1.0",
|
||||||
"node-stream-zip": "^1.15.0",
|
"node-stream-zip": "^1.15.0",
|
||||||
@ -212,7 +213,6 @@
|
|||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||||
"eslint-plugin-unused-imports": "^4.1.4",
|
"eslint-plugin-unused-imports": "^4.1.4",
|
||||||
"express": "^5.1.0",
|
|
||||||
"fast-diff": "^1.3.0",
|
"fast-diff": "^1.3.0",
|
||||||
"fast-xml-parser": "^5.2.0",
|
"fast-xml-parser": "^5.2.0",
|
||||||
"fetch-socks": "1.3.2",
|
"fetch-socks": "1.3.2",
|
||||||
|
|||||||
@ -22,12 +22,14 @@ class ConfigManager {
|
|||||||
})
|
})
|
||||||
|
|
||||||
this._config = {
|
this._config = {
|
||||||
|
enabled: settings?.apiServer?.enabled ?? false,
|
||||||
port: settings?.apiServer?.port ?? 23333,
|
port: settings?.apiServer?.port ?? 23333,
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
apiKey: generatedKey
|
apiKey: generatedKey
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this._config = {
|
this._config = {
|
||||||
|
enabled: settings?.apiServer?.enabled ?? false,
|
||||||
port: settings?.apiServer?.port ?? 23333,
|
port: settings?.apiServer?.port ?? 23333,
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
apiKey: settings.apiServer.apiKey
|
apiKey: settings.apiServer.apiKey
|
||||||
@ -38,6 +40,7 @@ class ConfigManager {
|
|||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
logger.warn('Failed to load config from Redux, using defaults:', error)
|
logger.warn('Failed to load config from Redux, using defaults:', error)
|
||||||
this._config = {
|
this._config = {
|
||||||
|
enabled: false,
|
||||||
port: 23333,
|
port: 23333,
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
apiKey: `cs-sk-${uuidv4()}`
|
apiKey: `cs-sk-${uuidv4()}`
|
||||||
|
|||||||
@ -143,9 +143,13 @@ if (!app.requestSingleInstanceLock()) {
|
|||||||
|
|
||||||
// Start API server if enabled
|
// Start API server if enabled
|
||||||
try {
|
try {
|
||||||
await apiServerService.start()
|
const config = await apiServerService.getCurrentConfig()
|
||||||
|
logger.info('API server config:', config)
|
||||||
|
if (config.enabled) {
|
||||||
|
await apiServerService.start()
|
||||||
|
}
|
||||||
} catch (error: any) {
|
} 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[]
|
defaultActiveKey?: string[]
|
||||||
activeKey?: string[]
|
activeKey?: string[]
|
||||||
collapsible?: 'header' | 'icon' | 'disabled'
|
collapsible?: 'header' | 'icon' | 'disabled'
|
||||||
|
onChange?: (activeKeys: string | string[]) => void
|
||||||
style?: React.CSSProperties
|
style?: React.CSSProperties
|
||||||
styles?: {
|
styles?: {
|
||||||
header?: React.CSSProperties
|
header?: React.CSSProperties
|
||||||
@ -26,6 +27,7 @@ const CustomCollapse: FC<CustomCollapseProps> = ({
|
|||||||
defaultActiveKey = ['1'],
|
defaultActiveKey = ['1'],
|
||||||
activeKey,
|
activeKey,
|
||||||
collapsible = undefined,
|
collapsible = undefined,
|
||||||
|
onChange,
|
||||||
style,
|
style,
|
||||||
styles
|
styles
|
||||||
}) => {
|
}) => {
|
||||||
@ -78,7 +80,10 @@ const CustomCollapse: FC<CustomCollapseProps> = ({
|
|||||||
activeKey={activeKey}
|
activeKey={activeKey}
|
||||||
destroyInactivePanel={destroyInactivePanel}
|
destroyInactivePanel={destroyInactivePanel}
|
||||||
collapsible={collapsible}
|
collapsible={collapsible}
|
||||||
onChange={setActiveKeys}
|
onChange={(keys) => {
|
||||||
|
setActiveKeys(keys)
|
||||||
|
onChange?.(keys)
|
||||||
|
}}
|
||||||
expandIcon={({ isActive }) => (
|
expandIcon={({ isActive }) => (
|
||||||
<ChevronRight
|
<ChevronRight
|
||||||
size={16}
|
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 { MinusOutlined, PlusOutlined } from '@ant-design/icons'
|
||||||
import { loggerService } from '@logger'
|
import { loggerService } from '@logger'
|
||||||
import CustomCollapse from '@renderer/components/CustomCollapse'
|
import SvgSpinners180Ring from '@renderer/components/Icons/SvgSpinners180Ring'
|
||||||
import CustomTag from '@renderer/components/CustomTag'
|
|
||||||
import ExpandableText from '@renderer/components/ExpandableText'
|
|
||||||
import ModelIdWithTags from '@renderer/components/ModelIdWithTags'
|
|
||||||
import NewApiAddModelPopup from '@renderer/components/ModelList/NewApiAddModelPopup'
|
import NewApiAddModelPopup from '@renderer/components/ModelList/NewApiAddModelPopup'
|
||||||
import NewApiBatchAddModelPopup from '@renderer/components/ModelList/NewApiBatchAddModelPopup'
|
import NewApiBatchAddModelPopup from '@renderer/components/ModelList/NewApiBatchAddModelPopup'
|
||||||
import Scrollbar from '@renderer/components/Scrollbar'
|
|
||||||
import { TopView } from '@renderer/components/TopView'
|
import { TopView } from '@renderer/components/TopView'
|
||||||
import {
|
import {
|
||||||
getModelLogo,
|
|
||||||
groupQwenModels,
|
groupQwenModels,
|
||||||
isEmbeddingModel,
|
isEmbeddingModel,
|
||||||
isFunctionCallingModel,
|
isFunctionCallingModel,
|
||||||
@ -21,7 +16,6 @@ import {
|
|||||||
SYSTEM_MODELS
|
SYSTEM_MODELS
|
||||||
} from '@renderer/config/models'
|
} from '@renderer/config/models'
|
||||||
import { useProvider } from '@renderer/hooks/useProvider'
|
import { useProvider } from '@renderer/hooks/useProvider'
|
||||||
import FileItem from '@renderer/pages/files/FileItem'
|
|
||||||
import { fetchModels } from '@renderer/services/ApiService'
|
import { fetchModels } from '@renderer/services/ApiService'
|
||||||
import { Model, Provider } from '@renderer/types'
|
import { Model, Provider } from '@renderer/types'
|
||||||
import {
|
import {
|
||||||
@ -31,16 +25,19 @@ import {
|
|||||||
isFreeModel,
|
isFreeModel,
|
||||||
runAsyncFunction
|
runAsyncFunction
|
||||||
} from '@renderer/utils'
|
} 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 Input from 'antd/es/input/Input'
|
||||||
import { groupBy, isEmpty, uniqBy } from 'lodash'
|
import { groupBy, isEmpty, uniqBy } from 'lodash'
|
||||||
import { debounce } from 'lodash'
|
import { debounce } from 'lodash'
|
||||||
import { Search } from 'lucide-react'
|
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 { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
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 {
|
interface ShowParams {
|
||||||
provider: Provider
|
provider: Provider
|
||||||
@ -50,15 +47,6 @@ interface Props extends ShowParams {
|
|||||||
resolve: (data: any) => void
|
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 PopupContainer: React.FC<Props> = ({ provider: _provider, resolve }) => {
|
||||||
const [open, setOpen] = useState(true)
|
const [open, setOpen] = useState(true)
|
||||||
const { provider, models, addModel, removeModel } = useProvider(_provider.id)
|
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])
|
}, [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 (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title={<ModalHeader />}
|
title={<ModalHeader />}
|
||||||
@ -388,38 +332,15 @@ const PopupContainer: React.FC<Props> = ({ provider: _provider, resolve }) => {
|
|||||||
<ListContainer>
|
<ListContainer>
|
||||||
{loading || isFilterTypePending || isSearchPending ? (
|
{loading || isFilterTypePending || isSearchPending ? (
|
||||||
<Flex justify="center" align="center" style={{ height: '70%' }}>
|
<Flex justify="center" align="center" style={{ height: '70%' }}>
|
||||||
<Spin size="large" />
|
<Spin indicator={<SvgSpinners180Ring color="var(--color-text-2)" />} />
|
||||||
</Flex>
|
</Flex>
|
||||||
) : (
|
) : (
|
||||||
Object.keys(modelGroups).map((group, i) => {
|
<ManageModelsList
|
||||||
return (
|
modelGroups={modelGroups}
|
||||||
<CustomCollapse
|
provider={provider}
|
||||||
key={i}
|
onAddModel={onAddModel}
|
||||||
defaultActiveKey={['1']}
|
onRemoveModel={onRemoveModel}
|
||||||
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>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
)}
|
)}
|
||||||
{!(loading || isFilterTypePending || isSearchPending) && isEmpty(list) && (
|
{!(loading || isFilterTypePending || isSearchPending) && isEmpty(list) && (
|
||||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={t('settings.models.empty')} />
|
<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`
|
const SearchContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -480,19 +369,8 @@ const TopToolsWrapper = styled.div`
|
|||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
`
|
`
|
||||||
|
|
||||||
const ListContainer = styled(Scrollbar)`
|
const ListContainer = styled.div`
|
||||||
height: calc(100vh - 300px);
|
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`
|
const ModelHeaderTitle = styled.div`
|
||||||
@ -502,10 +380,12 @@ const ModelHeaderTitle = styled.div`
|
|||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
`
|
`
|
||||||
|
|
||||||
export default class EditModelsPopup {
|
const TopViewKey = 'ManageModelsPopup'
|
||||||
|
|
||||||
|
export default class ManageModelsPopup {
|
||||||
static topviewId = 0
|
static topviewId = 0
|
||||||
static hide() {
|
static hide() {
|
||||||
TopView.hide('EditModelsPopup')
|
TopView.hide(TopViewKey)
|
||||||
}
|
}
|
||||||
static show(props: ShowParams) {
|
static show(props: ShowParams) {
|
||||||
return new Promise<any>((resolve) => {
|
return new Promise<any>((resolve) => {
|
||||||
@ -517,7 +397,7 @@ export default class EditModelsPopup {
|
|||||||
this.hide()
|
this.hide()
|
||||||
}}
|
}}
|
||||||
/>,
|
/>,
|
||||||
'EditModelsPopup'
|
TopViewKey
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -1,9 +1,11 @@
|
|||||||
import CollapsibleSearchBar from '@renderer/components/CollapsibleSearchBar'
|
import CollapsibleSearchBar from '@renderer/components/CollapsibleSearchBar'
|
||||||
|
import CustomTag from '@renderer/components/CustomTag'
|
||||||
import { StreamlineGoodHealthAndWellBeing } from '@renderer/components/Icons/SVGIcon'
|
import { StreamlineGoodHealthAndWellBeing } from '@renderer/components/Icons/SVGIcon'
|
||||||
|
import SvgSpinners180Ring from '@renderer/components/Icons/SvgSpinners180Ring'
|
||||||
import { HStack } from '@renderer/components/Layout'
|
import { HStack } from '@renderer/components/Layout'
|
||||||
import AddModelPopup from '@renderer/components/ModelList/AddModelPopup'
|
import AddModelPopup from '@renderer/components/ModelList/AddModelPopup'
|
||||||
import EditModelsPopup from '@renderer/components/ModelList/EditModelsPopup'
|
import EditModelPopup from '@renderer/components/ModelList/EditModelPopup'
|
||||||
import ModelEditContent from '@renderer/components/ModelList/ModelEditContent'
|
import ManageModelsPopup from '@renderer/components/ModelList/ManageModelsPopup'
|
||||||
import NewApiAddModelPopup from '@renderer/components/ModelList/NewApiAddModelPopup'
|
import NewApiAddModelPopup from '@renderer/components/ModelList/NewApiAddModelPopup'
|
||||||
import { PROVIDER_CONFIG } from '@renderer/config/providers'
|
import { PROVIDER_CONFIG } from '@renderer/config/providers'
|
||||||
import { useAssistants, useDefaultModel } from '@renderer/hooks/useAssistant'
|
import { useAssistants, useDefaultModel } from '@renderer/hooks/useAssistant'
|
||||||
@ -14,10 +16,10 @@ import { useAppDispatch } from '@renderer/store'
|
|||||||
import { setModel } from '@renderer/store/assistants'
|
import { setModel } from '@renderer/store/assistants'
|
||||||
import { Model } from '@renderer/types'
|
import { Model } from '@renderer/types'
|
||||||
import { filterModelsByKeywords } from '@renderer/utils'
|
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 { groupBy, sortBy, toPairs } from 'lodash'
|
||||||
import { ListCheck, Plus } from 'lucide-react'
|
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 { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import ModelListGroup from './ModelListGroup'
|
import ModelListGroup from './ModelListGroup'
|
||||||
@ -27,6 +29,21 @@ interface ModelListProps {
|
|||||||
providerId: string
|
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 操作和健康检查
|
* 模型列表组件,用于 CRUD 操作和健康检查
|
||||||
*/
|
*/
|
||||||
@ -41,8 +58,13 @@ const ModelList: React.FC<ModelListProps> = ({ providerId }) => {
|
|||||||
const docsWebsite = providerConfig?.websites?.docs
|
const docsWebsite = providerConfig?.websites?.docs
|
||||||
const modelsWebsite = providerConfig?.websites?.models
|
const modelsWebsite = providerConfig?.websites?.models
|
||||||
|
|
||||||
const [editingModel, setEditingModel] = useState<Model | null>(null)
|
|
||||||
const [searchText, _setSearchText] = useState('')
|
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)
|
const { isChecking: isHealthChecking, modelStatuses, runHealthCheck } = useHealthCheck(provider, models)
|
||||||
|
|
||||||
@ -52,20 +74,22 @@ const ModelList: React.FC<ModelListProps> = ({ providerId }) => {
|
|||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const modelGroups = useMemo(() => {
|
useEffect(() => {
|
||||||
const filteredModels = searchText ? filterModelsByKeywords(searchText, models) : models
|
if (models.length > MODEL_COUNT_THRESHOLD) {
|
||||||
return groupBy(filteredModels, 'group')
|
startTransition(() => {
|
||||||
}, [searchText, models])
|
setDisplayedModelGroups(calculateModelGroups(models, searchText))
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
setDisplayedModelGroups(calculateModelGroups(models, searchText))
|
||||||
|
}
|
||||||
|
}, [models, searchText])
|
||||||
|
|
||||||
const sortedModelGroups = useMemo(() => {
|
const modelCount = useMemo(() => {
|
||||||
return sortBy(toPairs(modelGroups), [0]).reduce((acc, [key, value]) => {
|
return Object.values(displayedModelGroups ?? {}).reduce((acc, group) => acc + group.length, 0)
|
||||||
acc[key] = value
|
}, [displayedModelGroups])
|
||||||
return acc
|
|
||||||
}, {})
|
|
||||||
}, [modelGroups])
|
|
||||||
|
|
||||||
const onManageModel = useCallback(() => {
|
const onManageModel = useCallback(() => {
|
||||||
EditModelsPopup.show({ provider })
|
ManageModelsPopup.show({ provider })
|
||||||
}, [provider])
|
}, [provider])
|
||||||
|
|
||||||
const onAddModel = useCallback(() => {
|
const onAddModel = useCallback(() => {
|
||||||
@ -76,10 +100,6 @@ const ModelList: React.FC<ModelListProps> = ({ providerId }) => {
|
|||||||
}
|
}
|
||||||
}, [provider, t])
|
}, [provider, t])
|
||||||
|
|
||||||
const onEditModel = useCallback((model: Model) => {
|
|
||||||
setEditingModel(model)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const onUpdateModel = useCallback(
|
const onUpdateModel = useCallback(
|
||||||
(updatedModel: Model) => {
|
(updatedModel: Model) => {
|
||||||
const updatedModels = models.map((m) => (m.id === updatedModel.id ? updatedModel : m))
|
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]
|
[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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<SettingSubtitle style={{ marginBottom: 5 }}>
|
<SettingSubtitle style={{ marginBottom: 5 }}>
|
||||||
<HStack alignItems="center" justifyContent="space-between" style={{ width: '100%' }}>
|
<HStack alignItems="center" justifyContent="space-between" style={{ width: '100%' }}>
|
||||||
<HStack alignItems="center" gap={8}>
|
<HStack alignItems="center" gap={8}>
|
||||||
<SettingSubtitle style={{ marginTop: 0 }}>{t('common.models')}</SettingSubtitle>
|
<SettingSubtitle style={{ marginTop: 0 }}>{t('common.models')}</SettingSubtitle>
|
||||||
|
{modelCount > 0 && (
|
||||||
|
<CustomTag color="#8c8c8c" size={10}>
|
||||||
|
{modelCount}
|
||||||
|
</CustomTag>
|
||||||
|
)}
|
||||||
<CollapsibleSearchBar onSearch={setSearchText} />
|
<CollapsibleSearchBar onSearch={setSearchText} />
|
||||||
</HStack>
|
</HStack>
|
||||||
<HStack>
|
<HStack>
|
||||||
@ -123,41 +158,47 @@ const ModelList: React.FC<ModelListProps> = ({ providerId }) => {
|
|||||||
</HStack>
|
</HStack>
|
||||||
</HStack>
|
</HStack>
|
||||||
</SettingSubtitle>
|
</SettingSubtitle>
|
||||||
<Flex gap={12} vertical>
|
{displayedModelGroups === null ? (
|
||||||
{Object.keys(sortedModelGroups).map((group, i) => (
|
<Flex align="center" justify="center" style={{ minHeight: '8rem' }}>
|
||||||
<ModelListGroup
|
<Spin indicator={<SvgSpinners180Ring color="var(--color-text-2)" />} />
|
||||||
key={group}
|
</Flex>
|
||||||
groupName={group}
|
) : (
|
||||||
models={sortedModelGroups[group]}
|
<Flex gap={12} vertical>
|
||||||
modelStatuses={modelStatuses}
|
{Object.keys(displayedModelGroups).map((group, i) => (
|
||||||
defaultOpen={i <= 5}
|
<ModelListGroup
|
||||||
disabled={isHealthChecking}
|
key={group}
|
||||||
onEditModel={onEditModel}
|
groupName={group}
|
||||||
onRemoveModel={removeModel}
|
models={displayedModelGroups[group]}
|
||||||
onRemoveGroup={() => modelGroups[group].forEach((model) => removeModel(model))}
|
modelStatuses={modelStatuses}
|
||||||
/>
|
defaultOpen={i <= 5}
|
||||||
))}
|
disabled={isHealthChecking}
|
||||||
{docsWebsite || modelsWebsite ? (
|
onEditModel={onEditModel}
|
||||||
<SettingHelpTextRow>
|
onRemoveModel={removeModel}
|
||||||
<SettingHelpText>{t('settings.provider.docs_check')} </SettingHelpText>
|
onRemoveGroup={() => displayedModelGroups[group].forEach((model) => removeModel(model))}
|
||||||
{docsWebsite && (
|
/>
|
||||||
<SettingHelpLink target="_blank" href={docsWebsite}>
|
))}
|
||||||
{getProviderLabel(provider.id) + ' '}
|
{docsWebsite || modelsWebsite ? (
|
||||||
{t('common.docs')}
|
<SettingHelpTextRow>
|
||||||
</SettingHelpLink>
|
<SettingHelpText>{t('settings.provider.docs_check')} </SettingHelpText>
|
||||||
)}
|
{docsWebsite && (
|
||||||
{docsWebsite && modelsWebsite && <SettingHelpText>{t('common.and')}</SettingHelpText>}
|
<SettingHelpLink target="_blank" href={docsWebsite}>
|
||||||
{modelsWebsite && (
|
{getProviderLabel(provider.id) + ' '}
|
||||||
<SettingHelpLink target="_blank" href={modelsWebsite}>
|
{t('common.docs')}
|
||||||
{t('common.models')}
|
</SettingHelpLink>
|
||||||
</SettingHelpLink>
|
)}
|
||||||
)}
|
{docsWebsite && modelsWebsite && <SettingHelpText>{t('common.and')}</SettingHelpText>}
|
||||||
<SettingHelpText>{t('settings.provider.docs_more_details')}</SettingHelpText>
|
{modelsWebsite && (
|
||||||
</SettingHelpTextRow>
|
<SettingHelpLink target="_blank" href={modelsWebsite}>
|
||||||
) : (
|
{t('common.models')}
|
||||||
<div style={{ height: 5 }} />
|
</SettingHelpLink>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
<SettingHelpText>{t('settings.provider.docs_more_details')}</SettingHelpText>
|
||||||
|
</SettingHelpTextRow>
|
||||||
|
) : (
|
||||||
|
<div style={{ height: 5 }} />
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
<Flex gap={10} style={{ marginTop: 10 }}>
|
<Flex gap={10} style={{ marginTop: 10 }}>
|
||||||
<Button type="primary" onClick={onManageModel} icon={<ListCheck size={16} />} disabled={isHealthChecking}>
|
<Button type="primary" onClick={onManageModel} icon={<ListCheck size={16} />} disabled={isHealthChecking}>
|
||||||
{t('button.manage')}
|
{t('button.manage')}
|
||||||
@ -166,16 +207,6 @@ const ModelList: React.FC<ModelListProps> = ({ providerId }) => {
|
|||||||
{t('button.add')}
|
{t('button.add')}
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</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 CustomCollapse from '@renderer/components/CustomCollapse'
|
||||||
import { Model } from '@renderer/types'
|
import { Model } from '@renderer/types'
|
||||||
import { ModelWithStatus } from '@renderer/types/healthCheck'
|
import { ModelWithStatus } from '@renderer/types/healthCheck'
|
||||||
|
import { useVirtualizer } from '@tanstack/react-virtual'
|
||||||
import { Button, Flex, Tooltip } from 'antd'
|
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 { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
@ -31,11 +32,35 @@ const ModelListGroup: React.FC<ModelListGroupProps> = ({
|
|||||||
onRemoveGroup
|
onRemoveGroup
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
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 (
|
return (
|
||||||
<CustomCollapseWrapper>
|
<CustomCollapseWrapper>
|
||||||
<CustomCollapse
|
<CustomCollapse
|
||||||
defaultActiveKey={defaultOpen ? ['1'] : []}
|
defaultActiveKey={defaultOpen ? ['1'] : []}
|
||||||
|
onChange={handleCollapseChange}
|
||||||
label={
|
label={
|
||||||
<Flex align="center" gap={10}>
|
<Flex align="center" gap={10}>
|
||||||
<span style={{ fontWeight: 'bold' }}>{groupName}</span>
|
<span style={{ fontWeight: 'bold' }}>{groupName}</span>
|
||||||
@ -47,23 +72,53 @@ const ModelListGroup: React.FC<ModelListGroupProps> = ({
|
|||||||
type="text"
|
type="text"
|
||||||
className="toolbar-item"
|
className="toolbar-item"
|
||||||
icon={<MinusOutlined />}
|
icon={<MinusOutlined />}
|
||||||
onClick={onRemoveGroup}
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
onRemoveGroup()
|
||||||
|
}}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}>
|
}>
|
||||||
<Flex gap={10} vertical style={{ marginTop: 10 }}>
|
<ScrollContainer ref={scrollerRef}>
|
||||||
{models.map((model) => (
|
<div
|
||||||
<ModelListItem
|
style={{
|
||||||
key={model.id}
|
height: `${virtualizer.getTotalSize()}px`,
|
||||||
model={model}
|
width: '100%',
|
||||||
modelStatus={modelStatuses.find((status) => status.model.id === model.id)}
|
position: 'relative'
|
||||||
onEdit={onEditModel}
|
}}>
|
||||||
onRemove={onRemoveModel}
|
<div
|
||||||
disabled={disabled}
|
style={{
|
||||||
/>
|
position: 'absolute',
|
||||||
))}
|
top: 0,
|
||||||
</Flex>
|
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>
|
</CustomCollapse>
|
||||||
</CustomCollapseWrapper>
|
</CustomCollapseWrapper>
|
||||||
)
|
)
|
||||||
@ -79,6 +134,17 @@ const CustomCollapseWrapper = styled.div`
|
|||||||
&:hover .toolbar-item {
|
&:hover .toolbar-item {
|
||||||
opacity: 1;
|
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)
|
export default memo(ModelListGroup)
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
export { default as AddModelPopup } from './AddModelPopup'
|
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 HealthCheckPopup } from './HealthCheckPopup'
|
||||||
|
export { default as ManageModelsPopup } from './ManageModelsPopup'
|
||||||
export { default as ModelEditContent } from './ModelEditContent'
|
export { default as ModelEditContent } from './ModelEditContent'
|
||||||
export { default as ModelList } from './ModelList'
|
export { default as ModelList } from './ModelList'
|
||||||
export { default as NewApiAddModelPopup } from './NewApiAddModelPopup'
|
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>
|
<Button icon={emoji && <span style={{ fontSize: 20 }}>{emoji}</span>}>{t('common.select')}</Button>
|
||||||
</Popover>
|
</Popover>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||||
import { loggerService } from '@renderer/services/LoggerService'
|
import { loggerService } from '@renderer/services/LoggerService'
|
||||||
import { RootState, useAppDispatch } from '@renderer/store'
|
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 { IpcChannel } from '@shared/IpcChannel'
|
||||||
import { Button, Input, InputNumber, Tooltip, Typography } from 'antd'
|
import { Button, Input, InputNumber, Tooltip, Typography } from 'antd'
|
||||||
import { Copy, ExternalLink, Play, RotateCcw, Square } from 'lucide-react'
|
import { Copy, ExternalLink, Play, RotateCcw, Square } from 'lucide-react'
|
||||||
@ -64,6 +64,7 @@ const ApiServerSettings: FC = () => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
window.message.error(t('apiServer.messages.operationFailed') + (error as Error).message)
|
window.message.error(t('apiServer.messages.operationFailed') + (error as Error).message)
|
||||||
} finally {
|
} finally {
|
||||||
|
dispatch(setApiServerEnabled(enabled))
|
||||||
setApiServerLoading(false)
|
setApiServerLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -109,7 +109,7 @@ const AssistantSettings: FC = () => {
|
|||||||
theme={theme}>
|
theme={theme}>
|
||||||
<SettingSubtitle style={{ marginTop: 0 }}>{t('common.name')}</SettingSubtitle>
|
<SettingSubtitle style={{ marginTop: 0 }}>{t('common.name')}</SettingSubtitle>
|
||||||
<HStack gap={8} alignItems="center">
|
<HStack gap={8} alignItems="center">
|
||||||
<Popover content={<EmojiPicker onEmojiClick={handleEmojiSelect} />} arrow>
|
<Popover content={<EmojiPicker onEmojiClick={handleEmojiSelect} />} arrow trigger="click">
|
||||||
<EmojiButtonWrapper>
|
<EmojiButtonWrapper>
|
||||||
<Button style={{ fontSize: 20, padding: '4px', minWidth: '30px', height: '30px' }}>{emoji}</Button>
|
<Button style={{ fontSize: 20, padding: '4px', minWidth: '30px', height: '30px' }}>{emoji}</Button>
|
||||||
{emoji && (
|
{emoji && (
|
||||||
|
|||||||
@ -19,7 +19,7 @@ import {
|
|||||||
} from '@renderer/utils'
|
} from '@renderer/utils'
|
||||||
import { Avatar, Button, Card, Dropdown, Input, MenuProps, Tag } from 'antd'
|
import { Avatar, Button, Card, Dropdown, Input, MenuProps, Tag } from 'antd'
|
||||||
import { Eye, EyeOff, Search, UserPen } from 'lucide-react'
|
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 { useTranslation } from 'react-i18next'
|
||||||
import { useSearchParams } from 'react-router-dom'
|
import { useSearchParams } from 'react-router-dom'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
@ -34,12 +34,19 @@ const ProvidersList: FC = () => {
|
|||||||
const [searchParams] = useSearchParams()
|
const [searchParams] = useSearchParams()
|
||||||
const providers = useAllProviders()
|
const providers = useAllProviders()
|
||||||
const { updateProviders, addProvider, removeProvider, updateProvider } = useProviders()
|
const { updateProviders, addProvider, removeProvider, updateProvider } = useProviders()
|
||||||
const [selectedProvider, setSelectedProvider] = useState<Provider>(providers[0])
|
const [selectedProvider, _setSelectedProvider] = useState<Provider>(providers[0])
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [searchText, setSearchText] = useState<string>('')
|
const [searchText, setSearchText] = useState<string>('')
|
||||||
const [dragging, setDragging] = useState(false)
|
const [dragging, setDragging] = useState(false)
|
||||||
const [providerLogos, setProviderLogos] = useState<Record<string, string>>({})
|
const [providerLogos, setProviderLogos] = useState<Record<string, string>>({})
|
||||||
|
|
||||||
|
const setSelectedProvider = useCallback(
|
||||||
|
(provider: Provider) => {
|
||||||
|
startTransition(() => _setSelectedProvider(provider))
|
||||||
|
},
|
||||||
|
[_setSelectedProvider]
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadAllLogos = async () => {
|
const loadAllLogos = async () => {
|
||||||
const logos: Record<string, string> = {}
|
const logos: Record<string, string> = {}
|
||||||
@ -71,7 +78,7 @@ const ProvidersList: FC = () => {
|
|||||||
setSelectedProvider(providers[0])
|
setSelectedProvider(providers[0])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [providers, searchParams])
|
}, [providers, searchParams, setSelectedProvider])
|
||||||
|
|
||||||
// Handle provider add key from URL schema
|
// Handle provider add key from URL schema
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@ -312,16 +312,18 @@ async function fetchExternalTool(
|
|||||||
let memorySearchReferences: MemoryItem[] | undefined
|
let memorySearchReferences: MemoryItem[] | undefined
|
||||||
|
|
||||||
const parentSpanId = currentSpan(lastUserMessage.topicId, assistant.model?.name)?.spanContext().spanId
|
const parentSpanId = currentSpan(lastUserMessage.topicId, assistant.model?.name)?.spanContext().spanId
|
||||||
// 并行执行搜索
|
if (shouldWebSearch) {
|
||||||
if (shouldWebSearch || shouldKnowledgeSearch || shouldSearchMemory) {
|
webSearchResponseFromSearch = await searchTheWeb(extractResults, parentSpanId)
|
||||||
;[webSearchResponseFromSearch, knowledgeReferencesFromSearch, memorySearchReferences] = await Promise.all([
|
}
|
||||||
searchTheWeb(extractResults, parentSpanId),
|
|
||||||
searchKnowledgeBase(extractResults, parentSpanId, assistant.model?.name),
|
if (shouldKnowledgeSearch) {
|
||||||
searchMemory()
|
knowledgeReferencesFromSearch = await searchKnowledgeBase(extractResults, parentSpanId, assistant.model?.name)
|
||||||
])
|
}
|
||||||
|
|
||||||
|
if (shouldSearchMemory) {
|
||||||
|
memorySearchReferences = await searchMemory()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 存储搜索结果
|
|
||||||
if (lastUserMessage) {
|
if (lastUserMessage) {
|
||||||
if (webSearchResponseFromSearch) {
|
if (webSearchResponseFromSearch) {
|
||||||
window.keyv.set(`web-search-${lastUserMessage.id}`, webSearchResponseFromSearch)
|
window.keyv.set(`web-search-${lastUserMessage.id}`, webSearchResponseFromSearch)
|
||||||
@ -492,7 +494,7 @@ export async function fetchChatCompletion({
|
|||||||
// Post-conversation memory processing
|
// Post-conversation memory processing
|
||||||
const globalMemoryEnabled = selectGlobalMemoryEnabled(store.getState())
|
const globalMemoryEnabled = selectGlobalMemoryEnabled(store.getState())
|
||||||
if (globalMemoryEnabled && assistant.enableMemory) {
|
if (globalMemoryEnabled && assistant.enableMemory) {
|
||||||
await processConversationMemory(messages, assistant)
|
processConversationMemory(messages, assistant)
|
||||||
}
|
}
|
||||||
|
|
||||||
return await AI.completionsForTrace(completionsParams, requestOptions)
|
return await AI.completionsForTrace(completionsParams, requestOptions)
|
||||||
|
|||||||
@ -1925,6 +1925,7 @@ const migrateConfig = {
|
|||||||
// Initialize API server configuration if not present
|
// Initialize API server configuration if not present
|
||||||
if (!state.settings.apiServer) {
|
if (!state.settings.apiServer) {
|
||||||
state.settings.apiServer = {
|
state.settings.apiServer = {
|
||||||
|
enabled: false,
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: 23333,
|
port: 23333,
|
||||||
apiKey: `cs-sk-${uuid()}`
|
apiKey: `cs-sk-${uuid()}`
|
||||||
|
|||||||
@ -391,6 +391,7 @@ export const initialState: SettingsState = {
|
|||||||
navbarPosition: 'left',
|
navbarPosition: 'left',
|
||||||
// API Server
|
// API Server
|
||||||
apiServer: {
|
apiServer: {
|
||||||
|
enabled: false,
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: 23333,
|
port: 23333,
|
||||||
apiKey: `cs-sk-${uuid()}`
|
apiKey: `cs-sk-${uuid()}`
|
||||||
@ -800,6 +801,12 @@ const settingsSlice = createSlice({
|
|||||||
state.navbarPosition = action.payload
|
state.navbarPosition = action.payload
|
||||||
},
|
},
|
||||||
// API Server actions
|
// API Server actions
|
||||||
|
setApiServerEnabled: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state.apiServer = {
|
||||||
|
...state.apiServer,
|
||||||
|
enabled: action.payload
|
||||||
|
}
|
||||||
|
},
|
||||||
setApiServerPort: (state, action: PayloadAction<number>) => {
|
setApiServerPort: (state, action: PayloadAction<number>) => {
|
||||||
state.apiServer = {
|
state.apiServer = {
|
||||||
...state.apiServer,
|
...state.apiServer,
|
||||||
@ -935,6 +942,7 @@ export const {
|
|||||||
setEnableDeveloperMode,
|
setEnableDeveloperMode,
|
||||||
setNavbarPosition,
|
setNavbarPosition,
|
||||||
// API Server actions
|
// API Server actions
|
||||||
|
setApiServerEnabled,
|
||||||
setApiServerPort,
|
setApiServerPort,
|
||||||
setApiServerApiKey
|
setApiServerApiKey
|
||||||
} = settingsSlice.actions
|
} = settingsSlice.actions
|
||||||
|
|||||||
@ -842,6 +842,7 @@ export type S3Config = {
|
|||||||
export type { Message } from './newMessage'
|
export type { Message } from './newMessage'
|
||||||
|
|
||||||
export interface ApiServerConfig {
|
export interface ApiServerConfig {
|
||||||
|
enabled: boolean
|
||||||
host: string
|
host: string
|
||||||
port: number
|
port: number
|
||||||
apiKey: string
|
apiKey: string
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user