Files
recipe-app/.kilo/plans/flyerimporter.md
T
Nils-Johan Gynther 9e513c2f5e
Test Suite / backend-pr-quick (push) Has been skipped
Test Suite / quick-import-pr-quick (push) Has been skipped
Test Suite / backend-full (push) Successful in 2m57s
Test Suite / flutter-quality (push) Failing after 1m16s
chore(docs): consolidate legacy documentation into new structure
- Removed outdated documentation files (MVP_CHECKLISTA.md, NEXT_STEPS.md, README.md, TEKNISK_BESKRIVNING.md, filanalys.md, flyerimporter.md, kilo.json, plan-dokumentation.md)
- Added new centralized documentation structure under docs/ directory
- Added .kilo/ directory for Kilo AI agent configuration and plans

BREAKING CHANGE: Legacy documentation files removed and replaced with new centralized structure
2026-05-25 08:14:35 +02:00

9.9 KiB

flyerimporter.md 📌 Steg 1: Skapa en funktion för att extrahera text från PDF:en Använd pdf-parse som primär metod och Tesseract.js som fallback för OCR. Kod: extractFlyerText.ts typescript Copy

import * as fs from 'fs'; import * as pdf from 'pdf-parse'; import Tesseract from 'tesseract.js';

/**

  • Extraherar text från en PDF-fil (flyer), med fallback till OCR.
  • @param pdfPath Sökväg till PDF-filen.
  • @returns Extraherad text. */ export async function extractFlyerText(pdfPath: string): Promise { try { // Försök med pdf-parse först 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...'); }

// Fallback till Tesseract.js för OCR 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.'); } }

📌 Steg 2: Skapa en funktion för att skicka texten till Mistral Tiny Använd Mistral Tiny för att extrahera och strukturera all produktinformation från flyern. Kod: importFlyerWithAI.ts typescript Copy

import { MistralClient } from '@mistralai/mistralai';

const mistral = new MistralClient({ apiKey: process.env.MISTRAL_API_KEY, });

/**

  • Skickar flyer-texten till Mistral Tiny för att extrahera strukturerad data.

  • @param text Texten från flyern.

  • @returns Strukturerad data (JSON-array). */ export async function importFlyerWithAI(text: string): Promise<any[]> { const prompt = ` Du är en expert på att tolka svenska matvaruflyers (t.ex. från Willys). Extrahera ALL produktinformation från följande text och returnera den som en JSON-array.

    För varje produkt, inkludera:

    • name: Produktnamn (fullständigt namn)
    • weight: Vikt (om tillgänglig, t.ex. "150g", "Ca 1kg")
    • origin: Ursprung/land/märke (om tillgänglig, t.ex. "FALKENBERG", "NYBERGS DELI • Sverige")
    • price: Pris (som ett nummer, t.ex. 39.90)
    • comparisonPrice: Jämförpris (som ett nummer, t.ex. 266.00)
    • unit: Enhet (kg, st, förp, l, etc.)
    • offer: Erbjudande (t.ex. ["Max 3 köp/hushåll", "Lägsta 30-dgrspris 125:00 kr"])
    • category: Kategori (t.ex. "Fisk", "Kött", "Mejeri", "Grönsaker", "Frukt", "Dryck")
    • validFrom: Giltig från (datum i formatet YYYY-MM-DD, om tillgängligt)
    • validTo: Giltig till (datum i formatet YYYY-MM-DD, om tillgängligt)

    Texten att tolka: ${text}

    Returnera ENDAST en JSON-array. Inga andra kommentarer. Exempel på utdata: [ { "name": "KALLRÖKT LAX, GRAVAD LAX", "weight": "150g", "origin": "FALKENBERG", "price": 39.90, "comparisonPrice": 266.00, "unit": "kg", "offer": ["Max 3 köp/hushåll"], "category": "Fisk", "validFrom": "2026-05-18", "validTo": "2026-05-24" } ] `;

try { const response = await mistral.chat({ model: 'mistral-tiny', // Använder den enklaste modellen messages: [{ role: 'user', content: prompt }], temperature: 0.1, // Låg temperatur för mer deterministiska svar });

// Rensa upp JSON-strängen
const jsonString = response.choices[0].message.content
  .replace(/```json|```/g, '')
  .trim();

// Parsa JSON:en
return JSON.parse(jsonString);

} catch (error) { console.error('Fel vid AI-import:', error); throw new Error('Kunde inte importera flyern med AI.'); } }

📌 Steg 3: Fullständigt importflöde Kombinera text-extrahering och AI-import i ett fullständigt flöde. Kod: flyerImportService.ts typescript Copy

import { extractFlyerText } from './extractFlyerText'; import { importFlyerWithAI } from './importFlyerWithAI';

/**

  • Importerar en flyer (PDF) och returnerar strukturerad data.

  • @param pdfPath Sökväg till PDF-filen.

  • @returns Strukturerad data från flyern. */ export async function importFlyer(pdfPath: string) { try { // 1. Extrahera text från PDF:en console.log('Extraherar text från flyern...'); const text = await extractFlyerText(pdfPath);

    // 2. Skicka texten till Mistral Tiny för att extrahera data console.log('Skickar text till Mistral Tiny för extrahering...'); const products = await importFlyerWithAI(text);

    return { success: true, products, text, }; } catch (error) { console.error('Fel vid import:', error); return { success: false, error: error instanceof Error ? error.message : 'Okänt fel', }; } }

📌 Steg 4: API-Endpoint för flyer-import Skapa en Express-endpoint för att hantera uppladdning och import av flyers. Kod: flyerImportRouter.ts typescript Copy

import express from 'express'; import multer from 'multer'; import { importFlyer } from '../services/flyerImportService'; import * as fs from 'fs';

const router = express.Router(); const upload = multer({ dest: 'uploads/' });

// Endpoint för att ladda upp och importera en flyer router.post('/import/flyer', upload.single('flyer'), async (req, res) => { try { if (!req.file) { return res.status(400).json({ error: 'Ingen flyer uppladdad.' }); }

const result = await importFlyer(req.file.path);

// Rensa upp uppladdad fil
fs.unlinkSync(req.file.path);

if (!result.success) {
  return res.status(500).json({ error: result.error });
}

res.json(result);

} catch (error) { console.error('Fel vid flyer-import:', error); res.status(500).json({ error: 'Kunde inte importera flyern.' }); } });

export default router;

📌 Steg 5: Exempel på utdata När du skickar texten från Willys flyer till Mistral Tiny, kommer du att få tillbaka en JSON-array med alla produkter, t.ex.: json Copy

[ { "name": "KALLRÖKT LAX, GRAVAD LAX", "weight": "150g", "origin": "FALKENBERG", "price": 39.9, "comparisonPrice": 266.0, "unit": "kg", "offer": ["Max 3 köp/hushåll"], "category": "Fisk", "validFrom": "2026-05-18", "validTo": "2026-05-24" }, { "name": "FLÄSKYTTERFILÉ", "weight": "Ca 1kg", "origin": "NYBERGS DELI • Sverige", "price": 64.9, "comparisonPrice": 64.9, "unit": "kg", "offer": ["Max 3 förp/hushåll"], "category": "Kött", "validFrom": "2026-05-18", "validTo": "2026-05-24" } ]

📌 Steg 6: Använda den extraherade datan När du har den strukturerade datan, kan du:

Lagra den i din databas (t.ex. för att jämföra med inventory). Visa den för användaren (t.ex. i en tabell). Använda den för att generera recept (med eller utan AI). Exempel: Lagra i databasen typescript Copy

// Antas att du har en Prisma-modell för flyer-produkter await prisma.flyerProduct.createMany({ data: products.map((product) => ({ name: product.name, weight: product.weight, origin: product.origin, price: product.price, comparisonPrice: product.comparisonPrice, unit: product.unit, offer: JSON.stringify(product.offer), category: product.category, validFrom: product.validFrom ? new Date(product.validFrom) : null, validTo: product.validTo ? new Date(product.validTo) : null, })), });

📌 Steg 7: Frontend-Integrering (Exempel: React) Här är hur du kan integrera flyer-importen i din frontend: Kod: FlyerImportForm.tsx tsx Copy

import { useState } from 'react'; import axios from 'axios';

function FlyerImportForm() { const [file, setFile] = useState<File | null>(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('flyer', file);

try {
  const response = await axios.post('/api/import/flyer', formData, {
    headers: { 'Content-Type': 'multipart/form-data' },
  });
  setResult(response.data);
} catch (error) {
  console.error('Fel vid uppladdning:', error);
} finally {
  setIsLoading(false);
}

};

return (

Importera flyer

<input type="file" accept=".pdf" onChange={(e) => setFile(e.target.files?.[0] || null)} required /> {isLoading ? 'Importerar...' : 'Importera flyer'}

  {result?.success && (
    <div>
      <h3>Importerade produkter ({result.products.length})</h3>
      <table>
        <thead>
          <tr>
            <th>Namn</th>
            <th>Pris</th>
            <th>Jämförpris</th>
            <th>Kategori</th>
            <th>Erbjudande</th>
          </tr>
        </thead>
        <tbody>
          {result.products.map((product: any, index: number) => (
            <tr key={index}>
              <td>{product.name}</td>
              <td>{product.price} {product.unit}</td>
              <td>{product.comparisonPrice} {product.unit}</td>
              <td>{product.category}</td>
              <td>{product.offer.join(', ')}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  )}
</div>

); }

export default FlyerImportForm;

📌 Miljövariabler (.env) env Copy

Mistral API-nyckel

MISTRAL_API_KEY=din_api_nyckel_här