Compare commits

..

1 Commits

Author SHA1 Message Date
kangfenmao
ebf61b1ce9 feat: plugins 2024-12-30 23:45:47 +08:00
51 changed files with 512 additions and 822 deletions

View File

@@ -1,19 +0,0 @@
name: Auto Assign
on:
issues:
types: [opened]
pull_request:
types: [opened]
jobs:
run:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- name: 'Auto-assign issue'
uses: pozil/auto-assign-issue@v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
assignees: kangfenmao
numOfAssignee: 1

View File

@@ -1,19 +0,0 @@
diff --git a/src/markdown-loader.js b/src/markdown-loader.js
index 8a17cb7f5a68d90d2be21682db6e95ce22a3e71c..9ee868ef9d4ff3dc914b3abc3c8006deb1e9c6c6 100644
--- a/src/markdown-loader.js
+++ b/src/markdown-loader.js
@@ -1,5 +1,4 @@
import { micromark } from 'micromark';
-import { mdxJsx } from 'micromark-extension-mdx-jsx';
import { gfmHtml, gfm } from 'micromark-extension-gfm';
import createDebugMessages from 'debug';
import fs from 'node:fs';
@@ -21,7 +20,7 @@ export class MarkdownLoader extends BaseLoader {
? (await getSafe(this.filePathOrUrl, { format: 'buffer' })).body
: await stream2buffer(fs.createReadStream(this.filePathOrUrl));
this.debug('MarkdownLoader stream created');
- const result = micromark(buffer, { extensions: [gfm(), mdxJsx()], htmlExtensions: [gfmHtml()] });
+ const result = micromark(buffer, { extensions: [gfm()], htmlExtensions: [gfmHtml()] });
this.debug('Markdown parsed...');
const webLoader = new WebLoader({
urlOrContent: result,

View File

@@ -78,8 +78,4 @@ afterPack: scripts/after-pack.js
afterSign: scripts/notarize.js
releaseInfo:
releaseNotes: |
文件支持删除
增加 Hika 小程序
增加 WebDAV 同步状态显示
自定义参数增加 JSON 类型
腾讯混元的联网开关
增加 Genspark 小程序

View File

@@ -1,6 +1,6 @@
{
"name": "CherryStudio",
"version": "0.9.4",
"version": "0.9.2",
"private": true,
"description": "A powerful AI assistant for producer.",
"main": "./out/main/index.js",
@@ -53,7 +53,7 @@
"@llm-tools/embedjs": "patch:@llm-tools/embedjs@npm%3A0.1.25#~/.yarn/patches/@llm-tools-embedjs-npm-0.1.25-ec5645cf36.patch",
"@llm-tools/embedjs-libsql": "patch:@llm-tools/embedjs-libsql@npm%3A0.1.25#~/.yarn/patches/@llm-tools-embedjs-libsql-npm-0.1.25-fad000d74c.patch",
"@llm-tools/embedjs-loader-csv": "^0.1.25",
"@llm-tools/embedjs-loader-markdown": "patch:@llm-tools/embedjs-loader-markdown@npm%3A0.1.25#~/.yarn/patches/@llm-tools-embedjs-loader-markdown-npm-0.1.25-d1d536d640.patch",
"@llm-tools/embedjs-loader-markdown": "^0.1.25",
"@llm-tools/embedjs-loader-msoffice": "^0.1.25",
"@llm-tools/embedjs-loader-pdf": "^0.1.25",
"@llm-tools/embedjs-loader-sitemap": "^0.1.25",
@@ -64,7 +64,6 @@
"adm-zip": "^0.5.16",
"apache-arrow": "^18.1.0",
"docx": "^9.0.2",
"dompurify": "^3.2.3",
"electron-log": "^5.1.5",
"electron-store": "^8.2.0",
"electron-updater": "^6.3.9",
@@ -96,7 +95,7 @@
"@types/tinycolor2": "^1",
"@vitejs/plugin-react": "^4.2.1",
"antd": "^5.22.5",
"axios": "^1.7.3",
"axios": "^1.7.9",
"browser-image-compression": "^2.0.2",
"dayjs": "^1.11.11",
"dexie": "^4.0.8",

View File

@@ -1,7 +1,9 @@
import fs from 'node:fs'
import path from 'node:path'
import vm from 'node:vm'
import { Shortcut, ThemeMode } from '@types'
import axios from 'axios'
import { BrowserWindow, ipcMain, ProxyConfig, session, shell } from 'electron'
import log from 'electron-log'
@@ -154,4 +156,10 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
ipcMain.handle('knowledge-base:add', KnowledgeService.add)
ipcMain.handle('knowledge-base:remove', KnowledgeService.remove)
ipcMain.handle('knowledge-base:search', KnowledgeService.search)
// vm
ipcMain.handle('run-js', (_, code: string) => {
const context = vm.createContext(Object.assign({ fetch: fetch, URL: URL, axios: axios }, global))
return vm.runInContext(code, context)
})
}

View File

@@ -45,14 +45,14 @@ class KnowledgeService {
azureOpenAIApiDeploymentName: model,
azureOpenAIApiInstanceName: getInstanceName(baseURL),
dimensions,
batchSize: 10
batchSize: 15
})
: new OpenAiEmbeddings({
model,
apiKey,
configuration: { baseURL },
dimensions,
batchSize: 10
batchSize: 15
})
)
.setVectorDatabase(new LibSqlDb({ path: path.join(this.storageDir, id) }))
@@ -122,7 +122,7 @@ class KnowledgeService {
return await ragApplication.addLoader(new ExcelLoader({ filePathOrUrl: file.path }) as any, forceReload)
}
if (['.md'].includes(file.ext)) {
if (['.md', '.mdx'].includes(file.ext)) {
return await ragApplication.addLoader(new MarkdownLoader({ filePathOrUrl: file.path }) as any, forceReload)
}

View File

@@ -76,6 +76,9 @@ declare global {
remove: ({ uniqueId, base }: { uniqueId: string; base: KnowledgeBaseParams }) => Promise<void>
search: ({ search, base }: { search: string; base: KnowledgeBaseParams }) => Promise<ExtractChunkData[]>
}
vm: {
run: (code: string) => Promise<any>
}
}
}
}

View File

