# @standardagents/react

React hooks and components for Standard Agents - connect to AI agent threads with real-time updates, send messages, manage files, and listen for custom events.

## Installation

```bash
npm install @standardagents/react
# or
pnpm add @standardagents/react
# or
yarn add @standardagents/react
```

## Quick Start

```tsx
import {
  AgentBuilderProvider,
  ThreadProvider,
  useThread,
} from "@standardagents/react"

function App() {
  return (
    <AgentBuilderProvider config={{ endpoint: "https://your-api.com" }}>
      <ThreadProvider threadId="thread-123">
        <ChatInterface />
      </ThreadProvider>
    </AgentBuilderProvider>
  )
}

function ChatInterface() {
  const { messages, sendMessage, status } = useThread()

  const handleSend = async (text: string) => {
    await sendMessage({ role: "user", content: text })
  }

  return (
    <div>
      <p>Status: {status}</p>
      {messages.map((msg) => (
        <div key={msg.id}>
          <strong>{msg.role}:</strong> {msg.content}
        </div>
      ))}
      <input onKeyDown={(e) => e.key === "Enter" && handleSend(e.currentTarget.value)} />
    </div>
  )
}
```

## Authentication

The package reads the authentication token from `localStorage` using the key `standardagents_auth_token`:

```tsx
// Set the token before using the hooks
localStorage.setItem("standardagents_auth_token", "your-token-here")
```

All API requests and WebSocket connections will automatically include this token.

## API Reference

### `AgentBuilderProvider`

Context provider that configures the Standard Agents client for all child components.

**Props:**

- `config.endpoint: string` - The API endpoint URL

```tsx
<AgentBuilderProvider config={{ endpoint: "https://api.example.com" }}>
  {children}
</AgentBuilderProvider>
```

---

### `ThreadProvider`

Context provider that establishes a WebSocket connection to a specific thread. Must be nested inside `AgentBuilderProvider`.

**Props:**

- `threadId: string` - The thread ID to connect to
- `preload?: boolean` - Fetch existing messages on mount (default: `true`)
- `live?: boolean` - Enable WebSocket for real-time updates (default: `true`)
- `useWorkblocks?: boolean` - Transform tool calls into workblocks (default: `false`)
- `depth?: number` - Message depth level for nested conversations (default: `0`)
- `includeSilent?: boolean` - Include silent messages (default: `false`)
- `endpoint?: string` - Override the endpoint from context

```tsx
<AgentBuilderProvider config={{ endpoint: "https://api.example.com" }}>
  <ThreadProvider threadId="thread-123" live={true}>
    <YourComponents />
  </ThreadProvider>
</AgentBuilderProvider>
```

---

### `useThread()`

Hook to access the full thread context. Must be used within a `ThreadProvider`.

**Returns:** `ThreadContextValue`

- `threadId: string` - The thread ID
- `messages: Message[]` - Array of messages
- `workblocks: ThreadMessage[]` - Messages transformed to workblocks (if `useWorkblocks` is true)
- `status: ConnectionStatus` - WebSocket connection status (`"connecting"` | `"connected"` | `"disconnected"` | `"reconnecting"`)
- `loading: boolean` - Whether messages are loading (alias: `isLoading`)
- `error: Error | null` - Any error that occurred
- `options: ThreadProviderOptions` - Options passed to the provider
- `sendMessage: (payload: SendMessagePayload) => Promise<Message>` - Send a message
- `stopExecution: () => Promise<void>` - Stop current execution
- `onEvent: <T>(eventType, listener) => () => void` - Subscribe to custom events (alias: `subscribeToEvent`)
- `files: ThreadFile[]` - All files (pending + committed)
- `addFiles: (files: File[] | FileList) => void` - Upload files
- `removeFile: (id: string) => void` - Remove a pending file
- `getFileUrl: (file: ThreadFile) => string` - Get file URL
- `getThumbnailUrl: (file: ThreadFile) => string` - Get thumbnail URL
- `getPreviewUrl: (file: ThreadFile) => string | null` - Get preview URL

**Example:**

```tsx
function ChatView() {
  const {
    messages,
    sendMessage,
    stopExecution,
    status,
    isLoading,
    files,
    addFiles,
  } = useThread()

  return (
    <div>
      <p>Status: {status}</p>
      {isLoading && <p>Loading...</p>}

      {messages.map((msg) => (
        <div key={msg.id}>
          <strong>{msg.role}:</strong> {msg.content}
        </div>
      ))}

      <input
        type="file"
        multiple
        onChange={(e) => e.target.files && addFiles(e.target.files)}
      />

      <div>
        {files.map((file) => (
          <span key={file.id}>{file.name} ({file.status})</span>
        ))}
      </div>

      <button onClick={() => sendMessage({ role: "user", content: "Hello!" })}>Send</button>
      <button onClick={stopExecution}>Stop</button>
    </div>
  )
}
```

---

### `useThreadEvent<T>(eventType)`

Hook to listen for custom events emitted by the agent. Must be used within a `ThreadProvider`.

**Parameters:**

- `eventType: string` - The custom event type to listen for

**Returns:** `T | null` - The latest event value, or `null` if no event received yet

