見出し画像

★過去の検証あ~かいぶ★Geminiは最高の開発パートナー!?。企画立案から開発まで実質半日。サーバーレスRAG風ボット 開発記。


序章:「半日プロトタイプ」チャレンジ…


「毎月最低一本。お客様の心を動かすデモアプリを開発する。」

わたしたちの部門が掲げたこのミッションを達成するためには、企画から実装までのスピードが命です。しかし、従来の開発フローでは、企画会議、要件定義、環境構築、コーディング...と、どんなに早くても数日はかかってしまいます。

そこで、今回私たちは「Geminiを最高の開発パートナー」として仲間に迎え、ミッションクリアにチャレンジしてみることにしました。
小さい会社なもんで…手の空いてるできる人間もいないしね苦笑

最終的に開発したアプリは「自社の専門知識をAIに覚えさせたい」というニーズに応える「あなたの専用 Q&Aナレッジボット」になりました。

最終成果:

  • アプリ名: The My AIボット(Q&Aナレッジボット即席生成アプリ)

  • コア技術: Gemini API

  • 開発工数: 実質半日(企画の壁打ち・コード生成・調整・検証まで)

  • 環境: サーバーレス(HTML + JavaScript)で完結

  • 機能: ファイルアップロードによる知識注入(簡易RAG)と、役割設定(ペルソナ)による回答品質の調整

本記事では、Gemini&わたしの共同企画・開発のリアルをまとめてみます。



本論1:企画立案編|Geminiを「優秀なシステムコンサルタント」として使う


スタートは、Geminiとの「壁打ち」です。

まず、Geminiに「優秀なシステムコンサルタント兼エンジニア」という役割(ペルソナ)を与え、実現可能な企画のアイデアを求めました。


究極の問い:「サーバーレス・3日工数・感動」を満たせるか?


私たちがGeminiにインプットした制約は以下の通りです。

  1. 工数は2〜3日程度(生成AIを活用してコード生成OK)。

  2. お客様に驚きや感動を与える企画。

  3. サーバー不要でHTML+JavaScriptで完結すること。

  4. LLMはGeminiを使うこと。

貴方は優秀なシステムコンサルタント兼エンジニアです。そのように振舞ってください。

私は毎月1本、お客様のDX推進イメージを高めるためのデモアプリを企画/開発しています。
今月分のネタを考えたいと思いますので、貴方の力を貸してください。

## 企画検討の前提

* 2~3日間程度の工数で開発できる(生成AIを活用してソースコード生成してよい)
* お客様に驚きや感動を与える企画。お客様ご自身のDX推進モチベーションを上げる。
* シンプルで面白い。
* UIも複雑ではなく操作しやすいアプリ
* できれば、サーバ不要でHTML+JavaScriptで実現できるアプリがよい。

## 当月の追加テーマ

* 生成AIを組み込んだアプリ。
* LLMにはGeminiを使う。APIはGoogle AI Studioから取得し無料で使う。
* お客様に生成AIの楽しさを理解してもらう。
* また自分の業務に生成AIを適用できるか、考えるきっかけとなるアプリ。

こんな感じです。3つ程度、アプリ企画を提示してください。


この制約に対し、Geminiは3つの企画案を提示してきました。その中から、今回は「業界特化型 Q&Aナレッジボット即席生成」を選択しました。

もちろんです。優秀なシステムコンサルタント兼エンジニアとして、お客様のDX推進を強力に後押しする、Geminiを活用したデモアプリの企画を3つご提案させていただきます。
お客様に「これならすぐにでも使える!」と感じていただけるよう、驚き、感動、シンプルさ、そして**サーバーレス(HTML+JavaScript)**での実装を前提に考案しました。

