// 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 = true;
}
let content = parseInlineMarkdown(listMatch[2]);
htmlLines.push(`${content} `);
continue;
} else 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, '');
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 = `
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 = `
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 = `
`;
} 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 = ` `;
} 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);
});
}