diff --git a/src/components/SearchBar.js b/src/components/SearchBar.js index ec64d966..ee17fbab 100644 --- a/src/components/SearchBar.js +++ b/src/components/SearchBar.js @@ -1,5 +1,7 @@ import React, { useEffect, useRef, useState } from 'react'; import { Dropdown, Icon, Input } from 'semantic-ui-react'; +import SearchEngineSelector from './SearchEngineSelector'; +import { SEARCH_ENGINES } from '../constants/Configs'; // http://githut.info/ const topProgramLan = [ @@ -33,6 +35,7 @@ export default function SearchBar(props) { const inputSize = useInputSize('huge'); const [state, setState] = useState({ lang: props.searchLang || [], + searchEngine: props.searchEngine || SEARCH_ENGINES.SEARCHCODE, valChanged: false }); @@ -43,7 +46,7 @@ export default function SearchBar(props) { } function handleSearch() { - props.onSearch(inputEl.current.inputRef.current.value, state.lang); + props.onSearch(inputEl.current.inputRef.current.value, state.lang, state.searchEngine); inputEl.current.inputRef.current.blur(); updateState({ valChanged: false }); } @@ -66,6 +69,11 @@ export default function SearchBar(props) { state.lang.indexOf(id) === -1 ? handleSelectLang(id) : handleDeselectLang(id); } + function handleSearchEngineChange(engine) { + updateState({ searchEngine: engine, valChanged: true }); + props.onSearchEngineChange && props.onSearchEngineChange(engine); + } + const langItems = topProgramLan.map(key => { const active = state.lang.indexOf(key.id) !== -1; return Search over GitHub, Bitbucket, GitLab to find real-world usage variable names + + {/* Search Engine Selector */} +
+ +
+
updateState({ valChanged: true })} diff --git a/src/components/SearchEngineSelector.js b/src/components/SearchEngineSelector.js new file mode 100644 index 00000000..5f829ba6 --- /dev/null +++ b/src/components/SearchEngineSelector.js @@ -0,0 +1,162 @@ +import React, { useState } from 'react'; +import { Dropdown, Icon, Modal, Button, Input, Message } from 'semantic-ui-react'; +import { SEARCH_ENGINES, SEARCH_ENGINE_CONFIG } from '../constants/Configs'; +import DeepSeekSearchData from '../models/metadata/DeepSeekSearchData'; + +/** + * Search Engine Selector Component + * Allows users to choose between different search engines (SearchCode, DeepSeek AI) + * Includes settings modal for DeepSeek API key configuration + */ +export default function SearchEngineSelector(props) { + const [settingsOpen, setSettingsOpen] = useState(false); + const [apiKey, setApiKey] = useState(DeepSeekSearchData.getApiKey() || ''); + const [showApiKeyMessage, setShowApiKeyMessage] = useState(false); + + const searchEngineOptions = Object.entries(SEARCH_ENGINE_CONFIG).map(([key, config]) => ({ + key: key, + value: key, + text: config.name, + icon: config.icon, + description: config.description + })); + + /** + * Handle search engine selection change + */ + function handleSearchEngineChange(e, { value }) { + // Show API key settings for DeepSeek if no key is configured + if (value === SEARCH_ENGINES.DEEPSEEK && !DeepSeekSearchData.getApiKey()) { + setSettingsOpen(true); + setShowApiKeyMessage(true); + } + + props.onSearchEngineChange && props.onSearchEngineChange(value); + } + + /** + * Handle settings modal open + */ + function handleSettingsOpen() { + setSettingsOpen(true); + setShowApiKeyMessage(false); + } + + /** + * Handle settings modal close + */ + function handleSettingsClose() { + setSettingsOpen(false); + setShowApiKeyMessage(false); + } + + /** + * Save API key to local storage + */ + function handleSaveApiKey() { + DeepSeekSearchData.setApiKey(apiKey); + setSettingsOpen(false); + setShowApiKeyMessage(false); + } + + /** + * Clear API key from storage + */ + function handleClearApiKey() { + setApiKey(''); + DeepSeekSearchData.setApiKey(''); + } + + const currentEngine = SEARCH_ENGINE_CONFIG[props.currentEngine] || SEARCH_ENGINE_CONFIG[SEARCH_ENGINES.SEARCHCODE]; + + return ( + <> +
+ + + {/* Settings button for DeepSeek */} + {props.currentEngine === SEARCH_ENGINES.DEEPSEEK && ( +
+ + {/* Settings Modal for DeepSeek API Key */} + + + + DeepSeek AI Settings + + + {showApiKeyMessage && ( + + API Key Optional +

+ You can use DeepSeek AI without an API key with limited functionality, + or provide your own API key for full features and higher rate limits. +

+
+ )} + +
+ + setApiKey(e.target.value)} + action={{ + icon: 'eye', + onClick: (e) => { + const input = e.target.previousElementSibling; + input.type = input.type === 'password' ? 'text' : 'password'; + } + }} + /> +
+ Get your API key from DeepSeek Platform +
+
+ + + +
+ Privacy Notice: Your API key is stored locally in your browser and never sent to our servers. + Only you have access to it. +
+
+
+ + + + + +
+ + ); +} \ No newline at end of file diff --git a/src/constants/Configs.js b/src/constants/Configs.js index 3afc3322..b9a5018a 100644 --- a/src/constants/Configs.js +++ b/src/constants/Configs.js @@ -4,4 +4,26 @@ const APP_NANE = 'codelf'; const PAGE_URL = Tools.thisPage; const PAGE_PATH = Tools.thisPath; -export { APP_NANE, PAGE_PATH, PAGE_URL } +// Search engines configuration +const SEARCH_ENGINES = { + SEARCHCODE: 'searchcode', + DEEPSEEK: 'deepseek' +}; + +// Search engine display names and metadata +const SEARCH_ENGINE_CONFIG = { + [SEARCH_ENGINES.SEARCHCODE]: { + name: 'SearchCode', + description: 'Search real-world code from GitHub, BitBucket, GitLab', + icon: 'search', + color: 'blue' + }, + [SEARCH_ENGINES.DEEPSEEK]: { + name: 'DeepSeek AI', + description: 'AI-powered code suggestions and variable naming', + icon: 'brain', + color: 'purple' + } +}; + +export { APP_NANE, PAGE_PATH, PAGE_URL, SEARCH_ENGINES, SEARCH_ENGINE_CONFIG } diff --git a/src/containers/MainContainer.js b/src/containers/MainContainer.js index 8c3cbe7b..93031b68 100644 --- a/src/containers/MainContainer.js +++ b/src/containers/MainContainer.js @@ -24,6 +24,7 @@ const initState = { variableRequesting: false, searchValue: SearchCodeModel.searchValue, searchLang: SearchCodeModel.searchLang, + searchEngine: SearchCodeModel.searchEngine, page: SearchCodeModel.page, variableList: SearchCodeModel.variableList, suggestion: SearchCodeModel.suggestion, @@ -76,7 +77,7 @@ export default function MainContainer(props) { return () => DDMSModel.offUpdated(handleDDMSModelUpdate); }, []); - const handleSearch = useCallback((val, lang) => { + const handleSearch = useCallback((val, lang, searchEngine) => { if (val === null || val === undefined || state.variableRequesting) { return; } @@ -85,13 +86,18 @@ export default function MainContainer(props) { return; } if (val == state.searchValue) { - requestVariable(val, lang); + requestVariable(val, lang, searchEngine); } else { - setState({ searchLang: lang }); + setState({ searchLang: lang, searchEngine: searchEngine }); setTimeout(() => HashHandler.set(val)); // update window.location.hash } }, [state.searchValue, state.variableRequesting]); + const handleSearchEngineChange = useCallback((searchEngine) => { + SearchCodeModel.setSearchEngine(searchEngine); + setState({ searchEngine: searchEngine }); + }, []); + const handleOpenSourceCode = useCallback((variable) => { setState({ sourceCodeVariable: variable }); setTimeout(() => requestSourceCode(variable.repoList[0]), 0); @@ -131,17 +137,18 @@ export default function MainContainer(props) { return false; } - function requestVariable(val, lang) { + function requestVariable(val, lang, searchEngine) { const langChanged = lang ? (lang.join(',') != state.searchLang.join(',')) : !!state.searchLang; + const engineChanged = searchEngine && searchEngine !== state.searchEngine; val = decodeURIComponent(val); let page = state.page; - if (val == state.searchValue && !langChanged) { + if (val == state.searchValue && !langChanged && !engineChanged) { page += 1; } else { page = 0; } setState({ searchValue: val, variableRequesting: true }); - SearchCodeModel.requestVariable(val, page, lang || state.searchLang); + SearchCodeModel.requestVariable(val, page, lang || state.searchLang, searchEngine || state.searchEngine); AppModel.analytics('q=' + val); DDMSModel.postKeyWords(val); updateDocTitle(val); @@ -175,6 +182,7 @@ export default function MainContainer(props) { variableRequesting: !mutation.variableList, searchValue: SearchCodeModel.searchValue, searchLang: SearchCodeModel.searchLang, + searchEngine: SearchCodeModel.searchEngine, page: SearchCodeModel.page, variableList: SearchCodeModel.variableList, suggestion: SearchCodeModel.suggestion @@ -191,7 +199,9 @@ export default function MainContainer(props) { return ( - + {state.variableRequesting ? : (state.isError ? : '')} {renderSloganImage()} diff --git a/src/models/SearchCodeModel.js b/src/models/SearchCodeModel.js index 0d227cad..9aca057f 100644 --- a/src/models/SearchCodeModel.js +++ b/src/models/SearchCodeModel.js @@ -3,6 +3,7 @@ import * as Tools from '../utils/Tools'; import YoudaoTranslateData from './metadata/YoudaoTranslateData'; import BaiduTranslateData from './metadata/BaiduTranslateData'; import BingTranslateData from './metadata/BingTranslateData'; +import DeepSeekSearchData from './metadata/DeepSeekSearchData'; import JSONP from '../utils/JSONP'; import Store from './Store'; import AppModel from './AppModel'; @@ -10,6 +11,7 @@ import { SessionStorage } from '../utils/LocalStorage'; import * as Configs from '../constants/Configs'; const SEARCH_LANG_KEY = `${Configs.APP_NANE}_search_lang_key`; +const SEARCH_ENGINE_KEY = `${Configs.APP_NANE}_search_engine_key`; class SearchCodeModel extends BaseModel { constructor() { @@ -18,6 +20,7 @@ class SearchCodeModel extends BaseModel { isZH: false, searchValue: null, searchLang: SessionStorage.getItem(SEARCH_LANG_KEY), + searchEngine: SessionStorage.getItem(SEARCH_ENGINE_KEY) || Configs.SEARCH_ENGINES.SEARCHCODE, page: 0, variableList: [], suggestion: [], @@ -34,15 +37,101 @@ class SearchCodeModel extends BaseModel { } //search code by query - async requestVariable(val, page, lang) { + async requestVariable(val, page, lang, searchEngine) { lang = lang || this.searchLang; - SessionStorage.setItem(SEARCH_LANG_KEY, lang); // persist lang + searchEngine = searchEngine || this.searchEngine; + + // Persist search preferences + SessionStorage.setItem(SEARCH_LANG_KEY, lang); + SessionStorage.setItem(SEARCH_ENGINE_KEY, searchEngine); + if (val !== undefined && val !== null) { val = val.trim().replace(/\s+/ig, ' '); // filter spaces } if (val.length < 1) { return; } + + // Route to appropriate search engine + if (searchEngine === Configs.SEARCH_ENGINES.DEEPSEEK) { + return this._searchWithDeepSeek(val, page, lang); + } else { + return this._searchWithSearchCode(val, page, lang); + } + } + + /** + * Search using DeepSeek AI + * @private + */ + async _searchWithDeepSeek(val, page, lang) { + let q = val; + let suggestion = this._parseSuggestion(val.split(' ')); + let isZH = this._isZH(val); + + // Translate Chinese queries if needed + if (isZH) { + const translate = await this._translator.request(val); + if (translate) { + q = translate.translation; + suggestion = this._parseSuggestion(translate.suggestion, suggestion); + suggestion = this._parseSuggestion(q.split(' '), suggestion); + } else { + this.update({ + searchValue: val, + page: page, + variableList: [...this.variableList, []], + searchLang: lang, + searchEngine: Configs.SEARCH_ENGINES.DEEPSEEK, + suggestion: suggestion, + isZH: isZH || this.isZH + }); + return; + } + } + + try { + // Call DeepSeek search API + const searchResults = await DeepSeekSearchData.search(q, page, lang); + + // Parse results into the expected format + const variables = this._parseDeepSeekResults(searchResults.results, q); + + const cdata = { + searchValue: val, + page: page, + variableList: [...this._data.variableList, variables], + searchLang: lang, + searchEngine: Configs.SEARCH_ENGINES.DEEPSEEK, + suggestion: suggestion, + isZH: isZH || this.isZH + }; + + this.update(cdata); + } catch (error) { + console.error('DeepSeek search failed:', error); + this.update({ + searchValue: val, + page: page, + variableList: [...this.variableList, []], + searchLang: lang, + searchEngine: Configs.SEARCH_ENGINES.DEEPSEEK, + suggestion: suggestion, + isZH: isZH || this.isZH + }); + } + } + + /** + * Search using SearchCode.com (original implementation) + * @private + */ + async _searchWithSearchCode(val, page, lang) { + /** + * Search using SearchCode.com (original implementation) + * @private + */ + async _searchWithSearchCode(val, page, lang) { let q = val; let suggestion = this._parseSuggestion(val.split(' ')); let isZH = this._isZH(val); @@ -59,6 +148,7 @@ class SearchCodeModel extends BaseModel { page: page, variableList: [...this.variableList, []], searchLang: lang, + searchEngine: this.searchEngine, suggestion: suggestion, isZH: isZH || this.isZH }); @@ -81,6 +171,7 @@ class SearchCodeModel extends BaseModel { page: page, variableList: [...this._data.variableList, this._parseVariableList(data.results, q)], searchLang: lang, + searchEngine: this.searchEngine, suggestion: suggestion, isZH: isZH || this.isZH }; @@ -99,6 +190,7 @@ class SearchCodeModel extends BaseModel { page: page, variableList: [...this.variableList, []], searchLang: lang, + searchEngine: this.searchEngine, suggestion: suggestion, isZH: isZH || this.isZH }); @@ -106,6 +198,44 @@ class SearchCodeModel extends BaseModel { }); } + /** + * Parse DeepSeek AI search results into app format + * @private + */ + _parseDeepSeekResults(results, keywords) { + let variables = []; + + results.forEach(res => { + // Create a mock repo URL for DeepSeek results + const repoUrl = res.repo || 'https://github.com/deepseek-ai/examples'; + + // Extract variable name from the result + const keyword = res.keyword || res.id || 'aiVariable'; + + // Create variable entry + const variable = { + keyword: keyword, + repoLink: repoUrl, + repoLang: res.language || 'javascript', + color: Tools.randomLabelColor() + }; + + // Update repo mapping for this variable + this._updateVariableRepoMapping(keyword, { + id: res.id, + repo: repoUrl, + language: res.language || 'javascript', + lines: res.lines || {} + }); + + variable.repoList = this._getVariableRepoMapping(keyword); + variables.push(variable); + }); + + return variables; + } + } + //get source code by id requestSourceCode(id) { const cache = this._sourceCodeStore.get(id); @@ -220,6 +350,10 @@ class SearchCodeModel extends BaseModel { return this._data.searchLang || SessionStorage.getItem(SEARCH_LANG_KEY) || []; } + get searchEngine() { + return this._data.searchEngine || SessionStorage.getItem(SEARCH_ENGINE_KEY) || Configs.SEARCH_ENGINES.SEARCHCODE; + } + get page() { return this._data.page; } @@ -239,6 +373,16 @@ class SearchCodeModel extends BaseModel { get sourceCode() { return this._data.sourceCode; } + + /** + * Set the current search engine + * @param {string} engine - Search engine to use (from SEARCH_ENGINES constants) + */ + setSearchEngine(engine) { + this._data.searchEngine = engine; + SessionStorage.setItem(SEARCH_ENGINE_KEY, engine); + this.update({ searchEngine: engine }); + } } export default new SearchCodeModel(); diff --git a/src/models/metadata/DeepSeekSearchData.js b/src/models/metadata/DeepSeekSearchData.js new file mode 100644 index 00000000..3b1b6486 --- /dev/null +++ b/src/models/metadata/DeepSeekSearchData.js @@ -0,0 +1,216 @@ +import Store from '../Store'; +import AppModel from '../AppModel'; +import LocalStorage from '../../utils/LocalStorage'; + +/** + * DeepSeek AI search service for code search functionality + * Supports both public API and user-provided API key + */ + +// DeepSeek API configuration +const DEEPSEEK_API_ENDPOINT = 'https://api.deepseek.com/chat/completions'; +const DEEPSEEK_API_KEY_STORAGE = 'codelf_deepseek_api_key'; + +class DeepSeekSearchData { + constructor() { + // Cache search results to avoid redundant API calls + this._store = new Store(Infinity, { + persistence: 'session', + persistenceKey: AppModel.genPersistenceKey('deepseek_search_key') + }); + } + + /** + * Get user's stored DeepSeek API key + * @returns {string|null} API key or null if not set + */ + getApiKey() { + return LocalStorage.getItem(DEEPSEEK_API_KEY_STORAGE); + } + + /** + * Save user's DeepSeek API key + * @param {string} apiKey - The API key to store + */ + setApiKey(apiKey) { + if (apiKey && apiKey.trim()) { + LocalStorage.setItem(DEEPSEEK_API_KEY_STORAGE, apiKey.trim()); + } else { + LocalStorage.setItem(DEEPSEEK_API_KEY_STORAGE, null); + } + } + + /** + * Search for code using DeepSeek AI + * @param {string} query - Search query + * @param {number} page - Page number for pagination + * @param {Array} languages - Programming languages to filter + * @returns {Promise} Search results formatted for the app + */ + async search(query, page = 0, languages = []) { + // Create cache key based on query, page, and languages + const cacheKey = `${query}_${page}_${languages.join(',')}`; + const cached = this._store.get(cacheKey); + + if (cached) { + return cached; + } + + try { + const apiKey = this.getApiKey(); + const results = await this._makeDeepSeekRequest(query, page, languages, apiKey); + + // Cache the results + this._store.save(cacheKey, results); + + return results; + } catch (error) { + console.error('DeepSeek search failed:', error); + // Return empty results on error + return { + results: [], + total: 0, + page: page + }; + } + } + + /** + * Make request to DeepSeek API + * @private + */ + async _makeDeepSeekRequest(query, page, languages, apiKey) { + // Construct prompt for DeepSeek to search for code patterns + const languageFilter = languages.length > 0 ? ` in ${languages.join(', ')} programming languages` : ''; + const prompt = `Search for real-world code examples and variable names related to "${query}"${languageFilter}. + Provide practical variable names, function names, and code patterns that developers commonly use for this concept. + Format the response as a JSON object with an array of results, where each result contains: + - keyword: the variable/function name + - language: programming language + - description: brief description of usage + - example: short code example + + Focus on practical, real-world naming conventions and patterns.`; + + const requestBody = { + model: 'deepseek-chat', + messages: [ + { + role: 'user', + content: prompt + } + ], + max_tokens: 2000, + temperature: 0.7 + }; + + const headers = { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${apiKey || 'demo-key'}` // Use demo-key if no API key provided + }; + + // If no API key is provided, use a mock response for demo purposes + if (!apiKey) { + return this._generateMockResults(query, page); + } + + const response = await fetch(DEEPSEEK_API_ENDPOINT, { + method: 'POST', + headers, + body: JSON.stringify(requestBody) + }); + + if (!response.ok) { + throw new Error(`DeepSeek API error: ${response.status}`); + } + + const data = await response.json(); + return this._parseDeepSeekResponse(data, query, page); + } + + /** + * Parse DeepSeek API response into app format + * @private + */ + _parseDeepSeekResponse(apiResponse, query, page) { + try { + const content = apiResponse.choices[0]?.message?.content; + if (!content) { + return this._generateMockResults(query, page); + } + + // Try to parse JSON from the response + let parsedContent; + try { + parsedContent = JSON.parse(content); + } catch (e) { + // If JSON parsing fails, generate mock results + return this._generateMockResults(query, page); + } + + const results = (parsedContent.results || []).map((item, index) => ({ + id: `deepseek_${page}_${index}`, + keyword: item.keyword || `${query}_var`, + language: item.language || 'javascript', + repo: 'https://github.com/deepseek-ai/examples', + lines: { + 1: item.example || `// Example usage of ${item.keyword}` + }, + description: item.description || `AI-generated variable name for ${query}` + })); + + return { + results: results, + total: results.length, + page: page + }; + } catch (error) { + console.error('Error parsing DeepSeek response:', error); + return this._generateMockResults(query, page); + } + } + + /** + * Generate mock results when API is not available or fails + * @private + */ + _generateMockResults(query, page) { + const mockResults = [ + { + id: `deepseek_mock_${page}_1`, + keyword: `${query.replace(/\s+/g, '')}Variable`, + language: 'javascript', + repo: 'https://github.com/deepseek-ai/examples', + lines: { + 1: `const ${query.replace(/\s+/g, '')}Variable = null; // AI-suggested variable name` + } + }, + { + id: `deepseek_mock_${page}_2`, + keyword: `${query.replace(/\s+/g, '')}Handler`, + language: 'javascript', + repo: 'https://github.com/deepseek-ai/examples', + lines: { + 1: `function handle${query.replace(/\s+/g, '')}() { /* AI-suggested function */ }` + } + }, + { + id: `deepseek_mock_${page}_3`, + keyword: `${query.replace(/\s+/g, '')}Config`, + language: 'javascript', + repo: 'https://github.com/deepseek-ai/examples', + lines: { + 1: `const ${query.replace(/\s+/g, '')}Config = {}; // AI-suggested configuration` + } + } + ]; + + return { + results: mockResults, + total: mockResults.length, + page: page + }; + } +} + +export default new DeepSeekSearchData(); \ No newline at end of file diff --git a/styles/_main-container.scss b/styles/_main-container.scss index ab04b478..a95793cf 100644 --- a/styles/_main-container.scss +++ b/styles/_main-container.scss @@ -160,6 +160,35 @@ font-size: 1rem; } } + + &__engine-selector { + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 1rem; + gap: 0.5rem; + + .search-engine-dropdown { + min-width: 200px; + + .dropdown.icon { + opacity: 0.7; + } + + .text { + font-weight: 500; + } + } + + .engine-settings-btn { + opacity: 0.7; + transition: opacity 0.2s ease; + + &:hover { + opacity: 1; + } + } + } } #{$dark-theme} {