💡 Gemini活用デモアプリ企画 3選
企画名
概要
狙い(お客様への訴求ポイント)
開発難易度/工数目安
1. 業務フロー図 即時言語化&改善提案
ユーザーがアップロードした業務フロー図の画像やスクリーンショットを認識し、その内容を正確に言語化・構造化します。さらに、そのフロー図に対してAIによる改善点やDX推進のアイデアを提案します。
「AIは業務の『意味』を理解し、次の行動を促せる」ことを示し、現状業務の棚卸しと改善のスピードが劇的に上がることを実感していただきます。
低(Geminiのマルチモーダル能力と自然言語生成)/ 2日
2. 業界特化型 Q&Aナレッジボット即席生成
ユーザーがアップロードした自社のマニュアルや専門資料(PDF, テキストなど)の内容をGeminiに学習(プロンプトに組み込み)させ、その場で質問応答ボットを生成・利用できるアプリです。
「自社のナレッジが即座にAI化できる」感動体験を提供し、マニュアル作成・検索工数の削減や新人教育の効率化といった具体的な業務適用イメージを強く持っていただけます。
中(ファイル読み込みとプロンプトへのデータ挿入)/ 3日
3. パーソナライズド・未来の業務イメージジェネレーター
ユーザーが**「今の業務の悩み」や「理想とするDX後の姿」を簡単な自然言語で入力すると、Geminiがその情報を基に未来の業務風景を具体的に描写**(テキスト生成)し、それに合うイメージ画像(例: DALL-E 3などの外部APIまたはGeminiの画像生成能力を利用したキャプション生成)を提案します。
**「DX後の未来が具体的になる」ことで、DXへの抽象的な不安を解消し、『自分事』**として捉えていただくモチベーション向上に直結します。楽しさと驚きを提供できます。
低~中(自然言語処理と、結果を魅力的に見せる工夫)/ 2日


(注 ------長いので、この後ろは割愛)


企画採用の決め手:「専門性」と「即時性」への感動


採用を決めたポイントは、このアプリが提供する「感動体験」にあります。

  • 即時性: 自分のナレッジをアップロードして数秒で使えるようになる。

  • 専門性: 自分のナレッジに特化した質問に、正確に回答してくれる。

  • シンプルさ: サーバー構築不要。HTMLファイルを開くだけ。

この企画により、「あなたの知識やノウハウをAIの頭脳へ」というコアコンセプトを定義。開発プロセスへと進むことができました。



本論2:開発実践編|コード生成を命じる「プロンプト」もGeminiに考えさせる。


企画が固まれば、次は実装です。ここからはGeminiに「優秀なフロントエンドエンジニア」の役割を担ってもらいます。

単なる「コードを書いて」というプロンプトにしても面白くない。ここまでの工程でできあがった簡易企画書に加えて、UIイメージ、細かな技術要件も盛り込んでみます。目指すべきは「究極の開発指示書」ですけども…
今回は「究極」までいかなくてもいいね笑 ラクに行きましょう、ラクに。  


開発指示プロンプトの核となる要素


  1. 役割設定: 「優秀なフロントエンドエンジニア」として振る舞い、仕様を完全に満たすこと。

  2. 技術スタックの制約: サーバーレス、HTML/CSS/JavaScript、Gemini APIの使用を厳命。

  3. コア機能の明確化: ドキュメントアップロード(D&D対応)、役割設定、チャットセッション維持、そしてRAG簡易版プロンプト構築をテーブル形式で指示。

こんな感じで、Geminiに開発用プロンプト作成を指示。

ソースコード生成作業は、貴方(2.5flash)ではなく、Gemini2.5Proを使います。
Gemini2.5Proに指示するためのプロンプトを作成してもらえますか。
仕様に従い、品質の良いソースコードを開発させたいので協力してください。
私のアイデアですが、ここまでの貴方とのやりとりをドキュメント化したものがあります。
参考にこのメッセージに添付します。(2本)
これをコンテキストに加えて投入すれば、
だいぶ良いのではないかと思うのですがいかがでしょうか?


すると…Geminiは以下のプロンプトを返してくれた。

