Skip to content

Commit c549f73

Browse files
authored
Integrating DFM landing page into the site (#318)
1 parent 7905df7 commit c549f73

34 files changed

+5727
-13
lines changed

.github/workflows/documentation.yml

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# creates the documentation on pushes it to the gh-pages branch
1+
# creates the documentation and pushes it to the gh-pages branch
22
name: Documentation
33

44
on:
@@ -7,7 +7,6 @@ on:
77
push:
88
branches: [main]
99

10-
1110
permissions:
1211
contents: write
1312

@@ -26,14 +25,24 @@ jobs:
2625
- uses: actions/setup-python@v5
2726
with:
2827
python-version: '3.10'
29-
28+
3029
- name: Dependencies
3130
run: uv sync
3231

33-
- name: Build and Deploy
32+
- name: Build MkDocs
33+
run: make build-docs
34+
35+
- name: Prepare landing page
3436
if: github.event_name == 'push'
35-
run: uv run mkdocs gh-deploy --force
36-
37-
- name: Build
38-
if: github.event_name == 'pull_request'
39-
run: make build-docs
37+
run: |
38+
mkdir -p site
39+
touch site/.nojekyll
40+
cp -rv landing/* site/
41+
42+
- name: Deploy to GitHub Pages
43+
if: github.event_name == 'push'
44+
uses: peaceiris/actions-gh-pages@v3
45+
with:
46+
github_token: ${{ secrets.GITHUB_TOKEN }}
47+
publish_dir: ./site
48+
publish_branch: gh-pages

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,3 +162,6 @@ outputs/*
162162
# training artifacts
163163
logs/
164164
separate-logs/
165+
166+
# tailwindcss
167+
scripts/tailwindcss

.nojekyll

Whitespace-only changes.

backend/app.py

Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
1+
from datetime import UTC, datetime, timedelta
2+
from flask import Flask, request, jsonify, send_from_directory
3+
from flask_cors import CORS
4+
from flask_limiter import Limiter
5+
from flask_limiter.util import get_remote_address
6+
from flask_mail import Mail, Message
7+
import json
8+
import os
9+
import uuid
10+
11+
app = Flask(__name__, static_folder='../landing', static_url_path='')
12+
app.config['NEWS_FILE'] = os.getenv('NEWS_FILE', 'data/news.jsonl')
13+
app.config['ADMIN_PASSWORD'] = os.getenv('ADMIN_PASSWORD', 'changeme')
14+
app.config['CONTACT_FILE'] = os.getenv('CONTACT_FILE', 'data/contact.jsonl')
15+
app.config['NEWSLETTER_FILE'] = os.getenv('NEWSLETTER_FILE', 'data/newsletter.jsonl')
16+
app.config['ROADMAP_FILE'] = os.getenv('ROADMAP_FILE', 'data/roadmap.jsonl')
17+
app.config['MAIL_SERVER'] = os.getenv('MAIL_SERVER', 'smtp.gmail.com')
18+
app.config['MAIL_PORT'] = int(os.getenv('MAIL_PORT', 587))
19+
app.config['MAIL_USE_TLS'] = True
20+
app.config['MAIL_USERNAME'] = os.getenv('MAIL_USERNAME') # Your email address
21+
app.config['MAIL_PASSWORD'] = os.getenv('MAIL_PASSWORD') # Your email password or app password
22+
app.config['MAIL_DEFAULT_SENDER'] = os.getenv('MAIL_DEFAULT_SENDER', app.config['MAIL_USERNAME'])
23+
app.config['MAIL_RECIPIENT'] = os.getenv('MAIL_RECIPIENT', app.config['MAIL_USERNAME'])
24+
limiter = Limiter(app=app, key_func=get_remote_address)
25+
mail = Mail(app)
26+
CORS(app)
27+
28+
TOKENS = {}
29+
30+
def load_news():
31+
if not os.path.exists(app.config['NEWS_FILE']):
32+
return []
33+
with open(app.config['NEWS_FILE']) as f:
34+
return [json.loads(line) for line in f]
35+
36+
def save_news(item):
37+
with open(app.config['NEWS_FILE'], 'a') as f:
38+
f.write(json.dumps(item) + '\n')
39+
40+
@app.route('/api/news', methods=['GET'])
41+
def get_news():
42+
return jsonify(load_news())
43+
44+
@app.route('/api/news', methods=['POST'])
45+
def post_news():
46+
auth = request.headers.get('Authorization', '')
47+
token = auth.replace('Bearer ', '')
48+
token_expiry = TOKENS.get(token, None)
49+
if not token_expiry or datetime.now(UTC) > token_expiry:
50+
TOKENS.pop(token, None)
51+
return jsonify({'error': 'Unauthorized'}), 401
52+
data = request.json
53+
item = {
54+
'id': str(uuid.uuid4()),
55+
'title': data.get('title', '').strip(),
56+
'date': data.get('date', '').strip(),
57+
'content': data.get('content', '').strip()
58+
}
59+
if not item['title'] or not item['date'] or not item['content']:
60+
return jsonify({'error': 'Missing fields'}), 400
61+
save_news(item)
62+
return jsonify(item), 201
63+
64+
@app.route('/api/news/<news_id>', methods=['DELETE'])
65+
def delete_news(news_id):
66+
auth = request.headers.get('Authorization', '')
67+
token = auth.replace('Bearer ', '')
68+
token_expiry = TOKENS.get(token, None)
69+
if not token_expiry or datetime.now(UTC) > token_expiry:
70+
TOKENS.pop(token, None)
71+
return jsonify({'error': 'Unauthorized'}), 401
72+
news_items = load_news()
73+
updated_news = [item for item in news_items if item.get('id') != news_id]
74+
if len(news_items) == len(updated_news):
75+
return jsonify({'error': 'News item not found'}), 404
76+
with open(app.config['NEWS_FILE'], 'w') as f:
77+
for item in updated_news:
78+
f.write(json.dumps(item) + '\n')
79+
return jsonify({'success': True})
80+
81+
@app.route('/api/news/<news_id>', methods=['PUT'])
82+
def update_news(news_id):
83+
auth = request.headers.get('Authorization', '')
84+
token = auth.replace('Bearer ', '')
85+
token_expiry = TOKENS.get(token, None)
86+
if not token_expiry or datetime.now(UTC) > token_expiry:
87+
TOKENS.pop(token, None)
88+
return jsonify({'error': 'Unauthorized'}), 401
89+
data = request.json
90+
title = data.get('title', '').strip()
91+
date = data.get('date', '').strip()
92+
content = data.get('content', '').strip()
93+
if not title or not date or not content:
94+
return jsonify({'error': 'Missing fields'}), 400
95+
news_items = load_news()
96+
updated = False
97+
for item in news_items:
98+
if item.get('id') == news_id:
99+
item['title'] = title
100+
item['date'] = date
101+
item['content'] = content
102+
updated = True
103+
break
104+
if not updated:
105+
return jsonify({'error': 'News item not found'}), 404
106+
with open(app.config['NEWS_FILE'], 'w') as f:
107+
for item in news_items:
108+
f.write(json.dumps(item) + '\n')
109+
return jsonify({'success': True})
110+
111+
def load_roadmap():
112+
if not os.path.exists(app.config['ROADMAP_FILE']):
113+
return []
114+
with open(app.config['ROADMAP_FILE']) as f:
115+
return [json.loads(line) for line in f]
116+
117+
def save_roadmap(items):
118+
with open(app.config['ROADMAP_FILE'], 'w') as f:
119+
for item in items:
120+
f.write(json.dumps(item) + '\n')
121+
122+
@app.route('/api/roadmap', methods=['GET'])
123+
def get_roadmap():
124+
return jsonify(load_roadmap())
125+
126+
@app.route('/api/roadmap', methods=['POST'])
127+
def post_roadmap():
128+
auth = request.headers.get('Authorization', '')
129+
token = auth.replace('Bearer ', '')
130+
token_expiry = TOKENS.get(token)
131+
if not token_expiry or datetime.now(UTC) > token_expiry:
132+
TOKENS.pop(token, None)
133+
return jsonify({'error': 'Unauthorized'}), 401
134+
135+
data = request.json
136+
item = {
137+
'id': str(uuid.uuid4()),
138+
'title': data.get('title', '').strip(),
139+
'quarter': data.get('quarter', '').strip(),
140+
'description': data.get('description', '').strip(),
141+
'status': data.get('status', '').strip(),
142+
}
143+
if not item['title'] or not item['quarter'] or not item['description']:
144+
return jsonify({'error': 'Missing required fields'}), 400
145+
146+
items = load_roadmap()
147+
items.append(item)
148+
save_roadmap(items)
149+
return jsonify(item), 201
150+
151+
@app.route('/api/roadmap/<roadmap_id>', methods=['PUT'])
152+
def update_roadmap(roadmap_id):
153+
auth = request.headers.get('Authorization', '')
154+
token = auth.replace('Bearer ', '')
155+
token_expiry = TOKENS.get(token)
156+
if not token_expiry or datetime.now(UTC) > token_expiry:
157+
TOKENS.pop(token, None)
158+
return jsonify({'error': 'Unauthorized'}), 401
159+
160+
data = request.json
161+
items = load_roadmap()
162+
updated = False
163+
164+
for item in items:
165+
if item.get('id') == roadmap_id:
166+
item['title'] = data.get('title', '').strip()
167+
item['quarter'] = data.get('quarter', '').strip()
168+
item['description'] = data.get('description', '').strip()
169+
item['status'] = data.get('status', '').strip()
170+
updated = True
171+
break
172+
173+
if not updated:
174+
return jsonify({'error': 'Roadmap item not found'}), 404
175+
176+
save_roadmap(items)
177+
return jsonify({'success': True})
178+
179+
@app.route('/api/roadmap/<roadmap_id>', methods=['DELETE'])
180+
def delete_roadmap(roadmap_id):
181+
auth = request.headers.get('Authorization', '')
182+
token = auth.replace('Bearer ', '')
183+
token_expiry = TOKENS.get(token)
184+
if not token_expiry or datetime.now(UTC) > token_expiry:
185+
TOKENS.pop(token, None)
186+
return jsonify({'error': 'Unauthorized'}), 401
187+
188+
items = load_roadmap()
189+
updated_items = [item for item in items if item.get('id') != roadmap_id]
190+
191+
if len(updated_items) == len(items):
192+
return jsonify({'error': 'Roadmap item not found'}), 404
193+
194+
save_roadmap(updated_items)
195+
return jsonify({'success': True})
196+
197+
@app.route('/api/roadmap/order', methods=['POST'])
198+
def reorder_roadmap():
199+
auth = request.headers.get('Authorization', '')
200+
token = auth.replace('Bearer ', '')
201+
if not TOKENS.get(token) or datetime.now(UTC) > TOKENS[token]:
202+
TOKENS.pop(token, None)
203+
return jsonify({'error': 'Unauthorized'}), 401
204+
205+
data = request.json
206+
new_order = data.get('order', [])
207+
if not isinstance(new_order, list):
208+
return jsonify({'error': 'Invalid format'}), 400
209+
210+
current_items = load_roadmap()
211+
id_map = {item['id']: item for item in current_items}
212+
reordered = [id_map[i] for i in new_order if i in id_map]
213+
214+
# Keep unlisted items (optional)
215+
extras = [item for item in current_items if item['id'] not in new_order]
216+
save_roadmap(reordered + extras)
217+
218+
return jsonify({'success': True})
219+
220+
@app.route('/api/login', methods=['POST'])
221+
@limiter.limit("5 per minute")
222+
def login():
223+
data = request.json
224+
password = data.get('password')
225+
if password == app.config['ADMIN_PASSWORD']:
226+
token = str(uuid.uuid4())
227+
TOKENS[token] = datetime.now(UTC) + timedelta(hours=1)
228+
return jsonify({'token': token})
229+
return jsonify({'error': 'Invalid password'}), 403
230+
231+
@app.route('/api/contact', methods=['POST'])
232+
def contact():
233+
data = request.json
234+
name = data.get('name', '').strip()
235+
email = data.get('email', '').strip()
236+
subject = data.get('subject', '').strip()
237+
message = data.get('message', '').strip()
238+
if not name or not email or not subject or not message:
239+
return jsonify({'error': 'All fields are required.'}), 400
240+
timestamp = datetime.now(UTC).isoformat()
241+
entry = {
242+
"name": name,
243+
"email": email,
244+
"subject": subject,
245+
"message": message,
246+
"timestamp": timestamp
247+
}
248+
with open(app.config['CONTACT_FILE'], 'a') as f:
249+
f.write(json.dumps(entry) + '\n')
250+
try:
251+
msg = Message(subject=f"[DFM] Contact: {subject}",
252+
sender=app.config['MAIL_DEFAULT_SENDER'],
253+
recipients=[app.config['MAIL_RECIPIENT']])
254+
msg.body = f"""
255+
New contact form submission:
256+
257+
Name: {name}
258+
Email: {email}
259+
Subject: {subject}
260+
Message:
261+
{message}
262+
263+
Timestamp: {timestamp}
264+
"""
265+
mail.send(msg)
266+
except Exception as e:
267+
return jsonify({'error': f'Failed to send email: {str(e)}'}), 500
268+
return jsonify({'success': True, 'message': 'Message received and email sent.'}), 200
269+
270+
@app.route('/api/newsletter', methods=['POST'])
271+
def newsletter():
272+
data = request.json
273+
email = data.get('email', '').strip()
274+
consent = data.get('consent', False)
275+
timestamp = datetime.now(UTC).isoformat()
276+
if not email:
277+
return jsonify({'error': 'Email is required.'}), 400
278+
if not consent:
279+
return jsonify({'error': 'You must provide GDPR consent to subscribe.'}), 400
280+
entry = {
281+
"email": email,
282+
"consent": True,
283+
"timestamp": timestamp
284+
}
285+
with open(app.config['NEWSLETTER_FILE'], 'a') as f:
286+
f.write(json.dumps(entry) + '\n')
287+
try:
288+
msg = Message(subject=f"[DFM] Newsletter: signup {email}",
289+
sender=app.config['MAIL_DEFAULT_SENDER'],
290+
recipients=[app.config['MAIL_RECIPIENT']])
291+
msg.body = f"""
292+
New newsletter signup:
293+
Email: {email}
294+
Timestamp: {timestamp}
295+
GDPR Consent: Yes
296+
"""
297+
mail.send(msg)
298+
except Exception as e:
299+
return jsonify({'error': f'Failed to send email: {str(e)}'}), 500
300+
return jsonify({'success': True, 'message': 'Subscription successful with GDPR consent.'}), 200
301+
302+
@app.route('/')
303+
def index():
304+
return send_from_directory(app.static_folder, 'landing.html')
305+
306+
@app.route('/js/config.js')
307+
def config():
308+
return send_from_directory(app.static_folder, 'js/config_flask.js')
309+
310+
@app.route('/<path:path>')
311+
def static_proxy(path):
312+
return send_from_directory(app.static_folder, path)
313+
314+
if __name__ == '__main__':
315+
app.run(debug=True)

backend/requirements.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
flask
2+
flask-cors
3+
flask-limiter
4+
flask-mail
5+
gunicorn

data/contact.jsonl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{"name": "Peter", "email": "[email protected]", "subject": "collaboration", "message": "I want to work with you!", "timestamp": "2025-05-09T06:14:29.938486"}
2+
{"name": "Peter", "email": "[email protected]", "subject": "collaboration", "message": "I want to work with you!!", "timestamp": "2025-05-09T06:20:08.116463"}
3+
{"name": "Hello", "email": "world@stuff", "subject": "collaboration", "message": "yes", "timestamp": "2025-05-09T06:27:10.384707+00:00"}
4+
{"name": "Test", "email": "[email protected]", "subject": "collaboration", "message": "now", "timestamp": "2025-05-09T06:30:37.873857+00:00"}
5+
{"name": "Test", "email": "[email protected]", "subject": "technical", "message": "Haha!", "timestamp": "2025-05-14T10:26:11.307573+00:00"}
6+
{"name": "dhdsvkjhjkhsvdf", "email": "[email protected]", "subject": "technical", "message": "does not work!", "timestamp": "2025-05-14T11:17:51.228307+00:00"}

data/news.jsonl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{"id": "test-1", "title": "Test news", "date": "2025-05-25", "content": "Something awesome happened just now!\n<br/>\nWe finished a training ...\n<br/>\n<br/>\n<br/>\n<br/>\nAnd a cat appeared!"}
2+
{"id": "test-2", "title": "Another day", "date": "2025-05-25", "content": "<p>Lorem ipsum dolor sit amet, <a href=\"#contact\">consectetur</a> adipiscing elit. Sed at augue euismod, ultrices massa at, vehicula risus.</p>\n<p>Nullam vitae lacinia neque. Aenean condimentum, velit sed convallis laoreet, odio est sollicitudin elit, at cursus nulla orci nec nibh. In eget dignissim arcu. Pellentesque ut nulla a lacus ultricies egestas. Suspendisse potenti. Vestibulum a scelerisque orci. Etiam in felis nec odio finibus vehicula.</p>\n<p>Integer et purus nec justo tincidunt fermentum. Aliquam erat volutpat. Nunc sit amet est ligula. Mauris vehicula orci et nulla fermentum, non vulputate turpis scelerisque. Cras cursus lacus nec eros faucibus, in blandit metus elementum. Phasellus lobortis metus nec lorem finibus, sed facilisis nulla faucibus. Suspendisse nec sem in augue feugiat rhoncus. Curabitur at metus lacinia, rutrum velit nec, consequat felis.</p>\n<p>Ut sit amet vestibulum metus. Proin hendrerit orci nec lacus malesuada, at placerat ante scelerisque. Vestibulum id semper tortor. Nullam a felis in sem pretium tincidunt. Praesent id rhoncus ligula, ut fermentum risus. Duis mattis nibh vitae odio luctus, non vehicula magna pretium. Vivamus at tincidunt ligula. Nam imperdiet lacus at mauris volutpat, non varius nulla tincidunt.</p>"}
3+
{"id": "06a3d1cd-bf72-48bc-9758-9998d49c72bd", "title": "Hello World!", "date": "2025-05-28", "content": "This is yet another fine DFM model, don't ya think?"}
4+
{"id": "f598ac76-c9b9-4d2c-af9b-fdf2c5b0e8b4", "title": "Another test", "date": "2025-05-12", "content": "Another day, another test! But it's always you."}
5+
{"id": "c090ba7d-9ebb-4d49-a210-929b88086834", "title": "I'm first!", "date": "2025-05-13", "content": "Haha!"}
6+
{"id": "1c545126-c170-4104-aa10-5a80272724a8", "title": "Munin 2.0", "date": "2025-05-15", "content": "dcskjvdskfjnsd"}

data/newsletter.jsonl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{"email": "[email protected]", "consent": true, "timestamp": "2025-05-09T07:18:44.125196+00:00"}
2+
{"email": "[email protected]", "consent": true, "timestamp": "2025-05-09T07:28:29.838522+00:00"}
3+
{"email": "[email protected]", "consent": true, "timestamp": "2025-05-14T10:28:05.247876+00:00"}

data/roadmap.jsonl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{"id": "phase-1", "title": "Phase 1: Research & Planning", "quarter": "Q1 2023", "description": "Establish research framework, gather Danish language datasets, and define model architecture.", "status": "In Progress"}
2+
{"id": "phase-2", "title": "Phase 2: Initial Model Development", "quarter": "Q2-Q3 2023", "description": "Develop first versions of Danish language models, focusing on core language understanding capabilities.", "status": "Completed"}
3+
{"id": "phase-3", "title": "Phase 3: Community Testing", "quarter": "Q4 2023", "description": "Release beta versions to research community for feedback and improvement.", "status": "In Progress"}
4+
{"id": "phase-4", "title": "Phase 4: Model Optimization", "quarter": "Q1 2024", "description": "Refine models based on community feedback, improve efficiency and Danish-specific performance.", "status": ""}
5+
{"id": "phase-5", "title": "Phase 5: Public Release", "quarter": "Q2 2024", "description": "Release stable versions of Danish foundation models with documentation and examples.", "status": ""}

0 commit comments

Comments
 (0)