feat: new app sidebar

fix: adjust navbar and title bar dimensions, update icon handling

feat: implement ChatNavbar component and enhance MainNavbar with search functionality

fix: invert transparency setting for WindowService based on OS

refactor: clean up MainSidebar and useChat hooks, remove unused state handling and improve topic selection logic

refactor: simplify HomeTabs component by removing unused imports and commented code, update AssistantAddItem hover styles

fix: set WindowService transparency to false for consistent behavior across platforms

feat: add event listener to MainSidebar for topic tab navigation

feat: enhance summarization prompt and add topic sidebar visibility toggle

feat(Inputbar): add SettingButton component for settings access

style(color.scss): update border color to improve UI consistency

style: update chat background colors and margins for improved UI consistency

refactor(MainSidebar, i18n): update MCP title in sidebar and localization files, remove unused MCP entries

feat: remove prompt component

refactor(SettingsTab, OpenAISettingsGroup): restructure settings components for improved readability and maintainability, update layout and styling for better user experience

feat(ChatNavbar): add maximize and minimize icons for narrow mode toggle

style(markdown.scss, CodeBlockView): update border-radius for improved UI consistency and adjust CodeBlock styling

style(SettingsTab, OpenAISettingsGroup): adjust padding and minimum height for improved layout, comment out unused components for cleaner code

fix(i18n, MessageEditor, Settings): update localization keys for code settings and adjust border radius in MessageEditor, add gap to SettingRow for improved layout

feat(ChatNavbar, SVGIcon): add ExpandWidth icon and integrate it into ChatNavbar for narrow mode toggle

refactor(ImageBlock, MessageBlockRenderer): enhance image block styling and layout for better responsiveness

refactor(MessageAttachments): enhance file icon rendering and filename display with truncation for better UX

feat(MainSidebar, AssistantItem): integrate AssistantItem into MainSidebar for topic view, enhance event handling and styling adjustments

refactor(MessageAnchorLine): replace DownOutlined icon with CircleChevronDown for improved styling

feat(NarrowModeIcon, ChatNavbar): add NarrowModeIcon component and integrate it into ChatNavbar for narrow mode toggle

fix(MainSidebar, Inputbar, McpServersList): update event handling, adjust textarea rows, and enhance DragableList styling for improved layout and functionality

feat(MainSidebar): enhance submenu animation with framer-motion for improved user experience

style(CodeEditor, markdown.scss, SettingsTab): update border-radius to inherit and remove unused SettingDivider for improved consistency

style(MainNavbar, Message): adjust padding in MainNavbar and refine alignment logic in MessageItem for improved layout consistency

style(Inputbar, SelectModelButton): adjust margins and padding for improved layout consistency and add icon to SelectModelButton

wip

feat(MainSidebar): restructure sidebar components and add MainNavbar for enhanced navigation and user experience

style(MainSidebar, AssistantsTab): adjust margins and padding for improved layout consistency, integrate Scrollbar component for better scrolling experience

fix(MessageAnchorLine): prevent rendering of clear message type

refactor(SearchMessage, TopicMessages, MessagesService): update locateToMessage function to accept additional parameters for setting active assistant and topic, enhancing navigation logic

style(ColorStyles, Messages): update chat background colors for improved visibility and remove redundant background styles in message bubbles

revert: hide token show

refactor: settings tab

refactor(ChatNavbar): remove unused topic handling and simplify shortcut functions

fix(useChat): prevent setting active topic if it already exists in active assistant's topics

refactor(NavigationHandler, ChatNavbar, HomePage, MainSidebar): streamline navigation logic and remove unused code

chore: update react-router and react-router-dom to version 7.6.2, refactor routing logic in App component to use a RouteContainer for better location management

refactor(i18n): remove unused topic position settings and assistant display options from English, Japanese, Russian, Chinese, and Greek translations

refactor(MainSidebar, PinnedApps): remove unused imports and streamline component structure; update styling for better layout

refactor(TopicsTab): remove unused setTopicPosition function and streamline topic time display; update font size and family for TopicTime component

refactor(MainSidebar): integrate UserPopup for user settings access; streamline theme toggle logic and enhance styling for active menu items

refactor: remove topic position

fix(MainSidebar): update settings navigation path from '/settings/general' to '/settings/provider'

wip

refactor(SettingsTab): replace StyledSelect with Selector component and update styles for better UI consistency

chore(release): update fetch depth in GitHub Actions workflow for full history retrieval
This commit is contained in:
kangfenmao 2025-06-09 11:20:41 +08:00
parent a33a8da5c1
commit 11a8154458
119 changed files with 2492 additions and 2255 deletions

View File

@ -27,7 +27,7 @@ jobs:
- name: Check out Git repository - name: Check out Git repository
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
ref: main fetch-depth: 0
- name: Get release tag - name: Get release tag
id: get-tag id: get-tag
@ -149,4 +149,4 @@ jobs:
token: ${{ secrets.REPO_DISPATCH_TOKEN }} token: ${{ secrets.REPO_DISPATCH_TOKEN }}
repository: CherryHQ/cherry-studio-docs repository: CherryHQ/cherry-studio-docs
event-type: update-download-version event-type: update-download-version
client-payload: '{"version": "${{ steps.get-tag.outputs.tag }}"}' client-payload: '{"version": "${{ steps.get-tag.outputs.tag }}"}'

View File