@@ -70,6 +70,9 @@ const api = {
ipcRenderer.invoke('knowledge-base:remove', { uniqueId, base }),
search: ({ search, base }: { search: string; base: KnowledgeBaseParams }) =>
ipcRenderer.invoke('knowledge-base:search', { search, base })
},
vm: {
run: (code: string) => ipcRenderer.invoke('run-js', code)
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

View File

@@ -3,7 +3,7 @@ import { isMac } from '@renderer/config/constant'
import { isLocalAi, UserAvatar } from '@renderer/config/env'
import { useTheme } from '@renderer/context/ThemeProvider'
import useAvatar from '@renderer/hooks/useAvatar'
import { modelGenerating, useRuntime } from '@renderer/hooks/useRuntime'
import { useRuntime } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings'
import { Tooltip } from 'antd'
import { Avatar } from 'antd'
@@ -19,6 +19,7 @@ const Sidebar: FC = () => {
const { pathname } = useLocation()
const avatar = useAvatar()
const { minappShow } = useRuntime()
const { generating } = useRuntime()
const { t } = useTranslation()
const navigate = useNavigate()
const { windowStyle, showMinappIcon, showFilesIcon } = useSettings()
@@ -32,8 +33,11 @@ const Sidebar: FC = () => {
const macTransparentWindow = isMac && windowStyle === 'transparent'
const sidebarBgColor = macTransparentWindow ? 'transparent' : 'var(--navbar-background)'
const to = async (path: string) => {
await modelGenerating()
const to = (path: string) => {
if (generating) {
window.message.warning({ content: t('message.switch.disabled'), key: 'switch-assistant' })
return
}
navigate(path)
}

View File

@@ -8,7 +8,6 @@ import FeloAppLogo from '@renderer/assets/images/apps/felo.png'
import GeminiAppLogo from '@renderer/assets/images/apps/gemini.png'
import GensparkLogo from '@renderer/assets/images/apps/genspark.jpg'
import GithubCopilotLogo from '@renderer/assets/images/apps/github-copilot.webp'
import HikaLogo from '@renderer/assets/images/apps/hika.webp'
import HuggingChatLogo from '@renderer/assets/images/apps/huggingchat.svg'
import KimiAppLogo from '@renderer/assets/images/apps/kimi.jpg'
import MetasoAppLogo from '@renderer/assets/images/apps/metaso.webp'
@@ -227,13 +226,6 @@ const _apps: MinAppType[] = [
url: 'https://thinkany.ai/',
bodered: true
},
{
id: 'hika',
name: 'Hika',
logo: HikaLogo,
url: 'https://hika.fyi/',
bodered: true
},
{
id: 'github-copilot',
name: 'GitHub Copilot',

View File

@@ -961,11 +961,6 @@ export const TEXT_TO_IMAGES_MODELS = [
}
]
export const TEXT_TO_IMAGES_MODELS_SUPPORT_IMAGE_ENHANCEMENT = [
'stabilityai/stable-diffusion-2-1',
'stabilityai/stable-diffusion-xl-base-1.0'
]
export function isTextToImageModel(model: Model): boolean {
return TEXT_TO_IMAGE_REGEX.test(model.id)
}
@@ -1009,13 +1004,5 @@ export function isWebSearchModel(model: Model): boolean {
return false
}
if (provider.id === 'gemini' || provider?.type === 'gemini') {
return model?.id === 'gemini-2.0-flash-exp'
}
if (provider.id === 'hunyuan') {
return model?.id !== 'hunyuan-lite'
}
return false
return (provider.id === 'gemini' || provider?.type === 'gemini') && model?.id === 'gemini-2.0-flash-exp'
}

View File

@@ -14,7 +14,6 @@ export function usePaintings() {
paintings,
addPainting: () => {
const newPainting: Painting = {
model: TEXT_TO_IMAGES_MODELS[0].id,
id: uuid(),
urls: [],
files: [],
@@ -25,7 +24,7 @@ export function usePaintings() {
seed: generateRandomSeed(),
steps: 25,
guidanceScale: 4.5,
promptEnhancement: true
model: TEXT_TO_IMAGES_MODELS[0].id
}
dispatch(addPainting(newPainting))
return newPainting

View File

@@ -1,17 +1,5 @@
import i18n from '@renderer/i18n'
import store, { useAppSelector } from '@renderer/store'
import { useAppSelector } from '@renderer/store'
export function useRuntime() {
return useAppSelector((state) => state.runtime)
}
export function modelGenerating() {
const generating = store.getState().runtime.generating
if (generating) {
window.message.warning({ content: i18n.t('message.switch.disabled'), key: 'model-generating' })
return Promise.reject()
}
return Promise.resolve()
}

View File

@@ -184,12 +184,7 @@
"open": "Open",
"size": "Size",
"text": "Text",
"title": "Files",
"edit": "Edit",
"delete": "Delete",
"delete.title": "Delete File",
"delete.content": "Deleting a file will delete its reference from all messages. Are you sure you want to delete this file?",
"delete.paintings.warning": "Image contains this file, deletion is not possible"
"title": "Files"
},
"history": {
"continue_chat": "Continue Chatting",
@@ -284,9 +279,7 @@
"regenerate.confirm": "This will replace your existing generated images. Do you want to continue?",
"seed": "Seed",
"seed_tip": "The same seed and prompt can produce similar images",
"title": "Images",
"prompt_enhancement": "Prompt Enhancement",
"prompt_enhancement_tip": "Rewrite prompts into detailed, model-friendly versions when switched on"
"title": "Images"
},
"provider": {
"aihubmix": "AiHubMix",
@@ -369,12 +362,7 @@
"webdav.minutes": "Minutes",
"webdav.restore.button": "Restore from WebDAV",
"webdav.title": "WebDAV",
"webdav.user": "WebDAV User",
"webdav.syncStatus": "Sync Status",
"webdav.autoSync.off": "Off",
"webdav.noSync": "Waiting for next sync",
"webdav.syncError": "Sync Error",
"webdav.lastSync": "Last Sync"
"webdav.user": "WebDAV User"
},
"display.title": "Display Settings",
"font_size.title": "Message font size",
@@ -580,7 +568,6 @@
"directory_placeholder": "Enter Directory Path",
"model_info": "Model Info",
"not_support": "Knowledge base database engine updated, the knowledge base will no longer be supported, please create a new knowledge base",
"no_provider": "Knowledge base model provider is not set, the knowledge base will no longer be supported, please create a new knowledge base",
"source": "Source"
},
"models": {
@@ -607,8 +594,7 @@
"parameter_type": {
"string": "Text",
"number": "Number",
"boolean": "Boolean",
"json": "JSON"
"boolean": "Boolean"
}
},
"prompts": {

View File

@@ -184,12 +184,7 @@
"open": "開く",
"size": "サイズ",
"text": "テキスト",
"title": "ファイル",
"edit": "編集",
"delete": "削除",
"delete.title": "ファイルを削除",
"delete.content": "ファイルを削除すると、ファイルがすべてのメッセージで参照されることを削除します。このファイルを削除してもよろしいですか?",
"delete.paintings.warning": "画像に含まれているため、削除できません"
"title": "ファイル"
},
"history": {
"continue_chat": "チャットを続ける",
@@ -282,9 +277,7 @@
"regenerate.confirm": "これにより、既存の生成画像が置き換えられます。続行しますか?",
"seed": "シード",
"seed_tip": "同じシードとプロンプトで似た画像を生成できます",
"title": "画像",
"prompt_enhancement": "プロンプト強化",
"prompt_enhancement_tip": "オンにすると、プロンプトを詳細でモデルに適したバージョンに書き直します"
"title": "画像"
},
"provider": {
"aihubmix": "AiHubMix",
@@ -367,12 +360,7 @@
"webdav.minutes": "分",
"webdav.restore.button": "WebDAVから復元",
"webdav.title": "WebDAV",
"webdav.user": "WebDAVユーザー",
"webdav.syncStatus": "同期状態",
"webdav.autoSync.off": "オフ",
"webdav.noSync": "次回の同期を待っています",
"webdav.syncError": "同期エラー",
"webdav.lastSync": "最終同期"
"webdav.user": "WebDAVユーザー"
},
"display.title": "表示設定",
"font_size.title": "メッセージのフォントサイズ",
@@ -563,11 +551,7 @@
"sitemap_placeholder": "サイトマップURLを入力",
"directories": "ディレクトリ",
"add_directory": "ディレクトリを追加",
"directory_placeholder": "ディレクトリパスを入力",
"model_info": "モデル情報",
"not_support": "ナレッジベースデータベースエンジンが更新されました。このナレッジベースはもうサポートされていません。新しいナレッジベースを作成してください",
"no_provider": "ナレッジベースモデルプロバイダーが設定されていません。ナレッジベースはもうサポートされていません。新しいナレッジベースを作成してください",
"source": "ソース"
"directory_placeholder": "ディレクトリパスを入力"
},
"models": {
"pinned": "固定済み",
@@ -593,8 +577,7 @@
"parameter_type": {
"string": "テキスト",
"number": "数値",
"boolean": "真偽値",
"json": "JSON"
"boolean": "真偽値"
}
},
"prompts": {

View File

@@ -184,12 +184,7 @@
"open": "Открыть",
"size": "Размер",
"text": "Текст",
"title": "Файлы",
"edit": "Редактировать",
"delete": "Удалить",
"delete.title": "Удалить файл",
"delete.content": "Удаление файла удалит его из всех сообщений, вы уверены, что хотите удалить этот файл?",
"delete.paintings.warning": "В изображениях содержится этот файл, удаление невозможно"
"title": "Файлы"
},
"history": {
"continue_chat": "Продолжить чат",
@@ -284,9 +279,7 @@
"regenerate.confirm": "Это заменит ваши существующие сгенерированные изображения. Хотите продолжить?",
"seed": "Ключ генерации",
"seed_tip": "Одинаковый ключ генерации и промпт могут производить похожие изображения",
"title": "Изображения",
"prompt_enhancement": "Улучшение промпта",
"prompt_enhancement_tip": "При включении переписывает промпт в более детальную, модель-ориентированную версию"
"title": "Изображения"
},
"provider": {
"aihubmix": "AiHubMix",
@@ -369,12 +362,7 @@
"webdav.minutes": "минут",
"webdav.restore.button": "Восстановление с WebDAV",
"webdav.title": "WebDAV",
"webdav.user": "Пользователь WebDAV",
"webdav.syncStatus": "Статус синхронизации",
"webdav.autoSync.off": "Выключено",
"webdav.noSync": "Ожидание следующей синхронизации",
"webdav.syncError": "Ошибка синхронизации",
"webdav.lastSync": "Последняя синхронизация"
"webdav.user": "Пользователь WebDAV"
},
"display.title": "Настройки отображения",
"font_size.title": "Размер шрифта сообщений",
@@ -580,7 +568,6 @@
"directory_placeholder": "Введите путь к директории",
"model_info": "Модель информации",
"not_support": "База знаний базы данных движок обновлен, база знаний больше не поддерживается, пожалуйста, создайте новую базу знаний",
"no_provider": "База знаний модель поставщика не настроена, база знаний больше не поддерживается, пожалуйста, создайте новую базу знаний",
"source": "Источник"
},
"models": {
@@ -607,8 +594,7 @@
"parameter_type": {
"string": "Текст",
"number": "Число",
"boolean": "Логическое",
"json": "JSON"
"boolean": "Логическое"
}
},
"prompts": {

View File

@@ -185,12 +185,7 @@
"open": "打开",
"size": "大小",
"text": "文本",
"title": "文件",
"edit": "编辑",
"delete": "删除",
"delete.title": "删除文件",
"delete.content": "删除文件会删除文件在所有消息中的引用,确定要删除此文件吗?",
"delete.paintings.warning": "绘图中包含该图片,暂时无法删除"
"title": "文件"
},
"history": {
"continue_chat": "继续聊天",
@@ -285,9 +280,7 @@
"regenerate.confirm": "这将覆盖已生成的图片,是否继续?",
"seed": "随机种子",
"seed_tip": "相同的种子和提示词可以生成相似的图片",
"title": "图片",
"prompt_enhancement": "提示词增强",
"prompt_enhancement_tip": "开启后将提示重写为详细的、适合模型的版本"
"title": "图片"
},
"provider": {
"aihubmix": "AiHubMix",
@@ -370,12 +363,7 @@
"webdav.minutes": "分钟",
"webdav.restore.button": "从 WebDAV 恢复",
"webdav.title": "WebDAV",
"webdav.user": "WebDAV 用户名",
"webdav.syncStatus": "同步状态",
"webdav.autoSync.off": "关闭",
"webdav.noSync": "等待下次同步",
"webdav.syncError": "同步错误",
"webdav.lastSync": "上次同步时间"
"webdav.user": "WebDAV 用户名"
},
"display.title": "显示设置",
"font_size.title": "消息字体大小",
@@ -569,7 +557,6 @@
"directory_placeholder": "请输入目录路径",
"model_info": "模型信息",
"not_support": "知识库数据库引擎已更新,该知识库将不再支持,请重新创建知识库",
"no_provider": "知识库模型服务商丢失,该知识库将不再支持,请重新创建知识库",
"source": "来源"
},
"models": {
@@ -596,8 +583,7 @@
"parameter_type": {
"string": "文本",
"number": "数字",
"boolean": "布尔值",
"json": "JSON"
"boolean": "布尔值"
}
},
"prompts": {

View File

@@ -184,12 +184,7 @@
"open": "打開",
"size": "大小",
"text": "文本",
"title": "檔案",
"edit": "編輯",
"delete": "刪除",
"delete.title": "刪除檔案",
"delete.content": "刪除檔案會刪除檔案在所有消息中的引用,確定要刪除此檔案嗎?",
"delete.paintings.warning": "繪圖中包含該圖片,暫時無法刪除"
"title": "檔案"
},
"history": {
"continue_chat": "繼續聊天",
@@ -284,9 +279,7 @@
"regenerate.confirm": "這將覆蓋已生成的圖片,是否繼續?",
"seed": "隨機種子",
"seed_tip": "相同的種子和提示詞可以生成相似的圖片",
"title": "繪圖",
"prompt_enhancement": "提示詞增強",
"prompt_enhancement_tip": "開啟後將提示重寫為詳細的、適合模型的版本"
"title": "繪圖"
},
"provider": {
"aihubmix": "AiHubMix",
@@ -369,12 +362,7 @@
"webdav.minutes": "分鐘",
"webdav.restore.button": "從 WebDAV 恢復",
"webdav.title": "WebDAV",
"webdav.user": "WebDAV 使用者名稱",
"webdav.syncStatus": "同步狀態",
"webdav.autoSync.off": "關閉",
"webdav.noSync": "等待下次同步",
"webdav.syncError": "同步錯誤",
"webdav.lastSync": "上次同步時間"
"webdav.user": "WebDAV 使用者名稱"
},
"display.title": "顯示設定",
"font_size.title": "訊息字體大小",
@@ -568,7 +556,6 @@
"directory_placeholder": "請輸入目錄路徑",
"model_info": "模型信息",
"not_support": "知識庫數據庫引擎已更新,該知識庫將不再支持,請重新創建知識庫",
"no_provider": "知識庫模型提供商遺失,該知識庫將不再支持,請重新創建知識庫",
"source": "來源"
},
"models": {
@@ -595,8 +582,7 @@
"parameter_type": {
"string": "文字",
"number": "數字",
"boolean": "布林值",
"json": "JSON"
"boolean": "布林值"
}
},
"prompts": {

View File

@@ -76,8 +76,7 @@ const AppsContainer = styled.div`
display: flex;
min-width: 930px;
max-width: 930px;
max-height: 520px;
min-height: 520px;
max-height: 500px;
display: grid;
grid-template-columns: repeat(8, minmax(90px, 1fr));
gap: 25px 25px;

View File

@@ -1,21 +1,11 @@
import {
DeleteOutlined,
EditOutlined,
EllipsisOutlined,
FileImageOutlined,
FilePdfOutlined,
FileTextOutlined
} from '@ant-design/icons'
import { FileImageOutlined, FilePdfOutlined, FileTextOutlined } from '@ant-design/icons'
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
import TextEditPopup from '@renderer/components/Popups/TextEditPopup'
import Scrollbar from '@renderer/components/Scrollbar'
import db from '@renderer/databases'
import FileManager from '@renderer/services/FileManager'
import store from '@renderer/store'
import { FileType, FileTypes } from '@renderer/types'
import { formatFileSize } from '@renderer/utils'
import type { MenuProps } from 'antd'
import { Button, Col, Dropdown, Image, Menu, Row, Spin, Table } from 'antd'
import { Col, Image, Menu, Row, Spin, Table } from 'antd'
import dayjs from 'dayjs'
import { useLiveQuery } from 'dexie-react-hooks'
import { FC, useState } from 'react'
@@ -33,88 +23,14 @@ const FilesPage: FC = () => {
return db.files.where('type').equals(fileType).sortBy('count')
}, [fileType])
const handleDelete = async (fileId: string) => {
const file = await FileManager.getFile(fileId)
const paintings = await store.getState().paintings.paintings
const paintingsFiles = paintings.flatMap((p) => p.files)
if (paintingsFiles.some((p) => p.id === fileId)) {
window.modal.warning({ content: t('files.delete.paintings.warning'), centered: true })
return
}
if (file) {
await FileManager.deleteFile(fileId, true)
}
const topics = await db.topics
.filter((topic) => topic.messages.some((message) => message.files?.some((f) => f.id === fileId)))
.toArray()
if (topics.length > 0) {
for (const topic of topics) {
const updatedMessages = topic.messages.map((message) => ({
...message,
files: message.files?.filter((f) => f.id !== fileId)
}))
await db.topics.update(topic.id, { messages: updatedMessages })
}
}
}
const handleRename = async (fileId: string) => {
const file = await FileManager.getFile(fileId)
if (file) {
const newName = await TextEditPopup.show({ text: file.origin_name })
if (newName) {
FileManager.updateFile({ ...file, origin_name: newName })
}
}
}
const getActionMenu = (fileId: string): MenuProps['items'] => [
{
key: 'rename',
icon: <EditOutlined />,
label: t('files.edit'),
onClick: () => handleRename(fileId)
},
{
key: 'delete',
icon: <DeleteOutlined />,
label: t('files.delete'),
danger: true,
onClick: () => {
window.modal.confirm({
title: t('files.delete.title'),
content: t('files.delete.content'),
centered: true,
okButtonProps: { danger: true },
onOk: () => handleDelete(fileId)
})
}
}
]
const dataSource = files?.map((file) => {
return {
key: file.id,
file: (
<FileNameText className="text-nowrap" onClick={() => window.api.file.openPath(file.path)}>
{file.origin_name}
</FileNameText>
),
file: <FileNameText className="text-nowrap">{file.origin_name}</FileNameText>,
size: formatFileSize(file),
size_bytes: file.size,
count: file.count,
created_at: dayjs(file.created_at).format('MM-DD HH:mm'),
created_at_unix: dayjs(file.created_at).unix(),
actions: (
<Dropdown menu={{ items: getActionMenu(file.id) }} trigger={['click']}>
<Button type="text" size="small" icon={<EllipsisOutlined />} />
</Dropdown>
)
actions: <a href={'file://' + FileManager.getSafePath(file)}>{t('files.open')}</a>
}
})
@@ -129,25 +45,19 @@ const FilesPage: FC = () => {
title: t('files.size'),
dataIndex: 'size',
key: 'size',
width: '80px',
sorter: (a: { size_bytes: number }, b: { size_bytes: number }) => b.size_bytes - a.size_bytes,
align: 'center'
width: '80px'
},
{
title: t('files.count'),
dataIndex: 'count',
key: 'count',
width: '60px',
sorter: (a: { count: number }, b: { count: number }) => b.count - a.count,
align: 'center'
width: '60px'
},
{
title: t('files.created_at'),
dataIndex: 'created_at',
key: 'created_at',
width: '120px',
align: 'center',
sorter: (a: { created_at_unix: number }, b: { created_at_unix: number }) => b.created_at_unix - a.created_at_unix
width: '120px'
},
{
title: t('files.actions'),
@@ -203,7 +113,7 @@ const FilesPage: FC = () => {
) : (
<Table
dataSource={dataSource}
columns={columns as any}
columns={columns}
style={{ width: '100%' }}
size="small"
pagination={{ pageSize: 100 }}
@@ -239,7 +149,6 @@ const TableContainer = styled(Scrollbar)`
const FileNameText = styled.div`
font-size: 14px;
color: var(--color-text);
cursor: pointer;
`
const ImageWrapper = styled.div`

View File

@@ -36,7 +36,7 @@ const HomePage: FC = () => {
}, [state])
return (
<Container id="home-page">
<Container>
<Navbar activeAssistant={activeAssistant} activeTopic={activeTopic} setActiveTopic={setActiveTopic} />
<ContentContainer id="content-container">
{showAssistants && (

View File

@@ -13,7 +13,7 @@ import TranslateButton from '@renderer/components/TranslateButton'
import { isVisionModel, isWebSearchModel } from '@renderer/config/models'
import db from '@renderer/databases'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { modelGenerating, useRuntime } from '@renderer/hooks/useRuntime'
import { useRuntime } from '@renderer/hooks/useRuntime'
import { useMessageStyle, useSettings } from '@renderer/hooks/useSettings'
import { useShortcut, useShortcutDisplay } from '@renderer/hooks/useShortcuts'
import { useShowTopics } from '@renderer/hooks/useStore'
@@ -25,7 +25,7 @@ import { translateText } from '@renderer/services/TranslateService'
import store, { useAppDispatch, useAppSelector } from '@renderer/store'
import { setGenerating, setSearching } from '@renderer/store/runtime'
import { Assistant, FileType, KnowledgeBase, Message, Topic } from '@renderer/types'
import { classNames, delay, getFileExtension, uuid } from '@renderer/utils'
import { delay, getFileExtension, uuid } from '@renderer/utils'
import { documentExts, imageExts, textExts } from '@shared/config/constant'
import { Button, Popconfirm, Tooltip } from 'antd'
import TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
@@ -97,7 +97,9 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
_base = selectedKnowledgeBase
const sendMessage = useCallback(async () => {
await modelGenerating()
if (generating) {
return
}
if (inputEmpty) {
return
@@ -205,7 +207,10 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
}
const addNewTopic = useCallback(async () => {
await modelGenerating()
if (generating) {
window.message.warning({ content: t('message.switch.disabled'), key: 'generating' })
return
}
const topic = getDefaultTopic(assistant.id)
@@ -221,7 +226,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
setActiveTopic(topic)
clickAssistantToShowTopic && setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR), 0)
}, [addTopic, assistant, clickAssistantToShowTopic, setActiveTopic, setModel])
}, [addTopic, assistant, clickAssistantToShowTopic, generating, setActiveTopic, setModel, t])
const clearTopic = async () => {
if (generating) {
@@ -383,12 +388,9 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
}
return (
<Container onDragOver={handleDragOver} onDrop={handleDrop} className="inputbar">
<Container onDragOver={handleDragOver} onDrop={handleDrop}>
<AttachmentPreview files={files} setFiles={setFiles} />
<InputBarContainer
id="inputbar"
className={classNames('inputbar-container', inputFocus && 'focus')}
ref={containerRef}>
<InputBarContainer id="inputbar" className={inputFocus ? 'focus' : ''} ref={containerRef}>
<Textarea
value={text}
onChange={(e) => setText(e.target.value)}

View File

@@ -62,7 +62,6 @@ const KnowledgeBaseButton: FC<Props> = ({ selectedBase, onSelect, disabled, Tool
<Popover
placement="top"
content={<KnowledgeBaseSelector selectedBase={selectedBase} onSelect={onSelect} />}
overlayStyle={{ maxWidth: 400 }}
trigger="click">
<ToolbarButton type="text" onClick={() => selectedBase && onSelect(undefined)} disabled={disabled}>
<FileSearchOutlined style={{ color: selectedBase ? 'var(--color-link)' : 'var(--color-icon)' }} />

View File

@@ -3,7 +3,6 @@ import CopyIcon from '@renderer/components/Icons/CopyIcon'
import { useSyntaxHighlighter } from '@renderer/context/SyntaxHighlighterProvider'
import { useSettings } from '@renderer/hooks/useSettings'
import React, { memo, useEffect, useRef, useState } from 'react'
import DOMPurify from 'dompurify'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
@@ -38,7 +37,6 @@ const ExpandButton: React.FC<{
</ExpandButtonWrapper>
)
}
const ALLOWED_TAGS = ['sub'] // 允许的HTML标签
const CodeBlock: React.FC<CodeBlockProps> = ({ children, className }) => {
const match = /language-(\w+)/.exec(className || '')
@@ -135,15 +133,7 @@ const CodeBlock: React.FC<CodeBlockProps> = ({ children, className }) => {
{language === 'html' && children?.includes('</html>') && <Artifacts html={children} />}
</CodeBlockWrapper>
) : (
<code
className={className}
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(children, {
ALLOWED_TAGS,
ALLOWED_ATTR: [] // 不允许任何属性
})
}}
/>
<code className={className}>{children}</code>
)
}

View File

@@ -54,7 +54,7 @@ const MessageHeader: FC<Props> = memo(({ assistant, model, message }) => {
: undefined
return (
<Container className="message-header">
<Container>
<AvatarWrapper style={avatarStyle}>
{isAssistantMessage ? (
<Avatar

View File

@@ -11,11 +11,10 @@ import {
} from '@ant-design/icons'
import SelectModelPopup from '@renderer/components/Popups/SelectModelPopup'
import TextEditPopup from '@renderer/components/Popups/TextEditPopup'
import { modelGenerating } from '@renderer/hooks/useRuntime'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { translateText } from '@renderer/services/TranslateService'
import { Message, Model } from '@renderer/types'
import { removeTrailingDoubleSpaces, uuid } from '@renderer/utils'
import { removeTrailingDoubleSpaces } from '@renderer/utils'
import { Button, Dropdown, Popconfirm, Tooltip } from 'antd'
import dayjs from 'dayjs'
import { FC, useCallback, useMemo, useState } from 'react'
@@ -70,8 +69,7 @@ const MessageMenubar: FC<Props> = (props) => {
[setModel]
)
const onNewBranch = useCallback(async () => {
await modelGenerating()
const onNewBranch = useCallback(() => {
EventEmitter.emit(EVENT_NAMES.NEW_BRANCH, index)
window.message.success({
content: t('chat.message.new.branch.created'),
@@ -79,8 +77,7 @@ const MessageMenubar: FC<Props> = (props) => {
})
}, [index, t])
const onResend = useCallback(async () => {
await modelGenerating()
const onResend = useCallback(() => {
const _messages = onGetMessages?.() || []
const index = _messages.findIndex((m) => m.id === message.id)
const nextIndex = index + 1
@@ -95,12 +92,7 @@ const MessageMenubar: FC<Props> = (props) => {
translatedContent: undefined
})
}
if (!nextMessage) {
onDeleteMessage?.(message)
EventEmitter.emit(EVENT_NAMES.SEND_MESSAGE, { ...message, id: uuid() })
}
}, [assistantModel?.id, message, model?.id, onDeleteMessage, onGetMessages])
}, [assistantModel?.id, message.id, model?.id, onGetMessages])
const onEdit = useCallback(async () => {
let resendMessage = false
@@ -166,23 +158,57 @@ const MessageMenubar: FC<Props> = (props) => {
onClick: onEdit
},
{
label: t('chat.message.new.branch'),
key: 'new-branch',
icon: <ForkOutlined />,
onClick: onNewBranch
label: t('chat.translate'),
key: 'translate',
icon: isTranslating ? <SyncOutlined spin /> : <TranslationOutlined />,
children: [
{
label: '🇨🇳 ' + t('languages.chinese'),
key: 'translate-chinese',
onClick: () => handleTranslate('chinese')
},
{
label: '🇭🇰 ' + t('languages.chinese-traditional'),
key: 'translate-chinese-traditional',
onClick: () => handleTranslate('chinese-traditional')
},
{
label: '🇬🇧 ' + t('languages.english'),
key: 'translate-english',
onClick: () => handleTranslate('english')
},
{
label: '🇯🇵 ' + t('languages.japanese'),
key: 'translate-japanese',
onClick: () => handleTranslate('japanese')
},
{
label: '🇰🇷 ' + t('languages.korean'),
key: 'translate-korean',
onClick: () => handleTranslate('korean')
},
{
label: '🇷🇺 ' + t('languages.russian'),
key: 'translate-russian',
onClick: () => handleTranslate('russian')
},
{
label: '✖ ' + t('translate.close'),
key: 'translate-close',
onClick: () => onEditMessage?.({ ...message, translatedContent: undefined })
}
]
}
],
[message, onEdit, onNewBranch, t]
[handleTranslate, isTranslating, message, onEdit, onEditMessage, t]
)
const onAtModelRegenerate = async () => {
await modelGenerating()
const selectedModel = await SelectModelPopup.show({ model })
selectedModel && onRegenerate(selectedModel)
}
const onDeleteAndRegenerate = async () => {
await modelGenerating()
const onDeleteAndRegenerate = () => {
onEditMessage?.({
...message,
content: '',
@@ -228,56 +254,12 @@ const MessageMenubar: FC<Props> = (props) => {
</ActionButton>
</Tooltip>
)}
{!isUserMessage && (
<Dropdown
menu={{
items: [
{
label: '🇨🇳 ' + t('languages.chinese'),
key: 'translate-chinese',
onClick: () => handleTranslate('chinese')
},
{
label: '🇭🇰 ' + t('languages.chinese-traditional'),
key: 'translate-chinese-traditional',
onClick: () => handleTranslate('chinese-traditional')
},
{
label: '🇬🇧 ' + t('languages.english'),
key: 'translate-english',
onClick: () => handleTranslate('english')
},
{
label: '🇯🇵 ' + t('languages.japanese'),
key: 'translate-japanese',
onClick: () => handleTranslate('japanese')
},
{
label: '🇰🇷 ' + t('languages.korean'),
key: 'translate-korean',
onClick: () => handleTranslate('korean')
},
{
label: '🇷🇺 ' + t('languages.russian'),
key: 'translate-russian',
onClick: () => handleTranslate('russian')
},
{
label: '✖ ' + t('translate.close'),
key: 'translate-close',
onClick: () => onEditMessage?.({ ...message, translatedContent: undefined })
}
]
}}
trigger={['click']}
placement="topRight"
arrow>
<Tooltip title={t('chat.translate')} mouseEnterDelay={1.2}>
<ActionButton className="message-action-button">
<TranslationOutlined />
</ActionButton>
</Tooltip>
</Dropdown>
{isAssistantMessage && (
<Tooltip title={t('chat.message.new.branch')} mouseEnterDelay={0.8}>
<ActionButton className="message-action-button" onClick={onNewBranch}>
<ForkOutlined />
</ActionButton>
</Tooltip>
)}
<Popconfirm
title={t('message.message.delete.content')}

View File

@@ -136,7 +136,6 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
(message: Message) => {
const _messages = messages.filter((m) => m.id !== message.id)
setMessages(_messages)
setDisplayMessages(_messages)
db.topics.update(topic.id, { messages: _messages })
deleteMessageFiles(message)
},

View File

@@ -41,7 +41,7 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant }) => {
})
return (
<Navbar className="home-navbar">
<Navbar>
{showAssistants && (
<NavbarLeft style={{ justifyContent: 'space-between', borderRight: 'none', padding: '0 8px' }}>
<NavbarIcon onClick={toggleShowAssistants} style={{ marginLeft: isMac ? 8 : 0 }}>
@@ -52,9 +52,7 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant }) => {
</NavbarIcon>
</NavbarLeft>
)}
<NavbarRight
style={{ justifyContent: 'space-between', paddingRight: isWindows ? 140 : 12, flex: 1 }}
className="home-navbar-right">
<NavbarRight style={{ justifyContent: 'space-between', paddingRight: isWindows ? 140 : 12, flex: 1 }}>
<HStack alignItems="center">
{!showAssistants && (
<NavbarIcon

View File

@@ -4,11 +4,11 @@ import CopyIcon from '@renderer/components/Icons/CopyIcon'
import Scrollbar from '@renderer/components/Scrollbar'
import { useAgents } from '@renderer/hooks/useAgents'
import { useAssistant, useAssistants } from '@renderer/hooks/useAssistant'
import { modelGenerating } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings'
import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings'
import { getDefaultTopic } from '@renderer/services/AssistantService'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { useAppSelector } from '@renderer/store'
import { Assistant } from '@renderer/types'
import { uuid } from '@renderer/utils'
import { Dropdown } from 'antd'
@@ -32,6 +32,7 @@ const Assistants: FC<Props> = ({
onCreateDefaultAssistant
}) => {
const { assistants, removeAssistant, addAssistant, updateAssistants } = useAssistants()
const generating = useAppSelector((state) => state.runtime.generating)
const [dragging, setDragging] = useState(false)
const { removeAllTopics } = useAssistant(activeAssistant.id)
const { clickAssistantToShowTopic, topicPosition } = useSettings()
@@ -40,7 +41,7 @@ const Assistants: FC<Props> = ({
const onDelete = useCallback(
(assistant: Assistant) => {
const _assistant: Assistant | undefined = last(assistants.filter((a) => a.id !== assistant.id))
const _assistant = last(assistants.filter((a) => a.id !== assistant.id))
_assistant ? setActiveAssistant(_assistant) : onCreateDefaultAssistant()
removeAssistant(assistant.id)
},
@@ -116,8 +117,13 @@ const Assistants: FC<Props> = ({
)
const onSwitchAssistant = useCallback(
async (assistant: Assistant) => {
await modelGenerating()
(assistant: Assistant): any => {
if (generating) {
return window.message.warning({
content: t('message.switch.disabled'),
key: 'switch-assistant'
})
}
if (topicPosition === 'left' && clickAssistantToShowTopic) {
EventEmitter.emit(EVENT_NAMES.SWITCH_TOPIC_SIDEBAR)
@@ -125,11 +131,11 @@ const Assistants: FC<Props> = ({
setActiveAssistant(assistant)
},
[clickAssistantToShowTopic, setActiveAssistant, topicPosition]
[clickAssistantToShowTopic, generating, setActiveAssistant, t, topicPosition]
)
return (
<Container className="assistants-tab">
<Container>
<DragableList
list={assistants}
onUpdate={updateAssistants}

View File

@@ -1,4 +1,4 @@
import { CheckOutlined, QuestionCircleOutlined, ReloadOutlined } from '@ant-design/icons'
import { CheckOutlined, DeleteOutlined, PlusOutlined, QuestionCircleOutlined, ReloadOutlined } from '@ant-design/icons'
import { HStack } from '@renderer/components/Layout'
import Scrollbar from '@renderer/components/Scrollbar'
import {
@@ -29,7 +29,7 @@ import {
setShowMessageDivider
} from '@renderer/store/settings'
import { Assistant, AssistantSettings, ThemeMode } from '@renderer/types'
import { Col, InputNumber, Row, Select, Slider, Switch, Tooltip } from 'antd'
import { Button, Col, Input, InputNumber, Row, Select, Slider, Switch, Tooltip } from 'antd'
import { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
@@ -117,7 +117,7 @@ const SettingsTab: FC<Props> = (props) => {
}, [assistant])
return (
<Container className="settings-tab">
<Container>
<SettingGroup style={{ marginTop: 10 }}>
<SettingSubtitle style={{ marginTop: 0 }}>
{t('settings.messages.model.title')}{' '}
@@ -203,6 +203,106 @@ const SettingsTab: FC<Props> = (props) => {
/>
</Col>
</Row>
{assistant?.settings?.customParameters?.map((param, index) => (
<ParameterCard key={index}>
<Row align="middle" gutter={8} style={{ marginBottom: 8 }}>
<Col span={14}>
<Input
placeholder={t('models.parameter_name')}
value={param.name}
onChange={(e) => {
const newParams = [...(assistant?.settings?.customParameters || [])]
newParams[index] = { ...param, name: e.target.value }
onUpdateAssistantSettings({ customParameters: newParams })
}}
/>
</Col>
<Col span={10}>
<Select
value={param.type}
onChange={(value: 'string' | 'number' | 'boolean') => {
const newParams = [...(assistant?.settings?.customParameters || [])]
let defaultValue: any = ''
switch (value) {
case 'number':
defaultValue = 0
break
case 'boolean':
defaultValue = false
break
default:
defaultValue = ''
}
newParams[index] = { ...param, type: value, value: defaultValue }
onUpdateAssistantSettings({ customParameters: newParams })
}}
style={{ width: '100%' }}>
<Select.Option value="string">{t('models.parameter_type.string')}</Select.Option>
<Select.Option value="number">{t('models.parameter_type.number')}</Select.Option>
<Select.Option value="boolean">{t('models.parameter_type.boolean')}</Select.Option>
</Select>
</Col>
</Row>
<Row align="middle" gutter={10}>
<Col span={20}>
{param.type === 'boolean' ? (
<Switch
checked={param.value as boolean}
onChange={(checked) => {
const newParams = [...(assistant?.settings?.customParameters || [])]
newParams[index] = { ...param, value: checked }
onUpdateAssistantSettings({ customParameters: newParams })
}}
/>
) : param.type === 'number' ? (
<InputNumber
style={{ width: '100%' }}
value={param.value as number}
onChange={(value) => {
const newParams = [...(assistant?.settings?.customParameters || [])]
newParams[index] = { ...param, value: value || 0 }
onUpdateAssistantSettings({ customParameters: newParams })
}}
step={0.01}
/>
) : (
<Input
value={typeof param.value === 'string' ? param.value : JSON.stringify(param.value)}
onChange={(e) => {
const newParams = [...(assistant?.settings?.customParameters || [])]
newParams[index] = { ...param, value: e.target.value }
onUpdateAssistantSettings({ customParameters: newParams })
}}
/>
)}
</Col>
<Col span={4}>
<Button
icon={<DeleteOutlined />}
onClick={() => {
const newParams = [...(assistant?.settings?.customParameters || [])]
newParams.splice(index, 1)
onUpdateAssistantSettings({ customParameters: newParams })
}}
danger
style={{ width: '100%' }}
/>
</Col>
</Row>
</ParameterCard>
))}
<Button
icon={<PlusOutlined />}
onClick={() => {
const newParams = [
...(assistant?.settings?.customParameters || []),
{ name: '', value: '', type: 'string' as const }
]
onUpdateAssistantSettings({ customParameters: newParams })
}}
style={{ marginBottom: 0, width: '100%', borderStyle: 'dashed' }}>
{t('models.add_parameter')}
</Button>
</SettingGroup>
<SettingGroup>
<SettingSubtitle style={{ marginTop: 0 }}>{t('settings.messages.title')}</SettingSubtitle>
@@ -391,7 +491,6 @@ const Container = styled(Scrollbar)`
padding: 0 10px;
padding-right: 5px;
padding-top: 2px;
padding-bottom: 10px;
`
const Label = styled.p`
@@ -411,11 +510,24 @@ const SettingRowTitleSmall = styled(SettingRowTitle)`
`
export const SettingGroup = styled.div<{ theme?: ThemeMode }>`
padding: 0 5px;
padding: 10px;
width: 100%;
margin-top: 0;
border-radius: 8px;
margin-bottom: 10px;
border: 0.5px solid var(--color-border);
background: var(--color-group-background);
`
const ParameterCard = styled.div`
margin-bottom: 8px;
padding: 8px;
border: 1px solid var(--color-border);
border-radius: 6px;
background: var(--color-background);
&:last-child {
margin-bottom: 12px;
}
`
export default SettingsTab

View File

@@ -10,12 +10,11 @@ import DragableList from '@renderer/components/DragableList'
import PromptPopup from '@renderer/components/Popups/PromptPopup'
import Scrollbar from '@renderer/components/Scrollbar'
import { useAssistant, useAssistants } from '@renderer/hooks/useAssistant'
import { modelGenerating } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings'
import { TopicManager } from '@renderer/hooks/useTopic'
import { fetchMessagesSummary } from '@renderer/services/ApiService'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import store from '@renderer/store'
import store, { useAppSelector } from '@renderer/store'
import { setGenerating } from '@renderer/store/runtime'
import { Assistant, Topic } from '@renderer/types'
import { exportTopicAsMarkdown, topicToMarkdown } from '@renderer/utils/export'
@@ -36,36 +35,46 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
const { assistants } = useAssistants()
const { assistant, removeTopic, moveTopic, updateTopic, updateTopics } = useAssistant(_assistant.id)
const { t } = useTranslation()
const generating = useAppSelector((state) => state.runtime.generating)
const { showTopicTime, topicPosition } = useSettings()
const borderRadius = showTopicTime ? 12 : 17
const onDeleteTopic = useCallback(
async (topic: Topic) => {
await modelGenerating()
(topic: Topic) => {
if (generating) {
window.message.warning({ content: t('message.switch.disabled'), key: 'generating' })
return
}
const index = findIndex(assistant.topics, (t) => t.id === topic.id)
setActiveTopic(assistant.topics[index + 1 === assistant.topics.length ? 0 : index + 1])
removeTopic(topic)
},
[assistant.topics, removeTopic, setActiveTopic]
[assistant.topics, generating, removeTopic, setActiveTopic, t]
)
const onMoveTopic = useCallback(
async (topic: Topic, toAssistant: Assistant) => {
await modelGenerating()
(topic: Topic, toAssistant: Assistant) => {
if (generating) {
window.message.warning({ content: t('message.switch.disabled'), key: 'generating' })
return
}
const index = findIndex(assistant.topics, (t) => t.id === topic.id)
setActiveTopic(assistant.topics[index + 1 === assistant.topics.length ? 0 : index + 1])
moveTopic(topic, toAssistant)
},
[assistant.topics, moveTopic, setActiveTopic]
[assistant.topics, generating, moveTopic, setActiveTopic, t]
)
const onSwitchTopic = useCallback(
async (topic: Topic) => {
await modelGenerating()
(topic: Topic) => {
if (generating) {
window.message.warning({ content: t('message.switch.disabled'), key: 'generating' })
return
}
setActiveTopic(topic)
},
[setActiveTopic]
[generating, setActiveTopic, t]
)
const onClearMessages = useCallback(() => {
@@ -177,7 +186,7 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
)
return (
<Container right={topicPosition === 'right'} className="topics-tab">
<Container right={topicPosition === 'right'}>
<DragableList list={assistant.topics} onUpdate={updateTopics}>
{(topic) => {
const isActive = topic.id === activeTopic?.id

View File

@@ -94,7 +94,7 @@ const HomeTabs: FC<Props> = ({ activeAssistant, activeTopic, setActiveAssistant,
}, [position, tab, topicPosition])
return (
<Container style={border} className="home-tabs">
<Container style={border}>
{showTab && (
<Segmented
value={tab}
@@ -125,7 +125,7 @@ const HomeTabs: FC<Props> = ({ activeAssistant, activeTopic, setActiveAssistant,
block
/>
)}
<TabContent className="home-tabs-content">
<TabContent>
{tab === 'assistants' && (
<Assistants
activeAssistant={activeAssistant}

View File

@@ -4,7 +4,7 @@ import SelectModelPopup from '@renderer/components/Popups/SelectModelPopup'
import { isLocalAi } from '@renderer/config/env'
import { isVisionModel } from '@renderer/config/models'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { getProviderName } from '@renderer/services/ProviderService'
import { getProviderByModel } from '@renderer/services/AssistantService'
import { Assistant } from '@renderer/types'
import { Button } from 'antd'
import { FC } from 'react'
@@ -31,14 +31,13 @@ const SelectModelButton: FC<Props> = ({ assistant }) => {
}
}
const providerName = getProviderName(model?.provider)
return (
<DropdownButton size="small" type="default" onClick={onSelectModel}>
<ButtonContent>
<ModelAvatar model={model} size={20} />
<ModelName>
{model ? model.name : t('button.select_model')} {providerName ? '| ' + providerName : ''}
{model ? model.name : t('button.select_model')} |{' '}
{t(`provider.${model?.provider}`, { defaultValue: getProviderByModel(model)?.name })}
</ModelName>
{isVisionModel(model) && <VisionIcon style={{ marginLeft: 0 }} />}
</ButtonContent>

View File

@@ -13,7 +13,6 @@ import TextEditPopup from '@renderer/components/Popups/TextEditPopup'
import Scrollbar from '@renderer/components/Scrollbar'
import { useKnowledge } from '@renderer/hooks/useKnowledge'
import FileManager from '@renderer/services/FileManager'
import { getProviderName } from '@renderer/services/ProviderService'
import { FileType, FileTypes, KnowledgeBase } from '@renderer/types'
import { Alert, Button, Card, Divider, message, Tag, Typography, Upload } from 'antd'
import { FC } from 'react'
@@ -30,7 +29,7 @@ interface KnowledgeContentProps {
selectedBase: KnowledgeBase
}
const fileTypes = ['.pdf', '.docx', '.pptx', '.xlsx', '.txt', '.md']
const fileTypes = ['.pdf', '.docx', '.pptx', '.xlsx', '.txt', '.md', '.mdx']
const FlexColumn = styled.div`
display: flex;
@@ -75,17 +74,11 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
addDirectory
} = useKnowledge(selectedBase.id || '')
const providerName = getProviderName(base?.model.provider || '')
const disabled = !base?.version || !providerName
if (!base) {
return null
}
const handleAddFile = () => {
if (disabled) {
return
}
const input = document.createElement('input')
input.type = 'file'
input.multiple = true
@@ -98,10 +91,6 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
}
const handleDrop = async (files: File[]) => {
if (disabled) {
return
}
if (files) {
const _files: FileType[] = files.map((file) => ({
id: file.name,
@@ -121,10 +110,6 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
}
const handleAddUrl = async () => {
if (disabled) {
return
}
const url = await PromptPopup.show({
title: t('knowledge_base.add_url'),
message: '',
@@ -150,10 +135,6 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
}
const handleAddSitemap = async () => {
if (disabled) {
return
}
const url = await PromptPopup.show({
title: t('knowledge_base.add_sitemap'),
message: '',
@@ -179,28 +160,16 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
}
const handleAddNote = async () => {
if (disabled) {
return
}
const note = await TextEditPopup.show({ text: '', textareaProps: { rows: 20 } })
note && addNote(note)
}
const handleEditNote = async (note: any) => {
if (disabled) {
return
}
const editedText = await TextEditPopup.show({ text: note.content as string, textareaProps: { rows: 20 } })
editedText && updateNoteContent(note.id, editedText)
}
const handleAddDirectory = async () => {
if (disabled) {
return
}
const path = await window.api.file.selectFolder()
console.log('[KnowledgeContent] Selected directory:', path)
path && addDirectory(path)
@@ -211,13 +180,10 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
{!base?.version && (
<Alert message={t('knowledge_base.not_support')} type="error" style={{ marginBottom: 20 }} showIcon />
)}
{!providerName && (
<Alert message={t('knowledge_base.no_provider')} type="error" style={{ marginBottom: 20 }} showIcon />
)}
<FileSection>
<TitleWrapper>
<Title level={5}>{t('files.title')}</Title>
<Button icon={<PlusOutlined />} onClick={handleAddFile} disabled={disabled}>
<Button icon={<PlusOutlined />} onClick={handleAddFile}>
{t('knowledge_base.add_file')}
</Button>
</TitleWrapper>
@@ -257,7 +223,7 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
<ContentSection>
<TitleWrapper>
<Title level={5}>{t('knowledge_base.directories')}</Title>
<Button icon={<PlusOutlined />} onClick={handleAddDirectory} disabled={disabled}>
<Button icon={<PlusOutlined />} onClick={handleAddDirectory}>
{t('knowledge_base.add_directory')}
</Button>
</TitleWrapper>
@@ -284,7 +250,7 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
<ContentSection>
<TitleWrapper>
<Title level={5}>{t('knowledge_base.urls')}</Title>
<Button icon={<PlusOutlined />} onClick={handleAddUrl} disabled={disabled}>
<Button icon={<PlusOutlined />} onClick={handleAddUrl}>
{t('knowledge_base.add_url')}
</Button>
</TitleWrapper>
@@ -311,7 +277,7 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
<ContentSection>
<TitleWrapper>
<Title level={5}>{t('knowledge_base.sitemaps')}</Title>
<Button icon={<PlusOutlined />} onClick={handleAddSitemap} disabled={disabled}>
<Button icon={<PlusOutlined />} onClick={handleAddSitemap}>
{t('knowledge_base.add_sitemap')}
</Button>
</TitleWrapper>
@@ -338,7 +304,7 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
<ContentSection>
<TitleWrapper>
<Title level={5}>{t('knowledge_base.notes')}</Title>
<Button icon={<PlusOutlined />} onClick={handleAddNote} disabled={disabled}>
<Button icon={<PlusOutlined />} onClick={handleAddNote}>
{t('knowledge_base.add_note')}
</Button>
</TitleWrapper>
@@ -366,15 +332,11 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
<label htmlFor="model-info">{t('knowledge_base.model_info')}</label>
<Tag color="blue">{base.model.name}</Tag>
<Tag color="cyan">{t('models.dimensions', { dimensions: base.dimensions || 0 })}</Tag>
{providerName && <Tag color="purple">{providerName}</Tag>}
<Tag color="purple">{base.model.provider}</Tag>
</ModelInfo>
<IndexSection>
<Button
type="primary"
onClick={() => KnowledgeSearchPopup.show({ base })}
icon={<SearchOutlined />}
disabled={disabled}>
<Button type="primary" onClick={() => KnowledgeSearchPopup.show({ base })} icon={<SearchOutlined />}>
{t('knowledge_base.search')}
</Button>
</IndexSection>

View File

@@ -6,7 +6,7 @@ import ImageSize3_4 from '@renderer/assets/images/paintings/image-size-3-4.svg'
import ImageSize9_16 from '@renderer/assets/images/paintings/image-size-9-16.svg'
import ImageSize16_9 from '@renderer/assets/images/paintings/image-size-16-9.svg'
import { Navbar, NavbarCenter, NavbarRight } from '@renderer/components/app/Navbar'
import { HStack, VStack } from '@renderer/components/Layout'
import { VStack } from '@renderer/components/Layout'
import Scrollbar from '@renderer/components/Scrollbar'
import TranslateButton from '@renderer/components/TranslateButton'
import { isMac } from '@renderer/config/constant'
@@ -25,7 +25,7 @@ import { DEFAULT_PAINTING } from '@renderer/store/paintings'
import { setGenerating } from '@renderer/store/runtime'
import { FileType, Painting } from '@renderer/types'
import { getErrorMessage } from '@renderer/utils'
import { Button, Input, InputNumber, Radio, Select, Slider, Switch, Tooltip } from 'antd'
import { Button, Input, InputNumber, Radio, Select, Slider, Tooltip } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import { FC, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
@@ -149,13 +149,8 @@ const PaintingsPage: FC = () => {
dispatch(setGenerating(true))
const AI = new AiProvider(provider)
if (!painting.model) {
return
}
try {
const urls = await AI.generateImage({
model: painting.model,
prompt,
negativePrompt: painting.negativePrompt || '',
imageSize: painting.imageSize || '1024x1024',
@@ -163,8 +158,7 @@ const PaintingsPage: FC = () => {
seed: painting.seed || undefined,
numInferenceSteps: painting.steps || 25,
guidanceScale: painting.guidanceScale || 4.5,
signal: controller.signal,
promptEnhancement: painting.promptEnhancement || false
signal: controller.signal
})
if (urls.length > 0) {
@@ -366,15 +360,13 @@ const PaintingsPage: FC = () => {
<InfoIcon />
</Tooltip>
</SettingTitle>
<SliderContainer>
<Slider min={1} max={50} value={painting.steps} onChange={(v) => updatePaintingState({ steps: v })} />
<StyledInputNumber
min={1}
max={50}
value={painting.steps}
onChange={(v) => updatePaintingState({ steps: (v as number) || 25 })}
/>
</SliderContainer>
<Slider min={1} max={50} value={painting.steps} onChange={(v) => updatePaintingState({ steps: v })} />
<InputNumber
min={1}
max={50}
value={painting.steps}
onChange={(v) => updatePaintingState({ steps: v || 25 })}
/>
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>
{t('paintings.guidance_scale')}
@@ -382,22 +374,21 @@ const PaintingsPage: FC = () => {
<InfoIcon />
</Tooltip>
</SettingTitle>
<SliderContainer>
<Slider
min={1}
max={20}
step={0.1}
value={painting.guidanceScale}
onChange={(v) => updatePaintingState({ guidanceScale: v })}
/>
<StyledInputNumber
min={1}
max={20}
step={0.1}
value={painting.guidanceScale}
onChange={(v) => updatePaintingState({ guidanceScale: (v as number) || 4.5 })}
/>
</SliderContainer>
<Slider
min={1}
max={20}
step={0.1}
value={painting.guidanceScale}
onChange={(v) => updatePaintingState({ guidanceScale: v })}
/>
<InputNumber
min={1}
max={20}
step={0.1}
value={painting.guidanceScale}
onChange={(v) => updatePaintingState({ guidanceScale: v || 4.5 })}
/>
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>
{t('paintings.negative_prompt')}
<Tooltip title={t('paintings.negative_prompt_tip')}>
@@ -409,18 +400,6 @@ const PaintingsPage: FC = () => {
onChange={(e) => updatePaintingState({ negativePrompt: e.target.value })}
rows={4}
/>
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>
{t('paintings.prompt_enhancement')}
<Tooltip title={t('paintings.prompt_enhancement_tip')}>
<InfoIcon />
</Tooltip>
</SettingTitle>
<HStack>
<Switch
checked={painting.promptEnhancement}
onChange={(checked) => updatePaintingState({ promptEnhancement: checked })}
/>
</HStack>
</LeftContainer>
<MainContainer>
<Artboard
@@ -568,18 +547,4 @@ const InfoIcon = styled(QuestionCircleOutlined)`
}
`
const SliderContainer = styled.div`
display: flex;
align-items: center;
gap: 16px;
.ant-slider {
flex: 1;
}
`
const StyledInputNumber = styled(InputNumber)`
width: 70px;
`
export default PaintingsPage

View File

@@ -4,9 +4,9 @@ import { HStack } from '@renderer/components/Layout'
import SelectModelPopup from '@renderer/components/Popups/SelectModelPopup'
import { DEFAULT_CONTEXTCOUNT, DEFAULT_TEMPERATURE } from '@renderer/config/constant'
import { SettingRow } from '@renderer/pages/settings'
import { Assistant, AssistantSettingCustomParameters, AssistantSettings } from '@renderer/types'
import { Assistant, AssistantSettings } from '@renderer/types'
import { Button, Col, Divider, Input, InputNumber, Row, Select, Slider, Switch, Tooltip } from 'antd'
import { FC, useEffect, useRef, useState } from 'react'
import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
@@ -25,14 +25,13 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
const [streamOutput, setStreamOutput] = useState(assistant?.settings?.streamOutput ?? true)
const [defaultModel, setDefaultModel] = useState(assistant?.defaultModel)
const [topP, setTopP] = useState(assistant?.settings?.topP ?? 1)
const [customParameters, setCustomParameters] = useState<AssistantSettingCustomParameters[]>(
assistant?.settings?.customParameters ?? []
)
const customParametersRef = useRef(customParameters)
customParametersRef.current = customParameters
const [customParameters, setCustomParameters] = useState<
Array<{
name: string
value: string | number | boolean
type: 'string' | 'number' | 'boolean'
}>
>(assistant?.settings?.customParameters ?? [])
const { t } = useTranslation()
const onTemperatureChange = (value) => {
@@ -69,7 +68,7 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
const onUpdateCustomParameter = (
index: number,
field: 'name' | 'value' | 'type',
value: string | number | boolean | object
value: string | number | boolean
) => {
const newParams = [...customParameters]
if (field === 'type') {
@@ -81,9 +80,6 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
case 'boolean':
defaultValue = false
break
case 'json':
defaultValue = ''
break
default:
defaultValue = ''
}
@@ -96,6 +92,7 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
newParams[index] = { ...newParams[index], [field]: value }
}
setCustomParameters(newParams)
updateAssistantSettings({ customParameters: newParams })
}
const renderParameterValueInput = (param: (typeof customParameters)[0], index: number) => {
@@ -116,20 +113,6 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
onChange={(checked) => onUpdateCustomParameter(index, 'value', checked)}
/>
)
case 'json':
return (
<Input
value={typeof param.value === 'string' ? param.value : JSON.stringify(param.value, null, 2)}
onChange={(e) => {
try {
const jsonValue = JSON.parse(e.target.value)
onUpdateCustomParameter(index, 'value', jsonValue)
} catch {
onUpdateCustomParameter(index, 'value', e.target.value)
}
}}
/>
)
default:
return (
<Input
@@ -176,10 +159,6 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
}
}
useEffect(() => {
return () => updateAssistantSettings({ customParameters: customParametersRef.current })
}, [])
return (
<Container>
<Row align="middle" style={{ marginBottom: 10 }}>
@@ -358,7 +337,7 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
</Button>
</SettingRow>
{customParameters.map((param, index) => (
<Row key={index} align="stretch" gutter={10} style={{ marginTop: 10 }}>
<Row key={index} align="middle" gutter={10} style={{ marginTop: 10 }}>
<Col span={6}>
<Input
placeholder={t('models.parameter_name')}
@@ -374,11 +353,10 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
<Select.Option value="string">{t('models.parameter_type.string')}</Select.Option>
<Select.Option value="number">{t('models.parameter_type.number')}</Select.Option>
<Select.Option value="boolean">{t('models.parameter_type.boolean')}</Select.Option>
<Select.Option value="json">{t('models.parameter_type.json')}</Select.Option>
</Select>
</Col>
<Col span={12}>{renderParameterValueInput(param, index)}</Col>
<Col span={2} style={{ display: 'flex', justifyContent: 'flex-end' }}>
<Col span={11}>{renderParameterValueInput(param, index)}</Col>
<Col span={3}>
<Button icon={<DeleteOutlined />} onClick={() => onDeleteCustomParameter(index)} danger />
</Col>
</Row>

View File

@@ -1,6 +1,5 @@
import { FolderOpenOutlined, SaveOutlined, SyncOutlined } from '@ant-design/icons'
import { FolderOpenOutlined, SaveOutlined } from '@ant-design/icons'
import { HStack } from '@renderer/components/Layout'
import { useRuntime } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings'
import { backupToWebdav, restoreFromWebdav, startAutoSync, stopAutoSync } from '@renderer/services/BackupService'
import { useAppDispatch } from '@renderer/store'
@@ -12,8 +11,7 @@ import {
setWebdavSyncInterval as _setWebdavSyncInterval,
setWebdavUser as _setWebdavUser
} from '@renderer/store/settings'
import { Button, Input, Select } from 'antd'
import dayjs from 'dayjs'
import { Button, Input, Select, Switch } from 'antd'
import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
@@ -25,6 +23,7 @@ const WebDavSettings: FC = () => {
webdavUser: webDAVUser,
webdavPass: webDAVPass,
webdavPath: webDAVPath,
webdavAutoSync: webDAVAutoSync,
webdavSyncInterval: webDAVSyncInterval
} = useSettings()
@@ -33,6 +32,7 @@ const WebDavSettings: FC = () => {
const [webdavPass, setWebdavPass] = useState<string | undefined>(webDAVPass)
const [webdavPath, setWebdavPath] = useState<string | undefined>(webDAVPath)
const [autoSync, setAutoSync] = useState<boolean>(webDAVAutoSync)
const [syncInterval, setSyncInterval] = useState<number>(webDAVSyncInterval)
const [backuping, setBackuping] = useState(false)
@@ -42,8 +42,6 @@ const WebDavSettings: FC = () => {
const { t } = useTranslation()
const { webdavSync } = useRuntime()
// 把之前备份的文件定时上传到 webdav首先先配置 webdav 的 host, port, user, pass, path
const onBackup = async () => {
@@ -66,40 +64,18 @@ const WebDavSettings: FC = () => {
setRestoring(false)
}
const onSyncIntervalChange = (value: number) => {
setSyncInterval(value)
dispatch(_setWebdavSyncInterval(value))
if (value === 0) {
dispatch(setWebdavAutoSync(false))
stopAutoSync()
} else {
dispatch(setWebdavAutoSync(true))
const onToggleAutoSync = (checked: boolean) => {
dispatch(setWebdavAutoSync(checked))
if (checked) {
startAutoSync()
} else {
stopAutoSync()
}
}
const renderSyncStatus = () => {
if (!webdavHost) return null
if (!webdavSync.lastSyncTime && !webdavSync.syncing && !webdavSync.lastSyncError) {
return <span style={{ color: 'var(--text-secondary)' }}>{t('settings.data.webdav.noSync')}</span>
}
return (
<HStack gap="5px" alignItems="center">
{webdavSync.syncing && <SyncOutlined spin />}
{webdavSync.lastSyncTime && (
<span style={{ color: 'var(--text-secondary)' }}>
{t('settings.data.webdav.lastSync')}: {dayjs(webdavSync.lastSyncTime).format('HH:mm:ss')}
</span>
)}
{webdavSync.lastSyncError && (
<span style={{ color: 'var(--error-color)' }}>
{t('settings.data.webdav.syncError')}: {webdavSync.lastSyncError}
</span>
)}
</HStack>
)
const onSyncIntervalChange = (value: number) => {
setSyncInterval(value)
dispatch(_setWebdavSyncInterval(value))
}
return (
@@ -151,6 +127,32 @@ const WebDavSettings: FC = () => {
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.data.webdav.autoSync')}</SettingRowTitle>
<HStack gap="10px" alignItems="center">
<Switch
checked={autoSync}
onChange={(checked) => {
setAutoSync(checked)
onToggleAutoSync(checked)
}}
disabled={!webdavHost}
/>
<Select
value={syncInterval || 5}
onChange={onSyncIntervalChange}
disabled={!webdavHost || !autoSync}
style={{ width: 120 }}>
<Select.Option value={1}>1 {t('settings.data.webdav.minutes')}</Select.Option>
<Select.Option value={5}>5 {t('settings.data.webdav.minutes')}</Select.Option>
<Select.Option value={15}>15 {t('settings.data.webdav.minutes')}</Select.Option>
<Select.Option value={30}>30 {t('settings.data.webdav.minutes')}</Select.Option>
<Select.Option value={60}>60 {t('settings.data.webdav.minutes')}</Select.Option>
<Select.Option value={120}>120 {t('settings.data.webdav.minutes')}</Select.Option>
</Select>
</HStack>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.general.backup.title')}</SettingRowTitle>
<HStack gap="5px" justifyContent="space-between">
@@ -163,28 +165,6 @@ const WebDavSettings: FC = () => {
</Button>
</HStack>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.data.webdav.autoSync')}</SettingRowTitle>
<Select value={syncInterval} onChange={onSyncIntervalChange} disabled={!webdavHost} style={{ width: 120 }}>
<Select.Option value={0}>{t('settings.data.webdav.autoSync.off')}</Select.Option>
<Select.Option value={1}>1 {t('settings.data.webdav.minutes')}</Select.Option>
<Select.Option value={5}>5 {t('settings.data.webdav.minutes')}</Select.Option>
<Select.Option value={15}>15 {t('settings.data.webdav.minutes')}</Select.Option>
<Select.Option value={30}>30 {t('settings.data.webdav.minutes')}</Select.Option>
<Select.Option value={60}>60 {t('settings.data.webdav.minutes')}</Select.Option>
<Select.Option value={120}>120 {t('settings.data.webdav.minutes')}</Select.Option>
</Select>
</SettingRow>
{webdavSync && syncInterval > 0 && (
<>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.data.webdav.syncStatus')}</SettingRowTitle>
{renderSyncStatus()}
</SettingRow>
</>
)}
</>
)
}

View File

@@ -41,39 +41,22 @@ const PopupContainer: React.FC<Props> = ({ title, provider, resolve }) => {
resolve({})
}
const onAddModel = (values: FieldType) => {
const id = values.id.trim()
if (find(models, { id })) {
window.message.error('Model ID already exists')
const onFinish: FormProps<FieldType>['onFinish'] = (values) => {
if (find(models, { id: values.id })) {
Modal.error({ title: 'Error', content: 'Model ID already exists' })
return
}
const model: Model = {
id,
provider: provider.id,
name: values.name ? values.name : id.toUpperCase(),
group: getDefaultGroupName(values.group || id)
id: values.id,
name: values.name ? values.name : values.id.toUpperCase(),
group: getDefaultGroupName(values.group || values.id)
}
addModel(model)
return true
}
const onFinish: FormProps<FieldType>['onFinish'] = (values) => {
const id = values.id.trim().replaceAll('', ',')
if (id.includes(',')) {
const ids = id.split(',')
ids.forEach((id) => onAddModel({ id, name: id } as FieldType))
resolve({})
return
}
if (onAddModel(values)) {
resolve({})
}
resolve(model)
}
return (

View File

@@ -1,6 +1,6 @@
import BaseProvider from '@renderer/providers/BaseProvider'
import ProviderFactory from '@renderer/providers/ProviderFactory'
import { Assistant, GenerateImageParams, Message, Model, Provider, Suggestion } from '@renderer/types'
import { Assistant, Message, Model, Provider, Suggestion } from '@renderer/types'
import OpenAI from 'openai'
import { CompletionsParams } from '.'
@@ -48,7 +48,16 @@ export default class AiProvider {
return this.sdk.getApiKey()
}
public async generateImage(params: GenerateImageParams): Promise<string[]> {
public async generateImage(params: {
prompt: string
negativePrompt: string
imageSize: string
batchSize: number
seed?: string
numInferenceSteps: number
guidanceScale: number
signal?: AbortSignal
}): Promise<string[]> {
return this.sdk.generateImage(params)
}

View File

@@ -2,8 +2,8 @@ import { REFERENCE_PROMPT } from '@renderer/config/prompts'
import { getOllamaKeepAliveTime } from '@renderer/hooks/useOllama'
import { getKnowledgeReferences } from '@renderer/services/KnowledgeService'
import store from '@renderer/store'
import { Assistant, GenerateImageParams, Message, Model, Provider, Suggestion } from '@renderer/types'
import { delay, isJSON } from '@renderer/utils'
import { Assistant, Message, Model, Provider, Suggestion } from '@renderer/types'
import { delay } from '@renderer/utils'
import OpenAI from 'openai'
import { CompletionsParams } from '.'
@@ -26,7 +26,16 @@ export default abstract class BaseProvider {
abstract generateText({ prompt, content }: { prompt: string; content: string }): Promise<string>
abstract check(): Promise<{ valid: boolean; error: Error | null }>
abstract models(): Promise<OpenAI.Models.Model[]>
abstract generateImage(params: GenerateImageParams): Promise<string[]>
abstract generateImage(_params: {
prompt: string
negativePrompt: string
imageSize: string
batchSize: number
seed?: string
numInferenceSteps: number
guidanceScale: number
signal?: AbortSignal
}): Promise<string[]>
abstract getEmbeddingDimensions(model: Model): Promise<number>
public getBaseURL(): string {
@@ -92,16 +101,13 @@ export default abstract class BaseProvider {
protected getCustomParameters(assistant: Assistant) {
return (
assistant?.settings?.customParameters?.reduce((acc, param) => {
if (!param.name?.trim()) {
return acc
}
if (param.type === 'json') {
const value = param.value as string
return { ...acc, [param.name]: isJSON(value) ? JSON.parse(value) : value }
}
return { ...acc, [param.name]: param.value }
}, {}) || {}
assistant?.settings?.customParameters?.reduce(
(acc, param) => ({
...acc,
[param.name]: param.value
}),
{}
) || {}
)
}
}

View File

@@ -1,17 +1,20 @@
import { isEmbeddingModel, isSupportedModel, isVisionModel, isWebSearchModel } from '@renderer/config/models'
import { isEmbeddingModel, isSupportedModel, isVisionModel } from '@renderer/config/models'
import { getStoreSetting } from '@renderer/hooks/useSettings'
import i18n from '@renderer/i18n'
import { getAssistantSettings, getDefaultModel, getTopNamingModel } from '@renderer/services/AssistantService'
import { EVENT_NAMES } from '@renderer/services/EventService'
import { filterContextMessages } from '@renderer/services/MessagesService'
import { Assistant, FileTypes, GenerateImageParams, Message, Model, Provider, Suggestion } from '@renderer/types'
import DuckDuckGoLiteSearch from '@renderer/tools/DuckDuckGoLiteSearch/function.json'
import DuckDuckGoLiteSearchCode from '@renderer/tools/DuckDuckGoLiteSearch/index.js?raw'
import { Assistant, FileTypes, Message, Model, Provider, Suggestion } from '@renderer/types'
import { removeQuotes } from '@renderer/utils'
import { last, takeRight } from 'lodash'
import OpenAI, { AzureOpenAI } from 'openai'
import {
ChatCompletionContentPart,
ChatCompletionCreateParamsNonStreaming,
ChatCompletionMessageParam
ChatCompletionMessageParam,
ChatCompletionTool
} from 'openai/resources'
import { CompletionsParams } from '.'
@@ -133,7 +136,6 @@ export default class OpenAIProvider extends BaseProvider {
}
const isOpenAIo1 = model.id.includes('o1-')
const isSupportStreamOutput = streamOutput
let time_first_token_millsec = 0
const start_time_millsec = new Date().getTime()
@@ -148,13 +150,28 @@ export default class OpenAIProvider extends BaseProvider {
top_p: assistant?.settings?.topP,
max_tokens: maxTokens,
keep_alive: this.keepAliveTime,
stream: isSupportStreamOutput,
...(isWebSearchModel(model) ? { enable_enhancement: true } : {}),
stream: streamOutput,
tools: [DuckDuckGoLiteSearch as ChatCompletionTool],
...this.getCustomParameters(assistant)
})
if (!isSupportStreamOutput) {
if (!streamOutput) {
const time_completion_millsec = new Date().getTime() - start_time_millsec
stream.choices[0].message?.tool_calls?.forEach(async (toolCall) => {
const functionName = toolCall.function.name
const params = toolCall.function.arguments
console.log(functionName, DuckDuckGoLiteSearchCode)
const result = await window.api.vm.run(`
var params = ${params};
${DuckDuckGoLiteSearchCode}
`)
console.log(result)
})
return onChunk({
text: stream.choices[0].message?.content || '',
usage: stream.usage,
@@ -199,8 +216,7 @@ export default class OpenAIProvider extends BaseProvider {
model: model.id,
messages: messages as ChatCompletionMessageParam[],
stream: false,
keep_alive: this.keepAliveTime,
temperature: assistant?.settings?.temperature
keep_alive: this.keepAliveTime
})
return response.choices[0].message?.content || ''
@@ -346,7 +362,6 @@ export default class OpenAIProvider extends BaseProvider {
}
public async generateImage({
model,
prompt,
negativePrompt,
imageSize,
@@ -354,23 +369,30 @@ export default class OpenAIProvider extends BaseProvider {
seed,
numInferenceSteps,
guidanceScale,
signal,
promptEnhancement
}: GenerateImageParams): Promise<string[]> {
signal
}: {
prompt: string
negativePrompt?: string
imageSize: string
batchSize: number
seed?: string
numInferenceSteps: number
guidanceScale: number
signal?: AbortSignal
}): Promise<string[]> {
const response = (await this.sdk.request({
method: 'post',
path: '/images/generations',
signal,
body: {
model,
model: 'stabilityai/stable-diffusion-3-5-large',
prompt,
negative_prompt: negativePrompt,
image_size: imageSize,
batch_size: batchSize,
seed: seed ? parseInt(seed) : undefined,
num_inference_steps: numInferenceSteps,
guidance_scale: guidanceScale,
prompt_enhancement: promptEnhancement
guidance_scale: guidanceScale
}
})) as { data: Array<{ url: string }> }

View File

@@ -23,11 +23,6 @@ export function getDefaultTranslateAssistant(targetLanguage: string, text: strin
const translateModel = getTranslateModel()
const assistant: Assistant = getDefaultAssistant()
assistant.model = translateModel
assistant.settings = {
temperature: 0.7
}
assistant.prompt = store
.getState()
.settings.translateModelPrompt.replace('{{target_language}}', targetLanguage)

View File

@@ -1,7 +1,6 @@
import db from '@renderer/databases'
import i18n from '@renderer/i18n'
import store from '@renderer/store'
import { setWebDAVSyncState } from '@renderer/store/runtime'
import dayjs from 'dayjs'
export async function backup() {
@@ -64,9 +63,6 @@ export async function backupToWebdav({ showMessage = true }: { showMessage?: boo
console.log('[Backup] Manual backup already in progress')
return
}
store.dispatch(setWebDAVSyncState({ syncing: true, lastSyncError: null }))
const { webdavHost, webdavUser, webdavPass, webdavPath } = store.getState().settings
const backupData = await getBackupData()
@@ -80,19 +76,11 @@ export async function backupToWebdav({ showMessage = true }: { showMessage?: boo
webdavPath
})
if (success) {
store.dispatch(
setWebDAVSyncState({
lastSyncTime: Date.now(),
lastSyncError: null
})
)
showMessage && window.message.success({ content: i18n.t('message.backup.success'), key: 'backup' })
} else {
store.dispatch(setWebDAVSyncState({ lastSyncError: 'Backup failed' }))
showMessage && window.message.error({ content: i18n.t('message.backup.failed'), key: 'backup' })
}
} catch (error: any) {
store.dispatch(setWebDAVSyncState({ lastSyncError: error.message }))
console.error('[backup] backupToWebdav: Error uploading file to WebDAV:', error)
showMessage &&
window.modal.error({
@@ -100,7 +88,6 @@ export async function backupToWebdav({ showMessage = true }: { showMessage?: boo
content: error.message
})
} finally {
store.dispatch(setWebDAVSyncState({ syncing: false }))
isManualBackupRunning = false
}
}
@@ -138,9 +125,9 @@ export function startAutoSync() {
return
}
const { webdavAutoSync, webdavHost } = store.getState().settings
const { webdavAutoSync, webdavHost, webdavSyncInterval } = store.getState().settings
if (!webdavAutoSync || !webdavHost) {
if (!webdavAutoSync || !webdavHost || webdavSyncInterval <= 0) {
console.log('[AutoSync] Invalid sync settings, auto sync disabled')
return
}
@@ -157,16 +144,7 @@ export function startAutoSync() {
syncTimeout = null
}
const { webdavSyncInterval } = store.getState().settings
if (webdavSyncInterval <= 0) {
console.log('[AutoSync] Invalid sync interval, auto sync disabled')
stopAutoSync()
return
}
syncTimeout = setTimeout(performAutoBackup, webdavSyncInterval * 60 * 1000)
console.log(`[AutoSync] Next sync scheduled in ${webdavSyncInterval} minutes`)
}

View File

@@ -57,29 +57,20 @@ class FileManager {
return file
}
static async deleteFile(id: string, force: boolean = false): Promise<void> {
static async deleteFile(id: string): Promise<void> {
const file = await this.getFile(id)
console.debug('[FileManager] Deleting file:', file)
if (!file) {
return
}
if (!force) {
if (file.count > 1) {
await db.files.update(id, { ...file, count: file.count - 1 })
return
}
if (file.count > 1) {
await db.files.update(id, { ...file, count: file.count - 1 })
return
}
await db.files.delete(id)
try {
await window.api.file.delete(id + file.ext)
} catch (error) {
console.error('[FileManager] Failed to delete file:', error)
}
await window.api.file.delete(id + file.ext)
}
static async deleteFiles(files: FileType[]): Promise<void> {
@@ -102,14 +93,6 @@ class FileManager {
const filesPath = store.getState().runtime.filesPath
return 'file://' + filesPath + '/' + file.name
}
static async updateFile(file: FileType) {
if (!file.origin_name.includes(file.ext)) {
file.origin_name = file.origin_name + file.ext
}
await db.files.update(file.id, file)
}
}
export default FileManager

View File

@@ -1,15 +0,0 @@
import i18n from '@renderer/i18n'
import store from '@renderer/store'
export function getProviderName(id: string) {
const provider = store.getState().llm.providers.find((p) => p.id === id)
if (!provider) {
return ''
}
if (provider.isSystem) {
return i18n.t(`provider.${provider.id}`, { defaultValue: provider.name })
}
return provider?.name
}

View File

@@ -10,12 +10,6 @@ export interface UpdateState {
available: boolean
}
export interface WebDAVSyncState {
lastSyncTime: number | null
syncing: boolean
lastSyncError: string | null
}
export interface RuntimeState {
avatar: string
generating: boolean
@@ -23,7 +17,6 @@ export interface RuntimeState {
searching: boolean
filesPath: string
update: UpdateState
webdavSync: WebDAVSyncState
}
const initialState: RuntimeState = {
@@ -38,11 +31,6 @@ const initialState: RuntimeState = {
downloading: false,
downloadProgress: 0,
available: false
},
webdavSync: {
lastSyncTime: null,
syncing: false,
lastSyncError: null
}
}
@@ -67,21 +55,11 @@ const runtimeSlice = createSlice({
},
setUpdateState: (state, action: PayloadAction<Partial<UpdateState>>) => {
state.update = { ...state.update, ...action.payload }
},
setWebDAVSyncState: (state, action: PayloadAction<Partial<WebDAVSyncState>>) => {
state.webdavSync = { ...state.webdavSync, ...action.payload }
}
}
})
export const {
setAvatar,
setGenerating,
setMinappShow,
setSearching,
setFilesPath,
setUpdateState,
setWebDAVSyncState
} = runtimeSlice.actions
export const { setAvatar, setGenerating, setMinappShow, setSearching, setFilesPath, setUpdateState } =
runtimeSlice.actions
export default runtimeSlice.reducer

View File

@@ -0,0 +1,22 @@
{
"type": "function",
"function": {
"name": "DuckDuckGoLiteSearch",
"description": "A search engine useful for answering questions about current events.",
"parameters": {
"type": "object",
"properties": {
"q": {
"type": "string",
"description": "Keywords for query"
},
"kl": {
"type": "string",
"description": "Language/region code (e.g., wt-wt, us-en, uk-en)",
"default": "wt-wt"
}
},
"required": ["q"]
}
}
}

View File

@@ -0,0 +1,23 @@
new Promise((resolve, reject) => {
async function makeRequest() {
try {
const response = await axios.request({
method: 'post',
maxBodyLength: Infinity,
url: 'https://google.serper.dev/search',
headers: {
'X-API-KEY': 'fa70255d0ab3402ee2ddb6455f6b317e73588fc7',
'Content-Type': 'application/json'
},
data: params
})
console.log(JSON.stringify(response.data))
resolve(response.data)
} catch (error) {
console.log(error)
reject(error.toString())
}
}
makeRequest()
})

View File

@@ -21,12 +21,6 @@ export type AssistantMessage = {
content: string
}
export type AssistantSettingCustomParameters = {
name: string
value: string | number | boolean | object
type: 'string' | 'number' | 'boolean' | 'json'
}
export type AssistantSettings = {
contextCount: number
temperature: number
@@ -36,7 +30,11 @@ export type AssistantSettings = {
streamOutput: boolean
hideMessages: boolean
autoResetModel: boolean
customParameters?: AssistantSettingCustomParameters[]
customParameters?: {
name: string
value: string | number | boolean
type: 'string' | 'number' | 'boolean'
}[]
}
export type Agent = Omit<Assistant, 'model'>
@@ -114,7 +112,6 @@ export type Suggestion = {
export interface Painting {
id: string
model?: string
urls: string[]
files: FileType[]
prompt?: string
@@ -124,7 +121,7 @@ export interface Painting {
seed?: string
steps?: number
guidanceScale?: number
promptEnhancement?: boolean
model?: string
}
export type MinAppType = {
@@ -227,16 +224,3 @@ export type KnowledgeBaseParams = {
apiVersion?: string
baseURL: string
}
export type GenerateImageParams = {
model: string
prompt: string
negativePrompt?: string
imageSize: string
batchSize: number
seed?: string
numInferenceSteps: number
guidanceScale: number
signal?: AbortSignal
promptEnhancement?: boolean
}

View File

@@ -1577,7 +1577,7 @@ __metadata:
languageName: node
linkType: hard
"@llm-tools/embedjs-loader-markdown@npm:0.1.25":
"@llm-tools/embedjs-loader-markdown@npm:^0.1.25":
version: 0.1.25
resolution: "@llm-tools/embedjs-loader-markdown@npm:0.1.25"
dependencies:
@@ -1592,21 +1592,6 @@ __metadata:
languageName: node
linkType: hard
"@llm-tools/embedjs-loader-markdown@patch:@llm-tools/embedjs-loader-markdown@npm%3A0.1.25#~/.yarn/patches/@llm-tools-embedjs-loader-markdown-npm-0.1.25-d1d536d640.patch":
version: 0.1.25
resolution: "@llm-tools/embedjs-loader-markdown@patch:@llm-tools/embedjs-loader-markdown@npm%3A0.1.25#~/.yarn/patches/@llm-tools-embedjs-loader-markdown-npm-0.1.25-d1d536d640.patch::version=0.1.25&hash=3a7d12"
dependencies:
"@llm-tools/embedjs-interfaces": "npm:0.1.25"
"@llm-tools/embedjs-loader-web": "npm:0.1.25"
debug: "npm:^4.4.0"
md5: "npm:^2.3.0"
micromark: "npm:^4.0.1"
micromark-extension-gfm: "npm:^3.0.0"
micromark-extension-mdx-jsx: "npm:^3.0.1"
checksum: 10c0/8e91a1260f8c94ec516be13a5105055bf140b5c63a85fa3c7641cc8f6799a0410ddb6bce61db858e91712f7dbc2b333269eb7c3ce813c1d95416f49f4f4f31a5
languageName: node
linkType: hard
"@llm-tools/embedjs-loader-msoffice@npm:^0.1.25":
version: 0.1.25
resolution: "@llm-tools/embedjs-loader-msoffice@npm:0.1.25"
@@ -2639,13 +2624,6 @@ __metadata:
languageName: node
linkType: hard
"@types/trusted-types@npm:^2.0.7":
version: 2.0.7
resolution: "@types/trusted-types@npm:2.0.7"
checksum: 10c0/4c4855f10de7c6c135e0d32ce462419d8abbbc33713b31d294596c0cc34ae1fa6112a2f9da729c8f7a20707782b0d69da3b1f8df6645b0366d08825ca1522e0c
languageName: node
linkType: hard
"@types/unist@npm:*, @types/unist@npm:^3.0.0":
version: 3.0.3
resolution: "@types/unist@npm:3.0.3"
@@ -2868,7 +2846,7 @@ __metadata:
"@llm-tools/embedjs": "patch:@llm-tools/embedjs@npm%3A0.1.25#~/.yarn/patches/@llm-tools-embedjs-npm-0.1.25-ec5645cf36.patch"
"@llm-tools/embedjs-libsql": "patch:@llm-tools/embedjs-libsql@npm%3A0.1.25#~/.yarn/patches/@llm-tools-embedjs-libsql-npm-0.1.25-fad000d74c.patch"
"@llm-tools/embedjs-loader-csv": "npm:^0.1.25"
"@llm-tools/embedjs-loader-markdown": "patch:@llm-tools/embedjs-loader-markdown@npm%3A0.1.25#~/.yarn/patches/@llm-tools-embedjs-loader-markdown-npm-0.1.25-d1d536d640.patch"
"@llm-tools/embedjs-loader-markdown": "npm:^0.1.25"
"@llm-tools/embedjs-loader-msoffice": "npm:^0.1.25"
"@llm-tools/embedjs-loader-pdf": "npm:^0.1.25"
"@llm-tools/embedjs-loader-sitemap": "npm:^0.1.25"
@@ -2889,13 +2867,12 @@ __metadata:
adm-zip: "npm:^0.5.16"
antd: "npm:^5.22.5"
apache-arrow: "npm:^18.1.0"
axios: "npm:^1.7.3"
axios: "npm:^1.7.9"
browser-image-compression: "npm:^2.0.2"
dayjs: "npm:^1.11.11"
dexie: "npm:^4.0.8"
dexie-react-hooks: "npm:^1.1.7"
docx: "npm:^9.0.2"
dompurify: "npm:^3.2.3"
dotenv-cli: "npm:^7.4.2"
electron: "npm:31.7.6"
electron-builder: "npm:^24.13.3"
@@ -3529,14 +3506,14 @@ __metadata:
languageName: node
linkType: hard
"axios@npm:^1.7.3":
version: 1.7.7
resolution: "axios@npm:1.7.7"
"axios@npm:^1.7.9":
version: 1.7.9
resolution: "axios@npm:1.7.9"
dependencies:
follow-redirects: "npm:^1.15.6"
form-data: "npm:^4.0.0"
proxy-from-env: "npm:^1.1.0"
checksum: 10c0/4499efc89e86b0b49ffddc018798de05fab26e3bf57913818266be73279a6418c3ce8f9e934c7d2d707ab8c095e837fc6c90608fb7715b94d357720b5f568af7
checksum: 10c0/b7a41e24b59fee5f0f26c1fc844b45b17442832eb3a0fb42dd4f1430eb4abc571fe168e67913e8a1d91c993232bd1d1ab03e20e4d1fee8c6147649b576fc1b0b
languageName: node
linkType: hard
@@ -4990,18 +4967,6 @@ __metadata:
languageName: node
linkType: hard
"dompurify@npm:^3.2.3":
version: 3.2.3
resolution: "dompurify@npm:3.2.3"
dependencies:
"@types/trusted-types": "npm:^2.0.7"
dependenciesMeta:
"@types/trusted-types":
optional: true
checksum: 10c0/0ce5cb89b76f396d800751bcb48e0d137792891d350ccc049f1bc9a5eca7332cc69030c25007ff4962e0824a5696904d4d74264df9277b5ad955642dfb6f313f
languageName: node
linkType: hard
"domutils@npm:^3.0.1":
version: 3.1.0
resolution: "domutils@npm:3.1.0"