初始化模版工程

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,211 @@
import {
Clock3,
Globe,
Search,
Sparkles,
} from 'lucide-react'
import { ScrollArea } from '@/components/ui/scroll-area'
import { cn } from '@/utils/cn'
export interface WebSearchItem {
url?: string
title?: string
snippet?: string
date?: string
position?: number
[key: string]: unknown
}
export interface WebSearchPreviewProps {
/** 搜索结果列表(通常来自 info_search_web 的 tool_output */
results?: unknown
/** 查询词,优先来自 tool_input.arguments[0] */
searchQuery?: string | null
className?: string
}
function normalizeResults(results: unknown): WebSearchItem[] {
if (!Array.isArray(results)) return []
return results
.filter(item => item && typeof item === 'object')
.map(item => item as WebSearchItem)
.filter(item => typeof item.url === 'string' && !!item.url)
}
function getHostText(url?: string): string {
if (!url) return 'Unknown source'
try {
return new URL(url).hostname.replace(/^www\./, '')
} catch {
return 'Unknown source'
}
}
function getFaviconUrl(url?: string): string | null {
if (!url) return null
try {
const host = new URL(url).hostname
return `https://www.google.com/s2/favicons?domain=${host}&sz=64`
} catch {
return null
}
}
function formatDateLabel(date?: string): string {
if (!date) return ''
const normalized = date.trim()
if (!normalized) return ''
// 常见相对时间(如 "3 days ago")直接保留
if (/\b(ago|yesterday|today)\b/i.test(normalized)) {
return normalized
}
const parsed = new Date(normalized)
if (Number.isNaN(parsed.getTime())) {
return normalized
}
return new Intl.DateTimeFormat('zh-CN', {
year: 'numeric',
month: 'short',
day: 'numeric',
}).format(parsed)
}
export function WebSearchPreview({ results, searchQuery, className }: WebSearchPreviewProps) {
const items = normalizeResults(results)
const queryText = searchQuery?.trim() || items[0]?.title || 'Search for premium insights...'
if (!items.length) {
return (
<div className={cn('flex-1 h-full flex items-center justify-center p-8 text-muted-foreground', className)}>
<div className="flex items-center gap-2 text-sm">
<Search className="h-4 w-4" />
</div>
</div>
)
}
return (
<div
className={cn(
'flex h-full flex-col bg-background pt-[56px] text-foreground',
className,
)}
>
<ScrollArea className="flex-1">
<div className="min-h-full">
<header className="sticky top-0 z-10 border-b border-border/80 bg-background/92 backdrop-blur-sm">
<div className="px-4 md:px-8 py-4">
<div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-4 flex-1">
<div className="flex shrink-0 items-center gap-2 text-primary">
<Sparkles className="h-6 w-6" />
<h2 className="hidden md:block text-lg font-bold tracking-tight">Search</h2>
</div>
<div className="relative group flex-1">
<Search className="absolute left-4 top-1/2 h-4 w-4 -translate-y-1/2 text-primary" />
<input
readOnly
value={queryText}
className="w-full rounded-full border-2 border-primary/30 bg-card/76 py-2.5 pl-11 pr-4 text-sm text-foreground outline-none backdrop-blur-sm placeholder:text-muted-foreground focus-visible:outline-none md:text-base"
/>
</div>
</div>
<div className="flex items-center gap-2">
<div className="flex h-9 w-9 items-center justify-center rounded-full border border-primary/20 bg-primary/10 text-xs font-semibold text-primary">
AI
</div>
</div>
</div>
</div>
</header>
<main className="px-4 md:px-8 py-4">
<div className="mb-6 flex items-center justify-between">
<p className="text-sm text-muted-foreground">
About {items.length.toLocaleString()} results
</p>
</div>
<div className="space-y-9">
{items.map((item, idx) => {
const favicon = getFaviconUrl(item.url)
const host = getHostText(item.url)
return (
<article key={`${item.url}-${idx}`} className="group max-w-3xl">
<div className="flex items-center gap-3 mb-2">
<div className="flex h-6 w-6 items-center justify-center overflow-hidden rounded-md border border-border/80 bg-card">
{favicon ? (
<img src={favicon} alt={`${host} favicon`} className="w-full h-full object-contain" />
) : (
<Globe className="h-3.5 w-3.5 text-muted-foreground" />
)}
</div>
<div className="flex flex-col min-w-0">
<span className="truncate text-xs font-medium text-foreground/78">
{host}
</span>
<span className="truncate text-[10px] text-muted-foreground">
{item.url}
</span>
</div>
</div>
<a
href={item.url}
target="_blank"
rel="noopener noreferrer"
className="mb-2 inline-block text-lg font-semibold leading-tight text-primary underline-offset-4 hover:underline md:text-xl wrap-break-word"
>
{item.title || item.url}
</a>
{item.snippet && (
<p className="line-clamp-3 text-sm leading-relaxed text-foreground/74 md:text-base">
{item.snippet}
</p>
)}
<div className="mt-2 flex items-center gap-3 text-xs text-muted-foreground">
{item.date && (
<span className="flex items-center gap-1">
<Clock3 className="h-3.5 w-3.5" />
{formatDateLabel(item.date)}
</span>
)}
{typeof item.position === 'number' && item.position <= 3 && (
<span className="inline-flex items-center text-[10px] font-semibold uppercase tracking-wider text-primary">
Top Result
</span>
)}
</div>
</article>
)
})}
</div>
</main>
<footer className="sticky bottom-0 mt-auto border-t border-border/80 bg-secondary/55 px-4 py-6 backdrop-blur-sm md:px-8">
<div className="flex flex-col md:flex-row justify-between items-center gap-5">
<div className="flex items-center gap-5 text-sm text-muted-foreground">
<span className="cursor-default transition-colors hover:text-foreground">Help Center</span>
<span className="cursor-default transition-colors hover:text-foreground">Privacy</span>
<span className="cursor-default transition-colors hover:text-foreground">Terms</span>
</div>
<div className="text-xs text-muted-foreground">
Powered by Web Search Tool
</div>
</div>
</footer>
</div>
</ScrollArea>
</div>
)
}
export default WebSearchPreview