diff --git a/src/plugins/built-in/writing-studio/panels/goals-panel.js b/src/plugins/built-in/writing-studio/panels/goals-panel.js new file mode 100644 index 0000000..7a75beb --- /dev/null +++ b/src/plugins/built-in/writing-studio/panels/goals-panel.js @@ -0,0 +1,99 @@ +function renderGoalsPanel(container, { engines, settings }) { + const dailyGoal = settings.get('dailyGoal') || 1000; + const progress = engines.goals.getDailyProgress(dailyGoal); + const streak = engines.goals.getStreak(dailyGoal); + const weekly = engines.goals.getWeeklyTotal(); + const last30 = engines.goals.getLast30Days(); + + container.replaceChildren(); + + const panel = document.createElement('div'); + panel.className = 'ws-panel'; + + // Daily progress section + const section1 = document.createElement('div'); + section1.className = 'ws-section'; + const heading1 = document.createElement('h3'); + heading1.className = 'ws-heading'; + heading1.textContent = 'Daily Progress'; + section1.appendChild(heading1); + + const bar = document.createElement('div'); + bar.className = 'ws-progress-bar'; + const fill = document.createElement('div'); + fill.className = 'ws-progress-fill'; + fill.style.width = progress.pct + '%'; + bar.appendChild(fill); + section1.appendChild(bar); + + const row = document.createElement('div'); + row.className = 'ws-stat-row'; + const label = document.createElement('span'); + label.textContent = progress.written.toLocaleString() + ' / ' + dailyGoal.toLocaleString() + ' words'; + const pct = document.createElement('span'); + pct.className = 'ws-pct'; + pct.textContent = progress.pct + '%'; + row.appendChild(label); + row.appendChild(pct); + section1.appendChild(row); + panel.appendChild(section1); + + // Stats cards + const section2 = document.createElement('div'); + section2.className = 'ws-section'; + const grid = document.createElement('div'); + grid.className = 'ws-stat-grid'; + + const streakCard = document.createElement('div'); + streakCard.className = 'ws-stat-card'; + const streakVal = document.createElement('span'); + streakVal.className = 'ws-stat-value'; + streakVal.textContent = String(streak); + const streakLbl = document.createElement('span'); + streakLbl.className = 'ws-stat-label'; + streakLbl.textContent = 'Day Streak'; + streakCard.appendChild(streakVal); + streakCard.appendChild(streakLbl); + + const weekCard = document.createElement('div'); + weekCard.className = 'ws-stat-card'; + const weekVal = document.createElement('span'); + weekVal.className = 'ws-stat-value'; + weekVal.textContent = weekly.toLocaleString(); + const weekLbl = document.createElement('span'); + weekLbl.className = 'ws-stat-label'; + weekLbl.textContent = 'This Week'; + weekCard.appendChild(weekVal); + weekCard.appendChild(weekLbl); + + grid.appendChild(streakCard); + grid.appendChild(weekCard); + section2.appendChild(grid); + panel.appendChild(section2); + + // 30-day chart + const section3 = document.createElement('div'); + section3.className = 'ws-section'; + const heading3 = document.createElement('h3'); + heading3.className = 'ws-heading'; + heading3.textContent = 'Last 30 Days'; + section3.appendChild(heading3); + + const chart = document.createElement('div'); + chart.className = 'ws-chart'; + const maxWords = Math.max(...last30.map(d => d.words), 1); + for (const day of last30) { + const barEl = document.createElement('div'); + const height = Math.max(2, (day.words / maxWords) * 60); + barEl.className = 'ws-bar' + (day.words >= dailyGoal ? ' ws-bar-met' : ''); + barEl.style.height = height + 'px'; + barEl.title = day.date + ': ' + day.words + ' words'; + chart.appendChild(barEl); + } + section3.appendChild(chart); + panel.appendChild(section3); + + container.appendChild(panel); +} + +module.exports = { renderGoalsPanel }; diff --git a/src/plugins/built-in/writing-studio/panels/manuscript-panel.js b/src/plugins/built-in/writing-studio/panels/manuscript-panel.js new file mode 100644 index 0000000..469b463 --- /dev/null +++ b/src/plugins/built-in/writing-studio/panels/manuscript-panel.js @@ -0,0 +1,124 @@ +function renderManuscriptPanel(container, { engines, editor, settings }) { + const projectDir = settings.get('projectDir'); + + container.replaceChildren(); + const panel = document.createElement('div'); + panel.className = 'ws-panel'; + + if (!projectDir) { + const empty = document.createElement('div'); + empty.className = 'ws-empty'; + const p = document.createElement('p'); + p.textContent = 'No manuscript project open'; + empty.appendChild(p); + const btn = document.createElement('button'); + btn.className = 'ws-btn ws-btn-primary'; + btn.id = 'ws-new-project'; + btn.textContent = 'New Project'; + btn.addEventListener('click', () => { + const name = prompt('Project name:'); + if (!name) return; + settings.set('projectDir', name); + renderManuscriptPanel(container, { engines, editor, settings }); + }); + empty.appendChild(btn); + panel.appendChild(empty); + container.appendChild(panel); + return; + } + + const project = engines.projects.loadProject(projectDir); + if (!project) { + const empty = document.createElement('div'); + empty.className = 'ws-empty'; + const p = document.createElement('p'); + p.textContent = 'Project not found at ' + projectDir; + empty.appendChild(p); + const btn = document.createElement('button'); + btn.className = 'ws-btn'; + btn.id = 'ws-clear-project'; + btn.textContent = 'Clear Project'; + btn.addEventListener('click', () => { + settings.set('projectDir', null); + renderManuscriptPanel(container, { engines, editor, settings }); + }); + empty.appendChild(btn); + panel.appendChild(empty); + container.appendChild(panel); + return; + } + + const stats = engines.projects.getStats(projectDir); + + // Project title + progress + const section1 = document.createElement('div'); + section1.className = 'ws-section'; + const heading = document.createElement('h3'); + heading.className = 'ws-heading'; + heading.textContent = project.title; + section1.appendChild(heading); + + const bar = document.createElement('div'); + bar.className = 'ws-progress-bar'; + const fill = document.createElement('div'); + fill.className = 'ws-progress-fill'; + fill.style.width = stats.pctComplete + '%'; + bar.appendChild(fill); + section1.appendChild(bar); + + const row = document.createElement('div'); + row.className = 'ws-stat-row'; + const label = document.createElement('span'); + label.textContent = stats.totalWords.toLocaleString() + ' / ' + stats.targetWords.toLocaleString() + ' words'; + const pct = document.createElement('span'); + pct.className = 'ws-pct'; + pct.textContent = stats.pctComplete + '%'; + row.appendChild(label); + row.appendChild(pct); + section1.appendChild(row); + panel.appendChild(section1); + + // Chapters list + const section2 = document.createElement('div'); + section2.className = 'ws-section'; + const heading2 = document.createElement('h3'); + heading2.className = 'ws-heading'; + heading2.textContent = 'Chapters (' + project.chapters.length + ')'; + section2.appendChild(heading2); + + const chList = document.createElement('div'); + chList.className = 'ws-chapter-list'; + for (const ch of project.chapters) { + const item = document.createElement('div'); + item.className = 'ws-chapter-item'; + const title = document.createElement('span'); + title.className = 'ws-chapter-title'; + title.textContent = ch.title || ch.file; + const status = document.createElement('span'); + status.className = 'ws-chapter-status ws-status-' + (ch.status || 'draft'); + status.textContent = ch.status || 'draft'; + item.appendChild(title); + item.appendChild(status); + chList.appendChild(item); + } + section2.appendChild(chList); + panel.appendChild(section2); + + // Compile button + const section3 = document.createElement('div'); + section3.className = 'ws-section'; + const compileBtn = document.createElement('button'); + compileBtn.className = 'ws-btn ws-btn-primary'; + compileBtn.id = 'ws-compile'; + compileBtn.textContent = 'Compile Manuscript'; + compileBtn.addEventListener('click', () => { + const compiled = engines.projects.compileManuscript(projectDir); + editor.insertAtCursor(compiled); + }); + section3.appendChild(compileBtn); + panel.appendChild(section3); + + container.appendChild(panel); +} + +module.exports = { renderManuscriptPanel }; diff --git a/src/plugins/built-in/writing-studio/panels/proofread-panel.js b/src/plugins/built-in/writing-studio/panels/proofread-panel.js new file mode 100644 index 0000000..4314970 --- /dev/null +++ b/src/plugins/built-in/writing-studio/panels/proofread-panel.js @@ -0,0 +1,97 @@ +function renderProofreadPanel(container, { events, editor }) { + const hasAI = events.hasHandler('ai:analyze'); + + container.replaceChildren(); + + const panel = document.createElement('div'); + panel.className = 'ws-panel'; + + const section = document.createElement('div'); + section.className = 'ws-section'; + + const btn = document.createElement('button'); + btn.className = 'ws-btn ws-btn-primary'; + btn.id = 'ws-proofread'; + btn.textContent = hasAI ? 'Check Document' : 'AI Plugin Required'; + btn.disabled = !hasAI; + section.appendChild(btn); + + if (!hasAI) { + const note = document.createElement('p'); + note.className = 'ws-muted'; + note.textContent = 'Install the AI Assistant plugin to enable proofreading.'; + section.appendChild(note); + } + panel.appendChild(section); + + const issuesList = document.createElement('div'); + issuesList.className = 'ws-issues-list'; + issuesList.id = 'ws-issues'; + panel.appendChild(issuesList); + + container.appendChild(panel); + + if (!hasAI) return; + + container.querySelector('#ws-proofread').addEventListener('click', () => { + const content = editor.getContent() || ''; + events.emit('ai:analyze', { + text: content, + type: 'grammar', + callback: (result) => { + if (result && result.issues) { + renderIssues(issuesList, result.issues); + } + } + }); + }); +} + +function renderIssues(container, issues) { + container.replaceChildren(); + if (!issues || issues.length === 0) { + const p = document.createElement('p'); + p.className = 'ws-muted'; + p.textContent = 'No issues found.'; + container.appendChild(p); + return; + } + for (let i = 0; i < issues.length; i++) { + const issue = issues[i]; + const item = document.createElement('div'); + item.className = 'ws-issue-item'; + + const type = document.createElement('div'); + type.className = 'ws-issue-type'; + type.textContent = (issue.type || 'grammar').toUpperCase(); + item.appendChild(type); + + const text = document.createElement('div'); + text.className = 'ws-issue-text'; + text.textContent = issue.message || issue.text || ''; + item.appendChild(text); + + if (issue.suggestion) { + const sug = document.createElement('div'); + sug.className = 'ws-issue-suggestion'; + sug.textContent = 'Suggestion: ' + issue.suggestion; + item.appendChild(sug); + } + + const actions = document.createElement('div'); + actions.className = 'ws-issue-actions'; + for (const [action, label] of [['accept', 'Accept'], ['dismiss', 'Dismiss']]) { + const actionBtn = document.createElement('button'); + actionBtn.className = 'ws-btn ws-btn-sm'; + actionBtn.textContent = label; + actionBtn.addEventListener('click', () => { + item.remove(); + }); + actions.appendChild(actionBtn); + } + item.appendChild(actions); + container.appendChild(item); + } +} + +module.exports = { renderProofreadPanel }; diff --git a/src/plugins/built-in/writing-studio/panels/snapshots-panel.js b/src/plugins/built-in/writing-studio/panels/snapshots-panel.js new file mode 100644 index 0000000..da65642 --- /dev/null +++ b/src/plugins/built-in/writing-studio/panels/snapshots-panel.js @@ -0,0 +1,81 @@ +function renderSnapshotsPanel(container, { engines, editor }) { + const snapshots = engines.snapshots.list(); + + container.replaceChildren(); + + const panel = document.createElement('div'); + panel.className = 'ws-panel'; + + // Header with take snapshot button + const section = document.createElement('div'); + section.className = 'ws-section'; + const btn = document.createElement('button'); + btn.className = 'ws-btn ws-btn-primary'; + btn.id = 'ws-take-snapshot'; + btn.textContent = 'Take Snapshot'; + section.appendChild(btn); + const count = document.createElement('span'); + count.className = 'ws-muted'; + count.textContent = snapshots.length + ' snapshots'; + section.appendChild(count); + panel.appendChild(section); + + // Snapshot list + const list = document.createElement('div'); + list.className = 'ws-snapshot-list'; + for (const s of snapshots) { + const item = document.createElement('div'); + item.className = 'ws-snapshot-item'; + + const header = document.createElement('div'); + header.className = 'ws-snapshot-header'; + const sLabel = document.createElement('span'); + sLabel.className = 'ws-snapshot-label'; + sLabel.textContent = s.label; + const sWords = document.createElement('span'); + sWords.textContent = s.wordCount + ' words'; + header.appendChild(sLabel); + header.appendChild(sWords); + item.appendChild(header); + + const time = document.createElement('div'); + time.className = 'ws-snapshot-time'; + time.textContent = new Date(s.timestamp).toLocaleString(); + item.appendChild(time); + + const actions = document.createElement('div'); + actions.className = 'ws-snapshot-actions'; + for (const [action, text, cls] of [['restore', 'Restore', ''], ['diff', 'Diff', ''], ['delete', 'Delete', 'ws-btn-danger']]) { + const actionBtn = document.createElement('button'); + actionBtn.className = 'ws-btn ws-btn-sm' + (cls ? ' ' + cls : ''); + actionBtn.textContent = text; + actionBtn.addEventListener('click', () => { + if (action === 'restore') { + const content = engines.snapshots.restore(s.id); + editor.insertAtCursor(content); + } else if (action === 'delete') { + engines.snapshots.delete(s.id); + renderSnapshotsPanel(container, { engines, editor }); + } else if (action === 'diff') { + const current = editor.getContent() || ''; + const result = engines.snapshots.diff(s.id, current); + alert('+' + result.added + ' lines added, -' + result.removed + ' lines removed'); + } + }); + actions.appendChild(actionBtn); + } + item.appendChild(actions); + list.appendChild(item); + } + panel.appendChild(list); + container.appendChild(panel); + + // Take snapshot button handler + container.querySelector('#ws-take-snapshot').addEventListener('click', () => { + const content = editor.getContent() || ''; + engines.snapshots.create(content, 'manual'); + renderSnapshotsPanel(container, { engines, editor }); + }); +} + +module.exports = { renderSnapshotsPanel };