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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,10 +2,9 @@ import '@renderer/databases'
import store, { persistor } from '@renderer/store'
import { Provider } from 'react-redux'
import { HashRouter, Route, Routes } from 'react-router-dom'
import { HashRouter } from 'react-router-dom'
import { PersistGate } from 'redux-persist/integration/react'
import Sidebar from './components/app/Sidebar'
import TopViewContainer from './components/TopView'
import AntdProvider from './context/AntdProvider'
import { CodeStyleProvider } from './context/CodeStyleProvider'
@ -13,14 +12,8 @@ import { NotificationProvider } from './context/NotificationProvider'
import StyleSheetManager from './context/StyleSheetManager'
import { ThemeProvider } from './context/ThemeProvider'
import NavigationHandler from './handler/NavigationHandler'
import AgentsPage from './pages/agents/AgentsPage'
import AppsPage from './pages/apps/AppsPage'
import FilesPage from './pages/files/FilesPage'
import HomePage from './pages/home/HomePage'
import KnowledgePage from './pages/knowledge/KnowledgePage'
import PaintingsRoutePage from './pages/paintings/PaintingsRoutePage'
import SettingsPage from './pages/settings/SettingsPage'
import TranslatePage from './pages/translate/TranslatePage'
import MainSidebar from './pages/home/MainSidebar/MainSidebar'
import Routes from './Routes'
function App(): React.ReactElement {
return (
@ -34,17 +27,8 @@ function App(): React.ReactElement {
<TopViewContainer>
<HashRouter>
<NavigationHandler />
<Sidebar />
<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>
<MainSidebar />
<Routes />
</HashRouter>
</TopViewContainer>
</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 {
max-width: calc(100vw - var(--sidebar-width));
.ant-drawer-content-wrapper {
box-shadow: none;
}
@ -33,7 +32,7 @@
position: absolute;
-webkit-app-region: drag;
min-height: calc(var(--navbar-height) + 0.5px);
width: calc(100vw - var(--sidebar-width));
width: 100%;
margin-top: -0.5px;
border-bottom: none;
}

View File

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

View File

@ -1,6 +1,14 @@
#content-container {
background-color: var(--color-background);
border-top: 0.5px solid var(--color-border);
border-top-left-radius: 10px;
border-left: 0.5px solid var(--color-border);
}
.group-container {
.context-menu-container {
width: 100%;
}
}
.context-menu-container {
max-width: 100%;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -229,8 +229,8 @@ const CodeEditor = ({
style={{
...style,
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 {
children: React.ReactNode
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 [contextMenuPosition, setContextMenuPosition] = useState<{ x: number; y: number } | null>(null)
const [selectedText, setSelectedText] = useState<string>('')
@ -67,7 +66,7 @@ const ContextMenu: React.FC<ContextMenuProps> = ({ children, onContextMenu, styl
]
return (
<ContextContainer onContextMenu={handleContextMenu} className="context-menu-container" style={style}>
<ContextContainer onContextMenu={handleContextMenu} className="context-menu-container">
{contextMenuPosition && (
<Dropdown
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.dragHandleProps}
style={{
marginBottom: 8,
...listStyle,
...provided.draggableProps.style,
marginBottom: 8
...provided.draggableProps.style
}}>
{children(item, index)}
</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>
)
}
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%'}
maskClosable={false}
closeIcon={null}
style={{
marginLeft: 'var(--sidebar-width)',
backgroundColor: window.root.style.background
}}>
style={{ backgroundColor: window.root.style.background }}>
{!isReady && (
<EmptyView>
<Avatar
@ -418,7 +415,7 @@ const TitleContainer = styled.div`
display: flex;
flex-direction: row;
align-items: center;
padding-left: ${isMac ? '20px' : '10px'};
padding-left: ${isMac ? '80px' : '10px'};
padding-right: 10px;
position: absolute;
top: 0;

View File

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

View File

@ -14,14 +14,7 @@ interface Props {
position: 'left' | 'right'
}
const FloatingSidebar: FC<Props> = ({
children,
activeAssistant,
setActiveAssistant,
activeTopic,
setActiveTopic,
position = 'left'
}) => {
const FloatingSidebar: FC<Props> = ({ children, activeAssistant, setActiveAssistant, activeTopic, setActiveTopic }) => {
const [open, setOpen] = useState(false)
useHotkeys('esc', () => {
@ -45,12 +38,11 @@ const FloatingSidebar: FC<Props> = ({
const content = (
<PopoverContent maxHeight={maxHeight}>
<HomeTabs
tab="assistants"
activeAssistant={activeAssistant}
activeTopic={activeTopic}
setActiveAssistant={setActiveAssistant}
setActiveTopic={setActiveTopic}
position={position}
forceToSeeAllTab={true}
style={{
background: 'transparent',
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 onOk = () => {
resolve(true)
setOpen(false)
}
const onCancel = () => {
resolve(false)
setOpen(false)
}
const onClose = () => {
resolve({})
TopView.hide(TopViewKey)
}
TemplatePopup.hide = onCancel
@ -51,16 +53,7 @@ export default class TemplatePopup {
}
static show(props: ShowParams) {
return new Promise<any>((resolve) => {
TopView.show(
<PopupContainer
{...props}
resolve={(v) => {
resolve(v)
TopView.hide(TopViewKey)
}}
/>,
TopViewKey
)
TopView.show(<PopupContainer {...props} resolve={resolve} />, 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()}`) })}
arrow>
<ToolbarButton onClick={handleTranslate} disabled={disabled || isTranslating} style={style} type="text">
{isTranslating ? <LoadingOutlined spin /> : <Languages size={18} />}
{isTranslating ? <LoadingOutlined spin /> : <Languages size={16} />}
</ToolbarButton>
</Tooltip>
)

View File

@ -1,24 +1,16 @@
import { isLinux, isMac, isWindows } from '@renderer/config/constant'
import { useFullscreen } from '@renderer/hooks/useFullscreen'
import useNavBackgroundColor from '@renderer/hooks/useNavBackgroundColor'
import { Button } from 'antd'
import { CircleArrowLeft, X } from 'lucide-react'
import type { FC, PropsWithChildren } from 'react'
import type { HTMLAttributes } from 'react'
import { useNavigate } from 'react-router-dom'
import styled from 'styled-components'
type Props = PropsWithChildren & HTMLAttributes<HTMLDivElement>
export const Navbar: FC<Props> = ({ children, ...props }) => {
const backgroundColor = useNavBackgroundColor()
return (
<NavbarContainer {...props} style={{ backgroundColor }}>
{children}
</NavbarContainer>
)
}
export const NavbarLeft: FC<Props> = ({ children, ...props }) => {
return <NavbarLeftContainer {...props}>{children}</NavbarLeftContainer>
return <NavbarContainer {...props}>{children}</NavbarContainer>
}
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 }) => {
const isFullscreen = useFullscreen()
return (
<NavbarMainContainer {...props} $isFullscreen={isFullscreen}>
<CloseIcon />
{children}
<MacCloseIcon />
</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`
min-width: 100%;
display: flex;
flex-direction: row;
min-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;
`
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);
background-color: var(--color-background);
`
const NavbarRightContainer = styled.div<{ $isFullscreen: boolean }>`
@ -78,7 +81,7 @@ const NavbarRightContainer = styled.div<{ $isFullscreen: boolean }>`
display: flex;
align-items: center;
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;
`
@ -87,9 +90,26 @@ const NavbarMainContainer = styled.div<{ $isFullscreen: boolean }>`
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: 0 ${isMac ? '20px' : 0};
padding-left: ${isMac ? '70px' : '10px'};
font-weight: bold;
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 { AppLogo, UserAvatar } from '@renderer/config/env'
import { AppLogo } from '@renderer/config/env'
import { useTheme } from '@renderer/context/ThemeProvider'
import useAvatar from '@renderer/hooks/useAvatar'
import { useFullscreen } from '@renderer/hooks/useFullscreen'
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
import { useMinapps } from '@renderer/hooks/useMinapps'
@ -11,9 +9,8 @@ import { modelGenerating, useRuntime } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings'
import i18n from '@renderer/i18n'
import { ThemeMode } from '@renderer/types'
import { isEmoji } from '@renderer/utils'
import type { MenuProps } from 'antd'
import { Avatar, Dropdown, Tooltip } from 'antd'
import { Dropdown, Tooltip } from 'antd'
import {
CircleHelp,
FileSearch,
@ -35,7 +32,6 @@ import styled from 'styled-components'
import DragableList from '../DragableList'
import MinAppIcon from '../Icons/MinAppIcon'
import UserPopup from '../Popups/UserPopup'
const Sidebar: FC = () => {
const { hideMinappPopup, openMinapp } = useMinappPopup()
@ -47,11 +43,8 @@ const Sidebar: FC = () => {
const navigate = useNavigate()
const { theme, settedTheme, toggleTheme } = useTheme()
const avatar = useAvatar()
const { t } = useTranslation()
const onEditUser = () => UserPopup.show()
const backgroundColor = useNavBackgroundColor()
const showPinnedApps = pinned.length > 0 && sidebarIcons.visible.includes('minapp')
@ -79,13 +72,6 @@ const Sidebar: FC = () => {
$isFullscreen={isFullscreen}
id="app-sidebar"
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>
<Menus onClick={hideMinappPopup}>
<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`
display: flex;
flex: 1;

View File

@ -47,7 +47,7 @@ As [role name], with [list skills], strictly adhering to [list constraints], usi
`
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
export const SEARCH_SUMMARY_PROMPT = `

View File

@ -1,14 +1,21 @@
import NavigationService from '@renderer/services/NavigationService'
import { useAppSelector } from '@renderer/store'
import { useEffect } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import { useLocation, useNavigate } from 'react-router-dom'
const NavigationHandler: React.FC = () => {
const location = useLocation()
const navigate = useNavigate()
const showSettingsShortcutEnabled = useAppSelector(
(state) => state.shortcuts.shortcuts.find((s) => s.key === 'show_settings')?.enabled
)
useEffect(() => {
NavigationService.setNavigate(navigate)
}, [navigate])
useHotkeys(
'meta+, ! ctrl+,',
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 { useSettings } from '@renderer/hooks/useSettings' // 使用设置中的值
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { useAppDispatch } from '@renderer/store'
import {
setCurrentMinappId,
@ -32,6 +33,7 @@ export const useMinappPopup = () => {
/** Open a minapp (popup shows and minapp loaded) */
const openMinapp = useCallback(
(app: MinAppType, keepAlive: boolean = false) => {
EventEmitter.emit(EVENT_NAMES.OPEN_MINAPP, app)
if (keepAlive) {
// 如果小程序已经打开,只切换显示
if (openedKeepAliveMinapps.some((item) => item.id === app.id)) {

View File

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

View File

@ -1,47 +1,16 @@
import db from '@renderer/databases'
import i18n from '@renderer/i18n'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { deleteMessageFiles } from '@renderer/services/MessagesService'
import store from '@renderer/store'
import { updateTopic } from '@renderer/store/assistants'
import { loadTopicMessagesThunk } from '@renderer/store/thunk/messageThunk'
import { Assistant, Topic } from '@renderer/types'
import { findMainTextBlocks } from '@renderer/utils/messageUtils/find'
import { find, isEmpty } from 'lodash'
import { useEffect, useState } from 'react'
import { isEmpty } from 'lodash'
import { useAssistant } from './useAssistant'
import { getStoreSetting } from './useSettings'
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) {
return assistant?.topics.find((topic) => topic.id === topicId)
}
@ -86,7 +55,6 @@ export const autoRenameTopic = async (assistant: Assistant, topicId: string) =>
.substring(0, 50)
if (topicName) {
const data = { ...topic, name: topicName } as Topic
_setActiveTopic(data)
store.dispatch(updateTopic({ assistantId: assistant.id, topic: data }))
}
return
@ -97,7 +65,6 @@ export const autoRenameTopic = async (assistant: Assistant, topicId: string) =>
const summaryText = await fetchMessagesSummary({ messages: topic.messages, assistant })
if (summaryText) {
const data = { ...topic, name: summaryText }
_setActiveTopic(data)
store.dispatch(updateTopic({ assistantId: assistant.id, topic: data }))
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -918,7 +918,6 @@
"about.updateNotAvailable": "Tu software ya está actualizado",
"about.website.button": "Ver",
"about.website.title": "Sitio web oficial",
"advanced.auto_switch_to_topics": "Cambiar automáticamente a temas",
"advanced.title": "Configuración avanzada",
"assistant": "Asistente predeterminado",
"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.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.cherrycss": "Obtener desde cherrycss.com",
"display.custom.css.placeholder": "/* Escribe tu CSS personalizado aquí */",
@ -1303,7 +1301,6 @@
"advancedSettings": "Configuración avanzada"
},
"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_popover_trigger": "Desencadenante de detalles de cuadrícula",
"messages.grid_popover_trigger.click": "Mostrar al hacer clic",
@ -1485,9 +1482,6 @@
"theme.window.style.title": "Estilo de ventana",
"theme.window.style.transparent": "Ventana transparente",
"title": "Configuración",
"topic.position": "Posición del tema",
"topic.position.left": "Izquierda",
"topic.position.right": "Derecha",
"topic.show.time": "Mostrar tiempo del tema",
"tray.onclose": "Minimizar a la bandeja al cerrar",
"tray.show": "Mostrar bandera del sistema",
@ -1610,7 +1604,6 @@
"assistant.icon.type.none": "No mostrar",
"general.auto_check_update.title": "Actualización automática",
"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_delete_model": "Habilitar la eliminación con la tecla de borrado para modelos/archivos adjuntos introducidos",
"messages.math_engine.none": "Ninguno",

View File

@ -917,7 +917,6 @@
"about.updateNotAvailable": "Votre logiciel est déjà à jour",
"about.website.button": "Visiter le site web",
"about.website.title": "Site web officiel",
"advanced.auto_switch_to_topics": "Basculer automatiquement vers les sujets",
"advanced.title": "Paramètres avancés",
"assistant": "Assistant par défaut",
"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.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.cherrycss": "Obtenir depuis cherrycss.com",
"display.custom.css.placeholder": "/* Écrire votre CSS personnalisé ici */",
@ -1304,7 +1302,6 @@
"advancedSettings": "Расширенные настройки"
},
"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_popover_trigger": "Déclencheur de popover de la grille",
"messages.grid_popover_trigger.click": "Afficher au clic",
@ -1486,9 +1483,6 @@
"theme.window.style.title": "Style de fenêtre",
"theme.window.style.transparent": "Fenêtre transparente",
"title": "Paramètres",
"topic.position": "Position du sujet",
"topic.position.left": "Gauche",
"topic.position.right": "Droite",
"topic.show.time": "Afficher l'heure du sujet",
"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",
@ -1611,7 +1605,6 @@
"assistant.icon.type.none": "Ne pas afficher",
"general.auto_check_update.title": "Mise à jour automatique",
"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_delete_model": "Activer la touche Supprimer pour effacer le modèle/pièce jointe saisie",
"messages.math_engine.none": "Aucun",

View File

@ -919,7 +919,6 @@
"about.updateNotAvailable": "Seu software já está atualizado",
"about.website.button": "Ver",
"about.website.title": "Site oficial",
"advanced.auto_switch_to_topics": "Alternar automaticamente para tópicos",
"advanced.title": "Configurações avançadas",
"assistant": "Assistente padrão",
"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.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.cherrycss": "Obter do cherrycss.com",
"display.custom.css.placeholder": "/* Escreva seu CSS personalizado aqui */",
@ -1306,7 +1304,6 @@
"advancedSettings": "Configurações Avançadas"
},
"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_popover_trigger": "Disparador de detalhes da grade",
"messages.grid_popover_trigger.click": "Clique para mostrar",
@ -1488,9 +1485,6 @@
"theme.window.style.title": "Estilo de janela",
"theme.window.style.transparent": "Janela transparente",
"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",
"tray.onclose": "Minimizar para bandeja ao fechar",
"tray.show": "Mostrar ícone de bandeja",
@ -1613,7 +1607,6 @@
"assistant.icon.type.none": "Não mostrar",
"general.auto_check_update.title": "Atualização automática",
"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_delete_model": "Ativar tecla de exclusão para remover modelos/anexos inseridos",
"messages.math_engine.none": "Nenhum",

View File

@ -1,5 +1,5 @@
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 ListItem from '@renderer/components/ListItem'
import Scrollbar from '@renderer/components/Scrollbar'
@ -152,7 +152,7 @@ const AgentsPage: FC = () => {
return (
<Container>
<Navbar>
<NavbarMain>
<NavbarCenter style={{ borderRight: 'none', justifyContent: 'space-between' }}>
{t('agents.title')}
<Input
@ -169,9 +169,9 @@ const AgentsPage: FC = () => {
onChange={(e) => setSearchInput(e.target.value)}
onPressEnter={handleSearch}
/>
<div style={{ width: 80 }} />
<div style={{ width: 1 }} />
</NavbarCenter>
</Navbar>
</NavbarMain>
<Main id="content-container">
<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 { 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 { useTranslation } from 'react-i18next'
import { useLocation } from 'react-router'
@ -41,8 +41,8 @@ const AppsPage: FC = () => {
return (
<Container onContextMenu={handleContextMenu}>
<Navbar>
<NavbarMain>
<NavbarMain>
<NavbarCenter>
{t('minapp.title')}
<Input
placeholder={t('common.search')}
@ -50,10 +50,7 @@ const AppsPage: FC = () => {
style={{
width: '30%',
height: 28,
borderRadius: 15,
position: 'absolute',
left: '50vw',
transform: 'translateX(-50%)'
borderRadius: 15
}}
size="small"
variant="filled"
@ -65,11 +62,11 @@ const AppsPage: FC = () => {
<Button
type="text"
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)}
/>
</NavbarMain>
</Navbar>
</NavbarCenter>
</NavbarMain>
<ContentContainer id="content-container">
{isSettingsOpen && <MiniAppSettings />}
{!isSettingsOpen && (

View File

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

View File

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

View File

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

View File

@ -1,12 +1,10 @@
import { ContentSearch, ContentSearchRef } from '@renderer/components/ContentSearch'
import MultiSelectActionPopup from '@renderer/components/Popups/MultiSelectionPopup'
import { QuickPanelProvider } from '@renderer/components/QuickPanel'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { useChat } from '@renderer/hooks/useChat'
import { useChatContext } from '@renderer/hooks/useChatContext'
import { useSettings } from '@renderer/hooks/useSettings'
import { useShortcut } from '@renderer/hooks/useShortcuts'
import { useShowTopics } from '@renderer/hooks/useStore'
import { Assistant, Topic } from '@renderer/types'
import { Flex } from 'antd'
import { debounce } from 'lodash'
import React, { FC, useMemo, useState } from 'react'
@ -15,31 +13,20 @@ import styled from 'styled-components'
import Inputbar from './Inputbar/Inputbar'
import Messages from './Messages/Messages'
import Tabs from './Tabs'
interface Props {
assistant: Assistant
activeTopic: Topic
setActiveTopic: (topic: Topic) => void
setActiveAssistant: (assistant: Assistant) => void
}
const Chat: FC<Props> = (props) => {
const { assistant } = useAssistant(props.assistant.id)
const { topicPosition, messageStyle, showAssistants } = useSettings()
const { showTopics } = useShowTopics()
const { isMultiSelectMode } = useChatContext(props.activeTopic)
const Chat: FC = () => {
const { activeAssistant, activeTopic, setActiveTopic } = useChat()
const { messageStyle, showAssistants } = useSettings()
const { isMultiSelectMode } = useChatContext(activeTopic)
const mainRef = React.useRef<HTMLDivElement>(null)
const contentSearchRef = React.useRef<ContentSearchRef>(null)
const [filterIncludeUser, setFilterIncludeUser] = useState(false)
const maxWidth = useMemo(() => {
const showRightTopics = showTopics && topicPosition === 'right'
const minusAssistantsWidth = showAssistants ? '- var(--assistants-width)' : ''
const minusRightTopicsWidth = showRightTopics ? '- var(--assistants-width)' : ''
return `calc(100vw - var(--sidebar-width) ${minusAssistantsWidth} ${minusRightTopicsWidth})`
}, [showAssistants, showTopics, topicPosition])
return `calc(100vw - ${minusAssistantsWidth})`
}, [showAssistants])
useHotkeys('esc', () => {
contentSearchRef.current?.disable()
@ -116,36 +103,24 @@ const Chat: FC<Props> = (props) => {
onIncludeUserChange={userOutlinedItemClickHandler}
/>
<Messages
key={props.activeTopic.id}
assistant={assistant}
topic={props.activeTopic}
setActiveTopic={props.setActiveTopic}
key={activeTopic.id}
assistant={activeAssistant}
topic={activeTopic}
setActiveTopic={setActiveTopic}
onComponentUpdate={messagesComponentUpdateHandler}
onFirstUpdate={messagesComponentFirstUpdateHandler}
/>
<QuickPanelProvider>
<Inputbar assistant={assistant} setActiveTopic={props.setActiveTopic} topic={props.activeTopic} />
{isMultiSelectMode && <MultiSelectActionPopup topic={props.activeTopic} />}
<Inputbar />
{isMultiSelectMode && <MultiSelectActionPopup topic={activeTopic} />}
</QuickPanelProvider>
</Main>
{topicPosition === 'right' && showTopics && (
<Tabs
activeAssistant={assistant}
activeTopic={props.activeTopic}
setActiveAssistant={props.setActiveAssistant}
setActiveTopic={props.setActiveTopic}
position="right"
/>
)}
</Container>
)
}
const Container = styled.div`
display: flex;
flex-direction: row;
height: 100%;
flex: 1;
`
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 { useActiveTopic } from '@renderer/hooks/useTopic'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import NavigationService from '@renderer/services/NavigationService'
import { Assistant } from '@renderer/types'
import { FC, useEffect, useState } from 'react'
import { useLocation, useNavigate } from 'react-router-dom'
import { FC, useEffect } from 'react'
import styled from 'styled-components'
import Chat from './Chat'
import Navbar from './Navbar'
import HomeTabs from './Tabs'
let _activeAssistant: Assistant
import ChatNavbar from './ChatNavbar'
const HomePage: FC = () => {
const { assistants } = useAssistants()
const navigate = useNavigate()
const location = useLocation()
const state = location.state
const [activeAssistant, setActiveAssistant] = useState(state?.assistant || _activeAssistant || assistants[0])
const { activeTopic, setActiveTopic } = useActiveTopic(activeAssistant, state?.topic)
const { showAssistants, showTopics, topicPosition } = useSettings()
_activeAssistant = activeAssistant
useEffect(() => {
NavigationService.setNavigate(navigate)
}, [navigate])
useEffect(() => {
state?.assistant && setActiveAssistant(state?.assistant)
state?.topic && setActiveTopic(state?.topic)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [state])
useEffect(() => {
const unsubscribe = EventEmitter.on(EVENT_NAMES.SWITCH_ASSISTANT, (assistantId: string) => {
const newAssistant = assistants.find((a) => a.id === assistantId)
if (newAssistant) {
setActiveAssistant(newAssistant)
}
})
return () => {
unsubscribe()
}
}, [assistants, setActiveAssistant])
useEffect(() => {
const canMinimize = topicPosition == 'left' ? !showAssistants : !showAssistants && !showTopics
window.api.window.setMinimumSize(canMinimize ? 520 : 1080, 600)
window.api.window.setMinimumSize(showAssistants ? 1080 : 520, 600)
return () => {
window.api.window.resetMinimumSize()
@ -61,45 +18,22 @@ const HomePage: FC = () => {
return (
<Container id="home-page">
<Navbar
activeAssistant={activeAssistant}
activeTopic={activeTopic}
setActiveTopic={setActiveTopic}
setActiveAssistant={setActiveAssistant}
position="left"
/>
<ChatNavbar />
<ContentContainer id="content-container">
{showAssistants && (
<HomeTabs
activeAssistant={activeAssistant}
activeTopic={activeTopic}
setActiveAssistant={setActiveAssistant}
setActiveTopic={setActiveTopic}
position="left"
/>
)}
<Chat
assistant={activeAssistant}
activeTopic={activeTopic}
setActiveTopic={setActiveTopic}
setActiveAssistant={setActiveAssistant}
/>
<Chat />
</ContentContainer>
</Container>
)
}
const Container = styled.div`
min-width: 0;
display: flex;
flex: 1;
flex-direction: column;
max-width: calc(100vw - var(--sidebar-width));
`
const ContentContainer = styled.div`
display: flex;
flex: 1;
flex-direction: row;
overflow: hidden;
`

View File

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

View File

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

View File

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

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}
</CodeBlockView>
) : (
<code className={className} style={{ textWrap: 'wrap' }}>
<code className={className} style={{ textWrap: 'wrap', fontSize: '95%', padding: '2px 4px' }}>
{children}
</code>
)

View File

@ -93,15 +93,19 @@ const Markdown: FC<Props> = ({ block }) => {
} as Partial<Components>
}, [onSaveCodeBlock])
if (messageContent.includes('<style>')) {
components.style = MarkdownShadowDOMRenderer as any
}
const urlTransform = useCallback((value: string) => {
if (value.startsWith('data:image/png') || value.startsWith('data:image/jpeg')) return 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 (
<ReactMarkdown
rehypePlugins={rehypePlugins}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -21,6 +21,7 @@ import MessageEditor from './MessageEditor'
import MessageErrorBoundary from './MessageErrorBoundary'
import MessageHeader from './MessageHeader'
import MessageMenubar from './MessageMenubar'
import MessageTokens from './MessageTokens'
interface Props {
message: Message
@ -98,7 +99,7 @@ const MessageItem: FC<Props> = ({
const isAssistantMessage = message.role === 'assistant'
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 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 (
<MessageContainer
key={message.id}
@ -138,100 +155,35 @@ const MessageItem: FC<Props> = ({
'message-user': !isAssistantMessage
})}
ref={messageContainerRef}
style={{
...style,
justifyContent: isBubbleStyle ? (isAssistantMessage ? 'flex-start' : 'flex-end') : undefined,
flex: isBubbleStyle ? undefined : 1
}}>
{isEditing && (
<ContextMenu
style={{ ...style, alignItems: isBubbleStyle ? (isAssistantMessage ? undefined : 'end') : undefined }}>
<ContextMenu>
<MessageHeader message={message} assistant={assistant} model={model} key={getModelUniqId(model)} />
<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={{
display: 'flex',
flexDirection: 'column',
alignSelf: isAssistantMessage ? 'flex-start' : 'flex-end',
width: isBubbleStyle ? '70%' : '100%'
fontFamily: messageFont === 'serif' ? 'var(--font-family-serif)' : 'var(--font-family)',
fontSize,
background: messageBackground,
overflowY: 'visible',
maxWidth: narrowMode ? 760 : undefined
}}>
<MessageHeader
message={message}
assistant={assistant}
model={model}
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 && (
<MessageErrorBoundary>
<MessageContent message={message} />
</MessageErrorBoundary>
{showMenubar && (
<MessageFooter
className="MessageFooter"
style={{
borderTop: messageBorder,
flexDirection: !isAssistantMessage ? 'row-reverse' : undefined
border: messageBorder,
flexDirection: isLastMessage || isBubbleStyle ? 'row-reverse' : undefined
}}>
<MessageTokens message={message} isLastMessage={isLastMessage} />
<MessageMenubar
message={message}
assistant={assistant}
@ -246,8 +198,8 @@ const MessageItem: FC<Props> = ({
/>
</MessageFooter>
)}
</ContextMenu>
)}
</MessageContentContainer>
</ContextMenu>
</MessageContainer>
)
}
@ -262,10 +214,10 @@ const getMessageBackground = (isBubbleStyle: boolean, isAssistantMessage: boolea
const MessageContainer = styled.div`
display: flex;
width: 100%;
flex-direction: column;
position: relative;
transition: background-color 0.3s ease;
padding: 0 20px;
padding: 0 24px;
transform: translateZ(0);
will-change: transform;
&.message-highlight {
@ -305,12 +257,12 @@ const MessageFooter = styled.div`
align-items: center;
padding: 2px 0;
margin-top: 2px;
border-top: 0.5px dotted var(--color-border);
gap: 20px;
`
const NewContextMessage = styled.div`
cursor: pointer;
flex: 1;
`
export default memo(MessageItem)

View File

@ -1,4 +1,3 @@
import { DownOutlined } from '@ant-design/icons'
import EmojiAvatar from '@renderer/components/Avatar/EmojiAvatar'
import { APP_NAME, AppLogo, isLocalAi } from '@renderer/config/env'
import { getModelLogo } from '@renderer/config/models'
@ -14,6 +13,7 @@ import type { Message } from '@renderer/types/newMessage'
import { isEmoji, removeLeadingEmoji } from '@renderer/utils'
import { getMainTextContent } from '@renderer/utils/messageUtils/find'
import { Avatar } from 'antd'
import { CircleChevronDown } from 'lucide-react'
import { type FC, useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
@ -183,16 +183,9 @@ const MessageAnchorLine: FC<MessageLineProps> = ({ messages }) => {
opacity: mouseY ? 0.5 + calculateValueByDistance('bottom-anchor', 1) : 0.6
}}
onClick={scrollToBottom}>
<MessageItemContainer
style={{ transform: `scale(${1 + calculateValueByDistance('bottom-anchor', 1)})` }}></MessageItemContainer>
<Avatar
icon={<DownOutlined style={{ color: theme === 'dark' ? 'var(--color-text)' : 'var(--color-primary)' }} />}
<CircleChevronDown
size={10 + calculateValueByDistance('bottom-anchor', 20)}
style={{
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
}}
style={{ color: theme === 'dark' ? 'var(--color-text)' : 'var(--color-primary)' }}
/>
</MessageItem>
{messages.map((message, index) => {
@ -203,6 +196,8 @@ const MessageAnchorLine: FC<MessageLineProps> = ({ messages }) => {
const username = removeLeadingEmoji(getUserName(message))
const content = getMainTextContent(message)
if (message.type === 'clear') return null
return (
<MessageItem
key={message.id}
@ -262,7 +257,6 @@ const MessageItemContainer = styled.div`
justify-content: space-between;
text-align: right;
gap: 4px;
text-shadow: 0 0 2px rgba(255, 255, 255, 0.5);
opacity: 0;
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 { FileType } from '@renderer/types'
import type { FileMessageBlock } from '@renderer/types/newMessage'
import { Upload } from 'antd'
import { FC } from 'react'
import styled from 'styled-components'
@ -8,76 +23,88 @@ interface Props {
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 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) {
return null
}
// 由图片块代替
// if (block.file.type === FileTypes.IMAGE) {
// return (
// <Container style={{ marginBottom: 8 }}>
// <Image
// src={FileManager.getFileUrl(block.file)}
// key={block.file.id}
// width="33%"
// preview={{
// toolbarRender: (
// _,
// {
// transform: { scale },
// actions: { onFlipY, onFlipX, onRotateLeft, onRotateRight, onZoomOut, onZoomIn, onReset }
// }
// ) => (
// <ToobarWrapper size={12} className="toolbar-wrapper">
// <SwapOutlined rotate={90} onClick={onFlipY} />
// <SwapOutlined onClick={onFlipX} />
// <RotateLeftOutlined onClick={onRotateLeft} />
// <RotateRightOutlined onClick={onRotateRight} />
// <ZoomOutOutlined disabled={scale === 1} onClick={onZoomOut} />
// <ZoomInOutlined disabled={scale === 50} onClick={onZoomIn} />
// <UndoOutlined onClick={onReset} />
// <CopyOutlined onClick={() => handleCopyImage(block.file)} />
// <DownloadOutlined onClick={() => download(FileManager.getFileUrl(block.file))} />
// </ToobarWrapper>
// )
// }}
// />
// </Container>
// )
// }
const MAX_FILENAME_DISPLAY_LENGTH = 20
function truncateFileName(name: string, maxLength: number = MAX_FILENAME_DISPLAY_LENGTH) {
if (name.length <= maxLength) return name
return name.slice(0, maxLength - 3) + '...'
}
const getFileIcon = (type?: string) => {
if (!type) return <FileUnknownFilled />
const ext = type.toLowerCase()
if (['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'].includes(ext)) {
return <FileImageFilled />
}
if (['.doc', '.docx'].includes(ext)) {
return <FileWordFilled />
}
if (['.xls', '.xlsx'].includes(ext)) {
return <FileExcelFilled />
}
if (['.ppt', '.pptx'].includes(ext)) {
return <FilePptFilled />
}
if (ext === '.pdf') {
return <FilePdfFilled />
}
if (['.md', '.markdown'].includes(ext)) {
return <FileMarkdownFilled />
}
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 (
<Container style={{ marginTop: 2, marginBottom: 8 }} className="message-attachments">
<StyledUpload
listType="text"
disabled
fileList={[
{
uid: block.file.id,
url: 'file://' + FileManager.getSafePath(block.file),
status: 'done' as const,
name: FileManager.formatFileName(block.file)
}
]}
/>
<CustomTag key={block.file.id} icon={getFileIcon(block.file.ext)} color="#37a5aa">
<FileNameRender file={block.file} />
</CustomTag>
</Container>
)
}
@ -89,23 +116,11 @@ const Container = styled.div`
margin-top: 8px;
`
// const Image = styled(AntdImage)`
// border-radius: 10px;
// `
// 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;
// }
// `
const FileName = styled.span`
cursor: pointer;
&:hover {
text-decoration: underline;
}
`
export default MessageAttachments

View File

@ -14,7 +14,7 @@ const MessageContent: React.FC<Props> = ({ message }) => {
return (
<>
{!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>)}
</Flex>
)}

View File

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

View File

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

View File

@ -7,6 +7,7 @@ import {
} from '@ant-design/icons'
import { HStack } from '@renderer/components/Layout'
import { useMessageOperations } from '@renderer/hooks/useMessageOperations'
import { useSettings } from '@renderer/hooks/useSettings'
import { MultiModelMessageStyle } from '@renderer/store/settings'
import type { Topic } from '@renderer/types'
import type { Message } from '@renderer/types/newMessage'
@ -37,6 +38,7 @@ const MessageGroupMenuBar: FC<Props> = ({
}) => {
const { t } = useTranslation()
const { deleteGroupMessages } = useMessageOperations(topic)
const { showAssistants } = useSettings()
const handleDeleteGroup = async () => {
const askId = messages[0]?.askId
@ -53,8 +55,12 @@ const MessageGroupMenuBar: FC<Props> = ({
onOk: () => deleteGroupMessages(askId)
})
}
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' }}>
<LayoutContainer>
{['fold', 'vertical', 'horizontal', 'grid'].map((layout) => (
@ -104,7 +110,7 @@ const GroupMenuBar = styled.div<{ $layout: MultiModelMessageStyle }>`
margin: 0 20px;
padding: 6px 10px;
border-radius: 6px;
margin-top: 10px;
margin-top: 6px;
justify-content: space-between;
overflow: hidden;
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 styled from 'styled-components'
import MessageTokens from './MessageTokens'
interface Props {
message: Message
assistant: Assistant
model?: Model
index: number | undefined
}
const getAvatarSource = (isLocalAi: boolean, modelId: string | undefined) => {
@ -31,7 +28,7 @@ const getAvatarSource = (isLocalAi: boolean, modelId: string | 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 { theme } = useTheme()
const { userName, sidebarIcons } = useSettings()
@ -55,11 +52,9 @@ const MessageHeader: FC<Props> = memo(({ assistant, model, message, index }) =>
const isAssistantMessage = message.role === 'assistant'
const showMinappIcon = sidebarIcons.visible.includes('minapp')
const { showTokens } = useSettings()
const avatarName = useMemo(() => firstLetter(assistant?.name).toUpperCase(), [assistant?.name])
const username = useMemo(() => removeLeadingEmoji(getUserName()), [getUserName])
const isLastMessage = index === 0
const showMiniApp = useCallback(() => {
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}
</UserName>
<InfoWrap
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>
<MessageTime>{dayjs(message?.updatedAt ?? message.createdAt).format('MM/DD HH:mm')}</MessageTime>
</UserWrap>
</AvatarWrapper>
</Container>
@ -152,19 +140,6 @@ const UserWrap = styled.div`
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 }>`
font-size: 14px;
font-weight: 600;

View File

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

View File

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

View File

@ -38,7 +38,6 @@ import ChatNavigation from './ChatNavigation'
import MessageAnchorLine from './MessageAnchorLine'
import MessageGroup from './MessageGroup'
import NarrowLayout from './NarrowLayout'
import Prompt from './Prompt'
interface MessagesProps {
assistant: Assistant
@ -53,7 +52,7 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic, o
`topic-${topic.id}`
)
const { t } = useTranslation()
const { showPrompt, messageNavigation } = useSettings()
const { messageNavigation } = useSettings()
const { updateTopic, addTopic } = useAssistant(assistant.id)
const dispatch = useAppDispatch()
const [displayMessages, setDisplayMessages] = useState<Message[]>([])
@ -271,7 +270,7 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic, o
id="messages"
className="messages-container"
ref={scrollContainerRef}
style={{ position: 'relative', paddingTop: showPrompt ? 10 : 0 }}
style={{ position: 'relative' }}
key={assistant.id}
onScroll={handleScrollPosition}>
<NarrowLayout style={{ display: 'flex', flexDirection: 'column-reverse' }}>
@ -299,7 +298,6 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic, o
)}
</ScrollContainer>
</InfiniteScroll>
{showPrompt && <Prompt assistant={assistant} key={assistant.prompt} topic={topic} />}
</NarrowLayout>
{messageNavigation === 'anchor' && <MessageAnchorLine messages={displayMessages} />}
{messageNavigation === 'buttons' && <ChatNavigation containerId="messages" />}
@ -372,7 +370,6 @@ const MessagesContainer = styled(Scrollbar)<ContainerProps>`
flex-direction: column-reverse;
padding: 10px 0 20px;
overflow-x: hidden;
background-color: var(--color-background);
z-index: 1;
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 { useTags } from '@renderer/hooks/useTags'
import { Assistant, AssistantsSortType } from '@renderer/types'
import { Divider, Tooltip } from 'antd'
import { Tooltip } from 'antd'
import { FC, useCallback, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
@ -80,7 +80,7 @@ const Assistants: FC<AssistantsTabProps> = ({
if (assistantsTabSortType === 'tags') {
return (
<Container className="assistants-tab" ref={containerRef}>
<div style={{ marginBottom: '8px' }}>
<div style={{ display: 'flex', flexDirection: 'column', marginBottom: 4, gap: 10 }}>
{getGroupedAssistants.map((group) => (
<TagsContainer key={group.tag}>
{group.tag !== t('assistants.tags.untagged') && (
@ -95,7 +95,7 @@ const Assistants: FC<AssistantsTabProps> = ({
{group.tag}
</GroupTitleName>
</Tooltip>
<Divider style={{ margin: '12px 0' }}></Divider>
<GroupTitleDivider />
</GroupTitle>
)}
{!collapsedTags[group.tag] && (
@ -176,7 +176,7 @@ const Assistants: FC<AssistantsTabProps> = ({
const Container = styled(Scrollbar)`
display: flex;
flex-direction: column;
padding: 10px;
padding: 4px 10px;
`
const TagsContainer = styled.div`
@ -197,23 +197,20 @@ const AssistantAddItem = styled.div`
cursor: pointer;
&:hover {
background-color: var(--color-background-soft);
}
&.active {
background-color: var(--color-background-soft);
border: 0.5px solid var(--color-border);
background-color: var(--color-list-item-hover);
}
`
const GroupTitle = styled.div`
padding: 8px 0;
position: relative;
color: var(--color-text-2);
font-size: 12px;
font-weight: 500;
margin-bottom: -8px;
cursor: pointer;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
height: 24px;
`
const GroupTitleName = styled.div`
@ -221,13 +218,18 @@ const GroupTitleName = styled.div`
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
background-color: var(--color-background);
box-sizing: border-box;
padding: 0 4px;
color: var(--color-text);
position: absolute;
transform: translateY(2px);
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`

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 {
DEFAULT_CONTEXTCOUNT,
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 Selector from '@renderer/components/Selector'
import { isMac, isWindows } from '@renderer/config/constant'
import { useSettings } from '@renderer/hooks/useSettings'
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 {
SendMessageShortcut,
setAutoTranslateWithSpace,
setCodeCollapsible,
setCodeEditor,
setCodeExecution,
setCodePreview,
setCodeShowLineNumbers,
setCodeWrappable,
setEnableBackspaceDeleteModel,
setEnableQuickPanelTriggers,
setFontSize,
setMathEngine,
setMessageFont,
setMessageNavigation,
setMessageStyle,
setMultiModelMessageStyle,
setPasteLongTextAsFile,
setPasteLongTextThreshold,
setRenderInputMessageAsMarkdown,
setShowInputEstimatedTokens,
setShowMessageDivider,
setShowPrompt,
setShowTokens,
setShowTranslateConfirm,
setThoughtAutoCollapse
setShowTranslateConfirm
} from '@renderer/store/settings'
import {
Assistant,
AssistantSettings,
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 { ThemeMode, TranslateLanguageVarious } from '@renderer/types'
import { InputNumber, Switch } from 'antd'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import OpenAISettingsGroup from './components/OpenAISettingsGroup'
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 SettingsTab: FC = () => {
const { t } = useTranslation()
const { language } = useSettings()
const dispatch = useAppDispatch()
const {
showPrompt,
showMessageDivider,
messageFont,
showInputEstimatedTokens,
sendMessageShortcut,
setSendMessageShortcut,
@ -100,600 +35,164 @@ const SettingsTab: FC<Props> = (props) => {
setTargetLanguage,
pasteLongTextAsFile,
renderInputMessageAsMarkdown,
codeShowLineNumbers,
codeCollapsible,
codeWrappable,
codeEditor,
codePreview,
codeExecution,
mathEngine,
autoTranslateWithSpace,
pasteLongTextThreshold,
multiModelMessageStyle,
thoughtAutoCollapse,
messageNavigation,
enableQuickPanelTriggers,
enableBackspaceDeleteModel,
showTranslateConfirm,
showTokens
showTranslateConfirm
} = 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 (
<Container className="settings-tab">
<CollapsibleSettingGroup
title={t('assistants.settings.title')}
defaultExpanded={true}
extra={
<HStack alignItems="center" gap={2}>
<Button
type="text"
size="small"
icon={<Settings2 size={16} />}
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>
<SettingGroup>
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.input.show_estimated_tokens')}</SettingRowTitleSmall>
<Switch
size="small"
checked={showInputEstimatedTokens}
onChange={(checked) => dispatch(setShowInputEstimatedTokens(checked))}
/>
</SettingRow>
<SettingDivider />
</CollapsibleSettingGroup>
<CollapsibleSettingGroup title={t('settings.messages.input.title')} defaultExpanded={true}>
<SettingGroup>
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.input.show_estimated_tokens')}</SettingRowTitleSmall>
<Switch
size="small"
checked={showInputEstimatedTokens}
onChange={(checked) => dispatch(setShowInputEstimatedTokens(checked))}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.input.paste_long_text_as_file')}</SettingRowTitleSmall>
<Switch
size="small"
checked={pasteLongTextAsFile}
onChange={(checked) => dispatch(setPasteLongTextAsFile(checked))}
/>
</SettingRow>
{pasteLongTextAsFile && (
<>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.input.paste_long_text_threshold')}</SettingRowTitleSmall>
<InputNumber
size="small"
min={500}
max={10000}
step={100}
value={pasteLongTextThreshold}
onChange={(value) => dispatch(setPasteLongTextThreshold(value ?? 500))}
style={{ width: 80 }}
/>
</SettingRow>
</>
)}
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.markdown_rendering_input_message')}</SettingRowTitleSmall>
<Switch
size="small"
checked={renderInputMessageAsMarkdown}
onChange={(checked) => dispatch(setRenderInputMessageAsMarkdown(checked))}
/>
</SettingRow>
<SettingDivider />
{!language.startsWith('en') && (
<>
<SettingRow>
<SettingRowTitleSmall>{t('settings.input.auto_translate_with_space')}</SettingRowTitleSmall>
<Switch
size="small"
checked={autoTranslateWithSpace}
onChange={(checked) => dispatch(setAutoTranslateWithSpace(checked))}
/>
</SettingRow>
<SettingDivider />
</>
)}
<SettingRow>
<SettingRowTitleSmall>{t('settings.input.show_translate_confirm')}</SettingRowTitleSmall>
<Switch
size="small"
checked={showTranslateConfirm}
onChange={(checked) => dispatch(setShowTranslateConfirm(checked))}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.input.enable_quick_triggers')}</SettingRowTitleSmall>
<Switch
size="small"
checked={enableQuickPanelTriggers}
onChange={(checked) => dispatch(setEnableQuickPanelTriggers(checked))}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.input.enable_delete_model')}</SettingRowTitleSmall>
<Switch
size="small"
checked={enableBackspaceDeleteModel}
onChange={(checked) => dispatch(setEnableBackspaceDeleteModel(checked))}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.input.target_language')}</SettingRowTitleSmall>
<StyledSelect
defaultValue={'english' as TranslateLanguageVarious}
size="small"
value={targetLanguage}
menuItemSelectedIcon={<CheckOutlined />}
options={[
{ value: 'chinese', label: t('settings.input.target_language.chinese') },
{ value: 'chinese-traditional', label: t('settings.input.target_language.chinese-traditional') },
{ value: 'english', label: t('settings.input.target_language.english') },
{ value: 'japanese', label: t('settings.input.target_language.japanese') },
{ value: 'russian', label: t('settings.input.target_language.russian') }
]}
onChange={(value) => setTargetLanguage(value as TranslateLanguageVarious)}
style={{ width: 135 }}
/>
</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>
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.input.paste_long_text_as_file')}</SettingRowTitleSmall>
<Switch
size="small"
checked={pasteLongTextAsFile}
onChange={(checked) => dispatch(setPasteLongTextAsFile(checked))}
/>
</SettingRow>
{pasteLongTextAsFile && (
<>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.input.paste_long_text_threshold')}</SettingRowTitleSmall>
<InputNumber
size="small"
min={500}
max={10000}
step={100}
value={pasteLongTextThreshold}
onChange={(value) => dispatch(setPasteLongTextThreshold(value ?? 500))}
style={{ width: 80, backgroundColor: 'transparent' }}
/>
</SettingRow>
</>
)}
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.markdown_rendering_input_message')}</SettingRowTitleSmall>
<Switch
size="small"
checked={renderInputMessageAsMarkdown}
onChange={(checked) => dispatch(setRenderInputMessageAsMarkdown(checked))}
/>
</SettingRow>
<SettingDivider />
{!language.startsWith('en') && (
<>
<SettingRow>
<SettingRowTitleSmall>{t('settings.input.auto_translate_with_space')}</SettingRowTitleSmall>
<Switch
size="small"
checked={autoTranslateWithSpace}
onChange={(checked) => dispatch(setAutoTranslateWithSpace(checked))}
/>
</SettingRow>
<SettingDivider />
</>
)}
<SettingRow>
<SettingRowTitleSmall>{t('settings.input.show_translate_confirm')}</SettingRowTitleSmall>
<Switch
size="small"
checked={showTranslateConfirm}
onChange={(checked) => dispatch(setShowTranslateConfirm(checked))}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.input.enable_quick_triggers')}</SettingRowTitleSmall>
<Switch
size="small"
checked={enableQuickPanelTriggers}
onChange={(checked) => dispatch(setEnableQuickPanelTriggers(checked))}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.input.enable_delete_model')}</SettingRowTitleSmall>
<Switch
size="small"
checked={enableBackspaceDeleteModel}
onChange={(checked) => dispatch(setEnableBackspaceDeleteModel(checked))}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.input.target_language')}</SettingRowTitleSmall>
<Selector
value={targetLanguage || 'english'}
options={[
{ value: 'chinese', label: t('settings.input.target_language.chinese') },
{ value: 'chinese-traditional', label: t('settings.input.target_language.chinese-traditional') },
{ value: 'english', label: t('settings.input.target_language.english') },
{ value: 'japanese', label: t('settings.input.target_language.japanese') },
{ value: 'russian', label: t('settings.input.target_language.russian') }
]}
onChange={(value) => setTargetLanguage(value as TranslateLanguageVarious)}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.input.send_shortcuts')}</SettingRowTitleSmall>
<Selector
value={sendMessageShortcut}
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)}
/>
</SettingRow>
</SettingGroup>
</Container>
)
}
const Container = styled(Scrollbar)`
min-width: 500px;
max-width: 60vw;
display: flex;
flex: 1;
flex-direction: column;
padding: 0 8px;
padding-right: 0;
padding-top: 2px;
padding-bottom: 10px;
margin-top: 3px;
padding: 10px;
.ant-tabs-nav {
.ant-tabs-tab {
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)`
@ -706,14 +205,7 @@ const SettingGroup = styled.div<{ theme?: ThemeMode }>`
margin-top: 0;
border-radius: 8px;
margin-bottom: 10px;
`
const StyledSelect = styled(Select)`
.ant-select-selector {
border-radius: 15px !important;
padding: 4px 10px !important;
height: 26px !important;
}
margin-top: 10px;
`
export default SettingsTab

View File

@ -4,7 +4,6 @@ import {
DeleteOutlined,
EditOutlined,
FolderOutlined,
MenuOutlined,
PushpinOutlined,
QuestionCircleOutlined,
UploadOutlined
@ -55,7 +54,7 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
const { assistants } = useAssistants()
const { assistant, removeTopic, moveTopic, updateTopic, updateTopics } = useAssistant(_assistant.id)
const { t } = useTranslation()
const { showTopicTime, pinTopicsToTop, setTopicPosition } = useSettings()
const { showTopicTime, pinTopicsToTop } = useSettings()
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'),
key: 'copy',
@ -404,7 +386,6 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
setActiveTopic,
onPinTopic,
onClearMessages,
setTopicPosition,
onMoveTopic,
onDeleteTopic
])
@ -482,9 +463,7 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
{fullTopicPrompt}
</TopicPromptText>
)}
{showTopicTime && (
<TopicTime className="time">{dayjs(topic.createdAt).format('MM/DD HH:mm')}</TopicTime>
)}
{showTopicTime && <TopicTime className="time">{dayjs(topic.createdAt).format('MM/DD')}</TopicTime>}
</TopicListItem>
)
}}
@ -573,7 +552,8 @@ const TopicPromptText = styled.div`
const TopicTime = styled.div`
color: var(--color-text-3);
font-size: 11px;
font-size: 12px;
font-family: ubuntu;
`
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 { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
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 { Dropdown, MenuProps } from 'antd'
import { Button, Dropdown, MenuProps } from 'antd'
import { omit } from 'lodash'
import { AlignJustify, Plus, Settings2, Tag, Tags } from 'lucide-react'
import { FC, memo, startTransition, useCallback, useEffect, useMemo, useState } from 'react'
import { AlignJustify, EllipsisVertical, Plus, Settings2, Tag, Tags } from 'lucide-react'
import { FC, memo, useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import * as tinyPinyin from 'tiny-pinyin'
@ -43,6 +43,7 @@ interface AssistantItemProps {
addAssistant: (assistant: Assistant) => void
onTagClick?: (tag: string) => void
handleSortByChange?: (sortType: AssistantsSortType) => void
singleLine?: boolean
}
const AssistantItem: FC<AssistantItemProps> = ({
@ -53,16 +54,18 @@ const AssistantItem: FC<AssistantItemProps> = ({
onDelete,
addAgent,
addAssistant,
handleSortByChange
handleSortByChange,
singleLine = false
}) => {
const { t } = useTranslation()
const { allTags } = useTags()
const { removeAllTopics } = useAssistant(assistant.id)
const { clickAssistantToShowTopic, topicPosition, assistantIconType, setAssistantIconType } = useSettings()
const { assistantIconType, setAssistantIconType } = useSettings()
const defaultModel = getDefaultModel()
const { assistants, updateAssistants } = useAssistants()
const [isPending, setIsPending] = useState(false)
const [isMenuOpen, setIsMenuOpen] = useState(false)
useEffect(() => {
if (isActive) {
@ -121,17 +124,13 @@ const AssistantItem: FC<AssistantItemProps> = ({
)
const handleSwitch = useCallback(async () => {
if (clickAssistantToShowTopic) {
if (topicPosition === 'left') {
EventEmitter.emit(EVENT_NAMES.SWITCH_TOPIC_SIDEBAR)
}
onSwitch(assistant)
} else {
startTransition(() => {
onSwitch(assistant)
})
if (isMenuOpen) {
return
}
}, [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 fullAssistantName = useMemo(
@ -139,31 +138,61 @@ const AssistantItem: FC<AssistantItemProps> = ({
[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 (
<Dropdown menu={{ items: menuItems }} trigger={['contextMenu']}>
<Container onClick={handleSwitch} className={isActive ? 'active' : ''}>
<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>
{isActive && (
<MenuButton onClick={() => EventEmitter.emit(EVENT_NAMES.SWITCH_TOPIC_SIDEBAR)}>
<TopicCount className="topics-count">{assistant.topics.length}</TopicCount>
</MenuButton>
)}
<Container
onClick={handleSwitch}
className={classNames({ active: isActive, 'is-menu-open': isMenuOpen, singleLine })}>
{assistantNave}
<Dropdown menu={{ items: menuItems }} trigger={['click']} onOpenChange={setIsMenuOpen}>
<Button
className="item-menu-button"
type="text"
size="small"
icon={<EllipsisVertical size={16} color="var(--color-text-3)" />}
onClick={(e) => e.stopPropagation()}
/>
</Dropdown>
</Container>
</Dropdown>
)
@ -382,6 +411,7 @@ const Container = styled.div`
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 0 8px;
height: 37px;
position: relative;
@ -389,12 +419,28 @@ const Container = styled.div`
border: 0.5px solid transparent;
width: calc(var(--assistants-width) - 20px);
cursor: pointer;
&.is-menu-open {
.item-menu-button {
display: block;
}
}
&:hover {
background-color: var(--color-list-item-hover);
.item-menu-button {
display: block;
}
}
&.active {
background-color: var(--color-list-item);
}
.item-menu-button {
display: none;
}
&.singleLine {
.item-menu-button {
display: block;
}
}
`
const AssistantNameRow = styled.div`
@ -410,31 +456,4 @@ const AssistantName = styled.div`
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)

View File

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

View File

@ -1,65 +1,27 @@
import AddAssistantPopup from '@renderer/components/Popups/AddAssistantPopup'
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 { uuid } from '@renderer/utils'
import { Segmented as AntSegmented, SegmentedProps } from 'antd'
import { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { FC } from 'react'
import styled from 'styled-components'
import Assistants from './AssistantsTab'
import Settings from './SettingsTab'
import Topics from './TopicsTab'
interface Props {
tab: Tab
activeAssistant: Assistant
activeTopic: Topic
setActiveAssistant: (assistant: Assistant) => void
setActiveTopic: (topic: Topic) => void
position: 'left' | 'right'
forceToSeeAllTab?: boolean
style?: React.CSSProperties
}
type Tab = 'assistants' | 'topic' | 'settings'
type Tab = 'assistants' | 'topic'
let _tab: any = ''
const HomeTabs: FC<Props> = ({
activeAssistant,
activeTopic,
setActiveAssistant,
setActiveTopic,
position,
forceToSeeAllTab,
style
}) => {
const HomeTabs: FC<Props> = ({ tab, activeAssistant, activeTopic, setActiveAssistant, setActiveTopic, style }) => {
const { addAssistant } = useAssistants()
const [tab, setTab] = useState<Tab>(position === 'left' ? _tab || 'assistants' : 'topic')
const { topicPosition } = useSettings()
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 assistant = await AddAssistantPopup.show()
@ -72,68 +34,8 @@ const HomeTabs: FC<Props> = ({
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 (
<Container style={{ ...border, ...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 />
</>
)}
<Container style={{ ...style }} className="home-tabs">
<TabContent className="home-tabs-content">
{tab === 'assistants' && (
<Assistants
@ -146,7 +48,6 @@ const HomeTabs: FC<Props> = ({
{tab === 'topic' && (
<Topics assistant={activeAssistant} activeTopic={activeTopic} setActiveTopic={setActiveTopic} />
)}
{tab === 'settings' && <Settings assistant={activeAssistant} />}
</TabContent>
</Container>
)
@ -154,6 +55,7 @@ const HomeTabs: FC<Props> = ({
const Container = styled.div`
display: flex;
flex: 1;
flex-direction: column;
max-width: var(--assistants-width);
min-width: var(--assistants-width);
@ -173,68 +75,4 @@ const TabContent = styled.div`
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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
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 ListItem from '@renderer/components/ListItem'
import PromptPopup from '@renderer/components/Popups/PromptPopup'
@ -92,9 +92,9 @@ const KnowledgePage: FC = () => {
return (
<Container>
<Navbar>
<NavbarMain>
<NavbarCenter style={{ borderRight: 'none' }}>{t('knowledge.title')}</NavbarCenter>
</Navbar>
</NavbarMain>
<ContentContainer id="content-container">
<SideNav>
<ScrollContainer>

View File

@ -1,15 +1,14 @@
import { CheckCircleOutlined, QuestionCircleOutlined, WarningOutlined } from '@ant-design/icons'
import { Center, VStack } from '@renderer/components/Layout'
import { SettingDescription, SettingRow, SettingSubtitle } from '@renderer/pages/settings'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import { setIsBunInstalled, setIsUvInstalled } from '@renderer/store/mcp'
import { Alert, Button } from 'antd'
import { FC, useCallback, useEffect, useState } from 'react'
import { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router'
import styled from 'styled-components'
import { SettingDescription, SettingRow, SettingSubtitle } from '..'
interface Props {
mini?: boolean
}
@ -26,7 +25,7 @@ const InstallNpxUv: FC<Props> = ({ mini = false }) => {
const [binariesDir, setBinariesDir] = useState<string | null>(null)
const { t } = useTranslation()
const navigate = useNavigate()
const checkBinaries = useCallback(async () => {
const checkBinaries = async () => {
const uvExists = await window.api.isBinaryExist('uv')
const bunExists = await window.api.isBinaryExist('bun')
const { uvPath, bunPath, dir } = await window.api.mcp.getInstallInfo()
@ -36,7 +35,7 @@ const InstallNpxUv: FC<Props> = ({ mini = false }) => {
setUvPath(uvPath)
setBunPath(bunPath)
setBinariesDir(dir)
}, [dispatch])
}
const installUV = async () => {
try {
@ -69,7 +68,7 @@ const InstallNpxUv: FC<Props> = ({ mini = false }) => {
useEffect(() => {
checkBinaries()
}, [checkBinaries])
}, [])
if (mini) {
const installed = isUvInstalled && isBunInstalled
@ -82,7 +81,7 @@ const InstallNpxUv: FC<Props> = ({ mini = false }) => {
icon={installed ? <CheckCircleOutlined /> : <WarningOutlined />}
className="nodrag"
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 Scrollbar from '@renderer/components/Scrollbar'
import { useMCPServers } from '@renderer/hooks/useMCPServers'
import { SettingTitle } from '@renderer/pages/settings'
import { MCPServer } from '@renderer/types'
import { formatMcpError } from '@renderer/utils/error'
import { Button, Dropdown, Empty, Switch, Tag } from 'antd'
@ -12,7 +13,6 @@ import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router'
import styled from 'styled-components'
import { SettingTitle } from '..'
import AddMcpServerModal from './AddMcpServerModal'
import EditMcpJsonPopup from './EditMcpJsonPopup'
import SyncServersPopup from './SyncServersPopup'
@ -36,7 +36,7 @@ const McpServersList: FC = () => {
isActive: false
}
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' })
}, [addMCPServer, navigate, t])
@ -50,7 +50,7 @@ const McpServersList: FC = () => {
setIsAddModalVisible(false)
window.message.success({ content: t('settings.mcp.addSuccess'), key: 'mcp-quick-add' })
// Optionally navigate to the new server's settings page
// navigate(`/settings/mcp/settings`, { state: { server } })
// navigate(`/mcp-servers/settings`, { state: { server } })
},
[addMCPServer, t]
)
@ -117,9 +117,9 @@ const McpServersList: FC = () => {
</Button>
</ButtonGroup>
</ListHeader>
<DragableList style={{ width: '100%' }} list={mcpServers} onUpdate={updateMcpServers}>
<DragableList list={mcpServers} onUpdate={updateMcpServers} listStyle={{ marginBottom: 12 }}>
{(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>
<ServerName>
{server.logoUrl && <ServerLogo src={server.logoUrl} alt={`${server.name} logo`} />}
@ -148,7 +148,7 @@ const McpServersList: FC = () => {
<Button
icon={<Settings2 size={16} />}
type="text"
onClick={() => navigate(`/settings/mcp/settings`, { state: { server } })}
onClick={() => navigate(`/mcp-servers/settings`, { state: { server } })}
/>
</StatusIndicator>
</ServerHeader>
@ -207,6 +207,7 @@ const ListHeader = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
margin: 0 auto;
h2 {
font-size: 22px;
@ -217,8 +218,8 @@ const ListHeader = styled.div`
const ServerCard = styled.div`
display: flex;
flex-direction: column;
border: 0.5px solid var(--color-border);
border-radius: var(--list-item-border-radius);
border: 1px solid var(--color-border);
border-radius: 12px;
padding: 10px 16px;
transition: all 0.2s ease;
background-color: var(--color-background);

View File

@ -1,7 +1,8 @@
import { DeleteOutlined, SaveOutlined } from '@ant-design/icons'
import { useTheme } from '@renderer/context/ThemeProvider'
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 { formatMcpError } from '@renderer/utils/error'
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 styled from 'styled-components'
import { SettingContainer, SettingDivider, SettingGroup, SettingTitle } from '..'
import MCPPromptsSection from './McpPrompt'
import MCPResourcesSection from './McpResource'
import MCPToolsSection from './McpTool'
@ -319,7 +319,7 @@ const McpSettings: React.FC = () => {
await window.api.mcp.removeServer(server)
deleteMCPServer(server.id)
window.message.success({ content: t('settings.mcp.deleteSuccess'), key: 'mcp-list' })
navigate('/settings/mcp')
navigate('/mcp-servers')
}
})
} catch (error: any) {
@ -608,7 +608,7 @@ const McpSettings: React.FC = () => {
}
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)' }}>
<SettingTitle>
<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 { isLinux, isWindows } from '@renderer/config/constant'
import { useFullscreen } from '@renderer/hooks/useFullscreen'
import { Button, Dropdown, Menu, type MenuProps } from 'antd'
import { ChevronDown, Search } from 'lucide-react'
import { useTranslation } from 'react-i18next'
@ -74,29 +71,27 @@ export const McpSettingsNavbar = () => {
}))
return (
<NavbarRight style={{ paddingRight: useFullscreen() ? '12px' : isWindows ? 150 : isLinux ? 120 : 12 }}>
<HStack alignItems="center" gap={5}>
<HStack alignItems="center" gap={5} style={{ marginRight: 10 }}>
<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
size="small"
type="text"
onClick={() => navigate('/settings/mcp/npx-search')}
icon={<Search size={14} />}
className="nodrag"
style={{ fontSize: 13, height: 28, borderRadius: 20 }}>
{t('settings.mcp.searchNpx')}
style={{ fontSize: 13, height: 28, borderRadius: 20, display: 'flex', alignItems: 'center' }}>
{t('settings.mcp.findMore')}
<ChevronDown size={16} />
</Button>
<Dropdown menu={{ items: resourceMenuItems }} trigger={['click']}>
<Button
size="small"
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>
</Dropdown>
<InstallNpxUv mini />
</HStack>
)
}

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