# `Nous.Message.ContentPart`
[🔗](https://github.com/nyo16/nous/blob/v0.17.1/lib/nous/message/content_part.ex#L1)

Represents a part of message content supporting multi-modal inputs.

ContentPart enables rich message composition with text, images, files,
and other content types. Each part has a type and content, with optional
provider-specific metadata.

## Content Types

- `:text` - Plain text content
- `:image_url` - Image from URL or data URI
- `:image` - Image with base64 data and metadata
- `:file` - File attachment
- `:file_url` - File from URL
- `:thinking` - Reasoning/thinking content (for models that support it)

## Examples

    # Text content
    iex> ContentPart.text("Hello, world!")
    %ContentPart{type: :text, content: "Hello, world!", options: %{}}

    # Image from URL
    iex> ContentPart.image_url("https://example.com/image.jpg")
    %ContentPart{type: :image_url, content: "https://example.com/image.jpg", options: %{}}

    # Image with metadata
    iex> ContentPart.image("base64data", media_type: "image/jpeg")
    %ContentPart{type: :image, content: "base64data", options: %{media_type: "image/jpeg"}}

# `t`

```elixir
@type t() :: %Nous.Message.ContentPart{
  content: String.t(),
  options: map(),
  type: atom()
}
```

# `base64_to_data_url`

```elixir
@spec base64_to_data_url(String.t(), String.t()) :: String.t()
```

Convert base64 string to a data URL.

## Examples

    iex> base64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJ"
    iex> ContentPart.base64_to_data_url(base64, "image/png")
    "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJ"

# `consolidate`

```elixir
@spec consolidate([t()]) :: String.t() | [t()]
```

Collapse a homogeneous list of text (or thinking) parts into one string.

Providers legitimately split a single response into multiple text or
thought blocks (e.g. text surrounding a tool_use, or a long multi-paragraph
answer). Joining homogeneous lists lets the result fit `Message.content`
(a `:string` field) instead of raising `Ecto.InvalidChangesetError` in
`Message.new!/1`. Mixed-type lists pass through unchanged.

# `data_url?`

```elixir
@spec data_url?(String.t()) :: boolean()
```

Check if a string is a data URL.

## Examples

    iex> ContentPart.data_url?("data:image/png;base64,abc")
    true

    iex> ContentPart.data_url?("https://example.com/img.jpg")
    false

# `detect_mime_type`

```elixir
@spec detect_mime_type(String.t()) :: String.t()
```

Detect MIME type from file extension.

## Examples

    iex> ContentPart.detect_mime_type("/path/to/image.jpg")
    "image/jpeg"

    iex> ContentPart.detect_mime_type("photo.png")
    "image/png"

    iex> ContentPart.detect_mime_type("unknown.xyz")
    "application/octet-stream"

# `extract_text`

```elixir
@spec extract_text([t()]) :: String.t()
```

Extract text content from a list of content parts.

## Examples

    iex> parts = [ContentPart.text("Hello"), ContentPart.text(" world")]
    iex> ContentPart.extract_text(parts)
    "Hello world"

# `file`

```elixir
@spec file(
  String.t(),
  keyword()
) :: t()
```

Create a file content part.

## Examples

    iex> ContentPart.file("/path/to/file.pdf", media_type: "application/pdf")
    %ContentPart{type: :file, content: "/path/to/file.pdf", options: %{media_type: "application/pdf"}}

# `file_url`

```elixir
@spec file_url(String.t()) :: t()
```

Create a file URL content part.

## Examples

    iex> ContentPart.file_url("https://example.com/document.pdf")
    %ContentPart{type: :file_url, content: "https://example.com/document.pdf"}

# `from_binary`

```elixir
@spec from_binary(binary(), String.t()) :: t()
```

Create an image content part from binary data with automatic MIME detection.

## Examples

    iex> image_data = <<255, 216, 255, 224>>
    iex> part = ContentPart.from_binary(image_data, "photo.jpg")
    iex> {part.type, part.content}
    {:image_url, "data:image/jpeg;base64,/9j/4A=="}

# `from_file`

```elixir
@spec from_file(String.t()) :: {:ok, t()} | {:error, atom()}
```

Create an image content part from a local file path.

Reads the file, detects the MIME type, and converts to base64 data URL.

## Examples

    iex> path = Path.join(System.tmp_dir!(), "nous_from_file_example.png")
    iex> File.write!(path, <<137, 80, 78, 71, 13, 10, 26, 10>>)
    :ok
    iex> {:ok, part} = ContentPart.from_file(path)
    iex> {part.type, String.starts_with?(part.content, "data:image/png;base64,")}
    {:image_url, true}
    iex> File.rm!(path)
    :ok

    iex> ContentPart.from_file("/nonexistent.jpg")
    {:error, :enoent}

# `from_file!`

```elixir
@spec from_file!(String.t()) :: t()
```

Create an image content part from a local file path, raising on error.

## Examples

    iex> path = Path.join(System.tmp_dir!(), "nous_from_file_bang_example.jpg")
    iex> File.write!(path, <<255, 216, 255, 224>>)
    :ok
    iex> part = ContentPart.from_file!(path)
    iex> String.starts_with?(part.content, "data:image/jpeg;base64,")
    true
    iex> File.rm!(path)
    :ok

# `http_url?`

```elixir
@spec http_url?(String.t()) :: boolean()
```

Check if a string is an HTTP(S) URL.

## Examples

    iex> ContentPart.http_url?("https://example.com/img.jpg")
    true

    iex> ContentPart.http_url?("data:image/png;base64,abc")
    false

# `image`

```elixir
@spec image(
  String.t(),
  keyword()
) :: t()
```

Create an image content part with metadata.

## Options

- `:media_type` - MIME type (e.g., "image/jpeg", "image/png")
- `:cache_control` - Caching hints for providers that support it

## Examples

    iex> ContentPart.image("base64data", media_type: "image/jpeg")
    %ContentPart{type: :image, content: "base64data", options: %{media_type: "image/jpeg"}}

# `image_url`

```elixir
@spec image_url(String.t()) :: t()
```

Create an image URL content part.

## Examples

    iex> ContentPart.image_url("https://example.com/image.jpg")
    %ContentPart{type: :image_url, content: "https://example.com/image.jpg"}

    iex> ContentPart.image_url("data:image/jpeg;base64,/9j/4AAQ...")
    %ContentPart{type: :image_url, content: "data:image/jpeg;base64,/9j/4AAQ..."}

# `merge`

```elixir
@spec merge(t(), t()) :: t() | {:error, :incompatible_types}
```

Merge content parts of the same type.

Useful for streaming where deltas arrive incrementally.

## Examples

    iex> part1 = ContentPart.text("Hello")
    iex> part2 = ContentPart.text(" world")
    iex> ContentPart.merge(part1, part2)
    %ContentPart{type: :text, content: "Hello world"}

# `new`

```elixir
@spec new(map()) :: {:ok, t()} | {:error, Ecto.Changeset.t()}
```

Create a new content part.

Returns `{:ok, content_part}` on success or `{:error, changeset}` on validation failure.

## Examples

    iex> ContentPart.new(%{type: :text, content: "Hello"})
    {:ok, %ContentPart{type: :text, content: "Hello"}}

    iex> {:error, changeset} = ContentPart.new(%{type: :invalid})
    iex> changeset.valid?
    false

# `new!`

```elixir
@spec new!(map()) :: t()
```

Create a new content part, raising on validation failure.

## Examples

    iex> ContentPart.new!(%{type: :text, content: "Hello"})
    %ContentPart{type: :text, content: "Hello"}

# `parse_data_url`

```elixir
@spec parse_data_url(String.t()) :: {:ok, String.t(), String.t()} | {:error, atom()}
```

Parse a data URL into its MIME type and base64 data components.

## Examples

    iex> ContentPart.parse_data_url("data:image/jpeg;base64,/9j/4AAQ...")
    {:ok, "image/jpeg", "/9j/4AAQ..."}

    iex> ContentPart.parse_data_url("https://example.com/image.jpg")
    {:error, :not_data_url}

# `test_image`

```elixir
@spec test_image() :: t()
```

Create a 1x1 pixel test image as base64 data URL.

Useful for testing multi-modal functionality.

## Examples

    iex> test_image = ContentPart.test_image()
    iex> test_image.type
    :image_url
    iex> String.starts_with?(test_image.content, "data:image/png;base64,")
    true

# `text`

```elixir
@spec text(String.t()) :: t()
```

Create a text content part.

## Examples

    iex> ContentPart.text("Hello, world!")
    %ContentPart{type: :text, content: "Hello, world!"}

# `thinking`

```elixir
@spec thinking(String.t()) :: t()
```

Create a thinking content part for models that support reasoning.

## Examples

    iex> ContentPart.thinking("Let me think about this step by step...")
    %ContentPart{type: :thinking, content: "Let me think about this step by step..."}

# `to_data_url`

```elixir
@spec to_data_url(binary(), String.t()) :: String.t()
```

Convert binary image data to a data URL.

## Examples

    iex> binary_data = <<137, 80, 78, 71, 13, 10, 26, 10>>
    iex> ContentPart.to_data_url(binary_data, "image/png")
    "data:image/png;base64,iVBORw0KGgo="

# `to_text`

```elixir
@spec to_text([t()]) :: String.t()
```

Convert content parts to plain text representation.

## Examples

    iex> parts = [
    ...>   ContentPart.text("Look at this: "),
    ...>   ContentPart.image_url("https://example.com/img.jpg"),
    ...>   ContentPart.text(" Amazing!")
    ...> ]
    iex> ContentPart.to_text(parts)
    "Look at this: [Image: https://example.com/img.jpg] Amazing!"

---

*Consult [api-reference.md](api-reference.md) for complete listing*
