ralphflow 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/{chunk-TCCMQDVT.js → chunk-DOC64TD6.js} +32 -2
- package/dist/ralphflow.js +584 -24
- package/dist/{server-DOSLU36L.js → server-EX5MWYW4.js} +210 -10
- package/package.json +6 -2
- package/src/dashboard/ui/app.js +203 -0
- package/src/dashboard/ui/archives.js +167 -0
- package/src/dashboard/ui/index.html +2 -3210
- package/src/dashboard/ui/loop-detail.js +880 -0
- package/src/dashboard/ui/notifications.js +151 -0
- package/src/dashboard/ui/prompt-builder.js +362 -0
- package/src/dashboard/ui/sidebar.js +97 -0
- package/src/dashboard/ui/state.js +54 -0
- package/src/dashboard/ui/styles.css +2140 -0
- package/src/dashboard/ui/templates.js +1858 -0
- package/src/dashboard/ui/utils.js +115 -0
- package/src/templates/code-implementation/loops/00-story-loop/prompt.md +73 -11
- package/src/templates/code-implementation/loops/01-tasks-loop/prompt.md +51 -2
- package/src/templates/code-implementation/loops/02-delivery-loop/prompt.md +48 -4
- package/src/templates/research/loops/00-discovery-loop/prompt.md +58 -5
- package/src/templates/research/loops/01-research-loop/prompt.md +44 -2
- package/src/templates/research/loops/02-story-loop/prompt.md +42 -1
- package/src/templates/research/loops/03-document-loop/prompt.md +42 -1
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// Archive browsing: listing, file tree, file viewer.
|
|
2
|
+
|
|
3
|
+
import { state, actions } from './state.js';
|
|
4
|
+
import { fetchJson, esc } from './utils.js';
|
|
5
|
+
|
|
6
|
+
export function switchAppTab(tab) {
|
|
7
|
+
if (tab === state.activeAppTab) return;
|
|
8
|
+
state.activeAppTab = tab;
|
|
9
|
+
state.expandedArchive = null;
|
|
10
|
+
state.archiveFilesCache = {};
|
|
11
|
+
state.viewingArchiveFile = null;
|
|
12
|
+
actions.renderContent();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function loadArchives(appName) {
|
|
16
|
+
const container = document.getElementById('archivesContainer');
|
|
17
|
+
if (!container) return;
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
state.archivesData = await fetchJson(`/api/apps/${encodeURIComponent(appName)}/archives`);
|
|
21
|
+
renderArchivesView(container, appName);
|
|
22
|
+
} catch {
|
|
23
|
+
container.innerHTML = '<div class="archive-empty"><div>Error loading archives</div></div>';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function renderArchivesView(container, appName) {
|
|
28
|
+
if (state.archivesData.length === 0) {
|
|
29
|
+
container.innerHTML = `<div class="archive-empty">
|
|
30
|
+
<div class="archive-empty-icon">🗃</div>
|
|
31
|
+
<div>No archives yet</div>
|
|
32
|
+
<div style="margin-top:8px;font-size:12px">Use the Archive button to snapshot current work</div>
|
|
33
|
+
</div>`;
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let html = '<div class="archive-timeline">';
|
|
38
|
+
for (const archive of state.archivesData) {
|
|
39
|
+
const isExpanded = state.expandedArchive === archive.timestamp;
|
|
40
|
+
const dateStr = formatArchiveTimestamp(archive.timestamp);
|
|
41
|
+
html += `<div class="archive-card${isExpanded ? ' expanded' : ''}" data-archive="${esc(archive.timestamp)}">
|
|
42
|
+
<div class="archive-card-header" data-archive-toggle="${esc(archive.timestamp)}">
|
|
43
|
+
<span class="archive-card-date">${esc(dateStr)}</span>
|
|
44
|
+
<div class="archive-card-stats">
|
|
45
|
+
<span class="archive-card-stat">Stories: <span class="stat-val">${archive.summary.storyCount}</span></span>
|
|
46
|
+
<span class="archive-card-stat">Tasks: <span class="stat-val">${archive.summary.taskCount}</span></span>
|
|
47
|
+
<span class="archive-card-stat">Files: <span class="stat-val">${archive.fileCount}</span></span>
|
|
48
|
+
<span class="archive-card-chevron">▶</span>
|
|
49
|
+
</div>
|
|
50
|
+
</div>`;
|
|
51
|
+
|
|
52
|
+
if (isExpanded) {
|
|
53
|
+
const files = state.archiveFilesCache[archive.timestamp];
|
|
54
|
+
if (files) {
|
|
55
|
+
html += '<div class="archive-files">';
|
|
56
|
+
for (const file of files) {
|
|
57
|
+
const isActive = state.viewingArchiveFile === file.path;
|
|
58
|
+
html += `<div class="archive-file-item${isActive ? ' active' : ''}" data-archive-file="${esc(file.path)}" data-archive-ts="${esc(archive.timestamp)}">
|
|
59
|
+
<span class="archive-file-icon">📄</span>
|
|
60
|
+
<span>${esc(file.path)}</span>
|
|
61
|
+
</div>`;
|
|
62
|
+
}
|
|
63
|
+
html += '</div>';
|
|
64
|
+
|
|
65
|
+
if (state.viewingArchiveFile) {
|
|
66
|
+
html += `<div class="archive-file-viewer">
|
|
67
|
+
<div class="archive-file-viewer-header">
|
|
68
|
+
<span>${esc(state.viewingArchiveFile)}</span>
|
|
69
|
+
<button class="archive-file-viewer-close" data-close-viewer="true">×</button>
|
|
70
|
+
</div>
|
|
71
|
+
<div class="archive-file-content" id="archiveFileContent">Loading...</div>
|
|
72
|
+
</div>`;
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
html += '<div class="archive-files" style="padding:16px;color:var(--text-dim);font-size:12px">Loading files...</div>';
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
html += '</div>';
|
|
80
|
+
}
|
|
81
|
+
html += '</div>';
|
|
82
|
+
|
|
83
|
+
container.innerHTML = html;
|
|
84
|
+
|
|
85
|
+
// Bind archive card toggle clicks
|
|
86
|
+
container.querySelectorAll('.archive-card-header').forEach(header => {
|
|
87
|
+
header.addEventListener('click', () => toggleArchiveCard(appName, header.dataset.archiveToggle));
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Bind archive file clicks
|
|
91
|
+
container.querySelectorAll('.archive-file-item').forEach(item => {
|
|
92
|
+
item.addEventListener('click', () => {
|
|
93
|
+
viewArchiveFile(appName, item.dataset.archiveTs, item.dataset.archiveFile);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Bind file viewer close button
|
|
98
|
+
const closeBtn = container.querySelector('[data-close-viewer]');
|
|
99
|
+
if (closeBtn) {
|
|
100
|
+
closeBtn.addEventListener('click', () => {
|
|
101
|
+
state.viewingArchiveFile = null;
|
|
102
|
+
renderArchivesView(container, appName);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Load file content if viewer is open
|
|
107
|
+
if (state.viewingArchiveFile && state.expandedArchive) {
|
|
108
|
+
loadArchiveFileContent(appName, state.expandedArchive, state.viewingArchiveFile);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function toggleArchiveCard(appName, timestamp) {
|
|
113
|
+
const container = document.getElementById('archivesContainer');
|
|
114
|
+
if (!container) return;
|
|
115
|
+
|
|
116
|
+
if (state.expandedArchive === timestamp) {
|
|
117
|
+
state.expandedArchive = null;
|
|
118
|
+
state.viewingArchiveFile = null;
|
|
119
|
+
renderArchivesView(container, appName);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
state.expandedArchive = timestamp;
|
|
124
|
+
state.viewingArchiveFile = null;
|
|
125
|
+
|
|
126
|
+
// Load files if not cached
|
|
127
|
+
if (!state.archiveFilesCache[timestamp]) {
|
|
128
|
+
renderArchivesView(container, appName);
|
|
129
|
+
try {
|
|
130
|
+
const files = await fetchJson(`/api/apps/${encodeURIComponent(appName)}/archives/${encodeURIComponent(timestamp)}/files`);
|
|
131
|
+
state.archiveFilesCache[timestamp] = files;
|
|
132
|
+
} catch {
|
|
133
|
+
state.archiveFilesCache[timestamp] = [];
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
renderArchivesView(container, appName);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function viewArchiveFile(appName, timestamp, filePath) {
|
|
141
|
+
const container = document.getElementById('archivesContainer');
|
|
142
|
+
if (!container) return;
|
|
143
|
+
|
|
144
|
+
state.viewingArchiveFile = filePath;
|
|
145
|
+
renderArchivesView(container, appName);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function loadArchiveFileContent(appName, timestamp, filePath) {
|
|
149
|
+
const contentEl = document.getElementById('archiveFileContent');
|
|
150
|
+
if (!contentEl) return;
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
const data = await fetchJson(`/api/apps/${encodeURIComponent(appName)}/archives/${encodeURIComponent(timestamp)}/files/${filePath}`);
|
|
154
|
+
contentEl.textContent = data.content || '(empty file)';
|
|
155
|
+
} catch {
|
|
156
|
+
contentEl.textContent = '(Error loading file)';
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function formatArchiveTimestamp(ts) {
|
|
161
|
+
// Format: 2026-03-14_15-30 → Mar 14, 2026 at 15:30
|
|
162
|
+
const match = ts.match(/^(\d{4})-(\d{2})-(\d{2})_(\d{2})-(\d{2})/);
|
|
163
|
+
if (!match) return ts;
|
|
164
|
+
const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
|
|
165
|
+
const [, year, month, day, hour, min] = match;
|
|
166
|
+
return `${months[parseInt(month, 10) - 1]} ${parseInt(day, 10)}, ${year} at ${hour}:${min}`;
|
|
167
|
+
}
|