feat: 增加模型备注功能 (#5392)

* feat: 增加模型备注功能

* fix: 移除未使用变量

---------

Co-authored-by: liutao <>
This commit is contained in:
africa1207 2025-04-28 09:11:55 +08:00 committed by GitHub
parent 38830d34aa
commit da16397902
10 changed files with 218 additions and 6 deletions

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,93 @@
import 'katex/dist/katex.min.css'
import React, { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import ReactMarkdown from 'react-markdown'
import rehypeKatex from 'rehype-katex'
import rehypeRaw from 'rehype-raw'
import remarkCjkFriendly from 'remark-cjk-friendly'
import remarkGfm from 'remark-gfm'
import remarkMath from 'remark-math'
import styled from 'styled-components'
interface MarkdownEditorProps {
value: string
onChange: (value: string) => void
placeholder?: string
height?: string | number
autoFocus?: boolean
}
const MarkdownEditor: FC<MarkdownEditorProps> = ({
value,
onChange,
placeholder = '请输入Markdown格式文本...',
height = '300px',
autoFocus = false
}) => {
const { t } = useTranslation()
const [inputValue, setInputValue] = useState(value || '')
useEffect(() => {
setInputValue(value || '')
}, [value])
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const newValue = e.target.value
setInputValue(newValue)
onChange(newValue)
}
return (
<EditorContainer style={{ height }}>
<InputArea value={inputValue} onChange={handleChange} placeholder={placeholder} autoFocus={autoFocus} />
<PreviewArea>
<ReactMarkdown
remarkPlugins={[remarkGfm, remarkCjkFriendly, remarkMath]}
rehypePlugins={[rehypeRaw, rehypeKatex]}
className="markdown">
{inputValue || t('settings.provider.notes.markdown_editor_default_value')}
</ReactMarkdown>
</PreviewArea>
</EditorContainer>
)
}
const EditorContainer = styled.div`
display: flex;
border: 1px solid var(--color-border);
border-radius: 8px;
overflow: hidden;
width: 100%;
`
const InputArea = styled.textarea`
flex: 1;
padding: 12px;
border: none;
resize: none;
font-family: var(--font-family);
font-size: 14px;
line-height: 1.5;
color: var(--color-text);
background-color: var(--color-bg-1);
border-right: 1px solid var(--color-border);
outline: none;
&:focus {
outline: none;
}
&::placeholder {
color: var(--color-text-3);
}
`
const PreviewArea = styled.div`
flex: 1;
padding: 12px;
overflow: auto;
background-color: var(--color-bg-1);
`
export default MarkdownEditor

View File

@ -1329,7 +1329,12 @@
"remove_invalid_keys": "Remove Invalid Keys",
"search": "Search Providers...",
"search_placeholder": "Search model id or name",
"title": "Model Provider"
"title": "Model Provider",
"notes": {
"title": "Model Notes",
"placeholder": "Enter Markdown content...",
"markdown_editor_default_value": "Preview area"
}
},
"proxy": {
"mode": {

View File

@ -1327,7 +1327,12 @@
"remove_invalid_keys": "無効なキーを削除",
"search": "プロバイダーを検索...",
"search_placeholder": "モデルIDまたは名前を検索",
"title": "モデルプロバイダー"
"title": "モデルプロバイダー",
"notes": {
"title": "モデルノート",
"placeholder": "Markdown形式の内容を入力してください...",
"markdown_editor_default_value": "プレビュー領域"
}
},
"proxy": {
"mode": {

View File

@ -1327,7 +1327,12 @@
"remove_invalid_keys": "Удалить недействительные ключи",
"search": "Поиск поставщиков...",
"search_placeholder": "Поиск по ID или имени модели",
"title": "Провайдеры моделей"
"title": "Провайдеры моделей",
"notes": {
"title": "Заметки модели",
"placeholder": "Введите содержимое в формате Markdown...",
"markdown_editor_default_value": "Область предварительного просмотра"
}
},
"proxy": {
"mode": {

View File

@ -1329,7 +1329,12 @@
"remove_invalid_keys": "删除无效密钥",
"search": "搜索模型平台...",
"search_placeholder": "搜索模型 ID 或名称",
"title": "模型服务"
"title": "模型服务",
"notes": {
"title": "模型备注",
"placeholder": "请输入Markdown格式内容...",
"markdown_editor_default_value": "预览区域"
}
},
"proxy": {
"mode": {

View File

@ -1328,7 +1328,12 @@
"remove_invalid_keys": "刪除無效金鑰",
"search": "搜尋模型平臺...",
"search_placeholder": "搜尋模型 ID 或名稱",
"title": "模型提供者"
"title": "模型提供者",
"notes": {
"title": "模型備註",
"placeholder": "輸入Markdown格式內容...",
"markdown_editor_default_value": "預覽區域"
}
},
"proxy": {
"mode": {

View File

@ -0,0 +1,84 @@
import MarkdownEditor from '@renderer/components/MarkdownEditor'
import { TopView } from '@renderer/components/TopView'
import { useProvider } from '@renderer/hooks/useProvider'
import { Provider } from '@renderer/types'
import { Modal } from 'antd'
import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
interface ShowParams {
provider: Provider
}
interface Props extends ShowParams {
resolve: (data: any) => void
}
const PopupContainer: FC<Props> = ({ provider: _provider, resolve }) => {
const { t } = useTranslation()
const [open, setOpen] = useState(true)
const { provider, updateProvider } = useProvider(_provider.id)
const [notes, setNotes] = useState<string>(provider.notes || '')
const handleSave = () => {
updateProvider({
...provider,
notes
})
setOpen(false)
}
const onCancel = () => {
setOpen(false)
}
const onClose = () => {
resolve({})
}
return (
<Modal
title={t('settings.provider.notes.title')}
open={open}
onOk={handleSave}
onCancel={onCancel}
afterClose={onClose}
width={800}
centered>
<EditorContainer>
<MarkdownEditor
value={notes}
onChange={setNotes}
placeholder={t('settings.provider.notes.placeholder')}
height="400px"
/>
</EditorContainer>
</Modal>
)
}
const EditorContainer = styled.div`
margin-top: 16px;
height: 400px;
`
export default class ModelNotesPopup {
static hide() {
TopView.hide('ModelNotesPopup')
}
static show(props: ShowParams) {
return new Promise<any>((resolve) => {
TopView.show(
<PopupContainer
{...props}
resolve={(v) => {
resolve(v)
this.hide()
}}
/>,
'ModelNotesPopup'
)
})
}
}

View File

@ -1,4 +1,4 @@
import { CheckOutlined, LoadingOutlined } from '@ant-design/icons'
import { CheckOutlined, FileTextOutlined, LoadingOutlined } from '@ant-design/icons'
import { StreamlineGoodHealthAndWellBeing } from '@renderer/components/Icons/SVGIcon'
import { HStack } from '@renderer/components/Layout'
import { isEmbeddingModel, isRerankModel } from '@renderer/config/models'
@ -35,6 +35,7 @@ import HealthCheckPopup from './HealthCheckPopup'
import LMStudioSettings from './LMStudioSettings'
import ModelList, { ModelStatus } from './ModelList'
import ModelListSearchBar from './ModelListSearchBar'
import ModelNotesPopup from './ModelNotesPopup'
import ProviderOAuth from './ProviderOAuth'
import ProviderSettingsPopup from './ProviderSettingsPopup'
import SelectProviderModelPopup from './SelectProviderModelPopup'
@ -273,6 +274,10 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
return formatApiHost(apiHost) + 'chat/completions'
}
const onShowNotes = useCallback(() => {
ModelNotesPopup.show({ provider })
}, [provider])
useEffect(() => {
if (provider.id === 'copilot') {
return
@ -308,6 +313,9 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
onClick={() => ProviderSettingsPopup.show({ provider })}
/>
)}
<Tooltip title={t('settings.provider.notes.title')}>
<Button type="text" onClick={onShowNotes} icon={<FileTextOutlined />} />
</Tooltip>
</Flex>
<Switch
value={provider.enabled}

View File

@ -140,6 +140,7 @@ export type Provider = {
isAuthed?: boolean
rateLimit?: number
isNotSupportArrayContent?: boolean
notes?: string
}
export type ProviderType = 'openai' | 'anthropic' | 'gemini' | 'qwenlm' | 'azure-openai'