ローカルファイルをAIに分析させる「Local Navigator」

【更新履歴】

・2026/2/19 バージョン1.0公開。
・2026/2/19 バージョン1.1公開。

画像
メイン画面

Local Navigator ユーザーマニュアル

Local Navigatorは、あなたのローカル環境にある大量のファイル(ソースコードやドキュメント)を、外部のAI(ChatGPT, Claude, Geminiなど)を使って効率的に解析・編集するための「コンテキスト・エンジニアリングツール」です。

Pythonなどの環境構築は不要。ブラウザ(Chrome/Edge)とこのHTMLファイルだけで動作し、ローカルで動作するAIモデル(Transformers.js)によるベクトル検索までサポートしています。


はじめに:このツールの目的

通常のRAG(検索拡張生成)や、AIへのファイルアップロードには以下の課題があります。

  • トークン制限: 全ファイルを送るとコンテキストが溢れる、または課金が高額になる。

  • 検索精度: 単なるキーワード検索では、機能的な関連性(例:「認証」で「Login.ts」を探すなど)が見つからない。

  • プライバシー/手間: サーバーを立てたり、APIキーを管理するのが面倒。

LocalNavigator3は、**「AIにファイル構造(地図)だけを渡し、AIが必要なファイルだけを指名して読む」**というアプローチでこれらを解決します。さらに、ブラウザ内で完結するベクトル検索により、高度な意味検索も実現しました。


 セットアップと起動

  1. ファイルの準備:

    1. 提供されたHTMLコードを、index.html という名前でPC上の任意の場所に保存します。

  2. ブラウザで開く:

    1. index.html を Google Chrome または Microsoft Edge で開きます。

      • 注意: SafariやFirefoxでは「File System Access API」が完全にはサポートされていないため、動作しない場合があります。

  3. 初回ロード(重要):

    1. 起動直後、ブラウザは自動的に軽量AIモデル(約30MB〜)のダウンロードを開始します。

      • 画面右上の「VECTOR AI」バッジが点灯するまで数秒〜数分お待ちください。

      • 一度ダウンロードされれば、次回以降はキャッシュが使われるため一瞬で起動します。


画面構成

  • 左パネル(エクスプローラー):

    • 1. フォルダを開く: 解析対象のプロジェクトフォルダを選択します。

    • ファイル構造: ツリー形式でディレクトリ構造が表示されます。

    • オプション: スマート読込やベクトル検索のON/OFFが可能です。

  • 右パネル(インタラクション):

    • STEP 1: AIへの要望を入力し、最初の指示プロンプトを作成します。

    • STEP 2: AIからの返答(コマンド)を貼り付け、ファイルを読み込んで次のプロンプトを作成します。


使用ワークフロー

このツールは、**「Web上のAI(脳)」「ローカルのあなた(手足)」**を繋ぐブリッジとして機能します。

Step 0: フォルダの選択

  1. 左上の [1. フォルダを開く] ボタンをクリックします。

  2. 解析したいプロジェクトのルートフォルダを選択します。

  3. ブラウザが「ファイルの表示権限」を求めてくるので、「許可」 をクリックしてください。

    • ファイルパスのベクトル化(意味付け)がバックグラウンドで走ります。完了するとステータスバーに「準備完了」と表示されます。

Step 1: 要望の入力とプロンプト生成

  1. 右側の STEP 1 エリアにあるテキストボックスに、やりたいことを入力します。

    • 例:「認証周りのコードを読んで、セキュリティ上の脆弱性がないかチェックして」

    • 例:「src/components の中のReactコンポーネントをVue.jsに書き換える方法を教えて」

  2. [プロンプト生成 »] ボタンを押します。

  3. 下に生成された黒いボックスをクリックして、テキストをコピーします。

