// StrataGravity IDE - Gemini & Antigravity Style Command Center let currentFile = "index.html"; let isComposing = false; let currentModelMode = "auto"; let attachedImageBase64 = null; let ws = null; const renderedMessageIds = new Set(); // ==================== 初期化 ==================== document.addEventListener("DOMContentLoaded", () => { initWebSocket(); initIMEHandling(); initImageAttachment(); initEmergencyStop(); initModelPills(); initArtifactsPane(); initWorkspaceEditor(); initTerminal(); initApkBuilder(); initNetworkInfo(); initShortcuts(); initSessionsDrawer(); initMemoryManager(); // 会話履歴の復元・同期 loadChatHistory(); loadMediaGallery(); loadSessionsList(); // プレビュー更新ボタン document.getElementById("btn-refresh-preview").addEventListener("click", refreshPreview); document.getElementById("btn-refresh-media").addEventListener("click", loadMediaGallery); // モーダル制御 const modal = document.getElementById("mobile-modal"); document.getElementById("btn-show-mobile-qr").addEventListener("click", () => { modal.classList.add("active"); }); document.getElementById("btn-close-modal").addEventListener("click", () => { modal.classList.remove("active"); }); // 参照ソースクリアボタン const clearSourcesBtn = document.getElementById("btn-clear-sources"); if (clearSourcesBtn) { clearSourcesBtn.addEventListener("click", () => { renderSources([], ""); }); } }); // ==================== WebSocket 通信 (全端末リアルタイム同期) ==================== function initWebSocket() { const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; const wsUrl = `${protocol}//${window.location.host}/ws/agent`; ws = new WebSocket(wsUrl); ws.onopen = () => { console.log("[WebSocket] Connected to StrataGravity backend."); document.getElementById("status-dot").style.background = "#10b981"; document.getElementById("system-status-text").textContent = "Strata エンジン稼働中 (同期接続)"; }; ws.onmessage = (event) => { try { const msg = JSON.parse(event.data); if (msg.type === "chat_message") { renderSingleMessage(msg.message); } else if (msg.type === "session_switched") { renderAllMessages(msg.messages || []); loadSessionsList(); } else if (msg.type === "session_deleted") { renderAllMessages(msg.messages || []); loadSessionsList(); } else if (msg.type === "agent_status") { handleAgentStatus(msg.data); if (msg.data && msg.data.step === "sources" && msg.data.data && msg.data.data.sources) { renderSources(msg.data.data.sources, msg.data.data.query); } } else if (msg.type === "search_sources") { renderSources(msg.sources || [], msg.query || ""); } else if (msg.type === "terminal_output") { appendTerminal(msg.text); } else if (msg.type === "system_stopped") { hideAgentStatus(); renderSingleMessage(msg.message); document.getElementById("system-status-text").textContent = "全動作停止中 (メモリ解放済)"; document.getElementById("status-dot").style.background = "#ef4444"; } } catch (e) { console.error("WS Parse error", e); } }; ws.onclose = () => { console.warn("[WebSocket] Disconnected. Reconnecting..."); document.getElementById("status-dot").style.background = "#ef4444"; document.getElementById("system-status-text").textContent = "再接続待機中..."; setTimeout(initWebSocket, 3000); }; } // ==================== チャット履歴同期 & 描画 ==================== function renderAllMessages(messages) { const container = document.getElementById("chat-messages"); container.innerHTML = ""; renderedMessageIds.clear(); if (Array.isArray(messages)) { messages.forEach(msg => { renderSingleMessage(msg); }); } setTimeout(() => { container.scrollTop = container.scrollHeight; }, 60); } async function loadChatHistory() { try { const res = await fetch("/api/chat/history"); const history = await res.json(); renderAllMessages(history); } catch (e) { console.error("Failed to load chat history", e); } } function renderSingleMessage(msg) { if (renderedMessageIds.has(msg.id)) return; renderedMessageIds.add(msg.id); const container = document.getElementById("chat-messages"); const item = document.createElement("div"); item.className = `message-item message-${msg.role}`; item.id = msg.id; const bubble = document.createElement("div"); bubble.className = "message-bubble"; if (msg.role === "user") { bubble.textContent = msg.content; } else { bubble.innerHTML = renderMarkdownHtml(msg.content); } // 添付画像 if (msg.image) { const imgEl = document.createElement("img"); imgEl.src = msg.image; imgEl.className = "chat-image-attachment"; imgEl.alt = "添付画像"; imgEl.addEventListener("click", () => window.open(msg.image, "_blank")); bubble.appendChild(imgEl); } // アシスタントメッセージで成果物が含まれる場合、メディアやカードを埋め込む if (msg.role === "assistant") { detectAndEmbedMedia(bubble, msg.content); } item.appendChild(bubble); const meta = document.createElement("div"); meta.className = "message-meta"; const timeStr = msg.timestamp || ""; const modelStr = msg.model ? ` · ${msg.model}` : ""; meta.textContent = `${timeStr}${modelStr}`; item.appendChild(meta); container.appendChild(item); container.scrollTop = container.scrollHeight; requestAnimationFrame(() => { container.scrollTop = container.scrollHeight; }); } // ==================== マークダウン HTML レンダラー (Gemini / ChatGPT スタイル) ==================== function renderMarkdownHtml(rawText) { if (!rawText) return ""; let lines = rawText.split("\n"); let inCodeBlock = false; let codeBlockLang = ""; let codeBlockLines = []; let htmlLines = []; let inList = false; for (let i = 0; i < lines.length; i++) { let line = lines[i]; // コードブロック開始・終了 if (line.trim().startsWith("```")) { if (!inCodeBlock) { inCodeBlock = true; codeBlockLang = line.trim().slice(3).trim(); codeBlockLines = []; continue; } else { inCodeBlock = false; if (codeBlockLang.startsWith("action:")) { htmlLines.push(`
⚡ 実行ツール: ${escapeHtml(codeBlockLang)}
`); } else { htmlLines.push(`
${escapeHtml(codeBlockLines.join("\n"))}
`); } continue; } } if (inCodeBlock) { codeBlockLines.push(line); continue; } // リスト項目 (- または * または 1.) let listMatch = line.match(/^(\s*)[-*]\s+(.*)$/); if (listMatch) { if (!inList) { htmlLines.push(''); inList = false; } // details / summary タグ (思考プロセス用) if (line.trim().startsWith("") || line.trim().startsWith("")) { htmlLines.push(line); continue; } // 見出し if (line.startsWith("#### ")) { htmlLines.push(`

