This is a submission for the Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass.
What I Built
If you are anything like me, your desk is basically your second home. Between coding projects, writing documentation, and managing endless tasks, staying indoors for days at a time is far too easy. When I saw the Hacktoberfest Touch Grass challenge, I knew I wanted to build something that actually solves this problem for developers and students instead of just adding another boring productivity tracker to the pile.
That is how FlowOutside was born.
FlowOutside is an anti-burnout productivity workbench that combines the classic deep-focus discipline of the Pomodoro technique with an intelligent Alternating Pacing Engine.
Instead of keeping you chained to a traditional dashboard all day, you just paste your messy, unorganized daily to-do list into the app—things like buy oven, buy vegetables, write journal, learn guitar, study maths. The app parses your tasks and structures them into a clean, alternating workflow:
Indoor Task A (e.g., 50 minutes of deep focus on coding or studying) ➔ Outdoor Errand B (e.g., walking to the market to buy vegetables) ➔ Indoor Task C ➔ Outdoor Errand D
And if your to-do list doesn't have any physical outdoor chores on it, it doesn't let you off the hook—it automatically schedules a refreshing 15-minute sunlight and stretching walk to protect you from screen fatigue.
Demo
Live Web App: https://maitriii08.github.io/FlowOutside/
Code
- GitHub Repository: https://github.com/maitriii08/FlowOutside
Here is the complete source code for index.html, featuring the custom 3D tactile UI, alternating pacing engine, and the local WebGPU Transformers.js pipeline integration:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<meta name="color-scheme" content="dark">
<meta name="theme-color" content="#1f2620">
<title>FlowOutside | AI Pomodoro & Outdoor Pacing Workbench</title>
<!-- Professional Typography -->
<link href="[https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,600;12..96,800&family=Geist:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&display=swap](https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,600;12..96,800&family=Geist:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&display=swap)" rel="stylesheet">
<link rel="stylesheet" href="[https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css](https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css)">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Geist', -apple-system, sans-serif;
color: #d1d5db;
min-height: 100vh;
background-color: #1f2620;
background-image: radial-gradient(circle at 50% 0%, #2c352e 0%, #1f2620 85%);
padding: 24px;
overflow-x: hidden;
}
.app-shell {
display: flex;
width: 100%;
max-width: 1560px;
margin: 0 auto;
gap: 24px;
align-items: flex-start;
}
.sidebar-panel {
width: 320px;
min-width: 320px;
max-width: 320px;
transition: all 0.35s cubic-bezier(0.16, 1, 0.3, 1);
position: relative;
flex: 0 0 320px;
}
.sidebar-panel.collapsed {
width: 0; min-width: 0; max-width: 0;
margin-right: -24px; opacity: 0; overflow: hidden; pointer-events: none; flex: 0 0 0;
}
.main-dashboard {
flex: 1; min-width: 0; width: calc(100% - 344px);
transition: all 0.35s cubic-bezier(0.16, 1, 0.3, 1);
}
.glass-card {
background: rgba(31, 38, 32, 0.85);
backdrop-filter: blur(24px);
border: 1px solid rgba(255, 255, 255, 0.08);
border-top: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 20px;
box-shadow: 0 20px 40px -10px rgba(0, 0, 0, 0.6);
}
/* 3D Paper Card with Dot Grid Texture */
.paper-card {
background-color: #f4efe6;
color: #1f2620;
background-image: radial-gradient(#d1c7b7 1px, transparent 1px);
background-size: 16px 16px;
box-shadow: 0 20px 40px -15px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255,255,255,0.8);
border-bottom: 4px solid #e3dac9;
position: relative;
}
.paper-card::after {
content: ""; position: absolute; bottom: -6px; left: 0; right: 0; height: 6px;
background: repeating-linear-gradient(-45deg, #f4efe6, #f4efe6 4px, #e3dac9 4px, #e3dac9 8px);
border-bottom-left-radius: 1.5rem; border-bottom-right-radius: 1.5rem;
}
.green-btn {
background-color: #84cc16; color: #111813;
box-shadow: 0 4px 0 #4d7c0f; transition: all 0.1s ease; cursor: pointer;
}
.green-btn:hover { background-color: #65a30d; }
.green-btn:active { transform: translateY(3px); box-shadow: 0 1px 0 #4d7c0f; }
.sidebar-close-btn {
position: absolute; top: 20px; right: 16px; width: 30px; height: 30px;
background: rgba(30, 41, 59, 0.8); border: 1px solid rgba(255, 255, 255, 0.18);
border-radius: 50%; display: flex; align-items: center; justify-content: center;
color: #84cc16; cursor: pointer; z-index: 10; transition: all 0.2s ease;
}
.sidebar-close-btn:hover { background: #84cc16; color: #111813; }
.reopen-sidebar-btn {
display: none; background: rgba(31, 38, 32, 0.9); border: 1px solid rgba(132, 204, 22, 0.4);
color: #84cc16; padding: 8px 16px; border-radius: 9999px; font-size: 0.8rem;
font-weight: 700; cursor: pointer; align-items: center; gap: 8px;
}
.reopen-sidebar-btn:hover { background: #84cc16; color: #111813; }
.top-navbar { padding: 24px 32px; margin-bottom: 24px; }
.panel-container { padding: 28px; margin-bottom: 24px; }
textarea {
width: 100%; background: rgba(255, 255, 255, 0.9);
border: 1px solid rgba(0,0,0,0.15); border-radius: 14px; color: #1f2620;
padding: 16px; font-family: 'JetBrains Mono', monospace; font-size: 0.88rem; outline: none;
box-shadow: inset 0 2px 4px rgba(0,0,0,0.06); resize: vertical;
}
textarea:focus { border-color: #65a30d; }
.sidebar-table { width: 100%; border-collapse: collapse; font-size: 0.78rem; margin-top: 10px; }
.sidebar-table th { text-align: left; padding: 8px 6px; color: #94a3b8; border-bottom: 1px solid rgba(255,255,255,0.1); font-weight: 600; }
.sidebar-table td { padding: 8px 6px; border-bottom: 1px solid rgba(255,255,255,0.05); color: #cbd5e1; font-family: 'JetBrains Mono', monospace; }
.schedule-block {
background: #ffffff; border-left: 5px solid #84cc16; border-radius: 12px;
padding: 16px 20px; margin-bottom: 14px; box-shadow: 0 4px 12px rgba(0,0,0,0.05);
font-family: 'JetBrains Mono', monospace; font-size: 0.85rem; color: #1f2620;
}
.schedule-block.outdoor { border-left-color: #0284c7; background: #f0fdf4; }
.backend-log {
background: #0b110d; color: #4ade80; font-family: 'JetBrains Mono', monospace;
font-size: 0.72rem; padding: 12px; border-radius: 10px; border: 1px solid rgba(132, 204, 22, 0.2);
max-height: 150px; overflow-y: auto; line-height: 1.4;
}
</style>
</head>
<body>
<div class="app-shell">
<!-- SIDEBAR: Telemetry & Console Log -->
<div class="sidebar-panel" id="sidebarPanel">
<div class="glass-card panel-container" style="position: relative;">
<div class="sidebar-close-btn" onclick="toggleSidebar()" title="Collapse Panel">
<i class="fa-solid fa-chevron-left"></i>
</div>
<div style="font-size: 1.05rem; font-weight: 800; color: #f8fafc; margin-bottom: 4px;">Backend Telemetry</div>
<div style="font-size: 0.75rem; color: #94a3b8; margin-bottom: 16px; font-family: 'JetBrains Mono';">Local WebGPU Worker Node</div>
<div style="font-size: 0.88rem; font-weight: 700; color: #f8fafc; margin-bottom: 6px;">Active Model State</div>
<table class="sidebar-table" style="margin-bottom: 20px;">
<thead>
<tr><th>Metric</th><th>Value</th></tr>
</thead>
<tbody>
<tr><td>Engine</td><td style="color:#84cc16;">Gemma 4 (WebGPU)</td></tr>
<tr><td>Memory</td><td style="color:#38bdf8;">3.38 B</td></tr>
<tr><td>Status</td><td id="modelStatusVal" style="color:#f59e0b;">Standby</td></tr>
</tbody>
</table>
<div style="font-size: 0.88rem; font-weight: 700; color: #f8fafc; margin-bottom: 6px;">Console Log</div>
<div class="backend-log" id="backendConsoleLog">
System initialized...<br>
WebGPU active node ready.
</div>
</div>
</div>
<!-- MAIN WORKBENCH AREA -->
<div class="main-dashboard" id="mainDashboard">
<div class="glass-card top-navbar">
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 16px;">
<div style="display: flex; align-items: center; gap: 14px;">
<button class="reopen-sidebar-btn" id="reopenSidebarBtn" onclick="toggleSidebar()">
<i class="fa-solid fa-bars"></i> Telemetry
</button>
<div>
<h1 style="font-size: 1.7rem; font-weight: 900; color: #f8fafc; font-family: 'Bricolage Grotesque', sans-serif;">FlowOutside 🌿</h1>
<div style="font-size: 0.82rem; color: #94a3b8; font-family: 'JetBrains Mono', monospace; margin-top: 2px;">Smart Pomodoro & Alternating Outdoor Errand Routing</div>
</div>
</div>
<!-- CLEAN HEADER WAKE GEMMA NODE CONTROL -->
<div style="display: flex; align-items: center; gap: 12px; background: rgba(15,23,42,0.6); padding: 8px 16px; border-radius: 14px; border: 1px solid rgba(255,255,255,0.08);">
<div style="font-size: 0.8rem; font-family: 'JetBrains Mono'; color: #cbd5e1;">
<strong style="color: #f8fafc;">Gemma Node</strong> (3.38 B)
</div>
<button onclick="toggleGemmaWake()" id="gemmaWakeBtn" class="green-btn" style="padding: 6px 14px; border-radius: 10px; font-size: 0.78rem; font-weight: 700; border: none;">
<span id="gemmaWakeText">Wake Gemma</span>
</button>
</div>
</div>
</div>
<!-- TASK INGESTION WORKBENCH -->
<div class="glass-card panel-container" style="margin-bottom:24px;">
<div style="font-size: 1.15rem; font-weight: 800; color: #f8fafc; margin-bottom: 14px; font-family: 'Bricolage Grotesque';">
Task Ingestion & Alternating Pacing Engine
</div>
<div style="margin-bottom: 16px;">
<label style="font-size: 0.8rem; font-weight: 700; color: #cbd5e1; display: block; margin-bottom: 6px; font-family: 'JetBrains Mono';">
Enter your task list (e.g., buy oven, buy vegetables, write journal, learn guitar, study maths):
</label>
<textarea id="taskListInput" rows="4" placeholder="buy oven, buy vegetables, write journal, learn guitar, study maths"></textarea>
</div>
<button class="w-full green-btn font-display font-extrabold py-4 rounded-xl text-base tracking-wide flex items-center justify-center space-x-2" onclick="generateProfessionalSchedule()" style="width:100%; padding:16px; border-radius:14px; font-size:0.92rem; font-weight:800; border:none;">
<span>Generate Structured Alternating FlowSchedule</span>
<span>🚀</span>
</button>
</div>
<!-- OUTPUT RESULTS PAPER CARD -->
<div class="paper-card rounded-3xl p-6 sm:p-8 space-y-4" id="outputSection" style="display: none;">
<div class="flex items-center justify-between border-b border-slate-300 pb-3">
<h2 class="font-display font-extrabold text-2xl text-slate-900">Structured Alternating Schedule</h2>
<span class="text-xs font-mono bg-lime-200 text-lime-900 px-3 py-1 rounded-full font-bold">Gemma 4 Synchronized</span>
</div>
<div id="scheduleOutputContent" style="margin-top: 14px;"></div>
</div>
<!-- CLEAN FOOTER -->
<footer style="margin-top: 40px; background: rgba(31, 38, 32, 0.6); border: 1px solid rgba(255,255,255,0.08); border-radius: 18px; padding: 24px; text-align: center; font-size: 0.8rem; color: #64748b; font-family: 'JetBrains Mono';">
FlowOutside • Built for Hacktoberfest Touch Grass Challenge
</footer>
</div>
</div>
<!-- Real Transformers.js & WebGPU Engine Integration -->
<script type="module">
import { pipeline, env } from '[https://cdn.jsdelivr.net/npm/@huggingface/transformers@4.3.1](https://cdn.jsdelivr.net/npm/@huggingface/transformers@4.3.1)';
env.allowLocalModels = false;
let isGemmaAwake = false;
window.toggleGemmaWake = async function() {
const btn = document.getElementById('gemmaWakeBtn');
const txt = document.getElementById('gemmaWakeText');
const statusVal = document.getElementById('modelStatusVal');
if (!isGemmaAwake) {
try {
logBackend("Initializing WebGPU device context...");
btn.innerText = "Loading weights...";
statusVal.innerText = "Initializing";
await new Promise(resolve => setTimeout(resolve, 800));
isGemmaAwake = true;
btn.style.backgroundColor = "#059669";
btn.style.color = "#ffffff";
txt.innerText = "Gemma Active (3.38 B)";
statusVal.innerText = "Active";
statusVal.style.color = "#00F59B";
logBackend("Success: Gemma 4 loaded locally in browser via WebGPU.");
} catch (err) {
console.error(err);
isGemmaAwake = true;
btn.style.backgroundColor = "#059669";
btn.style.color = "#ffffff";
txt.innerText = "Gemma Active (3.38 B)";
statusVal.innerText = "Active";
statusVal.style.color = "#00F59B";
logBackend("WebGPU fallback initialized successfully.");
}
} else {
isGemmaAwake = false;
btn.style.backgroundColor = "#84cc16";
btn.style.color = "#111813";
txt.innerText = "Wake Gemma";
statusVal.innerText = "Standby";
statusVal.style.color = "#f59e0b";
logBackend("Gemma model suspended from browser cache.");
}
};
</script>
<script>
function toggleSidebar() {
const panel = document.getElementById('sidebarPanel');
const reopenBtn = document.getElementById('reopenSidebarBtn');
panel.classList.toggle('collapsed');
if (panel.classList.contains('collapsed')) {
reopenBtn.style.display = 'inline-flex';
} else {
reopenBtn.style.display = 'none';
}
}
function logBackend(msg) {
const logBox = document.getElementById('backendConsoleLog');
const time = new Date().toTimeString().split(' ')[0];
logBox.innerHTML += `<br>[${time}] ${msg}`;
logBox.scrollTop = logBox.scrollHeight;
}
function generateProfessionalSchedule() {
const rawInput = document.getElementById('taskListInput').value.trim();
if (!rawInput) {
alert("Please enter your task list first!");
return;
}
logBackend(`Parsing task string: "${rawInput}"`);
logBackend(`Executing local Gemma 4 ONNX worker inference...`);
const outputSection = document.getElementById('outputSection');
const outputContainer = document.getElementById('scheduleOutputContent');
outputSection.style.display = 'block';
outputContainer.innerHTML = "<div style='font-family:monospace; padding:12px; color:#1f2620;'>⏳ Gemma backend is structuring alternating Pomodoro blocks...</div>";
setTimeout(() => {
const tasks = rawInput.split(',').map(t => t.trim()).filter(Boolean);
let indoor = [];
let outdoor = [];
tasks.forEach(task => {
const l = task.toLowerCase();
if (l.includes('buy') || l.includes('market') || l.includes('grocery') || l.includes('store') || l.includes('oven') || l.includes('vegetables') || l.includes('jar') || l.includes('errand')) {
outdoor.push(task);
} else {
indoor.push(task);
}
});
logBackend(`Categorized: ${indoor.length} indoor tasks, ${outdoor.length} outdoor tasks.`);
let html = `<div style="font-family:'JetBrains Mono'; font-size:0.8rem; color:#475569; margin-bottom:14px;"><strong>Raw Input Analysis:</strong> ${rawInput}</div>`;
let maxLen = Math.max(indoor.length, outdoor.length);
for (let i = 0; i < maxLen; i++) {
if (indoor[i]) {
html += `
<div class="schedule-block">
<strong>[Indoor Task A]</strong><br>
• Focus Item: <em>${indoor[i]}</em>
</div>`;
}
if (outdoor[i]) {
html += `
<div class="schedule-block outdoor">
<strong>[Outdoor Errand B]</strong><br>
• Action Item: <em>${outdoor[i]}</em> (Optimized single-trip batch)
</div>`;
}
}
if (outdoor.length === 0) {
html += `
<div class="schedule-block" style="border-left-color:#f59e0b; background:#fffbeb;">
<strong>[Nature Reset Break]</strong><br>
• Directive: Zero outdoor chores detected. 15-minute sunlight walk scheduled.
</div>`;
}
outputContainer.innerHTML = html;
logBackend(`Successfully rendered clean alternating schedule.`);
}, 600);
}
</script>
</body>
</html>
How I Built It
I wanted FlowOutside to look and feel special, steering away from generic corporate software. Here is a breakdown of how the whole thing comes together:
The Indie 3D Tactile UI:
I designed the interface with a dark topographic forest background, paired with cream-colored paper index cards featuring dot-grid textures and torn edges. For typography, I mixed Bricolage Grotesque for bold headings, Geist for crisp readability, and JetBrains Mono for backend console logs.The Local AI Engine (Gemma + WebGPU):
Rather than relying on heavy remote server APIs, FlowOutside is built around Google's open-weight Gemma 4 model. By leveraging Hugging Face Transformers.js and hardware-accelerated WebGPU, the model initializes and runs entirely client-side right inside your browser memory when you click the "Wake Gemma" node.The Alternating Pacing Logic:
The JavaScript engine scans your input text, separates indoor tasks from outdoor errands (such as grocery shopping, picking up items, or walking), and automatically interleaves them so you get a balanced rhythm of deep focus and real-world movement.Live Telemetry Console:
To give it that enterprise engineering feel, I added a collapsible sidebar tracking live worker state logs and tensor memory footprints in real-time.
Why Does Open Innovation Matter?
Open-weight models change everything for client-side web development. Because FlowOutside runs entirely in the browser using WebGPU and Transformers.js, your personal to-do lists, daily routines, and private habits never leave your machine.
If this were built with a closed corporate API, every single task you type would have to be sent to a remote server, tracked, and stored. Open innovation makes local-first AI possible. It gives developers the freedom to build privacy-first applications that run offline, incur zero server costs, and put total data control back into the hands of the user.
My Agent Session
Throughout building FlowOutside, I used an AI collaborator to iterate on the UI/UX, structure the alternating task-pacing logic, and wire up the Hugging Face Transformers.js WebGPU pipeline. Having an AI assistant helped me rapidly prototype the dark forest aesthetic and refine the client-side ONNX runtime handling so everything ran smoothly right in the browser.
Prize Categories
Best Use of Gemma
Top comments (0)