Step 2: AIとの対話(ループ)

  1. AIに送信:

    1. コピーしたテキストを、ChatGPT / Claude / Gemini などのチャット欄に貼り付けて送信します。

  2. AIの返答をコピー:

    1. AIはファイルの中身を知らないため、「では、src/auth/login.ts を読み込んでください(READ: src/auth/login.ts)」のように返してきます。

    2. このAIの返答テキストを、すべてコピーしてください。

  3. ツールで実行:

    1. LocalNavigator3に戻り、STEP 2 のテキストボックスに貼り付け、[実行 & 次のプロンプト生成 »] を押します。

      • ツールは自動的に READ: や SEARCH: コマンドを抽出し、指定されたファイルの中身や検索結果を取得します。

      • 挨拶文(「分かりました」など)は自動削除されます。

  4. 次のプロンプトをAIへ:

    1. 生成された「ファイルの中身入りプロンプト」をクリックしてコピーし、再びAIのチャット欄に貼り付けて送信します。

AIが「解決しました」と回答するまで、このStep 2の手順を繰り返してください。


高度な機能

ベクトル検索 (Neural Search)

このツールには all-MiniLM-L6-v2 モデルが内蔵されています。

AIがファイルパスを知らない場合でも、AIに SEARCH: ログイン処理 とコマンドを出させれば、ツールはファイル名に「ログイン」が含まれていなくても、意味的に近い AuthService.ts や SessionManager.js などを発見してAIに提示します。

スマート読込 (Smart Read)

左下のオプション [スマート読込] がONの場合(デフォルト)、300行を超える巨大なファイルは、「先頭200行 + 末尾50行」 だけを切り出してAIに渡します。

  • これにより、トークン消費を抑えつつ、ファイルの概要と重要なロジック(冒頭のimportや定義、末尾のexportなど)をAIに把握させることができます。


トラブルシューティング

  • Q. フォルダを選択しても何も起きない / エラーになる

    • ブラウザの権限設定を確認してください。また、システム保護されたフォルダ(Windowsフォルダなど)はアクセスできない場合があります。

  • Q. ベクトル検索が動かない(AIモデルロードエラー)

    • お使いのブラウザやハードウェアが WebGPU / WebAssembly に対応していない可能性があります。その場合でも、ツールは自動的に「キーワード一致検索」モードに切り替わって動作します。

  • Q. AIが READ: コマンドを出してくれない

    • Step 1で生成されるプロンプトには「ファイルを読むときは READ: パス と出力して」という指示が含まれていますが、AIによっては無視して勝手にコードを生成し始めることがあります。その場合は、「ファイルの中身を確認してから回答してください」とAIに追加で指示してください。


⚠️ 注意事項

  • データプライバシー:

    1. このツールはファイルを外部サーバーにアップロードしません。AIモデルのダウンロード以外で外部通信は行わず、ファイル解析はすべてあなたのブラウザ内(ローカル)で行われます。ただし、**生成されたプロンプトをChatGPT等に貼り付けた際、その内容は各AIサービスのプライバシーポリシーに従って扱われます。**機密情報の扱いにはご注意ください。

・ダウンロードされる方はこちら。↓

 


