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-face {
font-family: 'iconfont'; /* Project id 4753420 */ font-family: "iconfont"; /* Project id 4753420 */
src: url('iconfont.woff2?t=1733224456443') format('woff2'); src: url('iconfont.woff2?t=1736309723926') format('woff2'),
url('iconfont.woff?t=1736309723926') format('woff'),
url('iconfont.ttf?t=1736309723926') format('truetype');
} }
.iconfont { .iconfont {
font-family: 'iconfont' !important; font-family: "iconfont" !important;
font-size: 16px; font-size: 16px;
font-style: normal; font-style: normal;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.icon-at1:before { .icon-at:before {
content: '\e7df'; content: "\e623";
} }
.icon-at:before { .icon-icon-adaptive-width:before {
content: '\e630'; content: "\e87a";
} }
.icon-a-darkmode:before { .icon-a-darkmode:before {
content: '\e6cd'; content: "\e6cd";
} }
.icon-ai-model:before { .icon-ai-model:before {
content: '\e827'; content: "\e827";
} }
.icon-ai-model1:before { .icon-ai-model1:before {
content: '\ec09'; content: "\ec09";
} }
.icon-gridlines:before { .icon-gridlines:before {
content: '\e942'; content: "\e942";
} }
.icon-inbox:before { .icon-inbox:before {
content: '\e869'; content: "\e869";
} }
.icon-business-smart-assistant:before { .icon-business-smart-assistant:before {
content: '\e601'; content: "\e601";
} }
.icon-copy:before { .icon-copy:before {
content: '\e6ae'; content: "\e6ae";
} }
.icon-ic_send:before { .icon-ic_send:before {
content: '\e795'; content: "\e795";
} }
.icon-dark1:before { .icon-dark1:before {
content: '\e72f'; content: "\e72f";
} }
.icon-theme-light:before { .icon-theme-light:before {
content: '\e6b7'; content: "\e6b7";
} }
.icon-translate_line:before { .icon-translate_line:before {
content: '\e7de'; content: "\e7de";
} }
.icon-history:before { .icon-history:before {
content: '\e758'; content: "\e758";
} }
.icon-hide-sidebar:before { .icon-hide-sidebar:before {
content: '\e8eb'; content: "\e8eb";
} }
.icon-show-sidebar:before { .icon-show-sidebar:before {
content: '\e944'; content: "\e944";
} }
.icon-appstore:before { .icon-appstore:before {
content: '\e792'; content: "\e792";
} }
.icon-chat:before { .icon-chat:before {
content: '\e615'; content: "\e615";
} }
.icon-setting:before { .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 { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
import NarrowLayout from '../Messages/NarrowLayout'
import AttachmentButton from './AttachmentButton' import AttachmentButton from './AttachmentButton'
import AttachmentPreview from './AttachmentPreview' import AttachmentPreview from './AttachmentPreview'
import KnowledgeBaseButton from './KnowledgeBaseButton' import KnowledgeBaseButton from './KnowledgeBaseButton'
@ -387,114 +388,116 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
return ( return (
<Container onDragOver={handleDragOver} onDrop={handleDrop} className="inputbar"> <Container onDragOver={handleDragOver} onDrop={handleDrop} className="inputbar">
<AttachmentPreview files={files} setFiles={setFiles} /> <NarrowLayout style={{ width: '100%' }}>
<InputBarContainer <AttachmentPreview files={files} setFiles={setFiles} />
id="inputbar" <InputBarContainer
className={classNames('inputbar-container', inputFocus && 'focus')} id="inputbar"
ref={containerRef}> className={classNames('inputbar-container', inputFocus && 'focus')}
<Textarea ref={containerRef}>
value={text} <Textarea
onChange={(e) => setText(e.target.value)} value={text}
onKeyDown={handleKeyDown} onChange={(e) => setText(e.target.value)}
placeholder={isTranslating ? t('chat.input.translating') : t('chat.input.placeholder')} onKeyDown={handleKeyDown}
autoFocus placeholder={isTranslating ? t('chat.input.translating') : t('chat.input.placeholder')}
contextMenu="true" autoFocus
variant="borderless" contextMenu="true"
spellCheck={false} variant="borderless"
rows={textareaRows} spellCheck={false}
ref={textareaRef} rows={textareaRows}
style={{ fontSize }} ref={textareaRef}
styles={{ textarea: TextareaStyle }} style={{ fontSize }}
onFocus={() => setInputFocus(true)} styles={{ textarea: TextareaStyle }}
onBlur={() => setInputFocus(false)} onFocus={() => setInputFocus(true)}
onInput={onInput} onBlur={() => setInputFocus(false)}
disabled={searching} onInput={onInput}
onPaste={(e) => onPaste(e.nativeEvent)} disabled={searching}
onClick={() => searching && dispatch(setSearching(false))} onPaste={(e) => onPaste(e.nativeEvent)}
/> onClick={() => searching && dispatch(setSearching(false))}
<Toolbar> />
<ToolbarMenu> <Toolbar>
<Tooltip placement="top" title={t('chat.input.new_topic', { Command: newTopicShortcut })} arrow> <ToolbarMenu>
<ToolbarButton type="text" onClick={addNewTopic}> <Tooltip placement="top" title={t('chat.input.new_topic', { Command: newTopicShortcut })} arrow>
<FormOutlined /> <ToolbarButton type="text" onClick={addNewTopic}>
</ToolbarButton> <FormOutlined />
</Tooltip> </ToolbarButton>
{isWebSearchModel(model) && ( </Tooltip>
<Tooltip placement="top" title={t('chat.input.web_search')} arrow> {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 <ToolbarButton
type="text" type="text"
onClick={() => updateAssistant({ ...assistant, enableWebSearch: !assistant.enableWebSearch })}> onClick={() => {
<GlobalOutlined !showTopics && toggleShowTopics()
style={{ color: assistant.enableWebSearch ? 'var(--color-link)' : 'var(--color-icon)' }} setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_CHAT_SETTINGS), 0)
/> }}>
<ControlOutlined />
</ToolbarButton> </ToolbarButton>
</Tooltip> </Tooltip>
)} {showKnowledgeIcon && (
<Tooltip placement="top" title={t('chat.input.clear')} arrow> <KnowledgeBaseButton
<Popconfirm selectedBase={selectedKnowledgeBase}
title={t('chat.input.clear.content')} onSelect={handleKnowledgeBaseSelect}
placement="top" ToolbarButton={ToolbarButton}
onConfirm={clearTopic} disabled={files.length > 0}
okButtonProps={{ danger: true }} />
icon={<QuestionCircleOutlined style={{ color: 'red' }} />} )}
okText={t('chat.input.clear')}> <AttachmentButton model={model} files={files} setFiles={setFiles} ToolbarButton={ToolbarButton} />
<ToolbarButton type="text"> <ToolbarButton type="text" onClick={onNewContext}>
<ClearOutlined /> <Tooltip placement="top" title={t('chat.input.new.context')}>
</ToolbarButton> <PicCenterOutlined />
</Popconfirm> </Tooltip>
</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 />
</ToolbarButton> </ToolbarButton>
</Tooltip> <Tooltip placement="top" title={expended ? t('chat.input.collapse') : t('chat.input.expand')} arrow>
{showKnowledgeIcon && ( <ToolbarButton type="text" onClick={onToggleExpended}>
<KnowledgeBaseButton {expended ? <FullscreenExitOutlined /> : <FullscreenOutlined />}
selectedBase={selectedKnowledgeBase} </ToolbarButton>
onSelect={handleKnowledgeBaseSelect} </Tooltip>
<TokenCount
estimateTokenCount={estimateTokenCount}
inputTokenCount={inputTokenCount}
contextCount={contextCount}
ToolbarButton={ToolbarButton} ToolbarButton={ToolbarButton}
disabled={files.length > 0} onClick={onNewContext}
/> />
)} </ToolbarMenu>
<AttachmentButton model={model} files={files} setFiles={setFiles} ToolbarButton={ToolbarButton} /> <ToolbarMenu>
<ToolbarButton type="text" onClick={onNewContext}> {!language.startsWith('en') && (
<Tooltip placement="top" title={t('chat.input.new.context')}> <TranslateButton text={text} onTranslated={onTranslated} isLoading={isTranslating} />
<PicCenterOutlined /> )}
</Tooltip> {generating && (
</ToolbarButton> <Tooltip placement="top" title={t('chat.input.pause')} arrow>
<Tooltip placement="top" title={expended ? t('chat.input.collapse') : t('chat.input.expand')} arrow> <ToolbarButton type="text" onClick={onPause} style={{ marginRight: -2, marginTop: 1 }}>
<ToolbarButton type="text" onClick={onToggleExpended}> <PauseCircleOutlined style={{ color: 'var(--color-error)', fontSize: 20 }} />
{expended ? <FullscreenExitOutlined /> : <FullscreenOutlined />} </ToolbarButton>
</ToolbarButton> </Tooltip>
</Tooltip> )}
<TokenCount {!generating && <SendMessageButton sendMessage={sendMessage} disabled={generating || inputEmpty} />}
estimateTokenCount={estimateTokenCount} </ToolbarMenu>
inputTokenCount={inputTokenCount} </Toolbar>
contextCount={contextCount} </InputBarContainer>
ToolbarButton={ToolbarButton} </NarrowLayout>
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>
</Container> </Container>
) )
} }

View File

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

View File

@ -26,6 +26,7 @@ import styled from 'styled-components'
import Suggestions from '../components/Suggestions' import Suggestions from '../components/Suggestions'
import MessageItem from './Message' import MessageItem from './Message'
import NarrowLayout from './NarrowLayout'
import Prompt from './Prompt' import Prompt from './Prompt'
interface Props { interface Props {
@ -283,33 +284,35 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
key={assistant.id} key={assistant.id}
ref={containerRef} ref={containerRef}
right={topicPosition === 'left'}> right={topicPosition === 'left'}>
<Suggestions assistant={assistant} messages={messages} /> <NarrowLayout style={{ display: 'flex', flexDirection: 'column-reverse' }}>
<InfiniteScroll <Suggestions assistant={assistant} messages={messages} />
dataLength={displayMessages.length} <InfiniteScroll
next={loadMoreMessages} dataLength={displayMessages.length}
hasMore={hasMore} next={loadMoreMessages}
loader={null} hasMore={hasMore}
inverse={true} loader={null}
scrollableTarget="messages"> inverse={true}
<ScrollContainer> scrollableTarget="messages">
<LoaderContainer $loading={isLoadingMore}> <ScrollContainer>
<BeatLoader size={8} color="var(--color-text-2)" /> <LoaderContainer $loading={isLoadingMore}>
</LoaderContainer> <BeatLoader size={8} color="var(--color-text-2)" />
{displayMessages.map((message, index) => ( </LoaderContainer>
<MessageItem {displayMessages.map((message, index) => (
key={message.id} <MessageItem
message={message} key={message.id}
topic={topic} message={message}
index={index} topic={topic}
hidePresetMessages={assistant.settings?.hideMessages} index={index}
onSetMessages={setMessages} hidePresetMessages={assistant.settings?.hideMessages}
onDeleteMessage={onDeleteMessage} onSetMessages={setMessages}
onGetMessages={onGetMessages} onDeleteMessage={onDeleteMessage}
/> onGetMessages={onGetMessages}
))} />
</ScrollContainer> ))}
</InfiniteScroll> </ScrollContainer>
<Prompt assistant={assistant} key={assistant.prompt} /> </InfiniteScroll>
<Prompt assistant={assistant} key={assistant.prompt} />
</NarrowLayout>
</Container> </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; padding: 10px 20px;
background-color: var(--color-background-soft); background-color: var(--color-background-soft);
margin-bottom: 20px; margin-bottom: 20px;
margin: 0 20px 0 20px; margin: 4px 20px 0 20px;
border-radius: 6px; border-radius: 6px;
cursor: pointer; cursor: pointer;
border: 0.5px solid var(--color-border); 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 { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore'
import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings' import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' 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 { Assistant, Topic } from '@renderer/types'
import { FC } from 'react' import { FC } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
@ -25,8 +27,9 @@ interface Props {
const HeaderNavbar: FC<Props> = ({ activeAssistant }) => { const HeaderNavbar: FC<Props> = ({ activeAssistant }) => {
const { assistant } = useAssistant(activeAssistant.id) const { assistant } = useAssistant(activeAssistant.id)
const { showAssistants, toggleShowAssistants } = useShowAssistants() const { showAssistants, toggleShowAssistants } = useShowAssistants()
const { topicPosition, sidebarIcons } = useSettings() const { topicPosition, sidebarIcons, narrowMode } = useSettings()
const { showTopics, toggleShowTopics } = useShowTopics() const { showTopics, toggleShowTopics } = useShowTopics()
const dispatch = useAppDispatch()
useShortcut('toggle_show_assistants', () => { useShortcut('toggle_show_assistants', () => {
toggleShowAssistants() toggleShowAssistants()
@ -75,19 +78,22 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant }) => {
</TitleText> </TitleText>
<SelectModelButton assistant={assistant} /> <SelectModelButton assistant={assistant} />
</HStack> </HStack>
<HStack alignItems="center"> <HStack alignItems="center" gap={8}>
<NavbarIcon onClick={() => SearchPopup.show()}> <NavbarIcon onClick={() => SearchPopup.show()}>
<SearchOutlined /> <SearchOutlined />
</NavbarIcon> </NavbarIcon>
<NavbarIcon onClick={() => dispatch(setNarrowMode(!narrowMode))}>
<i className="iconfont icon-icon-adaptive-width"></i>
</NavbarIcon>
{sidebarIcons.visible.includes('minapp') && ( {sidebarIcons.visible.includes('minapp') && (
<AppStorePopover> <AppStorePopover>
<NavbarIcon style={{ marginLeft: isMac ? 5 : 10 }}> <NavbarIcon>
<i className="iconfont icon-appstore" /> <i className="iconfont icon-appstore" />
</NavbarIcon> </NavbarIcon>
</AppStorePopover> </AppStorePopover>
)} )}
{topicPosition === 'right' && ( {topicPosition === 'right' && (
<NavbarIcon onClick={toggleShowTopics} style={{ marginLeft: isMac ? 5 : 10 }}> <NavbarIcon onClick={toggleShowTopics}>
<i className={`iconfont icon-${showTopics ? 'show' : 'hide'}-sidebar`} /> <i className={`iconfont icon-${showTopics ? 'show' : 'hide'}-sidebar`} />
</NavbarIcon> </NavbarIcon>
)} )}

View File

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

View File

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