feat: add attachment button

This commit is contained in:
kangfenmao 2024-09-01 23:22:21 +08:00
parent 89bdab58f7
commit cb95562e58
9 changed files with 133 additions and 29 deletions

View 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)
}

View File

@ -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': '模型温度',

View 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

View File

@ -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);

View File

@ -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">

View 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

View File

@ -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"

View File

@ -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) => {

View File

@ -22,6 +22,7 @@ export type Message = {
id: string
role: 'user' | 'assistant'
content: string
images?: string[]
assistantId: string
topicId: string
modelId?: string