Files
2026-03-20 07:33:46 +00:00

134 lines
3.9 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useEffect, useState } from 'react'
import { Globe, Monitor } from 'lucide-react'
import { cn } from '@/utils/cn'
import { useNovaKit } from '../../context/useNovaKit'
import { ImagePreview } from '@/components/ui/image-preview'
export interface BrowserUseActionProps {
/** 显示名称action_name如"正在浏览" */
name?: string
/** 参数数组,第一个通常是 URL */
arguments?: string[]
/** 工具原始输出tool_output */
toolOutput?: unknown
className?: string
}
interface BrowserUseOutput {
result?: {
clean_screenshot_path?: string
screenshot_path?: string
url?: string
title?: string
}
}
/**
* browser_use 工具调用专属渲染组件
* 对应原始 task-computer-use.tsx去除了 Antd / UnoCSS / store 依赖
*/
function InnerBrowserUseAction({
name,
arguments: args,
toolOutput,
className,
}: BrowserUseActionProps) {
const { api } = useNovaKit()
const [screenshotUrl, setScreenshotUrl] = useState<string>()
const [loading, setLoading] = useState(false)
const url = args?.[0] || ''
// 提取为字符串原始值作为 effect 依赖,只有路径真正变化才重新请求
const screenshotPath =
(toolOutput as BrowserUseOutput)?.result?.clean_screenshot_path ||
(toolOutput as BrowserUseOutput)?.result?.screenshot_path
// 加载截图api 不作为依赖Context 每次渲染都会返回新引用,加入会反复触发)
useEffect(() => {
if (!screenshotPath) return
let cancelled = false
queueMicrotask(() => {
if (cancelled) return
setLoading(true)
})
api
.getArtifactUrl?.({ path: screenshotPath, file_name: '', file_type: 'png' })
.then(res => {
if (cancelled) return
if (res?.data) setScreenshotUrl(res.data)
})
.catch(() => {})
.finally(() => {
if (cancelled) return
setLoading(false)
})
return () => {
cancelled = true
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [screenshotPath])
return (
<div
className={cn(
'flex flex-col rounded-xl border border-gray-200 overflow-hidden bg-white w-full max-w-[480px] mb-4',
className,
)}
>
{/* 标题栏 */}
<div className="flex items-center gap-2 px-3 py-2 bg-gray-50 border-b border-gray-100">
<Globe className="w-4 h-4 text-muted-foreground shrink-0" />
<div className="flex flex-col min-w-0 flex-1">
<span className="text-sm font-medium leading-tight">
{name || '正在浏览'}
</span>
{url && (
<span className="text-xs text-muted-foreground truncate">
{url}
</span>
)}
</div>
</div>
{/* 内容区 */}
<div
className={cn(
'relative bg-white',
screenshotUrl || loading ? 'pt-[56.25%]' : 'py-8',
)}
>
{loading && (
<div className="absolute inset-0 flex items-center justify-center">
<div className="w-6 h-6 border-2 border-muted border-t-primary rounded-full animate-spin" />
</div>
)}
{screenshotUrl && !loading && (
<ImagePreview src={screenshotUrl} alt="browser screenshot">
<img
className="absolute inset-0 w-full h-full object-contain cursor-zoom-in"
src={screenshotUrl}
alt="browser screenshot"
/>
</ImagePreview>
)}
{!screenshotUrl && !loading && (
<div className="flex flex-col items-center justify-center gap-2 text-muted-foreground">
<Monitor className="w-8 h-8 opacity-40" />
<span className="text-xs"></span>
</div>
)}
</div>
</div>
)
}
export const BrowserUseAction = React.memo(InnerBrowserUseAction)
export default BrowserUseAction