Power Appsでモダンなチャットアプリを作ってみる (1)

# はじめに
[Power Apps Advent Calendar 2024](https://qiita.com/advent-calendar/2024/powerapps) 12月16日分。今回は`Power Apps`で`JSON`ファイルをデータソースにしたモダンなチャットアプリを作っていきたいと思います。

デザインは下記のような`Power Apps キャンバス アプリ`です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/e595a726-2c31-4551-0b0f-9273bd1cdb11.png)

画面デザインがそれらしいですね。`HTML 文字列`を活用しています。
また`HTML 文字列`の格納先は、`高さが伸縮可能なギャラリー コントロール`です。

```plaintext:階層構造
Chat Screen (画面)
└── conMainScreen (垂直コンテナー)
    └── conChatLayout (水平コンテナー)
        ├── conChatListSidebar (垂直コンテナー)
        │   ├── conUserProfileHeader (水平コンテナー)
        │   │   ├── imgUserProfile (画像)
        │   │   └── lblUserName (ラベル)
        │   ├── conNewChatHeader (水平コンテナー)
        │   │   ├── icoNewChat (アイコン)
        │   │   └── lblicoNewChat (ラベル)
        │   └── galChatList (Gallery)
        │       ├── lblCreatedTime (ラベル)
        │       ├── txtAvatar (テキスト入力)
        │       └── lblChatTitle (Label)
        └── conChatMain (垂直コンテナー)
            ├── conChatHeader (水平コンテナー)
            │   ├── icoChatHeader (画像)
            │   └── lblChatHeader (ラベル)
            ├── galMessages (高さが伸縮可能なギャラリー)
            │   └── htmlMessageContent (Html 文字列)
            └── conMessageInput (水平コンテナー)
                ├── inpMessageText (テキスト入力)
                └── btnSendMessage (ボタン)
```
:::note info
モダン コントロールは、`ボタン コントロール`のみ利用しています。
:::

