import * as React from 'react'; import SwipeableDrawer from '@mui/material/SwipeableDrawer'; import Typography from '@mui/material/Typography'; import { Button, Grid, Box, TextField, IconButton, Tooltip } from '@mui/material'; import { useCallback, useEffect, useRef, useState } from "react"; import { useBrowserDimensionsStore } from "../../context/browserDimensions"; import Table from '@mui/material/Table'; import TableBody from '@mui/material/TableBody'; import TableCell from '@mui/material/TableCell'; import TableContainer from '@mui/material/TableContainer'; import TableHead from '@mui/material/TableHead'; import TableRow from '@mui/material/TableRow'; import Paper from '@mui/material/Paper'; import StorageIcon from '@mui/icons-material/Storage'; import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward'; import CloseIcon from '@mui/icons-material/Close'; import CheckIcon from '@mui/icons-material/Check'; import { SidePanelHeader } from '../recorder/SidePanelHeader'; import { useGlobalInfoStore } from '../../context/globalInfo'; import { useThemeMode } from '../../context/theme-provider'; import { useTranslation } from 'react-i18next'; import { useBrowserSteps } from '../../context/browserSteps'; import { useActionContext } from '../../context/browserActions'; interface InterpretationLogProps { isOpen: boolean; setIsOpen: (isOpen: boolean) => void; } export const InterpretationLog: React.FC = ({ isOpen, setIsOpen }) => { const { t } = useTranslation(); const [captureListData, setCaptureListData] = useState([]); const [captureTextData, setCaptureTextData] = useState([]); const [screenshotData, setScreenshotData] = useState([]); const [activeTab, setActiveTab] = useState(0); const [activeListTab, setActiveListTab] = useState(0); const [activeScreenshotTab, setActiveScreenshotTab] = useState(0); const [editingField, setEditingField] = useState<{listId: number, fieldKey: string} | null>(null); const [editingValue, setEditingValue] = useState(''); const [editingListName, setEditingListName] = useState(null); const [editingListNameValue, setEditingListNameValue] = useState(''); const [editingTextGroupName, setEditingTextGroupName] = useState(false); const [editingTextGroupNameValue, setEditingTextGroupNameValue] = useState('Text Data'); const [editingTextLabel, setEditingTextLabel] = useState(null); const [editingTextLabelValue, setEditingTextLabelValue] = useState(''); const [editingScreenshotName, setEditingScreenshotName] = useState(null); const [editingScreenshotNameValue, setEditingScreenshotNameValue] = useState(''); const logEndRef = useRef(null); const autoFocusedListIds = useRef>(new Set()); const previousDataLengths = useRef>(new Map()); const hasAutoFocusedTextTab = useRef(false); const previousGetText = useRef(false); const autoFocusedScreenshotIndices = useRef>(new Set()); const { browserSteps, updateListTextFieldLabel, removeListTextField, updateListStepName, updateScreenshotStepName, updateBrowserTextStepLabel, deleteBrowserStep, emitForStepId } = useBrowserSteps(); const { captureStage, getText } = useActionContext(); const { browserWidth, outputPreviewHeight, outputPreviewWidth } = useBrowserDimensionsStore(); const { currentWorkflowActionsState, shouldResetInterpretationLog, currentTextGroupName, setCurrentTextGroupName } = useGlobalInfoStore(); const [showPreviewData, setShowPreviewData] = useState(false); const userClosedDrawer = useRef(false); const lastListDataLength = useRef(0); const lastTextDataLength = useRef(0); const lastScreenshotDataLength = useRef(0); const toggleDrawer = (newOpen: boolean) => (event: React.KeyboardEvent | React.MouseEvent) => { if ( event.type === 'keydown' && ((event as React.KeyboardEvent).key === 'Tab' || (event as React.KeyboardEvent).key === 'Shift') ) { return; } if (!newOpen && isOpen) { userClosedDrawer.current = true; } setIsOpen(newOpen); }; const handleStartEdit = (listId: number, fieldKey: string, currentLabel: string) => { setEditingField({ listId, fieldKey }); setEditingValue(currentLabel); }; const handleSaveEdit = () => { if (editingField && editingValue.trim()) { const listStep = browserSteps.find(step => step.id === editingField.listId); const actionId = listStep?.actionId; updateListTextFieldLabel(editingField.listId, editingField.fieldKey, editingValue.trim()); // Emit updated action to backend after state update completes if (actionId) { setTimeout(() => emitForStepId(actionId), 0); } setEditingField(null); setEditingValue(''); } }; const handleCancelEdit = () => { setEditingField(null); setEditingValue(''); }; const handleDeleteField = (listId: number, fieldKey: string) => { const listStep = browserSteps.find(step => step.id === listId); const actionId = listStep?.actionId; removeListTextField(listId, fieldKey); // Emit updated action to backend after state update completes if (actionId) { setTimeout(() => emitForStepId(actionId), 0); } }; const handleStartEditListName = (listId: number, currentName: string) => { setEditingListName(listId); setEditingListNameValue(currentName); }; const handleSaveListName = () => { if (editingListName !== null) { const trimmedName = editingListNameValue.trim(); const finalName = trimmedName || `List Data ${captureListData.findIndex(l => l.id === editingListName) + 1}`; updateListStepName(editingListName, finalName); // Use ref-synced version of browserSteps via emitForStepId const listStep = browserSteps.find(step => step.id === editingListName); if (listStep?.actionId) { // small async delay ensures React state commit setTimeout(() => emitForStepId(listStep.actionId!), 0); } setEditingListName(null); setEditingListNameValue(''); } }; const handleStartEditTextGroupName = () => { setEditingTextGroupName(true); setEditingTextGroupNameValue(currentTextGroupName); }; const handleSaveTextGroupName = () => { const trimmedName = editingTextGroupNameValue.trim(); const finalName = trimmedName || 'Text Data'; console.log("SAVING TEXT GROUP NAME:", finalName); setCurrentTextGroupName(finalName); setEditingTextGroupName(false); // Emit after React updates global state setTimeout(() => { const activeTextStep = captureTextData.find(step => step.actionId); if (activeTextStep?.actionId) emitForStepId(activeTextStep.actionId); }, 0); }; const handleStartEditTextLabel = (textId: number, currentLabel: string) => { setEditingTextLabel(textId); setEditingTextLabelValue(currentLabel); }; const handleSaveTextLabel = () => { if (editingTextLabel !== null && editingTextLabelValue.trim()) { const textStep = browserSteps.find(step => step.id === editingTextLabel); const actionId = textStep?.actionId; updateBrowserTextStepLabel(editingTextLabel, editingTextLabelValue.trim()); // Emit updated action to backend after state update completes if (actionId) { setTimeout(() => emitForStepId(actionId), 0); } setEditingTextLabel(null); setEditingTextLabelValue(''); } }; const handleCancelTextLabel = () => { setEditingTextLabel(null); setEditingTextLabelValue(''); }; const handleDeleteTextStep = (textId: number) => { const textStep = browserSteps.find(step => step.id === textId); const actionId = textStep?.actionId; deleteBrowserStep(textId); // Emit updated action to backend after deletion if (actionId) { // Small delay to ensure state update completes setTimeout(() => emitForStepId(actionId), 0); } }; const handleStartEditScreenshotName = (screenshotStepId: number, currentName: string) => { setEditingScreenshotName(screenshotStepId); setEditingScreenshotNameValue(currentName); }; const handleSaveScreenshotName = () => { if (editingScreenshotName !== null) { const trimmedName = editingScreenshotNameValue.trim(); const screenshotSteps = browserSteps.filter(step => step.type === 'screenshot'); const screenshotIndex = screenshotSteps.findIndex(s => s.id === editingScreenshotName); const finalName = trimmedName || `Screenshot ${screenshotIndex + 1}`; updateScreenshotStepName(editingScreenshotName, finalName); const screenshotStep = browserSteps.find(step => step.id === editingScreenshotName); if (screenshotStep?.actionId) { const originalName = screenshotStep.name?.trim() || ""; const trimmedName = editingScreenshotNameValue.trim(); // 🚫 Only emit if name actually changed if (trimmedName && trimmedName !== originalName) { setTimeout(() => emitForStepId(screenshotStep.actionId!), 500); } else { console.log("🧠 Skipping emit — screenshot name unchanged."); } } setEditingScreenshotName(null); setEditingScreenshotNameValue(''); } }; const previousTabsCount = useRef({ lists: 0, texts: 0, screenshots: 0 }); const updateActiveTab = useCallback(() => { const availableTabs = getAvailableTabs(); const hasNewListData = captureListData.length > previousTabsCount.current.lists; const hasNewTextData = captureTextData.length > previousTabsCount.current.texts; const hasNewScreenshotData = screenshotData.length > previousTabsCount.current.screenshots; previousTabsCount.current = { lists: captureListData.length, texts: captureTextData.length, screenshots: screenshotData.length }; if (hasNewListData && availableTabs.findIndex(tab => tab.id === 'captureList') !== -1) { setActiveTab(availableTabs.findIndex(tab => tab.id === 'captureList')); } else if (hasNewTextData && availableTabs.findIndex(tab => tab.id === 'captureText') !== -1) { setActiveTab(availableTabs.findIndex(tab => tab.id === 'captureText')); } else if (hasNewScreenshotData && availableTabs.findIndex(tab => tab.id === 'captureScreenshot') !== -1) { setActiveTab(availableTabs.findIndex(tab => tab.id === 'captureScreenshot')); // Set the active screenshot tab to the latest screenshot setActiveScreenshotTab(screenshotData.length - 1); } }, [captureListData.length, captureTextData.length, screenshotData.length]); useEffect(() => { const textSteps = browserSteps.filter(step => step.type === 'text'); setCaptureTextData(textSteps); if (!getText && previousGetText.current && textSteps.length > 0) { if (!hasAutoFocusedTextTab.current) { hasAutoFocusedTextTab.current = true; setTimeout(() => { handleStartEditTextGroupName(); }, 300); } } previousGetText.current = getText; const listSteps = browserSteps.filter(step => step.type === 'list'); setCaptureListData(listSteps); const screenshotSteps = browserSteps.filter(step => step.type === 'screenshot' ) as Array<{ type: 'screenshot'; id: number; name?: string; fullPage: boolean; actionId?: string; screenshotData?: string }>; const screenshotsWithData = screenshotSteps.filter(step => step.screenshotData); const screenshots = screenshotsWithData.map(step => step.screenshotData!); setScreenshotData(screenshots); if (textSteps.length > 0 || listSteps.length > 0 || screenshots.length > 0) { setShowPreviewData(true); } else { setShowPreviewData(false); } updateActiveTab(); }, [browserSteps, updateActiveTab, getText]); useEffect(() => { if (shouldResetInterpretationLog) { setCaptureListData([]); setCaptureTextData([]); setScreenshotData([]); setActiveTab(0); setShowPreviewData(false); autoFocusedListIds.current.clear(); previousDataLengths.current.clear(); autoFocusedScreenshotIndices.current.clear(); userClosedDrawer.current = false; lastListDataLength.current = 0; lastTextDataLength.current = 0; lastScreenshotDataLength.current = 0; previousTabsCount.current = { lists: 0, texts: 0, screenshots: 0 }; hasAutoFocusedTextTab.current = false; previousGetText.current = false; } }, [shouldResetInterpretationLog]); const getAvailableTabs = useCallback(() => { const tabs = []; if (captureListData.length > 0) { tabs.push({ id: 'captureList', label: 'Lists' }); } if (captureTextData.length > 0) { tabs.push({ id: 'captureText', label: 'Texts' }); } if (screenshotData.length > 0) { tabs.push({ id: 'captureScreenshot', label: 'Screenshots' }); } return tabs; }, [captureListData.length, captureTextData.length, screenshotData.length, showPreviewData]); const availableTabs = getAvailableTabs(); useEffect(() => { if (activeTab >= availableTabs.length && availableTabs.length > 0) { setActiveTab(0); } }, [activeTab, availableTabs.length]); const { hasScrapeListAction, hasScreenshotAction, hasScrapeSchemaAction } = currentWorkflowActionsState; useEffect(() => { let shouldOpenDrawer = false; let switchToTextTab = false; let switchToScreenshotTab = false; if (hasScrapeListAction && captureListData.length > 0 && captureListData[0]?.data?.length > 0) { setShowPreviewData(true); if (captureListData.length > lastListDataLength.current) { userClosedDrawer.current = false; shouldOpenDrawer = true; } lastListDataLength.current = captureListData.length; } if (hasScrapeSchemaAction && captureTextData.length > 0 && !getText) { setShowPreviewData(true); if (captureTextData.length > lastTextDataLength.current) { userClosedDrawer.current = false; shouldOpenDrawer = true; switchToTextTab = true; } lastTextDataLength.current = captureTextData.length; } if (hasScreenshotAction && screenshotData.length > 0) { setShowPreviewData(true); if (screenshotData.length > lastScreenshotDataLength.current) { userClosedDrawer.current = false; shouldOpenDrawer = true; switchToScreenshotTab = true; } lastScreenshotDataLength.current = screenshotData.length; } if (shouldOpenDrawer) { setIsOpen(true); if (switchToTextTab) { setTimeout(() => { const textTabIndex = getAvailableTabs().findIndex(tab => tab.id === 'captureText'); if (textTabIndex !== -1) { setActiveTab(textTabIndex); } }, 100); } else if (switchToScreenshotTab) { setTimeout(() => { const screenshotTabIndex = getAvailableTabs().findIndex(tab => tab.id === 'captureScreenshot'); if (screenshotTabIndex !== -1) { setActiveTab(screenshotTabIndex); const latestIndex = screenshotData.length - 1; setActiveScreenshotTab(latestIndex); if (!autoFocusedScreenshotIndices.current.has(latestIndex)) { autoFocusedScreenshotIndices.current.add(latestIndex); setTimeout(() => { const screenshotSteps = browserSteps.filter(step => step.type === 'screenshot') as Array<{ id: number; name?: string; type: 'screenshot' }>; const latestScreenshotStep = screenshotSteps[latestIndex]; if (latestScreenshotStep) { const screenshotName = latestScreenshotStep.name || `Screenshot ${latestIndex + 1}`; handleStartEditScreenshotName(latestScreenshotStep.id, screenshotName); } }, 300); } } }, 100); } } }, [hasScrapeListAction, hasScrapeSchemaAction, hasScreenshotAction, captureListData, captureTextData, screenshotData, setIsOpen, getText]); useEffect(() => { if (captureListData.length > 0 && isOpen && captureStage === 'initial') { const latestListIndex = captureListData.length - 1; const latestList = captureListData[latestListIndex]; if (latestList && latestList.data && latestList.data.length > 0 && !editingListName) { const previousLength = previousDataLengths.current.get(latestList.id) || 0; const currentLength = latestList.data.length; if (previousLength === 0 && currentLength > 0) { if (!autoFocusedListIds.current.has(latestList.id)) { autoFocusedListIds.current.add(latestList.id); setActiveListTab(latestListIndex); setTimeout(() => { handleStartEditListName(latestList.id, latestList.name || `List Data ${latestListIndex + 1}`); }, 300); } } previousDataLengths.current.set(latestList.id, currentLength); } } }, [captureListData.length, isOpen, captureStage]); useEffect(() => { if (screenshotData.length > 0 && isOpen) { const latestScreenshotIndex = screenshotData.length - 1; setActiveScreenshotTab(latestScreenshotIndex); } }, [screenshotData.length, isOpen]); const { darkMode } = useThemeMode(); const shouldShowTabs = availableTabs.length > 1; const getSingleContentType = () => { if (availableTabs.length === 1) { return availableTabs[0].id; } return null; }; const singleContentType = getSingleContentType(); return (
{t('interpretation_log.titles.output_preview')} {!(hasScrapeListAction || hasScrapeSchemaAction || hasScreenshotAction) && ( {t('interpretation_log.messages.no_selection')} )} {showPreviewData && availableTabs.length > 0 && ( <> {shouldShowTabs && ( {availableTabs.map((tab, index) => ( setActiveTab(index)} sx={{ px: 4, py: 2, cursor: 'pointer', // borderBottom: activeTab === index ? '2px solid' : 'none', borderColor: activeTab === index ? (darkMode ? '#ff00c3' : '#ff00c3') : 'transparent', backgroundColor: activeTab === index ? (darkMode ? '#121111ff' : '#e9ecef') : 'transparent', color: darkMode ? 'white' : 'black', fontWeight: activeTab === index ? 500 : 400, textAlign: 'center', position: 'relative', '&:hover': { backgroundColor: activeTab !== index ? (darkMode ? '#121111ff' : '#e2e6ea') : undefined } }} > {tab.label} ))} )} {(activeTab === availableTabs.findIndex(tab => tab.id === 'captureList') || singleContentType === 'captureList') && captureListData.length > 0 && ( {/* List Tabs */} {captureListData.map((listItem, index) => { const isEditing = editingListName === listItem.id; const isActive = activeListTab === index; return ( { if (!isEditing) { setActiveListTab(index); } }} onDoubleClick={() => { handleStartEditListName( listItem.id, listItem.name || `List Data ${index + 1}` ); }} sx={{ px: 3, py: 1.25, cursor: isEditing ? 'text' : 'pointer', borderRadius: '8px 8px 0 0', backgroundColor: darkMode ? '#131313ff' : '#ffffff', color: isActive ? darkMode ? '#ffffff' : '#000000' : darkMode ? '#b0b0b0' : '#555555', fontWeight: isActive ? 600 : 400, fontSize: '0.875rem', border: '1px solid', borderColor: darkMode ? '#2a2a2a' : '#d0d0d0', borderBottom: isActive ? darkMode ? '2px solid #1c1c1c' : '2px solid #ffffff' : '2px solid transparent', transition: 'all 0.2s ease', '&:hover': { backgroundColor: isActive ? undefined : darkMode ? '#161616' : '#e9ecef', }, }} > {isEditing ? ( setEditingListNameValue(e.target.value)} onBlur={handleSaveListName} onKeyDown={(e) => { if (e.key === 'Enter') handleSaveListName(); if (e.key === 'Escape') { setEditingListName(null); setEditingListNameValue(''); } }} autoFocus size="small" variant="standard" sx={{ minWidth: '120px', '& .MuiInputBase-input': { color: darkMode ? '#fff' : '#000', fontSize: 'inherit', fontWeight: 'inherit', padding: 0, }, '& .MuiInput-underline:before': { display: 'none' }, '& .MuiInput-underline:after': { display: 'none' }, '& .MuiInput-underline:hover:before': { display: 'none' }, }} /> ) : ( listItem.name || `List Data ${index + 1}` )} ); })} {/* Table Below Tabs */} {Object.entries(captureListData[activeListTab]?.fields || {}).map(([fieldKey, field]: [string, any]) => { const isEditing = editingField?.listId === captureListData[activeListTab]?.id && editingField?.fieldKey === fieldKey; const isFirstField = Object.keys(captureListData[activeListTab]?.fields || {}).indexOf(fieldKey) === 0; return ( {isEditing ? ( setEditingValue(e.target.value)} onBlur={handleSaveEdit} onKeyDown={(e) => { if (e.key === 'Enter') handleSaveEdit(); if (e.key === 'Escape') handleCancelEdit(); }} autoFocus size="small" sx={{ flex: 1, minWidth: '150px', '& .MuiInputBase-root': { backgroundColor: darkMode ? '#2a2929' : '#fff' } }} /> ) : ( handleStartEdit(captureListData[activeListTab]?.id, fieldKey, field.label)} > {field.label} handleDeleteField(captureListData[activeListTab]?.id, fieldKey)} sx={{ position: 'absolute', right: 4, top: '50%', transform: 'translateY(-50%)', opacity: 0, transition: 'opacity 0.2s', color: darkMode ? '#999' : '#666', padding: '4px', '&:hover': { color: '#f44336', backgroundColor: darkMode ? 'rgba(244, 67, 54, 0.1)' : 'rgba(244, 67, 54, 0.05)' } }} > )} ); })} {(captureListData[activeListTab]?.data || []) .slice(0, Math.min(captureListData[activeListTab]?.limit || 10, 5)) .map((row: any, rowIndex: any) => ( {Object.values(captureListData[activeListTab]?.fields || {}).map((field: any, colIndex) => ( {typeof row[field.label] === 'object' ? JSON.stringify(row[field.label]) : String(row[field.label] || '')} ))} )) }
)} {(activeTab === availableTabs.findIndex(tab => tab.id === 'captureScreenshot') || singleContentType === 'captureScreenshot') && screenshotData.length > 0 && ( {/* Screenshot Tabs */} {(() => { const screenshotSteps = browserSteps.filter(step => step.type === 'screenshot' && step.screenshotData) as Array<{ id: number; name?: string; type: 'screenshot'; screenshotData?: string }>; return screenshotData.map((screenshot, index) => { const screenshotStep = screenshotSteps[index]; if (!screenshotStep) return null; const isActive = activeScreenshotTab === index; const isEditing = editingScreenshotName === screenshotStep.id; const screenshotName = screenshotStep.name || `Screenshot ${index + 1}`; return ( { if (!isEditing) { setActiveScreenshotTab(index); } }} onDoubleClick={() => { handleStartEditScreenshotName(screenshotStep.id, screenshotName); }} sx={{ px: 3, py: 1.25, cursor: isEditing ? 'text' : 'pointer', borderRadius: '8px 8px 0 0', backgroundColor: darkMode ? '#131313ff' : '#ffffff', color: isActive ? darkMode ? '#ffffff' : '#000000' : darkMode ? '#b0b0b0' : '#555555', fontWeight: isActive ? 600 : 400, fontSize: '0.875rem', border: '1px solid', borderColor: darkMode ? '#2a2a2a' : '#d0d0d0', borderBottom: isActive ? darkMode ? '2px solid #1c1c1c' : '2px solid #ffffff' : '2px solid transparent', transition: 'all 0.2s ease', '&:hover': { backgroundColor: isActive ? undefined : darkMode ? '#161616' : '#e9ecef', }, }} > {isEditing ? ( setEditingScreenshotNameValue(e.target.value)} onBlur={handleSaveScreenshotName} onKeyDown={(e) => { if (e.key === 'Enter') handleSaveScreenshotName(); if (e.key === 'Escape') { setEditingScreenshotName(null); setEditingScreenshotNameValue(''); } }} autoFocus size="small" variant="standard" sx={{ minWidth: '120px', '& .MuiInputBase-input': { color: darkMode ? '#fff' : '#000', fontSize: 'inherit', fontWeight: 'inherit', padding: 0, }, '& .MuiInput-underline:before': { display: 'none' }, '& .MuiInput-underline:after': { display: 'none' }, '& .MuiInput-underline:hover:before': { display: 'none' }, }} /> ) : ( screenshotName )} ); }); })()} {/* Screenshot Image */} {`Screenshot )} {(activeTab === availableTabs.findIndex(tab => tab.id === 'captureText') || singleContentType === 'captureText') && captureTextData.length > 0 && ( {editingTextGroupName ? ( setEditingTextGroupNameValue(e.target.value)} onBlur={handleSaveTextGroupName} onKeyDown={(e) => { if (e.key === 'Enter') handleSaveTextGroupName(); if (e.key === 'Escape') { setEditingTextGroupName(false); setEditingTextGroupNameValue(currentTextGroupName); } }} autoFocus size="small" variant="standard" sx={{ minWidth: '120px', '& .MuiInputBase-input': { color: darkMode ? '#fff' : '#000', fontSize: 'inherit', fontWeight: 'inherit', padding: 0, }, '& .MuiInput-underline:before': { display: 'none' }, '& .MuiInput-underline:after': { display: 'none' }, '& .MuiInput-underline:hover:before': { display: 'none' }, }} /> ) : ( currentTextGroupName )} Label Value {captureTextData.map((textStep: any, index) => { const isEditing = editingTextLabel === textStep.id; return ( {isEditing ? ( setEditingTextLabelValue(e.target.value)} onBlur={handleSaveTextLabel} onKeyDown={(e) => { if (e.key === 'Enter') handleSaveTextLabel(); if (e.key === 'Escape') handleCancelTextLabel(); }} autoFocus size="small" sx={{ flex: 1, minWidth: '150px', '& .MuiInputBase-root': { backgroundColor: darkMode ? '#2a2929' : '#fff' } }} /> ) : ( handleStartEditTextLabel(textStep.id, textStep.label)} > {textStep.label} handleDeleteTextStep(textStep.id)} sx={{ position: 'absolute', right: 4, top: '50%', transform: 'translateY(-50%)', opacity: 0, transition: 'opacity 0.2s', color: darkMode ? '#999' : '#666', padding: '4px', '&:hover': { color: '#f44336', backgroundColor: darkMode ? 'rgba(244, 67, 54, 0.1)' : 'rgba(244, 67, 54, 0.05)' } }} > )} {typeof textStep.data === 'object' ? JSON.stringify(textStep.data) : String(textStep.data || '')} ); })}
)}
)}
); };