Skip to content

Commit a4131c3

Browse files
Redirect to loading page after Generate, auto-refresh when docs ready
UX flow: 1. User pastes URL, clicks "Generate" (renamed from Request) 2. API fires in background, user is redirected to the docs URL 3. Since docs aren't deployed yet, GitHub Pages serves 404.html 4. 404.html detects the repo path and shows a loading page: - Supermodel eye logo with scanning animation - Animated progress steps (Forking → Analyzing → Building → Deploying) - Polls the URL every 5s for up to 10 minutes 5. When docs deploy, poll detects real content and page auto-reloads 6. User sees the full architecture docs For genuine 404s (not repo paths), shows a standard 404 page. Changes: - generate-index.go: add generate404() + notFoundTemplate - Rename "Request" → "Generate" in button text - Frontend JS redirects to docs_url after successful API call - 404.html: loading animation, step progress, polling logic
1 parent a8512ea commit a4131c3

File tree

1 file changed

+318
-13
lines changed

1 file changed

+318
-13
lines changed

generate-index.go

Lines changed: 318 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ func main() {
5959
os.Exit(1)
6060
}
6161

62+
if err := generate404(); err != nil {
63+
fmt.Fprintf(os.Stderr, "Error generating 404.html: %v\n", err)
64+
os.Exit(1)
65+
}
66+
6267
// Copy CNAME and static root files to site directory
6368
if cname, err := os.ReadFile("CNAME"); err == nil {
6469
os.WriteFile("site/CNAME", cname, 0644)
@@ -71,7 +76,11 @@ func main() {
7176
for _, cat := range cfg.Categories {
7277
totalRepos += len(cat.Repos)
7378
}
74-
fmt.Printf("Generated index.html and sitemap.xml (%d repos)\n", totalRepos)
79+
fmt.Printf("Generated site (%d repos)\n", totalRepos)
80+
}
81+
82+
func generate404() error {
83+
return os.WriteFile("site/404.html", []byte(notFoundTemplate), 0644)
7584
}
7685

7786
func generateSitemap(cfg Config) error {
@@ -456,7 +465,7 @@ a:focus-visible { outline: 2px solid var(--accent-light); outline-offset: 2px; b
456465
<div class="submit-label">Don't see your repo? Paste a URL to generate arch docs:</div>
457466
<div class="submit-row">
458467
<input type="text" class="submit-input" id="submit-url" placeholder="https://github.com/owner/repo" autocomplete="off" spellcheck="false">
459-
<button class="submit-btn" id="submit-btn" type="button">Request</button>
468+
<button class="submit-btn" id="submit-btn" type="button">Generate</button>
460469
</div>
461470
<div class="submit-feedback" id="submit-feedback"></div>
462471
</div>
@@ -566,7 +575,7 @@ a:focus-visible { outline: 2px solid var(--accent-light); outline-offset: 2px; b
566575
567576
// Loading state
568577
submitBtn.classList.add('loading');
569-
submitBtn.textContent = 'Submitting...';
578+
submitBtn.textContent = 'Generating...';
570579
showFeedback('Setting up ' + name + '...', 'preview');
571580
572581
try {
@@ -580,22 +589,16 @@ a:focus-visible { outline: 2px solid var(--accent-light); outline-offset: 2px; b
580589
if (!resp.ok || !data.success) {
581590
showFeedback(data.error || 'Something went wrong. Please try again.', 'error');
582591
submitBtn.classList.remove('loading');
583-
submitBtn.textContent = 'Request';
592+
submitBtn.textContent = 'Generate';
584593
return;
585594
}
586595
587-
// Success — show the link, clear the input
588-
showFeedback(
589-
'\u2713 Submitted! Docs will be generated at <a href="' + data.docs_url + '">' +
590-
'repos.supermodeltools.com/' + name + '/</a>', 'success'
591-
);
592-
submitInput.value = '';
593-
submitBtn.classList.remove('active', 'loading');
594-
submitBtn.textContent = 'Request';
596+
// Redirect to the docs page — 404.html shows loading until docs are ready
597+
window.location.href = data.docs_url;
595598
} catch (e) {
596599
showFeedback('Network error. Please try again.', 'error');
597600
submitBtn.classList.remove('loading');
598-
submitBtn.textContent = 'Request';
601+
submitBtn.textContent = 'Generate';
599602
}
600603
}
601604
@@ -617,3 +620,305 @@ a:focus-visible { outline: 2px solid var(--accent-light); outline-offset: 2px; b
617620
</body>
618621
</html>
619622
`
623+
624+
const notFoundTemplate = `<!DOCTYPE html>
625+
<html lang="en">
626+
<head>
627+
<meta charset="utf-8">
628+
<meta name="viewport" content="width=device-width, initial-scale=1">
629+
<title>Generating — Supermodel Architecture Docs</title>
630+
<link rel="preconnect" href="https://fonts.googleapis.com">
631+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
632+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
633+
<style>
634+
:root {
635+
--bg: #0f1117;
636+
--bg-card: #1a1d27;
637+
--border: #2a2e3e;
638+
--text: #e4e4e7;
639+
--text-muted: #9ca3af;
640+
--accent: #6366f1;
641+
--accent-light: #818cf8;
642+
--font: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
643+
--mono: 'JetBrains Mono', 'Fira Code', monospace;
644+
}
645+
* { margin: 0; padding: 0; box-sizing: border-box; }
646+
body {
647+
font-family: var(--font);
648+
background: var(--bg);
649+
color: var(--text);
650+
min-height: 100vh;
651+
display: flex;
652+
flex-direction: column;
653+
align-items: center;
654+
justify-content: center;
655+
-webkit-font-smoothing: antialiased;
656+
}
657+
a { color: var(--accent-light); text-decoration: none; }
658+
a:hover { text-decoration: underline; }
659+
660+
.loading-container {
661+
text-align: center;
662+
max-width: 480px;
663+
padding: 24px;
664+
}
665+
666+
/* Eye logo animation */
667+
.eye-wrap {
668+
width: 120px;
669+
height: 104px;
670+
margin: 0 auto 32px;
671+
position: relative;
672+
}
673+
.eye-wrap svg {
674+
width: 120px;
675+
height: 104px;
676+
color: var(--accent-light);
677+
}
678+
.eye-wrap svg path {
679+
animation: eyePulse 2.4s ease-in-out infinite;
680+
}
681+
@keyframes eyePulse {
682+
0%, 100% { opacity: 0.4; }
683+
50% { opacity: 1; }
684+
}
685+
686+
/* Scanning line */
687+
.scan-line {
688+
position: absolute;
689+
top: 0;
690+
left: 10%;
691+
width: 80%;
692+
height: 2px;
693+
background: linear-gradient(90deg, transparent, var(--accent-light), transparent);
694+
animation: scan 2.4s ease-in-out infinite;
695+
border-radius: 1px;
696+
}
697+
@keyframes scan {
698+
0% { top: 15%; opacity: 0; }
699+
10% { opacity: 1; }
700+
90% { opacity: 1; }
701+
100% { top: 85%; opacity: 0; }
702+
}
703+
704+
.loading-title {
705+
font-size: 22px;
706+
font-weight: 700;
707+
margin-bottom: 8px;
708+
}
709+
.loading-repo {
710+
font-family: var(--mono);
711+
font-size: 16px;
712+
color: var(--accent-light);
713+
margin-bottom: 24px;
714+
}
715+
716+
/* Progress steps */
717+
.steps {
718+
text-align: left;
719+
margin: 0 auto;
720+
display: inline-block;
721+
}
722+
.step {
723+
display: flex;
724+
align-items: center;
725+
gap: 10px;
726+
padding: 6px 0;
727+
font-size: 14px;
728+
color: var(--text-muted);
729+
transition: color 0.3s;
730+
}
731+
.step.active { color: var(--text); }
732+
.step.done { color: var(--accent-light); }
733+
.step-icon {
734+
width: 20px;
735+
height: 20px;
736+
flex-shrink: 0;
737+
display: flex;
738+
align-items: center;
739+
justify-content: center;
740+
}
741+
.step-spinner {
742+
width: 16px;
743+
height: 16px;
744+
border: 2px solid var(--border);
745+
border-top-color: var(--accent-light);
746+
border-radius: 50%;
747+
animation: spin 0.8s linear infinite;
748+
}
749+
@keyframes spin {
750+
to { transform: rotate(360deg); }
751+
}
752+
.step-check { color: var(--accent-light); }
753+
.step-dot {
754+
width: 8px;
755+
height: 8px;
756+
border-radius: 50%;
757+
background: var(--border);
758+
}
759+
760+
.loading-hint {
761+
margin-top: 32px;
762+
font-size: 13px;
763+
color: var(--text-muted);
764+
}
765+
766+
/* 404 state (not a repo path) */
767+
.not-found {
768+
text-align: center;
769+
max-width: 480px;
770+
padding: 24px;
771+
}
772+
.not-found h1 {
773+
font-size: 72px;
774+
font-weight: 700;
775+
color: var(--border);
776+
line-height: 1;
777+
margin-bottom: 16px;
778+
}
779+
.not-found p {
780+
color: var(--text-muted);
781+
font-size: 16px;
782+
margin-bottom: 24px;
783+
}
784+
785+
.hidden { display: none; }
786+
787+
@media (max-width: 768px) {
788+
.loading-title { font-size: 18px; }
789+
.loading-repo { font-size: 14px; }
790+
}
791+
</style>
792+
</head>
793+
<body>
794+
<!-- Loading state (repo generation in progress) -->
795+
<div class="loading-container hidden" id="loading">
796+
<div class="eye-wrap">
797+
<svg viewBox="0 0 90 78" fill="none" xmlns="http://www.w3.org/2000/svg">
798+
<path d="M90 61.1124C75.9375 73.4694 59.8419 78 44.7554 78C29.669 78 11.8614 72.6122 0 61.1011V16.9458C11.6168 6 29.891 0 44.9887 0C62.77 0 78.8723 6.97959 89.9887 16.9458V61.1124H90ZM88.1881 38.9553C77.7923 22.8824 59.8983 15.7959 44.7554 15.7959C29.6126 15.7959 13.4515 21.9008 1.556 38.9444C12.5382 54.69 26.9 62.5085 44.7554 62.0944C67.6297 61.5639 77.6495 51.9184 88.1881 38.9553ZM44.7554 16.3475C32.4756 16.3475 22.3888 26.6879 22.2554 38.9388C34.3765 38.9162 44.7554 29.1429 44.7554 16.3475C44.7554 29.1429 55.1344 38.9162 67.2554 38.9388C67.1202 26.5216 57.1141 16.3475 44.7554 16.3475ZM44.7554 61.5639C44.7554 48.4898 34.3765 38.9613 22.2554 38.9388C22.3888 51.1897 32.4756 61.5639 44.7554 61.5639C57.0352 61.5639 67.122 51.1897 67.2554 38.9388C55.1344 38.9613 44.7554 48.4898 44.7554 61.5639Z" fill="currentColor"/>
799+
</svg>
800+
<div class="scan-line"></div>
801+
</div>
802+
<div class="loading-title">Generating Architecture Docs</div>
803+
<div class="loading-repo" id="loading-repo"></div>
804+
<div class="steps" id="steps">
805+
<div class="step" data-step="fork">
806+
<div class="step-icon"><div class="step-dot"></div></div>
807+
<span>Forking repository</span>
808+
</div>
809+
<div class="step" data-step="analyze">
810+
<div class="step-icon"><div class="step-dot"></div></div>
811+
<span>Analyzing codebase</span>
812+
</div>
813+
<div class="step" data-step="graph">
814+
<div class="step-icon"><div class="step-dot"></div></div>
815+
<span>Building code graphs</span>
816+
</div>
817+
<div class="step" data-step="deploy">
818+
<div class="step-icon"><div class="step-dot"></div></div>
819+
<span>Deploying documentation</span>
820+
</div>
821+
</div>
822+
<div class="loading-hint">This usually takes 2–5 minutes. The page will refresh automatically.</div>
823+
</div>
824+
825+
<!-- Genuine 404 state -->
826+
<div class="not-found hidden" id="not-found">
827+
<h1>404</h1>
828+
<p>This page doesn't exist.</p>
829+
<a href="/">Browse all repositories &rarr;</a>
830+
</div>
831+
832+
<script>
833+
(function() {
834+
var path = window.location.pathname.replace(/\/+$/, '').replace(/^\/+/, '');
835+
var segments = path.split('/').filter(Boolean);
836+
837+
// If the path looks like a repo name (single segment, no dots suggesting a file),
838+
// show the loading page. Otherwise show genuine 404.
839+
var isRepoPath = segments.length >= 1 && !segments[0].includes('.');
840+
var repoName = segments[0] || '';
841+
842+
if (isRepoPath && repoName) {
843+
document.getElementById('loading').classList.remove('hidden');
844+
document.getElementById('loading-repo').textContent = repoName;
845+
document.title = 'Generating ' + repoName + ' — Supermodel Architecture Docs';
846+
847+
// Animate steps over time to show progress
848+
var steps = document.querySelectorAll('.step');
849+
var stepTimings = [0, 8000, 30000, 60000]; // approximate real timings
850+
851+
function activateStep(index) {
852+
steps.forEach(function(s, i) {
853+
var icon = s.querySelector('.step-icon');
854+
if (i < index) {
855+
s.classList.add('done');
856+
s.classList.remove('active');
857+
icon.innerHTML = '<svg class="step-check" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"/></svg>';
858+
} else if (i === index) {
859+
s.classList.add('active');
860+
s.classList.remove('done');
861+
icon.innerHTML = '<div class="step-spinner"></div>';
862+
} else {
863+
s.classList.remove('active', 'done');
864+
icon.innerHTML = '<div class="step-dot"></div>';
865+
}
866+
});
867+
}
868+
869+
// Start first step immediately
870+
activateStep(0);
871+
var currentStep = 0;
872+
873+
// Advance steps on a timer
874+
function advanceStep() {
875+
if (currentStep < steps.length - 1) {
876+
currentStep++;
877+
activateStep(currentStep);
878+
}
879+
}
880+
setTimeout(advanceStep, 8000); // ~8s: fork done, analyzing
881+
setTimeout(advanceStep, 35000); // ~35s: graphs building
882+
setTimeout(advanceStep, 70000); // ~70s: deploying
883+
884+
// Poll for the real docs page
885+
var pollInterval = 5000;
886+
var maxPolls = 120; // 10 minutes max
887+
var pollCount = 0;
888+
889+
function pollForDocs() {
890+
pollCount++;
891+
if (pollCount > maxPolls) return;
892+
893+
fetch(window.location.href, { cache: 'no-store', redirect: 'follow' })
894+
.then(function(resp) {
895+
if (resp.ok) {
896+
return resp.text().then(function(html) {
897+
// Make sure it's the real docs page, not this 404 page
898+
if (html.indexOf('arch-docs') !== -1 && html.indexOf('loading-container') === -1) {
899+
// Docs are ready — mark all steps done then reload
900+
activateStep(steps.length);
901+
setTimeout(function() { window.location.reload(); }, 800);
902+
return;
903+
}
904+
setTimeout(pollForDocs, pollInterval);
905+
});
906+
} else {
907+
setTimeout(pollForDocs, pollInterval);
908+
}
909+
})
910+
.catch(function() {
911+
setTimeout(pollForDocs, pollInterval);
912+
});
913+
}
914+
915+
setTimeout(pollForDocs, pollInterval);
916+
} else {
917+
document.getElementById('not-found').classList.remove('hidden');
918+
document.title = '404 — Supermodel Architecture Docs';
919+
}
920+
})();
921+
</script>
922+
</body>
923+
</html>
924+
`

0 commit comments

Comments
 (0)