Spaces:
Running
Running
| <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> |