feat: add minapp window

This commit is contained in:
kangfenmao
2024-08-17 17:11:48 +08:00
parent 0567a4f505
commit 6051016684
15 changed files with 284 additions and 22 deletions
+3 -1
View File
@@ -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)
+32
View 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()
}
+18 -5
View File
@@ -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 = () => {
+1 -1
View File
@@ -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 }}>
+1 -1
View File
@@ -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>
+51
View 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)
}
}, [])
}
+8
View File
@@ -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">