import { create } from 'zustand'; import { persist } from 'zustand/middleware'; export interface SearchCategory { id: string; name: string; baseSites: string[]; enabled: boolean; } export interface SettingsState { // AI & Models ollamaUrl: string; selectedModel: string; systemPrompt: string; contextWindow: number; // Web Search webSearch: { enabled: boolean; maxResults: number; timeout: number; excludedDomains: string[]; maxConcurrentTabs: number; totalSourcesLimit: number; categories: SearchCategory[]; userCustomSites: string[]; }; // Actions setOllamaUrl: (url: string) => void; setSelectedModel: (model: string) => void; setSystemPrompt: (prompt: string) => void; setContextWindow: (ctx: number) => void; setWebSearchEnabled: (enabled: boolean) => void; setWebSearchMaxResults: (max: number) => void; setWebSearchTimeout: (timeout: number) => void; setWebSearchMaxConcurrentTabs: (max: number) => void; setWebSearchTotalSourcesLimit: (limit: number) => void; addExcludedDomain: (domain: string) => void; removeExcludedDomain: (domain: string) => void; toggleCategory: (categoryId: string) => void; updateCategory: (category: SearchCategory) => void; addCustomSite: (site: string) => void; removeCustomSite: (site: string) => void; resetSettings: () => void; } const defaultSystemPrompt = `Você é um assistente de IA local integrado ao OllaHub. ## REGRAS ABSOLUTAS 1. **Nunca** escreva tags \`\`, JSON oculto ou textos como "Metadados:". Toda resposta deve ser apenas texto visível ao usuário. 2. Use Markdown limpo: títulos, listas e blocos de código com linguagem indicada. 3. Seja direto, técnico e objetivo. Resumos iniciais são bem-vindos em respostas longas. 4. Comandos e códigos devem estar em blocos triplos (\`\`\`bash, \`\`\`json, etc.). 5. Se receber um bloco [CONTEXTO WEB], use essas informações prioritariamente para responder. ## CONTEXTO WEB - Quando informações da web forem fornecidas, use-as como fonte principal. - Cite as fontes usando [1], [2], [3] ao final das frases quando usar informações do contexto web. - Se o contexto não for suficiente, diga isso claramente. ## FORMATO SUGERIDO - Resumo inicial (1–2 frases) quando necessário. - Seções \`##\` para organizar o conteúdo. - Listas para passos/itens. - Blocos de código com linguagem específica. ## ESTILO - Cite limitações, hipóteses e próximos passos. - Inclua links apenas quando essencial. - Respostas sempre limpas, sem metadados ou conteúdo oculto.`; // Categorias padrão curadas const defaultCategories: SearchCategory[] = [ { id: 'academic', name: 'Acadêmico', baseSites: [ 'scielo.br', 'arxiv.org', 'scholar.google.com', 'pubmed.ncbi.nlm.nih.gov', 'researchgate.net', ], enabled: false, }, { id: 'news', name: 'Notícias', baseSites: [ 'g1.globo.com', 'bbc.com', 'cnnbrasil.com.br', 'folha.uol.com.br', 'estadao.com.br', ], enabled: true, }, { id: 'tech', name: 'Tech & Dev', baseSites: [ 'github.com', 'stackoverflow.com', 'dev.to', 'tabnews.com.br', 'medium.com', ], enabled: true, }, { id: 'finance', name: 'Financeiro', baseSites: [ 'infomoney.com.br', 'bloomberg.com', 'valor.globo.com', 'investing.com', 'yahoo.com/finance', ], enabled: false, }, ]; const initialState = { ollamaUrl: 'http://localhost:11434', selectedModel: '', systemPrompt: defaultSystemPrompt, contextWindow: 4096, webSearch: { enabled: true, maxResults: 5, timeout: 10, excludedDomains: ['youtube.com', 'linkedin.com'], maxConcurrentTabs: 5, totalSourcesLimit: 40, categories: defaultCategories, userCustomSites: [], }, }; export const useSettingsStore = create()( persist( (set) => ({ ...initialState, setOllamaUrl: (url) => set({ ollamaUrl: url }), setSelectedModel: (model) => set({ selectedModel: model }), setSystemPrompt: (prompt) => set({ systemPrompt: prompt }), setContextWindow: (ctx) => set({ contextWindow: ctx }), setWebSearchEnabled: (enabled) => set((state) => ({ webSearch: { ...state.webSearch, enabled }, })), setWebSearchMaxResults: (max) => set((state) => ({ webSearch: { ...state.webSearch, maxResults: max }, })), setWebSearchTimeout: (timeout) => set((state) => ({ webSearch: { ...state.webSearch, timeout }, })), setWebSearchMaxConcurrentTabs: (max) => set((state) => ({ webSearch: { ...state.webSearch, maxConcurrentTabs: max }, })), setWebSearchTotalSourcesLimit: (limit) => set((state) => ({ webSearch: { ...state.webSearch, totalSourcesLimit: limit }, })), addExcludedDomain: (domain) => set((state) => { const normalized = domain.toLowerCase().trim().replace(/^https?:\/\//, '').replace(/\/$/, ''); if (!normalized || state.webSearch.excludedDomains.includes(normalized)) { return state; } return { webSearch: { ...state.webSearch, excludedDomains: [...state.webSearch.excludedDomains, normalized], }, }; }), removeExcludedDomain: (domain) => set((state) => ({ webSearch: { ...state.webSearch, excludedDomains: state.webSearch.excludedDomains.filter((d) => d !== domain), }, })), toggleCategory: (categoryId) => set((state) => ({ webSearch: { ...state.webSearch, categories: state.webSearch.categories.map((cat) => cat.id === categoryId ? { ...cat, enabled: !cat.enabled } : cat ), }, })), updateCategory: (category) => set((state) => ({ webSearch: { ...state.webSearch, categories: state.webSearch.categories.map((cat) => cat.id === category.id ? category : cat ), }, })), addCustomSite: (site) => set((state) => { const normalized = site.toLowerCase().trim().replace(/^https?:\/\//, '').replace(/\/$/, ''); if (!normalized || state.webSearch.userCustomSites.includes(normalized)) { return state; } return { webSearch: { ...state.webSearch, userCustomSites: [...state.webSearch.userCustomSites, normalized], }, }; }), removeCustomSite: (site) => set((state) => ({ webSearch: { ...state.webSearch, userCustomSites: state.webSearch.userCustomSites.filter((s) => s !== site), }, })), resetSettings: () => set(initialState), }), { name: 'ollahub-settings', version: 1, } ) );