**Example:**

```tsx
function TodoProgress() {
  const todos = useThreadEvent<{ todos: string[]; completed: number }>("todo-updated")

  if (!todos) return <div>Waiting for updates...</div>

  return (
    <div>
      <p>Progress: {todos.completed} / {todos.todos.length}</p>
      <ul>
        {todos.todos.map((todo, i) => (
          <li key={i}>{todo}</li>
        ))}
      </ul>
    </div>
  )
}
```

---

### `onThreadEvent<T>(eventType, callback)`

Hook to listen for custom events with a callback. Must be used within a `ThreadProvider`.

**Parameters:**

- `eventType: string` - The custom event type to listen for
- `callback: (data: T) => void` - Called when event is received

**Example:**

```tsx
function Notifications() {
  onThreadEvent<{ message: string }>("notification", (data) => {
    alert(data.message)
  })

  return null
}
```

---

## File Management

The `useThread()` hook provides file management capabilities:

```tsx
function FileUploader() {
  const { files, addFiles, removeFile, getFileUrl, getPreviewUrl } = useThread()

  return (
    <div>
      <input
        type="file"
        multiple
        accept="image/*,.pdf,.txt"
        onChange={(e) => e.target.files && addFiles(e.target.files)}
      />

      {files.map((file) => (
        <div key={file.id}>
          {file.isImage && file.status !== 'uploading' && (
            <img src={getPreviewUrl(file) || ''} alt={file.name} />
          )}
          <span>{file.name}</span>
          <span>{file.status}</span>
          {file.status === 'uploading' && <span>Uploading...</span>}
          {file.status === 'error' && <span>Error: {file.error}</span>}
          {file.status !== 'committed' && (
            <button onClick={() => removeFile(file.id)}>Remove</button>
          )}
        </div>
      ))}
    </div>
  )
}
```

### File States

- `uploading` - File is being uploaded
- `ready` - Upload complete, file ready to attach to message
- `committed` - File is attached to a sent message
- `error` - Upload failed

---

## Types

```typescript
interface Message {
  id: string
  role: "user" | "assistant" | "system" | "tool"
  content: string | null
  created_at: number
  attachments?: string // JSON array of AttachmentRef
}

interface SendMessagePayload {
  role: "user" | "assistant" | "system"
  content: string
  silent?: boolean
  attachments?: string[]  // Paths of files to attach
}

interface ThreadFile {
  id: string
  name: string
  mimeType: string
  size: number
  isImage: boolean
  status: "uploading" | "ready" | "committed" | "error"
  error?: string
  path?: string
  localPreviewUrl: string | null
}

type ConnectionStatus = "connecting" | "connected" | "disconnected" | "reconnecting"
```

---

## Complete Example

```tsx
import { useState, useEffect } from "react"
import {
  AgentBuilderProvider,
  ThreadProvider,
  useThread,
  useThreadEvent,
} from "@standardagents/react"

function App() {
  useEffect(() => {
    localStorage.setItem("standardagents_auth_token", "your-token")
  }, [])

  return (
    <AgentBuilderProvider config={{ endpoint: "https://api.example.com" }}>
      <ThreadProvider threadId="thread-123">
        <AgentChat />
      </ThreadProvider>
    </AgentBuilderProvider>
  )
}

function AgentChat() {
  const [input, setInput] = useState("")

  const {
    messages,
    sendMessage,
    stopExecution,
    status,
    isLoading,
    files,
    addFiles,
    removeFile,
    getPreviewUrl,
  } = useThread()

  const progress = useThreadEvent<{ step: string; percent: number }>("progress")

  const handleSend = async () => {
    if (!input.trim()) return
    await sendMessage({ role: "user", content: input })
    setInput("")
  }

  return (
    <div>
      {progress && (
        <div>
          <p>{progress.step}</p>
          <progress value={progress.percent} max={100} />
        </div>
      )}

      {isLoading && <p>Loading messages...</p>}

      {messages.map((msg) => (
        <div key={msg.id}>
          <strong>{msg.role}:</strong> {msg.content}
        </div>
      ))}

      <input
        type="file"
        multiple
        onChange={(e) => e.target.files && addFiles(e.target.files)}
      />

      {files.filter(f => f.status !== 'committed').map((file) => (
        <div key={file.id}>
          {file.isImage && getPreviewUrl(file) && (
            <img src={getPreviewUrl(file)!} alt={file.name} width={50} />
          )}
          <span>{file.name} ({file.status})</span>
          <button onClick={() => removeFile(file.id)}>x</button>
        </div>
      ))}

      <div>
        <input
          value={input}
          onChange={(e) => setInput(e.target.value)}
          onKeyDown={(e) => e.key === "Enter" && handleSend()}
        />
        <button onClick={handleSend}>Send</button>
        <button onClick={stopExecution}>Stop</button>
        <span>Status: {status}</span>
      </div>
    </div>
  )
}
```

---

## TypeScript Support

The package includes full TypeScript definitions:

```tsx
import type {
  Message,
  SendMessagePayload,
  ThreadFile,
  ConnectionStatus,
  ThreadContextValue,
} from "@standardagents/react"
```
