diff --git a/docker-compose.yml b/docker-compose.yml index 107121c..7613d2c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,6 +13,8 @@ services: OLLAMA_URL: "http://ollama:11434" OLLAMA_MODEL: "${OLLAMA_MODEL:-llama3.1}" SECRET_KEY: "${SECRET_KEY:-dev-secret-change-me}" + # Uncomment and set to enable web research via SearXNG: + # SEARXNG_URL: "http://searxng:8080" healthcheck: test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:5000/api/health')"] interval: 10s @@ -40,6 +42,7 @@ services: - ollama_models:/root/.ollama environment: OLLAMA_MAX_LOADED_MODELS: "2" + OLLAMA_NUM_PARALLEL: "2" OLLAMA_KEEP_ALIVE: "30m" OLLAMA_FLASH_ATTENTION: "1" deploy: diff --git a/frontend/src/components/ToolCallCard.vue b/frontend/src/components/ToolCallCard.vue index 298494a..75408b7 100644 --- a/frontend/src/components/ToolCallCard.vue +++ b/frontend/src/components/ToolCallCard.vue @@ -52,6 +52,12 @@ const label = computed(() => { return "Completed todo"; case "todo_deleted": return "Deleted todo"; + case "web_search": + return "Web search"; + case "research_pending": + return "Research started"; + case "research_note": + return "Research note"; default: return "Tool call"; } @@ -167,6 +173,12 @@ const taskCount = computed(() => { return (data.total as number) ?? 0; }); +const webSearch = computed(() => { + const data = props.toolCall.result.data; + if (!data || props.toolCall.result.type !== "web_search") return null; + return data as { query: string; results: { url: string; title: string; snippet: string }[]; count: number }; +}); + const searchResults = computed(() => { const data = props.toolCall.result.data; if (!data || props.toolCall.result.type !== "search") return null; @@ -317,6 +329,19 @@ async function applyTag(tag: string) { + @@ -400,6 +425,13 @@ async function applyTag(tag: string) { color: var(--color-text-muted); margin-right: 0.1rem; } +.web-search-results { + flex-direction: column; + flex-wrap: nowrap; +} +.web-search-results .tool-search-item::after { + content: none; +} .tool-event-title { font-weight: 600; color: var(--color-text); diff --git a/frontend/src/views/ChatView.vue b/frontend/src/views/ChatView.vue index d42e7fc..8116212 100644 --- a/frontend/src/views/ChatView.vue +++ b/frontend/src/views/ChatView.vue @@ -22,6 +22,10 @@ const sending = ref(false); const summarizing = ref(false); const sidebarOpen = ref(false); +// Research modal state +const showResearchModal = ref(false); +const researchTopic = ref(""); + // Note picker state const attachedNote = ref<{ id: number; title: string } | null>(null); const showNotePicker = ref(false); @@ -239,6 +243,27 @@ function onInputKeydown(e: KeyboardEvent) { } } +// Research modal +function toggleResearchModal() { + showResearchModal.value = !showResearchModal.value; + if (showResearchModal.value) { + researchTopic.value = ""; + nextTick(() => { + const el = document.querySelector(".research-topic-input") as HTMLInputElement; + el?.focus(); + }); + } +} + +function submitResearch() { + const topic = researchTopic.value.trim(); + if (!topic) return; + messageInput.value = `Research: ${topic}`; + showResearchModal.value = false; + researchTopic.value = ""; + sendMessage(); +} + // Note picker function toggleNotePicker() { showNotePicker.value = !showNotePicker.value; @@ -302,7 +327,12 @@ function removeIncludedNote(noteId: number) { // Keyboard shortcuts function onGlobalKeydown(e: KeyboardEvent) { if (e.key !== "Escape") return; - // Close note picker first + // Close research modal first + if (showResearchModal.value) { + showResearchModal.value = false; + return; + } + // Close note picker if (showNotePicker.value) { showNotePicker.value = false; return; @@ -479,6 +509,37 @@ onUnmounted(() => {
+ +
+ + + +
+
Research topic
+ +
+ + +
+
+
+