Refactor: Approve tool use

This commit is contained in:
gpt-engineer-app[bot]
2025-10-06 18:24:20 +00:00
parent c2c70524f7
commit 63a7877314
4 changed files with 175 additions and 33 deletions

View File

@@ -128,39 +128,61 @@ interface PhotoDeletionPreviewProps {
export function PhotoDeletionPreview({ photo, compact = false }: PhotoDeletionPreviewProps) {
if (compact) {
return (
<Badge variant="outline" className="text-red-600 dark:text-red-400">
<Trash2 className="h-3 w-3 mr-1" />
Delete Photo
</Badge>
<div className="flex items-center gap-2 p-2 rounded-md bg-destructive/10 border border-destructive/20">
<Trash2 className="h-4 w-4 text-destructive" />
<span className="text-sm font-medium text-destructive">Delete Photo</span>
{photo.deletion_reason && (
<span className="text-xs text-muted-foreground">- {photo.deletion_reason}</span>
)}
</div>
);
}
return (
<div className="flex flex-col gap-2 p-3 rounded-md bg-destructive/10">
<div className="text-sm font-medium text-red-600 dark:text-red-400">
<Trash2 className="h-4 w-4 inline mr-1" />
Deleting Photo
<div className="flex flex-col gap-3 p-4 rounded-lg bg-destructive/10 border-2 border-destructive/30">
<div className="flex items-center gap-2 text-destructive font-semibold">
<Trash2 className="h-5 w-5" />
<span>Photo Deletion Request</span>
</div>
<div className="flex gap-3">
<div className="flex gap-4">
{photo.url && (
<img
src={photo.url}
alt={photo.title || photo.caption || 'Photo to be deleted'}
className="w-32 h-32 object-cover rounded opacity-75"
className="w-48 h-48 object-cover rounded-lg border-2 border-destructive/40 shadow-lg"
loading="lazy"
/>
)}
<div className="flex-1 text-sm space-y-2">
{photo.title && <div className="font-medium">{photo.title}</div>}
{photo.caption && <div className="text-muted-foreground">{photo.caption}</div>}
{photo.entity_type && photo.entity_name && (
<div className="text-xs text-muted-foreground">
From: <span className="capitalize">{photo.entity_type}</span> - {photo.entity_name}
<div className="flex-1 space-y-3">
{photo.title && (
<div>
<span className="text-xs font-medium text-muted-foreground">Title:</span>
<div className="font-medium">{photo.title}</div>
</div>
)}
{photo.caption && (
<div>
<span className="text-xs font-medium text-muted-foreground">Caption:</span>
<div className="text-sm">{photo.caption}</div>
</div>
)}
{photo.entity_type && photo.entity_name && (
<div>
<span className="text-xs font-medium text-muted-foreground">From Entity:</span>
<div className="text-sm">
<span className="capitalize">{photo.entity_type.replace('_', ' ')}</span> - <span className="font-medium">{photo.entity_name}</span>
</div>
</div>
)}
{photo.deletion_reason && (
<div className="text-xs p-2 bg-destructive/5 rounded border border-destructive/20">
<span className="font-medium">Reason:</span> {photo.deletion_reason}
<div className="p-3 bg-destructive/20 rounded-md border border-destructive/30">
<span className="text-xs font-bold text-destructive uppercase">Deletion Reason:</span>
<p className="mt-1 text-sm font-medium">{photo.deletion_reason}</p>
</div>
)}
</div>

View File

@@ -85,6 +85,33 @@ export function SubmissionChangesDisplay({
);
if (view === 'summary') {
// Special compact display for photo deletions
if (item.item_type === 'photo_delete') {
return (
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2 flex-wrap">
{getEntityIcon()}
<span className="font-medium">{changes.entityName}</span>
{getActionBadge()}
</div>
{changes.photoChanges.length > 0 && changes.photoChanges[0].type === 'deleted' && (
<PhotoDeletionPreview
photo={{
url: changes.photoChanges[0].photo?.url || item.item_data?.cloudflare_image_url || '',
title: changes.photoChanges[0].photo?.title || item.item_data?.title,
caption: changes.photoChanges[0].photo?.caption || item.item_data?.caption,
entity_type: item.item_data?.entity_type,
entity_name: changes.entityName,
deletion_reason: changes.photoChanges[0].photo?.deletion_reason || item.item_data?.deletion_reason || item.item_data?.reason
}}
compact={true}
/>
)}
</div>
);
}
return (
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2 flex-wrap">
@@ -146,7 +173,34 @@ export function SubmissionChangesDisplay({
);
}
// Detailed view
// Detailed view - special handling for photo deletions
if (item.item_type === 'photo_delete') {
return (
<div className="flex flex-col gap-4">
<div className="flex items-center gap-2">
{getEntityIcon()}
<h3 className="text-lg font-semibold">{changes.entityName}</h3>
{getActionBadge()}
</div>
{changes.photoChanges.length > 0 && changes.photoChanges[0].type === 'deleted' && (
<PhotoDeletionPreview
photo={{
url: changes.photoChanges[0].photo?.url || item.item_data?.cloudflare_image_url || '',
title: changes.photoChanges[0].photo?.title || item.item_data?.title,
caption: changes.photoChanges[0].photo?.caption || item.item_data?.caption,
entity_type: item.item_data?.entity_type,
entity_name: changes.entityName,
deletion_reason: changes.photoChanges[0].photo?.deletion_reason || item.item_data?.deletion_reason || item.item_data?.reason
}}
compact={false}
/>
)}
</div>
);
}
// Detailed view for other items
return (
<div className="flex flex-col gap-4">
<div className="flex items-center gap-2">

View File

@@ -136,10 +136,13 @@ export async function detectChanges(
const itemData = item.item_data || {};
const originalData = item.original_data || {};
// Determine action type
const action: 'create' | 'edit' | 'delete' =
!originalData || Object.keys(originalData).length === 0 ? 'create' :
itemData.deleted ? 'delete' : 'edit';
// Determine action type - special handling for photo_delete
let action: 'create' | 'edit' | 'delete' = 'edit';
if (item.item_type === 'photo_delete' || itemData.action === 'delete' || itemData.deleted) {
action = 'delete';
} else if (!originalData || Object.keys(originalData).length === 0) {
action = 'create';
}
const fieldChanges: FieldChange[] = [];
const imageChanges: ImageChange[] = [];
@@ -269,22 +272,32 @@ export async function detectChanges(
const entityId = itemData.entity_id;
if (entityType === 'park') {
const { data } = await supabase.from('parks').select('name').eq('id', entityId).single();
const { data } = await supabase.from('parks').select('name').eq('id', entityId).maybeSingle();
if (data?.name) entityName = `${data.name} (${formatEntityType(entityType)})`;
} else if (entityType === 'ride') {
const { data } = await supabase.from('rides').select('name').eq('id', entityId).single();
const { data } = await supabase.from('rides').select('name').eq('id', entityId).maybeSingle();
if (data?.name) entityName = `${data.name} (${formatEntityType(entityType)})`;
} else if (entityType === 'ride_model') {
const { data } = await supabase.from('ride_models').select('name').eq('id', entityId).single();
const { data } = await supabase.from('ride_models').select('name').eq('id', entityId).maybeSingle();
if (data?.name) entityName = `${data.name} (${formatEntityType(entityType)})`;
} else if (['manufacturer', 'operator', 'designer', 'property_owner'].includes(entityType)) {
const { data } = await supabase.from('companies').select('name').eq('id', entityId).single();
const { data } = await supabase.from('companies').select('name').eq('id', entityId).maybeSingle();
if (data?.name) entityName = `${data.name} (${formatEntityType(entityType)})`;
}
} catch (err) {
console.error('Error fetching entity name:', err);
console.error('Error fetching entity name for photo operation:', err);
}
}
// Add debugging warning if critical data is missing
if (!itemData.entity_name && item.item_type === 'photo_delete') {
console.warn(`[Photo Delete] Missing entity_name for photo_delete item`, {
item_type: item.item_type,
has_entity_type: !!itemData.entity_type,
has_entity_id: !!itemData.entity_id,
has_cloudflare_url: !!itemData.cloudflare_image_url
});
}
} else {
// For regular entities, use name field
entityName = itemData.name || originalData?.name || 'Unknown';

View File

@@ -0,0 +1,53 @@
-- Phase 1: Fix existing photo_delete submissions with missing entity_name
-- Create a function to backfill missing entity names for photo_delete submissions
CREATE OR REPLACE FUNCTION backfill_photo_delete_entity_names()
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public'
AS $$
DECLARE
item_record RECORD;
entity_name_value TEXT;
BEGIN
-- Find all photo_delete submission_items missing entity_name
FOR item_record IN
SELECT id, item_data
FROM submission_items
WHERE item_type = 'photo_delete'
AND item_data->>'entity_name' IS NULL
AND item_data->>'entity_id' IS NOT NULL
AND item_data->>'entity_type' IS NOT NULL
LOOP
-- Fetch entity name based on entity_type
CASE item_record.item_data->>'entity_type'
WHEN 'park' THEN
SELECT name INTO entity_name_value FROM parks WHERE id = (item_record.item_data->>'entity_id')::uuid;
WHEN 'ride' THEN
SELECT name INTO entity_name_value FROM rides WHERE id = (item_record.item_data->>'entity_id')::uuid;
WHEN 'ride_model' THEN
SELECT name INTO entity_name_value FROM ride_models WHERE id = (item_record.item_data->>'entity_id')::uuid;
WHEN 'manufacturer', 'operator', 'designer', 'property_owner' THEN
SELECT name INTO entity_name_value FROM companies WHERE id = (item_record.item_data->>'entity_id')::uuid;
ELSE
entity_name_value := NULL;
END CASE;
-- Update the item_data if we found a name
IF entity_name_value IS NOT NULL THEN
UPDATE submission_items
SET item_data = item_data || jsonb_build_object('entity_name', entity_name_value)
WHERE id = item_record.id;
RAISE NOTICE 'Updated submission_item % with entity_name: %', item_record.id, entity_name_value;
END IF;
END LOOP;
END;
$$;
-- Run the backfill function
SELECT backfill_photo_delete_entity_names();
-- Drop the function after use (optional, but keeps things clean)
DROP FUNCTION IF EXISTS backfill_photo_delete_entity_names();