mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-26 03:31:24 +08:00
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:
parent
a33a8da5c1
commit
11a8154458
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@ -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 }}"}'
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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'
|
||||
}
|
||||
|
||||
@ -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'),
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
38
src/renderer/src/Routes.tsx
Normal file
38
src/renderer/src/Routes.tsx
Normal 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
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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%;
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -229,8 +229,8 @@ const CodeEditor = ({
|
||||
style={{
|
||||
...style,
|
||||
fontSize: `${fontSize - 1}px`,
|
||||
border: '0.5px solid transparent',
|
||||
marginTop: 0
|
||||
marginTop: 0,
|
||||
borderRadius: 'inherit'
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
@ -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 }}
|
||||
|
||||
@ -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>
|
||||
|
||||
35
src/renderer/src/components/Icons/NarrowModeIcon.tsx
Normal file
35
src/renderer/src/components/Icons/NarrowModeIcon.tsx
Normal 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
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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',
|
||||
|
||||
387
src/renderer/src/components/Popups/MessageSettingsPopup.tsx
Normal file
387
src/renderer/src/components/Popups/MessageSettingsPopup.tsx
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
76
src/renderer/src/components/Selector.tsx
Normal file
76
src/renderer/src/components/Selector.tsx
Normal 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
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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);
|
||||
`
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 = `
|
||||
|
||||
@ -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 () {
|
||||
|
||||
50
src/renderer/src/hooks/useChat.tsx
Normal file
50
src/renderer/src/hooks/useChat.tsx
Normal 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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)) {
|
||||
|
||||
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 }))
|
||||
}
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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": "閉じるときにトレイに最小化",
|
||||
|
||||
@ -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": "Свернуть в трей при закрытии",
|
||||
|
||||
@ -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": "关闭时最小化到托盘",
|
||||
|
||||
@ -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": "關閉時最小化到系统匣",
|
||||
|
||||
@ -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": "Κανένα",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 && (
|
||||
|
||||
@ -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) => (
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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" />
|
||||
|
||||
@ -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)`
|
||||
|
||||
140
src/renderer/src/pages/home/ChatNavbar.tsx
Normal file
140
src/renderer/src/pages/home/ChatNavbar.tsx
Normal 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
|
||||
@ -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;
|
||||
`
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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
|
||||
}}
|
||||
|
||||
31
src/renderer/src/pages/home/Inputbar/SettingButton.tsx
Normal file
31
src/renderer/src/pages/home/Inputbar/SettingButton.tsx
Normal 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
|
||||
72
src/renderer/src/pages/home/MainSidebar/MainNavbar.tsx
Normal file
72
src/renderer/src/pages/home/MainSidebar/MainNavbar.tsx
Normal 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
|
||||
362
src/renderer/src/pages/home/MainSidebar/MainSidebar.tsx
Normal file
362
src/renderer/src/pages/home/MainSidebar/MainSidebar.tsx
Normal 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
|
||||
@ -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;
|
||||
`
|
||||
121
src/renderer/src/pages/home/MainSidebar/OpenedMinapps.tsx
Normal file
121
src/renderer/src/pages/home/MainSidebar/OpenedMinapps.tsx
Normal 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
|
||||
67
src/renderer/src/pages/home/MainSidebar/PinnedApps.tsx
Normal file
67
src/renderer/src/pages/home/MainSidebar/PinnedApps.tsx
Normal 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
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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;
|
||||
`
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
) : (
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 (
|
||||
<>
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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;
|
||||
`
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
@ -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%;
|
||||
|
||||
@ -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 ''
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
@ -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;
|
||||
`
|
||||
|
||||
@ -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
|
||||
@ -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`
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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`
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -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);
|
||||
@ -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 }}>
|
||||
@ -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
Loading…
Reference in New Issue
Block a user