diff --git a/.github/workflows/dotnet-desktop.yml b/.github/workflows/dotnet-desktop.yml
index c5030e8..d082363 100644
--- a/.github/workflows/dotnet-desktop.yml
+++ b/.github/workflows/dotnet-desktop.yml
@@ -1,78 +1,42 @@
name: Build WPF Application
on:
- push:
- branches: [ "main" ]
pull_request:
- branches: [ "main" ]
+ branches:
+ - main
+ - release/**
-permissions:
- contents: write
+env:
+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
build:
- runs-on: windows-latest
-
+ runs-on: windows-2025-vs2026
+
steps:
- - uses: actions/checkout@v4
-
- - name: Setup MSBuild
- uses: microsoft/setup-msbuild@v1
- with:
- vs-version: '[17.0, 18.0]'
-
- - name: Setup NuGet
- uses: NuGet/setup-nuget@v1
- with:
- nuget-version: 'latest'
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '8.0.x'
+
+ - name: Setup MSBuild
+ uses: microsoft/setup-msbuild@v2
+ with:
+ vs-version: '17.0'
- - name: Restore NuGet packages
- run: nuget restore SampleCSharpUI.sln
-
- - name: Build WPF Application
- run: msbuild SampleCSharpUI.sln /p:Configuration=Release /p:Platform="Any CPU"
-
- - name: Upload Build Artifacts
- uses: actions/upload-artifact@v4
- with:
- name: SampleCSharpUI-${{ github.run_number }}
- path: |
- $(Build.ArtifactStagingDirectory)\**
- **\bin\Release\**
- retention-days: 7
-
- - name: Prepare release ZIP
- shell: pwsh
- run: |
- $zipName = "SampleCSharpUI-${{ github.run_number }}.zip"
- # ビルド出力 (任意のサブフォルダーの bin\Release を含むすべてのファイル) を収集
- $files = Get-ChildItem -Path . -Recurse -File | Where-Object { $_.FullName -like "*\bin\Release\*" } | Select-Object -ExpandProperty FullName
- if (-not $files) {
- Write-Error "No files found under bin\\Release"
- exit 1
- }
- Compress-Archive -Path $files -DestinationPath $zipName -Force
- Write-Output "Created $zipName"
+ # ★ 追加:packages.config 用
+ - name: NuGet Restore
+ run: nuget restore SampleCSharpUI.sln
- - name: Create GitHub Release
- if: github.event_name == 'push' && github.ref == 'refs/heads/main'
- id: create_release
- uses: actions/create-release@v1
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- with:
- tag_name: v${{ github.run_number }}
- release_name: Release ${{ github.run_number }}
- draft: false
- prerelease: false
+ # 念のため残してもOK(SDK-style project 用)
+ - name: dotnet Restore
+ run: dotnet restore SampleCSharpUI.sln
- - name: Upload ZIP to Release
- if: github.event_name == 'push' && github.ref == 'refs/heads/main'
- uses: actions/upload-release-asset@v1
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- with:
- upload_url: ${{ steps.create_release.outputs.upload_url }}
- asset_path: SampleCSharpUI-${{ github.run_number }}.zip
- asset_name: SampleCSharpUI-${{ github.run_number }}.zip
- asset_content_type: application/zipp
+ - name: Build (Release)
+ run: msbuild SampleCSharpUI.sln `
+ /p:Configuration=Release `
+ /p:Platform="Any CPU" `
+ /t:Build
diff --git a/.github/workflows/release-upload-zip.yml b/.github/workflows/release-upload-zip.yml
new file mode 100644
index 0000000..ded636c
--- /dev/null
+++ b/.github/workflows/release-upload-zip.yml
@@ -0,0 +1,67 @@
+name: Release - Upload ZIP
+
+on:
+ push:
+ tags:
+ - 'v*' # v1.3.0.0 など
+
+env:
+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
+
+jobs:
+ release:
+ runs-on: windows-2025-vs2026
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '8.0.x'
+
+ - name: Setup MSBuild
+ uses: microsoft/setup-msbuild@v2
+ with:
+ vs-version: '17.0'
+
+ # packages.config 対応
+ - name: NuGet Restore
+ run: nuget restore SampleCSharpUI.sln
+
+ - name: Build (Release)
+ run: msbuild SampleCSharpUI.sln `
+ /p:Configuration=Release `
+ /p:Platform="Any CPU" `
+ /t:Build
+
+ # ZIP 作成(タグ名を使用)
+ - name: Create ZIP
+ run: |
+ Compress-Archive `
+ -Path SampleCSharpUI/bin/Release/* `
+ -DestinationPath SampleCSharpUI-${{ github.ref_name }}.zip
+
+ # Release 作成(タグ名を使用)
+ - name: Create Release
+ id: create_release
+ uses: actions/create-release@v1
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ tag_name: ${{ github.ref_name }}
+ release_name: Release ${{ github.ref_name }}
+ draft: false
+ prerelease: false
+
+ # ZIP を Release に Upload(ご指定どおり)
+ - name: Upload ZIP to Release
+ uses: actions/upload-release-asset@v1
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ upload_url: ${{ steps.create_release.outputs.upload_url }}
+ asset_path: SampleCSharpUI-${{ github.ref_name }}.zip
+ asset_name: SampleCSharpUI-${{ github.ref_name }}.zip
+ asset_content_type: application/zip
diff --git a/README.md b/README.md
index 09244a5..c1dc15c 100644
--- a/README.md
+++ b/README.md
@@ -7,14 +7,16 @@ Fujitsu Cloud Service Genearative AI Platform の API を利用するための C
## 起動
1. SampleCSharpUI.exe をクリックして起動
2. 【初回起動時のみ】
-自動的にテナント名とクライアントIDを入力する設定画面を表示するので、利用している Generative AI Platform 環境のテナント名とクライアント ID を入力して [OK] をクリックして保存してください。
+自動的にテナント名とクライアントIDを入力する [ 設定 ] 画面を表示するので、利用している Generative AI Platform 環境のテナント名とクライアント ID を入力して [OK] をクリックして保存してください。
3. 自動的にサインインが始まりますので、利用している EntraID の ID とパスワードを入力してください。
4. 「 General Use 」というルームがない場合は、自動的にRAGなしでルームを作成します。
5. 前回利用時のルームの内容を自動的に表示します。初回起動時のみ「 General Use 」ルームが選択されます。
- 「 General Use 」ルームは起動時に会話内容が全クリアされる特別なルームです。
- 他のルームは再起動しても会話内容は維持されます。
-### 設定画面詳細
+### [ 設定 ] 画面詳細
+[ ファイル ]-[ 接続設定 ]メニューで接続情報を後から変更可能です。
+
| 項目 | 用途 |
| ---- | ---- |
| 対話認証 | EntraID でサインインする場合に選択 |
@@ -106,27 +108,34 @@ RAG自体を削除したり、RAGに格納されているデータを変更し
- 過去履歴を踏まえたプロンプトパターンの実現
- ルームへ会話を記録することによる後日閲覧の実現
- 各種トークン数の設定
-しかしながら、ルーム不要でTakaneに対してプロンプトを入力し回答を得るだけでよい場合には、ルームの設定や管理などの事前実行が必要なWebAPI呼び出しは煩雑に感じてしまいます。
+しかしながら、ルーム不要でTakaneに対してプロンプトを入力し回答を得るだけでよい場合には、ルームの設定や管理などの事前実行が必要なWebAPI呼び出しは煩雑に感じてしまいます。
そのようなユースケースでは「ルームなし会話」モードがあります。
### 本サンプル画面での使い方
-ルームの選択で「(none)」を選択するとチャットルームなしでの利用となります。
+ルームの選択で「(none)」を選択するとチャットルームなしでの利用となります。
サンプルコード内で画面に表示されている過去履歴を渡しているので、過去履歴を踏まえたやりとりが可能となっておりますが、過去履歴を渡さずにAPIコールをすれば、必ず入力したプロンプトだけを踏まえた回答となります。
#### 制限事項
-ルームを「(none)」に選択してる場合は、ルーム編集や新規作成の機能はご利用になれません。
+ルームを「(none)」に選択してる場合は、ルーム編集や新規作成の機能はご利用になれません。
ルーム編集やルーム新規作成が必要な場合は、他のルームを選択してから実行してください。
-### ルームなし会話の実現方法
-具体的には、BODYに入力されたプロンプトを設定し、下記のAPIをPOSTすることで戻り値としてAIの回答を得ることが出来ます。
+### マルチモーダル対応
+
+
+ルームなし会話では、マルチモーダル対応として、添付画像に対する質問を入力することができます。
+添付できる画像ファイル形式は、png、jpeg、webp、非アニメーションgif形式の5MB以下となります。
+1. 画像の添付方法は、入力欄下の [+] ボタンをクリックしてファイル選択
+2. 画像の添付解除は、画像横の [-] ボタンをクリックして解除
+
+マルチモーダル対応は、Cohere v2 Chat 互換 API (/v2/chat)、または、Cohere OpenAI Chat Completions 互換 API (/compatibility/v1/chat/completions) を呼び出すことで実現可能です。
+サンプルコードでは、Cohere v2 Chat 互換 API を使用しています。
+
-```
-/api/v1/action/defined/text:simple_chat/call
-```
+OpenAI 互換 API、または、Cohere V2 Chat 互換 API を呼び出すことで実現可能です。
# サンプルコードの build 方法
-サンプルコードは、Visual Studio 2026 または、Visual Studio Code を使用して実行ファイルを build できます。
-Visual Studio 2026 であれば、SampleCSharpUI.sln を開いていただければ、あとは UI 上で実行や build が可能です。
+サンプルコードは、Visual Studio 2026 または、Visual Studio Code を使用して実行ファイルを build できます。
+Visual Studio 2026 であれば、SampleCSharpUI.sln を開いていただければ、あとは UI 上で実行や build が可能です。
Visual Studio Code の場合は、環境設定などが必要です。
# 最後に
diff --git a/SampleCSharpUI/Commons/APIData.cs b/SampleCSharpUI/Commons/APIData.cs
index d7f163b..ede1bf3 100644
--- a/SampleCSharpUI/Commons/APIData.cs
+++ b/SampleCSharpUI/Commons/APIData.cs
@@ -208,5 +208,58 @@ public class TChatStream
[DataMember]
public string token { get; set; }
}
+
+ [DataContract]
+ public class TCohereV2ChatRequest
+ {
+ [DataMember]
+ public string model { get; set; }
+ [DataMember]
+ public TCohereV2ChatMessage[] messages { get; set; }
+ [DataMember]
+ public float temperature { get; set; }
+ [DataMember]
+ public uint max_tokens { get; set; }
+
+ }
+
+ [DataContract]
+ public class TCohereV2ChatMessage
+ {
+ [DataMember]
+ public string role { get; set; }
+ [DataMember]
+ public TContent[] content { get; set; }
+ }
+
+ [DataContract]
+ public class TContent
+ {
+ [DataMember]
+ public string type { get; set; }
+ [DataMember]
+ public string text { get; set; }
+ [DataMember]
+ public TImageUrl image_url { get; set; }
+ }
+
+ [DataContract]
+ public class TImageUrl
+ {
+ [DataMember]
+ public string url { get; set; }
+ }
+
+ [DataContract]
+ public class TCohereV2ChatResponse
+ {
+ [DataMember]
+ public string id { get; set; }
+ [DataMember]
+ public TCohereV2ChatMessage message { get; set; }
+ [DataMember]
+ public string finish_reason { get; set; }
+
+ }
}
}
diff --git a/SampleCSharpUI/Commons/Base64Helper.cs b/SampleCSharpUI/Commons/Base64Helper.cs
new file mode 100644
index 0000000..b86850e
--- /dev/null
+++ b/SampleCSharpUI/Commons/Base64Helper.cs
@@ -0,0 +1,70 @@
+using System;
+using System.IO;
+using System.Net.Http;
+using System.Threading.Tasks;
+
+namespace SampleCSharpUI.Commons
+{
+ ///
+ /// 画像データをBase64エンコードするためのヘルパーメソッドを提供します。
+ ///
+ internal static class Base64Helper
+ {
+ ///
+ /// 指定したファイルパスの画像ファイルを非同期に読み込み、Base64エンコードした文字列を返します。
+ ///
+ /// 読み込む画像ファイルのパス。null、空白は許可されません。
+ /// 画像データをBase64エンコードした文字列を表す非同期タスク。
+ /// が null または空白の場合にスローされます。
+ /// 指定したパスのファイルが存在しない場合にスローされる可能性があります。
+ /// ファイルアクセス権が不足している場合にスローされる可能性があります。
+ /// ファイル サイズが int.MaxValue を超える場合にスローされます(メモリ処理不可)。
+ ///
+ /// このメソッドはファイル全体をメモリに読み込んでからBase64変換を行います。
+ /// 大きなファイルを扱う場合はメモリ使用量に注意してください。
+ /// ファイルの部分読み込みを考慮した安全な読み取りループを使用しています。
+ ///
+ internal static async Task ImageFileToBase64Async(string path)
+ {
+ if (string.IsNullOrWhiteSpace(path))
+ throw new ArgumentException("path is null or empty", nameof(path));
+
+ using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, useAsync: true))
+ {
+ if (fs.Length > int.MaxValue)
+ throw new NotSupportedException("File too large to process in memory.");
+
+ var bytes = new byte[checked((int)fs.Length)];
+ var read = 0;
+ while (read < bytes.Length)
+ {
+ var r = await fs.ReadAsync(bytes, read, bytes.Length - read).ConfigureAwait(false);
+ if (r == 0) break;
+ read += r;
+ }
+
+ return Convert.ToBase64String(bytes);
+ }
+ }
+
+ ///
+ /// 指定した URL からバイト配列を取得し、Base64 エンコードした文字列を返します。
+ ///
+ /// 画像などのリソースを取得する URL。
+ /// 取得したデータをBase64エンコードした文字列を表す非同期タスク。
+ /// が null の場合にスローされる可能性があります。
+ /// HTTP 要求の送信や応答の取得に失敗した場合にスローされます。
+ ///
+ /// この実装では呼び出しごとに新しい を生成しています。
+ /// 長時間または高頻度で使用する場合は、ソケット枯渇を避けるために を再利用することを検討してください。
+ ///
+ internal static async Task ImageUrlToBase64Async(string url)
+ {
+ using (var http = new HttpClient())
+ {
+ var bytes = await http.GetByteArrayAsync(url).ConfigureAwait(false);
+ return Convert.ToBase64String(bytes);
+ }
+ }
+ }
+}
diff --git a/SampleCSharpUI/Commons/HttpHelper.cs b/SampleCSharpUI/Commons/HttpHelper.cs
index 315dc28..48d688d 100644
--- a/SampleCSharpUI/Commons/HttpHelper.cs
+++ b/SampleCSharpUI/Commons/HttpHelper.cs
@@ -147,8 +147,16 @@ private static async Task SendRequestAsync(HttpMethod method, string api
// リクエスト送信とレスポンス受信
using (var response = await SendClient.SendAsync(request))
{
- response.EnsureSuccessStatusCode();
- answer = await response.Content.ReadAsStringAsync();
+ if (response.StatusCode == System.Net.HttpStatusCode.BadRequest)
+ {
+ answer = await response.Content.ReadAsStringAsync();
+
+ }
+ else
+ {
+ response.EnsureSuccessStatusCode();
+ answer = await response.Content.ReadAsStringAsync();
+ }
}
}
}
diff --git a/SampleCSharpUI/Converters/Null2CollapsedConverter.cs b/SampleCSharpUI/Converters/Null2CollapsedConverter.cs
new file mode 100644
index 0000000..559a3db
--- /dev/null
+++ b/SampleCSharpUI/Converters/Null2CollapsedConverter.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Data;
+
+namespace SampleCSharpUI.Converters
+{
+ internal class Null2CollapsedConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is string)
+ {
+ return string.IsNullOrEmpty((string)value) ? Visibility.Collapsed : Visibility.Visible;
+ }
+ else
+ {
+ return Visibility.Collapsed;
+ }
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
\ No newline at end of file
diff --git a/SampleCSharpUI/Models/ChatModel.cs b/SampleCSharpUI/Models/ChatModel.cs
index b01785d..b5e4aee 100644
--- a/SampleCSharpUI/Models/ChatModel.cs
+++ b/SampleCSharpUI/Models/ChatModel.cs
@@ -8,6 +8,7 @@
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
+using System.Security.Policy;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@@ -175,8 +176,8 @@ public ChatModel()
}
#region "チャット関連"
- public ObservableCollection ChatRooms = new ObservableCollection();
- public ObservableCollection Messages = new ObservableCollection();
+ public ObservableCollection ChatRooms { get; set; } = new ObservableCollection();
+ public ObservableCollection Messages { get; set; } = new ObservableCollection();
private Models.TDataChatRoom _SelectedChatRoom = null;
public Models.TDataChatRoom SelectedChatRoom
@@ -226,15 +227,16 @@ internal async Task GetChatRoomsAsync(bool isUseNone = false)
{
var ser = new System.Runtime.Serialization.Json.DataContractJsonSerializer(typeof(List));
{
- var result = ser.ReadObject(json) as List;
- foreach (var chat in result)
+ var results = ser.ReadObject(json) as List;
+ foreach (var item in results?.OrderByDescending((x) => x.created_date))
{
var chatRoom = new Models.TDataChatRoom()
{
- ID = chat.id,
- Name = chat.name,
- ChatTemplateId = chat.chat_template_id,
- RetrieverIDs = chat.retriever_ids,
+ ID = item.id,
+ Name = item.name,
+ ChatTemplateId = item.chat_template_id,
+ RetrieverIDs = item.retriever_ids,
+ CreateDateTime = DateTimeOffset.FromUnixTimeMilliseconds(item.created_date).ToLocalTime().DateTime,
};
this.ChatRooms.Add(chatRoom);
}
@@ -455,10 +457,17 @@ internal async Task SendMessageAsync(string id, string inputText)
{
// 質問のメッセージ追加
var item = ser.ReadObject(json) as APIData.TChatMessage;
- this.SetMessage(item.role,
- item.content,
- DateTimeOffset.FromUnixTimeMilliseconds(item.timeunix).ToLocalTime().DateTime,
- item.ref_chunks is null ? new List() : item.ref_chunks?.Select((x) => x.text.Replace("\n\n", "\n")).ToList());
+ if (item?.role != null)
+ {
+ this.SetMessage(item.role,
+ item.content,
+ DateTimeOffset.FromUnixTimeMilliseconds(item.timeunix).ToLocalTime().DateTime,
+ item.ref_chunks is null ? new List() : item.ref_chunks?.Select((x) => x.text.Replace("\n\n", "\n")).ToList());
+ }
+ else
+ {
+ throw new Exception(jsonString);
+ }
}
json.Close();
@@ -531,10 +540,17 @@ internal async Task SendMessageStreamingAsync(string id, string inputText)
{
// 質問のメッセージ追加
var item = ser.ReadObject(json) as APIData.TChatMessage;
- this.SetMessage(item.role,
- item.content,
- DateTimeOffset.FromUnixTimeMilliseconds(item.timeunix).ToLocalTime().DateTime,
- item.ref_chunks is null ? new List() : item.ref_chunks?.Select((x) => x.text.Replace("\n\n", "\n")).ToList());
+ if (item?.role != null)
+ {
+ this.SetMessage(item.role,
+ item.content,
+ DateTimeOffset.FromUnixTimeMilliseconds(item.timeunix).ToLocalTime().DateTime,
+ item.ref_chunks is null ? new List() : item.ref_chunks?.Select((x) => x.text.Replace("\n\n", "\n")).ToList());
+ }
+ else
+ {
+ throw new Exception(jsonString);
+ }
}
json.Close();
@@ -610,7 +626,8 @@ internal async Task SendMessageStreamingAsync(string id, string inputText)
}
OnPropertyChanged("Messages_Item");
}
- catch {
+ catch
+ {
this.IsStreaming = false;
// エラー発生時はAI回答欄を削除する。
this.Messages.Remove(this.Messages.Where((x) => x.Id == msgId).FirstOrDefault());
@@ -645,10 +662,11 @@ private async Task GetLastAIResponseAsync(string id)
var result = ser.ReadObject(json) as APIData.TChat;
var item = result.messages.Where((x) => x.role == "ai").LastOrDefault();
var lastItem = this.Messages.Where((x) => x.Role == "ai").LastOrDefault();
- if (lastItem != null) {
+ if (lastItem != null && !string.IsNullOrEmpty(lastItem.Content))
+ {
SetMessage(lastItem.Id,
- item.role, item.content,
- DateTimeOffset.FromUnixTimeMilliseconds(item.timeunix).ToLocalTime().DateTime,
+ item.role, item.content,
+ DateTimeOffset.FromUnixTimeMilliseconds(item.timeunix).ToLocalTime().DateTime,
item.ref_chunks is null ? new List() : item.ref_chunks?.Select((x) => x.text.Replace("\n\n", "\n")).ToList());
}
@@ -698,7 +716,14 @@ internal async Task SendMessageAsync(List histories, float temperature
{
// 回答のメッセージ追加
var item = ser.ReadObject(json) as APIData.TNonRoomResponse;
- this.SetMessage(msgId, "ai", item.answer, DateTime.UtcNow.ToLocalTime(), new List());
+ if (item?.answer != null)
+ {
+ this.SetMessage(msgId, "ai", item.answer, DateTime.UtcNow.ToLocalTime(), new List());
+ }
+ else
+ {
+ throw new Exception(jsonString);
+ }
}
json.Close();
@@ -742,15 +767,39 @@ private APIData.TChatMessage[] SetHistories(List histories)
}
}
+ // 履歴設定(チャットルームなし)
+ private APIData.TCohereV2ChatMessage[] SetCohereV2ChatHistories(List histories)
+ {
+ if (histories == null || histories.Count == 0)
+ {
+ return new APIData.TCohereV2ChatMessage[] { };
+ }
+ else
+ {
+ var messages = new APIData.TCohereV2ChatMessage[histories.Count];
+ for (int i = 0; i < histories.Count; i++)
+ {
+ var history = histories[i];
+ messages[i] = new APIData.TCohereV2ChatMessage()
+ {
+ role = history.Role == "ai" ? "assistant" : "user",
+ content = new APIData.TContent[] { new APIData.TContent() { type = "text", text = history.Content } },
+ };
+ }
+ return messages;
+ }
+ }
+
// メッセージ追加(チャットルームなし)
- private Guid SetMessage(string role, string content, DateTime time, List refs)
+ private Guid SetMessage(string role, string content, DateTime time, List refs, string image = null)
{
var msg = new TMessage()
{
Role = role,
Content = content,
Time = time,
- Refs = refs
+ Refs = refs,
+ Image = image
};
msg.PropertyChanged += (s, e) => { OnPropertyChanged("Messages_Item"); };
this.Messages.Add(msg);
@@ -778,6 +827,103 @@ private Guid SetMessage(Guid id, string role, string content, DateTime time, Lis
}
}
+
+ ///
+ /// プロンプト入力(チャットルームなし/マルチモーダル)
+ ///
+ /// 入力
+ internal async Task SendMessageWithFileAsync(List histories, float temperature, int token, string inputText, string filePath)
+ {
+ var content = inputText ?? string.Empty;
+ var base64ImageData = !string.IsNullOrEmpty(filePath) ? await Base64Helper.ImageFileToBase64Async(filePath) : string.Empty;
+ if (!string.IsNullOrWhiteSpace(content))
+ {
+ var body = new APIData.TCohereV2ChatRequest()
+ {
+ model = "takane",
+ messages = this.SetCohereV2ChatHistories(histories),
+ temperature = temperature,
+ max_tokens = (uint)token,
+ };
+
+ // ここで配列の末尾に要素を追加する(body.messages が配列であることを前提)
+ var existing = body.messages ?? new APIData.TCohereV2ChatMessage[] { };
+ var newArr = new APIData.TCohereV2ChatMessage[existing.Length + 1];
+ if (existing.Length > 0)
+ {
+ Array.Copy(existing, newArr, existing.Length);
+ }
+ if (!string.IsNullOrEmpty(base64ImageData))
+ {
+ newArr[newArr.Length - 1] = new APIData.TCohereV2ChatMessage()
+ {
+ role = "user",
+ content = new APIData.TContent[] {
+ new APIData.TContent() { type = "text", text = inputText },
+ new APIData.TContent() { type = "image_url", image_url = new APIData.TImageUrl() { url= $"data:image/png;base64,{base64ImageData}" } }
+ },
+ };
+ }
+ else
+ {
+ newArr[newArr.Length - 1] = new APIData.TCohereV2ChatMessage()
+ {
+ role = "user",
+ content = new APIData.TContent[] {
+ new APIData.TContent() { type = "text", text = inputText },
+ },
+ };
+ }
+ body.messages = newArr;
+
+ // 質問のメッセージ追加
+ this.SetMessage("user", content, DateTime.UtcNow.ToLocalTime(), new List(), filePath);
+
+ // ここで body を JSON 文字列にシリアライズして変数に格納する
+ using (var ms = new MemoryStream())
+ {
+ var msgId = this.SetMessage("ai", Resources.Streaming, DateTime.UtcNow.ToLocalTime(), new List());
+ OnPropertyChanged("Messages_Item");
+ var serializer = new System.Runtime.Serialization.Json.DataContractJsonSerializer(typeof(APIData.TCohereV2ChatRequest));
+ {
+ serializer.WriteObject(ms, body);
+ try
+ {
+ var bodyJsonString = Encoding.UTF8.GetString(ms.ToArray());
+ var jsonString = await HttpHelper.PostRequestAsync($"/api/v1/pass-through/takane/v2/chat", this.IdToken, bodyJsonString);
+ using (var json = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(jsonString)))
+ {
+ var ser = new System.Runtime.Serialization.Json.DataContractJsonSerializer(typeof(APIData.TCohereV2ChatResponse));
+ {
+ // 回答のメッセージ追加
+ var item = ser.ReadObject(json) as APIData.TCohereV2ChatResponse;
+ if (item?.id != null)
+ {
+ this.SetMessage(msgId, "ai", item.message.content[0].text, DateTime.UtcNow.ToLocalTime(), new List());
+ }
+ else
+ {
+ throw new Exception(jsonString);
+ }
+ }
+ json.Close();
+
+ // 最新行表示
+ OnPropertyChanged("Messages_Item");
+ }
+ }
+ catch (Exception ex)
+ {
+ // エラー発生時はAI回答欄を削除し、exceptionを投げる
+ this.Messages.Remove(this.Messages.Where((x) => x.Id == msgId).FirstOrDefault());
+ throw ex;
+ }
+ }
+ }
+ }
+ }
+
+
///
/// 最新の入力プロンプトまでを削除する(AIからの回答がある場合は、その回答まで削除する
///
@@ -878,6 +1024,7 @@ public class TDataChatRoom
public string Name { get; set; } = string.Empty;
public string ChatTemplateId { get; set; } = string.Empty;
public string[] RetrieverIDs { get; set; } = null;
+ public DateTime CreateDateTime { get; set; } = DateTime.Now;
}
///
@@ -889,5 +1036,6 @@ public class TDataRetriever
public string Name { get; set; } = string.Empty;
public string EmbeddingModel { get; set; } = string.Empty;
public string[] OriginIDs { get; set; } = null;
+ public DateTime CreateDateTime { get; set; } = DateTime.Now;
}
}
\ No newline at end of file
diff --git a/SampleCSharpUI/Models/RetrieverModel.cs b/SampleCSharpUI/Models/RetrieverModel.cs
index a78013e..6e72d6f 100644
--- a/SampleCSharpUI/Models/RetrieverModel.cs
+++ b/SampleCSharpUI/Models/RetrieverModel.cs
@@ -44,13 +44,13 @@ public Models.TDataRetriever SelectedRetriever
///
/// 対象リトリーバ
///
- private APIData.TRetriever _Retriever = null;
+ private APIData.TRetriever _RetrieverData = null;
public APIData.TRetriever RetrieverData
{
- get { return this._Retriever; }
+ get { return this._RetrieverData; }
set
{
- this._Retriever = value;
+ this._RetrieverData = value;
OnPropertyChanged();
}
}
@@ -104,12 +104,13 @@ internal async Task GetRetrieversAsync(bool isUseNone = false)
var ser = new System.Runtime.Serialization.Json.DataContractJsonSerializer(typeof(APIData.TRetrievers));
{
var result = ser.ReadObject(json) as APIData.TRetrievers;
- foreach (var item in result.results)
+ foreach (var item in result.results?.OrderByDescending((x)=> x.created_at))
{
var retriever = new Models.TDataRetriever()
{
ID = item.id,
- Name = item.name
+ Name = item.name,
+ CreateDateTime = DateTimeOffset.FromUnixTimeMilliseconds(item.created_at).ToLocalTime().DateTime,
};
this.Retrievers.Add(retriever);
}
diff --git a/SampleCSharpUI/Models/TMessage.cs b/SampleCSharpUI/Models/TMessage.cs
index eda7ba7..9d689f2 100644
--- a/SampleCSharpUI/Models/TMessage.cs
+++ b/SampleCSharpUI/Models/TMessage.cs
@@ -76,6 +76,23 @@ internal set
}
}
+ ///
+ /// 添付イメージファイル名
+ ///
+ private string _Image { get; set; } = string.Empty;
+ public string Image
+ {
+ get { return _Image; }
+ internal set
+ {
+ if (_Image != value)
+ {
+ _Image = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
// プロパティが変更されたときに通知するイベント
public event PropertyChangedEventHandler PropertyChanged;
diff --git a/SampleCSharpUI/Properties/AssemblyInfo.cs b/SampleCSharpUI/Properties/AssemblyInfo.cs
index f3fd2b3..7949290 100644
--- a/SampleCSharpUI/Properties/AssemblyInfo.cs
+++ b/SampleCSharpUI/Properties/AssemblyInfo.cs
@@ -48,5 +48,5 @@
// ビルド番号
// リビジョン
//
-[assembly: AssemblyVersion("1.2.0.0")]
-[assembly: AssemblyFileVersion("1.2.0.0")]
+[assembly: AssemblyVersion("1.3.0.0")]
+[assembly: AssemblyFileVersion("1.3.0.0")]
diff --git a/SampleCSharpUI/Properties/Resources.Designer.cs b/SampleCSharpUI/Properties/Resources.Designer.cs
index c59bd1f..0ca83d3 100644
--- a/SampleCSharpUI/Properties/Resources.Designer.cs
+++ b/SampleCSharpUI/Properties/Resources.Designer.cs
@@ -78,6 +78,33 @@ public static string AnswerTokenBudget {
}
}
+ ///
+ /// + に類似しているローカライズされた文字列を検索します。
+ ///
+ public static string Attach {
+ get {
+ return ResourceManager.GetString("Attach", resourceCulture);
+ }
+ }
+
+ ///
+ /// ファイルを添付 に類似しているローカライズされた文字列を検索します。
+ ///
+ public static string AttachFile {
+ get {
+ return ResourceManager.GetString("AttachFile", resourceCulture);
+ }
+ }
+
+ ///
+ /// 画像ファイル (*.png;*.jpeg;*.jpg;*.gif;*.webp)|*.png;*.jpeg;*.jpg;*.gif;*.webp に類似しているローカライズされた文字列を検索します。
+ ///
+ public static string AttachFileFilter {
+ get {
+ return ResourceManager.GetString("AttachFileFilter", resourceCulture);
+ }
+ }
+
///
/// 認証方式 に類似しているローカライズされた文字列を検索します。
///
@@ -186,6 +213,24 @@ public static string DeleteMessage {
}
}
+ ///
+ /// ー に類似しているローカライズされた文字列を検索します。
+ ///
+ public static string Detach {
+ get {
+ return ResourceManager.GetString("Detach", resourceCulture);
+ }
+ }
+
+ ///
+ /// 添付を削除 に類似しているローカライズされた文字列を検索します。
+ ///
+ public static string DetachFile {
+ get {
+ return ResourceManager.GetString("DetachFile", resourceCulture);
+ }
+ }
+
///
/// 参照ドキュメント表示 に類似しているローカライズされた文字列を検索します。
///
@@ -249,6 +294,15 @@ public static string HistoryTokenBudget {
}
}
+ ///
+ /// 選択されたファイルはサポートされていない形式です。 に類似しているローカライズされた文字列を検索します。
+ ///
+ public static string InvalidFileType {
+ get {
+ return ResourceManager.GetString("InvalidFileType", resourceCulture);
+ }
+ }
+
///
/// ルーム管理 に類似しているローカライズされた文字列を検索します。
///
diff --git a/SampleCSharpUI/Properties/Resources.resx b/SampleCSharpUI/Properties/Resources.resx
index 25b60ad..4fd2ba1 100644
--- a/SampleCSharpUI/Properties/Resources.resx
+++ b/SampleCSharpUI/Properties/Resources.resx
@@ -282,4 +282,22 @@
Markdownファイル (*.md)|*.md|すべてのファイル (*.*)|*.*
+
+ 選択されたファイルはサポートされていない形式です。
+
+
+ ファイルを添付
+
+
+ 画像ファイル (*.png;*.jpeg;*.jpg;*.gif;*.webp)|*.png;*.jpeg;*.jpg;*.gif;*.webp
+
+
+ +
+
+
+ ー
+
+
+ 添付を削除
+
\ No newline at end of file
diff --git a/SampleCSharpUI/SampleCSharpUI.csproj b/SampleCSharpUI/SampleCSharpUI.csproj
index 6596a65..738907a 100644
--- a/SampleCSharpUI/SampleCSharpUI.csproj
+++ b/SampleCSharpUI/SampleCSharpUI.csproj
@@ -148,6 +148,7 @@
Designer
+
@@ -159,6 +160,7 @@
+
diff --git a/SampleCSharpUI/ViewModels/MainViewModel.cs b/SampleCSharpUI/ViewModels/MainViewModel.cs
index 3cdef35..c343320 100644
--- a/SampleCSharpUI/ViewModels/MainViewModel.cs
+++ b/SampleCSharpUI/ViewModels/MainViewModel.cs
@@ -113,6 +113,37 @@ public string InputText
}
}
+ ///
+ /// 添付ファイル(1ファイルのみ)
+ ///
+ private string _AttachedFilePath;
+ public string AttachedFilePath
+ {
+ get => _AttachedFilePath;
+ set
+ {
+ if (_AttachedFilePath != value)
+ {
+ _AttachedFilePath = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ private string _AttachedFileName;
+ public string AttachedFileName
+ {
+ get => _AttachedFileName;
+ set
+ {
+ if (_AttachedFileName != value)
+ {
+ _AttachedFileName = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
///
/// 参照ドキュメント一覧
///
@@ -347,13 +378,18 @@ public RelayCommand PreviewKeyDownCommand
if (!string.IsNullOrEmpty(this.SelectedChatRoom?.ID))
{
- //await this.Model.SendMessageAsync(this.SelectedChatRoom.ID, content);
await this.Model.SendMessageStreamingAsync(this.SelectedChatRoom.ID, content);
}
- else
+ else if (string.IsNullOrEmpty(this.AttachedFilePath))
{
await this.Model.SendMessageAsync(this.Messages.ToList(), (float)0.5, 1024, content);
}
+ else
+ {
+ await this.Model.SendMessageWithFileAsync(this.Messages.ToList(), (float)0.5, 1024, content, this.AttachedFilePath);
+ this.AttachedFilePath = null;
+ this.AttachedFileName = null;
+ }
}
catch (Exception ex)
{
diff --git a/SampleCSharpUI/ViewModels/ManageRetrieversViewModel.cs b/SampleCSharpUI/ViewModels/ManageRetrieversViewModel.cs
index 7c1fa03..e85eb49 100644
--- a/SampleCSharpUI/ViewModels/ManageRetrieversViewModel.cs
+++ b/SampleCSharpUI/ViewModels/ManageRetrieversViewModel.cs
@@ -49,6 +49,20 @@ public string Message
}
}
+ ///
+ /// リトリーバーID
+ ///
+ private string _ID = string.Empty;
+ public string ID
+ {
+ get { return _ID; }
+ set
+ {
+ _ID = value;
+ OnPropertyChanged();
+ }
+ }
+
///
/// リトリーバー名
///
@@ -187,6 +201,7 @@ internal async Task GetRetrieversAsync()
///
internal async Task GetEditDataAsync(string id)
{
+ this.ID = id;
await this.Model.GetRetrieverAsync(id);
}
@@ -246,13 +261,13 @@ public RelayCommand SaveCommand
this.Message = "登録処理中…";
if (this.IsUseUrl || this.IsUseFile || this.IsUseFolder)
{
- var id = await this.Model.CreateRetrieverFromDataAsync(
+ this.ID = await this.Model.CreateRetrieverFromDataAsync(
this.RetrieverName,
this.IsUseUrl ? new List() { this.Url } : null,
files);
- if (!string.IsNullOrEmpty(id))
+ if (!string.IsNullOrEmpty(this.ID))
{
- var item = this.Model.Retrievers.FirstOrDefault((x) => x.ID == id);
+ var item = this.Model.Retrievers.FirstOrDefault((x) => x.ID == this.ID);
if (item != null)
{
this.Model.SelectedRetriever = item;
@@ -304,14 +319,14 @@ public RelayCommand UpdateCommand
if (this.IsUseUrl || this.IsUseFile || this.IsUseFolder)
{
- var id = await this.Model.SetRetrieverFromDataAsync(
+ this.ID = await this.Model.SetRetrieverFromDataAsync(
this.Model.RetrieverData,
this.RetrieverName,
this.IsUseUrl ? new List() { this.Url } : null,
files);
- if (!string.IsNullOrEmpty(id))
+ if (!string.IsNullOrEmpty(this.ID))
{
- var item = this.Model.Retrievers.FirstOrDefault((x) => x.ID == id);
+ var item = this.Model.Retrievers.FirstOrDefault((x) => x.ID == this.ID);
if (item != null)
{
this.Model.SelectedRetriever = item;
diff --git a/SampleCSharpUI/Views/EditRetrieverWindow.xaml b/SampleCSharpUI/Views/EditRetrieverWindow.xaml
index 43ade6e..7bf6bc5 100644
--- a/SampleCSharpUI/Views/EditRetrieverWindow.xaml
+++ b/SampleCSharpUI/Views/EditRetrieverWindow.xaml
@@ -35,26 +35,29 @@
+
-
-
-
+
+
+
+
-
-
+
-
-
-
+
-
-
+
-
+
diff --git a/SampleCSharpUI/Views/MainWindow.xaml b/SampleCSharpUI/Views/MainWindow.xaml
index f709935..d67234d 100644
--- a/SampleCSharpUI/Views/MainWindow.xaml
+++ b/SampleCSharpUI/Views/MainWindow.xaml
@@ -148,40 +148,105 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SampleCSharpUI/Views/MainWindow.xaml.cs b/SampleCSharpUI/Views/MainWindow.xaml.cs
index 68d625d..9432f3f 100644
--- a/SampleCSharpUI/Views/MainWindow.xaml.cs
+++ b/SampleCSharpUI/Views/MainWindow.xaml.cs
@@ -14,6 +14,7 @@ public partial class MainWindow : Window
private SynchronizationContext Context { get; set; } = SynchronizationContext.Current;
public ViewModels.MainViewModel ViewModel { get; } = App.MainVM;
+
public MainWindow()
{
InitializeComponent();
@@ -123,7 +124,6 @@ public MainWindow()
case "Save":
{
- // ファイル保存ダイアログを開いて、選択されたパスを ViewModel 経由で保存処理に渡す
var dlg = new Microsoft.Win32.SaveFileDialog()
{
Filter = Properties.Resources.SaveFilter,
@@ -180,6 +180,11 @@ public MainWindow()
}
}
}
+ else if (e.PropertyName == "SelectedChatRoom")
+ {
+ this.ViewModel.AttachedFileName = string.Empty;
+ this.ViewModel.AttachedFilePath = string.Empty;
+ }
};
}
@@ -194,5 +199,41 @@ private void ShowDialog(Window target)
this.IsEnabled = false;
target.Show();
}
+
+ // --- 添付関連の公開メソッド(XAML の CallMethodAction から呼び出す) ---
+ public void OpenAttachFile()
+ {
+ var dlg = new Microsoft.Win32.OpenFileDialog()
+ {
+ Title = Properties.Resources.AttachFile,
+ // 画像ファイルのみ許可(png, jpeg, jpg, gif, webp)
+ Filter = Properties.Resources.AttachFileFilter,
+ Multiselect = false
+ };
+
+ var result = dlg.ShowDialog(this);
+ if (result == true)
+ {
+ // 追加の安全チェック: 拡張子を厳密に確認する(大文字小文字を無視)
+ var ext = System.IO.Path.GetExtension(dlg.FileName) ?? string.Empty;
+ ext = ext.ToLowerInvariant();
+ if (ext == ".png" || ext == ".jpeg" || ext == ".jpg" || ext == ".gif" || ext == ".webp")
+ {
+ // ViewModel 側のプロパティへ設定
+ this.ViewModel.AttachedFilePath = dlg.FileName;
+ this.ViewModel.AttachedFileName = string.IsNullOrEmpty(dlg.FileName) ? null : System.IO.Path.GetFileName(dlg.FileName);
+ }
+ else
+ {
+ MessageBox.Show(this, Properties.Resources.InvalidFileType, this.Title, MessageBoxButton.OK, MessageBoxImage.Warning);
+ }
+ }
+ }
+
+ public void RemoveAttachedFile()
+ {
+ this.ViewModel.AttachedFilePath = null;
+ this.ViewModel.AttachedFileName = null;
+ }
}
}
diff --git a/SampleCSharpUI/Views/ManageRetrieversWindow.xaml b/SampleCSharpUI/Views/ManageRetrieversWindow.xaml
index af8a1be..600fe3e 100644
--- a/SampleCSharpUI/Views/ManageRetrieversWindow.xaml
+++ b/SampleCSharpUI/Views/ManageRetrieversWindow.xaml
@@ -60,7 +60,7 @@
-
+