Skip to content

Commit

Permalink
feat: chat widget for DocsGPT (#606)
Browse files Browse the repository at this point in the history
* feat: demo of DocsGPTChatWidget has been verified

* add message loading animation & freeze input when waiting for answer

* check if docs is availabel for the current repo

* add prompts for requesting for new docs

* also powered by X-lab

* handle repo switching

* support history & fix bugs

* add custom styles

* support Dark and Light themes

* support to enable/disable the feature

* update issue link

* tell users that the feature can be toggled

* workaround to change emoji picker color

* disable emoji
  • Loading branch information
tyn1998 authored Mar 15, 2023
1 parent aaf61c5 commit 947b7ee
Show file tree
Hide file tree
Showing 11 changed files with 1,423 additions and 4 deletions.
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"jquery": "^3.6.0",
"office-ui-fabric-react": "^7.183.0",
"react": "^17.0.2",
"react-chat-widget": "^3.1.4",
"react-dom": "^17.0.2",
"react-hot-loader": "^4.13.0",
"react-tooltip": "^4.2.21"
Expand Down Expand Up @@ -74,5 +75,9 @@
"webpack": "^5.76.0",
"webpack-cli": "^4.9.2",
"webpack-dev-server": "^4.3.1"
},
"resolutions": {
"@types/react": "17.0.2",
"@types/react-dom": "17.0.2"
}
}
83 changes: 83 additions & 0 deletions src/anchors/DocsGPTChatWidget.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import React from 'react';
import { render } from 'react-dom';
import $ from 'jquery';
import { utils } from 'github-url-detection';

import PerceptorBase from '../PerceptorBase';
import { runsWhen, isPublicRepo, getGithubTheme } from '../utils/utils';
import DocsGPTChatWidget from '../views/DocsGPTChatWidget';

const DOCS_META_DATA_URL =
'https://oss.x-lab.info/hypercrx/docsgpt_active_docs.json';

interface DocsMetaItem {
type: 'repo' | 'org';
name: string; // GitHub repo name or org name
key: string; // corresponding docs name
}

@runsWhen([isPublicRepo])
class DocsGPTChatWidgetAnchor extends PerceptorBase {
private _currentRepo: string;
private _docsMetaData: DocsMetaItem[];

constructor() {
super();
this._currentRepo = '';
this._docsMetaData = [];
}

private async _getDocsMetaData() {
const response = await fetch(DOCS_META_DATA_URL);
if (response.ok) {
this._docsMetaData = await response.json();
} else {
throw new Error('Failed to fetch docs meta data');
}
}

private get _currentDocsName(): string | null {
const orgName = this._currentRepo.split('/')[0];
let result = null;
for (const item of this._docsMetaData) {
if (item.type === 'repo' && item.name === this._currentRepo) {
result = item.key;
break;
} else if (item.type === 'org' && item.name === orgName) {
result = item.key;
break;
}
}
return result;
}

public async run(): Promise<void> {
const repoName = utils.getRepositoryInfo(window.location)!.nameWithOwner;

if ($('#docs-gpt-chat-widget').length !== 0) {
if ($('#docs-gpt-chat-widget').data('repo') === repoName) {
return; // should not re-render for same repo
} else {
$('#docs-gpt-chat-widget').remove();
}
}

this._currentRepo = repoName;
await this._getDocsMetaData();

const container = document.createElement('div');
container.id = 'docs-gpt-chat-widget';
container.className = getGithubTheme()!;
container.dataset.repo = this._currentRepo; // mark current repo by data-repo
render(
<DocsGPTChatWidget
currentRepo={this._currentRepo}
currentDocsName={this._currentDocsName}
/>,
container
);
$('body').append(container);
}
}

export default DocsGPTChatWidgetAnchor;
3 changes: 3 additions & 0 deletions src/locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -316,5 +316,8 @@
},
"merged_lines_popup_title": {
"message": "Merged Addition & Deletion Code Lines"
},
"docs_gpt_chat_widget": {
"message": "OSS-GPT Chat Bot"
}
}
3 changes: 3 additions & 0 deletions src/locales/zh_CN/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -316,5 +316,8 @@
},
"merged_lines_popup_title": {
"message": "通过 PR 合入增加和删除的代码行数"
},
"docs_gpt_chat_widget": {
"message": "OSS-GPT 聊天机器人"
}
}
2 changes: 2 additions & 0 deletions src/pages/ContentScripts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { initializeIcons } from '@fluentui/react/lib/Icons';
initializeIcons();

import DocsGPTChatWidget from '../../anchors/DocsGPTChatWidget';
import RepoHeaderLabelsAnchor from '../../anchors/RepoHeaderLabelsAnchor';
import RepoDetailPRAnchor from '../../anchors/RepoDetailPRAnchor';
import RepoDetailIssueAnchor from '../../anchors/RepoDetailIssueAnchor';
Expand All @@ -21,6 +22,7 @@ import { loadSettings } from '../../utils/settings';
import './index.scss';

