diff --git a/src/renderer/src/components/MarkdownEditor/README.md b/src/renderer/src/components/MarkdownEditor/README.md new file mode 100644 index 0000000000..0519ecba6e --- /dev/null +++ b/src/renderer/src/components/MarkdownEditor/README.md @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/renderer/src/components/MarkdownEditor/index.tsx b/src/renderer/src/components/MarkdownEditor/index.tsx new file mode 100644 index 0000000000..5a355a07c2 --- /dev/null +++ b/src/renderer/src/components/MarkdownEditor/index.tsx @@ -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 = ({ + 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) => { + const newValue = e.target.value + setInputValue(newValue) + onChange(newValue) + } + + return ( + + + + + {inputValue || t('settings.provider.notes.markdown_editor_default_value')} + + + + ) +} + +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 diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index b8e7dd67e5..7307618bfc 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -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": { diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 1c1164ac96..510fd0d257 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -1327,7 +1327,12 @@ "remove_invalid_keys": "無効なキーを削除", "search": "プロバイダーを検索...", "search_placeholder": "モデルIDまたは名前を検索", - "title": "モデルプロバイダー" + "title": "モデルプロバイダー", + "notes": { + "title": "モデルノート", + "placeholder": "Markdown形式の内容を入力してください...", + "markdown_editor_default_value": "プレビュー領域" + } }, "proxy": { "mode": { diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index d6d156af12..f0919d7662 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -1327,7 +1327,12 @@ "remove_invalid_keys": "Удалить недействительные ключи", "search": "Поиск поставщиков...", "search_placeholder": "Поиск по ID или имени модели", - "title": "Провайдеры моделей" + "title": "Провайдеры моделей", + "notes": { + "title": "Заметки модели", + "placeholder": "Введите содержимое в формате Markdown...", + "markdown_editor_default_value": "Область предварительного просмотра" + } }, "proxy": { "mode": { diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index de8e422e9f..c15510cf26 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -1329,7 +1329,12 @@ "remove_invalid_keys": "删除无效密钥", "search": "搜索模型平台...", "search_placeholder": "搜索模型 ID 或名称", - "title": "模型服务" + "title": "模型服务", + "notes": { + "title": "模型备注", + "placeholder": "请输入Markdown格式内容...", + "markdown_editor_default_value": "预览区域" + } }, "proxy": { "mode": { diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index c6ac493d34..dfab7788ce 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -1328,7 +1328,12 @@ "remove_invalid_keys": "刪除無效金鑰", "search": "搜尋模型平臺...", "search_placeholder": "搜尋模型 ID 或名稱", - "title": "模型提供者" + "title": "模型提供者", + "notes": { + "title": "模型備註", + "placeholder": "輸入Markdown格式內容...", + "markdown_editor_default_value": "預覽區域" + } }, "proxy": { "mode": { diff --git a/src/renderer/src/pages/settings/ProviderSettings/ModelNotesPopup.tsx b/src/renderer/src/pages/settings/ProviderSettings/ModelNotesPopup.tsx new file mode 100644 index 0000000000..2547fb41b0 --- /dev/null +++ b/src/renderer/src/pages/settings/ProviderSettings/ModelNotesPopup.tsx @@ -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 = ({ provider: _provider, resolve }) => { + const { t } = useTranslation() + const [open, setOpen] = useState(true) + const { provider, updateProvider } = useProvider(_provider.id) + const [notes, setNotes] = useState(provider.notes || '') + + const handleSave = () => { + updateProvider({ + ...provider, + notes + }) + setOpen(false) + } + + const onCancel = () => { + setOpen(false) + } + + const onClose = () => { + resolve({}) + } + + return ( + + + + + + ) +} + +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((resolve) => { + TopView.show( + { + resolve(v) + this.hide() + }} + />, + 'ModelNotesPopup' + ) + }) + } +} diff --git a/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx b/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx index 926a1792dc..7ab45b6f08 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx @@ -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 = ({ 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 = ({ provider: _provider }) => { onClick={() => ProviderSettingsPopup.show({ provider })} /> )} + +