|
/** @jsxImportSource https://esm.sh/[email protected] */ |
|
import OpenAI from "https://esm.sh/[email protected]"; |
|
import { createRoot } from "https://esm.sh/[email protected]/client"; |
|
import React, { useEffect, useRef, useState } from "https://esm.sh/[email protected]"; |
|
|
|
interface Pokemon { |
|
id: number; |
|
name: string; |
|
sprites: { front_default: string }; |
|
stats: Array<{ base_stat: number; stat: { name: string } }>; |
|
types: Array<{ type: { name: string } }>; |
|
} |
|
|
|
// Server-side API key retrieval function |
|
async function getCerebrasKey() { |
|
try { |
|
const response = await fetch("/api/cerebras-key"); |
|
const data = await response.json(); |
|
return data.apiKey; |
|
} catch (error) { |
|
console.error("Failed to retrieve Cerebras API key:", error); |
|
return null; |
|
} |
|
} |
|
|
|
// Utility function to parse and format recommendation |
|
function formatRecommendation(text: string) { |
|
// Split recommendation into sections based on emojis |
|
const sections = text.split(/🏆|🔍|⚔️/).filter(Boolean); |
|
const sectionTitles = text.match(/🏆|🔍|⚔️/g) || []; |
|
|
|
return sections.map((section, index) => ( |
|
<div key={index} style={{ marginBottom: "15px" }}> |
|
<h3 style={{ display: "flex", alignItems: "center", color: "#2c3e50" }}> |
|
<span style={{ marginRight: "10px" }}>{sectionTitles[index]}</span> |
|
{index === 0 && "Recommended Pokémon"} |
|
{index === 1 && "Strategy"} |
|
{index === 2 && "Type Advantages"} |
|
</h3> |
|
<p style={{ lineHeight: "1.6", color: "#34495e" }}>{section.trim()}</p> |
|
</div> |
|
)); |
|
} |
|
|
|
function App() { |
|
const [pokemons, setPokemons] = useState<Pokemon[]>([]); |
|
const [battleRecommendation, setBattleRecommendation] = useState<string>(""); |
|
const [challengerInput, setChallengerInput] = useState<string>(""); |
|
const [error, setError] = useState<string | null>(null); |
|
const [apiKey, setApiKey] = useState<string | null>(null); |
|
|
|
// State for user-specified Pokémon inputs |
|
const [userPokemonInputs, setUserPokemonInputs] = useState<string[]>(["", "", ""]); |
|
|
|
// Initialize cache using useRef |
|
const pokemonCache = useRef<{ [key: number]: Pokemon }>({}); |
|
|
|
// Data Validation Function |
|
function validatePokemonData(pokemon: any): Pokemon | null { |
|
if ( |
|
typeof pokemon.id !== "number" |
|
|| typeof pokemon.name !== "string" |
|
|| !pokemon.sprites?.front_default |
|
|| !Array.isArray(pokemon.stats) |
|
|| !Array.isArray(pokemon.types) |
|
) { |
|
console.warn("Invalid Pokémon data:", pokemon); |
|
return null; |
|
} |
|
|
|
// Further validation can be added here (e.g., checking stats ranges) |
|
return pokemon as Pokemon; |
|
} |
|
|
|
// Handle input change for user-specified Pokémon |
|
function handleUserInputChange(index: number, value: string) { |
|
const newInputs = [...userPokemonInputs]; |
|
newInputs[index] = value; |
|
setUserPokemonInputs(newInputs); |
|
} |
|
|
|
// Load user-specified Pokémon with verification |
|
async function loadUserPokemons() { |
|
const fetchedPokemons: Pokemon[] = []; |
|
for (const name of userPokemonInputs) { |
|
if (name.trim() === "") continue; |
|
try { |
|
const response = await fetch(`https://pokeapi.co/api/v2/pokemon/${name.toLowerCase()}`); |
|
if (!response.ok) { |
|
setError(`Pokémon "${name}" not found. Please check the name and try again.`); |
|
return; |
|
} |
|
const data = await response.json(); |
|
const validatedPokemon = validatePokemonData(data); |
|
if (validatedPokemon) { |
|
fetchedPokemons.push(validatedPokemon); |
|
// Cache the valid Pokémon |
|
pokemonCache.current[validatedPokemon.id] = validatedPokemon; |
|
} else { |
|
setError(`Invalid data for Pokémon: ${name}`); |
|
return; |
|
} |
|
} catch (err) { |
|
console.error(`Error fetching Pokémon ${name}:`, err); |
|
setError(`Failed to fetch Pokémon: ${name}. Please try again.`); |
|
return; |
|
} |
|
} |
|
|
|
if (fetchedPokemons.length > 0) { |
|
setPokemons(fetchedPokemons); |
|
setError(null); |
|
} else { |
|
setError("No valid Pokémon names entered."); |
|
} |
|
} |
|
|
|
// Fetch random Pokémon with verification |
|
async function fetchRandomPokemon() { |
|
const pokemonIds = Array.from({ length: 3 }, () => Math.floor(Math.random() * 898) + 1); |
|
const fetchedPokemons: Pokemon[] = []; |
|
|
|
for (const id of pokemonIds) { |
|
// Check cache first |
|
if (pokemonCache.current[id]) { |
|
fetchedPokemons.push(pokemonCache.current[id]); |
|
} else { |
|
try { |
|
const res = await fetch(`https://pokeapi.co/api/v2/pokemon/${id}`); |
|
if (!res.ok) { |
|
setError("Failed to fetch a valid Pokémon. Please try again."); |
|
return; |
|
} |
|
const data = await res.json(); |
|
const validatedPokemon = validatePokemonData(data); |
|
if (validatedPokemon) { |
|
pokemonCache.current[id] = validatedPokemon; // Cache the valid Pokémon |
|
fetchedPokemons.push(validatedPokemon); |
|
} else { |
|
setError("Fetched Pokémon data is invalid. Please try again."); |
|
return; |
|
} |
|
} catch (err) { |
|
console.error(`Error fetching Pokémon with ID ${id}:`, err); |
|
setError("Failed to fetch Pokémon data. Please try again."); |
|
return; |
|
} |
|
} |
|
} |
|
|
|
setPokemons(fetchedPokemons); |
|
// Reset any previous errors when fetching new Pokémon |
|
setError(null); |
|
} |
|
|
|
useEffect(() => { |
|
// Initially load random Pokémon |
|
fetchRandomPokemon(); |
|
// Fetch API key when component mounts |
|
getCerebrasKey().then(key => { |
|
if (key) { |
|
setApiKey(key); |
|
} else { |
|
setError("Failed to retrieve Cerebras API key"); |
|
} |
|
}); |
|
}, []); |
|
|
|
async function generateBattleRecommendation() { |
|
// Clear previous recommendation and errors |
|
setBattleRecommendation(""); |
|
setError(null); |
|
|
|
// Validate inputs |
|
if (pokemons.length === 0) { |
|
setError("Please generate or specify Pokémon first"); |
|
return; |
|
} |
|
|
|
if (!challengerInput.trim()) { |
|
setError("Please enter a challenger's Pokémon"); |
|
return; |
|
} |
|
|
|
if (!apiKey) { |
|
setError("Cerebras API key is not available"); |
|
return; |
|
} |
|
|
|
try { |
|
const { OpenAI } = await import("https://esm.sh/openai"); |
|
const client = new OpenAI({ |
|
apiKey: apiKey, |
|
baseURL: "https://api.cerebras.ai/v1", |
|
dangerouslyAllowBrowser: true, // Added to allow browser-based API calls |
|
}); |
|
|
|
const recommendation = await client.chat.completions.create({ |
|
model: "llama-3.3-70b", |
|
messages: [ |
|
{ |
|
role: "system", |
|
content: ` |
|
You are an expert Pokémon battle strategist familiar with all official Pokémon game mechanics, types, stats, abilities, and move sets. Provide detailed and realistic battle recommendations that adhere to the actual rules and dynamics of Pokémon battles. |
|
Ensure that all suggestions are viable within the context of the Pokémon games and do not include overpowered or non-existent strategies. |
|
Format your responses with the following sections using these prefixes: |
|
🏆 Recommended Pokémon: |
|
🔍 Strategy: |
|
⚔️ Type Advantages: |
|
`, |
|
}, |
|
{ |
|
role: "user", |
|
content: `Comprehensive battle strategy analysis: |
|
My Team: ${ |
|
pokemons.map(p => |
|
`${p.name.toUpperCase()} (${p.types.map(t => t.type.name.toUpperCase()).join("/")}) |
|
Stats - HP: ${p.stats.find(s => s.stat.name === "hp")?.base_stat}, |
|
Attack: ${p.stats.find(s => s.stat.name === "attack")?.base_stat}, |
|
Defense: ${p.stats.find(s => s.stat.name === "defense")?.base_stat}` |
|
).join(", ") |
|
} |
|
Challenger's Pokémon: ${challengerInput.toUpperCase()} |
|
|
|
Provide a strategic battle recommendation that includes: |
|
1. Type advantages and disadvantages |
|
2. Recommended Pokémon from my team |
|
3. Potential battle strategies |
|
4. Specific move suggestions |
|
5. Defensive and offensive considerations`, |
|
}, |
|
], |
|
max_tokens: 1500, |
|
}); |
|
|
|
const recommendationText = recommendation.choices[0]?.message?.content; |
|
|
|
if (recommendationText) { |
|
setBattleRecommendation(recommendationText); |
|
} else { |
|
setError("No recommendation could be generated. Please try again."); |
|
} |
|
} catch (error) { |
|
console.error("Battle recommendation error:", error); |
|
setError(`Failed to generate recommendation: ${error instanceof Error ? error.message : "Unknown error"}`); |
|
} |
|
} |
|
|
|
return ( |
|
<div |
|
style={{ |
|
fontFamily: "Arial, sans-serif", |
|
maxWidth: "800px", |
|
margin: "auto", |
|
padding: "20px", |
|
boxSizing: "border-box", |
|
width: "100%", |
|
}} |
|
> |
|
<h1 style={{ textAlign: "center", color: "#2c3e50" }}>🎮 Pokémon Battle Advisor 🃏</h1> |
|
<p style={{ textAlign: "center", color: "#7f8c8d" }}> |
|
Get tailored battle strategies by specifying your Pokémon or randomizing them! |
|
</p> |
|
|
|
{/* User-Specified Pokémon Inputs */} |
|
<div style={{ marginTop: "20px" }}> |
|
<h2>🔧 Specify Your Pokémon</h2> |
|
<p style={{ color: "#7f8c8d" }}>Enter the names of up to three Pokémon to include in your team.</p> |
|
<div style={{ display: "flex", flexDirection: "column", gap: "10px" }}> |
|
{userPokemonInputs.map((input, index) => ( |
|
<input |
|
key={index} |
|
type="text" |
|
value={input} |
|
onChange={(e) => handleUserInputChange(index, e.target.value)} |
|
placeholder={`Enter Pokémon ${index + 1} Name`} |
|
style={{ |
|
width: "100%", |
|
padding: "10px", |
|
border: "1px solid #ccc", |
|
borderRadius: "5px", |
|
boxSizing: "border-box", |
|
}} |
|
title={`Enter the name of your Pokémon ${index + 1}`} |
|
/> |
|
))} |
|
<button |
|
onClick={loadUserPokemons} |
|
style={{ |
|
padding: "10px 20px", |
|
backgroundColor: "#8e44ad", |
|
color: "white", |
|
border: "none", |
|
borderRadius: "5px", |
|
cursor: "pointer", |
|
transition: "background-color 0.3s", |
|
}} |
|
onMouseEnter={(e) => { |
|
(e.currentTarget as HTMLButtonElement).style.backgroundColor = "#732d91"; |
|
}} |
|
onMouseLeave={(e) => { |
|
(e.currentTarget as HTMLButtonElement).style.backgroundColor = "#8e44ad"; |
|
}} |
|
title="Load your specified Pokémon into the team" |
|
> |
|
🎯 Load Specified Pokémon |
|
</button> |
|
</div> |
|
</div> |
|
|
|
{/* Or */} |
|
<div style={{ textAlign: "center", margin: "20px 0", color: "#7f8c8d" }}> |
|
<span>— OR —</span> |
|
</div> |
|
|
|
{/* Randomize Pokémon */} |
|
<div style={{ textAlign: "center" }}> |
|
<button |
|
onClick={fetchRandomPokemon} |
|
style={{ |
|
padding: "10px 20px", |
|
backgroundColor: "#4CAF50", |
|
color: "white", |
|
border: "none", |
|
borderRadius: "5px", |
|
cursor: "pointer", |
|
transition: "background-color 0.3s", |
|
width: "100%", |
|
}} |
|
onMouseEnter={(e) => { |
|
(e.currentTarget as HTMLButtonElement).style.backgroundColor = "#45a049"; |
|
}} |
|
onMouseLeave={(e) => { |
|
(e.currentTarget as HTMLButtonElement).style.backgroundColor = "#4CAF50"; |
|
}} |
|
title="Fetch a random set of Pokémon for your team" |
|
> |
|
🔀 Randomize Pokémon |
|
</button> |
|
</div> |
|
|
|
{/* Pokémon Display Grid */} |
|
<div |
|
style={{ |
|
display: "grid", |
|
gridTemplateColumns: "repeat(auto-fill, minmax(200px, 1fr))", |
|
gap: "10px", |
|
marginTop: "20px", |
|
}} |
|
> |
|
{pokemons.map((pokemon, index) => ( |
|
<div |
|
key={pokemon.id} |
|
style={{ |
|
border: "1px solid #ddd", |
|
borderRadius: "8px", |
|
padding: "10px", |
|
textAlign: "center", |
|
backgroundColor: "#f9f9f9", |
|
transition: "transform 0.2s", |
|
}} |
|
onMouseEnter={(e) => { |
|
(e.currentTarget as HTMLDivElement).style.transform = "scale(1.05)"; |
|
}} |
|
onMouseLeave={(e) => { |
|
(e.currentTarget as HTMLDivElement).style.transform = "scale(1)"; |
|
}} |
|
> |
|
<img |
|
src={pokemon.sprites.front_default} |
|
alt={pokemon.name} |
|
style={{ width: "100%", maxHeight: "150px" }} |
|
/> |
|
<h3 style={{ color: "#3498db" }}>{pokemon.name.toUpperCase()}</h3> |
|
<p>Type: {pokemon.types.map(t => t.type.name).join("/")}</p> |
|
<div> |
|
{pokemon.stats.map(stat => ( |
|
<div key={stat.stat.name}> |
|
<strong>{stat.stat.name.toUpperCase()}:</strong> {stat.base_stat} |
|
</div> |
|
))} |
|
</div> |
|
</div> |
|
))} |
|
</div> |
|
|
|
{/* Challenger Input */} |
|
<div style={{ marginTop: "20px" }}> |
|
<h2>🗡️ Enter Challenger's Pokémon</h2> |
|
<p style={{ color: "#7f8c8d" }}> |
|
Provide the name of your opponent's Pokémon to receive a battle recommendation. |
|
</p> |
|
<input |
|
type="text" |
|
value={challengerInput} |
|
onChange={(e) => setChallengerInput(e.target.value)} |
|
placeholder="Enter challenger's Pokémon" |
|
style={{ |
|
width: "100%", |
|
padding: "10px", |
|
marginBottom: "10px", |
|
border: "1px solid #ccc", |
|
borderRadius: "5px", |
|
boxSizing: "border-box", |
|
}} |
|
title="Enter the name of your challenger's Pokémon" |
|
/> |
|
<button |
|
onClick={generateBattleRecommendation} |
|
style={{ |
|
padding: "10px 20px", |
|
backgroundColor: "#2196F3", |
|
color: "white", |
|
border: "none", |
|
borderRadius: "5px", |
|
cursor: "pointer", |
|
transition: "background-color 0.3s", |
|
width: "100%", |
|
}} |
|
onMouseEnter={(e) => { |
|
(e.currentTarget as HTMLButtonElement).style.backgroundColor = "#1976D2"; |
|
}} |
|
onMouseLeave={(e) => { |
|
(e.currentTarget as HTMLButtonElement).style.backgroundColor = "#2196F3"; |
|
}} |
|
title="Generate a battle recommendation based on your team and the challenger" |
|
> |
|
🤖 Get Battle Recommendation |
|
</button> |
|
</div> |
|
|
|
{/* Error Message */} |
|
{error && ( |
|
<div |
|
style={{ |
|
marginTop: "20px", |
|
padding: "15px", |
|
backgroundColor: "#ffdddd", |
|
borderRadius: "8px", |
|
color: "#ff0000", |
|
}} |
|
> |
|
<h3>⚠️ Error:</h3> |
|
<p>{error}</p> |
|
</div> |
|
)} |
|
|
|
{/* Battle Recommendation Display */} |
|
{battleRecommendation && ( |
|
<div |
|
style={{ |
|
marginTop: "20px", |
|
padding: "15px", |
|
backgroundColor: "#ecf0f1", |
|
borderRadius: "8px", |
|
boxShadow: "0 4px 6px rgba(0,0,0,0.1)", |
|
}} |
|
> |
|
<h3 |
|
style={{ |
|
color: "#2c3e50", |
|
borderBottom: "2px solid #3498db", |
|
paddingBottom: "10px", |
|
display: "flex", |
|
alignItems: "center", |
|
}} |
|
> |
|
🏆 Battle Recommendation |
|
</h3> |
|
<div |
|
style={{ |
|
backgroundColor: "white", |
|
padding: "15px", |
|
borderRadius: "5px", |
|
maxHeight: "400px", |
|
overflowY: "auto", |
|
}} |
|
> |
|
{formatRecommendation(battleRecommendation)} |
|
</div> |
|
</div> |
|
)} |
|
|
|
{/* View Source Link */} |
|
<a |
|
href={import.meta.url.replace("esm.town", "val.town")} |
|
target="_top" |
|
style={{ |
|
display: "block", |
|
marginTop: "20px", |
|
color: "#888", |
|
textDecoration: "none", |
|
}} |
|
> |
|
View Source |
|
</a> |
|
</div> |
|
); |
|
} |
|
|
|
function client() { |
|
createRoot(document.getElementById("root")).render(<App />); |
|
} |
|
if (typeof document !== "undefined") { client(); } |
|
|
|
export default async function server(request: Request): Promise<Response> { |
|
// Check if this is a request for the API key |
|
const url = new URL(request.url); |
|
if (url.pathname === "/api/cerebras-key") { |
|
const apiKey = Deno.env.get("CEREBRAS_API_KEY"); |
|
return new Response(JSON.stringify({ apiKey }), { |
|
headers: { "Content-Type": "application/json" }, |
|
}); |
|
} |
|
|
|
// Regular HTML response |
|
return new Response( |
|
` |
|
<html> |
|
<head> |
|
<title>Pokémon Battle Advisor</title> |
|
<meta name="viewport" content="width=device-width, initial-scale=1"> |
|
</head> |
|
<body> |
|
<div id="root"></div> |
|
<script src="https://esm.town/v/std/catch"></script> |
|
<script type="module" src="${import.meta.url}"></script> |
|
</body> |
|
</html> |
|
`, |
|
{ |
|
headers: { "content-type": "text/html" }, |
|
}, |
|
); |
|
} |