diff --git a/.gitignore b/.gitignore index dee8bfd4..2752bcbc 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,6 @@ app/css app/js app/images app/fonts + +# Test files +test-*.js diff --git a/README.md b/README.md index 50c778fb..b0ac8635 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,34 @@ CODELF(变量命名神器) ![image](https://user-images.githubusercontent.com/799578/51435509-a2595d00-1cb3-11e9-8f4e-85ecbc3a2325.png) +DeepSeek AI Search Integration +================= + +CODELF now supports DeepSeek AI for intelligent code search alongside the traditional SearchCode.com results. + +### Configuration + +1. Visit [DeepSeek Platform](https://platform.deepseek.com/api_keys) to get your API key +2. In CODELF, click the search source dropdown and select "DeepSeek AI" +3. Enter your API key when prompted +4. Start searching with AI-powered code suggestions + +### Features + +- **AI-Powered Search**: Get intelligent code suggestions based on your search terms +- **Multiple Language Support**: Search across different programming languages +- **Real-world Examples**: Find practical variable naming examples from AI analysis +- **Seamless Integration**: Switch between SearchCode.com and DeepSeek seamlessly + +### Benefits + +- More contextual and relevant code suggestions +- Better understanding of your search intent +- Complementary results to traditional code search +- Support for natural language queries + +*Note: DeepSeek search requires an API key and is subject to DeepSeek's usage policies and rate limits.* + WIKI ================= [简体中文](https://github.com/unbug/codelf/wiki) diff --git a/__static/app/src/model/SearchcodeModel.js b/__static/app/src/model/SearchcodeModel.js index 975119df..b28d5824 100644 --- a/__static/app/src/model/SearchcodeModel.js +++ b/__static/app/src/model/SearchcodeModel.js @@ -19,7 +19,9 @@ module.exports = new function () { .addPrimaryKey(['id'], true); var persistLangsName = 'codelf_langs_selected'; + var persistSearchSourceName = 'codelf_search_source'; var langs = Util.localStorage.get(persistLangsName), langQuery; + var searchSource = Util.localStorage.get(persistSearchSourceName) || 'searchcode'; var page = 0; var lastVal; var cacheSourceCodes = {}; @@ -43,6 +45,15 @@ module.exports = new function () { return langs; } + this.setSearchSource = function (val) { + searchSource = val || 'searchcode'; + Util.localStorage.set(persistSearchSourceName, searchSource); + } + + this.getSearchSource = function () { + return searchSource; + } + function genLangQuery(val) { if (!!val) { var arr1 = val.replace(/\s+/g, ',').split(','), diff --git a/src/components/SearchBar.js b/src/components/SearchBar.js index ec64d966..573619fd 100644 --- a/src/components/SearchBar.js +++ b/src/components/SearchBar.js @@ -1,5 +1,6 @@ import React, { useEffect, useRef, useState } from 'react'; -import { Dropdown, Icon, Input } from 'semantic-ui-react'; +import { Dropdown, Icon, Input, Modal, Button, Form } from 'semantic-ui-react'; +import * as Configs from '../constants/Configs'; // http://githut.info/ const topProgramLan = [ @@ -28,12 +29,21 @@ const topProgramLan = [ { id: 42, language: 'ActionScript' } ]; +// Search source options +const searchSources = [ + { key: Configs.SEARCH_SOURCES.SEARCHCODE, text: 'SearchCode', value: Configs.SEARCH_SOURCES.SEARCHCODE }, + { key: Configs.SEARCH_SOURCES.DEEPSEEK, text: 'DeepSeek AI', value: Configs.SEARCH_SOURCES.DEEPSEEK } +]; + export default function SearchBar(props) { const inputEl = useRef(null); const inputSize = useInputSize('huge'); const [state, setState] = useState({ lang: props.searchLang || [], - valChanged: false + searchSource: props.searchSource || Configs.SEARCH_SOURCES.SEARCHCODE, + valChanged: false, + showDeepSeekModal: false, + deepSeekApiKey: props.deepSeekApiKey || '' }); function updateState(vals) { @@ -43,7 +53,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.searchSource); inputEl.current.inputRef.current.blur(); updateState({ valChanged: false }); } @@ -66,6 +76,27 @@ export default function SearchBar(props) { state.lang.indexOf(id) === -1 ? handleSelectLang(id) : handleDeselectLang(id); } + function handleSearchSourceChange(e, { value }) { + if (value === Configs.SEARCH_SOURCES.DEEPSEEK && !props.isDeepSeekConfigured) { + updateState({ showDeepSeekModal: true }); + return; + } + updateState({ searchSource: value, valChanged: true }); + props.onSearchSourceChange && props.onSearchSourceChange(value); + } + + function handleDeepSeekApiKeySubmit() { + if (state.deepSeekApiKey.trim()) { + props.onDeepSeekApiKeyChange && props.onDeepSeekApiKeyChange(state.deepSeekApiKey.trim()); + updateState({ + showDeepSeekModal: false, + searchSource: Configs.SEARCH_SOURCES.DEEPSEEK, + valChanged: true + }); + props.onSearchSourceChange && props.onSearchSourceChange(Configs.SEARCH_SOURCES.DEEPSEEK); + } + } + 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 +
+ +
updateState({ valChanged: true })} @@ -118,6 +160,46 @@ export default function SearchBar(props) { Alfred + + {/* DeepSeek API Key Configuration Modal */} + updateState({ showDeepSeekModal: false })} + size='small' + > + Configure DeepSeek API + + + + + updateState({ deepSeekApiKey: e.target.value })} + /> + +

