diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bea18d50b5..7a007e4e91 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,7 +27,7 @@ jobs: - name: Check out Git repository uses: actions/checkout@v4 with: - ref: main + fetch-depth: 0 - name: Get release tag id: get-tag @@ -149,4 +149,4 @@ jobs: token: ${{ secrets.REPO_DISPATCH_TOKEN }} repository: CherryHQ/cherry-studio-docs event-type: update-download-version - client-payload: '{"version": "${{ steps.get-tag.outputs.tag }}"}' \ No newline at end of file + client-payload: '{"version": "${{ steps.get-tag.outputs.tag }}"}' diff --git a/package.json b/package.json index 6d0575bf65..adb3764281 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "CherryStudio", - "version": "1.4.2", + "version": "1.4.2-ui-preview", "private": true, "description": "A powerful AI assistant for producer.", "main": "./out/main/index.js", @@ -193,8 +193,8 @@ "react-infinite-scroll-component": "^6.1.0", "react-markdown": "^9.0.1", "react-redux": "^9.1.2", - "react-router": "6", - "react-router-dom": "6", + "react-router": "^7.6.2", + "react-router-dom": "^7.6.2", "react-spinners": "^0.14.1", "react-window": "^1.8.11", "redux": "^5.0.1", diff --git a/src/main/config.ts b/src/main/config.ts index 84dc1b846d..32665a55b8 100644 --- a/src/main/config.ts +++ b/src/main/config.ts @@ -11,13 +11,13 @@ if (isDev) { export const DATA_PATH = getDataPath() export const titleBarOverlayDark = { - height: 40, + height: 42, color: 'rgba(255,255,255,0)', symbolColor: '#fff' } export const titleBarOverlayLight = { - height: 40, + height: 42, color: 'rgba(255,255,255,0)', symbolColor: '#000' } diff --git a/src/main/services/WindowService.ts b/src/main/services/WindowService.ts index 3f37d7c406..469ba1bd5e 100644 --- a/src/main/services/WindowService.ts +++ b/src/main/services/WindowService.ts @@ -56,14 +56,14 @@ export class WindowService { minHeight: 600, show: false, autoHideMenuBar: true, - transparent: isMac, + transparent: false, vibrancy: 'sidebar', visualEffectState: 'active', titleBarStyle: 'hidden', titleBarOverlay: nativeTheme.shouldUseDarkColors ? titleBarOverlayDark : titleBarOverlayLight, backgroundColor: isMac ? undefined : nativeTheme.shouldUseDarkColors ? '#181818' : '#FFFFFF', darkTheme: nativeTheme.shouldUseDarkColors, - trafficLightPosition: { x: 8, y: 12 }, + trafficLightPosition: { x: 12, y: 12 }, ...(isLinux ? { icon } : {}), webPreferences: { preload: join(__dirname, '../preload/index.js'), diff --git a/src/main/services/urlschema/mcp-install.ts b/src/main/services/urlschema/mcp-install.ts index e5f0a76501..3131830a16 100644 --- a/src/main/services/urlschema/mcp-install.ts +++ b/src/main/services/urlschema/mcp-install.ts @@ -65,7 +65,7 @@ export function handleMcpProtocolUrl(url: URL) { const mainWindow = windowService.getMainWindow() if (mainWindow && !mainWindow.isDestroyed()) { - mainWindow.webContents.executeJavaScript("window.navigate('/settings/mcp')") + mainWindow.webContents.executeJavaScript("window.navigate('/mcp-servers')") } break } diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index b46910cd65..a327cef59d 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -2,10 +2,9 @@ import '@renderer/databases' import store, { persistor } from '@renderer/store' import { Provider } from 'react-redux' -import { HashRouter, Route, Routes } from 'react-router-dom' +import { HashRouter } from 'react-router-dom' import { PersistGate } from 'redux-persist/integration/react' -import Sidebar from './components/app/Sidebar' import TopViewContainer from './components/TopView' import AntdProvider from './context/AntdProvider' import { CodeStyleProvider } from './context/CodeStyleProvider' @@ -13,14 +12,8 @@ import { NotificationProvider } from './context/NotificationProvider' import StyleSheetManager from './context/StyleSheetManager' import { ThemeProvider } from './context/ThemeProvider' import NavigationHandler from './handler/NavigationHandler' -import AgentsPage from './pages/agents/AgentsPage' -import AppsPage from './pages/apps/AppsPage' -import FilesPage from './pages/files/FilesPage' -import HomePage from './pages/home/HomePage' -import KnowledgePage from './pages/knowledge/KnowledgePage' -import PaintingsRoutePage from './pages/paintings/PaintingsRoutePage' -import SettingsPage from './pages/settings/SettingsPage' -import TranslatePage from './pages/translate/TranslatePage' +import MainSidebar from './pages/home/MainSidebar/MainSidebar' +import Routes from './Routes' function App(): React.ReactElement { return ( @@ -34,17 +27,8 @@ function App(): React.ReactElement { - - - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - + + diff --git a/src/renderer/src/Routes.tsx b/src/renderer/src/Routes.tsx new file mode 100644 index 0000000000..06e5c2e64b --- /dev/null +++ b/src/renderer/src/Routes.tsx @@ -0,0 +1,38 @@ +import { Route, Routes, useLocation } from 'react-router-dom' + +import AgentsPage from './pages/agents/AgentsPage' +import AppsPage from './pages/apps/AppsPage' +import FilesPage from './pages/files/FilesPage' +import HomePage from './pages/home/HomePage' +import KnowledgePage from './pages/knowledge/KnowledgePage' +import McpServersPage from './pages/mcp-servers' +import PaintingsRoutePage from './pages/paintings/PaintingsRoutePage' +import SettingsPage from './pages/settings/SettingsPage' +import TranslatePage from './pages/translate/TranslatePage' + +const RouteContainer = () => { + const location = useLocation() + const isHomePage = location.pathname === '/' + + return ( +
+
+ +
+
+ + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + +
+
+ ) +} + +export default RouteContainer diff --git a/src/renderer/src/assets/styles/ant.scss b/src/renderer/src/assets/styles/ant.scss index d4788439e5..f311510c0c 100644 --- a/src/renderer/src/assets/styles/ant.scss +++ b/src/renderer/src/assets/styles/ant.scss @@ -25,7 +25,6 @@ } .minapp-drawer { - max-width: calc(100vw - var(--sidebar-width)); .ant-drawer-content-wrapper { box-shadow: none; } @@ -33,7 +32,7 @@ position: absolute; -webkit-app-region: drag; min-height: calc(var(--navbar-height) + 0.5px); - width: calc(100vw - var(--sidebar-width)); + width: 100%; margin-top: -0.5px; border-bottom: none; } diff --git a/src/renderer/src/assets/styles/color.scss b/src/renderer/src/assets/styles/color.scss index 6100e1d0ee..5e87a1c0b5 100644 --- a/src/renderer/src/assets/styles/color.scss +++ b/src/renderer/src/assets/styles/color.scss @@ -29,7 +29,7 @@ --color-text-secondary: rgba(235, 235, 245, 0.7); --color-icon: #ffffff99; --color-icon-white: #ffffff; - --color-border: #ffffff19; + --color-border: #383838; --color-border-soft: #ffffff10; --color-border-mute: #ffffff05; --color-error: #f44336; @@ -44,8 +44,8 @@ --color-reference-text: #ffffff; --color-reference-background: #0b0e12; - --color-list-item: #222; - --color-list-item-hover: #1e1e1e; + --color-list-item: rgba(255, 255, 255, 0.1); + --color-list-item-hover: rgba(255, 255, 255, 0.05); --modal-background: #1f1f1f; @@ -56,7 +56,7 @@ --navbar-background-mac: rgba(20, 20, 20, 0.55); --navbar-background: #1f1f1f; - --navbar-height: 40px; + --navbar-height: 42px; --sidebar-width: 50px; --status-bar-height: 40px; --input-bar-height: 100px; @@ -66,12 +66,13 @@ --settings-width: 250px; --scrollbar-width: 5px; - --chat-background: #111111; - --chat-background-user: #28b561; - --chat-background-assistant: #2c2c2c; + --chat-background: transparent; + --chat-background-user: rgba(255, 255, 255, 0.08); + --chat-background-assistant: transparent; --chat-text-user: var(--color-black); - --list-item-border-radius: 20px; + --list-item-border-radius: 8px; + --border-width: 0.5px; } [theme-mode='light'] { @@ -120,8 +121,8 @@ --color-reference-text: #000000; --color-reference-background: #f1f7ff; - --color-list-item: #eee; - --color-list-item-hover: #f5f5f5; + --color-list-item: rgba(255, 255, 255, 0.9); + --color-list-item-hover: rgba(255, 255, 255, 0.5); --modal-background: var(--color-white); @@ -132,8 +133,10 @@ --navbar-background-mac: rgba(255, 255, 255, 0.55); --navbar-background: rgba(244, 244, 244); - --chat-background: #f3f3f3; - --chat-background-user: #95ec69; - --chat-background-assistant: #ffffff; + --chat-background: transparent; + --chat-background-user: rgba(0, 0, 0, 0.045); + --chat-background-assistant: transparent; --chat-text-user: var(--color-text); + + --border-width: 0.5px; } diff --git a/src/renderer/src/assets/styles/container.scss b/src/renderer/src/assets/styles/container.scss index 8be4027981..c20e181060 100644 --- a/src/renderer/src/assets/styles/container.scss +++ b/src/renderer/src/assets/styles/container.scss @@ -1,6 +1,14 @@ #content-container { background-color: var(--color-background); border-top: 0.5px solid var(--color-border); - border-top-left-radius: 10px; - border-left: 0.5px solid var(--color-border); +} + +.group-container { + .context-menu-container { + width: 100%; + } +} + +.context-menu-container { + max-width: 100%; } diff --git a/src/renderer/src/assets/styles/index.scss b/src/renderer/src/assets/styles/index.scss index b91b3c3a54..1c9b8a21f2 100644 --- a/src/renderer/src/assets/styles/index.scss +++ b/src/renderer/src/assets/styles/index.scss @@ -112,46 +112,21 @@ ul { } .bubble { - background-color: var(--chat-background); - #chat-main { - background-color: var(--chat-background); - } - #messages { - background-color: var(--chat-background); - } - #inputbar { - margin: -5px 15px 15px 15px; - background: var(--color-background); - } .system-prompt { background-color: var(--chat-background-assistant); } .message-content-container { margin: 5px 0; border-radius: 8px; - padding: 0.5rem 1rem; } - - .block-wrapper { - display: flow-root; - } - - .message-content-container > *:last-child { - margin-bottom: 0; - } - .message-thought-container { margin-top: 8px; } - .message-user { - color: var(--chat-text-user); - .message-content-container-user .anticon { - color: var(--chat-text-user) !important; - } - - .markdown { - color: var(--chat-text-user); + .message-content-container { + margin: 5px 0; + border-radius: 8px 0 8px 8px; + padding: 10px 15px 0 15px; } } .group-grid-container.horizontal, @@ -172,12 +147,6 @@ ul { code { color: var(--color-text); } - .markdown { - display: flow-root; - *:last-child { - margin-bottom: 0; - } - } } .lucide { diff --git a/src/renderer/src/assets/styles/markdown.scss b/src/renderer/src/assets/styles/markdown.scss index 569fa9f1dd..d2b77d4016 100644 --- a/src/renderer/src/assets/styles/markdown.scss +++ b/src/renderer/src/assets/styles/markdown.scss @@ -119,7 +119,7 @@ } pre { - border-radius: 5px; + border-radius: 8px; overflow-x: auto; font-family: 'Fira Code', 'Courier New', Courier, monospace; background-color: var(--color-background-mute); @@ -306,7 +306,7 @@ mjx-container { /* CodeMirror 相关样式 */ .cm-editor { - border-radius: 5px; + border-radius: inherit; &.cm-focused { outline: none; diff --git a/src/renderer/src/components/CodeBlockView/CodePreview.tsx b/src/renderer/src/components/CodeBlockView/CodePreview.tsx index d3c56f295b..04990d4d87 100644 --- a/src/renderer/src/components/CodeBlockView/CodePreview.tsx +++ b/src/renderer/src/components/CodeBlockView/CodePreview.tsx @@ -238,12 +238,12 @@ const ContentContainer = styled.div<{ }>` position: relative; overflow: auto; - border: 0.5px solid transparent; - border-radius: 5px; + border-radius: inherit; margin-top: 0; .shiki { padding: 1em; + border-radius: inherit; code { display: flex; diff --git a/src/renderer/src/components/CodeBlockView/HtmlArtifacts.tsx b/src/renderer/src/components/CodeBlockView/HtmlArtifacts.tsx index 0dbb0aabb2..ccf760d27b 100644 --- a/src/renderer/src/components/CodeBlockView/HtmlArtifacts.tsx +++ b/src/renderer/src/components/CodeBlockView/HtmlArtifacts.tsx @@ -47,7 +47,7 @@ const Artifacts: FC = ({ html }) => { } return ( - + diff --git a/src/renderer/src/components/CodeBlockView/index.tsx b/src/renderer/src/components/CodeBlockView/index.tsx index 944e6f66e3..64a3bb6c02 100644 --- a/src/renderer/src/components/CodeBlockView/index.tsx +++ b/src/renderer/src/components/CodeBlockView/index.tsx @@ -290,6 +290,10 @@ const SplitViewWrapper = styled.div` flex: 1 1 auto; width: 100%; } + + &:not(:has(+ .html-artifacts)) { + border-radius: 0 0 8px 8px; + } ` export default memo(CodeBlockView) diff --git a/src/renderer/src/components/CodeEditor/index.tsx b/src/renderer/src/components/CodeEditor/index.tsx index d92fd91e8e..977eea6ac0 100644 --- a/src/renderer/src/components/CodeEditor/index.tsx +++ b/src/renderer/src/components/CodeEditor/index.tsx @@ -229,8 +229,8 @@ const CodeEditor = ({ style={{ ...style, fontSize: `${fontSize - 1}px`, - border: '0.5px solid transparent', - marginTop: 0 + marginTop: 0, + borderRadius: 'inherit' }} /> ) diff --git a/src/renderer/src/components/ContextMenu/index.tsx b/src/renderer/src/components/ContextMenu/index.tsx index 195fcb2a38..bb3136067c 100644 --- a/src/renderer/src/components/ContextMenu/index.tsx +++ b/src/renderer/src/components/ContextMenu/index.tsx @@ -6,10 +6,9 @@ import styled from 'styled-components' interface ContextMenuProps { children: React.ReactNode onContextMenu?: (e: React.MouseEvent) => void - style?: React.CSSProperties } -const ContextMenu: React.FC = ({ children, onContextMenu, style }) => { +const ContextMenu: React.FC = ({ children, onContextMenu }) => { const { t } = useTranslation() const [contextMenuPosition, setContextMenuPosition] = useState<{ x: number; y: number } | null>(null) const [selectedText, setSelectedText] = useState('') @@ -67,7 +66,7 @@ const ContextMenu: React.FC = ({ children, onContextMenu, styl ] return ( - + {contextMenuPosition && ( > = ({ {...provided.draggableProps} {...provided.dragHandleProps} style={{ + marginBottom: 8, ...listStyle, - ...provided.draggableProps.style, - marginBottom: 8 + ...provided.draggableProps.style }}> {children(item, index)} diff --git a/src/renderer/src/components/Icons/NarrowModeIcon.tsx b/src/renderer/src/components/Icons/NarrowModeIcon.tsx new file mode 100644 index 0000000000..ac3bc01513 --- /dev/null +++ b/src/renderer/src/components/Icons/NarrowModeIcon.tsx @@ -0,0 +1,35 @@ +import { FC } from 'react' +import styled from 'styled-components' + +interface Props { + isNarrowMode: boolean +} + +const NarrowModeIcon: FC = ({ isNarrowMode }) => { + return ( + + + + + ) +} + +const Container = styled.div<{ $isNarrowMode: boolean }>` + width: 16px; + height: 16px; + border: 1.5px solid var(--color-text-2); + border-radius: 3px; + display: flex; + align-items: center; + justify-content: ${({ $isNarrowMode }) => ($isNarrowMode ? 'space-evenly' : 'space-between')}; + padding: 2px; +` + +const Line = styled.div` + width: 1.5px; + height: 10px; + background-color: var(--color-text-2); + border-radius: 5px; +` + +export default NarrowModeIcon diff --git a/src/renderer/src/components/Icons/SVGIcon.tsx b/src/renderer/src/components/Icons/SVGIcon.tsx index d58eab7ee5..a146bbcd0b 100644 --- a/src/renderer/src/components/Icons/SVGIcon.tsx +++ b/src/renderer/src/components/Icons/SVGIcon.tsx @@ -66,3 +66,18 @@ export function MdiLightbulbOn90(props: SVGProps) { ) } + +export function ExpandWidth(props: SVGProps) { + return ( + + + + + + ) +} diff --git a/src/renderer/src/components/MinApp/MinappPopupContainer.tsx b/src/renderer/src/components/MinApp/MinappPopupContainer.tsx index bd360c8a30..252c25fe14 100644 --- a/src/renderer/src/components/MinApp/MinappPopupContainer.tsx +++ b/src/renderer/src/components/MinApp/MinappPopupContainer.tsx @@ -395,10 +395,7 @@ const MinappPopupContainer: React.FC = () => { height={'100%'} maskClosable={false} closeIcon={null} - style={{ - marginLeft: 'var(--sidebar-width)', - backgroundColor: window.root.style.background - }}> + style={{ backgroundColor: window.root.style.background }}> {!isReady && ( = ({ - children, - activeAssistant, - setActiveAssistant, - activeTopic, - setActiveTopic, - position = 'left' -}) => { +const FloatingSidebar: FC = ({ children, activeAssistant, setActiveAssistant, activeTopic, setActiveTopic }) => { const [open, setOpen] = useState(false) useHotkeys('esc', () => { @@ -45,12 +38,11 @@ const FloatingSidebar: FC = ({ const content = ( void +} + +const PopupContainer: React.FC = ({ title, resolve }) => { + const [open, setOpen] = useState(true) + + const { messageStyle, fontSize } = useSettings() + const { theme } = useTheme() + const { themeNames } = useCodeStyle() + + const [fontSizeValue, setFontSizeValue] = useState(fontSize) + const { t } = useTranslation() + + const dispatch = useAppDispatch() + + const { + showMessageDivider, + messageFont, + codeShowLineNumbers, + codeCollapsible, + codeWrappable, + codeEditor, + codePreview, + codeExecution, + mathEngine, + multiModelMessageStyle, + thoughtAutoCollapse, + messageNavigation + } = useSettings() + + const codeStyle = useMemo(() => { + return codeEditor.enabled + ? theme === ThemeMode.light + ? codeEditor.themeLight + : codeEditor.themeDark + : theme === ThemeMode.light + ? codePreview.themeLight + : codePreview.themeDark + }, [ + codeEditor.enabled, + codeEditor.themeLight, + codeEditor.themeDark, + theme, + codePreview.themeLight, + codePreview.themeDark + ]) + + const onCodeStyleChange = useCallback( + (value: CodeStyleVarious) => { + const field = theme === ThemeMode.light ? 'themeLight' : 'themeDark' + const action = codeEditor.enabled ? setCodeEditor : setCodePreview + dispatch(action({ [field]: value })) + }, + [dispatch, theme, codeEditor.enabled] + ) + + const onOk = () => { + resolve(true) + setOpen(false) + } + + const onCancel = () => { + resolve(false) + setOpen(false) + } + + const onClose = () => { + TopView.hide(TopViewKey) + } + + MessageSettingsPopup.hide = onCancel + + return ( + + + + {t('settings.messages.divider')} + dispatch(setShowMessageDivider(checked))} + /> + + + + {t('settings.messages.use_serif_font')} + dispatch(setMessageFont(checked ? 'serif' : 'system'))} + /> + + + + + {t('chat.settings.thought_auto_collapse')} + + + + + dispatch(setThoughtAutoCollapse(checked))} + /> + + + + {t('message.message.style')} + dispatch(setMessageStyle(value as 'plain' | 'bubble'))} + style={{ width: 135 }} + size="small"> + {t('message.message.style.plain')} + {t('message.message.style.bubble')} + + + + + {t('message.message.multi_model_style')} + + dispatch(setMultiModelMessageStyle(value as 'fold' | 'vertical' | 'horizontal' | 'grid')) + } + style={{ width: 135 }}> + {t('message.message.multi_model_style.fold')} + {t('message.message.multi_model_style.vertical')} + {t('message.message.multi_model_style.horizontal')} + {t('message.message.multi_model_style.grid')} + + + + + {t('settings.messages.navigation')} + dispatch(setMessageNavigation(value as 'none' | 'buttons' | 'anchor'))} + style={{ width: 135 }}> + {t('settings.messages.navigation.none')} + {t('settings.messages.navigation.buttons')} + {t('settings.messages.navigation.anchor')} + + + + + {t('settings.messages.math_engine')} + dispatch(setMathEngine(value as MathEngine))} + style={{ width: 135 }} + size="small"> + KaTeX + MathJax + {t('settings.messages.math_engine.none')} + + + + + {t('settings.font_size.title')} + + + + setFontSizeValue(value)} + onChangeComplete={(value) => dispatch(setFontSize(value))} + min={12} + max={22} + step={1} + marks={{ + 12: A, + 14: {t('common.default')}, + 22: A + }} + /> + + + + + + {t('message.message.code_style')} + onCodeStyleChange(value as CodeStyleVarious)} + style={{ width: 135 }} + size="small"> + {themeNames.map((theme) => ( + + {theme} + + ))} + + + + + + {t('chat.settings.code_execution.title')} + + + + + dispatch(setCodeExecution({ enabled: checked }))} + /> + + {codeExecution.enabled && ( + <> + + + + {t('chat.settings.code_execution.timeout_minutes')} + + + + + dispatch(setCodeExecution({ timeoutMinutes: value ?? 1 }))} + style={{ width: 80 }} + /> + + + )} + + + {t('chat.settings.code_editor.title')} + dispatch(setCodeEditor({ enabled: checked }))} + /> + + {codeEditor.enabled && ( + <> + + + {t('chat.settings.code_editor.highlight_active_line')} + dispatch(setCodeEditor({ highlightActiveLine: checked }))} + /> + + + + {t('chat.settings.code_editor.fold_gutter')} + dispatch(setCodeEditor({ foldGutter: checked }))} + /> + + + + {t('chat.settings.code_editor.autocompletion')} + dispatch(setCodeEditor({ autocompletion: checked }))} + /> + + + + {t('chat.settings.code_editor.keymap')} + dispatch(setCodeEditor({ keymap: checked }))} + /> + + + )} + + + {t('chat.settings.show_line_numbers')} + dispatch(setCodeShowLineNumbers(checked))} + /> + + + + {t('chat.settings.code_collapsible')} + dispatch(setCodeCollapsible(checked))} + /> + + + + {t('chat.settings.code_wrappable')} + dispatch(setCodeWrappable(checked))} /> + + + + ) +} + +const SettingRowTitleSmall = styled(SettingRowTitle)` + font-size: 13px; +` + +const SettingGroup = styled.div<{ theme?: ThemeMode }>` + padding: 0; + width: 100%; + margin-top: 0; + border-radius: 8px; + margin-bottom: 10px; + margin-top: 10px; +` + +const StyledSelect = styled(Select)` + .ant-select-selector { + border-radius: 15px !important; + padding: 4px 10px !important; + height: 26px !important; + } +` + +const TopViewKey = 'MessageSettingsPopup' + +export default class MessageSettingsPopup { + static topviewId = 0 + static hide() { + TopView.hide(TopViewKey) + } + static show(props: ShowParams) { + return new Promise((resolve) => { + TopView.show(, TopViewKey) + }) + } +} diff --git a/src/renderer/src/components/Popups/TemplatePopup.tsx b/src/renderer/src/components/Popups/TemplatePopup.tsx index 67dc50febf..f9b0494899 100644 --- a/src/renderer/src/components/Popups/TemplatePopup.tsx +++ b/src/renderer/src/components/Popups/TemplatePopup.tsx @@ -15,15 +15,17 @@ const PopupContainer: React.FC = ({ title, resolve }) => { const [open, setOpen] = useState(true) const onOk = () => { + resolve(true) setOpen(false) } const onCancel = () => { + resolve(false) setOpen(false) } const onClose = () => { - resolve({}) + TopView.hide(TopViewKey) } TemplatePopup.hide = onCancel @@ -51,16 +53,7 @@ export default class TemplatePopup { } static show(props: ShowParams) { return new Promise((resolve) => { - TopView.show( - { - resolve(v) - TopView.hide(TopViewKey) - }} - />, - TopViewKey - ) + TopView.show(, TopViewKey) }) } } diff --git a/src/renderer/src/components/Selector.tsx b/src/renderer/src/components/Selector.tsx new file mode 100644 index 0000000000..f405b1202d --- /dev/null +++ b/src/renderer/src/components/Selector.tsx @@ -0,0 +1,76 @@ +import { ConfigProvider, Dropdown } from 'antd' +import { Check, ChevronsUpDown } from 'lucide-react' +import { FC, useMemo } from 'react' +import styled from 'styled-components' + +interface SelectorProps { + options: { label: string; value: string }[] + value: string | number | undefined + placement?: 'topLeft' | 'topCenter' | 'topRight' | 'bottomLeft' | 'bottomCenter' | 'bottomRight' | 'top' | 'bottom' + /** 字体大小 */ + size?: number + onChange: (value: string) => void +} + +const Selector: FC = ({ options, value, onChange, placement = 'bottomRight', size = 13 }) => { + const label = useMemo(() => options?.find((option) => option.value === value)?.label, [options, value]) + + const items = useMemo(() => { + return options.map((option) => ({ + key: option.value, + label: option.label, + extra: {option.value === value && } + })) + }, [options, value]) + + function onClick(e: { key: string }) { + onChange(e.key) + } + + return ( + + + + + + ) +} + +const Label = styled.div<{ $size: number }>` + display: flex; + align-items: center; + gap: 4px; + border-radius: 99px; + padding: 1px 2px 1px 10px; + font-size: ${({ $size }) => $size}px; + cursor: pointer; + transition: background-color 0.2s; + &:hover { + background-color: var(--color-background-mute); + } +` + +const LabelIcon = styled(ChevronsUpDown)` + border-radius: 4px; + padding: 2px 0; + background-color: var(--color-background-mute); +` + +const CheckIcon = styled.div` + width: 20px; + display: flex; + align-items: center; + justify-content: end; +` + +export default Selector diff --git a/src/renderer/src/components/TranslateButton.tsx b/src/renderer/src/components/TranslateButton.tsx index 78dd1b7d34..b96341d777 100644 --- a/src/renderer/src/components/TranslateButton.tsx +++ b/src/renderer/src/components/TranslateButton.tsx @@ -78,7 +78,7 @@ const TranslateButton: FC = ({ text, onTranslated, disabled, style, isLoa title={t('chat.input.translate', { target_language: t(`languages.${targetLanguage.toString()}`) })} arrow> - {isTranslating ? : } + {isTranslating ? : } ) diff --git a/src/renderer/src/components/app/Navbar.tsx b/src/renderer/src/components/app/Navbar.tsx index e6176c2a00..384622c96f 100644 --- a/src/renderer/src/components/app/Navbar.tsx +++ b/src/renderer/src/components/app/Navbar.tsx @@ -1,24 +1,16 @@ import { isLinux, isMac, isWindows } from '@renderer/config/constant' import { useFullscreen } from '@renderer/hooks/useFullscreen' -import useNavBackgroundColor from '@renderer/hooks/useNavBackgroundColor' +import { Button } from 'antd' +import { CircleArrowLeft, X } from 'lucide-react' import type { FC, PropsWithChildren } from 'react' import type { HTMLAttributes } from 'react' +import { useNavigate } from 'react-router-dom' import styled from 'styled-components' type Props = PropsWithChildren & HTMLAttributes export const Navbar: FC = ({ children, ...props }) => { - const backgroundColor = useNavBackgroundColor() - - return ( - - {children} - - ) -} - -export const NavbarLeft: FC = ({ children, ...props }) => { - return {children} + return {children} } export const NavbarCenter: FC = ({ children, ...props }) => { @@ -36,41 +28,52 @@ export const NavbarRight: FC = ({ children, ...props }) => { export const NavbarMain: FC = ({ children, ...props }) => { const isFullscreen = useFullscreen() + return ( + {children} + ) } +const MacCloseIcon = () => { + const navigate = useNavigate() + + if (!isMac) { + return null + } + + return diff --git a/src/renderer/src/pages/history/components/TopicMessages.tsx b/src/renderer/src/pages/history/components/TopicMessages.tsx index 27372db4f3..49f1c2ac89 100644 --- a/src/renderer/src/pages/history/components/TopicMessages.tsx +++ b/src/renderer/src/pages/history/components/TopicMessages.tsx @@ -2,12 +2,12 @@ import { ArrowRightOutlined, MessageOutlined } from '@ant-design/icons' import { HStack } from '@renderer/components/Layout' import SearchPopup from '@renderer/components/Popups/SearchPopup' import { MessageEditingProvider } from '@renderer/context/MessageEditingContext' +import { useChat } from '@renderer/hooks/useChat' import useScrollPosition from '@renderer/hooks/useScrollPosition' import { useSettings } from '@renderer/hooks/useSettings' import { getAssistantById } from '@renderer/services/AssistantService' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' -import { isGenerating, locateToMessage } from '@renderer/services/MessagesService' -import NavigationService from '@renderer/services/NavigationService' +import { locateToMessage } from '@renderer/services/MessagesService' import { useAppDispatch } from '@renderer/store' import { loadTopicMessagesThunk } from '@renderer/store/thunk/messageThunk' import { Topic } from '@renderer/types' @@ -23,10 +23,10 @@ interface Props extends React.HTMLAttributes { } const TopicMessages: FC = ({ topic, ...props }) => { - const navigate = NavigationService.navigate! const { handleScroll, containerRef } = useScrollPosition('TopicMessages') const { messageStyle } = useSettings() const dispatch = useAppDispatch() + const { setActiveAssistant, setActiveTopic } = useChat() useEffect(() => { topic && dispatch(loadTopicMessagesThunk(topic.id)) @@ -39,11 +39,13 @@ const TopicMessages: FC = ({ topic, ...props }) => { } const onContinueChat = async (topic: Topic) => { - await isGenerating() SearchPopup.hide() const assistant = getAssistantById(topic.assistantId) - navigate('/', { state: { assistant, topic } }) - setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR), 100) + if (assistant) { + setActiveAssistant(assistant) + setActiveTopic(topic) + setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR), 100) + } } return ( @@ -57,7 +59,7 @@ const TopicMessages: FC = ({ topic, ...props }) => { type="text" size="middle" style={{ color: 'var(--color-text-3)', position: 'absolute', right: 0, top: 5 }} - onClick={() => locateToMessage(navigate, message)} + onClick={() => locateToMessage({ message, setActiveAssistant, setActiveTopic })} icon={} /> diff --git a/src/renderer/src/pages/home/Chat.tsx b/src/renderer/src/pages/home/Chat.tsx index e2fdbb740c..1e2851d92c 100644 --- a/src/renderer/src/pages/home/Chat.tsx +++ b/src/renderer/src/pages/home/Chat.tsx @@ -1,12 +1,10 @@ import { ContentSearch, ContentSearchRef } from '@renderer/components/ContentSearch' import MultiSelectActionPopup from '@renderer/components/Popups/MultiSelectionPopup' import { QuickPanelProvider } from '@renderer/components/QuickPanel' -import { useAssistant } from '@renderer/hooks/useAssistant' +import { useChat } from '@renderer/hooks/useChat' import { useChatContext } from '@renderer/hooks/useChatContext' import { useSettings } from '@renderer/hooks/useSettings' import { useShortcut } from '@renderer/hooks/useShortcuts' -import { useShowTopics } from '@renderer/hooks/useStore' -import { Assistant, Topic } from '@renderer/types' import { Flex } from 'antd' import { debounce } from 'lodash' import React, { FC, useMemo, useState } from 'react' @@ -15,31 +13,20 @@ import styled from 'styled-components' import Inputbar from './Inputbar/Inputbar' import Messages from './Messages/Messages' -import Tabs from './Tabs' -interface Props { - assistant: Assistant - activeTopic: Topic - setActiveTopic: (topic: Topic) => void - setActiveAssistant: (assistant: Assistant) => void -} - -const Chat: FC = (props) => { - const { assistant } = useAssistant(props.assistant.id) - const { topicPosition, messageStyle, showAssistants } = useSettings() - const { showTopics } = useShowTopics() - const { isMultiSelectMode } = useChatContext(props.activeTopic) +const Chat: FC = () => { + const { activeAssistant, activeTopic, setActiveTopic } = useChat() + const { messageStyle, showAssistants } = useSettings() + const { isMultiSelectMode } = useChatContext(activeTopic) const mainRef = React.useRef(null) const contentSearchRef = React.useRef(null) const [filterIncludeUser, setFilterIncludeUser] = useState(false) const maxWidth = useMemo(() => { - const showRightTopics = showTopics && topicPosition === 'right' const minusAssistantsWidth = showAssistants ? '- var(--assistants-width)' : '' - const minusRightTopicsWidth = showRightTopics ? '- var(--assistants-width)' : '' - return `calc(100vw - var(--sidebar-width) ${minusAssistantsWidth} ${minusRightTopicsWidth})` - }, [showAssistants, showTopics, topicPosition]) + return `calc(100vw - ${minusAssistantsWidth})` + }, [showAssistants]) useHotkeys('esc', () => { contentSearchRef.current?.disable() @@ -116,36 +103,24 @@ const Chat: FC = (props) => { onIncludeUserChange={userOutlinedItemClickHandler} /> - - {isMultiSelectMode && } + + {isMultiSelectMode && } - {topicPosition === 'right' && showTopics && ( - - )} ) } const Container = styled.div` - display: flex; - flex-direction: row; height: 100%; - flex: 1; ` const Main = styled(Flex)` diff --git a/src/renderer/src/pages/home/ChatNavbar.tsx b/src/renderer/src/pages/home/ChatNavbar.tsx new file mode 100644 index 0000000000..9c81b82d54 --- /dev/null +++ b/src/renderer/src/pages/home/ChatNavbar.tsx @@ -0,0 +1,140 @@ +import { Navbar } from '@renderer/components/app/Navbar' +import NarrowModeIcon from '@renderer/components/Icons/NarrowModeIcon' +import { HStack } from '@renderer/components/Layout' +import MinAppsPopover from '@renderer/components/Popups/MinAppsPopover' +import SearchPopup from '@renderer/components/Popups/SearchPopup' +import { isLinux, isMac, isWindows } from '@renderer/config/constant' +import { useAssistant } from '@renderer/hooks/useAssistant' +import { useChat } from '@renderer/hooks/useChat' +import { useFullscreen } from '@renderer/hooks/useFullscreen' +import { modelGenerating } from '@renderer/hooks/useRuntime' +import { useSettings } from '@renderer/hooks/useSettings' +import { useShortcut } from '@renderer/hooks/useShortcuts' +import { useShowAssistants } from '@renderer/hooks/useStore' +import { useAppDispatch } from '@renderer/store' +import { setNarrowMode } from '@renderer/store/settings' +import { Tooltip } from 'antd' +import { t } from 'i18next' +import { LayoutGrid, PanelLeft, PanelRight, Search } from 'lucide-react' +import { FC } from 'react' +import styled from 'styled-components' + +import SelectModelButton from './components/SelectModelButton' +import UpdateAppButton from './components/UpdateAppButton' + +const ChatNavbar: FC = () => { + const { activeAssistant } = useChat() + const { assistant } = useAssistant(activeAssistant.id) + const { showAssistants, toggleShowAssistants } = useShowAssistants() + const isFullscreen = useFullscreen() + const { sidebarIcons, narrowMode } = useSettings() + const dispatch = useAppDispatch() + + useShortcut('search_message', SearchPopup.show) + + const handleNarrowModeToggle = async () => { + await modelGenerating() + dispatch(setNarrowMode(!narrowMode)) + } + + return ( + + + + toggleShowAssistants()} + style={{ marginRight: 8, marginLeft: isMac && !isFullscreen ? 4 : -12 }}> + {showAssistants ? : } + + + + + + {isMac && ( + + SearchPopup.show()}> + + + + )} + + + + + + {sidebarIcons.visible.includes('minapp') && ( + + + + + + + + )} + + + + ) +} + +const NavbarContainer = styled.div<{ $isFullscreen: boolean; $showSidebar: boolean }>` + flex: 1; + display: flex; + flex-direction: row; + align-items: center; + height: var(--navbar-height); + max-height: var(--navbar-height); + min-height: var(--navbar-height); + justify-content: space-between; + padding-left: ${({ $showSidebar }) => (isMac ? ($showSidebar ? '10px' : '75px') : '25px')}; + font-weight: bold; + color: var(--color-text-1); + padding-right: ${({ $isFullscreen }) => ($isFullscreen ? '12px' : isWindows ? '140px' : isLinux ? '120px' : '12px')}; + -webkit-app-region: drag; +` + +export const NavbarIcon = styled.div` + -webkit-app-region: none; + border-radius: 8px; + height: 30px; + padding: 0 7px; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + transition: all 0.2s ease-in-out; + cursor: pointer; + .iconfont { + font-size: 18px; + color: var(--color-icon); + &.icon-a-addchat { + font-size: 20px; + } + &.icon-a-darkmode { + font-size: 20px; + } + &.icon-appstore { + font-size: 20px; + } + } + .anticon { + color: var(--color-icon); + font-size: 16px; + } + &:hover { + background-color: var(--color-background-mute); + color: var(--color-icon-white); + } + &.active { + background-color: var(--color-background-mute); + color: var(--color-icon-white); + } +` + +const NarrowIcon = styled(NavbarIcon)` + @media (max-width: 1000px) { + display: none; + } +` + +export default ChatNavbar diff --git a/src/renderer/src/pages/home/HomePage.tsx b/src/renderer/src/pages/home/HomePage.tsx index 3bc47468fd..45b2ac2367 100644 --- a/src/renderer/src/pages/home/HomePage.tsx +++ b/src/renderer/src/pages/home/HomePage.tsx @@ -1,58 +1,15 @@ -import { useAssistants } from '@renderer/hooks/useAssistant' import { useSettings } from '@renderer/hooks/useSettings' -import { useActiveTopic } from '@renderer/hooks/useTopic' -import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' -import NavigationService from '@renderer/services/NavigationService' -import { Assistant } from '@renderer/types' -import { FC, useEffect, useState } from 'react' -import { useLocation, useNavigate } from 'react-router-dom' +import { FC, useEffect } from 'react' import styled from 'styled-components' import Chat from './Chat' -import Navbar from './Navbar' -import HomeTabs from './Tabs' - -let _activeAssistant: Assistant +import ChatNavbar from './ChatNavbar' const HomePage: FC = () => { - const { assistants } = useAssistants() - const navigate = useNavigate() - - const location = useLocation() - const state = location.state - - const [activeAssistant, setActiveAssistant] = useState(state?.assistant || _activeAssistant || assistants[0]) - const { activeTopic, setActiveTopic } = useActiveTopic(activeAssistant, state?.topic) const { showAssistants, showTopics, topicPosition } = useSettings() - _activeAssistant = activeAssistant - useEffect(() => { - NavigationService.setNavigate(navigate) - }, [navigate]) - - useEffect(() => { - state?.assistant && setActiveAssistant(state?.assistant) - state?.topic && setActiveTopic(state?.topic) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [state]) - - useEffect(() => { - const unsubscribe = EventEmitter.on(EVENT_NAMES.SWITCH_ASSISTANT, (assistantId: string) => { - const newAssistant = assistants.find((a) => a.id === assistantId) - if (newAssistant) { - setActiveAssistant(newAssistant) - } - }) - - return () => { - unsubscribe() - } - }, [assistants, setActiveAssistant]) - - useEffect(() => { - const canMinimize = topicPosition == 'left' ? !showAssistants : !showAssistants && !showTopics - window.api.window.setMinimumSize(canMinimize ? 520 : 1080, 600) + window.api.window.setMinimumSize(showAssistants ? 1080 : 520, 600) return () => { window.api.window.resetMinimumSize() @@ -61,45 +18,22 @@ const HomePage: FC = () => { return ( - + - {showAssistants && ( - - )} - + ) } const Container = styled.div` + min-width: 0; display: flex; flex: 1; flex-direction: column; - max-width: calc(100vw - var(--sidebar-width)); ` const ContentContainer = styled.div` - display: flex; - flex: 1; - flex-direction: row; overflow: hidden; ` diff --git a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx index 83b3373093..60d3ed8f3e 100644 --- a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx +++ b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx @@ -11,11 +11,12 @@ import { } from '@renderer/config/models' import db from '@renderer/databases' import { useAssistant } from '@renderer/hooks/useAssistant' +import { useChat } from '@renderer/hooks/useChat' import { useKnowledgeBases } from '@renderer/hooks/useKnowledge' import { useMCPServers } from '@renderer/hooks/useMCPServers' import { useMessageOperations, useTopicLoading } from '@renderer/hooks/useMessageOperations' -import { modelGenerating, useRuntime } from '@renderer/hooks/useRuntime' -import { useMessageStyle, useSettings } from '@renderer/hooks/useSettings' +import { useRuntime } from '@renderer/hooks/useRuntime' +import { useSettings } from '@renderer/hooks/useSettings' import { useShortcut, useShortcutDisplay } from '@renderer/hooks/useShortcuts' import { useSidebarIconShow } from '@renderer/hooks/useSidebarIcon' import { getDefaultTopic } from '@renderer/services/AssistantService' @@ -30,7 +31,7 @@ import WebSearchService from '@renderer/services/WebSearchService' import { useAppDispatch, useAppSelector } from '@renderer/store' import { setSearching } from '@renderer/store/runtime' import { sendMessage as _sendMessage } from '@renderer/store/thunk/messageThunk' -import { Assistant, FileType, KnowledgeBase, KnowledgeItem, Model, Topic } from '@renderer/types' +import { FileType, KnowledgeBase, KnowledgeItem, Model } from '@renderer/types' import type { MessageInputBaseParams } from '@renderer/types/newMessage' import { classNames, delay, formatFileSize, getFileExtension } from '@renderer/utils' import { formatQuotedText } from '@renderer/utils/formats' @@ -52,21 +53,17 @@ import InputbarTools, { InputbarToolsRef } from './InputbarTools' import KnowledgeBaseInput from './KnowledgeBaseInput' import MentionModelsInput from './MentionModelsInput' import SendMessageButton from './SendMessageButton' +import SettingButton from './SettingButton' import TokenCount from './TokenCount' -interface Props { - assistant: Assistant - setActiveTopic: (topic: Topic) => void - topic: Topic -} - let _text = '' let _files: FileType[] = [] -const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) => { +const Inputbar: FC = () => { + const { activeAssistant, activeTopic: topic, setActiveTopic } = useChat() const [text, setText] = useState(_text) const [inputFocus, setInputFocus] = useState(false) - const { assistant, addTopic, model, setModel, updateAssistant } = useAssistant(_assistant.id) + const { assistant, addTopic, model, setModel, updateAssistant } = useAssistant(activeAssistant.id) const { targetLanguage, sendMessageShortcut, @@ -86,7 +83,6 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = const { t } = useTranslation() const containerRef = useRef(null) const { searching } = useRuntime() - const { isBubbleStyle } = useMessageStyle() const { pauseMessages } = useMessageOperations(topic) const loading = useTopicLoading(topic) const dispatch = useAppDispatch() @@ -139,17 +135,20 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = _text = text _files = files - const resizeTextArea = useCallback(() => { - const textArea = textareaRef.current?.resizableTextArea?.textArea - if (textArea) { - // 如果已经手动设置了高度,则不自动调整 - if (textareaHeight) { - return + const resizeTextArea = useCallback( + (force: boolean = false) => { + const textArea = textareaRef.current?.resizableTextArea?.textArea + if (textArea) { + // 如果已经手动设置了高度,则不自动调整 + if (textareaHeight && !force) { + return + } + textArea.style.height = 'auto' + textArea.style.height = textArea?.scrollHeight > 400 ? '400px' : `${textArea?.scrollHeight}px` } - textArea.style.height = 'auto' - textArea.style.height = textArea?.scrollHeight > 400 ? '400px' : `${textArea?.scrollHeight}px` - } - }, [textareaHeight]) + }, + [textareaHeight] + ) const sendMessage = useCallback(async () => { if (inputEmpty || loading) { @@ -405,8 +404,6 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = } const addNewTopic = useCallback(async () => { - await modelGenerating() - const topic = getDefaultTopic(assistant.id) await db.topics.add({ id: topic.id, messages: [] }) @@ -629,11 +626,6 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = useEffect(() => { const _setEstimateTokenCount = debounce(setEstimateTokenCount, 100, { leading: false, trailing: true }) const unsubscribes = [ - // EventEmitter.on(EVENT_NAMES.EDIT_MESSAGE, (message: Message) => { - // setText(message.content) - // textareaRef.current?.focus() - // setTimeout(() => resizeTextArea(), 0) - // }), EventEmitter.on(EVENT_NAMES.ESTIMATED_TOKEN_COUNT, ({ tokensCount, contextCount }) => { _setEstimateTokenCount(tokensCount) setContextCount({ current: contextCount.current, max: contextCount.max }) // 现在contextCount是一个对象而不是单个数值 @@ -693,8 +685,6 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = setSelectedKnowledgeBases(showKnowledgeIcon ? (assistant.knowledge_bases ?? []) : []) }, [assistant.id, assistant.knowledge_bases, showKnowledgeIcon]) - const textareaRows = window.innerHeight >= 1000 || isBubbleStyle ? 2 : 1 - const handleKnowledgeBaseSelect = (bases?: KnowledgeBase[]) => { updateAssistant({ ...assistant, knowledge_bases: bases }) setSelectedKnowledgeBases(bases ?? []) @@ -752,12 +742,7 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = } else { textArea.style.height = 'auto' setTextareaHeight(undefined) - requestAnimationFrame(() => { - if (textArea) { - const contentHeight = textArea.scrollHeight - textArea.style.height = contentHeight > 400 ? '400px' : `${contentHeight}px` - } - }) + setTimeout(() => resizeTextArea(true), 0) } textareaRef.current?.focus() @@ -802,7 +787,7 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = contextMenu="true" variant="borderless" spellCheck={false} - rows={textareaRows} + rows={2} ref={textareaRef} style={{ fontSize, @@ -858,11 +843,12 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = ToolbarButton={ToolbarButton} onClick={onNewContext} /> + {loading && ( - - + + )} @@ -909,10 +895,10 @@ const Container = styled.div` ` const InputBarContainer = styled.div` - border: 0.5px solid var(--color-border); + border: 1px solid var(--color-border); transition: all 0.2s ease; position: relative; - margin: 14px 20px; + margin: 16px 20px; margin-top: 0; border-radius: 15px; padding-top: 6px; // 为拖动手柄留出空间 @@ -949,10 +935,13 @@ const Textarea = styled(TextArea)` overflow: auto; width: 100%; box-sizing: border-box; - transition: height 0.2s ease; + transition: none !important; &.ant-input { line-height: 1.4; } + .ant-input-textarea-show-count::after { + transition: none !important; + } ` const Toolbar = styled.div` @@ -961,7 +950,7 @@ const Toolbar = styled.div` justify-content: space-between; padding: 0 8px; padding-bottom: 0; - margin-bottom: 4px; + margin-bottom: 5px; height: 30px; gap: 16px; position: relative; diff --git a/src/renderer/src/pages/home/Inputbar/MCPToolsButton.tsx b/src/renderer/src/pages/home/Inputbar/MCPToolsButton.tsx index b022377f18..e069f77685 100644 --- a/src/renderer/src/pages/home/Inputbar/MCPToolsButton.tsx +++ b/src/renderer/src/pages/home/Inputbar/MCPToolsButton.tsx @@ -176,7 +176,7 @@ const MCPToolsButton: FC = ({ ref, setInputValue, resizeTextArea, Toolbar newList.push({ label: t('settings.mcp.addServer') + '...', icon: , - action: () => navigate('/settings/mcp') + action: () => navigate('/mcp-servers') }) newList.unshift({ diff --git a/src/renderer/src/pages/home/Inputbar/SendMessageButton.tsx b/src/renderer/src/pages/home/Inputbar/SendMessageButton.tsx index 0c807e1d20..51f956ea9e 100644 --- a/src/renderer/src/pages/home/Inputbar/SendMessageButton.tsx +++ b/src/renderer/src/pages/home/Inputbar/SendMessageButton.tsx @@ -13,7 +13,7 @@ const SendMessageButton: FC = ({ disabled, sendMessage }) => { style={{ cursor: disabled ? 'not-allowed' : 'pointer', color: disabled ? 'var(--color-text-3)' : 'var(--color-primary)', - fontSize: 22, + fontSize: 30, transition: 'all 0.2s', marginRight: 2 }} diff --git a/src/renderer/src/pages/home/Inputbar/SettingButton.tsx b/src/renderer/src/pages/home/Inputbar/SettingButton.tsx new file mode 100644 index 0000000000..f35f8b170c --- /dev/null +++ b/src/renderer/src/pages/home/Inputbar/SettingButton.tsx @@ -0,0 +1,31 @@ +import { Assistant } from '@renderer/types' +import { Popover } from 'antd' +import { SlidersHorizontal } from 'lucide-react' +import { FC } from 'react' + +import SettingsTab from '../Tabs/SettingsTab' + +interface Props { + assistant: Assistant + ToolbarButton: any +} + +const SettingButton: FC = ({ ToolbarButton }) => { + return ( + } + trigger="click" + styles={{ + body: { + padding: '4px 2px 4px 2px' + } + }}> + + + + + ) +} + +export default SettingButton diff --git a/src/renderer/src/pages/home/MainSidebar/MainNavbar.tsx b/src/renderer/src/pages/home/MainSidebar/MainNavbar.tsx new file mode 100644 index 0000000000..b2e2a57cb1 --- /dev/null +++ b/src/renderer/src/pages/home/MainSidebar/MainNavbar.tsx @@ -0,0 +1,72 @@ +import SearchPopup from '@renderer/components/Popups/SearchPopup' +import { isMac } from '@renderer/config/constant' +import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' +import { Tooltip } from 'antd' +import { t } from 'i18next' +import { MessageSquareDiff, Search } from 'lucide-react' +import { FC } from 'react' +import styled from 'styled-components' + +interface Props {} + +const HeaderNavbar: FC = () => { + return ( + +
+ {!isMac && ( + + SearchPopup.show()}> + + + + )} +
+ + EventEmitter.emit(EVENT_NAMES.ADD_NEW_TOPIC)} style={{ marginRight: 5 }}> + + + +
+ ) +} + +const Container = styled.div` + display: flex; + width: var(--assistant-width); + flex-direction: row; + justify-content: space-between; + align-items: center; + padding: 0; + height: var(--navbar-height); + min-height: var(--navbar-height); + background-color: transparent; + -webkit-app-region: drag; + padding: 0 8px; + padding-left: ${isMac ? '75px' : '10px'}; +` + +export const NavbarIcon = styled.div` + -webkit-app-region: none; + border-radius: 8px; + height: 30px; + padding: 0 7px; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + transition: all 0.2s ease-in-out; + -webkit-app-region: no-drag; + cursor: pointer; + &:hover { + background-color: var(--color-background-mute); + color: var(--color-icon-white); + } +` + +const NarrowIcon = styled(NavbarIcon)` + @media (max-width: 1000px) { + display: none; + } +` + +export default HeaderNavbar diff --git a/src/renderer/src/pages/home/MainSidebar/MainSidebar.tsx b/src/renderer/src/pages/home/MainSidebar/MainSidebar.tsx new file mode 100644 index 0000000000..65b5753a62 --- /dev/null +++ b/src/renderer/src/pages/home/MainSidebar/MainSidebar.tsx @@ -0,0 +1,362 @@ +import EmojiAvatar from '@renderer/components/Avatar/EmojiAvatar' +import UserPopup from '@renderer/components/Popups/UserPopup' +import { UserAvatar } from '@renderer/config/env' +import { useTheme } from '@renderer/context/ThemeProvider' +import { useAssistants } from '@renderer/hooks/useAssistant' +import useAvatar from '@renderer/hooks/useAvatar' +import { useChat } from '@renderer/hooks/useChat' +import { useSettings } from '@renderer/hooks/useSettings' +import { useShortcut } from '@renderer/hooks/useShortcuts' +import { useShowAssistants } from '@renderer/hooks/useStore' +import AssistantItem from '@renderer/pages/home/Tabs/components/AssistantItem' +import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' +import { ThemeMode } from '@renderer/types' +import { isEmoji } from '@renderer/utils' +import { Avatar, Tooltip } from 'antd' +import { AnimatePresence, motion } from 'framer-motion' +import { + Blocks, + ChevronDown, + ChevronRight, + FileSearch, + Folder, + Languages, + LayoutGrid, + Moon, + Palette, + Settings, + Sparkle, + SquareTerminal, + Sun, + SunMoon +} from 'lucide-react' +import { FC, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useLocation, useNavigate } from 'react-router-dom' +import styled from 'styled-components' + +import Tabs from '../../../pages/home/Tabs' +import MainNavbar from './MainNavbar' +import { + Container, + MainMenu, + MainMenuItem, + MainMenuItemIcon, + MainMenuItemLeft, + MainMenuItemRight, + MainMenuItemText, + SubMenu +} from './MainSidebarStyles' +import OpenedMinappTabs from './OpenedMinapps' +import PinnedApps from './PinnedApps' + +type Tab = 'assistants' | 'topic' + +const MainSidebar: FC = () => { + const { assistants } = useAssistants() + const navigate = useNavigate() + const [tab, setTab] = useState('assistants') + const avatar = useAvatar() + const { userName, defaultPaintingProvider } = useSettings() + const { t } = useTranslation() + const { theme } = useTheme() + const [isAppMenuExpanded, setIsAppMenuExpanded] = useState(false) + const { showAssistants, toggleShowAssistants } = useShowAssistants() + + const location = useLocation() + const { pathname } = location + + const { activeAssistant, activeTopic, setActiveAssistant, setActiveTopic } = useChat() + const { showTopics } = useSettings() + + useShortcut('toggle_show_assistants', toggleShowAssistants) + useShortcut('toggle_show_topics', () => EventEmitter.emit(EVENT_NAMES.SWITCH_TOPIC_SIDEBAR)) + + useEffect(() => { + const unsubscribe = [ + EventEmitter.on(EVENT_NAMES.SHOW_TOPIC_SIDEBAR, () => setTab('topic')), + EventEmitter.on(EVENT_NAMES.SWITCH_TOPIC_SIDEBAR, () => { + setTab(tab === 'topic' ? 'assistants' : 'topic') + !showAssistants && toggleShowAssistants() + }) + ] + return () => unsubscribe.forEach((unsubscribe) => unsubscribe()) + }, [isAppMenuExpanded, showAssistants, tab, toggleShowAssistants]) + + useEffect(() => { + const unsubscribes = [ + EventEmitter.on(EVENT_NAMES.SWITCH_ASSISTANT, (assistantId: string) => { + const newAssistant = assistants.find((a) => a.id === assistantId) + if (newAssistant) { + setActiveAssistant(newAssistant) + } + }), + EventEmitter.on(EVENT_NAMES.SWITCH_TOPIC_SIDEBAR, () => setTab(tab === 'topic' ? 'assistants' : 'topic')), + EventEmitter.on(EVENT_NAMES.OPEN_MINAPP, () => { + setTimeout(() => setIsAppMenuExpanded(false), 1000) + }) + ] + + return () => unsubscribes.forEach((unsubscribe) => unsubscribe()) + }, [assistants, setActiveAssistant, tab]) + + useEffect(() => { + const canMinimize = !showAssistants && !showTopics + window.api.window.setMinimumSize(canMinimize ? 520 : 1080, 600) + + return () => { + window.api.window.resetMinimumSize() + } + }, [showAssistants, showTopics]) + + useEffect(() => { + setIsAppMenuExpanded(false) + }, [activeAssistant.id, activeTopic.id]) + + const appMenuItems = [ + { icon: , text: t('agents.title'), path: '/agents' }, + { icon: , text: t('translate.title'), path: '/translate' }, + { + icon: , + text: t('paintings.title'), + path: `/paintings/${defaultPaintingProvider}` + }, + { icon: , text: t('minapp.title'), path: '/apps' }, + { icon: , text: t('knowledge.title'), path: '/knowledge' }, + { icon: , text: t('settings.mcp.title'), path: '/mcp-servers' }, + { icon: , text: t('files.title'), path: '/files' } + ] + + const isRoutes = (path: string): boolean => pathname.startsWith(path) + + if (!showAssistants) { + return null + } + + if (location.pathname !== '/') { + return null + } + + return ( + + + + setIsAppMenuExpanded(!isAppMenuExpanded)}> + + + + + {isAppMenuExpanded ? t('common.collapse') : t('common.apps')} + + + {isAppMenuExpanded ? ( + + ) : ( + + )} + + + + {isAppMenuExpanded && ( + + + {appMenuItems.map((item) => ( + { + navigate(item.path) + setIsAppMenuExpanded(false) + }}> + + {item.icon} + {item.text} + + + ))} + + + + )} + + + + + {tab === 'topic' && ( + setIsAppMenuExpanded(false)}> + {}} + onDelete={() => {}} + addAgent={() => {}} + addAssistant={() => {}} + onCreateDefaultAssistant={() => {}} + handleSortByChange={() => {}} + singleLine + /> + + )} + + + + UserPopup.show()}> + {isEmoji(avatar) ? ( + + {avatar} + + ) : ( + + )} + {userName} + + + navigate('/settings/provider')} className="settings-icon"> + + + + + + ) +} + +export const ThemeIcon = () => { + const { t } = useTranslation() + const { theme, settedTheme, toggleTheme } = useTheme() + + const onChageTheme = (e: React.MouseEvent) => { + e.stopPropagation() + toggleTheme() + } + + return ( + + + {settedTheme === ThemeMode.dark ? ( + + ) : settedTheme === ThemeMode.light ? ( + + ) : ( + + )} + + + ) +} + +const AssistantContainer = styled.div` + margin: 0 10px; + margin-top: 4px; +` + +const UserMenu = styled.div` + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + margin: 10px; + margin-bottom: 10px; + gap: 5px; + border-radius: 8px; +` + +const UserMenuLeft = styled.div` + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; + padding: 4px 8px; + border-radius: 8px; + + &:hover { + background-color: var(--color-list-item); + } +` + +const AvatarImg = styled(Avatar)` + width: 28px; + height: 28px; + background-color: var(--color-background-soft); + border: none; + cursor: pointer; +` + +const UserMenuText = styled.div` + font-size: 14px; + font-weight: 500; +` + +const Icon = styled.div<{ theme: string }>` + width: 30px; + height: 30px; + display: flex; + justify-content: center; + align-items: center; + border-radius: 50%; + box-sizing: border-box; + -webkit-app-region: none; + border: 0.5px solid transparent; + &.settings-icon { + width: 34px; + height: 34px; + } + &:hover { + background-color: ${({ theme }) => (theme === 'dark' ? 'var(--color-black)' : 'var(--color-white)')}; + opacity: 0.8; + cursor: pointer; + .icon { + color: var(--color-icon-white); + } + } + &.active { + background-color: ${({ theme }) => (theme === 'dark' ? 'var(--color-black)' : 'var(--color-white)')}; + border: 0.5px solid var(--color-border); + .icon { + color: var(--color-primary); + } + } + + @keyframes borderBreath { + 0% { + opacity: 0.1; + } + 50% { + opacity: 1; + } + 100% { + opacity: 0.1; + } + } + + &.opened-minapp { + position: relative; + } + &.opened-minapp::after { + content: ''; + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + border-radius: inherit; + opacity: 0.3; + border: 0.5px solid var(--color-primary); + } +` + +export default MainSidebar diff --git a/src/renderer/src/pages/home/MainSidebar/MainSidebarStyles.tsx b/src/renderer/src/pages/home/MainSidebar/MainSidebarStyles.tsx new file mode 100644 index 0000000000..f0ae3f7f74 --- /dev/null +++ b/src/renderer/src/pages/home/MainSidebar/MainSidebarStyles.tsx @@ -0,0 +1,94 @@ +import Scrollbar from '@renderer/components/Scrollbar' +import styled from 'styled-components' + +export const MainMenuItem = styled.div<{ active?: boolean }>` + display: flex; + justify-content: space-between; + align-items: center; + gap: 5px; + background-color: ${({ active }) => (active ? 'var(--color-list-item)' : 'transparent')}; + padding: 5px 10px; + border-radius: 5px; + border-radius: 8px; + opacity: ${({ active }) => (active ? 0.6 : 1)}; + &.active { + background-color: var(--color-list-item); + } + &:hover { + background-color: ${({ active }) => (active ? 'var(--color-list-item)' : 'var(--color-list-item-hover)')}; + } +` + +export const MainMenuItemLeft = styled.div` + display: flex; + align-items: center; + gap: 10px; +` + +export const MainMenuItemRight = styled.div` + display: flex; + align-items: center; + gap: 5px; + margin-right: -3px; +` + +export const MainMenuItemIcon = styled.div` + display: flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; +` + +export const MainMenuItemText = styled.div` + font-size: 14px; + font-weight: 500; +` + +export const Container = styled.div` + display: flex; + flex-direction: column; + width: var(--assistant-width); + max-width: var(--assistant-width); + border-right: 0.5px solid var(--color-border); +` + +export const MainMenu = styled.div` + display: flex; + flex-direction: column; + padding: 0 10px; +` + +export const SubMenu = styled.div` + display: flex; + flex-direction: column; + gap: 5px; + overflow: hidden; + padding: 5px 0; +` + +export const TabsContainer = styled.div` + display: flex; + flex-direction: column; + align-items: center; + -webkit-app-region: none; + position: relative; + width: 100%; + margin-top: 5px; + + &::-webkit-scrollbar { + display: none; + } +` + +export const TabsWrapper = styled(Scrollbar as any)` + width: 100%; + padding: 5px 0; + max-height: 50vh; +` + +export const Menus = styled.div` + display: flex; + flex-direction: column; + gap: 5px; +` diff --git a/src/renderer/src/pages/home/MainSidebar/OpenedMinapps.tsx b/src/renderer/src/pages/home/MainSidebar/OpenedMinapps.tsx new file mode 100644 index 0000000000..d8638c3444 --- /dev/null +++ b/src/renderer/src/pages/home/MainSidebar/OpenedMinapps.tsx @@ -0,0 +1,121 @@ +import MinAppIcon from '@renderer/components/Icons/MinAppIcon' +import IndicatorLight from '@renderer/components/IndicatorLight' +import { Center } from '@renderer/components/Layout' +import { useMinappPopup } from '@renderer/hooks/useMinappPopup' +import { useRuntime } from '@renderer/hooks/useRuntime' +import { useSettings } from '@renderer/hooks/useSettings' +import type { MenuProps } from 'antd' +import { Empty } from 'antd' +import { Dropdown } from 'antd' +import { isEmpty } from 'lodash' +import { FC, useEffect } from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' + +import { + MainMenuItem, + MainMenuItemIcon, + MainMenuItemLeft, + MainMenuItemRight, + MainMenuItemText, + Menus, + TabsContainer, + TabsWrapper +} from './MainSidebarStyles' + +const OpenedMinapps: FC = () => { + const { minappShow, openedKeepAliveMinapps, currentMinappId } = useRuntime() + const { openMinappKeepAlive, hideMinappPopup, closeMinapp, closeAllMinapps } = useMinappPopup() + const { showOpenedMinappsInSidebar } = useSettings() + const { t } = useTranslation() + + const handleOnClick = (app) => { + if (minappShow && currentMinappId === app.id) { + hideMinappPopup() + } else { + openMinappKeepAlive(app) + } + } + + useEffect(() => { + 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 + 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 = showOpenedMinappsInSidebar && openedKeepAliveMinapps.length > 0 + + if (!isShowOpened) return + + return ( + + + + + {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() + } + ] + + return ( + handleOnClick(app)}> + + + + + {app.name} + + + + + + + + ) + })} + {isEmpty(openedKeepAliveMinapps) && ( +
+ +
+ )} +
+
+ +
+ ) +} + +const Divider = styled.div` + width: 100%; + height: 1px; + background-color: var(--color-border); + margin: 5px 0; + opacity: 0.5; +` + +export default OpenedMinapps diff --git a/src/renderer/src/pages/home/MainSidebar/PinnedApps.tsx b/src/renderer/src/pages/home/MainSidebar/PinnedApps.tsx new file mode 100644 index 0000000000..ab77cd2893 --- /dev/null +++ b/src/renderer/src/pages/home/MainSidebar/PinnedApps.tsx @@ -0,0 +1,67 @@ +import DragableList from '@renderer/components/DragableList' +import MinAppIcon from '@renderer/components/Icons/MinAppIcon' +import { useMinappPopup } from '@renderer/hooks/useMinappPopup' +import { useMinapps } from '@renderer/hooks/useMinapps' +import { useRuntime } from '@renderer/hooks/useRuntime' +import type { MenuProps } from 'antd' +import { Dropdown } from 'antd' +import { isEmpty } from 'lodash' +import { FC } from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' + +import { MainMenuItem, MainMenuItemIcon, MainMenuItemLeft, MainMenuItemText } from './MainSidebarStyles' + +const PinnedApps: FC = () => { + const { pinned, updatePinnedMinapps } = useMinapps() + const { t } = useTranslation() + const { openMinappKeepAlive } = useMinappPopup() + const { openedKeepAliveMinapps } = useRuntime() + + if (isEmpty(pinned)) { + return null + } + + return ( +
+ + + {(app) => { + const menuItems: MenuProps['items'] = [ + { + key: 'togglePin', + label: t('minapp.sidebar.remove.title'), + onClick: () => { + const newPinned = pinned.filter((item) => item.id !== app.id) + updatePinnedMinapps(newPinned) + } + } + ] + + return ( + + openMinappKeepAlive(app)}> + + + + + {app.name} + + + + ) + }} + + {isEmpty(openedKeepAliveMinapps) && } +
+ ) +} + +const Divider = styled.div` + width: 100%; + height: 1px; + background-color: var(--color-border); + opacity: 0.5; +` + +export default PinnedApps diff --git a/src/renderer/src/pages/home/Markdown/CodeBlock.tsx b/src/renderer/src/pages/home/Markdown/CodeBlock.tsx index 5692d50bf7..f6a2905448 100644 --- a/src/renderer/src/pages/home/Markdown/CodeBlock.tsx +++ b/src/renderer/src/pages/home/Markdown/CodeBlock.tsx @@ -27,7 +27,7 @@ const CodeBlock: React.FC = ({ children, className, id, onSave }) => { {children} ) : ( - + {children} ) diff --git a/src/renderer/src/pages/home/Markdown/Markdown.tsx b/src/renderer/src/pages/home/Markdown/Markdown.tsx index d82905cf7f..686017fe28 100644 --- a/src/renderer/src/pages/home/Markdown/Markdown.tsx +++ b/src/renderer/src/pages/home/Markdown/Markdown.tsx @@ -93,15 +93,19 @@ const Markdown: FC = ({ block }) => { } as Partial }, [onSaveCodeBlock]) - if (messageContent.includes('