Pinterest
クリックまたはドラッグ&ドロップ
Etsy
クリックまたはドラッグ&ドロップ
Analysis
Save
}); if (res.ok) { localStorage.setItem('authed', '1'); showApp(); } else { document.getElementById('loginError').style.display = 'block'; } } catch (e) { document.getElementById('loginError').style.display = 'block'; } } function showApp() { document.getElementById('loginScreen').style.display = 'none'; document.getElementById('appLayout').style.display = 'flex'; loadSize(); const context = localStorage.getItem('mt_research_context') || ''; document.getElementById('researchContext').value = context; window.gsLastResult = JSON.parse(localStorage.getItem('mt_cache_google') || 'null'); // ── Google Search UI復元 ── if (window.gsLastResult?.suggestions) { const resultEl = document.getElementById('gs-result'); const actionsEl = document.getElementById('gs-actions'); if (resultEl) { resultEl.innerHTML = `
${window.gsLastResult.suggestions.join('\n')}

📂 GitHubから復元(${window.gsLastResult.keyword} · ${window.gsLastResult.timestamp?.slice(0, 10)})

`; if (actionsEl) actionsEl.style.display = 'flex'; } } // ── GitHubから検索履歴を復元(localStorageが空の場合)── if (!window.gsLastResult) { restoreFromGitHub(); } } async function restoreFromGitHub() { try { // 今日のファイルがあればそれを使う。無ければ一覧から最新を探す const today = new Date().toISOString().slice(0, 10); let path = `data/backup_${today}.json`; let res = await fetch(`/api/github?path=${encodeURIComponent(path)}`, { headers: { Accept: 'application/vnd.github.v3+json' } }); if (!res.ok) { // 今日のファイルが無ければ data/ 一覧から最新backupを探す const listRes = await fetch(`/api/github?path=${encodeURIComponent('data')}`, { headers: { Accept: 'application/vnd.github.v3+json' } }); if (!listRes.ok) return; const files = await listRes.json(); const backups = files .filter(f => /^backup_\d{4}-\d{2}-\d{2}\.json$/.test(f.name)) .sort((a, b) => b.name.localeCompare(a.name)); if (backups.length === 0) return; path = `data/${backups[0].name}`; res = await fetch(`/api/github?path=${encodeURIComponent(path)}`, { headers: { Accept: 'application/vnd.github.v3+json' } }); if (!res.ok) return; } const json = await res.json(); const content = JSON.parse(decodeURIComponent(escape(atob(json.content.replace(/\n/g, ''))))); // Google Search復元 if (content.cache_google) { window.gsLastResult = content.cache_google; localStorage.setItem('mt_cache_google', JSON.stringify(content.cache_google)); const resultEl = document.getElementById('gs-result'); const actionsEl = document.getElementById('gs-actions'); if (resultEl && window.gsLastResult.suggestions) { resultEl.innerHTML = `
${window.gsLastResult.suggestions.join('\n')}

📂 GitHubから復元(${window.gsLastResult.keyword} · ${window.gsLastResult.timestamp?.slice(0, 10)})

`; if (actionsEl) actionsEl.style.display = 'flex'; } } // Save系(Pinterest / Etsy / Analysis / Prompts)復元 const saveKeys = ['mt_save_google', 'mt_save_pinterest', 'mt_save_etsy', 'mt_save_analysis']; saveKeys.forEach(key => { if (content[key] !== undefined && !localStorage.getItem(key)) { localStorage.setItem(key, JSON.stringify(content[key])); } }); if (typeof renderSave === 'function') renderSave(); } catch (e) { /* 復元失敗は無視 */ } } // ── Nav ── function switchTool(tool) { document.querySelectorAll('.nav-item').forEach(el => el.classList.toggle('active', el.dataset.tool === tool)); document.querySelectorAll('.tool-panel').forEach(el => el.classList.toggle('active', el.id === 'panel-' + tool)); if (tool === 'save') renderSave(); } // ── Font size ── function setSize(k) { document.documentElement.style.setProperty('--fs-base', { s: '9px', m: '11px', l: '14px' }[k]); try { localStorage.setItem('mt_size', k); } catch (e) { } } function loadSize() { setSize(localStorage.getItem('mt_size') || 'm'); } // ── Helpers ── function esc(s) { return encodeURIComponent(s || ''); } function revealEntry(elId, encoded) { const el = document.getElementById(elId); const isHidden = el.textContent.includes('•'); el.textContent = isHidden ? decodeURIComponent(encoded) : (el.textContent.length > 12 ? '••••••••••••••••' : '••••••••'); } function copyVal(encoded) { navigator.clipboard.writeText(decodeURIComponent(encoded)).then(() => toast('Copied')); } // ── Google Search ── // ── Image Analysis ── function handleIaDrop(e) { const file = e.dataTransfer.files[0]; if (!file) return; const dt = new DataTransfer(); dt.items.add(file); document.getElementById('ia-file').files = dt.files; updateIaDropzone(document.getElementById('ia-file')); } function updateIaDropzone(input) { const dz = document.getElementById('ia-dropzone'); if (input.files.length > 0) { dz.textContent = `📎 ${input.files[0].name}`; dz.style.color = 'var(--text)'; } } async function runImageAnalysis() { const fileInput = document.getElementById('ia-file'); const customPrompt = document.getElementById('ia-prompt').value.trim(); const resultEl = document.getElementById('ia-result'); const actionsEl = document.getElementById('ia-actions'); if (!fileInput.files || fileInput.files.length === 0) { toast('画像ファイルを選択してください'); return; } const file = fileInput.files[0]; const supportedTypes = ['image/png', 'image/jpeg', 'image/webp', 'image/gif', 'image/bmp']; if (!supportedTypes.includes(file.type)) { toast('非対応フォーマットです(PNG / JPG / WEBP / GIF / BMP)'); return; } resultEl.innerHTML = '

🖼 画像解析中...

'; actionsEl.style.display = 'none'; // Base64 変換 const base64 = await new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result.split(',')[1]); reader.onerror = () => reject(new Error('ファイル読み込み失敗')); reader.readAsDataURL(file); }); const prompt = customPrompt || `あなたはPinterest/Etsy向けビジュアルリサーチの専門家です。 この画像を分析し、以下の3つのセクションで簡潔に回答してください。 ## 1. ビジュアル要約 - 色・雰囲気・構図を5行以内で簡潔に ## 2. 画像生成プロンプト(English) 以下の3パターンをそれぞれ Positive / Negative で記述: - Pattern 1: メインビジュアル(商品・アイキャッチ向け) - Pattern 2: ライフスタイル系(雰囲気・世界観重視) - Pattern 3: ミニマル(テキスト合成・ブログアイキャッチ向け) ## 3. タグ(英語10個) Pinterest/Etsy で使えるタグを10個`; try { const res = await fetch( `/api/gemini?model=gemini-2.5-flash:generateContent`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ contents: [{ parts: [ { inline_data: { mime_type: file.type, data: base64 } }, { text: prompt } ] }], generationConfig: { maxOutputTokens: 8192, temperature: 1.0, } }) } ); if (!res.ok) { const err = await res.json(); throw new Error(err.error?.message || `HTTP ${res.status}`); } const data = await res.json(); const text = data.candidates?.[0]?.content?.parts ?.filter(p => p.text) ?.map(p => p.text) ?.join('') || ''; if (!text) throw new Error('レスポンスが空です'); const tokens = data.usageMetadata?.totalTokenCount ?? 0; window.iaLastResult = { keyword: file.name, prompts: [text], tokens, }; resultEl.innerHTML = `
📎 ${file.name} / ${tokens.toLocaleString()} tokens
${text.replace(/\n/g, '
')}
`; actionsEl.style.display = 'flex'; } catch (e) { resultEl.innerHTML = `

❌ エラー: ${e.message}

`; window.iaLastResult = null; } } async function runGoogleSearch() { const keyword = document.getElementById('gs-keyword').value.trim(); if (!keyword) { toast('キーワードを入力してください'); return; } const expand = document.getElementById('gs-expand').checked; const negative = document.getElementById('gs-negative')?.checked || false; const resultEl = document.getElementById('gs-result'); const actionsEl = document.getElementById('gs-actions'); resultEl.innerHTML = '

📡 サジェスト収集中...

'; actionsEl.style.display = 'none'; async function fetchSuggestions(query) { try { const res = await fetch(`/api/suggest?q=${encodeURIComponent(query)}`); const text = await res.text(); const xml = new DOMParser().parseFromString(text, 'text/xml'); return [...xml.querySelectorAll('suggestion')].map(el => el.getAttribute('data')).filter(Boolean); } catch (e) { return []; } } // 悩み・不満ワード(英語圏向け) const NEGATIVE_WORDS = [ 'problems', 'complaints', 'frustrating', "doesn't work", 'wish it had', 'hate about', 'annoying', 'worst thing about', 'struggles', 'mistakes', 'wish I knew' ]; const baseSet = new Set(); const alphaSet = new Set(); const negativeSet = new Set(); const base = await fetchSuggestions(keyword); base.forEach(s => baseSet.add(s)); resultEl.innerHTML = `

📡 ベース:${base.length}件取得${expand ? ' アルファベット展開中...' : ''}${negative ? ' 悩みワード展開中...' : ''}

`; if (expand) { const ALPHABET = 'abcdefghijklmnopqrstuvwxyz'; for (const letter of ALPHABET) { const suggestions = await fetchSuggestions(`${keyword} ${letter}`); suggestions.forEach(s => alphaSet.add(s)); await new Promise(r => setTimeout(r, 300)); } } if (negative) { for (const word of NEGATIVE_WORDS) { const suggestions = await fetchSuggestions(`${keyword} ${word}`); suggestions.forEach(s => negativeSet.add(s)); await new Promise(r => setTimeout(r, 300)); } } // 重複除去(ベース優先 → アルファベット → 悩みワードの順で一意化) const allSuggestions = new Set([...baseSet, ...alphaSet, ...negativeSet]); const suggestions = [...allSuggestions].sort(); const negativeOnly = [...negativeSet].filter(s => !baseSet.has(s) && !alphaSet.has(s)).sort(); if (!suggestions.length) { resultEl.innerHTML = '

❌ サジェストが取得できませんでした

'; return; } window.gsLastResult = { keyword, suggestions, negativeSuggestions: negativeOnly, analysis: suggestions.map(s => `- ${s}`).join('\n'), timestamp: new Date().toISOString() }; localStorage.setItem('mt_cache_google', JSON.stringify(window.gsLastResult)); resultEl.innerHTML = `
取得件数:${suggestions.length}件${negative ? `(うち悩みワード由来:${negativeOnly.length}件)` : ''}
${negative && negativeOnly.length ? `
🔴 悩みワード由来のサジェスト(${negativeOnly.length}件)
${negativeOnly.join('\n')}
` : ''}
全サジェスト一覧(${suggestions.length}件)
${suggestions.join('\n')}
`; actionsEl.style.display = 'flex'; toast('✅ サジェスト取得完了'); } // ── Pinterest ── const _pinterestImages = []; // { file, base64, name } async function handlePinterestImages(files) { const previews = document.getElementById('pinterest-image-previews'); for (const file of Array.from(files)) { const base64 = await new Promise((res, rej) => { const r = new FileReader(); r.onload = () => res(r.result.split(',')[1]); r.onerror = rej; r.readAsDataURL(file); }); _pinterestImages.push({ file, base64, name: file.name }); // プレビュー表示 const img = document.createElement('img'); img.src = URL.createObjectURL(file); img.style.cssText = 'width:80px;height:80px;object-fit:cover;border-radius:6px;border:1px solid var(--border)'; previews.appendChild(img); } document.getElementById('pinterest-drop-zone').textContent = `${_pinterestImages.length}枚 選択済み`; } function sendToPinterest(from) { switchTool('pinterest'); setTimeout(() => runPinterest(), 100); } async function runPinterest() { // ← すでにある行 const context = localStorage.getItem('mt_research_context') || ''; const resultEl = document.getElementById('pinterest-result'); const actionsEl = document.getElementById('pinterest-actions'); const collectedEl = document.getElementById('pinterest-collected'); // 引き継ぎデータ確認 const gsData = window.gsLastResult; if (!gsData && _pinterestImages.length === 0) { toast('⚠️ Google Searchの結果か画像が必要です'); return; } // 引き継ぎ状態表示 const sources = []; if (gsData) sources.push(`Google: "${gsData.keyword}"`); if (_pinterestImages.length > 0) sources.push(`画像: ${_pinterestImages.length}枚`); collectedEl.textContent = `📥 引き継ぎ: ${sources.join(' / ')}`; resultEl.innerHTML = '

生成中...

'; actionsEl.style.display = 'none'; try { // ── 画像解析(スクショがある場合)── let imageInsights = ''; if (_pinterestImages.length > 0) { imageInsights = await analyzePinterestImages(context); } // ── Pinテキスト・ハッシュタグ・画像生成プロンプト生成 ── const pinContent = await generatePinContent(context, gsData, imageInsights); // キャッシュ window.pinterestLastResult = { imageInsights, pinContent, keyword: gsData?.keyword || '', timestamp: new Date().toISOString() }; // 表示 let html = ''; if (imageInsights) { html += `

📷 スクショ解析

${imageInsights}
`; } html += `

📌 Pin生成

${pinContent}
`; resultEl.innerHTML = html; actionsEl.style.display = 'flex'; toast('✅ Pinterest生成完了'); } catch (e) { resultEl.innerHTML = `

エラー: ${e.message}

`; toast('❌ エラーが発生しました'); } } async function analyzePinterestImages(context) { const parts = []; for (const img of _pinterestImages) { parts.push({ inline_data: { mime_type: img.file.type || 'image/png', data: img.base64 } }); } parts.push({ text: `あなたはPinterestのリサーチアシスタントです。 ${context ? `コンテキスト:\n${context}\n\n` : ''} これらはPinterestの検索結果やボードのスクリーンショットです。 以下の観点で分析してください: ## 📌 検出キーワード・タグ (画像内に見えるキーワード、タイトル、タグをリストアップ) ## 🎨 ビジュアルトレンド (色使い、テイスト、レイアウト、フォントスタイルの傾向) ## 💡 人気コンテンツの特徴 (よく見られるテーマ、訴求ポイント) ## 🏷️ Etsy・Pinterest向けタイトルフレーズ (そのまま使えそうなフレーズを5〜10個) 除外してほしいカテゴリ:メモリアル・追悼・物理的ハンドメイド商品` }); return await callGeminiWithRetry({ contents: [{ parts }] }, 'Gemini画像解析エラー'); } async function generatePinContent(context, gsData, imageInsights) { let prompt = `あなたはPinterestのピン作成アシスタントです。 ${context ? `コンテキスト:\n${context}\n\n` : ''}`; if (gsData) { prompt += `## Google Searchリサーチ結果(キーワード:${gsData.keyword})\n${gsData.analysis || ''}\n\n`; if (gsData.negativeSuggestions && gsData.negativeSuggestions.length) { prompt += `## ユーザーの悩み・不満ワード(Pin description やハッシュタグで積極的に訴求してください)\n${gsData.negativeSuggestions.join('\n')}\n\n`; } } if (imageInsights) { prompt += `## Pinterestスクショ解析結果\n${imageInsights}\n\n`; } prompt += `上記のリサーチ結果をもとに、以下を生成してください: ## 📝 Pinディスクリプション(3パターン) (英語、各150〜200文字、自然でSEOを意識した文章) ## #️⃣ ハッシュタグ (英語、15〜20個、#付き) ## 🖼️ 画像生成プロンプト(3パターン) (英語、Midjourney/Stable Diffusion向け、具体的なビジュアル指示込み) ## 🔑 キーワードバリエーション(隣接キーワード5個) (Pinterest検索で使えるロングテールキーワード)`; return await callGeminiWithRetry({ contents: [{ parts: [{ text: prompt }] }] }, 'Gemini生成エラー'); } // ── Geminiリトライヘルパー ── async function callGeminiWithRetry(body, errorLabel = 'Geminiエラー', maxRetries = 3) { for (let i = 0; i < maxRetries; i++) { const res = await fetch( `/api/gemini?model=gemini-3.5-flash:generateContent`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) } ); if (res.ok) { const data = await res.json(); return data.candidates?.[0]?.content?.parts?.[0]?.text || '生成結果なし'; } if ((res.status === 429 || res.status === 503) && i < maxRetries - 1) { const wait = (i + 1) * 15000; document.getElementById('pinterest-result').innerHTML = `

⏳ Gemini混雑中... ${wait / 1000}秒後にリトライ (${i + 1}/${maxRetries - 1})

`; await new Promise(r => setTimeout(r, wait)); } else { throw new Error(`${errorLabel}: ${res.status}`); } } } // ── Etsy ── const _etsyImages = []; async function handleEtsyImages(files) { const previews = document.getElementById('etsy-image-previews'); for (const file of Array.from(files)) { const base64 = await new Promise((res, rej) => { const r = new FileReader(); r.onload = () => res(r.result.split(',')[1]); r.onerror = rej; r.readAsDataURL(file); }); _etsyImages.push({ file, base64, name: file.name }); const img = document.createElement('img'); img.src = URL.createObjectURL(file); img.style.cssText = 'width:80px;height:80px;object-fit:cover;border-radius:6px;border:1px solid var(--border)'; previews.appendChild(img); } document.getElementById('etsy-drop-zone').textContent = `${_etsyImages.length}枚 選択済み`; } async function runEtsy() { const context = localStorage.getItem('mt_research_context') || ''; const resultEl = document.getElementById('etsy-result'); const actionsEl = document.getElementById('etsy-actions'); const collectedEl = document.getElementById('etsy-collected'); const gsData = window.gsLastResult; const pinData = window.pinterestLastResult; if (!gsData && _etsyImages.length === 0) { toast('⚠️ Google Searchの結果か画像が必要です'); return; } const sources = []; if (gsData) sources.push(`Google: "${gsData.keyword}"`); if (pinData) sources.push(`Pinterest分析済み`); if (_etsyImages.length > 0) sources.push(`画像: ${_etsyImages.length}枚`); collectedEl.textContent = `📥 引き継ぎ: ${sources.join(' / ')}`; resultEl.innerHTML = '

分析中...

'; actionsEl.style.display = 'none'; try { let imageInsights = ''; if (_etsyImages.length > 0) { imageInsights = await analyzeEtsyImages(context); } const etsyContent = await generateEtsyContent(context, gsData, pinData, imageInsights); window.etsyLastResult = { imageInsights, etsyContent, keyword: gsData?.keyword || '', timestamp: new Date().toISOString() }; let html = ''; if (imageInsights) { html += `

📷 スクショ解析

${imageInsights}
`; } html += `

🛍️ Etsy商品提案

${etsyContent}
`; resultEl.innerHTML = html; actionsEl.style.display = 'flex'; toast('✅ Etsy分析完了'); } catch (e) { resultEl.innerHTML = `

エラー: ${e.message}

`; toast('❌ エラーが発生しました'); } } async function analyzeEtsyImages(context) { const parts = []; for (const img of _etsyImages) { parts.push({ inline_data: { mime_type: img.file.type || 'image/png', data: img.base64 } }); } parts.push({ text: `あなたはEtsyのマーケットリサーチアシスタントです。 ${context ? `コンテキスト:\n${context}\n\n` : ''} これらはEtsyの検索結果やEverBeeの分析画面のスクリーンショットです。 以下の観点で分析してください: ## 🏷️ 売れ筋商品のタイトルパターン (共通するワード・フレーズ・構造) ## 💰 価格帯の傾向 (最安値・最高値・最も多い価格帯) ## 🎨 サムネイルのビジュアル傾向 (背景色、構図、テキストオーバーレイの有無、スタイル) ## 🔑 よく使われているキーワード・タグ (タイトルやタグに頻出するワード) ## 📊 競合レベルの所感 (レビュー数・販売数から見る市場の飽和度)` }); const res = await fetch( `/api/gemini?model=gemini-3.5-flash:generateContent`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ contents: [{ parts }] }) } ); if (!res.ok) throw new Error(`Gemini画像解析エラー: ${res.status}`); const data = await res.json(); return data.candidates?.[0]?.content?.parts?.[0]?.text || '解析結果なし'; } async function generateEtsyContent(context, gsData, pinData, imageInsights) { let prompt = `あなたはEtsyの商品戦略アドバイザーです。 ${context ? `コンテキスト:\n${context}\n\n` : ''}`; if (gsData) { prompt += `## Google Searchリサーチ(キーワード:${gsData.keyword})\n${gsData.analysis || ''}\n\n`; if (gsData.negativeSuggestions && gsData.negativeSuggestions.length) { prompt += `## ユーザーの悩み・不満ワード(商品説明やタグで積極的に訴求してください)\n${gsData.negativeSuggestions.join('\n')}\n\n`; } } if (pinData) prompt += `## Pinterest分析結果\n${pinData.pinContent || ''}\n\n`; if (imageInsights) prompt += `## Etsyスクショ解析\n${imageInsights}\n\n`; prompt += `上記のリサーチをもとに、以下を生成してください: ## 📦 作るべき商品の提案(3案) (具体的な商品アイデア、差別化ポイント込み) ## 📝 Etsyタイトル(各案3パターン) (英語、130文字以内、SEOキーワード含む) ## 🏷️ Etsyタグ(13個) (英語、各20文字以内) ## 📄 商品説明文の冒頭(英語、3〜4文) (検索意図に刺さる書き出し) ## 💡 サムネイル制作のヒント (競合と差別化できるビジュアル戦略)`; const res = await fetch( `/api/gemini?model=gemini-3.5-flash:generateContent`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ contents: [{ parts: [{ text: prompt }] }] }) } ); if (!res.ok) throw new Error(`Gemini生成エラー: ${res.status}`); const data = await res.json(); return data.candidates?.[0]?.content?.parts?.[0]?.text || '生成結果なし'; } // ── Analysis ── async function runAnalysis() { const text1 = document.getElementById('an-text1').value.trim(); const text2 = document.getElementById('an-text2').value.trim(); if (!text1 && !text2) { showToast('テキストを1つ以上入力してください'); return; } const resultEl = document.getElementById('an-result'); const actionsEl = document.getElementById('an-actions'); resultEl.textContent = '分析中...'; actionsEl.style.display = 'none'; const context = localStorage.getItem('mt_research_context') || ''; const parts = []; if (text1) parts.push(`【テキスト1】\n${text1}`); if (text2) parts.push(`【テキスト2】\n${text2}`); const prompt = `${context ? `## コンテキスト\n${context}\n\n` : ''}あなたはリサーチアナリストです。以下のテキストを分析し、下記の形式で出力してください。 ${parts.join('\n\n')} ## 出力形式 ### 共通キーワード・テーマ - (両テキストに共通する重要ワード・テーマを箇条書き) ### 差異・特徴 - (それぞれのテキストにしか出てこない特徴・視点) ### 統合インサイト (2つを合わせて見えてくる市場ニーズ・機会・提案を3〜5行で) ### 推奨アクション 1. 2. 3. `; try { const res = await fetch( `/api/gemini?model=gemini-3.5-flash:generateContent`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ contents: [{ parts: [{ text: prompt }] }] }) } ); if (!res.ok) throw new Error(`Gemini生成エラー: ${res.status}`); const data = await res.json(); const text = data.candidates?.[0]?.content?.parts?.[0]?.text || '生成結果なし'; resultEl.textContent = text; window.analysisLastResult = text; actionsEl.style.display = 'flex'; } catch (e) { resultEl.textContent = 'エラー: ' + e.message; } } // ── Save / Prompts ── function saveResult(category) { const keyMap = { 'google-search': { cacheKey: 'gsLastResult', storeKey: 'mt_save_google' }, 'pinterest': { cacheKey: 'pinterestLastResult', storeKey: 'mt_save_pinterest' }, 'etsy': { cacheKey: 'etsyLastResult', storeKey: 'mt_save_etsy' }, 'analysis': { cacheKey: 'analysisLastResult', storeKey: 'mt_save_analysis' }, }; const map = keyMap[category]; if (!map) return; const result = window[map.cacheKey]; if (!result) { toast('⚠️ 保存するデータがありません'); return; } const existing = JSON.parse(localStorage.getItem(map.storeKey) || '[]'); const entry = { id: Date.now(), timestamp: new Date().toISOString(), category, data: result, }; existing.unshift(entry); // 新しい順 localStorage.setItem(map.storeKey, JSON.stringify(existing)); if (category === 'image-analysis') { } toast('✅ 保存しました'); renderSave(); } function renderSave() { const el = document.getElementById('save-list'); if (!el) return; const categories = [ { key: 'mt_save_google', label: 'Google Search' }, { key: 'mt_save_pinterest', label: 'Pinterest' }, { key: 'mt_save_etsy', label: 'Etsy' }, { key: 'mt_save_analysis', label: 'Analysis' }, ]; let html = ''; categories.forEach(({ key, label }) => { const items = JSON.parse(localStorage.getItem(key) || '[]'); if (!items.length) return; html += `
${label}
`; items.forEach(item => { const d = item.data; let contentHtml = ''; if (key === 'mt_save_pinterest' && d && d.pinContent) { const header = `キーワード:${d.keyword || 'なし'}`; const textToParse = d.pinContent || ''; const parts = textToParse.split(/(?=^##\s+)/m); const sections = parts.map(part => { const lines = part.trim().split('\n'); const firstLine = lines[0]; if (firstLine.startsWith('## ')) { const title = firstLine.replace(/^##\s+/, '').trim(); const body = lines.slice(1).join('\n').trim(); return { title, body }; } else { return { title: '概要', body: part.trim() }; } }).filter(p => p.body); if (d.imageInsights) { sections.push({ title: '🖼️ スクショ解析結果', body: d.imageInsights.trim() }); } const sectionsHtml = sections.map(s => `
${s.title}
${s.body}
`).join(''); contentHtml = `
${header}${sectionsHtml}
`; } else { const content = (() => { if (typeof d === 'string') return d; if (d.keyword && d.results) { const coreKeywords = []; const aroundKeywords = []; const extractKeywords = (textBlock) => { if (!textBlock) return []; const cleaned = textBlock.trim(); if (cleaned === '' || cleaned === 'なし' || cleaned === '無し') return []; return cleaned.split(/[\n,,、//]+/) .map(item => item.replace(/^[\s\-*・\d+[\.\)]]+/, '').trim()) .filter(v => v && v !== 'なし' && v !== '無し'); }; d.results.forEach(r => { const analysis = r.analysis || ''; const coreMatch = analysis.match(/###\s*コアキーワード\s*\r?\n([\s\S]*?)(?=\r?\n\s*#|$)/); if (coreMatch) { coreKeywords.push(...extractKeywords(coreMatch[1])); } const aroundMatch = analysis.match(/###\s*周辺キーワード\s*\r?\n([\s\S]*?)(?=\r?\n\s*#|$)/); if (aroundMatch) { aroundKeywords.push(...extractKeywords(aroundMatch[1])); } }); return [ `検索:${d.keyword} / ${d.results.length}本分析`, coreKeywords.length ? `コア:${coreKeywords.join('、')}` : '', aroundKeywords.length ? `周辺:${aroundKeywords.join('、')}` : '', ].filter(Boolean).join('\n'); } if (d.keyword && d.suggestions) { const sugList = (Array.isArray(d.suggestions) ? d.suggestions : []) .map(s => typeof s === 'string' ? s.trim() : s) .filter(Boolean); return `キーワード:${d.keyword}\n${sugList.join('、')}`; } if (d.pinContent) return `キーワード:${d.keyword}\n\n${d.pinContent}${d.imageInsights ? '\n\n---\n\n' + d.imageInsights : ''}`; if (d.etsyContent) return `キーワード:${d.keyword}\n\n${d.etsyContent}${d.imageInsights ? '\n\n---\n\n' + d.imageInsights : ''}`; if (d.keyword) return `キーワード:${d.keyword}`; return JSON.stringify(d, null, 2); })(); contentHtml = `
${content}
`; } html += `
${contentHtml}
`; }); html += '
'; }); el.innerHTML = html || '

保存済みデータはありません

'; } function deleteSave(storeKey, id) { const existing = JSON.parse(localStorage.getItem(storeKey) || '[]'); localStorage.setItem(storeKey, JSON.stringify(existing.filter(e => e.id !== id))); renderSave(); toast('🗑 削除しました'); } async function saveToGitHub() { const btn = document.getElementById('btn-github-save'); const status = document.getElementById('github-save-status'); btn.disabled = true; status.textContent = '保存中...'; const data = {}; // localStorageの保存済みキーを全部拾う keys.forEach(key => { const val = localStorage.getItem(key); if (val) { try { data[key] = JSON.parse(val); } catch (e) { data[key] = val; } } }); // 検索履歴も保存 if (window.gsLastResult) { data['cache_google'] = window.gsLastResult; } const date = new Date().toISOString().slice(0, 10); const path = `data/backup_${date}.json`; const content = btoa(unescape(encodeURIComponent(JSON.stringify(data, null, 2)))); try { const getRes = await fetch(`/api/github?path=${encodeURIComponent(path)}`, { headers: { Accept: 'application/vnd.github.v3+json' } }); const sha = getRes.ok ? (await getRes.json()).sha : undefined; const putRes = await fetch(`/api/github?path=${encodeURIComponent(path)}`, { method: 'PUT', headers: { Accept: 'application/vnd.github.v3+json', 'Content-Type': 'application/json' }, body: JSON.stringify({ message: `backup: ${date}`, content, ...(sha ? { sha } : {}) }) }); if (putRes.ok) { status.textContent = `✅ 保存完了 ${date}`; toast('✅ GitHubに保存しました'); } else { status.textContent = '❌ 保存失敗'; toast('❌ 保存に失敗しました'); } } catch (e) { status.textContent = '❌ エラー'; toast('❌ エラーが発生しました'); } btn.disabled = false; } // ── Boot ── if (localStorage.getItem('authed') === '1') showApp(); function logout() { localStorage.removeItem('authed'); document.getElementById('appLayout').style.display = 'none'; document.getElementById('loginScreen').style.display = 'flex'; document.getElementById('loginPass').value = ''; document.getElementById('loginError').style.display = 'none'; } function toggleEye(inputId, btn) { const input = document.getElementById(inputId); const isPassword = input.type === 'password'; input.type = isPassword ? 'text' : 'password'; btn.innerHTML = isPassword ? `` : ``; } function setSize(k) { document.documentElement.style.setProperty('--fs-base', { s: '9px', m: '11px', l: '14px' }[k]); document.querySelectorAll('.sz-btn').forEach(b => b.classList.toggle('active', b.classList.contains(k))); try { localStorage.setItem('mt_size', k); } catch (e) { } } function loadSize() { setSize(localStorage.getItem('mt_size') || 'm'); }