mirror of
https://github.com/christianvidalwolf-prog/Craze-Data-check.git
synced 2026-08-03 15:05:23 +02:00
175 lines
8.4 KiB
TypeScript
175 lines
8.4 KiB
TypeScript
import React, { useState } from 'react';
|
|||
|
|
import { ExcelRow, COLUMNS } from '../types';
|
||
|
|
import { X, Sparkles, Save, Loader2 } from 'lucide-react';
|
||
|
|
import { generateDescription } from '../services/anthropic';
|
||
|
|
import { cn } from '../lib/utils';
|
||
|
|
|
||
|
|
interface EditPanelProps {
|
||
|
|
row: ExcelRow;
|
||
|
|
rowIndex: number;
|
||
|
|
onSave: (rowIndex: number, updatedRow: ExcelRow) => void;
|
||
|
|
onClose: () => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function EditPanel({ row, rowIndex, onSave, onClose }: EditPanelProps) {
|
||
|
|
const [formData, setFormData] = useState({
|
||
|
|
longDe: row[COLUMNS.LONG_DE] || '',
|
||
|
|
longEn: row[COLUMNS.LONG_EN] || '',
|
||
|
|
shortDe: row[COLUMNS.SHORT_DE] || '',
|
||
|
|
shortEn: row[COLUMNS.SHORT_EN] || '',
|
||
|
|
});
|
||
|
|
|
||
|
|
const [loadingField, setLoadingField] = useState<string | null>(null);
|
||
|
|
const [error, setError] = useState<string | null>(null);
|
||
|
|
|
||
|
|
const isModified = (field: keyof typeof formData) => {
|
||
|
|
const colMap = {
|
||
|
|
longDe: COLUMNS.LONG_DE,
|
||
|
|
longEn: COLUMNS.LONG_EN,
|
||
|
|
shortDe: COLUMNS.SHORT_DE,
|
||
|
|
shortEn: COLUMNS.SHORT_EN,
|
||
|
|
};
|
||
|
|
return formData[field] !== (row[colMap[field]] || '');
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleGenerate = async (field: keyof typeof formData) => {
|
||
|
|
setLoadingField(field);
|
||
|
|
setError(null);
|
||
|
|
|
||
|
|
try {
|
||
|
|
let prompt = '';
|
||
|
|
const baseContext = `Article Name: ${row[COLUMNS.ARTICLE_NAME]}\nArticle Details (EN): ${row[COLUMNS.DETAILS_EN] || 'N/A'}\nArticle Details (DE): ${row[COLUMNS.DETAILS_DE] || 'N/A'}`;
|
||
|
|
|
||
|
|
const systemPrompt = "You are a professional copywriter for CRAZE GmbH, a German toy company. Write commercial product descriptions for B2B buyers (retailers, distributors). Use clear, engaging, professional language. Never invent features not mentioned in the source material.";
|
||
|
|
|
||
|
|
if (field === 'longDe') {
|
||
|
|
prompt = `Based on the following product details, generate a long commercial description in German. It should be fluent, oriented towards B2B toy buyers, 3-5 paragraphs long. Include features, benefits, and material if available.\n\n${baseContext}`;
|
||
|
|
} else if (field === 'longEn') {
|
||
|
|
if (formData.longDe) {
|
||
|
|
prompt = `Translate the following German product description into perfect English. Keep the same tone and length.\n\nGerman Description:\n${formData.longDe}`;
|
||
|
|
} else {
|
||
|
|
prompt = `Based on the following product details, generate a long commercial description in English. It should be fluent, oriented towards B2B toy buyers, 3-5 paragraphs long. Include features, benefits, and material if available.\n\n${baseContext}`;
|
||
|
|
}
|
||
|
|
} else if (field === 'shortDe') {
|
||
|
|
if (formData.longDe) {
|
||
|
|
prompt = `Create a short version (2-4 sentences max) of the following German product description. Include only the most relevant info. Use a direct and commercial tone.\n\nGerman Description:\n${formData.longDe}`;
|
||
|
|
} else {
|
||
|
|
prompt = `Based on the following product details, generate a short commercial description in German (2-4 sentences max). Include only the most relevant info. Use a direct and commercial tone.\n\n${baseContext}`;
|
||
|
|
}
|
||
|
|
} else if (field === 'shortEn') {
|
||
|
|
if (formData.shortDe) {
|
||
|
|
prompt = `Translate the following short German product description into perfect English.\n\nGerman Description:\n${formData.shortDe}`;
|
||
|
|
} else {
|
||
|
|
prompt = `Based on the following product details, generate a short commercial description in English (2-4 sentences max). Include only the most relevant info. Use a direct and commercial tone.\n\n${baseContext}`;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const generatedText = await generateDescription(prompt, systemPrompt);
|
||
|
|
setFormData(prev => ({ ...prev, [field]: generatedText.trim() }));
|
||
|
|
} catch (err: any) {
|
||
|
|
setError(err.message || 'An error occurred during generation.');
|
||
|
|
} finally {
|
||
|
|
setLoadingField(null);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleSave = () => {
|
||
|
|
const newRow = [...row];
|
||
|
|
newRow[COLUMNS.LONG_DE] = formData.longDe;
|
||
|
|
newRow[COLUMNS.LONG_EN] = formData.longEn;
|
||
|
|
newRow[COLUMNS.SHORT_DE] = formData.shortDe;
|
||
|
|
newRow[COLUMNS.SHORT_EN] = formData.shortEn;
|
||
|
|
onSave(rowIndex, newRow);
|
||
|
|
};
|
||
|
|
|
||
|
|
const FieldEditor = ({ title, field }: { title: string, field: keyof typeof formData }) => (
|
||
|
|
<div className="flex flex-col gap-2">
|
||
|
|
<div className="flex items-center justify-between">
|
||
|
|
<label className="text-sm font-medium text-slate-300">{title}</label>
|
||
|
|
<button
|
||
|
|
onClick={() => handleGenerate(field)}
|
||
|
|
disabled={loadingField !== null}
|
||
|
|
className="flex items-center gap-1.5 text-xs font-medium bg-blue-600/20 text-blue-400 hover:bg-blue-600 hover:text-white px-2 py-1 rounded transition-colors disabled:opacity-50"
|
||
|
|
>
|
||
|
|
{loadingField === field ? <Loader2 className="w-3 h-3 animate-spin" /> : <Sparkles className="w-3 h-3" />}
|
||
|
|
Generate with AI
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
<textarea
|
||
|
|
value={formData[field]}
|
||
|
|
onChange={e => setFormData(prev => ({ ...prev, [field]: e.target.value }))}
|
||
|
|
className={cn(
|
||
|
|
"w-full h-32 bg-slate-900 border rounded-md p-3 text-sm text-white focus:outline-none focus:ring-1 transition-colors resize-none",
|
||
|
|
isModified(field) ? "border-blue-500 focus:ring-blue-500" : "border-slate-700 focus:border-slate-500 focus:ring-slate-500"
|
||
|
|
)}
|
||
|
|
placeholder={`Enter ${title}...`}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<>
|
||
|
|
<div className="fixed inset-0 bg-slate-950/50 backdrop-blur-sm z-40" onClick={onClose} />
|
||
|
|
<div className="fixed right-0 top-0 bottom-0 w-[600px] bg-slate-800 border-l border-slate-700 shadow-2xl z-50 flex flex-col animate-in slide-in-from-right duration-200">
|
||
|
|
<div className="flex items-center justify-between p-6 border-b border-slate-700 bg-slate-800/50">
|
||
|
|
<div>
|
||
|
|
<h2 className="text-xl font-bold text-white">Edit Product</h2>
|
||
|
|
<p className="text-sm text-slate-400 mt-1">{row[COLUMNS.ARTICLE_NO]} - {row[COLUMNS.ARTICLE_NAME]}</p>
|
||
|
|
</div>
|
||
|
|
<button onClick={onClose} className="p-2 text-slate-400 hover:text-white hover:bg-slate-700 rounded-full transition-colors">
|
||
|
|
<X className="w-5 h-5" />
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="flex-1 overflow-auto p-6 space-y-6">
|
||
|
|
{error && (
|
||
|
|
<div className="bg-red-500/10 border border-red-500/50 text-red-400 p-4 rounded-md text-sm">
|
||
|
|
{error}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
<div className="grid grid-cols-2 gap-4 bg-slate-900/50 p-4 rounded-lg border border-slate-700/50">
|
||
|
|
<div>
|
||
|
|
<span className="block text-xs text-slate-500 mb-1">Line</span>
|
||
|
|
<span className="text-sm text-slate-300 font-medium">{row[COLUMNS.LINE] || '-'}</span>
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<span className="block text-xs text-slate-500 mb-1">License</span>
|
||
|
|
<span className="text-sm text-slate-300 font-medium">{row[COLUMNS.LICENSE] || '-'}</span>
|
||
|
|
</div>
|
||
|
|
<div className="col-span-2">
|
||
|
|
<span className="block text-xs text-slate-500 mb-1">Details (DE)</span>
|
||
|
|
<p className="text-sm text-slate-300 line-clamp-2" title={row[COLUMNS.DETAILS_DE]}>{row[COLUMNS.DETAILS_DE] || '-'}</p>
|
||
|
|
</div>
|
||
|
|
<div className="col-span-2">
|
||
|
|
<span className="block text-xs text-slate-500 mb-1">Details (EN)</span>
|
||
|
|
<p className="text-sm text-slate-300 line-clamp-2" title={row[COLUMNS.DETAILS_EN]}>{row[COLUMNS.DETAILS_EN] || '-'}</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<FieldEditor title="Long Description (DE)" field="longDe" />
|
||
|
|
<FieldEditor title="Long Description (EN)" field="longEn" />
|
||
|
|
<FieldEditor title="Short Description (DE)" field="shortDe" />
|
||
|
|
<FieldEditor title="Short Description (EN)" field="shortEn" />
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="p-6 border-t border-slate-700 bg-slate-800/50 flex justify-end gap-3">
|
||
|
|
<button
|
||
|
|
onClick={onClose}
|
||
|
|
className="px-4 py-2 text-sm font-medium text-slate-300 hover:text-white hover:bg-slate-700 rounded-md transition-colors"
|
||
|
|
>
|
||
|
|
Cancel
|
||
|
|
</button>
|
||
|
|
<button
|
||
|
|
onClick={handleSave}
|
||
|
|
className="flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-md shadow-lg shadow-blue-900/20 transition-colors"
|
||
|
|
>
|
||
|
|
<Save className="w-4 h-4" />
|
||
|
|
Save to Memory
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</>
|
||
|
|
);
|
||
|
|
}
|