Technology Blog Posts by SAP
cancel
Showing results for 
Search instead for 
Did you mean: 
Ryota_Ito
Product and Topic Expert
Product and Topic Expert
742

生成 AI は研究室の外へ飛び出し、今やビジネス現場の常識を塗り替えています。SAPは全速力でその波に乗っています。このブログシリーズでは、SAP AI Core の既定モデルを最速で呼び出し、実務で使える AI エージェントへ拡張する“秒速ハンズオン”をお届けします。

お知らせ
You can find the English version here.

 

📖 本シリーズで学べること

  • SAP AI Core 上でカスタム AI Agent を “秒速” で動かす方法
  • LangChain・Google 検索ツール・RAGを使った実装
  • AI Agent を REST API 化し、SAPUI5/Fiori の UI に載せ、Cloud Foundryにデプロイする手順

学習時間
各Part は 10–15 分 で読める&手を動かせるボリュームを予定しています。

 

🗺️ 連載ロードマップ

この記事がお役に立ったら、ぜひ Kudos を押していただけると励みになります。 「ここをもっと詳しく知りたい」「別のテーマも取り上げてほしい」など、ご要望があればお気軽にコメントください!


SAPUI5 でチャットUI を構築

1 | はじめに

今回のPart では、前回までに構築した AI Agent を、SAP UI5/Fiori ベースのチャット UI から呼び出せるようにします。

SAP UI5 は SAP が提供する UI フレームワークであり、企業向けのモダンな Web アプリケーションを短時間で開発できる点が特徴です。特に SAP Business Application Studio(BAS) を利用すると、Fiori 用テンプレートからプロジェクト構成を自動生成できるため、ディレクトリや設定ファイルの手動作成が不要になります。

 

2 | 事前準備

  • BTP サブアカウント
  • SAP AI Core インスタンス
  • SAP AI Launchpadのサブスクリプション
  • Python 3.13環境 & pip
  • VSCodeやBASなどのIDE

Trial 環境の注意
Trial の HANA Cloud インスタンスは 毎晩自動停止 されます。日をまたぐ作業の場合は翌日インスタンスを再起動してください

 

3 | Fiori アプリケーションの準備

SAP 系 UI 開発では、BAS のテンプレート機能の利用を強く推奨します。テンプレートから生成されるフォルダ構成と設定ファイルをベースにすることで、Fiori アプリ固有のマニフェスト(mta.yaml)やモジュール構成を素早く整備できます。

 

BAS で Create Dev Space をクリックし、テンプレートに SAP Fiori を選択します。併せて前回に作成した Python API をローカルで起動したいので、Additional SAP Extensions から Python Tools を選択します。

Screenshot 2025-06-10 at 16.58.38.png

左上のハンバーガーメニュー → File > New Project From Template を選択します。

Screenshot 2025-06-07 at 15.54.13.png

Template Selection は Basic を選択します。

Screenshot 2025-06-07 at 15.53.57.png

Data Source and Service Selection では、UI に表示するデータは Python API から取得するため None を選択します。

Screenshot 2025-06-07 at 15.55.09.png

Entity Selection で Entity name に ChatEntity と入力します。

Screenshot 2025-06-07 at 15.57.51.png

Project Attributes を以下のように設定し、Add deployment configurationYes にします。

項目
Module Namemy-ai-agent-ui
Application TitleMy AI Chat
DescriptionChat UI for AI Agent

Screenshot 2025-06-11 at 13.59.53.png

Deployment Configuration では次のように設定します。

項目
TargetCloud Foundry
DestinationNone
Add Router ModuleAdd Application to Managed Application Router

Screenshot 2025-06-11 at 14.00.18.png

まれに Target を CF にしても Router Module のオプションが表示されないことがあります。その場合は、一度 TargetABAP に切り替えてから Cloud Foundry に戻すと正しく表示されます。

 

最後に、Python API の配置と動作確認を実施します。構成は次のようになります。

