posimai-chronicle/index.html

610 lines
28 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="ja" data-app-id="posimai-chronicle">
<head>
<meta charset="UTF-8">
<meta name="robots" content="noindex, nofollow">
<script>
(function () {
var t = localStorage.getItem('posimai-chronicle-theme') || 'system';
var dark = t === 'dark' || (t === 'system' && matchMedia('(prefers-color-scheme:dark)').matches);
document.documentElement.setAttribute('data-theme', dark ? 'dark' : 'light');
document.documentElement.setAttribute('data-theme-pref', t);
var p = new URLSearchParams(location.search);
var tk = p.get('token');
if (tk) {
localStorage.setItem('posimai_token', tk);
p.delete('token');
history.replaceState({}, '', location.pathname + (p.toString() ? '?' + p.toString() : '') + location.hash);
}
})();
</script>
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<meta name="description" content="開発ログ・気分・学びを束ねて Journal 下書きを生成する">
<meta name="color-scheme" content="dark light">
<meta name="theme-color" content="#0D0D0D" media="(prefers-color-scheme: dark)">
<meta name="theme-color" content="#F9FAFB" media="(prefers-color-scheme: light)">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="Chronicle">
<link rel="manifest" href="/manifest.json">
<link rel="icon" type="image/svg+xml" href="/logo.svg">
<link rel="apple-touch-icon" href="/logo.svg">
<title>Posimai Chronicle</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Geist:wght@300;400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://posimai-ui.vercel.app/v1/base.css">
<script src="https://unpkg.com/lucide@0.344.0/dist/umd/lucide.min.js" integrity="sha384-tTkFttkBclaU1cloKwOi9xk3pbao3VZxTjLNBt8iFABWDBQibbAbWpVmO28zMuxq" crossorigin="anonymous"></script>
<style>
/* base.css の main グローバルルールを打ち消す */
#main-content { padding: 0; max-width: none; width: 100%; margin: 0; }
/* ── レイアウト ── */
.wrap { max-width: 960px; margin: 0 auto; padding: 20px 16px calc(56px + env(safe-area-inset-bottom)); }
.two-col { display: grid; gap: 12px; grid-template-columns: 1fr; }
@media (min-width: 800px) { .two-col { grid-template-columns: 1fr 1fr; } }
/* ── ピリオドバー ── */
.period-bar {
display: flex; align-items: center; gap: 8px;
background: var(--surface); border: 1px solid var(--border);
border-radius: var(--radius); padding: 10px 14px;
margin-bottom: 12px;
}
.period-bar .period-label { font-size: 11px; color: var(--text3); }
.period-bar .period-range { font-size: 13px; color: var(--text); font-weight: 500; flex: 1; }
.period-tabs { display: flex; gap: 4px; margin-left: auto; }
.period-tab {
font-size: 11px; padding: 4px 10px; border-radius: 6px;
border: 1px solid var(--border); background: none; color: var(--text2);
cursor: pointer; transition: all 0.15s;
}
.period-tab.active { background: var(--accent-dim); border-color: var(--accent-border); color: var(--accent); }
/* ── 入力カード ── */
.input-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 16px; }
.field { margin-bottom: 14px; }
.field:last-child { margin-bottom: 0; }
.field-label { font-size: 11px; color: var(--text3); text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 6px; display: block; }
.field textarea {
width: 100%; box-sizing: border-box;
background: var(--surface2); border: 1px solid var(--border);
border-radius: var(--radius-sm); color: var(--text);
font-family: inherit; font-size: 13px; line-height: 1.6;
padding: 10px 12px; resize: vertical; min-height: 80px;
transition: border-color 0.15s;
}
.field textarea:focus { outline: none; border-color: var(--accent-border); }
.field textarea::placeholder { color: var(--text3); }
/* ── スライダーグループ ── */
.sliders { display: grid; gap: 10px; }
.slider-row { display: flex; align-items: center; gap: 10px; }
.slider-name { font-size: 12px; color: var(--text2); width: 60px; flex-shrink: 0; }
.slider-input {
flex: 1; -webkit-appearance: none; appearance: none;
height: 4px; border-radius: 2px; background: var(--border);
outline: none; cursor: pointer;
}
.slider-input::-webkit-slider-thumb {
-webkit-appearance: none; width: 16px; height: 16px;
border-radius: 50%; background: var(--accent); cursor: pointer;
box-shadow: 0 0 0 3px var(--accent-dim);
}
.slider-input::-moz-range-thumb {
width: 16px; height: 16px; border: none;
border-radius: 50%; background: var(--accent); cursor: pointer;
}
.slider-val { font-size: 12px; color: var(--accent); font-weight: 600; width: 20px; text-align: right; }
/* ── 生成ボタンエリア ── */
.generate-area { margin: 12px 0; text-align: center; }
.btn-generate {
display: inline-flex; align-items: center; gap: 8px;
padding: 12px 28px; border-radius: var(--radius-sm);
background: var(--accent); color: #0D0D0D;
font-size: 14px; font-weight: 600; border: none; cursor: pointer;
transition: opacity 0.15s, transform 0.1s;
}
.btn-generate:hover { opacity: 0.9; }
.btn-generate:active { transform: scale(0.98); }
.btn-generate:disabled { opacity: 0.45; cursor: not-allowed; transform: none; }
/* ── 下書きカード ── */
.draft-card {
background: var(--surface); border: 1px solid var(--border);
border-radius: var(--radius); padding: 16px; margin-top: 12px;
}
.draft-header { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; }
.draft-title { font-size: 13px; font-weight: 600; color: var(--text); flex: 1; }
.draft-actions { display: flex; gap: 6px; }
/* ── 下書きプレビュー ── */
.draft-body {
font-size: 13px; line-height: 1.75; color: var(--text2);
min-height: 120px;
}
.draft-body h1, .draft-body h2, .draft-body h3 {
color: var(--text); font-weight: 600; margin: 16px 0 6px;
}
.draft-body h2 { font-size: 14px; }
.draft-body h3 { font-size: 13px; }
.draft-body p { margin: 0 0 10px; }
.draft-body strong { color: var(--text); }
/* ── 空状態 ── */
.empty-state {
display: flex; flex-direction: column; align-items: center;
justify-content: center; gap: 10px; padding: 40px 20px;
color: var(--text3); text-align: center;
}
.empty-state p { font-size: 12px; line-height: 1.6; margin: 0; }
/* ── 認証バナー ── */
.auth-banner {
background: var(--surface); border: 1px solid var(--border);
border-radius: var(--radius); padding: 14px 16px;
display: flex; align-items: center; gap: 12px;
margin-bottom: 12px; font-size: 13px; color: var(--text2);
}
.auth-banner a { color: var(--accent); text-decoration: none; font-weight: 500; }
.auth-banner.hidden { display: none; }
/* ── ローディングスピナー ── */
.spinner {
display: inline-block; width: 14px; height: 14px;
border: 2px solid rgba(13,13,13,0.3);
border-top-color: #0D0D0D; border-radius: 50%;
animation: spin 0.7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* ── セクションラベル ── */
.sec-label {
font-size: 11px; color: var(--text3); text-transform: uppercase;
letter-spacing: 0.08em; margin-bottom: 8px; display: flex;
align-items: center; gap: 6px;
}
.sec-label::after { content: ''; flex: 1; height: 1px; background: var(--border); }
</style>
</head>
<body>
<a href="#main-content" class="skip-link" tabindex="0" style="position:absolute;top:-100%;left:8px;background:var(--accent);color:#0D0D0D;padding:8px 16px;border-radius:8px;font-weight:600;font-size:13px;z-index:10000;text-decoration:none">コンテンツへスキップ</a>
<aside class="settings-panel" id="settingsPanel" role="complementary">
<div class="settings-panel-header">
<span class="settings-panel-title">設定</span>
<button class="icon-btn" id="settingsCloseBtn" aria-label="設定を閉じる">
<i data-lucide="x" style="width:18px;height:18px;stroke-width:1.5"></i>
</button>
</div>
<div class="settings-panel-body">
<div class="settings-group-label">GitHub 連携</div>
<div class="settings-item" style="flex-direction:column;align-items:flex-start;gap:6px">
<div class="settings-item-label">Personal Access Token</div>
<div style="font-size:11px;color:var(--text3);line-height:1.5">
github.com → Settings → Developer settings<br>
→ Fine-grained tokens → Generate new token<br>
Permissions: <strong style="color:var(--text2)">Contents: Read-only</strong>
</div>
<div style="display:flex;gap:6px;width:100%">
<input type="password" id="ghPatInput" placeholder="github_pat_..." autocomplete="off"
style="flex:1;background:var(--surface2);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);font-family:inherit;font-size:12px;padding:7px 10px;min-width:0">
<button class="btn btn-ghost" id="ghPatSaveBtn" style="font-size:12px;padding:6px 12px;flex-shrink:0">保存</button>
</div>
<div id="ghPatStatus" style="font-size:11px;color:var(--text3)"></div>
</div>
<div class="settings-group-label" style="margin-top:16px">外観</div>
<div class="settings-item">
<div class="settings-item-label">テーマ</div>
<div class="theme-selector">
<button class="theme-btn" data-theme-val="dark"><i data-lucide="moon" style="width:12px;height:12px;stroke-width:1.5"></i>ダーク</button>
<button class="theme-btn" data-theme-val="light"><i data-lucide="sun" style="width:12px;height:12px;stroke-width:1.5"></i>ライト</button>
<button class="theme-btn" data-theme-val="system"><i data-lucide="monitor" style="width:12px;height:12px;stroke-width:1.5"></i>自動</button>
</div>
</div>
</div>
</aside>
<div class="overlay" id="overlay" aria-hidden="true"></div>
<header class="header">
<div class="header-brand">
<div class="header-dot" aria-hidden="true"></div>
<span class="header-title">Chronicle</span>
</div>
<button class="icon-btn" id="settingsBtn" aria-label="設定" aria-expanded="false">
<i data-lucide="settings" style="width:18px;height:18px;stroke-width:1.5"></i>
</button>
</header>
<main id="main-content">
<div class="wrap">
<!-- 認証バナー -->
<div class="auth-banner" id="authBanner">
<i data-lucide="log-in" style="width:16px;height:16px;stroke-width:1.5;flex-shrink:0;color:var(--text3)"></i>
<span>下書き生成には Posimai ログインが必要です。</span>
<a href="https://posimai.soar-enrich.com" target="_blank" rel="noopener">ログインする</a>
</div>
<!-- 期間バー -->
<div class="period-bar">
<i data-lucide="calendar" style="width:14px;height:14px;stroke-width:1.5;color:var(--text3)"></i>
<span class="period-label">期間</span>
<span class="period-range" id="periodRange"></span>
<div class="period-tabs">
<button class="period-tab active" data-days="7">今週</button>
<button class="period-tab" data-days="14">2週間</button>
<button class="period-tab" data-days="30">今月</button>
</div>
</div>
<div class="two-col">
<!-- 入力フォーム -->
<div>
<div class="sec-label">入力</div>
<div class="input-card">
<div class="field">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:6px">
<label class="field-label" for="activities" style="margin-bottom:0">主な活動</label>
<button class="btn btn-ghost" id="loadCommitsBtn"
style="font-size:11px;padding:4px 10px;display:inline-flex;align-items:center;gap:5px">
<i data-lucide="git-commit-horizontal" style="width:12px;height:12px;stroke-width:1.5"></i>
コミットから読み込む
</button>
</div>
<textarea id="activities" rows="5"
placeholder="実装した機能、解決した問題、取り組んだ作業などを自由に書いてください。箇条書きでもOK。"></textarea>
</div>
<div class="field">
<label class="field-label">気分・体調</label>
<div class="sliders">
<div class="slider-row">
<span class="slider-name">気分</span>
<input class="slider-input" type="range" id="mood" min="1" max="5" value="3">
<span class="slider-val" id="moodVal">3</span>
</div>
<div class="slider-row">
<span class="slider-name">エネルギー</span>
<input class="slider-input" type="range" id="energy" min="1" max="5" value="3">
<span class="slider-val" id="energyVal">3</span>
</div>
<div class="slider-row">
<span class="slider-name">集中度</span>
<input class="slider-input" type="range" id="focus" min="1" max="5" value="3">
<span class="slider-val" id="focusVal">3</span>
</div>
</div>
</div>
<div class="field">
<label class="field-label" for="learnings">学んだこと</label>
<textarea id="learnings" rows="3"
placeholder="技術的な発見、うまくいった方法、改善した点など。"></textarea>
</div>
<div class="field">
<label class="field-label" for="nexts">次の課題</label>
<textarea id="nexts" rows="3"
placeholder="次にやること、積み残し、気になっている問題など。"></textarea>
</div>
</div>
<div class="generate-area">
<button class="btn-generate" id="generateBtn">
<i data-lucide="sparkles" style="width:16px;height:16px;stroke-width:1.5"></i>
下書きを生成
</button>
</div>
</div>
<!-- 下書き出力 -->
<div>
<div class="sec-label">下書き</div>
<div class="draft-card">
<div class="draft-header">
<span class="draft-title" id="draftLabel">生成待ち</span>
<div class="draft-actions" id="draftActions" style="display:none">
<button class="btn btn-ghost" id="copyBtn" style="font-size:12px;padding:6px 12px">
<i data-lucide="copy" style="width:14px;height:14px;stroke-width:1.5"></i>
コピー
</button>
<a class="btn btn-ghost" href="https://journal.posimai.soar-enrich.com" target="_blank" rel="noopener"
style="font-size:12px;padding:6px 12px;display:inline-flex;align-items:center;gap:6px;text-decoration:none">
<i data-lucide="external-link" style="width:14px;height:14px;stroke-width:1.5"></i>
Journal
</a>
</div>
</div>
<div class="draft-body" id="draftBody">
<div class="empty-state">
<i data-lucide="file-text" style="width:24px;height:24px;stroke-width:1.5"></i>
<p>左の入力フォームを埋めて<br>「下書きを生成」を押してください</p>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
<div id="toast" role="status" aria-live="polite"></div>
<script src="https://posimai-ui.vercel.app/v1/base.js" defer></script>
<script>
const API = 'https://api.soar-enrich.com';
// ── 期間 ──────────────────────────────
let activeDays = 7;
function formatDate(d) {
return `${d.getFullYear()}/${String(d.getMonth()+1).padStart(2,'0')}/${String(d.getDate()).padStart(2,'0')}`;
}
function updatePeriod() {
const end = new Date();
const start = new Date(end - activeDays * 86400000);
document.getElementById('periodRange').textContent = `${formatDate(start)} - ${formatDate(end)}`;
}
document.querySelectorAll('.period-tab').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.period-tab').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
activeDays = +btn.dataset.days;
updatePeriod();
});
});
updatePeriod();
// ── スライダー ────────────────────────
['mood','energy','focus'].forEach(id => {
const input = document.getElementById(id);
const val = document.getElementById(id + 'Val');
input.addEventListener('input', () => { val.textContent = input.value; });
});
// ── 認証バナー ────────────────────────
function getToken() { return localStorage.getItem('posimai_token') || ''; }
const banner = document.getElementById('authBanner');
if (getToken()) banner.classList.add('hidden');
// ── Markdown 簡易レンダラー ──────────
function renderMd(text) {
const escaped = text
.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
const lines = escaped.split('\n');
let html = '';
let inP = false;
for (const line of lines) {
if (!line.trim()) {
if (inP) { html += '</p>'; inP = false; }
continue;
}
if (line.startsWith('### ')) {
if (inP) { html += '</p>'; inP = false; }
html += `<h3>${inline(line.slice(4))}</h3>`;
} else if (line.startsWith('## ')) {
if (inP) { html += '</p>'; inP = false; }
html += `<h2>${inline(line.slice(3))}</h2>`;
} else if (line.startsWith('# ')) {
if (inP) { html += '</p>'; inP = false; }
html += `<h2>${inline(line.slice(2))}</h2>`;
} else {
if (!inP) { html += '<p>'; inP = true; }
else html += '<br>';
html += inline(line);
}
}
if (inP) html += '</p>';
return html;
}
function inline(s) {
return s
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.+?)\*/g, '<em>$1</em>');
}
// ── 生成 ──────────────────────────────
let lastDraft = '';
document.getElementById('generateBtn').addEventListener('click', async () => {
const token = getToken();
if (!token) {
banner.classList.remove('hidden');
showToast('Posimai へのログインが必要です');
return;
}
const activities = document.getElementById('activities').value.trim();
if (!activities) {
showToast('「主な活動」を入力してください');
document.getElementById('activities').focus();
return;
}
const btn = document.getElementById('generateBtn');
btn.disabled = true;
btn.innerHTML = '<span class="spinner"></span> 生成中…';
const end = new Date();
const start = new Date(end - activeDays * 86400000);
const period = `${formatDate(start)} から ${formatDate(end)}`;
const mood = document.getElementById('mood').value;
const energy = document.getElementById('energy').value;
const focus = document.getElementById('focus').value;
const learnings = document.getElementById('learnings').value.trim();
const nexts = document.getElementById('nexts').value.trim();
const systemPrompt = `あなたは開発者の振り返りを支援するライターです。
入力された活動情報をもとに、日本語の開発日記の下書きを作成してください。
ルール:
- markdown 形式(## 見出し + 本文段落)
- 3〜4段落構成進捗 → 気づき・学び → 体調との関係 → 次の意志
- 「今週は」「この期間に」など期間を主語にする(一人称「私」は避ける)
- 技術的な内容はそのまま使い、誇張しない
- 公開できる読み物として書く(ですます調)
- 500〜700字程度
- コードブロックは使わない`;
const userPrompt = `期間: ${period}
主な活動:
${activities}
気分: ${mood}/5、エネルギー: ${energy}/5、集中度: ${focus}/5
${learnings ? `学んだこと:\n${learnings}` : ''}
${nexts ? `次の課題:\n${nexts}` : ''}`;
try {
const resp = await fetch(`${API}/ai/generate`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
systemPrompt,
contents: [{ role: 'user', parts: [{ text: userPrompt }] }],
config: { maxOutputTokens: 800, temperature: 0.7 }
})
});
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
throw new Error(err.error || `HTTP ${resp.status}`);
}
const data = await resp.json();
lastDraft = data.text || '';
document.getElementById('draftBody').innerHTML = renderMd(lastDraft);
document.getElementById('draftLabel').textContent = `下書き — ${period}`;
document.getElementById('draftActions').style.display = 'flex';
showToast('下書きを生成しました');
} catch (e) {
showToast(e.message.includes('401') || e.message.includes('403')
? 'ログインが切れています。再ログインしてください'
: `生成に失敗しました: ${e.message}`);
} finally {
btn.disabled = false;
btn.innerHTML = '<i data-lucide="sparkles" style="width:16px;height:16px;stroke-width:1.5"></i> 下書きを生成';
lucide.createIcons();
}
});
// ── コピー ────────────────────────────
document.getElementById('copyBtn').addEventListener('click', async () => {
if (!lastDraft) return;
try {
await navigator.clipboard.writeText(lastDraft);
showToast('クリップボードにコピーしました');
} catch {
showToast('コピーに失敗しました');
}
});
// ── GitHub PAT 設定 ───────────────────
const GH_PAT_KEY = 'posimai-chronicle-gh-pat';
function getGhPat() { return localStorage.getItem(GH_PAT_KEY) || ''; }
(function initGhPatUI() {
const input = document.getElementById('ghPatInput');
const status = document.getElementById('ghPatStatus');
const pat = getGhPat();
if (pat) {
input.placeholder = '設定済み(変更する場合は再入力)';
status.textContent = '設定済み';
status.style.color = 'var(--accent)';
}
document.getElementById('ghPatSaveBtn').addEventListener('click', () => {
const val = input.value.trim();
if (!val) {
localStorage.removeItem(GH_PAT_KEY);
status.textContent = '削除しました';
status.style.color = 'var(--text3)';
return;
}
localStorage.setItem(GH_PAT_KEY, val);
status.textContent = '保存しました';
status.style.color = 'var(--accent)';
});
})();
// ── コミット読み込み ──────────────────
document.getElementById('loadCommitsBtn').addEventListener('click', async () => {
const ghPat = getGhPat();
if (!ghPat) {
showToast('設定パネルで GitHub PAT を設定してください');
document.getElementById('settingsBtn').click();
return;
}
const btn = document.getElementById('loadCommitsBtn');
btn.disabled = true;
btn.innerHTML = '<span class="spinner" style="border-color:rgba(0,0,0,.15);border-top-color:var(--text2)"></span> 読み込み中';
const since = new Date(Date.now() - activeDays * 86400000).toISOString();
try {
const resp = await fetch(
'https://api.github.com/orgs/posimai/events?per_page=100',
{ headers: { 'Authorization': `Bearer ${ghPat}`, 'Accept': 'application/vnd.github+json' } }
);
if (resp.status === 401) throw new Error('PAT が無効です。設定を確認してください');
if (!resp.ok) throw new Error(`GitHub API ${resp.status}`);
const events = await resp.json();
const commits = [];
const byRepo = {};
for (const ev of events) {
if (ev.type !== 'PushEvent') continue;
if (ev.created_at < since) continue;
const repo = ev.repo.name.replace('posimai/', '');
if (!byRepo[repo]) byRepo[repo] = [];
for (const c of ev.payload?.commits || []) {
const msg = c.message.split('\n')[0];
if (/^Merge\b/i.test(msg)) continue;
byRepo[repo].push(msg);
commits.push(msg);
}
}
if (!commits.length) {
showToast('この期間にコミットが見つかりませんでした');
return;
}
const lines = [];
for (const [repo, messages] of Object.entries(byRepo)) {
lines.push(`${repo}`);
for (const m of messages) lines.push(`- ${m}`);
}
document.getElementById('activities').value = lines.join('\n');
showToast(`${commits.length} 件のコミットを読み込みました`);
} catch (e) {
showToast(`読み込み失敗: ${e.message}`);
} finally {
btn.disabled = false;
btn.innerHTML = '<i data-lucide="git-commit-horizontal" style="width:12px;height:12px;stroke-width:1.5"></i> コミットから読み込む';
lucide.createIcons();
}
});
// ── Toast ─────────────────────────────
function showToast(msg) {
const el = document.getElementById('toast');
el.textContent = msg;
el.classList.add('show');
setTimeout(() => el.classList.remove('show'), 3000);
}
document.addEventListener('DOMContentLoaded', () => {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js');
}
});
</script>
</body>
</html>