feat: add minapp window
This commit is contained in:
+3
-1
@@ -49,7 +49,9 @@ app.whenReady().then(() => {
|
||||
|
||||
ipcMain.handle('save-file', saveFile)
|
||||
|
||||
ipcMain.handle('minapp', (_, url: string) => createMinappWindow(url))
|
||||
ipcMain.handle('minapp', (_, args) => {
|
||||
createMinappWindow(args)
|
||||
})
|
||||
|
||||
ipcMain.handle('set-theme', (_, theme: 'light' | 'dark') => {
|
||||
appConfig.set('theme', theme)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
+18
-5
@@ -5,6 +5,7 @@ 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
|
||||
@@ -76,7 +77,13 @@ export function createMainWindow() {
|
||||
return mainWindow
|
||||
}
|
||||
|
||||
export function createMinappWindow(url) {
|
||||
export function createMinappWindow({
|
||||
url,
|
||||
windowOptions
|
||||
}: {
|
||||
url: string
|
||||
windowOptions?: Electron.BrowserWindowConstructorOptions
|
||||
}) {
|
||||
const width = 500
|
||||
const height = 800
|
||||
const headerHeight = 40
|
||||
@@ -88,20 +95,26 @@ export function createMinappWindow(url) {
|
||||
alwaysOnTop: true,
|
||||
titleBarOverlay: titleBarOverlayDark,
|
||||
titleBarStyle: 'hidden',
|
||||
...windowOptions,
|
||||
webPreferences: {
|
||||
preload: join(__dirname, '../preload/minapp.js'),
|
||||
sandbox: false
|
||||
}
|
||||
})
|
||||
|
||||
minappWindow.loadFile(app.getAppPath() + '/resources/minapp.html')
|
||||
|
||||
const view = new BrowserView()
|
||||
|
||||
minappWindow.setBrowserView(view)
|
||||
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,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { CloseOutlined, ExportOutlined, ReloadOutlined } from '@ant-design/icons'
|
||||
import { useBridge } from '@renderer/hooks/useBridge'
|
||||
import store from '@renderer/store'
|
||||
import { setMinappShow } from '@renderer/store/runtime'
|
||||
import { Drawer } from 'antd'
|
||||
@@ -20,6 +21,8 @@ const PopupContainer: React.FC<Props> = ({ title, url, resolve }) => {
|
||||
const [open, setOpen] = useState(true)
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null)
|
||||
|
||||
useBridge()
|
||||
|
||||
const canOpenExternalLink = url.startsWith('http://') || url.startsWith('https://')
|
||||
|
||||
const onClose = () => {
|
||||
|
||||
@@ -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,7 +24,7 @@ const Sidebar: FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Container style={{ backgroundColor: minappShow ? 'var(--color-background)' : sidebarBackgroundColor }}>
|
||||
<Container style={{ backgroundColor: minappShow ? 'var(--navbar-background)' : sidebarBackgroundColor }}>
|
||||
<AvatarImg src={avatar || Logo} draggable={false} className="dragdisable" onClick={onEditUser} />
|
||||
<MainMenus>
|
||||
<Menus>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}, [])
|
||||
}
|
||||
@@ -220,6 +220,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'
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -441,6 +445,10 @@ const resources = {
|
||||
},
|
||||
error: {
|
||||
'chat.response': '出错了,如果没有配置 API 密钥,请前往设置 > 模型提供商中配置密钥'
|
||||
},
|
||||
words: {
|
||||
knowledgeGraph: '知识图谱',
|
||||
visualization: '可视化'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import MinApp from '@renderer/components/MinApp'
|
||||
import { 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[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}`
|
||||
MinApp.start({ url, title: t('words.knowledgeGraph') })
|
||||
}
|
||||
|
||||
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
|
||||
@@ -14,7 +14,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}
|
||||
|
||||
@@ -22,6 +22,7 @@ import styled from 'styled-components'
|
||||
import { SettingContainer, SettingSubtitle, SettingTitle } from '..'
|
||||
import AddModelPopup from './AddModelPopup'
|
||||
import EditModelsPopup from './EditModelsPopup'
|
||||
import GraphRAGSettings from './GraphRAGSettings'
|
||||
import OllamSettings from './OllamaSettings'
|
||||
|
||||
interface Props {
|
||||
@@ -128,6 +129,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">
|
||||
|
||||
Reference in New Issue
Block a user