mirror of https://github.com/kortix-ai/suna.git
auto kill other active agent runs on start of new
This commit is contained in:
parent
12c37c663b
commit
6f9db5ba88
|
@ -87,13 +87,14 @@ async def restore_running_agent_runs():
|
||||||
async def check_for_active_project_agent_run(client, project_id: str):
|
async def check_for_active_project_agent_run(client, project_id: str):
|
||||||
"""
|
"""
|
||||||
Check if there is an active agent run for any thread in the given project.
|
Check if there is an active agent run for any thread in the given project.
|
||||||
|
If found, returns the ID of the active run, otherwise returns None.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
client: The Supabase client
|
client: The Supabase client
|
||||||
project_id: The project ID to check
|
project_id: The project ID to check
|
||||||
|
|
||||||
Raises:
|
Returns:
|
||||||
HTTPException: If an agent run is already active for the project
|
str or None: The ID of the active agent run if found, None otherwise
|
||||||
"""
|
"""
|
||||||
# Get all threads from this project
|
# Get all threads from this project
|
||||||
project_threads = await client.table('threads').select('thread_id').eq('project_id', project_id).execute()
|
project_threads = await client.table('threads').select('thread_id').eq('project_id', project_id).execute()
|
||||||
|
@ -104,10 +105,9 @@ async def check_for_active_project_agent_run(client, project_id: str):
|
||||||
active_runs = await client.table('agent_runs').select('id').in_('thread_id', project_thread_ids).eq('status', 'running').execute()
|
active_runs = await client.table('agent_runs').select('id').in_('thread_id', project_thread_ids).eq('status', 'running').execute()
|
||||||
|
|
||||||
if active_runs.data and len(active_runs.data) > 0:
|
if active_runs.data and len(active_runs.data) > 0:
|
||||||
raise HTTPException(
|
return active_runs.data[0]['id']
|
||||||
status_code=409,
|
|
||||||
detail="Another agent is already running for this project. Please wait for it to complete or stop it before starting a new one."
|
return None
|
||||||
)
|
|
||||||
|
|
||||||
@router.post("/thread/{thread_id}/agent/start")
|
@router.post("/thread/{thread_id}/agent/start")
|
||||||
async def start_agent(thread_id: str, user_id: str = Depends(get_current_user_id)):
|
async def start_agent(thread_id: str, user_id: str = Depends(get_current_user_id)):
|
||||||
|
@ -127,7 +127,12 @@ async def start_agent(thread_id: str, user_id: str = Depends(get_current_user_id
|
||||||
project_id = thread_result.data[0]['project_id']
|
project_id = thread_result.data[0]['project_id']
|
||||||
|
|
||||||
# Check if there is already an active agent run for this project
|
# Check if there is already an active agent run for this project
|
||||||
await check_for_active_project_agent_run(client, project_id)
|
active_run_id = await check_for_active_project_agent_run(client, project_id)
|
||||||
|
|
||||||
|
# If there's an active run, stop it first
|
||||||
|
if active_run_id:
|
||||||
|
logger.info(f"Stopping existing agent run {active_run_id} before starting new one")
|
||||||
|
await stop_agent_run(active_run_id)
|
||||||
|
|
||||||
# Create a new agent run
|
# Create a new agent run
|
||||||
agent_run = await client.table('agent_runs').insert({
|
agent_run = await client.table('agent_runs').insert({
|
||||||
|
|
|
@ -0,0 +1,99 @@
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const galleryContainer = document.getElementById('galleryContainer');
|
||||||
|
const galleryImages = document.getElementById('galleryImages');
|
||||||
|
const prevBtn = document.getElementById('galleryPrev');
|
||||||
|
const nextBtn = document.getElementById('galleryNext');
|
||||||
|
const imageUpload = document.getElementById('imageUpload');
|
||||||
|
const uploadBtn = document.getElementById('uploadImageBtn');
|
||||||
|
|
||||||
|
// Sample images for demo
|
||||||
|
const demoImages = [
|
||||||
|
'https://source.unsplash.com/random/300x200?nature,1',
|
||||||
|
'https://source.unsplash.com/random/300x200?city,1',
|
||||||
|
'https://source.unsplash.com/random/300x200?technology,1',
|
||||||
|
'https://source.unsplash.com/random/300x200?animals,1'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Load images from localStorage or use demo images
|
||||||
|
let images = JSON.parse(localStorage.getItem('galleryImages')) || demoImages;
|
||||||
|
let currentIndex = 0;
|
||||||
|
|
||||||
|
// Initialize gallery
|
||||||
|
renderGallery();
|
||||||
|
|
||||||
|
// Event listeners
|
||||||
|
prevBtn.addEventListener('click', showPreviousImage);
|
||||||
|
nextBtn.addEventListener('click', showNextImage);
|
||||||
|
uploadBtn.addEventListener('click', handleImageUpload);
|
||||||
|
|
||||||
|
function renderGallery() {
|
||||||
|
if (images.length === 0) {
|
||||||
|
galleryImages.innerHTML = '<p class="gallery-empty">No images in gallery. Add some!</p>';
|
||||||
|
prevBtn.disabled = true;
|
||||||
|
nextBtn.disabled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showImage(currentIndex);
|
||||||
|
updateButtons();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showImage(index) {
|
||||||
|
galleryImages.innerHTML = '';
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.src = images[index];
|
||||||
|
img.alt = `Gallery image ${index + 1}`;
|
||||||
|
img.className = 'gallery-img';
|
||||||
|
|
||||||
|
const counter = document.createElement('div');
|
||||||
|
counter.className = 'gallery-counter';
|
||||||
|
counter.textContent = `${index + 1} / ${images.length}`;
|
||||||
|
|
||||||
|
galleryImages.appendChild(img);
|
||||||
|
galleryImages.appendChild(counter);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showPreviousImage() {
|
||||||
|
currentIndex = (currentIndex - 1 + images.length) % images.length;
|
||||||
|
showImage(currentIndex);
|
||||||
|
updateButtons();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showNextImage() {
|
||||||
|
currentIndex = (currentIndex + 1) % images.length;
|
||||||
|
showImage(currentIndex);
|
||||||
|
updateButtons();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateButtons() {
|
||||||
|
prevBtn.disabled = images.length <= 1;
|
||||||
|
nextBtn.disabled = images.length <= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleImageUpload() {
|
||||||
|
const imageUrl = imageUpload.value.trim();
|
||||||
|
if (imageUrl) {
|
||||||
|
// Simple validation - check if it looks like a URL
|
||||||
|
if (imageUrl.startsWith('http') && (imageUrl.endsWith('.jpg') ||
|
||||||
|
imageUrl.endsWith('.jpeg') || imageUrl.endsWith('.png') ||
|
||||||
|
imageUrl.endsWith('.gif') || imageUrl.includes('unsplash'))) {
|
||||||
|
|
||||||
|
addImage(imageUrl);
|
||||||
|
imageUpload.value = '';
|
||||||
|
} else {
|
||||||
|
alert('Please enter a valid image URL (ending with .jpg, .jpeg, .png, or .gif)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addImage(url) {
|
||||||
|
images.push(url);
|
||||||
|
saveImages();
|
||||||
|
currentIndex = images.length - 1; // Show the newly added image
|
||||||
|
renderGallery();
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveImages() {
|
||||||
|
localStorage.setItem('galleryImages', JSON.stringify(images));
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,315 @@
|
||||||
|
// Test Runner Script
|
||||||
|
console.log("Test Runner initialized");
|
||||||
|
|
||||||
|
// Function to run all tests automatically
|
||||||
|
function autoRunTests() {
|
||||||
|
console.log("Auto-running tests...");
|
||||||
|
|
||||||
|
// Tests to run in sequence
|
||||||
|
const tests = [
|
||||||
|
testColorChanger,
|
||||||
|
testCounter,
|
||||||
|
testTodoList,
|
||||||
|
testWeatherWidget,
|
||||||
|
testGallery
|
||||||
|
];
|
||||||
|
|
||||||
|
// Run tests sequentially
|
||||||
|
runTestSequence(tests, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run tests in sequence
|
||||||
|
function runTestSequence(tests, index) {
|
||||||
|
if (index >= tests.length) {
|
||||||
|
console.log("All tests completed!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Running test ${index + 1}/${tests.length}...`);
|
||||||
|
|
||||||
|
// Run current test
|
||||||
|
const currentTest = tests[index];
|
||||||
|
currentTest(() => {
|
||||||
|
// When test completes, run next test
|
||||||
|
runTestSequence(tests, index + 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test Color Changer
|
||||||
|
function testColorChanger(callback) {
|
||||||
|
console.log("Testing Color Changer...");
|
||||||
|
|
||||||
|
const changeColorBtn = document.getElementById('changeColorBtn');
|
||||||
|
const colorDisplay = document.getElementById('colorDisplay');
|
||||||
|
|
||||||
|
if (!changeColorBtn || !colorDisplay) {
|
||||||
|
console.error("Color changer elements not found!");
|
||||||
|
if (callback) callback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get initial state
|
||||||
|
const initialColor = colorDisplay.style.backgroundColor;
|
||||||
|
console.log(`Initial color: ${initialColor || 'none'}`);
|
||||||
|
|
||||||
|
// Click button
|
||||||
|
changeColorBtn.click();
|
||||||
|
|
||||||
|
// Check result
|
||||||
|
setTimeout(() => {
|
||||||
|
const newColor = colorDisplay.style.backgroundColor;
|
||||||
|
console.log(`New color: ${newColor}`);
|
||||||
|
|
||||||
|
if (newColor && newColor !== initialColor) {
|
||||||
|
console.log("✅ Color changer test passed!");
|
||||||
|
} else {
|
||||||
|
console.error("❌ Color changer test failed!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (callback) callback();
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test Counter
|
||||||
|
function testCounter(callback) {
|
||||||
|
console.log("Testing Counter...");
|
||||||
|
|
||||||
|
const decrementBtn = document.getElementById('decrementBtn');
|
||||||
|
const incrementBtn = document.getElementById('incrementBtn');
|
||||||
|
const counterValue = document.getElementById('counterValue');
|
||||||
|
|
||||||
|
if (!decrementBtn || !incrementBtn || !counterValue) {
|
||||||
|
console.error("Counter elements not found!");
|
||||||
|
if (callback) callback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get initial value
|
||||||
|
const initialValue = parseInt(counterValue.textContent);
|
||||||
|
console.log(`Initial counter value: ${initialValue}`);
|
||||||
|
|
||||||
|
// Test increment
|
||||||
|
incrementBtn.click();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
const afterIncrement = parseInt(counterValue.textContent);
|
||||||
|
console.log(`After increment: ${afterIncrement}`);
|
||||||
|
|
||||||
|
if (afterIncrement === initialValue + 1) {
|
||||||
|
console.log("✅ Increment test passed!");
|
||||||
|
} else {
|
||||||
|
console.error("❌ Increment test failed!");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test decrement
|
||||||
|
decrementBtn.click();
|
||||||
|
decrementBtn.click();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
const afterDecrement = parseInt(counterValue.textContent);
|
||||||
|
console.log(`After decrement: ${afterDecrement}`);
|
||||||
|
|
||||||
|
if (afterDecrement === initialValue - 1) {
|
||||||
|
console.log("✅ Decrement test passed!");
|
||||||
|
} else {
|
||||||
|
console.error("❌ Decrement test failed!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (callback) callback();
|
||||||
|
}, 500);
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test Todo List
|
||||||
|
function testTodoList(callback) {
|
||||||
|
console.log("Testing Todo List...");
|
||||||
|
|
||||||
|
const todoInput = document.getElementById('todoInput');
|
||||||
|
const addTodoBtn = document.getElementById('addTodoBtn');
|
||||||
|
const todoList = document.getElementById('todoList');
|
||||||
|
|
||||||
|
if (!todoInput || !addTodoBtn || !todoList) {
|
||||||
|
console.error("Todo list elements not found!");
|
||||||
|
if (callback) callback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get initial todo count
|
||||||
|
const initialCount = todoList.querySelectorAll('.todo-item').length;
|
||||||
|
console.log(`Initial todo count: ${initialCount}`);
|
||||||
|
|
||||||
|
// Add a new todo
|
||||||
|
const testTodoText = `Test Todo ${Date.now()}`;
|
||||||
|
todoInput.value = testTodoText;
|
||||||
|
addTodoBtn.click();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
// Check if todo was added
|
||||||
|
const newCount = todoList.querySelectorAll('.todo-item').length;
|
||||||
|
console.log(`New todo count: ${newCount}`);
|
||||||
|
|
||||||
|
if (newCount === initialCount + 1) {
|
||||||
|
console.log("✅ Add todo test passed!");
|
||||||
|
|
||||||
|
// Get the last todo item
|
||||||
|
const lastTodo = todoList.querySelector('.todo-item:last-child');
|
||||||
|
const checkbox = lastTodo.querySelector('input[type="checkbox"]');
|
||||||
|
const deleteBtn = lastTodo.querySelector('.delete-btn');
|
||||||
|
|
||||||
|
// Test toggle functionality
|
||||||
|
checkbox.click();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (lastTodo.classList.contains('completed')) {
|
||||||
|
console.log("✅ Toggle todo test passed!");
|
||||||
|
} else {
|
||||||
|
console.error("❌ Toggle todo test failed!");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test delete functionality
|
||||||
|
deleteBtn.click();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
const finalCount = todoList.querySelectorAll('.todo-item').length;
|
||||||
|
console.log(`Final todo count: ${finalCount}`);
|
||||||
|
|
||||||
|
if (finalCount === initialCount) {
|
||||||
|
console.log("✅ Delete todo test passed!");
|
||||||
|
} else {
|
||||||
|
console.error("❌ Delete todo test failed!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (callback) callback();
|
||||||
|
}, 500);
|
||||||
|
}, 500);
|
||||||
|
} else {
|
||||||
|
console.error("❌ Add todo test failed!");
|
||||||
|
if (callback) callback();
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test Weather Widget
|
||||||
|
function testWeatherWidget(callback) {
|
||||||
|
console.log("Testing Weather Widget...");
|
||||||
|
|
||||||
|
const weatherCity = document.getElementById('weatherCity');
|
||||||
|
const weatherForm = document.getElementById('weatherForm');
|
||||||
|
const weatherDisplay = document.getElementById('weatherDisplay');
|
||||||
|
|
||||||
|
if (!weatherCity || !weatherForm || !weatherDisplay) {
|
||||||
|
console.error("Weather widget elements not found!");
|
||||||
|
if (callback) callback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if initial weather is displayed
|
||||||
|
const initialWeatherInfo = weatherDisplay.querySelector('.weather-info');
|
||||||
|
|
||||||
|
if (initialWeatherInfo) {
|
||||||
|
console.log("✅ Initial weather display test passed!");
|
||||||
|
} else {
|
||||||
|
console.log("❓ No initial weather displayed, will test form submission");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test form submission
|
||||||
|
weatherCity.value = "Tokyo";
|
||||||
|
|
||||||
|
// Create and dispatch submit event
|
||||||
|
const submitEvent = new Event('submit');
|
||||||
|
weatherForm.dispatchEvent(submitEvent);
|
||||||
|
|
||||||
|
console.log("Weather form submitted with city: Tokyo");
|
||||||
|
|
||||||
|
// Wait for API response
|
||||||
|
setTimeout(() => {
|
||||||
|
const weatherInfo = weatherDisplay.querySelector('.weather-info');
|
||||||
|
|
||||||
|
if (weatherInfo) {
|
||||||
|
const cityName = weatherInfo.querySelector('h3');
|
||||||
|
const temperature = weatherInfo.querySelector('.temperature');
|
||||||
|
|
||||||
|
if (cityName && temperature) {
|
||||||
|
console.log("✅ Weather API test passed!");
|
||||||
|
console.log(`City: ${cityName.textContent}, Temperature: ${temperature.textContent}`);
|
||||||
|
} else {
|
||||||
|
console.error("❌ Weather display structure test failed!");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error("❌ Weather API test failed!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (callback) callback();
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test Gallery
|
||||||
|
function testGallery(callback) {
|
||||||
|
console.log("Testing Image Gallery...");
|
||||||
|
|
||||||
|
const galleryImages = document.getElementById('galleryImages');
|
||||||
|
const prevBtn = document.getElementById('galleryPrev');
|
||||||
|
const nextBtn = document.getElementById('galleryNext');
|
||||||
|
const imageUpload = document.getElementById('imageUpload');
|
||||||
|
const uploadBtn = document.getElementById('uploadImageBtn');
|
||||||
|
|
||||||
|
if (!galleryImages || !prevBtn || !nextBtn || !imageUpload || !uploadBtn) {
|
||||||
|
console.error("Gallery elements not found!");
|
||||||
|
if (callback) callback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if initial gallery is displayed
|
||||||
|
const initialImage = galleryImages.querySelector('.gallery-img');
|
||||||
|
|
||||||
|
if (initialImage) {
|
||||||
|
console.log("✅ Initial gallery display test passed!");
|
||||||
|
|
||||||
|
// Test navigation
|
||||||
|
const initialCounter = galleryImages.querySelector('.gallery-counter').textContent;
|
||||||
|
console.log(`Initial image: ${initialCounter}`);
|
||||||
|
|
||||||
|
// Test next button
|
||||||
|
nextBtn.click();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
const newCounter = galleryImages.querySelector('.gallery-counter').textContent;
|
||||||
|
console.log(`After next: ${newCounter}`);
|
||||||
|
|
||||||
|
if (newCounter !== initialCounter) {
|
||||||
|
console.log("✅ Gallery navigation test passed!");
|
||||||
|
} else {
|
||||||
|
console.error("❌ Gallery navigation test failed!");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test image upload
|
||||||
|
const testImageUrl = "https://source.unsplash.com/random/300x200?test";
|
||||||
|
imageUpload.value = testImageUrl;
|
||||||
|
uploadBtn.click();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
const finalCounter = galleryImages.querySelector('.gallery-counter').textContent;
|
||||||
|
console.log(`After upload: ${finalCounter}`);
|
||||||
|
|
||||||
|
if (finalCounter.includes(testImageUrl)) {
|
||||||
|
console.log("✅ Image upload test passed!");
|
||||||
|
} else {
|
||||||
|
console.log("⚠️ Image upload test inconclusive - can't verify image URL");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (callback) callback();
|
||||||
|
}, 500);
|
||||||
|
}, 500);
|
||||||
|
} else {
|
||||||
|
console.log("⚠️ No initial gallery image displayed");
|
||||||
|
if (callback) callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start tests when page is fully loaded
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
console.log("DOM loaded, waiting for all resources...");
|
||||||
|
|
||||||
|
// Wait a bit longer to ensure all scripts are initialized
|
||||||
|
setTimeout(autoRunTests, 1000);
|
||||||
|
});
|
|
@ -0,0 +1,54 @@
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const weatherWidget = document.getElementById('weatherWidget');
|
||||||
|
const weatherCity = document.getElementById('weatherCity');
|
||||||
|
const weatherForm = document.getElementById('weatherForm');
|
||||||
|
const weatherDisplay = document.getElementById('weatherDisplay');
|
||||||
|
|
||||||
|
weatherForm.addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const city = weatherCity.value.trim();
|
||||||
|
if (city) {
|
||||||
|
fetchWeather(city);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function fetchWeather(city) {
|
||||||
|
try {
|
||||||
|
weatherDisplay.innerHTML = '<p>Loading weather data...</p>';
|
||||||
|
|
||||||
|
// Using OpenWeatherMap API with a free tier (you would need to replace with your own API key in production)
|
||||||
|
const apiKey = 'demo'; // Using demo mode for example purposes
|
||||||
|
const response = await fetch(`https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${apiKey}&units=metric`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('City not found or API limit reached');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
displayWeather(data);
|
||||||
|
} catch (error) {
|
||||||
|
weatherDisplay.innerHTML = `<p class="weather-error">Error: ${error.message}</p>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayWeather(data) {
|
||||||
|
// For demo purposes, we'll show a simplified version since the API key is demo
|
||||||
|
weatherDisplay.innerHTML = `
|
||||||
|
<div class="weather-info">
|
||||||
|
<h3>${data.name}, ${data.sys.country}</h3>
|
||||||
|
<div class="weather-main">
|
||||||
|
<span class="temperature">${Math.round(data.main.temp)}°C</span>
|
||||||
|
<span class="description">${data.weather[0].description}</span>
|
||||||
|
</div>
|
||||||
|
<div class="weather-details">
|
||||||
|
<p>Feels like: ${Math.round(data.main.feels_like)}°C</p>
|
||||||
|
<p>Humidity: ${data.main.humidity}%</p>
|
||||||
|
<p>Wind: ${data.wind.speed} m/s</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize with a default city
|
||||||
|
fetchWeather('London');
|
||||||
|
});
|
Loading…
Reference in New Issue