chore: expurgo de blobs excedentes e realinhamento de estado
release.yml (push) Invalid workflow file

This commit is contained in:
Evandro Fonseca Junior committed 2026-02-22 14:06:01 -03:00
1 parent 593efd42e0
commit a7cbd1a3b4
114 files changed
+20716 -4258

No files matched your search

+16
View File
@@ -49,6 +49,14 @@ export function useAppUpdater(): AppUpdaterState {
// Verificar atualizações
const checkUpdate = useCallback(async () => {
<<<<<<< HEAD
// Ignorar verificação de updates em ambiente de desenvolvimento
if (process.env.NODE_ENV === 'development') {
return;
}
=======
>>>>>>> 593efd42e091a845dea82ee6646e027bce1e18c5
setIsChecking(true);
setError(null);
@@ -104,6 +112,14 @@ export function useAppUpdater(): AppUpdaterState {
// Verificação automática periódica (a cada 6 horas)
useEffect(() => {
<<<<<<< HEAD
// Ignorar verificação automática em desenvolvimento
if (process.env.NODE_ENV === 'development') {
return;
}
=======
>>>>>>> 593efd42e091a845dea82ee6646e027bce1e18c5
if (!autoCheckEnabled) return;
// Verificar imediatamente ao montar (com delay para não interferir no carregamento)
+21 -1
View File
@@ -15,7 +15,8 @@ export function useAutoLabelingModel() {
const checkAndDownload = async () => {
try {
const installed = await invoke<boolean>('check_if_model_installed', { name: TARGET_MODEL });
// O comando Tauri é check_if_model_installed_command, mas o Tauri remove o sufixo _command
const installed = await invoke<boolean>('check_if_model_installed_command', { name: TARGET_MODEL });
if (installed) {
setIsReady(true);
return;
@@ -49,6 +50,25 @@ export function useAutoLabelingModel() {
setIsDownloading(false);
setIsReady(true);
} catch (error) {
// Detectar erros de conexão/rede e tratá-los silenciosamente
const errorMessage = error instanceof Error ? error.message : String(error);
const isConnectionError =
errorMessage.toLowerCase().includes('connection') ||
errorMessage.toLowerCase().includes('network') ||
errorMessage.toLowerCase().includes('econnrefused') ||
errorMessage.toLowerCase().includes('timeout') ||
errorMessage.toLowerCase().includes('failed to connect') ||
errorMessage.toLowerCase().includes('connection refused');
if (isConnectionError) {
// Erro de conexão: Ollama está offline - tratar silenciosamente
// Não logar console.error para não poluir o console
setIsDownloading(false);
setIsReady(false);
return;
}
// Outros erros: manter log atual para debug
console.error("Failed to setup auto-labeling model:", error);
setIsDownloading(false);
}
+9 -16
View File
@@ -37,24 +37,17 @@ export function useBatchVerify() {
const callLLM = async (model: string, prompt: string, json = false) => {
try {
const response = await fetch('http://localhost:11434/api/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model,
prompt,
stream: false,
const { invoke } = await import('@tauri-apps/api/core');
const response = await invoke<string>('generate_completion', {
model,
prompt,
options: {
temperature: 0.3,
num_predict: 1024,
format: json ? 'json' : undefined,
options: {
temperature: 0.3,
num_predict: 1024,
},
}),
},
});
if (!response.ok) throw new Error('Failed to call LLM');
const data = await response.json();
return data.response;
return response;
} catch (e) {
console.error('LLM Call Error:', e);
throw e;
+16 -43
View File
@@ -254,29 +254,15 @@ export function useChatStorage() {
const generateTitleFromUserMessage = async (userMessage: string): Promise<string> => {
// Generate title immediately from user's first message
try {
const response = await fetch('http://localhost:11434/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: TITLE_MODEL,
messages: [
{ role: 'system', content: 'Generate a short title (3-5 words) for a chat conversation based on this user message. Output ONLY the title, no explanation.' },
{ role: 'user', content: userMessage }
],
stream: false,
}),
const { invoke } = await import('@tauri-apps/api/core');
let title = await invoke<string>('generate_chat_completion', {
model: TITLE_MODEL,
messages: [
{ role: 'system', content: 'Generate a short title (3-5 words) for a chat conversation based on this user message. Output ONLY the title, no explanation.' },
{ role: 'user', content: userMessage }
],
options: undefined,
});
if (!response.ok) {
// If model not available, use fallback
if (response.status === 404 || response.status >= 500) {
throw new Error(`Model ${TITLE_MODEL} not available (status: ${response.status})`);
}
throw new Error(`Title generation failed with status: ${response.status}`);
}
const data = await response.json();
let title = data.message?.content?.trim() || "";
// If empty from start, use fallback
if (!title || title.length === 0) {
@@ -320,30 +306,17 @@ export function useChatStorage() {
content: removeMetadataNoise(m.content),
}));
const response = await fetch('http://localhost:11434/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: TITLE_MODEL,
messages: [
{ role: 'system', content: 'Summarize the following conversation in 3 to 5 words representing a short title. Output ONLY the title, sem metadados.' },
const { invoke } = await import('@tauri-apps/api/core');
const rawTitle = await invoke<string>('generate_chat_completion', {
model: TITLE_MODEL,
messages: [
{ role: 'system', content: 'Summarize the following conversation in 3 to 5 words representing a short title. Output ONLY the title, sem metadados.' },
...sanitizedMessages
],
stream: false,
}),
],
options: undefined,
});
if (!response.ok) {
// If model not found or unavailable, throw to trigger fallback
if (response.status === 404 || response.status >= 500) {
throw new Error(`Model ${TITLE_MODEL} not available (status: ${response.status})`);
}
// For other errors, use fallback
throw new Error(`Title generation failed with status: ${response.status}`);
}
const data = await response.json();
let title = removeMetadataNoise(data.message?.content?.trim() || "");
let title = removeMetadataNoise(rawTitle?.trim() || "");
// If empty from start, throw to trigger fallback
if (!title || title.length === 0) {
+16 -3
View File
@@ -26,6 +26,8 @@ export interface ThinkingMessageMetadata {
error?: string;
timestamp: number;
duration?: number;
activeQueries?: Array<{ query: string; source: string; round?: number; startedAt: number }>;
activeUrls?: Array<{ url: string; title?: string; status: string; source?: string; startedAt: number; duration?: number }>;
}
export interface Message {
@@ -528,10 +530,21 @@ export function useChat() {
}
};
const stop = () => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
const stop = async () => {
try {
// Cancelar processamento de scraping (prioridade alta)
try {
await invoke('cancel_scraping');
} catch (e) {
console.error('Erro ao cancelar scraping:', e);
}
if (currentSessionIdRef.current) {
await invoke('stop_processing', { sessionId: currentSessionIdRef.current });
}
} finally {
setIsLoading(false);
abortControllerRef.current = null;
}
};
+95 -70
View File
@@ -23,7 +23,7 @@ export interface DeepResearchLog {
timestamp: number;
input: string;
rawOutput?: string;
parsedOutput?: any;
parsedOutput?: unknown;
error?: string;
}
@@ -47,7 +47,7 @@ function logStep(message: string) {
log.info(message);
}
function logData(label: string, data: any) {
function logData(label: string, data: unknown) {
if (typeof data === 'string' && data.length > 500) {
log.info(`${label}: ${data.substring(0, 500)}... [truncated, ${data.length} chars total]`);
} else if (typeof data === 'object') {
@@ -57,7 +57,7 @@ function logData(label: string, data: any) {
}
}
function logError(message: string, error?: any) {
function logError(message: string, error?: unknown) {
log.error(`❌ ERROR: ${message}${error ? ` - ${error}` : ''}`);
}
@@ -75,17 +75,17 @@ function extractQueriesFromResponse(raw: string): string[] {
logData('Raw response', raw);
// 1. Strip markdown code blocks (```json ... ``` or ``` ... ```)
let cleaned = raw
const cleaned = raw
.replace(/```json\s*/gi, '')
.replace(/```\s*/g, '')
.trim();
// 2. Try to parse as JSON
let parsed: any;
let parsed: unknown;
try {
parsed = JSON.parse(cleaned);
logStep('Direct JSON parse successful');
} catch (e) {
} catch {
logStep('Direct JSON parse failed, trying to extract JSON from string...');
// If JSON parse fails, try to find JSON-like content in the string
const jsonMatch = cleaned.match(/\[[\s\S]*\]|\{[\s\S]*\}/);
@@ -93,7 +93,7 @@ function extractQueriesFromResponse(raw: string): string[] {
try {
parsed = JSON.parse(jsonMatch[0]);
logStep('Extracted JSON from string successfully');
} catch (e2) {
} catch {
logError('Could not extract JSON from response');
throw new Error('Could not extract JSON from response');
}
@@ -105,7 +105,7 @@ function extractQueriesFromResponse(raw: string): string[] {
// 3. Handle different formats
if (Array.isArray(parsed)) {
const result = parsed.filter(item => typeof item === 'string' && item.trim().length > 0);
const result = (parsed as unknown[]).filter(item => typeof item === 'string' && (item as string).trim().length > 0) as string[];
logSuccess(`Parsed as plain array: ${result.length} queries`);
return result;
}
@@ -113,17 +113,19 @@ function extractQueriesFromResponse(raw: string): string[] {
// 4. Check common object keys used by different models
const commonKeys = ['queries', 'query', 'plan', 'searches', 'search_queries', 'questions'];
for (const key of commonKeys) {
if (parsed[key] && Array.isArray(parsed[key])) {
const result = parsed[key].filter((item: any) => typeof item === 'string' && item.trim().length > 0);
const obj = parsed as Record<string, unknown>;
const val = obj[key];
if (Array.isArray(val)) {
const result = (val as unknown[]).filter((item: unknown) => typeof item === 'string' && (item as string).trim().length > 0) as string[];
logSuccess(`Found queries under key "${key}": ${result.length} queries`);
return result;
}
}
// 5. Fallback: find first array value in the object
for (const val of Object.values(parsed)) {
if (Array.isArray(val) && val.length > 0 && typeof val[0] === 'string') {
const result = (val as string[]).filter((item: any) => typeof item === 'string' && item.trim().length > 0);
for (const val of Object.values(parsed as Record<string, unknown>)) {
if (Array.isArray(val) && val.length > 0 && typeof (val as unknown[])[0] === 'string') {
const result = (val as unknown[]).filter((item: unknown) => typeof item === 'string' && (item as string).trim().length > 0) as string[];
logSuccess(`Found queries in object value: ${result.length} queries`);
return result;
}
@@ -151,29 +153,19 @@ export function useDeepResearch() {
logData('Prompt length', `${prompt.length} chars`);
try {
const response = await fetch('http://localhost:11434/api/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model,
prompt,
stream: false,
const { invoke } = await import('@tauri-apps/api/core');
const response = await invoke<string>('generate_completion', {
model,
prompt,
options: {
temperature: 0.2,
num_predict: 2048,
format: json ? 'json' : undefined,
options: {
temperature: 0.2,
num_predict: 2048,
},
}),
},
});
if (!response.ok) {
logError(`LLM API returned status ${response.status}`);
throw new Error('Failed to call LLM');
}
const data = await response.json();
logSuccess(`LLM response received (${data.response?.length || 0} chars)`);
return data.response;
logSuccess(`LLM response received (${response?.length || 0} chars)`);
return response;
} catch (e) {
const errorMsg = e instanceof Error ? e.message : String(e);
logError('LLM Call Error', errorMsg);
@@ -255,40 +247,41 @@ export function useDeepResearch() {
plan = [query];
}
// Safety Check
logStep('--- Safety Check ---');
const keywords = query.toLowerCase().split(/\s+/).filter(w => w.length > 3);
logData('Keywords from query', keywords);
const isRelevant = plan.some(q =>
keywords.some(k => q.toLowerCase().includes(k))
);
logData('Plan is relevant', isRelevant);
if (!isRelevant && keywords.length > 0) {
logError('Safety Check Failed: Plan is irrelevant to query');
logData('Generated plan', plan);
logStep('Falling back to original query');
addLog({
stage: 'decomposition',
timestamp: Date.now(),
input: prompt,
rawOutput: response,
parsedOutput: plan,
error: 'Safety Check Failed: Plan irrelevant to query'
});
plan = [query];
const isSmallModel = /(?:270m|0\.5b|1b|1\.7b|2b|mini|small|abliterated|uncensored|qwen3|llama2)/i.test(model);
if (isSmallModel) {
logStep('Safety Check bypassed for small/experimental model');
} else {
logSuccess(`Decomposition complete: ${plan.length} search queries generated`);
addLog({
stage: 'decomposition',
timestamp: Date.now(),
input: prompt,
rawOutput: response,
parsedOutput: plan,
error: parseError
});
const isRelevant = plan.some(q =>
keywords.some(k => q.toLowerCase().includes(k))
);
logData('Plan is relevant', isRelevant);
if (!isRelevant && keywords.length > 0) {
logError('Safety Check Failed: Plan is irrelevant to query');
logData('Generated plan', plan);
logStep('Falling back to original query');
addLog({
stage: 'decomposition',
timestamp: Date.now(),
input: prompt,
rawOutput: response,
parsedOutput: plan,
error: 'Safety Check Failed: Plan irrelevant to query'
});
plan = [query];
}
}
addLog({
stage: 'decomposition',
timestamp: Date.now(),
input: prompt,
rawOutput: response,
parsedOutput: plan,
error: parseError
});
logSuccess(`Decomposition complete: ${plan.length} search queries generated`);
setState(s => ({ ...s, plan }));
return plan;
@@ -303,31 +296,63 @@ export function useDeepResearch() {
// --- ADD TO KNOWLEDGE BASE ---
const addToKnowledgeBase = useCallback((query: string, sources: ScrapedContent[]) => {
logStep(`--- Adding to Knowledge Base ---`);
logSection('ADDING TO KNOWLEDGE BASE');
logData('Query', query);
logData('Sources count', sources.length);
logData('Timestamp', new Date().toISOString());
const entries: ResearchEntry[] = sources.map((src, idx) => ({
id: `${query.substring(0, 10)}-${idx}-${Date.now()}`,
query,
sourceUrl: src.url,
title: src.title,
content: src.markdown,
content: src.markdown || src.content || '',
timestamp: Date.now(),
}));
// Log detalhado de cada fonte
sources.forEach((src, idx) => {
logStep(` → Source ${idx + 1}: ${src.title} (${src.url})`);
const contentLength = src.markdown?.length || 0;
logData(` Content length`, `${contentLength} chars`);
logStep(`\n--- Source ${idx + 1}/${sources.length} ---`);
logData('Title', src.title);
logData('URL', src.url);
const contentLength = src.markdown?.length || src.content?.length || 0;
const snippetLength = src.snippet?.length || 0;
logData('Content length (markdown)', `${contentLength} chars`);
logData('Snippet length', `${snippetLength} chars`);
logData('Cached', src.cached ? 'Yes' : 'No');
if (src.markdown && contentLength > 0) {
const preview = src.markdown.substring(0, 300);
logData(` Preview`, preview + (contentLength > 300 ? '...' : ''));
// Log preview mais detalhado
const preview = src.markdown.substring(0, 500);
logData('Content preview', preview + (contentLength > 500 ? '...' : ''));
// Estatísticas do conteúdo
const wordCount = src.markdown.split(/\s+/).length;
const paragraphCount = src.markdown.split(/\n\n/).length;
logData('Word count', wordCount);
logData('Paragraph count', paragraphCount);
} else if (src.content && src.content.length > 0) {
const preview = src.content.substring(0, 500);
logData('Content preview (raw)', preview + (src.content.length > 500 ? '...' : ''));
} else {
logStep(` ⚠️ No content extracted`);
logError('No content extracted from this source');
}
// Log snippet se disponível
if (src.snippet) {
logData('Snippet', src.snippet.substring(0, 200) + (snippetLength > 200 ? '...' : ''));
}
});
// Estatísticas agregadas
const totalContentLength = entries.reduce((sum, e) => sum + e.content.length, 0);
const avgContentLength = entries.length > 0 ? totalContentLength / entries.length : 0;
logStep('\n--- Aggregated Statistics ---');
logData('Total entries', entries.length);
logData('Total content length', `${totalContentLength} chars`);
logData('Average content length', `${Math.round(avgContentLength)} chars`);
setState(s => ({
...s,
+102
View File
@@ -0,0 +1,102 @@
import { useState, useMemo } from 'react';
import { useLocalModels, LocalModel } from '@/hooks/use-local-models';
import { MODEL_REGISTRY, ModelDefinition } from '@/lib/model-registry';
const STORAGE_KEY = 'last_used_model';
export interface UseModelSelectionResult {
myModels: ModelDefinition[];
availableModels: ModelDefinition[];
selectedModel: string | null;
setSelectedModel: (model: string) => void;
isLoading: boolean;
installedVariantsByFamily: Record<string, string[]>;
refreshModels: () => Promise<void>;
}
export function useModelSelection(): UseModelSelectionResult {
const { models: installedModels, loading: isLoadingModels, refresh: refreshLocalModels } = useLocalModels();
const [selectedModel, setSelectedModelState] = useState<string | null>(null);
const { myModels, availableModels } = useMemo(() => {
const installedNames = new Set(installedModels.map(m => m.name));
const my: ModelDefinition[] = [];
const available: ModelDefinition[] = [];
const registryByModel = new Map<string, ModelDefinition>();
for (const def of MODEL_REGISTRY) {
registryByModel.set(def.model, def);
}
for (const def of MODEL_REGISTRY) {
const isInstalled = def.variants.some(v => installedNames.has(v.name));
if (isInstalled) {
my.push(def);
} else {
available.push(def);
}
}
const installedByBase = new Map<string, LocalModel[]>();
for (const m of installedModels) {
const base = m.name.split(':')[0];
const list = installedByBase.get(base) || [];
list.push(m);
installedByBase.set(base, list);
}
for (const [base, models] of installedByBase.entries()) {
if (!registryByModel.has(base)) {
const variants = models.map((m) => ({
name: m.name,
size: m.size,
context: 'Unknown',
inputType: 'Text',
}));
my.push({
model: base,
url: '',
description: 'Modelo instalado localmente',
variantsCount: variants.length,
variants,
});
}
}
return { myModels: my, availableModels: available };
}, [installedModels]);
const installedVariantsByFamily = useMemo(() => {
const map: Record<string, string[]> = {};
for (const m of installedModels) {
const base = m.name.split(':')[0];
if (!map[base]) map[base] = [];
map[base].push(m.name);
}
return map;
}, [installedModels]);
const defaultSelectedModel = useMemo(() => {
if (isLoadingModels) return null;
const lastUsed = typeof window !== 'undefined' ? localStorage.getItem(STORAGE_KEY) : null;
const installedNames = installedModels.map(m => m.name);
if (lastUsed && installedNames.includes(lastUsed)) return lastUsed;
if (installedModels.length > 0) return installedModels[0].name;
return null;
}, [installedModels, isLoadingModels]);
// Persistence wrapper
const setSelectedModel = (model: string) => {
setSelectedModelState(model);
localStorage.setItem(STORAGE_KEY, model);
};
return {
myModels,
availableModels,
selectedModel: selectedModel ?? defaultSelectedModel,
setSelectedModel,
isLoading: isLoadingModels,
installedVariantsByFamily,
refreshModels: refreshLocalModels,
};
}
+190 -16
View File
@@ -1,50 +1,224 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, useRef, useCallback } from 'react';
import { invoke } from '@tauri-apps/api/core';
export type OllamaStatus = 'checking' | 'not_installed' | 'installed_stopped' | 'running';
export function useOllamaCheck() {
const [status, setStatus] = useState<OllamaStatus>('checking');
interface UseOllamaCheckReturn {
status: OllamaStatus;
isChecking: boolean;
check: () => Promise<void>;
startPolling: () => void;
stopPolling: () => void;
reset: () => void;
elapsedTime: number;
}
const check = async () => {
setStatus('checking');
const INITIAL_INTERVAL = 2000; // 2 segundos
const MAX_INTERVAL = 30000; // 30 segundos
const BACKOFF_MULTIPLIER = 1.5;
const TOTAL_TIMEOUT = 300000; // 5 minutos
export function useOllamaCheck(): UseOllamaCheckReturn {
const [status, setStatus] = useState<OllamaStatus>('not_installed');
const [isChecking, setIsChecking] = useState(false);
const [elapsedTime, setElapsedTime] = useState(0);
const intervalRef = useRef<NodeJS.Timeout | null>(null);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
const startTimeRef = useRef<number | null>(null);
const currentIntervalRef = useRef<number>(INITIAL_INTERVAL);
const attemptCountRef = useRef<number>(0);
const isPollingActiveRef = useRef<boolean>(false);
const elapsedTimerRef = useRef<NodeJS.Timeout | null>(null);
const performCheck = useCallback(async (): Promise<boolean> => {
try {
// Usar verificação completa que checa instalação E execução
const result = await invoke<{
installed: boolean;
running: boolean;
status: string;
}>('check_ollama_full');
setStatus(result.status as OllamaStatus);
const newStatus = result.status as OllamaStatus;
setStatus(newStatus);
// Se Ollama está rodando, polling deve parar
if (newStatus === 'running' && result.installed && result.running) {
return true; // Sucesso, parar polling
}
return false; // Continua polling
} catch (error) {
console.error('Failed to check ollama:', error);
// Se falhar, tentar verificação individual como fallback
// Se falhar, tentar verificação robusta como fallback
try {
const isInstalledAndRunning = await invoke<boolean>('verify_ollama_integrity_command');
if (isInstalledAndRunning) {
setStatus('running');
return true; // Sucesso, parar polling
}
// Se verify_ollama_integrity retornou false, pode ser instalado mas não rodando
// ou não instalado. Verificar separadamente para determinar status exato
const installed = await invoke<boolean>('check_ollama_installed');
if (!installed) {
setStatus('not_installed');
return;
return false; // Continua polling (não instalado ainda)
}
const running = await invoke<boolean>('check_ollama_running');
if (!running) {
setStatus('installed_stopped');
return;
return false; // Continua polling (instalado mas não rodando)
}
setStatus('running');
return true; // Sucesso, parar polling
} catch (fallbackError) {
console.error('Fallback check also failed:', fallbackError);
setStatus('not_installed');
return false; // Continua polling
}
}
};
useEffect(() => {
check();
}, []);
return { status, check };
}
const startPolling = useCallback(() => {
if (isPollingActiveRef.current) {
return; // Já está rodando
}
isPollingActiveRef.current = true;
setIsChecking(true);
startTimeRef.current = Date.now();
attemptCountRef.current = 0;
currentIntervalRef.current = INITIAL_INTERVAL;
// Atualizar tempo decorrido a cada segundo
elapsedTimerRef.current = setInterval(() => {
if (startTimeRef.current) {
const elapsed = Math.floor((Date.now() - startTimeRef.current) / 1000);
setElapsedTime(elapsed);
// Verificar timeout total
if (elapsed * 1000 >= TOTAL_TIMEOUT) {
setIsChecking(false);
isPollingActiveRef.current = false;
if (elapsedTimerRef.current) {
clearInterval(elapsedTimerRef.current);
elapsedTimerRef.current = null;
}
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
return;
}
}
}, 1000);
// Primeira verificação imediata
performCheck().then((shouldStop) => {
if (shouldStop) {
stopPolling();
return;
}
// Iniciar polling com exponential backoff
const scheduleNext = () => {
if (!isPollingActiveRef.current) {
return;
}
attemptCountRef.current += 1;
// Calcular próximo intervalo com exponential backoff
const nextInterval = Math.min(
INITIAL_INTERVAL * Math.pow(BACKOFF_MULTIPLIER, attemptCountRef.current - 1),
MAX_INTERVAL
);
currentIntervalRef.current = nextInterval;
intervalRef.current = setTimeout(async () => {
if (!isPollingActiveRef.current) {
return;
}
const shouldStop = await performCheck();
if (shouldStop) {
stopPolling();
} else {
scheduleNext();
}
}, nextInterval);
};
scheduleNext();
});
// Timeout total
timeoutRef.current = setTimeout(() => {
if (isPollingActiveRef.current) {
console.warn('Ollama check polling timeout após 5 minutos');
stopPolling();
}
}, TOTAL_TIMEOUT);
}, [performCheck]);
const stopPolling = useCallback(() => {
isPollingActiveRef.current = false;
setIsChecking(false);
if (intervalRef.current) {
clearTimeout(intervalRef.current);
intervalRef.current = null;
}
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
if (elapsedTimerRef.current) {
clearInterval(elapsedTimerRef.current);
elapsedTimerRef.current = null;
}
}, []);
const check = useCallback(async () => {
// Forçar verificação manual
await performCheck();
}, [performCheck]);
const reset = useCallback(() => {
stopPolling();
currentIntervalRef.current = INITIAL_INTERVAL;
attemptCountRef.current = 0;
setElapsedTime(0);
startTimeRef.current = null;
startPolling();
}, [stopPolling, startPolling]);
// Limpar polling ao desmontar
useEffect(() => {
return () => {
stopPolling();
};
}, [stopPolling]);
// Parar polling automaticamente quando status for 'running'
useEffect(() => {
if (status === 'running' && isPollingActiveRef.current) {
stopPolling();
}
}, [status, stopPolling]);
return {
status,
isChecking,
check,
startPolling,
stopPolling,
reset,
elapsedTime,
};
}
+158 -11
View File
@@ -12,6 +12,13 @@ interface DownloadState {
isInstalled: boolean;
isChecking: boolean;
checkStatus: string;
installStatus: string | null;
isInstalling: boolean;
speed: string;
downloadedBytes: number;
totalBytes: number;
eta: number;
status: 'downloading' | 'verifying' | 'completed';
}
export function useOllamaDownload() {
@@ -24,6 +31,13 @@ export function useOllamaDownload() {
isInstalled: false,
isChecking: false,
checkStatus: '',
installStatus: null,
isInstalling: false,
speed: '0 MB/s',
downloadedBytes: 0,
totalBytes: 0,
eta: 0,
status: 'downloading',
});
const checkingIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
@@ -31,17 +45,71 @@ export function useOllamaDownload() {
// Escutar eventos de progresso do download
useEffect(() => {
const unlistenPromise = listen('installer-download-progress', (event: any) => {
const unlistenDownloadPromise = listen('installer-download-progress', (event: any) => {
const data = event.payload;
const statusText = data.status || '';
const isVerifying = statusText.includes('Verificando');
const isCompleted = data.progress === 100 && !isVerifying;
setState(prev => ({
...prev,
downloadProgress: data.progress || 0,
downloadStatus: data.status || '',
downloadStatus: statusText,
speed: data.speed_formatted || '0 MB/s',
downloadedBytes: data.downloaded || 0,
totalBytes: data.total || 0,
eta: data.eta_seconds || 0,
status: isVerifying ? 'verifying' : isCompleted ? 'completed' : 'downloading',
isDownloading: !isCompleted,
}));
});
// Escutar eventos de progresso da instalação
const unlistenInstallPromise = listen('installer-progress', (event: any) => {
const data = event.payload;
const status = data.status || 'unknown';
const message = data.message || 'Processando...';
setState(prev => ({
...prev,
isInstalling: status !== 'success' && status !== 'error' && status !== 'partial',
installStatus: message,
isInstalled: status === 'success',
downloadError: status === 'error' ? message : prev.downloadError,
}));
// Se a instalação foi bem-sucedida, iniciar verificação automática
if (status === 'success') {
// Usar setTimeout para evitar dependência circular
setTimeout(() => {
// startAutoCheck será chamado via closure do componente
// Por enquanto, apenas atualizar o estado
setState(prev => ({
...prev,
isChecking: true,
checkStatus: 'Verificando instalação...',
}));
}, 100);
}
});
// Escutar evento de Ollama pronto
const unlistenReadyPromise = listen('ollama-ready', () => {
setState(prev => ({
...prev,
isInstalling: false,
installStatus: 'Ollama instalado e rodando!',
isInstalled: true,
isChecking: false,
checkStatus: 'Ollama encontrado e rodando!',
}));
window.dispatchEvent(new CustomEvent('ollama-installed'));
});
return () => {
unlistenPromise.then(unlisten => unlisten());
unlistenDownloadPromise.then(unlisten => unlisten());
unlistenInstallPromise.then(unlisten => unlisten());
unlistenReadyPromise.then(unlisten => unlisten());
};
}, []);
@@ -55,6 +123,16 @@ export function useOllamaDownload() {
isInstalled: false,
isChecking: false,
checkStatus: '',
<<<<<<< HEAD
installStatus: null,
isInstalling: false,
speed: '0 MB/s',
downloadedBytes: 0,
totalBytes: 0,
eta: 0,
status: 'downloading',
=======
>>>>>>> 593efd42e091a845dea82ee6646e027bce1e18c5
});
try {
@@ -70,6 +148,16 @@ export function useOllamaDownload() {
isInstalled: false,
isChecking: false,
checkStatus: '',
<<<<<<< HEAD
installStatus: null,
isInstalling: false,
speed: '0 MB/s',
downloadedBytes: 0,
totalBytes: 0,
eta: 0,
status: 'completed',
=======
>>>>>>> 593efd42e091a845dea82ee6646e027bce1e18c5
});
return;
}
@@ -85,6 +173,16 @@ export function useOllamaDownload() {
isInstalled: false,
isChecking: false,
checkStatus: '',
<<<<<<< HEAD
installStatus: null,
isInstalling: false,
speed: '0 MB/s',
downloadedBytes: 0,
totalBytes: 0,
eta: 0,
status: 'completed',
=======
>>>>>>> 593efd42e091a845dea82ee6646e027bce1e18c5
});
} catch (error) {
setState({
@@ -96,6 +194,16 @@ export function useOllamaDownload() {
isInstalled: false,
isChecking: false,
checkStatus: '',
<<<<<<< HEAD
installStatus: null,
isInstalling: false,
speed: '0 MB/s',
downloadedBytes: 0,
totalBytes: 0,
eta: 0,
status: 'downloading',
=======
>>>>>>> 593efd42e091a845dea82ee6646e027bce1e18c5
});
}
};
@@ -109,6 +217,24 @@ export function useOllamaDownload() {
return;
}
<<<<<<< HEAD
// Atualizar estado para mostrar que a instalação está começando
setState(prev => ({
...prev,
isInstalling: true,
installStatus: 'Iniciando instalação silenciosa...',
downloadError: null,
}));
// Chamar Tauri em background (não bloquear)
runInstaller(state.filePath)
.then(() => {
// Após iniciar a instalação, começar a verificação automática após um delay
setTimeout(() => {
startAutoCheck();
}, 2000);
})
=======
// Optimistic UI: atualizar estado imediatamente antes da chamada Tauri
setState(prev => ({
...prev,
@@ -120,27 +246,40 @@ export function useOllamaDownload() {
// Chamar Tauri em background (não bloquear)
runInstaller(state.filePath)
>>>>>>> 593efd42e091a845dea82ee6646e027bce1e18c5
.catch((error) => {
setState(prev => ({
...prev,
downloadError: error instanceof Error ? error.message : 'Erro ao executar instalador',
<<<<<<< HEAD
isInstalling: false,
installStatus: null,
=======
isInstalled: false, // Reverter se falhar
>>>>>>> 593efd42e091a845dea82ee6646e027bce1e18c5
}));
});
};
const checkOllama = async (): Promise<boolean> => {
try {
const installed = await invoke<boolean>('check_ollama_installed');
if (!installed) {
return false;
}
const running = await invoke<boolean>('check_ollama_running');
return running;
// Usar verificação robusta multi-camada (PATH, caminhos absolutos, HTTP)
const isInstalledAndRunning = await invoke<boolean>('verify_ollama_integrity_command');
return isInstalledAndRunning;
} catch (error) {
console.error('Erro ao verificar Ollama:', error);
return false;
// Fallback para verificação simples se o comando robusto falhar
try {
const installed = await invoke<boolean>('check_ollama_installed');
if (!installed) {
return false;
}
const running = await invoke<boolean>('check_ollama_running');
return running;
} catch (fallbackError) {
console.error('Fallback check also failed:', fallbackError);
return false;
}
}
};
@@ -237,6 +376,7 @@ export function useOllamaDownload() {
filePath: existingPath,
downloadProgress: 100,
downloadStatus: 'Download já concluído',
status: 'completed',
}));
}
};
@@ -252,6 +392,13 @@ export function useOllamaDownload() {
isInstalled: false,
isChecking: false,
checkStatus: '',
installStatus: null,
isInstalling: false,
speed: '0 MB/s',
downloadedBytes: 0,
totalBytes: 0,
eta: 0,
status: 'downloading',
});
};
+8
View File
@@ -56,3 +56,11 @@ export function useOperatingSystem() {
+9 -15
View File
@@ -1,4 +1,5 @@
import { useState } from 'react';
import { invoke } from '@tauri-apps/api/core';
// Hardcoded for now to avoid FS complexity in v1, but ideally read from file
const PROMPT_GEN_SYSTEM = `Você é um Especialista em Engenharia de Prompts com raciocínio sequencial e acesso a fontes técnicas de primeira qualidade.
@@ -40,23 +41,16 @@ export function usePromptGenerator() {
const generatePrompt = async (userGoal: string, model: string): Promise<string> => {
setIsGenerating(true);
try {
const response = await fetch('http://localhost:11434/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model,
messages: [
{ role: 'system', content: PROMPT_GEN_SYSTEM },
{ role: 'user', content: userGoal }
],
stream: false, // We want the full prompt at once for this utility
}),
const response = await invoke<string>('generate_chat_completion', {
model,
messages: [
{ role: 'system', content: PROMPT_GEN_SYSTEM },
{ role: 'user', content: userGoal }
],
options: undefined,
});
if (!response.ok) throw new Error('Failed to generate prompt');
const data = await response.json();
return data.message?.content || "";
return response || "";
} catch (error) {
console.error("Prompt generation failed:", error);
throw error;
+8 -19
View File
@@ -1,4 +1,5 @@
import { useState, useCallback } from 'react';
import { invoke } from '@tauri-apps/api/core';
/**
* Hook para gerar query de busca otimizada usando a LLM
@@ -33,27 +34,15 @@ Pergunta do usuário: "${userInput}"
Query de busca (ou NO_SEARCH):`;
const response = await fetch('http://localhost:11434/api/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model,
prompt,
stream: false,
options: {
temperature: 0.3, // Baixa temperatura para respostas mais determinísticas
num_predict: 50, // Limitar tokens para resposta rápida
},
}),
const generatedText = await invoke<string>('generate_completion', {
model,
prompt,
options: {
temperature: 0.3, // Baixa temperatura para respostas mais determinísticas
num_predict: 50, // Limitar tokens para resposta rápida
},
});
if (!response.ok) {
throw new Error('Falha ao gerar query');
}
const data = await response.json();
const generatedText = (data.response || '').trim();
// Limpar resposta (remover aspas, pontos finais, etc)
let query = generatedText
.replace(/^["']|["']$/g, '') // Remover aspas
+19
View File
@@ -43,12 +43,31 @@ export function useSetupCheck() {
// Aguardar um pouco para o servidor iniciar
await new Promise(resolve => setTimeout(resolve, 2000));
<<<<<<< HEAD
// Verificar integridade completa (instalação + serviço rodando)
try {
const isInstalledAndRunning = await invoke<boolean>('verify_ollama_integrity_command');
if (!isInstalledAndRunning) {
// Ainda não está rodando ou instalação está corrompida
return 'needs_setup';
}
} catch (verifyError) {
// Fallback para verificação simples se o comando robusto falhar
console.warn('verify_ollama_integrity failed, using fallback:', verifyError);
const running = await invoke<boolean>('check_ollama_running');
if (!running) {
return 'needs_setup';
}
}
=======
// Verificar novamente se está rodando
const running = await invoke<boolean>('check_ollama_running');
if (!running) {
// Ainda não está rodando, precisa de intervenção do usuário
return 'needs_setup';
}
>>>>>>> 593efd42e091a845dea82ee6646e027bce1e18c5
// Agora está rodando, verificar modelos
const hasAnyModels = await checkModels();
return hasAnyModels ? 'ready' : 'needs_setup';
+366
View File
@@ -0,0 +1,366 @@
import { useState, useEffect, useCallback } from 'react';
import { invoke } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event';
export interface SetupWizardState {
currentStep: number;
completedSteps: number[];
ollamaInstalled: boolean;
ollamaRunning: boolean;
modelDownloaded: boolean;
modelName: string | null;
lastError: string | null;
progressMessage: string | null;
progressStatus: string | null;
}
const CHECKPOINT_KEY = 'ollahub_setup_wizard_checkpoint';
const DEFAULT_MODEL = 'llama3.2:1b';
const MAX_STEPS = 4;
const MIN_STEP = 1;
function getDefaultState(): SetupWizardState {
return {
currentStep: 1,
completedSteps: [],
ollamaInstalled: false,
ollamaRunning: false,
modelDownloaded: false,
modelName: null,
lastError: null,
progressMessage: null,
progressStatus: null,
};
}
function validateWizardState(state: unknown): SetupWizardState | null {
// Verificar se é um objeto
if (!state || typeof state !== 'object' || Array.isArray(state)) {
return null;
}
const stateObj = state as Record<string, unknown>;
// Validar currentStep
const currentStep = typeof stateObj.currentStep === 'number'
? Math.max(MIN_STEP, Math.min(MAX_STEPS, Math.round(stateObj.currentStep)))
: MIN_STEP;
// Validar completedSteps
const completedSteps = Array.isArray(stateObj.completedSteps)
? stateObj.completedSteps
.filter((step: unknown) => typeof step === 'number' && step >= MIN_STEP && step <= MAX_STEPS)
.map((step: unknown) => Math.round(step as number))
.filter((step: number, index: number, arr: number[]) => arr.indexOf(step) === index) // Remover duplicatas
: [];
// Validar tipos dos campos booleanos
const ollamaInstalled = typeof stateObj.ollamaInstalled === 'boolean' ? stateObj.ollamaInstalled : false;
const ollamaRunning = typeof stateObj.ollamaRunning === 'boolean' ? stateObj.ollamaRunning : false;
const modelDownloaded = typeof stateObj.modelDownloaded === 'boolean' ? stateObj.modelDownloaded : false;
// Validar modelName (pode ser null ou string)
const modelName = stateObj.modelName === null || typeof stateObj.modelName === 'string'
? stateObj.modelName as string | null
: null;
// Validar lastError (pode ser null ou string)
const lastError = stateObj.lastError === null || typeof stateObj.lastError === 'string'
? stateObj.lastError as string | null
: null;
// Validar progressMessage (pode ser null ou string)
const progressMessage = stateObj.progressMessage === null || typeof stateObj.progressMessage === 'string'
? stateObj.progressMessage as string | null
: null;
// Validar progressStatus (pode ser null ou string)
const progressStatus = stateObj.progressStatus === null || typeof stateObj.progressStatus === 'string'
? stateObj.progressStatus as string | null
: null;
return {
currentStep,
completedSteps,
ollamaInstalled,
ollamaRunning,
modelDownloaded,
modelName,
lastError,
progressMessage,
progressStatus,
};
}
export function useSetupWizard() {
const [state, setState] = useState<SetupWizardState>(() => {
if (typeof window !== 'undefined') {
const saved = localStorage.getItem(CHECKPOINT_KEY);
if (saved) {
try {
const parsed = JSON.parse(saved);
const validated = validateWizardState(parsed);
if (validated) {
return validated;
} else {
// Estado inválido, remover do localStorage e usar estado inicial
console.warn('Estado do wizard inválido detectado, resetando para estado inicial');
localStorage.removeItem(CHECKPOINT_KEY);
}
} catch (error) {
// Se falhar ao fazer parse, remover item corrompido
console.error('Erro ao fazer parse do estado do wizard:', error);
localStorage.removeItem(CHECKPOINT_KEY);
}
}
}
return getDefaultState();
});
useEffect(() => {
if (typeof window !== 'undefined') {
localStorage.setItem(CHECKPOINT_KEY, JSON.stringify(state));
}
}, [state]);
useEffect(() => {
const unlistenPromise = listen<{ status: string; message: string }>('setup-progress', (event) => {
const { status, message } = event.payload;
setState(prev => ({
...prev,
progressStatus: status,
progressMessage: message,
}));
});
return () => {
unlistenPromise.then(unlisten => unlisten());
};
}, []);
const checkSetupState = useCallback(async () => {
try {
const setupState = await invoke<{
ollama_installed: boolean;
ollama_running: boolean;
default_model_downloaded: boolean;
default_model_name: string | null;
}>('get_setup_state_command');
setState(prev => ({
...prev,
ollamaInstalled: setupState.ollama_installed,
ollamaRunning: setupState.ollama_running,
modelDownloaded: setupState.default_model_downloaded,
modelName: setupState.default_model_name || DEFAULT_MODEL,
}));
let initialStep = 1;
if (setupState.ollama_installed && setupState.ollama_running) {
initialStep = 2;
if (setupState.default_model_downloaded) {
initialStep = 4;
} else {
initialStep = 3;
}
}
setState(prev => ({
...prev,
currentStep: Math.max(prev.currentStep, initialStep),
}));
return setupState;
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
setState(prev => ({
...prev,
lastError: errorMsg,
}));
throw error;
}
}, []);
const nextStep = useCallback(() => {
setState(prev => {
const next = prev.currentStep + 1;
return {
...prev,
currentStep: next,
completedSteps: [...prev.completedSteps, prev.currentStep],
lastError: null,
};
});
}, []);
const previousStep = useCallback(() => {
setState(prev => ({
...prev,
currentStep: Math.max(1, prev.currentStep - 1),
lastError: null,
}));
}, []);
const goToStep = useCallback((step: number) => {
setState(prev => ({
...prev,
currentStep: Math.max(1, Math.min(4, step)),
lastError: null,
}));
}, []);
const checkChocolatey = useCallback(async (): Promise<boolean> => {
try {
return await invoke<boolean>('check_chocolatey_installed_command');
} catch (error) {
console.error('Erro ao verificar Chocolatey:', error);
return false;
}
}, []);
const installChocolatey = useCallback(async (): Promise<void> => {
try {
setState(prev => ({
...prev,
progressMessage: 'Instalando Chocolatey...',
progressStatus: 'installing_chocolatey',
lastError: null,
}));
await invoke('install_chocolatey_command');
setState(prev => ({
...prev,
progressMessage: 'Chocolatey instalado com sucesso',
progressStatus: null,
}));
} catch (error) {
const errorMsg = error instanceof Error ? error.message : 'Erro ao instalar Chocolatey';
setState(prev => ({
...prev,
lastError: errorMsg,
progressStatus: null,
}));
throw error;
}
}, []);
const installOllamaViaChoco = useCallback(async (): Promise<void> => {
try {
setState(prev => ({
...prev,
progressMessage: 'Instalando Ollama via Chocolatey...',
progressStatus: 'installing_ollama',
lastError: null,
}));
await invoke('install_ollama_choco_command');
setState(prev => ({
...prev,
progressMessage: 'Ollama instalado com sucesso',
progressStatus: null,
}));
} catch (error) {
const errorMsg = error instanceof Error ? error.message : 'Erro ao instalar Ollama via Chocolatey';
setState(prev => ({
...prev,
lastError: errorMsg,
progressStatus: null,
}));
throw error;
}
}, []);
const installOllama = useCallback(async (installerPath?: string) => {
try {
setState(prev => ({
...prev,
lastError: null,
progressMessage: 'Iniciando instalação do Ollama...',
progressStatus: 'installing_ollama',
}));
await invoke('install_ollama_silently_command', {
installerPath: installerPath || null,
});
await checkSetupState();
setState(prev => ({
...prev,
ollamaInstalled: true,
ollamaRunning: true,
progressMessage: 'Ollama instalado e rodando!',
progressStatus: 'ollama_ready',
}));
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
setState(prev => ({
...prev,
lastError: errorMsg,
progressMessage: `Erro: ${errorMsg}`,
progressStatus: 'error',
}));
throw error;
}
}, [checkSetupState]);
const downloadModel = useCallback(async (modelName: string = DEFAULT_MODEL) => {
try {
setState(prev => ({
...prev,
lastError: null,
progressMessage: `Baixando modelo ${modelName}...`,
progressStatus: 'downloading_model',
}));
await invoke('download_default_model_command', { modelName });
setState(prev => ({
...prev,
modelDownloaded: true,
modelName,
progressMessage: `Modelo ${modelName} baixado com sucesso!`,
progressStatus: 'model_ready',
}));
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
setState(prev => ({
...prev,
lastError: errorMsg,
progressMessage: `Erro ao baixar modelo: ${errorMsg}`,
progressStatus: 'error',
}));
throw error;
}
}, []);
const resetWizard = useCallback(() => {
if (typeof window !== 'undefined') {
localStorage.removeItem(CHECKPOINT_KEY);
}
setState(getDefaultState());
}, []);
const isStepComplete = useCallback((step: number) => {
return state.completedSteps.includes(step);
}, [state.completedSteps]);
return {
state,
checkSetupState,
nextStep,
previousStep,
goToStep,
checkChocolatey,
installChocolatey,
installOllamaViaChoco,
installOllama,
downloadModel,
resetWizard,
isStepComplete,
};
}
+3
View File
@@ -34,7 +34,10 @@ export function useSystemHealth() {
title: 'Modo de Economia Ativado',
description: 'Recursos limitados para manter estabilidade. Algumas funcionalidades podem estar temporariamente indisponíveis.',
variant: 'destructive',
<<<<<<< HEAD
=======
duration: 5000,
>>>>>>> 593efd42e091a845dea82ee6646e027bce1e18c5
});
setHasShownCriticalToast(true);
} else if (newHealth.status !== 'critical') {
+161
View File
@@ -0,0 +1,161 @@
import { useState, useEffect, useCallback } from 'react';
import { listen } from '@tauri-apps/api/event';
export interface ActiveQuery {
query: string;
source: string;
round?: number;
startedAt: number;
}
export interface ActiveUrl {
url: string;
title?: string;
status: 'started' | 'completed' | 'failed' | 'cached';
source?: string;
startedAt: number;
duration?: number;
}
interface SearchQueryEvent {
query: string;
source: string;
round?: number;
timestamp: number;
}
interface SearchQueryCompletedEvent {
query: string;
source: string;
results_count: number;
success: boolean;
timestamp: number;
}
interface SearchSourceSwitchEvent {
from: string;
to: string;
reason: string;
timestamp: number;
}
interface ScrapingUrlEvent {
url: string;
title?: string;
status: string;
duration_ms?: number;
source?: string;
timestamp: number;
}
export function useWebSearchEvents() {
const [activeQueries, setActiveQueries] = useState<ActiveQuery[]>([]);
const [activeUrls, setActiveUrls] = useState<ActiveUrl[]>([]);
useEffect(() => {
let unlistenQueries: (() => void) | null = null;
let unlistenCompleted: (() => void) | null = null;
let unlistenSwitched: (() => void) | null = null;
let unlistenScraping: (() => void) | null = null;
const setupListeners = async () => {
// Listener para query iniciada
unlistenQueries = await listen<SearchQueryEvent>('search-query-started', (event) => {
const { query, source, round, timestamp } = event.payload;
setActiveQueries((prev) => {
// Remover query anterior se existir (mesma query)
const filtered = prev.filter((q) => q.query !== query);
return [
...filtered,
{
query,
source,
round,
startedAt: timestamp,
},
];
});
});
// Listener para query completada
unlistenCompleted = await listen<SearchQueryCompletedEvent>('search-query-completed', (event) => {
const { query, source } = event.payload;
setActiveQueries((prev) => prev.filter((q) => !(q.query === query && q.source === source)));
});
// Listener para mudança de fonte
unlistenSwitched = await listen<SearchSourceSwitchEvent>('search-source-switched', (event) => {
const { from, to, reason } = event.payload;
console.log(`[WebSearchEvents] Source switched: ${from} → ${to} (${reason})`);
// Atualizar query ativa para nova fonte
setActiveQueries((prev) =>
prev.map((q) => (q.source === from ? { ...q, source: to } : q))
);
});
// Listener para eventos de scraping
unlistenScraping = await listen<ScrapingUrlEvent>('scraping-url-event', (event) => {
const { url, title, status, duration_ms, source, timestamp } = event.payload;
setActiveUrls((prev) => {
const existingIndex = prev.findIndex((u) => u.url === url);
const newUrl: ActiveUrl = {
url,
title,
status: status as ActiveUrl['status'],
source,
startedAt: timestamp,
duration: duration_ms,
};
if (existingIndex >= 0) {
// Atualizar URL existente
const updated = [...prev];
updated[existingIndex] = newUrl;
return updated;
} else {
// Adicionar nova URL
return [...prev, newUrl];
}
});
// Remover URLs completadas/falhadas após 5 segundos
if (status === 'completed' || status === 'failed' || status === 'cached') {
setTimeout(() => {
setActiveUrls((prev) => prev.filter((u) => u.url !== url));
}, 5000);
}
});
};
setupListeners();
return () => {
if (unlistenQueries) unlistenQueries();
if (unlistenCompleted) unlistenCompleted();
if (unlistenSwitched) unlistenSwitched();
if (unlistenScraping) unlistenScraping();
};
}, []);
const clearActiveQueries = useCallback(() => {
setActiveQueries([]);
}, []);
const clearActiveUrls = useCallback(() => {
setActiveUrls([]);
}, []);
const clearAll = useCallback(() => {
setActiveQueries([]);
setActiveUrls([]);
}, []);
return {
activeQueries,
activeUrls,
clearActiveQueries,
clearActiveUrls,
clearAll,
};
}
+42 -3
View File
@@ -1,8 +1,10 @@
import { useState, useCallback } from 'react';
import { useState, useCallback, useEffect } from 'react';
import { invoke } from '@tauri-apps/api/core';
import { webSearchService, ScrapedContent, SearchConfig } from '@/services/webSearch';
import { useSettingsStore } from '@/store/settings-store';
import { useWebSearchEvents, type ActiveQuery, type ActiveUrl } from './use-web-search-events';
export type SearchStatus = 'idle' | 'searching' | 'scraping' | 'completed' | 'error';
export type SearchStatus = 'idle' | 'searching' | 'scraping' | 'completed' | 'error' | 'cancelled';
export interface WebSearchState {
isEnabled: boolean;
@@ -10,6 +12,8 @@ export interface WebSearchState {
currentQuery: string;
scrapedSources: ScrapedContent[];
error: string | null;
activeQueries: ActiveQuery[];
activeUrls: ActiveUrl[];
}
const DEFAULT_STATE: WebSearchState = {
@@ -18,12 +22,16 @@ const DEFAULT_STATE: WebSearchState = {
currentQuery: '',
scrapedSources: [],
error: null,
activeQueries: [],
activeUrls: [],
};
/**
* Hook para gerenciar estado do Web Search
*/
export function useWebSearch() {
const events = useWebSearchEvents();
const [state, setState] = useState<WebSearchState>(() => {
// Carregar preferência do localStorage
if (typeof window !== 'undefined') {
@@ -36,6 +44,15 @@ export function useWebSearch() {
return DEFAULT_STATE;
});
// Sincronizar eventos com estado usando useEffect
useEffect(() => {
setState((prev) => ({
...prev,
activeQueries: events.activeQueries,
activeUrls: events.activeUrls,
}));
}, [events.activeQueries, events.activeUrls]);
/**
* Ativa/desativa Web Search
*/
@@ -175,7 +192,7 @@ export function useWebSearch() {
} catch (error) {
// Se houver erro, logar mas retornar array vazio (não quebrar o fluxo)
const errorMessage = error instanceof Error ? error.message : 'Erro desconhecido no smartSearchRag';
console.warn('Erro em smartSearchRag (retornando vazio):', errorMessage);
console.warn('Erro em smartSearchRag (retornando vazio para usar fallback de modelo local):', errorMessage);
setState(prev => ({
...prev,
@@ -185,6 +202,7 @@ export function useWebSearch() {
}));
// Retornar vazio em vez de throw para não quebrar o fluxo
// O sistema continuará usando o modelo local como fallback
return [];
}
}, []);
@@ -218,6 +236,22 @@ export function useWebSearch() {
}
}, []);
/**
* Cancela a busca atual
*/
const cancel = useCallback(async () => {
try {
await invoke('cancel_scraping');
setState(prev => ({
...prev,
status: 'cancelled',
error: null
}));
} catch (error) {
console.error('Erro ao cancelar busca:', error);
}
}, []);
/**
* Reseta o estado
*/
@@ -235,14 +269,19 @@ export function useWebSearch() {
webSearchService.clearCache();
}, []);
return {
...state,
activeQueries: events.activeQueries,
activeUrls: events.activeUrls,
setEnabled,
search,
smartSearchRag,
extractUrl,
cancel,
reset,
clearCache,
setState, // Expor setState para permitir atualização externa em caso de erro
};
}