# フォルダ構成
PROJECTS
├── my-ai-agent-ui/
├── my-ai-agent-api/
    ├── main.py
    ├── requirements.txt
    └── .env

my-ai-agent-api 配下で仮想環境を作成・有効化し、以下のコマンドでローカルサーバが起動するか確認します。

gunicorn -w 1 -k uvicorn.workers.UvicornWorker main:app --bind 0.0.0.0:${PORT:-8000}

これで、Fioriアプリケーションの構築準備が整いました!

 

4 | Fiori アプリケーションの改良

生成された Fiori プロジェクトをベースに、チャット UI が AI Agent と通信できるようにコードを調整していきましょう。

まずコントローラー(my-ai-agent-ui/webapp/controller/ChatEntity.controller.js)を以下のように書きます。

sap.ui.define([
  "sap/ui/core/mvc/Controller",
  "sap/ui/model/json/JSONModel",
  "sap/m/MessageToast",
  "sap/ui/core/BusyIndicator"
], function (Controller, JSONModel, MessageToast, BusyIndicator) {
  "use strict";

  // 環境によってエンドポイントを切り替え
  const ENDPOINT = (
    ["localhost", "applicationstudio"].some(h => window.location.hostname.includes(h)) ||
    window.location.port === "8080"
  ) ? "" : "https://my-ai-agent-api-relaxed-raven-ie.cfapps.us10-001.hana.ondemand.com";

  return Controller.extend("myaiagentui.controller.ChatEntity", {
    /** 初期化 */
    onInit() {
      this.getView().setModel(new JSONModel({
        busy: false,
        txtInput: "",
        uploadedFiles: [],
        messages: [{
          role: "assistant",
          content: "こんにちは!私はチャットボットです。何かお手伝いできることはありますか?",
          hasThinkingProcess: false
        }]
      }), "ui");

      this.byId("fileUploader")?.setUploadUrl(`${ENDPOINT}/agent/upload`);
    },

    /** チャット履歴を初期化 */
    onClearChatPress() {
      this.getView().getModel("ui").setProperty("/messages", [{
        role: "assistant",
        content: "こんにちは!私はチャットボットです。何かお手伝いできることはありますか?",
        hasThinkingProcess: false
      }]);
      MessageToast.show("チャットをクリアしました");
    },

    /** メッセージ送信 */
    async onBtnChatbotSendPress(){
      const ui=this.getView().getModel("ui");
      const input=(ui.getProperty("/txtInput")||"").trim();
      if(!input) return;
      const msgs=ui.getProperty("/messages");
      msgs.push({role:"user", content:input});
      ui.setProperty("/messages", msgs);
      ui.setProperty("/txtInput", "");
      ui.setProperty("/busy", true);
      try{
        const {output, intermediate_steps=[]}=await this._apiChatCompletion(input);
        msgs.push({role:"assistant", content:output, hasThinkingProcess:Boolean(intermediate_steps.length), thinkingProcess:intermediate_steps.map((s,i)=>({...s, stepIndex:i+1, observationTruncated:s.observation?.slice(0,100)+(s.observation?.length>100?"...":""), observationFull:s.observation, isObservationExpanded:false, hasLongObservation:(s.observation?.length||0)>100})), isExpanded:false});
        ui.setProperty("/messages", msgs);
        this._scrollToBottom();
      }catch(err){
        console.error(err);
        MessageToast.show(`エラー: ${err.message}`);
      }finally{
        ui.setProperty("/busy", false);
      }
    },

    /** 思考プロセス表示切替 */
    onToggleThinkingProcess(oEvent) {
      const ctx = oEvent.getSource()?.getBindingContext("ui");
      if (ctx) this._toggleFlag(ctx, "isExpanded");
    },    

    /** Observation 表示切替 */
    onToggleObservation(oEvent) {
      const ctx = oEvent.getSource()?.getBindingContext("ui");
      if (ctx) this._toggleFlag(ctx, "isObservationExpanded");
    },

    /** 任意フラグを反転 */
    _toggleFlag(ctx, flag) {
      const ui = this.getView().getModel("ui");
      const path = ctx.getPath();
      ui.setProperty(`${path}/${flag}`, !ui.getProperty(`${path}/${flag}`));
    },

    /** チャットを最下部へスクロール */
    _scrollToBottom() {
      setTimeout(() => {
        const sc = this.byId("chatScrollContainer");
        const items = sc?.getContent()[0].getItems();
        if (items?.length) sc.scrollToElement(items[items.length - 1]);
      }, 100);
    },

    /** AI チャット API 呼び出し */
    async _apiChatCompletion(query) {
      const res = await fetch(`${ENDPOINT}/agent/chat`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ query })
      });
      if (!res.ok) throw new Error(`サーバーエラー (${res.status}): ${await res.text()}`);
      return res.json();
    }
  });
});

 

