mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-08-02 18:10:18 +05:30
feat(writing-studio): add four sidebar panels (goals, snapshots, manuscript, proofread)
Amit Haridas
This commit is contained in:
@@ -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 };
|
||||||
@@ -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 };
|
||||||
@@ -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 };
|
||||||
@@ -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 };
|
||||||
Reference in New Issue
Block a user