mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-24 18:50:56 +08:00
feat: 增加模型备注功能 (#5392)
* feat: 增加模型备注功能 * fix: 移除未使用变量 --------- Co-authored-by: liutao <>
This commit is contained in:
parent
38830d34aa
commit
da16397902
1
src/renderer/src/components/MarkdownEditor/README.md
Normal file
1
src/renderer/src/components/MarkdownEditor/README.md
Normal file
@ -0,0 +1 @@
|
||||
|
||||
93
src/renderer/src/components/MarkdownEditor/index.tsx
Normal file
93
src/renderer/src/components/MarkdownEditor/index.tsx
Normal 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
|
||||
@ -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": {
|
||||
|
||||
@ -1327,7 +1327,12 @@
|
||||
"remove_invalid_keys": "無効なキーを削除",
|
||||
"search": "プロバイダーを検索...",
|
||||
"search_placeholder": "モデルIDまたは名前を検索",
|
||||
"title": "モデルプロバイダー"
|
||||
"title": "モデルプロバイダー",
|
||||
"notes": {
|
||||
"title": "モデルノート",
|
||||
"placeholder": "Markdown形式の内容を入力してください...",
|
||||
"markdown_editor_default_value": "プレビュー領域"
|
||||
}
|
||||
},
|
||||
"proxy": {
|
||||
"mode": {
|
||||
|
||||
@ -1327,7 +1327,12 @@
|
||||
"remove_invalid_keys": "Удалить недействительные ключи",
|
||||
"search": "Поиск поставщиков...",
|
||||
"search_placeholder": "Поиск по ID или имени модели",
|
||||
"title": "Провайдеры моделей"
|
||||
"title": "Провайдеры моделей",
|
||||
"notes": {
|
||||
"title": "Заметки модели",
|
||||
"placeholder": "Введите содержимое в формате Markdown...",
|
||||
"markdown_editor_default_value": "Область предварительного просмотра"
|
||||
}
|
||||
},
|
||||
"proxy": {
|
||||
"mode": {
|
||||
|
||||
@ -1329,7 +1329,12 @@
|
||||
"remove_invalid_keys": "删除无效密钥",
|
||||
"search": "搜索模型平台...",
|
||||
"search_placeholder": "搜索模型 ID 或名称",
|
||||
"title": "模型服务"
|
||||
"title": "模型服务",
|
||||
"notes": {
|
||||
"title": "模型备注",
|
||||
"placeholder": "请输入Markdown格式内容...",
|
||||
"markdown_editor_default_value": "预览区域"
|
||||
}
|
||||
},
|
||||
"proxy": {
|
||||
"mode": {
|
||||
|
||||
@ -1328,7 +1328,12 @@
|
||||
"remove_invalid_keys": "刪除無效金鑰",
|
||||
"search": "搜尋模型平臺...",
|
||||
"search_placeholder": "搜尋模型 ID 或名稱",
|
||||
"title": "模型提供者"
|
||||
"title": "模型提供者",
|
||||
"notes": {
|
||||
"title": "模型備註",
|
||||
"placeholder": "輸入Markdown格式內容...",
|
||||
"markdown_editor_default_value": "預覽區域"
|
||||
}
|
||||
},
|
||||
"proxy": {
|
||||
"mode": {
|
||||
|
||||
@ -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'
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -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}
|
||||
|
||||
@ -140,6 +140,7 @@ export type Provider = {
|
||||
isAuthed?: boolean
|
||||
rateLimit?: number
|
||||
isNotSupportArrayContent?: boolean
|
||||
notes?: string
|
||||
}
|
||||
|
||||
export type ProviderType = 'openai' | 'anthropic' | 'gemini' | 'qwenlm' | 'azure-openai'
|
||||
|
||||
Loading…
Reference in New Issue
Block a user