Här är en **komplett plan i markdown-format** som du kan spara som en fil (t.ex. `premium_recipe_feature_plan.md`) och ladda upp i **VS Code-chatten** för att implementera **AI som en premium-funktion** för receptgenerering. Filen är strukturerad för att vara **lätt att följa** och innehåller alla nödvändiga steg, kodsnuttar och förklaringar. --- ```markdown # 📌 Plan: Implementera AI som Premium-Funktion för Receptgenerering --- ## **🎯 Översikt** Denna plan beskriver hur du implementerar **AI som en premium-funktion** i din app, där: - **Gratis-användare** får **grundläggande recept** (genererade från mallar). - **Premium-användare** får **AI-genererade recept** (mer kreativa, personliga och intelligenta). - Systemet är **modulärt** och kan enkelt utökas. --- --- ## **📋 Förutsättningar** 1. **Node.js/TypeScript** (backend). 2. **Prisma** (databas). 3. **pdf-parse** och **Tesseract.js** (PDF-extrahering). 4. **Mistral API-nyckel** (för AI-funktioner). 5. **Frontend-ramverk** (t.ex. React, Next.js, eller liknande). --- --- ## **🛠️ Databasschema (Prisma)** Lägg till fält för att spåra **premium-status** i din `user`-tabell: ```prisma model User { id Int @id @default(autoincrement()) // ... andra fält ... is_premium Boolean @default(false) premium_expiry_date DateTime? } ``` **SQL för att lägga till fält om tabellen redan finns:** ```sql ALTER TABLE "User" ADD COLUMN "is_premium" BOOLEAN DEFAULT false; ALTER TABLE "User" ADD COLUMN "premium_expiry_date" TIMESTAMP; ``` --- --- ## **📁 Filstruktur** ``` src/ ├── services/ │ ├── pdfTextExtractor.ts # Extraherar text från PDF (pdf-parse + OCR) │ ├── willysParser.ts # Parsar text till strukturerad data (regex) │ ├── inventoryMatcher.ts # Matchar produkter med inventory (fuzzy matching) │ └── recipeGenerator.ts # Genererar recept (AI för premium, mallar för gratis) ├── api/ │ ├── pdfImport.ts # API-endpoint för PDF-import │ └── premium.ts # API-endpoint för premium-uppgradering └── types/ └── recipeTypes.ts # Typer för recept och produkter ``` --- --- ## **📄 1. `pdfTextExtractor.ts` – Extrahera text från PDF** Använder `pdf-parse` som primär metod och `Tesseract.js` som fallback för OCR. ```typescript import * as fs from 'fs'; import * as pdf from 'pdf-parse'; import Tesseract from 'tesseract.js'; /** * Extraherar text från en PDF-fil, med fallback till OCR om nödvändigt. * @param pdfPath Sökväg till PDF-filen. * @returns Extraherad text. */ export async function extractTextFromPDF(pdfPath: string): Promise { try { const dataBuffer = fs.readFileSync(pdfPath); const data = await pdf(dataBuffer); if (data.text.trim()) return data.text; } catch (error) { console.warn('pdf-parse misslyckades, försöker med OCR...', error); } try { const { data: { text } } = await Tesseract.recognize(pdfPath, 'swe', { logger: (m) => console.log(m), }); return text; } catch (error) { console.error('OCR misslyckades:', error); throw new Error('Kunde inte extrahera text från PDF:en.'); } } ``` --- --- ## **📄 2. `willysParser.ts` – Parsa text till strukturerad data** Använder **regex** för att extrahera produktnamn, priser, jämförpriser, etc. från Willys veckoblad. ```typescript /** * Parsar text från Willys veckoblad till strukturerad data. * @param text Den extraherade texten. * @returns Strukturerad data (JSON-array). */ export function parseWillysText(text: string): any[] { const lines = text.split('\n'); const products: any[] = []; let currentProduct: any = {}; let currentCategory: string | null = null; // Regex-mönster const productLineRegex = /^(.*?)\s*•\s*(.*?)\s*•\s*(.*?)\s*•?\s*Jämförpris\s*([\d:]+)\s*kr\/([a-z]+)/i; const simpleProductLineRegex = /^(.*?)\s*•\s*(.*?)\s*•\s*(.*?)$/i; const priceLineRegex = /^([\d:]+)\s*(Per\s*(förp|kg|st|l|))?/i; const offerLineRegex = /^(Max \d+ (köp|förp)\/hushåll|Lägsta 30-dgrspris [\d:]+ kr)/i; const categoryLineRegex = /^(Fisk|Kött|Mejeri|Grönsaker|Frukt|Dryck|Bröd|Pasta|Ris)/i; for (const line of lines) { const trimmedLine = line.trim(); if (!trimmedLine) continue; // Matcha kategori const categoryMatch = trimmedLine.match(categoryLineRegex); if (categoryMatch) { currentCategory = categoryMatch[1]; continue; } // Matcha produktrad (med jämförpris) const productMatch = trimmedLine.match(productLineRegex); if (productMatch) { if (Object.keys(currentProduct).length > 0) { currentProduct.category = currentCategory; products.push(currentProduct); currentProduct = {}; } currentProduct = { name: productMatch[1].trim(), weight: productMatch[2].trim(), origin: productMatch[3].trim(), comparisonPrice: `${productMatch[4].replace(':', '.')} kr/${productMatch[5]}`, category: currentCategory, }; continue; } // Matcha enkel produktrad const simpleProductMatch = trimmedLine.match(simpleProductLineRegex); if (simpleProductMatch) { if (Object.keys(currentProduct).length > 0) { currentProduct.category = currentCategory; products.push(currentProduct); currentProduct = {}; } currentProduct = { name: simpleProductMatch[1].trim(), weight: simpleProductMatch[2].trim(), origin: simpleProductMatch[3].trim(), category: currentCategory, }; continue; } // Matcha pris const priceMatch = trimmedLine.match(priceLineRegex); if (priceMatch) { currentProduct.price = `${priceMatch[1].replace(':', '.')} kr/${priceMatch[2]?.trim() || 'förp'}`; continue; } // Matcha erbjudande const offerMatch = trimmedLine.match(offerLineRegex); if (offerMatch) { currentProduct.offer = offerMatch[1]; continue; } } if (Object.keys(currentProduct).length > 0) { currentProduct.category = currentCategory; products.push(currentProduct); } return products; } ``` --- --- ## **📄 3. `inventoryMatcher.ts` – Matcha produkter med inventory** Använder **fuzzy matching** för att matcha produktnamn från PDF:en med användarens inventory. ```typescript import { stringSimilarity } from 'string-similarity'; /** * Matchar produkter från PDF:en med användarens inventory. * @param products Produkter från PDF:en. * @param userId Användarens ID. * @returns Matchade produkter med inventory-status. */ export async function matchProductsWithInventory(products: any[], userId: number) { const inventory = await prisma.userInventory.findMany({ where: { userId }, }); const inventoryNames = inventory.map((item) => ({ name: item.name.toLowerCase(), id: item.id, quantity: item.quantity, category: item.category, })); return products.map((product) => { const productName = product.name.toLowerCase(); const bestMatch = inventoryNames.reduce( (best, item) => { const similarity = stringSimilarity.compareTwoStrings(productName, item.name); return similarity > best.similarity ? { ...item, similarity } : best; }, { name: '', similarity: 0, id: null, quantity: 0, category: null } ); const inventoryItem = bestMatch.similarity > 0.6 ? inventory.find((item) => item.id === bestMatch.id) : null; return { ...product, inInventory: !!inventoryItem, inventoryId: inventoryItem?.id || null, inventoryQuantity: inventoryItem?.quantity || 0, inventoryMatchSimilarity: bestMatch.similarity, }; }); } ``` --- --- ## **📄 4. `recipeGenerator.ts` – Generera recept (AI eller mallar)** Modulär funktion som använder **AI för premium-användare** och **mallar för gratis-användare**. ```typescript import { MistralClient } from '@mistralai/mistralai'; import { prisma } from '../prisma'; // Antas att Prisma är konfigurerat const mistral = new MistralClient({ apiKey: process.env.MISTRAL_API_KEY }); /** * Genererar recept baserat på matchade produkter. * @param matchedProducts Produkter matchade med inventory. * @param userId Användarens ID. * @param userPreferences Användarens preferenser. * @returns Genererade recept. */ export async function generateRecipes( matchedProducts: any[], userId: number, userPreferences: any = {} ): Promise { const user = await prisma.user.findUnique({ where: { id: userId } }); const isPremium = user?.is_premium || false; if (isPremium) { return generateRecipesWithAI(matchedProducts, userPreferences); } else { return generateRecipesFromTemplates(matchedProducts); } } /** * Genererar AI-recept för premium-användare. */ async function generateRecipesWithAI(matchedProducts: any[], userPreferences: any): Promise { const relevantProducts = matchedProducts.filter((p) => p.inInventory || p.offer); if (relevantProducts.length === 0) return []; const prompt = ` Du är en kock som skapar kostnadseffektiva, läckra och personliga recept. Skapa **3 recept** baserat på följande produkter: ${JSON.stringify(relevantProducts, null, 2)} Användarens preferenser: - Diet: ${userPreferences.diet || 'Inga restriktioner'} - Köksstil: ${userPreferences.cuisine || 'Nordisk'} - Tid: ${userPreferences.time || 'Under 30 minuter'} Recepten ska: 1. Använda produkter från inventory eller på erbjudande. 2. Vara lätt att laga. 3. Inkludera näringsinformation (uppskattad). 4. Vara på svenska. Returnera recepten i JSON-format: { "recipes": [ { "title": "Receptets titel", "description": "Beskrivning", "ingredients": [ {"name": "Produktnamn", "quantity": "Mängd", "fromInventory": true/false, "onOffer": true/false} ], "instructions": ["Steg 1", "Steg 2"], "cost": "Uppskattad kostnad", "time": "Tillagningstid", "nutritionalInfo": {"calories": "Kalorier", "protein": "Protein (g)"} } ] } `; try { const response = await mistral.chat({ model: 'mistral-small-latest', messages: [{ role: 'user', content: prompt }], temperature: 0.7, }); const jsonString = response.choices[0].message.content .replace(/```json|```/g, '') .trim(); const parsed = JSON.parse(jsonString); return parsed.recipes || []; } catch (error) { console.error('AI-generering misslyckades:', error); return generateRecipesFromTemplates(matchedProducts); } } /** * Genererar recept från mallar för gratis-användare. */ function generateRecipesFromTemplates(matchedProducts: any[]): any[] { const recipes: any[] = []; const relevantProducts = matchedProducts.filter((p) => p.inInventory || p.offer); if (relevantProducts.length === 0) return recipes; const productsByCategory: Record = {}; relevantProducts.forEach((product) => { const category = product.category || 'Okänt'; if (!productsByCategory[category]) productsByCategory[category] = []; productsByCategory[category].push(product); }); const templates: Record = { Fisk: [ { title: 'Lax med potatis och grönsaker', ingredients: [ { name: 'Lax', category: 'Fisk', required: true }, { name: 'Potatis', category: 'Grönsaker', required: true }, ], instructions: [ 'Koka potatisen i 15 minuter.', 'Stek laxen i 5 minuter på varje sida.', 'Servera med grönsaker.', ], time: '25 minuter', }, ], Kött: [ { title: 'Köttbullar med potatismos', ingredients: [ { name: 'Köttfärs', category: 'Kött', required: true }, { name: 'Potatis', category: 'Grönsaker', required: true }, ], instructions: [ 'Blanda köttfärs med salt och peppar.', 'Stek köttbullarna i en panna.', 'Koka och mosa potatisen.', ], time: '30 minuter', }, ], }; for (const [category, categoryTemplates] of Object.entries(templates)) { const categoryProducts = productsByCategory[category] || []; for (const template of categoryTemplates) { const missingIngredients = template.ingredients.filter( (ing: any) => ing.required && !categoryProducts.some((p) => p.name.toLowerCase().includes(ing.name.toLowerCase()) ) ); if (missingIngredients.length === 0) { recipes.push({ title: template.title, description: `Ett enkelt ${category.toLowerCase()}-recept.`, ingredients: template.ingredients.map((ing: any) => { const matchedProduct = categoryProducts.find((p) => p.name.toLowerCase().includes(ing.name.toLowerCase()) ); return { name: matchedProduct?.name || ing.name, quantity: matchedProduct?.weight || '1 portion', fromInventory: matchedProduct?.inInventory || false, onOffer: matchedProduct?.offer ? true : false, }; }), instructions: template.instructions, time: template.time, cost: calculateRecipeCost(template.ingredients, categoryProducts), }); } } } return recipes; } function calculateRecipeCost(ingredients: any[], products: any[]): string { let totalCost = 0; for (const ing of ingredients) { const matchedProduct = products.find((p) => p.name.toLowerCase().includes(ing.name.toLowerCase()) ); if (matchedProduct?.price) { const price = parseFloat(matchedProduct.price.replace(/[^\d.]/g, '')); totalCost += price; } } return `Ca ${totalCost.toFixed(2)} kr`; } ``` --- --- ## **📄 5. `pdfImportService.ts` – Fullständigt importflöde** Haneterar hela flödet från PDF-import till receptgenerering. ```typescript import { extractTextFromPDF } from './pdfTextExtractor'; import { parseWillysText } from './willysParser'; import { matchProductsWithInventory } from './inventoryMatcher'; import { generateRecipes } from './recipeGenerator'; import { prisma } from '../prisma'; /** * Importerar en PDF och genererar recept. * @param pdfPath Sökväg till PDF-filen. * @param userId Användarens ID. * @param userPreferences Användarens preferenser. * @returns Resultat med produkter och recept. */ export async function importPDFAndGenerateRecipes( pdfPath: string, userId: number, userPreferences: any = {} ) { try { // 1. Extrahera text const text = await extractTextFromPDF(pdfPath); // 2. Parsa texten const products = parseWillysText(text); // 3. Matcha med inventory const matchedProducts = await matchProductsWithInventory(products, userId); // 4. Generera recept const recipes = await generateRecipes(matchedProducts, userId, userPreferences); // 5. Spara recepten const savedRecipes = []; for (const recipe of recipes) { const savedRecipe = await prisma.encryptedData.create({ data: { encrypted_data: JSON.stringify(recipe), owner_id: userId, created_at: new Date(), is_generated: true, source: user.is_premium ? 'AI (Premium)' : 'Mallar (Gratis)', }, }); savedRecipes.push(savedRecipe); } return { success: true, products: matchedProducts, recipes: savedRecipes, isPremium: await isUserPremium(userId), }; } catch (error) { console.error('Fel vid import:', error); return { success: false, error: error instanceof Error ? error.message : 'Okänt fel' }; } } /** * Kontrollerar om en användare är premium. */ async function isUserPremium(userId: number): Promise { const user = await prisma.user.findUnique({ where: { id: userId } }); if (!user) return false; if (user.premium_expiry_date && new Date(user.premium_expiry_date) < new Date()) { await prisma.user.update({ where: { id: userId }, data: { is_premium: false }, }); return false; } return user.is_premium; } ``` --- --- ## **📄 6. API-Endpoints** ### **🔹 PDF-import (`/api/import/pdf`)** ```typescript import express from 'express'; import multer from 'multer'; import { importPDFAndGenerateRecipes } from '../services/pdfImportService'; const router = express.Router(); const upload = multer({ dest: 'uploads/' }); router.post('/import/pdf', upload.single('pdf'), async (req, res) => { try { if (!req.file) { return res.status(400).json({ error: 'Ingen PDF-fil uppladdad.' }); } const userId = req.user.id; const userPreferences = req.body.preferences || {}; const result = await importPDFAndGenerateRecipes( req.file.path, userId, userPreferences ); fs.unlinkSync(req.file.path); // Rensa upp if (!result.success) { return res.status(500).json({ error: result.error }); } res.json(result); } catch (error) { console.error('Fel vid PDF-import:', error); res.status(500).json({ error: 'Kunde inte importera PDF:en.' }); } }); export default router; ``` ### **🔹 Premium-uppgradering (`/api/user/upgrade`)** ```typescript router.post('/upgrade-premium', async (req, res) => { try { const userId = req.user.id; // Antas att betalning är verifierad (t.ex. via Stripe) const expiryDate = new Date(); expiryDate.setMonth(expiryDate.getMonth() + 1); // 1 månad premium await prisma.user.update({ where: { id: userId }, data: { is_premium: true, premium_expiry_date: expiryDate, }, }); res.json({ success: true, premiumExpiryDate: expiryDate }); } catch (error) { console.error('Fel vid uppgradering:', error); res.status(500).json({ error: 'Kunde inte uppgradera till premium.' }); } }); ``` --- --- ## **📄 7. Frontend-Integrering (Exempel: React)** ### **🔹 Visa premium-status** ```tsx import { useState, useEffect } from 'react'; function PremiumStatus({ user }) { const [isPremium, setIsPremium] = useState(user.is_premium); useEffect(() => { setIsPremium(user.is_premium); }, [user]); return (
{isPremium ? (
✅ Premium (gäller till: {new Date(user.premium_expiry_date).toLocaleDateString('sv-SE')})

Du har tillgång till AI-genererade recept!

) : (
🔒 Uppgradera till premium för AI-recept!
)}
); } ``` ### **🔹 PDF-import-formulär** ```tsx import { useState } from 'react'; import axios from 'axios'; function PDFImportForm({ user }) { const [file, setFile] = useState(null); const [isLoading, setIsLoading] = useState(false); const [result, setResult] = useState(null); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!file) return; setIsLoading(true); const formData = new FormData(); formData.append('pdf', file); formData.append('preferences', JSON.stringify({ diet: 'Inga restriktioner', cuisine: 'Nordisk', })); try { const response = await axios.post('/api/import/pdf', formData, { headers: { 'Content-Type': 'multipart/form-data' }, }); setResult(response.data); } catch (error) { console.error('Fel vid uppladdning:', error); } finally { setIsLoading(false); } }; return (

Importera veckoblad

setFile(e.target.files?.[0] || null)} required />
{result && (

Resultat

Hittade {result.products?.length || 0} produkter.

Genererade {result.recipes?.length || 0} recept.

{result.recipes?.map((recipe: any, index: number) => (

{recipe.title}

{recipe.description}

    {recipe.ingredients?.map((ing: any, i: number) => (
  • {ing.quantity} {ing.name} {ing.onOffer && '🔥'}
  • ))}
))}
)}
); } ``` --- --- ## **📌 Miljövariabler (`.env`)** ```env # Mistral API-nyckel (för premium-funktioner) MISTRAL_API_KEY=din_api_nyckel_här # Databas-URL DATABASE_URL=postgresql://user:password@localhost:5432/db_name # Session-hemlighet (för autentisering) SESSION_SECRET=din_hemliga_nyckel_här ``` --- --- ## **📌 Paket som behövs (`package.json`)** ```json { "dependencies": { "express": "^4.18.2", "multer": "^1.4.5-lts.1", "pdf-parse": "^1.1.1", "tesseract.js": "^4.0.2", "@mistralai/mistralai": "^0.0.1", "@prisma/client": "^5.0.0", "prisma": "^5.0.0", "string-similarity": "^4.0.4", "typescript": "^5.0.0" } } ``` --- --- ## **📌 Steg-för-steg Implementeringsguide** ### **1. Förbered databasen** - Kör migrationskommandot för att lägga till `is_premium` och `premium_expiry_date` i `User`-tabellen: ```bash npx prisma migrate dev --name add_premium_fields ``` ### **2. Installera beroenden** ```bash npm install express multer pdf-parse tesseract.js @mistralai/mistralai string-similarity npm install --save-dev typescript @types/node @types/express @types/multer prisma ``` ### **3. Konfigurera Prisma** - Skapa eller uppdatera `schema.prisma` med `User`-modellen (se ovan). - Kör `npx prisma generate` för att generera Prisma-klienten. ### **4. Skapa filerna** - Skapa mappen `src/services/` och lägg till filerna: - `pdfTextExtractor.ts` - `willysParser.ts` - `inventoryMatcher.ts` - `recipeGenerator.ts` - `pdfImportService.ts` - Skapa mappen `src/api/` och lägg till: - `pdfImport.ts` - `premium.ts` ### **5. Konfigurera Express-server** - Skapa en `server.ts` för att starta din Express-server: ```typescript import express from 'express'; import pdfImportRouter from './api/pdfImport'; import premiumRouter from './api/premium'; const app = express(); app.use(express.json()); // API-endpoints app.use('/api/import', pdfImportRouter); app.use('/api/user', premiumRouter); const PORT = process.env.PORT || 3000; app.listen(PORT, () => { console.log(`Server körs på port ${PORT}`); }); ``` ### **6. Testa flödet** 1. **Ladda upp en PDF** (t.ex. Willys veckoblad) via `/api/import/pdf`. 2. **Kontrollera att recept genereras** (mallar för gratis, AI för premium). 3. **Testa premium-uppgraderingen** via `/api/user/upgrade`. --- --- ## **📌 Säkerhetsöverväganden** 1. **API-nycklar**: - Lagra `MISTRAL_API_KEY` i miljövariabler (aldrig i koden). 2. **Premium-status**: - Kontrollera alltid `is_premium` innan AI-anrop. 3. **Filuppladdning**: - Validera filtypen (endast PDF). - Begränsa filstorleken (t.ex. max 10MB). 4. **Autentisering**: - Se till att användaren är inloggad innan de kan ladda upp PDF:er eller uppgradera. --- --- ## **📌 Felsökning** | Problem | Lösning | |---------|---------| | **PDF:en läses inte in** | Kontrollera att filen är en giltig PDF. Använd `Tesseract.js` som fallback. | | **Regex matchar inte** | Justera regex-mönstren baserat på PDF:ens struktur. | | **AI-generering misslyckas** | Fallback till mallar. Kontrollera API-nyckeln. | | **Premium-status uppdateras inte** | Kontrollera att `is_premium` och `premium_expiry_date` uppdateras korrekt. | | **Inga recept genereras** | Kontrollera att produkterna matchas korrekt med inventory. | --- --- ## **🚀 Nästa steg** 1. **Testa med riktiga PDF:er** (t.ex. Willys veckoblad). 2. **Justera regex-mönstren** för att fånga fler produktdetaljer. 3. **Lägg till fler receptmallar** för olika kategorier. 4. **Implementera en betalningslösning** (t.ex. Stripe) för premium-uppgraderingar. 5. **Monitorera användningen** av premium-funktioner. --- --- ## **📌 Exempel på betalningsintegrering (Stripe)** Om du vill lägga till **verklig betalning** för premium-uppgraderingar, kan du använda **Stripe**: ### **1. Installera Stripe** ```bash npm install stripe @stripe/stripe-js ``` ### **2. Skapa en Stripe-endpoint** ```typescript import Stripe from 'stripe'; const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { apiVersion: '2023-10-16', }); router.post('/create-checkout-session', async (req, res) => { const userId = req.user.id; const session = await stripe.checkout.sessions.create({ payment_method_types: ['card'], line_items: [ { price_data: { currency: 'sek', product_data: { name: 'Premium-medlemskap (1 månad)', }, unit_amount: 9900, // 99 kr }, quantity: 1, }, ], mode: 'payment', success_url: `${process.env.FRONTEND_URL}/success?session_id={CHECKOUT_SESSION_ID}`, cancel_url: `${process.env.FRONTEND_URL}/cancel`, metadata: { userId }, }); res.json({ url: session.url }); }); ``` ### **3. Hantera framgångsrik betalning** ```typescript router.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => { const sig = req.headers['stripe-signature'] as string; const event = stripe.webhooks.constructEvent( req.body, sig, process.env.STRIPE_WEBHOOK_SECRET! ); if (event.type === 'checkout.session.completed') { const session = event.data.object as Stripe.Checkout.Session; const userId = parseInt(session.metadata.userId); const expiryDate = new Date(); expiryDate.setMonth(expiryDate.getMonth() + 1); await prisma.user.update({ where: { id: userId }, data: { is_premium: true, premium_expiry_date: expiryDate, }, }); } res.json({ received: true }); }); ``` --- --- ## **📌 Sammanfattning** Denna plan ger dig en **komplett lösning** för att: 1. **Importera PDF:er** (t.ex. Willys veckoblad). 2. **Extrahera och strukturera data** (med regex och OCR). 3. **Matcha produkter med inventory** (fuzzy matching). 4. **Generera recept**: - **Gratis**: Med mallar. - **Premium**: Med AI (Mistral). 5. **Hantera premium-uppgraderingar** (med Stripe). --- --- ## **📥 Hur du använder denna plan i VS Code** 1. **Spara denna text** som `premium_recipe_feature_plan.md` i din projektmapp. 2. **Öppna filen i VS Code**. 3. **Använd VS Code-chatten** (t.ex. med **Mistral Vibe** eller **Continue**) för att: - **Fråga om förtydliganden** för specifika steg. - **Be om hjälp** med att implementera enskilda filer. - **Debugga** om något inte fungerar. 4. **Kopiera och klistra in kodsnuttarna** i dina egna filer. --- --- ## **💡 Tips för VS Code** - **Använd `Ctrl+Shift+P`** för att öppna kommandopaletten och köra `Prisma: Generate Client`. - **Installera ESLint** för att fånga syntaxfel tidigt. - **Använd Git** för att spåra dina ändringar: ```bash git init git add . git commit -m "Lade till premium-receptfunktion" ``` ``` --- **Spara denna text som `premium_recipe_feature_plan.md` och ladda upp den i VS Code-chatten när du behöver hjälp med implementeringen!** 🚀