目的
あなたは、**「業界特化型 Q&Aナレッジボット即席生成」**アプリを開発する、非常に優秀なフロントエンドエンジニアです。与えられた仕様書とUI設計に基づき、**サーバーレス(HTML+JavaScript)**で動作するプロトタイプコードを生成してください。
開発要件と制約
サーバーレス: 外部サーバーやフレームワークは一切使用せず、HTML/CSS/JavaScriptのみで完結させてください。
ファイル構成: index.html(HTML/CSS/UIロジック)と、Gemini API連携などのコアロジックを担う**app.js**に分離して作成してください。CSSはindex.htmlに<style>タグで埋め込んでも構いません。
技術スタック:
フロントエンド: HTML5, CSS3,JavaScript。
AIエンジン: Google Gemini APIを使用。
通信: JavaScriptのfetchまたは推奨されるクライアントライブラリを使用し、Chatセッション機能を用いて会話履歴を維持してください。
工数: 2〜3日間で完了可能な、シンプルかつデモ効果の高い実装に留めてください。


実装すべきコア機能(最重要)
以下の仕様を完全に満たすように実装してください。
機能名
詳細仕様
実装時のポイント
ドキュメントアップロード
テキストファイル(.txt)のドラッグ&ドロップおよびファイル選択に対応 。JavaScriptでファイル内容を読み込み、テキスト変数に格納する。


UIイメージ(左側パネル)通りの大きなD&Dエリアを実装。


役割設定
ユーザーが入力したテキスト(ボットの専門分野)を取得。




Gemini Chatセッション


Chatセッションを開始し、会話履歴を維持した連続対話を実現する 10。


プロンプト構築(RAG簡易版)


システム命令として役割設定を渡し、ユーザーの質問時にアップロードされたドキュメント全体をコンテキストとして挿入する構造を厳守 11。


チャットUI
ユーザーとボットの発言が区別できるチャットバブルUIを実装 12。UIイメージ(右側パネル)のデザインを踏襲 13。




注意書き表示
チャットエリアの下部に、**「※注意:この回答は提供されたドキュメントに基づいています。最終的な判断は人間が行ってください。」**という文言を朱書きで表示 14。






出力フォーマット
以下の3つのファイルの内容を、それぞれコードブロックで区切って出力してください。
index.html (UI/レイアウト/CSSを含む)
app.js (JavaScriptロジック。APIキーはYOUR_GEMINI_API_KEYというプレースホルダーを使用すること)
README.md (簡単なセットアップと使い方を記述すること)

[プロンプト本体]
**私は、これからあなたに、私が設計した「業界特化型 Q&Aナレッジボット即席生成」プロトタイプアプリの仕様書とUIイメージを提供します。あなたは、この仕様書を完璧に理解し、**HTML、CSS、JavaScript**のみで構成されたサーバーレスのプロトタイプコードを生成してください。
【開発要件】
Gemini APIのChatセッション機能を使い、アップロードされた**.txtファイル**の内容をコンテキストとして利用したQ&A機能を実現してください。
プロンプトには、**役割設定(ペルソナ)と制約(ドキュメント外の情報には「関連情報が見当たらない」と回答)**をシステム命令として含めてください。
UIは添付の**UIイメージ**に従い、日本語で実装してください。
ファイルは**index.htmlとapp.js**に分離し、app.jsにはYOUR_GEMINI_API_KEYのプレースホルダーを挿入してください。
以上の要件に基づき、index.html、app.js、README.mdの3つのファイルの内容を、それぞれのコードブロックで出力してください。


これをもとに…私のノウハウを加え、最終投入版のプロンプト完成。


メモ)今回のRAG簡易版プロンプトの設計思想

このボットの精度を決定づけるのは、Geminiへの指示方法ですかね。

JavaScript

// 【ステップ 1: システムプロンプトによる役割と制約の付与】
const SYSTEM_INSTRUCTION = `あなたは、提供された【ドキュメント】の内容のみに基づいて正確に回答する、知識豊富な専門家です。回答は日本語で行い、質問がドキュメントにない情報に関する場合は、「このドキュメントには関連情報が見当たりません」と回答してください。あなたの専門分野は「${ユーザーが入力した役割}」です。`;

// 【ステップ 2: ドキュメントの挿入(RAGの簡易実装)】
const DOCUMENT_CONTEXT = `--- 参照ドキュメントの開始 ---
${アップロードされたドキュメントの全文}
--- 参照ドキュメントの終了 ---`;

