diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0ba644b --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +transformerviz.egg-info +node_modules/ +dist/ +env/ +__pycache__ +*.pyc + +transformerviz/assets/app-build.js +transformerviz/assets/app.css + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..ebf23ac --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,14 @@ +# Contributing + +This project welcomes contributions and suggestions. Most contributions require you to +agree to a Contributor License Agreement (CLA) declaring that you have the right to, +and actually do, grant us the rights to use your contribution. For details, visit +https://cla.microsoft.com. + +When you submit a pull request, a CLA-bot will automatically determine whether you need +to provide a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the +instructions provided by the bot. You will only need to do this once across all repositories using our CLA. + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). +For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) +or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. diff --git a/README.md b/README.md index 5cd7cec..64453f3 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,20 @@ -# Project +# Transformer Visualizations +Investigations on Transformer Visualizations by the Aether Prototyping and Incubation team. -> This repo has been populated by an initial template to help get you started. Please -> make sure to update the content to build a great experience for community-building. +## Getting Started -As the maintainer of this project, please make a few updates: +### Development Environment Setup +- `python -m pip install --upgrade pip` +- `pip install -r requirements.txt --cache-dir ` (The `cache-dir` parameter is optional). +- `pip install -e .` +- `npm install` +- `npm run build` -- Improving this README.MD file to provide a great experience -- Updating SUPPORT.MD with content about this project's support experience -- Understanding the security reporting process in SECURITY.MD -- Remove this section from the README +### Running + `PERSPECTIVE_API_KEY= GPT2_MODEL_VERSION= NUM_RETURN_SEQUENCES= TRANSFORMERS_CACHE= FLASK_APP=transformerviz/server.py flask run --host 0.0.0.0 --port 5000` (The `TRANSFORMERS_CACHE` environment variable is optional). +- Point your browser to port 5000 of the server on which the app is running. ## Contributing - This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. @@ -25,7 +28,6 @@ For more information see the [Code of Conduct FAQ](https://opensource.microsoft. contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. ## Trademarks - This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft trademarks or logos is subject to and must follow [Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). diff --git a/SUPPORT.md b/SUPPORT.md deleted file mode 100644 index dc72f0e..0000000 --- a/SUPPORT.md +++ /dev/null @@ -1,25 +0,0 @@ -# TODO: The maintainer of this repo has not yet edited this file - -**REPO OWNER**: Do you want Customer Service & Support (CSS) support for this product/project? - -- **No CSS support:** Fill out this template with information about how to file issues and get help. -- **Yes CSS support:** Fill out an intake form at [aka.ms/spot](https://aka.ms/spot). CSS will work with/help you to determine next steps. More details also available at [aka.ms/onboardsupport](https://aka.ms/onboardsupport). -- **Not sure?** Fill out a SPOT intake as though the answer were "Yes". CSS will help you decide. - -*Then remove this first heading from this SUPPORT.MD file before publishing your repo.* - -# Support - -## How to file issues and get help - -This project uses GitHub Issues to track bugs and feature requests. Please search the existing -issues before filing new issues to avoid duplicates. For new issues, file your bug or -feature request as a new Issue. - -For help and questions about using this project, please **REPO MAINTAINER: INSERT INSTRUCTIONS HERE -FOR HOW TO ENGAGE REPO OWNERS OR COMMUNITY FOR HELP. COULD BE A STACK OVERFLOW TAG OR OTHER -CHANNEL. WHERE WILL YOU HELP PEOPLE?**. - -## Microsoft Support Policy - -Support for this **PROJECT or PRODUCT** is limited to the resources listed above. diff --git a/development/index.html b/development/index.html new file mode 100644 index 0000000..8d7090d --- /dev/null +++ b/development/index.html @@ -0,0 +1,27 @@ + + + + + + + + +
+
+ + + + \ No newline at end of file diff --git a/frontend/MainContainer.tsx b/frontend/MainContainer.tsx new file mode 100644 index 0000000..3149cb4 --- /dev/null +++ b/frontend/MainContainer.tsx @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import React, { useEffect, useState } from "react"; +import { bindActionCreators } from "redux"; +import { connect } from 'react-redux'; +import { + requestGenerateText, + setGeneratedTextResults, + generateTextFailed, + generateText, + selectText, + submitEditText, + deleteEditText, + foldForm +} from './actions'; +import TextGenerationControl from "./components/TextGenerationControl"; +import TextGenerationResults from "./components/TextGenerationResults"; +import TextDetailedAnalysis from "./components/TextDetailedAnalysis"; +import TextRecord from "./models/TextRecord"; + +type ContainerProps = { + analysisResults: TextRecord[], + error: any, + loading: any, + isFolded: boolean, + selectedTextIds: Array, + requestGenerateText: Function, + setGeneratedTextResults: Function, + generateTextFailed: any, + generateText: any + selectText: Function, + submitEditText: Function, + deleteEditText: Function, + foldForm: Function, +} + +const Container: React.FunctionComponent = ({ + analysisResults, + error, + loading, + isFolded, + selectedTextIds, + requestGenerateText, + setGeneratedTextResults, + generateTextFailed, + generateText, + selectText, + submitEditText, + deleteEditText, + foldForm + }) => { + const getDetailedAnalysisComponent = () => { + if (selectedTextIds.length == 0) { + return (); + } else { + const selectedText = selectedTextIds.map(id => analysisResults.find(item => id == item.id)) + return ( + + ); + } + } + + useEffect(() => { + const form = document.getElementById("text_gen_form"); + const grid = document.getElementById("two_column_grid"); + if (grid) { + grid.style.height = `calc(93.5vh - ${form?.offsetHeight}px)` + } + }) + + return ( +
+ foldForm()} isFolded={isFolded} /> +
+ + {getDetailedAnalysisComponent()} +
+
+ ); +} + +function mapStateToProps (state) { + return { + analysisResults: state.analysisResults, + error: state.error, + loading: state.loading, + selectedTextIds: state.selectedTextIds, + isFolded: state.isFolded + }; +} + +function mapDispatchToProps (dispatch) { + return bindActionCreators({ + requestGenerateText: requestGenerateText, + setGeneratedTextResults: setGeneratedTextResults, + generateTextFailed: generateTextFailed, + generateText: generateText, + selectText: selectText, + submitEditText: submitEditText, + deleteEditText: deleteEditText, + foldForm: foldForm, + }, dispatch); + } + +const MainContainer = connect(mapStateToProps, mapDispatchToProps)(Container) + +export default MainContainer; diff --git a/frontend/actions.ts b/frontend/actions.ts new file mode 100644 index 0000000..153b00a --- /dev/null +++ b/frontend/actions.ts @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { makeGetCall, makePostCall } from "./api"; + + +function requestGenerateText() { + return { + type: "REQUEST_GENERATE_TEXT" + } +} + +function requestEditText(id) { + return { + type: "REQUEST_EDIT_TEXT", + id: id + } +} + +function setGeneratedTextResults(generateTextResults) { + return { + type: "SET_GENERATED_TEXT_RESULTS", + text_generation_results: generateTextResults + } +} + +function generateTextFailed(error) { + return { + type: "REQUEST_GENERATE_TEXT_FAILED", + error: error + } +} + +function generateText(config) { + return function(dispatch) { + dispatch(requestGenerateText()); + makePostCall("api/v1/generate_text", config) + .then(response => dispatch(setGeneratedTextResults(response.data.text_generation_results))) + .catch(error => dispatch(generateTextFailed(error))); + } +} + +function foldForm() { + return { + type: "FOLD_FORM" + } +} + +function selectText(id: number) { + return { + type: "SELECT_TEXT", + id: id + } +} + +function setEditTextResults(id: number, text: string, analysis: any) { + return { + type: "EDIT_TEXT_SUCCEEDED", + id: id, + text: text, + analysis: analysis + } + +} + +function editTextFailed(id: number, error) { + return { + type: "EDIT_TEXT_FAILED", + id: id, + error: error + } +} + +function submitEditText(id: number, text: string) { + return function(dispatch) { + dispatch(requestEditText(id)); + makePostCall("api/v1/analyze_text", {text: text}) + .then(response => dispatch(setEditTextResults(id, text, response.data.text_analysis_result))) + .catch(error => dispatch(editTextFailed(id, error))); + } +} + +function deleteEditText(id: number) { + return { + type: "DELETE_EDIT_TEXT", + id: id + } +} + + +export { + requestGenerateText, + setGeneratedTextResults, + generateTextFailed, + generateText, + selectText, + submitEditText, + deleteEditText, + foldForm, +}; diff --git a/frontend/api.ts b/frontend/api.ts new file mode 100644 index 0000000..3f6dadd --- /dev/null +++ b/frontend/api.ts @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import axios from 'axios'; + +var apiServiceEnvironment = window["API_SERVICE_ENVIRONMENT"]; +var port = apiServiceEnvironment.port; + +/* +if (window.location.port != "") { + port = window.location.port; +}*/ + +var apiBaseUrl = `${window.location.protocol}//${window.location.hostname}:${port}`; + + +function makeGetCall(endpoint: string) { + return axios.get(`${apiBaseUrl}/${endpoint}`); +} + +function makePostCall(endpoint: string, payload: any) { + return axios.post(`${apiBaseUrl}/${endpoint}`, payload); +} + +export { + apiBaseUrl, + makeGetCall, + makePostCall +}; diff --git a/frontend/app.css b/frontend/app.css new file mode 100644 index 0000000..3c0fbda --- /dev/null +++ b/frontend/app.css @@ -0,0 +1,77 @@ +/** + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ + +html { + height: 100%; +} + +body { + min-height: 100%; + max-height: 100vh; + overflow: hidden; +} + +.bar { +} + +.highlighted-bar { + stroke-width: 2px; + stroke: rgb(0,0,0, 1); +} + +h1 { + font-size: 20px; + font-weight: 600; + line-height: 20px; +} + +.plot-spider { + display: flex; + justify-content: center; +} + +.light-gray-text { + color: #8A8886; +} + +.text-gen-form-grid { + display: grid; + grid-template-columns: 1fr 2fr 2fr 3fr; +} + +.stacked-text-field-grid { + display: grid; + grid-template-rows: 1fr 1fr; + grid-template-columns: 1fr 2fr; + align-items: center; +} + +.ms-Checkbox-label { + font-weight: 600; +} + +.two-column-grid { + width: 100%; + display: grid; + grid-template-columns: 1fr 1fr; + grid-template-rows: 1fr; + align-items: stretch; +} + +.page-column { + overflow-y: scroll; +} + +.sort-button { + background: #FFFFFF; + border: 1px solid #167DF5; + box-sizing: border-box; + border-radius: 2px; +} + +.sort-button.disabled-button { + background: #EFF2F4; + border: solid #EFF2F4 1px; +} \ No newline at end of file diff --git a/frontend/components/InteractiveText.tsx b/frontend/components/InteractiveText.tsx new file mode 100644 index 0000000..bbf2271 --- /dev/null +++ b/frontend/components/InteractiveText.tsx @@ -0,0 +1,225 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import React from "react"; +import TextRecord from "../models/TextRecord"; +import AnalyzedText from "../models/AnalyzedText"; +import { FiEdit, FiX, FiCheck } from "react-icons/fi"; + +type InteractiveTextProps = { + textRecord: TextRecord; + tabColor: string; + editColor: string; + submitEditText: Function; + deleteEditText: Function; +}; + +type InteractiveTextState = { + editMode: boolean; + editText: string; +}; + +class InteractiveText extends React.Component< + InteractiveTextProps, + InteractiveTextState +> { + constructor(props) { + super(props); + + let editText; + if (this.props.textRecord.edited) { + editText = this.props.textRecord.edited.text; + } else { + editText = this.props.textRecord.original.text; + } + + this.state = { + editMode: false, + editText: editText, + }; + + this.enableEditMode = this.enableEditMode.bind(this); + this.handleTextSubmit = this.handleTextSubmit.bind(this); + this.handleTextClear = this.handleTextClear.bind(this); + this.handleTextEdit = this.handleTextEdit.bind(this); + } + + enableEditMode() { + this.setState({ editMode: true }); + } + + handleTextSubmit() { + this.props.submitEditText(this.props.textRecord.id, this.state.editText); + this.setState({ editMode: false }); + } + + handleTextClear() { + this.props.deleteEditText(this.props.textRecord.id); + this.setState({ editMode: false }); + } + + handleTextEdit(event) { + this.setState({ editText: event.target.value }); + } + + render() { + const getEditText = (record: TextRecord) => { + if (record.editLoading) { + return "Loading..."; + } else if (record.editError) { + return ( + <> +
Error: {record.editError.message}
+
{record.edited?.text}
+ + ); + } else { + return record.edited.text; + } + } + + return ( + <> +
+
+ {this.props.textRecord.id} +
+
+ {this.props.textRecord.original.text} +
+ +
+ {(this.props.textRecord.edited || this.props.textRecord.editLoading) && !this.state.editMode ? ( +
+
+ {this.props.textRecord.id}* +
+
+ { getEditText(this.props.textRecord) } +
+ +
+ ) : null} + {this.state.editMode ? ( +
+
+ {this.props.textRecord.id}* +
+