megaCode.IDE / index.html
hotateProject's picture
Update index.html
eed9f00 verified
Raw
History Blame Contribute Delete
13.1 kB
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI Web Agent IDE (Search Enabled)</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; height: 100vh; display: flex; flex-direction: column; background: #1e1e2e; color: #cdd6f4; }
/* ヘッダー */
header { height: 50px; background: #181825; display: flex; align-items: center; justify-content: space-between; padding: 0 16px; border-bottom: 1px solid #313244; }
.logo { font-weight: bold; font-size: 16px; color: #89b4fa; }
.btn { background: #45475a; color: #cdd6f4; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 13px; }
.btn:hover { background: #585b70; }
.btn-primary { background: #89b4fa; color: #11111b; font-weight: bold; }
.btn-primary:hover { background: #b4befe; }
/* メインレイアウト */
.main-container { flex: 1; display: grid; grid-template-columns: 220px 1fr 1fr; overflow: hidden; }
/* 1. ファイルツリー */
.sidebar { background: #181825; border-right: 1px solid #313244; padding: 12px; overflow-y: auto; font-size: 13px; }
.sidebar h3 { font-size: 11px; text-transform: uppercase; color: #6c7086; margin-bottom: 8px; }
.file-item { padding: 4px 8px; border-radius: 4px; cursor: pointer; color: #a6adc8; }
.file-item:hover { background: #313244; color: #cdd6f4; }
/* 2. AIチャット */
.chat-panel { display: flex; flex-direction: column; background: #1e1e2e; border-right: 1px solid #313244; }
.messages { flex: 1; padding: 16px; overflow-y: auto; display: flex; flex-direction: column; gap: 12px; }
.msg { padding: 10px 14px; border-radius: 8px; max-width: 85%; font-size: 14px; line-height: 1.5; white-space: pre-wrap; }
.msg.user { background: #313244; align-self: flex-end; }
.msg.ai { background: #45475a; align-self: flex-start; }
.msg.status { background: #313244; color: #f9e2af; align-self: center; font-size: 12px; border: 1px dashed #f9e2af; }
.input-area { padding: 12px; background: #181825; border-top: 1px solid #313244; display: flex; gap: 8px; }
textarea { flex: 1; background: #313244; border: 1px solid #45475a; color: #cdd6f4; padding: 8px; border-radius: 6px; resize: none; height: 50px; font-family: inherit; }
textarea:focus { outline: 1px solid #89b4fa; }
/* 3. プレビュー */
.preview-panel { background: #ffffff; display: flex; flex-direction: column; }
iframe { width: 100%; height: 100%; border: none; background: #fff; }
/* 設定モーダル */
.modal { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.7); justify-content: center; align-items: center; }
.modal.open { display: flex; }
.modal-content { background: #1e1e2e; padding: 24px; border-radius: 8px; width: 420px; border: 1px solid #313244; }
.modal-content h2 { margin-bottom: 16px; font-size: 18px; }
.form-group { margin-bottom: 14px; }
.form-group label { display: block; font-size: 12px; margin-bottom: 4px; color: #a6adc8; }
.form-group input { width: 100%; padding: 8px; background: #313244; border: 1px solid #45475a; color: #fff; border-radius: 4px; }
.form-group small { display: block; font-size: 11px; color: #6c7086; margin-top: 2px; }
</style>
</head>
<body>
<header>
<div class="logo">⚡ ChromeOS AI IDE</div>
<div>
<button class="btn" id="openDirBtn">📁 フォルダを開く</button>
<button class="btn" id="settingsBtn">⚙️ API設定</button>
</div>
</header>
<div class="main-container">
<div class="sidebar">
<h3>Files</h3>
<div id="fileList">フォルダが選択されていません</div>
</div>
<div class="chat-panel">
<div class="messages" id="messages">
<div class="msg ai">こんにちは!Webアプリの作成指示を出してください。<br>TavilyのAPIキーが登録されていると、必要に応じて最新情報を自動検索してコードを作成します!</div>
</div>
<div class="input-area">
<textarea id="promptInput" placeholder="AIへの指示を入力... (Ctrl + Enterで送信)"></textarea>
<button class="btn btn-primary" id="sendBtn">送信</button>
</div>
</div>
<div class="preview-panel">
<iframe id="previewFrame"></iframe>
</div>
</div>
<div class="modal" id="settingsModal">
<div class="modal-content">
<h2>設定</h2>
<div class="form-group">
<label>OpenRouter API Key (必須)</label>
<input type="password" id="openRouterKey" placeholder="sk-or-v1-...">
</div>
<div class="form-group">
<label>Tavily API Key (任意 / Web検索用)</label>
<input type="password" id="tavilyKey" placeholder="tvly-...">
<small>設定しておくと最新仕様のライブラリやエラーの自動検索が可能になります。</small>
</div>
<div style="display: flex; justify-content: flex-end; gap: 8px; margin-top: 20px;">
<button class="btn" id="closeModalBtn">キャンセル</button>
<button class="btn btn-primary" id="saveSettingsBtn">保存</button>
</div>
</div>
</div>
<script>
// --- 状態管理 ---
let dirHandle = null;
let openRouterKey = localStorage.getItem('OPENROUTER_API_KEY') || '';
let tavilyKey = localStorage.getItem('TAVILY_API_KEY') || '';
// --- DOM要素 ---
const openDirBtn = document.getElementById('openDirBtn');
const settingsBtn = document.getElementById('settingsBtn');
const settingsModal = document.getElementById('settingsModal');
const closeModalBtn = document.getElementById('closeModalBtn');
const saveSettingsBtn = document.getElementById('saveSettingsBtn');
const openRouterKeyInput = document.getElementById('openRouterKey');
const tavilyKeyInput = document.getElementById('tavilyKey');
const fileListEl = document.getElementById('fileList');
const sendBtn = document.getElementById('sendBtn');
const promptInput = document.getElementById('promptInput');
const messagesEl = document.getElementById('messages');
const previewFrame = document.getElementById('previewFrame');
openRouterKeyInput.value = openRouterKey;
tavilyKeyInput.value = tavilyKey;
// --- 設定モーダル ---
settingsBtn.onclick = () => settingsModal.classList.add('open');
closeModalBtn.onclick = () => settingsModal.classList.remove('open');
saveSettingsBtn.onclick = () => {
openRouterKey = openRouterKeyInput.value.trim();
tavilyKey = tavilyKeyInput.value.trim();
localStorage.setItem('OPENROUTER_API_KEY', openRouterKey);
localStorage.setItem('TAVILY_API_KEY', tavilyKey);
settingsModal.classList.remove('open');
alert('設定を保存しました!');
};
// --- ファイル操作 ---
openDirBtn.onclick = async () => {
try {
dirHandle = await window.showDirectoryPicker();
await renderFileList();
} catch (err) {
console.error('フォルダ選択キャンセル', err);
}
};
async function renderFileList() {
if (!dirHandle) return;
fileListEl.innerHTML = '';
for await (const entry of dirHandle.values()) {
if (entry.kind === 'file') {
const div = document.createElement('div');
div.className = 'file-item';
div.textContent = '📄 ' + entry.name;
fileListEl.appendChild(div);
}
}
}
async function saveAndPreview(code) {
previewFrame.srcdoc = code;
if (dirHandle) {
try {
const fileHandle = await dirHandle.getFileHandle('index.html', { create: true });
const writable = await fileHandle.createWritable();
await writable.write(code);
await writable.close();
await renderFileList();
} catch (e) {
console.error('保存エラー:', e);
}
}
}
// --- Tavily 検索機能 ---
async function searchTavily(query) {
if (!tavilyKey) return null;
try {
const res = await fetch("https://api.tavily.com/search", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
api_key: tavilyKey,
query: query,
search_depth: "basic",
max_results: 3
})
});
const data = await res.json();
return data.results.map(r => `【タイトル】${r.title}\n【概要】${r.content}`).join("\n\n");
} catch (e) {
console.error("Tavily検索エラー:", e);
return null;
}
}
// --- メイン処理 ---
sendBtn.onclick = handleSend;
promptInput.onkeydown = (e) => {
if (e.key === 'Enter' && e.ctrlKey) handleSend();
};
async function handleSend() {
const userPrompt = promptInput.value.trim();
if (!userPrompt) return;
if (!openRouterKey) {
alert('右上の「⚙️ API設定」から OpenRouter Key を入力してください!');
settingsModal.classList.add('open');
return;
}
addMessage(userPrompt, 'user');
promptInput.value = '';
const statusMsg = addMessage('🔍 指示を分析中...', 'status');
try {
let searchResultsText = '';
// Tavilyキーがある場合、まず検索が必要かAIに判断させる
if (tavilyKey) {
const checkRes = await fetch('https://openrouter.ai/api/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${openRouterKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: 'qwen/qwen-2.5-coder-32b-instruct:free',
messages: [
{
role: 'system',
content: 'ユーザーの要望を叶えるために最新のWeb検索が必要な場合、検索キーワード(1文)だけを出力してください。不要な場合は「NONE」とだけ出力してください。'
},
{ role: 'user', content: userPrompt }
]
})
});
const checkData = await checkRes.json();
const keyword = checkData.choices[0].message.content.trim();
if (keyword && !keyword.includes('NONE')) {
statusMsg.textContent = `🌐 ネット検索中: "${keyword}"`;
const data = await response.json();
// 🛠️ APIからエラーが返ってきた場合の安全チェックを追加
if (data.error) {
throw new Error(`OpenRouterエラー: ${data.error.message || 'モデルが見つかりません(404)'}`);
}
if (!data.choices || !data.choices[0]) {
throw new Error('AIからの返答データが取得できませんでした。');
}
const aiResponse = data.choices[0].message.content;
// 最終的なコード生成リクエスト
const finalResponse = await fetch('https://openrouter.ai/api/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${openRouterKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: 'meta-llama/llama-3.3-70b-instruct:free',
messages: [
{
role: 'system',
content: 'あなたはUI/UX設計やさまざまなプログラム言語を理解した専門家です。ユーザーの要望に沿った完全なコードを出力してください。初心者でもわかるようにわかりやすく書いてください'
},
{
role: 'user',
content: userPrompt + searchResultsText
}
]
})
});
const finalData = await finalResponse.json();
const aiResponse = finalData.choices[0].message.content;
const codeMatch = aiResponse.match(/```html([\s\S]*?)```/) || aiResponse.match(/```([\s\S]*?)```/);
const cleanCode = codeMatch ? codeMatch[1].trim() : aiResponse;
statusMsg.textContent = '✅ 生成完了!プレビューとファイルに反映しました。';
statusMsg.className = 'msg ai';
await saveAndPreview(cleanCode);
} catch (err) {
console.error(err);
statusMsg.textContent = '❌ エラーが発生しました。APIキーや通信状態を確認してください。';
}
}
function addMessage(text, sender) {
const div = document.createElement('div');
div.className = `msg ${sender}`;
div.textContent = text;
messagesEl.appendChild(div);
messagesEl.scrollTop = messagesEl.scrollHeight;
return div;
}
</script>
</body>
</html>