// 【ステップ 3: 最終的な質問文の構築】
const FINAL_PROMPT = `${DOCUMENT_CONTEXT}\n\n上記のドキュメントを参照して、次の質問に回答してください: 「${ユーザーの質問}」`;

アプリ実行時、ユーザーがアップロードした「ドキュメント全文」を毎回質問文のコンテキストとして挿入し、同時に「ドキュメントにない質問には答えない」という制約をかけることで、あたかもRAG(Retrieval-Augmented Generation)のような「専門特化したボット」の挙動が実現できるのではないかい?


さて、プログラム開発。文字ベースですみませんが…この流れです。

★前工程で作った『プロンプト』と追加コンテキスト2本を用意。
★ここからはGoogle AI Studioを使う。https://aistudio.google.com/
【手順】

①GoogleAIStudioを開く。使用するLLMを2.5Proにする。
②準備したプロンプトをチャットボックスにコピー&ペーストする。
③準備したドキュメントをコンテキストとして添付する。
④プロンプトを実行する。初期開発完了を待つ。(2~3分)
⑤出力内容を読み、理解する。
⑥生成されたソースコードやREADMEテキストをダウンロードする。
⑦ソースコード(Javascript)を修正する。*APIKEYの埋め込みや、意図しないLLMの修正。
 APIKEYはGoogleAIStudioから取得したものをセットしました。
 今回のLLMはGemini2.5Flashでいいね。モデルコード: gemini-2.5-flash
⑧初回起動。動作テストを行う。評価&反復&修正を繰り返す。

今回は...あまり調子よくなかったな苦笑
技術力が無い人は修正指示するにもやや難易度高いレベルだったかな。

これも投入したプロンプトが良くなかったという評価。
…やはり「究極の開発指示書」を追求したいですね♪

結果、出来上がりましたが、htmlファイル一本に強引に統合し、処理もすこぶるシンプルにし、定めた仕様とUXを実現させましたとさ…

補足)この日の工数都合上、ナレッジとしてアップロードできるのは、txtファイルだけになりました。デモアプリとしては十分。暇なときにPDF等もアップロードできるようにブラッシュアップしようと思います。


まだバグがあるかもしれませんが…今回のアウトプットは以下。

ソースをコピーし、index.htmlなど、適当に名前をつけ、ローカルに保存。

const API_KEY = 'ここにあなたのAPIキーをセットしてください';

の部分に、あなたのAPIキーをセットし上書き保存すれば動くと思います。
自分の環境で、テストデータ(txtファイル)を用意し投入すれば試せます。

