Weather Search
Self-contained weather search component with integrated results display. Perfect for embedding Infactory-powered weather queries into any React application.
6b107ccf-44b1-42cc-a03a-8967d9448d95"use client"
import React from "react"
import { Suggestion, Suggestions } from "@/components/ui/suggestion"
import { WeatherSearch } from "@/components/ui/weather-search"
// Read from environment variables
const INFACTORY_CONFIG = {
projectId: process.env.NEXT_PUBLIC_INFACTORY_PROJECT_ID || "",
apiKey: process.env.NEXT_PUBLIC_INFACTORY_API_KEY || "",
apiUrl:
process.env.NEXT_PUBLIC_INFACTORY_API_URL || "https://api.infactory.ai",
}
const exampleQueries = [
"What's the weather for 43130",
"Naples fl weather",
"Orlando weather forecast",
"Weather forecast for topsail nc",
"What is the wind speed today",
"Avondale weather tomorrow",
"Are we gonna have rain this week",
"Give me the weather for next week in las vegas",
]
export function WeatherSearchDemo() {
const [query, setQuery] = React.useState("")
const isConfigured = INFACTORY_CONFIG.projectId && INFACTORY_CONFIG.apiKey
const handleResults = React.useCallback((results: string[]) => {
console.log("Weather results received:", results)
}, [])
const handleError = React.useCallback((error: string) => {
console.error("Weather search error:", error)
}, [])
const handleSuggestionClick = React.useCallback((suggestion: string) => {
setQuery(suggestion)
}, [])
return (
<div className="w-full max-w-2xl space-y-4">
{/* Configuration Status */}
{!isConfigured ? (
<div className="rounded-lg border border-amber-200 bg-amber-50/50 p-4 text-sm">
<div className="font-medium text-amber-900">
Configuration Required
</div>
<div className="mt-2 text-amber-700">
Set these environment variables to connect:
</div>
<div className="mt-2 rounded bg-amber-100 p-2 font-mono text-xs text-amber-800">
NEXT_PUBLIC_INFACTORY_PROJECT_ID=your-project-id
<br />
NEXT_PUBLIC_INFACTORY_API_KEY=your-api-key
<br />
NEXT_PUBLIC_INFACTORY_API_URL=https://api.infactory.ai
</div>
</div>
) : (
<div className="rounded-lg border border-green-200 bg-green-50/50 p-4 text-sm">
<div className="font-medium text-green-900">
✓ Connected to Infactory
</div>
<div className="text-green-700">
Project:{" "}
<code className="text-xs">{INFACTORY_CONFIG.projectId}</code>
</div>
</div>
)}
{/* Example Queries as Suggestions */}
<div className="space-y-2">
<div className="text-sm font-medium">Try these queries:</div>
<Suggestions>
{exampleQueries.map((suggestion) => (
<Suggestion
key={suggestion}
onClick={handleSuggestionClick}
suggestion={suggestion}
/>
))}
</Suggestions>
</div>
{/* Weather Search Component */}
<WeatherSearch
projectId={INFACTORY_CONFIG.projectId}
apiKey={INFACTORY_CONFIG.apiKey}
apiUrl={INFACTORY_CONFIG.apiUrl}
program="weather_flow"
placeholder="Ask about weather..."
value={query}
onValueChange={setQuery}
onResults={handleResults}
onError={handleError}
/>
</div>
)
}
Installation
pnpm dlx shadcn@latest add https://ui.infactory.ai//r/weather-search.json
This will install the weather search component, input-group, Infactory client, and all dependencies.
Folder structure
"use client"import * as React from "react"import { Loader2, Search } from "lucide-react"import { FacetState } from "@/lib/facets/base/base-facet"import { InfiniteWeatherRendererFacet } from "@/lib/facets/implementations/renderer/infinite_weather_renderer_facet"import { WeatherCalculationRendererFacet } from "@/lib/facets/implementations/renderer/weather_calculation_renderer_facet"import { WeatherCardRendererFacet } from "@/lib/facets/implementations/renderer/weather_card_renderer_facet"import type { DataFrameItem, Item, ScalarItem, StringItem, StructuredData,} from "@/lib/facets/structured-data"import type { RendererFacet } from "@/lib/facets/types"import { InfactoryClient, type ExecutionResult,} from "@/lib/infactory/client"import { StructuredDataAccumulator } from "@/lib/structured-data"import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput,} from "@/components/ui/input-group"import { ShineBorder } from "@/components/ui/shine-border"import { cn } from "../lib/utils"export interface WeatherSearchProps { /** Infactory project ID */ projectId: string /** Infactory API key */ apiKey: string /** Optional API URL (defaults to https://api.infactory.ai) */ apiUrl?: string /** Optional query program to execute (defaults to a weather query program) */ program?: string /** Placeholder text for the input */ placeholder?: string /** Optional className for the container */ className?: string /** Optional controlled value for the search input */ value?: string /** Optional callback when the input value changes (for controlled mode) */ onValueChange?: (value: string) => void /** Optional callback when results are received */ onResults?: (results: string[]) => void /** Optional callback when an error occurs */ onError?: (error: string) => void /** Enable SSE streaming mode (default: true) */ streamingMode?: boolean /** Rendering mode: 'auto' uses facet renderers, 'simple' uses basic cards (default: 'auto') */ renderingMode?: "auto" | "simple"}interface WeatherResult { text: string title?: string description?: string}/** * Extract text results from StructuredData */function extractResults(data: StructuredData | null): WeatherResult[] { if (!data || !data.items) return [] const results: WeatherResult[] = [] // Iterate through all items Object.values(data.items).forEach((item: Item) => { if (item.item_t === "nf:item/string") { const stringItem = item.item as StringItem results.push({ text: stringItem.value, title: stringItem.title, description: stringItem.description, }) } else if (item.item_t === "nf:item/scalar") { const scalarItem = item.item as ScalarItem results.push({ text: scalarItem.value, title: scalarItem.title, description: scalarItem.description, }) } else if (item.item_t === "nf:item/dataframe") { const dataframeItem = item.item as DataFrameItem // For dataframes, we'll extract text from rows if (dataframeItem.rows && dataframeItem.rows.length > 0) { dataframeItem.rows.forEach((row) => { // Combine all column values into text const text = row.filter(Boolean).join(" | ") if (text) { results.push({ text, title: dataframeItem.title, description: dataframeItem.description, }) } }) } } else if (item.item_t === "nf:item/map") { // For maps, try to find text values const mapItem = item.item as any if (mapItem.entries) { Object.values(mapItem.entries).forEach((entry: any) => { if (entry.item_t === "nf:item/string") { results.push({ text: entry.item.value }) } else if (entry.item_t === "nf:item/scalar") { results.push({ text: entry.item.value }) } }) } } }) return results}// Helper to check if data is StructuredDatafunction isStructuredData(data: any): data is StructuredData { return ( data && typeof data === "object" && "purpose" in data && ("items" in data || "metadata" in data) )}export function WeatherSearch({ projectId, apiKey, apiUrl = "https://api.infactory.ai", program = "weather_flow", placeholder = "Ask about weather...", className, value: controlledValue, onValueChange, onResults, onError, streamingMode = true, renderingMode = "auto",}: WeatherSearchProps) { const [internalQuery, setInternalQuery] = React.useState("") const [results, setResults] = React.useState<WeatherResult[]>([]) const [isLoading, setIsLoading] = React.useState(false) const [error, setError] = React.useState<string | null>(null) // State for streaming and facet rendering const [accumulatedData, setAccumulatedData] = React.useState<StructuredData | null>(null) const [renderingHint, setRenderingHint] = React.useState<string | null>(null) const [isStreaming, setIsStreaming] = React.useState(false) const [isDataFlowComplete, setIsDataFlowComplete] = React.useState(false) const [dialogId, setDialogId] = React.useState<string | null>(null) const abortControllerRef = React.useRef<AbortController | null>(null) // Support both controlled and uncontrolled modes const isControlled = controlledValue !== undefined const query = isControlled ? controlledValue : internalQuery // Create client instance const client = React.useMemo( () => new InfactoryClient({ projectId, apiKey, apiUrl, }), [projectId, apiKey, apiUrl] ) // Create a dialog on mount React.useEffect(() => { const createDialog = async () => { try { const dialog = await client.createDialog({ name: "Weather Search", }) setDialogId(dialog.id) } catch (err) { console.error("Failed to create dialog:", err) } } createDialog() }, [client]) // Initialize facet renderers const facetRenderers = React.useMemo<RendererFacet[]>(() => { return [ new WeatherCalculationRendererFacet(), // Priority 85 - for calculated/aggregated data new WeatherCardRendererFacet(), // Default weather card new InfiniteWeatherRendererFacet(), // Infinite scroll timeline ] }, []) // Select the appropriate facet based on rendering hint const selectedFacet = React.useMemo(() => { console.log("[WeatherSearch] Selecting facet:", { hasAccumulatedData: !!accumulatedData, renderingMode, renderingHint, itemKeys: accumulatedData?.items ? Object.keys(accumulatedData.items) : [], }) if (!accumulatedData || renderingMode === "simple") return null for (const facet of facetRenderers) { const canRender = facet.canRender({ responseData: accumulatedData, renderingHint: renderingHint || undefined, }) console.log( `[WeatherSearch] Facet ${facet.getCapabilities().displayName} canRender:`, canRender ) if (canRender) { console.log( `[WeatherSearch] Selected facet:`, facet.getCapabilities().displayName ) return facet } } console.log("[WeatherSearch] No facet selected") return null }, [accumulatedData, renderingHint, facetRenderers, renderingMode]) // Handle SSE streaming const handleStreamingSubmit = React.useCallback( async (e?: React.FormEvent) => { e?.preventDefault() if (!query.trim()) { return } if (!dialogId) { setError("Dialog not initialized yet") return } // Abort any existing stream if (abortControllerRef.current) { abortControllerRef.current.abort() } // Create new abort controller const abortController = new AbortController() abortControllerRef.current = abortController setIsStreaming(true) setIsLoading(true) setError(null) setResults([]) setAccumulatedData(null) setRenderingHint(null) setIsDataFlowComplete(false) try { // Use dialog flow like explore-dialog does const url = `/api/infactory/v1/run/dialog/${dialogId}` const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}`, }, body: JSON.stringify({ mode: "direct", reason: "standard", api_path: program, api_params: { request: query.trim(), }, }), signal: abortController.signal, }) if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`) } const stream = response.body if (!stream) { throw new Error("No response body") } const reader = stream.getReader() const decoder = new TextDecoder() let buffer = "" // Use StructuredDataAccumulator for proper data merging const accumulator = new StructuredDataAccumulator() while (true) { const { done, value } = await reader.read() if (done) break buffer += decoder.decode(value, { stream: true }) const messages = buffer.split("\r\n\r\n") buffer = messages.pop() || "" for (const message of messages) { if (message.trim()) { const lines = message.split("\n") let eventType = "" let dataLines: string[] = [] for (const line of lines) { if (line.startsWith("event: ")) { eventType = line.slice(7).trim() } else if (line.startsWith("data: ")) { dataLines.push(line.slice(6)) } } if (dataLines.length > 0) { try { const dataContent = dataLines.join("") const data = JSON.parse(dataContent) if ( eventType === "StructuredData" && isStructuredData(data) ) { // Check for complete/finalize if ( data.purpose === "complete" || data.purpose === "finalize" ) { console.log( "[WeatherSearch] Setting isDataFlowComplete to true, purpose:", data.purpose ) setIsDataFlowComplete(true) // Skip accumulating finalize/complete messages with empty items // These are just control messages, not data updates const hasItems = data.items && Object.keys(data.items).length > 0 if (!hasItems) { console.log( "[WeatherSearch] Skipping empty finalize/complete message" ) continue } } // Accumulate data using StructuredDataAccumulator // This properly handles StringItem concatenation for "append" purpose accumulator.accumulate(data) // Update rendering hint from metadata if (data.metadata?.rendering_hint) { setRenderingHint(data.metadata.rendering_hint) } // Get the accumulated data and update state const accumulated = accumulator.getAccumulated() if (accumulated) { console.log("[WeatherSearch] Setting accumulated data:", { purpose: data.purpose, itemKeys: Object.keys(accumulated.items || {}), isComplete: data.purpose === "complete" || data.purpose === "finalize", }) setAccumulatedData({ ...accumulated }) } } } catch (e) { console.warn("Failed to parse SSE data:", e) } } } } } setIsStreaming(false) setIsLoading(false) } catch (err) { if (err instanceof Error && err.name === "AbortError") { console.log("Stream aborted") } else { const errorMessage = err instanceof Error ? err.message : "Failed to fetch results" setError(errorMessage) onError?.(errorMessage) } setIsStreaming(false) setIsLoading(false) } }, [query, dialogId, program, onError] ) // Handle non-streaming submit const handleSimpleSubmit = React.useCallback( async (e?: React.FormEvent) => { e?.preventDefault() if (!query.trim()) { return } setIsLoading(true) setError(null) setResults([]) try { // Execute the query program const result: ExecutionResult = await client.run(program, { request: query.trim(), }) if (result.error) { setError(result.error) onError?.(result.error) } else { // Extract results from StructuredData const extractedResults = extractResults(result.structuredData) if (extractedResults.length === 0) { setError("No results found") onError?.("No results found") } else { setResults(extractedResults) onResults?.(extractedResults.map((r) => r.text)) } } } catch (err) { const errorMessage = err instanceof Error ? err.message : "Failed to fetch results" setError(errorMessage) onError?.(errorMessage) } finally { setIsLoading(false) } }, [query, client, program, onResults, onError] ) // Choose submit handler based on streaming mode const handleSubmit = streamingMode ? handleStreamingSubmit : handleSimpleSubmit const handleKeyDown = React.useCallback( (e: React.KeyboardEvent<HTMLInputElement>) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault() handleSubmit() } }, [handleSubmit] ) return ( <div className={cn("w-full space-y-0", className)}> {/* Search Input */} <form onSubmit={handleSubmit} className="relative z-10"> <div className="relative overflow-visible rounded-2xl"> {isLoading && ( <ShineBorder shineColor={[ "#9333ea", // Purple "#3b82f6", // Blue "#06b6d4", // Cyan "#8b5cf6", // Violet "#ec4899", // Pink "#3b82f6", // Blue "#9333ea", // Purple ]} borderWidth={3} duration={3} className="z-10" /> )} <InputGroup className="relative z-0 rounded-2xl"> <InputGroupAddon> <Search className="size-4" /> </InputGroupAddon> <InputGroupInput placeholder={placeholder} value={query} onChange={(e) => { const newValue = e.target.value if (isControlled) { onValueChange?.(newValue) } else { setInternalQuery(newValue) } }} onKeyDown={handleKeyDown} disabled={isLoading} autoComplete="off" data-form-type="other" data-lpignore="true" data-1p-ignore="true" /> <InputGroupAddon align="inline-end"> <InputGroupButton variant="secondary" type="submit" disabled={isLoading || !query.trim() || !dialogId} > {isLoading ? ( <> <Loader2 className="size-4 animate-spin" /> {isStreaming ? "Streaming..." : "Searching..."} </> ) : !dialogId ? ( <> <Loader2 className="size-4 animate-spin" /> Initializing... </> ) : ( "Search" )} </InputGroupButton> </InputGroupAddon> </InputGroup> </div> </form> {/* Error State */} {error && ( <div className="bg-destructive/10 text-destructive border-destructive/20 animate-in fade-in slide-in-from-top-2 mt-3 rounded-2xl border p-3 text-sm transition-all duration-200 ease-out"> <div className="font-medium">Error</div> <div className="text-destructive/80 mt-1 text-xs">{error}</div> </div> )} {/* Dialog initialization message */} {!dialogId && !error && ( <div className="text-muted-foreground mt-3 rounded-2xl border border-dashed p-4 text-center text-sm"> <Loader2 className="mx-auto mb-2 h-4 w-4 animate-spin" /> Initializing weather search... </div> )} {/* Facet Renderer (when using auto mode and facet is selected) */} {(() => { // Show facet immediately when loading starts, even without data const shouldRender = streamingMode && (isLoading || (selectedFacet && accumulatedData)) console.log("[WeatherSearch] Render check:", { streamingMode, hasSelectedFacet: !!selectedFacet, hasAccumulatedData: !!accumulatedData, shouldRender, isLoading, isDataFlowComplete, }) if (!shouldRender) return null // Use selected facet if available, otherwise use WeatherCardRendererFacet for skeleton // Note: WeatherCardRendererFacet is at index 1 (after WeatherCalculationRendererFacet) const weatherCardFacet = facetRenderers.find( (f) => f.getCapabilities().displayName === "Weather Card Renderer" ) || facetRenderers[1] const facetToRender = selectedFacet || weatherCardFacet // Create empty StructuredData for skeleton state const emptyData = { id: "skeleton", purpose: "skeleton", items: {}, timestamp: new Date().toISOString(), metadata: {}, infactory_structured_data_version: 1, } // Always use accumulated data if available (for streaming tokens) // But control visibility via facetState const dataToShow = accumulatedData || emptyData return ( <div className="ease-out-cubic relative z-0 transition-all duration-200"> {facetToRender.render({ responseData: dataToShow, facetState: isLoading ? FacetState.SKELETON : FacetState.ENABLED, isDataFlowComplete, })} </div> ) })()} {/* Simple Results (fallback or simple mode) */} {!streamingMode && results.length > 0 && ( <div className="animate-in fade-in slide-in-from-bottom-2 mt-3 space-y-2 transition-all duration-200 ease-out"> {results.map((result, index) => ( <div key={index} className="bg-muted/50 hover:bg-muted/70 ease rounded-2xl border p-3 text-sm shadow-xs transition-all duration-200" > {result.title && ( <div className="text-foreground mb-1 font-medium"> {result.title} </div> )} <div className="text-foreground">{result.text}</div> {result.description && ( <div className="text-muted-foreground mt-1 text-xs"> {result.description} </div> )} </div> ))} </div> )} {/* Empty State (when not loading, no error, and no results) */} {!isLoading && !error && results.length === 0 && !accumulatedData && !query.trim() && dialogId && ( <div className="text-muted-foreground mt-3 rounded-2xl border border-dashed p-6 text-center text-sm"> Enter a weather query and press Search or Enter </div> )} </div> )}export default WeatherSearchOverview
The Weather Search component is a self-contained, production-ready component that enables natural language weather queries powered by Infactory. Simply pass your credentials and it handles all the API integration internally.
Key Features
- Zero Configuration UI - Just pass
projectIdandapiKeyas props - Integrated Results - Displays responses in styled cards below the input
- Smart Parsing - Extracts text from multiple StructuredData types
- Loading States - Shows spinner during API calls
- Error Handling - User-friendly error messages with retry
- Keyboard Support - Submit with Enter key
- Fully Typed - Complete TypeScript support
- Callbacks - Optional
onResultsandonErrorhooks
Usage
Basic Setup
import { WeatherSearch } from "@/components/ui/weather-search"
export default function WeatherWidget() {
return (
<WeatherSearch
projectId="your-project-id"
apiKey="your-api-key"
placeholder="Ask about weather..."
/>
)
}With Environment Variables
# .env.local
NEXT_PUBLIC_INFACTORY_PROJECT_ID=your-project-id
NEXT_PUBLIC_INFACTORY_API_KEY=your-api-key
NEXT_PUBLIC_INFACTORY_API_URL=https://api.infactory.aiconst INFACTORY_CONFIG = {
projectId: process.env.NEXT_PUBLIC_INFACTORY_PROJECT_ID!,
apiKey: process.env.NEXT_PUBLIC_INFACTORY_API_KEY!,
apiUrl: process.env.NEXT_PUBLIC_INFACTORY_API_URL,
}
<WeatherSearch {...INFACTORY_CONFIG} />With Callbacks
"use client"
import { WeatherSearch } from "@/components/ui/weather-search"
export default function WeatherPage() {
const handleResults = (results: string[]) => {
console.log("Weather data received:", results)
// Process results in your app
}
const handleError = (error: string) => {
console.error("Weather search failed:", error)
// Handle errors
}
return (
<WeatherSearch
projectId={process.env.NEXT_PUBLIC_INFACTORY_PROJECT_ID!}
apiKey={process.env.NEXT_PUBLIC_INFACTORY_API_KEY!}
placeholder="What's the weather?"
onResults={handleResults}
onError={handleError}
/>
)
}API Reference
WeatherSearch
A self-contained component that handles weather queries with integrated result display.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| projectId | string | - | Required. Your Infactory project ID |
| apiKey | string | - | Required. Your Infactory API key |
| apiUrl | string | "https://api.infactory.ai" | Infactory API base URL |
| program | string | "weather_flow" | Query program to execute |
| placeholder | string | "Ask about weather..." | Input placeholder text |
| className | string | - | Optional container class name |
| onResults | (results: string[]) => void | - | Callback when results are received |
| onError | (error: string) => void | - | Callback when an error occurs |
Example Queries
The component handles various weather queries:
- "What's the weather for 43130"
- "Naples fl weather"
- "Orlando weather forecast"
- "Weather forecast for topsail nc"
- "What is the wind speed today"
- "What is the feel like temperature outside right now"
- "Avondale weather tomorrow"
- "Are we gonna have rain this week"
- "Give me the weather for next week in las vegas"
Features
Internal API Integration
The component creates an InfactoryClient instance internally and handles all API communication:
// Automatically handled inside the component
const client = new InfactoryClient({
projectId,
apiKey,
apiUrl,
})
// Creates dialog and executes program
const result = await client.run(program, { query: userInput })Smart Result Extraction
The component intelligently extracts text from various StructuredData item types:
- String items - Direct text values
- Scalar items - Numeric and text values
- DataFrame items - Tabular data rows
- Map items - Nested data structures
Loading States
Shows a spinner icon in the search button during API calls:
{isLoading ? (
<Loader2 className="size-4 animate-spin" />
) : (
"Search"
)}Error Handling
Displays user-friendly error messages in styled alert boxes:
- Network errors
- API failures
- Invalid credentials
- Empty results
Keyboard Navigation
- Press Enter to submit the query
- Works alongside the Search button
- Disabled during loading
Integration Example
Complete example for a weather dashboard:
"use client"
import { useState } from "react"
import { WeatherSearch } from "@/components/ui/weather-search"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
export default function WeatherDashboard() {
const [weatherData, setWeatherData] = useState<string[]>([])
return (
<div className="container mx-auto p-6">
<Card>
<CardHeader>
<CardTitle>Weather Search</CardTitle>
</CardHeader>
<CardContent>
<WeatherSearch
projectId={process.env.NEXT_PUBLIC_INFACTORY_PROJECT_ID!}
apiKey={process.env.NEXT_PUBLIC_INFACTORY_API_KEY!}
onResults={(results) => {
setWeatherData(results)
// Store in state, display in charts, etc.
}}
onError={(error) => {
// Show toast notification
console.error(error)
}}
/>
</CardContent>
</Card>
{weatherData.length > 0 && (
<Card className="mt-6">
<CardHeader>
<CardTitle>Weather Data</CardTitle>
</CardHeader>
<CardContent>
<pre className="text-sm">{JSON.stringify(weatherData, null, 2)}</pre>
</CardContent>
</Card>
)}
</div>
)
}Getting Your Infactory Credentials
- Sign up at platform.infactory.ai
- Create a project or use an existing one
- Get your Project ID from the project settings
- Generate an API key from your account settings
- Set up your weather query program in Infactory
- Start using the component with your credentials!
Notes
- Works anywhere - Any React app, any framework
- Self-contained - No external state management needed
- Production ready - Full error handling and loading states
- Customizable - Override the query program, API URL, and styling
- Accessible - Proper ARIA labels and keyboard navigation
- Responsive - Works on all screen sizes
- Type safe - Full TypeScript support with proper interfaces
Use Cases
Perfect for:
- Weather.com integration - Embed in existing weather platforms
- Dashboard widgets - Add weather search to admin panels
- Mobile apps - React Native compatible
- Customer portals - Let users query weather data
- Internal tools - Quick weather lookups for teams
On This Page
InstallationFolder structureOverviewKey FeaturesUsageBasic SetupWith Environment VariablesWith CallbacksAPI ReferenceWeatherSearchPropsExample QueriesFeaturesInternal API IntegrationSmart Result ExtractionLoading StatesError HandlingKeyboard NavigationIntegration ExampleGetting Your Infactory CredentialsNotesUse Cases