Merge branch 'main' into translate-auto-detect

This commit is contained in:
自由的世界人
2025-05-27 21:03:07 +08:00
committed by GitHub
41 changed files with 1095 additions and 2897 deletions
@@ -0,0 +1,37 @@
diff --git a/dist/utils/tiktoken.js b/dist/utils/tiktoken.js
index 8e41ee6f00f2f9c7fa2c59fa2b2f4297634b97aa..6708abfaabee108b8967c3fbd7fccc8dfb95e693 100644
--- a/dist/utils/tiktoken.js
+++ b/dist/utils/tiktoken.js
@@ -3,18 +3,20 @@ import { AsyncCaller } from "./async_caller.js";
const cache = {};
const caller = /* #__PURE__ */ new AsyncCaller({});
export async function getEncoding(encoding) {
- if (!(encoding in cache)) {
- cache[encoding] = caller
- .fetch(`https://tiktoken.pages.dev/js/${encoding}.json`)
- .then((res) => res.json())
- .then((data) => new Tiktoken(data))
- .catch((e) => {
- delete cache[encoding];
- throw e;
- });
- }
- return await cache[encoding];
+ // if (!(encoding in cache)) {
+ // cache[encoding] = caller
+ // .fetch(`https://tiktoken.pages.dev/js/${encoding}.json`)
+ // .then((res) => res.json())
+ // .then((data) => new Tiktoken(data))
+ // .catch((e) => {
+ // delete cache[encoding];
+ // throw e;
+ // });
+ // }
+ // return await cache[encoding];
+ throw new Error("TikToken Not implemented");
}
export async function encodingForModel(model) {
- return getEncoding(getEncodingNameForModel(model));
+ // return getEncoding(getEncodingNameForModel(model));
+ throw new Error("TikToken Not implemented");
}
+4 -1
View File
@@ -36,8 +36,11 @@ files:
- '!**/*.{spec,test}.{js,jsx,ts,tsx}'
- '!**/*.min.*.map'
- '!**/*.d.ts'
- '!**/dist/es6/**'
- '!**/dist/demo/**'
- '!**/amd/**'
- '!**/{.DS_Store,Thumbs.db,thumbs.db,__pycache__}'
- '!**/{LICENSE,LICENSE.txt,LICENSE-MIT.txt,*.LICENSE.txt,NOTICE.txt,README.md,readme.md,CHANGELOG.md}'
- '!**/{LICENSE,license,LICENSE.*,*.LICENSE.txt,NOTICE.txt,README.md,readme.md,CHANGELOG.md}'
- '!node_modules/rollup-plugin-visualizer'
- '!node_modules/js-tiktoken'
- '!node_modules/@tavily/core/node_modules/js-tiktoken'
+1 -19
View File
@@ -9,25 +9,7 @@ const visualizerPlugin = (type: 'renderer' | 'main') => {
export default defineConfig({
main: {
plugins: [
externalizeDepsPlugin({
exclude: [
'@cherrystudio/embedjs',
'@cherrystudio/embedjs-openai',
'@cherrystudio/embedjs-loader-web',
'@cherrystudio/embedjs-loader-markdown',
'@cherrystudio/embedjs-loader-msoffice',
'@cherrystudio/embedjs-loader-xml',
'@cherrystudio/embedjs-loader-pdf',
'@cherrystudio/embedjs-loader-sitemap',
'@cherrystudio/embedjs-libsql',
'@cherrystudio/embedjs-loader-image',
'p-queue',
'webdav'
]
}),
...visualizerPlugin('main')
],
plugins: [externalizeDepsPlugin(), ...visualizerPlugin('main')],
resolve: {
alias: {
'@main': resolve('src/main'),
+3 -6
View File
@@ -38,7 +38,6 @@
"publish": "yarn build:check && yarn release patch push",
"pulish:artifacts": "cd packages/artifacts && npm publish && cd -",
"generate:agents": "yarn workspace @cherry-studio/database agents",
"generate:icons": "electron-icon-builder --input=./build/logo.png --output=build",
"analyze:renderer": "VISUALIZER_RENDERER=true yarn build",
"analyze:main": "VISUALIZER_MAIN=true yarn build",
"typecheck": "npm run typecheck:node && npm run typecheck:web",
@@ -112,7 +111,7 @@
"@emotion/is-prop-valid": "^1.3.1",
"@eslint-react/eslint-plugin": "^1.36.1",
"@eslint/js": "^9.22.0",
"@google/genai": "^0.13.0",
"@google/genai": "^1.0.1",
"@hello-pangea/dnd": "^16.6.0",
"@kangfenmao/keyv-storage": "^0.1.0",
"@modelcontextprotocol/sdk": "^1.11.4",
@@ -138,7 +137,6 @@
"@types/react-infinite-scroll-component": "^5.0.0",
"@types/react-window": "^1",
"@types/tinycolor2": "^1",
"@types/ws": "^8",
"@uiw/codemirror-extensions-langs": "^4.23.12",
"@uiw/codemirror-themes-all": "^4.23.12",
"@uiw/react-codemirror": "^4.23.12",
@@ -159,7 +157,6 @@
"electron": "35.4.0",
"electron-builder": "26.0.15",
"electron-devtools-installer": "^3.2.0",
"electron-icon-builder": "^2.0.1",
"electron-vite": "^3.1.0",
"emittery": "^1.0.3",
"emoji-picker-element": "^1.22.1",
@@ -220,13 +217,13 @@
"pdf-parse@npm:1.1.1": "patch:pdf-parse@npm%3A1.1.1#~/.yarn/patches/pdf-parse-npm-1.1.1-04a6109b2a.patch",
"@langchain/openai@npm:^0.3.16": "patch:@langchain/openai@npm%3A0.3.16#~/.yarn/patches/@langchain-openai-npm-0.3.16-e525b59526.patch",
"@langchain/openai@npm:>=0.1.0 <0.4.0": "patch:@langchain/openai@npm%3A0.3.16#~/.yarn/patches/@langchain-openai-npm-0.3.16-e525b59526.patch",
"node-gyp": "^9.1.0",
"libsql@npm:^0.4.4": "patch:libsql@npm%3A0.4.7#~/.yarn/patches/libsql-npm-0.4.7-444e260fb1.patch",
"openai@npm:^4.77.0": "patch:openai@npm%3A4.96.0#~/.yarn/patches/openai-npm-4.96.0-0665b05cb9.patch",
"pkce-challenge@npm:^4.1.0": "patch:pkce-challenge@npm%3A4.1.0#~/.yarn/patches/pkce-challenge-npm-4.1.0-fbc51695a3.patch",
"app-builder-lib@npm:26.0.13": "patch:app-builder-lib@npm%3A26.0.13#~/.yarn/patches/app-builder-lib-npm-26.0.13-a064c9e1d0.patch",
"openai@npm:^4.87.3": "patch:openai@npm%3A4.96.0#~/.yarn/patches/openai-npm-4.96.0-0665b05cb9.patch",
"app-builder-lib@npm:26.0.15": "patch:app-builder-lib@npm%3A26.0.15#~/.yarn/patches/app-builder-lib-npm-26.0.15-360e5b0476.patch"
"app-builder-lib@npm:26.0.15": "patch:app-builder-lib@npm%3A26.0.15#~/.yarn/patches/app-builder-lib-npm-26.0.15-360e5b0476.patch",
"@langchain/core@npm:^0.3.26": "patch:@langchain/core@npm%3A0.3.44#~/.yarn/patches/@langchain-core-npm-0.3.44-41d5c3cb0a.patch"
},
"packageManager": "yarn@4.9.1",
"lint-staged": {
+1
View File
@@ -111,6 +111,7 @@ export enum IpcChannel {
File_WriteWithId = 'file:writeWithId',
File_SaveImage = 'file:saveImage',
File_Base64Image = 'file:base64Image',
File_SaveBase64Image = 'file:saveBase64Image',
File_Download = 'file:download',
File_Copy = 'file:copy',
File_BinaryImage = 'file:binaryImage',
+1 -8
View File
@@ -18,7 +18,6 @@ import CopilotService from './services/CopilotService'
import { ExportService } from './services/ExportService'
import FileService from './services/FileService'
import FileStorage from './services/FileStorage'
import { GeminiService } from './services/GeminiService'
import KnowledgeService from './services/KnowledgeService'
import mcpService from './services/MCPService'
import NotificationService from './services/NotificationService'
@@ -250,6 +249,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
ipcMain.handle(IpcChannel.File_WriteWithId, fileManager.writeFileWithId)
ipcMain.handle(IpcChannel.File_SaveImage, fileManager.saveImage)
ipcMain.handle(IpcChannel.File_Base64Image, fileManager.base64Image)
ipcMain.handle(IpcChannel.File_SaveBase64Image, fileManager.saveBase64Image)
ipcMain.handle(IpcChannel.File_Base64File, fileManager.base64File)
ipcMain.handle(IpcChannel.File_Download, fileManager.downloadFile)
ipcMain.handle(IpcChannel.File_Copy, fileManager.copyFile)
@@ -298,13 +298,6 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
}
})
// gemini
ipcMain.handle(IpcChannel.Gemini_UploadFile, GeminiService.uploadFile)
ipcMain.handle(IpcChannel.Gemini_Base64File, GeminiService.base64File)
ipcMain.handle(IpcChannel.Gemini_RetrieveFile, GeminiService.retrieveFile)
ipcMain.handle(IpcChannel.Gemini_ListFiles, GeminiService.listFiles)
ipcMain.handle(IpcChannel.Gemini_DeleteFile, GeminiService.deleteFile)
// mini window
ipcMain.handle(IpcChannel.MiniWindow_Show, () => windowService.showMiniWindow())
ipcMain.handle(IpcChannel.MiniWindow_Hide, () => windowService.hideMiniWindow())
+45
View File
@@ -268,6 +268,51 @@ class FileStorage {
}
}
public saveBase64Image = async (_: Electron.IpcMainInvokeEvent, base64Data: string): Promise<FileType> => {
try {
if (!base64Data) {
throw new Error('Base64 data is required')
}
// 移除 base64 头部信息(如果存在)
const base64String = base64Data.replace(/^data:.*;base64,/, '')
const buffer = Buffer.from(base64String, 'base64')
const uuid = uuidv4()
const ext = '.png'
const destPath = path.join(this.storageDir, uuid + ext)
logger.info('[FileStorage] Saving base64 image:', {
storageDir: this.storageDir,
destPath,
bufferSize: buffer.length
})
// 确保目录存在
if (!fs.existsSync(this.storageDir)) {
fs.mkdirSync(this.storageDir, { recursive: true })
}
await fs.promises.writeFile(destPath, buffer)
const fileMetadata: FileType = {
id: uuid,
origin_name: uuid + ext,
name: uuid + ext,
path: destPath,
created_at: new Date().toISOString(),
size: buffer.length,
ext: ext.slice(1),
type: getFileType(ext),
count: 1
}
return fileMetadata
} catch (error) {
logger.error('[FileStorage] Failed to save base64 image:', error)
throw error
}
}
public base64File = async (_: Electron.IpcMainInvokeEvent, id: string): Promise<{ data: string; mime: string }> => {
const filePath = path.join(this.storageDir, id)
const buffer = await fs.promises.readFile(filePath)
-79
View File
@@ -1,79 +0,0 @@
import { File, FileState, GoogleGenAI, Pager } from '@google/genai'
import { FileType } from '@types'
import fs from 'fs'
import { CacheService } from './CacheService'
export class GeminiService {
private static readonly FILE_LIST_CACHE_KEY = 'gemini_file_list'
private static readonly CACHE_DURATION = 3000
static async uploadFile(
_: Electron.IpcMainInvokeEvent,
file: FileType,
{ apiKey, baseURL }: { apiKey: string; baseURL: string }
): Promise<File> {
const sdk = new GoogleGenAI({
vertexai: false,
apiKey,
httpOptions: {
baseUrl: baseURL
}
})
return await sdk.files.upload({
file: file.path,
config: {
mimeType: 'application/pdf',
name: file.id,
displayName: file.origin_name
}
})
}
static async base64File(_: Electron.IpcMainInvokeEvent, file: FileType) {
return {
data: Buffer.from(fs.readFileSync(file.path)).toString('base64'),
mimeType: 'application/pdf'
}
}
static async retrieveFile(_: Electron.IpcMainInvokeEvent, file: FileType, apiKey: string): Promise<File | undefined> {
const sdk = new GoogleGenAI({ vertexai: false, apiKey })
const cachedResponse = CacheService.get<any>(GeminiService.FILE_LIST_CACHE_KEY)
if (cachedResponse) {
return GeminiService.processResponse(cachedResponse, file)
}
const response = await sdk.files.list()
CacheService.set(GeminiService.FILE_LIST_CACHE_KEY, response, GeminiService.CACHE_DURATION)
return GeminiService.processResponse(response, file)
}
private static async processResponse(response: Pager<File>, file: FileType) {
for await (const f of response) {
if (f.state === FileState.ACTIVE) {
if (f.displayName === file.origin_name && Number(f.sizeBytes) === file.size) {
return f
}
}
}
return undefined
}
static async listFiles(_: Electron.IpcMainInvokeEvent, apiKey: string): Promise<File[]> {
const sdk = new GoogleGenAI({ vertexai: false, apiKey })
const files: File[] = []
for await (const f of await sdk.files.list()) {
files.push(f)
}
return files
}
static async deleteFile(_: Electron.IpcMainInvokeEvent, fileId: string, apiKey: string) {
const sdk = new GoogleGenAI({ vertexai: false, apiKey })
await sdk.files.delete({ name: fileId })
}
}
+1
View File
@@ -76,6 +76,7 @@ const api = {
selectFolder: () => ipcRenderer.invoke(IpcChannel.File_SelectFolder),
saveImage: (name: string, data: string) => ipcRenderer.invoke(IpcChannel.File_SaveImage, name, data),
base64Image: (fileId: string) => ipcRenderer.invoke(IpcChannel.File_Base64Image, fileId),
saveBase64Image: (data: string) => ipcRenderer.invoke(IpcChannel.File_SaveBase64Image, data),
download: (url: string, isUseContentType?: boolean) =>
ipcRenderer.invoke(IpcChannel.File_Download, url, isUseContentType),
copy: (fileId: string, destPath: string) => ipcRenderer.invoke(IpcChannel.File_Copy, fileId, destPath),
@@ -5,12 +5,12 @@ export const TOOL_SPECS: Record<string, CodeToolSpec> = {
copy: {
id: 'copy',
type: 'core',
order: 10
order: 11
},
download: {
id: 'download',
type: 'core',
order: 11
order: 10
},
edit: {
id: 'edit',
+2 -2
View File
@@ -137,7 +137,7 @@ const MainMenus: FC = () => {
const { hideMinappPopup } = useMinappPopup()
const { t } = useTranslation()
const { pathname } = useLocation()
const { sidebarIcons } = useSettings()
const { sidebarIcons, defaultPaintingProvider } = useSettings()
const { minappShow } = useRuntime()
const navigate = useNavigate()
const { theme } = useTheme()
@@ -158,7 +158,7 @@ const MainMenus: FC = () => {
const pathMap = {
assistants: '/',
agents: '/agents',
paintings: '/paintings',
paintings: `/paintings/${defaultPaintingProvider}`,
translate: '/translate',
minapp: '/apps',
knowledge: '/knowledge',
+1 -1
View File
@@ -6,13 +6,13 @@ import {
setAutoCheckUpdate as _setAutoCheckUpdate,
setLaunchOnBoot,
setLaunchToTray,
setPinTopicsToTop,
setSendMessageShortcut as _setSendMessageShortcut,
setSidebarIcons,
setTargetLanguage,
setTheme,
SettingsState,
setTopicPosition,
setPinTopicsToTop,
setTray as _setTray,
setTrayOnClose,
setWindowStyle
+25 -2
View File
@@ -820,12 +820,12 @@
"seed_desc_tip": "The same seed and prompt can generate similar images, setting -1 will generate different results each time",
"title": "Images",
"magic_prompt_option": "Magic Prompt",
"model": "Model Version",
"model": "Model",
"aspect_ratio": "Aspect Ratio",
"style_type": "Style",
"rendering_speed": "Rendering Speed",
"learn_more": "Learn More",
"paint_course":"tutorial",
"paint_course": "tutorial",
"prompt_placeholder_edit": "Enter your image description, text drawing uses \"double quotes\" to wrap",
"proxy_required": "Currently, you need to open a proxy to view the generated images, it will be supported in the future",
"image_file_required": "Please upload an image first",
@@ -846,6 +846,29 @@
"turbo": "Turbo",
"quality": "Quality"
},
"quality_options": {
"auto": "Auto",
"low": "Low",
"medium": "Medium",
"high": "High"
},
"moderation_options": {
"auto": "Auto",
"low": "Low"
},
"background_options": {
"auto": "Auto",
"transparent": "Transparent",
"opaque": "Opaque"
},
"aspect_ratios": {
"square": "Square",
"portrait": "Portrait",
"landscape": "Landscape"
},
"quality": "Quality",
"moderation": "Moderation",
"background": "Background",
"mode": {
"generate": "Draw",
"edit": "Edit",
+25 -2
View File
@@ -820,12 +820,12 @@
"seed_desc_tip": "同じシードとプロンプトで類似した画像を生成できますが、-1 に設定すると毎回異なる結果が生成されます",
"title": "画像",
"magic_prompt_option": "プロンプト強化",
"model": "モデルバージョン",
"model": "モデル",
"aspect_ratio": "画幅比例",
"style_type": "スタイル",
"learn_more": "詳しくはこちら",
"prompt_placeholder_edit": "画像の説明を入力します。テキスト描画には '二重引用符' を使用します",
"paint_course":"チュートリアル",
"paint_course": "チュートリアル",
"proxy_required": "現在、プロキシを開く必要があります。これは、将来サポートされる予定です",
"image_file_required": "画像を先にアップロードしてください",
"image_file_retry": "画像を先にアップロードしてください",
@@ -844,6 +844,29 @@
"turbo": "高速",
"quality": "高品質"
},
"quality_options": {
"auto": "自動",
"low": "低",
"medium": "中",
"high": "高"
},
"moderation_options": {
"auto": "自動",
"low": "低"
},
"background_options": {
"auto": "自動",
"transparent": "透明",
"opaque": "不透明"
},
"aspect_ratios": {
"square": "正方形",
"portrait": "縦図",
"landscape": "横図"
},
"quality": "品質",
"moderation": "敏感度",
"background": "背景",
"mode": {
"generate": "画像生成",
"edit": "部分編集",
+25 -3
View File
@@ -820,13 +820,13 @@
"seed_desc_tip": "Одинаковые сиды и промпты могут генерировать похожие изображения, установка -1 будет создавать разные результаты каждый раз",
"title": "Изображения",
"magic_prompt_option": "Улучшение промпта",
"model": "Версия",
"model": "Модель",
"aspect_ratio": "Пропорции изображения",
"style_type": "Стиль",
"rendering_speed": "Скорость рендеринга",
"learn_more": "Узнать больше",
"prompt_placeholder_edit": "Введите ваше описание изображения, текстовая отрисовка использует двойные кавычки для обертки",
"paint_course":"Руководство / Учебник",
"paint_course": "Руководство / Учебник",
"proxy_required": "Сейчас необходимо открыть прокси для просмотра сгенерированных изображений, в будущем будет поддерживаться прямое соединение",
"image_file_required": "Пожалуйста, сначала загрузите изображение",
"image_file_retry": "Пожалуйста, сначала загрузите изображение",
@@ -841,11 +841,34 @@
"3d": "3D",
"anime": "Аниме"
},
"quality_options": {
"auto": "Авто",
"low": "Низкое",
"medium": "Среднее",
"high": "Высокое"
},
"moderation_options": {
"auto": "Авто",
"low": "Низкое"
},
"background_options": {
"auto": "Авто",
"transparent": "Прозрачный",
"opaque": "Непрозрачный"
},
"rendering_speeds": {
"default": "По умолчанию",
"turbo": "Быстро",
"quality": "Качественно"
},
"aspect_ratios": {
"square": "Квадрат",
"portrait": "Портрет",
"landscape": "Пейзаж"
},
"quality": "Качество",
"moderation": "Сенсорность",
"background": "Фон",
"mode": {
"generate": "Рисование",
"edit": "Редактирование",
@@ -892,7 +915,6 @@
"seed_tip": "Контролирует случайный характер увеличения изображений для воспроизводимых результатов",
"magic_prompt_option_tip": "Улучшает увеличение изображений с помощью интеллектуального оптимизирования промптов"
},
"rendering_speed": "Скорость рендеринга",
"text_desc_required": "Пожалуйста, сначала введите описание изображения"
},
"prompts": {
+26 -3
View File
@@ -820,12 +820,12 @@
"seed_desc_tip": "相同的种子和提示词可以生成相似的图片,设置 -1 每次生成都不一样",
"title": "图片",
"magic_prompt_option": "提示词增强",
"model": "版本",
"model": "模型",
"aspect_ratio": "画幅比例",
"style_type": "风格",
"rendering_speed": "渲染速度",
"learn_more": "了解更多",
"paint_course":"教程",
"paint_course": "教程",
"prompt_placeholder_edit": "输入你的图片描述,文本绘制用 \"双引号\" 包裹",
"proxy_required": "目前需要打开代理才能查看生成图片,后续会支持国内直连",
"image_file_required": "请先上传图片",
@@ -846,11 +846,34 @@
"turbo": "快速",
"quality": "高质量"
},
"quality_options": {
"auto": "自动",
"low": "低",
"medium": "中",
"high": "高"
},
"moderation_options": {
"auto": "自动",
"low": "低"
},
"background_options": {
"auto": "自动",
"transparent": "透明",
"opaque": "不透明"
},
"aspect_ratios": {
"square": "方形",
"portrait": "竖图",
"landscape": "横图"
},
"quality": "质量",
"moderation": "敏感度",
"background": "背景",
"mode": {
"generate": "绘图",
"edit": "编辑",
"remix": "混合",
"upscale": "放大"
"upscale": "高清增强"
},
"generate": {
"model_tip": "模型版本:V3 为最新版本,V2 为之前版本,V2A 为快速模型、V_1 为初代模型,_TURBO 为加速版本",
+25 -2
View File
@@ -821,12 +821,12 @@
"seed_desc_tip": "相同的種子和提示詞可以生成相似的圖片,設置 -1 每次生成都不一樣",
"title": "繪圖",
"magic_prompt_option": "提示詞增強",
"model": "版本",
"model": "模型",
"aspect_ratio": "畫幅比例",
"style_type": "風格",
"learn_more": "了解更多",
"prompt_placeholder_edit": "輸入你的圖片描述,文本繪製用 '雙引號' 包裹",
"paint_course":"教程",
"paint_course": "教程",
"proxy_required": "目前需要打開代理才能查看生成圖片,後續會支持國內直連",
"image_file_required": "請先上傳圖片",
"image_file_retry": "請重新上傳圖片",
@@ -846,6 +846,29 @@
"turbo": "快速",
"quality": "高品質"
},
"quality_options": {
"auto": "自動",
"low": "低",
"medium": "中",
"high": "高"
},
"moderation_options": {
"auto": "自動",
"low": "低"
},
"background_options": {
"auto": "自動",
"transparent": "透明",
"opaque": "不透明"
},
"aspect_ratios": {
"square": "方形",
"portrait": "豎圖",
"landscape": "橫圖"
},
"quality": "品質",
"moderation": "敏感度",
"background": "背景",
"mode": {
"generate": "繪圖",
"edit": "編輯",
@@ -5,8 +5,6 @@ import { Col, Image, Row, Spin, Table } from 'antd'
import React, { memo } from 'react'
import styled from 'styled-components'
import GeminiFiles from './GeminiFiles'
interface ContentViewProps {
id: FileTypes | 'all' | string
files?: FileType[]
@@ -45,10 +43,6 @@ const ContentView: React.FC<ContentViewProps> = ({ id, files, dataSource, column
)
}
if (id.startsWith('gemini_')) {
return <GeminiFiles id={id.replace('gemini_', '') as string} />
}
return (
<Table
dataSource={dataSource}
@@ -8,7 +8,6 @@ import React, { memo } from 'react'
import styled from 'styled-components'
import FileItem from './FileItem'
import GeminiFiles from './GeminiFiles'
interface FileItemProps {
id: FileTypes | 'all' | string
@@ -58,10 +57,6 @@ const FileList: React.FC<FileItemProps> = ({ id, list, files }) => {
)
}
if (id.startsWith('gemini_')) {
return <GeminiFiles id={id.replace('gemini_', '') as string} />
}
return (
<VirtualList
data={list}
@@ -1,102 +0,0 @@
import { DeleteOutlined } from '@ant-design/icons'
import type { File } from '@google/genai'
import { useProvider } from '@renderer/hooks/useProvider'
import { runAsyncFunction } from '@renderer/utils'
import { MB } from '@shared/config/constant'
import { Spin } from 'antd'
import dayjs from 'dayjs'
import { FC, useCallback, useEffect, useState } from 'react'
import styled from 'styled-components'
import FileItem from './FileItem'
interface GeminiFilesProps {
id: string
}
const GeminiFiles: FC<GeminiFilesProps> = ({ id }) => {
const { provider } = useProvider(id)
const [files, setFiles] = useState<File[]>([])
const [loading, setLoading] = useState(false)
const fetchFiles = useCallback(async () => {
const files = await window.api.gemini.listFiles(provider.apiKey)
files && setFiles(files.filter((file) => file.state === 'ACTIVE'))
}, [provider])
useEffect(() => {
runAsyncFunction(async () => {
try {
setLoading(true)
await fetchFiles()
setLoading(false)
} catch (error: any) {
console.error('Failed to fetch files:', error)
window.message.error(error.message)
setLoading(false)
}
})
}, [fetchFiles])
useEffect(() => {
setFiles([])
}, [id])
if (loading) {
return (
<Container>
<LoadingWrapper>
<Spin />
</LoadingWrapper>
</Container>
)
}
return (
<Container>
<FileListContainer>
{files.map((file) => (
<FileItem
key={file.name}
fileInfo={{
name: file.displayName,
ext: `.${file.name?.split('.').pop()}`,
extra: `${dayjs(file.createTime).format('MM-DD HH:mm')} · ${(parseInt(file.sizeBytes || '0') / MB).toFixed(2)} MB`,
actions: (
<DeleteOutlined
style={{ cursor: 'pointer', color: 'var(--color-error)' }}
onClick={() => {
setFiles(files.filter((f) => f.name !== file.name))
window.api.gemini.deleteFile(file.name!, provider.apiKey).catch((error) => {
console.error('Failed to delete file:', error)
setFiles((prev) => [...prev, file])
})
}}
/>
)
}}
/>
))}
</FileListContainer>
</Container>
)
}
const Container = styled.div`
width: 100%;
`
const FileListContainer = styled.div`
display: flex;
flex-direction: column;
gap: 8px;
`
const LoadingWrapper = styled.div`
display: flex;
justify-content: center;
align-items: center;
min-height: 200px;
`
export default GeminiFiles
@@ -163,7 +163,7 @@ const MainTextBlock: React.FC<Props> = ({ block, citationBlockId, role, mentions
</Flex>
)}
{role === 'user' && !renderInputMessageAsMarkdown ? (
<p className="markdown" style={{ marginBottom: 5 }}>
<p className="markdown" style={{ marginBottom: 5, whiteSpace: 'pre-line' }}>
{block.content}
</p>
) : (
+153 -337
View File
@@ -22,17 +22,16 @@ import { Avatar, Button, Input, InputNumber, Radio, Segmented, Select, Slider, S
import TextArea from 'antd/es/input/TextArea'
import { Info } from 'lucide-react'
import type { FC } from 'react'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useLocation, useNavigate } from 'react-router-dom'
import styled from 'styled-components'
import SendMessageButton from '../home/Inputbar/SendMessageButton'
import { SettingHelpLink, SettingTitle } from '../settings'
import Artboard from './Artboard'
import { type ConfigItem, createModeConfigs } from './config/aihubmixConfig'
import { DEFAULT_PAINTING } from './config/constants'
import PaintingsList from './PaintingsList'
import Artboard from './components/Artboard'
import PaintingsList from './components/PaintingsList'
import { type ConfigItem, createModeConfigs, DEFAULT_PAINTING } from './config/aihubmixConfig'
// 使用函数创建配置项
const modeConfigs = createModeConfigs()
@@ -74,12 +73,13 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
{ label: t('paintings.mode.upscale'), value: 'upscale' }
]
const getNewPainting = () => {
const getNewPainting = useCallback(() => {
return {
...DEFAULT_PAINTING,
model: mode === 'generate' ? 'gpt-image-1' : 'V_3',
id: uuid()
}
}
}, [mode])
const textareaRef = useRef<any>(null)
@@ -89,6 +89,47 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
updatePainting(mode, updatedPainting)
}
const handleError = (error: unknown) => {
if (error instanceof Error && error.name !== 'AbortError') {
window.modal.error({
content: getErrorMessage(error),
centered: true
})
}
}
const downloadImages = async (urls: string[]) => {
const downloadedFiles = await Promise.all(
urls.map(async (url) => {
try {
if (!url?.trim()) {
console.error('图像URL为空,可能是提示词违禁')
window.message.warning({
content: t('message.empty_url'),
key: 'empty-url-warning'
})
return null
}
return await window.api.file.download(url)
} catch (error) {
console.error('下载图像失败:', error)
if (
error instanceof Error &&
(error.message.includes('Failed to parse URL') || error.message.includes('Invalid URL'))
) {
window.message.warning({
content: t('message.empty_url'),
key: 'empty-url-warning'
})
}
return null
}
})
)
return downloadedFiles.filter((file): file is FileType => file !== null)
}
const onGenerate = async () => {
if (painting.files.length > 0) {
const confirmed = await window.modal.confirm({
@@ -129,11 +170,11 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
dispatch(setGenerating(true))
let body: string | FormData = ''
const headers: Record<string, string> = {
let headers: Record<string, string> = {
'Api-Key': aihubmixProvider.apiKey
}
let url = aihubmixProvider.apiHost + `/ideogram/` + mode
// 不使用 AiProvider 的通用规则,而是直接调用自定义接口
try {
if (mode === 'generate') {
if (painting.model === 'V_3') {
@@ -214,67 +255,47 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
console.log('V3 API响应:', data)
const urls = data.data.map((item) => item.url)
// Rest of the code for handling image downloads is the same
if (urls.length > 0) {
const downloadedFiles = await Promise.all(
urls.map(async (url) => {
try {
// 检查URL是否为空
if (!url || url.trim() === '') {
console.error('图像URL为空,可能是提示词违禁')
window.message.warning({
content: t('message.empty_url'),
key: 'empty-url-warning'
})
return null
}
return await window.api.file.download(url)
} catch (error) {
console.error('下载图像失败:', error)
// 检查是否是URL解析错误
if (
error instanceof Error &&
(error.message.includes('Failed to parse URL') || error.message.includes('Invalid URL'))
) {
window.message.warning({
content: t('message.empty_url'),
key: 'empty-url-warning'
})
}
return null
}
})
)
const validFiles = downloadedFiles.filter((file): file is FileType => file !== null)
const validFiles = await downloadImages(urls)
await FileManager.addFiles(validFiles)
updatePaintingState({ files: validFiles, urls })
}
return
} catch (error: unknown) {
if (error instanceof Error && error.name !== 'AbortError') {
window.modal.error({
content: getErrorMessage(error),
centered: true
})
}
handleError(error)
} finally {
setIsLoading(false)
dispatch(setGenerating(false))
setAbortController(null)
}
} else {
// Existing V1/V2 API
const requestData = {
image_request: {
let requestData: any = {}
if (painting.model === 'gpt-image-1') {
requestData = {
prompt,
model: painting.model,
aspect_ratio: painting.aspectRatio,
num_images: painting.numImages,
style_type: painting.styleType,
seed: painting.seed ? +painting.seed : undefined,
negative_prompt: painting.negativePrompt || undefined,
magic_prompt_option: painting.magicPromptOption ? 'ON' : 'OFF'
size: painting.size === 'auto' ? undefined : painting.size,
n: painting.n,
quality: painting.quality,
moderation: painting.moderation
}
url = aihubmixProvider.apiHost + `/v1/images/generations`
headers = {
Authorization: `Bearer ${aihubmixProvider.apiKey}`
}
} else {
// Existing V1/V2 API
requestData = {
image_request: {
prompt,
model: painting.model,
aspect_ratio: painting.aspectRatio,
num_images: painting.numImages,
style_type: painting.styleType,
seed: painting.seed ? +painting.seed : undefined,
negative_prompt: painting.negativePrompt || undefined,
magic_prompt_option: painting.magicPromptOption ? 'ON' : 'OFF'
}
}
}
body = JSON.stringify(requestData)
@@ -352,37 +373,7 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
// Handle the downloaded images
if (urls.length > 0) {
const downloadedFiles = await Promise.all(
urls.map(async (url) => {
try {
// 检查URL是否为空
if (!url || url.trim() === '') {
console.error('图像URL为空,可能是提示词违禁')
window.message.warning({
content: t('message.empty_url'),
key: 'empty-url-warning'
})
return null
}
return await window.api.file.download(url)
} catch (error) {
console.error('下载图像失败:', error)
// 检查是否是URL解析错误
if (
error instanceof Error &&
(error.message.includes('Failed to parse URL') || error.message.includes('Invalid URL'))
) {
window.message.warning({
content: t('message.empty_url'),
key: 'empty-url-warning'
})
}
return null
}
})
)
const validFiles = downloadedFiles.filter((file): file is FileType => file !== null)
const validFiles = await downloadImages(urls)
await FileManager.addFiles(validFiles)
updatePaintingState({ files: validFiles, urls })
}
@@ -405,119 +396,6 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
form.append('image_file', fileMap[painting.imageFile] as unknown as Blob)
body = form
}
} else if (mode === 'edit') {
if (!painting.imageFile) {
window.modal.error({
content: t('paintings.image_file_required'),
centered: true
})
return
}
if (!fileMap[painting.imageFile]) {
window.modal.error({
content: t('paintings.image_file_retry'),
centered: true
})
return
}
if (painting.model === 'V_3') {
// V3 Edit API
const formData = new FormData()
formData.append('prompt', prompt)
formData.append('rendering_speed', painting.renderingSpeed || 'DEFAULT')
formData.append('num_images', String(painting.numImages || 1))
if (painting.styleType) {
formData.append('style_type', painting.styleType)
}
if (painting.seed) {
formData.append('seed', painting.seed)
}
if (painting.magicPromptOption !== undefined) {
formData.append('magic_prompt', painting.magicPromptOption ? 'ON' : 'OFF')
}
// Add the image file
formData.append('image', fileMap[painting.imageFile] as unknown as Blob)
// Add the mask if available
if (painting.mask) {
formData.append('mask', painting.mask as unknown as Blob)
}
body = formData
// For V3 Edit endpoint
const response = await fetch(`${aihubmixProvider.apiHost}/ideogram/v1/ideogram-v3/edit`, {
method: 'POST',
headers: { 'Api-Key': aihubmixProvider.apiKey },
body
})
if (!response.ok) {
const errorData = await response.json()
console.error('V3 Edit API错误:', errorData)
throw new Error(errorData.error?.message || '图像编辑失败')
}
const data = await response.json()
console.log('V3 Edit API响应:', data)
const urls = data.data.map((item) => item.url)
// Handle the downloaded images
if (urls.length > 0) {
const downloadedFiles = await Promise.all(
urls.map(async (url) => {
try {
// 检查URL是否为空
if (!url || url.trim() === '') {
console.error('图像URL为空,可能是提示词违禁')
window.message.warning({
content: t('message.empty_url'),
key: 'empty-url-warning'
})
return null
}
return await window.api.file.download(url)
} catch (error) {
console.error('下载图像失败:', error)
// 检查是否是URL解析错误
if (
error instanceof Error &&
(error.message.includes('Failed to parse URL') || error.message.includes('Invalid URL'))
) {
window.message.warning({
content: t('message.empty_url'),
key: 'empty-url-warning'
})
}
return null
}
})
)
const validFiles = downloadedFiles.filter((file): file is FileType => file !== null)
await FileManager.addFiles(validFiles)
updatePaintingState({ files: validFiles, urls })
}
return
} else {
// Existing V1/V2 API for edit
const form = new FormData()
const imageRequest: Record<string, any> = {
prompt,
model: painting.model,
style_type: painting.styleType,
num_images: painting.numImages,
seed: painting.seed ? +painting.seed : undefined,
magic_prompt_option: painting.magicPromptOption ? 'ON' : 'OFF'
}
form.append('image_request', JSON.stringify(imageRequest))
form.append('image_file', fileMap[painting.imageFile] as unknown as Blob)
body = form
}
} else if (mode === 'upscale') {
if (!painting.imageFile) {
window.modal.error({
@@ -549,9 +427,9 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
}
// 只针对非V3模型使用通用接口
if (!painting.model?.includes('V_3')) {
if (!painting.model?.includes('V_3') || mode === 'upscale') {
// 直接调用自定义接口
const response = await fetch(`${aihubmixProvider.apiHost}/ideogram/${mode}`, { method: 'POST', headers, body })
const response = await fetch(url, { method: 'POST', headers, body })
if (!response.ok) {
const errorData = await response.json()
@@ -561,53 +439,27 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
const data = await response.json()
console.log('通用API响应:', data)
const urls = data.data.map((item) => item.url)
const urls = data.data.filter((item) => item.url).map((item) => item.url)
const base64s = data.data.filter((item) => item.b64_json).map((item) => item.b64_json)
if (urls.length > 0) {
const downloadedFiles = await Promise.all(
urls.map(async (url) => {
try {
// 检查URL是否为空
if (!url || url.trim() === '') {
console.error('图像URL为空,可能是提示词违禁')
window.message.warning({
content: t('message.empty_url'),
key: 'empty-url-warning'
})
return null
}
return await window.api.file.download(url)
} catch (error) {
console.error('下载图像失败:', error)
// 检查是否是URL解析错误
if (
error instanceof Error &&
(error.message.includes('Failed to parse URL') || error.message.includes('Invalid URL'))
) {
window.message.warning({
content: t('message.empty_url'),
key: 'empty-url-warning'
})
}
return null
}
const validFiles = await downloadImages(urls)
await FileManager.addFiles(validFiles)
updatePaintingState({ files: validFiles, urls })
}
if (base64s?.length > 0) {
const validFiles = await Promise.all(
base64s.map(async (base64) => {
return await window.api.file.saveBase64Image(base64)
})
)
const validFiles = downloadedFiles.filter((file): file is FileType => file !== null)
await FileManager.addFiles(validFiles)
updatePaintingState({ files: validFiles, urls })
updatePaintingState({ files: validFiles, urls: validFiles.map((file) => file.name) })
}
}
} catch (error: unknown) {
if (error instanceof Error && error.name !== 'AbortError') {
window.modal.error({
content: getErrorMessage(error),
centered: true
})
}
handleError(error)
} finally {
setIsLoading(false)
dispatch(setGenerating(false))
@@ -617,43 +469,15 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
const handleRetry = async (painting: PaintingAction) => {
setIsLoading(true)
const downloadedFiles = await Promise.all(
painting.urls.map(async (url) => {
try {
// 检查URL是否为空
if (!url || url.trim() === '') {
console.error('图像URL为空,可能是提示词违禁')
window.message.warning({
content: t('message.empty_url'),
key: 'empty-url-warning'
})
return null
}
return await window.api.file.download(url)
} catch (error) {
console.error('下载图像失败:', error)
// 检查是否是URL解析错误
if (
error instanceof Error &&
(error.message.includes('Failed to parse URL') || error.message.includes('Invalid URL'))
) {
window.message.warning({
content: t('message.empty_url'),
key: 'empty-url-warning'
})
}
setIsLoading(false)
return null
}
})
)
const validFiles = downloadedFiles.filter((file): file is FileType => file !== null)
await FileManager.addFiles(validFiles)
updatePaintingState({ files: validFiles, urls: painting.urls })
setIsLoading(false)
try {
const validFiles = await downloadImages(painting.urls)
await FileManager.addFiles(validFiles)
updatePaintingState({ files: validFiles, urls: painting.urls })
} catch (error) {
handleError(error)
} finally {
setIsLoading(false)
}
}
const onCancel = () => {
@@ -754,20 +578,8 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
}
// 渲染配置项的函数
const renderConfigItem = (item: ConfigItem, index: number) => {
const renderConfigForm = (item: ConfigItem) => {
switch (item.type) {
case 'title': {
return (
<SettingTitle key={index} style={{ marginBottom: 5, marginTop: 15 }}>
{t(item.title!)}
{item.tooltip && (
<Tooltip title={t(item.tooltip)}>
<InfoIcon />
</Tooltip>
)}
</SettingTitle>
)
}
case 'select': {
// 处理函数类型的disabled属性
const isDisabled = typeof item.disabled === 'function' ? item.disabled(item, painting) : item.disabled
@@ -786,10 +598,11 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
return (
<Select
key={index}
style={{ width: '100%' }}
listHeight={500}
disabled={isDisabled}
value={painting[item.key!] || item.initialValue}
options={selectOptions}
options={selectOptions as any}
onChange={(v) => updatePaintingState({ [item.key!]: v })}
/>
)
@@ -809,8 +622,7 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
return (
<Radio.Group
key={index}
value={painting[item.key!]}
value={painting[item.key!] || item.initialValue}
onChange={(e) => updatePaintingState({ [item.key!]: e.target.value })}>
{radioOptions!.map((option) => (
<Radio.Button key={option.value} value={option.value}>
@@ -822,96 +634,82 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
}
case 'slider': {
return (
<SliderContainer key={index}>
<SliderContainer>
<Slider
min={item.min}
max={item.max}
step={item.step}
value={painting[item.key!] as number}
value={(painting[item.key!] || item.initialValue) as number}
onChange={(v) => updatePaintingState({ [item.key!]: v })}
/>
<StyledInputNumber
min={item.min}
max={item.max}
step={item.step}
value={painting[item.key!] as number}
value={(painting[item.key!] || item.initialValue) as number}
onChange={(v) => updatePaintingState({ [item.key!]: v })}
/>
</SliderContainer>
)
}
case 'input': {
// 处理随机种子按钮的特殊情况
if (item.key === 'seed') {
return (
<Input
key={index}
value={painting[item.key] as string}
onChange={(e) => updatePaintingState({ [item.key!]: e.target.value })}
suffix={
<RedoOutlined onClick={handleRandomSeed} style={{ cursor: 'pointer', color: 'var(--color-text-2)' }} />
}
/>
)
}
case 'input':
return (
<Input
key={index}
value={painting[item.key!] as string}
value={(painting[item.key!] || item.initialValue) as string}
onChange={(e) => updatePaintingState({ [item.key!]: e.target.value })}
suffix={item.suffix}
suffix={
item.key === 'seed' ? (
<RedoOutlined onClick={handleRandomSeed} style={{ cursor: 'pointer', color: 'var(--color-text-2)' }} />
) : (
item.suffix
)
}
/>
)
}
case 'inputNumber': {
case 'inputNumber':
return (
<InputNumber
key={index}
min={item.min}
max={item.max}
style={{ width: '100%' }}
value={painting[item.key!] as number}
value={(painting[item.key!] || item.initialValue) as number}
onChange={(v) => updatePaintingState({ [item.key!]: v })}
/>
)
}
case 'textarea': {
case 'textarea':
return (
<TextArea
key={index}
value={painting[item.key!] as string}
value={(painting[item.key!] || item.initialValue) as string}
onChange={(e) => updatePaintingState({ [item.key!]: e.target.value })}
spellCheck={false}
rows={4}
/>
)
}
case 'switch': {
case 'switch':
return (
<HStack key={index}>
<HStack>
<Switch
checked={painting[item.key!] as boolean}
checked={(painting[item.key!] || item.initialValue) as boolean}
onChange={(checked) => updatePaintingState({ [item.key!]: checked })}
/>
</HStack>
)
}
case 'image': {
return (
<ImageUploadButton
key={index}
accept="image/png, image/jpeg, image/gif"
maxCount={1}
showUploadList={false}
listType="picture-card"
onChange={async ({ file }) => {
const path = file.originFileObj?.path || ''
setFileMap({ ...fileMap, [path]: file.originFileObj as unknown as FileType })
beforeUpload={(file) => {
const path = URL.createObjectURL(file)
setFileMap({ ...fileMap, [path]: file as unknown as FileType })
updatePaintingState({ [item.key!]: path })
return false // 阻止默认上传行为
}}>
{painting[item.key!] ? (
<ImagePreview>
<img src={'file://' + painting[item.key!]} alt="预览图" />
<img src={painting[item.key!]} alt="预览图" />
</ImagePreview>
) : (
<ImageSizeImage src={IcImageUp} theme={theme} />
@@ -924,6 +722,23 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
}
}
// 渲染配置项的函数
const renderConfigItem = (item: ConfigItem, index: number) => {
return (
<div key={index}>
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>
{t(item.title!)}
{item.tooltip && (
<Tooltip title={t(item.tooltip)}>
<InfoIcon />
</Tooltip>
)}
</SettingTitle>
{renderConfigForm(item)}
</div>
)
}
const onSelectPainting = (newPainting: PaintingAction) => {
if (generating) return
setPainting(newPainting)
@@ -936,12 +751,13 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
addPainting(mode, newPainting)
setPainting(newPainting)
}
}, [filteredPaintings, mode, addPainting, painting])
}, [filteredPaintings, mode, addPainting, painting, getNewPainting])
useEffect(() => {
const timer = spaceClickTimer.current
return () => {
if (spaceClickTimer.current) {
clearTimeout(spaceClickTimer.current)
if (timer) {
clearTimeout(timer)
}
}
}, [])
@@ -985,7 +801,7 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
</Select>
{/* 使用JSON配置渲染设置项 */}
{modeConfigs[mode].map(renderConfigItem)}
{modeConfigs[mode].filter((item) => (item.condition ? item.condition(painting) : true)).map(renderConfigItem)}
</LeftContainer>
<MainContainer>
{/* 添加功能切换分段控制器 */}
@@ -26,7 +26,8 @@ import styled from 'styled-components'
import SendMessageButton from '../home/Inputbar/SendMessageButton'
import { SettingHelpLink, SettingTitle } from '../settings'
import Artboard from './Artboard'
import Artboard from './components/Artboard'
import PaintingsList from './components/PaintingsList'
import {
COURSE_URL,
DEFAULT_PAINTING,
@@ -34,7 +35,6 @@ import {
STYLE_TYPE_OPTIONS,
TEXT_TO_IMAGES_MODELS
} from './config/DmxapiConfig'
import PaintingsList from './PaintingsList'
const generateRandomSeed = () => Math.floor(Math.random() * 1000000).toString()
@@ -1,16 +1,30 @@
import { FC } from 'react'
import { Route, Routes } from 'react-router-dom'
import { useAppDispatch } from '@renderer/store'
import { setDefaultPaintingProvider } from '@renderer/store/settings'
import { PaintingProvider } from '@renderer/types'
import { FC, useEffect } from 'react'
import { Route, Routes, useParams } from 'react-router-dom'
import AihubmixPage from './AihubmixPage'
import DmxapiPage from './DmxapiPage'
import SiliconPage from './PaintingsPage'
import SiliconPage from './SiliconPage'
const Options = ['aihubmix', 'silicon', 'dmxapi']
const PaintingsRoutePage: FC = () => {
const params = useParams()
const provider = params['*']
const dispatch = useAppDispatch()
useEffect(() => {
console.debug('defaultPaintingProvider', provider)
if (provider && Options.includes(provider)) {
dispatch(setDefaultPaintingProvider(provider as PaintingProvider))
}
}, [provider, dispatch])
return (
<Routes>
<Route path="/" element={<AihubmixPage Options={Options} />} />
<Route path="*" element={<AihubmixPage Options={Options} />} />
<Route path="/aihubmix" element={<AihubmixPage Options={Options} />} />
<Route path="/silicon" element={<SiliconPage Options={Options} />} />
<Route path="/dmxapi" element={<DmxapiPage Options={Options} />} />
@@ -35,8 +35,8 @@ import styled from 'styled-components'
import SendMessageButton from '../home/Inputbar/SendMessageButton'
import { SettingTitle } from '../settings'
import Artboard from './Artboard'
import PaintingsList from './PaintingsList'
import Artboard from './components/Artboard'
import PaintingsList from './components/PaintingsList'
const IMAGE_SIZES = [
{
@@ -88,7 +88,7 @@ const DEFAULT_PAINTING: Painting = {
// let _painting: Painting
const PaintingsPage: FC<{ Options: string[] }> = ({ Options }) => {
const SiliconPage: FC<{ Options: string[] }> = ({ Options }) => {
const { t } = useTranslation()
const { paintings, addPainting, removePainting, updatePainting } = usePaintings()
const [painting, setPainting] = useState<Painting>(paintings[0] || DEFAULT_PAINTING)
@@ -645,4 +645,4 @@ const StyledInputNumber = styled(InputNumber)`
width: 70px;
`
export default PaintingsPage
export default SiliconPage
@@ -7,7 +7,7 @@ import React, { FC } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import ImagePreview from '../home/Markdown/ImagePreview'
import ImagePreview from '../../home/Markdown/ImagePreview'
interface ArtboardProps {
painting: Painting
@@ -98,8 +98,8 @@ const Artboard: FC<ArtboardProps> = ({
{painting.urls.length > 0 && retry ? (
<div>
<ImageList>
{painting.urls.map((url) => (
<ImageListItem key={url}>{url}</ImageListItem>
{painting.urls.map((url, index) => (
<ImageListItem key={url || index}>{url}</ImageListItem>
))}
</ImageList>
<div>
@@ -109,11 +109,11 @@ const Artboard: FC<ArtboardProps> = ({
</Button>
</div>
</div>
) : imageCover ? (
imageCover
) : (
imageCover ?
imageCover:
(<div>{t('paintings.image_placeholder')}</div>)
)}
<div>{t('paintings.image_placeholder')}</div>
)}
</ImagePlaceholder>
)}
{isLoading && (
@@ -1,6 +1,14 @@
import type { PaintingAction, PaintingsState } from '@renderer/types'
import type { PaintingAction } from '@renderer/types'
import { ASPECT_RATIOS, RENDERING_SPEED_OPTIONS, STYLE_TYPES, V3_STYLE_TYPES } from './constants'
import {
ASPECT_RATIOS,
BACKGROUND_OPTIONS,
MODERATION_OPTIONS,
QUALITY_OPTIONS,
RENDERING_SPEED_OPTIONS,
STYLE_TYPES,
V3_STYLE_TYPES
} from './constants'
// 配置项类型定义
export type ConfigItem = {
@@ -19,7 +27,14 @@ export type ConfigItem = {
title?: string
tooltip?: string
options?:
| Array<{ label: string; value: string | number; icon?: string; onlyV2?: boolean }>
| Array<{
label: string
title?: string
value?: string | number
icon?: string
onlyV2?: boolean
options?: Array<{ label: string; value: string | number; icon?: string; onlyV2?: boolean }>
}>
| ((
config: ConfigItem,
painting: Partial<PaintingAction>
@@ -32,189 +47,158 @@ export type ConfigItem = {
disabled?: boolean | ((config: ConfigItem, painting: Partial<PaintingAction>) => boolean)
initialValue?: string | number
required?: boolean
condition?: (painting: PaintingAction) => boolean
}
export type AihubmixMode = keyof PaintingsState
export type AihubmixMode = 'generate' | 'remix' | 'upscale'
// 创建配置项函数
export const createModeConfigs = (): Record<AihubmixMode, ConfigItem[]> => {
return {
paintings: [],
DMXAPIPaintings: [],
generate: [
{ type: 'title', title: 'paintings.model', tooltip: 'paintings.generate.model_tip' },
{
type: 'select',
key: 'model',
title: 'paintings.model',
tooltip: 'paintings.generate.model_tip',
options: [
{ label: 'ideogram_V_3', value: 'V_3' },
{ label: 'ideogram_V_2', value: 'V_2' },
{ label: 'ideogram_V_2_TURBO', value: 'V_2_TURBO' },
{ label: 'ideogram_V_2A', value: 'V_2A' },
{ label: 'ideogram_V_2A_TURBO', value: 'V_2A_TURBO' },
{ label: 'ideogram_V_1', value: 'V_1' },
{ label: 'ideogram_V_1_TURBO', value: 'V_1_TURBO' }
{
label: 'OpenAI',
title: 'OpenAI',
options: [{ label: 'gpt-image-1', value: 'gpt-image-1' }]
},
{
label: 'ideogram',
title: 'ideogram',
options: [
{ label: 'ideogram_V_3', value: 'V_3' },
{ label: 'ideogram_V_2', value: 'V_2' },
{ label: 'ideogram_V_2_TURBO', value: 'V_2_TURBO' },
{ label: 'ideogram_V_2A', value: 'V_2A' },
{ label: 'ideogram_V_2A_TURBO', value: 'V_2A_TURBO' },
{ label: 'ideogram_V_1', value: 'V_1' },
{ label: 'ideogram_V_1_TURBO', value: 'V_1_TURBO' }
]
}
]
},
{ type: 'title', title: 'paintings.rendering_speed', tooltip: 'paintings.generate.rendering_speed_tip' },
{
type: 'select',
key: 'renderingSpeed',
title: 'paintings.rendering_speed',
tooltip: 'paintings.generate.rendering_speed_tip',
options: RENDERING_SPEED_OPTIONS,
initialValue: 'DEFAULT',
disabled: (_config, painting) => {
const model = painting?.model
return !model || !model.includes('V_3')
}
condition: (painting) => painting.model === 'V_3'
},
{ type: 'title', title: 'paintings.aspect_ratio' },
{
type: 'select',
key: 'aspectRatio',
options: ASPECT_RATIOS.map((size) => ({
label: size.label,
value: size.value,
icon: size.icon
}))
},
{
type: 'title',
title: 'paintings.number_images',
tooltip: 'paintings.generate.number_images_tip'
title: 'paintings.aspect_ratio',
options: ASPECT_RATIOS,
condition: (painting) => Boolean(painting.model?.startsWith('V_'))
},
{
type: 'slider',
key: 'numImages',
title: 'paintings.number_images',
tooltip: 'paintings.generate.number_images_tip',
min: 1,
max: 8
},
{
type: 'title',
title: 'paintings.style_type',
tooltip: 'paintings.generate.style_type_tip'
max: 8,
condition: (painting) => Boolean(painting.model?.startsWith('V_'))
},
{
type: 'select',
key: 'styleType',
title: 'paintings.style_type',
tooltip: 'paintings.generate.style_type_tip',
options: (_config, painting) => {
// 根据模型选择显示不同的样式类型选项
return painting?.model?.includes('V_3') ? V3_STYLE_TYPES : STYLE_TYPES
},
disabled: false
},
{
type: 'title',
title: 'paintings.seed',
tooltip: 'paintings.generate.seed_tip'
disabled: false,
condition: (painting) => Boolean(painting.model?.startsWith('V_'))
},
{
type: 'input',
key: 'seed'
},
{
type: 'title',
title: 'paintings.negative_prompt',
tooltip: 'paintings.generate.negative_prompt_tip'
key: 'seed',
title: 'paintings.seed',
tooltip: 'paintings.generate.seed_tip',
condition: (painting) => Boolean(painting.model?.startsWith('V_'))
},
{
type: 'textarea',
key: 'negativePrompt'
},
{
type: 'title',
title: 'paintings.magic_prompt_option',
tooltip: 'paintings.generate.magic_prompt_option_tip'
key: 'negativePrompt',
title: 'paintings.negative_prompt',
tooltip: 'paintings.generate.negative_prompt_tip',
condition: (painting) => Boolean(painting.model?.startsWith('V_'))
},
{
type: 'switch',
key: 'magicPromptOption'
}
],
edit: [
{ type: 'title', title: 'paintings.edit.image_file' },
{
type: 'image',
key: 'imageFile'
key: 'magicPromptOption',
title: 'paintings.magic_prompt_option',
tooltip: 'paintings.generate.magic_prompt_option_tip',
condition: (painting) => Boolean(painting.model?.startsWith('V_'))
},
{ type: 'title', title: 'paintings.model', tooltip: 'paintings.edit.model_tip' },
{
type: 'select',
key: 'model',
key: 'size',
title: 'paintings.aspect_ratio',
options: [
{ label: 'ideogram_V_3', value: 'V_3' },
{ label: 'ideogram_V_2', value: 'V_2' },
{ label: 'ideogram_V_2_TURBO', value: 'V_2_TURBO' },
{ label: 'ideogram_V_2A', value: 'V_2A' },
{ label: 'ideogram_V_2A_TURBO', value: 'V_2A_TURBO' },
{ label: 'ideogram_V_1', value: 'V_1' },
{ label: 'ideogram_V_1_TURBO', value: 'V_1_TURBO' }
]
},
{ type: 'title', title: 'paintings.rendering_speed', tooltip: 'paintings.edit.rendering_speed_tip' },
{
type: 'select',
key: 'renderingSpeed',
options: RENDERING_SPEED_OPTIONS,
initialValue: 'DEFAULE',
disabled: (_config, painting) => {
const model = painting?.model
return !model || !model.includes('V_3')
}
},
{
type: 'title',
title: 'paintings.number_images',
tooltip: 'paintings.edit.number_images_tip'
{ label: '自动', value: 'auto' },
{ label: '1:1', value: '1024x1024' },
{ label: '3:2', value: '1536x1024' },
{ label: '2:3', value: '1024x1536' }
],
initialValue: '1024x1024',
condition: (painting) => painting.model === 'gpt-image-1'
},
{
type: 'slider',
key: 'numImages',
key: 'n',
title: 'paintings.number_images',
tooltip: 'paintings.generate.number_images_tip',
min: 1,
max: 8
},
{
type: 'title',
title: 'paintings.style_type',
tooltip: 'paintings.edit.style_type_tip'
max: 10,
initialValue: 1,
condition: (painting) => painting.model === 'gpt-image-1'
},
{
type: 'select',
key: 'styleType',
options: (_config, painting) => {
// 根据模型选择显示不同的样式类型选项
return painting?.model?.includes('V_3') ? V3_STYLE_TYPES : STYLE_TYPES
},
disabled: false
key: 'quality',
title: 'paintings.quality',
options: QUALITY_OPTIONS,
initialValue: 'auto',
condition: (painting) => painting.model === 'gpt-image-1'
},
{
type: 'title',
title: 'paintings.seed',
tooltip: 'paintings.edit.seed_tip'
type: 'select',
key: 'moderation',
title: 'paintings.moderation',
options: MODERATION_OPTIONS,
initialValue: 'auto',
condition: (painting) => painting.model === 'gpt-image-1'
},
{
type: 'input',
key: 'seed'
},
{
type: 'title',
title: 'paintings.magic_prompt_option',
tooltip: 'paintings.edit.magic_prompt_option_tip'
},
{
type: 'switch',
key: 'magicPromptOption'
type: 'select',
key: 'background',
title: 'paintings.background',
options: BACKGROUND_OPTIONS,
initialValue: 'auto',
condition: (painting) => painting.model === 'gpt-image-1'
}
],
remix: [
{ type: 'title', title: 'paintings.remix.image_file' },
{
type: 'image',
key: 'imageFile'
key: 'imageFile',
title: 'paintings.remix.image_file'
},
{ type: 'title', title: 'paintings.model', tooltip: 'paintings.remix.model_tip' },
{
type: 'select',
key: 'model',
title: 'paintings.model',
tooltip: 'paintings.remix.model_tip',
options: [
{ label: 'ideogram_V_3', value: 'V_3' },
{ label: 'ideogram_V_2', value: 'V_2' },
@@ -225,10 +209,10 @@ export const createModeConfigs = (): Record<AihubmixMode, ConfigItem[]> => {
{ label: 'ideogram_V_1_TURBO', value: 'V_1_TURBO' }
]
},
{ type: 'title', title: 'paintings.rendering_speed', tooltip: 'paintings.remix.rendering_speed_tip' },
{
type: 'select',
key: 'renderingSpeed',
title: 'paintings.rendering_speed',
options: RENDERING_SPEED_OPTIONS,
initialValue: 'DEFAULT',
disabled: (_config, painting) => {
@@ -236,42 +220,32 @@ export const createModeConfigs = (): Record<AihubmixMode, ConfigItem[]> => {
return !model || !model.includes('V_3')
}
},
{ type: 'title', title: 'paintings.aspect_ratio' },
{
type: 'select',
key: 'aspectRatio',
options: ASPECT_RATIOS.map((size) => ({
label: size.label,
value: size.value,
icon: size.icon
}))
title: 'paintings.aspect_ratio',
options: ASPECT_RATIOS
},
{ type: 'title', title: 'paintings.remix.image_weight' },
{
type: 'slider',
key: 'imageWeight',
title: 'paintings.remix.image_weight',
min: 1,
max: 100
},
{
type: 'title',
title: 'paintings.number_images',
tooltip: 'paintings.remix.number_images_tip'
},
{
type: 'slider',
key: 'numImages',
title: 'paintings.number_images',
tooltip: 'paintings.remix.number_images_tip',
min: 1,
max: 8
},
{
type: 'title',
title: 'paintings.style_type',
tooltip: 'paintings.remix.style_type_tip'
},
{
type: 'select',
key: 'styleType',
title: 'paintings.style_type',
tooltip: 'paintings.remix.style_type_tip',
options: (_config, painting) => {
// 根据模型选择显示不同的样式类型选项
return painting?.model?.includes('V_3') ? V3_STYLE_TYPES : STYLE_TYPES
@@ -279,78 +253,92 @@ export const createModeConfigs = (): Record<AihubmixMode, ConfigItem[]> => {
disabled: false
},
{
type: 'title',
type: 'input',
key: 'seed',
title: 'paintings.seed',
tooltip: 'paintings.remix.seed_tip'
},
{
type: 'input',
key: 'seed'
},
{
type: 'title',
type: 'textarea',
key: 'negativePrompt',
title: 'paintings.negative_prompt',
tooltip: 'paintings.remix.negative_prompt_tip'
},
{
type: 'textarea',
key: 'negativePrompt'
},
{
type: 'title',
type: 'switch',
key: 'magicPromptOption',
title: 'paintings.magic_prompt_option',
tooltip: 'paintings.remix.magic_prompt_option_tip'
},
{
type: 'switch',
key: 'magicPromptOption'
}
],
upscale: [
{ type: 'title', title: 'paintings.upscale.image_file' },
{
type: 'image',
key: 'imageFile',
title: 'paintings.upscale.image_file',
required: true
},
{ type: 'title', title: 'paintings.upscale.resemblance', tooltip: 'paintings.upscale.resemblance_tip' },
{ type: 'slider', key: 'resemblance', min: 1, max: 100 },
{ type: 'title', title: 'paintings.upscale.detail', tooltip: 'paintings.upscale.detail_tip' },
{
type: 'slider',
key: 'detail',
key: 'resemblance',
title: 'paintings.upscale.resemblance',
min: 1,
max: 100
},
{
type: 'title',
title: 'paintings.number_images',
tooltip: 'paintings.upscale.number_images_tip'
type: 'slider',
key: 'detail',
title: 'paintings.upscale.detail',
tooltip: 'paintings.upscale.detail_tip',
min: 1,
max: 100
},
{
type: 'slider',
key: 'numImages',
title: 'paintings.number_images',
tooltip: 'paintings.upscale.number_images_tip',
min: 1,
max: 8
},
{
type: 'title',
type: 'input',
key: 'seed',
title: 'paintings.seed',
tooltip: 'paintings.upscale.seed_tip'
},
{
type: 'input',
key: 'seed'
},
{
type: 'title',
type: 'switch',
key: 'magicPromptOption',
title: 'paintings.magic_prompt_option',
tooltip: 'paintings.upscale.magic_prompt_option_tip'
},
{
type: 'switch',
key: 'magicPromptOption'
}
]
}
}
// 几种默认的绘画配置
export const DEFAULT_PAINTING: PaintingAction = {
id: 'aihubmix_1',
model: 'gpt-image-1',
aspectRatio: 'ASPECT_1_1',
numImages: 1,
styleType: 'AUTO',
prompt: '',
negativePrompt: '',
magicPromptOption: true,
seed: '',
imageWeight: 50,
resemblance: 50,
detail: 50,
imageFile: undefined,
mask: undefined,
files: [],
urls: [],
renderingSpeed: 'DEFAULT',
size: '1024x1024',
background: 'auto',
quality: 'auto',
moderation: 'auto',
n: 1
}
@@ -1,87 +1,78 @@
import ImageSize1_1 from '@renderer/assets/images/paintings/image-size-1-1.svg'
import ImageSize1_2 from '@renderer/assets/images/paintings/image-size-1-2.svg'
import ImageSize3_2 from '@renderer/assets/images/paintings/image-size-3-2.svg'
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 type { PaintingAction } from '@renderer/types'
// 几种默认的绘画配置
export const DEFAULT_PAINTING: PaintingAction = {
id: 'aihubmix_1',
model: 'V_3',
aspectRatio: 'ASPECT_1_1',
numImages: 1,
styleType: 'AUTO',
prompt: '',
negativePrompt: '',
magicPromptOption: true,
seed: '',
imageWeight: 50,
resemblance: 50,
detail: 50,
imageFile: undefined,
mask: undefined,
files: [],
urls: [],
renderingSpeed: 'DEFAULT'
}
export const ASPECT_RATIOS = [
{
label: '1:1',
value: 'ASPECT_1_1',
icon: ImageSize1_1
label: 'paintings.aspect_ratios.square',
options: [
{
label: '1:1',
value: 'ASPECT_1_1'
}
]
},
{
label: '3:1',
value: 'ASPECT_3_1',
icon: ImageSize3_2
label: 'paintings.aspect_ratios.landscape',
options: [
{
label: '1:2',
value: 'ASPECT_1_2'
},
{
label: '1:3',
value: 'ASPECT_1_3'
},
{
label: '2:3',
value: 'ASPECT_2_3'
},
{
label: '3:4',
value: 'ASPECT_3_4'
},
{
label: '4:5',
value: 'ASPECT_4_5'
},
{
label: '9:16',
value: 'ASPECT_9_16'
},
{
label: '10:16',
value: 'ASPECT_10_16'
}
]
},
{
label: '1:3',
value: 'ASPECT_1_3',
icon: ImageSize1_2
},
{
label: '3:2',
value: 'ASPECT_3_2',
icon: ImageSize3_2
},
{
label: '2:3',
value: 'ASPECT_2_3',
icon: ImageSize1_2
},
{
label: '4:3',
value: 'ASPECT_4_3',
icon: ImageSize3_4
},
{
label: '3:4',
value: 'ASPECT_3_4',
icon: ImageSize3_4
},
{
label: '16:9',
value: 'ASPECT_16_9',
icon: ImageSize16_9
},
{
label: '9:16',
value: 'ASPECT_9_16',
icon: ImageSize9_16
},
{
label: '16:10',
value: 'ASPECT_16_10',
icon: ImageSize16_9
},
{
label: '10:16',
value: 'ASPECT_10_16',
icon: ImageSize9_16
label: 'paintings.aspect_ratios.landscape',
options: [
{
label: '2:1',
value: 'ASPECT_2_1'
},
{
label: '3:1',
value: 'ASPECT_3_1'
},
{
label: '3:2',
value: 'ASPECT_3_2'
},
{
label: '4:3',
value: 'ASPECT_4_3'
},
{
label: '5:4',
value: 'ASPECT_5_4'
},
{
label: '16:9',
value: 'ASPECT_16_9'
},
{
label: '16:10',
value: 'ASPECT_16_10'
}
]
}
]
@@ -132,3 +123,21 @@ export const RENDERING_SPEED_OPTIONS = [
value: 'QUALITY'
}
]
export const QUALITY_OPTIONS = [
{ label: 'paintings.quality_options.auto', value: 'auto' },
{ label: 'paintings.quality_options.low', value: 'low' },
{ label: 'paintings.quality_options.medium', value: 'medium' },
{ label: 'paintings.quality_options.high', value: 'high' }
]
export const MODERATION_OPTIONS = [
{ label: 'paintings.moderation_options.auto', value: 'auto' },
{ label: 'paintings.moderation_options.low', value: 'low' }
]
export const BACKGROUND_OPTIONS = [
{ label: 'paintings.background_options.auto', value: 'auto' },
{ label: 'paintings.background_options.transparent', value: 'transparent' },
{ label: 'paintings.background_options.opaque', value: 'opaque' }
]
@@ -1,6 +1,7 @@
import {
Content,
File,
FileState,
FinishReason,
FunctionCall,
GenerateContentConfig,
@@ -9,6 +10,7 @@ import {
HarmBlockThreshold,
HarmCategory,
Modality,
Pager,
Part,
PartUnion,
SafetySetting,
@@ -27,6 +29,7 @@ import {
import { getStoreSetting } from '@renderer/hooks/useSettings'
import i18n from '@renderer/i18n'
import { getAssistantSettings, getDefaultModel, getTopNamingModel } from '@renderer/services/AssistantService'
import { CacheService } from '@renderer/services/CacheService'
import { EVENT_NAMES } from '@renderer/services/EventService'
import {
filterContextMessages,
@@ -91,7 +94,7 @@ export default class GeminiProvider extends BaseProvider {
const isSmallFile = file.size < smallFileSize
if (isSmallFile) {
const { data, mimeType } = await window.api.gemini.base64File(file)
const { data, mimeType } = await this.base64File(file)
return {
inlineData: {
data,
@@ -101,7 +104,7 @@ export default class GeminiProvider extends BaseProvider {
}
// Retrieve file from Gemini uploaded files
const fileMetadata: File | undefined = await window.api.gemini.retrieveFile(file, this.apiKey)
const fileMetadata: File | undefined = await this.retrieveFile(file)
if (fileMetadata) {
return {
@@ -113,10 +116,7 @@ export default class GeminiProvider extends BaseProvider {
}
// If file is not found, upload it to Gemini
const result = await window.api.gemini.uploadFile(file, {
apiKey: this.apiKey,
baseURL: this.getBaseURL()
})
const result = await this.uploadFile(file)
return {
fileData: {
@@ -1153,4 +1153,62 @@ export default class GeminiProvider extends BaseProvider {
}
return
}
private async uploadFile(file: FileType): Promise<File> {
return await this.sdk.files.upload({
file: file.path,
config: {
mimeType: 'application/pdf',
name: file.id,
displayName: file.origin_name
}
})
}
private async base64File(file: FileType) {
const { data } = await window.api.file.base64File(file.id + file.ext)
return {
data,
mimeType: 'application/pdf'
}
}
private async retrieveFile(file: FileType): Promise<File | undefined> {
const cachedResponse = CacheService.get<any>('gemini_file_list')
if (cachedResponse) {
return this.processResponse(cachedResponse, file)
}
const response = await this.sdk.files.list()
CacheService.set('gemini_file_list', response, 3000)
return this.processResponse(response, file)
}
private async processResponse(response: Pager<File>, file: FileType) {
for await (const f of response) {
if (f.state === FileState.ACTIVE) {
if (f.displayName === file.origin_name && Number(f.sizeBytes) === file.size) {
return f
}
}
}
return undefined
}
// @ts-ignore unused
private async listFiles(): Promise<File[]> {
const files: File[] = []
for await (const f of await this.sdk.files.list()) {
files.push(f)
}
return files
}
// @ts-ignore unused
private async deleteFile(fileId: string) {
await this.sdk.files.delete({ name: fileId })
}
}
@@ -1,6 +1,6 @@
import { WebSearchState } from '@renderer/store/websearch'
import { WebSearchProvider, WebSearchProviderResponse } from '@renderer/types'
import { BochaSearchParams, BochaSearchResponse } from '@renderer/types/bocha'
import { BochaSearchParams, BochaSearchResponse } from '@renderer/utils/bocha'
import BaseWebSearchProvider from './BaseWebSearchProvider'
+74
View File
@@ -0,0 +1,74 @@
interface CacheItem<T> {
data: T
timestamp: number
duration: number
}
export class CacheService {
private static cache: Map<string, CacheItem<any>> = new Map()
/**
* Set cache
* @param key Cache key
* @param data Cache data
* @param duration Cache duration (in milliseconds)
*/
static set<T>(key: string, data: T, duration: number): void {
this.cache.set(key, {
data,
timestamp: Date.now(),
duration
})
}
/**
* Get cache
* @param key Cache key
* @returns Returns data if cache exists and not expired, otherwise returns null
*/
static get<T>(key: string): T | null {
const item = this.cache.get(key)
if (!item) return null
const now = Date.now()
if (now - item.timestamp > item.duration) {
this.remove(key)
return null
}
return item.data
}
/**
* Remove specific cache
* @param key Cache key
*/
static remove(key: string): void {
this.cache.delete(key)
}
/**
* Clear all cache
*/
static clear(): void {
this.cache.clear()
}
/**
* Check if cache exists and is valid
* @param key Cache key
* @returns boolean
*/
static has(key: string): boolean {
const item = this.cache.get(key)
if (!item) return false
const now = Date.now()
if (now - item.timestamp > item.duration) {
this.remove(key)
return false
}
return true
}
}
+16
View File
@@ -39,6 +39,22 @@ class FileManager {
return fileData.data
}
static async addBase64File(file: FileType): Promise<FileType> {
Logger.log(`[FileManager] Adding base64 file: ${JSON.stringify(file)}`)
const base64File = await window.api.file.base64File(file.id + file.ext)
const fileRecord = await db.files.get(base64File.id)
if (fileRecord) {
await db.files.update(fileRecord.id, { ...fileRecord, count: fileRecord.count + 1 })
return fileRecord
}
await db.files.add(base64File)
return base64File
}
static async uploadFile(file: FileType): Promise<FileType> {
Logger.log(`[FileManager] Uploading file: ${JSON.stringify(file)}`)
+9 -2
View File
@@ -6,6 +6,7 @@ import {
MathEngine,
OpenAIServiceTier,
OpenAISummaryText,
PaintingProvider,
ThemeMode,
TranslateLanguageVarious
} from '@renderer/types'
@@ -167,6 +168,7 @@ export interface SettingsState {
backup: boolean
knowledgeEmbed: boolean
}
defaultPaintingProvider: PaintingProvider
}
export type MultiModelMessageStyle = 'horizontal' | 'vertical' | 'fold' | 'grid'
@@ -298,7 +300,8 @@ export const initialState: SettingsState = {
assistant: false,
backup: false,
knowledgeEmbed: false
}
},
defaultPaintingProvider: 'aihubmix'
}
const settingsSlice = createSlice({
@@ -632,6 +635,9 @@ const settingsSlice = createSlice({
},
setNotificationSettings: (state, action: PayloadAction<SettingsState['notification']>) => {
state.notification = action.payload
},
setDefaultPaintingProvider: (state, action: PayloadAction<PaintingProvider>) => {
state.defaultPaintingProvider = action.payload
}
}
})
@@ -730,7 +736,8 @@ export const {
setEnableBackspaceDeleteModel,
setOpenAISummaryText,
setOpenAIServiceTier,
setNotificationSettings
setNotificationSettings,
setDefaultPaintingProvider
} = settingsSlice.actions
export default settingsSlice.reducer
+9 -2
View File
@@ -1,7 +1,7 @@
import type { WebSearchResultBlock } from '@anthropic-ai/sdk/resources'
import type { GroundingMetadata } from '@google/genai'
import type OpenAI from 'openai'
import React from 'react'
import type { CSSProperties } from 'react'
import type { Message } from './newMessage'
@@ -185,6 +185,8 @@ export type PaintingParams = {
files: FileType[]
}
export type PaintingProvider = 'aihubmix' | 'silicon' | 'dmxapi'
export interface Painting extends PaintingParams {
model?: string
prompt?: string
@@ -207,6 +209,11 @@ export interface GeneratePainting extends PaintingParams {
negativePrompt?: string
magicPromptOption?: boolean
renderingSpeed?: string
quality?: string
moderation?: string
n?: number
size?: string
background?: string
}
export interface EditPainting extends PaintingParams {
@@ -274,7 +281,7 @@ export type MinAppType = {
url: string
bodered?: boolean
background?: string
style?: React.CSSProperties
style?: CSSProperties
addTime?: string
type?: 'Custom' | 'Default' // Added the 'type' property
}
-222
View File
@@ -1,222 +0,0 @@
import { z } from 'zod'
export const InputType = z.enum(['text', 'image', 'audio', 'video', 'document'])
export type InputType = z.infer<typeof InputType>
export const OutputType = z.enum(['text', 'image', 'audio', 'video', 'vector'])
export type OutputType = z.infer<typeof OutputType>
export const OutputMode = z.enum(['sync', 'streaming'])
export type OutputMode = z.infer<typeof OutputMode>
export const ModelCapability = z.enum([
'audioGeneration',
'cache',
'codeExecution',
'embedding',
'fineTuning',
'imageGeneration',
'OCR',
'realTime',
'rerank',
'reasoning',
'streaming',
'structuredOutput',
'textGeneration',
'translation',
'transcription',
'toolUse',
'videoGeneration',
'webSearch'
])
export type ModelCapability = z.infer<typeof ModelCapability>
export const ModelSchema = z
.object({
id: z.string(),
modelId: z.string(),
providerId: z.string(),
name: z.string(),
group: z.string(),
description: z.string().optional(),
owned_by: z.string().optional(),
supportedInputs: z.array(InputType),
supportedOutputs: z.array(OutputType),
supportedOutputModes: z.array(OutputMode),
limits: z
.object({
inputTokenLimit: z.number().optional(),
outputTokenLimit: z.number().optional(),
contextWindow: z.number().optional()
})
.optional(),
price: z
.object({
inputTokenPrice: z.number().optional(),
outputTokenPrice: z.number().optional()
})
.optional(),
capabilities: z.array(ModelCapability)
})
.refine(
(data) => {
// 如果模型支持streaming,则必须支持streamingOutputMode
if (data.capabilities.includes('streaming') && !data.supportedOutputModes.includes('streaming')) {
return false
}
// 如果模型有OCR能力,则必须支持图像输入类型或者文件输入类型
if (
data.capabilities.includes('OCR') &&
!data.supportedInputs.includes('image') &&
!data.supportedInputs.includes('document')
) {
return false
}
// 如果模型有图像生成能力,则必须支持图像输出
if (data.capabilities.includes('imageGeneration') && !data.supportedOutputs.includes('image')) {
return false
}
// 如果有音频生成能力,则必须支持音频输出类型
if (data.capabilities.includes('audioGeneration') && !data.supportedOutputs.includes('audio')) {
return false
}
// 如果有音频识别能力,则必须支持音频输入类型
if (
(data.capabilities.includes('transcription') || data.capabilities.includes('translation')) &&
!data.supportedInputs.includes('audio')
) {
return false
}
// 如果有视频生成能力,则必须支持视频输出类型
if (data.capabilities.includes('videoGeneration') && !data.supportedOutputs.includes('video')) {
return false
}
// 如果模型有embedding能力,则必须支持向量输出类型
if (data.capabilities.includes('embedding') && !data.supportedOutputs.includes('vector')) {
return false
}
// 如果模型有toolUse, Reasoning, streaming, cache, codeExecution, imageGeneration, audioGeneration, videoGeneration webSearch能力,则必须支持文字的输入
if (
(data.capabilities.includes('toolUse') ||
data.capabilities.includes('reasoning') ||
data.capabilities.includes('streaming') ||
data.capabilities.includes('cache') ||
data.capabilities.includes('codeExecution') ||
data.capabilities.includes('imageGeneration') ||
data.capabilities.includes('audioGeneration') ||
data.capabilities.includes('videoGeneration') ||
data.capabilities.includes('webSearch')) &&
!data.supportedInputs.includes('text')
) {
return false
}
// 如果模型有toolUse, Reasoning, streaming, cache, codeExecution, OCR, textGeneration, translation, transcription, webSearch, structuredOutput能力,则必须支持文字的输出
if (
(data.capabilities.includes('toolUse') ||
data.capabilities.includes('reasoning') ||
data.capabilities.includes('streaming') ||
data.capabilities.includes('cache') ||
data.capabilities.includes('codeExecution') ||
data.capabilities.includes('OCR') ||
data.capabilities.includes('textGeneration') ||
data.capabilities.includes('translation') ||
data.capabilities.includes('transcription') ||
data.capabilities.includes('webSearch') ||
data.capabilities.includes('structuredOutput')) &&
!data.supportedOutputs.includes('text')
) {
return false
}
return true
},
{
message: 'ModelCard has inconsistent capabilities and supported input/output type'
}
)
export type ModelCard = z.infer<typeof ModelSchema>
export function createModelCard(model: ModelCard): ModelCard {
return ModelSchema.parse(model)
}
export function supportesInputType(model: ModelCard, inputType: InputType) {
return model.supportedInputs.includes(inputType)
}
export function supportesOutputType(model: ModelCard, outputType: OutputType) {
return model.supportedOutputs.includes(outputType)
}
export function supportesOutputMode(model: ModelCard, outputMode: OutputMode) {
return model.supportedOutputModes.includes(outputMode)
}
export function supportesCapability(model: ModelCard, capability: ModelCapability) {
return model.capabilities.includes(capability)
}
export function isVisionModel(model: ModelCard) {
return supportesInputType(model, 'image')
}
export function isImageGenerationModel(model: ModelCard) {
return isVisionModel(model) && supportesCapability(model, 'imageGeneration')
}
export function isAudioModel(model: ModelCard) {
return supportesInputType(model, 'audio')
}
export function isAudioGenerationModel(model: ModelCard) {
return supportesCapability(model, 'audioGeneration')
}
export function isVideoModel(model: ModelCard) {
return supportesInputType(model, 'video')
}
export function isEmbedModel(model: ModelCard) {
return supportesOutputType(model, 'vector') && supportesCapability(model, 'embedding')
}
export function isTextEmbeddingModel(model: ModelCard) {
return isEmbedModel(model) && supportesInputType(model, 'text') && model.supportedInputs.length === 1
}
export function isMultiModalEmbeddingModel(model: ModelCard) {
return isEmbedModel(model) && model.supportedInputs.length > 1
}
export function isRerankModel(model: ModelCard) {
return supportesCapability(model, 'rerank')
}
export function isReasoningModel(model: ModelCard) {
return supportesCapability(model, 'reasoning')
}
export function isToolUseModel(model: ModelCard) {
return supportesCapability(model, 'toolUse')
}
export function isOnlyStreamingModel(model: ModelCard) {
return (
supportesCapability(model, 'streaming') &&
supportesOutputMode(model, 'streaming') &&
model.supportedOutputModes.length === 1
)
}
+1 -1
View File
@@ -1,4 +1,4 @@
import { CompletionUsage } from 'openai/resources'
import type { CompletionUsage } from 'openai/resources'
import type {
Assistant,
@@ -15,7 +15,7 @@ import ActionGeneral from './components/ActionGeneral'
import ActionTranslate from './components/ActionTranslate'
const SelectionActionApp: FC = () => {
const { language } = useSettings()
const { language, customCss } = useSettings()
const { t } = useTranslation()
@@ -61,6 +61,20 @@ const SelectionActionApp: FC = () => {
i18n.changeLanguage(language || navigator.language || defaultLanguage)
}, [language])
useEffect(() => {
let customCssElement = document.getElementById('user-defined-custom-css') as HTMLStyleElement
if (customCssElement) {
customCssElement.remove()
}
if (customCss) {
customCssElement = document.createElement('style')
customCssElement.id = 'user-defined-custom-css'
customCssElement.textContent = customCss
document.head.appendChild(customCssElement)
}
}, [customCss])
useEffect(() => {
const contentEl = contentElementRef.current
if (contentEl) {
@@ -65,8 +65,10 @@ const ActionIcons: FC<{
const renderActionButton = useCallback(
(action: ActionItem) => {
const displayName = action.isBuiltIn ? t(action.name) : action.name
return (
<ActionButton key={action.id} onClick={() => handleAction(action)}>
<ActionButton key={action.id} onClick={() => handleAction(action)} title={isCompact ? displayName : undefined}>
<ActionIcon>
{action.id === 'copy' ? (
renderCopyIcon()
@@ -79,9 +81,7 @@ const ActionIcons: FC<{
/>
)}
</ActionIcon>
{!isCompact && (
<ActionTitle className="btn-title">{action.isBuiltIn ? t(action.name) : action.name}</ActionTitle>
)}
{!isCompact && <ActionTitle className="btn-title">{displayName}</ActionTitle>}
</ActionButton>
)
},
@@ -95,7 +95,7 @@ const ActionIcons: FC<{
* demo is used in the settings page
*/
const SelectionToolbar: FC<{ demo?: boolean }> = ({ demo = false }) => {
const { language } = useSettings()
const { language, customCss } = useSettings()
const { isCompact, actionItems } = useSelectionAssistant()
const [animateKey, setAnimateKey] = useState(0)
const [copyIconStatus, setCopyIconStatus] = useState<'normal' | 'success' | 'fail'>('normal')
@@ -150,6 +150,20 @@ const SelectionToolbar: FC<{ demo?: boolean }> = ({ demo = false }) => {
i18n.changeLanguage(language || navigator.language || defaultLanguage)
}, [language])
useEffect(() => {
let customCssElement = document.getElementById('user-defined-custom-css') as HTMLStyleElement
if (customCssElement) {
customCssElement.remove()
}
if (customCss) {
customCssElement = document.createElement('style')
customCssElement.id = 'user-defined-custom-css'
customCssElement.textContent = customCss
document.head.appendChild(customCssElement)
}
}, [customCss])
const onHideCleanUp = () => {
setCopyIconStatus('normal')
setCopyIconAnimation('none')
+234 -1812
View File
File diff suppressed because it is too large Load Diff