次に View(my-ai-agent-ui/webapp/view/ChatEntity.view.xml)を以下のように変更します。

<mvc:View controllerName="myaiagentui.controller.ChatEntity"
    xmlns:core="sap.ui.core"
    xmlns:mvc="sap.ui.core.mvc"
    xmlns:f="sap.f"
    xmlns:u="sap.ui.unified"
    displayBlock="true"
    xmlns="sap.m">

    <f:DynamicPage id="mainDynamicPage" stickySubheaderProvider="iconTabBar" class="sapUiNoContentPadding">
        <f:title>
            <f:DynamicPageTitle id="mainDynamicTitle">
                <f:heading>
                    <Title id="pageTitle" text="AI チャット with Fiori"/>
                </f:heading>
                <f:actions>
                    <Button id="clearChatButton" 
                            text="チャットをクリア" 
                            icon="sap-icon://refresh"
                            type="Emphasized"
                            press="onClearChatPress"/>
                </f:actions>
            </f:DynamicPageTitle>
        </f:title>

        <f:content>
            <!-- チャット UI メインエリア -->
            <VBox id="chatMainAreaBox" class="sapUiNoContentPadding chatMainArea">
                <IconTabBar id="iconTabBar"
                            class="sapUiNoContentPadding"
                            stretchContentHeight="true"
                            expanded="true"
                            expandable="false">

                    <items>
                        <IconTabFilter id="chatbotTab" text="チャットボット" class="sapUiNoContentPadding">
                            <VBox id="chatContainerBox" class="chatContainer sapUiNoContentPadding">
                                <!-- チャットメッセージエリア -->
                                <ScrollContainer id="chatScrollContainer"
                                               height="100%" 
                                               width="100%" 
                                               vertical="true" 
                                               focusable="true"
                                               class="chatScrollContainer">
                                    <VBox id="chatMessagesBox" items="{ui>/messages}" class="chatMessagesContainer">
                                        <VBox id="messageItemBox">
                                            <!-- assistant コメント -->
                                            <VBox id="assistantMessageVBox" visible="{= ${ui>role} === 'assistant'}"
                                                  class="chatMessageWrapper">
                                                <HBox id="assistantMessageHBox" direction="Row" class="chatMessageRow">
                                                    <HBox id="assistantMessageHBox1" backgroundDesign="Solid"
                                                          class="chatBubbleAssistant">
                                                        <core:Icon id="assistantMessageIcon" decorative="true"
                                                                   src="sap-icon://ai"
                                                                   class="chatIcon"/>
                                                        <FormattedText id="assistantMessageFormattedText" htmlText="{ui>content}"/>
                                                    </HBox>
                                                </HBox>
                                                
                                                <!-- 思考プロセスアコーディオン -->
                                                <HBox id="thinkingToggleBox" visible="{ui>hasThinkingProcess}" 
                                                      class="thinkingProcessContainer">
                                                    <Button id="thinkingToggleButton" icon="{= ${ui>isExpanded} ? 'sap-icon://collapse' : 'sap-icon://expand' }"
                                                            text="思考プロセスを{= ${ui>isExpanded} ? '隠す' : '表示' }"
                                                            type="Transparent"
                                                            press="onToggleThinkingProcess"
                                                            class="thinkingProcessToggle"/>
                                                </HBox>
                                                
                                                <VBox id="thinkingVBox" visible="{= ${ui>isExpanded} &amp;&amp; ${ui>hasThinkingProcess} }"
                                                      class="thinkingProcessContent">
                                                    <VBox id="thinkingVBox1" items="{ui>thinkingProcess}">
                                                        <VBox id="thinkingStepBox" class="thinkingStepBox">
                                                            <Title id="thinkingStepTitle"
                                                                   text="{= 'Step ' + ${ui>stepIndex} }" 
                                                                   level="H5" 
                                                                   class="thinkingStepTitle"/>
                                                        <VBox id="thinkingStepContent" class="thinkingStepContent">
                                                            <HBox id="thinkingStepThought" class="thinkingStepItem">
                                                                <Label id="ThoughtLabel" text="Thought:" design="Bold" class="thinkingLabel"/>
                                                                <Text id="ThoughtText" text="{ui>thought}" wrapping="true" class="thinkingText"/>
                                                            </HBox>
                                                            <HBox id="thinkingStepAction" class="thinkingStepItem">
                                                                <Label id="ActionLabel" text="Action:" design="Bold" class="thinkingLabel"/>
                                                                <Text id="ActionText" text="{ui>action}" class="thinkingText"/>
                                                            </HBox>
                                                            <HBox id="thinkingStepInput" class="thinkingStepItem">
                                                                <Label id="InputLabel" text="Input:" design="Bold" class="thinkingLabel"/>
                                                                <Text id="InputText" text="{ui>action_input}" class="thinkingText"/>
                                                            </HBox>
                                                            <HBox id="thinkingStepObservation" class="thinkingStepItem">
                                                                <Label id="ObservationLabel" text="Observation:" design="Bold" class="thinkingLabel"/>
                                                                <VBox id="ObservationVBox" class="thinkingText">
                                                                    <Text id="ObservationText" text="{= ${ui>isObservationExpanded} ? ${ui>observationFull} : ${ui>observationTruncated} }" 
                                                                          wrapping="true"/>
                                                                    <Link id="ObservationLink"
                                                                          text="{= ${ui>isObservationExpanded} ? '折りたたむ' : '続きを読む' }"
                                                                          visible="{ui>hasLongObservation}"
                                                                          press="onToggleObservation"
                                                                          class="observationToggleLink"/>
                                                                </VBox>
                                                            </HBox>
                                                        </VBox>
                                                    </VBox>
                                                    </VBox>
                                                </VBox>
                                            </VBox>

                                            <!-- user コメント -->
                                            <HBox id="UserComment"
                                                  visible="{= ${ui>role} === 'user'}"
                                                  direction="RowReverse"
                                                  class="chatMessageRow">
                                                <HBox id="UserCommentBox"
                                                      direction="RowReverse"
                                                      backgroundDesign="Solid"
                                                      class="chatBubbleUser sapThemeBrand-asBackgroundColor">
                                                    <core:Icon id="UserCommentIcon"
                                                               decorative="true"
                                                               src="sap-icon://customer"
                                                               class="chatIcon sapThemeTextInverted"/>
                                                    <Text id="UserCommentText" class="sapThemeTextInverted" text="{ui>content}"/>
                                                </HBox>
                                            </HBox>
                                        </VBox>
                                    </VBox>
                                </ScrollContainer>

                                <!-- 入力欄+送信ボタン -->
                                <HBox id="chatInputAreaBox" class="chatInputArea">
                                    <TextArea id="chatInput" value="{ui>/txtInput}"
                                              width="100%"
                                              growing="true"
                                              placeholder="メッセージを入力してください..."
                                              editable="{= !${ui>/busy}}"
                                              busyIndicatorDelay="0"
                                              class="chatTextArea">
                                        <layoutData>
                                            <FlexItemData id="InputData" growFactor="1"/>
                                        </layoutData>
                                    </TextArea>

                                    <Button id="sendButton" class="chatSendButton"
                                            type="Emphasized"
                                            icon="sap-icon://paper-plane"
                                            text="送信"
                                            press="onBtnChatbotSendPress"
                                            busy="{ui>/busy}"
                                            busyIndicatorDelay="0"/>
                                </HBox>
                            </VBox>
                        </IconTabFilter>
                    </items>
                </IconTabBar>
            </VBox>
        </f:content>
    </f:DynamicPage>
