feat: 一些UI上的优化和重构 (#7479)

- 调整AntdProvider中主题配置,包括颜色、尺寸
- 重构聊天气泡模式的样式
- 重构多选模式的样式
- 添加Selector组件取代ant Select组件
- 重构消息搜索弹窗界面
- 重构知识库搜索弹窗界面
- 优化其他弹框UI
This commit is contained in:
Teo 2025-06-25 14:34:18 +08:00 committed by GitHub
parent 3df5aeb3c3
commit 64b01cce47
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
76 changed files with 1637 additions and 1326 deletions

View File

@ -58,166 +58,80 @@
}
}
.mention-models-dropdown {
&.ant-dropdown {
background: rgba(var(--color-base-rgb), 0.65) !important;
backdrop-filter: blur(35px) saturate(150%) !important;
animation-duration: 0.15s !important;
}
/* 移动其他样式到 mention-models-dropdown 类下 */
.ant-slide-up-enter .ant-dropdown-menu,
.ant-slide-up-appear .ant-dropdown-menu,
.ant-slide-up-leave .ant-dropdown-menu,
.ant-slide-up-enter-active .ant-dropdown-menu,
.ant-slide-up-appear-active .ant-dropdown-menu,
.ant-slide-up-leave-active .ant-dropdown-menu {
background: rgba(var(--color-base-rgb), 0.65) !important;
backdrop-filter: blur(35px) saturate(150%) !important;
}
.ant-dropdown-menu {
/* 保持原有的下拉菜单样式,但限定在 mention-models-dropdown 类下 */
max-height: 400px;
overflow-y: auto;
overflow-x: hidden;
padding: 4px 12px;
position: relative;
background: rgba(var(--color-base-rgb), 0.65) !important;
backdrop-filter: blur(35px) saturate(150%) !important;
border: 0.5px solid rgba(var(--color-border-rgb), 0.3);
border-radius: 10px;
box-shadow:
0 0 0 0.5px rgba(0, 0, 0, 0.15),
0 4px 16px rgba(0, 0, 0, 0.15),
0 2px 8px rgba(0, 0, 0, 0.12),
inset 0 0 0 0.5px rgba(255, 255, 255, var(--inner-glow-opacity, 0.1));
transform-origin: top;
will-change: transform, opacity;
transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
margin-bottom: 0;
&.no-scrollbar {
padding-right: 12px;
}
&.has-scrollbar {
padding-right: 2px;
}
// Scrollbar styles
&::-webkit-scrollbar {
width: 14px;
height: 6px;
}
&::-webkit-scrollbar-thumb {
border: 4px solid transparent;
background-clip: padding-box;
border-radius: 7px;
background-color: var(--color-scrollbar-thumb);
min-height: 50px;
transition: all 0.2s;
}
&:hover::-webkit-scrollbar-thumb {
background-color: var(--color-scrollbar-thumb);
}
&::-webkit-scrollbar-thumb:hover {
background-color: var(--color-scrollbar-thumb-hover);
}
&::-webkit-scrollbar-thumb:active {
background-color: var(--color-scrollbar-thumb-hover);
}
&::-webkit-scrollbar-track {
background: transparent;
border-radius: 7px;
}
}
.ant-dropdown-menu-item-group {
margin-bottom: 4px;
&:not(:first-child) {
margin-top: 4px;
}
.ant-dropdown-menu-item-group-title {
padding: 5px 12px;
color: var(--color-text-3);
font-size: 12px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.03em;
opacity: 0.7;
}
}
// Handle no-results case margin
.no-results {
padding: 8px 12px;
color: var(--color-text-3);
cursor: default;
font-size: 13px;
opacity: 0.8;
margin-bottom: 40px;
&:hover {
background: none;
}
}
.ant-dropdown-menu-item {
padding: 5px 12px;
margin: 0 -12px;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
align-items: center;
gap: 8px;
border-radius: 6px;
font-size: 13px;
&:hover {
background: rgba(var(--color-hover-rgb), 0.5);
}
&.ant-dropdown-menu-item-selected {
background-color: rgba(var(--color-primary-rgb), 0.12);
color: var(--color-primary);
}
.ant-dropdown-menu-item-icon {
margin-right: 0;
opacity: 0.9;
}
}
.ant-dropdown-menu .ant-dropdown-menu-sub {
max-height: 50vh;
width: max-content;
overflow-y: auto;
overflow-x: hidden;
border: 0.5px solid var(--color-border);
}
.ant-dropdown {
background-color: var(--ant-color-bg-elevated);
overflow: hidden;
border-radius: var(--ant-border-radius-lg);
.ant-dropdown-menu {
max-height: 50vh;
overflow-y: auto;
border: 0.5px solid var(--color-border);
.ant-dropdown-menu-sub {
max-height: 50vh;
width: max-content;
overflow-y: auto;
overflow-x: hidden;
border: 0.5px solid var(--color-border);
}
}
.ant-dropdown-arrow + .ant-dropdown-menu {
border: none;
}
}
.ant-select-dropdown {
border: 0.5px solid var(--color-border);
}
.ant-dropdown-menu-submenu {
background-color: var(--ant-color-bg-elevated);
overflow: hidden;
border-radius: var(--ant-border-radius-lg);
}
.ant-popover {
.ant-popover-inner {
border: 0.5px solid var(--color-border);
.ant-popover-inner-content {
max-height: 70vh;
overflow-y: auto;
}
}
.ant-popover-arrow + .ant-popover-content {
.ant-popover-inner {
border: none;
}
}
}
.ant-modal:not(.ant-modal-confirm) {
.ant-modal-confirm-body-has-title {
padding: 16px 0 0 0;
}
.ant-modal-content {
border-radius: 10px;
border: 0.5px solid var(--color-border);
padding: 0 0 8px 0;
.ant-modal-header {
padding: 16px 16px 0 16px;
border-radius: 10px;
}
.ant-modal-body {
max-height: 80vh;
overflow-y: auto;
padding: 0 16px 0 16px;
}
.ant-modal-footer {
padding: 0 16px 8px 16px;
}
.ant-modal-confirm-btns {
margin-bottom: 8px;
}
}
}
.ant-modal.ant-modal-confirm.ant-modal-confirm-confirm {
.ant-modal-content {
padding: 16px;
}
}
.ant-collapse {
border: 1px solid var(--color-border);
@ -227,8 +141,14 @@
}
.ant-collapse-content {
border-top: 1px solid var(--color-border) !important;
border-top: 0.5px solid var(--color-border) !important;
.ant-color-picker & {
border-top: none !important;
}
}
.ant-slider {
.ant-slider-handle::after {
box-shadow: 0 1px 4px 0px rgb(128 128 128 / 50%) !important;
}
}

View File

@ -47,7 +47,7 @@
--color-list-item: #222;
--color-list-item-hover: #1e1e1e;
--modal-background: #1f1f1f;
--modal-background: #111111;
--color-highlight: rgba(0, 0, 0, 1);
--color-background-highlight: rgba(255, 255, 0, 0.9);
@ -66,9 +66,9 @@
--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;
@ -132,8 +132,8 @@
--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);
}

View File

