Weather Search

Previous

Self-contained weather search component with integrated results display. Perfect for embedding Infactory-powered weather queries into any React application.

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

components
ui
weather-search.tsx
lib
facets
base
base-facet.ts
base-renderer-facet.ts
implementations
renderer
infinite-weather-renderer-facet.tsx
weather-card-renderer-facet.tsx
structured-data.ts
types.ts
infactory
client.ts
structured-data.ts
utils.ts
weather-search.tsx
"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 WeatherSearch

Overview

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 projectId and apiKey as 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 onResults and onError hooks

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.ai
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,
}
 
<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

PropTypeDefaultDescription
projectIdstring-Required. Your Infactory project ID
apiKeystring-Required. Your Infactory API key
apiUrlstring"https://api.infactory.ai"Infactory API base URL
programstring"weather_flow"Query program to execute
placeholderstring"Ask about weather..."Input placeholder text
classNamestring-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

  1. Sign up at platform.infactory.ai
  2. Create a project or use an existing one
  3. Get your Project ID from the project settings
  4. Generate an API key from your account settings
  5. Set up your weather query program in Infactory
  6. 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