feat: MinApp tabs on sidebar, we can keep MinApps alive and re-open it without loading again.

This commit is contained in:
fullex 2025-03-27 13:16:08 +08:00 committed by 亢奋猫
parent 433d562599
commit 57ba91072d
23 changed files with 856 additions and 335 deletions

View File

@ -0,0 +1,369 @@
import {
CloseOutlined,
CodeOutlined,
ExportOutlined,
MinusOutlined,
PushpinOutlined,
ReloadOutlined
} from '@ant-design/icons'
import { isMac, isWindows } from '@renderer/config/constant'
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
import { useBridge } from '@renderer/hooks/useBridge'
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
import { useMinapps } from '@renderer/hooks/useMinapps'
import { useRuntime } from '@renderer/hooks/useRuntime'
import { MinAppType } from '@renderer/types'
import { delay } from '@renderer/utils'
import { Avatar, Drawer, Tooltip } from 'antd'
import { WebviewTag } from 'electron'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import BeatLoader from 'react-spinners/BeatLoader'
import styled from 'styled-components'
import WebviewContainer from './WebviewContainer'
interface AppExtraInfo {
canPinned: boolean
isPinned: boolean
canOpenExternalLink: boolean
}
type AppInfo = MinAppType & AppExtraInfo
/** The main container for MinApp popup */
const MinappPopupContainer: React.FC = () => {
const { openedKeepAliveMinapps, openedOneOffMinapp, currentMinappId, minappShow } = useRuntime()
const { closeMinapp, hideMinappPopup } = useMinappPopup()
const { pinned, updatePinnedMinapps } = useMinapps()
const { t } = useTranslation()
/** control the drawer open or close */
const [isPopupShow, setIsPopupShow] = useState(true)
/** whether the current minapp is ready */
const [isReady, setIsReady] = useState(false)
/** store the last minapp id and show status */
const lastMinappId = useRef<string | null>(null)
const lastMinappShow = useRef<boolean>(false)
/** store the webview refs, one of the key to make them keepalive */
const webviewRefs = useRef<Map<string, WebviewTag | null>>(new Map())
/** indicate whether the webview has loaded */
const webviewLoadedRefs = useRef<Map<string, boolean>>(new Map())
const isInDevelopment = process.env.NODE_ENV === 'development'
useBridge()
/** set the popup display status */
useEffect(() => {
if (minappShow) {
setIsPopupShow(true)
if (webviewLoadedRefs.current.get(currentMinappId)) {
setIsReady(true)
/** the case that open the minapp from sidebar */
} else if (lastMinappId.current !== currentMinappId && lastMinappShow.current === minappShow) {
setIsReady(false)
}
} else {
setIsPopupShow(false)
setIsReady(false)
}
return () => {
/** renew the last minapp id and show status */
lastMinappId.current = currentMinappId
lastMinappShow.current = minappShow
}
}, [minappShow, currentMinappId])
useEffect(() => {
if (!webviewRefs.current) return
/** set the webview display status
* DO NOT use the state to set the display status,
* to AVOID the re-render of the webview container
*/
webviewRefs.current.forEach((webviewRef, appid) => {
if (!webviewRef) return
webviewRef.style.display = appid === currentMinappId ? 'inline-flex' : 'none'
})
//delete the extra webviewLoadedRefs
webviewLoadedRefs.current.forEach((_, appid) => {
if (!webviewRefs.current.has(appid)) {
webviewLoadedRefs.current.delete(appid)
}
})
}, [currentMinappId])
/** combine the openedKeepAliveMinapps and openedOneOffMinapp */
const combinedApps = useMemo(() => {
return [...openedKeepAliveMinapps, ...(openedOneOffMinapp ? [openedOneOffMinapp] : [])]
}, [openedKeepAliveMinapps, openedOneOffMinapp])
/** get the extra info of the apps */
const appsExtraInfo = useMemo(() => {
return combinedApps.reduce(
(acc, app) => ({
...acc,
[app.id]: {
canPinned: DEFAULT_MIN_APPS.some((item) => item.id === app.id),
isPinned: pinned.some((item) => item.id === app.id),
canOpenExternalLink: app.url.startsWith('http://') || app.url.startsWith('https://')
}
}),
{} as Record<string, AppExtraInfo>
)
}, [combinedApps, pinned])
/** get the current app info with extra info */
let currentAppInfo: AppInfo | null = null
if (currentMinappId) {
const currentApp = combinedApps.find((item) => item.id === currentMinappId) as MinAppType
currentAppInfo = { ...currentApp, ...appsExtraInfo[currentApp.id] }
}
/** will close the popup and delete the webview */
const handlePopupClose = async (appid: string) => {
setIsPopupShow(false)
await delay(0.3)
webviewLoadedRefs.current.delete(appid)
closeMinapp(appid)
}
/** will hide the popup and remain the webviews */
const handlePopupMinimize = async () => {
setIsPopupShow(false)
await delay(0.3)
hideMinappPopup()
}
/** the callback function to set the webviews ref */
const handleWebviewSetRef = (appid: string, element: WebviewTag | null) => {
webviewRefs.current.set(appid, element)
if (!webviewRefs.current.has(appid)) {
webviewRefs.current.set(appid, null)
return
}
if (element) {
webviewRefs.current.set(appid, element)
} else {
webviewRefs.current.delete(appid)
}
}
/** the callback function to set the webviews loaded indicator */
const handleWebviewLoaded = (appid: string) => {
webviewLoadedRefs.current.set(appid, true)
if (appid == currentMinappId) {
setTimeout(() => setIsReady(true), 200)
}
}
/** will open the devtools of the minapp */
const handleOpenDevTools = (appid: string) => {
const webview = webviewRefs.current.get(appid)
if (webview) {
webview.openDevTools()
}
}
/** only reload the original url */
const handleReload = (appid: string) => {
const webview = webviewRefs.current.get(appid)
if (webview) {
const url = combinedApps.find((item) => item.id === appid)?.url
if (url) {
webview.src = url
}
}
}
/** only open the current url */
const handleOpenLink = (appid: string) => {
const webview = webviewRefs.current.get(appid)
if (webview) {
window.api.openWebsite(webview.getURL())
}
}
/** toggle the pin status of the minapp */
const handleTogglePin = (appid: string) => {
const app = combinedApps.find((item) => item.id === appid)
if (!app) return
const newPinned = appsExtraInfo[appid].isPinned ? pinned.filter((item) => item.id !== appid) : [...pinned, app]
updatePinnedMinapps(newPinned)
}
/** Title bar of the popup */
const Title = ({ appInfo }: { appInfo: AppInfo | null }) => {
if (!appInfo) return null
return (
<TitleContainer style={{ justifyContent: 'space-between' }}>
<TitleText>{appInfo.name}</TitleText>
<ButtonsGroup className={isWindows ? 'windows' : ''}>
<Tooltip title={t('minapp.popup.refresh')} mouseEnterDelay={0.8} placement="bottom">
<Button onClick={() => handleReload(appInfo.id)}>
<ReloadOutlined />
</Button>
</Tooltip>
{appInfo.canPinned && (
<Tooltip
title={appInfo.isPinned ? t('minapp.sidebar.remove.title') : t('minapp.sidebar.add.title')}
mouseEnterDelay={0.8}
placement="bottom">
<Button onClick={() => handleTogglePin(appInfo.id)} className={appInfo.isPinned ? 'pinned' : ''}>
<PushpinOutlined style={{ fontSize: 16 }} />
</Button>
</Tooltip>
)}
{appInfo.canOpenExternalLink && (
<Tooltip title={t('minapp.popup.openExternal')} mouseEnterDelay={0.8} placement="bottom">
<Button onClick={() => handleOpenLink(appInfo.id)}>
<ExportOutlined />
</Button>
</Tooltip>
)}
{isInDevelopment && (
<Tooltip title={t('minapp.popup.devtools')} mouseEnterDelay={0.8} placement="bottom">
<Button onClick={() => handleOpenDevTools(appInfo.id)}>
<CodeOutlined />
</Button>
</Tooltip>
)}
<Tooltip title={t('minapp.popup.minimize')} mouseEnterDelay={0.8} placement="bottom">
<Button onClick={() => handlePopupMinimize()}>
<MinusOutlined />
</Button>
</Tooltip>
<Tooltip title={t('minapp.popup.close')} mouseEnterDelay={0.8} placement="bottom">
<Button onClick={() => handlePopupClose(appInfo.id)}>
<CloseOutlined />
</Button>
</Tooltip>
</ButtonsGroup>
</TitleContainer>
)
}
/** group the webview containers with Memo, one of the key to make them keepalive */
const WebviewContainerGroup = useMemo(() => {
return combinedApps.map((app) => (
<WebviewContainer
key={app.id}
appid={app.id}
url={app.url}
onSetRefCallback={handleWebviewSetRef}
onLoadedCallback={handleWebviewLoaded}
/>
))
// because the combinedApps is enough
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [combinedApps])
return (
<Drawer
title={<Title appInfo={currentAppInfo} />}
placement="bottom"
onClose={handlePopupMinimize}
open={isPopupShow}
destroyOnClose={false}
mask={false}
rootClassName="minapp-drawer"
maskClassName="minapp-mask"
height={'100%'}
maskClosable={false}
closeIcon={null}
style={{ marginLeft: 'var(--sidebar-width)' }}>
{!isReady && (
<EmptyView>
<Avatar
src={currentAppInfo?.logo}
size={80}
style={{ border: '1px solid var(--color-border)', marginTop: -150 }}
/>
<BeatLoader color="var(--color-text-2)" size="10px" style={{ marginTop: 15 }} />
</EmptyView>
)}
{WebviewContainerGroup}
</Drawer>
)
}
const TitleContainer = styled.div`
display: flex;
flex-direction: row;
align-items: center;
padding-left: ${isMac ? '20px' : '10px'};
padding-right: 10px;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: transparent;
`
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`
display: flex;
flex-direction: row;
align-items: center;
gap: 5px;
-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`
cursor: pointer;
width: 30px;
height: 30px;
border-radius: 5px;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
color: var(--color-text-2);
transition: all 0.2s ease;
font-size: 14px;
&:hover {
color: var(--color-text-1);
background-color: var(--color-background-mute);
}
&.pinned {
color: var(--color-primary);
background-color: var(--color-primary-bg);
}
`
const EmptyView = styled.div`
display: flex;
flex: 1;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
background-color: var(--color-background);
`
export default MinappPopupContainer

View File

@ -0,0 +1,11 @@
import MinappPopupContainer from '@renderer/components/MinApp/MinappPopupContainer'
import { useRuntime } from '@renderer/hooks/useRuntime'
const TopViewMinappContainer = () => {
const { openedKeepAliveMinapps, openedOneOffMinapp } = useRuntime()
const isCreate = openedKeepAliveMinapps.length > 0 || openedOneOffMinapp !== null
return <>{isCreate && <MinappPopupContainer />}</>
}
export default TopViewMinappContainer

View File

@ -0,0 +1,84 @@
import { WebviewTag } from 'electron'
import { memo, useEffect, useRef } from 'react'
/**
* WebviewContainer is a component that renders a webview element.
* It is used in the MinAppPopupContainer component.
* The webcontent can be remain in memory
*/
const WebviewContainer = memo(
({
appid,
url,
onSetRefCallback,
onLoadedCallback
}: {
appid: string
url: string
onSetRefCallback: (appid: string, element: WebviewTag | null) => void
onLoadedCallback: (appid: string) => void
}) => {
const webviewRef = useRef<WebviewTag | null>(null)
const setRef = (appid: string) => {
onSetRefCallback(appid, null)
return (element: WebviewTag | null) => {
onSetRefCallback(appid, element)
if (element) {
webviewRef.current = element
} else {
webviewRef.current = null
}
}
}
useEffect(() => {
if (!webviewRef.current) return
const handleNewWindow = (event: any) => {
event.preventDefault()
if (webviewRef.current?.loadURL) {
webviewRef.current.loadURL(event.url)
}
}
const handleLoaded = () => {
onLoadedCallback(appid)
}
webviewRef.current.addEventListener('new-window', handleNewWindow)
webviewRef.current.addEventListener('did-finish-load', handleLoaded)
// we set the url when the webview is ready
webviewRef.current.src = url
return () => {
webviewRef.current?.removeEventListener('new-window', handleNewWindow)
webviewRef.current?.removeEventListener('did-finish-load', handleLoaded)
}
// because the appid and url are enough, no need to add onLoadedCallback
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [appid, url])
return (
<webview
key={appid}
ref={setRef(appid)}
style={WebviewStyle}
allowpopups={'true' as any}
partition={`persist:webview-${appid}`}
nodeintegration={'true' as any}
/>
)
}
)
const WebviewStyle: React.CSSProperties = {
width: 'calc(100vw - var(--sidebar-width))',
height: 'calc(100vh - var(--navbar-height))',
backgroundColor: 'white',
display: 'inline-flex'
}
export default WebviewContainer

View File

@ -1,282 +0,0 @@
import { CloseOutlined, CodeOutlined, ExportOutlined, PushpinOutlined, ReloadOutlined } from '@ant-design/icons'
import { isMac, isWindows } from '@renderer/config/constant'
import { AppLogo } from '@renderer/config/env'
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
import { useBridge } from '@renderer/hooks/useBridge'
import { useMinapps } from '@renderer/hooks/useMinapps'
import store from '@renderer/store'
import { setMinappShow } from '@renderer/store/runtime'
import { MinAppType } from '@renderer/types'
import { delay } from '@renderer/utils'
import { Avatar, Drawer } from 'antd'
import { WebviewTag } from 'electron'
import { useEffect, useRef, useState } from 'react'
import BeatLoader from 'react-spinners/BeatLoader'
import styled from 'styled-components'
import { TopView } from '../TopView'
interface Props {
app: MinAppType
resolve: (data: any) => void
}
const PopupContainer: React.FC<Props> = ({ app, resolve }) => {
const { pinned, updatePinnedMinapps } = useMinapps()
const isPinned = pinned.some((p) => p.id === app.id)
const [open, setOpen] = useState(true)
const [opened, setOpened] = useState(false)
const [isReady, setIsReady] = useState(false)
const webviewRef = useRef<WebviewTag | null>(null)
useBridge()
const canOpenExternalLink = app.url.startsWith('http://') || app.url.startsWith('https://')
const canPinned = DEFAULT_MIN_APPS.some((i) => i.id === app?.id)
const onClose = async (_delay = 0.3) => {
setOpen(false)
await delay(_delay)
resolve({})
}
MinApp.onClose = onClose
const openDevTools = () => {
if (webviewRef.current) {
webviewRef.current.openDevTools()
}
}
const onReload = () => {
if (webviewRef.current) {
webviewRef.current.src = app.url
}
}
const onOpenLink = () => {
if (webviewRef.current) {
const currentUrl = webviewRef.current.getURL()
window.api.openWebsite(currentUrl)
}
}
const onTogglePin = () => {
const newPinned = isPinned ? pinned.filter((item) => item.id !== app.id) : [...pinned, app]
updatePinnedMinapps(newPinned)
}
const isInDevelopment = process.env.NODE_ENV === 'development'
const Title = () => {
return (
<TitleContainer style={{ justifyContent: 'space-between' }}>
<TitleText>{app.name}</TitleText>
<ButtonsGroup className={isWindows ? 'windows' : ''}>
<Button onClick={onReload}>
<ReloadOutlined />
</Button>
{canPinned && (
<Button onClick={onTogglePin} className={isPinned ? 'pinned' : ''}>
<PushpinOutlined style={{ fontSize: 16 }} />
</Button>
)}
{canOpenExternalLink && (
<Button onClick={onOpenLink}>
<ExportOutlined />
</Button>
)}
{isInDevelopment && (
<Button onClick={openDevTools}>
<CodeOutlined />
</Button>
)}
<Button onClick={() => onClose()}>
<CloseOutlined />
</Button>
</ButtonsGroup>
</TitleContainer>
)
}
useEffect(() => {
const webview = webviewRef.current
if (webview) {
const handleNewWindow = (event: any) => {
event.preventDefault()
if (webview.loadURL) {
webview.loadURL(event.url)
}
}
const onLoaded = () => setIsReady(true)
webview.addEventListener('new-window', handleNewWindow)
webview.addEventListener('did-finish-load', onLoaded)
return () => {
webview.removeEventListener('new-window', handleNewWindow)
webview.removeEventListener('did-finish-load', onLoaded)
}
}
return () => {}
}, [opened])
useEffect(() => {
setTimeout(() => setOpened(true), 350)
}, [])
return (
<Drawer
title={<Title />}
placement="bottom"
onClose={() => onClose()}
open={open}
mask={true}
rootClassName="minapp-drawer"
maskClassName="minapp-mask"
height={'100%'}
maskClosable={false}
closeIcon={null}
style={{ marginLeft: 'var(--sidebar-width)' }}>
{!isReady && (
<EmptyView>
<Avatar src={app.logo} size={80} style={{ border: '1px solid var(--color-border)', marginTop: -150 }} />
<BeatLoader color="var(--color-text-2)" size="10" style={{ marginTop: 15 }} />
</EmptyView>
)}
{opened && (
<webview
src={app.url}
ref={webviewRef}
style={WebviewStyle}
allowpopups={'true' as any}
partition="persist:webview"
nodeintegration={true}
/>
)}
</Drawer>
)
}
const WebviewStyle: React.CSSProperties = {
width: 'calc(100vw - var(--sidebar-width))',
height: 'calc(100vh - var(--navbar-height))',
backgroundColor: 'white',
display: 'inline-flex'
}
const TitleContainer = styled.div`
display: flex;
flex-direction: row;
align-items: center;
padding-left: ${isMac ? '20px' : '10px'};
padding-right: 10px;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: transparent;
`
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`
display: flex;
flex-direction: row;
align-items: center;
gap: 5px;
-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`
cursor: pointer;
width: 30px;
height: 30px;
border-radius: 5px;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
color: var(--color-text-2);
transition: all 0.2s ease;
font-size: 14px;
&:hover {
color: var(--color-text-1);
background-color: var(--color-background-mute);
}
&.pinned {
color: var(--color-primary);
background-color: var(--color-primary-bg);
}
`
const EmptyView = styled.div`
display: flex;
flex: 1;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
background-color: var(--color-background);
`
export default class MinApp {
static topviewId = 0
static onClose = () => {}
static app: MinAppType | null = null
static async start(app: MinAppType) {
if (app?.id && MinApp.app?.id === app?.id) {
return
}
if (MinApp.app) {
// @ts-ignore delay params
await MinApp.onClose(0)
await delay(0)
}
if (!app.logo) {
app.logo = AppLogo
}
MinApp.app = app
store.dispatch(setMinappShow(true))
return new Promise<any>((resolve) => {
TopView.show(
<PopupContainer
app={app}
resolve={(v) => {
resolve(v)
this.close()
}}
/>,
'MinApp'
)
})
}
static close() {
TopView.hide('MinApp')
store.dispatch(setMinappShow(false))
MinApp.app = null
MinApp.onClose = () => {}
}
}

View File

@ -1,3 +1,4 @@
import TopViewMinappContainer from '@renderer/components/MinApp/TopViewMinappContainer'
import { useAppInit } from '@renderer/hooks/useAppInit'
import { message, Modal } from 'antd'
import React, { PropsWithChildren, useCallback, useEffect, useRef, useState } from 'react'
@ -76,6 +77,7 @@ const TopViewContainer: React.FC<Props> = ({ children }) => {
{children}
{messageContextHolder}
{modalContextHolder}
<TopViewMinappContainer />
{elements.map(({ element: Element, id }) => (
<FullScreenContainer key={`TOPVIEW_${id}`}>
{typeof Element === 'function' ? <Element /> : Element}

View File

@ -9,35 +9,36 @@ import { isMac } from '@renderer/config/constant'
import { AppLogo, UserAvatar } from '@renderer/config/env'
import { useTheme } from '@renderer/context/ThemeProvider'
import useAvatar from '@renderer/hooks/useAvatar'
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
import { useMinapps } from '@renderer/hooks/useMinapps'
import useNavBackgroundColor from '@renderer/hooks/useNavBackgroundColor'
import { modelGenerating, useRuntime } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings'
import { isEmoji } from '@renderer/utils'
import type { MenuProps } from 'antd'
import { Tooltip } from 'antd'
import { Avatar } from 'antd'
import { Dropdown } from 'antd'
import { FC } from 'react'
import { Avatar, Dropdown, Tooltip } from 'antd'
import { FC, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { useLocation, useNavigate } from 'react-router-dom'
import styled from 'styled-components'
import DragableList from '../DragableList'
import MinAppIcon from '../Icons/MinAppIcon'
import MinApp from '../MinApp'
import UserPopup from '../Popups/UserPopup'
const Sidebar: FC = () => {
const { pathname } = useLocation()
const avatar = useAvatar()
const { minappShow } = useRuntime()
const { t } = useTranslation()
const navigate = useNavigate()
const { hideMinappPopup, openMinapp } = useMinappPopup()
const { minappShow, currentMinappId } = useRuntime()
const { sidebarIcons } = useSettings()
const { theme, settingTheme, toggleTheme } = useTheme()
const { pinned } = useMinapps()
const { pathname } = useLocation()
const navigate = useNavigate()
const { theme, settingTheme, toggleTheme } = useTheme()
const avatar = useAvatar()
const { t } = useTranslation()
const onEditUser = () => UserPopup.show()
const backgroundColor = useNavBackgroundColor()
@ -49,9 +50,10 @@ const Sidebar: FC = () => {
navigate(path)
}
const docsId = 'cherrystudio-docs'
const onOpenDocs = () => {
MinApp.start({
id: 'docs',
openMinapp({
id: docsId,
name: t('docs.title'),
url: 'https://docs.cherry-ai.com/',
logo: AppLogo
@ -66,9 +68,10 @@ const Sidebar: FC = () => {
<AvatarImg src={avatar || UserAvatar} draggable={false} className="nodrag" onClick={onEditUser} />
)}
<MainMenusContainer>
<Menus onClick={MinApp.onClose}>
<Menus onClick={hideMinappPopup}>
<MainMenus />
</Menus>
<SidebarOpenedMinappTabs />
{showPinnedApps && (
<AppsContainer>
<Divider />
@ -80,10 +83,7 @@ const Sidebar: FC = () => {
</MainMenusContainer>
<Menus>
<Tooltip title={t('docs.title')} mouseEnterDelay={0.8} placement="right">
<Icon
theme={theme}
onClick={onOpenDocs}
className={minappShow && MinApp.app?.url === 'https://docs.cherry-ai.com/' ? 'active' : ''}>
<Icon theme={theme} onClick={onOpenDocs} className={minappShow && currentMinappId === docsId ? 'active' : ''}>
<QuestionCircleOutlined />
</Icon>
</Tooltip>
@ -102,7 +102,7 @@ const Sidebar: FC = () => {
<Tooltip title={t('settings.title')} mouseEnterDelay={0.8} placement="right">
<StyledLink
onClick={async () => {
minappShow && (await MinApp.close())
hideMinappPopup()
await modelGenerating()
await to('/settings/provider')
}}>
@ -117,6 +117,7 @@ const Sidebar: FC = () => {
}
const MainMenus: FC = () => {
const { hideMinappPopup } = useMinappPopup()
const { t } = useTranslation()
const { pathname } = useLocation()
const { sidebarIcons } = useSettings()
@ -155,7 +156,7 @@ const MainMenus: FC = () => {
<Tooltip key={icon} title={t(`${icon}.title`)} mouseEnterDelay={0.8} placement="right">
<StyledLink
onClick={async () => {
minappShow && (await MinApp.close())
hideMinappPopup()
await modelGenerating()
navigate(path)
}}>
@ -168,11 +169,99 @@ const MainMenus: FC = () => {
})
}
/** Tabs of opened minapps in sidebar */
const SidebarOpenedMinappTabs: FC = () => {
const { minappShow, openedKeepAliveMinapps, currentMinappId } = useRuntime()
const { openMinappKeepAlive, hideMinappPopup, closeMinapp, closeAllMinapps } = useMinappPopup()
const { theme } = useTheme()
const { t } = useTranslation()
const handleOnClick = (app) => {
if (minappShow && currentMinappId === app.id) {
hideMinappPopup()
} else {
openMinappKeepAlive(app)
}
}
// animation for minapp switch indicator
useEffect(() => {
//hacky way to get the height of the icon
const iconDefaultHeight = 40
const iconDefaultOffset = 17
const container = document.querySelector('.TabsContainer') as HTMLElement
const activeIcon = document.querySelector('.TabsContainer .opened-active') as HTMLElement
let indicatorTop = 0,
indicatorRight = 0
if (minappShow && activeIcon && container) {
indicatorTop = activeIcon.offsetTop + activeIcon.offsetHeight / 2 - 4 // 4 is half of the indicator's height (8px)
indicatorRight = 0
} else {
indicatorTop =
((openedKeepAliveMinapps.length > 0 ? openedKeepAliveMinapps.length : 1) / 2) * iconDefaultHeight +
iconDefaultOffset -
4
indicatorRight = -50
}
container.style.setProperty('--indicator-top', `${indicatorTop}px`)
container.style.setProperty('--indicator-right', `${indicatorRight}px`)
}, [currentMinappId, openedKeepAliveMinapps, minappShow])
const isShowOpened = openedKeepAliveMinapps.length > 0
if (!isShowOpened) return <TabsContainer className="TabsContainer" />
return (
<TabsContainer className="TabsContainer">
<Divider />
<TabsWrapper>
<Menus>
{openedKeepAliveMinapps.map((app) => {
const menuItems: MenuProps['items'] = [
{
key: 'closeApp',
label: t('minapp.sidebar.close.title'),
onClick: () => {
closeMinapp(app.id)
}
},
{
key: 'closeAllApp',
label: t('minapp.sidebar.closeall.title'),
onClick: () => {
closeAllMinapps()
}
}
]
const isActive = minappShow && currentMinappId === app.id
return (
<Tooltip key={app.id} title={app.name} mouseEnterDelay={0.8} placement="right">
<StyledLink>
<Dropdown menu={{ items: menuItems }} trigger={['contextMenu']} overlayStyle={{ zIndex: 10000 }}>
<Icon
theme={theme}
onClick={() => handleOnClick(app)}
className={`${isActive ? 'opened-active' : ''}`}>
<MinAppIcon size={20} app={app} style={{ borderRadius: 6 }} />
</Icon>
</Dropdown>
</StyledLink>
</Tooltip>
)
})}
</Menus>
</TabsWrapper>
</TabsContainer>
)
}
const PinnedApps: FC = () => {
const { pinned, updatePinnedMinapps } = useMinapps()
const { t } = useTranslation()
const { minappShow } = useRuntime()
const { minappShow, openedKeepAliveMinapps, currentMinappId } = useRuntime()
const { theme } = useTheme()
const { openMinappKeepAlive } = useMinappPopup()
return (
<DragableList list={pinned} onUpdate={updatePinnedMinapps} listStyle={{ marginBottom: 5 }}>
@ -187,12 +276,15 @@ const PinnedApps: FC = () => {
}
}
]
const isActive = minappShow && MinApp.app?.id === app.id
const isActive = minappShow && currentMinappId === app.id
return (
<Tooltip key={app.id} title={app.name} mouseEnterDelay={0.8} placement="right">
<StyledLink>
<Dropdown menu={{ items: menuItems }} trigger={['contextMenu']}>
<Icon theme={theme} onClick={() => MinApp.start(app)} className={isActive ? 'active' : ''}>
<Dropdown menu={{ items: menuItems }} trigger={['contextMenu']} overlayStyle={{ zIndex: 10000 }}>
<Icon
theme={theme}
onClick={() => openMinappKeepAlive(app)}
className={`${isActive ? 'active' : ''} ${openedKeepAliveMinapps.some((item) => item.id === app.id) ? 'opened-animation' : ''}`}>
<MinAppIcon size={20} app={app} style={{ borderRadius: 6 }} />
</Icon>
</Dropdown>
@ -293,6 +385,23 @@ const Icon = styled.div<{ theme: string }>`
color: var(--color-icon-white);
}
}
@keyframes borderBreath {
0% {
border-color: var(--color-primary-mute);
}
50% {
border-color: var(--color-primary);
}
100% {
border-color: var(--color-primary-mute);
}
}
&.opened-animation {
border: 0.5px solid var(--color-primary);
animation: borderBreath 4s ease-in-out infinite;
}
`
const StyledLink = styled.div`
@ -323,4 +432,37 @@ const Divider = styled.div`
border-bottom: 0.5px solid var(--color-border);
`
const TabsContainer = styled.div`
display: flex;
flex-direction: column;
align-items: center;
-webkit-app-region: none;
position: relative;
width: 100%;
&::after {
content: '';
position: absolute;
right: var(--indicator-right, 0);
top: var(--indicator-top, 0);
width: 4px;
height: 8px;
background-color: var(--color-primary);
transition:
top 0.3s cubic-bezier(0.4, 0, 0.2, 1),
right 0.3s ease-in-out;
border-radius: 2px;
}
&::-webkit-scrollbar {
display: none;
}
`
const TabsWrapper = styled.div`
background-color: rgba(128, 128, 128, 0.1);
border-radius: 20px;
overflow: hidden;
`
export default Sidebar

View File

@ -49,9 +49,7 @@ import DeepSeekProviderLogo from '@renderer/assets/images/providers/deepseek.png
import GroqProviderLogo from '@renderer/assets/images/providers/groq.png?url'
import OpenAiProviderLogo from '@renderer/assets/images/providers/openai.png?url'
import SiliconFlowProviderLogo from '@renderer/assets/images/providers/silicon.png?url'
import MinApp from '@renderer/components/MinApp'
import { MinAppType } from '@renderer/types'
export const DEFAULT_MIN_APPS: MinAppType[] = [
{
id: 'openai',
@ -395,8 +393,3 @@ export const DEFAULT_MIN_APPS: MinAppType[] = [
bodered: true
}
]
export function startMinAppById(id: string) {
const app = DEFAULT_MIN_APPS.find((app) => app?.id === id)
app && MinApp.start(app)
}

View File

@ -0,0 +1,120 @@
import { useRuntime } from '@renderer/hooks/useRuntime'
import { useAppDispatch } from '@renderer/store'
import {
setCurrentMinappId,
setMinappShow,
setOpenedKeepAliveMinapps,
setOpenedOneOffMinapp
} from '@renderer/store/runtime'
import { MinAppType } from '@renderer/types'
/** The max number of keep alive minapps */
const MINAPP_MAX_KEEPALIVE = 3
/**
* Usage:
*
* To control the minapp popup, you can use the following hooks:
* import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
*
* in the component:
* const { openMinapp, openMinappKeepAlive, openMinappById,
* closeMinapp, hideMinappPopup, closeAllMinapps } = useMinappPopup()
*
* To use some key states of the minapp popup:
* import { useRuntime } from '@renderer/hooks/useRuntime'
* const { openedKeepAliveMinapps, openedOneOffMinapp, minappShow } = useRuntime()
*/
export const useMinappPopup = () => {
const { openedKeepAliveMinapps, openedOneOffMinapp, minappShow } = useRuntime()
const dispatch = useAppDispatch()
/** Open a minapp (popup shows and minapp loaded) */
const openMinapp = (app: MinAppType, keepAlive: boolean = false) => {
if (keepAlive) {
//if the minapp is already opened, do nothing
if (openedKeepAliveMinapps.some((item) => item.id === app.id)) {
dispatch(setCurrentMinappId(app.id))
dispatch(setMinappShow(true))
return
}
//if the minapp is not opened, open it
//check if the keep alive minapps meet the max limit
if (openedKeepAliveMinapps.length < MINAPP_MAX_KEEPALIVE) {
//always put new minapp at the first
dispatch(setOpenedKeepAliveMinapps([app, ...openedKeepAliveMinapps]))
} else {
//pop the last one
dispatch(setOpenedKeepAliveMinapps([app, ...openedKeepAliveMinapps.slice(0, MINAPP_MAX_KEEPALIVE - 1)]))
}
dispatch(setOpenedOneOffMinapp(null))
dispatch(setCurrentMinappId(app.id))
dispatch(setMinappShow(true))
return
}
//if the minapp is not keep alive, open it as one-off minapp
dispatch(setOpenedOneOffMinapp(app))
dispatch(setCurrentMinappId(app.id))
dispatch(setMinappShow(true))
return
}
/** a wrapper of openMinapp(app, true) */
const openMinappKeepAlive = (app: MinAppType) => {
openMinapp(app, true)
}
/** Open a minapp by id (look up the minapp in DEFAULT_MIN_APPS) */
const openMinappById = (id: string, keepAlive: boolean = false) => {
import('@renderer/config/minapps').then(({ DEFAULT_MIN_APPS }) => {
const app = DEFAULT_MIN_APPS.find((app) => app?.id === id)
if (app) {
openMinapp(app, keepAlive)
}
})
}
/** Close a minapp immediately (popup hides and minapp unloaded) */
const closeMinapp = (appid: string) => {
if (openedKeepAliveMinapps.some((item) => item.id === appid)) {
dispatch(setOpenedKeepAliveMinapps(openedKeepAliveMinapps.filter((item) => item.id !== appid)))
} else if (openedOneOffMinapp?.id === appid) {
dispatch(setOpenedOneOffMinapp(null))
}
dispatch(setCurrentMinappId(''))
dispatch(setMinappShow(false))
return
}
/** Close all minapps (popup hides and all minapps unloaded) */
const closeAllMinapps = () => {
dispatch(setOpenedKeepAliveMinapps([]))
dispatch(setOpenedOneOffMinapp(null))
dispatch(setCurrentMinappId(''))
dispatch(setMinappShow(false))
}
/** Hide the minapp popup (only one-off minapp unloaded) */
const hideMinappPopup = () => {
if (!minappShow) return
if (openedOneOffMinapp) {
dispatch(setOpenedOneOffMinapp(null))
dispatch(setCurrentMinappId(''))
}
dispatch(setMinappShow(false))
}
return {
openMinapp,
openMinappKeepAlive,
openMinappById,
closeMinapp,
hideMinappPopup,
closeAllMinapps
}
}

View File

@ -542,10 +542,19 @@
"warn.siyuan.exporting": "Exporting to Siyuan Note, please do not request export repeatedly!"
},
"minapp": {
"popup": {
"refresh": "Refresh",
"close": "Close MinApp",
"minimize": "Minimize MinApp",
"devtools": "Developer Tools",
"openExternal": "Open in Browser"
},
"sidebar.add.title": "Add to sidebar",
"sidebar.remove.title": "Remove from sidebar",
"title": "MinApp",
"sidebar.hide.title": "Hide MinApp"
"sidebar.close.title": "Close",
"sidebar.closeall.title": "Close All",
"sidebar.hide.title": "Hide MinApp",
"title": "MinApp"
},
"miniwindow": {
"clipboard": {

View File

@ -542,8 +542,17 @@
"error.yuque.no_config": "語雀のAPIアドレスまたはトークンが設定されていません"
},
"minapp": {
"popup": {
"refresh": "更新",
"close": "ミニアプリを閉じる",
"minimize": "ミニアプリを最小化",
"devtools": "開発者ツール",
"openExternal": "ブラウザで開く"
},
"sidebar.add.title": "サイドバーに追加",
"sidebar.remove.title": "サイドバーから削除",
"sidebar.close.title": "閉じる",
"sidebar.closeall.title": "すべて閉じる",
"sidebar.hide.title": "ミニアプリを隠す",
"title": "ミニアプリ"
},

View File

@ -542,8 +542,17 @@
"warn.siyuan.exporting": "Экспортируется в Siyuan, пожалуйста, не отправляйте повторные запросы!"
},
"minapp": {
"popup": {
"refresh": "Обновить",
"close": "Закрыть встроенное приложение",
"minimize": "Свернуть встроенное приложение",
"devtools": "Инструменты разработчика",
"openExternal": "Открыть в браузере"
},
"sidebar.add.title": "Добавить в боковую панель",
"sidebar.remove.title": "Удалить из боковой панели",
"sidebar.close.title": "Закрыть",
"sidebar.closeall.title": "Закрыть все",
"sidebar.hide.title": "Скрыть приложение",
"title": "Встроенные приложения"
},

View File

@ -542,8 +542,17 @@
"warn.siyuan.exporting": "正在导出到思源笔记,请勿重复请求导出!"
},
"minapp": {
"popup": {
"refresh": "刷新",
"close": "关闭小程序",
"minimize": "最小化小程序",
"devtools": "开发者工具",
"openExternal": "在浏览器中打开"
},
"sidebar.add.title": "添加到侧边栏",
"sidebar.remove.title": "从侧边栏移除",
"sidebar.close.title": "关闭",
"sidebar.closeall.title": "全部关闭",
"sidebar.hide.title": "隐藏小程序",
"title": "小程序"
},

View File

@ -542,8 +542,17 @@
"warn.siyuan.exporting": "正在導出到思源筆記,請勿重複請求導出!"
},
"minapp": {
"popup": {
"refresh": "重新整理",
"close": "關閉小工具",
"minimize": "最小化小工具",
"devtools": "開發者工具",
"openExternal": "在瀏覽器中開啟"
},
"sidebar.add.title": "新增到側邊欄",
"sidebar.remove.title": "從側邊欄移除",
"sidebar.close.title": "關閉",
"sidebar.closeall.title": "全部關閉",
"sidebar.hide.title": "隱藏小工具",
"title": "小工具"
},

View File

@ -1,5 +1,5 @@
import MinAppIcon from '@renderer/components/Icons/MinAppIcon'
import MinApp from '@renderer/components/MinApp'
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
import { useMinapps } from '@renderer/hooks/useMinapps'
import { MinAppType } from '@renderer/types'
import type { MenuProps } from 'antd'
@ -15,13 +15,14 @@ interface Props {
}
const App: FC<Props> = ({ app, onClick, size = 60 }) => {
const { openMinappKeepAlive } = useMinappPopup()
const { t } = useTranslation()
const { minapps, pinned, disabled, updateMinapps, updateDisabledMinapps, updatePinnedMinapps } = useMinapps()
const isPinned = pinned.some((p) => p.id === app.id)
const isVisible = minapps.some((m) => m.id === app.id)
const handleClick = () => {
MinApp.start(app)
openMinappKeepAlive(app)
onClick?.()
}

View File

@ -1,6 +1,6 @@
import { DownloadOutlined, ExpandOutlined, LinkOutlined } from '@ant-design/icons'
import MinApp from '@renderer/components/MinApp'
import { AppLogo } from '@renderer/config/env'
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
import { extractTitle } from '@renderer/utils/formats'
import { Button } from 'antd'
import { FC } from 'react'
@ -14,6 +14,7 @@ interface Props {
const Artifacts: FC<Props> = ({ html }) => {
const { t } = useTranslation()
const title = extractTitle(html) || 'Artifacts ' + t('chat.artifacts.button.preview')
const { openMinapp } = useMinappPopup()
/**
*
@ -22,7 +23,8 @@ const Artifacts: FC<Props> = ({ html }) => {
const path = await window.api.file.create('artifacts-preview.html')
await window.api.file.write(path, html)
const filePath = `file://${path}`
MinApp.start({
openMinapp({
id: 'artifacts-preview',
name: title,
logo: AppLogo,
url: filePath

View File

@ -1,9 +1,9 @@
import UserPopup from '@renderer/components/Popups/UserPopup'
import { APP_NAME, AppLogo, isLocalAi } from '@renderer/config/env'
import { startMinAppById } from '@renderer/config/minapps'
import { getModelLogo } from '@renderer/config/models'
import { useTheme } from '@renderer/context/ThemeProvider'
import useAvatar from '@renderer/hooks/useAvatar'
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
import { useMessageStyle, useSettings } from '@renderer/hooks/useSettings'
import { getMessageModelId } from '@renderer/services/MessagesService'
import { getModelName } from '@renderer/services/ModelService'
@ -32,6 +32,7 @@ const MessageHeader: FC<Props> = memo(({ assistant, model, message }) => {
const { userName, sidebarIcons } = useSettings()
const { t } = useTranslation()
const { isBubbleStyle } = useMessageStyle()
const { openMinappById } = useMinappPopup()
const avatarSource = useMemo(() => getAvatarSource(isLocalAi, getMessageModelId(message)), [message])
@ -54,7 +55,9 @@ const MessageHeader: FC<Props> = memo(({ assistant, model, message }) => {
const username = useMemo(() => removeLeadingEmoji(getUserName()), [getUserName])
const showMiniApp = useCallback(() => {
showMinappIcon && model?.provider && startMinAppById(model.provider)
showMinappIcon && model?.provider && openMinappById(model.provider)
// because don't need openMinappById to be a dependency
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [model?.provider, showMinappIcon])
const avatarStyle: CSSProperties | undefined = isBubbleStyle

View File

@ -2,9 +2,9 @@ import { GithubOutlined } from '@ant-design/icons'
import { FileProtectOutlined, GlobalOutlined, MailOutlined, SoundOutlined } from '@ant-design/icons'
import IndicatorLight from '@renderer/components/IndicatorLight'
import { HStack } from '@renderer/components/Layout'
import MinApp from '@renderer/components/MinApp'
import { APP_NAME, AppLogo } from '@renderer/config/env'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
import { useRuntime } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings'
import { useAppDispatch } from '@renderer/store'
@ -29,6 +29,7 @@ const AboutSettings: FC = () => {
const { theme } = useTheme()
const dispatch = useAppDispatch()
const { update } = useRuntime()
const { openMinapp } = useMinappPopup()
const onCheckUpdate = debounce(
async () => {
@ -70,7 +71,8 @@ const AboutSettings: FC = () => {
const showLicense = async () => {
const { appPath } = await window.api.getAppInfo()
MinApp.start({
openMinapp({
id: 'cherrystudio-license',
name: t('settings.about.license.title'),
url: `file://${appPath}/resources/cherry-studio/license.html`,
logo: AppLogo
@ -79,7 +81,8 @@ const AboutSettings: FC = () => {
const showReleases = async () => {
const { appPath } = await window.api.getAppInfo()
MinApp.start({
openMinapp({
id: 'cherrystudio-releases',
name: t('settings.about.releases.title'),
url: `file://${appPath}/resources/cherry-studio/releases.html?theme=${theme === ThemeMode.dark ? 'dark' : 'light'}`,
logo: AppLogo

View File

@ -1,7 +1,7 @@
import { InfoCircleOutlined } from '@ant-design/icons'
import { HStack } from '@renderer/components/Layout'
import MinApp from '@renderer/components/MinApp'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
import { RootState, useAppDispatch } from '@renderer/store'
import { setJoplinToken, setJoplinUrl } from '@renderer/store/settings'
import { Button, Tooltip } from 'antd'
@ -16,6 +16,7 @@ const JoplinSettings: FC = () => {
const { t } = useTranslation()
const { theme } = useTheme()
const dispatch = useAppDispatch()
const { openMinapp } = useMinappPopup()
const joplinToken = useSelector((state: RootState) => state.settings.joplinToken)
const joplinUrl = useSelector((state: RootState) => state.settings.joplinUrl)
@ -64,7 +65,7 @@ const JoplinSettings: FC = () => {
}
const handleJoplinHelpClick = () => {
MinApp.start({
openMinapp({
id: 'joplin-help',
name: 'Joplin Help',
url: 'https://joplinapp.org/help/apps/clipper'

View File

@ -1,8 +1,8 @@
import { InfoCircleOutlined } from '@ant-design/icons'
import { Client } from '@notionhq/client'
import { HStack } from '@renderer/components/Layout'
import MinApp from '@renderer/components/MinApp'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
import { RootState, useAppDispatch } from '@renderer/store'
import {
setNotionApiKey,
@ -22,6 +22,7 @@ const NotionSettings: FC = () => {
const { t } = useTranslation()
const { theme } = useTheme()
const dispatch = useAppDispatch()
const { openMinapp } = useMinappPopup()
const notionApiKey = useSelector((state: RootState) => state.settings.notionApiKey)
const notionDatabaseID = useSelector((state: RootState) => state.settings.notionDatabaseID)
@ -68,7 +69,7 @@ const NotionSettings: FC = () => {
}
const handleNotionTitleClick = () => {
MinApp.start({
openMinapp({
id: 'notion-help',
name: 'Notion Help',
url: 'https://docs.cherry-ai.com/advanced-basic/notion'

View File

@ -1,7 +1,7 @@
import { InfoCircleOutlined } from '@ant-design/icons'
import { HStack } from '@renderer/components/Layout'
import MinApp from '@renderer/components/MinApp'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
import { RootState, useAppDispatch } from '@renderer/store'
import { setSiyuanApiUrl, setSiyuanBoxId, setSiyuanRootPath, setSiyuanToken } from '@renderer/store/settings'
import { Button, Tooltip } from 'antd'
@ -13,6 +13,7 @@ import { useSelector } from 'react-redux'
import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..'
const SiyuanSettings: FC = () => {
const { openMinapp } = useMinappPopup()
const { t } = useTranslation()
const { theme } = useTheme()
const dispatch = useAppDispatch()
@ -39,7 +40,7 @@ const SiyuanSettings: FC = () => {
}
const handleSiyuanHelpClick = () => {
MinApp.start({
openMinapp({
id: 'siyuan-help',
name: 'Siyuan Help',
url: 'https://docs.cherry-ai.com/advanced-basic/siyuan'

View File

@ -1,7 +1,7 @@
import { InfoCircleOutlined } from '@ant-design/icons'
import { HStack } from '@renderer/components/Layout'
import MinApp from '@renderer/components/MinApp'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
import { RootState, useAppDispatch } from '@renderer/store'
import { setYuqueRepoId, setYuqueToken, setYuqueUrl } from '@renderer/store/settings'
import { Button, Tooltip } from 'antd'
@ -16,6 +16,7 @@ const YuqueSettings: FC = () => {
const { t } = useTranslation()
const { theme } = useTheme()
const dispatch = useAppDispatch()
const { openMinapp } = useMinappPopup()
const yuqueToken = useSelector((state: RootState) => state.settings.yuqueToken)
const yuqueUrl = useSelector((state: RootState) => state.settings.yuqueUrl)
@ -64,7 +65,7 @@ const YuqueSettings: FC = () => {
}
const handleYuqueHelpClick = () => {
MinApp.start({
openMinapp({
id: 'yuque-help',
name: 'Yuque Help',
url: 'https://www.yuque.com/settings/tokens'

View File

@ -1,4 +1,4 @@
import MinApp from '@renderer/components/MinApp'
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
import { MinAppType, Provider } from '@renderer/types'
import { Button } from 'antd'
import { FC } from 'react'
@ -15,18 +15,20 @@ 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 { openMinapp } = useMinappPopup()
const onShowGraphRAG = async () => {
const { appPath } = await window.api.getAppInfo()
const url = `file://${appPath}/resources/graphrag.html?apiUrl=${apiUrl}&modelId=${modalId}`
const app: MinAppType = {
id: 'graphrag',
name: t('words.knowledgeGraph'),
logo: '',
url
}
MinApp.start(app)
openMinapp(app)
}
if (!modalId) {

View File

@ -1,7 +1,7 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { AppLogo, UserAvatar } from '@renderer/config/env'
import type { MinAppType } from '@renderer/types'
import type { UpdateInfo } from 'builder-util-runtime'
export interface UpdateState {
info: UpdateInfo | null
checking: boolean
@ -14,7 +14,14 @@ export interface UpdateState {
export interface RuntimeState {
avatar: string
generating: boolean
/** whether the minapp popup is shown */
minappShow: boolean
/** the minapps that are opened and should be keep alive */
openedKeepAliveMinapps: MinAppType[]
/** the minapp that is opened for one time */
openedOneOffMinapp: MinAppType | null
/** the current minapp id */
currentMinappId: string
searching: boolean
filesPath: string
resourcesPath: string
@ -30,6 +37,9 @@ const initialState: RuntimeState = {
avatar: UserAvatar,
generating: false,
minappShow: false,
openedKeepAliveMinapps: [],
openedOneOffMinapp: null,
currentMinappId: '',
searching: false,
filesPath: '',
resourcesPath: '',
@ -59,6 +69,15 @@ const runtimeSlice = createSlice({
setMinappShow: (state, action: PayloadAction<boolean>) => {
state.minappShow = action.payload
},
setOpenedKeepAliveMinapps: (state, action: PayloadAction<MinAppType[]>) => {
state.openedKeepAliveMinapps = action.payload
},
setOpenedOneOffMinapp: (state, action: PayloadAction<MinAppType | null>) => {
state.openedOneOffMinapp = action.payload
},
setCurrentMinappId: (state, action: PayloadAction<string>) => {
state.currentMinappId = action.payload
},
setSearching: (state, action: PayloadAction<boolean>) => {
state.searching = action.payload
},
@ -81,6 +100,9 @@ export const {
setAvatar,
setGenerating,
setMinappShow,
setOpenedKeepAliveMinapps,
setOpenedOneOffMinapp,
setCurrentMinappId,
setSearching,
setFilesPath,
setResourcesPath,