From dc74e751264b8c7d3acf2000aa24ae2b3b19c3da Mon Sep 17 00:00:00 2001 From: Floke Date: Mon, 22 Dec 2025 20:01:03 +0000 Subject: [PATCH] Dateien nach "b2b-marketing-assistant/components" hochladen --- b2b-marketing-assistant/components/Icons.tsx | 85 +++++ .../components/InputForm.tsx | 135 ++++++++ .../components/StepDisplay.tsx | 299 ++++++++++++++++++ 3 files changed, 519 insertions(+) create mode 100644 b2b-marketing-assistant/components/Icons.tsx create mode 100644 b2b-marketing-assistant/components/InputForm.tsx create mode 100644 b2b-marketing-assistant/components/StepDisplay.tsx diff --git a/b2b-marketing-assistant/components/Icons.tsx b/b2b-marketing-assistant/components/Icons.tsx new file mode 100644 index 00000000..70d9014c --- /dev/null +++ b/b2b-marketing-assistant/components/Icons.tsx @@ -0,0 +1,85 @@ + +import React from 'react'; + +export const LoadingSpinner: React.FC<{ className?: string }> = ({ className = '' }) => ( + + + + +); + +export const SparklesIcon: React.FC<{ className?: string }> = ({ className = '' }) => ( + + + +); + +export const CopyIcon: React.FC<{ className?: string }> = ({ className = '' }) => ( + + + +); + +export const BotIcon: React.FC<{ className?: string }> = ({ className = '' }) => ( + + + + +); + +export const DownloadIcon: React.FC<{ className?: string }> = ({ className = '' }) => ( + + + +); + +export const PrintIcon: React.FC<{ className?: string }> = ({ className = '' }) => ( + + + +); + +export const MarkdownIcon: React.FC<{ className?: string }> = ({ className = '' }) => ( + + + +); + +export const ChevronDownIcon: React.FC<{ className?: string }> = ({ className = '' }) => ( + + + +); + +export const ClipboardTableIcon: React.FC<{ className?: string }> = ({ className = '' }) => ( + + + + + +); + +export const SearchIcon: React.FC<{ className?: string }> = ({ className = '' }) => ( + + + +); + +export const TrashIcon: React.FC<{ className?: string }> = ({ className = '' }) => ( + + + +); + +export const CheckIcon: React.FC<{ className?: string }> = ({ className = '' }) => ( + + + +); + +export const XMarkIcon: React.FC<{ className?: string }> = ({ className = '' }) => ( + + + + +); diff --git a/b2b-marketing-assistant/components/InputForm.tsx b/b2b-marketing-assistant/components/InputForm.tsx new file mode 100644 index 00000000..52316e7a --- /dev/null +++ b/b2b-marketing-assistant/components/InputForm.tsx @@ -0,0 +1,135 @@ + +import React from 'react'; +import type { InputData } from '../types'; +import { LANGUAGE_OPTIONS, CHANNEL_OPTIONS, translations } from '../constants'; +import { LoadingSpinner, SparklesIcon } from './Icons'; + +interface InputFormProps { + inputData: InputData; + setInputData: React.Dispatch>; + onGenerate: () => void; + isLoading: boolean; + t: typeof translations.de; +} + +export const InputForm: React.FC = ({ inputData, setInputData, onGenerate, isLoading, t }) => { + + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setInputData(prev => ({ ...prev, [name]: value })); + }; + + const handleChannelChange = (channel: string) => { + setInputData(prev => { + const newChannels = prev.channels.includes(channel) + ? prev.channels.filter(c => c !== channel) + : [...prev.channels, channel]; + return { ...prev, channels: newChannels }; + }); + }; + + return ( +
{ e.preventDefault(); onGenerate(); }} className="space-y-6"> +
+ + +
+ +
+
+ + +
+
+ + +
+
+
+ + +
+
+ +
+ {CHANNEL_OPTIONS.map(channel => ( + + ))} +
+
+ +
+ +
+
+ ); +}; diff --git a/b2b-marketing-assistant/components/StepDisplay.tsx b/b2b-marketing-assistant/components/StepDisplay.tsx new file mode 100644 index 00000000..e79aeb4e --- /dev/null +++ b/b2b-marketing-assistant/components/StepDisplay.tsx @@ -0,0 +1,299 @@ + +import React, { useState, useMemo, useRef, useEffect } from 'react'; +import { CopyIcon, ClipboardTableIcon, SearchIcon, TrashIcon, LoadingSpinner, CheckIcon, XMarkIcon } from './Icons'; +import { convertArrayToTsv } from '../services/export'; +import { translations } from '../constants'; + +interface StepDisplayProps { + title: string; + summary?: string[]; + headers: string[]; + rows: string[][]; + onDataChange: (newRows: string[][]) => void; + canAddRows?: boolean; + canDeleteRows?: boolean; + onEnrichRow?: (productName: string, productUrl?: string) => Promise; + isEnriching?: boolean; + t: typeof translations.de; +} + +export const StepDisplay: React.FC = ({ title, summary, headers, rows, onDataChange, canAddRows = false, canDeleteRows = false, onEnrichRow, isEnriching = false, t }) => { + const [copySuccess, setCopySuccess] = useState(''); + const [filterQuery, setFilterQuery] = useState(''); + const [isAddingRow, setIsAddingRow] = useState(false); + const [newRowValue, setNewRowValue] = useState(''); + const [newRowUrl, setNewRowUrl] = useState(''); + const inputRef = useRef(null); + + const filteredRows = useMemo(() => { + if (!filterQuery) { + return rows; + } + return rows.filter(row => + row.some(cell => + cell.toLowerCase().includes(filterQuery.toLowerCase()) + ) + ); + }, [rows, filterQuery]); + + useEffect(() => { + if (isAddingRow && inputRef.current) { + inputRef.current.focus(); + } + }, [isAddingRow]); + + const handleCellChange = (originalRowIndex: number, colIndex: number, value: string) => { + const newRows = rows.map((row, rIdx) => + rIdx === originalRowIndex + ? row.map((cell, cIdx) => cIdx === colIndex ? value : cell) + : row + ); + onDataChange(newRows); + }; + + const handleCopyToClipboard = (text: string) => { + navigator.clipboard.writeText(text).catch(err => console.error('Failed to copy text: ', err)); + }; + + const handleCopyTable = () => { + const tsvString = convertArrayToTsv(headers, filteredRows); + navigator.clipboard.writeText(tsvString).then(() => { + setCopySuccess(t.copySuccess); + setTimeout(() => setCopySuccess(''), 2000); + }).catch(err => { + console.error('Failed to copy table: ', err); + setCopySuccess(t.copyFailure); + setTimeout(() => setCopySuccess(''), 2000); + }); + }; + + const handleAddRowClick = () => { + if (onEnrichRow) { + setIsAddingRow(true); + } else { + const newEmptyRow = Array(headers.length).fill(''); + onDataChange([...rows, newEmptyRow]); + } + }; + + const handleConfirmAddRow = () => { + if (newRowValue.trim() && onEnrichRow) { + onEnrichRow(newRowValue.trim(), newRowUrl.trim()); + setNewRowValue(''); + setNewRowUrl(''); + setIsAddingRow(false); + } + }; + + const handleCancelAddRow = () => { + setNewRowValue(''); + setNewRowUrl(''); + setIsAddingRow(false); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + e.preventDefault(); + handleConfirmAddRow(); + } else if (e.key === 'Escape') { + handleCancelAddRow(); + } + }; + + const handleDeleteRow = (rowIndexToDelete: number) => { + const newRows = rows.filter((_, index) => index !== rowIndexToDelete); + onDataChange(newRows); + }; + + const getColumnStyle = (header: string): React.CSSProperties => { + const lowerHeader = header.toLowerCase(); + + const wideColumnHeaders = [ + 'beschreibung', + 'description', + 'painpoint', + 'gain', + 'nutzen', + 'benefit', + 'kernbotschaft', + 'core message', + 'li-dm', + 'verantwortungsbereich', + 'area of responsibility' + ]; + + if (wideColumnHeaders.some(keyword => lowerHeader.includes(keyword))) { + return { width: '25%' }; + } + + return {}; + }; + + const loadingText = t.loadingButton.replace('...', ''); + const isLoadingCell = (cell: string) => cell.toLowerCase().includes(loadingText.toLowerCase()); + + return ( +
+
+

{title}

+
+ + {copySuccess && {copySuccess}} +
+
+ + {summary && summary.length > 0 && ( +
+

{t.summaryTitle}

+
    + {summary.map((item, index) =>
  • {item}
  • )} +
+
+ )} + +
+
+
+ +
+ setFilterQuery(e.target.value)} + placeholder={t.filterPlaceholder} + className="block w-full rounded-md border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-900 py-2 pl-10 pr-3 shadow-sm focus:border-sky-500 focus:ring-sky-500 sm:text-sm" + /> +
+
+ +
+ + + + {headers.map((header, index) => ( + + ))} + {canDeleteRows && } + + + + {filteredRows.map((row, rowIndex) => { + const originalRowIndex = rows.findIndex(originalRow => originalRow === row); + return ( + + {row.map((cell, colIndex) => ( +
{header}Aktionen
+ {isLoadingCell(cell) ? ( +
+ + {t.loadingButton} +
+ ) : ( +
+