</mvc:View>

 

さらに、UI の詳細なデザイン設定として、スタイル(my-ai-agent-ui/webapp/css/style.css)を設定します。

/* チャットメインエリア */
.chatMainArea {
    height: 100vh;
    min-height: 300px;
}

/* チャットコンテナ */
.chatContainer {
    display: flex;
    flex-direction: column;
    height: calc(90vh - 100px);
    padding: 0;
}

/* チャットスクロールコンテナ */
.chatScrollContainer {
    flex: 1;
    min-height: 300px;
    background-color: #f8f9fa;
    border: none;
    border-radius: 0;
    padding: 0;
    margin: 0;
}

/* チャットメッセージコンテナ */
.chatMessagesContainer {
    padding: 1rem;
}

/* チャットメッセージ行 */
.chatMessageRow {
    width: 100%;
    margin-bottom: 0.5rem;
}

/* チャットメッセージラッパー */
.chatMessageWrapper {
    margin-bottom: 1rem;
}

/* チャットコメント - アシスタント */
.chatBubbleAssistant {
    max-width: 70%;
    min-width: 100px;
    background-color: #ffffff;
    border: 1px solid #d0d7de;
    border-radius: 18px;
    padding: 12px 16px;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
    align-items: flex-start;
}

.chatBubbleAssistant .sapMFormattedText {
    word-wrap: break-word;
    line-height: 1.4;
}

