9e513c2f5e
- 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
359 lines
9.9 KiB
Markdown
359 lines
9.9 KiB
Markdown
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<string> {
|
|
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<any>(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 (
|
|
<div>
|
|
<h2>Importera flyer</h2>
|
|
<form onSubmit={handleSubmit}>
|
|
<input
|
|
type="file"
|
|
accept=".pdf"
|
|
onChange={(e) => setFile(e.target.files?.[0] || null)}
|
|
required
|
|
/>
|
|
<button type="submit" disabled={isLoading}>
|
|
{isLoading ? 'Importerar...' : 'Importera flyer'}
|
|
</button>
|
|
</form>
|
|
|
|
{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 |