・ソースコードはこちら。↓

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Local Navigator 1.1</title>
    <script type="module">
        import { pipeline, env } from 'https://cdn.jsdelivr.net/npm/@xenova/transformers@2.16.0';
        env.allowLocalModels = false;
        env.useBrowserCache = true;

        window.AI_PIPELINE = null;
        window.AI_MODEL_STATUS = "init"; 

        window.loadModel = async () => {
            try {
                updateStatus("AIモデルをダウンロード中... (初回のみ数分かかります)");
                window.AI_PIPELINE = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2');
                window.AI_MODEL_STATUS = "ready";
                updateStatus("AIモデル準備完了: ベクトル検索が可能です");
                document.getElementById('indicator-ai').classList.add('active');
            } catch (e) {
                console.error(e);
                window.AI_MODEL_STATUS = "error";
                updateStatus("AIモデルのロードに失敗しました (WebGPU/WASM非対応の可能性)");
            }
        };

        function updateStatus(msg) {
            const el = document.getElementById('sys-msg');
            if(el) el.innerText = msg;
        }

        window.loadModel();
    </script>
    <style>
        :root { --bg:#080808; --panel:#141414; --accent:#00ffcc; --text:#e0e0e0; --border:#333; --warn:#ffcc00; --danger:#ff4466; }
        body { margin:0; background:var(--bg); color:var(--text); font-family:'Segoe UI', sans-serif; height:100vh; display:flex; flex-direction:column; overflow:hidden; }
        
        #header { height:50px; border-bottom:1px solid var(--border); display:flex; align-items:center; padding:0 20px; justify-content:space-between; background:#000; }
        .logo { font-weight:bold; color:var(--accent); font-size:18px; letter-spacing:1px; display:flex; align-items:center; gap:10px; }
        .badge { font-size:10px; padding:2px 6px; border-radius:4px; background:#333; color:#888; }
        .badge.active { background:rgba(0, 255, 204, 0.2); color:var(--accent); border:1px solid var(--accent); }

        #main { flex:1; display:flex; overflow:hidden; }
        
        #left { width:320px; background:var(--panel); border-right:1px solid var(--border); display:flex; flex-direction:column; padding:15px; gap:15px; font-size:12px; }
        #file-tree { flex:1; overflow-y:auto; border:1px solid #222; padding:8px; color:#999; white-space:pre; background:#0a0a0a; font-family:monospace; }
        
        #right { flex:1; display:flex; flex-direction:column; background:#0e0e0e; padding:20px; gap:20px; overflow-y:auto; }

        .step-card { background:#1a1a1a; border:1px solid var(--border); padding:20px; border-radius:6px; position:relative; transition:0.3s; }
        .step-card.active { border-color:var(--accent); box-shadow:0 0 15px rgba(0,255,204,0.05); }
        /* 修正点: pointer-events: none を削除し、操作可能にしました */
        .step-card.inactive { opacity:0.6; filter:grayscale(0.8); }
        .step-tag { position:absolute; top:-10px; left:15px; background:var(--bg); color:var(--accent); padding:0 8px; font-size:11px; font-weight:bold; border:1px solid var(--accent); }

        textarea { width:100%; height:80px; background:#050505; border:1px solid #333; color:#fff; padding:12px; font-family:monospace; box-sizing:border-box; resize:vertical; font-size:13px; }
        textarea:focus { border-color:var(--accent); outline:none; }
        
        .btn { background:#222; color:#ccc; border:1px solid #444; padding:10px 20px; cursor:pointer; font-size:12px; transition:0.2s; border-radius:4px; font-weight:bold; }
        .btn:hover { background:var(--accent); color:#000; border-color:var(--accent); }
        .btn-primary { background:rgba(0, 255, 204, 0.15); color:var(--accent); border:1px solid var(--accent); width:100%; }

        .result-box { margin-top:15px; background:#111; border-left:3px solid var(--accent); padding:10px; font-family:monospace; font-size:11px; color:#aaa; cursor:pointer; max-height:250px; overflow-y:auto; white-space:pre-wrap; }
        .result-box:hover { background:#181818; color:#fff; }
        .result-box::after { content:'クリックしてコピー'; display:block; text-align:right; font-size:10px; color:var(--accent); margin-top:8px; opacity:0.7; }
        
        .status-bar { font-size:11px; color:#666; display:flex; justify-content:space-between; }
        #sys-msg { color:var(--warn); animation: pulse 2s infinite; }
        @keyframes pulse { 0% { opacity:0.7; } 50% { opacity:1; } 100% { opacity:0.7; } }
    </style>
</head>
<body>

<div id="header">
    <div class="logo">
        Local Navigator <span style="font-size:10px; opacity:0.7;">NEURAL EDITION</span>
    </div>
    <div style="display:flex; gap:10px;">
        <span id="indicator-fs" class="badge">FILE SYSTEM</span>
        <span id="indicator-ai" class="badge">VECTOR AI</span>
    </div>
</div>

<div id="main">
    <div id="left">
        <button class="btn btn-primary" onclick="app.selectFolder()">1. フォルダを開く</button>
        <div id="folder-name" style="color:#fff; font-weight:bold; text-align:center;">未選択</div>
        
        <div class="status-bar">
            <span id="stat-files">0 ファイル</span>
            <span id="stat-vecs">0 ベクトル</span>
        </div>

        <div style="margin-top:10px; font-size:11px;">ファイル構造:</div>
        <div id="file-tree"></div>
        
        <div style="margin-top:auto; border-top:1px solid #333; padding-top:10px;">
            <div style="font-size:10px; color:#888; margin-bottom:5px;">オプション</div>
            <label style="display:flex; align-items:center; gap:5px; cursor:pointer;"><input type="checkbox" id="opt-smart" checked> スマート読込 (先頭200行+末尾50行)</label>
            <label style="display:flex; align-items:center; gap:5px; cursor:pointer;"><input type="checkbox" id="opt-vector" checked> ベクトル検索を有効化</label>
            <button class="btn" style="width:100%; margin-top:10px; font-size:10px;" onclick="location.reload()">リセット</button>
        </div>
    </div>

    <div id="right">
        <div style="font-size:11px;" id="sys-msg">システム待機中...</div>

        <div class="step-card active" id="step-1">
            <div class="step-tag">STEP 1: 要望の入力</div>
            <p style="font-size:12px; color:#888; margin:0 0 10px 0;">AIに依頼したい内容を入力してください。(例:「認証機能のコードを解析して」)</p>
            <textarea id="user-req" placeholder="ここに要望を入力..."></textarea>
            <div style="margin-top:10px; text-align:right;">
                <button class="btn" onclick="app.genPrompt()">プロンプト生成 &raquo;</button>
            </div>
            <div id="out-prompt-1" class="result-box" style="display:none;" onclick="app.copyText(this)"></div>
        </div>

        <div class="step-card inactive" id="step-2">
            <div class="step-tag">STEP 2: AI返答の処理</div>
            <p style="font-size:12px; color:#888; margin:0 0 10px 0;">Web上のAIからの返答(READ: xxx や SEARCH: xxx を含むもの)を貼り付けてください。</p>
            <textarea id="ai-resp" placeholder="AIの返答をここにペースト..."></textarea>
            <div style="margin-top:10px; text-align:right;">
                <button class="btn" onclick="app.processResponse()">実行 & 次のプロンプト生成 &raquo;</button>
            </div>
            <div id="out-prompt-2" class="result-box" style="display:none;" onclick="app.copyText(this)"></div>
        </div>
    </div>
</div>

<script type="module">
const app = {
    dirHandle: null,
    fileMap: {},
    vectors: [],
    treeText: "",
    
    selectFolder: async () => {
        try {
            app.dirHandle = await window.showDirectoryPicker();
            document.getElementById('indicator-fs').classList.add('active');
            document.getElementById('folder-name').innerText = app.dirHandle.name;
            
            app.fileMap = {};
            app.vectors = [];
            const lines = [];
            
            updateStatus("ファイルをスキャン中...");
            await app.scanDir(app.dirHandle, "", lines, 0);
            
            app.treeText = lines.join('\n');
            document.getElementById('file-tree').innerText = app.treeText;
            document.getElementById('stat-files').innerText = `${Object.keys(app.fileMap).length} ファイル`;
            
            if (window.AI_MODEL_STATUS === 'ready') {
                updateStatus("ファイルパスのベクトル化を実行中...");
                await app.vectorizePaths();
                updateStatus("準備完了。");
            } else {
                updateStatus("AIモデルのロード待ちです... ロード後に自動でベクトル化されます。");
            }

            // フォルダ選択後はStep 1にフォーカス
            app.switchStep(1);

        } catch(e) { console.error(e); alert("キャンセルされました"); }
    },

    scanDir: async (handle, path, lines, depth) => {
        const entries = [];
        for await (const entry of handle.values()) entries.push(entry);
        entries.sort((a,b) => (a.kind === b.kind ? a.name.localeCompare(b.name) : (a.kind === 'directory' ? -1 : 1)));

        for (let i=0; i<entries.length; i++) {
            const entry = entries[i];
            if(['node_modules','.git','bin','obj','.vs','dist','build','vendor'].includes(entry.name)) continue;

            const isLast = (i === entries.length - 1);
            const prefix = "  ".repeat(depth) + (isLast ? "+-- " : "|-- ");
            
            if (entry.kind === 'file') {
                const ext = entry.name.split('.').pop().toLowerCase();
                if(!['exe','dll','png','jpg','jpeg','gif','zip','mp4','pdf'].includes(ext)) {
                    lines.push(prefix + entry.name);
                    const fullPath = path + entry.name;
                    app.fileMap[fullPath] = entry;
                    app.vectors.push({ path: fullPath, embedding: null });
                }
            } else {
                lines.push(prefix + entry.name + "/");
                await app.scanDir(entry, path + entry.name + "/", lines, depth + 1);
            }
        }
    },

    vectorizePaths: async () => {
        if (!window.AI_PIPELINE) return;
        const total = app.vectors.length;
        
        for (let i = 0; i < total; i++) {
            const item = app.vectors[i];
            const textToEmbed = item.path.replace(/[\/\._-]/g, ' '); 
            const output = await window.AI_PIPELINE(textToEmbed, { pooling: 'mean', normalize: true });
            item.embedding = output.data;
            
            if (i % 10 === 0) updateStatus(`ベクトル生成中... ${i}/${total}`);
        }
        document.getElementById('stat-vecs').innerText = `${total} 埋め込み済`;
    },

    searchFiles: async (query) => {
        if (!window.AI_PIPELINE || !document.getElementById('opt-vector').checked) {
            return app.vectors.filter(v => v.path.toLowerCase().includes(query.toLowerCase())).map(v => ({ item: v, score: 1 }));
        }

        const qOutput = await window.AI_PIPELINE(query, { pooling: 'mean', normalize: true });
        const qVec = qOutput.data;

        const results = app.vectors.map(item => {
            if (!item.embedding) return { item, score: 0 };
            
            let dot = 0;
            for (let i = 0; i < qVec.length; i++) {
                dot += qVec[i] * item.embedding[i];
            }
            if (item.path.toLowerCase().includes(query.toLowerCase())) {
                dot += 0.3; 
            }
            return { item, score: dot };
        });

        return results.sort((a,b) => b.score - a.score).slice(0, 5);
    },

    genPrompt: () => {
        const req = document.getElementById('user-req').value.trim();
        if(!req) return alert("要望を入力してください");

        const prompt = `
[システム指示]
あなたは高度なコーディングアシスタント「LocalNavigator」です。
現在、ユーザーのローカルファイルに直接アクセスできませんが、以下の「ファイル構造」は見えています。

[ユーザーの要望]
${req}

[プロトコル]
1. 要望とファイル構造を分析してください。
2. 情報を得るために、以下のコマンドを出力してください(挨拶や前置きは不要です)。
   - ファイルの中身を読む: "READ: パス"
   - ファイルを探す(パスが不明な場合): "SEARCH: 検索キーワード"
3. 情報が十分なら、回答を行ってください。

[ファイル構造]
${app.treeText}
`;
        app.showResult('out-prompt-1', prompt);
        
        // Step 2へフォーカスを移すが、Step 1は操作可能なままにする
        app.switchStep(2);
    },

    processResponse: async () => {
        const raw = document.getElementById('ai-resp').value;
        if(!raw.trim()) return;

        updateStatus("AI返答を解析中...");
        
        const lines = raw.split('\n');
        const commands = { reads: [], searches: [] };
        
        lines.forEach(line => {
            const r = line.match(/READ:\s*(.+)/);
            const s = line.match(/SEARCH:\s*(.+)/);
            if (r) commands.reads.push(r[1].trim().replace(/['"`]/g,''));
            if (s) commands.searches.push(s[1].trim().replace(/['"`]/g,''));
        });

        let output = "[システム] 以下の情報を取得しました:\n\n";
        let hasData = false;

        if (commands.searches.length > 0) {
            hasData = true;
            for (const q of commands.searches) {
                const hits = await app.searchFiles(q);
                output += `=== "SEARCH: ${q}" の結果 ===\n`;
                if(hits.length===0) output += "該当なし\n";
                else hits.forEach(h => output += `- ${h.item.path} (関連度: ${h.score.toFixed(2)})\n`);
                output += "\n";
            }
        }

        if (commands.reads.length > 0) {
            hasData = true;
            const useSmart = document.getElementById('opt-smart').checked;
            
            for (const path of commands.reads) {
                const cleanPath = Object.keys(app.fileMap).find(p => p.endsWith(path)) || path;
                
                if (app.fileMap[cleanPath]) {
                    try {
                        const f = await app.fileMap[cleanPath].getFile();
                        const text = await f.text();
                        const lines = text.split('\n');
                        let content = text;
                        
                        if (useSmart && lines.length > 300) {
                            content = lines.slice(0, 200).join('\n') + 
                                      `\n\n... (中略: ${lines.length - 250}行) ...\n\n` + 
                                      lines.slice(-50).join('\n');
                        }
                        
                        output += `=== FILE: ${cleanPath} ===\n${content}\n======================\n\n`;
                    } catch(e) {
                        output += `[エラー] ${cleanPath} を読み込めませんでした: ${e.message}\n`;
                    }
                } else {
                    output += `[エラー] ファイルが見つかりません: ${path}\n`;
                }
            }
        }

        if (!hasData) {
            output = "[システム] ファイル読み込み/検索コマンドが検出されませんでした。\nこれが最終回答であれば、ユーザーへの返答として扱ってください。\nもし操作が必要なら、正しい形式 (READ: xxx) で出力してください。";
        } else {
            output += "\n[指示] 上記の情報を元に回答を作成するか、さらに必要な情報があれば READ/SEARCH コマンドを出力してください。";
        }

        app.showResult('out-prompt-2', output);
        document.getElementById('ai-resp').value = ""; 
        updateStatus("処理完了。次のプロンプトをコピーしてください。");
    },

    switchStep: (n) => {
        // 両方のステップを「見た目」だけで切り替えるが、操作はブロックしない
        document.getElementById('step-1').className = n===1 ? 'step-card active' : 'step-card inactive';
        document.getElementById('step-2').className = n===2 ? 'step-card active' : 'step-card inactive';
    },

    showResult: (id, text) => {
        const el = document.getElementById(id);
        el.innerText = text;
        el.style.display = 'block';
    },

    copyText: (el) => {
        navigator.clipboard.writeText(el.innerText);
        const bg = el.style.backgroundColor;
        el.style.backgroundColor = '#333';
        setTimeout(() => el.style.backgroundColor = '', 200);
    }
};

window.app = app;

function updateStatus(msg) {
    const el = document.getElementById('sys-msg');
    if(el) el.innerText = msg;
}
</script>
</body>
</html>

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

コメント

コメントするには、 ログイン または 会員登録 をお願いします。
小ぶりなプログラムを試しに作っているんですが、 ここではその説明書きをしていこうと思います。 こういう機能をつけてみてほしいだとかいった要望があれば コメント欄に書いてみて下さい。 ひまをみて対応します。
ローカルファイルをAIに分析させる「Local Navigator」|古井和雄
word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word

mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1