diff --git a/apps/shinkai-visor/src/components/inbox-input/inbox-input.tsx b/apps/shinkai-visor/src/components/inbox-input/inbox-input.tsx index ad706eda9..757956fd9 100644 --- a/apps/shinkai-visor/src/components/inbox-input/inbox-input.tsx +++ b/apps/shinkai-visor/src/components/inbox-input/inbox-input.tsx @@ -1,10 +1,11 @@ import { zodResolver } from '@hookform/resolvers/zod'; -import { Loader2, Send } from 'lucide-react'; +import { Send } from 'lucide-react'; import { useForm } from 'react-hook-form'; import { useIntl } from 'react-intl'; import { z } from 'zod'; import { Button } from '../ui/button'; +import DotsLoader from '../ui/dots-loader'; import { Form, FormControl, @@ -15,7 +16,7 @@ import { import { Input } from '../ui/input'; const formSchema = z.object({ - message: z.string().nonempty(), + message: z.string().min(1), }); type InboxInputFieldType = z.infer<typeof formSchema>; @@ -60,7 +61,6 @@ export const InboxInput = (props: InboxInputProps) => { <FormItem> <FormControl> <Input - disabled={props.loading} placeholder={intl.formatMessage({ id: 'tmwtd', })} @@ -75,12 +75,12 @@ export const InboxInput = (props: InboxInputProps) => { </div> <Button className="grow-0" - disabled={!form.formState.isValid || props.disabled} + disabled={!form.formState.isValid || props.disabled || props.loading} > {props.loading ? ( - <Loader2 className="h-4 w-4 animate-spin" /> + <DotsLoader className="w-6 h-4"></DotsLoader> ) : ( - <Send className="w-4 h-4" /> + <Send className="w-6 h-4" /> )} </Button> </form> diff --git a/apps/shinkai-visor/src/components/inbox/inbox.tsx b/apps/shinkai-visor/src/components/inbox/inbox.tsx index 2bd597484..006312c2e 100644 --- a/apps/shinkai-visor/src/components/inbox/inbox.tsx +++ b/apps/shinkai-visor/src/components/inbox/inbox.tsx @@ -57,6 +57,7 @@ export const Inbox = () => { const fromPreviousMessagesRef = useRef<boolean>(false); const [decodedInboxId, setDecodedInboxId] = useState<string>(''); const [isJobInbox, setIsJobInbox] = useState<boolean>(false); + const [isJobProcessing, setIsJobProcessing] = useState<boolean>(false); const fetchPreviousMessages = useCallback(async () => { const firstMessage = data?.pages?.[0]?.[0]; fromPreviousMessagesRef.current = true; @@ -78,6 +79,10 @@ export const Inbox = () => { chatContainerElement.scrollTop = currentHeight - previousHeight; } }, [fetchPreviousMessages, hasPreviousPage]); + const scrollToBottom = () => { + if (!chatContainerRef.current) return; + chatContainerRef.current.scrollTop = chatContainerRef.current.scrollHeight; + }; useEffect(() => { const chatContainerElement = chatContainerRef.current; if (!chatContainerElement) return; @@ -96,16 +101,23 @@ export const Inbox = () => { setIsJobInbox(checkIsJobInbox(decodedInboxId)); } }, [decodedInboxId]); - const scrollToBottom = () => { - if (!chatContainerRef.current) return; - chatContainerRef.current.scrollTop = chatContainerRef.current.scrollHeight; - }; useEffect(() => { if (!fromPreviousMessagesRef.current) { scrollToBottom(); } }, [data?.pages]); - + useEffect(() => { + const [firstMessagePage] = data?.pages || []; + const lastMessage = ([...firstMessagePage || []]).pop(); + if (lastMessage) { + const isLocal = isLocalMessage( + lastMessage, + auth?.shinkai_identity ?? '', + auth?.profile ?? '' + ); + setIsJobProcessing(isJobInbox && (isSendingMessage || isLocal)); + } + }, [data?.pages, auth, isSendingMessage, isJobInbox]); const submitSendMessage = (value: string) => { if (!auth) return; fromPreviousMessagesRef.current = false; @@ -266,7 +278,7 @@ export const Inbox = () => { </ScrollArea> <InboxInput disabled={isSendingMessage} - loading={isSendingMessage} + loading={isJobProcessing} onSubmit={(value) => submitSendMessage(value)} ></InboxInput> </div> diff --git a/apps/shinkai-visor/src/components/ui/dots-loader.tsx b/apps/shinkai-visor/src/components/ui/dots-loader.tsx new file mode 100644 index 000000000..c3f9b07bf --- /dev/null +++ b/apps/shinkai-visor/src/components/ui/dots-loader.tsx @@ -0,0 +1,13 @@ +const DotsLoader = ({ className }: { className?: string }) => { + return ( + <div className={`${className} flex flex-col justify-center`}> + <div className="flex flex-row space-x-1 items-center justify-center"> + <div className="h-1 w-1 rounded-full bg-slate-100 animate-bounce [animation-delay:-0.3s]"></div> + <div className="h-1 w-1 rounded-full bg-slate-100 animate-bounce [animation-delay:-0.15s]"></div> + <div className="h-1 w-1 rounded-full bg-slate-100 animate-bounce"></div> + </div> + </div> + ); +}; + +export default DotsLoader; diff --git a/apps/shinkai-visor/tailwind.config.js b/apps/shinkai-visor/tailwind.config.js index 205677ca1..d0e183145 100644 --- a/apps/shinkai-visor/tailwind.config.js +++ b/apps/shinkai-visor/tailwind.config.js @@ -88,12 +88,10 @@ module.exports = { from: { height: 'var(--radix-accordion-content-height)' }, to: { height: 0 }, }, - keyframes: { - breath: { - '0%, 100%': { transform: 'opacity: 1' }, - '50%': { transform: 'opacity: .5' }, - } - } + breath: { + '0%, 100%': { transform: 'opacity: 1' }, + '50%': { transform: 'opacity: .5' }, + }, }, animation: { 'accordion-down': 'accordion-down 0.2s ease-out',