*なお、ソースへのAPIキー直埋めはセキュリティ的にもNGです。
このアプリを正式開発する際にはこの部分も修正です…

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <!-- ★修正点: アプリ名を変更 -->
    <title>The My AIボット</title>
    <style>
        :root {
            --primary-color: #007bff;
            --secondary-color: #6c757d;
            --background-color: #f8f9fa;
            --panel-background: #ffffff;
            --border-color: #dee2e6;
            --text-color: #212529;
            --light-text-color: #6c757d;
            --danger-color: #dc3545;
        }
        html, body {
            height: 100%;
            margin: 0;
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
            overflow: hidden;
        }
        body {
            display: flex;
            justify-content: center;
            align-items: center;
            padding: 20px;
            background-color: var(--background-color);
            box-sizing: border-box;
        }
        .container {
            display: flex;
            width: 100%;
            max-width: 1200px;
            height: 100%;
            max-height: 95vh;
            background-color: var(--panel-background);
            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
            border-radius: 12px;
            overflow: hidden;
        }
        .panel {
            padding: 30px;
            display: flex;
            flex-direction: column;
            box-sizing: border-box;
        }
        .left-panel {
            flex: 1;
            border-right: 1px solid var(--border-color);
            overflow-y: auto;
        }
        .right-panel {
            flex: 2;
        }
        .right-panel.disabled {
            opacity: 0.5;
            pointer-events: none;
        }
        h1 {
            font-size: 24px;
            margin-top: 0;
            color: var(--primary-color);
            display: flex;
            align-items: center;
            flex-shrink: 0;
        }
        h1 .logo {
            font-weight: bold; background: var(--primary-color); color: white; padding: 5px 10px; border-radius: 5px; margin-right: 10px; font-size: 16px;
        }
        h2 {
            font-size: 18px; margin-bottom: 10px; border-bottom: 2px solid var(--border-color); padding-bottom: 10px; flex-shrink: 0;
        }
        .setting-section {
            margin-bottom: 25px;
        }
        #drop-zone {
            display: block;
            border: 2px dashed var(--border-color);
            border-radius: 8px;
            padding: 15px;
            text-align: center;
            cursor: pointer;
            transition: background-color 0.3s, border-color 0.3s;
        }
        #drop-zone.dragover {
            background-color: #e9ecef; border-color: var(--primary-color);
        }
        #drop-zone p, #drop-zone .icon {
             pointer-events: none;
        }
        #drop-zone p {
            margin: 0; 
            color: var(--light-text-color);
            font-size: 0.9em;
        }
        #drop-zone .icon {
            font-size: 28px;
            color: var(--primary-color); 
            margin-bottom: 5px;
        }
        #file-input {
            display: none;
        }
        #file-name {
            text-align: center; 
            height: 1.2em; 
            margin-top: 10px;
            font-weight: bold;
            color: var(--primary-color);
        }
        #role-input {
            width: 100%; padding: 10px; border: 1px solid var(--border-color); border-radius: 4px; font-size: 14px; box-sizing: border-box;
        }
        /* ★修正点: ボタンを格納するコンテナ */
        .button-container {
            margin-top: auto; /* コンテナを一番下に配置 */
        }
        #start-button {
            width: 100%; padding: 12px; background-color: var(--primary-color); color: white; border: none; border-radius: 5px; font-size: 16px; cursor: pointer; transition: background-color 0.3s;
        }
        #start-button:hover {
            background-color: #0056b3;
        }
        #start-button:disabled {
            background-color: var(--secondary-color); cursor: not-allowed;
        }
        /* ★修正点: クリアボタンのスタイル */
        #clear-button {
            width: 100%;
            padding: 8px;
            margin-top: 10px;
            background-color: transparent;
            color: var(--secondary-color);
            border: 1px solid var(--border-color);
            border-radius: 5px;
            font-size: 14px;
            cursor: pointer;
            transition: background-color 0.3s, color 0.3s;
        }
        #clear-button:hover {
            background-color: #e9ecef;
            color: var(--text-color);
        }

        #chat-header {
            font-weight: bold; padding-bottom: 10px; border-bottom: 1px solid var(--border-color); margin-bottom: 20px; flex-shrink: 0;
        }
        #chat-box {
            flex-grow: 1; overflow-y: auto; padding-right: 10px; margin-bottom: 20px;
        }
        .chat-message {
            margin-bottom: 15px; display: flex;
        }
        .chat-bubble {
            max-width: 80%; padding: 10px 15px; border-radius: 18px; line-height: 1.5; white-space: pre-wrap; word-break: break-word;
        }
        .user { justify-content: flex-end; }
        .user .chat-bubble { background-color: var(--primary-color); color: white; border-bottom-right-radius: 4px; }
        .bot { justify-content: flex-start; }
        .bot .chat-bubble { background-color: #e9ecef; color: var(--text-color); border-bottom-left-radius: 4px; }
        .bot.loading .chat-bubble { display: flex; align-items: center; }
        .bot.loading .dot { width: 8px; height: 8px; margin: 0 2px; background-color: var(--secondary-color); border-radius: 50%; display: inline-block; animation: bounce 1.4s infinite ease-in-out both; }
        .bot.loading .dot:nth-child(1) { animation-delay: -0.32s; }
        .bot.loading .dot:nth-child(2) { animation-delay: -0.16s; }
        @keyframes bounce { 0%, 80%, 100% { transform: scale(0); } 40% { transform: scale(1.0); } }
        .chat-input-area {
            flex-shrink: 0; border-top: 1px solid var(--border-color); padding-top: 20px;
        }
        #chat-form {
            display: flex;
        }
        #user-input {
            flex-grow: 1; padding: 10px; border: 1px solid var(--border-color); border-radius: 5px; resize: none; font-size: 14px; margin-right: 10px;
        }
        #send-button {
            width: 45px; border: none; background-color: var(--primary-color); color: white; border-radius: 5px; cursor: pointer; font-size: 20px; display: flex; justify-content: center; align-items: center;
        }
        .warning {
            font-size: 12px; color: var(--danger-color); text-align: center; padding-top: 10px;
        }
    </style>