// inject to Perceptor's static variable
inject2Perceptor(DocsGPTChatWidget);
inject2Perceptor(RepoHeaderLabelsAnchor);
inject2Perceptor(RepoDetailPRAnchor);
inject2Perceptor(RepoDetailIssueAnchor);
Expand Down
11 changes: 11 additions & 0 deletions src/pages/Options/Options.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,17 @@ const Options: React.FC = () => {
await saveSettings(settings);
}}
/>
<Checkbox
label={getMessageByLocale(
'docs_gpt_chat_widget',
settings.locale
)}
defaultChecked={settings.docsGPTChatWidget}
onChange={async (e, checked) => {
settings.docsGPTChatWidget = checked;
await saveSettings(settings);
}}
/>
</Stack>
</Stack.Item>
<Stack.Item className="Box">
Expand Down
6 changes: 6 additions & 0 deletions src/utils/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ class Settings {
checkForUpdates: boolean | undefined;
developerNetwork: boolean | undefined;
projectNetwork: boolean | undefined;
docsGPTChatWidget: boolean | undefined;
locale: string;

constructor() {
this.isEnabled = true;
this.checkForUpdates = true;
this.developerNetwork = true;
this.projectNetwork = true;
this.docsGPTChatWidget = true;
const language = chrome.i18n.getUILanguage();
if (language.startsWith('zh')) {
this.locale = 'zh_CN';
Expand All @@ -33,6 +35,9 @@ class Settings {
if ('projectNetwork' in data) {
this.projectNetwork = data['projectNetwork'];
}
if ('docsGPTChatWidget' in data) {
this.docsGPTChatWidget = data['docsGPTChatWidget'];
}
if ('locale' in data) {
this.locale = data['locale'];
}
Expand All @@ -44,6 +49,7 @@ class Settings {
result['checkForUpdates'] = this.checkForUpdates;
result['developerNetwork'] = this.developerNetwork;
result['projectNetwork'] = this.projectNetwork;
result['docsGPTChatWidget'] = this.docsGPTChatWidget;
result['locale'] = this.locale;
return result;
}
Expand Down
76 changes: 76 additions & 0 deletions src/views/DocsGPTChatWidget/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import React, { useState, useEffect } from 'react';
import {
Widget,
addResponseMessage,
deleteMessages,
toggleMsgLoader,
toggleInputDisabled,
} from 'react-chat-widget';

import { getAnswer } from './service';
import './rcw.scss';

interface Props {
currentRepo: string;
currentDocsName: string | null;
}

const displayWelcome = (repoName: string) => {
addResponseMessage(
`Hi, I'm an assistant powered by [DocsGPT](https://github.com/arc53/docsgpt) and [X-lab](https://github.com/X-lab2017). Ask me anything about \`${repoName}\`!`
);
};

const displayNotAvailable = (repoName: string) => {
addResponseMessage(
`OSS-GPT currently is **NOT AVAILABLE** for \`${repoName}\`. If you want docs support for the repository, please check [this](https://github.com/hypertrons/hypertrons-crx/issues/609) issue and make a request there :)\n\nSee [all available docs](https://oss.x-lab.info/hypercrx/docsgpt_active_docs.json)\n\nThis chat widget can also be enable/disabled in the extension options page whenever you want.`
);
};

const View = ({ currentRepo, currentDocsName }: Props): JSX.Element => {
const subtitle = currentDocsName
? `Ask anything about ${currentRepo}`
: `NOT AVAILABLE for ${currentRepo}`;

const [history, setHistory] = useState<[string, string]>(['', '']);

const handleNewUserMessage = async (newMessage: string) => {
toggleMsgLoader();
toggleInputDisabled();

if (currentDocsName) {
const answer = await getAnswer(currentDocsName, newMessage, history);
addResponseMessage(answer);
setHistory([newMessage, answer]); // update history
} else {
displayNotAvailable(currentRepo);
}

toggleMsgLoader();
toggleInputDisabled();
};

useEffect(() => {
// when repo changes
deleteMessages(Infinity); // delete all messages after repo switching
setHistory(['', '']); // clear history
if (currentDocsName) {
// if docs for current repo is available
displayWelcome(currentRepo);
} else {
displayNotAvailable(currentRepo);
}
}, [currentRepo, currentDocsName]);

return (
<Widget
title="OSS-GPT"
subtitle={subtitle}
emojis={false} // will be enabled after style is fine tuned for two themes
resizable={true}
handleNewUserMessage={handleNewUserMessage}
/>
);
};

export default View;
Loading

0 comments on commit 947b7ee

Please sign in to comment.