|
@@ -75,6 +75,7 @@ interface ScraperContent {
|
|
|
content_type: 'shipping' | 'contacts' | 'terms' | 'faq';
|
|
content_type: 'shipping' | 'contacts' | 'terms' | 'faq';
|
|
|
title?: string;
|
|
title?: string;
|
|
|
content: string;
|
|
content: string;
|
|
|
|
|
+ enabled: boolean;
|
|
|
scraped_at: string;
|
|
scraped_at: string;
|
|
|
metadata?: Record<string, unknown>;
|
|
metadata?: Record<string, unknown>;
|
|
|
}
|
|
}
|
|
@@ -550,12 +551,21 @@ export function ManageStoreDataContent({ defaultTab }: ManageStoreDataContentPro
|
|
|
fetchCustomUrls();
|
|
fetchCustomUrls();
|
|
|
} else {
|
|
} else {
|
|
|
const error = await response.json();
|
|
const error = await response.json();
|
|
|
|
|
+ // Handle 409 conflict - URL already exists in scraped content
|
|
|
|
|
+ if (response.status === 409 || error.message?.includes('already exists') || error.message?.includes('already been scraped')) {
|
|
|
|
|
+ toast({
|
|
|
|
|
+ title: t('common.error'),
|
|
|
|
|
+ description: t('manageStoreData.website.toast.urlAlreadyExists') || 'This URL has already been scraped from your sitemap. You can enable/disable it in the scraped content list.',
|
|
|
|
|
+ variant: "destructive"
|
|
|
|
|
+ });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
throw new Error(error.error || 'Failed to add custom URL');
|
|
throw new Error(error.error || 'Failed to add custom URL');
|
|
|
}
|
|
}
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
toast({
|
|
toast({
|
|
|
title: t('common.error'),
|
|
title: t('common.error'),
|
|
|
- description: error.message || t('manageStoreData.website.toast.urlAddError'),
|
|
|
|
|
|
|
+ description: (error as Error).message || t('manageStoreData.website.toast.urlAddError'),
|
|
|
variant: "destructive"
|
|
variant: "destructive"
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
@@ -1545,75 +1555,42 @@ export function ManageStoreDataContent({ defaultTab }: ManageStoreDataContentPro
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
<div className="flex items-center gap-2 ml-2">
|
|
<div className="flex items-center gap-2 ml-2">
|
|
|
- <Button
|
|
|
|
|
- variant="outline"
|
|
|
|
|
- size="sm"
|
|
|
|
|
- onClick={() => {
|
|
|
|
|
- setPreviewContent(content);
|
|
|
|
|
- setPreviewMode('rendered');
|
|
|
|
|
- }}
|
|
|
|
|
- className="border-slate-600 hover:bg-slate-700"
|
|
|
|
|
- >
|
|
|
|
|
- <Eye className="w-3 h-3 mr-1" />
|
|
|
|
|
- Preview
|
|
|
|
|
- </Button>
|
|
|
|
|
|
|
+ {content.enabled && (
|
|
|
|
|
+ <Button
|
|
|
|
|
+ variant="outline"
|
|
|
|
|
+ size="sm"
|
|
|
|
|
+ onClick={() => {
|
|
|
|
|
+ setPreviewContent(content);
|
|
|
|
|
+ setPreviewMode('rendered');
|
|
|
|
|
+ }}
|
|
|
|
|
+ className="border-slate-600 hover:bg-slate-700 text-slate-200"
|
|
|
|
|
+ >
|
|
|
|
|
+ <Eye className="w-3 h-3 mr-1" />
|
|
|
|
|
+ {t('manageStoreData.website.contentViewer.preview', 'Preview')}
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ )}
|
|
|
<Switch
|
|
<Switch
|
|
|
- checked={(() => {
|
|
|
|
|
- // Check if this URL exists in customUrls as disabled
|
|
|
|
|
- const customUrl = customUrls.find(cu => cu.url === content.url);
|
|
|
|
|
- return customUrl ? customUrl.enabled : true;
|
|
|
|
|
- })()}
|
|
|
|
|
- onCheckedChange={async (checked) => {
|
|
|
|
|
- // Toggle URL enabled/disabled
|
|
|
|
|
- try {
|
|
|
|
|
- const sessionData = localStorage.getItem('session_data');
|
|
|
|
|
- if (!sessionData) throw new Error('No session data found');
|
|
|
|
|
-
|
|
|
|
|
- const session = JSON.parse(sessionData);
|
|
|
|
|
-
|
|
|
|
|
- // Check if URL already exists in custom URLs
|
|
|
|
|
- const existingCustomUrl = customUrls.find(cu => cu.url === content.url);
|
|
|
|
|
-
|
|
|
|
|
- if (existingCustomUrl) {
|
|
|
|
|
- // Update existing custom URL
|
|
|
|
|
- const response = await fetch(`${API_URL}/scraper-management/custom-urls/${existingCustomUrl.id}/toggle`, {
|
|
|
|
|
- method: 'PATCH',
|
|
|
|
|
- headers: {
|
|
|
|
|
- 'Authorization': `Bearer ${session.session.access_token}`,
|
|
|
|
|
- 'Content-Type': 'application/json'
|
|
|
|
|
- },
|
|
|
|
|
- body: JSON.stringify({
|
|
|
|
|
- store_id: selectedStore?.id,
|
|
|
|
|
- enabled: checked
|
|
|
|
|
- })
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- if (!response.ok) {
|
|
|
|
|
- throw new Error('Failed to toggle URL');
|
|
|
|
|
- }
|
|
|
|
|
- } else {
|
|
|
|
|
- // Add as new custom URL with specified enabled status
|
|
|
|
|
- const response = await fetch(`${API_URL}/scraper-management/custom-urls`, {
|
|
|
|
|
- method: 'POST',
|
|
|
|
|
- headers: {
|
|
|
|
|
- 'Authorization': `Bearer ${session.session.access_token}`,
|
|
|
|
|
- 'Content-Type': 'application/json'
|
|
|
|
|
- },
|
|
|
|
|
- body: JSON.stringify({
|
|
|
|
|
- store_id: selectedStore?.id,
|
|
|
|
|
- url: content.url,
|
|
|
|
|
- content_type: content.content_type,
|
|
|
|
|
- })
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- if (!response.ok) {
|
|
|
|
|
- throw new Error('Failed to add custom URL');
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // If we want to disable it, toggle it right after adding
|
|
|
|
|
- if (!checked) {
|
|
|
|
|
- const newCustomUrl = await response.json();
|
|
|
|
|
- await fetch(`${API_URL}/scraper-management/custom-urls/${newCustomUrl.id}/toggle`, {
|
|
|
|
|
|
|
+ checked={content.enabled}
|
|
|
|
|
+ onCheckedChange={(checked) => {
|
|
|
|
|
+ // Show warning dialog about toggle consequences
|
|
|
|
|
+ const isEnabling = checked;
|
|
|
|
|
+ setConfirmDialog({
|
|
|
|
|
+ open: true,
|
|
|
|
|
+ title: isEnabling
|
|
|
|
|
+ ? t('manageStoreData.website.urlToggle.enableTitle', 'Enable URL Scraping')
|
|
|
|
|
+ : t('manageStoreData.website.urlToggle.disableTitle', 'Disable URL Scraping'),
|
|
|
|
|
+ description: isEnabling
|
|
|
|
|
+ ? t('manageStoreData.website.urlToggle.enableWarning', 'Re-enabling this URL will immediately re-scrape the content and create new embeddings. This may take a few moments.')
|
|
|
|
|
+ : t('manageStoreData.website.urlToggle.disableWarning', 'Disabling this URL will remove all scraped content and embeddings for this page. If you re-enable it later, the content will be re-scraped immediately.'),
|
|
|
|
|
+ action: async () => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const sessionData = localStorage.getItem('session_data');
|
|
|
|
|
+ if (!sessionData) throw new Error('No session data found');
|
|
|
|
|
+
|
|
|
|
|
+ const session = JSON.parse(sessionData);
|
|
|
|
|
+
|
|
|
|
|
+ // Call the content enable/disable endpoint
|
|
|
|
|
+ const response = await fetch(`${API_URL}/scraper-management/content/${content.id}`, {
|
|
|
method: 'PATCH',
|
|
method: 'PATCH',
|
|
|
headers: {
|
|
headers: {
|
|
|
'Authorization': `Bearer ${session.session.access_token}`,
|
|
'Authorization': `Bearer ${session.session.access_token}`,
|
|
@@ -1621,28 +1598,36 @@ export function ManageStoreDataContent({ defaultTab }: ManageStoreDataContentPro
|
|
|
},
|
|
},
|
|
|
body: JSON.stringify({
|
|
body: JSON.stringify({
|
|
|
store_id: selectedStore?.id,
|
|
store_id: selectedStore?.id,
|
|
|
- enabled: false
|
|
|
|
|
|
|
+ enabled: checked
|
|
|
})
|
|
})
|
|
|
});
|
|
});
|
|
|
|
|
+
|
|
|
|
|
+ if (!response.ok) {
|
|
|
|
|
+ const errorData = await response.json().catch(() => ({}));
|
|
|
|
|
+ throw new Error(errorData.error || 'Failed to toggle content');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ toast({
|
|
|
|
|
+ title: checked
|
|
|
|
|
+ ? t('manageStoreData.website.toast.urlEnabled', 'Content enabled')
|
|
|
|
|
+ : t('manageStoreData.website.toast.urlDisabled', 'Content disabled'),
|
|
|
|
|
+ description: checked
|
|
|
|
|
+ ? t('manageStoreData.website.urlToggle.enableSuccess', 'URL has been re-scraped and content is now available')
|
|
|
|
|
+ : t('manageStoreData.website.urlToggle.disableSuccess', 'Scraped content and embeddings have been removed'),
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // Refresh content list
|
|
|
|
|
+ fetchScraperContent();
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('Error toggling content:', error);
|
|
|
|
|
+ toast({
|
|
|
|
|
+ title: t('manageStoreData.website.toast.urlToggleError', 'Error'),
|
|
|
|
|
+ description: error instanceof Error ? error.message : 'Failed to toggle content status',
|
|
|
|
|
+ variant: "destructive",
|
|
|
|
|
+ });
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- toast({
|
|
|
|
|
- title: checked ? "URL enabled" : "URL disabled",
|
|
|
|
|
- description: checked ? "This URL will be included in future scrapes" : "This URL will be skipped in future scrapes",
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- // Refresh both content and custom URLs
|
|
|
|
|
- fetchScraperContent();
|
|
|
|
|
- fetchCustomUrls();
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- console.error('Error toggling URL:', error);
|
|
|
|
|
- toast({
|
|
|
|
|
- title: "Error",
|
|
|
|
|
- description: "Failed to toggle URL status",
|
|
|
|
|
- variant: "destructive",
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ });
|
|
|
}}
|
|
}}
|
|
|
className="data-[state=checked]:bg-cyan-500"
|
|
className="data-[state=checked]:bg-cyan-500"
|
|
|
/>
|
|
/>
|
|
@@ -1720,22 +1705,26 @@ export function ManageStoreDataContent({ defaultTab }: ManageStoreDataContentPro
|
|
|
</span>
|
|
</span>
|
|
|
<div className="flex items-center gap-2 ml-4">
|
|
<div className="flex items-center gap-2 ml-4">
|
|
|
<Button
|
|
<Button
|
|
|
- variant={previewMode === 'rendered' ? 'default' : 'outline'}
|
|
|
|
|
|
|
+ variant="outline"
|
|
|
size="sm"
|
|
size="sm"
|
|
|
onClick={() => setPreviewMode('rendered')}
|
|
onClick={() => setPreviewMode('rendered')}
|
|
|
- className={previewMode === 'rendered' ? 'bg-cyan-500 hover:bg-cyan-600 text-white' : 'border-slate-600 text-slate-200 hover:text-white hover:bg-slate-700'}
|
|
|
|
|
|
|
+ className={previewMode === 'rendered'
|
|
|
|
|
+ ? 'bg-cyan-500 hover:bg-cyan-600 text-white border-cyan-500'
|
|
|
|
|
+ : 'bg-slate-700 border-slate-600 text-slate-200 hover:text-white hover:bg-slate-600'}
|
|
|
>
|
|
>
|
|
|
<Eye className="w-3 h-3 mr-1" />
|
|
<Eye className="w-3 h-3 mr-1" />
|
|
|
- Rendered
|
|
|
|
|
|
|
+ {t('manageStoreData.website.contentViewer.rendered', 'Rendered')}
|
|
|
</Button>
|
|
</Button>
|
|
|
<Button
|
|
<Button
|
|
|
- variant={previewMode === 'raw' ? 'default' : 'outline'}
|
|
|
|
|
|
|
+ variant="outline"
|
|
|
size="sm"
|
|
size="sm"
|
|
|
onClick={() => setPreviewMode('raw')}
|
|
onClick={() => setPreviewMode('raw')}
|
|
|
- className={previewMode === 'raw' ? 'bg-cyan-500 hover:bg-cyan-600 text-white' : 'border-slate-600 text-slate-200 hover:text-white hover:bg-slate-700'}
|
|
|
|
|
|
|
+ className={previewMode === 'raw'
|
|
|
|
|
+ ? 'bg-cyan-500 hover:bg-cyan-600 text-white border-cyan-500'
|
|
|
|
|
+ : 'bg-slate-700 border-slate-600 text-slate-200 hover:text-white hover:bg-slate-600'}
|
|
|
>
|
|
>
|
|
|
<Code className="w-3 h-3 mr-1" />
|
|
<Code className="w-3 h-3 mr-1" />
|
|
|
- Raw
|
|
|
|
|
|
|
+ {t('manageStoreData.website.contentViewer.raw', 'Raw')}
|
|
|
</Button>
|
|
</Button>
|
|
|
</div>
|
|
</div>
|
|
|
</DialogTitle>
|
|
</DialogTitle>
|
|
@@ -1750,20 +1739,20 @@ export function ManageStoreDataContent({ defaultTab }: ManageStoreDataContentPro
|
|
|
className="text-cyan-400 hover:text-cyan-300 underline text-xs flex items-center gap-1"
|
|
className="text-cyan-400 hover:text-cyan-300 underline text-xs flex items-center gap-1"
|
|
|
>
|
|
>
|
|
|
<Globe className="w-3 h-3" />
|
|
<Globe className="w-3 h-3" />
|
|
|
- Open source URL
|
|
|
|
|
|
|
+ {t('manageStoreData.website.contentViewer.openSourceUrl', 'Open source URL')}
|
|
|
<ExternalLink className="w-3 h-3" />
|
|
<ExternalLink className="w-3 h-3" />
|
|
|
</a>
|
|
</a>
|
|
|
</DialogDescription>
|
|
</DialogDescription>
|
|
|
</DialogHeader>
|
|
</DialogHeader>
|
|
|
- <div className="flex-1 overflow-y-auto mt-4">
|
|
|
|
|
|
|
+ <div className="flex-1 overflow-y-auto mt-4 bg-slate-900 rounded-lg p-4">
|
|
|
{previewMode === 'rendered' ? (
|
|
{previewMode === 'rendered' ? (
|
|
|
- <div className="prose prose-invert prose-cyan max-w-none prose-headings:text-white prose-p:text-slate-200 prose-li:text-slate-200 prose-strong:text-white prose-a:text-cyan-400">
|
|
|
|
|
|
|
+ <div className="prose prose-invert max-w-none text-slate-200 prose-headings:text-white prose-p:text-slate-200 prose-li:text-slate-200 prose-strong:text-white prose-a:text-cyan-400 prose-code:text-cyan-300 prose-blockquote:text-slate-300 prose-blockquote:border-slate-600">
|
|
|
<ReactMarkdown>
|
|
<ReactMarkdown>
|
|
|
{previewContent?.content || ''}
|
|
{previewContent?.content || ''}
|
|
|
</ReactMarkdown>
|
|
</ReactMarkdown>
|
|
|
</div>
|
|
</div>
|
|
|
) : (
|
|
) : (
|
|
|
- <pre className="bg-slate-900 p-4 rounded-lg text-slate-300 text-sm overflow-x-auto whitespace-pre-wrap">
|
|
|
|
|
|
|
+ <pre className="text-slate-300 text-sm overflow-x-auto whitespace-pre-wrap">
|
|
|
{previewContent?.content || ''}
|
|
{previewContent?.content || ''}
|
|
|
</pre>
|
|
</pre>
|
|
|
)}
|
|
)}
|
|
@@ -1771,7 +1760,7 @@ export function ManageStoreDataContent({ defaultTab }: ManageStoreDataContentPro
|
|
|
{previewContent?.scraped_at && (
|
|
{previewContent?.scraped_at && (
|
|
|
<div className="mt-4 pt-4 border-t border-slate-700 text-slate-400 text-xs">
|
|
<div className="mt-4 pt-4 border-t border-slate-700 text-slate-400 text-xs">
|
|
|
<Clock className="w-3 h-3 inline mr-1" />
|
|
<Clock className="w-3 h-3 inline mr-1" />
|
|
|
- Scraped: {new Date(previewContent.scraped_at).toLocaleString()}
|
|
|
|
|
|
|
+ {t('manageStoreData.website.contentViewer.scraped', 'Scraped')}: {new Date(previewContent.scraped_at).toLocaleString()}
|
|
|
</div>
|
|
</div>
|
|
|
)}
|
|
)}
|
|
|
</DialogContent>
|
|
</DialogContent>
|