diff --git a/.gitignore b/.gitignore index 3dcfc570877..ebe5951d39b 100644 --- a/.gitignore +++ b/.gitignore @@ -57,4 +57,4 @@ test/perf/.generated # Dependencies node_modules/ -*/.idea/ \ No newline at end of file +**/.idea/ \ No newline at end of file diff --git a/config/gni/devtools_grd_files.gni b/config/gni/devtools_grd_files.gni index df946b4aba5..7efd81f0fcf 100644 --- a/config/gni/devtools_grd_files.gni +++ b/config/gni/devtools_grd_files.gni @@ -636,6 +636,9 @@ grd_files_release_sources = [ "front_end/panels/ai_chat/tools/StreamlinedSchemaExtractorTool.js", "front_end/panels/ai_chat/tools/VisitHistoryManager.js", "front_end/panels/ai_chat/tools/FullPageAccessibilityTreeToMarkdownTool.js", + "front_end/panels/ai_chat/tools/VectorDBClient.js", + "front_end/panels/ai_chat/tools/BookmarkStoreTool.js", + "front_end/panels/ai_chat/tools/DocumentSearchTool.js", "front_end/panels/ai_chat/common/utils.js", "front_end/panels/ai_chat/common/log.js", "front_end/panels/ai_chat/common/context.js", @@ -646,6 +649,9 @@ grd_files_release_sources = [ "front_end/panels/ai_chat/agent_framework/AgentRunner.js", "front_end/panels/ai_chat/agent_framework/ConfigurableAgentTool.js", "front_end/panels/ai_chat/agent_framework/implementation/ConfiguredAgents.js", + "front_end/panels/ai_chat/agent_framework/implementation/EmailAgents.js", + "front_end/panels/ai_chat/agent_framework/implementation/JobApplicationAgents.js", + "front_end/panels/ai_chat/agent_framework/implementation/OnboardingAgents.js", "front_end/panels/ai_chat/common/MarkdownViewerUtil.js", "front_end/panels/ai_chat/evaluation/runner/VisionAgentEvaluationRunner.js", "front_end/panels/ai_chat/evaluation/runner/EvaluationRunner.js", @@ -654,6 +660,12 @@ grd_files_release_sources = [ "front_end/panels/ai_chat/evaluation/framework/MarkdownReportGenerator.js", "front_end/panels/ai_chat/evaluation/framework/types.js", "front_end/panels/ai_chat/evaluation/test-cases/action-agent-tests.js", + "front_end/panels/ai_chat/evaluation/test-cases/email-agent-tests.js", + "front_end/panels/ai_chat/evaluation/test-cases/job-application-agent-tests.js", + "front_end/panels/ai_chat/evaluation/test-cases/onboarding-agent-tests.js", + "front_end/panels/ai_chat/evaluation/examples/run-email-agent-tests.js", + "front_end/panels/ai_chat/evaluation/examples/run-job-application-tests.js", + "front_end/panels/ai_chat/evaluation/examples/run-onboarding-agent-tests.js", "front_end/panels/ai_chat/evaluation/test-cases/research-agent-tests.js", "front_end/panels/ai_chat/evaluation/test-cases/schema-extractor-tests.js", "front_end/panels/ai_chat/evaluation/test-cases/streamlined-schema-extractor-tests.js", diff --git a/front_end/panels/ai_chat/BUILD.gn b/front_end/panels/ai_chat/BUILD.gn index 53e7b8af75b..0dc089e04b1 100644 --- a/front_end/panels/ai_chat/BUILD.gn +++ b/front_end/panels/ai_chat/BUILD.gn @@ -54,9 +54,15 @@ devtools_module("ai_chat") { "tools/StreamlinedSchemaExtractorTool.ts", "tools/CombinedExtractionTool.ts", "tools/FullPageAccessibilityTreeToMarkdownTool.ts", + "tools/VectorDBClient.ts", + "tools/BookmarkStoreTool.ts", + "tools/DocumentSearchTool.ts", "agent_framework/ConfigurableAgentTool.ts", "agent_framework/AgentRunner.ts", "agent_framework/implementation/ConfiguredAgents.ts", + "agent_framework/implementation/EmailAgents.ts", + "agent_framework/implementation/JobApplicationAgents.ts", + "agent_framework/implementation/OnboardingAgents.ts", "evaluation/framework/types.ts", "evaluation/framework/judges/LLMEvaluator.ts", "evaluation/framework/GenericToolEvaluator.ts", @@ -70,6 +76,12 @@ devtools_module("ai_chat") { "evaluation/test-cases/streamlined-schema-extractor-tests.ts", "evaluation/test-cases/research-agent-tests.ts", "evaluation/test-cases/action-agent-tests.ts", + "evaluation/test-cases/email-agent-tests.ts", + "evaluation/test-cases/job-application-agent-tests.ts", + "evaluation/test-cases/onboarding-agent-tests.ts", + "evaluation/examples/run-email-agent-tests.ts", + "evaluation/examples/run-job-application-tests.ts", + "evaluation/examples/run-onboarding-agent-tests.ts", "evaluation/runner/EvaluationRunner.ts", "evaluation/runner/VisionAgentEvaluationRunner.ts", "common/MarkdownViewerUtil.ts", @@ -135,9 +147,15 @@ _ai_chat_sources = [ "tools/StreamlinedSchemaExtractorTool.ts", "tools/CombinedExtractionTool.ts", "tools/FullPageAccessibilityTreeToMarkdownTool.ts", + "tools/VectorDBClient.ts", + "tools/BookmarkStoreTool.ts", + "tools/DocumentSearchTool.ts", "agent_framework/ConfigurableAgentTool.ts", "agent_framework/AgentRunner.ts", "agent_framework/implementation/ConfiguredAgents.ts", + "agent_framework/implementation/EmailAgents.ts", + "agent_framework/implementation/JobApplicationAgents.ts", + "agent_framework/implementation/OnboardingAgents.ts", "evaluation/framework/types.ts", "evaluation/framework/judges/LLMEvaluator.ts", "evaluation/framework/GenericToolEvaluator.ts", @@ -151,6 +169,12 @@ _ai_chat_sources = [ "evaluation/test-cases/streamlined-schema-extractor-tests.ts", "evaluation/test-cases/research-agent-tests.ts", "evaluation/test-cases/action-agent-tests.ts", + "evaluation/test-cases/email-agent-tests.ts", + "evaluation/test-cases/job-application-agent-tests.ts", + "evaluation/test-cases/onboarding-agent-tests.ts", + "evaluation/examples/run-email-agent-tests.ts", + "evaluation/examples/run-job-application-tests.ts", + "evaluation/examples/run-onboarding-agent-tests.ts", "evaluation/runner/EvaluationRunner.ts", "evaluation/runner/VisionAgentEvaluationRunner.ts", "common/MarkdownViewerUtil.ts", diff --git a/front_end/panels/ai_chat/agent_framework/implementation/ConfiguredAgents.ts b/front_end/panels/ai_chat/agent_framework/implementation/ConfiguredAgents.ts index 8fecb185597..3280c2e0e24 100644 --- a/front_end/panels/ai_chat/agent_framework/implementation/ConfiguredAgents.ts +++ b/front_end/panels/ai_chat/agent_framework/implementation/ConfiguredAgents.ts @@ -6,6 +6,8 @@ import { FetcherTool } from '../../tools/FetcherTool.js'; import { FinalizeWithCritiqueTool } from '../../tools/FinalizeWithCritiqueTool.js'; import { SchemaBasedExtractorTool } from '../../tools/SchemaBasedExtractorTool.js'; import { StreamlinedSchemaExtractorTool } from '../../tools/StreamlinedSchemaExtractorTool.js'; +import { BookmarkStoreTool } from '../../tools/BookmarkStoreTool.js'; +import { DocumentSearchTool } from '../../tools/DocumentSearchTool.js'; import { NavigateURLTool, PerformActionTool, GetAccessibilityTreeTool, SearchContentTool, NavigateBackTool, NodeIDsToURLsTool, TakeScreenshotTool } from '../../tools/Tools.js'; import { AIChatPanel } from '../../ui/AIChatPanel.js'; import { ChatMessageEntity, type ChatMessage } from '../../ui/ChatView.js'; @@ -13,6 +15,9 @@ import { ConfigurableAgentTool, ToolRegistry, type AgentToolConfig, type ConfigurableAgentArgs } from '../ConfigurableAgentTool.js'; +import { initializeEmailAgents } from './EmailAgents.js'; +import { initializeJobApplicationAgents } from './JobApplicationAgents.js'; +import { initializeOnboardingAgents } from './OnboardingAgents.js'; /** * Initialize all configured agents @@ -31,6 +36,10 @@ export function initializeConfiguredAgents(): void { ToolRegistry.registerToolFactory('get_page_content', () => new GetAccessibilityTreeTool()); ToolRegistry.registerToolFactory('search_content', () => new SearchContentTool()); ToolRegistry.registerToolFactory('take_screenshot', () => new TakeScreenshotTool()); + + // Register bookmark and document search tools + ToolRegistry.registerToolFactory('bookmark_store', () => new BookmarkStoreTool()); + ToolRegistry.registerToolFactory('document_search', () => new DocumentSearchTool()); // Create and register Research Agent const researchAgentConfig = createResearchAgentConfig(); @@ -77,6 +86,15 @@ export function initializeConfiguredAgents(): void { const ecommerceProductInfoAgentConfig = createEcommerceProductInfoAgentConfig(); const ecommerceProductInfoAgent = new ConfigurableAgentTool(ecommerceProductInfoAgentConfig); ToolRegistry.registerToolFactory('ecommerce_product_info_fetcher_tool', () => ecommerceProductInfoAgent); + + // Initialize all email agents + initializeEmailAgents(); + + // Initialize all job application agents + initializeJobApplicationAgents(); + + // Initialize all onboarding agents + initializeOnboardingAgents(); } /** @@ -148,7 +166,9 @@ Remember: You are a tool that executes research autonomously. Complete your task 'navigate_back', 'fetcher_tool', 'schema_based_extractor', - 'node_ids_to_urls' + 'node_ids_to_urls', + 'bookmark_store', + 'document_search' ], maxIterations: 15, modelName: () => AIChatPanel.getMiniModel(), diff --git a/front_end/panels/ai_chat/core/BaseOrchestratorAgent.ts b/front_end/panels/ai_chat/core/BaseOrchestratorAgent.ts index a43aff8a2e4..14fdf718a36 100644 --- a/front_end/panels/ai_chat/core/BaseOrchestratorAgent.ts +++ b/front_end/panels/ai_chat/core/BaseOrchestratorAgent.ts @@ -298,6 +298,7 @@ export function getSystemPrompt(agentType: string): string { export function getAgentTools(agentType: string): Array> { return AGENT_CONFIGS[agentType]?.availableTools || [ ToolRegistry.getToolInstance('action_agent') || (() => { throw new Error('action_agent tool not found'); })(), + ToolRegistry.getToolInstance('document_search') || (() => { throw new Error('document_search tool not found'); })(), new NavigateURLTool(), new NavigateBackTool(), new SchemaBasedExtractorTool(), diff --git a/front_end/panels/ai_chat/tools/BookmarkStoreTool.ts b/front_end/panels/ai_chat/tools/BookmarkStoreTool.ts new file mode 100644 index 00000000000..c0dc27fc5af --- /dev/null +++ b/front_end/panels/ai_chat/tools/BookmarkStoreTool.ts @@ -0,0 +1,250 @@ +// Copyright 2025 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import * as SDK from '../../../core/sdk/sdk.js'; +import * as Utils from '../common/utils.js'; +import { createLogger } from '../core/Logger.js'; +import { HTMLToMarkdownTool } from './HTMLToMarkdownTool.js'; +import { VectorDBClient, type VectorDocument, type VectorStoreResponse } from './VectorDBClient.js'; +import type { Tool } from './Tools.js'; +import { integer } from '../../../generated/protocol.js'; + +const logger = createLogger('Tool:BookmarkStore'); + +/** + * Arguments for bookmark store operation + */ +export interface BookmarkStoreArgs { + title?: string; + tags?: string[]; + reasoning: string; + includeFullContent?: boolean; +} + +/** + * Result from bookmark store operation + */ +export interface BookmarkStoreResult { + success: boolean; + id?: integer; + url?: string; + title?: string; + error?: string; + message?: string; +} + +/** + * Tool for storing current page content as a bookmark in vector database + */ +export class BookmarkStoreTool implements Tool { + name = 'bookmark_store'; + description = 'Stores the current page content and metadata in a vector database for later retrieval. Extracts clean markdown content and makes it searchable.'; + + schema = { + type: 'object', + properties: { + title: { + type: 'string', + description: 'Custom title for the bookmark (optional, will use page title if not provided)' + }, + tags: { + type: 'array', + items: { + type: 'string' + }, + description: 'Tags to categorize the bookmark for easier discovery' + }, + reasoning: { + type: 'string', + description: 'Reasoning for bookmarking this page, displayed to the user' + }, + includeFullContent: { + type: 'boolean', + description: 'Whether to include full page content or just a summary (default: true)' + } + }, + required: ['reasoning'] + }; + + private htmlToMarkdownTool = new HTMLToMarkdownTool(); + + /** + * Execute the bookmark store operation + */ + async execute(args: BookmarkStoreArgs): Promise { + logger.info('Executing bookmark store with args', { args }); + + try { + // Get the current page target + const target = SDK.TargetManager.TargetManager.instance().primaryPageTarget(); + if (!target) { + return { + success: false, + error: 'No page target available - cannot bookmark current page' + }; + } + + // Get current page URL and title + const { url, pageTitle } = await this.getCurrentPageInfo(target); + if (!url) { + return { + success: false, + error: 'Could not determine current page URL' + }; + } + + // Get vector DB configuration + const vectorDBConfig = this.getVectorDBConfig(); + if (!vectorDBConfig.endpoint) { + return { + success: false, + error: 'Vector database not configured. Please set up vector DB endpoint in Settings.' + }; + } + + // Extract page content as markdown + logger.info('Extracting page content for bookmark'); + const markdownResult = await this.htmlToMarkdownTool.execute({ + instruction: `Extract the main content from this page for bookmarking. Focus on the primary article or content that would be useful for later reference.`, + reasoning: 'Extracting content for bookmark storage' + }); + + if (!markdownResult.success || !markdownResult.markdownContent) { + return { + success: false, + error: `Failed to extract page content: ${markdownResult.error || 'Unknown error'}` + }; + } + + // Prepare document for storage + const document: VectorDocument = { + content: markdownResult.markdownContent, + metadata: { + url, + title: args.title || pageTitle || 'Untitled Page', + timestamp: Date.now(), + domain: this.extractDomain(url), + tags: args.tags || [], + } + }; + + // Store in vector database + const vectorClient = new VectorDBClient(vectorDBConfig); + const storeResult = await vectorClient.storeDocument(document); + + if (!storeResult.success) { + return { + success: false, + error: `Failed to store bookmark: ${storeResult.error}` + }; + } + + logger.info('Bookmark stored successfully', { + id: storeResult.id, + url, + title: document.metadata.title + }); + + return { + success: true, + id: storeResult.id, + url, + title: document.metadata.title, + message: `Successfully bookmarked "${document.metadata.title}" - content is now searchable in your document library.` + }; + + } catch (error: any) { + logger.error('Error storing bookmark', { error: error.message, stack: error.stack }); + return { + success: false, + error: `Error storing bookmark: ${error.message}` + }; + } + } + + /** + * Get current page URL and title + */ + private async getCurrentPageInfo(target: SDK.Target.Target): Promise<{ + url: string; + pageTitle: string; + }> { + try { + // Get the runtime model to execute JavaScript + const runtimeModel = target.model(SDK.RuntimeModel.RuntimeModel); + if (!runtimeModel) { + throw new Error('Runtime model not available'); + } + + // Get the execution context for evaluation + const executionContext = runtimeModel.defaultExecutionContext(); + if (!executionContext) { + throw new Error('No execution context available'); + } + + // Execute JavaScript to get URL and title + const result = await executionContext.evaluate( + { + expression: `({ + url: window.location.href, + title: document.title + })`, + objectGroup: 'temp', + includeCommandLineAPI: false, + silent: true, + returnByValue: true, + generatePreview: false + }, + /* userGesture */ false, + /* awaitPromise */ false + ); + + if ('error' in result) { + throw new Error(`Failed to get page information: ${result.error}`); + } + + if (!result.object || !result.object.value) { + throw new Error('Failed to get page information: No result returned'); + } + + const pageInfo = result.object.value; + return { + url: pageInfo.url || '', + pageTitle: pageInfo.title || '' + }; + + } catch (error: any) { + logger.error('Failed to get current page info', { error: error.message }); + return { + url: '', + pageTitle: '' + }; + } + } + + /** + * Extract domain from URL + */ + private extractDomain(url: string): string { + try { + const urlObj = new URL(url); + return urlObj.hostname; + } catch { + return 'unknown'; + } + } + + /** + * Get vector database configuration from localStorage + */ + private getVectorDBConfig() { + return { + endpoint: localStorage.getItem('ai_chat_milvus_endpoint') || '', + username: localStorage.getItem('ai_chat_milvus_username') || 'root', + password: localStorage.getItem('ai_chat_milvus_password') || 'Milvus', + collection: localStorage.getItem('ai_chat_milvus_collection') || 'bookmarks', + openaiApiKey: localStorage.getItem('ai_chat_milvus_openai_key') || undefined, + }; + } +} \ No newline at end of file diff --git a/front_end/panels/ai_chat/tools/DocumentSearchTool.ts b/front_end/panels/ai_chat/tools/DocumentSearchTool.ts new file mode 100644 index 00000000000..d491d1c9c9f --- /dev/null +++ b/front_end/panels/ai_chat/tools/DocumentSearchTool.ts @@ -0,0 +1,273 @@ +// Copyright 2025 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import { createLogger } from '../core/Logger.js'; +import { VectorDBClient, type VectorSearchResult } from './VectorDBClient.js'; +import type { Tool } from './Tools.js'; + +const logger = createLogger('Tool:DocumentSearch'); + +/** + * Arguments for document search operation + */ +export interface DocumentSearchArgs { + query: string; + limit?: number; + tags?: string[]; + domain?: string; + reasoning: string; +} + +/** + * Formatted search result for display + */ +export interface FormattedSearchResult { + id: string; + title: string; + url: string; + content: string; + relevanceScore: number; + domain: string; + tags: string[]; + bookmarkedAt: string; +} + +/** + * Result from document search operation + */ +export interface DocumentSearchResult { + success: boolean; + results?: FormattedSearchResult[]; + totalResults?: number; + query?: string; + error?: string; + message?: string; +} + +/** + * Tool for searching previously bookmarked documents using semantic similarity + */ +export class DocumentSearchTool implements Tool { + name = 'document_search'; + description = 'Searches through previously bookmarked documents using semantic similarity. Finds relevant content based on natural language queries, not just keyword matching.'; + + schema = { + type: 'object', + properties: { + query: { + type: 'string', + description: 'Natural language search query to find relevant documents' + }, + limit: { + type: 'number', + description: 'Maximum number of results to return (default: 10, max: 50)', + minimum: 1, + maximum: 50 + }, + tags: { + type: 'array', + items: { + type: 'string' + }, + description: 'Filter results by specific tags' + }, + domain: { + type: 'string', + description: 'Filter results by domain (e.g., "github.com", "stackoverflow.com")' + }, + reasoning: { + type: 'string', + description: 'Reasoning for the search, displayed to the user' + } + }, + required: ['query', 'reasoning'] + }; + + /** + * Execute the document search operation + */ + async execute(args: DocumentSearchArgs): Promise { + logger.info('Executing document search with args', { args }); + + try { + // Validate input + if (!args.query || args.query.trim().length === 0) { + return { + success: false, + error: 'Search query cannot be empty' + }; + } + + // Get vector DB configuration + const vectorDBConfig = this.getVectorDBConfig(); + if (!vectorDBConfig.endpoint) { + return { + success: false, + error: 'Vector database not configured. Please set up vector DB endpoint in Settings.' + }; + } + + // Prepare search parameters + const limit = Math.min(args.limit || 5, 20); // Default to 5, max 20 + const filter = this.buildSearchFilter(args); + + // Execute search + const vectorClient = new VectorDBClient(vectorDBConfig); + const searchResult = await vectorClient.searchDocuments( + args.query, + limit, + filter + ); + + if (!searchResult.success) { + return { + success: false, + error: `Search failed: ${searchResult.error}` + }; + } + + // Format results for display + const formattedResults = this.formatSearchResults(searchResult.results || []); + + logger.info('Document search completed', { + query: args.query, + resultsCount: formattedResults.length + }); + + // Generate appropriate message + const message = this.generateSearchMessage(args.query, formattedResults.length); + + return { + success: true, + results: formattedResults, + totalResults: formattedResults.length, + query: args.query, + message + }; + + } catch (error: any) { + logger.error('Error searching documents', { error: error.message, stack: error.stack }); + return { + success: false, + error: `Error searching documents: ${error.message}` + }; + } + } + + /** + * Build search filter based on arguments + */ + private buildSearchFilter(args: DocumentSearchArgs): Record | undefined { + const filter: Record = {}; + + if (args.tags && args.tags.length > 0) { + filter.tags = { $in: args.tags }; + } + + if (args.domain) { + filter.domain = args.domain; + } + + // Return undefined if no filters, otherwise return the filter object + return Object.keys(filter).length > 0 ? filter : undefined; + } + + /** + * Format search results for display + */ + private formatSearchResults(results: VectorSearchResult[]): FormattedSearchResult[] { + return results.map(result => { + // Extract excerpt from content (first 300 characters) + const contentExcerpt = this.extractContentExcerpt(result.content); + + // Format timestamp + const bookmarkedAt = this.formatTimestamp(result.metadata.timestamp); + + return { + id: result.id, + title: result.metadata.title, + url: result.metadata.url, + content: contentExcerpt, + relevanceScore: Math.round(result.score * 100) / 100, // Round to 2 decimal places + domain: result.metadata.domain || 'unknown', + tags: result.metadata.tags || [], + bookmarkedAt + }; + }); + } + + /** + * Extract a meaningful excerpt from content + */ + private extractContentExcerpt(content: string): string { + // Remove markdown formatting for cleaner excerpt + const cleanContent = content + .replace(/#{1,6}\s/g, '') // Remove headers + .replace(/\*\*(.*?)\*\*/g, '$1') // Remove bold + .replace(/\*(.*?)\*/g, '$1') // Remove italic + .replace(/`(.*?)`/g, '$1') // Remove inline code + .replace(/\[(.*?)\]\(.*?\)/g, '$1') // Remove links, keep text + .replace(/\n\s*\n/g, ' ') // Replace multiple newlines with space + .trim(); + + // Return first 300 characters with ellipsis if longer + if (cleanContent.length <= 300) { + return cleanContent; + } + + // Find a good breaking point near 300 characters + const breakPoint = cleanContent.indexOf(' ', 280); + return cleanContent.substring(0, breakPoint > 0 ? breakPoint : 300) + '...'; + } + + /** + * Format timestamp for display + */ + private formatTimestamp(timestamp: number): string { + const date = new Date(timestamp); + const now = new Date(); + const diffMs = now.getTime() - date.getTime(); + const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); + + if (diffDays === 0) { + return 'Today'; + } else if (diffDays === 1) { + return 'Yesterday'; + } else if (diffDays < 7) { + return `${diffDays} days ago`; + } else if (diffDays < 30) { + return `${Math.floor(diffDays / 7)} weeks ago`; + } else if (diffDays < 365) { + return `${Math.floor(diffDays / 30)} months ago`; + } else { + return date.toLocaleDateString(); + } + } + + /** + * Generate appropriate search message + */ + private generateSearchMessage(query: string, resultCount: number): string { + if (resultCount === 0) { + return `No documents found for "${query}". Try a different search term or check if you have any bookmarks saved.`; + } else if (resultCount === 1) { + return `Found 1 relevant document for "${query}".`; + } else { + return `Found ${resultCount} relevant documents for "${query}". Results are ranked by semantic similarity.`; + } + } + + /** + * Get vector database configuration from localStorage + */ + private getVectorDBConfig() { + return { + endpoint: localStorage.getItem('ai_chat_milvus_endpoint') || '', + username: localStorage.getItem('ai_chat_milvus_username') || 'root', + password: localStorage.getItem('ai_chat_milvus_password') || 'Milvus', + collection: localStorage.getItem('ai_chat_milvus_collection') || 'bookmarks', + openaiApiKey: localStorage.getItem('ai_chat_milvus_openai_key') || undefined, + }; + } +} \ No newline at end of file diff --git a/front_end/panels/ai_chat/tools/VectorDBClient.ts b/front_end/panels/ai_chat/tools/VectorDBClient.ts new file mode 100644 index 00000000000..782f28f59c8 --- /dev/null +++ b/front_end/panels/ai_chat/tools/VectorDBClient.ts @@ -0,0 +1,463 @@ +// Copyright 2025 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import { integer } from '../../../generated/protocol.js'; +import { createLogger } from '../core/Logger.js'; + +const logger = createLogger('VectorDBClient'); + +/** + * Document data structure for storing in vector database + */ +export interface VectorDocument { + content: string; + metadata: { + url: string; + title: string; + timestamp: number; + domain: string; + tags?: string[]; + excerpt?: string; + }; +} + +/** + * Search result from vector database + */ +export interface VectorSearchResult { + id: string; + content: string; + score: number; + metadata: { + url: string; + title: string; + timestamp: number; + domain: string; + tags?: string[]; + }; +} + +/** + * Vector database store response + */ +export interface VectorStoreResponse { + success: boolean; + id?: integer; + error?: string; +} + +/** + * Vector database search response + */ +export interface VectorSearchResponse { + success: boolean; + results?: VectorSearchResult[]; + error?: string; +} + +/** + * Milvus configuration + */ +export interface MilvusConfig { + endpoint: string; + username?: string; + password?: string; + collection: string; + openaiApiKey?: string; +} + +/** + * Milvus vector database client + * Implements semantic search for browser bookmarks using Milvus + OpenAI embeddings + */ +export class VectorDBClient { + private endpoint: string; + private token: string | undefined; + private collectionName: string; + private openaiApiKey: string | undefined; + private dimension = 1536; // OpenAI text-embedding-3-small dimension + + constructor(config: MilvusConfig) { + this.endpoint = config.endpoint.endsWith('/') ? config.endpoint.slice(0, -1) : config.endpoint; + this.collectionName = config.collection; + this.openaiApiKey = config.openaiApiKey; + + // Handle authentication: API token or username:password + if (config.password) { + if (config.username && config.username !== 'root') { + // Username provided and not default - use username:password authentication (self-hosted) + this.token = btoa(`${config.username}:${config.password}`); + } else { + // No username or default username - treat password as direct API token (Milvus Cloud) + this.token = config.password; + } + } + } + + /** + * Ensure collection exists with proper schema for bookmarks + */ + async ensureCollection(): Promise { + try { + // Check if collection exists + const describeResponse = await this.makeRequest('GET', `/v2/vectordb/collections/describe`, { + collectionName: this.collectionName + }); + + if (describeResponse.ok) { + logger.info('Collection already exists', { collection: this.collectionName }); + return; + } + + // Create collection with bookmark schema + logger.info('Creating collection for bookmarks', { collection: this.collectionName }); + + const createResponse = await this.makeRequest('POST', '/v2/vectordb/collections/create', { + collectionName: this.collectionName, + dimension: this.dimension, + fields: [ + { + fieldName: 'id', + dataType: 'Int64', + isPrimary: true + }, + { + fieldName: 'url', + dataType: 'VarChar', + elementTypeParams: { max_length: '2000' } + }, + { + fieldName: 'title', + dataType: 'VarChar', + elementTypeParams: { max_length: '500' } + }, + { + fieldName: 'content', + dataType: 'VarChar', + elementTypeParams: { max_length: '65535' } + }, + { + fieldName: 'vector', + dataType: 'FloatVector', + elementTypeParams: { dim: this.dimension.toString() } + }, + { + fieldName: 'timestamp', + dataType: 'Int64' + }, + { + fieldName: 'domain', + dataType: 'VarChar', + elementTypeParams: { max_length: '200' } + }, + { + fieldName: 'tags', + dataType: 'JSON' + } + ], + indexParams: [ + { + fieldName: 'vector', + indexName: 'vector_index', + indexType: 'HNSW', + metricType: 'COSINE', + params: { M: '16', efConstruction: '200' } + } + ] + }); + + if (!createResponse.ok) { + const errorText = await createResponse.text(); + throw new Error(`Failed to create collection: ${errorText}`); + } + + logger.info('Collection created successfully', { collection: this.collectionName }); + } catch (error: any) { + logger.error('Failed to ensure collection', { error: error.message }); + throw error; + } + } + + /** + * Generate OpenAI embedding for text content + */ + async generateEmbedding(text: string): Promise { + if (!this.openaiApiKey) { + throw new Error('OpenAI API key not configured'); + } + + try { + const response = await fetch('https://api.openai.com/v1/embeddings', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${this.openaiApiKey}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + input: text, + model: 'text-embedding-3-small', + dimensions: this.dimension + }) + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`OpenAI API error: ${errorText}`); + } + + const data = await response.json(); + return data.data[0].embedding; + } catch (error: any) { + logger.error('Failed to generate embedding', { error: error.message }); + throw error; + } + } + + /** + * Store a document in Milvus with embedding + */ + async storeDocument(document: VectorDocument): Promise { + try { + logger.info('Storing document in Milvus', { + url: document.metadata.url, + title: document.metadata.title + }); + + // Ensure collection exists + await this.ensureCollection(); + + // Generate embedding for content + const embedding = await this.generateEmbedding(document.content); + + logger.info('Generated embedding for document', { url: document.metadata.url, embedding }); + // Create document ID from URL hash + const documentId = this.generateDocumentId(document.metadata.url); + + // Prepare entity for Milvus + const entity = { + id: documentId, + url: document.metadata.url, + title: document.metadata.title, + content: document.content, + vector: embedding, + timestamp: document.metadata.timestamp || Date.now(), + domain: document.metadata.domain, + tags: document.metadata.tags || [] + }; + + logger.info('Prepared entity for Milvus', { entity }); + // Insert into Milvus + const response = await this.makeRequest('POST', '/v2/vectordb/entities/insert', { + collectionName: this.collectionName, + data: [entity] + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Failed to insert document: ${errorText}`); + } + + const result = await response.json(); + logger.info('Document stored successfully', { id: documentId }); + + return { + success: true, + id: documentId + }; + } catch (error: any) { + logger.error('Failed to store document', { error: error.message }); + return { + success: false, + error: error.message + }; + } + } + + /** + * Search documents using semantic similarity + */ + async searchDocuments(query: string, limit: number = 5, filters?: Record): Promise { + try { + logger.info('Searching documents in Milvus', { query, limit, filters }); + + // Generate embedding for query + const queryEmbedding = await this.generateEmbedding(query); + + // Build search request + const searchRequest = { + collectionName: this.collectionName, + data: [queryEmbedding], + annsField: 'vector', + limit, + outputFields: ['id', 'url', 'title', 'content', 'timestamp', 'domain', 'tags'], + filter: this.buildFilter(filters) + }; + + // Execute search + const response = await this.makeRequest('POST', '/v2/vectordb/entities/search', searchRequest); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Search failed: ${errorText}`); + } + + const result = await response.json(); + const searchResults = this.formatSearchResults(result.data || []); + + logger.info('Document search completed', { resultCount: searchResults.length }); + + return { + success: true, + results: searchResults + }; + } catch (error: any) { + logger.error('Failed to search documents', { error: error.message }); + return { + success: false, + error: error.message + }; + } + } + + /** + * Test connection to Milvus + */ + async testConnection(): Promise<{ success: boolean; error?: string }> { + try { + logger.info('Testing Milvus connection', { endpoint: this.endpoint }); + + const response = await this.makeRequest('POST', '/v2/vectordb/collections/list'); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Connection failed: ${errorText}`); + } + + logger.info('Milvus connection test successful'); + return { success: true }; + } catch (error: any) { + logger.error('Milvus connection test failed', { error: error.message }); + return { + success: false, + error: error.message + }; + } + } + + /** + * Make HTTP request to Milvus + */ + private async makeRequest(method: string, path: string, body?: any): Promise { + const url = `${this.endpoint}${path}`; + const headers: Record = { + 'Content-Type': 'application/json' + }; + + if (this.token) { + headers['Authorization'] = `Bearer ${this.token}`; + } + + const config: RequestInit = { + method, + headers + }; + + if (body && (method === 'POST' || method === 'PUT')) { + config.body = JSON.stringify(body); + } else if (body && method === 'GET') { + // For GET requests, add params to URL + const params = new URLSearchParams(); + Object.entries(body).forEach(([key, value]) => { + params.append(key, String(value)); + }); + const fullUrl = `${url}?${params.toString()}`; + return fetch(fullUrl, config); + } + + return fetch(url, config); + } + + /** + * Build Milvus filter expression from search options + */ + private buildFilter(filters?: Record): string | undefined { + if (!filters) return undefined; + + const conditions: string[] = []; + + if (filters.domain) { + conditions.push(`domain == "${filters.domain}"`); + } + + if (filters.tags && Array.isArray(filters.tags) && filters.tags.length > 0) { + const tagConditions = filters.tags.map(tag => `JSON_CONTAINS(tags, '"${tag}"')`); + conditions.push(`(${tagConditions.join(' OR ')})`); + } + + if (filters.dateFrom || filters.dateTo) { + if (filters.dateFrom) conditions.push(`timestamp >= ${filters.dateFrom}`); + if (filters.dateTo) conditions.push(`timestamp <= ${filters.dateTo}`); + } + + return conditions.length > 0 ? conditions.join(' AND ') : undefined; + } + + /** + * Format Milvus search results to our interface + */ + private formatSearchResults(milvusResults: any[]): VectorSearchResult[] { + logger.info('results from Milvus', { milvusResults }); + return milvusResults.map((result: any) => ({ + id: result.id, + content: result.content, + score: result.distance || result.score || 0, + metadata: { + url: result.url, + title: result.title, + timestamp: result.timestamp, + domain: result.domain, + tags: Array.isArray(result.tags) ? result.tags : [] + } + })); + } + + /** + * Generate document ID from URL + */ + private generateDocumentId(url: string): integer { + // Simple hash function for URL to create stable document IDs + let hash = 0; + for (let i = 0; i < url.length; i++) { + const char = url.charCodeAt(i); + hash = ((hash << 5) - hash) + char; + hash = hash & hash; // Convert to 32-bit integer + } + // Return the absolute value of the hash as an integer + return Math.abs(hash); + } + + /** + * Create VectorDBClient from localStorage settings + */ + static fromSettings(): VectorDBClient | null { + const endpoint = localStorage.getItem('ai_chat_milvus_endpoint'); + const username = localStorage.getItem('ai_chat_milvus_username') || 'root'; + const password = localStorage.getItem('ai_chat_milvus_password') || 'Milvus'; + const collection = localStorage.getItem('ai_chat_milvus_collection') || 'bookmarks'; + const openaiApiKey = localStorage.getItem('ai_chat_milvus_openai_key'); + + if (!endpoint) { + logger.warn('No Milvus endpoint configured in settings'); + return null; + } + + return new VectorDBClient({ + endpoint, + username, + password, + collection, + openaiApiKey: openaiApiKey || undefined + }); + } +} \ No newline at end of file diff --git a/front_end/panels/ai_chat/ui/AIChatPanel.ts b/front_end/panels/ai_chat/ui/AIChatPanel.ts index 037e13212cf..870bda204ea 100644 --- a/front_end/panels/ai_chat/ui/AIChatPanel.ts +++ b/front_end/panels/ai_chat/ui/AIChatPanel.ts @@ -5,6 +5,7 @@ import type * as Common from '../../../core/common/common.js'; import * as Host from '../../../core/host/host.js'; import * as i18n from '../../../core/i18n/i18n.js'; +import * as SDK from '../../../core/sdk/sdk.js'; import * as Buttons from '../../../ui/components/buttons/buttons.js'; import * as UI from '../../../ui/legacy/legacy.js'; import * as Lit from '../../../ui/lit/lit.js'; @@ -107,6 +108,10 @@ const UIStrings = { * @description Run evaluation tests */ runEvaluationTests: 'Run Evaluation Tests', + /** + * @description Bookmark current page + */ + bookmarkPage: 'Bookmark Page', } as const; const str_ = i18n.i18n.registerUIStrings('panels/ai_chat/ui/AIChatPanel.ts', UIStrings); @@ -119,6 +124,7 @@ interface ToolbarViewInput { onHelpClick: () => void; onSettingsClick: () => void; onEvaluationTestClick: () => void; + onBookmarkClick: () => void; isDeleteHistoryButtonVisible: boolean; isCenteredView: boolean; } @@ -165,6 +171,13 @@ function toolbarView(input: ToolbarViewInput): Lit.LitTemplate { .jslogContext=${'ai-chat.evaluation-tests'} .variant=${Buttons.Button.Variant.TOOLBAR} @click=${input.onEvaluationTestClick}> + 1, isCenteredView, }), this.#toolbarContainer, { host: this }); @@ -1101,6 +1115,97 @@ export class AIChatPanel extends UI.Panel.Panel { #onEvaluationTestClick(): void { EvaluationDialog.show(); } + + /** + * Handles the bookmark button click event and bookmarks the current page + */ + async #onBookmarkClick(): Promise { + try { + // Import the BookmarkStoreTool dynamically + const { BookmarkStoreTool } = await import('../tools/BookmarkStoreTool.js'); + const bookmarkTool = new BookmarkStoreTool(); + + // Get current page title for better user feedback + const currentPageTitle = await this.#getCurrentPageTitle(); + + // Execute the bookmark tool + const result = await bookmarkTool.execute({ + reasoning: 'User clicked bookmark button to save current page', + includeFullContent: true + }); + + if (result.success) { + // Add success message to chat + this.#messages.push({ + entity: ChatMessageEntity.MODEL, + action: 'final', + answer: result.message || `Successfully bookmarked "${result.title || currentPageTitle}"`, + isFinalAnswer: true, + }); + this.performUpdate(); + logger.info('Page bookmarked successfully', { url: result.url, title: result.title }); + } else { + // Add error message to chat + this.#messages.push({ + entity: ChatMessageEntity.MODEL, + action: 'final', + answer: `Failed to bookmark page: ${result.error}`, + isFinalAnswer: true, + }); + this.performUpdate(); + logger.error('Failed to bookmark page', { error: result.error }); + } + } catch (error: any) { + logger.error('Error in bookmark click handler', { error: error.message }); + this.#messages.push({ + entity: ChatMessageEntity.MODEL, + action: 'final', + answer: `Error bookmarking page: ${error.message}`, + isFinalAnswer: true, + }); + this.performUpdate(); + } + } + + /** + * Get current page title for user feedback + */ + async #getCurrentPageTitle(): Promise { + try { + const target = SDK.TargetManager.TargetManager.instance().primaryPageTarget(); + if (!target) return 'Current Page'; + + const runtimeModel = target.model(SDK.RuntimeModel.RuntimeModel); + if (!runtimeModel) return 'Current Page'; + + const executionContext = runtimeModel.defaultExecutionContext(); + if (!executionContext) return 'Current Page'; + + const result = await executionContext.evaluate( + { + expression: 'document.title', + objectGroup: 'temp', + includeCommandLineAPI: false, + silent: true, + returnByValue: true, + generatePreview: false + }, + /* userGesture */ false, + /* awaitPromise */ false + ); + + if ('error' in result) { + return 'Current Page'; + } + + if (result.object && result.object.value) { + return result.object.value; + } + } catch (error) { + logger.warn('Failed to get current page title', { error }); + } + return 'Current Page'; + } /** * Handles changes made in the settings dialog diff --git a/front_end/panels/ai_chat/ui/SettingsDialog.ts b/front_end/panels/ai_chat/ui/SettingsDialog.ts index 5f92ec88363..df59b15f4aa 100644 --- a/front_end/panels/ai_chat/ui/SettingsDialog.ts +++ b/front_end/panels/ai_chat/ui/SettingsDialog.ts @@ -23,6 +23,12 @@ const NANO_MODEL_STORAGE_KEY = 'ai_chat_nano_model'; const LITELLM_ENDPOINT_KEY = 'ai_chat_litellm_endpoint'; const LITELLM_API_KEY_STORAGE_KEY = 'ai_chat_litellm_api_key'; const PROVIDER_SELECTION_KEY = 'ai_chat_provider'; +// Vector DB configuration keys - Milvus format +const MILVUS_ENDPOINT_KEY = 'ai_chat_milvus_endpoint'; +const MILVUS_USERNAME_KEY = 'ai_chat_milvus_username'; +const MILVUS_PASSWORD_KEY = 'ai_chat_milvus_password'; +const MILVUS_COLLECTION_KEY = 'ai_chat_milvus_collection'; +const MILVUS_OPENAI_KEY = 'ai_chat_milvus_openai_key'; // UI Strings const UIStrings = { @@ -162,6 +168,66 @@ const UIStrings = { *@description Important notice title */ importantNotice: 'Important Notice', + /** + *@description Milvus DB section label + */ + vectorDBLabel: 'Milvus Vector Database (Bookmarks)', + /** + *@description Milvus endpoint label + */ + vectorDBEndpoint: 'Milvus Endpoint', + /** + *@description Milvus endpoint hint + */ + vectorDBEndpointHint: 'Enter the URL for your Milvus server (e.g., http://localhost:19530 or https://your-milvus.com)', + /** + *@description Milvus username label + */ + vectorDBApiKey: 'Milvus Username', + /** + *@description Milvus username hint + */ + vectorDBApiKeyHint: 'For self-hosted: username (default: root). For Milvus Cloud: leave as root', + /** + *@description Vector DB collection label + */ + vectorDBCollection: 'Collection Name', + /** + *@description Vector DB collection hint + */ + vectorDBCollectionHint: 'Name of the collection to store bookmarks (default: bookmarks)', + /** + *@description Milvus password/token label + */ + milvusPassword: 'Password/API Token', + /** + *@description Milvus password/token hint + */ + milvusPasswordHint: 'For self-hosted: password (default: Milvus). For Milvus Cloud: API token directly', + /** + *@description OpenAI API key for embeddings label + */ + milvusOpenAIKey: 'OpenAI API Key (for embeddings)', + /** + *@description OpenAI API key for embeddings hint + */ + milvusOpenAIKeyHint: 'Required for generating embeddings using OpenAI text-embedding-3-small model', + /** + *@description Test vector DB connection button + */ + testVectorDBConnection: 'Test Connection', + /** + *@description Vector DB connection testing status + */ + testingVectorDBConnection: 'Testing connection...', + /** + *@description Vector DB connection success message + */ + vectorDBConnectionSuccess: 'Vector DB connection successful!', + /** + *@description Vector DB connection failed message + */ + vectorDBConnectionFailed: 'Vector DB connection failed', }; const str_ = i18n.i18n.registerUIStrings('panels/ai_chat/ui/SettingsDialog.ts', UIStrings); @@ -951,6 +1017,207 @@ export class SettingsDialog { // Initialize LiteLLM model selectors updateLiteLLMModelSelectors(); + // Add Vector DB configuration section + const vectorDBSection = document.createElement('div'); + vectorDBSection.classList.add('settings-section'); + contentDiv.appendChild(vectorDBSection); + + const vectorDBTitle = document.createElement('h3'); + vectorDBTitle.textContent = i18nString(UIStrings.vectorDBLabel); + vectorDBTitle.classList.add('settings-subtitle'); + vectorDBSection.appendChild(vectorDBTitle); + + // Vector DB Endpoint + const vectorDBEndpointDiv = document.createElement('div'); + vectorDBEndpointDiv.classList.add('settings-field'); + vectorDBSection.appendChild(vectorDBEndpointDiv); + + const vectorDBEndpointLabel = document.createElement('label'); + vectorDBEndpointLabel.textContent = i18nString(UIStrings.vectorDBEndpoint); + vectorDBEndpointLabel.classList.add('settings-label'); + vectorDBEndpointDiv.appendChild(vectorDBEndpointLabel); + + const vectorDBEndpointHint = document.createElement('div'); + vectorDBEndpointHint.textContent = i18nString(UIStrings.vectorDBEndpointHint); + vectorDBEndpointHint.classList.add('settings-hint'); + vectorDBEndpointDiv.appendChild(vectorDBEndpointHint); + + const vectorDBEndpointInput = document.createElement('input'); + vectorDBEndpointInput.classList.add('settings-input'); + vectorDBEndpointInput.type = 'text'; + vectorDBEndpointInput.placeholder = 'http://localhost:19530'; + vectorDBEndpointInput.value = localStorage.getItem(MILVUS_ENDPOINT_KEY) || ''; + vectorDBEndpointDiv.appendChild(vectorDBEndpointInput); + + // Vector DB API Key + const vectorDBApiKeyDiv = document.createElement('div'); + vectorDBApiKeyDiv.classList.add('settings-field'); + vectorDBSection.appendChild(vectorDBApiKeyDiv); + + const vectorDBApiKeyLabel = document.createElement('label'); + vectorDBApiKeyLabel.textContent = i18nString(UIStrings.vectorDBApiKey); + vectorDBApiKeyLabel.classList.add('settings-label'); + vectorDBApiKeyDiv.appendChild(vectorDBApiKeyLabel); + + const vectorDBApiKeyHint = document.createElement('div'); + vectorDBApiKeyHint.textContent = i18nString(UIStrings.vectorDBApiKeyHint); + vectorDBApiKeyHint.classList.add('settings-hint'); + vectorDBApiKeyDiv.appendChild(vectorDBApiKeyHint); + + const vectorDBApiKeyInput = document.createElement('input'); + vectorDBApiKeyInput.classList.add('settings-input'); + vectorDBApiKeyInput.type = 'text'; + vectorDBApiKeyInput.placeholder = 'root'; + vectorDBApiKeyInput.value = localStorage.getItem(MILVUS_USERNAME_KEY) || 'root'; + vectorDBApiKeyDiv.appendChild(vectorDBApiKeyInput); + + // Milvus Password + const milvusPasswordDiv = document.createElement('div'); + milvusPasswordDiv.classList.add('settings-field'); + vectorDBSection.appendChild(milvusPasswordDiv); + + const milvusPasswordLabel = document.createElement('label'); + milvusPasswordLabel.textContent = i18nString(UIStrings.milvusPassword); + milvusPasswordLabel.classList.add('settings-label'); + milvusPasswordDiv.appendChild(milvusPasswordLabel); + + const milvusPasswordHint = document.createElement('div'); + milvusPasswordHint.textContent = i18nString(UIStrings.milvusPasswordHint); + milvusPasswordHint.classList.add('settings-hint'); + milvusPasswordDiv.appendChild(milvusPasswordHint); + + const milvusPasswordInput = document.createElement('input'); + milvusPasswordInput.classList.add('settings-input'); + milvusPasswordInput.type = 'password'; + milvusPasswordInput.placeholder = 'Milvus (self-hosted) or API token (cloud)'; + milvusPasswordInput.value = localStorage.getItem(MILVUS_PASSWORD_KEY) || 'Milvus'; + milvusPasswordDiv.appendChild(milvusPasswordInput); + + // OpenAI API Key for embeddings + const milvusOpenAIDiv = document.createElement('div'); + milvusOpenAIDiv.classList.add('settings-field'); + vectorDBSection.appendChild(milvusOpenAIDiv); + + const milvusOpenAILabel = document.createElement('label'); + milvusOpenAILabel.textContent = i18nString(UIStrings.milvusOpenAIKey); + milvusOpenAILabel.classList.add('settings-label'); + milvusOpenAIDiv.appendChild(milvusOpenAILabel); + + const milvusOpenAIHint = document.createElement('div'); + milvusOpenAIHint.textContent = i18nString(UIStrings.milvusOpenAIKeyHint); + milvusOpenAIHint.classList.add('settings-hint'); + milvusOpenAIDiv.appendChild(milvusOpenAIHint); + + const milvusOpenAIInput = document.createElement('input'); + milvusOpenAIInput.classList.add('settings-input'); + milvusOpenAIInput.type = 'password'; + milvusOpenAIInput.placeholder = 'sk-...'; + milvusOpenAIInput.value = localStorage.getItem(MILVUS_OPENAI_KEY) || ''; + milvusOpenAIDiv.appendChild(milvusOpenAIInput); + + // Vector DB Collection Name + const vectorDBCollectionDiv = document.createElement('div'); + vectorDBCollectionDiv.classList.add('settings-field'); + vectorDBSection.appendChild(vectorDBCollectionDiv); + + const vectorDBCollectionLabel = document.createElement('label'); + vectorDBCollectionLabel.textContent = i18nString(UIStrings.vectorDBCollection); + vectorDBCollectionLabel.classList.add('settings-label'); + vectorDBCollectionDiv.appendChild(vectorDBCollectionLabel); + + const vectorDBCollectionHint = document.createElement('div'); + vectorDBCollectionHint.textContent = i18nString(UIStrings.vectorDBCollectionHint); + vectorDBCollectionHint.classList.add('settings-hint'); + vectorDBCollectionDiv.appendChild(vectorDBCollectionHint); + + const vectorDBCollectionInput = document.createElement('input'); + vectorDBCollectionInput.classList.add('settings-input'); + vectorDBCollectionInput.type = 'text'; + vectorDBCollectionInput.placeholder = 'bookmarks'; + vectorDBCollectionInput.value = localStorage.getItem(MILVUS_COLLECTION_KEY) || 'bookmarks'; + vectorDBCollectionDiv.appendChild(vectorDBCollectionInput); + + // Test Vector DB Connection Button + const vectorDBTestDiv = document.createElement('div'); + vectorDBTestDiv.classList.add('settings-field', 'test-connection-field'); + vectorDBSection.appendChild(vectorDBTestDiv); + + const vectorDBTestButton = document.createElement('button'); + vectorDBTestButton.classList.add('settings-button', 'test-button'); + vectorDBTestButton.setAttribute('type', 'button'); + vectorDBTestButton.textContent = i18nString(UIStrings.testVectorDBConnection); + vectorDBTestDiv.appendChild(vectorDBTestButton); + + const vectorDBTestStatus = document.createElement('div'); + vectorDBTestStatus.classList.add('settings-status'); + vectorDBTestStatus.style.display = 'none'; + vectorDBTestDiv.appendChild(vectorDBTestStatus); + + // Save Vector DB settings on input change + const saveVectorDBSettings = () => { + localStorage.setItem(MILVUS_ENDPOINT_KEY, vectorDBEndpointInput.value); + localStorage.setItem(MILVUS_USERNAME_KEY, vectorDBApiKeyInput.value); + localStorage.setItem(MILVUS_PASSWORD_KEY, milvusPasswordInput.value); + localStorage.setItem(MILVUS_COLLECTION_KEY, vectorDBCollectionInput.value); + localStorage.setItem(MILVUS_OPENAI_KEY, milvusOpenAIInput.value); + }; + + vectorDBEndpointInput.addEventListener('input', saveVectorDBSettings); + vectorDBApiKeyInput.addEventListener('input', saveVectorDBSettings); + milvusPasswordInput.addEventListener('input', saveVectorDBSettings); + vectorDBCollectionInput.addEventListener('input', saveVectorDBSettings); + milvusOpenAIInput.addEventListener('input', saveVectorDBSettings); + + // Test Vector DB connection + vectorDBTestButton.addEventListener('click', async () => { + const endpoint = vectorDBEndpointInput.value.trim(); + + if (!endpoint) { + vectorDBTestStatus.textContent = 'Please enter an endpoint URL'; + vectorDBTestStatus.style.color = 'var(--color-accent-red)'; + vectorDBTestStatus.style.display = 'block'; + setTimeout(() => { + vectorDBTestStatus.style.display = 'none'; + }, 3000); + return; + } + + vectorDBTestButton.disabled = true; + vectorDBTestStatus.textContent = i18nString(UIStrings.testingVectorDBConnection); + vectorDBTestStatus.style.color = 'var(--color-text-secondary)'; + vectorDBTestStatus.style.display = 'block'; + + try { + // Import and test the Vector DB client + const { VectorDBClient } = await import('../tools/VectorDBClient.js'); + const vectorClient = new VectorDBClient({ + endpoint, + username: vectorDBApiKeyInput.value || 'root', + password: milvusPasswordInput.value || 'Milvus', + collection: vectorDBCollectionInput.value || 'bookmarks', + openaiApiKey: milvusOpenAIInput.value || undefined + }); + + const testResult = await vectorClient.testConnection(); + + if (testResult.success) { + vectorDBTestStatus.textContent = i18nString(UIStrings.vectorDBConnectionSuccess); + vectorDBTestStatus.style.color = 'var(--color-accent-green)'; + } else { + vectorDBTestStatus.textContent = `${i18nString(UIStrings.vectorDBConnectionFailed)}: ${testResult.error}`; + vectorDBTestStatus.style.color = 'var(--color-accent-red)'; + } + } catch (error: any) { + vectorDBTestStatus.textContent = `${i18nString(UIStrings.vectorDBConnectionFailed)}: ${error.message}`; + vectorDBTestStatus.style.color = 'var(--color-accent-red)'; + } finally { + vectorDBTestButton.disabled = false; + setTimeout(() => { + vectorDBTestStatus.style.display = 'none'; + }, 5000); + } + }); + // Add disclaimer section const disclaimerSection = document.createElement('div'); disclaimerSection.classList.add('settings-section', 'disclaimer-section');