/* チャットコメント - ユーザー */
.chatBubbleUser {
    max-width: 70%;
    min-width: 100px;
    border-radius: 18px;
    padding: 12px 16px;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
    align-items: flex-start;
}

.chatBubbleUser .sapMText {
    word-wrap: break-word;
    line-height: 1.4;
}

/* 思考プロセスコンテナ */
.thinkingProcessContainer {
    margin-top: 0.5rem;
    margin-left: 2.5rem;
}

/* 思考プロセストグルボタン */
.thinkingProcessToggle {
    font-size: 0.875rem;
    color: #0073e6;
}

.thinkingProcessToggle:hover {
    background-color: rgba(0, 115, 230, 0.08) !important;
}

/* 思考プロセスコンテンツ */
.thinkingProcessContent {
    margin-left: 2.5rem;
    margin-top: 0.5rem;
    max-width: 70%;
}

/* 思考ステップボックス */
.thinkingStepBox {
    margin-bottom: 1.5rem;
    padding: 1rem;
    background-color: #f5f7fa;
    border: 1px solid #e4e7ea;
    border-radius: 8px;
}

.thinkingStepBox:last-child {
    margin-bottom: 0;
}

/* 思考ステップタイトル */
.thinkingStepTitle {
    color: #0a6ed1;
    margin-bottom: 0.75rem;
    font-size: 1rem;
}

/* 思考ステップコンテンツ */
.thinkingStepContent {
    padding-left: 0.5rem;
}

/* 思考ステップ項目 */
.thinkingStepItem {
    margin-bottom: 0.5rem;
    align-items: flex-start;
}

.thinkingStepItem:last-child {
    margin-bottom: 0;
}

/* 思考ラベル */
.thinkingLabel {
    min-width: 100px;
    margin-right: 1rem;
    color: #32363a;
    font-size: 0.875rem;
}

/* 思考テキスト */
.thinkingText {
    flex: 1;
    font-size: 0.875rem;
    color: #515456;
    line-height: 1.5;
}

/* Observation続きを読むリンク */
.observationToggleLink {
    margin-top: 0.25rem;
    font-size: 0.875rem;
}

