feat: add attachment button
This commit is contained in:
parent
89bdab58f7
commit
cb95562e58
7
src/renderer/src/hooks/useModel.ts
Normal file
7
src/renderer/src/hooks/useModel.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { useProviders } from './useProvider'
|
||||
|
||||
export function useModel(id?: string) {
|
||||
const { providers } = useProviders()
|
||||
const allModels = providers.map((p) => p.models).flat()
|
||||
return allModels.find((m) => m.id === id)
|
||||
}
|
||||
@ -77,6 +77,7 @@ const resources = {
|
||||
'input.send': 'Send',
|
||||
'input.pause': 'Pause',
|
||||
'input.settings': 'Settings',
|
||||
'input.upload': 'Upload image png、jpg、jpeg',
|
||||
'input.context_count.tip': 'Context Count',
|
||||
'input.estimated_tokens.tip': 'Estimated tokens',
|
||||
'settings.temperature': 'Temperature',
|
||||
@ -314,6 +315,7 @@ const resources = {
|
||||
'input.send': '发送',
|
||||
'input.pause': '暂停',
|
||||
'input.settings': '设置',
|
||||
'input.upload': '上传图片 png、jpg、jpeg',
|
||||
'input.context_count.tip': '上下文数',
|
||||
'input.estimated_tokens.tip': '预估 token 数',
|
||||
'settings.temperature': '模型温度',
|
||||
|
||||
45
src/renderer/src/pages/home/Inputbar/AttachmentButton.tsx
Normal file
45
src/renderer/src/pages/home/Inputbar/AttachmentButton.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
import { PaperClipOutlined } from '@ant-design/icons'
|
||||
import { Tooltip, Upload } from 'antd'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface Props {
|
||||
images: string[]
|
||||
setImages: (images: string[]) => void
|
||||
ToolbarButton: any
|
||||
}
|
||||
|
||||
const AttachmentButton: FC<Props> = ({ images, setImages, ToolbarButton }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Tooltip placement="top" title={t('chat.input.upload')} arrow>
|
||||
<Upload
|
||||
customRequest={() => {}}
|
||||
accept="image/*"
|
||||
itemRender={() => null}
|
||||
maxCount={1}
|
||||
onChange={async ({ file }) => {
|
||||
try {
|
||||
const _file = file.originFileObj as File
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e: ProgressEvent<FileReader>) => {
|
||||
const result = e.target?.result
|
||||
if (typeof result === 'string') {
|
||||
setImages([result])
|
||||
}
|
||||
}
|
||||
reader.readAsDataURL(_file)
|
||||
} catch (error: any) {
|
||||
window.message.error(error.message)
|
||||
}
|
||||
}}>
|
||||
<ToolbarButton type="text" className={images.length ? 'active' : ''}>
|
||||
<PaperClipOutlined style={{ rotate: '135deg' }} />
|
||||
</ToolbarButton>
|
||||
</Upload>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
export default AttachmentButton
|
||||
@ -26,6 +26,7 @@ import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import AttachmentButton from './AttachmentButton'
|
||||
import SendMessageButton from './SendMessageButton'
|
||||
|
||||
interface Props {
|
||||
@ -44,6 +45,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
||||
const [estimateTokenCount, setEstimateTokenCount] = useState(0)
|
||||
const generating = useAppSelector((state) => state.runtime.generating)
|
||||
const inputRef = useRef<TextAreaRef>(null)
|
||||
const [images, setImages] = useState<string[]>([])
|
||||
const { t } = useTranslation()
|
||||
|
||||
_text = text
|
||||
@ -67,13 +69,18 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
||||
status: 'success'
|
||||
}
|
||||
|
||||
if (images.length > 0) {
|
||||
message.images = images
|
||||
}
|
||||
|
||||
EventEmitter.emit(EVENT_NAMES.SEND_MESSAGE, message)
|
||||
|
||||
setText('')
|
||||
setImages([])
|
||||
setTimeout(() => setText(''), 500)
|
||||
|
||||
setExpend(false)
|
||||
}, [assistant.id, assistant.topics, generating, text])
|
||||
}, [assistant.id, assistant.topics, generating, images, text])
|
||||
|
||||
const inputTokenCount = useMemo(() => estimateInputTokenCount(text), [text])
|
||||
|
||||
@ -153,6 +160,20 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
||||
id="inputbar"
|
||||
style={{ minHeight: expended ? '60%' : 'var(--input-bar-height)' }}
|
||||
className={inputFocus ? 'focus' : ''}>
|
||||
<Textarea
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={t('chat.input.placeholder')}
|
||||
autoFocus
|
||||
contextMenu="true"
|
||||
variant="borderless"
|
||||
ref={inputRef}
|
||||
styles={{ textarea: { paddingLeft: 0 } }}
|
||||
onFocus={() => setInputFocus(true)}
|
||||
onBlur={() => setInputFocus(false)}
|
||||
style={{ fontSize }}
|
||||
/>
|
||||
<Toolbar>
|
||||
<ToolbarMenu>
|
||||
<Tooltip placement="top" title={t('chat.input.new_topic')} arrow>
|
||||
@ -183,6 +204,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
||||
<ControlOutlined />
|
||||
</ToolbarButton>
|
||||
</Tooltip>
|
||||
<AttachmentButton images={images} setImages={setImages} ToolbarButton={ToolbarButton} />
|
||||
<Tooltip placement="top" title={expended ? t('chat.input.collapse') : t('chat.input.expand')} arrow>
|
||||
<ToolbarButton type="text" onClick={() => setExpend(!expended)}>
|
||||
{expended ? <FullscreenExitOutlined /> : <FullscreenOutlined />}
|
||||
@ -221,20 +243,6 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
||||
{!generating && <SendMessageButton sendMessage={sendMessage} disabled={generating || !text} />}
|
||||
</ToolbarMenu>
|
||||
</Toolbar>
|
||||
<Textarea
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={t('chat.input.placeholder')}
|
||||
autoFocus
|
||||
contextMenu="true"
|
||||
variant="borderless"
|
||||
ref={inputRef}
|
||||
styles={{ textarea: { paddingLeft: 0 } }}
|
||||
onFocus={() => setInputFocus(true)}
|
||||
onBlur={() => setInputFocus(false)}
|
||||
style={{ fontSize }}
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@ -257,7 +265,7 @@ const Textarea = styled(TextArea)`
|
||||
border-radius: 0;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
margin: 0 15px 5px 15px;
|
||||
margin: 10px 15px 0 15px;
|
||||
font-family: Ubuntu;
|
||||
resize: vertical;
|
||||
overflow: auto;
|
||||
@ -269,8 +277,8 @@ const Toolbar = styled.div`
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
padding: 0 8px;
|
||||
padding-top: 3px;
|
||||
padding-bottom: 0;
|
||||
margin: 3px 0;
|
||||
`
|
||||
|
||||
const ToolbarMenu = styled.div`
|
||||
@ -291,7 +299,8 @@ const ToolbarButton = styled(Button)`
|
||||
transition: all 0.3s ease;
|
||||
color: var(--color-icon);
|
||||
}
|
||||
&:hover {
|
||||
&:hover,
|
||||
&.active {
|
||||
background-color: var(--color-background-soft);
|
||||
.anticon {
|
||||
color: var(--color-text-1);
|
||||
|
||||
@ -11,6 +11,7 @@ import { FONT_FAMILY } from '@renderer/config/constant'
|
||||
import { getModelLogo } from '@renderer/config/provider'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import useAvatar from '@renderer/hooks/useAvatar'
|
||||
import { useModel } from '@renderer/hooks/useModel'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useRuntime } from '@renderer/hooks/useStore'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
|
||||
@ -25,6 +26,7 @@ import styled from 'styled-components'
|
||||
|
||||
import SelectModelDropdown from '../components/SelectModelDropdown'
|
||||
import Markdown from '../Markdown/Markdown'
|
||||
import MessageAttachments from './MessageAttachments'
|
||||
|
||||
interface Props {
|
||||
message: Message
|
||||
@ -37,7 +39,8 @@ interface Props {
|
||||
const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) => {
|
||||
const avatar = useAvatar()
|
||||
const { t } = useTranslation()
|
||||
const { assistant, model, setModel } = useAssistant(message.assistantId)
|
||||
const { assistant, setModel } = useAssistant(message.assistantId)
|
||||
const model = useModel(message.modelId)
|
||||
const { userName, showMessageDivider, messageFont, fontSize } = useSettings()
|
||||
const { generating } = useRuntime()
|
||||
const [copied, setCopied] = useState(false)
|
||||
@ -67,9 +70,9 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
|
||||
|
||||
const getUserName = useCallback(() => {
|
||||
if (message.id === 'assistant') return assistant?.name
|
||||
if (message.role === 'assistant') return upperFirst(model.name || model.id)
|
||||
if (message.role === 'assistant') return upperFirst(model?.name || model?.id)
|
||||
return userName || t('common.you')
|
||||
}, [assistant?.name, message.id, message.role, model.id, model.name, t, userName])
|
||||
}, [assistant?.name, message.id, message.role, model?.id, model?.name, t, userName])
|
||||
|
||||
const fontFamily = useMemo(() => {
|
||||
return messageFont === 'serif' ? FONT_FAMILY.replace('sans-serif', 'serif').replace('Ubuntu, ', '') : FONT_FAMILY
|
||||
@ -115,8 +118,13 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
|
||||
)
|
||||
}
|
||||
|
||||
return <Markdown message={message} />
|
||||
}, [message])
|
||||
return (
|
||||
<>
|
||||
<Markdown message={message} />
|
||||
<MessageAttachments message={message} />
|
||||
</>
|
||||
)
|
||||
}, [message, t])
|
||||
|
||||
return (
|
||||
<MessageContainer key={message.id} className="message">
|
||||
|
||||
25
src/renderer/src/pages/home/Messages/MessageAttachments.tsx
Normal file
25
src/renderer/src/pages/home/Messages/MessageAttachments.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import { Message } from '@renderer/types'
|
||||
import { Image as AntdImage } from 'antd'
|
||||
import { FC } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface Props {
|
||||
message: Message
|
||||
}
|
||||
|
||||
const MessageAttachments: FC<Props> = ({ message }) => {
|
||||
return <Container>{message.images?.map((image) => <Image src={image} key={image} width="33%" />)}</Container>
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
margin-top: 8px;
|
||||
`
|
||||
|
||||
const Image = styled(AntdImage)`
|
||||
border-radius: 10px;
|
||||
`
|
||||
|
||||
export default MessageAttachments
|
||||
@ -8,7 +8,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface Props extends DropdownProps {
|
||||
model: Model
|
||||
model?: Model
|
||||
onSelect: (model: Model) => void
|
||||
}
|
||||
|
||||
@ -37,7 +37,7 @@ const SelectModelDropdown: FC<Props & PropsWithChildren> = ({ children, model, o
|
||||
|
||||
return (
|
||||
<DropdownMenu
|
||||
menu={{ items, style: { maxHeight: '80vh', overflow: 'auto' }, selectedKeys: [model?.id] }}
|
||||
menu={{ items, style: { maxHeight: '80vh', overflow: 'auto' }, selectedKeys: model ? [model.id] : [] }}
|
||||
trigger={['click']}
|
||||
arrow
|
||||
placement="bottom"
|
||||
|
||||
@ -51,10 +51,17 @@ export default class ProviderSDK {
|
||||
const { contextCount, maxTokens } = getAssistantSettings(assistant)
|
||||
|
||||
const systemMessage = assistant.prompt ? { role: 'system', content: assistant.prompt } : undefined
|
||||
const userMessages = takeRight(messages, contextCount + 1).map((message) => ({
|
||||
role: message.role,
|
||||
content: message.content
|
||||
}))
|
||||
const userMessages = takeRight(messages, contextCount + 1).map((message) => {
|
||||
return {
|
||||
role: message.role,
|
||||
content: message.images
|
||||
? [
|
||||
{ type: 'text', text: message.content },
|
||||
...message.images!.map((image) => ({ type: 'image_url', image_url: image }))
|
||||
]
|
||||
: message.content
|
||||
}
|
||||
})
|
||||
|
||||
if (this.isAnthropic) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
|
||||
@ -22,6 +22,7 @@ export type Message = {
|
||||
id: string
|
||||
role: 'user' | 'assistant'
|
||||
content: string
|
||||
images?: string[]
|
||||
assistantId: string
|
||||
topicId: string
|
||||
modelId?: string
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user