FireWatch AI Wildfire Detection Console
An early wildfire detection and monitoring agent that analyzes sensor, thermal, weather, camera, and location evidence to classify risk, explain uncertainty, and recommend next actions in a structured operational format.
Preview Mode
This is a preview with sample data. The template uses placeholders like
which will be replaced with actual agent data.
About This Template
FireWatch AI Wildfire Detection Console is a browser-executable AI agent template built on AgentOp. It runs entirely in the browser using Python (via Pyodide) and can be deployed without a server — just download the generated HTML file and open it locally or host it anywhere.
Template Metadata
- Slug
- firewatch-ai-wildfire-detection-console
- Created By
- JovinThebuilder
- Created
- Sep 20, 2026
- Usage Count
- 1
Tags
Code Statistics
- HTML Lines
- 108
- CSS Lines
- 283
- JS Lines
- 139
- Python Lines
- 122
Source Code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{ agent_name }}</title>
<style>
{{ css_code }}
</style>
</head>
<body>
<div class="app-shell">
<header class="hero">
<div>
<div class="eyebrow">🔥 Wildfire Detection Console</div>
<h1>{{ agent_name }}</h1>
<p class="hero-copy">{{ description }}</p>
</div>
<div class="status-card" id="runtime-status-card">
<div class="status-title">System Status</div>
<div class="status-pill" id="runtime-status">Initializing…</div>
<div class="status-help" id="runtime-help">Loading analysis engine.</div>
</div>
</header>
<main class="workspace-grid">
<section class="panel intake-panel">
<div class="panel-head">
<h2>📡 Incident Intake</h2>
<p>Enter a wildfire monitoring snapshot with any available evidence.</p>
</div>
<label for="report-input" class="field-label">Wildfire sensor and observation report</label>
<textarea id="report-input" class="report-input" placeholder="Example:\nLocation: Sector Pine-7, 34.211,-117.412\nTime: 2026-09-20 14:22 local\nAir temperature: 39.4 C (up from 31.8 C in 20 min)\nHumidity: 14%\nSmoke sensor: elevated\nPM2.5: 182 ug/m3\nThermal camera: hotspot detected at tree line, approx 84 C\nVisual camera: faint gray plume, intermittent\nWind: 18 km/h gusting to 31 km/h from SW\nNotes: no lightning observed, area is dry brush"></textarea>
<div class="field-grid">
<div>
<label for="priority-select" class="field-label">Operational priority</label>
<select id="priority-select" class="select-input">
<option value="routine">Routine monitoring</option>
<option value="elevated">Elevated concern</option>
<option value="critical">Critical watch</option>
</select>
</div>
<div>
<label for="output-style" class="field-label">Output style</label>
<select id="output-style" class="select-input">
<option value="ops">Operations summary</option>
<option value="technical">Technical detail</option>
</select>
</div>
</div>
<div class="action-row">
<button id="analyze-btn" class="primary-btn">Analyze wildfire risk</button>
<button id="clear-btn" class="secondary-btn" type="button">Clear report</button>
</div>
<div id="inline-error" class="inline-error hidden" role="alert"></div>
<div class="tips-card">
<div class="tips-title">What to include</div>
<ul>
<li>🌡️ Temperature and rate of change</li>
<li>💧 Humidity and dryness conditions</li>
<li>🌫️ Smoke, PM, and air-quality indicators</li>
<li>📷 Camera or observer notes</li>
<li>♨️ Thermal hotspot details</li>
<li>📍 GPS or sector location and weather context</li>
</ul>
</div>
</section>
<section class="panel output-panel">
<div class="panel-head">
<h2>🚒 Assessment Output</h2>
<p>Structured classification, evidence, uncertainty, and next action.</p>
</div>
<div id="results-container" class="results-container">
<div class="empty-state" id="empty-state">
<div class="empty-emoji">🌲</div>
<h3>No wildfire assessment yet</h3>
<p>Submit a sensor snapshot to generate an operational risk assessment.</p>
<ul>
<li>Fuse thermal, camera, weather, and air-quality signals</li>
<li>Flag unusual or rapidly changing conditions</li>
<li>Separate confirmed evidence from uncertainty</li>
<li>Recommend the next field action</li>
</ul>
</div>
</div>
</section>
</main>
</div>
<div id="loading-overlay" class="loading-overlay hidden" aria-hidden="true">
<div class="spinner-card">
<div class="spinner"></div>
<div class="spinner-text">Analyzing wildfire indicators…</div>
</div>
</div>
<script>
{{ js_code }}
</script>
</body>
</html>
:root {
--bg: #0b1220;
--panel: #121b2d;
--panel-2: #182338;
--card: #1c2942;
--text: #edf3ff;
--muted: #9fb1cc;
--accent: #ff7a18;
--accent-2: #ffb347;
--danger: #ff5d5d;
--warn: #ffd166;
--ok: #4dd08a;
--border: rgba(255,255,255,0.12);
--shadow: 0 18px 44px rgba(0,0,0,0.28);
--radius: 18px;
--radius-sm: 12px;
--font: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
* { box-sizing: border-box; }
html, body {
margin: 0;
padding: 0;
background: linear-gradient(180deg, #09111d 0%, #0e1627 100%);
color: var(--text);
font-family: var(--font);
min-height: 100%;
}
body { padding: 24px; }
.app-shell { max-width: 1440px; margin: 0 auto; }
.hidden { display: none !important; }
.hero {
display: grid;
grid-template-columns: 1.8fr 0.9fr;
gap: 20px;
align-items: stretch;
margin-bottom: 22px;
}
.eyebrow {
color: var(--accent-2);
font-size: 0.82rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
margin-bottom: 8px;
}
h1 {
margin: 0 0 10px;
font-size: clamp(2rem, 4vw, 3rem);
line-height: 1.05;
}
.hero-copy {
margin: 0;
color: var(--muted);
max-width: 900px;
line-height: 1.55;
}
.status-card, .panel, .spinner-card {
background: rgba(18, 27, 45, 0.94);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow);
}
.status-card {
padding: 18px;
display: flex;
flex-direction: column;
justify-content: center;
}
.status-title {
font-size: 0.84rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--muted);
margin-bottom: 10px;
}
.status-pill {
display: inline-flex;
align-self: flex-start;
padding: 8px 12px;
border-radius: 999px;
background: rgba(255, 179, 71, 0.16);
color: var(--accent-2);
font-weight: 700;
margin-bottom: 8px;
}
.status-help {
color: var(--muted);
line-height: 1.45;
}
.workspace-grid {
display: grid;
grid-template-columns: minmax(340px, 0.95fr) minmax(420px, 1.25fr);
gap: 22px;
}
.panel { padding: 22px; }
.panel-head h2 { margin: 0 0 6px; font-size: 1.25rem; }
.panel-head p { margin: 0 0 18px; color: var(--muted); }
.field-label {
display: block;
font-weight: 700;
margin-bottom: 8px;
}
.report-input, .select-input {
width: 100%;
background: var(--panel-2);
color: var(--text);
border: 1px solid rgba(255,255,255,0.1);
border-radius: 14px;
padding: 14px 15px;
font: inherit;
}
.report-input {
min-height: 300px;
resize: vertical;
line-height: 1.5;
}
.field-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 14px;
margin-top: 16px;
}
.action-row {
display: flex;
gap: 12px;
margin-top: 18px;
flex-wrap: wrap;
}
.primary-btn, .secondary-btn {
border: none;
border-radius: 14px;
padding: 13px 18px;
font: inherit;
font-weight: 800;
cursor: pointer;
transition: transform 0.15s ease, opacity 0.15s ease, background 0.15s ease;
}
.primary-btn {
background: linear-gradient(135deg, var(--accent), #ff4d00);
color: white;
}
.secondary-btn {
background: rgba(255,255,255,0.08);
color: var(--text);
border: 1px solid rgba(255,255,255,0.12);
}
.primary-btn:hover, .secondary-btn:hover { transform: translateY(-1px); }
.primary-btn:disabled, .secondary-btn:disabled {
opacity: 0.55;
cursor: not-allowed;
transform: none;
}
.report-input:focus-visible, .select-input:focus-visible, .primary-btn:focus-visible, .secondary-btn:focus-visible {
outline: 3px solid rgba(255, 122, 24, 0.35);
outline-offset: 2px;
}
.inline-error {
margin-top: 14px;
padding: 12px 14px;
border-radius: 12px;
background: rgba(255, 93, 93, 0.12);
border: 1px solid rgba(255, 93, 93, 0.28);
color: #ffd6d6;
}
.tips-card {
margin-top: 18px;
padding: 16px;
border-radius: 16px;
background: rgba(255,255,255,0.04);
border: 1px solid rgba(255,255,255,0.08);
}
.tips-title { font-weight: 800; margin-bottom: 10px; }
.tips-card ul { margin: 0; padding-left: 18px; color: var(--muted); line-height: 1.55; }
.results-container {
min-height: 620px;
background: linear-gradient(180deg, rgba(255,255,255,0.03), rgba(255,255,255,0.015));
border: 1px dashed rgba(255,255,255,0.12);
border-radius: 18px;
padding: 18px;
}
.empty-state {
height: 100%;
min-height: 560px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
color: var(--muted);
}
.empty-emoji { font-size: 2rem; margin-bottom: 8px; }
.empty-state h3 { color: var(--text); margin: 0 0 8px; }
.empty-state p { margin: 0 0 10px; }
.empty-state ul { margin: 0; padding-left: 18px; line-height: 1.55; }
.message { margin-bottom: 16px; }
.message.assistant {
background: var(--card);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 16px;
padding: 16px;
}
.message pre {
white-space: pre-wrap;
word-break: break-word;
margin: 0;
color: var(--text);
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 0.93rem;
line-height: 1.55;
}
.loading-overlay {
position: fixed;
inset: 0;
background: rgba(6, 10, 18, 0.62);
display: flex;
align-items: center;
justify-content: center;
z-index: 999;
}
.spinner-card {
padding: 24px 28px;
text-align: center;
}
.spinner {
width: 42px;
height: 42px;
border-radius: 50%;
border: 4px solid rgba(255,255,255,0.14);
border-top-color: var(--accent);
margin: 0 auto 12px;
animation: spin 1s linear infinite;
}
.spinner-text { color: var(--muted); }
@keyframes spin { to { transform: rotate(360deg); } }
@media (max-width: 1200px) {
.hero, .workspace-grid { grid-template-columns: 1fr; }
.results-container { min-height: 520px; }
}
@media (max-width: 768px) {
body { padding: 14px; }
.panel { padding: 16px; }
.field-grid { grid-template-columns: 1fr; }
.action-row { flex-direction: column; }
.primary-btn, .secondary-btn { width: 100%; }
.empty-state { min-height: 380px; }
}
function escapeHtml(value) {
return String(value)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
(function () {
const reportInput = document.getElementById('report-input');
const prioritySelect = document.getElementById('priority-select');
const outputStyleSelect = document.getElementById('output-style');
const analyzeBtn = document.getElementById('analyze-btn');
const clearBtn = document.getElementById('clear-btn');
const inlineError = document.getElementById('inline-error');
const resultsContainer = document.getElementById('results-container');
const emptyState = document.getElementById('empty-state');
const overlay = document.getElementById('loading-overlay');
const runtimeStatus = document.getElementById('runtime-status');
const runtimeHelp = document.getElementById('runtime-help');
let pyReady = false;
function setRuntimeState(label, helpText) {
runtimeStatus.textContent = label;
runtimeHelp.textContent = helpText;
}
function isLocalModelReady() {
if (window.PROVIDER !== 'local') return true;
return !!(window.agentManager && window.agentManager.isLoaded);
}
function updateReadyState() {
if (!pyReady) {
setRuntimeState('Initializing…', 'Loading analysis engine.');
analyzeBtn.disabled = true;
return;
}
if (!isLocalModelReady()) {
setRuntimeState('Model not loaded', 'Load a local model from the selector bar above to enable wildfire analysis.');
analyzeBtn.disabled = true;
return;
}
setRuntimeState('Ready', 'FireWatch AI can assess wildfire evidence now.');
analyzeBtn.disabled = false;
}
function showError(message) {
inlineError.textContent = message;
inlineError.classList.remove('hidden');
}
function clearError() {
inlineError.textContent = '';
inlineError.classList.add('hidden');
}
function setLoading(isLoading) {
analyzeBtn.disabled = isLoading || !pyReady || !isLocalModelReady();
clearBtn.disabled = isLoading;
overlay.classList.toggle('hidden', !isLoading);
overlay.setAttribute('aria-hidden', isLoading ? 'false' : 'true');
}
window.addMessage = function (type, content) {
if (emptyState) emptyState.classList.add('hidden');
resultsContainer.innerHTML = '<div class="message ' + escapeHtml(type) + '"><pre>' + escapeHtml(content) + '</pre></div>';
};
async function runAnalysis() {
clearError();
const report = reportInput.value.trim();
const priority = prioritySelect.value;
const outputStyle = outputStyleSelect.value;
if (!report) {
showError('Please enter a wildfire sensor or observation report before running analysis.');
return;
}
if (!pyReady) {
showError('The analysis engine is still initializing. Please wait a moment and try again.');
return;
}
if (!isLocalModelReady()) {
showError('A local model is not loaded yet. Use the model selector bar above, then run the assessment again.');
return;
}
const composedQuery = [
'Operational priority: ' + priority,
'Output style: ' + outputStyle,
'Wildfire report:',
report
].join('\n');
setLoading(true);
try {
window.pyodide.globals.set('user_query', composedQuery);
const result = await window.pyodide.runPythonAsync('await process_user_query(user_query)');
window.addMessage('assistant', String(result));
} catch (error) {
showError('Analysis failed. ' + (error && error.message ? error.message : 'Unknown error.'));
} finally {
setLoading(false);
updateReadyState();
}
}
function clearForm() {
reportInput.value = '';
prioritySelect.value = 'routine';
outputStyleSelect.value = 'ops';
clearError();
resultsContainer.innerHTML = '';
if (emptyState) {
resultsContainer.appendChild(emptyState);
emptyState.classList.remove('hidden');
}
}
analyzeBtn.addEventListener('click', runAnalysis);
clearBtn.addEventListener('click', clearForm);
prioritySelect.addEventListener('change', clearError);
outputStyleSelect.addEventListener('change', clearError);
reportInput.addEventListener('input', clearError);
document.addEventListener('pyodide-ready', function () {
pyReady = true;
updateReadyState();
});
if (window.pyodideReady) {
pyReady = true;
}
updateReadyState();
setInterval(updateReadyState, 1200);
})();
import json
import re
from typing import Dict, List
MAX_CHARS = [[[MAX_CHARS|6000]]]
TEMPLATE_SYSTEM_PROMPT = """You are FireWatch AI, an early wildfire detection analyst.
Task:
- Review the user's wildfire report.
- Combine multiple pieces of evidence when possible.
- Do not rely on one weak signal alone.
- Distinguish evidence from uncertainty.
- Avoid claiming a fire is verified unless evidence is strong.
- Watch for unusual or rapidly changing conditions.
Return plain text with this exact structure:
CLASSIFICATION: one of NORMAL | INVESTIGATE | POSSIBLE FIRE | CONFIRMED/VERIFIED FIRE
CONFIDENCE: Low | Medium | High
EVIDENCE:
- bullet list of observed evidence that supports the assessment
UNCERTAINTIES:
- bullet list of missing, conflicting, or weak signals
UNUSUAL OR RAPID CHANGES:
- bullet list, or '- none noted'
RECOMMENDED NEXT ACTION:
- one primary action
STRUCTURED SUMMARY:
{"classification":"...","confidence":"...","evidence":[...],"uncertainties":[...],"unusual_changes":[...],"recommended_action":"...","location":"...","requires_human_review":true}
Rules:
- If the evidence is incomplete, say so.
- If there is no clear smoke, thermal, or visual evidence, be cautious.
- Use precise operational language.
- Keep the structured summary valid JSON on one line.
"""
def extract_signal_summary(report: str) -> str:
"""Extract a simple signal summary from a wildfire report.
Args:
report: Free-form wildfire sensor and observation report text.
Returns:
Short bullet-style summary of detected keywords and values.
"""
text = report[:MAX_CHARS]
findings: List[str] = []
patterns = {
"temperature": r"(?:temperature|temp)\s*[:=]?\s*([-+]?\d+(?:\.\d+)?)",
"humidity": r"humidity\s*[:=]?\s*(\d+(?:\.\d+)?)\s*%",
"pm": r"(?:pm2\.5|pm10|particulate|particulates)\s*[:=]?\s*([-+]?\d+(?:\.\d+)?)",
"thermal": r"(?:thermal|hotspot|heat signature)[^\n]{0,50}?([-+]?\d+(?:\.\d+)?)\s*(?:c|f)?",
"gps": r"(-?\d{1,3}\.\d+)\s*,\s*(-?\d{1,3}\.\d+)"
}
for label, pattern in patterns.items():
match = re.search(pattern, text, flags=re.IGNORECASE)
if match:
if label == "gps":
findings.append(f"{label}: {match.group(1)}, {match.group(2)}")
else:
findings.append(f"{label}: {match.group(1)}")
keyword_flags = [
"smoke", "plume", "ash", "burning", "flame", "glow", "hotspot",
"wind", "gust", "dry brush", "dry vegetation", "rapid increase", "camera"
]
for keyword in keyword_flags:
if keyword in text.lower():
findings.append(f"keyword: {keyword}")
if not findings:
return "- No distinct signal keywords or values were automatically extracted."
return "\n".join(f"- {item}" for item in findings[:20])
async def process_user_query(query: str) -> str:
"""Analyze a wildfire monitoring report and return a structured assessment.
Args:
query: User-supplied wildfire report plus operating preferences.
Returns:
Structured wildfire risk assessment text.
"""
trimmed = query[:MAX_CHARS]
signal_summary = extract_signal_summary(trimmed)
prompt = f"""Analyze this wildfire monitoring report.
Signal summary extracted from the report:
{signal_summary}
Full report:
{trimmed}
Instructions:
- Base the decision on combined evidence.
- Explicitly separate evidence from uncertainty.
- Mention unusual or rapidly changing conditions.
- Recommend the next best action.
- Be careful not to overstate confirmation.
- Follow the exact output structure from the system prompt.
"""
answer = await agentop_llm.generate(prompt, globals().get("TEMPLATE_SYSTEM_PROMPT", ""))
if not isinstance(answer, str):
return str(answer)
cleaned = answer.strip()
try:
json_match = re.search(r"\{.*\}", cleaned, flags=re.DOTALL)
if json_match:
parsed = json.loads(json_match.group(0))
parsed.setdefault("requires_human_review", True)
except Exception:
pass
return cleaned
Name your agent
Based on . You'll get your own private copy to try and customize.