first commit
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
.env*
|
||||
node_modules/
|
||||
dist/
|
||||
Vendored
+16
@@ -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",
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+22
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
@@ -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>
|
||||
Generated
+2523
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -0,0 +1,11 @@
|
||||
import Header from "./components/Header"
|
||||
import Main from "./components/Main"
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<Main />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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>);
|
||||
@@ -0,0 +1,8 @@
|
||||
import {defineConfig} from "vite"
|
||||
import react from "@vitejs/plugin-react"
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
react()
|
||||
]
|
||||
})
|
||||
Reference in New Issue
Block a user