feat: narrow layout

This commit is contained in:
kangfenmao 2025-01-08 12:44:01 +08:00
parent ea7a42f736
commit 42ede42f62
10 changed files with 215 additions and 164 deletions

View File

@ -1,88 +1,91 @@
@font-face {
font-family: 'iconfont'; /* Project id 4753420 */
src: url('iconfont.woff2?t=1733224456443') format('woff2');
font-family: "iconfont"; /* Project id 4753420 */
src: url('iconfont.woff2?t=1736309723926') format('woff2'),
url('iconfont.woff?t=1736309723926') format('woff'),
url('iconfont.ttf?t=1736309723926') format('truetype');
}
.iconfont {
font-family: 'iconfont' !important;
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-at1:before {
content: '\e7df';
.icon-at:before {
content: "\e623";
}
.icon-at:before {
content: '\e630';
.icon-icon-adaptive-width:before {
content: "\e87a";
}
.icon-a-darkmode:before {
content: '\e6cd';
content: "\e6cd";
}
.icon-ai-model:before {
content: '\e827';
content: "\e827";
}
.icon-ai-model1:before {
content: '\ec09';
content: "\ec09";
}
.icon-gridlines:before {
content: '\e942';
content: "\e942";
}
.icon-inbox:before {
content: '\e869';
content: "\e869";
}
.icon-business-smart-assistant:before {
content: '\e601';
content: "\e601";
}
.icon-copy:before {
content: '\e6ae';
content: "\e6ae";
}
.icon-ic_send:before {
content: '\e795';
content: "\e795";
}
.icon-dark1:before {
content: '\e72f';
content: "\e72f";
}
.icon-theme-light:before {
content: '\e6b7';
content: "\e6b7";
}
.icon-translate_line:before {
content: '\e7de';
content: "\e7de";
}
.icon-history:before {
content: '\e758';
content: "\e758";
}
.icon-hide-sidebar:before {
content: '\e8eb';
content: "\e8eb";
}
.icon-show-sidebar:before {
content: '\e944';
content: "\e944";
}
.icon-appstore:before {
content: '\e792';
content: "\e792";
}
.icon-chat:before {
content: '\e615';
content: "\e615";
}
.icon-setting:before {
content: '\e78e';
content: "\e78e";
}

View File

@ -35,6 +35,7 @@ import { CSSProperties, FC, useCallback, useEffect, useMemo, useRef, useState }
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import NarrowLayout from '../Messages/NarrowLayout'
import AttachmentButton from './AttachmentButton'
import AttachmentPreview from './AttachmentPreview'
import KnowledgeBaseButton from './KnowledgeBaseButton'
@ -387,114 +388,116 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
return (
<Container onDragOver={handleDragOver} onDrop={handleDrop} className="inputbar">
<AttachmentPreview files={files} setFiles={setFiles} />
<InputBarContainer
id="inputbar"
className={classNames('inputbar-container', inputFocus && 'focus')}
ref={containerRef}>
<Textarea
value={text}
onChange={(e) => setText(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={isTranslating ? t('chat.input.translating') : t('chat.input.placeholder')}
autoFocus
contextMenu="true"
variant="borderless"
spellCheck={false}
rows={textareaRows}
ref={textareaRef}
style={{ fontSize }}
styles={{ textarea: TextareaStyle }}
onFocus={() => setInputFocus(true)}
onBlur={() => setInputFocus(false)}
onInput={onInput}
disabled={searching}
onPaste={(e) => onPaste(e.nativeEvent)}
onClick={() => searching && dispatch(setSearching(false))}
/>
<Toolbar>
<ToolbarMenu>
<Tooltip placement="top" title={t('chat.input.new_topic', { Command: newTopicShortcut })} arrow>
<ToolbarButton type="text" onClick={addNewTopic}>
<FormOutlined />
</ToolbarButton>
</Tooltip>
{isWebSearchModel(model) && (
<Tooltip placement="top" title={t('chat.input.web_search')} arrow>
<NarrowLayout style={{ width: '100%' }}>
<AttachmentPreview files={files} setFiles={setFiles} />
<InputBarContainer
id="inputbar"
className={classNames('inputbar-container', inputFocus && 'focus')}
ref={containerRef}>
<Textarea
value={text}
onChange={(e) => setText(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={isTranslating ? t('chat.input.translating') : t('chat.input.placeholder')}
autoFocus
contextMenu="true"
variant="borderless"
spellCheck={false}
rows={textareaRows}
ref={textareaRef}
style={{ fontSize }}
styles={{ textarea: TextareaStyle }}
onFocus={() => setInputFocus(true)}
onBlur={() => setInputFocus(false)}
onInput={onInput}
disabled={searching}
onPaste={(e) => onPaste(e.nativeEvent)}
onClick={() => searching && dispatch(setSearching(false))}
/>
<Toolbar>
<ToolbarMenu>
<Tooltip placement="top" title={t('chat.input.new_topic', { Command: newTopicShortcut })} arrow>
<ToolbarButton type="text" onClick={addNewTopic}>
<FormOutlined />
</ToolbarButton>
</Tooltip>
{isWebSearchModel(model) && (
<Tooltip placement="top" title={t('chat.input.web_search')} arrow>
<ToolbarButton
type="text"
onClick={() => updateAssistant({ ...assistant, enableWebSearch: !assistant.enableWebSearch })}>
<GlobalOutlined
style={{ color: assistant.enableWebSearch ? 'var(--color-link)' : 'var(--color-icon)' }}
/>
</ToolbarButton>
</Tooltip>
)}
<Tooltip placement="top" title={t('chat.input.clear')} arrow>
<Popconfirm
title={t('chat.input.clear.content')}
placement="top"
onConfirm={clearTopic}
okButtonProps={{ danger: true }}
icon={<QuestionCircleOutlined style={{ color: 'red' }} />}
okText={t('chat.input.clear')}>
<ToolbarButton type="text">
<ClearOutlined />
</ToolbarButton>
</Popconfirm>
</Tooltip>
<Tooltip placement="top" title={t('chat.input.settings')} arrow>
<ToolbarButton
type="text"
onClick={() => updateAssistant({ ...assistant, enableWebSearch: !assistant.enableWebSearch })}>
<GlobalOutlined
style={{ color: assistant.enableWebSearch ? 'var(--color-link)' : 'var(--color-icon)' }}
/>
onClick={() => {
!showTopics && toggleShowTopics()
setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_CHAT_SETTINGS), 0)
}}>
<ControlOutlined />
</ToolbarButton>
</Tooltip>
)}
<Tooltip placement="top" title={t('chat.input.clear')} arrow>
<Popconfirm
title={t('chat.input.clear.content')}
placement="top"
onConfirm={clearTopic}
okButtonProps={{ danger: true }}
icon={<QuestionCircleOutlined style={{ color: 'red' }} />}
okText={t('chat.input.clear')}>
<ToolbarButton type="text">
<ClearOutlined />
</ToolbarButton>
</Popconfirm>
</Tooltip>
<Tooltip placement="top" title={t('chat.input.settings')} arrow>
<ToolbarButton
type="text"
onClick={() => {
!showTopics && toggleShowTopics()
setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_CHAT_SETTINGS), 0)
}}>
<ControlOutlined />
{showKnowledgeIcon && (
<KnowledgeBaseButton
selectedBase={selectedKnowledgeBase}
onSelect={handleKnowledgeBaseSelect}
ToolbarButton={ToolbarButton}
disabled={files.length > 0}
/>
)}
<AttachmentButton model={model} files={files} setFiles={setFiles} ToolbarButton={ToolbarButton} />
<ToolbarButton type="text" onClick={onNewContext}>
<Tooltip placement="top" title={t('chat.input.new.context')}>
<PicCenterOutlined />
</Tooltip>
</ToolbarButton>
</Tooltip>
{showKnowledgeIcon && (
<KnowledgeBaseButton
selectedBase={selectedKnowledgeBase}
onSelect={handleKnowledgeBaseSelect}
<Tooltip placement="top" title={expended ? t('chat.input.collapse') : t('chat.input.expand')} arrow>
<ToolbarButton type="text" onClick={onToggleExpended}>
{expended ? <FullscreenExitOutlined /> : <FullscreenOutlined />}
</ToolbarButton>
</Tooltip>
<TokenCount
estimateTokenCount={estimateTokenCount}
inputTokenCount={inputTokenCount}
contextCount={contextCount}
ToolbarButton={ToolbarButton}
disabled={files.length > 0}
onClick={onNewContext}
/>
)}
<AttachmentButton model={model} files={files} setFiles={setFiles} ToolbarButton={ToolbarButton} />
<ToolbarButton type="text" onClick={onNewContext}>
<Tooltip placement="top" title={t('chat.input.new.context')}>
<PicCenterOutlined />
</Tooltip>
</ToolbarButton>
<Tooltip placement="top" title={expended ? t('chat.input.collapse') : t('chat.input.expand')} arrow>
<ToolbarButton type="text" onClick={onToggleExpended}>
{expended ? <FullscreenExitOutlined /> : <FullscreenOutlined />}
</ToolbarButton>
</Tooltip>
<TokenCount
estimateTokenCount={estimateTokenCount}
inputTokenCount={inputTokenCount}
contextCount={contextCount}
ToolbarButton={ToolbarButton}
onClick={onNewContext}
/>
</ToolbarMenu>
<ToolbarMenu>
{!language.startsWith('en') && (
<TranslateButton text={text} onTranslated={onTranslated} isLoading={isTranslating} />
)}
{generating && (
<Tooltip placement="top" title={t('chat.input.pause')} arrow>
<ToolbarButton type="text" onClick={onPause} style={{ marginRight: -2, marginTop: 1 }}>
<PauseCircleOutlined style={{ color: 'var(--color-error)', fontSize: 20 }} />
</ToolbarButton>
</Tooltip>
)}
{!generating && <SendMessageButton sendMessage={sendMessage} disabled={generating || inputEmpty} />}
</ToolbarMenu>
</Toolbar>
</InputBarContainer>
</ToolbarMenu>
<ToolbarMenu>
{!language.startsWith('en') && (
<TranslateButton text={text} onTranslated={onTranslated} isLoading={isTranslating} />
)}
{generating && (
<Tooltip placement="top" title={t('chat.input.pause')} arrow>
<ToolbarButton type="text" onClick={onPause} style={{ marginRight: -2, marginTop: 1 }}>
<PauseCircleOutlined style={{ color: 'var(--color-error)', fontSize: 20 }} />
</ToolbarButton>
</Tooltip>
)}
{!generating && <SendMessageButton sendMessage={sendMessage} disabled={generating || inputEmpty} />}
</ToolbarMenu>
</Toolbar>
</InputBarContainer>
</NarrowLayout>
</Container>
)
}

View File

@ -224,7 +224,7 @@ const MessageMenubar: FC<Props> = (props) => {
{canRegenerate && (
<Tooltip title={t('chat.message.regenerate.model')} mouseEnterDelay={0.8}>
<ActionButton className="message-action-button" onClick={onAtModelRegenerate}>
<i className="iconfont icon-at1"></i>
<i className="iconfont icon-at"></i>
</ActionButton>
</Tooltip>
)}
@ -335,7 +335,7 @@ const ActionButton = styled.div`
&:hover {
color: var(--color-text-1);
}
.icon-at1 {
.icon-at {
font-size: 16px;
}
`

View File

@ -26,6 +26,7 @@ import styled from 'styled-components'
import Suggestions from '../components/Suggestions'
import MessageItem from './Message'
import NarrowLayout from './NarrowLayout'
import Prompt from './Prompt'
interface Props {
@ -283,33 +284,35 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
key={assistant.id}
ref={containerRef}
right={topicPosition === 'left'}>
<Suggestions assistant={assistant} messages={messages} />
<InfiniteScroll
dataLength={displayMessages.length}
next={loadMoreMessages}
hasMore={hasMore}
loader={null}
inverse={true}
scrollableTarget="messages">
<ScrollContainer>
<LoaderContainer $loading={isLoadingMore}>
<BeatLoader size={8} color="var(--color-text-2)" />
</LoaderContainer>
{displayMessages.map((message, index) => (
<MessageItem
key={message.id}
message={message}
topic={topic}
index={index}
hidePresetMessages={assistant.settings?.hideMessages}
onSetMessages={setMessages}
onDeleteMessage={onDeleteMessage}
onGetMessages={onGetMessages}
/>
))}
</ScrollContainer>
</InfiniteScroll>
<Prompt assistant={assistant} key={assistant.prompt} />
<NarrowLayout style={{ display: 'flex', flexDirection: 'column-reverse' }}>
<Suggestions assistant={assistant} messages={messages} />
<InfiniteScroll
dataLength={displayMessages.length}
next={loadMoreMessages}
hasMore={hasMore}
loader={null}
inverse={true}
scrollableTarget="messages">
<ScrollContainer>
<LoaderContainer $loading={isLoadingMore}>
<BeatLoader size={8} color="var(--color-text-2)" />
</LoaderContainer>
{displayMessages.map((message, index) => (
<MessageItem
key={message.id}
message={message}
topic={topic}
index={index}
hidePresetMessages={assistant.settings?.hideMessages}
onSetMessages={setMessages}
onDeleteMessage={onDeleteMessage}
onGetMessages={onGetMessages}
/>
))}
</ScrollContainer>
</InfiniteScroll>
<Prompt assistant={assistant} key={assistant.prompt} />
</NarrowLayout>
</Container>
)
}

View File

@ -0,0 +1,24 @@
import { useSettings } from '@renderer/hooks/useSettings'
import { FC, HTMLAttributes } from 'react'
import styled from 'styled-components'
interface Props extends HTMLAttributes<HTMLDivElement> {
children: React.ReactNode
}
const NarrowLayout: FC<Props> = ({ children, ...props }) => {
const { narrowMode } = useSettings()
if (narrowMode) {
return <Container {...props}>{children}</Container>
}
return children
}
const Container = styled.div`
max-width: 800px;
margin: 0 auto;
`
export default NarrowLayout

View File

@ -28,7 +28,7 @@ const Container = styled.div`
padding: 10px 20px;
background-color: var(--color-background-soft);
margin-bottom: 20px;
margin: 0 20px 0 20px;
margin: 4px 20px 0 20px;
border-radius: 6px;
cursor: pointer;
border: 0.5px solid var(--color-border);

View File

@ -10,6 +10,8 @@ import { useShortcut } from '@renderer/hooks/useShortcuts'
import { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore'
import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { useAppDispatch } from '@renderer/store'
import { setNarrowMode } from '@renderer/store/settings'
import { Assistant, Topic } from '@renderer/types'
import { FC } from 'react'
import styled from 'styled-components'
@ -25,8 +27,9 @@ interface Props {
const HeaderNavbar: FC<Props> = ({ activeAssistant }) => {
const { assistant } = useAssistant(activeAssistant.id)
const { showAssistants, toggleShowAssistants } = useShowAssistants()
const { topicPosition, sidebarIcons } = useSettings()
const { topicPosition, sidebarIcons, narrowMode } = useSettings()
const { showTopics, toggleShowTopics } = useShowTopics()
const dispatch = useAppDispatch()
useShortcut('toggle_show_assistants', () => {
toggleShowAssistants()
@ -75,19 +78,22 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant }) => {
</TitleText>
<SelectModelButton assistant={assistant} />
</HStack>
<HStack alignItems="center">
<HStack alignItems="center" gap={8}>
<NavbarIcon onClick={() => SearchPopup.show()}>
<SearchOutlined />
</NavbarIcon>
<NavbarIcon onClick={() => dispatch(setNarrowMode(!narrowMode))}>
<i className="iconfont icon-icon-adaptive-width"></i>
</NavbarIcon>
{sidebarIcons.visible.includes('minapp') && (
<AppStorePopover>
<NavbarIcon style={{ marginLeft: isMac ? 5 : 10 }}>
<NavbarIcon>
<i className="iconfont icon-appstore" />
</NavbarIcon>
</AppStorePopover>
)}
{topicPosition === 'right' && (
<NavbarIcon onClick={toggleShowTopics} style={{ marginLeft: isMac ? 5 : 10 }}>
<NavbarIcon onClick={toggleShowTopics}>
<i className={`iconfont icon-${showTopics ? 'show' : 'hide'}-sidebar`} />
</NavbarIcon>
)}

View File

@ -133,7 +133,13 @@ export default class OpenAIProvider extends BaseProvider {
}
const isOpenAIo1 = model.id.includes('o1-')
const isSupportStreamOutput = streamOutput
const isSupportStreamOutput = () => {
if (this.provider.id === 'github' && isOpenAIo1) {
return false
}
return streamOutput
}
let time_first_token_millsec = 0
const start_time_millsec = new Date().getTime()
@ -148,12 +154,12 @@ export default class OpenAIProvider extends BaseProvider {
top_p: assistant?.settings?.topP,
max_tokens: maxTokens,
keep_alive: this.keepAliveTime,
stream: isSupportStreamOutput,
stream: isSupportStreamOutput(),
...(assistant.enableWebSearch ? getWebSearchParams(model) : {}),
...this.getCustomParameters(assistant)
})
if (!isSupportStreamOutput) {
if (!isSupportStreamOutput()) {
const time_completion_millsec = new Date().getTime() - start_time_millsec
return onChunk({
text: stream.choices[0].message?.content || '',

View File

@ -60,6 +60,7 @@ export interface SettingsState {
visible: SidebarIcon[]
disabled: SidebarIcon[]
}
narrowMode: boolean
}
const initialState: SettingsState = {
@ -103,7 +104,8 @@ const initialState: SettingsState = {
sidebarIcons: {
visible: DEFAULT_SIDEBAR_ICONS,
disabled: []
}
},
narrowMode: false
}
const settingsSlice = createSlice({
@ -230,6 +232,9 @@ const settingsSlice = createSlice({
},
setSidebarIcons: (state, action: PayloadAction<{ visible: SidebarIcon[]; disabled: SidebarIcon[] }>) => {
state.sidebarIcons = action.payload
},
setNarrowMode: (state, action: PayloadAction<boolean>) => {
state.narrowMode = action.payload
}
}
})
@ -274,7 +279,8 @@ export const {
setPasteLongTextThreshold,
setCustomCss,
setTopicNamingPrompt,
setSidebarIcons
setSidebarIcons,
setNarrowMode
} = settingsSlice.actions
export default settingsSlice.reducer