Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion my-index.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@
<script src="/reactapp/common.bundle.js"></script>

<link href="/reactapp/prefixed-bootstrap.min.css" rel="stylesheet"></link>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.css" rel="stylesheet" />
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" rel="stylesheet" />

</head>
<body>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,9 @@ export type ClinicalAttributeCountFilter = {
'sampleListId': string

};
export type UserMessage = {
'message': string
};
export type ClinicalData = {
'clinicalAttribute': ClinicalAttribute

Expand Down Expand Up @@ -8480,4 +8483,55 @@ export default class CBioPortalAPIInternal {
return response.body;
});
};

/**
* Send a support message to the AI support endpoint.
* @method
* @name CBioPortalAPIInternal#getSupportUsingPOST
* @param {Object} parameters - Parameters for the request.
* @param {UserMessage} [parameters.userMessage] - The message to send to the AI support system. This can contain user queries, questions, or other requests.
* @param {string} [parameters.$domain] - Optional override for the API domain. Defaults to the instance's domain if not provided.
*/
getSupportUsingPOSTWithHttpInfo(parameters: {
'userMessage' ? : UserMessage,
$domain ? : string
}): Promise < request.Response > {
const domain = parameters.$domain ? parameters.$domain : this.domain;
const errorHandlers = this.errorHandlers;
const request = this.request;
let path = '/api/assistant';
let body: any;
let queryParameters: any = {};
let headers: any = {};
let form: any = {};
return new Promise(function(resolve, reject) {
headers['Accept'] = 'application/json';
headers['Content-Type'] = 'application/json';

if (parameters['userMessage'] !== undefined) {
body = parameters['userMessage'];
}

request('POST', domain + path, body, headers, queryParameters, form, reject, resolve, errorHandlers);

});
};

