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

344 lines
17 KiB
TypeScript
Raw Normal View History

import React, { useState } from 'react';
import { ExcelRow, COLUMNS } from '../types';
import { X, Sparkles, Save, Loader2, Languages, Package, CheckCircle2 } from 'lucide-react';
import { generateGemini } from '../services/gemini';
import { cn } from '../lib/utils';
import { ConfirmModal } from './ConfirmModal';
interface EditPanelProps {
row: ExcelRow;
rowIndex: number;
onSave: (rowIndex: number, updatedRow: ExcelRow) => void;
onClose: () => void;
onCaptureState: (message: string) => void;
}
export function EditPanel({ row, rowIndex, onSave, onClose, onCaptureState }: 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] || '',
detailsDe: row[COLUMNS.DETAILS_DE] || '',
detailsEn: row[COLUMNS.DETAILS_EN] || '',
innerW: row[COLUMNS.INNER_W] || '',
innerL: row[COLUMNS.INNER_L] || '',
innerH: row[COLUMNS.INNER_H] || '',
outerW: row[COLUMNS.OUTER_W] || '',
outerL: row[COLUMNS.OUTER_L] || '',
outerH: row[COLUMNS.OUTER_H] || '',
unitsOuter: row[COLUMNS.UNITS_OUTER] || '',
moq: row[COLUMNS.MOQ] || '',
});
const [loadingField, setLoadingField] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [isConfirmOpen, setIsConfirmOpen] = useState(false);
const [pendingGeminiField, setPendingGeminiField] = useState<keyof typeof formData | null>(null);
const [generatedFields, setGeneratedFields] = useState<Set<string>>(new Set());
const isModified = (field: keyof typeof formData) => {
const colMap: Record<string, number> = {
longDe: COLUMNS.LONG_DE,
longEn: COLUMNS.LONG_EN,
shortDe: COLUMNS.SHORT_DE,
shortEn: COLUMNS.SHORT_EN,
innerW: COLUMNS.INNER_W,
innerL: COLUMNS.INNER_L,
innerH: COLUMNS.INNER_H,
outerW: COLUMNS.OUTER_W,
outerL: COLUMNS.OUTER_L,
outerH: COLUMNS.OUTER_H,
unitsOuter: COLUMNS.UNITS_OUTER,
moq: COLUMNS.MOQ,
detailsDe: COLUMNS.DETAILS_DE,
detailsEn: COLUMNS.DETAILS_EN,
};
const colIndex = (colMap as Record<string, number>)[field as string];
if (colIndex === undefined) return false;
return formData[field] !== (row[colIndex] || '');
};
const handleGenerate = async (field: keyof typeof formData) => {
setPendingGeminiField(field);
};
const executeGenerate = async () => {
if (!pendingGeminiField) return;
const field = pendingGeminiField;
setPendingGeminiField(null);
setLoadingField(field);
setError(null);
try {
let prompt = '';
const baseContext = `Article Name: ${row[COLUMNS.ARTICLE_NAME]}\nArticle Details (EN): ${formData.detailsEn || 'N/A'}\nArticle Details (DE): ${formData.detailsDe || 'N/A'}`;
const systemPrompt = "You are a professional copywriter and expert translator for CRAZE GmbH, a German toy company. You excel at translating product descriptions between German and English, maintaining the commercial and professional tone while ensuring all technical toy details are accurate. Use clear, engaging language.";
if (field === 'longDe') {
if (formData.longEn) {
prompt = `Translate the EVERYTHING below from English into professional German.
CRITICAL INSTRUCTIONS:
1. Do NOT summarize.
2. Maintain the EXACT same length and detailed information.
3. Keep all technical specs intact.
4. Translate every single paragraph into natural, commercial German for toy buyers.
English Text to Translate:
${formData.longEn}`;
} else {
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 EVERYTHING below from German into professional English.
CRITICAL INSTRUCTIONS:
1. Do NOT summarize.
2. Maintain the EXACT same length and detailed information.
3. Keep all technical specs intact.
4. Translate every single paragraph into natural, commercial English for toy buyers.
German Text to Translate:
${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.\n\n${baseContext}`;
}
} else if (field === 'shortDe') {
if (formData.shortEn) {
prompt = `Translate exactly this short English product description into professional German for the toy market:\n\n${formData.shortEn}`;
} else if (formData.longDe) {
const targetChars = Math.round(formData.longDe.length * 0.3);
prompt = `Create a concise summary of the following German product description. The summary must be approximately ${targetChars} characters long (about 30% of the original). Preserve the most important commercial highlights and features in natural, professional German for B2B toy buyers. Do not use bullet points — write flowing prose.\n\nOriginal description:\n${formData.longDe}`;
} else {
prompt = `Based on the following product details, generate a short commercial description in German (2-4 sentences max).\n\n${baseContext}`;
}
} else if (field === 'shortEn') {
if (formData.shortDe) {
prompt = `Translate exactly this short German product description into professional English for the toy market:\n\n${formData.shortDe}`;
} else if (formData.longEn) {
const targetChars = Math.round(formData.longEn.length * 0.3);
prompt = `Create a concise summary of the following English product description. The summary must be approximately ${targetChars} characters long (about 30% of the original). Preserve the most important commercial highlights and features in natural, professional English for B2B toy buyers. Do not use bullet points — write flowing prose.\n\nOriginal description:\n${formData.longEn}`;
} else {
prompt = `Based on the following product details, generate a short commercial description in English (2-4 sentences max).\n\n${baseContext}`;
}
}
const generatedText = await generateGemini(prompt, systemPrompt);
setFormData(prev => ({ ...prev, [field]: generatedText.trim() }));
setGeneratedFields(prev => new Set(prev).add(field));
} catch (err: any) {
setError(err.message || 'An error occurred during generation.');
} finally {
setLoadingField(null);
}
};
const handleSave = () => {
setIsConfirmOpen(false);
const hasModifications = Object.keys(formData).some(k => isModified(k as keyof typeof formData));
if (hasModifications) {
onCaptureState(`Updated product ${row[COLUMNS.ARTICLE_NO]}`);
}
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;
newRow[COLUMNS.INNER_W] = formData.innerW;
newRow[COLUMNS.INNER_L] = formData.innerL;
newRow[COLUMNS.INNER_H] = formData.innerH;
newRow[COLUMNS.OUTER_W] = formData.outerW;
newRow[COLUMNS.OUTER_L] = formData.outerL;
newRow[COLUMNS.OUTER_H] = formData.outerH;
newRow[COLUMNS.UNITS_OUTER] = formData.unitsOuter;
newRow[COLUMNS.MOQ] = formData.moq;
newRow[COLUMNS.DETAILS_DE] = formData.detailsDe;
newRow[COLUMNS.DETAILS_EN] = formData.detailsEn;
onSave(rowIndex, newRow);
};
const DimensionInput = ({ label, field, placeholder }: { label: string, field: keyof typeof formData, placeholder?: string }) => (
<div className="flex flex-col gap-1.5">
<label className="text-[10px] font-medium text-slate-500 uppercase tracking-wider">{label}</label>
<input
type="text"
value={formData[field]}
onChange={e => setFormData(prev => ({ ...prev, [field]: e.target.value }))}
placeholder={placeholder}
className={cn(
"w-full bg-slate-900 border rounded p-2 text-sm text-white focus:outline-none focus:ring-1 transition-colors",
isModified(field) ? "border-blue-500 focus:ring-blue-500" : "border-slate-700 focus:border-slate-500"
)}
/>
</div>
);
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>
<div className="flex items-center gap-2">
{((field === 'longDe' && formData.longEn) ||
(field === 'longEn' && formData.longDe) ||
(field === 'shortDe' && (formData.longDe || formData.shortEn)) ||
(field === 'shortEn' && (formData.shortDe || formData.longEn))) && (
<div className="flex items-center gap-1 text-[10px] text-slate-500 italic">
<Languages className="w-3 h-3" />
Can translate from existing
</div>
)}
<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" />}
{((field === 'longDe' && formData.longEn) || (field === 'longEn' && formData.longDe) || (field === 'shortDe' && formData.shortEn) || (field === 'shortEn' && formData.shortDe)) ? 'Translate with Gemini' : 'Generate with Gemini'}
</button>
</div>
{generatedFields.has(field) && (
<div className="flex items-center gap-1.5 text-[10px] text-emerald-400 bg-emerald-400/10 px-2 py-0.5 rounded w-fit animate-in fade-in slide-in-from-top-1 duration-300">
<CheckCircle2 className="w-3 h-3" />
AI Generated - You can still edit manually
</div>
)}
</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>
<textarea
value={formData.detailsDe}
onChange={e => setFormData(prev => ({ ...prev, detailsDe: e.target.value }))}
className={cn(
"w-full h-20 bg-slate-900 border rounded p-2 text-sm text-white focus:outline-none focus:ring-1 transition-colors resize-none",
isModified('detailsDe') ? "border-blue-500 focus:ring-blue-500" : "border-slate-700/50"
)}
placeholder="Enter details in German..."
/>
</div>
<div className="col-span-2">
<span className="block text-xs text-slate-500 mb-1">Details (EN)</span>
<textarea
value={formData.detailsEn}
onChange={e => setFormData(prev => ({ ...prev, detailsEn: e.target.value }))}
className={cn(
"w-full h-20 bg-slate-900 border rounded p-2 text-sm text-white focus:outline-none focus:ring-1 transition-colors resize-none",
isModified('detailsEn') ? "border-blue-500 focus:ring-blue-500" : "border-slate-700/50"
)}
placeholder="Enter details in English..."
/>
</div>
</div>
<div className="space-y-4 bg-slate-900/30 p-4 rounded-lg border border-slate-700/50">
<h3 className="text-xs font-semibold text-slate-400 uppercase tracking-widest flex items-center gap-2">
<Package className="w-3 h-3" /> Dimensions & Packaging
</h3>
<div className="grid grid-cols-3 gap-3">
<div className="col-span-3 text-[10px] text-slate-500 font-medium">INNER BOX (L × W × H) cm</div>
<DimensionInput label="Length" field="innerL" placeholder="L" />
<DimensionInput label="Width" field="innerW" placeholder="W" />
<DimensionInput label="Height" field="innerH" placeholder="H" />
</div>
<div className="grid grid-cols-3 gap-3">
<div className="col-span-3 text-[10px] text-slate-500 font-medium">OUTER BOX (L × W × H) cm</div>
<DimensionInput label="Length" field="outerL" placeholder="L" />
<DimensionInput label="Width" field="outerW" placeholder="W" />
<DimensionInput label="Height" field="outerH" placeholder="H" />
</div>
<div className="grid grid-cols-2 gap-3">
<DimensionInput label="Units per Outer" field="unitsOuter" />
<DimensionInput label="MOQ" field="moq" />
</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={() => setIsConfirmOpen(true)}
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>
<ConfirmModal
isOpen={isConfirmOpen}
onConfirm={handleSave}
onCancel={() => setIsConfirmOpen(false)}
title="Confirm Changes"
message={`Are you sure you want to save the modifications for product ${row[COLUMNS.ARTICLE_NO]}?`}
type="info"
confirmText="Save changes"
/>
<ConfirmModal
isOpen={!!pendingGeminiField}
onConfirm={executeGenerate}
onCancel={() => setPendingGeminiField(null)}
title="Confirm AI Action"
message={`Are you sure you want to use Gemini to ${((pendingGeminiField === 'longEn' && formData.longDe) || (pendingGeminiField === 'shortEn' && formData.shortDe)) ? 'translate' : 'generate'} the ${pendingGeminiField} field?`}
type="info"
confirmText="Start Generation"
/>
</div>
</>
);
}