# AI SDK Frontend Integration

## 1. Overview

This guide covers frontend integration with the Vercel AI SDK using `@ai-sdk/react`. Key topics include:

- Using `@ai-sdk/react` for React integration
- Streaming chat with the `useChat` hook
- Tool call handling with proper format detection

## 2. Basic Chat with useChat

The `useChat` hook provides a simple interface for chat functionality:

```typescript
"use client";

import { useChat } from "@ai-sdk/react";

export function ChatPanel() {
  const { messages, input, handleInputChange, handleSubmit, status } = useChat({
    api: "/api/chat",
  });

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

      <form onSubmit={handleSubmit}>
        <input
          value={input}
          onChange={handleInputChange}
          placeholder="Type a message..."
          disabled={status === "streaming"}
        />
        <button type="submit" disabled={status === "streaming"}>
          Send
        </button>
      </form>
    </div>
  );
}
```

## 3. Custom Transport with oRPC

When using oRPC instead of standard fetch:

```typescript
import { useChat } from "@ai-sdk/react";
import { eventIteratorToStream } from "@orpc/client";
import { orpcClient } from "@/lib/orpc-client";

export function ChatPanel({ sessionId }: { sessionId: string }) {
  const { messages, sendMessage, status } = useChat({
    id: sessionId,
    transport: {
      async sendMessages(options) {
        return eventIteratorToStream(
          await orpcClient.chat.send(
            {
              sessionId,
              messages: options.messages,
            },
            { signal: options.abortSignal }
          )
        );
      },
      reconnectToStream() {
        throw new Error("Reconnect not supported");
      },
    },
  });

  // ... rest of component
}
```

## 4. Tool Calls Handling

**CRITICAL**: Tool calls have TWO different formats that must both be handled:

### Format 1: Real-time Streaming

During streaming, tool results appear as:

```typescript
{
  type: "tool-createTask",  // tool-{toolName}
  toolCallId: "call_abc123",
  state: "output-available",
  input: { title: "...", priority: "high" },
  output: { success: true, taskId: "task_xyz" }  // Direct object
}
```

### Format 2: History Restore

When loading from history/database:

```typescript
{
  type: "tool-result",
  toolName: "createTask",
  toolCallId: "call_abc123",
  output: {
    type: "json",
    value: { success: true, taskId: "task_xyz" }  // Nested in value
  }
}
```

### Unified Handling Pattern

```typescript
import { useChat } from "@ai-sdk/react";
import { useEffect, useState, useRef } from "react";

export function AssistantPanel({ sessionId }: { sessionId: string }) {
  const [createdItems, setCreatedItems] = useState<Map<string, CreatedItem>>(new Map());
  const toolCallsRef = useRef<Map<string, string>>(new Map());

  const { messages, status } = useChat({
    id: sessionId,
    transport: { /* ... */ },

    // Handle real-time tool results
    onData: (dataPart) => {
      const payload = typeof dataPart === "object" && "json" in dataPart
        ? (dataPart as { json: unknown }).json
        : dataPart;

      if (typeof payload === "object" && payload !== null && "type" in payload) {
        const { type, data } = payload as { type: string; data: any };

        if (type === "tool-output-available" || type === "tool-result") {
          const { toolCallId, output } = data;
          const toolName = toolCallsRef.current.get(toolCallId);

          if (toolName === "createTask" && output?.success) {
            setCreatedItems((prev) => {
              if (prev.has(toolCallId)) return prev;
              return new Map(prev).set(toolCallId, {
                id: output.taskId,
                title: output.title,
              });
            });
          }
        }
      }
    },
  });

  // Handle history restore
  useEffect(() => {
    messages.forEach((message) => {
      if (message.role !== "assistant") return;

      const parts = (message as any).parts || [];
      parts.forEach((part: any) => {
        // Match both formats
        const isRealTime = part.type === "tool-createTask" && part.state === "output-available";
        const isRestored = part.type === "tool-result" && part.toolName === "createTask";

        if ((isRealTime || isRestored) && part.output) {
          const key = part.toolCallId || message.id;

          // Extract output (handle nested structure)
          const rawOutput = part.output;
          const output = rawOutput?.type === "json" && rawOutput?.value
            ? rawOutput.value
            : rawOutput;

          if (output?.success) {
            setCreatedItems((prev) => {
              if (prev.has(key)) return prev;
              return new Map(prev).set(key, {
                id: output.taskId,
                title: output.title,
              });
            });
          }
        }
      });
    });
  }, [messages, status]);

  return (
    <div>
      {messages.map((message) => (
        <MessageBubble key={message.id} message={message} />
      ))}

      {/* Display created items */}
      {Array.from(createdItems.values()).map((item) => (
        <CreatedItemCard key={item.id} item={item} />
      ))}
    </div>
  );
}
```

