ローカルファイルをAIに分析させる「Local Navigator」
【このツールの概要】
・指定したフォルダ内のファイルを
AIに分析させます。
【手動モードの操作説明】
・手動モードの場合は、
まず入力欄に要望を書いて
確定ボタンを押して下さい。
・するとプロンプトができますので、
このプロンプトをクリックすると
コピーされますので、これを
ブラウザのプロンプト入力欄に貼り付けて
AIから解答を得ます。
・そしてまたそれを
本ツール側の入力欄に貼り付けて
ボタンを押します。
・このようにしていくと、分析結果が出ます。
・ダウンロードされる方はこちら。↓
・ソースコードはこちら。↓
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>NeuroContext Navigator v1.0</title>
<style>
:root {
--bg: #050505;
--panel: rgba(12, 16, 20, 0.95);
--accent: #00ffcc;
--text: #e0f0ff;
--border: #334455;
--danger: #ff4466;
--warn: #ffcc00;
}
body { margin: 0; background: var(--bg); color: var(--text); font-family: 'Segoe UI', 'Roboto', monospace; overflow: hidden; height: 100vh; display: flex; flex-direction: column; }
/* Layout */
#header {
height: 50px; background: rgba(10, 12, 16, 0.9); border-bottom: 1px solid var(--border);
display: flex; align-items: center; padding: 0 20px; justify-content: space-between;
box-shadow: 0 2px 20px rgba(0, 255, 204, 0.1); z-index: 10;
}
.logo { font-weight: bold; letter-spacing: 2px; color: var(--accent); display: flex; align-items: center; gap: 10px; }
.logo::before { content: '??'; filter: drop-shadow(0 0 5px var(--accent)); }
#main-area { flex: 1; display: flex; overflow: hidden; position: relative; }
/* Left Panel (Controls & Status) */
#left-panel {
width: 300px; background: var(--panel); border-right: 1px solid var(--border);
padding: 20px; display: flex; flex-direction: column; gap: 20px;
backdrop-filter: blur(10px); z-index: 5;
}
/* Right Panel (Chat & Interaction) */
#right-panel { flex: 1; display: flex; flex-direction: column; position: relative; }
#chat-history {
flex: 1; overflow-y: auto; padding: 40px; scroll-behavior: smooth;
background: radial-gradient(circle at center, #0a0f14 0%, #000 100%);
}
#input-area {
height: 120px; background: var(--panel); border-top: 1px solid var(--border);
padding: 20px; display: flex; gap: 10px; align-items: flex-start;
}
/* Elements */
.btn {
background: rgba(0, 255, 204, 0.1); border: 1px solid var(--border); color: var(--accent);
padding: 10px 20px; cursor: pointer; font-family: monospace; transition: 0.2s;
text-transform: uppercase; font-size: 11px; letter-spacing: 1px;
}
.btn:hover { background: var(--accent); color: #000; box-shadow: 0 0 15px var(--accent); }
.btn:disabled { opacity: 0.5; cursor: not-allowed; filter: grayscale(100%); }
.btn-danger { border-color: var(--danger); color: var(--danger); background: rgba(255, 68, 102, 0.1); }
input, textarea, select {
background: #080a0c; border: 1px solid var(--border); color: var(--text);
padding: 10px; font-family: monospace; width: 100%; box-sizing: border-box; outline: none;
}
input:focus, textarea:focus { border-color: var(--accent); }
textarea { resize: none; height: 100%; font-size: 14px; }
/* Chat Bubbles */
.msg { margin-bottom: 20px; max-width: 80%; line-height: 1.6; font-size: 13px; animation: fadeIn 0.3s ease; }
.msg.user { margin-left: auto; text-align: right; }
.msg.user .bubble { background: rgba(0, 255, 204, 0.1); border: 1px solid var(--border); border-radius: 8px 0 8px 8px; color: #fff; }
.msg.ai .bubble { background: rgba(255, 255, 255, 0.05); border: 1px solid #333; border-radius: 0 8px 8px 8px; color: #ccc; }
.msg.system .bubble { background: rgba(255, 204, 0, 0.1); border: 1px solid var(--warn); color: var(--warn); width: 100%; max-width: 100%; text-align: left; }
.bubble { padding: 15px; display: inline-block; white-space: pre-wrap; position: relative; }
/* Code Block / Copy Box */
.copy-box {
background: #000; border: 1px solid #333; padding: 10px; margin-top: 10px;
font-size: 11px; overflow-x: auto; color: #00ffcc; cursor: pointer; position: relative;
}
.copy-box:hover { border-color: var(--accent); }
.copy-box::after { content: 'CLICK TO COPY'; position: absolute; top:0; right:0; background:var(--accent); color:#000; font-size:9px; padding:2px 5px; }
/* Status Indicators */
.status-row { display: flex; justify-content: space-between; font-size: 11px; color: #666; margin-bottom: 5px; }
.stat-val { color: var(--text); }
/* Tabs */
.tabs { display: flex; border-bottom: 1px solid var(--border); margin-bottom: 20px; }
.tab { flex: 1; text-align: center; padding: 10px; cursor: pointer; color: #666; font-size: 12px; }
.tab.active { color: var(--accent); border-bottom: 2px solid var(--accent); background: linear-gradient(to top, rgba(0,255,204,0.1), transparent); }
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
/* Helper for file tree preview */
#file-tree-preview { height: 150px; overflow-y: auto; font-size: 10px; color: #888; border: 1px solid #222; padding: 5px; margin-top: 10px; }
</style>
</head>
<body>
<div id="header">
<div class="logo">NEURO CONTEXT NAVIGATOR</div>
<div style="font-size: 11px; color: #666;">LOCAL FILE ACCESS // AI ORCHESTRATION</div>
</div>
<div id="main-area">
<div id="left-panel">
<div class="status-row"><span>STATUS</span><span class="stat-val" id="sys-status">IDLE</span></div>
<div class="status-row"><span>TARGET</span><span class="stat-val" id="target-folder">NONE</span></div>
<button class="btn" onclick="nav.selectFolder()">?? SELECT FOLDER</button>
<div id="file-tree-preview">No folder selected.</div>
<div style="height: 1px; background: var(--border); margin: 10px 0;"></div>
<div class="tabs">
<div class="tab active" onclick="nav.setMode('manual')" id="tab-manual">MANUAL</div>
<div class="tab" onclick="nav.setMode('auto')" id="tab-auto">AUTO API</div>
</div>
<div id="manual-controls">
<p style="font-size:11px; color:#888;">
<strong>Manual Mode:</strong><br>
1. Generate Prompt (Map + Question)<br>
2. Copy & Paste to Gemini/ChatGPT<br>
3. Paste AI's "READ_REQUEST" here<br>
4. Repeat until solved.
</p>
</div>
<div id="auto-controls" style="display:none;">
<input type="password" id="api-key" placeholder="Gemini API Key (Auto Mode)">
<p style="font-size:11px; color:#888; margin-top:10px;">
Directly calls Google Gemini API to iterate through files automatically.
</p>
</div>
</div>
<div id="right-panel">
<div id="chat-history">
<div class="msg system"><div class="bubble">Welcome. Select a local folder to begin context navigation.</div></div>
</div>
<div id="input-area">
<textarea id="user-input" placeholder="Enter your requirement here... (e.g., 'Analyze the authentication logic in this project')"></textarea>
<div style="display:flex; flex-direction:column; gap:10px;">
<button class="btn" onclick="nav.send()" style="height: 100%;">EXECUTE</button>
<button class="btn btn-danger" onclick="nav.reset()" style="height: 40px;">RESET</button>
</div>
</div>
</div>
</div>
<script>
// --- CORE LOGIC ---
const nav = {
dirHandle: null,
fileMap: [],
fileHandleMap: {}, // Path -> Handle
mode: 'manual', // 'manual' or 'auto'
step: 'initial', // 'initial' or 'followup'
history: [], // For Auto API
init: () => {
console.log("NeuroContext Navigator Initialized");
},
setMode: (m) => {
nav.mode = m;
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.getElementById(`tab-${m}`).classList.add('active');
document.getElementById('manual-controls').style.display = m === 'manual' ? 'block' : 'none';
document.getElementById('auto-controls').style.display = m === 'auto' ? 'block' : 'none';
},
selectFolder: async () => {
try {
nav.dirHandle = await window.showDirectoryPicker();
document.getElementById('target-folder').innerText = nav.dirHandle.name.toUpperCase();
document.getElementById('sys-status').innerText = "SCANNING...";
nav.fileMap = [];
nav.fileHandleMap = {};
await nav.scanDir(nav.dirHandle, "");
document.getElementById('file-tree-preview').innerText = nav.fileMap.join('\n');
document.getElementById('sys-status').innerText = "READY";
nav.addMsg("System", `Target set: ${nav.dirHandle.name} (${nav.fileMap.length} files mapped).`);
} catch (e) {
console.error(e);
alert("Folder access cancelled or failed.");
}
},
scanDir: async (dirHandle, path) => {
for await (const entry of dirHandle.values()) {
const relativePath = path ? `${path}/${entry.name}` : entry.name;
// Filter out common clutter
if (['node_modules', '.git', 'bin', 'obj', '.vs'].includes(entry.name)) continue;
if (entry.kind === 'file') {
// Extension filter (Optional, allowing text-based files)
const ext = entry.name.split('.').pop().toLowerCase();
if (['exe', 'dll', 'png', 'jpg', 'zip'].includes(ext)) continue;
nav.fileMap.push(relativePath);
nav.fileHandleMap[relativePath] = entry;
} else if (entry.kind === 'directory') {
await nav.scanDir(entry, relativePath);
}
}
},
reset: () => {
nav.step = 'initial';
document.getElementById('chat-history').innerHTML = '';
nav.addMsg("System", "Session reset.");
nav.history = [];
},
send: async () => {
const inputEl = document.getElementById('user-input');
const text = inputEl.value.trim();
if (!text) return;
if (!nav.dirHandle) { alert("Please select a folder first."); return; }
inputEl.value = "";
if (nav.mode === 'manual') {
await nav.processManual(text);
} else {
await nav.processAuto(text);
}
},
// --- MANUAL MODE LOGIC ---
processManual: async (userInput) => {
// If we are in 'initial' step, user input is the QUESTION.
// If we are in 'followup' step, user input is the AI RESPONSE (containing READ_REQUEST).
if (nav.step === 'initial') {
nav.addMsg("User", userInput);
const prompt = nav.buildSystemPrompt() + `\n\n[FILE MAP]:\n${nav.fileMap.join('\n')}\n\n[USER QUESTION]:\n${userInput}`;
nav.addMsg("System", "Copy this prompt and send it to your AI:", prompt);
nav.step = 'followup';
document.getElementById('user-input').placeholder = "Paste the AI's response here (specifically if it asks for READ_REQUEST)...";
} else if (nav.step === 'followup') {
// Parse AI response for READ_REQUEST
nav.addMsg("User", "(Pasted AI Response)");
const requests = userInput.match(/READ_REQUEST:\s*(.+)/g);
if (requests && requests.length > 0) {
let contentBlock = "";
for (let req of requests) {
const path = req.replace("READ_REQUEST:", "").trim();
const content = await nav.readFileContent(path);
contentBlock += `\n--- START FILE: ${path} ---\n${content}\n--- END FILE: ${path} ---\n`;
}
const nextPrompt = `[SYSTEM] Here are the file contents you requested:\n${contentBlock}\n\nAnalyze this and answer the original question, or request more files using 'READ_REQUEST: path'.`;
nav.addMsg("System", "Files read. Copy this next prompt:", nextPrompt);
} else {
nav.addMsg("System", "No READ_REQUEST found. Assuming this is the final answer or conversational text.");
nav.addMsg("AI", userInput);
nav.step = 'initial'; // Reset logic loop
document.getElementById('user-input').placeholder = "Enter new requirement...";
}
}
},
// --- AUTO MODE LOGIC (Gemini) ---
processAuto: async (userInput) => {
const apiKey = document.getElementById('api-key').value;
if (!apiKey) { alert("API Key required for Auto Mode."); return; }
nav.addMsg("User", userInput);
document.getElementById('sys-status').innerText = "AI THINKING...";
// 1. First prompt with Map
if (nav.history.length === 0) {
const sys = nav.buildSystemPrompt();
nav.history.push({ role: "user", parts: [{ text: sys + `\n\n[FILE MAP]:\n${nav.fileMap.join('\n')}\n\n[USER]: ${userInput}` }] });
} else {
nav.history.push({ role: "user", parts: [{ text: userInput }] });
}
try {
let loopCount = 0;
let keepGoing = true;
while (keepGoing && loopCount < 5) {
const response = await nav.callGemini(apiKey, nav.history);
const aiText = response.candidates[0].content.parts[0].text;
nav.history.push({ role: "model", parts: [{ text: aiText }] });
// Check for READ_REQUEST
const requests = aiText.match(/READ_REQUEST:\s*(.+)/g);
if (requests && requests.length > 0) {
nav.addMsg("AI", `I need to read ${requests.length} files...`);
let contentBlock = "";
for (let req of requests) {
const path = req.replace("READ_REQUEST:", "").trim();
const content = await nav.readFileContent(path);
contentBlock += `\n--- FILE: ${path} ---\n${content}\n`;
}
// Feed back to AI
nav.history.push({ role: "user", parts: [{ text: `[SYSTEM] File Contents:\n${contentBlock}` }] });
loopCount++;
} else {
nav.addMsg("AI", aiText);
keepGoing = false;
}
}
} catch (e) {
nav.addMsg("System", "API Error: " + e.message);
}
document.getElementById('sys-status').innerText = "READY";
},
callGemini: async (key, history) => {
const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${key}`;
const res = await fetch(url, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ contents: history })
});
return await res.json();
},
// --- HELPERS ---
buildSystemPrompt: () => {
return `You are a Context Navigator AI.
Goal: Answer the user's question about their codebase/files.
Constraint: You initially only see a FILE MAP (list of paths). You CANNOT see file contents unless you request them.
Protocol:
1. Analyze the User Question and the File Map.
2. If you need to read a file, output EXACTLY: "READ_REQUEST: path/to/file.ext" (one per line).
3. Stop and wait. The user (or system) will provide the content in the next message.
4. Once you have enough info, provide the final answer.
`;
},
readFileContent: async (path) => {
try {
const handle = nav.fileHandleMap[path];
if (!handle) return `[ERROR] File not found in map: ${path}`;
const file = await handle.getFile();
return await file.text();
} catch (e) {
return `[ERROR] Reading file: ${e.message}`;
}
},
addMsg: (role, text, codeToCopy = null) => {
const div = document.createElement('div');
div.className = `msg ${role.toLowerCase()}`;
let html = `<div class="bubble"><strong>${role}:</strong><br>${text.replace(/\n/g, '<br>')}</div>`;
if (codeToCopy) {
html += `<div class="copy-box" onclick="nav.copyText(this)">${codeToCopy.replace(/</g, '<')}</div>`;
}
div.innerHTML = html;
document.getElementById('chat-history').appendChild(div);
document.getElementById('chat-history').scrollTop = document.getElementById('chat-history').scrollHeight;
},
copyText: (el) => {
const text = el.innerText;
navigator.clipboard.writeText(text);
const original = el.style.borderColor;
el.style.borderColor = "#fff";
setTimeout(() => el.style.borderColor = original, 200);
}
};
window.nav = nav;
nav.init();
</script>
</body>
</html>

コメント