Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c2014f1d6 | ||
|
|
647dd3e751 | ||
|
|
4225312d4a | ||
|
|
c2a4613e32 | ||
|
|
5d5c1eee74 | ||
|
|
c1b5e6b183 | ||
|
|
fd37ba18dc | ||
|
|
4a26f7ce78 | ||
|
|
8b38ebcac4 | ||
|
|
e8dac28787 | ||
|
|
3ccebb503f | ||
|
|
42327836de | ||
|
|
4d7a3bb8c3 | ||
|
|
1996e163c9 | ||
|
|
e43f7f87ab | ||
|
|
47a83fa67f |
@@ -56,6 +56,4 @@ electronDownload:
|
||||
afterSign: scripts/notarize.js
|
||||
releaseInfo:
|
||||
releaseNotes: |
|
||||
增加 Gemini、豆包、阶跃星辰等服务商支持
|
||||
修复 Anthropic 回复问题
|
||||
新的 Windows, Linux 侧边栏样式
|
||||
新增AI小程序模块
|
||||
|
||||
@@ -7,7 +7,15 @@ export default defineConfig({
|
||||
plugins: [externalizeDepsPlugin()]
|
||||
},
|
||||
preload: {
|
||||
plugins: [externalizeDepsPlugin()]
|
||||
plugins: [externalizeDepsPlugin()],
|
||||
build: {
|
||||
rollupOptions: {
|
||||
input: {
|
||||
index: resolve(__dirname, 'src/preload/index.ts'),
|
||||
minapp: resolve(__dirname, 'src/preload/minapp.ts')
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
renderer: {
|
||||
resolve: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cherry-studio",
|
||||
"version": "0.5.7",
|
||||
"version": "0.6.0",
|
||||
"description": "A powerful AI assistant for producer.",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "kangfenmao@qq.com",
|
||||
|
||||
68
resources/graphrag.html
Normal file
@@ -0,0 +1,68 @@
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
<script src="https://unpkg.com/3d-force-graph"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="3d-graph"></div>
|
||||
<script src="./js/bridge.js"></script>
|
||||
<script type="module">
|
||||
import { getQueryParam } from './js/utils.js'
|
||||
|
||||
const apiUrl = getQueryParam('apiUrl')
|
||||
const modelId = getQueryParam('modelId')
|
||||
const jsonUrl = `${apiUrl}/v1/global_graph/${modelId}`
|
||||
|
||||
const infoCard = document.createElement('div')
|
||||
infoCard.style.position = 'fixed'
|
||||
infoCard.style.backgroundColor = 'rgba(255, 255, 255, 0.9)'
|
||||
infoCard.style.padding = '8px'
|
||||
infoCard.style.borderRadius = '4px'
|
||||
infoCard.style.boxShadow = '0 2px 5px rgba(0,0,0,0.2)'
|
||||
infoCard.style.fontSize = '12px'
|
||||
infoCard.style.maxWidth = '200px'
|
||||
infoCard.style.display = 'none'
|
||||
infoCard.style.zIndex = '1000'
|
||||
document.body.appendChild(infoCard)
|
||||
|
||||
document.addEventListener('mousemove', (event) => {
|
||||
infoCard.style.left = `${event.clientX + 10}px`
|
||||
infoCard.style.top = `${event.clientY + 10}px`
|
||||
})
|
||||
|
||||
const elem = document.getElementById('3d-graph')
|
||||
const Graph = ForceGraph3D()(elem)
|
||||
.jsonUrl(jsonUrl)
|
||||
.nodeAutoColorBy((node) => node.properties.type || 'default')
|
||||
.nodeVal((node) => node.properties.degree)
|
||||
.linkWidth((link) => link.properties.weight)
|
||||
.onNodeHover((node) => {
|
||||
if (node) {
|
||||
infoCard.innerHTML = `
|
||||
<div style="font-weight: bold; margin-bottom: 4px; color: #333;">
|
||||
${node.properties.title}
|
||||
</div>
|
||||
<div style="color: #666;">
|
||||
${node.properties.description}
|
||||
</div>`
|
||||
infoCard.style.display = 'block'
|
||||
} else {
|
||||
infoCard.style.display = 'none'
|
||||
}
|
||||
})
|
||||
.onNodeClick((node) => {
|
||||
const url = `${apiUrl}/v1/references/${modelId}/entities/${node.properties.human_readable_id}`
|
||||
window.api.minApp({
|
||||
url,
|
||||
windowOptions: {
|
||||
title: node.properties.title,
|
||||
width: 500,
|
||||
height: 800
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
36
resources/js/bridge.js
Normal file
@@ -0,0 +1,36 @@
|
||||
;(() => {
|
||||
let messageId = 0
|
||||
const pendingCalls = new Map()
|
||||
|
||||
function api(method, ...args) {
|
||||
const id = messageId++
|
||||
return new Promise((resolve, reject) => {
|
||||
pendingCalls.set(id, { resolve, reject })
|
||||
window.parent.postMessage({ id, type: 'api-call', method, args }, '*')
|
||||
})
|
||||
}
|
||||
|
||||
window.addEventListener('message', (event) => {
|
||||
if (event.data.type === 'api-response') {
|
||||
const { id, result, error } = event.data
|
||||
const pendingCall = pendingCalls.get(id)
|
||||
if (pendingCall) {
|
||||
if (error) {
|
||||
pendingCall.reject(new Error(error))
|
||||
} else {
|
||||
pendingCall.resolve(result)
|
||||
}
|
||||
pendingCalls.delete(id)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
window.api = new Proxy(
|
||||
{},
|
||||
{
|
||||
get: (target, prop) => {
|
||||
return (...args) => api(prop, ...args)
|
||||
}
|
||||
}
|
||||
)
|
||||
})()
|
||||
5
resources/js/utils.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export function getQueryParam(paramName) {
|
||||
const url = new URL(window.location.href)
|
||||
const params = new URLSearchParams(url.search)
|
||||
return params.get(paramName)
|
||||
}
|
||||
70
resources/minapp.html
Normal file
@@ -0,0 +1,70 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>MinApp</title>
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
header {
|
||||
height: 40px;
|
||||
background-color: #303030;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
#header-left {
|
||||
margin-left: 10px;
|
||||
margin-right: auto;
|
||||
}
|
||||
#header-center {
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
#header-right {
|
||||
margin-left: auto;
|
||||
margin-right: 10px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
border-radius: 3px;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
button:hover {
|
||||
background-color: #555;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div id="header-left"></div>
|
||||
<div id="header-center"></div>
|
||||
<div id="header-right"></div>
|
||||
</header>
|
||||
<script type="module">
|
||||
import { getQueryParam } from './js/utils.js'
|
||||
const title = getQueryParam('title')
|
||||
document.getElementById('header-center').innerHTML = title
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,84 +1,12 @@
|
||||
import { electronApp, is, optimizer } from '@electron-toolkit/utils'
|
||||
import { electronApp, optimizer } from '@electron-toolkit/utils'
|
||||
import * as Sentry from '@sentry/electron/main'
|
||||
import { app, BrowserWindow, ipcMain, Menu, MenuItem, session, shell } from 'electron'
|
||||
import { app, BrowserWindow, ipcMain, session, shell } from 'electron'
|
||||
import installExtension, { REDUX_DEVTOOLS } from 'electron-devtools-installer'
|
||||
import windowStateKeeper from 'electron-window-state'
|
||||
import { join } from 'path'
|
||||
|
||||
import icon from '../../build/icon.png?asset'
|
||||
import { appConfig, titleBarOverlayDark, titleBarOverlayLight } from './config'
|
||||
import { saveFile } from './event'
|
||||
import AppUpdater from './updater'
|
||||
|
||||
function createWindow() {
|
||||
// Load the previous state with fallback to defaults
|
||||
const mainWindowState = windowStateKeeper({
|
||||
defaultWidth: 1080,
|
||||
defaultHeight: 670
|
||||
})
|
||||
|
||||
const theme = appConfig.get('theme') || 'light'
|
||||
|
||||
// Create the browser window.
|
||||
const mainWindow = new BrowserWindow({
|
||||
x: mainWindowState.x,
|
||||
y: mainWindowState.y,
|
||||
width: mainWindowState.width,
|
||||
height: mainWindowState.height,
|
||||
minWidth: 1080,
|
||||
minHeight: 600,
|
||||
show: true,
|
||||
autoHideMenuBar: true,
|
||||
transparent: process.platform === 'darwin',
|
||||
vibrancy: 'fullscreen-ui',
|
||||
titleBarStyle: 'hidden',
|
||||
titleBarOverlay: theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight,
|
||||
trafficLightPosition: { x: 8, y: 12 },
|
||||
...(process.platform === 'linux' ? { icon } : {}),
|
||||
webPreferences: {
|
||||
preload: join(__dirname, '../preload/index.js'),
|
||||
sandbox: false,
|
||||
webSecurity: false
|
||||
// devTools: !app.isPackaged,
|
||||
}
|
||||
})
|
||||
|
||||
mainWindowState.manage(mainWindow)
|
||||
|
||||
mainWindow.webContents.on('context-menu', () => {
|
||||
const menu = new Menu()
|
||||
menu.append(new MenuItem({ label: '复制', role: 'copy', sublabel: '⌘ + C' }))
|
||||
menu.append(new MenuItem({ label: '粘贴', role: 'paste', sublabel: '⌘ + V' }))
|
||||
menu.append(new MenuItem({ label: '剪切', role: 'cut', sublabel: '⌘ + X' }))
|
||||
menu.append(new MenuItem({ type: 'separator' }))
|
||||
menu.append(new MenuItem({ label: '全选', role: 'selectAll', sublabel: '⌘ + A' }))
|
||||
menu.popup()
|
||||
})
|
||||
|
||||
mainWindow.webContents.on('will-navigate', (event, url) => {
|
||||
event.preventDefault()
|
||||
shell.openExternal(url)
|
||||
})
|
||||
|
||||
mainWindow.on('ready-to-show', () => {
|
||||
mainWindow.show()
|
||||
})
|
||||
|
||||
mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||
shell.openExternal(details.url)
|
||||
return { action: 'deny' }
|
||||
})
|
||||
|
||||
// HMR for renderer base on electron-vite cli.
|
||||
// Load the remote URL for development or the local html file for production.
|
||||
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
|
||||
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
|
||||
} else {
|
||||
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
|
||||
}
|
||||
|
||||
return mainWindow
|
||||
}
|
||||
import { createMainWindow, createMinappWindow } from './window'
|
||||
|
||||
// This method will be called when Electron has finished
|
||||
// initialization and is ready to create browser windows.
|
||||
@@ -97,17 +25,18 @@ app.whenReady().then(() => {
|
||||
app.on('activate', function () {
|
||||
// On macOS it's common to re-create a window in the app when the
|
||||
// dock icon is clicked and there are no other windows open.
|
||||
if (BrowserWindow.getAllWindows().length === 0) createWindow()
|
||||
if (BrowserWindow.getAllWindows().length === 0) createMainWindow()
|
||||
})
|
||||
|
||||
const mainWindow = createWindow()
|
||||
const mainWindow = createMainWindow()
|
||||
|
||||
const { autoUpdater } = new AppUpdater(mainWindow)
|
||||
|
||||
// IPC
|
||||
ipcMain.handle('get-app-info', () => ({
|
||||
version: app.getVersion(),
|
||||
isPackaged: app.isPackaged
|
||||
isPackaged: app.isPackaged,
|
||||
appPath: app.getAppPath()
|
||||
}))
|
||||
|
||||
ipcMain.handle('open-website', (_, url: string) => {
|
||||
@@ -120,6 +49,10 @@ app.whenReady().then(() => {
|
||||
|
||||
ipcMain.handle('save-file', saveFile)
|
||||
|
||||
ipcMain.handle('minapp', (_, args) => {
|
||||
createMinappWindow(args)
|
||||
})
|
||||
|
||||
ipcMain.handle('set-theme', (_, theme: 'light' | 'dark') => {
|
||||
appConfig.set('theme', theme)
|
||||
mainWindow?.setTitleBarOverlay &&
|
||||
|
||||
32
src/main/utils.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* 将 JavaScript 对象转换为 URL 查询参数字符串
|
||||
* @param obj - 要转换的对象
|
||||
* @param options - 配置选项
|
||||
* @returns 转换后的查询参数字符串
|
||||
*/
|
||||
export function objectToQueryParams(
|
||||
obj: Record<string, string | number | boolean | null | undefined | object>,
|
||||
options: {
|
||||
skipNull?: boolean
|
||||
skipUndefined?: boolean
|
||||
} = {}
|
||||
): string {
|
||||
const { skipNull = false, skipUndefined = false } = options
|
||||
|
||||
const params = new URLSearchParams()
|
||||
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if (skipNull && value === null) continue
|
||||
if (skipUndefined && value === undefined) continue
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((item) => params.append(key, String(item)))
|
||||
} else if (typeof value === 'object' && value !== null) {
|
||||
params.append(key, JSON.stringify(value))
|
||||
} else if (value !== undefined && value !== null) {
|
||||
params.append(key, String(value))
|
||||
}
|
||||
}
|
||||
|
||||
return params.toString()
|
||||
}
|
||||
151
src/main/window.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import { is } from '@electron-toolkit/utils'
|
||||
import { app, BrowserView, BrowserWindow, Menu, MenuItem, shell } from 'electron'
|
||||
import windowStateKeeper from 'electron-window-state'
|
||||
import { join } from 'path'
|
||||
|
||||
import icon from '../../build/icon.png?asset'
|
||||
import { appConfig, titleBarOverlayDark, titleBarOverlayLight } from './config'
|
||||
import { objectToQueryParams } from './utils'
|
||||
|
||||
export function createMainWindow() {
|
||||
// Load the previous state with fallback to defaults
|
||||
const mainWindowState = windowStateKeeper({
|
||||
defaultWidth: 1080,
|
||||
defaultHeight: 670
|
||||
})
|
||||
|
||||
const theme = appConfig.get('theme') || 'light'
|
||||
|
||||
// Create the browser window.
|
||||
const mainWindow = new BrowserWindow({
|
||||
x: mainWindowState.x,
|
||||
y: mainWindowState.y,
|
||||
width: mainWindowState.width,
|
||||
height: mainWindowState.height,
|
||||
minWidth: 1080,
|
||||
minHeight: 600,
|
||||
show: true,
|
||||
autoHideMenuBar: true,
|
||||
transparent: process.platform === 'darwin',
|
||||
vibrancy: 'fullscreen-ui',
|
||||
titleBarStyle: 'hidden',
|
||||
titleBarOverlay: theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight,
|
||||
trafficLightPosition: { x: 8, y: 12 },
|
||||
...(process.platform === 'linux' ? { icon } : {}),
|
||||
webPreferences: {
|
||||
preload: join(__dirname, '../preload/index.js'),
|
||||
sandbox: false,
|
||||
webSecurity: false
|
||||
// devTools: !app.isPackaged,
|
||||
}
|
||||
})
|
||||
|
||||
mainWindowState.manage(mainWindow)
|
||||
|
||||
mainWindow.webContents.on('context-menu', () => {
|
||||
const menu = new Menu()
|
||||
menu.append(new MenuItem({ label: '复制', role: 'copy', sublabel: '⌘ + C' }))
|
||||
menu.append(new MenuItem({ label: '粘贴', role: 'paste', sublabel: '⌘ + V' }))
|
||||
menu.append(new MenuItem({ label: '剪切', role: 'cut', sublabel: '⌘ + X' }))
|
||||
menu.append(new MenuItem({ type: 'separator' }))
|
||||
menu.append(new MenuItem({ label: '全选', role: 'selectAll', sublabel: '⌘ + A' }))
|
||||
menu.popup()
|
||||
})
|
||||
|
||||
mainWindow.on('ready-to-show', () => {
|
||||
mainWindow.show()
|
||||
})
|
||||
|
||||
mainWindow.webContents.on('will-navigate', (event, url) => {
|
||||
event.preventDefault()
|
||||
shell.openExternal(url)
|
||||
})
|
||||
|
||||
mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||
const websiteReg = /accounts.google.com/i
|
||||
|
||||
if (websiteReg.test(details.url)) {
|
||||
createMinappWindow({ url: details.url, windowOptions: { width: 1000, height: 680 } })
|
||||
} else {
|
||||
shell.openExternal(details.url)
|
||||
}
|
||||
|
||||
return { action: 'deny' }
|
||||
})
|
||||
|
||||
mainWindow.webContents.session.webRequest.onHeadersReceived({ urls: ['*://*/*'] }, (details, callback) => {
|
||||
if (details.responseHeaders?.['X-Frame-Options']) {
|
||||
delete details.responseHeaders['X-Frame-Options']
|
||||
}
|
||||
if (details.responseHeaders?.['x-frame-options']) {
|
||||
delete details.responseHeaders['x-frame-options']
|
||||
}
|
||||
if (details.responseHeaders?.['Content-Security-Policy']) {
|
||||
delete details.responseHeaders['Content-Security-Policy']
|
||||
}
|
||||
if (details.responseHeaders?.['content-security-policy']) {
|
||||
delete details.responseHeaders['content-security-policy']
|
||||
}
|
||||
callback({ cancel: false, responseHeaders: details.responseHeaders })
|
||||
})
|
||||
|
||||
// HMR for renderer base on electron-vite cli.
|
||||
// Load the remote URL for development or the local html file for production.
|
||||
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
|
||||
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
|
||||
} else {
|
||||
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
|
||||
}
|
||||
|
||||
return mainWindow
|
||||
}
|
||||
|
||||
export function createMinappWindow({
|
||||
url,
|
||||
windowOptions
|
||||
}: {
|
||||
url: string
|
||||
windowOptions?: Electron.BrowserWindowConstructorOptions
|
||||
}) {
|
||||
const width = 1000
|
||||
const height = 680
|
||||
const headerHeight = 40
|
||||
|
||||
const minappWindow = new BrowserWindow({
|
||||
width,
|
||||
height,
|
||||
autoHideMenuBar: true,
|
||||
alwaysOnTop: true,
|
||||
titleBarOverlay: titleBarOverlayDark,
|
||||
titleBarStyle: 'hidden',
|
||||
...windowOptions,
|
||||
webPreferences: {
|
||||
preload: join(__dirname, '../preload/minapp.js'),
|
||||
sandbox: false
|
||||
}
|
||||
})
|
||||
|
||||
const view = new BrowserView()
|
||||
view.setBounds({ x: 0, y: headerHeight, width, height: height - headerHeight })
|
||||
view.webContents.loadURL(url)
|
||||
|
||||
const minappWindowParams = {
|
||||
title: windowOptions?.title || 'CherryStudio'
|
||||
}
|
||||
|
||||
const appPath = app.getAppPath()
|
||||
const minappHtmlPath = appPath + '/resources/minapp.html'
|
||||
|
||||
minappWindow.loadURL('file://' + minappHtmlPath + '?' + objectToQueryParams(minappWindowParams))
|
||||
minappWindow.setBrowserView(view)
|
||||
minappWindow.on('resize', () => {
|
||||
view.setBounds({
|
||||
x: 0,
|
||||
y: headerHeight,
|
||||
width: minappWindow.getBounds().width,
|
||||
height: minappWindow.getBounds().height - headerHeight
|
||||
})
|
||||
})
|
||||
|
||||
return minappWindow
|
||||
}
|
||||
2
src/preload/index.d.ts
vendored
@@ -7,12 +7,14 @@ declare global {
|
||||
getAppInfo: () => Promise<{
|
||||
version: string
|
||||
isPackaged: boolean
|
||||
appPath: string
|
||||
}>
|
||||
checkForUpdate: () => void
|
||||
openWebsite: (url: string) => void
|
||||
setProxy: (proxy: string | undefined) => void
|
||||
saveFile: (path: string, content: string) => void
|
||||
setTheme: (theme: 'light' | 'dark') => void
|
||||
minApp: (url: string) => void
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@ const api = {
|
||||
openWebsite: (url: string) => ipcRenderer.invoke('open-website', url),
|
||||
setProxy: (proxy: string) => ipcRenderer.invoke('set-proxy', proxy),
|
||||
saveFile: (path: string, content: string) => ipcRenderer.invoke('save-file', path, content),
|
||||
setTheme: (theme: 'light' | 'dark') => ipcRenderer.invoke('set-theme', theme)
|
||||
setTheme: (theme: 'light' | 'dark') => ipcRenderer.invoke('set-theme', theme),
|
||||
minApp: (url: string) => ipcRenderer.invoke('minapp', url)
|
||||
}
|
||||
|
||||
// Use `contextBridge` APIs to expose Electron APIs to
|
||||
|
||||
14
src/preload/minapp.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { contextBridge } from 'electron'
|
||||
|
||||
const api = {}
|
||||
|
||||
if (process.contextIsolated) {
|
||||
try {
|
||||
contextBridge.exposeInMainWorld('api', api)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
} else {
|
||||
// @ts-ignore (define in dts)
|
||||
window.api = api
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
<meta name="viewport" content="initial-scale=1, width=device-width" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self'; connect-src *; script-src 'self' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; img-src 'self' data:; frame-src *" />
|
||||
content="default-src 'self'; connect-src *; script-src 'self' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; img-src 'self' data:; frame-src * file:" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { PersistGate } from 'redux-persist/integration/react'
|
||||
import Sidebar from './components/app/Sidebar'
|
||||
import TopViewContainer from './components/TopView'
|
||||
import AgentsPage from './pages/agents/AgentsPage'
|
||||
import AppsPage from './pages/apps/AppsPage'
|
||||
import HomePage from './pages/home/HomePage'
|
||||
import SettingsPage from './pages/settings/SettingsPage'
|
||||
import TranslatePage from './pages/translate/TranslatePage'
|
||||
@@ -23,8 +24,9 @@ function App(): JSX.Element {
|
||||
<Sidebar />
|
||||
<Routes>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/apps" element={<AgentsPage />} />
|
||||
<Route path="/agents" element={<AgentsPage />} />
|
||||
<Route path="/translate" element={<TranslatePage />} />
|
||||
<Route path="/apps" element={<AppsPage />} />
|
||||
<Route path="/settings/*" element={<SettingsPage />} />
|
||||
</Routes>
|
||||
</HashRouter>
|
||||
|
||||
@@ -1,63 +1,64 @@
|
||||
@font-face {
|
||||
font-family: "iconfont"; /* Project id 4563475 */
|
||||
src: url('iconfont.woff2?t=1723186111414') format('woff2'),
|
||||
url('iconfont.woff?t=1723186111414') format('woff'),
|
||||
url('iconfont.ttf?t=1723186111414') format('truetype');
|
||||
font-family: 'iconfont'; /* Project id 4563475 */
|
||||
src: url('iconfont.woff2?t=1724204739157') format('woff2');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
font-family: "iconfont" !important;
|
||||
font-family: 'iconfont' !important;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-business-smart-assistant:before {
|
||||
content: '\e601';
|
||||
}
|
||||
|
||||
.icon-copy:before {
|
||||
content: "\e6ae";
|
||||
content: '\e6ae';
|
||||
}
|
||||
|
||||
.icon-ic_send:before {
|
||||
content: "\e795";
|
||||
content: '\e795';
|
||||
}
|
||||
|
||||
.icon-dark1:before {
|
||||
content: "\e72f";
|
||||
content: '\e72f';
|
||||
}
|
||||
|
||||
.icon-theme-light:before {
|
||||
content: "\e6b7";
|
||||
content: '\e6b7';
|
||||
}
|
||||
|
||||
.icon-translate_line:before {
|
||||
content: "\e7de";
|
||||
content: '\e7de';
|
||||
}
|
||||
|
||||
.icon-history:before {
|
||||
content: "\e758";
|
||||
content: '\e758';
|
||||
}
|
||||
|
||||
.icon-hidesidebarhoriz:before {
|
||||
content: "\e8eb";
|
||||
content: '\e8eb';
|
||||
}
|
||||
|
||||
.icon-showsidebarhoriz:before {
|
||||
content: "\e944";
|
||||
content: '\e944';
|
||||
}
|
||||
|
||||
.icon-a-addchat:before {
|
||||
content: "\e658";
|
||||
content: '\e658';
|
||||
}
|
||||
|
||||
.icon-appstore:before {
|
||||
content: "\e792";
|
||||
content: '\e792';
|
||||
}
|
||||
|
||||
.icon-chat:before {
|
||||
content: "\e615";
|
||||
content: '\e615';
|
||||
}
|
||||
|
||||
.icon-setting:before {
|
||||
content: "\e78e";
|
||||
content: '\e78e';
|
||||
}
|
||||
|
||||
|
||||
BIN
src/renderer/src/assets/images/apps/360-ai.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
src/renderer/src/assets/images/apps/baidu-ai.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
src/renderer/src/assets/images/apps/baixiaoying.webp
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
src/renderer/src/assets/images/apps/kimi.jpg
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
src/renderer/src/assets/images/apps/yuewen.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
src/renderer/src/assets/images/models/hailuo.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
src/renderer/src/assets/images/models/yi.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
@@ -1,7 +0,0 @@
|
||||
<svg width="600" height="600" viewBox="0 0 600 600" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="300" cy="300" r="300" fill="white"/>
|
||||
<rect x="409.733" y="340.032" width="42.3862" height="151.648" rx="21.1931" fill="#003425"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M422.005 133.354C413.089 125.771 399.714 126.851 392.131 135.768L273.699 275.021C270.643 278.614 268.994 282.932 268.698 287.302C268.532 288.371 268.446 289.466 268.446 290.581V468.603C268.446 480.308 277.934 489.796 289.639 489.796C301.344 489.796 310.832 480.308 310.832 468.603V296.784L424.419 163.228C432.002 154.312 430.921 140.937 422.005 133.354Z" fill="#003425"/>
|
||||
<rect x="113.972" y="134.25" width="42.3862" height="174.745" rx="21.1931" transform="rotate(-39.3441 113.972 134.25)" fill="#003425"/>
|
||||
<circle cx="460.126" cy="279.278" r="25.9027" fill="#00DD20"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 869 B |
BIN
src/renderer/src/assets/images/providers/minimax.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 6.5 KiB |
BIN
src/renderer/src/assets/images/providers/moonshot.jpg
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
src/renderer/src/assets/images/providers/yi.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
@@ -1,7 +0,0 @@
|
||||
<svg width="600" height="600" viewBox="0 0 600 600" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="300" cy="300" r="300" fill="#003425"/>
|
||||
<rect x="409.733" y="340.031" width="42.3862" height="151.648" rx="21.1931" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M422.005 133.354C413.089 125.771 399.714 126.851 392.131 135.767L273.699 275.021C270.643 278.614 268.994 282.932 268.698 287.302C268.532 288.371 268.446 289.466 268.446 290.581V468.603C268.446 480.308 277.934 489.796 289.639 489.796C301.344 489.796 310.832 480.308 310.832 468.603V296.784L424.419 163.228C432.002 154.312 430.921 140.937 422.005 133.354Z" fill="white"/>
|
||||
<rect x="113.972" y="134.25" width="42.3862" height="174.745" rx="21.1931" transform="rotate(-39.3441 113.972 134.25)" fill="white"/>
|
||||
<circle cx="460.126" cy="279.278" r="25.9027" fill="#00FF25"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 865 B |
@@ -1,7 +1,7 @@
|
||||
@import './markdown.scss';
|
||||
@import './scrollbar.scss';
|
||||
@import '../fonts/icon-fonts/iconfont.css';
|
||||
@import '../fonts/Ubuntu/Ubuntu.css';
|
||||
@import '../fonts/ubuntu/ubuntu.css';
|
||||
|
||||
:root {
|
||||
--color-white: #ffffff;
|
||||
@@ -44,12 +44,13 @@
|
||||
--input-bar-background: rgba(255, 255, 255, 0.02);
|
||||
|
||||
--navbar-height: 42px;
|
||||
--sidebar-width: 55px;
|
||||
--sidebar-width: 52px;
|
||||
--status-bar-height: 40px;
|
||||
--input-bar-height: 85px;
|
||||
|
||||
--assistants-width: 245px;
|
||||
--topic-list-width: 260px;
|
||||
--settings-width: var(--assistants-width);
|
||||
--status-bar-height: 40px;
|
||||
--input-bar-height: 85px;
|
||||
}
|
||||
|
||||
body[theme-mode='light'] {
|
||||
@@ -101,6 +102,14 @@ body[theme-mode='light'] {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
*:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
* {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
}
|
||||
@@ -165,37 +174,31 @@ body,
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.dragable {
|
||||
.drag {
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
.dragdisable {
|
||||
.nodrag {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.minapp-drawer {
|
||||
.ant-drawer-header-title {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
.ant-drawer-close {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 15px;
|
||||
padding: 15px;
|
||||
margin-right: -5px;
|
||||
-webkit-app-region: no-drag;
|
||||
z-index: 100000;
|
||||
.ant-drawer-content-wrapper {
|
||||
box-shadow: none;
|
||||
}
|
||||
.ant-drawer-header {
|
||||
height: calc(var(--navbar-height) + 0.5px);
|
||||
position: absolute;
|
||||
-webkit-app-region: drag;
|
||||
min-height: calc(var(--navbar-height) + 0.5px);
|
||||
background: var(--navbar-background);
|
||||
width: calc(100vw - var(--sidebar-width));
|
||||
padding-right: 10px !important;
|
||||
border-bottom: 0.5px solid var(--color-border);
|
||||
margin-top: -0.5px;
|
||||
}
|
||||
.ant-drawer-body {
|
||||
padding: 0;
|
||||
margin-top: var(--navbar-height);
|
||||
overflow: hidden;
|
||||
}
|
||||
.minapp-mask {
|
||||
background-color: transparent !important;
|
||||
|
||||
7
src/renderer/src/components/Icons/CopyIcon.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import { FC } from 'react'
|
||||
|
||||
const CopyIcon: FC<React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>> = (props) => {
|
||||
return <i {...props} className={`iconfont icon-copy ${props.className}`} />
|
||||
}
|
||||
|
||||
export default CopyIcon
|
||||
@@ -1,25 +1,28 @@
|
||||
import { CloseOutlined, ExportOutlined, ReloadOutlined } from '@ant-design/icons'
|
||||
import { isMac, isWindows } from '@renderer/config/constant'
|
||||
import { useBridge } from '@renderer/hooks/useBridge'
|
||||
import store from '@renderer/store'
|
||||
import { setMinappShow } from '@renderer/store/runtime'
|
||||
import { MinAppType } from '@renderer/types'
|
||||
import { Drawer } from 'antd'
|
||||
import { useRef, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { TopView } from '../TopView'
|
||||
|
||||
interface ShowParams {
|
||||
title: string
|
||||
url: string
|
||||
}
|
||||
|
||||
interface Props extends ShowParams {
|
||||
interface Props {
|
||||
app: MinAppType
|
||||
resolve: (data: any) => void
|
||||
}
|
||||
|
||||
const PopupContainer: React.FC<Props> = ({ title, url, resolve }) => {
|
||||
const PopupContainer: React.FC<Props> = ({ app, resolve }) => {
|
||||
const [open, setOpen] = useState(true)
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null)
|
||||
|
||||
useBridge()
|
||||
|
||||
const canOpenExternalLink = app.url.startsWith('http://') || app.url.startsWith('https://')
|
||||
|
||||
const onClose = () => {
|
||||
setOpen(false)
|
||||
setTimeout(() => resolve({}), 300)
|
||||
@@ -27,17 +30,38 @@ const PopupContainer: React.FC<Props> = ({ title, url, resolve }) => {
|
||||
|
||||
const onReload = () => {
|
||||
if (iframeRef.current) {
|
||||
iframeRef.current.src = url
|
||||
iframeRef.current.src = app.url
|
||||
}
|
||||
}
|
||||
|
||||
const onOpenLink = () => {
|
||||
window.api.openWebsite(url)
|
||||
window.api.openWebsite(app.url)
|
||||
}
|
||||
|
||||
const Title = () => {
|
||||
return (
|
||||
<TitleContainer style={{ justifyContent: 'space-between' }}>
|
||||
<TitleText>{app.name}</TitleText>
|
||||
<ButtonsGroup className={isWindows ? 'windows' : ''}>
|
||||
<Button onClick={onReload}>
|
||||
<ReloadOutlined />
|
||||
</Button>
|
||||
{canOpenExternalLink && (
|
||||
<Button onClick={onOpenLink}>
|
||||
<ExportOutlined />
|
||||
</Button>
|
||||
)}
|
||||
<Button onClick={onClose}>
|
||||
<CloseOutlined />
|
||||
</Button>
|
||||
</ButtonsGroup>
|
||||
</TitleContainer>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
title={title}
|
||||
title={<Title />}
|
||||
placement="bottom"
|
||||
onClose={onClose}
|
||||
open={open}
|
||||
@@ -48,18 +72,7 @@ const PopupContainer: React.FC<Props> = ({ title, url, resolve }) => {
|
||||
maskClosable={false}
|
||||
closeIcon={null}
|
||||
style={{ marginLeft: 'var(--sidebar-width)' }}>
|
||||
<Frame src={url} ref={iframeRef} />
|
||||
<ButtonsGroup>
|
||||
<Button onClick={onReload}>
|
||||
<ReloadOutlined />
|
||||
</Button>
|
||||
<Button onClick={onOpenLink}>
|
||||
<ExportOutlined />
|
||||
</Button>
|
||||
<Button onClick={onClose}>
|
||||
<CloseOutlined />
|
||||
</Button>
|
||||
</ButtonsGroup>
|
||||
<Frame src={app.url} ref={iframeRef} />
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
@@ -68,22 +81,46 @@ const Frame = styled.iframe`
|
||||
width: calc(100vw - var(--sidebar-width));
|
||||
height: calc(100vh - var(--navbar-height));
|
||||
border: none;
|
||||
background-color: white;
|
||||
`
|
||||
|
||||
const TitleContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding-left: ${isMac ? '20px' : '15px'};
|
||||
padding-right: 10px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
`
|
||||
|
||||
const TitleText = styled.div`
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
color: var(--color-text-1);
|
||||
margin-right: 10px;
|
||||
user-select: none;
|
||||
`
|
||||
|
||||
const ButtonsGroup = styled.div`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
height: var(--navbar-height);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 0 10px;
|
||||
-webkit-app-region: no-drag;
|
||||
&.windows {
|
||||
margin-right: ${isWindows ? '130px' : 0};
|
||||
background-color: var(--color-background-mute);
|
||||
border-radius: 50px;
|
||||
padding: 0 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
`
|
||||
|
||||
const Button = styled.div`
|
||||
-webkit-app-region: no-drag;
|
||||
cursor: pointer;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
@@ -107,12 +144,12 @@ export default class MinApp {
|
||||
TopView.hide('MinApp')
|
||||
store.dispatch(setMinappShow(false))
|
||||
}
|
||||
static start(props: ShowParams) {
|
||||
static start(app: MinAppType) {
|
||||
store.dispatch(setMinappShow(true))
|
||||
return new Promise<any>((resolve) => {
|
||||
TopView.show(
|
||||
<PopupContainer
|
||||
{...props}
|
||||
app={app}
|
||||
resolve={(v) => {
|
||||
resolve(v)
|
||||
this.close()
|
||||
|
||||
@@ -9,7 +9,7 @@ const navbarBackgroundColor = isMac ? 'var(--navbar-background-mac)' : 'var(--na
|
||||
|
||||
export const Navbar: FC<Props> = ({ children, ...props }) => {
|
||||
const { minappShow } = useRuntime()
|
||||
const backgroundColor = minappShow ? 'var(--color-background)' : navbarBackgroundColor
|
||||
const backgroundColor = minappShow ? 'var(--navbar-background)' : navbarBackgroundColor
|
||||
|
||||
return (
|
||||
<NavbarContainer {...props} style={{ backgroundColor }}>
|
||||
|
||||
@@ -24,8 +24,8 @@ const Sidebar: FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Container style={{ backgroundColor: minappShow ? 'var(--color-background)' : sidebarBackgroundColor }}>
|
||||
<AvatarImg src={avatar || Logo} draggable={false} className="dragdisable" onClick={onEditUser} />
|
||||
<Container style={{ backgroundColor: minappShow ? 'var(--navbar-background)' : sidebarBackgroundColor }}>
|
||||
<AvatarImg src={avatar || Logo} draggable={false} className="nodrag" onClick={onEditUser} />
|
||||
<MainMenus>
|
||||
<Menus>
|
||||
<StyledLink to="/">
|
||||
@@ -33,9 +33,9 @@ const Sidebar: FC = () => {
|
||||
<i className="iconfont icon-chat"></i>
|
||||
</Icon>
|
||||
</StyledLink>
|
||||
<StyledLink to="/apps">
|
||||
<Icon className={isRoute('/apps')}>
|
||||
<i className="iconfont icon-appstore"></i>
|
||||
<StyledLink to="/agents">
|
||||
<Icon className={isRoute('/agents')}>
|
||||
<i className="iconfont icon-business-smart-assistant"></i>
|
||||
</Icon>
|
||||
</StyledLink>
|
||||
<StyledLink to="/translate">
|
||||
@@ -43,6 +43,11 @@ const Sidebar: FC = () => {
|
||||
<TranslationOutlined />
|
||||
</Icon>
|
||||
</StyledLink>
|
||||
<StyledLink to="/apps">
|
||||
<Icon className={isRoute('/apps')}>
|
||||
<i className="iconfont icon-appstore"></i>
|
||||
</Icon>
|
||||
</StyledLink>
|
||||
</Menus>
|
||||
</MainMenus>
|
||||
<Menus>
|
||||
@@ -62,6 +67,7 @@ const Container = styled.div`
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
width: var(--sidebar-width);
|
||||
min-width: var(--sidebar-width);
|
||||
height: ${isMac ? 'calc(100vh - var(--navbar-height))' : '100vh'};
|
||||
-webkit-app-region: drag !important;
|
||||
border-right: 0.5px solid var(--color-border);
|
||||
|
||||
@@ -353,6 +353,36 @@ export const SYSTEM_MODELS: Record<string, SystemModel[]> = {
|
||||
}
|
||||
],
|
||||
doubao: [],
|
||||
minimax: [
|
||||
{
|
||||
id: 'abab6.5s-chat',
|
||||
provider: 'minimax',
|
||||
name: 'abab6.5s',
|
||||
group: 'abab6',
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
id: 'abab6.5g-chat',
|
||||
provider: 'minimax',
|
||||
name: 'abab6.5g',
|
||||
group: 'abab6',
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
id: 'abab6.5t-chat',
|
||||
provider: 'minimax',
|
||||
name: 'abab6.5t',
|
||||
group: 'abab6',
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
id: 'abab5.5s-chat',
|
||||
provider: 'minimax',
|
||||
name: 'abab5.5s',
|
||||
group: 'abab5',
|
||||
enabled: true
|
||||
}
|
||||
],
|
||||
aihubmix: [
|
||||
{
|
||||
id: 'gpt-4o-mini',
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import BaicuanAppLogo from '@renderer/assets/images/apps/baixiaoying.webp'
|
||||
import KimiAppLogo from '@renderer/assets/images/apps/kimi.jpg'
|
||||
import YuewenAppLogo from '@renderer/assets/images/apps/yuewen.png'
|
||||
import BaichuanModelLogo from '@renderer/assets/images/models/baichuan.png'
|
||||
import ChatGLMModelLogo from '@renderer/assets/images/models/chatglm.jpeg'
|
||||
import ChatGPTModelLogo from '@renderer/assets/images/models/chatgpt.jpeg'
|
||||
@@ -7,13 +10,14 @@ import DoubaoModelLogo from '@renderer/assets/images/models/doubao.png'
|
||||
import EmbeddingModelLogo from '@renderer/assets/images/models/embedding.png'
|
||||
import GeminiModelLogo from '@renderer/assets/images/models/gemini.png'
|
||||
import GemmaModelLogo from '@renderer/assets/images/models/gemma.jpeg'
|
||||
import HailuoModelLogo from '@renderer/assets/images/models/hailuo.png'
|
||||
import LlamaModelLogo from '@renderer/assets/images/models/llama.jpeg'
|
||||
import MicrosoftModelLogo from '@renderer/assets/images/models/microsoft.png'
|
||||
import MixtralModelLogo from '@renderer/assets/images/models/mixtral.jpeg'
|
||||
import PalmModelLogo from '@renderer/assets/images/models/palm.svg'
|
||||
import QwenModelLogo from '@renderer/assets/images/models/qwen.png'
|
||||
import StepModelLogo from '@renderer/assets/images/models/step.jpg'
|
||||
import YiModelLogo from '@renderer/assets/images/models/yi.svg'
|
||||
import YiModelLogo from '@renderer/assets/images/models/yi.png'
|
||||
import AiHubMixProviderLogo from '@renderer/assets/images/providers/aihubmix.jpg'
|
||||
import AnthropicProviderLogo from '@renderer/assets/images/providers/anthropic.jpeg'
|
||||
import BaichuanProviderLogo from '@renderer/assets/images/providers/baichuan.png'
|
||||
@@ -23,14 +27,15 @@ import DoubaoProviderLogo from '@renderer/assets/images/providers/doubao.png'
|
||||
import GeminiProviderLogo from '@renderer/assets/images/providers/gemini.png'
|
||||
import GraphRagProviderLogo from '@renderer/assets/images/providers/graph-rag.png'
|
||||
import GroqProviderLogo from '@renderer/assets/images/providers/groq.png'
|
||||
import MoonshotProviderLogo from '@renderer/assets/images/providers/moonshot.jpeg'
|
||||
import MoonshotModelLogo from '@renderer/assets/images/providers/moonshot.jpeg'
|
||||
import MinimaxProviderLogo from '@renderer/assets/images/providers/minimax.png'
|
||||
import MoonshotProviderLogo from '@renderer/assets/images/providers/moonshot.jpg'
|
||||
import MoonshotModelLogo from '@renderer/assets/images/providers/moonshot.jpg'
|
||||
import OllamaProviderLogo from '@renderer/assets/images/providers/ollama.png'
|
||||
import OpenAiProviderLogo from '@renderer/assets/images/providers/openai.png'
|
||||
import OpenRouterProviderLogo from '@renderer/assets/images/providers/openrouter.png'
|
||||
import SiliconFlowProviderLogo from '@renderer/assets/images/providers/silicon.png'
|
||||
import StepFunProviderLogo from '@renderer/assets/images/providers/stepfun.png'
|
||||
import YiProviderLogo from '@renderer/assets/images/providers/yi.svg'
|
||||
import YiProviderLogo from '@renderer/assets/images/providers/yi.png'
|
||||
import ZhipuProviderLogo from '@renderer/assets/images/providers/zhipu.png'
|
||||
|
||||
export function getProviderLogo(providerId: string) {
|
||||
@@ -69,6 +74,8 @@ export function getProviderLogo(providerId: string) {
|
||||
return DoubaoProviderLogo
|
||||
case 'graphrag-kylin-mountain':
|
||||
return GraphRagProviderLogo
|
||||
case 'minimax':
|
||||
return MinimaxProviderLogo
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
@@ -98,6 +105,7 @@ export function getModelLogo(modelId: string) {
|
||||
bison: PalmModelLogo,
|
||||
palm: PalmModelLogo,
|
||||
step: StepModelLogo,
|
||||
abab: HailuoModelLogo,
|
||||
'ep-202': DoubaoModelLogo
|
||||
}
|
||||
|
||||
@@ -121,6 +129,11 @@ export const PROVIDER_CONFIG = {
|
||||
apiKey: 'https://platform.openai.com/api-keys',
|
||||
docs: 'https://platform.openai.com/docs',
|
||||
models: 'https://platform.openai.com/docs/models'
|
||||
},
|
||||
app: {
|
||||
name: 'ChatGPT',
|
||||
url: 'https://chatgpt.com/',
|
||||
logo: OpenAiProviderLogo
|
||||
}
|
||||
},
|
||||
gemini: {
|
||||
@@ -133,6 +146,11 @@ export const PROVIDER_CONFIG = {
|
||||
apiKey: 'https://aistudio.google.com/app/apikey',
|
||||
docs: 'https://ai.google.dev/gemini-api/docs',
|
||||
models: 'https://ai.google.dev/gemini-api/docs/models/gemini'
|
||||
},
|
||||
app: {
|
||||
name: 'Gemini',
|
||||
url: 'https://gemini.google.com/',
|
||||
logo: GeminiProviderLogo
|
||||
}
|
||||
},
|
||||
silicon: {
|
||||
@@ -145,6 +163,11 @@ export const PROVIDER_CONFIG = {
|
||||
apiKey: 'https://cloud.siliconflow.cn/account/ak?referrer=clxty1xuy0014lvqwh5z50i88',
|
||||
docs: 'https://docs.siliconflow.cn/',
|
||||
models: 'https://docs.siliconflow.cn/docs/model-names'
|
||||
},
|
||||
app: {
|
||||
name: 'SiliconFlow',
|
||||
url: 'https://cloud.siliconflow.cn/playground/chat',
|
||||
logo: SiliconFlowProviderLogo
|
||||
}
|
||||
},
|
||||
deepseek: {
|
||||
@@ -157,6 +180,11 @@ export const PROVIDER_CONFIG = {
|
||||
apiKey: 'https://platform.deepseek.com/api_keys',
|
||||
docs: 'https://platform.deepseek.com/api-docs/',
|
||||
models: 'https://platform.deepseek.com/api-docs/'
|
||||
},
|
||||
app: {
|
||||
name: 'DeepSeek',
|
||||
url: 'https://chat.deepseek.com/',
|
||||
logo: DeepSeekProviderLogo
|
||||
}
|
||||
},
|
||||
yi: {
|
||||
@@ -169,6 +197,11 @@ export const PROVIDER_CONFIG = {
|
||||
apiKey: 'https://platform.lingyiwanwu.com/apikeys',
|
||||
docs: 'https://platform.lingyiwanwu.com/docs',
|
||||
models: 'https://platform.lingyiwanwu.com/docs#%E6%A8%A1%E5%9E%8B'
|
||||
},
|
||||
app: {
|
||||
name: 'Yi',
|
||||
url: 'https://www.wanzhi.com/',
|
||||
logo: YiProviderLogo
|
||||
}
|
||||
},
|
||||
zhipu: {
|
||||
@@ -181,6 +214,11 @@ export const PROVIDER_CONFIG = {
|
||||
apiKey: 'https://open.bigmodel.cn/usercenter/apikeys',
|
||||
docs: 'https://open.bigmodel.cn/dev/howuse/introduction',
|
||||
models: 'https://open.bigmodel.cn/modelcenter/square'
|
||||
},
|
||||
app: {
|
||||
name: '智谱',
|
||||
url: 'https://chatglm.cn/main/alltoolsdetail',
|
||||
logo: ZhipuProviderLogo
|
||||
}
|
||||
},
|
||||
moonshot: {
|
||||
@@ -193,6 +231,11 @@ export const PROVIDER_CONFIG = {
|
||||
apiKey: 'https://platform.moonshot.cn/console/api-keys',
|
||||
docs: 'https://platform.moonshot.cn/docs/',
|
||||
models: 'https://platform.moonshot.cn/docs/intro#%E6%A8%A1%E5%9E%8B%E5%88%97%E8%A1%A8'
|
||||
},
|
||||
app: {
|
||||
name: 'Kimi',
|
||||
url: 'https://kimi.moonshot.cn/',
|
||||
logo: KimiAppLogo
|
||||
}
|
||||
},
|
||||
baichuan: {
|
||||
@@ -205,6 +248,11 @@ export const PROVIDER_CONFIG = {
|
||||
apiKey: 'https://platform.baichuan-ai.com/console/apikey',
|
||||
docs: 'https://platform.baichuan-ai.com/docs',
|
||||
models: 'https://platform.baichuan-ai.com/price'
|
||||
},
|
||||
app: {
|
||||
name: '百小应',
|
||||
url: 'https://ying.baichuan-ai.com/chat',
|
||||
logo: BaicuanAppLogo
|
||||
}
|
||||
},
|
||||
dashscope: {
|
||||
@@ -217,6 +265,11 @@ export const PROVIDER_CONFIG = {
|
||||
apiKey: 'https://help.aliyun.com/zh/dashscope/developer-reference/acquisition-and-configuration-of-api-key',
|
||||
docs: 'https://help.aliyun.com/zh/dashscope/',
|
||||
models: 'https://dashscope.console.aliyun.com/model'
|
||||
},
|
||||
app: {
|
||||
name: '通义千问',
|
||||
url: 'https://tongyi.aliyun.com/qianwen/',
|
||||
logo: QwenModelLogo
|
||||
}
|
||||
},
|
||||
stepfun: {
|
||||
@@ -229,6 +282,11 @@ export const PROVIDER_CONFIG = {
|
||||
apiKey: 'https://platform.stepfun.com/interface-key',
|
||||
docs: 'https://platform.stepfun.com/docs/overview/concept',
|
||||
models: 'https://platform.stepfun.com/docs/llm/text'
|
||||
},
|
||||
app: {
|
||||
name: '跃问',
|
||||
url: 'https://yuewen.cn/chats/new',
|
||||
logo: YuewenAppLogo
|
||||
}
|
||||
},
|
||||
doubao: {
|
||||
@@ -241,6 +299,28 @@ export const PROVIDER_CONFIG = {
|
||||
apiKey: 'https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey',
|
||||
docs: 'https://www.volcengine.com/docs/82379/1182403',
|
||||
models: 'https://console.volcengine.com/ark/region:ark+cn-beijing/endpoint'
|
||||
},
|
||||
app: {
|
||||
name: '豆包',
|
||||
url: 'https://www.doubao.com/chat/',
|
||||
logo: DoubaoProviderLogo
|
||||
}
|
||||
},
|
||||
minimax: {
|
||||
api: {
|
||||
url: 'https://api.minimax.chat/v1/',
|
||||
editable: true
|
||||
},
|
||||
websites: {
|
||||
official: 'https://platform.minimaxi.com/',
|
||||
apiKey: 'https://platform.minimaxi.com/user-center/basic-information/interface-key',
|
||||
docs: 'https://platform.minimaxi.com/document/Announcement',
|
||||
models: 'https://platform.minimaxi.com/document/Models'
|
||||
},
|
||||
app: {
|
||||
name: '海螺',
|
||||
url: 'https://hailuoai.com/',
|
||||
logo: HailuoModelLogo
|
||||
}
|
||||
},
|
||||
'graphrag-kylin-mountain': {
|
||||
@@ -271,6 +351,11 @@ export const PROVIDER_CONFIG = {
|
||||
apiKey: 'https://console.groq.com/keys',
|
||||
docs: 'https://console.groq.com/docs/quickstart',
|
||||
models: 'https://console.groq.com/docs/models'
|
||||
},
|
||||
app: {
|
||||
name: 'Groq',
|
||||
url: 'https://groq.com/',
|
||||
logo: GroqProviderLogo
|
||||
}
|
||||
},
|
||||
ollama: {
|
||||
@@ -294,6 +379,11 @@ export const PROVIDER_CONFIG = {
|
||||
apiKey: 'https://console.anthropic.com/settings/keys',
|
||||
docs: 'https://docs.anthropic.com/en/docs',
|
||||
models: 'https://docs.anthropic.com/en/docs/about-claude/models'
|
||||
},
|
||||
app: {
|
||||
name: 'Claude',
|
||||
url: 'https://claude.ai/',
|
||||
logo: AnthropicProviderLogo
|
||||
}
|
||||
},
|
||||
aihubmix: {
|
||||
|
||||
51
src/renderer/src/hooks/useBridge.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { useEffect } from 'react'
|
||||
|
||||
export function useBridge() {
|
||||
useEffect(() => {
|
||||
const handleMessage = async (event: MessageEvent) => {
|
||||
const targetOrigin = { targetOrigin: '*' }
|
||||
|
||||
try {
|
||||
if (event.origin !== 'file://') {
|
||||
return
|
||||
}
|
||||
|
||||
const { type, method, args, id } = event.data
|
||||
|
||||
if (type !== 'api-call' || !window.api) {
|
||||
return
|
||||
}
|
||||
|
||||
const apiMethod = window.api[method]
|
||||
|
||||
if (typeof apiMethod !== 'function') {
|
||||
return
|
||||
}
|
||||
|
||||
event.source?.postMessage(
|
||||
{
|
||||
id,
|
||||
type: 'api-response',
|
||||
result: await apiMethod(...args)
|
||||
},
|
||||
targetOrigin
|
||||
)
|
||||
} catch (error) {
|
||||
event.source?.postMessage(
|
||||
{
|
||||
id: event.data?.id,
|
||||
type: 'api-response',
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
},
|
||||
targetOrigin
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('message', handleMessage)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('message', handleMessage)
|
||||
}
|
||||
}, [])
|
||||
}
|
||||
@@ -121,6 +121,7 @@ const resources = {
|
||||
aihubmix: 'AiHubMix',
|
||||
stepfun: 'StepFun',
|
||||
doubao: 'Doubao',
|
||||
minimax: 'MiniMax',
|
||||
'graphrag-kylin-mountain': 'GraphRAG'
|
||||
},
|
||||
settings: {
|
||||
@@ -220,6 +221,10 @@ const resources = {
|
||||
},
|
||||
error: {
|
||||
'chat.response': 'Something went wrong. Please check if you have set your API key in the Settings > Providers'
|
||||
},
|
||||
words: {
|
||||
knowledgeGraph: 'Knowledge Graph',
|
||||
visualization: 'Visualization'
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -342,6 +347,7 @@ const resources = {
|
||||
aihubmix: 'AiHubMix',
|
||||
stepfun: '阶跃星辰',
|
||||
doubao: '豆包',
|
||||
minimax: 'MiniMax',
|
||||
'graphrag-kylin-mountain': 'GraphRAG'
|
||||
},
|
||||
settings: {
|
||||
@@ -441,6 +447,10 @@ const resources = {
|
||||
},
|
||||
error: {
|
||||
'chat.response': '出错了,如果没有配置 API 密钥,请前往设置 > 模型提供商中配置密钥'
|
||||
},
|
||||
words: {
|
||||
knowledgeGraph: '知识图谱',
|
||||
visualization: '可视化'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import './assets/styles/index.scss'
|
||||
import './init'
|
||||
import './i18n'
|
||||
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
|
||||
49
src/renderer/src/pages/apps/App.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import MinApp from '@renderer/components/MinApp'
|
||||
import { useTheme } from '@renderer/providers/ThemeProvider'
|
||||
import { MinAppType } from '@renderer/types'
|
||||
import { FC } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface Props {
|
||||
app: MinAppType
|
||||
}
|
||||
|
||||
const App: FC<Props> = ({ app }) => {
|
||||
const { theme } = useTheme()
|
||||
|
||||
const onClick = () => {
|
||||
MinApp.start(app)
|
||||
}
|
||||
|
||||
return (
|
||||
<Container onClick={onClick}>
|
||||
<AppIcon src={app.logo} style={{ border: theme === 'dark' ? 'none' : '1px solid var(--color-border' }} />
|
||||
<AppTitle>{app.name}</AppTitle>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
`
|
||||
|
||||
const AppIcon = styled.img`
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 16px;
|
||||
user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
`
|
||||
|
||||
const AppTitle = styled.div`
|
||||
font-size: 12px;
|
||||
margin-top: 5px;
|
||||
color: var(--color-text-soft);
|
||||
text-align: center;
|
||||
`
|
||||
|
||||
export default App
|
||||
77
src/renderer/src/pages/apps/AppsPage.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import AiAssistantAppLogo from '@renderer/assets/images/apps/360-ai.png'
|
||||
import BaiduAiAppLogo from '@renderer/assets/images/apps/baidu-ai.png'
|
||||
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
||||
import { PROVIDER_CONFIG } from '@renderer/config/provider'
|
||||
import { MinAppType } from '@renderer/types'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import App from './App'
|
||||
|
||||
const _apps: MinAppType[] = [
|
||||
{
|
||||
name: 'AI 助手',
|
||||
logo: AiAssistantAppLogo,
|
||||
url: 'https://bot.360.com/'
|
||||
},
|
||||
{
|
||||
name: '文心一言',
|
||||
logo: BaiduAiAppLogo,
|
||||
url: 'https://yiyan.baidu.com/'
|
||||
}
|
||||
]
|
||||
|
||||
const AppsPage: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const apps: MinAppType[] = (Object.entries(PROVIDER_CONFIG) as any[])
|
||||
.filter(([, config]) => config.app)
|
||||
.map(([key, config]) => ({ id: key, ...config.app }))
|
||||
.concat(_apps)
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Navbar>
|
||||
<NavbarCenter style={{ borderRight: 'none' }}>{t('agents.title')}</NavbarCenter>
|
||||
</Navbar>
|
||||
<ContentContainer>
|
||||
<AppsContainer>
|
||||
{apps.map((app) => (
|
||||
<App key={app.name} app={app} />
|
||||
))}
|
||||
</AppsContainer>
|
||||
</ContentContainer>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
`
|
||||
|
||||
const ContentContainer = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
background-color: var(--color-background);
|
||||
padding: 50px;
|
||||
`
|
||||
|
||||
const AppsContainer = styled.div`
|
||||
display: flex;
|
||||
min-width: 900px;
|
||||
max-width: 900px;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-content: flex-start;
|
||||
gap: 50px;
|
||||
`
|
||||
|
||||
export default AppsPage
|
||||
@@ -70,6 +70,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
||||
EventEmitter.emit(EVENT_NAMES.SEND_MESSAGE, message)
|
||||
|
||||
setText('')
|
||||
setTimeout(() => setText(''), 500)
|
||||
|
||||
setExpend(false)
|
||||
}, [assistant.id, assistant.topics, generating, text])
|
||||
@@ -193,15 +194,17 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
||||
<Tag
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
borderRadius: '20px',
|
||||
borderRadius: '6px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '2px 8px'
|
||||
padding: '2px 8px',
|
||||
borderWidth: 0.5
|
||||
}}>
|
||||
<i className="iconfont icon-history" style={{ marginRight: '3px' }} />
|
||||
{assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT}
|
||||
<Divider type="vertical" style={{ marginTop: 2, marginLeft: 5, marginRight: 5 }} />↑
|
||||
{`${inputTokenCount} / ${estimateTokenCount}`}
|
||||
<Divider type="vertical" style={{ marginTop: 2, marginLeft: 5, marginRight: 5 }} />↑{inputTokenCount}
|
||||
<span style={{ margin: '0 2px' }}>/</span>
|
||||
{estimateTokenCount}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
</TextCount>
|
||||
@@ -303,7 +306,6 @@ const TextCount = styled.div`
|
||||
padding: 2px;
|
||||
border-top-left-radius: 7px;
|
||||
user-select: none;
|
||||
margin-right: 10px;
|
||||
`
|
||||
|
||||
export default Inputbar
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { CheckOutlined, CopyOutlined } from '@ant-design/icons'
|
||||
import { CheckOutlined } from '@ant-design/icons'
|
||||
import CopyIcon from '@renderer/components/Icons/CopyIcon'
|
||||
import { initMermaid } from '@renderer/init'
|
||||
import { useTheme } from '@renderer/providers/ThemeProvider'
|
||||
import { ThemeMode } from '@renderer/store/settings'
|
||||
@@ -39,7 +40,7 @@ const CodeBlock: React.FC<CodeBlockProps> = ({ children, className, ...rest }) =
|
||||
<div>
|
||||
<CodeHeader>
|
||||
<CodeLanguage>{'<' + match[1].toUpperCase() + '>'}</CodeLanguage>
|
||||
{!copied && <CopyOutlined className="copy" onClick={onCopy} />}
|
||||
{!copied && <CopyIcon className="copy" onClick={onCopy} />}
|
||||
{copied && <CheckOutlined style={{ color: 'var(--color-primary)' }} />}
|
||||
</CodeHeader>
|
||||
<SyntaxHighlighter
|
||||
|
||||
@@ -2,7 +2,7 @@ import { getModelLogo } from '@renderer/config/provider'
|
||||
import { useProviders } from '@renderer/hooks/useProvider'
|
||||
import { Model } from '@renderer/types'
|
||||
import { Avatar, Dropdown, DropdownProps, MenuProps } from 'antd'
|
||||
import { first, sortBy, upperFirst } from 'lodash'
|
||||
import { first, reverse, sortBy, upperFirst } from 'lodash'
|
||||
import { FC, PropsWithChildren } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
@@ -22,7 +22,7 @@ const SelectModelDropdown: FC<Props & PropsWithChildren> = ({ children, model, o
|
||||
key: p.id,
|
||||
label: p.isSystem ? t(`provider.${p.id}`) : p.name,
|
||||
type: 'group',
|
||||
children: sortBy(p.models, 'name').map((m) => ({
|
||||
children: reverse(sortBy(p.models, 'name')).map((m) => ({
|
||||
key: m?.id,
|
||||
label: upperFirst(m?.name),
|
||||
defaultSelectedKeys: [model?.id],
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import MinApp from '@renderer/components/MinApp'
|
||||
import { MinAppType, Provider } from '@renderer/types'
|
||||
import { Button } from 'antd'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { SettingSubtitle } from '..'
|
||||
|
||||
interface Props {
|
||||
provider: Provider
|
||||
}
|
||||
|
||||
const GraphRAGSettings: FC<Props> = ({ provider }) => {
|
||||
const apiUrl = provider.apiHost
|
||||
const modalId = provider.models.filter((model) => model.id.includes('global'))[0]?.id
|
||||
const { t } = useTranslation()
|
||||
|
||||
const onShowGraphRAG = async () => {
|
||||
const { appPath } = await window.api.getAppInfo()
|
||||
const url = `file://${appPath}/resources/graphrag.html?apiUrl=${apiUrl}&modelId=${modalId}`
|
||||
|
||||
const app: MinAppType = {
|
||||
name: t('words.knowledgeGraph'),
|
||||
logo: '',
|
||||
url
|
||||
}
|
||||
|
||||
MinApp.start(app)
|
||||
}
|
||||
|
||||
if (!modalId) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<SettingSubtitle>{t('words.knowledgeGraph')}</SettingSubtitle>
|
||||
<Button style={{ marginTop: 10 }} onClick={onShowGraphRAG}>
|
||||
{t('words.visualization')}
|
||||
</Button>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div``
|
||||
|
||||
export default GraphRAGSettings
|
||||
@@ -4,8 +4,7 @@ import { FC, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { SettingSubtitle } from '..'
|
||||
import { HelpText, HelpTextRow } from '../ProviderSettings/ProviderSetting'
|
||||
import { SettingHelpText, SettingHelpTextRow, SettingSubtitle } from '..'
|
||||
|
||||
const OllamSettings: FC = () => {
|
||||
const { keepAliveTime, setKeepAliveTime } = useOllamaSettings()
|
||||
@@ -14,7 +13,7 @@ const OllamSettings: FC = () => {
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<SettingSubtitle>{t('ollama.keep_alive_time.title')}</SettingSubtitle>
|
||||
<SettingSubtitle style={{ marginBottom: 5 }}>{t('ollama.keep_alive_time.title')}</SettingSubtitle>
|
||||
<InputNumber
|
||||
style={{ width: '100%' }}
|
||||
value={keepAliveMinutes}
|
||||
@@ -23,9 +22,9 @@ const OllamSettings: FC = () => {
|
||||
suffix={t('ollama.keep_alive_time.placeholder')}
|
||||
step={5}
|
||||
/>
|
||||
<HelpTextRow>
|
||||
<HelpText>{t('ollama.keep_alive_time.description')}</HelpText>
|
||||
</HelpTextRow>
|
||||
<SettingHelpTextRow>
|
||||
<SettingHelpText>{t('ollama.keep_alive_time.description')}</SettingHelpText>
|
||||
</SettingHelpTextRow>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -19,9 +19,17 @@ import { FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { SettingContainer, SettingSubtitle, SettingTitle } from '..'
|
||||
import {
|
||||
SettingContainer,
|
||||
SettingHelpLink,
|
||||
SettingHelpText,
|
||||
SettingHelpTextRow,
|
||||
SettingSubtitle,
|
||||
SettingTitle
|
||||
} from '..'
|
||||
import AddModelPopup from './AddModelPopup'
|
||||
import EditModelsPopup from './EditModelsPopup'
|
||||
import GraphRAGSettings from './GraphRAGSettings'
|
||||
import OllamSettings from './OllamaSettings'
|
||||
|
||||
interface Props {
|
||||
@@ -110,11 +118,11 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
||||
</Button>
|
||||
</Space.Compact>
|
||||
{apiKeyWebsite && (
|
||||
<HelpTextRow>
|
||||
<HelpLink target="_blank" href={apiKeyWebsite}>
|
||||
<SettingHelpTextRow>
|
||||
<SettingHelpLink target="_blank" href={apiKeyWebsite}>
|
||||
{t('settings.provider.get_api_key')}
|
||||
</HelpLink>
|
||||
</HelpTextRow>
|
||||
</SettingHelpLink>
|
||||
</SettingHelpTextRow>
|
||||
)}
|
||||
<SettingSubtitle>{t('settings.provider.api_host')}</SettingSubtitle>
|
||||
<Space.Compact style={{ width: '100%', marginTop: 5 }}>
|
||||
@@ -128,6 +136,9 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
||||
{apiEditable && <Button onClick={onReset}>{t('settings.provider.api.url.reset')}</Button>}
|
||||
</Space.Compact>
|
||||
{provider.id === 'ollama' && <OllamSettings />}
|
||||
{provider.id === 'graphrag-kylin-mountain' && provider.models.length > 0 && (
|
||||
<GraphRAGSettings provider={provider} />
|
||||
)}
|
||||
<SettingSubtitle style={{ marginBottom: 5 }}>{t('common.models')}</SettingSubtitle>
|
||||
{Object.keys(modelGroups).map((group) => (
|
||||
<Card key={group} type="inner" title={group} style={{ marginBottom: '10px' }} size="small">
|
||||
@@ -145,18 +156,18 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
||||
</Card>
|
||||
))}
|
||||
{docsWebsite && (
|
||||
<HelpTextRow>
|
||||
<HelpText>{t('settings.provider.docs_check')} </HelpText>
|
||||
<HelpLink target="_blank" href={docsWebsite}>
|
||||
<SettingHelpTextRow>
|
||||
<SettingHelpText>{t('settings.provider.docs_check')} </SettingHelpText>
|
||||
<SettingHelpLink target="_blank" href={docsWebsite}>
|
||||
{t(`provider.${provider.id}`) + ' '}
|
||||
{t('common.docs')}
|
||||
</HelpLink>
|
||||
<HelpText>{t('common.and')}</HelpText>
|
||||
<HelpLink target="_blank" href={modelsWebsite}>
|
||||
</SettingHelpLink>
|
||||
<SettingHelpText>{t('common.and')}</SettingHelpText>
|
||||
<SettingHelpLink target="_blank" href={modelsWebsite}>
|
||||
{t('common.models')}
|
||||
</HelpLink>
|
||||
<HelpText>{t('settings.provider.docs_more_details')}</HelpText>
|
||||
</HelpTextRow>
|
||||
</SettingHelpLink>
|
||||
<SettingHelpText>{t('settings.provider.docs_more_details')}</SettingHelpText>
|
||||
</SettingHelpTextRow>
|
||||
)}
|
||||
<Flex gap={10} style={{ marginTop: '10px' }}>
|
||||
<Button type="primary" onClick={onManageModel} icon={<EditOutlined />}>
|
||||
@@ -184,24 +195,6 @@ const ModelListHeader = styled.div`
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
export const HelpTextRow = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 5px 0;
|
||||
`
|
||||
|
||||
export const HelpText = styled.div`
|
||||
font-size: 11px;
|
||||
color: var(--color-text);
|
||||
opacity: 0.4;
|
||||
`
|
||||
|
||||
const HelpLink = styled(Link)`
|
||||
font-size: 11px;
|
||||
padding: 0 5px;
|
||||
`
|
||||
|
||||
const RemoveIcon = styled(MinusCircleOutlined)`
|
||||
font-size: 18px;
|
||||
margin-left: 10px;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Divider } from 'antd'
|
||||
import Link from 'antd/es/typography/Link'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import SettingsPage from './SettingsPage'
|
||||
@@ -51,4 +52,22 @@ export const SettingRowTitle = styled.div`
|
||||
color: var(--color-text-1);
|
||||
`
|
||||
|
||||
export const SettingHelpTextRow = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 5px 0;
|
||||
`
|
||||
|
||||
export const SettingHelpText = styled.div`
|
||||
font-size: 11px;
|
||||
color: var(--color-text);
|
||||
opacity: 0.4;
|
||||
`
|
||||
|
||||
export const SettingHelpLink = styled(Link)`
|
||||
font-size: 11px;
|
||||
padding: 0 5px;
|
||||
`
|
||||
|
||||
export default SettingsPage
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import {
|
||||
CheckOutlined,
|
||||
CopyOutlined,
|
||||
SendOutlined,
|
||||
SettingOutlined,
|
||||
SwapOutlined,
|
||||
WarningOutlined
|
||||
} from '@ant-design/icons'
|
||||
import { CheckOutlined, SendOutlined, SettingOutlined, SwapOutlined, WarningOutlined } from '@ant-design/icons'
|
||||
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
||||
import CopyIcon from '@renderer/components/Icons/CopyIcon'
|
||||
import { useDefaultModel } from '@renderer/hooks/useAssistant'
|
||||
import { fetchTranslate } from '@renderer/services/api'
|
||||
import { getDefaultAssistant } from '@renderer/services/assistant'
|
||||
@@ -211,7 +205,7 @@ const TranslatePage: FC = () => {
|
||||
<CopyButton
|
||||
onClick={onCopy}
|
||||
disabled={!result}
|
||||
icon={copied ? <CheckOutlined style={{ color: 'var(--color-primary)' }} /> : <CopyOutlined />}
|
||||
icon={copied ? <CheckOutlined style={{ color: 'var(--color-primary)' }} /> : <CopyIcon />}
|
||||
/>
|
||||
</OutputContainer>
|
||||
</TranslateInputWrapper>
|
||||
|
||||
@@ -22,7 +22,7 @@ const persistedReducer = persistReducer(
|
||||
{
|
||||
key: 'cherry-studio',
|
||||
storage,
|
||||
version: 21,
|
||||
version: 22,
|
||||
blacklist: ['runtime'],
|
||||
migrate
|
||||
},
|
||||
|
||||
@@ -121,6 +121,15 @@ const initialState: LlmState = {
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
id: 'minimax',
|
||||
name: 'MiniMax',
|
||||
apiKey: '',
|
||||
apiHost: 'https://api.minimax.chat/v1/',
|
||||
models: SYSTEM_MODELS.minimax.filter((m) => m.enabled),
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
id: 'aihubmix',
|
||||
name: 'AiHubMix',
|
||||
|
||||
@@ -343,6 +343,26 @@ const migrateConfig = {
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
'22': (state: RootState) => {
|
||||
return {
|
||||
...state,
|
||||
llm: {
|
||||
...state.llm,
|
||||
providers: [
|
||||
...state.llm.providers,
|
||||
{
|
||||
id: 'minimax',
|
||||
name: 'MiniMax',
|
||||
apiKey: '',
|
||||
apiHost: 'https://api.minimax.chat/v1/',
|
||||
models: SYSTEM_MODELS.minimax.filter((m) => m.enabled),
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -75,3 +75,9 @@ export type Agent = {
|
||||
export type Suggestion = {
|
||||
content: string
|
||||
}
|
||||
|
||||
export type MinAppType = {
|
||||
name: string
|
||||
logo: string
|
||||
url: string
|
||||
}
|
||||
|
||||