/* 入力エリア */
.chatInputArea {
    background-color: #ffffff;
    padding: 1rem;
    border-top: 1px solid #e4e7ea;
    align-items: flex-end;
    flex-shrink: 0;
}

/* テキストエリアのスタイル調整 */
.chatTextArea .sapMTextAreaInner {
    border: 2px solid #e4e7ea;
    background-color: #ffffff;
}

.chatTextArea .sapMTextAreaInner:focus-within {
    border-color: #0073e6;
}

/* 送信ボタン */
.chatSendButton {
    min-height: 40px;
    border-radius: 20px;
    margin-left: 0.5rem;
}

/* アイコン調整 */
.chatIcon {
    font-size: 1.2rem;
    margin-top: 2px;
}

.chatBubbleAssistant .chatIcon {
    margin-right: 0.5rem;
}

.chatBubbleUser .chatIcon {
    margin-left: 0.5rem;
}

/* IconTabBar のパディング削除 */
.sapMITB {
    padding: 0;
}

.sapMITBContent {
    padding: 0;
    background-color: transparent;
}

/* レスポンシブ対応 */
@media (max-width: 768px) {
    .chatBubbleAssistant,
    .chatBubbleUser {
        max-width: 85%;
    }
    
    .chatContainer {
        height: calc(100vh - 80px);
    }
    
    .chatScrollContainer {
        min-height: 300px;
    }
    
    .chatSendButton .sapMBtnInner .sapMBtnContent {
        font-size: 0;
    }
    
    .chatSendButton .sapMBtnIcon {
        margin-right: 0;
    }
    
    .thinkingProcessContent {
        max-width: 85%;
    }
    
    .thinkingLabel {
        min-width: 80px;
        margin-right: 0.5rem;
    }
}

 

チャット UI から Python API へリクエストを転送するため、プロキシミドルウェアを追加します。まず依存関係をインストールしましょう。

npm install ui5-middleware-simpleproxy --save-dev

 

そして my-ai-agent-ui/package.json に次の設定を追記してください。

