Merge pull request #3 from xihajun/codex/add-image-support-to-ctrl+e-input-m06lwh

feat: enable image input in mini assistant
This commit is contained in:
who is 2025-12-10 09:33:17 +00:00 committed by GitHub
commit 047a581220
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 101 additions and 5 deletions

View File

@ -41,6 +41,7 @@ import type { FeatureMenusRef } from './components/FeatureMenus'
import FeatureMenus from './components/FeatureMenus' import FeatureMenus from './components/FeatureMenus'
import Footer from './components/Footer' import Footer from './components/Footer'
import InputBar from './components/InputBar' import InputBar from './components/InputBar'
import PastedFilesPreview from './components/PastedFilesPreview'
const logger = loggerService.withContext('HomeWindow') const logger = loggerService.withContext('HomeWindow')
@ -87,6 +88,8 @@ const HomeWindow: FC<{ draggable?: boolean }> = ({ draggable = true }) => {
return userInputText.trim() return userInputText.trim()
}, [isFirstMessage, referenceText, userInputText]) }, [isFirstMessage, referenceText, userInputText])
const hasChatInput = useMemo(() => Boolean(userContent) || files.length > 0, [files.length, userContent])
useEffect(() => { useEffect(() => {
i18n.changeLanguage(language || navigator.language || defaultLanguage) i18n.changeLanguage(language || navigator.language || defaultLanguage)
}, [language]) }, [language])
@ -171,7 +174,7 @@ const HomeWindow: FC<{ draggable?: boolean }> = ({ draggable = true }) => {
if (isLoading) return if (isLoading) return
e.preventDefault() e.preventDefault()
if (userContent) { if (userContent || files.length > 0) {
if (route === 'home') { if (route === 'home') {
featureMenusRef.current?.useFeature() featureMenusRef.current?.useFeature()
} else { } else {
@ -242,7 +245,7 @@ const HomeWindow: FC<{ draggable?: boolean }> = ({ draggable = true }) => {
const handleSendMessage = useCallback( const handleSendMessage = useCallback(
async (prompt?: string) => { async (prompt?: string) => {
if (isEmpty(userContent) || !currentTopic.current) { if ((isEmpty(userContent) && files.length === 0) || !currentTopic.current) {
return return
} }
@ -251,8 +254,10 @@ const HomeWindow: FC<{ draggable?: boolean }> = ({ draggable = true }) => {
const uploadedFiles = files.length ? await FileManager.uploadFiles(files) : [] const uploadedFiles = files.length ? await FileManager.uploadFiles(files) : []
const content = [prompt, userContent].filter(Boolean).join('\n\n') || undefined
const { message: userMessage, blocks } = getUserMessage({ const { message: userMessage, blocks } = getUserMessage({
content: [prompt, userContent].filter(Boolean).join('\n\n'), content,
assistant: currentAssistant, assistant: currentAssistant,
topic: currentTopic.current, topic: currentTopic.current,
files: uploadedFiles files: uploadedFiles
@ -481,6 +486,10 @@ const HomeWindow: FC<{ draggable?: boolean }> = ({ draggable = true }) => {
[files, userContent, currentAssistant] [files, userContent, currentAssistant]
) )
const handleRemoveFile = useCallback((filePath: string) => {
setFiles((prevFiles) => prevFiles.filter((file) => file.path !== filePath))
}, [])
const handlePause = useCallback(() => { const handlePause = useCallback(() => {
if (currentAskId.current) { if (currentAskId.current) {
abortCompletion(currentAskId.current) abortCompletion(currentAskId.current)
@ -575,6 +584,7 @@ const HomeWindow: FC<{ draggable?: boolean }> = ({ draggable = true }) => {
handlePaste={handlePaste} handlePaste={handlePaste}
ref={inputBarRef} ref={inputBarRef}
/> />
<PastedFilesPreview files={files} onRemove={handleRemoveFile} />
<Divider style={{ margin: '10px 0' }} /> <Divider style={{ margin: '10px 0' }} />
</> </>
)} )}
@ -620,6 +630,7 @@ const HomeWindow: FC<{ draggable?: boolean }> = ({ draggable = true }) => {
handlePaste={handlePaste} handlePaste={handlePaste}
ref={inputBarRef} ref={inputBarRef}
/> />
<PastedFilesPreview files={files} onRemove={handleRemoveFile} />
<Divider style={{ margin: '10px 0' }} /> <Divider style={{ margin: '10px 0' }} />
<ClipboardPreview referenceText={referenceText} clearClipboard={clearClipboard} t={t} /> <ClipboardPreview referenceText={referenceText} clearClipboard={clearClipboard} t={t} />
<Main> <Main>
@ -627,6 +638,7 @@ const HomeWindow: FC<{ draggable?: boolean }> = ({ draggable = true }) => {
setRoute={setRoute} setRoute={setRoute}
onSendMessage={handleSendMessage} onSendMessage={handleSendMessage}
text={userContent} text={userContent}
hasChatInput={hasChatInput}
ref={featureMenusRef} ref={featureMenusRef}
/> />
</Main> </Main>

View File

@ -11,6 +11,7 @@ interface FeatureMenusProps {
text: string text: string
setRoute: Dispatch<SetStateAction<'translate' | 'summary' | 'chat' | 'explanation' | 'home'>> setRoute: Dispatch<SetStateAction<'translate' | 'summary' | 'chat' | 'explanation' | 'home'>>
onSendMessage: (prompt?: string) => void onSendMessage: (prompt?: string) => void
hasChatInput: boolean
} }
export interface FeatureMenusRef { export interface FeatureMenusRef {
@ -23,6 +24,7 @@ export interface FeatureMenusRef {
const FeatureMenus = ({ const FeatureMenus = ({
ref, ref,
text, text,
hasChatInput,
setRoute, setRoute,
onSendMessage onSendMessage
}: FeatureMenusProps & { ref?: React.RefObject<FeatureMenusRef | null> }) => { }: FeatureMenusProps & { ref?: React.RefObject<FeatureMenusRef | null> }) => {
@ -36,7 +38,7 @@ const FeatureMenus = ({
title: t('miniwindow.feature.chat'), title: t('miniwindow.feature.chat'),
active: true, active: true,
onClick: () => { onClick: () => {
if (text) { if (hasChatInput) {
setRoute('chat') setRoute('chat')
onSendMessage() onSendMessage()
} }
@ -68,7 +70,7 @@ const FeatureMenus = ({
} }
} }
], ],
[onSendMessage, setRoute, t, text] [hasChatInput, onSendMessage, setRoute, t, text]
) )
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({

View File

@ -0,0 +1,82 @@
import { CloseOutlined, FileImageOutlined, FileOutlined } from '@ant-design/icons'
import type { FileMetadata } from '@renderer/types'
import { FileTypes } from '@renderer/types'
import { Tooltip } from 'antd'
import type { FC } from 'react'
import styled from 'styled-components'
interface PastedFilesPreviewProps {
files: FileMetadata[]
onRemove: (filePath: string) => void
}
const PastedFilesPreview: FC<PastedFilesPreviewProps> = ({ files, onRemove }) => {
if (!files.length) return null
return (
<Container>
{files.map((file) => (
<FileChip key={file.path} className="nodrag">
<IconWrapper>{file.type === FileTypes.IMAGE ? <FileImageOutlined /> : <FileOutlined />}</IconWrapper>
<Tooltip title={file.name} placement="topLeft">
<FileName>{file.name}</FileName>
</Tooltip>
<RemoveButton onClick={() => onRemove(file.path)}>
<CloseOutlined />
</RemoveButton>
</FileChip>
))}
</Container>
)
}
const Container = styled.div`
display: flex;
flex-wrap: wrap;
gap: 8px;
margin: 8px 0 2px;
`
const FileChip = styled.div`
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
border-radius: 8px;
background: var(--color-background-opacity);
border: 1px solid var(--color-border);
color: var(--color-text);
max-width: 100%;
`
const IconWrapper = styled.span`
display: inline-flex;
align-items: center;
justify-content: center;
color: var(--color-text-secondary);
`
const FileName = styled.span`
font-size: 12px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 180px;
`
const RemoveButton = styled.button`
display: inline-flex;
align-items: center;
justify-content: center;
background: none;
border: none;
color: var(--color-text-secondary);
cursor: pointer;
padding: 2px;
&:hover {
color: var(--color-text);
}
`
export default PastedFilesPreview