Merge pull request #5044 from menloresearch/chore/chat-input-inline-message
chore: add inline message chat input
This commit is contained in:
commit
e4168a4c17
@ -16,6 +16,7 @@ import {
|
|||||||
IconCodeCircle2,
|
IconCodeCircle2,
|
||||||
IconPlayerStopFilled,
|
IconPlayerStopFilled,
|
||||||
IconBrandSpeedtest,
|
IconBrandSpeedtest,
|
||||||
|
IconX,
|
||||||
} from '@tabler/icons-react'
|
} from '@tabler/icons-react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useGeneralSetting } from '@/hooks/useGeneralSetting'
|
import { useGeneralSetting } from '@/hooks/useGeneralSetting'
|
||||||
@ -56,8 +57,16 @@ const ChatInput = ({
|
|||||||
|
|
||||||
const { selectedModel } = useModelProvider()
|
const { selectedModel } = useModelProvider()
|
||||||
const { sendMessage } = useChat()
|
const { sendMessage } = useChat()
|
||||||
|
const [message, setMessage] = useState('')
|
||||||
|
|
||||||
console.log(model)
|
const handleSendMesage = (prompt: string) => {
|
||||||
|
if (!selectedModel) {
|
||||||
|
setMessage('Please select a model to start chatting.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setMessage('')
|
||||||
|
sendMessage(prompt)
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleFocusIn = () => {
|
const handleFocusIn = () => {
|
||||||
@ -130,154 +139,173 @@ const ChatInput = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div
|
<div className="relative">
|
||||||
className={cn(
|
|
||||||
'relative overflow-hidden p-[2px] rounded-lg',
|
|
||||||
Boolean(streamingContent) && 'opacity-70'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{streamingContent && (
|
|
||||||
<div className="absolute inset-0">
|
|
||||||
<MovingBorder rx="10%" ry="10%">
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'h-100 w-100 bg-[radial-gradient(var(--app-primary),transparent_60%)]'
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</MovingBorder>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative z-20 px-0 pb-10 border border-main-view-fg/5 rounded-lg text-main-view-fg bg-main-view',
|
'relative overflow-hidden p-[2px] rounded-lg',
|
||||||
isFocused && 'ring-1 ring-main-view-fg/10'
|
Boolean(streamingContent) && 'opacity-70'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<TextareaAutosize
|
{streamingContent && (
|
||||||
ref={textareaRef}
|
<div className="absolute inset-0">
|
||||||
disabled={Boolean(streamingContent)}
|
<MovingBorder rx="10%" ry="10%">
|
||||||
minRows={2}
|
<div
|
||||||
rows={1}
|
className={cn(
|
||||||
maxRows={10}
|
'h-100 w-100 bg-[radial-gradient(var(--app-primary),transparent_60%)]'
|
||||||
value={prompt}
|
)}
|
||||||
onChange={(e) => {
|
/>
|
||||||
setPrompt(e.target.value)
|
</MovingBorder>
|
||||||
// Count the number of newlines to estimate rows
|
</div>
|
||||||
const newRows = (e.target.value.match(/\n/g) || []).length + 1
|
)}
|
||||||
setRows(Math.min(newRows, maxRows))
|
<div
|
||||||
}}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === 'Enter' && !e.shiftKey && prompt) {
|
|
||||||
e.preventDefault()
|
|
||||||
// Submit the message when Enter is pressed without Shift
|
|
||||||
sendMessage(prompt)
|
|
||||||
// When Shift+Enter is pressed, a new line is added (default behavior)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
placeholder={t('common.placeholder.chatInput')}
|
|
||||||
autoFocus
|
|
||||||
spellCheck={spellCheckChatInput}
|
|
||||||
data-gramm={spellCheckChatInput}
|
|
||||||
data-gramm_editor={spellCheckChatInput}
|
|
||||||
data-gramm_grammarly={spellCheckChatInput}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
'bg-transparent pt-4 w-full flex-shrink-0 border-none resize-none outline-0 px-4',
|
'relative z-20 px-0 pb-10 border border-main-view-fg/5 rounded-lg text-main-view-fg bg-main-view',
|
||||||
rows < maxRows && 'scrollbar-hide',
|
isFocused && 'ring-1 ring-main-view-fg/10'
|
||||||
className
|
|
||||||
)}
|
)}
|
||||||
/>
|
>
|
||||||
</div>
|
<TextareaAutosize
|
||||||
</div>
|
ref={textareaRef}
|
||||||
<div className="absolute z-20 bg-transparent bottom-0 w-full p-2 ">
|
disabled={Boolean(streamingContent)}
|
||||||
<div className="flex justify-between items-center w-full">
|
minRows={2}
|
||||||
<div className="px-1 flex items-center gap-1">
|
rows={1}
|
||||||
<div
|
maxRows={10}
|
||||||
|
value={prompt}
|
||||||
|
onChange={(e) => {
|
||||||
|
setPrompt(e.target.value)
|
||||||
|
// Count the number of newlines to estimate rows
|
||||||
|
const newRows = (e.target.value.match(/\n/g) || []).length + 1
|
||||||
|
setRows(Math.min(newRows, maxRows))
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey && prompt) {
|
||||||
|
e.preventDefault()
|
||||||
|
// Submit the message when Enter is pressed without Shift
|
||||||
|
handleSendMesage(prompt)
|
||||||
|
// When Shift+Enter is pressed, a new line is added (default behavior)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder={t('common.placeholder.chatInput')}
|
||||||
|
autoFocus
|
||||||
|
spellCheck={spellCheckChatInput}
|
||||||
|
data-gramm={spellCheckChatInput}
|
||||||
|
data-gramm_editor={spellCheckChatInput}
|
||||||
|
data-gramm_grammarly={spellCheckChatInput}
|
||||||
className={cn(
|
className={cn(
|
||||||
'px-1 flex items-center gap-1',
|
'bg-transparent pt-4 w-full flex-shrink-0 border-none resize-none outline-0 px-4',
|
||||||
streamingContent && 'opacity-50 pointer-events-none'
|
rows < maxRows && 'scrollbar-hide',
|
||||||
)}
|
className
|
||||||
>
|
|
||||||
{model?.provider === 'llama.cpp' && loadingModel ? (
|
|
||||||
<ModelLoader />
|
|
||||||
) : (
|
|
||||||
<DropdownModelProvider model={model} />
|
|
||||||
)}
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* File attachment - always available */}
|
<div className="absolute z-20 bg-transparent bottom-0 w-full p-2 ">
|
||||||
<div className="h-6 p-1 flex items-center justify-center rounded-sm hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out gap-1">
|
<div className="flex justify-between items-center w-full">
|
||||||
<IconPaperclip size={18} className="text-main-view-fg/50" />
|
<div className="px-1 flex items-center gap-1">
|
||||||
</div>
|
<div
|
||||||
|
className={cn(
|
||||||
|
'px-1 flex items-center gap-1',
|
||||||
|
streamingContent && 'opacity-50 pointer-events-none'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{model?.provider === 'llama.cpp' && loadingModel ? (
|
||||||
|
<ModelLoader />
|
||||||
|
) : (
|
||||||
|
<DropdownModelProvider model={model} />
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Microphone - always available - Temp Hide */}
|
{/* File attachment - always available */}
|
||||||
{/* <div className="h-6 p-1 flex items-center justify-center rounded-sm hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out gap-1">
|
<div className="h-6 p-1 flex items-center justify-center rounded-sm hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out gap-1">
|
||||||
|
<IconPaperclip size={18} className="text-main-view-fg/50" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Microphone - always available - Temp Hide */}
|
||||||
|
{/* <div className="h-6 p-1 flex items-center justify-center rounded-sm hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out gap-1">
|
||||||
<IconMicrophone size={18} className="text-main-view-fg/50" />
|
<IconMicrophone size={18} className="text-main-view-fg/50" />
|
||||||
</div> */}
|
</div> */}
|
||||||
|
|
||||||
{selectedModel?.capabilities?.includes('vision') && (
|
{selectedModel?.capabilities?.includes('vision') && (
|
||||||
<div className="h-6 p-1 flex items-center justify-center rounded-sm hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out gap-1">
|
<div className="h-6 p-1 flex items-center justify-center rounded-sm hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out gap-1">
|
||||||
<IconEye size={18} className="text-main-view-fg/50" />
|
<IconEye size={18} className="text-main-view-fg/50" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{selectedModel?.capabilities?.includes('embeddings') && (
|
{selectedModel?.capabilities?.includes('embeddings') && (
|
||||||
<div className="h-6 p-1 flex items-center justify-center rounded-sm hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out gap-1">
|
<div className="h-6 p-1 flex items-center justify-center rounded-sm hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out gap-1">
|
||||||
<IconCodeCircle2 size={18} className="text-main-view-fg/50" />
|
<IconCodeCircle2
|
||||||
</div>
|
size={18}
|
||||||
)}
|
className="text-main-view-fg/50"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{selectedModel?.capabilities?.includes('tools') && (
|
{selectedModel?.capabilities?.includes('tools') && (
|
||||||
<div className="h-6 p-1 flex items-center justify-center rounded-sm hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out gap-1">
|
<div className="h-6 p-1 flex items-center justify-center rounded-sm hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out gap-1">
|
||||||
<IconTool size={18} className="text-main-view-fg/50" />
|
<IconTool size={18} className="text-main-view-fg/50" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{selectedModel?.capabilities?.includes('web_search') && (
|
{selectedModel?.capabilities?.includes('web_search') && (
|
||||||
<div className="h-6 p-1 flex items-center justify-center rounded-sm hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out gap-1">
|
<div className="h-6 p-1 flex items-center justify-center rounded-sm hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out gap-1">
|
||||||
<IconWorld size={18} className="text-main-view-fg/50" />
|
<IconWorld size={18} className="text-main-view-fg/50" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{selectedModel?.capabilities?.includes('reasoning') && (
|
{selectedModel?.capabilities?.includes('reasoning') && (
|
||||||
<div className="h-6 p-1 flex items-center justify-center rounded-sm hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out gap-1">
|
<div className="h-6 p-1 flex items-center justify-center rounded-sm hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out gap-1">
|
||||||
<IconAtom size={18} className="text-main-view-fg/50" />
|
<IconAtom size={18} className="text-main-view-fg/50" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showSpeedToken && (
|
||||||
|
<div className="flex items-center gap-1 text-main-view-fg/60 text-xs">
|
||||||
|
<IconBrandSpeedtest size={18} />
|
||||||
|
<span>
|
||||||
|
{Math.round(tokenSpeed?.tokenSpeed ?? 0)} tokens/sec
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showSpeedToken && (
|
{streamingContent ? (
|
||||||
<div className="flex items-center gap-1 text-main-view-fg/60 text-xs">
|
<Button
|
||||||
<IconBrandSpeedtest size={18} />
|
variant="destructive"
|
||||||
<span>
|
size="icon"
|
||||||
{Math.round(tokenSpeed?.tokenSpeed ?? 0)} tokens/sec
|
onClick={() => stopStreaming(streamingContent.thread_id)}
|
||||||
</span>
|
>
|
||||||
</div>
|
<IconPlayerStopFilled />
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant={!prompt ? null : 'default'}
|
||||||
|
size="icon"
|
||||||
|
disabled={!prompt}
|
||||||
|
onClick={() => handleSendMesage(prompt)}
|
||||||
|
>
|
||||||
|
{streamingContent ? (
|
||||||
|
<span className="animate-spin h-4 w-4 border-2 border-current border-t-transparent rounded-full" />
|
||||||
|
) : (
|
||||||
|
<ArrowRight className="text-primary-fg" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{streamingContent ? (
|
|
||||||
<Button
|
|
||||||
variant="destructive"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => stopStreaming(streamingContent.thread_id)}
|
|
||||||
>
|
|
||||||
<IconPlayerStopFilled />
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
variant={!prompt ? null : 'default'}
|
|
||||||
size="icon"
|
|
||||||
disabled={!prompt}
|
|
||||||
onClick={() => sendMessage(prompt)}
|
|
||||||
>
|
|
||||||
{streamingContent ? (
|
|
||||||
<span className="animate-spin h-4 w-4 border-2 border-current border-t-transparent rounded-full" />
|
|
||||||
) : (
|
|
||||||
<ArrowRight className="text-primary-fg" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{message && !selectedModel && (
|
||||||
|
<div className="bg-main-view-fg/2 -mt-0.5 mx-2 pb-2 px-3 pt-1.5 rounded-b-lg text-xs text-destructive transition-all duration-200 ease-in-out">
|
||||||
|
<div className="flex items-center gap-1 justify-between">
|
||||||
|
{message}
|
||||||
|
<IconX
|
||||||
|
className="size-3 text-main-view-fg/30 cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
setMessage('')
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user