</head>
<body>

<div class="container">
    <div class="panel left-panel">
        <!-- ★修正点: アプリ名を変更 -->
        <h1><span class="logo">DX Solutions</span>The My AIボット</h1>
        
        <div class="setting-section">
            <h2>1. ドキュメントをアップロード</h2>
            <label id="drop-zone" for="file-input">
                <div class="icon">📄</div>
                <p>ファイル(.txt)をドラッグ&ドロップorクリック選択</p>
            </label>
            <input type="file" id="file-input" accept=".txt">
            <p id="file-name"></p>
        </div>

        <div class="setting-section">
            <h2>2. 役割設定</h2>
            <input type="text" id="role-input" placeholder="例:卸売産業の専門家、品質管理者">
        </div>

        <!-- ★修正点: ボタンをコンテナで囲む -->
        <div class="button-container">
            <button id="start-button" disabled>ボットを起動しチャット開始</button>
            <button id="clear-button">設定をクリア</button>
        </div>
    </div>

    <div class="panel right-panel disabled" id="chat-panel">
        <div id="chat-header">AIナレッジボット</div>
        <div id="chat-box">
            <!-- 初期メッセージはJSで動的に生成 -->
        </div>
        <div class="chat-input-area">
            <form id="chat-form">
                <textarea id="user-input" placeholder="質問を入力..." rows="2"></textarea>
                <button id="send-button" type="submit">
                    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="pointer-events: none;"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></svg>
                </button>
            </form>
            <p class="warning">※注意:この回答は提供されたドキュメントに基づいています。最終的な判断は人間が行ってください。</p>
        </div>
    </div>
</div>

