first commit

This commit is contained in:
2026-05-26 15:19:33 +02:00
commit 273d9477d7
18 changed files with 2913 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
.env*
node_modules/
dist/
+16
View File
@@ -0,0 +1,16 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Launch in Chrome${cwd}",
"url": "http://localhost:5173",
"webRoot": "${workspaceFolder}/src",
"preLaunchTask": "Start Dev Server",
}
]
}
+22
View File
@@ -0,0 +1,22 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "Start Dev Server",
"type": "shell",
"command": "npx vite",
"isBackground": true,
"problemMatcher": {
"owner": "vite",
"pattern": {
"regexp": "^$"
},
"background": {
"activeOnStart": true,
"beginsPattern": "VITE",
"endsPattern": "Local:.*localhost"
}
}
}
]
}
+6
View File
@@ -0,0 +1,6 @@
Projet issu de la formation React de Scrimba : Générateur de recette.
Utilisation de HuggingFace pour la récupération de recette.
TODO :
▢ Possibilité de supprimer un ingrédient
+21
View File
@@ -0,0 +1,21 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{js,jsx}'],
extends: [
js.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
globals: globals.browser,
parserOptions: { ecmaFeatures: { jsx: true } },
},
},
])
+21
View File
@@ -0,0 +1,21 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<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=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap"
rel="stylesheet">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="src/index.css">
<title>Chef Claude</title>
</head>
<body>
<div id="root"></div>
<script src="/src/index.jsx" type="module"></script>
</body>
</html>
+2523
View File
File diff suppressed because it is too large Load Diff
+28
View File
@@ -0,0 +1,28 @@
{
"type": "module",
"name": "ScrimbaChefClaude",
"description": "https://scrimba.com/learn-react-c0e/~0zg2/s061pnqcvh/head",
"scripts": {
"start": "vite",
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@huggingface/inference": "4.13.15",
"marked-react": "^4.0.0",
"react": "^19.2.6",
"react-dom": "^19.2.6"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.2",
"eslint": "^10.3.0",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.6.0",
"vite": "^8.0.14"
}
}
+11
View File
@@ -0,0 +1,11 @@
import Header from "./components/Header"
import Main from "./components/Main"
export default function App() {
return (
<>
<Header />
<Main />
</>
)
}
+41
View File
@@ -0,0 +1,41 @@
import { InferenceClient } from '@huggingface/inference'
const SYSTEM_PROMPT = `
You are an assistant that receives a list of ingredients that a user has and suggests a recipe they could make with some or all of those ingredients. You don't need to use every ingredient they mention in your recipe. The recipe can include additional ingredients they didn't mention, but try not to include too many extra ingredients. Format your response in markdown to make it easier to render to a web page
`
const hf = new InferenceClient(import.meta.env.VITE_HF_TOKEN)
export async function getRecipeFromHuggingFace(ingredientsArr) {
const ingredientsString = ingredientsArr.join(", ")
const model = await getLightModel()
try {
const response = await hf.chatCompletion({
provider: "hf-inference",
model: model,
messages: [
{ role: "system", content: SYSTEM_PROMPT },
{ role: "user", content: `I have ${ingredientsString}. Please give me a recipe you'd recommend I make!` },
],
max_tokens: 1024,
})
console.log(response)
return response.choices[0].message.content
} catch (err) {
console.error(err.message)
return "Couldn't get recipe."
}
}
async function getLightModel() {
try {
const response = await fetch(
"https://huggingface.co/api/models?inference_provider=hf-inference&pipeline_tag=text-generation&limit=5&filter=conversational",
)
const models = await response.json()
return models[0].id
} catch {
return "katanemo/Arch-Router-1.5B"
}
}
+10
View File
@@ -0,0 +1,10 @@
import chefClaudeLogo from "../images/chef-claude-icon.png"
export default function Header() {
return (
<header>
<img src={chefClaudeLogo} />
<h1>Chef Claude</h1>
</header>
)
}
+18
View File
@@ -0,0 +1,18 @@
export default function IngredientsList(props) {
const ingredientsListItems = props.ingredients.map(ingredient => (
<li key={ingredient}>{ingredient}</li>
))
return (
<section>
<h2>Ingredients on hand:</h2>
<ul className="ingredients-list" aria-live="polite">{ingredientsListItems}</ul>
{props.ingredients.length > 3 && <div className="get-recipe-container">
<div ref={props.ref}>
<h3>Ready for a recipe?</h3>
<p>Generate a recipe from your list of ingredients.</p>
</div>
<button onClick={props.getRecipe}>Get a recipe</button>
</div>}
</section>
)
}
+53
View File
@@ -0,0 +1,53 @@
import React from "react"
import IngredientsList from "./IngredientsList"
import Recipe from "./Recipe"
import { getRecipeFromHuggingFace } from "../ai"
export default function Main() {
const [ingredients, setIngredients] = React.useState(
[]
)
const [recipe, setRecipe] = React.useState("")
const recipeSection = React.useRef(null)
React.useEffect(() => {
if (recipe !== "" && recipeSection.current !== null) {
recipeSection.current.scrollIntoView({ behavior: "smooth" })
}
}, [recipe])
async function getRecipe() {
const result = await getRecipeFromHuggingFace(ingredients)
setRecipe(result)
}
function addIngredient(formData) {
const newIngredient = formData.get("ingredient")
setIngredients(prevIngredients => [...prevIngredients, newIngredient])
}
return (
<main>
<form action={addIngredient} className="add-ingredient-form">
<input
type="text"
placeholder="e.g. oregano"
aria-label="Add ingredient"
name="ingredient"
/>
<button>Add ingredient</button>
</form>
{ingredients.length > 0 &&
<IngredientsList
ref={recipeSection}
ingredients={ingredients}
getRecipe={getRecipe}
/>
}
{recipe && <Recipe recipe={recipe} />}
</main>
)
}
+10
View File
@@ -0,0 +1,10 @@
import Markdown from 'marked-react';
export default function Recipe(props) {
return (
<section className="suggested-recipe-container" aria-live="polite">
<h2>Chef Claude recommends:</h2>
<Markdown>{props.recipe}</Markdown>
</section>
)
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

+117
View File
@@ -0,0 +1,117 @@
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: Inter, sans-serif;
background-color: #FAFAF8;
}
header {
display: flex;
justify-content: center;
align-items: center;
gap: 11px;
height: 80px;
background-color: white;
box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.10), 0px 1px 2px 0px rgba(0, 0, 0, 0.06);
}
header > img {
width: 50px;
}
header > h1 {
font-weight: 400;
}
main {
padding: 30px 30px 10px;
}
.add-ingredient-form {
display: flex;
justify-content: center;
gap: 12px;
height: 38px;
}
.add-ingredient-form > input {
border-radius: 6px;
border: 1px solid #D1D5DB;
padding: 9px 13px;
box-shadow: 0px 1px 2px 0px rgba(0, 0, 0, 0.05);
flex-grow: 1;
min-width: 150px;
max-width: 400px;
}
.add-ingredient-form > button {
font-family: Inter, sans-serif;
border-radius: 6px;
border: none;
background-color: #141413;
color: #FAFAF8;
width: 150px;
font-size: 0.875rem;
font-weight: 500;
}
.add-ingredient-form > button::before {
content: "+";
margin-right: 5px;
}
ul.ingredients-list {
margin-bottom: 48px;
}
ul.ingredients-list > li {
color: #475467;
line-height: 28px;
}
.get-recipe-container {
display: flex;
justify-content: space-between;
align-items: center;
border-radius: 8px;
background: #F0EFEB;
padding: 10px 28px;
}
.get-recipe-container h3 {
font-size: 1.125rem;
font-weight: 500;
line-height: 24px;
}
.get-recipe-container p {
color: #6B7280;
font-size: 0.875rem;
line-height: 20px;
}
.get-recipe-container button {
border: none;
border-radius: 6px;
background: #D17557;
box-shadow: 0px 1px 2px 0px rgba(0, 0, 0, 0.05);
color: #FAFAF8;
padding: 9px 17px;
font-family: Inter, sans-serif;
font-size: 0.875rem;
cursor: pointer;
}
.suggested-recipe-container {
color: #475467;
line-height: 28px;
font-size: 1.125rem;
font-weight: 400;
}
.suggested-recipe-container ul li, .suggested-recipe-container ol li {
margin-bottom: 8px;
}
+5
View File
@@ -0,0 +1,5 @@
import ReactDOM from 'react-dom/client';
import App from "./App"
import { StrictMode } from 'react';
ReactDOM.createRoot(document.getElementById('root')).render(<StrictMode><App /></StrictMode>);
+8
View File
@@ -0,0 +1,8 @@
import {defineConfig} from "vite"
import react from "@vitejs/plugin-react"
export default defineConfig({
plugins: [
react()
]
})