+ + You need a DeepSeek API key to use DeepSeek search. + Get your API key from DeepSeek Platform. + +

+ +
+ + + + +
) } diff --git a/src/constants/Configs.js b/src/constants/Configs.js index 3afc3322..d4890826 100644 --- a/src/constants/Configs.js +++ b/src/constants/Configs.js @@ -4,4 +4,23 @@ const APP_NANE = 'codelf'; const PAGE_URL = Tools.thisPage; const PAGE_PATH = Tools.thisPath; -export { APP_NANE, PAGE_PATH, PAGE_URL } +// Search source types +const SEARCH_SOURCES = { + SEARCHCODE: 'searchcode', + DEEPSEEK: 'deepseek' +}; + +// DeepSeek API configuration +const DEEPSEEK_API_BASE = 'https://api.deepseek.com/v1'; +const DEEPSEEK_API_KEY_STORAGE = `${APP_NANE}_deepseek_api_key`; +const SEARCH_SOURCE_STORAGE = `${APP_NANE}_search_source`; + +export { + APP_NANE, + PAGE_PATH, + PAGE_URL, + SEARCH_SOURCES, + DEEPSEEK_API_BASE, + DEEPSEEK_API_KEY_STORAGE, + SEARCH_SOURCE_STORAGE +} diff --git a/src/containers/MainContainer.js b/src/containers/MainContainer.js index 8c3cbe7b..f72e8c53 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, + searchSource: SearchCodeModel.searchSource, page: SearchCodeModel.page, variableList: SearchCodeModel.variableList, suggestion: SearchCodeModel.suggestion, @@ -32,6 +33,8 @@ const initState = { sourceCodeVisible: false, sourceCodeVariable: null, sourceCodeRepo: null, + deepSeekApiKey: SearchCodeModel.getDeepSeekApiKey(), + isDeepSeekConfigured: SearchCodeModel.isDeepSeekConfigured(), }; function reducer(state, action) { @@ -76,7 +79,7 @@ export default function MainContainer(props) { return () => DDMSModel.offUpdated(handleDDMSModelUpdate); }, []); - const handleSearch = useCallback((val, lang) => { + const handleSearch = useCallback((val, lang, searchSource) => { if (val === null || val === undefined || state.variableRequesting) { return; } @@ -85,13 +88,26 @@ export default function MainContainer(props) { return; } if (val == state.searchValue) { - requestVariable(val, lang); + requestVariable(val, lang, searchSource); } else { - setState({ searchLang: lang }); + setState({ searchLang: lang, searchSource: searchSource }); setTimeout(() => HashHandler.set(val)); // update window.location.hash } }, [state.searchValue, state.variableRequesting]); + const handleSearchSourceChange = useCallback((searchSource) => { + setState({ searchSource: searchSource }); + SearchCodeModel.setSearchSource(searchSource); + }, []); + + const handleDeepSeekApiKeyChange = useCallback((apiKey) => { + SearchCodeModel.setDeepSeekApiKey(apiKey); + setState({ + deepSeekApiKey: apiKey, + isDeepSeekConfigured: SearchCodeModel.isDeepSeekConfigured() + }); + }, []); + const handleOpenSourceCode = useCallback((variable) => { setState({ sourceCodeVariable: variable }); setTimeout(() => requestSourceCode(variable.repoList[0]), 0); @@ -131,18 +147,19 @@ export default function MainContainer(props) { return false; } - function requestVariable(val, lang) { + function requestVariable(val, lang, searchSource) { const langChanged = lang ? (lang.join(',') != state.searchLang.join(',')) : !!state.searchLang; + const sourceChanged = searchSource && searchSource !== state.searchSource; val = decodeURIComponent(val); let page = state.page; - if (val == state.searchValue && !langChanged) { + if (val == state.searchValue && !langChanged && !sourceChanged) { page += 1; } else { page = 0; } setState({ searchValue: val, variableRequesting: true }); - SearchCodeModel.requestVariable(val, page, lang || state.searchLang); - AppModel.analytics('q=' + val); + SearchCodeModel.requestVariable(val, page, lang || state.searchLang, searchSource || state.searchSource); + AppModel.analytics('q=' + val + (searchSource ? '&source=' + searchSource : '')); DDMSModel.postKeyWords(val); updateDocTitle(val); } @@ -175,6 +192,7 @@ export default function MainContainer(props) { variableRequesting: !mutation.variableList, searchValue: SearchCodeModel.searchValue, searchLang: SearchCodeModel.searchLang, + searchSource: SearchCodeModel.searchSource, page: SearchCodeModel.page, variableList: SearchCodeModel.variableList, suggestion: SearchCodeModel.suggestion @@ -186,12 +204,23 @@ export default function MainContainer(props) { sourceCode: SearchCodeModel.sourceCode }); } + if (mutation.searchSource) { + setState({ + searchSource: SearchCodeModel.searchSource + }); + } } return ( - + {state.variableRequesting ? : (state.isError ? : '')} {renderSloganImage()} diff --git a/src/models/SearchCodeModel.js b/src/models/SearchCodeModel.js index 0d227cad..80f98576 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 DeepSeekData from './metadata/DeepSeekData'; import JSONP from '../utils/JSONP'; import Store from './Store'; import AppModel from './AppModel'; @@ -18,6 +19,7 @@ class SearchCodeModel extends BaseModel { isZH: false, searchValue: null, searchLang: SessionStorage.getItem(SEARCH_LANG_KEY), + searchSource: SessionStorage.getItem(Configs.SEARCH_SOURCE_STORAGE) || Configs.SEARCH_SOURCES.SEARCHCODE, page: 0, variableList: [], suggestion: [], @@ -34,9 +36,12 @@ class SearchCodeModel extends BaseModel { } //search code by query - async requestVariable(val, page, lang) { + async requestVariable(val, page, lang, searchSource) { lang = lang || this.searchLang; + searchSource = searchSource || this.searchSource; SessionStorage.setItem(SEARCH_LANG_KEY, lang); // persist lang + SessionStorage.setItem(Configs.SEARCH_SOURCE_STORAGE, searchSource); // persist search source + if (val !== undefined && val !== null) { val = val.trim().replace(/\s+/ig, ' '); // filter spaces } @@ -59,19 +64,46 @@ class SearchCodeModel extends BaseModel { page: page, variableList: [...this.variableList, []], searchLang: lang, + searchSource: searchSource, suggestion: suggestion, isZH: isZH || this.isZH }); } } - const cacheId = Tools.MD5(q + page + (lang && lang.length ? lang.join(',') : '')); + + const cacheId = Tools.MD5(q + page + (lang && lang.length ? lang.join(',') : '') + searchSource); const cache = this._variableListStore.get(cacheId); if (cache) { this.update(cache); return; } + + // Route to appropriate search provider + if (searchSource === Configs.SEARCH_SOURCES.DEEPSEEK) { + await this._searchWithDeepSeek(val, q, page, lang, suggestion, isZH, cacheId); + } else { + await this._searchWithSearchCode(val, q, page, lang, suggestion, isZH, cacheId); + } + } + + //get source code by id + requestSourceCode(id) { + const cache = this._sourceCodeStore.get(id); + if (cache) { + this.update({ sourceCode: cache }); + return; + } + id && fetch('https://searchcode.com/api/result/' + id + '/') + .then(res => res.json()) + .then(data => { + this._sourceCodeStore.save(id, data.code); + this.update({ sourceCode: data.code }); + }); + } + + // Search with searchcode.com (original implementation) + async _searchWithSearchCode(val, q, page, lang, suggestion, isZH, cacheId) { // multiple val separate with '+' - // const url = `//searchcode.com/api/codesearch_I/?q=${q.replace(' ', '+')}&p=${page}&per_page=42${lang.length ? ('&lan=' + lang.join(',')) : ''}`; const langParams = lang.length ? ('&lan=' + lang.join(',').split(',').join('&lan=')) : ''; const qParams = q.replace(' ', '+'); const url = `//searchcode.com/api/jsonp_codesearch_I/?callback=?&q=${qParams}&p=${page}&per_page=42${langParams}`; @@ -81,6 +113,7 @@ class SearchCodeModel extends BaseModel { page: page, variableList: [...this._data.variableList, this._parseVariableList(data.results, q)], searchLang: lang, + searchSource: this.searchSource, suggestion: suggestion, isZH: isZH || this.isZH }; @@ -99,6 +132,7 @@ class SearchCodeModel extends BaseModel { page: page, variableList: [...this.variableList, []], searchLang: lang, + searchSource: this.searchSource, suggestion: suggestion, isZH: isZH || this.isZH }); @@ -106,19 +140,35 @@ class SearchCodeModel extends BaseModel { }); } - //get source code by id - requestSourceCode(id) { - const cache = this._sourceCodeStore.get(id); - if (cache) { - this.update({ sourceCode: cache }); - return; - } - id && fetch('https://searchcode.com/api/result/' + id + '/') - .then(res => res.json()) - .then(data => { - this._sourceCodeStore.save(id, data.code); - this.update({ sourceCode: data.code }); + // Search with DeepSeek API + async _searchWithDeepSeek(val, q, page, lang, suggestion, isZH, cacheId) { + try { + const data = await DeepSeekData.searchCode(q, page, lang); + const cdata = { + searchValue: val, + page: page, + variableList: [...this._data.variableList, this._parseVariableList(data.results, q)], + searchLang: lang, + searchSource: this.searchSource, + suggestion: suggestion, + isZH: isZH || this.isZH + }; + this.update(cdata); + this._variableListStore.save(cacheId, cdata); + } catch (error) { + console.error('DeepSeek search failed:', error); + // Fallback to empty results with error indicator + this.update({ + searchValue: val, + page: page, + variableList: [...this.variableList, []], + searchLang: lang, + searchSource: this.searchSource, + suggestion: suggestion, + isZH: isZH || this.isZH, + error: error.message }); + } } getKeyWordReg(keyword) { @@ -220,6 +270,10 @@ class SearchCodeModel extends BaseModel { return this._data.searchLang || SessionStorage.getItem(SEARCH_LANG_KEY) || []; } + get searchSource() { + return this._data.searchSource || SessionStorage.getItem(Configs.SEARCH_SOURCE_STORAGE) || Configs.SEARCH_SOURCES.SEARCHCODE; + } + get page() { return this._data.page; } @@ -239,6 +293,27 @@ class SearchCodeModel extends BaseModel { get sourceCode() { return this._data.sourceCode; } + + // DeepSeek configuration methods + setDeepSeekApiKey(apiKey) { + DeepSeekData.setApiKey(apiKey); + } + + getDeepSeekApiKey() { + return DeepSeekData.getApiKey(); + } + + isDeepSeekConfigured() { + return DeepSeekData.isConfigured(); + } + + setSearchSource(source) { + if (Object.values(Configs.SEARCH_SOURCES).includes(source)) { + this._data.searchSource = source; + SessionStorage.setItem(Configs.SEARCH_SOURCE_STORAGE, source); + this.update({ searchSource: source }); + } + } } export default new SearchCodeModel(); diff --git a/src/models/metadata/DeepSeekData.js b/src/models/metadata/DeepSeekData.js new file mode 100644 index 00000000..f375d76b --- /dev/null +++ b/src/models/metadata/DeepSeekData.js @@ -0,0 +1,236 @@ +import Store from '../Store'; +import * as Tools from '../../utils/Tools'; +import { LocalStorage } from '../../utils/LocalStorage'; +import AppModel from '../AppModel'; +import * as Configs from '../../constants/Configs'; + +/** + * DeepSeek API integration for code search + * Uses OpenAI-compatible API with Bearer token authentication + */ + +class DeepSeekData { + constructor() { + this._store = new Store(Infinity, { + persistence: 'session', + persistenceKey: AppModel.genPersistenceKey('deepseek_search_key') + }); + } + + /** + * Search code using DeepSeek API + * @param {string} query - search query + * @param {number} page - page number + * @param {Array} languages - programming languages filter + * @returns {Promise} search results + */ + async searchCode(query, page = 0, languages = []) { + const apiKey = this.getApiKey(); + if (!apiKey) { + throw new Error('DeepSeek API key not configured'); + } + + const cacheId = Tools.MD5(`deepseek_${query}_${page}_${languages.join(',')}`); + const cache = this._store.get(cacheId); + if (cache) { + return cache; + } + + // Prepare search prompt for DeepSeek + const langFilter = languages.length > 0 ? ` in ${languages.join(', ')} programming language(s)` : ''; + const prompt = `Search for variable names and code examples related to "${query}"${langFilter}. Provide real-world usage examples from popular repositories. Format the response as a JSON array with items containing: id, name, repo, language, lines (code snippets), and url fields.`; + + try { + const response = await fetch(`${Configs.DEEPSEEK_API_BASE}/chat/completions`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${apiKey}` + }, + body: JSON.stringify({ + model: 'deepseek-coder', + messages: [ + { + role: 'user', + content: prompt + } + ], + max_tokens: 2048, + temperature: 0.3 + }) + }); + + if (!response.ok) { + throw new Error(`DeepSeek API request failed: ${response.status} ${response.statusText}`); + } + + const data = await response.json(); + const content = data.choices?.[0]?.message?.content; + + if (!content) { + throw new Error('Invalid response from DeepSeek API'); + } + + // Parse the response and adapt to searchcode.com format + const results = this._parseDeepSeekResponse(content, query); + + const searchResult = { + results: results, + total: results.length, + page: page, + query: query + }; + + this._store.save(cacheId, searchResult); + return searchResult; + + } catch (error) { + console.error('DeepSeek API search failed:', error); + throw error; + } + } + + /** + * Parse DeepSeek response and adapt to searchcode.com format + * @param {string} content - raw response content + * @param {string} query - original search query + * @returns {Array} formatted results + */ + _parseDeepSeekResponse(content, query) { + try { + // Try to extract JSON from the response + let jsonData; + try { + jsonData = JSON.parse(content); + } catch (e) { + // If not valid JSON, try to extract JSON from markdown code blocks + const jsonMatch = content.match(/```(?:json)?\s*(\[[\s\S]*?\])\s*```/); + if (jsonMatch) { + jsonData = JSON.parse(jsonMatch[1]); + } else { + // Fallback: create synthetic results based on content + return this._createSyntheticResults(content, query); + } + } + + if (!Array.isArray(jsonData)) { + return this._createSyntheticResults(content, query); + } + + // Adapt to searchcode.com format + return jsonData.map((item, index) => ({ + id: `deepseek_${Tools.MD5(JSON.stringify(item))}_${index}`, + name: item.name || `${query}_example_${index + 1}`, + repo: item.repo || 'https://github.com/example/repository', + language: item.language || 'JavaScript', + lines: item.lines || { 1: item.code || `// Example usage of ${query}` }, + url: item.url || `https://github.com/example/repository/blob/main/example.js`, + filename: item.filename || `example.${this._getFileExtension(item.language || 'JavaScript')}`, + score: 100 - index // Higher score for earlier results + })); + + } catch (error) { + console.error('Failed to parse DeepSeek response:', error); + return this._createSyntheticResults(content, query); + } + } + + /** + * Create synthetic results when parsing fails + * @param {string} content - response content + * @param {string} query - search query + * @returns {Array} synthetic results + */ + _createSyntheticResults(content, query) { + const lines = content.split('\n').filter(line => line.trim()); + const codeLines = lines.filter(line => + line.includes(query) || + line.match(/[a-zA-Z_][a-zA-Z0-9_]*/) || + line.includes('function') || + line.includes('const') || + line.includes('var') || + line.includes('let') + ); + + if (codeLines.length === 0) { + return [{ + id: `deepseek_${Tools.MD5(content)}_0`, + name: `${query}_example`, + repo: 'https://github.com/deepseek-ai/DeepSeek-Coder', + language: 'JavaScript', + lines: { 1: `// DeepSeek suggested: ${query}` }, + url: 'https://github.com/deepseek-ai/DeepSeek-Coder', + filename: 'example.js', + score: 100 + }]; + } + + return codeLines.slice(0, 10).map((line, index) => ({ + id: `deepseek_${Tools.MD5(line)}_${index}`, + name: `${query}_example_${index + 1}`, + repo: 'https://github.com/deepseek-ai/DeepSeek-Coder', + language: 'JavaScript', + lines: { [index + 1]: line }, + url: 'https://github.com/deepseek-ai/DeepSeek-Coder', + filename: `example_${index + 1}.js`, + score: 100 - index + })); + } + + /** + * Get file extension for language + * @param {string} language - programming language + * @returns {string} file extension + */ + _getFileExtension(language) { + const extensions = { + 'JavaScript': 'js', + 'TypeScript': 'ts', + 'Python': 'py', + 'Java': 'java', + 'C++': 'cpp', + 'C': 'c', + 'C#': 'cs', + 'Go': 'go', + 'Rust': 'rs', + 'PHP': 'php', + 'Ruby': 'rb', + 'Swift': 'swift', + 'Kotlin': 'kt', + 'Scala': 'scala', + 'HTML': 'html', + 'CSS': 'css' + }; + return extensions[language] || 'txt'; + } + + /** + * Set DeepSeek API key + * @param {string} apiKey - API key + */ + setApiKey(apiKey) { + if (apiKey) { + LocalStorage.setItem(Configs.DEEPSEEK_API_KEY_STORAGE, apiKey); + } else { + LocalStorage.removeItem(Configs.DEEPSEEK_API_KEY_STORAGE); + } + } + + /** + * Get DeepSeek API key + * @returns {string|null} API key + */ + getApiKey() { + return LocalStorage.getItem(Configs.DEEPSEEK_API_KEY_STORAGE); + } + + /** + * Check if DeepSeek is configured + * @returns {boolean} true if API key is set + */ + isConfigured() { + return !!this.getApiKey(); + } +} + +export default new DeepSeekData(); \ No newline at end of file diff --git a/src/utils/LocalStorage.js b/src/utils/LocalStorage.js index 5d097ad6..f1edc03c 100644 --- a/src/utils/LocalStorage.js +++ b/src/utils/LocalStorage.js @@ -39,10 +39,18 @@ class Storage { // todo } } + + removeItem(key) { + try { + this._store.removeItem(key); + } catch (e) { + // todo + } + } } const LocalStorage = new Storage(window.localStorage); const SessionStorage = new Storage(window.sessionStorage); -export { SessionStorage }; +export { SessionStorage, LocalStorage }; export default LocalStorage;