-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.html
More file actions
1254 lines (1129 loc) · 64.6 KB
/
Copy pathindex.html
File metadata and controls
1254 lines (1129 loc) · 64.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Grant Engine v3 — Search, Vet, Draft, Track</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body { background: #0a0a0a; color: #e8e2d4; font-family: Georgia, 'Times New Roman', serif; min-height: 100vh; }
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: #0a0a0a; }
::-webkit-scrollbar-thumb { background: #3a3024; }
::-webkit-scrollbar-thumb:hover { background: #c89a3c; }
header { border-bottom: 4px solid #c89a3c; padding: 24px 36px; background: linear-gradient(180deg, #1a1410 0%, #0a0a0a 100%); }
.header-row { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 14px; }
.brand-tag { font-size: 10px; letter-spacing: 4px; color: #c89a3c; margin-bottom: 4px; text-transform: uppercase; }
h1 { font-size: 34px; font-weight: 400; letter-spacing: -1px; }
.tagline { font-size: 12px; color: #8b7355; margin-top: 4px; font-style: italic; }
.llc-switcher { display: flex; align-items: center; gap: 10px; }
.llc-switcher label { font-size: 10px; letter-spacing: 2px; color: #8b7355; text-transform: uppercase; }
.llc-switcher select { background: #1a1410; color: #c89a3c; border: 1px solid #c89a3c; padding: 8px 14px; font-family: Georgia, serif; font-size: 13px; font-weight: 700; cursor: pointer; }
nav { display: flex; border-bottom: 1px solid #2a2520; background: #0f0c08; overflow-x: auto; }
.tab { padding: 16px 22px; background: transparent; border: none; border-bottom: 3px solid transparent; color: #8b7355; font-size: 11px; letter-spacing: 2px; text-transform: uppercase; cursor: pointer; font-family: Georgia, serif; transition: all 0.15s; white-space: nowrap; }
.tab.active { background: #1a1410; border-bottom-color: #c89a3c; color: #c89a3c; }
.tab:hover:not(.active) { color: #c89a3c; }
.panel { display: none; padding: 28px 36px; }
.panel.active { display: block; }
.ai-status { padding: 12px 18px; margin-bottom: 20px; border: 1px solid; display: flex; align-items: center; justify-content: space-between; gap: 14px; flex-wrap: wrap; font-size: 12px; }
.ai-status.disabled { border-color: #c89a3c; background: #1a1410; color: #c89a3c; }
.ai-status.enabled { border-color: #2d6a4f; background: #0d1f15; color: #2d6a4f; }
.ai-status button { background: #c89a3c; color: #0a0a0a; border: none; padding: 7px 14px; font-size: 10px; letter-spacing: 2px; text-transform: uppercase; cursor: pointer; font-family: Georgia, serif; font-weight: 700; }
.search-box, select, input[type=text], input[type=email], input[type=number], input[type=url], textarea {
background: #1a1410; border: 1px solid #3a3024; color: #e8e2d4; padding: 11px 13px; font-family: Georgia, serif; font-size: 14px; outline: none;
}
input:focus, textarea:focus, select:focus { border-color: #c89a3c; }
textarea { resize: vertical; min-height: 70px; width: 100%; }
.quick-cat-row { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 16px; }
.quick-cat { padding: 8px 14px; background: #1a1410; border: 1px solid #3a3024; color: #a89c80; cursor: pointer; font-family: Georgia, serif; font-size: 12px; letter-spacing: 1px; text-transform: uppercase; transition: all 0.15s; }
.quick-cat:hover { border-color: #c89a3c; color: #c89a3c; }
.search-row { display: flex; gap: 10px; margin-bottom: 16px; flex-wrap: wrap; }
.search-row .search-box { flex: 1; min-width: 240px; }
.btn { padding: 9px 14px; font-size: 11px; letter-spacing: 1.5px; text-transform: uppercase; text-decoration: none; text-align: center; cursor: pointer; font-family: Georgia, serif; border: 1px solid; background: transparent; transition: all 0.15s; display: inline-flex; align-items: center; justify-content: center; gap: 6px; }
.btn-gold { border-color: #c89a3c; color: #c89a3c; }
.btn-gold:hover { background: #c89a3c; color: #0a0a0a; }
.btn-solid { background: #c89a3c; border-color: #c89a3c; color: #0a0a0a; font-weight: 700; }
.btn-solid:hover { background: #d4a84a; }
.btn-ghost { border-color: #3a3024; color: #8b7355; }
.btn-ghost:hover { border-color: #c89a3c; color: #c89a3c; }
.btn-danger { border-color: #3a3024; color: #9b2226; padding: 8px 11px; }
.result-card, .grant-card { background: #13100b; border: 1px solid #2a2520; padding: 18px; margin-bottom: 12px; }
.grant-card { border-left-width: 4px; }
.result-card:hover { border-color: #c89a3c; }
.result-title { font-size: 16px; font-weight: 600; line-height: 1.3; margin-bottom: 6px; }
.result-meta { font-size: 11px; color: #8b7355; margin-bottom: 8px; display: flex; gap: 14px; flex-wrap: wrap; }
.result-meta strong { color: #c89a3c; }
.result-desc { font-size: 13px; color: #a89c80; line-height: 1.55; margin-bottom: 10px; }
.result-actions { display: flex; gap: 8px; flex-wrap: wrap; }
.form-field { margin-bottom: 14px; }
.form-label { display: block; font-size: 10px; letter-spacing: 2px; color: #8b7355; text-transform: uppercase; margin-bottom: 5px; }
.pill { padding: 3px 9px; font-size: 10px; letter-spacing: 1.5px; text-transform: uppercase; font-weight: 700; }
.pill-status { border: 1px solid; }
.empty-state { text-align: center; padding: 50px 20px; border: 2px dashed #3a3024; background: #13100b; color: #8b7355; }
.modal-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,0.85); display: none; align-items: center; justify-content: center; z-index: 1000; padding: 20px; }
.modal-backdrop.active { display: flex; }
.modal { background: #13100b; border: 1px solid #c89a3c; padding: 26px; max-width: 600px; width: 100%; max-height: 90vh; overflow-y: auto; }
.modal h3 { color: #c89a3c; font-size: 20px; font-weight: 400; margin-bottom: 16px; }
.loading-spinner { display: inline-block; width: 12px; height: 12px; border: 2px solid #c89a3c; border-top-color: transparent; border-radius: 50%; animation: spin 0.8s linear infinite; margin-right: 6px; vertical-align: middle; }
@keyframes spin { to { transform: rotate(360deg); } }
.footer { padding: 18px 36px; border-top: 1px solid #2a2520; text-align: center; font-size: 10px; color: #5a4f3f; letter-spacing: 2px; text-transform: uppercase; margin-top: 28px; }
.llc-tabs { display: flex; gap: 6px; margin-bottom: 18px; flex-wrap: wrap; }
.llc-tab { padding: 8px 14px; background: #1a1410; border: 1px solid #3a3024; color: #8b7355; cursor: pointer; font-family: Georgia, serif; font-size: 11px; letter-spacing: 1px; text-transform: uppercase; }
.llc-tab.active { background: #c89a3c; color: #0a0a0a; border-color: #c89a3c; font-weight: 700; }
.draft-section { background: #0a0a0a; border: 1px solid #2a2520; padding: 14px; margin-bottom: 10px; }
.draft-section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
.draft-section-title { font-size: 11px; letter-spacing: 2px; color: #c89a3c; text-transform: uppercase; font-weight: 700; }
.draft-section-body { font-size: 13px; color: #e8e2d4; line-height: 1.6; white-space: pre-wrap; }
.scrape-list { background: #13100b; border: 1px solid #2a2520; padding: 18px; margin-bottom: 14px; }
.scrape-list h4 { font-size: 12px; letter-spacing: 2px; text-transform: uppercase; margin-bottom: 8px; }
.scrape-list ul { padding-left: 18px; font-size: 13px; color: #a89c80; line-height: 1.7; }
.scrape-good h4 { color: #2d6a4f; }
.scrape-hit h4 { color: #c89a3c; }
.scrape-bad h4 { color: #9b2226; }
.vetting-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-top: 10px; }
@media (max-width: 680px) { .vetting-grid { grid-template-columns: 1fr; } header, .panel, .footer { padding-left: 16px; padding-right: 16px; } h1 { font-size: 26px; } }
.toast { position: fixed; bottom: 20px; right: 20px; background: #2d6a4f; color: #e8e2d4; padding: 12px 18px; font-size: 12px; letter-spacing: 1px; text-transform: uppercase; z-index: 2000; opacity: 0; transition: opacity 0.3s; }
.toast.show { opacity: 1; }
</style>
</head>
<body>
<header>
<div class="header-row">
<div>
<div class="brand-tag">The Grant Engine — v3</div>
<h1>Search · Vet · Draft · Track</h1>
<div class="tagline">Live Grants.gov API · Multi-LLC · AI-Drafted Applications</div>
</div>
<div class="llc-switcher">
<label>Active LLC</label>
<select id="active-llc-select" onchange="setActiveLlc(this.value)"></select>
</div>
</div>
</header>
<nav>
<button class="tab active" data-tab="search">01 · Search</button>
<button class="tab" data-tab="pipeline">02 · Pipeline</button>
<button class="tab" data-tab="profiles">03 · LLC Profiles</button>
<button class="tab" data-tab="export">04 · Export / CRM</button>
<button class="tab" data-tab="scraping">05 · Scraping Truth</button>
<button class="tab" data-tab="instructions">06 · How To Use</button>
</nav>
<!-- AI STATUS BANNER (shown across tabs that need it) -->
<div id="ai-banner-host"></div>
<!-- 01 SEARCH -->
<section id="panel-search" class="panel active">
<h2 style="font-size:22px;color:#c89a3c;font-weight:400;margin-bottom:10px;">Live Search · Grants.gov Federal API</h2>
<p style="font-size:12px;color:#8b7355;margin-bottom:18px;line-height:1.6;">Free public API. No auth needed. Searches the entire federal grant database in real time. Click any result to add to your active LLC's pipeline.</p>
<div class="search-row">
<input type="text" class="search-box" id="search-input" placeholder='Search keywords (e.g. "music education", "Hispanic-owned", "solar South Texas")...' onkeydown="if(event.key==='Enter')doSearch()">
<button class="btn btn-solid" onclick="doSearch()">Search Grants.gov</button>
</div>
<div style="font-size:11px;color:#8b7355;letter-spacing:1px;text-transform:uppercase;margin-bottom:8px;">Quick categories</div>
<div class="quick-cat-row" id="quick-cats"></div>
<div id="search-status" style="font-size:12px;color:#8b7355;margin:14px 0;"></div>
<div id="search-results"></div>
</section>
<!-- 02 PIPELINE -->
<section id="panel-pipeline" class="panel">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px;flex-wrap:wrap;gap:10px;">
<div>
<h2 style="font-size:22px;color:#c89a3c;font-weight:400;">Pipeline · <span id="pipeline-llc-name"></span></h2>
<div id="pipeline-summary" style="font-size:12px;color:#8b7355;margin-top:4px;"></div>
</div>
<button class="btn btn-solid" onclick="openAddModal()">+ Add Grant Manually</button>
</div>
<div style="background:#1a1410;border:2px solid #c89a3c;padding:18px;margin-bottom:18px;">
<div style="font-size:11px;letter-spacing:2px;color:#c89a3c;text-transform:uppercase;font-weight:700;margin-bottom:10px;">Quick Add by URL</div>
<div style="font-size:12px;color:#a89c80;margin-bottom:10px;">Paste any grant URL. AI mode ON → auto-ingest. AI OFF → just saves the URL.</div>
<div style="display:flex;gap:8px;flex-wrap:wrap;">
<input type="url" id="quick-url" placeholder="https://..." style="flex:1;min-width:280px;background:#0a0a0a;">
<input type="text" id="quick-name" placeholder="Grant name (optional)" style="flex:1;min-width:180px;background:#0a0a0a;">
<button class="btn btn-solid" onclick="addFromUrl()">Add</button>
</div>
</div>
<div id="pipeline-list"></div>
</section>
<!-- 03 LLC PROFILES -->
<section id="panel-profiles" class="panel">
<h2 style="font-size:22px;color:#c89a3c;font-weight:400;margin-bottom:6px;">LLC Profiles</h2>
<p style="font-size:12px;color:#8b7355;margin-bottom:18px;">Each LLC has its own profile. AI uses the active LLC's profile when vetting and drafting. Rename any LLC anytime (especially the 4th slot).</p>
<div style="background:#1a1410;border:1px solid #c89a3c;padding:18px;margin-bottom:24px;">
<div class="form-field">
<label class="form-label">Anthropic API Key (shared across all LLCs)</label>
<input type="text" id="api-key" placeholder="sk-ant-..." style="width:100%;">
<div style="font-size:11px;color:#5a4f3f;margin-top:4px;">Stored in your browser only. Get a key at console.anthropic.com → API keys. $5 min top-up.</div>
</div>
<button class="btn btn-solid" onclick="saveApiKey()">Save API Key</button>
</div>
<div class="llc-tabs" id="llc-tabs"></div>
<div id="llc-profile-form"></div>
</section>
<!-- 04 EXPORT / CRM -->
<section id="panel-export" class="panel">
<h2 style="font-size:22px;color:#c89a3c;font-weight:400;margin-bottom:6px;">Export · Manual CRM Sync</h2>
<p style="font-size:12px;color:#8b7355;margin-bottom:24px;line-height:1.6;">
Tonight: export your pipeline as CSV (Google Sheets), JSON (full backup), or Markdown (Google Docs / Notion). Upload to your Google Drive manually.<br>
Future Path C deploy: Khan will wire this to auto-sync to Google Sheets so you, Khan, and any management person see the same live CRM.
</p>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:14px;margin-bottom:24px;">
<div style="background:#13100b;border:1px solid #2a2520;padding:18px;">
<h3 style="font-size:14px;color:#c89a3c;letter-spacing:2px;text-transform:uppercase;margin-bottom:8px;">📊 CSV for Google Sheets</h3>
<p style="font-size:12px;color:#a89c80;margin-bottom:14px;line-height:1.6;">Tabular format. 11 columns: LLC, Name, Funder, Amount, Deadline, Status, Fit Score, Apply URL, Notes, Added, Last Updated. Open in Google Sheets via File → Import.</p>
<button class="btn btn-gold" onclick="exportCsv()" style="width:100%;">Download CSV</button>
</div>
<div style="background:#13100b;border:1px solid #2a2520;padding:18px;">
<h3 style="font-size:14px;color:#c89a3c;letter-spacing:2px;text-transform:uppercase;margin-bottom:8px;">💾 JSON Full Backup</h3>
<p style="font-size:12px;color:#a89c80;margin-bottom:14px;line-height:1.6;">Complete data — all grants, drafts, vetting reports, profiles. Use this to back up before clearing browser data or moving to another machine.</p>
<button class="btn btn-gold" onclick="exportJson()" style="width:100%;">Download JSON</button>
</div>
<div style="background:#13100b;border:1px solid #2a2520;padding:18px;">
<h3 style="font-size:14px;color:#c89a3c;letter-spacing:2px;text-transform:uppercase;margin-bottom:8px;">📝 Markdown for Google Docs</h3>
<p style="font-size:12px;color:#a89c80;margin-bottom:14px;line-height:1.6;">One .md file with every grant's drafted application. Open in Google Docs / Notion / VS Code. Paste sections into the real application forms.</p>
<button class="btn btn-gold" onclick="exportMarkdown()" style="width:100%;">Download Markdown</button>
</div>
</div>
<div style="background:#1a1410;border:1px solid #c89a3c;padding:18px;">
<h3 style="font-size:14px;color:#c89a3c;letter-spacing:2px;text-transform:uppercase;margin-bottom:10px;">📥 Restore from JSON Backup</h3>
<p style="font-size:12px;color:#a89c80;margin-bottom:12px;">Replace all current data with a previous JSON backup (e.g. from another browser, machine, or Khan).</p>
<input type="file" id="restore-file" accept=".json" style="margin-bottom:10px;display:block;color:#e8e2d4;">
<button class="btn btn-gold" onclick="restoreFromJson()">Restore Data</button>
</div>
<div style="margin-top:32px;background:#0f0c08;border:1px dashed #3a3024;padding:18px;">
<h3 style="font-size:13px;color:#8b7355;letter-spacing:2px;text-transform:uppercase;margin-bottom:8px;">🎯 Google Drive Strategy</h3>
<p style="font-size:12px;color:#a89c80;line-height:1.7;">
<strong style="color:#c89a3c;">Recommended folder structure</strong> in Google Drive (create manually for now):<br>
📁 Grant Engine (root)<br>
├─ 📁 HVAC Contractor LLC → spreadsheet + per-grant Google Doc<br>
├─ 📁 Real Estate LLC → spreadsheet + per-grant Google Doc<br>
├─ 📁 Marketing Agency LLC → spreadsheet + per-grant Google Doc<br>
└─ 📁 [renameable] LLC → spreadsheet + per-grant Google Doc<br><br>
Workflow: export CSV per LLC, paste into the LLC's sheet. Export Markdown per LLC, paste into the per-grant Doc. Share each folder with Khan/management. Path C deploy will automate all of this.
</p>
</div>
</section>
<!-- 05 SCRAPING TRUTH -->
<section id="panel-scraping" class="panel">
<h2 style="font-size:22px;color:#c89a3c;font-weight:400;margin-bottom:8px;">Scraping · Honest Reality</h2>
<p style="font-size:13px;color:#a89c80;margin-bottom:22px;line-height:1.7;">
The Ingest URL button uses a public CORS proxy. Some sites work great; some block scrapers. Here's the honest picture so you know what to expect.
</p>
<div class="scrape-list scrape-good">
<h4>✅ Scraping Works Reliably</h4>
<ul>
<li><strong>Grants.gov API</strong> (live federal grant search — use the Search tab, no scraping needed)</li>
<li>Foundation pages (Candid, FoundationCenter, Council on Foundations)</li>
<li>Corporate grant pages (FedEx Small Business, Comcast RISE, Verizon, Hello Alice)</li>
<li>State arts/economic dev (Texas Commission on the Arts, Texas Governor's Office)</li>
<li>USDA, DOE, HUD, NSF landing pages</li>
<li>LaunchSA, San Antonio Economic Development</li>
<li>Most simple .gov pages with the opportunity text on a single URL</li>
</ul>
</div>
<div class="scrape-list scrape-hit">
<h4>⚠️ Hit or Miss (have manual fallback ready)</h4>
<ul>
<li>SBA grant pages (heavy JavaScript)</li>
<li>Cloudflare-protected pages (returns blocked HTML)</li>
<li>Sites with cookie walls or "I am human" checks</li>
<li>Long PDFs linked from a page (text not in the HTML)</li>
<li>Grants.gov opportunity detail pages (use the API in Search tab instead)</li>
</ul>
</div>
<div class="scrape-list scrape-bad">
<h4>❌ Won't Work (and the fix)</h4>
<ul>
<li><strong>Submittable, GrantHub, FluidReview</strong> — require login → fix = Chrome Extension (Path C, future)</li>
<li><strong>Anything behind your personal account</strong> → fix = Chrome Extension</li>
<li><strong>PDF-only opportunity announcements</strong> → fix = download PDF, drop into <a href="https://claude.ai" target="_blank" style="color:#c89a3c;">claude.ai</a>, ask for structured extraction, paste back into Notes</li>
<li><strong>State portals with captcha</strong> → fix = manual copy/paste into Notes, AI Draft still works</li>
</ul>
</div>
<div style="background:#1a1410;border:1px solid #c89a3c;padding:18px;margin-top:20px;">
<h3 style="font-size:14px;color:#c89a3c;letter-spacing:2px;text-transform:uppercase;margin-bottom:10px;">When Scraping Fails — The Manual Path Still Works</h3>
<p style="font-size:13px;color:#a89c80;line-height:1.7;">
Even if Ingest URL fails on a site, you can: <strong style="color:#c89a3c;">(1)</strong> open the grant page yourself, <strong style="color:#c89a3c;">(2)</strong> copy the full description text, <strong style="color:#c89a3c;">(3)</strong> click Edit on the grant in your pipeline, <strong style="color:#c89a3c;">(4)</strong> paste the description into Notes, <strong style="color:#c89a3c;">(5)</strong> click Vet Fit and AI Draft — both work using just the Notes field. Scraping is convenience, not a hard requirement.
</p>
</div>
</section>
<!-- 06 INSTRUCTIONS -->
<section id="panel-instructions" class="panel">
<h2 style="font-size:22px;color:#c89a3c;font-weight:400;margin-bottom:20px;">How To Use</h2>
<div id="instructions-list" style="max-width:820px;"></div>
</section>
<!-- ADD GRANT MODAL -->
<div id="add-modal" class="modal-backdrop" onclick="if(event.target===this)closeAddModal()">
<div class="modal">
<h3 id="modal-title">Add Grant</h3>
<div class="form-field"><label class="form-label">Grant Name *</label><input type="text" id="m-name" style="width:100%;"></div>
<div class="form-field"><label class="form-label">Application URL</label><input type="url" id="m-url" style="width:100%;" placeholder="https://..."></div>
<div class="form-field"><label class="form-label">Funder / Agency</label><input type="text" id="m-funder" style="width:100%;"></div>
<div class="form-field"><label class="form-label">Amount</label><input type="text" id="m-amount" style="width:100%;" placeholder="$25,000"></div>
<div class="form-field"><label class="form-label">Deadline</label><input type="text" id="m-deadline" style="width:100%;" placeholder="Dec 15, 2026"></div>
<div class="form-field"><label class="form-label">Notes / Description (paste full grant description here)</label><textarea id="m-notes"></textarea></div>
<div style="display:flex;gap:10px;justify-content:flex-end;">
<button class="btn btn-ghost" onclick="closeAddModal()">Cancel</button>
<button class="btn btn-solid" onclick="saveGrantFromModal()">Save</button>
</div>
</div>
</div>
<div class="footer">The Grant Engine v3 · Joint Venture build · Multi-LLC · Find · Read · Vet · Draft · Submit · Track</div>
<div class="toast" id="toast"></div>
<script>
// ============================================================
// GRANT ENGINE v3 - search, multi-LLC, draft, export
// ============================================================
const QUICK_CATEGORIES = [
{ label: 'Music & Arts', q: 'music arts' },
{ label: 'HVAC / Energy', q: 'energy efficiency HVAC' },
{ label: 'Real Estate / Housing', q: 'housing community development' },
{ label: 'Hispanic-Owned', q: 'minority business hispanic' },
{ label: 'Women-Owned', q: 'women owned business' },
{ label: 'Veteran-Owned', q: 'veteran owned business' },
{ label: 'Small Business', q: 'small business' },
{ label: 'Nonprofit', q: 'nonprofit' },
{ label: 'Tech / Innovation', q: 'technology innovation' },
{ label: 'Education', q: 'education training' },
{ label: 'Solar / Renewable', q: 'solar renewable energy' },
{ label: 'Rural / Texas', q: 'rural texas' },
];
const STATUS_CONFIG = {
found: { label: 'Found', color: '#8b7355' },
drafting: { label: 'Drafting', color: '#c89a3c' },
submitted: { label: 'Submitted', color: '#3a6b8c' },
awarded: { label: 'Awarded', color: '#2d6a4f' },
denied: { label: 'Denied', color: '#9b2226' }
};
const DEFAULT_LLCS = [
{ id: 'hvac', name: 'HVAC Contractor LLC', industry: 'HVAC + Electrical Contracting', renameable: false },
{ id: 'realestate', name: 'Real Estate LLC', industry: 'Real Estate Investment / Development', renameable: false },
{ id: 'marketing', name: 'Marketing Agency LLC', industry: 'Digital Marketing / AI Automation', renameable: false },
{ id: 'flex', name: '[Rename Me] LLC', industry: '', renameable: true }
];
// Section labels for AI Draft output
const DRAFT_SECTIONS = [
'Executive Summary',
'Statement of Need',
'Project Description',
'Goals and Measurable Outcomes',
'Organizational Capacity',
'Budget Narrative',
'Sustainability',
'Evaluation and Reporting'
];
// State
let state = {
api_key: '',
active_llc: 'hvac',
llcs: {},
grants: {}, // grants[llcId] = [grant, grant, ...]
edit_llc: 'hvac', // for the profiles tab editor
editing_grant: null
};
// === Storage ===
function loadState() {
try {
const s = localStorage.getItem('ge3_state');
if (s) {
const loaded = JSON.parse(s);
state = Object.assign(state, loaded);
}
} catch (e) { console.error(e); }
// Ensure all default LLCs exist
DEFAULT_LLCS.forEach(d => {
if (!state.llcs[d.id]) {
state.llcs[d.id] = { name: d.name, industry: d.industry, renameable: d.renameable };
}
if (!state.grants[d.id]) state.grants[d.id] = [];
});
}
function persist() { localStorage.setItem('ge3_state', JSON.stringify(state)); }
// === Utility ===
function escapeHtml(s) { if (s == null) return ''; return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,'''); }
function escapeAttr(s) { return escapeHtml(s); }
function uid() { return 'g_' + Date.now() + '_' + Math.random().toString(36).slice(2, 6); }
function toast(msg, color) {
const el = document.getElementById('toast');
el.textContent = msg;
if (color) el.style.background = color;
el.classList.add('show');
setTimeout(() => el.classList.remove('show'), 2400);
}
function aiEnabled() { return !!(state.api_key && state.api_key.startsWith('sk-ant-')); }
function activeGrants() { return state.grants[state.active_llc] || []; }
function activeLlc() { return state.llcs[state.active_llc]; }
async function callClaude(messages, maxTokens) {
if (!aiEnabled()) throw new Error('Add Anthropic API key in LLC Profiles tab first.');
const res = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': state.api_key,
'anthropic-version': '2023-06-01',
'anthropic-dangerous-direct-browser-access': 'true'
},
body: JSON.stringify({ model: 'claude-sonnet-4-5', max_tokens: maxTokens || 2000, messages })
});
const data = await res.json();
if (data.error) throw new Error(data.error.message || JSON.stringify(data.error));
return data.content.filter(b => b.type === 'text').map(b => b.text).join('\n');
}
function extractJson(text) {
const match = text.match(/\{[\s\S]*\}/);
if (!match) throw new Error('No JSON in AI response');
return JSON.parse(match[0]);
}
function extractGrantsFromHtml(html) {
// Grants.gov React app embeds initial state in a script tag
const hits = [];
// Try to find embedded JSON state
const jsonMatches = html.match(/"oppHits"\s*:\s*(\[[\s\S]*?\])(?=\s*[,}])/);
if (jsonMatches) {
try {
const arr = JSON.parse(jsonMatches[1]);
arr.forEach(o => {
hits.push({
id: o.id || o.oppId || '',
title: o.title || o.opportunityTitle || '',
number: o.number || o.opportunityNumber || '',
agencyName: o.agencyName || o.agency || '',
agency: o.agency || '',
openDate: o.openDate || o.postedDate || '',
closeDate: o.closeDate || o.closingDate || '',
oppStatus: o.oppStatus || o.opportunityStatus || ''
});
});
} catch (e) { /* fall through */ }
}
// Fallback: regex-extract from rendered HTML (works for static-rendered results)
if (hits.length === 0) {
const cardRegex = /<a[^>]+href="\/search-results-detail\/(\d+)"[^>]*>([\s\S]*?)<\/a>/gi;
let m;
while ((m = cardRegex.exec(html)) !== null && hits.length < 25) {
const titleMatch = m[2].match(/>([^<]{10,})</);
hits.push({
id: m[1],
title: (titleMatch ? titleMatch[1] : 'Untitled').trim(),
number: '', agencyName: '', agency: '', openDate: '', closeDate: '', oppStatus: ''
});
}
}
return hits;
}
// === Tabs ===
function switchTab(name) {
document.querySelectorAll('.tab').forEach(t => t.classList.toggle('active', t.dataset.tab === name));
document.querySelectorAll('.panel').forEach(p => p.classList.toggle('active', p.id === 'panel-' + name));
renderAiBanner();
}
document.querySelectorAll('.tab').forEach(t => t.addEventListener('click', () => switchTab(t.dataset.tab)));
// === Active LLC ===
function renderLlcSwitcher() {
const sel = document.getElementById('active-llc-select');
sel.innerHTML = DEFAULT_LLCS.map(d => {
const llc = state.llcs[d.id];
return `<option value="${d.id}" ${state.active_llc===d.id?'selected':''}>${escapeHtml(llc.name)}</option>`;
}).join('');
}
function setActiveLlc(id) {
state.active_llc = id;
persist();
renderPipeline();
renderAiBanner();
toast('Switched to ' + state.llcs[id].name);
}
// === AI Banner ===
function renderAiBanner() {
const host = document.getElementById('ai-banner-host');
const cls = aiEnabled() ? 'enabled' : 'disabled';
const text = aiEnabled()
? `<div><strong>AI ON.</strong> Active LLC: <strong>${escapeHtml(activeLlc().name)}</strong>. Pipeline: ${activeGrants().length} grants.</div>`
: `<div><strong>AI OFF.</strong> Browse & manage manually. Add your Anthropic key in <em>03 · LLC Profiles</em> to enable Ingest / Vet / Draft.</div><button onclick="switchTab('profiles')">Enable AI →</button>`;
host.innerHTML = `<div class="ai-status ${cls}" style="margin:14px 36px 0;">${text}</div>`;
}
// === SEARCH (Grants.gov API) ===
function renderQuickCats() {
document.getElementById('quick-cats').innerHTML = QUICK_CATEGORIES.map(c =>
`<button class="quick-cat" onclick="quickSearch('${escapeAttr(c.q)}')">${escapeHtml(c.label)}</button>`
).join('');
}
function quickSearch(q) {
document.getElementById('search-input').value = q;
doSearch();
}
async function doSearch() {
const keyword = document.getElementById('search-input').value.trim();
if (!keyword) { toast('Enter a search keyword first', '#9b2226'); return; }
const statusEl = document.getElementById('search-status');
const resultsEl = document.getElementById('search-results');
statusEl.innerHTML = '<span class="loading-spinner"></span>Searching grants.gov...';
resultsEl.innerHTML = '';
try {
// Strategy: scrape the public Grants.gov search results page through allorigins
// (proven to work since Ingest uses this proxy successfully)
const searchPageUrl = 'https://www.grants.gov/search-grants?keywords=' + encodeURIComponent(keyword) + '&oppStatuses=forecasted%7Cposted';
const proxyUrl = 'https://api.allorigins.win/get?url=' + encodeURIComponent(searchPageUrl);
const res = await fetch(proxyUrl);
if (!res.ok) throw new Error('Proxy returned ' + res.status);
const wrapped = await res.json();
const html = wrapped.contents || '';
if (html.length < 500) throw new Error('Empty response from Grants.gov');
// Try to extract embedded JSON from the React app's initial state
const hits = extractGrantsFromHtml(html);
if (hits.length === 0) {
// Fallback: provide manual workaround
throw new Error('No structured results parsed. Try: open ' + searchPageUrl + ' in a new tab, copy a grant URL, paste it into Pipeline > Quick Add.');
}
statusEl.textContent = hits.length + ' opportunities (forecasted + posted) match "' + keyword + '"';
if (hits.length === 0) {
resultsEl.innerHTML = '<div class="empty-state">No results. Try broader keywords.</div>';
return;
}
resultsEl.innerHTML = hits.map(h => {
const detailUrl = 'https://www.grants.gov/search-results-detail/' + encodeURIComponent(h.id);
const status = (h.oppStatus || '').toLowerCase();
const statusColor = status === 'posted' ? '#2d6a4f' : '#c89a3c';
return `
<div class="result-card">
<div class="result-title">${escapeHtml(h.title || 'Untitled')}</div>
<div class="result-meta">
<span><strong>Agency:</strong> ${escapeHtml(h.agencyName || h.agency || '-')}</span>
${h.openDate ? `<span><strong>Opens:</strong> ${escapeHtml(h.openDate)}</span>` : ''}
${h.closeDate ? `<span><strong>Closes:</strong> ${escapeHtml(h.closeDate)}</span>` : ''}
<span style="color:${statusColor};font-weight:700;text-transform:uppercase;">${escapeHtml(h.oppStatus || '')}</span>
</div>
<div class="result-desc">Opportunity Number: <strong style="color:#c89a3c;">${escapeHtml(h.number || '-')}</strong></div>
<div class="result-actions">
<a class="btn btn-gold" href="${escapeAttr(detailUrl)}" target="_blank" rel="noopener">↗ View on Grants.gov</a>
<button class="btn btn-solid" onclick='addFromSearchResult(${JSON.stringify(h).replace(/'/g, "'")})'>+ Add to ${escapeHtml(activeLlc().name)}</button>
</div>
</div>
`;
}).join('');
} catch (e) {
statusEl.textContent = '';
const fallbackUrl = 'https://www.grants.gov/search-grants?keywords=' + encodeURIComponent(keyword);
resultsEl.innerHTML = `<div class="empty-state">
<div style="color:#c89a3c;font-size:14px;margin-bottom:14px;">Live search hit an error: ${escapeHtml(e.message)}</div>
<div style="font-size:13px;color:#a89c80;line-height:1.7;margin-bottom:18px;">No problem — use the manual workflow below. Always works.</div>
<a class="btn btn-solid" href="${escapeAttr(fallbackUrl)}" target="_blank" rel="noopener" style="display:inline-block;margin-bottom:14px;padding:12px 20px;">↗ Open Grants.gov Search for "${escapeHtml(keyword)}"</a>
<div style="font-size:12px;color:#8b7355;line-height:1.7;text-align:left;max-width:520px;margin:14px auto 0;">
<strong style="color:#c89a3c;">Manual workflow (works for any grant site):</strong><br>
1. Click the button above (opens Grants.gov in a new tab)<br>
2. Browse the results, click into any grant you like<br>
3. Copy the URL from your browser's address bar<br>
4. Come back here → tab 02 · Pipeline → paste URL into "Quick Add by URL"<br>
5. With AI enabled, click Ingest to auto-extract details
</div>
</div>`;
}
}
function addFromSearchResult(h) {
const grants = state.grants[state.active_llc] = state.grants[state.active_llc] || [];
if (grants.some(g => g.gov_id === h.id)) { toast('Already in this LLC pipeline', '#c89a3c'); return; }
grants.unshift({
id: uid(),
gov_id: h.id,
name: h.title || 'Untitled',
apply_url: 'https://www.grants.gov/search-results-detail/' + encodeURIComponent(h.id),
funder: h.agencyName || h.agency || '',
amount: '',
deadline: h.closeDate || '',
status: 'found',
notes: 'Opportunity Number: ' + (h.number || '-'),
ingested: null,
vetting: null,
fit_score: null,
draft: '',
added_at: new Date().toISOString(),
updated_at: new Date().toISOString()
});
persist();
toast('Added to ' + activeLlc().name);
switchTab('pipeline');
renderPipeline();
}
// === Pipeline ===
function renderPipeline() {
document.getElementById('pipeline-llc-name').textContent = activeLlc().name;
const list = document.getElementById('pipeline-list');
const grants = activeGrants();
document.getElementById('pipeline-summary').textContent =
grants.length + ' total · ' + grants.filter(g => ['found','drafting','submitted'].includes(g.status)).length + ' active';
if (grants.length === 0) {
list.innerHTML = `<div class="empty-state">
<div style="font-size:15px;color:#a89c80;margin-bottom:8px;">No grants in this LLC's pipeline yet.</div>
<div style="font-size:12px;">Go to Search tab to find federal grants, or use Quick Add above with any URL.</div>
</div>`;
return;
}
list.innerHTML = grants.map(g => {
const cfg = STATUS_CONFIG[g.status];
const v = g.vetting || {};
return `
<div class="grant-card" style="border-color:${cfg.color}60;border-left-color:${cfg.color};">
<div style="display:flex;justify-content:space-between;align-items:flex-start;flex-wrap:wrap;gap:10px;">
<div style="flex:1;min-width:220px;">
<div style="display:flex;gap:6px;flex-wrap:wrap;align-items:center;margin-bottom:6px;">
<span class="pill pill-status" style="border-color:${cfg.color};color:${cfg.color};">${cfg.label}</span>
${g.fit_score != null ? `<span class="pill" style="background:${g.fit_score>=70?'#2d6a4f':g.fit_score>=40?'#c89a3c':'#9b2226'};color:#fff;">Fit ${g.fit_score}/100</span>` : ''}
${v.recommendation ? `<span class="pill" style="border:1px solid #c89a3c;color:#c89a3c;">${escapeHtml(v.recommendation)}</span>` : ''}
</div>
<div style="font-size:17px;font-weight:600;line-height:1.3;">${escapeHtml(g.name)}</div>
<div style="display:flex;gap:12px;flex-wrap:wrap;font-size:11px;color:#8b7355;margin-top:5px;">
${g.funder ? `<span><strong style="color:#c89a3c;">Funder:</strong> ${escapeHtml(g.funder)}</span>` : ''}
${g.amount || g.ingested?.award_amount ? `<span><strong style="color:#c89a3c;">Amount:</strong> ${escapeHtml(g.amount || g.ingested.award_amount)}</span>` : ''}
${g.deadline ? `<span><strong style="color:#c89a3c;">Deadline:</strong> ${escapeHtml(g.deadline)}</span>` : ''}
</div>
</div>
<div style="display:flex;gap:5px;flex-wrap:wrap;align-items:center;">
<select onchange="updateGrant('${g.id}', { status: this.value })" style="padding:6px;font-size:11px;">
${Object.entries(STATUS_CONFIG).map(([k,c])=>`<option value="${k}" ${g.status===k?'selected':''}>${c.label}</option>`).join('')}
</select>
${g.apply_url ? `<a class="btn btn-solid" href="${escapeAttr(g.apply_url)}" target="_blank" rel="noopener" style="padding:6px 10px;font-size:10px;">Apply ↗</a>` : ''}
<button class="btn btn-gold" onclick="ingestGrant('${g.id}')" id="ingest-${g.id}" style="padding:6px 10px;font-size:10px;">↓ Ingest</button>
<button class="btn btn-gold" onclick="vetGrant('${g.id}')" id="vet-${g.id}" style="padding:6px 10px;font-size:10px;">⊕ Vet</button>
<button class="btn btn-gold" onclick="draftGrant('${g.id}')" id="draft-${g.id}" style="padding:6px 10px;font-size:10px;">✎ Draft</button>
<button class="btn btn-ghost" onclick="editGrant('${g.id}')" style="padding:6px 9px;font-size:10px;">Edit</button>
<button class="btn btn-danger" onclick="if(confirm('Delete?'))deleteGrant('${g.id}')">🗑</button>
</div>
</div>
${g.notes ? `<div style="font-size:12px;color:#a89c80;padding:10px;background:#0a0a0a;border-left:2px solid #3a3024;margin-top:8px;">${escapeHtml(g.notes).replace(/\n/g,'<br>')}</div>` : ''}
${g.ingested ? `
<div style="margin-top:10px;padding:12px;background:#0a0a0a;border:1px solid #2a2520;">
<div style="font-size:10px;letter-spacing:2px;color:#8b7355;text-transform:uppercase;margin-bottom:6px;">Ingested Summary</div>
<div style="font-size:12px;color:#a89c80;line-height:1.6;">${escapeHtml(g.ingested.summary||'')}</div>
${g.ingested.eligibility_summary ? `<div style="font-size:11px;color:#8b7355;margin-top:6px;"><strong style="color:#c89a3c;">Eligibility:</strong> ${escapeHtml(g.ingested.eligibility_summary)}</div>` : ''}
</div>` : ''}
${g.vetting ? `
<div style="margin-top:10px;padding:14px;background:#0a0a0a;border:1px solid #c89a3c40;">
<div style="font-size:10px;letter-spacing:2px;color:#c89a3c;text-transform:uppercase;margin-bottom:6px;">Vetting Report</div>
<div style="font-size:12px;color:#e8e2d4;line-height:1.6;margin-bottom:8px;">${escapeHtml(v.rationale||'')}</div>
<div class="vetting-grid">
<div><div style="font-size:10px;color:#2d6a4f;letter-spacing:1px;text-transform:uppercase;margin-bottom:4px;">Advantages</div><ul style="padding-left:14px;font-size:11px;color:#a89c80;line-height:1.6;">${(v.advantages||[]).map(x=>`<li>${escapeHtml(x)}</li>`).join('')}</ul></div>
<div><div style="font-size:10px;color:#9b2226;letter-spacing:1px;text-transform:uppercase;margin-bottom:4px;">Gaps</div><ul style="padding-left:14px;font-size:11px;color:#a89c80;line-height:1.6;">${(v.gaps||[]).map(x=>`<li>${escapeHtml(x)}</li>`).join('')}</ul></div>
</div>
${v.win_themes && v.win_themes.length ? `<div style="margin-top:10px;"><div style="font-size:10px;color:#c89a3c;letter-spacing:1px;text-transform:uppercase;margin-bottom:4px;">Win Themes</div><ul style="padding-left:14px;font-size:11px;color:#a89c80;line-height:1.6;">${v.win_themes.map(x=>`<li>${escapeHtml(x)}</li>`).join('')}</ul></div>` : ''}
${v.effort_estimate ? `<div style="margin-top:8px;font-size:10px;color:#8b7355;">Effort: <strong style="color:#c89a3c;">${escapeHtml(v.effort_estimate)}</strong> — ${escapeHtml(v.effort_reasoning||'')}</div>` : ''}
${v.llc_recommendation ? `<div style="margin-top:8px;font-size:11px;color:#a89c80;"><strong style="color:#c89a3c;">LLC Recommendation:</strong> ${escapeHtml(v.llc_recommendation)}</div>` : ''}
${v.website_strategy ? `<div style="margin-top:6px;font-size:11px;color:#a89c80;"><strong style="color:#c89a3c;">Website Strategy:</strong> ${escapeHtml(v.website_strategy)}</div>` : ''}
</div>` : ''}
${g.draft ? renderDraftSections(g) : ''}
</div>`;
}).join('');
}
function renderDraftSections(g) {
// g.draft is the full text; we split by section markers
const sections = splitDraft(g.draft);
return `
<div style="margin-top:12px;padding:14px;background:#0a0a0a;border:1px solid #c89a3c40;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;flex-wrap:wrap;gap:8px;">
<div style="font-size:11px;letter-spacing:2px;color:#c89a3c;text-transform:uppercase;font-weight:700;">Drafted Application — ${sections.length} sections</div>
<div style="display:flex;gap:6px;flex-wrap:wrap;">
<button class="btn btn-gold" onclick="copyDraftFull('${g.id}')" style="padding:5px 10px;font-size:10px;">📋 Copy All</button>
<button class="btn btn-gold" onclick="downloadGrantDoc('${g.id}')" style="padding:5px 10px;font-size:10px;">📥 Download .md</button>
</div>
</div>
${sections.map((s, i) => `
<div class="draft-section">
<div class="draft-section-header">
<div class="draft-section-title">${i+1}. ${escapeHtml(s.title)}</div>
<button class="btn btn-gold" onclick="copyText(this, ${JSON.stringify(s.body).replace(/'/g,"'")})" style="padding:4px 9px;font-size:9px;">📋 Copy Section</button>
</div>
<div class="draft-section-body">${escapeHtml(s.body)}</div>
</div>
`).join('')}
</div>`;
}
function splitDraft(text) {
// Split on numbered headers like "1. EXECUTIVE SUMMARY" or "## Executive Summary"
const sections = [];
const lines = text.split('\n');
let current = null;
for (const line of lines) {
// Detect section header
const m1 = line.match(/^\s*(?:\d+\.|##|#)\s*([A-Z][A-Z\s&/]+?)\s*$/);
const m2 = line.match(/^\s*(?:\d+\.|##|#)\s*\*\*([^*]+)\*\*\s*$/);
const m3 = line.match(/^\s*(\d+)\.\s+([A-Z][A-Za-z\s&/]+)\s*$/);
const match = m1 || m2 || m3;
if (match) {
if (current) sections.push(current);
const title = (m3 ? match[2] : match[1]).trim();
current = { title, body: '' };
} else if (current) {
current.body += line + '\n';
} else {
// text before first header — start a "Preamble" section
current = { title: 'Preamble', body: line + '\n' };
}
}
if (current) sections.push(current);
// Trim each body
sections.forEach(s => s.body = s.body.trim());
return sections.filter(s => s.body || s.title !== 'Preamble');
}
function copyText(btn, text) {
navigator.clipboard.writeText(text).then(() => {
const original = btn.textContent;
btn.textContent = '✓ Copied';
setTimeout(() => btn.textContent = original, 1400);
});
}
function copyDraftFull(id) {
const g = allGrants().find(x => x.id === id);
if (!g) return;
navigator.clipboard.writeText(g.draft).then(() => toast('Full draft copied'));
}
function downloadGrantDoc(id) {
const g = allGrants().find(x => x.id === id);
if (!g) return;
const llc = activeLlc();
const md = `# Grant Application Draft\n\n**LLC:** ${llc.name}\n**Grant:** ${g.name}\n**Funder:** ${g.funder || '-'}\n**Amount:** ${g.amount || g.ingested?.award_amount || '-'}\n**Deadline:** ${g.deadline || '-'}\n**Apply URL:** ${g.apply_url || '-'}\n**Status:** ${STATUS_CONFIG[g.status].label}\n${g.fit_score != null ? `**Fit Score:** ${g.fit_score}/100\n` : ''}${g.vetting?.recommendation ? `**Recommendation:** ${g.vetting.recommendation}\n` : ''}\n---\n\n${g.draft}\n`;
downloadFile(`${sanitize(llc.name)}-${sanitize(g.name).slice(0,40)}.md`, md, 'text/markdown');
}
function updateGrant(id, patch) {
const grants = activeGrants();
const g = grants.find(x => x.id === id);
if (!g) return;
Object.assign(g, patch, { updated_at: new Date().toISOString() });
persist();
renderPipeline();
}
function deleteGrant(id) {
state.grants[state.active_llc] = activeGrants().filter(g => g.id !== id);
persist();
renderPipeline();
}
function allGrants() {
return Object.values(state.grants).flat();
}
// === Add/Edit Modal ===
function openAddModal() {
state.editing_grant = null;
document.getElementById('modal-title').textContent = 'Add Grant to ' + activeLlc().name;
['m-name','m-url','m-funder','m-amount','m-deadline','m-notes'].forEach(id => document.getElementById(id).value = '');
document.getElementById('add-modal').classList.add('active');
}
function editGrant(id) {
const g = activeGrants().find(x => x.id === id);
if (!g) return;
state.editing_grant = id;
document.getElementById('modal-title').textContent = 'Edit Grant';
document.getElementById('m-name').value = g.name || '';
document.getElementById('m-url').value = g.apply_url || '';
document.getElementById('m-funder').value = g.funder || '';
document.getElementById('m-amount').value = g.amount || '';
document.getElementById('m-deadline').value = g.deadline || '';
document.getElementById('m-notes').value = g.notes || '';
document.getElementById('add-modal').classList.add('active');
}
function closeAddModal() {
document.getElementById('add-modal').classList.remove('active');
state.editing_grant = null;
}
function saveGrantFromModal() {
const name = document.getElementById('m-name').value.trim();
if (!name) { toast('Grant name required', '#9b2226'); return; }
const data = {
name,
apply_url: document.getElementById('m-url').value.trim(),
funder: document.getElementById('m-funder').value.trim(),
amount: document.getElementById('m-amount').value.trim(),
deadline: document.getElementById('m-deadline').value.trim(),
notes: document.getElementById('m-notes').value.trim(),
updated_at: new Date().toISOString()
};
if (state.editing_grant) {
updateGrant(state.editing_grant, data);
} else {
state.grants[state.active_llc].unshift({
id: uid(),
...data,
status: 'found',
ingested: null, vetting: null, fit_score: null, draft: '',
added_at: new Date().toISOString()
});
persist();
renderPipeline();
}
closeAddModal();
}
function addFromUrl() {
const url = document.getElementById('quick-url').value.trim();
const name = document.getElementById('quick-name').value.trim();
if (!url) { toast('Paste a URL first', '#9b2226'); return; }
const grant = {
id: uid(),
name: name || 'Untitled (will be filled by Ingest)',
apply_url: url,
funder: '', amount: '', deadline: '', status: 'found', notes: '',
ingested: null, vetting: null, fit_score: null, draft: '',
added_at: new Date().toISOString(),
updated_at: new Date().toISOString()
};
state.grants[state.active_llc].unshift(grant);
persist();
document.getElementById('quick-url').value = '';
document.getElementById('quick-name').value = '';
renderPipeline();
if (aiEnabled()) ingestGrant(grant.id);
else toast('Added (enable AI to auto-ingest)');
}
// === AI: Ingest / Vet / Draft ===
async function ingestGrant(id) {
if (!aiEnabled()) { toast('Enable AI first (Profiles tab)', '#9b2226'); switchTab('profiles'); return; }
const g = activeGrants().find(x => x.id === id);
if (!g || !g.apply_url) { toast('No URL — click Edit to add one', '#9b2226'); return; }
const btn = document.getElementById('ingest-' + id);
btn.innerHTML = '<span class="loading-spinner"></span>';
btn.disabled = true;
try {
const proxyUrl = 'https://api.allorigins.win/get?url=' + encodeURIComponent(g.apply_url);
const fetchRes = await fetch(proxyUrl);
const fetchData = await fetchRes.json();
const html = fetchData.contents || '';
const text = html.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '').replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '').replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').slice(0, 40000);
if (text.length < 200) throw new Error('Page content too short — likely blocked. See Scraping Truth tab.');
const response = await callClaude([{
role: 'user',
content: `Extract structured grant data. Return ONLY JSON, no markdown fences:
{
"title": "string",
"funder": "string",
"award_amount": "string or 'not specified'",
"deadline": "string or 'Rolling' or 'not specified'",
"eligibility_summary": "1-2 sentences",
"eligibility_requirements": ["array"],
"use_of_funds": "string",
"match_required": "string or 'none'",
"application_components": ["array"],
"evaluation_criteria": ["array"],
"summary": "2-3 sentences"
}
Page text:
${text}`
}], 2000);
const parsed = extractJson(response);
g.ingested = parsed;
if (parsed.title && g.name.startsWith('Untitled')) g.name = parsed.title;
if (parsed.funder && !g.funder) g.funder = parsed.funder;
if (parsed.award_amount && !g.amount && parsed.award_amount !== 'not specified') g.amount = parsed.award_amount;
if (parsed.deadline && !g.deadline && parsed.deadline !== 'not specified') g.deadline = parsed.deadline;
g.updated_at = new Date().toISOString();
persist();
renderPipeline();
toast('Ingested ✓');
} catch (e) {
toast('Ingest failed: ' + e.message, '#9b2226');
btn.innerHTML = '↓ Ingest';
btn.disabled = false;
}
}
async function vetGrant(id) {
if (!aiEnabled()) { toast('Enable AI first', '#9b2226'); switchTab('profiles'); return; }
const g = activeGrants().find(x => x.id === id);
if (!g) return;
const llc = activeLlc();
if (!llc.org_name) { toast('Fill in this LLC profile first', '#9b2226'); switchTab('profiles'); return; }
const btn = document.getElementById('vet-' + id);
btn.innerHTML = '<span class="loading-spinner"></span>';
btn.disabled = true;
try {
const ing = g.ingested || {};
const prompt = `Vet whether this LLC should pursue this grant. Be specific, quantitative, honest. Return ONLY JSON:
LLC: ${llc.name}
Legal/DBA: ${llc.org_name || llc.name}
Industry: ${llc.industry || 'not specified'}
Years in business: ${llc.years || 'not specified'}
Location: ${llc.location || 'not specified'}
Certifications: ${llc.certs || 'none'}
Demographic designations: ${llc.demo || 'none'}
Strengths: ${llc.strengths || ''}
Past projects: ${llc.projects || ''}
GRANT:
Title: ${g.name}
Funder: ${ing.funder || g.funder || ''}
Award: ${ing.award_amount || g.amount || 'not specified'}
Deadline: ${ing.deadline || g.deadline || ''}
Eligibility: ${ing.eligibility_summary || g.notes || ''}
Requirements: ${(ing.eligibility_requirements||[]).join('; ')}
Use of funds: ${ing.use_of_funds || ''}
Match: ${ing.match_required || 'none'}
Evaluation criteria: ${(ing.evaluation_criteria||[]).join('; ')}
Return ONLY this JSON:
{
"fit_score": 0-100,
"recommendation": "PURSUE | SKIP | PARK",
"advantages": ["3-6 specific matches"],
"gaps": ["1-5 specific gaps or risks"],
"effort_estimate": "LOW | MEDIUM | HIGH",
"effort_reasoning": "1 sentence",
"win_themes": ["3-5 angles"],
"rationale": "2-3 sentences",
"llc_recommendation": "Should this LLC pursue, or would a different/new LLC structure be better? 1-2 sentences.",
"website_strategy": "What website/digital presence elements would strengthen this app? 1-2 sentences."
}`;
const text = await callClaude([{ role: 'user', content: prompt }], 2000);
const parsed = extractJson(text);
g.vetting = parsed;
g.fit_score = parsed.fit_score;
g.updated_at = new Date().toISOString();
persist();
renderPipeline();
toast('Vetted ✓');
} catch (e) {
toast('Vet failed: ' + e.message, '#9b2226');
btn.innerHTML = '⊕ Vet';
btn.disabled = false;
}
}
async function draftGrant(id) {
if (!aiEnabled()) { toast('Enable AI first', '#9b2226'); switchTab('profiles'); return; }
const g = activeGrants().find(x => x.id === id);
if (!g) return;
const llc = activeLlc();
if (!llc.org_name) { toast('Fill in this LLC profile first', '#9b2226'); switchTab('profiles'); return; }
const btn = document.getElementById('draft-' + id);
btn.innerHTML = '<span class="loading-spinner"></span>';
btn.disabled = true;
try {
const ing = g.ingested || {};
const v = g.vetting || {};
const prompt = `Write a complete fundable grant application narrative tailored to THIS grant and THIS LLC.
LLC:
Name: ${llc.name}
Legal/DBA: ${llc.org_name || llc.name}
Industry: ${llc.industry || ''}
Years in business: ${llc.years || 'newly founded'}
Location: ${llc.location || 'United States'}
Certifications: ${llc.certs || 'none'}