AnthoneoJ commited on
Commit
cac59f2
·
1 Parent(s): 1fcfb9b

Upload 14 files

Browse files
.eslintrc.cjs ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ module.exports = {
2
+ root: true,
3
+ env: { browser: true, es2020: true },
4
+ extends: [
5
+ 'eslint:recommended',
6
+ 'plugin:react/recommended',
7
+ 'plugin:react/jsx-runtime',
8
+ 'plugin:react-hooks/recommended',
9
+ ],
10
+ ignorePatterns: ['dist', '.eslintrc.cjs'],
11
+ parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
12
+ settings: { react: { version: '18.2' } },
13
+ plugins: ['react-refresh'],
14
+ rules: {
15
+ 'react-refresh/only-export-components': [
16
+ 'warn',
17
+ { allowConstantExport: true },
18
+ ],
19
+ },
20
+ }
.gitignore ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ *.local
14
+
15
+ # Editor directories and files
16
+ .vscode/*
17
+ !.vscode/extensions.json
18
+ .idea
19
+ .DS_Store
20
+ *.suo
21
+ *.ntvs*
22
+ *.njsproj
23
+ *.sln
24
+ *.sw?
index.html ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>Semantic Similarity - Vite + React</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.jsx"></script>
12
+ </body>
13
+ </html>
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "react-semantic-similarity",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
10
+ "preview": "vite preview"
11
+ },
12
+ "dependencies": {
13
+ "@xenova/transformers": "^2.5.0",
14
+ "react": "^18.2.0",
15
+ "react-dom": "^18.2.0"
16
+ },
17
+ "devDependencies": {
18
+ "@types/react": "^18.2.15",
19
+ "@types/react-dom": "^18.2.7",
20
+ "@vitejs/plugin-react": "^4.0.3",
21
+ "eslint": "^8.45.0",
22
+ "eslint-plugin-react": "^7.32.2",
23
+ "eslint-plugin-react-hooks": "^4.6.0",
24
+ "eslint-plugin-react-refresh": "^0.4.3",
25
+ "vite": "^4.4.5"
26
+ }
27
+ }
public/vite.svg ADDED
src/App.css ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #root {
2
+ max-width: 1280px;
3
+ margin: 0 auto;
4
+ padding: 2rem;
5
+ text-align: center;
6
+ }
7
+
8
+ .textbox-container {
9
+ display: flex;
10
+ justify-content: center;
11
+ gap: 20px;
12
+ width: 800px;
13
+ }
14
+
15
+ .textbox-container>textarea, .textbox-container>.candidates-container {
16
+ width: 50%;
17
+ }
18
+
19
+ .candidate-line-container {
20
+ display: flex;
21
+ flex-direction: column;
22
+ }
23
+
24
+ .output-score {
25
+ font-size: 1.0em;
26
+ font-style: italic;
27
+ }
28
+
29
+ .progress-container {
30
+ position: relative;
31
+ font-size: 14px;
32
+ color: white;
33
+ background-color: #e9ecef;
34
+ border: solid 1px;
35
+ border-radius: 8px;
36
+ text-align: left;
37
+ overflow: hidden;
38
+ }
39
+
40
+ .progress-bar {
41
+ padding: 0 4px;
42
+ z-index: 0;
43
+ top: 0;
44
+ width: 1%;
45
+ height: 100%;
46
+ overflow: hidden;
47
+ background-color: #007bff;
48
+ white-space: nowrap;
49
+ }
50
+
51
+ .progress-text {
52
+ z-index: 2;
53
+ }
54
+
55
+ .selector-container {
56
+ display: flex;
57
+ gap: 20px;
58
+ }
59
+
60
+ .progress-bars-container {
61
+ padding: 8px;
62
+ height: 140px;
63
+ }
64
+
65
+ .container {
66
+ margin: 25px;
67
+ display: flex;
68
+ flex-direction: column;
69
+ gap: 10px;
70
+ }
71
+
72
+
73
+
74
+ .logo {
75
+ height: 6em;
76
+ padding: 1.5em;
77
+ will-change: filter;
78
+ transition: filter 300ms;
79
+ }
80
+ .logo:hover {
81
+ filter: drop-shadow(0 0 2em #646cffaa);
82
+ }
83
+ .logo.react:hover {
84
+ filter: drop-shadow(0 0 2em #61dafbaa);
85
+ }
86
+
87
+ @keyframes logo-spin {
88
+ from {
89
+ transform: rotate(0deg);
90
+ }
91
+ to {
92
+ transform: rotate(360deg);
93
+ }
94
+ }
95
+
96
+ @media (prefers-reduced-motion: no-preference) {
97
+ a:nth-of-type(2) .logo {
98
+ animation: logo-spin infinite 20s linear;
99
+ }
100
+ }
101
+
102
+ .card {
103
+ padding: 2em;
104
+ }
105
+
106
+ .read-the-docs {
107
+ color: #888;
108
+ }
src/App.jsx ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Remember to import the relevant hooks
2
+ import { useEffect, useRef, useState } from 'react'
3
+ import Progress from './components/Progress';
4
+
5
+ import './App.css'
6
+
7
+ function App() {
8
+ // Model loading
9
+ const [ready, setReady] = useState(null);
10
+ const [disabled, setDisabled] = useState(false);
11
+ const [progressItems, setProgressItems] = useState([]);
12
+
13
+ // Reference and candidate sentences
14
+ const [referenceSentence, setReferenceSentence] = useState('Dumbledore lost the fight against Voldemort.');
15
+ const [candidateSentence1, setCandidateSentence1] = useState('I\'m sad that Voldemort won the battle.');
16
+ const [candidateSentence2, setCandidateSentence2] = useState('The evil prevailed.');
17
+ const [candidateSentence3, setCandidateSentence3] = useState('The good prevailed.');
18
+
19
+ // Similarity output
20
+ const [output_1, setOutput1] = useState('');
21
+ const [output_2, setOutput2] = useState('');
22
+ const [output_3, setOutput3] = useState('');
23
+
24
+ // Create a reference to the worker object.
25
+ const worker = useRef(null);
26
+
27
+ // We use the `useEffect` hook to setup the worker as soon as the `App` component is mounted.
28
+ useEffect(() => {
29
+ if (!worker.current) {
30
+ // Create the worker if it does not yet exist.
31
+ worker.current = new Worker(new URL('./worker.js', import.meta.url), {
32
+ type: 'module'
33
+ });
34
+ }
35
+
36
+ // Create a callback function for messages from the worker thread.
37
+ const onMessageReceived = (e) => {
38
+ switch (e.data.status) {
39
+ case 'initiate':
40
+ // Model file start load: add a new progress item to the list.
41
+ setReady(false);
42
+ setProgressItems(prev => [...prev, e.data]);
43
+ break;
44
+
45
+ case 'progress':
46
+ // Model file progress: update one of the progress items.
47
+ setProgressItems(
48
+ prev => prev.map(item => {
49
+ if (item.file === e.data.file) {
50
+ return { ...item, progress: e.data.progress }
51
+ }
52
+ return item;
53
+ })
54
+ );
55
+ break;
56
+
57
+ case 'done':
58
+ // Model file loaded: remove the progress item from the list.
59
+ setProgressItems(
60
+ prev => prev.filter(item => item.file !== e.data.file)
61
+ );
62
+ break;
63
+
64
+ case 'ready':
65
+ // Pipeline ready: the worker is ready to accept messages.
66
+ setReady(true);
67
+ break;
68
+
69
+ case 'update_1':
70
+ // Generation update: update the output text.
71
+ setOutput1(e.data.output);
72
+ break;
73
+
74
+ case 'update_2':
75
+ // Generation update: update the output text.
76
+ setOutput2(e.data.output);
77
+ break;
78
+
79
+ case 'update_3':
80
+ // Generation update: update the output text.
81
+ setOutput3(e.data.output);
82
+ break;
83
+
84
+ case 'complete':
85
+ // Generation complete: re-enable the "Compute" button
86
+ setDisabled(false);
87
+ break;
88
+ }
89
+ };
90
+
91
+ // Attach the callback function as an event listener.
92
+ worker.current.addEventListener('message', onMessageReceived);
93
+
94
+ // Define a cleanup function for when the component is unmounted.
95
+ return () => worker.current.removeEventListener('message', onMessageReceived);
96
+ });
97
+
98
+ const compute = () => {
99
+ setDisabled(true);
100
+ worker.current.postMessage({
101
+ refsent: referenceSentence,
102
+ cand1: candidateSentence1,
103
+ cand2: candidateSentence2,
104
+ cand3: candidateSentence3,
105
+ });
106
+ }
107
+
108
+ return (
109
+ <>
110
+ <h1>Semantic Similarity</h1>
111
+ <h2>Measure how similar two sentences are in meaning.</h2>
112
+
113
+ <div className='container'>
114
+ <div className='textbox-container'>
115
+ <textarea value={referenceSentence} rows={3} onChange={e => setReferenceSentence(e.target.value)}></textarea>
116
+ <div className='candidates-container'>
117
+ <div className='candidate-line-container'>
118
+ <input type='text' value={candidateSentence1} onChange={e => setCandidateSentence1(e.target.value)}></input>
119
+ <p className='output-score'>{output_1}</p>
120
+ </div>
121
+ <div className='candidate-line-container'>
122
+ <input type='text' value={candidateSentence2} onChange={e => setCandidateSentence2(e.target.value)}></input>
123
+ <p className='output-score'>{output_2}</p>
124
+ </div>
125
+ <div className='candidate-line-container'>
126
+ <input type='text' value={candidateSentence3} onChange={e => setCandidateSentence3(e.target.value)}></input>
127
+ <p className='output-score'>{output_3}</p>
128
+ </div>
129
+ </div>
130
+ </div>
131
+ </div>
132
+
133
+ <button disabled={disabled} onClick={compute}>Compute</button>
134
+
135
+ <div className='progress-bars-container'>
136
+ {ready === false && (
137
+ <label>Loading models... (this only runs once)</label>
138
+ )}
139
+ {progressItems.map(data => (
140
+ <div key={data.file}>
141
+ <Progress text={data.file} percentage={data.progress}/>
142
+ </div>
143
+ ))}
144
+ </div>
145
+
146
+ </>
147
+ )
148
+ }
149
+
150
+ export default App
src/assets/react.svg ADDED
src/components/progress.jsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ export default function Progress({ text, percentage }) {
2
+ percentage = percentage ?? 0;
3
+ return (
4
+ <div className="progress-container">
5
+ <div className='progress-bar' style={{ 'width': `${percentage}%` }}>
6
+ {text} ({`${percentage.toFixed(2)}%`})
7
+ </div>
8
+ </div>
9
+ );
10
+ }
src/index.css ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
3
+ line-height: 1.5;
4
+ font-weight: 400;
5
+
6
+ color-scheme: light dark;
7
+ color: rgba(255, 255, 255, 0.87);
8
+ background-color: #242424;
9
+
10
+ font-synthesis: none;
11
+ text-rendering: optimizeLegibility;
12
+ -webkit-font-smoothing: antialiased;
13
+ -moz-osx-font-smoothing: grayscale;
14
+ -webkit-text-size-adjust: 100%;
15
+ }
16
+
17
+ a {
18
+ font-weight: 500;
19
+ color: #646cff;
20
+ text-decoration: inherit;
21
+ }
22
+ a:hover {
23
+ color: #535bf2;
24
+ }
25
+
26
+ body {
27
+ margin: 0;
28
+ display: flex;
29
+ place-items: center;
30
+ min-width: 320px;
31
+ min-height: 100vh;
32
+ }
33
+
34
+ h1 {
35
+ font-size: 3.2em;
36
+ line-height: 1.1;
37
+ }
38
+
39
+ h1,
40
+ h2 {
41
+ margin: 8px;
42
+ }
43
+
44
+ textarea {
45
+ font-size: 1.5em;
46
+ padding: 0.6em;
47
+ }
48
+
49
+ input {
50
+ font-size: 1.2em;
51
+ padding: 0.5em;
52
+ }
53
+
54
+ button {
55
+ border-radius: 8px;
56
+ border: 1px solid transparent;
57
+ padding: 0.6em 1.2em;
58
+ font-size: 1em;
59
+ font-weight: 500;
60
+ font-family: inherit;
61
+ background-color: #1a1a1a;
62
+ cursor: pointer;
63
+ transition: border-color 0.25s;
64
+ }
65
+ button:hover {
66
+ border-color: #646cff;
67
+ }
68
+ button:focus,
69
+ button:focus-visible {
70
+ outline: 4px auto -webkit-focus-ring-color;
71
+ }
72
+ button[disabled] {
73
+ cursor: not-allowed;
74
+ }
75
+
76
+ @media (prefers-color-scheme: light) {
77
+ :root {
78
+ color: #213547;
79
+ background-color: #ffffff;
80
+ }
81
+ a:hover {
82
+ color: #747bff;
83
+ }
84
+ button {
85
+ background-color: #f9f9f9;
86
+ }
87
+ }
src/main.jsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react'
2
+ import ReactDOM from 'react-dom/client'
3
+ import App from './App.jsx'
4
+ import './index.css'
5
+
6
+ ReactDOM.createRoot(document.getElementById('root')).render(
7
+ <React.StrictMode>
8
+ <App />
9
+ </React.StrictMode>,
10
+ )
src/worker.js ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { pipeline, env } from '@xenova/transformers';
2
+
3
+ // Specify a custom location for models (defaults to '/models/').
4
+ env.localModelPath = '/models/';
5
+
6
+ // Disable the loading of remote models from the Hugging Face Hub:
7
+ env.allowRemoteModels = false;
8
+
9
+ // cache the model in the .cache directory in the current working directory
10
+ env.cacheDir = './.cache';
11
+
12
+ // Use the Singleton pattern to enable lazy construction of the pipeline.
13
+ class SemanticSimilarityPipeline {
14
+ static task = 'feature-extraction'; // follow model's task
15
+ static model = 'Xenova/multi-qa-MiniLM-L6-cos-v1'; //https://huggingface.co/Xenova/multi-qa-MiniLM-L6-cos-v1
16
+ static instance = null;
17
+
18
+ static async getInstance(progress_callback = null) {
19
+ if (this.instance === null) {
20
+ this.instance = pipeline(this.task, this.model, { progress_callback });
21
+ }
22
+
23
+ return this.instance;
24
+ }
25
+ }
26
+
27
+ function dotProduct(a, b) {
28
+ if (a.length !== b.length) {
29
+ throw new Error('Both arguments must have the same length');
30
+ }
31
+ let result = 0;
32
+ for (let i = 0; i < a.length; i++) {
33
+ result += a[i] * b[i]
34
+ }
35
+ return result.toFixed(3);
36
+ }
37
+
38
+ // Listen for messages from the main thread
39
+ self.addEventListener('message', async (event) => {
40
+ // Retrieve the similarity pipeline. When called for the first time,
41
+ // this will load the pipeline and save it for future use.
42
+ let extractor = await SemanticSimilarityPipeline.getInstance(x => {
43
+ // We also add a progress callback to the pipeline so that we can track model loading.
44
+ self.postMessage(x);
45
+ });
46
+ /* let extractor = await pipeline('feature-extraction', 'Xenova/multi-qa-MiniLM-L6-cos-v1', {
47
+ progress_callback: x => {
48
+ self.postMessage(x);
49
+ }
50
+ }); */
51
+
52
+ // Actually compute the similarity
53
+ let ref_embeddings = await extractor(event.data.refsent, {
54
+ pooling: 'mean',
55
+ normalize: true
56
+ });
57
+ console.log(ref_embeddings);
58
+ let cand1_embeddings = await extractor(event.data.cand1, {
59
+ pooling: 'mean',
60
+ normalize: true
61
+ });
62
+ let cand2_embeddings = await extractor(event.data.cand2, {
63
+ pooling: 'mean',
64
+ normalize: true
65
+ });
66
+ let cand3_embeddings = await extractor(event.data.cand3, {
67
+ pooling: 'mean',
68
+ normalize: true
69
+ });
70
+ let output_1 = dotProduct(ref_embeddings.data, cand1_embeddings.data);
71
+ let output_2 = dotProduct(ref_embeddings.data, cand2_embeddings.data);
72
+ let output_3 = dotProduct(ref_embeddings.data, cand3_embeddings.data);
73
+
74
+
75
+ // Send the output back to the main thread
76
+ self.postMessage({
77
+ status: 'update_1',
78
+ output: output_1
79
+ });
80
+ self.postMessage({
81
+ status: 'update_2',
82
+ output: output_2
83
+ });
84
+ self.postMessage({
85
+ status: 'update_3',
86
+ output: output_3
87
+ });
88
+ self.postMessage({
89
+ status: 'complete',
90
+ });
91
+ });
92
+
93
+ /**
94
+ * This JavaScript code is essentially using a machine learning model to perform semantic similarity tasks on some
95
+ * input data. It uses the library "@xenova/transformers" which is a JavaScript implementation similar to Hugging
96
+ * Face Transformers. It uses a specific model from Hugging Face ('Xenova/multi-qa-MiniLM-L6-cos-v1') to perform the
97
+ * task.
98
+
99
+ Here is a breakdown of the script:
100
+
101
+ 1. **SemanticSimilarityPipeline class**: This is a singleton class, meaning it restricts the instantiation of a class
102
+ to a single instance. It will create an instance of the pipeline using the specified task and model only if an
103
+ instance does not already exist. This is done through the `getInstance` method. This method takes an optional
104
+ progress_callback function, which would be called to report the progress of model loading.
105
+
106
+ 2. **The `message` event listener**: This is listening for messages sent from the main thread. The event object it
107
+ receives contains the data for computing the semantic similarity. When a message is received, it retrieves the
108
+ singleton instance of the SemanticSimilarityPipeline, performs the computation, and then sends the result back to
109
+ the main thread using `postMessage`.
110
+
111
+ 3. **The `extractor` function call**: This uses the instance of the SemanticSimilarityPipeline to compute semantic
112
+ similarity. It takes two arguments. The first one is a reference sentence (`event.data.refsent`). The second argument
113
+ is an object that includes the candidate sentences (`cand1`, `cand2`, and `cand3`) and a callback function. This
114
+ callback function is called after computing the similarity for each candidate sentence and it sends partial output
115
+ back to the main thread.
116
+
117
+ 4. **Sending result back to the main thread**: After the semantic similarity computation is complete, the output is
118
+ sent back to the main thread using `postMessage`.
119
+
120
+ */
vite.config.js ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+
4
+ // https://vitejs.dev/config/
5
+ export default defineConfig({
6
+ plugins: [react()],
7
+ })