${parseInlineMarkdown(line.slice(5))}

`); } else if (line.startsWith("### ")) { htmlLines.push(`

${parseInlineMarkdown(line.slice(4))}

`); } else if (line.startsWith("## ")) { htmlLines.push(`

${parseInlineMarkdown(line.slice(3))}

`); } else if (line.startsWith("# ")) { htmlLines.push(`

${parseInlineMarkdown(line.slice(2))}

`); } else if (line.startsWith("> ")) { htmlLines.push(`
${parseInlineMarkdown(line.slice(2))}
`); } else if (line.trim() === "---") { htmlLines.push('
'); } else if (line.trim() === "") { htmlLines.push('
'); } else { htmlLines.push(`

${parseInlineMarkdown(line)}

`); } } if (inList) { htmlLines.push(''); } return htmlLines.join("\n"); } function parseInlineMarkdown(str) { if (!str) return ""; let s = escapeHtml(str); s = s.replace(/!\[([^\]]*)\]\((\/api\/media\/[^\)]+)\)/g, '
$1
'); s = s.replace(/\[Generated Video\]\((\/api\/media\/[^\)]+)\)/g, '
'); s = s.replace(/\*\*(.*?)\*\*/g, '$1'); s = s.replace(/\*(.*?)\*/g, '$1'); s = s.replace(/`([^`]+)`/g, '$1'); s = s.replace(/\[([^\]]+)\]\((https?:\/\/[^\)]+)\)/g, '$1 ↗'); return s; } // メッセージ内のアクション(画像、音声、動画、コード)を検知してUIを豊かにする function detectAndEmbedMedia(bubble, text) { if (text.includes("action:create_file") || text.includes("action:planning")) { const card = document.createElement("div"); card.className = "artifact-notify-card"; card.innerHTML = `
🌐 アプリケーション成果物を生成しました
`; bubble.appendChild(card); refreshPreview(); loadWorkspaceTree(); } // Krea 2 スタイルの画像生成インライン埋め込み (メッセージ内に直接画像タグがない場合のみローディングカード作成) if (text.includes("action:generate_image") && !text.includes("![Generated Image]")) { const card = document.createElement("div"); card.className = "inline-media-card inline-image-card"; card.innerHTML = `
🎨 ComfyUI Qwen-Rapid AI高画質画像生成中
🎨 AIが指示に従って画像を生成・編集中です... (約20秒)
RTX 4070 Ti 12GB 最適化推論
`; bubble.appendChild(card); loadMediaGallery(); } // H3 / Hailuo スタイルの動画生成インライン埋め込み if (text.includes("action:generate_video") && !text.includes("[Generated Video]")) { const card = document.createElement("div"); card.className = "inline-media-card inline-video-card"; card.innerHTML = `
🎬 H3 Video Engine シネマティック動画生成中
動画をレンダリング中...
MP4動画 (24fps)
`; bubble.appendChild(card); loadMediaGallery(); } if (text.includes("action:generate_audio")) { const card = document.createElement("div"); card.className = "artifact-notify-card"; card.innerHTML = `
🎙️ 音声ファイルを生成しました
`; bubble.appendChild(card); loadMediaGallery(); } if (text.includes("参照ソース") || text.includes("Webソース") || text.includes("Web検索")) { const card = document.createElement("div"); card.className = "chat-sources-trigger"; card.innerHTML = `
🔍 参照したWebソース・詳細リンクを見る
➔ タブを開く `; card.addEventListener("click", () => openArtifactTab('tab-sources')); bubble.appendChild(card); } } // 最新の生成メディアを非同期でフェッチしてインラインプレビュー async function fetchLatestMedia(type, container) { if (!container) return; for (let attempt = 0; attempt < 5; attempt++) { try { await new Promise(r => setTimeout(r, attempt === 0 ? 600 : 1000)); const res = await fetch("/api/media/list"); const list = await res.json(); const item = list.find(m => m.type === type); if (item) { if (type === "image") { container.innerHTML = ` ${escapeHtml(item.prompt || 'Generated Image')} `; } else if (type === "video") { container.innerHTML = ` `; } return; } } catch (e) { console.warn("Fetch media retry attempt:", attempt, e); } } container.innerHTML = `
メディア成果物は右ペインの「生成メディア」タブで確認・再生できます
`; } // ==================== 参照ソース (Web情報) 描画 ==================== let currentSourcesList = []; function renderSources(sources, query = "") { const container = document.getElementById("sources-list-container"); const countBadge = document.getElementById("sources-count-badge"); const queryLabel = document.getElementById("sources-query-label"); if (!sources || sources.length === 0) { container.innerHTML = `
質問や調査を行うと、AIが参照したWebサイトやリンクがここに自動表示されます。
`; if (countBadge) countBadge.textContent = "0"; return; } currentSourcesList = sources; if (countBadge) countBadge.textContent = sources.length; if (queryLabel && query) queryLabel.textContent = `クエリ: 「${query}」 (${sources.length}件)`; container.innerHTML = ""; sources.forEach((item, index) => { const card = document.createElement("div"); card.className = "source-card"; const title = item.title || "Webページ"; const url = item.url || "#"; const snippet = item.snippet || ""; card.innerHTML = `
🔗 ${escapeHtml(url)}
${snippet ? `
${escapeHtml(snippet)}
` : ''} ↗ ブラウザで開く `; container.appendChild(card); }); } function escapeHtml(str) { if (!str) return ""; return String(str) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } // ==================== 成果物 (Artifacts) パネル制御 ==================== function initArtifactsPane() { const pane = document.getElementById("artifacts-pane"); const toggleBtn = document.getElementById("btn-toggle-artifacts"); const closeBtn = document.getElementById("btn-close-artifacts"); // モバイル端末(画面幅900px以下)では初期状態で閉じてチャットを全画面表示 if (window.innerWidth <= 900) { pane.classList.add("closed"); } toggleBtn.addEventListener("click", () => { pane.classList.toggle("closed"); }); closeBtn.addEventListener("click", () => { pane.classList.add("closed"); }); // タブ切り替え const tabItems = document.querySelectorAll(".artifacts-tabs .tab-item"); tabItems.forEach(tab => { tab.addEventListener("click", () => { tabItems.forEach(t => t.classList.remove("active")); document.querySelectorAll(".artifact-tab-content").forEach(c => c.classList.remove("active")); tab.classList.add("active"); const targetId = tab.getAttribute("data-tab"); document.getElementById(targetId).classList.add("active"); }); }); } function openArtifactTab(tabId) { const pane = document.getElementById("artifacts-pane"); pane.classList.remove("closed"); document.querySelectorAll(".artifacts-tabs .tab-item").forEach(t => { t.classList.toggle("active", t.getAttribute("data-tab") === tabId); }); document.querySelectorAll(".artifact-tab-content").forEach(c => { c.classList.toggle("active", c.id === tabId); }); } // ==================== 生成メディアギャラリー ==================== async function loadMediaGallery() { try { const res = await fetch("/api/media/list"); const mediaList = await res.json(); const container = document.getElementById("media-gallery"); if (mediaList.length === 0) { container.innerHTML = `
チャットで「〜の画像を生成して」「このテキストを音声で読んで」「動画を作って」と指示すると、ここに成果物がストックされます。
`; return; } container.innerHTML = ""; mediaList.forEach(item => { const card = document.createElement("div"); card.className = "media-card"; let previewHtml = ""; if (item.type === "image") { previewHtml = `${item.filename}`; } else if (item.type === "audio") { previewHtml = ``; } else if (item.type === "video") { previewHtml = ``; } card.innerHTML = `
${item.filename}
${previewHtml}
`; container.appendChild(card); }); } catch (e) { console.error("Failed to load media gallery", e); } } // ==================== 画像添付 & クリップボード対応 ==================== function initImageAttachment() { const fileInput = document.getElementById("image-file-input"); const triggerBtn = document.getElementById("btn-trigger-file-input"); const removeBtn = document.getElementById("btn-remove-image"); const chatInput = document.getElementById("chat-input"); triggerBtn.addEventListener("click", () => fileInput.click()); fileInput.addEventListener("change", (e) => { const file = e.target.files[0]; if (file) handleImageFile(file); }); removeBtn.addEventListener("click", clearAttachedImage); // クリップボードからの貼り付け (Ctrl+V) chatInput.addEventListener("paste", (e) => { const items = (e.clipboardData || e.originalEvent.clipboardData).items; for (const item of items) { if (item.type.indexOf("image") !== -1) { const blob = item.getAsFile(); handleImageFile(blob); break; } } }); // ドラッグ&ドロップ対応 chatInput.addEventListener("dragover", (e) => e.preventDefault()); chatInput.addEventListener("drop", (e) => { e.preventDefault(); if (e.dataTransfer.files && e.dataTransfer.files[0]) { handleImageFile(e.dataTransfer.files[0]); } }); } function handleImageFile(file) { if (!file.type.startsWith("image/")) { alert("画像ファイルを選択してください"); return; } const reader = new FileReader(); reader.onload = (e) => { attachedImageBase64 = e.target.result; document.getElementById("attached-img-thumb").src = attachedImageBase64; document.getElementById("attached-img-name").textContent = file.name || "貼り付け画像"; document.getElementById("image-preview-bar").style.display = "flex"; }; reader.readAsDataURL(file); } function clearAttachedImage() { attachedImageBase64 = null; document.getElementById("image-preview-bar").style.display = "none"; document.getElementById("attached-img-thumb").src = ""; document.getElementById("image-file-input").value = ""; } // ==================== 全動作停止・メモリ解放 ==================== function initEmergencyStop() { const stopBtn = document.getElementById("btn-emergency-stop"); stopBtn.addEventListener("click", async () => { if (!confirm("実行中のタスク、ビルド、Strata推論サーバーをすべて停止し、ゲーム用にVRAM・メモリを全解放しますか?")) { return; } try { stopBtn.textContent = "解放中..."; stopBtn.disabled = true; const res = await fetch("/api/system/emergency_stop", { method: "POST" }); const data = await res.json(); hideAgentStatus(); console.log("Emergency stop result:", data); } catch (e) { alert("停止要求エラー: " + e.message); } finally { stopBtn.innerHTML = "🛑 全停止・メモリ解放"; stopBtn.disabled = false; } }); } // ==================== IME 安全ハンドリング ==================== function initIMEHandling() { const chatInput = document.getElementById("chat-input"); chatInput.addEventListener("compositionstart", () => { isComposing = true; }); chatInput.addEventListener("compositionend", () => { isComposing = false; }); chatInput.addEventListener("keydown", (e) => { // 日本語変換確定時のEnterは送信しない if (e.isComposing || isComposing) return; if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); sendChatMessage(); } }); document.getElementById("btn-send-chat").addEventListener("click", () => { if (isGenerating) { stopCurrentAgent(); } else { sendChatMessage(); } }); const stopAgentBtn = document.getElementById("btn-stop-agent"); if (stopAgentBtn) { stopAgentBtn.addEventListener("click", stopCurrentAgent); } // Escapeキーで思考停止 window.addEventListener("keydown", (e) => { if (e.key === "Escape" && isGenerating) { stopCurrentAgent(); } }); } let isGenerating = false; function setGeneratingState(generating) { isGenerating = generating; const sendBtn = document.getElementById("btn-send-chat"); if (!sendBtn) return; if (generating) { sendBtn.classList.add("btn-stopping"); sendBtn.innerHTML = `⏹️ 停止`; sendBtn.title = "思考・生成をその場で中断 (Escキーでも停止可能)"; } else { sendBtn.classList.remove("btn-stopping"); sendBtn.innerHTML = `送信➔`; sendBtn.title = "送信"; } } async function stopCurrentAgent() { try { showAgentStatus("stopped", "思考停止中...", "処理を中断しています"); await fetch("/api/chat/stop", { method: "POST" }); } catch (err) { console.error("Stop agent error:", err); } finally { setGeneratingState(false); hideAgentStatus(); } } // ==================== チャット送信 ==================== async function sendChatMessage() { if (isGenerating) { await stopCurrentAgent(); return; } const input = document.getElementById("chat-input"); const prompt = input.value.trim(); if (!prompt && !attachedImageBase64) return; const currentImg = attachedImageBase64; input.value = ""; clearAttachedImage(); setGeneratingState(true); showAgentStatus("thinking", "Strata知能エンジン思考中..."); const searchEl = document.getElementById("chk-enable-search"); const enableSearch = searchEl ? searchEl.checked : true; try { const res = await fetch("/api/chat", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ prompt: prompt || "この画像に基づいて処理を実行してください。", image: currentImg, model_mode: currentModelMode, task_mode: currentTaskMode, enable_search: enableSearch }) }); const data = await res.json(); hideAgentStatus(); if (data.sources && Array.isArray(data.sources) && data.sources.length > 0) { renderSources(data.sources, prompt); } if (data.success) { loadWorkspaceTree(); refreshPreview(); loadMediaGallery(); } } catch (err) { hideAgentStatus(); console.error("Chat error:", err); } finally { setGeneratingState(false); } } // エージェント進捗表示 function handleAgentStatus(data) { const stepLabels = { "searching": "🔍 Web検索グラウンディング", "grounding": "🌐 参照コンテキスト統合", "thinking": "🧠 タスク計画 & 思考中", "planning": "📋 開発計画立案", "writing_file": "📝 ファイル作成・更新", "running_command": "⚙️ ターミナルコマンド実行", "generating_media": "🎨 メディア成果物生成中", "building_apk": "📦 Android APKビルド", "debugging": "🛠️ 自己修復デバッグ", "completed": "✅ 全タスク完了", "stopped": "🛑 停止完了", "error": "❌ エラー" }; showAgentStatus(data.step, stepLabels[data.step] || data.step, data.detail); if (data.step === "completed" || data.step === "stopped") { setTimeout(hideAgentStatus, 4000); loadWorkspaceTree(); refreshPreview(); loadMediaGallery(); } } function showAgentStatus(step, label, detail = "") { const panel = document.getElementById("agent-status-panel"); const labelEl = document.getElementById("agent-step-label"); const detailEl = document.getElementById("agent-step-detail"); panel.style.display = "flex"; labelEl.textContent = label; detailEl.textContent = detail; } function hideAgentStatus() { document.getElementById("agent-status-panel").style.display = "none"; } // ==================== モードピル選択 (Auto / 写真 / 動画 / コード / 音楽 / ディープリサーチ) ==================== let currentTaskMode = "auto"; function initModelPills() { const pills = document.querySelectorAll(".mode-pill-selector .mode-pill-btn, .model-pill-selector .model-pill-btn"); pills.forEach(pill => { pill.addEventListener("click", () => { pills.forEach(p => p.classList.remove("active")); pill.classList.add("active"); currentTaskMode = pill.getAttribute("data-mode"); currentModelMode = (currentTaskMode === "code") ? "coder" : "auto"; const placeholders = { "auto": "質問、最新情報の調査、アプリの作成、画像・音声・動画の生成など何でも指示できます...", "photo": "写真・画像生成モード: 被写体、シチュエーション、アングル、衣装などを指定してください...", "video": "動画生成モード: カメラワーク、被写体のアクション、世界観などを指定してください...", "code": "コード生成モード: 作成したいWebアプリ、ツール、機能要件を指定してください...", "music": "音楽生成モード: 読み上げたいテキストやイメージを指定してください...", "deep_research": "ディープリサーチモード: 徹底的に深掘り調査したいキーワードや課題を入力してください..." }; const input = document.getElementById("chat-input"); if (input && placeholders[currentTaskMode]) { input.placeholder = placeholders[currentTaskMode]; } }); }); } // ==================== ワークスペース & コード ==================== async function initWorkspaceEditor() { await loadWorkspaceTree(); loadFileContent("index.html"); document.getElementById("btn-save-file").addEventListener("click", saveCurrentFile); } async function loadWorkspaceTree() { try { const res = await fetch("/api/files/tree"); const tree = await res.json(); renderFileTree(tree); } catch (e) { console.error("Failed to load tree", e); } } function renderFileTree(nodes) { const container = document.getElementById("file-tree"); container.innerHTML = ""; function traverse(list) { list.forEach(node => { const el = document.createElement("div"); el.className = "tree-file-node" + (node.path === currentFile ? " active" : ""); el.textContent = (node.type === "directory" ? "📁 " : "📄 ") + node.name; if (node.type === "file") { el.addEventListener("click", () => { document.querySelectorAll(".tree-file-node").forEach(n => n.classList.remove("active")); el.classList.add("active"); loadFileContent(node.path); }); } container.appendChild(el); if (node.children) traverse(node.children); }); } traverse(nodes); } async function loadFileContent(filePath) { currentFile = filePath; document.getElementById("editor-file-path").textContent = filePath; try { const res = await fetch(`/api/files/read?path=${encodeURIComponent(filePath)}`); const data = await res.json(); document.getElementById("code-content").value = data.content; } catch (e) { console.error("Failed to read file", e); } } async function saveCurrentFile() { const content = document.getElementById("code-content").value; try { const res = await fetch("/api/files/write", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ path: currentFile, content: content }) }); const data = await res.json(); if (data.success) { refreshPreview(); alert("ファイルを保存しました"); } } catch (e) { alert("保存失敗: " + e.message); } } function refreshPreview() { const iframe = document.getElementById("preview-frame"); iframe.src = `/preview/index.html?t=${Date.now()}`; } // ==================== ターミナル ==================== function initTerminal() { const input = document.getElementById("terminal-input"); input.addEventListener("keydown", async (e) => { if (e.key === "Enter") { const cmd = input.value.trim(); if (!cmd) return; input.value = ""; await fetch("/api/terminal/execute", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ command: cmd }) }); } }); } function appendTerminal(text) { const consoleEl = document.getElementById("terminal-console"); consoleEl.textContent += text; consoleEl.scrollTop = consoleEl.scrollHeight; } // ==================== APK ビルド ==================== function initApkBuilder() { const btn = document.getElementById("btn-trigger-apk-build"); btn.addEventListener("click", async () => { btn.disabled = true; btn.textContent = "ビルド中..."; try { const res = await fetch("/api/apk/build", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ app_name: "StrataApp", package_id: "com.strata.app" }) }); const data = await res.json(); if (data.success) { const resultCard = document.getElementById("apk-result-card"); resultCard.style.display = "block"; document.getElementById("apk-details").textContent = `${data.filename} (${(data.size / 1024).toFixed(1)} KB)`; document.getElementById("apk-qr-image").src = data.qr_code; const link = document.getElementById("apk-direct-download-link"); link.href = data.download_url; link.download = data.filename; } else { alert("APKビルド失敗: " + data.error); } } catch (e) { alert("APKビルド要求エラー: " + e.message); } finally { btn.disabled = false; btn.innerHTML = "🚀 Android APKをビルドする"; } }); } // ==================== ネットワーク & QR ==================== async function initNetworkInfo() { try { const res = await fetch("/api/network/info"); const data = await res.json(); document.getElementById("lan-qr-image").src = data.qr_code; document.getElementById("lan-url-text").textContent = data.server_url; document.getElementById("modal-lan-qr").src = data.qr_code; document.getElementById("modal-lan-url").textContent = data.server_url; } catch (e) { console.error("Failed to load network info", e); } } // ==================== ショートカット ==================== function initShortcuts() { document.addEventListener("keydown", (e) => { if ((e.ctrlKey || e.metaKey) && e.key === "s") { e.preventDefault(); saveCurrentFile(); } }); } // ==================== 会話セッション管理 (マルチスレッド) ==================== function initSessionsDrawer() { const drawer = document.getElementById("sessions-drawer"); const overlay = document.getElementById("sessions-drawer-overlay"); const toggleBtn = document.getElementById("btn-toggle-sessions"); const closeBtn = document.getElementById("btn-close-sessions"); const newBtn = document.getElementById("btn-new-session"); function openDrawer() { drawer.classList.add("active"); overlay.classList.add("active"); loadSessionsList(); } function closeDrawer() { drawer.classList.remove("active"); overlay.classList.remove("active"); } if (toggleBtn) toggleBtn.addEventListener("click", openDrawer); if (closeBtn) closeBtn.addEventListener("click", closeDrawer); if (overlay) overlay.addEventListener("click", closeDrawer); if (newBtn) { newBtn.addEventListener("click", async () => { try { const res = await fetch("/api/sessions", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ title: "新しい会話" }) }); const data = await res.json(); if (data.session) { renderAllMessages(data.session.messages || []); } closeDrawer(); loadSessionsList(); } catch (e) { alert("新規会話の作成に失敗しました: " + e.message); } }); } } async function loadSessionsList() { const container = document.getElementById("sessions-list-container"); if (!container) return; try { const res = await fetch("/api/sessions"); const data = await res.json(); const sessions = data.sessions || []; const currentId = data.current_session_id; container.innerHTML = ""; if (sessions.length === 0) { container.innerHTML = `
会話履歴がありません
`; return; } sessions.forEach(sess => { const item = document.createElement("div"); item.className = `session-item ${sess.id === currentId ? "active" : ""}`; const info = document.createElement("div"); info.className = "session-info"; info.innerHTML = ` ${sess.title}
${sess.updated_at || ""} · ${sess.message_count}件
`; info.addEventListener("click", async () => { if (sess.id === currentId) { document.getElementById("sessions-drawer").classList.remove("active"); document.getElementById("sessions-drawer-overlay").classList.remove("active"); return; } try { const switchRes = await fetch("/api/sessions/switch", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ session_id: sess.id }) }); const switchData = await switchRes.json(); if (switchData.session) { renderAllMessages(switchData.session.messages || []); } document.getElementById("sessions-drawer").classList.remove("active"); document.getElementById("sessions-drawer-overlay").classList.remove("active"); loadSessionsList(); } catch (e) { alert("会話の切り替えに失敗しました: " + e.message); } }); const delBtn = document.createElement("button"); delBtn.className = "session-delete-btn"; delBtn.innerHTML = "🗑️"; delBtn.title = "この会話を削除"; delBtn.addEventListener("click", async (e) => { e.stopPropagation(); if (!confirm(`「${sess.title}」を削除してもよろしいですか?`)) return; try { const delRes = await fetch(`/api/sessions/${sess.id}`, { method: "DELETE" }); const delData = await delRes.json(); loadSessionsList(); loadChatHistory(); } catch (err) { alert("会話の削除に失敗しました: " + err.message); } }); item.appendChild(info); item.appendChild(delBtn); container.appendChild(item); }); } catch (e) { console.error("Failed to load sessions list", e); } } // ==================== 記憶(メモリー)管理 ==================== function initMemoryManager() { const modal = document.getElementById("memory-modal"); const openBtn = document.getElementById("btn-open-memory-modal"); const closeBtn = document.getElementById("btn-close-memory"); const addBtn = document.getElementById("btn-add-memory"); const inputEl = document.getElementById("memory-input"); if (openBtn) { openBtn.addEventListener("click", () => { modal.classList.add("active"); loadMemories(); }); } if (closeBtn) { closeBtn.addEventListener("click", () => { modal.classList.remove("active"); }); } if (modal) { modal.addEventListener("click", (e) => { if (e.target === modal) { modal.classList.remove("active"); } }); } if (addBtn && inputEl) { addBtn.addEventListener("click", async () => { const text = inputEl.value.trim(); if (!text) return; try { const res = await fetch("/api/memory", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text }) }); const data = await res.json(); inputEl.value = ""; renderMemoryList(data.memories || []); } catch (e) { alert("記憶の追加に失敗しました: " + e.message); } }); inputEl.addEventListener("keydown", (e) => { if (e.key === "Enter") { addBtn.click(); } }); } } async function loadMemories() { try { const res = await fetch("/api/memory"); const data = await res.json(); renderMemoryList(data.memories || []); } catch (e) { console.error("Failed to load memories", e); } } function renderMemoryList(memories) { const container = document.getElementById("memory-items-container"); if (!container) return; container.innerHTML = ""; if (!memories || memories.length === 0) { container.innerHTML = `
登録された記憶事項はありません
`; return; } memories.forEach((mem, idx) => { const card = document.createElement("div"); card.className = "memory-item-card"; const textEl = document.createElement("div"); textEl.className = "memory-item-text"; textEl.textContent = `・ ${mem}`; const delBtn = document.createElement("button"); delBtn.className = "memory-item-delete"; delBtn.textContent = "✕ 削除"; delBtn.addEventListener("click", async () => { try { const res = await fetch(`/api/memory/${idx}`, { method: "DELETE" }); const data = await res.json(); renderMemoryList(data.memories || []); } catch (e) { alert("記憶の削除に失敗しました: " + e.message); } }); card.appendChild(textEl); card.appendChild(delBtn); container.appendChild(card); }); }