<script type="module">
    import { GoogleGenerativeAI } from "https://cdn.jsdelivr.net/npm/@google/generative-ai/+esm";

    // --- DOM要素の取得 ---
    const dropZone = document.getElementById('drop-zone');
    const fileInput = document.getElementById('file-input');
    const fileNameDisplay = document.getElementById('file-name');
    const roleInput = document.getElementById('role-input');
    const startButton = document.getElementById('start-button');
    const chatPanel = document.getElementById('chat-panel');
    const chatBox = document.getElementById('chat-box');
    const chatForm = document.getElementById('chat-form');
    const userInput = document.getElementById('user-input');
    // ★修正点: クリアボタンを取得
    const clearButton = document.getElementById('clear-button');

    // --- 状態管理変数 ---
    const API_KEY = 'ここにあなたのAPIキーをセットしてください';
    let documentText = '';
    let botRole = '';
    let chatSession = null;
    let genAI = null;

    // --- イベントリスナー ---
    dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('dragover'); });
    dropZone.addEventListener('dragleave', () => { dropZone.classList.remove('dragover'); });
    dropZone.addEventListener('drop', (e) => {
        e.preventDefault();
        dropZone.classList.remove('dragover');
        const files = e.dataTransfer.files;
        if (files.length > 0) { handleFile(files[0]); }
    });
    fileInput.addEventListener('change', (e) => {
        if (e.target.files.length > 0) { handleFile(e.target.files[0]); }
    });
    
    roleInput.addEventListener('input', checkReadyState);
    startButton.addEventListener('click', startChat);
    chatForm.addEventListener('submit', handleFormSubmit);
    // ★修正点: クリアボタンのクリックイベントを追加
    clearButton.addEventListener('click', resetApp);
    // ★修正点: アプリ起動時に初期状態をセット
    document.addEventListener('DOMContentLoaded', resetApp);


    // --- 機能実装 ---

    // ★修正点: アプリを初期状態にリセットする関数を追加
    function resetApp() {
        documentText = '';
        botRole = '';
        fileInput.value = ''; // inputの値をリセット
        fileNameDisplay.textContent = '';
        roleInput.value = '';
        chatSession = null;

        chatPanel.classList.add('disabled');
        chatBox.innerHTML = '';
        addInitialMessage();
        
        startButton.textContent = 'ボットを起動しチャット開始';
        checkReadyState();
    }
    
    // ★修正点: 初期メッセージの表示を関数化
    function addInitialMessage() {
        const initialMessageHTML = `
            <div class="chat-message bot">
                <div class="chat-bubble">こんにちは!ドキュメントをアップロードし、役割を設定してからボットを起動してください。</div>
            </div>`;
        chatBox.innerHTML = initialMessageHTML;
    }

    function handleFile(file) {
        if (!file || file.type !== 'text/plain') {
            alert('テキストファイル (.txt) を選択してください。');
            return;
        }
        const reader = new FileReader();
        reader.onload = (e) => {
            documentText = e.target.result;
            fileNameDisplay.textContent = `ファイル名: ${file.name}`;
            checkReadyState();
        };
        reader.onerror = () => {
            alert('ファイルの読み込みに失敗しました。');
            resetApp();
        };
        reader.readAsText(file);
    }

    function checkReadyState() {
        botRole = roleInput.value.trim();
        startButton.disabled = !(documentText && botRole);
    }

    async function startChat() {
        if (API_KEY === 'YOUR_GEMINI_API_KEY' || API_KEY === '') {
            alert('エラー: Gemini APIキーが設定されていません。コード内のYOUR_GEMINI_API_KEYを書き換えてください。');
            return;
        }
        startButton.disabled = true;
        startButton.textContent = '起動中...';
        try {
            genAI = new GoogleGenerativeAI(API_KEY);
            const model = genAI.getGenerativeModel({ model: "gemini-2.5-flash" });
            const SYSTEM_INSTRUCTION = `あなたは、提供された【ドキュメント】の内容のみに基づいて正確に回答する、知識豊富な専門家です。
回答は日本語で行い、質問がドキュメントにない情報に関する場合は、「このドキュメントには関連情報が見当たりません」と回答してください。
あなたの専門分野は「${botRole}」です。`;
            chatSession = model.startChat({
                systemInstruction: {
                  role: "system",
                  parts: [{ text: SYSTEM_INSTRUCTION }],
                },
            });
            chatPanel.classList.remove('disabled');
            chatBox.innerHTML = '';
            addMessageToChatBox(`こんにちは!私は「${botRole}」です。アップロードされたドキュメントに関する質問にお答えします。`, 'bot');
        } catch (error) {
            console.error("チャットの開始に失敗しました:", error);
            alert("チャットの開始に失敗しました。APIキーまたは設定を確認してください。");
            startButton.textContent = 'ボットを起動しチャット開始';
            checkReadyState();
        }
    }
    
    async function handleFormSubmit(e) {
        e.preventDefault();
        const userMessage = userInput.value.trim();
        if (userMessage && chatSession) {
            userInput.value = '';
            await sendMessage(userMessage);
        }
    }

    async function sendMessage(message) {
        addMessageToChatBox(message, 'user');
        const loadingIndicator = addMessageToChatBox('...', 'bot', true);
        try {
            const prompt = `--- 参照ドキュメントの開始 ---
${documentText}
--- 参照ドキュメントの終了 ---

上記のドキュメントを参照して、次の質問に回答してください: 「${message}」`;
            const result = await chatSession.sendMessage(prompt);
            const response = result.response;
            const botMessage = response.text();
            loadingIndicator.remove();
            addMessageToChatBox(botMessage, 'bot');

        } catch (error) {
            console.error("メッセージの送信に失敗しました:", error);
            loadingIndicator.remove();
            addMessageToChatBox("エラーが発生しました。しばらくしてからもう一度お試しください。", 'bot');
        }
    }

    function addMessageToChatBox(text, sender, isLoading = false) {
        const messageElement = document.createElement('div');
        messageElement.classList.add('chat-message', sender);
        const bubbleElement = document.createElement('div');
        bubbleElement.classList.add('chat-bubble');
        if (isLoading) {
            messageElement.classList.add('loading');
            bubbleElement.innerHTML = '<div class="dot"></div><div class="dot"></div><div class="dot"></div>';
        } else {
            bubbleElement.textContent = text;
        }
        messageElement.appendChild(bubbleElement);
        chatBox.appendChild(messageElement);
        chatBox.scrollTop = chatBox.scrollHeight;
        return messageElement;
    }