## 5. Tool Call State Lifecycle

During streaming, tool parts go through these states:

| State | Description |
|-------|-------------|
| `input-streaming` | Tool input is being generated |
| `input-available` | Complete input ready |
| `output-available` | Tool executed, result available |
| `output-error` | Tool execution failed |

## 6. Displaying Thought Process

Show users what the AI is "thinking":

```typescript
const [thoughtSteps, setThoughtSteps] = useState<ThoughtStep[]>([]);

// In onData handler
if (type === "tool-input-start" || type === "tool-call") {
  const { toolCallId, toolName, input } = data;
  toolCallsRef.current.set(toolCallId, toolName);

  setThoughtSteps((prev) => [
    ...prev,
    { id: toolCallId, toolName, status: "pending", input },
  ]);
}

if (type === "tool-output-available") {
  setThoughtSteps((prev) =>
    prev.map((step) =>
      step.id === toolCallId
        ? { ...step, status: "done", result: output }
        : step
    )
  );
}
```

## Scenario: Elder AI Analysis Panel MVP

### 1. Scope / Trigger

- Trigger: UI surfaces that render elder AI analysis, knowledge retrieval results, or AI history.
- Current project contract: the MVP elder workflow is a fixed structured analysis panel, not chat and not streaming.
- Do not use `@ai-sdk/react` hooks for this MVP panel; use the existing project API result shape through fetch until a generated client exists for these routes.

### 2. Signatures

- Elder row action:
  - Visible only when current permissions include both `ai:read` and `elder:read`.
  - Opens `ElderAiAnalysisDialog` with `width="wide"`.
- Client APIs:
  - `GET /api/ai/elders/[id]/analyses` -> `{ success, reason, history }`
  - `POST /api/ai/elders/[id]/analyses` -> `{ success, reason, analysis }`
  - `GET /api/ai/knowledge` -> `{ success, reason, entries }`
  - Knowledge mutations -> `{ success, reason, entry }`

### 3. Contracts

- Render completed analysis from fixed fields:
  - `overallRiskLevel`
  - `summary`
  - `keyFindings[]`
  - `recommendations[]`
  - `dataGaps[]`
  - `citations[]`
  - `confidence`
  - `modelSummary`
- Render `restricted: true` history items as placeholders only; do not assume `result` or citations exist.
- Render failed history from sanitized `errorCategory` / `errorReason`; never display raw provider errors.
- The MVP UI must not provide one-click business mutations or confirmable drafts from recommendations.

### 4. Validation & Error Matrix

- Missing `ai:read` or `elder:read` -> hide row action.
- API failure -> show `reason` in the dialog or settings page message area.
- `restricted: true` history -> show an access-restricted placeholder.
- `status === "failed"` -> show failed badge and sanitized reason.
- Empty history -> show empty state.
- Generation pending -> disable generate button and show loading label.

### 5. Good/Base/Bad Cases

- Good: authorized manager opens dialog, generates analysis, sees latest completed result, citations, data gaps, and history list.
- Base: generation fails because provider is not configured; user sees a structured failure message and failed history remains visible.
- Bad: viewer without `ai:read` sees an `AI 分析` row action.
- Bad: frontend renders recommendations as buttons that create care tasks, alert handling notes, or health review records.

### 6. Tests Required

- Component or route smoke coverage for hidden AI action when permissions are missing.
- History rendering coverage for completed, failed, restricted, and empty states.
- Knowledge settings coverage for read-only users and `knowledge:manage` users.
- Build check must include both unscoped `/app/settings/knowledge` and scoped `/app/[organizationSlug]/settings/knowledge`.

### 7. Wrong vs Correct

#### Wrong

```tsx
// Do not expose an AI action based only on elder read permission.
{permissions.includes("elder:read") ? <button>AI 分析</button> : null}
```

#### Correct

```tsx
const canUseAi = permissions.includes("ai:read") && permissions.includes("elder:read");
{canUseAi ? <Button onClick={() => setAiTarget(elder)}>AI 分析</Button> : null}
```

## 7. Best Practices Summary

| Rule | Description |
|------|-------------|
| Handle both tool formats | Real-time and history restore |
| Use toolCallId as key | Correlate calls across formats |
| Use useRef for toolName mapping | Avoid React state timing issues |
| onData for real-time UI | useEffect for history restore |
| Show thought process | Better UX for tool-heavy flows |
