feat: scrollbar

This commit is contained in:
kangfenmao 2024-10-24 14:50:16 +08:00
parent 6e7b6d8387
commit 077a66c675
14 changed files with 542 additions and 433 deletions

View File

@ -93,6 +93,7 @@
"openai": "^4.52.1",
"prettier": "^3.2.4",
"react": "^18.2.0",
"react-custom-scrollbars-2": "^4.5.0",
"react-dom": "^18.2.0",
"react-i18next": "^14.1.2",
"react-markdown": "^9.0.1",

View File

@ -1,5 +1,6 @@
@import './markdown.scss';
@import './ant.scss';
@import './scrollbar.scss';
@import '../fonts/icon-fonts/iconfont.css';
@import '../fonts/ubuntu/ubuntu.css';
@ -36,9 +37,6 @@
--color-error: #f44336;
--color-link: #1677ff;
--color-code-background: #323232;
--color-scrollbar-thumb: rgba(255, 255, 255, 0.08);
--color-scrollbar-thumb-hover: rgba(255, 255, 255, 0.15);
--color-scrollbar-thumb-active: rgba(255, 255, 255, 0.2);
--color-hover: rgba(40, 40, 40, 1);
--color-active: rgba(55, 55, 55, 1);
@ -48,7 +46,7 @@
--navbar-height: 40px;
--sidebar-width: 50px;
--status-bar-height: 40px;
--input-bar-height: 85px;
--input-bar-height: 100px;
--assistants-width: 275px;
--topic-list-width: 275px;
@ -88,9 +86,6 @@ body[theme-mode='light'] {
--color-error: #f44336;
--color-link: #1677ff;
--color-code-background: #e3e3e3;
--color-scrollbar-thumb: rgba(0, 0, 0, 0.08);
--color-scrollbar-thumb-hover: rgba(0, 0, 0, 0.15);
--color-scrollbar-thumb-active: rgba(0, 0, 0, 0.2);
--color-hover: var(--color-white-mute);
--color-active: var(--color-white-soft);

View File

@ -1,7 +1,17 @@
:root {
--color-scrollbar-thumb: #6b6b6b;
--color-scrollbar-thumb-hover: #939393;
}
body[theme-mode='light'] {
--color-scrollbar-thumb: #b1b1b1;
--color-scrollbar-thumb-hover: #7d7d7d;
}
/* 全局初始化滚动条样式 */
::-webkit-scrollbar {
width: 4px;
height: 2px;
width: 5px;
height: 5px;
}
::-webkit-scrollbar-track {
@ -9,8 +19,17 @@
}
::-webkit-scrollbar-thumb {
border-radius: 10px;
background: var(--color-scrollbar-thumb);
&:hover {
background: var(--color-scrollbar-thumb-hover);
}
}
pre::-webkit-scrollbar-thumb {
border-radius: 0;
background: rgba(0, 0, 0, 0.08);
&:hover {
background: rgba(0, 0, 0, 0.15);
}
}

View File

@ -0,0 +1,22 @@
import { ScrollbarProps, Scrollbars } from 'react-custom-scrollbars-2'
import styled from 'styled-components'
export const Scrollbar: React.FC<ScrollbarProps> = ({ children, ...props }) => {
return (
<Scrollbars
autoHide
{...props}
renderThumbVertical={(props) => <Thumb {...props} />}
renderTrackHorizontal={(props) => <Thumb {...props} />}>
{children}
</Scrollbars>
)
}
const Thumb = styled.div`
border-radius: 10px;
background-color: var(--color-scrollbar-thumb);
&:hover {
background-color: var(--color-scrollbar-thumb-hover);
}
`

View File

@ -45,7 +45,6 @@ export function useAppInit() {
useEffect(() => {
const transparentWindow = windowStyle === 'transparent' && isMac && !minappShow
window.root.style.background = transparentWindow ? 'var(--navbar-background-mac)' : 'var(--navbar-background)'
!isMac && import('@renderer/assets/styles/scrollbar.scss')
}, [windowStyle, minappShow])
useEffect(() => {

View File

@ -2,6 +2,7 @@ import { DeleteOutlined, EditOutlined, MoreOutlined, PlusOutlined } from '@ant-d
import AssistantSettingsPopup from '@renderer/components/AssistantSettings'
import DragableList from '@renderer/components/DragableList'
import { HStack } from '@renderer/components/Layout'
import { Scrollbar } from '@renderer/components/Scrollbar'
import { useAgents } from '@renderer/hooks/useAgents'
import { createAssistantFromAgent } from '@renderer/services/assistant'
import { Agent } from '@renderer/types'
@ -56,42 +57,45 @@ const Agents: React.FC<Props> = ({ onClick }) => {
)
return (
<Container style={{ paddingBottom: dragging ? 30 : 0 }}>
{agents.length > 0 && (
<DragableList
list={agents}
onUpdate={updateAgents}
onDragStart={() => setDragging(true)}
onDragEnd={() => setDragging(false)}>
{(agent: Agent) => (
<Dropdown menu={{ items: getMenuItems(agent) }} trigger={['contextMenu']}>
<AgentItem onClick={() => onClick(agent)}>
<HStack alignItems="center" justifyContent="space-between" h="36px">
<AgentItemName className="text-nowrap">
{agent.emoji} {agent.name}
</AgentItemName>
<ActionButton className="actions" gap="15px" onClick={(e) => e.stopPropagation()}>
<Dropdown menu={{ items: getMenuItems(agent) }} trigger={['hover']}>
<MoreOutlined style={{ cursor: 'pointer' }} />
</Dropdown>
</ActionButton>
</HStack>
<AgentItemPrompt>{agent.prompt}</AgentItemPrompt>
</AgentItem>
</Dropdown>
)}
</DragableList>
)}
{!dragging && (
<Button
type="dashed"
icon={<PlusOutlined />}
onClick={() => AddAgentPopup.show()}
style={{ borderRadius: 20, height: 34 }}>
{t('agents.add.title')}
</Button>
)}
</Container>
<Scrollbar style={{ width: 280 }}>
<Container style={{ paddingBottom: dragging ? 30 : 0 }}>
{agents.length > 0 && (
<DragableList
list={agents}
onUpdate={updateAgents}
onDragStart={() => setDragging(true)}
onDragEnd={() => setDragging(false)}>
{(agent: Agent) => (
<Dropdown menu={{ items: getMenuItems(agent) }} trigger={['contextMenu']}>
<AgentItem onClick={() => onClick(agent)}>
<HStack alignItems="center" justifyContent="space-between" h="36px">
<AgentItemName className="text-nowrap">
{agent.emoji} {agent.name}
</AgentItemName>
<ActionButton className="actions" gap="15px" onClick={(e) => e.stopPropagation()}>
<Dropdown menu={{ items: getMenuItems(agent) }} trigger={['hover']}>
<MoreOutlined style={{ cursor: 'pointer' }} />
</Dropdown>
</ActionButton>
</HStack>
<AgentItemPrompt>{agent.prompt}</AgentItemPrompt>
</AgentItem>
</Dropdown>
)}
</DragableList>
)}
{!dragging && (
<Button
type="dashed"
icon={<PlusOutlined />}
onClick={() => AddAgentPopup.show()}
style={{ borderRadius: 20, height: 34 }}>
{t('agents.add.title')}
</Button>
)}
<div style={{ height: 10 }} />
</Container>
</Scrollbar>
)
}
@ -99,10 +103,8 @@ const Container = styled.div`
padding: 15px;
display: flex;
flex-direction: column;
width: 280px;
height: calc(100vh - var(--navbar-height));
border-right: 0.5px solid var(--color-border);
overflow-y: scroll;
min-height: calc(100vh - var(--navbar-height));
`
const AgentItem = styled.div`

View File

@ -1,5 +1,6 @@
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
import { VStack } from '@renderer/components/Layout'
import { Scrollbar } from '@renderer/components/Scrollbar'
import SystemAgents from '@renderer/config/agents.json'
import { createAssistantFromAgent } from '@renderer/services/assistant'
import { Agent } from '@renderer/types'
@ -60,32 +61,34 @@ const AgentsPage: FC = () => {
</Navbar>
<ContentContainer id="content-container">
<Agents onClick={onAddAgentConfirm} />
<AssistantsContainer>
<VStack style={{ flex: 1 }}>
{Object.keys(agentGroups)
.reverse()
.map((group) => (
<div key={group}>
<Title level={5} key={group} style={{ marginBottom: 16 }}>
{group}
</Title>
<Row gutter={16}>
{agentGroups[group].map((agent, index) => {
return (
<Col span={8} key={group + index}>
<AgentCard
onClick={() => onAddAgentConfirm(getAgentFromSystemAgent(agent))}
agent={agent as any}
/>
</Col>
)
})}
</Row>
</div>
))}
<div style={{ minHeight: 20 }} />
</VStack>
</AssistantsContainer>
<Scrollbar>
<AssistantsContainer>
<VStack style={{ flex: 1 }}>
{Object.keys(agentGroups)
.reverse()
.map((group) => (
<div key={group}>
<Title level={5} key={group} style={{ marginBottom: 16 }}>
{group}
</Title>
<Row gutter={16}>
{agentGroups[group].map((agent, index) => {
return (
<Col span={8} key={group + index}>
<AgentCard
onClick={() => onAddAgentConfirm(getAgentFromSystemAgent(agent))}
agent={agent as any}
/>
</Col>
)
})}
</Row>
</div>
))}
<div style={{ minHeight: 20 }} />
</VStack>
</AssistantsContainer>
</Scrollbar>
</ContentContainer>
</Container>
)
@ -112,7 +115,6 @@ const AssistantsContainer = styled.div`
flex-direction: row;
height: calc(100vh - var(--navbar-height));
padding: 15px 20px;
overflow-y: scroll;
`
const AgentPrompt = styled.div`

View File

@ -1,5 +1,6 @@
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
import { VStack } from '@renderer/components/Layout'
import { Scrollbar } from '@renderer/components/Scrollbar'
import db from '@renderer/databases'
import FileManager from '@renderer/services/file'
import { FileType, FileTypes } from '@renderer/types'
@ -66,15 +67,17 @@ const FilesPage: FC = () => {
<NavbarCenter style={{ borderRight: 'none' }}>{t('files.title')}</NavbarCenter>
</Navbar>
<ContentContainer id="content-container">
<VStack style={{ width: '100%' }}>
<Table
dataSource={dataSource}
columns={columns}
style={{ width: '100%', marginBottom: 20 }}
size="small"
pagination={{ pageSize: 100 }}
/>
</VStack>
<Scrollbar>
<VStack style={{ width: '100%', padding: 15 }}>
<Table
dataSource={dataSource}
columns={columns}
style={{ width: '100%', marginBottom: 20 }}
size="small"
pagination={{ pageSize: 100 }}
/>
</VStack>
</Scrollbar>
</ContentContainer>
</Container>
)
@ -93,8 +96,6 @@ const ContentContainer = styled.div`
flex-direction: row;
justify-content: center;
height: 100%;
overflow-y: scroll;
padding: 15px;
`
const FileNameText = styled.div`

View File

@ -1,4 +1,4 @@
import { isWindows } from '@renderer/config/constant'
import { Scrollbar } from '@renderer/components/Scrollbar'
import db from '@renderer/databases'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { useSettings } from '@renderer/hooks/useSettings'
@ -9,7 +9,7 @@ import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
import { deleteMessageFiles, filterMessages, getContextCount } from '@renderer/services/messages'
import { estimateHistoryTokens, estimateMessageUsage } from '@renderer/services/tokens'
import { Assistant, Message, Model, Topic } from '@renderer/types'
import { captureScrollableDiv, classNames, runAsyncFunction, uuid } from '@renderer/utils'
import { captureScrollableDiv, runAsyncFunction, uuid } from '@renderer/utils'
import { t } from 'i18next'
import { flatten, last, reverse, take } from 'lodash'
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
@ -220,45 +220,33 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
}, [assistant, messages])
return (
<Container
id="messages"
className={classNames(isWindows && 'scrollbar')}
style={{ maxWidth }}
key={assistant.id}
ref={containerRef}>
<Suggestions assistant={assistant} messages={messages} lastMessage={lastMessage} />
{lastMessage && <MessageItem key={lastMessage.id} message={lastMessage} lastMessage />}
{reverse([...messages]).map((message, index) => (
<MessageItem
key={message.id}
message={message}
index={index}
hidePresetMessages={assistant.settings?.hideMessages}
onEditMessage={onEditMessage}
onDeleteMessage={onDeleteMessage}
/>
))}
<Prompt assistant={assistant} key={assistant.prompt} />
</Container>
<Scrollbar>
<Container id="messages" style={{ maxWidth }} key={assistant.id} ref={containerRef}>
<Suggestions assistant={assistant} messages={messages} lastMessage={lastMessage} />
{lastMessage && <MessageItem key={lastMessage.id} message={lastMessage} lastMessage />}
{reverse([...messages]).map((message, index) => (
<MessageItem
key={message.id}
message={message}
index={index}
hidePresetMessages={assistant.settings?.hideMessages}
onEditMessage={onEditMessage}
onDeleteMessage={onDeleteMessage}
/>
))}
<Prompt assistant={assistant} key={assistant.prompt} />
</Container>
</Scrollbar>
)
}
const Container = styled.div`
position: relative;
display: flex;
flex-direction: column;
overflow-y: auto;
flex-direction: column-reverse;
max-height: calc(100vh - var(--input-bar-height) - var(--navbar-height));
padding: 10px 0;
background-color: var(--color-background);
padding-bottom: 20px;
overflow-x: hidden;
&.scrollbar {
&::-webkit-scrollbar {
width: 10px;
}
}
`
export default Messages

View File

@ -2,6 +2,7 @@ import { DeleteOutlined, EditOutlined, MinusCircleOutlined, PlusOutlined, SaveOu
import AssistantSettingsPopup from '@renderer/components/AssistantSettings'
import DragableList from '@renderer/components/DragableList'
import CopyIcon from '@renderer/components/Icons/CopyIcon'
import { Scrollbar } from '@renderer/components/Scrollbar'
import { useAgents } from '@renderer/hooks/useAgents'
import { useAssistant, useAssistants } from '@renderer/hooks/useAssistant'
import { useSettings } from '@renderer/hooks/useSettings'
@ -178,69 +179,69 @@ const Assistants: FC<Props> = ({
}, [activeAssistant?.id, list, onSwitchAssistant])
return (
<Container>
{assistants.length >= 10 && (
<SearchContainer>
<Input
placeholder={t('chat.assistant.search.placeholder')}
suffix={<CommandKey>+K</CommandKey>}
value={search}
onChange={(e) => setSearch(e.target.value)}
style={{ borderRadius: 16, borderWidth: 0.5 }}
onKeyDown={onSearch}
ref={searchRef}
onFocus={() => dispatch(setSearching(true))}
onBlur={() => {
dispatch(setSearching(false))
setSearch('')
}}
allowClear
/>
</SearchContainer>
)}
<DragableList
list={list}
onUpdate={updateAssistants}
droppableProps={{ isDropDisabled: !isEmpty(search) }}
style={{ paddingBottom: dragging ? '34px' : 0 }}
onDragStart={() => setDragging(true)}
onDragEnd={() => setDragging(false)}>
{(assistant) => {
const isCurrent = assistant.id === activeAssistant?.id
return (
<Dropdown key={assistant.id} menu={{ items: getMenuItems(assistant) }} trigger={['contextMenu']}>
<AssistantItem onClick={() => onSwitchAssistant(assistant)} className={isCurrent ? 'active' : ''}>
<AssistantName className="name">{assistant.name || t('chat.default.name')}</AssistantName>
{isCurrent && (
<ArrowRightButton onClick={() => EventEmitter.emit(EVENT_NAMES.SWITCH_TOPIC_SIDEBAR)}>
<i className="iconfont icon-gridlines" />
</ArrowRightButton>
)}
{false && <TopicCount className="topics-count">{assistant.topics.length}</TopicCount>}
</AssistantItem>
</Dropdown>
)
}}
</DragableList>
{!dragging && (
<AssistantItem onClick={onCreateAssistant}>
<AssistantName>
<PlusOutlined style={{ color: 'var(--color-text-2)', marginRight: 4 }} />
{t('chat.add.assistant.title')}
</AssistantName>
</AssistantItem>
)}
</Container>
<Scrollbar>
<Container>
{assistants.length >= 10 && (
<SearchContainer>
<Input
placeholder={t('chat.assistant.search.placeholder')}
suffix={<CommandKey>+K</CommandKey>}
value={search}
onChange={(e) => setSearch(e.target.value)}
style={{ borderRadius: 16, borderWidth: 0.5 }}
onKeyDown={onSearch}
ref={searchRef}
onFocus={() => dispatch(setSearching(true))}
onBlur={() => {
dispatch(setSearching(false))
setSearch('')
}}
allowClear
/>
</SearchContainer>
)}
<DragableList
list={list}
onUpdate={updateAssistants}
droppableProps={{ isDropDisabled: !isEmpty(search) }}
style={{ paddingBottom: dragging ? '34px' : 0 }}
onDragStart={() => setDragging(true)}
onDragEnd={() => setDragging(false)}>
{(assistant) => {
const isCurrent = assistant.id === activeAssistant?.id
return (
<Dropdown key={assistant.id} menu={{ items: getMenuItems(assistant) }} trigger={['contextMenu']}>
<AssistantItem onClick={() => onSwitchAssistant(assistant)} className={isCurrent ? 'active' : ''}>
<AssistantName className="name">{assistant.name || t('chat.default.name')}</AssistantName>
{isCurrent && (
<ArrowRightButton onClick={() => EventEmitter.emit(EVENT_NAMES.SWITCH_TOPIC_SIDEBAR)}>
<i className="iconfont icon-gridlines" />
</ArrowRightButton>
)}
{false && <TopicCount className="topics-count">{assistant.topics.length}</TopicCount>}
</AssistantItem>
</Dropdown>
)
}}
</DragableList>
{!dragging && (
<AssistantItem onClick={onCreateAssistant}>
<AssistantName>
<PlusOutlined style={{ color: 'var(--color-text-2)', marginRight: 4 }} />
{t('chat.add.assistant.title')}
</AssistantName>
</AssistantItem>
)}
<div style={{ minHeight: 10 }}></div>
</Container>
</Scrollbar>
)
}
const Container = styled.div`
display: flex;
flex-direction: column;
height: calc(100vh - var(--navbar-height));
overflow-y: auto;
padding-top: 10px;
padding-bottom: 10px;
`
const AssistantItem = styled.div`

View File

@ -1,5 +1,6 @@
import { CheckOutlined, QuestionCircleOutlined, ReloadOutlined } from '@ant-design/icons'
import { HStack } from '@renderer/components/Layout'
import { Scrollbar } from '@renderer/components/Scrollbar'
import { DEFAULT_CONEXTCOUNT, DEFAULT_MAX_TOKENS, DEFAULT_TEMPERATURE } from '@renderer/config/constant'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { useSettings } from '@renderer/hooks/useSettings'
@ -97,183 +98,185 @@ const SettingsTab: FC<Props> = (props) => {
}, [assistant])
return (
<Container>
<SettingSubtitle style={{ marginTop: 5 }}>
{t('settings.messages.model.title')}{' '}
<Tooltip title={t('chat.settings.reset')}>
<ReloadOutlined onClick={onReset} style={{ cursor: 'pointer', fontSize: 12, padding: '0 3px' }} />
</Tooltip>
</SettingSubtitle>
<SettingDivider />
<Row align="middle">
<Label>{t('chat.settings.temperature')}</Label>
<Tooltip title={t('chat.settings.temperature.tip')}>
<QuestionIcon />
</Tooltip>
</Row>
<Row align="middle" gutter={10}>
<Col span={24}>
<Slider
min={0}
max={2}
onChange={setTemperature}
onChangeComplete={onTemperatureChange}
value={typeof temperature === 'number' ? temperature : 0}
step={0.1}
/>
</Col>
</Row>
<Row align="middle">
<Label>{t('chat.settings.conext_count')}</Label>
<Tooltip title={t('chat.settings.conext_count.tip')}>
<QuestionIcon />
</Tooltip>
</Row>
<Row align="middle" gutter={10}>
<Col span={24}>
<Slider
min={0}
max={20}
onChange={setConextCount}
onChangeComplete={onConextCountChange}
value={typeof contextCount === 'number' ? contextCount : 0}
step={1}
/>
</Col>
</Row>
<SettingRow>
<SettingRowTitleSmall>{t('model.stream_output')}</SettingRowTitleSmall>
<Switch
size="small"
checked={streamOutput}
onChange={(checked) => {
setStreamOutput(checked)
onUpdateAssistantSettings({ streamOutput: checked })
}}
/>
</SettingRow>
<SettingDivider />
<Row align="middle" justify="space-between">
<HStack alignItems="center">
<Label>{t('chat.settings.max_tokens')}</Label>
<Tooltip title={t('chat.settings.max_tokens.tip')}>
<Scrollbar>
<Container>
<SettingSubtitle style={{ marginTop: 5 }}>
{t('settings.messages.model.title')}{' '}
<Tooltip title={t('chat.settings.reset')}>
<ReloadOutlined onClick={onReset} style={{ cursor: 'pointer', fontSize: 12, padding: '0 3px' }} />
</Tooltip>
</SettingSubtitle>
<SettingDivider />
<Row align="middle">
<Label>{t('chat.settings.temperature')}</Label>
<Tooltip title={t('chat.settings.temperature.tip')}>
<QuestionIcon />
</Tooltip>
</HStack>
<Switch
size="small"
checked={enableMaxTokens}
onChange={(enabled) => {
setEnableMaxTokens(enabled)
onUpdateAssistantSettings({ enableMaxTokens: enabled })
}}
/>
</Row>
<Row align="middle" gutter={10}>
<Col span={24}>
<Slider
disabled={!enableMaxTokens}
min={0}
max={32000}
onChange={setMaxTokens}
onChangeComplete={onMaxTokensChange}
value={typeof maxTokens === 'number' ? maxTokens : 0}
step={100}
/>
</Col>
</Row>
<SettingSubtitle>{t('settings.messages.title')}</SettingSubtitle>
<SettingDivider />
<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.show_line_numbers')}</SettingRowTitleSmall>
<Switch
size="small"
checked={codeShowLineNumbers}
onChange={(checked) => dispatch(setCodeShowLineNumbers(checked))}
/>
</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>
</Row>
<Row align="middle" gutter={10}>
<Col span={24}>
<Slider
min={0}
max={2}
onChange={setTemperature}
onChangeComplete={onTemperatureChange}
value={typeof temperature === 'number' ? temperature : 0}
step={0.1}
/>
</Col>
</Row>
<Row align="middle">
<Label>{t('chat.settings.conext_count')}</Label>
<Tooltip title={t('chat.settings.conext_count.tip')}>
<QuestionIcon />
</Tooltip>
</Row>
<Row align="middle" gutter={10}>
<Col span={24}>
<Slider
min={0}
max={20}
onChange={setConextCount}
onChangeComplete={onConextCountChange}
value={typeof contextCount === 'number' ? contextCount : 0}
step={1}
/>
</Col>
</Row>
<SettingRow>
<SettingRowTitleSmall>{t('model.stream_output')}</SettingRowTitleSmall>
<Switch
size="small"
checked={streamOutput}
onChange={(checked) => {
setStreamOutput(checked)
onUpdateAssistantSettings({ streamOutput: checked })
}}
/>
</Col>
</Row>
<SettingSubtitle>{t('settings.messages.input.title')}</SettingSubtitle>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.input.show_estimated_tokens')}</SettingRowTitleSmall>
<Switch
size="small"
checked={showInputEstimatedTokens}
onChange={(checked) => dispatch(setShowInputEstimatedTokens(checked))}
</SettingRow>
<SettingDivider />
<Row align="middle" justify="space-between">
<HStack alignItems="center">
<Label>{t('chat.settings.max_tokens')}</Label>
<Tooltip title={t('chat.settings.max_tokens.tip')}>
<QuestionIcon />
</Tooltip>
</HStack>
<Switch
size="small"
checked={enableMaxTokens}
onChange={(enabled) => {
setEnableMaxTokens(enabled)
onUpdateAssistantSettings({ enableMaxTokens: enabled })
}}
/>
</Row>
<Row align="middle" gutter={10}>
<Col span={24}>
<Slider
disabled={!enableMaxTokens}
min={0}
max={32000}
onChange={setMaxTokens}
onChangeComplete={onMaxTokensChange}
value={typeof maxTokens === 'number' ? maxTokens : 0}
step={100}
/>
</Col>
</Row>
<SettingSubtitle>{t('settings.messages.title')}</SettingSubtitle>
<SettingDivider />
<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.show_line_numbers')}</SettingRowTitleSmall>
<Switch
size="small"
checked={codeShowLineNumbers}
onChange={(checked) => dispatch(setCodeShowLineNumbers(checked))}
/>
</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>
<SettingSubtitle>{t('settings.messages.input.title')}</SettingSubtitle>
<SettingDivider />
<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>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.markdown_rendering_input_message')}</SettingRowTitleSmall>
<Switch
size="small"
checked={renderInputMessageAsMarkdown}
onChange={(checked) => dispatch(setRenderInputMessageAsMarkdown(checked))}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.input.send_shortcuts')}</SettingRowTitleSmall>
</SettingRow>
<Select
value={sendMessageShortcut}
menuItemSelectedIcon={<CheckOutlined />}
options={[
{ value: 'Enter', label: `Enter ${t('chat.input.send')}` },
{ value: 'Shift+Enter', label: `Shift + Enter ${t('chat.input.send')}` }
]}
onChange={(value) => setSendMessageShortcut(value)}
style={{ width: '100%', marginTop: 10 }}
/>
</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>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.markdown_rendering_input_message')}</SettingRowTitleSmall>
<Switch
size="small"
checked={renderInputMessageAsMarkdown}
onChange={(checked) => dispatch(setRenderInputMessageAsMarkdown(checked))}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.input.send_shortcuts')}</SettingRowTitleSmall>
</SettingRow>
<Select
value={sendMessageShortcut}
menuItemSelectedIcon={<CheckOutlined />}
options={[
{ value: 'Enter', label: `Enter ${t('chat.input.send')}` },
{ value: 'Shift+Enter', label: `Shift + Enter ${t('chat.input.send')}` }
]}
onChange={(value) => setSendMessageShortcut(value)}
style={{ width: '100%', marginTop: 10 }}
/>
</Container>
</Container>
</Scrollbar>
)
}

View File

@ -8,6 +8,7 @@ import {
} from '@ant-design/icons'
import DragableList from '@renderer/components/DragableList'
import PromptPopup from '@renderer/components/Popups/PromptPopup'
import { Scrollbar } from '@renderer/components/Scrollbar'
import { useAssistant, useAssistants } from '@renderer/hooks/useAssistant'
import { useSettings } from '@renderer/hooks/useSettings'
import { TopicManager } from '@renderer/hooks/useTopic'
@ -177,38 +178,40 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
)
return (
<Container>
<DragableList list={assistant.topics} onUpdate={updateTopics}>
{(topic) => {
const isActive = topic.id === activeTopic?.id
return (
<Dropdown menu={{ items: getTopicMenuItems(topic) }} trigger={['contextMenu']} key={topic.id}>
<TopicListItem
className={isActive ? 'active' : ''}
style={{ borderRadius }}
onClick={() => onSwitchTopic(topic)}>
<TopicName className="name">{topic.name.replace('`', '')}</TopicName>
{showTopicTime && <TopicTime>{dayjs(topic.createdAt).format('MM/DD HH:mm')}</TopicTime>}
{isActive && (
<MenuButton
className="menu"
onClick={(e) => {
e.stopPropagation()
if (assistant.topics.length === 1) {
return onClearMessages()
}
onDeleteTopic(topic)
}}>
<CloseOutlined />
</MenuButton>
)}
</TopicListItem>
</Dropdown>
)
}}
</DragableList>
<div style={{ minHeight: '10px' }}></div>
</Container>
<Scrollbar>
<Container>
<DragableList list={assistant.topics} onUpdate={updateTopics}>
{(topic) => {
const isActive = topic.id === activeTopic?.id
return (
<Dropdown menu={{ items: getTopicMenuItems(topic) }} trigger={['contextMenu']} key={topic.id}>
<TopicListItem
className={isActive ? 'active' : ''}
style={{ borderRadius }}
onClick={() => onSwitchTopic(topic)}>
<TopicName className="name">{topic.name.replace('`', '')}</TopicName>
{showTopicTime && <TopicTime>{dayjs(topic.createdAt).format('MM/DD HH:mm')}</TopicTime>}
{isActive && (
<MenuButton
className="menu"
onClick={(e) => {
e.stopPropagation()
if (assistant.topics.length === 1) {
return onClearMessages()
}
onDeleteTopic(topic)
}}>
<CloseOutlined />
</MenuButton>
)}
</TopicListItem>
</Dropdown>
)
}}
</DragableList>
<div style={{ minHeight: '10px' }}></div>
</Container>
</Scrollbar>
)
}
@ -216,7 +219,6 @@ const Container = styled.div`
display: flex;
flex-direction: column;
padding-top: 10px;
max-height: calc(100vh - var(--navbar-height) - 70px);
`
const TopicListItem = styled.div`

View File

@ -1,5 +1,6 @@
import { DeleteOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons'
import { DragDropContext, Draggable, Droppable, DropResult } from '@hello-pangea/dnd'
import { Scrollbar } from '@renderer/components/Scrollbar'
import { getProviderLogo } from '@renderer/config/providers'
import { useAllProviders, useProviders } from '@renderer/hooks/useProvider'
import { Provider } from '@renderer/types'
@ -85,56 +86,58 @@ const ProvidersList: FC = () => {
return (
<Container>
<ProviderListContainer>
<ProviderList>
<DragDropContext onDragStart={() => setDragging(true)} onDragEnd={onDragEnd}>
<Droppable droppableId="droppable">
{(provided) => (
<div {...provided.droppableProps} ref={provided.innerRef}>
{providers.map((provider, index) => (
<Draggable key={`draggable_${provider.id}_${index}`} draggableId={provider.id} index={index}>
{(provided) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={{ ...provided.draggableProps.style, marginBottom: 5 }}>
<Dropdown
menu={{ items: provider.isSystem ? [] : getDropdownMenus(provider) }}
trigger={['contextMenu']}>
<ProviderListItem
key={JSON.stringify(provider)}
className={provider.id === selectedProvider?.id ? 'active' : ''}
onClick={() => setSelectedProvider(provider)}>
{provider.isSystem && (
<ProviderLogo shape="square" src={getProviderLogo(provider.id)} size={25} />
)}
{!provider.isSystem && (
<ProviderLogo
size={25}
shape="square"
style={{ backgroundColor: generateColorFromChar(provider.name), minWidth: 25 }}>
{getFirstCharacter(provider.name)}
</ProviderLogo>
)}
<ProviderItemName className="text-nowrap">
{provider.isSystem ? t(`provider.${provider.id}`) : provider.name}
</ProviderItemName>
{provider.enabled && (
<Tag color="green" style={{ marginLeft: 'auto' }}>
ON
</Tag>
)}
</ProviderListItem>
</Dropdown>
</div>
)}
</Draggable>
))}
</div>
)}
</Droppable>
</DragDropContext>
</ProviderList>
<Scrollbar>
<ProviderList>
<DragDropContext onDragStart={() => setDragging(true)} onDragEnd={onDragEnd}>
<Droppable droppableId="droppable">
{(provided) => (
<div {...provided.droppableProps} ref={provided.innerRef}>
{providers.map((provider, index) => (
<Draggable key={`draggable_${provider.id}_${index}`} draggableId={provider.id} index={index}>
{(provided) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={{ ...provided.draggableProps.style, marginBottom: 5 }}>
<Dropdown
menu={{ items: provider.isSystem ? [] : getDropdownMenus(provider) }}
trigger={['contextMenu']}>
<ProviderListItem
key={JSON.stringify(provider)}
className={provider.id === selectedProvider?.id ? 'active' : ''}
onClick={() => setSelectedProvider(provider)}>
{provider.isSystem && (
<ProviderLogo shape="square" src={getProviderLogo(provider.id)} size={25} />
)}
{!provider.isSystem && (
<ProviderLogo
size={25}
shape="square"
style={{ backgroundColor: generateColorFromChar(provider.name), minWidth: 25 }}>
{getFirstCharacter(provider.name)}
</ProviderLogo>
)}
<ProviderItemName className="text-nowrap">
{provider.isSystem ? t(`provider.${provider.id}`) : provider.name}
</ProviderItemName>
{provider.enabled && (
<Tag color="green" style={{ marginLeft: 'auto' }}>
ON
</Tag>
)}
</ProviderListItem>
</Dropdown>
</div>
)}
</Draggable>
))}
</div>
)}
</Droppable>
</DragDropContext>
</ProviderList>
</Scrollbar>
{!dragging && (
<AddButtonWrapper>
<Button type="dashed" style={{ width: '100%' }} icon={<PlusOutlined />} onClick={onAddProvider} />
@ -159,15 +162,12 @@ const ProviderListContainer = styled.div`
width: var(--assistants-width);
height: calc(100vh - var(--navbar-height));
border-right: 0.5px solid var(--color-border);
overflow-y: auto;
`
const ProviderList = styled.div`
display: flex;
flex: 1;
flex-direction: column;
height: calc(100vh - var(--navbar-height));
overflow: auto;
padding: 8px;
`

View File

@ -2344,6 +2344,7 @@ __metadata:
openai: "npm:^4.52.1"
prettier: "npm:^3.2.4"
react: "npm:^18.2.0"
react-custom-scrollbars-2: "npm:^4.5.0"
react-dom: "npm:^18.2.0"
react-i18next: "npm:^14.1.2"
react-markdown: "npm:^9.0.1"
@ -2413,6 +2414,13 @@ __metadata:
languageName: node
linkType: hard
"add-px-to-style@npm:1.0.0":
version: 1.0.0
resolution: "add-px-to-style@npm:1.0.0"
checksum: 10c0/d05d0e3242360e296b5b244d1bfbb946a06338653685af95962291da39ee6db9b33ccc2299a5a0ebef8fde62d39b085997b0b76d4c6098f4c164e539afa8d0f4
languageName: node
linkType: hard
"agent-base@npm:6, agent-base@npm:^6.0.2":
version: 6.0.2
resolution: "agent-base@npm:6.0.2"
@ -4292,6 +4300,17 @@ __metadata:
languageName: node
linkType: hard
"dom-css@npm:^2.0.0":
version: 2.1.0
resolution: "dom-css@npm:2.1.0"
dependencies:
add-px-to-style: "npm:1.0.0"
prefix-style: "npm:2.0.1"
to-camel-case: "npm:1.0.0"
checksum: 10c0/80975ea794f740b8da0ebde8b4a7203bcf017f44027c669f45c71822f9d298fcf62cd5333134af82d9f886719655149a3257ca3c669af920b523bc7e1fc6723c
languageName: node
linkType: hard
"dom-walk@npm:^0.1.0":
version: 0.1.2
resolution: "dom-walk@npm:0.1.2"
@ -9301,6 +9320,13 @@ __metadata:
languageName: node
linkType: hard
"prefix-style@npm:2.0.1":
version: 2.0.1
resolution: "prefix-style@npm:2.0.1"
checksum: 10c0/1db0449b2f7578d30e0ca96cf3014b9dbe42531f0d97dac7ecc7f1369dfbf326fa8e8a321e468ee86e2959f001115c109a0ebe0a4182ca1e920cba10b2d8344d
languageName: node
linkType: hard
"prelude-ls@npm:^1.2.1":
version: 1.2.1
resolution: "prelude-ls@npm:1.2.1"
@ -9392,7 +9418,7 @@ __metadata:
languageName: node
linkType: hard
"prop-types@npm:^15.8.1":
"prop-types@npm:^15.5.10, prop-types@npm:^15.8.1":
version: 15.8.1
resolution: "prop-types@npm:15.8.1"
dependencies:
@ -9501,6 +9527,15 @@ __metadata:
languageName: node
linkType: hard
"raf@npm:^3.1.0":
version: 3.4.1
resolution: "raf@npm:3.4.1"
dependencies:
performance-now: "npm:^2.1.0"
checksum: 10c0/337f0853c9e6a77647b0f499beedafea5d6facfb9f2d488a624f88b03df2be72b8a0e7f9118a3ff811377d534912039a3311815700d2b6d2313f82f736f9eb6e
languageName: node
linkType: hard
"rc-cascader@npm:~3.28.1":
version: 3.28.1
resolution: "rc-cascader@npm:3.28.1"
@ -10049,6 +10084,20 @@ __metadata:
languageName: node
linkType: hard
"react-custom-scrollbars-2@npm:^4.5.0":
version: 4.5.0
resolution: "react-custom-scrollbars-2@npm:4.5.0"
dependencies:
dom-css: "npm:^2.0.0"
prop-types: "npm:^15.5.10"
raf: "npm:^3.1.0"
peerDependencies:
react: ^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
react-dom: ^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
checksum: 10c0/9670bf15bebabbe6c5b75be577430d6a990875874725019fa75db73b0e51300d65158ef962f9b534513b9b70b900244deb11df55c429ba426e580e478634f4f9
languageName: node
linkType: hard
"react-dom@npm:^18.2.0":
version: 18.3.1
resolution: "react-dom@npm:18.3.1"
@ -11786,6 +11835,15 @@ __metadata:
languageName: node
linkType: hard
"to-camel-case@npm:1.0.0":
version: 1.0.0
resolution: "to-camel-case@npm:1.0.0"
dependencies:
to-space-case: "npm:^1.0.0"
checksum: 10c0/357921548908053d774d4b836f42437139c6fc9d73aaf40a1aa59d7317d760541a19667eb2884d9db83902065a90d80b0fe74c59bc13943e8489df9ef4335069
languageName: node
linkType: hard
"to-fast-properties@npm:^2.0.0":
version: 2.0.0
resolution: "to-fast-properties@npm:2.0.0"
@ -11793,6 +11851,13 @@ __metadata:
languageName: node
linkType: hard
"to-no-case@npm:^1.0.0":
version: 1.0.2
resolution: "to-no-case@npm:1.0.2"
checksum: 10c0/c035b04e1042ed67ceb23dc5c7c20ccde11a83ab1d2b3947c17918472b5d26dd4ffdb4cf9464752e7707ab9f3af4a106f9b61244c724bc6810422acd5984da3d
languageName: node
linkType: hard
"to-regex-range@npm:^5.0.1":
version: 5.0.1
resolution: "to-regex-range@npm:5.0.1"
@ -11802,6 +11867,15 @@ __metadata:
languageName: node
linkType: hard
"to-space-case@npm:^1.0.0":
version: 1.0.0
resolution: "to-space-case@npm:1.0.0"
dependencies:
to-no-case: "npm:^1.0.0"
checksum: 10c0/b99e1b5d0f3c90a8d47fa3b155d515027bd83a370740e82ee7cb064f86e3655f030f068bddcb8d18239e7408761b4376d89ab91e5ccdb17dc859d8fd4f570ac5
languageName: node
linkType: hard
"toggle-selection@npm:^1.0.6":
version: 1.0.6
resolution: "toggle-selection@npm:1.0.6"