/**
* Send a support message to the AI support endpoint and return only the response body.
* @method
* @name CBioPortalAPIInternal#getSupprtUsingPOST
* @param {Object} parameters - Parameters for the request.
* @param {UserMessage} [parameters.userMessage] - The message to send to the AI support system.
* @param {string} [parameters.$domain] - Optional override for the API domain.
*/
getSupportUsingPOST(parameters: {
'userMessage' ? : UserMessage,
$domain ? : string
}): Promise<{ aiResponse: string }>
{
return this.getSupportUsingPOSTWithHttpInfo(parameters).then(function(response: request.Response) {
return response.body;
});
};
}
1 change: 1 addition & 0 deletions packages/cbioportal-ts-api-client/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export {
CustomDriverAnnotationReport,
StructuralVariant,
StructuralVariantFilter,
UserMessage,
StructuralVariantQuery,
StructuralVariantGeneSubQuery,
StructuralVariantFilterQuery,
Expand Down
4 changes: 2 additions & 2 deletions src/appShell/App/PortalFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -187,9 +187,9 @@ export default class PortalFooter extends React.Component<
<li>
<a
target="_blank"
href="https://www.twitter.com/cbioportal"
href="https://www.x.com/cbioportal"
>
Twitter
X
</a>
</li>
</If>
Expand Down
1 change: 1 addition & 0 deletions src/config/IAppConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ export interface IServerConfig {
skin_study_view_show_sv_table: boolean; // this has a default
enable_study_tags: boolean;
clickhouse_mode: boolean;
spring_ai_enabled: boolean;
download_custom_buttons_json: string;
feature_study_export: boolean;
}
Binary file added src/globalStyles/images/cbioportal_icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
288 changes: 288 additions & 0 deletions src/shared/components/query/GeneAssistant.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
import * as React from 'react';
import ReactMarkdown from 'react-markdown';
import { observer } from 'mobx-react';
import { action, observable, makeObservable } from 'mobx';
import styles from './styles/styles.module.scss';
import internalClient from '../../../shared/api/cbioportalInternalClientInstance';
import { UserMessage } from 'cbioportal-ts-api-client/dist/generated/CBioPortalAPIInternal';
import { QueryStoreComponent } from './QueryStore';

enum OQLError {
io = 'Something went wrong, please try again',
invalid = 'Please submit a valid OQL question',
}

const ErrorMessage: React.FC<{ message: string }> = ({ message }) => (
<div className={styles.errorMessage}>{message}</div>
);

@observer
export default class GeneAssistant extends QueryStoreComponent<{}, {}> {
constructor(props: any) {
super(props);
makeObservable(this);
}
@observable private userMessage = '';
@observable private pending = false;
@observable private showErrorMessage = false;
@observable private errorMessage = OQLError.io;
private examples = {
'Find mutations in tumor suppressor genes':
'TP53 RB1 CDKN2A PTEN SMAD4 ARID1A...',
'Somatic missense mutations in PIK3CA': 'PIK3CA: MISSENSE_SOMATIC',
'Find KRAS mutations excluding silent ones': 'KRAS: MUT',
};

@action.bound
private toggleSupport() {
this.store.showSupport = !this.store.showSupport;
}

@action.bound
private submitOQL(oql: string) {
this.store.geneQuery = oql;
}

@action.bound
private queryExample(example: string) {
this.userMessage = example;
}

@action.bound
private handleInputChange(event: React.ChangeEvent<HTMLInputElement>) {
this.userMessage = event.target.value;
}

@action.bound
private handleSendMessage(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();

if (!this.userMessage.trim()) return;

this.store.messages.push({
speaker: 'User',
text: this.userMessage,
});
this.getResponse();
this.userMessage = '';
}

@action.bound
private async getResponse() {
this.showErrorMessage = false;
this.pending = true;

let userMessage = {
message: this.userMessage,
} as UserMessage;

try {
const response = await internalClient.getSupportUsingPOST({
userMessage,
});
const parts = response.aiResponse.split('OQL: ', 2);

if (parts.length < 2 || parts[1].trim().toUpperCase() === 'FALSE') {
this.showErrorMessage = true;
this.errorMessage = OQLError.invalid;
} else {
this.store.messages.push({
speaker: 'AI',
text: parts[0].trim(),
});
}
this.pending = false;
} catch (error) {
this.pending = false;
this.showErrorMessage = true;
this.errorMessage = OQLError.io;
}
}

renderButton() {
return (
<button
style={{ borderRadius: '8px', fontSize: '13px' }}
className="btn btn-primary btn-lg"
data-test="aiButton"
onClick={this.toggleSupport}
>
{!this.store.showSupport ? (
<div>
<i
className="fa-solid fa-robot"
style={{ paddingRight: '5px' }}
/>
Gene Assistant
</div>
) : (
<div>
<i
className="fa-solid fa-robot"
style={{ paddingRight: '5px' }}
/>
Hide Assistant
</div>
)}
</button>
);
}

renderThinking() {
return (
<div className={styles.thinking}>
<span className={styles.dots}>
<span className={styles.dot} />
<span className={styles.dot} />
<span className={styles.dot} />
</span>
</div>
);
}

renderErrorMessage(error: string) {
return <div className={styles.errorMessage}>{error}</div>;
}

renderMessages() {
return (
<div>
{this.store.messages.map((msg, index) => {
const isUser = msg.speaker === 'User';
return (
<div
key={index}
className={
styles.messageRow +
(isUser ? ' ' + styles.messageRowRight : '')
}
>
<div
className={
isUser ? styles.question : styles.message
}
>
{msg.text.split('\n').map((line, i) => (
<p key={i} className={styles.messageLine}>
<ReactMarkdown key={i}>
{line}
</ReactMarkdown>
</p>
))}
</div>
{!isUser && index !== 0 && (
<div>
<button
onClick={() => this.submitOQL(msg.text)}
style={{
fontSize: '20px',
color: '#3498db',
marginRight: '8px',
border: 0,
background: 'none',
}}
>
<i className="fa-solid fa-right-to-bracket"></i>
</button>
</div>
)}
</div>
);
})}
</div>
);
}

renderExamples() {
return (
<div className={styles.examplesarea}>
<h2>
<i
className="fa-solid fa-lightbulb"
style={{ paddingRight: '10px' }}
/>
Quick Examples:
</h2>

<div className={styles.examplestext}>
{Object.entries(this.examples).map(([example, genes]) => (
<div
className={styles.exampleitem}
onClick={() => this.queryExample(example)}
>
<strong className={styles.exampletitle}>
{example}
</strong>
<span className={styles.exampledescription}>
{genes}
</span>
</div>
))}
</div>
</div>
);
}

render() {
return (
<div className={styles.supportContainer}>
{this.renderButton()}
{this.store.showSupport && (
<div className={styles.chatWindow}>
<section className={styles.titlearea}>
<img
src={require('../../../globalStyles/images/cbioportal_icon.png')}
className={styles.titleIcon}
alt="cBioPortal icon"
/>
<span>cBioPortal Gene Assistant</span>
</section>

{this.renderExamples()}

<div className={styles.textarea}>
<div className={styles.textheader}>
Please ask your cBioPortal querying questions
here, for example how to correctly format a
query using Onco Query Language (OQL).
</div>
{this.renderMessages()}
{this.pending && this.renderThinking()}
{this.showErrorMessage && (
<ErrorMessage message={this.errorMessage} />
)}
</div>

<div className={styles.inputarea}>
<form
className={styles.form}
onSubmit={this.handleSendMessage}
>
<input
className={styles.input}
type="text"
value={this.userMessage}
onChange={this.handleInputChange}
placeholder="Ask me about genes, cancer types or OQL syntax!"
/>
<button
type="submit"
aria-hidden="true"
style={{
fontSize: '20px',
color: '#3498db',
marginRight: '8px',
border: 0,
background: 'none',
}}
>
<i className="fa-solid fa-paper-plane"></i>
</button>
</form>
</div>
</div>
)}
</div>
);
}
}
Loading
Loading