chore: expurgo de blobs excedentes e realinhamento de estado
release.yml (push) Invalid workflow file
release.yml (push) Invalid workflow file
This commit is contained in:
1 parent
593efd42e0
commit
a7cbd1a3b4
114 files changed
+20716
-4258
No files matched your search
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
@@ -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',
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -56,3 +56,11 @@ export function useOperatingSystem() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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') {
|
||||
|
||||
@@ -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
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user