Files
Craze-Data-check/src/components/EditPanel.tsx
T

175 lines
8.4 KiB
TypeScript
Raw Normal View History

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>
</>
);
}