@ -111,27 +111,7 @@ ul {
word-wrap: break-word;
}
.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;
}
.bubble:not(.multi-select-mode) {
.block-wrapper {
display: flow-root;
}
@ -149,30 +129,35 @@ ul {
}
.message-user {
color: var(--chat-text-user);
.message-content-container-user .anticon {
color: var(--chat-text-user) !important;
.message-header {
flex-direction: row-reverse;
text-align: right;
.message-header-info-wrap {
flex-direction: row-reverse;
text-align: right;
}
}
.markdown {
color: var(--chat-text-user);
}
}
.group-grid-container.horizontal,
.group-grid-container.grid {
.message-content-container-assistant {
padding: 0;
}
}
.group-message-wrapper {
background-color: var(--color-background);
.message-content-container {
width: 100%;
border-radius: 10px 0 10px 10px;
padding: 10px 16px 10px 16px;
background-color: var(--chat-background-user);
align-self: self-end;
}
.MessageFooter {
margin-top: 2px;
align-self: self-end;
}
}
.group-menu-bar {
background-color: var(--color-background);
.message-assistant {
.message-content-container {
padding-left: 0;
}
.MessageFooter {
margin-left: 0;
}
}
code {
color: var(--color-text);
}
@ -196,3 +181,9 @@ span.highlight {
span.highlight.selected {
background-color: var(--color-background-highlight-accent);
}
textarea {
&::-webkit-resizer {
display: none;
}
}

View File

@ -98,7 +98,6 @@
border: none;
border-top: 0.5px solid var(--color-border);
margin: 20px 0;
background-color: var(--color-border);
}
span {
@ -119,7 +118,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);
@ -157,15 +156,28 @@
}
table {
border-collapse: collapse;
--table-border-radius: 8px;
margin: 1em 0;
width: 100%;
border-radius: var(--table-border-radius);
overflow: hidden;
border-collapse: separate;
border: 0.5px solid var(--color-border);
border-spacing: 0;
}
th,
td {
border: 0.5px solid var(--color-border);
border-right: 0.5px solid var(--color-border);
border-bottom: 0.5px solid var(--color-border);
padding: 0.5em;
&:last-child {
border-right: none;
}
}
tr:last-child td {
border-bottom: none;
}
th {
@ -238,6 +250,10 @@
text-decoration: underline;
}
}
> *:last-child {
margin-bottom: 0 !important;
}
}
.footnotes {
@ -309,7 +325,7 @@ mjx-container {
/* CodeMirror 相关样式 */
.cm-editor {
border-radius: 5px;
border-radius: inherit;
&.cm-focused {
outline: none;
@ -317,7 +333,7 @@ mjx-container {
.cm-scroller {
font-family: var(--code-font-family);
border-radius: 5px;
border-radius: inherit;
.cm-gutters {
line-height: 1.6;

View File

@ -244,8 +244,7 @@ const ContentContainer = styled.div<{
}>`
position: relative;
overflow: auto;
border: 0.5px solid transparent;
border-radius: 5px;
border-radius: inherit;
margin-top: 0;
/* 动态宽度计算 */
@ -254,6 +253,7 @@ const ContentContainer = styled.div<{
.shiki {
padding: 1em;
border-radius: inherit;
code {
display: flex;
@ -301,7 +301,7 @@ const ContentContainer = styled.div<{
}
}
animation: ${(props) => (props.$fadeIn ? 'contentFadeIn 0.3s ease-in-out forwards' : 'none')};
animation: ${(props) => (props.$fadeIn ? 'contentFadeIn 0.1s ease-in forwards' : 'none')};
`
const CodePlaceholder = styled.div`

View File

@ -273,6 +273,7 @@ const CodeHeader = styled.div<{ $isInSpecialView: boolean }>`
align-items: center;
color: var(--color-text);
font-size: 14px;
line-height: 1;
font-weight: bold;
padding: 0 10px;
border-top-left-radius: 8px;
@ -288,6 +289,10 @@ const SplitViewWrapper = styled.div`
flex: 1 1 auto;
width: 100%;
}
&:not(:has(+ [class*='Container'])) {
border-radius: 0 0 8px 8px;
}
`
export default memo(CodeBlockView)

View File

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

View File

@ -1,87 +1,59 @@
import { Dropdown } from 'antd'
import { useCallback, useEffect, useState } from 'react'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
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 }) => {
const { t } = useTranslation()
const [contextMenuPosition, setContextMenuPosition] = useState<{ x: number; y: number } | null>(null)
const [selectedText, setSelectedText] = useState<string>('')
const [selectedText, setSelectedText] = useState<string | undefined>(undefined)
const handleContextMenu = useCallback(
(e: React.MouseEvent) => {
e.preventDefault()
const _selectedText = window.getSelection()?.toString()
if (_selectedText) {
setContextMenuPosition({ x: e.clientX, y: e.clientY })
setSelectedText(_selectedText)
}
onContextMenu?.(e)
},
[onContextMenu]
)
const contextMenuItems = useMemo(() => {
if (!selectedText) return []
useEffect(() => {
const handleClick = () => {
setContextMenuPosition(null)
}
document.addEventListener('click', handleClick)
return () => {
document.removeEventListener('click', handleClick)
}
}, [])
// 获取右键菜单项
const getContextMenuItems = (t: (key: string) => string, selectedText: string) => [
{
key: 'copy',
label: t('common.copy'),
onClick: () => {
if (selectedText) {
navigator.clipboard
.writeText(selectedText)
.then(() => {
window.message.success({ content: t('message.copied'), key: 'copy-message' })
})
.catch(() => {
window.message.error({ content: t('message.copy.failed'), key: 'copy-message-failed' })
})
}
}
},
{
key: 'quote',
label: t('chat.message.quote'),
onClick: () => {
if (selectedText) {
window.api?.quoteToMainWindow(selectedText)
return [
{
key: 'copy',
label: t('common.copy'),
onClick: () => {
if (selectedText) {
navigator.clipboard
.writeText(selectedText)
.then(() => {
window.message.success({ content: t('message.copied'), key: 'copy-message' })
})
.catch(() => {
window.message.error({ content: t('message.copy.failed'), key: 'copy-message-failed' })
})
}
}
},
{
key: 'quote',
label: t('chat.message.quote'),
onClick: () => {
if (selectedText) {
window.api?.quoteToMainWindow(selectedText)
}
}
}
]
}, [selectedText, t])
const onOpenChange = (open: boolean) => {
if (open) {
const selectedText = window.getSelection()?.toString()
setSelectedText(selectedText)
}
]
}
return (
<ContextContainer onContextMenu={handleContextMenu} className="context-menu-container" style={style}>
{contextMenuPosition && (
<Dropdown
overlayStyle={{ position: 'fixed', left: contextMenuPosition.x, top: contextMenuPosition.y, zIndex: 1000 }}
menu={{ items: getContextMenuItems(t, selectedText) }}
open={true}
trigger={['contextMenu']}>
<div />
</Dropdown>
)}
<Dropdown onOpenChange={onOpenChange} menu={{ items: contextMenuItems }} trigger={['contextMenu']}>
{children}
</ContextContainer>
</Dropdown>
)
}
const ContextContainer = styled.div``
export default ContextMenu

View File

@ -1,5 +1,6 @@
import { Collapse } from 'antd'
import { merge } from 'lodash'
import { ChevronRight } from 'lucide-react'
import { FC, memo, useMemo, useState } from 'react'
interface CustomCollapseProps {
@ -78,6 +79,14 @@ const CustomCollapse: FC<CustomCollapseProps> = ({
destroyInactivePanel={destroyInactivePanel}
collapsible={collapsible}
onChange={setActiveKeys}
expandIcon={({ isActive }) => (
<ChevronRight
size={16}
color="var(--color-text-3)"
strokeWidth={1.5}
style={{ transform: isActive ? 'rotate(90deg)' : 'rotate(0deg)' }}
/>
)}
items={[
{
styles: collapseItemStyles,

View File

@ -0,0 +1,114 @@
import { InputNumber } from 'antd'
import { FC, useEffect, useRef, useState } from 'react'
import styled from 'styled-components'
export interface EditableNumberProps {
value?: number | null
min?: number
max?: number
step?: number
precision?: number
placeholder?: string
disabled?: boolean
changeOnBlur?: boolean
onChange?: (value: number | null) => void
onBlur?: () => void
style?: React.CSSProperties
className?: string
size?: 'small' | 'middle' | 'large'
suffix?: string
prefix?: string
align?: 'start' | 'center' | 'end'
}
const EditableNumber: FC<EditableNumberProps> = ({
value,
min,
max,
step = 0.01,
precision,
placeholder,
disabled = false,
onChange,
onBlur,
changeOnBlur = false,
style,
className,
size = 'middle',
align = 'end'
}) => {
const [isEditing, setIsEditing] = useState(false)
const [inputValue, setInputValue] = useState(value)
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
setInputValue(value)
}, [value])
const handleFocus = () => {
if (disabled) return
setIsEditing(true)
}
const handleInputChange = (newValue: number | null) => {
onChange?.(newValue ?? null)
}
const handleBlur = () => {
setIsEditing(false)
onBlur?.()
}
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
handleBlur()
} else if (e.key === 'Escape') {
setInputValue(value)
setIsEditing(false)
}
}
return (
<Container>
<InputNumber
style={{ ...style, opacity: isEditing ? 1 : 0 }}
ref={inputRef}
value={inputValue}
min={min}
max={max}
step={step}
precision={precision}
size={size}
onChange={handleInputChange}
onBlur={handleBlur}
onFocus={handleFocus}
onKeyDown={handleKeyDown}
className={className}
controls={isEditing}
changeOnBlur={changeOnBlur}
/>
<DisplayText style={style} className={className} $align={align} $isEditing={isEditing}>
{value ?? placeholder}
</DisplayText>
</Container>
)
}
const Container = styled.div`
display: inline-block;
position: relative;
`
const DisplayText = styled.div<{
$align: 'start' | 'center' | 'end'
$isEditing: boolean
}>`
position: absolute;
inset: 0;
display: ${({ $isEditing }) => ($isEditing ? 'none' : 'flex')};
align-items: center;
justify-content: ${({ $align }) => $align};
pointer-events: none;
`
export default EditableNumber

View File

@ -35,17 +35,38 @@ const MultiSelectActionPopup: FC<Props> = ({ topic }) => {
<SelectionCount>{t('common.selectedMessages', { count: selectedMessageIds.length })}</SelectionCount>
<ActionButtons>
<Tooltip title={t('common.save')}>
<ActionButton icon={<Save size={16} />} disabled={isActionDisabled} onClick={() => handleAction('save')} />
<Button
shape="circle"
color="default"
variant="text"
icon={<Save size={16} />}
disabled={isActionDisabled}
onClick={() => handleAction('save')}
/>
</Tooltip>
<Tooltip title={t('common.copy')}>
<ActionButton icon={<Copy size={16} />} disabled={isActionDisabled} onClick={() => handleAction('copy')} />
<Button
shape="circle"
color="default"
variant="text"
icon={<Copy size={16} />}
disabled={isActionDisabled}
onClick={() => handleAction('copy')}
/>
</Tooltip>
<Tooltip title={t('common.delete')}>
<ActionButton danger icon={<Trash size={16} />} onClick={() => handleAction('delete')} />
<Button
shape="circle"
color="danger"
variant="text"
danger
icon={<Trash size={16} />}
onClick={() => handleAction('delete')}
/>
</Tooltip>
</ActionButtons>
<Tooltip title={t('chat.navigation.close')}>
<ActionButton icon={<X size={16} />} onClick={handleClose} />
<Button shape="circle" color="default" variant="text" icon={<X size={16} />} onClick={handleClose} />
</Tooltip>
</ActionBar>
</Container>
@ -53,45 +74,38 @@ const MultiSelectActionPopup: FC<Props> = ({ topic }) => {
}
const Container = styled.div`
width: 100%;
padding: 36px 20px;
background-color: var(--color-background);
border-top: 1px solid var(--color-border);
position: fixed;
inset: auto 0 0 0;
z-index: 1000;
display: flex;
justify-content: center;
align-items: center;
padding: 16px;
`
const ActionBar = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
background-color: var(--color-background);
padding: 4px 4px;
border-radius: 99px;
box-shadow: 0 0px 5px 0px rgb(128 128 128 / 30%);
border: 0.5px solid var(--color-border);
gap: 16px;
`
const ActionButtons = styled.div`
display: flex;
gap: 16px;
`
const ActionButton = styled(Button)`
display: flex;
align-items: center;
justify-content: center;
padding: 8px 16px;
border-radius: 50%;
.anticon {
font-size: 16px;
}
&:hover {
background-color: var(--color-background-mute);
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
gap: 8px;
`
const SelectionCount = styled.div`
margin-right: 15px;
color: var(--color-text-2);
font-size: 14px;
padding-left: 8px;
flex-shrink: 0;
`
export default MultiSelectActionPopup

View File

@ -32,16 +32,23 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
onCancel={onCancel}
afterClose={onClose}
title={null}
width="920px"
width={700}
transitionName="animation-move-down"
styles={{
content: {
borderRadius: 20,
padding: 0,
border: `1px solid var(--color-frame-border)`
overflow: 'hidden',
paddingBottom: 16
},
body: { height: '85vh' }
body: {
height: '80vh',
maxHeight: 'inherit',
padding: 0
}
}}
centered
closable={false}
footer={null}>
<HistoryPage />
</Modal>

View File

@ -388,8 +388,11 @@ const PopupContainer: React.FC<Props> = ({ model, resolve }) => {
borderRadius: 20,
padding: 0,
overflow: 'hidden',
paddingBottom: 20,
border: '1px solid var(--color-border)'
paddingBottom: 16
},
body: {
maxHeight: 'inherit',
padding: 0
}
}}
closeIcon={null}

View File

@ -48,17 +48,17 @@ const Scrollbar: FC<Props> = ({ ref: passedRef, children, onScroll: externalOnSc
}, [throttledInternalScrollHandler, clearScrollingTimeout])
return (
<Container
<ScrollBarContainer
{...htmlProps} // Pass other HTML attributes
$isScrolling={isScrolling}
onScroll={combinedOnScroll} // Use the combined handler
ref={passedRef}>
{children}
</Container>
</ScrollBarContainer>
)
}
const Container = styled.div<{ $isScrolling: boolean }>`
const ScrollBarContainer = styled.div<{ $isScrolling: boolean }>`
overflow-y: auto;
&::-webkit-scrollbar-thumb {
transition: background 2s ease;

View File

@ -0,0 +1,192 @@
import { Dropdown, DropdownProps } from 'antd'
import { Check, ChevronsUpDown } from 'lucide-react'
import { ReactNode, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled, { css } from 'styled-components'
interface SelectorOption<V = string | number> {
label: string | ReactNode
value: V
type?: 'group'
options?: SelectorOption<V>[]
disabled?: boolean
}
interface BaseSelectorProps<V = string | number> {
options: SelectorOption<V>[]
placeholder?: string
placement?: 'topLeft' | 'topCenter' | 'topRight' | 'bottomLeft' | 'bottomCenter' | 'bottomRight' | 'top' | 'bottom'
/** 字体大小 */
size?: number
/** 是否禁用 */
disabled?: boolean
}
interface SingleSelectorProps<V> extends BaseSelectorProps<V> {
multiple?: false
value?: V
onChange: (value: V) => void
}
interface MultipleSelectorProps<V> extends BaseSelectorProps<V> {
multiple: true
value?: V[]
onChange: (value: V[]) => void
}
type SelectorProps<V> = SingleSelectorProps<V> | MultipleSelectorProps<V>
const Selector = <V extends string | number>({
options,
value,
onChange = () => {},
placement = 'bottomRight',
size = 13,
placeholder,
disabled = false,
multiple = false
}: SelectorProps<V>) => {
const [open, setOpen] = useState(false)
const { t } = useTranslation()
const inputRef = useRef<any>(null)
useEffect(() => {
if (open) {
setTimeout(() => {
inputRef.current?.focus()
}, 1)
}
}, [open])
const selectedValues = useMemo(() => {
if (multiple) {
return (value as V[]) || []
}
return value !== undefined ? [value as V] : []
}, [value, multiple])
const label = useMemo(() => {
if (selectedValues.length > 0) {
const findLabels = (opts: SelectorOption<V>[]): (string | ReactNode)[] => {
const labels: (string | ReactNode)[] = []
for (const opt of opts) {
if (selectedValues.includes(opt.value)) {
labels.push(opt.label)
}
if (opt.options) {
labels.push(...findLabels(opt.options))
}
}
return labels
}
const labels = findLabels(options)
if (labels.length === 0) return placeholder
if (labels.length === 1) return labels[0]
return t('common.selectedItems', { count: labels.length })
}
return placeholder
}, [selectedValues, placeholder, options, t])
const items = useMemo(() => {
const mapOption = (option: SelectorOption<V>) => ({
key: option.value,
label: option.label,
extra: <CheckIcon>{selectedValues.includes(option.value) && <Check size={14} />}</CheckIcon>,
disabled: option.disabled,
type: option.type || (option.options ? 'group' : undefined),
children: option.options?.map(mapOption)
})
return options.map(mapOption)
}, [options, selectedValues])
function onClick(e: { key: string }) {
if (disabled) return
const newValue = e.key as V
if (multiple) {
const newValues = selectedValues.includes(newValue)
? selectedValues.filter((v) => v !== newValue)
: [...selectedValues, newValue]
;(onChange as MultipleSelectorProps<V>['onChange'])(newValues)
} else {
;(onChange as SingleSelectorProps<V>['onChange'])(newValue)
setOpen(false)
}
}
const handleOpenChange: DropdownProps['onOpenChange'] = (nextOpen, info) => {
if (disabled) return
if (info.source === 'trigger' || nextOpen) {
setOpen(nextOpen)
}
}
return (
<Dropdown
overlayClassName="selector-dropdown"
menu={{ items, onClick }}
trigger={['click']}
placement={placement}
open={open && !disabled}
onOpenChange={handleOpenChange}>
<Label $size={size} $open={open} $disabled={disabled} $isPlaceholder={label === placeholder}>
{label}
<LabelIcon size={size + 3} />
</Label>
</Dropdown>
)
}
const LabelIcon = styled(ChevronsUpDown)`
border-radius: 4px;
padding: 2px 0;
background-color: var(--color-background-soft);
transition: background-color 0.2s;
`
const Label = styled.div<{ $size: number; $open: boolean; $disabled: boolean; $isPlaceholder: boolean }>`
display: flex;
align-items: center;
gap: 4px;
border-radius: 99px;
padding: 3px 2px 3px 10px;
font-size: ${({ $size }) => $size}px;
line-height: 1;
cursor: ${({ $disabled }) => ($disabled ? 'not-allowed' : 'pointer')};
opacity: ${({ $disabled }) => ($disabled ? 0.6 : 1)};
color: ${({ $isPlaceholder }) => ($isPlaceholder ? 'var(--color-text-2)' : 'inherit')};
transition:
background-color 0.2s,
opacity 0.2s;
&:hover {
${({ $disabled }) =>
!$disabled &&
css`
background-color: var(--color-background-mute);
${LabelIcon} {
background-color: var(--color-background-mute);
}
`}
}
${({ $open, $disabled }) =>
$open &&
!$disabled &&
css`
background-color: var(--color-background-mute);
${LabelIcon} {
background-color: var(--color-background-mute);
}
`}
`
const CheckIcon = styled.div`
width: 20px;
display: flex;
align-items: center;
justify-content: end;
`
export default Selector

View File

@ -38,7 +38,20 @@ const AntdProvider: FC<PropsWithChildren> = ({ children }) => {
boxShadowSecondary: 'none',
defaultShadow: 'none',
dangerShadow: 'none',
primaryShadow: 'none'
primaryShadow: 'none',
controlHeight: 30,
paddingInline: 10
},
Input: {
controlHeight: 30,
colorBorder: 'var(--color-border)'
},
InputNumber: {
colorBorder: 'var(--color-border)'
},
Select: {
controlHeight: 30,
colorBorder: 'var(--color-border)'
},
Collapse: {
headerBg: 'transparent'
@ -50,13 +63,47 @@ const AntdProvider: FC<PropsWithChildren> = ({ children }) => {
fontFamily: 'var(--code-font-family)'
},
Segmented: {
itemActiveBg: 'var(--color-background-mute)',
itemHoverBg: 'var(--color-background-mute)'
itemActiveBg: 'var(--color-background-soft)',
itemHoverBg: 'var(--color-background-soft)',
trackBg: 'rgba(153,153,153,0.15)'
},
Switch: {
colorTextQuaternary: 'rgba(153,153,153,0.20)',
trackMinWidth: 40,
handleSize: 19,
trackMinWidthSM: 28,
trackHeightSM: 17,
handleSizeSM: 14,
trackPadding: 1.5
},
Dropdown: {
controlPaddingHorizontal: 8,
borderRadiusLG: 10,
borderRadiusSM: 8
},
Popover: {
borderRadiusLG: 10
},
Slider: {
handleLineWidth: 1.5,
handleSize: 15,
handleSizeHover: 15,
dotSize: 7,
railSize: 5,
colorBgElevated: '#ffffff'
},
Modal: {
colorBgElevated: 'var(--modal-background)'
},
Divider: {
colorSplit: 'rgba(128,128,128,0.15)'
}
},
token: {
colorPrimary: colorPrimary,
fontFamily: 'var(--font-family)'
fontFamily: 'var(--font-family)',
colorBgMask: _theme === 'dark' ? 'rgba(0,0,0,0.7)' : 'rgba(255,255,255,0.8)',
motionDurationMid: '100ms'
}
}}>
{children}

View File

@ -412,6 +412,7 @@
"search": "Search",
"select": "Select",
"selectedMessages": "Selected {{count}} messages",
"selectedItems": "Selected {{count}} items",
"success": "Success",
"topics": "Topics",
"warning": "Warning",

View File

@ -412,6 +412,7 @@
"search": "検索",
"select": "選択",
"selectedMessages": "{{count}}件のメッセージを選択しました",
"selectedItems": "{{count}}件の項目を選択しました",
"success": "成功",
"topics": "トピック",
"warning": "警告",

View File

@ -412,6 +412,7 @@
"search": "Поиск",
"select": "Выбрать",
"selectedMessages": "Выбрано {{count}} сообщений",
"selectedItems": "Выбрано {{count}} элементов",
"success": "Успешно",
"topics": "Топики",
"warning": "Предупреждение",

View File

@ -412,6 +412,7 @@
"search": "搜索",
"select": "选择",
"selectedMessages": "选中 {{count}} 条消息",
"selectedItems": "已选择 {{count}} 项",
"success": "成功",
"topics": "话题",
"warning": "警告",

View File

@ -412,6 +412,7 @@
"search": "搜尋",
"select": "選擇",
"selectedMessages": "選中 {{count}} 條訊息",
"selectedItems": "已選擇 {{count}} 項",
"success": "成功",
"topics": "話題",
"warning": "警告",

View File

@ -14,6 +14,7 @@ import { Agent, KnowledgeBase } from '@renderer/types'
import { getLeadingEmoji, uuid } from '@renderer/utils'
import { Button, Form, FormInstance, Input, Modal, Popover, Select, SelectProps } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import { ChevronDown } from 'lucide-react'
import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import stringWidth from 'string-width'
@ -150,7 +151,7 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
maskClosable={false}
afterClose={onClose}
okText={t('agents.add.title')}
width={800}
width={600}
transitionName="animation-move-down"
centered>
<Form
@ -212,6 +213,7 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
.toLowerCase()
.includes(input.toLowerCase())
}
suffixIcon={<ChevronDown size={16} color="var(--color-border)" />}
/>
</Form.Item>
)}

View File

@ -4,7 +4,7 @@ import { getDefaultModel } from '@renderer/services/AssistantService'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { Agent } from '@renderer/types'
import { uuid } from '@renderer/utils'
import { Button, Form, Input, Modal, Radio, Space } from 'antd'
import { Button, Flex, Form, Input, Modal, Radio } from 'antd'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -98,7 +98,14 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
title={t('agents.import.title')}
open={open}
onCancel={onCancel}
footer={null}
footer={
<Flex justify="end" gap={8}>
<Button onClick={onCancel}>{t('common.cancel')}</Button>
<Button type="primary" onClick={() => form.submit()} loading={loading}>
{t('agents.import.button')}
</Button>
</Flex>
}
transitionName="animation-move-down"
centered>
<Form form={form} onFinish={onFinish} layout="vertical">
@ -120,15 +127,6 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
<Button onClick={() => form.submit()}>{t('agents.import.select_file')}</Button>
</Form.Item>
)}
<Form.Item>
<Space>
<Button onClick={onCancel}>{t('common.cancel')}</Button>
<Button type="primary" onClick={() => form.submit()} loading={loading}>
{t('agents.import.button')}
</Button>
</Space>
</Form.Item>
</Form>
</Modal>
)

View File

@ -1,11 +1,11 @@
import { ArrowLeftOutlined, EnterOutlined } from '@ant-design/icons'
import { HStack } from '@renderer/components/Layout'
import { useAppDispatch } from '@renderer/store'
import { loadTopicMessagesThunk } from '@renderer/store/thunk/messageThunk'
import { Topic } from '@renderer/types'
import type { Message } from '@renderer/types/newMessage'
import { Input, InputRef } from 'antd'
import { Divider, Input, InputRef } from 'antd'
import { last } from 'lodash'
import { Search } from 'lucide-react'
import { ChevronLeft, CornerDownLeft, Search } from 'lucide-react'
import { FC, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
@ -73,26 +73,35 @@ const TopicsPage: FC = () => {
return (
<Container>
<Header>
{stack.length > 1 && (
<HeaderLeft>
<MenuIcon onClick={goBack}>
<ArrowLeftOutlined />
</MenuIcon>
</HeaderLeft>
)}
<SearchInput
placeholder={t('history.search.placeholder')}
type="search"
value={search}
autoFocus
allowClear
<HStack style={{ padding: '0 12px', marginTop: 8 }}>
<Input
prefix={
stack.length > 1 ? (
<SearchIcon className="back-icon" onClick={goBack}>
<ChevronLeft size={16} />
</SearchIcon>
) : (
<SearchIcon>
<Search size={15} />
</SearchIcon>
)
}
suffix={search.length >= 2 ? <CornerDownLeft size={16} /> : null}
ref={inputRef}
placeholder={t('history.search.placeholder')}
value={search}
onChange={(e) => setSearch(e.target.value.trimStart())}
suffix={search.length >= 2 ? <EnterOutlined /> : <Search size={16} />}
allowClear
autoFocus
spellCheck={false}
style={{ paddingLeft: 0 }}
variant="borderless"
size="middle"
onPressEnter={onSearch}
/>
</Header>
</HStack>
<Divider style={{ margin: 0, marginTop: 4, borderBlockStartWidth: 0.5 }} />
<TopicsHistory
keywords={search}
onClick={onTopicClick as any}
@ -118,50 +127,23 @@ const Container = styled.div`
height: 100%;
`
const Header = styled.div`
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding: 12px 0;
width: 100%;
position: relative;
background-color: var(--color-background-mute);
border-top-left-radius: 8px;
border-top-right-radius: 8px;
border-bottom: 0.5px solid var(--color-frame-border);
`
const HeaderLeft = styled.div`
display: flex;
flex-direction: row;
align-items: center;
position: absolute;
top: 12px;
left: 15px;
`
const MenuIcon = styled.div`
cursor: pointer;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
width: 33px;
height: 33px;
const SearchIcon = styled.div`
width: 32px;
height: 32px;
border-radius: 50%;
&:hover {
background-color: var(--color-background);
.anticon {
color: var(--color-text-1);
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
background-color: var(--color-background-soft);
margin-right: 2px;
&.back-icon {
cursor: pointer;
transition: background-color 0.2s;
&:hover {
background-color: var(--color-background-mute);
}
}
`
const SearchInput = styled(Input)`
border-radius: 30px;
width: 800px;
height: 36px;
`
export default TopicsPage

View File

@ -1,7 +1,5 @@
import { ArrowRightOutlined } from '@ant-design/icons'
import { HStack } from '@renderer/components/Layout'
import { MessageEditingProvider } from '@renderer/context/MessageEditingContext'
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'
@ -10,6 +8,7 @@ import { Topic } from '@renderer/types'
import type { Message } from '@renderer/types/newMessage'
import { runAsyncFunction } from '@renderer/utils'
import { Button } from 'antd'
import { Forward } from 'lucide-react'
import { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
@ -20,7 +19,6 @@ 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)
@ -43,18 +41,18 @@ const SearchMessage: FC<Props> = ({ message, ...props }) => {
return (
<MessageEditingProvider>
<MessagesContainer {...props} className={messageStyle}>
<ContainerWrapper style={{ paddingTop: 20, paddingBottom: 20, position: 'relative' }}>
<MessagesContainer {...props}>
<ContainerWrapper>
<MessageItem message={message} topic={topic} hideMenuBar={true} />
<Button
type="text"
size="middle"
style={{ color: 'var(--color-text-3)', position: 'absolute', right: 0, top: 10 }}
style={{ color: 'var(--color-text-3)', position: 'absolute', right: 16, top: 16 }}
onClick={() => locateToMessage(navigate, message)}
icon={<ArrowRightOutlined />}
icon={<Forward size={16} />}
/>
<HStack mt="10px" justifyContent="center">
<Button onClick={() => locateToMessage(navigate, message)} icon={<ArrowRightOutlined />}>
<Button onClick={() => locateToMessage(navigate, message)} icon={<Forward size={16} />}>
{t('history.locate.message')}
</Button>
</HStack>
@ -74,12 +72,11 @@ const MessagesContainer = styled.div`
`
const ContainerWrapper = styled.div`
width: 800px;
width: 100%;
display: flex;
flex-direction: column;
.message {
padding: 0;
}
padding: 16px;
position: relative;
`
export default SearchMessage

View File

@ -151,7 +151,8 @@ const Container = styled.div`
`
const ContainerWrapper = styled.div`
width: 800px;
width: 100%;
padding: 0 16px;
display: flex;
flex-direction: column;
`

View File

@ -1,9 +1,8 @@
import { ArrowRightOutlined, MessageOutlined } from '@ant-design/icons'
import { 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 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'
@ -13,6 +12,7 @@ import { loadTopicMessagesThunk } from '@renderer/store/thunk/messageThunk'
import { Topic } from '@renderer/types'
import { Button, Divider, Empty } from 'antd'
import { t } from 'i18next'
import { Forward } from 'lucide-react'
import { FC, useEffect } from 'react'
import styled from 'styled-components'
@ -25,7 +25,6 @@ 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()
useEffect(() => {
@ -48,8 +47,8 @@ const TopicMessages: FC<Props> = ({ topic, ...props }) => {
return (
<MessageEditingProvider>
<MessagesContainer {...props} ref={containerRef} onScroll={handleScroll} className={messageStyle}>
<ContainerWrapper style={{ paddingTop: 30, paddingBottom: 30 }}>
<MessagesContainer {...props} ref={containerRef} onScroll={handleScroll}>
<ContainerWrapper>
{topic?.messages.map((message) => (
<div key={message.id} style={{ position: 'relative' }}>
<MessageItem message={message} topic={topic} hideMenuBar={true} />
@ -58,7 +57,7 @@ const TopicMessages: FC<Props> = ({ topic, ...props }) => {
size="middle"
style={{ color: 'var(--color-text-3)', position: 'absolute', right: 0, top: 5 }}
onClick={() => locateToMessage(navigate, message)}
icon={<ArrowRightOutlined />}
icon={<Forward size={16} />}
/>
<Divider style={{ margin: '8px auto 15px' }} variant="dashed" />
</div>
@ -86,12 +85,10 @@ const MessagesContainer = styled.div`
`
const ContainerWrapper = styled.div`
width: 800px;
width: 100%;
padding: 16px;
display: flex;
flex-direction: column;
.message {
padding: 0;
}
`
export default TopicMessages

View File

@ -78,7 +78,8 @@ const TopicsHistory: React.FC<Props> = ({ keywords, onClick, onSearch, ...props
}
const ContainerWrapper = styled.div`
width: 800px;
width: 100%;
padding: 0 16px;
display: flex;
flex-direction: column;
`

View File

@ -7,6 +7,7 @@ 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 { classNames } from '@renderer/utils'
import { Flex } from 'antd'
import { debounce } from 'lodash'
import React, { FC, useMemo, useState } from 'react'
@ -106,7 +107,7 @@ const Chat: FC<Props> = (props) => {
}
return (
<Container id="chat" className={messageStyle}>
<Container id="chat" className={classNames([messageStyle, { 'multi-select-mode': isMultiSelectMode }])}>
<Main ref={mainRef} id="chat-main" vertical flex={1} justify="space-between" style={{ maxWidth }}>
<ContentSearch
ref={contentSearchRef}

View File

@ -139,17 +139,21 @@ 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
}
if (textArea?.scrollHeight) {
textArea.style.height = Math.min(textArea.scrollHeight, 400) + 'px'
}
}
textArea.style.height = 'auto'
textArea.style.height = textArea?.scrollHeight > 400 ? '400px' : `${textArea?.scrollHeight}px`
}
}, [textareaHeight])
},
[textareaHeight]
)
const sendMessage = useCallback(async () => {
if (inputEmpty || loading) {
@ -749,13 +753,13 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
}
return (
<Container
onDragOver={handleDragOver}
onDrop={handleDrop}
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
className="inputbar">
<NarrowLayout style={{ width: '100%' }}>
<NarrowLayout style={{ width: '100%' }}>
<Container
onDragOver={handleDragOver}
onDrop={handleDrop}
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
className="inputbar">
<QuickPanelView setInputText={setText} />
<InputBarContainer
id="inputbar"
@ -787,7 +791,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
ref={textareaRef}
style={{
fontSize,
minHeight: textareaHeight ? `${textareaHeight}px` : undefined
minHeight: textareaHeight ? `${textareaHeight}px` : '30px'
}}
styles={{ textarea: TextareaStyle }}
onFocus={(e: React.FocusEvent<HTMLTextAreaElement>) => {
@ -851,8 +855,8 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
</ToolbarMenu>
</Toolbar>
</InputBarContainer>
</NarrowLayout>
</Container>
</Container>
</NarrowLayout>
)
}
@ -887,16 +891,15 @@ const Container = styled.div`
flex-direction: column;
position: relative;
z-index: 2;
padding: 0 16px 16px 16px;
`
const InputBarContainer = styled.div`
border: 0.5px solid var(--color-border);
transition: all 0.2s ease;
position: relative;
margin: 14px 20px;
margin-top: 0;
border-radius: 15px;
padding-top: 6px; // 为拖动手柄留出空间
padding-top: 8px; // 为拖动手柄留出空间
background-color: var(--color-background-opacity);
&.file-dragging {
@ -919,7 +922,7 @@ const InputBarContainer = styled.div`
const TextareaStyle: CSSProperties = {
paddingLeft: 0,
padding: '6px 15px 8px' // 减小顶部padding
padding: '6px 15px 0px' // 减小顶部padding
}
const Textarea = styled(TextArea)`
@ -934,16 +937,17 @@ const Textarea = styled(TextArea)`
&.ant-input {
line-height: 1.4;
}
&::-webkit-scrollbar {
width: 3px;
}
`
const Toolbar = styled.div`
display: flex;
flex-direction: row;
justify-content: space-between;
padding: 0 8px;
padding-bottom: 0;
margin-bottom: 4px;
height: 30px;
padding: 5px 8px;
height: 40px;
gap: 16px;
position: relative;
z-index: 2;

View File

@ -45,7 +45,7 @@ const TokenCount: FC<Props> = ({ estimateTokenCount, inputTokenCount, contextCou
return (
<Container>
<Popover content={PopoverContent}>
<Popover content={PopoverContent} arrow={false}>
<MenuOutlined /> {contextCount.current} / {formatMaxCount(contextCount.max)}
<Divider type="vertical" style={{ marginTop: 0, marginLeft: 5, marginRight: 5 }} />
<ArrowUpOutlined />

View File

@ -54,9 +54,10 @@ const CitationTooltip: React.FC<CitationTooltipProps> = ({ children, citation })
return (
<Tooltip
arrow={false}
overlay={tooltipContent}
placement="top"
color="var(--color-background-mute)"
color="var(--color-background)"
styles={{
body: {
border: '1px solid var(--color-border)',

View File

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

View File

@ -93,7 +93,7 @@ describe('CitationTooltip', () => {
const tooltip = screen.getByTestId('tooltip-wrapper')
expect(tooltip).toHaveAttribute('data-placement', 'top')
expect(tooltip).toHaveAttribute('data-color', 'var(--color-background-mute)')
expect(tooltip).toHaveAttribute('data-color', 'var(--color-background)')
const styles = JSON.parse(tooltip.getAttribute('data-styles') || '{}')
expect(styles.body).toEqual({

View File

@ -47,7 +47,7 @@ exports[`CitationTooltip > basic rendering > should match snapshot 1`] = `
}
<div
data-color="var(--color-background-mute)"
data-color="var(--color-background)"
data-placement="top"
data-styles="{"body":{"border":"1px solid var(--color-border)","padding":"12px","borderRadius":"8px"}}"
data-testid="tooltip-wrapper"

View File

@ -18,12 +18,12 @@ const ImageBlock: React.FC<Props> = ({ block }) => {
? [`file://${block?.file?.path}`]
: []
return (
<Container style={{ marginBottom: 8 }}>
<Container>
{images.map((src, index) => (
<ImageViewer
src={src}
key={`image-${index}`}
style={{ maxWidth: 500, maxHeight: 500, padding: 5, borderRadius: 8 }}
style={{ maxWidth: 500, maxHeight: 500, padding: 0, borderRadius: 8 }}
/>
))}
</Container>
@ -34,6 +34,5 @@ const Container = styled.div`
display: flex;
flex-direction: row;
gap: 10px;
margin-top: 8px;
`
export default React.memo(ImageBlock)

View File

@ -3,7 +3,7 @@ import { useSettings } from '@renderer/hooks/useSettings'
import { MessageBlockStatus, type ThinkingMessageBlock } from '@renderer/types/newMessage'
import { lightbulbVariants } from '@renderer/utils/motionVariants'
import { Collapse, message as antdMessage, Tooltip } from 'antd'
import { Lightbulb } from 'lucide-react'
import { ChevronRight, Lightbulb } from 'lucide-react'
import { motion } from 'motion/react'
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -57,6 +57,14 @@ const ThinkingBlock: React.FC<Props> = ({ block }) => {
size="small"
onChange={() => setActiveKey((key) => (key ? '' : 'thought'))}
className="message-thought-container"
expandIcon={({ isActive }) => (
<ChevronRight
color="var(--color-text-3)"
size={16}
strokeWidth={1.5}
style={{ transform: isActive ? 'rotate(90deg)' : 'rotate(0deg)' }}
/>
)}
expandIconPosition="end"
items={[
{

View File

@ -164,17 +164,7 @@ export default React.memo(MessageBlockRenderer)
const ImageBlockGroup = styled.div`
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
grid-template-columns: repeat(3, minmax(200px, 1fr));
gap: 8px;
max-width: 960px;
/* > * {
min-width: 200px;
} */
@media (min-width: 1536px) {
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
max-width: 1280px;
> * {
min-width: 250px;
}
}
`

View File

@ -1,10 +1,9 @@
import ContextMenu from '@renderer/components/ContextMenu'
import Favicon from '@renderer/components/Icons/FallbackFavicon'
import { HStack } from '@renderer/components/Layout'
import { fetchWebContent } from '@renderer/utils/fetch'
import { cleanMarkdownContent } from '@renderer/utils/formats'
import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query'
import { Button, Drawer, message, Skeleton } from 'antd'
import { Button, message, Popover, Skeleton } from 'antd'
import { Check, Copy, FileSearch } from 'lucide-react'
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -48,16 +47,49 @@ const truncateText = (text: string, maxLength = 100) => {
const CitationsList: React.FC<CitationsListProps> = ({ citations }) => {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
const previewItems = citations.slice(0, 3)
const count = citations.length
if (!count) return null
const popoverContent = (
<PopoverContent>
{citations.map((citation) => (
<PopoverContentItem key={citation.url || citation.number}>
{citation.type === 'websearch' ? (
<WebSearchCitation citation={citation} />
) : (
<KnowledgeCitation citation={citation} />
)}
</PopoverContentItem>
))}
</PopoverContent>
)
return (
<QueryClientProvider client={queryClient}>
<>
<OpenButton type="text" onClick={() => setOpen(true)}>
<Popover
arrow={false}
content={popoverContent}
title={
<div
style={{
padding: '8px 12px 8px',
marginBottom: -8,
fontWeight: 'bold',
borderBottom: '0.5px solid var(--color-border)'
}}>
{t('message.citations')}
</div>
}
placement="right"
trigger="hover"
styles={{
body: {
padding: '0 0 8px 0'
}
}}>
<OpenButton type="text">
<PreviewIcons>
{previewItems.map((c, i) => (
<PreviewIcon key={i} style={{ zIndex: previewItems.length - i }}>
@ -71,27 +103,7 @@ const CitationsList: React.FC<CitationsListProps> = ({ citations }) => {
</PreviewIcons>
{t('message.citation', { count })}
</OpenButton>
<Drawer
title={t('message.citations')}
placement="right"
onClose={() => setOpen(false)}
open={open}
width={680}
styles={{ header: { border: 'none' }, body: { paddingTop: 0 } }}
destroyOnClose={false}>
{open &&
citations.map((citation) => (
<HStack key={citation.url || citation.number} style={{ alignItems: 'center', gap: 8, marginBottom: 12 }}>
{citation.type === 'websearch' ? (
<WebSearchCitation citation={citation} />
) : (
<KnowledgeCitation citation={citation} />
)}
</HStack>
))}
</Drawer>
</>
</Popover>
</QueryClientProvider>
)
}
@ -136,16 +148,17 @@ const WebSearchCitation: React.FC<{ citation: Citation }> = ({ citation }) => {
})
return (
<WebSearchCard>
<ContextMenu>
<ContextMenu>
<WebSearchCard>
<WebSearchCardHeader>
<CitationIndex>{citation.number}</CitationIndex>
{citation.showFavicon && citation.url && (
<Favicon hostname={new URL(citation.url).hostname} alt={citation.title || citation.hostname || ''} />
)}
<CitationLink className="text-nowrap" href={citation.url} onClick={(e) => handleLinkClick(citation.url, e)}>
{citation.title || <span className="hostname">{citation.hostname}</span>}
</CitationLink>
<CitationIndex>{citation.number}</CitationIndex>
{fetchedContent && <CopyButton content={fetchedContent} />}
</WebSearchCardHeader>
{isLoading ? (
@ -153,28 +166,29 @@ const WebSearchCitation: React.FC<{ citation: Citation }> = ({ citation }) => {
) : (
<WebSearchCardContent className="selectable-text">{fetchedContent}</WebSearchCardContent>
)}
</ContextMenu>
</WebSearchCard>
</WebSearchCard>
</ContextMenu>
)
}
const KnowledgeCitation: React.FC<{ citation: Citation }> = ({ citation }) => {
return (
<WebSearchCard>
<ContextMenu>
<ContextMenu>
<WebSearchCard>
<WebSearchCardHeader>
<CitationIndex>{citation.number}</CitationIndex>
{citation.showFavicon && <FileSearch width={16} />}
<CitationLink className="text-nowrap" href={citation.url} onClick={(e) => handleLinkClick(citation.url, e)}>
{citation.title}
</CitationLink>
<CitationIndex>{citation.number}</CitationIndex>
{citation.content && <CopyButton content={citation.content} />}
</WebSearchCardHeader>
<WebSearchCardContent className="selectable-text">
{citation.content && truncateText(citation.content, 100)}
</WebSearchCardContent>
</ContextMenu>
</WebSearchCard>
</WebSearchCard>
</ContextMenu>
)
}
@ -213,10 +227,19 @@ const PreviewIcon = styled.div`
`
const CitationIndex = styled.div`
font-size: 14px;
width: 14px;
height: 14px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background-color: var(--color-reference);
font-size: 10px;
line-height: 1.6;
color: var(--color-text-2);
margin-right: 8px;
color: var(--color-reference-text);
flex-shrink: 0;
opacity: 1;
transition: opacity 0.3s ease;
`
const CitationLink = styled.a`
@ -224,7 +247,7 @@ const CitationLink = styled.a`
line-height: 1.6;
color: var(--color-text-1);
text-decoration: none;
flex: 1;
.hostname {
color: var(--color-link);
}
@ -236,10 +259,14 @@ const CopyIconWrapper = styled.div`
align-items: center;
justify-content: center;
color: var(--color-text-2);
opacity: 0.6;
margin-left: auto;
opacity: 0;
padding: 4px;
border-radius: 4px;
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
transition: opacity 0.3s ease;
&:hover {
opacity: 1;
@ -251,11 +278,17 @@ const WebSearchCard = styled.div`
display: flex;
flex-direction: column;
width: 100%;
padding: 12px;
border-radius: var(--list-item-border-radius);
background-color: var(--color-background);
padding: 12px 0;
transition: all 0.3s ease;
position: relative;
&:hover {
${CopyIconWrapper} {
opacity: 1;
}
${CitationIndex} {
opacity: 0;
}
}
`
const WebSearchCardHeader = styled.div`
@ -265,6 +298,7 @@ const WebSearchCardHeader = styled.div`
gap: 8px;
margin-bottom: 6px;
width: 100%;
position: relative;
`
const WebSearchCardContent = styled.div`
@ -273,6 +307,7 @@ const WebSearchCardContent = styled.div`
color: var(--color-text-2);
user-select: text;
cursor: text;
word-break: break-all;
&.selectable-text {
-webkit-user-select: text;
@ -282,4 +317,16 @@ const WebSearchCardContent = styled.div`
}
`
const PopoverContent = styled.div`
max-width: min(340px, 60vw);
max-height: 60vh;
padding: 0 12px;
`
const PopoverContentItem = styled.div`
border-bottom: 0.5px solid var(--color-border);
&:last-child {
border-bottom: none;
}
`
export default CitationsList

View File

@ -1,9 +1,8 @@
import ContextMenu from '@renderer/components/ContextMenu'
import { useMessageEditing } from '@renderer/context/MessageEditingContext'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { useMessageOperations } from '@renderer/hooks/useMessageOperations'
import { useModel } from '@renderer/hooks/useModel'
import { useMessageStyle, useSettings } from '@renderer/hooks/useSettings'
import { useSettings } from '@renderer/hooks/useSettings'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { getMessageModelId } from '@renderer/services/MessagesService'
import { getModelUniqId } from '@renderer/services/ModelService'
@ -42,14 +41,12 @@ const MessageItem: FC<Props> = ({
index,
hideMenuBar = false,
isGrouped,
isStreaming = false,
style
isStreaming = false
}) => {
const { t } = useTranslation()
const { assistant, setModel } = useAssistant(message.assistantId)
const model = useModel(getMessageModelId(message), message.model?.provider) || message.model
const { isBubbleStyle } = useMessageStyle()
const { showMessageDivider, messageFont, fontSize, narrowMode, messageStyle } = useSettings()
const { messageFont, fontSize } = useSettings()
const { editMessageBlocks, resendUserMessageWithEdit, editMessage } = useMessageOperations(topic)
const messageContainerRef = useRef<HTMLDivElement>(null)
const { editingMessageId, stopEditing } = useMessageEditing()
@ -101,9 +98,6 @@ 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 messageBackground = getMessageBackground(isBubbleStyle, isAssistantMessage)
const messageHighlightHandler = useCallback((highlight: boolean = true) => {
if (messageContainerRef.current) {
messageContainerRef.current.scrollIntoView({ behavior: 'smooth' })
@ -140,101 +134,38 @@ const MessageItem: FC<Props> = ({
'message-assistant': isAssistantMessage,
'message-user': !isAssistantMessage
})}
ref={messageContainerRef}
style={{
...style,
justifyContent: isBubbleStyle ? (isAssistantMessage ? 'flex-start' : 'flex-end') : undefined,
flex: isBubbleStyle ? undefined : 1
}}>
ref={messageContainerRef}>
<MessageHeader
message={message}
assistant={assistant}
model={model}
key={getModelUniqId(model)}
index={index}
topic={topic}
/>
{isEditing && (
<ContextMenu
style={{
display: 'flex',
flexDirection: 'column',
alignSelf: isAssistantMessage ? 'flex-start' : 'flex-end',
width: isBubbleStyle ? '70%' : '100%'
}}>
<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>
<MessageEditor
message={message}
onSave={handleEditSave}
onResend={handleEditResend}
onCancel={handleEditCancel}
/>
)}
{!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'
}
className="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
overflowY: 'visible'
}}>
<MessageErrorBoundary>
<MessageContent message={message} />
</MessageErrorBoundary>
{showMenubar && !isBubbleStyle && (
<MessageFooter
className="MessageFooter"
style={{
borderTop: messageBorder,
flexDirection: !isLastMessage ? 'row-reverse' : undefined
}}>
<MessageMenubar
message={message}
assistant={assistant}
model={model}
index={index}
topic={topic}
isLastMessage={isLastMessage}
isAssistantMessage={isAssistantMessage}
isGrouped={isGrouped}
messageContainerRef={messageContainerRef as React.RefObject<HTMLDivElement>}
setModel={setModel}
/>
</MessageFooter>
)}
</MessageContentContainer>
{showMenubar && isBubbleStyle && (
<MessageFooter
className="MessageFooter"
style={{
borderTop: messageBorder,
flexDirection: !isAssistantMessage ? 'row-reverse' : undefined
}}>
{showMenubar && (
<MessageFooter className="MessageFooter">
<MessageMenubar
message={message}
assistant={assistant}
@ -249,28 +180,22 @@ const MessageItem: FC<Props> = ({
/>
</MessageFooter>
)}
</ContextMenu>
</>
)}
</MessageContainer>
)
}
const getMessageBackground = (isBubbleStyle: boolean, isAssistantMessage: boolean) => {
return isBubbleStyle
? isAssistantMessage
? 'var(--chat-background-assistant)'
: 'var(--chat-background-user)'
: undefined
}
const MessageContainer = styled.div`
display: flex;
flex-direction: column;
width: 100%;
position: relative;
transition: background-color 0.3s ease;
padding: 0 20px;
transform: translateZ(0);
will-change: transform;
padding: 10px 10px 0 10px;
border-radius: 10px;
&.message-highlight {
background-color: var(--color-primary-mute);
}
@ -292,11 +217,7 @@ const MessageContainer = styled.div`
const MessageContentContainer = styled.div`
max-width: 100%;
display: flex;
flex: 1;
flex-direction: column;
justify-content: space-between;
margin-left: 46px;
padding-left: 46px;
margin-top: 5px;
overflow-y: auto;
`
@ -306,9 +227,8 @@ const MessageFooter = styled.div`
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 2px 0;
margin-top: 2px;
gap: 20px;
margin-left: 46px;
`
const NewContextMessage = styled.div`

View File

@ -184,7 +184,7 @@ const MessageAnchorLine: FC<MessageLineProps> = ({ messages }) => {
else messageItemsRef.current.delete('bottom-anchor')
}}
style={{
opacity: mouseY ? 0.5 + calculateValueByDistance('bottom-anchor', 1) : 0.6
opacity: mouseY ? 0.5 : Math.max(0, 0.6 - (0.3 * Math.abs(0 - messages.length / 2)) / 5)
}}
onClick={scrollToBottom}>
<CircleChevronDown
@ -194,7 +194,7 @@ const MessageAnchorLine: FC<MessageLineProps> = ({ messages }) => {
</MessageItem>
{messages.map((message, index) => {
const opacity = 0.5 + calculateValueByDistance(message.id, 1)
const scale = 1 + calculateValueByDistance(message.id, 1)
const scale = 1 + calculateValueByDistance(message.id, 1.2)
const size = 10 + calculateValueByDistance(message.id, 20)
const avatarSource = getAvatarSource(isLocalAi, getMessageModelId(message))
const username = removeLeadingEmoji(getUserName(message))
@ -219,15 +219,14 @@ const MessageAnchorLine: FC<MessageLineProps> = ({ messages }) => {
</MessageItemContainer>
{message.role === 'assistant' ? (
<Avatar
<MessageItemAvatar
src={avatarSource}
size={size}
style={{
border: isLocalAi ? '1px solid var(--color-border-soft)' : 'none',
filter: theme === 'dark' ? 'invert(0.05)' : undefined
}}>
A
</Avatar>
}}
/>
) : (
<>
{isEmoji(avatar) ? (
@ -241,7 +240,7 @@ const MessageAnchorLine: FC<MessageLineProps> = ({ messages }) => {
{avatar}
</EmojiAvatar>
) : (
<Avatar src={avatar} size={size} />
<MessageItemAvatar src={avatar} size={size} />
)}
</>
)}
@ -260,17 +259,28 @@ const MessageItemContainer = styled.div`
align-items: flex-end;
justify-content: space-between;
text-align: right;
gap: 4px;
gap: 3px;
opacity: 0;
transform-origin: right center;
transition: transform cubic-bezier(0.25, 1, 0.5, 1) 150ms;
will-change: transform;
`
const MessageItemAvatar = styled(Avatar)`
transition:
width,
height,
cubic-bezier(0.25, 1, 0.5, 1) 150ms;
will-change: width, height;
`
const MessageLineContainer = styled.div<{ $height: number | null }>`
width: 14px;
position: fixed;
top: ${(props) => (props.$height ? `calc(${props.$height / 2}px + var(--status-bar-height))` : '50%')};
top: calc(50% - var(--status-bar-height) - 10px);
right: 13px;
max-height: ${(props) => (props.$height ? `${props.$height}px` : 'calc(100% - var(--status-bar-height) * 2)')};
max-height: ${(props) =>
props.$height ? `${props.$height - 20}px` : 'calc(100% - var(--status-bar-height) * 2 - 20px)'};
transform: translateY(-50%);
z-index: 0;
user-select: none;
@ -280,7 +290,7 @@ const MessageLineContainer = styled.div<{ $height: number | null }>`
font-size: 5px;
overflow: hidden;
&:hover {
width: 440px;
width: 500px;
overflow-x: visible;
overflow-y: hidden;
${MessageItemContainer} {

View File

@ -308,10 +308,11 @@ const MessageBlockEditor: FC<Props> = ({ message, onSave, onResend, onCancel })
const EditorContainer = styled.div`
padding: 8px 0;
border: 1px solid var(--color-border);
border: 0.5px solid var(--color-border);
transition: all 0.2s ease;
border-radius: 15px;
margin-top: 5px;
margin-bottom: 10px;
background-color: var(--color-background-opacity);
width: 100%;

View File

@ -1,4 +1,3 @@
import Scrollbar from '@renderer/components/Scrollbar'
import { MessageEditingProvider } from '@renderer/context/MessageEditingContext'
import { useChatContext } from '@renderer/hooks/useChatContext'
import { useMessageOperations } from '@renderer/hooks/useMessageOperations'
@ -10,11 +9,10 @@ import type { Message } from '@renderer/types/newMessage'
import { classNames } from '@renderer/utils'
import { Popover } from 'antd'
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import styled, { css } from 'styled-components'
import styled from 'styled-components'
import MessageItem from './Message'
import MessageGroupMenuBar from './MessageGroupMenuBar'
import SelectableMessage from './MessageSelect'
interface Props {
messages: (Message & { index: number })[]
@ -62,7 +60,6 @@ const MessageGroup = ({ messages, topic, registerMessageElement }: Props) => {
)
const isGrouped = isMultiSelectMode ? false : messageLength > 1 && messages.every((m) => m.role === 'assistant')
const isHorizontal = multiModelMessageStyle === 'horizontal'
const isGrid = multiModelMessageStyle === 'grid'
useEffect(() => {
@ -166,25 +163,19 @@ const MessageGroup = ({ messages, topic, registerMessageElement }: Props) => {
isGrouped,
message,
topic,
index: message.index,
style: {
paddingTop: isGrouped && ['horizontal', 'grid'].includes(multiModelMessageStyle) ? 0 : 15
}
index: message.index
}
const messageContent = (
<MessageWrapper
id={`message-${message.id}`}
$layout={multiModelMessageStyle}
// $selected={index === selectedIndex}
$isGrouped={isGrouped}
key={message.id}
className={classNames({
// 加个卡片布局
'group-message-wrapper': message.role === 'assistant' && (isHorizontal || isGrid) && isGrouped,
[multiModelMessageStyle]: isGrouped,
selected: message.id === selectedMessageId
})}>
className={classNames([
{
[multiModelMessageStyle]: message.role === 'assistant',
selected: message.id === selectedMessageId
}
])}>
<MessageItem {...messageProps} />
</MessageWrapper>
)
@ -193,47 +184,43 @@ const MessageGroup = ({ messages, topic, registerMessageElement }: Props) => {
return (
<Popover
key={message.id}
destroyTooltipOnHide
content={
<MessageWrapper
$layout={multiModelMessageStyle}
// $selected={index === selectedIndex}
$isGrouped={isGrouped}
$isInPopover={true}>
className={classNames([
'in-popover',
{
[multiModelMessageStyle]: message.role === 'assistant',
selected: message.id === selectedMessageId
}
])}>
<MessageItem {...messageProps} />
</MessageWrapper>
}
trigger={gridPopoverTrigger}
styles={{ root: { maxWidth: '60vw', minWidth: '550px', overflowY: 'auto', zIndex: 1000 } }}>
<div style={{ cursor: 'pointer' }}>{messageContent}</div>
styles={{
root: { maxWidth: '60vw', overflowY: 'auto', zIndex: 1000 },
body: { padding: 2 }
}}>
{messageContent}
</Popover>
)
}
return (
<SelectableMessage
key={`selectable-${message.id}`}
messageId={message.id}
topic={topic}
isClearMessage={message.type === 'clear'}>
{messageContent}
</SelectableMessage>
)
return messageContent
},
[isGrid, isGrouped, topic, multiModelMessageStyle, isHorizontal, selectedMessageId, gridPopoverTrigger]
[isGrid, isGrouped, topic, multiModelMessageStyle, selectedMessageId, gridPopoverTrigger]
)
return (
<MessageEditingProvider>
<GroupContainer
id={`message-group-${messages[0].askId}`}
$isGrouped={isGrouped}
$layout={multiModelMessageStyle}
className={classNames([isGrouped && 'group-container', isHorizontal && 'horizontal', isGrid && 'grid'])}>
id={messages[0].askId ? `message-group-${messages[0].askId}` : undefined}
className={classNames([multiModelMessageStyle, { 'multi-select-mode': isMultiSelectMode }])}>
<GridContainer
$count={messageLength}
$layout={multiModelMessageStyle}
$gridColumns={gridColumns}
className={classNames([isGrouped && 'group-grid-container', isHorizontal && 'horizontal', isGrid && 'grid'])}>
className={classNames([multiModelMessageStyle, { 'multi-select-mode': isMultiSelectMode }])}>
{messages.map(renderMessage)}
</GridContainer>
{isGrouped && (
@ -256,73 +243,103 @@ const MessageGroup = ({ messages, topic, registerMessageElement }: Props) => {
)
}
const GroupContainer = styled.div<{ $isGrouped: boolean; $layout: MultiModelMessageStyle }>`
padding-top: ${({ $isGrouped, $layout }) => ($isGrouped && 'horizontal' === $layout ? '15px' : '0')};
&.group-container.horizontal,
&.group-container.grid {
padding: 0 20px;
.message {
padding: 0;
}
const GroupContainer = styled.div`
&.horizontal,
&.grid {
padding: 4px 10px;
.group-menu-bar {
margin-left: 0;
margin-right: 0;
}
}
&.multi-select-mode {
padding: 5px 10px;
}
`
const GridContainer = styled.div<{ $count: number; $layout: MultiModelMessageStyle; $gridColumns: number }>`
const GridContainer = styled.div<{ $count: number; $gridColumns: number }>`
width: 100%;
display: grid;
gap: ${({ $layout }) => ($layout === 'horizontal' ? '16px' : '0')};
grid-template-columns: repeat(
${({ $layout, $count }) => (['fold', 'vertical'].includes($layout) ? 1 : $count)},
minmax(480px, 1fr)
);
@media (max-width: 800px) {
grid-template-columns: repeat(
${({ $layout, $count }) => (['fold', 'vertical'].includes($layout) ? 1 : $count)},
minmax(400px, 1fr)
);
overflow-y: visible;
gap: 16px;
&.horizontal {
padding-bottom: 4px;
grid-template-columns: repeat(${({ $count }) => $count}, minmax(480px, 1fr));
overflow-x: auto;
}
&.fold,
&.vertical {
grid-template-columns: repeat(1, minmax(0, 1fr));
gap: 8px;
}
&.grid {
grid-template-columns: repeat(
${({ $count, $gridColumns }) => ($count > 1 ? $gridColumns || 2 : 1)},
minmax(0, 1fr)
);
grid-template-rows: auto;
}
&.multi-select-mode {
grid-template-columns: repeat(1, minmax(0, 1fr));
gap: 10px;
.message {
border: 0.5px solid var(--color-border);
border-radius: 10px;
padding: 10px;
.message-content-container {
max-height: 200px;
overflow-y: hidden !important;
}
.MessageFooter {
display: none;
}
}
}
${({ $layout }) =>
$layout === 'horizontal' &&
css`
margin-top: 15px;
`}
${({ $gridColumns, $layout, $count }) =>
$layout === 'grid' &&
css`
margin-top: 15px;
grid-template-columns: repeat(${$count > 1 ? $gridColumns || 2 : 1}, minmax(0, 1fr));
grid-template-rows: auto;
gap: 16px;
`}
${({ $layout }) => {
return $layout === 'horizontal'
? css`
overflow-y: auto;
`
: 'overflow-y: visible;'
}}
`
interface MessageWrapperProps {
$layout: 'fold' | 'horizontal' | 'vertical' | 'grid'
// $selected: boolean
$isGrouped: boolean
$isInPopover?: boolean
}
const MessageWrapper = styled(Scrollbar)<MessageWrapperProps>`
width: 100%;
display: flex;
const MessageWrapper = styled.div<MessageWrapperProps>`
&.horizontal {
display: inline-block;
overflow-y: auto;
.message {
border: 0.5px solid var(--color-border);
border-radius: 10px;
}
.message-content-container {
padding-left: 0;
max-height: calc(100vh - 350px);
overflow-y: auto !important;
margin-right: -10px;
}
.MessageFooter {
margin-left: 0;
margin-top: 2px;
margin-bottom: 2px;
}
}
&.grid {
display: inline-block;
height: 300px;
overflow-y: hidden;
border: 0.5px solid var(--color-border);
border-radius: 10px;
cursor: pointer;
}
&.in-popover {
height: auto;
border: none;
max-height: 50vh;
overflow-y: auto;
cursor: default;
.message-content-container {
padding-left: 0;
}
.MessageFooter {
margin-left: 0;
}
}
&.fold {
display: none;
@ -330,38 +347,6 @@ const MessageWrapper = styled(Scrollbar)<MessageWrapperProps>`
display: inline-block;
}
}
${({ $layout, $isGrouped }) => {
if ($layout === 'horizontal' && $isGrouped) {
return css`
border: 0.5px solid var(--color-border);
padding: 10px;
border-radius: 6px;
max-height: 600px;
margin-bottom: 10px;
`
}
return ''
}}
${({ $layout, $isInPopover, $isGrouped }) => {
// 如果布局是grid并且是组消息则设置最大高度和溢出行为卡片不可滚动点击展开后可滚动
// 如果布局是horizontal则设置溢出行为卡片可滚动
// 如果布局是fold、vertical高度不限制与正常消息流布局一致则设置卡片不可滚动visible
return $layout === 'grid' && $isGrouped
? css`
max-height: ${$isInPopover ? '50vh' : '300px'};
overflow-y: ${$isInPopover ? 'auto' : 'hidden'};
border: 0.5px solid ${$isInPopover ? 'transparent' : 'var(--color-border)'};
padding: 10px;
border-radius: 6px;
background-color: var(--color-background);
`
: css`
overflow-y: ${$layout === 'horizontal' ? 'auto' : 'visible'};
border-radius: 6px;
`
}}
`
export default memo(MessageGroup)

View File

@ -59,6 +59,7 @@ const MessageGroupMenuBar: FC<Props> = ({
<LayoutContainer>
{['fold', 'vertical', 'horizontal', 'grid'].map((layout) => (
<Tooltip
mouseEnterDelay={0.5}
key={layout}
title={t(`message.message.multi_model_style`) + ': ' + t(`message.message.multi_model_style.${layout}`)}>
<LayoutOption
@ -101,15 +102,13 @@ const GroupMenuBar = styled.div<{ $layout: MultiModelMessageStyle }>`
flex-direction: row;
align-items: center;
gap: 10px;
margin: 0 20px;
padding: 6px 10px;
border-radius: 6px;
margin-top: 10px;
padding: 8px;
border-radius: 10px;
margin: 8px 10px 16px;
justify-content: space-between;
overflow: hidden;
border: 0.5px solid var(--color-border);
height: 40px;
background-color: var(--color-background);
`
const LayoutContainer = styled.div`

View File

@ -1,10 +1,11 @@
import { SettingOutlined } from '@ant-design/icons'
import Selector from '@renderer/components/Selector'
import { useSettings } from '@renderer/hooks/useSettings'
import { SettingDivider } from '@renderer/pages/settings'
import { SettingRow } from '@renderer/pages/settings'
import { useAppDispatch } from '@renderer/store'
import { setGridColumns, setGridPopoverTrigger } from '@renderer/store/settings'
import { Col, Row, Select, Slider } from 'antd'
import { Col, Row, Slider } from 'antd'
import { Popover } from 'antd'
import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -18,19 +19,21 @@ const MessageGroupSettings: FC = () => {
return (
<Popover
arrow={false}
trigger={undefined}
showArrow
content={
<div style={{ padding: 10 }}>
<div style={{ padding: 8 }}>
<SettingRow>
<div style={{ marginRight: 10 }}>{t('settings.messages.grid_popover_trigger')}</div>
<Select
<Selector
size={14}
value={gridPopoverTrigger || 'hover'}
onChange={(value) => dispatch(setGridPopoverTrigger(value as 'hover' | 'click'))}
size="small">
<Select.Option value="hover">{t('settings.messages.grid_popover_trigger.hover')}</Select.Option>
<Select.Option value="click">{t('settings.messages.grid_popover_trigger.click')}</Select.Option>
</Select>
options={[
{ label: t('settings.messages.grid_popover_trigger.hover'), value: 'hover' },
{ label: t('settings.messages.grid_popover_trigger.click'), value: 'click' }
]}
/>
</SettingRow>
<SettingDivider />
<SettingRow>

View File

@ -4,16 +4,17 @@ import { APP_NAME, AppLogo, isLocalAi } from '@renderer/config/env'
import { getModelLogo } from '@renderer/config/models'
import { useTheme } from '@renderer/context/ThemeProvider'
import useAvatar from '@renderer/hooks/useAvatar'
import { useChatContext } from '@renderer/hooks/useChatContext'
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
import { useMessageStyle, useSettings } from '@renderer/hooks/useSettings'
import { getMessageModelId } from '@renderer/services/MessagesService'
import { getModelName } from '@renderer/services/ModelService'
import type { Assistant, Model } from '@renderer/types'
import type { Assistant, Model, Topic } from '@renderer/types'
import type { Message } from '@renderer/types/newMessage'
import { firstLetter, isEmoji, removeLeadingEmoji } from '@renderer/utils'
import { Avatar } from 'antd'
import { Avatar, Checkbox } from 'antd'
import dayjs from 'dayjs'
import { CSSProperties, FC, memo, useCallback, useMemo } from 'react'
import { FC, memo, useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
@ -24,6 +25,7 @@ interface Props {
assistant: Assistant
model?: Model
index: number | undefined
topic: Topic
}
const getAvatarSource = (isLocalAi: boolean, modelId: string | undefined) => {
@ -31,7 +33,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, index, topic }) => {
const avatar = useAvatar()
const { theme } = useTheme()
const { userName, sidebarIcons } = useSettings()
@ -39,6 +41,10 @@ const MessageHeader: FC<Props> = memo(({ assistant, model, message, index }) =>
const { isBubbleStyle } = useMessageStyle()
const { openMinappById } = useMinappPopup()
const { isMultiSelectMode, selectedMessageIds, handleSelectMessage } = useChatContext(topic)
const isSelected = selectedMessageIds?.includes(message.id)
const avatarSource = useMemo(() => getAvatarSource(isLocalAi, getMessageModelId(message)), [message])
const getUserName = useCallback(() => {
@ -67,65 +73,54 @@ const MessageHeader: FC<Props> = memo(({ assistant, model, message, index }) =>
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [model?.provider, showMinappIcon])
const avatarStyle: CSSProperties | undefined = isBubbleStyle
? {
flexDirection: isAssistantMessage ? 'row' : 'row-reverse',
textAlign: isAssistantMessage ? 'left' : 'right'
}
: undefined
const containerStyle = isBubbleStyle
? {
justifyContent: isAssistantMessage ? 'flex-start' : 'flex-end'
}
: undefined
return (
<Container className="message-header" style={containerStyle}>
<AvatarWrapper style={avatarStyle}>
{isAssistantMessage ? (
<Avatar
src={avatarSource}
size={35}
style={{
borderRadius: '25%',
cursor: showMinappIcon ? 'pointer' : 'default',
border: isLocalAi ? '1px solid var(--color-border-soft)' : 'none',
filter: theme === 'dark' ? 'invert(0.05)' : undefined
}}
onClick={showMiniApp}>
{avatarName}
</Avatar>
) : (
<>
{isEmoji(avatar) ? (
<EmojiAvatar onClick={() => UserPopup.show()} size={35} fontSize={20}>
{avatar}
</EmojiAvatar>
) : (
<Avatar
src={avatar}
size={35}
style={{ borderRadius: '25%', cursor: 'pointer' }}
onClick={() => UserPopup.show()}
/>
)}
</>
)}
<UserWrap>
<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>
</UserWrap>
</AvatarWrapper>
<Container className="message-header">
{isAssistantMessage ? (
<Avatar
src={avatarSource}
size={35}
style={{
borderRadius: '25%',
cursor: showMinappIcon ? 'pointer' : 'default',
border: isLocalAi ? '1px solid var(--color-border-soft)' : 'none',
filter: theme === 'dark' ? 'invert(0.05)' : undefined
}}
onClick={showMiniApp}>
{avatarName}
</Avatar>
) : (
<>
{isEmoji(avatar) ? (
<EmojiAvatar onClick={() => UserPopup.show()} size={35} fontSize={20}>
{avatar}
</EmojiAvatar>
) : (
<Avatar
src={avatar}
size={35}
style={{ borderRadius: '25%', cursor: 'pointer' }}
onClick={() => UserPopup.show()}
/>
)}
</>
)}
<UserWrap>
<UserName isBubbleStyle={isBubbleStyle} theme={theme}>
{username}
</UserName>
<InfoWrap className="message-header-info-wrap">
<MessageTime>{dayjs(message?.updatedAt ?? message.createdAt).format('MM/DD HH:mm')}</MessageTime>
{showTokens && <DividerContainer style={{ color: 'var(--color-text-3)' }}> | </DividerContainer>}
<MessageTokens message={message} isLastMessage={isLastMessage} />
</InfoWrap>
</UserWrap>
{isMultiSelectMode && (
<Checkbox
checked={isSelected}
onChange={(e) => handleSelectMessage(message.id, e.target.checked)}
style={{ position: 'absolute', right: 0, top: 0 }}
/>
)}
</Container>
)
})
@ -133,23 +128,18 @@ const MessageHeader: FC<Props> = memo(({ assistant, model, message, index }) =>
MessageHeader.displayName = 'MessageHeader'
const Container = styled.div`
display: flex;
flex-direction: row;
align-items: center;
padding-bottom: 4px;
`
const AvatarWrapper = styled.div`
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
position: relative;
`
const UserWrap = styled.div`
display: flex;
flex-direction: column;
justify-content: space-between;
flex: 1;
`
const InfoWrap = styled.div`

View File

@ -507,8 +507,7 @@ const MessageMenubar: FC<Props> = (props) => {
<Dropdown
menu={{ items: dropdownItems, onClick: (e) => e.domEvent.stopPropagation() }}
trigger={['click']}
placement="topRight"
arrow>
placement="topRight">
<ActionButton
className="message-action-button"
onClick={(e) => e.stopPropagation()}

View File

@ -1,3 +1,4 @@
import ContextMenu from '@renderer/components/ContextMenu'
import SvgSpinners180Ring from '@renderer/components/Icons/SvgSpinners180Ring'
import Scrollbar from '@renderer/components/Scrollbar'
import { LOAD_MORE_COUNT } from '@renderer/config/constant'
@ -271,7 +272,6 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic, o
id="messages"
className="messages-container"
ref={scrollContainerRef}
style={{ position: 'relative', paddingTop: showPrompt ? 10 : 0 }}
key={assistant.id}
onScroll={handleScrollPosition}>
<NarrowLayout style={{ display: 'flex', flexDirection: 'column-reverse' }}>
@ -283,22 +283,25 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic, o
scrollableTarget="messages"
inverse
style={{ overflow: 'visible' }}>
<ScrollContainer>
{groupedMessages.map(([key, groupMessages]) => (
<MessageGroup
key={key}
messages={groupMessages}
topic={topic}
registerMessageElement={registerMessageElement}
/>
))}
{isLoadingMore && (
<LoaderContainer>
<SvgSpinners180Ring color="var(--color-text-2)" />
</LoaderContainer>
)}
</ScrollContainer>
<ContextMenu>
<ScrollContainer>
{groupedMessages.map(([key, groupMessages]) => (
<MessageGroup
key={key}
messages={groupMessages}
topic={topic}
registerMessageElement={registerMessageElement}
/>
))}
{isLoadingMore && (
<LoaderContainer>
<SvgSpinners180Ring color="var(--color-text-2)" />
</LoaderContainer>
)}
</ScrollContainer>
</ContextMenu>
</InfiniteScroll>
{showPrompt && <Prompt assistant={assistant} key={assistant.prompt} topic={topic} />}
</NarrowLayout>
{messageNavigation === 'anchor' && <MessageAnchorLine messages={displayMessages} />}
@ -361,6 +364,10 @@ const LoaderContainer = styled.div`
const ScrollContainer = styled.div`
display: flex;
flex-direction: column-reverse;
padding: 20px 10px 20px 16px;
.multi-select-mode & {
padding-bottom: 60px;
}
`
interface ContainerProps {
@ -370,11 +377,9 @@ interface ContainerProps {
const MessagesContainer = styled(Scrollbar)<ContainerProps>`
display: flex;
flex-direction: column-reverse;
padding: 10px 0 20px;
overflow-x: hidden;
background-color: var(--color-background);
z-index: 1;
margin-right: 2px;
position: relative;
`
export default Messages

View File

@ -10,7 +10,11 @@ const NarrowLayout: FC<Props> = ({ children, ...props }) => {
const { narrowMode } = useSettings()
if (narrowMode) {
return <Container {...props}>{children}</Container>
return (
<Container className="narrow-mode" {...props}>
{children}
</Container>
)
}
return children

View File

@ -30,11 +30,11 @@ const Prompt: FC<Props> = ({ assistant, topic }) => {
}
const Container = styled.div<{ $isDark: boolean }>`
padding: 10px 20px;
margin: 5px 20px 0 20px;
padding: 10px 16px;
border-radius: 10px;
cursor: pointer;
border: 0.5px solid var(--color-border);
margin: 10px 10px 0 10px;
`
const Text = styled.div`

View File

@ -1,6 +1,7 @@
import { CheckOutlined } from '@ant-design/icons'
import EditableNumber from '@renderer/components/EditableNumber'
import { HStack } from '@renderer/components/Layout'
import Scrollbar from '@renderer/components/Scrollbar'
import Selector from '@renderer/components/Selector'
import { DEFAULT_CONTEXTCOUNT, DEFAULT_MAX_TOKENS, DEFAULT_TEMPERATURE } from '@renderer/config/constant'
import {
isOpenAIModel,
@ -38,7 +39,6 @@ import {
setPasteLongTextThreshold,
setRenderInputMessageAsMarkdown,
setShowInputEstimatedTokens,
setShowMessageDivider,
setShowPrompt,
setShowTokens,
setShowTranslateConfirm,
@ -54,7 +54,7 @@ import {
} from '@renderer/types'
import { modalConfirm } from '@renderer/utils'
import { getSendMessageShortcutLabel } from '@renderer/utils/input'
import { Button, Col, InputNumber, Row, Select, Slider, Switch, Tooltip } from 'antd'
import { Button, Col, InputNumber, Row, Slider, Switch, Tooltip } from 'antd'
import { CircleHelp, Settings2 } from 'lucide-react'
import { FC, useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -86,7 +86,6 @@ const SettingsTab: FC<Props> = (props) => {
const {
showPrompt,
showMessageDivider,
messageFont,
showInputEstimatedTokens,
sendMessageShortcut,
@ -312,20 +311,6 @@ const SettingsTab: FC<Props> = (props) => {
<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
@ -351,56 +336,56 @@ const SettingsTab: FC<Props> = (props) => {
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('message.message.style')}</SettingRowTitleSmall>
<StyledSelect
<Selector
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>
options={[
{ value: 'plain', label: t('message.message.style.plain') },
{ value: 'bubble', label: t('message.message.style.bubble') }
]}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('message.message.multi_model_style')}</SettingRowTitleSmall>
<StyledSelect
size="small"
<Selector
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>
options={[
{ value: 'fold', label: t('message.message.multi_model_style.fold') },
{ value: 'vertical', label: t('message.message.multi_model_style.vertical') },
{ value: 'horizontal', label: t('message.message.multi_model_style.horizontal') },
{ value: 'grid', label: t('message.message.multi_model_style.grid') }
]}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.navigation')}</SettingRowTitleSmall>
<StyledSelect
size="small"
<Selector
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>
options={[
{ value: 'none', label: t('settings.messages.navigation.none') },
{ value: 'buttons', label: t('settings.messages.navigation.buttons') },
{ value: 'anchor', label: t('settings.messages.navigation.anchor') }
]}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.math_engine')}</SettingRowTitleSmall>
<StyledSelect
<Selector
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>
options={[
{ value: 'KaTeX', label: 'KaTeX' },
{ value: 'MathJax', label: 'MathJax' },
{ value: 'none', label: t('settings.messages.math_engine.none') }
]}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
@ -430,17 +415,14 @@ const SettingsTab: FC<Props> = (props) => {
<SettingGroup>
<SettingRow>
<SettingRowTitleSmall>{t('message.message.code_style')}</SettingRowTitleSmall>
<StyledSelect
<Selector
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>
options={themeNames.map((theme) => ({
value: theme,
label: theme
}))}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
@ -466,7 +448,7 @@ const SettingsTab: FC<Props> = (props) => {
<CircleHelp size={14} style={{ marginLeft: 4 }} color="var(--color-text-2)" />
</Tooltip>
</SettingRowTitleSmall>
<InputNumber
<EditableNumber
size="small"
min={1}
max={60}
@ -577,7 +559,7 @@ const SettingsTab: FC<Props> = (props) => {
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.input.paste_long_text_threshold')}</SettingRowTitleSmall>
<InputNumber
<EditableNumber
size="small"
min={500}
max={10000}
@ -641,11 +623,9 @@ const SettingsTab: FC<Props> = (props) => {
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.input.target_language')}</SettingRowTitleSmall>
<StyledSelect
defaultValue={'english' as TranslateLanguageVarious}
size="small"
<Selector
value={targetLanguage}
menuItemSelectedIcon={<CheckOutlined />}
onChange={(value) => setTargetLanguage(value as TranslateLanguageVarious)}
options={[
{ value: 'chinese', label: t('settings.input.target_language.chinese') },
{ value: 'chinese-traditional', label: t('settings.input.target_language.chinese-traditional') },
@ -653,17 +633,14 @@ const SettingsTab: FC<Props> = (props) => {
{ 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"
<Selector
value={sendMessageShortcut}
menuItemSelectedIcon={<CheckOutlined />}
onChange={(value) => setSendMessageShortcut(value as SendMessageShortcut)}
options={[
{ value: 'Enter', label: getSendMessageShortcutLabel('Enter') },
{ value: 'Ctrl+Enter', label: getSendMessageShortcutLabel('Ctrl+Enter') },
@ -671,8 +648,6 @@ const SettingsTab: FC<Props> = (props) => {
{ value: 'Command+Enter', label: getSendMessageShortcutLabel('Command+Enter') },
{ value: 'Shift+Enter', label: getSendMessageShortcutLabel('Shift+Enter') }
]}
onChange={(value) => setSendMessageShortcut(value as SendMessageShortcut)}
style={{ width: 135 }}
/>
</SettingRow>
</SettingGroup>
@ -704,12 +679,4 @@ const SettingGroup = styled.div<{ theme?: ThemeMode }>`
margin-bottom: 10px;
`
const StyledSelect = styled(Select)`
.ant-select-selector {
border-radius: 15px !important;
padding: 4px 10px !important;
height: 26px !important;
}
`
export default SettingsTab

View File

@ -1,3 +1,4 @@
import Selector from '@renderer/components/Selector'
import { SettingDivider, SettingRow } from '@renderer/pages/settings'
import { CollapsibleSettingGroup } from '@renderer/pages/settings/SettingGroup'
import { RootState, useAppDispatch } from '@renderer/store'
@ -102,13 +103,11 @@ const OpenAISettingsGroup: FC<Props> = ({
<CircleHelp size={14} style={{ marginLeft: 4 }} color="var(--color-text-2)" />
</Tooltip>
</SettingRowTitleSmall>
<StyledSelect
<Selector
value={serviceTierMode}
style={{ width: 135 }}
onChange={(value) => {
setServiceTierMode(value as OpenAIServiceTier)
}}
size="small"
options={serviceTierOptions}
/>
</SettingRow>
@ -135,6 +134,7 @@ const OpenAISettingsGroup: FC<Props> = ({
</>
)}
</SettingGroup>
<SettingDivider />
</CollapsibleSettingGroup>
)
}

View File

@ -13,6 +13,7 @@ import { KnowledgeBase, Model } from '@renderer/types'
import { getErrorMessage } from '@renderer/utils/error'
import { Flex, Form, Input, InputNumber, Modal, Select, Slider, Switch } from 'antd'
import { find, sortBy } from 'lodash'
import { ChevronDown } from 'lucide-react'
import { nanoid } from 'nanoid'
import { useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -116,6 +117,7 @@ const PopupContainer: React.FC<Props> = ({ title, resolve }) => {
const aiProvider = new AiProvider(provider)
values.dimensions = await aiProvider.getEmbeddingDimensions(selectedEmbeddingModel)
} catch (error) {
console.error('Error getting embedding dimensions:', error)
window.message.error(t('message.error.get_embedding_dimensions') + '\n' + getErrorMessage(error))
setLoading(false)
return
@ -181,7 +183,12 @@ const PopupContainer: React.FC<Props> = ({ title, resolve }) => {
label={t('models.embedding_model')}
tooltip={{ title: t('models.embedding_model_tooltip'), placement: 'right' }}
rules={[{ required: true, message: t('message.error.enter.model') }]}>
<Select style={{ width: '100%' }} options={embeddingSelectOptions} placeholder={t('settings.models.empty')} />
<Select
style={{ width: '100%' }}
options={embeddingSelectOptions}
placeholder={t('settings.models.empty')}
suffixIcon={<ChevronDown size={16} color="var(--color-border)" />}
/>
</Form.Item>
<Form.Item
@ -189,7 +196,12 @@ const PopupContainer: React.FC<Props> = ({ title, resolve }) => {
label={t('models.rerank_model')}
tooltip={{ title: t('models.rerank_model_tooltip'), placement: 'right' }}
rules={[{ required: false, message: t('message.error.enter.model') }]}>
<Select style={{ width: '100%' }} options={rerankSelectOptions} placeholder={t('settings.models.empty')} />
<Select
style={{ width: '100%' }}
options={rerankSelectOptions}
placeholder={t('settings.models.empty')}
suffixIcon={<ChevronDown size={16} color="var(--color-border)" />}
/>
</Form.Item>
<SettingHelpText style={{ marginTop: -15, marginBottom: 20 }}>
{t('models.rerank_model_not_support_provider', {
@ -201,13 +213,7 @@ const PopupContainer: React.FC<Props> = ({ title, resolve }) => {
label={t('knowledge.document_count')}
initialValue={DEFAULT_KNOWLEDGE_DOCUMENT_COUNT} // 设置初始值
tooltip={{ title: t('knowledge.document_count_help') }}>
<Slider
style={{ width: '100%' }}
min={1}
max={30}
step={1}
marks={{ 1: '1', 6: t('knowledge.document_count_default'), 30: '30' }}
/>
<Slider min={1} max={30} step={1} marks={{ 1: '1', 6: t('knowledge.document_count_default'), 30: '30' }} />
</Form.Item>
<Form.Item
name="autoDims"

View File

@ -1,14 +1,15 @@
import { CopyOutlined } from '@ant-design/icons'
import type { ExtractChunkData } from '@cherrystudio/embedjs-interfaces'
import { HStack } from '@renderer/components/Layout'
import { TopView } from '@renderer/components/TopView'
import { searchKnowledgeBase } from '@renderer/services/KnowledgeService'
import { FileType, KnowledgeBase } from '@renderer/types'
import { Input, List, message, Modal, Spin, Tooltip, Typography } from 'antd'
import { useRef, useState } from 'react'
import { Divider, Input, List, message, Modal, Spin, Tooltip, Typography } from 'antd'
import { Search } from 'lucide-react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
const { Search } = Input
const { Text, Paragraph } = Typography
interface ShowParams {
@ -25,7 +26,6 @@ const PopupContainer: React.FC<Props> = ({ base, resolve }) => {
const [results, setResults] = useState<Array<ExtractChunkData & { file: FileType | null }>>([])
const [searchKeyword, setSearchKeyword] = useState('')
const { t } = useTranslation()
const searchInputRef = useRef<any>(null)
const handleSearch = async (value: string) => {
if (!value.trim()) {
@ -84,77 +84,98 @@ const PopupContainer: React.FC<Props> = ({ base, resolve }) => {
return (
<Modal
title={t('knowledge.search')}
title={null}
open={open}
onOk={onOk}
onCancel={onCancel}
afterClose={onClose}
afterOpenChange={(visible) => visible && searchInputRef.current?.focus()}
width={800}
width={700}
footer={null}
centered
transitionName="animation-move-down">
<SearchContainer>
<Search
placeholder={t('knowledge.search_placeholder')}
closable={false}
transitionName="animation-move-down"
styles={{
content: {
borderRadius: 20,
padding: 0,
overflow: 'hidden',
paddingBottom: 12
},
body: {
maxHeight: '80vh',
overflow: 'hidden',
padding: 0
}
}}>
<HStack style={{ padding: '0 12px', marginTop: 8 }}>
<Input
prefix={
<SearchIcon>
<Search size={15} />
</SearchIcon>
}
value={searchKeyword}
placeholder={t('knowledge.search')}
allowClear
enterButton
size="large"
onSearch={handleSearch}
ref={searchInputRef}
autoFocus
spellCheck={false}
style={{ paddingLeft: 0 }}
variant="borderless"
size="middle"
onChange={(e) => setSearchKeyword(e.target.value)}
onPressEnter={() => handleSearch(searchKeyword)}
/>
<ResultsContainer>
{loading ? (
<LoadingContainer>
<Spin size="large" />
</LoadingContainer>
) : (
<List
dataSource={results}
renderItem={(item) => (
<List.Item>
<ResultItem>
<TagContainer>
<ScoreTag>Score: {(item.score * 100).toFixed(1)}%</ScoreTag>
<Tooltip title={t('common.copy')}>
<CopyButton onClick={() => handleCopy(item.pageContent)}>
<CopyOutlined />
</CopyButton>
</Tooltip>
</TagContainer>
<Paragraph style={{ userSelect: 'text' }}>{highlightText(item.pageContent)}</Paragraph>
<MetadataContainer>
<Text type="secondary">
{t('knowledge.source')}:{' '}
{item.file ? (
<a href={`http://file/${item.file.name}`} target="_blank" rel="noreferrer">
{item.file.origin_name}
</a>
) : (
item.metadata.source
)}
</Text>
</MetadataContainer>
</ResultItem>
</List.Item>
)}
/>
)}
</ResultsContainer>
</SearchContainer>
</HStack>
<Divider style={{ margin: 0, marginTop: 4, borderBlockStartWidth: 0.5 }} />
<ResultsContainer>
{loading ? (
<LoadingContainer>
<Spin size="large" />
</LoadingContainer>
) : (
<List
dataSource={results}
renderItem={(item) => (
<List.Item>
<ResultItem>
<MetadataContainer>
<Text type="secondary" ellipsis>
{t('knowledge.source')}:{' '}
{item.file ? (
<a href={`http://file/${item.file.name}`} target="_blank" rel="noreferrer">
{item.file.origin_name}
</a>
) : (
item.metadata.source
)}
</Text>
<ScoreTag>Score: {(item.score * 100).toFixed(1)}%</ScoreTag>
</MetadataContainer>
<TagContainer>
<Tooltip title={t('common.copy')}>
<CopyButton onClick={() => handleCopy(item.pageContent)}>
<CopyOutlined />
</CopyButton>
</Tooltip>
</TagContainer>
<Paragraph style={{ userSelect: 'text', marginBottom: 0 }}>
{highlightText(item.pageContent)}
</Paragraph>
</ResultItem>
</List.Item>
)}
/>
)}
</ResultsContainer>
</Modal>
)
}
const SearchContainer = styled.div`
display: flex;
flex-direction: column;
gap: 20px;
`
const ResultsContainer = styled.div`
max-height: 60vh;
padding: 0 16px;
overflow-y: auto;
max-height: 70vh;
`
const LoadingContainer = styled.div`
@ -164,21 +185,29 @@ const LoadingContainer = styled.div`
height: 200px;
`
const TagContainer = styled.div`
position: absolute;
top: 58px;
right: 16px;
display: flex;
align-items: center;
gap: 8px;
opacity: 0;
transition: opacity 0.2s;
`
const ResultItem = styled.div`
width: 100%;
position: relative;
padding: 16px;
background: var(--color-background-soft);
border-radius: 8px;
`
const TagContainer = styled.div`
position: absolute;
top: 8px;
right: 8px;
display: flex;
align-items: center;
gap: 8px;
&:hover {
${TagContainer} {
opacity: 1 !important;
}
}
`
const ScoreTag = styled.div`
@ -187,6 +216,7 @@ const ScoreTag = styled.div`
color: white;
border-radius: 4px;
font-size: 12px;
flex-shrink: 0;
`
const CopyButton = styled.div`
@ -195,7 +225,7 @@ const CopyButton = styled.div`
justify-content: center;
width: 24px;
height: 24px;
background: var(--color-background);
background: var(--color-background-mute);
color: var(--color-text);
border-radius: 4px;
cursor: pointer;
@ -208,12 +238,35 @@ const CopyButton = styled.div`
`
const MetadataContainer = styled.div`
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid var(--color-border);
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
margin-bottom: 8px;
padding-bottom: 8px;
border-bottom: 1px solid var(--color-border);
user-select: text;
`
const SearchIcon = styled.div`
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
background-color: var(--color-background-soft);
margin-right: 2px;
&.back-icon {
cursor: pointer;
transition: background-color 0.2s;
&:hover {
background-color: var(--color-background-mute);
}
}
`
const TopViewKey = 'KnowledgeSearchPopup'
export default class KnowledgeSearchPopup {

View File

@ -1,4 +1,4 @@
import { DownOutlined, WarningOutlined } from '@ant-design/icons'
import { WarningOutlined } from '@ant-design/icons'
import { TopView } from '@renderer/components/TopView'
import { DEFAULT_KNOWLEDGE_DOCUMENT_COUNT } from '@renderer/config/constant'
import { getEmbeddingMaxContext } from '@renderer/config/embedings'
@ -10,11 +10,11 @@ import { useProviders } from '@renderer/hooks/useProvider'
import { SettingHelpText } from '@renderer/pages/settings'
import { getModelUniqId } from '@renderer/services/ModelService'
import { KnowledgeBase } from '@renderer/types'
import { Alert, Form, Input, InputNumber, Modal, Select, Slider } from 'antd'
import { Alert, Button, Form, Input, InputNumber, Modal, Select, Slider } from 'antd'
import { sortBy } from 'lodash'
import { ChevronDown } from 'lucide-react'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
interface ShowParams {
base: KnowledgeBase
@ -140,7 +140,13 @@ const PopupContainer: React.FC<Props> = ({ base: _base, resolve }) => {
initialValue={getModelUniqId(base.model)}
tooltip={{ title: t('models.embedding_model_tooltip'), placement: 'right' }}
rules={[{ required: true, message: t('message.error.enter.model') }]}>
<Select style={{ width: '100%' }} options={selectOptions} placeholder={t('settings.models.empty')} disabled />
<Select
style={{ width: '100%' }}
options={selectOptions}
placeholder={t('settings.models.empty')}
disabled
suffixIcon={<ChevronDown size={16} color="var(--color-border)" />}
/>
</Form.Item>
<Form.Item
@ -154,6 +160,7 @@ const PopupContainer: React.FC<Props> = ({ base: _base, resolve }) => {
options={rerankSelectOptions}
placeholder={t('settings.models.empty')}
allowClear
suffixIcon={<ChevronDown size={16} color="var(--color-border)" />}
/>
</Form.Item>
<SettingHelpText style={{ marginTop: -15, marginBottom: 20 }}>
@ -166,27 +173,21 @@ const PopupContainer: React.FC<Props> = ({ base: _base, resolve }) => {
name="documentCount"
label={t('knowledge.document_count')}
tooltip={{ title: t('knowledge.document_count_help') }}>
<Slider
style={{ width: '100%' }}
min={1}
max={30}
step={1}
marks={{ 1: '1', 6: t('knowledge.document_count_default'), 30: '30' }}
/>
<Slider min={1} max={30} step={1} marks={{ 1: '1', 6: t('knowledge.document_count_default'), 30: '30' }} />
</Form.Item>
<AdvancedSettingsButton onClick={() => setShowAdvanced(!showAdvanced)}>
<DownOutlined
<Button color="default" variant="filled" onClick={() => setShowAdvanced(!showAdvanced)}>
{t('common.advanced_settings')}
<ChevronDown
size={16}
style={{
transform: showAdvanced ? 'rotate(180deg)' : 'rotate(0deg)',
transition: 'transform 0.3s',
marginRight: 8
transition: 'transform 0.3s'
}}
/>
{t('common.advanced_settings')}
</AdvancedSettingsButton>
</Button>
<div style={{ display: showAdvanced ? 'block' : 'none' }}>
<div style={{ display: showAdvanced ? 'block' : 'none', marginTop: 16 }}>
<Form.Item
name="chunkSize"
label={t('knowledge.chunk_size')}
@ -269,15 +270,6 @@ const PopupContainer: React.FC<Props> = ({ base: _base, resolve }) => {
const TopViewKey = 'KnowledgeSettingsPopup'
const AdvancedSettingsButton = styled.div`
cursor: pointer;
margin-bottom: 16px;
margin-top: -10px;
color: var(--color-primary);
display: flex;
align-items: center;
`
export default class KnowledgeSettingsPopup {
static hide() {
TopView.hide(TopViewKey)

View File

@ -3,7 +3,7 @@ import { Box } from '@renderer/components/Layout'
import { useAppSelector } from '@renderer/store'
import { Assistant, AssistantSettings } from '@renderer/types'
import { Row, Segmented, Select, SelectProps, Tooltip } from 'antd'
import { CircleHelp } from 'lucide-react'
import { ChevronDown, CircleHelp } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
@ -46,6 +46,7 @@ const AssistantKnowledgeBaseSettings: React.FC<Props> = ({ assistant, updateAssi
.toLowerCase()
.includes(input.toLowerCase())
}
suffixIcon={<ChevronDown size={16} color="var(--color-border)" />}
/>
<Row align="middle" style={{ marginTop: 10 }}>
<Label>{t('assistants.settings.knowledge_base.recognition')}</Label>

View File

@ -1,13 +1,16 @@
import { DeleteOutlined, PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons'
import ModelAvatar from '@renderer/components/Avatar/ModelAvatar'
import EditableNumber from '@renderer/components/EditableNumber'
import { HStack } from '@renderer/components/Layout'
import SelectModelPopup from '@renderer/components/Popups/SelectModelPopup'
import Selector from '@renderer/components/Selector'
import { DEFAULT_CONTEXTCOUNT, DEFAULT_TEMPERATURE } from '@renderer/config/constant'
import { SettingRow } from '@renderer/pages/settings'
import { Assistant, AssistantSettingCustomParameters, AssistantSettings } from '@renderer/types'
import { modalConfirm } from '@renderer/utils'
import { Button, Col, Divider, Input, InputNumber, Row, Select, Slider, Switch, Tooltip } from 'antd'
import { isNull } from 'lodash'
import { ChevronDown } from 'lucide-react'
import { FC, useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
@ -107,9 +110,15 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
)
case 'boolean':
return (
<Switch
checked={param.value as boolean}
onChange={(checked) => onUpdateCustomParameter(index, 'value', checked)}
<Select
value={param.value as boolean}
onChange={(value) => onUpdateCustomParameter(index, 'value', value)}
style={{ width: '100%' }}
options={[
{ label: 'true', value: true },
{ label: 'false', value: false }
]}
suffixIcon={<ChevronDown size={16} color="var(--color-border)" />}
/>
)
case 'json':
@ -188,38 +197,57 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
return (
<Container>
<Row align="middle" style={{ marginBottom: 10 }}>
<Label style={{ marginBottom: 10 }}>{t('assistants.settings.default_model')}</Label>
<Col span={24}>
<HStack alignItems="center" gap={5}>
<HStack alignItems="center" justifyContent="space-between" style={{ marginBottom: 10 }}>
<Label>{t('assistants.settings.default_model')}</Label>
<HStack alignItems="center" gap={5}>
<ModelSelectButton
icon={defaultModel ? <ModelAvatar model={defaultModel} size={20} /> : <PlusOutlined />}
onClick={onSelectModel}>
<ModelName>{defaultModel ? defaultModel.name : t('agents.edit.model.select.title')}</ModelName>
</ModelSelectButton>
{defaultModel && (
<Button
icon={defaultModel ? <ModelAvatar model={defaultModel} size={20} /> : <PlusOutlined />}
onClick={onSelectModel}>
{defaultModel ? defaultModel.name : t('agents.edit.model.select.title')}
</Button>
{defaultModel && (
<Button
icon={<DeleteOutlined />}
type="text"
onClick={() => {
setDefaultModel(undefined)
updateAssistant({ ...assistant, defaultModel: undefined })
}}
danger
/>
)}
</HStack>
</Col>
</Row>
color="danger"
variant="filled"
icon={<DeleteOutlined />}
onClick={() => {
setDefaultModel(undefined)
updateAssistant({ ...assistant, defaultModel: undefined })
}}
danger
/>
)}
</HStack>
</HStack>
<Divider style={{ margin: '10px 0' }} />
<Row align="middle">
<Label>{t('chat.settings.temperature')}</Label>
<Tooltip title={t('chat.settings.temperature.tip')}>
<QuestionIcon />
</Tooltip>
</Row>
<Row align="middle" gutter={20}>
<Col span={20}>
<Label>
{t('chat.settings.temperature')}
<Tooltip title={t('chat.settings.temperature.tip')}>
<QuestionIcon />
</Tooltip>
</Label>
</Col>
<Col span={4}>
<EditableNumber
min={0}
max={2}
step={0.01}
precision={2}
value={temperature}
onChange={(value) => {
if (value !== null) {
setTemperature(value)
setTimeout(() => updateAssistantSettings({ temperature: value }), 500)
}
}}
style={{ width: '100%' }}
/>
</Col>
</Row>
<Row align="middle" gutter={24}>
<Col span={24}>
<Slider
min={0}
max={2}
@ -230,43 +258,20 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
step={0.01}
/>
</Col>
<Col span={4}>
<InputNumber
min={0}
max={2}
step={0.01}
value={temperature}
changeOnBlur
onChange={(value) => {
if (!isNull(value)) {
setTemperature(value)
setTimeout(() => updateAssistantSettings({ temperature: value }), 500)
}
}}
style={{ width: '100%' }}
/>
</Col>
</Row>
<Divider style={{ margin: '10px 0' }} />
<Row align="middle">
<Label>{t('chat.settings.top_p')}</Label>
<Tooltip title={t('chat.settings.top_p.tip')}>
<QuestionIcon />
</Tooltip>
</Row>
<Row align="middle" gutter={20}>
<Col span={20}>
<Slider
min={0}
max={1}
onChange={setTopP}
onChangeComplete={onTopPChange}
value={typeof topP === 'number' ? topP : 1}
marks={{ 0: '0', 1: '1' }}
step={0.01}
/>
<Label>
{t('chat.settings.top_p')}
<Tooltip title={t('chat.settings.top_p.tip')}>
<QuestionIcon />
</Tooltip>
</Label>
</Col>
<Col span={4}>
<InputNumber
<EditableNumber
min={0}
max={1}
step={0.01}
@ -282,29 +287,32 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
/>
</Col>
</Row>
<Row align="middle">
<Label>
{t('chat.settings.context_count')}{' '}
<Tooltip title={t('chat.settings.context_count.tip')}>
<QuestionIcon />
</Tooltip>
</Label>
</Row>
<Row align="middle" gutter={20}>
<Col span={20}>
<Row align="middle" gutter={24}>
<Col span={24}>
<Slider
min={0}
max={100}
onChange={setContextCount}
onChangeComplete={onContextCountChange}
value={typeof contextCount === 'number' ? contextCount : 0}
marks={{ 0: '0', 25: '25', 50: '50', 75: '75', 100: t('chat.settings.max') }}
step={1}
tooltip={{ formatter: formatSliderTooltip }}
max={1}
onChange={setTopP}
onChangeComplete={onTopPChange}
value={typeof topP === 'number' ? topP : 1}
marks={{ 0: '0', 1: '1' }}
step={0.01}
/>
</Col>
</Row>
<Divider style={{ margin: '10px 0' }} />
<Row align="middle">
<Col span={20}>
<Label>
{t('chat.settings.context_count')}{' '}
<Tooltip title={t('chat.settings.context_count.tip')}>
<QuestionIcon />
</Tooltip>
</Label>
</Col>
<Col span={4}>
<InputNumber
<EditableNumber
min={0}
max={20}
step={1}
@ -320,6 +328,20 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
/>
</Col>
</Row>
<Row align="middle" gutter={24}>
<Col span={24}>
<Slider
min={0}
max={100}
onChange={setContextCount}
onChangeComplete={onContextCountChange}
value={typeof contextCount === 'number' ? contextCount : 0}
marks={{ 0: '0', 25: '25', 50: '50', 75: '75', 100: t('chat.settings.max') }}
step={1}
tooltip={{ formatter: formatSliderTooltip }}
/>
</Col>
</Row>
<Divider style={{ margin: '10px 0' }} />
<SettingRow style={{ minHeight: 30 }}>
<HStack alignItems="center">
@ -382,16 +404,18 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
<Divider style={{ margin: '10px 0' }} />
<SettingRow style={{ minHeight: 30 }}>
<Label>{t('assistants.settings.tool_use_mode')}</Label>
<Select
<Selector
value={toolUseMode}
style={{ width: 110 }}
options={[
{ label: t('assistants.settings.tool_use_mode.prompt'), value: 'prompt' },
{ label: t('assistants.settings.tool_use_mode.function'), value: 'function' }
]}
onChange={(value) => {
setToolUseMode(value)
updateAssistantSettings({ toolUseMode: value })
}}>
<Select.Option value="prompt">{t('assistants.settings.tool_use_mode.prompt')}</Select.Option>
<Select.Option value="function">{t('assistants.settings.tool_use_mode.function')}</Select.Option>
</Select>
}}
size={14}
/>
</SettingRow>
<Divider style={{ margin: '10px 0' }} />
<SettingRow style={{ minHeight: 30 }}>
@ -409,20 +433,26 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
onChange={(e) => onUpdateCustomParameter(index, 'name', e.target.value)}
/>
</Col>
<Col span={4}>
<Col span={6}>
<Select
value={param.type}
onChange={(value) => onUpdateCustomParameter(index, 'type', value)}
style={{ width: '100%' }}>
style={{ width: '100%' }}
suffixIcon={<ChevronDown size={16} color="var(--color-border)" />}>
<Select.Option value="string">{t('models.parameter_type.string')}</Select.Option>
<Select.Option value="number">{t('models.parameter_type.number')}</Select.Option>
<Select.Option value="boolean">{t('models.parameter_type.boolean')}</Select.Option>
<Select.Option value="json">{t('models.parameter_type.json')}</Select.Option>
</Select>
</Col>
<Col span={12}>{renderParameterValueInput(param, index)}</Col>
<Col span={10}>{renderParameterValueInput(param, index)}</Col>
<Col span={2} style={{ display: 'flex', justifyContent: 'flex-end' }}>
<Button icon={<DeleteOutlined />} onClick={() => onDeleteCustomParameter(index)} danger />
<Button
color="danger"
variant="filled"
icon={<DeleteOutlined />}
onClick={() => onDeleteCustomParameter(index)}
/>
</Col>
</Row>
))}
@ -440,13 +470,16 @@ const Container = styled.div`
display: flex;
flex: 1;
flex-direction: column;
overflow: hidden;
padding: 5px;
`
const Label = styled.p`
margin-right: 5px;
font-weight: 500;
display: flex;
align-items: center;
gap: 5px;
flex-shrink: 0;
`
const QuestionIcon = styled(QuestionCircleOutlined)`
@ -455,4 +488,21 @@ const QuestionIcon = styled(QuestionCircleOutlined)`
color: var(--color-text-3);
`
const ModelSelectButton = styled(Button)`
max-width: 300px;
justify-content: flex-start;
.ant-btn-icon {
flex-shrink: 0;
}
`
const ModelName = styled.span`
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: inline-block;
`
export default AssistantModelSettings

View File

@ -65,7 +65,7 @@ const AssistantPromptSettings: React.FC<Props> = ({ assistant, updateAssistant }
<HStack gap={8} alignItems="center">
<Popover content={<EmojiPicker onEmojiClick={handleEmojiSelect} />} arrow trigger="click">
<EmojiButtonWrapper>
<Button style={{ fontSize: 20, padding: '4px', minWidth: '32px', height: '32px' }}>{emoji}</Button>
<Button style={{ fontSize: 18, padding: '4px', minWidth: '28px', height: '28px' }}>{emoji}</Button>
{emoji && (
<CloseCircleFilled
className="delete-icon"

View File

@ -89,12 +89,14 @@ const AssistantSettingPopupContainer: React.FC<Props> = ({ resolve, tab, ...prop
styles={{
content: {
padding: 0,
overflow: 'hidden',
background: 'var(--color-background)'
overflow: 'hidden'
},
header: { padding: '10px 15px', borderBottom: '0.5px solid var(--color-border)', margin: 0 }
header: { padding: '10px 15px', borderBottom: '0.5px solid var(--color-border)', margin: 0, borderRadius: 0 },
body: {
padding: 0
}
}}
width="70vw"
width="min(800px, 70vw)"
height="80vh"
centered>
<HStack>
@ -145,15 +147,14 @@ const AssistantSettingPopupContainer: React.FC<Props> = ({ resolve, tab, ...prop
}
const LeftMenu = styled.div`
background-color: var(--color-background);
height: calc(80vh - 20px);
border-right: 0.5px solid var(--color-border);
`
const Settings = styled.div`
flex: 1;
padding: 10px 20px;
height: calc(80vh - 20px);
padding: 16px 16px;
height: calc(80vh - 16px);
overflow-y: scroll;
`
@ -163,6 +164,7 @@ const StyledModal = styled(Modal)`
}
.ant-modal-close {
top: 4px;
right: 4px;
}
.ant-menu-item {
height: 36px;

View File

@ -39,7 +39,6 @@ const AgentsSubscribeUrlSettings: FC = () => {
/>
</HStack>
</SettingRow>
<SettingDivider />
</SettingGroup>
)
}

View File

@ -4,7 +4,7 @@ import { useTheme } from '@renderer/context/ThemeProvider'
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
import { RootState, useAppDispatch } from '@renderer/store'
import { setJoplinExportReasoning, setJoplinToken, setJoplinUrl } from '@renderer/store/settings'
import { Button, Switch, Tooltip } from 'antd'
import { Button, Space, Switch, Tooltip } from 'antd'
import Input from 'antd/es/input/Input'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
@ -106,14 +106,15 @@ const JoplinSettings: FC = () => {
</Tooltip>
</SettingRowTitle>
<HStack alignItems="center" gap="5px" style={{ width: 315 }}>
<Input
type="password"
value={joplinToken || ''}
onChange={handleJoplinTokenChange}
style={{ width: 250 }}
placeholder={t('settings.data.joplin.token_placeholder')}
/>
<Button onClick={handleJoplinConnectionCheck}>{t('settings.data.joplin.check.button')}</Button>
<Space.Compact style={{ width: '100%' }}>
<Input
type="password"
value={joplinToken || ''}
onChange={handleJoplinTokenChange}
placeholder={t('settings.data.joplin.token_placeholder')}
/>
<Button onClick={handleJoplinConnectionCheck}>{t('settings.data.joplin.check.button')}</Button>
</Space.Compact>
</HStack>
</SettingRow>
<SettingDivider />

View File

@ -10,7 +10,7 @@ import {
setNotionExportReasoning,
setNotionPageNameKey
} from '@renderer/store/settings'
import { Button, Switch, Tooltip } from 'antd'
import { Button, Space, Switch, Tooltip } from 'antd'
import Input from 'antd/es/input/Input'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
@ -121,15 +121,16 @@ const NotionSettings: FC = () => {
<SettingRow>
<SettingRowTitle>{t('settings.data.notion.api_key')}</SettingRowTitle>
<HStack alignItems="center" gap="5px" style={{ width: 315 }}>
<Input
type="password"
value={notionApiKey || ''}
onChange={handleNotionTokenChange}
onBlur={handleNotionTokenChange}
style={{ width: 250 }}
placeholder={t('settings.data.notion.api_key_placeholder')}
/>
<Button onClick={handleNotionConnectionCheck}>{t('settings.data.notion.check.button')}</Button>
<Space.Compact style={{ width: '100%' }}>
<Input
type="password"
value={notionApiKey || ''}
onChange={handleNotionTokenChange}
onBlur={handleNotionTokenChange}
placeholder={t('settings.data.notion.api_key_placeholder')}
/>
<Button onClick={handleNotionConnectionCheck}>{t('settings.data.notion.check.button')}</Button>
</Space.Compact>
</HStack>
</SettingRow>
<SettingDivider />

View File

@ -1,6 +1,7 @@
import { CheckOutlined, FolderOutlined, LoadingOutlined, SyncOutlined, WarningOutlined } from '@ant-design/icons'
import { HStack } from '@renderer/components/Layout'
import NutstorePathPopup from '@renderer/components/Popups/NutsorePathPopup'
import Selector from '@renderer/components/Selector'
import { WebdavBackupManager } from '@renderer/components/WebdavBackupManager'
import { useWebdavBackupModal, WebdavBackupModal } from '@renderer/components/WebdavModals'
import { useTheme } from '@renderer/context/ThemeProvider'
@ -23,7 +24,7 @@ import {
} from '@renderer/store/nutstore'
import { modalConfirm } from '@renderer/utils'
import { NUTSTORE_HOST } from '@shared/config/nutstore'
import { Button, Input, Select, Switch, Tooltip, Typography } from 'antd'
import { Button, Input, Switch, Tooltip, Typography } from 'antd'
import dayjs from 'dayjs'
import { FC, useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -279,18 +280,23 @@ const NutstoreSettings: FC = () => {
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.data.webdav.autoSync')}</SettingRowTitle>
<Select value={syncInterval} onChange={onSyncIntervalChange} style={{ width: 120 }}>
<Select.Option value={0}>{t('settings.data.webdav.autoSync.off')}</Select.Option>
<Select.Option value={1}>{t('settings.data.webdav.minute_interval', { count: 1 })}</Select.Option>
<Select.Option value={5}>{t('settings.data.webdav.minute_interval', { count: 5 })}</Select.Option>
<Select.Option value={15}>{t('settings.data.webdav.minute_interval', { count: 15 })}</Select.Option>
<Select.Option value={30}>{t('settings.data.webdav.minute_interval', { count: 30 })}</Select.Option>
<Select.Option value={60}>{t('settings.data.webdav.hour_interval', { count: 1 })}</Select.Option>
<Select.Option value={120}>{t('settings.data.webdav.hour_interval', { count: 2 })}</Select.Option>
<Select.Option value={360}>{t('settings.data.webdav.hour_interval', { count: 6 })}</Select.Option>
<Select.Option value={720}>{t('settings.data.webdav.hour_interval', { count: 12 })}</Select.Option>
<Select.Option value={1440}>{t('settings.data.webdav.hour_interval', { count: 24 })}</Select.Option>
</Select>
<Selector
size={14}
value={syncInterval}
onChange={onSyncIntervalChange}
options={[
{ label: t('settings.data.webdav.autoSync.off'), value: 0 },
{ label: t('settings.data.webdav.minute_interval', { count: 1 }), value: 1 },
{ label: t('settings.data.webdav.minute_interval', { count: 5 }), value: 5 },
{ label: t('settings.data.webdav.minute_interval', { count: 15 }), value: 15 },
{ label: t('settings.data.webdav.minute_interval', { count: 30 }), value: 30 },
{ label: t('settings.data.webdav.hour_interval', { count: 1 }), value: 60 },
{ label: t('settings.data.webdav.hour_interval', { count: 2 }), value: 120 },
{ label: t('settings.data.webdav.hour_interval', { count: 6 }), value: 360 },
{ label: t('settings.data.webdav.hour_interval', { count: 12 }), value: 720 },
{ label: t('settings.data.webdav.hour_interval', { count: 24 }), value: 1440 }
]}
/>
</SettingRow>
{nutstoreAutoSync && syncInterval > 0 && (
<>

View File

@ -4,7 +4,7 @@ import { useTheme } from '@renderer/context/ThemeProvider'
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
import { RootState, useAppDispatch } from '@renderer/store'
import { setSiyuanApiUrl, setSiyuanBoxId, setSiyuanRootPath, setSiyuanToken } from '@renderer/store/settings'
import { Button, Tooltip } from 'antd'
import { Button, Space, Tooltip } from 'antd'
import Input from 'antd/es/input/Input'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
@ -108,14 +108,15 @@ const SiyuanSettings: FC = () => {
</Tooltip>
</SettingRowTitle>
<HStack alignItems="center" gap="5px" style={{ width: 315 }}>
<Input
type="password"
value={siyuanToken || ''}
onChange={handleTokenChange}
style={{ width: 250 }}
placeholder={t('settings.data.siyuan.token_placeholder')}
/>
<Button onClick={handleCheckConnection}>{t('settings.data.siyuan.check.button')}</Button>
<Space.Compact style={{ width: '100%' }}>
<Input
type="password"
value={siyuanToken || ''}
onChange={handleTokenChange}
placeholder={t('settings.data.siyuan.token_placeholder')}
/>
<Button onClick={handleCheckConnection}>{t('settings.data.siyuan.check.button')}</Button>
</Space.Compact>
</HStack>
</SettingRow>
<SettingDivider />

View File

@ -1,5 +1,6 @@
import { FolderOpenOutlined, SaveOutlined, SyncOutlined, WarningOutlined } from '@ant-design/icons'
import { HStack } from '@renderer/components/Layout'
import Selector from '@renderer/components/Selector'
import { WebdavBackupManager } from '@renderer/components/WebdavBackupManager'
import { useWebdavBackupModal, WebdavBackupModal } from '@renderer/components/WebdavModals'
import { useTheme } from '@renderer/context/ThemeProvider'
@ -16,7 +17,7 @@ import {
setWebdavSyncInterval as _setWebdavSyncInterval,
setWebdavUser as _setWebdavUser
} from '@renderer/store/settings'
import { Button, Input, Select, Switch, Tooltip } from 'antd'
import { Button, Input, Switch, Tooltip } from 'antd'
import dayjs from 'dayjs'
import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -173,31 +174,43 @@ const WebDavSettings: FC = () => {
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.data.webdav.autoSync')}</SettingRowTitle>
<Select value={syncInterval} onChange={onSyncIntervalChange} disabled={!webdavHost} style={{ width: 120 }}>
<Select.Option value={0}>{t('settings.data.webdav.autoSync.off')}</Select.Option>
<Select.Option value={1}>{t('settings.data.webdav.minute_interval', { count: 1 })}</Select.Option>
<Select.Option value={5}>{t('settings.data.webdav.minute_interval', { count: 5 })}</Select.Option>
<Select.Option value={15}>{t('settings.data.webdav.minute_interval', { count: 15 })}</Select.Option>
<Select.Option value={30}>{t('settings.data.webdav.minute_interval', { count: 30 })}</Select.Option>
<Select.Option value={60}>{t('settings.data.webdav.hour_interval', { count: 1 })}</Select.Option>
<Select.Option value={120}>{t('settings.data.webdav.hour_interval', { count: 2 })}</Select.Option>
<Select.Option value={360}>{t('settings.data.webdav.hour_interval', { count: 6 })}</Select.Option>
<Select.Option value={720}>{t('settings.data.webdav.hour_interval', { count: 12 })}</Select.Option>
<Select.Option value={1440}>{t('settings.data.webdav.hour_interval', { count: 24 })}</Select.Option>
</Select>
<Selector
size={14}
value={syncInterval}
onChange={onSyncIntervalChange}
disabled={!webdavHost}
options={[
{ label: t('settings.data.webdav.autoSync.off'), value: 0 },
{ label: t('settings.data.webdav.minute_interval', { count: 1 }), value: 1 },
{ label: t('settings.data.webdav.minute_interval', { count: 5 }), value: 5 },
{ label: t('settings.data.webdav.minute_interval', { count: 15 }), value: 15 },
{ label: t('settings.data.webdav.minute_interval', { count: 30 }), value: 30 },
{ label: t('settings.data.webdav.hour_interval', { count: 1 }), value: 60 },
{ label: t('settings.data.webdav.hour_interval', { count: 2 }), value: 120 },
{ label: t('settings.data.webdav.hour_interval', { count: 6 }), value: 360 },
{ label: t('settings.data.webdav.hour_interval', { count: 12 }), value: 720 },
{ label: t('settings.data.webdav.hour_interval', { count: 24 }), value: 1440 }
]}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.data.webdav.maxBackups')}</SettingRowTitle>
<Select value={maxBackups} onChange={onMaxBackupsChange} disabled={!webdavHost} style={{ width: 120 }}>
<Select.Option value={0}>{t('settings.data.webdav.maxBackups.unlimited')}</Select.Option>
<Select.Option value={1}>1</Select.Option>
<Select.Option value={3}>3</Select.Option>
<Select.Option value={5}>5</Select.Option>
<Select.Option value={10}>10</Select.Option>
<Select.Option value={20}>20</Select.Option>
<Select.Option value={50}>50</Select.Option>
</Select>
<Selector
size={14}
value={maxBackups}
onChange={onMaxBackupsChange}
disabled={!webdavHost}
options={[
{ label: t('settings.data.webdav.maxBackups.unlimited'), value: 0 },
{ label: '1', value: 1 },
{ label: '3', value: 3 },
{ label: '5', value: 5 },
{ label: '10', value: 10 },
{ label: '20', value: 20 },
{ label: '50', value: 50 }
]}
/>
</SettingRow>
<SettingDivider />
<SettingRow>

View File

@ -4,7 +4,7 @@ import { useTheme } from '@renderer/context/ThemeProvider'
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
import { RootState, useAppDispatch } from '@renderer/store'
import { setYuqueRepoId, setYuqueToken, setYuqueUrl } from '@renderer/store/settings'
import { Button, Tooltip } from 'antd'
import { Button, Space, Tooltip } from 'antd'
import Input from 'antd/es/input/Input'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
@ -100,14 +100,15 @@ const YuqueSettings: FC = () => {
</Tooltip>
</SettingRowTitle>
<HStack alignItems="center" gap="5px" style={{ width: 315 }}>
<Input
type="password"
value={yuqueToken || ''}
onChange={handleYuqueTokenChange}
style={{ width: 250 }}
placeholder={t('settings.data.yuque.token_placeholder')}
/>
<Button onClick={handleYuqueConnectionCheck}>{t('settings.data.yuque.check.button')}</Button>
<Space.Compact style={{ width: '100%' }}>
<Input
type="password"
value={yuqueToken || ''}
onChange={handleYuqueTokenChange}
placeholder={t('settings.data.yuque.token_placeholder')}
/>
<Button onClick={handleYuqueConnectionCheck}>{t('settings.data.yuque.check.button')}</Button>
</Space.Compact>
</HStack>
</SettingRow>
</SettingGroup>

View File

@ -196,7 +196,7 @@ const DisplaySettings: FC = () => {
value={userTheme.colorPrimary}
onChange={(color) => handleColorPrimaryChange(color.toHexString())}
showText
style={{ width: '110px' }}
size="small"
presets={[
{
label: 'Presets',
@ -222,13 +222,15 @@ const DisplaySettings: FC = () => {
<SettingRow>
<SettingRowTitle>{t('settings.zoom.title')}</SettingRowTitle>
<ZoomButtonGroup>
<Button onClick={() => handleZoomFactor(-0.1)} icon={<Minus size="14" />} />
<Button onClick={() => handleZoomFactor(-0.1)} icon={<Minus size="14" />} color="default" variant="text" />
<ZoomValue>{Math.round(currentZoom * 100)}%</ZoomValue>
<Button onClick={() => handleZoomFactor(0.1)} icon={<Plus size="14" />} />
<Button onClick={() => handleZoomFactor(0.1)} icon={<Plus size="14" />} color="default" variant="text" />
<Button
onClick={() => handleZoomFactor(0, true)}
style={{ marginLeft: 8 }}
icon={<RotateCcw size="14" />}
color="default"
variant="text"
/>
</ZoomButtonGroup>
</SettingRow>

View File

@ -1,3 +1,4 @@
import Selector from '@renderer/components/Selector'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useSettings } from '@renderer/hooks/useSettings'
import i18n from '@renderer/i18n'
@ -15,7 +16,7 @@ import { LanguageVarious } from '@renderer/types'
import { NotificationSource } from '@renderer/types/notification'
import { isValidProxyUrl } from '@renderer/utils'
import { defaultLanguage } from '@shared/config/constant'
import { Input, Select, Space, Switch } from 'antd'
import { Flex, Input, Switch } from 'antd'
import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
@ -92,7 +93,7 @@ const GeneralSettings: FC = () => {
window.api.setProxy(proxyUrl)
}
const proxyModeOptions = [
const proxyModeOptions: { value: 'system' | 'custom' | 'none'; label: string }[] = [
{ value: 'system', label: t('settings.proxy.mode.system') },
{ value: 'custom', label: t('settings.proxy.mode.custom') },
{ value: 'none', label: t('settings.proxy.mode.none') }
@ -153,59 +154,27 @@ const GeneralSettings: FC = () => {
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('common.language')}</SettingRowTitle>
<Select defaultValue={language || defaultLanguage} style={{ width: 180 }} onChange={onSelectLanguage}>
{languagesOptions.map((lang) => (
<Select.Option key={lang.value} value={lang.value}>
<Space.Compact direction="horizontal" block>
<Space.Compact block>{lang.label}</Space.Compact>
<Selector
size={14}
value={language || defaultLanguage}
onChange={onSelectLanguage}
options={languagesOptions.map((lang) => ({
label: (
<Flex align="center" gap={8}>
<span role="img" aria-label={lang.flag}>
{lang.flag}
</span>
</Space.Compact>
</Select.Option>
))}
</Select>
{lang.label}
</Flex>
),
value: lang.value
}))}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.general.spell_check')}</SettingRowTitle>
<Switch checked={enableSpellCheck} onChange={handleSpellCheckChange} />
</SettingRow>
{enableSpellCheck && (
<>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.general.spell_check.languages')}</SettingRowTitle>
<Select
mode="multiple"
value={spellCheckLanguages}
style={{ width: 280 }}
placeholder={t('settings.general.spell_check.languages')}
onChange={handleSpellCheckLanguagesChange}
options={spellCheckLanguageOptions.map((lang) => ({
value: lang.value,
label: (
<Space.Compact direction="horizontal" block>
<Space.Compact block>{lang.label}</Space.Compact>
<span role="img" aria-label={lang.flag}>
{lang.flag}
</span>
</Space.Compact>
)
}))}
/>
</SettingRow>
</>
)}
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.proxy.mode.title')}</SettingRowTitle>
<Select
value={storeProxyMode}
style={{ width: 180 }}
onChange={onProxyModeChange}
options={proxyModeOptions}
/>
<Selector value={storeProxyMode} onChange={onProxyModeChange} options={proxyModeOptions} />
</SettingRow>
{storeProxyMode === 'custom' && (
<>
@ -223,6 +192,37 @@ const GeneralSettings: FC = () => {
</SettingRow>
</>
)}
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.general.spell_check')}</SettingRowTitle>
<Switch checked={enableSpellCheck} onChange={handleSpellCheckChange} />
</SettingRow>
{enableSpellCheck && (
<>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.general.spell_check.languages')}</SettingRowTitle>
<Selector<string>
size={14}
multiple
value={spellCheckLanguages}
placeholder={t('settings.general.spell_check.languages')}
onChange={handleSpellCheckLanguagesChange}
options={spellCheckLanguageOptions.map((lang) => ({
value: lang.value,
label: (
<Flex align="center" gap={8}>
<span role="img" aria-label={lang.flag}>
{lang.flag}
</span>
{lang.label}
</Flex>
)
}))}
/>
</SettingRow>
</>
)}
</SettingGroup>
<SettingGroup theme={theme}>
<SettingTitle>{t('settings.notification.title')}</SettingTitle>

View File

@ -7,7 +7,7 @@ import { useTheme } from '@renderer/context/ThemeProvider'
import { useDefaultAssistant } from '@renderer/hooks/useAssistant'
import { AssistantSettings as AssistantSettingsType } from '@renderer/types'
import { getLeadingEmoji, modalConfirm } from '@renderer/utils'
import { Button, Col, Input, InputNumber, Modal, Popover, Row, Slider, Switch, Tooltip } from 'antd'
import { Button, Col, Flex, Input, InputNumber, Modal, Popover, Row, Slider, Switch, Tooltip } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import { Dispatch, FC, SetStateAction, useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -101,7 +101,7 @@ const AssistantSettings: FC = () => {
<HStack gap={8} alignItems="center" style={{ margin: '10px 0' }}>
<Popover content={<EmojiPicker onEmojiClick={handleEmojiSelect} />} arrow>
<EmojiButtonWrapper>
<Button style={{ fontSize: 20, padding: '4px', minWidth: '32px', height: '32px' }}>{emoji}</Button>
<Button style={{ fontSize: 20, padding: '4px', minWidth: '30px', height: '30px' }}>{emoji}</Button>
{emoji && (
<CloseCircleFilled
className="delete-icon"
@ -145,7 +145,7 @@ const AssistantSettings: FC = () => {
justifyContent: 'space-between'
}}>
{t('settings.assistant.model_params')}
<Button onClick={onReset} style={{ width: 90 }}>
<Button onClick={onReset} style={{ width: 81 }}>
{t('chat.settings.reset')}
</Button>
</SettingSubtitle>
@ -156,7 +156,7 @@ const AssistantSettings: FC = () => {
</Tooltip>
</Row>
<Row align="middle" style={{ marginBottom: 10 }} gutter={20}>
<Col span={21}>
<Col span={19}>
<Slider
min={0}
max={2}
@ -167,7 +167,7 @@ const AssistantSettings: FC = () => {
step={0.01}
/>
</Col>
<Col span={3}>
<Col span={5}>
<InputNumber
min={0}
max={2}
@ -185,7 +185,7 @@ const AssistantSettings: FC = () => {
</Tooltip>
</Row>
<Row align="middle" style={{ marginBottom: 10 }} gutter={20}>
<Col span={21}>
<Col span={19}>
<Slider
min={0}
max={1}
@ -196,7 +196,7 @@ const AssistantSettings: FC = () => {
step={0.01}
/>
</Col>
<Col span={3}>
<Col span={5}>
<InputNumber min={0} max={1} step={0.01} value={topP} onChange={onTopPChange} style={{ width: '100%' }} />
</Col>
</Row>
@ -207,7 +207,7 @@ const AssistantSettings: FC = () => {
</Tooltip>
</Row>
<Row align="middle" style={{ marginBottom: 10 }} gutter={20}>
<Col span={21}>
<Col span={19}>
<Slider
min={0}
max={20}
@ -218,7 +218,7 @@ const AssistantSettings: FC = () => {
step={1}
/>
</Col>
<Col span={3}>
<Col span={5}>
<InputNumber
min={0}
max={20}
@ -229,7 +229,7 @@ const AssistantSettings: FC = () => {
/>
</Col>
</Row>
<Row align="middle" style={{ marginBottom: 10 }}>
<Flex justify="space-between" align="center" style={{ marginBottom: 10 }}>
<HStack alignItems="center">
<Label>{t('chat.settings.max_tokens')}</Label>
<Tooltip title={t('chat.settings.max_tokens.tip')}>
@ -255,7 +255,7 @@ const AssistantSettings: FC = () => {
onUpdateAssistantSettings({ enableMaxTokens: enabled })
}}
/>
</Row>
</Flex>
{enableMaxTokens && (
<Row align="middle" gutter={20}>
<Col span={24}>
@ -307,7 +307,7 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
afterClose={onClose}
transitionName="animation-move-down"
centered
width={800}
width={500}
footer={null}>
<AssistantSettings />
</Modal>

View File

@ -121,7 +121,7 @@ const PopupContainer: React.FC<Props> = ({ title, provider, resolve }) => {
tooltip={t('settings.models.add.group_name.tooltip')}>
<Input placeholder={t('settings.models.add.group_name.placeholder')} spellCheck={false} />
</Form.Item>
<Form.Item style={{ marginBottom: 0, textAlign: 'center' }}>
<Form.Item style={{ marginBottom: 8, textAlign: 'center' }}>
<Flex justify="end" align="center" style={{ position: 'relative' }}>
<Button type="primary" htmlType="submit" size="middle">
{t('settings.models.add.add_model')}

View File

@ -256,10 +256,6 @@ const PopupContainer: React.FC<Props> = ({ provider: _provider, resolve }) => {
afterClose={onClose}
footer={null}
width="800px"
styles={{
content: { padding: 0 },
header: { padding: '16px 22px 30px 22px' }
}}
transitionName="animation-move-down"
centered>
<SearchContainer>
@ -381,8 +377,6 @@ const SearchContainer = styled.div`
display: flex;
flex-direction: column;
gap: 15px;
padding: 0 22px;
margin-top: -10px;
.ant-radio-group {
display: flex;
@ -399,12 +393,10 @@ const TopToolsWrapper = styled.div`
const ListContainer = styled.div`
height: calc(100vh - 300px);
overflow-y: scroll;
padding: 0 6px 16px 6px;
margin-left: 16px;
margin-right: 10px;
display: flex;
flex-direction: column;
gap: 16px;
padding-right: 2px;
`
const FlexColumn = styled.div`

View File

@ -1,4 +1,3 @@
import { DownOutlined, UpOutlined } from '@ant-design/icons'
import CopyIcon from '@renderer/components/Icons/CopyIcon'
import {
isEmbeddingModel,
@ -10,6 +9,7 @@ import {
import { Model, ModelType } from '@renderer/types'
import { getDefaultGroupName } from '@renderer/utils'
import { Button, Checkbox, Divider, Flex, Form, Input, InputNumber, message, Modal, Select } from 'antd'
import { ChevronDown, ChevronUp } from 'lucide-react'
import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
@ -133,23 +133,26 @@ const ModelEditContent: FC<ModelEditContentProps> = ({ model, onUpdateModel, ope
tooltip={t('settings.models.add.group_name.tooltip')}>
<Input placeholder={t('settings.models.add.group_name.placeholder')} spellCheck={false} />
</Form.Item>
<Form.Item style={{ marginBottom: 15, textAlign: 'center' }}>
<Flex justify="center" align="center" style={{ position: 'relative' }}>
<MoreSettingsRow
<Form.Item style={{ marginBottom: 8, textAlign: 'center' }}>
<Flex justify="space-between" align="center" style={{ position: 'relative' }}>
<Button
color="default"
variant="filled"
icon={showMoreSettings ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
iconPosition="end"
onClick={() => setShowMoreSettings(!showMoreSettings)}
style={{ position: 'absolute', right: 0 }}>
style={{ color: 'var(--color-text-3)' }}>
{t('settings.moresetting')}
<ExpandIcon>{showMoreSettings ? <UpOutlined /> : <DownOutlined />}</ExpandIcon>
</MoreSettingsRow>
</Button>
<Button type="primary" htmlType="submit" size="middle">
{t('common.save')}
</Button>
</Flex>
</Form.Item>
{showMoreSettings && (
<div>
<Divider style={{ margin: '0 0 15px 0' }} />
<TypeTitle>{t('models.type.select')}</TypeTitle>
<div style={{ marginBottom: 8 }}>
<Divider style={{ margin: '16px 0 16px 0' }} />
<TypeTitle>{t('models.type.select')}:</TypeTitle>
{(() => {
const defaultTypes = [
...(isVisionModel(model) ? ['vision'] : []),
@ -235,6 +238,7 @@ const ModelEditContent: FC<ModelEditContentProps> = ({ model, onUpdateModel, ope
}
}}
dropdownMatchSelectWidth={false}
suffixIcon={<ChevronDown size={16} color="var(--color-border)" />}
/>
</Form.Item>
@ -281,32 +285,9 @@ const ModelEditContent: FC<ModelEditContentProps> = ({ model, onUpdateModel, ope
}
const TypeTitle = styled.div`
margin-top: 16px;
margin-bottom: 12px;
margin: 12px 0;
font-size: 14px;
font-weight: 600;
`
const ExpandIcon = styled.div`
font-size: 12px;
color: var(--color-text-3);
`
const MoreSettingsRow = styled.div`
display: flex;
align-items: center;
gap: 8px;
color: var(--color-text-3);
cursor: pointer;
padding: 4px 8px;
border-radius: 4px;
max-width: 150px;
overflow: hidden;
text-overflow: ellipsis;
&:hover {
background-color: var(--color-background-soft);
}
`
export default ModelEditContent

View File

@ -1,5 +1,5 @@
import { TopView } from '@renderer/components/TopView'
import { Button, Form, FormProps, Input, Modal } from 'antd'
import { Button, Flex, Form, FormProps, Input, Modal } from 'antd'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -66,7 +66,7 @@ const PopupContainer: React.FC<Props> = ({ title, resolve }) => {
centered>
<Form
form={form}
labelCol={{ flex: '150px' }}
labelCol={{ flex: '110px' }}
labelAlign="right"
colon={false}
style={{ marginTop: 25 }}
@ -89,11 +89,11 @@ const PopupContainer: React.FC<Props> = ({ title, resolve }) => {
<Form.Item name="name" label={t('settings.websearch.subscribe_name')}>
<Input placeholder={t('settings.websearch.subscribe_name.placeholder')} spellCheck={false} />
</Form.Item>
<Form.Item label=" ">
<Flex justify="end" style={{ marginBottom: 8 }}>
<Button type="primary" htmlType="submit">
{t('settings.websearch.subscribe_add')}
</Button>
</Form.Item>
</Flex>
</Form>
</Modal>
)

View File

@ -1,8 +1,8 @@
import Selector from '@renderer/components/Selector'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useDefaultWebSearchProvider, useWebSearchProviders } from '@renderer/hooks/useWebSearchProviders'
import { WebSearchProvider } from '@renderer/types'
import { hasObjectKey } from '@renderer/utils'
import { Select } from 'antd'
import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -37,9 +37,9 @@ const WebSearchSettings: FC = () => {
<SettingRow>
<SettingRowTitle>{t('settings.websearch.search_provider')}</SettingRowTitle>
<div style={{ display: 'flex', gap: '8px' }}>
<Select
<Selector
size={14}
value={selectedProvider?.id}
style={{ width: '200px' }}
onChange={(value: string) => updateSelectedWebSearchProvider(value)}
placeholder={t('settings.websearch.search_provider_placeholder')}
options={providers.map((p) => ({

View File

@ -23,7 +23,7 @@ import TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
import dayjs from 'dayjs'
import { useLiveQuery } from 'dexie-react-hooks'
import { find, isEmpty, sortBy } from 'lodash'
import { HelpCircle, Settings2, TriangleAlert } from 'lucide-react'
import { ChevronDown, HelpCircle, Settings2, TriangleAlert } from 'lucide-react'
import { FC, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import ReactMarkdown from 'react-markdown'
@ -126,6 +126,7 @@ const TranslateSettings: FC<{
}}
options={selectOptions}
showSearch
suffixIcon={<ChevronDown strokeWidth={1.5} size={16} color="var(--color-text-3)" />}
/>
</HStack>
{!translateModel && (
@ -187,6 +188,7 @@ const TranslateSettings: FC<{
</Space.Compact>
)
}))}
suffixIcon={<ChevronDown strokeWidth={1.5} size={16} color="var(--color-text-3)" />}
/>
<span></span>
<Select
@ -204,6 +206,7 @@ const TranslateSettings: FC<{
</Space.Compact>
)
}))}
suffixIcon={<ChevronDown strokeWidth={1.5} size={16} color="var(--color-text-3)" />}
/>
</Flex>
)}
@ -452,6 +455,7 @@ const TranslatePage: FC = () => {
</Space.Compact>
)
}))}
suffixIcon={<ChevronDown strokeWidth={1.5} size={16} color="var(--color-text-3)" />}
/>
)
}
@ -551,6 +555,7 @@ const TranslatePage: FC = () => {
)
}))
]}
suffixIcon={<ChevronDown strokeWidth={1.5} size={16} color="var(--color-text-3)" />}
/>
<Button
type="text"

View File

@ -85,7 +85,6 @@ const MessageContentContainer = styled.div`
flex: 1;
flex-direction: column;
justify-content: space-between;
margin-left: 46px;
margin-top: 5px;
`