enhancement: tool call block should be wrapped in a collapsible scroll area
This commit is contained in:
parent
1630e2eb77
commit
a81b644a8f
@ -4,7 +4,7 @@ import { ThreadMessage } from '@janhq/core'
|
|||||||
import { ScrollArea } from '@janhq/joi'
|
import { ScrollArea } from '@janhq/joi'
|
||||||
import { useVirtualizer } from '@tanstack/react-virtual'
|
import { useVirtualizer } from '@tanstack/react-virtual'
|
||||||
|
|
||||||
import { useAtomValue } from 'jotai'
|
import { useAtomValue, useSetAtom } from 'jotai'
|
||||||
|
|
||||||
import { loadModelErrorAtom } from '@/hooks/useActiveModel'
|
import { loadModelErrorAtom } from '@/hooks/useActiveModel'
|
||||||
|
|
||||||
@ -12,6 +12,8 @@ import ChatItem from '../ChatItem'
|
|||||||
|
|
||||||
import LoadModelError from '../LoadModelError'
|
import LoadModelError from '../LoadModelError'
|
||||||
|
|
||||||
|
import { toolCallBlockStateAtom } from '../TextMessage/ToolCallBlock'
|
||||||
|
|
||||||
import EmptyThread from './EmptyThread'
|
import EmptyThread from './EmptyThread'
|
||||||
|
|
||||||
import { getCurrentChatMessagesAtom } from '@/helpers/atoms/ChatMessage.atom'
|
import { getCurrentChatMessagesAtom } from '@/helpers/atoms/ChatMessage.atom'
|
||||||
@ -65,9 +67,11 @@ const ChatBody = memo(
|
|||||||
const parentRef = useRef<HTMLDivElement>(null)
|
const parentRef = useRef<HTMLDivElement>(null)
|
||||||
const prevScrollTop = useRef(0)
|
const prevScrollTop = useRef(0)
|
||||||
const isUserManuallyScrollingUp = useRef(false)
|
const isUserManuallyScrollingUp = useRef(false)
|
||||||
|
const isNestedScrollviewExpanding = useRef(false)
|
||||||
const currentThread = useAtomValue(activeThreadAtom)
|
const currentThread = useAtomValue(activeThreadAtom)
|
||||||
const isBlockingSend = useAtomValue(isBlockingSendAtom)
|
const isBlockingSend = useAtomValue(isBlockingSendAtom)
|
||||||
const showScrollBar = useAtomValue(showScrollBarAtom)
|
const showScrollBar = useAtomValue(showScrollBarAtom)
|
||||||
|
const setToolCallExpanded = useSetAtom(toolCallBlockStateAtom)
|
||||||
|
|
||||||
const count = useMemo(
|
const count = useMemo(
|
||||||
() => (messages?.length ?? 0) + (loadModelError ? 1 : 0),
|
() => (messages?.length ?? 0) + (loadModelError ? 1 : 0),
|
||||||
@ -97,7 +101,10 @@ const ChatBody = memo(
|
|||||||
_,
|
_,
|
||||||
instance
|
instance
|
||||||
) => {
|
) => {
|
||||||
if (isUserManuallyScrollingUp.current === true && isBlockingSend)
|
if (
|
||||||
|
isNestedScrollviewExpanding ||
|
||||||
|
(isUserManuallyScrollingUp.current === true && isBlockingSend)
|
||||||
|
)
|
||||||
return false
|
return false
|
||||||
return (
|
return (
|
||||||
// item.start < (instance.scrollOffset ?? 0) &&
|
// item.start < (instance.scrollOffset ?? 0) &&
|
||||||
@ -130,6 +137,22 @@ const ChatBody = memo(
|
|||||||
[isBlockingSend]
|
[isBlockingSend]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const preserveScrollOnExpand = (callback: () => void) => {
|
||||||
|
isNestedScrollviewExpanding.current = true
|
||||||
|
const scrollEl = parentRef.current
|
||||||
|
const prevScrollTop = scrollEl?.scrollTop ?? 0
|
||||||
|
const prevScrollHeight = scrollEl?.scrollHeight ?? 0
|
||||||
|
|
||||||
|
callback() // Expand content (e.g. setIsExpanded(true))
|
||||||
|
|
||||||
|
if (scrollEl)
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
const newScrollHeight = scrollEl?.scrollHeight ?? 0
|
||||||
|
scrollEl.scrollTop =
|
||||||
|
prevScrollTop + (newScrollHeight - prevScrollHeight)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-col overflow-x-hidden">
|
<div className="flex h-full w-full flex-col overflow-x-hidden">
|
||||||
<ScrollArea
|
<ScrollArea
|
||||||
@ -179,6 +202,14 @@ const ChatBody = memo(
|
|||||||
isCurrentMessage={
|
isCurrentMessage={
|
||||||
virtualRow.index === messages?.length - 1
|
virtualRow.index === messages?.length - 1
|
||||||
}
|
}
|
||||||
|
onExpand={(props) =>
|
||||||
|
preserveScrollOnExpand(() => {
|
||||||
|
setToolCallExpanded((prev) => ({
|
||||||
|
...prev,
|
||||||
|
...props,
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -22,6 +22,7 @@ type Props = {
|
|||||||
loadModelError?: string
|
loadModelError?: string
|
||||||
isCurrentMessage?: boolean
|
isCurrentMessage?: boolean
|
||||||
index: number
|
index: number
|
||||||
|
onExpand: (props: { [id: number]: boolean }) => void
|
||||||
} & ThreadMessage
|
} & ThreadMessage
|
||||||
|
|
||||||
const ChatItem = forwardRef<Ref, Props>((message, ref) => {
|
const ChatItem = forwardRef<Ref, Props>((message, ref) => {
|
||||||
@ -81,6 +82,7 @@ const ChatItem = forwardRef<Ref, Props>((message, ref) => {
|
|||||||
status={status}
|
status={status}
|
||||||
index={message.index}
|
index={message.index}
|
||||||
isCurrentMessage={message.isCurrentMessage ?? false}
|
isCurrentMessage={message.isCurrentMessage ?? false}
|
||||||
|
onExpand={message.onExpand}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,8 +1,11 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
|
import { ScrollArea } from '@janhq/joi'
|
||||||
import { atom, useAtom } from 'jotai'
|
import { atom, useAtom } from 'jotai'
|
||||||
import { ChevronDown, ChevronUp, Loader } from 'lucide-react'
|
import { ChevronDown, ChevronUp, Loader } from 'lucide-react'
|
||||||
|
|
||||||
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
import { MarkdownTextMessage } from './MarkdownTextMessage'
|
import { MarkdownTextMessage } from './MarkdownTextMessage'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -10,16 +13,18 @@ interface Props {
|
|||||||
name: string
|
name: string
|
||||||
id: number
|
id: number
|
||||||
loading: boolean
|
loading: boolean
|
||||||
|
onExpand: (props: { [id: number]: boolean }) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const toolCallBlockStateAtom = atom<{ [id: number]: boolean }>({})
|
export const toolCallBlockStateAtom = atom<{ [id: number]: boolean }>({})
|
||||||
|
|
||||||
const ToolCallBlock = ({ id, name, result, loading }: Props) => {
|
const ToolCallBlock = ({ id, name, result, loading, onExpand }: Props) => {
|
||||||
const [collapseState, setCollapseState] = useAtom(toolCallBlockStateAtom)
|
const [collapseState, setCollapseState] = useAtom(toolCallBlockStateAtom)
|
||||||
|
|
||||||
const isExpanded = collapseState[id] ?? false
|
const isExpanded = collapseState[id] ?? false
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
setCollapseState((prev) => ({ ...prev, [id]: !isExpanded }))
|
onExpand({ [id]: !isExpanded })
|
||||||
|
// setCollapseState((prev) => ({ ...prev, [id]: !isExpanded }))
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto w-full">
|
<div className="mx-auto w-full">
|
||||||
@ -38,17 +43,21 @@ const ToolCallBlock = ({ id, name, result, loading }: Props) => {
|
|||||||
<ChevronDown className="h-4 w-4" />
|
<ChevronDown className="h-4 w-4" />
|
||||||
)}
|
)}
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
{' '}
|
|
||||||
View result from <span className="font-bold">{name}</span>
|
View result from <span className="font-bold">{name}</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isExpanded && (
|
<ScrollArea
|
||||||
|
className={twMerge(
|
||||||
|
'w-full overflow-hidden transition-all duration-300',
|
||||||
|
isExpanded ? 'max-h-96' : 'max-h-0'
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div className="mt-2 overflow-x-hidden pl-6 text-[hsla(var(--text-secondary))]">
|
<div className="mt-2 overflow-x-hidden pl-6 text-[hsla(var(--text-secondary))]">
|
||||||
<span>{result ?? ''} </span>
|
<span>{result ?? ''} </span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</ScrollArea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -30,7 +30,11 @@ import { selectedModelAtom } from '@/helpers/atoms/Model.atom'
|
|||||||
import { chatWidthAtom } from '@/helpers/atoms/Setting.atom'
|
import { chatWidthAtom } from '@/helpers/atoms/Setting.atom'
|
||||||
|
|
||||||
const MessageContainer: React.FC<
|
const MessageContainer: React.FC<
|
||||||
ThreadMessage & { isCurrentMessage: boolean; index: number }
|
ThreadMessage & {
|
||||||
|
isCurrentMessage: boolean
|
||||||
|
index: number
|
||||||
|
onExpand: (props: { [id: number]: boolean }) => void
|
||||||
|
}
|
||||||
> = (props) => {
|
> = (props) => {
|
||||||
const isUser = props.role === ChatCompletionRole.User
|
const isUser = props.role === ChatCompletionRole.User
|
||||||
const isSystem = props.role === ChatCompletionRole.System
|
const isSystem = props.role === ChatCompletionRole.System
|
||||||
@ -195,6 +199,7 @@ const MessageContainer: React.FC<
|
|||||||
<>
|
<>
|
||||||
{props.metadata.tool_calls.map((toolCall) => (
|
{props.metadata.tool_calls.map((toolCall) => (
|
||||||
<ToolCallBlock
|
<ToolCallBlock
|
||||||
|
onExpand={props.onExpand}
|
||||||
id={toolCall.tool?.id}
|
id={toolCall.tool?.id}
|
||||||
name={toolCall.tool?.function?.name ?? ''}
|
name={toolCall.tool?.function?.name ?? ''}
|
||||||
key={toolCall.tool?.id}
|
key={toolCall.tool?.id}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user