chore: styling tool call funtion render UI

This commit is contained in:
Faisal Amir 2025-05-16 22:09:43 +07:00
parent 95f90f601d
commit 05ce85d9b1
4 changed files with 148 additions and 33 deletions

View File

@ -9,9 +9,10 @@ import {
IconPencil,
} from '@tabler/icons-react'
import { useAppState } from '@/hooks/useAppState'
import ThinkingBlock from './ThinkingBlock'
import { cn } from '@/lib/utils'
import { useMessages } from '@/hooks/useMessages'
import ThinkingBlock from '@/containers/ThinkingBlock'
import ToolCallBlock from '@/containers/ToolCallBlock'
const CopyButton = ({ text }: { text: string }) => {
const [copied, setCopied] = useState(false)
@ -81,6 +82,12 @@ export const ThreadContent = memo(
const { deleteMessage } = useMessages()
const isToolCalls =
item.metadata &&
'tool_calls' in item.metadata &&
Array.isArray(item.metadata.tool_calls) &&
item.metadata.tool_calls.length
return (
<Fragment>
{item.content?.[0]?.text && item.role === 'user' && (
@ -124,41 +131,59 @@ export const ThreadContent = memo(
text={reasoningSegment}
/>
)}
<RenderMarkdown content={textSegment} components={linkComponents} />
<div className="flex items-center gap-2 mt-2 text-main-view-fg/60 text-xs">
<div
className={cn(
'flex items-center gap-2',
item.isLastMessage &&
streamingContent &&
'opacity-0 visinility-hidden pointer-events-none'
)}
>
<CopyButton text={item.content?.[0]?.text.value || ''} />
<button
className="flex items-center gap-1 hover:text-accent transition-colors cursor-pointer group relative"
onClick={() => {
deleteMessage(item.thread_id, item.id)
}}
{isToolCalls && item.metadata?.tool_calls ? (
<>
{(item.metadata.tool_calls as ToolCall[]).map((toolCall) => (
<ToolCallBlock
id={toolCall.tool?.id ?? 0}
name={toolCall.tool?.function?.name ?? ''}
key={toolCall.tool?.id}
result={JSON.stringify(toolCall.response)}
loading={toolCall.state === 'pending'}
/>
))}
</>
) : null}
{!isToolCalls && (
<div className="flex items-center gap-2 mt-2 text-main-view-fg/60 text-xs">
<div
className={cn(
'flex items-center gap-2',
item.isLastMessage &&
streamingContent &&
'opacity-0 visinility-hidden pointer-events-none'
)}
>
<IconTrash size={16} />
<span className="opacity-0 w-0 overflow-hidden whitespace-nowrap group-hover:w-auto group-hover:opacity-100 transition-all duration-300 ease-in-out">
Delete
</span>
</button>
<button
className="flex items-center gap-1 hover:text-accent transition-colors cursor-pointer group relative"
onClick={() => {
console.log('Regenerate clicked')
}}
>
<IconRefresh size={16} />
<span className="opacity-0 w-0 overflow-hidden whitespace-nowrap group-hover:w-auto group-hover:opacity-100 transition-all duration-300 ease-in-out">
Regenerate
</span>
</button>
<CopyButton text={item.content?.[0]?.text.value || ''} />
<button
className="flex items-center gap-1 hover:text-accent transition-colors cursor-pointer group relative"
onClick={() => {
deleteMessage(item.thread_id, item.id)
}}
>
<IconTrash size={16} />
<span className="opacity-0 w-0 overflow-hidden whitespace-nowrap group-hover:w-auto group-hover:opacity-100 transition-all duration-300 ease-in-out">
Delete
</span>
</button>
<button
className="flex items-center gap-1 hover:text-accent transition-colors cursor-pointer group relative"
onClick={() => {
console.log('Regenerate clicked')
}}
>
<IconRefresh size={16} />
<span className="opacity-0 w-0 overflow-hidden whitespace-nowrap group-hover:w-auto group-hover:opacity-100 transition-all duration-300 ease-in-out">
Regenerate
</span>
</button>
</div>
</div>
</div>
)}
</>
)}
{item.type === 'image_url' && image && (

View File

@ -0,0 +1,79 @@
import { ChevronDown, ChevronUp, Loader } from 'lucide-react'
import { cn } from '@/lib/utils'
import { create } from 'zustand'
import { RenderMarkdown } from './RenderMarkdown'
interface Props {
result: string
name: string
id: number
loading: boolean
}
type ToolCallBlockState = {
collapseState: { [id: number]: boolean }
setCollapseState: (id: number, expanded: boolean) => void
}
const useToolCallBlockStore = create<ToolCallBlockState>((set) => ({
collapseState: {},
setCollapseState: (id, expanded) =>
set((state) => ({
collapseState: {
...state.collapseState,
[id]: expanded,
},
})),
}))
const ToolCallBlock = ({ id, name, result, loading }: Props) => {
const { collapseState, setCollapseState } = useToolCallBlockStore()
const isExpanded = collapseState[id] ?? false
const handleClick = () => {
const newExpandedState = !isExpanded
setCollapseState(id, newExpandedState)
}
return (
<div className="mx-auto w-full cursor-pointer mt-4" onClick={handleClick}>
<div className="mb-4 rounded-lg bg-main-view-fg/4 border border-dashed border-main-view-fg/10">
<div className="flex items-center gap-3 p-2">
{loading && (
<Loader className="size-4 animate-spin text-main-view-fg/60" />
)}
<button className="flex items-center gap-2 focus:outline-none">
{isExpanded ? (
<ChevronUp className="h-4 w-4" />
) : (
<ChevronDown className="h-4 w-4" />
)}
<span className="font-medium text-main-view-fg/80">
View result from{' '}
<span className="font-medium text-main-view-fg">{name}</span>
</span>
</button>
</div>
<div
className={cn(
'h-fit w-full overflow-auto transition-all duration-300 px-2',
isExpanded ? '' : 'max-h-0 overflow-hidden'
)}
>
<div className="mt-2 text-main-view-fg/60">
<RenderMarkdown
content={
'```json\n' +
JSON.stringify(result ? JSON.parse(result) : null, null, 2) +
'\n```'
}
/>
</div>
</div>
</div>
</div>
)
}
export default ToolCallBlock

View File

@ -177,6 +177,7 @@ function ThreadDetail() {
messages.map((item, index) => {
// Only pass isLastMessage to the last message in the array
const isLastMessage = index === messages.length - 1
console.log(messages, 'messages')
return (
<div
key={item.id}

10
web-app/src/types/message.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
type ToolCall = {
tool: {
id?: number
function?: {
name?: string
}
}
response?: unknown
state?: string
}