@ -1,6 +1,6 @@
{ {
"name": "CherryStudio", "name": "CherryStudio",
"version": "1.4.2", "version": "1.4.2-ui-preview",
"private": true, "private": true,
"description": "A powerful AI assistant for producer.", "description": "A powerful AI assistant for producer.",
"main": "./out/main/index.js", "main": "./out/main/index.js",
@ -193,8 +193,8 @@
"react-infinite-scroll-component": "^6.1.0", "react-infinite-scroll-component": "^6.1.0",
"react-markdown": "^9.0.1", "react-markdown": "^9.0.1",
"react-redux": "^9.1.2", "react-redux": "^9.1.2",
"react-router": "6", "react-router": "^7.6.2",
"react-router-dom": "6", "react-router-dom": "^7.6.2",
"react-spinners": "^0.14.1", "react-spinners": "^0.14.1",
"react-window": "^1.8.11", "react-window": "^1.8.11",
"redux": "^5.0.1", "redux": "^5.0.1",

View File

@ -11,13 +11,13 @@ if (isDev) {
export const DATA_PATH = getDataPath() export const DATA_PATH = getDataPath()
export const titleBarOverlayDark = { export const titleBarOverlayDark = {
height: 40, height: 42,
color: 'rgba(255,255,255,0)', color: 'rgba(255,255,255,0)',
symbolColor: '#fff' symbolColor: '#fff'
} }
export const titleBarOverlayLight = { export const titleBarOverlayLight = {
height: 40, height: 42,
color: 'rgba(255,255,255,0)', color: 'rgba(255,255,255,0)',
symbolColor: '#000' symbolColor: '#000'
} }

View File

@ -56,14 +56,14 @@ export class WindowService {
minHeight: 600, minHeight: 600,
show: false, show: false,
autoHideMenuBar: true, autoHideMenuBar: true,
transparent: isMac, transparent: false,
vibrancy: 'sidebar', vibrancy: 'sidebar',
visualEffectState: 'active', visualEffectState: 'active',
titleBarStyle: 'hidden', titleBarStyle: 'hidden',
titleBarOverlay: nativeTheme.shouldUseDarkColors ? titleBarOverlayDark : titleBarOverlayLight, titleBarOverlay: nativeTheme.shouldUseDarkColors ? titleBarOverlayDark : titleBarOverlayLight,
backgroundColor: isMac ? undefined : nativeTheme.shouldUseDarkColors ? '#181818' : '#FFFFFF', backgroundColor: isMac ? undefined : nativeTheme.shouldUseDarkColors ? '#181818' : '#FFFFFF',
darkTheme: nativeTheme.shouldUseDarkColors, darkTheme: nativeTheme.shouldUseDarkColors,
trafficLightPosition: { x: 8, y: 12 }, trafficLightPosition: { x: 12, y: 12 },
...(isLinux ? { icon } : {}), ...(isLinux ? { icon } : {}),
webPreferences: { webPreferences: {
preload: join(__dirname, '../preload/index.js'), preload: join(__dirname, '../preload/index.js'),

View File

@ -65,7 +65,7 @@ export function handleMcpProtocolUrl(url: URL) {
const mainWindow = windowService.getMainWindow() const mainWindow = windowService.getMainWindow()
if (mainWindow && !mainWindow.isDestroyed()) { if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.executeJavaScript("window.navigate('/settings/mcp')") mainWindow.webContents.executeJavaScript("window.navigate('/mcp-servers')")
} }
break break
} }

View File

@ -2,10 +2,9 @@ import '@renderer/databases'
import store, { persistor } from '@renderer/store' import store, { persistor } from '@renderer/store'
import { Provider } from 'react-redux' 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 { PersistGate } from 'redux-persist/integration/react'
import Sidebar from './components/app/Sidebar'
import TopViewContainer from './components/TopView' import TopViewContainer from './components/TopView'
import AntdProvider from './context/AntdProvider' import AntdProvider from './context/AntdProvider'
import { CodeStyleProvider } from './context/CodeStyleProvider' import { CodeStyleProvider } from './context/CodeStyleProvider'
@ -13,14 +12,8 @@ import { NotificationProvider } from './context/NotificationProvider'
import StyleSheetManager from './context/StyleSheetManager' import StyleSheetManager from './context/StyleSheetManager'
import { ThemeProvider } from './context/ThemeProvider' import { ThemeProvider } from './context/ThemeProvider'
import NavigationHandler from './handler/NavigationHandler' import NavigationHandler from './handler/NavigationHandler'
import AgentsPage from './pages/agents/AgentsPage' import MainSidebar from './pages/home/MainSidebar/MainSidebar'
import AppsPage from './pages/apps/AppsPage' import Routes from './Routes'
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'
function App(): React.ReactElement { function App(): React.ReactElement {
return ( return (
@ -34,17 +27,8 @@ function App(): React.ReactElement {
<TopViewContainer> <TopViewContainer>
<HashRouter> <HashRouter>
<NavigationHandler /> <NavigationHandler />
<Sidebar /> <MainSidebar />
<Routes> <Routes />
<Route path="/" element={<HomePage />} />
<Route path="/agents" element={<AgentsPage />} />
<Route path="/paintings/*" element={<PaintingsRoutePage />} />
<Route path="/translate" element={<TranslatePage />} />
<Route path="/files" element={<FilesPage />} />
<Route path="/knowledge" element={<KnowledgePage />} />
<Route path="/apps" element={<AppsPage />} />
<Route path="/settings/*" element={<SettingsPage />} />
</Routes>
</HashRouter> </HashRouter>
</TopViewContainer> </TopViewContainer>
</PersistGate> </PersistGate>

View File

@ -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 (
<div style={{ display: 'flex', width: '100%', height: '100%' }}>
<div style={{ display: isHomePage ? 'flex' : 'none', flex: 1 }}>
<HomePage />
</div>
<div style={{ display: isHomePage ? 'none' : 'flex', flex: 1 }}>
<Routes location={location}>
<Route path="/agents" element={<AgentsPage />} />
<Route path="/paintings/*" element={<PaintingsRoutePage />} />
<Route path="/translate" element={<TranslatePage />} />
<Route path="/files" element={<FilesPage />} />
<Route path="/knowledge" element={<KnowledgePage />} />
<Route path="/apps" element={<AppsPage />} />
<Route path="/mcp-servers/*" element={<McpServersPage />} />
<Route path="/settings/*" element={<SettingsPage />} />
</Routes>
</div>
</div>
)
}
export default RouteContainer

View File

@ -25,7 +25,6 @@
} }
.minapp-drawer { .minapp-drawer {
max-width: calc(100vw - var(--sidebar-width));
.ant-drawer-content-wrapper { .ant-drawer-content-wrapper {
box-shadow: none; box-shadow: none;
} }
@ -33,7 +32,7 @@
position: absolute; position: absolute;
-webkit-app-region: drag; -webkit-app-region: drag;
min-height: calc(var(--navbar-height) + 0.5px); min-height: calc(var(--navbar-height) + 0.5px);
width: calc(100vw - var(--sidebar-width)); width: 100%;
margin-top: -0.5px; margin-top: -0.5px;
border-bottom: none; border-bottom: none;
} }

View File

@ -29,7 +29,7 @@
--color-text-secondary: rgba(235, 235, 245, 0.7); --color-text-secondary: rgba(235, 235, 245, 0.7);
--color-icon: #ffffff99; --color-icon: #ffffff99;
--color-icon-white: #ffffff; --color-icon-white: #ffffff;
--color-border: #ffffff19; --color-border: #383838;
--color-border-soft: #ffffff10; --color-border-soft: #ffffff10;
--color-border-mute: #ffffff05; --color-border-mute: #ffffff05;
--color-error: #f44336; --color-error: #f44336;
@ -44,8 +44,8 @@
--color-reference-text: #ffffff; --color-reference-text: #ffffff;
--color-reference-background: #0b0e12; --color-reference-background: #0b0e12;
--color-list-item: #222; --color-list-item: rgba(255, 255, 255, 0.1);
--color-list-item-hover: #1e1e1e; --color-list-item-hover: rgba(255, 255, 255, 0.05);
--modal-background: #1f1f1f; --modal-background: #1f1f1f;
@ -56,7 +56,7 @@
--navbar-background-mac: rgba(20, 20, 20, 0.55); --navbar-background-mac: rgba(20, 20, 20, 0.55);
--navbar-background: #1f1f1f; --navbar-background: #1f1f1f;
--navbar-height: 40px; --navbar-height: 42px;
--sidebar-width: 50px; --sidebar-width: 50px;
--status-bar-height: 40px; --status-bar-height: 40px;
--input-bar-height: 100px; --input-bar-height: 100px;
@ -66,12 +66,13 @@
--settings-width: 250px; --settings-width: 250px;
--scrollbar-width: 5px; --scrollbar-width: 5px;
--chat-background: #111111; --chat-background: transparent;
--chat-background-user: #28b561; --chat-background-user: rgba(255, 255, 255, 0.08);
--chat-background-assistant: #2c2c2c; --chat-background-assistant: transparent;
--chat-text-user: var(--color-black); --chat-text-user: var(--color-black);
--list-item-border-radius: 20px; --list-item-border-radius: 8px;
--border-width: 0.5px;
} }
[theme-mode='light'] { [theme-mode='light'] {
@ -120,8 +121,8 @@
--color-reference-text: #000000; --color-reference-text: #000000;
--color-reference-background: #f1f7ff; --color-reference-background: #f1f7ff;
--color-list-item: #eee; --color-list-item: rgba(255, 255, 255, 0.9);
--color-list-item-hover: #f5f5f5; --color-list-item-hover: rgba(255, 255, 255, 0.5);
--modal-background: var(--color-white); --modal-background: var(--color-white);
@ -132,8 +133,10 @@
--navbar-background-mac: rgba(255, 255, 255, 0.55); --navbar-background-mac: rgba(255, 255, 255, 0.55);
--navbar-background: rgba(244, 244, 244); --navbar-background: rgba(244, 244, 244);
--chat-background: #f3f3f3; --chat-background: transparent;
--chat-background-user: #95ec69; --chat-background-user: rgba(0, 0, 0, 0.045);
--chat-background-assistant: #ffffff; --chat-background-assistant: transparent;
--chat-text-user: var(--color-text); --chat-text-user: var(--color-text);
--border-width: 0.5px;
} }

View File

@ -1,6 +1,14 @@
#content-container { #content-container {
background-color: var(--color-background); background-color: var(--color-background);
border-top: 0.5px solid var(--color-border); 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%;
} }

View File

@ -112,46 +112,21 @@ ul {
} }
.bubble { .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 { .system-prompt {
background-color: var(--chat-background-assistant); background-color: var(--chat-background-assistant);
} }
.message-content-container { .message-content-container {
margin: 5px 0; margin: 5px 0;
border-radius: 8px; border-radius: 8px;
padding: 0.5rem 1rem;
} }
.block-wrapper {
display: flow-root;
}
.message-content-container > *:last-child {
margin-bottom: 0;
}
.message-thought-container { .message-thought-container {
margin-top: 8px; margin-top: 8px;
} }
.message-user { .message-user {
color: var(--chat-text-user); .message-content-container {
.message-content-container-user .anticon { margin: 5px 0;
color: var(--chat-text-user) !important; border-radius: 8px 0 8px 8px;
} padding: 10px 15px 0 15px;
.markdown {
color: var(--chat-text-user);
} }
} }
.group-grid-container.horizontal, .group-grid-container.horizontal,
@ -172,12 +147,6 @@ ul {
code { code {
color: var(--color-text); color: var(--color-text);
} }
.markdown {
display: flow-root;
*:last-child {
margin-bottom: 0;
}
}
} }
.lucide { .lucide {

View File

@ -119,7 +119,7 @@
} }
pre { pre {
border-radius: 5px; border-radius: 8px;
overflow-x: auto; overflow-x: auto;
font-family: 'Fira Code', 'Courier New', Courier, monospace; font-family: 'Fira Code', 'Courier New', Courier, monospace;
background-color: var(--color-background-mute); background-color: var(--color-background-mute);
@ -306,7 +306,7 @@ mjx-container {
/* CodeMirror 相关样式 */ /* CodeMirror 相关样式 */
.cm-editor { .cm-editor {
border-radius: 5px; border-radius: inherit;
&.cm-focused { &.cm-focused {
outline: none; outline: none;

View File

@ -238,12 +238,12 @@ const ContentContainer = styled.div<{
}>` }>`
position: relative; position: relative;
overflow: auto; overflow: auto;
border: 0.5px solid transparent; border-radius: inherit;
border-radius: 5px;
margin-top: 0; margin-top: 0;
.shiki { .shiki {
padding: 1em; padding: 1em;
border-radius: inherit;
code { code {
display: flex; display: flex;

View File

@ -47,7 +47,7 @@ const Artifacts: FC<Props> = ({ html }) => {
} }
return ( return (
<Container> <Container className="html-artifacts">
<Button icon={<ExpandOutlined />} onClick={handleOpenInApp}> <Button icon={<ExpandOutlined />} onClick={handleOpenInApp}>
{t('chat.artifacts.button.preview')} {t('chat.artifacts.button.preview')}
</Button> </Button>

View File

@ -290,6 +290,10 @@ const SplitViewWrapper = styled.div`
flex: 1 1 auto; flex: 1 1 auto;
width: 100%; width: 100%;
} }
&:not(:has(+ .html-artifacts)) {
border-radius: 0 0 8px 8px;
}
` `
export default memo(CodeBlockView) export default memo(CodeBlockView)

View File

@ -229,8 +229,8 @@ const CodeEditor = ({
style={{ style={{
...style, ...style,
fontSize: `${fontSize - 1}px`, fontSize: `${fontSize - 1}px`,
border: '0.5px solid transparent', marginTop: 0,
marginTop: 0 borderRadius: 'inherit'
}} }}
/> />
) )

View File

@ -6,10 +6,9 @@ import styled from 'styled-components'
interface ContextMenuProps { interface ContextMenuProps {
children: React.ReactNode children: React.ReactNode
onContextMenu?: (e: React.MouseEvent) => void onContextMenu?: (e: React.MouseEvent) => void
style?: React.CSSProperties
} }
const ContextMenu: React.FC<ContextMenuProps> = ({ children, onContextMenu, style }) => { const ContextMenu: React.FC<ContextMenuProps> = ({ children, onContextMenu }) => {
const { t } = useTranslation() const { t } = useTranslation()
const [contextMenuPosition, setContextMenuPosition] = useState<{ x: number; y: number } | null>(null) const [contextMenuPosition, setContextMenuPosition] = useState<{ x: number; y: number } | null>(null)
const [selectedText, setSelectedText] = useState<string>('') const [selectedText, setSelectedText] = useState<string>('')
@ -67,7 +66,7 @@ const ContextMenu: React.FC<ContextMenuProps> = ({ children, onContextMenu, styl
] ]
return ( return (
<ContextContainer onContextMenu={handleContextMenu} className="context-menu-container" style={style}> <ContextContainer onContextMenu={handleContextMenu} className="context-menu-container">
{contextMenuPosition && ( {contextMenuPosition && (
<Dropdown <Dropdown
overlayStyle={{ position: 'fixed', left: contextMenuPosition.x, top: contextMenuPosition.y, zIndex: 1000 }} overlayStyle={{ position: 'fixed', left: contextMenuPosition.x, top: contextMenuPosition.y, zIndex: 1000 }}

View File

@ -59,9 +59,9 @@ const DragableList: FC<Props<any>> = ({
{...provided.draggableProps} {...provided.draggableProps}
{...provided.dragHandleProps} {...provided.dragHandleProps}
style={{ style={{
marginBottom: 8,
...listStyle, ...listStyle,
...provided.draggableProps.style, ...provided.draggableProps.style
marginBottom: 8
}}> }}>
{children(item, index)} {children(item, index)}
</div> </div>

View File

@ -0,0 +1,35 @@
import { FC } from 'react'
import styled from 'styled-components'
interface Props {
isNarrowMode: boolean
}
const NarrowModeIcon: FC<Props> = ({ isNarrowMode }) => {
return (
<Container $isNarrowMode={isNarrowMode}>
<Line />
<Line />
</Container>
)
}
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

View File

@ -66,3 +66,18 @@ export function MdiLightbulbOn90(props: SVGProps<SVGSVGElement>) {
</svg> </svg>
) )
} }
export function ExpandWidth(props: SVGProps<SVGSVGElement>) {
return (
<svg width="1em" height="1em" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<g>
<path
id="path"
d="M0 25L0 175C0 179.41 3.58 183 8 183L12 183C16.41 183 20 179.41 20 175L20 25C20 20.58 16.41 17 12 17L8 17C3.58 17 0 20.58 0 25ZM60.41 58.43L29.42 94.81C26.87 97.8 26.87 102.19 29.42 105.18L60.38 141.53C65.2 147.19 74.47 143.78 74.47 136.34L74.47 121.83C74.47 117.41 78.06 113.83 82.47 113.83L117.5 113.83C121.91 113.83 125.5 117.41 125.5 121.83L125.5 136.35C125.5 143.78 134.76 147.2 139.58 141.54L170.57 105.18C173.12 102.19 173.12 97.8 170.57 94.81L139.59 58.43C134.76 52.77 125.5 56.18 125.5 63.62L125.5 78.16C125.5 82.58 121.91 86.16 117.5 86.16L82.5 86.16C78.08 86.16 74.5 82.58 74.5 78.16L74.5 63.62C74.5 56.18 65.23 52.77 60.41 58.43ZM188 17L192 17C196.41 17 200 20.58 200 25L200 175C200 179.41 196.41 183 192 183L188 183C183.58 183 180 179.41 180 175L180 25C180 20.58 183.58 17 188 17Z"
fill="currentColor"
fillRule="nonzero"
/>
</g>
</svg>
)
}

View File

@ -395,10 +395,7 @@ const MinappPopupContainer: React.FC = () => {
height={'100%'} height={'100%'}
maskClosable={false} maskClosable={false}
closeIcon={null} closeIcon={null}
style={{ style={{ backgroundColor: window.root.style.background }}>
marginLeft: 'var(--sidebar-width)',
backgroundColor: window.root.style.background
}}>
{!isReady && ( {!isReady && (
<EmptyView> <EmptyView>
<Avatar <Avatar
@ -418,7 +415,7 @@ const TitleContainer = styled.div`
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
padding-left: ${isMac ? '20px' : '10px'}; padding-left: ${isMac ? '80px' : '10px'};
padding-right: 10px; padding-right: 10px;
position: absolute; position: absolute;
top: 0; top: 0;

View File

@ -78,7 +78,7 @@ const WebviewContainer = memo(
) )
const WebviewStyle: React.CSSProperties = { const WebviewStyle: React.CSSProperties = {
width: 'calc(100vw - var(--sidebar-width))', width: '100vw',
height: 'calc(100vh - var(--navbar-height))', height: 'calc(100vh - var(--navbar-height))',
backgroundColor: 'var(--color-background)', backgroundColor: 'var(--color-background)',
display: 'inline-flex' display: 'inline-flex'

View File

@ -14,14 +14,7 @@ interface Props {
position: 'left' | 'right' position: 'left' | 'right'
} }
const FloatingSidebar: FC<Props> = ({ const FloatingSidebar: FC<Props> = ({ children, activeAssistant, setActiveAssistant, activeTopic, setActiveTopic }) => {
children,
activeAssistant,
setActiveAssistant,
activeTopic,
setActiveTopic,
position = 'left'
}) => {
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
useHotkeys('esc', () => { useHotkeys('esc', () => {
@ -45,12 +38,11 @@ const FloatingSidebar: FC<Props> = ({
const content = ( const content = (
<PopoverContent maxHeight={maxHeight}> <PopoverContent maxHeight={maxHeight}>
<HomeTabs <HomeTabs
tab="assistants"
activeAssistant={activeAssistant} activeAssistant={activeAssistant}
activeTopic={activeTopic} activeTopic={activeTopic}
setActiveAssistant={setActiveAssistant} setActiveAssistant={setActiveAssistant}
setActiveTopic={setActiveTopic} setActiveTopic={setActiveTopic}
position={position}
forceToSeeAllTab={true}
style={{ style={{
background: 'transparent', background: 'transparent',
border: 'none', border: 'none',

View File

@ -0,0 +1,387 @@
import { useCodeStyle } from '@renderer/context/CodeStyleProvider'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useSettings } from '@renderer/hooks/useSettings'
import { SettingDivider, SettingRow, SettingRowTitle } from '@renderer/pages/settings'
import { useAppDispatch } from '@renderer/store'
import {
setCodeCollapsible,
setCodeEditor,
setCodeExecution,
setCodePreview,
setCodeShowLineNumbers,
setCodeWrappable,
setFontSize,
setMathEngine,
setMessageFont,
setMessageNavigation,
setMessageStyle,
setMultiModelMessageStyle,
setShowMessageDivider,
setThoughtAutoCollapse
} from '@renderer/store/settings'
import { CodeStyleVarious, MathEngine, ThemeMode } from '@renderer/types'
import { Col, InputNumber, Modal, Row, Select, Slider, Switch, Tooltip } from 'antd'
import { CircleHelp } from 'lucide-react'
import { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { TopView } from '../TopView'
interface ShowParams {
title: string
}
interface Props extends ShowParams {
resolve: (data: any) => void
}
const PopupContainer: React.FC<Props> = ({ 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 (
<Modal
title={title}
open={open}
onOk={onOk}
onCancel={onCancel}
afterClose={onClose}
transitionName="animation-move-down"
footer={null}
centered>
<SettingGroup>
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.divider')}</SettingRowTitleSmall>
<Switch
size="small"
checked={showMessageDivider}
onChange={(checked) => dispatch(setShowMessageDivider(checked))}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.use_serif_font')}</SettingRowTitleSmall>
<Switch
size="small"
checked={messageFont === 'serif'}
onChange={(checked) => dispatch(setMessageFont(checked ? 'serif' : 'system'))}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>
{t('chat.settings.thought_auto_collapse')}
<Tooltip title={t('chat.settings.thought_auto_collapse.tip')}>
<CircleHelp size={14} style={{ marginLeft: 4 }} color="var(--color-text-2)" />
</Tooltip>
</SettingRowTitleSmall>
<Switch
size="small"
checked={thoughtAutoCollapse}
onChange={(checked) => dispatch(setThoughtAutoCollapse(checked))}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('message.message.style')}</SettingRowTitleSmall>
<StyledSelect
value={messageStyle}
onChange={(value) => dispatch(setMessageStyle(value as 'plain' | 'bubble'))}
style={{ width: 135 }}
size="small">
<Select.Option value="plain">{t('message.message.style.plain')}</Select.Option>
<Select.Option value="bubble">{t('message.message.style.bubble')}</Select.Option>
</StyledSelect>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('message.message.multi_model_style')}</SettingRowTitleSmall>
<StyledSelect
size="small"
value={multiModelMessageStyle}
onChange={(value) =>
dispatch(setMultiModelMessageStyle(value as 'fold' | 'vertical' | 'horizontal' | 'grid'))
}
style={{ width: 135 }}>
<Select.Option value="fold">{t('message.message.multi_model_style.fold')}</Select.Option>
<Select.Option value="vertical">{t('message.message.multi_model_style.vertical')}</Select.Option>
<Select.Option value="horizontal">{t('message.message.multi_model_style.horizontal')}</Select.Option>
<Select.Option value="grid">{t('message.message.multi_model_style.grid')}</Select.Option>
</StyledSelect>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.navigation')}</SettingRowTitleSmall>
<StyledSelect
size="small"
value={messageNavigation}
onChange={(value) => dispatch(setMessageNavigation(value as 'none' | 'buttons' | 'anchor'))}
style={{ width: 135 }}>
<Select.Option value="none">{t('settings.messages.navigation.none')}</Select.Option>
<Select.Option value="buttons">{t('settings.messages.navigation.buttons')}</Select.Option>
<Select.Option value="anchor">{t('settings.messages.navigation.anchor')}</Select.Option>
</StyledSelect>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.math_engine')}</SettingRowTitleSmall>
<StyledSelect
value={mathEngine}
onChange={(value) => dispatch(setMathEngine(value as MathEngine))}
style={{ width: 135 }}
size="small">
<Select.Option value="KaTeX">KaTeX</Select.Option>
<Select.Option value="MathJax">MathJax</Select.Option>
<Select.Option value="none">{t('settings.messages.math_engine.none')}</Select.Option>
</StyledSelect>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.font_size.title')}</SettingRowTitleSmall>
</SettingRow>
<Row align="middle" gutter={10}>
<Col span={24}>
<Slider
value={fontSizeValue}
onChange={(value) => setFontSizeValue(value)}
onChangeComplete={(value) => dispatch(setFontSize(value))}
min={12}
max={22}
step={1}
marks={{
12: <span style={{ fontSize: '12px' }}>A</span>,
14: <span style={{ fontSize: '14px' }}>{t('common.default')}</span>,
22: <span style={{ fontSize: '18px' }}>A</span>
}}
/>
</Col>
</Row>
</SettingGroup>
<SettingGroup>
<SettingRow>
<SettingRowTitleSmall>{t('message.message.code_style')}</SettingRowTitleSmall>
<StyledSelect
value={codeStyle}
onChange={(value) => onCodeStyleChange(value as CodeStyleVarious)}
style={{ width: 135 }}
size="small">
{themeNames.map((theme) => (
<Select.Option key={theme} value={theme}>
{theme}
</Select.Option>
))}
</StyledSelect>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>
{t('chat.settings.code_execution.title')}
<Tooltip title={t('chat.settings.code_execution.tip')}>
<CircleHelp size={14} style={{ marginLeft: 4 }} color="var(--color-text-2)" />
</Tooltip>
</SettingRowTitleSmall>
<Switch
size="small"
checked={codeExecution.enabled}
onChange={(checked) => dispatch(setCodeExecution({ enabled: checked }))}
/>
</SettingRow>
{codeExecution.enabled && (
<>
<SettingDivider />
<SettingRow style={{ paddingLeft: 8 }}>
<SettingRowTitleSmall>
{t('chat.settings.code_execution.timeout_minutes')}
<Tooltip title={t('chat.settings.code_execution.timeout_minutes.tip')}>
<CircleHelp size={14} style={{ marginLeft: 4 }} color="var(--color-text-2)" />
</Tooltip>
</SettingRowTitleSmall>
<InputNumber
size="small"
min={1}
max={60}
step={1}
value={codeExecution.timeoutMinutes}
onChange={(value) => dispatch(setCodeExecution({ timeoutMinutes: value ?? 1 }))}
style={{ width: 80 }}
/>
</SettingRow>
</>
)}
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('chat.settings.code_editor.title')}</SettingRowTitleSmall>
<Switch
size="small"
checked={codeEditor.enabled}
onChange={(checked) => dispatch(setCodeEditor({ enabled: checked }))}
/>
</SettingRow>
{codeEditor.enabled && (
<>
<SettingDivider />
<SettingRow style={{ paddingLeft: 8 }}>
<SettingRowTitleSmall>{t('chat.settings.code_editor.highlight_active_line')}</SettingRowTitleSmall>
<Switch
size="small"
checked={codeEditor.highlightActiveLine}
onChange={(checked) => dispatch(setCodeEditor({ highlightActiveLine: checked }))}
/>
</SettingRow>
<SettingDivider />
<SettingRow style={{ paddingLeft: 8 }}>
<SettingRowTitleSmall>{t('chat.settings.code_editor.fold_gutter')}</SettingRowTitleSmall>
<Switch
size="small"
checked={codeEditor.foldGutter}
onChange={(checked) => dispatch(setCodeEditor({ foldGutter: checked }))}
/>
</SettingRow>
<SettingDivider />
<SettingRow style={{ paddingLeft: 8 }}>
<SettingRowTitleSmall>{t('chat.settings.code_editor.autocompletion')}</SettingRowTitleSmall>
<Switch
size="small"
checked={codeEditor.autocompletion}
onChange={(checked) => dispatch(setCodeEditor({ autocompletion: checked }))}
/>
</SettingRow>
<SettingDivider />
<SettingRow style={{ paddingLeft: 8 }}>
<SettingRowTitleSmall>{t('chat.settings.code_editor.keymap')}</SettingRowTitleSmall>
<Switch
size="small"
checked={codeEditor.keymap}
onChange={(checked) => dispatch(setCodeEditor({ keymap: checked }))}
/>
</SettingRow>
</>
)}
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('chat.settings.show_line_numbers')}</SettingRowTitleSmall>
<Switch
size="small"
checked={codeShowLineNumbers}
onChange={(checked) => dispatch(setCodeShowLineNumbers(checked))}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('chat.settings.code_collapsible')}</SettingRowTitleSmall>
<Switch
size="small"
checked={codeCollapsible}
onChange={(checked) => dispatch(setCodeCollapsible(checked))}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('chat.settings.code_wrappable')}</SettingRowTitleSmall>
<Switch size="small" checked={codeWrappable} onChange={(checked) => dispatch(setCodeWrappable(checked))} />
</SettingRow>
</SettingGroup>
</Modal>
)
}
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<any>((resolve) => {
TopView.show(<PopupContainer {...props} resolve={resolve} />, TopViewKey)
})
}
}

View File

@ -15,15 +15,17 @@ const PopupContainer: React.FC<Props> = ({ title, resolve }) => {
const [open, setOpen] = useState(true) const [open, setOpen] = useState(true)
const onOk = () => { const onOk = () => {
resolve(true)
setOpen(false) setOpen(false)
} }
const onCancel = () => { const onCancel = () => {
resolve(false)
setOpen(false) setOpen(false)
} }
const onClose = () => { const onClose = () => {
resolve({}) TopView.hide(TopViewKey)
} }
TemplatePopup.hide = onCancel TemplatePopup.hide = onCancel
@ -51,16 +53,7 @@ export default class TemplatePopup {
} }
static show(props: ShowParams) { static show(props: ShowParams) {
return new Promise<any>((resolve) => { return new Promise<any>((resolve) => {
TopView.show( TopView.show(<PopupContainer {...props} resolve={resolve} />, TopViewKey)
<PopupContainer
{...props}
resolve={(v) => {
resolve(v)
TopView.hide(TopViewKey)
}}
/>,
TopViewKey
)
}) })
} }
} }

View File

@ -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<SelectorProps> = ({ 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: <CheckIcon>{option.value === value && <Check size={14} />}</CheckIcon>
}))
}, [options, value])
function onClick(e: { key: string }) {
onChange(e.key)
}
return (
<ConfigProvider
theme={{
components: {
Dropdown: {
controlPaddingHorizontal: 5
}
}
}}>
<Dropdown menu={{ items, onClick }} trigger={['click']} placement={placement}>
<Label $size={size}>
{label}
<LabelIcon size={size + 3} />
</Label>
</Dropdown>
</ConfigProvider>
)
}
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

View File

@ -78,7 +78,7 @@ const TranslateButton: FC<Props> = ({ text, onTranslated, disabled, style, isLoa
title={t('chat.input.translate', { target_language: t(`languages.${targetLanguage.toString()}`) })} title={t('chat.input.translate', { target_language: t(`languages.${targetLanguage.toString()}`) })}
arrow> arrow>
<ToolbarButton onClick={handleTranslate} disabled={disabled || isTranslating} style={style} type="text"> <ToolbarButton onClick={handleTranslate} disabled={disabled || isTranslating} style={style} type="text">
{isTranslating ? <LoadingOutlined spin /> : <Languages size={18} />} {isTranslating ? <LoadingOutlined spin /> : <Languages size={16} />}
</ToolbarButton> </ToolbarButton>
</Tooltip> </Tooltip>
) )

View File

@ -1,24 +1,16 @@
import { isLinux, isMac, isWindows } from '@renderer/config/constant' import { isLinux, isMac, isWindows } from '@renderer/config/constant'
import { useFullscreen } from '@renderer/hooks/useFullscreen' 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 { FC, PropsWithChildren } from 'react'
import type { HTMLAttributes } from 'react' import type { HTMLAttributes } from 'react'
import { useNavigate } from 'react-router-dom'
import styled from 'styled-components' import styled from 'styled-components'
type Props = PropsWithChildren & HTMLAttributes<HTMLDivElement> type Props = PropsWithChildren & HTMLAttributes<HTMLDivElement>
export const Navbar: FC<Props> = ({ children, ...props }) => { export const Navbar: FC<Props> = ({ children, ...props }) => {
const backgroundColor = useNavBackgroundColor() return <NavbarContainer {...props}>{children}</NavbarContainer>
return (
<NavbarContainer {...props} style={{ backgroundColor }}>
{children}
</NavbarContainer>
)
}
export const NavbarLeft: FC<Props> = ({ children, ...props }) => {
return <NavbarLeftContainer {...props}>{children}</NavbarLeftContainer>
} }
export const NavbarCenter: FC<Props> = ({ children, ...props }) => { export const NavbarCenter: FC<Props> = ({ children, ...props }) => {
@ -36,41 +28,52 @@ export const NavbarRight: FC<Props> = ({ children, ...props }) => {
export const NavbarMain: FC<Props> = ({ children, ...props }) => { export const NavbarMain: FC<Props> = ({ children, ...props }) => {
const isFullscreen = useFullscreen() const isFullscreen = useFullscreen()
return ( return (
<NavbarMainContainer {...props} $isFullscreen={isFullscreen}> <NavbarMainContainer {...props} $isFullscreen={isFullscreen}>
<CloseIcon />
{children} {children}
<MacCloseIcon />
</NavbarMainContainer> </NavbarMainContainer>
) )
} }
const MacCloseIcon = () => {
const navigate = useNavigate()
if (!isMac) {
return null
}
return <Button type="text" icon={<X size={18} />} onClick={() => navigate('/')} className="nodrag" />
}
const CloseIcon = () => {
const navigate = useNavigate()
if (isMac) {
return null
}
return (
<Button
type="text"
onClick={() => navigate('/')}
className="nodrag"
style={{ marginRight: 2 }}
icon={<CircleArrowLeft size={20} color="var(--color-icon)" style={{ marginTop: 2 }} />}
/>
)
}
const NavbarContainer = styled.div` const NavbarContainer = styled.div`
min-width: 100%; min-width: 100%;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
min-height: var(--navbar-height); min-height: var(--navbar-height);
max-height: var(--navbar-height); max-height: var(--navbar-height);
margin-left: ${isMac ? 'calc(var(--sidebar-width) * -1)' : 0};
padding-left: ${isMac ? 'var(--sidebar-width)' : 0};
-webkit-app-region: drag; -webkit-app-region: drag;
` background-color: var(--color-background);
const NavbarLeftContainer = styled.div`
min-width: var(--assistants-width);
padding: 0 10px;
display: flex;
flex-direction: row;
align-items: center;
font-weight: bold;
color: var(--color-text-1);
`
const NavbarCenterContainer = styled.div`
flex: 1;
display: flex;
align-items: center;
padding: 0 ${isMac ? '20px' : 0};
font-weight: bold;
color: var(--color-text-1);
` `
const NavbarRightContainer = styled.div<{ $isFullscreen: boolean }>` const NavbarRightContainer = styled.div<{ $isFullscreen: boolean }>`
@ -78,7 +81,7 @@ const NavbarRightContainer = styled.div<{ $isFullscreen: boolean }>`
display: flex; display: flex;
align-items: center; align-items: center;
padding: 0 12px; padding: 0 12px;
padding-right: ${({ $isFullscreen }) => ($isFullscreen ? '12px' : isWindows ? '140px' : isLinux ? '120px' : '12px')}; padding-right: ${({ $isFullscreen }) => ($isFullscreen ? '12px' : isWindows ? '135px' : isLinux ? '120px' : '12px')};
justify-content: flex-end; justify-content: flex-end;
` `
@ -87,9 +90,26 @@ const NavbarMainContainer = styled.div<{ $isFullscreen: boolean }>`
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
height: var(--navbar-height);
max-height: var(--navbar-height);
min-height: var(--navbar-height);
justify-content: space-between; justify-content: space-between;
padding: 0 ${isMac ? '20px' : 0}; padding-left: ${isMac ? '70px' : '10px'};
font-weight: bold; font-weight: bold;
color: var(--color-text-1); color: var(--color-text-1);
padding-right: ${({ $isFullscreen }) => ($isFullscreen ? '12px' : isWindows ? '140px' : isLinux ? '120px' : '12px')}; padding-right: ${({ $isFullscreen }) => ($isFullscreen ? '12px' : isWindows ? '135px' : isLinux ? '120px' : '12px')};
-webkit-app-region: drag;
`
const NavbarCenterContainer = styled.div`
flex: 1;
display: flex;
align-items: center;
height: var(--navbar-height);
max-height: var(--navbar-height);
min-height: var(--navbar-height);
padding: 0 8px;
font-weight: bold;
justify-content: space-between;
color: var(--color-text-1);
` `

View File

@ -1,8 +1,6 @@
import EmojiAvatar from '@renderer/components/Avatar/EmojiAvatar'
import { isMac } from '@renderer/config/constant' import { isMac } from '@renderer/config/constant'
import { AppLogo, UserAvatar } from '@renderer/config/env' import { AppLogo } from '@renderer/config/env'
import { useTheme } from '@renderer/context/ThemeProvider' import { useTheme } from '@renderer/context/ThemeProvider'
import useAvatar from '@renderer/hooks/useAvatar'
import { useFullscreen } from '@renderer/hooks/useFullscreen' import { useFullscreen } from '@renderer/hooks/useFullscreen'
import { useMinappPopup } from '@renderer/hooks/useMinappPopup' import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
import { useMinapps } from '@renderer/hooks/useMinapps' import { useMinapps } from '@renderer/hooks/useMinapps'
@ -11,9 +9,8 @@ import { modelGenerating, useRuntime } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
import i18n from '@renderer/i18n' import i18n from '@renderer/i18n'
import { ThemeMode } from '@renderer/types' import { ThemeMode } from '@renderer/types'
import { isEmoji } from '@renderer/utils'
import type { MenuProps } from 'antd' import type { MenuProps } from 'antd'
import { Avatar, Dropdown, Tooltip } from 'antd' import { Dropdown, Tooltip } from 'antd'
import { import {
CircleHelp, CircleHelp,
FileSearch, FileSearch,
@ -35,7 +32,6 @@ import styled from 'styled-components'
import DragableList from '../DragableList' import DragableList from '../DragableList'
import MinAppIcon from '../Icons/MinAppIcon' import MinAppIcon from '../Icons/MinAppIcon'
import UserPopup from '../Popups/UserPopup'
const Sidebar: FC = () => { const Sidebar: FC = () => {
const { hideMinappPopup, openMinapp } = useMinappPopup() const { hideMinappPopup, openMinapp } = useMinappPopup()
@ -47,11 +43,8 @@ const Sidebar: FC = () => {
const navigate = useNavigate() const navigate = useNavigate()
const { theme, settedTheme, toggleTheme } = useTheme() const { theme, settedTheme, toggleTheme } = useTheme()
const avatar = useAvatar()
const { t } = useTranslation() const { t } = useTranslation()
const onEditUser = () => UserPopup.show()
const backgroundColor = useNavBackgroundColor() const backgroundColor = useNavBackgroundColor()
const showPinnedApps = pinned.length > 0 && sidebarIcons.visible.includes('minapp') const showPinnedApps = pinned.length > 0 && sidebarIcons.visible.includes('minapp')
@ -79,13 +72,6 @@ const Sidebar: FC = () => {
$isFullscreen={isFullscreen} $isFullscreen={isFullscreen}
id="app-sidebar" id="app-sidebar"
style={{ backgroundColor, zIndex: minappShow ? 10000 : 'initial' }}> style={{ backgroundColor, zIndex: minappShow ? 10000 : 'initial' }}>
{isEmoji(avatar) ? (
<EmojiAvatar onClick={onEditUser} className="sidebar-avatar" size={31} fontSize={18}>
{avatar}
</EmojiAvatar>
) : (
<AvatarImg src={avatar || UserAvatar} draggable={false} className="nodrag" onClick={onEditUser} />
)}
<MainMenusContainer> <MainMenusContainer>
<Menus onClick={hideMinappPopup}> <Menus onClick={hideMinappPopup}>
<MainMenus /> <MainMenus />
@ -339,16 +325,6 @@ const Container = styled.div<{ $isFullscreen: boolean }>`
} }
` `
const AvatarImg = styled(Avatar)`
width: 31px;
height: 31px;
background-color: var(--color-background-soft);
margin-bottom: ${isMac ? '12px' : '12px'};
margin-top: ${isMac ? '0px' : '2px'};
border: none;
cursor: pointer;
`
const MainMenusContainer = styled.div` const MainMenusContainer = styled.div`
display: flex; display: flex;
flex: 1; flex: 1;

View File

@ -47,7 +47,7 @@ As [role name], with [list skills], strictly adhering to [list constraints], usi
` `
export const SUMMARIZE_PROMPT = export const SUMMARIZE_PROMPT =
"You are an assistant skilled in conversation. You need to summarize the user's conversation into a title within 10 words. The language of the title should be consistent with the user's primary language. Do not use punctuation marks or other special symbols" "You are an assistant skilled in conversation. You need to summarize the user's conversation into a title within 10 words. The language of the title should be consistent with the user's primary language. Do not use punctuation marks, markdown language markers, or other special symbols"
// https://github.com/ItzCrazyKns/Perplexica/blob/master/src/lib/prompts/webSearch.ts // https://github.com/ItzCrazyKns/Perplexica/blob/master/src/lib/prompts/webSearch.ts
export const SEARCH_SUMMARY_PROMPT = ` export const SEARCH_SUMMARY_PROMPT = `

View File

@ -1,14 +1,21 @@
import NavigationService from '@renderer/services/NavigationService'
import { useAppSelector } from '@renderer/store' import { useAppSelector } from '@renderer/store'
import { useEffect } from 'react'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
import { useLocation, useNavigate } from 'react-router-dom' import { useLocation, useNavigate } from 'react-router-dom'
const NavigationHandler: React.FC = () => { const NavigationHandler: React.FC = () => {
const location = useLocation() const location = useLocation()
const navigate = useNavigate() const navigate = useNavigate()
const showSettingsShortcutEnabled = useAppSelector( const showSettingsShortcutEnabled = useAppSelector(
(state) => state.shortcuts.shortcuts.find((s) => s.key === 'show_settings')?.enabled (state) => state.shortcuts.shortcuts.find((s) => s.key === 'show_settings')?.enabled
) )
useEffect(() => {
NavigationService.setNavigate(navigate)
}, [navigate])
useHotkeys( useHotkeys(
'meta+, ! ctrl+,', 'meta+, ! ctrl+,',
function () { function () {

View File

@ -0,0 +1,50 @@
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import { setActiveAssistant, setActiveTopic } from '@renderer/store/runtime'
import { loadTopicMessagesThunk } from '@renderer/store/thunk/messageThunk'
import { Assistant } from '@renderer/types'
import { Topic } from '@renderer/types'
import { useEffect } from 'react'
import { useAssistants } from './useAssistant'
import { useSettings } from './useSettings'
export const useChat = () => {
const { assistants } = useAssistants()
const activeAssistant = useAppSelector((state) => state.runtime.chat.activeAssistant) || assistants[0]
const activeTopic = useAppSelector((state) => state.runtime.chat.activeTopic) || activeAssistant?.topics[0]!
const { clickAssistantToShowTopic } = useSettings()
const dispatch = useAppDispatch()
useEffect(() => {
if (activeTopic) {
dispatch(loadTopicMessagesThunk(activeTopic.id))
EventEmitter.emit(EVENT_NAMES.CHANGE_TOPIC, activeTopic)
}
}, [activeTopic, dispatch])
useEffect(() => {
if (activeAssistant?.topics?.find((topic) => topic.id === activeTopic?.id)) {
return
}
const firstTopic = activeAssistant.topics[0]
firstTopic && dispatch(setActiveTopic(firstTopic))
}, [activeAssistant, activeTopic?.id, dispatch])
useEffect(() => {
if (clickAssistantToShowTopic) {
EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR)
}
}, [clickAssistantToShowTopic, activeAssistant])
return {
activeAssistant,
activeTopic,
setActiveAssistant: (assistant: Assistant) => {
dispatch(setActiveAssistant(assistant))
},
setActiveTopic: (topic: Topic) => {
dispatch(setActiveTopic(topic))
}
}
}

View File

@ -1,5 +1,6 @@
import { useRuntime } from '@renderer/hooks/useRuntime' import { useRuntime } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings' // 使用设置中的值 import { useSettings } from '@renderer/hooks/useSettings' // 使用设置中的值
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { useAppDispatch } from '@renderer/store' import { useAppDispatch } from '@renderer/store'
import { import {
setCurrentMinappId, setCurrentMinappId,
@ -32,6 +33,7 @@ export const useMinappPopup = () => {
/** Open a minapp (popup shows and minapp loaded) */ /** Open a minapp (popup shows and minapp loaded) */
const openMinapp = useCallback( const openMinapp = useCallback(
(app: MinAppType, keepAlive: boolean = false) => { (app: MinAppType, keepAlive: boolean = false) => {
EventEmitter.emit(EVENT_NAMES.OPEN_MINAPP, app)
if (keepAlive) { if (keepAlive) {
// 如果小程序已经打开,只切换显示 // 如果小程序已经打开,只切换显示
if (openedKeepAliveMinapps.some((item) => item.id === app.id)) { if (openedKeepAliveMinapps.some((item) => item.id === app.id)) {

View File

@ -9,7 +9,6 @@ import {
setLaunchToTray, setLaunchToTray,
setPinTopicsToTop, setPinTopicsToTop,
setSendMessageShortcut as _setSendMessageShortcut, setSendMessageShortcut as _setSendMessageShortcut,
setShowTokens,
setSidebarIcons, setSidebarIcons,
setTargetLanguage, setTargetLanguage,
setTheme, setTheme,
@ -91,9 +90,6 @@ export function useSettings() {
}, },
setAssistantIconType(assistantIconType: AssistantIconType) { setAssistantIconType(assistantIconType: AssistantIconType) {
dispatch(setAssistantIconType(assistantIconType)) dispatch(setAssistantIconType(assistantIconType))
},
setShowTokens(showTokens: boolean) {
dispatch(setShowTokens(showTokens))
} }
} }
} }

View File

@ -1,47 +1,16 @@
import db from '@renderer/databases' import db from '@renderer/databases'
import i18n from '@renderer/i18n' import i18n from '@renderer/i18n'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { deleteMessageFiles } from '@renderer/services/MessagesService' import { deleteMessageFiles } from '@renderer/services/MessagesService'
import store from '@renderer/store' import store from '@renderer/store'
import { updateTopic } from '@renderer/store/assistants' import { updateTopic } from '@renderer/store/assistants'
import { loadTopicMessagesThunk } from '@renderer/store/thunk/messageThunk'
import { Assistant, Topic } from '@renderer/types' import { Assistant, Topic } from '@renderer/types'
import { findMainTextBlocks } from '@renderer/utils/messageUtils/find' import { findMainTextBlocks } from '@renderer/utils/messageUtils/find'
import { find, isEmpty } from 'lodash' import { isEmpty } from 'lodash'
import { useEffect, useState } from 'react'
import { useAssistant } from './useAssistant'
import { getStoreSetting } from './useSettings' import { getStoreSetting } from './useSettings'
const renamingTopics = new Set<string>() const renamingTopics = new Set<string>()
let _activeTopic: Topic
let _setActiveTopic: (topic: Topic) => void
export function useActiveTopic(_assistant: Assistant, topic?: Topic) {
const { assistant } = useAssistant(_assistant.id)
const [activeTopic, setActiveTopic] = useState(topic || _activeTopic || assistant?.topics[0])
_activeTopic = activeTopic
_setActiveTopic = setActiveTopic
useEffect(() => {
if (activeTopic) {
store.dispatch(loadTopicMessagesThunk(activeTopic.id))
EventEmitter.emit(EVENT_NAMES.CHANGE_TOPIC, activeTopic)
}
}, [activeTopic])
useEffect(() => {
// activeTopic not in assistant.topics
if (assistant && !find(assistant.topics, { id: activeTopic?.id })) {
setActiveTopic(assistant.topics[0])
}
}, [activeTopic?.id, assistant])
return { activeTopic, setActiveTopic }
}
export function useTopic(assistant: Assistant, topicId?: string) { export function useTopic(assistant: Assistant, topicId?: string) {
return assistant?.topics.find((topic) => topic.id === topicId) return assistant?.topics.find((topic) => topic.id === topicId)
} }
@ -86,7 +55,6 @@ export const autoRenameTopic = async (assistant: Assistant, topicId: string) =>
.substring(0, 50) .substring(0, 50)
if (topicName) { if (topicName) {
const data = { ...topic, name: topicName } as Topic const data = { ...topic, name: topicName } as Topic
_setActiveTopic(data)
store.dispatch(updateTopic({ assistantId: assistant.id, topic: data })) store.dispatch(updateTopic({ assistantId: assistant.id, topic: data }))
} }
return return
@ -97,7 +65,6 @@ export const autoRenameTopic = async (assistant: Assistant, topicId: string) =>
const summaryText = await fetchMessagesSummary({ messages: topic.messages, assistant }) const summaryText = await fetchMessagesSummary({ messages: topic.messages, assistant })
if (summaryText) { if (summaryText) {
const data = { ...topic, name: summaryText } const data = { ...topic, name: summaryText }
_setActiveTopic(data)
store.dispatch(updateTopic({ assistantId: assistant.id, topic: data })) store.dispatch(updateTopic({ assistantId: assistant.id, topic: data }))
} }
} }

View File

@ -201,6 +201,7 @@
"message.quote": "Quote", "message.quote": "Quote",
"message.regenerate.model": "Switch Model", "message.regenerate.model": "Switch Model",
"message.useful": "Helpful", "message.useful": "Helpful",
"message.settings": "Settings",
"multiple.select": "Multiple Select", "multiple.select": "Multiple Select",
"multiple.select.empty": "No Messages Selected", "multiple.select.empty": "No Messages Selected",
"navigation": { "navigation": {
@ -215,7 +216,7 @@
}, },
"resend": "Resend", "resend": "Resend",
"save": "Save", "save": "Save",
"settings.code.title": "Code Block Settings", "settings.code.title": "Code Settings",
"settings.code_editor": { "settings.code_editor": {
"title": "Code Editor", "title": "Code Editor",
"highlight_active_line": "Highlight active line", "highlight_active_line": "Highlight active line",
@ -422,7 +423,8 @@
"pinyin.asc": "Sort by Pinyin (A-Z)", "pinyin.asc": "Sort by Pinyin (A-Z)",
"pinyin.desc": "Sort by Pinyin (Z-A)" "pinyin.desc": "Sort by Pinyin (Z-A)"
}, },
"no_results": "No results" "no_results": "No results",
"apps": "Apps"
}, },
"docs": { "docs": {
"title": "Docs" "title": "Docs"
@ -1058,7 +1060,6 @@
"about.updateNotAvailable": "You are using the latest version", "about.updateNotAvailable": "You are using the latest version",
"about.website.button": "Website", "about.website.button": "Website",
"about.website.title": "Official Website", "about.website.title": "Official Website",
"advanced.auto_switch_to_topics": "Auto switch to topic",
"advanced.title": "Advanced Settings", "advanced.title": "Advanced Settings",
"assistant": "Default Assistant", "assistant": "Default Assistant",
"assistant.model_params": "Model Parameters", "assistant.model_params": "Model Parameters",
@ -1272,7 +1273,6 @@
"message_title.use_topic_naming.title": "Use topic naming model to create titles for exported messages", "message_title.use_topic_naming.title": "Use topic naming model to create titles for exported messages",
"message_title.use_topic_naming.help": "When enabled, use topic naming model to create titles for exported messages. This will also affect all Markdown export methods." "message_title.use_topic_naming.help": "When enabled, use topic naming model to create titles for exported messages. This will also affect all Markdown export methods."
}, },
"display.assistant.title": "Assistant Settings",
"display.custom.css": "Custom CSS", "display.custom.css": "Custom CSS",
"display.custom.css.cherrycss": "Get from cherrycss.com", "display.custom.css.cherrycss": "Get from cherrycss.com",
"display.custom.css.placeholder": "/* Put custom CSS here */", "display.custom.css.placeholder": "/* Put custom CSS here */",
@ -1515,9 +1515,7 @@
"advancedSettings": "Advanced Settings" "advancedSettings": "Advanced Settings"
}, },
"messages.prompt": "Show prompt", "messages.prompt": "Show prompt",
"messages.tokens": "Show token usage",
"messages.divider": "Show divider between messages", "messages.divider": "Show divider between messages",
"messages.divider.tooltip": "Not applicable to bubble-style message",
"messages.grid_columns": "Message grid display columns", "messages.grid_columns": "Message grid display columns",
"messages.grid_popover_trigger": "Grid detail trigger", "messages.grid_popover_trigger": "Grid detail trigger",
"messages.grid_popover_trigger.click": "Click to display", "messages.grid_popover_trigger.click": "Click to display",
@ -1730,9 +1728,6 @@
"theme.window.style.title": "Window Style", "theme.window.style.title": "Window Style",
"theme.window.style.transparent": "Transparent Window", "theme.window.style.transparent": "Transparent Window",
"title": "Settings", "title": "Settings",
"topic.position": "Topic position",
"topic.position.left": "Left",
"topic.position.right": "Right",
"topic.show.time": "Show topic time", "topic.show.time": "Show topic time",
"topic.pin_to_top": "Pin Topics to Top", "topic.pin_to_top": "Pin Topics to Top",
"tray.onclose": "Minimize to Tray on Close", "tray.onclose": "Minimize to Tray on Close",

View File

@ -201,6 +201,7 @@
"message.quote": "引用", "message.quote": "引用",
"message.regenerate.model": "モデルを切り替え", "message.regenerate.model": "モデルを切り替え",
"message.useful": "役立つ", "message.useful": "役立つ",
"message.settings": "設定",
"multiple.select": "選択", "multiple.select": "選択",
"multiple.select.empty": "メッセージが選択されていません", "multiple.select.empty": "メッセージが選択されていません",
"navigation": { "navigation": {
@ -422,7 +423,8 @@
"pinyin.asc": "ピンインで昇順ソート", "pinyin.asc": "ピンインで昇順ソート",
"pinyin.desc": "ピンインで降順ソート" "pinyin.desc": "ピンインで降順ソート"
}, },
"no_results": "検索結果なし" "no_results": "検索結果なし",
"apps": "アプリ"
}, },
"docs": { "docs": {
"title": "ドキュメント" "title": "ドキュメント"
@ -1055,7 +1057,6 @@
"about.updateNotAvailable": "最新バージョンを使用しています", "about.updateNotAvailable": "最新バージョンを使用しています",
"about.website.button": "ウェブサイト", "about.website.button": "ウェブサイト",
"about.website.title": "公式ウェブサイト", "about.website.title": "公式ウェブサイト",
"advanced.auto_switch_to_topics": "トピックに自動的に切り替える",
"advanced.title": "詳細設定", "advanced.title": "詳細設定",
"assistant": "デフォルトアシスタント", "assistant": "デフォルトアシスタント",
"assistant.model_params": "モデルパラメータ", "assistant.model_params": "モデルパラメータ",
@ -1269,7 +1270,6 @@
"notion.export_reasoning.title": "エクスポート時に思考チェーンを含める", "notion.export_reasoning.title": "エクスポート時に思考チェーンを含める",
"notion.export_reasoning.help": "有効にすると、Notionにエクスポートする際に思考チェーンの内容が含まれます。" "notion.export_reasoning.help": "有効にすると、Notionにエクスポートする際に思考チェーンの内容が含まれます。"
}, },
"display.assistant.title": "アシスタント設定",
"display.custom.css": "カスタムCSS", "display.custom.css": "カスタムCSS",
"display.custom.css.cherrycss": "cherrycss.comから取得", "display.custom.css.cherrycss": "cherrycss.comから取得",
"display.custom.css.placeholder": "/* ここにカスタムCSSを入力 */", "display.custom.css.placeholder": "/* ここにカスタムCSSを入力 */",
@ -1508,9 +1508,7 @@
"advancedSettings": "詳細設定" "advancedSettings": "詳細設定"
}, },
"messages.prompt": "プロンプト表示", "messages.prompt": "プロンプト表示",
"messages.tokens": "トークン使用量を表示",
"messages.divider": "メッセージ間に区切り線を表示", "messages.divider": "メッセージ間に区切り線を表示",
"messages.divider.tooltip": "バブルスタイルのメッセージには適用されません",
"messages.grid_columns": "メッセージグリッドの表示列数", "messages.grid_columns": "メッセージグリッドの表示列数",
"messages.grid_popover_trigger": "グリッド詳細トリガー", "messages.grid_popover_trigger": "グリッド詳細トリガー",
"messages.grid_popover_trigger.click": "クリックで表示", "messages.grid_popover_trigger.click": "クリックで表示",
@ -1717,9 +1715,6 @@
"theme.window.style.title": "ウィンドウスタイル", "theme.window.style.title": "ウィンドウスタイル",
"theme.window.style.transparent": "透明ウィンドウ", "theme.window.style.transparent": "透明ウィンドウ",
"title": "設定", "title": "設定",
"topic.position": "トピックの位置",
"topic.position.left": "左",
"topic.position.right": "右",
"topic.show.time": "トピックの時間を表示", "topic.show.time": "トピックの時間を表示",
"topic.pin_to_top": "固定トピックを上部に表示", "topic.pin_to_top": "固定トピックを上部に表示",
"tray.onclose": "閉じるときにトレイに最小化", "tray.onclose": "閉じるときにトレイに最小化",

View File

@ -201,6 +201,7 @@
"message.quote": "Цитата", "message.quote": "Цитата",
"message.regenerate.model": "Переключить модель", "message.regenerate.model": "Переключить модель",
"message.useful": "Полезно", "message.useful": "Полезно",
"message.settings": "Настройки",
"multiple.select": "Множественный выбор", "multiple.select": "Множественный выбор",
"multiple.select.empty": "Ничего не выбрано", "multiple.select.empty": "Ничего не выбрано",
"navigation": { "navigation": {
@ -422,7 +423,8 @@
"pinyin.asc": "Сортировать по пиньинь (А-Я)", "pinyin.asc": "Сортировать по пиньинь (А-Я)",
"pinyin.desc": "Сортировать по пиньинь (Я-А)" "pinyin.desc": "Сортировать по пиньинь (Я-А)"
}, },
"no_results": "Результатов не найдено" "no_results": "Результатов не найдено",
"apps": "Приложения"
}, },
"docs": { "docs": {
"title": "Документация" "title": "Документация"
@ -1055,7 +1057,6 @@
"about.updateNotAvailable": "Вы используете последнюю версию", "about.updateNotAvailable": "Вы используете последнюю версию",
"about.website.button": "Сайт", "about.website.button": "Сайт",
"about.website.title": "Официальный сайт", "about.website.title": "Официальный сайт",
"advanced.auto_switch_to_topics": "Автоматически переключаться на топик",
"advanced.title": "Расширенные настройки", "advanced.title": "Расширенные настройки",
"assistant": "Ассистент по умолчанию", "assistant": "Ассистент по умолчанию",
"assistant.model_params": "Параметры модели", "assistant.model_params": "Параметры модели",
@ -1269,7 +1270,6 @@
"message_title.use_topic_naming.title": "Использовать модель именования тем для создания заголовков сообщений", "message_title.use_topic_naming.title": "Использовать модель именования тем для создания заголовков сообщений",
"message_title.use_topic_naming.help": "Этот параметр влияет на все методы экспорта в Markdown, такие как Notion, Yuque и т.д." "message_title.use_topic_naming.help": "Этот параметр влияет на все методы экспорта в Markdown, такие как Notion, Yuque и т.д."
}, },
"display.assistant.title": "Настройки ассистентов",
"display.custom.css": "Пользовательский CSS", "display.custom.css": "Пользовательский CSS",
"display.custom.css.cherrycss": "Получить из cherrycss.com", "display.custom.css.cherrycss": "Получить из cherrycss.com",
"display.custom.css.placeholder": "/* Здесь введите пользовательский CSS */", "display.custom.css.placeholder": "/* Здесь введите пользовательский CSS */",
@ -1508,9 +1508,7 @@
"advancedSettings": "Расширенные настройки" "advancedSettings": "Расширенные настройки"
}, },
"messages.prompt": "Показывать подсказки", "messages.prompt": "Показывать подсказки",
"messages.tokens": "Показать использование токенов",
"messages.divider": "Показывать разделитель между сообщениями", "messages.divider": "Показывать разделитель между сообщениями",
"messages.divider.tooltip": "Не применимо к сообщениям в стиле пузырей",
"messages.grid_columns": "Количество столбцов сетки сообщений", "messages.grid_columns": "Количество столбцов сетки сообщений",
"messages.grid_popover_trigger": "Триггер для отображения подробной информации в сетке", "messages.grid_popover_trigger": "Триггер для отображения подробной информации в сетке",
"messages.grid_popover_trigger.click": "Нажатие для отображения", "messages.grid_popover_trigger.click": "Нажатие для отображения",
@ -1717,9 +1715,6 @@
"theme.window.style.title": "Стиль окна", "theme.window.style.title": "Стиль окна",
"theme.window.style.transparent": "Прозрачное окно", "theme.window.style.transparent": "Прозрачное окно",
"title": "Настройки", "title": "Настройки",
"topic.position": "Позиция топиков",
"topic.position.left": "Слева",
"topic.position.right": "Справа",
"topic.show.time": "Показывать время топика", "topic.show.time": "Показывать время топика",
"topic.pin_to_top": "Закрепленные топики сверху", "topic.pin_to_top": "Закрепленные топики сверху",
"tray.onclose": "Свернуть в трей при закрытии", "tray.onclose": "Свернуть в трей при закрытии",

View File

@ -218,6 +218,7 @@
"message.new.context": "清除上下文", "message.new.context": "清除上下文",
"message.quote": "引用", "message.quote": "引用",
"message.regenerate.model": "切换模型", "message.regenerate.model": "切换模型",
"message.settings": "设置",
"message.useful": "有用", "message.useful": "有用",
"multiple.select": "多选", "multiple.select": "多选",
"multiple.select.empty": "未选中任何消息", "multiple.select.empty": "未选中任何消息",
@ -233,7 +234,7 @@
}, },
"resend": "重新发送", "resend": "重新发送",
"save": "保存", "save": "保存",
"settings.code.title": "代码设置", "settings.code.title": "代码设置",
"settings.code_editor": { "settings.code_editor": {
"title": "代码编辑器", "title": "代码编辑器",
"highlight_active_line": "高亮当前行", "highlight_active_line": "高亮当前行",
@ -422,7 +423,8 @@
"pinyin.asc": "按拼音升序", "pinyin.asc": "按拼音升序",
"pinyin.desc": "按拼音降序" "pinyin.desc": "按拼音降序"
}, },
"no_results": "无结果" "no_results": "无结果",
"apps": "应用"
}, },
"docs": { "docs": {
"title": "帮助文档" "title": "帮助文档"
@ -1058,7 +1060,6 @@
"about.updateNotAvailable": "你的软件已是最新版本", "about.updateNotAvailable": "你的软件已是最新版本",
"about.website.button": "查看", "about.website.button": "查看",
"about.website.title": "官方网站", "about.website.title": "官方网站",
"advanced.auto_switch_to_topics": "自动切换到话题",
"advanced.title": "高级设置", "advanced.title": "高级设置",
"assistant": "默认助手", "assistant": "默认助手",
"assistant.model_params": "模型参数", "assistant.model_params": "模型参数",
@ -1272,7 +1273,6 @@
"new_folder.button": "新建文件夹" "new_folder.button": "新建文件夹"
} }
}, },
"display.assistant.title": "助手设置",
"display.custom.css": "自定义 CSS", "display.custom.css": "自定义 CSS",
"display.custom.css.cherrycss": "从 cherrycss.com 获取", "display.custom.css.cherrycss": "从 cherrycss.com 获取",
"display.custom.css.placeholder": "/* 这里写自定义CSS */", "display.custom.css.placeholder": "/* 这里写自定义CSS */",
@ -1515,9 +1515,7 @@
"advancedSettings": "高级设置" "advancedSettings": "高级设置"
}, },
"messages.prompt": "显示提示词", "messages.prompt": "显示提示词",
"messages.tokens": "显示Token用量",
"messages.divider": "消息分割线", "messages.divider": "消息分割线",
"messages.divider.tooltip": "不适用于气泡样式消息",
"messages.grid_columns": "消息网格展示列数", "messages.grid_columns": "消息网格展示列数",
"messages.grid_popover_trigger": "网格详情触发", "messages.grid_popover_trigger": "网格详情触发",
"messages.grid_popover_trigger.click": "点击显示", "messages.grid_popover_trigger.click": "点击显示",
@ -1730,9 +1728,6 @@
"theme.window.style.title": "窗口样式", "theme.window.style.title": "窗口样式",
"theme.window.style.transparent": "透明窗口", "theme.window.style.transparent": "透明窗口",
"title": "设置", "title": "设置",
"topic.position": "话题位置",
"topic.position.left": "左侧",
"topic.position.right": "右侧",
"topic.show.time": "显示话题时间", "topic.show.time": "显示话题时间",
"topic.pin_to_top": "固定话题置顶", "topic.pin_to_top": "固定话题置顶",
"tray.onclose": "关闭时最小化到托盘", "tray.onclose": "关闭时最小化到托盘",

View File

@ -201,6 +201,7 @@
"message.quote": "引用", "message.quote": "引用",
"message.regenerate.model": "切換模型", "message.regenerate.model": "切換模型",
"message.useful": "有用", "message.useful": "有用",
"message.settings": "設定",
"multiple.select": "多選", "multiple.select": "多選",
"multiple.select.empty": "未選中任何訊息", "multiple.select.empty": "未選中任何訊息",
"navigation": { "navigation": {
@ -215,7 +216,7 @@
}, },
"resend": "重新傳送", "resend": "重新傳送",
"save": "儲存", "save": "儲存",
"settings.code.title": "程式碼區塊", "settings.code.title": "程式碼設定",
"settings.code_editor": { "settings.code_editor": {
"title": "程式碼編輯器", "title": "程式碼編輯器",
"highlight_active_line": "高亮當前行", "highlight_active_line": "高亮當前行",
@ -422,7 +423,8 @@
"pinyin.asc": "按拼音升序", "pinyin.asc": "按拼音升序",
"pinyin.desc": "按拼音降序" "pinyin.desc": "按拼音降序"
}, },
"no_results": "沒有結果" "no_results": "沒有結果",
"apps": "應用"
}, },
"docs": { "docs": {
"title": "說明文件" "title": "說明文件"
@ -1058,7 +1060,6 @@
"about.updateNotAvailable": "您正在使用最新版本", "about.updateNotAvailable": "您正在使用最新版本",
"about.website.button": "網站", "about.website.button": "網站",
"about.website.title": "官方網站", "about.website.title": "官方網站",
"advanced.auto_switch_to_topics": "自動切換到話題",
"advanced.title": "進階設定", "advanced.title": "進階設定",
"assistant": "預設助手", "assistant": "預設助手",
"assistant.model_params": "模型參數", "assistant.model_params": "模型參數",
@ -1272,7 +1273,6 @@
"message_title.use_topic_naming.title": "使用話題命名模型為導出的消息創建標題", "message_title.use_topic_naming.title": "使用話題命名模型為導出的消息創建標題",
"message_title.use_topic_naming.help": "此設定會影響所有通過Markdown導出的方式如Notion、語雀等" "message_title.use_topic_naming.help": "此設定會影響所有通過Markdown導出的方式如Notion、語雀等"
}, },
"display.assistant.title": "助手設定",
"display.custom.css": "自訂 CSS", "display.custom.css": "自訂 CSS",
"display.custom.css.cherrycss": "從 cherrycss.com 取得", "display.custom.css.cherrycss": "從 cherrycss.com 取得",
"display.custom.css.placeholder": "/* 這裡寫自訂 CSS */", "display.custom.css.placeholder": "/* 這裡寫自訂 CSS */",
@ -1512,9 +1512,7 @@
"advancedSettings": "高級設定" "advancedSettings": "高級設定"
}, },
"messages.prompt": "提示詞顯示", "messages.prompt": "提示詞顯示",
"messages.tokens": "Token用量顯示",
"messages.divider": "訊息間顯示分隔線", "messages.divider": "訊息間顯示分隔線",
"messages.divider.tooltip": "不適用於氣泡樣式消息",
"messages.grid_columns": "訊息網格展示列數", "messages.grid_columns": "訊息網格展示列數",
"messages.grid_popover_trigger": "網格詳細資訊觸發", "messages.grid_popover_trigger": "網格詳細資訊觸發",
"messages.grid_popover_trigger.click": "點選顯示", "messages.grid_popover_trigger.click": "點選顯示",
@ -1721,9 +1719,6 @@
"theme.window.style.title": "視窗樣式", "theme.window.style.title": "視窗樣式",
"theme.window.style.transparent": "透明視窗", "theme.window.style.transparent": "透明視窗",
"title": "設定", "title": "設定",
"topic.position": "話題位置",
"topic.position.left": "左側",
"topic.position.right": "右側",
"topic.show.time": "顯示話題時間", "topic.show.time": "顯示話題時間",
"topic.pin_to_top": "固定話題置頂", "topic.pin_to_top": "固定話題置頂",
"tray.onclose": "關閉時最小化到系统匣", "tray.onclose": "關閉時最小化到系统匣",

View File

@ -917,7 +917,6 @@
"about.updateNotAvailable": "Το λογισμικό σας είναι ήδη στην πιο πρόσφατη έκδοση", "about.updateNotAvailable": "Το λογισμικό σας είναι ήδη στην πιο πρόσφατη έκδοση",
"about.website.button": "Προβολή", "about.website.button": "Προβολή",
"about.website.title": "Ιστοσελίδα", "about.website.title": "Ιστοσελίδα",
"advanced.auto_switch_to_topics": "Αυτόματη μετάβαση σε θέματα",
"advanced.title": "Ρυθμίσεις Ανώτερου Νiveau", "advanced.title": "Ρυθμίσεις Ανώτερου Νiveau",
"assistant": "Πρόεδρος Υπηρεσίας", "assistant": "Πρόεδρος Υπηρεσίας",
"assistant.model_params": "Παράμετροι Μοντέλου", "assistant.model_params": "Παράμετροι Μοντέλου",
@ -1124,7 +1123,6 @@
"message_title.use_topic_naming.title": "Δημιουργία τίτλων μηνυμάτων χρησιμοποιώντας μοντέλο ονομασίας θεμάτων", "message_title.use_topic_naming.title": "Δημιουργία τίτλων μηνυμάτων χρησιμοποιώντας μοντέλο ονομασίας θεμάτων",
"message_title.use_topic_naming.help": "Όταν είναι ενεργό, δημιουργεί τίτλους για τα μηνύματα που εξάγονται χρησιμοποιώντας μοντέλο ονομασίας θεμάτων. Αυτό επηρεάζει επίσης όλες τις μεθόδους εξαγωγής μέσω Markdown." "message_title.use_topic_naming.help": "Όταν είναι ενεργό, δημιουργεί τίτλους για τα μηνύματα που εξάγονται χρησιμοποιώντας μοντέλο ονομασίας θεμάτων. Αυτό επηρεάζει επίσης όλες τις μεθόδους εξαγωγής μέσω Markdown."
}, },
"display.assistant.title": "Ρυθμίσεις Υπηρεσίας",
"display.custom.css": "Προσαρμοστική CSS", "display.custom.css": "Προσαρμοστική CSS",
"display.custom.css.cherrycss": "Λήψη από cherrycss.com", "display.custom.css.cherrycss": "Λήψη από cherrycss.com",
"display.custom.css.placeholder": "/* Γράψτε εδώ την προσαρμοστική CSS */", "display.custom.css.placeholder": "/* Γράψτε εδώ την προσαρμοστική CSS */",
@ -1304,7 +1302,6 @@
"advancedSettings": "Προχωρημένες Ρυθμίσεις" "advancedSettings": "Προχωρημένες Ρυθμίσεις"
}, },
"messages.divider": "Διαχωριστική γραμμή μηνυμάτων", "messages.divider": "Διαχωριστική γραμμή μηνυμάτων",
"messages.divider.tooltip": "Δεν ισχύει για μηνύματα με στυλ φυσαλίδας",
"messages.grid_columns": "Αριθμός στήλων γριλ μηνυμάτων", "messages.grid_columns": "Αριθμός στήλων γριλ μηνυμάτων",
"messages.grid_popover_trigger": "Καταγραφή στοιχείων στο grid", "messages.grid_popover_trigger": "Καταγραφή στοιχείων στο grid",
"messages.grid_popover_trigger.click": "Εμφάνιση κλικ", "messages.grid_popover_trigger.click": "Εμφάνιση κλικ",
@ -1486,9 +1483,6 @@
"theme.window.style.title": "Στυλ παραθύρων", "theme.window.style.title": "Στυλ παραθύρων",
"theme.window.style.transparent": "Διαφανή παράθυρα", "theme.window.style.transparent": "Διαφανή παράθυρα",
"title": "Ρυθμίσεις", "title": "Ρυθμίσεις",
"topic.position": "Θέση θεμάτων",
"topic.position.left": "Αριστερά",
"topic.position.right": "Δεξιά",
"topic.show.time": "Εμφάνιση ώρας θέματος", "topic.show.time": "Εμφάνιση ώρας θέματος",
"tray.onclose": "Μειωμένο στη συνδρομή κατά την κλεισιά", "tray.onclose": "Μειωμένο στη συνδρομή κατά την κλεισιά",
"tray.show": "Εμφάνιση εικονιδίου συνδρομής", "tray.show": "Εμφάνιση εικονιδίου συνδρομής",
@ -1611,7 +1605,6 @@
"assistant.icon.type.none": "Κανένα", "assistant.icon.type.none": "Κανένα",
"general.auto_check_update.title": "Αυτόματη ενημέρωση", "general.auto_check_update.title": "Αυτόματη ενημέρωση",
"input.show_translate_confirm": "Εμφάνιση παραθύρου επιβεβαίωσης μετάφρασης", "input.show_translate_confirm": "Εμφάνιση παραθύρου επιβεβαίωσης μετάφρασης",
"messages.prompt": "Λήμμα προτροπής",
"messages.input.enable_quick_triggers": "Ενεργοποίηση των '/' και '@' για γρήγορη πρόσβαση σε μενού", "messages.input.enable_quick_triggers": "Ενεργοποίηση των '/' και '@' για γρήγορη πρόσβαση σε μενού",
"messages.input.enable_delete_model": "Ενεργοποίηση διαγραφής μοντέλων/επισυναπτόμενων αρχείων με το πλήκτρο διαγραφής", "messages.input.enable_delete_model": "Ενεργοποίηση διαγραφής μοντέλων/επισυναπτόμενων αρχείων με το πλήκτρο διαγραφής",
"messages.math_engine.none": "Κανένα", "messages.math_engine.none": "Κανένα",

View File

@ -918,7 +918,6 @@
"about.updateNotAvailable": "Tu software ya está actualizado", "about.updateNotAvailable": "Tu software ya está actualizado",
"about.website.button": "Ver", "about.website.button": "Ver",
"about.website.title": "Sitio web oficial", "about.website.title": "Sitio web oficial",
"advanced.auto_switch_to_topics": "Cambiar automáticamente a temas",
"advanced.title": "Configuración avanzada", "advanced.title": "Configuración avanzada",
"assistant": "Asistente predeterminado", "assistant": "Asistente predeterminado",
"assistant.model_params": "Parámetros del modelo", "assistant.model_params": "Parámetros del modelo",
@ -1123,7 +1122,6 @@
"message_title.use_topic_naming.title": "Usar el modelo de nombramiento temático para crear títulos de mensajes exportados", "message_title.use_topic_naming.title": "Usar el modelo de nombramiento temático para crear títulos de mensajes exportados",
"message_title.use_topic_naming.help": "Al activarlo, se utilizará el modelo de nombramiento temático para generar títulos de mensajes exportados. Esta opción también afectará a todos los métodos de exportación mediante Markdown." "message_title.use_topic_naming.help": "Al activarlo, se utilizará el modelo de nombramiento temático para generar títulos de mensajes exportados. Esta opción también afectará a todos los métodos de exportación mediante Markdown."
}, },
"display.assistant.title": "Configuración del asistente",
"display.custom.css": "CSS personalizado", "display.custom.css": "CSS personalizado",
"display.custom.css.cherrycss": "Obtener desde cherrycss.com", "display.custom.css.cherrycss": "Obtener desde cherrycss.com",
"display.custom.css.placeholder": "/* Escribe tu CSS personalizado aquí */", "display.custom.css.placeholder": "/* Escribe tu CSS personalizado aquí */",
@ -1303,7 +1301,6 @@
"advancedSettings": "Configuración avanzada" "advancedSettings": "Configuración avanzada"
}, },
"messages.divider": "Separador de mensajes", "messages.divider": "Separador de mensajes",
"messages.divider.tooltip": "No aplicable para mensajes de estilo burbuja",
"messages.grid_columns": "Número de columnas en la cuadrícula de mensajes", "messages.grid_columns": "Número de columnas en la cuadrícula de mensajes",
"messages.grid_popover_trigger": "Desencadenante de detalles de cuadrícula", "messages.grid_popover_trigger": "Desencadenante de detalles de cuadrícula",
"messages.grid_popover_trigger.click": "Mostrar al hacer clic", "messages.grid_popover_trigger.click": "Mostrar al hacer clic",
@ -1485,9 +1482,6 @@
"theme.window.style.title": "Estilo de ventana", "theme.window.style.title": "Estilo de ventana",
"theme.window.style.transparent": "Ventana transparente", "theme.window.style.transparent": "Ventana transparente",
"title": "Configuración", "title": "Configuración",
"topic.position": "Posición del tema",
"topic.position.left": "Izquierda",
"topic.position.right": "Derecha",
"topic.show.time": "Mostrar tiempo del tema", "topic.show.time": "Mostrar tiempo del tema",
"tray.onclose": "Minimizar a la bandeja al cerrar", "tray.onclose": "Minimizar a la bandeja al cerrar",
"tray.show": "Mostrar bandera del sistema", "tray.show": "Mostrar bandera del sistema",
@ -1610,7 +1604,6 @@
"assistant.icon.type.none": "No mostrar", "assistant.icon.type.none": "No mostrar",
"general.auto_check_update.title": "Actualización automática", "general.auto_check_update.title": "Actualización automática",
"input.show_translate_confirm": "Mostrar diálogo de confirmación de traducción", "input.show_translate_confirm": "Mostrar diálogo de confirmación de traducción",
"messages.prompt": "Palabra de indicación",
"messages.input.enable_quick_triggers": "Habilitar menú rápido con '/' y '@'", "messages.input.enable_quick_triggers": "Habilitar menú rápido con '/' y '@'",
"messages.input.enable_delete_model": "Habilitar la eliminación con la tecla de borrado para modelos/archivos adjuntos introducidos", "messages.input.enable_delete_model": "Habilitar la eliminación con la tecla de borrado para modelos/archivos adjuntos introducidos",
"messages.math_engine.none": "Ninguno", "messages.math_engine.none": "Ninguno",

View File

@ -917,7 +917,6 @@
"about.updateNotAvailable": "Votre logiciel est déjà à jour", "about.updateNotAvailable": "Votre logiciel est déjà à jour",
"about.website.button": "Visiter le site web", "about.website.button": "Visiter le site web",
"about.website.title": "Site web officiel", "about.website.title": "Site web officiel",
"advanced.auto_switch_to_topics": "Basculer automatiquement vers les sujets",
"advanced.title": "Paramètres avancés", "advanced.title": "Paramètres avancés",
"assistant": "Assistant par défaut", "assistant": "Assistant par défaut",
"assistant.model_params": "Paramètres du modèle", "assistant.model_params": "Paramètres du modèle",
@ -1124,7 +1123,6 @@
"message_title.use_topic_naming.title": "Utiliser le modèle de dénomination thématique pour créer les titres des messages exportés", "message_title.use_topic_naming.title": "Utiliser le modèle de dénomination thématique pour créer les titres des messages exportés",
"message_title.use_topic_naming.help": "Lorsque cette option est activée, le modèle de dénomination thématique sera utilisé pour créer les titres des messages exportés. Cette option affectera également toutes les méthodes d'exportation au format Markdown." "message_title.use_topic_naming.help": "Lorsque cette option est activée, le modèle de dénomination thématique sera utilisé pour créer les titres des messages exportés. Cette option affectera également toutes les méthodes d'exportation au format Markdown."
}, },
"display.assistant.title": "Paramètres de l'assistant",
"display.custom.css": "CSS personnalisé", "display.custom.css": "CSS personnalisé",
"display.custom.css.cherrycss": "Obtenir depuis cherrycss.com", "display.custom.css.cherrycss": "Obtenir depuis cherrycss.com",
"display.custom.css.placeholder": "/* Écrire votre CSS personnalisé ici */", "display.custom.css.placeholder": "/* Écrire votre CSS personnalisé ici */",
@ -1304,7 +1302,6 @@
"advancedSettings": "Расширенные настройки" "advancedSettings": "Расширенные настройки"
}, },
"messages.divider": "Séparateur de messages", "messages.divider": "Séparateur de messages",
"messages.divider.tooltip": "Non applicable aux messages de style bulle",
"messages.grid_columns": "Nombre de colonnes de la grille de messages", "messages.grid_columns": "Nombre de colonnes de la grille de messages",
"messages.grid_popover_trigger": "Déclencheur de popover de la grille", "messages.grid_popover_trigger": "Déclencheur de popover de la grille",
"messages.grid_popover_trigger.click": "Afficher au clic", "messages.grid_popover_trigger.click": "Afficher au clic",
@ -1486,9 +1483,6 @@
"theme.window.style.title": "Style de fenêtre", "theme.window.style.title": "Style de fenêtre",
"theme.window.style.transparent": "Fenêtre transparente", "theme.window.style.transparent": "Fenêtre transparente",
"title": "Paramètres", "title": "Paramètres",
"topic.position": "Position du sujet",
"topic.position.left": "Gauche",
"topic.position.right": "Droite",
"topic.show.time": "Afficher l'heure du sujet", "topic.show.time": "Afficher l'heure du sujet",
"tray.onclose": "Minimiser dans la barre d'état système lors de la fermeture", "tray.onclose": "Minimiser dans la barre d'état système lors de la fermeture",
"tray.show": "Afficher l'icône dans la barre d'état système", "tray.show": "Afficher l'icône dans la barre d'état système",
@ -1611,7 +1605,6 @@
"assistant.icon.type.none": "Ne pas afficher", "assistant.icon.type.none": "Ne pas afficher",
"general.auto_check_update.title": "Mise à jour automatique", "general.auto_check_update.title": "Mise à jour automatique",
"input.show_translate_confirm": "Afficher la boîte de dialogue de confirmation de traduction", "input.show_translate_confirm": "Afficher la boîte de dialogue de confirmation de traduction",
"messages.prompt": "Mot-clé d'affichage",
"messages.input.enable_quick_triggers": "Activer les menus rapides avec '/' et '@'", "messages.input.enable_quick_triggers": "Activer les menus rapides avec '/' et '@'",
"messages.input.enable_delete_model": "Activer la touche Supprimer pour effacer le modèle/pièce jointe saisie", "messages.input.enable_delete_model": "Activer la touche Supprimer pour effacer le modèle/pièce jointe saisie",
"messages.math_engine.none": "Aucun", "messages.math_engine.none": "Aucun",

View File

@ -919,7 +919,6 @@
"about.updateNotAvailable": "Seu software já está atualizado", "about.updateNotAvailable": "Seu software já está atualizado",
"about.website.button": "Ver", "about.website.button": "Ver",
"about.website.title": "Site oficial", "about.website.title": "Site oficial",
"advanced.auto_switch_to_topics": "Alternar automaticamente para tópicos",
"advanced.title": "Configurações avançadas", "advanced.title": "Configurações avançadas",
"assistant": "Assistente padrão", "assistant": "Assistente padrão",
"assistant.model_params": "Parâmetros do modelo", "assistant.model_params": "Parâmetros do modelo",
@ -1126,7 +1125,6 @@
"message_title.use_topic_naming.title": "Usar modelo de nomeação por tópico para criar títulos das mensagens exportadas", "message_title.use_topic_naming.title": "Usar modelo de nomeação por tópico para criar títulos das mensagens exportadas",
"message_title.use_topic_naming.help": "Ativando esta opção, será usado um modelo de nomeação por tópico para criar os títulos das mensagens exportadas. Esta configuração também afetará todas as formas de exportação feitas por meio de Markdown." "message_title.use_topic_naming.help": "Ativando esta opção, será usado um modelo de nomeação por tópico para criar os títulos das mensagens exportadas. Esta configuração também afetará todas as formas de exportação feitas por meio de Markdown."
}, },
"display.assistant.title": "Configurações do assistente",
"display.custom.css": "CSS personalizado", "display.custom.css": "CSS personalizado",
"display.custom.css.cherrycss": "Obter do cherrycss.com", "display.custom.css.cherrycss": "Obter do cherrycss.com",
"display.custom.css.placeholder": "/* Escreva seu CSS personalizado aqui */", "display.custom.css.placeholder": "/* Escreva seu CSS personalizado aqui */",
@ -1306,7 +1304,6 @@
"advancedSettings": "Configurações Avançadas" "advancedSettings": "Configurações Avançadas"
}, },
"messages.divider": "Divisor de mensagens", "messages.divider": "Divisor de mensagens",
"messages.divider.tooltip": "Não aplicável a mensagens de estilo bolha",
"messages.grid_columns": "Número de colunas da grade de mensagens", "messages.grid_columns": "Número de colunas da grade de mensagens",
"messages.grid_popover_trigger": "Disparador de detalhes da grade", "messages.grid_popover_trigger": "Disparador de detalhes da grade",
"messages.grid_popover_trigger.click": "Clique para mostrar", "messages.grid_popover_trigger.click": "Clique para mostrar",
@ -1488,9 +1485,6 @@
"theme.window.style.title": "Estilo de janela", "theme.window.style.title": "Estilo de janela",
"theme.window.style.transparent": "Janela transparente", "theme.window.style.transparent": "Janela transparente",
"title": "Configurações", "title": "Configurações",
"topic.position": "Posição do tópico",
"topic.position.left": "Esquerda",
"topic.position.right": "Direita",
"topic.show.time": "Mostrar tempo do tópico", "topic.show.time": "Mostrar tempo do tópico",
"tray.onclose": "Minimizar para bandeja ao fechar", "tray.onclose": "Minimizar para bandeja ao fechar",
"tray.show": "Mostrar ícone de bandeja", "tray.show": "Mostrar ícone de bandeja",
@ -1613,7 +1607,6 @@
"assistant.icon.type.none": "Não mostrar", "assistant.icon.type.none": "Não mostrar",
"general.auto_check_update.title": "Atualização automática", "general.auto_check_update.title": "Atualização automática",
"input.show_translate_confirm": "Mostrar diálogo de confirmação de tradução", "input.show_translate_confirm": "Mostrar diálogo de confirmação de tradução",
"messages.prompt": "Exibir palavra-chave",
"messages.input.enable_quick_triggers": "Ativar menu rápido com '/' e '@'", "messages.input.enable_quick_triggers": "Ativar menu rápido com '/' e '@'",
"messages.input.enable_delete_model": "Ativar tecla de exclusão para remover modelos/anexos inseridos", "messages.input.enable_delete_model": "Ativar tecla de exclusão para remover modelos/anexos inseridos",
"messages.math_engine.none": "Nenhum", "messages.math_engine.none": "Nenhum",

View File

@ -1,5 +1,5 @@
import { ImportOutlined, PlusOutlined } from '@ant-design/icons' import { ImportOutlined, PlusOutlined } from '@ant-design/icons'
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar' import { NavbarCenter, NavbarMain } from '@renderer/components/app/Navbar'
import CustomTag from '@renderer/components/CustomTag' import CustomTag from '@renderer/components/CustomTag'
import ListItem from '@renderer/components/ListItem' import ListItem from '@renderer/components/ListItem'
import Scrollbar from '@renderer/components/Scrollbar' import Scrollbar from '@renderer/components/Scrollbar'
@ -152,7 +152,7 @@ const AgentsPage: FC = () => {
return ( return (
<Container> <Container>
<Navbar> <NavbarMain>
<NavbarCenter style={{ borderRight: 'none', justifyContent: 'space-between' }}> <NavbarCenter style={{ borderRight: 'none', justifyContent: 'space-between' }}>
{t('agents.title')} {t('agents.title')}
<Input <Input
@ -169,9 +169,9 @@ const AgentsPage: FC = () => {
onChange={(e) => setSearchInput(e.target.value)} onChange={(e) => setSearchInput(e.target.value)}
onPressEnter={handleSearch} onPressEnter={handleSearch}
/> />
<div style={{ width: 80 }} /> <div style={{ width: 1 }} />
</NavbarCenter> </NavbarCenter>
</Navbar> </NavbarMain>
<Main id="content-container"> <Main id="content-container">
<AgentsGroupList> <AgentsGroupList>

View File

@ -1,7 +1,7 @@
import { Navbar, NavbarMain } from '@renderer/components/app/Navbar' import { NavbarCenter, NavbarMain } from '@renderer/components/app/Navbar'
import { useMinapps } from '@renderer/hooks/useMinapps' import { useMinapps } from '@renderer/hooks/useMinapps'
import { Button, Input } from 'antd' import { Button, Input } from 'antd'
import { Search, SettingsIcon, X } from 'lucide-react' import { Search, SettingsIcon } from 'lucide-react'
import React, { FC, useEffect, useState } from 'react' import React, { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useLocation } from 'react-router' import { useLocation } from 'react-router'
@ -41,8 +41,8 @@ const AppsPage: FC = () => {
return ( return (
<Container onContextMenu={handleContextMenu}> <Container onContextMenu={handleContextMenu}>
<Navbar> <NavbarMain>
<NavbarMain> <NavbarCenter>
{t('minapp.title')} {t('minapp.title')}
<Input <Input
placeholder={t('common.search')} placeholder={t('common.search')}
@ -50,10 +50,7 @@ const AppsPage: FC = () => {
style={{ style={{
width: '30%', width: '30%',
height: 28, height: 28,
borderRadius: 15, borderRadius: 15
position: 'absolute',
left: '50vw',
transform: 'translateX(-50%)'
}} }}
size="small" size="small"
variant="filled" variant="filled"
@ -65,11 +62,11 @@ const AppsPage: FC = () => {
<Button <Button
type="text" type="text"
className="nodrag" className="nodrag"
icon={isSettingsOpen ? <X size={18} /> : <SettingsIcon size={18} color="var(--color-text-2)" />} icon={<SettingsIcon size={18} color={isSettingsOpen ? 'var(--color-primary)' : 'var(--color-text-2)'} />}
onClick={() => setIsSettingsOpen(!isSettingsOpen)} onClick={() => setIsSettingsOpen(!isSettingsOpen)}
/> />
</NavbarMain> </NavbarCenter>
</Navbar> </NavbarMain>
<ContentContainer id="content-container"> <ContentContainer id="content-container">
{isSettingsOpen && <MiniAppSettings />} {isSettingsOpen && <MiniAppSettings />}
{!isSettingsOpen && ( {!isSettingsOpen && (

View File

@ -5,7 +5,7 @@ import {
SortAscendingOutlined, SortAscendingOutlined,
SortDescendingOutlined SortDescendingOutlined
} from '@ant-design/icons' } from '@ant-design/icons'
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar' import { NavbarCenter, NavbarMain } from '@renderer/components/app/Navbar'
import ListItem from '@renderer/components/ListItem' import ListItem from '@renderer/components/ListItem'
import TextEditPopup from '@renderer/components/Popups/TextEditPopup' import TextEditPopup from '@renderer/components/Popups/TextEditPopup'
import Logger from '@renderer/config/logger' import Logger from '@renderer/config/logger'
@ -207,9 +207,9 @@ const FilesPage: FC = () => {
return ( return (
<Container> <Container>
<Navbar> <NavbarMain>
<NavbarCenter style={{ borderRight: 'none' }}>{t('files.title')}</NavbarCenter> <NavbarCenter style={{ borderRight: 'none' }}>{t('files.title')}</NavbarCenter>
</Navbar> </NavbarMain>
<ContentContainer id="content-container"> <ContentContainer id="content-container">
<SideNav> <SideNav>
{menuItems.map((item) => ( {menuItems.map((item) => (

View File

@ -1,11 +1,11 @@
import { ArrowRightOutlined } from '@ant-design/icons' import { ArrowRightOutlined } from '@ant-design/icons'
import { HStack } from '@renderer/components/Layout' import { HStack } from '@renderer/components/Layout'
import { MessageEditingProvider } from '@renderer/context/MessageEditingContext' import { MessageEditingProvider } from '@renderer/context/MessageEditingContext'
import { useChat } from '@renderer/hooks/useChat'
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
import { getTopicById } from '@renderer/hooks/useTopic' import { getTopicById } from '@renderer/hooks/useTopic'
import { default as MessageItem } from '@renderer/pages/home/Messages/Message' import { default as MessageItem } from '@renderer/pages/home/Messages/Message'
import { locateToMessage } from '@renderer/services/MessagesService' import { locateToMessage } from '@renderer/services/MessagesService'
import NavigationService from '@renderer/services/NavigationService'
import { Topic } from '@renderer/types' import { Topic } from '@renderer/types'
import type { Message } from '@renderer/types/newMessage' import type { Message } from '@renderer/types/newMessage'
import { runAsyncFunction } from '@renderer/utils' import { runAsyncFunction } from '@renderer/utils'
@ -19,10 +19,10 @@ interface Props extends React.HTMLAttributes<HTMLDivElement> {
} }
const SearchMessage: FC<Props> = ({ message, ...props }) => { const SearchMessage: FC<Props> = ({ message, ...props }) => {
const navigate = NavigationService.navigate!
const { messageStyle } = useSettings() const { messageStyle } = useSettings()
const { t } = useTranslation() const { t } = useTranslation()
const [topic, setTopic] = useState<Topic | null>(null) const [topic, setTopic] = useState<Topic | null>(null)
const { setActiveAssistant, setActiveTopic } = useChat()
useEffect(() => { useEffect(() => {
runAsyncFunction(async () => { runAsyncFunction(async () => {
@ -50,11 +50,13 @@ const SearchMessage: FC<Props> = ({ message, ...props }) => {
type="text" type="text"
size="middle" size="middle"
style={{ color: 'var(--color-text-3)', position: 'absolute', right: 0, top: 10 }} style={{ color: 'var(--color-text-3)', position: 'absolute', right: 0, top: 10 }}
onClick={() => locateToMessage(navigate, message)} onClick={() => locateToMessage({ message, setActiveAssistant, setActiveTopic })}
icon={<ArrowRightOutlined />} icon={<ArrowRightOutlined />}
/> />
<HStack mt="10px" justifyContent="center"> <HStack mt="10px" justifyContent="center">
<Button onClick={() => locateToMessage(navigate, message)} icon={<ArrowRightOutlined />}> <Button
onClick={() => locateToMessage({ message, setActiveAssistant, setActiveTopic })}
icon={<ArrowRightOutlined />}>
{t('history.locate.message')} {t('history.locate.message')}
</Button> </Button>
</HStack> </HStack>

View File

@ -2,12 +2,12 @@ import { ArrowRightOutlined, MessageOutlined } from '@ant-design/icons'
import { HStack } from '@renderer/components/Layout' import { HStack } from '@renderer/components/Layout'
import SearchPopup from '@renderer/components/Popups/SearchPopup' import SearchPopup from '@renderer/components/Popups/SearchPopup'
import { MessageEditingProvider } from '@renderer/context/MessageEditingContext' import { MessageEditingProvider } from '@renderer/context/MessageEditingContext'
import { useChat } from '@renderer/hooks/useChat'
import useScrollPosition from '@renderer/hooks/useScrollPosition' import useScrollPosition from '@renderer/hooks/useScrollPosition'
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
import { getAssistantById } from '@renderer/services/AssistantService' import { getAssistantById } from '@renderer/services/AssistantService'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { isGenerating, locateToMessage } from '@renderer/services/MessagesService' import { locateToMessage } from '@renderer/services/MessagesService'
import NavigationService from '@renderer/services/NavigationService'
import { useAppDispatch } from '@renderer/store' import { useAppDispatch } from '@renderer/store'
import { loadTopicMessagesThunk } from '@renderer/store/thunk/messageThunk' import { loadTopicMessagesThunk } from '@renderer/store/thunk/messageThunk'
import { Topic } from '@renderer/types' import { Topic } from '@renderer/types'
@ -23,10 +23,10 @@ interface Props extends React.HTMLAttributes<HTMLDivElement> {
} }
const TopicMessages: FC<Props> = ({ topic, ...props }) => { const TopicMessages: FC<Props> = ({ topic, ...props }) => {
const navigate = NavigationService.navigate!
const { handleScroll, containerRef } = useScrollPosition('TopicMessages') const { handleScroll, containerRef } = useScrollPosition('TopicMessages')
const { messageStyle } = useSettings() const { messageStyle } = useSettings()
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const { setActiveAssistant, setActiveTopic } = useChat()
useEffect(() => { useEffect(() => {
topic && dispatch(loadTopicMessagesThunk(topic.id)) topic && dispatch(loadTopicMessagesThunk(topic.id))
@ -39,11 +39,13 @@ const TopicMessages: FC<Props> = ({ topic, ...props }) => {
} }
const onContinueChat = async (topic: Topic) => { const onContinueChat = async (topic: Topic) => {
await isGenerating()
SearchPopup.hide() SearchPopup.hide()
const assistant = getAssistantById(topic.assistantId) const assistant = getAssistantById(topic.assistantId)
navigate('/', { state: { assistant, topic } }) if (assistant) {
setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR), 100) setActiveAssistant(assistant)
setActiveTopic(topic)
setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR), 100)
}
} }
return ( return (
@ -57,7 +59,7 @@ const TopicMessages: FC<Props> = ({ topic, ...props }) => {
type="text" type="text"
size="middle" size="middle"
style={{ color: 'var(--color-text-3)', position: 'absolute', right: 0, top: 5 }} style={{ color: 'var(--color-text-3)', position: 'absolute', right: 0, top: 5 }}
onClick={() => locateToMessage(navigate, message)} onClick={() => locateToMessage({ message, setActiveAssistant, setActiveTopic })}
icon={<ArrowRightOutlined />} icon={<ArrowRightOutlined />}
/> />
<Divider style={{ margin: '8px auto 15px' }} variant="dashed" /> <Divider style={{ margin: '8px auto 15px' }} variant="dashed" />

View File

@ -1,12 +1,10 @@
import { ContentSearch, ContentSearchRef } from '@renderer/components/ContentSearch' import { ContentSearch, ContentSearchRef } from '@renderer/components/ContentSearch'
import MultiSelectActionPopup from '@renderer/components/Popups/MultiSelectionPopup' import MultiSelectActionPopup from '@renderer/components/Popups/MultiSelectionPopup'
import { QuickPanelProvider } from '@renderer/components/QuickPanel' 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 { useChatContext } from '@renderer/hooks/useChatContext'
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
import { useShortcut } from '@renderer/hooks/useShortcuts' import { useShortcut } from '@renderer/hooks/useShortcuts'
import { useShowTopics } from '@renderer/hooks/useStore'
import { Assistant, Topic } from '@renderer/types'
import { Flex } from 'antd' import { Flex } from 'antd'
import { debounce } from 'lodash' import { debounce } from 'lodash'
import React, { FC, useMemo, useState } from 'react' import React, { FC, useMemo, useState } from 'react'
@ -15,31 +13,20 @@ import styled from 'styled-components'
import Inputbar from './Inputbar/Inputbar' import Inputbar from './Inputbar/Inputbar'
import Messages from './Messages/Messages' import Messages from './Messages/Messages'
import Tabs from './Tabs'
interface Props { const Chat: FC = () => {
assistant: Assistant const { activeAssistant, activeTopic, setActiveTopic } = useChat()
activeTopic: Topic const { messageStyle, showAssistants } = useSettings()
setActiveTopic: (topic: Topic) => void const { isMultiSelectMode } = useChatContext(activeTopic)
setActiveAssistant: (assistant: Assistant) => void
}
const Chat: FC<Props> = (props) => {
const { assistant } = useAssistant(props.assistant.id)
const { topicPosition, messageStyle, showAssistants } = useSettings()
const { showTopics } = useShowTopics()
const { isMultiSelectMode } = useChatContext(props.activeTopic)
const mainRef = React.useRef<HTMLDivElement>(null) const mainRef = React.useRef<HTMLDivElement>(null)
const contentSearchRef = React.useRef<ContentSearchRef>(null) const contentSearchRef = React.useRef<ContentSearchRef>(null)
const [filterIncludeUser, setFilterIncludeUser] = useState(false) const [filterIncludeUser, setFilterIncludeUser] = useState(false)
const maxWidth = useMemo(() => { const maxWidth = useMemo(() => {
const showRightTopics = showTopics && topicPosition === 'right'
const minusAssistantsWidth = showAssistants ? '- var(--assistants-width)' : '' const minusAssistantsWidth = showAssistants ? '- var(--assistants-width)' : ''
const minusRightTopicsWidth = showRightTopics ? '- var(--assistants-width)' : '' return `calc(100vw - ${minusAssistantsWidth})`
return `calc(100vw - var(--sidebar-width) ${minusAssistantsWidth} ${minusRightTopicsWidth})` }, [showAssistants])
}, [showAssistants, showTopics, topicPosition])
useHotkeys('esc', () => { useHotkeys('esc', () => {
contentSearchRef.current?.disable() contentSearchRef.current?.disable()
@ -116,36 +103,24 @@ const Chat: FC<Props> = (props) => {
onIncludeUserChange={userOutlinedItemClickHandler} onIncludeUserChange={userOutlinedItemClickHandler}
/> />
<Messages <Messages
key={props.activeTopic.id} key={activeTopic.id}
assistant={assistant} assistant={activeAssistant}
topic={props.activeTopic} topic={activeTopic}
setActiveTopic={props.setActiveTopic} setActiveTopic={setActiveTopic}
onComponentUpdate={messagesComponentUpdateHandler} onComponentUpdate={messagesComponentUpdateHandler}
onFirstUpdate={messagesComponentFirstUpdateHandler} onFirstUpdate={messagesComponentFirstUpdateHandler}
/> />
<QuickPanelProvider> <QuickPanelProvider>
<Inputbar assistant={assistant} setActiveTopic={props.setActiveTopic} topic={props.activeTopic} /> <Inputbar />
{isMultiSelectMode && <MultiSelectActionPopup topic={props.activeTopic} />} {isMultiSelectMode && <MultiSelectActionPopup topic={activeTopic} />}
</QuickPanelProvider> </QuickPanelProvider>
</Main> </Main>
{topicPosition === 'right' && showTopics && (
<Tabs
activeAssistant={assistant}
activeTopic={props.activeTopic}
setActiveAssistant={props.setActiveAssistant}
setActiveTopic={props.setActiveTopic}
position="right"
/>
)}
</Container> </Container>
) )
} }
const Container = styled.div` const Container = styled.div`
display: flex;
flex-direction: row;
height: 100%; height: 100%;
flex: 1;
` `
const Main = styled(Flex)` const Main = styled(Flex)`

View File

@ -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 (
<Navbar className="home-navbar">
<NavbarContainer $isFullscreen={isFullscreen} $showSidebar={showAssistants} className="home-navbar-right">
<HStack alignItems="center">
<NavbarIcon
onClick={() => toggleShowAssistants()}
style={{ marginRight: 8, marginLeft: isMac && !isFullscreen ? 4 : -12 }}>
{showAssistants ? <PanelLeft size={18} /> : <PanelRight size={18} />}
</NavbarIcon>
<SelectModelButton assistant={assistant} />
</HStack>
<HStack alignItems="center" gap={8}>
<UpdateAppButton />
{isMac && (
<Tooltip title={t('chat.assistant.search.placeholder')} mouseEnterDelay={0.8}>
<NarrowIcon onClick={() => SearchPopup.show()}>
<Search size={18} />
</NarrowIcon>
</Tooltip>
)}
<Tooltip title={t('navbar.expand')} mouseEnterDelay={0.8}>
<NarrowIcon onClick={handleNarrowModeToggle}>
<NarrowModeIcon isNarrowMode={narrowMode} />
</NarrowIcon>
</Tooltip>
{sidebarIcons.visible.includes('minapp') && (
<MinAppsPopover>
<Tooltip title={t('minapp.title')} mouseEnterDelay={0.8}>
<NarrowIcon>
<LayoutGrid size={18} />
</NarrowIcon>
</Tooltip>
</MinAppsPopover>
)}
</HStack>
</NavbarContainer>
</Navbar>
)
}
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

View File

@ -1,58 +1,15 @@
import { useAssistants } from '@renderer/hooks/useAssistant'
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
import { useActiveTopic } from '@renderer/hooks/useTopic' import { FC, useEffect } from 'react'
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 styled from 'styled-components' import styled from 'styled-components'
import Chat from './Chat' import Chat from './Chat'
import Navbar from './Navbar' import ChatNavbar from './ChatNavbar'
import HomeTabs from './Tabs'
let _activeAssistant: Assistant
const HomePage: FC = () => { 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() const { showAssistants, showTopics, topicPosition } = useSettings()
_activeAssistant = activeAssistant
useEffect(() => { useEffect(() => {
NavigationService.setNavigate(navigate) window.api.window.setMinimumSize(showAssistants ? 1080 : 520, 600)
}, [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)
return () => { return () => {
window.api.window.resetMinimumSize() window.api.window.resetMinimumSize()
@ -61,45 +18,22 @@ const HomePage: FC = () => {
return ( return (
<Container id="home-page"> <Container id="home-page">
<Navbar <ChatNavbar />
activeAssistant={activeAssistant}
activeTopic={activeTopic}
setActiveTopic={setActiveTopic}
setActiveAssistant={setActiveAssistant}
position="left"
/>
<ContentContainer id="content-container"> <ContentContainer id="content-container">
{showAssistants && ( <Chat />
<HomeTabs
activeAssistant={activeAssistant}
activeTopic={activeTopic}
setActiveAssistant={setActiveAssistant}
setActiveTopic={setActiveTopic}
position="left"
/>
)}
<Chat
assistant={activeAssistant}
activeTopic={activeTopic}
setActiveTopic={setActiveTopic}
setActiveAssistant={setActiveAssistant}
/>
</ContentContainer> </ContentContainer>
</Container> </Container>
) )
} }
const Container = styled.div` const Container = styled.div`
min-width: 0;
display: flex; display: flex;
flex: 1; flex: 1;
flex-direction: column; flex-direction: column;
max-width: calc(100vw - var(--sidebar-width));
` `
const ContentContainer = styled.div` const ContentContainer = styled.div`
display: flex;
flex: 1;
flex-direction: row;
overflow: hidden; overflow: hidden;
` `

View File

@ -11,11 +11,12 @@ import {
} from '@renderer/config/models' } from '@renderer/config/models'
import db from '@renderer/databases' import db from '@renderer/databases'
import { useAssistant } from '@renderer/hooks/useAssistant' import { useAssistant } from '@renderer/hooks/useAssistant'
import { useChat } from '@renderer/hooks/useChat'
import { useKnowledgeBases } from '@renderer/hooks/useKnowledge' import { useKnowledgeBases } from '@renderer/hooks/useKnowledge'
import { useMCPServers } from '@renderer/hooks/useMCPServers' import { useMCPServers } from '@renderer/hooks/useMCPServers'
import { useMessageOperations, useTopicLoading } from '@renderer/hooks/useMessageOperations' import { useMessageOperations, useTopicLoading } from '@renderer/hooks/useMessageOperations'
import { modelGenerating, useRuntime } from '@renderer/hooks/useRuntime' import { useRuntime } from '@renderer/hooks/useRuntime'
import { useMessageStyle, useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
import { useShortcut, useShortcutDisplay } from '@renderer/hooks/useShortcuts' import { useShortcut, useShortcutDisplay } from '@renderer/hooks/useShortcuts'
import { useSidebarIconShow } from '@renderer/hooks/useSidebarIcon' import { useSidebarIconShow } from '@renderer/hooks/useSidebarIcon'
import { getDefaultTopic } from '@renderer/services/AssistantService' import { getDefaultTopic } from '@renderer/services/AssistantService'
@ -30,7 +31,7 @@ import WebSearchService from '@renderer/services/WebSearchService'
import { useAppDispatch, useAppSelector } from '@renderer/store' import { useAppDispatch, useAppSelector } from '@renderer/store'
import { setSearching } from '@renderer/store/runtime' import { setSearching } from '@renderer/store/runtime'
import { sendMessage as _sendMessage } from '@renderer/store/thunk/messageThunk' 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 type { MessageInputBaseParams } from '@renderer/types/newMessage'
import { classNames, delay, formatFileSize, getFileExtension } from '@renderer/utils' import { classNames, delay, formatFileSize, getFileExtension } from '@renderer/utils'
import { formatQuotedText } from '@renderer/utils/formats' import { formatQuotedText } from '@renderer/utils/formats'
@ -52,21 +53,17 @@ import InputbarTools, { InputbarToolsRef } from './InputbarTools'
import KnowledgeBaseInput from './KnowledgeBaseInput' import KnowledgeBaseInput from './KnowledgeBaseInput'
import MentionModelsInput from './MentionModelsInput' import MentionModelsInput from './MentionModelsInput'
import SendMessageButton from './SendMessageButton' import SendMessageButton from './SendMessageButton'
import SettingButton from './SettingButton'
import TokenCount from './TokenCount' import TokenCount from './TokenCount'
interface Props {
assistant: Assistant
setActiveTopic: (topic: Topic) => void
topic: Topic
}
let _text = '' let _text = ''
let _files: FileType[] = [] let _files: FileType[] = []
const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) => { const Inputbar: FC = () => {
const { activeAssistant, activeTopic: topic, setActiveTopic } = useChat()
const [text, setText] = useState(_text) const [text, setText] = useState(_text)
const [inputFocus, setInputFocus] = useState(false) const [inputFocus, setInputFocus] = useState(false)
const { assistant, addTopic, model, setModel, updateAssistant } = useAssistant(_assistant.id) const { assistant, addTopic, model, setModel, updateAssistant } = useAssistant(activeAssistant.id)
const { const {
targetLanguage, targetLanguage,
sendMessageShortcut, sendMessageShortcut,
@ -86,7 +83,6 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
const { t } = useTranslation() const { t } = useTranslation()
const containerRef = useRef(null) const containerRef = useRef(null)
const { searching } = useRuntime() const { searching } = useRuntime()
const { isBubbleStyle } = useMessageStyle()
const { pauseMessages } = useMessageOperations(topic) const { pauseMessages } = useMessageOperations(topic)
const loading = useTopicLoading(topic) const loading = useTopicLoading(topic)
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
@ -139,17 +135,20 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
_text = text _text = text
_files = files _files = files
const resizeTextArea = useCallback(() => { const resizeTextArea = useCallback(
const textArea = textareaRef.current?.resizableTextArea?.textArea (force: boolean = false) => {
if (textArea) { const textArea = textareaRef.current?.resizableTextArea?.textArea
// 如果已经手动设置了高度,则不自动调整 if (textArea) {
if (textareaHeight) { // 如果已经手动设置了高度,则不自动调整
return 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 () => { const sendMessage = useCallback(async () => {
if (inputEmpty || loading) { if (inputEmpty || loading) {
@ -405,8 +404,6 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
} }
const addNewTopic = useCallback(async () => { const addNewTopic = useCallback(async () => {
await modelGenerating()
const topic = getDefaultTopic(assistant.id) const topic = getDefaultTopic(assistant.id)
await db.topics.add({ id: topic.id, messages: [] }) await db.topics.add({ id: topic.id, messages: [] })
@ -629,11 +626,6 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
useEffect(() => { useEffect(() => {
const _setEstimateTokenCount = debounce(setEstimateTokenCount, 100, { leading: false, trailing: true }) const _setEstimateTokenCount = debounce(setEstimateTokenCount, 100, { leading: false, trailing: true })
const unsubscribes = [ 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 }) => { EventEmitter.on(EVENT_NAMES.ESTIMATED_TOKEN_COUNT, ({ tokensCount, contextCount }) => {
_setEstimateTokenCount(tokensCount) _setEstimateTokenCount(tokensCount)
setContextCount({ current: contextCount.current, max: contextCount.max }) // 现在contextCount是一个对象而不是单个数值 setContextCount({ current: contextCount.current, max: contextCount.max }) // 现在contextCount是一个对象而不是单个数值
@ -693,8 +685,6 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
setSelectedKnowledgeBases(showKnowledgeIcon ? (assistant.knowledge_bases ?? []) : []) setSelectedKnowledgeBases(showKnowledgeIcon ? (assistant.knowledge_bases ?? []) : [])
}, [assistant.id, assistant.knowledge_bases, showKnowledgeIcon]) }, [assistant.id, assistant.knowledge_bases, showKnowledgeIcon])
const textareaRows = window.innerHeight >= 1000 || isBubbleStyle ? 2 : 1
const handleKnowledgeBaseSelect = (bases?: KnowledgeBase[]) => { const handleKnowledgeBaseSelect = (bases?: KnowledgeBase[]) => {
updateAssistant({ ...assistant, knowledge_bases: bases }) updateAssistant({ ...assistant, knowledge_bases: bases })
setSelectedKnowledgeBases(bases ?? []) setSelectedKnowledgeBases(bases ?? [])
@ -752,12 +742,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
} else { } else {
textArea.style.height = 'auto' textArea.style.height = 'auto'
setTextareaHeight(undefined) setTextareaHeight(undefined)
requestAnimationFrame(() => { setTimeout(() => resizeTextArea(true), 0)
if (textArea) {
const contentHeight = textArea.scrollHeight
textArea.style.height = contentHeight > 400 ? '400px' : `${contentHeight}px`
}
})
} }
textareaRef.current?.focus() textareaRef.current?.focus()
@ -802,7 +787,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
contextMenu="true" contextMenu="true"
variant="borderless" variant="borderless"
spellCheck={false} spellCheck={false}
rows={textareaRows} rows={2}
ref={textareaRef} ref={textareaRef}
style={{ style={{
fontSize, fontSize,
@ -858,11 +843,12 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
ToolbarButton={ToolbarButton} ToolbarButton={ToolbarButton}
onClick={onNewContext} onClick={onNewContext}
/> />
<SettingButton assistant={assistant} ToolbarButton={ToolbarButton} />
<TranslateButton text={text} onTranslated={onTranslated} isLoading={isTranslating} /> <TranslateButton text={text} onTranslated={onTranslated} isLoading={isTranslating} />
{loading && ( {loading && (
<Tooltip placement="top" title={t('chat.input.pause')} arrow> <Tooltip placement="top" title={t('chat.input.pause')} arrow>
<ToolbarButton type="text" onClick={onPause} style={{ marginRight: -2, marginTop: 1 }}> <ToolbarButton type="text" onClick={onPause} style={{ width: 30, height: 30, marginRight: 2 }}>
<CirclePause style={{ color: 'var(--color-error)', fontSize: 20 }} /> <CirclePause style={{ color: 'var(--color-error)' }} size={28} />
</ToolbarButton> </ToolbarButton>
</Tooltip> </Tooltip>
)} )}
@ -909,10 +895,10 @@ const Container = styled.div`
` `
const InputBarContainer = styled.div` const InputBarContainer = styled.div`
border: 0.5px solid var(--color-border); border: 1px solid var(--color-border);
transition: all 0.2s ease; transition: all 0.2s ease;
position: relative; position: relative;
margin: 14px 20px; margin: 16px 20px;
margin-top: 0; margin-top: 0;
border-radius: 15px; border-radius: 15px;
padding-top: 6px; // 为拖动手柄留出空间 padding-top: 6px; // 为拖动手柄留出空间
@ -949,10 +935,13 @@ const Textarea = styled(TextArea)`
overflow: auto; overflow: auto;
width: 100%; width: 100%;
box-sizing: border-box; box-sizing: border-box;
transition: height 0.2s ease; transition: none !important;
&.ant-input { &.ant-input {
line-height: 1.4; line-height: 1.4;
} }
.ant-input-textarea-show-count::after {
transition: none !important;
}
` `
const Toolbar = styled.div` const Toolbar = styled.div`
@ -961,7 +950,7 @@ const Toolbar = styled.div`
justify-content: space-between; justify-content: space-between;
padding: 0 8px; padding: 0 8px;
padding-bottom: 0; padding-bottom: 0;
margin-bottom: 4px; margin-bottom: 5px;
height: 30px; height: 30px;
gap: 16px; gap: 16px;
position: relative; position: relative;

View File

@ -176,7 +176,7 @@ const MCPToolsButton: FC<Props> = ({ ref, setInputValue, resizeTextArea, Toolbar
newList.push({ newList.push({
label: t('settings.mcp.addServer') + '...', label: t('settings.mcp.addServer') + '...',
icon: <Plus />, icon: <Plus />,
action: () => navigate('/settings/mcp') action: () => navigate('/mcp-servers')
}) })
newList.unshift({ newList.unshift({

View File

@ -13,7 +13,7 @@ const SendMessageButton: FC<Props> = ({ disabled, sendMessage }) => {
style={{ style={{
cursor: disabled ? 'not-allowed' : 'pointer', cursor: disabled ? 'not-allowed' : 'pointer',
color: disabled ? 'var(--color-text-3)' : 'var(--color-primary)', color: disabled ? 'var(--color-text-3)' : 'var(--color-primary)',
fontSize: 22, fontSize: 30,
transition: 'all 0.2s', transition: 'all 0.2s',
marginRight: 2 marginRight: 2
}} }}

View File

@ -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<Props> = ({ ToolbarButton }) => {
return (
<Popover
placement="topLeft"
content={<SettingsTab />}
trigger="click"
styles={{
body: {
padding: '4px 2px 4px 2px'
}
}}>
<ToolbarButton type="text">
<SlidersHorizontal size={16} />
</ToolbarButton>
</Popover>
)
}
export default SettingButton

View File

@ -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<Props> = () => {
return (
<Container>
<div>
{!isMac && (
<Tooltip title={t('chat.assistant.search.placeholder')} mouseEnterDelay={0.8}>
<NarrowIcon onClick={() => SearchPopup.show()}>
<Search size={18} />
</NarrowIcon>
</Tooltip>
)}
</div>
<Tooltip title={t('settings.shortcuts.new_topic')} mouseEnterDelay={0.8}>
<NavbarIcon onClick={() => EventEmitter.emit(EVENT_NAMES.ADD_NEW_TOPIC)} style={{ marginRight: 5 }}>
<MessageSquareDiff size={18} />
</NavbarIcon>
</Tooltip>
</Container>
)
}
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

View File

@ -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<Tab>('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: <Sparkle size={18} className="icon" />, text: t('agents.title'), path: '/agents' },
{ icon: <Languages size={18} className="icon" />, text: t('translate.title'), path: '/translate' },
{
icon: <Palette size={18} className="icon" />,
text: t('paintings.title'),
path: `/paintings/${defaultPaintingProvider}`
},
{ icon: <LayoutGrid size={18} className="icon" />, text: t('minapp.title'), path: '/apps' },
{ icon: <FileSearch size={18} className="icon" />, text: t('knowledge.title'), path: '/knowledge' },
{ icon: <SquareTerminal size={18} className="icon" />, text: t('settings.mcp.title'), path: '/mcp-servers' },
{ icon: <Folder size={18} className="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 (
<Container id="main-sidebar">
<MainNavbar />
<MainMenu>
<MainMenuItem active={isAppMenuExpanded} onClick={() => setIsAppMenuExpanded(!isAppMenuExpanded)}>
<MainMenuItemLeft>
<MainMenuItemIcon>
<Blocks size={19} className="icon" />
</MainMenuItemIcon>
<MainMenuItemText>{isAppMenuExpanded ? t('common.collapse') : t('common.apps')}</MainMenuItemText>
</MainMenuItemLeft>
<MainMenuItemRight>
{isAppMenuExpanded ? (
<ChevronDown size={18} color="var(--color-text-3)" />
) : (
<ChevronRight size={18} color="var(--color-text-3)" />
)}
</MainMenuItemRight>
</MainMenuItem>
<AnimatePresence initial={false}>
{isAppMenuExpanded && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.2 }}>
<SubMenu>
{appMenuItems.map((item) => (
<MainMenuItem
key={item.path}
active={isRoutes(item.path)}
onClick={() => {
navigate(item.path)
setIsAppMenuExpanded(false)
}}>
<MainMenuItemLeft>
<MainMenuItemIcon>{item.icon}</MainMenuItemIcon>
<MainMenuItemText>{item.text}</MainMenuItemText>
</MainMenuItemLeft>
</MainMenuItem>
))}
<PinnedApps />
</SubMenu>
</motion.div>
)}
</AnimatePresence>
<OpenedMinappTabs />
</MainMenu>
{tab === 'topic' && (
<AssistantContainer onClick={() => setIsAppMenuExpanded(false)}>
<AssistantItem
key={activeAssistant.id}
assistant={activeAssistant}
isActive={false}
sortBy="list"
onSwitch={() => {}}
onDelete={() => {}}
addAgent={() => {}}
addAssistant={() => {}}
onCreateDefaultAssistant={() => {}}
handleSortByChange={() => {}}
singleLine
/>
</AssistantContainer>
)}
<Tabs
tab={tab}
activeAssistant={activeAssistant}
activeTopic={activeTopic}
setActiveAssistant={setActiveAssistant}
setActiveTopic={setActiveTopic}
/>
<UserMenu>
<UserMenuLeft onClick={() => UserPopup.show()}>
{isEmoji(avatar) ? (
<EmojiAvatar className="sidebar-avatar" size={31} fontSize={18}>
{avatar}
</EmojiAvatar>
) : (
<AvatarImg src={avatar || UserAvatar} draggable={false} className="nodrag" />
)}
<UserMenuText>{userName}</UserMenuText>
</UserMenuLeft>
<Tooltip title={t('settings.title')} mouseEnterDelay={0.8} placement="right">
<Icon theme={theme} onClick={() => navigate('/settings/provider')} className="settings-icon">
<Settings size={18} className="icon" />
</Icon>
</Tooltip>
</UserMenu>
</Container>
)
}
export const ThemeIcon = () => {
const { t } = useTranslation()
const { theme, settedTheme, toggleTheme } = useTheme()
const onChageTheme = (e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
toggleTheme()
}
return (
<Tooltip
title={t('settings.theme.title') + ': ' + t(`settings.theme.${settedTheme}`)}
mouseEnterDelay={0.8}
placement="right">
<Icon theme={theme} onClick={onChageTheme}>
{settedTheme === ThemeMode.dark ? (
<Moon size={18} className="icon" />
) : settedTheme === ThemeMode.light ? (
<Sun size={18} className="icon" />
) : (
<SunMoon size={18} className="icon" />
)}
</Icon>
</Tooltip>
)
}
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

View File

@ -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;
`

View File

@ -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 <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()
}
]
return (
<MainMenuItem key={app.id} onClick={() => handleOnClick(app)}>
<MainMenuItemLeft>
<MainMenuItemIcon>
<MinAppIcon size={22} app={app} style={{ borderRadius: 6 }} sidebar />
</MainMenuItemIcon>
<MainMenuItemText>{app.name}</MainMenuItemText>
</MainMenuItemLeft>
<MainMenuItemRight style={{ marginRight: 4 }}>
<Dropdown menu={{ items: menuItems }} trigger={['contextMenu']} overlayStyle={{ zIndex: 10000 }}>
<IndicatorLight color="var(--color-primary)" shadow={false} animation={false} size={5} />
</Dropdown>
</MainMenuItemRight>
</MainMenuItem>
)
})}
{isEmpty(openedKeepAliveMinapps) && (
<Center>
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
</Center>
)}
</Menus>
</TabsWrapper>
<Divider />
</TabsContainer>
)
}
const Divider = styled.div`
width: 100%;
height: 1px;
background-color: var(--color-border);
margin: 5px 0;
opacity: 0.5;
`
export default OpenedMinapps

View File

@ -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 (
<div style={{ marginBottom: -10 }}>
<Divider style={{ marginBottom: 5, marginTop: 5 }} />
<DragableList list={pinned} onUpdate={updatePinnedMinapps} listStyle={{ margin: '5px 0', marginBottom: 0 }}>
{(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 (
<Dropdown menu={{ items: menuItems }} trigger={['contextMenu']} overlayStyle={{ zIndex: 10000 }}>
<MainMenuItem key={app.id} onClick={() => openMinappKeepAlive(app)}>
<MainMenuItemLeft>
<MainMenuItemIcon>
<MinAppIcon size={22} app={app} style={{ borderRadius: 6 }} sidebar />
</MainMenuItemIcon>
<MainMenuItemText>{app.name}</MainMenuItemText>
</MainMenuItemLeft>
</MainMenuItem>
</Dropdown>
)
}}
</DragableList>
{isEmpty(openedKeepAliveMinapps) && <Divider style={{ marginBottom: 5, marginTop: 5 }} />}
</div>
)
}
const Divider = styled.div`
width: 100%;
height: 1px;
background-color: var(--color-border);
opacity: 0.5;
`
export default PinnedApps

View File

@ -27,7 +27,7 @@ const CodeBlock: React.FC<Props> = ({ children, className, id, onSave }) => {
{children} {children}
</CodeBlockView> </CodeBlockView>
) : ( ) : (
<code className={className} style={{ textWrap: 'wrap' }}> <code className={className} style={{ textWrap: 'wrap', fontSize: '95%', padding: '2px 4px' }}>
{children} {children}
</code> </code>
) )

View File

@ -93,15 +93,19 @@ const Markdown: FC<Props> = ({ block }) => {
} as Partial<Components> } as Partial<Components>
}, [onSaveCodeBlock]) }, [onSaveCodeBlock])
if (messageContent.includes('<style>')) {
components.style = MarkdownShadowDOMRenderer as any
}
const urlTransform = useCallback((value: string) => { const urlTransform = useCallback((value: string) => {
if (value.startsWith('data:image/png') || value.startsWith('data:image/jpeg')) return value if (value.startsWith('data:image/png') || value.startsWith('data:image/jpeg')) return value
return defaultUrlTransform(value) return defaultUrlTransform(value)
}, []) }, [])
// if (role === 'user' && !renderInputMessageAsMarkdown) {
// return <p style={{ marginBottom: 5, whiteSpace: 'pre-wrap' }}>{messageContent}</p>
// }
if (messageContent.includes('<style>')) {
components.style = MarkdownShadowDOMRenderer as any
}
return ( return (
<ReactMarkdown <ReactMarkdown
rehypePlugins={rehypePlugins} rehypePlugins={rehypePlugins}

View File

@ -31,7 +31,7 @@ const MessageErrorInfo: React.FC<{ block: ErrorMessageBlock }> = ({ block }) =>
} }
const Alert = styled(AntdAlert)` const Alert = styled(AntdAlert)`
margin: 0.5rem 0; margin: 15px 0 8px;
padding: 10px; padding: 10px;
font-size: 12px; font-size: 12px;
` `

View File

@ -21,7 +21,7 @@ const ImageBlock: React.FC<Props> = ({ block }) => {
<ImageViewer <ImageViewer
src={src} src={src}
key={`image-${index}`} key={`image-${index}`}
style={{ maxWidth: 500, maxHeight: 500, padding: 5, borderRadius: 8 }} style={{ maxWidth: 500, maxHeight: 'min(500px, 55vh)', borderRadius: 8 }}
/> />
))} ))}
</Container> </Container>

View File

@ -151,7 +151,7 @@ const MainTextBlock: React.FC<Props> = ({ block, citationBlockId, role, mentions
</Flex> </Flex>
)} )}
{role === 'user' && !renderInputMessageAsMarkdown ? ( {role === 'user' && !renderInputMessageAsMarkdown ? (
<p className="markdown" style={{ whiteSpace: 'pre-wrap' }}> <p className="markdown" style={{ marginBottom: 5, whiteSpace: 'pre-wrap' }}>
{block.content} {block.content}
</p> </p>
) : ( ) : (

View File

@ -42,7 +42,6 @@ const blockWrapperVariants = {
const AnimatedBlockWrapper: React.FC<AnimatedBlockWrapperProps> = ({ children, enableAnimation }) => { const AnimatedBlockWrapper: React.FC<AnimatedBlockWrapperProps> = ({ children, enableAnimation }) => {
return ( return (
<motion.div <motion.div
className="block-wrapper"
variants={blockWrapperVariants} variants={blockWrapperVariants}
initial={enableAnimation ? 'hidden' : 'static'} initial={enableAnimation ? 'hidden' : 'static'}
animate={enableAnimation ? 'visible' : 'static'}> animate={enableAnimation ? 'visible' : 'static'}>
@ -86,7 +85,7 @@ const MessageBlockRenderer: React.FC<Props> = ({ blocks, message }) => {
const groupKey = block.map((imageBlock) => imageBlock.id).join('-') const groupKey = block.map((imageBlock) => imageBlock.id).join('-')
return ( return (
<AnimatedBlockWrapper key={groupKey} enableAnimation={message.status.includes('ing')}> <AnimatedBlockWrapper key={groupKey} enableAnimation={message.status.includes('ing')}>
<ImageBlockGroup> <ImageBlockGroup $columns={block.length}>
{block.map((imageBlock) => ( {block.map((imageBlock) => (
<ImageBlock key={imageBlock.id} block={imageBlock as ImageMessageBlock} /> <ImageBlock key={imageBlock.id} block={imageBlock as ImageMessageBlock} />
))} ))}
@ -162,9 +161,9 @@ const MessageBlockRenderer: React.FC<Props> = ({ blocks, message }) => {
export default React.memo(MessageBlockRenderer) export default React.memo(MessageBlockRenderer)
const ImageBlockGroup = styled.div` const ImageBlockGroup = styled.div<{ $columns: number }>`
display: grid; display: grid;
grid-template-columns: repeat(3, minmax(200px, 1fr)); grid-template-columns: repeat(${({ $columns }) => Math.min(3, $columns)}, minmax(200px, 1fr));
gap: 8px; gap: 8px;
width: 100%; width: 100%;
max-width: 960px; max-width: 960px;

View File

@ -6,7 +6,6 @@ import {
VerticalAlignBottomOutlined, VerticalAlignBottomOutlined,
VerticalAlignTopOutlined VerticalAlignTopOutlined
} from '@ant-design/icons' } from '@ant-design/icons'
import { useSettings } from '@renderer/hooks/useSettings'
import { RootState } from '@renderer/store' import { RootState } from '@renderer/store'
// import { selectCurrentTopicId } from '@renderer/store/newMessage' // import { selectCurrentTopicId } from '@renderer/store/newMessage'
import { Button, Drawer, Tooltip } from 'antd' import { Button, Drawer, Tooltip } from 'antd'
@ -43,8 +42,6 @@ const ChatNavigation: FC<ChatNavigationProps> = ({ containerId }) => {
const [manuallyClosedUntil, setManuallyClosedUntil] = useState<number | null>(null) const [manuallyClosedUntil, setManuallyClosedUntil] = useState<number | null>(null)
const currentTopicId = useSelector((state: RootState) => state.messages.currentTopicId) const currentTopicId = useSelector((state: RootState) => state.messages.currentTopicId)
const lastMoveTime = useRef(0) const lastMoveTime = useRef(0)
const { topicPosition, showTopics } = useSettings()
const showRightTopics = topicPosition === 'right' && showTopics
// Reset hide timer and make buttons visible // Reset hide timer and make buttons visible
const resetHideTimer = useCallback(() => { const resetHideTimer = useCallback(() => {
@ -273,14 +270,7 @@ const ChatNavigation: FC<ChatNavigationProps> = ({ containerId }) => {
// Calculate if the mouse is in the trigger area // Calculate if the mouse is in the trigger area
const triggerWidth = 60 // Same as the width in styled component const triggerWidth = 60 // Same as the width in styled component
// Safe way to calculate position when using calc expressions const rightPosition = window.innerWidth - triggerWidth
let rightOffset = RIGHT_GAP // Default right offset
if (showRightTopics) {
// When topics are shown on right, we need to account for topic list width
rightOffset += 275 // --topic-list-width
}
const rightPosition = window.innerWidth - rightOffset - triggerWidth
const topPosition = window.innerHeight * 0.35 // 35% from top const topPosition = window.innerHeight * 0.35 // 35% from top
const height = window.innerHeight * 0.3 // 30% of window height const height = window.innerHeight * 0.3 // 30% of window height
@ -325,16 +315,7 @@ const ChatNavigation: FC<ChatNavigationProps> = ({ containerId }) => {
clearTimeout(hideTimer) clearTimeout(hideTimer)
} }
} }
}, [ }, [containerId, hideTimer, resetHideTimer, isNearButtons, handleMouseEnter, handleMouseLeave, manuallyClosedUntil])
containerId,
hideTimer,
resetHideTimer,
isNearButtons,
handleMouseEnter,
handleMouseLeave,
showRightTopics,
manuallyClosedUntil
])
return ( return (
<> <>

View File

@ -21,6 +21,7 @@ import MessageEditor from './MessageEditor'
import MessageErrorBoundary from './MessageErrorBoundary' import MessageErrorBoundary from './MessageErrorBoundary'
import MessageHeader from './MessageHeader' import MessageHeader from './MessageHeader'
import MessageMenubar from './MessageMenubar' import MessageMenubar from './MessageMenubar'
import MessageTokens from './MessageTokens'
interface Props { interface Props {
message: Message message: Message
@ -98,7 +99,7 @@ const MessageItem: FC<Props> = ({
const isAssistantMessage = message.role === 'assistant' const isAssistantMessage = message.role === 'assistant'
const showMenubar = !hideMenuBar && !isStreaming && !message.status.includes('ing') && !isEditing const showMenubar = !hideMenuBar && !isStreaming && !message.status.includes('ing') && !isEditing
const messageBorder = !isBubbleStyle && showMessageDivider ? '1px dotted var(--color-border)' : 'none' const messageBorder = showMessageDivider ? undefined : 'none'
const messageBackground = getMessageBackground(isBubbleStyle, isAssistantMessage) const messageBackground = getMessageBackground(isBubbleStyle, isAssistantMessage)
const messageHighlightHandler = useCallback((highlight: boolean = true) => { const messageHighlightHandler = useCallback((highlight: boolean = true) => {
@ -129,6 +130,22 @@ const MessageItem: FC<Props> = ({
) )
} }
if (isEditing) {
return (
<MessageContainer style={{ paddingTop: 15 }}>
<MessageHeader message={message} assistant={assistant} model={model} key={getModelUniqId(model)} />
<div style={{ paddingLeft: messageStyle === 'plain' ? 46 : undefined }}>
<MessageEditor
message={message}
onSave={handleEditSave}
onResend={handleEditResend}
onCancel={handleEditCancel}
/>
</div>
</MessageContainer>
)
}
return ( return (
<MessageContainer <MessageContainer
key={message.id} key={message.id}
@ -138,100 +155,35 @@ const MessageItem: FC<Props> = ({
'message-user': !isAssistantMessage 'message-user': !isAssistantMessage
})} })}
ref={messageContainerRef} ref={messageContainerRef}
style={{ style={{ ...style, alignItems: isBubbleStyle ? (isAssistantMessage ? undefined : 'end') : undefined }}>
...style, <ContextMenu>
justifyContent: isBubbleStyle ? (isAssistantMessage ? 'flex-start' : 'flex-end') : undefined, <MessageHeader message={message} assistant={assistant} model={model} key={getModelUniqId(model)} />
flex: isBubbleStyle ? undefined : 1 <MessageContentContainer
}}> className={
{isEditing && ( message.role === 'user'
<ContextMenu ? 'message-content-container message-content-container-user'
: message.role === 'assistant'
? 'message-content-container message-content-container-assistant'
: 'message-content-container'
}
style={{ style={{
display: 'flex', fontFamily: messageFont === 'serif' ? 'var(--font-family-serif)' : 'var(--font-family)',
flexDirection: 'column', fontSize,
alignSelf: isAssistantMessage ? 'flex-start' : 'flex-end', background: messageBackground,
width: isBubbleStyle ? '70%' : '100%' overflowY: 'visible',
maxWidth: narrowMode ? 760 : undefined
}}> }}>
<MessageHeader <MessageErrorBoundary>
message={message} <MessageContent message={message} />
assistant={assistant} </MessageErrorBoundary>
model={model} {showMenubar && (
key={getModelUniqId(model)}
index={index}
/>
<div style={{ paddingLeft: messageStyle === 'plain' ? 46 : undefined }}>
<MessageEditor
message={message}
onSave={handleEditSave}
onResend={handleEditResend}
onCancel={handleEditCancel}
/>
</div>
</ContextMenu>
)}
{!isEditing && (
<ContextMenu
style={{
display: 'flex',
flexDirection: 'column',
alignSelf: isAssistantMessage ? 'flex-start' : 'flex-end',
flex: 1,
maxWidth: '100%'
}}>
<MessageHeader
message={message}
assistant={assistant}
model={model}
key={getModelUniqId(model)}
index={index}
/>
<MessageContentContainer
className={
message.role === 'user'
? 'message-content-container message-content-container-user'
: message.role === 'assistant'
? 'message-content-container message-content-container-assistant'
: 'message-content-container'
}
style={{
fontFamily: messageFont === 'serif' ? 'var(--font-family-serif)' : 'var(--font-family)',
fontSize,
background: messageBackground,
overflowY: 'visible',
maxWidth: narrowMode ? 760 : undefined,
alignSelf: isBubbleStyle ? (isAssistantMessage ? 'start' : 'end') : undefined
}}>
<MessageErrorBoundary>
<MessageContent message={message} />
</MessageErrorBoundary>
{showMenubar && !isBubbleStyle && (
<MessageFooter
className="MessageFooter"
style={{
borderTop: messageBorder,
flexDirection: !isLastMessage ? 'row-reverse' : undefined
}}>
<MessageMenubar
message={message}
assistant={assistant}
model={model}
index={index}
topic={topic}
isLastMessage={isLastMessage}
isAssistantMessage={isAssistantMessage}
isGrouped={isGrouped}
messageContainerRef={messageContainerRef as React.RefObject<HTMLDivElement>}
setModel={setModel}
/>
</MessageFooter>
)}
</MessageContentContainer>
{showMenubar && isBubbleStyle && (
<MessageFooter <MessageFooter
className="MessageFooter" className="MessageFooter"
style={{ style={{
borderTop: messageBorder, border: messageBorder,
flexDirection: !isAssistantMessage ? 'row-reverse' : undefined flexDirection: isLastMessage || isBubbleStyle ? 'row-reverse' : undefined
}}> }}>
<MessageTokens message={message} isLastMessage={isLastMessage} />
<MessageMenubar <MessageMenubar
message={message} message={message}
assistant={assistant} assistant={assistant}
@ -246,8 +198,8 @@ const MessageItem: FC<Props> = ({
/> />
</MessageFooter> </MessageFooter>
)} )}
</ContextMenu> </MessageContentContainer>
)} </ContextMenu>
</MessageContainer> </MessageContainer>
) )
} }
@ -262,10 +214,10 @@ const getMessageBackground = (isBubbleStyle: boolean, isAssistantMessage: boolea
const MessageContainer = styled.div` const MessageContainer = styled.div`
display: flex; display: flex;
width: 100%; flex-direction: column;
position: relative; position: relative;
transition: background-color 0.3s ease; transition: background-color 0.3s ease;
padding: 0 20px; padding: 0 24px;
transform: translateZ(0); transform: translateZ(0);
will-change: transform; will-change: transform;
&.message-highlight { &.message-highlight {
@ -305,12 +257,12 @@ const MessageFooter = styled.div`
align-items: center; align-items: center;
padding: 2px 0; padding: 2px 0;
margin-top: 2px; margin-top: 2px;
border-top: 0.5px dotted var(--color-border);
gap: 20px; gap: 20px;
` `
const NewContextMessage = styled.div` const NewContextMessage = styled.div`
cursor: pointer; cursor: pointer;
flex: 1;
` `
export default memo(MessageItem) export default memo(MessageItem)

View File

@ -1,4 +1,3 @@
import { DownOutlined } from '@ant-design/icons'
import EmojiAvatar from '@renderer/components/Avatar/EmojiAvatar' import EmojiAvatar from '@renderer/components/Avatar/EmojiAvatar'
import { APP_NAME, AppLogo, isLocalAi } from '@renderer/config/env' import { APP_NAME, AppLogo, isLocalAi } from '@renderer/config/env'
import { getModelLogo } from '@renderer/config/models' import { getModelLogo } from '@renderer/config/models'
@ -14,6 +13,7 @@ import type { Message } from '@renderer/types/newMessage'
import { isEmoji, removeLeadingEmoji } from '@renderer/utils' import { isEmoji, removeLeadingEmoji } from '@renderer/utils'
import { getMainTextContent } from '@renderer/utils/messageUtils/find' import { getMainTextContent } from '@renderer/utils/messageUtils/find'
import { Avatar } from 'antd' import { Avatar } from 'antd'
import { CircleChevronDown } from 'lucide-react'
import { type FC, useCallback, useEffect, useRef, useState } from 'react' import { type FC, useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
@ -183,16 +183,9 @@ const MessageAnchorLine: FC<MessageLineProps> = ({ messages }) => {
opacity: mouseY ? 0.5 + calculateValueByDistance('bottom-anchor', 1) : 0.6 opacity: mouseY ? 0.5 + calculateValueByDistance('bottom-anchor', 1) : 0.6
}} }}
onClick={scrollToBottom}> onClick={scrollToBottom}>
<MessageItemContainer <CircleChevronDown
style={{ transform: `scale(${1 + calculateValueByDistance('bottom-anchor', 1)})` }}></MessageItemContainer>
<Avatar
icon={<DownOutlined style={{ color: theme === 'dark' ? 'var(--color-text)' : 'var(--color-primary)' }} />}
size={10 + calculateValueByDistance('bottom-anchor', 20)} size={10 + calculateValueByDistance('bottom-anchor', 20)}
style={{ style={{ color: theme === 'dark' ? 'var(--color-text)' : 'var(--color-primary)' }}
backgroundColor: theme === 'dark' ? 'var(--color-background-soft)' : 'var(--color-primary-light)',
border: `1px solid ${theme === 'dark' ? 'var(--color-border-soft)' : 'var(--color-primary-soft)'}`,
opacity: 0.9
}}
/> />
</MessageItem> </MessageItem>
{messages.map((message, index) => { {messages.map((message, index) => {
@ -203,6 +196,8 @@ const MessageAnchorLine: FC<MessageLineProps> = ({ messages }) => {
const username = removeLeadingEmoji(getUserName(message)) const username = removeLeadingEmoji(getUserName(message))
const content = getMainTextContent(message) const content = getMainTextContent(message)
if (message.type === 'clear') return null
return ( return (
<MessageItem <MessageItem
key={message.id} key={message.id}
@ -262,7 +257,6 @@ const MessageItemContainer = styled.div`
justify-content: space-between; justify-content: space-between;
text-align: right; text-align: right;
gap: 4px; gap: 4px;
text-shadow: 0 0 2px rgba(255, 255, 255, 0.5);
opacity: 0; opacity: 0;
transform-origin: right center; transform-origin: right center;
` `

View File

@ -1,6 +1,21 @@
import {
FileExcelFilled,
FileImageFilled,
FileMarkdownFilled,
FilePdfFilled,
FilePptFilled,
FileTextFilled,
FileUnknownFilled,
FileWordFilled,
FileZipFilled,
FolderOpenFilled,
GlobalOutlined,
LinkOutlined
} from '@ant-design/icons'
import CustomTag from '@renderer/components/CustomTag'
import FileManager from '@renderer/services/FileManager' import FileManager from '@renderer/services/FileManager'
import { FileType } from '@renderer/types'
import type { FileMessageBlock } from '@renderer/types/newMessage' import type { FileMessageBlock } from '@renderer/types/newMessage'
import { Upload } from 'antd'
import { FC } from 'react' import { FC } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
@ -8,76 +23,88 @@ interface Props {
block: FileMessageBlock block: FileMessageBlock
} }
const StyledUpload = styled(Upload)`
.ant-upload-list-item-name {
max-width: 220px;
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: bottom;
}
`
const MessageAttachments: FC<Props> = ({ block }) => { const MessageAttachments: FC<Props> = ({ block }) => {
// const handleCopyImage = async (image: FileType) => {
// const data = await FileManager.readFile(image)
// const blob = new Blob([data], { type: 'image/png' })
// const item = new ClipboardItem({ [blob.type]: blob })
// await navigator.clipboard.write([item])
// }
if (!block.file) { if (!block.file) {
return null return null
} }
// 由图片块代替
// if (block.file.type === FileTypes.IMAGE) { const MAX_FILENAME_DISPLAY_LENGTH = 20
// return ( function truncateFileName(name: string, maxLength: number = MAX_FILENAME_DISPLAY_LENGTH) {
// <Container style={{ marginBottom: 8 }}> if (name.length <= maxLength) return name
// <Image return name.slice(0, maxLength - 3) + '...'
// src={FileManager.getFileUrl(block.file)} }
// key={block.file.id}
// width="33%" const getFileIcon = (type?: string) => {
// preview={{ if (!type) return <FileUnknownFilled />
// toolbarRender: (
// _, const ext = type.toLowerCase()
// {
// transform: { scale }, if (['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'].includes(ext)) {
// actions: { onFlipY, onFlipX, onRotateLeft, onRotateRight, onZoomOut, onZoomIn, onReset } return <FileImageFilled />
// } }
// ) => (
// <ToobarWrapper size={12} className="toolbar-wrapper"> if (['.doc', '.docx'].includes(ext)) {
// <SwapOutlined rotate={90} onClick={onFlipY} /> return <FileWordFilled />
// <SwapOutlined onClick={onFlipX} /> }
// <RotateLeftOutlined onClick={onRotateLeft} /> if (['.xls', '.xlsx'].includes(ext)) {
// <RotateRightOutlined onClick={onRotateRight} /> return <FileExcelFilled />
// <ZoomOutOutlined disabled={scale === 1} onClick={onZoomOut} /> }
// <ZoomInOutlined disabled={scale === 50} onClick={onZoomIn} /> if (['.ppt', '.pptx'].includes(ext)) {
// <UndoOutlined onClick={onReset} /> return <FilePptFilled />
// <CopyOutlined onClick={() => handleCopyImage(block.file)} /> }
// <DownloadOutlined onClick={() => download(FileManager.getFileUrl(block.file))} /> if (ext === '.pdf') {
// </ToobarWrapper> return <FilePdfFilled />
// ) }
// }} if (['.md', '.markdown'].includes(ext)) {
// /> return <FileMarkdownFilled />
// </Container> }
// )
// } if (['.zip', '.rar', '.7z', '.tar', '.gz'].includes(ext)) {
return <FileZipFilled />
}
if (['.txt', '.json', '.log', '.yml', '.yaml', '.xml', '.csv'].includes(ext)) {
return <FileTextFilled />
}
if (['.url'].includes(ext)) {
return <LinkOutlined />
}
if (['.sitemap'].includes(ext)) {
return <GlobalOutlined />
}
if (['.folder'].includes(ext)) {
return <FolderOpenFilled />
}
return <FileUnknownFilled />
}
const FileNameRender: FC<{ file: FileType }> = ({ file }) => {
const fullName = FileManager.formatFileName(file)
const displayName = truncateFileName(fullName)
return (
<FileName
onClick={() => {
const path = FileManager.getSafePath(file)
if (path) {
window.api.file.openPath(path)
}
}}
title={fullName}>
{displayName}
</FileName>
)
}
return ( return (
<Container style={{ marginTop: 2, marginBottom: 8 }} className="message-attachments"> <Container style={{ marginTop: 2, marginBottom: 8 }} className="message-attachments">
<StyledUpload <CustomTag key={block.file.id} icon={getFileIcon(block.file.ext)} color="#37a5aa">
listType="text" <FileNameRender file={block.file} />
disabled </CustomTag>
fileList={[
{
uid: block.file.id,
url: 'file://' + FileManager.getSafePath(block.file),
status: 'done' as const,
name: FileManager.formatFileName(block.file)
}
]}
/>
</Container> </Container>
) )
} }
@ -89,23 +116,11 @@ const Container = styled.div`
margin-top: 8px; margin-top: 8px;
` `
// const Image = styled(AntdImage)` const FileName = styled.span`
// border-radius: 10px; cursor: pointer;
// ` &:hover {
text-decoration: underline;
// const ToobarWrapper = styled(Space)` }
// padding: 0px 24px; `
// color: #fff;
// font-size: 20px;
// background-color: rgba(0, 0, 0, 0.1);
// border-radius: 100px;
// .anticon {
// padding: 12px;
// cursor: pointer;
// }
// .anticon:hover {
// opacity: 0.3;
// }
// `
export default MessageAttachments export default MessageAttachments

View File

@ -14,7 +14,7 @@ const MessageContent: React.FC<Props> = ({ message }) => {
return ( return (
<> <>
{!isEmpty(message.mentions) && ( {!isEmpty(message.mentions) && (
<Flex gap="8px" wrap> <Flex gap="8px" wrap style={{ marginBottom: 10 }}>
{message.mentions?.map((model) => <MentionTag key={getModelUniqId(model)}>{'@' + model.name}</MentionTag>)} {message.mentions?.map((model) => <MentionTag key={getModelUniqId(model)}>{'@' + model.name}</MentionTag>)}
</Flex> </Flex>
)} )}

View File

@ -261,7 +261,7 @@ const EditorContainer = styled.div`
padding: 8px 0; padding: 8px 0;
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
transition: all 0.2s ease; transition: all 0.2s ease;
border-radius: 15px; border-radius: var(--list-item-border-radius);
margin-top: 5px; margin-top: 5px;
background-color: var(--color-background-opacity); background-color: var(--color-background-opacity);
width: 100%; width: 100%;

View File

@ -168,7 +168,9 @@ const MessageGroup = ({ messages, topic, registerMessageElement }: Props) => {
topic, topic,
index: message.index, index: message.index,
style: { style: {
paddingTop: isGrouped && ['horizontal', 'grid'].includes(multiModelMessageStyle) ? 0 : 15 ...(isGrouped && ['horizontal', 'grid'].includes(multiModelMessageStyle)
? { padding: 0 }
: { paddingTop: 15 })
} }
} }
@ -180,8 +182,7 @@ const MessageGroup = ({ messages, topic, registerMessageElement }: Props) => {
$isGrouped={isGrouped} $isGrouped={isGrouped}
key={message.id} key={message.id}
className={classNames({ className={classNames({
// 加个卡片布局 'group-message-wrapper': message.role === 'assistant' && isHorizontal && isGrouped,
'group-message-wrapper': message.role === 'assistant' && (isHorizontal || isGrid) && isGrouped,
[multiModelMessageStyle]: isGrouped, [multiModelMessageStyle]: isGrouped,
selected: message.id === selectedMessageId selected: message.id === selectedMessageId
})}> })}>
@ -203,7 +204,10 @@ const MessageGroup = ({ messages, topic, registerMessageElement }: Props) => {
</MessageWrapper> </MessageWrapper>
} }
trigger={gridPopoverTrigger} trigger={gridPopoverTrigger}
styles={{ root: { maxWidth: '60vw', minWidth: '550px', overflowY: 'auto', zIndex: 1000 } }}> styles={{
root: { maxWidth: '60vw', minWidth: '550px', overflowY: 'auto', zIndex: 1000 },
body: { padding: 2 }
}}>
<div style={{ cursor: 'pointer' }}>{messageContent}</div> <div style={{ cursor: 'pointer' }}>{messageContent}</div>
</Popover> </Popover>
) )
@ -260,7 +264,7 @@ const GroupContainer = styled.div<{ $isGrouped: boolean; $layout: MultiModelMess
padding-top: ${({ $isGrouped, $layout }) => ($isGrouped && 'horizontal' === $layout ? '15px' : '0')}; padding-top: ${({ $isGrouped, $layout }) => ($isGrouped && 'horizontal' === $layout ? '15px' : '0')};
&.group-container.horizontal, &.group-container.horizontal,
&.group-container.grid { &.group-container.grid {
padding: 0 20px; padding: 0 24px;
.message { .message {
padding: 0; padding: 0;
} }
@ -316,13 +320,12 @@ interface MessageWrapperProps {
const MessageWrapper = styled(Scrollbar)<MessageWrapperProps>` const MessageWrapper = styled(Scrollbar)<MessageWrapperProps>`
width: 100%; width: 100%;
display: flex;
&.horizontal { &.horizontal {
display: inline-block; height: 100%;
} }
&.grid { &.grid {
display: inline-block; height: 100%;
} }
&.fold { &.fold {
display: none; display: none;
@ -335,10 +338,9 @@ const MessageWrapper = styled(Scrollbar)<MessageWrapperProps>`
if ($layout === 'horizontal' && $isGrouped) { if ($layout === 'horizontal' && $isGrouped) {
return css` return css`
border: 0.5px solid var(--color-border); border: 0.5px solid var(--color-border);
padding: 10px; padding: 10px 10px 0 10px;
border-radius: 6px; border-radius: 6px;
max-height: 600px; max-height: 600px;
margin-bottom: 10px;
` `
} }
return '' return ''

View File

@ -7,6 +7,7 @@ import {
} from '@ant-design/icons' } from '@ant-design/icons'
import { HStack } from '@renderer/components/Layout' import { HStack } from '@renderer/components/Layout'
import { useMessageOperations } from '@renderer/hooks/useMessageOperations' import { useMessageOperations } from '@renderer/hooks/useMessageOperations'
import { useSettings } from '@renderer/hooks/useSettings'
import { MultiModelMessageStyle } from '@renderer/store/settings' import { MultiModelMessageStyle } from '@renderer/store/settings'
import type { Topic } from '@renderer/types' import type { Topic } from '@renderer/types'
import type { Message } from '@renderer/types/newMessage' import type { Message } from '@renderer/types/newMessage'
@ -37,6 +38,7 @@ const MessageGroupMenuBar: FC<Props> = ({
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const { deleteGroupMessages } = useMessageOperations(topic) const { deleteGroupMessages } = useMessageOperations(topic)
const { showAssistants } = useSettings()
const handleDeleteGroup = async () => { const handleDeleteGroup = async () => {
const askId = messages[0]?.askId const askId = messages[0]?.askId
@ -53,8 +55,12 @@ const MessageGroupMenuBar: FC<Props> = ({
onOk: () => deleteGroupMessages(askId) onOk: () => deleteGroupMessages(askId)
}) })
} }
return ( return (
<GroupMenuBar $layout={multiModelMessageStyle} className="group-menu-bar"> <GroupMenuBar
$layout={multiModelMessageStyle}
className="group-menu-bar"
style={{ maxWidth: showAssistants ? 'calc(100vw - var(--assistants-width))' : '100vw' }}>
<HStack style={{ alignItems: 'center', flex: 1, overflow: 'hidden' }}> <HStack style={{ alignItems: 'center', flex: 1, overflow: 'hidden' }}>
<LayoutContainer> <LayoutContainer>
{['fold', 'vertical', 'horizontal', 'grid'].map((layout) => ( {['fold', 'vertical', 'horizontal', 'grid'].map((layout) => (
@ -104,7 +110,7 @@ const GroupMenuBar = styled.div<{ $layout: MultiModelMessageStyle }>`
margin: 0 20px; margin: 0 20px;
padding: 6px 10px; padding: 6px 10px;
border-radius: 6px; border-radius: 6px;
margin-top: 10px; margin-top: 6px;
justify-content: space-between; justify-content: space-between;
overflow: hidden; overflow: hidden;
border: 0.5px solid var(--color-border); border: 0.5px solid var(--color-border);

View File

@ -17,13 +17,10 @@ import { CSSProperties, FC, memo, useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
import MessageTokens from './MessageTokens'
interface Props { interface Props {
message: Message message: Message
assistant: Assistant assistant: Assistant
model?: Model model?: Model
index: number | undefined
} }
const getAvatarSource = (isLocalAi: boolean, modelId: string | undefined) => { const getAvatarSource = (isLocalAi: boolean, modelId: string | undefined) => {
@ -31,7 +28,7 @@ const getAvatarSource = (isLocalAi: boolean, modelId: string | undefined) => {
return modelId ? getModelLogo(modelId) : undefined return modelId ? getModelLogo(modelId) : undefined
} }
const MessageHeader: FC<Props> = memo(({ assistant, model, message, index }) => { const MessageHeader: FC<Props> = memo(({ assistant, model, message }) => {
const avatar = useAvatar() const avatar = useAvatar()
const { theme } = useTheme() const { theme } = useTheme()
const { userName, sidebarIcons } = useSettings() const { userName, sidebarIcons } = useSettings()
@ -55,11 +52,9 @@ const MessageHeader: FC<Props> = memo(({ assistant, model, message, index }) =>
const isAssistantMessage = message.role === 'assistant' const isAssistantMessage = message.role === 'assistant'
const showMinappIcon = sidebarIcons.visible.includes('minapp') const showMinappIcon = sidebarIcons.visible.includes('minapp')
const { showTokens } = useSettings()
const avatarName = useMemo(() => firstLetter(assistant?.name).toUpperCase(), [assistant?.name]) const avatarName = useMemo(() => firstLetter(assistant?.name).toUpperCase(), [assistant?.name])
const username = useMemo(() => removeLeadingEmoji(getUserName()), [getUserName]) const username = useMemo(() => removeLeadingEmoji(getUserName()), [getUserName])
const isLastMessage = index === 0
const showMiniApp = useCallback(() => { const showMiniApp = useCallback(() => {
showMinappIcon && model?.provider && openMinappById(model.provider) showMinappIcon && model?.provider && openMinappById(model.provider)
@ -116,14 +111,7 @@ const MessageHeader: FC<Props> = memo(({ assistant, model, message, index }) =>
<UserName isBubbleStyle={isBubbleStyle} theme={theme}> <UserName isBubbleStyle={isBubbleStyle} theme={theme}>
{username} {username}
</UserName> </UserName>
<InfoWrap <MessageTime>{dayjs(message?.updatedAt ?? message.createdAt).format('MM/DD HH:mm')}</MessageTime>
style={{
flexDirection: !isAssistantMessage && isBubbleStyle ? 'row-reverse' : undefined
}}>
<MessageTime>{dayjs(message?.updatedAt ?? message.createdAt).format('MM/DD HH:mm')}</MessageTime>
{showTokens && <DividerContainer style={{ color: 'var(--color-text-3)' }}> | </DividerContainer>}
<MessageTokens message={message} isLastMessage={isLastMessage} />
</InfoWrap>
</UserWrap> </UserWrap>
</AvatarWrapper> </AvatarWrapper>
</Container> </Container>
@ -152,19 +140,6 @@ const UserWrap = styled.div`
justify-content: space-between; justify-content: space-between;
` `
const InfoWrap = styled.div`
display: flex;
flex-direction: row;
align-items: center;
gap: 4px;
`
const DividerContainer = styled.div`
font-size: 10px;
color: var(--color-text-3);
margin: 0 2px;
`
const UserName = styled.div<{ isBubbleStyle?: boolean; theme?: string }>` const UserName = styled.div<{ isBubbleStyle?: boolean; theme?: string }>`
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;

View File

@ -1,11 +1,11 @@
import { CheckOutlined, EditOutlined, MenuOutlined, QuestionCircleOutlined, SyncOutlined } from '@ant-design/icons' import { CheckOutlined, EditOutlined, MenuOutlined, QuestionCircleOutlined, SyncOutlined } from '@ant-design/icons'
import MessageSettingsPopup from '@renderer/components/Popups/MessageSettingsPopup'
import ObsidianExportPopup from '@renderer/components/Popups/ObsidianExportPopup' import ObsidianExportPopup from '@renderer/components/Popups/ObsidianExportPopup'
import SelectModelPopup from '@renderer/components/Popups/SelectModelPopup' import SelectModelPopup from '@renderer/components/Popups/SelectModelPopup'
import { TranslateLanguageOptions } from '@renderer/config/translate' import { TranslateLanguageOptions } from '@renderer/config/translate'
import { useMessageEditing } from '@renderer/context/MessageEditingContext' import { useMessageEditing } from '@renderer/context/MessageEditingContext'
import { useChatContext } from '@renderer/hooks/useChatContext' import { useChatContext } from '@renderer/hooks/useChatContext'
import { useMessageOperations, useTopicLoading } from '@renderer/hooks/useMessageOperations' import { useMessageOperations, useTopicLoading } from '@renderer/hooks/useMessageOperations'
import { useMessageStyle } from '@renderer/hooks/useSettings'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { getMessageTitle } from '@renderer/services/MessagesService' import { getMessageTitle } from '@renderer/services/MessagesService'
import { translateText } from '@renderer/services/TranslateService' import { translateText } from '@renderer/services/TranslateService'
@ -29,7 +29,7 @@ import { removeTrailingDoubleSpaces } from '@renderer/utils/markdown'
import { findMainTextBlocks, findTranslationBlocks, getMainTextContent } from '@renderer/utils/messageUtils/find' import { findMainTextBlocks, findTranslationBlocks, getMainTextContent } from '@renderer/utils/messageUtils/find'
import { Dropdown, Popconfirm, Tooltip } from 'antd' import { Dropdown, Popconfirm, Tooltip } from 'antd'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { AtSign, Copy, Languages, Menu, RefreshCw, Save, Share, Split, ThumbsUp, Trash } from 'lucide-react' import { AtSign, Copy, Languages, Menu, RefreshCw, Save, Settings2, Share, Split, ThumbsUp, Trash } from 'lucide-react'
import { FilePenLine } from 'lucide-react' import { FilePenLine } from 'lucide-react'
import { FC, memo, useCallback, useMemo, useState } from 'react' import { FC, memo, useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -68,9 +68,6 @@ const MessageMenubar: FC<Props> = (props) => {
appendAssistantResponse, appendAssistantResponse,
removeMessageBlock removeMessageBlock
} = useMessageOperations(topic) } = useMessageOperations(topic)
const { isBubbleStyle } = useMessageStyle()
const loading = useTopicLoading(topic) const loading = useTopicLoading(topic)
const isUserMessage = message.role === 'user' const isUserMessage = message.role === 'user'
@ -197,6 +194,12 @@ const MessageMenubar: FC<Props> = (props) => {
toggleMultiSelectMode(true) toggleMultiSelectMode(true)
} }
}, },
{
label: t('chat.message.settings'),
key: 'message-settings',
icon: <Settings2 size={16} />,
onClick: () => MessageSettingsPopup.show({ title: t('chat.message.settings') })
},
{ {
label: t('chat.topics.export.title'), label: t('chat.topics.export.title'),
key: 'export', key: 'export',
@ -342,29 +345,24 @@ const MessageMenubar: FC<Props> = (props) => {
return translationBlocks.length > 0 return translationBlocks.length > 0
}, [message]) }, [message])
const softHoverBg = isBubbleStyle && !isLastMessage
return ( return (
<MenusBar className={`menubar ${isLastMessage && 'show'}`}> <MenusBar className={`menubar ${isLastMessage && 'show'}`}>
{message.role === 'user' && ( {message.role === 'user' && (
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}> <Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
<ActionButton <ActionButton className="message-action-button" onClick={() => handleResendUserMessage()}>
className="message-action-button"
onClick={() => handleResendUserMessage()}
$softHoverBg={isBubbleStyle}>
<SyncOutlined /> <SyncOutlined />
</ActionButton> </ActionButton>
</Tooltip> </Tooltip>
)} )}
{message.role === 'user' && ( {message.role === 'user' && (
<Tooltip title={t('common.edit')} mouseEnterDelay={0.8}> <Tooltip title={t('common.edit')} mouseEnterDelay={0.8}>
<ActionButton className="message-action-button" onClick={onEdit} $softHoverBg={softHoverBg}> <ActionButton className="message-action-button" onClick={onEdit}>
<EditOutlined /> <EditOutlined />
</ActionButton> </ActionButton>
</Tooltip> </Tooltip>
)} )}
<Tooltip title={t('common.copy')} mouseEnterDelay={0.8}> <Tooltip title={t('common.copy')} mouseEnterDelay={0.8}>
<ActionButton className="message-action-button" onClick={onCopy} $softHoverBg={softHoverBg}> <ActionButton className="message-action-button" onClick={onCopy}>
{!copied && <Copy size={16} />} {!copied && <Copy size={16} />}
{copied && <CheckOutlined style={{ color: 'var(--color-primary)' }} />} {copied && <CheckOutlined style={{ color: 'var(--color-primary)' }} />}
</ActionButton> </ActionButton>
@ -381,7 +379,7 @@ const MessageMenubar: FC<Props> = (props) => {
mouseEnterDelay={0.8} mouseEnterDelay={0.8}
open={showRegenerateTooltip} open={showRegenerateTooltip}
onOpenChange={setShowRegenerateTooltip}> onOpenChange={setShowRegenerateTooltip}>
<ActionButton className="message-action-button" $softHoverBg={softHoverBg}> <ActionButton className="message-action-button">
<RefreshCw size={16} /> <RefreshCw size={16} />
</ActionButton> </ActionButton>
</Tooltip> </Tooltip>
@ -389,7 +387,7 @@ const MessageMenubar: FC<Props> = (props) => {
)} )}
{isAssistantMessage && ( {isAssistantMessage && (
<Tooltip title={t('message.mention.title')} mouseEnterDelay={0.8}> <Tooltip title={t('message.mention.title')} mouseEnterDelay={0.8}>
<ActionButton className="message-action-button" onClick={onMentionModel} $softHoverBg={softHoverBg}> <ActionButton className="message-action-button" onClick={onMentionModel}>
<AtSign size={16} /> <AtSign size={16} />
</ActionButton> </ActionButton>
</Tooltip> </Tooltip>
@ -459,10 +457,7 @@ const MessageMenubar: FC<Props> = (props) => {
placement="top" placement="top"
arrow> arrow>
<Tooltip title={t('chat.translate')} mouseEnterDelay={1.2}> <Tooltip title={t('chat.translate')} mouseEnterDelay={1.2}>
<ActionButton <ActionButton className="message-action-button" onClick={(e) => e.stopPropagation()}>
className="message-action-button"
onClick={(e) => e.stopPropagation()}
$softHoverBg={softHoverBg}>
<Languages size={16} /> <Languages size={16} />
</ActionButton> </ActionButton>
</Tooltip> </Tooltip>
@ -470,7 +465,7 @@ const MessageMenubar: FC<Props> = (props) => {
)} )}
{isAssistantMessage && isGrouped && ( {isAssistantMessage && isGrouped && (
<Tooltip title={t('chat.message.useful')} mouseEnterDelay={0.8}> <Tooltip title={t('chat.message.useful')} mouseEnterDelay={0.8}>
<ActionButton className="message-action-button" onClick={onUseful} $softHoverBg={softHoverBg}> <ActionButton className="message-action-button" onClick={onUseful}>
{message.useful ? ( {message.useful ? (
<ThumbsUp size={17.5} fill="var(--color-primary)" strokeWidth={0} /> <ThumbsUp size={17.5} fill="var(--color-primary)" strokeWidth={0} />
) : ( ) : (
@ -485,7 +480,7 @@ const MessageMenubar: FC<Props> = (props) => {
icon={<QuestionCircleOutlined style={{ color: 'red' }} />} icon={<QuestionCircleOutlined style={{ color: 'red' }} />}
onOpenChange={(open) => open && setShowDeleteTooltip(false)} onOpenChange={(open) => open && setShowDeleteTooltip(false)}
onConfirm={() => deleteMessage(message.id)}> onConfirm={() => deleteMessage(message.id)}>
<ActionButton className="message-action-button" onClick={(e) => e.stopPropagation()} $softHoverBg={softHoverBg}> <ActionButton className="message-action-button" onClick={(e) => e.stopPropagation()}>
<Tooltip <Tooltip
title={t('common.delete')} title={t('common.delete')}
mouseEnterDelay={1} mouseEnterDelay={1}
@ -501,10 +496,7 @@ const MessageMenubar: FC<Props> = (props) => {
trigger={['click']} trigger={['click']}
placement="topRight" placement="topRight"
arrow> arrow>
<ActionButton <ActionButton className="message-action-button" onClick={(e) => e.stopPropagation()}>
className="message-action-button"
onClick={(e) => e.stopPropagation()}
$softHoverBg={softHoverBg}>
<Menu size={19} /> <Menu size={19} />
</ActionButton> </ActionButton>
</Dropdown> </Dropdown>
@ -521,7 +513,7 @@ const MenusBar = styled.div`
gap: 6px; gap: 6px;
` `
const ActionButton = styled.div<{ $softHoverBg?: boolean }>` const ActionButton = styled.div`
cursor: pointer; cursor: pointer;
border-radius: 8px; border-radius: 8px;
display: flex; display: flex;
@ -532,11 +524,8 @@ const ActionButton = styled.div<{ $softHoverBg?: boolean }>`
height: 30px; height: 30px;
transition: all 0.2s ease; transition: all 0.2s ease;
&:hover { &:hover {
background-color: ${(props) => background-color: var(--color-background-mute);
props.$softHoverBg ? 'var(--color-background-soft)' : 'var(--color-background-mute)'}; .anticon {
color: var(--color-text-1);
.anticon,
.lucide {
color: var(--color-text-1); color: var(--color-text-1);
} }
} }
@ -546,6 +535,9 @@ const ActionButton = styled.div<{ $softHoverBg?: boolean }>`
font-size: 14px; font-size: 14px;
color: var(--color-icon); color: var(--color-icon);
} }
&:hover {
color: var(--color-text-1);
}
.icon-at { .icon-at {
font-size: 16px; font-size: 16px;
} }

View File

@ -1,5 +1,4 @@
// import { useRuntime } from '@renderer/hooks/useRuntime' // import { useRuntime } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import type { Message } from '@renderer/types/newMessage' import type { Message } from '@renderer/types/newMessage'
import { Popover } from 'antd' import { Popover } from 'antd'
@ -12,7 +11,6 @@ interface MessageTokensProps {
} }
const MessgeTokens: React.FC<MessageTokensProps> = ({ message }) => { const MessgeTokens: React.FC<MessageTokensProps> = ({ message }) => {
const { showTokens } = useSettings()
// const { generating } = useRuntime() // const { generating } = useRuntime()
const locateMessage = () => { const locateMessage = () => {
EventEmitter.emit(EVENT_NAMES.LOCATE_MESSAGE + ':' + message.id, false) EventEmitter.emit(EVENT_NAMES.LOCATE_MESSAGE + ':' + message.id, false)
@ -25,7 +23,7 @@ const MessgeTokens: React.FC<MessageTokensProps> = ({ message }) => {
if (message.role === 'user') { if (message.role === 'user') {
return ( return (
<MessageMetadata className="message-tokens" onClick={locateMessage}> <MessageMetadata className="message-tokens" onClick={locateMessage}>
{showTokens && `Tokens: ${message?.usage?.total_tokens}`} Tokens: {message?.usage?.total_tokens}
</MessageMetadata> </MessageMetadata>
) )
} }
@ -56,7 +54,7 @@ const MessgeTokens: React.FC<MessageTokensProps> = ({ message }) => {
<MessageMetadata className="message-tokens" onClick={locateMessage}> <MessageMetadata className="message-tokens" onClick={locateMessage}>
{hasMetrics ? ( {hasMetrics ? (
<Popover content={metrixs} placement="top" trigger="hover" styles={{ root: { fontSize: 11 } }}> <Popover content={metrixs} placement="top" trigger="hover" styles={{ root: { fontSize: 11 } }}>
{showTokens && tokensInfo} {tokensInfo}
</Popover> </Popover>
) : ( ) : (
tokensInfo tokensInfo
@ -69,14 +67,19 @@ const MessgeTokens: React.FC<MessageTokensProps> = ({ message }) => {
} }
const MessageMetadata = styled.div` const MessageMetadata = styled.div`
font-size: 10px; font-size: 11px;
color: var(--color-text-3); color: var(--color-text-2);
user-select: text; user-select: text;
margin: 2px 0;
cursor: pointer; cursor: pointer;
text-align: right; text-align: right;
.tokens span { .tokens {
padding: 0 2px; display: block;
span {
padding: 0 2px;
}
} }
` `

View File

@ -38,7 +38,6 @@ import ChatNavigation from './ChatNavigation'
import MessageAnchorLine from './MessageAnchorLine' import MessageAnchorLine from './MessageAnchorLine'
import MessageGroup from './MessageGroup' import MessageGroup from './MessageGroup'
import NarrowLayout from './NarrowLayout' import NarrowLayout from './NarrowLayout'
import Prompt from './Prompt'
interface MessagesProps { interface MessagesProps {
assistant: Assistant assistant: Assistant
@ -53,7 +52,7 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic, o
`topic-${topic.id}` `topic-${topic.id}`
) )
const { t } = useTranslation() const { t } = useTranslation()
const { showPrompt, messageNavigation } = useSettings() const { messageNavigation } = useSettings()
const { updateTopic, addTopic } = useAssistant(assistant.id) const { updateTopic, addTopic } = useAssistant(assistant.id)
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const [displayMessages, setDisplayMessages] = useState<Message[]>([]) const [displayMessages, setDisplayMessages] = useState<Message[]>([])
@ -271,7 +270,7 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic, o
id="messages" id="messages"
className="messages-container" className="messages-container"
ref={scrollContainerRef} ref={scrollContainerRef}
style={{ position: 'relative', paddingTop: showPrompt ? 10 : 0 }} style={{ position: 'relative' }}
key={assistant.id} key={assistant.id}
onScroll={handleScrollPosition}> onScroll={handleScrollPosition}>
<NarrowLayout style={{ display: 'flex', flexDirection: 'column-reverse' }}> <NarrowLayout style={{ display: 'flex', flexDirection: 'column-reverse' }}>
@ -299,7 +298,6 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic, o
)} )}
</ScrollContainer> </ScrollContainer>
</InfiniteScroll> </InfiniteScroll>
{showPrompt && <Prompt assistant={assistant} key={assistant.prompt} topic={topic} />}
</NarrowLayout> </NarrowLayout>
{messageNavigation === 'anchor' && <MessageAnchorLine messages={displayMessages} />} {messageNavigation === 'anchor' && <MessageAnchorLine messages={displayMessages} />}
{messageNavigation === 'buttons' && <ChatNavigation containerId="messages" />} {messageNavigation === 'buttons' && <ChatNavigation containerId="messages" />}
@ -372,7 +370,6 @@ const MessagesContainer = styled(Scrollbar)<ContainerProps>`
flex-direction: column-reverse; flex-direction: column-reverse;
padding: 10px 0 20px; padding: 10px 0 20px;
overflow-x: hidden; overflow-x: hidden;
background-color: var(--color-background);
z-index: 1; z-index: 1;
margin-right: 2px; margin-right: 2px;
` `

View File

@ -1,231 +0,0 @@
import { Navbar, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar'
import { HStack } from '@renderer/components/Layout'
import FloatingSidebar from '@renderer/components/Popups/FloatingSidebar'
import MinAppsPopover from '@renderer/components/Popups/MinAppsPopover'
import SearchPopup from '@renderer/components/Popups/SearchPopup'
import { isMac } from '@renderer/config/constant'
import { useAssistant } from '@renderer/hooks/useAssistant'
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, useShowTopics } from '@renderer/hooks/useStore'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { useAppDispatch } from '@renderer/store'
import { setNarrowMode } from '@renderer/store/settings'
import { Assistant, Topic } from '@renderer/types'
import { Tooltip } from 'antd'
import { t } from 'i18next'
import { LayoutGrid, MessageSquareDiff, PanelLeftClose, PanelRightClose, Search } from 'lucide-react'
import { FC, useCallback, useState } from 'react'
import styled from 'styled-components'
import SelectModelButton from './components/SelectModelButton'
import UpdateAppButton from './components/UpdateAppButton'
interface Props {
activeAssistant: Assistant
activeTopic: Topic
setActiveTopic: (topic: Topic) => void
setActiveAssistant: (assistant: Assistant) => void
position: 'left' | 'right'
}
const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant, activeTopic, setActiveTopic }) => {
const { assistant } = useAssistant(activeAssistant.id)
const { showAssistants, toggleShowAssistants } = useShowAssistants()
const isFullscreen = useFullscreen()
const { topicPosition, sidebarIcons, narrowMode } = useSettings()
const { showTopics, toggleShowTopics } = useShowTopics()
const dispatch = useAppDispatch()
const [sidebarHideCooldown, setSidebarHideCooldown] = useState(false)
// Function to toggle assistants with cooldown
const handleToggleShowAssistants = useCallback(() => {
if (showAssistants) {
// When hiding sidebar, set cooldown
toggleShowAssistants()
setSidebarHideCooldown(true)
// setTimeout(() => {
// setSidebarHideCooldown(false)
// }, 10000) // 10 seconds cooldown
} else {
// When showing sidebar, no cooldown needed
toggleShowAssistants()
}
}, [showAssistants, toggleShowAssistants])
const handleToggleShowTopics = useCallback(() => {
if (showTopics) {
// When hiding sidebar, set cooldown
toggleShowTopics()
setSidebarHideCooldown(true)
// setTimeout(() => {
// setSidebarHideCooldown(false)
// }, 10000) // 10 seconds cooldown
} else {
// When showing sidebar, no cooldown needed
toggleShowTopics()
}
}, [showTopics, toggleShowTopics])
useShortcut('toggle_show_assistants', handleToggleShowAssistants)
useShortcut('toggle_show_topics', () => {
if (topicPosition === 'right') {
toggleShowTopics()
} else {
EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR)
}
})
useShortcut('search_message', () => {
SearchPopup.show()
})
const handleNarrowModeToggle = async () => {
await modelGenerating()
dispatch(setNarrowMode(!narrowMode))
}
return (
<Navbar className="home-navbar">
{showAssistants && (
<NavbarLeft style={{ justifyContent: 'space-between', borderRight: 'none', padding: 0 }}>
<Tooltip title={t('navbar.hide_sidebar')} mouseEnterDelay={0.8}>
<NavbarIcon onClick={handleToggleShowAssistants} style={{ marginLeft: isMac && !isFullscreen ? 16 : 0 }}>
<PanelLeftClose size={18} />
</NavbarIcon>
</Tooltip>
<Tooltip title={t('settings.shortcuts.new_topic')} mouseEnterDelay={0.8}>
<NavbarIcon onClick={() => EventEmitter.emit(EVENT_NAMES.ADD_NEW_TOPIC)} style={{ marginRight: 5 }}>
<MessageSquareDiff size={18} />
</NavbarIcon>
</Tooltip>
</NavbarLeft>
)}
<NavbarRight style={{ justifyContent: 'space-between', flex: 1 }} className="home-navbar-right">
<HStack alignItems="center">
{!showAssistants && !sidebarHideCooldown && (
<FloatingSidebar
activeAssistant={assistant}
setActiveAssistant={setActiveAssistant}
activeTopic={activeTopic}
setActiveTopic={setActiveTopic}
position={'left'}>
<Tooltip title={t('navbar.show_sidebar')} mouseEnterDelay={2}>
<NavbarIcon
onClick={() => toggleShowAssistants()}
style={{ marginRight: 8, marginLeft: isMac && !isFullscreen ? 4 : -12 }}>
<PanelRightClose size={18} />
</NavbarIcon>
</Tooltip>
</FloatingSidebar>
)}
{!showAssistants && sidebarHideCooldown && (
<Tooltip title={t('navbar.show_sidebar')} mouseEnterDelay={0.8}>
<NavbarIcon
onClick={() => toggleShowAssistants()}
style={{ marginRight: 8, marginLeft: isMac && !isFullscreen ? 4 : -12 }}
onMouseOut={() => setSidebarHideCooldown(false)}>
<PanelRightClose size={18} />
</NavbarIcon>
</Tooltip>
)}
<SelectModelButton assistant={assistant} />
</HStack>
<HStack alignItems="center" gap={8}>
<UpdateAppButton />
<Tooltip title={t('chat.assistant.search.placeholder')} mouseEnterDelay={0.8}>
<NarrowIcon onClick={() => SearchPopup.show()}>
<Search size={18} />
</NarrowIcon>
</Tooltip>
<Tooltip title={t('navbar.expand')} mouseEnterDelay={0.8}>
<NarrowIcon onClick={handleNarrowModeToggle}>
<i className="iconfont icon-icon-adaptive-width"></i>
</NarrowIcon>
</Tooltip>
{sidebarIcons.visible.includes('minapp') && (
<MinAppsPopover>
<Tooltip title={t('minapp.title')} mouseEnterDelay={0.8}>
<NarrowIcon>
<LayoutGrid size={18} />
</NarrowIcon>
</Tooltip>
</MinAppsPopover>
)}
{topicPosition === 'right' && !showTopics && !sidebarHideCooldown && (
<FloatingSidebar
activeAssistant={assistant}
setActiveAssistant={setActiveAssistant}
activeTopic={activeTopic}
setActiveTopic={setActiveTopic}
position={'right'}>
<Tooltip title={t('navbar.show_sidebar')} mouseEnterDelay={2}>
<NavbarIcon onClick={() => toggleShowTopics()}>
<PanelLeftClose size={18} />
</NavbarIcon>
</Tooltip>
</FloatingSidebar>
)}
{topicPosition === 'right' && !showTopics && sidebarHideCooldown && (
<Tooltip title={t('navbar.show_sidebar')} mouseEnterDelay={2}>
<NavbarIcon onClick={() => toggleShowTopics()} onMouseOut={() => setSidebarHideCooldown(false)}>
<PanelLeftClose size={18} />
</NavbarIcon>
</Tooltip>
)}
{topicPosition === 'right' && showTopics && (
<Tooltip title={t('navbar.hide_sidebar')} mouseEnterDelay={2}>
<NavbarIcon onClick={() => handleToggleShowTopics()}>
<PanelRightClose size={18} />
</NavbarIcon>
</Tooltip>
)}
</HStack>
</NavbarRight>
</Navbar>
)
}
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);
}
`
const NarrowIcon = styled(NavbarIcon)`
@media (max-width: 1000px) {
display: none;
}
`
export default HeaderNavbar

View File

@ -6,7 +6,7 @@ import { useAssistants } from '@renderer/hooks/useAssistant'
import { useAssistantsTabSortType } from '@renderer/hooks/useStore' import { useAssistantsTabSortType } from '@renderer/hooks/useStore'
import { useTags } from '@renderer/hooks/useTags' import { useTags } from '@renderer/hooks/useTags'
import { Assistant, AssistantsSortType } from '@renderer/types' import { Assistant, AssistantsSortType } from '@renderer/types'
import { Divider, Tooltip } from 'antd' import { Tooltip } from 'antd'
import { FC, useCallback, useRef, useState } from 'react' import { FC, useCallback, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
@ -80,7 +80,7 @@ const Assistants: FC<AssistantsTabProps> = ({
if (assistantsTabSortType === 'tags') { if (assistantsTabSortType === 'tags') {
return ( return (
<Container className="assistants-tab" ref={containerRef}> <Container className="assistants-tab" ref={containerRef}>
<div style={{ marginBottom: '8px' }}> <div style={{ display: 'flex', flexDirection: 'column', marginBottom: 4, gap: 10 }}>
{getGroupedAssistants.map((group) => ( {getGroupedAssistants.map((group) => (
<TagsContainer key={group.tag}> <TagsContainer key={group.tag}>
{group.tag !== t('assistants.tags.untagged') && ( {group.tag !== t('assistants.tags.untagged') && (
@ -95,7 +95,7 @@ const Assistants: FC<AssistantsTabProps> = ({
{group.tag} {group.tag}
</GroupTitleName> </GroupTitleName>
</Tooltip> </Tooltip>
<Divider style={{ margin: '12px 0' }}></Divider> <GroupTitleDivider />
</GroupTitle> </GroupTitle>
)} )}
{!collapsedTags[group.tag] && ( {!collapsedTags[group.tag] && (
@ -176,7 +176,7 @@ const Assistants: FC<AssistantsTabProps> = ({
const Container = styled(Scrollbar)` const Container = styled(Scrollbar)`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 10px; padding: 4px 10px;
` `
const TagsContainer = styled.div` const TagsContainer = styled.div`
@ -197,23 +197,20 @@ const AssistantAddItem = styled.div`
cursor: pointer; cursor: pointer;
&:hover { &:hover {
background-color: var(--color-background-soft); background-color: var(--color-list-item-hover);
}
&.active {
background-color: var(--color-background-soft);
border: 0.5px solid var(--color-border);
} }
` `
const GroupTitle = styled.div` const GroupTitle = styled.div`
padding: 8px 0;
position: relative;
color: var(--color-text-2); color: var(--color-text-2);
font-size: 12px; font-size: 12px;
font-weight: 500; font-weight: 500;
margin-bottom: -8px;
cursor: pointer; cursor: pointer;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
height: 24px;
` `
const GroupTitleName = styled.div` const GroupTitleName = styled.div`
@ -221,13 +218,18 @@ const GroupTitleName = styled.div`
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
background-color: var(--color-background);
box-sizing: border-box; box-sizing: border-box;
padding: 0 4px; padding: 0 4px;
color: var(--color-text); color: var(--color-text);
position: absolute;
transform: translateY(2px);
font-size: 13px; font-size: 13px;
line-height: 24px;
margin-right: 5px;
display: flex;
`
const GroupTitleDivider = styled.div`
flex: 1;
border-top: 1px solid var(--color-border);
` `
const AssistantName = styled.div` const AssistantName = styled.div`

View File

@ -1,98 +1,33 @@
import { CheckOutlined } from '@ant-design/icons'
import { HStack } from '@renderer/components/Layout'
import Scrollbar from '@renderer/components/Scrollbar' import Scrollbar from '@renderer/components/Scrollbar'
import { import Selector from '@renderer/components/Selector'
DEFAULT_CONTEXTCOUNT, import { isMac, isWindows } from '@renderer/config/constant'
DEFAULT_MAX_TOKENS,
DEFAULT_TEMPERATURE,
isMac,
isWindows
} from '@renderer/config/constant'
import {
isOpenAIModel,
isSupportedFlexServiceTier,
isSupportedReasoningEffortOpenAIModel
} from '@renderer/config/models'
import { useCodeStyle } from '@renderer/context/CodeStyleProvider'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { useProvider } from '@renderer/hooks/useProvider'
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
import { SettingDivider, SettingRow, SettingRowTitle } from '@renderer/pages/settings' import { SettingDivider, SettingRow, SettingRowTitle } from '@renderer/pages/settings'
import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings'
import { CollapsibleSettingGroup } from '@renderer/pages/settings/SettingGroup'
import { getDefaultModel } from '@renderer/services/AssistantService'
import { useAppDispatch } from '@renderer/store' import { useAppDispatch } from '@renderer/store'
import { import {
SendMessageShortcut, SendMessageShortcut,
setAutoTranslateWithSpace, setAutoTranslateWithSpace,
setCodeCollapsible,
setCodeEditor,
setCodeExecution,
setCodePreview,
setCodeShowLineNumbers,
setCodeWrappable,
setEnableBackspaceDeleteModel, setEnableBackspaceDeleteModel,
setEnableQuickPanelTriggers, setEnableQuickPanelTriggers,
setFontSize,
setMathEngine,
setMessageFont,
setMessageNavigation,
setMessageStyle,
setMultiModelMessageStyle,
setPasteLongTextAsFile, setPasteLongTextAsFile,
setPasteLongTextThreshold, setPasteLongTextThreshold,
setRenderInputMessageAsMarkdown, setRenderInputMessageAsMarkdown,
setShowInputEstimatedTokens, setShowInputEstimatedTokens,
setShowMessageDivider, setShowTranslateConfirm
setShowPrompt,
setShowTokens,
setShowTranslateConfirm,
setThoughtAutoCollapse
} from '@renderer/store/settings' } from '@renderer/store/settings'
import { import { ThemeMode, TranslateLanguageVarious } from '@renderer/types'
Assistant, import { InputNumber, Switch } from 'antd'
AssistantSettings, import { FC } from 'react'
CodeStyleVarious,
MathEngine,
ThemeMode,
TranslateLanguageVarious
} from '@renderer/types'
import { modalConfirm } from '@renderer/utils'
import { Button, Col, InputNumber, Row, Select, Slider, Switch, Tooltip } from 'antd'
import { CircleHelp, Settings2 } from 'lucide-react'
import { FC, useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
import OpenAISettingsGroup from './components/OpenAISettingsGroup' const SettingsTab: FC = () => {
interface Props {
assistant: Assistant
}
const SettingsTab: FC<Props> = (props) => {
const { assistant, updateAssistantSettings } = useAssistant(props.assistant.id)
const { provider } = useProvider(assistant.model.provider)
const { messageStyle, fontSize, language } = useSettings()
const { theme } = useTheme()
const { themeNames } = useCodeStyle()
const [temperature, setTemperature] = useState(assistant?.settings?.temperature ?? DEFAULT_TEMPERATURE)
const [contextCount, setContextCount] = useState(assistant?.settings?.contextCount ?? DEFAULT_CONTEXTCOUNT)
const [enableMaxTokens, setEnableMaxTokens] = useState(assistant?.settings?.enableMaxTokens ?? false)
const [maxTokens, setMaxTokens] = useState(assistant?.settings?.maxTokens ?? 0)
const [fontSizeValue, setFontSizeValue] = useState(fontSize)
const [streamOutput, setStreamOutput] = useState(assistant?.settings?.streamOutput ?? true)
const { t } = useTranslation() const { t } = useTranslation()
const { language } = useSettings()
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const { const {
showPrompt,
showMessageDivider,
messageFont,
showInputEstimatedTokens, showInputEstimatedTokens,
sendMessageShortcut, sendMessageShortcut,
setSendMessageShortcut, setSendMessageShortcut,
@ -100,600 +35,164 @@ const SettingsTab: FC<Props> = (props) => {
setTargetLanguage, setTargetLanguage,
pasteLongTextAsFile, pasteLongTextAsFile,
renderInputMessageAsMarkdown, renderInputMessageAsMarkdown,
codeShowLineNumbers,
codeCollapsible,
codeWrappable,
codeEditor,
codePreview,
codeExecution,
mathEngine,
autoTranslateWithSpace, autoTranslateWithSpace,
pasteLongTextThreshold, pasteLongTextThreshold,
multiModelMessageStyle,
thoughtAutoCollapse,
messageNavigation,
enableQuickPanelTriggers, enableQuickPanelTriggers,
enableBackspaceDeleteModel, enableBackspaceDeleteModel,
showTranslateConfirm, showTranslateConfirm
showTokens
} = useSettings() } = useSettings()
const onUpdateAssistantSettings = (settings: Partial<AssistantSettings>) => {
updateAssistantSettings(settings)
}
const onTemperatureChange = (value) => {
if (!isNaN(value as number)) {
onUpdateAssistantSettings({ temperature: value })
}
}
const onContextCountChange = (value) => {
if (!isNaN(value as number)) {
onUpdateAssistantSettings({ contextCount: value })
}
}
const onMaxTokensChange = (value) => {
if (!isNaN(value as number)) {
onUpdateAssistantSettings({ maxTokens: value })
}
}
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]
)
useEffect(() => {
setTemperature(assistant?.settings?.temperature ?? DEFAULT_TEMPERATURE)
setContextCount(assistant?.settings?.contextCount ?? DEFAULT_CONTEXTCOUNT)
setEnableMaxTokens(assistant?.settings?.enableMaxTokens ?? false)
setMaxTokens(assistant?.settings?.maxTokens ?? DEFAULT_MAX_TOKENS)
setStreamOutput(assistant?.settings?.streamOutput ?? true)
}, [assistant])
const assistantContextCount = assistant?.settings?.contextCount || 20
const maxContextCount = assistantContextCount > 20 ? assistantContextCount : 20
const model = assistant.model || getDefaultModel()
const isOpenAI = isOpenAIModel(model)
const isOpenAIReasoning =
isSupportedReasoningEffortOpenAIModel(model) &&
!model.id.includes('o1-pro') &&
(provider.type === 'openai-response' || provider.id === 'aihubmix')
const isOpenAIFlexServiceTier = isSupportedFlexServiceTier(model)
return ( return (
<Container className="settings-tab"> <Container className="settings-tab">
<CollapsibleSettingGroup <SettingGroup>
title={t('assistants.settings.title')} <SettingRow>
defaultExpanded={true} <SettingRowTitleSmall>{t('settings.messages.input.show_estimated_tokens')}</SettingRowTitleSmall>
extra={ <Switch
<HStack alignItems="center" gap={2}> size="small"
<Button checked={showInputEstimatedTokens}
type="text" onChange={(checked) => dispatch(setShowInputEstimatedTokens(checked))}
size="small" />
icon={<Settings2 size={16} />} </SettingRow>
onClick={() => AssistantSettingsPopup.show({ assistant, tab: 'model' })}
/>
</HStack>
}>
<SettingGroup style={{ marginTop: 5 }}>
<Row align="middle">
<SettingRowTitleSmall>{t('chat.settings.temperature')}</SettingRowTitleSmall>
<Tooltip title={t('chat.settings.temperature.tip')}>
<CircleHelp size={14} style={{ marginLeft: 4 }} color="var(--color-text-2)" />
</Tooltip>
</Row>
<Row align="middle" gutter={10}>
<Col span={23}>
<Slider
min={0}
max={2}
onChange={setTemperature}
onChangeComplete={onTemperatureChange}
value={typeof temperature === 'number' ? temperature : 0}
step={0.1}
/>
</Col>
</Row>
<Row align="middle">
<SettingRowTitleSmall>{t('chat.settings.context_count')}</SettingRowTitleSmall>
<Tooltip title={t('chat.settings.context_count.tip')}>
<CircleHelp size={14} style={{ marginLeft: 4 }} color="var(--color-text-2)" />
</Tooltip>
</Row>
<Row align="middle" gutter={10}>
<Col span={23}>
<Slider
min={0}
max={maxContextCount}
onChange={setContextCount}
onChangeComplete={onContextCountChange}
value={typeof contextCount === 'number' ? contextCount : 0}
step={1}
/>
</Col>
</Row>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('models.stream_output')}</SettingRowTitleSmall>
<Switch
size="small"
checked={streamOutput}
onChange={(checked) => {
setStreamOutput(checked)
onUpdateAssistantSettings({ streamOutput: checked })
}}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<Row align="middle">
<SettingRowTitleSmall>{t('chat.settings.max_tokens')}</SettingRowTitleSmall>
<Tooltip title={t('chat.settings.max_tokens.tip')}>
<CircleHelp size={14} style={{ marginLeft: 4 }} color="var(--color-text-2)" />
</Tooltip>
</Row>
<Switch
size="small"
checked={enableMaxTokens}
onChange={async (enabled) => {
if (enabled) {
const confirmed = await modalConfirm({
title: t('chat.settings.max_tokens.confirm'),
content: t('chat.settings.max_tokens.confirm_content'),
okButtonProps: {
danger: true
}
})
if (!confirmed) return
}
setEnableMaxTokens(enabled)
onUpdateAssistantSettings({ enableMaxTokens: enabled })
}}
/>
</SettingRow>
{enableMaxTokens && (
<Row align="middle" gutter={10} style={{ marginTop: 10 }}>
<Col span={24}>
<InputNumber
disabled={!enableMaxTokens}
min={0}
max={10000000}
step={100}
value={typeof maxTokens === 'number' ? maxTokens : 0}
changeOnBlur
onChange={(value) => value && setMaxTokens(value)}
onBlur={() => onMaxTokensChange(maxTokens)}
style={{ width: '100%' }}
/>
</Col>
</Row>
)}
<SettingDivider />
</SettingGroup>
</CollapsibleSettingGroup>
{isOpenAI && (
<OpenAISettingsGroup
isOpenAIReasoning={isOpenAIReasoning}
isSupportedFlexServiceTier={isOpenAIFlexServiceTier}
SettingGroup={SettingGroup}
SettingRowTitleSmall={SettingRowTitleSmall}
/>
)}
<CollapsibleSettingGroup title={t('settings.messages.title')} defaultExpanded={true}>
<SettingGroup>
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.prompt')}</SettingRowTitleSmall>
<Switch size="small" checked={showPrompt} onChange={(checked) => dispatch(setShowPrompt(checked))} />
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.tokens')}</SettingRowTitleSmall>
<Switch size="small" checked={showTokens} onChange={(checked) => dispatch(setShowTokens(checked))} />
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>
{t('settings.messages.divider')}
<Tooltip title={t('settings.messages.divider.tooltip')}>
<CircleHelp size={14} style={{ marginLeft: 4 }} color="var(--color-text-2)" />
</Tooltip>
</SettingRowTitleSmall>
<Switch
size="small"
checked={showMessageDivider}
onChange={(checked) => dispatch(setShowMessageDivider(checked))}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.use_serif_font')}</SettingRowTitleSmall>
<Switch
size="small"
checked={messageFont === 'serif'}
onChange={(checked) => dispatch(setMessageFont(checked ? 'serif' : 'system'))}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>
{t('chat.settings.thought_auto_collapse')}
<Tooltip title={t('chat.settings.thought_auto_collapse.tip')}>
<CircleHelp size={14} style={{ marginLeft: 4 }} color="var(--color-text-2)" />
</Tooltip>
</SettingRowTitleSmall>
<Switch
size="small"
checked={thoughtAutoCollapse}
onChange={(checked) => dispatch(setThoughtAutoCollapse(checked))}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('message.message.style')}</SettingRowTitleSmall>
<StyledSelect
value={messageStyle}
onChange={(value) => dispatch(setMessageStyle(value as 'plain' | 'bubble'))}
style={{ width: 135 }}
size="small">
<Select.Option value="plain">{t('message.message.style.plain')}</Select.Option>
<Select.Option value="bubble">{t('message.message.style.bubble')}</Select.Option>
</StyledSelect>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('message.message.multi_model_style')}</SettingRowTitleSmall>
<StyledSelect
size="small"
value={multiModelMessageStyle}
onChange={(value) =>
dispatch(setMultiModelMessageStyle(value as 'fold' | 'vertical' | 'horizontal' | 'grid'))
}
style={{ width: 135 }}>
<Select.Option value="fold">{t('message.message.multi_model_style.fold')}</Select.Option>
<Select.Option value="vertical">{t('message.message.multi_model_style.vertical')}</Select.Option>
<Select.Option value="horizontal">{t('message.message.multi_model_style.horizontal')}</Select.Option>
<Select.Option value="grid">{t('message.message.multi_model_style.grid')}</Select.Option>
</StyledSelect>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.navigation')}</SettingRowTitleSmall>
<StyledSelect
size="small"
value={messageNavigation}
onChange={(value) => dispatch(setMessageNavigation(value as 'none' | 'buttons' | 'anchor'))}
style={{ width: 135 }}>
<Select.Option value="none">{t('settings.messages.navigation.none')}</Select.Option>
<Select.Option value="buttons">{t('settings.messages.navigation.buttons')}</Select.Option>
<Select.Option value="anchor">{t('settings.messages.navigation.anchor')}</Select.Option>
</StyledSelect>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.math_engine')}</SettingRowTitleSmall>
<StyledSelect
value={mathEngine}
onChange={(value) => dispatch(setMathEngine(value as MathEngine))}
style={{ width: 135 }}
size="small">
<Select.Option value="KaTeX">KaTeX</Select.Option>
<Select.Option value="MathJax">MathJax</Select.Option>
<Select.Option value="none">{t('settings.messages.math_engine.none')}</Select.Option>
</StyledSelect>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.font_size.title')}</SettingRowTitleSmall>
</SettingRow>
<Row align="middle" gutter={10}>
<Col span={24}>
<Slider
value={fontSizeValue}
onChange={(value) => setFontSizeValue(value)}
onChangeComplete={(value) => dispatch(setFontSize(value))}
min={12}
max={22}
step={1}
marks={{
12: <span style={{ fontSize: '12px' }}>A</span>,
14: <span style={{ fontSize: '14px' }}>{t('common.default')}</span>,
22: <span style={{ fontSize: '18px' }}>A</span>
}}
/>
</Col>
</Row>
<SettingDivider />
</SettingGroup>
</CollapsibleSettingGroup>
<CollapsibleSettingGroup title={t('chat.settings.code.title')} defaultExpanded={true}>
<SettingGroup>
<SettingRow>
<SettingRowTitleSmall>{t('message.message.code_style')}</SettingRowTitleSmall>
<StyledSelect
value={codeStyle}
onChange={(value) => onCodeStyleChange(value as CodeStyleVarious)}
style={{ width: 135 }}
size="small">
{themeNames.map((theme) => (
<Select.Option key={theme} value={theme}>
{theme}
</Select.Option>
))}
</StyledSelect>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>
{t('chat.settings.code_execution.title')}
<Tooltip title={t('chat.settings.code_execution.tip')}>
<CircleHelp size={14} style={{ marginLeft: 4 }} color="var(--color-text-2)" />
</Tooltip>
</SettingRowTitleSmall>
<Switch
size="small"
checked={codeExecution.enabled}
onChange={(checked) => dispatch(setCodeExecution({ enabled: checked }))}
/>
</SettingRow>
{codeExecution.enabled && (
<>
<SettingDivider />
<SettingRow style={{ paddingLeft: 8 }}>
<SettingRowTitleSmall>
{t('chat.settings.code_execution.timeout_minutes')}
<Tooltip title={t('chat.settings.code_execution.timeout_minutes.tip')}>
<CircleHelp size={14} style={{ marginLeft: 4 }} color="var(--color-text-2)" />
</Tooltip>
</SettingRowTitleSmall>
<InputNumber
size="small"
min={1}
max={60}
step={1}
value={codeExecution.timeoutMinutes}
onChange={(value) => dispatch(setCodeExecution({ timeoutMinutes: value ?? 1 }))}
style={{ width: 80 }}
/>
</SettingRow>
</>
)}
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('chat.settings.code_editor.title')}</SettingRowTitleSmall>
<Switch
size="small"
checked={codeEditor.enabled}
onChange={(checked) => dispatch(setCodeEditor({ enabled: checked }))}
/>
</SettingRow>
{codeEditor.enabled && (
<>
<SettingDivider />
<SettingRow style={{ paddingLeft: 8 }}>
<SettingRowTitleSmall>{t('chat.settings.code_editor.highlight_active_line')}</SettingRowTitleSmall>
<Switch
size="small"
checked={codeEditor.highlightActiveLine}
onChange={(checked) => dispatch(setCodeEditor({ highlightActiveLine: checked }))}
/>
</SettingRow>
<SettingDivider />
<SettingRow style={{ paddingLeft: 8 }}>
<SettingRowTitleSmall>{t('chat.settings.code_editor.fold_gutter')}</SettingRowTitleSmall>
<Switch
size="small"
checked={codeEditor.foldGutter}
onChange={(checked) => dispatch(setCodeEditor({ foldGutter: checked }))}
/>
</SettingRow>
<SettingDivider />
<SettingRow style={{ paddingLeft: 8 }}>
<SettingRowTitleSmall>{t('chat.settings.code_editor.autocompletion')}</SettingRowTitleSmall>
<Switch
size="small"
checked={codeEditor.autocompletion}
onChange={(checked) => dispatch(setCodeEditor({ autocompletion: checked }))}
/>
</SettingRow>
<SettingDivider />
<SettingRow style={{ paddingLeft: 8 }}>
<SettingRowTitleSmall>{t('chat.settings.code_editor.keymap')}</SettingRowTitleSmall>
<Switch
size="small"
checked={codeEditor.keymap}
onChange={(checked) => dispatch(setCodeEditor({ keymap: checked }))}
/>
</SettingRow>
</>
)}
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('chat.settings.show_line_numbers')}</SettingRowTitleSmall>
<Switch
size="small"
checked={codeShowLineNumbers}
onChange={(checked) => dispatch(setCodeShowLineNumbers(checked))}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('chat.settings.code_collapsible')}</SettingRowTitleSmall>
<Switch
size="small"
checked={codeCollapsible}
onChange={(checked) => dispatch(setCodeCollapsible(checked))}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('chat.settings.code_wrappable')}</SettingRowTitleSmall>
<Switch size="small" checked={codeWrappable} onChange={(checked) => dispatch(setCodeWrappable(checked))} />
</SettingRow>
</SettingGroup>
<SettingDivider /> <SettingDivider />
</CollapsibleSettingGroup> <SettingRow>
<CollapsibleSettingGroup title={t('settings.messages.input.title')} defaultExpanded={true}> <SettingRowTitleSmall>{t('settings.messages.input.paste_long_text_as_file')}</SettingRowTitleSmall>
<SettingGroup> <Switch
<SettingRow> size="small"
<SettingRowTitleSmall>{t('settings.messages.input.show_estimated_tokens')}</SettingRowTitleSmall> checked={pasteLongTextAsFile}
<Switch onChange={(checked) => dispatch(setPasteLongTextAsFile(checked))}
size="small" />
checked={showInputEstimatedTokens} </SettingRow>
onChange={(checked) => dispatch(setShowInputEstimatedTokens(checked))} {pasteLongTextAsFile && (
/> <>
</SettingRow> <SettingDivider />
<SettingDivider /> <SettingRow>
<SettingRow> <SettingRowTitleSmall>{t('settings.messages.input.paste_long_text_threshold')}</SettingRowTitleSmall>
<SettingRowTitleSmall>{t('settings.messages.input.paste_long_text_as_file')}</SettingRowTitleSmall> <InputNumber
<Switch size="small"
size="small" min={500}
checked={pasteLongTextAsFile} max={10000}
onChange={(checked) => dispatch(setPasteLongTextAsFile(checked))} step={100}
/> value={pasteLongTextThreshold}
</SettingRow> onChange={(value) => dispatch(setPasteLongTextThreshold(value ?? 500))}
{pasteLongTextAsFile && ( style={{ width: 80, backgroundColor: 'transparent' }}
<> />
<SettingDivider /> </SettingRow>
<SettingRow> </>
<SettingRowTitleSmall>{t('settings.messages.input.paste_long_text_threshold')}</SettingRowTitleSmall> )}
<InputNumber <SettingDivider />
size="small" <SettingRow>
min={500} <SettingRowTitleSmall>{t('settings.messages.markdown_rendering_input_message')}</SettingRowTitleSmall>
max={10000} <Switch
step={100} size="small"
value={pasteLongTextThreshold} checked={renderInputMessageAsMarkdown}
onChange={(value) => dispatch(setPasteLongTextThreshold(value ?? 500))} onChange={(checked) => dispatch(setRenderInputMessageAsMarkdown(checked))}
style={{ width: 80 }} />
/> </SettingRow>
</SettingRow> <SettingDivider />
</> {!language.startsWith('en') && (
)} <>
<SettingDivider /> <SettingRow>
<SettingRow> <SettingRowTitleSmall>{t('settings.input.auto_translate_with_space')}</SettingRowTitleSmall>
<SettingRowTitleSmall>{t('settings.messages.markdown_rendering_input_message')}</SettingRowTitleSmall> <Switch
<Switch size="small"
size="small" checked={autoTranslateWithSpace}
checked={renderInputMessageAsMarkdown} onChange={(checked) => dispatch(setAutoTranslateWithSpace(checked))}
onChange={(checked) => dispatch(setRenderInputMessageAsMarkdown(checked))} />
/> </SettingRow>
</SettingRow> <SettingDivider />
<SettingDivider /> </>
{!language.startsWith('en') && ( )}
<> <SettingRow>
<SettingRow> <SettingRowTitleSmall>{t('settings.input.show_translate_confirm')}</SettingRowTitleSmall>
<SettingRowTitleSmall>{t('settings.input.auto_translate_with_space')}</SettingRowTitleSmall> <Switch
<Switch size="small"
size="small" checked={showTranslateConfirm}
checked={autoTranslateWithSpace} onChange={(checked) => dispatch(setShowTranslateConfirm(checked))}
onChange={(checked) => dispatch(setAutoTranslateWithSpace(checked))} />
/> </SettingRow>
</SettingRow> <SettingDivider />
<SettingDivider /> <SettingRow>
</> <SettingRowTitleSmall>{t('settings.messages.input.enable_quick_triggers')}</SettingRowTitleSmall>
)} <Switch
<SettingRow> size="small"
<SettingRowTitleSmall>{t('settings.input.show_translate_confirm')}</SettingRowTitleSmall> checked={enableQuickPanelTriggers}
<Switch onChange={(checked) => dispatch(setEnableQuickPanelTriggers(checked))}
size="small" />
checked={showTranslateConfirm} </SettingRow>
onChange={(checked) => dispatch(setShowTranslateConfirm(checked))} <SettingDivider />
/> <SettingRow>
</SettingRow> <SettingRowTitleSmall>{t('settings.messages.input.enable_delete_model')}</SettingRowTitleSmall>
<SettingDivider /> <Switch
<SettingRow> size="small"
<SettingRowTitleSmall>{t('settings.messages.input.enable_quick_triggers')}</SettingRowTitleSmall> checked={enableBackspaceDeleteModel}
<Switch onChange={(checked) => dispatch(setEnableBackspaceDeleteModel(checked))}
size="small" />
checked={enableQuickPanelTriggers} </SettingRow>
onChange={(checked) => dispatch(setEnableQuickPanelTriggers(checked))} <SettingDivider />
/> <SettingRow>
</SettingRow> <SettingRowTitleSmall>{t('settings.input.target_language')}</SettingRowTitleSmall>
<SettingDivider /> <Selector
<SettingRow> value={targetLanguage || 'english'}
<SettingRowTitleSmall>{t('settings.messages.input.enable_delete_model')}</SettingRowTitleSmall> options={[
<Switch { value: 'chinese', label: t('settings.input.target_language.chinese') },
size="small" { value: 'chinese-traditional', label: t('settings.input.target_language.chinese-traditional') },
checked={enableBackspaceDeleteModel} { value: 'english', label: t('settings.input.target_language.english') },
onChange={(checked) => dispatch(setEnableBackspaceDeleteModel(checked))} { value: 'japanese', label: t('settings.input.target_language.japanese') },
/> { value: 'russian', label: t('settings.input.target_language.russian') }
</SettingRow> ]}
<SettingDivider /> onChange={(value) => setTargetLanguage(value as TranslateLanguageVarious)}
<SettingRow> />
<SettingRowTitleSmall>{t('settings.input.target_language')}</SettingRowTitleSmall> </SettingRow>
<StyledSelect <SettingDivider />
defaultValue={'english' as TranslateLanguageVarious} <SettingRow>
size="small" <SettingRowTitleSmall>{t('settings.messages.input.send_shortcuts')}</SettingRowTitleSmall>
value={targetLanguage} <Selector
menuItemSelectedIcon={<CheckOutlined />} value={sendMessageShortcut}
options={[ options={[
{ value: 'chinese', label: t('settings.input.target_language.chinese') }, { value: 'Enter', label: 'Enter' },
{ value: 'chinese-traditional', label: t('settings.input.target_language.chinese-traditional') }, { value: 'Shift+Enter', label: 'Shift + Enter' },
{ value: 'english', label: t('settings.input.target_language.english') }, { value: 'Ctrl+Enter', label: 'Ctrl + Enter' },
{ value: 'japanese', label: t('settings.input.target_language.japanese') }, { value: 'Command+Enter', label: `${isMac ? '⌘' : isWindows ? 'Win' : 'Super'} + Enter` }
{ value: 'russian', label: t('settings.input.target_language.russian') } ]}
]} onChange={(value) => setSendMessageShortcut(value as SendMessageShortcut)}
onChange={(value) => setTargetLanguage(value as TranslateLanguageVarious)} />
style={{ width: 135 }} </SettingRow>
/> </SettingGroup>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.input.send_shortcuts')}</SettingRowTitleSmall>
<StyledSelect
size="small"
value={sendMessageShortcut}
menuItemSelectedIcon={<CheckOutlined />}
options={[
{ value: 'Enter', label: 'Enter' },
{ value: 'Shift+Enter', label: 'Shift + Enter' },
{ value: 'Ctrl+Enter', label: 'Ctrl + Enter' },
{ value: 'Command+Enter', label: `${isMac ? '⌘' : isWindows ? 'Win' : 'Super'} + Enter` }
]}
onChange={(value) => setSendMessageShortcut(value as SendMessageShortcut)}
style={{ width: 135 }}
/>
</SettingRow>
</SettingGroup>
</CollapsibleSettingGroup>
</Container> </Container>
) )
} }
const Container = styled(Scrollbar)` const Container = styled(Scrollbar)`
min-width: 500px;
max-width: 60vw;
display: flex; display: flex;
flex: 1;
flex-direction: column; flex-direction: column;
padding: 0 8px; padding: 10px;
padding-right: 0;
padding-top: 2px; .ant-tabs-nav {
padding-bottom: 10px; .ant-tabs-tab {
margin-top: 3px; padding: 0 20px;
}
}
.ant-tabs {
.ant-tabs-content-holder {
padding: 0 10px;
overflow: auto;
min-height: 460px;
}
.ant-tabs-content {
height: 100%;
}
.ant-tabs-tabpane {
height: 100%;
overflow: auto;
padding-right: 8px;
padding-left: 8px !important;
}
}
` `
const SettingRowTitleSmall = styled(SettingRowTitle)` const SettingRowTitleSmall = styled(SettingRowTitle)`
@ -706,14 +205,7 @@ const SettingGroup = styled.div<{ theme?: ThemeMode }>`
margin-top: 0; margin-top: 0;
border-radius: 8px; border-radius: 8px;
margin-bottom: 10px; margin-bottom: 10px;
` margin-top: 10px;
const StyledSelect = styled(Select)`
.ant-select-selector {
border-radius: 15px !important;
padding: 4px 10px !important;
height: 26px !important;
}
` `
export default SettingsTab export default SettingsTab

View File

@ -4,7 +4,6 @@ import {
DeleteOutlined, DeleteOutlined,
EditOutlined, EditOutlined,
FolderOutlined, FolderOutlined,
MenuOutlined,
PushpinOutlined, PushpinOutlined,
QuestionCircleOutlined, QuestionCircleOutlined,
UploadOutlined UploadOutlined
@ -55,7 +54,7 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
const { assistants } = useAssistants() const { assistants } = useAssistants()
const { assistant, removeTopic, moveTopic, updateTopic, updateTopics } = useAssistant(_assistant.id) const { assistant, removeTopic, moveTopic, updateTopic, updateTopics } = useAssistant(_assistant.id)
const { t } = useTranslation() const { t } = useTranslation()
const { showTopicTime, pinTopicsToTop, setTopicPosition } = useSettings() const { showTopicTime, pinTopicsToTop } = useSettings()
const borderRadius = showTopicTime ? 12 : 'var(--list-item-border-radius)' const borderRadius = showTopicTime ? 12 : 'var(--list-item-border-radius)'
@ -249,23 +248,6 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
}) })
} }
}, },
{
label: t('settings.topic.position'),
key: 'topic-position',
icon: <MenuOutlined />,
children: [
{
label: t('settings.topic.position.left'),
key: 'left',
onClick: () => setTopicPosition('left')
},
{
label: t('settings.topic.position.right'),
key: 'right',
onClick: () => setTopicPosition('right')
}
]
},
{ {
label: t('chat.topics.copy.title'), label: t('chat.topics.copy.title'),
key: 'copy', key: 'copy',
@ -404,7 +386,6 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
setActiveTopic, setActiveTopic,
onPinTopic, onPinTopic,
onClearMessages, onClearMessages,
setTopicPosition,
onMoveTopic, onMoveTopic,
onDeleteTopic onDeleteTopic
]) ])
@ -482,9 +463,7 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
{fullTopicPrompt} {fullTopicPrompt}
</TopicPromptText> </TopicPromptText>
)} )}
{showTopicTime && ( {showTopicTime && <TopicTime className="time">{dayjs(topic.createdAt).format('MM/DD')}</TopicTime>}
<TopicTime className="time">{dayjs(topic.createdAt).format('MM/DD HH:mm')}</TopicTime>
)}
</TopicListItem> </TopicListItem>
) )
}} }}
@ -573,7 +552,8 @@ const TopicPromptText = styled.div`
const TopicTime = styled.div` const TopicTime = styled.div`
color: var(--color-text-3); color: var(--color-text-3);
font-size: 11px; font-size: 12px;
font-family: ubuntu;
` `
const MenuButton = styled.div` const MenuButton = styled.div`

View File

@ -20,12 +20,12 @@ import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings'
import { getDefaultModel, getDefaultTopic } from '@renderer/services/AssistantService' import { getDefaultModel, getDefaultTopic } from '@renderer/services/AssistantService'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { Assistant, AssistantsSortType } from '@renderer/types' import { Assistant, AssistantsSortType } from '@renderer/types'
import { getLeadingEmoji, uuid } from '@renderer/utils' import { classNames, getLeadingEmoji, uuid } from '@renderer/utils'
import { hasTopicPendingRequests } from '@renderer/utils/queue' import { hasTopicPendingRequests } from '@renderer/utils/queue'
import { Dropdown, MenuProps } from 'antd' import { Button, Dropdown, MenuProps } from 'antd'
import { omit } from 'lodash' import { omit } from 'lodash'
import { AlignJustify, Plus, Settings2, Tag, Tags } from 'lucide-react' import { AlignJustify, EllipsisVertical, Plus, Settings2, Tag, Tags } from 'lucide-react'
import { FC, memo, startTransition, useCallback, useEffect, useMemo, useState } from 'react' import { FC, memo, useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
import * as tinyPinyin from 'tiny-pinyin' import * as tinyPinyin from 'tiny-pinyin'
@ -43,6 +43,7 @@ interface AssistantItemProps {
addAssistant: (assistant: Assistant) => void addAssistant: (assistant: Assistant) => void
onTagClick?: (tag: string) => void onTagClick?: (tag: string) => void
handleSortByChange?: (sortType: AssistantsSortType) => void handleSortByChange?: (sortType: AssistantsSortType) => void
singleLine?: boolean
} }
const AssistantItem: FC<AssistantItemProps> = ({ const AssistantItem: FC<AssistantItemProps> = ({
@ -53,16 +54,18 @@ const AssistantItem: FC<AssistantItemProps> = ({
onDelete, onDelete,
addAgent, addAgent,
addAssistant, addAssistant,
handleSortByChange handleSortByChange,
singleLine = false
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const { allTags } = useTags() const { allTags } = useTags()
const { removeAllTopics } = useAssistant(assistant.id) const { removeAllTopics } = useAssistant(assistant.id)
const { clickAssistantToShowTopic, topicPosition, assistantIconType, setAssistantIconType } = useSettings() const { assistantIconType, setAssistantIconType } = useSettings()
const defaultModel = getDefaultModel() const defaultModel = getDefaultModel()
const { assistants, updateAssistants } = useAssistants() const { assistants, updateAssistants } = useAssistants()
const [isPending, setIsPending] = useState(false) const [isPending, setIsPending] = useState(false)
const [isMenuOpen, setIsMenuOpen] = useState(false)
useEffect(() => { useEffect(() => {
if (isActive) { if (isActive) {
@ -121,17 +124,13 @@ const AssistantItem: FC<AssistantItemProps> = ({
) )
const handleSwitch = useCallback(async () => { const handleSwitch = useCallback(async () => {
if (clickAssistantToShowTopic) { if (isMenuOpen) {
if (topicPosition === 'left') { return
EventEmitter.emit(EVENT_NAMES.SWITCH_TOPIC_SIDEBAR)
}
onSwitch(assistant)
} else {
startTransition(() => {
onSwitch(assistant)
})
} }
}, [clickAssistantToShowTopic, onSwitch, assistant, topicPosition])
EventEmitter.emit(EVENT_NAMES.SWITCH_TOPIC_SIDEBAR)
onSwitch(assistant)
}, [isMenuOpen, onSwitch, assistant])
const assistantName = useMemo(() => assistant.name || t('chat.default.name'), [assistant.name, t]) const assistantName = useMemo(() => assistant.name || t('chat.default.name'), [assistant.name, t])
const fullAssistantName = useMemo( const fullAssistantName = useMemo(
@ -139,31 +138,61 @@ const AssistantItem: FC<AssistantItemProps> = ({
[assistant.emoji, assistantName] [assistant.emoji, assistantName]
) )
const assistantNave = (
<AssistantNameRow className="name" title={fullAssistantName}>
{assistantIconType === 'model' ? (
<ModelAvatar
model={assistant.model || defaultModel}
size={24}
className={isPending && !isActive ? 'animation-pulse' : ''}
/>
) : (
assistantIconType === 'emoji' && (
<EmojiIcon
emoji={assistant.emoji || getLeadingEmoji(assistantName)}
className={isPending && !isActive ? 'animation-pulse' : ''}
/>
)
)}
<AssistantName className="text-nowrap">{assistantName}</AssistantName>
</AssistantNameRow>
)
if (singleLine) {
return (
<Container
onClick={handleSwitch}
className={classNames({ active: isActive, 'is-menu-open': isMenuOpen, singleLine })}>
{assistantNave}
<Button
className="item-menu-button"
type="text"
size="small"
icon={<Settings2 size={16} color="var(--color-text-3)" />}
onClick={(e) => {
e.stopPropagation()
AssistantSettingsPopup.show({ assistant })
}}
/>
</Container>
)
}
return ( return (
<Dropdown menu={{ items: menuItems }} trigger={['contextMenu']}> <Dropdown menu={{ items: menuItems }} trigger={['contextMenu']}>
<Container onClick={handleSwitch} className={isActive ? 'active' : ''}> <Container
<AssistantNameRow className="name" title={fullAssistantName}> onClick={handleSwitch}
{assistantIconType === 'model' ? ( className={classNames({ active: isActive, 'is-menu-open': isMenuOpen, singleLine })}>
<ModelAvatar {assistantNave}
model={assistant.model || defaultModel} <Dropdown menu={{ items: menuItems }} trigger={['click']} onOpenChange={setIsMenuOpen}>
size={24} <Button
className={isPending && !isActive ? 'animation-pulse' : ''} className="item-menu-button"
/> type="text"
) : ( size="small"
assistantIconType === 'emoji' && ( icon={<EllipsisVertical size={16} color="var(--color-text-3)" />}
<EmojiIcon onClick={(e) => e.stopPropagation()}
emoji={assistant.emoji || getLeadingEmoji(assistantName)} />
className={isPending && !isActive ? 'animation-pulse' : ''} </Dropdown>
/>
)
)}
<AssistantName className="text-nowrap">{assistantName}</AssistantName>
</AssistantNameRow>
{isActive && (
<MenuButton onClick={() => EventEmitter.emit(EVENT_NAMES.SWITCH_TOPIC_SIDEBAR)}>
<TopicCount className="topics-count">{assistant.topics.length}</TopicCount>
</MenuButton>
)}
</Container> </Container>
</Dropdown> </Dropdown>
) )
@ -382,6 +411,7 @@ const Container = styled.div`
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
align-items: center;
padding: 0 8px; padding: 0 8px;
height: 37px; height: 37px;
position: relative; position: relative;
@ -389,12 +419,28 @@ const Container = styled.div`
border: 0.5px solid transparent; border: 0.5px solid transparent;
width: calc(var(--assistants-width) - 20px); width: calc(var(--assistants-width) - 20px);
cursor: pointer; cursor: pointer;
&.is-menu-open {
.item-menu-button {
display: block;
}
}
&:hover { &:hover {
background-color: var(--color-list-item-hover); background-color: var(--color-list-item-hover);
.item-menu-button {
display: block;
}
} }
&.active { &.active {
background-color: var(--color-list-item); background-color: var(--color-list-item);
} }
.item-menu-button {
display: none;
}
&.singleLine {
.item-menu-button {
display: block;
}
}
` `
const AssistantNameRow = styled.div` const AssistantNameRow = styled.div`
@ -410,31 +456,4 @@ const AssistantName = styled.div`
font-size: 13px; font-size: 13px;
` `
const MenuButton = styled.div`
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
min-width: 22px;
height: 22px;
min-height: 22px;
border-radius: 11px;
position: absolute;
background-color: var(--color-background);
right: 9px;
top: 6px;
padding: 0 5px;
border: 0.5px solid var(--color-border);
`
const TopicCount = styled.div`
color: var(--color-text);
font-size: 10px;
border-radius: 10px;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
`
export default memo(AssistantItem) export default memo(AssistantItem)

View File

@ -1,5 +1,4 @@
import { SettingDivider, SettingRow } from '@renderer/pages/settings' import { SettingDivider, SettingRow } from '@renderer/pages/settings'
import { CollapsibleSettingGroup } from '@renderer/pages/settings/SettingGroup'
import { RootState, useAppDispatch } from '@renderer/store' import { RootState, useAppDispatch } from '@renderer/store'
import { setOpenAIServiceTier, setOpenAISummaryText } from '@renderer/store/settings' import { setOpenAIServiceTier, setOpenAISummaryText } from '@renderer/store/settings'
import { OpenAIServiceTier, OpenAISummaryText } from '@renderer/types' import { OpenAIServiceTier, OpenAISummaryText } from '@renderer/types'
@ -93,49 +92,49 @@ const OpenAISettingsGroup: FC<Props> = ({
}, [serviceTierMode, serviceTierOptions, setServiceTierMode]) }, [serviceTierMode, serviceTierOptions, setServiceTierMode])
return ( return (
<CollapsibleSettingGroup title={t('settings.openai.title')} defaultExpanded={true}> <SettingGroup>
<SettingGroup> {/* <SettingTitle>{t('settings.openai.title')}</SettingTitle>
<SettingRow> <SettingDivider /> */}
<SettingRowTitleSmall> <SettingRow>
{t('settings.openai.service_tier.title')}{' '} <SettingRowTitleSmall>
<Tooltip title={t('settings.openai.service_tier.tip')}> {t('settings.openai.service_tier.title')}{' '}
<CircleHelp size={14} style={{ marginLeft: 4 }} color="var(--color-text-2)" /> <Tooltip title={t('settings.openai.service_tier.tip')}>
</Tooltip> <CircleHelp size={14} style={{ marginLeft: 4 }} color="var(--color-text-2)" />
</SettingRowTitleSmall> </Tooltip>
<StyledSelect </SettingRowTitleSmall>
value={serviceTierMode} <StyledSelect
style={{ width: 135 }} value={serviceTierMode}
onChange={(value) => { style={{ width: 135 }}
setServiceTierMode(value as OpenAIServiceTier) onChange={(value) => {
}} setServiceTierMode(value as OpenAIServiceTier)
size="small" }}
options={serviceTierOptions} size="small"
/> options={serviceTierOptions}
</SettingRow> />
{isOpenAIReasoning && ( </SettingRow>
<> {isOpenAIReasoning && (
<SettingDivider /> <>
<SettingRow> <SettingDivider />
<SettingRowTitleSmall> <SettingRow>
{t('settings.openai.summary_text_mode.title')}{' '} <SettingRowTitleSmall>
<Tooltip title={t('settings.openai.summary_text_mode.tip')}> {t('settings.openai.summary_text_mode.title')}{' '}
<CircleHelp size={14} style={{ marginLeft: 4 }} color="var(--color-text-2)" /> <Tooltip title={t('settings.openai.summary_text_mode.tip')}>
</Tooltip> <CircleHelp size={14} style={{ marginLeft: 4 }} color="var(--color-text-2)" />
</SettingRowTitleSmall> </Tooltip>
<StyledSelect </SettingRowTitleSmall>
value={summaryText} <StyledSelect
style={{ width: 135 }} value={summaryText}
onChange={(value) => { style={{ width: 135 }}
setSummaryText(value as OpenAISummaryText) onChange={(value) => {
}} setSummaryText(value as OpenAISummaryText)
size="small" }}
options={summaryTextOptions} size="small"
/> options={summaryTextOptions}
</SettingRow> />
</> </SettingRow>
)} </>
</SettingGroup> )}
</CollapsibleSettingGroup> </SettingGroup>
) )
} }

View File

@ -1,65 +1,27 @@
import AddAssistantPopup from '@renderer/components/Popups/AddAssistantPopup' import AddAssistantPopup from '@renderer/components/Popups/AddAssistantPopup'
import { useAssistants, useDefaultAssistant } from '@renderer/hooks/useAssistant' import { useAssistants, useDefaultAssistant } from '@renderer/hooks/useAssistant'
import { useSettings } from '@renderer/hooks/useSettings'
import { useShowTopics } from '@renderer/hooks/useStore'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { Assistant, Topic } from '@renderer/types' import { Assistant, Topic } from '@renderer/types'
import { uuid } from '@renderer/utils' import { uuid } from '@renderer/utils'
import { Segmented as AntSegmented, SegmentedProps } from 'antd' import { FC } from 'react'
import { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
import Assistants from './AssistantsTab' import Assistants from './AssistantsTab'
import Settings from './SettingsTab'
import Topics from './TopicsTab' import Topics from './TopicsTab'
interface Props { interface Props {
tab: Tab
activeAssistant: Assistant activeAssistant: Assistant
activeTopic: Topic activeTopic: Topic
setActiveAssistant: (assistant: Assistant) => void setActiveAssistant: (assistant: Assistant) => void
setActiveTopic: (topic: Topic) => void setActiveTopic: (topic: Topic) => void
position: 'left' | 'right'
forceToSeeAllTab?: boolean
style?: React.CSSProperties style?: React.CSSProperties
} }
type Tab = 'assistants' | 'topic' | 'settings' type Tab = 'assistants' | 'topic'
let _tab: any = '' const HomeTabs: FC<Props> = ({ tab, activeAssistant, activeTopic, setActiveAssistant, setActiveTopic, style }) => {
const HomeTabs: FC<Props> = ({
activeAssistant,
activeTopic,
setActiveAssistant,
setActiveTopic,
position,
forceToSeeAllTab,
style
}) => {
const { addAssistant } = useAssistants() const { addAssistant } = useAssistants()
const [tab, setTab] = useState<Tab>(position === 'left' ? _tab || 'assistants' : 'topic')
const { topicPosition } = useSettings()
const { defaultAssistant } = useDefaultAssistant() const { defaultAssistant } = useDefaultAssistant()
const { showTopics, toggleShowTopics } = useShowTopics()
const { t } = useTranslation()
const borderStyle = '0.5px solid var(--color-border)'
const border =
position === 'left' ? { borderRight: borderStyle } : { borderLeft: borderStyle, borderTopLeftRadius: 0 }
if (position === 'left' && topicPosition === 'left') {
_tab = tab
}
const showTab = !(position === 'left' && topicPosition === 'right')
const assistantTab = {
label: t('assistants.abbr'),
value: 'assistants'
// icon: <BotIcon size={16} />
}
const onCreateAssistant = async () => { const onCreateAssistant = async () => {
const assistant = await AddAssistantPopup.show() const assistant = await AddAssistantPopup.show()
@ -72,68 +34,8 @@ const HomeTabs: FC<Props> = ({
setActiveAssistant(assistant) setActiveAssistant(assistant)
} }
useEffect(() => {
const unsubscribes = [
EventEmitter.on(EVENT_NAMES.SHOW_ASSISTANTS, (): any => {
showTab && setTab('assistants')
}),
EventEmitter.on(EVENT_NAMES.SHOW_TOPIC_SIDEBAR, (): any => {
showTab && setTab('topic')
}),
EventEmitter.on(EVENT_NAMES.SHOW_CHAT_SETTINGS, (): any => {
showTab && setTab('settings')
}),
EventEmitter.on(EVENT_NAMES.SWITCH_TOPIC_SIDEBAR, () => {
showTab && setTab('topic')
if (position === 'left' && topicPosition === 'right') {
toggleShowTopics()
}
})
]
return () => unsubscribes.forEach((unsub) => unsub())
}, [position, showTab, tab, toggleShowTopics, topicPosition])
useEffect(() => {
if (position === 'right' && topicPosition === 'right' && tab === 'assistants') {
setTab('topic')
}
if (position === 'left' && topicPosition === 'right' && forceToSeeAllTab != true && tab !== 'assistants') {
setTab('assistants')
}
}, [position, tab, topicPosition, forceToSeeAllTab])
return ( return (
<Container style={{ ...border, ...style }} className="home-tabs"> <Container style={{ ...style }} className="home-tabs">
{(showTab || (forceToSeeAllTab == true && !showTopics)) && (
<>
<Segmented
value={tab}
style={{ borderRadius: 50 }}
shape="round"
options={
[
(position === 'left' && topicPosition === 'left') || (forceToSeeAllTab == true && position === 'left')
? assistantTab
: undefined,
{
label: t('common.topics'),
value: 'topic'
// icon: <MessageSquareQuote size={16} />
},
{
label: t('settings.title'),
value: 'settings'
// icon: <SettingsIcon size={16} />
}
].filter(Boolean) as SegmentedProps['options']
}
onChange={(value) => setTab(value as 'topic' | 'settings')}
block
/>
<Divider />
</>
)}
<TabContent className="home-tabs-content"> <TabContent className="home-tabs-content">
{tab === 'assistants' && ( {tab === 'assistants' && (
<Assistants <Assistants
@ -146,7 +48,6 @@ const HomeTabs: FC<Props> = ({
{tab === 'topic' && ( {tab === 'topic' && (
<Topics assistant={activeAssistant} activeTopic={activeTopic} setActiveTopic={setActiveTopic} /> <Topics assistant={activeAssistant} activeTopic={activeTopic} setActiveTopic={setActiveTopic} />
)} )}
{tab === 'settings' && <Settings assistant={activeAssistant} />}
</TabContent> </TabContent>
</Container> </Container>
) )
@ -154,6 +55,7 @@ const HomeTabs: FC<Props> = ({
const Container = styled.div` const Container = styled.div`
display: flex; display: flex;
flex: 1;
flex-direction: column; flex-direction: column;
max-width: var(--assistants-width); max-width: var(--assistants-width);
min-width: var(--assistants-width); min-width: var(--assistants-width);
@ -173,68 +75,4 @@ const TabContent = styled.div`
overflow-x: hidden; overflow-x: hidden;
` `
const Divider = styled.div`
border-top: 0.5px solid var(--color-border);
margin-top: 10px;
margin-left: 10px;
margin-right: 10px;
`
const Segmented = styled(AntSegmented)`
font-family: var(--font-family);
&.ant-segmented {
background-color: transparent;
margin: 0 10px;
margin-top: 10px;
padding: 0;
}
.ant-segmented-item {
overflow: hidden;
transition: none !important;
height: 34px;
line-height: 34px;
background-color: transparent;
user-select: none;
border-radius: var(--list-item-border-radius);
box-shadow: none;
}
.ant-segmented-item-selected,
.ant-segmented-item-selected:active {
transition: none !important;
background-color: var(--color-list-item);
}
.ant-segmented-item-label {
align-items: center;
display: flex;
flex-direction: row;
justify-content: center;
font-size: 13px;
height: 100%;
}
.ant-segmented-item-label[aria-selected='true'] {
color: var(--color-text);
}
.icon-business-smart-assistant {
margin-right: -2px;
}
.ant-segmented-thumb {
transition: none !important;
background-color: var(--color-list-item);
border-radius: var(--list-item-border-radius);
box-shadow: none;
&:hover {
background-color: transparent;
}
}
.ant-segmented-item-label,
.ant-segmented-item-icon {
display: flex;
align-items: center;
}
/* These styles ensure the same appearance as before */
border-radius: 0;
box-shadow: none;
`
export default HomeTabs export default HomeTabs

View File

@ -6,6 +6,7 @@ import { useAssistant } from '@renderer/hooks/useAssistant'
import { getProviderName } from '@renderer/services/ProviderService' import { getProviderName } from '@renderer/services/ProviderService'
import { Assistant } from '@renderer/types' import { Assistant } from '@renderer/types'
import { Button } from 'antd' import { Button } from 'antd'
import { ChevronsUpDown } from 'lucide-react'
import { FC } from 'react' import { FC } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
@ -45,9 +46,10 @@ const SelectModelButton: FC<Props> = ({ assistant }) => {
<ButtonContent> <ButtonContent>
<ModelAvatar model={model} size={20} /> <ModelAvatar model={model} size={20} />
<ModelName> <ModelName>
{model ? model.name : t('button.select_model')} {providerName ? '| ' + providerName : ''} {model ? model.name : t('button.select_model')} {providerName ? ' | ' + providerName : ''}
</ModelName> </ModelName>
</ButtonContent> </ButtonContent>
<ChevronsUpDown size={14} color="var(--color-icon)" />
</DropdownButton> </DropdownButton>
) )
} }
@ -55,7 +57,7 @@ const SelectModelButton: FC<Props> = ({ assistant }) => {
const DropdownButton = styled(Button)` const DropdownButton = styled(Button)`
font-size: 11px; font-size: 11px;
border-radius: 15px; border-radius: 15px;
padding: 12px 8px 12px 3px; padding: 12px 5px;
-webkit-app-region: none; -webkit-app-region: none;
box-shadow: none; box-shadow: none;
background-color: transparent; background-color: transparent;
@ -65,11 +67,12 @@ const DropdownButton = styled(Button)`
const ButtonContent = styled.div` const ButtonContent = styled.div`
display: flex; display: flex;
align-items: center; align-items: center;
gap: 5px; gap: 6px;
` `
const ModelName = styled.span` const ModelName = styled.span`
font-weight: 500; font-weight: 500;
margin-right: -2px;
` `
export default SelectModelButton export default SelectModelButton

View File

@ -22,7 +22,7 @@ import styled from 'styled-components'
import CustomCollapse from '../../components/CustomCollapse' import CustomCollapse from '../../components/CustomCollapse'
import FileItem from '../files/FileItem' import FileItem from '../files/FileItem'
import { NavbarIcon } from '../home/Navbar' import { NavbarIcon } from '../home/ChatNavbar'
import KnowledgeSearchPopup from './components/KnowledgeSearchPopup' import KnowledgeSearchPopup from './components/KnowledgeSearchPopup'
import KnowledgeSettingsPopup from './components/KnowledgeSettingsPopup' import KnowledgeSettingsPopup from './components/KnowledgeSettingsPopup'
import StatusIcon from './components/StatusIcon' import StatusIcon from './components/StatusIcon'

View File

@ -1,5 +1,5 @@
import { DeleteOutlined, EditOutlined, SettingOutlined } from '@ant-design/icons' import { DeleteOutlined, EditOutlined, SettingOutlined } from '@ant-design/icons'
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar' import { NavbarCenter, NavbarMain } from '@renderer/components/app/Navbar'
import DragableList from '@renderer/components/DragableList' import DragableList from '@renderer/components/DragableList'
import ListItem from '@renderer/components/ListItem' import ListItem from '@renderer/components/ListItem'
import PromptPopup from '@renderer/components/Popups/PromptPopup' import PromptPopup from '@renderer/components/Popups/PromptPopup'
@ -92,9 +92,9 @@ const KnowledgePage: FC = () => {
return ( return (
<Container> <Container>
<Navbar> <NavbarMain>
<NavbarCenter style={{ borderRight: 'none' }}>{t('knowledge.title')}</NavbarCenter> <NavbarCenter style={{ borderRight: 'none' }}>{t('knowledge.title')}</NavbarCenter>
</Navbar> </NavbarMain>
<ContentContainer id="content-container"> <ContentContainer id="content-container">
<SideNav> <SideNav>
<ScrollContainer> <ScrollContainer>

View File

@ -1,15 +1,14 @@
import { CheckCircleOutlined, QuestionCircleOutlined, WarningOutlined } from '@ant-design/icons' import { CheckCircleOutlined, QuestionCircleOutlined, WarningOutlined } from '@ant-design/icons'
import { Center, VStack } from '@renderer/components/Layout' import { Center, VStack } from '@renderer/components/Layout'
import { SettingDescription, SettingRow, SettingSubtitle } from '@renderer/pages/settings'
import { useAppDispatch, useAppSelector } from '@renderer/store' import { useAppDispatch, useAppSelector } from '@renderer/store'
import { setIsBunInstalled, setIsUvInstalled } from '@renderer/store/mcp' import { setIsBunInstalled, setIsUvInstalled } from '@renderer/store/mcp'
import { Alert, Button } from 'antd' import { Alert, Button } from 'antd'
import { FC, useCallback, useEffect, useState } from 'react' import { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router' import { useNavigate } from 'react-router'
import styled from 'styled-components' import styled from 'styled-components'
import { SettingDescription, SettingRow, SettingSubtitle } from '..'
interface Props { interface Props {
mini?: boolean mini?: boolean
} }
@ -26,7 +25,7 @@ const InstallNpxUv: FC<Props> = ({ mini = false }) => {
const [binariesDir, setBinariesDir] = useState<string | null>(null) const [binariesDir, setBinariesDir] = useState<string | null>(null)
const { t } = useTranslation() const { t } = useTranslation()
const navigate = useNavigate() const navigate = useNavigate()
const checkBinaries = useCallback(async () => { const checkBinaries = async () => {
const uvExists = await window.api.isBinaryExist('uv') const uvExists = await window.api.isBinaryExist('uv')
const bunExists = await window.api.isBinaryExist('bun') const bunExists = await window.api.isBinaryExist('bun')
const { uvPath, bunPath, dir } = await window.api.mcp.getInstallInfo() const { uvPath, bunPath, dir } = await window.api.mcp.getInstallInfo()
@ -36,7 +35,7 @@ const InstallNpxUv: FC<Props> = ({ mini = false }) => {
setUvPath(uvPath) setUvPath(uvPath)
setBunPath(bunPath) setBunPath(bunPath)
setBinariesDir(dir) setBinariesDir(dir)
}, [dispatch]) }
const installUV = async () => { const installUV = async () => {
try { try {
@ -69,7 +68,7 @@ const InstallNpxUv: FC<Props> = ({ mini = false }) => {
useEffect(() => { useEffect(() => {
checkBinaries() checkBinaries()
}, [checkBinaries]) }, [])
if (mini) { if (mini) {
const installed = isUvInstalled && isBunInstalled const installed = isUvInstalled && isBunInstalled
@ -82,7 +81,7 @@ const InstallNpxUv: FC<Props> = ({ mini = false }) => {
icon={installed ? <CheckCircleOutlined /> : <WarningOutlined />} icon={installed ? <CheckCircleOutlined /> : <WarningOutlined />}
className="nodrag" className="nodrag"
color={installed ? 'green' : 'danger'} color={installed ? 'green' : 'danger'}
onClick={() => navigate('/settings/mcp/mcp-install')} onClick={() => navigate('/mcp-servers/mcp-install')}
/> />
) )
} }

View File

@ -3,6 +3,7 @@ import { nanoid } from '@reduxjs/toolkit'
import DragableList from '@renderer/components/DragableList' import DragableList from '@renderer/components/DragableList'
import Scrollbar from '@renderer/components/Scrollbar' import Scrollbar from '@renderer/components/Scrollbar'
import { useMCPServers } from '@renderer/hooks/useMCPServers' import { useMCPServers } from '@renderer/hooks/useMCPServers'
import { SettingTitle } from '@renderer/pages/settings'
import { MCPServer } from '@renderer/types' import { MCPServer } from '@renderer/types'
import { formatMcpError } from '@renderer/utils/error' import { formatMcpError } from '@renderer/utils/error'
import { Button, Dropdown, Empty, Switch, Tag } from 'antd' import { Button, Dropdown, Empty, Switch, Tag } from 'antd'
@ -12,7 +13,6 @@ import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router' import { useNavigate } from 'react-router'
import styled from 'styled-components' import styled from 'styled-components'
import { SettingTitle } from '..'
import AddMcpServerModal from './AddMcpServerModal' import AddMcpServerModal from './AddMcpServerModal'
import EditMcpJsonPopup from './EditMcpJsonPopup' import EditMcpJsonPopup from './EditMcpJsonPopup'
import SyncServersPopup from './SyncServersPopup' import SyncServersPopup from './SyncServersPopup'
@ -36,7 +36,7 @@ const McpServersList: FC = () => {
isActive: false isActive: false
} }
addMCPServer(newServer) addMCPServer(newServer)
navigate(`/settings/mcp/settings`, { state: { server: newServer } }) navigate(`/mcp-servers/settings`, { state: { server: newServer } })
window.message.success({ content: t('settings.mcp.addSuccess'), key: 'mcp-list' }) window.message.success({ content: t('settings.mcp.addSuccess'), key: 'mcp-list' })
}, [addMCPServer, navigate, t]) }, [addMCPServer, navigate, t])
@ -50,7 +50,7 @@ const McpServersList: FC = () => {
setIsAddModalVisible(false) setIsAddModalVisible(false)
window.message.success({ content: t('settings.mcp.addSuccess'), key: 'mcp-quick-add' }) window.message.success({ content: t('settings.mcp.addSuccess'), key: 'mcp-quick-add' })
// Optionally navigate to the new server's settings page // Optionally navigate to the new server's settings page
// navigate(`/settings/mcp/settings`, { state: { server } }) // navigate(`/mcp-servers/settings`, { state: { server } })
}, },
[addMCPServer, t] [addMCPServer, t]
) )
@ -117,9 +117,9 @@ const McpServersList: FC = () => {
</Button> </Button>
</ButtonGroup> </ButtonGroup>
</ListHeader> </ListHeader>
<DragableList style={{ width: '100%' }} list={mcpServers} onUpdate={updateMcpServers}> <DragableList list={mcpServers} onUpdate={updateMcpServers} listStyle={{ marginBottom: 12 }}>
{(server: MCPServer) => ( {(server: MCPServer) => (
<ServerCard key={server.id} onClick={() => navigate(`/settings/mcp/settings`, { state: { server } })}> <ServerCard key={server.id} onClick={() => navigate(`/mcp-servers/settings`, { state: { server } })}>
<ServerHeader> <ServerHeader>
<ServerName> <ServerName>
{server.logoUrl && <ServerLogo src={server.logoUrl} alt={`${server.name} logo`} />} {server.logoUrl && <ServerLogo src={server.logoUrl} alt={`${server.name} logo`} />}
@ -148,7 +148,7 @@ const McpServersList: FC = () => {
<Button <Button
icon={<Settings2 size={16} />} icon={<Settings2 size={16} />}
type="text" type="text"
onClick={() => navigate(`/settings/mcp/settings`, { state: { server } })} onClick={() => navigate(`/mcp-servers/settings`, { state: { server } })}
/> />
</StatusIndicator> </StatusIndicator>
</ServerHeader> </ServerHeader>
@ -207,6 +207,7 @@ const ListHeader = styled.div`
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin: 0 auto;
h2 { h2 {
font-size: 22px; font-size: 22px;
@ -217,8 +218,8 @@ const ListHeader = styled.div`
const ServerCard = styled.div` const ServerCard = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
border: 0.5px solid var(--color-border); border: 1px solid var(--color-border);
border-radius: var(--list-item-border-radius); border-radius: 12px;
padding: 10px 16px; padding: 10px 16px;
transition: all 0.2s ease; transition: all 0.2s ease;
background-color: var(--color-background); background-color: var(--color-background);

View File

@ -1,7 +1,8 @@
import { DeleteOutlined, SaveOutlined } from '@ant-design/icons' import { DeleteOutlined, SaveOutlined } from '@ant-design/icons'
import { useTheme } from '@renderer/context/ThemeProvider' import { useTheme } from '@renderer/context/ThemeProvider'
import { useMCPServer, useMCPServers } from '@renderer/hooks/useMCPServers' import { useMCPServer, useMCPServers } from '@renderer/hooks/useMCPServers'
import MCPDescription from '@renderer/pages/settings/MCPSettings/McpDescription' import MCPDescription from '@renderer/pages/mcp-servers/McpDescription'
import { SettingContainer, SettingDivider, SettingGroup, SettingTitle } from '@renderer/pages/settings'
import { MCPPrompt, MCPResource, MCPServer, MCPTool } from '@renderer/types' import { MCPPrompt, MCPResource, MCPServer, MCPTool } from '@renderer/types'
import { formatMcpError } from '@renderer/utils/error' import { formatMcpError } from '@renderer/utils/error'
import { Button, Flex, Form, Input, Radio, Select, Switch, Tabs } from 'antd' import { Button, Flex, Form, Input, Radio, Select, Switch, Tabs } from 'antd'
@ -12,7 +13,6 @@ import { useTranslation } from 'react-i18next'
import { useLocation, useNavigate } from 'react-router' import { useLocation, useNavigate } from 'react-router'
import styled from 'styled-components' import styled from 'styled-components'
import { SettingContainer, SettingDivider, SettingGroup, SettingTitle } from '..'
import MCPPromptsSection from './McpPrompt' import MCPPromptsSection from './McpPrompt'
import MCPResourcesSection from './McpResource' import MCPResourcesSection from './McpResource'
import MCPToolsSection from './McpTool' import MCPToolsSection from './McpTool'
@ -319,7 +319,7 @@ const McpSettings: React.FC = () => {
await window.api.mcp.removeServer(server) await window.api.mcp.removeServer(server)
deleteMCPServer(server.id) deleteMCPServer(server.id)
window.message.success({ content: t('settings.mcp.deleteSuccess'), key: 'mcp-list' }) window.message.success({ content: t('settings.mcp.deleteSuccess'), key: 'mcp-list' })
navigate('/settings/mcp') navigate('/mcp-servers')
} }
}) })
} catch (error: any) { } catch (error: any) {
@ -608,7 +608,7 @@ const McpSettings: React.FC = () => {
} }
return ( return (
<SettingContainer theme={theme} style={{ width: '100%', paddingTop: 55, backgroundColor: 'transparent' }}> <SettingContainer theme={theme} style={{ width: '100%', paddingTop: 0, backgroundColor: 'transparent' }}>
<SettingGroup style={{ marginBottom: 0, borderRadius: 'var(--list-item-border-radius)' }}> <SettingGroup style={{ marginBottom: 0, borderRadius: 'var(--list-item-border-radius)' }}>
<SettingTitle> <SettingTitle>
<Flex justify="space-between" align="center" gap={5} style={{ marginRight: 10 }}> <Flex justify="space-between" align="center" gap={5} style={{ marginRight: 10 }}>

View File

@ -1,7 +1,4 @@
import { NavbarRight } from '@renderer/components/app/Navbar'
import { HStack } from '@renderer/components/Layout' import { HStack } from '@renderer/components/Layout'
import { isLinux, isWindows } from '@renderer/config/constant'
import { useFullscreen } from '@renderer/hooks/useFullscreen'
import { Button, Dropdown, Menu, type MenuProps } from 'antd' import { Button, Dropdown, Menu, type MenuProps } from 'antd'
import { ChevronDown, Search } from 'lucide-react' import { ChevronDown, Search } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -74,29 +71,27 @@ export const McpSettingsNavbar = () => {
})) }))
return ( return (
<NavbarRight style={{ paddingRight: useFullscreen() ? '12px' : isWindows ? 150 : isLinux ? 120 : 12 }}> <HStack alignItems="center" gap={5} style={{ marginRight: 10 }}>
<HStack alignItems="center" gap={5}> <Button
size="small"
type="text"
onClick={() => navigate('/mcp-servers/npx-search')}
icon={<Search size={14} />}
className="nodrag"
style={{ fontSize: 13, height: 28, borderRadius: 20 }}>
{t('settings.mcp.searchNpx')}
</Button>
<Dropdown menu={{ items: resourceMenuItems }} trigger={['click']}>
<Button <Button
size="small" size="small"
type="text" type="text"
onClick={() => navigate('/settings/mcp/npx-search')}
icon={<Search size={14} />}
className="nodrag" className="nodrag"
style={{ fontSize: 13, height: 28, borderRadius: 20 }}> style={{ fontSize: 13, height: 28, borderRadius: 20, display: 'flex', alignItems: 'center' }}>
{t('settings.mcp.searchNpx')} {t('settings.mcp.findMore')}
<ChevronDown size={16} />
</Button> </Button>
<Dropdown menu={{ items: resourceMenuItems }} trigger={['click']}> </Dropdown>
<Button <InstallNpxUv mini />
size="small" </HStack>
type="text"
className="nodrag"
style={{ fontSize: 13, height: 28, borderRadius: 20, display: 'flex', alignItems: 'center' }}>
{t('settings.mcp.findMore')}
<ChevronDown size={16} />
</Button>
</Dropdown>
<InstallNpxUv mini />
</HStack>
</NavbarRight>
) )
} }

Some files were not shown because too many files have changed in this diff Show More