...
  "main": "webapp/index.html",
  "devDependencies": {
   (省略)
  },
  /* ここから
  "ui5": {
  "dependencies": [
    "ui5-middleware-simpleproxy"
    ]
  },
 ここまで */
  "scripts": {
....

これでチャット UI からの /api リクエストがローカルの Python API へ転送されるようになります。

 

5 | Fiori アプリケーションのテスト

ローカル環境でアプリと API を同時に起動し、実際にチャット機能が動作するか確認しましょう。

まず、my-ai-agent-api ディレクトリで以下のコマンドを実行して Python API を起動します。

gunicorn -w 1 -k uvicorn.workers.UvicornWorker main:app --bind 0.0.0.0:${PORT:-8000}

また、別のターミナルを開き、my-ai-agent-ui のルートで次のコマンドを実行します。

npm start

Screenshot 2025-06-12 at 21.19.19.png

 

ログに表示された URL(通常は http://localhost:8080)をブラウザで開き、チャット入力欄にメッセージを送ってみてください。AI エージェントから返答が表示されれば成功です!

Screenshot 2025-06-12 at 21.22.04.png

 

チャット画面の チャットをクリア ボタンをクリックすると履歴がリセットされ、「チャットをクリアしました」と確認できます。新しい会話を問題なく始められるか確認しましょう!

Screenshot 2025-06-12 at 21.22.42.png

 

6 | チャレンジ – テキストファイルアップロードボタンを追加しよう

テキストを追加でアップロードして、追加したドキュメントを踏まえた AI Agent とのやりとりを実現して見ましょう。なお、このチャレンジは前回のチャレンジを実施していないと実装できないので注意しましょう。

チャット UI のヘッダーに .txt ファイル専用のアップロードボタン を追加し、選択したファイルを Python API へ送信し、サーバ側でファイルをチャンク化して保存するようにします。途中経過をわかりやすくするため、アップロード中は BusyIndicator で画面をブロックするようにしましょう。

 

ChatEntity.controller.js で次の 3 点を追加します(パスや名前空間は環境に合わせてください):

  • 冒頭の sap/ui/core/BusyIndicator インポート
  • onFileChange ハンドラ – ファイル種別チェック & アップロード呼び出し
  • _uploadFile メソッド – fetch + FormData で API /agent/upload へ POST
/** ファイル選択時 */
async onFileChange(oEvent) {
  const oFileUploader = oEvent.getSource();
  const file = (oEvent.getParameter("files") || [])[0];
  if (!file) return;
  if (!file.name.endsWith(".txt")) {
    MessageToast.show("txt形式のファイルのみアップロード可能です。");
    oFileUploader.clear();
    return;
  }
  await this._uploadFile(file);
  oFileUploader.clear();
},

/** ファイルアップロード */
async _uploadFile(file) {
  const ui = this.getView().getModel("ui");
  ui.setProperty("/busy", true);
  BusyIndicator.show(0);
  try {
    const fd = new FormData();
    fd.append("file", file);
    const res = await fetch(`${ENDPOINT}/agent/upload`, {
      method: "POST",
      body: fd
    });
    if (!res.ok) throw new Error(await res.text());
    const { filename, chunks_created } = await res.json();
    const files = ui.getProperty("/uploadedFiles") || [];
    ui.setProperty("/uploadedFiles", [...files, {
      name: filename,
      chunks: chunks_created,
      uploadedAt: new Date()
    }]);
    MessageToast.show(`"${filename}" をアップロードしました(${chunks_created} チャンク作成)`);
  } catch (err) {
    console.error(err);
    MessageToast.show(`アップロードエラー: ${err.message}`);
  } finally {
    ui.setProperty("/busy", false);
    BusyIndicator.hide();
  }
}

 

次に、ヘッダーへ FileUploader と Clear ボタンを並べます。FileUploader は buttonOnly="true" にして、アイコンボタンにするのがコツです。

<f:actions>
  <u:FileUploader id="fileUploader"
                  name="file"
                  placeholder="ファイルを選択..."
                  fileType="txt"
                  change="onFileChange"
                  buttonText=""
                  buttonOnly="true"
                  icon="sap-icon://upload"
                  class="fileUploaderButton" />
  <Button id="clearChatButton"
          text="チャットをクリア"
          icon="sap-icon://refresh"
          type="Emphasized"
          press="onClearChatPress" />
</f:actions>

 

さらにヘッダー下部に "アップロード済ファイル一覧" を出すことで、どのファイルが何チャンクに分割されたかを視覚化できます。

<HBox id="fileUploadAreaBox"
      visible="{= ${ui>/uploadedFiles}.length > 0}">
  <Text text="追加でアップロードしたファイル:" />
  <HBox items="{ui>/uploadedFiles}">
    <ObjectStatus text="{ui>name}" icon="sap-icon://document-text" />
  </HBox>
</HBox>

 

上級チャレンジ – リセットボタン

ファイルをアップロードしていくとベクトル DB がどんどん膨らみます。テスト用に データベースを一括リセット するボタンを追加してみましょう。

  • API 側: /agent/delete を実装(ヒント: Part4 における DB を初期化するコード)。
  • Controller: onDeletePress を実装し、/agent/delete を呼び出して UI のファイル一覧も空にする。
  • View: ヘッダーのアップロードボタンとクリアボタンの間に Delete ボタンを配置。

実装ヒント
UI Model の /uploadedFiles を空配列 [] にすると、ファイル一覧エリアが自動的に非表示になります。 

Screenshot 2025-06-13 at 11.32.45.png

 

7 | 次回予告

Part 8 CloudFoundry にデプロイ

Part 8 では、完成したアプリケーションを Cloud Foundry へデプロイします。UI は mta build → cf deploy、Python バックエンドは manifest.yaml を使って cf push ― 2 本立てで “秒速デプロイ” に挑戦します。お楽しみに!

 

免責事項

本ブログに記載された見解および意見はすべて私個人のものであり、私の個人的な立場で発信しています。SAP は本ブログの内容について一切の責任を負いません。