初始化模版工程

This commit is contained in:
Cloud Bot
2026-03-20 07:33:46 +00:00
commit 23717e0ecd
386 changed files with 51675 additions and 0 deletions

View File

@@ -0,0 +1,133 @@
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