</script>

</body>
</html>

ローカル保存したhtmlをクリックすると
ブラウザが起動され、この画面が立ち上がります。


本論3:成果物レビュー編|サーバーレスRAGが実現した世界


以下のテストデータをテキストファイルで用意して。

社内経費精算規定

1. 対象者
本規定は、正社員、契約社員を含む全従業員を対象とします。

2. 交通費
- 原則として、目的地までの最も経済的なルートを利用してください。
- 片道100km以上の移動を伴う出張の場合、新幹線の利用を許可します。ただし、グリーン車の利用は認められません。
- タクシーの利用は原則として認められませんが、以下の場合に限り許可します。
  - 22時以降の深夜移動
  - 重い機材や大量の資料を運搬する場合
  - 上記の場合でも、上限は5,000円までとし、必ず領収書の提出が必要です。

3. 宿泊費
- 国内出張における宿泊費の上限は、以下の通りとします。
  - 東京23区、大阪市、名古屋市:1泊あたり12,000円(税抜)
  - その他の地域:1泊あたり9,000円(税抜)
- 上限を超える宿泊施設を利用する場合は、事前に所属部署の上長の承認を得てください。

4. 申請方法
- 全ての経費は、社内システム「Keihi-Manager」を通じて申請してください。
- 経費の発生日から5営業日以内に申請を完了させてください。
- 申請時には、全ての領収書の原本をスキャンし、添付する必要があります。

The My AIボットに喰わせる。
役割は「会社の経費精算ルールに詳しい、総務部の担当者」でいいかな。

起動させると…OH!それっぽい雰囲気。

ファイルをアップロードし役割も設定
開始ボタンを押した後の画面


何か聞いてみる。
そうですね。国内出張における宿泊費の上限に絡めて…
「東京23区に出張した場合の宿泊費の上限金額は?」って聞いてみる。
その結果は…

正しく答えてくれたみたい♪


特筆すべきは、「ドキュメントにない質問」には、「このドキュメントには関連情報が見当たりません」と潔く回答を拒否する点です。

ドキュメントに無い情報は答えませんっ!とのこと

ハルシネーション(誤情報)のリスクを抑え、ナレッジボットの信頼性を高めることができている…ような気がしてレビューは終わり。



結論:Geminiは最高の開発パートナーになるか?…それはあなたしだい。


工数を実測すると、企画から成果物のテスト/レビューまで約3~4時間。
「実質半日」でお客様におみせできるデモアプリができたという結果。
開発時のプロンプトをもっとまじめに作り込んでおけば、途中の反復も半分ぐらいには減らせたのではないか?と思います。
…Geminiに開発プロンプトを作らせる研究と実験、継続していきます。

Geminiを開発パートナーとしてみた場合

  • 企画立案フェーズでは、 優秀なコンサルタントとして、制約条件下で実現性の高いアイデアを生み出すサポートをしてくれる。

  • 開発フェーズでは、 優秀なエンジニアとして、複雑な仕様を含むコードを即座に生成し、開発工数削減に貢献してくれる。

「アイデアを思いついたら、1~2時間ですぐ開発して検証する」
このスピード感。そして百聞は一見に如かず。
お客様のアイデアをその場で形にできることはDX推進の武器。

…Geminiを優秀な開発パートナーすることができるか否かという点。
それは使う人間の能力しだいだと思います。

そう。「コミュニケーション能力」は高めな続けなければなりませんね。Geminiとのコミュニケーションは…プロンプト。
コミュニケーション力の中でも「伝える力」の大切さ。

相手方が人間でもGeminiでも。
やりたいことを正しく伝えるのは難しいもんだと感じる今日この頃です。


最後までお読みいただきありがとうございました♪

see you. ⊿


いいなと思ったら応援しよう!