Initial commit - WhyRating Engine (Google Reviews Scraper)
This commit is contained in:
158
web/hooks/useReviewIQAnalytics.ts
Normal file
158
web/hooks/useReviewIQAnalytics.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import type {
|
||||
ReviewIQAnalyticsResponse,
|
||||
ReviewIQFilters,
|
||||
} from '@/components/reviewiq/types';
|
||||
|
||||
interface UseReviewIQAnalyticsOptions {
|
||||
jobId?: string | null;
|
||||
businessId?: string | null;
|
||||
filters: ReviewIQFilters;
|
||||
issuesPage?: number;
|
||||
issuesPageSize?: number;
|
||||
spansPage?: number;
|
||||
spansPageSize?: number;
|
||||
}
|
||||
|
||||
interface UseReviewIQAnalyticsResult {
|
||||
data: ReviewIQAnalyticsResponse | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
refetch: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom hook for fetching ReviewIQ analytics data.
|
||||
* Uses a single API call to fetch all dashboard data.
|
||||
*/
|
||||
export function useReviewIQAnalytics({
|
||||
jobId,
|
||||
businessId,
|
||||
filters,
|
||||
issuesPage = 1,
|
||||
issuesPageSize = 10,
|
||||
spansPage = 1,
|
||||
spansPageSize = 10,
|
||||
}: UseReviewIQAnalyticsOptions): UseReviewIQAnalyticsResult {
|
||||
const [data, setData] = useState<ReviewIQAnalyticsResponse | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [refetchTrigger, setRefetchTrigger] = useState(0);
|
||||
|
||||
// Build query params from filters
|
||||
const queryParams = useMemo(() => {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (jobId) {
|
||||
params.set('job_id', jobId);
|
||||
}
|
||||
if (businessId) {
|
||||
params.set('business_id', businessId);
|
||||
}
|
||||
|
||||
params.set('time_range', filters.timeRange);
|
||||
|
||||
if (filters.sentiment.length > 0) {
|
||||
params.set('sentiment', filters.sentiment.join(','));
|
||||
}
|
||||
|
||||
if (filters.urtDomain) {
|
||||
params.set('urt_domain', filters.urtDomain);
|
||||
}
|
||||
|
||||
if (filters.intensity.length > 0) {
|
||||
params.set('intensity', filters.intensity.join(','));
|
||||
}
|
||||
|
||||
params.set('issues_page', issuesPage.toString());
|
||||
params.set('issues_page_size', issuesPageSize.toString());
|
||||
params.set('spans_page', spansPage.toString());
|
||||
params.set('spans_page_size', spansPageSize.toString());
|
||||
|
||||
return params.toString();
|
||||
}, [jobId, businessId, filters, issuesPage, issuesPageSize, spansPage, spansPageSize]);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
if (!jobId && !businessId) {
|
||||
setLoading(false);
|
||||
setData(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/pipelines/reviewiq/analytics?${queryParams}`);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.detail || `HTTP error ${response.status}`);
|
||||
}
|
||||
|
||||
const responseData: ReviewIQAnalyticsResponse = await response.json();
|
||||
setData(responseData);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch ReviewIQ analytics:', err);
|
||||
setError(err instanceof Error ? err.message : 'Failed to fetch analytics');
|
||||
setData(null);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [queryParams, jobId, businessId]);
|
||||
|
||||
// Fetch data when params change
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData, refetchTrigger]);
|
||||
|
||||
const refetch = useCallback(() => {
|
||||
setRefetchTrigger((prev) => prev + 1);
|
||||
}, []);
|
||||
|
||||
return { data, loading, error, refetch };
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for fetching spans related to a specific issue.
|
||||
*/
|
||||
export function useIssueSpans(issueId: string | null) {
|
||||
const [data, setData] = useState<ReviewIQAnalyticsResponse['spans']['items']>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!issueId) {
|
||||
setData([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchSpans = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/pipelines/reviewiq/issues/${issueId}/spans`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error ${response.status}`);
|
||||
}
|
||||
|
||||
const spans = await response.json();
|
||||
setData(spans);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch issue spans:', err);
|
||||
setError(err instanceof Error ? err.message : 'Failed to fetch spans');
|
||||
setData([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchSpans();
|
||||
}, [issueId]);
|
||||
|
||||
return { data, loading, error };
|
||||
}
|
||||
111
web/hooks/useTranslation.ts
Normal file
111
web/hooks/useTranslation.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useCallback, useRef } from 'react';
|
||||
|
||||
interface TranslationState {
|
||||
translated: string;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
isTranslated: boolean;
|
||||
}
|
||||
|
||||
interface TranslationsMap {
|
||||
[key: string]: TranslationState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for translating text on demand using MyMemory free API
|
||||
* - 1000 translations/day free (anonymous)
|
||||
* - 10000/day with free email registration
|
||||
* - No API key required for basic usage
|
||||
*/
|
||||
export function useTranslation(targetLang: string = 'en') {
|
||||
const [translations, setTranslations] = useState<TranslationsMap>({});
|
||||
const translationsRef = useRef<TranslationsMap>({});
|
||||
|
||||
// Keep ref in sync with state
|
||||
translationsRef.current = translations;
|
||||
|
||||
const translate = useCallback(async (text: string, id: string, sourceLang: string = 'auto') => {
|
||||
const current = translationsRef.current[id];
|
||||
|
||||
// If already translated, toggle back to original
|
||||
if (current?.isTranslated) {
|
||||
setTranslations(prev => ({
|
||||
...prev,
|
||||
[id]: { ...prev[id], isTranslated: false }
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
// If we have a cached translation, just show it
|
||||
if (current?.translated) {
|
||||
setTranslations(prev => ({
|
||||
...prev,
|
||||
[id]: { ...prev[id], isTranslated: true }
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
// Start loading
|
||||
setTranslations(prev => ({
|
||||
...prev,
|
||||
[id]: { translated: '', isLoading: true, error: null, isTranslated: false }
|
||||
}));
|
||||
|
||||
try {
|
||||
// Use MyMemory free translation API
|
||||
// Docs: https://mymemory.translated.net/doc/spec.php
|
||||
// Note: 500 character limit per query
|
||||
const truncatedText = text.length > 500 ? text.substring(0, 497) + '...' : text;
|
||||
const langPair = sourceLang === 'auto' ? `autodetect|${targetLang}` : `${sourceLang}|${targetLang}`;
|
||||
const url = `https://api.mymemory.translated.net/get?q=${encodeURIComponent(truncatedText)}&langpair=${langPair}`;
|
||||
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Translation failed');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Handle "same language" error - text is already in target language
|
||||
if (data.responseStatus !== 200) {
|
||||
const errorMsg = data.responseDetails || '';
|
||||
if (errorMsg.includes('DISTINCT LANGUAGES') || errorMsg.includes('same language')) {
|
||||
// Text is already in target language, just show original
|
||||
setTranslations(prev => ({
|
||||
...prev,
|
||||
[id]: { translated: text, isLoading: false, error: null, isTranslated: true }
|
||||
}));
|
||||
return;
|
||||
}
|
||||
throw new Error(errorMsg || 'Translation failed');
|
||||
}
|
||||
|
||||
const translatedText = data.responseData.translatedText;
|
||||
|
||||
setTranslations(prev => ({
|
||||
...prev,
|
||||
[id]: { translated: translatedText, isLoading: false, error: null, isTranslated: true }
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('[Translation] Error:', error);
|
||||
setTranslations(prev => ({
|
||||
...prev,
|
||||
[id]: {
|
||||
translated: '',
|
||||
isLoading: false,
|
||||
error: error instanceof Error ? error.message : 'Translation failed',
|
||||
isTranslated: false
|
||||
}
|
||||
}));
|
||||
}
|
||||
}, [targetLang]);
|
||||
|
||||
const getState = useCallback((id: string): TranslationState => {
|
||||
return translations[id] || { translated: '', isLoading: false, error: null, isTranslated: false };
|
||||
}, [translations]);
|
||||
|
||||
return { translate, getState };
|
||||
}
|
||||
Reference in New Issue
Block a user