@ -1,11 +1,17 @@
|
||||
# Reading the Docs
|
||||
|
||||
## Instructions
|
||||
|
||||
There are many tools that a web developer may need that are on the [MDN documentation for client-side tooling](https://developer.mozilla.org/docs/Learn/Tools_and_testing/Understanding_client-side_tools/Overview). Select 3 tools not covered in the lesson, explain why a web developer would use it, and search for a tool that falls under this category and share its documentation. Do not use the same tool example on MDN docs.
|
||||
There are many tools that a web developer may need that are listed on the [MDN documentation for client-side tooling](https://developer.mozilla.org/docs/Learn/Tools_and_testing/Understanding_client-side_tools/Overview). Select **three tools** that are **not covered in this lesson** (excluding [list specific tools or refer to lesson content]), explain **why** a web developer would use each tool, and find a tool that fits each category. For each, share a link to its official documentation (not the example used on MDN).
|
||||
|
||||
**Format:**
|
||||
- Tool name
|
||||
- Why a web developer would use it (2-3 sentences)
|
||||
- Link to documentation
|
||||
|
||||
**Length:**
|
||||
- Each explanation should be 2-3 sentences.
|
||||
|
||||
## Rubric
|
||||
|
||||
Exemplary | Adequate | Needs Improvement
|
||||
--- | --- | -- |
|
||||
|Explained why web developer would use tool| Explained how, but not why developer would use tool| Did not mention how or why a developer would use tool |
|
||||
Explained why web developer would use tool | Explained how, but not why developer would use tool | Did not mention how or why a developer would use tool |
|
||||
@ -0,0 +1,374 @@
|
||||
# AI Framework
|
||||
|
||||
There are many AI frameworks out there that when used can severely quicken up the time it takes to build a project. In this project we will focus on understanding what problems these frameworks address and build such a project ourselves.
|
||||
|
||||
## Why a framework
|
||||
|
||||
When it comes to using AI there are different approaches and different reasons for choosing these approaches, here are some:
|
||||
|
||||
- **No SDK**, most AI models allows you to interact directly with the AI model via for example HTTP requests. That approach works and may sometimes be your only option if an SDK option is missing.
|
||||
- **SDK**. Using an SDK is usually the recommended approach as it allows you to type less code to interact with your model. It usually is limited to a specific model and if using different models, you might need to write new code to support those additional models.
|
||||
- **A framework**. A framework usually takes things to another level in the sense that if you need to use different models, there's one API for all of them, what differs is usually the initial set up. Additionally frameworks brings in useful abstractions like in the AI space, they can deal with tools, memory, workflows, agents and more while writing less code. Because frameworks are usually opinionated they can really be helpful if you buy into how they do things but may fall short if you try to do something bespoke that the framework isn't made for. Sometimes a framework can also simplify too much and you may therefore not learn an important topic that later may harm performance for example.
|
||||
|
||||
Generally, use the right tool for the job.
|
||||
|
||||
## Introduction
|
||||
|
||||
In this lesson, we'll learn to:
|
||||
|
||||
- Use a common AI framework.
|
||||
- Address common problems like chat conversations, tool usage, memory and context.
|
||||
- Leverage this to build AI apps.
|
||||
|
||||
## First prompt
|
||||
|
||||
In our first app example, we'll learn how to connect to an AI model and query it using a prompt.
|
||||
|
||||
### Using Python
|
||||
|
||||
For this example, we'll use Langchain to connect to GitHub Models. We can use a class called `ChatOpenAI` and give it the fields `api_key`, `base_url` and `model`. The token is something that automatically is populated within GitHub Codespaces and if you're running the app locally, you need to set up a personal access token for this to work.
|
||||
|
||||
```python
|
||||
from langchain_openai import ChatOpenAI
|
||||
import os
|
||||
|
||||
llm = ChatOpenAI(
|
||||
api_key=os.environ["GITHUB_TOKEN"],
|
||||
base_url="https://models.github.ai/inference",
|
||||
model="openai/gpt-4o-mini",
|
||||
)
|
||||
|
||||
# works
|
||||
response = llm.invoke("What's the capital of France?")
|
||||
print(response.content)
|
||||
```
|
||||
|
||||
In this code, we:
|
||||
|
||||
- Call `ChatOpenAI` to create a client.
|
||||
- Use `llm.invoke` with a prompt to create a response
|
||||
- Print the response with `print(response.content)`.
|
||||
|
||||
You should see a response similar to:
|
||||
|
||||
```text
|
||||
The capital of France is Paris.
|
||||
```
|
||||
|
||||
## Chat conversation
|
||||
|
||||
In the preceding section, you saw how we used what's normally known as zero shot prompting, a single prompt followed by a response.
|
||||
|
||||
However, often you find yourself in a situation where you need to maintain a conversation of several messages being exchanged between yourself and the AI assistant.
|
||||
|
||||
### Using Python
|
||||
|
||||
In langchain, we can store the conversation in a list. The `HumanMessage` represents a message from a user, and `SystemMessage` is a message meant to set the "personality" of the AI. In below example you see how we instruct the AI to assume the personality of Captain Picard and for the human/user to ask "Tell me about you" as the prompt.
|
||||
|
||||
```python
|
||||
messages = [
|
||||
SystemMessage(content="You are Captain Picard of the Starship Enterprise"),
|
||||
HumanMessage(content="Tell me about you"),
|
||||
]
|
||||
```
|
||||
|
||||
The full code for this example looks like so:
|
||||
|
||||
```python
|
||||
from langchain_core.messages import HumanMessage, SystemMessage
|
||||
from langchain_openai import ChatOpenAI
|
||||
import os
|
||||
|
||||
llm = ChatOpenAI(
|
||||
api_key=os.environ["GITHUB_TOKEN"],
|
||||
base_url="https://models.github.ai/inference",
|
||||
model="openai/gpt-4o-mini",
|
||||
)
|
||||
|
||||
messages = [
|
||||
SystemMessage(content="You are Captain Picard of the Starship Enterprise"),
|
||||
HumanMessage(content="Tell me about you"),
|
||||
]
|
||||
|
||||
|
||||
# works
|
||||
response = llm.invoke(messages)
|
||||
print(response.content)
|
||||
```
|
||||
|
||||
You should see an outcome similar to:
|
||||
|
||||
```text
|
||||
I am Captain Jean-Luc Picard, the commanding officer of the USS Enterprise (NCC-1701-D), a starship in the United Federation of Planets. My primary mission is to explore new worlds, seek out new life and new civilizations, and boldly go where no one has gone before.
|
||||
|
||||
I believe in the importance of diplomacy, reason, and the pursuit of knowledge. My crew is diverse and skilled, and we often face challenges that test our resolve, ethics, and ingenuity. Throughout my career, I have encountered numerous species, grappled with complex moral dilemmas, and have consistently sought peaceful solutions to conflicts.
|
||||
|
||||
I hold the ideals of the Federation close to my heart, believing in the importance of cooperation, understanding, and respect for all sentient beings. My experiences have shaped my leadership style, and I strive to be a thoughtful and just captain. How may I assist you further?
|
||||
```
|
||||
|
||||
To keep the state of the conversation, you can add the response from a chat, so conversation is remembered, here's how to do that:
|
||||
|
||||
```python
|
||||
from langchain_core.messages import HumanMessage, SystemMessage
|
||||
from langchain_openai import ChatOpenAI
|
||||
import os
|
||||
|
||||
llm = ChatOpenAI(
|
||||
api_key=os.environ["GITHUB_TOKEN"],
|
||||
base_url="https://models.github.ai/inference",
|
||||
model="openai/gpt-4o-mini",
|
||||
)
|
||||
|
||||
messages = [
|
||||
SystemMessage(content="You are Captain Picard of the Starship Enterprise"),
|
||||
HumanMessage(content="Tell me about you"),
|
||||
]
|
||||
|
||||
|
||||
# works
|
||||
response = llm.invoke(messages)
|
||||
|
||||
print(response.content)
|
||||
|
||||
print("---- Next ----")
|
||||
|
||||
messages.append(response)
|
||||
messages.append(HumanMessage(content="Now that I know about you, I'm Chris, can I be in your crew?"))
|
||||
|
||||
response = llm.invoke(messages)
|
||||
|
||||
print(response.content)
|
||||
|
||||
```
|
||||
|
||||
What we can see from the above conversation is how we invoke the LLM two times, first with the conversation consisting of just two messages but then a second time with more messages added to the conversation.
|
||||
|
||||
In fact, if you run this, you will see the second response being something like:
|
||||
|
||||
```text
|
||||
Welcome aboard, Chris! It's always a pleasure to meet those who share a passion for exploration and discovery. While I cannot formally offer you a position on the Enterprise right now, I encourage you to pursue your aspirations. We are always in need of talented individuals with diverse skills and backgrounds.
|
||||
|
||||
If you are interested in space exploration, consider education and training in the sciences, engineering, or diplomacy. The values of curiosity, resilience, and teamwork are crucial in Starfleet. Should you ever find yourself on a starship, remember to uphold the principles of the Federation: peace, understanding, and respect for all beings. Your journey can lead you to remarkable adventures, whether in the stars or on the ground. Engage!
|
||||
```
|
||||
|
||||
I'll take that as a maybe ;)
|
||||
|
||||
## Streaming responses
|
||||
|
||||
TODO
|
||||
|
||||
## Prompt templates
|
||||
|
||||
TODO
|
||||
|
||||
## Structured output
|
||||
|
||||
TODO
|
||||
|
||||
## Tool calling
|
||||
|
||||
Tools are how we give the LLM extra skills. The idea is to tell the LLM about functions it has and if a prompt is made that matches the description of one of these tools then we call them.
|
||||
|
||||
### Using Python
|
||||
|
||||
Let's add some tools like so:
|
||||
|
||||
```python
|
||||
from typing_extensions import Annotated, TypedDict
|
||||
|
||||
class add(TypedDict):
|
||||
"""Add two integers."""
|
||||
|
||||
# Annotations must have the type and can optionally include a default value and description (in that order).
|
||||
a: Annotated[int, ..., "First integer"]
|
||||
b: Annotated[int, ..., "Second integer"]
|
||||
|
||||
tools = [add]
|
||||
|
||||
functions = {
|
||||
"add": lambda a, b: a + b
|
||||
}
|
||||
```
|
||||
|
||||
What we're doing here is to create a description of a tool called `add`. By inheriting from `TypedDict` and adding members like `a` and `b` of type `Annotated` this can be converted to a schema that the LLM can understand. The creation of functions is a dictionary that ensures that we know what to do if a specific tool is identified.
|
||||
|
||||
Let's see how we call the LLM with this tool next:
|
||||
|
||||
```python
|
||||
llm = ChatOpenAI(
|
||||
api_key=os.environ["GITHUB_TOKEN"],
|
||||
base_url="https://models.github.ai/inference",
|
||||
model="openai/gpt-4o-mini",
|
||||
)
|
||||
|
||||
llm_with_tools = llm.bind_tools(tools)
|
||||
```
|
||||
|
||||
Here we call `bind_tools` with our `tools` array and thereby the LLM `llm_with_tools` now has knowledge of this tool.
|
||||
|
||||
To use this new LLM, we can type the following code:
|
||||
|
||||
```python
|
||||
query = "What is 3 + 12?"
|
||||
|
||||
res = llm_with_tools.invoke(query)
|
||||
if(res.tool_calls):
|
||||
for tool in res.tool_calls:
|
||||
print("TOOL CALL: ", functions[tool["name"]](**tool["args"]))
|
||||
print("CONTENT: ",res.content)
|
||||
```
|
||||
|
||||
Now that we call `invoke` on this new llm, that has tools, we maybe the the property `tool_calls` populated. If so, any identified tools has a `name` and `args` property that identifies what tool should be called and with arguments. The full code looks like so:
|
||||
|
||||
```python
|
||||
from langchain_core.messages import HumanMessage, SystemMessage
|
||||
from langchain_openai import ChatOpenAI
|
||||
import os
|
||||
from typing_extensions import Annotated, TypedDict
|
||||
|
||||
class add(TypedDict):
|
||||
"""Add two integers."""
|
||||
|
||||
# Annotations must have the type and can optionally include a default value and description (in that order).
|
||||
a: Annotated[int, ..., "First integer"]
|
||||
b: Annotated[int, ..., "Second integer"]
|
||||
|
||||
tools = [add]
|
||||
|
||||
functions = {
|
||||
"add": lambda a, b: a + b
|
||||
}
|
||||
|
||||
llm = ChatOpenAI(
|
||||
api_key=os.environ["GITHUB_TOKEN"],
|
||||
base_url="https://models.github.ai/inference",
|
||||
model="openai/gpt-4o-mini",
|
||||
)
|
||||
|
||||
llm_with_tools = llm.bind_tools(tools)
|
||||
|
||||
query = "What is 3 + 12?"
|
||||
|
||||
res = llm_with_tools.invoke(query)
|
||||
if(res.tool_calls):
|
||||
for tool in res.tool_calls:
|
||||
print("TOOL CALL: ", functions[tool["name"]](**tool["args"]))
|
||||
print("CONTENT: ",res.content)
|
||||
```
|
||||
|
||||
Running this code, you should see output similar to:
|
||||
|
||||
```text
|
||||
TOOL CALL: 15
|
||||
CONTENT:
|
||||
```
|
||||
|
||||
What this output mean is that the LLM analyzed the prompt "What is 3 + 12" as meaning that the `add` tool should be called and it knew that thanks to its name, description and member field descriptions. That the answer is 15 is because of our code using the dictionary `functions` to invoke it:
|
||||
|
||||
```python
|
||||
print("TOOL CALL: ", functions[tool["name"]](**tool["args"]))
|
||||
```
|
||||
|
||||
### A more interesting tool that calls a web api
|
||||
|
||||
Tools that adds two numbers is interesting as it illustrates how tool calling works but usually tools tend to do something more interesting like for example calling a Web API, let's do just that with this code:
|
||||
|
||||
```python
|
||||
class joke(TypedDict):
|
||||
"""Tell a joke."""
|
||||
|
||||
# Annotations must have the type and can optionally include a default value and description (in that order).
|
||||
category: Annotated[str, ..., "The joke category"]
|
||||
|
||||
def get_joke(category: str) -> str:
|
||||
response = requests.get(f"https://api.chucknorris.io/jokes/random?category={category}", headers={"Accept": "application/json"})
|
||||
if response.status_code == 200:
|
||||
return response.json().get("value", f"Here's a {category} joke!")
|
||||
return f"Here's a {category} joke!"
|
||||
|
||||
functions = {
|
||||
"add": lambda a, b: a + b,
|
||||
"joke": lambda category: get_joke(category)
|
||||
}
|
||||
|
||||
query = "Tell me a joke about animals"
|
||||
|
||||
# the rest of the code is the same
|
||||
```
|
||||
|
||||
Now if you run this code you will get a response saying something like:
|
||||
|
||||
```text
|
||||
TOOL CALL: Chuck Norris once rode a nine foot grizzly bear through an automatic car wash, instead of taking a shower.
|
||||
CONTENT:
|
||||
```
|
||||
|
||||
Here's the code in its entirety:
|
||||
|
||||
```python
|
||||
from langchain_openai import ChatOpenAI
|
||||
import requests
|
||||
import os
|
||||
from typing_extensions import Annotated, TypedDict
|
||||
|
||||
class add(TypedDict):
|
||||
"""Add two integers."""
|
||||
|
||||
# Annotations must have the type and can optionally include a default value and description (in that order).
|
||||
a: Annotated[int, ..., "First integer"]
|
||||
b: Annotated[int, ..., "Second integer"]
|
||||
|
||||
class joke(TypedDict):
|
||||
"""Tell a joke."""
|
||||
|
||||
# Annotations must have the type and can optionally include a default value and description (in that order).
|
||||
category: Annotated[str, ..., "The joke category"]
|
||||
|
||||
tools = [add, joke]
|
||||
|
||||
def get_joke(category: str) -> str:
|
||||
response = requests.get(f"https://api.chucknorris.io/jokes/random?category={category}", headers={"Accept": "application/json"})
|
||||
if response.status_code == 200:
|
||||
return response.json().get("value", f"Here's a {category} joke!")
|
||||
return f"Here's a {category} joke!"
|
||||
|
||||
functions = {
|
||||
"add": lambda a, b: a + b,
|
||||
"joke": lambda category: get_joke(category)
|
||||
}
|
||||
|
||||
llm = ChatOpenAI(
|
||||
api_key=os.environ["GITHUB_TOKEN"],
|
||||
base_url="https://models.github.ai/inference",
|
||||
model="openai/gpt-4o-mini",
|
||||
)
|
||||
|
||||
llm_with_tools = llm.bind_tools(tools)
|
||||
|
||||
query = "Tell me a joke about animals"
|
||||
|
||||
res = llm_with_tools.invoke(query)
|
||||
if(res.tool_calls):
|
||||
for tool in res.tool_calls:
|
||||
# print("TOOL CALL: ", tool)
|
||||
print("TOOL CALL: ", functions[tool["name"]](**tool["args"]))
|
||||
print("CONTENT: ",res.content)
|
||||
```
|
||||
|
||||
## Embedding
|
||||
|
||||
vectorize content, compare via cosine similarity
|
||||
|
||||
https://python.langchain.com/docs/how_to/embed_text/
|
||||
|
||||
### document loaders
|
||||
|
||||
pdf and csv
|
||||
|
||||
## Building an app
|
||||
|
||||
TODO
|
||||
|
||||
## Assignment
|
||||
|
||||
## Summary
|
||||
@ -0,0 +1,29 @@
|
||||
from langchain_core.messages import HumanMessage, SystemMessage
|
||||
from langchain_openai import ChatOpenAI
|
||||
import os
|
||||
|
||||
llm = ChatOpenAI(
|
||||
api_key=os.environ["GITHUB_TOKEN"],
|
||||
base_url="https://models.github.ai/inference",
|
||||
model="openai/gpt-4o-mini",
|
||||
)
|
||||
|
||||
messages = [
|
||||
SystemMessage(content="You are Captain Picard of the Starship Enterprise"),
|
||||
HumanMessage(content="Tell me about you"),
|
||||
]
|
||||
|
||||
|
||||
# works
|
||||
response = llm.invoke(messages)
|
||||
|
||||
print(response.content)
|
||||
|
||||
print("---- Next ----")
|
||||
|
||||
messages.append(response)
|
||||
messages.append(HumanMessage(content="Now that I know about you, I'm Chris, can I be in your crew?"))
|
||||
|
||||
response = llm.invoke(messages)
|
||||
|
||||
print(response.content)
|
||||
@ -0,0 +1,47 @@
|
||||
from langchain_openai import ChatOpenAI
|
||||
import requests
|
||||
import os
|
||||
from typing_extensions import Annotated, TypedDict
|
||||
|
||||
class add(TypedDict):
|
||||
"""Add two integers."""
|
||||
|
||||
# Annotations must have the type and can optionally include a default value and description (in that order).
|
||||
a: Annotated[int, ..., "First integer"]
|
||||
b: Annotated[int, ..., "Second integer"]
|
||||
|
||||
class joke(TypedDict):
|
||||
"""Tell a joke."""
|
||||
|
||||
# Annotations must have the type and can optionally include a default value and description (in that order).
|
||||
category: Annotated[str, ..., "The joke category"]
|
||||
|
||||
tools = [add, joke]
|
||||
|
||||
def get_joke(category: str) -> str:
|
||||
response = requests.get(f"https://api.chucknorris.io/jokes/random?category={category}", headers={"Accept": "application/json"})
|
||||
if response.status_code == 200:
|
||||
return response.json().get("value", f"Here's a {category} joke!")
|
||||
return f"Here's a {category} joke!"
|
||||
|
||||
functions = {
|
||||
"add": lambda a, b: a + b,
|
||||
"joke": lambda category: get_joke(category)
|
||||
}
|
||||
|
||||
llm = ChatOpenAI(
|
||||
api_key=os.environ["GITHUB_TOKEN"],
|
||||
base_url="https://models.github.ai/inference",
|
||||
model="openai/gpt-4o-mini",
|
||||
)
|
||||
|
||||
llm_with_tools = llm.bind_tools(tools)
|
||||
|
||||
query = "Tell me a joke about animals"
|
||||
|
||||
res = llm_with_tools.invoke(query)
|
||||
if(res.tool_calls):
|
||||
for tool in res.tool_calls:
|
||||
# print("TOOL CALL: ", tool)
|
||||
print("TOOL CALL: ", functions[tool["name"]](**tool["args"]))
|
||||
print("CONTENT: ",res.content)
|
||||
@ -0,0 +1,14 @@
|
||||
# pip install -qU "langchain[openai]"
|
||||
|
||||
from langchain_openai import ChatOpenAI
|
||||
import os
|
||||
|
||||
llm = ChatOpenAI(
|
||||
api_key=os.environ["GITHUB_TOKEN"],
|
||||
base_url="https://models.github.ai/inference",
|
||||
model="openai/gpt-4o-mini",
|
||||
)
|
||||
|
||||
# works
|
||||
response = llm.invoke("What is the capital of France")
|
||||
print(response.content)
|
||||
@ -1,11 +1,27 @@
|
||||
# CSS Refactoring
|
||||
# CSS Refactoring Assignment
|
||||
|
||||
## Objective
|
||||
|
||||
Refactor the terrarium project to use **Flexbox** or **CSS Grid** for layout. Update the HTML and CSS as needed to achieve a modern, responsive design. You do not need to implement draggable elements—focus on layout and styling only.
|
||||
|
||||
## Instructions
|
||||
|
||||
Restyle the terrarium using either Flexbox or CSS Grid, and take screenshots to show that you have tested it on several browsers. You might need to change the markup so create a new version of the app with the art in place for your refactor. Don't worry about making the elements draggable; only refactor the HTML and CSS for now.
|
||||
1. **Create a new version** of the terrarium app. Update the markup and CSS to use Flexbox or CSS Grid for layout.
|
||||
2. **Ensure the art and elements are in place** as in the original version.
|
||||
3. **Test your design** in at least two different browsers (e.g., Chrome, Firefox, Edge).
|
||||
4. **Take screenshots** of your terrarium in each browser to demonstrate cross-browser compatibility.
|
||||
5. **Submit** your updated code and screenshots.
|
||||
|
||||
## Rubric
|
||||
|
||||
| Criteria | Exemplary | Adequate | Needs Improvement |
|
||||
| -------- | ----------------------------------------------------------------- | ----------------------------- | ------------------------------------ |
|
||||
| | Present a completely restyled terrarium using Flexbox or CSS Grid | Restyle a few of the elements | Fail to restyle the terrarium at all |
|
||||
| Criteria | Exemplary | Adequate | Needs Improvement |
|
||||
|------------|--------------------------------------------------------------------------|---------------------------------------|----------------------------------------|
|
||||
| Layout | Fully refactored using Flexbox or CSS Grid; visually appealing and responsive | Some elements refactored; partial use of Flexbox or Grid | Little or no use of Flexbox or Grid; layout unchanged |
|
||||
| Cross-Browser | Screenshots provided for multiple browsers; consistent appearance | Screenshots for one browser; minor inconsistencies | No screenshots or major inconsistencies |
|
||||
| Code Quality | Clean, well-organized HTML/CSS; clear comments | Some organization; few comments | Disorganized code; lacks comments |
|
||||
|
||||
## Tips
|
||||
|
||||
- Review [Flexbox](https://css-tricks.com/snippets/css/a-guide-to-flexbox/) and [CSS Grid](https://css-tricks.com/snippets/css/complete-guide-grid/) guides.
|
||||
- Use browser developer tools to test responsiveness.
|
||||
- Comment your code for clarity.
|
||||
@ -1,76 +1,109 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Welcome to my Virtual Terrarium</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<!-- import the webpage's stylesheet -->
|
||||
<link rel="stylesheet" href="./style.css" />
|
||||
<!-- import the webpage's JavaScript file -->
|
||||
<script src="./script.js" defer></script>
|
||||
</head>
|
||||
<head>
|
||||
<title>Welcome to my Virtual Terrarium</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<!-- Font Awesome CDN for icons -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css" />
|
||||
<!-- import the webpage's stylesheet -->
|
||||
<link rel="stylesheet" href="./style.css" />
|
||||
<!-- import the webpage's JavaScript file -->
|
||||
<script src="./script.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="page">
|
||||
<!-- Navigation Bar -->
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="#"><i class="fas fa-home"></i> Home</a></li>
|
||||
<li><a href="#"><i class="fas fa-leaf"></i> Plants</a></li>
|
||||
<li><a href="#"><i class="fas fa-seedling"></i> Care Tips</a></li>
|
||||
<li><a href="#"><i class="fas fa-info-circle"></i> About</a></li>
|
||||
<li><a href="#"><i class="fas fa-envelope"></i> Contact</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<body>
|
||||
<div id="page">
|
||||
<h1>My Terrarium</h1>
|
||||
<header>
|
||||
<h1><i class="fas fa-jar"></i> My Terrarium</h1>
|
||||
<p>Welcome to your virtual plant paradise! <i class="fas fa-smile-beam"></i></p>
|
||||
</header>
|
||||
|
||||
<div id="left-container" class="container">
|
||||
<div class="plant-holder">
|
||||
<img class="plant" alt="plant" id="plant1" src="./images/plant1.png" />
|
||||
</div>
|
||||
<div class="plant-holder">
|
||||
<img class="plant" alt="plant" id="plant2" src="./images/plant2.png" />
|
||||
</div>
|
||||
<div class="plant-holder">
|
||||
<img class="plant" alt="plant" id="plant3" src="./images/plant3.png" />
|
||||
</div>
|
||||
<div class="plant-holder">
|
||||
<img class="plant" alt="plant" id="plant4" src="./images/plant4.png" />
|
||||
</div>
|
||||
<div class="plant-holder">
|
||||
<img class="plant" alt="plant" id="plant5" src="./images/plant5.png" />
|
||||
</div>
|
||||
<div class="plant-holder">
|
||||
<img class="plant" alt="plant" id="plant6" src="./images/plant6.png" />
|
||||
</div>
|
||||
<div class="plant-holder">
|
||||
<img class="plant" alt="plant" id="plant7" src="./images/plant7.png" />
|
||||
</div>
|
||||
</div>
|
||||
<div id="right-container" class="container">
|
||||
<div class="plant-holder">
|
||||
<img class="plant" alt="plant" id="plant8" src="./images/plant8.png" />
|
||||
</div>
|
||||
<div class="plant-holder">
|
||||
<img class="plant" alt="plant" id="plant9" src="./images/plant9.png" />
|
||||
</div>
|
||||
<div class="plant-holder">
|
||||
<img class="plant" alt="plant" id="plant10" src="./images/plant10.png" />
|
||||
</div>
|
||||
<div class="plant-holder">
|
||||
<img class="plant" alt="plant" id="plant11" src="./images/plant11.png" />
|
||||
</div>
|
||||
<div class="plant-holder">
|
||||
<img class="plant" alt="plant" id="plant12" src="./images/plant12.png" />
|
||||
</div>
|
||||
<div class="plant-holder">
|
||||
<img class="plant" alt="plant" id="plant13" src="./images/plant13.png" />
|
||||
</div>
|
||||
<div class="plant-holder">
|
||||
<img class="plant" alt="plant" id="plant14" src="./images/plant14.png" />
|
||||
</div>
|
||||
</div>
|
||||
<main>
|
||||
<section id="plant-selection">
|
||||
<div id="left-container" class="container">
|
||||
<!-- Left-side plants -->
|
||||
<div class="plant-holder">
|
||||
<img class="plant" alt="plant" id="plant1" src="./images/plant1.png" />
|
||||
</div>
|
||||
<div class="plant-holder">
|
||||
<img class="plant" alt="plant" id="plant2" src="./images/plant2.png" />
|
||||
</div>
|
||||
<div class="plant-holder">
|
||||
<img class="plant" alt="plant" id="plant3" src="./images/plant3.png" />
|
||||
</div>
|
||||
<div class="plant-holder">
|
||||
<img class="plant" alt="plant" id="plant4" src="./images/plant4.png" />
|
||||
</div>
|
||||
<div class="plant-holder">
|
||||
<img class="plant" alt="plant" id="plant5" src="./images/plant5.png" />
|
||||
</div>
|
||||
<div class="plant-holder">
|
||||
<img class="plant" alt="plant" id="plant6" src="./images/plant6.png" />
|
||||
</div>
|
||||
<div class="plant-holder">
|
||||
<img class="plant" alt="plant" id="plant7" src="./images/plant7.png" />
|
||||
</div>
|
||||
</div>
|
||||
<div id="right-container" class="container">
|
||||
<!-- Right-side plants -->
|
||||
<div class="plant-holder">
|
||||
<img class="plant" alt="plant" id="plant8" src="./images/plant8.png" />
|
||||
</div>
|
||||
<div class="plant-holder">
|
||||
<img class="plant" alt="plant" id="plant9" src="./images/plant9.png" />
|
||||
</div>
|
||||
<div class="plant-holder">
|
||||
<img class="plant" alt="plant" id="plant10" src="./images/plant10.png" />
|
||||
</div>
|
||||
<div class="plant-holder">
|
||||
<img class="plant" alt="plant" id="plant11" src="./images/plant11.png" />
|
||||
</div>
|
||||
<div class="plant-holder">
|
||||
<img class="plant" alt="plant" id="plant12" src="./images/plant12.png" />
|
||||
</div>
|
||||
<div class="plant-holder">
|
||||
<img class="plant" alt="plant" id="plant13" src="./images/plant13.png" />
|
||||
</div>
|
||||
<div class="plant-holder">
|
||||
<img class="plant" alt="plant" id="plant14" src="./images/plant14.png" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div id="terrarium">
|
||||
<div class="jar-top"></div>
|
||||
<div class="jar-walls">
|
||||
<div class="jar-glossy-long"></div>
|
||||
<div class="jar-glossy-short"></div>
|
||||
</div>
|
||||
<div class="dirt"></div>
|
||||
<div class="jar-bottom"></div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
<section id="terrarium-section">
|
||||
<div id="terrarium">
|
||||
<div class="jar-top"></div>
|
||||
<div class="jar-walls">
|
||||
<div class="jar-glossy-long"></div>
|
||||
<div class="jar-glossy-short"></div>
|
||||
</div>
|
||||
<div class="dirt"></div>
|
||||
<div class="jar-bottom"></div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<p>
|
||||
<i class="fas fa-copyright"></i> 2025 My Terrarium. All rights reserved.
|
||||
|
|
||||
<a href="https://github.com/" target="_blank"><i class="fab fa-github"></i> GitHub</a>
|
||||
|
|
||||
<a href="mailto:info@myterrarium.com"><i class="fas fa-envelope"></i> Email</a>
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,23 +1,110 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Typing</title>
|
||||
<link rel="stylesheet" href="./index.css" />
|
||||
</head>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Typing</title>
|
||||
<!-- Font Awesome CDN for icons -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css" />
|
||||
<link rel="stylesheet" href="./index.css" />
|
||||
<style>
|
||||
|
||||
|
||||
#datetime {
|
||||
text-align: center;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.1rem;
|
||||
color: #2d7c6f;
|
||||
}
|
||||
|
||||
#datetime .fas {
|
||||
margin-right: 0.3rem;
|
||||
color: #3a241d;
|
||||
font-size: 1.1em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* Hide label visually but keep for accessibility */
|
||||
.hidden-label {
|
||||
position: absolute;
|
||||
left: -10000px;
|
||||
top: auto;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ...existing code... */
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>Practice your typing</h1>
|
||||
<div>Click start to have a quote displayed. Type the quote as fast as you can!</div>
|
||||
<body>
|
||||
<h1>
|
||||
<i class="fas fa-keyboard"></i> Practice your typing
|
||||
</h1>
|
||||
<div>
|
||||
<i class="fas fa-info-circle"></i>
|
||||
Click start to have a quote displayed. Type the quote as fast as you can!
|
||||
</div>
|
||||
|
||||
<p id="quote"></p>
|
||||
<p id="message"></p>
|
||||
<div>
|
||||
<input type="text" aria-label="current word" id="typed-value" />
|
||||
</div>
|
||||
<div>
|
||||
<button id="start" type="button">Start</button>
|
||||
</div>
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
<p id="quote"></p>
|
||||
<p id="message"></p>
|
||||
<div>
|
||||
<label for="typed-value" class="hidden-label">Current word</label>
|
||||
<input type="text" aria-label="current word" id="typed-value" placeholder="Type here..." />
|
||||
<i class="fas fa-pen"></i>
|
||||
</div>
|
||||
<div>
|
||||
<button id="start" type="button">
|
||||
<i class="fas fa-play"></i> Start
|
||||
</button>
|
||||
</div>
|
||||
<script src="./index.js"></script>
|
||||
<!-- ...existing code... -->
|
||||
<body>
|
||||
<h1>
|
||||
<i class="fas fa-keyboard"></i> Practice your typing
|
||||
</h1>
|
||||
<div id="datetime">
|
||||
<i class="fas fa-calendar-day"></i>
|
||||
<span id="current-date"></span>
|
||||
|
|
||||
<i class="fas fa-clock"></i>
|
||||
<span id="current-time"></span>
|
||||
</div>
|
||||
<!-- ...existing code... -->
|
||||
</body>
|
||||
<!-- ...existing code... -->
|
||||
<script>
|
||||
// ...existing code...
|
||||
|
||||
function updateDateTime() {
|
||||
const now = new Date();
|
||||
|
||||
// Format date as DD/MM/YYYY
|
||||
const day = String(now.getDate()).padStart(2, '0');
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0');
|
||||
const year = now.getFullYear();
|
||||
const formattedDate = `${day}/${month}/${year}`;
|
||||
|
||||
// Format time as HH:MM AM/PM (12-hour)
|
||||
let hours = now.getHours();
|
||||
const minutes = String(now.getMinutes()).padStart(2, '0');
|
||||
const ampm = hours >= 12 ? 'PM' : 'AM';
|
||||
hours = hours % 12;
|
||||
hours = hours ? hours : 12; // 0 should be 12
|
||||
const formattedTime = `${hours}:${minutes} ${ampm}`;
|
||||
|
||||
document.getElementById('current-date').textContent = formattedDate;
|
||||
document.getElementById('current-time').textContent = formattedTime;
|
||||
}
|
||||
|
||||
// Initial call and update every minute
|
||||
updateDateTime();
|
||||
setInterval(updateDateTime, 60000);
|
||||
|
||||
// ...existing code...
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
@ -1,94 +1,94 @@
|
||||
// Typing Game - Modern ES6+ Version
|
||||
|
||||
// Quotes pool
|
||||
const quotes = [
|
||||
'When you have eliminated the impossible, whatever remains, however improbable, must be the truth.',
|
||||
'There is nothing more deceptive than an obvious fact.',
|
||||
'I ought to know by this time that when a fact appears to be opposed to a long train of deductions it invariably proves to be capable of bearing some other interpretation.',
|
||||
'I never make exceptions. An exception disproves the rule.',
|
||||
'What one man can invent another can discover.',
|
||||
'Nothing clears up a case so much as stating it to another person.',
|
||||
'Education never ends, Watson. It is a series of lessons, with the greatest for the last.',
|
||||
"When you have eliminated the impossible, whatever remains, however improbable, must be the truth.",
|
||||
"There is nothing more deceptive than an obvious fact.",
|
||||
"I ought to know by this time that when a fact appears to be opposed to a long train of deductions it invariably proves to be capable of bearing some other interpretation.",
|
||||
"I never make exceptions. An exception disproves the rule.",
|
||||
"What one man can invent another can discover.",
|
||||
"Nothing clears up a case so much as stating it to another person.",
|
||||
"Education never ends, Watson. It is a series of lessons, with the greatest for the last."
|
||||
];
|
||||
|
||||
// array for storing the words of the current challenge
|
||||
// State
|
||||
let words = [];
|
||||
// stores the index of the word the player is currently typing
|
||||
let wordIndex = 0;
|
||||
// default value for startTime (will be set on start)
|
||||
let startTime = Date.now();
|
||||
let startTime = 0;
|
||||
|
||||
// UI Elements
|
||||
const quoteElement = document.querySelector("#quote");
|
||||
const messageElement = document.querySelector("#message");
|
||||
const typedValueElement = document.querySelector("#typed-value");
|
||||
const startButton = document.querySelector("#start");
|
||||
|
||||
// Messages system
|
||||
const messages = {
|
||||
success: (seconds) => `🎉 Congratulations! You finished in ${seconds.toFixed(2)} seconds.`,
|
||||
error: "⚠️ Oops! There's a mistake.",
|
||||
start: "⌨️ Start typing to begin the game."
|
||||
};
|
||||
|
||||
// grab UI items
|
||||
const quoteElement = document.getElementById('quote');
|
||||
const messageElement = document.getElementById('message')
|
||||
const typedValueElement = document.getElementById('typed-value');
|
||||
// Utility: pick random quote
|
||||
const getRandomQuote = () => quotes[Math.floor(Math.random() * quotes.length)];
|
||||
|
||||
document.getElementById('start').addEventListener('click', function () {
|
||||
// get a quote
|
||||
const quoteIndex = Math.floor(Math.random() * quotes.length);
|
||||
const quote = quotes[quoteIndex];
|
||||
// Put the quote into an array of words
|
||||
words = quote.split(' ');
|
||||
// reset the word index for tracking
|
||||
wordIndex = 0;
|
||||
// Utility: render quote as spans
|
||||
const renderQuote = (quote) => {
|
||||
quoteElement.innerHTML = quote
|
||||
.split(" ")
|
||||
.map((word, i) => `<span ${i === 0 ? 'class="highlight"' : ""}>${word}</span>`)
|
||||
.join(" ");
|
||||
};
|
||||
|
||||
// UI updates
|
||||
// Create an array of span elements so we can set a class
|
||||
const spanWords = words.map(function (word) { return `<span>${word} </span>` });
|
||||
// Convert into string and set as innerHTML on quote display
|
||||
quoteElement.innerHTML = spanWords.join('');
|
||||
// Highlight the first word
|
||||
quoteElement.childNodes[0].className = 'highlight';
|
||||
// Clear any prior messages
|
||||
messageElement.innerText = '';
|
||||
// Utility: highlight current word
|
||||
const highlightWord = (index) => {
|
||||
[...quoteElement.children].forEach((el, i) => {
|
||||
el.classList.toggle("highlight", i === index);
|
||||
});
|
||||
};
|
||||
|
||||
// Setup the textbox
|
||||
// Clear the textbox
|
||||
typedValueElement.value = '';
|
||||
// set focus
|
||||
typedValueElement.focus();
|
||||
// set the event handler
|
||||
// Game start
|
||||
const startGame = () => {
|
||||
const quote = getRandomQuote();
|
||||
words = quote.split(" ");
|
||||
wordIndex = 0;
|
||||
renderQuote(quote);
|
||||
|
||||
// Start the timer
|
||||
startTime = new Date().getTime();
|
||||
});
|
||||
messageElement.textContent = messages.start;
|
||||
typedValueElement.value = "";
|
||||
typedValueElement.focus();
|
||||
|
||||
typedValueElement.addEventListener('input', (e) => {
|
||||
// Get the current word
|
||||
const currentWord = words[wordIndex];
|
||||
// get the current value
|
||||
const typedValue = typedValueElement.value;
|
||||
startTime = Date.now();
|
||||
};
|
||||
|
||||
if (typedValue === currentWord && wordIndex === words.length - 1) {
|
||||
// end of quote
|
||||
// Display success
|
||||
const elapsedTime = new Date().getTime() - startTime;
|
||||
const message = `CONGRATULATIONS! You finished in ${elapsedTime / 1000} seconds.`;
|
||||
messageElement.innerText = message;
|
||||
} else if (typedValue.endsWith(' ') && typedValue.trim() === currentWord) {
|
||||
// end of word
|
||||
// clear the typedValueElement for the new word
|
||||
typedValueElement.value = '';
|
||||
// move to the next word
|
||||
wordIndex++;
|
||||
// reset the class name for all elements in quote
|
||||
for (const wordElement of quoteElement.childNodes) {
|
||||
wordElement.className = '';
|
||||
}
|
||||
// highlight the new word
|
||||
quoteElement.childNodes[wordIndex].className = 'highlight';
|
||||
} else if (currentWord.startsWith(typedValue)) {
|
||||
// currently correct
|
||||
// highlight the next word
|
||||
typedValueElement.className = '';
|
||||
} else {
|
||||
// error state
|
||||
typedValueElement.className = 'error';
|
||||
}
|
||||
});
|
||||
// Typing logic
|
||||
const handleTyping = () => {
|
||||
const currentWord = words[wordIndex];
|
||||
const typedValue = typedValueElement.value;
|
||||
|
||||
// Add this at the end of the file
|
||||
const messages = {
|
||||
success: "CONGRATULATIONS! You finished in {seconds} seconds.",
|
||||
error: "Oops! There's a mistake.",
|
||||
start: "Start typing to begin the game."
|
||||
if (typedValue === currentWord && wordIndex === words.length - 1) {
|
||||
// Game finished
|
||||
const elapsedTime = (Date.now() - startTime) / 1000;
|
||||
messageElement.textContent = messages.success(elapsedTime);
|
||||
typedValueElement.disabled = true;
|
||||
} else if (typedValue.endsWith(" ") && typedValue.trim() === currentWord) {
|
||||
// Word completed
|
||||
typedValueElement.value = "";
|
||||
wordIndex++;
|
||||
highlightWord(wordIndex);
|
||||
} else if (currentWord.startsWith(typedValue)) {
|
||||
// Correct so far
|
||||
typedValueElement.classList.remove("error");
|
||||
} else {
|
||||
// Error
|
||||
typedValueElement.classList.add("error");
|
||||
messageElement.textContent = messages.error;
|
||||
}
|
||||
};
|
||||
|
||||
export default messages;
|
||||
// Event Listeners
|
||||
startButton.addEventListener("click", startGame);
|
||||
typedValueElement.addEventListener("input", handleTyping);
|
||||
|
||||
// Default state
|
||||
messageElement.textContent = "👉 Click Start to begin!";
|
||||
|
||||
@ -1,34 +1,47 @@
|
||||
function loadTexture(path) {
|
||||
return new Promise((resolve) => {
|
||||
const img = new Image();
|
||||
img.src = path;
|
||||
img.onload = () => {
|
||||
resolve(img);
|
||||
async function loadTexture(path) {
|
||||
return new Promise(resolve => {
|
||||
const img = new Image();
|
||||
img.src = path;
|
||||
img.onload = () => resolve(img);
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function createEnemies(ctx, canvas, enemyImg) {
|
||||
const MONSTER_TOTAL = 5;
|
||||
const MONSTER_WIDTH = MONSTER_TOTAL * 98;
|
||||
const START_X = (canvas.width - MONSTER_WIDTH) / 2;
|
||||
const STOP_X = START_X + MONSTER_WIDTH;
|
||||
|
||||
for (let x = START_X; x < STOP_X; x += 98) {
|
||||
for (let y = 0; y < 50 * 5; y += 50) {
|
||||
ctx.drawImage(enemyImg, x, y);
|
||||
function drawText(ctx, text, x, y, fontSize = 40, color = "#ff007f", align = "center") {
|
||||
ctx.save();
|
||||
ctx.font = `${fontSize}px Orbitron, monospace`;
|
||||
ctx.fillStyle = color;
|
||||
ctx.textAlign = align;
|
||||
ctx.shadowColor = "#181733";
|
||||
ctx.shadowBlur = 12;
|
||||
ctx.fillText(text, x, y);
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.onload = async() => {
|
||||
canvas = document.getElementById("canvas");
|
||||
ctx = canvas.getContext("2d");
|
||||
const heroImg = await loadTexture('assets/player.png')
|
||||
const enemyImg = await loadTexture('assets/enemyShip.png')
|
||||
|
||||
ctx.fillStyle = 'black';
|
||||
ctx.fillRect(0,0, canvas.width, canvas.height);
|
||||
ctx.drawImage(heroImg, canvas.width/2 - 45, canvas.height - (canvas.height /4));
|
||||
createEnemies(ctx, canvas, enemyImg);
|
||||
};
|
||||
function createEnemies(ctx, canvas, enemyImg) {
|
||||
const MONSTER_TOTAL = 7;
|
||||
const MONSTER_ROWS = 5;
|
||||
const ENEMY_SIZE = 65;
|
||||
const START_X = (canvas.width - (MONSTER_TOTAL * ENEMY_SIZE)) / 2;
|
||||
for (let row = 0; row < MONSTER_ROWS; row++) {
|
||||
for (let col = 0; col < MONSTER_TOTAL; col++) {
|
||||
const x = START_X + col * ENEMY_SIZE;
|
||||
const y = 60 + row * ENEMY_SIZE;
|
||||
ctx.drawImage(enemyImg, x, y, ENEMY_SIZE, ENEMY_SIZE);
|
||||
}
|
||||
}
|
||||
}
|
||||
window.onload = async () => {
|
||||
const canvas = document.getElementById("canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
// Use emoji icons as game characters if images aren't available
|
||||
const heroImg = await loadTexture('assets/player.png');
|
||||
const enemyImg = await loadTexture('assets/enemyShip.png');
|
||||
ctx.fillStyle = '#181733';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
drawText(ctx, "Prepare for Battle!", canvas.width/2, 70, 48, "#fff");
|
||||
// Draw hero
|
||||
ctx.drawImage(heroImg, canvas.width/2 - 45, canvas.height - (canvas.height / 4), 90, 90);
|
||||
createEnemies(ctx, canvas, enemyImg);
|
||||
// Example: Draw power up icon beside hero
|
||||
ctx.font = "38px FontAwesome";
|
||||
ctx.fillStyle = "#ffd700";
|
||||
ctx.fillText("\uf059", canvas.width/2 + 60, canvas.height - (canvas.height / 4) + 42); // Star icon
|
||||
};
|
||||
|
||||
@ -1,6 +1,49 @@
|
||||
<html>
|
||||
<body>
|
||||
<canvas id ="canvas" width="1024" height="768"></canvas>
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Enhanced Canvas Game</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
|
||||
<style>
|
||||
body {
|
||||
background: linear-gradient(135deg, #2d2a4a 0%, #181733 100%);
|
||||
color: #fff;
|
||||
font-family: 'Orbitron', 'Arial', sans-serif;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
h1 {
|
||||
font-family: 'Orbitron', monospace;
|
||||
margin-top: 32px;
|
||||
font-size: 2.7rem;
|
||||
letter-spacing: 2px;
|
||||
text-shadow: 0 3px 12px #ff007f;
|
||||
}
|
||||
.info-bar {
|
||||
margin: 20px 0;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
#canvas {
|
||||
border: 4px solid #ff007f;
|
||||
box-shadow: 0 0 40px #ff007f22;
|
||||
display: block;
|
||||
margin: 20px auto;
|
||||
background: #12121c;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>
|
||||
<i class="fa-solid fa-rocket"></i>
|
||||
Arcade Monster Attack
|
||||
<i class="fa-solid fa-skull-crossbones"></i>
|
||||
</h1>
|
||||
<div class="info-bar">
|
||||
<i class="fa-solid fa-star"></i> Score: <span id="score">0</span>
|
||||
<i class="fa-solid fa-heart"></i> Lives: <span id="lives">3</span>
|
||||
</div>
|
||||
<canvas id="canvas" width="1024" height="768"></canvas>
|
||||
</html>
|
||||
|
||||
@ -1,20 +1,523 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Space Game</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="description" content="Canvas-based Space Game with accessible controls, HUD, and settings." />
|
||||
<meta name="theme-color" content="#0b1023" />
|
||||
|
||||
<!-- Google Fonts: performance preconnect + fonts (apply via CSS later) -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<!-- Swap or add families as desired -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&family=Poppins:wght@400;600;800&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Font Awesome 6 (Free) via cdnjs -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css" integrity="sha512-bWn0gM3Yx6w5OV6v4p+WB6l3Zl9sJ2jQ4kN0N1bK2qgkvI6l3F8Vd6k1oB7f/0g0a9Qyxgkq+K6X1JQmJv9zYg==" crossorigin="anonymous" referrerpolicy="no-referrer">
|
||||
|
||||
<!-- Optional PWA hooks (files to be created later) -->
|
||||
<link rel="manifest" href="manifest.webmanifest">
|
||||
</head>
|
||||
<style>
|
||||
/* Space Game — CSS */
|
||||
/* Typography + Theme */
|
||||
:root {
|
||||
--bg: #070a1a;
|
||||
--bg-2: #0b1023;
|
||||
--panel: rgba(12, 17, 38, 0.75);
|
||||
--panel-solid: #0c1126;
|
||||
--primary: #5ac8fa;
|
||||
--accent: #a78bfa;
|
||||
--danger: #ff6b6b;
|
||||
--success: #34d399;
|
||||
--warning: #fbbf24;
|
||||
--text: #e6eaf2;
|
||||
--muted: #9aa4bf;
|
||||
--border: #192243;
|
||||
--shadow: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
color: var(--text);
|
||||
background:
|
||||
radial-gradient(circle at 20% 10%, #0f1533 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 30%, #0b1b3a 0%, transparent 40%),
|
||||
radial-gradient(circle at 30% 80%, #111a39 0%, transparent 40%),
|
||||
var(--bg);
|
||||
font-family: "Poppins", system-ui, -apple-system, Segoe UI, Roboto, "Helvetica Neue", Arial, "Noto Sans", "Apple Color Emoji", "Segoe UI Emoji";
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Header + Nav */
|
||||
header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 20;
|
||||
background: linear-gradient(180deg, rgba(7,10,26,0.85) 0%, rgba(7,10,26,0.6) 100%);
|
||||
backdrop-filter: blur(6px);
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
margin: 0;
|
||||
font-family: "Orbitron", "Poppins", sans-serif;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.5px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
|
||||
nav[aria-label="Primary"] {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
nav[aria-label="Primary"] button {
|
||||
appearance: none;
|
||||
border: 1px solid var(--border);
|
||||
background: linear-gradient(180deg, #121a3a 0%, #0d1430 100%);
|
||||
color: var(--text);
|
||||
border-radius: 10px;
|
||||
padding: 0.55rem 0.85rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
cursor: pointer;
|
||||
transition: transform 0.06s ease, box-shadow 0.2s ease, border-color 0.2s ease;
|
||||
box-shadow: 0 2px 12px var(--shadow);
|
||||
}
|
||||
|
||||
nav[aria-label="Primary"] button:hover {
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 4px 18px rgba(90, 200, 250, 0.25);
|
||||
}
|
||||
|
||||
nav[aria-label="Primary"] button:active {
|
||||
transform: translateY(1px) scale(0.98);
|
||||
}
|
||||
|
||||
nav [data-action="start"] { background: linear-gradient(180deg, #10314a 0%, #0d2438 100%); }
|
||||
nav [data-action="pause"] { background: linear-gradient(180deg, #1a1f3f 0%, #131936 100%); }
|
||||
nav [data-action="reset"] { background: linear-gradient(180deg, #3b0d0d 0%, #2a0a0a 100%); }
|
||||
nav [data-action="mute"][aria-pressed="true"] { background: linear-gradient(180deg, #3f1a1a 0%, #2a0e0e 100%); }
|
||||
nav [data-action="fullscreen"] { background: linear-gradient(180deg, #162c3a 0%, #0f2331 100%); }
|
||||
nav [data-action="open-settings"] { background: linear-gradient(180deg, #1a1440 0%, #13103a 100%); }
|
||||
|
||||
/* Main Layout */
|
||||
main {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
/* HUD overlay */
|
||||
#hud {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
top: 64px;
|
||||
transform: translateX(-50%);
|
||||
width: min(100% - 2rem, 1180px);
|
||||
z-index: 15;
|
||||
}
|
||||
|
||||
#hud [role="status"] {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem 1rem;
|
||||
align-items: center;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 0.5rem 0.75rem;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
#hud span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
color: var(--muted);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
#hud output {
|
||||
color: var(--primary);
|
||||
font-family: "Orbitron", "Poppins", sans-serif;
|
||||
letter-spacing: 0.4px;
|
||||
}
|
||||
|
||||
/* Canvas section */
|
||||
#play {
|
||||
position: relative;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
#play figure {
|
||||
margin: 0;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 14px;
|
||||
background:
|
||||
radial-gradient(1200px 600px at 70% 30%, rgba(70, 78, 150, 0.15), transparent 60%),
|
||||
radial-gradient(800px 400px at 20% 70%, rgba(88, 131, 186, 0.12), transparent 60%),
|
||||
#081022;
|
||||
box-shadow: 0 8px 28px rgba(0,0,0,0.45), inset 0 0 0 1px rgba(255,255,255,0.02);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#play figcaption {
|
||||
position: absolute;
|
||||
z-index: 5;
|
||||
inset: 0 auto auto 0;
|
||||
margin: 0.5rem 0 0 0.5rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
background: rgba(8, 16, 34, 0.7);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
color: var(--muted);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
#myCanvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
background: linear-gradient(180deg, #000814 0%, #001d3d 100%);
|
||||
}
|
||||
|
||||
/* Help + Leaderboard */
|
||||
#help, #leaderboard {
|
||||
margin-top: 1rem;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
#help h2, #leaderboard h2 {
|
||||
margin-top: 0;
|
||||
font-family: "Orbitron", "Poppins", sans-serif;
|
||||
}
|
||||
|
||||
#help ul {
|
||||
margin: 0.5rem 0 0 1rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#scores {
|
||||
margin: 0.5rem 0 0 1rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#scores li {
|
||||
margin: 0.25rem 0;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* Dialogs */
|
||||
dialog {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 14px;
|
||||
padding: 1rem;
|
||||
background: var(--panel-solid);
|
||||
color: var(--text);
|
||||
width: min(520px, calc(100% - 2rem));
|
||||
box-shadow: 0 24px 60px rgba(0,0,0,0.55);
|
||||
}
|
||||
|
||||
dialog header h2 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-family: "Orbitron", "Poppins", sans-serif;
|
||||
}
|
||||
|
||||
dialog section {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
dialog label {
|
||||
font-size: 0.95rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
dialog input[type="range"],
|
||||
dialog select,
|
||||
dialog input[type="url"],
|
||||
dialog input[type="file"] {
|
||||
width: 100%;
|
||||
color: var(--text);
|
||||
background: #0f1733;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
dialog menu {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
margin: 0;
|
||||
padding: 0.5rem 0 0 0;
|
||||
}
|
||||
|
||||
dialog button {
|
||||
appearance: none;
|
||||
border: 1px solid var(--border);
|
||||
background: linear-gradient(180deg, #121a3a 0%, #0d1430 100%);
|
||||
color: var(--text);
|
||||
border-radius: 10px;
|
||||
padding: 0.5rem 0.85rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
dialog::backdrop {
|
||||
background: rgba(3, 6, 20, 0.6);
|
||||
backdrop-filter: blur(3px);
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
footer {
|
||||
margin: 1rem 0;
|
||||
color: var(--muted);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Focus styles */
|
||||
:focus-visible {
|
||||
outline: 2px solid var(--primary);
|
||||
outline-offset: 2px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
/* Fullscreen adjustments */
|
||||
#play:fullscreen, /* section fullscreen */
|
||||
#myCanvas:fullscreen {
|
||||
background: #000;
|
||||
}
|
||||
|
||||
#play:fullscreen #myCanvas,
|
||||
#myCanvas:fullscreen {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
/* Small screens */
|
||||
@media (max-width: 600px) {
|
||||
#hud [role="status"] {
|
||||
justify-content: space-between;
|
||||
}
|
||||
nav[aria-label="Primary"] {
|
||||
gap: 0.4rem;
|
||||
}
|
||||
nav[aria-label="Primary"] button {
|
||||
padding: 0.5rem 0.65rem;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
<body>
|
||||
|
||||
<p>Image to use:</p>
|
||||
<p>Canvas:</p>
|
||||
<a href="#play" aria-label="Skip to game">Skip to Game</a>
|
||||
|
||||
<header>
|
||||
<h1 aria-label="Space Game">
|
||||
<i class="fa-solid fa-rocket" aria-hidden="true"></i>
|
||||
Space Game
|
||||
</h1>
|
||||
|
||||
<nav aria-label="Primary">
|
||||
<button type="button" data-action="start" aria-label="Start">
|
||||
<i class="fa-solid fa-play" aria-hidden="true"></i> Start
|
||||
</button>
|
||||
<button type="button" data-action="pause" aria-label="Pause">
|
||||
<i class="fa-solid fa-circle-pause" aria-hidden="true"></i> Pause
|
||||
</button>
|
||||
<button type="button" data-action="reset" aria-label="Reset">
|
||||
<i class="fa-solid fa-rotate-right" aria-hidden="true"></i> Reset
|
||||
</button>
|
||||
<button type="button" data-action="mute" aria-pressed="false" aria-label="Mute">
|
||||
<i class="fa-solid fa-volume-xmark" aria-hidden="true"></i> Mute
|
||||
</button>
|
||||
<button type="button" data-action="fullscreen" aria-label="Toggle fullscreen">
|
||||
<i class="fa-solid fa-expand" aria-hidden="true"></i> Fullscreen
|
||||
</button>
|
||||
<button type="button" data-action="open-settings" aria-controls="settingsDialog" aria-haspopup="dialog">
|
||||
<i class="fa-solid fa-gear" aria-hidden="true"></i> Settings
|
||||
</button>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main id="main" tabindex="-1">
|
||||
|
||||
<!-- HUD -->
|
||||
<section id="hud" aria-label="Game HUD">
|
||||
<div role="status" aria-live="polite">
|
||||
<span><i class="fa-solid fa-trophy" aria-hidden="true"></i> Score: <output id="score" name="score">0</output></span>
|
||||
<span><i class="fa-solid fa-layer-group" aria-hidden="true"></i> Level: <output id="level" name="level">1</output></span>
|
||||
<span><i class="fa-solid fa-heart" aria-hidden="true"></i> Lives: <output id="lives" name="lives">3</output></span>
|
||||
<span><i class="fa-solid fa-shield-halved" aria-hidden="true"></i> Shield: <output id="shield" name="shield">100%</output></span>
|
||||
<span><i class="fa-solid fa-gauge" aria-hidden="true"></i> FPS: <output id="fps" name="fps">0</output></span>
|
||||
<span><i class="fa-regular fa-clock" aria-hidden="true"></i> Time: <output id="time" name="time">00:00</output></span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Game Canvas -->
|
||||
<section id="play" aria-label="Game Canvas">
|
||||
<figure>
|
||||
<figcaption>Canvas</figcaption>
|
||||
<canvas id="myCanvas" width="1024" height="768">
|
||||
Your browser does not support the HTML5 canvas tag.
|
||||
</canvas>
|
||||
</figure>
|
||||
</section>
|
||||
|
||||
<!-- Controls Help -->
|
||||
<section id="help" aria-label="Controls Guide">
|
||||
<h2><i class="fa-solid fa-gamepad" aria-hidden="true"></i> Controls</h2>
|
||||
<ul>
|
||||
<li>Move: Arrow keys or WASD</li>
|
||||
<li>Fire: Space</li>
|
||||
<li>Special: Shift</li>
|
||||
<li>Pause: P</li>
|
||||
<li>Toggle Fullscreen: F</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<!-- Assets and Audio Hooks (managed by app.js) -->
|
||||
<section id="assets" hidden aria-hidden="true">
|
||||
<h2><i class="fa-regular fa-image" aria-hidden="true"></i> Assets</h2>
|
||||
|
||||
<!-- Optional: let players provide an image URL for background/sprites (read by app.js) -->
|
||||
<label for="bgImageUrl">Background image URL</label>
|
||||
<input id="bgImageUrl" type="url" placeholder="https://example.com/space.jpg" inputmode="url">
|
||||
|
||||
<label for="spriteUpload">Upload sprite images</label>
|
||||
<input id="spriteUpload" type="file" accept="image/*" multiple>
|
||||
|
||||
<!-- Preloadable audio (sources to be provided) -->
|
||||
<audio id="sfxShoot" preload="auto">
|
||||
<source src="assets/audio/shoot.mp3" type="audio/mpeg">
|
||||
</audio>
|
||||
<audio id="sfxExplosion" preload="auto">
|
||||
<source src="assets/audio/explosion.mp3" type="audio/mpeg">
|
||||
</audio>
|
||||
<audio id="bgMusic" preload="auto" loop>
|
||||
<source src="assets/audio/music.mp3" type="audio/mpeg">
|
||||
</audio>
|
||||
</section>
|
||||
|
||||
<!-- Leaderboard -->
|
||||
<section id="leaderboard" aria-label="Leaderboard">
|
||||
<h2><i class="fa-solid fa-ranking-star" aria-hidden="true"></i> Leaderboard</h2>
|
||||
<ol id="scores">
|
||||
<li data-initials="AAA" data-score="10000">AAA — 10,000</li>
|
||||
<li data-initials="BBB" data-score="8000">BBB — 8,000</li>
|
||||
<li data-initials="CCC" data-score="5000">CCC — 5,000</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<!-- Settings Dialog (open via Settings button; JS can call showModal()) -->
|
||||
<dialog id="settingsDialog" aria-labelledby="settingsTitle">
|
||||
<form method="dialog">
|
||||
<header>
|
||||
<h2 id="settingsTitle">
|
||||
<i class="fa-solid fa-sliders" aria-hidden="true"></i>
|
||||
Settings
|
||||
</h2>
|
||||
</header>
|
||||
|
||||
<section>
|
||||
<label for="difficulty">Difficulty</label>
|
||||
<select id="difficulty" name="difficulty">
|
||||
<option value="easy">Easy</option>
|
||||
<option value="normal" selected>Normal</option>
|
||||
<option value="hard">Hard</option>
|
||||
<option value="insane">Insane</option>
|
||||
</select>
|
||||
|
||||
<label for="graphics">Graphics</label>
|
||||
<select id="graphics" name="graphics">
|
||||
<option value="low">Low</option>
|
||||
<option value="medium" selected>Medium</option>
|
||||
<option value="high">High</option>
|
||||
<option value="ultra">Ultra</option>
|
||||
</select>
|
||||
|
||||
<label for="musicVolume">Music Volume</label>
|
||||
<input id="musicVolume" name="musicVolume" type="range" min="0" max="100" value="60" />
|
||||
|
||||
<label for="sfxVolume">SFX Volume</label>
|
||||
<input id="sfxVolume" name="sfxVolume" type="range" min="0" max="100" value="80" />
|
||||
|
||||
<label for="controlScheme">Control Scheme</label>
|
||||
<select id="controlScheme" name="controlScheme">
|
||||
<option value="wasd" selected>WASD + Space</option>
|
||||
<option value="arrows">Arrows + Space</option>
|
||||
<option value="custom">Custom</option>
|
||||
</select>
|
||||
</section>
|
||||
|
||||
<menu>
|
||||
<button value="cancel" aria-label="Close settings">
|
||||
<i class="fa-solid fa-xmark" aria-hidden="true"></i>
|
||||
Close
|
||||
</button>
|
||||
<button value="apply" data-action="apply-settings" aria-label="Apply settings">
|
||||
<i class="fa-solid fa-check" aria-hidden="true"></i>
|
||||
Apply
|
||||
</button>
|
||||
</menu>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<!-- Pause Overlay (can be shown via JS) -->
|
||||
<dialog id="pauseDialog" aria-labelledby="pauseTitle">
|
||||
<form method="dialog">
|
||||
<h2 id="pauseTitle"><i class="fa-solid fa-circle-pause" aria-hidden="true"></i> Paused</h2>
|
||||
<p>Game is paused.</p>
|
||||
<menu>
|
||||
<button value="resume" data-action="resume">
|
||||
<i class="fa-solid fa-play" aria-hidden="true"></i>
|
||||
Resume
|
||||
</button>
|
||||
<button value="restart" data-action="reset">
|
||||
<i class="fa-solid fa-rotate-right" aria-hidden="true"></i>
|
||||
Restart
|
||||
</button>
|
||||
</menu>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<canvas id="myCanvas" width="1024" height="768" style="border:1px solid #d3d3d3;">
|
||||
Your browser does not support the HTML5 canvas tag.</canvas>
|
||||
</main>
|
||||
|
||||
<script src="app.js">
|
||||
<footer>
|
||||
<small>
|
||||
<i class="fa-regular fa-circle-question" aria-hidden="true"></i>
|
||||
Tip: Ensure app.js wires up data-action buttons, dialog show/close, audio, and canvas rendering.
|
||||
</small>
|
||||
</footer>
|
||||
|
||||
</script>
|
||||
<noscript>
|
||||
JavaScript is required to play this game.
|
||||
</noscript>
|
||||
|
||||
<!-- Keep your game logic here -->
|
||||
<script defer src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@ -1,30 +1,56 @@
|
||||
# api
|
||||
|
||||
from flask import Flask, request, jsonify
|
||||
from llm import call_llm
|
||||
from flask_cors import CORS
|
||||
import logging
|
||||
|
||||
app = Flask(__name__)
|
||||
CORS(app) # * example.com
|
||||
|
||||
# Configure CORS (allow all origins for development; restrict in production)
|
||||
CORS(app, resources={r"/*": {"origins": "*"}})
|
||||
|
||||
# Set up logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
@app.route("/", methods=["GET"])
|
||||
def index():
|
||||
return "Welcome to this lesson"
|
||||
"""Root endpoint for API status."""
|
||||
return jsonify({
|
||||
"status": "ok",
|
||||
"message": "Welcome to the Chat Project API"
|
||||
})
|
||||
|
||||
@app.route("/health", methods=["GET"])
|
||||
def health():
|
||||
"""Health check endpoint."""
|
||||
return jsonify({"status": "healthy"}), 200
|
||||
|
||||
@app.route("/test", methods=["GET"])
|
||||
def test():
|
||||
return "Test"
|
||||
"""Simple test endpoint."""
|
||||
return jsonify({"result": "Test successful"}), 200
|
||||
|
||||
@app.route("/hello", methods=["POST"])
|
||||
def hello():
|
||||
# get message from request body { "message": "do this taks for me" }
|
||||
data = request.get_json()
|
||||
message = data.get("message", "")
|
||||
"""
|
||||
Chat endpoint.
|
||||
Expects JSON: { "message": "your message" }
|
||||
Returns: { "response": "LLM response" }
|
||||
"""
|
||||
try:
|
||||
data = request.get_json(force=True)
|
||||
message = data.get("message", "").strip()
|
||||
if not message:
|
||||
logging.warning("No message provided in request.")
|
||||
return jsonify({"error": "No message provided."}), 400
|
||||
|
||||
response = call_llm(message, "You are a helpful assistant.")
|
||||
return jsonify({
|
||||
"response": response
|
||||
})
|
||||
logging.info(f"Received message: {message}")
|
||||
response = call_llm(message, "You are a helpful assistant.")
|
||||
return jsonify({"response": response}), 200
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error in /hello endpoint: {e}")
|
||||
return jsonify({"error": "Internal server error."}), 500
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=5000)
|
||||
# Run the app with debug mode for development
|
||||
app.run(host="0.0.0.0", port=5000, debug=True)
|
||||
@ -1,29 +1,53 @@
|
||||
import os
|
||||
import logging
|
||||
from openai import OpenAI
|
||||
|
||||
# To authenticate with the model you will need to generate a personal access token (PAT) in your GitHub settings.
|
||||
# Create your PAT token by following instructions here: https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Environment variable check for GitHub token
|
||||
GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN")
|
||||
if not GITHUB_TOKEN:
|
||||
logger.error("GITHUB_TOKEN environment variable not set.")
|
||||
raise EnvironmentError("GITHUB_TOKEN environment variable not set.")
|
||||
|
||||
# Model and endpoint configuration
|
||||
MODEL_NAME = os.environ.get("LLM_MODEL", "openai/gpt-4o-mini")
|
||||
BASE_URL = os.environ.get("LLM_BASE_URL", "https://models.github.ai/inference")
|
||||
|
||||
# Initialize OpenAI client
|
||||
client = OpenAI(
|
||||
base_url="https://models.github.ai/inference",
|
||||
api_key=os.environ["GITHUB_TOKEN"],
|
||||
base_url=BASE_URL,
|
||||
api_key=GITHUB_TOKEN,
|
||||
)
|
||||
|
||||
def call_llm(prompt: str, system_message: str):
|
||||
response = client.chat.completions.create(
|
||||
messages=[
|
||||
{
|
||||
"role": "system",
|
||||
"content": system_message,
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": prompt,
|
||||
}
|
||||
],
|
||||
model="openai/gpt-4o-mini",
|
||||
temperature=1,
|
||||
max_tokens=4096,
|
||||
top_p=1
|
||||
)
|
||||
def call_llm(prompt: str, system_message: str, temperature: float = 1.0, max_tokens: int = 4096, top_p: float = 1.0) -> str:
|
||||
"""
|
||||
Calls the LLM with the given prompt and system message.
|
||||
Returns the model's response as a string.
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Calling LLM model '{MODEL_NAME}' with prompt: {prompt}")
|
||||
response = client.chat.completions.create(
|
||||
messages=[
|
||||
{"role": "system", "content": system_message},
|
||||
{"role": "user", "content": prompt}
|
||||
],
|
||||
model=MODEL_NAME,
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens,
|
||||
top_p=top_p
|
||||
)
|
||||
content = response.choices[0].message.content
|
||||
logger.info("LLM response received successfully.")
|
||||
return content
|
||||
except Exception as e:
|
||||
logger.error(f"Error calling LLM: {e}")
|
||||
return "Sorry, there was an error processing your request."
|
||||
|
||||
return response.choices[0].message.content
|
||||
# Example usage (for testing)
|
||||
if __name__ == "__main__":
|
||||
test_prompt = "Hello, how are you?"
|
||||
test_system = "You are a friendly assistant."
|
||||
print(call_llm(test_prompt, test_system))
|
||||
@ -0,0 +1,401 @@
|
||||
# AGENTS.md
|
||||
|
||||
## Project Overview
|
||||
|
||||
This is an educational curriculum repository for teaching web development fundamentals to beginners. The curriculum is a comprehensive 12-week course developed by Microsoft Cloud Advocates, featuring 24 hands-on lessons covering JavaScript, CSS, and HTML.
|
||||
|
||||
### Key Components
|
||||
|
||||
- **Educational Content**: 24 structured lessons organized into project-based modules
|
||||
- **Practical Projects**: Terrarium, Typing Game, Browser Extension, Space Game, Banking App, Code Editor, and AI Chat Assistant
|
||||
- **Interactive Quizzes**: 48 quizzes with 3 questions each (pre/post-lesson assessments)
|
||||
- **Multi-language Support**: Automated translations for 50+ languages via GitHub Actions
|
||||
- **Technologies**: HTML, CSS, JavaScript, Vue.js 3, Vite, Node.js, Express, Python (for AI projects)
|
||||
|
||||
### Architecture
|
||||
|
||||
- Educational repository with lesson-based structure
|
||||
- Each lesson folder contains README, code examples, and solutions
|
||||
- Standalone projects in separate directories (quiz-app, various lesson projects)
|
||||
- Translation system using GitHub Actions (co-op-translator)
|
||||
- Documentation served via Docsify and available as PDF
|
||||
|
||||
## Setup Commands
|
||||
|
||||
This repository is primarily for educational content consumption. For working with specific projects:
|
||||
|
||||
### Main Repository Setup
|
||||
|
||||
```bash
|
||||
git clone https://github.com/microsoft/Web-Dev-For-Beginners.git
|
||||
cd Web-Dev-For-Beginners
|
||||
```
|
||||
|
||||
### Quiz App Setup (Vue 3 + Vite)
|
||||
|
||||
```bash
|
||||
cd quiz-app
|
||||
npm install
|
||||
npm run dev # Start development server
|
||||
npm run build # Build for production
|
||||
npm run lint # Run ESLint
|
||||
```
|
||||
|
||||
### Bank Project API (Node.js + Express)
|
||||
|
||||
```bash
|
||||
cd 7-bank-project/api
|
||||
npm install
|
||||
npm start # Start API server
|
||||
npm run lint # Run ESLint
|
||||
npm run format # Format with Prettier
|
||||
```
|
||||
|
||||
### Browser Extension Projects
|
||||
|
||||
```bash
|
||||
cd 5-browser-extension/solution
|
||||
npm install
|
||||
# Follow browser-specific extension loading instructions
|
||||
```
|
||||
|
||||
### Space Game Projects
|
||||
|
||||
```bash
|
||||
cd 6-space-game/solution
|
||||
npm install
|
||||
# Open index.html in browser or use Live Server
|
||||
```
|
||||
|
||||
### Chat Project (Python Backend)
|
||||
|
||||
```bash
|
||||
cd 9-chat-project/solution/backend/python
|
||||
pip install openai
|
||||
# Set GITHUB_TOKEN environment variable
|
||||
python api.py
|
||||
```
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### For Content Contributors
|
||||
|
||||
1. **Fork the repository** to your GitHub account
|
||||
2. **Clone your fork** locally
|
||||
3. **Create a new branch** for your changes
|
||||
4. Make changes to lesson content or code examples
|
||||
5. Test any code changes in relevant project directories
|
||||
6. Submit pull requests following contribution guidelines
|
||||
|
||||
### For Learners
|
||||
|
||||
1. Fork or clone the repository
|
||||
2. Navigate to lesson directories sequentially
|
||||
3. Read README files for each lesson
|
||||
4. Complete pre-lesson quizzes at https://ff-quizzes.netlify.app/web/
|
||||
5. Work through code examples in lesson folders
|
||||
6. Complete assignments and challenges
|
||||
7. Take post-lesson quizzes
|
||||
|
||||
### Live Development
|
||||
|
||||
- **Documentation**: Run `docsify serve` in root (port 3000)
|
||||
- **Quiz App**: Run `npm run dev` in quiz-app directory
|
||||
- **Projects**: Use VS Code Live Server extension for HTML projects
|
||||
- **API Projects**: Run `npm start` in respective API directories
|
||||
|
||||
## Testing Instructions
|
||||
|
||||
### Quiz App Testing
|
||||
|
||||
```bash
|
||||
cd quiz-app
|
||||
npm run lint # Check for code style issues
|
||||
npm run build # Verify build succeeds
|
||||
```
|
||||
|
||||
### Bank API Testing
|
||||
|
||||
```bash
|
||||
cd 7-bank-project/api
|
||||
npm run lint # Check for code style issues
|
||||
node server.js # Verify server starts without errors
|
||||
```
|
||||
|
||||
### General Testing Approach
|
||||
|
||||
- This is an educational repository without comprehensive automated tests
|
||||
- Manual testing focuses on:
|
||||
- Code examples run without errors
|
||||
- Links in documentation work correctly
|
||||
- Project builds complete successfully
|
||||
- Examples follow best practices
|
||||
|
||||
### Pre-submission Checks
|
||||
|
||||
- Run `npm run lint` in directories with package.json
|
||||
- Verify markdown links are valid
|
||||
- Test code examples in browser or Node.js
|
||||
- Check that translations maintain proper structure
|
||||
|
||||
## Code Style Guidelines
|
||||
|
||||
### JavaScript
|
||||
|
||||
- Use modern ES6+ syntax
|
||||
- Follow standard ESLint configurations provided in projects
|
||||
- Use meaningful variable and function names for educational clarity
|
||||
- Add comments explaining concepts for learners
|
||||
- Format using Prettier where configured
|
||||
|
||||
### HTML/CSS
|
||||
|
||||
- Semantic HTML5 elements
|
||||
- Responsive design principles
|
||||
- Clear class naming conventions
|
||||
- Comments explaining CSS techniques for learners
|
||||
|
||||
### Python
|
||||
|
||||
- PEP 8 style guidelines
|
||||
- Clear, educational code examples
|
||||
- Type hints where helpful for learning
|
||||
|
||||
### Markdown Documentation
|
||||
|
||||
- Clear heading hierarchy
|
||||
- Code blocks with language specification
|
||||
- Links to additional resources
|
||||
- Screenshots and images in `images/` directories
|
||||
- Alt text for images for accessibility
|
||||
|
||||
### File Organization
|
||||
|
||||
- Lessons numbered sequentially (1-getting-started-lessons, 2-js-basics, etc.)
|
||||
- Each project has `solution/` and often `start/` or `your-work/` directories
|
||||
- Images stored in lesson-specific `images/` folders
|
||||
- Translations in `translations/{language-code}/` structure
|
||||
|
||||
## Build and Deployment
|
||||
|
||||
### Quiz App Deployment (Azure Static Web Apps)
|
||||
|
||||
The quiz-app is configured for Azure Static Web Apps deployment:
|
||||
|
||||
```bash
|
||||
cd quiz-app
|
||||
npm run build # Creates dist/ folder
|
||||
# Deploys via GitHub Actions workflow on push to main
|
||||
```
|
||||
|
||||
Azure Static Web Apps configuration:
|
||||
- **App location**: `/quiz-app`
|
||||
- **Output location**: `dist`
|
||||
- **Workflow**: `.github/workflows/azure-static-web-apps-ashy-river-0debb7803.yml`
|
||||
|
||||
### Documentation PDF Generation
|
||||
|
||||
```bash
|
||||
npm install # Install docsify-to-pdf
|
||||
npm run convert # Generate PDF from docs
|
||||
```
|
||||
|
||||
### Docsify Documentation
|
||||
|
||||
```bash
|
||||
npm install -g docsify-cli # Install Docsify globally
|
||||
docsify serve # Serve on localhost:3000
|
||||
```
|
||||
|
||||
### Project-specific Builds
|
||||
|
||||
Each project directory may have its own build process:
|
||||
- Vue projects: `npm run build` creates production bundles
|
||||
- Static projects: No build step, serve files directly
|
||||
|
||||
## Pull Request Guidelines
|
||||
|
||||
### Title Format
|
||||
|
||||
Use clear, descriptive titles indicating the area of change:
|
||||
- `[Quiz-app] Add new quiz for lesson X`
|
||||
- `[Lesson-3] Fix typo in terrarium project`
|
||||
- `[Translation] Add Spanish translation for lesson 5`
|
||||
- `[Docs] Update setup instructions`
|
||||
|
||||
### Required Checks
|
||||
|
||||
Before submitting a PR:
|
||||
|
||||
1. **Code Quality**:
|
||||
- Run `npm run lint` in affected project directories
|
||||
- Fix all linting errors and warnings
|
||||
|
||||
2. **Build Verification**:
|
||||
- Run `npm run build` if applicable
|
||||
- Ensure no build errors
|
||||
|
||||
3. **Link Validation**:
|
||||
- Test all markdown links
|
||||
- Verify image references work
|
||||
|
||||
4. **Content Review**:
|
||||
- Proofread for spelling and grammar
|
||||
- Ensure code examples are correct and educational
|
||||
- Verify translations maintain original meaning
|
||||
|
||||
### Contribution Requirements
|
||||
|
||||
- Agree to Microsoft CLA (automated check on first PR)
|
||||
- Follow the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/)
|
||||
- See [CONTRIBUTING.md](./CONTRIBUTING.md) for detailed guidelines
|
||||
- Reference issue numbers in PR description if applicable
|
||||
|
||||
### Review Process
|
||||
|
||||
- PRs reviewed by maintainers and community
|
||||
- Educational clarity is prioritized
|
||||
- Code examples should follow current best practices
|
||||
- Translations reviewed for accuracy and cultural appropriateness
|
||||
|
||||
## Translation System
|
||||
|
||||
### Automated Translation
|
||||
|
||||
- Uses GitHub Actions with co-op-translator workflow
|
||||
- Translates to 50+ languages automatically
|
||||
- Source files in main directories
|
||||
- Translated files in `translations/{language-code}/` directories
|
||||
|
||||
### Adding Manual Translation Improvements
|
||||
|
||||
1. Locate file in `translations/{language-code}/`
|
||||
2. Make improvements while preserving structure
|
||||
3. Ensure code examples remain functional
|
||||
4. Test any localized quiz content
|
||||
|
||||
### Translation Metadata
|
||||
|
||||
Translated files include metadata header:
|
||||
```markdown
|
||||
<!--
|
||||
CO_OP_TRANSLATOR_METADATA:
|
||||
{
|
||||
"original_hash": "...",
|
||||
"translation_date": "...",
|
||||
"source_file": "...",
|
||||
"language_code": "..."
|
||||
}
|
||||
-->
|
||||
```
|
||||
|
||||
## Debugging and Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Quiz app fails to start**:
|
||||
- Check Node.js version (v14+ recommended)
|
||||
- Delete `node_modules` and `package-lock.json`, run `npm install` again
|
||||
- Check for port conflicts (default: Vite uses port 5173)
|
||||
|
||||
**API server won't start**:
|
||||
- Verify Node.js version meets minimum (node >=10)
|
||||
- Check if port is already in use
|
||||
- Ensure all dependencies installed with `npm install`
|
||||
|
||||
**Browser extension won't load**:
|
||||
- Verify manifest.json is properly formatted
|
||||
- Check browser console for errors
|
||||
- Follow browser-specific extension installation instructions
|
||||
|
||||
**Python chat project issues**:
|
||||
- Ensure OpenAI package installed: `pip install openai`
|
||||
- Verify GITHUB_TOKEN environment variable is set
|
||||
- Check GitHub Models access permissions
|
||||
|
||||
**Docsify not serving docs**:
|
||||
- Install docsify-cli globally: `npm install -g docsify-cli`
|
||||
- Run from repository root directory
|
||||
- Check that `docs/_sidebar.md` exists
|
||||
|
||||
### Development Environment Tips
|
||||
|
||||
- Use VS Code with Live Server extension for HTML projects
|
||||
- Install ESLint and Prettier extensions for consistent formatting
|
||||
- Use browser DevTools for debugging JavaScript
|
||||
- For Vue projects, install Vue DevTools browser extension
|
||||
|
||||
### Performance Considerations
|
||||
|
||||
- Large number of translated files (50+ languages) means full clones are large
|
||||
- Use shallow clone if only working on content: `git clone --depth 1`
|
||||
- Exclude translations from searches when working on English content
|
||||
- Build processes may be slow on first run (npm install, Vite build)
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Environment Variables
|
||||
|
||||
- API keys should never be committed to repository
|
||||
- Use `.env` files (already in `.gitignore`)
|
||||
- Document required environment variables in project READMEs
|
||||
|
||||
### Python Projects
|
||||
|
||||
- Use virtual environments: `python -m venv venv`
|
||||
- Keep dependencies updated
|
||||
- GitHub tokens should have minimal required permissions
|
||||
|
||||
### GitHub Models Access
|
||||
|
||||
- Personal Access Tokens (PAT) required for GitHub Models
|
||||
- Tokens should be stored as environment variables
|
||||
- Never commit tokens or credentials
|
||||
|
||||
## Additional Notes
|
||||
|
||||
### Target Audience
|
||||
|
||||
- Complete beginners to web development
|
||||
- Students and self-learners
|
||||
- Teachers using the curriculum in classrooms
|
||||
- Content is designed for accessibility and gradual skill building
|
||||
|
||||
### Educational Philosophy
|
||||
|
||||
- Project-based learning approach
|
||||
- Frequent knowledge checks (quizzes)
|
||||
- Hands-on coding exercises
|
||||
- Real-world application examples
|
||||
- Focus on fundamentals before frameworks
|
||||
|
||||
### Repository Maintenance
|
||||
|
||||
- Active community of learners and contributors
|
||||
- Regular updates to dependencies and content
|
||||
- Issues and discussions monitored by maintainers
|
||||
- Translation updates automated via GitHub Actions
|
||||
|
||||
### Related Resources
|
||||
|
||||
- [Microsoft Learn modules](https://docs.microsoft.com/learn/)
|
||||
- [Student Hub resources](https://docs.microsoft.com/learn/student-hub/)
|
||||
- [GitHub Copilot](https://marketplace.visualstudio.com/items?itemName=GitHub.copilot) recommended for learners
|
||||
- Additional courses: Generative AI, Data Science, ML, IoT curricula available
|
||||
|
||||
### Working with Specific Projects
|
||||
|
||||
For detailed instructions on individual projects, refer to the README files in:
|
||||
- `quiz-app/README.md` - Vue 3 quiz application
|
||||
- `7-bank-project/README.md` - Banking application with authentication
|
||||
- `5-browser-extension/README.md` - Browser extension development
|
||||
- `6-space-game/README.md` - Canvas-based game development
|
||||
- `9-chat-project/README.md` - AI chat assistant project
|
||||
|
||||
### Monorepo Structure
|
||||
|
||||
While not a traditional monorepo, this repository contains multiple independent projects:
|
||||
- Each lesson is self-contained
|
||||
- Projects don't share dependencies
|
||||
- Work on individual projects without affecting others
|
||||
- Clone entire repo for the full curriculum experience
|
||||
@ -1,3 +1,108 @@
|
||||
.app-nav ul li {
|
||||
width: max-content;
|
||||
}
|
||||
body {
|
||||
font-family: 'Inter', Arial, sans-serif;
|
||||
}
|
||||
/* Body and Typography */
|
||||
body {
|
||||
font-family: 'Inter', Arial, sans-serif;
|
||||
background: #f7f8fa;
|
||||
color: #222;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Header Styling */
|
||||
h1, h2, h3 {
|
||||
font-weight: 700;
|
||||
margin-top: 1.5em;
|
||||
margin-bottom: 0.5em;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
h1 i, h2 i, h3 i {
|
||||
color: #0078d4;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
/* Navigation Bar */
|
||||
.app-nav {
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
|
||||
padding: 1em 2em;
|
||||
}
|
||||
|
||||
.app-nav ul {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
gap: 2em;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.app-nav ul li {
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.app-nav a {
|
||||
text-decoration: none;
|
||||
color: #0078d4;
|
||||
font-weight: 500;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.app-nav a:hover {
|
||||
color: #005fa3;
|
||||
}
|
||||
|
||||
/* Card Component */
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,0.07);
|
||||
padding: 2em;
|
||||
margin: 2em 0;
|
||||
transition: box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.12);
|
||||
}
|
||||
|
||||
/* Button Styling */
|
||||
.button {
|
||||
background: #0078d4;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.75em 1.5em;
|
||||
font-size: 1em;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background: #005fa3;
|
||||
}
|
||||
|
||||
/* Font Awesome Icon Styling */
|
||||
.fa {
|
||||
margin-right: 0.5em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* Responsive Layout */
|
||||
@media (max-width: 600px) {
|
||||
.app-nav ul {
|
||||
flex-direction: column;
|
||||
gap: 1em;
|
||||
}
|
||||
.card {
|
||||
padding: 1em;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
@ -0,0 +1,36 @@
|
||||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.js`. The page auto-updates as you edit the file.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||
|
After Width: | Height: | Size: 25 KiB |
@ -0,0 +1,26 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export const metadata = {
|
||||
title: "Memory Game",
|
||||
description: "A fun and challenging memory game built with Next.js.",
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
import MemoryGame from '@/components/MemoryGame'
|
||||
import React from 'react'
|
||||
|
||||
const page = () => {
|
||||
return (
|
||||
<div>
|
||||
<MemoryGame />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default page
|
||||
@ -0,0 +1,162 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState, useCallback } from "react";
|
||||
|
||||
// Fisher-Yates shuffle for unbiased randomization
|
||||
function fisherYatesShuffle(array) {
|
||||
const arr = array.slice();
|
||||
for (let i = arr.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[arr[i], arr[j]] = [arr[j], arr[i]];
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
const MemoryGame = () => {
|
||||
const [gridSize, setGridSize] = useState(2);
|
||||
|
||||
const [array, setArray] = useState([]);
|
||||
const [flipped, setFlipped] = useState([]);
|
||||
const [selectedPairs, setSelectedPairs] = useState([]);
|
||||
const [disabled, setDisabled] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const [won, setWon] = useState(false);
|
||||
|
||||
const handleGridSize = (e) => {
|
||||
const size = parseInt(e.target.value);
|
||||
if (2 <= size && size <= 10 && (size % 2 === 0)) {
|
||||
setGridSize(size);
|
||||
setError("");
|
||||
} else {
|
||||
setError("Please enter a grid size where size is even (e.g., 2, 4, 6, 8, 10)");
|
||||
}
|
||||
};
|
||||
|
||||
const initializeGame = useCallback(() => {
|
||||
const totalCards = gridSize * gridSize;
|
||||
const pairCount = totalCards / 2;
|
||||
|
||||
const numbers = [...Array(pairCount).keys()].map((n) => n + 1);
|
||||
const cardNumbers = [...numbers, ...numbers];
|
||||
const shuffledCardNumbers = fisherYatesShuffle(cardNumbers);
|
||||
const shuffledCards = shuffledCardNumbers.map((number, index) => ({
|
||||
id: index,
|
||||
number,
|
||||
}));
|
||||
|
||||
setArray(shuffledCards);
|
||||
setFlipped([]);
|
||||
setSelectedPairs([]);
|
||||
setDisabled(false);
|
||||
setWon(false);
|
||||
}, [gridSize]);
|
||||
|
||||
useEffect(() => {
|
||||
initializeGame();
|
||||
}, [initializeGame]);
|
||||
|
||||
const handleMatch = (secondId) => {
|
||||
const [firstId] = flipped;
|
||||
|
||||
if (array[firstId].number === array[secondId].number) {
|
||||
setSelectedPairs([...selectedPairs, firstId, secondId]);
|
||||
setFlipped([]);
|
||||
setDisabled(false);
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
setDisabled(false);
|
||||
setFlipped([]);
|
||||
}, 1000);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClick = (id) => {
|
||||
if (disabled || won) return;
|
||||
|
||||
if (flipped.length === 0) {
|
||||
setFlipped([id]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (flipped.length === 1) {
|
||||
setDisabled(true);
|
||||
if (id !== flipped[0]) {
|
||||
setFlipped([...flipped, id]);
|
||||
handleMatch(id);
|
||||
} else {
|
||||
setFlipped([]);
|
||||
setDisabled(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const isFlipped = (id) => flipped.includes(id) || selectedPairs.includes(id);
|
||||
const isSelectedPairs = (id) => selectedPairs.includes(id);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedPairs.length === array.length && array.length > 0) {
|
||||
setWon(true);
|
||||
}
|
||||
}, [selectedPairs, array]);
|
||||
|
||||
return (
|
||||
<div className="h-screen flex flex-col justify-center items-center p-4 bg-gray-100 ">
|
||||
{/* Heading */}
|
||||
<h1 className="text-3xl font-bold mb-6">Memory Game</h1>
|
||||
{/* Grid Size */}
|
||||
<div className="mb-4">
|
||||
<label htmlFor="gridSize">Grid Size: (max 10)</label>
|
||||
<input
|
||||
type="number"
|
||||
className="w-[50px] ml-3 rounded border-2 px-1.5 py-1"
|
||||
min="2"
|
||||
max="10"
|
||||
value={gridSize}
|
||||
onChange={handleGridSize}
|
||||
/>
|
||||
{error && (
|
||||
<div className="text-sm text-red-500 mt-2">{error}</div>
|
||||
)}
|
||||
</div>
|
||||
{/* Cards */}
|
||||
<div
|
||||
className="grid gap-2 mb-4"
|
||||
style={{
|
||||
gridTemplateColumns: `repeat(${gridSize}, minmax(0,1fr))`,
|
||||
width: `min(100%,${gridSize * 5.5}rem)`,
|
||||
}}
|
||||
>
|
||||
{array.map((card) => (
|
||||
<div
|
||||
key={card.id}
|
||||
onClick={() => handleClick(card.id)}
|
||||
className={`aspect-square flex items-center justify-center text-xl transition-all duration-300 font-bold rounded-lg cursor-pointer ${
|
||||
isFlipped(card.id)
|
||||
? isSelectedPairs(card.id)
|
||||
? "bg-green-500 text-white"
|
||||
: "bg-blue-500 text-white"
|
||||
: "bg-gray-300 text-gray-400"
|
||||
}`}
|
||||
>
|
||||
{isFlipped(card.id) ? card.number : "?"}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{/* Result */}
|
||||
<div className="text-2xl text-green-500 font-bold">
|
||||
{won ? "You Won!" : ""}
|
||||
</div>
|
||||
|
||||
{/* Reset Button */}
|
||||
<button
|
||||
className="px-5 py-2 bg-green-500 rounded text-white mt-5"
|
||||
onClick={initializeGame}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MemoryGame;
|
||||
@ -0,0 +1,25 @@
|
||||
import { dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
});
|
||||
|
||||
const eslintConfig = [
|
||||
...compat.extends("next/core-web-vitals"),
|
||||
{
|
||||
ignores: [
|
||||
"node_modules/**",
|
||||
".next/**",
|
||||
"out/**",
|
||||
"build/**",
|
||||
"next-env.d.ts",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default eslintConfig;
|
||||
@ -0,0 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {};
|
||||
|
||||
export default nextConfig;
|
||||
@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "memory-game",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build --turbopack",
|
||||
"start": "next start",
|
||||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"next": "15.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"tailwindcss": "^4",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.5.2",
|
||||
"@eslint/eslintrc": "^3"
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
const config = {
|
||||
plugins: ["@tailwindcss/postcss"],
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
After Width: | Height: | Size: 391 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 128 B |
|
After Width: | Height: | Size: 385 B |
|
After Width: | Height: | Size: 281 KiB |
|
After Width: | Height: | Size: 279 KiB |
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 295 KiB |
|
After Width: | Height: | Size: 291 KiB |
|
After Width: | Height: | Size: 690 KiB |
|
After Width: | Height: | Size: 689 KiB |
|
After Width: | Height: | Size: 5.4 MiB |
|
After Width: | Height: | Size: 5.4 MiB |
|
After Width: | Height: | Size: 430 B |
|
After Width: | Height: | Size: 430 B |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 386 KiB |
|
After Width: | Height: | Size: 394 KiB |
|
After Width: | Height: | Size: 383 KiB |
|
After Width: | Height: | Size: 398 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 831 KiB |
|
After Width: | Height: | Size: 831 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 119 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 23 KiB |