Blazor C# チュートリアル - Claude APIで画像入力

# はじめに

前回「C#(Blazor)でClaude APIを使う方法について」の続きです。
本記事では画像入力の機能について説明したいと思います。

https://qiita.com/firesign2023/items/702407263cdfff117e05

# 完成イメージ

このような感じで画像入力ができるように機能を追加したいと思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3343142/065703fa-3c0e-b866-d4ad-977c160bca4b.png
)

# 概要説明

ファイルの選択ボタンを押して画像を選択し、その画像を入力に使います。
・BlazorApp1の`Home.razor`、`Home.razor.cs`に変更を加えます。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3343142/f39180de-f941-9b86-58b3-534dbde03ab1.png)



以下のリンク`Upload File`を参考にコードを追加します。

https://github.com/Cysharp/Claudia

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3343142/1325609b-f162-8d60-e9bc-b8dbf4316d7c.png)

:::note info
Anthropic ConsoleにWorkbenchという機能があります。
API実行および実行時のコードやデータ構造を確認することができます。
コードはPythonとTypeScriptですが、参考になると思います。
:::

https://console.anthropic.com/workbench/

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3343142/b9b82d4d-e5f3-abc2-63b1-d4b97e1af8a5.png)


# チュートリアル

それでは機能追加していきましょう。

`ファイルを追加する`ボタンを追加します。
ボタンを押すとファイルを選択できるようになります。
ファイルが追加された後の処理`UploadImage`にて画像をbyte配列に変換します。

```C#:Home.razor
        <div class="p-4">
            <InputFile id="InputFileImage" OnChange="@UploadImage" type="file" accept="image/*" />
        </div>
```


```C#:Home.razor.cs
    bool isLoading = false;
    string imageType = "";
    byte[]? imageBytes = null;

    private async Task UploadImage(InputFileChangeEventArgs e)
    {
        if (isLoading) return;

        isLoading = true;

        // Get the uploaded image(s)
        var files = e.GetMultipleFiles();

        // Read the image data
        var buffer = new byte[files[0].Size];
        await files[0].OpenReadStream().ReadAsync(buffer);

        // Convert the image data to a base64 string
        imageBytes = buffer;
        imageInput = Convert.ToBase64String(buffer);
        imageType = files[0].ContentType;

        isLoading = false;
    }
```

:::note info
Claude APIを呼び出すタイミングはSendボタンを押した時にしています。
入力データは「テキスト」or「画像＋テキスト」の形式です。
:::




```C#:Home.razor.cs
    async Task SendClick()
    {
        if (running) return;
        if (string.IsNullOrWhiteSpace(textInput)) return;

        running = true;
        try
        {
            chatMessages.Add(new() { 
                    Role = Roles.User,                 
                    Content = (imageBytes == null) ? textInput : [new(imageBytes, imageType), textInput ]
                });

            var stream = Anthropic.Messages.CreateStreamAsync(new()
            {
                Model = Models.Claude3Opus,
                MaxTokens = 1024,
                Temperature = temperature,
                System = string.IsNullOrWhiteSpace(systemInput) ? null : systemInput,
                Messages = chatMessages.ToArray()
            });

            var currentMessage = new Message { Role = Roles.Assistant, Content = "" };
            chatMessages.Add(currentMessage);

            textInput = ""; // clear input.
            imageType = "";
            imageBytes = null;

            await JSRuntime.InvokeVoidAsync("resetFileInput", null);

            StateHasChanged();

            await foreach (var messageStreamEvent in stream)
            {
                if (messageStreamEvent is ContentBlockDelta content)
                {
                    currentMessage.Content[0].Text += content.Delta.Text;
                    StateHasChanged();
                }
            }
        }
        finally
        {
            running = false;
        }
    }
```

# チャット表示部の変更
`switch`文で分岐しています。入力に画像が含まれる場合は`item.Content[0].Text`が`null`なのでご注意ください。

```C#:Home.razor
            @foreach (var item in chatMessages)
            {
                @if (item.Role == Roles.User)
                {
                    <div class="mb-2">
                        <span class="font-bold text-blue-500">User:</span>
                        @switch (item.Content[0].Type)
                        {
                            case ContentTypes.Text:
                                @((MarkupString)item.Content[0].Text!.Replace("\n", "<br />"))
                                break;
                            case ContentTypes.Image:
                                @((MarkupString)item.Content[1].Text!.Replace("\n", "<br />"))
                                // 画像のbyte配列からBase64文字列を生成
                                var base64Image = Convert.ToBase64String(item.Content[0].Source.Data.Span);
                                <img src="data:@item.Content[0].Source.MediaType;base64,@base64Image" alt="User Image" />                            
                                break;
                            default:
                                
                                break;
                        }
                    </div>
                }
                else
                {
                    <div class="mb-2">
                        <span class="font-bold text-green-500">Assistant:</span> @((MarkupString)item.Content[0].Text!.Replace("\n", "<br />"))
                    </div>
                }
            }
```


# 画像入力後にリセットする処理：resetFileInput

```diff_apache:Home.razor
<script>
    var rangeInput = document.querySelector(".range_slider");
    var rangeBubble = document.querySelector(".range_bubble");

    rangeInput.addEventListener("input", () => {
        setBubble(rangeInput, rangeBubble);
    });
    rangeInput.addEventListener("mouseover", () => {
        setBubble(rangeInput, rangeBubble);
    });
    rangeInput.addEventListener("mouseleave", () => {
        rangeBubble.style.display = "none";
    });

    function setBubble(range, bubble) {
        var val = range.value;
        var min = range.min ? range.min : 0;
        var max = range.max ? range.max : 100;
        var newVal = Number(((val - min) * 100) / (max - min));
        bubble.innerHTML = val;
        bubble.style.left = `calc(${newVal}% + (${-5 - newVal * 0.15}px))`;
        rangeBubble.style.display = "inline-block";
    }

    function downloadFromBase64String(fileName, base64String) {
        const a = document.createElement('a');
        a.href = "data:application/octet-stream;base64," + base64String;
        a.download = fileName;
        a.click();
        a.remove();
    }

+    function resetFileInput(element) {
+        if (element) {
+            element.value = null;
+        } else {
+            const fileInputs = document.querySelectorAll('input[type="file"]');
+            fileInputs.forEach(input => input.value = null);
+        }
+    }
</script>
```

# おわり

複数画像には対応していません。画像を使うと通信データ量が大きくなる為、クレジットの減りが早くなるのでご注意ください。
また、画像サイズが大きいとうまく動かない場合があります。
`MaxTokens = 1024`を変更するか、画像を小さくして対応してください。