今回のポイントは`JSON`ファイルのコンテンツの書き込み、読み込みによるデータのやり取りになります。
`Power Automate`をつかった手法です。

 - `Power Apps`では、[ParseJSON 関数](https://learn.microsoft.com/ja-jp/power-platform/power-fx/reference/function-parsejson)
 - `Power Automate`では[ファイル コンテンツの取得](https://learn.microsoft.com/ja-jp/sharepoint/dev/business-apps/power-automate/sharepoint-connector-actions-triggers#get-file-content)、[ファイルの作成](https://learn.microsoft.com/ja-jp/sharepoint/dev/business-apps/power-automate/sharepoint-connector-actions-triggers#create-file)、[ファイルの更新](https://learn.microsoft.com/ja-jp/sharepoint/dev/business-apps/power-automate/sharepoint-connector-actions-triggers#update-file)

 上記を活用します。順番に見ていきます。

https://learn.microsoft.com/ja-jp/power-platform/power-fx/reference/function-parsejson

https://learn.microsoft.com/ja-jp/sharepoint/dev/business-apps/power-automate/sharepoint-connector-actions-triggers

## レイアウト作成案のコツ
最近の私のレイアウト案の進め方ですが、**AIファースト**で案出しを進めています。
`Copilot`、`ChatGPT`や`Claude`どれも非常に優秀です。
その中でも`Claude`は[Artifacts](https://youtu.be/vUdNaAAc4FY?si=A_ssnzbZd2qxixR4)機能があり、視覚的にアプリのレイアウトを提案してくれます。

https://youtu.be/vUdNaAAc4FY?si=A_ssnzbZd2qxixR4

::: note info
レイアウトに自信がない方や、どのような形で作成するか迷った際には、こういった進め方もあるんだな、くらいに見ていただければ幸いです。
:::

# JSONファイル活用におけるデータ戦略
今回はチャットの履歴を`JSON`ファイルそのものに格納します。内容は都度`Power Automate`で読み込む進め方です。

図解すると下記のようになります

```mermaid
flowchart LR
    subgraph PowerApps["Power Apps"]
        UI["ユーザーインターフェース"]
        colChat["ローカルコレクション\ncolChat"]
    end

    subgraph PowerAutomate["Power Automate"]
        GetFlow["取得フロー"]
        PostFlow["新規作成、更新フロー"]
    end

    subgraph JSON["JSONファイル"]
        MessageData["メッセージデータ、チャット履歴、メタデータ"]
    end

    UI -->|"1. チャット選択\nlocMessageID"| GetFlow
    GetFlow -->|"2. チャット履歴取得"| MessageData
    GetFlow -->|"3. 取得データセット"| colChat
    colChat -->|"4. メッセージ表示"| UI
    
    UI -->|"5. メッセージ送信"| colChat
    colChat -->|"6. 保存要求"| PostFlow
    PostFlow -->|"7. データ保存"| MessageData

    classDef powerApps fill:#742774,color:#fff,stroke:#fff
    classDef powerAutomate fill:#0066FF,color:#fff,stroke:#fff
    classDef json fill:#4a4a4a,color:#fff,stroke:#fff
    
    class UI,colChat powerApps
    class GetFlow,PostFlow powerAutomate
    class MessageData json
```

あらためて必要な項目に目を向けてみます。
下記のような項目を挙げて進めてみることにします。

 - `MessageID`: 参照用に設けるGUID列
 - `LastMessageText`: 最後に投稿されたメッセージ
 - `LastMessageTime`: 最後に投稿された日時
 - `LastSender`: 最後に投稿したユーザー名
 - `MessageCount`: メッセージ数
 - `CreatedBy`: チャットを作成したユーザー名
 - `CreatedTime`: 作成日時
 - `UpdatedTime`: 更新日時
 - `Subject`: 会話タイトル
     - `MessageIndex`: メッセージの順番
     - `SenderMailAddress`: 送信元のメールアドレス
     - `SenderName`: 送信元のユーザー
     - `TimeStamp`: 送信日時
 
上記のうち、第一階層である`LastMessageText` ~ `Subject`までは、`SharePoint ドキュメント ライブラリ`の機能を活用し、メタデータとして列を設けます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/e7617c06-ecf0-b8f7-393d-df54e615cadb.png)

`CreatedTime`や`UpdatedTime`はシステム列と内容がかぶりますが、メタデータとして今回は追加します。

| 列名              | データ型                 |
|--------------------|--------------------------|
| MessageID         | 1 行テキスト             |
| LastMessageText   | 1 行テキスト             |
| LastMessageTime   | 日付と時刻               |
| LastSender        | 1 行テキスト             |
| MessageCount      | 数値                     |
| CreatedBy         | 1 行テキスト             |
| CreatedTime       | 日付と時刻               |
| UpdatedTime       | 日付と時刻               |
| Subject           | 1 行テキスト             |

そして、このメタデータが付記された`JSON`ファイルに、下記の値を追加していきます。

```json: 読み取り、書き込み対象のJSON ファイル
{
  "Log": [
    {
      "MessageIndex": "メッセージの順番",
      "SenderMailAddress": "送信元のメールアドレス",
      "SenderName": "送信元のユーザー",
      "TimeStamp": "送信日時"
    },
    {
      "MessageIndex": "メッセージの順番",
      "SenderMailAddress": "送信元のメールアドレス",
      "SenderName": "送信元のユーザー",
      "TimeStamp": "送信日時"
    } // 末尾に値を追加していくイメージ
  ]
}
```

:::note info
海外のMVPのYouTubeを見ていると[ParseJSON 関数](https://learn.microsoft.com/ja-jp/power-platform/power-fx/reference/function-parsejson)を使った大規模データ対応を見かけます。
それであれば、書き込み先そのものが`JSON`でいいのでは🧐という思想に基づいて作成しています。
:::


# アプリの機能
それでは`Power Apps`の解説にうつります。このアプリのサイドパネルには、以下の項目が表示しています。

- 自分自身の名前  
- チャットの履歴  
- チャット相手との詳細なやり取り  

複数のチャットの履歴は、`SharePoint`の`ドキュメント ライブラリ`をデータソースにします。
ここの`JSON ファイル`が表示されるイメージです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/c6f68e4e-df74-eebf-23c1-0d01540f9cfb.png)

このギャラリー コントロールがクリックされた際に、`JSON`ファイル コンテンツを読み込む処理を走らせます。

```yaml: OnSelect
// ローカル変数に一意の値であるMessageIDを保持する
UpdateContext({locMessageID: ThisItem.MessageID});

// JSONファイルコンテンツをパースしてテーブルに変換する
ClearCollect(
    colChat,
    ForAll(
        Table(ParseJSON('get-json-content'.Run(locMessageID).response).Log) As Result,
        {
            MessageIndex: Value(Result.Value.MessageIndex),
            MessageText: Text(Result.Value.MessageText),
            SenderName: Text(Result.Value.SenderName),
            TimeStamp: DateTimeValue(Result.Value.TimeStamp)
        }
    )
);

```

## チャットの内容
メッセージボックス、つまりチャット内容のやり取りについては、以下の関数を使用して、`HTML文字列`で表現します。

```javascript:HtmlText
With(
    {
        avatarStyle: "width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 14px;",
        messageWrapperBase: "max-width: 70%; margin-bottom: 16px; display: flex; gap: 8px;",
        messageBubbleBase: "padding: 8px 12px; border-radius: 8px; box-shadow: 0 1px 2px rgba(0,0,0,0.1);",
        nameStyle: "font-size: 12px; color: #666666; margin-bottom: 4px;",
        timeStyle: "font-size: 11px; color: #666666; text-align: right;"
    },
    Switch(
        Mod(ThisItem.MessageIndex, 2),
        0,
        $"<div style='{messageWrapperBase} margin-right: auto;'>
            <div style='{avatarStyle} background-color: #e0e0e0;'>
                {Left(ThisItem.SenderName, 2)}
            </div>
            <div>
                <div style='{nameStyle}'>{ThisItem.SenderName}</div>
                <div style='{messageBubbleBase} background-color: #ffffff;'>
                    <div style='margin-bottom: 4px;'>{ThisItem.MessageText}</div>
                    <div style='{timeStyle}'>{ThisItem.TimeStamp}</div>
                </div>
            </div>
        </div>",
        1,
        $"<div style='{messageWrapperBase} margin-left: auto; flex-direction: row-reverse;'>
            <div style='{avatarStyle} background-color: #128C7E; color: white;'>Me</div>
            <div>
                <div style='{nameStyle} text-align: right;'>自分</div>
                <div style='{messageBubbleBase} background-color: #dcf8c6;'>
                    <div style='margin-bottom: 4px;'>{ThisItem.MessageText}</div>
                    <div style='{timeStyle}'>{ThisItem.TimeStamp}</div>
                </div>
            </div>
        </div>"
    )
)
```

登場する関数は、下記の三つです。

- [Switch 関数](https://learn.microsoft.com/ja-jp/power-platform/power-fx/reference/function-if)  
- [If 関数](https://learn.microsoft.com/ja-jp/power-platform/power-fx/reference/function-if)  
- [With 関数](https://learn.microsoft.com/ja-jp/power-platform/power-fx/reference/function-with)

https://learn.microsoft.com/ja-jp/power-platform/power-fx/reference/function-if

https://learn.microsoft.com/ja-jp/power-platform/power-fx/reference/function-with

これらを組み合わせ、HTML文字列を用いた高さの自動調整機能や伸縮可能なギャラリーを活用することで、動的なデザインを実現されます。

まったくローコードではないですね。`AI`の力を借りないと作成できません。

なぜ`HTML文字列`を採用しているのか、理由はコントロール数が抑えられることにあります。
`Power Apps キャンバスアプリ`に設定できる[コントロール数には限りがあります。](https://learn.microsoft.com/ja-jp/power-apps/maker/canvas-apps/performance-tips#%E3%82%B3%E3%83%B3%E3%83%88%E3%83%AD%E3%83%BC%E3%83%AB%E6%95%B0%E3%81%AE%E5%88%B6%E9%99%90)
デザインにこだわればこだわるほど、コントロール数は増加傾向にあり、自分のやりたいことと離れる可能性があります。

https://learn.microsoft.com/ja-jp/power-apps/maker/canvas-apps/performance-tips#%E3%82%B3%E3%83%B3%E3%83%88%E3%83%AD%E3%83%BC%E3%83%AB%E6%95%B0%E3%81%AE%E5%88%B6%E9%99%90

AI黎明期の今、`Copilot in Edge`でも上記のような関数は提案してくださいます。
ぜひ活用を検討してみてください。

### チャットの投稿パネル
チャットの投稿用に`テキスト入力`と`送信用のボタン`を設けます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/3cd2ff4a-0765-93f3-16cc-fa91a892e8af.png)

こちらに後述する`Power Automate`のフローを追加します。

```javascript:OnSelect
// コレクションのアイテムの数で、処理を分ける
If(
    CountRows(colChat) = 0,
    // 初回投稿
    ClearCollect(
        colChat,
        {
            MessageIndex: 1,
            SenderName: Office365ユーザー.MyProfileV2().displayName,
            MessageText: inpMessageText.Text,
            TimeStamp: Now()
        }
    );
    Collect(
        colChat,
        {
            MessageIndex: (CountRows(colChat) + 1),
            SenderName: "AI Builder",
            MessageText: 'AI Reply'.Predict(inpMessageText.Text).Text,
            TimeStamp: Now()
        }
    );
    // AI Builderのカスタムプロンプトでタイトルを作成する
    UpdateContext({locAiResponse: 'Custom-prompt-response'.Predict(inpMessageText.Text).StructuredOutput});
    ,
    // 初回投稿ではないときは、自分の投稿とAI Replyの値を設定する
    Collect(
        colChat,
        {
            MessageIndex: (CountRows(colChat) + 1),
            SenderName: Office365ユーザー.MyProfileV2().displayName,
            MessageText: inpMessageText.Text,
            TimeStamp: Now()
        }
    );
    Collect(
        colChat,
        {
            MessageIndex: (CountRows(colChat) + 1),
            SenderName: "AI Builder",
            MessageText: 'AI Reply'.Predict(inpMessageText.Text).Text,
            TimeStamp: Now()
        }
    );
    
);
// 初回の投稿と最後の投稿をWith関数に格納
With(
    {
        FirstPost: First(colChat),
        LastPost: Last(colChat)
    },
    // JSON文字列用にオブジェクトを設定
    UpdateContext(
        {
            spMetaData: {
                MessageID: Coalesce(
                    locMessageID,
                    Text(GUID())
                ),
                LastMessageText: LastPost.MessageText,
                LastMessageTime: LastPost.TimeStamp,
                LastSender: LastPost.SenderName,
                MessageCount: CountRows(colChat),
                CreateBy: FirstPost.SenderName,
                CreatedTime: FirstPost.TimeStamp,
                UpdatedTime: LastPost.TimeStamp,
                Subject: Coalesce(
                    locAiResponse.response,
                    galChatList.Selected.Subject
                )
            }
        }
    )
);
// Power Automateを実施、初回データ作成時用にMessageIdを戻す
UpdateContext(
    {
        locMessageID: 'post-json-chat'.Run(
            JSON(spMetaData),
            JSON(
                {Log: Table(colChat)},
                JSONFormat.Compact
            )
        ).messageid
    }
);
// テキスト入力をクリアする
Reset(inpMessageText);

// Filter関数で引数に設定しているギャラリーコントロール 最新化用の値
UpdateContext({locFilter: false});
UpdateContext({locFilter: true});

```

後述しますが、JSON ファイルの中身は、`Power Apps`から都度設定し、Power Automateに送る流れです。

[JSON 関数](https://learn.microsoft.com/ja-jp/power-platform/power-fx/reference/function-json)を使って、チャットのやり取りを構造化された文字列に変換しましょう。

https://learn.microsoft.com/ja-jp/power-platform/power-fx/reference/function-json

:::note info
JSON ファイルにユーザーを表すプロパティを用意しています。ユーザー列に該当する`SenderMailAddress`と`SenderName`を文字列に設定しているのは、プロフィールの画像データなど不要な列を含めてしまうことを避けるためです。
:::

## AI Builderの活用
この関数の中で`AI Builder`を利用しています。
まずチャットの返信は[AIReply](https://learn.microsoft.com/ja-jp/power-platform/power-fx/reference/function-ai)で実施しています。

https://learn.microsoft.com/ja-jp/power-platform/power-fx/reference/function-ai

`GPT`による返答の恩恵が受けられます。
単純に応答を返すだけの関数ですね。

またチャットの要約、タイトルを設定するために`カスタム プロンプト`を利用します。

**AI ハブ**から簡単にJSONで構造化された応答を返すことが可能です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/75f245f7-11d4-dc72-bde3-b711166dd32a.png)

https://learn.microsoft.com/ja-jp/power-platform/release-plan/2024wave2/ai-builder/define-specific-output-formats-prompt-builder

[プロンプト ビルダーを使用して JSON で GPT の出力形式を定義](https://learn.microsoft.com/ja-jp/power-platform/release-plan/2024wave2/ai-builder/define-specific-output-formats-prompt-builder)します。

```plaintext: プロンプト
下記はチャットを開始するときの文言です。内容から会話の内容を決定し、20文字ほどの一文に要約してください。また要約した内容からチャットのアイコンを作成します。16進法で文字色と背景色を提案してください。
 - 文章は一文でまとめてください
 - 体言止めを用いてください
 - 句読点は出力に含めないでください

### チャット
{引数のチャット} 
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/cc79d715-886b-a4dd-d405-2eada8236f66.png)

引数は右上の画面から設定します。
テキストだけではなく、`画像`や`データソース`の利用も可能です。

出力は`JSON`を指定して

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/323219f0-5a55-1528-efa9-5848bdf7a319.png)

下記のように加工しやすい形に変換します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/4c170ce6-c1eb-d0f6-ea81-44d1b13d9114.png)

戻り値が構造的になるため、非常に便利です。
こちらの詳細は[Microsoft learn](https://learn.microsoft.com/ja-jp/ai-builder/use-a-custom-prompt-in-app)に譲らせてください。

https://learn.microsoft.com/ja-jp/ai-builder/use-a-custom-prompt-in-app

# ここから`Power Automate`の解説といきたいところですが・・・

ここまでで結構長くなってしまいました。

Power Automateは次回投稿に内容を譲ることにします。
お楽しみに！