mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-31 00:10:22 +08:00
feat: 一些UI上的优化和重构 (#7479)
- 调整AntdProvider中主题配置,包括颜色、尺寸 - 重构聊天气泡模式的样式 - 重构多选模式的样式 - 添加Selector组件取代ant Select组件 - 重构消息搜索弹窗界面 - 重构知识库搜索弹窗界面 - 优化其他弹框UI
This commit is contained in:
parent
3df5aeb3c3
commit
64b01cce47
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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`
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
114
src/renderer/src/components/EditableNumber/index.tsx
Normal file
114
src/renderer/src/components/EditableNumber/index.tsx
Normal 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
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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;
|
||||
|
||||
192
src/renderer/src/components/Selector.tsx
Normal file
192
src/renderer/src/components/Selector.tsx
Normal 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
|
||||
@ -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}
|
||||
|
||||
@ -412,6 +412,7 @@
|
||||
"search": "Search",
|
||||
"select": "Select",
|
||||
"selectedMessages": "Selected {{count}} messages",
|
||||
"selectedItems": "Selected {{count}} items",
|
||||
"success": "Success",
|
||||
"topics": "Topics",
|
||||
"warning": "Warning",
|
||||
|
||||
@ -412,6 +412,7 @@
|
||||
"search": "検索",
|
||||
"select": "選択",
|
||||
"selectedMessages": "{{count}}件のメッセージを選択しました",
|
||||
"selectedItems": "{{count}}件の項目を選択しました",
|
||||
"success": "成功",
|
||||
"topics": "トピック",
|
||||
"warning": "警告",
|
||||
|
||||
@ -412,6 +412,7 @@
|
||||
"search": "Поиск",
|
||||
"select": "Выбрать",
|
||||
"selectedMessages": "Выбрано {{count}} сообщений",
|
||||
"selectedItems": "Выбрано {{count}} элементов",
|
||||
"success": "Успешно",
|
||||
"topics": "Топики",
|
||||
"warning": "Предупреждение",
|
||||
|
||||
@ -412,6 +412,7 @@
|
||||
"search": "搜索",
|
||||
"select": "选择",
|
||||
"selectedMessages": "选中 {{count}} 条消息",
|
||||
"selectedItems": "已选择 {{count}} 项",
|
||||
"success": "成功",
|
||||
"topics": "话题",
|
||||
"warning": "警告",
|
||||
|
||||
@ -412,6 +412,7 @@
|
||||
"search": "搜尋",
|
||||
"select": "選擇",
|
||||
"selectedMessages": "選中 {{count}} 條訊息",
|
||||
"selectedItems": "已選擇 {{count}} 項",
|
||||
"success": "成功",
|
||||
"topics": "話題",
|
||||
"warning": "警告",
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -151,7 +151,8 @@ const Container = styled.div`
|
||||
`
|
||||
|
||||
const ContainerWrapper = styled.div`
|
||||
width: 800px;
|
||||
width: 100%;
|
||||
padding: 0 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
`
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 />
|
||||
|
||||
@ -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)',
|
||||
|
||||
@ -27,7 +27,7 @@ const CodeBlock: React.FC<Props> = ({ children, className, id, onSave }) => {
|
||||
{children}
|
||||
</CodeBlockView>
|
||||
) : (
|
||||
<code className={className} style={{ textWrap: 'wrap' }}>
|
||||
<code className={className} style={{ textWrap: 'wrap', fontSize: '95%', padding: '2px 4px' }}>
|
||||
{children}
|
||||
</code>
|
||||
)
|
||||
|
||||
@ -93,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({
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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={[
|
||||
{
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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`
|
||||
|
||||
@ -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} {
|
||||
|
||||
@ -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%;
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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`
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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`
|
||||
|
||||
@ -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()}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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`
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -39,7 +39,6 @@ const AgentsSubscribeUrlSettings: FC = () => {
|
||||
/>
|
||||
</HStack>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
</SettingGroup>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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 />
|
||||
|
||||
@ -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 />
|
||||
|
||||
@ -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 && (
|
||||
<>
|
||||
|
||||
@ -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 />
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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')}
|
||||
|
||||
@ -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`
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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) => ({
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -85,7 +85,6 @@ const MessageContentContainer = styled.div`
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
margin-left: 46px;
|
||||
margin-top: 5px;
|
||||
`
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user